提示:本文内容参考慕课课程:《ARM微控制器与嵌入式系统》
嵌入式开发中的C语言
前言
介绍了C语言的历史渊源,C语言中的关键字,数据类型和位操作等基本语法,从C语言视角看待嵌入式开发的总结和注意事项。
介绍了强制类型转换的意义和volatile关键字;如何使用指针访问寄存器,读写特定地址;编写中断服务子程序的注意事项;程序执行时的起始代码以及微控制器C语言编程的特点。
一、C语言是一种什么样的语言
1.C语言结构化,代码简洁,容易实现底层按位的操作。
2.高效可靠,可剪裁,容易在不同的平台和系统上移植。
3.编程人员可直接掌控硬件里的东西。
丹尼斯·里奇–C语言的开发者,最初开发了一款小游戏,当尝试把游戏移植到另一台型号的机器上时,发现操作系统移植困难,因为大量代码是用汇编写的,于是他们决定使用B language来编写新的操作系统,但是这个时候计算机已经进化到了以字节为单位来操作,所以原有的B语言不够用。
于是在从游戏开发到写操作系统,在做操作过程中对它的语法进行扩充,于是产生了C语言。
二、C语言基本语法
1.关键字和运算符
2.C语言中的数据类型
例如一下这个延迟函数
void delay(void)
{
unsigned char i,j;
for(i=0;i<10;i++)
for(j=0;j<60000;j++);
}
由于j为8位无符号整型,最大只能到255,所以该delay函数不能实现其预期的功能。
注意事项
1.选择合适的数据类型可以节省代码空间,提高运行速度。尽量使用满足要求的最小的变量类型。
2.避免使用浮点数和双精度,可以将浮点数在有限精度下通过移动小数点的位数全部转换成整数运算等。
3.每个数据类型的宽度在不同的编译环境下是一个可以设定的值,但要养成良好的代码习惯。
例如不同的编译环境下的声明:
/*8bit的MCU或CPU上*/
typedef unsigned char uint8_t;
typedef int uint16_t;
typedef unsigned long uint32_t;
/*32bit的MCU或CPU上*/
typedef unsigned char uint8_t;
typedef short uint16_t;
typedef unsigned int uint32_t;
那么声明后,就使用uint16_t来表示一个变量16位的,移植程序代码到新的构架时,只需根据当前所使用CPU或编译器修改typedef即可。
3.C语言中的位操作
例1:
/*将第4位和第5位置1*/
uchar temp = 0x34;
uchar temp |= 0b30;//0b00110000;
例2:为增强可读性,设置宏定义
/*将第1位和第2位置1*/
#define BIT3_MASK 0b00001000
#define BIT2_MASK 0b00000100
#define BIT1_MASK 0b00001010
uchar temp = 0x34;
uchar temp |= (BIT1_MASK |BIT2_MASK);
例3:
/*将第1位和第2位置0*/
#define BIT3_MASK 0b00001000
#define BIT2_MASK 0b00000100
#define BIT1_MASK 0b00001010
uchar temp = 0x34;
uchar temp |= ~(BIT1_MASK + BIT2_MASK);
例4:
/*左移/右移4位*/
uchar temp = 0x34;
uchar temp >>= 4; //temp=0x03;
uchar temp = 0x34;
uchar temp <<= 4; //temp=0x40;
例5:
/*取某个变量的高四位和低四位*/
#define H4_MASK 0b11110000
#define L4_MASK 0b00001111
uchar temp = 0x34;
uchar temp_l = temp & L4_MASK ;
uchar temp_h = (temp & H4_MASK) >> 4;
例6:
/*判断变量里的值*/
#define BIT3_MASK 0b00001000
#define BIT2_MASK 0b00000100
#define BIT1_MASK 0b00001010
uchar temp = 0x34;
/*如果第3位为1*/
if((temp & BIT3_MASK) != 0)
/*如果第2位为0*/
if((temp & BIT2_MASK) == 0)
三、如何使用C语言访问寄存器
1.寄存器本质
寄存器在嵌入式中就是映射在一个统一地址映射上不同物理地址的一些电路实现。
可按地址访问,每个寄存器8bit/32bit。
例如:GPIO_PDOR映射在一个地址上占了4个字节,32bit,每个bit对应芯片周围实际存在的物理引脚。某个bit设为1,相对应引脚输出时即为高电平。
2.强制类型转换的意义
例1:下方将16bit变量强制转化为8bit变量:
uint_var = 0x1234;
uchar_var = (uisigned char) uint_var;
此时,高8位被舍弃
uchar_var = 0x34;
经过这样的操作,实际上是对这个变量所存储的地址,按照目标变量的地址进行读写访问,取出所要的字长进行使用。
例2:
uchar_var = *((int*)(0x400FF0C0));
0x400FF0C0是一个地址,(int*)是一个指向整型变量的一个指针,(int*)(0x400FF0C0)即地址被强制转换成了一个指向int型变量的指针,按照int型变量读取4个字节来访问这个地址。
指针是一个指向地址的数据类型,指针的值是强制类型转换之前后面的这个数,所以(int*)(0x400FF0C0)这个指针的值就是400FF0C0。
*作用到一个指针上:是根据指针所指向的地址,读取地址上的数。
所以上句代码得到的结果就是0x400FF0C0地址的值被赋给了uchar_var。
例3:直接声明一个int*类型变量的指针,变量pGPIOD_PDOR的初值为地址 0x400FF0C0,即指针指向了这个地址。
int * pGPIOD_PDOR = 0x400FF0C0;
那么用这个指针加一个*号就可以访问这个地址了,读写这个寄存器了。
3.C语言读写特定地址
编程的本质就是给寄存器赋值。
在stm32f4xx.h中把每一组IO所使用的寄存器放在一起声明成了一个结构体,再把结构体强制声明成了一个指针型变量,再用这个结构体的指针型变量指向寄存器的起始地址。
实现使用变量名称就可以对对应地址赋值的功能。
#define PERIPH_BASE ((uint32_t)0x40000000)
#define AHB1PERIPH_BASE (PERIPH_BASE + 0x00020000)
#define GPIOA_BASE (AHB1PERIPH_BASE + 0x0000)
#define GPIOB_BASE (AHB1PERIPH_BASE + 0x0400)
#define GPIOC_BASE (AHB1PERIPH_BASE + 0x0800)
#define GPIOD_BASE (AHB1PERIPH_BASE + 0x0C00)
#define GPIOE_BASE (AHB1PERIPH_BASE + 0x1000)
#define GPIOF_BASE (AHB1PERIPH_BASE + 0x1400)
#define GPIOG_BASE (AHB1PERIPH_BASE + 0x1800)
#define GPIOH_BASE (AHB1PERIPH_BASE + 0x1C00)
#define GPIOI_BASE (AHB1PERIPH_BASE + 0x2000)
#define GPIOJ_BASE (AHB1PERIPH_BASE + 0x2400)
#define GPIOK_BASE (AHB1PERIPH_BASE + 0x2800)
#define GPIOA ((GPIO_TypeDef *) GPIOA_BASE)
#define GPIOB ((GPIO_TypeDef *) GPIOB_BASE)
#define GPIOC ((GPIO_TypeDef *) GPIOC_BASE)
#define GPIOD ((GPIO_TypeDef *) GPIOD_BASE)
#define GPIOE ((GPIO_TypeDef *) GPIOE_BASE)
#define GPIOF ((GPIO_TypeDef *) GPIOF_BASE)
#define GPIOG ((GPIO_TypeDef *) GPIOG_BASE)
#define GPIOH ((GPIO_TypeDef *) GPIOH_BASE)
#define GPIOI ((GPIO_TypeDef *) GPIOI_BASE)
#define GPIOJ ((GPIO_TypeDef *) GPIOJ_BASE)
#define GPIOK ((GPIO_TypeDef *) GPIOK_BASE)
typedef struct
{
__IO uint32_t MODER; /*!< GPIO port mode register */
__IO uint32_t OTYPER; /*!< GPIO port output type register */
__IO uint32_t OSPEEDR; /*!< GPIO port output speed register */
__IO uint32_t PUPDR; /*!< GPIO port pull-up/pull-down register */
__IO uint32_t IDR; /*!< GPIO port input data register */
__IO uint32_t ODR; /*!< GPIO port output data register */
__IO uint16_t BSRRL; /*!< GPIO port bit set/reset low register */
__IO uint16_t BSRRH; /*!< GPIO port bit set/reset high register*/
__IO uint32_t LCKR; /*!< GPIO port configuration lock register*/
__IO uint32_t AFR[2]; /*!< GPIO alternate function registers */
} GPIO_TypeDef;
遇到新出的芯片,我们就可以自己来写这个头文件代码,使用变量名称就可以对对应地址赋值。
那么在编写代码使用时:
mian(void)
{
GPIOD_PDDR |= 0XFF;
}
相当于
mian(void)
{
*((int*)(0x400FF0D4))|= 0XFF;
}
4.C语言如何知道这是一个寄存器
volatile(易失的,可变的)关键字:放在变量的声明之前,使编译器知道,这个变量如果在程序流程里没有被赋新值,但自身会改变的值。
因为IO对应外部引脚,外部电路导致引脚电平变化,进而会导致IO的寄存器值改变。
例1:两次读取+延迟是为了按键消抖,确定按键被按下
//检查按键有没有被按下
uint16_t GetKey(void)
{
volatile uint16_t temp1;
temp1 = GPIO_ReadInputDataBit(KEY1_GPIO_PORT,KEY1_PIN); //temp读取为0,表示按下
Delay(0xEFFFF);
temp1 = GPIO_ReadInputDataBit(KEY1_GPIO_PORT,KEY1_PIN);
if(temp1 == 0x00)
key = 1;
return key;
}
如果在uint16_t temp1前不加volatile,此时把编译器的优化选项打开,第二条读取就会被优化掉,程序运行会出问题,使用这个关键字之后,在优化程序时,对于这一类的优化会跳过去。
三、中断服务子程序
1.中断服务子程序是由CPU收到中断后自动去调用这个函数,不在某个函数里去调用。
2.中断服务子程序没有传递参数和返回值
3.ARM Cortex的构架里,中断返回的指令和普通C语言函数返回的指令是同一个指令。
所以在ARM构架里编写中断服务子程序,只需要写成void类型+void返回值
四、从微控制器到main
起始代码(Startup code)
单片机上电后,从中断向量表里读取他的中断向量和堆栈指针寄存器分别放在CPU内部的堆栈寄存器,把堆栈指向内存的最低地址,让PC指针指向程序代码的第一条指令。
PC指针指向程序代码的第一条指令不是main,是一段代码的初始地址,这段代码一般来说是编译器帮我们生成的。
在一个嵌入式计算机平台上完成5个步骤:
1.把堆栈指针寄存器初始到某个地址
2.所有内存的初始值为0
3.给全局变量赋值
4.如果使用C++,还有一个全局的构造函数
5.main()函数
这段初始代码如果没有集成开发环境,只有编译器的情况下可以自己写。
五、微控制器C语言编程的特点
1.有限RAM和ROM
2.针对具体的微控制器型号
3.没有操作系统支持
4.中断子程序的编写
5.精简的C语言函数库
6.stdin,stdout,stderr,使用这一类函数,首先会define一个底层函数,帮它完成指明标准输入输出设备是什么
7.同一地址映射,不可重定地址
8.不会从main函数中退出,所以里面要写死循环