C语言易犯错误

179 篇文章 183 订阅

作者:zhzht19861011《嵌入式软件可靠性设计的一些理解》

来源:CSDN

原文:https://blog.csdn.net/zhzht19861011/article/details/17117819

版权声明:本文为博主原创文章,转载请附上博文链接!

 

一、指针的加减运算是特殊的。下面的代码运行在32位ARM架构上,执行之后,a和p的值分别是多少?

              int a=1;

              int *p=(int*)0x00001000;

              a=a+1;

              p=p+1;

       对于a的值很容判断出结果为2,但是p的结果却是0x00001004。指针p加1后,p的值增加了4,这是为什么呢?原因是指针做加减运算时是以指针的数据类型为单位。p+1实际上是p+1*sizeof(int)。不理解这一点,在使用指针直接操作数据时极易犯错。比如下面对连续RAM初始化零操作代码:

unsigned int *pRAMaddr;                   //定义地址指针变量

for(pRAMaddr=StartAddr;pRAMaddr<EndAddr;pRAMaddr+=4)

{

           *pRAMaddr=0x00000000;    //指定RAM地址清零

}

       由于pRAMaddr是一个指针变量,所以pRAMaddr+=4代码其实使pRAMaddr偏移了4*sizeof(int)=16个字节,所以每执行一次for循环,会使变量pRAMaddr偏移16个字节空间,但只有4字节空间被初始化为零。其它的12字节数据的内容,在大多数架构处理器中都会是随机数。

 

二、

对于sizeof(),这里强调两点,第一它是一个关键字,而不是函数,并且它默认返回无符号整形数据(要记住是无符号);

第二,使用sizeof获取数组长度时,不要对指针应用sizeof操作符,比如下面的例子:

void ClearRAM(char array[])
{
    int i ;
    for(i=0;i<sizeof(array)/sizeof(array[0]);i++)             //这里用法错误,array实际上是指针
       {
              array[i]=0x00;
       }
}


int main(void)
{
       char Fle[20];    
       ClearRAM(Fle);                   //只能清除数组Fle中的前四个元素
}

       我们知道,对于一个数组array[20],我们使用代码sizeof(array)/sizeof(array[0])可以获得数组的元素(这里为20),但数组名和指针往往是容易混淆的,而且有且只有一种情况下是可以当做指针的,那就是数组名作为函数形参时,数组名被认为是指针。同时,它不能再兼任数组名。注意只有这种情况下,数组名才可以当做指针,但不幸的是这种情况下容易引发风险。在ClearRAM函数内,作为形参的array[]不再是数组名了,而成了指针。sizeof(array)相当于求指针变量占用的字节数,在32位系统下,该值为4,sizeof(array)/sizeof(array[0])的运算结果也为4。

 

三、如果局部数组越界,可能引发ARM架构硬件异常。同事的一个设备用于接收无线传感器的数据,一次软件升级后,发现接收设备工作一段时间后会死机。调试表明ARM7处理器发生了硬件异常,异常处理代码是一段死循环(死机的直接原因)。接收设备有一个硬件模块用于接收无线传感器的整包数据并存在自己的硬件缓冲区中,当一帧数据接收完成后,使用外部中断通知设备取数据,外部中断服务程序精简后如下所示:

       __irq ExintHandler(void)
       {
              unsignedchar DataBuf[50];
              GetData(DataBug);        //从硬件缓冲区取一帧数据
              …
       }

       由于存在多个无线传感器近乎同时发送数据的可能加之GetData()函数保护力度不够,数组DataBuf在取数据过程中发生越界。由于数组DataBuf为局部变量,被分配在堆栈中,同在此堆栈中的还有中断发生时的运行环境以及中断返回地址。溢出的数据将这些数据破坏掉,中断返回时PC指针可能变成一个不合法值,硬件异常由此产生。

 

四、volatile的错误

在模块A的源文件中,定义变量:

volatile unsigned int TimerCount=0;

       该变量用来在一个定时器服务程序中进行软件计时:

              TimerCount++;                           //读取IO端口1的值

       在模块A的头文件中,声明变量:

extern unsigned int TimerCount;   //这里漏掉了类型限定符volatile

       在模块B中,要使用TimerCount变量进行精确的软件延时:

              #include “...A.h”   //首先包含模块A的头文件
              …
              TimerCount=0;
              while(TimerCount>=TIMER_VALUE);      //延时一段时间


       实际上,这是一个死循环。由于模块A头文件中声明变量TimerCount时漏掉了volatile限定符,在模块B中,变量TimerCount是被当作unsigned int类型变量。由于寄存器速度远快于RAM,编译器在使用非volatile限定变量时是先将变量从RAM中拷贝到寄存器中,如果同一个代码块再次用到该变量,就不再从RAM中拷贝数据而是直接使用之前寄存器备份值。代码while(TimerCount>=TIMER_VALUE)中,变量TimerCount仅第一次执行时被使用,之后都是使用的寄存器备份值,而这个寄存器值一直为0,所以程序无限循环。下面的流程图说明了程序使用限定符volatile和不使用volatile的执行过程。

 

五、运算符优先级

 

六、 隐式转换和强制转换

       这又是C语言的一大诡异之处,它造成的危害程度与数组和指针有的一拼。语句或表达式通常应该只使用一种类型的变量和常量。然而,如果你混合使用类型,C使用一个规则集合来自动完成类型转换。这可能很方便,但也很危险。

       a.当出现在表达式里时,有符号和无符号的char和short类型都将自动被转换为int类型,在需要的情况下,将自动被转换为unsigned int(在short和int具有相同大小时)。这称为类型提升。提升在算数运算中通常不会有什么大的坏处,但如果位运算符 ~ 和 << 应用在基本类型为unsigned char或unsigned short 的操作数,结果应该立即强制转换为unsigned char或者unsigned short类型(取决于操作时使用的类型)。

       uint8_t  port =0x5aU;
       uint8_t  result_8;
       result_8= (~port) >> 4;


       假如我们不了解表达式里的类型提升,认为在运算过程中变量port一直是unsigned char类型的。我们来看一下运算过程:~port结果为0xa5,0xa5>>4结果为0x0a,这是我们期望的值。但实际上,result_8的结果却是0xfa!在ARM结构下,int类型为32位。变量port在运算前被提升为int类型:~port结果为0xffffffa5,0xa5>>4结果为0x0ffffffa,赋值给变量result_8,发生类型截断(这也是隐式的!),result_8=0xfa。经过这么诡异的隐式转换,结果跟我们期望的值,已经大相径庭!正确的表达式语句应该为:

       result_8=(unsigned char) (~port) >> 4;             /*强制转换*/

       b.在包含两种数据类型的任何运算里,两个值都会被转换成两种类型里较高的级别。类型级别从高到低的顺序是long double、double、float、unsigned long long、long long、unsigned long、long、unsigned int、int。这种类型提升通常都是件好事,但往往有很多程序员不能真正理解这句话,从而做一些想当然的事情,比如下面的例子,int类型表示16位。

       uint16_t  u16a = 40000;            /* 16位无符号变量*/
       uint16_t  u16b= 30000;          /*16位无符号变量*/
       uint32_t  u32x;                        /*32位无符号变量 */
       uint32_t  u32y;
       u32x = u16a +u16b;                /* u32x = 70000还是4464 ? */
       u32y =(uint32_t)(u16a + u16b);   /* u32y = 70000 还是4464 ? */


       u32x和u32y的结果都是4464(70000%65536)!不要认为表达式中有一个高类别uint32_t类型变量,编译器都会帮你把所有其他低类别都提升到uint32_t类型。正确的书写方式:

                    u32x = (uint32_t)u16a +(uint32_t)u16b;或者:
                    u32x = (uint32_t)u16a + u16b;


       后一种写法在本表达式中是正确的,但是在其它表达式中不一定正确,比如:

                   uint16_t u16a,u16b,u16c;
                   uint32_t  u32x;
                   u32x= u16a + u16b + (uint32_t)u16c;/*错误写法,u16a+ u16b仍可能溢出*/


       c.在赋值语句里,计算的最后结果被转换成将要被赋予值得那个变量的类型。这一过程可能导致类型提升也可能导致类型降级。降级可能会导致问题。比如将运算结果为321的值赋值给8位char类型变量。程序必须对运算时的数据溢出做合理的处理。

       很多其他语言,像Pascal语言(好笑的是C语言设计者之一曾撰文狠狠批评过Pascal语言),都不允许混合使用类型,但C语言不会限制你的自由,即便这经常引起Bug。

       d.当作为函数的参数被传递时,char和short会被转换为int,float会被转换为double。

       e.C语言支持强制类型转换,如果你必须要进行强制类型转换时,要确保你对类型转换有足够了解:

并非所有强制类型转换都是由风险的,把一个整数值转换为一种具有相同符号的更宽类型时,是绝对安全的。
精度高的类型强制转换为精度低的类型时,通过丢弃适当数量的最高有效位来获取结果,也就是说会发生数据截断,并且可能改变数据的符号位。
 精度低的类型强制转换为精度高的类型时,如果两种类型具有相同的符号,那么没什么问题;需要注意的是负的有符号精度低类型强制转换为无符号精度高类型时,会不直观的执行符号扩展,例如:

unsigned int bob;
signed char fred = -1;
 
bob=(unsigned int )fred;              /*发生符号扩展,此时bob为0xFFFFFFFF*/

 

七、

检测除数是否为零
检测运算溢出情况
2.5.1 有符号整数除法,仅检测除数为零就够了吗?

       两个整数相除,除了要检测除数是否为零外,还要检测除法是否溢出。对于一个signed long类型变量,它能表示的数值范围为:-2147483648 ~ +2147483647,如果让-2147483648 / -1,那么结果应该是+ 2147483648,但是这个结果已经超出了signed long所能表示的范围了。

#include <limits.h>
signed long sl1,sl2,result;
    /*初始化sl1和sl2*/
    if((sl2==0)||((sl1==LONG_MIN) && (sl2==-1))){
    //处理错误
} else {
    result = sl1 / sl2;
}

 

八、

1、关键数据多区备份,取数据采用“表决法”

       RAM中的数据在受到干扰情况下有可能被改变,对于系统关键数据必须进行保护。关键数据包括全局变量、静态变量以及需要保护的数据区域。数据备份与原数据不应该处于相邻位置,因此不应由编译器默认分配备份数据位置,而应该由程序员指定区域存储。可以将RAM分为3个区域,第一个区域保存原码,第二个区域保存反码,第三个区域保存异或码,区域之间预留一定量的“空白”RAM作为隔离。可以使用编译器的“分散加载”机制将变量分别存储在这些区域。需要进行读取时,同时读出3份数据并进行表决,取至少有两个相同的那个值。

        假如设备的RAM从0x1000_0000开始,我需要在RAM的0x1000_0000~0x10007FFF内存储原码,在0x1000_9000~0x10009FFF内存储反码,在0x1000_B000~0x1000BFFF内存储0xAA的异或码,编译器的分散加载可以设置为:

1.	LR_IROM1 0x00000000 0x00080000  {    ; load region size_region  
2.	  ER_IROM1 0x00000000 0x00080000  {  ; load address = execution address  
3.	   *.o (RESET, +First)  
4.	   *(InRoot$$Sections)  
5.	   .ANY (+RO)  
6.	  }  
7.	  RW_IRAM1 0x10000000 0x00008000  {  ;保存原码  
8.	   .ANY (+RW +ZI )  
9.	  }  
10.	    
11.	  RW_IRAM3 0x10009000 0x00001000{    ;保存反码  
12.	   .ANY (MY_BK1)  
13.	  }  
14.	    
15.	  RW_IRAM2 0x1000B000 0x00001000  {  ;保存异或码  
16.	   .ANY (MY_BK2)  
17.	  }  
18.	} 

      如果一个关键变量需要多处备份,可以按照下面方式定义变量,将三个变量分别指定到三个不连续的RAM区中,并在定义时按照原码、反码、0xAA的异或码进行初始化。

