Millisecond delay on the STM32F103

Controlling the timer peripherals on the STM32F103 chip can be quite daunting because of the large number of ways in which the timers can be set up and used. However, going to the effort to understand the hardware timers is well worth the effort, as there is so much you can do with the timers, from running servo or stepper motors, to generating delayed pulses on an input trigger, to timing pulse durations to drive an ultrasonic transducer. In this post I thought we would try something relatively simple, while still being useful: a hardware delay word. This is a good way to get the basic idea for how a timer should operate.

Mecrisp does not contain a hardware delay word like us for microsecond-scale delays or ms for longer delays. We can simulate it in hardware by running through an empty DO...LOOP data structure. On my STM32P103 board, a delay caused by counting to around 12000 is enough for a 1-ms delay. But this is imprecise, and system dependent, and also unnecessary when the microcontroller has a hardware timer.

The STM32F103RB has 1 advanced control timer, TIM1, and three other general-purpose (GP) timers (TIM2-TIM4). There isn’t a lot of difference between these timers, although the advanced timer has both the normal output and its complement, whereas the GP timers have only a single output. Other chips in this family also have simple timers with very basic functionality, and if this chip had such a timer we would use it, but it doesn’t, so in this case we are going to achieve our delay with TIM4, one of the general-purpose timers. I could have done this with any of the timers, but the delay is probably best done with the timer you might otherwise use last, so you still have the one advanced timers, and two GP timers for other timing tasks.

To do what we need to do with the general-purpose timers, we first need to have the bit-setting words described in this post: General Forth Words for GPIO On The STM32F103. We will be using the set_bits word to set or reset the appropriate bits on the timer register. So if you have not looked at that article, take the time to do it now, and load the words described there into Mecrisp Forth and Save them to flash, as you will need them to do what I describe below.

What Timers Do

A timer is mostly just a combination of a clock and a counter, with logic that tells the timer what to do when the counter reaches certain pre-set values. All the counters on the STM32F103RB chip are 16-bit counters, meaning that they can count from 0 to 65535. They are able to drive GPIO pins once the counter has reached the pre-set values, or they can start or stop counting when a GPIO pin has changed its state. This allows counters to time input pulses and to generate output pulses with a given duration.

Some of the Chips in the ST family have precision timers with 32-bit resolution for high-resolution timing applications. We won’t discuss them here.

The Important Timer Registers

A general-purpose timer timer has many registers, as outlined in the chip’s manual. Here we refer to the general operations of timers the way the manual does, so TIMx refers to any of TIM1, TIM2, TIM3, TIM4. When searching through the manual, refer to TIMx rather than the individual timer you are interested in. It’s important to note that the Advanced, GP and Simple timers each have their own separate chapters in the chip manual – don’t look at the advanced timer chapter if you are looking at the GP timers! Thankfully, most of the registers are the same for the different types of timers, so most of the information described below also applies for the advanced timers. But there are some small differences in places, so it’s best to look in the appropriate chapter for the timer you are using.

For basic operation, these are the important registers:

  • RCC_APB1ENR: this register turns on the clock for driving the timers. We have already seen the companion register RCC_APB2ENR when we wanted to drive the GPIO clocks.
  • TIMx_PSC: the prescale register. This is a clock divider where the timer takes the system clock frequency and divides it by the value in this register (plus 1) and divides the clock speed by this number. We do this so we can time longer duration pulses. If there were no prescaler then for a clock operating at 72 MHz frequency, we could only count to 65536 at 72 million clock cycles/second, or about 910 microseconds. By scaling down the clock speed, for example if you were to put 71 in this register, you would slow the clock down from 72 MHz to 1 MHz, allowing for longer times to be measured.
  • TIMx_ARR: This is the auto-reload register, a 16-bit register that contains the maximum number the counter will count up to or down from. If counting up, the timer will reset to zero after reaching this number. If counting down, when the counter reaches zero it resets to this number so it can count down again. This register can contain any number from 0 to 65535. If you are generating a continuous waveform with the counter (using something we refer to as pulse-width modulation or PWM) then changing the value in the ARR register is the same as changing the period of the pulse.
  • TIMx_CR1 and TIMx_CR2: The control registers for the timer that determine the type of counter, direction of counter, trigger for the counter to start etc.
  • TIMx_CNT: The register containing the actual count value for the timer.

