GPIO ——从端口位电路到八种工作模式

Ciallo(∠・ω< )⌒☆

系列专栏 直达链接
🔤 C 语言 进入专栏 →
🧱 数据结构 进入专栏 →
🔧 C++ 进入专栏 →
🐍 Python 进入专栏 →
🔌 STM32 进入专栏 →

🏠 博客主页:暖焰核心

📦 作者仓库:github | gitee

文章目录

引言

数完 STM32F103ZET6 的引脚之后,我心里还留着一个问题。

144 个引脚,去掉电源、复位、晶振、启动模式这些功能固定的,剩下 112 个可以随便编程的,分成 PAPG 七组,每组 16 个。名字怎么来的、编号怎么数,前面已经理清了。

但引脚本身到底是个什么东西?

同一个引脚,接上 LED 就能点亮它,接上按键就能读它按没按;改个配置,它又能挂到串口上收发数据。芯片里面拿什么在支撑这种"什么都能干"?为什么它既可以被程序写,又可以被程序读?

我原以为这大概是常识,翻了两页手册才发现里面有一整套电路:保护二极管、上拉下拉电阻、触发器、两个 MOS 管。看起来最"入门"的 GPIO,内部结构其实相当完整。


一、GPIO 的定位

GPIOGeneral Purpose Input/Output 的缩写,中文叫"通用输入输出"。这两个词分别对应两个问题:输入输出的是什么,以及为什么叫"通用"。

输入输出的内容

引脚上能有的东西就那么两种:高电平或者低电平。上层语言把它看成 0 和 1,下层硬件把它当成控制外设的手段。

输出高电平,LED 会理解成"点亮",电机会理解成"正转",串口对面的接收方会理解成"送来一个 1"。输出低电平则相反。

输入是另一回事。单片机的本职是"控制",要控制外设,前提是先知道外设的状态:开关合上没有、传感器有没有读到变化、设备是不是在休眠。这些信息从引脚上读回来,程序再决定下一步做什么。

所以高低电平只是手段,获取外设信息、控制外设才是目的。

"通用"两个字的来处

"通用"的意思是这东西造出来就是给"大家"用的,跟英语被叫做世界通用语言是一个道理:英国人说,美国人也说,中国人还说。

GPIO 也是。它自己能输入输出高低电平,可 MCUMicrocontroller Unit,微控制器)里面还集成了一堆其他外设控制器:ADCDACUSARTTIMI2CSPI 等等。这些缩写现在不用记全,知道它们各管一摊事就够了:ADCDAC 管模拟量,USART 管串口通信,TIM 管定时,I2CSPI 是两种芯片之间常用的通信总线。它们要控制外设,同样得输出高低电平。难道给每个控制器都单独焊一套输入输出电路?

那样做,芯片内部的复杂度、成本和功耗都会往上走。既然 GPIO 已经把输入输出这件事做了,其他外设控制器直接用就行。这就是"通用"的由来,也是后面会反复出现的"复用 GPIO"这个概念。

MCU 与 CPU 的关系

要把上面这层关系看明白,得先纠正一个容易混的地方:MCU 不等于 CPU

CPU 只是 MCU 里面的一部分。以 Cortex-M3 为内核的 MCU,内部大致分三层:

text 复制代码
+-----------------------------------------------------------+
|                         MCU 内部                          |
|                                                           |
|   +----------------+              +-------------------+   |
|   |    ARM-CPU     |    总线连接  |   片上外设控制器  |   |
|   |   Cortex-M3    |<------------>|  ADC  DAC  USART  |   |
|   +-------+--------+              |  TIM  I2C  SPI    |   |
|           |                       +---------+---------+   |
|           | 直接控制 GPIO                   | 复用 GPIO   |
|           v                                 v             |
|   +---------------------------------------------------+   |
|   |                       GPIO                        |   |
|   +-------------------------+-------------------------+   |
+-----------------------------|-----------------------------+
                              | 输入 / 输出高低电平
                              v
                     +------------------+
                     |     板级外设     |
                     |  LED 按键 传感器 |
                     +------------------+

这三层可以拿古代的朝廷打个比方。ARM-CPU 是坐在上面的决策者,负责下命令;片上外设控制器是各个衙门,把事情管起来、把命令转成动作;板级外设是地方上真正干活的,灯亮、电机转、按键弹起来,都发生在这一层。

顺带说清一个容易含糊的地方:严格讲 GPIO 自己也是一个片上外设,参考手册里它单独占一章。它的特殊之处在于不只给 CPU 用,还是别的片上外设对外进出的那条公共通道,所以这里把它单独画在下面一层。

这个比方里有个关键点:GPIO 被设计出来,不是只给 CPU 读写用的 。所有片上外设都要借它的手出去,所以芯片上一大半引脚都是 GPIO 引脚。这也是学单片机要从 GPIO 开始的原因,它是整个片上外设体系的出入口。

明白了这一层,前面那个问题就顺了一半:GPIO 之所以要有这么多工作模式,是因为用它的"人"实在太多。每个外设的场景都不一样,GPIO 只能把功能做得足够丰富,让大家都挑得着。就像键盘,只打游戏的话 WASD 几个键就够,但键盘不能只有 WASD,因为它还要被拿来写文档、写代码、聊天。


