STM32单片机一般使用C语言编程,单片机C语言与计算机C语言大部分相同,但由于编程对象不同,故两者有些地方略有区别。本节主要介绍单片机C语言的一些基础知识,在后面章节大量的编程实例中,有这些知识的实际应用,同时还会扩展介绍更多的C语言知识。
常量是指程序运行时其值不会变化的量。常量分为整型常量、浮点型常量(也称实型常量)、字符型常量和符号常量。
(1)十进制数:编程时直接写出,如0、18、-6。
(2)八进制数:编程时在数值前加“0”表示八进制数,如“012”为八进制数,相当于十进制数的“10”。
(3)十六进制数:编程时在数值前加“0x”表示十六进制数,如“0x0b”为十六进制数,相当于十进制数的“11”。
浮点型常量又称实数或浮点数。在C语言中可以用小数形式或指数形式来表示浮点型常量。
(1)小数形式表示:由数字和小数点组成的一种实数表示形式,如0.123、.123、123.、0.0等都是合法的浮点型常量。小数形式表示的浮点型常量必须有小数点。
(2)指数形式表示:这种形式类似数学中的指数形式。在数学中,浮点型常量可以用幂的形式来表示,如2.3026可以表示为0.23026×10^1、2.3026×10^0、23.026×10^-1等形式。在C语言中,则以“e”或“E”后跟一个整数来表示以“10”为底数的幂。2.3026可以表示为0.23026E1、2.3026e0、23.026e-1。C语言规定,字母e或E之前必须有数字,且e或E后面的指数必须为整数,如e3、5e3.6、.e、e等都是非法的指数形式。在字母e或E的前后及数字之间不得插入空格。
字符型常量是用单引号括起来的单个普通字符或转义字符。
(1)普通字符常量:用单引号括起来的普通字符,如'b'、'xyz'、'?'等。字符型常量在计算机中是以其代码(一般采用ASCII代码)储存的。
(2)转义字符常量:用单引号括起来的前面带反斜杠的字符,如'\n'、'\xhh'等,其含义是将反斜杠后面的字符转换成另外的含义。表1-3列出一些常用的转义字符及其含义。
表1-3 一些常用的转义字符及其含义
在C语言中,可以用一个标识符来表示一个常量,称为符号常量。在程序开头对符号常量进行定义后,在程序中可以直接调用符号常量,其值不会更改。符号常量在使用之前必须先定义,其一般形式为:
例如,在程序开头编写“#define PRICE 25”,就将PRICE定义为符号常量,在程序中,PRICE就代表25。
变量是指程序运行时其值可以改变的量。每个变量都有一个变量名,变量名必须以字母或下画线“_”开头。在使用变量前需要先声明,以便程序在存储区域为该变量留出一定的空间,比如在程序中编写“unsigned char num=3”,就声明了一个无符号字符型变量num,程序会在存储区域留出1字节的存储空间,将该空间命名(变量名)为num,且在该空间存储的数据(变量值)为3。
变量分为位变量、字符型变量、整型变量和浮点型变量。
(1)位变量(bit):占用的存储空间为1位,位变量的值为0或1。
(2)字符型变量(char):占用的存储空间为1字节(8位),无符号字符型变量的数值范围为0~255,有符号字符型变量的数值范围为-128~+127。
(3)整型变量:可分为短整型变量(int或short)和长整型变量(long),短整型变量的长度(即占用的存储空间)为2字节,长整型变量的长度为4字节。
(4)浮点型变量:可分为单精度浮点型变量(float)和双精度浮点型变量(double),单精度浮点型变量的长度(即占用的存储空间)为4字节,双精度浮点型变量的长度为8字节。由于浮点型变量会占用较多的空间,故单片机编程时尽量少用浮点型变量。
单片机C语言变量的类型、长度和取值范围见表1-4。
表1-4 单片机C语言变量的类型、长度和取值范围
单片机C语言的运算符可分为算术运算符、关系运算符、逻辑运算符、位运算符和复合赋值运算符。
单片机C语言的算术运算符见表1-5。在进行算术运算时,按“先乘除模,后加减,括号最优先”的原则进行,即乘、除、模(相除求余)运算优先级相同,加、减优先级相同且最低,括号优先级最高,在优先级相同时,运算按先后顺序进行。
表1-5 单片机C语言的算术运算符
在采用单片机C语言编程时,经常会用到加1符号“++”和减1符号“--”,这两个符号使用比较灵活。常见的用法如下。
y=x++(先将x赋给y,再将x加1);
y=x--(先将x赋给y,再将x减1);
y=++x(先将x加1,再将x赋给y);
y=--x(先将x减1,再将x赋给y);
x=x+1可写成x++或++x;
x=x-1可写成x--或--x;
%为模运算,即相除求余数运算,如9%5结果为4;
^为乘幂运算,如2^3表示2的3次方(2 3 ),2^2表示2的平方(2 2 )。
单片机C语言的关系运算符见表1-6。<、>、<=和>=运算优先级高且相同,==、!=运算优先级低且相同,如“a>b!=c”相当于“(a>b)!=c”。
表1-6 单片机C语言的关系运算符
用关系运算符将两个表达式(可以是算术表达式、关系表达式、逻辑表达式或字符表达式)连接起来的式子称为关系表达式,关系表达式的运算结果为一个逻辑值,即真(1)或假(0)。
例如,a=4、b=3、c=1,则
a>b的结果为真,表达式值为1;
b+c<a的结果为假,表达式值为0;
(a>b)==c的结果为真,表达式值为1,因为a>b的值为1,c值也为1;
d=a>b,d的值为1;
f=a>b>c,由于关系运算符的结合性为左结合,a>b的值为1,而1>c的值为0,所以f值为0。
单片机C语言的逻辑运算符见表1-7。&&、||为双目运算符,要求有两个运算对象,!为单目运算符,只要求有一个运算对象。&&、||运算优先级低且相同,!运算优先级高。
表1-7 单片机C语言的逻辑运算符
与关系表达式一样,逻辑表达式的运算结果也为一个逻辑值,即真(1)或假(0)。
例如,a=4、b=5,则
!a的结果为假,因为a=4为真(a值非0即为真),!a为假(0);
a||b的结果为真(1);
!a&&b的结果为假(0),因为!的优先级高于&&,故先运算!a的结果为0,而0&&b的结果也为0。
在进行算术、关系、逻辑和赋值混合运算时,其优先级从高到低依次为:!(非)→算术运算符→关系运算符→&&和||→=(赋值运算符)。
单片机C语言的位运算符见表1-8。位运算的对象必须是位型、整型或字符型数,不能为浮点型数。
表1-8 单片机C语言的位运算符
位运算举例见表1-9。
表1-9 位运算举例
复合赋值运算符就是在赋值运算符“=”前面加上其他运算符,单片机C语言常用的复合赋值运算符见表1-10。
表1-10 单片机C语言常用的复合赋值运算符
复合赋值运算中变量与表达式先按运算符运算,再将运算结果值赋给参与运算的变量。凡是双目运算(两个对象参与运算),都可以采用复合赋值运算符去简化表达。
复合赋值运算的一般形式为
变量 复合赋值运算符 表达式
例如,a+=28相当于a=a+28。
在单片机C语言中,会使用一些具有特定含义的字符串,称为“关键字”。这些关键字已被软件使用,编程时不能将其定义为常量、变量和函数的名称。单片机C语言关键字分为两大类:由ANSI(美国国家标准学会)标准定义的关键字和Keil单片机C语言编译器扩充的关键字。
由ANSI标准定义的关键字有char、double、enum、float、int、long、short、signed、struct、union、unsigned、void、break、case、continue、default、do、else、for、goto、if、return、switch、while、auto、extern、register、static、const、sizeof、typedef、volatile等。这些关键字可分为以下几类。
(1)数据类型关键字:用来定义变量、函数或其他数据结构的类型,如unsigned char、int等。
(2)控制语句关键字:在程序中起控制作用的语句,如while、for、if、case等。
(3)预处理关键字:表示预处理命令的关键字,如define、include等。
(4)存储类型关键字:表示存储类型的关键字,如static、auto、extern等。
(5)其他关键字:如const、sizeof等。
数组也常称作表格,是指具有相同数据类型的数据集合。在定义数组时,程序会将一段连续的存储单元分配给数组,存储单元的最低地址存放数组的第一个元素,最高地址存放数组的最后一个元素。
根据维数不同,数组可分为一维数组、二维数组和多维数组;根据数据类型不同,数组可分为字符型数组、整型数组、浮点型数组和指针型数组。在用单片机C语言编程时,最常用的是字符型一维数组和整型一维数组。
一维数组的一般定义形式如下。
方括号(又称中括号)中的下标也称常量表达式,表示数组中的元素个数。
一维数组定义举例如下。
以上定义了一个无符号整型数组,数组名为a,数组中存放5个元素,元素类型均为整型,由于每个整型数据占2字节,故该数组占用了10字节的存储空间,该数组中的第1~5个元素分别用a[0]~a[4]表示。
在定义数组时,也可同时指定数组中的各个元素(即数组赋值),比如:
在数组a中,a[0]=2,a[4]=512;在数组b中,b[0]=2,b[4]=512,b[5]~b[7]均未赋值,全部自动填0。
在定义数组时,要注意以下几点。
(1)数组名应与变量名一样,必须遵循标识符命名规则,在同一个程序中,数组名不能与变量名相同。
(2)数组中的每个元素的数据类型必须相同,并且与数组类型一致。
(3)数组名后面的下标表示数组的元素个数(又称数组长度),必须用方括号括起来,下标是一个整型值,可以是常数或符号常量,不能包含变量。
二维数组的一般定义形式如下。
下标1表示行数,下标2表示列数。
二维数组定义举例如下。
以上定义了一个无符号整型二维数组,数组名为a,数组为2行3列,共6个元素,这6个元素依次用a[0] [0]、a[0] [1]、a[0] [2]、a[1] [0]、a[1] [1]、a[1] [2]表示。
二维数组赋值有以下两种方法。
(1)按存储顺序赋值。例如:
(2)按行分段赋值。例如:
字符型数组用来存储字符型数据。字符型数组可以在定义时进行初始化赋值。例如:
以上定义了一个字符型数组,数组名为c,数组中存放4个字符型元素(占用4字节的存储空间),分别是A、B、C、D(实际上存放的是这4个字母的ASCII码,即0x41、0x42、0x43、0x44)。如果对全体元素赋值,数组的长度(下标)也可省略,即上述数组定义也可写成:
如果要在字符型数组中存放一个字符串“good”,可采用以下3种方法。
当定义二维字符数组存放多个字符串时,二维字符数组的下标1为字符串的个数,下标2为每个字符串的长度,下标1可以不写,下标2则必须写,并且其值应较最长字符串的字符数(空格也算一个字符)至少多出一个。例如:
例中“\n”是一种转义符号,其含义是换行,将当前位置移到下一行开头。
当程序定义了一个变量时,系统会根据变量的类型分配一定的存储空间,比如,为int型(整型)变量分配2字节的内存单元,为char型(字符型)变量分配1字节的内存单元。变量存放的地址称为变量的指针,有一种变量专门用来存放其他变量的地址(指针),这种变量称为指向变量的指针变量,简称指针变量,指针变量的值就是指针(地址)。
C语言要求所有的变量在使用前必须定义,以确定其类型。指针变量定义的一般形式为:
例如:
指针变量有“&”和“*”两个有关的运算符,&为取地址运算符,*为指针运算符或间接访问运算符,&a表示取变量a的地址,*a表示将变量a的值作为地址,取该地址单元的值。下面通过表1-11中的程序来说明“&”和“*”运算符的使用。
表1-11 “&”和“*”运算符的使用程序及说明
将多个不同类型的变量组合在一起构成的组合型变量称为结构体变量,简称结构体(又称结构),而数组则是由多个相同类型的变量构成的组合型变量。
结构体类型和变量的定义使用关键字“struct”,定义的方法有3种,见表1-12,“struct student”为结构体类型,zhangsan和lisi为具有struct student类型的结构体变量,即zhangsan和lisi结构体变量中都包含int num、char sex、float score三个成员。
表1-12 结构体类型和变量的定义
结构体中的成员类型除可以是int、char等外,也可以是结构体类型。在图1-16左边的程序中,struct data结构体类型含有int month、int day、int year三个成员,struct student结构体类型含有struct data结构体类型的成员birthday,这段程序相当于生成一个图1-16中右图所示的表格。
结构体类型用于定义结构体的成员,结构体变量用于存放结构体类型定义的成员的值。结构体变量可以赋值和运算。结构体变量引用要点及说明见表1-13。
图1-16 结构体类型中可以包含结构体类型的成员
表1-13 结构体变量引用要点及说明
结构体变量可以与其他类型变量一样赋初值。例如:
如果一个变量只有有限的几个值,可将这些值一一列举出来,在编程时该变量的值在这些值之间选择,这样就不会发生错误。枚举用于实现这种功能。
枚举的定义与结构体的定义一样,可采用3种方式,具体见表1-14。用关键字enum声明了一个名为week的枚举类型,该枚举类型包含7个常量,wk1、wk2是两个枚举变量,只能取week中的某个常量值。
表1-14 枚举的3种定义方式
枚举类型中的枚举常量为整数型常量,在没有指明枚举常量的值时,其值为顺序号;枚举变量只能取枚举类型中某个枚举常量值。以表1-14为例,若wk1=Sun,wk2=Wed,则wk1=0,wk2=3。
枚举常量可以在定义枚举类型时赋值。例如,若“enum week {Sun=7,Mon=1,Tue,Wed=3,Thu,Fri,Sat}wk1,wk2;”,则Sun、Mon、Tue、Wed、Thu、Fri、Sat的值分别是7、1、2、3、4、5、6。
不能直接将一个整数值赋给枚举变量,只能将枚举常量赋给枚举变量。例如,“wk1=2”是错误的,而“wk1=(enum week)2”是正确的。
在编程时,如果需要某段程序反复执行,可使用循环语句。单片机C语言的循环语句主要有3种:while语句、do…while语句和for语句。
while语句格式为“while(表达式){语句组;}”,编程时为了书写、阅读方便,一般按以下方式编写。
执行while语句时,先判断表达式是否为真(非0即为真)或表达式是否成立。若为真或表达式成立,则执行大括号(也称花括号)内的语句组(也称循环体);否则,不执行大括号内的语句组,直接跳出while语句,执行大括号之后的内容。
在使用while语句时,要注意以下几点。
(1)当while语句的大括号内只有一条语句时,可以省略大括号,但使用大括号可使程序更安全、可靠。
(2)若while语句的大括号内无任何语句(空语句),应在大括号内写上分号“;”,即“while(表达式){;}”,简写就是“while(表达式);”。
(3)如果while语句的表达式是递增或递减表达式,则while语句每执行一次,表达式的值就增1或减1。例如,“while(i++){语句组;}”。
(4)如果希望某语句组无限循环执行,可使用“while(1){语句组;}”。如果希望程序停在某处等待,待条件(即表达式)满足时往下执行,可使用“while(表达式);”。如果希望程序始终停在某处不往下执行,则可使用“while(1);”,即让while语句无限执行一条空语句。
do…while语句格式如下。
执行do…while语句时,先执行大括号内的语句组(也称循环体),然后用while判断表达式是否为真(非0即为真)或表达式是否成立。若为真或表达式成立,则执行大括号内的语句组,直到while表达式为0或不成立,直接跳出do…while语句,执行之后的内容。
do…while语句是先执行一次循环体,再判断表达式的真假以确定是否再次执行循环体;而while语句是先判断表达式的真假,以确定是否执行循环体。
for语句格式如下。
执行for语句时,先用初始化表达式(如i=0)给变量赋初值,然后判断条件表达式(如i<8)是否成立,不成立则跳出for语句,成立则执行大括号内的语句组。执行完语句组后再执行增量表达式(如i++),接着再次判断条件表达式是否成立,以确定是否再次执行大括号内的语句组,直到条件表达式不成立才跳出for语句。
单片机C语言常用的选择语句有if语句和switch…case语句。
if语句有3种形式:基本if语句、if…else…语句和if…else if…语句。
基本if语句格式如下。
执行if语句时,首先判断表达式是否为真(非0即为真)或表达式是否成立。若为真或表达式成立,则执行大括号内的语句组(执行完后跳出if语句);否则,不执行大括号内的语句组,直接跳出if语句,执行大括号之后的内容。
if…else…语句格式如下。
执行if…else…语句时,首先判断表达式是否为真(非0即为真)或表达式是否成立。若为真或表达式成立,则执行语句组1,否则执行语句组2,执行完语句组1或语句组2后跳出if…else…语句。
if…else if…语句格式如下。
执行if…else if…语句时,首先判断表达式1是否为真(非0即为真)或表达式是否成立,为真或表达式成立则执行语句组1;然后判断表达式2是否为真或表达式是否成立,为真或表达式2成立则执行语句组2……最后判断表达式 n 是否为真或表达式是否成立,为真或表达式 n 成立则执行语句组 n 。如果所有的表达式都不成立或为假,则跳出if…else if…语句。
switch…case语句格式如下。
执行switch…case语句时,首先计算表达式的值,然后按顺序逐个与各case后面的常量表达式的值进行比较,当与某个常量表达式的值相等时,则执行该常量表达式后面的语句组,再执行break而跳出switch…case语句。如果表达式与所有case后面的常量表达式的值都不相等,则执行default后面的语句组,并跳出switch…case语句。