嵌入式开发中C语言的编译环境不同,用法存在一定的区别,本文详解C语言在嵌入式开发中条件编译、指针用法、回调函数和位运算。希望能对大家有帮助。

条件编译

可以使用预处理指令创建条件编译,即可以使用这些指令告诉编译器根据编译时的条件执行或忽略代码块。

  1. #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(必须存在)来标记指令块。

  1. #ifndef指令
    #ifndef指令与#ifdef指令的用法类似,也可以和#else、#endif一起使用,但是它的逻辑和#ifdef指令相反。#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

条件编译还有一个用途是让程序更容易移植。改变文件开头部分的几个关键的定义即可根据不同的系统设置不同的值和包含不同的文件。

指针用法

什么是指针?从根本上看,指针是一个值为内存地址的变量。正如char类型变量的值是字符,int类型变量的值是整数,指针变量的值是地址。

因为计算机或者嵌入式设备的硬件指令非常依赖地址,指针在某种程度上把程序员想要表达的指令以更接近机器的方式表达,因此,使用指针的程序更有效率。尤其是指针能够有效的处理数组,而数组表示法其实是在变相的使用指针,比如:数组名是数组首元素的地址。

要创建指针变量,首先要声明指针变量的类型。加入想把ptr声明为储存int类型变量地址的指针,就要使用间接运算符*来声明。

假设已知ptr指向bah,如下表示:

ptr = &bah;

然后使用间接运算符*找出储存在bah中的值:value = *ptr;此运算符有时也被称为解引用运算符。语句ptr=&bah;value=*ptr;放在一起的效果等效于:value=bah;

那么该如何声明一个指针变量呢?是这样吗:

pointer ptr; // 不能这样声明一个指针变量

为什么不能这样声明一个指针变量呢?因为声明指针变量时必须指定指针所指向变量的类型,不同的变量类型所占据的储存空间是不同的,一些指针操作需要知道操作对象的大小。另外程序必须知道储存在指定地址的数据类型。例如:

int *pi; // pi 是指向 int 类型变量的指针
char *str; // str 是指向 char 类型变量的指针
float *pf, *pg; // pf, pg 都是只想 float 类型变量的指针

类型说明符表明了指针所指向对象的类型,解引用符号*表明声明的变量是一个指针。int *pi声明的意思是pi是一个指针,*pi是int类型,如图 5.3.4 所示。

这仅仅是指针的简单使用,实际指针的世界千变万化,丰富多彩,纵使多年C语言开发的老手,有时在面对指针的使用也会出错,后继者更应谨慎求索,后面将会对指针常见的应用和注意事项进行介绍。

  1. 指针与数组
    前面提到可以使用地址运算符&获取变量所在的地址,而在数组中同样可以使用取地址运算符获取数组成员中任意成员的地址,例如:
int week[7] = {1, 2, 3, 4, 5, 6, 7};
int *pw;
pw = &week[2];
printf("week is: %d", *pw);

输出的结果是:week is 3。对这段代码的释义参照上图 5.3.3。

  1. 指针与函数
    指针在函数中的使用最简单的是作为函数的形参,比如:
int sum(int *pdata)
{
int i = 0;
int temp = 0;
for(i=0;i<10;i++) {
temp = temp + (*pdata);
pdata++; }
return temp;
}

这个例子有几点值得讲解的地方,第1点指针pdata是作为函数的形参存在,指向一个储存int类型变量的地址;第2点指针pdata++;语句执行后,pdata只想的地址自增的不是1,而是int类型所占的大小,加入pdata最初的值是0,int类型占2个字节,那么pdata++;语句执行后,pdata的值就变成了2,而不是1,而*pdata的值是地址2所在的值不是地址1所在的值;第3点这个函数有个危险,即函数实现的是从pdata最初指向的地址开始往后的10个int类型变量的和,假如我们这样使用:

int data[5] = {1, 2, 3, -1, -2};
int x = sum(data);

可以看到数组data的数组名即数组的首地址作为参数输入到函数sum里,而数组的大小只有5个int,函数sum计算的却是10个数的和,因而就会出现地址溢出,得不到正确的结果甚至于程序跑飞。为了避免这个问题,通常的解决方法是加一个数量形参:

int sum(int *pdata, int length)
{
int i = 0;
int temp = 0;
for(i=0;i<length;i++) {
temp = temp + (*pdata);
pdata++; }
return temp;
}x = sum(data, 5);

或者给出指针范围:

int sum(int *pStart, int *pEnd)
{
int i = 0;
int temp = 0;
int length = (pEnd - pStart)/2; // 假设一个 int 占 2 个字节
for(i=0;i<length;i++) {
temp = temp + (*pdata);
pdata++; }
return temp;
}x = sum(data, &data[4]);

指针与函数的关系除了指针作为函数形参外还有另一个重要的应用,那边是函数指针,比如再typedef用法章节的那个例子:

typedef void (*pFunction)(void);

在这个例子中,首先*表明pFunction是一个指针变量,其次前面的void表示这个指针变量返回一个void类型的值,最后括号里面的void表明这个函数指针的形参是void类型的。如何使用函数指针调用函数呢?

看下面这个例子:

int max(int a, int b)
{
return ((a>b)?a:b);
}
int main(void) {
int (*pfun)(int, int);
int a=-1, b=2, c=0;
pfun = max;
c=pfun(a, b);
printf("max: %d", c);
return 0; }