二、引脚与端口

引脚、针脚、管脚

GPIO 最直接的印象就是芯片外面那一圈金属针脚。这颗芯片用的是 LQFP144 封装(LQFPLow-profile Quad Flat Package,薄型方形扁平封装,144 是引脚数)。下面这张是它的引脚分布图,标成红色的 PAxPGx 就是 GPIO 引脚:

下面这张是这颗芯片的实拍照片:

两相对照,图上的位置和实物上的针脚是一一对应的。针脚、引脚、管脚是同一个概念,都指这些金属针脚。数一数就能看出来,芯片四周这一圈绝大多数都是 GPIO 引脚。

按键与 LED 两个例子

一根 GPIO 引脚在功能上只有两种工作方式,输入或者输出。用两个最简单的例子就能说明白。

按键是输入。按键一端接引脚,另一端接 GND(地);同时引脚还要通过一个电阻接到 VDD(电源正极),这个电阻的作用是把引脚平时拉到高电平。

按键没按下去的时候,引脚和 GND 之间是断开的,引脚被电阻拉在 VDD 上,读到的就是高电平;按下去,引脚直接接通 GND,读到的就是低电平。

text 复制代码
        VDD
         |
      [上拉电阻]        <- 按键没按下时,引脚被拉到 VDD,读到高电平
         |
         +--------------------------- 引脚 PA0
         |
      [按键]            <- 按下时,引脚接地,读到低电平
         |
        GND

LED 是输出。LED 阳极(长脚)接 VDD,阴极接到引脚上。引脚输出低电平,电流从 VDDLED 流进引脚,灯亮;引脚输出高电平,LED 两端电压一样、没有压差,就没有电流,灯灭。

text 复制代码
        VDD
         |
      [ LED ]           <- 阳极接 VDD,阴极接引脚
         |
         +--------------------------- 引脚 PA1
                                      MCU 内部(输出电平)

LED 这盏灯要的是低电平,所以"输出高"反倒让它灭了。引脚输出什么,跟外设要什么,是两件事。

Port 与 Pin 的层级

引脚在外面是一根一根的,到了芯片内部,MCU 得把它们管起来。管理方式是分组的:每 16 个引脚算一组,叫做一个 GPIO Port (端口)。STM32F103ZET6 一共 7 组,GPIOAGPIOG

GPIOA 为例,它管着 PA0PA15 共 16 个引脚,每个引脚在组里有个序号:

组内序号 外部物理引脚名
0 PA0
1 PA1
... ...
15 PA15

程序里要指定某一根引脚,用的就是这个组内序号,也就是"这一组的第几位"。外面叫 PA3,内部就是 GPIOA 的第 3 位。

层层往下看,是个多叉树的形状:

text 复制代码
GPIO 控制器
 |-- GPIOA  ---+-- Pin 0  -> PA0
 |             +-- Pin 1  -> PA1
 |             +-- ...
 |             +-- Pin 15 -> PA15
 |
 |-- GPIOB  ---+-- Pin 0  -> PB0
 |             +-- ...
 |
 |-- ...
 |
 |-- GPIOG  ---+-- Pin 0  -> PG0
               +-- ...

这里有个细节值得停一下:为什么不叫 GPIO Group,而叫 GPIO Port

Group 只能表达"分组"。而 Port 这个词本义是"港口",在计算机领域引申成"数据交换的入口或出口"。一组 GPIO 引脚在一起,本质是芯片和外部世界交换数据的一道口子,用 Port 比用 Group 准确。

引脚在内部的一套电路

到这里,引脚"可见"的部分算是看完了。真正要研究的是:一根引脚在芯片内部,究竟靠什么完成输入输出。

下图是 GPIO 端口位的基本结构,所谓"端口位"就是端口里的单个引脚,比如 PA0PB3

这张图要慢慢看,它也是后面所有内容的落脚点。先记住两条:

第一,每一个引脚在内部都有一套这样的电路 。做 16 套就是一组,做 112 套就是全部的 GPIO。反过来说,只要搞清楚一个引脚怎么工作,其余的都通了。

第二,这张图逻辑上分成两部分:上面是输入路径,下面是输出路径。看的时候记住四个字------上 I 下 O,就不会在里面绕晕。

图右边那两个"保护二极管"是电路的最外层,紧贴着引脚。下一节细嗦。


三、端口保护电路

保护电路的位置

回看端口位结构图的右端:紧挨着 I/O 引脚的地方,上下各画了一个二极管,标注是"保护二极管"。这两个管子组成了 I/O 端口最外面的一道墙。

