单片机C语言基础


前言

  介绍一下单片机开发的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;

补充

  1. 布尔型变量
      _Bool类型长度为1,只能取值范围为0或1。将任意非零值赋值给_Bool类型,都会先转换为1,表示真。将零值赋值给_Bool类型,结果为0,表示假。如下所示变量MCU_IO1只能取0或者1:
//#include <stdbool.h>
_Bool MCU_IO1 @PC_ODR:3;	//同步信号端口
  1. 位变量
      在 C51 中,允许用户通过位类型符定义位变量。位类型符有两个:bit 和 sbit。可以定义两种位变量。bit 位类型符用于定义一般的可位处理位变量。sbit 位类型符用于定义在可位寻址字节或特殊功能寄存器中的位,定义时须指明其位地址,可以是位直接地址,可以是可位寻址变量带位号,也可以是特殊功能寄存器名带位号。格式如下:
bit 位变量名;
sbit 位变量名=位地址;
  1. 特殊功能寄存器变量
      在 C51 中,允许用户对特殊功能寄存器进行访问,访问时须通过 sfr 或sfr16 类型说明符进行定义,定义时须指明它们所对应的片内 RAM 单元的地址。sfr 用于对 51 单片机中单字节的特殊功能寄存器进行定义,sfr16 用于对双字节特殊功能寄存器进行定义。特殊功能寄存器名一般用大写字母表示。地址一般用直接地址形式。格式如下:
sfr 或 sfr16 特殊功能寄存器名=地址;

1.1.3 存储属性

  1. register
      使用 register 定义的变量称为寄存器变量。它定义的变量存放在 CPU 内部的寄存器中,处理速度快,但数目少。C51 编译器编译时能自动识别程序中使用频率最高的变量,并自动将其作为寄存器变量,用户可以无需专门声明。
  2. auto
      使用 auto 定义的变量称为自动变量,其作用范围在定义它的函数体或复合语句内部,当定义它的函数体或复合语句执行时,C51 才为该变量分配内存空间,结束时占用的内存空间释放。自动变量一般分配在内存的堆栈空间中。定义变量时,如果省略存储种类,则该变量默认为自动(auto)变量。
  3. static
      使用 static 定义的变量称为静态变量。它又分为内部静态变量和外部静态变量。在函数体内部定义的静态变量为内部静态变量,它在对应的函数体内有效,一直存在,但在函数体外不可见,这样不仅使变量在定义它的函数体外被保护,还可以实现当离开函数时值不被改变。外部静态变量上在函数外部定义的静态变量,它在程序中一直存在,但在定义的范围之外是不可见的。如在多文件或多模块处理中,外部静态变量只在文件内部或模块内部有效,内部静态变量是全局变量
  4. extern
      使用 extern 定义的变量称为外部变量。C语言中extern可以置于变量或者函数前,以表示变量或者函数的定义在别的文件中,提示编译器遇到此变量和函数时在其他模块中寻找其定义。这里面要注意,对于 extern 申明变量可以多次,但定义只有一次
  5. 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 运算符

  1. 赋值运算符

变量=表达式;

  1. 算术运算符
符号含义
+加或取正值运算符
-减或取负值运算符
*乘运算符
/除运算符
%取余运算符
  1. 关系运算符
Operator符号
等于==
不等于!=
大于>
小于<
大于或等于>=
小于或等于<=
  1. 逻辑运算符
Operator符号
&&
||

注意:&&左右都得成立为真;||左右有一个成立为真

  1. 位运算符
符号作用
&按位与
I按位或
^按位异或
~按位取反
<<左移
>>右移

注意:~是按位取反;!是逻辑取反。

  1. 其他运算符
    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 语句实现。

  1. if-else语句
if (表达式 1{语句 1}
else if (表达式 2) (语句 2;)
else if (表达式 3) (语句 3;)
……
else if (表达式 n-1) (语句 n-1;)
else {语句 n}
  1. switch语句
      使用break关键字跳出switch语句,若没有,则会顺次执行后面的语句,直到遇到 break 或结束。
switch (表达式)
{
	case 常量表达式 1{语句 1}breakcase 常量表达式 2{语句 2}break;
	……
	case 常量表达式 n:{语句 n;}breakdefault{语句 n+1}
}

1.3.2 循环结构

  在程序处理过程中,有时需要某一段程序重复执行多次,这时就需要循环结构来实现,循环结构就是能够使程序段重复执行的结构。循环结构可以用while和for语句,如下:
在这里插入图片描述

  1. 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;
  1. do-while
      do-while语句特点是先执行循环体,后判断条件,如
do
{
	语句;
} /*循环体*/
while(表达式);
  1. 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 指针数组与数组指针

  1. 指针数组
      指针数组本质上还是一个数组,只是数组的元素是指针形式,如下:

#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;
  1. 数组指针
      数组指针首先是一个指针,只不过这个指针是一个数组,可以理解为指向数组的指针
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 函数指针与指针函数

  1. 函数指针
      函数指针是指向函数的指针变量,即本质是一个指针变量。它允许通过变量名引用函数,而不是通过函数名。在C语言中,函数名实际上是函数代码的内存地址。因此,函数指针存储着代码段中相应的地址。使用函数指针可以方便地在代码中传递和使用函数作为参数,也可以在程序运行时动态地指定需要调用的函数。如:
int add(int x, int y) {
	return x+y;
}

int (*funcPtr)(int, int);	//定义函数指针

funcPtr = add;
  1. 指针函数
      指针函数是返回指针的函数,即本质是一个函数。它允许返回指向指针的指针,也可以返回指针数组。指针函数可以用于动态内存分配、数据结构遍历等场景。此外,它也可以浓缩代码思路,提高代码的可读性和可维护性。如:
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)VOHminVOLmaxVIHminVILmax
TTL电平2.40.42.00.8
CMOS电平4.990.013.51.5

  TTL和CMOS 的逻辑电平转换:CMOS 电平能驱动 TTL 电平,但 TTL 电平不能驱动 CMOS 电平,需加上拉电阻。

2.2 逻辑运算

  1. 进制转换
      在学逻辑运算前,首先要知道逻辑运算是对二进制的逻辑运算。而二进制的0he1就代表低电平和高电平,不过由于位数多了二进制太长,不方便阅读,又有了八进制、十进制和十六进制。有关具体进制转换这里不做介绍,一般在数电第一章就介绍,可以用电脑计算器直接计算,如下图所示:
    在这里插入图片描述

  2. 逻辑运算作用

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);
}

三、代码规则

  1. 如何提高代码的可移植性——就是把跟硬件相关的IO都用宏定义来实现
  2. 在建工程Define里调用的宏定义必须和头文件参数一致,或者修改与硬件一致
  3. 字长定义
    unit8_t= u8是unsigned char(无符号字节,其中unit8_t是标准定义)
    unit16_t=u16是unsigned short
    unit32_t=u32是unsigned int
  4. 命名
    宏定义全部大写字母
    函数、数组、变量首字母大写,如果全局变量、数组要用_
    局部变量小写字母
  • 3
    点赞
  • 28
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

别问,问就是全会

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值