A Count-down Delay

For the case of a millisecond delay word, all we need to do is set up the timer, set it to count the appropriate number of counts with the correct prescaler, then set it going. If we configure the counter as a down-counter, we must then keep checking to see whether the counter has decreased to zero. If it has, the delay has been completed and the code can continue to do what it was already doing.

The Code

The first thing we do is set the clock speed. The 72MHz word has already been defined in Warp Speed in Mecrisp-Stellaris. Once we are operating at the right speed, we set the variable Freq to that speed. Then we define the base address and offsets for TIM4. Note that you can use the same offset values for any of the timers, so there is no need to redefine them, or to have variables like TIM1_ARR and TIM2_ARR etc. We just need to define the base address of the timer peripheral we want to use and then call the ARR word (for example) to add the appropriate offset for the autoreload register.

72MHz \ Set the system clock to 72 MHz if it wasn't already
72000000 constant Freq \ PSC clock frequency
\ Define registers
$40000800 constant TIM4 
: CR1 ;
: EGR $14 + ;
: PSC $28 + ;
: ARR $2C + ;
: CNT $24 + ;

The next word we define is init_delay. This word turns on the clock for the timer and disables it, allowing the other registers to be changed without affecting the output of the timer. We run this word when loading the file containing this word set to be sure that the timer is clocked but turned off.

: init_delay ( -- )
  RCC_APB1ENR %1 1 2 set_bits \ Turn on clock for timer 4
  0 TIM4 CR1 ! \ Disable the counter

The next word we define is delay, which is a word that performs a delay for a given number of clock counts. This particular word will work regardless of whether we want delays in microseconds or in milliseconds. The particular type of delay will be defined later, and will be designed to call delay with the appropriate arguments and register settings to give the delay we need. The word delay determines a down-counting single-shot delay, then turns on the counter. A BEGIN...UNTIL loop will wait until the down-counter reaches zero, at which point execution of the word will cease.

: delay ( count -- )
  TIM4 EGR %1 1 0 set_bits 	\ Reinitialise counter and update registers
  DUP TIM4 ARR H! TIM4 CNT H!     \ Set the value in the ARR and CNT Registers
  TIM4 CR1 %11001 5 4 set_bits 	\ Down-count, single shot, enable the counter
  BEGIN 1 TIM4 CR1 bit@ 0= UNTIL

The first line uses the set_bits word defined in General Forth Words for GPIO On The STM32F103 to set bit 0 of the EGR register (the UG bit), which resets the counter and the timer registers. Then it takes the count value and stores it in both the ARR and the CNT registers of the timer. The third line sets the parameters of the timer in the CR1 register, and the final line tests for when the timer has decremented to 0. Because the timer has been set to one-shot operation, there is no danger of missing the zero count.

Once the delay word has been defined, it only remains to make words for microsecond and millisecond delays, which just have to set an appropriate value for the prescaler. Now in the STM32 timer chips, the prescaled clock frequency is related to the system clock frequency and the value in the PSC register via the following relationship:

\[ f_{PSC}= \frac{f_{CLK}}{PSC + 1}\]

or, alternatively the value in the prescaler is given by

\[ PSC = \frac{f_{CLK}}{f_{PSC}} – 1 \]

The 1 added or subtracted in these two equations comes from the fact that when the prescaler is set to 0, the frequency of the counter clock is the same as that of the system clock. Knowing this, we can define our microsecond and millisecond delay words, us and ms, respectively:

: us ( n -- )
  DUP 60001 < IF
    Freq 1000000 / 1- TIM4 PSC H!
  ELSE CR . ." us delay too long.  Use ms instead."
  THEN ;
: ms ( n -- )
  \ Times up to 30 seconds
  DUP 30001 < IF 
    Freq 2001 / TIM4 PSC H!
    2* 1- delay
  ELSE CR . ." ms Delay too long."
  THEN ;

