RCC->APB1ENR1 |= RCC_APB1ENR1_USART2EN;
RCC->AHB2ENR |= RCC_AHB2ENR_GPIOAEN;
GPIOA->MODER &= ~(3U << (2 * 2));
GPIOA->MODER |= (2U << (2 * 2));
GPIOA->AFR[0] &= ~(0xFU << (2 * 4));
GPIOA->AFR[0] |= (7U << (2 * 4));
USART2->BRR = 4000000 / 9600;
USART2->CR1 |= USART_CR1_TE | USART_CR1_RE | USART_CR1_UE;
while(!(USART2->ISR & USART_ISR_TXE));
USART2->TDR = 'A';
Execution Theory
Setting up UART without HAL requires routing the internal USART peripheral to a physical pin using the Alternate Function Register (AFR). We first enable the APB1 clock for USART2 and AHB2 for GPIOA. We configure PA2 into Alternate Function mode and specifically assign it AF7, which is the hardware map for USART2_TX. We then calculate the baud rate by dividing our core clock frequency (4MHz) by our desired baud rate (9600) and inserting it into the BRR register. Finally, we enable the transmitter (TE) and the peripheral (UE). To send data, we wait for the Transmit Data Register Empty (TXE) flag before loading a character into TDR.
RCC->APB2ENR |= RCC_APB2ENR_SPI1EN;
SPI1->CR1 = 0;
SPI1->CR1 |= SPI_CR1_MSTR | (3 << SPI_CR1_BR_Pos);
SPI1->CR2 = 0;
SPI1->CR2 |= (7 << SPI_CR2_DS_Pos) | SPI_CR2_SSOE;
SPI1->CR1 |= SPI_CR1_SSM | SPI_CR1_SSI;
SPI1->CR1 |= SPI_CR1_SPE;
Execution Theory
Initializing the SPI peripheral in Master mode. We first enable the clock to SPI1 via APB2. We set the Master Mode (`MSTR`) bit and configure the Baud Rate (`BR`) prescaler to divide the peripheral clock by 16. We set the Data Size (`DS`) to 8-bit (which is value 7 in the register). For flexibility, we use Software Slave Management (`SSM`) and set the Internal Slave Select (`SSI`) bit high, allowing us to manually control the Chip Select pin via a standard GPIO rather than hardware control. Finally, we enable the SPI peripheral (`SPE`).
RCC->APB1ENR1 |= RCC_APB1ENR1_I2C1EN;
I2C1->CR1 &= ~I2C_CR1_PE;
I2C1->TIMINGR = 0x00200C28;
I2C1->CR1 |= I2C_CR1_PE;
I2C1->CR2 = (SlaveAddress << 1) | (1 << I2C_CR2_NBYTES_Pos);
I2C1->CR2 |= I2C_CR2_START;
Execution Theory
Configuring I2C in bare-metal requires strictly disabling the peripheral (PE=0) before setting the timing register. The 32-bit hex value placed in TIMINGR is calculated based on the system clock frequency (4MHz) and defines the exact SCL high/low periods, data setup/hold times, and clock prescaler to achieve a standard 100kHz I2C bus speed. To initiate communication, we load the 7-bit slave address shifted left by 1, set the number of bytes to transfer, and trigger the START condition in CR2.
RCC->AHB2ENR |= RCC_AHB2ENR_ADCEN;
ADC1->CR &= ~ADC_CR_DEEPPWD;
ADC1->CR |= ADC_CR_ADVREGEN;
for(volatile int i=0; i<1000; i++);
ADC1->CR |= ADC_CR_ADCAL;
while(ADC1->CR & ADC_CR_ADCAL);
ADC1->CR |= ADC_CR_ADEN;
while(!(ADC1->ISR & ADC_ISR_ADRDY));
ADC1->SQR1 = (5 << ADC_SQR1_SQ1_Pos);
ADC1->CR |= ADC_CR_ADSTART;
while(!(ADC1->ISR & ADC_ISR_EOC));
uint16_t result = ADC1->DR;
Execution Theory
Analog to Digital initialization on the STM32 requires waking the ADC from deep power-down mode and enabling its voltage regulator. We then run an automatic internal calibration (ADCAL). Once calibrated and enabled (ADEN), we configure the regular sequence register (SQR1) to sample channel 5. Setting ADSTART begins the conversion, and we block until the End of Conversion (EOC) flag is thrown before reading the Data Register (DR).
RCC->AHB2ENR |= (1U << 1);
GPIOB->MODER &= ~(3U << (3 * 2));
GPIOB->MODER |= (1U << (3 * 2));
while(1) {
GPIOB->ODR ^= (1U << 3);
for(volatile int i = 0; i < 100000; i++);
}
Execution Theory
First, we enable the clock to Port B via the Reset and Clock Control (RCC) AHB2 bus. Without this, the GPIO peripheral is dead. Next, we locate Pin 3 in the `MODER` (Mode Register) and set its 2-bit field to `01`, which defines it as an output. Finally, in an infinite loop, we use the bitwise XOR operator (`^=`) on the Output Data Register (`ODR`) to rapidly flip the bit, turning the LED on and off.