从零开始手搓一个STM32与机智云的小项目——GPIO的输入输出
文章目录前言GPIO简介GPIO的命名与数量GPIO的功能STM32F1 GPIO的寄存器库函数开发搭建库函数的工程查看原理图WACK_UP输入按键继电器输出138控制流水灯代码编写库函数简介GPIO输出模式控制继电器通过138控制ledGPIO实现按键输入的操作编写逻辑代码实物效果总结前言上一篇中对整个板子的硬件组成做了一个简单的介绍本文开始进入程序编写的环节首先来搞定最简单的GPIO输入输出控制。GPIO简介GPIO全称叫做通用输入输出接口它是单片机内核、片上外设与外部电路连接的桥梁是单片机与外界进行数据交换的通道。GPIO的命名与数量GPIO的端口号是从PA、PB——PI一共9个端口号每个端口号上有0-15共16个管脚号也就是说STM32最多可以有16*9144个GPIO。但是在实际设计过程中有很多小项目是用不上这么GPIO口的所以厂商会对IO口进行裁剪但是命名规则还是保持不变只是会裁剪一些组别。就拿本次使用STM32F103C8T6来说它的封装一共有48个引脚但是GPIO的数量只有37个还有11个引脚不是GPIO。如下图中的红色框的电源引脚9个、绿色框的复位引脚1个、橙色框的BOOT选择BOOT0 1个这些引脚是已经定死了功能的除此之外还有一部分GPIO如蓝色框的外部时钟引脚以及BOOT1的PB2这些都是GPIO口但是又兼顾着外部时钟输入、BOOT选择的功能一般也不做为GPIO来使用。除了上述这些引脚外还有支持SW下载与JTAG下载调试的GPIO也需要注意在实际使用过程中需要配置后才可正常使用。GPIO的功能了解了GPIO的命名和数量之后接下来就需要知道GPIO有哪些功能。在嵌入式学习笔记——认识STM32的 GPIO口中我们详细分析了STM32F4的GPIO口的结构以及框图知道了GPIO有模拟、复用、通用三种大模式其中模拟功能是专用于处理模拟量的输入复用模式主要是配合各类片上外设完成各种输入或者输出的操作通用模式就是检测输入的高低电平以及控制输出的高低电平。那么STM32F1的GPIO会有区别吗首先从功能来说几乎一致只是在具体的配置过程中需要做一点小小的修改具体的不同之处在寄存器部分介绍。STM32F1 GPIO的寄存器首先来对比一下F4与F1的GPIO寄存器对比二者的中文编程手册可以看出来F1没有F4的端口模式寄存器、端口输出类型寄存器以及端口输出速度寄存器。取而代之的是端口配置低寄存器和端口配置高寄存器。那么具体的使用是怎样的呢在编程手册中查看一下寄存器的具体描述就可以搞清楚了如下图所示原来F1的端口配置寄存器就是将F4中的模式寄存器、输出类型、输出速度寄存器给整合在一起了。每四位控制一个GPIO管脚低两位控制模式与输出速度高两位根据输入或者输出模式进一步细化功能。端口配置低寄存器控制管脚0-7端口配置高寄存器控制管脚8-15。然后就是输入数据寄存器与输出数据寄存器的使用这部分与F4的是一样的这里不再做赘述需要了解的可以去上面的嵌入式笔记链接查看。本文先使用最简单的通用模式完成输入按键的检测以及输出控制LED的操作。库函数开发由于在上一个系列中笔者用的全部是寄存器的开发为了更加完善整个系列这个小项目就使用基础库函数来进行开发库函数开发的优势在于可以减少开发者翻阅手册的时间以提高开发效率但是初学者建议可以去钻研一下寄存器的开发通过最底层的寄存器操作会让自己对单片机的整个结构有一个更加清晰的认识这样对于库函数的使用也有一些帮助如果直接上库函数的话总会有一些小地方会让你不明所以最后搞的自己云里雾里似懂非懂的。搭建库函数的工程这个工程搭建的具体步骤就不做记录了后面看大家的反馈情况如果有需要笔者后面再出一期其实大致流程与之前的F4那个差不多只是需要将库的源文件都导入工程。工程搭建好后编写一个简单的main.c进行验证编译结果显示0errors即可。关于编译出来的一堆东西上图中也为大家做了一个简单的介绍其中Code 和 RO-data之和表示程序占用 Flash 空间的大小RW-data 和ZI-data之和表示运行时占用的 RAM 的大小而我们实际烧录进单片机的数据所占Flash大小则是CodeRO-dataRW-data的大小。上面提到了有关代码段数据段在ZI-data中其实还包含有堆和栈的这部分与C语言中的内存分配有异曲同工想要了解的同学可以看看下面这几篇的介绍1.【IoT】STM32 内存分配详解2.stm32的内存分布3.keil 编译完 Program Size: Code RO-data RW-data ZI-data 的含义4.C语言内存分配—栈区、堆区、全局区、常量区和代码区好了上面这个只是一个拓展小知识感兴趣的可以去了解一下回归正题接下来开始代码的编写。查看原理图WACK_UP输入按键还记得这个板子的按键输入与GPIO输出有哪些吗看一眼原理图首先是按键输入在之前的原理图介绍中提到了这个五方向按键的上下左右用的是ADC采样来实现而WACK_UP按键采用的是普通的按键输入模式。观察原理图可以发现WACK_UP没有按下的时候是低电平当按键按下的时候是高电平因此对应的PA0需要在配置过程中直接配置为浮空输入即可。继电器输出然后来看通用输出模式控制的外设第一个是继电器模块这里的继电器使用了一个NMOS来做下半臂的控制当栅极也就是RELAY没有电压时输出0NMOS不导通继电器线圈不得电常开触点不吸合USB口无输出当RELAY有电压时输出高NMOS导通,继电器线圈的电常开触点吸合USB口有输出。也就说PA12需要配置为通用的推挽输出。138控制流水灯除了继电器之外还有一个电路也是使用到了GOIO的通用推挽输出模式来实现的那就是失败了一半的74HC138译码器控制LED流水灯的电路。138的译码器的管脚介绍如下图所示A0-A2三个脚输入控制Y0-Y7的输出E1、E2为使能脚低电平有效E3也是使能脚高电平有效三个使能脚要同时在有效电平才可以正常工作。其真值表如下可以看到当E1、E2、E3分别为001时随着A0-A2的输入的改变Y0-Y7的输出会做出对应的更改。注意原理图在LED与138之间还有一个芯片叫做74HC245这个芯片这次主要是提高驱动能力由于138本身的驱动能力不强所以加了一个缓冲芯片它的功能表如下OE是使能脚低电平有效DIR是决定输出方向的当DIR为低电平时Bn端是输入An端的输出等于Bn对应口的输入。当DIR为高电平是An端为输入Bn端的输出等于An的输入。具体的芯片描述大家去查看一下芯片手册哈。经过一顿倒腾后LED的点亮输出控制逻辑如下代码编写好了弄清楚了上述模块的原理图后接下来就是编程实现对应功能了。库函数简介当我们拿到库函数后要怎么进行开发呢首先需要搞清楚库函数的结构官方按照各个模块进行了底层的初始化库函数的编写在实际使用过程中直接在对应模块名的.h文件中查找对应的内容即可下面以GPIO为例。打开stm32f10x_gpio.h可以发现整.h文件其实就是三大类内容一类是各种宏定义一类是结构体还有一类是函数的声明。1.宏定义往往是各类具体的配置定义 2.结构体是留给编程人员做配置的接口 3.函数声明则是具体的功能实现。库函数的好处就在于可以减少开发者翻阅手册底层查阅的次数使用结构体、宏定义、函数接口就可以完成对应模块的使用而具体的使用步骤笔者大致总结为如下流程1、查找对应的初始化结构体如上图中的GPIO_InitTypeDef2、根据结构体声明变量并根据实际的使用需求对结构体的各个成员变量进行赋值例如如配置管脚是GPIO_pin_123、赋值完成后需要调用初始化的函数将结构体的参数实际写入到底层寄存器中GPIO_Init4、调用相关的功能函数实现想要的功能GPIO_SetBits。下面就按照上面的步骤来实现一下具体的操作吧。GPIO输出模式控制继电器编程思路根据前面的原理图分析可以知道此处的对应的GPIO是PA12需要通过GPIO输出高低电平来实现继电器的吸合和断开。因此可以知道GPIOA12需要配置为通用推挽输出模式按照上一系列的寄存器编程思路需要去查找对应的寄存器和框图然后对着框图找到编程流程最后参考流程以及寄存器描述来进行编写代码但是在库函数中就不用这么麻烦了根据上面总结的初始化流程1.首先先将GPIO的初始化结构体来过来做个变量定义。GPIO_InitTypeDef GPIO_InitStructure;//定义一个结构体的变量2根据实际所需对结构体的成员进行配置结构体中一共有三个参数分别是引脚号、速度以及模式。这里的结构体变量成员的赋值已经在对应的宏定义组中给出了实际使用时只需要搜索定位到对应的宏定义组找到自己所需的参数即可举个例子吧现在需要初始化GPIOA12号管脚那么结构体的赋值中GPIO_Pin的赋值要怎么给定呢可以看见在成员变量的后面有一个注释“GPIO_pins_define”选中这个宏名然后查找下一个找到对应的管脚宏定义组在组内找到对应的管脚标号即可由于是GPIOA12所以选择GPIO_Pin_12。同样的操作结构体的第二个第三个参数也是如此操作当然对于采用枚举定义参数组可以直接在注释后面的参数上右键跳转能跳转过去的就直接选取参数不能跳转的就使用上面的查找方式来实现。“GPIO_Speed”右键跳转实现选择“GPIO_Speed_2MHz”“GPIO_Mode”右键跳转实现选择“GPIO_Mode_Out_PP”上面的模式选择与输出端口配置寄存器里面的模式是一一对应的关系。将结构体的成员进行如下的初始化。GPIO_InitStructure.GPIO_PinGPIO_Pin_12;//选择对应的引脚号GPIO_InitStructure.GPIO_SpeedGPIO_Speed_2MHz;//配置输出速度GPIO_InitStructure.GPIO_ModeGPIO_Mode_Out_PP;//通用推挽输出3.调用初始化函数将结构体的数据写入底层。第二步还只是将参数赋值给了结构体变量还没有实际生效还需要将结构体的内容实际写入到寄存器中才行因此需要调用下方的函数。在在对应的.c中有具体的描述包括了函数的功能以及上面的结构体指针。GPIO_Init(GPIOA,GPIO_InitStructure);//初始化GPIOA将接固体参数写入寄存器中。其实仔细观察底层的函数操作就可以看到在寄存器编程中常用的位操作本质是一样的都是操作寄存器。4 调用功能函数实现具体的功能在继电器的使用中使用函数来操作GPIO口输出高低电平。实际使用到的函数是如下两个一个是对对应位置位写1另一个是对对应位清除位写0。通过这两个函数就可以实现对指定管脚输出的高低电平控制了。但是完成上面这四步还不能实现控制因为整个过程中还有很重要的一步没有做之前在寄存器编程的阶段提到过所有的片上外设在使用之前必做的一步就是开启时钟这里还没有开启自然是不可以使用那么时钟的开启是不是也应该有对应的库函数呢答案是肯定的时钟对应的是stm32f10x_rcc.h在找对应的功能函数之前还需要找到GPIO所挂接的时钟总线位置在数据手册的框图位置通过下图可看见GPIOA是挂接在AP2上面的因此只需要在stm32f10x_rcc.h种找到APB2相关的初始化函数就可以了。在stm32f10x_rcc.h找到如下函数APB2对应上图中的APB2Periph是外设的意思ClockCmd时钟控制命令。跳转到在stm32f10x_rcc.c中查看具体的函数描述以及其参数的介绍通过上方的函数描述可以知道这个函数的两个形参值该怎么给定。第一个形参选择“RCC_APB2Periph_GPIOA”第二形参选择“ENABLE”然后将上面的这几句代码稍微封装一下变成一个初始化的函数同时将输出高低电平用宏定义修饰一下便于理解和调用。#define Relay_ONGPIO_SetBits(GPIOA,GPIO_Pin_12)//GPIO输出高#define Relay_OFFGPIO_ResetBits(GPIOA,GPIO_Pin_12)//GPIO输出低#define Relay_TUNGPIOA-ODR^(112)//异或操作实现翻转/********************************* 函数名Relay_Init 函数功能继电器初始化 形参void 返回值void 备注 Relay-----PA12--------通用推挽输出 **********************************/voidRelay_Init(void){GPIO_InitTypeDef GPIO_InitStructure;//定义一个结构体的变量RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA,ENABLE);//初始化GPIOA端口的时钟GPIO_InitStructure.GPIO_PinGPIO_Pin_12;GPIO_InitStructure.GPIO_SpeedGPIO_Speed_2MHz;GPIO_InitStructure.GPIO_ModeGPIO_Mode_Out_PP;//通用推挽输出GPIO_Init(GPIOA,GPIO_InitStructure);Relay_OFF;}这样继电器的控制就搞定了在主函数调用初始化然后在需要应用的地方调用宏执行操作即可。通过138控制led至于通过138来控制LED本质与继电器没啥区别这里就不做详细介绍了处理思路和上面一模一样有一点不同之处在于实际的输出控制中笔者使用了位带操作也就是类似51单片机种P0.01输出高P0.10输出低一样的操作这里实际上就是直接用宏定义操作了ODR的指定位置与之前的寄存器操作差异不大。感兴趣的同学可以自己根据宏定义去推导一下。这里给出关键代码#defineADD0PAout(4)// PA4#defineADD1PAout(5)// PA5#defineADD2PAout(6)// PA6#defineENPAout(7)//PA7#defineLED1_ON{ADD01;ADD11;ADD21;EN1;}//111,对应0111 1111#defineLED2_ON{ADD00;ADD11;ADD21;EN1;}//110,对应1011 1111#defineLED3_ON{ADD01;ADD10;ADD21;EN1;}//101对应1101 1111#defineLED4_ON{ADD00;ADD10;ADD21;EN1;}//100对应1110 1111/********************************* 函数名Led_Init 函数功能led灯初始化 形参void 返回值void 备注74HC138译码器的ADD0-ADD1-ADD2-EN ADD0-----PB4 ADD1-----PB5 ADD2-----PA6 EN-------PA7 **********************************/voidLed_Init(void){GPIO_InitTypeDef GPIO_InitStructure;//定义一个结构体的变量RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA,ENABLE);//初始化GPIOA端口的时钟GPIO_InitStructure.GPIO_PinGPIO_Pin_4|GPIO_Pin_5|GPIO_Pin_6|GPIO_Pin_7;GPIO_InitStructure.GPIO_SpeedGPIO_Speed_2MHz;GPIO_InitStructure.GPIO_ModeGPIO_Mode_Out_PP;//通用推挽输出GPIO_Init(GPIOA,GPIO_InitStructure);}GPIO实现按键输入的操作讲完了通用输出再来看看通用输入的使用前面的硬件介绍中知道此时操作的是GPIOA0配置为输入模式不需要上下拉的操作既然遇到了GPIO的初始化那么自然是需要借用上面初始化GPIO的思路只是需要在结构体成员的参数配置上修改即可。由于是输入模式所以结构体中的引脚输出速度这个成员就可以不用配置了。/********************************* 函数名Key_Init 函数功能按键初始化 形参void 返回值void 备注 KEY1wake up-----PA0-----高有效 **********************************/voidKey_Init(void){GPIO_InitTypeDef GPIO_InitStructure;//定义一个结构体的变量RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA,ENABLE);//初始化GPIOA端口的时钟GPIO_InitStructure.GPIO_PinGPIO_Pin_0;//指定初始化的管脚号GPIO_InitStructure.GPIO_ModeGPIO_Mode_IN_FLOATING;//浮空输入GPIO_Init(GPIOA,GPIO_InitStructure);//调用初始化函数写入到寄存器中。}关于获取按键的输入状态自然也是有对应的应用函数根据函数的描述为了在调用过程中更加易读与上面的输出操作一样使用宏定义来优化一下。#defineKEY1GPIO_ReadInputDataBit(GPIOA,GPIO_Pin_0)当然只要使用到按键就有一个避无可避的问题按键消抖的问题这里不在做赘述了不了解的同学可以去我上一个系列进行学习。这里也是直接给出代码/************** 函数名KEY_Scanf 函数功能按键初扫描函数 函数形参void 函数返回值u8 备注 KEY1wake up-----PA0-----高有效 ----返回键值1 **************/u8KEY_Scanf(void){u8 key_value0;//判断按键按下了//标志位staticu8 key_flag0;if(KEY11key_flag0)//判断是否按下了按键{Systick_Delay_ms(10);//消抖if(KEY11){key_flag1;//按下按键就锁上了key_value1;}}if(KEY10key_flag1)//按下了按键之后 是否松手了{key_flag0;//解锁}returnkey_value;}编写逻辑代码三个模块的初始化就完成了接下来就是在主函数中编写逻辑代码进行验证。这里我加了串口和系统滴答来辅助测试这两个东西在后面会介绍到。最终是可以正常检测按键输入以及实现LED、继电器的控制的。实物效果总结关于这个板子最基础的输入输出模块的功能实现就介绍到这文中如有不足之处欢迎大家批评指正。