📜 ⬆️ ⬇️

STM32 without HAL and SPL

At one time, more than 5 years old, when searching for information about 32-bit microcontrollers, I noticed that almost all examples for STM32 implied the use of SPL (Standard Peripherals Library). Quote from wikipedia:
STM32F10x Standard Peripherals Library (abbr. STM32F10x SPL) is a library created by STMicroelectronics in C language for its microcontrollers of the STM32F10x family. Contains functions, structures and macros to facilitate the work with the periphery of the microcontroller. "

Currently, it is proposed to use STM32CUBE to reduce the entry threshold and speed up development. Quote from STM website:
STM32Cube embedded software libraries, including:

HAL hardware abstraction layer, enabling portability between different STM32 devices via standardized API calls.
The Low-Layer (LL) APIs are a light-weight, optimized, expert-oriented set of APIs.
A collection of middleware components, like a RTOS, USB library, a file system, a TCP / IP stack, a touch sensing library or a Graphic Library (for MCU series)

In my opinion, for most projects, external libraries are not needed and it is easier to use access to the registers of the microcontroller using standard documentation.

There is an opinion that the use of microcontroller registers is a more complicated way than using wrappers from external libraries. I will try to show that this is not always the case.
Examples of register initialization with comments.

Initialization of the periphery.


Ports

.
For many projects, you simply need to turn the corresponding controller legs on or off and read the analog values.
')
Enable port: 1 line of code:

//  A************************* RCC->AHB1ENR |= RCC_AHB1ENR_GPIOAEN; 

Translation of 0 pin of port A to analog mode 2 lines:

  //PA0 - PA0/ADC1_ADC2_ADC3_IN0 // GPIO_Pin_0  A   GPIOA->MODER |= GPIO_MODER_MODER0_0; GPIOA->MODER |= GPIO_MODER_MODER0_1; 

Translation of 2 pins of port A to exit mode (push-pull) 1 line:

 GPIOA->MODER |= GPIO_MODER_MODER2_0; 

Using alternative functions is also not very difficult. Often a microcontroller is used to control a half-bridge converter. To do this, configure the appropriate port pins as complementary PWM outputs.

Definition of output 8 of port A, as PWM output CH1 of the counter TIM1 (3 lines):

  //PA8/TIM1_CH1 // Alternate function mode GPIOA->MODER &= ~GPIO_MODER_MODER8_0; //0 GPIOA->MODER |= GPIO_MODER_MODER8_1; //1 //GPIO alternate function high register (GPIOx_AFRL) //AFR8[3:0] = 0001: AF1 GPIOA -> AFR[1] |= 0x00000001; 

Definition of output 13 of port B as PWM output CH1N of the counter TIM1:

 //PB13/TIM1_CH1N // Alternate function mode GPIOB->MODER &= ~GPIO_MODER_MODER13_0; //0 GPIOB->MODER |= GPIO_MODER_MODER13_1; //1 //GPIO alternate function high register (GPIOx_AFRL) //AFR13[3:0] = 0001: AF1 GPIOB -> AFR[1] |= 0x00100000; 

A logical question: where to get the designation of the necessary registers and bits? Answer: in 3 documents (for example 746).

1. Reference manual STM32F7.pdf
2. STM32F745xx.pdf
3. stm32f746xx.h

These 3 files are completely enough to correctly access all registers of the microcontroller.

ADC


