☰
微机原理与接口技术 · 第5章《ARM 微控制器开发》知识点全梳理
2026/10/3 2:39:17 网站建设 项目流程

微机原理与接口技术 · 第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 后,处理器复位就会运行「复位流程」:

  1. 处理器取出MSP(主堆栈指针)的初始值;
  2. 取出复位向量(复位处理程序的入口地址);
  3. 开始执行复位处理。

这些信息都放在一个叫启动代码的程序文件里。

对于 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 块组成:

  1. 硬件初始化:时钟、PLL、外设等;
  2. 应用程序的处理部分:你真正想让芯片做的事;
  3. 中断服务程序:响应外部事件。

如果用了 C 库函数,编译器 / 链接器会把所需的库代码自动纳进程序映像。


三、输入和输出接口

嵌入式系统里可用的 I/O 五花八门:数字和模拟输入 / 输出、UART、I2C、SPI,很多微控制器还提供 USB、以太网、CAN、LCD、SD 卡等接口——这些接口都由**外设(peripheral)**控制。

外设的典型初始化过程一般分 4 步:

  1. 配置时钟控制回路:使能外设的时钟信号,必要时初始化相应引脚;
  2. 配置 I/O:引脚功能配置、电气特性配置;
  3. 配置外设:控制寄存器配置;
  4. 配置中断:配置 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 里,整型按长度分:

类型说明符长度
基本型int2 字节
短整型short int/short2 字节
长整型long int/long4 字节
无符号基本型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 类运算符:

  1. 算术:+ - * / % ++ --
  2. 关系:> < == >= <= !=
  3. 逻辑:&& || !
  4. 位操作:& | ~ ^ << >>
  5. 赋值:= += -= ...
  6. 条件:? :
  7. 逗号:,
  8. 指针:*取内容、&取地址
  9. 求字节:sizeof
  10. 特殊:括号()、下标[]、成员->.

算术运算符

共 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 层:

  1. 核心外设访问层:核心寄存器、NVIC、SCB、SysTick 的命名与访问;
  2. 中间件访问层:面向 USART、Ethernet、SPI 等通信接口的通用访问方法,提升跨芯片可移植性;
  3. 设备外设访问层(MCU 相关):具体 MCU 的寄存器定义与设备驱动;
  4. 外设访问函数(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是同一思路——外设寄存器必须防优化。


本章小结与思考题

核心知识地图:

  1. 开发流程与工具链(编译器 / 汇编器 / 链接器 / 模拟器 / Flash 编程器 / 调试器;Keil MDK + ST-LINK V2);
  2. 处理器启动过程(片上 Flash 与 BootROM、复位流程、启动代码 5 步、应用程序 3 部分);
  3. 输入输出接口(外设概念与初始化 4 步);
  4. 程序映像(向量表 + C 启动代码 + 程序代码 + C 库代码);
  5. C 语言开发 ARM 应用(数据类型 / 10 类运算符 / 流程控制 / 外设寄存器映射与volatile/ CMSIS);
  6. 固件库(寄存器直操 vs 库函数、PPP_Action命名、函数分类、变量编码规则)。

本文由课程课件与讲稿整理,供学习交流使用。如需转载或用于教学,请注明出处。欢迎在评论区交流讨论。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询