 My name's Andy Gross and I'm with Lunaro and I'm on the IoT team, what we call light. So I had given this talk two weeks ago in Lunaro Connect. I kind of mixed up my slides a little bit because most people here are going to be more familiar with device tree than some of the people that were there. But basically if you go and you look at the way that Zephyr works today configuration is done generally through Kconfig and the configuration is kind of spread throughout the system. For any one board whether it be NXP, Kinetis or some of the ST micro electronics boards you'll see the same thing. There's two directory structures that you can look through arch, arm, sock and then there's a board directory underneath there and there's also a board slash and then a board specific one as well. If you go into both of those directories and you look around you'll see a multitude of Kconfig files and inside those Kconfig files you'll see lots of information like SRAM base address, number of UArts, lots of different configuration parameters on the different IP blocks that are in the socks and you'll see that most of this configuration is hard-coded. If you go and you look at the DevConfig that's actually used to generate the initial configuration when you run the make DevConfig it goes through and creates this big Kconfig file that basically contains all of these values. If you look through all that and you compare them between boards you'll see a lot of the same stuff and even between boards that are very similar like if you go and you look at the ST micro electronics boards you'll see very small variations. I mean there'll be a lot of the same information because a lot of the board is very similar. Things that where you'll see differences are like let's say multiple devices like let's say two GPIOs versus five, three UArts versus one, things of that nature. The other thing is is that some of the definitions can come from multiple file sources in the system whether it be Simpsys headers which are generated from the ARM or from the vendor or other include files that are just created. The thing about this is it's not very extensible for similar boards. What you'll find is people actually copy and create new directories for variations of boards and so that means you actually have to copy things around and then modify it to be specific to that very that specific board that you're working on. The driver and board initialization is very similar. Again things come down to the Kconfig. There's a lot of if-defs that are used to work their way around device multiples and also used for initialization. Let's say you have an MPU, well if you have one there's an if-def block, you run through that. If there's not it's compiled out. If you have multiple UArts you'll see this in the driver code. You'll see if-def UART 0 and you'll have a block that does static structures and then it'll do the device API in a net call and you'll see you know UART 0, UART 1, UART 2 and you'll see the same thing for the GPIOs. So that to me is it's a little hard to take. To be able to add more UART ports you have to actually add code and these are hard-coded in it structures too. So that defined the platform data. So you know how can we how can we do things differently and make it a little bit more easier to deal with. My thoughts around that are basically well let's let's see if we can use device tree for this. Now in the Linux kernel device trees used not only for you know configuration and saying what's on the board it's also used during runtime. In our case runtime can be a little difficult because we don't have a lot of space but and I'll get to that later but we can talk about some of the things of why of how and why we'd want to use device tree for Zephyr. One device tree is architecturally neutral. It doesn't matter if you're on ARM x86 arc doesn't matter you can you can define the system. We would need less K config options. The reason for that is is that the configuration information would actually be described in the DTS and instead of having things like SRAM address and number of UARTs and and even base addresses IRQs and all that stuff you can actually get that out of the DTS you don't need a K config option for that. Device tree is very extensible you can use it to describe anything. If you have some differentiation in node information like you know UARTs are always going to have baud rate they're always going to have a base address IRQ pins there may be other things that make one board different from another you can actually describe that. Other layers could possibly use the device tree information perhaps and they can actually glean that information from the compiled DTB or even the DTS file and they can do that during compile time. The other thing that that's really nice about this approach is is adding boards new boards and new SOCs is a whole lot easier. In a lot of cases you can reuse a lot of the same DTS information. If you look in the Linux kernel you'll actually see DTS files broken apart. You'll see DTSI files which generally give you about a bunch of generic information for a SOC and then as you get more specific you can actually do includes. You can have your generic SOC and then you can have an actual board specification. It includes the generic SOC and only turns on the pieces that it needs and then adds additional information to describe specific things about the board that make it different than the generic SOC. So that makes it a whole lot easier and the other thing is that with the smaller DTS files that I'm talking about for the boards you immediately see what the differences are in the boards and how they're configured. So I kind of alluded to this just a minute ago. So to blob or not to blob. So with device tree you have two things. You have the DTS files themselves and you have what the output of the compiler gives you which is a flattened device tree blob. If that's what you specify when you do the compile. When I originally started thinking about this I was thinking of just using the DTC compiler as an error checker doing a DTS to DTS and then parsing the DTS file itself. I found that there's a ton of tools to actually look at and break down DTB files. There's not a lot of tools that parse DTS files themselves. So even though I don't think that we want to use the device tree blob in the runtime. I would probably still compile it to be a DTB just so I can parse it and use existing tools. I mean I like writing code but I don't like reinventing the wheel. So when I can leverage other people's work I really try to do that. So we're only interested in defining the configuration and the board initialization information. We aren't looking at runtime information. We're not going to use this in a running system. The thing about it is that a DTB file itself when you compile it can be fairly large. I mean with respect to the kinds of systems that we're talking about. I compiled a very small DTS file and got I think it was either 4K or 8K. Well that's huge. So yeah we can't use the DTB as is. Not without finding some way to pare it down if that's what we want to do and I don't think that's the answer. Space usage is at a premium. We're talking about systems with what, 192K of flash may be. We can't blow through that much using a DTB. So this approach requires a little bit of tooling. So the first thing is that when we create the DTS files and this is even true with the current K config options and even the include files that are in the system. A lot of them leverage either Simpsys headers or vendor files that are defined somewhere whether it be in the XT howl or whatever. We would still want to do this. When people write a DTS file it makes more sense when they enter in the configurations to actually use names that make sense. So instead of having let's say a base address that you would just say okay well it's hex 40 million. Well they may have a header somewhere that says oh no this is your zero base address. Well that makes more sense. Plug that in there and pull that include information in to the DTS file. And you can actually do that. You can have include files. The Linux kernel does that. They have their own separate directory structure where they store the stuff. The thing about the Linux kernel though is that people tend to split the definitions in the DT include area. It's only pound defines. And a lot of these files that we have in Zephyr there's a mixture of structures and pound defined information. And that's actually a problem for parsing. The C preprocessor has no problem with this. The DTC compiler does. So that stuff has to be split out. So in my initial work I kind of just said well I'll figure out how to deal with that. And I kind of went past that. I mean I've got to go back and fix that. But the idea is we want to leverage the include files. We want to use the C preprocessor to allow us to do that. We've just got to be able to split the information out. Or get the Simpsys, the ARM guys and or the vendors to split it out for us in a way that makes sense. That's probably a tougher sell than it is to just parse it in some way and split the information. So the idea is that we need to build our configuration. So we have to go through a process of collecting the include information, preprocessing replace, run it through the DTC compiler. And then once it's done we have a format that we can actually parse and create an include file that not only includes some pound defined information that we need, but also would include structures that are required. So what I've been doing in the near term, this is the past few weeks I've been working on the parsing of the DTP file. That also has involved actually defining the DTS files for at least the NXP Freedom Board. I've also looked at the TI and ST boards. I pulled the data sheets and started pulling together the information to create the DTS files. In some of these cases, some of these sock vendors actually have this information or can generate it. But I'm a glutton for punishment, so I'm doing it by hand, which is fun. But the good thing about that is actually I'm learning a lot about the different socks out there. It's funny how similar a lot of them are. Yeah, they're all Cortex-M based, but some of the other IP blocks that you have to have are very similar. So DTS files have to be in place. And then we need the tooling to create the include files. That's what I've been working on right now. So I've worked up a Python script that uses a Pi FDT, which is a fairly nice, simple DTP parser. And I've created a small include file that I'm going to show here once I get through some of the DT information I'm going to show. Once the configuration is generated, that can actually be used in the make files as part of the make process. When all this is done, the way I see this falling out is that we're going to be able to clean up a lot of the configuration directories. We're not going to have a lot of this K config spread out in the system. Of course, we're going to have to have K config that's going to be required for just to get different things working properly. But it's not going to contain a lot of the configuration specific information that probably had no business being in K config to begin with. And since that's the case, you know, like the board directory, if you go and you look at the board directory, there's not a lot in it. And in fact, the board.c file is empty. So perhaps we can get rid of at least one of these directories and have everything in one or the other. The last step, of course, I mean, you know, we want to leverage the generated files in the make. So the last part of this is okay, we've got the board configured, we've got the board initialized, now we got to deal with the drivers. So we're going to have the information that we pulled out of the DTS files. And we've got to work within the API device API and a net function calls to get the device drivers the information they need. I've looked a lot at the different drivers and how they work. I have some concerns with the pen boxing and the GPIO. But I think we can work through that that may require some changes to the APIs. But we'll get there. So I'm going to pause here real quick. Do we have any questions before I actually give some of the DT a DT example? I don't know if that's good or bad. So I took the freedom board and I created a device tree file. And if you're familiar with device tree, or if you've looked at it in the Linux kernel, you'll see some very similar things. The first thing is we're going to have a board compatible, which gives us, you generally are going to have more than one compatible name. And it goes from least specific to more specific. So this is a K64F and the board is the K64F12, or at least I think that's what the name is. So that's that's what I encoded here. It has one M4F processor. It has some SRAM at, what is that, 20 million hex. And the SRAM has, I don't know, it's 192K I think. And then you have the actual SOC structure, which actually will contain the device nodes themselves. The ARM Cortex-M, it has an interrupt controller called the INVIC. So we have a compatible block where we say, okay, this is an INVIC. Here's where it resides. The two things in here that are important, and this actually changed between different SOCs, are the number of priority bits and the number of IRQs. In some cases, a lot of vendors already have this information defined in Simpsys, or in their extended Simpsys header file. But we'd also want to put that here. And instead of having numbers here, you would actually want to plug in the Simpsys pound defines for these guys. In my case, I didn't, that's how you do it. Cortex-M4F, they have, it has a timer block, which is the SysTick. Probably going to need a little bit more fleshing out of that information. I put in a clock source because it's either the AHB or it's AHB divided by eight. There may or may not be an MPU that you want to define. I have it disabled here. If you did define it, you would have some regions that you would want to specify for the security. On the freedom boards, you have a master clock generator. It's their master clock control. That's where their system clock is. And I denote that with the system clock frequency. This is one of the things you'll see all through the system. The system clock frequency is depending on which board it is, will be defined in a cake and fig somewhere. This is definitely one of the things that would change between boards. And you see it a lot actually. They also have an oscillator and an RTC. These don't matter that much, but I wanted to at least enumerate them here to show how we're going to describe these things in DT. When the drivers initialize, the kinds of things that you need are the base address, the IRQs, things of that nature. And we have that information here. And we'll have an IRQ example down here. Yes. Yes. That's a magic number. So I think if it was HB divided by A to be one. And I wanted I should have put a pound of finding here, but I didn't want to mix things right now. I just wanted to keep it very clean. The SIM node in the Freedom Board is actually a clock gating control. So not only does it have some clock gating, it also has some divider information about the different buses. You're going to see this one referenced later. So the UART, this is a good example. So UART has a base address. It has a label. The labels are used in the device API in it. It has at least one interrupt, a BOD rate. And it has some pins that are defined. I set up something very similar to the Linux kernel with the pin controls. For most devices, you're going to want to have a default pin control or a pin moxing that denotes the active set of pins for the device. And you're also going to want to define a low power mode. The reason why you want to do that is that some of these devices are going to be operating off coin cells. So you're going to want to be able to put these things in low power mode, which that means you need to be able to describe the pins in a way that the driver knows which set is active and which set isn't. And so I did that here to show an example. I'm going to skip UART 1 because it's basically just like UART 0. The difference was I was too lazy to enumerate the pins on that. I didn't want to have to look through the chart. But let's go to the pin mox device. So this is actually the node in the system that controls the pin moxing. And I defined the different pin sets here, at least for the things that I used in this file. So you'll see a UART 0 default. You'll see a port which points to the GPIO port that these pins sit off of. And on this board, I think there's like five or six GPIOs. Pin 16, 17, and then there's a function which actually denotes what the function that you want to set in the pin mox. There may be up to, I don't know, five or six different functions for a pin. And depending on how you have your board set up, you want it set one way or another. For the low power mode for these two pins, you want to set it to GPIO and probably put a pull up. I didn't put a pull up here. For the spy pins, those also reside off of GPIOB 9, 10, and 11 function too. So after that one, we have some of the GPIO nodes. And you'll see here there's a label GPIOA and GPIOB. The GPIOB is what I referenced to before. So that's how we can get the addresses for some of these things when we do the parsing. We have a spy node here, which gives you some slight differences. The thing about spy is you have chip selects, so you've got to be able to define those. And those are typically GPIOs. And you'll see here it's GPIOB pins 9 and 10. If you noticed before 9, 10, and 11 were being used for the MISO, MOSI, and clock, so I have two of these are wrong. They need to be something else. But this is just an example just to show you how it would look. And you'll see here that the clocks, there's a clock gate for this guy. And it's a reference back to the SIM and it gives you the offset and it gives you the bit. So with this information, I'd be able to figure out what address and what bit in the clock gating node to go and set to enable the clock. I didn't do the second spy instance just for time. So with this file that I defined, I wrote a Python script to use the PyFDT and I parsed some of the information. Out of that, I generated a small include file. And this would be one of the things that we output from the DTB file. So all of the systems need an SRAM base address and that's what's used for saying, OK, this is where things reside. As I parsed the system, and I only got to the UArts, I'm still working through this. But I pulled the number of UArts out, the base address, all the information we'd need for the device API in a knit, or at least to build up a structure to pass to it, and some of the IRQ definition information. This file is going to get bigger as I add the extra parsing for all the different nodes. But this gives you an idea of what the include file is going to look like. And if you go and you look at the K configs, you'll see the same information. It's just that I collate it from the DTS file. I don't have it spread out in the system. So I'll pause real quick for any questions before going on. I think that was the last slide. But one thing I wanted to add, and I must have cut that from this, the other thing that we want to be able to do with the DTS is to pull the driver options that we're actually using. So in the Linux kernel, there's a thing called DT2Config. And they use that to, they run it over the system and the DTP file that's generated. And they're able to actually figure out what all the options are in the system that need to be configured properly to have the correct drivers compiled. I think we want to do the same thing. We have all the information we need about what devices are in the system. We'll be able to figure out what K-config options, or def-config options that we need to turn on. If you have a UART, OK, you want a UART driver. If you don't have a SPI driver, you don't need it. If you have different things configured, we want to turn it on. So that's another output that we're going to have from the DTS file, is we're going to get a set of K-config options. And that, I believe, is all I have. So unless anybody has it, OK. The impression may be data structures flowing into your DTS. Could you elaborate on that a little bit? What the concept is? OK. Yeah, so let's use the SimSys as a great example. So there's SimSys headers out there that a vendor may have extended. It contains a bunch of information. Some of the information is actually the base addresses for a lot of these things. Instead of using, if I do an include of the SimSys file at the top of the DTS, then anything that I define or use in there would be able to be processed by the C preprocessor. That's the idea. That's really all we want to do. The thing is, is that with these files, I'm going to have to split out the C structures from the pound defines, because the DTC compiler doesn't like it. It fails when you run it. Visualize how you're taking that and putting it. So you say something like, just a totally stupid example. You'd say, clock divider bus equals some structure. Yeah, that's a totally nonsensical thing. But we'll say interrupts equals, because there you have like three fields, and then you want to be able to drop in the data structure there. It depends. So yeah, so let's use PinMux as a great example. I think PinMux, we're going to actually absolutely have to create a structure, because there's no way you can pound define your way around that. So in a system, you'd want to define all the different pin combinations that you want to possibly have active when the system runs. And then the application, whatever driver it is, is going to have to go through and figure out, OK, I need these three sets of pins. I have the information I need. Just use it. So things like that are going to have to be in a structure. Because you're then going to flow it through to your include file, which you showed in a couple of sites. Right. So the output of the parsing is this guy. I wanted to show up. Right. So we won't just have pound defines in here. We're also going to have some data structures that are consumed by the system. And they're going to have to use well-known names so that the rest of the system can use them properly. Yeah. Sorry about that. No, that makes sense. They knew there was going to be more to it. Yeah. Trying to keep it simple. Yeah, so the first part of it was just using some of the simplest information in the system. And one of those things is the SRAM base address. Because without that, nothing works. So that's one of the first things I did. Now I've just got to plug it in. So I'm at a point now where I can just add my generic board-include file and strip out the information that we don't need. And then it'll work. So yes. I think you mentioned that you can reduce if-defs in the code. Right now they do. How will this, because this is generating the same kind of error file that the kconfig generates? Yeah, so the difference is this. So this is where the driver init stuff is going to have to change a little bit. If we know how many UR ports we have, and we have the UR information we need, we can just say, OK, for the number of UR ports, call this device API in init with the structure we build up for it. And we know how many structures we need to build up. And we have the structure information that we need. We could probably use a macro for that. So some things we can macro around and not have to have the big if-def blocks in the driver. We just need something that says, OK, you have a UR. Go and create some number of instances of the UR. That's all you got to do. You don't need that in the UR file itself. You need it somewhere. What we would use in this case is like, let's say we had two URs, we'd have two, number of URs, two. We'd have port zero information, port one information. We'd have a function or whatever that says, OK, for i is zero, two, the number of UR ports, macro, do something, and then device API init. That's the difference between how it works now, which is you've got these if-def, if I have five GPIO ports, I have to say if-def GPIO A, if-def GPIO B, C, D, E. We don't want to do that. So we can write a function that just loops through the number of URs and pulls the information that it needs and do it that way. And I would think we'd want to do that for every instance where we're going to have device multiples, not just URs by i squared c, things of that nature. Yeah. It will be equal to post-processed information, but it will be done by the parts that need to be done. Let's say if you were to set the driver, first, they have instances, which you do is i squared c, you create two functions and API tables, and the part of this will, the square, create as many as needed. So that'd be the C file, isn't it? Yeah. OK, so basically, you won't have to write this hash attack, which will possibly have an address. You're still going to have some information that you're going to have to create. I don't think you can get around that. But you won't have to have this if-def stuff going on. The idea is you don't want to have to keep modifying the driver files themselves when you add another instance. You want to be able to handle this in an easier way. The device name, to use this to generate unique numbers instead of strings for the device names? Yeah, we could. Yeah, because that would get rid of the string compare we have to do when we bend it by name. I just put the strings in here because that's what was being done. But I'm completely happy with changing it to a unique number. Yeah, I don't like string compares either. And when I saw that, I was like, you know, everybody kind of does things a little bit different because you'll see you are zero, or it's a different format for everybody, it seems like. That's the other thing. Either you have to enforce the standard or come up with some of their way of dealing with it. And that's the other thing about this is we'll probably have to enforce a little bit of a standard on some of the definitions just so that you don't have to do special stuff for all the different boards. So SRAM-based addresses is, of course, the same across everything. But some of these other things we need to actually kind of change people to use the generic generated names. So man, that's always a problem. Yeah, no, I understand. Because for like the ext house stuff, is that what you're referring to? Yeah, and you have basically a driver for SMI IP. It's a simple IP you have to try and just can do for the customers and the seats. So I want to print the name of the code number of the IP that's starting this code at some point. Right, then that would cause, yeah. It's definitely happening in the driver itself because people print it separately in this one. Right. It always brings up other. Right. And I think we've kind of run into that problem regardless, wouldn't we? I mean, with some things? I don't think you can have just a different problem than I need. Yeah. I think you can kind of wrap it around. Right, and I see where some people have kind of done these wrappers. If they're leveraging some of the ext house stuff, you don't see the, you kind of have to kind of follow it a little bit to see if they're actually calling out into the ext house. So yeah, we probably have to do something similar. Is there a notion of like a master DDS where you have all the different possibilities that you can generate one? Yeah, absolutely. So that's where the DTS files come in. You can generate all the different combinations. And then in the actual board specific file, you can say, I'm only interested in these. You're only going to use the ones that you're actually going to use. No, so the DTS doesn't have that. You'd have to have your own checker. I know in your case, TI's case, you have your own tool that generates that stuff. And it does a lot of that checking. But I'm not quite sure how that would be enforceable. And there may be cases where you want to switch from one device to another, where they're going to reuse the same pens anyway. And you would normally see that as a collision, and it's really not, right? Yeah. So I guess we're done if we have no other questions. So thank you for coming.