SPI, Kinetis, and NuttX

2026-01-06 published | 2565 words

This is a log of discoveries about Serial Peripheral Interface (SPI), Kinetis, and NuttX.

I work on the board support package (BSP) for NuttX. It is not as noble as it sounds. Just write some .c and .h files (with predefined structure to fit the NuttX architecture) that make our board (the board) with MK60FN1M0VLQ12 chip work.

The board has multiple peripherals; I started with the ILI9341 display and 25AA128 EEPROM, both connected on different SPI buses.

I made the display work by rewriting Kinetis’ SPI in NuttX. It was inspired by how it is done in other SPI NuttX drivers. Then I found that the changes do not work for EEPROM. Then I made it work for EEPROM. And finally, I dropped all my changes to Kinetis’ SPI in NuttX and made minimal number of changes needed to let LCD and EEPROM work on the board.

To make a use of few weeks spent on Kinetis’ SPI and NuttX, there is this post. It contains:

About NuttX drivers

NuttX is an (embedded) operating system we use for our boards. I wanted to get familiar with NuttX and the board and decided to make the display working first.

We use ILI9341 display and there is the ili9341.c in the driver/lcd directory of the NuttX repository. So making the display work could be as easy as call to the ili9341_initialize procedure somewhere in BSP, right?

Fundamentally, there are two types of drivers in NuttX. Device drivers and bus drivers. ILI9341 is a (display) device driver; its initialization returns a pointer to the struct lcd_dev_s – universal LCD interface.

ST7789 (from the st7789.c file) is also a (display) device driver. Its initialization also returns a pointer to the struct lcd_dev_s. And that is the point of a universal (LCD) interface – developers can use the pointer to a struct lcd_dev_s and do not need to care if it is ILI9341 or ST7789 display. Or any other.

Note about interface: Interface, generally, consists of two parts: (1) How to communicate and (2) what is being communicated.

struct lcd_dev_s, for example, contains setpower procedure as an answer to the How to set the display’s power? question. Then, the setpower procedure expects the power argument, which is the answer to the What to set as the display’s power? question.

ILI9341 defines what to communicate with the display. For example, 0x01 is the software reset command, 0x11 gets us out of the sleep mode, 0x29 switches the display on, and 0x2c followed by a bunch of data writes to the display’s memory.

ILI9341 also defines how to communicate with the display – using 8080 parallel interface or 3-/4-wire SPI.

SPI stands for the Serial Peripheral Interface and interface consists of two parts. The ILI9341’s what (like 0x11) is converted to the particular voltage levels at particular frequency, the SPI’s what. The SPI’s how says what pins of the display and chip need to change these voltage levels: PCS, MOSI, and SCLK, i.e., Periphery Chip Select (PCS), Master Out Slave In (MOSI), and Serial Clock (SCLK).

We can say that the board uses ILI9341 (interface) over 3-wire SPI (interface) to communicate with the display. And NuttX mirrors it: to communicate with the display, NuttX uses ILI9341 device driver that uses SPI bus driver.

It is just a little bit more complicated, because NuttX’s ILI9341 (device) driver does not use NuttX’s SPI (bus) driver directly, but indirectly, via struct lcddrv_lcd_s implemented in the lcddrv_spiif.c file. Nevermind, we only need to initialize the drivers properly, i.e.,

ili9341_initialize(
lcddrv_spiif_initialize(
kinetis_spibus_initialize(
...)))

Now, when we know about the NuttX drivers, we need to decide where to implement the 3-wire SPI, because it is not implemented for the ILI9341, yet.

ILI9341’s 3-wire SPI

First, what is 3-wire SPI, anyway? If I understand correctly, there are usually 8-bit chunks transferred via SPI. Each chunk is either command or data and there is a separate pin/wire to distinguish command from data. That is called 4-wire SPI.

To save chip’s pin, we can get rid of the pin/wire for distinguishing command from data by transferring 9-bit chunks, where the first bit says the transferred is command or data. That is called 3-wire SPI.

Now back to LCD. LCD’s interface from the application’s point of view is a pointer to the struct lcd_dev_s and its member variables like setpower, which is a pointer to a procedure for setting the LCD’s power. Or getplaneinfo member variable with its own member putrun. ILI9341 device driver provides such an LCD interface and uses 8080 parallel or 3-/4-wire SPI to communicate with the display (hardware).

To communicate with the display (hardware) over 3-/4-wire SPI, the ILI9341 device driver uses SPIIF driver that enables ILI9341 to, for example, sendcmd or sendparam using struct lcddrv_lcd_s. (SPIIF is there just because ILI9341 does not use struct spi_dev_s directly, compared to, e.g., ST7789, which does.)

