Cortex-M3&Cortex-M4-嵌入式软件开发

yaneng 2020-06-14

Cortex-M3&Cortex-M4-嵌入式软件开发

ARM微控制器是怎样构成的

微控制器中有多个部分,在许多微控制器中,处理器占的硅片面积小于10%,剩余部分被其他部件占用。例如:

  1. 程序存储器(如Flash存储器)。

  2. SRAM

  3. 外设。

  4. 内部总线。

  5. 时钟生成逻辑(包括锁相环)、复位生成器以及这些信号的分布网络。

  6. 电压调节和电源控制器回路。

  7. 其他模拟部件(如ADCDAC以及模拟参考回路)。

  8. I/O部分。

  9. 供生产测试使用的电路等。

这些部件中的一部分对编程人员是可见的,而其他部分则对开发者是不可见的(如供生产测试用的电路)。

软件开发流程

Cortex-M3&Cortex-M4-嵌入式软件开发

[注]简化的软件开发流程

  1. 创建工程。

  2. 添加文件到工程。

  3. 设置工程选项。

  4. 编译和链接。

  5. Flash编程。

  6. 执行程序和调试。

编译应用程序

首先,假定在开发自己的工程时使用C语言,这是在微控制器软件开发中最常用的编程语言。工程中可能还会包含一些汇编语言文件,如由微控制器供应商提供的启动代码。多数情况下的编译过程如下图所示。

Cortex-M3&Cortex-M4-嵌入式软件开发

[注] 常见的软件开发流程

多数开发组件中包含的工具列在下表中。

工具描 述
C编译器将C程序文件编译为目标文件
汇编器将汇编代码文件汇编为目标文件
链接器合并多个目标文件的工具,并定义存储器配置
Flash编程器将编译后的程序映像下载到微控制器的Flash存储器中的工具
调试器控制微控制器运行的工具,并可以访问内部运行信息,这样可以检查系统状态以及程序的执行情况.
模拟器在无实际硬件环境模拟程序执行的工具
其他工具多种工具,如将编译后的文件转换为各种格式

不同开发工具用不同的方式,指定微控制器系统中程序和数据存储器的布局,对于ARM工具链,则可以使用一种分散加载文件。以Keil MDK-ARM为例,分散加载文件可以由μVision开发环境自动生成。对于其他一些ARM工具链,可以使用命令行选项指定ROMRAM的位置。对于基于GNU的工具链,存储器的指定由链接器脚本完成,这些脚本一般会位于商业版gcc工具链的安装目录中。不过,有些gcc用户可能还需要自己创建这些文件。在使用GNUgcc工具链时,一般可以一次性地编译整个应用程序,而不是将编译和链接阶段拆开,如下图所示。

Cortex-M3&Cortex-M4-嵌入式软件开发

[注]GUN工具链的常见软件编译流程

gcc编译时会自动调用所需的链接器和汇编器,这种处理可以确保所需的详细参数和库被正确地传到链接器。单独使用链接器可能会引发错误,因此这种做法是多数gcc工具供应商所不提倡的。

软件流程

可以用很多种方法实现应用程序流程,下面介绍几个基本概念。

轮询

对于简单的应用,处理器可以等待数据准备好后进行处理,而后再等待。这种方式容易实现且非常适用于简单任务。如下图所示,为一个简单轮询程序流程图。

Cortex-M3&Cortex-M4-嵌入式软件开发

[注] 轮询方式的简单应用处理

多数情况下,微控制器要受多个接口的控制,因此需要支持处理多个任务。经过简单扩展,轮询程序流程就可以支持多个处理,如下图所示。这种处理有时也被称作“超级循环”。

Cortex-M3&Cortex-M4-嵌入式软件开发

[注] 轮询方式的应用中存在多个需要处理的设备

