C51,faster,faster,we need faster

  最近在帮同学做一个32*56的点阵屏,是用洞洞板做的,那个连线,那个密密麻麻,那个心碎啊......打住,打住,这不是今天的主要内容,这个留着下次说吧!
  在历尽千辛万苦,排除种种错焊,重重虚焊,终于将电路连接焊接好后,用51驱动却发现屏幕闪得厉害。要知道,我已经将原来单片机的11.0592M晶振换成32M晶振了(最高可以跑40M的SST89E58RDA换上40M晶振不知怎么的反而只有10来M)。诶,早知道之前在设计电路的时候就应该计算好,当时初略估计了下就以为速度没问题,于是为了节省引脚,就将点阵屏的驱动全部用了74HC595,整个屏幕总共用了18个595,所有的数据都是串行传送。现在可好了,屏幕闪烁了,怎么办?
  昨天纠结了一晚上,一直在想如何提高速度,最后都快没办法了,已经打算今天进行电路改造了(这种电路,好几百条的飞线,要下定这个决心真不容易啊)。今天早上起晚了,不想去实验室,于是想在寝室先调一调,看能不能软件上解决。结果真有收获,于是就决定不改电路了,花了一下午时间进行各种测试对比,研究如何让C51执行尽可能快。
  这次仿真时间是用的Keil3的Logical Analyzer(之前要嘛用keil2,要嘛用keil4,keil2没这个功能,kiel4不知怎么的不能仿真,网上也有人有这个问题,于是一直忘了keil有这个功能,虽然仿真时间和实际可能不一定相等,但是比较时间还是很可靠的),我一开始使用的的串行传送数据代码如下:
void SendData(u8 byte1,u8 byte2)
{
        data u8 i;
        
        for(i = 0;i < 8;i++)
        {                        
                COL_SCK_L;
                if(byte1 & (0x80 >> i))
                        COL_SI_H;
                else
                        COL_SI_L;
                COL_SCK_H;
        }
        for(i = 0;i < 8;i++)
        {                        
                COL_SCK_L;
                if(byte2 & (0x80 >> i))
                        COL_SI_H;
                else
                        COL_SI_L;
                COL_SCK_H;
        }
        COL_RCK_L;
        COL_RCK_H;
}

在主函数的while循环里要调用它:
while(1)
{
  ROW_SCK_L;
  ROW_SI_H;
  ROW_SCK_H;

  ROW_RCK_L;
  ROW_RCK_H;

  ROW_SI_L;
  for(j=0 ; j<112 ; j++)
  {
    SendData(Hanzi[(j*2)+1],Hanzi[(j*2)]);

    ROW_SCK_L;
    ROW_SCK_H;

    ROW_RCK_L;
    ROW_RCK_H;
  }

 }


一开始的时候我的keil里的优化等级选择lever 9(faver speed),整个for循环执行一次大约要416us,也就是说发送一次16位数据要416us,显示一帧要发送112次16位字节也就是要46.592ms(实际时间应该不止,不然应该不会有闪烁感)。
  优化等级已经达到最高了,是不是意味着速度也已经达到最优了。抱着试试看的态度,我试了下,改成lever 0(faver speed),结果意想不到的事情发生了,发送一次数据时间只要400us左右。这是怎么回事,优化的效果还不如不优化的。试一下其他等级看看吧,于是我把所有的优化等级实验了一遍,得到结果如下表:
优化等级
for语句+移位(n位)
时间/us
0(speed)
400.50
1(speed)
400.19
2(speed)
400.50
3(speed)
338.36
4(speed)
350.27
5(speed)
350.27
6(speed)
314.79
7(speed)
313.88(size:798)
8(speed)
314.79
9(speed)
416.46(size:776)
9(size)
423.84(size:776)

  从实验数据可以看到,速度最快的是lever 7(faver speed),或者说lever 6、7、8都差不多将速度优化到了最佳,执行时间从416.46us减少到了313.88us,我们实现了faster。但是,这点速度的提高还是不能解决闪烁问题,we need faster!
  有办法吗?别急,让我们先看看Logic Analyzer的图形。

  P1.2两个脉冲之间的时间就是执行一次SendData的执行时间,P1.7是串行通信移位时钟。怎么?有没有发现问题?串行时钟的逻辑0,怎么持续时间不一样?先由小变大,后又由小变大,数了下,变大的周期为8个串行时钟周期。这是怎么回事,让我们来看看汇编代码吧。
 
   126:         for(i = 0;i < 8;i++) 