SPIIF takes commands and data that ILI9341 device driver wants to send to the display (hardware) and sends them via SPI. SPIIF makes use of the struct spi_dev_s and SPI’s procedures SPI_SETBITS and SPI_SEND.

Finally, the implementation of the SPI_SETBITS and SPI_SEND is chip-specific – Kinetis-specific in our case. These procedures write appropriate bits to the appropriate registers, where appropriate is given by the 2117 pages of the K60 Sub-Family Reference Manual K60P144M150SF3RM.pdf.

I am too new to NuttX, so I will just guess that SPIIF, sitting in between ILI9341 device driver and SPI bus driver is a heritage of old days. But maybe not. Anyway, we can see how different layers of abstraction work:

We can consider each interface as a layer of abstraction and each driver as the implementation at that interface/abstraction layer.

It is not hard to guess where to implement 3-wire SPI now. Simplified, we need to update a part of NuttX source code where ILI9341 says “SPI_SEND” and just before that we need to change SPI_CMDDATA(true) to SPI_SETBITS(9). We need to update SPIIF driver.

SPI of Kinetis K60

When SPI is enabled for some Kinetis board in NuttX, there are few things that need to be done in the Board Support Package (BSP):

  1. What pins to use for SPI? We need to define PIN_SPI1_SCK, PIN_SPI1_OUT, and PIN_SPI1_SIN for kinetis_spibus_initialize(1) to work. (We use the pointer to the struct returned by the call to the kinetis_spibus_initialize as the SPI bus driver in our device driver.)

    kinetis_spibus_initialize(1) initializes SPI1. The chip may have multiple SPI interfaces.

  2. What pins to use for Periphery Chip Select (PCS)? We need to define SOME_PCS similar to defines from the kinetis_k60pinmux.h file. Then, we need to kinetis_pinconfig(SOME_PCS) to initialize the pin.

  3. How to select periphery? We need to implement kinetis_spi1select. We need to make SOME_PCS low when the periphery connected via SPI1 should be selected, and high otherwise.

  4. If we enable CMD/DATA in the NuttX’s menuconfig, we would also need to implement kinetis_spi1cmddata. But we consider 3-wire SPI, so we do not need that.

    However, it is at least interesting to consider if and how could we make 3-wire SPI to work by implementing kinetis_spi1cmddata?

Because kinetis_spibus_initialize does a lot, BSP developer can realize in the kinetis_spi1select procedure only. Usually, to select a periphery, there is SOME_PCS definition:

#define SOME_PCS (GPIO_OUTPUT | GPIO_OUTPUT_ONE | PIN_PORTB | PIN9)

SOME_PCS initialization:

kinetis_pinconfig(SOME_PCS);

and kinetis_spi1select implementation:

kinetis_gpiowrite(SOME_PCS, !selected);

where selected is an input argument to the kinetis_spi1select procedure. (!selected is to make PCS pin low.)

I found out that using General Purpose Input Output (GPIO) as Periphery Chip Select (PCS) is a common practice for SPI. However, there is another approach for Kinetis K60.

It is possible to send at most 16-bit chunks of command or data via K60’s SPI. These 16 bits of (command or) data are written to the lower 16 bits of the PUSHR register to be sent via SPI. PUSHR is a 32 bits wide register and upper 16 bits are for the SPI configuration. 6 bits of these upper 16 bits for the configuration specify the PCS, so…

Instead of using SOME_PCS as a GPIO, we can configure it as Kinetis’ PCS:

#define SOME_PCS (PIN_ALT2 | PIN_PORTB | PIN9)

then initialize the pin:

kinetis_pinconfig(SOME_PCS);

and implement the kinetis_spi1select:

if (selected) {
    /* Set bit to 1 for SOME_PCS == 1. */
    putreg32((1 << 1) << 16, KINETIS_SPI_PUSHR_OFFSET);
} else {
    putreg32(0 << 16, KINETIS_SPI_PUSHR_OFFSET);
}

And this is what does not work, because writing 8 or 16 bits to the PUSHR writes all 32 bits (K60 reference manual, 53.3.7), effectively re-using the data to be sent for SPI configuration.

So what happens if 8 or 16 bits are written to PUSHR? Some garbage is used for the SPI configuration. Does it matter? No.

No, because PCS pins are usually GPIO. And considering the other SPI configuration bits…

