首页 > 编程学习 > 嵌入式C语言基础

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

嵌入式C语言基础

  • 1、const用法
    • 修饰变量/数组
    • 修饰指针
  • 2、static用法
    • 静态局部变量
    • 静态全局变量
    • static修饰函数
  • 3、extern关键词
  • 4、volatile关键词
  • 5、enum用法
  • 6、typedef用法
  • 7、预处理器与预处理指令
    • 预处理指令
    • #define 与#undef 用法
    • 文件包含指令#include
    • 条件编译
  • 8、位运算
    • 按位与运算符(&)
    • 按位或运算符(|)
    • 按位取反运算符(~)
    • 左移(<<)和右移(>>) 运算符
    • 清0或置1

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,其它位保持不变的效果,实现了单独控制对应引脚电平输出高。

持续更新中…


本文链接:https://www.ngui.cc/article/show-861283.html
Copyright © 2010-2022 ngui.cc 版权所有 |关于我们| 联系方式| 豫B2-20100000