嵌入式笔记:GPIO
CommentGPIO
1.1PIN设备
单片机通过引脚(Pin)与外部连接。引脚可以输出高低电平来控制外部器件,也可以读取外部器件输入的电平状态。相当于是一种抽象概念,没有涉及到具体电路(把各种引脚形态封装好了的一种概念,只管调用)。这里的所有操作都基于操作系统(RT-Thread),要操作真正的物理引脚也可以绕过操作系统直接写引脚的寄存器(不赘述)。
PIN输出
PIN输出可以获取引脚编号、设置引脚为输出模式(写操作)可以写高/低电平。当调用 PIN 设备的输出接口写1或0时,总线会将指令传送到GPIO外设的寄存器,改变内部电路状态,最终让物理引脚输出3.3V(高电平)或0V(低电平)。
使用方式:
Step 1:获取引脚编号。
Step 2:使用 rt_pin_mode() 将引脚配置为输出模式。
Step 3:使用 rt_pin_write() 输出高低电平来控制外设。
PIN输入
外部电路改变了引脚的电压,这个电压被GPIO内部的输入数据寄存器捕获。当调用读取接口时,CPU就会去读取那个寄存器的地址,从而让软件“感知”到外部的状态。
使用方式:
轮询法(主动查,耗时,且切换频繁,不太实用):
Step 1: 获取引脚编号。
Step 2: 配置为输入模式(通常需要结合上下拉)。
Step 3: 在 while(1) 循环中不断使用 rt_pin_read() 去读取电平状态。
中断法(被动等待通知,效率最高):
Step 1: 获取引脚编号。
Step 2: 配置为输入模式。
Step 3: 编写一个中断回调函数。
Step 4: 使用 rt_pin_attach_irq() 将引脚、触发条件(如电平跳变)与回调函数绑定。
Step 5: 使用 rt_pin_irq_enable() 开启中断。
核心操作
内存映射 I/O
(Memory-Mapped I/O)
在单片机中,特定的内存地址并非连接真正的内存,而是映射到了外设(如 GPIO)的寄存器上。当程序向某个指定的寄存器地址写入数据时,芯片内部的总线会将这个请求传送到对应的 GPIO 外设,外设内部的电路状态随之改变,最终导致物理引脚的电平发生翻转。
1.2GPIO原理与结构
GPIO是一种真实的物理引脚,与pin的区别就在于它是具体的,而非笼统的概念。每种单片机的GPIO可能会有不同的电路设计,但此处不深入细究(不然就写太多了)。
GPIO (General Purpose Input Output),即通用输入输出,可以负责接收/产生数字电平()GPIO IN/GPIO OUT。常规的两种电平有两种,高电平1、低电平0。GPIO在输入状态(IN)接收电平、接受控制,而在输出状态(OUT)产生电平、输出控制。
GPIO内部结构:
输入/输出数据寄存器
输入数据寄存器 (IDR - Input Data Register) :
记录当前物理引脚上的真实电压状态。
是一个只读寄存器。硬件电路会每隔一个时钟周期自动去采样一次物理引脚的电压。如果是 3.3V,硬件就把IDR寄存器对应的比特位置为1;如果是0V,就置为0。
输出数据寄存器 (ODR - Output Data Register) :
决定单片机“期望”物理引脚输出什么电平。
是一个可读可写的寄存器。当你的程序向 ODR 寄存器的某一位写入 1 时,内部硬件就会驱动 P-MOS 管导通,尝试向外输出 3.3V 高电平。
ODR 的弱点: 如果你想让 PA0 输出高电平,但不想影响 PA1-PA15,你不能直接给 ODR 赋值。CPU 必须先读出 ODR 当前的值,把第0位改成1,然后再整体写回 ODR。这个“读-改-写”的过程需要执行3条底层汇编指令,在这个过程中如果被中断打断,就会造成严重的数据混乱。
BSRR : 正是因为 ODR 存在上述缺陷,STM32 硬件工程师专门设计了BSRR(端口置位/复位寄存器)。向 BSRR 写入数据是单指令的硬件级操作。BSRR 收到指令后,会在底层直接去修改 ODR,从而极大地提升了 IO 翻转的速度和安全性。
在绝大多数情况下,期望和现实是一致的(ODR 写 1,IDR 读出来也是 1)。但在开漏输出(OLED I2C 通信)场景下,就会出现期望与现实的分歧:当你向 ODR 写 1 时(开漏模式下等于松手不输出),如果总线上的其他设备把电平强行拉低了,此时你去读 IDR,读回来的现实就是 0。单片机正是通过对比 ODR 和 IDR 的差异,实现了高级的通信协议仲裁。
模式详解
输出模式:
推挽输出 (PIN_MODE_OUTPUT): 输出高电平时,连通 VCC(强推);输出低电平时,连通 GND(强挽)。最常用的输出模式,驱动能力强。
开漏输出 (PIN_MODE_OUTPUT_OD): 输出低电平时连通 GND;但要求输出高电平时,引脚处于“悬空/高阻”状态。它自己无法输出高电平,必须依靠外部电路接一个“上拉电阻”才能把电平拉高。用于实现“线与”逻辑,最典型的应用就是 **I2C ** 通信总线 。
输入模式 :
浮空输入 (PIN_MODE_INPUT): 内部上下拉电阻均断开。引脚电平完全由外部接入的信号决定。如果外部没有任何连接,引脚电平会像天线一样捕捉环境干扰,处于不确定的随机跳变状态。通常用于标准的外部数字信号输入,或者作为 ADC(模数转换)的模拟电压采集通道。
上拉输入 (PIN_MODE_INPUT_PULLUP) :闭合内部连接到 VCC 的上拉电阻。在外部没有信号输入时,引脚默认在 高电平 。
**应用:**按键接地的场景。比如你的 KEY2 (PA1) ,按下是低电平,松开时为了防止电平乱跳,必须配置为上拉输入,让它松开时稳定保持高电平。
下拉输入 (PIN_MODE_INPUT_PULLDOWN) :闭合内部连接到 GND 的下拉电阻。外部无信号时,引脚默认被“稳住”在 低电平 。
应用:按键接 VCC 的场景。比如 KEY1(PA0) ,按下是高电平,需要配置软件下拉电阻,让它松开时稳定在低电平。
中断触发模式 :
下降沿触发**(PIN_IRQ_MODE_FALLING):**捕获高电平瞬间变为低电平的瞬间。适用于检测上拉输入的按键按下的瞬间。
上升沿触发 (PIN_IRQ_MODE_RISING) :捕获低电平瞬间变为高电平的瞬间。适用于检测下拉输入的按键按下的瞬间。
双边沿触发 **(PIN_IRQ_MODE_RISING_FALLING):**高电平低电平互换瞬间都会触发。
1.3中断功能
回调函数的理解与外部中断的实现
中断 (Interrupt):电平跳变,主程序立刻暂停记录位置,处理突发事件。处理完后,再从记录的位置继续运行。
回调函数 (Callback Function):当中断触发时,CPU就会立刻精准地执行回调函数内容。执行完毕后,CPU返回。
中断流程:
1.准备阶段(配置引脚模式)
2.约定阶段(注册回调)
注册一个回调函数,使用 rt_pin_attach_irq 函数绑定回调函数
3.运行阶段(主函数执行)
4.突发阶段(按键按下)
5.报警阶段(触发中断)
底层的硬件逻辑检测到了这个跳变,立刻向 CPU 发出中断请求。CPU 收到信号后就会停下手中主函数的工作,并记住现在执行到了哪一行代码。
6.执行阶段(运行回调函数):
CPU 顺着绑定好的回调函数,直接跳到回调函数里去执行逻辑
7.回归阶段(继续主函数):
回调函数运行完毕,CPU回到主函数中断处继续执行。
中断的优先级高于普通的主线程(如 main 函数里的 while(1))。当 CPU 停下主函数去执行中断回调函数时,如果回调函数执行时间过长,主函数就会一直被卡住。
中断回调函数必须“快进快出”。绝对不能在回调函数中写入死循环,也严禁调用任何会引起阻塞的延时函数(如 rt_thread_mdelay())。如果按键触发后需要执行耗时操作,正确的做法是在回调函数中只改变一个全局标志位(Flag),然后立刻退出中断,由主循环不断检查该标志位并执行相应的耗时逻辑。
1.4消抖逻辑
**软件消抖逻辑:**在检测到第一次电平跳变后,程序不能立刻认定按键被按下,必须延时一小段时间(通常为 10ms~20ms)让物理电平稳定下来,然后再读取一次引脚电平。如果依然是有效电平,才确认为真正的按下动作。
1.5怎么匹配不同的外设该用什么模式
一、 输入模式怎么选?(看“空闲状态”)
输入引脚就像一个在风中摇摆的风筝。如果外部没有明确给它通电(比如按键没按下),它就会处于一种“测不准”的浮空状态,随便一个静电就能让它在 0 和 1 之间疯狂跳动。所以,我们需要用内部的“上下拉电阻”把它稳住。
判断口诀:按键按下是什么电平,空闲时就要用电阻拉到相反的电平。
- 场景 1:按键一头接 GND(按下变低电平,Active-Low)
- 分析: 既然按下才是 0,那不按的时候必须稳稳地保持在 1。
- 选择: 上拉输入 (
PIN_MODE_INPUT_PULLUP) 。 - 小车实战: 你的
KEY2(PA1) 按下是低电平,所以必须配上拉。
- 场景 2:按键一头接 VCC(按下变高电平,Active-High)
- 分析: 既然按下才是 1,那不按的时候必须稳稳地保持在 0。
- 选择: 下拉输入 (
PIN_MODE_INPUT_PULLDOWN) 。 - 小车实战: 你的
KEY1(PA0) 按下是高电平,所以必须配下拉。
- 场景 3:外部传感器自己输出强信号
- 分析: 比如超声波模块的回响引脚(Echo),传感器内部已经帮你把高低电平安排得明明白白了,不需要单片机多管闲事。
- 选择: 浮空输入 (
PIN_MODE_INPUT) 。
二、 输出模式怎么选?(看“驱动需求与通信协议”)
输出引脚是我们去控制别人,我们需要决定是“强硬地推过去”还是“留有余地”。
判断口诀:单向控制用推挽,双向通信(总线)用开漏。
- 场景 1:直接控制元器件(亮灯、响喇叭、转电机)
- 分析: 需要单片机干脆利落地输出 3.3V (强推) 或 0V (强挽) 来驱动器件。
- 选择: 推挽输出 (
PIN_MODE_OUTPUT) 。 - 小车实战: 你的 4 个 LED 车灯 (PD8-PD11,低电平点亮) 和蜂鸣器 (PA5,低电平响),统统无脑选择推挽输出。
- 场景 2:多设备共享一根数据线(典型如 I2C 协议)
- 分析: 如果用推挽,设备 A 强行输出高电平(3.3V),设备 B 强行输出低电平(0V),这俩线连在一起,主板直接就短路烧毁了。因此只能用内部没有上拉能力的“开漏”。开漏模式下,引脚只能拉低,要拉高必须靠外部电路共同的“上拉电阻”。这样大家谁也不和谁打架,谁想发信号就把线拉低,不发信号就松手。
- 选择: 开漏输出 (
PIN_MODE_OUTPUT_OD) 。 - 小车实战: 你的 OLED 屏幕连接的
SCL(PB6) 和SDA(PB7),在后续编写软件模拟 I2C 时序时,为了安全和符合协议规范,强烈建议配置为开漏输出(搭配外部或软件上拉)。
| 你的外设 | 引脚 | 硬件特性 | 应配模式 (RT-Thread API) | 理由 |
|---|---|---|---|---|
| KEY1 (核心板按键) | PA0 | 按下变高电平 | PIN_MODE_INPUT_PULLDOWN |
松开时需要下拉到低电平 |
| KEY2 (底板按键) | PA1 | 按下变低电平 | PIN_MODE_INPUT_PULLUP |
松开时需要上拉到高电平 |
| LED 车灯 (4个) | PD8-PD11 | 低电平点亮 | PIN_MODE_OUTPUT |
需要强驱动力 |
| 蜂鸣器 / 喇叭 | PA5 | 低电平响 | PIN_MODE_OUTPUT |
需要强驱动力 |
| OLED (I2C 模拟) | PB6, PB7 | I2C 通信总线 | PIN_MODE_OUTPUT_OD |
防止总线电平冲突短路 |
1.6输入模式与输出模式的继承关系
1. 永不关闭的“输入监听器” (IDR 寄存器)
在 STM32 的硬件设计中,引脚的“输入数据寄存器 (IDR)”通路是始终处于激活状态的(除非你把引脚配置成了模拟输入模式去测电压)。
这意味着, 即使你把引脚配置成了“推挽输出”或“开漏输出”,单片机依然可以随时通过读取 IDR 寄存器,获取当前物理引脚上的真实电平状态 。
2. 输出与输入的本质区别(物理开关的开与关)
- 配置为“输入模式”时: 芯片内部负责向外输出电流的 MOS 管电路被 完全断开(高阻态) 。此时,引脚只听不说,是一个纯粹的侦听器,电平完全由外部决定。
- 配置为“输出模式”时:
芯片内部的 MOS 管电路被 接通 ,开始努力把引脚强行拉高(3.3V)或拉低(0V)。但与此同时,那个侦听器(输入通路)依然在工作! 它可以随时“监听”自己发出去的电平到底有没有成功体现在物理引脚上。
3. 这个“输出带输入”的特性有什么大用?
你可能会问,既然是我自己输出的电平,我心里还能没数吗?为什么还要读回来?
在某些绝妙的场景下,你输出的电平,还真不一定等于引脚上的真实电平:
- 场景一:I2C 通信的精髓(开漏输出模式)
这是最经典的案例。前面我们提到,OLED 的 I2C 通信必须用 开漏输出 。在开漏模式下,当你输出1时,单片机内部其实是松手的(不输出 3.3V),引脚电平靠外部电阻拉高。
此时,如果总线上的另一个设备(比如某个传感器)强行把线拉低到了0, 你的输出寄存器里写的是1,但物理引脚上却是0。
这时候,那个始终在线的“输入监听器”就立大功了!单片机可以在输出1的同时去读取输入,如果读回来发现是0,说明“总线被别人占用了”。这就是 I2C 协议中“时钟同步”和“多主机仲裁”的硬件基石。 - 场景二:硬件故障检测
在推挽输出模式下,你强行输出1(3.3V),结果一读输入寄存器,发现居然是0。这就说明出大问题了——引脚很可能在外部对地短路了,电压被硬生生拉平了。通过这种方式,软件就能察觉到外部物理世界的硬件故障。