Tanzimul Ittahad
Published © GPL3+

STM32 PWM in bare metal

By directly manipulating the registers, to generate pwm. No library, no HAL functions.

BeginnerProtip1 hour68
STM32 PWM in bare metal

Things used in this project

Hardware components

STM32 Nucleo-64 Board
STMicroelectronics STM32 Nucleo-64 Board
×1

Software apps and online services

Ubuntu
Ubuntu
VS Code
Microsoft VS Code

Hand tools and fabrication machines

Premium Female/Female Jumper Wires, 40 x 3" (75mm)
Premium Female/Female Jumper Wires, 40 x 3" (75mm)

Story

Read more

Schematics

The stm32f411 Blackpill

Pin Mapping Table:

PB4 -> TIM3_CH1 (AF2) -> Resistor (330 Ohm) -> LED 1 -> GND (Signal: 1 kHz, 25% Duty Cycle)

PB5 -> TIM3_CH2 (AF2) -> Resistor (330 Ohm) -> LED 2 -> GND (Signal: 1 kHz, 75% Duty Cycle)

GND -> Ground Reference -> Common Ground Rail

Code

Code

C/C++
#include "stm32f411xe.h"

void SystemInit(void) {}
void _init(void)      {}

/*
 * PWM on PB4 (TIM3_CH1) and PB5 (TIM3_CH2)
 *
 * Source: DS10314 Rev 8, Table 9, page 48
 *   PB4 → AF2 → TIM3_CH1
 *   PB5 → AF2 → TIM3_CH2
 *
 * Clock: 16 MHz HSI (default, no PLL)
 * PSC = 15  → timer_clock = 16 MHz / 16 = 1 MHz
 * ARR = 999 → PWM freq    = 1 MHz / 1000 = 1 kHz
 *
 * CCR1 = 250 → PB4 duty = 250/1000 = 25%
 * CCR2 = 750 → PB5 duty = 750/1000 = 75%
 */

int main(void)
{
    /* ---- Step 1: Enable GPIO Port B clock -------------------------
     * RM0383 Section 6.3.10 — RCC_AHB1ENR, bit 1 = GPIOBEN
     * Without this, all GPIOB register writes are silently ignored. */
    RCC->AHB1ENR |= RCC_AHB1ENR_GPIOBEN;

    /* ---- Step 2: Enable TIM3 clock --------------------------------
     * RM0383 Section 6.3.12 — RCC_APB1ENR, bit 1 = TIM3EN
     * TIM3 is on the APB1 bus on the STM32F411. */
    RCC->APB1ENR |= RCC_APB1ENR_TIM3EN;

    /* ---- Steps 3 & 4: Configure PB4 and PB5 for Alternate Function
     * RM0383 Section 8.4.1 — GPIOx_MODER, 2 bits per pin
     *   PB4 → bits [9:8]  = 0b10 (AF mode)
     *   PB5 → bits [11:10] = 0b10 (AF mode)
     *
     * RM0383 Section 8.4.9 — GPIOx_AFRL, 4 bits per pin (pins 0-7)
     *   PB4 → bits [19:16] = 2 (AF2 = TIM3_CH1)
     *   PB5 → bits [23:20] = 2 (AF2 = TIM3_CH2)
     * Source: DS10314 Rev 8, Table 9, page 48               */

    /* Set PB4 and PB5 to Alternate Function mode */
    GPIOB->MODER &= ~( GPIO_MODER_MODER4 | GPIO_MODER_MODER5 );
    GPIOB->MODER |=  ( GPIO_MODER_MODER4_1 | GPIO_MODER_MODER5_1 );

    /* Select AF2 for PB4 and PB5 (both in AFRL — pins 0 to 7) */
    GPIOB->AFR[0] &= ~( GPIO_AFRL_AFSEL4 | GPIO_AFRL_AFSEL5 );
    GPIOB->AFR[0] |=  ( (2UL << GPIO_AFRL_AFSEL4_Pos) |
                        (2UL << GPIO_AFRL_AFSEL5_Pos) );

    /* ---- Steps 5 & 6: Set prescaler, period, and ARR preload -----
     * RM0383 Section 18.4.11 — TIMx_PSC
     * RM0383 Section 18.4.12 — TIMx_ARR
     * RM0383 Section 18.4.1  — TIMx_CR1, bit 7 = ARPE             */

    TIM3->PSC  = 15;     /* 16 MHz / (15+1) = 1 MHz timer clock     */
    TIM3->ARR  = 999;    /* 1 MHz  / (999+1) = 1 kHz PWM frequency  */
    TIM3->CR1 |= TIM_CR1_ARPE;   /* Enable ARR preload buffering     */

    /* ---- Step 7: Force-load PSC and ARR into active registers -----
     * RM0383 Section 18.4.6 — TIMx_EGR, bit 0 = UG (Update Generation)
     * Without this, PSC and ARR won't apply until the first overflow. */
    TIM3->EGR  = TIM_EGR_UG;

    /* ---- Step 8: Set duty cycles ----------------------------------
     * RM0383 Section 18.4.14 — TIMx_CCR1
     * RM0383 Section 18.4.15 — TIMx_CCR2                          */
    TIM3->CCR1 = 250;    /* PB4: 250/1000 = 25% duty cycle          */
    TIM3->CCR2 = 750;    /* PB5: 750/1000 = 75% duty cycle          */

    /* ---- Step 9: Configure PWM Mode 1 for CH1 and CH2 ------------
     * RM0383 Section 18.4.7 — TIMx_CCMR1
     *   OC1M  bits [6:4]   = 0b110 → PWM Mode 1, CH1
     *   OC1PE bit  [3]     = 1     → preload enable, CH1
     *   OC2M  bits [14:12] = 0b110 → PWM Mode 1, CH2
     *   OC2PE bit  [11]    = 1     → preload enable, CH2
     * Use |= to preserve any other bits in the register.            */
    TIM3->CCMR1 |= ( TIM_CCMR1_OC1PE |
                     (0b110UL << TIM_CCMR1_OC1M_Pos) |
                     TIM_CCMR1_OC2PE |
                     (0b110UL << TIM_CCMR1_OC2M_Pos) );

    /* ---- Step 10: Enable channel outputs --------------------------
     * RM0383 Section 18.4.9 — TIMx_CCER
     *   CC1E bit [0] = 1 → connect CH1 comparator output to PB4
     *   CC2E bit [4] = 1 → connect CH2 comparator output to PB5    */
    TIM3->CCER |= ( TIM_CCER_CC1E | TIM_CCER_CC2E );

    /* ---- Step 11: Start the counter — do this last ----------------
     * RM0383 Section 18.4.1 — TIMx_CR1, bit 0 = CEN               */
    TIM3->CR1 |= TIM_CR1_CEN;

    /*
     * PB4 and PB5 are now producing 1 kHz PWM in hardware.
     * The CPU does nothing for this — the timer runs autonomously.
     *
     * To change duty cycle at runtime, just write a new CCR value:
     *   TIM3->CCR1 = 500;   // PB4 → 50%
     *   TIM3->CCR2 = 100;   // PB5 → 10%
     * The preload buffer applies the change at the next period.
     */
    while (1)
    {
        /* Your application code goes here.
         * The PWM continues running without any CPU involvement. */
    }
}

Credits

Tanzimul Ittahad
1 project • 0 followers
Embedded System Engineer, Student at RUET (Bangladesh) in the Department of Electrical ans Computer Engineering

Comments