STM32单片机学习(4)

1. GPIO — 按键轮询

1.1 关于按键

  • 前面控制LED灯是让GPIO输出高低电平,而获取按键则是读取GPIO电平,从而获知用户是否按下按键。
  • 按键监测一般有两种:按键扫描和按键中断。按键扫描是间隔很短时间反复查询GPIO状态,从而得知是否有按键动作,这种方式代码简单,但比较耗资源。按键中断而是通过按键产生中断信号,从而实现按键的检测,这种方式需要使用到中断机制,需要对MCU了解深入一点,效果是最好的。
  • 按键一般占用一个GPIO口,通过监测该GPIO的电平变化得知按键操作,典型的电路如图 1 所示。
  • 当所需按键比较多时,则可以采用矩阵按键减少GPIO的占用。矩阵按键需要通过编程扫描等方式实现对多个按键的监控,这里以最简单的独立按键为基础进行介绍。
  • 图 1 按键与 MCU 的连接方式示意图
    在这里插入图片描述
  • 可以看到,在没有按下按键时,电源3.3V通过电阻连接到MCU的PA0脚上,此时MCU读取PA0的电平就是3.3V的高电平。在按键按下时,电源3.3V经过电阻,再经过按键连接到了地,此时PG3连接到接地的一端,
  • 读到的电平就是0V的低电平。由此,MCU就可用过读取对应引脚的电平值,得知按键的变化。
    常用的按键都是机械触点式按键,机械式按键在按下或释放的过程中,由于机械弹性作用的影响,会伴随机械抖动。
  • 图 2 机械按键抖动示意图
    在这里插入图片描述
  • 抖动的时长与机械开关特性相关,一般为5-10ms。在这抖动过程中,会产生多次高低电平,导致被识别为多次按键操作。为了避免机械触点按键检测误判,必须消抖处理。
  • 按键消抖有两种一种是硬件上处理,即在按键旁并联电容,吸收抖动的电平。另一种是软件处理,即通过延时,避开抖动。

1.2 硬件设计

如图 3 所示,是一种常见轻触按键,该按键有四个脚,①和②脚连接,③和④脚连接,按钮按
下后,四脚全相连,实现导通效果。

  • 图 3 轻触按键实物和等效原理图
    在这里插入图片描述
  • 如下开发板有四个用户按键,在开发板左下角,如图 4 所示。为了方便区分,以按键所处位置命名,分别为KEY1_U(up,上)、KEY2_D(down,下)、KEY3_L(left,左)、KEY4_R(right,右)。
  • 图 4 开发板用户按键
    在这里插入图片描述
  • 如下图 5 为开发板用户按键部分的原理图,四个按键除了所接GPIO引脚不同外,其它一模一样。以
    最左边的KEY1为例,E2的TVS二极管用于静电保护,可以看作不存在;C36的电容用于硬件去抖,也可以看作不存在。按键松开时,VDD_3V3经过上拉电阻R25,再经过限流电阻R29到GPIO KEY1(PA0),此时PA0读取电平为高电平;按键按下时,VDD_3V3经过上拉电阻R25,再通过按键接地,此时PA0读取电平为低电平。
  • 由此可知,按键按下,GPIO引脚电平变低,反之为高。四个按键所接GPIO分别为:KEY1(PA0)、KEY2(PG15)、KEY3(PC13)、KEY4(PE3)。
  • 图 5 用户按键原理图
    在这里插入图片描述

1.3 软件设计

1.3.1 软件设计思路

实验目的:本实验通过轮询读方式取GPIO的输入电平判断按键是否按下,并操作对应LED。

  1. 按键初始化:GPIO端口时钟使能、GPIO引脚设置为输入(PA0, PG15, PC13, PE3);
  2. 封装每个按键处理函数:读取按键GPIO状态,操作对应LED灯亮灭;
  3. 主函数轮询按键状态:一直检测是否有按键被按下;

1.3.2 软件设计讲解

  1. GPIO 宏定义与接口宏定义
  • 引脚宏定义(driver_key.h)代码如下:
/*********************
* 按键引脚状态定义
**********************/
#define PUSH_DOWN GPIO_PIN_RESET
#define SPRING_UP GPIO_PIN_SET
/*********************
* 引脚宏定义
**********************/
#define KEY_UP_GPIO_PIN GPIO_PIN_0
#define KEY_UP_GPIO_PORT GPIOA
#define KEY_UP_GPIO_CLK_EN() __HAL_RCC_GPIOA_CLK_ENABLE()

#define KEY_DOWN_GPIO_PIN GPIO_PIN_15
#define KEY_DOWN_GPIO_PORT GPIOG
#define KEY_DOWN_GPIO_CLK_EN() __HAL_RCC_GPIOG_CLK_ENABLE()

#define KEY_LEFT_GPIO_PIN GPIO_PIN_13
#define KEY_LEFT_GPIO_PORT GPIOC
#define KEY_LEFT_GPIO_CLK_EN() __HAL_RCC_GPIOC_CLK_ENABLE()

#define KEY_RIGHT_GPIO_PIN GPIO_PIN_3
#define KEY_RIGHT_GPIO_PORT GPIOE
#define KEY_RIGHT_GPIO_CLK_EN() __HAL_RCC_GPIOE_CLK_ENABLE()

/*********************
* 函数宏定义
**********************/
/*
* 按键状态读取函数宏定义
*/
#define KEY_UP HAL_GPIO_ReadPin(KEY_UP_GPIO_PORT, KEY_UP_GPIO_PIN)
#define KEY_DOWN HAL_GPIO_ReadPin(KEY_DOWN_GPIO_PORT, KEY_DOWN_GPIO_PIN)
#define KEY_LEFT HAL_GPIO_ReadPin(KEY_LEFT_GPIO_PORT, KEY_LEFT_GPIO_PIN)
#define KEY_RIGHT HAL_GPIO_ReadPin(KEY_RIGHT_GPIO_PORT, KEY_RIGHT_GPIO_PIN)
  • 根据硬件设计选定的四个按键的引脚,将其宏定义命名为UP/DOWN/LEFT/RIGHT,且对他们的读取函数进行重命名。其中“HAL_GPIO_ReadPin()”原型“GPIO_PinState HAL_GPIO_ReadPin(GPIO_TypeDef *GPIOx, uint16_t GPIO_Pin)”,参数依次是:引脚组,引脚号,返回的是0(低电平)或1(高电平)。
  1. GPIO 初始化
    按键初始化(driver_key.c)代码如下:
/*
* 函数名:void KeyInit(void)
* 输入参数:无
* 输出参数:无
* 返回值:无
* 函数作用:初始化按键的引脚,配置为输入
*/
void KeyInit(void)
{
		// 定义 GPIO 的结构体变量
		GPIO_InitTypeDef GPIO_InitStruct = {0};
		// 使能按键的 GPIO 对应的时钟
		KEY_UP_GPIO_CLK_EN();
		KEY_DOWN_GPIO_CLK_EN();
		KEY_LEFT_GPIO_CLK_EN();
		KEY_RIGHT_GPIO_CLK_EN();
		
		GPIO_InitStruct.Mode = GPIO_MODE_INPUT; // 设置为输入模式
		GPIO_InitStruct.Pull = GPIO_PULLUP; // 默认上拉
		GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_HIGH; // 引脚反转速度设置为快
		
		// 初始化'Up'键引脚配置
		GPIO_InitStruct.Pin = KEY_UP_GPIO_PIN; // 选择按键的引脚
		HAL_GPIO_Init(KEY_UP_GPIO_PORT, &GPIO_InitStruct);
		// 初始化'Down'键引脚配置
		GPIO_InitStruct.Pin = KEY_DOWN_GPIO_PIN; // 选择按键的引脚
		HAL_GPIO_Init(KEY_DOWN_GPIO_PORT, &GPIO_InitStruct);
		// 初始化 Left'键引脚配置
		GPIO_InitStruct.Pin = KEY_LEFT_GPIO_PIN; // 选择按键的引脚
		HAL_GPIO_Init(KEY_LEFT_GPIO_PORT, &GPIO_InitStruct);
		// 初始化'Right'键引脚配置
		GPIO_InitStruct.Pin = KEY_RIGHT_GPIO_PIN; // 选择按键的引脚
		HAL_GPIO_Init(KEY_RIGHT_GPIO_PORT, &GPIO_InitStruct);
}
  • 将引脚初始化为上拉输入,此处使用了一个小技巧,因为各个按键的除了引脚号不同之外其余参数都是一致的,所以将GPIO结构体除引脚号外的参数只赋值一遍,最后只改变引脚号的那个成员参数的值进行初始化就可以了,不需要每个按键都将所有的成员参数重新赋值一遍,简化了代码量。
  1. 按键读取 按键读取 函数因为四个按键这个函数的处理都几乎一致,所以此处只对KEY1,即UP键进行具体举例。
1 /*
2 * 函数名:void UpKeyPolling(void)
3 * 输入参数:无
4 * 输出参数:无
5 * 返回值:无
6 * 函数作用:使用轮询方式查询向上键是否按下,通过按下控制三色灯绿灯亮灭
7 */
8 static bool up_flag = false;
9 void UpKeyPolling(void)
10 {
11  	if(KEY_UP == PUSH_DOWN) // 如果检测到向上键被按下
12  	{
13 	    	HAL_Delay(8); //延时 8ms 防按键抖动
14   		if(KEY_UP == PUSH_DOWN) // 如果防抖动后向上键依然是处于被按下的状态,就认为向上键被按下过
15  		{
16  		up_flag = !up_flag; // 用一个标志位来判断向上键被按下次数,按下一次绿灯亮,再按一次绿灯灭,如此反复
17  		RLED(OFF);
18  		GLED(up_flag?OFF:ON);
19  		BLED(OFF);
20  		}
21  	}
22 }

 8行:定义了一个全局变量标志位“up_flag”,作为UP键被按下的标志;
 11行:获取该按键状态;
 13行:延时5-10ms,软件去抖;
 14行:再次获取该按键状态,此时依旧按下,说明是正常按键操作,非抖动;
 16行:将标志位置反,按键按一次置反一次(即0->1->0->1这样循环);
 17行:熄灭红色LED灯;
 18行:根据标志位“up_flag”的值,让绿色LED灯亮或灭;
 19行:熄灭蓝色LED灯;

  • 此时每按下一次UP键,绿色LED灯将亮灭交替。剩下的三个按键的效果分别是:DOWN->三个灯同时亮灭;LEFT->红灯亮灭;RIGHT->蓝灯亮灭。
  1. 主函数测试
  • 主函数控制逻辑(main.c)代码如下:
1 	// 初始化 LED
2 	LedGpioInit();
3 	// 初始化按键
4 	KeyInit();
5
6 	while(1)
7 	{
8		// 轮询向上键
9		UpKeyPolling();
10		// 轮询向下键
11		DownKeyPolling();
12		// 轮询向左键
13		LeftKeyPolling();
14		// 轮询向右键
15		RightKeyPolling();
16	}

 1~4行:初始化LED灯和按键;
 6~16行:一直循环查询每个按键当前状态,从而判断对应按键是否按下;

2. GPIO — 按键中断

2.1 关于STM32 的 EXTI

前面”重点—中断系统”介绍了STM32的中断和中断优先级,知道了所有外设中断都由NVIC管理,比如USART、ADC、I2C、SPI等。GPIO产生的中断也不例外,但在给NVIC管理之前,还有一
个EXTI(External interrupt/event controller,外部中断/事件控制器)先处理一下,如图 1 所示
图 1 STM32 中断处理机制
在这里插入图片描述

  • STM32F103系列的EXTI支持19个外部中断/事件请求(互联型系列的STM32支持20个),每个中断/事件都有独立的触发和屏蔽设置,支持中断模式和事件模式。
  • 中断模式是指外部信号产生电平变化时,EXTI将该信号给NVIC处理,从而触发中断,执行中断服务函数,完成对应操作。
  • 事件模式是指外部信号产生电平变化时,EXTI根据配置,联动ADC或TIM执行相关操作。
  • 中断和事件的产生源是一样的,中断需要软件实现相应功能,而事件是由硬件触发后执行相应操作。前者需要CPU参与功能实现,可以实现的功能更多,后者无需CPU参与,具有更高的响应速度。
  • EXTI的结构如图 2 所示,图中画斜线“/”的信号线表示这样的线共有19根。外部信号输入后,首
    先经过边缘检测电路,可以实现对上升沿或下降沿信号进行检测,从而得到硬件触发,也可由软件中断事件寄存器产生软件触发信号。无论是硬件触发还是软件触发,如果中断屏蔽寄存器允许,则产生中断给NVIC处理(绿色路线);如果事件屏蔽寄存器允许,则产生事件,脉冲发生器产生脉冲供其它模块使用(黄色路线)。
  • STM32F103的GPIO挂载APB总线上,如果要使用GPIO引脚作为外部中断/事件功能,则必须使能APB总线上该引脚对应端口的时钟和AFIO复用功能。
  • 图 2 STM32F103 系列 EXTI 结构框图
    在这里插入图片描述
  • STM32F103ZET6有7组GPIO,每组16个引脚,即112个GPIO引脚,但EXTI只支持19个外部中断/事件请求,因此需要将多个GPIO合成一组,共用一个中断线,STM32F103系列中断线分组如表 1 所示。
  • 表 1 EXIT 中断线分组
中断线描述
EXIT0PA0~PG0,7 个 GPIO 共享该中断线
EXIT1PA1~PG1,7 个 GPIO 共享该中断线
EXIT2PA2~PG2,7 个 GPIO 共享该中断线
EXIT3PA3~PG3,7 个 GPIO 共享该中断线
EXIT4PA4~PG4,7 个 GPIO 共享该中断线
EXIT5PA5~PG5,7 个 GPIO 共享该中断线
EXIT6PA6~PG6,7 个 GPIO 共享该中断线
EXIT7PA7~PG7,7 个 GPIO 共享该中断线
EXIT8PA8~PG8,7 个 GPIO 共享该中断线
EXIT9PA9~PG9,7 个 GPIO 共享该中断线
EXIT10PA10~PG10,7 个 GPIO 共享该中断线
EXIT11PA11~PG11,7 个 GPIO 共享该中断线
EXIT12PA12~PG12,7 个 GPIO 共享该中断线
EXIT13PA13~PG13,7 个 GPIO 共享该中断线
EXIT 14PA14~PG14,7 个 GPIO 共享该中断线
EXIT 15PA15~PG15,7 个 GPIO 共享该中断线
EXIT16PVD 输出
EXIT17RTC 闹钟事件
EXIT18USB 唤醒事件
EXIT19以太网唤醒事件(仅互联型支持)
  • 结合图 1 所示,EXTI0EXTI15作为GPIO中断线使用,同组的GPIO共享一条中断线,比如EXTI0组,PA0作为了中断源,则此时PB0PG0不能作为中断源。
  • 【总结】
  1. STM32有众多异常和中断,其中内部中断源(USART、ADC等)直接由NVIC处理。GPIO引脚可以产生外部中断或事件,如是中断则交由NVIC处理,如果是事件则产生脉冲信号联动其它模块工作。
  2. 无论是内部中断源,还是GPIO产生的中断,都由NVIC管理分组,然后根据中断优先级分组确
    定抢占优先级级数和子优先级级数。
  3. GPIO引脚众多,将引脚数字相同的作为一组,共享一个中断线。

2.2 硬件设计

同“1. 2 硬件设计”小结内容。

2.3 软件设计

2.3.1 软件设计思路

实验目的:本实验通过使用外部中断功能去判断按键的状态,通过中断的形式能够更加灵敏的读取到GPIO的电平,让用户更加直观的感受到STM32F103的中断,并学会如何使用和开发其中断功能。

  1. 按键初始化:GPIO端口时钟使能、AFIO复用功能时钟使能、GPIO引脚设置为下降沿触发中断(PA0,PG15, PC13, PE3);
  2. 填充每个按键中断处理函数:读取按键GPIO状态,操作对应LED灯亮灭;
  3. 主函数调用LED和按键初始化后,无需任何操作;

2.3.2 软件设计讲解

  1. GPIO 宏定义与接口宏定义
    引脚宏定义(driver_key.h)代码如下:
1 /*********************
2 * 按键引脚状态定义
3 **********************/
4 #define PUSH_DOWN GPIO_PIN_RESET
5 #define SPRING_UP GPIO_PIN_SET
6 
7 /*********************
8 * 引脚宏定义
9 **********************/
10 #define KEY_UP_GPIO_PIN GPIO_PIN_0
11 #define KEY_UP_GPIO_PORT GPIOA
12 #define KEY_UP_GPIO_CLK_EN() __HAL_RCC_GPIOA_CLK_ENABLE()
13
14 #define KEY_DOWN_GPIO_PIN GPIO_PIN_15
15 #define KEY_DOWN_GPIO_PORT GPIOG
16 #define KEY_DOWN_GPIO_CLK_EN() __HAL_RCC_GPIOG_CLK_ENABLE()
17
18 #define KEY_LEFT_GPIO_PIN GPIO_PIN_13
19 #define KEY_LEFT_GPIO_PORT GPIOC
20 #define KEY_LEFT_GPIO_CLK_EN() __HAL_RCC_GPIOC_CLK_ENABLE()
21 
22 #define KEY_RIGHT_GPIO_PIN GPIO_PIN_3
23 #define KEY_RIGHT_GPIO_PORT GPIOE
24 #define KEY_RIGHT_GPIO_CLK_EN() __HAL_RCC_GPIOE_CLK_ENABLE()
25 
26 /*********************
27 * 函数宏定义
28 **********************/
29 /*
30 * 按键状态读取函数宏定义
31 */
32 #define KEY_UP HAL_GPIO_ReadPin(KEY_UP_GPIO_PORT, KEY_UP_GPIO_PIN)
33 #define KEY_DOWN HAL_GPIO_ReadPin(KEY_DOWN_GPIO_PORT, KEY_DOWN_GPIO_PIN)
34 #define KEY_LEFT HAL_GPIO_ReadPin(KEY_LEFT_GPIO_PORT, KEY_LEFT_GPIO_PIN)
35 #define KEY_RIGHT HAL_GPIO_ReadPin(KEY_RIGHT_GPIO_PORT, KEY_RIGHT_GPIO_PIN)
  1. GPIO 初始化
    按键初始化(driver_key.c)代码如下:
1 	/*
2 	* 函数名:void KeyInit(void)
3 	* 输入参数:无
4	* 输出参数:无
5 	* 返回值:无
6 	* 函数作用:初始化按键的引脚,配置为下降沿触发外部中断
7 	*/
8 	void KeyInit(void)
9 	{
10 		// 定义 GPIO 的结构体变量
11 		GPIO_InitTypeDef GPIO_InitStruct = {0};
12 		// 使能按键的 GPIO 对应的时钟
13		KEY_UP_GPIO_CLK_EN();
14		KEY_DOWN_GPIO_CLK_EN();
15		KEY_LEFT_GPIO_CLK_EN();
16		KEY_RIGHT_GPIO_CLK_EN();
17		
18		GPIO_InitStruct.Mode = GPIO_MODE_IT_FALLING; // 设置为下降沿触发外部中断
19		GPIO_InitStruct.Pull = GPIO_PULLUP; // 默认上拉
20		GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_HIGH; // 引脚反转速度设置为快
21	
22		// 初始化'Up'键引脚配置
23		GPIO_InitStruct.Pin = KEY_UP_GPIO_PIN; // 选择按键的引脚
24		HAL_GPIO_Init(KEY_UP_GPIO_PORT, &GPIO_InitStruct);
25		// 初始化'Down'键引脚配置
26		GPIO_InitStruct.Pin = KEY_DOWN_GPIO_PIN; // 选择按键的引脚
27		HAL_GPIO_Init(KEY_DOWN_GPIO_PORT, &GPIO_InitStruct);
28		// 初始化 Left'键引脚配置
29		GPIO_InitStruct.Pin = KEY_LEFT_GPIO_PIN; // 选择按键的引脚
30		HAL_GPIO_Init(KEY_LEFT_GPIO_PORT, &GPIO_InitStruct);
31		// 初始化'Right'键引脚配置
32		GPIO_InitStruct.Pin = KEY_RIGHT_GPIO_PIN; // 选择按键的引脚
33		HAL_GPIO_Init(KEY_RIGHT_GPIO_PORT, &GPIO_InitStruct);
34		
35		/* EXTI interrupt init*/
36		HAL_NVIC_SetPriority(EXTI0_IRQn, 0, 0);
37		HAL_NVIC_EnableIRQ(EXTI0_IRQn);
38	
39		HAL_NVIC_SetPriority(EXTI3_IRQn, 0, 0);
40		HAL_NVIC_EnableIRQ(EXTI3_IRQn);
41	
42		HAL_NVIC_SetPriority(EXTI15_10_IRQn, 0, 0);
43		HAL_NVIC_EnableIRQ(EXTI15_10_IRQn);
44  }

 12~16行:使能按键对应GPIO端口时钟;
 18行:设置为下降沿触发外部中断,即按键按下瞬间触发中断。可根据需求设置为上升沿触发,即松开按键触发中断,双边缘触发,即按下松开都触发中断;
 22~33行:初始化每个按键对应的GPIO,“HAL_GPIO_Init()”里会判断该引脚是否为EXTI模式,如果是则调用“__HAL_RCC_AFIO_CLK_ENABLE()”使能AFIO时钟;
 36~37行:设置EXTI中断线0的优先级和使能,对应PA0;
 39~40行:设置EXTI中断线3的优先级和使能,对应PE3;
 42-43行:设置EXTI中断线10~15的优先级和使能,对应PC13、PG15;
3) 中断处理函数

  • 当中断发生后,则自动跳到代码段所对应的中断函数所在位置,执行中断函数的内容,因此我们需要编写中断函数的内容。
  • 此处有四个按键,理论上也就应该有4个中断处理函数。但在HAL库中,EXTI0-4这五个中断是各自独立的中断服务函数,EXTI5-9共用一个中断服务函数,EXTI10~15共用一个中断服务函数。
  • 按键中断处理函数(driver_key.c)代码如下:
	/*
		* 函数名:void EXTI0_IRQHandler(void)
		* 输入参数:无
		* 输出参数:无
		* 返回值:无
		* 函数作用:外部中断 0 的中断处理函数
	*/
	void EXTI0_IRQHandler(void)
	{
		HAL_GPIO_EXTI_IRQHandler(KEY_UP_GPIO_PIN);
	}
	
	/*
		* 函数名:void EXTI3_IRQHandler(void)
		* 输入参数:无
		* * 输出参数:无
		* 返回值:无
		* 函数作用:外部中断 3 的中断处理函数
	*/
	void EXTI3_IRQHandler(void)
	{
		HAL_GPIO_EXTI_IRQHandler(KEY_RIGHT_GPIO_PIN);
	}
	
	/*
		* 函数名:void EXTI15_10_IRQHandler(void)
		* 输入参数:无
		* 输出参数:无
		* 返回值:无
		* 函数作用:外部中断 10-15 的中断处理函数
	*/
	void EXTI15_10_IRQHandler(void)
	{
		HAL_GPIO_EXTI_IRQHandler(KEY_DOWN_GPIO_PIN);
		HAL_GPIO_EXTI_IRQHandler(KEY_LEFT_GPIO_PIN);
	}
  • 注意这里的中断处理函数的实现,可以放在“stm32f1xx_it.c”中,同时注意在“stm32f1xx_it.h”中声明。
  • 每个中断处理函数里,都调用的“HAL_GPIO_EXTI_IRQHandler()”准备后续处理,传入参数为外部中断的引脚号,该函数原型代码如下:
    外部中断处理函数(stm32f1xx_hal_gpio.c)代码:
	/**
		* @brief This function handles EXTI interrupt request.
		* @param GPIO_Pin: Specifies the pins connected EXTI line
		* @retval None
	*/
	void HAL_GPIO_EXTI_IRQHandler(uint16_t GPIO_Pin)
	{
		/* EXTI line interrupt detected */
		if (__HAL_GPIO_EXTI_GET_IT(GPIO_Pin) != 0x00u)
		{
			__HAL_GPIO_EXTI_CLEAR_IT(GPIO_Pin);
			HAL_GPIO_EXTI_Callback(GPIO_Pin);
		}
	}
  • 该函数先判断传入引脚是否产生了外部中断,确认该引脚产生了中断,则清理中断标志,再调用
    “HAL_GPIO_EXTI_Callback()”回调函数。在该回调函数,通过判断输入的引脚,完成对应的用户操作。
  • 外部中断处理函数回调函数(driver_key.c)代码如下:
1	/*
2	* 函数名:void HAL_GPIO_EXTI_Callback(void)
3	* 输入参数:无
4	* 输出参数:无
5	* 返回值:无
6	* 函数作用:外部中断处理函数的回调函数,用以处理不同引脚触发的中断服务最终函数
7	*/
8	static volatile bool up_flag = false; // 定义一个全局静态标志,用以判断按键按下的次数,上下左右键类似
9	static volatile bool down_flag = false;
10	static volatile bool left_flag = false;
11	static volatile bool right_flag = false;
12	void HAL_GPIO_EXTI_Callback(uint16_t GPIO_Pin)
13	{
14			switch(GPIO_Pin) // 判断是哪个按键
15			{
16				case KEY_UP_GPIO_PIN: // 如果是上键
17				{
18					up_flag = !up_flag; // 按下一次标志翻转一次
19					RLED(OFF);
20					GLED(up_flag?OFF:ON); // 根据标志控制绿灯的亮灭
21					BLED(OFF);
22					break;
23				}
24				case KEY_DOWN_GPIO_PIN: // 如果是下键
25				{
26					down_flag = !down_flag; // 按下一次标志翻转一次
27					RLED(down_flag?OFF:ON); // 三色灯全灭或全亮
28					GLED(down_flag?OFF:ON);
29					BLED(down_flag?OFF:ON);
30					break;
31				}
32				case KEY_LEFT_GPIO_PIN: // 如果是左键
33				{
34					left_flag = !left_flag; // 按下一次标志翻转一次
35					RLED(left_flag?OFF:ON); // 根据标志控制红灯的亮灭
36					GLED(OFF);
37					BLED(OFF);
38					break;
39				}
40				case KEY_RIGHT_GPIO_PIN: // 如果是右键
41				{
42					right_flag = !right_flag; // 按下一次标志翻转一次
43					RLED(OFF); // 根据标志控制蓝灯的亮灭
44					GLED(OFF);
45					BLED(right_flag?OFF:ON);
46				break;
47			}
48			default:break;
49		}
50	}

 9~11行:定义了全局变量标志位,用于记录按键按下状态;
 14~49行:根据传入的引脚号,得知是哪一个按键按下,从而控制对应LED灯亮灭;
4) 主函数 主函数 测试

  • 按键中断主函数(main.h)代码如下:
	// 初始化按键
	KeyInit();
	
	//初始化 LED
	LedGpioInit();
	
	while(1)
	{
	}
  • 主函数只需初始化LED和按键,无需任何操作。一旦按键按下产生中断,将自动跳转到对应中断向量位置,调用该位置的中断处理函数。读者需要自行填充中断处理函数内容,这里设置中断处理函数最终都调用外部中断回调函数“HAL_GPIO_EXTI_Callback()”,在该函数里判断是哪个引脚按下,执行相应操作。

3. GPIO — 蜂鸣器

3.1 关于蜂鸣器

  • 蜂鸣器是一种常见的电子发声元器件,采用直流电压供电,广泛应用于计算机、打印机、报警器、电子玩具、汽车电子设备等产品中,常见的蜂鸣器可分为有源蜂鸣器和无源蜂鸣器。
  • 有源蜂鸣器:内部有震荡源,只要通电即可自动发出固定频率的声音。
  • 无源蜂鸣器:内部无震荡源,需要外部脉冲信号驱动发声,声音频率可变。
  • 两种蜂鸣器实物如图 1 所示,从外观上来看两种蜂鸣器形状相似,有源蜂鸣器底部有黑胶密封,无源蜂鸣器底部可看到电路板(图中绿色部分),两种蜂鸣器都标注有正负极。
  • 图 1 有源蜂鸣器(左)和无源蜂鸣器(右)
    在这里插入图片描述

3.2 硬件设计

  • 通常蜂鸣器需要的驱动电流比较大,使用GPIO直接驱动蜂鸣器比较吃力,通常使用如图 2 所示电路。由GPIO控制三极管通断,蜂鸣器由系统电源驱动,MCU只需要很小的电流便可控制蜂鸣器。
  • Q1为NPN三极管,发射极(E)接地,基极(B)接单片机引脚PA8,集电极(C)接蜂鸣器到电源。
  • 当PA8输出低电平,U BE <U on ,三极管断开,此时蜂鸣器不工作。
  • 当PA8输出高电平,U BE >U on ,三极管导通,此时蜂鸣器工作。
  • 图 2 蜂鸣器原理图
    在这里插入图片描述

3.3 软件设计

3.3.1 软件设计思路

实验目的:本实验通过控制GPIO输出指定频率,驱动蜂鸣器发声。

  1. 引脚初始化:GPIO端口时钟使能、GPIO引脚设置为输出(PA8);
  2. 封装蜂鸣器函数:其中方波周期的延时,使用HAL库延时函数HAL_Delay()实现;
  3. 主函数改变方波的延时时间(即改变其周期),控制蜂鸣器声音;
    本实验配套代码位于“5_程序源码\7_GPIO—蜂鸣器\”。

3.3.2 软件设计讲解

  1. GPIO 选择与接口定义
  • 蜂鸣器引脚宏定义(driver_buzzer.h)代码如下:
/*********************
* 引脚宏定义
**********************/
#define Buzzer_GPIO_PIN GPIO_PIN_8
#define Buzzer_GPIO_PORT GPIOA
#define Buzzer_GPIO_CLK_EN() __HAL_RCC_GPIOA_CLK_ENABLE()

/*********************
* 函数宏定义
**********************/
#define Buzzer(flag) HAL_GPIO_WritePin(Buzzer_GPIO_PORT, Buzzer_GPIO_PIN, flag?GPIO_PIN_SET:GPIO_PIN_RESET)
  • 根据原理图可知蜂鸣器的控制引脚是PA8,因此引脚组和引脚号分别对应GPIOG、GPIO_PIN_8,引脚时钟使能函数是__HAL_RCC_GPIOA_CLK_ENABLE()。对其输出函数宏定义成接口Buzzer(flag),flag为1时输出高电平,三极管导通,flag为0时输出低电平,三极管不导通。
  1. GPIO 初始化
  • 蜂鸣器引脚初始化(driver_buzzer.c)代码如下:
/*
* 函数名:void BuzzerInit(void)
* 输入参数:无
* 输出参数:无
* 返回值:无
* 函数作用:初始化蜂鸣器的引脚
*/
void BuzzerInit(void)
{
// 定义 GPIO 的结构体变量
GPIO_InitTypeDef GPIO_InitStruct = {0};
// 使能蜂鸣器的 GPIO 对应的时钟
Buzzer_GPIO_CLK_EN();

GPIO_InitStruct.Pin = Buzzer_GPIO_PIN; // 选择蜂鸣器的引脚
GPIO_InitStruct.Mode = GPIO_MODE_OUTPUT_PP; // 设置为推挽输出模式
GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_HIGH; // 引脚反转速度设置为快

// 初始化引脚配置
HAL_GPIO_Init(Buzzer_GPIO_PORT, &GPIO_InitStruct);

// 关闭蜂鸣器
Buzzer(0);
}
  1. 方波输出函数
  • 方波输出函数(driver_buzzer.c)代码如下:
/*
* 函数名:void ControlBuzzerRang(uint16_t period)
* 输入参数:period-蜂鸣器发出响声的周期,f=1/(2*period)
* 输出参数:无
* 返回值:无
* 函数作用:输出方波
*/
void ControlBuzzerRang(uint16_t period)
{
Buzzer(1);
HAL_Delay(period);
Buzzer(0);
HAL_Delay(period);
}
  • 通过HAL_Delay()延时函数来实现方波周期的变换,从而控制PA8输出方波的频率来改变蜂鸣器发声的响度。
  1. 主函数 主函数 测试
  • 蜂鸣器主函数(main.c)代码如下:
// 初始化蜂鸣器
BuzzerInit();
while(1)
{
for(i=0; i<10; i++)
{
ControlBuzzerRang(i); // 蜂鸣器响 i ms,然后不响 i ms,构成一个 1/(2*i)kHz 频率的蜂鸣器响应
}
}
  • 首先对蜂鸣器的引脚进行初始化;然后通过一个for()循环改变局部变量i的值使其从0每次递增1至10,而这个i就是方波输出周期的一半。通过改变局部变量i来改变方波的周期,从而改变蜂鸣器的音调。
  • 蜂鸣器实验本质和LED实验一样,生活中很多模块也一样,只需要控制GPIO即可控制模块,实现相应效果。

4. 关于通信

