文档库 最新最全的文档下载
当前位置:文档库 › 嵌入式系统的C语言

嵌入式系统的C语言

嵌入式系统的C语言

译自《C for Embedded Systems》讲稿

刘永重译

一、C语言基础

1、什么是C?

…C?程序语言最初是由Dennis Ritchie在1971年为UNIX系统开发并实现的。C的一个最大优点是与任何特定的硬件或系统无关。这使得一个用户写的程序不作任何修改就能运行在几乎所有的机器上。

C通常被称为中级计算机语言,因为它将高级语言的要素与汇编语言的功能结合了在一起。

2、为什么用C?

C非常灵活,而且可随心所欲。这种自由赋予C非常强大的功能,有经验的用户可以掌握;C是一个相对小的语言,但是它经久耐用;C有时被认为是“高级汇编语言”;低级(位操作)编程也容易实现;松类型(不象其它高级语言);C是结构化编程语言;C允许你创建你脑海中已有的任何任务。

C保留了程序员知道正在做的事情的基本体系;它只需要他们明白地表达其意图。

3、为什么不用C?文化的问题…

当考虑转到C语言时,我们会遇到一些共同的问题:

产生大而低效的代码;标准IO程序的雍余代码(printf,scanf,strcpy等);存贮器定位的使用:malloc(),alloc()…;堆栈的使用,在C中不很直接;在RAM和ROM中数据的声明;难于写中断服务程序。

4、8位微控制器的ANSI C

对于嵌入式系统,纯粹的ANSI C并不方便,因为:

嵌入式系统与硬件打交道。ANSI C 提供的在固定存贮空间用寄存器寻址的工具非常拙劣;几乎所有的嵌入式系统使用中断;ANSI C有各种类型的促进规则,对8位机来说绝对是性能杀手;一些微控制器结构没有硬件支持C堆栈;很多微控制器有多个存贮空间。

5、打破一些C范例

当在低端的8位微控制器上用C语言,应想法使代码变小。这意味着打破一些编程规则:开/关全局中断;使用GOTO语句;全局标号;全局寄存器段;指针支持。

6、嵌入式与桌面编程

嵌入式编程环境的主要特点:

有限的RAM;有限的ROM;有限的栈空间;面向硬件编程;严格的定时(ISR,任务,…);很多不同种类的指针(far/near/rom/uni/paged/…);特殊关键字/标识符(@,interrupt,tiny,…)。

7、汇编与C

编译器只是一个能干的优秀汇编程序员。

写能够转换为高效率汇编代码的好的C代码,比手工写高效率的汇编代码容易得多。

C是终极解决办法,但其本身并未终结。

8、为什么改用C?

有很多原因用C语言而不用汇编:

C使我们提高效益;用C写的代码更可靠;C代码更容易升级和扩展;不同平台之间更容易迁移;代码容易维护;文档、书籍、第三方库和程序都可得到。

9、C代码结构

如下图所示,一个C程序基本由以下部分组成:

预处理命令、类型定义、函数原型(声明传给函数的函数类型和变量)、变量和函数。

一个程序必须有一个main()函数,每个命令行必须用分号(;)结束。

10、C函数

一个函数的结构如下:

类型函数名(参数)

{

本地变量

C语句

}

11、C关键字

1)数据类型

char short signed unsigned int float long double

2)修饰符

const static volatile restrict

3)标识符

struct union void enum

4)选择体

if else switch case default

5)存贮指定

register typedef auto extern

6)循环体

do while for

7)跳转

goto continue break return

8)功能指定

inline

9)预处理指示

#include #define #undef #line #error #pragma

10)条件编译

# if # ifdef # ifndef # elif # else # endif

12、C操作符

1)基本表达式和后缀操作符

( ) 子表达式和函数调用[ ] 数组下标-> 结构指针.结构成员

++ 增加(后缀) -- 减少(后缀)

2)一元操作符

! 逻辑非~ 取补++ 增加(前缀) --减少(后缀) -一元减+ 一元加(类型) 类型强制* 间接指针& 取地址sizeof 大小