1.	uint32  plc_pc=0;                                                       //原码  
2.	__attribute__((section("MY_BK1"))) uint32 plc_pc_not=~0x0;              //反码  
3.	__attribute__((section("MY_BK2"))) uint32 plc_pc_xor=0x0^0xAAAAAAAA;    //异或码

当需要写这个变量时,这三个位置都要更新;读取变量时,读取三个值做判断,取至少有两个相同的那个值。

      为什么选取异或码而不是补码?这是因为MDK的整数是按照补码存储的,正数的补码与原码相同,在这种情况下,原码和补码是一致的,不但起不到冗余作用,反而对可靠性有害。比如存储的一个非零整数区因为干扰,RAM都被清零,由于原码和补码一致,按照3取2的“表决法”,会将干扰值0当做正确的数据。
 

2、非易失性存储器的数据存储

       非易失性存储器包括但不限于Flash、EEPROM、铁电。仅仅将写入非易失性存储器中的数据再读出校验是不够的。强干扰情况下可能导致非易失性存储器内的数据错误,在写非易失性存储器的期间系统掉电将导致数据丢失,因干扰导致程序跑飞到写非易失性存储器函数中,将导致数据存储紊乱。一种可靠的办法是将非易失性存储器分成多个区,每个数据都将按照不同的形式写入到这些分区中,需要进行读取时,同时读出多份数据并进行表决,取相同数目较多的那个值。

       对于因干扰导致程序跑飞到写非易失性存储器函数,还应该配合软件锁以及严格的入口检验,单单依靠写数据到多个区是不够的也是不明智的,应该在源头进行阻截。

3、软件锁

       软件锁可以实现但不局限于环环相扣。对于初始化序列或者有一定先后顺序的函数调用,为了保证调用顺序或者确保每个函数都被调用,我们可以使用环环相扣,实质上这也是一种软件锁。此外对于一些安全关键代码语句(是语句,而不是函数),可以给它们设置软件锁,只有持有特定钥匙的,才可以访问这些关键代码。比如,向Flash写一个数据,我们会判断数据是否合法、写入的地址是否合法,计算要写入的扇区。之后调用写Flash子程序,在这个子程序中,判断扇区地址是否合法、数据长度是否合法,之后就要将数据写入Flash。由于写Flash语句是安全关键代码,所以程序给这些语句上锁:必须具有正确的钥匙才可以写Flash。这样即使是程序跑飞到写Flash子程序,也能大大降低误写的风险。

1.	/**************************************************************************** 
2.	* 名称:RamToFlash() 
3.	* 功能:复制RAM的数据到FLASH,命令代码51。 
4.	* 入口参数: dst        目标地址,即FLASH起始地址。以512字节为分界 
5.	*           src        源地址,即RAM地址。地址必须字对齐 
6.	*           no         复制字节个数,为512/1024/4096/8192 
7.	*           ProgStart  软件锁标志    
8.	* 出口参数:IAP返回值(paramout缓冲区) CMD_SUCCESS,SRC_ADDR_ERROR,DST_ADDR_ERROR, 
9.	SRC_ADDR_NOT_MAPPED,DST_ADDR_NOT_MAPPED,COUNT_ERROR,BUSY,未选择扇区 
10.	****************************************************************************/  
11.	void  RamToFlash(uint32 dst, uint32 src, uint32 no,uint8 ProgStart)  
12.	{     
13.	    PLC_ASSERT("Sector number",(dst>=0x00040000)&&(dst<=0x0007FFFF));  
14.	    PLC_ASSERT("Copy bytes number is 512",(no==512));  
15.	    PLC_ASSERT("ProgStart==0xA5",(ProgStart==0xA5));  
16.	      
17.	    paramin[0] = IAP_RAMTOFLASH;             // 设置命令字  
18.	    paramin[1] = dst;                        // 设置参数  
19.	    paramin[2] = src;  
20.	    paramin[3] = no;  
21.	    paramin[4] = Fcclk/1000;                      
22.	    if(ProgStart==0xA5)                     //只有软件锁标志正确时,才执行关键代码  
23.	    {                                       //当跑飞到这个地方,ProgStart不为0xA5,也不执行,起到保护关键代码的作用
24.	        iap_entry(paramin, paramout);       // 调用IAP服务程序                 
25.	        ProgStart=0;   
26.	    }  
27.	    else  
28.	    {  
29.	        paramout[0]=PROG_UNSTART;     
30.	    }  
31.	}

