嵌入式基础教程
——
STM32单片机卷2HelloWorld
节02GPIO通用输出

最新版本V1.0
王道嵌入式团队
COPYRIGHT ⓒ 2021-2025 王道版权所有

前置知识回顾

Gn!

首先我们先来回顾一下,前面我们已经讲解过的知识点,以便于进行进一步的课程内容学习。

目前我们已经讲过MCU的系统结构和引脚定义:

  1. 在讲系统结构时,我们提到:STM32基础的学习,某种程度上就是学习利用芯片控制片内外设,从而实现各种功能。

  2. 而在讲完芯片引脚定义后,我们做了这样的比喻:

    1. 某个片内外设就像人的一只手(假设这个人手很多),每只手都承担着特定的功能。

    2. 引脚就像手指头,不同的手指头功能不同。手要实现某个动作,需要动用手指头,甚至多个手指头共同协作。

    3. 操作片内外设,最终的落脚点也在于操作引脚,可以是单个引脚也可以是多个引脚的协作。

现在我们的需求是:操作PC13引脚,来控制PC13指示灯的亮灭。而PC13属于GPIOC外设的引脚之一,于是我们就引出了三个新问题:

  1. GPIO是什么?

  2. 如何操作引脚呢?

  3. PC13引脚的操作,是如何影响指示灯的亮灭呢?

下面我们逐一解决这些问题。

GPIO概述

Gn!

GPIO是词组General Purpose Input Output的首字母缩写,直接翻译成中文就是通用输入输出端口,是MCU中最基础、且最常用的外设部分。

STM32F103C8T6共有四大类GPIO端口,每个端口又包含数量不等的普通IO引脚,分别是:

  1. GPIOA:PA0、PA1、PA2、PA3、PA4、PA5、PA6、PA7、PA8、PA9、PA10、PA11、PA12、PA13、PA14、PA15

  2. GPIOB:PB0、PB1、PB2、PB3、PB4、PB5、PB6、PB7、PB8、PB9、PB10、PB11、PB12、PB13、PB14、PB15

  3. GPIOC:PC13、PC14、PC15

  4. GPIOD:PD0、PD1

注意:其中PD0和PD1引脚常用于配置启用外部晶振,通常都不能用于普通IO引脚,最小系统板上也没有将这两个引脚引出。

STM32F103C8T6一共有48个引脚,而GPIO下的普通IO引脚就占据了37个,这充分说明了,GPIO的重要性以及基础性。

GPIO可以配置为8种输入输出模式,简单来说是(下面会详细说):

  1. 四种输出模式,配置输出模式后,可以通过引脚输出高低电平。引脚输出的高电平一般就是3.3V,因为供电就是3.3V,所以不可能输出超过3.3V。(这些内容如果忘记了,可以再看一下硬件基础中讲高低电平的部分)

  2. 四种输入模式,配置输入模式后,可以通过引脚接收外部电路的高低电平。接收输入电平时,高电平一般是3.3V左右,部分FT接口允许输入5V电压。(这些内容如果忘记了,可以再看一下硬件基础中讲高低电平的部分)

输出模式下可控制端口输出高低电平,用以驱动LED(点灯大师)、控制蜂鸣器、模拟各种通信协议输出数据等。

输入模式下可读取端口的高低电平或电压,用于按键输入检测、开关状态检测、传感器信号采集、串口通信接收等。

总之GPIO是贯穿我们整个STM32学习的一种片内外设资源,它很重要,但其实它并不难,大家不要害怕,安心学好它。

如何操作引脚

Gn!

以操作GPIO的IO引脚为例子,我们来讲一下引脚的操作,其它引脚操作可能会有所不同,但大同小异。

要想弄明白如何操作引脚,就需要弄清楚引脚内部的电路是什么样的。我们打开《STM32F103参考手册》,找到章节GPIO后,在章节的开头手册中就给出了引脚的内部电路图,如下所示:

IO引脚内部电路-图

