寄存器读写基础
本笔记仍在学习中。目前只整理 volatile、寄存器基本读写和结构体映射。
当前覆盖范围
| 已整理 | 尚待补齐 |
|---|---|
volatile 的语义、读写指针、GPIO 地址映射、结构体访问 |
只读/只写/写 1 清零寄存器 |
STM32F1 GPIOC 的 IDR、ODR、BSRR 示例 |
读—改—写的并发风险与内存屏障 |
| CMSIS 头文件与裸寄存器写法的边界 | 不同厂商寄存器访问宏的对照 |
下文地址基于 STM32F1 GPIOC。更换芯片或 GPIO 端口时必须重新查参考手册;不要把示例地址直接复制到其他系列。
1. 理解 volatile 关键字
volatile 告诉编译器:这个对象可能在程序正常控制流之外发生变化,例如被硬件外设或中断修改。程序每次读写它时,都需要保留对应的实际访问。
为什么寄存器需要 volatile
未使用 volatile:
uint32_t *reg = (uint32_t *)0x40011008UL;
while ((*reg & 0x01UL) != 0U) {
/* 等待某一位变为 0。 */
}
编译器可能认为循环期间 *reg 不会变化,从而合并或省略重复读取。
使用 volatile:
volatile const uint32_t *reg =
(volatile const uint32_t *)0x40011008UL;
while ((*reg & 0x01UL) != 0U) {
/* 每次判断都会重新读取寄存器。 */
}
这里加入 const,表示软件只读取该寄存器,不通过这个指针写入。
volatile 的主要作用
保留每一次访问
volatile const uint32_t *reg =
(volatile const uint32_t *)0x40011008UL;
uint32_t value1 = *reg;
uint32_t value2 = *reg;
两次读取都会保留,因为硬件状态可能在两次访问之间发生变化。
按程序表达式执行易失访问
volatile uint32_t *reg1 =
(volatile uint32_t *)0x4001100CUL;
volatile uint32_t *reg2 =
(volatile uint32_t *)0x40011010UL;
*reg1 = 0x01UL;
*reg2 = 0x02UL;
volatile 会限制编译器对这些易失访问的删除、合并和随意移动。
读取硬件的最新状态
volatile const uint32_t *status_reg =
(volatile const uint32_t *)0x40011008UL;
while ((*status_reg & 0x01UL) == 0U) {
/* 等待硬件就绪。 */
}
volatile 不等于原子操作,也不能替代 CPU Cache 维护、临界区或内存屏障。它主要约束编译器如何处理这个对象。
2. 寄存器的基本读写
下面以 STM32F1 的 GPIOC 为例。地址仅适用于对应芯片系列,实际项目应优先使用 CMSIS 设备头文件中的定义。
| 寄存器 | GPIOC 地址 | 作用 |
|---|---|---|
IDR |
0x40011008 |
输入数据寄存器 |
ODR |
0x4001100C |
输出数据寄存器 |
BSRR |
0x40011010 |
位设置/清除寄存器 |
读取整个寄存器
#include <stdint.h>
uint32_t Read_GPIOC_IDR(void)
{
volatile const uint32_t *idr =
(volatile const uint32_t *)0x40011008UL;
return *idr;
}
读取某一位
uint32_t Read_Register_Bit(volatile const uint32_t *reg,
uint8_t bit_pos)
{
return (*reg >> bit_pos) & 0x01UL;
}
当前示例假设 bit_pos 的范围为 0~31。
写入整个寄存器
void Write_GPIOC_ODR(uint32_t value)
{
volatile uint32_t *odr =
(volatile uint32_t *)0x4001100CUL;
*odr = value;
}
原始示例把 0x40011010 当成 ODR,实际该地址是 BSRR;GPIOC 的 ODR 地址应为 0x4001100C。这一差异会直接改变写入行为,必须在代码审查时单独确认。
修改某一位
void Write_Register_Bit(volatile uint32_t *reg,
uint8_t bit_pos,
uint8_t value)
{
uint32_t mask = 1UL << bit_pos;
if (value != 0U) {
*reg |= mask;
} else {
*reg &= ~mask;
}
}
这是“读—改—写”操作。若中断或其他执行单元同时修改该寄存器,可能产生竞争;它也不适用于只写寄存器、写 1 清零寄存器等特殊类型。具体规则必须查参考手册。
3. 使用结构体访问寄存器
按照参考手册中的寄存器偏移定义结构体,可以用成员名称访问寄存器:
#include <stdint.h>
typedef struct {
volatile uint32_t CRL; /* 0x00:端口配置低寄存器 */
volatile uint32_t CRH; /* 0x04:端口配置高寄存器 */
volatile uint32_t IDR; /* 0x08:输入数据寄存器 */
volatile uint32_t ODR; /* 0x0C:输出数据寄存器 */
volatile uint32_t BSRR; /* 0x10:位设置/清除寄存器 */
volatile uint32_t BRR; /* 0x14:位清除寄存器 */
volatile uint32_t LCKR; /* 0x18:配置锁定寄存器 */
} GPIO_Registers;
#define GPIOA_BASE 0x40010800UL
#define GPIOB_BASE 0x40010C00UL
#define GPIOC_BASE 0x40011000UL
#define GPIOA ((GPIO_Registers *)GPIOA_BASE)
#define GPIOB ((GPIO_Registers *)GPIOB_BASE)
#define GPIOC ((GPIO_Registers *)GPIOC_BASE)
使用示例:
void GPIO_Operations_Example(void)
{
uint32_t input = GPIOC->IDR;
GPIOC->ODR = 0x2000UL;
GPIOC->BSRR = 1UL << 13U; /* PC13 置高。 */
GPIOC->BSRR = 1UL << (13U + 16U); /* PC13 置低。 */
(void)input;
}
设备厂商提供的 CMSIS 头文件已经定义了这类寄存器结构体和端口指针。这里重新定义只是为了理解映射原理,实际工程不要与官方的 GPIO_TypeDef 重复定义。
待继续
- 常用位置位、清零、翻转和掩码操作
- 只读、只写和写 1 清零寄存器
- 读—改—写的并发风险
- BSRR 等原子位操作寄存器
- 内存屏障与访问顺序