轮询的方法非常适合简单的应用,不过它还是具有诸多缺点。例如,当应用程序变得更加复杂时,轮询循环的设计维护非常困难。而且,使用轮询很难定义不同服务的优先级一结果可能就是反应缓慢,当处理器正在处理不重要的任务时,外设请求可能需要等待很长的时间。

中断驱动

轮询的另外一个缺点在于能耗效率差,在不需要服务时也会浪费很多能量。为了解决这个问题,几乎所有的微控制器都会提供某种休眠模式以降低功耗,在休眠模式下,外设在需要服务时可以将处理器唤醒,如下图所示。这通常被称作中断驱动的应用程序。

Cortex-M3&Cortex-M4-嵌入式软件开发

[注] 简单的中断驱动应用

在中断驱动的应用中,不同外设的中断可以被指定为不同的中断优先级。例如,重要(关键)的外设可以被指定为较高的优先级,这样,若中断产生时处理器正在处理更低优先级的中断,低优先级中断就会被暂停,而更高优先级的中断服务就会立即执行。这种设计的响应较快。

多数情况下,外设服务的数据处理可以分为两部分:第一部分需要快速处理,而另一部分则可以执行得稍微慢一些。在这种情况下,在编写程序时可以将中断驱动和轮询结合起来。在当外设需要服务时,它就会像中断驱动的应用一样触发一个中断请求。当第一部分中断服务执行后,它就会更新某些软件变量,以便第二部分可以在基于循环的应用程序代码中执行,如下图所示。

Cortex-M3&Cortex-M4-嵌入式软件开发

[注] 使用轮询和中断驱动两种方式的应用

通过这种处理,可以减少高优先级中断处理的持续时间,因此更低优先级的中断服务也可以更快地执行。同时,在不需要处理时,处理器还可以进人休眠状态以降低功耗。

多任务系统

当应用更加复杂时,轮询和中断驱动的程序架构未必能够满足处理需求。例如,有些执行时间长的任务可能会需要同步处理。要实现这一操作,可以将处理器时间划分为多个时间片并且将时间片分给这些任务。若技术上有通过手动分割任务且创建简单的调度器实现这种处理的需求,其在实际项目中通常却是不切实际的,因为这样会非常耗时间且会使程序维护和调试非常困难。在这些应用中,实时操作系统(RTOS)可用于处理任务调度,如下图所示。RTOS可以将处理器时间分为多个时间片且将时间片分给所需的进程,以实现多个进程同时执行。需要一个定时器来记录RTOS的时间,而且在每个时间片的最后,定时器会产生定时中断,它会触发任务调度器且确定是否要执行上下文切换。若需要执行,当前正在执行的任务就会被暂停,处理器转而执行另外一个任务。

Cortex-M3&Cortex-M4-嵌入式软件开发

[注] 使用RTOS处理多任务

