编程工具

1、快速排序算法

快速排序是由东尼•霍尔所发展的一种排序算法。在平均状况下,排序n个项目要Ο(nlogn)次比较。在最坏状况下则需要Ο(n2)次比较,但这种状况并不常见。事实上,快速排序通常明显比其他Ο(nlogn)算法更快,因为它的内部循环(innerloop)可以在大部分的架构上很有效率地被实现出来。

快速排序使用分治法(Divideandconquer)策略来把一个串行(list)分为两个子串行(sub-lists)。

算法步骤:

(1)从数列中挑出一个元素,称为“基准”(pivot),

(2)重新排序数列,所有元素比基准值小的摆放在基准前面,所有元素比基准值大的摆在基准的后面(相同的数可以到任一边)。在这个分区退出之后,该基准就处于数列的中间位置。这个称为分区(partition)操作。

(3)递归地(recursive)把小于基准值元素的子数列和大于基准值元素的子数列排序。

递归的最底部情形,是数列的大小是零或一,也就是永远都已经被排序好了。虽然一直递归下去,但是这个算法总会退出,因为在每次的迭代(iteration)中,它至少会把一个元素摆到它最后的位置去。

2、堆排序算法

堆排序(Heapsort)是指利用堆这种数据结构所设计的一种排序算法。堆积是一个近似完全二叉树的结构,并同时满足堆积的性质:即子结点的键值或索引总是小于(或者大于)它的父节点。

堆排序的平均时间复杂度为Ο(nlogn) 。

算法步骤:

(1)创建一个堆H[0..n-1]

(2)把堆首(最大值)和堆尾互换

(3)把堆的尺寸缩小1,并调用shift_down(0),目的是把新的数组顶端数据调整到相应位置

(4)重复步骤2,直到堆的尺寸为1

3、归并排序

归并排序(Mergesort,台湾译作:合并排序)是建立在归并操作上的一种有效的排序算法。该算法是采用分治法(DivideandConquer)的一个非常典型的应用。

算法步骤:

(1)申请空间,使其大小为两个已经排序序列之和,该空间用来存放合并后的序列

(2)设定两个指针,最初位置分别为两个已经排序序列的起始位置

(3)比较两个指针所指向的元素,选择相对小的元素放入到合并空间,并移动指针到下一位置

(4)重复步骤3直到某一指针达到序列尾

(5)将另一序列剩下的所有元素直接复制到合并序列尾

4、二分查找算法

二分查找算法是一种在有序数组中查找某一特定元素的搜索算法。搜素过程从数组的中间元素开始,如果中间元素正好是要查找的元素,则搜素过程结束;如果某一特定元素大于或者小于中间元素,则在数组大于或小于中间元素的那一半中查找,而且跟开始一样从中间元素开始比较。如果在某一步骤数组为空,则代表找不到。这种搜索算法每一次比较都使搜索范围缩小一半。折半搜索每次把搜索区域减少一半,时间复杂度为Ο(logn) 。

5、BFPRT(线性查找算法)

BFPRT 算法解决的问题十分经典,即从某n个元素的序列中选出第k大(第k小)的元素,通过巧妙的分析,BFPRT可以保证在最坏情况下仍为线性时间复杂度。该算法的思想与快速排序思想相似,当然,为使得算法在最坏情况下,依然能达到o(n)的时间复杂度,五位算法作者做了精妙的处理。

算法步骤:

(1)将n个元素每5个一组,分成n/5(上界)组。

(2)取出每一组的中位数,任意排序方法,比如插入排序。

(3)递归的调用selection算法查找上一步中所有中位数的中位数,设为x,偶数个中位数的情况下设定为选取中间小的一个。

(4)用x来分割数组,设小于等于x的个数为k,大于x的个数即为n-k。

(5)若i==k,返回x;若ik,在大于x的元素中递归查找第i-k小的元素。

终止条件:n=1时,返回的即是i小元素。

6、DFS(深度优先搜索)

深度优先搜索算法(Depth-First-Search),是搜索算法的一种。它沿着树的深度遍历树的节点,尽可能深的搜索树的分支。当节点v的所有边都己被探寻过,搜索将回溯到发现节点v的那条边的起始节点。这一过程一直进行到已发现从源节点可达的所有节点为止。如果还存在未被发现的节点,则选择其中一个作为源节点并重复以上过程,整个进程反复进行直到所有节点都被访问为止。DFS属于盲目搜索。

深度优先搜索是图论中的经典算法,利用深度优先搜索算法可以产生目标图的相应拓扑排序表,利用拓扑排序表可以方便的解决很多相关的图论问题,如最大路径问题等等。一般用堆数据结构来辅助实现DFS算法。

算法步骤:

(1)访问顶点v;

(2)依次从v的未被访问的邻接点出发,对图进行深度优先遍历;直至图中和v有路径相通的顶点都被访问;

(3)若此时图中尚有顶点未被访问,则从一个未被访问的顶点出发,重新进行深度优先遍历,直到图中所有顶点均被访问过为止。

上述描述可能比较抽象,举个实例:

DFS在访问图中某一起始顶点v后,由v出发,访问它的任一邻接顶点w1;再从w1出发,访问与w1邻接但还没有访问过的顶点w2;然后再从w2出发,进行类似的访问,…如此进行下去,直至到达所有的邻接顶点都被访问过的顶点u为止。

接着,退回一步,退到前一次刚访问过的顶点,看是否还有其它没有被访问的邻接顶点。如果有,则访问此顶点,之后再从此顶点出发,进行与前述类似的访问;如果没有,就再退回一步进行搜索。重复上述过程,直到连通图中所有顶点都被访问过为止。

7、BFS(广度优先搜索)

广度优先搜索算法(Breadth-First-Search),是一种图形搜索算法。简单的说,BFS是从根节点开始,沿着树(图)的宽度遍历树(图)的节点。如果所有节点均被访问,则算法中止。BFS同样属于盲目搜索。一般用队列数据结构来辅助实现BFS算法。

算法步骤:

(1)首先将根节点放入队列中。

(2)从队列中取出第一个节点,并检验它是否为目标。如果找到目标,则结束搜寻并回传结果。否则将它所有尚未检验过的直接子节点加入队列中。

(3)若队列为空,表示整张图都检查过了——亦即图中没有欲搜寻的目标。结束搜寻并回传“找不到目标”。

(4)重复步骤2。

8、Dijkstra算法

戴克斯特拉算法(Dijkstra’salgorithm)是由荷兰计算机科学家艾兹赫尔•戴克斯特拉提出。迪科斯彻算法使用了广度优先搜索解决非负权有向图的单源最短路径问题,算法最终得到一个最短路径树。该算法常用于路由算法或者作为其他图算法的一个子模块。

该算法的输入包含了一个有权重的有向图G,以及G中的一个来源顶点S。我们以V表示G中所有顶点的集合。每一个图中的边,都是两个顶点所形成的有序元素对。 (u,v)表示从顶点u到v有路径相连。我们以E表示G中所有边的集合,而边的权重则由权重函数w:E→[0,∞]定义。

因此,w(u,v)就是从顶点u到顶点v的非负权重(weight)。边的权重可以想像成两个顶点之间的距离。任两点间路径的权重,就是该路径上所有边的权重总和。已知有V中有顶点s及t,Dijkstra算法可以找到s到t的最低权重路径(例如,最短路径)。这个算法也可以在一个图中,找到从一个顶点s到任何其他顶点的最短路径。对于不含负权的有向图,Dijkstra算法是目前已知的最快的单源最短路径算法。

算法步骤:

(1)初始时令S={V0},T={其余顶点},T中顶点对应的距离值

若存在,d(V0,Vi)为弧上的权值

若不存在,d(V0,Vi)为∞

(2)从T中选取一个其距离值为最小的顶点W且不在S中,加入S

(3)对其余T中顶点的距离值进行修改:若加进W作中间顶点,从V0到Vi的距离值缩短,则修改此距离值
重复上述步骤2、3,直到S中包含所有顶点,即W=Vi为止

9、动态规划算法

动态规划(Dynamicprogramming)是一种在数学、计算机科学和经济学中使用的,通过把原问题分解为相对简单的子问题的方式求解复杂问题的方法。动态规划常常适用于有重叠子问题和最优子结构性质的问题,动态规划方法所耗时间往往远少于朴素解法。

动态规划背后的基本思想非常简单。大致上,若要解一个给定问题,我们需要解其不同部分(即子问题),再合并子问题的解以得出原问题的解。通常许多子问题非常相似,为此动态规划法试图仅仅解决每个子问题一次,从而减少计算量:一旦某个给定子问题的解已经算出,则将其记忆化存储,以便下次需要同一个子问题解之时直接查表。这种做法在重复子问题的数目关于输入的规模呈指数增长时特别有用。

关于动态规划最经典的问题当属背包问题。

算法步骤:

(1)最优子结构性质。如果问题的最优解所包含的子问题的解也是最优的,我们就称该问题具有最优子结构性质(即满足最优化原理)。最优子结构性质为动态规划算法解决问题提供了重要线索。

(2)子问题重叠性质。子问题重叠性质是指在用递归算法自顶向下对问题进行求解时,每次产生的子问题并不总是新问题,有些子问题会被重复计算多次。动态规划算法正是利用了这种子问题的重叠性质,对每一个子问题只计算一次,然后将其计算结果保存在一个表格中,当再次需要计算已经计算过的子问题时,只是在表格中简单地查看一下结果,从而获得较高的效率。

10、朴素贝叶斯分类算法

朴素贝叶斯分类算法是一种基于贝叶斯定理的简单概率分类算法。贝叶斯分类的基础是概率推理,就是在各种条件的存在不确定,仅知其出现概率的情况下,如何完成推理和决策任务。概率推理是与确定性推理相对应的。而朴素贝叶斯分类器是基于独立假设的,即假设样本每个特征与其他特征都不相关。

朴素贝叶斯分类器依靠精确的自然概率模型,在有监督学习的样本集中能获取得非常好的分类效果。在许多实际应用中,朴素贝叶斯模型参数估计使用最大似然估计方法,换言之朴素贝叶斯模型能工作并没有用到贝叶斯概率或者任何贝叶斯模型。

尽管是带着这些朴素思想和过于简单化的假设,但朴素贝叶斯分类器在很多复杂的现实情形中仍能够取得相当好的效果。

文章来源: 快课网

围观 359

3、标识符

a、变量的命名

