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:
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, containssetpowerprocedure as an answer to the How to set the display’s power? question. Then, thesetpowerprocedure expects thepowerargument, 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.
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 have a K60 chip and its reference manual, so we know which bits we need to set to which registers to make the chip communicate via SPI. However, there are many other chips and many uses for SPI. Therefore, we want to abstract the commons to an SPI interface and then implement the SPI bus driver for a specific chip.
Having SPI interface and its implementation in the form of SPI bus driver, we could implement ILI9341 device driver using the SPI bus driver. But maybe there is something common to the communication via 8080 parallel interface and 3-/4-wire SPI, so we abstract that to some interface and let SPIIF to be the implementation of that interface for the SPI.
Having that interface and its implementation in the form of SPIIF (neither device nor bus) driver, we can implement ILI9341 device driver using SPIIF driver. ILI9341 itself is an implementation of the LCD interface that is abstracted over other displays, like ST7789.
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.
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):
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.
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.
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.
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…
CONT): PCS signals remain asserted between
transfers.CTAS): 000 for CTAR0, 001 for
CTAR1, the rest is reserved.EOQ): Current SPI transfer is the last.CTCNT): Clear the transfer counter
TCNT.PUSHR are for data.)… 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.
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:
New defines for Kinetis K60.
Changes to SPIIF to enable 3-wire SPI and tiny changes to ILI9341 initialization.
Add IOCTL to set Block Protect for EEPROM. (Not related to SPI, but needed for BSP.)
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