除了任务调度,RTOS还具有其他许多特性,如信号量和消息传递等。许多RTOS都可用于Cortex-M处理器,而且不少还是免费的。(比如FreeRTOSμ/COSRT-Thread

C语言里面的数据类型

? C编程语言支持多种“标准”数据类型,不过,数据在硬件中的表示方式要取决于处理器架构和C编译器。对于不同的处理器架构,某种数据类型的大小可能是不一样的。例如,整数在8位或16位微控制器上一般是16位,而在ARM架构上则总是32位的。下表列出了ARM架构(其中包括所有的Cortex-M处理器)中的常见数据类型,所有的C编译器都支持这些数据类型。

? 由于特定数据类型大小的差异,将应用程序从8位或16位微控制器移植到ARM Cortex-M微控制器上时,可能需要修改源代码

? 在ARM编程中,如下表所示,还可以将数据大小称为bytehalfword word以及double word

? 这些叫法在ARM文档中非常普遍,其中包括指令集硬件描述文档

C和C99(stdint. h)数据类型位数范围(有符号)范围(无符号)
char , int8 t , uint8 t8\(-128\) ~ \(127\)\(0\) ~ \(255\)
short int16_t , uint16_t16\(-32768\) ~ \(32767\)\(0\)~ \(65535\)
int , int32_t , uint32_t32\(-2147483648\) ~ \(2147483647\)\(0\)~ \(4294967295\)
Long32\(-2147483648\) ~ \(2147483647\)\(0\) ~ \(4294967295\)
Long long , int64_t , uint64_t64\((-2^{63})\) ~ \((2^{63} - 1)\)\(0\) ~ \((2^{64} - 1)\)
C和C99(stdint. h)数据类型位数范围
Float32\(-3.4028234 \times 10^{38}\) ~ \(3.4028234 \times 10^{38}\)
Double64\(-1.7976931348623157 \times 10^{308}\) ~ \(1.7976931348623157 \times10^{308}\)
long double64\(-1.7976931348623157 \times 10^{308}\)~ \(1.7976931348623157 \times10^{308}\)
Pointers320x0 ~ 0xFFFFFFFF
Enum8/16/32可用的最小数据类型,除非由编译器选项指定
bool (只存在C++) , _Bool(只存在于C)8True或False
wchar_t160 ~ 65535

[注] 包括Cortex-M在内的ARM架构支持的数据类型的大小和范围

类型大小
byte(字节)8位
half word(半字)16位
word(字)32位
double word(双字)64位

[注] ARM处理器的数据类型定义

输入、输出和外设访问

一般来说,外设在使用前需要初始化,一般包括以下几步:

  1. 如果需要,设置时钟控制回路使能连接到外设和对应引脚的时钟。许多现代微控制器允许对时钟信号分布的精细调节,如使能/禁止到每个外设的时钟连接以节省功耗。外设时钟一般是默认关闭的,需要在编程外设前使能时钟。有些情况下,可能还需要使能外设总线系统的时钟。

  2. 有些情况下,可能还需要配置I/O引脚的操作模式。大多数微控制器都有复用的I/O 引脚,可用于多种目的。为了使用外设,配置I/O脚以匹配用途是很有必要的(如输入/输出方向、功能等)。另外,可能还需要编程其他的配置寄存器,定义输出类型等预想的电气特性(电压、上拉/下拉和开漏等)。

  3. 外设配置。多数外设中包含多个可编程寄存器,它们需要在使用前进行配置。有些情况下,会发现配置流程比8位微控制器要稍微复杂一些,这是因为32位微控制器的外设一般要比8位/16位系统的外设要复杂得多。另外,微控制器供应商一般会提供设备驱动库代码,可以使用这些驱动函数以降低所需的编程工作量。

  4. 中断配置。若外设需要使用中断操作,则需要编程Cortex-M3/M4处理器的中断控制器(NVIC),使能中断和配置中断优先级。要实现所有的这些初始化步骤,需要设置各种外设模块中的外设寄存器。前面已经提到,外设寄存器经过了存储器映射,因此可以使用指针访问。例如,可以将通用目的输入/输出(GPIO)寄存器定义为指针:

/ * STM32F 100RBT6BeGPIO A端口配置寄存器低字¥/
	# define GPIOA_CRL ( * ((volatile unsigned long * ) (0x40010800))
/ * STM32F 100RBT6BeGPIO A端口配置寄存器高字* /
	# define GPIOA_CRH ( * ((volatile unsigned long * ) (0x40010804)))
/ * STM32F 100RBT6BeGPIOA端口输人数据寄存器*/
	# define GPIOA_IDR ( * ((volatile unsigned long * ) (0x40010808)))
/ * STM32F 100RBT6BeGPIO A端口输出数据寄存器* ,
	#define  GPIOA_ODR ( * ((volatile unsigned 1ong * ) (0x4001080C)))
/ * STM32F 100RBT6BeGPIO A端口位设置/清除寄存器*/
	# define GPIOA_BSRR( * ((volatile unsigned long * ) (0x40010810)))
/ * STM32F 100RBT6BeGPIOA端口位清除寄存器* /
	# define GPIOA_BRR ( * ((volatile unsigned long * ) (0x40010814)))
/ * STM32F 100RBT6BeGPIO A端口配置锁定寄存器*/
	# define GPIOA_LCKR ( * ((volatile unsigned long * ) (0x40010818)))

下面直接使用这种定义。例如:

void GPIOA_reset(void) /*复位GPIOA */
{
	//设置所有引脚为模拟输人模式

	GPIOA_CRL = 0; //位0~7,都设置为模拟输人

	GPIOA_CRH = 0; //位8~15,都设置为模拟输入

	GPIOA_ODR = 0; //默认输出值为0

	return;
}

这种方法对少量的外设寄存器非常有用,不过,当外设寄存器数量变大时,这种编码风格

可能就会有问题。因为:

  1. 对于每个寄存器地址定义,程序需要存储32位地址常量,这样会增大代码体积。

  2. 当同一个外设具有多个实例时,例如,STM32微控制器有5个GPIO外设,每次实例化时都需要重复相同的定义,这样扩展性不强而且不易维护。

  3. 创建同一外设的多个实例共用的函数也是非常困难的。例如,按照上面例子中的定义,可能需要为每个GPIO端口创建相同的GPIO复位函数,这样会增加代码体积。

为了解决这些问题,通常的做法是将外设寄存器定义为数据结构体。例如,在微控制器供应商提供的设备驱动库中,可以找到:

typedef struct
{
	__IO uint32_t CRL;
	__IO uint32_t CRH;
	__IO uint32_t IDR;
	__IO uint32_t ODR;
	__IO uint32_t BSRR;
	__IO uint32_t BRR;
        __IO uint32_t LCKR;
} GPIO_TypeDef;

那么,每个外设基地址(GPIOA~GPIOG)都可以被定义为指向这个结构体的指针:

#define PERIPH_ BASE ( (uint32_t) 0x40000000)

/*!<位段区域中的外设基地址*/

# define APB2PERIPH_BASE (PERIPH_BASE + 0x10000)

# define GPIOA_BASE ( APB2PERIPH_BASE + 0x0800)

# define GPIOB_BASE ( APB2PERIPH_BASE + 0x0C00)

# define GPIOC_BASE ( APB2PERIPH_BASE + 0x1000)

# define GPIOD_BASE ( APB2PERIPH_BASE + 0x1400)

# define GPIOE_BASE ( APB2PERIPH_BASE + 0x1800)

# define GPIOA ( (GPIO_TypeDef * ) GPIOA_BASE)

# define GPIOB ( (GPIO_TypeDef * ) GPIOB_BASE)

# define GPIOC ( (GPIO_TypeDef * ) GPIOC_BASE)

# define GPIOD ( (GPIO_TypeDef * ) GPIOD_BASE)

# define GPI0E ( (GPIO_TypeDef * ) GPIOE_BASE)

……

? 这些代码段中存在一些之前没有介绍过的新东西:

IO”在CMSIS标准头文件中定义,它代表数据为volatile的(如外设寄存器),其可由软件进行读写。除了“IO" ,外设寄存器还可以被定义为“I”(只读)和“O”(只写)。

#ifdef _cplusplus
	#define _I volatile         / * !<定义"只读"权限* /
#else 
	#define _I volatile const  / * !<定义"只读"权限* /
#endif
#define _O volatile             / * !<定义"只写"权限* /
#define _IO volatile            / * !<定义"读/写"权限* /

[volatile] volatile 关键字是一种类型修饰符,用它声明的类型变量表示可以被某些编译器未知的因素更改,比如:操作系统、硬件或者其它线程等。遇到这个关键字声明的变量,编译器对访问该变量的代码就不再进行优化,从而可以提供对特殊地址的稳定访问。当要求使用 volatile 声明的变量的值的时候,系统总是重新从它所在的内存读取数据,即使它前面的指令刚刚从该处读取过数据。而且读取的数据立刻被保存。
[const] constconstant 的缩写,本意是不变的,不易改变的意思。在 C中是用来修饰内置类型变量,自定义对象,成员函数,返回值,函数参数。C const 允许指定一个语义约束,编译器会强制实施这个约束,允许程序员告诉编译器某值是保持不变的。如果在编程中确实有某个值保持不变,就应该明确使用const,这样可以获得编译器的帮助。被const修饰的变量a可以看做常量,对一个常量赋值是违法的事情,因为 a 被编译器认为是一个常量,其值不允许修改)

