 Welcome to my presentation, LUTO-SDR, the making of an ultra-low-cost high-performance Linux-based software-defined radio. My name is Michael Henrich. I am software engineering manager at Analog Devices in Munich, Germany. Let me introduce a little bit the story behind the ADAMLUTO, which is an active learning module. It is part of the educational program at Analog Devices, and it's meant to introduce the fundamentals of software-defined radio, RF radio frequency, wireless communication, embedded Linux, FPGA, HDL development, open-source software and open-source hardware to basically everyone. It's designed for users at all levels and all backgrounds. And one of the most important things, it needed to be low-cost so that it is student-affordable. To the right, you see that software-defined radio requires multiple skill sets from RF design to digital hardware to signal processing and software development. The LUTO-SDR is meant to be a platform to practice and to develop those skills. So what were the early-on requirements for this device? In order to be student-affordable, it needed to be a low-cost, also a zero-cost ecosystem. That means that there's no cost involved in order to build or use this device. It needed to be open. When I say open, I mean open hardware, schematics, Gerbers, open firmware, basically all the HDL, the bootloader, Linux, the user space, open host so that it is kind of a cross-platform with open drivers and open libraries, and of course also open applications, for example, new radio, MATLAB interface, SDR, ANGEL, GQRX, etc. It needed to be reliable, fail-safe and brick-free. That means if you encourage students to build custom firmware blobs, you need to make sure that you don't get returns because they screwed up the bootloader or anything else. It needed to be high-performance and fast. I'll talk about this a little bit later on. Intuitive, ease of use, supportable, extensible, flexible, and of course cross-platform so that people can use it on Windows, Linux, and MacOS, for example. What's inside? The Pluto SDR uses a Zynx 7000 FPGA from Xilinx. That is basically an FPGA plus ARM Cortex A9. It has 512 megabyte of DDR-free memory, 32 megabytes of spy-nour flash, USB-ALP5, and of course the 1893-63 RF transceiver from analog devices. It can capture RF data up to 61.44 mega-sample, and it has an allowed tuning range from 325 to 3.8 gigahertz or with unknown specs between 70 megahertz and 6 gigahertz. As you can see, there are two LEDs on the bottom, a tiny little button, and two USB checks. One is connected to the on-the-go port, and the other one is just meant to provide auxiliary power in case a USB host function is connected to the on-the-go port. In addition, there are some spell pins that are connected to the FPGA fabric that can be used for designing custom logic or to integrate it somehow into your use case. There is a Chatek port, UART pins for the console. There are some other spell pins over here that are connected to the transceiver, and of course there's an RX and the TX antenna port. The PlutoSDR runs Linux inside and uses the Linux Industrial IO framework to expose IQ data and control. It appears as a multifunction USB device to the host if you plug it into a PC with native IO over USB using function FS as a serial over USB using the CDC ACM layer. It has an R&Ds Ethernet function, a mass storage drive, and it also supports device firmware upgrade. If you use it as a host, there are a range of device drivers built into the firmware for Ethernet, Wi-Fi, audio, human interface, device, and mass storage. It is cross-platform so that it can connect it to Windows, Linux, or Mac PC. Like I said, it uses the IO framework and also the LibIO. The concept of LibIO can be seen over here. There are basically on the bottom IO devices, and then there is the library running on top of it. It communicates through the local backend with the Linux kernel and provides a high-level API to the user or to client applications that run on top of Linux. In addition, there is an IO daemon server that also runs on the Pluto that provides exactly the same interface over a network or USB link to a remote library running on a different computer. This is completely transparent to the user, so the client application running over here doesn't really know whether it's talking directly to the hardware or through a network or USB link. On this slide, you see a top-level block diagram of the system, simplified, of course. On the very bottom, you see the AD9361 RF transceiver, which is connected via some spy interface, some GPIOs for control, and high-speed CMOS interface with the FPGA. Inside the FPGA, we have several HDL cores. There are some hard macro cores for spy and GPIO, but very important here is the AD9361 transport layer core. The transport layer core connects via DMA to the Linux system. On top of Linux, we have split the management of the transceiver into three different drivers. One is called the AD9361-5. The purpose of this driver is to configure the transceiver, to set up the yellow frequencies, to do all the calibrations. Basically, everything you control over spy and GPIOs, then you have two other drivers. One for each direction, one for RX, one for TX. That's basically the capture drivers that manage the high-speed data transfer to and from the device. All in common are that those drivers are IAO drivers. Important to mention are the different other subsystems. These drivers register with the clock subsystem, for example, that allows, for example, the RX transport layer driver to know its sampling frequency and so forth. I talked about IAO. What is IAO? It's the Linux Kernel Industrial Input Output subsystem. And it's really not just for industrial IAO. It's basically for all non-human interface IAO. This can be ADC stacks, accelerometers, gyros magnetometers, humidity sensors, pressure rotation, and so forth. It's in the upstream Linux Kernel for more than 10 years now. Why do we use IAO for software-defined radio? It has a lot of advantages. It provides a very good hardware abstraction layer, which allows sharing of infrastructure. It allows the developer to focus on the solution and enables application reuse. Also, kernel mode drivers have very low latency access to direct memory access interrupts, memory mapped IAO, and so forth. All that being said, IAO provides a fast and very efficient data transport from the device to the application, from the application to the device, and also from the device to network storage. Why is fast and efficient data transport a SDR requirement? Think about the Nyquist theorem, that sampling of 20 MHz of real RF bandwidth, for example, that's exactly 88 to 108 MHz for FM radio, produces a constant data stream of 80 Mb per second at 6-in-bit samples. The Blur SDR maximum sampling rate is 61.44 Mb, and that would require a memory bandwidth of up to 245 Mb per second. And this is way too fast for any USB 2.0. So, first of all, it's very important that we have very efficient low latency zero-copy data transfers. IAO supports two types of interfaces. A low-speed interface, typically used with low-speed converters connected via a SPI or iSqc, and a high-speed e-mail-mastered interface, typically used with memory mapped devices. So, this being said, IAO is used to copy the data from and to the device or to memory. On the application side, the user uses mem-map to make those data available in the application. All this is zero-copy and allows low overhead high-speed data captures. Data is grouped into chunks which are called DMA blocks and the kernel driver manages just the ownership of those blocks. So, either the application or the hardware owns the blocks. The samples per blocks are configurable and also the number of blocks in the queue are configurable as well. So, this allows to mitigate the typical producer-consumer problem and to bridge some gaps if the user application requires some extra time during one block to process. When connecting such a device to a computer, the user doesn't want to wait a minute until the device is booted and is available. So, fast boot was a concern for us. So, there are many good recipes and, of course, there's no one's size fits all, but in general, less is typically more. So, what we did, we slimmed down the kernel size by removing unnecessary kernel options. In addition, we slimmed down the root file system size. And a good way to do this is to use lightweight things. For example, busybox, a multi-call binary that includes all the typical commands, shell commands. In single binary. Then we use a very lightweight init system just with ETC init tab and some ETC init d shell scripts. A very lightweight mdev implementation. The other thing that significantly reduced boot time was to use silent boot. In the kernel, you can specify this in the kernel command line with quiet and lock level equals four or even below. And in U-boot, you just set standard out to null def that prevents any writes to delay the boot. In addition, we used a initial RAM disk which is also pretty fast once it is inflated into memory. And last but not least, the typical thing, you want to set the boot delay in U-boot to zero. With all these measures, we got down to less than three seconds. And I think we just spent like one second booting Linux and the other two seconds we spent in the first stage boot loader and in U-boot to checking the fit image and inflating it into memory. So if you deal with an embedded Linux device and the user treats it like hot block removal that they just pull out the USB cable out of it, you want to avoid fast system corruptions. And we are doing this by actually just using a RAMFS. And on the other side, we restrict the flash memory write access to really the bare minimum. So we allow it during firmware upgrades and during U-boot environmental manipulations and storage. For example, we save a few things in the U-boot environment just like the host name or the IP address. In the last firmware release, we have enabled an auxiliary partition that we had in there. We have reserved one megabyte. It was called the partition used by a non-volatile memory file system. And we used the JFS to file system on that. And we use this to store persistent SSH keys or password changes and nothing else. In addition, as I said before, we need to prevent the user from accidentally deleting our fail-safe mechanism, our bootloader. And therefore, we use flash block protection on the first megabyte of the Spinoff flash. Then we have several fail-safe mechanisms. For example, one is if loading the fit image fails, we automatically enter DFU mode. Then there can be still the case where the user by accident erases or the U-boot environment or more problematic here is to just delete an important variable, maybe something that's related to booting from SpyFlash. We need a recovery mechanism for that as well. Like I said, on JFS 2 we just have things that are not really important and if they are lost, they can be replaced. But we have an additional MD5 checking mechanism on all the files that we actually put there. So before we copy the SSH keys from the JFS 2 partition back into the RAM disk, we check the MD5 sums and if they're not okay, we just don't copy those files. I mentioned the flash block protection feature, often called flash locking. It is a feature of the flash in question. We use Spinoff flashes and those devices often have, depending on the vendor, either a top or bottom protect and depending on the boot mode of your SOC, you basically require bottom protect because it starts executing or pulling data from block zero at address offset zero. Like I said, it is really vendor specific and the block protection bits are non-volatile control bits. That means if you power cycle the board, you're still being set. In the kernel, the driver's empty disk by NOR preserves those bits, so it's not messing with these bits. Where do we set those? If the kernel driver doesn't handle those, we kind of set them in U-boot during factory programming and there are ways to actually clear this if you have access to the U-boot root console. On the right, we see this bottom or top protect. It's basically you can protect the number of blocks and starting at block zero, it's one megabyte and the next block is two megabytes, four and so forth. During our development, we actually found the need to instruct the bootloader to come from the kernel to do something different. For example, let's enter DFU mode on the next reboot without pressing a button on the device or to enter a special DFU mode. We call it DFURAM mode and that is a really, really fast way to load a new firmware without flashing it. It just writes it to memory, it's very, very fast. Or to just halt in U-boot so that you don't need to press Ctrl C while system boots and hopefully you can bypass the boot delay check and get to a prompt. Or just to start the system with messages printed to the console so that you see what might be going wrong because the default boot is silent. Normally you would use FV-SET-INV that's kind of a U-boot tools command that allows us to manipulate or read and write the U-boot environment from your Linux kernel. But like I said, we want to restrict U-boot all kind of flash access, write access to the bare minimum. So we take a detour here instead of using the environment as the messaging variable. We use a general-purpose read-while field in the system level control register that preserves through all the different types of resets, except power-on-reset. That's power-on means really take the power away and reapply it, but it's still there if the reset is caused by a software reset or a watchdog reset or something else. And also this bit or bit field must not be modified by the bootrom or the hardware. So we have created this tiny little shell command that just takes an extra argument. For example, ram-sf-reset or break and writes the code to a CISFS file which is situated close to the SOC reset software reset. And it kind of puts it into the register and in U-boot we just pull that value back from that register and do the appropriate action. For example, if this register has a free in the lower nibble, we execute defu-mode for flash. If it's a 7, we do the verb-boot boot and so on and so forth. U-boot is in general pretty good. If the U-boot environment is corrupted then it just switches automatically to the default environment. And the default environment is kind of a compile-time environment, typically defined with a set of strings in U-boot. So if the CRC doesn't match, it automatically uses that. But what happens if the user has deleted a essential variable and that's causing U-boot not to boot? Therefore, we kind of have the fail-safe mechanism. So that's some of the early init code in U-boot, this is where it talks in. If the button is pressed during boot we instruct U-boot to also use the default environment and this default environment has all the U-boot scripting that allows it to enter defu-mode again to flash a new firmware or a new environment. So the control and interaction concept is pretty interesting. So the question is how to control, configure, or interact with a black box device that only has a hidden button to LEDs and use beyond the Go check. So many things, we have a Bari Zero Conf, we also have a Web Server running on there. Those are typically the recipes that you know from your DSL home router or Wi-Fi router. There's also some LEDs for sign-of-life or state indication that we use. There's a serial console and also SSH, but that's typically just for the expert users. You could do more with the button, maybe Morse code, it's probably a bad idea or to implement a special USB function and create a GUI for it, but that also is a pretty bad idea. So what else, and let me show a short video on how we interact with this device. In this video, I want to demonstrate the use of the master drive on the Pluto SDR. As soon as you plug your Pluto into your USB port, it will appear as a USB hard drive on your PC. If you open that drive, you will see basically three files. One is the main landing page. It's an HTML page that gives you an overview of what this is all about, how to get started, where to download drivers and software. It also links in a few third-party support applications. It has a firmware section, how it actually works. There's some embedded JavaScript that contacts the GitHub API, checks for the latest version, compares the current version of your firmware with the latest version, and then allows you to download a new firmware. You just download it to the master drive, inject the drive, and it will automatically flash. Besides that, there's also some version information, a serial number, what kind of version of Linux it runs on, what version of Lib.IO, what compiler has been used to build the firmware. And there's also a link to the sysroot, basically the buildroot sysroot. It allows you to externally build compile applications. You link it against the sysroot, and you just copy it over to your target. The password, some information about the IP address, and a few other things. But at least there's also link on how to get help and support. The other file is basically the legal information of the device, the written offer, what kind of software it uses, what versions and what license are those packages. The most interesting part is this config file. It is an in-ease-style file that allows you to customize certain settings of your Pluto. This is in particular important if you want to connect multiple Bluetooth to your PC at the same time, so you can assign them an individual hostname, give them another IP address, and there are also some actions in here. For example, you can generate a diagnostic report. So what you do is just set this to one, save the file, close it, and then it checked your master strive. It immediately comes back and has the desired information. For example, this diagnostic report, open it, it includes all kind of version information, the kernel startup messages, a lot of information that's useful, debug, IO info, the UWOOD environment, the processes that are running on, and so forth. All that can be useful for debugging. As we have just seen, the USB mass storage gadget allows your device to act like a USB mass storage device, just like a pen drive or a hard disk. What's required for this? It requires a backing storage. It's basically a block device, or in our case, it's a regular file. This regular file needs to be full-sized, so you kind of create it with DD, then you create a partition table within that file, and then you also create a file system in that. For cross-platform support, we decided that this file system need to be fed. That's really a very common thing because it is cross-platform. But the problem with this, and you might have noticed our detour using the eject button, is that this file can't be accessed, loop-mounted, etc., while it's being attached to the mass storage gadget, unless it's read-only from both sides, which would be totally pointless. So how does it work? The backing storage, in this case fed.emg, between the USB mass storage gadget and the Pluto. In order to assign the storage to the mass storage gadget, you basically write its file name into a config.fs variable over here. USB gadget composite, gadget, blah, blah, blah, loon file. In order to return ownership, the user on the remote side needs to eject the drive, and this is done by some SCSI eject command that is typically present on all operating systems. It is very similar to ejecting a CD-ROM out of a CD-ROM drive. Once the backing storage is idle, not owned by the mass storage gadget, we have a kind of a daemon or shell script running on the Pluto, which loop-mounts this backing storage and checks its contents for, let's say, a new firmware or for some manipulations on the config file. We kind of do the appropriate actions, flash the new firmware, and once we are done, we assign the backing storage back to the mass storage drive. So while we are flashing, we need to give the user some indication that something is happening on the system so that the user doesn't turn off power or pulls out the USB plug. And we do that by using LED triggers. So in Linux kernel, there is a LED subsystem. It's called, like, LED glass. What it basically does is it allows the users to control LEDs from user space. That's our control via files in SysFS under SysClass LEDs. LEDs have a brightness attribute, which sets the brightness of the LED between zero and max brightness. For LEDs that are controlled by GPIOs, every value greater than zero will turn the LED on. But more interesting here is the concept of LED triggers and triggers are kernel-based sources of LED events. So during normal operation, we use the heartbeat LED trigger, and that is kind of a nice flashing LED, and it looks similar to, like, a heartbeat. And if the system load goes up, it beats faster. So you know what's going on on the system. But while we're flashing, we want to get a better indicator. And therefore, we use the LED trigger timer, and we kind of let the LED flash really rapidly so that the user knows, oh, yeah, something is still going on. I better don't turn off power. In order to use those, on the right, we see a device-free snippet. You set compatible GPIO LEDs, and then under this node, you can define a number of LEDs. LEDs have labels. They have a P-handle for the GPIO that they control, and, of course, they can also have a default trigger. In this case, it's a heartbeat. So when the system starts, it automatically enters or uses this heartbeat flashing thing. From the user perspective, it's Sys-FS stuff. You write strings and values into Sys-FS attributes, and these are the two shell functions that we use for flash indication on and off GPIO keys. Like I mentioned earlier, we have this kind of this fail-safe mechanism where the user can press the button while it's being on, and that lets the bootloader enter like a DFU mode. We also have something similar in the Linux kernel, and therefore we have this GPIO connected to the GPIO keys driver, and the GPIO keys driver translates GPIO all interrupts into Linux input events. So these are Linux subsystem input events. And on the Bluetooth system, we use an input event theme that also is part of busybox to invoke certain commands based on these input events. Here it's very similar. You have a device tree, a node with compatible GPIO keys, and then you have a number of nodes for the different buttons in your system. Again, also a GPIO handle and some sensitivity and what kind of input event code or Linux code should be sent to the input subsystem when this button is pressed or not. In this case, we use button MISC. On the Bluetooth system, in basically user space, we have this input event theme in running, and this is kind of the configuration file for it. It has two sections. One is global, it should listen on DEF input event 0, and the button keys that it should do something with, and in case the button 0 event comes, it sets action to remove all and then calls this slip MDEF auto-mounted script that I will talk a little bit about on a later slide. Here are some tips and tricks about multi-function USB gadgets via config.fs. This is kind of something that has cost me quite some headages. In general, USB gadgets are seen as a set of configurations. Each configuration has a number of interfaces also referred as functions. So sometimes the order of those interfaces really matters. For example, Windows requires that R and D is always on interface 0. If it's not on interface 0, really strange things happen, and it all doesn't make any sense. This being said, it's just some heads up if you compose your own multi-function USB gadget. Some other tips and tricks around gadgets are here. In order to distinguish between different devices, the serial number stored in the device descriptor must be unique. So you think about you have multiple of the plurals connected to your computer, and you want to distinguish between them or have always comport 0 attached to the same device. Even if you plug it into a different port, if you don't have a unique serial number, you will get 145, and next time you plug it in, you get Comport 146. That all is not really nice. Therefore, get a unique serial number. This is where the problem begins. Assigning a unique persistent serial number is kind of expensive. If you would need to add some additional eProm, that adds costs to the problem, you would need to program that. You would also need to make sure that you don't program the serial, the same serial number twice. This is all kind of infrastructure. We looked around and actually found that the Spy Norflash has a unique ID code. That unique ID code is right behind the JEDEC manufacturer and device identification. It's not present on all Spy Norflashes, but MicroN has it. This is really useful. It's a 6-in-bit unique ID. What we do is that we kind of just read this and print it to the kernel console or the kernel message when the system boots. When we create the USB Gadget, we just parse, use Crap on Dmask, get the Spy Unique ID string and assign it to Gadget string serial number. That works pretty nicely. The USB host functions. The USB port isn't on the Go port, so you can plug it into your PC, but you can also plug in things that you typically plug into your PC. For example, memory sticks, Ethernet adapters, sound cards, human interface devices and so forth. The Pluto, if you plug in a memory stick or a hard drive, it will auto mount all mass storage drives such as thumb drives and the supported file systems are EXT4, MS-DOS and FOWFET, basically FET32. How we do that? There's a hot black mechanism in the Linux kernel and you kind of write the hot black handler into that Proxys kernel hot black. In our case, it's MDEV. MDEV uses a configuration in e-file and in the e-file, we kind of instruct it if there is a new device appearing called SDA, for example, 0-execute-lib-mdev-automounder-shell script. This automounder-shell script will of course mount the file system, but besides mounting, it will also look inside that file system for certain naming convention. For example, if there is a file called runme0.sh, it will execute this as a shell script. If it's just runme0, it will run it as a binary file. You can have multiple of those and those are being executed based on their index. So runme0 runs before runme1. There's also another function that I just introduced with the GPIO keys slide. This is about safely unmounting this volume. So if the user hits the button, it will unmount this volume so you don't corrupt the file system. So how can you use this? So we use this, for example, to upgrade firmware. We just put the firmware on a flash drive, plug it in, and the runme will call the update script. But a lot of people use this to switch from the build root of S to, let's say, a full-blown ARM hard-flow to boot-to-root file system. You can do that by using the busybox-switch-root command. The switch-root command is nothing else than kind of a change-root. So you change-root into the new file system and execute the in-the-process of that new file system. This is really handy and pretty powerful. Managing hardware revisions and boot configurations. There are always hardware revisions, no matter what you do. But how can we handle those transparent to the user? Can there be a single firmware file who rules them all? Does the solution have image integrity protection? Or can it be fail-safe? And yes, there is such a thing, and it's called qimage.fit. Let's explain a little bit. So fit images are flattened image trees. These are basically multi-component images, and they support multiple configurations. Fit images also support FDT overlays, hashing and signatures, and they're also good for verified boot. But verified boot is not needed here, because this Pluto is as open as possible. Fit images use a device tree structure. On the right, you basically see a part of our U-boot script that kind of loads the whole thing. The fit image with an additional configuration. And then it starts booting. It says standard out serial echo boot. No, if the boot fails, it turns on the serial again and says it fails to boot and it enters a defue mode. So let's look a little bit at the Pluto.its. That's the source image file for that fit image. So fit images are structured like this. There is a root node over here. Then you have a kind of a description and a magic. And then you have an images node. And inside that images node, you basically list all your different components. Here in this case, we have three different device trees for three different hardware versions. Ref A, Ref B, and Ref C. All these nodes have additional attributes here, type, architecture, whether things are compressed or not. They can have hashes and so forth. So the other thing is kind of the FPGA bit stream. That's kind of the configuration for the FPGA. So this one has a hash. It's not compressed. And then we have the Linux kernel image and our RAM disk. It kind of pulls this from root of a CPO.GZ. And it is Gzip compressed and also has an MD5 sum. And besides the image, these are kind of the inputs. We have these configurations over here, this configuration node. And in that configuration node, we have different configurations. Config 0, Config 1. Config 0 says we use kernel from 1, RAM disk 1, FPGA 1. Pluto currently only uses... So all hardware versions of Pluto use the same kernel, the same RAM disk, and the same FPGA. The only what's different is the device tree. So Ref A uses FTD 1, which is this. Ref B uses FTD 2. And there are actually two more nodes here which are omitted on here. So how do we handle hardware revisions? And that is kind of the poor man way of doing it. So each different hardware revision of Pluto has a slightly different bomb. So we have a voltage divider composed by these two registers. And for each Pluto ref, we kind of change R2 to be a different value. This results into a kind of a different voltage over here. And when the system starts, we read this ADC in UWOOD. Therefore, we have created this UWOOD command. We call it Pluto hardware ref. So we call Pluto hardware ref. And this is the function that it kind of does. It samples the ADC. And then it takes this value. And we have 100 millivolt increments. And we kind of check whether this value is within a certain range. And then we kind of get to an index. Then we use snprintf to print config at index. And we save this into the environmental variable fitconfig. So once we have executed this command, fitconfig is kind of set with config at one, for example, for refb. And then we just execute bootm command, fitload address config at fitconfig. And that's the whole magic. Works pretty well. So we're getting to the end of this presentation. A few people asked me whether is it working, making open hardware, open system. Yes, it is. We have shipped nearly 40,000 devices since the release in 2017. We have many, many users in education and academia. But there's another community which is like rapidly increasing. And that's the hem radio amateur community, which use the Bluetooth for satellite communication and all other crazy things. It has been widely adopted by the open source community. And there are many, many, many SDR integrations into all kind of different frameworks. The feedback that we get from the customer is very positive. And it just works. Some other comments I recently got was that it's very easy. This R&D Ethernet function is very easy to integrate it into a virtual machine. And of course, LibIO is very stable and cross-platform. Here are some links and good pointers. So if you want to learn more about the analog devices university engagements, here are some two links. There's the wiki documentation about the Pluto, good pointers. And of course, the source code can be found on our GitHub. And I also like to list three books on software-defined radio. If you got interested, this is very good reading. So thanks for your attention and let's open the Q&A session.