这里我们先来讲解一下两个保护二极管:

保护二极管

Gn!

IO引脚和内部电路之间有两个保护二极管,如下图所示:

保护二极管-图示

其中上面的二极管,我们称之为"正向保护二极管",它的作用是保护引脚内部电路不会因为过高的电压而损坏。

它的正极和IO引脚连接,负极接入电源正极。

如果引脚突然输入了一个很高的电压(大于Vdd的电压)时,引脚电压高于VDD,形成电势差,此时上方的正向保护二极管正向偏置,大电流会从引脚流向VDD,从而实现保护内部电路的作用。

比如人用手直接触摸芯片的引脚,由于人体携带有静电,瞬时电压可能高达上万V,若没有上方的正向保护二极管,内部电路就会很容易被破坏掉。

下方的二极管,称之为"反向保护二极管",其原理也是大同小异的。

它的正极接入电源负极/接地,负极和IO引脚连接。

如果引脚外部输入的电压非常低,甚至是一个负电压时,引脚电压低于VSS,形成电势差,此时下方的反向保护二极管也会正向偏置(上方的二极管反向偏置,无法形成电流),会形成从VSS到IO引脚的电流,避免引脚过低的电压对内部电路造成损害。

人体的静电或者反接电源等都有可能使得引脚输入低电压,反向保护二极管就起到了保护作用。

引脚输出模式时的简化电路图

Gn!

我们发现这张图的上半部分属于IO引脚的输入模式,本小节不讲输入模式,所以上半部分我们直接简化掉,下面的输出部分中有些部分也可以简化掉,最后我们可以得到这样的一张简化"输出模式"电路图

输出模式电路简化-图

从图中我们可以得出结论:程序员只要控制CPU向输出数据寄存器中写入0/1,就能够控制引脚输出了。

但是:

  1. 仅上图而言,这个引脚是没有任何输出的,因为两个MOS管(上PMOS,下NMOS)都是断开的,引脚和内部电路是断开的。

  2. 输出数据寄存器只能写入1或0两种选项,但两个MOS管的开闭组合显然不止两种。

所以在向输出数据寄存器写入0/1控制引脚输出之前,需要先配置引脚的输出模式,这同时是通过寄存器配置来实现的。

通过配置寄存器,配置不同的引脚输出模式,两个MOS管会有不同的通断表现。

在这里我们先来讲解两个常用的输出模式:

  1. 通用推挽输出模式

  2. 通用开漏输出模式

注意,这两种输出模式的差别就在于两个MOS管的通断模式。

通用推挽输出模式

Gn!

首先,通用推挽输出模式是最容易理解的输出模式,因为它的名字就表示它的作用:

  1. 推,也就是push,你可以想象成将电流从引脚"推"出去,也就是引脚输出高电平

  2. 挽,一个文艺好听的翻译,但使得它有点不太好理解。挽,其实就是拉,也就是pull,你可以想象成将电流从引脚"拉"回来,电流从高电势流向低电势,所以"拉"电流就意味着引脚输出低电平

向输出数据寄存器中写入0/1就恰好对应这两种状态,而且:

  1. 向输出数据寄存器中写入0,表示引脚输出低电平。

  2. 向输出数据寄存器中写入1,表示引脚输出高电平。

注意:这就是一种单纯编程设计,目的是便于程序员记忆,便于编程。毕竟低电平往往用0表示,写0就表示输出低电平,合情合理。

通用推挽输出模式的表现,就讲完了,那么两个MOS该如何开闭才能达成这样的效果呢?

我们分为两部分来讲解。

引脚输出高电平

Gn!

首先,图中的两个MOS管,我们先来标注一下它的三个极。如下图所示:

引脚输出高电平-示意图

很明显,这两个MOS管是不可能同时导通的,如果同时导通,PMOS的源极就会直连NMOS的源极,相当于电源正负极直接连在一起短路。

