本文翻译自:http://www.playembedded.org/blog/stm32-gpio-chibios-pal/
发表于 2017年9月2日 更新了 2018年7月5日
具有LQFP64封装的STM32F103VC
STM32配有极其灵活的通用输入输出 (或GPIO)外设,允许独立配置每个输入/输出。IO是STM32与外界之间最简单的接口。
正如我们在文章“ 从0到STM32 ”中所说的那样,各个STM32的子系列中有许多相同外设的版本,这就是每个子系列通常都有自己的参考手册。在本文档中,可以找到有关GPIO的所有功能信息并阅读许多RM,我们可以注意到GPIO外设有三个硬件版本:
GPIOv3是一个GPIOv2,具有一点点修饰功能,唯一的区别是附加功能和新版本的附加寄存器。GPIOv1就像GPIOv2的古老版本。我不得不说(我的个人意见)整个STM32F1在很多方面都显得很简陋。在它的辩护中,在2007年首次亮相,这是最古老的STM32亚科(它仍然踢屁股)。
在本文中,我们将主要关注GPIOv2,这是最广泛的GPIO,但我们将指出与其他版本的差异,而这是可能的。
尽管它很复杂,但可以通过ChibiOS / HAL的软件驱动程序轻松地对GPIO进行寻址,该驱动程序完全支持这些功能,因此我们无需在寄存器级别使用此外设。管理GPIO外设的驱动程序称为端口抽象层 (也缩短了PAL)。
在本文中,我们将介绍GPIO外设,解释如何使用PAL解释其API和常见用例。
港口的概念
STM32的I / O引脚由16个元素组成,每个元素编号为0到15.每个组都命名为Port并用字母标识:因此,我们有GPIOA,GPIOB,GPIOC等。引脚由字母的组合,因此标识P,端口标识符(甲,乙,Ç,...)和所述销标识符(0,1,2,...)。例如我们有PA1,PB12,PF3,PH0等。
从这里开始,我们将单个GPIO引脚称为引脚 ,将整个GPIO(16个Pad组)称为 端口。
每个端口都有许多32位寄存器,其中一些专用于配置:作用于这些寄存器,可以独立配置每个引脚,改变其模式,输出速度,输出类型和上拉/下拉电阻。
下图表示单个引脚的原理图
引脚的原理图
看看我们可以轻松发现的数字(从右到左):
作用于配置寄存器,可以选择引脚必须使用输入或输出驱动器,启用/禁用上拉/下拉电阻,绕过TTL施密特触发器,重新排列输出驱动器。
请注意,GPIO寄存器是同步的,并通过AHB(高级高性能总线)提供时钟。该寄存器值的刷新率取决于AHB的时钟速度。
上拉/下拉电阻
在电子逻辑电路,一个 拉 - 电阻 是一个 电阻器 连接在信号导体和正的电源电压之间,以确保所述信号处于高逻辑电平或状态时的外部设备断开的电路它自身中的高阻抗条件。
类似地,下拉电阻器是连接在信号导体和负电源电压(或地)之间的电阻器,以确保当外部设备断开或引入高阻抗时信号处于低逻辑电平状态。
GPIO配有可配置的上拉/下拉电阻。作用于上拉/下拉寄存器(缩写为 GPIOx_PUPDR,而x是端口的标识符),可以配置图2的深蓝色网络的行为,选择:
输出驱动程序
CMOS非门的图
输出驱动器可以使用CMOS NOT门进行示意图。该门反转输入信号,其工作原理如图3所示。
它由两个mosfet组成:高端的PMOS和低端的NMOS。这些驱动程序充当互补开关:它们不会同时关闭。
当输入为高电平时,NMOS充当短路,PMOS充当开路:输出被下拉至GND。当输入为低电平时,PMOS充当短路,NMOS充当开路:输出上拉至VCC。
由于上一个原因,PMOS也被称为有源上拉网络,NMOS也被称为有源下拉网络,这种配置也称为 推挽式。
开漏配置的使用案例:有线NOR
可以用弱上拉网络(即上拉电阻)代替PMOS,这种配置称为漏极开路(见图4)。
这种配置非常有趣,因为可以连接以这种方式配置的两个或多个端口的输出,从而获得 有线NOR:只要其中一个门的NMOS导通,输出就被下拉。该解决方案通常用于像I2C这样的竞争总线。
请注意,不可能将有线NOR连接推挽式门一起创建,因为这会在某些情况下导致短路 (见图5)。
短路情况获得了两个CMOS门的布线
输出缓冲器可配置为推挽和漏极开路。可以在输出类型寄存器(也称为 GPIOx_OTYPER)上执行此配置。
输出缓冲器可配置为具有作用于输出速度寄存器(也称为GPIOx_OSPEEDR)的不同压摆率 。
通过该寄存器可以以不同的方式设置输出端口的速度,例如STM32F4上的可用速度为:
而在STM32数据表的输入输出AC特性章节中报告了每个速度在频率方面的实际值。
GPIO模式
GPIO可以在不同的模式下工作。作用于GPIO模式寄存器(也称为GPIOx_MODER),可以在四种不同模式下对引脚进行编程。
输入模式
当我们需要采样引脚的逻辑电平时,使用此模式。当引脚被编程为输入时,其逻辑电平在每个时钟周期被采样,并且可以通过输入数据寄存器(也称为GPIOx_IDR)进行检索。
当引脚被编程为输入时,输出缓冲器被禁用,TTL施密特触发器被打开。 可以在GPIOx_PUPDR 上启用\禁用上拉/下拉电阻。 施密特触发器通常用于数字电路,因为其工作原理在输入信号出现噪声的情况下提供信号鲁棒性:由于其滞后,它能够避免端口换向期间的多次转换,通常称为毛刺。
输出模式
当我们需要将引脚用作数字输出时,使用此模式。当引脚被编程为通用输出时,输出缓冲器使能并根据GPIOx_OTYPER配置为推挽或漏极开路 ,并且TTL施密特触发器打开。 可以在GPIOx_PUPDR 上启用\禁用上拉\下拉电阻。输出压摆率可通过GPIOx_OSPEEDR 配置。
端口状态可以通过输出数据寄存器(也称为GPIOx_ODR)进行配置 。请注意, GPIOx_IDR 仍会报告当前端口状态的状态。
模拟模式
当我们需要通过DAC外设生成模拟信号或使用ADC外设对其进行采样时,使用此模式。当引脚被编程为模拟时,旁路TTL施密特触发器并禁用输出缓冲器以及上拉/下拉电阻。对GPIOx_IDR的 读访问权为“0”。
我们将在稍后的专门文章中讨论使用ADC和DAC。无论如何要注意,为了避免任何噪声,不应将未使用的引脚编程为模拟,而应将其设置为带上拉电阻的输入。
在GPIOv3中,还有一个名为模拟开关控制寄存器的寄存器(也称为GPIOx_ASCR)。要使用模拟模式,需要在该寄存器中为每个引脚设置一个附加位。
替代模式
该模式用于将引脚分配给STM32的外设。STM32配备了大量内部外设,通过多路复用器与GPIO引脚互连:作用于这些多路复用器,可以重新路由连接。
备用模式还需要多路复用器选择器来选择重新路由。可以根据名为备用功能高位寄存器(GPIOx_AFHR)和 备用功能低位寄存器(GPIOx_AFLR)的几个寄存器更改选择器。
重新路由是有限的,无法为任何引脚分配任何外设,无论如何,外设不仅映射到引脚:这保证了硬件设计阶段的更高灵活性。
备用功能图在STM32数据表中的章节引脚和引脚描述中报告在名为备用功能映射的表中(见图6)。
备用函数映射的屏幕截图
当引脚被编程为交替模式时,输出缓冲器被使能并根据GPIOx_OTYPER配置为推挽或漏极开路,引脚 由来自外设的信号驱动。
可以在GPIOx_PUPDR上启用\禁用上拉/下拉电阻。输出压摆率可通过GPIOx_OSPEEDR配置。
TTL施密特触发器打开并访问 GPIOx_IDR,可以检查当前端口状态。
GPIO绝对最大额定值
GPIO电气特性在STM32数据表的电气特性章节中报告,其中包括可接受的VDD范围。
GPIO由VDD供电,表示为整个STM32供电的正电压。VDD也表示STM32的高逻辑电平。大多数STM32可以在1.7 V和3.6 V之间工作。无论如何,VDD的实际值取决于电路板原理图,可以在使用的电路板的用户手册中找到。
大多数STM32开发板都配备了双电压LDO 5 V - 3.3 V.5 V用于某些外设,如USB OTG。STM32提供3.3 V电压,在此条件下,逻辑“1”对应3.3 V,而逻辑“0”为0 V.
STM32 GPIO具有5 V容差,并且与TTL逻辑电平兼容。
另一个重要的事情是单个引脚能够提供/吸收的最大电流。同样,这取决于MCU,但大多数STM32能够为每个引脚吸收/提供高达25 mA的电流,但所有引脚的总电流限制不得超过120 mA。这些电流足以驱动5 mm单色LED,通常吸收不超过10 mA。
初步说明
命名约定和自动完成
PAL驱动程序的每个API都以前缀“pal”开头。函数名称为驼峰式,预处理器常量为大写,变量为小写。
自动完成的一个例子
该前缀对于自动完成目的非常有用。例如,在Eclipse中,开始编写一个函数并按下CTRL + SPACE,IDE将显示自动完成的建议。在这种情况下写“pal”并按下CTRL + SPACE我们将看到PAL驱动程序的可用API作为建议,如图7所示。
对于其他ChibiOS / HAL驱动程序也是如此。例如,SPI驱动程序的功能以“spi”为前缀,PWM驱动程序的功能以“pwm”为前缀,ADC驱动程序的功能以“adc”为前缀,依此类推。
与Project Index相关的自动完成,我们的代码中包含的符号列表,如函数名称,全局变量,宏等,Eclipse创建自动构建项目。要使用自动完成,项目必须至少构建一次。
如果出现问题,可以强制索引重建右键单击项目并选择Index - > Rebuild。
逻辑水平
引脚的逻辑电平在PAL中定义为:
/** * @name Logic level constants * @{ */ /** * @brief Logical low state. */ #define PAL_LOW 0U
/** * @brief Logical high state. */ #define PAL_HIGH 1U /** @} */ |
端口,垫和线
当我们使用PAL驱动程序时,我们需要确定我们要编程的I / O引脚。基本上这个驱动程序有四组功能:
Line是一种在单个标识符中编码端口和填充信息的智能方法。用户可以使用宏生成行标识符
/** * @brief Forms a line identifier. * @details A port/pad pair are encoded into an @p ioline_t type. The encoding * of this type is platform-dependent. */ #define PAL_LINE(port, pad) \ ((ioline_t)((uint32_t)(port)) | ((uint32_t)(pad))) |
以代码为例
#define LINE_PA5 PAL_LINE(GPIOA, 5) |
我们正在为引脚PA5定义线路标识符。
要识别单个I / O,我们需要端口和填充标识符或其他行标识符。每种识别方法都有自己的一套功能。
从现在开始,我们通常依靠逐位运算符来进行位掩码。如果您不习惯这些操作员之王,我建议您阅读寄存器和位掩码。
板级的初始化
每个垫必须在使用前正确配置。每个Pad的配置通常取决于我们的应用,但在某些情况下,它由电路板原理图绑定。例如,让我们考虑图7中的原理图:该图表示STM32F3 Discovery用户手册中的原理图。
“STM32F3 Discovery用户手册”中的原理图
在这里可以看到彩色LED连接到PE8到PE15的焊盘并使用它们这些焊盘必须配置为输出推挽而没有上拉/下拉电阻。
项目屏幕及其电路板文件。
类似地,焊盘PA0连接到用户按钮(已提供或下拉电阻和去抖动电路)并使用它我们需要将此引脚配置为浮动输入。在该示意图中还可以看到需要其他引脚配置的一对MEMS。
好吧,我们不必太担心这些配置,因为初始化PAL驱动程序时,它会根据一系列配置对所有GPIO进行编程,这些配置存储在名为board.h的头文件中。每个板都有自己的板头文件。为了便于使用,将板文件链接到项目中,并将其链接到名为board的链接文件夹中。
电路板文件是了解如何预先配置引脚的快速参考。重述:
与GPIO相关的所有与板相关的配置都已由ChibiOS PAL驱动程序执行。这些配置存储在板文件中,可以作为快速参考进行探索。
初始化由 palInit()执行,只要启用了halconf.h中的PAL开关,该API就由halInit() 执行
/** * @brief Enables the PAL subsystem. */ #if !defined(HAL_USE_PAL) || defined(__DOXYGEN__) #define HAL_USE_PAL TRUE #endif |
为了提高电路板文件中的代码可读性,引脚标识符被重新定义为预处理器常量。例如,下一个代码来自STM32 Nucleo F401RE board.h文件
/* * IO pins assignments. */ #define GPIOA_ARD_A0 0U #define GPIOA_ADC1_IN0 0U #define GPIOA_ARD_A1 1U #define GPIOA_ADC1_IN1 1U #define GPIOA_ARD_D1 2U #define GPIOA_USART2_TX 2U #define GPIOA_ARD_D0 3U #define GPIOA_USART2_RX 3U #define GPIOA_ARD_A2 4U #define GPIOA_ADC1_IN4 4U #define GPIOA_LED_GREEN 5U #define GPIOA_ARD_D13 5U #define GPIOA_ARD_D12 6U #define GPIOA_ARD_D11 7U #define GPIOA_ARD_D7 8U #define GPIOA_ARD_D8 9U #define GPIOA_ARD_D2 10U #define GPIOA_OTG_FS_DM 11U #define GPIOA_OTG_FS_DP 12U #define GPIOA_SWDIO 13U #define GPIOA_SWCLK 14U #define GPIOA_PIN15 15U |
这意味着以下两个陈述是等价的
palClearPad(GPIOA, 5);
palClearPad(GPIOA, GPIOA_LED_GREEN);
palClearPad(GPIOA, GPIOA_ARD_D13); |
但第二个更清晰:我们正在对LED_GREEN采取行动。电路板文件还包含行重新定义
/* * IO lines assignments. */ #define LINE_ARD_A0 PAL_LINE(GPIOA, 0U) #define LINE_ADC1_IN0 PAL_LINE(GPIOA, 0U) #define LINE_ARD_A1 PAL_LINE(GPIOA, 1U) #define LINE_ADC1_IN1 PAL_LINE(GPIOA, 1U) #define LINE_ARD_D1 PAL_LINE(GPIOA, 2U) #define LINE_USART2_TX PAL_LINE(GPIOA, 2U) #define LINE_ARD_D0 PAL_LINE(GPIOA, 3U) #define LINE_USART2_RX PAL_LINE(GPIOA, 3U) #define LINE_ARD_A2 PAL_LINE(GPIOA, 4U) #define LINE_ADC1_IN4 PAL_LINE(GPIOA, 4U) #define LINE_LED_GREEN PAL_LINE(GPIOA, 5U) #define LINE_ARD_D13 PAL_LINE(GPIOA, 5U) #define LINE_ARD_D12 PAL_LINE(GPIOA, 6U) #define LINE_ARD_D11 PAL_LINE(GPIOA, 7U) #define LINE_ARD_D7 PAL_LINE(GPIOA, 8U) #define LINE_ARD_D8 PAL_LINE(GPIOA, 9U) #define LINE_ARD_D2 PAL_LINE(GPIOA, 10U) #define LINE_OTG_FS_DM PAL_LINE(GPIOA, 11U) #define LINE_OTG_FS_DP PAL_LINE(GPIOA, 12U) #define LINE_SWDIO PAL_LINE(GPIOA, 13U) #define LINE_SWCLK PAL_LINE(GPIOA, 14U) |
这意味着以下两个陈述也是等价的
palClearPad(GPIOA, GPIOA_LED_GREEN);
palClearLine(LINE_LED_GREEN); |
改变输出引脚的逻辑电平
PAL驱动程序最简单的用例是我们在之前的文章中已经看过的LED切换。当I / O配置为输出时,可以打开(和关闭)LED,只需改变引脚的逻辑电平即可。
我们可以使用许多函数来处理逻辑级别。为了处理单个引脚,我们可以使用“ Pad ”相关功能或“ Line ”相关功能。
通用API是Set / Clear / Toggle Pad / Line功能
palSetPad(port, pad) palSetLine(line)
palClearPad(port, pad) palClearLine(line)
palTogglePad(port, pad) palToggleLine(line) |
第一对将输出引脚设置为PAL_HIGH ,第二对将输出引脚设置为PAL_LOW,最后一对设置切换引脚状态,这意味着如果在切换功能调用后引脚逻辑电平为 PAL_HIGH,则会将其电平更改为PAL_LOW (和反之亦然)。
最后几个函数有点不同,因为它们会收到额外的参数
palWritePad(port, pad, bit) palWriteLine(line, bit) |
在这种情况下,参数位表示我们要在目标引脚(PAL_HIGH或PAL_LOW)上写入的逻辑电平。只是为了让事情更清楚,下一个陈述是等价的
palWriteLine(LINE_ARD_D1, PAL_HIGH); palSetLine(LINE_ARD_D1); |
并且
palWriteLine(LINE_ARD_D1, PAL_LOW); palClearLine(LINE_ARD_D1); |
同样,可以通过函数对整个端口进行操作
palSetPort(port) palClearPort(port) palTogglePort(port) palWritePort(port, bits) |
请注意,这里的位是16位无符号,表示每个引脚的状态。举个例子
palWritePort(GPIOA, 3); |
将PA0和PA1设置为PAL_HIGH,将PA2和PA15的焊盘设置为PAL_LOW。
也可以使用这些功能对一组焊盘或整个端口起作用
palWriteGroup(port, mask, offset, bits) |
而端口是端口标识符,掩码是焊盘选择掩码,偏移组位偏移,位表示要写入的位。此函数作用于GPIOx_ODR,替换掩码的位
mask << offset |
同
bit << offset |
理解我们刚才所说的那些了解位掩码如何工作的人应该几乎是立竿见影的。在任何情况下,您都可以查看寄存器和位掩码。
获得输入引脚的逻辑电平
PAL驱动程序提供了一些功能来处理设置为输入的引脚,类似于用于管理输出的引脚。在这种情况下,这些函数获得焊盘的逻辑电平。例如
palReadPad(port, pad) palReadLine(line) |
根据引脚的状态返回PAL_HIGH或PAL_LOW。通过这些功能也可以读取组和端口
palReadGroup(port, mask, offset) palReadPort(port) |
其中返回值是无符号整数,而每个位代表端口/组的状态。
即时配置引脚
某些应用程序需要在代码执行期间更改引脚配置。要做到这一点,PAL提供了一组特定的功能来改变引脚配置重新编程它们的行为。即使在这种情况下,也有配置单个引脚,一组引脚或整个端口的功能
palSetPadMode(port, pad, mode) palSetLineMode(line, mode) palSetGroupMode(port, mask, offset, mode) palSetPortMode(port, mode) |
在这里,我们必须花一些关于模式的话。 PAL设计用于许多微控制器,并带有一组适用于每个微控制器的通用模式。这些模式是:
当然这些模式是通用的,不能映射STM32微控制器的所有模式。因此,STM32还有一些额外的特定模式,可以混合到之前以获得所有需要的模式。
例如,我们有一个额外的模式来处理备用函数:
对于端口速度:
对于上拉\下行配置: