流水灯实验
前言
点灯是嵌入式系统开发的print("hello world")
,作为嵌入式领域中最基础和最常见的调试技术之一,可以帮助开发者了解嵌入式系统的工作原理和编程方法,同时也是入门者学习和掌握嵌入式系统开发的第一步。本实验使用STM32F103ZET6芯片,利用其HAL库完成点灯操作。
GPIO寄存器介绍
STM32F1每组通用GPIO口有7个32位寄存器控制,包括:
- 2个32位端口配置寄存器(CRL 和 CRH)
- 2个32位端口数据寄存器(IDR 和 ODR)
- 1个32位端口置位/复位寄存器(BSRR)
- 1个16位端口复位寄存器(BRR)
- 1个32位端口锁定寄存器(LCKR)
寄存器的作用是控制GPIO口的工作模式和工作速度
寄存器的工作模式
- 输入浮空模式:将GPIO口配置为输入浮空模式时,GPIO口处于高阻态,不接收任何信号。该模式通常用于读取外部信号时,确保输入端口不受干扰
- 输入上拉模式:将GPIO口配置为输入上拉模式时,GPIO口处于高电平状态,当外部信号接地时,GPIO口输出低电平。该模式通常用于读取开关或按钮的状态
- 输入下拉模式:将GPIO口配置为输入下拉模式时,GPIO口处于低电平状态,当外部信号接高电平时,GPIO口输出高电平。该模式通常用于读取开关或按钮的状态
- 模拟功能模式:将GPIO口配置为模拟功能模式时,GPIO口可以输出模拟电压信号。该模式通常用于控制模拟电路的输出,如控制电机转速等
- 开漏输出模式:将GPIO口配置为开漏输出模式时,GPIO口能够输出低电平信号,但无法输出高电平信号,只能输出高阻态。该模式通常用于驱动开关电路或者需要多个GPIO口连接到同一个总线上的情况
- 推挽输出模式:将GPIO口配置为推挽输出模式时,GPIO口可以输出高电平和低电平信号。该模式通常用于驱动电机或其他需要输出高电平和低电平信号的设备
- 开漏式复用功能模式:将GPIO口配置为开漏式复用功能模式时,GPIO口可以作为其他功能模块的输入或输出,同时输出开漏信号。该模式通常用于多个设备共享一个GPIO口的情况
- 推挽式复用功能模式:将GPIO口配置为推挽式复用功能模式时,GPIO口可以作为其他功能模块的输入或输出,同时输出推挽信号。该模式通常用于多个设备共享一个GPIO口的情况
端口配置寄存器(GPIOx_CRL 和 GPIO_x_CRH)
这两个寄存器都是GPIO口配置寄存器,不过CRL控制端口的低八位,CRH控制端口的高八位。
每组GPIO
下有16个IO
口,一个寄存器共32位,每4个位控制1个IO
口,所以需要两个寄存器完成
CRL寄存器
CRL寄存器每4位控制一个IO口,控制的是一组IO口中的低8个IO口。每一组GPIO中都会用它来配置IO的工作模式,决定IO是输入口还是输出口,作为输入口时是哪个输入模式,作为输出口时是哪个输出模式,并且速度是多少
这是它的32个位,可以看到,每个MODE有两位,每个CNF有两位,一共就是4位,控制的一个IO口,后面的标号0到7是指从第1个IO口到第8个IO口
CRH寄存器
CRH寄存器每4位控制一个IO口,控制的是一组IO口中的高8个IO口。每一组GPIO中都会用它来配置IO的工作模式,决定IO是输入口还是输出口,作为输入口时是哪个输入模式,作为输出口时是哪个输出模式,并且速度是多少
端口输出数据寄存器(ODR)
该寄存器用于控制GPIOx的输出高电平或者低电平,可读可写权限
该寄存器低16位有效,分别对应每一组GPIO的16个引脚。当CPU写访问该寄存器,如果对应的某位写0(ODRy=0),则表示该IO口输出的是低电平。如果写1(ODRy=1)则表示设置该IO口输出的是高电平y=0~15。
端口置位/复位寄存器(BSRR)
该寄存器也用于控制GPIOx的输出高电平或者低电平,只写权限
该寄存器32位有效,对于低16位(015),我们往相应的位写1(BSy=1)那么对应的IO口会输出高电平,往相应的信号写0(BSy=0)对于IO口没有任何影响,高16位(1631)作用刚好相反,对相应的位写1(BRy=1)会输出低电平,写0(BRy=0)没有任何影响,y=0~15。
HAL库函数方式实现
首先介绍一下需要用到的库函数
GPIO 的 HAL 库驱动分析
HAL_GPIO_Init 函数
要使用一个外设我们首先要对它进行初始化,所以我们先看外设GPIO的初始化函数,其声明如下
void HAL_GPIO_Init(GPIO_TypeDef *GPIOx, GPIO_InitTypeDef *GPIO_Init)
函数描述
用于配置GPIO功能模式,还可以设置EXTI功能
函数形参
形参1是端口号
形参2是GPIO_InitTypeDef
类型的结构体变量,其定义如下
typedef struct
{
uint32_t Pin; /*引脚号*/
uint32_t Mode; /*模式设置*/
uint32_t Pull; /*上拉下拉设置*/
uint32_t Speed; /*速度设置*/
} GPIO_InitTypeDef;
- 成员
Pin
表示引脚号,范围GPIO_PIN_0
到GPIO_PIN_15
,另外还有GPIO_PIN_ALL
和GPIO_PIN_MASK
可选 - 成员
Mode
是GPIO
的模式选择 - 成员
Speed
用于配置GPIO
的速度 - 成员
Pull
用于配置上下拉电阻
无函数返回值
HAL_GPIO_WritePin函数
HAL_GPIO_WritePin是GPIO口的写引脚函数,其声明如下:
void HAL_GPIO_WritePin(GPIO_TypeDef *GPIOx, uint16_t GPIO_Pin, GPIO_PinState PinState)
函数描述
用于设置引脚输出高电平或者低电平,通过BSRR寄存器复位或者置位操作
函数形参
形参1是端口号,可以选择范围:GPIOA~GPIOG
形参2是引脚号,可以选择范围:GPIO_PIN_0 到 GPIO_PIN_15
形参3是要设置输出的状态,有两个选择:GPIO_PIN_SET表示高电平,GPIO_PIN_RESET表示低电平
无函数返回值
HAL_GPIO_TogglePin 函数
HAL_GPIO_TogglePin 函数是 GPIO 口的电平翻转函数,其声明如下:
void HAL_GPIO_TogglePin(GPIO_TypeDef *GPIOx, uint16_t GPIO_Pin)
函数描述
用于设置引脚的电平翻转,也是通过BSRR寄存器复位或者置位操作
函数形参
形参1是端口号,可以选择范围:GPIOA~GPIOG
形参2是引脚号,可以选择范围:GPIO_PIN_0 到 GPIO_PIN_15
无函数返回值
实验过程
实验思路
使能对应时钟
STM32在使用任何外设之前,我们都要先使能器时钟。本实验用到PB5和PE5两个IO口,因此需要先使能GPIOB和GPIOE的时钟,代码如下:
_HAL_RCC_GPIOB_CLK_ENABLE();
__HAL_RCC_GPIOE_CLK_ENABLE();
设置对应GPIO工作模式(推挽输出)
本实验GPIO使用推挽输出模式,控制LED亮灭通过函数HAL_GPIO_Init
设置实现
控制GPIO引脚输出高低电平
在配置好GPIO工作模式后,我们就可以通过函数HAL_GPIO_WritePin
控制GPIO引脚输出高低电平,从而控制LED的亮灭了
代码实现
程序流程
程序解析
led驱动代码
下面我们先解析 led.h 的程序,我们把它分两部分功能进行讲解
LED灯引脚宏定义
由于LED灯在硬件上分别连接到PB5和PE5,再结合HAL库,我们做了下面的引脚定义
/*LED0 引脚定义 */
#define LED0_GPIO_PORT GPIOB
#define LED0_GPIO_PIN GPIO_PIN_5
#define LED0_GPIO_CLK_ENABLE() do{__HAL_RCC_GPIOB_CLK_ENABLE();}while(0)
/*LED1 引脚定义 */
#define LED1_GPIO_PORT GPIOE
#define LED1_GPIO_PIN GPIO_PIN_5
#define LED1_GPIO_CLK_ENABLE() do{__HAL_RCC_GPIOE_CLK_ENABLE();}while(0)
LED灯操作函数宏定义
为了后续对LED灯进行便捷的操作,我们为LED灯操作函数做了下面的定义
/* LED 端口操作定义 */
#define LED0(x) do{ x ? \ HAL_GPIO_WritePin(LED0_GPIO_PORT,LED0_GPIO_PIN, GPIO_PIN_SET) : \ HAL_GPIO_WritePin(LED0_GPIO_PORT,LED0_GPIO_PIN, GPIO_PIN_RESET);\ }while(0) /* LED0 翻转 */
#define LED1(x) do{ x ? \ HAL_GPIO_WritePin(LED1_GPIO_PORT, LED1_GPIO_PIN, GPIO_PIN_SET) : \ HAL_GPIO_WritePin(LED1_GPIO_PORT, LED1_GPIO_PIN, GPIO_PIN_RESET);}while(0) /* LED1 翻转 */
/* LED 电平翻转定义 */
#define LED0_TOGGLE() do{ HAL_GPIO_TogglePin(LED0_GPIO_PORT, LED0_GPIO_PIN); }while(0) /* LED0 = !LED0 */
#define LED1_TOGGLE() do{ HAL_GPIO_TogglePin(LED1_GPIO_PORT, LED1_GPIO_PIN); }while(0) /* LED1 = !LED1 */
下面我们再解析 led.c 的程序,这里只有一个函数 led_init,这是 LED 灯的初始化函数,其 定义如下:
/**
* @brief 初始化 LED 相关 IO 口, 并使能时钟
* @param 无
* @retval 无
**/
void led_init(void)
{
GPIO_InitTypeDef gpio_init_struct; LED0_GPIO_CLK_ENABLE(); /* LED0 时钟使能 */
LED1_GPIO_CLK_ENABLE(); /* LED1 时钟使能 */
gpio_init_struct.Pin = LED0_GPIO_PIN; /* LED0 引脚 */
gpio_init_struct.Mode = GPIO_MODE_OUTPUT_PP; /* 推挽输出 */
gpio_init_struct.Pull = GPIO_PULLUP; /* 上拉 */
gpio_init_struct.Speed = GPIO_SPEED_FREQ_HIGH; /* 高速 */
HAL_GPIO_Init(LED0_GPIO_PORT, &gpio_init_struct); /* 初始化 LED0 引脚 */
gpio_init_struct.Pin = LED1_GPIO_PIN; /* LED1 引脚 */
HAL_GPIO_Init(LED1_GPIO_PORT, &gpio_init_struct); /* 初始化 LED1 引脚 */
LED0(1); /* 关闭 LED0 */
LED1(1); /* 关闭 LED1 */
}
主函数代码
在main.c里面编写如下代码:
#include "./SYSTEM/sys/sys.h"
#include "./SYSTEM/usart/usart.h"
#include "./SYSTEM/delay/delay.h"
#include "./BSP/LED/led.h"
int main(void)
{
HAL_Init(); /*初始化HAL库*/
sys_stm32_clock_init(RCC_PLL_MUL9);/*设置时钟,72Mhz*/
delay_init(72); /*延时初始化*/
led_init(); /*初始化LED*/
while (1)
{
LED0(0); /*LED0 灭*/
LED1(1); /*LED1 亮*/
delay_ms(500); /*延时500ms*/
LED0(1); /*LED0 亮*/
LED1(0); /*LED1 灭*/
delay_ms(500); /*延时500ms*/
}
}
首先是调用系统级别的初始化:初始化 HAL 库、系统时钟和延时函数。接下来,调用 led_init 来初始化 LED 灯。最后在无限循环里面实现 LED0 和 LED1 间隔 500ms 交替闪烁一次