在通用推挽输出模式下,上下两个MOS管是交替闭合的,这就是通用推挽输出模式的MOS管通断表现。

为了引脚能够输出高电平,电路MOS的导通状态一定如下图所示,即上面的PMOS管导通:

引脚输出高电平-示意图2

那我们思考一下输出控制电路,加在两个MOS管栅极的电压是什么呢?

很明显:

  1. PMOS管导通,所以向PMOS管施加的是低电平,栅极G和源极S存在电势差,PMOS管导通。

  2. NMOS管断开,所以向NMOS管施加的也是低电平,栅极G和源极S没有电势差,NMOS管断开。

当然有些同学会觉得向输出数据寄存器写1,相当于"高电平",应该在两个MOS的栅极施加高电平,但实际上输出控制电路会将这个高电平转换成低电平施加在栅极。

引脚输出低电平

Gn!

为了让引脚能够输出低电平,电路MOS的导通状态一定如下图所示,即下面的NMOS管导通:

引脚输出低电平-示意图

那我们思考一下输出控制电路,加在两个MOS管栅极的电压是什么呢?

很明显:

  1. PMOS管断开,所以向PMOS管施加的是高电平,栅极G和源极S没有电势差,PMOS管断开。

  2. NMOS管导通,所以向NMOS管施加的也是高电平,栅极G和源极S有电势差,NMOS管闭合。

当然有些同学会觉得向输出数据寄存器写0,相当于"低电平",该在两个MOS的栅极施加低电平,但实际上输出控制电路会将这个低电平转换成高电平施加在栅极。

通用开漏输出模式

Gn!

上面我们解释了"推挽输出"为什么叫"推挽",进而就分析出了推挽模式的电路表现。通用开漏模式,仍然可以继承这样的分析模式。

什么是开漏呢?

电路图中,有什么东西和漏有关?

那自然是MOS管的漏极,所以开漏指的是MOS管的漏极处于开路状态,即MOS管的漏极不能和电源正极接在一起。

我们分析电路,发现:

若想实现这一点,就必须让PMOS管始终保持断开状态,也就是让电源正极不能接入电路。

所以当引脚处在通用开漏输出模式时,两个MOS管的通断表现为:

  1. PMOS管始终处于断开状态。

  2. 向输出数据寄存器写入0时,表示引脚输出低电平,此时NMOS管闭合。

  3. 向输出数据寄存器写入1时,引脚对外部电路表现为高阻抗,也就是电路断开,此时NMOS管也断开。

通用开漏输出模式-引脚输出低电平:

通用开漏输出模式-图1

通用开漏输出模式-引脚表现为悬空,高阻抗:

通用开漏输出模式-图2

注意:

无论是推挽输出、亦或者开漏输出模式,设置完成引脚输出模式后,引脚没有默认的输出电平,所以可以给引脚设置一个初始的输出电平。

总结

Gn!

对于GPIO的普通IO引脚的操作:

  1. 首先需要配置寄存器确定引脚的工作模式

  2. 然后再向输出数据寄存器写0/1

按照上述两个步骤进行,就可以实现各种引脚的操作,从而实现各种功能。

现在我们已经学习过两种GPIO的输出模式了:

  1. 推挽输出模式:

    1. 向输出寄存器写1,表示引脚输出高电平

    2. 向输出寄存器写0,表示引脚输出低电平

  2. 开漏输出模式:

    1. 向输出寄存器写1,表示引脚悬空,高阻抗。

    2. 向输出寄存器写0,表示引脚输出低电平

STM32的开发方式

Gn!

到此为止,我们可以做出以下推论:

我们使用各种MCU来实现功能,本质上是利用此款MCU当中的各种片内外设。

而操作片内外设,本质上就需要操作各种引脚。

而引脚的操作,则是通过对各类寄存器的操作来完成。

所以我们任何操作,最终的落脚点都在于操作寄存器。

那么什么是寄存器,如何理解寄存器呢?

寄存器就像是一个个小小的 “储物盒”,专门用来存放和管理数据。

