 Hello, I'm actually Thomas Bostecar, I've been working for several years in Intel, first around Linux Middleware and for the last four years mostly on Zephyr Artos, basically in the networking stack but also in some hardware adaptation and API. And today I would like to talk about you, about a very specific part of Zephyr, a corner of stone actually, which deserves to be put under light and that piece is the Zephyr device driver model. But first, I'd like to go back in time. So four years ago, around 2015, when actually Zephyr was not even called Zephyr and we not even yet open sourced of course, we had this tiny OS that we believed was the next big thing to work on and of course, as created by Intel and Wind River, it was an X86 on EOS. We only had a very few amount of engineers, basically you could count them actually on ten fingers and we had actually this, the kernel itself, but nothing else. So which means you had actually no buses, APIs or anything. The only thing that we had was basically the not-so-fun Galileo actually target, which was our first embedded device to work with. And of course, we had a major problem. We had no drivers. The two only drivers at that time were the UART driver and the IOAPIC for the interrupt handling, but these ones were entangled within the CPU specific code. It was not make general. There were obviously no device APIs like GPR, OEDC, SPI or I2C, nothing like that. Nothing to manage devices, interdependencies because most of the time there are devices that actually depends on others to be able to initialize and obviously you don't want that to be outcoded anywhere. And no way, to instance, as many devices as you want from the same driver, like you might have actually ten UART port, one driver, so ten instance of the same driver. There were nothing like that. And at that time, Derek Brandoie, our fellow colleague, which unfortunately passed away in 2016, came with a solution and he came with something that would solve all the issues. It was dedicated actually driver model. And that was actually attacking all the problems. Obviously, this was actually made device diagnostic. So generic in fact that it would be used for other problems also to be solved. It was very tiny. I mean, actually if you have some curiosity, I will give you also the commit number and the patch is very tiny. And it will solve that also because Zephyr is actually targeting very tiny targets and everything should be made at build time. And that was the easy idea from the start. So even the device actually ordering and interdependencies and so on was done directly at build time. And of course, it would enable devices to export their own device specific APIs, which still didn't exist at that time. But we were planning, of course, to do these GPIOs and SPI and what not. And this would solve that. So it works. It's very simple, very straightforward. There are two structures, but we could basically talk about only one, which is the struct device. And that one provides a mean to expose an initialization function, a name for the device instance. The API will export and if needed, specific data or specific configuration for the device, which is really a pair driver specific. So it's enabled, but might not be used. And this structure is then among all the other structures ordered at build time. And we will see how this is done. Basically, you have your device object and when you create your device object, you direct it to be inserted in a specific device section. And that device section will then gather all the device objects. And because you provide a priority number to your device, this is actually used to sort this array of devices. So you end up at build time with a section which has already an array of ordered devices. And this array from the linker exports its starting and ending point. So in the code directly, you can access this array as simple as the usual. So because of these two pointers, you know where it starts, where it ends. And in real, you have, in fact, four init levels. That's another part of it. And these four init levels actually creates four specific sections. All these actually, a subsection of the device sections are preordered, obviously. And you have the pre-cannel one and pre-cannel two and a post-cannel and application. They are kind of self-documented. Obviously, the pre-cannel ones are actually initialized before the post-cannel and the post-cannel after an application at the end. And all of it is done through just one macro for the developer. It's actually as simple as that. You just have one macro and all the heavy lifting is done from the code generated by the macro and the linker script. So you provide actually two names, the device name and the driver name. The difference is that the device name is actually not a string and the driver name is a string. We will see that later. A function for initialization, some data if you have, some configuration information if you have, a level, whatever it is between the four actually we saw previously. A priority between 0 and 99 and API pointer. But it was so generic, as I mentioned actually previously, that it also solved another issue later on. It was about this boot time ordered subsystems. So that's Benjamin Walsh actually from Wind River that actually brought that. And it was very simple actually. He just made one macro on top of this device, an API unique macro. And therefore, this solution uses the device infrastructure directly. Instead of actually creating or exposing most of the details of the device, it just actually generates a device which is anonymous, so no name and nothing to identify it. No device specific API obviously, no configuration, no data. But it lets you having actually some software or subsystem being sorted out with devices. So for instance, if you want to start the console actually, console subsystem, if you don't have a UART device started, then you can play with the levels and the priority. So that was a clever use of the device infrastructure. Unfortunately, like any software design, it always comes with some drawbacks. But there are not that many on that one. When it was designed, we realized that we didn't care much about actually reporting errors at initialization phase. This was a really conscious choice made at that time because we judged that if the device is unable to boot or initialize itself, then it's just dead actually. There is nothing you can do about recovering that. But later on, it became actually necessary to continue this boot and still function even if some peripherals would be dead. And the hack around that was actually to just not expose any device driver API if the initialization phase fails. But that's generated another drawback which affects actually the roamable capability of the device structure to be in a ROM. Because from the beginning, the point was actually to put all this device structure and device configuration into the ROM so that it wouldn't eat obviously any RAM. But it's not the case anymore because of this. So you end up eating a bit of RAM due to that. The other drawback is that for each and every device instance you have from your driver, you need to hard code it directly in your driver. You need actually one device and API in it, macro call for each instance of your driver. So that's a bit of a burden because if on another board actually your driver ends up having more instance, you will have to actually modify the driver and add this actually device and API in it, macro call. But this was not a flow, design flow. This was actually introduced because of we were using Kconfig at that time. And Kconfig isn't as flexible as device tree nowadays. And as we will see, there is a work tree going on to fix that issue hopefully. The other drawback which also actually was actually consciously introduced because of Kconfig and couldn't actually overcome that is the stored device name. I told you about the driver name and the device name. The string, actually the label, actually string is actually stored in the structure, the device structure and the object at runtime. Because when you get the bindings of your device, you need actually to do a lookup, a runtime lookup. And it's basically a string comparison actually. So going through all the devices, registered devices, and it does actually a string comparison in order to find your device. So it's of course a bit of memory, having this name stored and you need a pointer for it and whatnot. And the device lookup is a tiny bit slow. Obviously at runtime, usually you do that only once for each device get binding. You never actually forget about the bindings that you just looked up. So but it slows down a tiny bit the boot time. But still at that time when actually the device driver model was introduced, this was actually a very clever solution and we've been actually working with it since then. I mean if you compare to any other parts of Zephyr, even the kernel actually, the kernel has changed a lot. We used to talk about fibers, for instance, before it got open sourced, which are now the co-operative threads, for instance, and even the API of the kernel has changed. Even all the device driver-specific APIs have changed a lot, GPIOs, PI, and they're still changing. The device driver model hasn't. So it was actually quite perfect in a sense from the beginning. And if you want to know more, I really actually advised you taking a look at these two commits. They are very short, very self-documented, and in a way quite elegant also. So let's now take a look at use case, what it takes for a device driver developer actually to do. So first thing first, you get to know the API your driver will export. And for that I decided to work on the watchdog because it's the simplest device driver API that we have at the moment. And it's made only with four functions. And so as a developer, you just get to know these functions and you implement your driver with that. So you go along with the signature and you expose your hardware features through these four. Once it's done, then you are actually the only work remaining is actually to create instances of this driver. And that's actually very simple. You only need actually three steps in the driver itself, creating an initialization function, which if necessary will do some hardware initialization if needed. You obviously at the second stage expose, I mean provide your functions of this watchdog driver API here. And then you do this at third stage, this famous device and API in it call. Here I did it only once, so you have only one instance. But as I said, if you have n instances, you will have n calls of this micro macro. Now I will actually talk about the configuration of a device driver. This wasn't exactly part of the device driver model when it came because at that time we were using Kconfig to provide a specific configuration options to the device driver. But that has changed because Kconfig has some limitation it's not scalable also for the instances. So we moved on with the device tree. And if we take the as an example this watchdog driver that we did, if we have n instances of it, of course, one thing which will change from one instance to another is for instance the base address where you actually can attack the hardware, whether its registers are. And before that it was Kconfig based, which was quite bad. Nowadays we use actually a device tree and the device tree is made of two files. That's what the device driver developer will actually pay attention to. One actually a file, an e-mail file that actually describes the device configuration options. So here it will actually describe the register. It's an array with an address and so on. And then is the DTSI file which configures the device instance. Following what you actually described in the e-mail file. So if something doesn't match you won't be, the DTSI process will fail and tell you that there is something wrong. And that ends up creating for you these macro configuration options that you can use then in the device driver. So that's the process you should follow. Usually if you see any device driver PRs nowadays it will be made of three commits. One is the actual driver code. Second is the bindings. I mean the description of the device in DTS. And then the DTSI file for whatever actually SOC or board that the driver is being used on. There is still however actually quite a lot of work. We have been actually working with this device driver model very well so far. But as I said for instance about these instances it's actually quite a pain to maintain. And because we use DTS now and DTS knows everything about your device it knows how many instances you have from it. It knows the configuration. And even though it's actually used only to generate the configuration option so far we could actually use it also to generate the actual struct device instances. So there would be no device and API unit called anymore. So there had been a prototype being done by Bobi Note and Erwin Goryu from ST, Microclinics. The idea at that time was I don't know if it's still the idea but there would be something like that. Instead of calling n times the macro to generate your instance you would basically have a tiny commented area in your device driver with pseudocode. At that time that was actually some pseudocode or Python code. And that DTS will use actually to generate the structures on the fly at the build time. So only of course the drivers which are present in your final build will have these instances generated. So instead of having that nowadays the YAML file DTSI used to generate this generated DTSboard.conf file you would also have this. So it will generate the device instance in built Zephyr drivers or wherever it matters. And that would actually ease up the maintenance quite a lot and enables also a lot of other improvements that we could do actually on the device driver model. And one as I actually mentioned before about this name thingy and this lookup which is actually a bit of a drawback nowadays could be solved also through that because we could actually rework the structures and moving attributes and better removing the name. So I did a prototype like two years ago but that wasn't made around DTS. It was actually a hacker around Kconfig which is obviously what we don't want to do. We don't want to add a new type to Kconfig or whatnot. But I could actually show off that this would solve, we would actually save quite a bit of memory because there would be no name stored anywhere. And obviously you would get a faster boot because at the linking time when you do a device get binding you would already have the pointer at the linking time. So no runtime lookup, no string comparison anymore, nothing like that. And the other thing also is the reshuffling the structures attributes could lead to also a gain of ROM and RAM because as I mentioned this season it macro, the one which is used for software subsystems or ordered actually initialization. These ones they actually do get struct device config and created with them. But basically none, they don't need that because they don't expose a driver API, they don't need of specific data or configuration. So we could lose that and make a structured device config optional. So it would be only generated for the actual device instances and not the system the subsystems actually software subsystems devices. And so all in all tiny modifications to change the device driver model that would lead for a leaner image in the end, faster one. And that's it, yeah, any questions? Yes? That could be a nice improvement as well. There are many actually things that could derive from DTS because DTS has all the information there and there are plans even on the power management side actually to use DTS for that. And that would be definitely a good idea because currently your driver instead, your device instance, the initialization level and the priority is encoded as well. So which is a burden for maintenance point of view? I don't know, we would actually probably need to talk to Kumar Galak actually from Ninaro who is actually leading the DTS work. But let's see, yeah, how much, yeah, another question? Okay, so if you have any and you can actually talk to me anytime or actually also I'm also on Slack obviously. Yeah, thank you.