3)赋值符

= 相等赋值+= 加等于-= 减等于*= 乘等于/= 除等于%= 求余等于<<= 左移位等于>>=右移位等于&=按位与等于^= 按位异或等于

|= 按位或等于

4)位操作

& 位与^ 位异或| 位或< < 位左移>> 位右移

5)数学运算

* 乘/ 除%求余+ 加-减

6)关系运算

< 小于<= 小于或等于> 大于>=大于或等于==相等测试!=不等测试7)逻辑运算

&& 逻辑与|| 逻辑或

8)条件运算

?:条件测试

9)序列

二、嵌入式编程

1、变量

变量的类型决定其可带值的类型。也就是说,为变量选择一个类型与我们使用这个变量的方法直接相关。我们将学习C的基本类型、怎样写常量和声明这些变量。

1.1 选择一个类型

“值集合”是有限的。C的整数类型不能代表所有整数;它的浮点类型也不能代表所有浮点数。当声明一个变量并为它选择一个类型,你应紧记你需要的值和操作。

1.2 C的基本数据类型

ANSI标准并没为本地类型规定尺寸大小,但CodeWarrior规定了。C只有一些基本数据类型:

所有数量类型(除了char)缺省都是有符号的,例如:…int? = …signed int?。

注意:INT型的大小依赖于不同的机器。

1.3 CodeWarrior数据类型

例如,按ALT+F7打开工程的通用设置,选择“Compiler for HC08”面板并点击类型尺寸。这个窗口向你显示CodeWarrior 编译器使用的标准类型设置。

所有基本类型可以改变,尽管这可能不是个好主意。

1.4 数据类型的事实

代码大小和执行时间的最大节约可通过为变量选择最合适的数据类型得到。

8位微控制器内部的数据的长度是8位(一字节),然而C首选的数据类型是…int…。

8位机处理8位数据类型比16位类型效率更高。

“int“和大数据类型只有当所描述的数据的大小需要时才使用。

当效率非常重要时,双精度和浮点操作效率低,应当避免。

1.5 选择数据类型

8位微控制器选择数据类型有3个规则:

1)用最可能小的类型来完成工作,大小越小占用存贮空间越少;

2)若可能,用无符号类型;

3)在表达式内声明以将数据类型减到最少需要。

使用类型定义得到固定大小:

1)根据编译器和系统而改变;2)移植到不同的机器代码不变;3)当值需要固定位时使用。

Main函数内定义了三种不同类型的变量定义了一个数据类型的完整集合

只写了意义最少的位;寄存器用于

此目的。

每个变量剩余位用:clr ,x清除。

变量在堆栈中有一个

地址。

主函数外定义了三个不同

类型的变量。

编译器为作用的变量保留了内存。本例中VarA是唯一使用的变量。

所有声明的全局变量

均被使用。

在这种情况下,编译器为所有变量保留了内存。

根据变量大小的不同,每个加操

作用不同的方法完成。

变量声明的内存区,每个变量有不同的大小

(1、2和4字节)

2、存贮类修饰符

以下关键字用于声明变量,以指定特定需要或内存中变量存贮的相关条件。

static volatile const

这三个关键字,一起让我们不仅可写出好的代码,而且可写出紧凑的代码。

2.1 静态变量

使用静态有二个主要功能:

第一个最常用的用法是定义一个变量,在函数连续调用期间,变量不会消失。

第二个使用静态的用法是限制变量的范围。在模块级定义时,能被整个模块中所有函数访问,不能被其它函数访问。这非常重要,因为当严格限制全局变量众所周知的问题时,它让我们获得所有全局变量执行性能的好处。因此,如果我们有必须被一些函数频繁访问的数据结构,就应当将函数放入同一模块中,并将结构声明为静态。这样所有函数能够访问而不必通过一个访问函数的上层,同时与数据结构无关的代码禁止访问它。这一技术是一种变通方法,立即可访问变量在小的机器上实质上取得了足够的性能。

