Properly using the SPI peripheral to drive a screen
Posted: Fri Feb 02, 2024 7:34 pm
Hello everyone. I'm writing a driver for a 480x320 screen. I guided myself through the examples and managed to get it working to surprisingly good results, but I was wondering if there's a better way to handle this. Right now, I'm using the "spi_device_polling_transmit" function to send 10 480 pixel rows at a time. Which takes about 1000 us to update each row. Which makes sense because since it's 16 bits per pixel, 480 pixels per row, 10 rows per transaction and a clock of 80 MHz, it gives me a total time of:
16*480*10/80 = 960 us per block of 10 rows
Now, I tested that with the gptimer and it actually gives me very close results, each block taking between 980 us and 1010 us (But mostly close to 1000 us), so it makes sense to me, considering aditional delays from the execution, including the handling of the timer itself. The timing worked like this (DebugTimer is an object of a hardware timer class I wrote with many functionalities):
Then, I was wondering if I could make it more efficient somehow and read about the "spi_device_queue_trans" function. I simply replaced "spi_device_polling_transmit" with "spi_device_queue_trans" again and got that each transaction now takes a muuuuuch lower time, about 45 us each block, some a bit longer but every one was below the 80 us mark. Now, however, if I start timing before calling "spi_device_acquire_bus" the first ones take the same time, but after a couple the timing goes to over 800 us, and then it goes back down again.
My conclusion is that "spi_device_queue_trans" buffers the transaction and the SPI peripheral is in charge of sending everything to the device. I read something about that in the esp-idf documentation, but I had asumed that I needed to write code in an ISR to handle the transaction, however this is surprisingly easy. And the reason why it takes little time at first, then longer, and then little again when I start timing before acquiring the bus is that some kind of buffer gets filled with transactions so I just can't buffer them anymore and have to wait.
Now... there are some things I don't fully understand. I don't have any problem if the refresh of the screen takes a few ms longer, but I do have a problem if the CPU is blocked for a few ms trying to send data to the screen.
Is an ISR called by default by the esp-idf framework that does all transactions? If so, is that ISR blocking the execution of other FreeRTOS tasks for a long time? Or are they short lived? From what I read in the esp-idf, a polling transaction keeps the CPU busy, so from what I understand other tasks won't be executed until the polling ends.
This is proving to be a bit confusing to me. I don't mind waiting for the transaction to complete in the task that handles the transaction. But I want other tasks to be able to run while a transaction is ongoing, even if it means refreshing the screen takes longer.
Is using "spi_device_queue_trans" and making sure no two different tasks try to access the same SPI device all I have to do to achieve that? I'm sorry if I'm not being clear, I'll keep an eye out on the comments. Thanks!
EDIT: I forgot to mention, if the reason why after some transaction it takes longer when I also time "spi_device_acquire_bus" is that some buffer is full, and the function blocks the task until that buffer is freed... how does it know that the next transaction will fit in the buffer? Or if that were to happen the function "spi_device_queue_trans" would block the task?
16*480*10/80 = 960 us per block of 10 rows
Now, I tested that with the gptimer and it actually gives me very close results, each block taking between 980 us and 1010 us (But mostly close to 1000 us), so it makes sense to me, considering aditional delays from the execution, including the handling of the timer itself. The timing worked like this (DebugTimer is an object of a hardware timer class I wrote with many functionalities):
- spi_transaction_t Transaction = {};
- //Transaction struct config
- Transtaction.foo_ = foo;
- Transtaction.foo1_ = foo1; Transtaction.foo2_ = foo1;
- spi_device_acquire_bus(...);
- DebugTimer.Tic();
- spi_device_polling_transmit(...);
- printf("Time: %lu us\n", DebugTimer.TocUs());
- spi_device_release_bus(...);
My conclusion is that "spi_device_queue_trans" buffers the transaction and the SPI peripheral is in charge of sending everything to the device. I read something about that in the esp-idf documentation, but I had asumed that I needed to write code in an ISR to handle the transaction, however this is surprisingly easy. And the reason why it takes little time at first, then longer, and then little again when I start timing before acquiring the bus is that some kind of buffer gets filled with transactions so I just can't buffer them anymore and have to wait.
Now... there are some things I don't fully understand. I don't have any problem if the refresh of the screen takes a few ms longer, but I do have a problem if the CPU is blocked for a few ms trying to send data to the screen.
Is an ISR called by default by the esp-idf framework that does all transactions? If so, is that ISR blocking the execution of other FreeRTOS tasks for a long time? Or are they short lived? From what I read in the esp-idf, a polling transaction keeps the CPU busy, so from what I understand other tasks won't be executed until the polling ends.
This is proving to be a bit confusing to me. I don't mind waiting for the transaction to complete in the task that handles the transaction. But I want other tasks to be able to run while a transaction is ongoing, even if it means refreshing the screen takes longer.
Is using "spi_device_queue_trans" and making sure no two different tasks try to access the same SPI device all I have to do to achieve that? I'm sorry if I'm not being clear, I'll keep an eye out on the comments. Thanks!
EDIT: I forgot to mention, if the reason why after some transaction it takes longer when I also time "spi_device_acquire_bus" is that some buffer is full, and the function blocks the task until that buffer is freed... how does it know that the next transaction will fit in the buffer? Or if that were to happen the function "spi_device_queue_trans" would block the task?