【GPIO】从STM32F103入门GPIO寄存器

STM32 GPIO寄存器详解与操作对比

核心理论:每个GPIO端口(A-E)由7个寄存器控制,每组寄存器控制特定功能。下面按寄存器类型详细对比标准库和寄存器操作。


一、端口配置寄存器:GPIOx_CRL/CRH

功能 :控制引脚工作模式(输入/输出/复用)和输出速度
位结构

  • GPIOx_CRL :控制Pin0-Pin7(低8位)
  • GPIOx_CRH :控制Pin8-Pin15(高8位)
  • 每4位控制1个引脚(共16引脚 × 4bit = 64位)
场景1:配置PA2为浮空输入

标准库写法:

c 复制代码
// 1.使能时钟
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE);

// 2.配置引脚
GPIO_InitTypeDef gpio;
gpio.GPIO_Pin = GPIO_Pin_2;
gpio.GPIO_Mode = GPIO_Mode_IN_FLOATING;
GPIO_Init(GPIOA, &gpio);

寄存器写法:

c 复制代码
// 1.使能时钟
RCC->APB2ENR |= RCC_APB2ENR_IOPAEN;

// 2.配置CRL寄存器
GPIOA->CRL &= ~(0x0F << 8);  // 清空位[11:8]
GPIOA->CRL |=  (0x04 << 8);  // CNF=01(浮空输入), MODE=00(输入)

场景2:配置PA2为推挽输出(50MHz)

标准库写法:

c 复制代码
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE);

GPIO_InitTypeDef gpio;
gpio.GPIO_Pin = GPIO_Pin_2;
gpio.GPIO_Mode = GPIO_Mode_Out_PP;
gpio.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOA, &gpio);

寄存器写法:

c 复制代码
RCC->APB2ENR |= RCC_APB2ENR_IOPAEN;

GPIOA->CRL &= ~(0x0F << 8);  // 清空原有配置
GPIOA->CRL |=  (0x03 << 8);  // CNF=00(推挽), MODE=11(50MHz)

二、输入数据寄存器:GPIOx_IDR

功能 :读取引脚当前电平状态(只读)
位结构 :低16位对应引脚电平(0/1)

注意:该寄存器只能以16位的形式读出

场景:读取PA2电平

标准库写法:

c 复制代码
uint8_t value = GPIO_ReadInputDataBit(GPIOA, GPIO_Pin_2);

寄存器写法:

c 复制代码
// 方法1:位与操作(推荐)
uint8_t value = (GPIOA->IDR & GPIO_IDR_IDR2) ? 1 : 0;

// 方法2:移位法
uint8_t value = (GPIOA->IDR >> 2) & 0x01;

三、输出数据寄存器:GPIOx_ODR

功能 :控制输出电平 + 配置上拉/下拉电阻
位结构 :低16位控制输出电平

场景1:设置PA2输出高电平

标准库写法:

c 复制代码
GPIO_SetBits(GPIOA, GPIO_Pin_2);

寄存器写法:

c 复制代码
GPIOA->ODR |= GPIO_ODR_ODR2;  // 直接置位ODR对应位

场景2:配置PA2为上拉输入

标准库写法:

c 复制代码
GPIO_InitTypeDef gpio;
gpio.GPIO_Pin = GPIO_Pin_2;
gpio.GPIO_Mode = GPIO_Mode_IPU;  // 内置上拉配置
GPIO_Init(GPIOA, &gpio);

寄存器写法:

c 复制代码
// 配置CRL:CNF2=10(上拉输入), MODE2=00(输入)
GPIOA->CRL &= ~(GPIO_CRL_CNF2 | GPIO_CRL_MODE2); // 清除原有配置
GPIOA->CRL |= GPIO_CRL_CNF2_1;                   // CNF2[1]=1, CNF2[0]=0 → 10
// 或直接操作寄存器地址:
// *(volatile uint32_t*)0x40010800 &= ~(0x0F << 8); // 清除位[11:8]
// *(volatile uint32_t*)0x40010800 |= (0x08 << 8);   // 设置0x8(二进制1000)

// 启用上拉电阻
GPIOA->ODR |= GPIO_ODR_ODR2;  // ODR对应位置1
// 或直接操作寄存器地址:*(volatile uint32_t*)0x4001080C |= (1 << 2);

四、位设置寄存器:GPIOx_BSRR

功能 :原子操作输出电平(避免ODR直接操作冲突)
位结构

  • 低16位:置位引脚(1→高电平)
  • 高16位:复位引脚(1→低电平)
场景:PA2输出高电平+PA3输出低电平

标准库写法:

c 复制代码
GPIO_SetBits(GPIOA, GPIO_Pin_2);
GPIO_ResetBits(GPIOA, GPIO_Pin_3);

