您现在的位置是:首页 >学无止境 >从零开始手搓一个STM32与机智云的小项目——GPIO的输入输出网站首页学无止境

从零开始手搓一个STM32与机智云的小项目——GPIO的输入输出

小向是个Der 2024-09-27 12:01:03
简介从零开始手搓一个STM32与机智云的小项目——GPIO的输入输出

前言

上一篇中,对整个板子的硬件组成做了一个简单的介绍,本文开始进入程序编写的环节,首先来搞定最简单的GPIO输入输出控制。

GPIO简介

GPIO全称叫做通用输入输出接口,它是单片机内核、片上外设与外部电路连接的桥梁,是单片机与外界进行数据交换的通道。

GPIO的命名与数量

GPIO的端口号是从PA、PB——PI;一共9个端口号,每个端口号上有0-15共16个管脚号,也就是说STM32最多可以有16*9=144个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大小则是Code+RO-data+RW-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没有电压时(输出0),NMOS不导通,继电器线圈不得电,常开触点不吸合,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_InitTypeDef
2、根据结构体声明变量,并根据实际的使用需求对结构体的各个成员变量进行赋值(例如:如:配置管脚是GPIO_pin_12);
3、赋值完成后需要调用初始化的函数,将结构体的参数实际写入到底层寄存器中(GPIO_Init);
4、调用相关的功能函数实现想要的功能(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_Pin = GPIO_Pin_12;//选择对应的引脚号
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_2MHz;//配置输出速度
GPIO_InitStructure.GPIO_Mode =  GPIO_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对应上图中的APB2,Periph是外设的意思,ClockCmd时钟控制命令。
在这里插入图片描述
跳转到在stm32f10x_rcc.c中查看具体的函数描述,以及其参数的介绍:通过上方的函数描述,可以知道这个函数的两个形参值该怎么给定。第一个形参选择 “RCC_APB2Periph_GPIOA”,第二形参选择“ENABLE”
在这里插入图片描述
然后将上面的这几句代码稍微封装一下变成一个初始化的函数,同时将输出高低电平用宏定义修饰一下,便于理解和调用。

#define Relay_ON   GPIO_SetBits(GPIOA,GPIO_Pin_12)//GPIO输出高
#define Relay_OFF  GPIO_ResetBits(GPIOA,GPIO_Pin_12)//GPIO输出低
#define Relay_TUN  GPIOA->ODR ^= (1<<12)//异或操作实现翻转


/*********************************
函数名:Relay_Init
函数功能:继电器初始化
形参:void
返回值:void
备注:
Relay-----PA12--------通用推挽输出
**********************************/
void Relay_Init(void)
{
	GPIO_InitTypeDef  GPIO_InitStructure;//定义一个结构体的变量
	RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA,ENABLE);//初始化GPIOA端口的时钟
	GPIO_InitStructure.GPIO_Pin = GPIO_Pin_12;
	GPIO_InitStructure.GPIO_Speed = GPIO_Speed_2MHz;
	GPIO_InitStructure.GPIO_Mode =  GPIO_Mode_Out_PP;//通用推挽输出
	GPIO_Init(GPIOA,&GPIO_InitStructure );
	
	Relay_OFF;
}

这样继电器的控制就搞定了,在主函数调用初始化,然后在需要应用的地方调用宏执行操作即可。

通过138控制led

至于通过138来控制LED,本质与继电器没啥区别,这里就不做详细介绍了,处理思路和上面一模一样,有一点不同之处在于,实际的输出控制中,笔者使用了位带操作,也就是类似51单片机种P0.0=1输出高,P0.1=0输出低一样的操作,这里实际上就是直接用宏定义操作了ODR的指定位置,与之前的寄存器操作差异不大。感兴趣的同学可以自己根据宏定义去推导一下。
在这里插入图片描述
在这里插入图片描述
这里给出关键代码:


#define ADD0  PAout(4)// PA4
#define ADD1  PAout(5)// PA5
#define ADD2  PAout(6)// PA6
#define EN    PAout(7) //PA7
#define LED1_ON   {ADD0 = 1; ADD1=1;ADD2=1; EN=1;}//111,对应0111 1111 
#define LED2_ON   {ADD0 = 0; ADD1=1;ADD2=1; EN=1;}//110,对应1011 1111 
#define LED3_ON   {ADD0 = 1; ADD1=0;ADD2=1; EN=1;}//101,对应1101 1111
#define LED4_ON   {ADD0 = 0; ADD1=0;ADD2=1; EN=1;}//100,对应1110 1111
/*********************************
函数名:Led_Init
函数功能:led灯初始化
形参:void
返回值:void
备注:74HC138译码器的ADD0-ADD1-ADD2-EN
ADD0-----PB4
ADD1-----PB5
ADD2-----PA6
EN-------PA7
**********************************/
void Led_Init(void)
{
	GPIO_InitTypeDef  GPIO_InitStructure;//定义一个结构体的变量
	RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA ,ENABLE);//初始化GPIOA端口的时钟
	GPIO_InitStructure.GPIO_Pin = GPIO_Pin_4|GPIO_Pin_5|GPIO_Pin_6|GPIO_Pin_7;
	GPIO_InitStructure.GPIO_Speed = GPIO_Speed_2MHz;
	GPIO_InitStructure.GPIO_Mode =  GPIO_Mode_Out_PP;//通用推挽输出
	GPIO_Init(GPIOA,&GPIO_InitStructure );
}

GPIO实现按键输入的操作

讲完了通用输出,再来看看通用输入的使用,前面的硬件介绍中知道,此时操作的是GPIOA0,配置为输入模式,不需要上下拉的操作,既然遇到了GPIO的初始化,那么自然是需要借用上面初始化GPIO的思路,只是需要在结构体成员的参数配置上修改即可。由于是输入模式,所以结构体中的引脚输出速度这个成员就可以不用配置了。

/*********************************
函数名:Key_Init
函数功能:按键初始化
形参:void
返回值:void
备注:
KEY1(wake up)-----PA0-----高有效
**********************************/
void Key_Init(void)
{
	GPIO_InitTypeDef  GPIO_InitStructure;//定义一个结构体的变量
	RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA,ENABLE);//初始化GPIOA端口的时钟
	GPIO_InitStructure.GPIO_Pin = GPIO_Pin_0;//指定初始化的管脚号
	GPIO_InitStructure.GPIO_Mode =  GPIO_Mode_IN_FLOATING;//浮空输入
	GPIO_Init(GPIOA,&GPIO_InitStructure );//调用初始化函数写入到寄存器中。
}

关于获取按键的输入状态,自然也是有对应的应用函数:
在这里插入图片描述
根据函数的描述,为了在调用过程中更加易读,与上面的输出操作一样,使用宏定义来优化一下。

#define KEY1   GPIO_ReadInputDataBit(GPIOA , GPIO_Pin_0)

当然,只要使用到按键,就有一个避无可避的问题,按键消抖的问题,这里不在做赘述了,不了解的同学可以去我上一个系列进行学习。
这里也是直接给出代码:


/**************
函数名:KEY_Scanf
函数功能:按键初扫描函数
函数形参:void
函数返回值:u8
备注:
KEY1(wake up)-----PA0-----高有效 ----返回键值1
**************/
u8 KEY_Scanf(void)
{
	u8 key_value = 0;
	//判断按键按下了
	//标志位
	static u8 key_flag = 0;
	if( KEY1==1 && key_flag==0 )  //判断是否按下了按键
	{
		Systick_Delay_ms(10);//消抖
		if(KEY1==1)
		{
		  key_flag = 1;  //按下按键就锁上了
		  key_value=1;
		}
	}
	if( KEY1==0 && key_flag==1 )  //按下了按键之后 是否松手了
	{
		key_flag = 0;  //解锁
	}
	
	return key_value;
}

编写逻辑代码

三个模块的初始化就完成了,接下来就是在主函数中编写逻辑代码进行验证。这里我加了串口和系统滴答来辅助测试,这两个东西在后面会介绍到。最终是可以正常检测按键输入以及实现LED、继电器的控制的。
在这里插入图片描述

实物效果

在这里插入图片描述

总结

关于这个板子最基础的输入输出模块的功能实现就介绍到这,文中如有不足之处欢迎大家批评指正。

风语者!平时喜欢研究各种技术,目前在从事后端开发工作,热爱生活、热爱工作。