5.2.1 寄存器和I/O口的使用
(1) I/O口和寄存器的定位
普通变量的定义和访问同标准C语言,在嵌入式C语言中我们主要要解决映像寄存器变量和某些特殊变量的定位问题,即把这些变量存放在RAM中指定的位置。
在08C语言中操作寄存器及I/O口时,通常预先在头文件中使用宏定义,其定义方法如下:
#define 寄存器名 (*(volatile unsigned char *)寄存器地址)
#define I/O口名 (*(volatile unsigned char *)I/O口地址)
#define 变量名 (*(volatile unsigned char *)变量地址)
例如:在GP32中,I/O口A的地址为0x0000,定时器1的状态寄存器地址为0x0020,那么在头文件中可以做如下宏定义:
#define PTA (*(volatile unsigned char *) 0x0000)
#define T1SC (*(volatile unsigned char *)0x0020)
这个定义看起来很复杂,其实它也可以分解成几个很简单的部分来看:
①( volatile unsigned char * )是C语言中的强制类型转换,它的作用是把0x0000这个纯粹的十六进制数转换成为一个地址指针,其中volatile并不是必要的,它只是告诉编译器,这个值与外界环境有关,不要对它优化,volatile的具体用法在进一步讨论中有所讲述。
②接下来在外面又加了一个*号,就表示0x0000内存单元中的内容。经过这个宏定义之后,PTA就被可以做为一个普通的变量来操作,所有出现PTA的地方编译的时候都被替换成(*(volatile unsigned char * )0x0000)
。
③外面一层括号是为了保证里面的操作不会因为运算符优先级或者其它不可预测的原因被改变而无法得到预期的结果。
这种定义方法适合所有的C编译器,可移植性好,但PTA并不是一个真正的变量,只是一个宏名,当你调试一个程序的时候,无法在调试窗口观察它的值。
(2) I/O与寄存器的操作
使用上面定义的I/O口或寄存器宏,可以方便对I/O置高低电平或读取I/O的状态,读写寄存器。
例如:
unsigned char sPortA = PTA; //将A口的状态赋给sPortA变量
PTA = 0xff; //将0xff赋给A口,A口将全为高电平
5.2.2 位操作方法
在嵌入式系统编程中,位操作使用频率很高。所谓“位操作”是对一个字节中的某一位的值置1或清0,同时不改变其它位的值。例如:用某个I/O口控制小灯亮暗、LCD片选信号等,都需要对I/O口寄存器的某一位置高或置低,并且此时不能影响口上其它位的位值。对于这种操作,用位操作的汇编指令很容易实现。如:
BSET #1 , PTA //置PTA.1为高电平
C语言中没有直接的位指令,它实现位操作的思想是:读出整个寄存器或内存的字节值,改变需要置1或清0的位,然后把整个值写回到寄存器或内存中。注意:在位操作时,一个字节的最低位是第0位,最高位是第7位。
(1) 用位运算符实现位操作
C语言中提供了6种基本的位运算符:按位与(&)、按位或(|)、按位取反(~)、按位异或(^)、左移(<<)及右移(>>),其基本用法如表5-2所示。
根据与、或和异或运算的特点,可以得出如下结论:
“按位与”运算的特点是参加运算的操作数的某位有0时,结果中的该位就是0,所以“按位与”运算可以实现位的清0操作。“按位或”运算的特点是参加运算的操作数的某位有1时,结果中的该位就是1,所以“按位与”运算可以实
现位的置1操作“按位异或”运算的特点是参加运算的某一个操作数的某位有1时,结果中的该位是另一个操作数的相应位取反,所以“按位异或”运算可以实现位的取反操作。
例如:A=0b11011011 B=0b11110111 C=0b00000100 D=0b00000110
则: A&B、A|C、A^D的按位运算如下:
A 11011011
B 11110111
&(按位与)
11010011
A 11011011
C 00000100
|(按位或)
11011111
A 11011011
D 00000110
^(按位异或)
11011101
在A&B时,A的第3位清0;A|C时,A的第2位置1;A^D时,A的第1和第2位均取反。这里的B、C、D称之为掩码,通过设定不同的B、C、D值可以改变A中不同的位。
掩码可以用16进制或二进制常数表示,也可以用移位运算符的表达式来表示,上面的B和C可用以下方式来声明:
#define B ~(1 << 3) 等价于 #define B 0b11110111
#define C 1 << 2 等价于 #define C 0b00000100
掩码以一个常量值的移位表达式来表示,更明确地指示了要测试的位所在的位置,并且编译器会单独用一个常量来替换这样的常量值表达式,这种形式不会产生任何额外的代码。
例如:改变寄存器device_register第2位值的方法如下:
#define STATUS_MASK 1<<2 //定义掩码
device_register= device_register | STATUS_MASK //将第2位置1
device_register= device_register & (~STATUS_MASK) //将第2位置0
device_register= device_register ^ STATUS_MASK //将第2位取反
以上三种操作方法可以简写为:
device_register |= STATUS_MASK //将第2位置1
device_register &= (~STATUS_MASK) //将第2位置0
device_register ^= STATUS_MASK //将第2位取反
(2) 测试位
按位“与”运算最常用于测试单个位(或位域)的值,在需要关注的“位”所在位置由单独一个1组成的特征码与操作数作“与”运算,当关注的“位”是1时,结果才是非0值,即逻辑真值。在实际书写时,特征码通常用十六进制、二进制数或移位表达式。例如:要测试第4位是否为1,有以下几种写法:
if ( (bits & 0x10)!=0)
if ( bits & 0x10)
if ( bits & 0b00010000)
if ( bits & (1<<4))
由于任意非0值都解释为真,所以条件中可以省略对0的冗余比较。
表5-3给出了08C位操作语句编译后所对应的指令,从表中可以看出编译器在编译时,已经做了优化,将这些C语句变成了08CPU中的位指令,达到和汇编相同的执行效率。