方法一:采用匈牙利命名法。命名规则的主要思想是“在变量中加入前缀以增进人们对程序的理解”。

例如平时声明32位整型变量Length对应使用匈牙利命名法为unLength。现在列出经常用到的变量类型。

变量类型 示例

char cLength

unsigned char ucLength

short int sLength

unsigned short int usLength

int nLength

unsigned int unLength

char * szBuf

unsigned char * uszBuf

volatile unsigned char __ucLength

方法二:

Ø 局部变量以小写字母命名;

Ø 全局变量以首字母大写方式命名(骆驼式);

Ø 定义类型和宏定义常数以大写字母命名;

Ø 变量的作用域越大,它的名字所带有的信息就应该越多。

Ø 局部变量: int student_age;

Ø 全局变量: int StudentAge;

Ø 宏定义常数:#define STUDENT_NUM 10

Ø 类型定义: typedef INT16S int;

(我个人喜欢第二种方法)

b、 变量命名要注意缩写而且让人简单易懂,若是特别缩写要详细说明。

经常用到的缩写如:

Count 可缩写为Cnt

Message 可缩写为Msg

Packet 可缩写为Pkt

Temp 可缩写为Tmp

平时不经常用到的缩写,要注释:

SerialCommunication 可缩写为SrlComm //串口通信变量

SerialCommunicationStatus 可缩写为SrlCommStat //串口通信状态变量

c、全局变量和全局函数的命名一定要详细,不惜多用几个单词,例如函数UARTPrintfStringForLCD,

因为它们在整个项目的许多源文件中都会用到,必须让使用者明确这个变量或函数是干什么用的。局部变量和只在一个源文件中调用的内部函数的命名可以。简略一些,但不能太短,不要使用单个字母做变量名,只有一个例外:用i、j 、k 做循环变量是可以的。

d、用于编译开关的文件头,必须加上当前文件名称,防止编译时产生冲突。

例如在UARTInterface.h 头文件中,必须加上以下内容

#ifndef __UARTINTERFACE_H__

#define __UARTINTERFACE_H__

extern void UARTPrintfString(CONST INT8* str);

extern void UARTSendNBytes(UINT8 *ucSendBytes,UINT8 ucLen);

…… //其他外部声明的代码

#endif

e、禁止用汉语拼音作为标识符名称,可读性极差。呵呵。

f、 建议名称间的区别要显而易见。使用标识符名称要注意的一个相关问题是发生在名称之间只有一个字符或少数字符不同的情况,特别是名称比较长时,当名称间的区别很容易被误读时问题就比较显著,比如1(数字1)和l(L 的小写)、0 和O、2 和Z、5 和S,或者n 和h。

4、表达式和基本语句

a、不要编写太复杂的复合表达式;

例如:

i = a >= b && c < d && c + f <= g + h; //复合表达式过于复杂

b、不要有多用途的复合表达式;

例如:

d = (a = b + c) + r ; //应拆分为两个语句:

a = b + c;

d = a + r;

c、如果代码行中的运算符比较多,用括号确定表达式的操作顺序,避免使用默认的优先级。

例如:

if(a | b && a & c) //不良的风格

if((a | b) && (a & c)) //良好的风格

注意:只需记住加减运算的优先级低于乘除运算,其它地方一律加上括号。

d、 if 语句

d.a 布尔变量与零值比较

不可将布尔变量直接与TRUE、FALSE 或者1、0 进行比较。

根据布尔类型的语义,零值为“假”(记为FALSE),任何非零值都是“真”(记为TRUE)。TRUE的值究竟是什么并没有统一的标准。例如Visual C++ 将TRUE 定义为1,而Visual Basic 则将TRUE 定义为-1。

例:假设布尔变量名字为flag,它与零值比较的标准if 语句如下:

if (flag) // 表示flag为真时满足条件

if (!flag) // 表示flag为假时满足条件

其它的用法都属于不良风格,例如:

if (flag == TRUE)

if (flag == 1 )

if (flag == FALSE)

if (flag == 0)

d.b 整型变量与零值比较

应当将整型变量用“==”或“!=”直接与0比较。

例:假设整型变量为value,它与零值比较的标准if 语句如下:

if (value == 0)

if (value != 0)

不可模仿布尔变量的风格而写成

if (value) // 会让人误解 value 是布尔变量

if (!value)

小技巧:想必大家都有过将赋值操作符“=”当作比较相等操作符“==”用过,这个错误比较的隐晦,不易排查,而且编译器从不把这类事情当作是程序员犯下的错。避免的方法有两种,一种是养成良好的编程习惯,在比较数值时小心翼翼的处理;另一种方法见下面给出的代码:

if (NULL = = p)

{

……

}

是不是觉得这种书写方式很古怪?不是程序写错了?

当然不是!

有经验的程序员为了防止将 if (p = = NULL) 误写成 if (p = NULL),而有意把p 和NULL 颠倒。编译器认为 if (p = NULL) 是合法的,但是会指出 if (NULL = p)是错误的,因为NULL不能被赋值。所以,再次遇到判断整型变量是否与某个数相等时,请这样写吧:

if(2==flag)

{

……

}

d.c 浮点变量与零值比较

不可将浮点变量用“==”或“!=”与任何数字比较。

千万要留意,无论float 还是double 类型变量,都有精度限制。所以一定要避免将浮点变量用“==”或“!=”与数字比较,应该设法转化成“>=”或“<=”形式。

假设浮点变量的名字为x,应当将

if (x == 0.0) // 隐含错误的比较

转化为

if ((x>=-EPSINON) && (x<=EPSINON)) //EPSINON 是精度

5、杂项

a. 一些常量(如圆周率PI)或者常需要在调试时修改的参数最好用#define定义,但要注意宏定义只是简单的替换,因此有些括号不可少。

b. 不要轻易调用某些库函数,因为有些库函数代码很长(我是反对使用printf之类的库函数的,但是是一家之言,并不勉强各位)。

c. 对各运算符的优先级有所了解,记不得没关系,加括号就是,千万不要自作聪明说自己记得很牢。

d. 不管有没有无效分支,switch函数一定要defaut这个分支。一来让阅读者知道程序员并没有遗忘default,并且防止程序运行过程中出现的意外(健壮性)。

e. 函数的参数和返回值没有的话最好使用void。

f. 一些常数和表格之类的应该放到code中去以节省RAM。

g. 程序编完编译看有多少code多少data,注意不要使堆栈为难。

h. 减少函数本身或函数间的递归调用

i. 编写可重入函数时,若使用全局变量,则应通过关中断、信号量(即P、V操作)等手段对其加以保护。

j. 在多重循环中,应将最忙的循环放在最内层

k. 避免循环体内含判断语句,应将循环语句置于判断语句的代码块之中。

l. 系统运行之初,要初始化有关变量及运行环境,防止未经初始化的变量被引用。

m. 编写代码时要注意随时保存,并定期备份,防止由于断电、硬盘损坏等原因造成代码丢失。

文章来源: 博客园

围观 538

编程的总则:编程首要是要考虑程序的可行性,然后是可读性、可移植性、健壮性以及可测试性。大多数程序员只是关注程序的可行性,而忽略了可读性,可移植性和健壮性,其实我个人认为,程序的可行性和健壮性与程序的可读性有很大的关系,能写出可读性很好的程序的程序员,他写的程序的可行性和健壮性必然不会差,也会有不错的可移植性。程序的可读性需要程序员有一个良好的编程风格。

好风格应该成为一种习惯。如果你在开始写代码时就关心风格问题,如果你花时间去审视和改进它,你将会逐渐养成一种好的编程习惯。一旦这种习惯变成自动的东西,你的潜意识就会帮你照料许多细节问题,甚至你在工作压力下写出的代码也会更好。

1、排版

a、代码缩进空格数为4个。若是可能,尽量用空格来代替Tab键,因为有些编译器不支持Tab键(我自己至今未见过,但确实有这个风险),这给程序的移植带来了问题。在keil中这个问题很容易解决,只需在在keil主界面的菜单栏点击Edit—Configuration…,弹出Configuration窗口,点击Editor标签,在其中C/C++ File:、ASM、Other Files栏下,选中Insert spaces for tab:复选框,Tab对应的框中填4,这样按tab键就相当于按下四个空格键。

BOOL BufClr(UINT8 * dest,UINT32 size)

{

if(NULL ==dest || NULL==size)

{

return FALSE;

}

}

b、较长的语句要分2行来书写,并用‘/’符号隔开。

uncrc=calcCRC16(Packet.p,unlen);

if((UINT8) uncrc != Packet.down_ser.mCrc[0] /

||(UINT8)(uncrc>>8)!= Packet.down_ser.mCrc[1])

{

BELL(ON);

}

c、 函数代码的参数过长,分多行来书写。

void UARTSendAndRecv(UINT8 *ucSendBuf,

UINT8 ucSendLength,

UINT8 *ucRecvBuf,

UINT8 ucRecvLength)

{

……

}

d、 if、do、while、switch、for、case、default等关键字,必须加上大括号{}。

if(bSendEnd)

{

BELL(ON);

}

else

{

BELL(OFF);

}

//--------------------------

for(i=0; i< ucRecvLength; i++)

{

ucRecvBuf[i]=i;

}

//--------------------------

switch(ucintStatus)

{

case USB_INT_EP2_OUT:

{

USBCiEP2Send(USBMainBuf,ucrecvLen);

USBCiEP1Send(USBMainBuf,ucrecvLen);

}

break;

case USB_INT_EP2_IN:

{

USBCiWriteSingleCmd (CMD_UNLOCK_USB);

}

break;

……

}

2、注释

a、 边写代码边注释,修改代码同时修改相应的注释,以保证注释与代码的一致性。不再有用的注释要删除。

注释应当准确、易懂,防止注释有二义性。错误的注释不但无益反而有害。

尽量避免在注释中使用缩写,特别是不常用缩写。

注释的位置应与被描述的代码相邻,可以放在代码的上方或右方,不可放在下方。

b、说明性文件必选在文件头着重说明,例如*.c、*.h文件

/***************************************************************************

* 定时器+计数器测频

*

* 文 件: frequency.c

* 作 者: 小瓶盖

* 说 明:定时器+计数机测频率

* 编写时间: 2010.3.17

* 版 本:1.0

* 修改日期: 无

*---------------------------------------------------------------------------

* 注: 本程序定义6个数码管,经过实测,在200HZ~50KHZ时结果较准确,误差小于0.4%,

* 50KHZ以上频率未进行测量.据资料表明,可以测量到120KHZ,本程序未证明.

*****************************************************************************/

