单片机中的通用LED驱动

前言

项目中需要用到很多的LED灯,存在不同的闪烁方式,比如单闪,双闪,快闪,慢闪等等,我需要一个有如下特性的LED驱动

  • 方便的增加不同闪烁模式
  • 可以切换闪烁模式
  • 增加LED数目不会有太多的改动
  • 方便移植,要有良好的硬件对接接口

好,那就开整吧。
PS:本文中的程序源码只做演示,可运行的代码文末有链接

数据结构分析

首先考虑一颗LED的相关数据结构。
显然构建LED结构体应该有on,off接口,如下

typedef struct{void (*init)(void);	//初始化EDvoid (*on)(void);	//打开LEDvoid (*off)(void);	//关闭LED
}led_t;

LED闪烁是亮灭的交替,我们可以关注其中的两个参数,

  • LED亮起时长,标记为ontime
  • LED闪烁周期,标记为cycle
    在这里插入图片描述
    将这两个参数抽象为led_mode_t结构体
typedef struct {uint16_t cycle;		//LED闪烁周期uint16_t ontime;	//LED亮起的时长
}led_mode_t;

一颗LED可能会有很多闪烁模式,不同LED闪烁模式数量不同,所以当我们将led_mode_t集成到led_t的时候,应该采用指针形式,在运行的时候申请该结构的内存。由此,丰富led_t结构体如下

typedef struct{void (*init)(void);	//初始化EDvoid (*on)(void);	//打开LEDvoid (*off)(void);	//关闭LEDled_mode_t *mode;		//LED闪烁模式具体的数据uint8_t mode_count;		//LED闪烁模式个数uint8_t mode_current;	//当前闪烁模式编码
}led_t;

OK,LED结构体初步搭建完毕,接下来假设我们有4颗LED,每一颗闪烁的时间参数如下

  • 快闪:200ms亮起,200ms熄灭,周期400ms
  • 慢闪:500ms亮起,500ms熄灭,周期1000ms
  • 单闪:100ms亮起,900ms熄灭,周期1000ms
  • 双闪:30ms亮起, 70ms熄灭,30ms亮起, 870ms熄灭,周期1000ms

前三个都好说,最后一个需要简单分析一下。双闪可以看作两种闪烁模式的切换。第一种30ms亮起, 70ms熄灭,周期100ms。第二种30ms亮起, 870ms熄灭,周期900ms。
在这里插入图片描述

程序框架搭建

所以,初始化该情况下的代码如下

typedef struct {uint16_t cycle;		//LED闪烁周期uint16_t ontime;	//LED亮起的时长
}led_mode_t;typedef struct{void (*init)(void);	//初始化EDvoid (*on)(void);	//打开LEDvoid (*off)(void);	//关闭LEDled_mode_t *mode;		//LED闪烁模式具体的数据uint8_t mode_count;		//LED闪烁模式个数uint8_t mode_current;	//当前闪烁模式编码
}led_t;led_t led_array[4];void led0_init(void){}
void led1_init(void){}
void led2_init(void){}
void led3_init(void){}
void led0_on(void){}
void led1_on(void){}
void led2_on(void){}
void led3_on(void){}
void led0_off(void){}
void led1_off(void){}
void led2_off(void){}
void led3_off(void){}void bsp_led_init(void)
{//初始化函数指针led_array[0].on = led0_on;led_array[0].off = led0_off;led_array[0].init = led0_init;led_array[0].mode_count = 1;led_array[1].on = led1_on;led_array[1].off = led1_off;led_array[1].init = led1_init;led_array[1].mode_count = 1;	led_array[2].on = led2_on;led_array[2].off = led2_off;led_array[2].init = led2_init;led_array[2].mode_count = 1;led_array[3].on = led3_on;led_array[3].off = led3_off;led_array[3].init = led3_init;led_array[3].mode_count = 2;for(uint8_t i = 0; i < sizeof(led_array)/led_array[0]; i++){led_array[i].mode = malloc(sizeof(led_mode_t) * led_array[i].mode_count);memset(led_array[i].mode, 0, sizeof(led_mode_t) * led_array[i].mode_count);}//初始化mode时间参数led_array[0].mode[0].ontime = 200;	led_array[0].mode[0].cycle 	= 400;led_array[1].mode[0].ontime = 500;led_array[1].mode[0].cycle 	= 1000;led_array[2].mode[0].ontime = 100;led_array[2].mode[0].cycle 	= 1000;led_array[3].mode[0].ontime = 30;led_array[3].mode[0].cycle 	= 100;led_array[3].mode[1].ontime = 30;led_array[3].mode[1].cycle 	= 900;
}

