嵌入式C语言基础

阿里云国内75折 回扣 微信号:monov8
阿里云国际,腾讯云国际,低至75折。AWS 93折 免费开户实名账号 代冲值 优惠多多 微信号:monov8 飞机:@monov6

嵌入式开发中常用的C语言基础语法并不多因此对于想学习或者进入嵌入式领域的同学可以通过快速学习常用的C语言基础进而着手尝试开发小项目在开发过程中不断扩展知识库。

嵌入式C语言基础

1、const用法

C语言中使用const修饰变量功能是对变量声明为只读特性并保护变量值以防被修改。

修饰变量/数组

  • 当用const修饰定义变量时必须对变量进行初始化
  • const修饰变量可以起到节约空间的效果通常编译器并不给普通const只读变量分配空间而是将它们保存在符号列表中无需读写内存操作程序执行效率也会提高。

修饰指针

  • 常量指针常指针可以理解为常量的指针即这个是指针但指向的是个常量const限定了指针指向空间的值不可修改
  • 指针常量本质是一个常量。指针常量的值是指针这个值因为是常量所以不能被赋值。const限定了指针不可修改
int i = 5;
int k = 10;
int const *p1 = &i;   // 常量指针
int * const p2 = &k;  // 指针常量

对于指针p1 const修饰的是p1即p1指向的空间的值不可改变例如p1 = 20;就是错误的用法但是p1的值是可以改变的例如p1 = &k则没有任何问题。

对于指针p2 const修饰的是p2即指针本身p2不可更改而指针指向空间的值是可以改变的例如*p2= 15;是没有问题的而p2 = &i;则是错误的用法。

2、static用法

常见的局部变量和全局变量的特点可简单概况为

  • 局部变量会在每次声明的时候被重新初始化如果在声明的时候有初始化赋值不具有记忆能力其作用范围仅在某个块作用域可见
  • 全局变量只会被初始化一次之后会在程序的某个地方被修改其作用范围可以是当前的整个源文件或者工程。

static关键词在嵌入式开发中使用频率较高可以在一定程度上弥补局部变量和全局变量的局限性。

静态局部变量

满足局部变量的作用范围但是拥有记忆能力不会在每次生命周期内都初始化一次这个作用可来实现计数功能例如在下面这个函数中变量num就是静态局部变量在第一次进入cnt函数的时候被声明然后执行自加操作num的值就等于1当第二次进入cnt函数的时候 num不会被重新初始化变成0而是保持1再自增则变成了2以此类推 其作用域仍然是cnt这个函数体内。

void cnt(void)
{
    static int num = 0;
    num++;
}

静态全局变量

将全局变量的作用域缩减到了仅当前源文件可见其它文件不可见静态全局变量的优势是增强了程序的安全性和健壮性。

static修饰函数

让函数仅在本文件可见 其它文件无法对其进行调用例如在example1.c文件里面进行了如下定义

static void gt_fun(void)
{
    ...  
}

那么gt_fun这个函数就只能在example1.c中被调用在example2.c中就无法调用这个函数。而如果不使用static来修饰这个函数那么只需要在example2.c中使用extern关键字写下语句extern void gt_fun(void)即可调用gt_fun这个函数。

3、extern关键词

在C语言中 extern关键字用于指明函数或变量定义在其它文件中提示编译器遇到此函数或者变量的时候到其它模块去寻找其定义这样被extern声明的函数或变量就可以被本模块或其它模块使用。因而 extern关键字修饰的函数或者变量是一个声明而不是定义例如

/* example.c */
uint16_t a = 0;
uint16_t max(uint16_t i, uint16_t j) {
    return ((i>j)?i:j);
}

而在main.c中如果没有include example.c但又想使用example.c中定义的变量则使用extern关键词

/* main.c */
#include <stdio.h>
extern uint16_t a;
extern uint16_t max(uint16_t i, uint16_t j);
void main(void) {
    printf("a=%d\r\n", a);
    printf("Max number between 5 and 9: %d\r\n", max(5, 9));
}

extern关键字还有一个重要的作用就是如果在C++程序中要引用C语言的文件则需要用以下格式

#ifdef __cplusplus
extern "C"{
#endif
......
#ifdef __cplusplus
}
#endif

这段代码的含义是如果当前是C++环境 _cplusplus是C++编译器中定义的宏要编译花括号{}里面的内容需要使用C语言的文件格式进行编译而extern “C”就是向编译器指明这个功能的语句。

4、volatile关键词