这些"储物盒"有些在CPU内部,也有些在各种外设内部,但不管怎么样每个寄存器都有自己固定的地址和功能。好比家里的抽屉,每个抽屉都有固定的位置,并且用来存放特定的东西。

这些寄存器可以存储各种信息,比如控制引脚状态的信息、片内外设的配置参数、数据的暂存等等。当我们要操作 MCU 的某个功能时,就像是给这些 “储物盒” 里放入或取出特定的数据,从而改变 MCU 的工作状态和行为。

例如,要让一个 GPIO 引脚输出高电平,我们就通过编程把表示高电平的数据写入到对应的 GPIO 控制寄存器中,MCU 就会根据这个寄存器里的数据来设置引脚的状态。

总之,从程序员的角度来思考,寄存器就是程序和硬件沟通的"桥梁",通过操作寄存器,程序员就实现了操作引脚。

于是,我们就引出来STM32第一种,也是直接的开发方式,寄存器开发。

寄存器开发

Gn!

MCU的设计本身比较简单,一般也没有操作系统,所以也没有虚拟内存、MMU内存映射等概念。

一切寄存器都有一个固定的、用于给程序员操作的物理地址,而且MCU厂商都会在芯片参考手册中明确给出某个寄存器的物理地址。

比如STM32都是32位的寄存器,内存地址也是32位的,参考手册中就会给出一个8位十六进制的地址值,程序员使用这个地址值就可以直接操作相应的寄存器了。

寄存器开发的原理:

直接对 STM32 芯片内部的寄存器进行操作。每个外设都有一组特定的寄存器,通过对这些寄存器的地址进行读写操作,来配置外设的工作模式、控制其运行状态。

例如,要配置 GPIO 引脚为输出模式并输出高电平,就需要找到对应的 GPIO 控制寄存器,通过向寄存器写入特定的值来实现。

寄存器开发的优势:

  1. 代码执行效率高:直接操作硬件寄存器,没有中间层的开销,代码执行速度快,能充分发挥硬件性能。

  2. 资源占用少:直接使用寄存器开发是最直接的开发方式,不存在任何封装,各种资源的占用一般都是最少的。

  3. 有利于深入了解硬件:开发过程中需要深入了解芯片的硬件结构和寄存器功能,有助于开发者透彻掌握 STM32 的工作原理。

寄存器开发的劣势:

  1. 开发难度大:需要查阅/记住大量寄存器的地址、功能和操作方法,编程过程繁琐,容易出错。

  2. 开发效率低:每一个功能都需要从底层寄存器配置开始,代码量比较大,开发周期也会比较长。

  3. 可移植性差:不同型号的 STM32 芯片寄存器地址和功能可能存在差异,代码难以在不同型号间移植。

寄存器开发最大的优势就是性能强,但在很多场景中性能并不是我们追求的最大目标。

所以,寄存器开发并不是现在STM32开发的主流形式,尤其对于新手而言,如果入门就使用寄存器开发,将会最大化学习难度,而且及其繁琐,难以坚持学习。

在本教程当中,我们只不会将寄存器开发作为主流方式。

库函数开发

Gn!

ST公司(其实每个MCU厂商都是这样)在推出STM32系列MCU后,他们也觉得寄存器的开发方式过于原始,难度大且开发效率低。

所以ST公司把对寄存器的操作封装成一个个的库函数,开发者通过调用这些库函数来间接实现对寄存器的操作,从而实现对各种外设的操作,我们把这种开发方式称之为"标准外设库函数开发"、或者直接简称"库函数开发"

库函数开发的优势:

  1. 比较高的开发效率:相比于寄存器的开发方式,库函数的开发方式提高了开发效率。

  2. 代码的可读性更好:库函数的函数名和参数都具有明确的意义,代码逻辑清晰,易于理解和维护。

  3. 对初学者比较友好:库函数的开发方式在一定程度上屏蔽了底层寄存器的操作,所以相比较于寄存器的方式学习难度降低了。但库函数本质上是对寄存器操作的封装,使用库函数开发还是需要开发者对 STM32 的硬件架构和寄存器有一定了解的。这个平衡,恰好对于STM32的初学者是非常有利的。