先把二极管本身说清楚。它是一种单向导电的元件,只允许电流从正极(阳极)流向负极(阴极),反方向阻断。电流的产生需要电压差,有了压差,二极管才会导通。导通的时候它自己也承压,这个压降通常在 0.6 V 到 0.7 V 之间。[1](#1)

双二极管钳位

两个二极管的接法是固定的:上面那个,阳极靠引脚一侧、阴极接 VDD;下面那个反过来,阳极接 VSS(电源负极,也就是前面说的 GND)、阴极靠引脚一侧

  • 上路二极管的阴极接 VDD,意思是它只有在一个条件下才会导通:引脚上的电压比 VDD 还高出 0.7 V 以上。一旦满足,它导通,多余的电压被导走,引脚电压被按住,停在 VDD + 0.7 V 附近。
  • 下路二极管阳极接 VSS,它导通的条件是引脚电压比 VSS 还低 0.7 V 以上,导通后引脚电压被按住,停在 VSS − 0.7 V 附近。
  • 中间那一段谁都不导通,引脚爱是什么电压就是什么电压。

VDD = 3.3 V、VSS = 0 V、二极管压降 0.7 V 代入,三个区间是这样:

引脚电压 哪个二极管导通 结果
低于 −0.7 V 下路 电压被钳到 −0.7 V 附近,不再往下掉
−0.7 V ~ 4.0 V 两个都不导通 正常输入,引脚电压是多少就是多少
高于 4.0 V 上路 电压被钳到 4.0 V 附近,不再往上冲

这种接法的学名叫双二极管钳位电路 ,作用是防止不合理的电压进入芯片。如果没有它,一个远超 VDD 的电压接上引脚,会在芯片内部形成很大的电流,把里面的元器件击穿。

什么情况下会超范围

正常接线的时候引脚电压不会越界,越界往往来自一个想不到的地方:静电。

手上有静电的时候去碰芯片的针脚,瞬间电压可以到几千伏。具体的量级不用记,关键是从这个量级就能看出来,为什么手册里会专门写"不要用手去摸针脚"。

不过也不用把这件事看得太严重。偶尔一次超范围的电压进来,不一定就会击穿器件;真正危险的是持续的超范围电压。还有一点值得一提:电压的正负是人规定出来的,真正决定一切的始终是压差,别被符号绕进去。


四、八种工作模式

八种模式的全貌

GPIO 要负责输入和输出,而输入输出在具体场景里又各不相同,于是就有了下面八种模式:

方向 模式 说明
输出 通用推挽输出 CPU 直接控制,能输出高电平也能输出低电平
输出 通用开漏输出 CPU 直接控制,只能输出低电平和高阻抗
输出 复用推挽输出 片上外设控制,能输出高电平也能输出低电平
输出 复用开漏输出 片上外设控制,只能输出低电平和高阻抗
输入 上拉输入 内部接上拉电阻,悬空时读到 1
输入 下拉输入 内部接下拉电阻,悬空时读到 0
输入 浮空输入 内部两个电阻都断开,电平由外部决定
输入 模拟输入 直通,不做整形,交给 ADCDAC 这类外设

四种输出模式是"通用 / 复用"和"推挽 / 开漏"两个维度的组合:

推挽 开漏
通用 通用推挽输出 通用开漏输出
复用 复用推挽输出 复用开漏输出

通用与复用的分界

第一节说过这对词,这里给个准确的说法。

通用 就是通常使用,指的是由 ARM-CPU 直接控制 GPIO 输出。程序往数据寄存器里写什么,引脚上就出现什么。

复用 是重复使用的意思,指的是 MCU 内部的片上外设借 GPIO 来做输入输出。比如串口要发数据,它不会自己接一根线出来,而是把这个任务交给某个引脚,由串口控制器去驱动。

一句话区分:看谁在控制这根引脚。CPU 直接控制是通用,片上外设接管是复用。

至于为什么要这么多模式,第一节已经交代过了:GPIO 是给所有片上外设共用的,每个外设的场景都不一样,模式多才能覆盖得住。


五、输出驱动器的构成

输出部分的边界

研究输出模式之前,先把要研究的范围圈出来。下面是端口位结构图里属于输出的那一部分:

!\[STM32/attachments/GPIO输出部分结构框图.png\|从端口位结构图中框出的输出部分,右侧是驱动引脚的 P-MOS 与 N-MOS]

从左往右看这块电路,一共四样东西:左边是输出数据寄存器和"来自片上外设"的复用功能输出,中间是复用器和输出控制,右边是 P-MOSN-MOS 两只管子,最后接到引脚上。

复用器与输出控制

复用器的名字来自"复用",它的工作是从多组输入信号里选一个作为有效输入。

为什么要有它?因为 GPIO 到底归 CPU 用还是归片上外设用,得有人说了算。复用器就是那个做决定的:选数据寄存器,这根引脚就归 CPU;选复用功能输出,就归片上外设。

输出控制 接在复用器后面,负责把选中的信号送到 MOS 管的栅极。它做的是"力气的转换":内部逻辑电平本身很弱,驱动不了外面的负载,输出控制把它变成有足够驱动能力的电平,去控制管子的通断。

P-MOS 与 N-MOS

要理解输出,核心在最后这两只管子上。

MOSMetal-Oxide-Semiconductor Field-Effect Transistor(金属氧化物半导体场效应管)的缩写,也写作 MOSFET。前面带 P 的是 P 沟道,带 N 的是 N 沟道。P 取自 positive(正向的),N 取自 negative(反向的)。

这两种管子的符号长这样:

不管是 P 还是 N,都有三个极:

  • SSource,源极。载流子的来源。
  • DDrain,漏极。载流子从这里流出器件,它是一个电流路过的地方,不是终点。
  • GGate,栅极。Gate 本意是"门",开关的开启和关闭由它控制。

记住一句就够用:S 和 D 之间是电流要走的路,G 是控制这条路通不通的开关。实际接法上,S 和 D 会分别接到电源或者地,G 接控制电路。

至于"P"和"N"怎么区分,从电流方向上有个直观的分法:被控制的电流从 S 流向 D,就是 P 型;从 D 流向 S,就是 N 型。这跟 P 的"顺势而为"、N 的"逆水行舟"是一套说法,先当口诀记着。

而它们最本质的一点是:这是一个开关。不是放大器,不是别的东西,就是一个可以用小电压去控制的开关,最终目的是用很小的能量去控制大电流。

两只管子的接法

GPIO 的输出电路里,P-MOSN-MOS 的接法是固定的:

text 复制代码
        VDD (3.3V)
             |
             S
          [P-MOS]
             D
             |
             +------------------> I/O 引脚   <- 两只管子的 D 极在这里相接
             |
             D
          [N-MOS]
             S
            GND
  • P-MOS 在上面 ,S 极接 VDD
  • N-MOS 在下面 ,S 极接 GND(也就是 VSS,看成 0 V)。
  • 两只管子的 D 极接在一起,接出去就是引脚。

这个"上 P 下 N、D 极相接"的形状,是整个输出部分的骨架。接下来讲的推挽和开漏,都只是围着这两只管子做加减法。

简化之后的输出电路

为了后文说得清楚,把前面那些寄存器、复用器、输出控制先收起来,只留最要紧的部分:

text 复制代码
        VDD
         |
      [P-MOS]  <- 闭合则引脚接 VDD
         |
         +------------------> I/O 引脚
         |
      [N-MOS]  <- 闭合则引脚接 GND
         |
        GND

后面再看推挽和开漏,就只盯着"输出控制让哪只管子闭合"这一点。


六、通用推挽输出

两只管子交替工作

输出模式里最直观的是通用推挽输出(Push-Pull Output),它的核心思想就一句话:交替闭合和断开 P-MOSN-MOS

text 复制代码
输出数据寄存器写 1:闭合 P-MOS,断开 N-MOS

        VDD
         |
      [P-MOS]  <- 闭合
         |
         +------------------> I/O 引脚 = 高电平(通过 P-MOS 接上 VDD)
         |
      [N-MOS]  <- 断开
         |
        GND


输出数据寄存器写 0:断开 P-MOS,闭合 N-MOS

        VDD
         |
      [P-MOS]  <- 断开
         |
         +------------------> I/O 引脚 = 低电平(通过 N-MOS 接地)
         |
      [N-MOS]  <- 闭合
         |
        GND

引脚要么接在 VDD 上,要么接在 GND 上。接 VDD 就是高电平,接 GND 就是低电平,中间没有第三种状态。

"推挽"的命名来处

名字来自这两只管子干活的方式。

输出高电平时,P-MOS 把引脚接到 VDD,电流从芯片里"推"出去(提供电流);输出低电平时,N-MOS 把引脚接到 GND,电流从外面"挽"进来(吸收电流)。一推一挽,所以叫推挽。

一个不能出现的情况

看到这里可能会冒出一个念头:要是两只管子同时导通呢?

那就是 VDDGND 直接连起来,中间只剩两只几乎不承压的管子,电流会大到烧掉电路。所以输出控制单元有一条硬要求:保证两只 MOS 管永远不同时导通。这条在电路里靠逻辑设计保证,程序不需要操心。

导通条件

MOS 管要导通,栅极得加上足够的电压。这个门槛叫导通阈值电压 VthVoltage Threshold),一般零点几伏。判断条件是看源极和栅极之间的电压差够不够大,跟漏极无关:

  • P-MOSVsVg 超过 Vth,就导通;否则断开。
  • N-MOSVgVs 超过 Vth,就导通;否则断开。