声明模块级静态变量(与将其设为全局相反)能取得一些其他潜在的益处。静态变量由于定义,只能被一组特定的函数访问。因此,编译器和连接器能够明智地选择变量在存贮空间的放置。例如,对于静态变量,编译器/连接器也许选择将一个模块中所有静态变量放在连续的区域,这样增加了各种优化机会,例如用简单的增加或减少代替重载。相反,全局变量在存贮空间的位置通常计划于优化编译器的哈稀算法,这排除了可能的优化。

须着重指出, 这些变量不会存贮在堆栈中,因为它们必须保存其值。

下面给出一个静态变量怎样工作的例子:

#include //包含文件FILE2.c

//中的函数

void main (void){

//第一次进入MyFunction之前,myVar=0。

MyFunction(); //在FILE2.c中

//第二次进入MyFunction之前,myVar=1。

MyFunction(); //在FILE2.c中

}

FILE2.c

void MyFunction (void){ //FILE2.C中定义

//MyFunction函数

static char myVar = 0; //本地变量

//声明为static

myVar = myVar + 1; //尽管myVar是本地变量,但它保持了自己的值。

}

2.2 静态函数

一个静态函数只能被其所在模块中的其它函数调用。使用静态函数是结构化编程的好习惯。你也许惊讶地知道静态函数能产生小/快的代码。这是可能的,因为编译器在编译时确切地知道什么函数能调用一个给定的静态函数。因此,函数的相关内存区域能被调整,以致使用调用的一个短版本或跳转指令。潜在的改进甚至更好,编译器足够聪明地用跳转代替调用。

2.3 关键字“static”的使用

在函数体声明静态的变量,在函数调用期间保持其质;

在模块内声明静态的变量,(但在函数体之外)能被模块内所有函数访问;

在模块内声明静态的函数,只能被模块内其它函数调用。

对于嵌入式系统:封装持续生存的数据(包装);模块化编码(数据隐藏);在每个模块中隐藏内部处理。

2.4 可变(volatile)变量

可变变量是其值在正常程序流程以外可能改变的变量。在嵌入式系统中,这种情况通过两种主要途径发生:

通过一个中断服务程序,或作为硬件动作的结果。例如,通过一个串口接收到一个字符,结果串口状态寄存器更新,这完全在程序流程之外发生。很多程序员知道编译器不会试图优化一个volatile寄存器,而宁可每次重载它。

在嵌入式设备中,将所有外设寄存器声明为volatile是一个好习惯。

许多编译器供应商经常炫耀他们的代码优化,它们通常非常好,它们有些根本不明显,但能极大地减少周期和内存。但有时我们不想编译器聪明和优化一个部份,因为我们确实需要代码那样作。

我们怎样才能达到呢?那么,访问定义为volatile的变量从不会被编译器优化。

让我们分析一个例子,看看编译器是怎样处理一个volatile和一个非volatile变量…

volatile unsigned char PORTA @0x00;

volatile unsigned char SCS1 @0x16;

void main(void){

PORTA = 0x05; /* PORTA = 00000101 */

PORTA = 0x05; /* PORTA = 00000101 */

SCS1;

value = 10;

}

未使用V olatile关键字,编译器将其编译为:

MOV #5,PORTA

LDA #10

STA @value

使用V olatile关键字后,编译器将其编译为:

MOV #5,PORTA

MOV #5,PORTA

LDA SCS1

LDX #10

STX @value

这段代码实际上不做任何事,但它很好地表达了优化怎样强烈地影响程序的结果。在main()中连续两次使用语句:PORTA=5,这没有意义,但让我们假设这是正确开发程序所必须的…在这两个语句之后,明显地有一条无意义语句“SCS1;”。让我们看当不使用volatile变量会发生什么…

我们得到了优化过的汇编代码。重复的语句Port A = 5消失了只剩下一句“move #5 to Port A”。语句“SCS1;”似乎什么都不做,因此聪明的编译器将它消去了。最后,将10加载到累加器并作为值存贮。

使用volatile关键字声明PORTA 和SCS1,得到的汇编代码没有优化,连续两次在Port A写入数值5,然后将SCS1加载到累加器。最后由于累加器被使用,于是用X寄存器存贮数值10。