08C中除了上述的位操作的方法外,还可以综合共用体和位域等多种数据类型,很直观地实现位操作,关于这部分的讲述读者参考本章的进一步讨论部分。
5.2.3 中断处理
上一章已详述过中断的处理过程,C和汇编的这个过程是一致的,这里将不再展开。现在,需要关心的问题是如何在C工程中编写ISR(中断服务例程)。
首先,ISR与C中的正常子函数是有差别的:
第一,正常子函数被编译后的返回指令为RTS,而ISR被编译后的返回指令为RTI。
第二,正常子函数是通过调用方式进入的,而ISR是通过中断机制进入的。
第三,ISR的参数和返回类型总是void。
现在,就可以带着ISR的这些独有的特性编写它了:
①新建一个Vectors08.c,并加入工程中。
②定义中断向量表。
在HC08系列嵌入式Flash地址空间中,有一段是用来存储所有的中断矢量 (通常在最后的Flash页面上,参见第2章的存储器映像图),每两个字节存储的是一个中断处理函数的地址。而中断向量表在逻辑上组织了这些地址。
例如MC908GP32的中断向量表如下所示:
#pragma abs_address:0xffdc //中断向量表起始地址
void (* const _vectab[])(void) = {
isrDummy, //时基中断
isrDummy, //AD转换中断
isrDummy, //键盘中断
isrDummy, //SCI发送中断
isrDummy, //SCI接收中断
isrDummy, //SCI错误中断
isrDummy, //SPI发送中断
isrDummy, //SPI错误中断
isrDummy, //TIM2溢出中断
isrDummy, //TIM2通道1输入捕捉/输出比较中断
isrDummy, //TIM2通道0输入捕捉/输出比较中断
isrDummy, //TIM1溢出中断
isrDummy, //TIM1通道1输入捕捉/输出比较中断
isrDummy, //TIM1通道0输入捕捉/输出比较中断
isrDummy, //CGM的PLL锁相状态变化中断
isrDummy, //IRQ引脚中断
isrDummy //SWI指令中断
//RESET是特殊中断,其向量由开发环境直接设置(在本软件系统的crt08.o文件中)
}; #pragma end_abs_address
中断向量表是一个指针数组,内容是中断函数的地址。首先要定义该数组的地址,MC908GP32的中断矢量从0xffdc开始(不同的MCU中断矢量起始地址是不相同的,使用时需要查找相关的技术手册),要使用预编译指令将数组的首地址定义在0xffdc。预编译指令格式:“#pragma abs_address:地址”(可针对不同的芯片改变地址)中断数组格式:void (* const _vectab[])(void) = {中断处理函数名,中断处理函数名……}中断矢量表内容,是从中断矢量起始地址开始顺序增加,均与Flash的中断矢量地址相对应,如果某个中断不需要使用,要将在数组对应的项中填入isrDummy。isrDummy()是中断向量表中不需要使用的中断填入的函数,它是一个空函数。
#pragma interrupt_handler isrDummy
void isrDummy(void)
{
}
③定义ISR并在中断向量表中填入相应ISR的名称;
例如:
若工程中需要定时器1的溢出中断,可以定义如下:
#pragma interrupt_handler isrTimer1
void isrTimer1(void)
{
//存放中断处理语句------------------
… …
… …
//----------------------------------
}
在定义好中断处理程序之后,还要在中断矢量表的相应位置上填入该中断处理函数名“isrTimer1”。每个中断处理函数的前面都要加一个“#pragma interrupt_handler 函数名”,这是一个预编译指令,告诉编译器,下面的函数是中断处理函数,生成目标代码后,其返回指令将不使用RTS而使用RTI。
通过上述3个步骤,就可以定义好所需要的中断了。在实际编程中,可以直接从给定的C工程框架中得到“Vectors08.c”文件,该文件中只定义了一个空中断处理函数“isrDummy”,和由这个空函数名组成的中断向量表。用户只须定义所需的中断处理函数,并用该函数名代替向量表中相应位置上的“isrDummy”即可。
5.2.4 08C的常用库函数
08C提供一系列函数库供程序员使用,其中囊括了标准C所具有的大部分库函数和一些08C特有的函数,但是08C中的有些函数和标准C中的函数的功能不一样。这些函数的头文件位于安装目录的include目录下,库文件位于安装目录的lib目录下。下面对08C中一些常用的库函数做简要说明,更多的函数库说明参见附录D。
(1) 串口操作类函数
函数的声明如下:
①int printf(char *fmt,…)格式化输出
printf函数是一个标准库函数,它的函数原型在头文件“stdio.h”中。但作为一个特例,不要求在使用 printf 函数之前必须包含stdio.h文件。printf函数调用的一般形式为:printf(“格式控制字符串”,输出列表)其中格式控制字符串用于指定输出格式。格式控制串可由格式字符串和非格式字符串两种组成。格式字符串是以%开头的字符串,在%后面跟有各种格式字符,以说明输出数据的类型、形式、长度、小数位数等。如“%d”表示按十进制整型输出,“%ld”表示按十进制长整型输出,“%c”表示按字符型输出等。所有的输出都是发送至串口,而不是屏幕。
例如:
void main()
{
int a = 88,b = 89;
printf("%d %d\n", a, b);
printf("%c,%c\n", a, b);
printf("a = %d,b = %d", a, b);
}
②int putc(char c)
putc也是stdio.h中的函数,它只能发送一个字符。
例如:putc(‘a’); //串口发送字符a
③int puts(char *s)
puts比putc功能强一些,可以发送一个字符串,但无法像printf那样做格式化输出。
例如:puts(“hello”); //串口发送hello
④int getchar(void)
通过串行模块接收一个字符。
例如:receive=getchar();
(2) 内存操作类函数
memcpy
声明:void *memcpy(void *s1, void *s2, size_t n)
将以s2为起始地址的n个字节复制到以s1为起始地址的内存中。
例如:
//在0x50上存放着‘hello’5个字符
memcpy((void*)0x0120, (void *)0x50, 5);
//此时,hello已被复制到0x0120起始的内存单元中
5.2.5 08C语言与汇编语言的混合编程
在绝大多数场合采用C语言编程即可完成预期的目的,但是对一些特殊情况进行编程时要结合汇编语言。汇编语言具有直接和硬件打交道、执行代码的效率高等特点,可以做到C语言所不能做到的一些事情,例如:
①一个程序中的关键部分对执行速度有很高要求,实时性强。用汇编编程可以更有效地利用CPU的寄存器和指令集,因此,用其产生的代码比用编译器产生的代码运行更快。
②对特定硬件接口的访问。例如对I/O端口的访问,或者用于禁止、启用中断系统。因此,选用C语言编程时,还需要夹杂一些汇编程序,通过这种混合编程的方法将C语言和汇编语言的优点结合起来,这已经成为目前MCU开发最流行的编程方法。
(1) 在08C中使用汇编
目前大多数MCU系统,在C语言中使用汇编语言有两种情况:一种是汇编程序部分和C程序部分为不同的模块,或不同的文件,通常由C程序调用汇编程序模块的变量和函数(也可称为子程序或过程);另一种是嵌入式汇编,即在C语言程序中嵌入一段汇编语言程序。
①调用汇编指令构成的子程序
当汇编程序和C程序为不同模块时,程序一般可分为若干个C程序模块和汇编程序模块,C程序模块通常是程序的主体框架,而汇编程序模块通常由用C语言实现效率不高的函数组成,也可以是已经成熟的、没有必要再转化成C语言的汇编子程序。在这种混合编程技术中,关键是参数的传递和函数的返回值。它们必须有完整的约定,否则数据的交换就可能出错。
定义汇编子程序,定义格式如下:
_子程序名
代码
…
rts
这种使用方法要注意以下几点:
第一,在子程序名前加‘_’;
第二,汇编子程序只能放在*.s文件中,然后将该文件加入到工程中;
第三,在C代码中调用汇编子程序时可直接调用:子程序名();
第四,在子程序中不能使用映象寄存器的宏定义,只能用它们的直接地址。
第五,汇编子程序的编写时,对于使用过的寄存器需要进行保护。08C的编译器把寄存器封装在下层,不需要用户管理,如果汇编子程序没有保存这些寄存器,在返回时将造成不可预测的后果。
②嵌入汇编语句
对于嵌入式汇编,可以在C程序中使用一些关键字嵌入一些汇编程序,这种方法主要用于实现数学运算或中断处理,以便生成精练的代码,减少运行时间。当汇编函数不大,且内部没有复杂的跳转时,可以用嵌入式汇编实现。
使用关键字asm可以嵌入一条或多条汇编语句。例如:
asm(“SEI”); //单条指令
asm(“LDA $0000 \n” //多条指令
“AND #1\n”
“STA $0000\n”);
(2) 在汇编中使用C语言
在前面已经讲述了C代码中嵌入汇编程序的方法,实际上汇编中也可以调用C代码中的变量与子程序。使用C代码中定义的变量:在变量名前加’_’或’%’,例如unsigned char PortA;asm(“LDA %PortA”)或asm(“LDA _PortA”)
调用C代码中定义的函数:在函数名前加 ‘_’,例如:function1();asm(“JSR _function1”);
5.2.6 08C与标准C的其他一些不同之处
08C语言与标准C语言的语法基本相同,但是也存在一些不同之处,使用时应该注意。
(1) 部分数据类型不同
标准C中,double类型长度为8字节;而08C中,double类型长度为4字节。
(2) 地址分配不同
标准C语言适用于Intel CPU,08C适用于Freescale 08CPU,这两种CPU对多字节的数据分配地址时分别采用了“小端”和“大端”方式,在2.5.1节中有详细阐述。所以在标准C语言分配数据存放地址时,高字节的数据存放在高地址处,低字节的数据存放在低地址处,符合“高高低低”的分配原则。08C语言在分配数据存放地址时,高字节的数据存放在低地址处,低字节的数据存放在高地址处,符合“高低低高”的分配原则。
例:有共用体u1如下:
union abc{
char a;
int b;
long c;
}u1;
如果整型变量c的赋值为:
u1.c=0x12345678;
则u1.a和u1.b的值也被修改。在标准C语言中,它们的值为:
u1.a=0x78;
u1.b=0x5678;
在08C语言中,它们的值为:
u1.a=0x12;
u1.b=0x1234;


