Showing results for 
Search instead for 
Did you mean: 

High speed SPI on TIM PulseFinished interrupt?

Associate III

    Hallo everyone,

As a relative newbe in the world of MCU programming, I am searching for an acceptable solution for the fast readout of an ADC chip with a SPI interface. It concerns the MCP33131D/10 ADC (16bit, 1MS/s) to be controlled by a STM32F401CCu Blackpill MCU.

The ADC SPI works slightly different from the standard SPI protocol: the normally used "CS-not" is named 'CONVST' (CONVersion STart), which controls the 'start acquisition' by its rising edge and signals 'data-available' by its falling edge. As this pulse is produced by the MCU, the 'data-available' is not signaled by the ADC, but must predicted by the program (datasheet: >= 1us after the rising edge).

My application should digitize an analog signal every 8us, meaning the CONVST signal must be high during 1 ... 1.5us with a rep-rate of 8us or 125kHz. My first design was the use of a PWM timer signal with a period of 8us and a 1.25us pulse. The interrupt condition 'pulse falling edge' should fire 'PWM_PulseFinished', that invokes the readout of the bytes by 'HAL_SPI_Receive()'.

During my searches on the internet for information around my plan I read a reply of an experienced programmer who stated that even a well-designed ISR will need several hundred clock-ticks to execute. In my case (with a 84MHz clock), this means ~4-5us. So my plan looks feasible, but will require very efficient programming and leaves very little room for other tasks (like exporting the collected data to an USB port). Is this method a way to go or are there better solutions?

I also considered the use of DMA for reading the data from the SPI-bus, but have no clue how to organize this approach in code. The SPI interface must start reading after detection of the falling edge of a Timer PWM signal. After that the two bytes must be transferred to the correct entry in the global data-array. And finally, the program must be informed when the whole array has received new values, so the data export can be started. I think to know a solution for the data dumping (resembles a situation I handled earlier), but I have no idea how to tackle the other tasks.

I sincerely hope there are experienced members of this forum, willing to think with me for a feasible solution for this case. Any advise and/or comment will be highly appreciated,

many greetings from the Netherlands,

Fred Schimmel


Accepted Solutions

Hallo Jan,

Thank you for your honest sermon.

I fully agree with your reasoning that automated systems like CUBEIDE are quite comfortable when one wants to create simple, straight-forward applications without time-constraints. In those cases the invisible overhead is not bothering, maybe some higher power consumption. The constraints of that approach become a constricting harness once one wants something outside the foreseen concept. Plus it is obscuring the way things really happen, what is going on below the hood.

You gave me an additional push to dig deep to explore the inner part of functions and see how they finally end in adjusting register values. But on the other side, there is the wish to get my experiment running, for which the MCU-application is a requirement only. I should find a good balance between learning to program a MCU and making progress on my hobby-experiment.

Thanks for sharing your insights,

Fred Schimmel

View solution in original post


There are multiple ways to achieve this. The interrupt approach you outlined could work with some efficient coding as you say.

There are also ways to achieve this using DMA in the background that will not require CPU resources. Consider the following solution:

  • Set up SPI1 in master mode, 16-bit transfers.
  • Set up SPI1 reception to go into a circular DMA buffer.
  • Set up another DMA to transfer a 16-bit value (any value) into SPI1->DR when TIM1_CH1 goes low.
  • Set up TIM1_CH1 in PWM mode to produce the CNVST pulse at the desired period and duty cycle.
  • Once you start TIM1, this should all occur in the background. Use the half- and full-transfer complete flags/interrupts to process the data in the first and second half of the buffers.

For reference, here is the waveform you need. TIM1_CH1 will be CONVST. SPI1_SCK is SCLK, SPI1_MISO is SDO.



I would recommend working with registers directly for this instead of using HAL. The reference manual will be your friend here.

If you feel a post has answered your question, please click "Accept as Solution".

How much is N?

Besides of the timer method - which is the most flexible - some of these ADCs could be pulled out by I2S; others by SPI in Rx-Only (thus constantly running) in TI mode.


    Hallo Jan,

Thank you for your additions, I certainly will consider the options you proposed. It will take me some time to do the needed coding plus testing, please don't expect a quick update.

Fred Schimmel

Associate III

Dear TDK,

Thanks a lot for your extremely quick and promising reply. As soon as I can find the time I will try to implement the instructions you gave, I think I understand them, but the reality of coding & testing may prove the opposite.....

At this moment I am busy to "undress" the HAL routine 'HAL_SPI_Receive(...)', by stripping of most of the conditionals that are irrelevant because I know the SPI state when the ISR is called.