4.1 串行/并行通信

  • 按照数据传输的方式,通信可以分为串行通信和并行通信。串行通信简单的说就是数据依次传输,比如要传输0x11111111,一位一位的发送,需要发送8次。并行通信则是几个数据一起传输,同样是0x11111111,如果8位一起发送,只需要发送1次,如图 1 所示。

  • 图 1 串行/并行传输示意图在这里插入图片描述

  • 由此可见,串行传输占用的通信线更少,成本低,通信速度相对较慢;并行传输占用的通信线多,成本高,通信速度相对更快。

  • 但随着对传输速度要求越来越高,并行传输开始出现信号之间的干扰,串行通信受干扰影响较小,之后又发展出差分传输等技术,极大的提高了串行传输速率,使得串行通信速度可能比并行通信速度更快。

  • 串行通信就像单车道,行驶的车辆需要依次行驶。

  • 并行通信就像多车道,同时多辆汽车并排行驶。但当车速很快的时候,多车道上并列行驶的汽车之间会形成“气流”相互干扰,单车道则受影响较小,速度能够进一步提升。

4.2 全双工/ 半双工/ 单工传输

  • 按照数据传输的方向,通信可以分为全双工、半双工和单工。全双工指双方都可以同时收发信息;
  • 半双工双方都可以收发信息,但同一时刻只能一方发送信息;单工指只能一方发信息,一方接受信息,通信是单向的。
  • 全双工就像电话通信,双方任意时刻都可以同时收发信息;半双工就像对讲机通信,双向都可得到信息,但是同一时刻只能是一方发射另一方接收,发射和接收不能同时进行;单工就像收音机,只能由广播站发送给收音机,单向不可逆的。
  • 图 2 全双工/半双工/单工示意图
    在这里插入图片描述

4.3 同步/异步通信

  • 按数据同步的方式,通信可以分为同步通信和异步通信。数据在双方之间传输时,需要制定规则保证数据传输的准确。
  • 同步通信的做法是加一个时钟信号,发送方和接收方在这个时钟的节拍下传输数据,比如常
    见的SPI、I 2 C。
    而异步通信的做法是对数据进行封装,在数据开头加上起始信号,在数据结尾加上终止信号,
    双方就按这个规则传输数据,比如UART、1-Wire。因此,可以通过是否有时钟信号,初步判断是何种数据同步方式。

4.4通信速率

  • 对于同步通信,通信速率由时钟信号决定,时钟信号越快,传输速度就越快。
  • 对于异步通信,需要收发双方提前统一通信速率,这也就是我们串口调试时,波特率不对显示乱码的原因。
  • 时钟对通信的重要性,这里再举个例子:假设发送端时钟频率为1Mhz,对应时钟周期则为1us, 接收端时钟频率为10Mhz,对应时钟周期则为0.1us。现在发送端发送一个数据0x1,就会产生一个持续时间为1us高电平,接收端接收到这个1us的高电平时,会当作是10个0.1us的高电平,认为收到了10个0x1。这时双方数据就乱套了,因此需要一个统一的时钟标准。
  • 通常使用比特率来描述通信速率的快慢,与之容易混淆的是波特率。
  1. 比特率(Bitrate):系统在单位时间内传输的比特位(二进制0或1)个数,通常用R b 表示,单位是
    比特/秒(bit/s),缩写为bps;
  2. 波特率(Baudrate):系统在单位时间内传输的码元个数,通常用R B 表示,单位是波特(Bd);
  • 100bit/s即是一秒钟传输100个0或1,100Bd即是一秒钟传输100个码元。
  • 码元就是“承载信息量的基本信号单位”,以一条电线上传输的信号为例,码元就是电线上的电平值。
  • 如果电线上电平只有0和3.3V两种选择,传输的信号是这2种电平之一,码元的状态只有2种。接收方可以把0V认为是二进制的0,把3.3V认为是二进制1。即:传输1个码元时,能用来表示1位数据。
  • 如果电线上电平有0V、3.3V、5V、12V四种选择,传输的信号是这4种电平之一,码元的状态有4种。接收方可以把这4个电平认为是二级制的4个值:00、01、10、11。即:传输1个码元时,能用来表示2位数据。
  • 因此码元状态为2时,比特率等于波特率,码元状态越多,每次传输的码元能携带的信息越多,自然速率也越高。
  • 码元有N个状态时,比特率与波特率的关系式:
Rb = RB × 𝑙og 2^N

4.5 常见通信协议

  • 在嵌入式中,有众多通信协议,往往从性能、成本、稳定性、易用性等角度考虑选择合适的协议。常见的通信协议如表 1 所示。
  • 表 1 常见通信协议列表
通信接口串行/ 并行传输方向同步/ 异步电平标准主从组成
TTL串行全双工异步逻辑电平一主一从
RS232串行全双工异步逻辑电平一主一从
RS485串行半双工异步差分信号一主多从
USART串行全双工同步逻辑电平一主一从
I2C串行半双工同步逻辑电平一主多从
SPI串行全双工同步逻辑电平一主多从
CAN串行半双工异步差分信号多主多从
1-Wire串行半双工异步逻辑电平一主多从
USB2.0串行半双工异步差分信号一主多从
  • 1
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值