#include

void func(void)

{

}

c、函数头应该进行注释,例如函数名称、输入参数、返回值、功能说明。

/**************将所有参数写入AT24C64,共4字节*********************

*说明:将表号和用户电量共四字节数据写入AT24C64中

*入口参数:

* 1.数据间接寻址地址-buf

* 2.写入到AT24C64的地址字-addh,addrl

* 3.写入字节数-count

*出口参数:1表示写成功,0表示写失败

***************************************************************/

bit write_byte(unsigned char * buf,

unsigned char addrh,

unsigned char addrl,

unsigned char count)

{

……

}

d、全局变量要注释其功能,若为关键的局部变量同样需要注释其功能。

volatile UINT8 __ucSysMsg=SYS_IDLE;

void SYSSetMsgPriority(void)

{

SYSMSG Msgt;//临时存储消息

UINT8 i;

}

e、复杂的宏定义同样要加上注释。

/* SYS_MSG_MAP 建立一个消息映射

宏参数NAME:消息映射表的名字

宏参数NUM_OF_MSG:消息映射的个数

*/

#define SYS_MSG_MAP(NAME,NUM_OF_MSG) do/

{/

DEFINE_MSG_NAME((NAME));/

UINT8 i;/

for(i=0;i< NUM_OF_MSG;i++)/

{/

ININ_CUR_MSG(i)/

}/

}while(0)

f、复杂的结构体同样要加上注释。

/* 奇偶校验结构体*/

typedef struct _ PKT_PARITY

{

UINT8 m_ucHead1; //首部1

UINT8 m_ucHead2; //首部2

UINT8 m_ucOptCode; //操作码

UINT8 m_ucDataLength; //数据长度

UINT8 m_szDataBuf[16];//数据

UINT8 m_ucParity; //奇偶校验值

}PKT_PARITY;

g、 相对独立的语句组注释。对这一组语句做特别说明,写在语句组上侧,和此语句组之间不留空行,与当前语句组的缩进一致。注意,说明语句组的注释一定要写在语句组上面,不能写在语句组下面。

文章来源: 博客园

围观 449

1、前言

设备的可靠性涉及多个方面:稳定的硬件、优秀的软件架构、严格的测试以及市场和时间的检验等等。这里着重谈一下作者自己对嵌入式软件可靠性设计的一些理解,通过一定的技巧和方法提高软件可靠性。这里所说的嵌入式设备,是指使用单片机、ARM7、Cortex-M0,M3之类为核心的测控或工控系统。

嵌入式软件可靠性设计应该从防错、判错和容错三方面进行考虑。 此外,还需理解自己所使用的编译器特性。

2、防错

良好的软件架构、清晰的代码结构、掌握硬件、深入理解C语言是防错的要点,这里只谈一下C语言。

“人的思维和经验积累对软件可靠性有很大影响"。C语言诡异且有种种陷阱和缺陷,需要程序员多年历练才能达到较为完善的地步。“软件的质量是由程序员的质量以及他们相互之间的协作决定的”。因此,作者认为防错的重点是要考虑人的因素。

“深入一门语言编程,不要浮于表面”。软件的可靠性,与你理解的语言深度密切相关,嵌入式C更是如此。除了语言,作者认为嵌入式开发还必须深入理解编译器。

本节将对C语言的陷阱和缺陷做初步探讨。

2.1 处处皆陷阱

最初开始编程时,除了英文标点被误写成中文标点外,可能被大家普遍遇到的是将比较运算符==误写成赋值运算符=,代码如下所示:

if(x=5) { … }

这里本意是比较变量x是否等于常量5,但是误将’==’写成了’=’,if语句恒为真。如果在逻辑判断表达式中出现赋值运算符,现在的大多数编译器会给出警告信息。并非所有程序员都会注意到这类警告,因此有经验的程序员使用下面的代码来避免此类错误:

if(5==x) { … }

将常量放在变量x的左边,即使程序员误将’==’写成了’=’,编译器会产生一个任谁也不能无视的语法错误信息:不可给常量赋值!

+=与=+、-=与=-也是容易写混的。复合赋值运算符(+=、*=等等)虽然可以使表达式更加简洁并有可能产生更高效的机器代码,但某些复合赋值运算符也会给程序带来隐含Bug,如下所示代码:

tmp=+1;

该代码本意是想表达tmp=tmp+1,但是将复合赋值运算符+=误写成=+:将正整数常量1赋值给变量tmp。编译器会欣然接受这类代码,连警告都不会产生。

如果你能在调试阶段就发现这个Bug,你真应该庆祝一下,否则这很可能会成为一个重大隐含Bug,且不易被察觉。

-=与=-也是同样道理。与之类似的还有逻辑与&&和位与&、逻辑或||和位或|、逻辑非!和位取反~。此外字母l和数字1、字母O和数字0也易混淆,这种情况可借助编译器来纠正。

很多的软件BUG自于输入错误。在Google上搜索的时候,有些结果列表项中带有一条警告,表明Google认为它带有恶意代码。如果你在2009年1月31日一大早使用Google搜索的话,你就会看到,在那天早晨55分钟的时间内,Google的搜索结果标明每个站点对你的PC都是有害的。这涉及到整个Internet上的所有站点,包括Google自己的所有站点和服务。Google的恶意软件检测功能通过在一个已知攻击者的列表上查找站点,从而识别出危险站点。在1月31日早晨,对这个列表的更新意外地包含了一条斜杠(“/”)。所有的URL都包含一条斜杠,并且,反恶意软件功能把这条斜杠理解为所有的URL都是可疑的,因此,它愉快地对搜索结果中的每个站点都添加一条警告。很少见到如此简单的一个输入错误带来的结果如此奇怪且影响如此广泛,但程序就是这样,容不得一丝疏忽。

数组常常也是引起程序不稳定的重要因素,C语言数组的迷惑性与数组下标从0开始密不可分,你可以定义int a[30],但是你绝不可以使用数组元素a[30],除非你自己明确知道在做什么。

switch…case语句可以很方便的实现多分支结构,但要注意在合适的位置添加break关键字。程序员往往容易漏加break从而引起顺序执行多个case语句,这也许是C的一个缺陷之处。对于switch…case语句,从概率论上说,绝大多数程序一次只需执行一个匹配的case语句,而每一个这样的case语句后都必须跟一个break。去复杂化大概率事件,这多少有些不合常情。

break关键字用于跳出最近的那层循环语句或者switch语句,但程序员往往不够重视这一点。

1990年1月15日,AT&T电话网络位于纽约的一台交换机当机并且重启,引起它邻近交换机瘫痪,由此及彼,一个连着一个,很快,114台交换机每六秒当机重启一次,六万人九小时内不能打长途电话。当时的解决方式:工程师重装了以前的软件版本。事后的事故调查发现,这是break关键字误用造成的。《C专家编程》提供了一个简化版的问题源码:

network code()
{
switch(line) {
case THING1:
doit1();
break;
case THING2:
if(x==STUFF) {
do_first_stuff();
if(y==OTHER_STUFF)
break;
do_later_stuff();
} /*代码的意图是跳转到这里… …*/
initialize_modes_pointer();
break;
default:
processing();
}/*… …但事实上跳到了这里。*/
use_modes_pointer();/*致使modes_pointer未初始化*/
}

那个程序员希望从if语句跳出,但他却忘记了break关键字实际上跳出最近的那层循环语句或者switch语句。现在它跳出了switch语句,执行了use_modes_pointer()函数。但必要的初始化工作并未完成,为将来程序的失败埋下了伏笔。

将一个整形常量赋值给变量,代码如下所示:

int a=34, b=034;

变量a和b相等吗?答案是不相等的。我们知道,16进制常量以’0x’为前缀,10进制常量不需要前缀,那么8进制呢?它与10进制和16进制表示方法都不相通,它以数字’0’为前缀,这多少有点奇葩:三种进制的表示方法完全不相通。如果8进制也像16进制那样以数字和字母表示前缀的话,或许更有利于减少软件Bug,毕竟你使用8进制的次数可能都不会有误使用的次数多!下面展示一个误用8进制的例子,最后一个数组元素赋值错误:

a[0]=106; /*十进制数106*/
a[1]=112; /*十进制数112*/
a[2]=052; /*实际为十进制数42,本意为十进制52*/