寄存器写法:

c 复制代码
// 单条指令完成双操作
GPIOA->BSRR = GPIO_BSRR_BS2 | GPIO_BSRR_BR3;

五、位清除寄存器:GPIOx_BRR

功能 :快速清除输出电平(专用于低电平输出)
位结构 :低16位控制清零操作

场景:设置PA2输出低电平

标准库写法:

c 复制代码
GPIO_ResetBits(GPIOA, GPIO_Pin_2);

寄存器写法:

c 复制代码
// 专用清零寄存器
GPIOA->BRR = GPIO_BRR_BR2;  

六、锁定寄存器:GPIOx_LCKR

功能 :锁定CRL/CRH配置防止意外修改
锁定序列 :写1→写0→写1→读0→读1

场景:锁定PA2配置

标准库写法:

c 复制代码
GPIO_PinLockConfig(GPIOA, GPIO_Pin_2);

寄存器写法:

c 复制代码
// 锁定序列实现
GPIOA->LCKR = GPIO_LCKR_LCK2;    // 选择锁定PA2
GPIOA->LCKR |= GPIO_LCKR_LCKK;   // Step1: LCKK=1
GPIOA->LCKR &= ~GPIO_LCKR_LCKK;  // Step2: LCKK=0
GPIOA->LCKR |= GPIO_LCKR_LCKK;   // Step3: LCKK=1
volatile uint32_t tmp = GPIOA->LCKR;  // Step4: 读LCKK(应为0)
tmp = GPIOA->LCKR;               // Step5: 读LCKK(应为1)

