spi_write_*() should disable interrupts when used with hardware chip select and CPHA=1 #3141
Labels
No labels
bazel
bluetooth
board settings
boot_stage2
bug
build
c++
can't reproduce
clang
clib-interface
cmsis
code cleanliness
code size
compiler version issues
cyw43_driver
debug
discussion
documentation
duplicate
dynamic_clock
elf2uf2
enhancement
errata
filesystem
flash layout
floating point
FreeRTOS
hardware_adc
hardware_claim
hardware_clocks
hardware_dma
hardware_exception
hardware_flash
hardware_gpio
hardware_i2c
hardware_irq
hardware_pio
hardware_pwm
hardware_rcp
hardware_rtc
hardware_spi
hardware_sync
hardware_uart
hardware_vreg
hardware_watchdog
hardware_xosc
help wanted
host
IAR
installation
invalid
linker scripts
lto
lwip
mbedtls
missing API
needs reporter detail
networking
newlib
newlib-multicore
non-c
non-cmake
non-gcc
openocd
orphan PR
OS integration
pico_async_context
pico_base
pico_binary_info
pico_flash
picolibc
pico_lwip
pico_malloc
pico_multicore
pico_printf
pico_runtime
pico_stdio
pico_stdio_usb
pico_sync
pico_time
pico_util
pioasm
port
question
review
review2
rtos interop
runtime init
static library
svd
test
tinyusb
vscode
wifi integration
wontfix
No milestone
No assignees
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set
Reference
gh-raspberrypi/pico-sdk#3141
Loading…
Reference in a new issue
No description provided.
Delete branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
For context I am using SPI mode 3 with hardware chip select.
According to the datasheet, in this mode (and generally CPHA=1) the hardware chip select is toggled around the entire transfer as oppossed to per byte.
The chip select gets asserted (goes low) when the TX FIFO goes from empty to non-empty, and it gets deasserted (goes high) when the TX FIFO empties.
But I had the odd data corruption every ~1000 5 byte transfers or so. I've narrowed it down some 5 byte transfers getting broken up randomly mid way through with a logic analyzer.
After some more code debugging it turns out my USB ISR was firing mid-way the SPI transfer causing the TX FIFO to run empty and the chip select to deassert... The following code sequence fixed my issue:
I am not sure doing this internally in the SDK is the right approach, but the user should at the very least be warned about this possibly happening.
Yeah, I guess the SDK has no way of knowing if the SPI transfer is more important, or responding to the USB interrupt is more important 😉
Although the comment at https://github.com/raspberrypi/pico-sdk/blob/develop/src/rp2_common/hardware_spi/spi.c#L88 suggests that this code is (lightly) interruptible, so does this mean that your USB ISR is taking more time to run than it takes for the SPI FIFO to drain? Is there any way you could speed up your USB ISR? Or maybe you could have the SPI-code running on one core and the USB-code running on the other core?
It is not just about importance, I would actually be fine with the SPI transfer as a whole being delayed.
The problem is the SPI transfer will simply have incorrect framing in this scenario, and devices using the CPHA=1 chip select scheme, they certainly expect the entire transfer to be part of one CS pulse.
I am using TinyUSB here which I don't believe has a particularly heavy weight or uncommon USB implementation :)
I am also running fairly high speed SPI, the SPI hw transferring one byte is measured in single-to-double digit CPU cycles here, so anything but USB ISR written as few lines of hand-written ASM interrupting the SPI transfer will result in the same issue.
If you do disable interrupts around SPI activity, I guess you'd want them disabled for as short a time as possible (to reduce the chance of missing your USB interrupts) ?
So given that the SPI hardware has an 8-deep FIFO, and you're only transmitting 5 bytes at a time, I wonder if you could create a custom version of
spi_write_read_blockingwhich disables interrupts, writes all 5 bytes tospi_get_hw(spi)->dr(which loads them into the transmit FIFO), then re-enables interrupts, and then reads all 5 bytes out of the receive FIFO (as they become available) by reading fromspi_get_hw(spi)->dr? Because presumably once all the bytes are in the SPI hardware's FIFOs, it doesn't matter if the CPU gets interrupted or not? 🤔 (This is a bit of a stab in the dark, from having had a quick browse through https://documentation-service.arm.com/static/5e8e3bc7fd977155116a9361 and https://github.com/raspberrypi/pico-sdk/blob/develop/src/rp2_common/hardware_spi/spi.c )I might have gone slightly overbroad by also putting the receive part inside the interrupt disable block but the function below does seem to resolve my problem of the transfer being broken up:
Oh! If you're not actually using the returned value, why was your original code using
spi_write_read_blockingrather thanspi_write_blocking? 🤔 (But of coursespi_write_blockingwould still have the same problem, if interrupted while writing tospi_get_hw(spi)->dr)I guess you could also move the bit-shifting and masking to be before the interrupt disable block, if you really need to reduce the amount of time that interrupts are disabled for 😉
@lurch Oh sorry I have another operation that is 6 bytes called read_register that does use the returned value, it is almost identical besides that.
I think this would be a bit of a footgun for other users; disabling interrupts for (potentially) thousands of cycles would be fatal for anything that needs tightly bounded IRQ latency.
You can avoid the CSn deassertion by using a software-controlled chip select. That's one of the reasons software CSn is commonly used in practice, even though it's not pretty. You could also consider using DMA, so that processor IRQs don't interrupt the data flow.
Out of interest what was the reason for using the hardware chip select?
We don't provide DMA accessors for SPI because it's hard to write one that covers all use cases without re-implementing most of the DMA API, but it doesn't take much code. For an example see here in the MicroPython source:
github.com/micropython/micropython@1c3c201149/ports/rp2/machine_spi.c (L280-L320)@Wren6991
Yeah I am not 100% sure what solution (if any at all) could be good here. With software chip select (or I guess even the CPHA=0 behavior) gaps between bytes in the transaction do not really matter, obviously disabling interrupts unless necessary is heavy handed.
Why I reported this is because the SPI transfer API feels broken when combined with hardware chip select and the CPHA=1 behaviour, or at least it is a bit of a footgun.
I did use software chip select originally, and it does work for my application. But to get maximum performance I switched to hardware chip select.
Basically what's going on here is few non-critical
read_reg()andwrite_reg()operations happen first to program the flash controller for page read or page write, and then a sequence of identicalread_reg()operations yield 512 bytes of data, unfortunately 4 bytes per each CS assertion.To get maximum performance out of this at a given frequence, I found that hardware chip select both reduces the front and back porch of each transfer, and also allows me to fire off
read_reg()operations with as small delay between them as possible.I can use DMA for the bulk part of above I suppose, but I still need the ability to do individual non-DMA
read_reg()andwrite_reg()operations to not have to do the whole DMA setup for each single setup operation. Disabling the interrupts manually for those works for me for those I was just pondering about the API behaviour.