GPIO练习报告
任务介绍
- 要求点亮单片机上的LED1和LED2,随后让LED1和LED2交替闪烁(例如:LED1 亮0.5秒 -> LED2 亮0.5秒 -> 循环)模拟警灯效果
- 实现模式切换:通过Key1按键循环切换三种模式:
模式1(警示):上电默认状态,双灯交替闪烁
模式2(阅读):按一下 K1 进入。LED1 常亮,LED2 熄灭
模式3(睡眠):再按一下 K1 进入。LED1 熄灭,LED2 变为每 2 秒闪烁一次(亮0.2秒,灭1.8秒),表示系统正在休眠,再次按 K1,回到模式1,如此循环 - 实现亮度/频率调节:
在"阅读模式"下,使用 K2 键
短按 K2:无反应
长按 K2(超过1秒):LED1 开始快速闪烁(模拟调整中),松开后恢复常亮 - 紧急模式:无论系统当前处于什么模式(哪怕在长按 K2 的过程中),只要按下 K3,所有灯光必须立即全灭,再次按下 K3 后,系统才能恢复中断前的状态(或复位)
程序设计
本次开发使用的开发板主控芯片是GD32F407
按键模块
查阅扩展版原理图可知扩展版上的四个独立按键为共阴极连接,对应的引脚分别为PC0~PC3,且均为高电平触发

LED模块
根据原理图不难发现扩展版上有8颗LED灯,8颗LED灯为共阳极连接,且对应引脚分别为PD8~PD15,其中四颗LED为红灯,4颗LED为绿灯,因此在本程序中将所有绿灯看作LED1,将所有红灯看作LED2,