uint32_t(无符号32位整数)为C99支持的一种数据类型,与处理器架构无关,它可以确保数据大小为32位的,这样有助于提高软件的可移植性。要使用这个数据类型,工程中需要包含标准数据类型头文件(注意:若使用符合CMSIS的设备头文件,那么这个设备头文件就已经为你做好了)。

#include <stdint.h>/*包含标准类型*/
/*C99标准数据类型:
	uint8_t:8位无符号数,int8_t:8位有符号数,
	uint16_t : 16位无符号数,int16_t : 16位有符号数,
	uint32_t : 32位无符号数,int32_t : 32位有符号数,
	uint64_t : 64位无符号数,int64_t : 64位有符号数
*/

当外设使用这种方法声明时,可以创建能用于每个外设实例的函数。例如,复位GPIO端口的代码可以写作:

void GPIO_ reset(GPIO_ TypeDef * GPIOx)
{
	//设置所有引脚为模拟输人模式
	GPIOx->CRL = 0; //位0~7,都设置为模拟输人
	GPI0x->CRH = 0; //位8~15,都设置为模拟输人
	GPI0x->ODR = 0; //默认输出值为0
	return;
}

要使用这个函数,只需将外设基地址指针传递给函数:

GPIO_ reset(GPIOA); / *复位GPIOA * /
GPIO_ reset(GPI0B); / *复位GPIOB * /
……