具体数值不用记,记住方向就行:P-MOS 是源极电压高于栅极,N-MOS 是栅极电压高于源极。这也解释了前面那个口诀,P 管顺着 S 到 D 的方向走,N 管逆着来。

顺带记一个量级:MOS 管的开关速度可以到纳秒级。这也是它能用来做数字电路的原因。

所以程序往输出数据寄存器写 1 还是写 0,本质是让输出控制去挑 P-MOS 还是 N-MOS,最终决定引脚接 VDD 还是接 GND


七、通用开漏输出

开漏输出的构成

通用开漏输出(Open-Drain Output)的思路跟推挽反着来:不要 P-MOS 了,让它永远关闭,只留 N-MOS

只留一只管子,事情就简单了,也不用再考虑"两只同时导通"的问题。但代价是能力少了一半。

text 复制代码
输出数据寄存器写 0:闭合 N-MOS,引脚接地

        VDD
         |
      P-MOS 永远断开
         |
         +------------------> I/O 引脚 = 低电平
         |
      [N-MOS]  <- 闭合
         |
        GND


输出数据寄存器写 1:断开 N-MOS,内部与引脚断开

        VDD
         |
      P-MOS 永远断开
         |
         +------------------> I/O 引脚 = 高阻抗
         |
      [N-MOS]  <- 断开
         |
        GND

先记住这个结论,后面用得到:

  • 向输出数据寄存器写 0,表示 N-MOS 闭合,引脚输出低电平。
  • 向输出数据寄存器写 1,表示 N-MOS 断开,引脚输出高阻抗。

