In this final article of three, we're going to make our DMA-based ADC example from the second article run off a timer, and we'll do a small demonstration of how this might be used in a realistic application.

Timers on the STM32F767

The STM32F767 has a lot of timers. They come in five different varieties:

  • "Advanced-control" timers (TIM1, TIM8);
  • 32-bit general-purpose timers with 4 channels (TIM2, TIM5);
  • 16-bit general-purpose timers with 4 channels (TIM3, TIM4);
  • 16-bit up-counter timers with 2 channels (TIM9, TIM12);
  • 16-bit up-counter timers with 1 channel (TIM10, TIM11, TIM13, TIM14);
  • 16-bit basic timers (TIM6, TIM7).

The different timer types have different features (different PWM modes, for instance). For our purposes, the most important feature is the ability to trigger other peripherals, in particular, the ADC. The timer outputs that can be used to trigger ADC conversion are listed in the EXTSEL external event select field of the ADC's CR2 configuration register:

ADC trigger sources

Here, "TRGO" means the trigger output from the timers, which can be set up to be activated on a number of different timer-related events. In conjunction with the possible settings in the EXTEN field of CR2, this allows us to cause ADC conversions to be triggered on different edge transitions for any of these trigger inputs:

ADC trigger flags

We're going to use timer TIM2 for our examples. This is a 32-bit general-purpose timer that supports various counter modes, as well as having features for PWM waveform generation and timer capture. We're going to use the timer in its simple up-counter mode, generating a trigger pulse on the TRGO trigger output when timer counter overflows and reloads. This will generate a regular trigger pulse that we can connect to the ADC trigger input.

(The STM32 microcontrollers tend to use ad hoc mechanisms for inter-peripheral connections like this. In this case, we have a specialised set of configuration assignments we can do for the ADC peripheral to make it trigger on a signal from a selected timer. Some other ARM microcontrollers have a more systematic way of dealing with these kinds of peripheral interconnections. For example, the Nordic Semiconductor nRF52840 has a whole events and tasks infrastructure for each peripheral, along with a dedicated programmable peripheral interconnect system to wire events in one peripheral to tasks in another peripheral. You can wire up a timer event to the "sample" task of the ADC to do regular sampling. But using this setup, you could also do a whole range of completely different things.)

Example 4: timer-triggered ADC conversions

In example ex4.c, we're going to sample data from the ADC at 1 Hz, collecting 4 channels of ADC data into a buffer using DMA.

DMA configuration

DMA configuration is completely identical to the previous example. We use the same DMA modes, we're moving 4 half-word data items from a peripheral to memory, and so on. And we use the DMA transfer complete interrupt to detect when the ADC conversions are complete.

ADC configuration

The ADC configuration for this example differs from the previous example in only one way, and that's the way that ADC conversions are started. Everything else is the same: the number and order of channels to be converted, channel sample times, and so on.

For the previous example, we set the EXTEN field of ADC register CR2 to zero, which disabled external triggers for the ADC. That meant that we started ADC conversions manually by setting the SWSTART bit in the CR2 register. This time, we set the EXTEN and EXTSEL fields of the CR2 register to start ADC conversions on the rising edge of timer TIM2's TRGO trigger output:

  MODIFY_REG(ADC1->CR2, ADC_CR2_EXTSEL, 0x0B << ADC_CR2_EXTSEL_Pos);
  MODIFY_REG(ADC1->CR2, ADC_CR2_EXTEN, 0x01 << ADC_CR2_EXTEN_Pos);

Here, the 0x0B value for EXTSEL selects TIM2 TRGO as an external event, and the 0x01 value for EXTEN triggers ADC conversion on the rising edge of the trigger input.

Timer configuration

Configuring timer TIM2 to generate a trigger output pulse once every second requires a few steps. All timers run off a clock, and essentially just count clock pulses. So the first thing we need to know is how fast those clock pulses occur for the timer we're using.

In its up-count mode, the TIM2 timer we're going to use can be thought of as a simple counter clocked off the timer's input clock divided by a prescaler value. We can set the prescaler (so controlling how fast the counter counts up) and the reload value (controlling how often the counter overflows and a trigger event occurs).

1. Determine timer input clock frequency