几乎所有的Cortex-M微控制器设备驱动库都使用这种方法定义外设。

Cortex微控制器软件接口标准(CMSIS)

CMSIS-Core的组织结构

? CMSIS文件被集成在微控制器供应商提供的设备驱动库软件包中,设备驱动库中的有些

? 文件是ARM准备的,对于各家微控制器供应商都是一样的,其他文件则取决于供应商/设备,一般来说,可以将CMSIS定义为以下几层:

  1. 内核外设访问层。名称定义、地址定义以及访问内核寄存器和内核外设的辅助函数,这是处理器相关的,由ARM提供。

  2. 设备外设访问层。名称定义、外设寄存器的地址定义以及包括中断分配、异常向量定义等的系统设计,这是设备相关的(注意:同一家供应商的多个设备可能会使用同一组文件)。

  3. 外设访问函数。访问外设的驱动代码,这是供应商相关的,而且是可选的。在开发应用程序时,可以选择使用微控制器供应商提供的外设驱动代码,或者有必要,也可以直接访问外设。

? 对于外设访问还提出了另外一层:中间件访问层。该层在当前的CMSIS版本中不存在,现在的设想为,开发组用于访问UARTSPI以及以太网等常见外设的API。若该层存在,中间件开发人员可以基于该层开发自己的应用程序,这样软件在设备间移植也就更加容易。

各层的角色如下图所示

? 注意在有些情况下,设备驱动库中可能会包含用于微控制器供应商设计的NVIC的函数,它们是供应商定义的。CMSIS的目标为提供一个共同的起点,微控制器供应商也可以根据自己的意愿添加其他的函数。不过若软件需要在另外一个微控制器产品上重用,就需要移植。

Cortex-M3&Cortex-M4-嵌入式软件开发

[注] CMSIS-Core 结构