单片机中的通用LED驱动

news2024/10/6 20:25:11

前言

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

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

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

数据结构分析

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

typedef struct{
	void (*init)(void);	//初始化ED
	void (*on)(void);	//打开LED
	void (*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);	//初始化ED
	void (*on)(void);	//打开LED
	void (*off)(void);	//关闭LED
	led_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);	//初始化ED
	void (*on)(void);	//打开LED
	void (*off)(void);	//关闭LED
	led_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();
		else
			led_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);	//打开LED
	void (*off)(void *parameter);	//关闭LED
	void (* 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.coloradmin.cn/o/800598.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

《焊接点云处理》-V型焊缝处理

焊接点云处理-V型焊缝处理 前言一、代码二、处理步骤前言 处理效果 一、代码 主函数 #include "CGALRECONSTRUCT.h" #include"PCLFUNCTION.h"int main(int argc

vue+leaflet笔记之地图聚合

vueleaflet笔记之地图聚合 文章目录 vueleaflet笔记之地图聚合开发环境代码简介插件简介与安装使用简介 详细源码(Vue3) 本文介绍了Web端使用Leaflet开发库进行地图聚合查询的一种方法 (底图来源:中科星图)&#xff0c;结合Leaflet.markercluster插件能够快速的实现地图聚合查询…

【C++】做一个飞机空战小游戏(二)——利用getch()函数实现键盘控制单个字符移动

[导读]本系列博文内容链接如下&#xff1a; 【C】做一个飞机空战小游戏(一)——使用getch()函数获得键盘码值 【C】做一个飞机空战小游戏(二)——利用getch()函数实现键盘控制单个字符移动 在【C】做一个飞机空战小游戏(一)——使用getch()函数获得键盘码值一文中介绍了如何利用…

4.4 if选择结构

4.4 if选择结构 if单选择结构 我们很多时候需要去判断一个东西是否可行&#xff0c;然后我们才去执行&#xff0c;这样的一个过程在程序中用if语句来表示 语法 if(布尔表达式){//如果布尔表达式为true将执行的语句 }if单选择结构流程图 package com.baidu.www.struct;import …

JVM源码剖析之JIT工作流程

版本信息&#xff1a; jdk版本&#xff1a;jdk8u40思想至上 Hotspot中执行引擎分为解释器、JIT及时编译器&#xff0c;上篇文章描述到解释器过度到JIT的条件。JVM源码剖析之达到什么条件进行JIT优化 这篇文章大致讲述JIT的编译过程。在JDK中javac和JIT两部分跟编译原理挂钩&a…

MySQL 服务器的调优策略

点击上方↑“追梦 Java”关注&#xff0c;一起追梦&#xff01; 在工作中&#xff0c;我们发现慢查询一般有2个途径&#xff0c;一个是被动的&#xff0c;一个是主动的。被动的是当业务人员反馈某个查询界面响应的时间特别长&#xff0c;你才去处理。主动的是通过通过分析慢查询…

原生html—摆脱ps、excel 在线绘制财务表格加水印(html绘制表格js加水印)

文章目录 ⭐前言⭐html标签&#x1f496;table表格的属性&#x1f496;实现财务报表 ⭐结束 ⭐前言 大家好&#xff0c;我是yma16&#xff0c;本文分享原生html——绘制表格报表加水印。 背景&#xff1a;解决没有ps的情况下使用前端html制作表格报表。 html介绍 HTML&#xf…

【雕爷学编程】Arduino动手做(93)--- 0.96寸OLED液晶屏模块8

37款传感器与执行器的提法&#xff0c;在网络上广泛流传&#xff0c;其实Arduino能够兼容的传感器模块肯定是不止这37种的。鉴于本人手头积累了一些传感器和执行器模块&#xff0c;依照实践出真知&#xff08;一定要动手做&#xff09;的理念&#xff0c;以学习和交流为目的&am…

C++ inline 内联函数

1.什么是内联函数 在函数声明或定义中&#xff0c;在函数的返回类型前加上C关键字inline&#xff0c;即将函数指定为内联函数。这样可以**解决一些频繁调用的简单函数大量消耗栈空间&#xff08;栈内存&#xff09;**的问题。关键字inline必须与函数定义放在一起才能使函数成为…

C++模拟实现queue

1.前言 queue 遵循的原则是先进先出&#xff0c;那到底是用list 还是 vector呢&#xff1f;其实都可以&#xff0c;但是严格来讲vector是不可以的&#xff0c;因为他头删的效率太低了。所以vs官方是不允许用vector的&#xff1a; 因为底层的pop用的是pop_front(), vector是没有…

【C刷题】矩阵相等判断与序列中删除指定的数字

目录 BC105-矩阵相等判断 方法1:两矩阵输入完毕后&#xff0c;进行比较 方法2:在接收过程中直接比较 BC98 - 序列中删除指定的数字 方法1:把要删除的元素改为0 方法2:打印不用删除的元素 方法3:定义两个下标 i 和 j(动图演示) 此篇文章是关于牛客网刷题的做题思路和代码…

Java版知识付费平台免费搭建 前后端分离实现知识付费平台

提供职业教育、企业培训、知识付费系统搭建服务。系统功能包含&#xff1a;录播课、直播课、题库、营销、公司组织架构、员工入职培训等。 提供私有化部署&#xff0c;免费售后&#xff0c;专业技术指导&#xff0c;支持PC、APP、H5、小程序多终端同步&#xff0c;支持二次开发…

大模型开发(十一):Chat Completions模型的Function calling功能详解

全文共5000余字&#xff0c;预计阅读时间约15~25分钟 | 满满干货(附代码案例)&#xff0c;建议收藏&#xff01; 本文目标&#xff1a;介绍Chat Completions模型的Function calling参数和使用方法&#xff0c;并完整的实现一个Chat模型的Function calling功能案例。 代码下载地…

字节跳动 EB 级 Iceberg 数据湖的机器学习应用与优化

深度学习的模型规模越来越庞大&#xff0c;其训练数据量级也成倍增长&#xff0c;这对海量训练数据的存储方案也提出了更高的要求&#xff1a;怎样更高性能地读取训练样本、不使数据读取成为模型训练的瓶颈&#xff0c;怎样更高效地支持特征工程、更便捷地增删和回填特征。本文…

Java IO,BIO、NIO、AIO

操作系统中的 I/O 以上是 Java 对操作系统的各种 IO 模型的封装&#xff0c;【文件的输入、输出】在文件处理时&#xff0c;其实依赖操作系统层面的 IO 操作实现的。【把磁盘的数据读到内存种】操作系统中的 IO 有 5 种&#xff1a; 阻塞、 非阻塞、【轮询】 异步、 IO复…

STM32的SDIO功能框图及SDIO结构体

目录 STM32的SDIO功能框图及SDIO结构体 STM32的SDIO功能框图 SDIO适配器 命令路径 CPSM状态机 数据路径 DPSM状态机 数据FIFO 适配器寄存器 SDIO相关结构体 SDIO初始化结构体 SDIO命令初始化结构体 SDIO数据初始化结构体 STM32的SDIO功能框图及SDIO结构体 STM32的…

3d软件动物生活习性仿真互动教学有哪些优势

软体动物是一类广泛存在于海洋和淡水环境中的生物&#xff0c;其独特的形态和生活习性给学生带来了新奇和有趣的学习主题&#xff0c;为了方便相关专业学科日常授课教学&#xff0c;web3d开发公司深圳华锐视点基于真实的软体动物&#xff0c;制作软体动物3D虚拟展示系统&#x…

发点实用的快捷键(mac

切换输入法&#xff1a;ctrlspace /ctrloptionspace&#xff08;更快捷 切换网页&#xff1a; shifttab 切换应用界面&#xff1a;alttab 关闭页面&#xff1a;altw 搜索&#xff1a;altspace 展示mac隐藏文件&#xff1a; Commangshift . (点) 以下是一些浏览器快捷键&am…

【初阶C语言】认识和使用函数

1. 函数是什么 2. 库函数 3. 自定义函数 4. 函数参数 5. 函数调用 6. 函数的嵌套调用和链式访问 7. 函数的声明和定义 8. 函数递归 一、什么是函数 在数学中有函数&#xff0c;在C语言中也有函数&#xff0c;我们直接先给出一个定义&#xff1a; 在基维百科中函数被定义为子程…

MyBatisPlus入门到精通-1

MyBatisPlus(简称MP) 这篇博客主要讲解用MyBatisPlus进行三层架构中Dao层的开发 以这个为目的来进行我们的学习 我们会先通过一个概述和入门案例进行快速上手 之后我们再通过对我们原先的案列的问题进行分析 来进一步了解MP操作数据库的知识 快速入门 MP简介 MP是国人开发的…