简介:嵌入式系统的C语言开发中,经常遇到这样那样的问题。有些问题可能很快就能找到原因,但是有些问题必须有一定的经验积累才能快速找到原因。本着“吃一堑长一智;别人吃一堑,我长一智”的精神,本文整理了本人所了解的和经常遇到的嵌入式开发中的C语言典型问题,不足之处欢迎各位专家指摘赐教。
一、由编译优化引起的问题
例1、编译后的汇编语言处理逻辑跟C语言处理逻辑不一致
由于编译器的原因,在设置了编译优化的情况下,编译后有些代码的逻辑会发生变化。这种情况下会发生很奇怪的问题,一些函数的处理结果跟预想的不一致,而检查代码又看不出什么问题。
这种问题的解决办法一般是在充分分析软件处理逻辑,确认处理上没问题后,把编译后的列表文件(*.lis)和C语言处理逻辑逐行对照。把不一致的地方找出来,并寻找修正对策。
例2、编译后的一些处理被优化了
这种问题经常发生在硬件寄存器的操作上。对于硬件而言,每一次读写操作可能都有特定的含义:某些硬件寄存器要求读一下才能做后续其他处理;而某些寄存器要连续写几次。比如下面的情形:
#define TSTREG (unsigned char *) 0x00C00032 /*TEST REGISTER */
unsigned char *pTSTR;
pTSTR = TSTREG;
*pTSTR = 0x01; //这个操作很容易被编译器优化掉。
*pTSTR = 0x02;
……
作为对策之一,可以在定义变量时加上volatile关键字。比如:
volatile unsigned char *pTSTR;
volatile关键词影响编译器编译的结果,用volatile声明的变量表示该变量随时可能发生变化,与该变量有关的运算,不要进行编译优化,以免出错。使用volatile变量的几个场景:
1)中断服务程序中修改的供其它程序检测的变量需要加volatile。
2)多任务环境下各任务间共享的标志应该加volatile。
3)存储器映射的硬件寄存器通常也要加voliate,因为每次对它的读写都可能有不同意义。
二、由字节对齐引起的问题
一个结构体变量定义完之后,其在内存中的存储并不一定等于其所包含元素的宽度之和。因为这里涉及到字节对齐的问题。
结构体中元素的对齐基本上遵循两个基本原则:
原则一:结构体中元素是按照定义顺序一个一个放到内存中去的,但并不是紧密排列的。从结构体存储的首地址开始,每一个元素放置到内存中时,它都会认为内存是以它自己的大小来划分的,因此元素放置的位置一定会在自己宽度的整数倍上开始(以结构体变量首地址为0计算)。
原则二:在经过第一原则分析后,检查计算出的存储单元是否为所有元素中最宽的元素的长度的整数倍。若是,则结束;若不是,则补齐为它的整数倍。
每个特定平台上的编译器都有自己的默认“对齐系数”(也叫对齐模数或边界调整数)。通常,可以通过预编译命令#pragma pack(n),n=1,2,4,8,16来改变这一系数,其中的n就是你要指定的“对齐系数”。
#pragma pack(n) //编译器将按照n字节对齐
#pragma pack() //编译器将取消自定义字节对齐方式
在#pragma pack(n)和#pragma pack()之间的代码按n字节对齐。但是成员对齐有一个重要的条件,即每个成员按照自己的对齐方式对齐; 也就是说虽然指定了按n字节对齐,但并不是所有的成员都以n字节对齐。对齐的规则是:每个成员按其类型的对齐参数(通常是这个类型的大小)和指定对齐参数(这里是n字节)中较小的一个对齐,即min(n,sizeof(item)),并且结构的长度必须为所用过的所有对齐参数的整数倍,不够就补空字节。
以瑞萨SH7145F CPU和XASS-V编译器为例,根据XASS-V帮助文件,对于SH7145F,其结构体成员的默认对齐系数如下:
char:1
short:2
int:4
long:4
float:4
long long:4
double:4
指针:4
结构体/联合体:4
数组:根据类型而定
在编译的时候可以通过设置 /bond = n, n=4(一般情况),来指定边界调整系数。因此,实际采用/bond=n中的n和上述默认对齐系数中的较小者。
由于跟特定编译器有关,所以下面的例子仅限XASS-V编译器,目标CPU是瑞萨SH7145F。
例1、假设 /bond=4,进行如下定义:
typedef unsigned char UCHAR;
/* 结构体定义*/
typedef struct{
union{
UCHAR SO_bit7;
UCHAR DMY0_bit56;
UCHAR LNO_bit04;
} DL; /* 预想BYTE0 */
UCHAR ORDN;
union{
UCHAR RW_bit7;
UCHAR DTNO_bit06;
}RD; /* 预想BYTE1 */
UCHAR ADRH; /* 预想BYTE2 */
UCHAR ADRL; /* 预想BYTE3 */
UCHAR DATA; /* 预想BYTE4 */
}stTEST;
/* 变量定义*/
UCHAR TEST[6];
stTEST *pTEM=( stTEST *) TEST;
这样执行后,pTEM->ADRH并不是对应TEST [3],导致了数据处理错误。
原因分析:
由边界调整数决定,union只能在4的倍数的地址上存放;且UNION类型要占用4*X Byte,故后面有3Byte的Dummy。即UNION{xxx}DL占用了4Byte。
依次类推,整个结构体元素的内存分布如下:
UNION{xxx}DL; /* Byte0~Byte3: 后3Byte Dummy*/
UCHAR ORDN; /* Byte4~Byte7: 后3Byte Dummy*/
UNION{xxx}RD; /* Byte8~Byte11: 后3Byte Dummy*/
UCHAR ADRH; /* Byte12*/
UCHAR ADRL; /* Byte13*/
UCHAR DATA; /* Byte14~Byte15: 后1Byte Dummy*/
Byte5~Byte7的Dummy是因为UNION成员必须在4的倍数的地址上存放。
Byte15的Dummy是整个结构体大小必须是4的倍数。
所以sizeof(stTEST)=16, pTEM->ADRH对应为Byte12,不是预想的TEST[3]。
三、由变量类型不匹配引起的问题
例1、循环变量溢出
UCHAR i = 0x00;
/* 版本1:100个循环 */
for(i=0;i<100;i++) { /* 处理:略*/}
/* 版本2:1000个循环 */
for(i=0;i<1000;i++) { /* 处理:略*/}
一开始需求是100个循环,而后面需求变更为1000个循环,但忘记修改循环变量类型。以为UCHAR的有效范围是0~255,显然不满足版本2的要求。这种情况会发生在循环变量定义的位置距离循环体比较远的时候,在无意识中忽略了。
四、由数组下标越界引起的问题
常见的是指定的数组下标超过了数组最大有效下标。很多情况下不会导致程序奔溃,但是取出的数据显然是不正确的。
五、由字节序引起的问题
字节序主要体现大于1Byte的数据的存储方式上。对于CPU而言,有MSB FIRST和LSB FIRST两种存储方式。MSB指Most Significant Bit,即最高有效位;LSB指Lest Significant Bit,即最低有效位。简单地说,MSB FIRST就是高位优先存储,即高位存储在低地址上,低位存储在高地址上,简称“高低低高”。LSB FIRST则相反,即低位优先存储,高位存储在高地址上,低位存储在低地址上,简称“高高低低”。大部分嵌入式系统的CPU是MSB FIRST的,少部分是LSB FIRST的。常见的LSB FIRST的CPU是INTEL的。
类似的,在网络通信方面有两种字节序:“Big-Endian”和“Small-Endian”。 指的都是对于多字节的数据类型(比如4字节的32位整数),其多个字节的顺序问题,是最高字节在前(Big-Endian)还是最低字节在前(Small-Endian)。 比如对于123456789这个整数,其16进制为0x075BCD15,那么按照Big-Endian的方式,它在网络上传输(或者在内存里存储)的4个字节依次是:07 5B CD 15,而Small-Endian的顺序正相反,是:15 CD 5B 07。处于通信的双方必须按相同的字节序进行收发数据处理,才能得出正常的结果。
例1、应用程序A以075BCD15的字节序(Big-Endian)发送数据123456789给应用程序B,但应用程序B却以15CD5B07的字节序(Small-Endian)处理,则双方没法正常通信。
六、由UNION元素赋值引起的问题
一个UNION元素的值由最后那次设定决定的。有些时候,无意中对一个UNION元素连续赋值,就会发生意料之外的问题。
例1、以前面的结构体类型stTEST 为例,做如下设置。
stTEST tTst;
tTst.DL.SO_bit7=0x80; /* SO使用bit7 */
tTst.DL. LNO_bit04=0x01; /* LNO使用bit0~bit4 */
这样设置后,最终SO=0,而不是预先希望的SO=1。
七、由运算符优先级引起的问题
运算符优先级处理不好也会引入一些潜在的问题。
例1、逻辑运算符与条件运算符
int a=0x02;
if((a&0x03)!=0x00) /*表达式1*/
if(a&0x03!=0x00) /*表达式2*/
表达式1:先执行0x02&0x03=>0x02,再执行0x02 !=0x00,故结果为TRUE。
表达式2:先执行0x03 !=0x00=>TRUE(结果是0x01),再执行0x02&0x01,结果为0x00即FALSE。
显然,表达式1才是正确的写法。如果把用表达式2的形式就会引入一些潜在的问题。
例2、指针取值运算,逻辑运算符与条件运算符
if((*pSTSRG&0x01)==0x00) /*表达式1*/
if(((*pSTSRG)&0x01)==0x00) /*表达式2*/
if(*pSTSRG&0x01==0x00) /*表达式3*/
显然,表达式1和表达式2是正确写法,而且最保险的写法是表达式2。而表达式3是错误写法。
从上面的例子可以看出,由于运算符优先级不太方便**,也没必要去**,最规避这类问题的最好办法就是给表达式强制加上括号。 八、由中断优先级引起的问题
在多线程应用程序开发中,经常会用信号量等方式来保证共享资源的访问。但是在有多个中断的应用程序中,经常会无意识地略掉中断优先级,导致引入一些潜在的问题。
例1、中断IRQ1每1ms发生1次,中断IRQ4每4ms发生1次,且IRQ4的优先级高于IRQ1。在IRQ1和IRQ4的处理过程都会设置全局变量tData。该如何安排IRQ1和IRQ4的处理逻辑?
因为低优先级的中断IRQ1未处理完成时,如果发生高优先级的中断IRQ4,则IRQ1的处理会把相关上下文压栈,暂时挂起,等IRQ4处理完成后才继续处理IRQ1。由于IRQ4也会修改全局变量tData,如果没有任何保护措施,则IRQ1的处理可能会不完全正确。作为对策之一就是在IRQ1的处理中加入中断屏蔽。
/* IRQ1的中断处理函数 */
IRQ1_Handler()
{
xxx; /* 屏蔽所有中断 */
yyy; /* 相关处理 */
zzz; /* 解除中断屏蔽 */
}
当然,上述处理的前提是IRQ4是可屏蔽中断。
九、由汇编语言转C语言引起的问题
由于CPU的更换,原先用汇编语言开发的系统转换为用C语言开发的情形也是存在的。这种情况也经常会引入一些问题。
例1、CPU字节序不同而引起的问题
关于字节序参考前面的内容。当用到一个大于1Byte的变量的时候,必须了解新旧CPU的字节序,正确操作大于1Byte的变量,才能保证不会因为高低位倒置而引入问题。
例2、CPU频率不同而引起的问题
在汇编语言开发的系统中,经常会用一些循环来实现微秒级的延时。特别在串口通信中,硬件寄存器对时间非常敏感,如果在转换成C语言时没有考虑到这点,没有及时调整循环次数,就会因为CPU频率变高而导致延时不足。 |