Programming the STM32 MCU Peripherals

Each vendor has its own method of programming the peripherals and this section addresses the methods of programming the peripherals of the STM32F Cortex M devices.

System Architecture from the STM32 Reference Manual:

6-ProgrammingCortex00001.bmp

For this document, we are only interested in that the Cortex-M3 core is connected to the AHB system bus, which is then split into two bridges APB1 (low speed) and APB2 (high speed) with each bridge connecting to a set of peripherals.

An MCU device is constrained by the number of physical pins. As much, very often a single physical pin may be connected to multiple peripheral functions. On reset, most IO pins are set up as GPIO (General Purpose IO) pins by the device. To use it for other functions, you have to enable the alternate function for the pin. In addition, to increase flexibility of the pin assignments, some peripherals can be “remapped” to use different set of pins. Taken these together, then the first few steps in using a peripheral are:

1.      Enable the APBx bus

2.      Enable the alternate functions if needed

3.      Remap function pins if needed

4.      Enable the pins

If you are using the pins as GPIO, step 2 and 3 are skipped. At the basic level, all of the above steps involve modifying the device’s IO Registers. The reference manual gives the details of the registers’ names and functions.

The Bare Metal Approach

By including the relevant system header files, you can address the IO Registers using the same names as appeared in the reference manual. With the bare metal approach, you access the IO registers and their bits directly as described by the reference manual:

RCC->APB2RSTR |= 0b101100000011101;

// RESET:  USART1..SPI1 TIM1 ... IOPC IOPB IOPA..AFIO

RCC->APB2RSTR &= ~0b101100000011101;

// UNRESET:USART1..SPI1 TIM1 ... IOPC IOPB IOPA..AFIO

RCC->APB2ENR |= 0b101100000011101;

// ENABLE: USART1..SPI1 TIM1 ... IOPC IOPB IOPA..AFIO

 

RCC->APB1RSTR |= 0b110 << 16; // RESET: USART3 USART2

RCC->APB1RSTR &=~(0b110 << 16);// UNRESET:USART3 USART2

RCC->APB1ENR |= 0b110 << 16; // ENABLE: USART3 USART2

In this code fragment, 0b is the prefix for binary constant. The first 3 executable lines set up the APB2 bus and enable power to the peripherals on that bus that we are interested in. Likewise, the next 3 executable lines do the same for the APB1 bus.

Now we can setup the peripherals:

// SPI, alternate functions for PA 4, 5, 6 and 7

// does not use PA4 for NSS

GPIOA->CRL &= ~(0b1111 << 16); // PA4: ISP RESET

GPIOA->CRL |= (0b1011 << 16); // ALT OUT, 50 Mhz

GPIOA->CRL &= ~(0b1111 << 20); // PA5: SPI1 SCK

GPIOA->CRL |= (0b1011 << 20); // ALT OUT, 50 Mhz

GPIOA->CRL &= ~(0b1111 << 24); // PA6: SPI1 MISO

GPIOA->CRL |= (0b1000 << 24); // IN, PULL-UP/DOWN

GPIOA->CRL &= ~(0b1111 << 28); // PA7: SPI1 MOSI

GPIOA->CRL |= (0b1011 << 28); // ALT OUT, 50 Mhz

 

In this code fragment, SPI1 is enabled using the default alternate functions for pins PA4 to PA7. The GPIOx->CRH and GPIOx->CRL are the control registers for the GPIO block.

The bare metal approach involves consulting the reference manual to know which registers and which bits to modify. This can be error prone if the code changes a lot. If you use this approach, it’s very important to write and update the comments. The advantages are that the code is fast and efficient and you only need to consult the reference manual for the right names to use.

The STM CMSIS Approach

This code fragment demonstrates the STM approach (note: this code is not equivalent to the above bare metal code fragment):

USART_InitTypeDef USART_InitStructure;

GPIO_InitTypeDef GPIO_InitStructure;

 

USART_DeInit(USART2);

 

/* Enable USART peripheral system clock */

RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA |

                     RCC_APB2Periph_AFIO, ENABLE);

RCC_APB1PeriphClockCmd(RCC_APB1Periph_USART2, ENABLE);

 

// USART2 GPIO PIN configuration

GPIO_PinRemapConfig(GPIO_Remap_USART2, DISABLE);

 

// Configure USART Tx as alternate function push pull output

GPIO_InitStructure.GPIO_Pin = GPIO_Pin_2;

GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP;

GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;

/* GPIO configuration */

GPIO_Init(GPIOA, &GPIO_InitStructure);

 

/* Configure USART Rx as input floating */

GPIO_InitStructure.GPIO_Pin = GPIO_Pin_3;

GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IN_FLOATING;

/* GPIO configuration */

GPIO_Init(GPIOA, &GPIO_InitStructure);

 

USART_InitStructure.USART_BaudRate = 9600;

USART_InitStructure.USART_WordLength = USART_WordLength_8b;

USART_InitStructure.USART_StopBits = USART_StopBits_1;

USART_InitStructure.USART_Parity = USART_Parity_No;

USART_InitStructure.USART_Mode = USART_Mode_Rx |                                     USART_Mode_Tx;

USART_InitStructure.USART_HardwareFlowControl =

   USART_HardwareFlowControl_None;

 

/* USART configuration */

USART_Init(USART2, &USART_InitStructure);

 

/* Enable USART */

USART_Cmd(USART2, ENABLE);

Each peripheral uses a unique initialization data structure, e.g. USART_InitTypeDef and GPIO_InitTypeDef. You fill in the fields using #define names defined in the header files, then call the function xxxx_Init, e.g. USART_Init and GPIO_Init, passing along the global names USART2 and GPIOA respectively in the example. The calls initialize the IO registers, much like what are done in the bare metal approach.

The advantage of the CMSIS approach is that the #define names are self documenting and thus less error prone and easier to modify. The disadvantages are that you need to learn a different set of #define names (documented only in the STM provided header files), the functions are opaque unless you explore the CMSIS code, and the init function may need to perform calculations to locate the right bits to modify.

For example, GPIOA has 16 port pins, each pin has 4 bits in either the GPIOA->CRH or GPIOA->CRL, depending on the port pin number. In the bare metal approach, you modify the bits directly, but with the CMSIS function, it must determine which pin you are modifying, then either the CRH or CRL is accessed, after some bit shifting and masking.

These must be done at runtime and cannot be optimized by the compiler. Since these are part of the initialization code, this may not be an issue at all. However. purists may still opt for the bare metal approach since it’s also more transparent.

Most people would probably follow STM’s example and use the CMSIS functions. Even if you use the bare metal approach, some functionality is difficult to set up by hand, and a mix of CMSIS code can be interleaved with the bare metal code. For example, it’s difficult to calculate the correct value for the UART’s baudrate register, but a single call to the CMSIS function does the job nicely:

USART_SetBaudrate(USART1, 1000000);