Some NuttX boards don’t have full support for all the on-chip peripherals. If you need support for this hardware, you will either need to port a driver from another chip, or write one yourself. This section discusses how to do that.

Porting a Driver

Often support for on-chip peripherals exists in a closely related chip, or even a different family or from a different manufacturer. Many chips are made up of different Intellectual Property (IP) blocks that are licensed from vendors like Cadence, Synopsys, and others. The IP blocks may be similar enough to use another chip’s driver with little modification.

  • Find a similar driver in NuttX source code:

    • Look at register names listed in the datasheet for the peripheral.

    • Search the NuttX codebase for the register names (try several different ones).

    • Note that you’ll have to compare the datasheet to the header and code files to see if there are differences; there will usually be some differences between architectures, and they can be significant.

  • Find a similar driver in U-Boot, Zephyr or BSD Unix (OpenBSD, FreeBSD, NetBSD) source code:

    • Only for inspiration, you can’t copy code because of license incompatibility and Apache Foundation restrictions. (Apache License 2.0 and BSD code can come in with a software grant agreement from the original authors; this can sometimes be hard to get. Ask on the mailing list if you’re unsure.)

    • But you can debug to see how the driver works.

    • U-Boot drivers are often easier to understand than BSD Unix drivers because U-Boot is simpler.

  • Understanding how the driver works

    Here are a couple of techniques that helped me.

    • printf debugging

      • Sprinkle custinfo() logging statements through your code to see execution paths and look at variables while the code is running. The reason to use custinfo() as opposed to the other logging shortcuts (mcinfo(), etc.) is that you can turn on and off other other logging and still see your custom debug logging. Sometimes it’s useful to quiet the flood of logging that comes from a particular debug logging shortcut.

      • Note that printing info to the console will affect timing.

      • Keep a file with just your debug settings in it, like this (debugsettings):

      • Add the settings to the end of your .config file after running make menuconfig (that will reorder the file, making it harder to see and change the debug settings if you need to).

        $ cat .config debugsettings > .config1 ; mv .config1 .config
      • If you are using interrupts and threads (many things in NuttX run in different threads as a response to interrupts), you can use printf debugging to see overlapping execution.

        • Interrupts - here’s how to inspect the C stack frame to see what execution environment is currently running:

          uint32_t frame = 0;  /* MUST be the very first thing in the function */
          uint32_t p_frame;
          frame++;             /* make sure that frame doesn't get optimized out */
          p_frame = (uint32_t)(&frame);
          custinfo("p_frame: %08x\n", p_frame);
        • Threads - here’s how to get the thread identifier to see which thread is currently executing:

          pthread_t thread_id = pthread_self();
          custinfo("pthread_id: %08x\n", thread_id);
      • stack frame printf

      • thread printf

    • GDB — the GNU Debugger

      GDB is a great tool. In this guide we’ve already used it to load our program and run it. But it can do a lot more. You can single-step through code, examine variables and memory, set breakpoints, and more. I generally use it from the commandline, but have also used it from an IDE like JetBrains’ Clion, where it’s easier to see the code context.

      One add-on that I found to be essential is the ability to examine blocks of memory, like buffers that NuttX uses for reading and writing to storage media or network adapters. This Stack Overflow question on using GDB to examine memory includes a GDB command that is very handy. Add this to your .gdbinit file, and then use the xxd command to dump memory in an easy-to-read format:

      define xxd
        if $argc < 2
          set $size = sizeof(*$arg0)
          set $size = $arg1
        dump binary memory dump.bin $arg0 ((void *)$arg0)+$size
        eval "shell xxd -o %d dump.bin; rm dump.bin", ((void *)$arg0)
      document xxd
        Dump memory with xxd command (keep the address as offset)
        xxd addr [size]
          addr -- expression resolvable as an address
          size -- size (in byte) of memory to dump
                  sizeof(*addr) is used by default end

      Here’s a short GDB session that shows what this looks like in practice. Note that the memory location being examined (0x200aa9eo here) is a buffer being passed to mmcsd_readsingle:

      Program received signal SIGTRAP, Trace/breakpoint trap.
      0x200166e8 in up_idle () at common/arm_idle.c:78
      78      }
      (gdb) b mmcsd_readsingle
      Breakpoint 1 at 0x2006ea70: file mmcsd/mmcsd_sdio.c, line 1371.
      (gdb) c
      Breakpoint 1, mmcsd_readsingle (priv=0x200aa8c0, buffer=0x200aa9e0 "WRTEST  TXT \030", startblock=2432) at mmcsd/mmcsd_sdio.c:1371
      1371      finfo("startblock=%d\n", startblock);
      (gdb) xxd 0x200aa9e0 200
      200aa9e0: 5752 5445 5354 2020 5458 5420 1800 0000  WRTEST  TXT ....
      200aa9f0: 0000 0000 0000 0000 0000 5500 1100 0000  ..........U.....
      200aaa00: 5752 5445 5354 3520 5458 5420 1800 0000  WRTEST5 TXT ....
      200aaa10: 0000 0000 0000 0000 0000 5800 1500 0000  ..........X.....
      200aaa20: e552 5445 5854 3620 5458 5420 1800 0000  .RTEXT6 TXT ....
      200aaa30: 0000 0000 0000 0000 0000 5600 1200 0000  ..........V.....
      200aaa40: 5752 5445 5354 3620 5458 5420 1800 0000  WRTEST6 TXT ....
      200aaa50: 0000 0000 0000 0000 0000 5600 1200 0000  ..........V.....
      200aaa60: 0000 0000 0000 0000 0000 0000 0000 0000  ................
      200aaa70: 0000 0000 0000 0000 0000 0000 0000 0000  ................
      200aaa80: 0000 0000 0000 0000 0000 0000 0000 0000  ................
      200aaa90: 0000 0000 0000 0000 0000 0000 0000 0000  ................
      200aaaa0: 0000 0000 0000 0000                      ........

