对于MPLAB XC8编译器,本节并不准备全面讲述其所支持的PIC单片机C语言程序设计的基础知识,所列出的内容仅仅是在编写调试本书案例程序时需要参考的内容或需要引起注意的内容。
本书所有PIC单片机C语言程序设计的基本数据类型大部分与ANSI C相同,表1-1列出了常用的基本数据类型。
表1-1 C程序设计中常用的基本数据类型
设计单片机应用系统时,使用的多数都是无符号数,全书约定将0~255以内的无符号整数定义为INT8U,它相当于字节类型BYTE;0~65 535以内的无符号整数定义为INT16U,它相当于字类型WORD。
如果涉及有符号数处理,在定义时要注意使用 signed 类型(除非对符号位及数据位独立进行处理)。例如,温度控制程序中有正负温度,DS18B20温度传感器可处理的实际温度范围−55~125℃,如果忽略小数部分,则相关变量可定义为INT8类型(即signed char),其取值范围−128~127。
对于大量要将整数或浮点数显示在数码管上的案例,需要对待显示数据进行数位分解,例如:
又如:
要得到x的各个数位,可先将x乘以100,然后再分解各数位:
上面for循环中的循环条件一般会写成i>=0,但由于当i=0时,如果将i再减1,i将变为0xFF,这个无符号数仍被认为大于等于0,这样就不能保证5次循环了,因此要改写成i!=0xFF(或者i!=−1)。如果将i定义成INT8类型(signed char)而不是INT8U类型(unsigned char),使用i>=0时则可以得到正确结果。
使用<stdlib.h>提供的函数itoa或utoa可将有符号或无符号整数转换为字符串,将字符串中各字符编码减去0x30或‘0’也能分解出其各个数位。
十进数数位分解常大量用于数码管显示程序,显示时需要根据各数位到数码管段码表中提取对应的段码。
位操作在本书案例中也会大量出现,例如在LED流水灯、数码管位码控制、串行收/发、键盘扫描等大量案例中,各种位操作符都会频繁使用,例如位左移(<<)、右移(>>)、与(&)、或(|)、取反(~)、异或(^)等,这些位操作符都要熟练掌握和运用。
下面是有关位操作符的几个应用示例。
(1)例如,要PB端口RB7~RB0逐位循环轮流置1,可先定义变量i,并使之在7~0内循环取值,然后使PORTB=1<<i。在循环过程中,PORTB将分别输出10000000、01000000、00100000、…、00000001,如此往复。
(2)如果要RB7~RB0逐位循环轮流置0,可有PORTB=~(1<<i)。这类位操作在LED流水灯设计或集成式数码管位码扫描程序中都会用到。
(3)又如,已知RD7外接LED或蜂鸣器,要LED闪烁或蜂鸣器发声,可先定义:
#define LED_BLINK()RD7=~RD7或
#define BEEP()RD7=~RD7
然后在循环中反复调用LED_BLINK()或BEEP(),RD7引脚输出序列为…01010101…,如此即可实现所要求的输出。
(4)本书多个案例使用了字符液晶,向连接在PORTC端口的液晶屏发送显示数据时,需要先判断液晶是否忙,其忙标志在读取字节的最高位,因此又有类似语句:
由于“&”操作符的优先级低于“= = ”,因而要注意给“PORTC&0x80”加上括号提升其运算优先级。
(5)本节前面讨论了十进制数的数位分解及应用,如果有BCD码k=0x98,要分解出9和8,还可以有:
或使用位操作符分解出BCD码的两个数位,即:
如果要将BCD码0x98变为十进制的98,则进一步有(k>>4)*10+(k&0x0F)。本书多个案例使用了实时时钟芯片DS1302,其日期/时间格式就是BCD码,相应源代码中会见到类似的涉及位运算的表达式。
(6)有些应用需要将一组位变量放在同一个字节中,例如INT8U s,如果要判断其中第i位是否为1,可有:
对此还可以通过定义位域结构体(struct)加字节变量的联合体(union)来解决,例如:
如果要一次性设置标志信息为10101101,可以有:
如果要单独设置其中的某一位(如第3位),可以有:
如果要判断其中的第3位是否为1,可以有:
要注意对上述定义中的b0~b5,其命名并不是固定的,各位的名称可根据需要自行定义,例如头文件pic18f452.h中有定义:
它给PIC18F452单片机T2CON寄存器的每一位都赋予了独立的名称。
大量PIC单片机C语言程序均定义了数组,例如在数码管显示程序中,一般都会给出共阴(或共阳)数码管段码表SEG_CODE,它定义了7段数码管0~9(或者包括A~F)的段码,例如下面的共阳数码管段码表:
字符串在单片机程序设计中也会大量使用,特别是在有关液晶屏、点阵显示屏程序设计或串口通信程序设计中。下面是几个字符数组的定义:
这三种定义是相同的,它们都占用20字节空间,最后未明确赋值的字节全部为0x00(即'\0'),对于前两个字符数组,在字符液晶上显示时可用以下方法:
要注意:如果字符串长为16,而字符数组空间也只固定给出了16字节,那么上述方法中的后两种就不可靠了,因为最后一个字符后面不一定会自动分配有字符串结束标志'\0'。
字符串还可以这样定义:
这两种定义也是相同的,其串长均为16个字符,它们都占用17字节空间,因为字符串末尾被自动附加了结束标志字节0x00(即'\0')。
在已知串长时,上述三种字符串显示方法均可使用,在字符串长未知时可使用上述方法中的后两种,另外,上述显示方法还可以改写成:
除使用字符数组(字符串)外,还会使用到字符串数组,例如:
如果要在液晶上显示“Counter:
”这个字符串,可用以下语句实现:
涉及在英文字符液晶上显示数值时,需要将待显示的整型或浮点数据转换为字符串,这时可用此前提到的数位分解方法先分解出各位数字,然后加上0x30(即'0')得到对应数字的ASCII编码,这些ASCII编码可直接送字符液晶屏显示,如LM016L。
除使用上述方法外,还可以使用<stdlib.h>提供的将有符号或无符号整数转换为字符串的函数itoa和utoa,代码示例如下:
两个函数的第3个参数均用于设置基数(radix),这里所选择的是10(十进制)。经转换后,a、b所对应的字符串保存于字符数组sa、sb,这两个字符串可以直接送LCD显示。如果要显示在数码管上,除sa中的符号位以外,显示其他各位时,可通过执行sa[i-'0'或sb[i]-'0'将第i个字符转换为数字,然后根据数字0~9提取段码再送数码管显示。
另一种将数值转换为字符串的方法是使用sprintf函数,示例代码如下:
又如,假设已有语句:
执行sprintf(Buf+7,"%5.2f",x)时会使Buf中的字符串会变为:“Result:−123.45”,使用下面的语句也可以得到相同的结果:
显然,与itoa和utoa,包括未举例的ltoa、ultoa、ftoa等函数相比,使用sprintf的优点是它能根据要求输出“格式化”的字符串,而前者仅将数值直接转换为字符串,在实际应用中可根据具体需要选择。需要指出的是,虽然sprintf函数功能很强大,但编译时会生成较长的机器指令,在实时性要求较高的场合应尽量避免使用该函数。
XC编译器对sprintf函数中的“%f”支持得很好,如果选用的编译器不支持在sprintf函数中使用“%f”,则需要通过下面的代码进行处理。
以float x=123.45为例,相应的程序代码为:
显然,上述语句中将浮点数的整数与小数部分都转换为整数形式,然后再利用sprintf生成所需要的浮点数字符串形式。所得到的字符串结果为“Result:123.45”。
另外,还有多个与字符串/数字操作有关的函数,例如atoi、atol、atof、strtod、strtol、strtoul,其中部分函数将在本书有关“计算器”和“电子秤”的案例有中使用,这两个案例都涉及数字字符串输入及数据运算与显示。在程序设计中涉及数据输入/输出及运算与显示时可恰当使用这些函数。
指针是C语言的重要特色之一,对于语句:
其中,ptr指向数组a的第0字节,显示数组内容可使用下面的代码:
但是不能使用下面的代码:
因为数组名a虽然也是数组中第0字节的地址,但它不能在运行过程中改变,也正是因为数组名同样也是第0个元素的指针,因此某些函数定义中的形参为数组,调用函数时给出的实参常为指向同类型数据的指针,反之形参为指针,实参为数组名也很常见。
前面的字符串示例中也出现了指针应用,对它们同样要熟练掌握。
学习标准C语言程序设计时,读者已经很熟悉静态类型(static)和外部存储类型(extern)了,在设计PIC单片机C语言程序时,还要熟练掌握const、volatile、persistent这三个关键字。
(1)静态存储类型:static
静态变量具有固定的内存定位,如果在某个函数内部定义了静态变量及初值,仅仅在第一次调用该函数时,该静态变量被初始化,此后对该函数的调用都不会再初始化该变量,这类似于定义了一个专属于该函数的全局变量。
本书多个有关定时器的案例中,为了通过软件实现更长的延时,常常在定时器溢出中断函数内部定义static INT8U T_Count=0,通过在每次溢出中断发生时,累加T_Count变量来实现更长的延时。
(2)外部存储类型:extern
当所设计的项目包含多C程序文件,当前某C文件内需要引用定义在项目中其他C文件内的变量时,则该文件内该变量前需要添加extern关键字,以声明所使用的是外部变量。
(3)常数声明:const
ANSI C同样支持const关键字,const限定的数据在程序运行过程中不能修改。更重要的是,在设计PIC单片机C语言程序时,对于某些在运行过程中不会发生变化的数组,例如数码管段码表、图像或汉字点阵数据,通过添加const关键字,可将所有的数据内容保存到Flash程序空间,从而可大大节省对单片机RAM空间的占用。
(4)易变型变量声明:volatile
volatile标明一个单变量或数组的值是会随时变化的,即使源程序中没有任何专门对其赋值的语句。例如设为输入的I/O端口,其输入值随时可能因外部操作(或其他事件)影响而发生变化;在中断函数内被修改的变量相对于主程序流程来讲也是随时变化的;很多特殊功能寄存器的值也将随着指令的运行而动态变化。所有这些类型的变量必须将它们明确定义成“volatile”类型,该类型定义通知编译器的优化处理器,在优化处理过程中不能无故消除它们。
本书所有中断函数内部,凡所读/写的是全局变量或数组,而这些变量或数组又被主程序或其他函数引用时,一定要注意添加volatile关键字。
(5)非初始化变量声明:persistent
PIC单片机C语言程序会在最后生成的机器码中加入一小段初始化代码,将没有给出初值的相关变量或数组全部清零,这一操作在main函数之前被执行。但是,某些单片机应用系统中有的变量是不允许在程序复位后被清零的,为了实现这一要求,可使用persistent关键字声明此类变量不要在复位时自动清零。
用C语言开发PIC单片机程序时,流程控制语句if、switch、for、while、do while、goto都可能被频繁使用,下面仅对单片机程序中几个不同于常规的流程控制语句作简要说明。例如程序中可能会有如下代码(有关端口寄存器的设置可参阅后续内容):
这段代码可能会让初学者感到奇怪,PORTD设为0xF0(0B11110000),switch中的4个case怎么会出现匹配呢?
实际情况是:由于PORTD端口外接一组4×4键盘矩阵,程序运行过程中,当某按键按下时,从PORTD读取的值就会发生变化。可见,用C语言设计单片机程序时,某些寄存器的值不同于标准C语言程序中某些变量的值,它们的值会随时因某些事件(包括外部中断、输入捕获、计时溢出等)的影响而改变。
对于for循环,本书使用的编译器MPLAB XC8等允许将控制变量定义在循环内,例如:
在PIC单片机C语言程序的main函数内还会经常看到这样的代码块:
用标准C语言编写程序时,这段代码的循环体内通常会有退出循环的语句存在,但是编写PIC单片机C语言程序时,几乎所有主程序中类似语句内都找不到退出循环的语句,这是因为单片机应用系统不同于普通的软件系统,它一旦开始运行就会一直持续下去,对外部的操作或状态变化做出实时响应或处理,除非系统关闭或出现其他故障。
类似的,还有很多案例中主程序最后有一行代码:
这显然是两个死循环,在出现该语句的案例中,外部事件的处理工作必定被放在中断服务程序(ISR,Interrupt Service Routine)内,主程序完成若干初始化工作并使能中断以后就不再执行其他操作,它一直等待在while或for循环所在的语句行,一旦中断发生将立即保护现场,进入中断服务程序进行处理,完成后恢复现场并返回,直到有后续中断继续发生。
设计PIC单片机C语言程序时,涉及大量表达式的编写,对于多种类型运行符组合的表达式,要注意它们的优先级,表1-2给出了ANSI C的运算符优先级表格,阅读全书提供的源程序及进行编写实践时可作参考。例如,为判断从PORTC端口读取的低3位是否全1,可有语句:
如果将上述语句误写成:
在编译时不会提示任何错误,因为该语句的语法是正确的,但显然未实现所要求的目标。因为由运算符优先级表格可知,位运行符“&”的优先级低于关系运行符“= = ”的优先级,故需要将“PORTC&0x07”单独添加“()”提升其优先级。
表1-2 ANSI C语言运算符优先级
涉及英文或数字等字符(字符串)显示及串口收发等程序设计时,需要熟悉标准的ASCII码表,表1-3列出了十六进制ASCII编码为0x00~0x7F(即0~127)的字符ASCII码表,由该表可知:
(1)数字字符‘0’~‘9’的ASCII编码为0x30~0x39,与数字0~9的差值为0x30,两者相互转换时可±0x30,或者直接±‘0’;
(2)英文字符‘A’~‘Z’、‘a’~‘z’的编码为0x41~0x5A、0x61~0x7A,大小写转换时可±0x20;
(3)字符串的结束标识符为‘\0’,即表中编码为0x00(NUL或NULL)的非打印字符;
(4)常用的空格字符(SP/SPACE)的ASCII编码为0x20;
(5)在向串口发送字符串时,常以回车/换行符(CR/LF)为结束标志,回车符、换行符的ASCII码分别为0x0D、0x0A;
(6)表中前两行所列出的其他特殊控制字符(Control Characters)虽然多数已废弃,但有部分控制字符名称仍应用于某些现代产品设计。
例如,某种射频读卡器模块(RFID模块)所设计的链路层协议以STX、ETX(文本起始符/结束符,Start of Text/End of Text)作为数据帧的起止标识符,不过该厂商将STX与ETX的编码定义为0x82与0x83。另外,表中的应答与非应答(ACK、NAK或称NACK)字符,其概念仍应用于全书所有有关I 2 C器件的程序设计,所不同的是I 2 C协议中的应答与非应答仅仅是一个脉冲位(0/1),而不再是一个字节编码。
表1-3 ASCII码表(0x00~0x7F)
在实际应用过程中,如果需要临时查询某些字符编码,包括中文字符编码,可先用记事本输入字符内容,然后用UltraEdit打开,切换到十六进制模式查看编码。
图1-8显示了用记事本(NotePad)输入的字符(包括中文字符)以及在超级编辑器UltraEdit中查看的十六进制字符编码的效果,UltraEdit不仅显示了所输入英文数字等字符的ASCII编码(小于0x80),而且显示了所输入汉字的内码,例如“单片机C语言程序设计”的编码为“B5 A5C6 AC BB FA D3 EF D1 D4 B3 CC D0 F2 C9 E8 BC C6”,除了英文半角字符“C”的编码0x43以外,其他编码全部大于等于0xA0,这些编码(汉字内码)每两字节用于表示一个汉字,图中已用方框标出。
由于编译程序不支持在源代码中直接使用中文字符串,在处理中文字符串时必须使用编码的方式提供。本节提到的这种获取汉字编码(指汉字内码)的方法将在后续相关案例中使用。
图1-8 用NotePad与UltraEdit获取字符编码