好了,连续两次用数值5写PortA,假设这是需要这样做,但是加载SCS1到累加器有一个很有意义的值。这是串行通信接口SCI需要的,读SCS1寄存器目的是清除任何未决的标志。无意义的语句“SCS1;”被翻译为读寄存器的的汇编语句,这将清除SCI中未决的标志。

前面说过,在嵌入式设备中将所有外设寄存器声明为volatile是一个好习惯。在分开的头文件中定义所有外设的名字,能使所写代码更友好并使迁移简化。下面这个例子用volatile变量声明所有寄存器,这样做较妥当,因为任何这些寄存器能在任何时候在程序流程之外被修改。

/* MC68HC908GP20/32 Official Peripheral Register Names */

volatile unsigned char PORTA @0x0000; /* Ports and data direction */

volatile unsigned char PORTB @0x0001;

volatile unsigned char PORTC @0x0002;

volatile unsigned char PORTD @0x0003;

volatile unsigned char PORTE @0x0008;

volatile unsigned char DDRA @0x0004; /* Data Direction Registers */

volatile unsigned char DDRB @0x0005;

volatile unsigned char DDRC @0x0006;

volatile unsigned char DDRD @0x0007;

volatile unsigned char DDRE @0x000C;

volatile unsigned char PTAPUE @0x000D; /* Port pull-up enables */

volatile unsigned char PTCPUE @0x000E;

2.5 Const变量

关键字“const”,C语言中命名最差的关键字,并不表示恒量,而是代表“只读”。在嵌入式系统中,有很大的不同,这一会应会明白。

Const声明可用于任何变量,它告诉编译器将其存贮在ROM代码。编译器保留了那个位置程序存贮器地址。由于位于ROM中,其值不能改变。

由于它作为常量工作,必须赋一初值。如:const double PI = 3.14159265;

Const 变量与明显的常数相对,很多原文要求用const变量代替明显的常数。例如:

用const unsigned char channels = 8;代替#define CHANNELS 8 。

本方法的基本原理是在调试器内部,你能检查一个const变量,然而一个明显的常数不可访问。不幸的是,在很多8位机上你将为这一好处付出极大的代价。这两个主要代价是:? 一些编译器在RAM中创建一个真实的变量来支持cost变量,这是一个极大的惩罚。

? 一些编译器如CodeWarrior,知道变量为const,将把变量存贮在ROM中。无论怎样,变量仍作为变量处理和访问,典型地用某些变址寻址(16位)的方式。与直接寻址(8位)方式相比,这种方法通常很慢。

Const的用法:

const unsigned short a;

unsigned short const a;

const unsigned short *a;

unsigned short * const a;

2.6 Const volatile 变量

现在讨论一个深奥的问题,一个变量既能是常量,又能是可变量吗?如果是这样,这意味什么,怎样使用?答案是“能”。

这个修饰符应该用于能出乎意料地改变的任何存贮器位置,因此需要volatile限定语,由于const该变量是只读的。

最明显的例子是硬件状态寄存器,象SCI状态寄存器SCS1。这个寄存器包含信号状态标志,如发送空、发送完成、接收满以及其它。这是一个可变寄存器由于这些标志的改变依赖于串行通信的状态,这也是只读,由于标志不能被程序直接改写,它们只对模块的状态作出响应。这个状态寄存器最佳声明方法是:

const volatile unsigned char SCS1 @0x0016

3、资源映射

3.1 访问固定内存位置

嵌入式系统通常的特点是需要编程者访问一个指定的存贮器位置。

练习:在某个项目中需要将绝对地址0xFFA处整型变量的值设为0xAA55(编译器为纯粹的ANSI编译器)。完成这个任务的代码是:

Int * ptr;

ptr = (int *)0x2FFA;

*ptr = 0xAA55;

3.2 怎样访问I/O寄存器

