4. 案例4

任务目标

本案例基于STM32F103微控制器,实现Bootloader启动阶段必需的硬件初始化流程:配置系统时钟树(72MHz主频)、初始化启动状态指示LED的GPIO、初始化用于调试输出的USART1串口,并通过LED闪烁验证初始化流程正确执行。该初始化代码是Bootloader进入主程序前的硬件准备阶段。

硬件环境

  • MCU:STM32F103C8T6(Blue Pill开发板)
  • LED:PC13(板载,低电平点亮)
  • USART1:PA9(TX) / PA10(RX),波特率115200
  • 外部晶振:8MHz(HSE)

关键完整代码

/* main.c - Bootloader硬件初始化 */
#include "stm32f1xx.h"

/* 系统时钟树配置:HSE 8MHz → PLL ×9 → SYSCLK 72MHz */
void SystemClock_Config(void) {
    /* 使能 HSE */
    RCC->CR |= RCC_CR_HSEON;
    while (!(RCC->CR & RCC_CR_HSERDY));

    /* FLASH 等待周期:72MHz 需 2 个等待周期 */
    FLASH->ACR = FLASH_ACR_LATENCY_2;

    /* 配置 PLL:HSE 作为输入,倍频系数 ×9 */
    RCC->CFGR = RCC_CFGR_PLLSRC_HSE | RCC_CFGR_PLLMULL9;

    /* APB1 分频 /2(36MHz),APB2 不分频(72MHz) */
    RCC->CFGR |= RCC_CFGR_PPRE1_DIV2;

    /* 使能 PLL 并等待锁定 */
    RCC->CR |= RCC_CR_PLLON;
    while (!(RCC->CR & RCC_CR_PLLRDY));

    /* 切换系统时钟到 PLL */
    RCC->CFGR |= RCC_CFGR_SW_PLL;
    while ((RCC->CFGR & RCC_CFGR_SWS) != RCC_CFGR_SWS_PLL);
}

/* GPIO 初始化:PC13 推挽输出(LED) */
void GPIO_Init(void) {
    RCC->APB2ENR |= RCC_APB2ENR_IOPCEN;   /* 使能 GPIOC 时钟 */

    GPIOC->CRH &= ~(0xF << 20);           /* 清除 CNF13/ MODE13 */
    GPIOC->CRH |= (0x2 << 20);            /* MODE13=10: 输出模式, 2MHz */
    /* CNF13=00: 推挽输出 */
}

/* USART1 初始化:115200-8-N-1,TX 复用推挽输出 */
void USART1_Init(void) {
    RCC->APB2ENR |= RCC_APB2ENR_IOPAEN | RCC_APB2ENR_USART1EN;

    /* PA9(TX) 复用推挽输出 50MHz */
    GPIOA->CRH &= ~(0xF << 4);
    GPIOA->CRH |= (0xB << 4);             /* MODE9=11, CNF9=10 */

    /* PA10(RX) 浮空输入 */
    GPIOA->CRH &= ~(0xF << 8);
    GPIOA->CRH |= (0x4 << 8);             /* MODE10=00, CNF10=01 */

    /* 波特率计算:72MHz / (16 × 115200) ≈ 39.06 → 取整 39 */
    USART1->BRR = 0x271;                  /* 0x271 = 625 = 39.0625 的定点表示 */
    USART1->CR1 = USART_CR1_UE | USART_CR1_TE | USART_CR1_RE;
}

/* 简单字符发送 */
void USART1_SendChar(char c) {
    while (!(USART1->SR & USART_SR_TXE));
    USART1->DR = c;
}

/* 字符串发送 */
void USART1_SendString(const char *str) {
    while (*str) {
        USART1_SendChar(*str++);
    }
}

/* 毫秒级延时(简单循环,非精确) */
void Delay_ms(uint32_t ms) {
    for (uint32_t i = 0; i < ms * 4000; i++) {
        __NOP();
    }
}

int main(void) {
    /* 1. 时钟树配置:SYSCLK = 72MHz */
    SystemClock_Config();

    /* 2. GPIO 初始化:LED 引脚 */
    GPIO_Init();

    /* 3. 串口初始化 */
    USART1_Init();

    /* 4. 启动信息输出 */
    USART1_SendString("Bootloader HW Init OK\r\n");

    /* 5. LED 闪烁验证:亮 200ms,灭 200ms,循环 5 次 */
    for (int i = 0; i < 5; i++) {
        GPIOC->BSRR = (1 << 13);         /* PC13 = 0 → LED 亮 */
        Delay_ms(200);
        GPIOC->BRR = (1 << 13);          /* PC13 = 1 → LED 灭 */
        Delay_ms(200);
    }

    /* Bootloader 初始化完成,可跳转至主程序 */
    while (1) {
        /* 停留在此处,实际 Bootloader 可在此跳转 */
    }
}

编译与烧录

# 使用 arm-none-eabi-gcc 编译(以 Makefile 方式为例)
arm-none-eabi-gcc -c -mcpu=cortex-m3 -mthumb main.c -o main.o
arm-none-eabi-ld -T stm32f103c8t6.ld main.o -o bootloader.elf
arm-none-eabi-objcopy -O binary bootloader.elf bootloader.bin

# 使用 ST-Link 烧录
st-flash write bootloader.bin 0x08000000

验证方法与判定标准

  1. LED 闪烁验证:上电后板载 LED(PC13)应以 200ms 间隔闪烁 5 次(亮-灭交替),随后保持常灭。若观察到该现象,说明时钟树配置正确(否则延时异常)、GPIO 初始化成功。
  2. 串口输出验证:将 PA9(TX) 连接至 USB-TTL 模块,PC 端以 115200-8-N-1 打开串口。上电后应收到字符串 Bootloader HW Init OK。若收到该字符串,说明 USART1 初始化及波特率配置正确。
  3. 时钟频率验证:若 LED 闪烁周期明显偏离 200ms(如过快或过慢),说明 PLL 配置有误,系统时钟未达到 72MHz。

常见问题

  • LED 不亮:检查 GPIOC 时钟是否使能(RCC->APB2ENR),以及 PC13 引脚模式配置是否正确(推挽输出)。
  • 串口无输出:确认 PA9 复用推挽配置正确,且 BRR 值计算无误。若使用 8MHz HSE 但未正确配置 PLL,实际波特率会偏移导致乱码。
  • 程序跑飞:检查 FLASH 等待周期是否设置为 2(72MHz 必需),否则高速访问 Flash 会导致异常。