Writing a NuttX SPI Driver for the BCM2711
Recently, I wrote an SPI driver for the BCM2711 as part of my ongoing effort to introduce Raspberry Pi 4B support into the NuttX kernel. While the implementation was relatively straightforward just using the BCM2711 documentation, I encountered some difficulties that weren't immediately easy to figure out. Since the information about the BCM2711 is somewhat minimal, I wanted to document some of my findings here for others.
The actual pull request where I made these changes can be found here . I only concerned myself with the implementation of the non-auxiliary SPI controllers, since I only wanted an initial basic implementation (and the regular SPI interfaces outnumber the auxiliary ones). When I wrote this driver I also started by first implementing a polled version of the driver before attempting an interrupt-based driver. The PR does not reflect this, since I just rebased all my changes into a single commit before posting them for review. The polled implementation was meant to reduce the amount of debugging headaches by worrying about concurrency/maintaining state between interrupts.
As mentioned, the SPI driver is relatively straight-forward since the
way the BCM2711 implements the peripherals is straight-forward as well.
You mark a "transfer active" bit and start writing data directly to the
FIFO register. When you notice that the receive FIFO is getting full (by
checking the
RXR
bit), stop transmitting and read some data into your receive buffer
until you've drained the FIFO (
RXD
bit). Rinse and repeat until you're out of data, then mark the transfer
as being over. It's also roughly equivalent between interrupt calls as
well, and
the BCM2711 peripheral datasheet
actually has a short flow description of how to implement your driver
for polled, interrupt and DMA driven modes.
I won't claim that my implementation is the most efficient. For
starters, it doesn't use DMA. I also lost a little bit of an
optimization in my interrupt implementation, since my
spi_fill_txfifo
function only fills the transmit FIFO until the receive FIFO is ¾ full.
In reality, at the beginning of the transfer, we know that we can
transmit until the receive FIFO is entirely full before reading it. I
just wanted a working product to play with, and NuttX is a very
minimalistic RTOS which typically runs on much lower power devices. I
reasoned that a small SPI optimization on something as powerful as a
4-core Cortex A processor is not worth the time when I'd probably just
go back and re-write the driver to use DMA later.
To test this driver, I hooked up the Pi to my MCP3008 SPI-controlled ADC and tested the NuttX driver I had written for that device before. After a couple bug-fixes, I had something that worked very well. At least, I thought it worked very well, because I had performed all my tests on SPI0. Since the driver worked there, I figured it would work for all of the identically implemented SPI interfaces, SPI3 through 6. I added in the additional code for configuring which SPI interfaces to enable and what GPIO pins to use for the ones with more than one option. However, when I went to test on another SPI interface, I found that nothing there worked. My pull-up/pull-down resistors were being set properly, but no signals were being sent on the interfaces despite the GPIO pins being configured for the correct alternative function.
After much debugging and a lot of researching, I learned that the
Raspberry Pi 4B device tree is used by the (proprietary) bootup firmware
to initialize peripherals on the device. It has the option to load
additional device tree overlays by specifying
dtoverlay=name-of-overlay
in the
config.txt
file. You also need to include the compiled overlay file on the SD card
under the
overlays
directory. It took me a while to figure that out, since the boot
firmware does not report an error if it can't find the overlay file, it
just says it opened the overlay file regardless. If you load it
successfully though, it displays an additional success message. Odd
choice. All of the overlays are available in the
raspberrypi/firmware
GitHub repository. It has a
README
file with all of the overlays listed and what each of them do. So, I
tried to load the
spi4-1cs
overlay to try my driver on the SPI4 interface. However, it still didn't
work. As it turns out, the BCM2711 startup firmware doesn't do
everything that's required for the overlay to work, it only does some
basic house-keeping. The real magic is the Raspberry Pi Linux kernel,
which gets passed the overlaid device tree and is then responsible for
setting up peripherals. I suppose each of the SPI interfaces has some
separate enable register that I would need to modify, but that isn't
documented in the BCM2711 peripheral data sheet. Instead, I would have
to reverse-engineer the Linux code to see what's required. I decided
that would be a task for later, since there is a lot to sift through
there and I was not interested in delaying the initial support for that.
I'll likely take a look at setting up the extra peripherals (those not included in the base device tree for the Pi 4B) after I re-visit the I2C driver for the Raspberry Pi. I found a YouTube channel called Low Level Devel which has some videos detailing some basic driver code for the BCM2835 (older Pi processor), and I gather the I2C interfaces are identical or at least very similar to the BCM2711. I'll see if I can take the driver further this time around, and hopefully I won't get hung up on the no-stop and no-start options to the NuttX upper-half driver (the BCM2711 doesn't make that readily easy to implement).