换成一句话:开漏模式只能输出低电平和高阻抗,想要高电平得靠外部。

高阻抗的含义

"高阻抗"这个说法有点抽象,换个大白话的解释:MCU 内部和外部引脚彻底断开了,相当于开路 ,或者说 N-MOS 的漏极和外部引脚互相悬空了。

对引脚来说,既然内部已经断开了连接,那此刻的 MCU 就可以看成一个阻值无穷大的设备。阻值无穷大,我们就说它输出高阻抗。

"开漏"的命名来处

开漏的全称是漏极开路输出 。名字的来源很直白:P-MOS 被拿掉之后,N-MOS 的 D 漏极就相当于什么都没接,是"开路"的状态,所以叫漏极开路,简称开漏。

这里有个容易想岔的地方:不要把引脚当成一个设备,引脚就是 D 漏极那根线的延伸 。推挽输出里 P-MOSN-MOS 互为对方漏极接的设备,所以不算开漏;开漏里没有这个"对方",管子外面是空的。

高电平的外部来源

现在问题来了:开漏模式只能输出低电平和高阻抗,那高电平怎么办?

办法是在引脚外面接一个上拉电阻,把引脚拉到另一个电源上。

这个电阻为什么不能做在芯片里?参考手册给了直接的答案:端口配置成输出之后,内部的弱上拉和弱下拉都被禁止,所以输出模式下的上拉只能在外部加。至于加多大、拉到哪个电源上,取决于外部设备需要什么电压,得由开发板的设计者决定。

text 复制代码
        VDD (3.3V)
          |
       [上拉电阻]        <- 这个电阻在芯片外面,由开发板设计者决定
          |
          +-----------+ I/O 引脚
          |
      N-MOS 断开时:引脚被外部电阻拉到 VDD,输出高电平
      N-MOS 闭合时:引脚直接接地,输出低电平

加上这个电阻之后,开漏模式也能输出高低电平了:N-MOS 闭合时接地,输出低电平;N-MOS 断开时引脚被外部电阻拉上去,输出高电平。

高阻抗之后的引脚电压

这一步很多人是"记住结论"跳过去的,但它其实可以用高中物理讲明白。

先说一个常识:在一个电路回路里,电压会被各个元器件承接,电阻值越高的元器件,承压越多

为了方便算,把图上的几个电阻起个名字:外部上拉电阻记作 R_PULL;引脚本身记作 R1,它其实就是一根导线,阻值无限趋近 0;N-MOS 断开的位置记作 R2,阻值是无穷大。

然后请出欧姆定律 V = I × R。这里把 N-MOS 断开的位置当成一个阻值无穷大的电阻,这样从 VDDGND 就成了一条串着三个电阻的通路:

  • 整个回路的电流 I = 3.3 V ÷ (R_PULL + R1 + R2)。因为 R2 接近于无穷大,所以 I 约等于 0。
  • 引脚处的电压 = 3.3 V 减去上拉电阻上承掉的那部分 = 3.3 V − I × R_PULL
  • I 约等于 0,所以 3.3 V − 0 × R_PULL = 3.3 V。

引脚处就是 3.3 V,输出高电平。

但这个推导里藏着一个坑。既然开路,就没有电流,I = 0,那按 V = I × R 算,整个电路里都不该有电压,引脚处也不该有。跟上面的结论对不上。

问题出在对欧姆定律的理解上。一块裸露的电池什么都不接,它自己就是开路的,正极有没有电压?当然有。水龙头不打开,管子里照样有水压。

电压的本质是两端的电势差,开路的状态下它依然存在 。欧姆定律算出来的那个电压,是某个元器件在整个回路里承担的电压差,也就是电流经过它之后电压下降了多少,不是某一处的绝对电压。

假设一个回路上串三个电阻,电源 10 V,电阻分别是 2 欧、3 欧、5 欧:

text 复制代码
      VDD ---- [ R1 ] ---- [ R2 ] ---- [ R3 ] ---- GND
       10V      2欧          3欧          5欧

   回路电流  I  = 10V / (2 + 3 + 5) = 1A
   R1 承压       = 1A x 2欧 = 2V
   R2 承压       = 1A x 3欧 = 3V
   R3 承压       = 1A x 5欧 = 5V

R1 承担了 2 V,意思是电流流过 R1 之后,电压从 10 V 掉到 8 V。所以要知道 R1 右边那一点的电压是多少,就是 10 V 减去 2 V,等于 8 V。导线本身不承压,所以 R1 右边的电压就等于 R2 左边的电压。

回到开漏加上拉电阻那张图,R2 无穷大、电流约为 0、上拉电阻上什么都不承,引脚处自然就是 3.3 V。

上拉电阻的另一半作用

上拉电阻不只是为了输出高电平,它还管着另一件事:N-MOS 闭合、引脚接地的时候。

如果外部没有任何电阻限流,引脚一接地,就会从外面灌进来一股不受控制的大电流。串上这个电阻之后,电流被限制在一个合理范围里。所以这个电阻同时承担了"拉高电平"和"限流"两个职责。