串口模块
本次程序中输出调试信息所使用的时串口0,根据GD32F4的串口引脚图可知串口0对应的发送引脚为PA9.输出引脚为PA10
同时查询GD32F4的引脚功能服复用表可得PA9和PA10需要在引脚复用后才能实现串口收发的功能
代码编写
编写思路
根据分析任务需求我们编写代码的大体思路为:先实现LED的驱动代码,即在LED.c文件中实现LED的不同闪烁模式,以提供给其它文件快速调用,对于LED的多种模式切换我们可以尝试使用全局变量进行统一存储,在主函数的循环中不断检查状态变量的变化以便及时做出状态调整,同时通过串口不断向外发送状态信息以便实时检测程序状态并做出调整,状态的变化则通过中断处理函数来进行操作,将每个按键都绑定到对应的中断上,按键触发中断后在中断函数中进行状态改变的一系列操作
LED驱动代码
由于此次使用到的LED数量较多,为了方便进行后续操作我们可以创建LED数组,将不同的LED以此存放到LED数组中,方便同一管理和后续操作
LED在进行初始化等一系列操作时主要需要的参数为:
- 对应的系统时钟
- 对应的引脚组
- 对应的引脚编号
因此可得LED结构体定义如下
cpp
typedef struct
{
rcu_periph_enum rcu;
uint32_t port;
uint32_t pin;
} Led_GPIO;
接下来定义LED数组统一存放所有LED的信息
cpp
Led_GPIO gpio_list[] = {
{RCU_GPIOC, GPIOC, GPIO_PIN_6},
{RCU_GPIOD, GPIOD, GPIO_PIN_8},
{RCU_GPIOD, GPIOD, GPIO_PIN_9},
{RCU_GPIOD, GPIOD, GPIO_PIN_10},
{RCU_GPIOD, GPIOD, GPIO_PIN_11},
{RCU_GPIOD, GPIOD, GPIO_PIN_12},
{RCU_GPIOD, GPIOD, GPIO_PIN_13},
{RCU_GPIOD, GPIOD, GPIO_PIN_14},
{RCU_GPIOD, GPIOD, GPIO_PIN_15},
};
接下来演示一下如何通过调用LED数组来快速初始化所有LED灯
cpp
void led_config(rcu_periph_enum rcu, uint32_t port, uint32_t pin)
{
rcu_periph_clock_enable(rcu);
gpio_mode_set(port, GPIO_MODE_OUTPUT, GPIO_PUPD_NONE, pin);
gpio_output_options_set(port, GPIO_OTYPE_PP, GPIO_OSPEED_MAX, pin);
}
上述为用户自定义引脚配置函数,可以根据用户传入的LED时钟信息,引脚编号以及引脚所在组对相应引脚进行初始化,函数中将引脚初始化为推挽输出模式
cpp
void led_init()
{
uint8_t count = MAX_LEN;
for (uint8_t i = 0; i < count; i++)
{
Led_GPIO tmp_gpio = gpio_list[i];
//初始化
led_config(tmp_gpio.rcu, tmp_gpio.port, tmp_gpio.pin);
//默认情况下全部拉高电平
gpio_bit_write(tmp_gpio.port, tmp_gpio.pin, SET);
}
//总开关拉低(打开)
gpio_bit_write(gpio_list[0].port, gpio_list[0].pin, RESET);
}
这个函数为用户自定义LED初始化函数,该函数通过for循环遍历LED数组中的每一个元素,并将每一个LED对应的引脚都初始化为推挽输出模式并在默认情况下拉高电平,同时将总开关打开(拉低)
接下来即可编写对应的LED的三种模式对应的控制函数,
模式一(LED1和LED2交替闪烁)对应函数:
cpp
void led_mode1()
{
//打开所有红灯
turn_on_red();
//保持500ms
delay_1ms(500);
//关闭所有红灯
turn_off_red();
//打开所有绿灯
turn_on_green();
//保持500ms
delay_1ms(500);
//关闭所有绿灯
turn_off_green();
//打开所有红灯
turn_on_red();
}
模式二(LED1关闭,LED2开启)对应函数:
cpp
void led_mode2()
{
//关闭所有红灯
turn_off_red();
//打开所有绿灯
turn_on_green();
}
模式三(LED1快闪,LED2慢闪)对应函数:
cpp
void led_mode3()
{
//关闭所有绿灯
turn_off_green();
turn_on_red();
delay_1ms(200);
turn_off_red();
delay_1ms(1800);
}
中间模式(LED1快速闪烁)对应函数:
cpp
//绿灯快速闪烁
void led_spark_quick()
{
turn_off_green();
delay_1ms(200);
turn_on_green();
delay_1ms(200);
}
关闭所有LED灯对应函数:
cpp
//关闭所有LED灯
void turn_off_all_leds(void)
{
for (uint8_t i = 1; i < MAX_LEN; i++)
{
led_turn_off(i);
}
}
总结
至此LED模块驱动代码已经完成,在程序中为了方便统一管理多个LED灯我们选择了创建LED结构体并同时创建了LED数组,这种统一的管理方式减少了代码的重复性(在初始化和关闭所有LED灯时效果显著),同时也提高了代码的可读性
按键中断代码
程序中的三个按键负责改变程序中部存储状态的全局变量的值,在主函数中通过轮询的方式进行按键的状态检测对资源的消耗较大,且不便于我们去改变LED的状态,因此我们通过按键触发中断的模式在终端函数中对全局变量中存储的内容进行修改
- 在中断函数头文件中声明存储LED模式状态的全局变量
cpp
extern uint8_t Key1_flag_backup; // 保存中断前的模式状态
extern uint8_t Key2_flag_backup; // 保存中断前的按键状态
extern uint8_t force_off_mode; // 强制熄灭模式标志
extern uint8_t Key1_flag; //保存模式状态变量
extern uint8_t Key2_flag; //保存按键2状态
中断配置函数
将对应的按键绑定到对应的中断线上,当按键按下时中断触发,这里只展示按键一的中断配置,其它按键的中断配置与中断一类似
cpp
//中断一配置(PC0按键中断配置---------------------------------------------
/********************* GPIO config *********************/
// 时钟初始化
rcu_periph_clock_enable(RCU_GPIOC);
// 配置GPIO模式
gpio_mode_set(GPIOC, GPIO_MODE_INPUT, GPIO_PUPD_PULLUP, GPIO_PIN_0);
/********************* EXTI config *********************/
// 时钟配置
rcu_periph_clock_enable(RCU_SYSCFG);
// 配置中断源
syscfg_exti_line_config(EXTI_SOURCE_GPIOC, EXTI_SOURCE_PIN0);
// 中断初始化
exti_init(EXTI_0, EXTI_INTERRUPT, EXTI_TRIG_FALLING);
// 配置中断优先级
nvic_irq_enable(EXTI0_IRQn, 3, 3);
// 使能中断
exti_interrupt_enable(EXTI_0);
// 清除中断标志位
exti_interrupt_flag_clear(EXTI_0);
由上述代码不难看出中断配置的步骤为:
- 初始化时钟
- 配置中断源(将中断绑定到对应的引脚上)
- 中断初始化(配置相应中断线,并选择触发方式)
- 配置中断优先级(可配置响应优先级和抢占优先级)
- 使能中断(使能对应中断)
- 清除中断标志位(防止中断重复触发)
中断函数
中断函数的作用为当对应按键按下后便会执行一次对应的中断函数,因此中断函数在程序中就负责修改对应的状态变量
不同的中断对应不同的中断函数,中断也都有着固定的中断函数名,中断函数名可以在程序的CMSIS目录下的.s启动文件中进行查找,一下则是中断0到中断3对应的中断函数名
- 在EXTI.c文件中编写对应的中断函数逻辑,在本程序中模式的循环通过
Key1_flag变量进行实现,当Key1_flag的值为1时对应模式1,当Key1_flag的值为2时对应模式2,以此类推,因此在按键一对应的中断函数中我们需要进行的操作即为循环递增Key1_flag的值,因此中断0对应的中断函数的内容为
cpp
void EXTI0_IRQHandler(void)
{
if (SET == exti_interrupt_flag_get(EXTI_0))
{
// 添加消抖延时
for (volatile uint32_t i = 0; i < 10000; i++)
;
// 判断当前电平状态
if (gpio_input_bit_get(GPIOC, GPIO_PIN_0) == RESET)
{
// 只有低电平时才计数(按下状态)
if (Key1_flag < 3)
{
Key1_flag++;
}
else
{
Key1_flag = 1;
}
}
exti_interrupt_flag_clear(EXTI_0);
}
}
- 程序中
Key2_flag的作用为当LED在状态2时判定是否需要进入LED1快闪的状态,任务要求中当LED处在模式2时当按下按键2超过1秒钟后LED1则进入快闪状态,松开按键后则恢复常量状态,因此按键2对应的中断函数的作用即为判断按键2按下时长是否超过1秒种,并在条件满足时修改Key2_flag的值,因此中断1的中断函数内容如下
cpp
void EXTI1_IRQHandler(void)
{
if ((Key1_flag == 2) && (SET == exti_interrupt_flag_get(EXTI_1)))
{
FlagStatus cur_state = gpio_input_bit_get(GPIOC, GPIO_PIN_1);
// 如果当前状态为低电平,即为按下状态时延时判断是否按下1秒钟
if (cur_state == RESET)
{
delay_1ms(1000);
if (gpio_input_bit_get(GPIOC, GPIO_PIN_1) == RESET)
{
Key2_flag = 1;
}
}
//如果当前状态为高电平,即为抬起状态时如果按下的时长大于1秒钟则恢复绿灯常亮状态
else if(cur_state == SET)
{
if (Key2_flag == 1)
{
Key2_flag = 2;
}
}
exti_interrupt_flag_clear(EXTI_1);
}
}
观察上述代码不难发现我在中断种进行了1秒钟的延时操作,,这里一方面是进行一次错误示范,另一方面是当时我学习储备不足,并没有想出更好的解决方案。
- 按键三的作用为触发紧急模式,当触发紧急模式后LED1和LED2都会强制熄灭,再次按下按键三后即可回到紧急模式前的状态,因此在中断触发前需要先保存响应标志位内存储的状态信息
cpp
//保存中断前Key1的状态
uint8_t Key1_flag_backup = 1;
//保存终端前Key2的状态
uint8_t Key2_flag_backup = 0;
//强制熄灭标志
uint8_t force_off_mode = 0;
随后即可在中断函数中进行状态位的修改操作了,函数内容如下
cpp
void EXTI2_IRQHandler(void)
{
if (SET == exti_interrupt_flag_get(EXTI_2))
{
// 添加消抖延时
for (volatile uint32_t i = 0; i < 10000; i++)
;
// 判断当前电平状态
if (gpio_input_bit_get(GPIOC, GPIO_PIN_2) == RESET)
{
//保存状态并强制熄灯
if (force_off_mode == 0)
{
Key1_flag_backup = Key1_flag;
Key2_flag_backup = Key2_flag;
force_off_mode = 1;
turn_off_all_leds();
}
//回溯状态
else
{
force_off_mode = 0;
Key1_flag = Key1_flag_backup;
Key2_flag = Key2_flag_backup;
}
}
//清理中断标志位
exti_interrupt_flag_clear(EXTI_2);
}
}
中断标志位的作用
中断标志位的作用为记录中断时间的发生,每个中断都对应着自己的中断标志位,当中断发生时对应的中断标志位的值就会发生变化,每次中断函数在触发时都会检测中断标志位的状态,如果中断标志位的值为1,那么就进行中断函数的后续操作,否则直接返回,因此在中断初始化时需要将中断标志位清理为0,每次中断函数调用后也需要将中断标志位清理为0
为什么中断函数中不应该出现延时操作
因为这种做法在实际开发过程种是不可取的,在中断函数的编写应该遵循快进快出的原则,如果在中断中进行一秒钟的延时那么在接下来的一秒钟内所有中断优先级大于当前中断的中断都无法触发,且会占用大量CPU资源,当前程序开发使用的标准库的开发模式,因此在按键2的中断函数中我使用了延时函数貌似并没有造成什么严重后过,但是如果本程序采用的是HAL库进行开发那么就会导致程序死锁,原因为:HAL库中的延时函数HAL_Delay()函数依赖SysTick中断更新计时,在HAL库程序都会在主函数中调用函数HAL_Init()进行初始化操作,在该函数中配置了SysTick每1ms触发一次中断,在SysTick的中断函数中会调用HAL_IncTick()函数使全局变量uwTick自增1,而HAL_Delay()函数的实现逻辑即为通过HAL_GetTick()获取当前程序中uwTick的的值,并创建临时变量存储当前uwTick的值,随后在while循环中不断调用HAL_GetTick()获取uwTick的值并与临时变量的值进行减法运算,例如在调用函数时传入参数500,那么函数内部就会循环判断,直到当前程序中的uwTick值比最初的uwTick大500时停止循环。因此HAL库中的延时函数依赖SysTick的中断进行函数功能的实现,但是SysTick中断默认优先级往往时最低的,因此在用户中断出发时延时函数就会等待SysTick中断更新uwTick的值,但是由于中断优先级过低,导致中断无法触发,因此程序就会进入死锁状态,原理图如下

