返回首页

无 CubeMX 的 STM32F103 裸机固件

创建 STM32F103C8T6 无 CubeMX 和 HAL 裸机固件的指南。描述了链接脚本、带有 Reset_Handler 的启动代码、GPIO PC13 和 TIM2 初始化,用于 1 秒延迟。闪烁 LED 演示从复位到 main() 的完整周期。

无 CubeMX 的 STM32:从 Reset_Handler 到 LED
Advertisement 728x90

STM32F103 极简裸机固件:从链接脚本到 LED 闪烁

复位后,Cortex-M3 从 0x08000000 加载栈指针,从向量表跳转到 Reset_Handler,执行启动代码。在无 HAL 的裸机项目中,这一过程完全透明:手动设置内存、配置 GPIO 和 TIM2 寄存器。本项目展示 STM32F103C8T6 的实际应用——通过硬件定时器实现 PC13 引脚 LED 以 1 秒间隔闪烁。

项目结构分层:inc/ 存放接口,src/ 存放实现,startup/ 存放初始化代码,myLinker.ld 定义内存映射。

链接脚本:将各段放置到 FLASH 和 RAM

链接脚本定义了 STM32F103C8T6 的内存区域并分配各段:

Google AdInline article slot
  • FLASH (0x08000000, 64 KB):.isr_vector、.text、.rodata
  • RAM (0x20000000, 20 KB):.data、.bss、栈

启动所需的关键符号:

_estack = ORIGIN(RAM) + LENGTH(RAM);

MEMORY {
    FLASH (rx) : ORIGIN = 0x08000000, LENGTH = 0x00010000
    RAM   (rwx): ORIGIN = 0x20000000, LENGTH = 0x00005000
}

SECTIONS {
    .isr_vector : { KEEP(*(.isr_vector)) } > FLASH
    .text : { *(.text*) } > FLASH
    .rodata : { *(.rodata*) } > FLASH
    .data : {
        _sdata = .;
        *(.data*)
        _edata = .;
    } > RAM AT > FLASH
    _sidata = LOADADDR(.data);
    .bss : {
        _sbss = .;
        *(.bss*)
        _ebss = .;
    } > RAM
}

.data 段通过 _sidata → _sdata 到 _edata 的地址从 FLASH 复制到 RAM。.bss 段从 _sbss 到 _ebss 清零。_estack 设置初始栈指针。

启动代码:Reset_Handler 和向量表

启动代码在 main() 前运行:复制 .data、清零 .bss、调用 main()。向量表位于 .isr_vector 段:

Google AdInline article slot
#include <stdint.h>

extern uint32_t _estack;
extern uint32_t _sidata, _sdata, _edata, _sbss, _ebss;

int main(void);

void Reset_Handler(void);
void Default_Handler(void) {
    while (1);
}

void NMI_Handler(void)       __attribute__((weak, alias("Default_Handler")));
void HardFault_Handler(void) __attribute__((weak, alias("Default_Handler")));

__attribute__((used, section(".isr_vector")))
const void* vector_table[] = {
    &_estack,
    Reset_Handler,
    NMI_Handler,
    HardFault_Handler
};

void Reset_Handler(void) {
    uint32_t* src = &_sidata;
    uint32_t* dst = &_sdata;
    while (dst < &_edata) {
        *dst++ = *src++;
    }
    dst = &_sbss;
    while (dst < &_ebss) {
        *dst++ = 0;
    }
    main();
    while (1) {}
}

weak alias 将未实现的 ISR 路由到 Default_Handler。

寄存器访问:volatile 和直接寻址

裸机开发意味着直接按地址写入寄存器:

*(volatile uint32_t*)0x40021018 |= (1 << 4);  // RCC_APB2ENR, GPIOC

volatile 防止编译器优化:确保每次访问都生成代码。对于 TIM2,使用结构体:

Google AdInline article slot
typedef struct {
    volatile uint32_t CR1, CR2, SMCR, DIER, SR, EGR;
    volatile uint32_t CCMR1, CCMR2, CCER, CNT;
    volatile uint32_t PSC, ARR, RCR;
    volatile uint32_t CCR1, CCR2, CCR3, CCR4;
    volatile uint32_t BDTR, DCR, DMAR;
} TIM_TypeDef;

#define TIM2 ((TIM_TypeDef*)0x40000000)

访问方式:TIM2->PSC = 7999;

外设初始化:GPIO 和 TIM2

GPIO

使能 GPIOC 时钟:RCC->APB2ENR |= (1 << 4);。将 PC13 配置为输出:

  • GPIOC->CRH &= ~(0xF << 20);
  • GPIOC->CRH |= (0x2 << 20); (2.0 MODE, 00 CNF)

LED 控制:GPIOC->ODR |= (1 << 13); 点亮,&= ~(1 << 13); 熄灭。

TIM2

配置为 1 Hz (72 MHz / 8000 / 9000 = 1 Hz):

  • RCC->APB1ENR |= (1 << 0); — 使能 TIM2 时钟
  • TIM2->PSC = 7999; (预分频)
  • TIM2->ARR = 8999; (自动重载)
  • TIM2->CR1 |= (1 << 0); — 启动定时器

delayOneSecond():等待 TIM2->CNT >= 9000,复位 CNT。

main() 流程

  • GPIO_init()
  • TIMERS_init()
  • 无限循环:turnOnLED() → delayOneSecond() → turnOffLED() → delayOneSecond()

核心要点

  • 链接符号 (_sdata, _edata 等) 是内存初始化的关键。
  • volatile 对寄存器至关重要:否则编译器会缓存值。
  • 向量表 必须置于 .isr_vector,作为 FLASH 开头。
  • 寄存器结构体 比裸指针更简洁。
  • 裸机开发 揭示真实流程:启动 → main → 外设。

— Editorial Team

Advertisement 728x90

继续阅读