指针的加减运算是特殊的。下面的代码运行在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 {
*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 {
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。所以在main函数中调用ClearRAM(Fle),也只能清除数组Fle中的前四个元素了。

增量运算符++和减量运算符--既可以做前缀也可以做后缀。前缀和后缀的区别在于值的增加或减少这一动作发生的时间是不同的。作为前缀是先自加或自减然后做别的运算,作为后缀时,是先做运算,之后再自加或自减。许多程序员对此认识不够,就容易埋下隐患。下面的例子可以很好的解释前缀和后缀的区别。

int a=8,b=2,y;
y=a+++--b;

代码执行后,y的值是多少?

这个例子并非是挖空心思设计出来专门让你绞尽脑汁的C难题(如果你觉得自己对C细节掌握很有信心,做一些C难题检验一下是个不错的选择。那么,《The C Puzzle Book》这本书一定不要错过。),你甚至可以将这个难懂的语句作为不友好代码的反面例子。但是它也可以让你更好的理解C语言。根据运算符优先级以及编译器识别字符的贪心法原则,代码y=a+++--b;可以写成更明确的形式:

y=(a++)+(--b);

当赋值给变量y时,a的值为8,b的值为1,所以变量y的值为9;赋值完成后,变量a自加,a的值变为9,千万不要以为y的值为10。这条赋值语句相当于下面的两条语句:

y=a+(--b);
a=a+1;

2.2 玩具般的编译器语义检查

为了更简单的设计编译器,目前几乎所有编译器的语义检查都比较弱小,加之为了获得更快的执行效率,C语言被设计的足够灵活且几乎不进行任何运行时检查,比如数组越界、指针是否合法、运算结果是否溢出等等。

C语言足够灵活,对于一个数组a[30],它允许使用像a[-1]这样的形式来快速获取数组首元素所在地址前面的数据;允许将一个常数强制转换为函数指针,使用代码(*((void(*)())0))()来调用位于0地址的函数。C语言给了程序员足够的自由,但也由程序员承担滥用自由带来的责任。下面的两个例子都是死循环,如果在不常用分支中出现类似代码,将会造成看似莫名其妙的死机或者重启。

a. unsigned char i; b. unsigned chari;

for(i=0;i<256;i++) {… } for(i=10;i>=0;i--) { … }

对于无符号char类型,表示的范围为0~255,所以无符号char类型变量i永远小于256(第一个for循环无限执行),永远大于等于0(第二个for循环无线执行)。需要说明的是,赋值代码i=256是被C语言允许的,即使这个初值已经超出了变量i可以表示的范围。C语言会千方百计的为程序员创造出错的机会,可见一斑。

假如你在if语句后误加了一个分号改变了程序逻辑,编译器也会很配合的帮忙掩盖,甚至连警告都不提示。代码如下:

if(a>b); //这里误加了一个分号
a=b; //这句代码一直被执行

不但如此,编译器还会忽略掉多余的空格符和换行符,就像下面的代码也不会给出足够提示:

if(n<3)
return //这里少加了一个分号
logrec.data=x[0];
logrec.time=x[1];
logrec.code=x[2];

这段代码的本意是n<3时程序直接返回,由于程序员的失误,return少了一个结束分号。编译器将它翻译成返回表达式logrec.data=x[0]的结果,return后面即使是一个表达式也是C语言允许的。这样当n>=3时,表达式logrec.data=x[0];就不会被执行,给程序埋下了隐患。

可以毫不客气的说,弱小的编译器语义检查在很大程度上纵容了不可靠代码可以肆无忌惮的存在。

上文曾提到数组常常是引起程序不稳定的重要因素,程序员往往不经意间就会写数组越界。一位同事的代码在硬件上运行,一段时间后就会发现LCD显示屏上的一个数字不正常的被改变。经过一段时间的调试,问题被定位到下面的一段代码中:

int SensorData[30];
…for(i=30;i>0;i--)
{
SensorData[i]=…;

}

这里声明了拥有30个元素的数组,不幸的是for循环代码中误用了本不存在的数组元素SensorData[30],但C语言却默许这么使用,并欣然的按照代码改变了数组元素SensorData[30]所在位置的值, SensorData[30]所在的位置原本是一个LCD显示变量,这正是显示屏上的那个值不正常被改变的原因。真庆幸这么轻而易举的发现了这个Bug。

其实很多编译器会对上述代码产生一个警告:赋值超出数组界限。但并非所有程序员都对编译器警告保持足够敏感,况且,编译器也并不能检查出数组越界的所有情况。举一个例子,你在模块A中定义数组:

int SensorData[30];

在模块B中引用该数组,但由于你引用代码并不规范,这里没有显示声明数组大小,但编译器也允许这么做:

extern int SensorData[];

如果在模块B中存在和上面一样的代码:

for(i=30;i>0;i--)
{
SensorData[i]=…;

}

这次,编译器不会给出警告信息,因为编译器压根就不知道数组的元素个数。所以,当一个数组声明为具有外部链接,它的大小应该显式声明。

再举一个编译器检查不出数组越界的例子。函数func()的形参是一个数组形式,函数代码简化如下所示:

char * func(char SensorData[30])
{
unsignedint i;
for(i=30;i>0;i--)
{
SensorData[i]=…;

}
}

这个给SensorData[30]赋初值的语句,编译器也是不给任何警告的。实际上,编译器是将数组名Sensor隐含的转化为指向数组第一个元素的指针,函数体是使用指针的形式来访问数组的,它当然也不会知道数组元素的个数了。造成这种局面的原因之一是C编译器的作者们认为指针代替数组可以提高程序效率,而且,还可以简化编译器的复杂度。

指针和数组是容易给程序造成混乱的,我们有必要仔细的区分它们的不同。其实换一个角度想想,它们也是容易区分的:可以将数组名等同于指针的情况有且只有一处,就是上面例子提到的数组作为函数形参时。其它时候,数组名是数组名,指针是指针。

下面的例子编译器同样检查不出数组越界。

我们常常用数组来缓存通讯中的一帧数据。在通讯中断中将接收的数据保存到数组中,直到一帧数据完全接收后再进行处理。即使定义的数组长度足够长,接收数据的过程中也可能发生数组越界,特别是干扰严重时。这是由于外界的干扰破坏了数据帧的某些位,对一帧的数据长度判断错误,接收的数据超出数组范围,多余的数据改写与数组相邻的变量,造成系统崩溃。由于中断事件的异步性,这类数组越界编译器无法检查到。

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

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

}

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

如果我们精心设计溢出部分的数据,化数据为指令,就可以利用数组越界来修改PC指针的值,使之指向我们希望执行的代码。1988年,第一个网络蠕虫在一天之内感染了2000到6000台计算机,这个蠕虫程序利用的正是一个标准输入库函数的数组越界Bug。起因是一个标准输入输出库函数gets(),原来设计为从数据流中获取一段文本,遗憾的是,gets()函数没有规定输入文本的长度。gets()函数内部定义了一个500字节的数组,攻击者发送了大于500字节的数据,利用溢出的数据修改了堆栈中的PC指针,从而获取了系统权限。

一个程序模块通常由两个文件组成,源文件和头文件。如果你在源文件定义变量:

unsigned int a;

并在头文件中声明该变量:extern unsigned long a;

编译器会提示一个语法错误:变量’a’声明类型不一致。但如果你在源文件定义变量:

volatile unsigned int a,

在头文件中声明变量:extern unsigned int a; /*缺少volatile限定符*/

编译器却不会给出错误信息(有些编译器仅给出一条警告)。这里volatile属于类型限定符,另一个常见的类型限定符是const关键字。限定符volatile在嵌入式软件中至关重要,用来告诉编译器不要优化它修饰的变量。这里举一个刻意构造出的例子,因为现实中的volatile使用Bug大都隐含且难以理解。

在模块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的执行过程。

ARM架构下的编译器会频繁的使用堆栈,堆栈用于存储函数的返回值、AAPCS规定的必须保护的寄存器以及局部变量,包括局部数组、结构体、联合体和C++的类。从堆栈中分配的局部变量的初值是不确定的,因此需要运行时显式初始化该变量。一旦离开局部变量的作用域,这个变量立即被释放,其它代码也就可以使用它,因此堆栈中的一个内存位置可能对应整个程序的多个变量。

局部变量必须显式初始化,除非你确定知道你要做什么。下面的代码得到的温度值跟预期会有很大差别,因为在使用局部变量sum时,并不能保证它的初值为0。编译器会在第一次运行时清零堆栈区域,这加重了此类Bug的隐蔽性。

unsigned intGetTempValue(void)
{
unsigned int sum; //定义局部变量,保存总值
for(i=0;i<10;i++)
{
sum+=CollectTemp(); //函数CollectTemp可以得到当前的温度值
}
return (sum/10);
}

由于一旦程序离开局部变量的作用域即被释放,所以下面代码返回指向局部变量的指针是没有实际意义的,该指针指向的区域可能会被其它程序使用,其值会被改变。

char * GetData(void)
{
char buffer[100]; //局部数组

return buffer;
}

让人欣慰的是,现在越来越多的编译器意识到了语义检查的重要性,编译器的语义检查也越来越强大,比如著名的Keil MDK编译器在其 V4.47或以上版本中增加了动态语法检查并加强了语义检查,可以友好的提示更多警告信息。

2.3 不合理的优先级

C语言有32个关键字,却有34个运算符。要记住所有运算符的优先级是困难的。不合理的#define会加重优先级问题,让问题变得更加隐蔽。

#define READSDA IO0PIN&(1<<11) //定义宏,读IO口p0.11的端口状态
//判断端口p0.11是否为高电平
if(READSDA==(1<<11))
{

}

编译器在编译后将宏带入,原if语句变为:

if(IO0PIN&(1<<11) ==(1<<11))
{

}

运算符'=='的优先级是大于'&'的,代码IO0PIN&(1<<11) ==(1<<11))等效为IO0PIN&0x00000001:判断端口P0.0是否为高电平,这与原意相差甚远。

为了制造更多的软件Bug,C语言的运算符当然不会只止步于数目繁多。在此基础上,按照常规方式使用时,可能引起误会的运算符更是比比皆是!如下表所示:

2.4 隐式转换和强制转换

这又是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*/