NuttX Drivers as a Reference

If you’re not porting a NuttX driver from another architecture, it still helps to look at other similar NuttX drivers, if there are any. For instance, when implementing an Ethernet driver, look at other NuttX Ethernet drivers; for an SD Card driver, look at other NuttX SD Card drivers. Even if the chip-specific code won’t be the same, the structure to interface with NuttX can be used.

Using Chip Datasheets

To port or write a driver, you’ll have to be familiar with the information in the chip datasheet. Definitely find the datasheet for your chip, and read the sections relevant to the peripheral you’re working with. Doing so ahead of time will save a lot of time later.

Another thing that’s often helpful is to refer to sample code provided by the manufacturer, or driver code from another operating system (like U-Boot, Zephyr, or FreeBSD) while referring to the datasheet — seeing how working code implements the necessary algorithms often helps one understand how the driver needs to work.

  • How to use a datasheet

    Key pieces of information in System-on-a-Chip (SoC) datasheets are usually:

    • Chip Architecture Diagram — shows how the subsections of the chip (CPU, system bus, peripherals, I/O, etc.) connect to each other.

    • Memory Map — showing the location of peripheral registers in memory. This info usually goes into a header file.

    • DMA Engine — if Direct Memory Access (DMA) is used, this may have info on how to use it.

    • Peripheral — the datasheet usually has a section on how the peripheral works. Key parts of this include:

      • Registers List — name and offset from the base memory address of the peripheral. This needs to go into a header file.

      • Register Map — what is the size of each register, and what do the bits mean? You will need to create #defines in a header file that your code will use to operate on the registers. Refer to other driver header files for examples.

Logic Analyzers

For drivers that involve input and output (I/O), especially that involve complex protocols like SD Cards, SPI, I2C, etc., actually seeing the waveform that goes in and out the chip’s pins is extremely helpful. Logic Analyzers can capture that information and display it graphically, allowing you to see if the driver is doing the right thing on the wire.

DMA Debugging

  • Dump registers before, during, and after transfer. Some NuttX drivers (sam_sdmmc.c or imxrt_sdmmc.c for example) have built-in code for debugging register states, and can sample registers before, during, and immediately after a DMA transfer, as well as code that can dump the peripheral registers in a nicely-formatted way onto the console device (which can be a serial console, a network console, or memory). Consider using something like this to see what’s happening inside the chip if you’re trying to debug DMA transfer code.

  • Compare register settings to expected settings determined from the datasheet or from dumping registers from working code in another operating system (U-Boot, Zephyr, FreeBSD, etc.).

  • Use the xxd GDB tool mentioned above to dump NuttX memory buffers before, during, and after a transfer to see if data is being transferred correctly, if there are over- or under-runs, or to diagnose data being stored in incorrect locations.

  • printf debugging register states can also help here.

  • Remember that logging can change the timing of any algorithms you might be using, so things may start or stop working when logging is added or removed. Definitely test with logging disabled.