介绍
在单片机中,LED是最常见的电元件,通常的控制方式就是通过GPIO的推挽输出来控制LED的亮灭,对于LED而言,本身只有亮灭两种状态,但是利用单片机和人类视觉暂离的效果,LED可以实现亮灭、闪烁、PWM呼吸灯输出。然后再结合现实的一些场景需求,来看看LED的代码到底能写成什么样。
点亮LED
我使用的是STM32F103C8T6这款单片机来作为代码演示的设备,首先,使用GPIO来点亮一个LED
这是我的LED的控制原理图,可以看到PC13输出低电平,LED灯亮,PC13输出高电平,LED灯灭。
下面这段代码写了一个很简单的LED的闪烁的代码,通过延时函数,实现LED每500ms的亮灭
int main(void)
{
/*NVIC中断分组*/
NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2);//配置NVIC为分组2
timer_systime_init();
gpio_led_init();
while (1)
{
led_set_status(LED_ON);
mcu_time_delay_ms(500);
led_set_status(LED_OFF);
mcu_time_delay_ms(500);
}
}
需求:通过不同的闪烁时间来表示不同的信息,且不影响程序运行
现在想做一个能够随着事件触发来同时改变LED的闪烁时长,从而表现不同的信息。那么就需要一个通用的闪烁函数来对LED进行设置和一个函数来获取LED的当前的状态进行状态改变。
需求分析:
- 不同的闪烁时间,亮灭的时间也可能不一致
- 不影响程序运行,不能使用延时来等待时间,不能一直轮询的去查看LED是否到时间该变化了
程序设计:
- 设计一个符合此需求的LED的类
- 根据需求实现对于的LED的功能,看来是需要定时器中断来搭配实现了
首先设置LED的类设计
#include <stdint.h>
#include <stdio.h>
typedef void (*led_set_status_fun) (uint8_t);
#define LED_ON Bit_RESET
#define LED_OFF Bit_SET
typedef enum
{
LED_GREEN = 0,
LED_RED,
LED_ID_MAX//在这之前添加
}LED_ID;
typedef enum
{
LED_MODE_OFF = 0,
LED_MODE_ON ,
LED_MODE_FLICKER_BY_SAME,
LED_MODE_FLICKER_BY_DIFF,
LED_MODE_MAX
}LED_MODE;
typedef struct
{
LED_ID led_id :8; //设备号
LED_MODE mode :8; //模式
uint8_t status; //状态 LED_OFF:关闭 LED_ON:打开 内部使用外部无需初始化
uint8_t reserve; //保留
union flicker
{
struct diff_time
{
uint16_t on_time;
uint16_t off_time;
}diff_time;
uint32_t same_time;
} flicker; //根据模式决定初始化
uint32_t change_status_time;//改变状态的时间 内部使用外部无需初始化
led_set_status_fun set_status_fun;//led控制回调函数
} LED;
LED的核心函数 mcu_time_get_time函数是一个获取系统运行的时间戳的函数,由定时器实现1ms的中断累加。
static LED kernel_led[LED_ID_MAX] = {0};//内核LED对象组
/**
* @description: 设置LED的状态函数
* @time: 2024-10-27 15:10:51
* @param: 参数
* @return: 返回值
*/
void green_led_set_status(uint8_t status)
{
GPIO_WriteBit(GPIOC, GPIO_Pin_13, (BitAction)(status));
}
/**
* @brief : 设置LED对象
* @param :
* @note :
* @return: 0 成功 1失败
*/
uint8_t led_set(LED *led)
{
//1. 判断LED对象是否为空
if(led == NULL)
return 1;
//2. 设置LED对象
kernel_led[led->led_id].mode = led->mode;
kernel_led[led->led_id].set_status_fun = led->set_status_fun;
if(led->mode == LED_MODE_ON || led->mode == LED_MODE_OFF)
{
kernel_led[led->led_id].set_status_fun(led->status);
}
else if(kernel_led[led->led_id].mode == LED_MODE_FLICKER_BY_SAME)
{
kernel_led[led->led_id].flicker.same_time = led->flicker.same_time;
kernel_led[led->led_id].change_status_time = mcu_time_get_time();
}
else if(kernel_led[led->led_id].mode == LED_MODE_FLICKER_BY_DIFF)
{
kernel_led[led->led_id].flicker.diff_time.on_time = led->flicker.diff_time.on_time;
kernel_led[led->led_id].flicker.diff_time.off_time = led->flicker.diff_time.off_time;
kernel_led[led->led_id].change_status_time = mcu_time_get_time();
}
else
{
return 1;
}
return 0;
}
/**
* @brief : 关闭一个LED对象
* @param : 设备号 对应枚举LED_ID
* @note :
* @return:
*/
void led_reset(uint8_t led_id)
{
//1.关闭此LED
kernel_led[led_id].set_status_fun(LED_OFF);
//2.重置LED对象信息
kernel_led[led_id].mode = LED_MODE_OFF;
kernel_led[led_id].flicker.same_time = 0;
kernel_led[led_id].status = LED_OFF;
kernel_led[led_id].change_status_time = 0;
}
void led_task(void *obj)
{
//1.遍历所有LED对象,找到需要改变状态的LED
for(uint8_t i = 0; i < LED_ID_MAX; i++)
{
//2.判断此LED是否需要改变状态
if(kernel_led[i].change_status_time != 0 && kernel_led[i].change_status_time <= mcu_time_get_time())
{
//3.根据LED模式设置LED状态
switch (kernel_led[i].mode)
{
case LED_MODE_FLICKER_BY_SAME:
kernel_led[i].set_status_fun(!kernel_led[i].status);
kernel_led[i].change_status_time = mcu_time_get_time() + kernel_led[i].flicker.same_time;
kernel_led[i].status = !kernel_led[i].status;
break;
case LED_MODE_FLICKER_BY_DIFF:
if(kernel_led[i].status == LED_ON)
{
kernel_led[i].set_status_fun(!kernel_led[i].status);
kernel_led[i].change_status_time = mcu_time_get_time() + kernel_led[i].flicker.diff_time.off_time;
}
else
{
kernel_led[i].set_status_fun(!kernel_led[i].status);
kernel_led[i].change_status_time = mcu_time_get_time() + kernel_led[i].flicker.diff_time.on_time;
}
kernel_led[i].status = !kernel_led[i].status;
break;
}
}
}
}
简单的功能示例
void test_01(void)
{
uint8_t static step = 0;
switch (step)
{
case 0:
{
LED led_green;
led_green.set_status_fun = green_led_set_status;
led_green.led_id = LED_GREEN;
led_green.mode = LED_MODE_FLICKER_BY_DIFF;
led_green.flicker.diff_time.on_time = 1000;
led_green.flicker.diff_time.off_time = 2000;
led_set(&led_green);
g_mcu_timer[DELAY_TIMER] = 5000;
step++;
}
break;
case 1:
if(g_mcu_timer[DELAY_TIMER] == 0)
{
step++;
}
break;
case 2:
led_reset(LED_GREEN);
LED led_green;
led_green.set_status_fun = green_led_set_status;
led_green.led_id = LED_GREEN;
led_green.mode = LED_MODE_FLICKER_BY_SAME;
led_green.flicker.same_time = 1000;
led_set(&led_green);
step++;
break;
case 3:
if(mcu_time_get_time() == 300*1000)
{
step = 0;
}
default:
break;
}
}
int main(void)
{
/*NVIC中断分组*/
NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2);//配置NVIC为分组2
//GPIO初始化
timer_systime_init();
gpio_led_init();
//设备初始化
mcu_time_delay_ms(1);
//程序初始化
while (1)
{
test_01();
led_task(NULL);
}
}
这样,可以在不同时间去设置LED的属性,然后实现LED的复杂功能。但是也可以看到设置LED的部分代码就比较繁琐复杂,想要LED实现复杂的亮灭逻辑需要大量的代码去实现,比如一个LED先是亮200ms在灭300ms在亮400ms最后在灭500ms。这样的一个周期轮回,就不太适合用了。所有可以利用状态机的方式去表示一个LED的周期的状态,从而实现方便快捷的设置。
需求:实现多样变化的LED闪烁功能
其实想一想,发现对于LED而言,其实就只有开关两个状态,变化的只是这两个状态的持续时间,所以为什么不写一个数组来传递给LED去自动读取下一步该做的事呢?
举个简单的例子,一个LED先是亮200ms在灭300ms在亮400ms最后在灭500ms,转换成一个数组就是[{ON,200}, {OFF,300}, {ON,400},{OFF,500}]。如果说需要LED周期性的这样去做,那么我们可以加一个属性loop用来表示循环次数。
loop表示循环的次数 0 表示一直循环 1-N 表示限定次数。 我们对上面的代码进行修改,让每个LED一开始都初始化一次。后续只要修改每个LED的状态数组就可以实现LED的多样变化,需要注意的是,LED的数组使用的是同一个就要考虑加锁,防止LED的数组在修改的时候也在读取。
#ifndef _LED_H_
#define _LED_H_
#ifdef __cplusplus
extern "C" {
#endif
#include <stdint.h>
#include <string.h>
#include <stdio.h>
typedef void (*led_set_status_fun) (uint8_t); //LED的状态控制函数指针
#define LED_ACTION_MAX_NUM 10
#define LED_ON 1
#define LED_OFF 0
#define LED_LOCK 1
#define LED_UNLOCK 0
#define LED_WORK 1
#define LED_STOP 0
typedef enum
{
LED_GREEN = 0,
LED_RED,
LED_ID_MAX//在这之前添加
}LED_ID;
typedef enum
{
LED_MODE_OFF = 0,
LED_MODE_ON ,
}LED_MODE;
typedef struct
{
uint8_t status; //亮灭
uint32_t time; //时长
}LED_ACTION;
typedef struct
{
uint8_t queue_schedule; //队列的进度
uint8_t led_status; //led的状态 0 停止 1工作
uint8_t led_lock; //操作的锁
uint16_t loop; //循环次数 0表示死循环
uint16_t led_action_length; //执行的状态队列的长度
uint32_t timepiece; //计时器
led_set_status_fun set_status_fun; //led控制回调函数
LED_ACTION led_action_queue[LED_ACTION_MAX_NUM]; //led的状态队列
} LED;
void led_set_callback_fun(uint8_t led_id, led_set_status_fun fun);
uint8_t led_set_run_queue(LED_ID led_id, LED_ACTION * led_action_queue, uint16_t led_action_length, uint16_t loop);
void led_task(void);
#ifdef __cplusplus
}
#endif
#endif
#include "led.h"
static LED kernel_led[LED_ID_MAX] = {0};//内核LED对象组
//初始化LED的控制回调函数
void led_set_callback_fun(uint8_t led_id, led_set_status_fun fun)
{
kernel_led[led_id].set_status_fun = fun;
}
uint8_t led_set_run_queue(LED_ID led_id, LED_ACTION *led_action_queue, uint16_t led_action_length, uint16_t loop)
{
//1.数据校验
if(led_id >= LED_ID_MAX || led_action_queue == NULL || led_action_length >= LED_ACTION_MAX_NUM)
{
return 1;
}
while(kernel_led[led_id].led_lock == LED_LOCK)
{
}
kernel_led[led_id].led_lock = LED_LOCK;//设备加锁
//2.数据写入
for(uint16_t i = 0; i < led_action_length; i++)
{
kernel_led[led_id].led_action_queue[i].time = led_action_queue[i].time;
kernel_led[led_id].led_action_queue[i].status = led_action_queue[i].status;
}
kernel_led[led_id].led_status = LED_WORK;
kernel_led[led_id].led_action_length = led_action_length;
kernel_led[led_id].loop = loop;
kernel_led[led_id].timepiece = 0;
kernel_led[led_id].queue_schedule = 0;
kernel_led[led_id].led_lock = LED_UNLOCK;//设备解锁
return 0;
}
void led_task(void)
{
//1.遍历所有LED对象,找到需要改变状态的LED
for(uint8_t i = 0; i < LED_ID_MAX; i++)
{
//2.查看LED是否在工作
if(kernel_led[i].led_status == LED_WORK && kernel_led[i].led_lock == LED_UNLOCK)
{
kernel_led[i].led_lock = LED_LOCK;
if(kernel_led[i].timepiece == 0)
{
kernel_led[i].set_status_fun(kernel_led[i].led_action_queue[kernel_led[i].queue_schedule].status);
kernel_led[i].timepiece = kernel_led[i].led_action_queue[kernel_led[i].queue_schedule].time;
if(kernel_led[i].queue_schedule == kernel_led[i].led_action_length - 1)
{
if(kernel_led[i].loop != 0)
{
kernel_led[i].loop--;
if(kernel_led[i].loop == 0)
{
kernel_led[i].led_status = LED_STOP;
}
}
kernel_led[i].queue_schedule = 0;
}
else
{
kernel_led[i].queue_schedule++;
}
}
else
{
kernel_led[i].timepiece -= 1;//这里减去的数位此函数调用的间隔时间,要保证调用的间隔内这个函数一定要跑完,可以设置为10ms以上,如果数量在10个以上
}
kernel_led[i].led_lock = LED_UNLOCK;
}
}
}
#include "led.h"
#include <windows.h>
void redled_set_status(uint8_t status)
{
if(status == LED_ON)
{
printf("LED NOW IS ON ! \r\n");
}
else
{
printf("LED NOW IS OFF ! \r\n");
}
}
int main()
{
led_set_callback_fun(LED_RED, redled_set_status);
LED_ACTION red_led_queue[4] = {{LED_ON,5},{LED_OFF,10},{LED_ON,10},{LED_OFF,5}};
led_set_run_queue(LED_RED, red_led_queue, 4, 10);
while(1)
{
led_task();
Sleep(100);//休眠100ms
}
}
这样的话,只要两行代码就能设置LED,同时只要一个定时器中断的周期任务调用或者一个线程任务的调用就可以实现一个LED的多样复杂的动态显示。当然还有呼吸灯的效果也可以加进来,只是代码更复杂一些。
对于LED的结构体的内存,可以根据实际需求进行修改。
多个LED组合,实现花式表演
这个需要考虑LED之间的同步互斥,代码的实时性要求很高,一般都是在RTOS系统上跑,可以以OLED屏为例,在基础的驱动程序上,搭配双缓冲机制刷屏,有些代码都能实现30fps的动画效果。
对于多个LED,常见的就是红绿灯表示设备状态了,这个只需要简单的逻辑判断就行。复杂的LED的效果可能就需要对LED进行分组、分类,要考虑很多。