嵌入式产品的可靠性自然与硬件密不可分,但在硬件确定、并且没有第三方测试的前提下,使用防御性编程思想写出的代码,往往具有更高的稳定性。
防御性编程首先需要认清c语言的种种缺陷和陷阱,c语言对于运行时的检查十分弱小,需要程序员谨慎的考虑代码,在必要的时候增加判断;防御性编程的另一个核心思想是假设代码运行在并不可靠的硬件上,外接干扰有可能会打乱程序执行顺序、更改ram存储数据等等。
1 具有形参的函数,需判断传递来的实参是否合法
程序员可能无意识的传递了错误参数;外界的强干扰可能将传递的参数修改掉,或者使用随机参数意外的调用函数,因此在执行函数主体前,需要先确定实参是否合法。
2 仔细检查函数的返回值
对函数返回的错误码,要进行全面仔细处理,必要时做错误记录。
3 防止指针越界
如果动态计算一个地址时,要保证被计算的地址是合理的并指向某个有意义的地方。特别对于指向一个结构或数组的内部的指针,当指针增加或者改变后仍然指向同一个结构或数组。
4 防止数组越界
数组越界的问题前文已经讲述的很多了,由于c不会对数组进行有效的检测,因此必须在应用中显式的检测数组越界问题。下面的例子可用于中断接收通讯数据。
在使用一些库函数时,同样需要对边界进行检查,比如下面的memset(recbuf,0,len)函数把recbuf指指向的内存区的前len个字节用0填充,如果不注意len的长度,就会将数组recbuf之外的内存区清零:
5 数学算数运算
5.1除法运算,只检测除数为零就可靠吗?
除法运算前,检查除数是否为零几乎已经成为共识,但是仅检查除数是否为零就够了吗?
考虑两个整数相除,对于一个signed long类型变量,它能表示的数值范围为:-2147483648 ~+2147483647,如果让-2147483648/ -1,那么结果应该是+2147483648,但是这个结果已经超出了signedlong所能表示的范围了。所以,在这种情况下,除了要检测除数是否为零外,还要检测除法是否溢出。
#include signed long sl1,sl2,result; /*初始化sl1和sl2*/ if((sl2==0)||(sl1==long_min && sl2==-1)) { //处理错误 } else { result = sl1 / sl2; } 5.2检测运算溢出
整数的加减乘运算都有可能发生溢出,在讨论未定义行为时,给出过一个有符号整形加法溢出判断代码,这里再给出一个无符号整形加法溢出判断代码段:
#include unsigned int a,b,result; /*初始化a,b*/ if(uint_max-a=sizeof(unsigned int)*char_bit) { //处理错误 } else { uresult=ui1<=0x00040000)&&(dst<=0x0007ffff)); plc_assert(copy bytes number is 512,(no==512)); plc_assert(progstart==0xa5,(progstart==0xa5)); paramin[0] = iap_ramtoflash; // 设置命令字 paramin[1] = dst; // 设置参数 paramin[2] = src; paramin[3] = no; paramin[4] = fcclk/1000; if(progstart==0xa5) //只有软件锁标志正确时,才执行关键代码 { iap_entry(paramin, paramout); // 调用iap服务程序 progstart=0; } else { paramout[0]=prog_unstart; } } 该程序段是编程lpc1778内部flash,其中调用iap程序的函数iap_entry(paramin, paramout)是关键安全代码,所以在执行该代码前,先判断一个特定设置的安全锁标志progstart,只有这个标志符合设定值,才会执行编程flash操作。如果因为意外程序跑飞到该函数,由于progstart标志不正确,是不会对flash进行编程的。
10 通信
通讯线上的数据误码相对严重,通讯线越长,所处的环境越恶劣,误码会越严重。抛开硬件和环境的作用,我们的软件应能识别错误的通讯数据。对此有一些应用措施:
制定协议时,限制每帧的字节数;
每帧字节数越多,发生误码的可能性就越大,无效的数据也会越多。对此以太网规定每帧数据不大于1500字节,高可靠性的can收发器规定每帧数据不得多于8字节,对于rs485,基于rs485链路应用最广泛的modbus协议一帧数据规定不超过256字节。因此,建议制定内部通讯协议时,使用rs485时规定每帧数据不超过256字节;
使用多种校验
编写程序时应使能奇偶校验,每帧超过16字节的应用,建议至少编写crc16校验程序。
增加额外判断
1)增加缓冲区溢出判断。这是因为数据接收多是在中断中完成,编译器检测不出缓冲区是否溢出,需要手动检查,在上文介绍数据溢出一节中已经详细说明。
2)增加超时判断。当一帧数据接收到一半,长时间接收不到剩余数据,则认为这帧数据无效,重新开始接收。可选,跟不同的协议有关,但缓冲区溢出判断必须实现。这是因为对于需要帧头判断的协议,上位机可能发送完帧头后突然断电,重启后上位机是从新的帧开始发送的,但是下位机已经接收到了上次未发送完的帧头,所以上位机的这次帧头会被下位机当成正常数据接收。这有可能造成数据长度字段为一个很大的值,填满该长度的缓冲区需要相当多的数据(比如一帧可能1000字节),影响响应时间;另一方面,如果程序没有缓冲区溢出判断,那么缓冲区很可能溢出,后果是灾难性的。
重传机制
如果检测到通讯数据发生了错误,则要有重传机制重新发送出错的帧。
11 开关量输入的检测、确认
开关量容易受到尖脉冲干扰,如果不进行滤除,可能会造成误动作。一般情况下,需要对开关量输入信号进行多次采样,并进行逻辑判断直到确认信号无误为止。
12 开关量输出
开关信号简单的一次输出是不安全的,干扰信号可能会翻转开关量输出的状态。采取重复刷新输出可以有效防止电平的翻转。
13 初始化信息的保存和恢复
微处理器的寄存器值也可能会因外界干扰而改变,外设初始化值需要在寄存器中长期保存,最容易被破坏。由于flash中的数据相对不易被破坏,可以将初始化信息预先写入flash,待程序空闲时比较与初始化相关的寄存器值是否被更改,如果发现非法更改则使用flash中的值进行恢复。
公司目前使用的4.3寸lcd显示屏抗干扰能力一般。如果显示屏与控制器之间的排线距离过长或者对使用该显示屏的设备打静电或者脉冲群,显示屏有可能会花屏或者白屏。
对此,我们可以将初始化显示屏的数据保存在flash中,程序运行后,每隔一段时间从显示屏的寄存器读出当前值和flash存储的值相比较,如果发现两者不同,则重新初始化显示屏。下面给出校验源码,仅供参考。
定义数据结构:
定义const修饰的结构体变量,存储lcd部分寄存器的初始值,这个初始值跟具体的应用初始化有关,不一定是表中的数据,通常情况下,这个结构体变量被存储到flash中。
/*lcd部分寄存器设置值列表*/ lcd_redu_list_struct const lcd_redu_list_str[]= { {ssd1963_get_address_mode,{0x20} ,1}, /*1*/ {ssd1963_get_pll_mn ,{0x3b,0x02,0x04} ,3}, /*2*/ {ssd1963_get_pll_status ,{0x04} ,1}, /*3*/ {ssd1963_get_lcd_mode ,{0x24,0x20,0x01,0xdf,0x01,0x0f,0x00} ,7}, /*4*/ {ssd1963_get_hori_period ,{0x02,0x0c,0x00,0x2a,0x07,0x00,0x00,0x00},8}, /*5*/ {ssd1963_get_vert_period ,{0x01,0x1d,0x00,0x0b,0x09,0x00,0x00} ,7}, /*6*/ {ssd1963_get_power_mode ,{0x1c} ,1}, /*7*/ {ssd1963_get_display_mode,{0x03} ,1}, /*8*/ {ssd1963_get_gpio_conf ,{0x0f,0x01} ,2}, /*9*/ {ssd1963_get_lshift_freq ,{0x00,0xb8} ,2}, /*10*/ }; 实现函数如下所示,函数会遍历结构体变量中的每一个命令,以及每一个命令下的初始值,如果有一个不正确,则跳出循环,执行重新初始化和恢复措施。这个函数中的my_debugf宏是我自己的调试函数,使用串口打印调试信息,在接下来的第五部分将详细叙述。
通过这个函数,我可以长时间监控显示屏的哪些命令、哪些位容易被干扰。程序里使用了一个被妖魔化的关键字:goto。大多数c语言书籍对goto关键字谈之色变,但你应该有自己的判断。在函数内部跳出多重循环,除了goto关键字,又有哪种方法能如此简洁高效!
/** * lcd 显示冗余 * 每隔一段时间调用该程序一次 */ void lcd_redu(void) { uint8_t tmp[8]; uint32_t i,j; uint32_t lcd_init_flag; lcd_init_flag =0; for(i=0;i
对于8051内核单片机,由于没有相应的硬件支持,可以用纯软件设置软件陷阱,用来拦截一些程序跑飞。对于arm7或者cortex-m系列单片机,硬件已经内建了多种异常,软件需要根据硬件异常来编写陷阱程序,用来快速定位甚至恢复错误。
15 阻塞处理
有时候程序员会使用while(!flag);语句阻塞在此等待标志flag改变,比如串口发送时用来等待一字节数据发送完成。这样的代码时存在风险的,如果因为某些原因标志位一直不改变则会造成系统死机。
一个良好冗余的程序是设置一个超时定时器,超过一定时间后,强制程序退出while循环。
2003年8月11日发生的w32.blaster.worm蠕虫事件导致全球经济损失高达5亿美元,这个漏洞是利用了windows分布式组件对象模型的远程过程调用接口中的一个逻辑缺陷:在调用getmachinename()函数时,循环只设置了一个不充分的结束条件。
原代码简化如下所示:
微软发布的安全补丁ms03-026解决了这个问题,为getmachinename()函数设置了充分终止条件。一个解决代码简化如下所示(并非微软补丁代码):
可穿戴和IoT设备专用-LinkIt ONE开发板的原理图和PCB设计
继电器出口到美国需要做什么UL标准?UL508
NEPCON South China 2013成功闭幕
“工业4.0”将推动工控产品的市场新需求
小马智行与中国外运的深度合作再迎重要进展
C语言开发之防御性编程
钉钉给中国这个酷爱开会的民族,造了一个理想办公室?!
比特币现金BCH才是原始的比特币区块链
苹果公布最新iOS 14系统更新数据
深兰AI如何参与到灵感创作中
调查显示消费者对5G和蜂窝网络的质量并不满意
火爆蓝牙手环芯片
人工智能落地之路怎么走
i.MX6ULL嵌入式Linux开发2-uboot移植实践
炬光科技和New Source Technology LLC签署代理协议
中国WJ-700新型无人机首飞成功,综合素质达到了世界先进的水平
统联精密:现有MIM产能在旺季已到高位运行状态
锂金属电池失效的主要原因在于非活性锂
DCDC转换原理PWM占空比调制概述
Hi3861芯片开发板LiteOS-M的启动流程