目录
[1. I/O端口位的编程和访问限制](#1. I/O端口位的编程和访问限制)
[2. GPIOx_BSRR和GPIOx_BRR寄存器的作用](#2. GPIOx_BSRR和GPIOx_BRR寄存器的作用)
[3. IRQ不会发生危险的含义](#3. IRQ不会发生危险的含义)
[4. 具体例子](#4. 具体例子)
[5. 总结](#5. 总结)
在学习STM32的时候,我们最开始学习的就是控制GPIO成为点灯大师。本文将基于STM32系统结构图,解读GPIO的电路结构与工作模式,帮助初学者快速掌握STM32的GPIO控制核心基础。并使用C++进行封装,提高代码的可读性、可移植性。
一、GPIO原理图
在芯片手册中有一个描述了GPIO的电路图,通过对电路图的不同配置可以配置成不同的模式。

下面我们根据这个原理图分析一下各个电路模块有什么作用
VDD/VSS保护二极管:
位于 GPIO 引脚的上下两端,**用于防止引脚外部过高或过低的电压输入。**当引脚电压高于芯片供电电压(VDD)时,上方的二极管导通,将多余的电流引导到电源;当引脚电压低于地电位(VSS)时,下方的二极管导通,将电流引导到地,从而保护芯片免受损坏。
P-MOS 管和 N-MOS 管:
组成推挽输出电路的核心部分。在推挽输出模式下,当输出高电平时,P-MOS 管导通,N-MOS 管截止,引脚输出高电平;当输出低电平时,P-MOS 管截止,N-MOS 管导通,引脚输出低电平。推挽输出模式具有输出电流能力强、负载能力大等优点,适用于驱动外部负载。
不过单片机的P-MOS通常设置为弱上拉,主要是为了平衡驱动能力与功耗。让单片机既能生成高电平防止浮空状态的干扰,又能有效降低功耗。如果真的需要大电流的上拉功能,可以自行在芯片外部接一个上拉电阻或者驱动增强模块,一般情况下,这个弱上拉只是用做控制信号,而并非直接驱动。
施密特触发器:
图中写的是TTL肖特基触发器,可能是翻译的时候出问题了,实际上是叫做施密特触发器。它具有稳压和滤波的作用,能够将输入的模拟信号转换为稳定的数字信号。在输入模式下,施密特触发器可以过滤掉输入信号中的噪声干扰,确保输入电平的稳定性。
二、操作BSRR/BRR寄存器保证原子性
使用手册中有这样一句话:每个I/O端口位可以自由编程,然而必须按照32位字访问I/O端口寄存器(不允许半字或字节访问)。GPIOx_BSRR和GPIOx_BRR寄存器允许对任何GPIO寄存器进行读/更改的独立访问;这样,在读和更改访问之间产生IRQ时不会发生危险。
如何理解
1. I/O端口位的编程和访问限制
- 每个I/O端口位可以自由编程 :
- 这意味着GPIO的每个引脚(位)都可以独立设置为输入或输出,并且可以单独控制其状态(高电平或低电平)。
- 必须按照32位字访问I/O端口寄存器 :
- GPIO寄存器通常是一个32位的寄存器,访问时必须以32位为单位进行操作。尽管每个位都可以单独控制,但是必须要先记录所有位,然后统一写回操作。
- 不允许半字(16位)或字节(8位)访问 :
- 如果尝试以16位或8位的方式访问这些寄存器,可能会导致未定义的行为或硬件错误。
2. GPIOx_BSRR
和GPIOx_BRR
寄存器的作用
GPIOx_BSRR
(端口位设置/复位寄存器) :- 这是一个特殊的寄存器,允许同时设置(置1)或复位(清0)GPIO引脚的状态,而无需读取或修改其他引脚的状态。
- 通过写入不同的位,可以独立地设置或复位某个引脚,而不会影响其他引脚。
GPIOx_BRR
(端口位复位寄存器) :- 类似于
BSRR
,但专门用于复位(清0)GPIO引脚的状态。 - 在某些微控制器中,
GPIOx_BRR
可能被整合到GPIOx_BSRR
中,通过特定的位操作实现复位功能。
- 类似于
- 独立访问 :
GPIOx_BSRR
和GPIOx_BRR
的设计允许对GPIO寄存器进行独立的读/更改操作,而不需要先读取整个寄存器的值,修改后再写回。- 这种设计避免了在多任务或中断环境下可能出现的竞争条件(Race Condition)或数据不一致问题。
3. IRQ不会发生危险的含义
- IRQ(中断请求) :
- 当一个中断发生时,CPU会暂停当前任务,转而执行中断服务程序(ISR)。在ISR执行期间,如果其他任务或中断试图访问同一个GPIO寄存器,可能会导致数据竞争或不一致。
- 避免危险 :
- 使用
GPIOx_BSRR
和GPIOx_BRR
寄存器时,由于它们是独立访问的,不会读取或修改其他引脚的状态,因此即使在中断发生时,也不会影响其他引脚的状态。 - 这种设计减少了因中断导致的潜在错误,确保了系统的稳定性和可靠性。
- 使用
4. 具体例子
假设你正在修改某个GPIO引脚的状态(如设置高电平或低电平),以下操作可能导致问题:
- 传统方式:先读取当前状态,修改后再写回,可能导致数据竞争或冲突。
- **使用
GPIOx_BSRR
或GPIOx_BRR
直接修改寄存器值,可能导致其他任务(如显示控制)被中断或覆盖。 - 使用
GPIOx_BSRR
和GPIOx_BRR
:GPIOx_BSRR
通过位操作直接修改寄存器值,不会影响其他引脚。GPIOx_BRR
通过位清除操作实现复位,但可能覆盖其他位。
5. 总结
- 核心优势 :
GPIOx_BSRR
和GPIOx_BRR
寄存器的设计允许对GPIO引脚进行独立、安全的操作,避免了在多任务或中断环境下的数据竞争问题。
- 应用场景 :
- 在实时性要求高的场景(如电机控制、传感器数据采集)中,这种设计可以确保在中断发生时,不会因为数据竞争或覆盖问题导致系统错误。
通过这种方式,系统能够在高并发或中断环境下稳定运行,确保GPIO操作的原子性和一致性。
三、C++封装标准库的GPIO示例
cpp
#pragma once
#include "stm32f10x.h"
#include "stm32f10x_gpio.h" //GPIO模块
#include "stm32f10x_rcc.h" //RCC时钟控制模块
class GPIO_Base
{
private:
GPIO_TypeDef* port; // GPIO端口,如GPIOA, GPIOB等
uint16_t pin; // GPIO引脚号,如GPIO_Pin_0, GPIO_Pin_1等
public:
// 构造函数
GPIO_Base(GPIO_TypeDef* gpioPort, uint16_t gpioPin)
: port(gpioPort), pin(gpioPin) {}
// 初始化GPIO引脚(假设为输出模式)
void initAsOutput(uint32_t speed = GPIO_Speed_50MHz)
{
GPIO_InitTypeDef GPIO_InitStruct;
// 使能GPIO时钟(根据你的具体端口选择)
if (port == GPIOA)
{
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE);
}
else if (port == GPIOB)
{
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB, ENABLE);
}
// 可以根据需要添加更多端口的时钟使能
// 配置GPIO引脚
GPIO_InitStruct.GPIO_Pin = pin;
GPIO_InitStruct.GPIO_Mode = GPIO_Mode_Out_PP; // 推挽输出模式
GPIO_InitStruct.GPIO_Speed = (GPIOSpeed_TypeDef)speed;
GPIO_Init(port, &GPIO_InitStruct);
}
// 初始化GPIO引脚(假设为输入模式)
void initAsInput(GPIOMode_TypeDef mode = GPIO_Mode_IN_FLOATING)
{
GPIO_InitTypeDef GPIO_InitStruct;
// 使能GPIO时钟(根据你的具体端口选择)
if (port == GPIOA)
{
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE);
}
else if (port == GPIOB)
{
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB, ENABLE);
}
// 可以根据需要添加更多端口的时钟使能
// 配置GPIO引脚
GPIO_InitStruct.GPIO_Pin = pin;
GPIO_InitStruct.GPIO_Mode = mode; // 输入模式
GPIO_Init(port, &GPIO_InitStruct);
}
// 设置引脚为高电平
void setHigh()
{
GPIO_SetBits(port, pin);
}
// 设置引脚为低电平
void setLow()
{
GPIO_ResetBits(port, pin);
}
// 读取引脚状态
bool read()
{
return GPIO_ReadInputDataBit(port, pin);
}
};
cpp
#include"GPIO_Base.h"
// 示例用法
int main(void)
{
// 初始化系统时钟等(这里省略)
//通常不需要手动配置,stm32f10x系列默认是在启动文件中配置好了72MHz
// 创建一个GPIO_Base对象,假设使用GPIOA的Pin5
GPIO_Base led(GPIOA, GPIO_Pin_5);
// 初始化引脚为输出
led.initAsOutput();
while (1)
{
// 设置引脚为高电平
led.setHigh();
// 延时(这里省略具体的延时实现)
for (volatile int i = 0; i < 500000; i++); // 简单延时
// 设置引脚为低电平
led.setLow();
// 延时(这里省略具体的延时实现)
for (volatile int i = 0; i < 500000; i++); // 简单延时
}
}

调整为静态函数:
因为本人在后续封装别的模块的时候,发现如果对GPIO所有的功能都写到一个类中,非常不利于调整,所以修改为不再含有成员变量,仅仅封装处理函数,使用者直接输入GPIOx以及引脚pin号。
cpp
#include "stm32f10x.h" // 包含STM32F10x系列的标准外设库头文件
class GPIOHandler {
public:
// 静态方法:初始化GPIO引脚
static void InitGPIO(GPIO_TypeDef* gpioPort, uint16_t gpioPin, GPIOMode_TypeDef gpioMode) {
if (gpioPort == nullptr) return; // 简单的空指针检查
GPIO_InitTypeDef GPIO_InitStructure;
// 使能GPIO时钟
if (gpioPort == GPIOA) {
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE);
} else if (gpioPort == GPIOB) {
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB, ENABLE);
}
// 可以根据需要添加其他GPIO端口
// 配置GPIO
GPIO_InitStructure.GPIO_Pin = gpioPin;
GPIO_InitStructure.GPIO_Mode = gpioMode;
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(gpioPort, &GPIO_InitStructure);
}
// 静态方法:设置GPIO引脚输出高电平
static void SetPinHigh(GPIO_TypeDef* gpioPort, uint16_t gpioPin) {
if (gpioPort != nullptr) {
GPIO_SetBits(gpioPort, gpioPin);
}
}
// 静态方法:设置GPIO引脚输出低电平
static void SetPinLow(GPIO_TypeDef* gpioPort, uint16_t gpioPin) {
if (gpioPort != nullptr) {
GPIO_ResetBits(gpioPort, gpioPin);
}
}
};