STM32 without HAL and SPL
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.