Using this setup, we can type something like 200 ms to generate a delay of 200 milliseconds, or 1000 us to generate a delay of 1000 microseconds. Execution will pass to the next word to be evaluated once the delay word has completed by counting down to zero.

Note that the ms word halves the prescaler and doubles the number of counts, because otherwise the prescaler value would be 72000, which is larger than can be stored as a 16-bit number. I have had to modify the counter value a little from the expected value to remove an offset, but it provides an accurate delay between 1 and 30000 ms.

We can test the behaviour of these words by writing some test words that turn on and off a GPIO port pin, before and after execution of the delay. For example, the following are words to test the ms and us delay words:

: mstest ( n1  -- ) \ test for ms delay
  GPIOC enable
  GPIOC 10 ppout
  GPIOC 10 GPon
  GPIOC 10 GPoff ;
: ustest ( n1  -- ) \ test for us delay
  8 MAX 7 - \ remove offset of 7 us
  GPIOC enable
  GPIOC 10 ppout
  GPIOC 10 GPon
  GPIOC 10 GPoff ;

The 8 Max 7 – ensures that the 7 microsecond offset from executing the word is removed from the count, and that the delay is a minimum of 8 microseconds long. The delay in the code execution prevents us from using a lower delay than this.

Typing something like 2 mstest will generate a pulse that is 2 ms long on PC10. This will result in a waveform that looks like the one below:


Note that the utest word removes 7 microseconds from the count. This is done to compensate for the time required to execute the Forth words, which becomes significant at small delays.

Warp Speed in Mecrisp-Stellaris

Mecrisp-Stellaris Forth is an implementation of the Forth programming language for Arm Cortex microcontrollers. It is fast, small and elegant, and was clearly well designed. I use it in my teaching, and it’s my preferred implementation of the Forth programming language.

It can be downloaded from Sourceforge and has installation candidates for a wide range of microcontrollers. I use the STM32f103RB processor in the Olimex STM32p103 board, and sometimed the blue pill STM32f103 boards.

Out of the box, mecrisp uses the internal oscillator on the chip to run at 8 MHz. This is OK, but using the onboard PLL, you should be able to operate the chip at 72 MHz. That’s much more like it. I wanted to be able to do this, so started looking at the unofficial mecrisp guide, a site set up by Terry Porter to accumulate information about mecrisp, and about the closest thing it has to documentation. What I wanted to do was to start with the standard mecrisp forth for this processor, in a file called stm32f103rb.bin, and then add several words including this one, so I had some useful routines on the chip. Mecrisp forth makes this very easy by having two built-in words, compiletoflash and compiletoram. By running compiletoflash, any new word definitions are saved to flash rather than ram, allowing those words to be stored permanently as part of Forth. This is a really nice feature. There is also a word eraseflash that allows you to start from the original installed words, and someone has come up with a word called cornerstone that allows you to erase down to that point if you no longer need additional definitions.

If you have never seen Forth, it’s a stack-based interpreted programming environment that is conceptually quite simple. Numbers or addresses are passed on a stack as parameters to words (the equivalent of functions in other programming languages) to allow the words to perform the desired operations. Words are built from very simple building block words into more complex words by calling the primitive words in the desired order. New words can then call these words to do more complex things. Each time a new word is defined (in terms of existing words) it is compiled into a dictionary much as you would with a new word in English. As you are free to call the words whatever you like, you end up building a stack-based domain-specific programming language tailored to the problem you want to solve.

For example, you might make words that control a servo motor using pulsed-width-modulated timer. The primitive words would put the right numbers into the timing registers, and you would build your way up to having words that might address motors 1, 2, … n to turn a certain number of degrees. The program to turn the third motor by 30 degrees might look something like

3 motor 30 degrees turn

Words definitions tend to be short and closely linked to one another, because the overhead for defining a word is very small. Code tends to be fast, and takes up a small amount of memory, which is very advantageous when working with a microcontroller. You’ll see what I mean when I write the code further along.

