STM32F103 极简裸机固件:从链接脚本到 LED 闪烁
复位后,Cortex-M3 从 0x08000000 加载栈指针,从向量表跳转到 Reset_Handler,执行启动代码。在无 HAL 的裸机项目中,这一过程完全透明:手动设置内存、配置 GPIO 和 TIM2 寄存器。本项目展示 STM32F103C8T6 的实际应用——通过硬件定时器实现 PC13 引脚 LED 以 1 秒间隔闪烁。
项目结构分层:inc/ 存放接口,src/ 存放实现,startup/ 存放初始化代码,myLinker.ld 定义内存映射。
链接脚本:将各段放置到 FLASH 和 RAM
链接脚本定义了 STM32F103C8T6 的内存区域并分配各段:
- 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 段:
#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,使用结构体:
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
暂无评论。