目录
前言
介绍一下单片机开发的C语言使用,C语言知识博大精深,基础还是需要系统学一下的,这里由于篇幅有限
一、C语言基础
1.1 基本类型
1.1.1 基本数据类型
数据类型就是一个数据的存储类型,体现为占据空间的大小。单片机的 C 语言中常用的基本数据类型如下:
注意:当数据类型在运算时不同会进行类型转换,优先级如下:
bit→char→int→long→float→signed→unsigned
也就是说,当 char 型与 int 型进行运算时,先自动对 char 型扩展为 int 型,然后与 int 型进行运算,运算结果为 int 型。
补充:C51扩充数据类型主要如下(在文件“reg51.h”中):
1.1.2 常量与变量
C语言运算量分为常量和变量,常量其实和变量没有多大区别,有名字,占据存储空间,可以是任何的基本类型,但只有一点不同,常量的值不允许变更,通常用const关键字定义常量,如下所示:
//定义一个 float 型变量,以";"为语句终止符,表示一条语句结束
float a;
a=123.1234567;
//用const定义一个int整形变量,在定义时初始化,否则之后不能赋值
const int n=5;
补充:
- 布尔型变量
_Bool类型长度为1,只能取值范围为0或1。将任意非零值赋值给_Bool类型,都会先转换为1,表示真。将零值赋值给_Bool类型,结果为0,表示假。如下所示变量MCU_IO1只能取0或者1:
//#include <stdbool.h>
_Bool MCU_IO1 @PC_ODR:3; //同步信号端口
- 位变量
在 C51 中,允许用户通过位类型符定义位变量。位类型符有两个:bit 和 sbit。可以定义两种位变量。bit 位类型符用于定义一般的可位处理位变量。sbit 位类型符用于定义在可位寻址字节或特殊功能寄存器中的位,定义时须指明其位地址,可以是位直接地址,可以是可位寻址变量带位号,也可以是特殊功能寄存器名带位号。格式如下:
bit 位变量名;
sbit 位变量名=位地址;
- 特殊功能寄存器变量
在 C51 中,允许用户对特殊功能寄存器进行访问,访问时须通过 sfr 或sfr16 类型说明符进行定义,定义时须指明它们所对应的片内 RAM 单元的地址。sfr 用于对 51 单片机中单字节的特殊功能寄存器进行定义,sfr16 用于对双字节特殊功能寄存器进行定义。特殊功能寄存器名一般用大写字母表示。地址一般用直接地址形式。格式如下:
sfr 或 sfr16 特殊功能寄存器名=地址;
1.1.3 存储属性
- register
使用 register 定义的变量称为寄存器变量。它定义的变量存放在 CPU 内部的寄存器中,处理速度快,但数目少。C51 编译器编译时能自动识别程序中使用频率最高的变量,并自动将其作为寄存器变量,用户可以无需专门声明。 - auto
使用 auto 定义的变量称为自动变量,其作用范围在定义它的函数体或复合语句内部,当定义它的函数体或复合语句执行时,C51 才为该变量分配内存空间,结束时占用的内存空间释放。自动变量一般分配在内存的堆栈空间中。定义变量时,如果省略存储种类,则该变量默认为自动(auto)变量。 - static
使用 static 定义的变量称为静态变量。它又分为内部静态变量和外部静态变量。在函数体内部定义的静态变量为内部静态变量,它在对应的函数体内有效,一直存在,但在函数体外不可见,这样不仅使变量在定义它的函数体外被保护,还可以实现当离开函数时值不被改变。外部静态变量上在函数外部定义的静态变量,它在程序中一直存在,但在定义的范围之外是不可见的。如在多文件或多模块处理中,外部静态变量只在文件内部或模块内部有效,内部静态变量是全局变量 - extern
使用 extern 定义的变量称为外部变量。C语言中extern可以置于变量或者函数前,以表示变量或者函数的定义在别的文件中,提示编译器遇到此变量和函数时在其他模块中寻找其定义。这里面要注意,对于 extern 申明变量可以多次,但定义只有一次。 - typedef针对标识符
typedef用于为现有类型创建一个新的名字,或称为类型别名,用来简化变量的定义。typedef 在 MDK 用得最多的就是定义结构体的类型别名和枚举类型了。
//申明结构体
typedef struct SYS_PARAMETER //用typedef
{
float pos_duty_limit;
float neg_duty_limit;
}SYS_PARAMETER;
struct CTL_FLAG1 //不用typedef
{
unsigned DIR_PREV:1;
unsigned DIR_NOW:1;
unsigned DIR_SWITCH:1;
};
struct CTL_FLAG1 m_Ctl_Flag1; //不用typedef使用
SYS_PARAMETER m_Sys_Param; //用typedef申明的结构体不需要加struct
补充:存储器类型
存储器类型是用于指明变量所处的单片机的存储器区域情况。存储器类型与存储种类完全不同。C51编译器能识别的存储器类型有以下几种,见表所示:
存储器类型 | 描述 |
---|---|
data | 直接寻址的片内 RAM 低 128B,访问速度快 |
bdata | 片内 RAM 的可位寻址区(20H~2FH),允许字节和位混合访问 |
idata | 间接寻址访问的片内 RAM,允许访问全部片内 RAM |
pdata | 用 Ri 间接访问的片外 RAM 的低 256B |
xdata | 用 DPTR 间接访问的片外 RAM,允许访问全部 64k 片外 RAM |
code | 程序存储器 ROM 64k 空间 |
1.2 运算符
- 赋值运算符
变量=表达式;
- 算术运算符
符号 | 含义 |
---|---|
+ | 加或取正值运算符 |
- | 减或取负值运算符 |
* | 乘运算符 |
/ | 除运算符 |
% | 取余运算符 |
- 关系运算符
Operator | 符号 |
---|---|
等于 | == |
不等于 | != |
大于 | > |
小于 | < |
大于或等于 | >= |
小于或等于 | <= |
- 逻辑运算符
Operator | 符号 |
---|---|
与 | && |
或 | || |
非 | ! |
注意:&&左右都得成立为真;||左右有一个成立为真
- 位运算符
符号 | 作用 |
---|---|
& | 按位与 |
I | 按位或 |
^ | 按位异或 |
~ | 按位取反 |
<< | 左移 |
>> | 右移 |
注意:~是按位取反;!是逻辑取反。
- 其他运算符
1)复合赋值运算符
C语言中支持在赋值运算符“=”的前面加上其它运算符,组成复合赋值运算符。如“+=”,a+=b等价于a=a+b
2)逗号运算符
在C语言中,逗号“,”是一个特殊的运算符,可以用它将两个或两个以上的表达式连接起来,称为逗号表达式。
3)条件运算符
条件运算符“?:”是C语言中唯一的一个三目运算符,它要求有三个运算对象,用它可以将三个表达式连接在一起构成一个条件表达式。其功能是先计算逻辑表达式的值,当逻辑表达式的值为真(非 0 值)时,将计算的表达式 1 的值作为整个条件表达式的值;当逻辑表达式的值为假(0 值)时,将计算的表达式 2 的值作为整个条件表达式的值。条件表达式的一般格式为:
逻辑表达式?表达式 1:表达式 2
1.3 基本结构
顺序结构是最基本、最简单的结构,在这种结构中,程序由低地址到高地址依次执行,即程序是从上执行到下。除了顺序结构,还有选择结构和循环结构
1.3.1 选择结构
选择结构可使程序根据不同的情况,选择执行不同的分支,在选择结构中,程序先都对一个条件进行判断。当条件成立,即条件语句为“真”时,执行一个分支,当条件不成立时,即条件语句为“假”时,执行另一个分支。如图:
在C中,实现选择结构的语句为 if/else,if/else if 语句。另外还支持多分支结构,多分支结构既可以通过 if 和 else if 语句嵌套实现,可用 swith/case 语句实现。
- if-else语句
if (表达式 1) {语句 1;}
else if (表达式 2) (语句 2;)
else if (表达式 3) (语句 3;)
……
else if (表达式 n-1) (语句 n-1;)
else {语句 n}
- switch语句
使用break关键字跳出switch语句,若没有,则会顺次执行后面的语句,直到遇到 break 或结束。
switch (表达式)
{
case 常量表达式 1:{语句 1;}break;
case 常量表达式 2:{语句 2;}break;
……
case 常量表达式 n:{语句 n;}break;
default:{语句 n+1;}
}
1.3.2 循环结构
在程序处理过程中,有时需要某一段程序重复执行多次,这时就需要循环结构来实现,循环结构就是能够使程序段重复执行的结构。循环结构可以用while和for语句,如下:
- while
while语句特点是先判断条件,后执行循环体。循环语句可以使用continue结束本次循环进入下一次循环。也可以使用break直接跳出循环体,执行循环体后面的语句。如:
int b=0;
while(a<10)
{
b++;
if(b==8)break; //等b到8时跳出循环,执行c=0
if(a==3) continue; //a为3直接跳出本次循环,所以a一直为3
a++;
}
c=0;
- do-while
do-while语句特点是先执行循环体,后判断条件,如
do
{
语句;
} /*循环体*/
while(表达式);
- for循环
for循环执行流程:
①表达式1->表达式2->语句->表达式3
②表达式2->语句->表达式3
③一直循环到表达式2不成立停止循环
for(表达式 1;表达式 2;表达式 3)
{
语句;
} /*循环体*/
for(int a= 0;a<10;a++)
{
b+=a;
}
1.4 函数
C语言函数可以将一个常用的功能封装成API,给以后需要时来调用,函数定义如下:
函数类型 函数名(形式参数表) [修饰符]
{
局部变量定义
函数体
}
函数类型是函数返回类型,要配合return,如:
int max(int a,int b)
{
int z;
z = x>y ? x:y;
return(z);
}
int c = max(4,5);
如果函数类型是void即空类型,可以不用加return返回。修饰符在C51中常用的就是interrupt m 修饰符,m 的取值为 0~31代表中断向量表,如
void Int0() interrupt 0 //外部中断 0 的中断函数
{
delay(1000); //延时消抖
if(k3==0)
{
led=~led;
}
}
注意:当在一个C文件调用另一个C文件定义的函数时,除了要加头文件,还需要用extern来申明一下函数,函数原型一般形式如下:
extern 函数类型 函数名(形式参数表);
1.5 数组
数组在内存里是以堆栈形式存在的,它可以截取一段空间来存取固定数据类型的数据。一维数组定义形式如下:
数据类型说明符 数组名[数组长度][={初值,初值……}]
数组的数据类型可以是数值也可以是字符,如下:
int a[2]={1,2};
char string1[10]={"a"}; //字符串以“\0”作为结束符
注意:除了一维数组还有高维数组,比如二维数组一般用来进行矩阵运算,这里就不展开了
1.6 指针
1.6.1 指针的定义
指针是C语言中的一个十分重要的概念,在C中的数据类型中专门有一种指针类型。指针为变量的访问提供了另一种方式,变量的指针就是该变量的地址,还可以定义一个专门指向某个变量的地址的指针变量(指针即地址)。C中提供了两个专门的指针运算符:
符号 | 作用 |
---|---|
* | 指针运算符 |
& | 取地址运算符 |
指针运算符“*”放在指针变量前面,通过它实现访问以指针变量的内容为地址所指向的存储单元。取地址运算符“&”放在变量的前面,通过它取得变量的地址,变量的地址通常送给指针变量。例如:
int b=1,c; //假设b的地址是2000H
int *a; //定义一个指针变量a
*a = &b; //取b的地址,a的地址变成2000H
c = *a; //则c为1
1.6.2 指针数组与数组指针
- 指针数组
指针数组本质上还是一个数组,只是数组的元素是指针形式,如下:
#includeint main(void)
{
char **p, i;
char *strings[] ={"one", "two", "three"};
p = strings; //strings是地址的地址,所以要定义**p
for(i = 0; i < 3; i++)
printf("%s\n", *(p++)); //这里*(p++)是取出存储在数组中的每一个字符串的地址return 0;
- 数组指针
数组指针首先是一个指针,只不过这个指针是一个数组,可以理解为指向数组的指针
int main() {
int a[3] = {1,2,3};
int (*p)[3] = a; //p就是数组指针
printf("%p,%p,%p,%p,%d,%d\n",a,&a,p,*p,**p,*p[0]);
return 0;
}
1.6.3 函数指针与指针函数
- 函数指针
函数指针是指向函数的指针变量,即本质是一个指针变量。它允许通过变量名引用函数,而不是通过函数名。在C语言中,函数名实际上是函数代码的内存地址。因此,函数指针存储着代码段中相应的地址。使用函数指针可以方便地在代码中传递和使用函数作为参数,也可以在程序运行时动态地指定需要调用的函数。如:
int add(int x, int y) {
return x+y;
}
int (*funcPtr)(int, int); //定义函数指针
funcPtr = add;
- 指针函数
指针函数是返回指针的函数,即本质是一个函数。它允许返回指向指针的指针,也可以返回指针数组。指针函数可以用于动态内存分配、数据结构遍历等场景。此外,它也可以浓缩代码思路,提高代码的可读性和可维护性。如:
int* getArray() {
static int arr[3] = {1, 2, 3};
return arr;
}
int* arrPtr = getArray();
for(int i=0; i<3; i++) {
printf("%d ", arrPtr[i]);
}
注意:函数、指针和数组可以结合一起使用,如下:
/* 函数指针数组 */
pctr pfunclist_m1[6] =
{
&m1_uhwl, &m1_vhul, &m1_vhwl,
&m1_whvl, &m1_uhvl, &m1_whul
};
void m1_uhvl(void)
{
g_atimx_handle.Instance->CCR1 = g_bldc_motor1.pwm_duty;
g_atimx_handle.Instance->CCR2 = 0;
g_atimx_handle.Instance->CCR3 = 0;
HAL_GPIO_WritePin(M1_LOW_SIDE_V_PORT,M1_LOW_SIDE_V_PIN,GPIO_PIN_SET);
HAL_GPIO_WritePin(M1_LOW_SIDE_U_PORT,M1_LOW_SIDE_U_PIN,GPIO_PIN_RESET);
HAL_GPIO_WritePin(M1_LOW_SIDE_W_PORT,M1_LOW_SIDE_W_PIN,GPIO_PIN_RESET);
}
1.7 构造类型
1.7.1 结构体
结构体是很实用的数据类型,与数组的区别是,结构体可以定义不同变量类型的成员。声明结构体类型:
struct 结构体名{
成员列表;
}变量名列表;
结合指针,可以申明一个结构体指针,结构体指针成员变量引用方法是通过“->”符号实现
struct U_TYPE {
Int BaudRate
Int WordLength;
}usart1,usart2;
struct U_TYPE *Usart3; //定义结构体指针变量 usart1;
usart1.BaudRate; //引用usart1的成员BaudRate
Usart3->BaudRate; //访问Usart3结构体指针指向的结构体的成员变量BaudRate
结构体就是将多个变量组合为一个有机的整体,这样方便对数据的管理。如串口设置:
typedef struct
{
uint32_t USART_BaudRate;
uint16_t USART_WordLength;
uint16_t USART_StopBits;
uint16_t USART_Parity;
uint16_t USART_Mode;
uint16_t USART_HardwareFlowControl;
} USART_InitTypeDef;
于是,我们在初始化串口的时候入口参数就可以是 USART_InitTypeDef 类型的变量或者指针变量了,MDK 中是这样做的:
void USART_Init(USART_TypeDef* USARTx, USART_InitTypeDef* USART_InitStruct);
1.7.2 共用体
结构体的定义是需要使用关键字 struct,而共用体则是需要另一个关键字 union来进行定义。共用体只能在定义的时候声明变量,无法在后续的程序中再次声明变量的问题,可以通过 typedef 关键字来自定义数据类型的名称。共用体的定义方式:
typedef union{
int a;
char b;
double c;
}TybeA;
注意:共用体变量所占用的内存中仍然只是存储一个数据,空间为最大的数据所占用的空间大小。所以引用不同的类型的时候,会将内存中的数据进行覆盖重写。作用有两个:
①初始化时,可以直接对最长的变量初始化,其他都跟着初始化了
②当一个位置需要存储两种不同类型的数据的时候,可以单独存储(即只用其中一个类型)
③共用体是小端模式
1.7.3 枚举类型
如果变量的值确定,则可以定义枚举类型
enum weekday //用enum申明枚举类型
{
sun,
mon,
tue,
wed,
thu,
fri,
sat
};
注意:
①枚举型是一个集合,集合中的元素(枚举成员)是一些命名的整型常量,元素之间用逗号,隔开。
②第一个枚举成员的默认值为整型的0,后续枚举成员的值在前一个成员上加1。在当前值没有赋值的情况下,枚举类型的当前值总是前一个值+1.
二、单片机C语言补充
2.1 电平特性
单片机是一种数字集成芯片,数字电路中只有两种电平:高电平和低电平。了解电平特性后,可以使用万用表来判断引脚是否工作。常用的逻辑电平有很多,比如 TTL、CMOS、LVTTL、RS-232、RS-485 等。5V TTL 和 5V CMOS 是通用的逻辑电平。3.3V 及以下的逻辑电平被称为低电压逻辑电平,常用的为 LVTTL 电平。低电压逻辑电平还有 2.5V 和 1.8V 两种。RS-232 和 RS-485 是串口的接口标准,RS-232 是单端输入/输出。RS-485 是差分输入/输出。
假设I/O为输入/输出,H/L为高/低电平,TTL 电路和 CMOS 电路的逻辑电平关系如下:
电平(V) | VOHmin | VOLmax | VIHmin | VILmax |
---|---|---|---|---|
TTL电平 | 2.4 | 0.4 | 2.0 | 0.8 |
CMOS电平 | 4.99 | 0.01 | 3.5 | 1.5 |
TTL和CMOS 的逻辑电平转换:CMOS 电平能驱动 TTL 电平,但 TTL 电平不能驱动 CMOS 电平,需加上拉电阻。
2.2 逻辑运算
-
进制转换
在学逻辑运算前,首先要知道逻辑运算是对二进制的逻辑运算。而二进制的0he1就代表低电平和高电平,不过由于位数多了二进制太长,不方便阅读,又有了八进制、十进制和十六进制。有关具体进制转换这里不做介绍,一般在数电第一章就介绍,可以用电脑计算器直接计算,如下图所示:
-
逻辑运算作用
1)位与:操作对象&=屏蔽字
可实现目标字段清0,即屏蔽字为0的清零
2)位或:操作对象|=屏蔽字
可实现目标字段置位,即屏蔽字为1的置1
3)异或:操作对象^=屏蔽字
可实现目标字段取反,即屏蔽字为1的取反
2.3 @符号
@是IAR编译器里指定变量存储地址的一个符号
_Bool MCU_IO1 @PC_ODR:3; //设信号端口MCU_IO1为PC3
如设置绝对地址
u32 testsram[250000] __attribute__(at(0X68000000));//MDK绝对地址
u32 testsram[250000] @0X68000000;//IAR设置绝对地址
2.4 位段
struct test
{
int a:1;
};
不是给a赋初值,在内存中存取数据的最小单位一般是字节,但有时存储一个数据不必用一个字节。这是一种位域的结构体,这个结构里a占用的是一个字节中的1位,所以这里的a取值只能是0和1,因为它们都是用1位来表示的。类似bit和_Bool
2.5 volatile关键字
用volatile申明的变量可以防止被编译器优化而省略
2.6 define宏定义
define 是 C 语言中的预处理命令,它用于宏定义,可以提高源代码的可读性,为编程提供方便。宏定义在编译时,会将目标字符直接替换。常见的格式:
#define 标识符 字符串
注意:宏定义#define用\换行
2.7 ifdef条件编译
单片机程序开发过程中,经常会遇到一种情况,当满足某条件时对一组语句进行编译,而当条件不满足时则编译另一组语句。条件编译命令最常见的形式为:
#ifdef 标识符
程序段 1
#else
程序段 2
#endif
它的作用是:当标识符已经被定义过(一般是用#define 命令定义),则对程序段 1 进行编译,否则编译程序段 2。 其中#else 部分也可以没有
2.8 设置字节对齐
__align(4) u8 SDIO_DATA_BUFFER[512];//MDK设置4字节对齐
#pragma pack(push,4) //IAR指定字节对齐
__no_init u8 SDIO_DATA_BUFFER[512];
#pragma pack(pop)
注意:在STM32F407的ADC配置里,这两个等效
void ADC_DeInit(void)
{
/* Enable all ADCs reset state */
RCC_APB2PeriphResetCmd(RCC_APB2Periph_ADC, ENABLE);
/* Release all ADCs from reset state */
RCC_APB2PeriphResetCmd(RCC_APB2Periph_ADC, DISABLE);
}
三、代码规则
- 如何提高代码的可移植性——就是把跟硬件相关的IO都用宏定义来实现
- 在建工程Define里调用的宏定义必须和头文件参数一致,或者修改与硬件一致
- 字长定义
unit8_t= u8是unsigned char(无符号字节,其中unit8_t是标准定义)
unit16_t=u16是unsigned short
unit32_t=u32是unsigned int - 命名
宏定义全部大写字母
函数、数组、变量首字母大写,如果全局变量、数组要用_
局部变量小写字母