1000字范文,内容丰富有趣,学习的好帮手!
1000字范文 > ws2812驱动总结(包括对时序的详细分析 代码基于STC15系列单片机)

ws2812驱动总结(包括对时序的详细分析 代码基于STC15系列单片机)

时间:2022-11-30 09:25:00

相关推荐

ws2812驱动总结(包括对时序的详细分析 代码基于STC15系列单片机)

声明

本文版权归作者bxgj所有,未经作者授权,本文禁止以任何形式在任何平台(包括但不限于各网站、论坛、博客、微博、公众号等)部分或全部地转载,禁止二次修改后声明原创。授权转载内容请注明出处(如作者:xxx,转载自xxx),并标明本站网址。文中程序仅供学习使用,本人不承担任何由使用文中代码产生的法律责任。

ws2812相信有不少人都用过,大家对这款彩色LED真的是又爱又恨,爱的是它它使用简单,采用单总线通信方式,节约IO口,而且可以多级串联。而普通的彩色LED不是共阴就是共阳,每个颜色一个引脚,一般都是用PWM驱动,想要控制亮度、颜色就要分别控制每个引脚上的PWM占空比,想要驱动多个LED就更麻烦了。恨的是ws2812对时序的要求比较高,对低速单片机不太友好。今天我们就详细谈一谈ws2812的驱动。

不想看分析过程的直接跳到最后看总结。

拿到一款芯片,第一件事就是找datasheet,找datasheet很简单,找一份靠谱的datasheet有时很困难,特别是国内一些翻译后datasheet往往有错误,我被坑过很多次。说真的我也不想看英文手册,但是没办法,除非有官方中文版,还是要尽量看原版英文手册。就这款芯片来说,我找到了多个版本,不管是国外,国内的,官网上的,还是某些文库中的,关于时序的定义竟然完全没有一样的!!!!这里直接把时序部分摘出来给大家看一下。

第一种

第二种

第三种

第四种

第五种

第六种

看完这些datasheet我完全不知道该相信哪个,总的来说,第一张图片中的参数似乎比较平均,所以我就以它为基准来编写驱动。

在和大家分享我的驱动程序之前,先来看看别人的驱动程序。目前在网上能找到的驱动大多是STM32、STM8、Arduino的驱动,很少有STC单片机的驱动。

STM32的驱动方式目前我见到的有三种,第一种直接控制IO口,并精确调整延时,这个没什么好说的,略过;第二种将SPI的时钟调整为8MHz,发送一字节正好是1.25us,给ws2812发送0即通过SPI总线发送11000000b,发送1即通过SPI总线发送11111100b,非常巧妙的一种方式;第三种方式使用PWM,周期设置为3MHz,发送0就把占空比设置为33%,发送1就把占空比设置为66%,也是一种不错的方式。关于Arduino我不想说了,网上代码太多了,核心部分都是用汇编写的。STC的驱动也有一部分,但我真的不敢恭维,有的就是无脑堆_nop_();,有的要求时钟必须为24MHz,总之能用就行,没有一篇文章会详细分析时序,我真的无力吐槽。

所成我给我定个小目标:写一个简单好用的STC单片机ws2812驱动。

我们常用的主频一般是12MHz或11.0592MHz,我的驱动要争取能在这个主频下工作,况且不是所有的场合都要用24MHz这么高的频率,比如低功耗的场合,如果单片机中的某些代码精确依赖时钟,改变主频对整个代码的改动也比较大,比如定时器、串口的波特率都需要改。如果你没什么追求,只要能用就行,那么可以略过中间的分析过程直接跳到最后看结论。最后说一句,杠精自重!

先看看我们能不能借鉴别人的驱动方案,STC15的SPI速率最高为SYSCLK/4,也就是说主频要达到32MHz才可以,软件中最高只能设置到30MHz,何况这个主频超过我的目标,SPI方案PASS。PWM方案应该有可行性,但是8DIP封装的单片机没有PWM接口,也没有SPI接口,这个方案我也考虑。那么最后就只有控制IO口这一个方案了。

根据这个思路,我们会编写类似以下代码(这部分代码只做演示,杠精自重)

void ws2812_write_byte(u8 dat){u8 i = 0;for(i = 0; i < 8; i++){if((dat & 0x80) == 0x80){WS2812_IO = 1;delay_us(0.7);WS2812_IO = 0;delay_us(0.6);}else{WS2812_IO = 1;delay_us(0.35);WS2812_IO = 0;delay_us(0.8);}dat = dat << 1;}}

显然,这个代码在STC这样的低速单片机上是不能正常工作的,毕竟执行任何语句都需要时间。当然在STM32等高速单片机上这种代码是有可能正常工作的。

当然这个代码有优化空间,我们可以将一些相同的操作提取出来,比如设置IO口还有延时,于是我们可以得到这样的代码

void ws2812_write_byte(u8 dat){u8 i = 0;for(i = 0; i < 8; i++){WS2812_IO = 1;delay_us(0.3);if((dat & 0x80) == 0x80){delay_us(0.4);WS2812_IO = 0;delay_us(0.6);}else{WS2812_IO = 0;delay_us(0.8);}dat = dat << 1;}}

比前一个代码稍微好一些,但还是不实用

听说while循环比for循环效率高,使用自减计数比自加计数效率高,那我们就再改一版(关于循环下文有详细分析,杠精自重!)

void ws2812_write_byte(u8 dat){u8 i = 8;while(i--){WS2812_IO = 1;delay_us(0.3);if((dat & 0x80) == 0x80){delay_us(0.4);WS2812_IO = 0;delay_us(0.6);}else{WS2812_IO = 0;delay_us(0.8);}dat = dat << 1;}}

我们知道51单片机有一种数据类型是其他架构所不一定具备的,那就是布尔型(sbit,或称为位),直接对位进行操作要比使用与逻辑判断更快,零点几微秒的延时真的太短了,如果我们把delay_us函数直接展开成NOP指令还能节省函数调用的时间开销,于是我们得到一个比较复杂,但性能大幅提升的新代码

// 需根据实际情况适当调整NOP语句数量#define SEND_BIT0 {WS2812_IO=1;_nop_();_nop_();_nop_();WS2812_IO=0;_nop_();_nop_();_nop_();_nop_();_nop_();_nop_();_nop_();}#define SEND_BIT1 {WS2812_IO=1;_nop_();_nop_();_nop_();_nop_();_nop_();_nop_();_nop_();WS2812_IO=0;_nop_();_nop_();_nop_();}u8 bdata LED_DATA;sbit DAT_bit0 = LED_DATA^0;sbit DAT_bit1 = LED_DATA^1;sbit DAT_bit2 = LED_DATA^2;sbit DAT_bit3 = LED_DATA^3;sbit DAT_bit4 = LED_DATA^4;sbit DAT_bit5 = LED_DATA^5;sbit DAT_bit6 = LED_DATA^6;sbit DAT_bit7 = LED_DATA^7;void ws2812_write_byte(u8 dat){LED_DATA = dat;if(DAT_bit0){SEND_BIT1}else{SEND_BIT0};if(DAT_bit1){SEND_BIT1}else{SEND_BIT0};if(DAT_bit2){SEND_BIT1}else{SEND_BIT0};if(DAT_bit3){SEND_BIT1}else{SEND_BIT0};if(DAT_bit4){SEND_BIT1}else{SEND_BIT0};if(DAT_bit5){SEND_BIT1}else{SEND_BIT0};if(DAT_bit6){SEND_BIT1}else{SEND_BIT0};if(DAT_bit7){SEND_BIT1}else{SEND_BIT0};}

上面这个代码就比较实用了,但是我实际测试中发现,这个代码在24MHz下可以用,但12MHz下是不能用的(因为分支跳转指令比较耗时,12MHz时分支跳转占用的时间不能满足ws2812时序的要求),走到这一步如果还想继续优化就必须深入分析汇编代码。

本文所用的单片机型号为STC15W4K32S4,所用指令集为STC-Y5指令集,仅适用于STC15Fxx、STC15Lxx、STC15Wxx系列,不包括STC15F104E(A版)、STC15L104E(A版)、STC15F204EA(A版)、STC15L204EA(A版)。

STC-Y5以前的指令集很多指令的执行时间要大于STC-Y5的指令集;最新的STC8Fxx及STC8Axx系列采用的STC-Y6指令集大幅度优化,绝大多数指令被优化为单周期指令,执行速度远远快于STC-Y5。由于ws2812对时序的要求较高,本文中的代码高度依赖于这些指令的执行时间,因此文中代码完全不保证在其他单片机上能正常驱动ws2812。

列举并对比程序中用到的几条指令在不同版本指令集上的执行时间

先来分析时序,在ws2812的datasheet中可以看到TH+HL=1.25us,假设主频为12MHz,1.25us即0.00000125/(1/12M)=15个周期,也就是说我们要在15个周期内执行一定数量的指令来完成IO置高、IO置低、数据移位、跳转等所有必要的操作!

略去漫长的调试过程,最后我写出了最精简的汇编代码

void ws2812asm(unsigned char dat){#pragma asmMOV A, R7MOV R6, #0x08WS2812LOOP:SETB P3.7RLC AMOV P3.7, CNOPCLR P3.7DJNZ R6, WS2812LOOP#pragma endasm}

来分析一下这个代码,R7即函数调用时传入的dat,将dat放入累加器A中,然后将8放入R6中作为循环计数,这里我用的是P3.7引脚,用SETB语句将它置为高电平,然后用RLC指令将dat左移一位,最高位进入进位位C,使用MOV语句将进位位的值赋给P3.7引脚,NOP延时,之后将P3.7引脚置为低电平,DJNZ将计数值减一不为零则跳转到WS2812LOOP继续执行。整个代码的循环体耗时为3+1+3+1+3+4=15个CLK,完美!

我们从WS2812LOOP处分析一下时序,假设调用函数前发送了复位信号,此时IO状态为低电平

如果发送的数据为0,则高电平为0.33us,低电平为0.92us

如果发送的数据为1,则高电平为0.67us,低电平为0.58us

这个时序基本符合datasheet中的要求,误差也在±150us以内。

简单验证一下,试试看看究竟能不能驱动ws2812

void main(){WS2812_IO = 1;Delay100us();WS2812_IO = 0;Delay100us();ws2812asm(100);ws2812asm(0);ws2812asm(0);while(1){}}

事实证明这个代码完全可以正常驱动ws2812!

但是,等等,好像有哪里不对,我们这里只考虑循环体的执行时间,循环体外有两个MOV语句,函数调用前会有一个MOV语句将参数传入寄存器R7,之后LCALL语句调用函数,调用完之后还有RET语句返回,这样一来整个函数调用的总时间为3+4+1+2+14+4=28个CLK,约2.4us,超过了手册中要求的1.25us,即使算上600ns的误差也还是超时了,但是ws2812却被正常点亮了!也就是说即使时序不完全符合datasheet中的要求也是可以工作的!这是非常重要的一点!最后经过我的反复实验得出一个结论:小于45us的高电平为判定为逻辑0,大于45us的高电平被判定为逻辑1,低电平的时长只要不要超过复位信号的时长都可以完成数据的传输!

有了这个结论,我们的编程工作就会轻松许多,只要我们将循环的跳转、赋值、计算灯珠颜色等耗时的操作放在低电平时就行了。

作为一个有最求的人,我觉不满足于此,汇编代码比C代码要难一些,阅读也有点费劲,那么能不能写出符合时序的C代码呢?来尝试一下。

置位、清位、左移这三个操作时必须的,完全没有优化空间,那么只剩一个循环可以优化。前文说到要详细研究一下各种循环语句,一般处理器中都有为零跳转指令、减一为零跳转等类似指令,使用自减计数只需要一条指令即可完成循环的跳转,若果使用自加计数,一般会被编译为自加、判断、跳转三条指令,因此一般来说while循环配合自减计数是效率最高的。但是,对于GNU等编译器,它们会充分利用目标平台的指令的特点进行优化,甚至将for自加的循环优化为while自减的循环,另外对于PC或其他高性能的平台,一个循环上损失的效率可以忽略不计,编程时不用在这种细节上纠结。杠精自重!

我这里写了6种循环,编译出来逐一进行分析(这里统一采用无符号数,我知道还有有符号数、浮点数等计数方式,>=、<=、==等判断条件的方式,精力有限,有兴趣的自己研究,杠精自重!)

void test1(){u8 i = 8;while(i){_nop_();i--;}}C:0x19BE MOVR7,#0x08C:0x19C0 NOPC:0x19C1 DJNZ R7,C:19C0C:0x19C3 RET

汇编代码非常简单,是所有循环中效率最高的,只用一条DJNZ语句就完成了减一、判断和跳转,除去NOP语句,整个循环体耗时为4个CLK。

void test2(){u8 i = 8;while(i){i--;_nop_();}}C:0x199C MOVR7,#0x08C:0x199E MOVA,R7C:0x199F JZC:19A5C:0x19A1 DECR7C:0x19A2 NOPC:0x19A3 SJMP C:199EC:0x19A5 RET

和test1()函数的区别在于语句的顺序不同,自减、判断和跳转被拆分成三条语句,除去NOP语句,整个循环体耗时1+4+2+3=10个CLK

void test3(){u8 i = 0;while(i < 8){_nop_();i++;}}C:0x1993 CLRAC:0x19B0 MOVR7,AC:0x19B1 NOPC:0x19B2 INCR7C:0x19B3 CJNE R7,#0x08,C:19B1C:0x19B6 RET

与前两个函数不同,这里采用自加计数循环,除去NOP语句,循环体耗时2+4=6个CLK

void test4(){u8 i = 0;while(i < 8){i++;_nop_();}}C:0x008E CLRAC:0x008F MOVR7,AC:0x0090 MOVA,R7C:0x0091 CLRCC:0x0092 SUBB A,#0x08C:0x0094 JNCC:009AC:0x0096 INCR7C:0x0097 NOPC:0x0098 SJMP C:0090C:0x009A RET

同样是自加计数循环,这是所有循环中效率最低的一个,除去NOP语句,整个循环体用了6条语句,耗时1+1+2+3+2+3=12个CLK

void test5(){u8 i = 8;for(i; i != 0; i--){_nop_();}}C:0x0080 MOVR5,#0x08C:0x0082 NOPC:0x0088 DJNZR5,C:0082C:0x008A RET

这个编译出来的结果和test1()一样,略过

void test6(){u8 i = 0;for(i; i < 8; i++){_nop_();}}C:0x1965 CLRAC:0x1966 MOVR5,AC:0x1967 NOPC:0x196D INCR5C:0x196E CJNE R5,#0x08,C:1967C:0x1971 RET

这个编译出来的结果和test3()一样,略过

所以我们可以得出结论,采用while自减循环的方式效率最高,调试的过程略过,最后我们得到如下代码

void ws2812_write_byte(u8 dat){u8 i = 8;dat <<= 1;while(i){WS2812_IO = 1;WS2812_IO = CY;WS2812_IO = 0;dat <<= 1;i--;}}

来看一下上面的C代码编译后对应的汇编代码,比我写的汇编代码效率稍微低一点,但是完全可以正常工作。我不是针对某些编译器(GNU编译的代码真的让我感叹写出这种编译器的人真牛B),在这里我只说C51这个编译器,它编译出的代码不一定比自己写的更好,我知道设置里面可以选择优化体积或优化速度,我这里采用默认选择第8级优化,Reuse Common Entry Code,优化速度Fever Speed(杠精自重)。

C:0x1AB3 MOVR6,#0x08C:0x1AB5 MOVA,R7C:0x1AB6 ADDA,ACC(0xE0)C:0x1AB8 MOVR7,AC:0x1AB9 SETB P37(0xB0.7)C:0x1ABB MOVP37(0xB0.7),CC:0x1ABD CLRP37(0xB0.7)C:0x1ABF MOVA,R7C:0x1AC0 ADDA,ACC(0xE0)C:0x1AC2 MOVR7,AC:0x1AC3 DJNZ R6,C:1AB9C:0x1AC5 RET

C语言中的左移、右移依照被操作数是有符号数或无符号数被编译为算数左移、右移或逻辑左移右移指令,C51中的RL、RLC、RR、RRC这4条指令都是循环左移、右移,没有算数左移、右移和逻辑左移右移指令,因此在C51中被编译为MOV A,R7;ADD A,ACC(0xE0);MOV R7,A 这3条指令,执行的时间更长了。ADD A,ACC(0xE0)这句可能不太好理解,单独讲一下,这句是将直接地址单元中的数据加到累加器,这里的直接地址单元就是ACC,它的地址是0xE0,查阅手册我们发现0xE0这个地址就是累加器A本身,也就是说将累加器自己的内容加上自己,也就是相当于乘2,也就是左移一位。

最终我们得出了驱动ws2812最精简的C语言代码,可以正常工作于12MHz或11.0592MHz,其他主频请根据实际情况增减NOP语句的数量。

void ws2812_write_byte(u8 dat){u8 i = 8;dat <<= 1;while(i){WS2812_IO = 1;// 如果主频较高可在此处适当增加_nop_():// 将下面的dat <<= 1;移至此处也可以WS2812_IO = CY;WS2812_IO = 0;dat <<= 1;i--;}}

再次强调,本代码仅用于STC-Y5指令集的单片机,包括STC15Fxx、STC15Lxx、STC15Wxx系列,不包括STC15F104E(A版)、STC15L104E(A版)、STC15F204EA(A版)、STC15L204EA(A版)。由于ws2812对时序的要求较高,本代码高度依赖这些指令的执行时间,对于其他STC单片机或其他架构单片机完全不保证能正常驱动ws2812,请根据指令的执行周期、MCU主频自行修改!

结论

复位信号为50us以上的低电平,复位信号不会熄灭已经点亮的灯珠(假设已经发送了5个红色数据,此时复位,然后又发送2个蓝色数据,那么灯珠点亮的状态为蓝蓝红红红,而不是蓝蓝灭灭灭)小于0.45us的高电平为逻辑0,大于0.45us的高电平为逻辑1,低电平的时长不要超过复位信号的时长每位或每字节或每3字节传输完成后建议保持低电平,如果保持高电平且持续时间大于0.45us就会被认为逻辑1,假设下一个传输的数据是0就会出错,因此只建议在完成所有传输后保持高电平因为每位数据传输完成后是低电平,因此发送下一位数据的时间间隔不要超过50us,否则会被判定为复位信号,因此可以充分利用50us以内的低电平的这段时间做一些比较费时的操作,如读取数据、计算下一个灯珠的颜色等只有收到完整的24位数据才会点亮一颗灯珠,之后的数据被传送到下一颗灯珠,任意时刻都可以发送复位信号,未传输完成的数据会被丢弃

以上结论仅代表个人观点共大家参考,并不等同于官方说面,虽说是参考,但也非常有意义,其中部分细节datasheet中并未说明,以让大家少走弯路。市场上有ws2812的兼容灯珠,个人精力有限,对于这些兼容灯珠本人不保证此结论的正确性。

本内容不代表本网观点和政治立场,如有侵犯你的权益请联系我们处理。
网友评论
网友评论仅供其表达个人看法,并不表明网站立场。