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 从点灯看本质:裸机编程的核心思维

点灯看似简单,但它包含了裸机编程的所有要素:

  1. 时钟使能 —— 外设工作的前提
  2. 模式配置 —— 告诉芯片引脚干什么用
  3. 数据操作 —— 通过寄存器读写控制硬件

你想想看,串口通信、I2C、SPI,甚至更复杂的 DMA,底层逻辑都是一样的:配时钟、设模式、读写寄存器。点灯学会了,其他外设就是换汤不换药。

避坑指南: 我曾经在调试一块板子时,灯死活不亮。用逻辑笔量引脚电平,发现 ODR 已经置 1 了,但引脚还是低电平。查了半天,原来是 MODER 没配成输出模式,默认是输入模式。嗯,寄存器配置的顺序很重要——先配模式,再写数据。

2.5 知识体系:裸机寄存器操作全景图

下面这张图,把本章的核心逻辑串起来了。建议你保存下来,以后写代码时对照着看。

裸机寄存器操作知识体系 裸机寄存器操作知识体系 点灯(控制 GPIO) ① 使能时钟 ② 配置模式 ③ 读写数据 RCC->APB2ENR 设置对应位为 1 如:RCC->APB2ENR |= (1<<3) GPIOx->MODER 每 2 位控制一个引脚 01 = 输出模式 GPIOx->ODR / BSRR ODR:直接写电平 BSRR:原子置位/复位 进阶技巧:位带操作 → 原子级 bit 操作,代码更简洁 核心思维:时钟 → 模式 → 数据,万变不离其宗

2.6 实战要点总结

  • 时钟先行: 任何外设操作前,先检查时钟是否使能。这是最容易被忽略的一步。
  • 模式匹配: 输出引脚配输出模式,输入引脚配输入模式。配错了,灯不亮是小事,烧引脚是大事。
  • 寄存器操作: 用结构体指针,别用裸地址。代码可读性和可维护性都很重要。
  • 位带操作: 适合需要频繁、原子操作单个引脚的场景。但别滥用,位带区地址计算有开销。
最后提醒: 操作 BSRR 时,低 16 位是置位,高 16 位是复位。如果同时写同一个引脚的置位和复位位,置位优先。这个细节我踩过坑,分享给你。