Timer TIM2 is on the APB1 peripheral clock, so the clock input can be determined by looking at the APB1 prescaler value in the PPRE1 field of the RCC->CFGR register:

  uint32_t apb1_prescaler = READ_BIT(RCC->CFGR, RCC_CFGR_PPRE1);
  uint32_t timer_clock_frequency =
    SystemCoreClock >> APBPrescTable[apb1_prescaler >>  RCC_CFGR_PPRE1_Pos];
  if (apb1_prescaler != RCC_CFGR_PPRE1_DIV1) {
    timer_clock_frequency *= 2;
  }

Here, SystemCoreClock is the main system clock frequency maintained by the CMSIS board-support code, and APBPrescTable is a table of APB prescaler factors that the PPRE1 field in RCC->CFGR encodes. (There is a small wrinkle here, in that the timer input clock frequency has an extra factor of 2 if the prescaler factor is not unity. This is shown in the main clock tree for the processor in Figure 13 in the reference manual.)

For our case, with SystemCoreClock running at the maximum 216 MHz rate, the timer input clock frequency is 27 MHz (the APB1 prescaler is set to divide the system clock by 8 in the common setup code).

2. Determine time base prescaler and reload value

The timer prescaler register TIM2->PSC determines the relationship between the timer input clock (27 MHz) and the update rate of the timer's counter.

Timer prescaler register

Even though TIM2 is a 32-bit timer, the prescaler is 16 bits wide. This means we can have update frequencies ranging over:

  • PSC = 0 giving CK_CNT = 27 MHz / 1 = 27 MHz;
  • PSC = 0xFFFF giving CK_CNT = 27 MHz / 65536 = 411.987 Hz.

All other things being equal, it can be a good idea to choose the smallest possible prescaler value that you can. This gives a timer counter that updates as fast as possible, giving the finest possible time resolution to the timer. Whether or not this is actually the best thing to do depends on exactly which timer you're using.

In our case we're using a 32-bit timer. That means that if we use a prescaler of one, then the timer counter updates at 27 MHz, and the longest interval that we can time is 232 / 27,000,000 = 159 seconds. That's probably enough for most applications. However, if you were using a 16-bit timer, the longest interval would only be 216 / 27,000,000 = 2.4 ms, which is less useful! If you're using a 16-bit counter, you'd almost certainly want to use a larger prescaler value to slow down the clock driving the timer's counter, allowing you to measure longer intervals.

Let's choose a prescaler value to make calculations simple, by having the timer counter update at 1 MHz. This is 1/27 of the timer input clock frequency, so we need to set:

  uint32_t timer_prescaler = 27;

which we'll use to set the TIM2->PSC prescaler register.

We then need to work out the timer reload value we need to get trigger events at the required frequency. The timer counts up from zero to the value in the ARR reload register, then resets to zero. When the timer reloads its counter to zero, it generates an update event, which is what we use to pulse the TRGO trigger output.

The reload value is calculated as:

  uint32_t timer_reload =
    timer_clock_frequency / (timer_prescaler * TIMER_FREQUENCY);

Here, TIMER_FREQUENCY (1 Hz) is the desired timer update frequency. You can see that this makes sense by thinking about the calculation for a couple of indicative frequencies.

For a frequency of 1 Hz, with a 27 MHz input clock and a prescaler value of 27, we have that timer_reload = 1,000,000, i.e. we count up 1,000,000 pulses of the 1 MHz prescaled clock to give a total period between timer reloads of 1 second.

For a frequency of 100 Hz, again with a 27 MHz input clock and a prescaler value of 27, we have that timer_reload = 10,000, and we can count up to 10,000 one hunder times in the one second that it takes the 1 MHz prescaled input clock to count up to one million, i.e. we generate reload events 100 times per second.

3. Configure timer

As for other STM32 peripherals, the peripheral clock for the timer must be enabled, by setting the TIM2EN bit in the RCC->APB1ENR register.

The main part of the timer configuration consists of writing the appropriate values into the prescaler and reload value registers:

  WRITE_REG(TIM2->PSC, timer_prescaler - 1);
  WRITE_REG(TIM2->ARR, timer_reload - 1);

setting the timer mode (simple up-counter):

  MODIFY_REG(TIM2->CR1, (TIM_CR1_DIR | TIM_CR1_CMS), 0);

and enabling the TRGO trigger output on update (i.e. counter reload) events:

  MODIFY_REG(TIM2->CR2, TIM_CR2_MMS, 0x2 << TIM_CR2_MMS_Pos);

