任务 2.1 LED 流水灯的应用开发 2.1.1 任务分析 设计一个任务 LED 流水灯系统,具体要求如下。 当系统通电时,两个 LED 以 2s 为周期(亮 1s,灭 1s)交替闪烁,逐渐缩短周期(每次交付) 减 0.1s),直到周期变成 0.1s 之后,再恢复 2s,并循环往复。 分析本任务的要求,如果需要控制的话 LED 在一定的周期内闪烁,开发者必须在应用程序中添加延迟 时功能。在 STM32 延迟操作常用于应用开发过程中,如控制 LED 亮 1s 后熄灭,每隔一次 2s 收集一次环境温度和湿度值。在实际应用中,编程延迟通常存在 3 种方法。 一是用软件实现延时,这种方法通过让 MCU 实现空指令的缺点是延迟不准确。 二是通过控制使用定时器实现延迟。 MCU 实现内部定时器外设的缺点是需要占用 定时器资源的优点是延迟准确。 三是用 MCU 内核的 SysTick 实现延时,这种方法通过控制 SysTick 实现倒计数不需要 占用额外的定时器资源,实现精确延迟。 综上所述,使用 SysTick 实现延迟的方法有一定的优势,所以在实际应用中一般选择这种方法 实现最基本的延迟操作方法。 本任务涉及的知识点包括: ? STM32F4 系列微控制器 SysTick 工作原理及编程配置方法; ? STM32F4 通用系列微控制器 I/O 工作原理及编程配置方法。 2.1.2 知识链接 1.使用 SysTick 实现延时 SysTick 是 Cortex-M4 内核的外设,所以不单是 ST 公司的 STM32F4 内部有系列微控制器 SysTick,只要使用 Cortex-M4 内核的微控制器内部都有 SysTick。SysTick 是一 个 24 当计数到位时,倒计数定时器 0 时,SysTick 从重载值寄存器(STK_LOAD)中自动 重新加载计数初值。只要不是 SysTick 控制和状态寄存器(STK_CTRL)清除中间的使能位, 它可以持续运行。 STM32F4 微控制器系列 SysTick 时钟源来自 AHB,我们可以将其配置为AHB 时钟频率 的 或直接使用1/8AHB 前者通常用于实际应用,如图所示 2-1-1 中的阴影部 分所示。 接下来介绍和 SysTick 编程配置相关 4 寄存器:控制和状态寄存器、重载值寄存器 前值寄存器和校准值寄存器。 控制和状态寄存器(SysTick Control And Status Register,STK_CTRL)位段定义与功能 描述见表 2-1-1。
2.STM32F4 微控制器系列 GPIO 及其工作模式 通用 I/O 全名为通用输入/输出(General Purpose Input Output,GPIO)。微控制器的 GPIO 引脚的数量随着电影中资源的变化而变化。微控制器通过 GPIO 外设引脚 准备相应的控制操作和通信功能。STM32 全系列芯片 GPIO 被分成多组,每组 有 16 个引脚。以 STM32F407ZGT6 以型号微控制器为例,它 GPIO 引脚被分成 GPIOA、 GPIOB、…、GPIOG 共 7 组,共有 144 个引脚。GPIOA 组(也叫组) GPIOA 端口)含有 GPIOA.0 (也称为 PA0)、GPIOA.1、…、GPIOA.15 共 16 引脚,别的 GPIO 组亦然。 STM32F4 微控制器系列 GPIO 以下可配置引脚 8 一种工作模式:输入浮空, 输入上拉、输入下拉、模拟输入、泄漏输出、推拉输出、复用推拉输出和复用泄漏输出。 在实际应用中,应根据应用需要进行选择 GPIO 工作模式。例如:配置 GPIO 引脚用于 LED 的 亮灭控制时,一般选择推挽输出的工作模式。 3.与 GPIO 相关作模式配置相关的寄存器和函数 API (1)与 GPIO 相关寄存器的工作模式配置 与GPIO工作模式配置相关的寄存器主要有GPIOx_MODER、GPIOx_OTYPER、GPIOx_OSPEEDR 和 GPIOx_PUPDR(x=A,B,C…),下面分别介绍一下。 GPIO 控制寄存器的端口模式(GPIO Port Mode Register,GPIOx_MODER)用于控制 GPIOx 工作模式如图所示 2-1-2 所示。
GPIO 端口输出类型寄存器(GPIO Port Output Type Register,GPIOx_OTYPER)用于控 制 GPIOx 输出类型,在 GPIOx_MODER 适用于配置为01(即通用输出模式)的相应位置。 该寄存器低 16 位置有效,每个位置控制一个 GPIO 引脚的位置定义如图所示 2-1-3 所示。
GPIO 端口输出速度寄存器(GPIO Port Output Speed Register,GPIOx_OSPEEDR)用于 控制 GPIOx 端口的输出速度和相应的输出频率 2 MHz、25 MHz、50 MHz 和 100 MHz 这 4 种。GPIOx_OSPEEDR 位段定义如图所示 2-1-4 所示。
(2)与 GPIO 工作模式配置相关函数 API 与 GPIO 工作模式配置相关函数 API 主要位于“stm32f4xx_gpio.c”和“stm32f4xx_gpio.h” 文件中 STM32F4 应用开发标准外设库时,各种外设的初始化一般是通过初始化进行的 完成结构体成员赋值,GPIO 工作模式的配置也是如此。 GPIO 端口初始化函数原始化 型如下。 void GPIO_Init(GPIO_TypeDef* GPIOx, GPIO_InitTypeDef* GPIO_InitStruct) ; 第一个参数需要初始化 GPIO 端口,对于 STM32F407ZGT6 就型号而言,取值范围是 GPIOA~GPIOG。第二个参数是初始参数的结构体指针,结构体类型为 GPIO_InitTypeDef, 其原型定义如下。
typedef struct { uint32_t GPIO_Pin; // 要初始化的 GPIO 引脚编号 GPIOMode_TypeDef GPIO_Mode; //GPIO 端口的工作模式 GPIOSpeed_TypeDef GPIO_Speed; //GPIO 端口的输出速度 GPIOOType_TypeDef GPIO_OType; //GPIO 端口输出类型 GPIOPuPd_TypeDef GPIO_PuPd; //GPIO 端口的上拉 / 下拉形式 } GPIO_InitTypeDef;
在上述结构定义中,第一个成员 GPIO_Pin 初始化配置 GPIO 引脚号。如配置选项 “GPIO_Pin_9”代表对GPIOx 端口中的9 配置号引脚,即GPIOx.9(x=A,B,C,D,E…)。 第二个成员 GPIO_Mode 用于配置 GPIO 端口的工作模式,其类型枚举,实际配置 GPIOx_MODER 的值。其类型定义如下:
typedef enum { GPIO_Mode_IN = 0x00, //GPIO 输入模式 GPIO_Mode_OUT = 0x01, //GPIO 输出模式 GPIO_Mode_AF = 0x02, //GPIO 复用功能模式 GPIO_Mode_AN = 0x03 //GPIO 模拟输入模式 } GPIOMode_TypeDef;
第三个成员 GPIO_Speed 用于配置 GPIO 端口的输出速度,其类型枚举,实际配置 GPIOx_OSPEEDR 值。类型定义如下:
typedef enum { GPIO_Low_Speed = 0x00, // 低速 Low speed = 2 MHz GPIO_Medium_Speed = 0x01, // 中速 Medium speed = 25 MHz GPIO_Fast_Speed = 0x02, // 快速 Fast speed = 50 MHz GPIO_High_Speed = 0x03 // 高速 High speed = 100 MHz } GPIOSpeed_TypeDef;
第四个成员 GPIO_Otype 用于配置 GPIO 端口输出类型,其类型为枚举,实际配置 GPIOx_OTYPER 值。类型定义如下:
typedef enum { GPIO_OType_PP = 0x00, // 推挽输出模式 GPIO_OType_OD = 0x01 // 泄漏输出模式 } GPIOOType_TypeDef;
第五个成员 GPIO_PuPd 用于配置 GPIO 端口的上拉/下拉形式为枚举,实际配置 GPIOx_PUPDR 值。类型定义如下:
typedef enum { GPIO_PuPd_NOPULL = 0x00, // 无上拉下拉 GPIO_PuPd_UP = 0x01, // 上拉 GPIO_PuPd_DOWN = 0x02 // 下拉 } GPIOPuPd_TypeDef;
下面是一段 GPIO 端口初始化程序实例:
1 GPIO_InitTypeDef GPIO_InitStructure; 2 RCC_AHB1PeriphClockCmd(RCC_AHB1Periph_GPIOF, ENABLE); // 开启 GPIOF 端口的时钟 3 GPIO_InitStructure.GPIO_Pin = GPIO_Pin_9; // 配置 PF9 引脚 4 GPIO_InitStructure.GPIO_Mode = GPIO_Mode_OUT; // 输出模式 5 GPIO_InitStructure.GPIO_OType = GPIO_OType_PP; // 推挽输出 6 GPIO_InitStructure.GPIO_Speed = GPIO_High_Seed; // 高速, 100MHz
7 GPIO_InitStructure.GPIO_PuPd = GPIO_PuPd_UP; // 上拉
8 GPIO_Init(GPIOF, &GPIO_InitStructure); // 配置生效
4.与 GPIO 电平控制相关的寄存器和函数 API 介绍完与 GPIO 工作模式配置相关的寄存器和函数 API 后,接下来介绍与 GPIO 电平控制相 关的寄存器,并讲解如何调用相应的函数 API 以实现 GPIO 电平的控制。 与 GPIO 电平控制相关的寄存器有 GPIOx_IDR、GPIOx_ODR 和 GPIOx_BSRR,下面分别 对它们进行介绍。 (1)GPIOx_IDR 及其相关函数 API GPIO 端口输入数据寄存器(GPIO Port Input Data Register,GPIOx_IDR)用于读取某个 GPIOx 端口的输入电平,低 16 位有效,分别对应 16 个引脚的电平值。若某位的值为 0(如 GPIOA_IDR9=0),则说明该引脚(PA9)输入低电平;反之,输入高电平。GPIOx_IDR 的位段 定义如图 2-1-6 所示。
STM32F4标准外设库提供了GPIO_ReadInputData()函数和GPIO_ReadInputDataBit()函数 用于读取输入的电平,前者用于一次性读取某 GPIOx 端口中所有引脚的输入电平,后者用于读 取若干 GPIO 引脚的输入电平。它们的原型定义如下: uint16_t GPIO_ReadInputData(GPIO_TypeDef* GPIOx); uint8_t GPIO_ReadInputDataBit(GPIO_TypeDef* GPIOx, uint16_t GPIO_Pin); 使用实例 1: GPIO_ReadInputData(GPIOB); 上述实例一次性读取 GPIOB 端口中所有 GPIO 引脚的输入电平,返回 16 位二进制数据。 使用实例 2: GPIO_ReadInputDataBit(GPIOA, GPIO_Pin_8); 上述实例读取 GPIOA.8 引脚(PA8)的输入电平,返回 8 位二进制数据。 (2)GPIOx_ODR 及其相关函数 API GPIO 端口输出数据寄存器(GPIO Port Output Data Register,GPIOx_ODR)用于设置某 个 GPIOx 端口的输出电平,当某个 ODRy 位段被写入 0 时,相应的引脚输出低电平;否则,输 出高电平。GPIOx_ODR 的位段定义如图 2-1-7 所示。 STM32F4 标准外设库提供了 GPIO_Write()函数来实现对 GPIOx 端口输出电平的控制,其原 型定义如下: void GPIO_Write(GPIO_TypeDef* GPIOx, uint16_t PortVal);
GPIO_Write()函数的第一个参数是 GPIOx 端口,第二个参数是需要输出的值(16 位二进制 数据),其使用实例如下: GPIO_Write(GPIOF, 0xFFB7); 上述实例控制 GPIOF 端口的 16 位输出为 0xFFB7。 若要一次性读取某GPIOx 端口中所有GPIO引脚的输出电平,则可使用GPIO_ReadOutputData() 函数,其函数原型定义如下: uint16_t GPIO_ReadOutputData(GPIO_TypeDef* GPIOx); 也可使用GPIO_ReadOutputDataBit()函数读取某GPIOx端口中的一个或若干个GPIO引脚 的输出电平,其函数原型定义如下: uint8_t GPIO_ReadOutputDataBit(GPIO_TypeDef* GPIOx, uint16_t GPIO_Pin); (3)GPIOx_BSRR 及其相关函数 API GPIO 端口置位/复位寄存器(GPIO Port Bit Set/Reset Register,GPIOx_BSRR)用于设置 某 GPIOx 端口中的一个或若干个 GPIO 引脚的输出电平,高 16 位对应输出低电平,低 16 位对 应输出高电平。 对于 GPIOx_BSRR 的高 16 位(16~31)而言,往相应的位写入 1,则对应的 GPIO 引脚输出低 电平;若写入0,则不起作用。同理,对于GPIOx_BSRR 的低16 位(0~15)而言,往相应的位写入 1,则对应的 GPIO 引脚输出高电平;若写入 0,则不起作用。GPIOx_BSRR 的位段定义如图 2-1-8 所示。
STM32F4 标准外设库提供了两个设置GPIO 端口输出电平的函数,它们通过操作GPIOx_BSRR
来实现,其函数原型定义如下: void GPIO_SetBits(GPIO_TypeDef* GPIOx, uint16_t GPIO_Pin); void GPIO_ResetBits(GPIO_TypeDef* GPIOx, uint16_t GPIO_Pin); 使用实例 1: GPIO_SetBits(GPIOA, GPIO_Pin_4); 上述实例设置 PA4 引脚输出高电平。 使用实例 2: GPIO_ResetBits(GPIOC, GPIO_Pin_1 | GPIO_Pin_12); 上述实例设置 PC1 和 PC12 两个引脚输出低电平。
2.1.3 任务实施 1.配置 SysTick 以实现延时 复制一份任务 1.3 的工程,并将其重命名为“task2.1_WaterFlow_LED”。在“SYSTEM”目 录下新建子文件夹“delay”,新建“delay.c”和“delay.h”文件,将它们加入工程中,并配置 头文件包含路径。在“delay.c”文件中输入以下代码:
#include "delay.h"
static uint8_t fac_us=0; //μs 延时倍乘数
static uint16_t fac_ms=0; //ms 延时倍乘数
/**
* @brief SysTick 初始化
* @param SysCLK: 系统时钟频率 ( 单位 :MHz)
* @retval None
*/
void delay_init(uint8_t SysCLK)
{
/* SysTick 时钟源设置为 HCLK 的 1/8 */
SysTick_CLKSourceConfig(SysTick_CLKSource_HCLK_Div8);
fac_us = SysCLK / 8;
fac_ms = (uint16_t)fac_us * 1000;
}
/**
* @brief 延时 nμs
* @note nμs 最大值为 798915 μs(2^24/fac_us@fac_us=21)
* @param nus: 要延时的微秒值
* @retval None
*/
void delay_us(uint32_t nus)
{
uint32_t temp;
SysTick->LOAD = nus * fac_us; // 时间加载
SysTick->VAL = 0x00; // 清空计数器
SysTick->CTRL |= SysTick_CTRL_ENABLE_Msk ; // 开始倒数
do{
temp=SysTick->CTRL;
} while((temp&0x01)&&!(temp&(1<<16))); // 等待时间到达
SysTick->CTRL &= ~SysTick_CTRL_ENABLE_Msk; // 关闭计数器
SysTick->VAL = 0X00; // 清空计数器
}
/**
* @brief 延时 nms
* @note nms 最大值为 798 ms( 对于 SYSCLK=168 MHz)
* @param nms: 要延时的毫秒值
* @retval None
*/
void delay_xms(uint16_t nms)
{
uint32_t temp;
SysTick->LOAD = (uint32_t)nms * fac_ms; // 时间加载
SysTick->VAL = 0x00; // 清空计数器
SysTick->CTRL |= SysTick_CTRL_ENABLE_Msk; // 开始倒数
do{
temp = SysTick->CTRL;
} while((temp&0x01)&&!(temp&(1<<16))); // 等待时间到达
SysTick->CTRL &= ~SysTick_CTRL_ENABLE_Msk; // 关闭计数器
SysTick->VAL = 0X00; // 清空计数器
}
/**
* @brief 延时 nms
* @note 对 delay_xms() 函数重新封装,避免最大值越界
* @param nms: 要延时的毫秒值 ( 范围 0~65535)
* @retval None
*/
void delay_ms(uint16_t nms)
{
uint8_t repeat = nms/540;
uint16_t remain = nms%540;
while(repeat)
{
delay_xms(540);
repeat--;
}
if(remain) delay_xms(remain);
}
在“delay.h”文件中输入以下代码:
#ifndef __DELAY_H
#define __DELAY_H
#include "sys.h"
void delay_init(uint8_t SysCLK);
void delay_us(uint32_t nus);
void delay_ms(uint16_t nms);
#endif
2.编程实现对 LED 流水灯的控制 在工程根目录下新建“HARDWARE”文件夹用于存放与外设硬件驱动相关的程序。建立子 文件夹“LED”,新建“led.c”和“led.h”两个文件,将它们加入工程中,并配置头文件包含路 径。在“led.c”文件中输入以下代码:
#include "led.h"
#include "delay.h"
/**
* @brief LED GPIO 引脚初始化
* @param None
* @retval None
*/
void LED_Init(void)
{
GPIO_InitTypeDef GPIO_InitStructure;
/* 使能 GPIOF 端口时钟 */
RCC_AHB1PeriphClockCmd(RCC_AHB1Periph_GPIOF, ENABLE);
/* LED0 : PF9 | LED1 : PF10 */
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_9 | GPIO_Pin_10;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_OUT; // 输出模式
GPIO_InitStructure.GPIO_OType = GPIO_OType_PP; // 推挽输出
GPIO_InitStructure.GPIO_Speed = GPIO_High_Speed; // 高速, 100MHz
GPIO_InitStructure.GPIO_PuPd = GPIO_PuPd_UP; // 默认上拉
GPIO_Init(GPIOF, &GPIO_InitStructure); // 配置生效
/* PF9,PF10 默认输出高电平 */
GPIO_SetBits(GPIOF,GPIO_Pin_9 | GPIO_Pin_10);
}
/**
* @brief LED 流水灯程序 ( 库函数实现 )
* @param xms: 延时时间 ( 单位为 ms)
* @retval None
*/
void WaterFlow_LED(uint16_t xms)
{
GPIO_ResetBits(GPIOF,GPIO_Pin_9); //LED0 亮
GPIO_SetBits(GPIOF,GPIO_Pin_10); //LED1 灭
delay_ms(xms); // 延时一段时间
GPIO_SetBits(GPIOF,GPIO_Pin_9); //LED0 灭
GPIO_ResetBits(GPIOF,GPIO_Pin_10); //LED1 亮
delay_ms(xms); // 延时一段时间
}
在“led.h”文件中输入以下代码:
#ifndef __LED_H
#define __LED_H
#include "sys.h"
/* LED 相关 GPIO 引脚宏定义 */
#define LED0 PFout(9) //LED0
#define LED1 PFout(10) //LED1
void LED_Init(void); //LED 相关 GPIO 端口初始化
void WaterFlow_LED(uint16_t xms);
#endif
在“main.c”文件中输入以下代码:
#include "sys.h"
#include "delay.h"
#include "led.h"
static uint16_t delay_xms = 0;
int main(void)
{
delay_init(168); // 延时函数初始化
LED_Init(); //LED 相关 GPIO 端口初始化
delay_xms = 1000; // 流水灯周期赋初值
while(1)
{
/* 周期变为 0.1s 后恢复为 2s */
if(delay_xms == 0)
{
delay_xms = 1000;
}
/* 执行流水灯函数 */
WaterFlow_LED(delay_xms);
/* 周期每次减少 0.1s */
delay_xms -= 50;
}
}
任务 2.2 按键控制流水灯的应用开发 2.2.1 任务分析 本任务要求设计一个可通过按键进行控制的 LED 流水灯系统,具体要求如下。 LED 流水灯的工作模式有 3 种。 模式①:两个 LED 以 2s 为周期交替闪烁,并以此循环往复。 模式②:两个 LED 同时亮 0.2s,然后同时灭 0.8s,并以此循环往复。 模式③:两个 LED 同时亮 0.8s,然后同时灭 0.2s,并以此循环往复。 用户通过按键进行 LED 流水灯工作模式的切换。通电时系统 默认运行在模式①,按下按键 K2 切换为模式②,按下按键 K3 切 换为模式③,按下按键 K1 又切换为模式①。 按键模块的电路原理图如图 2-2-1 所示。 从图 2-2-1 中可以看到,K1 连接 MCU 的 PE4 引脚,K2 连 接 PE3 引脚,K3 连接 PE2 引脚。各 GPIO 引脚默认为上拉,当 按键按下时相应的 GPIO 引脚接地且为低电平。 分析本任务的要求,若要通过按键控制LED流水灯的工作模式, 开发者须在应用程序中增加按键驱动与按键检测功能。按键通过 GPIO 引脚与MCU 相连,因此其驱动程序的编写与LED 类似。 另外,常见的按键检测方式有多种,开发者应了解每种检测方式的工作原理及其特性,然后 根据应用需求选择合适的检测方式。 本任务涉及的知识点有: 嵌入式系统中的按键检测方式; STM32F4 系列微控制器中断管理的工作原理; STM32F4 系列微控制器的外部中断的配置和中断服务程序的编写技巧
2.2.2 知识链接 1.按键的检测方式 在任务 2.1 中,我们已经对 STM32F4 系列微控制器的 GPIO 的工作原理和配置方法进行了 学习。与 LED 相同,按键也是连接到 MCU 的 GPIO 引脚上的,因此其配置方法参照任务 2.1 中 相关内容即可。本小节主要探讨嵌入式系统中按键的检测方式。一般来说,要检测按键是否按下 有以下两种方式。 第一种是扫描方式。这种方式需要编写一个“按键扫描”函数,通过判断与按键相连的 GPIO
引脚电平高低来确定按键是否按下,进而决定下一步的程序流程。“按键扫描”函数运行在 main() 函数的 while 主循环中,反复执行。这种方式的优点是所需硬件资源较少,编程简单。但其缺点 也较为明显:一是“按键扫描”函数频繁执行须占用 CPU 时间;二是这种编程方式存在 MCU 无法及时响应甚至完全无法响应用户输入的问题。原因分析如下:while 主循环中的程序指令是 依次执行的,若用户按下按键时恰逢 CPU 时间被一段耗时的程序占用,则用户的此次输入无法 被 MCU 响应。扫描方式检测按键的示例代码片段如下:
while(1)
{
/* 按键扫描函数确定键值 */
keyValue = KEY_Scan(0);
/* 根据键值决定程序流程 */
if(keyValue == 1){
myLEDWorkMode = LED_MODE1;
}
else if(keyValue == 2){
myLEDWorkMode = LED_MODE2;
}
/* 执行流水灯函数,大约需要 10s */
WaterFlow_LED(myLEDWorkMode);
}
MCU 按照从上到下的顺序执行程序。假设当用户按下按键时 MCU 正在执行上述代码片段 中第 13 行的流水灯函数程序,由于流水灯函数的执行所需时间较长(大约 10s),因此,此次的 按键事件无法得到 MCU 的响应(由于还未执行到第 4 行的按键扫描程序)。而且用户按键的时 间(0.5s 以内)远小于流水灯函数的执行时间(10s),这意味着用户在按下按键时,MCU 有非 常大的概率正在执行流水灯函数,于是就出现了上文所说的 MCU 完全无法响应用户输入的问题。 第二种是中断方式。这种方式的编程实现需要用到MCU 的中断资源。对STM32F4 系列微控制器 而言,前述中断资源包括嵌套向量中断控制器和外部中断/事件控制器。用户先配置 GPIO 引脚与外部 中断线的映射关系,然后配置外部中断的优先级,最后编写“中断服务”函数。当用户按下按键时, 将触发相应的外部中断,程序跳转至“中断服务”函数执行后续处理程序。这种方式的优点如下: while 主循环无须频繁检测按键是否按下,仅当按键按下时才执行相应的操作,节省了 MCU 上的代码执行时间; 得益于中断的工作机制,这种编程方式可确保用户每次的按键输入都能及时得到 MCU 的响应。 2.STM32F4 的中断管理 (1)STM32F4 的中断/异常类型 Cortex-M4 内核支持 256 个中断,包括 16 个系统异常中断和 240 个可屏蔽中断,并具有 256 级可编程的中断优先级。STM32F4 系列微控制器在中断管理资源方面对 Cortex-M4 内核进 行了裁减,如 STM32F40xx/41xx 型号 MCU 仅支持 92 个中断,STM32F42xx/43xx 型号 MCU 仅支持 96 个中断。 STM32F40xx/41xx 型号 MCU 支持的 92 个中断由 10 个系统异常中断和 82 个可屏蔽中断组成,具有 16 级可编程的中断优先级,用户在编程时主要对 82 个可屏蔽中断进行管理与配置。 表 2-2-1 列出了 STM32F407ZGT6 的部分中断向量及其说明。
(2)STM32F4 的嵌套向量中断控制器 嵌套向量中断控制器(Nested Vectored Interrupt Controller,NVIC)是 Cortex-M4 内核的 外设,它控制 MCU 中与中断配置相关的功能。ST 公司在芯片设计时对 NVIC 的完整功能进行了 裁减,因此 STM32F4 的 NVIC 可以说是 Cortex-M4 内核的 NVIC 的子集。 NVIC 配置的相关寄存器在 core_cm4.h 文件中以结构体的形式被定义,如下所示:
typedef struct
{
__IO uint32_t ISER[8]; //Offset: 0x000(R/W) 中断使能寄存器
uint32_t RESERVED0[24];
__IO uint32_t ICER[8]; //Offset: 0x080(R/W) 中断清除寄存器
uint32_t RSERVED1[24];
__IO uint32_t ISPR[8]; //Offset: 0x100(R/W) 中断使能挂起寄存器
uint32_t RESERVED2[24];
__IO uint32_t ICPR[8]; //Offset: 0x180(R/W) 中断清除挂起寄存器
uint32_t RESERVED3[24];
__IO uint32_t IABR[8]; //Offset: 0x200(R/W) 中断有效位寄存器
uint32_t RESERVED4[56];
__IO uint8_t IPR[240]; //Offset: 0x300(R/W) 中断优先级寄存器 (8bit)
uint32_t RESERVED5[644];
__O uint32_t STIR; //Offset: 0xE00(/W) 软件触发中断寄存器
} NVIC_Type;
从上面的结构体定义中可以看到,中断配置相关寄存器有 7 种,常用的是 ISER、ICER 和 IPR 这 3 个。 中断使能寄存器(Interrupt Set Enable Registers,ISER)。它由 8 个 32 bit 寄存器组成,每 个 bit 控制一个中断,可管理 Cortex-M4 内核所有的 256 个中断,往相应的位写入“1”可使能 某个中断。但 STM32F407 系列微控制器只使用了前 82 bit,对应 82 个可屏蔽中断,即 ISER[0]、 ISER[1]以及 ISER[2]的 bit0~bit17。 中断清除寄存器(Interrupt Clear Enable Registers,ICER)。它的功能与 ISER 相反,往相 应的位写入“1”可清除某个中断。 中断优先级控制寄存器(Interrupt Priority Registers,IPR)。它 由 240 个 8 bit 的寄存器组 成,对应Cortex-M4 内核的240 个可屏蔽中断,可管理256(2 8 = 256)级中断优先级。STM32F407 系列微控制器只使用了其中 82 个寄存器,对应 82 个可屏蔽中断;8 bit 寄存器只使用了高 4 bit, 可管理 16(2 4 = 16)级中断优先级。 (3)STM32F4 的中断优先级分组 STM32F4 系列微控制器的中断优先级管理采取了分组的理念,将优先级分为“抢占优先级” 与“子优先级”。中断优先级分组由系统控制基本寄存器(System Control Base Register,SCBR) 中的应用程序中断和复位控制寄存器(Application Interrupt and Reset Control Register,AIRCR) 中的 PRIGROUP[10:8]位段决定,共分为 5 个组别,具体分组情况见表 2-2-2。
对“抢占优先级”和“子优先级”在程序执行过程中的判定规则说明如下: 若两个中断的“抢占优先级”与“子优先级”都相同,则哪个中断先发生就先执行哪个; “抢占优先级”高的中断可以打断“抢占优先级”低的中断; 若两个中断的“抢占优先级”相同,则当两个中断同时发生时,“子优先级”高的中断 先被执行,且“子优先级”高的中断不能打断“子优先级”低的中断。 STM32F4 的标准外设库提供了 NVIC_PriorityGroupConfig()函数用于中断优先级的分组,其 原型定义如下:
void NVIC_PriorityGroupConfig(uint32_t NVIC_PriorityGroup)
{
/* 检查参数 */
assert_param(IS_NVIC_PRIORITY_GROUP(NVIC_PriorityGroup));
/* 根据 NVIC_PriorityGroup 的值配置 PRIGROUP[10:8] 位段 */
SCB->AIRCR = AIRCR_VECTKEY_MASK | NVIC_PriorityGroup;
}
NVIC_PriorityGroupConfig()函数的输入参数 NVIC_PriorityGroup 的定义如下:
#define NVIC_PriorityGroup_0 ((uint32_t)0x700) // 对应组 0
#define NVIC_PriorityGroup_1 ((uint32_t)0x600) // 对应组 1
#define NVIC_PriorityGroup_2 ((uint32_t)0x500) // 对应组 2
#define NVIC_PriorityGroup_3 ((uint32_t)0x400) // 对应组 3
#define NVIC_PriorityGroup_4 ((uint32_t)0x300) // 对应组 4
使用实例: NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2); 上述实例将系统的优先级分组配置为“组 2”,通过查表 2-2-2 可知,该系统具备 4 级抢占 优先级和 4 级子优先级。 (4)STM32F4 的中断优先级配置 STM32F4 的标准外设库提供了 NVIC_Init()函数用于中断优先级的配置,其原型定义如下: void NVIC_Init(NVIC_InitTypeDef* NVIC_InitStruct); 函数参数 NVIC_InitStruct 所对应的 NVIC_InitTypeDef 结构体在“misc.h”文件中被定义, 如下所示:
typedef struct
{
uint8_t NVIC_IRQChannel; // 中断源
uint8_t NVIC_IRQChannelPreemptionPriority; // 抢占优先级
uint8_t NVIC_IRQChannelSubPriority; // 子优先级
FunctionalState NVIC_IRQChannelCmd; // 中断使能或失能
} NVIC_InitTypeDef;
接下来对结构体中的 4 个成员进行介绍。 NVIC_IRQChannel:该成员用于配置中断源,可选参数见“stm32f4xx.h”文件中的 IRQn_Type 枚举类型定义。 NVIC_IRQChannelPreemptionPriority:该成员用于配置抢占优先级。 NVIC_IRQChannelSubPriority:该成员用于配置子优先级。 NVIC_IRQChannelCmd:该成员用于配置使能(ENABLE)或失能(DISABLE)某个中断,对该成员的配置其实是对 ISER 与 ICER 进行操作。 中断优先级的配置实例见以下代码:
NVIC_InitTypeDef NVIC_InitStructure;
NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2); // 配置为组 2
NVIC_InitStructure.NVIC_IRQChannel = EXTI3_IRQn; // 外部中断 3
NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 0x02; // 抢占优先级 2
NVIC_InitStructure.NVIC_IRQChannelSubPriority = 0x02; // 子优先级 2
NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE; // 使能外部中断
NVIC_Init(&NVIC_InitStructure); //NVIC 配置生效
3.STM32F4 的外部中断/事件控制器 STM32F4 的外部中断/事件控制器(External Interrupt/Event Controller,EXTI)包含 23 个 可用于产生中断/事件请求的边沿检测器。STM32F4 系列微控制器的每个 GPIO 引脚都可作为外 部中断的中断输入口,且每个外部中断都设置了状态位,具备独立的触发和屏蔽设置的功能。这 23 个外部中断或事件介绍如下。 EXTI 线 0~15:对应外部 I/O 口的输入中断。 EXTI 线 16:连接到 PVD 输出。 EXTI 线 17:连接到 RTC 闹钟事件。 EXTI 线 18:连接到 USB_OTG_FS 唤醒事件。 EXTI 线 19:连接到以太网唤醒事件。 EXTI 线 20:连接到 USB_OTG_HS(在 FS 中配置)唤醒事件。 EXTI 线 21:连接到 RTC 入侵和时间戳事件。 EXTI 线 22:连接到 RTC 唤醒事件。 由上述介绍可知,STM32F4 系列微控制器供 GPIO 引脚使用的中断线有 16 条,即:EXTI0~ EXTI15。MCU 本身的 GPIO 引脚数量大于 16,因此需要制定 GPIO 引脚与中断线映射的规则。 ST 公司制定的规则如下:所有 GPIO 端口的引脚 0 共用 EXTI0 中断线,引脚 1 共用 EXTI1 中断 线,以此类推,引脚 15 共用 EXTI15 中断线,使用前再将某个 GPIO 引脚与中断线进行映射。 如 PA0、PB0、PC0、……、PI0 共用 EXTI0 中断线,则使用前须将 EXTI0 中断线与某 GPIO 端 口的引脚 0 进行映射。中断线与 GPIO 端口映射的示意如图 2-2-2 所示。
4.STM32F4 外部中断的编程配置步骤 使用 STM32F4 标准外设库进行外部中断的编程配置可按照以下步骤进行。 (1)配置 GPIO 引脚的工作模式为输入模式 要将某个GPIO引脚作为外部中断的输入口,应先配置该GPIO引脚的工作模式为输入模式。 任务 2.1 已经介绍了输出模式的 GPIO 引脚配置方法,输入模式的 GPIO 引脚配置方法与其类似, 此处不再赘述。 (2)开启系统配置控制器(SYSCFG)时钟,配置中断线与 GPIO 引脚的映射关系 由图 2-2-2 可知,中断线与 GPIO 引脚的映射配置实际上是通过修改 SYSCFG_EXTICRx (x=1,2,3,4)的参数实现的。例如:EXTI15 外部中断由 SYSCFG_EXTICR4 中的“EXTI15[3:0]” 位段进行配置。由于配置过程涉及 SYSCFG_EXTICRx,因此在配置前需要先开启 SYSCFG 时钟, 具体代码如下: RCC_APB2PeriphClockCmd(RCC_APB2Periph_SYSCFG, ENABLE); STM32F4 标准外设库提供了 SYSCFG_EXTILineConfig()函数用于配置中断线与 GPIO 引脚 的映射关系,其原型定义如下: void SYSCFG_EXTILineConfig(uint8_t EXTI_PortSourceGPIOx, uint8_t EXTI_PinSourcex); SYSCFG_EXTILineConfig()函数中有两个参数:EXTI_PortSourceGPIOx 参数指明了 GPIO 端口,如 GPIOA、GPIOB 等;EXTI_PinSourcex 参数指明了中断线编号,如 EXTI_PinSource0 对应外部中断 0。具体配置实例如下: SYSCFG_EXTILineConfig(EXTI_PortSourceGPIOA, uint8_t EXTI_PinSource4); 上述配置实例将外部中断 4 与 PA4 引脚进行了映射。 (3)配置外部中断的工作参数 配置好中断线与 GPIO 引脚的映射关系之后,我们还须对外部中断的工作参数进行配置, STM32F4 标准外设库提供了外部中断初始化结构体 EXTI_InitTypeDef 来完成此项配置。 EXTI_InitTypeDef 结构体可配置外部中断的工作模式、触发方式等参数,被初始化函数 EXTI_Init() 调用,其原型定义如下:
typedef struct
{
uint32_t EXTI_Line; // 中断或事件线
EXTIMode_TypeDef EXTI_Mode; //EXTI 的工作模式
EXTITrigger_TypeDef EXTI_Trigger; //EXTI 的触发方式
FunctionalState EXTI_LineCmd; // 是否使能
} EXTI_InitTypeDef;
接下来对 EXTI_InitTypeDef 结构体成员变量的作用进行介绍。 ① EXTI_Line 中断或事件线配置,共有 23 个选项可供选择,分别是 EXTI_Line0~EXTI_Line22。 ② EXTI_Mode EXTI 工作模式配置,可配置的参数如下: 产生中断(EXTI_Mode_Interrupt); 产生事件(EXTI_Mode_Event)。
③ EXTI_Trigger EXTI 边沿触发方式配置,可配置的参数如下: 上升沿触发(EXTI_Trigger_Rising); 下降沿触发(EXTI_Trigger_Falling); 上升沿与下降沿都触发(EXTI_Trigger_Rising_Falling)。 ④ EXTI_LineCmd 是否使能 EXTI 线配置,可配置的参数如下: 使能 EXTI 线(ENABLE); 禁用 EXTI 线(DISABLE)。 (4)通过 NVIC 配置外部中断的优先级 在 STM32F4 系列微控制器的编程应用中,任何使用 MCU 中断资源的应用程序都要通过 NVIC 配置中断的优先级,外部中断也不例外。具体的配置方法参照 NVIC 相关的介绍。 (5)编写外部中断服务函数 在引入中断工作机制后,应用程序提高了用户请求的响应速度。在中断工作机制中,用于处 理用户请求的函数被称为“中断服务”函数,该函数是中断工作机制的重要组成部分。 用户在编写中断服务函数的过程中,需要特别关注以下两点(注:适用于所有中断类型)。 一是中断服务函数的入口函数名是由 STM32F4 标准外设库规定的,用户不可随意命名。 二是应掌握中断服务函数的框架结构。在实际的编程应用中,各种中断服务函数的框架结构 都是类似的,应熟练掌握并灵活应用之。 对外部中断而言,虽然 STM32F4 系列微控制器支持 16 路外部中断,但标准外设库只定义 了 7 个外部中断函数。EXTI0~EXTI4 各自独享一个函数,EXTI5~EXTI9 共用一个函数, EXTI10~EXTI15 共用一个函数,具体函数名如表 2-2-3 所示。
从表 2-2-3 可知,有的中断服务函数被多个中断源共用。以 EXTI9_5_IRQHandler()中断服 务函数为例,它被外部中断 EXTI5~EXTI9 共用。即 EXTI5~EXTI9 共 5 个外部中断中,不论哪个 中断满足其触发条件,程序都将跳转入 EXTI9_5_IRQHandler()函数并执行。因此,在刚进入该 函数时,需要用条件判断语句检测究竟发生了何种中断。同时,执行完中断处理的相关程序之后, 应清除相应的中断标志位。 STM32F4 标准外设库提供了 EXTI_GetITStatus()函数,用于判断某个中断线上是否触发了中 断,其原型定义如下: ITStatus EXTI_GetITStatus(uint32_t EXTI_Line) ;
EXTI_GetITStatus()函数的参数为要判断的中断线编号,如 EXTI_Line3 对应中断线 3, EXTI_Line8 对应中断线 8。 函数 EXTI_ClearITPendingBit()用于清除某个中断线上的中断标志位,其原型定义如下: void EXTI_ClearITPendingBit(uint32_t EXTI_Line) ; 下面给出一个外部中断服务函数的框架,该函数以中断线 3 为例,适用于 STM32F4 系列微 控制器的其他中断,只须修改中断服务函数名、判断中断是否发生的语句和清除中断标志位的语 句即可。
/* EXTI3 中断服务函数 */
void EXTI3_IRQHandler(void)
{
/* 判断中断线上的中断是否发生 */
if(EXTI_GetITStatus(EXTI_Line3) != RESET)
{
// TODO 中断处理逻辑
……
/* 清除相应的中断标志位 */
EXTI_ClearITPendingBit(EXTI_Line3);
}
}
2.2.3 任务实施 本任务的知识链接部分介绍了按键的两种检测方式,本小节我们将分别用这两种方式实 现任务要求。 1.用扫描方式实现按键功能 (1)编写按键引脚的配置程序 按键引脚的配置与 LED 引脚的配置方法类似,不同之处在于 GPIO 引脚的工作模式:LED 引脚配置为 GPIO_Mode_OUT,按键引脚配置为 GPIO_Mode_IN。 复制一份任务 2.1 的工程,并将其重命名为“task2.2_KeyScan_WaterFlow_LED”。在 “HARDWARE”文件夹下新建“KEY”子文件夹,新建“key.c”和“key.h”两个文件,将它们 加入工程中,并配置头文件包含路径。在“key.c”文件中输入以下代码:
#include "key.h"
#include "delay.h"
/**
* @brief 按键 GPIO 引脚初始化
* @param None
* @retval None
*/
void Key_Init(void)
{
GPIO_InitTypeDef GPIO_InitStructure;
/* 使能 GPIOA , GPIOE 时钟 */
RCC_AHB1PeriphClockCmd(RCC_AHB1Periph_GPIOA|RCC_AHB1Periph_GPIOE, ENABLE);
/* PE4|PE3|PE2 初始化 */
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_2|GPIO_Pin_3|GPIO_Pin_4;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IN; // 输入模式
GPIO_InitStructure.GPIO_Speed = GPIO_High_Speed; // 高速, 100MHz
GPIO_InitStructure.GPIO_PuPd = GPIO_PuPd_UP; // 默认上拉
GPIO_Init(GPIOE, &GPIO_InitStructure); // 配置生效
/* PA0 初始化 */
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_0;
GPIO_InitStructure.GPIO_PuPd = GPIO_PuPd_DOWN; // 默认下拉
GPIO_Init(GPIOA, &GPIO_InitStructure);
}
(2)编写按键扫描函数 用扫描方式实现按键功能的主要函数是“按键扫描”函数,该函数通过读取按键 GPIO 引脚 的电平状态来判断某个按键是否被按下。继续在“key.c”文件中输入以下代码:
/**
* @brief 按键扫描函数
* @note 注意此函数按键有响应优先级, KEY_L(K1)>KEY_D(K2)>KEY_R(K3)>KEY_U(K4)
* @param mode 是否支持连续按, 1 支持连续按, 0 不支持连续按
* @retval 键值 ( 无按键按下返回 0)
*/
uint8_t Key_Scan(uint8_t mode)
{
static uint8_t key_up = 1; // 按键松开标志
if(mode) key_up = 1; // 支持连按
if(key_up && (KEY_L==0||KEY_D==0||KEY_R==0||KEY_U==1))
{
delay_ms(20); // 去抖动
key_up=0;
if(KEY_L==0) return KEY_L_PRESS; // 左键键值 1
else if(KEY_D==0)return KEY_D_PRESS; // 下键键值 2
else if(KEY_R==0)return KEY_R_PRESS; // 右键键值 3
else if(KEY_U==1)return KEY_U_PRESS; // 上键键值 4
}
else if(KEY_L==1&&KEY_D==1&&KEY_R==1&&KEY_U==0)
{
key_up = 1;
}
在“key.h”文件中输入以下代码:
#ifndef __KEY_H
#define __KEY_H
#include "sys.h"
/* 位带方式读取 GPIO 端口状态 */
#define KEY_L PEin(4) //PE4|K1|Key_Left
#define KEY_D PEin(3) //PE3|K2|Key_Down
#define KEY_R PEin(2) //PE2|K3|Key_Right
#define KEY_U PAin(0) //PA0|K4|Key_Up
/* 键值宏定义 */
#define KEY_L_PRESS 1
#define KEY_D_PRESS 2
#define KEY_R_PRESS 3
#define KEY_U_PRESS 4
void Key_Init(void); // 按键 GPIO 端口初始化
uint8_t Key_Scan(uint8_t mode); // 按键扫描函数
#endif
(3)编写 LED 流水灯的各种工作模式函数 在“led.c”文件中增加以下代码:
/**
* @brief LED 流水灯模式 1
* @param time: 延时时间 ( 单位 ms)
* @retval None
*/
void LED_Mode1(uint16_t time)
{
GPIO_ResetBits(GPIOF,GPIO_Pin_9); //LED0 亮
GPIO_SetBits(GPIOF,GPIO_Pin_10); //LED1 灭
delay_ms(time); // 延时一段时间
GPIO_SetBits(GPIOF,GPIO_Pin_9); //LED0 灭
GPIO_ResetBits(GPIOF,GPIO_Pin_10); //LED1 亮
delay_ms(time); // 延时一段时间
}
/**
* @brief LED 流水灯模式 2
* @param None
* @retval None
*/
void LED_Mode2(void)
{
GPIO_ResetBits(GPIOF,GPIO_Pin_9); //LED0 亮
GPIO_ResetBits(GPIOF,GPIO_Pin_10); //LED1 亮
delay_ms(200); // 延时 200 ms
GPIO_SetBits(GPIOF,GPIO_Pin_9); //LED0 灭
GPIO_SetBits(GPIOF,GPIO_Pin_10); //LED0 灭
delay_ms(800); // 延时 800 ms
}
/**
* @brief LED 流水灯模式 3
* @param None
* @retval None
*/
void LED_Mode3(void)
{
GPIO_ResetBits(GPIOF,GPIO_Pin_9); //LED0 亮
GPIO_ResetBits(GPIOF,GPIO_Pin_10); //LED1 亮
delay_ms(800); // 延时 800 ms
GPIO_SetBits(GPIOF,GPIO_Pin_9); //LED0 灭
GPIO_SetBits(GPIOF,GPIO_Pin_10); //LED1 灭
delay_ms(200); // 延时 200 ms
}
在“led.h”文件中输入以下代码:
#ifndef __LED_H
#define __LED_H
#include "sys.h"
/* LED 引脚宏定义 */
#define LED0 PFout(9) //LED0
#define LED1 PFout(10) //LED1
/* LED 流水灯工作模式枚举类型定义 */
typedef enum
{
LED_MODE1 = 0x00,
LED_MODE2,
LED_MODE3,
} LED_WorkModeTypeDef;
void LED_Init(void); //LED 引脚初始化
void LED_Mode1(uint16_t time);
void LED_Mode2(void);
void LED_Mode3(void);
void WaterFlow_LED(uint16_t xms);
#endif
(4)编写 main()函数 在“main.c”文件中输入以下代码:
#include "sys.h"
#include "delay.h"
#include "led.h"
#include "key.h"
uint8_t keyValue = 0;
LED_WorkModeTypeDef myLEDWorkMode = LED_MODE1;
int main(void)
{
delay_init(168); // 延时函数初始化
LED_Init(); //LED 引脚初始化
Key_Init(); // 按键引脚初始化
while(1)
{
/* 按键扫描,不支持连续按 */
keyValue = Key_Scan(0);
switch (keyValue)
{
case KEY_L_PRESS: //K1 按下
myLEDWorkMode = LED_MODE1; // 切换为工作模式 1
break;
case KEY_D_PRESS: //K2 按下
myLEDWorkMode = LED_MODE2; // 切换为工作模式 2
break;
case KEY_R_PRESS: //K3 按下
myLEDWorkMode = LED_MODE3; // 切换为工作模式 3
break;
default:
break;
}
if(myLEDWorkMode == LED_MODE1)
{
/* 执行 LED 流水灯模式 1 */
LED_Mode1(1000);
}
else if(myLEDWorkMode == LED_MODE2)
{
/* 执行 LED 流水灯模式 2 */
LED_Mode2();
}
else if(myLEDWorkMode == LED_MODE3)
{
/* 执行 LED 流水灯模式 3 */
LED_Mode3();
}
}
}
2.用中断方式实现按键功能 与扫描方式相比,使用中断方式实现按键功能需要增加外部中断配置函数与中断服务函数, 其他程序可以继续沿用。 (1)编写外部中断初始化程序 复制一份 task2.2_KeyScan_WaterFlow_LED 工程,并将其重命名为“task2.2_KeyEXTI_ WaterFlow_LED”。在“HARDWARE”文件夹下新建子文件夹“EXTI”,新建“exti.c”和“exti.h” 两个文件,将它们加入工程中,并配置头文件包含路径。在“exti.c”文件中输入以下代码:
#include "exti.h"
#include "delay.h"
#include "key.h"
extern uint8_t keyValue;
/**
* @brief 外部中断初始化程序
* @note 将 PE2 , PE3 , PE4 , PA0 映射到外部中断线
* @param None
* @retval None
*/
void EXTIx_Init(void)
{
NVIC_InitTypeDef NVIC_InitStructure;
EXTI_InitTypeDef EXTI_InitStructure;
/* 开启 SYSCFG 时钟 */
RCC_APB2PeriphClockCmd(RCC_APB2Periph_SYSCFG, ENABLE);
/* 将 PE2 , PE3 , PE4 , PA0 分别映射到中断线 2 , 3 , 4 , 0 */
SYSCFG_EXTILineConfig(EXTI_PortSourceGPIOE, EXTI_PinSource2);
SYSCFG_EXTILineConfig(EXTI_PortSourceGPIOE, EXTI_PinSource3);
SYSCFG_EXTILineConfig(EXTI_PortSourceGPIOE, EXTI_PinSource4);
SYSCFG_EXTILineConfig(EXTI_PortSourceGPIOA, EXTI_PinSource0);
/* 配置 EXTI_Line0 */
EXTI_InitStructure.EXTI_Line = EXTI_Line0;//LINE0
EXTI_InitStructure.EXTI_Mode = EXTI_Mode_Interrupt;// 外部中断
EXTI_InitStructure.EXTI_Trigger = EXTI_Trigger_Rising;// 上升沿触发
EXTI_InitStructure.EXTI_LineCmd = ENABLE;// 使能 LINE0
EXTI_Init(&EXTI_InitStructure);// 配置生效
/* 配置 EXTI_Line2 , 3 , 4 */
EXTI_InitStructure.EXTI_Line = EXTI_Line2 | EXTI_Line3 | EXTI_Line4;
EXTI_InitStructure.EXTI_Mode = EXTI_Mode_Interrupt;// 外部中断
EXTI_InitStructure.EXTI_Trigger = EXTI_Trigger_Falling;// 下降沿触发
EXTI_InitStructure.EXTI_LineCmd = ENABLE;// 中断线使能
EXTI_Init(&EXTI_InitStructure);// 配置生效
/* 配置外部中断优先级 */
NVIC_InitStructure.NVIC_IRQChannel = EXTI0_IRQn;// 外部中断 0
NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 0x00;// 抢占优先级 0
NVIC_InitStructure.NVIC_IRQChannelSubPriority = 0x01;// 子优先级 1
NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE;// 使能外部中断通道
NVIC_Init(&NVIC_InitStructure);// 配置生效
NVIC_InitStructure.NVIC_IRQChannel = EXTI2_IRQn;// 外部中断 2
NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 0x00;// 抢占优先级 0
NVIC_InitStructure.NVIC_IRQChannelSubPriority = 0x02;// 子优先级 2
NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE;// 使能外部中断通道
NVIC_Init(&NVIC_InitStructure);// 配置生效
NVIC_InitStructure.NVIC_IRQChannel = EXTI3_IRQn;// 外部中断 3
NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 0x00;// 抢占优先级 0
NVIC_InitStructure.NVIC_IRQChannelSubPriority = 0x03;// 子优先级 3
NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE;// 使能外部中断通道
NVIC_Init(&NVIC_InitStructure);// 配置生效
NVIC_InitStructure.NVIC_IRQChannel = EXTI4_IRQn;// 外部中断 4
NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 0x00;// 抢占优先级 0
NVIC_InitStructure.NVIC_IRQChannelSubPriority = 0x04;// 子优先级 4
NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE;// 使能外部中断通道
NVIC_Init(&NVIC_InitStructure);// 配置生效
}
(2)编写外部中断服务函数 本任务使用了 3 个按键,它们的 GPIO 引脚分别与外部中断线 EXTI_Line2、EXTI_Line3 和 EXTI_Line4 映射,因此需要编写 3 个外部中断服务函数。在“exti.c”文件中继续输入以下代码:
/**
* @brief 外部中断 2 服务函数
* @param None
* @retval None
*/
void EXTI2_IRQHandler(void)
{
delay_ms(10); // 消抖
if(KEY_R == 0)
{
keyValue = KEY_R_PRESS; //K3 键值 3
}
EXTI_ClearITPendingBit(EXTI_Line2); // 清除 LINE2 上的中断标志位
}
/**
* @brief 外部中断 3 服务函数
* @param None
* @retval None
*/
void EXTI3_IRQHandler(void)
{
delay_ms(10); // 消抖
if(KEY_D == 0)
{
keyValue = KEY_D_PRESS; //K2 键值 2
}
EXTI_ClearITPendingBit(EXTI_Line3); // 清除 LINE3 上的中断标志位
}
/**
* @brief 外部中断 4 服务程序
* @param None
* @retval None
*/
void EXTI4_IRQHandler(void)
{
delay_ms(10); // 消抖
if(KEY_L == 0)
{
keyValue = KEY_L_PRESS; //K1 键值 1
}
EXTI_ClearITPendingBit(EXTI_Line4); // 清除 LINE4 上的中断标志位
}
在“exti.h”文件中输入以下代码:
#ifndef __EXTI_H
#define __EXTI_H
#include "sys.h"
void EXTIx_Init(void);
#endif
(3)编写 main()函数 在“main.c”文件中输入以下代码:
#include "sys.h"
#include "delay.h"
#include "led.h"
#include "key.h"
#include "exti.h"
uint8_t keyValue = 0;
LED_WorkModeTypeDef myLEDWorkMode = LED_MODE1;
int main(void)
{
delay_init(168); // 延时函数初始化
LED_Init(); //LED 引脚初始化
Key_Init(); // 按键引脚初始化
EXTIx_Init(); // 外部中断初始化
while(1)
{
/* 无需按键扫描函数 */
//keyValue = Key_Scan(0);
switch (keyValue)
{
case KEY_L_PRESS: //K1 按下
myLEDWorkMode = LED_MODE1; // 切换为工作模式 1
break;
case KEY_D_PRESS: //K2 按下
myLEDWorkMode = LED_MODE2; // 切换为工作模式 2
break;
case KEY_R_PRESS: //K3 按下
myLEDWorkMode = LED_MODE3; // 切换为工作模式 3
break;
default:
break;
}
if(myLEDWorkMode == LED_MODE1)
{
/* 执行 LED 流水灯模式 1 */
LED_Mode1(1000);
}
else if(myLEDWorkMode == LED_MODE2)
{
/* 执行 LED 流水灯模式 2 */
LED_Mode2();
}
else if(myLEDWorkMode == LED_MODE3)
{
/* 执行 LED 流水灯模式 3 */
LED_Mode3();
}
}
}
使用外部中断实现按键功能无需“按键扫描”函数,因此将上述代码段第 20 行的内容作为注释。
任务 2.3 串行通信控制流水灯的应用开发 2.3.1 任务分析 本任务要求设计一个 LED 流水灯系统,该系统与上位机之间通过串行通信接口相连。上位 机可发送命令对 LED 流水灯系统进行控制,具体要求如下。 LED 流水灯的工作模式有 3 种。 模式①:两个 LED 以 2s 为周期交替闪烁,并以此循环往复。 模式②:两个 LED 同时亮 0.2s,然后同时灭 0.8s,并以此循环往复。 模式③:两个 LED 同时亮 0.8s,然后同时灭 0.2s,并以此循环往复。 上位机以串行通信的方式发送命令至该系统进行 LED 流水灯工作模式的切换,命令 “mode_1#”“mode_2#”“mode_3#”分别对应模式①、模式②和模式③的控制。 分析本任务的要求,上位机需要以串行通信的方式发送命令以控制 LED 流水灯系统, STM32F4 系列微控制器的通用同步/异步收发器具备与外部设备进行串行通信的功能,因此可利 用此收发器完成任务要求。 本任务涉及的知识点有: 串行通信的基础知识; 通用同步/异步收发器的工作原理; 通用同步/异步收发器的配置与数据收发程序的编写技巧。 2.3.2 知识链接 1.串行通信的基本知识 (1)什么是串行通信 在计算机网络与分布式工业控制系统中,设备之间经常通过各自配备的标准串行通信接口, 加上合适的通信电缆实现数据与信息的交换。所谓“串行通信”是指外设和计算机之间通过数据 信号线、地线与控制线等,按位进行数据传输的一种通信方式。 目前常见的串行通信接口标准有 RS-232、RS-422 和 RS-485 等,它们由美国电子工业协 会(Electronic Industries Association,EIA)发布。本小节主要学习与 RS-232 标准相关的内容。 (2)常见的电平信号及其电气特性 在电子产品开发领域,常见的电平信号有 TTL 电平、CMOS 电平、RS-232 电平和 USB 电 平等。由于它们对逻辑“1”和逻辑“0”的表示标准有所不同,因此在不同器件之间进行通信时, 要特别注意电平信号的电气特性。表 2-3-1 对常见电平信号的逻辑表示与电气特性进行了归纳。
RS-232 电平与 TTL 电平的逻辑表示对比如图 2-3-1 所示。
(3)串行通信的接口与信号连接 RS-232 标准最早是为远程通信设计的,用于连接数据终端设备(Data Teminal Equipment,DTE) 与数据通信设备(Data Communication Equipment,DCE),现在普遍用于计算机之间或计算机与外 设之间的近端连接。 RS-232 标准规定使用 25 针标准接口,使用 DB-25 连接器。但在实际使用中,只需 2 根数据线、6 根控制器线和 1 根地线共 9 根信号线。因此一些生产厂家对 RS-232 标准的接 口进行了简化,使用 9 针标准接口,即 DB-9 连接器。图 2-3-2 展示了上述两种连接器的公头与母头。
下面对 DB-9 连接器的引脚及其功能进行说明,如图 2-3-3 和表 2-3-2 所示。
由于 DB-9 连接器体积较大,因此近年来便携式计算机和 PC 主板渐渐淘汰了这种接口。随 着 USB 接口的普及,出现了一种 USB 电平转 TTL 电平的转换模块,可方便地实现上位机与外设 之间的串行通信。这种转换模块对传输所用的信号线进行了进一步精简,只需 3 根信号线:RXD、 TXD 与 GND。常见的转换芯片型号有 CH340、PL2303、CP2102 和 FT232 等,转换模块的实 物图及其与 MCU 的连接如图 2-3-4 所示。
(4)串行通信的数据帧 在异步串行通信中,数据是以数据帧(Data Frame)为单位进行传输的。每个数据帧承载 一个字符数据,异步串行通信的数据帧结构如图 2-3-5 所示。
下面对数据帧中各个组成部分进行说明。 空闲位:数据帧与数据帧之间的间隔,没有严格的时间要求,因此空闲位的长度可以是 n 个位。 起始位:数据帧的开始,低电平逻辑“0”,长度占 1 位。 数据位:主体数据内容,长度可选 5、6、7 或 8 位。 校验位:用于校验数据传输是否正确,可选奇校验、偶校验或者也可以不使用校验。 停止位:数据帧的结束,高电平逻辑“1”,长度可选 0.5、1、1.5 或 2 位。 2.STM32F4 系列微控制器的通用同步/异步收发器介绍 (1)通用同步/异步收发器概述 通用同步/异步收发器的英文全称是 Universal Synchronous Asynchronous Receiver and Transmitter,简称USART。STM32F4系列微控制器有多个收发器外设(俗称“串口”)可用于串行通 信,包括4 个USART 和2个通用异步收发器(Universal Asynchronous Receiverand Transmitter, UART),它们分别是:USART1、USART2、USART3、UART4、UART5 和 USART6。与 USART 相比,UART 裁减了同步通信的功能,只有异步通信功能。同步通信与异步通信的区别在于通信 过程中是否需要发送器输出同步时钟信号 USART_CK,实际应用中一般使用异步通信。 USART 是 MCU 的重要外设,在程序设计的调试阶段可发挥重要作用,如将开发板与 PC 通 过串行通信接口相连后,可将调试信息“打印”到串口调试助手等工具中,开发者可借助这些信 息了解程序运行情况。 STM32F4 系列微控制器的各个收发器外设的工作时钟来源于不同的 APB:USART1 和 USART6 挂载在 APB2 上,最大频率为 84 MHz;其他 4 个收发器外设则挂载在 APB1 上,最大 频率为 42 MHz。表 2-3-3 展示了 STM32F407ZGT6 芯片 USART/UART 的外部引脚分布。
从表 2-3-3 可知,除了 UART5,其他收发器外设的功能引脚都有多个选择,这给硬件电路 PCB 设计的布线提供了极大的方便。 (2)USART 的数据寄存器 通过 USART 收发的数据都由 USART_DR 数据寄存器存放(UART 则对应 UART_DR),实 际上该寄存器包含了两个寄存器:发送数据寄存器(TDR)和接收数据寄存器(RDR)。前者专 门用于发送,后者专门用于接收。使用 USART 进行发送操作时,写入 DR(数据寄存器)的数 据会自动存储至 TDR 中;进行读取操作时,将自动从 DR 的 RDR 中读取。 TDR 和 RDR 中的数据是通过移位寄存器与系统总线通信的。发送数据时,TDR 的数据被转 移到发送移位寄存器,然后一位一位地发送出去;接收数据时,把接收到的每一位数据按顺序存 放在接收移位寄存器中,然后再转移到 RDR 中,即可进行读取操作。USART 收发数据寄存器的 功能框图如图 2-3-6 所示。
USART 还支持直接存储访问(DMA)传输,可实现高速数据传输,这部分知识本书不做 介绍。
(3)USART 的发送控制与接收控制 USART 的数据收发由专门的发送控制单元和接收控制单元来完成,其功能框图如图 2-3-7 所示。
① 发送控制 根据图 2-3-7 可知,使用 USART 外设进行数据发送的流程如下: 向 USART_CR1 中的“UE”位写入 1,使能 USART; 对 USART_CR1 中的“M”位进行编程以定义字长; 对 USART_CR2 中的停止位数量进行编程; 使用 USART_BRR 选择所需波特率; 将 USART_CR1 中的“TE”位置 1,以便在首次发送数据时发送一个空闲帧; 在 USART_DR 中写入要发送的数据(该操作将清零“TXE”位); 向 USART_DR 写入最后一个数据后,等待至 TC=1,这表明最后一个帧的传送已完成; 如果 USART_CR1 的“TCIE”位被置 1,则将产生中断。 操作 USART 进行数据发送时可遵循以上步骤进行程序的编写,表 2-3-4 总结了编程中用 到的标志位及其功能描述。
② 接收控制 根据图 2-3-7 可知,使用 USART 外设进行数据接收的流程如下: 向 USART_CR1 中的“UE”位写入 1,使能 USART; 对 USART_CR1 中的“M”位进行编程以定义字长; 对 USART_CR2 中的停止位数量进行编程; 使用 USART_BRR 选择所需波特率; 将 USART_CR1 的“RE”位置 1,这一操作将使能接收器开始搜索起始位; 接收到字符时,USART_SR 的“RXNE”位置 1,这表明移位寄存器的内容已传送到 RDR; 也就是说,已接收到并可读取数据(以及相应的错误标志); 如果 USART_CR1 的“RXNEIE”位置 1,则会生成 RXNE 中断; 所有数据接收完毕,线路进入空闲状态时,如果 USART_CR1 的“IDLEIE”位被置 1, 则会产生检测到空闲线路(IDLE)中断。 操作 USART 进行数据接收时可遵循以上步骤进行程序的编写,表 2-3-5 总结了编程中用 到的标志位及其功能描述
(4)USART 的中断控制 STM32F4 系列微控制器的 USART 支持多种中断事件,与发送有关的中断有发送完成、清 除以发送(CTS 标志)和发送数据寄存器为空,与接收有关的中断有接收数据寄存器不为空、检 测到空闲线路、检测到上溢错误、奇偶校验错误、检测到局域互联网络(LIN)断路、多缓冲通 信中的噪声标志、上溢错误和帧错误。以上各中断事件的标志和使能控制位见表 2-3-6,常用 的中断事件标志有 TC、TXE、RXNE 和 IDLE
上述所有的中断事件都被连接到相同的中断向量 — USARTx_IRQn(如 USART1 对应的中 断向量为 USART1_IRQn),因此进入中断服务函数后,需要判断发生了何种中断事件。
3.STM32F4 的 USART 编程配置步骤 (1)开启 USART 时钟和 GPIOx 时钟 查表 2-3-3 可知 USART 所在的 APB。如果我们要使用 USART1,并使用 PA9 和 PA10 分 别作为 TX 和 RX 引脚,执行以下代码即可开启 USART1 时钟和 GPIOA 时钟。 RCC_APB2PeriphClockCmd(RCC_APB2Periph_USART1,ENABLE); // 开启 USART1 时钟 RCC_AHB1PeriphClockCmd(RCC_AHB1Periph_GPIOA,ENABLE); // 开启 GPIOA 时钟 (2)GPIO 端口复用映射 STM32F4 系列微控制器集成了多个外设,各外设的引脚是与 GPIO 端口复用的,而且一个 GPIO 端口可复用为多个外设的引脚。如 PA10 端口,可复用为 USART1 的 RX 引脚、TIM1_CH3 (定时器 1 的通道 3)或 OTG_FS_ID 等。因此在对
typedef struct
{
uint32_t USART_BaudRate; // 波特率
uint16_t USART_WordLength; // 数据帧字长
uint16_t USART_StopBits; // 停止位
uint16_t USART_Parity; // 奇偶校验
uint16_t USART_Mode; // 是否使能收发功能
uint16_t USART_HardwareFlowControl; // 是否启用流控
} USART_InitTypeDef;
外设功能进行初始化时,需要指明将 GPIO 端 口复用为哪个外设的引脚。执行以下代码可将 PA9 和 PA10 引脚分别复用为 USART1 的 TX 和 RX 引脚。 GPIO_PinAFConfig(GPIOA,GPIO_PinSource9,GPIO_AF_USART1);//PA9 复用为 USART1_TX GPIO_PinAFConfig(GPIOA,GPIO_PinSource10,GPIO_AF_USART1);//PA10 复用为 USART1_RX (3)GPIO 引脚的工作模式配置 USART 外设的 GPIO 引脚工作模式的配置方法与 LED 引脚的配置方法类似,但工作模式应 配置为“复用功能”。执行以下代码可实现 USART1 的 GPIO 引脚的工作模式配置。
GPIO_InitTypeDef GPIO_InitStructure;
RCC_AHB1PeriphClockCmd(RCC_AHB1Periph_GPIOA,ENABLE); // 开启 GPIOA 时钟
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_9 | GPIO_Pin_10; //PA9 与 PA10
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF; // 复用功能
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz; // 快速, 50 MHz
GPIO_InitStructure.GPIO_OType = GPIO_OType_PP; // 推挽复用输出
GPIO_InitStructure.GPIO_PuPd = GPIO_PuPd_UP; // 上拉
GPIO_Init(GPIOA,&GPIO_InitStructure); // 配置生效
(4)USART 工作参数配置 STM32F4 标准外设库提供了 USART_InitTypeDef 结构体,其可对 USART 工作参数进行配 置,包括波特率、数据帧字长、停止位、奇偶校验、是否使能收发功能、是否启用流控等,其原 型定义如下:
typedef struct
{
uint32_t USART_BaudRate; // 波特率
uint16_t USART_WordLength; // 数据帧字长
uint16_t USART_StopBits; // 停止位
uint16_t USART_Parity; // 奇偶校验
uint16_t USART_Mode; // 是否使能收发功能
uint16_t USART_HardwareFlowControl; // 是否启用流控
} USART_InitTypeDef;
执行以下代码可将 USART1 设置为波特率 115200bit/s、数据长度 8 bit、一个停止位、无奇 偶校验、同时使能接收与发送功能、不启用流控。
USART_InitTypeDef USART_InitStructure;
USART_InitStructure.USART_BaudRate = 115200;// 波特率为 115200bit/s
USART_InitStructure.USART_WordLength = USART_WordLength_8b; // 数据长度为 8bit
USART_InitStructure.USART_StopBits = USART_StopBits_1; // 一个停止位
USART_InitStructure.USART_Parity = USART_Parity_No; // 无奇偶校验
USART_InitStructure.USART_HardwareFlowControl = USART_HardwareFlowControl_None;
USART_InitStructure.USART_Mode = USART_Mode_Rx | USART_Mode_Tx; // 使能收发功能
USART_Init(USART1, &USART_InitStructure); // 配置生效
(5)使能 USART 中断并设置中断优先级 USART 常用的接收中断包括“准备好读取接收到的数据(RXNE)”中断和“检测到空闲线 路(IDLE)”中断,使能 USART 中断后还应配置 NVIC 进行优先级的设置。执行以下代码可使 能上述两个中断,并配置 USART1 的中断优先级。
USART_ITConfig(USART1, USART_IT_RXNE, ENABLE); // 使能 RXNE 中断
USART_ITConfig(USART1, USART_IT_IDLE, ENABLE); // 使能 IDLE 中断
/* USART1 NVIC 配置 */
NVIC_InitTypeDef NVIC_InitStructure;
NVIC_InitStructure.NVIC_IRQChannel = USART1_IRQn; // 配置 USART1 中断源
NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority=3; // 抢占优先级 3
NVIC_InitStructure.NVIC_IRQChannelSubPriority =3; // 子优先级 3
NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE; //IRQ 通道使能
NVIC_Init(&NVIC_InitStructure); // 配置生效
(6)使能 USART 当 USART1 配置完成后,应调用 USART_Cmd()函数进行 USART1 的使能,执行以下代码 可实现上述功能。 USART_Cmd(USART1, ENABLE); // 使能 USART1 (7)编写 USART 中断服务函数 如果在 USART 配置中使能了中断,则在相应事件发生的时候,程序会跳到中断服务函数中 执行。在中断服务函数中,需要根据中断标志位来判断当前发生了何种中断,再作相应的处理。 获取 USART 中断标志位的状态使用 USART_GetITStatus()函数,其原型定义如下: ITStatus USART_GetITStatus(USART_TypeDef* USARTx, uint16_t USART_IT); 执行完中断服务函数以后,需要将相应的中断标志位清除,以便响应下次的中断事件。根据 “STM32F4 标准外设库”的注释说明可知,清除 USART 外设的中断标志位的方法有两种:一是 调用 USART_ClearFlag()函数,二是读写 USART_SR 和 USART_DR(注:不同的中断标志位有 不同的方法,具体可参考 STM32F4 标准外设库说明)。如 IDLE 中断标志位可通过先读取 SR, 再读取 DR 清除。基本的 USART1