… we can say no again, unless we care about more control over SPI like precise SPI timing. Or 3-wire 9-bit SPI. Precise timing and the number of bits to be transferred via the SPI is set in CTAR0 or CTAR1. And the CTAS of the PUSHR specify if either CTAR0 or CTAR1 is used.

Let us send Sleep OUT command to the ILI9341 display via SPI. The Sleep OUT command is 0x11.

On 4-wire SPI, we would finally, after layers of NuttX drivers indirections, call putreg8(0x11, 0x4002d034) (for SPI1). That results in 0x10111111 in the PUSHR. So now the CTAR1 is used, but it does not matter, because 8 bits to transfer is the default.

For 3-wire SPI, we can set the number of bits to transfer to 9 using the SPI_SETBITS. We set it for CTAR0 only, because only CTAR0 is used in the NuttX source code for the Kinetis’ SPI. Then, when sending the command, we actually call putreg16(0 << 8 | 0x11, 0x4002d034) when removing layers of abstraction (0 at the 9th bit is for command, 1 is for data). The result of that putreg16 is 0x00110011 in the PUSHR, so CTAR0 is used along with the correct number of bits.

Therefore, there is another no for 3-wire 9-bit SPI.

Precise SPI timing configuration is not implemented for Kinetis’ SPI, which is the final no, it does not matter, it will work anyway. Although, this no is questionable, because there is spi_setfrequency procedure implemented for Kinetis’ SPI, which computes the values to be stored into CTAR0. Beware of sending Sleep OUT to ILI9341 display via 4-wire on an SPI with non-default frequency.

LCD and EEPROM via SPI

Remainder that we want to implement 3-wire SPI for the board so it can communicate with ILI9341 display and 25AA128 EEPROM.

The pseudocode for sending Sleep OUT using the 4-wire SPI is something like:

SPI_SELECT(1, true);   // Select LCD to communicate with via SPI1.
SPI_SETBITS(8);        // Transfer 8-bit chunks, the default.
SPI_CMDDATA(true);     // A command will be sent.
SPI_SEND(0x11);        // Out of the sleep.
SPI_CMDDATA(false);    // No more commands.
SPI_SELECT(1, false);  // No more communication with LCD via SPI1.

For 3-wire SPI, the commands would look like:

SPI_SELECT(1, true);      // Select LCD to communicate with via SPI1.
SPI_SETBITS(9);           // Transfer 9-bit chunks.
SPI_SEND(0 << 8 | 0x11);  // Out of the sleep; 0 ~ command.
SPI_SELECT(1, false);     // No more communication with LCD via SPI1.

where SPI_SELECT and SPI_SETBITS cover SPI configuration (upper 16 bits of PUSHR) and SPI_SEND works with the data to be sent via SPI (lower 16 bits of PUSHR).

For 3-wire SPI with PCS as GPIO, we need to change nothing: SPI_SELECT does not use PUSHR (PCS is GPIO), SPI_SETBITS changes the content of CTAR0 and 0 << 8 | 0x11 writes 9 bits, which is the same as writing 16-bit number 0x0011 to PUSHR (so CTAS is 0, which means CTAR0).

Would we want to use 3-/4-wire SPI with Kinetis’ PUSHR as intended, we would need to introduce something like spi_dev_s’s member variable to store upper 16 bits for PUSHR and use it whenever we write to the PUSHR to send some data. Because we need to write all 32 bits at once.

Also, we would need to introduce some kinetis_... procedures to set the stored upper 16 bits. That would be on the Kinetis’ SPI layer of abstraction. Abstracting above, we would re-use SPI_SELECT macro on the SPI interface layer of abstraction, which would call kinetis_... procedures.

Finally, we would need to re-implement direct memory access (DMA). Because we need to write all 32 bits at once, we would need to prepare a block of memory that contains 32-bit numbers. And we would need procedures that help with the creation. Beware that spi_dev_s’s member variable does not help here – ideally, DMA transfers the user-space block of memory directly to the PUSHR. We should probably start our design around DMA and extend later.

That is quite a lot of decisions to make 3-wire SPI work.

Researching possibilities is good for writing, but does not excuse the lost of the goal; I made the minimum changes needed and sent them upstream:

  1. New defines for Kinetis K60.

  2. Changes to SPIIF to enable 3-wire SPI and tiny changes to ILI9341 initialization.

  3. Add IOCTL to set Block Protect for EEPROM. (Not related to SPI, but needed for BSP.)

Wrap up

It was my first time to work with SPI and Kinetis at such low level; it was my first time to contribute to NuttX. I enjoyed the low-level work. I am not amazed by NuttX.

go back | CC BY-NC-SA 4.0 Jiri Vlasak