An example of the initialization of the ADC to work in DMA mode. In this mode, the 4 ACP2 channels are automatically switched in a circle and transmit data to the DMA controller, which adds the data to an array.

 void init_ADC1(void) { RCC->APB2ENR |= RCC_APB2ENR_ADC1EN; //   ADC1->CR2 |= ADC_CR2_ADON; //  ADC1->CR1 |= ADC_CR1_EOCIE; ADC1->CR1 |= ADC_CR1_SCAN; // Bit 8 SCAN: Scan mode ADC1->CR2 |= ADC_CR2_EOCS; //Bit 10 EOCS: End of conversion selection ADC1->CR2 |= ADC_CR2_DMA; //Bit 8 DMA: Direct memory access mode (for single ADC mode) ADC1->CR2 |= ADC_CR2_DDS; //Bit 9 DDS: DMA disable selection (for single ADC mode //Bits 23:20 L[3:0]: Regular channel sequence length (4) //0003: 4 conversion ADC1->SQR1 |= ADC_SQR1_L_0; //1 ADC1->SQR1 |= ADC_SQR1_L_1; //1 ADC1->SQR1 &= ~ADC_SQR1_L_2; //0 ADC1->SQR1 &= ~ADC_SQR1_L_3; //0 //Bits 4:0 SQ1[4:0]: 1st conversion in regular sequence PC0/ADC1_ADC2_ADC3_IN10 ADC1->SQR3 &= ~ADC_SQR3_SQ1_0; //0 ADC1->SQR3 |= ADC_SQR3_SQ1_1; //1 ADC1->SQR3 &= ~ADC_SQR3_SQ1_2; //0 ADC1->SQR3 |= ADC_SQR3_SQ1_3; //1 ADC1->SQR3 &= ~ADC_SQR3_SQ1_4; //0 //Bits 4:0 SQ2[4:0]: 2st conversion in regular sequence PC1/ADC1_ADC2_ADC3_IN11 ADC1->SQR3 |= ADC_SQR3_SQ2_0; //1 ADC1->SQR3 |= ADC_SQR3_SQ2_1; //1 ADC1->SQR3 &= ~ADC_SQR3_SQ2_2; //0 ADC1->SQR3 |= ADC_SQR3_SQ2_3; //1 ADC1->SQR3 &= ~ADC_SQR3_SQ2_4; //0 //Bits 4:0 SQ3[4:0]: 3st conversion in regular sequence PC2/ADC1_ADC2_ADC3_IN12 ADC1->SQR3 &= ~ADC_SQR3_SQ3_0; //0 ADC1->SQR3 &= ~ADC_SQR3_SQ3_1; //0 ADC1->SQR3 |= ADC_SQR3_SQ3_2; //1 ADC1->SQR3 |= ADC_SQR3_SQ3_3; //1 ADC1->SQR3 &= ~ADC_SQR3_SQ3_4; //0 //Bits 4:0 SQ4[4:0]: 4st conversion in regular sequence PC3/ADC1_ADC2_ADC3_IN13 ADC1->SQR3 |= ADC_SQR3_SQ4_0; //1 ADC1->SQR3 &= ~ADC_SQR3_SQ4_1; //0 ADC1->SQR3 |= ADC_SQR3_SQ4_2; //1 ADC1->SQR3 |= ADC_SQR3_SQ4_3; //1 ADC1->SQR3 &= ~ADC_SQR3_SQ4_4; //0 NVIC_EnableIRQ (ADC_IRQn); } 

RAP


Initialization of a single flow PDU for transferring data from the ADC to the array (see above)

  // DMA2 RCC->AHB1ENR |= RCC_AHB1ENR_DMA2EN; //Stream3 Channel 1 DMA2 -   ADC2_array 2 a //Bits 27:25 CHSEL[2:0]: Channel selection (1) DMA2_Stream3->CR |= DMA_SxCR_CHSEL_0; //1 DMA2_Stream3->CR &= ~DMA_SxCR_CHSEL_1; //0 DMA2_Stream3->CR &= ~DMA_SxCR_CHSEL_2; //0 //Bits 14:13 MSIZE[1:0]: Memory data size (16 bit) DMA2_Stream3->CR |= DMA_SxCR_MSIZE_0; //1 DMA2_Stream3->CR &= ~DMA_SxCR_MSIZE_1; //0 //Bits 12:11 PSIZE[1:0]: Peripheral data size (16 bit) DMA2_Stream3->CR |= DMA_SxCR_PSIZE_0; //1 DMA2_Stream3->CR &= ~DMA_SxCR_PSIZE_1; //0 //Bits 10 MINC: Memory increment mode DMA2_Stream3->CR |= DMA_SxCR_MINC; //Bits 7:6 DIR[1:0]: Data transfer direction (00: Peripheral-to-memory) DMA2_Stream3->CR &= ~DMA_SxCR_DIR_0; //0 DMA2_Stream3->CR &= ~DMA_SxCR_DIR_1; //0 //Bits 4 TCIE: Transfer complete interrupt enable DMA2_Stream3->CR |= DMA_SxCR_TCIE; //Bits 15:0 NDT[15:0]: Number of data items to transfer //1000 point x 4 channel DMA2_Stream3-> NDTR = 4000; //Bits 31:0 PAR[31:0]: Peripheral address DMA2_Stream3->PAR = (uint32_t) &(ADC2->DR); //Bits 31:0 M0A[31:0]: Memory 0 address DMA2_Stream3->M0AR = (uint32_t) ADC2_array; //Bits 0 EN: Stream enable / flag stream ready when read low DMA2_Stream3->CR |= DMA_SxCR_EN; NVIC_EnableIRQ (DMA2_Stream0_IRQn); 

Timers


Timer configuration example with complementary 12-bit PWM outputs for controlling 3 half bridges (3-phase inverter)

  // TIM1 PWM RCC -> APB2ENR |= RCC_APB2ENR_TIM1EN; // TIM1 TIM1->CR1 |= TIM_CR1_CMS_0; //Center-aligned mode 1 TIM1->CR1 |= TIM_CR1_ARPE; //  // 8   4000 - 3000  //  108  TIM1->PSC = 8; TIM1->ARR = 4000; TIM1->CCR1 = 1000; //  TIM1->CCR2 = 1000; TIM1->CCR3 = 1000; TIM1->CCMR1 &= ~TIM_CCMR1_OC1M_0; TIM1->CCMR1 |= TIM_CCMR1_OC1M_1; TIM1->CCMR1 |= TIM_CCMR1_OC1M_2; //110: PWM mode 1 TIM1->CCMR1 &= ~TIM_CCMR1_OC2M_0; TIM1->CCMR1 |= TIM_CCMR1_OC2M_1; TIM1->CCMR1 |= TIM_CCMR1_OC2M_2; //110: PWM mode 1 TIM1->CCMR2 &= ~TIM_CCMR2_OC3M_0; TIM1->CCMR2 |= TIM_CCMR2_OC3M_1; TIM1->CCMR2 |= TIM_CCMR2_OC3M_2; //110: PWM mode 1 TIM1->CCER |= TIM_CCER_CC1E; // Capture/Compare 1 output enable TIM1->CCER |= TIM_CCER_CC1NE; // Capture/Compare 1 complementary output enable TIM1->CCER |= TIM_CCER_CC2E; // Capture/Compare 2 output enable TIM1->CCER |= TIM_CCER_CC2NE; // Capture/Compare 2 complementary output enable TIM1->CCER |= TIM_CCER_CC3E; // Capture/Compare 3 output enable TIM1->CCER |= TIM_CCER_CC3NE; // Capture/Compare 3 complementary output enable //DTG[7:0]: Dead-time generator setup 1 mks TIM1->BDTR |= TIM_BDTR_DTG_0; TIM1->BDTR |= TIM_BDTR_DTG_1; TIM1->BDTR |= TIM_BDTR_DTG_2; TIM1->BDTR |= TIM_BDTR_DTG_3; TIM1->BDTR |= TIM_BDTR_DTG_4; TIM1->BDTR |= TIM_BDTR_DTG_5; TIM1->BDTR |= TIM_BDTR_DTG_6; TIM1->BDTR |= TIM_BDTR_DTG_7; TIM1->DIER |= TIM_DIER_CC1IE; //Capture/Compare 1 interrupt enable //TIM1->DIER |= TIM_DIER_CC2IE; //Capture/Compare 2 interrupt enable //TIM1->DIER |= TIM_DIER_CC3IE; //Capture/Compare 3 interrupt enable TIM1->CR1 |= TIM_CR1_CEN; //Bit 0 CEN: Counter enable TIM1->BDTR |= TIM_BDTR_MOE; //MOE: Main output enable NVIC_EnableIRQ (TIM1_CC_IRQn); //    

An example of a timer configuration for the formation of time interrupts. Interrupts from this timer are usually used to update data on interfaces. In this mode, the external timer outputs are not used.

  // TIM3 100  RCC -> APB1ENR |= RCC_APB1ENR_TIM3EN; //TIM3 Timer clock enable TIM3->CR1 |= TIM_CR1_CEN; //Bit 0 CEN: Counter enable TIM3->CR1 |= TIM_CR1_ARPE; //Bit 7 ARPE: Auto-reload preload enable TIM3->DIER |= TIM_DIER_UIE; //Bit 0 UIE: Update interrupt enable TIM3->PSC = 2000; TIM3->ARR = 5400; NVIC_EnableIRQ (TIM3_IRQn); //    

These configuration files are used almost without change for a long time. It all started on the 103 controller, now used on the 7 series :)

Of course, the initialization of the ADC, timers and PDP is a bit more complicated than the ports, but also a simple task.
It does not use external libraries, more compact code, more predictable controller behavior.

Source: https://habr.com/ru/post/337622/


All Articles