至于"上拉"这个名字,说的是它的工作方式:引脚的高电平是被外部电阻加上高电压起来的,所以叫上拉电阻。阻值一般取 4.7 kΩ 到 10 kΩ。


八、复用推挽与复用开漏

前面两节讲的都是"通用",控制权在 CPU 手里。剩下两种输出模式(复用推挽输出、复用开漏输出)只在一处不同:控制权交给了片上外设

具体来说,输出数据寄存器的位置换成了片上外设的输出信号,信号经过复用器进入输出控制,后面接的还是同样的 P-MOSN-MOS。所以推挽的能力、开漏的能力,两种版本完全一样:

  • 复用推挽输出:片上外设控制,能输出高电平也能输出低电平。
  • 复用开漏输出:片上外设控制,只能输出低电平和高阻抗,要输出高电平同样得靠外部上拉。

有个细节值得记一下:配置成复用输出之后,引脚和输出数据寄存器就断开了,转而接上片上外设的信号线。这时候程序再往数据寄存器里写值,引脚是不会理会的。

把四种输出模式放在一起对照:

模式 谁在控制 高电平 低电平 高阻抗
通用推挽输出 ARM-CPU 不能
通用开漏输出 ARM-CPU 不能(需外部上拉)
复用推挽输出 片上外设 不能
复用开漏输出 片上外设 不能(需外部上拉)

选择哪种模式,取决于这根引脚要接什么。

推挽驱动能力强,用来点灯、驱动电机这类负载;开漏能输出高阻抗,适合共享总线和电平转换,代价是少不了外部上拉电阻。至于通用还是复用,由这根引脚的用途决定:CPU 直连的用通用,交给片上外设的用复用。


九、输入侧与施密特触发器

输入部分的边界

输入端口的范围和输出是对称的。下面是端口位结构图里属于输入的那一部分:

从右往左看:引脚进来,先经过保护二极管(外层已经讲过),然后遇到两个带开关的电阻(上拉和下拉),接着是触发器 ,最后到输入数据寄存器,一路读给 CPU,一路送给片上外设。

现在就差中间那个触发器没交代了。它做的事说起来简单,但很关键。

数字信息与模拟信息

现实世界里的信息大致分两类。

一类是状态有限的:电梯门关好没有、安全带系上没有、共享单车锁上没有、门铃被按过没有。这类信息只有有限的几个状态,叫数字信息。

另一类是连续变化的:洗澡水的冷热、手机剩余电量、奶茶店背景音乐的音量、早高峰地铁的拥挤程度。这类信息在任意时刻都能取到无穷多个值,叫模拟信息。

计算机和单片机是为人类服务的,要提供服务就得先获取信息,两种信息都得能处理。为了处理它们,硬件工程师造出了一大堆电路,高校专门开了两门课来教:《数字电路》和《模拟电路》。

这一节只关心数字信息这一侧。

不规整的输入

理想情况下,引脚上收到的应该是一个方方正正的波形:要么干脆在高电平,要么干脆在低电平,切换的那一刻干净利落。

真实情况不是这样。引脚连着导线,导线连着外面的世界。电源线、人体、手机、电机、开关电源,甚至隔壁的 WiFi、蓝牙、对讲机,都在往外面辐射电磁场,这些干扰会实实在在地叠在输入信号上。于是本来该干净翻上去的波形,翻到一半就开始抖:

如果就这么交给后面的电路去判断高低电平,它会在 0 和 1 之间反复横跳,读到的值一会儿一个样。

TTL 施密特触发器

解决这个问题的单元叫 TTL 施密特触发器

TTLTransistor-Transistor Logic 的缩写,中文是"晶体管-晶体管逻辑"。施密特是人的姓。整个名字直译过来就是"晶体管-晶体管逻辑施密特触发器",名字不用记,记住它的作用就行:抗噪整形,把抖动的、带毛刺的、爬升缓慢的输入波形,整形成规则的方波。

原理在于它设有两个不同的阈值,而不是一个。翻上去的时候要越过正向阈值电压 VT+ 才算高电平,掉下来的时候要跌破负向阈值电压 VT− 才算低电平。两个阈值之间夹出来的那一段,叫迟滞区域

迟滞区域的作用在于:只要输入还在这个区间里抖,输出就保持上一次的状态不变。哪怕噪声恰好把电压顶到阈值附近来回晃,也不会引起输出的频繁跳变。

举个具体的:电压在 VT+ 附近来回抖动。高于 VT+ 的那一瞬间,输出是高电平;落回迟滞区域里,输出维持上一次的状态。而"上一次"既然是从 VT+ 上方掉下来的,那自然是高电平。所以结论是维持高电平不变 。在 VT− 附近抖动同理,维持低电平不变。

噪声就这样被吃掉了。真正送进输入数据寄存器的是触发器输出的那个方波,前面那段抖动的波形到不了后面。

中文手册里的一处矛盾

讲这个触发器的时候,有一件事我得专门说一下,因为它很容易让人以为是自己看错了。

中文版参考手册里,同一个东西出现了两种叫法:

  • 正文(8.1.7 到 8.1.10 四节)写的是"施密特触发输入被激活"。
  • 图 13(I/O 端口位的基本结构)的图里,标注写的是"TTL 肖特基触发器"。

