单片机时序被占用_cpu时序:单片机读外部数据存储器的时序图 - CSDN
  • 操作时序永远是使用IC芯片的关键所在,一个芯片的所有使用细节都包含在它的官方器件手册上。所以使用一个芯片,要做好的第一件事就是要把它器件手册上的所有内容提取并且掌握。介于我国目前芯片设计能力有限,大部分...

     

    操作时序永远是使用IC芯片的关键所在,一个芯片的所有使用细节都包含在它的官方器件手册上。所以使用一个芯片,要做好的第一件事就是要把它器件手册上的所有内容提取并且掌握。介于我国目前芯片设计能力有限,大部分器件都是外国几个IC巨头如TI、AT、MAXIM等这些公司生产的,器件资料自然是英文的,同时也可以提高下英语水平。

      首先来看1602的引脚定义,1602的引脚是很整齐的SIP单列直插封装,所以器件手册只给出了引脚的功能数据表:

        关注以下几个管脚:

      3脚:VL,液晶显示偏压信号,用于调整LCD1602的显示对比度,一般会外接电位器用以调整偏压信号,注意此脚电压为0时可以得到最强的对比度。

      4脚:RS,数据/命令选择端,当此脚为高电平时,可以对1602进行数据字节的传输操作,而此脚为低电平时,则是进行命令字节的传输操作。命令字节,即是用来对LCD1602的一些工作方式作设置的字节;数据字节,即使用以在1602上显示的字节。值得一提的是,LCD1602的数据是8位的。

      5脚:R/W,读写选择端。当此脚为高电平可对LCD1602进行读数据操作,反之进行写数据操作。此脚其实用处不大,直接接地永久置为低电平也不会影响其正常工作。但是尚未经过复杂系统验证,保留此意见。

      6脚:E,使能信号,其实是LCD1602的数据控制时钟信号,利用该信号的上升沿实现对LCD1602的数据传输。

      7~14脚:8位并行数据口,使得对LCD1602的数据读写大为方便。


      现在来看LCD1602的操作时序: 


      在此,先不读出它的数据的状态或者数据本身。所以只需要看两个写时序:

      ①当要写指令字,设置LCD1602的工作方式时:需要把RS置为低电平,RW置为低电平,然后将数据送到数据口D0~D7,最后E引脚一个高脉冲将数据写入。

      ②当要写入数据字,在1602上实现显示时:需要把RS置为高电平,RW置为低电平,然后将数据送到数据口D0~D7,最后E引脚一个高脉冲将数据写入。

      发现了么,写指令和写数据,差别仅仅在于RS的电平不一样而已。以下是LCD1602的时序图:

      大家要慢慢学会看时序图,要知道操作一个器件的精华便蕴藏在其中,看懂看准了时序,再操控芯片就是非常容易事了。1602的时序是最简单的时序: 


      1、注意时间轴,如果没有标明(其实大部分也都是不标明的),那么从左往右的方向为时间正向轴,即时间在增长。

      2、上图框出并注明了看懂此图的一些常识:

      (1)时序图最左边一般是某一根引脚的标识,表示此行图线体现该引脚的变化,上图分别标明了RS、R/W、E、DB0~DB7四类引脚的时序变化。

      (2)有线交叉状的部分,表示电平在变化,如上所标注。

      (3)应该比较容易理解,如上图右上角所示,两条平行线分别对应高低电平,也正好吻合(2)中电平变化的说法。

      (4)上图下,密封的菱形部分,注意要密封,表示数据有效,Valid Data这个词也显示了这点。

      3、需要十分严重注意的是,时序图里各个引脚的电平变化,基于的时间轴是一致的。一定要严格按照时间轴的增长方向来精确地观察时序图。要让器件严格的遵守时序图的变化。在类似于18B20这样的单总线器件对此要求尤为严格。

      4、以上几点,并不是LCD1602的时序图所特有的,绝大部分的时序图都遵循着这样的一般规则,所以大家要慢慢的习惯于这样的规则。

      也许你还注意到了上面有许多关于时间的标注,这也是个十分重要的信息,这些时间的标注表明了某些状态所要维持的最短或最长时间。因为器件的工作速度也是有限的,一般都跟不上主控芯片的速度,所以它们直接之间要有时序配合。话说现在各种处理器的主频也是疯狂增长,日后搞不好出现个双核单片机也不一定就是梦话。下面是时序参数表:


      大家要懂得估计主控芯片的指令时间,可以在官方数据手册上查到MCU的一些级别参数。比如现在用AVR M16做为主控芯片,外部12MHz晶振,指令周期就是一个时钟周期为(2/12MHz)s,所以至少确定了它执行一条指令的时间是us级别的。以上给的时间参数全部是ns级别的,所以即便在程序里不加延时程序,也应该可以很好的配合LCD1602的时序要求了。怎么看这个表呢?很简单,在时序图里可以找到TR1,对应时序参数表,可以查到这个是E上升沿/下降沿时间,最大值为25ns,表示E引脚上的电平变化,必须在最大为25ns之内的时间完成。大家看是不是这个意思?


      总结

      当需要写命令字节时,时间由左向右,RS变为低电平,R/W变为低电平,注意观察这里是RS的状态先变化完成。然后DB0~DB7上数据进入有效阶段,接着,E引脚有一个整脉冲跳变,维持时间最小值tpw=400ns的E脉冲宽度。然后,E引脚负跳变,RS电平变化,R/W电平变化。这样便是一个完整的LCD1602写命令时序过程。


    http://mp.weixin.qq.com/s?__biz=MzA3OTIwODcyNQ==&mid=201202558&idx=2&sn=e58448f4fd53077f60e4a5bbfe2eec90&scene=5#rd

    展开全文
  • 粗略统计51烧录时序,使用的单片机为STC11FXX,烧录软件为stc-isp-15xx-v6.72.


    STC51烧录程序时序分析


    Note1:关于STC51的冷启动下载和复位


    STC51单片机里面有一段出厂时固化的程序,这段程序的作用是检测串口是否要下载程序,不需要则执行单片机内的用户程序。每次启动时运行这端程序,这就是为什么每次下载时要冷启动。而复位后单片机是从地址0X0000H处开始执行,地址0X0000H又会指向主程序入口,即主函数处,即片内下载的用户程序而不会执行前面已经固化的检测串口那段程序。这就是为什么单片机每次下载要冷启动,而复位不行。注意,程序前面的宏定义什么的东西不占用系统时间,所以主程序即主函数处。


    有一个办法可以给那些需要加电立刻启动的用户,STC单片机可以设置为加电时只有P1.0/P1.1为低电平时开始下载程序,否则直接执行用户程序。这种办法快,不用等前面检测串口的那段时间。STC51单片机冷启动是必须的而复位电路不是必须的,不管是冷启动还是手动启动。也就是想下载程序必须冷启动,而有没有复位电路无所谓。但是最小系统板上必须有复位电路,无论是上电复位,还是上电加手动复位,虽然在一些简单的程序中看不出区别,但是因为复位操作会对一些特殊寄存器产生影响,这样没有复位操作的话再次执行函数的时候会出现错误。单片机断电后在通电也会从主函数处继续执行,可能是单片机断电后地址自动回到0X0000H,但是那些特殊寄存器里的值不会改变。


    单片机的上电复位和手动复位是比较简单的电路,只要是根据单片机手册要求的复位时间TRESET要小于复位电路中的RC常数。


    单片机下载程序一般都是通过串口,即上位机(STC-ISP)与单片机的底层通信都是通过串口协议实现的,两者之间更为高级的通信协议是建立在串口协议上的。


    Note2:上位机(STC-ISP)检测单片机时序


     波特率即为串口发送每比特所占用的时间,此处为2400,即每比特所占用的时间为1/2400=4.167*E-4 (S);可以点击检测MCU选项,上位机开始与单片机握手。整个握手过程可以从图2.2中获得,图中橙色为上位机发送的询问脉冲,蓝色为单片机的响应脉冲



    2.1  STC-ISP软件


     



    2.2  握手过程


     


     


    由图2.2可知,上位机先发送检测信号,此时单片机需要冷启动,检测信号可以参看图2.3。在单片机冷启动后,单片机会运行内部固化程序,自行检测串口是否有上位机检测信号。一旦发现检测信号,单片机回复响应信息,响应信息波形可见图2.4,内容则可见图2.5


     



    2.3  检测信号



    2.4  响应信号


     



    2.5  回复内容


     


    上位机接收到单片机响应后的回复信号,波特率为2400,每比特所占时间大约为416us,可以根据图2.7对上位机的回复进行数据解析。


     



    2.6  上位机回复内容

     



    2.7  上位机回复内容


    Note3:上位机(STC-ISP)程序烧录


       总的烧录过程为:上位机先检测单片机,再通知单片机开始烧录程序,每次发一小段程序,单片机会有一个ACK,之后再发结束指令,图3.2接图3.1



    3.1  程序烧录



    3.2  程序烧录


    Note4总结





    图4.1   汇总





                                                                                               WOLF


     


    
    展开全文
  • 说到51单片机的时钟首先想到51时怎么工作的呢?微型控制器要想工作必须要有一个“动力”,对于51单片机来说,这个“动力”就是时钟源。一般应用上会外接一个12MHz的晶振作为时钟源。 一般第一种接法用的比较多。...

    说到51单片机的时钟首先想到51时怎么工作的呢?微型控制器要想工作必须要有一个“动力”,对于51单片机来说,这个“动力”就是时钟源。一般应用上会外接一个12MHz的晶振作为时钟源。

    一般第一种接法用的比较多。选择内部震荡方式时晶振旁边有两个小电容。这两个电容叫晶振的负载电容,分别接在晶振的两个脚上和对地的电容,一般在几十皮发。它会影响到晶振的谐振频率和输出幅度。晶振的负载电容=[(Cd*Cg)/(Cd+Cg)]+Cic+△C式中Cd,Cg为分别接在晶振的两个脚上和对地的电容,Cic(集成电路内部电容)+△C(PCB上电容)经验值为3至5pf。
      各种逻辑芯片的晶振引脚可以等效为电容三点式振荡器。晶振引脚的内部通常是一个反相器, 或者是奇数个反相器串联。在晶振输出引脚 XO 和晶振输入引脚 XI 之间用一个电阻连接, 对于 CMOS 芯片通常是数 M 到数十M 欧之间。 很多芯片的引脚内部已经包含了这个电阻, 引脚外部就不用接了。这个电阻是为了使反相器在振荡初始时处与线性状态, 反相器就如同一个有很大增益的放大器, 以便于起振。
      为什么要选择12MHz的晶振作为时钟源呢?
      这就要说51单片机内部的几个周期了:指令周期、机器周期、时钟周期、振荡周期。
    振荡周期是指为单片机提供定时信号的振荡源的周期或外部输入时钟的周期。
    时钟周期又称作状态周期或状态时间S,它是振荡周期的两倍,分为P1节拍和P2节拍,通常在P1节拍完成算术逻辑操作,在P2节拍完成内部寄存器之间的数据传送操作。(注意P1 和P2的相位关系 )
    一个机器周期由6个状态组成,如果把一条指令的执行过程分作几个基本操作,则将完成一个基本操作所需的时间称作机器周期。单片机的单周期指令执行时间就为一个机器周期。
    指令周期是执行一条指令所需的全部时间。MCS-51单片机的指令周期通常由1、2、4个机器周期组成。
    对于12MHz的晶振,提供了12M的振荡周期,6M的时钟周期,1M的机器周期。所以此时51单片机的单指令周期为(1/1M)s=1us.这个1us有什么好处呢?后面大家会知道单片机应用中会有各种各样的时序,很多时候需要用一小段程序延时来达到时序要求,而1us的单指令周期比较方便计算延时时间以及确定每条语句的执行时间。应该明确的是单片机执行每条语句用的时间是非常确定的,是1us就是1us不会多一点也不会少一点。
    下面说说51单片机的总线扩展:

    这种结构就是上一篇说到的三总线结构,如图所示
    1、数据总线
    51 单片机的数据总线为P0 口,CPU 从P0 口送出和读回数据。
    2、地址总线
    51 系列单片机的地址总线为16 位。
    为了节约芯片引脚,采用P0 口复用方式,除了作为数据总线外,在ALE 信号时序匹配下,通过外置的数据锁存器,在总线访问前半周期从P0口送出低8位地址,后半周期从P0 口送出8 位数据。
    高8位地址则通过P2 口送出。
    3、控制总线
    51 系列单片机的控制总线包括(RD)读控制信号P3.7 和(WR)写控制信号P3.6 等,二者分别作为总线模式下数据读和数据写的使能信号。
    51 单片机总线时序如图 所示。

    从图2 中可以看出,完成一次总线( 读写) 操作周期为T,P0 口分时复用,在T0 期间,P0 口送出低8 位地址,在ALE 的下降沿完成数据锁存,送出低8位地址信号。在T1 期间,P0 口作为数据总线使用,送出或读入数据,数据的读写操作在读、写控制信号的低电平期间完成。
    需要注意的是,在控制信号( 读、写信号) 有效期间,P2 口送出高8位地址,配合数据锁存器输出的低8 位地址,实现16 位地址总线,即64kB 范围的内的寻址。
    由于CPU不可能同时执行读和写操作,所以读、写信号不可能同时有效。
    采用这种总线结构按照规定的时序能够最大程度的利用单片机的全部地址线。这就是单片机的编址。编址分为线选法和译码法,译码法分为全译码、部分译码。线选法的优点是简单但是非常浪费地址线;全译码法能够最大程度的利用全部地址线,但是电路很复杂需要一些与非门的配合才行。一般用部分译码法性价比最高,尤其是在外接了存储芯片时部分译码法很方便。这里写图片描述线选法
    这里写图片描述

    展开全文
  • ws2812总结 声明 本文版权归作者bxgj所有,未经作者授权,本文禁止以任何形式在任何平台(包括但不限于各网站、论坛、博客、微博、公众号等)部分或全部地转载,禁止二次修改后声明原创。授权转载内容请注明出处(如...

    声明

    本文版权归作者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。

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

    指令集 STC-Y1 STC-Y3 STC-Y5 STC-Y6
    代表型号 STC89C52 STC12C5A60S2 STC15W4K32S4 STC8A8K64S4A12
    MOV A, Rn
    (寄存器送入累加器)
    12 1 1 1
    MOV Rn, A
    (累加器送入寄存器)
    12 2 1 1
    MOV Rn, #data
    (立即数送入寄存器)
    12 2 2 1
    MOV bit, C
    (进位位送入直接地址位)
    24 4 3 1
    RL A
    (累加器循环左移)
    12 1 1 1
    RLC A
    (累加器带进位循环左移)
    12 1 1 1
    CLR A
    (清零累加器)
    12 1 1 1
    CLR C
    (清零进位位)
    12 1 1 1
    CLR bit
    (清零直接地址位)
    12 4 3 1
    SETB bit
    (置一直接地址位)
    12 4 3 1
    INC Rn
    (寄存器加一)
    12 3 2 1
    DEC Rn
    (寄存器减一)
    12 3 2 1
    ADD A, direct
    (直接地址单元中的数据加到累加器)
    12 3 2 1
    SUBB A, #data
    (累加器带借位键立即数)
    12 2 2 1
    JZ rel
    (累加器为零转移)
    24 3 4 条件不成立不转移1时钟;条件成立则转移3时钟
    JNC rel
    (进位为零转移)
    24 3 3 条件不成立不转移1时钟;条件成立则转移3时钟
    CJNE Rn, #data, rel
    (寄存器与立即数相比较,不相等则转移)
    24 4 4 条件不成立不转移2时钟;条件成立则转移3时钟
    SJMP rel
    (相对转移)
    24 3 3 3
    DJNZ Rn, rel
    (寄存器减1,非零转移)
    24 4 4 条件不成立不转移2时钟;条件成立则转移3时钟
    LCALL addr16
    (长调用子程序)
    24 6 4 3
    RET
    (子程序返回)
    24 4 4 3
    NOP
    (空指令)
    12 1 1 1

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

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

    void ws2812asm(unsigned char dat)
    {
    #pragma asm
    	MOV A, R7
    	MOV R6, #0x08
    WS2812LOOP:
    	SETB P3.7
    	RLC A
    	MOV P3.7, C
    	NOP
    	CLR P3.7
    	DJNZ 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状态为低电平

    CLK 时间/us 语句 IO状态 说明
    1 0.083 SETB P3.7 0
    2 0.166 SETB P3.7 0
    3 0.250 SETB P3.7 0
    4 0.333 RLC A 1 SETB语句执行完毕,IO被置高
    5 0.417 MOV P3.7, C 1
    6 0.500 MOV P3.7, C 1
    7 0.583 MOV P3.7, C 1
    8 0.667 NOP 0/1 这里IO的状态为0或1
    9 0.750 CLR P3.7 0/1 如果发送的数据为0即为0
    10 0.833 CLR P3.7 0/1 否则为1
    11 0.916 CLR P3.7 0/1
    12 1.000 DJNZ R6, WS2812LOOP 0
    13 1.083 DJNZ R6, WS2812LOOP 0
    14 1.166 DJNZ R6, WS2812LOOP 0
    15 1.250 DJNZ R6, WS2812LOOP 0

    如果发送的数据为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    MOV     R7,#0x08
    C:0x19C0    NOP
    C:0x19C1    DJNZ    R7,C:19C0
    C:0x19C3    RET
    

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

    void test2()
    {
    	u8 i = 8;
    	while(i)
    	{
    		i--;
    		_nop_();
    	}
    }
    
    C:0x199C    MOV     R7,#0x08
    C:0x199E    MOV     A,R7
    C:0x199F    JZ      C:19A5
    C:0x19A1    DEC     R7
    C:0x19A2    NOP
    C:0x19A3    SJMP    C:199E
    C:0x19A5    RET
    

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

    void test3()
    {
    	u8 i = 0;
    	while(i < 8)
    	{
    		_nop_();
    		i++;
    	}
    }
    
    C:0x1993    CLR     A
    C:0x19B0    MOV     R7,A
    C:0x19B1    NOP
    C:0x19B2    INC     R7
    C:0x19B3    CJNE    R7,#0x08,C:19B1
    C:0x19B6    RET
    

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

    void test4()
    {
    	u8 i = 0;
    	while(i < 8)
    	{
    		i++;
    		_nop_();
    	}
    }
    
    C:0x008E    CLR     A
    C:0x008F    MOV     R7,A
    C:0x0090    MOV     A,R7
    C:0x0091    CLR     C
    C:0x0092    SUBB    A,#0x08
    C:0x0094    JNC     C:009A
    C:0x0096    INC     R7
    C:0x0097    NOP
    C:0x0098    SJMP    C:0090
    C: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    MOV      R5,#0x08
    C:0x0082    NOP
    C:0x0088    DJNZ     R5,C:0082
    C:0x008A    RET
    

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

    void test6()
    {
    	u8 i = 0;
    	for(i; i < 8; i++)
    	{
    		_nop_();
    	}
    }
    
    C:0x1965    CLR     A
    C:0x1966    MOV     R5,A
    C:0x1967    NOP
    C:0x196D    INC     R5
    C:0x196E    CJNE    R5,#0x08,C:1967
    C: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    MOV     R6,#0x08
    C:0x1AB5    MOV     A,R7
    C:0x1AB6    ADD     A,ACC(0xE0)
    C:0x1AB8    MOV     R7,A
    C:0x1AB9    SETB    P37(0xB0.7)
    C:0x1ABB    MOV     P37(0xB0.7),C
    C:0x1ABD    CLR     P37(0xB0.7)
    C:0x1ABF    MOV     A,R7
    C:0x1AC0    ADD     A,ACC(0xE0)
    C:0x1AC2    MOV     R7,A
    C:0x1AC3    DJNZ    R6,C:1AB9
    C: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主频自行修改!

    结论

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

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

    展开全文
  • 单片机的基本时序 单片机的基本时序与《汇编语言》中讲的一致,分为振荡周期,时钟周期(状态周期),机器周期,指令周期。晶振相当于提供给单片机一个基础时钟单元,在这个基础上进行一系列电子元器件的动作。...

    单片机的基本时序

    单片机的基本时序与《汇编语言》中讲的一致,分为振荡周期,时钟周期(状态周期),机器周期,指令周期。晶振相当于提供给单片机一个基础时钟单元,在这个基础上进行一系列电子元器件的动作。单片机常见的晶振有12MHZ,11.0592MHZ,6MHZ。
    各周期的关系
    T指令=(1——4)T机器=6T时钟=12T振荡

    单片机延时的实现方式

    1. 软件实现
      软件实现是指单片机连续多次执行同一条空语句。C语言中通常用带有_NOP_( )的语句实现,如Delay10us()、Delay25us()等子函数实现。
    void Delay10us(){
    _NOP_( );
    _NOP_( );
    _NOP_( );
    _NOP_( );
    _NOP_( );
    _NOP_( );
    }
    

    Delay10us()函数中执行6次_NOP_()语句,主函数调用该子函数时,先执行一个LCALL指令(2us),然后执行6次_NOP_( )语句(6us),最后执行RET指令(2us),所以共需要10us。`

    Delay40us(){
    Delay10us();
    Delay10us();
    Delay10us();
    Delay10us();
    }
    

    在Delay40us()中直接调用4次Delay10us()函数,得到的延时时间为42us,执行过程如下:执行循环时先执行一次LCALL指令(2us),内部每次为10us,共为42us,执行完最后一个子循环之后,直接返回主程序。

    Delay80us(){
    Delay40us();
    Delay40us();
    }
    

    上面的程序总延时时间为86us。只有最内层函数执行RET指令,该指令直接返回上级函数或主函数。如果Delay80us()中执行8次Delay10us()函数,则总延时时间为82us。

    1. 定时器实现
      单片机系统一般常选用11.0592MHZ、12MHZ、6MHZ的晶振,第一种更容易产生各种标准的波特率。若定时器工作为方式2,则可实现极短时间的精确延时;如使用其他定时方式,则要考虑重装定时初值的时间(重装定时器初值占用2个机器周期)。
      实际应用中,采用中断方式,使用T/C延时从程序的执行效率和稳定性来考虑最佳。C51编写的中断服务程序编译后会自动增加PUSH ACC、PUSH PSW、 POP PSW、POP ACC语句,执行时占用4个机器周期,如果程序中还有计数值+1 的语句,则又会占用1个机器周期,计算定时器初值时需要从初值中将这些时间减去达到最小误差的目的。
    展开全文
  • 通信分为并行通信和串行通信,并行通信时的数据各个位同时传送,可以实现字节为单位通信,但通信线多占用资源,成本高。以前用到的的P1=0x55,一次给P1口的8个管脚分别赋值,同时进行信号输出,类似于8个车道可以过去...
  • 我们都知道通信从大的方面有两种:串行和并行。串行的最大优点是占用总线少,但是传输速率...比如利用单片机显示数码管单纯的显示一个数码管如果仅仅是为了显示 那么动用单片机一个端口(如P0或P1/P2/P3)那没有什么...
  • 1、CPU时序的有关知识 1)振荡周期:为单片机提供定时信号的振荡源的周期(晶振周期或外加振荡周期) 2)状态周期:2个振荡周期为1个状态周期,用S表示。振荡周期又称S周期或时钟周期。 3)机器周期:1个机器...
  • 目前普遍采用的MCS51 和PIC 系列单片机通常只有...8250 通用同步/异步接收发送芯片(USART),需额外占用单片机I/O 资源。本文介绍一种用单片机普通I/O 口实现串行通信的方法,可在单片机的最小应用系统中实现与两个以
  • 单片机原理及应用》复习提纲 单片机应用系统的典型结构图   单片机应用系统核心硬件技术包括: 1.时序 2.中断 3.地址译码   单片机应用系统核心软件技术包括: 1.寻址...
  • 一、单片机时序 单片机工作时,是在统一的时钟脉冲控制下一拍一拍地进行的。由于指令的字节数不同,取这些指令所需要的时间也就不同,即使是字节数相同的指令,由于执行操作有较大的差别,不同的指令执行时间也不...
  • 如何读懂时序图?

    2018-10-01 18:11:01
    读懂时序图 前言 读时序图之前,首先要明确几个概念 引脚 首先,时序图一般指同步时序图,异步时序例如串口(UART/SCI)不在讨论之列 判断同步时序的方法很简单,就是看是否存在专门的Clock信号引脚,其次...
  • 单片机面试问题集

    2018-03-23 17:12:48
     单片机单片机的最小系统?内部的主要结构?答:最小系统:电源、晶振(为系统提供基本的时钟信号)、复位电路;内部结构:ROM/RAM、计时器、中断、I/O串并行口、总线扩展控制。RAM和ROM的区别?答:ROM(只读...
  • SPI总线协议及SPI时序图详解【转】 SPI,是英语Serial Peripheral Interface的缩写,顾名思义就是串行外围设备接口。SPI,是一种高速的,全双工,同步的通信总线,并且在芯片的管脚上只占用四根线,节约了芯片的...
  • 单片机为什么还在用C语言编程?答案是:C语言是最适合单片机编程...第二、单片机编程的一个主要对象是对单片机的端口和内部寄存器的操作和配置,这个需要比较精确的时序控制。 第三、单片机算法运算中,尽量使用加法、
  • GPIO模拟UART串口时序

    2018-05-03 00:41:46
    单片机只能使用定时器来模拟时序,并通过外部下降沿中断触发启动,实时性受到限制;对于实时性要求较高的应用,需要同时处理发送和接收时(全双工)1路UART需要使用2个定时器;而半双工应用可以只使用一个定时器...
  • 我们都知道通信从大的方面有两种:串行和并行。串行的最大优点是占用总线少,但是传输...比如利用单片机显示数码管单纯的显示一个数码管如果仅仅是为了显示 那么动用单片机一个端口(如P0或P1/P2/P3)那没有什么,当...
  • 文章目录00 写在前面01 C51基本数据类型总结02 C51数据类型扩充定义03 关于单片机04 单片机工作的基本时序05 单片机复位06 80C51的中断系统07 定时器08 串口通信09 C语言基础10 C51P3引脚第二功能11 结尾 ...
  • 今天用ADC偶然发现了这个问题,分享给各位朋友。 89的I2CDelay:#define I2CDelay() {_nop_();_nop_();_nop_();_nop_();}12的I2CDelay:void Delay_us(unsigned char us) { do { _nop_(); _nop_();...
  • 概述 该设计用51单片机做控制器,显示采用LCD1602液晶屏。可以测量频率、脉宽、占空比、周期。...1.定时器工作于方式1时在中断服务程序中需要重新给定时器赋装载值,赋装载值得过程需要占用一定的时间,...
1 2 3 4 5 ... 20
收藏数 1,922
精华内容 768
关键字:

单片机时序被占用