So anyway, back to setting up the board to operate at high speed with the phase-locked loop. When I tried to use the word 72MHz as written, it would crash the board, and when that happens all the benefits of having an interactive programminh environment like Forth goes away. So I decided to move away from that code a little and start from scratch using the user manual for the STM32F103 chip. Upon looking up the appropriate registers, I was able to set the right bits in the registers that control the clocks on the chip, but it still crashed, producing a jumble of non-ascii characters on the screen and then not responding to typed text.

After many hours I worked out the problem: the original code was writing to the USART port (or serial port) 1, whereas on the STM32P103 the communications work on USART2. As the baud rate changes when the processor speed changes, the serial communication speed became all wrong, even though I thought I had fixed that (because I had fixed it for the wrong port). Once I realised this, it was a very easy fix to make the code work, and now I have 8MHz and 72MHz words that can change the speed of the processor on the fly, while keeping the baud rate at 115200 baud. Here’s the code:

8000000 variable clock-hz  \ the system clock is 8 MHz after reset

$40010000 constant AFIO
     AFIO $4 + constant AFIO_MAPR

$40013800 constant USART1
USART1 $08 + constant USART1_BRR
USART1 $0C + constant USART1_CR1
USART1 $10 + constant USART1_CR2

$40004400 constant USART2
USART2 $08 + constant USART2_BRR
USART2 $0C + constant USART2_CR1
USART2 $10 + constant USART2_CR2

$40021000 constant RCC
     RCC $00 + constant RCC_CR
     RCC $04 + constant RCC_CFGR
     RCC $10 + constant RCC_APB1RSTR
     RCC $14 + constant RCC_AHBENR
     RCC $18 + constant RCC_APB2ENR
     RCC $1C + constant RCC_APB1ENR

$40022000 constant FLASH
FLASH $0 + constant FLASH_ACR

$40010000 constant AFIO
AFIO $04 + constant AFIO_MAPR

: baud ( u -- u )  \ calculate baud rate divider, based on current clock rate
  clock-hz @ swap / ;

: 8MHz ( -- )  \ set the main clock back to 8 MHz, keep baud rate at 115200
  $0 RCC_CFGR !                   \ revert to HSI @ 8 MHz, no PLL
  $81 RCC_CR !                    \ turn off HSE and PLL, power-up value
  $18 FLASH_ACR !                 \ zero flash wait, enable half-cycle access
  8000000 clock-hz !  115200 baud USART2_BRR !  \ fix console baud rate
  $0 RCC_CFGR !                   \ remove PLL bits
  CR ." Reset to 115200 baud for 8 MHz clock speed" CR

: 72MHz ( -- )  \ set the main clock to 72 MHz, keep baud rate at 115200
\ Set to 8 MHz clock to start with, to make sure the PLL is off
  $0 RCC_CFGR !                   \ revert to HSI @ 8 MHz, no PLL
  $81 RCC_CR !                    \ turn off HSE and PLL, power-up value
  $18 FLASH_ACR !                 \ zero flash wait, enable half-cycle access
  8000000 clock-hz !  115200 baud USART2_BRR !  \ fix console baud rate
\  CR .USART2 
  $12 FLASH_ACR !                     \ two flash mem wait states
  16 bit RCC_CR bis!                  \ set HSEON
  begin 17 bit RCC_CR bit@ until      \ wait for HSERDY
  $0                                  \ start with 0
\  %111  24 lshift or                 \ operate the MCO from the PLL
  %111  18 lshift or                  \ PLL factor: 8 MHz * 9 = 72 MHz = HCLK
  %01   16 lshift or                  \ HSE clock is 8 MHz Xtal source for PLL
  %10   14 lshift or                  \ ADCPRE = PCLK2/6
  %100  8  lshift or                  \ PCLK1 = HCLK/2
              %10 or  RCC_CFGR !      \ PLL is the system clock
  24 bit RCC_CR bis!                  \ set PLLON
  begin 25 bit RCC_CR bit@ until      \ wait for PLLRDY

  72000000 clock-hz ! 115200 baud 2/ USART2_BRR !  \ fix console baud rate
  CR ." Reset to 115200 baud for 72 MHz clock speed" CR