"肖特基"和"施密特"是两种完全不同的器件,前者是 Schottky,后者是 Schmitt,不能混。[2](#2)

那到底哪个对?

对照英文原稿就清楚了:英文版参考手册里 Schmitt 出现了好几处,Schottky 一次都没有。所以正确译名是施密特,中文手册图里的那个"肖特基"是错的。

有意思的是,这不是"翻译翻错了",而是同一本手册里图和正文对不上。前面那张端口位基本电路结构图,就是从手册图 13 来的,图上印的也是"肖特基"。看书的时候如果只盯着图,很容易把错的那个名字记下来。


十、上拉输入与下拉输入

配置开关的真实含义

前面那张输入结构图上,上拉电阻和下拉电阻各串了一个开关。所谓"把这根引脚配置成上拉输入",落到电路上就是一件事:闭合上拉电阻的开关,断开下拉电阻的开关。反过来,配置成下拉输入就是闭合下拉、断开上拉。

两个开关都在芯片内部,程序通过配置寄存器去控制它们。

text 复制代码
上拉输入:闭合上拉开关,断开下拉开关

        VDD
         |
      [上拉电阻]  <- 开关闭合
         |
         +--------------------- 引脚
         |
      [下拉电阻]  <- 开关断开
         |
        VSS


下拉输入:闭合下拉开关,断开上拉开关

        VDD
         |
      [上拉电阻]  <- 开关断开
         |
         +--------------------- 引脚
         |
      [下拉电阻]  <- 开关闭合
         |
        VSS

上拉输入

配置成上拉输入之后,会发生三种情况:

引脚状态 输入数据寄存器读到
引脚接高电平 1
引脚接低电平 0
引脚悬空,外部什么都没接 1

前两种不奇怪,引脚上是什么就读到什么。关键在第三种:引脚什么都没接的时候,它被内部的上拉电阻拉到了 VDD,于是读到 1。

这就是上拉输入存在的理由:给输入一个确定的缺省值。外部设备可能还没接上、可能处于断开状态,有了上拉电阻,程序读到的至少是个明确的值,而不是随机的东西。

下拉输入

下拉输入就是把上面那套反过来,三种情况也对称:

引脚状态 输入数据寄存器读到
引脚接高电平 1
引脚接低电平 0
引脚悬空,外部什么都没接 0

区别只在缺省值:上拉给的是 1,下拉给的是 0。选哪个取决于电路的需求,比如按键一端接地时,平时希望读到 1,那就配上拉。

"弱"字的意思

芯片内部的这两个电阻,阻值偏大,通常在 30 kΩ 到 50 kΩ 这个量级。

阻值大,意味着它能提供的电流很小,对引脚电压的控制力就比较"弱"。所以内部上下拉常被叫做弱上拉、弱下拉。这个"弱"字后面还会用到。

顺便说清一个位置上的区别:输入这边说的上下拉电阻,是 MCU 内部自带的 ;而第七节开漏输出讲的那个上拉电阻,是芯片外面由开发板设计者加的。两者的作用类似,位置和责任人都不一样。

两个根本问题

讲到这里,有个疑问一直悬着:上拉输入的时候,如果引脚接低电平,上拉电阻不会把电平拉高吗?下拉输入的时候,如果引脚接高电平,下拉电阻不会把它拉低吗?

这两个问题原理上是同一个问题,本质上是在问"引脚上的电平和上下拉电阻对抗的时候,谁说了算"。

答案是不会。理由有两层。

第一层是强弱关系:内部上下拉电阻的阻值在几十 kΩ 这个量级,它提供的电流很小;而外部驱动器给出的低电平或者高电平要"结实"得多。对抗起来,外部信号说是什么就是什么。

第二层是第七节留下的结论:一个回路里电压会被各个元器件承接,阻值高的承压多。用等效电路看会更清楚。

上拉输入、引脚接低电平的情况,可以等效成引脚直接接地:

text 复制代码
        VDD
         |
      [上拉电阻]  <- 回路里唯一承压的元件,承压 = VDD
         |
         +--------------------- 引脚处电压 = 0V(导线不承压)
         |
      [外部的低电平]  <- 相当于直接把引脚接地
         |
        GND

这个回路里唯一需要承压的元件就是上拉电阻,它承担的压差是整个 VDD。而引脚就是一根导线,不承压,所以引脚处的电压就是 0 V,是低电平,不受上拉电阻的影响

下拉输入、引脚接高电平时对称:外部把引脚接到 VDD,回路里唯一承压的是下拉电阻,引脚处电压就是 VDD不受下拉电阻的影响

所以上下拉电阻的作用范围是有限定的:它只能在引脚悬空的时候决定电平,一旦外部有信号进来,决定权就交出去了。这也正是它被称为"弱"上下拉的原因。


十一、浮空输入

两个开关都断开

浮空输入就是内部的上拉和下拉都不配置,开关全部断开。等效出来是这样一个电路:

text 复制代码
浮空输入:断开上拉开关,断开下拉开关

        VDD
         |
      [上拉电阻]  <- 开关断开
         |
         +--------------------- 引脚
         |
      [下拉电阻]  <- 开关断开
         |
        VSS

引脚接高电平就读 1,接低电平就读 0,这两件事跟前面一样。真正的区别出现在引脚悬空的时候。

悬空的引脚像一根天线

引脚悬空、内部又没有上下拉把电平钉住,这根导线就变成了一根接收天线:

区别在于有没有人跟干扰较劲。上一节那些干扰源叠在信号上,至少还有一个外部驱动源在把电平往确定的方向拉;引脚悬空的时候连这个对手都没有,干扰直接决定了引脚上的电压。经过触发器之后,读到的就是随机的高低电平。

所以浮空输入在引脚悬空时,电平是不确定的。上一节强调的"缺省值",浮空输入给不出来。

适用场合

浮空输入不是不能用,条件是这个引脚一定会被外部驱动:要么接着一个明确的输出,要么接的器件在需要读的时候一定会给出确定电平。这种情况下不需要内部电阻来兜底,还能省掉上下拉带来的那点电流。

反过来说,如果这根引脚有可能处于悬空状态,又需要随时读它,那就别配成浮空。


十二、模拟输入

等效电路

输入模式里最后一种是模拟输入。回到输入结构图上看,模拟输入这条路径上,物理上等效于一根直通的导线

text 复制代码
模拟输入:直通,不做整形

     引脚 ------------------------------ 到片上外设
              (不经过触发器)

它和前面三种最大的不同在于:没有走触发器

触发器的作用是整形,把抖动的输入整理成干净的 0 和 1。这对数字信号是好事,但对模拟信号是灾难,因为整形这个动作本身就是在把连续的量变成两个离散的状态,信息全没了。所以模拟输入干脆把这条路掐掉,外面是什么就原样交给片上外设。

用一句话概括:数字输入要的是"这是高还是低",模拟输入要的是"这到底是多少"

模拟信号的去处

引脚把模拟信号原样送进来,总得有人处理。MCU 内部专门做这件事的片上外设是 ADCAnalog-to-Digital Converter,模数转换器)和 DACDigital-to-Analog Converter,数模转换器),前者把模拟量读成数字量,后者反过来。