In the ex4.c code, we also enable timer interrupts for debugging purposes, but it's not normally necessary to observe these interrupts: the trigger output from the timer starts the ADC conversion without any interrupts needing to be generated or serviced.

4. Enable timer

Once the timer is configured, it needs to be enabled by setting the CEN bit in the timer's CR1 configuration register:

  SET_BIT(TIM2->CR1, TIM_CR1_CEN);

One feature of the counters on the STM32 devices that requires some care is that register settings for things like prescaler and reload values are loaded into "shadow registers", and only become active on a timer update event (essentially a roll-over when the reload value is reached by the timer's counter). This prevents timer glitches when prescaler and reload values are modified while the timer is running.

When the timer is not running, reloads of the new configuration values need to be forced by manually generating a timer update event. This is done by writing to the timer's EGR event generation register:

  SET_BIT(TIM2->EGR, TIM_EGR_UG);

This generates a timer update, and the timer starts running at the configured rate, generating trigger events each time the timer's counter reaches the reload value.

Main program

As for the other examples, the main program for ex4.c is very simple: just a super-loop that examines some global flags set by interrupt service routines to detect DMA completion and timer events (for debugging). When a DMA transfer completes, the ADC conversion results are retrieved and sent out over the USART.

When this example is flashed to the Nucleo-144 board, ADC conversion results are written to the ST-Link's virtual serial port once per second. This is the result that we wanted to achieve!

Here's a quick recap of what's happening here:

  1. The TIM2 timer is running off the 27 MHz APB1 timer clock, using a prescaler of 27, so that the timer count register is updating at 1 MHz.

  2. When the TIM2 counter register values reaches 1,000,000 (the value in the ARR reload register), the timer counter is reset to zero.

  3. When the TIM2 timer resets its counter to zero, it generates a trigger pulse on its TRGO output. These trigger pulses occur at a frequency of 1 Hz.

  4. The TIM2 TRGO output is connected to the external trigger input of ADC ADC1. When a trigger pulse is generated by timer TIM2, the ADC starts its regular channel conversion sequence.

  5. The contents of the ADC1->SQRx registers cause the ADC to convert four analog inputs one after another in scan mode.

  6. The ADC is enabled for DMA transfers: after each ADC conversion is complete, a DMA transfer is initiated on stream 0 on DMA2.

  7. The DMA2 DMA controller transfers the latest ADC conversion result to the memory buffer configured in the DMA setup.

  8. After four ADC conversions, the DMA controller raises a "transfer complete" interrupt to indicate that it has performed all four transfers for which it was configured.

  9. In the ISR for the the DMA's "transfer complete" interrupt, we set a flag that is observed in the main program loop and is used to process the ADC samples collected into our buffer.

Apart from the final processing of the buffered results in response to the DMA interrupt, all of this happens without any action from the main processor core.

Example 5: wannabe USB oscilloscope

So we can do the thing we wanted to do, but it's a bit of an anticlimax. We're collecting data on a regular timer, but we're not doing much with it. That seems boring, so I decided to knock together a (completely useless) USB oscilloscope demonstration. This comes in two parts:

STM32 data capture program

The STM32 data-capture part of the code is in ex5.c. This is identical to ex4.c, except that the data is written to the USART as binary values (instead of being formatted to ASCII strings) and the sample rate is higher (500 Hz).

Python mini-oscilloscope application

The display side of the code, which runs on a PC, is written in Python, and is ex5-view.py. This uses the pyserial library to read data from the USB virtual serial port, and the pygame library to do very simple data plotting and display.

This can't be considered anything more than a very little demonstration, but it does work, it shows how easy it is to make these kinds of small and simple data display applications.

Experimental setup and screenshots

Here, I set up an Analog Discovery 2 as a waveform generator, connected its output channel to the first ADC input I'm using, and used the Python code to display the captured waveforms.

Here's a 50 Hz sinewave:

Here's a triangle wave:

Here's a more complex composite waveform:

Conclusions

You could definitely set all this up more quickly using the tools in the STM32Cube IDE and using the STM32 HAL hardware abstraction layer libraries, but I bet you wouldn't end up understanding as much about how peripherals work together in STM32F7 MCUs!

Learning this stuff is unfortunately quite a bit of work, as you have to spend a lot of time reading the processor reference manual and wading through descriptions of lots of registers and lots of modes of operation of the peripherals. But I do think it's worthwhile going through things this way at least once for a few different setups on your MCU before you dive into using the HAL.