库函数开发的劣势:

  1. 可能被弃用:ST官方虽然从没有正式说明要放弃更新标准外设库,但从越来越慢的更新频率以及新型号MCU开始不支持标准库等情况来看,都说明ST官方有弃用标准库的趋势。

  2. 部分高级功能比较欠缺:标准库函数在实现一些高级复杂功能时,往往还需要开发者自己编写代码

  3. 可移植性有,但并不高:库函数虽然在一定程度上屏蔽底层,屏蔽了不同型号芯片之间的寄存器差异,但实际上还是会有一些差异,库函数开发出来的程序,其移植性差强人意,不够好。

对于初学者而言,我们建议大家采用库函数的方式入门学习。库函数的开发方式在所有开发方式中,处于中间层,具有进可攻退可守的特点:

  1. 一方面,它不像寄存器开发那样过于底层和复杂,能让初学者快速上手,积累开发经验。

  2. 另一方面,相比于一些更高级的开发框架,它又能让开发者对硬件架构和寄存器有一定的了解,为后续深入学习打下基础。

而且基于市场惯性的影响,库函数开发方式在实际应用中仍然占据相当的市场份额。

本课程中,我们主要就以库函数开发的形式,来进行STM32的开发。

HAL库开发

Gn!

既然要屏蔽底层硬件,既然要简化开发,那就贯彻到底,HAL库就是这种思潮下的产物。

HAL(Hardware Abstraction Layer,硬件抽象层)库是 ST 公司为 STM32 系列微控制器开发的一种软件库,它的主要目的是将 STM32 微控制器的底层硬件细节进行抽象封装。

通过 HAL 库,开发者不需要直接操作复杂的硬件寄存器,而是调用库中提供的函数来实现对微控制器各种外设的操作和控制。这极大地简化了开发过程,降低了开发难度,尤其是对于初学者和需要快速开发的项目。

HAL库本质上仍然是对底层寄存器操作的封装,但HAL库有以下独特的优势

  1. 封装层次更高。HAL 库在对寄存器操作的封装上更为抽象和高级。它不仅仅是简单地将寄存器操作封装成函数,还在函数的设计和实现上进行了更高层次的抽象。

  2. 统一性更强,以实现更好的跨平台性。ST公司呕心沥血,为不同型号的 STM32 微控制器提供了统一的 API 接口,确保在不同的芯片上使用相同的函数和相似的参数来实现相同的功能。

  3. 复杂和高级功能更多,更好用。HAL 库对复杂的功能进行了集成和优化。对于像 USB、以太网、CAN 总线等复杂外设,HAL 库提供了一套完整的操作函数和状态机,开发者可以直接使用这些函数进行高级功能的开发,而无需像使用标准库那样可能需要自行组合多个基础函数或者编写额外的代码来实现这些功能。

  4. 开发效率非常高。由于HAL库的高度封装屏蔽底层,以及拥有风格统一的、功能完善的各类API,使用HAL库开发的开发效率是极高的。

  5. ST公司主推,倾注了主要精力。任何东西只要愿意雕琢,总会变得越来越好。HAL库就是ST公司精心雕琢的结果,可以预见这种强大的支持还会持续很长一段时间。比起标准库的"弃子"身份,HAL库显然优势明显。

使用HAL库开发也有一些劣势:

  1. 体积大,资源占用多。HAL库的深度封装使得使用HAL库开发生成的可执行程序体积偏大,占用资源也会更多。在资源紧张的MCU中,HAL库开发可能不是最好的选择。

  2. 性能稍弱。深度的封装带来了一定的性能损失。

  3. 对底层硬件理解不深。如果需要进行硬件层次的优化,可能难以下手。