代码很长,主要长度占用在以下三部分

  • 每一个led都有init,on,off函数
  • 初始化函数指针
  • 初始化mode时间参数

第一部分暂时不做优化,这样会方便我按照顺序说下去吧
第二第三部分,显然程序中有很多重复的代码,我们可以使用可变参数宏来优化

#define INIT_PTR(__index, __onptr, __offptr, __initptr, __modecount)	\led_array[__index].on = __onptr;									\led_array[__index].off = __offptr;									\led_array[__index].init = __initptr;								\led_array[__index].mode_count = __modecount;						#define INIT_MODE(__index, __mode, __ontime, __cycle)	\led_array[__index].mode[__mode].ontime = __ontime;	\led_array[__index].mode[__mode].cycle 	= __cycle;	

使用这两个宏之后,第二第三部分的代码被优化为如下,看起来少了好多

void bsp_led_init(void)
{INIT_PTR(0, led0_on, led0_off, led0_init, 1);INIT_PTR(1, led1_on, led1_off, led1_init, 1);INIT_PTR(2, led2_on, led2_off, led2_init, 1);INIT_PTR(3, led3_on, led3_off, led3_init, 2);for(uint8_t i = 0; i < sizeof(led_array)/led_array[0]; i++){led_array[i].mode = malloc(sizeof(led_mode_t) * led_array[i].mode_count);memset(led_array[i].mode, 0, sizeof(led_mode_t) * led_array[i].mode_count);}INIT_MODE(0, 0, 200, 400);INIT_MODE(1, 0, 500, 1000);INIT_MODE(2, 0, 100, 100);INIT_MODE(3, 0, 30, 100);INIT_MODE(3, 1, 30, 900);
}

OK,我们已经设定了各个LED的闪烁模式,对接了初始化,亮起,熄灭的函数,是时候让他跑起来了。
假设我们有一个bsp_led_tick函数,该函数每1ms调用一次。我们在该函数中定义一个递增的变量tick,比较其它和LED灯ontime的大小。tick比ontime小则LED亮起,比ontime大则LED熄灭。为了循环往复的工作,我们采用的比较值应该是tick对周期的求余而不是tick本身。示例如下

static void bsp_led_tick(void){
#define i_CYCLE_LENGTH  (led_array[i].mode[led_array[i].mode_current].cycle)
#define i_ON_TIME (led_array[i].mode[led_array[i].mode_current].ontime)static uint64_t tick;for(uint8_t i = 0; i <  i < sizeof(led_array)/led_array[0]; i++){if(tick % i_CYCLE_LENGTH < i_ON_TIME)led_array[i].on();elseled_array[i].off();}tick++;
}

我们使用了两个宏i_CYCLE_LENGTH ,i_ON_TIME 来减少代码长度,增加可读性。
可以看出,切换闪烁模式的话直接修改led_array[i].mode_current即可。
到此,我们的驱动框架就很清晰了。
接下来我会指出该框架的问题,并逐一修改。

问题修复

问题1:LED无法关闭
上面代码中LED一直会闪烁无法关闭
解决办法
LED关闭可以认为是一种模式。其中ontime为0,周期为任意值(0除外)
所以,我们可以占用mode[0],将其所有LED的mode[0]初始化为ontime=0, cycle=1,用户设置mode_current为0的时候调用该模式,关闭LED。注意,此时用户设置的闪烁模式需要从mode[1]开始

那么,同样的,如果需要LED常亮呢?我认为LED熄灭是大部分项目中必要的,而常亮却不一定,所以在代码中不做常亮模式的预先设置,如果用户需要的话可以另外设置一个mode,其中的ontime大于cycletime(cycletime不可以为0)即可

