程序员可能无意识的传递了错误参数;外界的强干扰可能将传递的参数修改掉,或者使用随机参数意外的调用函数,因此在执行函数主体前,需要先确定实参是否合法。
1. int exam_fun( unsigned char *str )
2. {
3. if( str != NULL ) // 检查“假设指针不为空”这个条件
4. {
5. //正常处理代码
6. }
7. else
8. {
9. //处理错误代码
10. }
11. }
对函数返回的错误码,要进行全面仔细处理,必要时做错误记录。
1. char *DoSomething(…)
2. {
3. char * p;
4. p=malloc(1024);
5. if(p==NULL) /*对函数返回值作出判断*/
6. {
7. UARTprintf(…); /*打印错误信息*/
8. return NULL;
9. }
10. retuen p;
11. }
如果动态计算一个地址时,要保证被计算的地址是合理的并指向某个有意义的地方。特别对于指向一个结构或数组的内部的指针,当指针增加或者改变后仍然指向同一个结构或数组。
数组越界的问题前文已经讲述的很多了,由于 C 不会对数组进行有效的检测,因此必须在应用中显式的检测数组越界问题。下面的例子可用于中断接收通讯数据。
1. #define REC_BUF_LEN 100
2. unsigned char RecBuf[REC_BUF_LEN];
3. //其它代码
4. void Uart_IRQHandler(void)
5. {
6. static RecCount=0; //接收数据长度计数器
7. //其它代码
8. if(RecCount< REC_BUF_LEN) //判断数组是否越界
9. {
10. RecBuf[RecCount]=…; //从硬件取数据
11. RecCount++;
12. //其它代码
13. }
14. else
15. {
16. //错误处理代码
17. }
18. //其它代码
19. }
1. #define REC_BUF_LEN 100
2. unsigned char RecBuf[REC_BUF_LEN];
3.
4. if(len< REC_BUF_LEN)
5. {
6. memset(RecBuf,0,len); //将数组RecBuf清零
7. }
8. else
9. {
10. //处理错误
11. }
考虑两个整数相除,对于一个 signed long 类型变量,它能表示的数值范围为:-2147483648 ~+2147483647,如果让-2147483648/ -1,那么结果应该是+2147483648,但是这个结果已经超出了signedlong所能表示的范围了。所以,在这种情况下,除了要检测除数是否为零外,还要检测除法是否溢出。
1. #include
2. signed long sl1,sl2,result;
3. /*初始化sl1和sl2*/
4. if((sl2==0)||(sl1==LONG_MIN && sl2==-1))
5. {
6. //处理错误
7. }
8. else
9. {
10. result = sl1 / sl2;
11. }
整数的加减乘运算都有可能发生溢出,在讨论未定义行为时,给出过一个有符号整形加法溢出判断代码,这里再给出一个无符号整形加法溢出判断代码段:
1. #include
2. unsigned int a,b,result;
3. /*初始化a,b*/
4. if(UINT_MAX-a5. {
6. //处理溢出
7. }
8. else
9. {
10. result=a+b;
11. }
嵌入式硬件一般没有浮点处理器,浮点数运算在嵌入式也比较少见并且溢出判断严重依赖 C 库支持,这里不讨论。
在讨论未定义行为时,提到有符号数右移、移位的数量是负值或者大于操作数的位数都是未定义行为,也提到不对有符号数进行位操作,但要检测移位的数量是否大于操作数的位数。下面给出一个无符号整数左移检测代码段:
1. unsigned int ui1;
2. unsigned int ui2;
3. unsigned int uresult;
4.
5. /*初始化ui1,ui2*/
6. if(ui2>=sizeof(unsigned int)*CHAR_BIT)
7. {
8. //处理错误
9. }
10. else
11. {
12. uresult=ui1<13. }
在其它一切措施都失效的情况下,看门狗可能是最后的防线。它的原理特别简单,但却能大大提高设备的可靠性。如果设备有硬件看门狗,一定要为它编写驱动程序。
这是因为从上电复位结束到开启看门狗的这段时间内,设备有可能被干扰而跳过看门狗初始化程序,导致看门狗失效。尽可能早的开启看门狗,可以降低这种概率;
在中断程序喂狗,由于干扰的存在,程序可能一直处于中断之中,这样会导致看门狗失效。如果在主程序中设置标志位,中断程序喂狗时与这个标志位联合判断,也是允许的;
产品的特性决定了喂狗间隔。对于不涉及安全性、实时性的设备,喂狗间隔比较宽松,但间隔时间不宜过长,否则被用户感知到,是影响用户体验的。对于设计安全性、有实时控制类的设备,原则是尽可能快的复位,否则会造成事故。
遗憾的是,1998年发射的近地号太空船(NEAR)也遇到了相同的问题。由于编程人员并未采纳建议,因此,当推进器减速器系统故障时,29公斤的储备燃料也随之报销──这同样是一个本来可经由看门狗定时器编程而避免的问题,同时也证明要从其他程序设计人员的错误中学习并不容易。
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 当做正确的数据。
非易失性存储器包括但不限于 Flash、EEPROM、铁电。仅仅将写入非易失性存储器中的数据再读出校验是不够的。强干扰情况下可能导致非易失性存储器内的数据错误,在写非易失性存储器的期间系统掉电将导致数据丢失,因干扰导致程序跑飞到写非易失性存储器函数中,将导致数据存储紊乱。一种可靠的办法是将非易失性存储器分成多个区,每个数据都将按照不同的形式写入到这些分区中,需要进行读取时,同时读出多份数据并进行表决,取相同数目较多的那个值。
对于初始化序列或者有一定先后顺序的函数调用,为了保证调用顺序或者确保每个函数都被调用,我们可以使用环环相扣,实质上这也是一种软件锁。此外对于一些安全关键代码语句(是语句,而不是函数),可以给它们设置软件锁,只有持有特定钥匙的,才可以访问这些关键代码。也可以通俗的理解为,关键安全代码不能按照单一条件执行,要额外的多设置一个标志。
比如,向 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. {
24. iap_entry(paramin, paramout); // 调用IAP服务程序
25. ProgStart=0;
26. }
27. else
28. {
29. paramout[0]=PROG_UNSTART;
30. }
31. }
每帧字节数越多,发生误码的可能性就越大,无效的数据也会越多。对此以太网规定每帧数据不大于1500字节,高可靠性的 CAN 收发器规定每帧数据不得多于 8 字节,对于 RS485,基于 RS485 链路应用最广泛的 Modbus 协议一帧数据规定不超过 256 字节。因此,建议制定内部通讯协议时,使用 RS485 时规定每帧数据不超过 256 字节;
编写程序时应使能奇偶校验,每帧超过 16 字节的应用,建议至少编写 CRC16 校验程序;
1) 增加缓冲区溢出判断。这是因为数据接收多是在中断中完成,编译器检测不出缓冲区是否溢出,需要手动检查,在上文介绍数据溢出一节中已经详细说明。
2) 增加超时判断。当一帧数据接收到一半,长时间接收不到剩余数据,则认为这帧数据无效,重新开始接收。可选,跟不同的协议有关,但缓冲区溢出判断必须实现。这是因为对于需要帧头判断的协议,上位机可能发送完帧头后突然断电,重启后上位机是从新的帧开始发送的,但是下位机已经接收到了上次未发送完的帧头,所以上位机的这次帧头会被下位机当成正常数据接收。这有可能造成数据长度字段为一个很大的值,填满该长度的缓冲区需要相当多的数据(比如一帧可能1000字节),影响响应时间;另一方面,如果程序没有缓冲区溢出判断,那么缓冲区很可能溢出,后果是灾难性的。
如果检测到通讯数据发生了错误,则要有重传机制重新发送出错的帧。
开关量容易受到尖脉冲干扰,如果不进行滤除,可能会造成误动作。一般情况下,需要对开关量输入信号进行多次采样,并进行逻辑判断直到确认信号无误为止。
开关信号简单的一次输出是不安全的,干扰信号可能会翻转开关量输出的状态。采取重复刷新输出可以有效防止电平的翻转。
公司目前使用的 4.3 寸 LCD 显示屏抗干扰能力一般。如果显示屏与控制器之间的排线距离过长或者对使用该显示屏的设备打静电或者脉冲群,显示屏有可能会花屏或者白屏。对此,我们可以将初始化显示屏的数据保存在 Flash 中,程序运行后,每隔一段时间从显示屏的寄存器读出当前值和 Flash 存储的值相比较,如果发现两者不同,则重新初始化显示屏。下面给出校验源码,仅供参考。
定义数据结构:
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;
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. };
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;j17. {
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. MY_DEBUGF(MENU_DEBUG,("读lcd寄存器值与预期不符,命令为:0x%x,第%d个参数,
23. 该参数正确值为:0x%x,实际读出值为:0x%x\n",lcd_redu_list_str[i].lcd_command,j+1,
24. lcd_redu_list_str[i].lcd_get_value[j],tmp[j]));
25. goto handle_lcd_init;
26. }
27. }
28. }
29.
30. handle_lcd_init:
31. if(lcd_init_flag==0x55)
32. {
33. //重新初始化LCD
34. //一些必要的恢复措施
35. }
36. }
对于 8051 内核单片机,由于没有相应的硬件支持,可以用纯软件设置软件陷阱,用来拦截一些程序跑飞。对于 ARM7 或者 Cortex-M 系列单片机,硬件已经内建了多种异常,软件需要根据硬件异常来编写陷阱程序,用来快速定位甚至恢复错误。
有时候程序员会使用 while(!flag); 语句阻塞在此等待标志 flag 改变,比如串口发送时用来等待一字节数据发送完成。这样的代码时存在风险的,如果因为某些原因标志位一直不改变则会造成系统死机。
一个良好冗余的程序是设置一个超时定时器,超过一定时间后,强制程序退出while循环。
原代码简化如下所示:
1. HRESULT GetMachineName ( WCHAR *pwszPath,
2. WCHAR wszMachineName[MAX_COMPUTTERNAME_LENGTH_FQDN+1])
3. {
4. WCHAR *pwszServerName = wszMachineName;
5. WCHAR *pwszTemp = pwszPath + 2;
6. while ( *pwszTemp != L’\\’ ) /* 这句代码循环结束条件不充分 */
7. *pwszServerName++= *pwszTemp++;
8. /*… */
9. }
微软发布的安全补丁 MS03-026 解决了这个问题,为 GetMachineName()函数设置了充分终止条件。一个解决代码简化如下所示(并非微软补丁代码):
1. HRESULT GetMachineName( WCHAR *pwszPath,
2. WCHAR wszMachineName[MAX_COMPUTTERNAME_LENGTH_FQDN+1])
3. {
4. WCHAR *pwszServerName = wszMachineName;
5. WCHAR *pwszTemp = pwszPath + 2;
6. WCHAR *end_addr = pwszServerName +MAX_COMPUTTERNAME_LENGTH_FQDN;
7. while ((*pwszTemp != L’\\’ ) && (*pwszTemp != L’\0’)
8. && (pwszServerName/*充分终止条件*/
9. *pwszServerName++= *pwszTemp++;
10. /*… */
11. }
思维再缜密的程序员也不可能编写完全无缺陷的程序,测试的目的正是尽可能多的发现这些缺陷并改正。这里说的测试,是指程序员的自测试。前期的自测试能够更早的发现错误,相应的修复成本也会很低,如果你不彻底测试自己的代码,恐怕你开发的就不只是代码,可能还会声名狼藉。
优质嵌入式 C 程序跟优质的基础元素关系密切,可以将函数作为基础元素,我们的测试正是从最基本的函数开始。判断哪些函数需要测试需要一定的经验积累,虽然代码行数跟逻辑复杂度并不成正比,但如果你不能判断某个函数是否要测试,一个简单粗暴的方法是:当函数有效代码超过 20 行,就测试它。
比如我们前文提及的使用 volatile 以及不使用 volatile 关键字编译后生成的汇编代码,再比如我们用低优化级别编译和使用高优化级别编译后生成的汇编代码,都可能相差很大,实际运行测试,可以暴漏这些隐含错误。最后,虽然可能性极小,编译器本身也可能有 BUG,特别是构造复杂表达式的情况下(应极力避免复杂表达式)。
使用硬件调试器(比如 J-link)测试是最通用的手段。可以单步运行、设置断点,可以很方便的查看当前寄存器、变量的值。在寻找缺陷方面,使用硬件调试器测试是最简单却又最有效的手段。
硬件调试器已经在公司普遍使用,这方面的测试不做介绍,想必大家都已经很熟悉了。
就像没有一种方法能完美解决所有问题,在实际项目中,硬件调试器也有难以触及的地方。可以举几个例子说明:
使用了比较大的协议栈,需要跟进到协议栈内部调试的缺陷
比如公司使用 lwIP 协议栈,如果跟踪数据的处理过程,需要从接收数据开始一直到应用层处理数据,之间会经过驱动层、IP层、TCP层和应用层,会经过十几个文件几十个函数,使用硬件调试器跟踪费时费力;
具有随机性的缺陷
有一些缺陷,可能是不定时出现的,有可能是几分钟出现,也有可能是几个小时甚至几天才出现,像这样的缺陷很难用硬件调试器捕捉到;
我们在初学C语言的时候,都接触过printf函数,这个函数可以方便的输出信息,并可以将各种变量格式化为指定格式的字符串,我们应当提供类似的函数;
在编码阶段,我们可能会往程序中加入大量的调试语句,但是程序发布时,需要将这些调试语句从代码中移除,这将是件恐怖的过程。我们必须提供一种策略,可以方便的移除这些调试语句。
I> 初始化串口
1. /**
2. * @brief 将C库中的printf函数重定向到指定的串口.
3. * @param ch:要发送的字符
4. * @param f :文件指针
5. */
6. int fputc(int ch, FILE *f)
7. {
8.
9. /*这里是一个跟硬件相关函数,将一个字符写到UART */
10. //举例:USART_SendData(UART_COM1, (uint8_t) ch);
11.
12. return ch;
13. }
1. #include /*支持函数接收不定量参数*/
2.
3. const char * const g_pcHex = "0123456789abcdef";
4.
5. /**
6. * 简介: 一个简单的printf函数,支持\%c, \%d, \%p, \%s, \%u,\%x, and \%X.
7. */
8. void UARTprintf(const uint8_t *pcString, ...)
9. {
10. uint32_t ulIdx;
11. uint32_t ulValue; //保存从不定量参数堆栈中取出的数值型变量
12. uint32_t ulPos, ulCount;
13. uint32_t ulBase; //保存进制基数,如十进制则为10,十六进制数则为16
14. uint32_t ulNeg; //为1表示从变量为负数
15. uint8_t *pcStr; //保存从不定量参数堆栈中取出的字符型变量
16. uint8_t pcBuf[32]; //保存数值型变量字符化后的字符
17. uint8_t cFill; //'%08x'->不足8个字符用'0'填充,cFill='0';
18. //'%8x '->不足8个字符用空格填充,cFill=' '
19. va_list vaArgP;
20.
21. va_start(vaArgP, pcString);
22. while(*pcString)
23. {
24. // 首先搜寻非%核字符串结束字符
25. for(ulIdx = 0; (pcString[ulIdx] != '%') && (pcString[ulIdx] != '\0'); ulIdx++)
26. { }
27. UARTwrite(pcString, ulIdx);
28.
29. pcString += ulIdx;
30. if(*pcString == '%')
31. {
32. pcString++;
33.
34. ulCount = 0;
35. cFill = ' ';
36. again:
37. switch(*pcString++)
38. {
39. case '0': case '1': case '2': case '3': case '4':
40. case '5': case '6': case '7': case '8': case '9':
41. {
42. // 如果第一个数字为0, 则使用0做填充,则用空格填充)
43. if((pcString[-1] == '0') && (ulCount == 0))
44. {
45. cFill = '0';
46. }
47. ulCount *= 10;
48. ulCount += pcString[-1] - '0';
49. goto again;
50. }
51. case 'c':
52. {
53. ulValue = va_arg(vaArgP, unsigned long);
54. UARTwrite((unsigned char *)&ulValue, 1);
55. break;
56. }
57. case 'd':
58. {
59. ulValue = va_arg(vaArgP, unsigned long);
60. ulPos = 0;
61.
62. if((long)ulValue < 0)
63. {
64. ulValue = -(long)ulValue;
65. ulNeg = 1;
66. }
67. else
68. {
69. ulNeg = 0;
70. }
71. ulBase = 10;
72. goto convert;
73. }
74. case 's':
75. {
76. pcStr = va_arg(vaArgP, unsigned char *);
77.
78. for(ulIdx = 0; pcStr[ulIdx] != '\0'; ulIdx++)
79. {
80. }
81. UARTwrite(pcStr, ulIdx);
82.
83. if(ulCount > ulIdx)
84. {
85. ulCount -= ulIdx;
86. while(ulCount--)
87. {
88. UARTwrite(" ", 1);
89. }
90. }
91. break;
92. }
93. case 'u':
94. {
95. ulValue = va_arg(vaArgP, unsigned long);
96. ulPos = 0;
97. ulBase = 10;
98. ulNeg = 0;
99. goto convert;
100. }
101. case 'x': case 'X': case 'p':
102. {
103. ulValue = va_arg(vaArgP, unsigned long);
104. ulPos = 0;
105. ulBase = 16;
106. ulNeg = 0;
107. convert: //将数值转换成字符
108. for(ulIdx = 1; (((ulIdx * ulBase) <= ulValue) &&(((ulIdx * ulBase) / ulBase) == ulIdx)); ulIdx *= ulBase, ulCount--)
109. { }
110. if(ulNeg)
111. {
112. ulCount--;
113. }
114. if(ulNeg && (cFill == '0'))
115. {
116. pcBuf[ulPos++] = '-';
117. ulNeg = 0;
118. }
119. if((ulCount > 1) && (ulCount < 16))
120. {
121. for(ulCount--; ulCount; ulCount--)
122. {
123. pcBuf[ulPos++] = cFill;
124. }
125. }
126.
127. if(ulNeg)
128. {
129. pcBuf[ulPos++] = '-';
130. }
131.
132. for(; ulIdx; ulIdx /= ulBase)
133. {
134. pcBuf[ulPos++] = g_pcHex[(ulValue / ulIdx) % ulBase];
135. }
136. UARTwrite(pcBuf, ulPos);
137. break;
138. }
139. case '%':
140. {
141. UARTwrite(pcString - 1, 1);
142. break;
143. }
144. default:
145. {
146. UARTwrite("ERROR", 5);
147. break;
148. }
149. }
150. }
151. }
152. //可变参数处理结束
153. va_end(vaArgP);
154. }
上文说到,我们增加的调试语句应能很方便的从最终发行版中去掉,因此我们不能直接调用 printf 或者自定义的 UARTprintf 函数,需要将这些调试函数做一层封装,以便随时从代码中去除这些调试语句。参考方法如下:
1. #ifdef MY_DEBUG
2. #define MY_DEBUGF(message) do { \
3. {UARTprintf message;} \
4. } while(0)
5. #else
6. #define MY_DEBUGF(message)
7. #endif /* PLC_DEBUG */
在我们编码测试期间,定义宏 MY_DEBUG,并使用宏 MY_DEBUGF(注意比前面那个宏多了一个‘F’)输出调试信息。经过预处理后,宏MY_DEBUGF(message) 会被 UARTprintf message 代替,从而实现了调试信息的输出;当正式发布时,只需要将宏 MY_DEBUG 注释掉,经过预处理后,所有MY_DEBUGF(message) 语句都会被空格代替,而从将调试信息从代码中去除掉。
《计算机程序的构造和解释》一书在开篇写到:程序写出来是给人看的,附带能在机器上运行。
使用什么样的编码样式一直都颇具争议性的,比如缩进和大括号的位置。因为编码的样式也会影响程序的可读性,面对一个乱放括号、对齐都不一致的源码,我们很难提起阅读它的兴趣。我们总要看别人的程序,如果彼此编码样式相近,读起源码来会觉得比较舒适。但是编码风格的问题是主观的,永远不可能在编码风格上达成统一意见。因此只要你的编码样式整洁、结构清晰就足够了。除此之外,对编码样式再没有其它要求。
提出匈牙利命名法的程序员、前微软首席架构师 Charles Simonyi 说:我觉得代码清单带给人的愉快同整洁的家差不多。你一眼就能分辨出家里是杂乱无章还是整洁如新。这也许意义不大。因为光是房子整洁说明不了什么,它仍可能藏污纳垢!但是第一印象很重要,它至少反映了程序的某些方面。我敢打赌,我在 3 米开外就能看出程序拙劣与否。我也许没法保证它很不错,但如果从 3 米外看起来就很糟,我敢保证这程序写得不用心。如果写得不用心,那它在逻辑上也许就不会优美。
关于如何命名,Charles Simonyi 说:面对一个具备某些属性的结构,不要随随便便地取个名字,然后让所有人去琢磨名字和属性之间有什么关联,你应该把属性本身,用作结构的名字。
注释向来也是争议之一。不加注释和过多的注释,我都是反对的。不加注释的代码显然是很糟糕的,但过多的注释也会妨碍程序的可读性,由于注释可能存在的歧义,有可能会误解程序真实意图,此外,过多的注释会增加程序员不必要的时间。如果你的编码样式整洁、命名又很清晰,那么,你的代码可读性不会差到哪去,而注释的本意就是为了便于理解程序。
这里建议使用良好的编码样式和清晰的命名来减少注释,对模块、函数、变量、数据结构、算法和关键代码做注释,应重视注释的质量而不是数量。如果你需要一大段注释才能说清楚程序做什么,那么你应该注意了:是否是因为程序变量命名不够清晰,或者代码逻辑过于混乱,这个时候你应该考虑的可能就不是注释,而是如何精简这个程序了。
前微软首席架构师 Charles Simonyi:编程的第一步是想象。就是要在脑海中对来龙去脉有极为清晰的把握。在这个初始阶段,我会使用纸和铅笔。我只是信手涂鸦,并不写代码。我也许会画些方框或箭头,但基本上只是涂鸦,因为真正的想法在我脑海里。我喜欢想象那些有待维护的结构,那些结构代表着我想编码的真实世界。一旦这个结构考虑得相当严谨和明确,我便开始写代码。我会坐到终端前,或者换在以前的话,就会拿张白纸,开始写代码。这相当容易。我只要把头脑中的想法变换成代码写下来,我知道结果应该是什么样的。大部分代码会水到渠成,不过我维护的那些数据结构才是关键。我会先想好数据结构,并在整个编码过程中将它们牢记于心。
开发过以太网和操作系统 SDS 940 的 Butler Lampson:(程序员)最重要的素质是能够把问题的解决方案组织成容易操控的结构。
开发 CP/M 操作系统的 Gary.A:如果不能确认数据结构是正确的,我是决不会开始编码的。我会先画数据结构,然后花很长时间思考数据结构。在确定数据结构之后我就开始写一些小段的代码,并不断地改善和监测。在编码过程中进行测试可以确保所做的修改是局部的,并且如果有什么问题的话,能够马上发现。
微软创始人比尔·盖茨:编写程序最重要的部分是设计数据结构。接下来重要的部分是分解各种代码块。
编写世界上第一个电子表格软件的 Dan Bricklin:在我看来,写程序最重要的部分是设计数据结构,此外,你还必须知道人机界面会是什么样的。
我们举个例子来说明。在介绍防御性编程的时候,提到公司使用的 LCD 显示屏抗干扰能力一般,为了提高 LCD 的稳定性,需要定期读出 LCD 内部的关键寄存器值,然后跟存在 Flash 中的初始值相比较。需要读出的 LCD 寄存器有十多个,从每个寄存器读出的值也不尽相同,从 1 个到 8 个字节都有可能。如果不考虑数据结构,编写出的程序将会很冗长。
1. void lcd_redu(void)
2. {
3. 读第一个寄存器值;
4. if(第一个寄存器值==Flash存储值)
5. {
6. 读第二个寄存器值;
7. if(第二个寄存器值==Flash存储值)
8. {
9. ...
10.
11. 读第十个寄存器值;
12. if(第十个寄存器值==Flash存储值)
13. {
14. 返回;
15. }
16. else
17. {
18. 重新初始化LCD;
19. }
20. }
21. else
22. {
23. 重新初始化LCD;
24. }
25. }
26. else
27. {
28. 重新初始化LCD;
29. }
30. }
我们可以先提取相同的元素,将之组织成数据结构:
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;
就本例而言,我们将要处理的数据都是事先固定的,所以定义好数据结构后,我们可以将这些数据组织成表格:
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. };
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;j17. {
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. }
本文介绍了编写优质嵌入式C程序涉及的多个方面。每年都有亿万计的 C 程序运行在单片机、ARM7、Cortex-M3 这些微处理器上,但在这些处理器上如何编写优质高效的 C 程序,几乎没有书籍做专门介绍。本文试图在这方面做一些努力。编写优质嵌入式 C 程序需要大量的专业知识,本文虽尽力描述编写嵌入式 C程序所需要的各种技能,但本文却无力将每一个方面都面面俱到的描述出来,所以本文最后会列举一些阅读书目,这些书大多都是真正大师的经验之谈。站在巨人的肩膀上,可以看的更远。
Susan Lammers 著 李琳骁、吴咏炜、张菁《编程大师访谈录》
文章来源于网络,版权归原作者所有,如有侵权,请联系删除。
关注我【一起学嵌入式】,一起学习,一起成长。
觉得文章不错,点击“分享”、“赞”、“在看” 呗!