MMInputFamily driver architecture

This page is about how all the bits of an MMInputFamily driver fit together. It is written from the point of view of its relationship to Mac OS X IOKit concepts and structures. I'll use the example of a PCI card. It'll be discussed in the order that the driver is built by Mac OS X from its various parts. It assumes familiarity with the basic concepts of IOKit drivers (e.g. personality dicts, lifecycle).

PCI interface chip driver

The first driver to be called into action is the PCI interface chip driver. This drives the chip on the DVB card that interfaces with the PCI bus. All PCI cards will have one of these chips. Sometimes, that chip is specialised and does the whole job of the card. Other times, it is just the first of a number of chips, all of which may be controlled by drivers. In DVB cards it is practically always the latter case.

In Mac OS X, drivers are matched against PCI cards by comparing the vendor and device ids declared by the card with the properties specified in the driver's personality. From the card's point of view, these ids are found in the configuration space of the device, which is usually small and part of the PCI spec. There are also subsystem vendor and subsystem device ids found in this configuration space. The primary ids tell about the PCI interface chip itself. The subsystem ids tell about the rest of the card that is hooked up to it. PCI interface chips that are intended for many applications usually have these subsystem ids. Most DVB cards have them.

Since we have funky inheritance and matching in Mac OS X, our PCI interface chip driver will be using only the primary vendor and device ids to do its matching. This is done with an 'IOPCIPrimaryMatch' in the driver's personality dict. Only code generic to this chip must go into this driver - code for a particular application must be left out (unless provided as optional library code or some such). The driver must be publicly available so that it can be extended and more features of the chip exposed, in case the same chip is used in an application you have not (yet) coded for. The driver's 'IOProviderClass' would be 'IOPCIDevice', and it would most liekly derive from the plain-old IOService class.

The chips used in DVB cards usually do two things: 1: Provide a bus to the other chips on the card. 2: Provide a DMA engine to transfer a data stream across the PCI bus.

First the bus. Usually we are talking about an I²C bus. This is a popular simple two-wire IC interconnection bus. It's the way you get access to the other chips on the card. An I²C bus has up to 127 addresses, and supports two operations being read and write. It has a master controlling the bus (for us the PCI interface chip). It works in units of 8-bit bytes, at a number of (usually selectable) quite high frequencies. The first byte in a transaction determines the target address (high 7 bits) as well as the operation (low 1 bit: write = 0, read = 1). After that for a write there are a variable number of bytes written out, and for a read there are a variable number of bytes read in. The number of bytes to read is usually determines by the master - it just stops reading when it's read enough - so this generally means drivers must tell it how many bytes to read. Most PCI interface chips provide an interface that controls the I²C bus one byte at a time. This is know as 'hardware I²C'. Some however just provide the two wires of the bus and leave the implementation of the I²C protocol up to software (you could of course implement any other protocol with them if you wished). In linux there are 'bit-banging' drivers to operate the bus one bit at a time via these lines. I haven't written any Mac OS X drivers like this yet as every chip has had hardware I²C support.

Many chips on I²C buses add a 'register' protocol layer above the bus protocol. In fact, all but the most basic I²C chips use this layer to expose a set of registers and/or commands. Th way it works is that the first byte in a write transaction is the index of the register to use. This is sometimes also known as a subaddress (the first address being the I²C address of the chip itself). To write to that register, the register index is followed by the new contents of the register. To read from that register, a one-byte write transaction is performed to set the index, and then a read transaction is performed to read its contents. Registers are usually one byte big. Most chips also support automatic incrementing of the subaddress, so if you write two bytes after the subaddress, then the second one will go into the following register. Same deal with reading. This is often useful where two adjacent 8-bit registers form a 16-bit value.

There are lots of goodies in MMInputFamily to help you implement an I²C bus, and even more goodies for users of the bus as we will see below. The first class to mention is I2CPipe. It is a class that abstracts an I²C bus, and provides queueing, automatic locking, scanning and other cool stuff. The interface to this class that you have to implement consists of the following:

bool reset( uint32 frequency = 0 ) [reset/clear the pipe for the next transaction. at the given freq. 0 means same as last time]
uint32 status() [current error status of pipe. see i2cStatus_ enums. hopefully a reset would clear them]
int maxDataLength() [max length of data in a transaction on the pipe]
int flushAttempt( int opTimeoutInMS ) [try to perform the transactions in messages_ ... do not clear messages_ when done!]
Other methods you may wish to override:
void flushCommence()
int flushConclude( int attemptRet )
[setup/teardown of a transaction ... e.g. if you have lots of setup
to do and don't want to redo it for each attempt]
--
int flush( uint32 frequency = 0, int maxRetries = 2, int opTimeoutInMS = 100 )
[override whole flush operation ... not recommended as you'd lose locking stuff]
--
void append( uint8 addr, bool readNotWrite, void * dataptr, int datalen )
[override default operation append function ... shouldn't have to do this and would lose locking stuff]
If suppressErrors_ is on then don't complain about errors (don't IOLog stuff to the system log) as the user isn't interested. This is done when scanning the I²C bus as errors are expected there.

Your driver should create an object that derives from I2CPipe as part of your driver. Note that I2CPipe does not derive from IOService - you can have as many I2CPipes as you like in your driver.

The next class is I2CNub. For each address on the I²C bus that a chip is at, you should create an I2CNub and register and attach it to yourself so that the Mac OS X matching process can find the appropriate drivers and attach them to the nubs. This is where all the IOKit stuff really starts to work. There are helper functions in I2CPipe to do this for you: scanBusGlue and stopBusGlue. Store your nubs in a vector that is a member of your class, and pass the vector into those functions to look for new nubs and to stop all existing ones. You must also implement the scanOne( uint8 addr ) virtual method to ping an address as best you can. Usually seeing if there is an error on a zero-byte write is the best way to scan an address, but sometimes the bus cannot do that operation. This will depend on your chip. The user interface to I2CNub is pretty powerful and is described below.

As for the DMA engine of the PCI interface bus, this is generally how it works: setup page table. map page into into DART with 'prepare' on an IOMemoryDescriptor. map RAM into DART in the same way. Tell the chip the physical (PCI) address of the page table pointer. Try to avoid using contiguous memory as the kernel sucks at allocating that. Usually DMA engines are good enough that you can avoid it. Nothing special in MMInputFamily to help you with any of this.

Card driver

The next driver to enter the scene is a card driver. This driver matches to the PCI interface chip driver for one specific hardware board. There are likely to be many different card drivers for the same PCI interface chip, for the different configurations in which the chip is employed.

This is the driver that implements the MMInput interface. In its personality dict, its 'IOProviderClass' is set to the class of the PCI interface chip driver, and the C++ class itself derives from the MMInput class. If you are writing a driver for a specialised PCI interface chip (or a specialised USB interface chip) then this driver and the driver mentioned above would be one and the same. There would not be this second level of matching - the IOProviderClass would be IOPCIDevice (or IOUSBInterface) and the C++ class would derive from MMInput. Anyway, the example will continue assuming they are separate.

The purpose of this driver is to expose the DVB functionality of the card to the system as a whole. Whenever a class that derives from MMInput is instantiated (and after it has called registerService), userspace programs that use MMInputLib can see the class and attach a user client to it, and then call the various MMInput methods through that interface. I'm not going to go into the details on how the user client and the user/kernel interface stuff works. All you need to know to implement a new MMInput driver is that you have to implement its virtual methods and then everything magically works :-)

The MMInput interface consists of the following virtual methods:

UInt32 identifier() const [implemented in base class]

MMInput * activate( DataPond & pond )
void deactivate()

TuningParams tuningSpace() const
bool tune( OSDictionary * params ) [see end of MMInput.hpp for expected keys]

DataKind dataKind() const
bool fill( bool go )

void situation( UInt32 & progress, UInt32 & strength, UInt32 & quality )
void statistics( UInt32 & blobsTotal, UInt32 & blobsErrors )
See the doxygen in MMInput.hpp for all the details on this interface.

DataPonds are a storage place for data. Effectively just a big circular buffer. User can find out where data is up to in the pond and read it out. Whole pond mapped into userspace. Up to user to keep up with data rate. level is ideally a PCI register which can be turned into a pond offset via the various 'level...' values.

Use DVBFrontend to find all the DVB I²C drivers and pass tuning/sitn/stats requests onto them. Handle the fill request yourself. Find I²C drivers by calling DVBFrontend::checkClient on each entry in the vector of nubs maintained by the PCI interface chip driver.

Demodulator driver

So that's the higher levels of the DVB driver covered. Now we'll delve back into the low levels and discuss the other chips on the board that the I²C bus provides access to.

The first of these is a demodulator. This is a chip that digitises the baseband DVB signal and produces and MPEG-2 transport stream out the other end. Usually it is a glorified DSP with a few dozen registers accessible through its I²C interface. These registers need to be programmed for the particulars of the stream that is being tuned to.

In the MMInputFamily architecture, a demodulator driver inherits from DVBFrontend, and matches to an I2CNub at some address. These chips are usually found on the same addresses, so an 'IOI2CAddressMatch' property should be set in the personality dict to that address. That is the first stage of I²C driver matching. It is wise to also include a 'probe' function to check that the chip really is what it seems to be. If the demod chip has a chip id register, then read that and check that it is as expected. Otherwise read some other register and see if the expected default value matches up. It is pretty dangerous to match just on I²C address alone. Another technique is to check the class of the I2CNub's provider, or even to check its vendor/device ids.

The driver must then implement the virtual methods in DVBFrontend. This is what they look like:

bool activate() [turn it on, upload firmware, whatever]
void deactivate() [turn it off]
bool tune( OSDictionary * params ) [look for params you know and setup registers to handle them]
void situation( UInt32 & progress, UInt32 & strength, UInt32 & quality ) [only fill it in if you can]
void statistics( UInt32 & blobsTotal, UInt32 & blobsErrors ) [only fill i in if you can]

It is recommended that the driver do as little as possible in its IOService 'start' method and instead do that in its DVBFrontend 'activate' method. Putting the chip to sleep or into some other low power mode in 'start' (as well as 'deactivate' of course) is a good idea.

There are many cool things that I2CNub can do for you to make accessing your chip over the I²C bus easy. I2CRegister with oeprator[] on the nub. Making a regs layout struct so you can use it with all the niceties of C++. See examples for how to do this.

Tuner driver

Dumb I²C device. Usually programmed with 4 bytes and no subaddresses. Often not read from at all. Frequently needs a passthrough from the demodulator in order to be seen on the I²C bus. Demod driver should enable passthrough and tell bus to rescan in its 'start' method so that chip can be found. Demod should then respond to kIOI2CPowerDependents{Up|Down} messages.

Tuner driver should also derive from DVBFrontend, and in general only worry about 'tune'. Generally first 2 bytes are freq to tune to - divide desired freq by clock/step freq (usually 50-200 kHz) after subtracting 'IF' frequency (usually 30-40 MHz). Bottom bytes are band select freq. May need to cache 'bandwidth' props from earlier tune requests as it's not guaranteed to be there if the user doesn't want it to be changed.

Power management

The theory: should work
The reality: often doesn't

Ok that's it. Let me know if you have any questions.