总之,HAL由于其快速开发的特性,加上ST公司的大力支持,已经成为STM32开发中的主流手段之一。

但是作为初学者,一方面需要了解一些底层硬件知识,另一方面在入门时,没必要同时兼顾多种开发手段增加负担,所以我们的课程会以标准库开发为主要手段。

相信当你理解掌握标准库后,再自己学习HAL库会非常简单。

电路接法和引脚输出模式选择

Gn!

为了点亮PC13指示灯,我们需要进行什么样的引脚操作呢?

为了讲清楚这一点,我们不妨先来看一看若希望使用引脚操作来控制一盏LED的亮灭,需要设计怎样的电路。我们可以直接将简化的引脚电路和LED相连接起来,按照输出模式的不同,分为两种情况:

  1. 推挽输出模式接法

  2. 开漏输出模式接法

推挽输出接法

Gn!

电路图如下所示:

推挽输出点亮LED-图

推挽输出模式下,引脚可以输出高电平和低电平。在这种情况下,为了能够控制LED的亮灭,LED的另一端必须接入低电平(接地)。

这样:

  1. 引脚输出高电平,LED亮

  2. 引脚输出低电平,LED灭

结合的寄存器的操作,如果采用通用推挽输出模式接法,操作流程是这样的:

  1. 通过配置引脚寄存器,将IO引脚的工作模式设置为推挽输出模式。

  2. 向输出数据寄存器中写1,使得引脚输出高电平,于是点亮LED

  3. 向输出数据寄存器中写0,使得引脚输出低电平,于是熄灭LED

开漏输出接法

Gn!

电路图如下所示:

开漏输出点亮LED-图

开漏输出模式下,引脚可以输出低电平和高阻抗。在这种情况下,为了能够控制LED的亮灭,LED的另一端必须接入高电平(电源正极)。

这样:

  1. 引脚输出低电平,LED亮

  2. 引脚输出高阻抗,LED灭

结合的寄存器的操作,如果采用通用开漏输出模式接法,操作流程是这样的:

  1. 通过配置引脚寄存器,将IO引脚的工作模式设置为开漏输出模式。

  2. 向输出数据寄存器中写0,使得引脚输出低电平,即点亮LED

  3. 向输出数据寄存器中写1,引脚对外表现为高阻态,此时LED熄灭。

也就是说,使用推挽输出和开漏输出两种输出模式都可以实现点灯,但需要连接的电路是不同的,寄存器的操作也稍有区别。

控制PC13指示灯的亮灭

Gn!

通过查阅《STM32F103C8T6核心板原理图》,可得知PC13指示灯的电路图如下所示:

PC13指示灯-电路图

这种电路的接法,若想控制LED的亮灭,需要使用什么输出模式呢?

其实这两种输出模式都是可以的。我们可以分析一下:

如果选择开漏输出模式:

  1. 向输出数据寄存器中写0,引脚输出低电平,电流正向通过LED,即点亮LED

  2. 向输出数据寄存器中写1,引脚状态为高阻抗(断路),电路中无电流通过,即熄灭LED

如果选择推挽输出模式:

  1. 向输出数据寄存器中写0,引脚输出低电平,电流正向通过LED,即点亮LED

  2. 向输出数据寄存器中写1,引脚输出高电平,电路两端没有电势差,不会有电流经过LED,即熄灭LED

当然,更好的选择是将PC13引脚设置为开漏输出模式:

  1. 因为PC13指示灯是典型的开漏接法电路

  2. 此电路的通断控制仅需要依赖"拉低--断开(高阻抗)"这两种状态,不需要高电平参与电路控制。

  3. 推挽模式虽然也可以用,但由于它不能完全"断开"电路,可能会出现"LED无法完全熄灭,半亮不亮"的情况。

至此,我们已经完成了所有操作的分析,万事俱备只欠东风,下面我们就开始通过标准库函数来实现对PC13指示灯的控制。

The End