GPIO的运用
目录
前言
因为一直负责项目中的硬件部分,深感不会软件而言硬件的局限性会变得相当大——只能往更加深入的硬件领域,比如专业的PCB layout,DC-DC或者射频方向发展。但是深入的研究往往意味着定型,毕竟人的精力有限,不是哪哪都能兼顾的。然而作为一个刚毕业的大学生,显然还是希望多方向的选择。
所以抱着想要试一试的心态记录一下学习软硬件的结合过程。
一、GPIO的运用
GPIO的运用应该算单片机入门的第一课(严谨地说应该是开始敲代码的第一课)。
如果不懂硬件的话,很多人对GPIO的概念无非就是拉低拉高,最多加上因为中断而了解的上升沿/下降沿。
然而,会有人在实际中经常会出现因为浮空/错误配置上下拉导致各种奇奇怪怪的问题,最后又归结到硬件上,所以对GPIO有一个初步了解真的非常重要。
二、硬件部分
这是STM32中文数据手册里关于GPIO的结构图 ,我们针对这张图进行一个简单的分析讨论
(图源百度,侵删)
2.1 输入
2.1.1 保护二极管
既然是输入,那自然要从I/O引脚作为图的出发点,先是经过保护二极管。
保护二极管顾名思义是起到保护作用。在I/O输入电压高于或低于预设的值时,保护二极管就会导通。这意味着I/O到上下拉电阻之前的值只能是VIN±0.7V(如果二极管的导通压降是0.7V的话,粗略翻了下数据手册并未看到详细的值)
当然,值得一提的是保护二极管在规范的产品设计里它的作用几乎可以忽略不计,因为在设计过程中就需要考虑I/O的容忍值,而非期待保护二极管发挥作用让你的硬件跑起来。芯片设计时这样的设计更多也是为了防静电考虑?笔者才疏学浅,并未对此深入了解。
2.1.2 上下拉电阻
输入的上下拉电阻,往往是为了防止芯片在浮空状态(未知状态)下,出现难以预料的情况而考虑的。试想,按钮如果没有上拉接到VCC,或者没有下拉接到GND,那他输入到I/O的状态如何保证?
这时候,通常就需要一个上下拉电阻来确定按钮在未按下时的一个确切状态,当然,这一般是外部电路设计的,当然如果为了考虑成本/忘记添加临时补救/增加一层保险,就可以选择芯片内部的上下拉电阻进行钳位,实现默认状态(初始状态)的确定性。
不论是软件还是硬件,规范的情况下,还是希望一切都在掌握之中,上下拉的其中一个作用就是为了保证这一点。
虽然图上下拉电阻只存在于输入,但是对于输出而言,同样可以配置上下拉电阻,此时,上下拉的电阻除了钳位之外,更多是为了增加驱动能力。在远古的三极管电路里,你很容易发现一个上拉电阻,这是为了防止单片机没法顺利驱动三极管打开
但是在如今的三极管电路里,更多是增加下拉电阻确保在不用的状态下三极管处于截止状态。
需要注意的是,电阻作为阻碍电子趋势的元器件,它的阻值也对电路有影响,这就是为什么要分出弱上拉,强上拉(下拉同理)的概念。对于STM32而言,芯片内置的上下拉电阻通常在47K左右,这就是弱上拉。
在一些对通讯速率有要求的场景(例如I2C通讯),上拉电阻的阻值影响了通讯速率,此时运用芯片自带的电阻就不是很合适。
2.1.3 肖特基触发器
输入电平是一个确切的电压值,需要将一个模拟值转换成数字值(0/1),就需要通过肖特基触发器,参照手册我们可以这样定义高低电平的范围:
由此我们可以明白,并非是3.3V才叫高电平,也并非0V才叫低电平,高低电平更多是一个范围的统称。
如果是模拟输入或者其他功能复用的话,就跳过肖特基触发器直接输入I/O口,由其他功能模块进行识别工作,在此就不展开描述。
2.2 输出
2.2.1 推挽输出
对于输出来说,我们更多是直接通过它的输出模式来谈论它的输出电路。
推挽输出
推挽输出实际上是利用MOS管的导通特性实现的。 输出上有反向器,输入到两个mos管的控制电压应该相同。
上面是PMOS,VIN和VDD电压相同时截至,VIN低于VDD电压值超过一定范围后逐渐导通。
下面是NMOS,VIN超过一定范围后逐渐导通。
当VIN=高电平时,上管导通,下管截止。VDD顺着上管往VOUT流,故输出高电平。
对于低电平来说,上管截止,那么下管的输入来自于VOUT,只要VOUT是高电平,那VIN低电平时自然导通,直接接到地去了。自然IO口的属性是低电平了。
(不过对于推挽输出检测输入的高低电平,我一直感到疑惑,不知道是否有人可以解释一下(正点原子里就拿推挽输出检测按钮的高低电平)
2.2.2 开漏输出
(图源知乎,侵删)
开漏输出可以理解为对推挽输出的查漏补缺,主要区别在于两个点:
①没有上拉电阻的情况下,开漏输出只能输出低电平。
②开漏输出可以用来线与逻辑,方便一些通讯协议的硬件支持(比如I2C)
三、软件部分
3.1.1 单片机内部结构
软件部分,以正点原子的精英板为例,实现:按钮(PE4)按下后,小灯(PB5)实现状态翻转。
我们首先明白单片机内部控制GPIO的总线
由图可知,GPIOB和GPIOE都挂载在APB2这根时钟线上
3.1.2 GPIO初始化
STM32的库函数已经把GPIO所需要配置的参数都封装在
typedef struct
{
uint16_t GPIO_Pin; /*!< Specifies the GPIO pins to be configured.
This parameter can be any value of @ref GPIO_pins_define */
GPIOSpeed_TypeDef GPIO_Speed; /*!< Specifies the speed for the selected pins.
This parameter can be a value of @ref GPIOSpeed_TypeDef */
GPIOMode_TypeDef GPIO_Mode; /*!< Specifies the operating mode for the selected pins.
This parameter can be a value of @ref GPIOMode_TypeDef */
}GPIO_InitTypeDef;
因为C语言需要先定义,故先在初始化函数里先定义结构体
GPIO_InitTypeDef GPIO_InitStructure; //定义一个结构体
然后需要使能APB2这根时钟线
RCC_APB2PeriphClockCmd(LED_GPIO_CLK,ENABLE); // 使能时钟线
这一步后,我们就可以对结构体内的成员进行配置,实现控制
但为了日后方便,比如突然更换了I/O口,可以快速移植和修改,故通过宏定义的方式对外设进行“标记”。
#define LED_GPIO_CLK RCC_APB2Periph_GPIOB
#define LED_GPIO_PORT GPIOB
#define LED_GPIO_PIN GPIO_Pin_5 //定义GPIO5为 LED灯的GPIO口
此处参考了野火的代码。
然后对结构体内的三个参数进行设置
GPIO_InitStructure.GPIO_Pin = LED_GPIO_PIN ;// 选择LED的GPIO
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz ;
GPIO_InitStructure.GPIO_Mode =GPIO_Mode_Out_PP;
因为是控制灯,所以是输出低电平,用推挽输出。
最后再调用函数,让配置的参数生效
GPIO_Init(LED_GPIO_PORT,&GPIO_InitStructure); //调用库函数对设置的参数进行配置
同理配置一下按钮
void Button_Init(void){
GPIO_InitTypeDef GPIO_InitStructure; //定义一个结构体
RCC_APB2PeriphClockCmd(Button_GPIO_CLK, ENABLE); // 使能时钟线
GPIO_InitStructure.GPIO_Pin = Button_GPIO_PIN ;// 选择BUTTOM的GPIO
//GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz ;
GPIO_InitStructure.GPIO_Mode =GPIO_Mode_IPU;
GPIO_Init(Button_GPIO_PORT, &GPIO_InitStructure); //调用库函数对设置的参数进行配置
}
因为正点原子的开发板按钮并未接上拉电阻,所以需要GPIO配置上拉,不然容易因为浮空出现奇怪的情况。
3.1.3 库函数调用
最后再写一下简单的扫描函数,全部是来源于库函数的调用,就不做赘述
void delay_us(u16 time)
{
u16 i=0;
while(time--)
{
i=10; //自己定义
while(i--) ;
}
}
//毫秒级的延时
void delay_ms(u16 time)
{
u16 i=0;
while(time--)
{
i=12000; //自己定义
while(i--) ;
}
}
void Button_Scan(void)
{
if(GPIO_ReadInputDataBit(Button_GPIO_PORT, Button_GPIO_PIN)==0){
delay_ms(10); //消抖
if(GPIO_ReadInputDataBit(Button_GPIO_PORT, Button_GPIO_PIN )==0){
while(GPIO_ReadInputDataBit(Button_GPIO_PORT, Button_GPIO_PIN)==0){ //等待释放
}
GPIO_WriteBit(LED_GPIO_PORT, LED_GPIO_PIN, (BitAction)!(GPIO_ReadOutputDataBit(LED_GPIO_PORT, LED_GPIO_PIN)));//翻转状态
}
}
}
切记while只是用来等待释放,翻转函数要写在循环外面!!!
最后再补全主函数
int main(void)
{
LED_Init();
Button_Init();
while(1){
Button_Scan();
}
}
3.1.4 最后一点小补充
图源菜鸟教程,帮我梳理了一直搞混的位与位或等操作。
感觉很有用
总结
感觉硬件的运用更吃知识的储备
软件的运用更吃编程的思维
都是需要慢慢学习的过程