4、通信数据的检错

      通讯线上的数据误码相对严重,通讯线越长,所处的环境越恶劣,误码会越严重。抛开硬件和环境的作用,我们的软件应能识别错误的通讯数据。对此有一些应用措施:

 制定协议时,限制每帧的字节数;
           每帧字节数越多,发生误码的可能性就越大,无效的数据也会越多。对此以太网规定每帧数据不大于1500字节,高可靠性的CAN收发器规定每帧数据不得多于8字节,对于RS485,基于RS485链路应用最广泛的Modbus协议一帧数据规定不超过256字节。因此,建议制定内部通讯协议时,使用RS485时规定每帧数据不超过256字节;

 使用多种校验
           编写程序时应使能奇偶校验,每帧超过16字节的应用,建议至少编写CRC16校验程序;

 增加额外判断
           1)增加缓冲区溢出判断。这是因为数据接收多是在中断中完成,编译器检测不出缓冲区是否溢出,需要手动检查,在上文介绍数据溢出一节中已经详细说明。

           2)增加超时判断。当一帧数据接收到一半,长时间接收不到剩余数据,则认为这帧数据无效,重新开始接收。可选,跟不同的协议有关,但缓冲区溢出判断必须实现。这是因为对于需要帧头判断的协议,上位机可能发送完帧头后突然断电,重启后上位机是从新的帧开始发送的,但是下位机已经接收到了上次未发送完的帧头,所以上位机的这次帧头会被下位机当成正常数据接收。这有可能造成数据长度字段为一个很大的值,填满该长度的缓冲区需要相当多的数据(比如一帧可能1000字节),影响响应时间;另一方面,如果程序没有缓冲区溢出判断,那么缓冲区很可能溢出,后果是灾难性的。

 重传机制
           如果检测到通讯数据发生了错误,则要有重传机制重新发送出错的帧。

5、初始化信息的保存与恢复

       微处理器的寄存器值也可能会因外界干扰而改变,外设初始化值需要在寄存器中长期保存,最容易被破坏。由于Flash中的数据相对不易被破坏,可以将初始化信息预先写入Flash,待程序空闲时比较与初始化相关的寄存器值是否被更改,如果发现非法更改则使用Flash中的值进行恢复。

        公司目前使用的4.3寸LCD显示屏抗干扰能力一般。如果显示屏与控制器之间的排线距离过长或者对使用该显示屏的设备打静电或者脉冲群,显示屏有可能会花屏或者白屏。对此,我们可以将初始化显示屏的数据保存在Flash中,程序运行后,每隔一段时间从显示屏的寄存器读出当前值和Flash存储的值相比较,如果发现两者不同,则重新初始化显示屏。

6、陷阱

       对于8051内核单片机,由于没有相应的硬件支持,可以用纯软件设置软件陷阱,用来拦截一些程序跑飞。对于ARM7或者Cortex-M系列单片机,硬件已经内建了多种异常,软件需要根据硬件异常来编写陷阱程序,用来快速定位甚至恢复错误。

7、 while循环

       有时候程序员会使用while(!flag);语句来等待标志flag改变,比如串口发送时用来等待一字节数据发送完成。这样的代码时存在风险的,如果因为某些原因标志位一直不改变则会造成系统死机。良好冗余的程序是设置一个超时定时器,超过一定时间后,强制程序退出while循环。
 