volatile原意是“易变的”在嵌入式环境中用volatile关键字声明的变量在每次对其值进行引用的时候都会从原始地址取值。由于该值“易变”的特性所以针对其的任何赋值或者获取值操作都会被执行而不会被优化。由于这个特性所以该关键字在嵌入式编译环境中经常用来消除编译器的优化可以分为以下三种情景

  1. 修饰硬件寄存器
  2. 修饰中断服务函数中的非自动变量
  3. 在有操作系统的工程中修饰被多个应用修改的变量
    如有操作系统比如RTOS、 UCOS-II、 Linux等的程序中如果有多个任务对同一个变量进行赋值或取值那么这一类变量也应使用volatile来修饰保证其可见性。所谓可见即当前任务修改了这一变量的值同一时刻其它任务此变量的值也发生了变化。

5、enum用法

enum是C语言中用来修饰枚举类型变量的关键字使用enum关键字可以创建一个新的“类型”并指定它可具有的值。要注意的是枚举类型是一种基本数据类型一个枚举常量的占的字节数为4个字节仅仅恰好和int类型的变量占的字节数相同并不意味着枚举类型等同于int型。

typedef enum week {
    Mon = 1, 
    Tues, 
    Wed, 
    Thurs
} day;
  • 在没有显式说明的情况下枚举常量默认第一个枚举常量的值为0往后每个枚举常量依次递增1
  • 在部分显式说明的情况下未指定的枚举名的值将依着之前最有一个指定值向后依次递增
  • 一个整数不能直接赋值给一个枚举变量必须用该枚举变量所属的枚举类型进行类型强制转换后才能赋值
  • 同一枚举类型中不同的枚举成员可以具有相同的值
  • 同一个程序中不能定义同名的枚举类型不同的枚举类型中也不能存在同名的枚举成员枚举常量。

枚举类型的目的是提高程序的可读性其语法与strut的语法类似。只要是能使用整型常量的地方就可以使用枚举常量例如在声明数组的时候可以使用枚举常量表示数组的大小在switch语句中可以把枚举常量作为标签。

6、typedef用法

typedef工具是一个高级数据特性利用typedef可以为某一类型自定义名称。 这方面与#define类似但是两者有三处不同

  • 与#define不同 typedef创建的符号只受限于类型不能用于值
  • tyedef由编译器解释不是预处理器
  • 在其受限范围内 typedef比#define更灵活

假设要用BYTE表示1字节的数组只需要像定义个char类型变量一样定义BYTE然后再定义前面加上关键字typedef即可

typedef unsigned char BYTE;

随后便可使用 BYTE 来定义变量

BYTE x, y[10];

为现有类型创建一个名称看起来是多此一举但是它有时的确很有用。在前面的示例中用BYTE代替unsigned char表明你打算用BYTE类型的变量表示数字而不是字符。使用typedef还能提高程序的可移植性。 用typedef来命名一个结构体类型的时候可以省略该结构的标签struct

typedef struct {     
    char name[50];     
    unsigned int age;     
    float score; 
} student_info;  

student_info student={“Bob”, 15, 90.5};

使用typedef的第二个原因是 tyedef常用于给复杂的类型命名例如 把pFunction声明为一个函数该函数返回一个指针该指针指向一个void型。

typedef void (*pFunction)(void);

7、预处理器与预处理指令

预处理指令

#define、 #include、 #ifdef、 #else、 #endif、 #ifndef、 #if、 #elif
#line、 #error、 #pragma 

根据程序中的预处理指令预处理器把符号缩写替换成其表示的内容 #define。预处理器可以包含程序所需的其它文件 #include可以选择让编译器查看哪些代码条件编译。预处理器并不知道C语法基本上它的工作是把一些文本转换成另外一些文本。

#define 与#undef 用法

每行#define逻辑行都由3部分组成。第1部分是#define指令本身第2部分是选定是缩写也称为宏有些宏代表值第3部分称为替换列表或替换体。

一旦预处理器在程序中找到宏的示例后就会用替换体代替该宏。从宏变成最终替换文本的过程称为宏展开。需要注意是预处理器会严格按照替换体直接替换不做计算不做优先级处理例如下面求取平方值的宏定义

#define sqr(x) x*x  
printf(2 的平反 %d”, sqr(2));  
输出的结果为4  

printf(2+2 的平方 %d”, sqr(2+2));  
编译器就会这样展开  
printf(2+2 的平方 %d”, 2+2 * 2+2);  
输出的结果为8  

但是实际按照逻辑2+2的平方是16得到8的结果是因为前面所说的预处理器不会做计算只会严格按照替换体的文本进行直接替换因而为了避免类似的问题出现我们应该这样改写平凡宏定义

#define sqr(x) ((x)*(x))  
printf(2+2 的平反 %d”, ((2+2)*(2+2))); 

文件包含指令#include

当预处理器发现#include预处理指令时会查看后面的文件名并把文件的内容包含到当前文件中即替换文件中的#include指令。这相当于把被包含文件的全部内容输入到源文件#include指令所在的位置。

#include指令有两种形式

#include <stdio.h> // 文件名在尖括号内
#include “myfile.h” // 文件名在双引号内  