输出的结果是:2。

  1. 指针与硬件地址
    指针与硬件地址的联系在volatile用法章节的例子中惊鸿一现,没有详细介绍,下面做详细说明。比如在STM32F103ZET6中内部SRAM的基地址是0x20000000,我们想对这片空间的前256个字节写入数据,就可以使用指针指向这个基地址,然后开始写:
volatile unsigned char *pData = (volatile unsigned char *)(0x20000000);
int main(void) {
int i = 0;
for(i=0; i<256; i++) {
pData[i] = i+10; }
return 0; }

除了内存地址,还可以指向硬件外设的寄存器地址,操作方式与上述例子类似。
指针应用的基本原则:

首先必须要指定指针的类型;如果是普通指针变量,非函数形参或者函数指针,必须要给指针变量指定地址,避免成为一个“野指针”;

回调函数

在C语言中回调函数是函数指针的高级应用。所谓回调函数,一个笼统简单的介绍就是一个被作为参数传递的函数。从字面上看,回调函数的意思是:一个回去调用的函数,如何理解这句话呢?从逻辑上分析,要“回去”,必然存在着一个已知的目的地,然后在某一个时刻去访问;那么回调函数就是存在一个已知的函数体A,将这个函数体A的地址即函数名“A”(函数名即是这个函数体的函数指针,指向这个函数的地址)告知给另外某个函数B,当那个函数B执行到某一步的时候就会去执行函数A。

回调函数的应用有很多,因之后的程序都是在STM32的HAL库下编写的,因而此处我们仅从HAL库出发来看其中的回调函数。

我们仅以GPIO的HAL库函数来看,文件名“stm32f1xx_hal_gpio.c”。我们用逆分析的方法来看这个回调函数。

首先是GPIO的回调函数声明:

__weak void HAL_GPIO_EXTI_Callback(uint16_t GPIO_Pin)

可以看到其函数名是:HAL_GPIO_EXTI_Callback,形参是GPIO_Pin表示引脚号(Px0~Px15, x=A,B,C,D,E,F,G),从这个函数的名称出发,可以大致明确这是一个引脚的外部中断(EXTI)的回调函数。然后大家看到前面还有个“__weak”,这是虚函数的修饰符,告诉编译器如果用户在其它地方用void HAL_GPIO_EXTI_Callback(uint16_t GPIO_Pin)重新定义了此回调函数那么优先调用用户定义的,否则调用这个虚函数修饰的回调函数。

紧接着我们来看此回调函数是在哪里被调用的:

void HAL_GPIO_EXTI_IRQHandler(uint16_t GPIO_Pin)
{
/* EXTI line interrupt detected */
if (__HAL_GPIO_EXTI_GET_IT(GPIO_Pin) != 0x00u) {
__HAL_GPIO_EXTI_CLEAR_IT(GPIO_Pin);
HAL_GPIO_EXTI_Callback(GPIO_Pin); } }

可以看到是在GPIO的外部中断服务函数中被调用的,与前面所说的这是一个外部引脚中断回调函数印证一致了。

GPIO的回调函数到此就说完了。其实STM32的HAL库中其它大多数的外设的回调函数基本都是如此,用户如果设计需求,就自己重定义需求的回调函数,然后在中断中被调用。

位运算

位运算是指二进制位之间的运算。在嵌入式系统设计中,常常要处理二进制的问题,例如将某个寄存器中的某一个位置1或者值0,将数据左移5位等,常用的位运算符如表 5.3.1 所示。

按位与运算符(&)
参与运算的两个操作数,每个二进制位进行“与”运算,若两个都为1,结果为1,否者为0。例如,1011&1001,第一位都为1,结果为1;第二位都为0,结果为0;第三位一个为1,一个为0,结果为0;第四位都为1,结果为1。最后结果为1001。

按位或运算符(|)
参与运算的两个操作数,每个二进制位进行“或”运算,若两个都为0,结果为1,否者为1。例如,1011 | 1001,第一位都为1,结果为1;第二位都为0,结果为0;第三位一个为1,一个为0,结果为1;第四位都为1,结果为1。最后结果为1011。

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

左移(<<)和右移(>>)运算符
左移(<<)运算符用于将一个数左移若干位,右移(>>)运算符用于将一个数右移若干位。例如,假设val为unsigned char型数据,对应的二进制数为10111001。若val=va<<3,表示val左移3位,然后赋值给val,左移过程中,高位移出去后被丢弃,低位补0,最后val结果为1100100;若val=val>>3,表示val右移3位,然后赋值给val,右移过程中,低位移出去后被丢弃,高位补0,最后val结果为00010111。

清零或置1
在嵌入式中,经常使用位预算符实现清零或置1。
例如,MCU的ODR寄存器控制引脚的输出电平高低,寄存器为32位,每位控制一个引脚的电平。假设需要控制GPIOB的1号引脚输出电平的高低,设置该寄存器第0位为1,输出高电平,设置该寄存器第0位为0,输出低电平。

#define GPIOB_ODR (*(volatile unsigned int *)(0x40010C0C))

GPIOB_ODR &= ~(1<<0);
GPIOB_ODR |= (1<<0);

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

以上C语言在嵌入式开发中条件编译、指针用法、回调函数和位运算。希望能对大家有帮助。