8、对调试函数进一步封装

      上文说到,我们增加的调试语句应能很方便的从最终发行版中去掉,因此我们不能直接调用printf或者自定义的UARTprintf函数,需要将这些调试函数做一层封装,以便随时从代码中去除这些调试语句。参考方法如下:

#ifdef MY_DEBUG  
#define MY_DEBUGF(message) do { \  
                                  {UARTprintf message;} \  
                               } while(0)  
#else    
#define MY_DEBUGF(message)    
#endif /* PLC_DEBUG */


在我们编码测试期间,定义宏MY_DEBUG,并使用宏MY_DEBUGF(注意比前面那个宏多了一个‘F’)输出调试信息。经过预处理后,宏MY_DEBUGF(message)会被UARTprintf message代替,从而实现了调试信息的输出;当正式发布时,只需要将宏MY_DEBUG注释掉,经过预处理后,所有MY_DEBUGF(message)语句都会被空格代替,而从将调试信息从代码中去除掉。

9、巧用结构体使代码简化

我们可以先提取相同的元素,将之组织成数据结构:

1.	typedef struct {  
2.	    uint8_t  lcd_command;           //LCD寄存器  
3.	    uint8_t  lcd_get_value[8];      //初始化时写入寄存器的值  
4.	    uint8_t  lcd_value_num;         //初始化时写入寄存器值的数目  
5.	}lcd_redu_list_struct;  

     这里lcd_command表示的是LCD寄存器命令号;lcd_get_value是一个数组,表示寄存器要初始化的值,这是因为对于一个LCD寄存器,可能要初始化多个字节,这是硬件特性决定的;lcd_value_num是指一个寄存器要多少个字节的初值,这是因为每一个寄存器的初值数目是不同的,我们用同一个方法处理数据时,是需要这个信息的。

      就本例而言,我们将要处理的数据都是事先固定的,所以定义好数据结构后,我们可以将这些数据组织成表格:
 