一些编程建议:

  • 深入理解嵌入式C语言以及编译器
  • 细致、谨慎的编程
  • 使用好的风格和合理的设计
  • 不要仓促编写代码,写每一行的代码时都要三思而后行:可能会出现什么样的错误?是否考虑了所有的逻辑分支?
  • 打开编译器所有警告开关
  • 使用静态分析工具分析代码
  • 安全的读写数据(检查所有数组边界…)
  • 检查指针的合法性
  • 检查函数入口参数合法性
  • 检查所有返回值
  • 在声明变量位置初始化所有变量
  • 合理的使用括号
  • 谨慎的进行强制转换
  • 使用好的诊断信息日志和工具
  • 3、判错

    工欲善其事必先利其器。判错的最终目的是用来暴露设计中的Bug并加以改正,所以将错误信息提供给编程者是必要的。有时候需要将故障信息储存于非易失性存储器中,便于查看。这里以使用串口打印错误信息到PC显示屏为例,来说明一般需要显示什么信息。

    编写或移植一个类似C标准库中的printf函数,可以格式化打印字符、字符串、十进制整数、十六进制整数。这里称为UARTprintf()。

    unsigned int WriteData(unsigned int addr)
    {
    if((addr>= BASE_ADDR)&&(addr<=END_ADDR)) {
    …/*地址合法,进行处理*/
    } else { /*地址错误,打印错误信息*/
    UARTprintf ("文件%s的第 %d 行写数据时发生地址错误,错误地址为:0x%x\n",__FILE__,__LINE__,addr);
    …/*错误处理代码*/
    }

    假设UARTprintf()函数位于main.c模块的第256行,并且WriteData()函数在读数据时传递了错误地址0x00000011,则会执行UARTprintf()函数,打印如下所示的信息:

    文件main.c的第256行写数据时发生地址错误,错误地址为:0x00000011。

    类似这样的信息会有助于程序员定位分析错误产生的根源,更快的消除Bug。

    3.1 具有形参的函数,需判断传递来的实参是否合法。

    程序员可能无意识的传递了错误参数;外界的强干扰可能将传递的参数修改掉,或者使用随机参数意外的调用函数,因此在执行函数主体前,需要先确定实参是否合法。

    int exam_fun( unsigned char *str )
    {
    if( str != NULL ){ // 检查“假设指针不为空”这个条件

    ... //正常处理代码
    } else {
    UARTprintf(…); // 打印错误信息
    …//处理错误代码
    }
    }

    3.2 仔细检查函数的返回值

    对函数返回的错误码,要进行全面仔细处理,必要时做错误记录。

    char *DoSomething(…)
    {
    char * p;
    p=malloc(1024);
    if(p==NULL) { /*对函数返回值作出判断*/
    UARTprintf(…); /*打印错误信息*/
    return NULL;
    }
    retuen p;
    }

    3.3 防止指针越界

    如果动态计算一个地址时,要保证被计算的地址是合理的并指向某个有意义的地方。特别对于指向一个结构或数组的内部的指针,当指针增加或者改变后仍然指向同一个结构或数组。

    3.4 防止数组越界

    数组越界的问题前文已经讲述的很多了,由于C不会对数组进行有效的检测,因此必须在应用中显式的检测数组越界问题。下面的例子可用于中断接收通讯数据。

    #define REC_BUF_LEN 100
    unsigned char RecBuf[REC_BUF_LEN];
    … //其它代码
    void Uart_IRQHandler(void)
    {
    static RecCount=0; //接收数据长度计数器
    … //其它代码
    if(RecCount< REC_BUF_LEN){
    RecBuf[RecCount]=…; //从硬件取数据
    RecCount++;
    … //其它代码
    } else {
    UARTprintf(…); //打印错误信息
    … //其它错误处理代码
    }

    }

    在使用一些库函数时,同样需要对边界进行检查:

    #define REC_BUF_LEN 100
    unsigned char RecBuf[REC_BUF_LEN];

    if(len< REC_BUF_LEN){
    memset(RecBuf,0,len); //将数组RecBuf清零
    } else {
    //处理错误
    }

    3.5 数学算数运算

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

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

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

    3.5.2 加法溢出检测

    a)无符号加法

    #include
    unsigned int a,b,result;
    /*初始化a,b*/
    if(UINT_MAX-a //处理溢出
    } else {
    result=a+b;
    }

    b)有符号加法

    #include
    signed int a,b,result;
    /*初始化a,b */
    if((a>0 && INT_MAX-ab)){
    //处理溢出
    } else {
    result=a+b;
    }

    3.5.3 乘法溢出检测

    a)无符号乘法

    #include
    unsigned int a,b,result;
    /*初始化a,b*/
    if((a!=0) && (UINT_MAX/a //
    } else {
    result=a*b;
    }

    b)有符号乘法

    #include
    signed int a,b,tmp,result;
    /*初始化a,b*/
    tmp=a * b;
    if(a!=0 && tmp/a!=b){
    //
    } else {
    result=tmp;
    }

  • 检测移位时丢失有效位
  • 3.6 其它可能出现运行时错误的地方

    运行时错误检查是C 程序员需要加以特别的注意的,这是因为C语言在提供任何运行时检测方面能力较弱。对于要求可靠性较高的软件来说,动态检测是必需的。因此C 程序员需要谨慎考虑的问题是,在任何可能出现运行时错误的地方增加代码的动态检测。大多数的动态检测与应用紧密相关,在程序设计过程中要根据系统需求设置动态代码检测。

    4、容错

    1980年,美苏尚处于冷战阶段。这年,北美防空联合司令部曾报告称美国遭受导弹袭击。后来证实,这是反馈系统电路故障问题,但反馈系统软件没有考虑故障问题引发的误报。

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

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

    4.2 非易失性存储器的数据存储

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

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

    4.3 软件锁

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

    4.4 通信数据的检错

    通讯线上的数据误码相对严重,通讯线越长,所处的环境越恶劣,误码会越严重。通讯数据除了传统的硬件奇偶校验外,还应该增加软件CRC校验。超过16字节的数据应至少使用CRC16。在通讯过程中,如果检测到发生了数据错误,则要求重新发送当前帧数据。

    4.5 开关量输入的检测、确认

    开关量容易受到尖脉冲干扰,如果不进行滤除,可能会造成误动作。一般情况下,需要对开关量输入信号进行多次采样,并进行逻辑判断直到确认信号无误为止。多次采样之间需要有一定时间间隔,具体跟开关量的最大切换频率有关,一般不小于1ms。

    4.6 开关量输出

    开关信号简单的一次输出是不安全的,干扰信号可能会翻转开关量输出的状态。采取重复刷新输出可以有效防止电平的翻转。

    4.7 初始化信息的保存与恢复

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

    4.8 陷阱

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

    4.9 while循环

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

    2003年8月11日发生的W32.Blaster.Worm蠕虫事件导致全球经济损失高达5亿美元,这个漏洞是利用了Windows分布式组件对象模型的远程过程调用接口中的一个逻辑缺陷:在调用GetMachineName()函数时,循环只设置了一个不充分的结束条件。

    原代码简化如下所示:

    HRESULT GetMachineName ( WCHAR *pwszPath,
    WCHARwszMachineName[MAX_COMPUTTERNAME_LENGTH_FQDN+1])
    {
    WCHAR *pwszServerName = wszMachineName;
    WCHAR *pwszTemp = pwszPath + 2;
    while ( *pwszTemp != L’\\’ ) /* 这句代码循环结束条件不充分 */
    *pwszServerName++= *pwszTemp++;
    /*… */
    }

    微软发布的安全补丁MS03-026解决了这个问题,为GetMachineName()函数设置了充分终止条件。一个解决代码简化如下所示(并非微软补丁代码):

    HRESULT GetMachineName( WCHAR *pwszPath,
    WCHARwszMachineName[MAX_COMPUTTERNAME_LENGTH_FQDN+1])
    {
    WCHAR *pwszServerName = wszMachineName;
    WCHAR *pwszTemp = pwszPath + 2;
    WCHAR *end_addr = pwszServerName +MAX_COMPUTTERNAME_LENGTH_FQDN;
    while ((*pwszTemp != L’\\’ ) && (*pwszTemp != L’\0’)
    && (pwszServerName *pwszServerName++= *pwszTemp++;
    /*… */
    }

    4.10 系统自检

    对CPU、RAM、Flash、外部掉电保存存储器以及其他线路自检。

    作者:襄坤在线

    文章来源:博客园

    围观 311

    在嵌入式系统开发中,目前使用的主要编程语言是C 和汇编,虽然C++已经有相应的编译器,但是现在使用还是比较少的。

    在稍大规模的嵌入式程序设计中,大部分的代码都是用C来编写的,主要是因为C语言具有较强的结构性,便于人的理解,并且具有大量的库支持。但对于一写硬件上的操作,很多地方还是要用到汇编语言,例如硬件系统的初始化中的CPU 状态的设定,中断的使能,主频的设定,RAM控制参数等。另外在一些对性能非常敏感的代码块,基于汇编与机器码一一对应的关系,这时不能依靠C编译器的生成代码,而要手工编写汇编,从而达到优化的目的。汇编语言是和CPU的指令集紧密相连的,作为涉及底层的嵌入式系统开发,熟练对应汇编语言的使用也是必须的。

    单纯的C或者汇编编程请参考相关的书籍或者手册,这里主要讨论C和汇编的混合编程,包括相互之间的函数调用。下面分四种情况来进行讨论,不涉及C++语言。

    一、在C语言中内嵌汇编

    在C中内嵌的汇编指令包含大部分的ARM和Thumb指令,不过使用与单纯的汇编程序使用的指令略有不同,存在一些限制,主要有下面几个方面:

    a 不能直接向PC 寄存器赋值,程序跳转要使用B或者BL指令;

    b 在使用物理寄存器时,不要使用过于复杂的C表达式,避免物理寄存器冲突;

    c R12和R13可能被编译器用来存放中间编译结果,计算表达式值时可能把R0-R3、R12及R14用于子程序调用,因此避免直接使用这些物理寄存器;

    d 一般不要直接指定物理寄存器;

    e 让编译器进行分配内嵌汇编使用的标记是__asm或asm关键字,用法如下:__asm{instruction [; instruction]}或 asm("instruction [; instruction]")。

    下面是一个例子来说明如何在C中内嵌汇编语言:

    //C语言文件*.
    #include
    void my_strcpy(const char *src, char *dest){
    char ch;
    __asm{
    loop:
    ldrb ch, [src], #1
    strb ch, [dest], #1
    cmp ch, #0
    bne loop
    }
    }
    int main(){
    char *a="forget it and move on!";
    char b[64];
    my_strcpy(a, b);
    printf("original: %s", a);
    printf("copyed: %s", b);
    return 0;
    }

    在此例子中C语言和汇编之间的值传递是用C语言的指针来实现的,因为指针对应的是地址,所以汇编中也可以访问。

    二、在汇编中使用C定义的全局变量

    内嵌汇编不用单独编辑汇编语言文件,比较简洁,但是有很多的限制。当汇编的代码较多时一般放在单独的汇编文件中,这时就需要在汇编文件和C文件之间进行一些数据的传递,最简便的办法就是使用全局变量。

    下面是一个C语言和汇编语言共享全局变量的例子:

    //C语言文件*.

    #include
    int gVar=12;
    extern asmDouble(void);
    int main(){
    printf("original value of gVar is: %d", gVar_1);
    asmDouble();
    printf(" modified value of gVar is: %d", gVar_1);
    return 0;
    }

    ;汇编语言文件*.

    AREA asmfile, CODE, READONLY EXPORT asmDouble
    IMPORT gVar
    asmDouble
    ldr r0, =gVar
    ldr r1, [r0]
    mov r2, #2
    mul r3, r1, r2
    str r3, [r0]
    mov pc, lr
    END

    在此例中,汇编文件与C文件之间相互传递了全局变量gVar和函数asmDouble,留意声明的关键字extern和IMPORT

    三、在C中调用汇编的函数

    有一些对机器要求高的敏感函数,通过C语言编写再通过C编译器翻译有时会出现误差,因此这样的函数一般采用汇编语言来编写,然后供C语言调用。在C文件中调用汇编文件中的函数,要注意的有两点,一是要在C文件中声明所调用的汇编函数原型,并加入extern关键字作为引入函数的声明;二是在汇编文件中对对应的汇编代码段标识用EXPORT关键字作为导出函数的声明,函数通过mov pc, lr指令返回。这样,就可以在C文件中使用该函数了。从C语言的角度的角度,并不知道调用的函数的实现是用C语言还是汇编汇编语言,原因C语言的函数名起到表明函数代码起始地址的作用,而这个作用和汇编语言的代码段标识符是一致的。

    下面是一个C语言调用汇编函数例子:

    //C语言文件*.

    #include
    extern void asm_strcpy(const char *src, char *dest);
    int main(){
    const char *s="seasons in the sun"; char d[32];
    asm_strcpy(s, d);
    printf("source: %s", s);
    printf(" destination: %s",d);
    return 0;
    }

    ;汇编语言文件*.

    AREA asmfile, CODE, READONLY
    EXPORT asm_strcpy
    asm_strcpy
    loop
    ldrb r4, [r0], #1
    cmp r4, #0
    beq over
    strb r4, [r1], #1
    b loop
    over
    mov pc, lr
    END

    在此例中,C语言和汇编语言之间的参数传递是通过对应的用R0-R3来进行传递,即R0传递第一个参数,R1传递第二个参数,多于4个时借助栈完成,函数的返回值通过R0来传递。这个规定叫作ATPCS(ARM Thumb Procedure Call Standard),具体见ATPCS规范。

    四、在汇编中调用C的函数

    在汇编语言中调用C语言的函数,需要在汇编中IMPORT对应的C函数名,然后将C的代码放在一个独立的C文件中进行编译,剩下的工作由连接器来处理。

    下面是一个汇编语言调用C语言函数例子:

    //C语言文件*.

    int cFun(int a, int b, int c){
    return a+b+c;
    }
    ;汇编语言文件*.

    AREA asmfile, CODE, READONLY
    EXPORT cFun
    start
    mov r0, #0x1
    mov r1, #0x2
    mov r2, #0x3
    bl cFun
    nop
    nop
    b start
    END

    在汇编语言中调用C语言的函数,参数的传递也是按照ATPCS规范来实现的。

    在这里简单介绍一下部分ATPCS规范:

    子程序间通过寄存器R0~R3来传递参数。

    A.在子程序中,使用寄存器R4~R11来保存局部变量。

    B.寄存器R12用于子程序间scratch寄存器(用于保存SP,在函数返回时使用该寄存器出桟),记作IP。

    C.寄存器R13用于数据栈指针,记作SP。寄存器SP在进入子程序时的值和退出子程序时的值必须相等。

    D.寄存器R14称为链接寄存器,记作LR。它用于保存子程序的返回地址。

    E.寄存器R15是程序计数器,记作PC

    F.参数不超过4个时,可以使用寄存器R0~R3来传递参数,当参数超过4个时,还可以使用数据栈来传递参数。

    G.结果为一个32位整数时,可以通过寄存器R0返回

    H.结果为一个64位整数时,可以通过寄存器R0和R1返回,依次类推。

    以上通过几个简单的例子演示了嵌入式开发中常用的C 和汇编混合编程的一些方法和基本的思路,其实最核心的问题就是如何在C 和汇编之间传值,剩下的问题就是各自用自己的方式来进行处理。以上只是抛砖引玉,更详细和复杂的使用方法要结合实际应用并参考相关的资料。

    围观 914

    本文主要总结一些比较实用的单片机编程经验:

    经验之一:用“软件陷阱+程序口令”对付PC指针的弹飞

    当CPU受到外界干扰,有时PC指针会飞到另一段程序中,或跳到空白段去。其实,如果PC指针飞到空白段去,倒也好处理。只要在空白段设立软件陷阱(拦截指令),将程序拦截到初始化段或程序错误处理段。但是,如果PC指针飞到另一段程序中去了,系统如何办?小匠在这里推荐一种方法——程序口令,思路如下:

    1、首先,程序必须模块化。每个模块(子程序)执行一个功能。每个模块只有一个出口(RET)。

    2、设立一个模块(子程序)ID寄存器。

    3、为每个子程序配置一个唯一的ID号码。

    4、每当子程序执行完毕,要返回(RET)之前,先将本子程序的ID号送入 ID寄存器。

    5、返回到上级程序后,先判断ID寄存器中的ID号。

    如果正确,则继续执行;如果不正确,则表示PC指针有可能已经跳错了,子程序没有按预计的出口返回,这时将程序拦截到初始化段或程序错误处理段。

    这种方法,如同在程序中设立了若干个岗哨,每次调用子程序返回后,都要对口令(ID号),验明正身后再放行。再配合软件陷阱,基本上可以将大多数PC指针弹飞的现象检测到。到了程序错误处理段,要杀要剐(冷启动还是热启动)就由您了。

    仅以一条代码来揭示程序飞跑的本质!750102H ;MOV 01H,#02H ,如当前PC不是指向75H,而是指向01H或02H,那么51内的指令译码器将把她们忠实地翻译成AJMP X01H 或 LJMP XXXXH 而XX01H XXXXH又是什么呢?天知道!这样恶性飞跑下去那还不死定!改革一下:

    CLR A ;0C4H

    INC A ;04H

    MOV R1,A ;0F9H

    INC A ;04H

    MOV @R1,A ;86H

    每一字节代码都不能在生成跳转和循环,且都是单字节指令!往那跑去?跑出去了都要自己回来!“在家”千日好!“跳出”事事难嘛!这样只要平时习惯了用累加器和寄存器把数倒一倒,把那些危险代码都给倒掉,这样虽说给PC的“足”上多加了两字节的“包”可它不好“跑”啊!“足包”====跑!有朋友会问:要是PC抓做02H--LJMP 又有抓做了老鼻子远的XXH,再抓做隔壁的YYH不就没用了吗?提这样的问题只有ZENYIN这种钻牛角得才会提!PC那一位最活跃啊?PC0啊!要“扯拐”显然发生在她身上,至于那PC15同志啊,睡得更死猪一样,雷爆(强干扰)来了都打不醒?此外如果干扰都强到了PC高位都出错的地步!关电!关电!不干了!“不是我们不行而是敌人太强大”!反过来要是敌人在你的专政下,只是偶尔出来捣捣乱,但一出来就冲到屁西(PC)高层,就要问问是不是你的王国根基(硬件)有问题了?而非出在意识形态(软件)上!硬件为本!软件为标!标本兼治铸就坚强体魄,方能百毒不侵!

    经验之二:不要轻信软件狗

    关于软件狗的讨论,论坛上多矣。匠人也曾经查阅过许多关于软件狗的文章。有些大师确实提出了一些比较有技巧性的方法。但是,匠人的忠告是:不要轻信软件狗!其实,软件狗相当于软件的一种自律行为。一般的思路都是通过设立一个计数器,在计时中断中对其+1,在主程序的适当地方对其清零。如果程序失控了,清零指令未被执行,但中断造常发生,则计数器溢出(狗狗叫了)。但是这里有个问题:万一干扰导致中断被屏蔽了,那软件狗就永远不会叫了!——针对这种可能,有人提出在主程序中反复刷新中断使能标志,保证不让中断被屏蔽。——但万一程序飞到某个死循环中去了,不再执行“刷新中断使能标志”这一功能了,还是有可能把狗狗活活饿死。

    所以,匠人的观点是:看门狗必须拥有独立的计数器。(即硬件看门狗)好在现在好多芯片都提供了内部WDT。这种狗都是自带计数器的。即使干扰导致程序失控,WDT还是会造常计数直到溢出。当然,匠人也没有要将软件狗一棍子全部打死的意思。毕竟不管是软狗还是硬狗,逮到耗子就是好狗嘛(狗拿耗子——多管闲事?)。如果哪位训狗专家确实养过一条能看门的好软件狗,请牵出来让大伙瞧瞧。

    经验之三:话说RAM冗余技术

    所谓的RAM冗余,就是:

    1、将重要的数据信息备份2份(或以上)并存放在RAM中不同的区域(指地址不相连)。

    2、当平时对这些数据进行修改时,同时也更新备份。

    3、当干扰发生并被拦截到“程序错误处理段”中时,将数据与备份做比较,采用表决方式(少数服从多数)选出正确(或可能正确?)的那个。

    4、备份越多,效果越好。(当然,你得有足够的存储空间)。

    5、只备份最最原始的数据。中间变量(指那些可以从原始数据重新推导出来的数据)不必备份,

    注:1、这种思路的理论依据,据说是源于一种“概率论”,即一个人被老婆打肿脸的概率是很大的,但如果他捂着脸去上班却发现全公司每个已婚男人的脸都青了,这种概率是很小的。同理,一个RAM寄存器数据被冲毁的概率是很大的,但地址不相连的多个RAM同时被冲毁的概率是很小的。

    2、前两年,小匠学徒时,用过一次这种方法,但效果不太理想。当时感觉可能是概率论在我这失效了?现在回想起来,可能是备份的时机选的不好。结果将已经冲毁的数据又备份进去了。这样以来,恢复出来的数据自然也就不对了。

    经验之四:话说指令冗余技术

    前面有个朋友问到指令冗余,按匠人的理解,指令冗余,就是动作冗余。举个例子,你要在某个输出口上输出一个高电平去驱动一个外部器件,你如果只送一次“1”,那么,当干扰来临时,这个“1”就有可能变成“0”了。正确的处理方式是,你定期刷新这个“1”。那么,即使偶然受了干扰,它也能恢复回来。除了I/O口动作的冗余,匠人强烈建议大家在下面各方面也采用这种方法:

    1、LCD的显示。有时,也许你会用一些LCD的专用驱动芯片(如HT1621),这种芯片有个好处,即你只要将显示数据传送给它,它就会不断的自动扫描LCD。但是,你千万不要以为这样就没你啥事了。正确的处理方式是,要记得定期刷新送显数据(即使显示内容没有改变)。对于CPU中自带LCD DRIVER 的,也要定期刷新LCD RAM。

    2、中断使能标志的设置。不要以为你在程序初始化段将中断设置好就OK了。应该在主程序中适当的地方定期刷新一下,以免你的中断被挂起来。

    3、其它一些标志字和参数寄存器(包括你自己定义的),也要记得常常刷新。

    4、其它一些你认为有必要反复刷新的地方。

    经验之五:10种软件滤波方法

    下面奉献——匠人呕心沥血搜肠刮肚冥思苦想东拼西凑整理出来的10种软件滤波方法:

    1、限幅滤波法(又称程序判断滤波法)

    A、方法:根据经验判断,确定两次采样允许的最大偏差值(设为A),每次检测到新值时判断:如果本次值与上次值之差<=A,则本次值有效。如果本次值与上次值之差>A,则本次值无效,放弃本次值,用上次值代替本次值

    B、优点:能有效克服因偶然因素引起的脉冲干扰。

    C、缺点:无法抑制那种周期性的干扰,平滑度差。

    2、中位值滤波法

    A、方法:连续采样N次(N取奇数),把N次采样值按大小排列,取中间值为本次有效值.

    B、优点:能有效克服因偶然因素引起的波动干扰,对温度、液位的变化缓慢的被测参数有良好的滤波效果。

    C、缺点:对流量、速度等快速变化的参数不宜。

    3、算术平均滤波法

    A、方法:连续取N个采样值进行算术平均运算。N值较大时:信号平滑度较高,但灵敏度较低;N值较小时:信号平滑度较低,但灵敏度较高。N值的选取:一般流量,N=12;压力:N=4

    B、优点:适用于对一般具有随机干扰的信号进行滤波,这样信号的特点是有一个平均值,信号在某一数值范围附近上下波动。

    C、缺点:对于测量速度较慢或要求数据计算速度较快的实时控制不适用,比较浪费RAM。

    4、递推平均滤波法(又称滑动平均滤波法)

    A、方法:把连续取N个采样值看成一个队列,队列的长度固定为N,每次采样到一个新数据放入队尾,并扔掉原来队首的一次数据.(先进先出原则),把队列中的N个数据进行算术平均运算,就可获得新的滤波结果。N值的选取:流量,N=12;压力:N=4;液面,N=4~12;温度,N=1~4

    B、优点:对周期性干扰有良好的抑制作用,平滑度高,适用于高频振荡的系统。

    C、缺点:灵敏度低 ,对偶然出现的脉冲性干扰的抑制作用较差,不易消除由于脉冲干扰所引起的采样值偏差,不适用于脉冲干扰比较严重的场合,比较浪费RAM

    5、中位值平均滤波法(又称防脉冲干扰平均滤波法)

    A、方法:相当于“中位值滤波法”+“算术平均滤波法”。连续采样N个数据,去掉一个最大值和一个最小值,然后计算N-2个数据的算术平均值。N值的选取:3~14

    B、优点:融合了两种滤波法的优点,对于偶然出现的脉冲性干扰,可消除由于脉冲干扰所引起的采样值偏差。

    C、缺点:测量速度较慢,和算术平均滤波法一样,比较浪费RAM。

    6、限幅平均滤波法

    A、方法:相当于“限幅滤波法”+“递推平均滤波法”,每次采样到的新数据先进行限幅处理,再送入队列进行递推平均滤波处理 。

    B、优点:融合了两种滤波法的优点,对于偶然出现的脉冲性干扰,可消除由于脉冲干扰所引起的采样值偏差。

    C、缺点:比较浪费RAM。

    7、一阶滞后滤波法

    A、方法:取a=0~1,本次滤波结果=(1-a)*本次采样值+a*上次滤波结果。

    B、优点:对周期性干扰具有良好的抑制作用,适用于波动频率较高的场合。

    C、缺点: 相位滞后,灵敏度低,滞后程度取决于a值大小,不能消除滤波频率高于采样频率的1/2的干扰信号。

    8、加权递推平均滤波法

    A、方法:是对递推平均滤波法的改进,即不同时刻的数据加以不同的权。通常是,越接近现时刻的数据,权取得越大。给予新采样值的权系数越大,则灵敏度越高,但信号平滑度越低。

    B、优点:适用于有较大纯滞后时间常数的对象和采样周期较短的系统。

    C、缺点:对于纯滞后时间常数较小,采样周期较长,变化缓慢的信号不能迅速反应系统当前所受干扰的严重程度,滤波效果差。

    9、消抖滤波法

    A、方法:设置一个滤波计数器将每次采样值与当前有效值比较:如果采样值=当前有效值,则计数器清零如果采样值<>当前有效值,则计数器+1,并判断计数器是否>=上限N(溢出),如果计数器溢出,则将本次值替换当前有效值,并清计数器 。

    B、优点:对于变化缓慢的被测参数有较好的滤波效果,可避免在临界值附近控制器的反复开/关跳动或显示器上数值抖动。

    C、缺点:对于快速变化的参数不宜,如果在计数器溢出的那一次采样到的值恰好是干扰值,则会将干扰值当作有效值导入系统。

    10、限幅消抖滤波法

    A、方法:相当于“限幅滤波法”+“消抖滤波法” 先限幅,后消抖。

    B、优点: 继承了“限幅”和“消抖”的优点改进了“消抖滤波法”中的某些缺陷,避免将干扰值导入系统。

    C、缺点:对于快速变化的参数不宜。

    IIR 数字滤波器

    A. 方法:确定信号带宽, 滤之。 Y(n) = a1*Y(n-1) + a2*Y(n-2) + . + ak*Y(n-k) + b0*X(n) + b1*X(n-1) + b2*X(n-2) + . + bk*X(n-k)。

    B. 优点:高通,低通,带通,带阻任意。设计简单(用matlab)

    C. 缺点:运算量大。

    文章来源:互联网(版权归原著作者所有)

    围观 290

    在进行单片机开发时,经常都会出现一些很不起眼的问题,这些问题其实都是很基础的C语言知识点,是一些小细节。

    但是正是因为很基础,又都是小细节,所以我们往往容易忽视它们。结果有时候我们会花很长的时间纠结一个问题,迟迟找不到问题的所在。

    当发现原因竟然是这么的简单和不起眼时,大家都会感到痛不欲生。这些问题要记录下来,时刻提醒自己!!

    1、! 和 ~ 不一样

    ! 是逻辑非符号,~ 是位取反符号。

    对IO口某个引脚赋值时不要错用 ! 如

    2、<< 和 >> 的优先级低于+、-

    比如要实现c=x*2+1,没有加括号会出错

    3、移位要防止溢出

    其实用移位代替乘除法是个不错的方法,笔者很喜欢拿到一段代码后用移位代替乘除法来进行优化。不过有时候却会出现问题,比如溢出问题。当很明显可能溢出的话我们是会注意的,比如

    但是有时候这个问题是不明显的,比如当移位出现在数组索引或函数参数时,有段用液晶显示字符的代码如下

    我们可以用左移运算来代替乘法进行优化,如

    这本是一个好方法,但是事实上上面的代码是错的。当执行c<<4时,因为没有明显的赋值过程,我们可能认为没问题,而事实上c的高位已经丢失了,所以得到错误的结果。一个可行的做法是先进行强制转换,如

    4、无符号数和有符号数混合运算都会被强制转换为无符号数运算

    当一个有符号数和一个无符号数进行算术运算时,系统会自动将有符号数强制转换为无符号数再进行运算(即使你使用有符号数强制类型转换),如下面两种写法的运输结果是一样的

    5、局部变量要初始化

    局部变量没有初始化的话,因为单片机每次为他分配的是同一个内存区域,当你在函数中是这么使用局部变量时,就可能出问题:

    如果第一次调用fun时,a传递的值为0,那么flag = 0x01;执行if(flag&0x01)后面的代码。以后再调用fun时,即使a不为0,但flag依然使用之前的内存区域,所以其值一直为0x01,一直执行的是if后面的代码,而不是else后面的。

    如果要避免这个错误,平时要养成对局部变量初始化的习惯。

    围观 304

    引言

    在嵌入式软件系统开发过程中,大量使用C语言进行应用程序开发以提高开发效率。同时,系统中经常包含一些决定整个系统性能的关键模块,此时为了获得最佳性能,经常使用汇编语言编写它们,或者某些特殊情况下,例如操作硬件等,也必须使用汇编语言。

    函数是C语言中一个重要的概念,在汇编语言中经常使用子例程或过程(subroutine or procedure)表达同样的概念,本文使用术语子例程。本文首先介绍ARM汇编语言子例程设计的一般方法,并以此为基础提出一种新的基于堆栈帧的设计方法,同时介绍与C语言交互技术。

    1、 一般方法

    在ARM汇编语言中一般使用BL(Branch and Link)指令调用某个子例程,BL指令首先将返回地址保存在链接寄存器R14(也称为LR)中,然后跳转到目标地址。子例程执行完毕后,通过将R14的内容复制到PC中实现从子例程返回。

    BL subr ;调用subr

    … ;返回到这里

    subr

    … ;子例程体

    MOV PC, LR ;从subr返回

    上面这种方法对于叶子例程(即不调用其它子例程的例程)来说已经足够了,但是它并不能处理嵌套或递归调用。假设subr内部又使用BL调用了另一个子例程,那么LR将被后一次调用的返回地址所改写,导致死循环无法从subr返回。为了解决这个问题,subr必须在调用第二个子例程之前保存LR。更进一步,为了使子例程能够以任意深度调用另外一个子例程,必须采取某种方法以保存任意数目的返回地址。最常用的方法是将返回地址保存在堆栈中,如下面的例子所示:

    subr

    STMFD SP!, {R4-R12, LR} ;保存所有的工作寄存器和返回地址,并更新堆栈指针

    … ;子例程体

    LDMFD SP! { R4-R12, PC} ;恢复所有的工作寄存器,使用保存的返回地址装载PC,

    ;更新堆栈指针

    在子例程入口点可以把subr中需要使用的任何工作寄存器和LR保存到堆栈上,在出口点将它们弹出,这样就可以安全的进行子例程调用,而不必担心返回地址被改写导致无法从子例程正常返回。注意在出口点直接使用返回地址装载PC,它等价于下面的两条指令:

    LDMFD SP! { R4-R12, LR}

    MOV PC, LR

    2、基于堆栈帧的子例程

    前面介绍的子例程设计方法虽然已经能够满足设计需要,但是对于熟悉x86汇编语言的程序员来说还是不太适应。众所周知,x86汇编语言子例程存在一个标准的堆栈结构,如图1所示。它的一个显著特点是EBP寄存器作为参考点用来引用参数和局部变量,例如第一个参数位于地址[EBP+8]处。堆栈帧的优点在于它统一了汇编子例程的编程风格,参数、返回地址、工作寄存器或者局部变量都有固定的位置,这样不仅能够提高代码的可读性也有利于代码的维护。基于上面的考虑,特将堆栈帧的概念引入ARM汇编语言子例程的设计之中,如下面的例子所示。为了简便,假设subr的原型为int subr(int a, int b, int c, int d, int e, int f);,很明显根据APCS(ARM过程调用标准),参数a-d通过寄存器R0-R3进行传递,剩下的两个参数e和f通过堆栈传递。最终形成的堆栈帧结构如图2所示,与图1中的x86帧结构相比,唯一的不同之处在于局部变量和工作寄存器的位置相反,而出现这种差异的原因是为了充分利用ARM中多寄存器load-store指令的优势。

    caller

    … ;省略了参数a-d的传递代码

    MOV R4, #2

    STR R4, [SP, #-4]! ;1)将参数f推入堆栈

    MOV R4, #1

    STR R4, [SP, #-4]! ;将参数e推入堆栈

    BL subr ;2)调用子例程subr

    ADD SP, SP, #8 ;8)平衡堆栈。subr返回到这里,返回值保存在R0中

    subr

    STMFD SP!, {R4-R7, FP,LR} ;3)保存工作寄存器、FP和LR

    ADD FP, SP, #16 ;4)计算帧指针

    SUB SP, SP, #8 ;5)为局部变量分配空间

    LDR R4, [FP, #8] ;载入参数e

    LDR R5, [FP, #12] ;载入参数f

    … ;subr子例程体

    ADD SP, SP, #8 ;6)释放局部变量空间

    LDMFD SP!, {R4-R7, FP, PC} ;7)恢复寄存器并返回

    图1 x86堆栈帧结构

    图2 ARM中的堆栈帧结构

    下面详细的说明如何一步步构建堆栈帧,其中序号与示例代码注释中的序号是一一对应的:

    1) 通常,使用STR Rn, [SP, #-4]!指令将子例程需要的参数推入堆栈。注意根据APCS,首先考虑通过寄存器R0-R3传递参数,剩下的参数以相反的顺序推入堆栈。如果通过寄存器R0-R3就可以传递所有的参数,那么可以省略这个步骤。

    2) BL指令将返回地址推入堆栈,然后跳转到指定的子例程继续执行。自此开始所有修改堆栈的工作转交给子例程。

    3) 如果子例程需要使用R4-R11工作寄存器,必须将它们推入堆栈;同时将旧的帧指针寄存器FP和链接寄存器LR推入堆栈,这些工作在一条指令中即可高效的完成。

    4) 调整帧指针FP,以便随后使用它引用堆栈参数和变量。在本例中,可以使用LDR R0, [FP, #8]引用参数e,LDR R0, [FP, #-20]引用第一个局部变量。

    5) 分配8个字节的堆栈空间存储子例程的局部变量。但是如果不需要使用局部变量,那么可以省略这个步骤。与CISC架构的x86处理器不同,RISC架构的ARM处理器拥有大量的通用寄存器,例如本例中的R0-R7、LR等,因此大多数情况下并不需要为局部变量分配堆栈空间。

    6) 如果先前为局部变量分配了堆栈空间,那么为了保持堆栈平衡需要释放它们。

    7) 恢复第三步保存到堆栈的各个寄存器,这里也是通过直接装载PC寄存器从子例程返回。

    8) 子例程subr执行完成后返回到这里。这一步非常重要,由于caller在调用subr前将参数e和f推入堆栈,因此从subr返回后caller必须将这两个参数弹出堆栈,以保持堆栈的平衡。当然如果是从C语言中调用子例程,那么编译器会负责完成堆栈平衡工作。

    3、汇编语言与C语言交互

    在完成汇编子例程的编写之后,下一个问题就是如何在C语言中调用它们。本质上,不管使用何种语言编写代码,交叉调用其它模块的例程必须遵循一个通用的参数和结果传递约定。对于ARM来说,这个约定称为ARM过程调用标准,其定义了:

    l 通用寄存器的特定用途

    l 使用何种类型的堆栈

    l 参数和结果的传递机制

    l ARM共享库机制支持

    由于编译器生成的代码总是严格遵循APCS,因此只需保证手动编写的汇编代码符合APCS即可。下面的示例展示了如何从C语言中调用汇编语言编写的实现内存拷贝功能的子例程,开发环境为RealView MDK3.22a。

    ;定义和导出mymemcpy的mymemcpy.s文件

    ; R0目的地址,R1指向源地址,R2拷贝长度

    AREA Demo, CODE, READONLY

    EXPORT mymemcpy

    mymemcpy

    STMFD SP!, {R4,LR}

    MOV R3,R0 ;取出目的地址

    MOV R12,R1 ;取出源地址

    copy

    CMP R2, #0 ;如果长度小于等于0则退出

    BLE exit

    SUB R2,R2, #0x1

    BEQ exit

    LDRB LR, [R12],#0x1

    STRB LR, [R3],#0x1

    B copy

    exit

    LDMFD R13!,{R4, PC}

    END

    //main.c 测试程序

    extern void *mymemcpy(void *dst, const void *src, size_t size);

    int main(int argc, char** argv)

    {

    const char *src = "First string - source ";

    char dst[] = "Second string - destination ";

    mymemcpy(dst, src, strlen(src)+1);

    return (0);

    }

    从汇编语言调用C函数的关键之处在于如何根据C函数的原型正确的传递参数。下面的示例展示了如何调用C库函数strcmp,其原型为int strcmp(const char *s1, const char *s2); ,它只有两个指针类型参数,因此R0和R1分别指向第一个和第二个字符串即可。注意由于使用了C库函数,请选中项目选项对话框、Target选项卡中的Use MicroLib选项。

    AREA |.text|, CODE, READONLY

    EXPORT main ;导出main

    IMPORT __main

    IMPORT strcmp ;导入strcmp函数

    main

    STMFD SP!, {R4,LR} ;保存LR

    ADR R0, big ;通过R0传递参数1

    ADR R1, small ;通过R1传递参数2

    BL strcmp ;调用strcmp库函数

    LDMFD SP!, {R4,PC}

    big

    DCB "big",0

    small

    DCB "SMALL",0

    END

    小结

    本文从作者的实践出发,谈了一些关于ARM汇编子例程设计方法及其与C语言交互的心得,不当之处请读者指正。

    文章来源:博客园

    围观 483

    CPU懂的机器语言

    单片机的CPU从存储器读取程序,但是一次只能读取一条指令,然后解释每条指令,并执行。存储器中保存的内容,不管是程序还是数据,都是二进制代码“0”和“1”组成的字符串。指令二进制代码告诉CPU要做什么,而数据二进制代码则是CPU操作或处理指令时要使用的值。CPU的操作包含加、减运算等指令。这些像密码一样排列的“0”和“1”字符串就是机器语言。比如图1左边显示的就是一个机器语言指令,意思是“将2放入寄存器A(寄存器是CPU内部的储存区域)。

    CPU总是按存储器地址的顺序读取指令代码,除非遇到跳跃指令。例如,如果复位后的地址是0000,则从0000开始按0001、0002、0003的顺序读取并执行指令。也可以说,一个程序就是按处理要求排列一系列的机器语言。

    CPU只能理解如上所述的机器语言。因此,为了使CPU运行,就必须使用机器语言的程序。但是,机器语言不易为人们识别和读写。因此,人们用了更简单易懂的字符串来代替机器语言,这就是汇编语言。例如,在“给寄存器A赋值2”这样的处理时,如果用汇编语言来表示,就很简单,请看图1的右边部分。汇编语言中,用MOV字符串表示赋值,所以“给寄存器A赋值2”的处理就可用“MOV A,#02”表示。

    图1:机器语言和汇编语言的比较

    虽然汇编语言比机器语言更加简单易懂了,但是人们读起来还是挺难理解的。而且,汇编语言还存在另一个问题,就是不同的CPU,机器语言的描述方式也不同。因此,如果更换了CPU,就必须改写与机器语言有着密不可分关系的汇编语言,工作量比较大。(以上例子中的机器语言和汇编语言均为瑞萨的RL78族单片机中的语言。)

    如上所述,每更换一次CPU都必须对程序进行改编,不但造成生产性低下,还加重了编程人员的负担。

    人性化的C语言

    能够解决上述问题的编程语言就是C语言。C语言具有不依存于特定的CPU,又具有程序移植性高等的特点。另外,由于编程时可使用人们熟悉的英文单词,所以对编程人员来说C语言是最容易使用的编程语言。下面我们将C语言和汇编语言做一个简单地比较。(图2)

    图2:汇编语言和C语言的比较

    虽然C语言不依存于CPU而且还是人们最容易使用的编程语言,但对于CPU来说,C语言却是一种完全无法理解的语言。因此,就需要一种可以将C语言翻译为机器语言的软件,这就是被称为编译器 (编译程序) 的软件。 经过编译器翻译的程序的文件格式被称为目标文件格式。如果目标文件格式最终没有被配置到存储器中,CPU就无法执行该程序。

    另外,近来由于程序越来越趋于复杂化,所以几乎都采取了将一个程序分割为多个C语言程序文件的结构。所以,还需要一个工具将多个目标文件格式汇总成一个机器语言并配置到存储器上,能够担当起此重任的就是连接编辑程序(linkage editor,也被称为“linker(链接器)”)。

    能够找出程序错误的调试器

    由人进行编程的应用程序难免会存在错误(bug)。而用来发现和帮助人们修正程序错误的工具被称为调试器(Debugger)。下面简单介绍调试器的类型。

    电路内仿真器(In-Circuit Emulator , 简称:ICE) :ICE可取代实际的单片机,与仿真专用的评价单片机(evaluation chip,评价芯片)连接并进行调试。其中,“In-Circuit Emulator”为美国英特尔公司的注册商标,瑞萨将其命名为“Full-spec Emulators”并向市场提供。

    J-TAG仿真器:J-TAG仿真器使用单片机内事先预留的调试电路进行调试。也就是说通过实际使用的单片机来进行调试。和ICE相比,J-TAG仿真器的价格较低。瑞萨将其命名为“On-chip Debug Emulator”并向市场提供。

    简易仿真器:简易仿真器是使调试用的监视程序在单片机上运行,在与PC通信的同时进行调试。除了调试对象的程序之外,还需启动其他监视程序,所以,与ICE或J-TAG仿真器相比,简易仿真器的程序运行速度慢而且还有各种功能限制。其最大的优点是价格非常低廉。

    综合开发环境

    正如上面所讲的,在进行单片机的软件开发时,使用了上述的编译器、连接编辑程序、调试器等各种工具。以前,这些软件都是作为单个软件分别提供的,一般是通过命令提示符调出各个程序、或是通过批处理程序调出使用。但是,最近开始以综合开发环境的方式给予提供,综合开发环境就是将各种程序综合到一个程序包中,只需通过Renesas CS+ 等便可很容易地将程序调出使用。

    围观 403

    页面

    订阅 RSS - 编程工具