模拟信号 是时间和幅度上都连续变化的物理量,物理世界本身是连续的。

数字信号在时间和幅度上都离散,是把连续的物理量量化成有限个取值之后的结果。


十三、八种模式的价值

回到开头那三个问题。

引脚是什么?是芯片内部一套完整电路的出口。这套电路里有保护二极管挡住过压,有上下拉电阻提供缺省值,有触发器把噪声整理干净,有两个 MOS 管负责往外输出。

为什么它既可以被程序读、又可以被程序写?因为输入路径和输出路径本来就是两套电路,只是共用了一个引脚。

为什么它能接 LED、能接按键、还能去做串口?因为有八种模式兜着。同样是这一根引脚,配上不同的模式,就是不同的用途:推挽能推能挽,适合驱动负载;开漏多了个高阻抗状态,适合共享总线;上拉下拉让输入有个确定的缺省值;浮空留给一定会被外部驱动的场合;模拟输入则把控制权整个让给 ADCDAC

这也是 GPIO 被称为"通用"的真正含义。它把自己做得很宽,是因为用它的不只是 CPU,而是整个片上外设体系。一个引脚不值钱,一百多个引脚加上八种模式,才让 MCU 有能力去感知外部世界发生了什么,再操控外部设备做出动作。

我的博客即将同步至腾讯云开发者社区,邀请大家一同入驻:https://cloud.tencent.com/developer/support-plan?invite_code=e09fc9j7z34


  1. 这里说的是普通硅二极管。肖特基二极管的导通压降更低,大约 0.3 V,所以常用在需要减小损耗的场合。 ↩︎

  2. SchmittSchottky 是两个不同的人名,中文分别译作施密特和肖特基,拼写接近但并不通用。 ↩︎

相关推荐
星栖与芯2 小时前
LiteOS-M 切换汇编逐行图解(5):HalPendSV 任务切换的现场搬运(五阶段逐行)
汇编·stm32·嵌入式硬件·harmonyos
LCG元2 小时前
STM32F103 双ADC交错采样实战:DUALMOD寄存器拆解、DMA位宽陷阱与1.71MSPS有效采样率实测
stm32·单片机·嵌入式硬件
星栖与芯11 小时前
LiteOS-M 切换汇编逐行图解(4):中断三件套与 HalTaskSchedule 触发
汇编·stm32·单片机·嵌入式硬件·harmonyos
LCG元19 小时前
STM32F103 SPI 驱动 W5500 以太网模块实战:硬件协议栈寄存器体系、Socket 状态机避坑与 TCP 吞吐实测
stm32·单片机·嵌入式硬件
Zhang.Yi20 小时前
STM32-SPI
stm32·单片机·嵌入式硬件
无敌贵点大王1 天前
RTThread学习记录10——C语言实现面向对象的编程
c语言·stm32·学习·链表
单片机仿真设计1 天前
【proteus仿真】基于 STM32F103 的汽车温度监测系统(仿真图+程序)
stm32·单片机·嵌入式硬件·proteus·课设
anew___1 天前
舵机——让 Arduino 动起来
c++·stm32·单片机·嵌入式硬件·c
周洲08301 天前
STM32 SPI 通信实战|高速通信、片选、数据收发
stm32·嵌入式硬件