串口驱动代码
虽然本程序中并没有要求去使用串口,但是串口在程序中也发挥着十分重要的作用,由于本程序中状态的切换都是依赖全局变量来实现的,因此全局变量应该代表着程序的状态,因此通过串口输出全局变量的值即可实现检测状态的功能,同时串口还可以用作错误排查等,因此在本程序中同样初始化并调用了串口
串口初始化
- 串口引脚初始化操作与其它外设基本相同,但是在引脚配置时需要进行引脚功能的复用配置以实现收发功能,在引脚初始化配置时需要将对应引脚配置为复用模式7,采用推挽输出
cpp
// gpio初始化
rcu_periph_clock_enable(RCU_GPIOA);
// PA9 TX
gpio_mode_set(GPIOA, GPIO_MODE_AF, GPIO_PUPD_NONE, GPIO_PIN_9);
gpio_af_set(GPIOA, GPIO_AF_7, GPIO_PIN_9);
gpio_output_options_set(GPIOA, GPIO_OTYPE_PP, GPIO_OSPEED_50MHZ, GPIO_PIN_9);
// PA10 RX
gpio_mode_set(GPIOA, GPIO_MODE_AF, GPIO_PUPD_NONE, GPIO_PIN_10);
gpio_af_set(GPIOA, GPIO_AF_7, GPIO_PIN_10);
gpio_output_options_set(GPIOA, GPIO_OTYPE_PP, GPIO_OSPEED_50MHZ, GPIO_PIN_10);
- 串口初始化主要需要配置的内容为
- 使能串口时钟
- 复位USART0寄存器
- 设置波特率
- 设置通信格式(包括有奇偶校验位,有效数据长度以及停止位)
- 数据传输顺序
- 使能发送和接收功能
- 配置中断优先级
- 开启串口
重定向printf函数
为了更贴合C语言的代码风格,也为了简化调用流程我们可以尝试重写fputc函数,这个函数是printf的底层逻辑函数,重写该函数后可以改变printf函数的输出逻辑,在本程序中我们改变了fputc函数的输出方向,让该函数将收到的数据通过串口输出出去,以实现后期的调用操作
cpp
void send_byte(uint8_t data)
{
// 将data发送出去
usart_data_transmit(USART0, data);
// 如果数据发送未完成,需要阻塞(返回0是失败,1是成功)
while (0 == usart_flag_get(USART0, USART_FLAG_TBE))
;
}
//重写printf函数依赖函数
int fputc(int ch, FILE *f)
{
send_byte((uint8_t)ch);
return ch;
}
注意事项
想要实现重定向功能需要对项目进行相应配置,在keil中勾选如下选项即可
主函数代码
上述外设全部初始化完成并提供调用接口后便可以在主函数中进行统一调用以实现最终逻辑,主函数中需要在while循环中不断检查各个全局变量存储的状态位信息,同时输出状态位信息用来调试
cpp
uint8_t Key1_flag = 1;
uint8_t Key2_flag = 0;
int main(void)
{
// 系统滴答定时器初始化
systick_config();
//LED初始化
led_init();
// 串口初始化
usart0_config();
//中断初始化
EXTI_config();
while (1)
{
printf("Key1_flag: %d\n", Key1_flag);
printf("Key2_flag: %d\n", Key2_flag);
printf("Off_flag: %d", force_off_mode);
if (force_off_mode == 1)
{
turn_off_all_leds();
delay_1ms(100);
continue;
}
switch (Key1_flag)
{
case 1:
led_mode1();
break;
case 2:
if (Key2_flag == 1)
{
led_spark_quick();
}
else if(Key2_flag == 2)
{
turn_on_green();
}
else
{
led_mode2();
}
break;
case 3:
led_mode3();
break;
default:
break;
}
}
}
程序总结
当前程序通过状态位存储状态信息,主函数中执行循环并在循环内依次检测状态位的信息,根据状态位的信息变化进行模式切换,,每个按键都绑定到了对应的中断线上,当中断触发时在不同的中断函数中对不同的中断标志位进行修改,修改后主循环内会据变化改变LED灯的状态