C:0x0225    E4       CLR      A
C:0x0226    FC       MOV      R4,A
   127:         {                      
   128:                 COL_SCK_L; 
   129:                 if(byte1 & (0x80 >> i)) 
C:0x0227    1202DD   LCALL    Com0038(C:02DD)
C:0x022A    8003     SJMP     C:022F
C:0x022C    1202E7   LCALL    L?0060(C:02E7)
C:0x022F    D8FB     DJNZ     R0,C:022C
C:0x0231    FF       MOV      R7,A
C:0x0232    E9       MOV      A,R1
C:0x0233    FB       MOV      R3,A
C:0x0234    EF       MOV      A,R7
C:0x0235    5B       ANL      A,R3
C:0x0236    6004     JZ       C:023C
   130:                         COL_SI_H; 
   131:                 else 
C:0x0238    D295     SETB     P1_5(0x90.5)
C:0x023A    8002     SJMP     C:023E
   132:                         COL_SI_L; 
C:0x023C    C295     CLR      P1_5(0x90.5)
   133:                 COL_SCK_H; 
C:0x023E    D297     SETB     P1_7(0x90.7)
   134:         } 
C:0x0240    0C       INC      R4
C:0x0241    BC08E3   CJNE     R4,#0x08,C:0227

这是SendData的主要执行代码,可以看到短短几句话,但是里面却还有两个LCALL,这两个LCALL是干什么用的呢?让我们再来看看吧:
C:0x02DD    C297     CLR      P1_7(0x90.7)
C:0x02DF    7480     MOV      A,#P0(0x80)
C:0x02E1    7E00     MOV      R6,#0x00
C:0x02E3    A804     MOV      R0,0x04
C:0x02E5    08       INC      R0
C:0x02E6    22       RET      
C:0x02E7    CE       XCH      A,R6
C:0x02E8    A2E7     MOV      C,0xE0.7
C:0x02EA    13       RRC      A
C:0x02EB    CE       XCH      A,R6
C:0x02EC    13       RRC      A
C:0x02ED    22       RET      

  不难分析C:0x02DD的子函数处的其实是for循环的处理和移位运算的初始化, C:0x02E7的子函数是一个移位函数。两个LCALL子函数其实挺小的,倒也不会产生很大问题,但是 (0x80 >> i)是要调用移位函数的,且移位的位数越多,调用的次数也越多,这也就为什么,串行时钟逻辑低电平的持续时间越来越长了。如果每次的时间都和第一次一样长多好啊!对,为什么不能做的一样长呢?干脆去掉移位,再把for循环也去了不就更省时间。于是,下面的代码就诞生了:
void SendData(u8 byte1,u8 byte2)
{
        COL_SCK_L;
        if(byte1 & 0x80)
                COL_SI_H;
        else
                COL_SI_L;
        COL_SCK_H;

        COL_SCK_L;
        if(byte1 & 0x40)
                COL_SI_H;
        else
                COL_SI_L;
        COL_SCK_H;
                
        COL_SCK_L;
        if(byte1 & 0x20)
                COL_SI_H;
        else
                COL_SI_L;
        COL_SCK_H;

        COL_SCK_L;
        if(byte1 & 0x10)
                COL_SI_H;
        else
                COL_SI_L;
        COL_SCK_H;
                
        COL_SCK_L;
        if(byte1 & 0x08)
                COL_SI_H;
        else
                COL_SI_L;
        COL_SCK_H;

        COL_SCK_L;
        if(byte1 & 0x04)
                COL_SI_H;
        else
                COL_SI_L;
        COL_SCK_H;
                
        COL_SCK_L;
        if(byte1 & 0x02)
                COL_SI_H;
        else
                COL_SI_L;
        COL_SCK_H;

        COL_SCK_L;
        if(byte1 & 0x01)
                COL_SI_H;
        else
                COL_SI_L;
        COL_SCK_H;

        COL_SCK_L;
        if(byte2 & 0x80)
                COL_SI_H;
        else
                COL_SI_L;
        COL_SCK_H;

        COL_SCK_L;
        if(byte2 & 0x40)
                COL_SI_H;
        else
                COL_SI_L;
        COL_SCK_H;
                
        COL_SCK_L;
        if(byte2 & 0x20)
                COL_SI_H;
        else
                COL_SI_L;
        COL_SCK_H;

        COL_SCK_L;
        if(byte2 & 0x10)
                COL_SI_H;
        else
                COL_SI_L;
        COL_SCK_H;
                
        COL_SCK_L;
        if(byte2 & 0x08)
                COL_SI_H;
        else
                COL_SI_L;
        COL_SCK_H;

        COL_SCK_L;
        if(byte2 & 0x04)
                COL_SI_H;
        else
                COL_SI_L;
        COL_SCK_H;
                
        COL_SCK_L;
        if(byte2 & 0x02)
                COL_SI_H;
        else
                COL_SI_L;
        COL_SCK_H;

        COL_SCK_L;
        if(byte2 & 0x01)
                COL_SI_H;
        else
                COL_SI_L;
        COL_SCK_H;
                
        COL_RCK_L;
        COL_RCK_H;
} 

