Hardware • 1:03:23
This session covers generic PCI device drivers for Mac OS X and includes information on Mac OS X I/O Kit, Fcode, and traditional ndrv drivers. Packaging and loading of drivers under Mac OS X will also be discussed.
Speakers: Mark Tozer-Vilchez, Wayne Flansburg, Josh de Cesare, Steve Martin, Jason Seng
Unlisted on Apple Developer site
Transcript
This transcript was generated using Whisper, it has known transcription errors. We are working on an improved version.
Good morning. Welcome to session 205, I/O Kit: PCI Drivers & Open Firmware. My name is Mark Tozer. I'm Desktop Technology Manager. Today we're going to go over a couple areas here with I/O Kit, specifically with the PCI driver development and assistance in also determining how to get into open firmware, what to look at open firmware. We'll actually have some demos of looking at a couple of PCI cards and actually looking at the open firmware code there. So today we'll start off the morning with Wayne Flansburg who will give this presentation.
Good morning. Let's begin. We think the session audience should be people that are interested in generic PCI cards and hardware developers. And we think the audience also might include people that are writing drivers for that and hardware testers and just people that are generally interested in low level PCI stuff and booting.
So what is a generic PCI device? Well, it doesn't belong to a framework. That's the biggest thing is you don't have a FireWire framework or a USB framework or any framework at all except I/O service, which is quite a lot. Your device is unique to your application. Nobody's going to try to install their driver or run their application onto your device.
Your device could be a sonar device, a radar device, something that isn't Mark Tozer-Vilchez, Wayne Flansburg, Josh de Cesare, Steve Martin, Jason Seng So the session agenda is basically we're going to look at OpenFirm, we're going to boot a device. We're going to boot one computer. We're going to stop in the OpenFirm or user interface.
And we're going to look at some devices and we're going to look at some support nodes that are important to you. And then we're going to go and look at the same stuff over again, but this time using the I/O registry. And then we're going to talk about a generic PCI driver and finally we'll have a Q&A session.
[Transcript missing]
So we're going to start with the open firmware. The device tree, we're going to look at nodes in there. We're going to look at configuration variables. Configuration variables have some very important aspects for you that we'd like to talk to you about today. And we're going to show you how to, from open firmware user interface, using your reg properties, how to actually read your configuration space.
And we're doing this because we want you to be able to get a new computer, plug your device in there, and make sure before you do anything that your device is recognized and built correctly into the open firmware device tree. And we're going to mention something about security.
So the device tree is a block diagram of the motherboard, but it's more. As I mentioned earlier, it has the support nodes in it, but it also, OpenFirmware, during its building of the device tree, goes out and looks at all the PCI slots, and if it finds a card in there, builds the node for you or allows you to build it. And in there you have your properties, your words, and if you have children, they'll also show up in there. Your words are only present, they're optional, if you have a device that is participating in the boot process, such as an Ethernet device or display device.
Mark Tozer-Violchez, Wayne Flansburg, Josh de Cesare, Steve Martin, Jason Seng this is a block diagram of the target machine we have up here. This is one of our latest desktop machines, and we're not going to boot it up entirely, but when you buy this machine, you can get the developer note for that machine off of our web page, and in that developer note, you can look at this block diagram.
The reason this is important to you is many people will come to DTS and say, with this last machine, my device stopped working. What did you change? Well, you don't really have to ask us. We're going to try to teach you enough so you can get a reasonably quick look with knowing very little about open firmware and everything else so that you can see what may have changed and start from there. So we're hoping to give you some leverage there.
Now we're going to display your node. We're using Telnet to do that. Last year we used Telnet. This year we-- or last year we didn't use Telnet. This year we're using Telnet and we're going to show you your whole tree, including your node, from the Mac OS X host system that is connected up to it. And like I said earlier, we'll display your words, properties, and children, if you have children and if you have words, but you definitely have properties.
This is from the PCI bus binding to IEEE 1275. It's just one section that we took out, but it's extremely important. I'd like to talk about that just for a moment. This is difficult to read, so I'm not going to go into it in great detail, but what you see here is the bits.
From 00 to 31, and you see FIS high, FIS mid, and FIS low. FIS high is really very, very important to you because it is how all the bridge controllers route messages from your virtual memory space on the host bus down through the hierarchy of ASICs on your bus.
Forgetting this N, P, and T thing that are there for a lot of legacy devices. You have a space code, which is either configuration space, memory space, or I/O space. But you also have 8 bits for your bus. So you can have 256 buses, and you can have 32 devices on a bus, and you can have 8 functions per device, and you can have 256 registers per device. So how do we get that message down to you? We set it down to you in FIS high.
And we put that out onto the PCI bus, and the controllers know enough to recognize that this message that's coming down, and they got one clock period to do this, 30 nanoseconds today I guess. They got one clock period to say that this is not for me, or this is for me, or it's for my children. And if it's for my children, it changes the type of message and sends it down through the bridge to the network.
So that's how we get there. And then FIS mid and FIS low are the 64-bit addresses. Presently, we only have a 32-bit bus, but the standard allows for going to 64 bits in the future. So today you'll see that all the H's, the FIS mid, is always 0.
and now I'm going to ask Steve Martin to come up and demo using Telnet in a host computer What we've done here is we've put a machine in Open Firmware already. Open Firmware, we're at a pretty low level. We don't really have the ability to cut and paste or scroll back up.
And we don't have much of a way to save any of our data. What we've done with this machine is we've used the Telnet app on 10 in the terminal program to link back in to the Open Firmware machine to give us an interface that allows us to save and capture our data. So what I'm going to do here is what Wayne was showing on the slide above. I'm going to select the root node of the device that we're looking at.
and that's the dev is the word that select that used is used to select our device and I'll list that and what you can see here is that this is roughly a representation in text of the same hardware block diagram that Wayne was showing before. We have children, those are the indented text and we'll go down here to where is it right about.
Here is our PCI bridge and we have two cards on this machine and they show up right here and right here. We'll select that one.
[Transcript missing]
Note here that we have the vendor ID, the device ID, and the revision ID. Are we going to go to the slide that shows the... No, I don't think so.
Mark Tozer- Okay. Well, I want you to note those and we'll go... Use a word that is actually, we've got a Q&A up on it, it's QA 1036 called Cregs, that will put this in a format that you'd be used to seeing in a configuration register dump from your card. And we'll use Fizz High, this is Fizz High right here under the Reg node that Wayne was talking about. So I'll use that fishigh to start this word as an input for this word. That's one. That's .crigs.
There you go. And you noticed here in the block diagram up above that the vendor ID and the device ID are the same as the first two entries here. This is in a format that comes right out of the PCI spec. and Wayne, you want to? Yeah, let's go back to this machine, which is two.
Let's go to the next slide. This is the PCI configuration space. Now that demo we just showed you, it's difficult to show a demo like this to an audience for the first time, but we do have the demo replicated, as Steve said, on the site so you can stop on your own and look at it. You as a PCI developer must supply a PCI configuration space of 64 headers. The blue ones are mandatory.
[Transcript missing]
The end input parameter on the stack for .cregs. And if you look at this before you go any further, if you just get into the Open Firmware user interface and go down and do this kind of stuff, and you find out that This doesn't look like I thought it would look.
Don't boot. Don't go any further. The damage is already done. And it's very important that you know how to see that your device tree was built correctly because OS X or any client of open firmware believes that the device tree is correct. And if it isn't, then the game's over.
So let's look at some of the support nodes that are going to-- oh, wrong thing. Some of the support nodes. I'm going to have Steve, we're bouncing back and forth because you've got to talk about this stuff and then show the example. Steve in a moment is going to do printenv. For the Unix people out there you've probably noticed that dev and slash and ls and now printenv looks a lot like Unix stuff.
and we're going to look at those words that word and we're going to look at three of the Mark Tozer-Violchez, Wayne Flansburg, Josh de Cesare, Steve Martin, Jason Seng Is that what you wanted? Yes. What got printed here was all the configuration variables. These are the open firmware configuration variables and they're mentioned in IEEE 1275 and in that PCI binding.
And you'll see things that are probably important to you. Some of these are. One of the devices, there's two columns here. One column, the far column, is what's stored in boot ROM, factory defaults. The other column is current. So if you were in OS X and you were in the terminal app and you started setting some configuration variables and you wanted to see that those were actually set, you could shut the machine off, boot back into open firmware, and type printenv.
Print ENV and you can see what the current values are and in fact if you set it correctly. We're going to be looking at boot device later with the boot picker because that's also a very important device. It's the only one that's off scale over there and the real important thing is that that comma separates one from the other and it's a very important device.
and we'll tell you more about that later. One other one that's important too, a lot of people want to know about what is and what isn't in NVRAM. NVRAM is in here and because on this machine it doesn't show anything, there isn't anything in NVRAM. And even if we were to put something in NVRAM right now, that script would not be used unless you set the use NVRAM RC question mark variable to turn it on. So let's look at the aliases node.
The importance here is that open firmware has to be able to move down a path. And the path, when it looks at a string that you give it, it's either going to start with a slash or it isn't. If it starts with a slash, it's an absolute path.
But you can see some of these paths are terrible. And if we stood up here and tried to type, we'd die of old age before we maybe got the right one. So these aliases are very important. But there's a caveat with aliases. And that is that the aliases on one machine might not be the same aliases on the other machine.
And although you can create your own, and we expect you would if you were doing a lot of booting and stuff like that, you need to check this. So devalias is another command. And all of this stuff, we're not giving you anything that we haven't put out on the internet already. So there's two other nodes that are very important. And that's the chosen and the options node. But let's look at the options node first.
One of the things you'll notice on your own spare time is what we did was we dev down to the options node and then we listed the properties. But when we said printenv, we saw these properties. So printenv is a nice way to print the environmental variables, but they're really configuration variables and they live in the options node.
And you need to know that because people don't. And it's very important and you can very quickly see what are your configuration variables. The other node that's of importance is the chosen node. You wanted us to do something? When we're done, we put stuff that we did for you in the chosen node.
The Mac Address. Every Ethernet card has to have a Mac address. So you stuff your machine with Mark Tozer-Violchez, Wayne Flansburg, Josh de Cesare, Steve Martin, Jason Seng This one. I don't know which one that points to, but at least that's the one. This machine has never been set up entirely, but your boot path and your boot arguments, if you had them, would have been there. So those three nodes, the The choices node, the chosen node, and the options node are all very important to you.
[Transcript missing]
When Open Firmware boots up, if you're holding down the command option PR keys for parameter RAM, which would set those defaults back that I showed you for the configuration variables, Open Firmware first checks to see if the amount of memory you have in the machine is different than when it's shut down, i.e., you had three sticks when you shut down and now you only have two sticks, it will unset the security for you. The good news is that it comes benign. If you don't know how to set it, you can use the machine forever and it will never bite you. It will always just sit there and do nothing. So that's a good feature of it.
[Transcript missing]
If you have a number of SCSI devices out there, and if there are SCSI people here, I have to ask you afterwards to come up and see me. And I want to get your business card and give you my business card because Paul's group has a request of the SCSI.
They want a new feature placed into the SCSI drivers. Whenever Open Firmware goes out, it doesn't know it's connected to SCSI. So if you have a bunch of SCSI devices connected up, it's going to have to go, "Are you there?" 1, 2, 3, 4, 5, 6, 7, oh, nobody's there.
"Are you there?" And it's got to do it a number of times, and that adds considerable to the amount of time to boot. Basically, the firmware goes off and looks at the boot device configuration variable, and it goes out there and tries to use it first. And also, since Open Firmware is not connected to SCSI, it's going to have to do it a number of times.
And that adds considerable to the amount of time to boot. Basically, the firmware goes off and looks at the boot device configuration variable, and it goes out there and tries to use it first. And so it's not using interrupts. Although Command Period, I think, will tell the boot picker to stop picking, looking for boot devices, Command Period won't be honored instantly, because it might be out waiting for a SCSI device. But when it's done, it comes back to see if there were some key presses. So basically, the boot picker displays the Rescan and the Boot buttons and searches the device tree for all the bootable volume. volume.
Very interesting address. This is a hybrid syntax in this. Remember the slash said it was an absolute address. That's the PCI. That's the node name. The @8k is the unit address. That's in 1275. I think it's in section 3.2.1.1. This slash says now I'm going down to the Mac I/O chip, which is the chip that goes out and has all the ATA drives and everything else. And it's at that unit address. Then it goes down to this ATI drive at that unit address and comes all the way down here.
And then, This part of it here, notice you have forward slashes and back slashes from here on out. Mark Tozer-Violchez, Wayne Flansburg, Josh de Cesare, Steve Martin, Jason Seng
[Transcript missing]
and he's going to basically show you the The I/O registry and its planes. This is another very, very important aspect.
The device tree in OS 9 mapped pretty close to the name registry, but the I/O registry is much more. Mark Tozer-Violchez, Wayne Flansburg, Josh de Cesare, Steve Martin, Jason Seng The I/O registry and its planes. This is another very, very important aspect. The device tree in OS 9 mapped pretty close to the name registry, but the I/O registry is much more.
[Transcript missing]
Which options did you want to highlight? One of the things I needed to learn, yes, one of the things I needed to learn was objects and classes. Who supported whom and where did you get your providers from? So let me switch back over to my machine for a second. And you want to determine who your provider is, and you want to determine your class, and you want it to read some variables.
And you're inheriting, because you don't have a framework of particular importance like FireWire or something, you're inheriting from I/O Service. But you can be provided by I/O PCI device, AGP device, PCI Bridge, and Cardbus device. And for all the PCI knowledgeable people in the audience, those are all PCI devices.
So show them the I/O Ridge with a minus B so we can see the BOL properties and a minus P so we see the I/O device tree plane. No need to worry if you're not a C++ expert. I'm not. I was immediately able to find the objects in the tree because they supplied me with the -b option.
And I'm also seeing who's providing for me. That's a big, big help. All we want to do is get you started because we know that most of the questions that you're asking right now is, "How do I get started?" Once you've got all the information, we get talented PCI devices all the time.
Jason, show them I/O PCI device.h. And then that'll be it. We'll go into the next part of the session. And the headers you'll be interested in are the IOPCIDevice.h, IOPCIDbridge, IOAGPDevice, and the Cardbus headers are in the kernel framework. Show them the, show them that header file, iopcidevice.h, the bottom header file, right there. Go down to the FISHI strut. There it is, right there. Get that strut right there. Remember how I showed you? Fizz High.
Look at it. Nine never had this. Ten has it. There it is. And it's in Little Indian and Big Indian. You don't even have to get out of bed anymore. I was told not to be talking like that, but I guess I won that one. Well that's it. I'm not going to talk anymore and Jason and I are going to get off the stage. And we're going to turn the podium over to Josh.
Good morning, everyone. My name is Josh de Cesare. I work in the Mac OS X CoreOS group on various things, but primarily I've been working on platform architecture and various aspects of how I/O Kit interfaces with the rest of the kernel. And today I'm going to talk to you about how you can write a lot of the things that you would use to write generic PCI drivers.
So, in writing a generic PCI driver, you're going to have to pay attention to several things, including how your driver gets matched in the system, both active and passive matching, how you initialize your driver, how your driver would process events, also how you would create a user client so that you can handle requests from various sources.
Now, also, since you're not part of one of the built-in frameworks that we provide, you're going to have to design the interface between your application and your driver. So we need to talk a little bit about how that's done. Additionally, we also want to tell you a little bit about how to deliver your driver so that it'll be there when you need it.
First off, we're going to talk about passive matching. So in your driver, there's going to be an Info.plist file that contains all the properties that describe your driver to the rest of the system. One of those properties in there is the I/O Kit personalities. You'll have one personality for each way your driver works.
And all the personalities have some similar properties. For matching, the important properties are things like the I/O Provider class, which tells the system what kind of device you're interested in being attached to. In the case of most of your drivers, you're going to be interested in having your I/O provider class be I/O PCI device. Tell the system that you're only interested in PCI devices. You're not interested in getting attached to USB devices or block devices, just the PCI devices.
I/O Name Match will allow you to have your driver matched against the name, compatible, or model properties that OpenFirmware or the Fcode for your device left in the device tree. So if you know that your device in the device tree is going to be called "My Company, My Device," you can use the I/O Name Match to do that.
Additionally, the different provider types can also provide their own kinds of matching, and IOPCI family does that as well. It has several kinds of matching. There's IOPCI match, which lets you match on some combination of the device and vendor IDs. There's also a primary and secondary match that allows you to match exclusively against either the sub-vendor and sub-device IDs or the regular vendor and device IDs.
There's also an IOPCI class matching key which allow you to match against certain classes of device. So if you were writing a USB something or other, or USB host controller, it would have a class code and you could use that to do that. But mostly what you guys are going to be using is either the IOPCI match to use the vendor and device ID, or name match if you have Fcode that puts a particular name on your device.
There are also other possible matching keys and you can find those in iokitkeys.h. The other major property you're going to be interested in is IO Probescore. Probescore allows several phases of matching to be adjusted in which order the various drivers are going to get attempted to attach to various devices. You can use the probescore to say that your driver should be given preference over another.
In the case that your company has two devices that are very similar, maybe same device and vendor IDs, it might have other variances, it might have other properties, or various things going on. You could use IO Providerscore to help make sure that one driver wins against the other. Both would potentially still have the chance to run, which is what we're going to look at next in active matching.
So one thing to keep in mind about the passive matching is it's passive, meaning that your driver didn't get to run any code. Active matching is different. You will get, your driver will be loaded, it will be linked into the system, and it will be given the opportunity to run your probe method. Now, the thing you note at the bottom there is it's not normally needed. Most devices, or most drivers just don't need a probe method. You can do what you need to get your driver loaded using the passive matching.
So what does probe allow you to do? It allows you to access your device's hardware. But that also means that when you're done, you have to make sure that you leave your hardware in a safe state. You don't want to leave it such that it's going to be trying to interrupt the system or holding the bus locked or any other strange stuff. You have to make sure that you left it in a safe state when you're done because some other driver may also get the opportunity to probe.
You must free any resources you've used. If you allocate any large chunk of memory or attach to various other providers in the system or hold locks, you need to make sure you release all of those because some other driver could get probed after you get probed and it turns out they might win. So you want to make sure that you've left everything else okay.
Another thing you can do with Probe, and this is one of the more important things that Probe does for you, is it can allow you to bind multiple devices to a single driver instance. Most people don't need to do this, but there are certain multifunction PCI devices that the two functions serve different purposes. Say the first function is a video device, the second function is an audio device, but you want them handled by the same driver. So by returning one driver instance for both of the two Probes, you can get that bound.
And I've got an example of that that's in Darwin, and I'll talk a little bit more about where to find that later. Just want to reiterate again, probe is not normally needed. You can do almost everything you need to do just using property matching from the passive schemes.
So now that your driver has been matched and the system decides that you're the one that wants to try, you're going to get started. One thing to remember is that start can also be used as an even later version of probe. Since almost every driver will have a start routine, you also have the opportunity of returning false from your start routine to say that you're not actually interested. Again, sort of the same rules apply with probe. If you did allocate any resources, you need to make sure to let them go and make sure your hardware is in a safe state.
So we're going to focus here on a driver whose provider is an I/O PCI device. Now this I/O PCI device, the nub you get, provides you access to your hardware. It provides it in several forms. It provides a way to get mappings for memory space and I/O space. And these are done through a couple of these methods, the Get Device Memory with Index or with Register, also I/O Device Memory.
Now I've got a lot of these class names, property names, or method calls coming up. And we're going to try to make sure that these slides are available for you later so you don't have to copy them down. And these will just serve as sort of crib notes for later when you're trying to go through and figure out how to put it all together.
So for configuration cycles, as you saw, there's the Cregs demo earlier that lets you look at what your configuration registers are. You can read and write your configuration registers by using the config.read32, write32, and various other methods on your NUB.
[Transcript missing]
So once you've figured out how you want to map your device, you need to figure out how it's going to be used by the rest of the system. And we have several classes that will help you do this. In particular, these are the I/O Work Loop and the various I/O Event Source subclasses.
and the subclasses that are likely to be most useful for you are I/O Interrupt Event Source and I/O Filter Interrupt Event Source for event processing, Timer Event Source for timers, and I/O Command Gate is sort of a catch-all that lets you sync stuff up. There's also the I/O command pool. Sort of helps you manage resources for within your driver. And I/O's register service helps you make your driver available for the rest of the system.
So a little more on register service. So when you call register service on your driver instance, your driver will get it added to the matching system and the matching system will come through and try to find all the devices that might be interested in attaching to you. Now most of the time you're not actually going to have other drivers attaching to your driver because you're sort of the, you're what we call a leaf node in the service plane.
Mark Tozer-Vilchez, Wayne Flansburg, Josh de Cesare, Steve Martin, Jason Seng Also can be used to let other drivers in the system know that your driver is ready. You might have two different devices for your product and you want to make sure that one driver references the other or will wait for the other. So you can have the second driver do wait for service and that will come back or stop blocking as soon as the other driver has called register service.
So after your driver's been initialized, you want to be able to process events. So here's a little bit more about some of these classes that we provide for event processing. So the I/O work loop allows you to serialize access to all of your driver's resources. It's meant to simplify everything. Keeps you from having to have tons of little locks to lock this, lock that.
It also makes it easier to access your hardware. You don't have to worry that two threads, one maybe a client thread, maybe your thread, are accessing your hardware at the same time. As long as you use the various accessor methods through the various event sources or work loop, you will not have to worry about having two threads try to talk to your hardware at the same time. The work loop also provides a thread for various I/O event source actions. We'll talk about that a little bit more later.
and the work loop also supplies itself as a choke point when your clients need to tell you to stop, they're not ready to receive callbacks, or other things in the stack need to tell you to stop for a moment. So each of the event sources can be added to the work loop and any time an event source fires and says that it's got something to do, the work loop will serialize any other outstanding events to make sure only one of these is happening at any given time. and each of these event sources will execute an action. This action will normally be executed on whatever client thread generated the trigger, but they can also happen on the work loops thread, but only one at a time.
So, in a little bit more detail on the types of event sources you're going to be using, primarily if your PCI device is likely to have an interrupt, you'll want to have some way of getting at it easily in your driver. We do provide lower level interrupt services that will let you control, enable, and disable on your interrupt, register interrupt handlers, but we do suggest that you use these higher level constructs because they will simplify many things for you.
The two types of interrupt event sources we have are the basic interrupt event source, it's a proxy for the device interrupt, it makes it so you don't actually have to talk to the device interrupt, you'll get notified when things happen. It also handles behind the scenes enabling and disabling your interrupt as required.
And the action will get-- so when an interrupt occurs, this action will happen on a thread. You don't have to worry about running an interrupt context. So when this thing does happen, you can block if you have to. You can allocate memory if you have to. You can take locks.
You don't have to worry about running in a very restricted environment. The one thing to keep in mind with the I/O Interrupt Event Source is that if you've got a level-sensitive interrupt like a PCI device would, when your interrupt happens in the hardware, the low-level interrupt controller stuff will process this interrupt and cause a thread to get scheduled. In the meantime, your interrupt will be disabled.
So if other interrupts come in for your device, they won't be able to do anything. You won't get notified of them happening. Until your interrupt action finishes. So if you want to have the ability to take multiple interrupts at once from your device, or potentially if you're sharing an interrupt with several devices, if you've got a single PCI card that has a bridge on it and then several of your devices behind it, they'd be sharing the one physical interrupt.
So to make sure that one device can get an interrupt while the other one's processing an interrupted thread level, we provide this I/O Filter Interrupt Event Source. It has the same basic features as a regular I/O Interrupt Event Source, but it also allows you to supply a filter function that runs at the interrupt level. Now, in that this is a real interrupt handler, you cannot allocate memory, you cannot take locks, you cannot do anything that would otherwise block the system. Otherwise, the system will deadlock.
Now what this filter function does allow you to do is quickly interrogate your hardware to determine whether or not the interrupt is for you. Also it allows you to say process the interrupt. If you can process the interrupt very quickly you can do that and you may not even have to send this interrupt, cause the interrupt action to cause your work loop thread to go.
You can, in a lot of cases where you might want to do that is if you're implementing some sort of software DMA where you have a lot of high frequency interrupts coming in, you just want to quickly take a bite off of a FIFO and put it into a buffer somewhere. And maybe when your buffer is starting to approach full, then you might want to decide that you should cause your interrupt action to fire and let the higher level parts of your driver process the rest of the interrupt.
So there's also the I/O timer event source. You can use it in two ways. The first way is a lot easier. It's basically to schedule a timeout. Suppose a client issues a transaction in your driver and you want to make sure that after some number of milliseconds to seconds that a notification is sent back up to say that it just didn't complete.
You can, at the same time that you issue the transaction to your hardware, you can also set up one of these timer event sources. When the timer event source fires, it gives you an opportunity to see if the transaction is done or if it isn't done, you can set an error up.
Now one thing to keep track of is that these timer event sources are one shots. It won't get rearmed unless you explicitly rearm it. So if you're going to attempt to use one of these for a periodic timer, you need to, after every time it fires, you need to rearm it.
Another one is the I/O command gate. This isn't really like a lot of the others. It doesn't have an explicit action attached with it, and it doesn't really have a trigger. But it does have a way for you to execute any function as if it were being executed by your work loop.
Now it's not going to get shuttled over to the work loop thread. It'll execute on whatever thread you've called the function on, so it's efficient. But it will block if necessary in case the work loop is in the middle of doing something. This allows you, in many cases, to take an I/O request coming into your driver that could be happening on multiple processors or in many different ways, and schedule the function that actually causes the I/O to be put into your driver by saying run action command on your command gate, and it will get serialized appropriately.
The command pool is mostly a helper. It's not likely to be used by most of your drivers, but is there if you need it. Basically, it lets you allocate some number of commands. Each one of these commands would hopefully have the total amount of memory needed to process completely one of your commands. At the same time, you don't want to pre-allocate too much memory since every piece of memory you pre-allocate isn't going to be available for the rest of the system.
This is an important notion if your driver is somehow related to the paging path. Because if the paging system is trying to free up memory or put stuff on disk, there's no memory to be allocated, you can't be allocating memory in your driver while it's doing that. The I/O command pool can be told to block when there isn't anything available.
Also, it can be set up to just return you an error to say that there is no memory available at this time. And you can, if you wanted to, return an error or you could block on some other higher level construct in your driver. It's also synchronized with a work loop to make sure that things don't get out of sequence.
So now that you've got a basic notion of the classes that are used to process events in your driver, you need to figure out what sort of event or how you start getting these events to your driver. So there's a couple ways to do that. One of them is basically generic, sort of generically through property access. The other is through user clients.
Now the property access stuff is done through basically gets or sets on properties in your driver's dictionary. And this is a very easy thing to do. It's connectionless. There's no clients to think about. It's kind of high overhead because as you come from outside the kernel, it has to get wrapped through a serialized, it's actually a chunk of text. It comes into the kernel, it gets parsed again, and created into a dictionary. And consequently, it's also rather low bandwidth, but it's easy.
To make your driver participate in this, the two methods that you need to override are serialized properties. This allows you to find out when somebody is trying to get properties from you and potentially return different properties. Also, set properties allows you to take or accept a dictionary from user space and then add it dynamically into whatever dictionary.
Now, if you're doing a more complicated system, you're probably going to have to use an IO user client. They're somewhat more complicated, so we're going to be trying to provide some examples for you, give you a baseline of what to do. It's connection-based, so you'll always know who all of your clients are, you'll know if clients go away, and you'll also have everything you need to serialize clients to make sure that they're not trying to talk to you all at once. It's very low overhead, and it can be very high bandwidth.
It allows you to map memory from a user process into the kernel's address space if necessary. You could DMA into the buffers, you could copy memory around, you can do pretty much whatever you need. You can also export new API through to user space. And the major property that you need to be interested in for doing this is the GIO user client class key. Basically, you add a new property to your driver's dictionary that says the name of the class that should be instantiated when a user client request comes in.
So how do you access your application from, or your driver from an application? And this is done pretty similarly for Cocoa or Carbon applications. Ultimately, they would have to link against the I/O Kit framework. And how this linking is done is a little bit complicated depending on whether you're Cocoa or Carbon. If you saw the I/O Kit overview on Monday, there was some stuff where you had to have a CF plug-in and various other things in between to get back and forth between the Mac OS context and CFM context.
But it boils down to you link with the I/O Kit framework, it provides you all the APIs that you need to shuttle back and forth across the user kernel boundary. It also provides a set of matching functions that you can use to find your device. After register service has been called in your driver, these user space commands will be able to find your device.
Now if you're trying to use the get and set property interfaces to your driver, the two functions that you're going to be interested in using from user space are ioRegistryEntryCreateCFProperties and ioRegistryEntrySetCFProperties. These allow you to pass a dictionary from user space into the kernel and have it end up going to the driver in the set properties. Or if you're getting, it allows you to ask the kernel to provide you a dictionary from serialized properties in your driver that you can then use.
Now if you're going to be creating a user client, after you have found the device, you'll want to use I/O Service Open, which will cause in the kernel the user client class to be created and you can then go from there.
[Transcript missing]
Mark Tozer-Violchez, Wayne Flansburg, Josh de Cesare, Steve Martin, Jason Seng So the other topic we need to talk about is driver delivery. So now that you've got a driver, you need to figure out how to get it into the system.
This can be done in a couple of ways. The standard driver format that you're going to be using is a KEXT. It's the standard CFBundle. It has the usual properties in it. The two that are very interesting for us, of course, are the Info.plist file that gives the properties, including the I/O Kit personalities. And there's also the binary for the driver.
So this whole thing is directory-based. It's not a flat file, which makes it rather difficult to put in the ROM on a PCI card. So we've also provided another method for storing drivers. This is a .mkext. We call it a multi-kext. It can contain multiple KEXTs. Normally, for your device, you're only going to be putting one KEXT inside your mkext. It turns into a flat file. It's compressed. It's also checksummed and is very easy for use in a ROM. It only contains an Info.plist and a drivers binary.
So how do you create this .mkext? First thing you need to do is you need to strip the debugging symbols and local symbols from your driver. This will reduce the size of your binary by about 90%. It will also simplify things. You don't have to worry about other people looking at your debugging symbols or gaining other information about the internals of your driver. To do this, you use the strip command with the dash lowercase x and point it to your driver. It then goes through and removes everything that isn't needed.
It's very important to do this because obviously the ROM space in your card is limited and this will greatly extend the size of the driver that you can, the actual working size of the driver that you can put in the ROM in your card. So how do you create the mkext? Well, first you have to run, after you strip the driver, then you can run this mkext cache utility. And this utility will take a KEXT or potentially a list of KEXTs and put them inside your driver, and create an mkext out of them.
It will compress them and checksum them to further reduce the size of your MCaxed. Now that you've got an mkext, you need to know what to do with it. So this involves the Fcode that you've written for your driver. Now Mac OS X uses basically the same Fcode as Mac OS 9. Really there's only optionally two properties to add that are needed.
and these are the driver, APL, Mac OS X, PowerPC property that's very similar to the Mac OS 9 version of the driver from that you're used to. It's a property in the device tree. You use encode file to put it there and there's an example there of what a sample string for how you would create it. This causes the entire contents of the mkext to be created as a property inside your driver, or inside the driver's properties. Now this can take up a lot of space in the registry, so one way you can get around this is by using the driver-reg/apl/macosx property.
This would just be a reference in the property list for where to find the driver in your expansion ROM. Now obviously you have to have an expansion ROM to do this, and unfortunately there aren't any good tools to do that right now, but it is possible to do and in the future we hope to have better tools.
Another topic I just wanted to briefly mention is firmware updates. So if later, after you've already released your card, you want to be able to update the firmware, this is a sample of one way you could do it. There are many different ways you could do this. And I want to talk about this a little bit because it is different than 9.
Obviously in Mac OS X you can't actually access your hardware from an application, you have to go through a driver. So this means that the hardware access done to, say, flash the ROM on your card, has to be done by the driver. The firmware update application just turns into a pretty front end.
You, one way or another, have to pass the firmware image from user space to the driver. This can be done in chunks. It can be done all at once by having a driver, by creating a dummy driver that just contains a property that is the image that somehow attaches to your driver.
Or you could use set property interface to put the chunks across one at a time. You could also even use a user client to have the chunk of address space in your user client. You could use a user client to have the chunk of address space in your user client.
You can have your user application have the entire firmware image in its memory space, then you could have your user client map that address space into the kernel that your driver could then take advantage of. Once you're in the driver, it will write these chunks into the flash on your card. You could also have it update properties so that your application could have a barbershop poll giving you a little bit of progress. And that's pretty much the basic idea of that.
So we're going to find some examples. The one thing that we really want to stress is the first thing you need to do if you're going to be writing I/O Kit drivers is you need to sign up as a Darwin developer. You need to go to the publicsource.apple.com website to find out how to do that.
And in there you can find out how to get access to everything inside Darwin. Three things of interest inside Darwin that's topical to today's discussion are X and U. This is the actual kernel's source. You can find inside here all the headers and source to various pieces of I/O Kit.
This will be a valuable resource. It also serves as many examples for how to do certain things. I've also been working on an example generic PCI driver. Unfortunately, it's not quite done, but it's got a pretty good going so far. You can find this in I/O Kit examples, Kernel, I/O Kit, generic, and generic PCI.
I will be posting updates or references on how to find it and status reports to the Darwin developer mailing list, so you should all sign up for that. Additionally, a couple years ago I wrote a simple driver for a BrookTree 878 device. It's one of these devices that has both a video and an audio function, and the driver is available inside Darwin to serve as a somewhat more complicated example of how to do that.