嵌入式即嵌入式系统,IEEE(美国电气和电子工程师协会)对其定义是用于控制、监视或者辅助操作机器和设备的装置,是一种专用的计算机系统;国内普遍认同的嵌入式系统定义是以应用为中心,以计算机技术为基础,软硬件可裁剪,适应应用系统对功能、可靠性、成本、体积、功耗等严格要求的专用计算机系统;从应用对象上加以定义来说,嵌入式系统是软件和硬件的综合体,还可以涵盖机械等附属装置。
嵌入式系统作为装置或设备的一部分,它是一个控制程序存储在ROM中的嵌入式处理器控制板。事实上,所有带有数字接口的设备,如手表、微波炉、录像机、汽车等,都使用嵌入式系统,有些嵌入式系统还包含操作系统,但大多数嵌入式系统都是由单个程序实现整个控制逻辑。
最广泛使用的系统编程语言是C语言,它是使用自由格式源代码的简单编程语言;它曾用于以前用汇编语言构建的应用程序中。嵌入式C是C语言的扩展,它在嵌入式系统中应用于编写嵌入式软件。
普通c语言启动程序搭载了windows或linux等通用操作系统的PC机,编译器会自动完成启动程序,对微处理器和外围设备进行初始化,然后再调用main函数,用户没有必要制作自己的启动程序。
嵌入式c语言搭载微处理器,用于嵌入式系统的启动程序要能够对目标系统的硬件和数据进行初始化,因此,用户必须做特定的启动程序。一般情况下,在支持微处理器的编译器中会捆绑相应的启动程序,如下图:
存储器ROM和RAM
RAM:随机存取存储器(random access memory)又称作“随机存储器”,是与CPU直接交换数据的内部存储器,也叫主存(内存)。它可以随时读写,而且速度很快,通常作为操作系统或其他正在运行中的程序的临时数据存储媒介。当电源关闭时RAM不能保留数据。如果需要保存数据,就必须把它们写入一个长期的存储设备中(例如硬盘)。RAM和ROM相比,两者的最大区别是RAM在断电以后保存在上面的数据会自动消失,而ROM不会自动消失,可以长时间断电保存。
ROM:只读存储器。ROM所存数据,一般是装入整机前事先写好的,整机工作过程中只能读出,而不像随机存储器那样能快速地、方便地加以改写。ROM所存数据稳定,断电后所存数据也不会改变。
1. 对于普通c程序,操作系统将程序和数据从外部存储设备载入RAM中运行。代码、数据、堆栈都在RAM中。
2. 对于嵌入式c程序,因没有通用的操作系统,嵌入式系统必须先将代码设置到ROM中,将数据、堆栈设置到RAM中才可以运行。
1.如上图所示:对于普通c语言而言,操作系统对计算机硬件设备进行操作,如控制声卡发出声音,控制显卡绘制图形等。
应用程序可以通知操作系统执行某个具体的动作,以便使应用程序间接的通过操作系统对硬件进行操作。
对于操作系统是怎样控制硬件设备的只需大致了解即可,此过程为应用程序对API调用的过程,这一过程称为系统调用,通过系统提供的接口函数就可以指挥操作系统来工作了。
2.如上图所示:嵌入式系统在访问硬件时,必须编写直接操作硬件的应用程序。
1.由于在嵌入式系统中使用小而耗电的组件,嵌入式系统具有有限的ROM和RAM以及较少的处理能力,因此在嵌入式C中编写程序时应该注意有限的资源。
2.在C语言中,台式计算机可以访问系统操作系统,存储器等,可以利用所有计算机资源。
1.C主要用于简单但逻辑的程序,基于操作系统的软件等。
2.嵌入式C用于电视,洗衣机等微控制器。
以下都是嵌入式编程中几个常用,但软件编程中用得不是很多的C语言知识。包括位操作、条件编译、结构体和结构体指针、typedef声明类型、以及extern变量声明、static关键字等内容。
包括位操作、条件编译、结构体和结构体指针、typedef声明类型、以及extern变量声明、static关键字等内容。
位操作简单说就是指对基本类型变量可以在位级别进行操作。下面先看几种位操作符:
& | 按位与 | ~ | 取反 |
| | 按位或 | << | 左移 |
^ | 按位异或 | >> | 右移 |
掌握了这六种操作否的用法,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时候实用的位操作技巧:
这个场景单片机开发中经常使用,方法就是先对需要设置的位用 & 操作符进行清零操作,然后用 | 操作符设值。比如我要改变GPIOA的状态,可以先对寄存器的值进行 & 清零操作
然后再与需要设置的值进行 | (或运算)。
GPIOA->CRL&=0XFFFFFF0F; //将第 4-7 位清 0
GPIOA->CRL|=0X00000040; //设置相应位的值,不改变其他位的值 (将CRL寄存器第7位设置为1)
位操作在单片机开发中也非常重要,我们来看看下面一行代码
GPIOx->ODR |= (((uint32_t)0x01) << pinpos);
这个操作就是将 ODR 寄存器的第 pinpos 位设置为 1,为什么要通过左移而不是直接设置一个固定的值呢?
其实,这是为了提高代码的可读性以及可重用性。这行代码可以很直观明了的知道,是将第 pinpos 位设置为 1。
如果你写成GPIOx->ODR =0x0040; 这样的代码就不好看也不好重用了。
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的时候要从寄存器上去理解原理,理解实现过程,但是如果真的需要做一个嵌入式项目,可能用库函数去开发比较方便,效率更好一点,这是博主自己的感受和观点。
define 是 C 语言中的预处理命令,它用于宏定义,可以提高源代码的可读性,为编程提供
方便。常见的格式:
#define 标识符 字符串
“标识符”为所定义的宏名。“字符串”可以是常数、表达式、格式串等。例如:
#define PLL_M 8
定义标识符 PLL_M 的值为 8。至于 define 宏定义的其他一些知识,比如宏定义带参数这里我们就不多讲解。
单片机程序开发过程中,经常会遇到一种情况, 当满足某条件时对一组语句进行编译,而当条件不满足时则编译另一组语句。 条件编译命令最常见的形式为:
#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;
声明结构体类型:
Struct 结构体名{
成员列表;
}变量名列表;
例如:
Struct U_TYPE {
Int BaudRate
Int WordLength;
}usart1,usart2;
在结构体申明的时候可以定义变量,也可以申明之后定义,方法是:
Struct 结构体名字 结构体变量列表 ;
例如:struct U_TYPE usart1,usart2;
结构体成员变量的引用方法是:
结构体变量名字.成员名
比如要引用 usart1 的成员 BaudRate,方法是:usart1.BaudRate;
结构体指针变量定义也是一样的,跟其他变量没有啥区别。
例如:struct U_TYPE *usart3;//定义结构体指针变量 usart1;
结构体指针成员变量引用方法是通过“->”符号实现,比如要访问 usart3 结构体指针指向的结
构体的成员变量 BaudRate,方法是:
Usart3->BaudRate;
使用结构体的好处是防止函数的入口参数过多,当然也利于增加变量时不用修改函数定义,对于一组描述
同一对象的参数,用结构体使他们形成一个整体,也有利于代码的可读性,不会使变量定义显得混乱。