2. 裸机编程基础与寄存器操作:从点灯看本质
点灯,是嵌入式世界的 "Hello World"。
很多人学完点灯,还是没搞明白——为什么写个 GPIOB->ODR = 0x01,灯就亮了?
说白了,你是在跟芯片的物理引脚直接对话。没有操作系统,没有驱动框架,就你写的几行 C 代码,直接操控了硬件。这就是裸机编程的本质。
2.1 GPIO 寄存器详解:芯片的 "开关面板"
每个 GPIO 端口背后,都有一组寄存器在干活。我习惯把它们想象成一个开关面板:
| 寄存器 | 全称 | 作用(一句话) |
|---|---|---|
| MODER | Mode Register | 设置引脚模式:输入、输出、复用、模拟 |
| OTYPER | Output Type Register | 推挽输出还是开漏输出 |
| OSPEEDR | Output Speed Register | 输出速度:低速、中速、高速、极速 |
| PUPDR | Pull-up/Pull-down Register | 内部上拉、下拉、浮空 |
| IDR | Input Data Register | 读引脚电平:0 或 1 |
| ODR | Output Data Register | 写引脚电平:输出高或低 |
| BSRR | Bit Set/Reset Register | 原子操作:单独置位或复位某引脚 |
| AFRL / AFRH | Alternate Function Register | 配置引脚复用功能(如 USART、SPI) |
我的经验: 刚入门时,我总记不住这些寄存器。后来发现,只要记住 MODER、ODR、BSRR 这三个,90% 的点灯场景就够了。其他寄存器,用到再查手册,别硬背。
2.2 使用指针操作寄存器:C 语言与硬件的 "握手"
寄存器在芯片内部,是有固定地址的。C 语言怎么访问它们?用指针。
STM32F103 的 GPIOB 基地址是 0x40010C00。ODR 寄存器相对基地址的偏移是 0x0C。所以:
// 方式一:直接地址操作(不推荐,可读性差)
*(uint32_t *)0x40010C0C = 0x00000020; // 让 PB5 输出高电平
// 方式二:结构体指针操作(推荐,STM32 标准做法)
#define GPIOB_BASE (0x40010C00)
GPIO_TypeDef *GPIOB = (GPIO_TypeDef *)GPIOB_BASE;
GPIOB->ODR = 0x00000020;
为什么方式二更好?因为 GPIO_TypeDef 结构体把寄存器按顺序排列好了,你写 GPIOB->ODR 比记偏移量直观得多。
注意: 操作寄存器前,一定要先使能对应外设的时钟!GPIOB 挂在 APB2 总线上,需要设置 RCC->APB2ENR 的位 3。我见过太多新手灯不亮,查了半天发现是时钟没开。
2.3 位带操作技巧:让点灯像控制变量一样简单
你有没有觉得,用 GPIOB->ODR |= (1 << 5) 来置位 PB5 有点啰嗦?
位带操作就是来解决这个问题的。它把每个 bit 映射到一个 32 位的字地址上。你直接读写这个地址,就等于读写那个 bit。
STM32 的位带区域有两个:
- SRAM 位带区: 0x20000000 ~ 0x200FFFFF(1MB)
- 外设位带区: 0x40000000 ~ 0x400FFFFF(1MB)
每个 bit 映射到位带别名区的公式:
别名区地址 = 位带区基地址 + (字节偏移 * 32) + (位号 * 4)
举个例子,GPIOB_ODR 的地址是 0x40010C0C,我们要操作第 5 位(PB5):
// 计算 PB5 在位带别名区的地址
#define PB5_ADDR (0x42000000 + (0x40010C0C - 0x40000000) * 32 + 5 * 4)
// 定义成指针
#define PB5 (*((volatile uint32_t *)PB5_ADDR))
// 然后就可以像操作变量一样操作 PB5 了
PB5 = 1; // 输出高电平
PB5 = 0; // 输出低电平
位带操作的好处:
- 读-改-写是原子操作,不会被中断打断
- 代码更简洁,可读性更高
- 适合用在时间敏感的场景(比如模拟时序)
我曾经在一个项目中,用位带操作模拟 I2C 时序。因为每个 bit 操作都是原子的,省去了关中断的麻烦,代码干净了很多。
2.4 从点灯看本质:裸机编程的核心思维
点灯看似简单,但它包含了裸机编程的所有要素:
- 时钟使能 —— 外设工作的前提
- 模式配置 —— 告诉芯片引脚干什么用
- 数据操作 —— 通过寄存器读写控制硬件
你想想看,串口通信、I2C、SPI,甚至更复杂的 DMA,底层逻辑都是一样的:配时钟、设模式、读写寄存器。点灯学会了,其他外设就是换汤不换药。
避坑指南: 我曾经在调试一块板子时,灯死活不亮。用逻辑笔量引脚电平,发现 ODR 已经置 1 了,但引脚还是低电平。查了半天,原来是 MODER 没配成输出模式,默认是输入模式。嗯,寄存器配置的顺序很重要——先配模式,再写数据。
2.5 知识体系:裸机寄存器操作全景图
下面这张图,把本章的核心逻辑串起来了。建议你保存下来,以后写代码时对照着看。
2.6 实战要点总结
- 时钟先行: 任何外设操作前,先检查时钟是否使能。这是最容易被忽略的一步。
- 模式匹配: 输出引脚配输出模式,输入引脚配输入模式。配错了,灯不亮是小事,烧引脚是大事。
- 寄存器操作: 用结构体指针,别用裸地址。代码可读性和可维护性都很重要。
- 位带操作: 适合需要频繁、原子操作单个引脚的场景。但别滥用,位带区地址计算有开销。
最后提醒: 操作 BSRR 时,低 16 位是置位,高 16 位是复位。如果同时写同一个引脚的置位和复位位,置位优先。这个细节我踩过坑,分享给你。