我想这是一段简单得再简单不过的代码了,没有什么复杂的语法,而且非常的冗长,但是如果我们想要得就是速度,牺牲一定空间又何乐而不为呢?we need faster,faster。让我们来看看代码执行的时间吧,你会吃一惊的(我们依然把实验从lever0到lever9做一遍):
优化等级
顺序语句
时间/us
0(speed)
68.23
1(speed)
68.23
2(speed)
68.19
3(speed)
66.70
4(speed)
65.31
5(speed)
65.21
6(speed)
63.33
7(speed)
63.37
8(speed)
63.37
9(speed)
63.31(size:935)
9(size)
70.87(size:935)
  怎么样,最少用时63.37,比最早时的速度整整提高了6.6倍。6.6倍,6.6倍啊,6.6倍意味着什么呢?意味着老子的屏幕不闪了,不闪了,不闪了......这是何等的欣慰啊!C51,faster,faster,we need faster!
  通过观察汇编窗口,发现这种代码在lever 9下的代码已经几乎是你能想到的最快的运行速度了。我们来看看我们现在代码的情况吧:SendData执行时间63.31us,总代码长度(还有其他代码)935字节;而之前的代码如果取lever 7下最快的情况对比则是:SenData执行时间313.88us,总代码长度798字节。采取第二种代码的话,我们将以137字节的代码空间换取4.96倍的速度,对于ROM不紧张的情况下何乐而不为呢?
  到这里我的文章是不是该完了呢?呵呵,不是的,虽然我没能将代码的执行速度再提高了(优化主函数while循环里for循环等还可以更快的,这个就留着以后有空再优化),但是我们或许还有一个不错的方案。毕竟这个方案在速度不是很吃紧,而ROM去很吃紧的情况下没人想采用的。有没有想过,只要稍作修改,就可以让代码空间更小,而且速度是第一种的3倍。
  没骗你的,其实这个方法很简单,而且也应该是一种习惯。第一种方案的串行模拟代码其实是很没有效率的,该被摒弃的。让我们再看看第一种方案,之前我们就说过,这个方案效率很低的原因就是移位函数的大量调用。要想不使用移位似乎是不大可能,不过要是每次只移位一位就好了。这是可以做到的,而且其实很多代码都是这么做的,只是我们没有注意到它的好处。这个方案的代码如下:
void SendData(u8 byte1,u8 byte2)
{
        data u8 i;
        
        for(i = 0;i < 8;i++)
        {                        
                COL_SCK_L;
                if(byte1 & 0x80)
                        COL_SI_H;
                else
                        COL_SI_L;
                COL_SCK_H;
                byte1 <<= 1;
        }
        for(i = 0;i < 8;i++)
        {                        
                COL_SCK_L;
                if(byte2 & 0x80)
                        COL_SI_H;
                else
                        COL_SI_L;
                COL_SCK_H;
                byte2 <<= 1;
        }
        COL_RCK_L;
        COL_RCK_H;
} 

怎么样,只是稍加改动就做到了每次只移位一次,效果就大不一样,不信你看:
优化等级
For语句+移位(1位)
时间/us
0(speed)
139.25
1(speed)
139.45
2(speed)
139.25
3(speed)
137.94
4(speed)
136.59
5(speed)
136.47
6(speed)
100.75
7(speed)
100.75
8(speed)
100.79
9(speed)
100.71(size:759)
9(size)
108.15(size:759)
  在lever 9(faver speed)下,SendData执行时间为100.7us,总代码长度为759字节。
  这种方案和第一种方案在串行通信时都有用到,但是他们的效率差别可见一斑。我们应该在平时书写是注意自己的习惯,用第三种方案而不是第一种,because we need faster,and we also need small。
  下面把三种方案的实验结果做了张表,对比下,来研究下keil的编译等级:

优化等级

for语句+移位(n位)

顺序语句

For语句+移位(1位)

时间/us

时间/us

时间/us

0(speed)

400.50

68.23

139.25

1(speed)

400.19

68.23

139.45

2(speed)

400.50

68.19

139.25

3(speed)

338.36

66.70

137.94

4(speed)

350.27

65.31

136.59

5(speed)

350.27

65.21

136.47

6(speed)

314.79

63.33

100.75

7(speed)

313.88(size:798)

63.37

100.75

8(speed)

314.79

63.37

100.79

9(speed)

416.46(size:776)

63.31(size:935)

100.71(size:759)

9(size)

423.84(size:776)

70.87(size:935)

108.15(size:759)



  从上表我们可以总结一下几点:
1.在keil的优化选项选择faver speed可以使生成代码的执行时间更少,而且在编译等级高的情况下也可以使代码空间大小达到一个很高的优化,对于本程序在lever 9下达到了和faver size一样的代码大小。
2.提高keil里的优化等级绝大部分情况下可以提高生成代码的执行速度,但是有时候不是,比如方案一的lever 9(我想不知道这是不是keil一开始默认的优化等级是8的原因)。
3.keil优化等级对速度的优化基本上分3个阶梯,lever0~lever2为第一个阶梯,速度最慢;lever3~lever5为第二个阶梯,速度较第一阶梯有较大优化;lever6~lever9为第三阶梯,速度较第二阶梯又有了较大的优化。
4.对比3种方案可以得出,第三种方案较第二种方案多了for循环和一位移位,所以执行时间翻了快一倍,而第一种方案与第三种方案的不同在于第一种方案存在多位移位,结果其执行时间又翻了3倍左右。于是我们可以总结,在C51中,多位移位并不是执行效率很高的方法,要想办法避免使用。
5.本实验的结论主要是在for循环和数组的索引( SendData(Hanzi[(j*2)+1],Hanzi[(j*2)]); hanzi[]即数组,对数组的索引,其地址的计算获得在for语句下应该是和效率有直接相关的)下进行的,对这类代码应该有较高的参考性,至于对其他类型的代码的优化可能会有差异,在此不多做研究。不过这种循环较数组索引的代码结构很常见,所以应该具有较高的参考性。
  在下面,附上keil优化等级的说明,其能够对本实验的一些结果进行解释,大家可以细细研究:

级别 说明
0    常数合并:编译器预先计算结果,尽可能用常数代替表达式。包括运行地址计算。
     优化简单访问:编译器优化访问8051系统的内部数据和位地址。
     跳转优化:编译器总是扩展跳转到最终目标,多级跳转指令被删除。

1    死代码删除:没用的代码段被删除。
     拒绝跳转:严密的检查条件跳转,以确定是否可以倒置测试逻辑来改进或删除。

2    数据覆盖:适合静态覆盖的数据和位段被确定,并内部标识。BL51连接/定位器可以通
     过全局数据流分  ,选择可被覆盖的段。

3    窥孔优化:清除多余的MOV指令。这包括不必要的从存储区加载和常数加载操作。当存
     储空间或执行时间可节省时,用简单操作代替复杂操作。

4    寄存器变量:如有可能,自动变量和函数参数分配到寄存器上。为这些变量保留的存 储区就省略了。
     优化扩展访问:IDATA、XDATA、PDATA和CODE的变量直接包含在操作中。在多数时间没
     必要使用中间寄存器。
     局部公共子表达式删除:如果用一个表达式重复进行相同的计算,则保存第一次计算
     结果,后面有可能就用这结果。多余的计算就被删除。
     Case/Switch优化:包含SWITCH和CASE的代码优化为跳转表或跳转队列。

5    全局公共子表达式删除:一个函数内相同的子表达式有可能就只计算一次。中间结果
     保存在寄存器中,在一个新的计算中使用。
     简单循环优化:用一个常数填充存储区的循环程序被修改和优化。

6    循环优化:如果结果程序代码更快和有效则程序对循环进行优化。

7    扩展索引访问优化:适当时对寄存器变量用DPTR。对指针和数组访问进行执行速度和 代码大小优化。

8    公共尾部合并:当一个函数有多个调用,一些设置代码可以复用,因此减少程序大小 。

9    公共块子程序:检测循环指令序列,并转换成子程序。Cx51甚至重排代码以得到更大的循环序列。

你可能感兴趣的:(51单片机,C51)