--- tags: [source-summary] type: source source: "尚硅谷嵌入式技术之STM32单片机(基础篇)V1.0.2 — 第4~5章:点亮LED案例 + GPIO详解" author: "尚硅谷研究院" date: 2026-07-15 created: 2026-07-15 --- # GPIO寄存器详解与三步进化法 > **用生活理解**:GPIO 就像一排开关(每个引脚一个),你可以用程序控制每个开关输出高/低电平(控制 LED),也可以读取每个开关的状态(读取按键)。"三步进化法"就是操作这些开关的方式从"直接掰电线"进化到"用遥控器按按钮"再到"声控"的过程。 --- ## GPIO概述 GPIO(General-purpose input/output,通用型输入输出)是 STM32 芯片与外界交互的最基本途径。 ### 总体说明 - STM32F103ZET6 共有 **7 组 GPIO 端口**:GPIOA~GPIOG,每组 16 个引脚,共 112 个 GPIO - 每个引脚电平范围:0~3.3V,部分引脚可兼容 5V - 最快翻转速度:每 2 个时钟周期翻转一次(最高 50MHz) - 每个 GPIO 均可作为外部中断输入 - 每个引脚可配置为 8 种工作模式之一 > **参考**:STM32F103ZET6 数据手册 §3(引脚功能定义表) > **参考**:开发板原理图(LED/KEY 电路部分) ### GPIO的主要特点 | 特性 | 说明 | | -------- | ------------------------------- | | 引脚电平 | 0~3.3V(FT 引脚兼容 5V) | | 翻转速度 | 最快 50MHz(2 个时钟周期) | | 上下拉 | 内置可配置上下拉电阻(约 40KΩ) | | 驱动能力 | 最大 ±25mA 拉/灌电流 | | 中断能力 | 每个引脚均可作为 EXTI 中断输入 | --- ## GPIO内部结构详解 ![GPIO 内部结构图](assets/gpio_内部结构.png) ### 保护二极管 GPIO 引脚内部有两个保护二极管,分别连接到 VDD(3.3V)和 VSS(0V): - 当输入电压高于 VDD+0.3V 时,上方二极管导通,将电压钳位到 VDD - 当输入电压低于 VSS-0.3V 时,下方二极管导通,将电压拉高到 VSS > **实际意义**:防止静电或意外的高/低电压损坏芯片。但这不是绝对的,超过极限电压仍会烧毁。 ### 上下拉电阻 两个开关控制引脚内部的上下拉电阻(典型值 40KΩ): - **上拉**:上开关闭合 → 引脚默认高电平 - **下拉**:下开关闭合 → 引脚默认低电平 - **浮空**:两个开关都断开 → 引脚电平不确定(高阻态) - **两个同时闭合**:VDD 到 VSS 短路 → 费电且无意义,不会这么用 ### 施密特触发器(Schmitt Trigger) 施密特触发器是一个带**迟滞特性**的比较器电路: - 输入信号从低到高:阈值约为 1.8V - 输入信号从高到低:阈值约为 1.2V - 这种**滞回特性**可以滤除信号上的噪声抖动,实现波形整形 施密特触发器的输出进入输入数据寄存器(IDR),供 CPU 读取。 > **注意**:在模拟输入模式下,施密特触发器被**关闭**以降低功耗。 ### 输出驱动电路 - **P-MOS**(上管):导通时输出高电平(VDD) - **N-MOS**(下管):导通时输出低电平(VSS) - **推挽输出**:P-MOS 和 N-MOS 交替导通,可输出高或低电平 - **开漏输出**:P-MOS 始终关断,只有 N-MOS 工作(只能输出低电平,高电平靠外部上拉电阻) --- ## GPIO的8种工作模式 | 模式 | 类型 | 用途 | P-MOS | N-MOS | 施密特触发器 | 上下拉 | | ------------ | ---- | ------------------ | ----- | ----- | ------------ | ------ | | 输入浮空 | 输入 | 按键、通信引脚 | 关闭 | 关闭 | 开 | 禁止 | | 输入上拉 | 输入 | 按键(默认高电平) | 关闭 | 关闭 | 开 | 上拉 | | 输入下拉 | 输入 | 按键(默认低电平) | 关闭 | 关闭 | 开 | 下拉 | | 模拟输入 | 输入 | ADC 采集 | 关闭 | 关闭 | **关闭** | 禁止 | | 通用推挽输出 | 输出 | LED、普通信号 | 交替 | 交替 | 开 | 禁止 | | 通用开漏输出 | 输出 | I2C 总线、电平转换 | 关闭 | 交替 | 开 | 禁止 | | 复用推挽输出 | 输出 | USART_TX、SPI_SCK | 交替 | 交替 | 开 | 禁止 | | 复用开漏输出 | 输出 | I2C_SCL/SDA | 关闭 | 交替 | 开 | 禁止 | ### 常见模式选择指导 | 应用场景 | 推荐模式 | | -------- | ---------------------------- | | 驱动 LED | 推挽输出 | | 读取按键 | 上拉输入 或 下拉输入 | | I2C 通信 | 复用开漏输出(需要外部上拉) | | 串口 TX | 复用推挽输出 | | 串口 RX | 浮空输入 或 上拉输入 | | ADC 采样 | 模拟输入 | | SPI 通信 | 复用推挽输出 | | PWM 输出 | 复用推挽输出 | --- ## GPIO的7个关键寄存器 每个 GPIO 端口有 7 个 32 位寄存器,基地址如下: - GPIOA: `0x4001 0800` - GPIOB: `0x4001 0C00` - GPIOC: `0x4001 1000` - GPIOD: `0x4001 1400` - GPIOE: `0x4001 1800` - GPIOF: `0x4001 1C00` - GPIOG: `0x4001 2000` > **参考**:参考手册 §1(存储器映射)+ §8(GPIO 寄存器描述) ### 1. GPIOx_CRL — 端口配置低寄存器(Address: 0x00) 控制 **Pin0~Pin7** 的配置模式。 | 位域 | 名称 | 说明 | | ----- | ---------- | ----------- | | 31:30 | CNF7[1:0] | Pin7 配置位 | | 29:28 | MODE7[1:0] | Pin7 模式位 | | ... | ... | ... | | 3:2 | CNF0[1:0] | Pin0 配置位 | | 1:0 | MODE0[1:0] | Pin0 模式位 | **MODE 位含义**: | MODE[1:0] | 模式 | | --------- | -------------------- | | 00 | 输入模式(复位状态) | | 01 | 输出,最大速度 10MHz | | 10 | 输出,最大速度 2MHz | | 11 | 输出,最大速度 50MHz | **CNF 位含义(输入模式下,即 MODE=00)**: | CNF[1:0] | 模式 | | -------- | -------------------- | | 00 | 模拟输入 | | 01 | 浮空输入(复位状态) | | 10 | 上拉/下拉输入 | | 11 | 保留 | **CNF 位含义(输出模式下,即 MODE>00)**: | CNF[1:0] | 模式 | | -------- | ---------------- | | 00 | 通用推挽输出 | | 01 | 通用开漏输出 | | 10 | 复用功能推挽输出 | | 11 | 复用功能开漏输出 | > **配置示例**:PA0 推挽输出 50MHz → MODE0[1:0]=11, CNF0[1:0]=00 → CRL 低 4 位 = 0011 = 0x3 ### 2. GPIOx_CRH — 端口配置高寄存器(Address: 0x04) 与 CRL 结构完全相同,控制 **Pin8~Pin15**。 ### 3. GPIOx_IDR — 端口输入数据寄存器(Address: 0x08) | 位 | 说明 | | ----- | ----------------------------------------- | | 31:16 | 保留 | | 15:0 | IDRy:读取 Pin y 的输入电平(0=低,1=高) | ### 4. GPIOx_ODR — 端口输出数据寄存器(Address: 0x0C) | 位 | 说明 | | ----- | ----------------------------------------- | | 31:16 | 保留 | | 15:0 | ODRy:写入 Pin y 的输出电平(0=低,1=高) | ### 5. GPIOx_BSRR — 端口位设置/清除寄存器(Address: 0x10) | 位 | 名称 | 功能 | | ----- | ---- | ------------------------------ | | 31:16 | BRy | 清除 ODR 的对应位(写 1 清 0) | | 15:0 | BSy | 设置 ODR 的对应位(写 1 置 1) | > **特点**:BSRR 是**原子操作**,同时写低 16 位和高 16 位不会冲突。 > **对比**:`GPIOA->BSRR = (1<<0)` 相当于 `GPIOA->ODR |= (1<<0)`,但 BSRR 不需要"读-改-写"三步,不会被打断。 ### 6. GPIOx_BRR — 端口位清除寄存器(Address: 0x14) | 位 | 说明 | | ----- | ----------------------------------- | | 31:16 | 保留 | | 15:0 | BRy:清除 ODR 的对应位(写 1 清 0) | 相当于 BSRR 高 16 位的简化版。 ### 7. GPIOx_LCKR — 端口配置锁定寄存器(Address: 0x18) 用于锁定 CRL/CRH 的配置,锁定后下次复位前**不可更改**。 锁定时序:写 1 → 写 0 → 写 1 → 读 0 → 读 1 --- ## 操作寄存器方式的"三步进化法" 尚硅谷的教学特色:用同一个功能(点亮 LED)展示三种逐渐优化的写法。 ### 硬件电路 LED1 连接 PA0,低电平点亮。 ![LED 电路原理图](assets/led_电路原理图.png) **原理**: ``` PA0=1(3.3V) → LED 灭(两端电压差接近 0) PA0=0(0V) → LED 亮(电流经 VCC→限流电阻→LED→PA0→GND) ``` 限流电阻计算:`R = (VCC - V_LED) / I_LED`,LED 工作电流约 5~10mA。 ### 进化 0:裸地址操作(直接操作内存) **文件:`stm32/01_led_register/User/main.c`** ```c #include "stm32f10x.h" int main(void) { /* 1. 开启GPIOA的时钟 */ // RCC_APB2ENR 寄存器的地址 = RCC基地址(0x40021000) + 偏移(0x18) = 0x40021018 // 第2位置1表示开启GPIOA时钟 // 0x00000004 = 二进制 0000 0000 0000 0000 0000 0000 0000 0100 // ↑第2位=1 *(uint32_t *)(0x40021000 + 0x18) = 4; /* 2. 配置PA0为推挽输出模式(最大速度50MHz) */ // GPIOA_CRL 寄存器的地址 = GPIOA基地址(0x40010800) + 偏移(0x00) = 0x40010800 // PA0对应CRL的最低4位: // MODE0[1:0] = 11 (50MHz输出) // CNF0[1:0] = 00 (通用推挽输出) // 所以低4位 = 0011 = 0x3 // 注意:这会覆盖PA1~PA7的配置(其他位变成0),是粗暴的做法 *(uint32_t *)(0x40010800 + 0x00) = 3; /* 3. PA0输出低电平(点亮LED) */ // GPIOA_ODR 寄存器的地址 = GPIOA基地址(0x40010800) + 偏移(0x0C) = 0x4001080C // 第0位置0 = 输出低电平;其他位置1 = 不影响其他引脚 // 0x0000FFFE = 二进制 ... 1111 1111 1111 1110 // ↑第0位=0 *(uint32_t *)(0x40010800 + 0x0C) = 0xFFFE; while (1) { } } ``` > **问题**:每次查手册计算地址,烦琐;直接赋值覆盖了其他位(不安全)。 ### 进化 1:使用结构体宏定义 ST 官方在 `stm32f10x.h` 中用结构体封装了所有寄存器: ```c // stm32f10x.h 中的定义(简化版) #define PERIPH_BASE ((uint32_t)0x40000000) // 外设基地址 #define AHBPERIPH_BASE (PERIPH_BASE + 0x20000) // AHB外设基地址 #define RCC_BASE (AHBPERIPH_BASE + 0x1000)// RCC基地址 #define RCC ((RCC_TypeDef *) RCC_BASE)// RCC结构体指针 typedef struct { __IO uint32_t CR; // 偏移 0x00 __IO uint32_t CFGR; // 偏移 0x04 __IO uint32_t CIR; // 偏移 0x08 __IO uint32_t APB2RSTR; // 偏移 0x0C __IO uint32_t APB1RSTR; // 偏移 0x10 __IO uint32_t AHBENR; // 偏移 0x14 __IO uint32_t APB2ENR; // 偏移 0x18 ← 我们用的寄存器 __IO uint32_t APB1ENR; // 偏移 0x1C /* ... */ } RCC_TypeDef; ``` > **巧妙之处**:结构体成员地址连续,CR 偏移 0x00,CFGR 偏移 0x04,APB2ENR 偏移 0x18(6×4=24=0x18),与查手册结果一致。 利用这个封装,代码进化为: **文件:`stm32/01_led_register/User/main.c`** ```c #include "stm32f10x.h" int main(void) { RCC->APB2ENR = 4; // RCC->APB2ENR 等价于 *(uint32_t*)(0x40021018) GPIOA->CRL = 3; // GPIOA->CRL 等价于 *(uint32_t*)(0x40010800) GPIOA->ODR = 0xFFFE; // GPIOA->ODR 等价于 *(uint32_t*)(0x4001080C) while (1) { } } ``` > **进步**:不再手动计算地址,代码可读性大幅提升。 > **仍存在的问题**:直接写值覆盖了整个寄存器,只操作特定某位才安全。 ### 进化 2:使用位运算实现只改变某一位 **文件:`stm32/01_led_register/User/main.c`** ```c #include "stm32f10x.h" int main(void) { // 1. 开启GPIOA时钟 // RCC->APB2ENR 的第2位置1,保持其他位不变 RCC->APB2ENR |= (1 << 2); // |= (1<<2) = 按位或: 将第2位置1,其他位不变 // 相当于: RCC->APB2ENR = RCC->APB2ENR | 0x00000004 // 2. 配置PA0为推挽输出50MHz // 先清除CNF0[1:0]=00 (清3,2位) GPIOA->CRL &= ~(1 << 3); // &= ~(1<<3): 按位与+取反: 保留其他位,将第3位清0 GPIOA->CRL &= ~(1 << 2); // 将第2位清0 // 再设置MODE0[1:0]=11 (置1,0位) GPIOA->CRL |= (1 << 1); // |= (1<<1): 按位或: 将第1位置1 GPIOA->CRL |= (1 << 0); // 将第0位置1 // 3. PA0输出低电平 GPIOA->ODR &= ~(1 << 0); // 将第0位清0,输出低电平 while (1) { } } ``` > **进步**:位运算只修改目标位,不影响其他位的原有配置。 > **仍存在的问题**:`1<<2` 这种写法需要查手册知道要移位几。 ### 进化 3:使用 ST 预定义的位宏(最终形态) ST 在 `stm32f10x.h` 中把每个位域的值也预先定义好了宏: ```c // stm32f10x.h 中的宏定义示例 #define RCC_APB2ENR_IOPAEN ((uint32_t)0x00000004) // = 1<<2 #define GPIO_CRL_MODE0 ((uint32_t)0x00000003) // MODE0的掩码 = 11B #define GPIO_CRL_MODE0_1 ((uint32_t)0x00000002) // MODE0的第1位 #define GPIO_CRL_MODE0_0 ((uint32_t)0x00000001) // MODE0的第0位 #define GPIO_CRL_CNF0 ((uint32_t)0x0000000C) // CNF0的掩码 = 1100B #define GPIO_CRL_CNF0_1 ((uint32_t)0x00000008) // CNF0的第3位 #define GPIO_CRL_CNF0_0 ((uint32_t)0x00000004) // CNF0的第2位 #define GPIO_ODR_ODR0 ((uint32_t)0x00000001) // ODR的第0位 ``` 利用这些宏,最终版代码: **文件:`stm32/01_led_register/User/main.c`** ```c #include "stm32f10x.h" int main(void) { // 1. 开启GPIOA时钟 // RCC_APB2ENR_IOPAEN = 0x00000004 = 1<<2 // |= 按位或: 将IOPAEN位置1,保持其他时钟位不变 RCC->APB2ENR |= RCC_APB2ENR_IOPAEN; // 2. 配置PA0为推挽输出 // GPIO_CRL_CNF0 = 0xC,清除CNF0两位(CNF0_1和CNF0_0) GPIOA->CRL &= ~GPIO_CRL_CNF0; // GPIO_CRL_MODE0 = 0x3,设置MODE0[1:0] = 11(50MHz输出) GPIOA->CRL |= GPIO_CRL_MODE0; // 3. PA0输出低电平 // GPIO_ODR_ODR0 = 0x1,将ODR0位清0 GPIOA->ODR &= ~GPIO_ODR_ODR0; while (1) { } } ``` > **最终形态**:代码简洁、可读性强、安全、易于维护。**这就是实际项目中推荐的方式。** ### 三步进化法总结 | 阶段 | 写法 | 优点 | 缺点 | | --------- | -------- | ------------------ | ------------------ | | 进化0 | 裸地址 | 理解硬件本质 | 烦琐、不安全 | | 进化1 | 结构体宏 | 可读性好 | 仍可能覆盖其他位 | | 进化2 | 位运算 | 只改需要的位 | 需查手册知移位次数 | | **进化3** | **位宏** | **最安全、最可读** | 需要了解宏名含义 | --- ## 核心位操作速查表 | 操作 | 语法 | 解释 | | --------- | --------------------- | ---------------------------------- | | 位置1 | `REG \|= BITMASK;` | 按位或:指定位设为1,其他位不变 | | 位清0 | `REG &= ~BITMASK;` | 按位与+取反:其他位不变,指定位清0 | | 位翻转 | `REG ^= BITMASK;` | 按位异或:指定位置1变0,0变1 | | 位移位 | `x << N` | 左移N位(相当于乘 2^N) | | 位移位 | `x >> N` | 右移N位(相当于除 2^N) | | 测试位 | `if (REG & BITMASK)` | 按位与:测试指定位是否为1 | | 连续位置1 | `REG \|= (MASK << N)` | 将 N 位开始的 MASK 位置1 | | 连续位清0 | `REG &= ~(MASK << N)` | 将 N 位开始的 MASK 位清0 | > **典型技巧**:`先清除后设置` 是嵌入式位操作的黄金法则。对连续位域(如 MODE[1:0])总是先 `&= ~MASK` 清空,再 `|= VALUE` 设置。 --- ## 核心函数速查表 (本笔记以寄存器操作为主,无标准库/HAL 库函数) | 操作 | 寄存器方式 | 说明 | | ---------- | ------------------------------------- | -------------- | | 设置输出高 | `GPIOx->BSRR = (1<BRR = (1<IDR >> n) & 1` | 提取第n位值 | | 翻转输出 | `GPIOx->ODR ^= (1<