GPIO各种点亮LED
# 点亮LED
# 对 CubeMX的配置
对应LED三色光的分别是 PH11, PH12 和 PH13,在设定的时候对其进行更改,OUTPUT
模式是必然的,而对于GPIO 的配置也很关键:
如图:三个引脚均为此配置。推挽输出、强电流是常用的模式,但是为什么要在这个地方使用上拉电阻呢?
首先回答这个问题,由于这种弱上拉对输出起到的作用很微小,不能影响其输出高低电位,而其仅有的两个作用则是:(至于这两个特性如何体现,请往下读)
上下拉对输出的作用
提高驱动能力
例如,用单片机输出高电平,但由于后续电路的影响,输出的高电平不高,就是达不到VCC,影响电路工作。所以要接上拉电阻。下拉电阻情况相反,让单片机引脚输出低电平,结果由于后续电路影响输出的低电平达不到GND,所以接个下拉电阻。
在单片机引脚电平不定的时候, 让后面有一个稳定的电平
例如上面接下拉电阻的情况下,在单片机刚上电的时候,电平是不定的,还有就是如果你连接的单片机在上电以后,单片机引脚是输入引脚而不是输出引脚,那这时候的单片机电平也是不定的,R18的作用就是如果前面的单片机引脚电平不定的话,强制让电平保持在低电平。
而需要了解的则是在Robomaster C板中 LED 本身有的一个下拉电阻:
由于 GPIO 本身的输出电流是很低的,所以采取外接电源的方式供电 LED。对于三极管,它的作用就是利用 IO 口输出电平的高低决定线路的通断,而红圈圈出的下拉电阻作用很大:
在输出低电平的时候,将控制级的点位下拉接地
在输出高电平的时候,起分压作用防止控制级电位过低
还记得刚刚提到的上拉电阻的弱上拉作用吗,为了防止输出的电压过低达不到要求,所以此处 GPIO 也设置为上拉
pull-up
还有一点就是对于 IO 口的命名,命名的话在生成的工程中也会有一个定义:
- 如原来的
GPIOH
,在命名后就是LED_R_GPIO_PORT
也就是会在LED_R
后面加上一个_GPIO_PORT
,但是为了同意组号,还是使用GPIOH
比较好 - IO 口就更简单,就是在
LED_R
之后加上_PIN
即LED_R_PIN
,使用 IO 口的名称将会便于阅读程序
# 实现代码
while (1)
{
//set GPIO output high level
HAL_GPIO_WritePin(LED_R_GPIO_Port, LED_R_Pin, GPIO_PIN_SET);
HAL_GPIO_WritePin(LED_G_GPIO_Port, LED_G_Pin, GPIO_PIN_SET);
HAL_GPIO_WritePin(LED_B_GPIO_Port, LED_B_Pin, GPIO_PIN_SET);
}
2
3
4
5
6
7
详细的函数说明请见这里,这个操作就是将三原色的灯都点亮,LED 显示白色。
# 闪烁—延时
为了实现 LED 可以闪烁而不是一直亮着,我们需要对程序延时,达到这一目的可以使用自定义或自带函数两种方法:
# 自定义延时函数
自定义延时函数也有两种方法:
第一种方法是执行嵌套的空for
语句达到延时的目的:
void user_delay_us(uint16_t us)
{
for (; us > 0; us--)
{
for (uint8_t i = 50; i > 0; i--)
{
; // Empty loop body
}
}
}
2
3
4
5
6
7
8
9
10
这个函数可以实现毫秒级(1ms = 1000μs)的延时,如果想用这样的方式实现微秒级的延时可以定义函数user_delay_ms
,调用微秒级的延时函数,并将参数设为1000即可:
void user_delay_ms(uint16_t ms)
{
for (; ms > 0; ms--)
{
user_delay_us(1000);
}
}
2
3
4
5
6
7
第二种方法则是通过执行空指令_nop()
来实现的:
void nop_delay_us(uint16_t us)
{
for (; us > 0; us--)
{
for (uint8_t i = 10; i > 0; i--)
{
_nop();
_nop();
_nop();
_nop();
_nop();
_nop();
_nop();
_nop();
_nop();
_nop();
_nop();
_nop();
_nop();
_nop();
_nop();
}
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
这个也会实现毫秒级的延时,如果要实现微秒级,方法与上面相同;
# 库函数
就是函数HAL_Delay()
关于它本身不再多说,请见这里。而我们要了解它背后是什么:
我们需要简要的介绍一下滴答计时器(SysTick):
在main
函数中有一个HAL_INIT()
,在这这个函数中STM32实现了 SysTick 以及底层硬件的初始化。对 SysTick 的初始化为:HAL_InitTick(TICK_INT_PRIORITY)
,这个函数将设定 SysTick 的定时周期为1ms,即频率为1000Hz,并使SysTick开始工作。每当滴答计时器递减到 0 时,会触发中断,使程序进入SysTick中断处理函数SysTick_Handler(void)
,这个函数中又只会调用HAL_IncTick()
函数,其内容是:
__weak void HAL_IncTick(void)
{
uwTick += uwTickFreq;
}
2
3
4
由于每个1ms的中断都会为uwTick
加上1ms的值,所以uwTick
变量中存储的是从STM32的 Systick 初始化以来所经过的时间(ms),uwTick
的存在相当于给整个程序提供了一个绝对的时间基准,而HAL_Delay()
函数延时功能便是通过uwTick的值完成的
如果想要在你的程序中也访问uwTick
的值,可以使用HAL_GetTick()
函数
HAL_Delay()
的优点是不用我们自己去自定义,而缺点在于只能到毫秒级
# 底层
# GPIO外设结构体
HAL 库为每个外设(GPIO 除外)创建了两个结构体,一个是外设初始化结构体,一个是外设句柄结构体,其中 GPIO 没有句柄结构体。这两个结构体都是定义在外设对应的驱动头文件中,比如stm32f4xx_hal_usart.h
文件。
初始化结构一般是做为句柄结构体的一个成员通过指针被引用,而句柄结构体则在外设 HAL 函数库实现被使用,比如在stm32f4xx_hal_usart.c
文件。这两个结构体内容几乎包括了外设的所有可选属性,理解这两个结构体内容对我们编程非常有帮助。
GPIO 外设只有一个初始化结构体,没有句柄结构体,所以 GPIO 初始化结构体直接在stm32f4xx_hal_gpio.c
文件中与相关初始化函数配合使用完成 GPIO 外设初始化配置
其代码如下:
typedef struct {
02 uint32_t Pin; /*GPIO 引脚编号选择 */
03 uint32_t Mode; /*GPIO 引脚工作模式 */
04 uint32_t Pull; /*GPIO 引脚上拉、下拉配置 */
05 uint32_t Speed; /*GPIO 引脚最大输出速度 */
06 uint32_t Alternate; /*GPIO 引脚的复用 */
07 } GPIO_InitTypeDef;
2
3
4
5
6
7
# 对GPIO编程的流程
如果使用 CubeMX 直接生成代码的话,这些过程是直接被省略的,但是为了深入理解它到底是如何工作的,我们还是来学习一下:
首先需要定义一个硬件初始化结构体:
GPIO_InitTypeDef GPIO_InitStruct;
1有标准库那味儿了吧
随后需要开启 LED 对应引脚对应的 IO 端口时钟:
__HAL_RCC_GPIOx_CLK_ENABLE()
1设置对应引脚首先输出低电位
HAL_GPIO_WritePin(LED1_GPIO, LED1_GPIO_PIN, GPIO_PIN_RESET);
1初始化结构体内参数
/* 设定LED1 对应引脚IO 编号 */ GPIO_InitStruct.Pin = LED1_GPIO_PIN; /* 设定LED1 对应引脚IO 为输出模式 */ GPIO_InitStruct.Mode = GPIO_MODE_OUTPUT_PP; /* 设定LED1 对应引脚IO 操作速度 */ GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_LOW; /* 初始化LED1 对应引脚IO */ HAL_GPIO_Init(LED1_GPIO, &GPIO_InitStruct);
1
2
3
4
5
6
7
8配置到硬件
HAL_GPIO_Init(LED3_GPIO, &GPIO_InitStruct);
1
# 参考
[1] Robomaster 开发板C型教程
[2] HAL 用户手册
[3] 硬石科技 YS-F4Pro 开发手册