摘要
主要介绍了嵌入式编程中几个常用,但软件编程中用得不是很多的C语言知识。包括位操作、条件编译、结构体和结构体指针、typedef声明类型、以及extern变量声明、static关键字等内容。
本文并没有将相关C语言知识点介绍地很详细,毕竟这么多知识点要想掌握绝对不是看几篇文档就能掌握的。因此博主建议,如果上述的C语言知识掌握得还不是很好的话,找一本C语言的书好好研究研究。尤其是结构体和结构体指针、还有函数的知识(本文没提到),一定要很熟练。
本文除了简要介绍C语言知识,也结合博主自己的感受简单谈了各个知识点用在了嵌入式编程的什么地方,有不详细和描述不准确的地方欢迎大家留言讨论。
要想学习STM32,C语言的基础是必须的。除了最基本的C语言的语法,如循环、判断、数组、结构体、函数、指针这些软件编程常用的知识外,还包括位操作、条件编译、结构体指针、typedef声明类型、以及extern变量声明、static关键字等常用内容。这里结合实际代码分析一下这些知识点,如果想完整系统地了解这些C语言知识,大家可以翻翻C语言教材,比如《C Primer Plus》(第六版)这本书,尤其对于位操作的知识讲得很详细。
位操作简单说就是指对基本类型变量可以在位级别进行操作。下面先看几种位操作符:
& | 按位与 | ~ | 取反 |
| | 按位或 | << | 左移 |
^ | 按位异或 | >> | 右移 |
掌握了这六种操作否的用法,C语言的位操作就差不多了。这六种操作符的解释如下:
1. & 按位与: 如果两个相应的二进制位都为1,则该位的结果值为1,否则为0。// 1&1 = 1 1&0 = 0 0&1 = 0 0&0 = 0
2.|按位或:两个相应的二进制位中只要有一个为1,该位的结果值为1。// 1|1 = 1 0|1 = 1 1|0 = 1 0|0 = 0
3.^按位异或: 若参加运算的两个二进制位值相同则为0,否则为1。// 1^1 = 0 0^1 = 1 1^0 = 1 0^0 = 0
4.!取反: 对一个二进制数按位取反,即将0变1,将1变0。// 1! = 0 0! = 1
5.<<左移:用来将一个数的各二进制位全部左移N位,右补0。// 00001100 << 2 = 00110000
6.>>右移:将一个数的各二进制位右移N位,移到右端的低位被舍弃,对于无符号数,高位补0。// 00001100 >> 2 = 00000011
下面介绍一些用寄存器开发STM32时候实用的位操作技巧:
1)不改变其他位的值的状况下,对某几个位进行设值。
这个场景单片机开发中经常使用,方法就是先对需要设置的位用 & 操作符进行清零操作,然后用 | 操作符设值。比如我要改变GPIOA的状态,可以先对寄存器的值进行 & 清零操作
然后再与需要设置的值进行 | (或运算)。
GPIOA->CRL&=0XFFFFFF0F; //将第 4-7 位清 0
GPIOA->CRL|=0X00000040; //设置相应位的值,不改变其他位的值 (将CRL寄存器第7位设置为1)
2)取反操作使用技巧
SR 寄存器的每一位都代表一个状态,某个时刻我们希望去设置某一位的值为 0,同时其他位都保留为 1,简单的作法是直接给寄存器设置一个值:
TIMx->SR=0xFFF7;
这样的做法设置第3位为0,但是这样的作法同样不好看,并且可读性很差。看看库函数代码中怎样使用的:
TIMx->SR = (uint16_t)~TIM_FLAG;
而 TIM_FLAG 是通过宏定义定义的值:
#define TIM_FLAG_Update ((uint16_t)0x0001)
#define TIM_FLAG_CC1 ((uint16_t)0x0002)
看这个应该很容易明白,可以直接从宏定义中看出 TIM_FLAG_Update 就是设置的第 0 位了,可读性非常强。
注:在STM32的开发中,更多的时间可能会直接使用官方的库函数,库函数实际上是将复杂的寄存器封装了一下。使用库函数可以避免复杂的位操作,使代码更具有可读性,但同样的项目,使用库函数其代码量可能会比直接通过操作寄存器写出来的工程的代码量稍微多一点,执行效率可能会稍微低一点,当然这只是一点点…………
学习STM32的时候要从寄存器上去理解原理,理解实现过程,但是如果真的需要做一个嵌入式项目,可能用库函数去开发比较方便,效率更好一点,这是博主自己的感受和观点。
单片机程序开发过程中,经常会遇到一种情况, 当满足某条件时对一组语句进行编译,而当条件不满足时则编译另一组语句。 条件编译命令最常见的形式为:
#ifdef 标识符
程序段 1
#else
程序段 2
#endif
它的作用是:当标识符已经被定义过(一般是用#define 命令定义),则对程序段 1 进行编译,否则编译程序段 2。 其中#else 部分也可以没有,即:
#ifdef
程序段 1
#endif
这个条件编译在 MDK 里面是用得很多的,在 stm32f10x.h 这个头文件中经常会看到这样的语句:
#ifdef STM32F10X_HD
大容量芯片需要的一些变量定义
#end
而 STM32F10X_HD 则是我们通过#define 来定义的。
条件编译理解起来也不是很困难,可以类比于C语言中的 if-else 语句去理解。条件编译在STM32的开发中还是比较常用的。自己写代码写 .h 文件的时候开头会用到。此外就是要能看懂库函数里面的条件编译了。
结构体是C语言中的基础知识,同时结构体和结构指针也是STM32开发中非常重要的东西,尤其在使用库函数的时候,库函数中很多函数的入口参数中都有结构体指针,所以如果我们要调用这种函数,就先在主调函数中声明一个结构体变量,然后对这个结构体变量的各个成员赋值,最后再调用相关函数,调用的时候看清楚函数原型,入口参数是结构体类型还是结构体指针,不要搞错了。这里再多说两句,这里的结构体每个成员可以赋的值往往都是通过枚举或者宏定义确定好的,不能自己乱写,而应该去查找宏定义部分的代码,选定需要的那个枚举字面值作为结构体相关成员的值。
关于结构体和结构体指针的例子可以看GPIO的初始化,这里就不再多说了:STM32 GPIO的介绍
如果学过数据结构,相信对typedef也不陌生。用typedef的一个好处就是使代码的可读性更高,写代码也更方便。typedef 在代码中用得最多的就是定义结构体的类型别名和枚举类型了。
struct _GPIO
{
__IO uint32_t CRL;
__IO uint32_t CRH;
…
};
定义了一个结构体 GPIO,这样我们定义变量的方式为:
struct _GPIO GPIOA;//定义结构体变量 GPIOA
但是这样很繁琐, MDK 中有很多这样的结构体变量需要定义。这里我们可以为结体定义一个别名 GPIO_TypeDef,这样我们就可以在其他地方通过别名 GPIO_TypeDef 来定义结构体变量了。方法如下:
typedef struct
{
__IO uint32_t CRL;
__IO uint32_t CRH;
…
} GPIO_TypeDef;
Typedef 为结构体定义一个别名 GPIO_TypeDef,这样我们可以通过 GPIO_TypeDef 来定义结构体变量:
GPIO_TypeDef GPIOA,GPIOB;
这里的 GPIO_TypeDef 就跟 struct _GPIO 是等同的作用了。 这样是不是方便很多?
除了用在结构体上,typedef类型别名也大量用在int、short等这种变量上, 所以写STM32代码的时候几乎就不会出现类似于定义int型变量这样的语句,全部用 u8、u16这样的量代替了,比如u16代表的就是一个无符号的16位整型数据(这一个描述可能有一点偏差)。
C 语言中 extern 可以置于变量或者函数前,以表示变量或者函数的定义在别的文件中,提示编译器遇到此变量和函数时在其他模块中寻找其定义。这里面要注意,对于 extern 申明变量可以多次,但定义只有一次。在我们的代码中你会看到看到这样的语句:
extern u16 USART_RX_STA;
这个语句是申明 USART_RX_STA 变量在其他文件中已经定义了,在这里要使用到。所以,你肯定可以找到在某个地方有变量定义的语句:
u16 USART_RX_STA;
嗯,extern关键字,说实话,博主自己写代码确实没用过。So……