问题2:on off重复调用
满足if(tick % i_cycle < i_ontime )条件的时候,程序会重复调用led_array[i].on();即使此时LED已经打开了。
解决办法
在led_t中增加state成员,标记LED状态,已经打开的时候不要重复调用on,已经关闭的不要重复调用off

	···for(uint8_t i = 0; i < sizeof(led_array)/led_array[0]; i++){//增加关闭模式,此后mode[0]就被占用了,用户定义的闪烁模式要从mode[1]开始led_array[i].mode = malloc(sizeof(led_mode_t) * (led_array[i].mode_count)+1);memset(led_array[i].mode, 0, sizeof(led_mode_t) *  (led_array[i].mode_count)+1t);}INIT_MODE(0, 1, 200, 400);INIT_MODE(1, 1, 500, 1000);INIT_MODE(2, 1, 100, 100);INIT_MODE(3, 1, 30, 100);INIT_MODE(3, 2, 30, 900);···
static void bsp_led_tick(void){······//cycletime == 0, LED不再闪烁if(i_CYCLE_LENGTH == 0){if( (lled_array[i]..state == 1)){led_array[i].state = 0;led_array[i].off();}continue;}else{if(tick % i_CYCLE_LENGTH < i_ON_TIME){if( (led_array[i].state == 0)){//如果之前是关闭的,那么开启led_array[i].state = 1;led_array[i].on();}}else{if( led_array.led[i].state == 1){//如果之前是开启的,那么关闭led_array[i].state = 0;led_array[i].off();}}}tick++;
}

问题3:如何实现双闪
我们之前说过,双闪是两种闪烁模式的交替闪烁,那么如何实现交替切换模式呢?
很简单,我们只要再bsp_led_tick中判断tick是否增加到mode[].cycle即可。

//周期回调函数
if(i_CYCLE_LENGTH-1 == (tick % i_CYCLE_LENGTH))
{led_array[i].cycle_func();
}

cycle_func是led_t中增加的新成员,作为周期结束的回调函数。使用前需要初始化指向led3_cyclefun,在该函数中做模式切换

static void led3_cyclefun()
{if(led_array[3].mode_current== 1)led_array[3].mode_current = 2;else if(led_array[3].mode_current== 2)led_array[3].mode_current = 1;
}

有了周期回调函数,再复杂的LED显示效果我们都可以做出来,只需要再周期结束修改显示模式即可。
但是理想很丰满,现实很骨感,这个地方还有一小片乌云等待解决。
在这里插入图片描述
我没有按照led3设置mode时间值,按照图上的会直观一点。
可以看到,在第二个闪烁模式tick=2时间段,此时ontime=1,tick%cycletime=2%5=2,那么第二模式根本不会亮起。此时的流程实际上变成了如下图
在这里插入图片描述
为了解决这个问题,我们需要明确,模式切换之后tick起始点是不同的,不可以笼统的写作tick%cycletime,模式切换之后应该有自己的tick起始点tick_start,按照(tick-tick_start)%cycletime求得余数
问题4:同步
为了解决问题三,我们引入了起点tick_start机制,这回带来新的问题。
每一个LED都有自己的起点,在模式切换的时候更新为当前tick。模式切换是使用该驱动的人决定的,这会造成即使相同周期的LED灯闪烁相位的差异,看起来在乱闪。
解决办法
在led_t中引入sync成员,ticks_start只有配置了sync为0的时候才会更新为当前tick,否则设置为0。
这将会明确一个事实,sync配置为1的LED无法实现循环的闪烁模式切换。即类似于双闪这种循环的切换模式是不被允许的,单次的切换可以。
双闪如果要做同步该怎么办?应该可以在周期回调函数中做一些工作,留给大家思考。

优化

这部分主要优化的是代码体积,假设我有十个灯,每一个都需要on,off,cyclefunc,这也太恐怖了。所以,我们应该再led_t结构体之上再做一个结构体led_array_t。内容如下

typedef struct{led_t led[LED_COUNT];void (*on)(void *parameter);	//打开LEDvoid (*off)(void *parameter);	//关闭LEDvoid (* cycle_func)(void *parameter);	//cycle结束的回调函数
}led_array_t;

这里设置了parameter参数,调用函数的时候将led_t传入以标识身份,在函数内部判断我是哪一个LED,进行相应的操作
举例如下

static void bsp_led_on(void *parameter)
{led_t *p = (led_t *)parameter;switch(p-led_array.led){case 0:DRV_DIO_ChannelOutSet(DRV_DIO_ID_PIO_9);break;case 1:DRV_DIO_ChannelOutSet(DRV_DIO_ID_PIO_25);break;case 2:DRV_DIO_ChannelOutSet(DRV_DIO_ID_PIO_14);break;case 3:DRV_DIO_ChannelOutSet(DRV_DIO_ID_PIO_15);break;default:break;};
}

OK,内容到此结束了,这样看来写好一个LED并不容易。
下面提供了一个基于RTOS的源码,如果要修改为裸机的并不需要耗费很大功夫,相信你可以做到
程序源码在此:https://gitee.com/nwwhhh/led_flash_driver

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.rhkb.cn/news/74853.html

如若内容造成侵权/违法违规/事实不符,请联系长河编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

Stable Diffusion 硬核生存指南:WebUI 中的 VAE

本文使用「署名 4.0 国际 (CC BY 4.0)」许可协议&#xff0c;欢迎转载、或重新修改使用&#xff0c;但需要注明来源。 署名 4.0 国际 (CC BY 4.0) 本文作者: 苏洋 创建时间: 2023年07月30日 统计字数: 11485字 阅读时间: 23分钟阅读 本文链接: https://soulteary.com/2023/07…

MySQL实用命令

一、 DISTINCT 去重 案例&#xff1a;user_test表对班级字段进行去重操作 SELECT DISTINCT class FROM user_test 二、 的作用 案例&#xff1a; SELECT NULL10 三、 concat 实现连接字符串 ​​​案例&#xff1a; SELECT CONCAT(NAME,class) AS 姓名班级 FROM user_test 四…

【多模态】23、RO-ViT | 基于 Transformer 的开发词汇目标检测(CVPR2023)

文章目录 一、背景二、方法2.1 基础内容2.2 Region-aware Image-text Pretraining2.3 Open-vocabulary Detector Finetuning 三、效果3.1 细节3.2 开放词汇目标检测效果3.3 Image-text retrieval3.4 Transfer object detection3.5 消融实验 论文&#xff1a;Region-Aware Pretr…

《网络是怎样连接的》(二.2)

(6条消息) 《网络是怎样连接的》&#xff08;二.1&#xff09;_qq_38480311的博客-CSDN博客 本文主要取材于 《网络是怎样连接的》 第二章 2.5 2.6章节。 目录 简述&#xff1a; 本文的主要内容是 以太网的收发操作 和 UDP协议的收发操作。 IP与以太网的包收发操作 包是什…

蓝海卓越计费管理系统任意文件读取下载

看了大佬的文章&#xff0c;太牛逼啦&#xff0c;下面是大佬文章原文。 蓝海卓越计费管理系统任意文件读取下载 鹰图语法&#xff1a;web.title"蓝海卓越计费管理系统" 访问url 直接更改url就行了 /download.php?file../../../../../etc/passwd

我能“C“——扫雷游戏

一.前言&#xff1a; 扫雷游戏&#xff0c;一款经典的游戏&#xff0c;没玩过的话也可以试着玩一玩&#xff0c;这样对写扫雷游戏这个小游戏的化是会有一个很好的思路的。那么本片博客就来介绍如何实现扫雷游戏的具体步骤。扫雷游戏链接&#x1f449; 扫雷游戏网页版 - Minesw…

【MMCV】mmpretrain/mmclassification概览、环境安装与验证

概览 MMPretrain 是一个全新升级的预训练开源算法框架,旨在提供各种强大的预训练主干网络, 并支持了不同的预训练策略。MMPretrain 源自著名的开源项目 MMClassification 和 MMSelfSup,并开发了许多令人兴奋的新功能。 目前,预训练阶段对于视觉识别至关重要,凭借丰富而强…

GPT Prompt编写的艺术:如何提高AI模型的表现力

随着AI技术的迅速发展&#xff0c;人工智能模型变得越来越强大&#xff0c;能够协助我们完成各种任务。然而&#xff0c;如何更好地利用AI的能力仍然存在很大的探索空间。在与AI进行交互的过程中&#xff0c;我们主要依赖于Prompt&#xff0c;不管是直接与大模型交互&#xff0…

Simulink仿真模块 - Relay

目录 说明 模块特性 Relay是在两个常量输出之间进行切换。 在仿真库中的位置为:Simulink / Discontinuitie模型为: 说明 Relay 模块的输出在两个指定值之间切换。打开中继时,它会一直保持打开,直到输入低于 Switch off point 参数的值为止。关闭中继时,它会一直保持关闭…

[C++项目] Boost文档 站内搜索引擎(1): 项目背景介绍、相关技术栈、相关概念介绍...

项目背景 Boost库是C中一个非常重要的开源库. 它实现了许多C标准库中没有涉及的特性和功能, 一度成为了C标准库的拓展库. C新标准的内容, 很大一部分脱胎于Boost库中. Boost库的高质量代码 以及 提供了更多实用方便的C组件, 使得Boost库在C开发中会被高频使用 为方便开发者学…

Excel功能总结

1&#xff09;每一张表格上都打印表头 “页面布局”-->“打印标题”-->页面设置“工作表”页-->打印标题“顶端标题行” 如&#xff1a;固定第1~2行&#xff0c;设置成“$1:$2” 2&#xff09;将页面内容打印在一页【缩印】 1.选好需要打印的区域&#xff0c;“页面布…

算法通关村——如何使用中序和后序来恢复一棵二叉树

通过序列构造二叉树 给出以下三个二叉树遍历的序列&#xff1a; (1) 前序: 1 2 3 4 5 6 8 7 9 10 11 12 13 15 14 (2) 中序: 3 4 8 6 7 5 2 1 10 9 11 15 13 14 12 (3) 后序: 8 7 6 5 4 3 2 10 15 14 13 12 11 9 1 前中序复原二叉树 所需序列 (1) 前序: 1 2 3 4 5 6 8 7 9 10 …

瑞芯微RK3568开发板保姆级护航入门学习嵌入式

资料优势 专为3568编写|迅为原创|拒绝网络拼凑 20个手册2800页手册进行结构分层适用于学习与开发 为了方便大家清晰快速的学习&#xff0c;迅为iTOP-3568开发板手册资料全面升级&#xff0c;对手册内容进行了结构分层&#xff0c;共计20个文档&#xff0c;超2800页的资料专为…

这个视频翻译英文的技巧带你畅享无障碍沟通

在一个充满魔法和奇迹的童话世界里&#xff0c;住着一个聪明勇敢的小女孩&#xff0c;她叫芳芳。芳芳一直梦想着探索更广阔的世界&#xff0c;直到有一天&#xff0c;她意外发现了一本神奇的书&#xff0c;名叫《翻译之光》。这本魔法书的每一页都流动着绚丽的彩虹光芒&#xf…

blender凹凸感和置换形变

一、怎么做出凹凸感 需要三个部分的内容&#xff1a; 1、一个基础的纹理&#xff1a;告诉计算机需要用一个什么样的纹理做凹凸&#xff0c;纹理一般采用黑白&#xff0c;在计算机里面&#xff0c;从 0 - 1之间的值可以用从黑到白之间不同的灰度来表示因此&#xff0c;有一张黑白…

kernel32.dll如何修复,快速解决kernel32.dll缺失的方法

Kernel32.dll是Windows操作系统中一个重要的系统文件&#xff0c;对于系统的正常运行至关重要。然而&#xff0c;由于各种原因&#xff0c;用户可能会遇到kernel32.dll文件的缺失问题。今天小编就来给大家详细的介绍一下kernel32.dll这个文件&#xff0c;并且详细的介绍一下ker…

《金融数据保护治理白皮书》发布(137页)

温馨提示&#xff1a;文末附完整PDF下载链接 导读 目前业界已出台数据保护方面的治理模型&#xff0c;但围绕金融数据保护治理的实践指导等尚不成熟&#xff0c;本课题围绕数据保护治理的金融实践、发展现状&#xff0c;探索和标准化相关能力要求&#xff0c;归纳总结相关建…

ApplicationArguments 接口的作用和使用介绍

在Spring Boot中&#xff0c;ApplicationArguments接口是用于获取应用程序的命令行参数的一个接口。它是Spring Boot提供的一种方便的方式&#xff0c;用于获取在应用程序启动时从命令行传递的参数。 ApplicationArguments接口提供了以下方法来获取命令行参数&#xff1a; ge…

TDesign中后台管理系统-访问后端服务

目录 1 修改后端服务地址2 解决跨域问题3 动态获取菜单4 测试后端接口5 前后端联调总结 目前我们已经搭建了TDesign的前端和express的后端&#xff0c;目前是两个独立的应用。通常我们需要把前后端集成在一起&#xff0c;TDesign已经配置了相关的信息&#xff0c;只需要修改后端…

【Linux】网络编程套接字

1 预备知识 1.1 IP地址 IP协议有两个版本&#xff0c;分别是IPv4和IPv6。没有特殊说明&#xff0c;默认都是IPv4对于IPv4&#xff0c;IP地址是一个四个字节32为的整数&#xff1b;对于IPv6来说&#xff0c;IP地址是128位的整数 我们通常也使用 “点分十进制” 的字符串表示IP…