I certainly will report my results once I have run tests with your proposed scheme. Because I'm a novice in this area of programming, the coding and testing will take quite some time, please be patient...

Fred Schimmel

Associate III

Hallo again TDK and Jan,

@ Jan: I guess your question about N concerned the #bytes to transfer for one cycle (?) A: 4000 uint16_t or 8000bytes.

New request for assistance:
During my quest to deduce the most efficient handling for the 125kHz interrupt, I encountered device-instance specific interrupt handler function 'void HAL_TIM_IRQHandler(TIM_HandleTypeDef *htim)' in the driver file 'stm32_fxxx_hal_tim.c' (generated for my project by CUBEIDE). This function is quite complex because it has to detect which callback function should be called. I have the foreknowledge there is only one ISR for a specific timer, so this function can be significantly reduced to "if (htim->Instance == TIMx) { ISR_Tim_x;}". But (if I understand well), a) a redefine of a function is not allowed and b) my modified code will be overwritten when the 'Device Code Generation' tool is run.

Is there a way to bypass these limitations?

Some explanation:
I have seen several experienced members detesting the use of CUBEIDE, because the overhead it produces to cover all possible situations. They preach coding by registers. I agree with this reasoning, but as novice in MCU programming I am unable to create a working application, with all basic tools of an OS, etc.
In this case I want to try to modify the code for time-critical tasks, to reduce the number of instructions.

I hope you (or someone else) can advise me,
thanks in advance,

Fred Schimmel


The IRQ handlers will have names something like TIM5_IRQHandler, but it varies based on the peripheral. Look in the end of the startup_*.s file for all of the IRQ handler names. HAL_TIM_IRQHandler is only called if you call it from within one of those IRQ handlers, it isn't called directly.

If you feel a post has answered your question, please click "Accept as Solution".
Associate III

Hallo TDK,

Thanks for responding so quickly (again).

In fact, I have seen the code you mention (see added file), it is in the file 'stm324xx_it.c', which is generated by the 'Device Code Generation' tool. Adapting this function looks to be the simplest way to reduce the quantity instructions (prevent the call of 'void HAL_TIM_IRQHandler(TIM_HandleTypeDef *htim)' with lots of instructions), but it is not allowed to re-define a function (compiler or linker complaint). Plus the modification will be overwritten the next time the 'Device Code Generation' tool is run.

The essence of my question was advise how to bypass these limitations.

Thanks anyway for your help, I really appreciate it!

Fred Schimmel


Yes, you need to edit those functions to suit your needs, or delete them from that file and redefine somewhere else.

You can put your code followed by "return;" in the first user code section to skip calling HAL and avoid having it delete anything upon regeneration.

If you feel a post has answered your question, please click "Accept as Solution".

> it it is not allowed to re-define a function (compiler or linker complaint). Plus the modification will be overwritten the next time the 'Device Code Generation' tool is run.
> The essence of my question was advise how to bypass these limitations.

Well, the obvious solution is not to use the Cubes at all.

<preach mode on>

Yes, it's preaching, but, sorry, that's also the naked truth. If you can't click exactly what you want in CubeMX, it will get into your way, and the more you want something less "usual" (as deemed by Cubes authors), the more it will be an obstacle rather than help.

I see where the lure of "quick" is, but there's no free lunch, and abandoning the easy path which is ultimately a dead end is way more painful than to bite the pill and do things properly from the start.

In this particular case, you have to learn, how exactly handles interrupts (which is designed to simplify usage at the cost of being cumbersome; and description of which btw. is also in Cube's manual, have you read it?), just to try to somehow coax it into doing less, then ultimately learning how to write proper ISR and rewriting it from scratch - what you could've learned at the first place. So now you not only had to learn and cope with both Cube's ways and the proper way, but also all things in between and their interactions. So what exactly did you gain?

No, proper programming is not easy, not simple, and does not guarantee results. That would be silly to expect. Contrary, it's all tears, blood, toil and sweat. And ST makes it harder by refusing to provide concise examples beyond the clicky. The keyword here is control (as in microcontroller).

So, ultimately, you do you.

<preach mode off>

There's plenty of "how to avoid CubeMX removing my own code" posts around in this forum (guess why); do your own search. One obvious way is to make proper backups before you do the "regenrate" things; the other is to do that only once, at the beginning, and then never again.

> How much is N?

I meant in the waveform TDK posted. The particular number influences whether what I've wrote about SPI-in-TI-mode and/or I2S may or may not be applicable. I am not bothered looking up myself, sorry.