Back to Home

STM32 without HAL and SPL

stm32 · microcontrollers · C · hal · stm32cube

STM32 without HAL and SPL

    At one time, more than 5 years, when searching for information about 32-bit microcontrollers, I noticed that almost all examples for STM32 involved the use of SPL (Standard Peripherals Library). Quote from Wikipedia:
    STM32F10x Standard Peripherals Library (abbreviated 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 peripherals of the microcontroller. "

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

    The HAL hardware abstraction layer, enabling portability between different STM32 devices via standardized API calls.
    The Low-Layer (LL) APIs, a light-weight, optimized, expert oriented set of APIs designed for both performance and runtime efficiency.
    A collection of Middleware components, like RTOS, USB library, file system, TCP / IP stack, Touch sensing library or Graphic Library (depending on the MCU series)

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

    There is an opinion that using 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.

    Peripheral initialization.


    Ports

    .
    For many projects, you just need to turn the corresponding feet of the controller on or off and read the analog values.

    Port activation: 1 line of code:

      //инициализация порта A*************************
      RCC->AHB1ENR |= RCC_AHB1ENR_GPIOAEN; 
    

    Translation of 0 pin of port A into 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; 
    

    Putting 2 pins of port A into 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 corresponding port pins as complementary PWM outputs.

    Determination of the output 8 of port A as a PWM output of CH1 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;

    Determination of output 13 of port B as PWM output of CH1N TIM1 counter:

    //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;
    

    Natural 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 the registers of the microcontroller.

    ADC


    An example of ADC initialization for operation in DMA mode. In this mode, the 4 channels of the ADC2 automatically switch 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 one DAP stream for data transfer 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


    Example timer configuration 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); //разрешить прерывания от таймера 
    

    Example timer configuration for generating time interrupts. Interrupts from this timer are typically 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 unchanged for a long time. It still started on the 103 controller, now it is used on the 7th series :)

    Of course, initializing the ADCs, timers and DAPs is a bit more complicated than the ports, but it is also a simple task.
    It does not use external libraries, more compact code, more predictable controller behavior.

    Read Next