嵌入式C语言编程要点
模块划分
(1) 模块即是一个.c文件和一个.h文件的结合,头文件是对于模块接口的声明。
(2) 某模块提供给其它模块调用的外部函数及数据需要在头文件中以extern声明。
(3) 模块内的函数和全局变量需要冠以static关键字声明。
(4) 永远不要在头文件中定义变量。定义变量和变量声明的区别在于“定义”会产生内存分配的操作,是汇编阶段的概念;而“声明”则只是告诉包含该声明的模块在连接阶段从其它模块寻找外部函数和变量。
(5) 防止重定义,使用“#ifndef - #define - #endif”结构。
单任务程序典型架构
(1) 从CPU复位时的指定地址开始执行。
(2) 跳转至汇编代码的startup处执行。
(3) 跳转至用户主程序main执行并完成:硬件设备和软件模块的初始化,进入无限循环以调用各模块的处理函数。
中断服务程序
中断是嵌入式系统中重要的组成部分,但是在标准C中不包含中断,许多编译器开发商在标准C中增加对于中断的支持,提供新的关键字用于中断服务程序(ISR),类似于__interrupt等。当一个函数被定义为ISR时,编译器会自动为该函数增加中断服务程序所需要的中断现场保护的入栈和出栈代码。
中断服务程序需要满足如下要求:
(1) 不能返回值
(2) 不能传递参数
(3) ISR应该尽量短小
(4) printf函数会带来重入和性能问题,不能采用
硬件驱动模块
硬件初始化过程:
(1) 修改寄存器,设置硬件参数
(2) 将ISR入口地址写入中断向量表(vector table)
(3) 设置CPU针对该硬件的控制线,如果控制线可用于PIO(可编程I/O)和控制信号用,则设置CPU内部对应寄存器使其作为控制信号;设置CPU内部的针对该设备的中断屏蔽位,设置中断方式(电平触发或边缘触发)
(4) 提供一系列针对该设备的操作接口函数
C的面向对象化
在面向对象语言里,出现了“类”的概念。类是对特定数据的特定操作的集合体。类包含两个范畴:数据和行为。而C语言中的struct仅仅是数据的集合,我们可以利用函数指针将struct模拟为一个包含数据和操作的“类”。
在C语言中,可以模拟出面向对象的三个特性:封装、继承、多态。但更多的时候,我们只是需要将数据和行为封装以解决软件结构混乱的问题。C模拟面向对象的思想不在于模拟行为本身,而在于解决某些情况下使用C语言编程时程序整体框架结构分散、数据和函数脱节的问题。
#ifndef C_Class
#define
C_Class struct
#endif
C_Class ClassA
...
{
C_Class ClassA *A_this; //this指针
void (*Func)(C_Class ClassA *A_this); //函数指针表示行为
//数据
int a;
int b;
}
;
数据指针
在使用绝对地址指针时,要注意指针自增自减的结果取决于指针指向的数据类型。p++或++p的结果等同于p = p + sizeof(int)。
int
*
p
=
(
int
*
)
0x0000FFFF
;
CPU以字节为单位编址,而C语言指针以指向的数据类型长度作自增和自减。
函数指针
(1) 调用函数实际上等同于“跳转指令+参数传递处理+回归位置入栈”,本质上最核心的操作是将函数生成的目标代码的首地址赋给CPU的pc寄存器。
(2) 因为函数调用的本质是跳转到某一个地址单元的code去执行,所以可以“调用”一个根本不存在的函数实体。
typedef
void
(
*
lpFunc)();
//
定义一个无参数无返回类型的函数指针类型
lpFunc reset
=
(lpFunc)
0xF000FFF0
;
//
函数指针,指向系统初始化的指令
reset();
//
调用函数
(3) 函数无他,唯指令集合耳。
数组和动态申请内存
(1) 尽可能选用数组,因为数组不能越界访问。
(2) 如果使用动态申请内存,则申请后一定要判断是否申请成功,并且malloc与free应成对出现。
关键字const
const
int
a;
//
该变量的值不能改变
int
const
a;
//
该变量的值不能改变
const
int
*
a;
//
该指针指向的内存区域的值不能改变
int
*
const
a;
//
该指针的值不能改变
int
const
*
a
const
;
//
该指针的值与指向的内存区域的值都不能改变
(1) 为阅读你的代码的人传达非常有用的信息。
(2) 使编译器很自然地去保护那些不希望被改变的参数,可以减少bug的出现。
关键字volatile
C语言编译器会对用户书写的代码进行优化,比如:
int
a, b, c;
a
=
in
(
0x100
);
//
读取I/O空间0x100端口的内容
b
=
a;
a
=
in
(
0x100
);
//
再次读入IO空间0x100端口的内容
c
=
a;
很可能会被编译器优化为:
int
a, b, c;
a
=
in
(
0x100
);
b
=
a;
c
=
a;
这样优化的结果很可能导致错误,因为在第一次读取I/O空间0x100端口的值后,很可能被其它程序写入新值,所以第二次读取的值和第一次的值就会不一样。而优化后,两次读取的值就一样了。在变量a定义时加上volatile关键字可以防止编译器的优化。
volatile变量一般使用于如下情况:
(1) 并行设备的硬件寄存器(如:状态寄存器)
(2) 一个中断服务程序中会访问到的非自动变量(也就是全局变量)
(3) 多线程应用中被几个任务共享的变量
CPU字长于存储器位宽
某CPU字长为16位,NVRAM的位宽为8位,在这种情况下,我们需要为NVRAM提供读写字节、字的操作,如下:
typedef unsigned
char
BYTE;
typedef unsigned
int
WORD;
BYTE readByte_NVRAM(WORD offset)
...
{
LPBYTE lpAddr = (BYTE *)(NVRAM + offset*2);
return *lpAddr;
}
WORD readWord_NVRAM(WORD offset)
...
{
WORD wTmp = 0;
LPBYTE lpAddr;
lpAddr = (BYTE *)(NVRAM + offset*2);
wTmp += (*lpAddr) << 8;
lpAddr = (BYTE *)(NVRAM + (offset+1)*2);
wTmp += *lpAddr;
return wTmp;
}
为什么在上例中偏移要乘以2?
因为16位的CPU与8位的NVRAM之间互联只能以地址线A1对其A0,CPU本身的A0与NVRAM不连接,因此NVRAM的地址只能是偶数地址,故每次以0x10为单位。
寄存器变量
当对一个变量频繁读写时,需要反复访问内存,从而花费大量存取时间。为此,C语言提供了一种变量,即寄存器变量。这种变量存放在CPU的寄存器中,使用时,不需要访问内存,而直接从寄存器中读写,从而提高效率。寄存器变量的说明符为register,循环计数是应用寄存器变量的最好候选者。
只有局部自动变量和形参才能定义成寄存器变量。因为寄存器变量属于动态储存方式,凡采用静态存储方式的变量都不能定义为寄存器变量,包括:模块间全局变量、模块内全局变量、局部static变量。
register是一个“建议型”关键字,表示程序建议该变量放于寄存器中,但最终该变量可能因为条件不满足而未能成为寄存器变量,被放置于存储器中,这个情况下,编译器并不会报错(在C++语言中,还有一“建议型”关键字,inline)。
内嵌汇编
程序中对时间要求苛刻的部分可以用内嵌汇编来重写,以带来速度上的显著提高。但是,开发和测试汇编代码是一件艰苦的工作,它将花费更长的时间,因而要慎重选择汇编的部分。
在程序中,存在一个80-20原则,即20%的程序消耗了80%的时间,因而我们要改进效率,最主要是考虑改进那20%的部分。
在C程序中,可以直接插入__asm{}内嵌汇编语句:
/**/
/* 把两个参数值相加,放入另一全局变量中 */
int
result;
void
add(
long
a,
long
*
b)
...
{
__asm
...{
MOV AX, a
MOV BX, b
ADD AX, [BX]
MOV result, AX
}
}
利用硬件特性
首先要明白CPU对各种存储器的访问速度,基本上是:
CPU内部RAM > 外部同步RAM > 外部异步RAM > FLASH/ROM
对于程序代码,已经被烧录在FLASH或ROM中,我们可以让CPU直接从中读取代码执行,但通常不是一个好方法,我们最好在系统启动时将FLASH或ROM中的目标代码复制到RAM中再执行以提高取指令的速度。
对于UART等设备,其内部有一定容量的接受BUFFER,我们应尽量在BUFFER被占满后再向CPU提出中断。
对某设备能采取DMA方式,就应该采用DMA方式。DMA方式在读取目标中包含的存储信息较大时效率较高,其数据传输基本单位是块,而所传输的数据是从设备直接送入内存。DMA方式比起中断驱动方式,减少了CPU对外设的干预,进一步提高了CPU与外设间的并行处理。
位操作
在C语言中的位操作可以减少除法和模运算。在计算机程序中数据的位是可以操作的最小单位,理论上可以用“位操作”来完成所有的运算和操作,因而,灵活的位操作可以有效提高程序运行的效率。
/**/
/* 效率较低的操作 */
int
i,
=
1024
/
16
;
int
j
=
986
%
32
;
/**/
/* 效率较高的操作 */
int
i
=
1024
>>
4
;
int
j
=
986
– (
986
>>
5
<<
5
);
对于以2的指数次方为“*”、“/”或“%”因子的数学运算,转化为移位运算通常可以提高算法的效率,因为乘除运算指令周期通常比移位运算要大。
C语言位操作除了可以提高运算效率外,在嵌入式系统编程中,最典型的应用是位间的与(&)、或(|)、非(~)操作,这个嵌入式系统的编程特点有很大关系。我们通常要对硬件寄存器进行置位。
/**/
/* 将某CPU中断屏蔽控制寄存器的低6位设置为0(开中断2) */
#define
INT_I2_MASK 0x0040
wTmp
=
in
(INT_MASK);
out
(INT_MASK, wTmp
&~
INT_I2_MASK);
/**/
/* 将该位设置为1 */
wTmp
=
in
(INT_MASK);
out
(INT_MASK, wTmp
|
INT_I2_MASK);