微机原理与接口技术 · 第5章《ARM 微控制器开发》知识点全梳理
本文整理自福州大学《微机原理与接口技术》吴衔誉教授第五章课件,系统讲解开发流程与工具链、处理器启动过程、输入输出接口、程序映像、C 语言开发以及固件库六大板块。
目录
- 一、开发流程与工具链
- 二、处理器的启动过程
- 三、输入和输出接口
- 四、程序映像
- 五、C 语言开发 ARM 应用
- 六、固件库
- 本章小结与思考题
一、开发流程与工具链
1.1 什么是工具链(toolchain)
工具链(toolchain,工具链条)不是某一个软件,而是一串工具首尾相接的组合:一个工具的输出,喂给下一个工具当输入,像流水线一样把源代码变成能烧进芯片的机器码。
ARM 微控制器开发常用工具包括:
- C 编译器:把
.c源文件翻译成汇编或目标代码; - 汇编器:把汇编源文件翻译成机器码;
- 链接器:把编译器产生的各个模块拼起来,再从 C 语言程序库里抽取需要的代码,组成一个可执行文件;
- 指令模拟器:在电脑上模拟跑代码,不用真板子也能调试逻辑;
- Flash 编程器:把最终二进制写进芯片的 Flash;
- 调试器:单步、设断点、看变量。
开发中可以用 C 语言、汇编语言,或者两者混合编程。
1.2 集成开发环境(IDE)与调试器
- IDE(Integrated Development Environment,集成开发环境):把代码编辑器、编译器、调试器、图形界面打包在一起的应用程序。本课程采用Keil MDK-RTM作为集成开发环境。
- ICE(In-Circuit Emulator,在线仿真器 / 在线调试器):调试嵌入式软件的硬件设备。本课程采用ST-LINK V2调试下载器。
大多数开发工具都自带友好的 IDE,把它和在线调试器配合使用,就能分步完成「创建工程 → 编译应用程序 → 下载到微控制器」这一整套流程。
一句话概括开发过程:在 Keil 里建工程、写代码、编译,再用 ST-LINK V2 把程序下载进 STM32 的 Flash。
二、处理器的启动过程
2.1 程序存在哪里
现代微控制器内部几乎都带有片上 Flash 存储器,用来保存编译好的程序代码。这些代码以二进制机器码的形式存放,所以无论是汇编程序还是 C 程序,都得先经过汇编或编译,才能烧写进 Flash。
有些微控制器还额外配了一块独立的启动 ROM(BootROM),里面固化了一个小的BootLoader(引导加载程序)。芯片启动后、执行 Flash 里的用户程序之前,BootLoader 会先运行。多数情况下 BootLoader 是固定的,只有 Flash 里的用户程序才是可变的。
2.2 复位以后发生了什么
程序烧写进 Flash 后,处理器复位就会运行「复位流程」:
- 处理器取出MSP(主堆栈指针)的初始值;
- 取出复位向量(复位处理程序的入口地址);
- 开始执行复位处理。
这些信息都放在一个叫启动代码的程序文件里。
对于 C 语言程序,在进入main()之前,启动代码就已经悄悄把变量、内存等都初始化好了——作为程序员完全不用操心,因为 C 语言开发工具会自动把启动代码插进程序映像里。而汇编程序则麻烦得多,设堆栈、设入口点都得自己来(参考第 4 章汇编程序框架)。
2.3 启动代码的核心 5 步
系统上电后,第一个执行的一般是由汇编语言写的启动文件startup_stm32f10x_hd.s,它的核心工作有 5 项:
| 步骤 | 动作 | 说明 |
|---|---|---|
| 1 | 初始化堆栈指针SP = _initial_sp(即 MSP) | 给程序一个能用的栈顶 |
| 2 | 初始化 PC 指针= Reset_Handler | 告诉芯片复位后跳去哪 |
| 3 | 初始化中断向量表 | 把各种异常 / 中断的入口地址排好 |
| 4 | 配置系统时钟 | 配置时钟控制电路、锁相环 PLL 等 |
| 5 | 调用 C 库函数_main初始化用户堆栈 | 最终调用main函数,进入 C 的世界 |
这 5 步一般由 ST 官方在固件库自带的汇编启动文件startup_stm32f10x_hd.s中实现,开发者一般无需手写。
2.4 应用程序通常包含 3 部分
启动代码跑完后,才轮到你的应用程序,它通常由 3 块组成:
- 硬件初始化:时钟、PLL、外设等;
- 应用程序的处理部分:你真正想让芯片做的事;
- 中断服务程序:响应外部事件。
如果用了 C 库函数,编译器 / 链接器会把所需的库代码自动纳进程序映像。
三、输入和输出接口
嵌入式系统里可用的 I/O 五花八门:数字和模拟输入 / 输出、UART、I2C、SPI,很多微控制器还提供 USB、以太网、CAN、LCD、SD 卡等接口——这些接口都由**外设(peripheral)**控制。
外设的典型初始化过程一般分 4 步:
- 配置时钟控制回路:使能外设的时钟信号,必要时初始化相应引脚;
- 配置 I/O:引脚功能配置、电气特性配置;
- 配置外设:控制寄存器配置;
- 配置中断:配置 NVIC(Nested Vectored Interrupt Controller,嵌套向量中断控制器)。
四、程序映像
ARM 的映像文件就是可执行文件,常见bin或hex两种格式,可以直接烧到 ROM 里执行。
4.1 程序映像的组成
Cortex-M3 的程序映像一般包含 4 部分:
| 组成 | 内容 |
|---|---|
| 向量表 | 存放堆栈顶、复位向量、各异常 / 中断入口地址 |
| C 启动代码 | 初始化全局变量、清零未初始化内存、跳main |
| 程序代码 | 应用程序指令 + 变量初值 + 常量(查找表、图像数据等) |
| C 库代码 | 用到特定 C / C++ 库函数时,链接时嵌入 |
4.2 向量表与启动文件片段
向量表用 C 或汇编实现,其入口需要编译器和链接器生成的内容,因此实现细节和工具链相关。Keil MDK 建 STM32 工程时,向量表是汇编启动代码的一部分,用DCD(定义常量数据)指令创建。来看startup_stm32f10x_hd.s里最关键的一段:
Stack_Size EQU 0x00000400 ; 栈大小 = 1 KB AREA STACK, NOINIT, READWRITE, ALIGN=3 Stack_Mem SPACE Stack_Size ; 分配栈空间 __initial_sp ; 栈顶标号 Heap_Size EQU 0x00000200 ; 堆大小 = 512 字节 AREA HEAP, NOINIT, READWRITE, ALIGN=3 __heap_base Heap_Mem SPACE Heap_Size ; 分配堆空间 __heap_limit PRESERVE8 ; 指示编译器 8 字节对齐 THUMB ; 后面的指令为 Thumb 指令 AREA RESET, DATA, READONLY ; 声明 RESET 只读数据段 EXPORT __Vectors ; 导出向量表标号 __Vectors DCD __initial_sp ; 栈顶 DCD Reset_Handler ; 复位处理程序入口 DCD NMI_Handler ; NMI 处理程序入口 DCD HardFault_Handler ; 硬件错误处理程序入口几个要点:
Stack_Size EQU 0x00000400:栈 1 KB;Heap_Size EQU 0x00000200:堆 512 字节。AREA ... NOINIT:只预留内存单元,不写入初始值。PRESERVE8:要求 8 字节对齐;THUMB:后续为 Thumb 指令。DCD从地址 0x00000000 开始,依次放栈顶(4 字节)、复位向量(0x00000004)、NMI(0x00000008)……这就是向量表在 Flash 最前面的排布。
4.3 C 启动代码与程序代码
- C 启动代码:设置全局变量、清零加载时未初始化的内存区域、若用到
malloc还要初始化堆控制变量,最后跳main()。它由编译器 / 链接器自动嵌入,纯汇编程序可能没有它。 - 程序代码:用户任务的指令,外加变量初值、常量、查找表、图像数据等。
- C 库代码:用到特定库函数时由链接器嵌入。
五、C 语言开发 ARM 应用
虽然汇编效率更高,但 C / C++ 仍是 ARM 嵌入式开发的主流语言:执行效率较高、语法自由、模块化好。本节重点讲 C 在 ARM 嵌入式里和写 PC 软件不太一样的地方。
5.1 数据类型
C 语言数据类型按构造特点分为 4 大类:
- 基本类型:整型、字符型、实型(单精度、双精度);
- 构造类型:数组、结构体、共用体、枚举;
- 指针类型:存变量在内存中的地址;
- 空类型:无返回值的函数定义为空类型。
整型(INTEGER)
ARM 嵌入式 C 里,整型按长度分:
| 类型 | 说明符 | 长度 |
|---|---|---|
| 基本型 | int | 2 字节 |
| 短整型 | short int/short | 2 字节 |
| 长整型 | long int/long | 4 字节 |
| 无符号基本型 | unsigned int/unsigned | — |
| 无符号短整型 | unsigned short | — |
| 无符号长整型 | unsigned long int | — |
整型常量可用十进制、八进制(0 开头)、十六进制(0x 开头)表示。
实型(浮点型)
- 实型常量两种写法:十进制数形式(必须有小数点,如
25.0)和指数形式(aEn,如5.789); - ARM 嵌入式代码里,实型常数统一按double(双精度)处理;
- 实型变量分单精度
float(一般占 4 字节)、双精度double、长双精度long double。
字符型与字符串
- 字符常量用单引号(
'a'、'+'),只占 1 字节; - 字符串常量用双引号(
"CHINA"、"C program"); - 字符串常量占的字节数 = 字符数 + 1,多出的 1 字节存
'\0'(ASCII 0),作为字符串结束标志; - ARM 嵌入式程序里没有字符串变量,要用字符数组来存字符串。
类型转换
- 自动转换:不同数据类型混合运算时由编译系统自动完成;
- 强制转换:
(类型说明符)(表达式),把结果强制转成目标类型。
经典易错示例:
#include<stdio.h>intmain(){floatx=11.20;inty=(int)x;// y = 11intz=(int)(100*(x-y));// 100×(11.1999998…−11)=19.99998,取整得 19printf("z=%d \n",z);// 输出 19return0;}11.20在float下存为11.1999998…,减去11得0.1999998…,乘100得19.99998,取整后被截断为19(而不是 20)。这是浮点舍入造成的,常被拿来考。
输入输出格式字符
| 格式字符 | 含义 |
|---|---|
d | 十进制带符号整数 |
o | 八进制无符号整数 |
x/X | 十六进制无符号整数 |
u | 十进制无符号整数 |
f | 小数形式单 / 双精度实数 |
e/E | 指数形式单 / 双精度实数 |
g/G | 较短宽度输出 |
c | 单个字符 |
s | 字符串 |
scanf的变量前记得加取地址符&。
5.2 数据运算
ARM 嵌入式 C 支持 10 类运算符:
- 算术:
+ - * / % ++ -- - 关系:
> < == >= <= != - 逻辑:
&& || ! - 位操作:
& | ~ ^ << >> - 赋值:
= += -= ... - 条件:
? : - 逗号:
, - 指针:
*取内容、&取地址 - 求字节:
sizeof - 特殊:括号
()、下标[]、成员->.
算术运算符
共 7 种:加、减、乘、除、求余(模)、自加、自减。注意 ARM 的乘法和除法有专用乘法器 / 除法器,减轻处理器排队压力,但会增加芯片成本。
坑点示例(整数除与浮点除混用):
printf("用%%d显示:20/7 = %d\n",20/7);// 2(整数除,截断)printf("用%%f显示:20.0/7 = %f\n",20.0/7);// 2.857143printf("对100求3的余数:%d\n",100%3);// 1优先级与结合性
运算符优先级共15 级(1 级最高)。优先级相同时按结合性处理:
- 左结合(自左至右):如
x - y + z,先算x-y再加z; - 右结合(自右至左):最典型是赋值号,
x = y = z等价于x = (y = z)。
自增与自减
++i(先加再用)、i++(先用再加),--同理。课件里的经典追踪:
inti=8;printf("%d\n",++i);// 9printf("%d\n",--i);// 8printf("%d\n",i++);// 8(打印后变9)printf("%d\n",i--);// 9(打印后变8)printf("%d\n",-i++);// -8printf("%d\n",-i--);// -9提示:除循环累加等明确场景外,含全局变量的代码里不建议滥用自增 / 自减。
复合赋值与逗号表达式
- 复合赋值:
a += b等价于a = a + b;还有-= *= /= %= <<= >>= &= ^= |=; - 逗号表达式:
(表达式1, 表达式2),依次求值,整个逗号表达式的值是表达式 2 的值。注意函数参数表里的逗号只是分隔符,不是逗号表达式。
5.3 流程控制
流程控制分两类:
- 判断语句:
if、if-else、if-else if、switch; - 循环语句:
goto、while、for。
switch注意:case 后不加break会「穿透」——匹配到某个 case 后会一路执行后面所有 case,直到遇break或结束。case/default顺序可任意调换,不影响结果;default可省略。
循环小结:
- 不提倡用
goto(容易让程序混乱); while循环体里要有让循环趋于结束的语句,且循环变量初始化要在while之前;for可以在「表达式 1」里完成循环变量初始化,最灵活。
5.4 用 C 语言操作外设
Cortex-M3 的外设寄存器被映射到系统存储器空间,可通过指针访问。以 USART1 为例:
#definePERIPH_BASE((uint32_t)0x40000000)// 外设基地址#defineAPB2PERIPH_BASE(PERIPH_BASE+0x10000)#defineUSART1_BASE(APB2PERIPH_BASE+0x3800)#defineUSART1((USART_TypeDef*)USART1_BASE)USART_TypeDef结构体把外设寄存器按顺序排好:
typedefstruct{__IOuint16_tSR;uint16_tRESERVED0;__IOuint16_tDR;uint16_tRESERVED1;__IOuint16_tBRR;/* ... 其余寄存器与保留位 ... */}USART_TypeDef;发一个字节只需:USART1->DR = mydata;
关键:volatile与__IO
结构体里的__IO实质上是volatile(易变)的宏定义(在core_m3.h里)。外设指针必须用volatile修饰,原因是:
volatile告诉编译器,这个变量随时可能变化(比如被硬件改写了),每次使用都必须重新从它的内存地址读取,不要图省事把上次读到的值缓存起来做优化。
没有volatile时,编译器发现两次读变量之间没动过它,就可能复用旧值,导致读不到外设的最新状态——这在 Release 优化版本里尤其容易出 bug。
进一步可以把外设操作封装成函数,STM32 固件库就是这样设计的:
voidUSART_SendData(USART_TypeDef*USARTx,uint16_tData){USARTx->DR=(Data&(uint16_t)0x01FF);}5.5 Cortex 微控制器软件接口标准(CMSIS)
CMSIS(Cortex Microcontroller Software Interface Standard,Cortex 微控制器软件接口标准)是 Cortex-M 处理器系列与供应商无关的硬件抽象层。
它提供的标准化内容包括:
- 访问 NVIC、SCB(System Control Block,系统控制块)、SysTick 的中断控制与初始化函数;
- NVIC、SCB、SysTick 寄存器的标准化定义;
- Cortex-M 特殊指令的标准化函数;
- 系统异常的标准化命名;
- 系统初始化函数统一命名为
void SystemInit(void); - 时钟频率信息变量统一命名为
SystemCoreClock。
CMSIS 分为 4 层:
- 核心外设访问层:核心寄存器、NVIC、SCB、SysTick 的命名与访问;
- 中间件访问层:面向 USART、Ethernet、SPI 等通信接口的通用访问方法,提升跨芯片可移植性;
- 设备外设访问层(MCU 相关):具体 MCU 的寄存器定义与设备驱动;
- 外设访问函数(MCU 相关):可选的外设辅助函数。
CMSIS 已集成在芯片厂商的设备驱动包里——你只要用了设备驱动库,就已经在用 CMSIS 了。
六、固件库
6.1 寄存器直接操作与固件库的取舍
- 直接操作寄存器:如
GPIOx->BRR = 0x11;,简单直接,但 STM32 寄存器极多,记起来麻烦; - 固件库(Firmware Library):函数的集合,向下负责直接操作寄存器,向上提供用户调用的接口(API,Application Programming Interface,应用程序编程接口)。
固件库把底层寄存器操作都封装起来,大多数场合你不需要知道操作的是哪个寄存器,只管调函数。所有驱动源码符合 ANSI-C 标准,不受开发环境影响,能大幅减少写代码时间、降低开发成本。
弊端:库函数是通用的,会包含全部外设操作,影响代码大小和执行速度;对体积 / 速度有严格要求时,可改用直接寄存器操作。此外固件库带实时错误检测(校验输入值),调试时好用,最终产品代码里可移除以优化。
6.2 命名规则 PPP_Action
固件库最精妙的设计之一是统一命名:PPP_Action,其中PPP是外设缩写(如GPIO、TIM、USART),Action是操作(如Init、SetBits)。整个函数名只允许一个下划线。
6.3 库函数分类(以 TIM 为例)
| 类别 | 函数 | 作用 |
|---|---|---|
| 初始化配置 | PPP_Init() | 按结构体参数初始化外设 |
PPP_DeInit() | 外设所有寄存器恢复默认值 | |
PPP_StructInit() | 用默认值填充初始化结构体 | |
| 使能控制 | PPP_Cmd() | 总开关,使能 / 失能整个外设 |
PPP_DMAConfig() | 配置外设的 DMA 功能 | |
| 中断与事件 | PPP_ITConfig() | 配置(使能 / 失能)特定中断源 |
| 状态标志 | PPP_GetFlagStatus() | 检查事件标志位是否置位 |
PPP_ClearFlag() | 清除事件标志位 | |
PPP_GetITStatus() | 检查特定中断是否发生 | |
PPP_ClearITPendingBit() | 清除中断挂起标志 |
命名规律:Config结尾一般是对某项功能配置;Get是读取 / 检查状态;Clear是清除标志。
以 GPIO 为例体会三个函数的区别:
| 函数 | 作用 | 类比 |
|---|---|---|
GPIO_DeInit() | 复位端口所有寄存器为默认值 | 把房子恢复成毛坯房 |
GPIO_StructInit() | 给结构体填一组安全默认值 | 拿到标准户型图 |
GPIO_Init() | 按你改好的结构体实际配置硬件寄存器 | 按方案施工 |
6.4 变量编码规则与常用类型
固件库在stm32f10x_type.h里定义了常用类型:
typedefsignedlongs32;typedefsignedshorts16;typedefsignedchars8;typedefenum{FALSE=0,TRUE=!FALSE}bool;typedefenum{RESET=0,SET=!RESET}FlagStatus;// 标志位:置位 / 复位typedefenum{DISABLE=0,ENABLE=!DISABLE}FunctionalState;// 功能:使能 / 失能typedefenum{ERROR=0,SUCCESS=!ERROR}ErrorStatus;// 错误:成功 / 出错外设结构体与外设声明(以 SPI 为例):
typedefstruct{vu16 CR1;u16 RESERVED0;vu16 CR2;u16 RESERVED1;/* ... */}SPI_TypeDef;#definePERIPH_BASE((u32)0x40000000)#defineAPB2PERIPH_BASE(PERIPH_BASE+0x10000)#defineSPI1_BASE(APB2PERIPH_BASE+0x3000)#defineSPI1((SPI_TypeDef*)SPI1_BASE)这里的vu16同样是基于volatile的「易变无符号 16 位」,和前面__IO是同一思路——外设寄存器必须防优化。
本章小结与思考题
核心知识地图:
- 开发流程与工具链(编译器 / 汇编器 / 链接器 / 模拟器 / Flash 编程器 / 调试器;Keil MDK + ST-LINK V2);
- 处理器启动过程(片上 Flash 与 BootROM、复位流程、启动代码 5 步、应用程序 3 部分);
- 输入输出接口(外设概念与初始化 4 步);
- 程序映像(向量表 + C 启动代码 + 程序代码 + C 库代码);
- C 语言开发 ARM 应用(数据类型 / 10 类运算符 / 流程控制 / 外设寄存器映射与
volatile/ CMSIS); - 固件库(寄存器直操 vs 库函数、
PPP_Action命名、函数分类、变量编码规则)。
本文由课程课件与讲稿整理,供学习交流使用。如需转载或用于教学,请注明出处。欢迎在评论区交流讨论。