在UNIX中尖括号<>告诉预处理器在标准系统目录中寻找该文件双引号“”告诉预处理器首先在当前目录或指定路径的目录中寻找该文件如果未找到再查找标准系统目录

#include <stdio.h> // 在标准系统目录中查找 stdio.h 文件
#include “myfile.h”  // 在当前目录中查找 myfile.h 文件
#include /project/header.h”  // 在 project 目录查找
#include ../myheader.h”  // 在当前文件的上一级目录查找

条件编译

可以使用预处理指令创建条件编译即可以使用这些指令告诉编译器根据编译时的条件执行或忽略代码块。条件编译还有一个用途是让程序更容易移植。改变文件开头部分的几个关键的定义即可根据不同的系统设置不同的值和包含不同的文件。

#ifdef、 #else和#endif指令

#ifdef HI /* 如果用#define 定义了符号 HI则执行下面的语句 */
#include <stdio.h>
#define STR "Hello world"
#else 
/* 如果没有用#define 定义符号 HI则执行下面的语句 */
#include "mychar.h"
#define STR "Hello China"
#endif

#ifdef指令说明如果预处理器已定义了后面的标识符则执行#else或#endif指令之前的所有指令并编译所有C代码如果未定义且有#elif指令则执行#else和#endif指令之间的代码。

#ifdef、 #else和C和if else很像两者的主要区别在于预处理器不识别用于标记块的花括号{}因此它使用#else如果需要的话和#endif必须存在来标记指令块。

#if和#elif

#if指令很像C语言中的if。 #if后面紧跟整型常量表达式如果表达式为非零则表达式为真可以在指令中使用C的关系运算符和逻辑运算符

#if MAX==1
printf("1");
#endif
可以按照 if else 的形式使用#if #elif
#if MAX==1
printf("1");
#elif MAX==2
printf("2");
#endif

8、位运算

按位与运算符&

参与运算的两个操作数每个二进制位进行“与”运算若两个都为1结果为1否者为0。

按位或运算符|

参与运算的两个操作数每个二进制位进行“ 或”运算若两个都为0结果为0 否则为1。

按位取反运算符~

按位取反运算符用于对一个二进制数按位取反。例如 ~1011第一位为1 取反为0第二位为0 取反为1第三位为1 取反为0结果为1第四位为 1 取反为0。最后结果为0100。

左移<<和右移>> 运算符

例如 假设val为unsigned char型数据对应的二进制数为10111001。若val=va<<3表示val左移3位然后赋值给val左移过程中高位移出去后被丢弃 低位补0最后val结果为11001000若val=val>>3表示val右移3位然后赋值给val 右移过程中 低位移出去后被丢弃 高位补0最后val结果为00010111。

清0或置1

在嵌入式中经常使用位运算符实现清0或置1。

例如 MCU的ODR寄存器控制引脚的输出电平高低寄存器为32位 每位控制一个引脚的电平。假设需要控制GPIOB的1号引脚输出电平的高低设置该寄存器第0位为1输出高电平设置该寄存器第0位为0输出低电平。

#define GPIOB_ODR (*(volatile unsigned int *)(0x40010C0C))
 
GPIOB_ODR &= ~(1<<0);   // 清0
GPIOB_ODR |= (1<<0);    // 置1

第一行 使用#define定义了GPIOB_ODR 对应的内存地址为0x40010C0C。 该地址为MCU的ODR寄存器地址。

第三行 GPIOB_ODR &= ~(1<<0)实际是GPIOB_ODR = GPIOB_ODR & ~(1<<0) 先将GPIOB_ODR和 ~(1<<0)的进行与运算运算结果赋值给GPIOB_ODR。 1<<0的值为00000000 00000000 00000000 00000001 再取反为11111111 11111111 11111111 11111110 则GPIO_ODR的第0位和0与运算 结果必为0其它位和1运算由GPIO_ODR原来的值决定结果。这就实现了只将GPIO_ODR的第0位清0其它位保持不变的效果实现了单独控制对应引脚电平输出低。

第四行 GPIOB_ODR |= (1<<0)实际是GPIOB_ODR = GPIOB_ODR | (1<<0)先将GPIOB_ODR和(1<<0)的进行或运算运算结果赋值给GPIOB_ODR。 1<<0的值为00000000 00000000 00000000 00000001则GPIO_ODR的第0位和0或运算结果必为1其它位和0运算由GPIO_ODR原来的值决定结果。这就实现了只将GPIO_ODR的第0位置1其它位保持不变的效果实现了单独控制对应引脚电平输出高。

持续更新中…

阿里云国内75折 回扣 微信号:monov8
阿里云国际,腾讯云国际,低至75折。AWS 93折 免费开户实名账号 代冲值 优惠多多 微信号:monov8 飞机:@monov6