1.	/*LCD部分寄存器设置值列表*/  
2.	lcd_redu_list_struct const lcd_redu_list_str[]=  
3.	{  
4.	  {SSD1963_Get_Address_Mode,{0x20}                                   ,1}, /*1*/ 
5.	  {SSD1963_Get_Pll_Mn      ,{0x3b,0x02,0x04}                         ,3}, /*2*/ 
6.	  {SSD1963_Get_Pll_Status  ,{0x04}                                   ,1}, /*3*  
7.	  {SSD1963_Get_Lcd_Mode    ,{0x24,0x20,0x01,0xdf,0x01,0x0f,0x00}     ,7}, /*4*/ 
8.	  {SSD1963_Get_Hori_Period ,{0x02,0x0c,0x00,0x2a,0x07,0x00,0x00,0x00},8}, /*5*/ 
9.	  {SSD1963_Get_Vert_Period ,{0x01,0x1d,0x00,0x0b,0x09,0x00,0x00}     ,7}, /*6*/ 
10.	  {SSD1963_Get_Power_Mode  ,{0x1c}                                   ,1}, /*7*/ 
11.	  {SSD1963_Get_Display_Mode,{0x03}                                   ,1}, /*8*/ 
12.	  {SSD1963_Get_Gpio_Conf   ,{0x0F,0x01}                              ,2}, /*9*/ 
13.	  {SSD1963_Get_Lshift_Freq ,{0x00,0xb8}                              ,2}, /*10* 
14.	}; 

    至此,我们就可以用一个处理过程来完成数十个LCD寄存器的读取、判断和异常处理了:

1.	/** 
2.	* lcd 显示冗余 
3.	* 每隔一段时间调用该程序一次 
4.	*/  
5.	void lcd_redu(void)  
6.	{  
7.	    uint8_t  tmp[8];  
8.	    uint32_t i,j;  
9.	    uint32_t lcd_init_flag;  
10.	      
11.	    lcd_init_flag =0;  
12.	    for(i=0;i<sizeof(lcd_redu_list_str)/sizeof(lcd_redu_list_str[0]);i++)  
13.	    {  
14.	        LCD_SendCommand(lcd_redu_list_str[i].lcd_command);  
15.	        uyDelay(10);  
16.	        for(j=0;j<lcd_redu_list_str[i].lcd_value_num;j++)  
17.	        {  
18.	            tmp[j]=LCD_ReadData();  
19.	            if(tmp[j]!=lcd_redu_list_str[i].lcd_get_value[j])  
20.	            {  
21.	                lcd_init_flag=0x55;  
22.	                //一些调试语句,打印出错的具体信息
23.	                goto handle_lcd_init;  
24.	            }  
25.	        }  
26.	    }  
27.	      
28.	    handle_lcd_init:  
29.	    if(lcd_init_flag==0x55)  
30.	    {  
31.	        //重新初始化LCD  
32.	        //一些必要的恢复措施  
33.	    }     
34.	}  

      通过合理的数据结构,我们可以将数据和处理过程分开,LCD冗余判断过程可以用很简洁的代码来实现。更重要的是,将数据和处理过程分开更有利于代码的维护。比如,通过实验发现,我们还需要增加一个LCD寄存器的值进行判断,这时候只需要将新增加的寄存器信息按照数据结构格式,放到LCD寄存器设置值列表中的任意位置即可,不用增加任何处理代码即可实现!这仅仅是数据结构的优势之一,使用数据结构还能简化编程,使复杂过程变的简单,这个只有实际编程后才会有更深的理解。
 

1、dma配置的时候由于长度没有设计好,导致其他变量被篡改的问题;

2、stm32以word保存变量到eeprom时候(或者flash)会出现hardfault,原因是哪个变量地址不是4字节对齐;

3、全局变量修改的时候要注意,需要"加锁"。多个地方(主循环、多个中断)同时修改某个变量,多个地方同时调用某个函数,需要做好处理(适时屏蔽中断)或者加互斥标志。

 

十、避免MCU初始化ZI变量:

     对于控制类产品,当系统复位后(非上电复位),可能要求保持住复位前RAM中的数据,用来快速恢复现场,或者不至于因瞬间复位而重启现场设备。而keil mdk在默认情况下,任何形式的复位都会将RAM区的非初始化变量数据清零。

      MDK编译程序生成的可执行文件中,每个输出段都最多有三个属性:RO属性、RW属性和ZI属性。对于一个全局变量或静态变量,用const修饰符修饰的变量最可能放在RO属性区,初始化的变量会放在RW属性区,那么剩下的变量就要放到ZI属性区了。默认情况下,ZI属性区的数据在每次复位后,程序执行main函数内的代码之前,由编译器“自作主张”的初始化为零。所以我们要在C代码中设置一些变量在复位后不被零初始化,那一定不能任由编译器“胡作非为”,我们要用一些规则,约束一下编译器。

      分散加载文件对于连接器来说至关重要,在分散加载文件中,使用UNINIT来修饰一个执行节,可以避免编译器对该区节的ZI数据进行零初始化。这是要解决非零初始化变量的关键。因此我们可以定义一个UNINIT修饰的数据节,然后将希望非零初始化的变量放入这个区域中。于是,就有了第一种方法:

      1)       修改分散加载文件,增加一个名为MYRAM的执行节,该执行节起始地址为0x1000A000,长度为0x2000字节(8KB),由UNINIT修饰:
 

            1:   LR_IROM1 0x00000000 0x00080000  {    ; load region size_region
   	    2:   ER_IROM1 0x00000000 0x00080000  {  ; load address = execution address
   	    3:    *.o (RESET, +First)
   	    4:    *(InRoot$$Sections)
   	    5:    .ANY (+RO)
   	    6:   }
   	    7:   RW_IRAM1 0x10000000 0x0000A000  {  ; RW data
   	    8:    .ANY (+RW +ZI)
   	    9:   }
  	    10:   MYRAM 0x1000A000 UNINIT 0x00002000  {
  	    11:    .ANY (NO_INIT)
  	    12:   }
  	    13: }

  那么,如果在程序中有一个数组,你不想让它复位后零初始化,就可以这样来定义变量:

1.	unsigned char  plc_eu_backup[32] __attribute__((at(0x1000A000)));

      变量属性修饰符__attribute__((at(adde)))用来将变量强制定位到adde所在地址处。由于地址0x1000A000开始的8KB区域ZI变量不会被零初始化,所以位于这一区域的数组plc_eu_backup也就不会被零初始化了。

      这种方法的缺点是显而易见的:要程序员手动分配变量的地址。如果非零初始化数据比较多,这将是件难以想象的大工程(以后的维护、增加、修改代码等等)。所以要找到一种办法,让编译器去自动分配这一区域的变量。

      2)       分散加载文件同方法1,如果还是定义一个数组,可以用下面方法:

unsigned char  plc_eu_backup[32] __attribute__((section("NO_INIT"),zero_init));

      变量属性修饰符__attribute__((section(“name”),zero_init))用于将变量强制定义到name属性数据节中,zero_init表示将未初始化的变量放到ZI数据节中。因为“NO_INIT”这显性命名的自定义节,具有UNINIT属性。

      3)       将一个模块内的非初始化变量都非零初始化

       假如该模块名字为test.c,修改分散加载文件如下所示:
 

        1: LR_IROM1 0x00000000 0x00080000  {    ; load region size_region
   	2:   ER_IROM1 0x00000000 0x00080000  {  ; load address = execution address
   	3:    *.o (RESET, +First)
   	4:    *(InRoot$$Sections)
   	5:    .ANY (+RO)
   	6:   }
   	7:   RW_IRAM1 0x10000000 0x0000A000  {  ; RW data
   	8:    .ANY (+RW +ZI)
   	9:   }
  	10:   RW_IRAM2 0x1000A000 UNINIT 0x00002000  {
  	11:    test.o (+ZI)
  	12:   }
  	13: }

 

  • 1
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
智慧校园整体解决方案是响应国家教育信息化政策,结合教育改革和技术创新的产物。该方案以物联网、大数据、人工智能和移动互联技术为基础,旨在打造一个安全、高效、互动且环保的教育环境。方案强调从数字化校园向智慧校园的转变,通过自动数据采集、智能分析和按需服务,实现校园业务的智能化管理。 方案的总体设计原则包括应用至上、分层设计和互联互通,确保系统能够满足不同用户角色的需求,并实现数据和资源的整合与共享。框架设计涵盖了校园安全、管理、教学、环境等多个方面,构建了一个全面的校园应用生态系统。这包括智慧安全系统、校园身份识别、智能排课及选课系统、智慧学习系统、精品录播教室方案等,以支持个性化学习和教学评估。 建设内容突出了智慧安全和智慧管理的重要性。智慧安全管理通过分布式录播系统和紧急预案一键启动功能,增强校园安全预警和事件响应能力。智慧管理系统则利用物联网技术,实现人员和设备的智能管理,提高校园运营效率。 智慧教学部分,方案提供了智慧学习系统和精品录播教室方案,支持专业级学习硬件和智能化网络管理,促进个性化学习和教学资源的高效利用。同时,教学质量评估中心和资源应用平台的建设,旨在提升教学评估的科学性和教育资源的共享性。 智慧环境建设则侧重于基于物联网的设备管理,通过智慧教室管理系统实现教室环境的智能控制和能效管理,打造绿色、节能的校园环境。电子班牌和校园信息发布系统的建设,将作为智慧校园的核心和入口,提供教务、一卡通、图书馆等系统的集成信息。 总体而言,智慧校园整体解决方案通过集成先进技术,不仅提升了校园的信息化水平,而且优化了教学和管理流程,为学生、教师和家长提供了更加便捷、个性化的教育体验。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值