Again, if you have never seen Forth, this might look a bit strange. The : character starts a new word definition, with the name of the word being the first word after the colon. A semicolon ; terminates the word definition. Then, if I load these definitions, I can simply type the name of the word 72MHz and the machine will start running at that speed. Backslashes indicate comments, as do the parentheses immediately after the word definition. These parentheses comments are special and are called stack constants because they remind the programmer of how many inputs need to be on the stack before the word is called, and how many remain after the word has finished running.

The constant definitions at the start of the code encode the addresses of various important registers. So after loading this file, if I were to type in AFIO_MAPR, then the address of the alternate function input-output register, $40010004 would be placed on the stack. One neat thing about Forth is how flexible it is in terms of its representation of words. For example, the word lshift is defined to take a number on the stack and shift its binary representation by a given number of bits to the left. This is like the C operator <<. So the code %011 24 lshift takes the number 3 (the % symbol represents a binary number) and shifts it left by 24 spaces, leaving the end result on the stack. So in the 72MHz word, we are able to shift the desired bit pattern into the correct locations as numbers and then logically OR the numbers together to give the number we want to put into the register to do what we want. The word that does the storing of the number into the register is ! (pronounced ‘store’). The word @ (pronounced ‘fetch’) gets the data from an address placed on the stack, so AFIO_MAPR @ would get the value currently stored in that register.

Now you may be thinking ‘lshift is a very unwieldy operator compared to <<‘. Well, one of the nice features of Forth is that if you don’t like it, you can change it easily, by defining : << ( n1 n2 --) lshift ; — again, the stuff between the parentheses in the definition is a stack comment and not code. Then you could type %011 24 << to get the same effect as lshift. The ease with which new words can be defined is either a great strength or a great weakness of the language, depending on your point of view. For some, it makes the language easily capable of conforming itself to the problem at hand. To others, it prevents the language from ever being properly standardised. Both points of view can be viewed as the correct one.

If you make a definition for a word init, mecrisp knows that it must, when stored in flash, execute the contents of that word when the controller is first booted. This allows a programmer to make a turnkey application that will run when the controller is switched on, and in this case if the word 72MHz is contained in the word init then it will be run on bootup, and the board will start running at full speed. Then, if the user wants to go back to 8 MHz operation, they can execute the 8MHz word interactively and the processor will drop back down to 8MHz.

Developing in Forth is very different to the C or arduino workflow commonly used for these microcontrollers. In the latter case, development is done on your pc, compiled to a binary program that is uploaded to the board if it compiles correctly, then executed, perhaps using a hardware debugger to ensure it’s doing what you thought it should be doing. In Forth, words are developed in RAM, tested in isolation and, when you are satisfied they work, are saved to flash from within the Forth interpreted environment. Development is done on the microcontroller via a serial terminal connection to a computer, but all the compiling is done in situ on the chip itself. In this way, you can use words like @ and ! to interrogate and change register settings in real time. Also, it’s very easy to write an assembler in Forth, allowing you to access the speed of assembly language from within Forth for those times when the overhead of the interpreter is too great (which with a processor as powerful as these is almost never, for my sorts of applications). When you get used to it, Forth can be a very powerful programming platform.

I’ve never really understood why this programming language never reached popularity in embedded programming circles. Forth has a reputation as a ‘forgotten’, or ‘old-fashioned’ language. People who say this forget how old C is. I guess C and its derivatives like arduino got a head start as a language that many programmers already knew from university courses, and I can see from an employer’s point of view it’s much easier to replace C programmers than Forth programmers — Forth code can be very idiosyncratic.

But for a small system like a microcontroller where codes are usually written by individuals or small teams, Forth’s interactivity and incremental debugging approach are very desirable characteristics. The biggest disadvantage, however, is that there is much less in the way of libraries for this language and, as in the case above, you end up doing everything at register level from scratch. Again, whether you find that an advantage or a disadvantage depends upon your viewpoint.

If you program microcontrollers and have not tried Forth, give mecrisp a try. I really enjoy programming microcontrollers this way. Who knows? You may, like me, never want to go back to the edit-compile-debug-upload-run way of doing things again!