在嵌入式领域,设备如微控制器有片上资源,应当被管理和访问。很多I/O和控制寄存器位于直接页,它们应如此声明,因此在可能时编译器能直接寻址。它们有定位的地址,但问题是它们不是存储器,那么怎样访问这些I/O寄存器呢?这是一个非常重要的问题,答案比你想的简单或者复杂。

一个普照通而有用的形式是使用如下的#define指示:

这构成了I/O寄存器,这种情况下,Port A为地址0x0000处字符型变量。#define实际做的是每次发现PortA时放置一个构件。也就是说在代码中写:PortA = 0x3F,实际做的就是告诉编译器0x0000是一个volatile-unsigned-char类型的指针,它的内容等于0x3F。

糊涂吗?有点…让我们看一些其它选择:

这样做的一个容易的方法是在变量声明中使用符号“@”,创建一个语句读作:在地址0x0000处创建一个volatile-unsigned-char型的变量PortA。

这是一个编译器特定的语法,它可读性高,但失去了兼容性。无论什么时候我们决定使用一个不同的编译器去编译该代码,也许会发现@不被识别。CodeWarrior和Cosmic包含了这个特殊语法。

CPU中的寄存器没有内存映射;指令集包含允许它们自修改的子集;C不提供直接访问寄存器的工具;C编译器允许在C代码中使用汇编指令,如:

1)_asm AssemblyInstuction;

2)asm (AssemblyInstruction);

3)asm {

----

----

}

修改CPU 中CCR的I

位的内容。

3.3 位域

在嵌入系统中,在一个给定的地址,一次能访问和修改一位或几位。

*位结构:

效率随编译器的不同而改变;跨编译器和目标不能移植。 *位类型:

不能移植(标准C 语言中没有);如 当使用时可提高代码的效率。 使用汇编指示,I 位被修改。

可移植,适当的效率;经常优化为位操作。

如果定义一个结构,但所有变量重叠在同一内存的开始位置,你应该使用联合体。联合体允许引用在联合体中定义的以任何形式描述的数据字节。联合体在内存中的尺寸大小为联合体中所列的最大类型的大小。点操作符用于选择需要的成员。

打开文件:Lab2-BitFields.mcp

联合体是一个变量,不同的

时间持有对象不同的类型

和大小,编译器跟踪变量的

大小和决定需要。

3.4 数组

C 允许程序员用几种不同方法存取数组的内容。

Unsigned char Array[]={0xAA,0xBB,0xCC};

依赖于执行,选择最适合于该应用的需要,将产生快而小的代码。数组访问方法: 1)硬编码:

Array[0]=12*UNIT_VOLTS;

编译时决定地址,执行速度快。

2)变址增加

Array[index++]=12*UNIT_VOLTS;

快速,比硬编码灵活。 3)数组指针

*(ArrayPtr++)=12*UNIT_VOLTS;

执行速度快,可读性差,可和循环一起使用。 如下图所示:

打开文件:Lab3-Arrays.mcp

联合体提供操作单一存贮区不同类型数据的方法,程序中没有嵌入任何依赖于机器的信息。

只有PS 位被修改

一条指令所写

硬编码

增量变址

数组指针

每种访问类型都

有各自的优点,使

用不同的寄存器

完成不同的操作。

函数指针与数据指针一样有用处,原因如下: 当你想要一个额外级别的间接时;

当你想用同一段代码依环境的不同调用不同的函数。

下面的代码定义了一个指向函数的指针,带了一个整型参数并返回一整数:

int (*function)(int);

(*function)周围的圆括符是必须的,因为定义中的优先关系。没有它们,我们则定义了一个函数返回一个整型指针。

例如:

下面举一个HC08QL 的例子:

函数指针初始化

函数指针现在指向一个不同的函数

SLIC模块仅有一个中断;用户必须读SLIC中断向量寄存器(SLCSV)来核实中断源。可能的解决方案:

switch 语句;嵌套的if语句;函数指针。

打开文件:Lab4-Pointers.mcp

定义了一个函

数数组。

每次调用一个不同的

函数

调试(1):

调试(2):

调试(3):

每次将执行一个不同的函

数。

相关文档