关键区别总结

  1. 配置效率

    • 标准库:封装性好,但存在函数调用开销
    • 寄存器:直接操作硬件,效率更高
  2. 多引脚操作

    • 标准库:需多次调用函数
    c 复制代码
    GPIO_SetBits(GPIOA, GPIO_Pin_0);
    GPIO_ResetBits(GPIOA, GPIO_Pin_1);
    • 寄存器:单条指令完成
    c 复制代码
    GPIOA->BSRR = GPIO_BSRR_BS0 | GPIO_BSRR_BR1;
  3. 代码可读性

    • 标准库:函数名自解释(GPIO_Mode_IN_FLOATING
    • 寄存器:需查阅手册理解位含义(CNF=01, MODE=00
  4. 安全性

    • 标准库:内置参数检查
    • 寄存器:直接操作硬件,需开发者保证正确性

扩展:✅位操作分析GPIOA_CRL &= ~(0xF << 8);

c 复制代码
// 正确操作
GPIOA_CRL &= ~(0xF << 8);  // 等价于 GPIOA_CRL &= 0xFFFFF0FF;
操作 二进制表示(32位) 作用范围
0xF 0000 0000 0000 1111
0xF << 8 0000 0000 1111 0000 0000 0000 位[11:8]区域
~(0xF << 8) 1111 1111 0000 1111 1111 1111 掩码(取反后)
最终效果 仅清空位[11:8] PA2专属区域

关键区别图示:

假设原始CRL值:0x12345678

复制代码
目标:仅清除PA2配置(位[11:8])

✅ 正确操作:
原始值:0001 0010 0011 0100 0101 0110 0111 1000
掩码:  1111 1111 1111 1111 0000 1111 1111 1111 (0xFFFFF0FF)
结果:  0001 0010 0011 0100 0000 0110 0111 1000 → 仅位[11:8]清零

❌ 错误操作:
原始值:0001 0010 0011 0100 0101 0110 0111 1000
掩码:  0000 0000 0000 0000 0000 0000 0000 0000 (全0)
结果:  0000 0000 0000 0000 0000 0000 0000 0000 → 整个寄存器清零!

最佳实践建议:

c 复制代码
// 推荐写法(可读性更高):
#define PA2_CLEAR_MASK  (0xF << 8)   // 定义清除掩码
GPIOA_CRL &= ~PA2_CLEAR_MASK;

// 或直接使用十六进制:
GPIOA_CRL &= ~0x00000F00;  // 0x00000F00 = (0xF << 8)

永远记住 :在嵌入式寄存器操作中,清零特定区域必须使用位掩码+取反,直接赋0会导致整个寄存器被意外清除!这是嵌入式开发中最常见的错误之一。


GPIO操作中移位运算的风险与BSRR/BRR解决方案

问题本质:直接操作ODR的风险

当直接使用移位运算操作GPIOx_ODR寄存器时,主要存在两个问题:

  1. 非原子操作:读-改-写过程可能被中断打断
  2. 位覆盖风险:移位操作可能意外改变其他引脚状态

风险代码示例

c 复制代码
// 危险操作:使用移位设置PA2输出高电平
GPIOA->ODR = (1 << 2);  // 将1左移2位后赋值给ODR

// 等效操作:
// 假设原始ODR = 0xFFFF (所有引脚高电平)
// 操作后ODR = 0x0004 (仅PA2高电平,其他全低)

移位操作风险详解

场景:同时控制PA2和PA3

c 复制代码
// 目标:PA2输出高,PA3输出低

// 错误实现:
GPIOA->ODR = (1 << 2);  // 设置PA2高
GPIOA->ODR = (0 << 3);  // 设置PA3低 → 实际清除了PA2!

// 实际效果:
// 第一条指令后:ODR = 0000 0000 0000 0100
// 第二条指令后:ODR = 0000 0000 0000 0000 (PA2也被清除)

位运算分析

c 复制代码
// 看似正确的错误写法:
GPIOA->ODR |= (1 << 2);  // 设置PA2高
GPIOA->ODR &= ~(1 << 3); // 清除PA3低

// 风险点:
// 1. 非原子操作:两条指令间可能被中断打断
// 2. 若PA2和PA3都需要改变,需要执行两次寄存器访问
// 3. 当多个任务操作同一GPIO端口时可能冲突

BSRR/BRR寄存器的解决方案

BSRR寄存器工作原理

  • 低16位 (BSy):置位操作 (1→高电平)
  • 高16位 (BRy):复位操作 (1→低电平)
  • 关键特性:写0的位不影响当前状态

安全实现方案

c 复制代码
// 原子操作:同时设置PA2高+PA3低
GPIOA->BSRR = (1 << 2) | (1 << (16 + 3)); 

// 位运算分解:
//  低16位: 0000 0000 0000 0100 (设置PA2)
//  高16位: 0000 0000 0000 1000 (清除PA3)
//  合并值: 0x00040008

BRR寄存器补充

c 复制代码
// 专用清零寄存器 (等效BSRR高16位)
GPIOA->BRR = (1 << 3);  // 清除PA3

// 等效于:
GPIOA->BSRR = (1 << (16 + 3));

对比实验

测试场景

控制开发板上两个LED:

  • LED1 (PA2):高电平点亮
  • LED2 (PA3):低电平点亮

危险代码(ODR移位)

c 复制代码
while(1) {
    // 尝试同时点亮两个LED
    GPIOA->ODR = (1 << 2);    // PA2高 (点亮LED1)
    GPIOA->ODR &= ~(1 << 3);  // PA3低 (点亮LED2)
    
    // 实际效果:LED1短暂亮后熄灭
    // 原因:第二行清除了PA2
}

安全代码(BSRR)

c 复制代码
while(1) {
    // 原子操作同时控制两个LED
    GPIOA->BSRR = (1 << 2) | (1 << (16 + 3)); 
    // LED1亮(PA2高) + LED2亮(PA3低)
    
    // 延时后关闭
    delay_ms(500);
    GPIOA->BSRR = (1 << (16 + 2)) | (1 << 3); 
    // LED1灭(PA2低) + LED2灭(PA3高)
}

位运算原理图解

ODR直接操作风险

复制代码
初始状态:PA0-PA15全高 (ODR=0xFFFF)
目标:设置PA2高,保持其他位不变

错误操作:
  ODR = (1 << 2) → 二进制 0000 0100
  结果:PA2高,但其他所有引脚被强制置低!

BSRR安全操作

复制代码
初始状态:任意
操作:BSRR = (1 << 2) | (1 << 19) 

位分解:
  [31:16] BR: 0000 0000 0000 1000 (清除PA3)
  [15:0]  BS: 0000 0000 0000 0100 (设置PA2)

效果:
  仅修改PA2和PA3,其他引脚保持不变

使用原则总结

  1. 设置单个引脚高电平

    c 复制代码
    // 推荐
    GPIOx->BSRR = (1 << Pin);
    
    // 避免
    GPIOx->ODR |= (1 << Pin);
  2. 设置单个引脚低电平

    c 复制代码
    // 推荐
    GPIOx->BRR = (1 << Pin);
    // 或
    GPIOx->BSRR = (1 << (16 + Pin));
  3. 同时设置多个引脚

    c 复制代码
    // 原子操作
    GPIOx->BSRR = (1 << PinA) | (1 << (16 + PinB));
  4. 切换引脚状态

    c 复制代码
    // 最优方案
    GPIOx->ODR ^= (1 << Pin);  // 异或操作切换状态
    
    // 替代方案(两条指令)
    if(GPIOx->ODR & (1 << Pin)) 
        GPIOx->BRR = (1 << Pin);
    else
        GPIOx->BSRR = (1 << Pin);

关键结论:BSRR/BRR寄存器通过"只影响目标位"的设计,从根本上解决了ODR直接操作时的位覆盖问题,同时提供原子操作保证,是多引脚控制场景的最佳选择。