# 无线升级程序
**Repository Path**: smartdao/iap
## Basic Information
- **Project Name**: 无线升级程序
- **Description**: 本设计针对《第十六届智能车竞速规则》中规定可使用的主控制器以及STM32系列微控制器,设计可通过串口/蓝牙/WiFi的智能小车在线升级调试系统,编写了IAP程序、电脑端上位机软件程序和手机端软件程序。
关于IAP升级目前已经有比较成熟的方案,考虑到智能车竞赛使用的主控制器内存资源比较有限,本设计将使用尽可能简洁的方案适配各型号实现IAP升级。
关键词:IAP;在线升级;无线升级;远程升级
- **Primary Language**: C
- **License**: LGPL-3.0
- **Default Branch**: master
- **Homepage**: https://jswyll.com
- **GVP Project**: No
## Statistics
- **Stars**: 0
- **Forks**: 21
- **Created**: 2024-07-10
- **Last Updated**: 2024-07-10
## Categories & Tags
**Categories**: Uncategorized
**Tags**: None
## README
# 在线升级调试程序(一)
> IAP指In-Application-Programming,在应用程序内在线编程,即在程序内可以修改程序存储器,使得用户可以使用自由的逻辑更新程序,从而实现远程升级等功能。本设计针对《第十六届智能车竞速规则》中规定可使用的主控制器以及STM32系列微控制器,设计基于无线通信的智能小车在线升级、调试系统,为此,编写了各单片机的IAP程序、电脑端上位机软件程序和手机端软件程序。关于IAP升级目前已经有比较成熟的方案,考虑到智能车竞赛使用的主控制器内存资源比较有限,本设计将使用尽可能简洁的方案适配各型号实现IAP升级。
>
> 关键词:IAP;在线升级;无线升级;远程升级
>
> > 本文原文链接:
--------------------------------------------------------------------------------------------------
## 快速使用
> 本文所提及的“编程”的含义不是平常所说的“编写程序”的“编程”,而是“FLASH编程”,即修改FLASH存储器上的内容。
程序直接把自身所在的程序编程是不合理的,因为CPU正在从程序存储器读取指令来运行,这时编程新的程序将导致运行错乱。所以,IAP技术需要将程序存储器划出一块区域专门用来更新真正的用户程序,本文把划出来的区域称为IAP区(相当于BootLoader,启动引导区),真正的用户程序区域称为APP区。
#### 实现效果
修改代码并编译程序,单片机的程序即可通过有线或无线自动更新:

 
#### 程序托管地址
- 各型号单片机端代码:[https://gitee.com/jswyll_com/iap](https://gitee.com/jswyll_com/iap)
- 电脑端上位机软件`单片机调试助手`:[https://gitee.com/jswyll_com/mcu_assistant](https://gitee.com/jswyll_com/mcu_assistant)
#### 通信方式
可以使用USB转串口线有线升级,也可以或蓝牙或WIFI模块无线升级。它们都是使用单片机上的某一对串口通信的,升级所使用的串口编号、波特率和引脚见代码文件“iap.h”的宏定义,以STC8单片机为例:
```c
/** @brief IAP端口 */
#define IAP_UART UART_1
#define IAP_UART_BAUD 115200
#define IAP_UART_TX_PIN UART1_TX_P31
#define IAP_UART_RX_PIN UART1_RX_P30
#define IAP_UART_TIM TIM_2
```
- 方式一:WIFI模块
使用WiFi模块需要使模块和电脑处于同一局域网之下,不需要连接互联网就能相互通信,可以是以下情形之一:
(1) 电脑开启WiFi热点,WiFi模块连接该热点。这样的好处在于电脑可以正常连接互联网使用其它软件,不足之处在于win10系统设置不允许在未连接互联网的情况下开启WiFi热点。
(2) 电脑和WiFi模块连接到同一个外部WiFi之下。
(3) WiFi模块开启WiFi热点,电脑连接该热点。
这里使用方式(1),模块和上位机之间通过TCP协议通信。上位机作为服务端(Server),模块作为客户端(Client),上位机可以查看模块是否已和电脑连接上。操作步骤:
1. 第一次使用需要使用传统方式(STC-ISP,Jlink,DAP等方式)下载IAP工程,各型号的IAP工程在`xx型号/IAP/`文件夹下。
2. 修改原来的工程代码为APP区工程,主要作用是:修改代码链接生成的地址、收到IAP升级指令时跳转到IAP区运行、中断向量表重定向。可以在`xx型号/APP/`文件夹下的APP工程添加你的代码,也可以将现有的代码工程修改为APP工程,具体方法见各型号下的文件`README.md`介绍。
3. 电脑开启移动热点。上位机监听电脑上的某一端口,这里设置7778。

4. 购买ESP8266/ESP-01S模块,使用串口线连接WiFi模块,配置WiFi模块上电后自动连接上一步的热点、自动连接上一步的TCP服务端端口并进入透传模式,模块配置的AT指令集具体介绍见。所谓透传,是指两个设备在已连接的情况下,从单片机串口TXD发出给模块的数据,模块内部将数据发到手机或电脑上的WiFi,进而被手机或电脑的控制器接收,最终显示在软件界面上;同理,从手机或电脑发出的数据,最终发到单片机的RXD端而被接收。

其中,③中电脑热点的ip地址可在选择端口-TCP Server的地址栏下拉查看,打开电脑WiFi热点后该处新增的ip地址就是热点的地址。

5. 将WiFi模块的串口和单片机的IAP串口交叉连接,VCC、GND引脚接3.3\~5V电源,注意和单片机共地——两者的GND相连。将模块重新上电,模块将自动连接电脑。
6. 在上位机打开APP工程生成的hex文件,选择对应单片机的扇区大小,单击`手动升级`,单片机上的程序将更新。

此后再编译APP工程,程序可以自动升级。
- 方式二:USB转串口
1. 购买USB转串口模块,第一次使用USB转串口需要下载对应的驱动(CH340或PL2303)并安装。将模块插在电脑USB上,模块的TXD、RXD和单片机上的IAP串口交叉连接。
2. 打开上位机,选择并打开对应的串口,在上位机的`设置-波特率`,选择与代码文件“iap.h”中`IAP_UART_BAUD`相同的值。

其它步骤同方式一。
- 方式三:蓝牙
在支持蓝牙的笔记本电脑的Win10系统上,第一次需要打开“系统设置-蓝牙-添加蓝牙或其他设备”配对使用的蓝牙模块,配对密码通常是1234。配对后在“更多蓝牙选项-COM端口”可以看到对应蓝牙名称的“传出”端口号,对应前面选择端口下拉列表包含“蓝牙链接上的标准串行”号,打开即可和该蓝牙模块通信。在不支持蓝牙的电脑上,需要在电脑USB外接一个蓝牙主机转串口模块,主机和智能小车上的蓝牙从机配对,其效果是等同的。

模块接线和升级方法同方式一。
#### 不同升级方式时间对比
升级程序所需时间主要与通信时间、FLASH擦除和FLASH编程时间有关。使用不同方式测试多次,不同型号单片机平均消耗时间如下:
- STC8
完整升级5KB程序:
| 波特率 | STC-ISP | 有线串口IAP | WIFI模块 |蓝牙 |
| :--------------------- | :---------------- | :---------------- | :--------------: | :--------------: |
| 9600bps(11.52KB/s) | 8.063ms(0.6KB/s) | 6000ms(0.8KB/s) | 10700ms(0.5KB/s) | 7020ms(0.7KB/s) |
| 115200bps(11.52KB/s) | 1266ms(4.0KB/s) | 800ms(6.4KB/s) | 1658ms(3.1KB/s) | 1378ms(3.7KB/s) |
- RT1064
完整升级88KB程序:
| 波特率 | 有线串口IAP | WIFI模块 | 蓝牙 |
| :--------------------- | :---------------- | :---------------- | :--------------: |
| 115200bps(11.52KB/s) | 9100ms(9.9KB/s) | 11444ms(7.9KB/s)| 9859ms(9.1KB/s) |
| 921600bps(92.16KB/s) | 2370ms(38.0KB/s)| 4200ms(21.5KB/s)| 蓝牙硬件不支持 |
| 1152000bps(115.2KB/s) | 2100ms(42.9KB/s)| 3130ms(28.8KB/s)| 蓝牙硬件不支持 |
- MM32F3277
完整升级11KB程序:
| 波特率 | 有线串口IAP | WIFI模块 | 蓝牙 |
| :---------------------- | :--------------- | :---------------- | :--------------: |
| 115200bps(11.52KB/s) | 1546ms(7.3KB/s) | 3530ms(3.2KB/s) | 2150ms(5.2KB/s) |
| 921600bps(92.16KB/s) | 685ms(16.9KB/s) | 1555ms(7.2KB/s) | 蓝牙硬件不支持 |
> **特别说明**
>
> - 在升级APP程序的过程中,若出现意外导致APP程序未完整升级,APP程序将很可能无法正常运行,并且也无法自动复位到IAP区程序,此时先在上位机单击`手动升级`,然后手动按下单片机上的复位按键或重新上电,即可正常升级程序。
>
> - 将现有工程移植为APP工程后,可以使用Jlink等传统方式下载程序。“只更新新的扇区”功能是指连续使用IAP功能时,后一次只升级程序时只更新相对于前一次成功升级有变化的程序扇区,如果中间使用了传统方式下载程序,再使用IAP升级时请取消勾选“只更新新的扇区”一次。
--------------------------------------------------------------------------------------------------
--------------------------------------------------------------------------------------------------
--------------------------------------------------------------------------------------------------
以下为实现原理
## 1 程序编译、下载、运行过程分析
本章以意法半导体STM32F1系列下的STM32F103C8T6型号MCU(Microcontroller Unit,微控制器,俗称单片机)为研究对象,该MCU基于ARM Cortex-M3内核,与后面将研究的灵动微电子MM32F3277单片机相似。打开STM32 CubeMX软件[\[1\]](https://www.st.com/zh/development-tools/stm32cubemx.html "CubeMX软件-初始化代码生成器")新建工程,在“Part Number”输入该型号,即可看到相关资源:

图 1-1 CubeMX软件 - MCU选择器
该单片机有64KB(kilobyte,千字节)的FLASH和20KB的RAM,属于中等密度(medium-density)的。其中,FLASH的基地址(首地址)为0x08000000、RAM的基地址为0x20000000。使用CubeMX软件生成MDK IDE[\[2\]](https://www2.keil.com/mdk5 "ARM内核MCU的程序开发软件")(以下简称MDK)的代码,打开生成的工程,编写“main.c”文件代码:
代码段 1-1
```c
uint32_t g_ZI_bss2 = 0; // ⑬
uint8_t g_ZI_bss[68]; // ⑬
uint32_t g_RW_data = 0x789ABCDE; // ⑪
const uint8_t g_RO_data[16] = {0x00, 0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77}; // ⑧
int main(void) // ⑥
{
int i; // ⑭
HAL_Init();
SystemClock_Config();
MX_GPIO_Init();
MX_USART1_UART_Init();
printf("g_RW_data: 0x%X, ZI_bss2:%d\r\n", g_RW_data, g_ZI_bss2);
for(i = 0; i < sizeof(g_ZI_bss); i++)
{
printf("g_ZI_bss:0x%X ", g_ZI_bss[i]);
}
for(i = 0; i < sizeof(g_RO_data); i++)
{
printf("g_RO_data:0x%X ", g_RO_data[i]);
}
while (1)
{
HAL_GPIO_TogglePin(LED_GPIO_Port, LED_Pin);
HAL_Delay(500);
}
}
```
然后编译:

图 1-2 MDK - 编写测试程序
双击上图中的红框标记处或者手动打开工程所在路径下输出目录(对应`魔术棒图标Options - Output - Select Folder for Objects`)的xxx.map文件,可以看到内存分布描述文件。省略部分内容,并对关键部分做标号,如下:
代码段 1-2
```sh
==============================================================================
Memory Map of the image
Image Entry point : 0x080000ed
①------------------------------------------------------------------------
Load Region LR_IROM1 (Base: 0x08000000, Size: 0x000013b0, Max: 0x00004000, ABSOLUTE)
②----------------------------------------------------------------------
Execution Region ER_IROM1 (Exec base: 0x08000000, Load base: 0x08000000, Size: 0x00001394, Max: 0x00004000, ABSOLUTE)
Exec Addr Load Addr Size Type Attr Idx E Section Name Object
③----------------------------------------------------------------------
0x08000000 0x08000000 0x000000ec Data RO 3 RESET startup_stm32f103xb.o
④----------------------------------------------------------------------
0x080000ec 0x080000ec 0x00000000 Code RO 622 * .ARM.Collect$$$$00000000 mc_w.l(entry.o)
0x08000100 0x08000100 0x00000024 Code RO 4 .text startup_stm32f103xb.o
...
⑤----------------------------------------------------------------------
0x08000210 0x08000210 0x00000002 Code RO 45 .text.BusFault_Handler stm32f1xx_it.o
0x08000214 0x08000214 0x00000002 Code RO 51 .text.DebugMon_Handler stm32f1xx_it.o
0x08000c54 0x08000c54 0x00000002 Code RO 41 .text.HardFault_Handler stm32f1xx_it.o
0x08000d40 0x08000d40 0x00000002 Code RO 49 .text.SVC_Handler stm32f1xx_it.o
0x08000d74 0x08000d74 0x00000008 Code RO 55 .text.SysTick_Handler stm32f1xx_it.o
0x08000f14 0x08000f14 0x00000002 Code RO 47 .text.UsageFault_Handler stm32f1xx_it.o
⑥----------------------------------------------------------------------
0x08000f8c 0x08000f8c 0x000000c0 Code RO 11 .text.main main.o
...
⑦----------------------------------------------------------------------
0x0800104c 0x0800104c 0x00000020 Code RO 760 i.__0printf$5 mc_w.l(printf5.o)
0x0800106c 0x0800106c 0x0000000e Code RO 939 i.__scatterload_copy mc_w.l(handlers.o)
0x0800107a 0x0800107a 0x00000002 Code RO 940 i.__scatterload_null mc_w.l(handlers.o)
0x0800107c 0x0800107c 0x0000000e Code RO 941 i.__scatterload_zeroinit mc_w.l(handlers.o)
...
⑧----------------------------------------------------------------------
0x08001364 0x08001364 0x00000010 Data RO 27 .rodata.g_RO_data main.o
⑨----------------------------------------------------------------------
Execution Region RW_IRAM1 (Exec base: 0x20000000, Load base: 0x08001398, Size: 0x000004a8, Max: 0x00001800, ABSOLUTE)
⑩----------------------------------------------------------------------
Exec Addr Load Addr Size Type Attr Idx E Section Name Object
⑪----------------------------------------------------------------------
0x20000000 0x08001398 0x00000004 Data RW 902 .data mc_w.l(stdout.o)
0x20000004 0x0800139c 0x00000004 Data RW 612 .data.SystemCoreClock system_stm32f1xx.o
0x20000008 0x080013a0 0x00000004 Data RW 26 .data.g_RW_data main.o
0x2000000c 0x080013a4 0x00000001 Data RW 275 .data.uwTickFreq stm32f1xx_hal.o
⑫----------------------------------------------------------------------
0x2000000d 0x080013a5 0x00000003 PAD
0x20000010 0x080013a8 0x00000004 Data RW 274 .data.uwTickPrio stm32f1xx_hal.o
0x20000014 0x080013ac 0x00000004 PAD
⑬----------------------------------------------------------------------
0x20000018 - 0x00000044 Zero RW 28 .bss.g_ZI_bss main.o
0x2000005c - 0x00000004 Zero RW 25 .bss.g_ZI_bss2 main.o
...
⑭----------------------------------------------------------------------
0x200000a8 - 0x00000400 Zero RW 1 STACK startup_stm32f103xb.o
⑮--------------------------------------------------------------------------
Total RO Size (Code + RO Data) 5012 ( 4.89kB)
Total RW Size (RW Data + ZI Data) 1192 ( 1.16kB)
Total ROM Size (Code + RO Data + RW Data) 5032 ( 4.91kB)
==============================================================================
==============================================================================
Image Symbol Table
Symbol Name Value Ov Type Size Object(Section)
⑯----------------------------------------------------------------------
Reset_Handler 0x08000101 Thumb Code 8 startup_stm32f103xb.o(.text)
...
⑰----------------------------------------------------------------------
__initial_sp 0x200004a8 Data 0 startup_stm32f103xb.o(STACK)
```
其中,Object一栏的xxx.o是编译器把对应名字的.c源文件和.h头文件编译生成的中间文件。
### 存储器
存储器有两大类:
- ROM(Read Only Memory,只读内存)。ROM的内容断电后仍然保存,在正常使用过程中ROM是只读的,当今的ROM存储器(EEPROM、nor flash、nand flash等)支持在指定的情况下修改其中的内容。
- RAM(Random Access Memory,随机存取内存)。RAM是可读写的,读取速度相对于ROM快得多,但RAM断电后内容将丢失。
为了断电后下次还能使用原来的程序,程序应该存储到ROM而不是RAM里,对于单片机来说,这个ROM的类型通常是FLASH。
在数字电子技术中,所有数据均以二进制的形式存储。值得注意的是,修改ROM时只能把二进制位(bit,比特位,缩写为b)的1改变成0,而不能把0变成1,所以每次向ROM写入数据时应先把该地址原来的内容擦除(每一位都变成1),然后再写入新内容。写入的操作单位可以是1字节(byte,缩写为B)、半字(half-word)、一字(word)、双字(doble-word),它们分别对应8、16、32、64位,而擦除操作的最小单位通常是一个扇区(例如512字节)或者是一堆块(Bank,其中包含了多个扇区),如果待写入的新内容小于一个扇区,直接擦除并写入数据将导致该扇区内其它位置的内容将被清除。
### 内存分布
编译器把代码分成了几段:

图 1-3 程序内存分布
- Code,段名称(Section Name)为`.text.函数名`或`i.函数名`: 代码段,对应CPU指令。如标号④~⑦,是程序中的main等函数和引用到的printf等库函数的代码语句。这里的加载地址(Load Addr)和执行地址(Exec Addr)一样,表示程序直接从该地址运行,不需要把它拷贝到其它地方去运行。
- RO Data,段名称为`.rodata.常量名`:常量数据段,例如常量数组、字符串常量。如标号③⑧。
- RW Data,段名称为`.data.变量名`:有初始值的全局变量,如标号⑪。
- ZI Data,段名称为`.bss.变量名`:无初始值或者初始值为0的全局变量,如标号⑬。
- Heap:堆,可以被程序员使用malloc函数动态分配和释放的。这个大小在汇编启动文件startup_xx.s的`Heap_Size`中指定,本示例中序中没有使用,编译器移除了这段。
- STACK:栈,见标号⑭,在调用函数时临时传递参数或者开辟局部变量时使用,这是为了最大化利用RAM内存。栈同样是动态分配的,不过这个分配过程是由编译器处理好的。栈大小在汇编启动文件startup_xx.s的Heap_Size中指定,一般是0x400(1KB),栈是向下(由高地址向低地址)生长的:最开始时栈是空的,初始栈顶地址为最高地址,每当数据暂存进栈(Push)时栈顶地址减少,从栈弹出数据(Pop)时栈地址增加。
### 目标程序
在`MDK - Options - Output`勾选生成HEX文件`Create HEX File`。使用NotePad++或VS Code软件打开输出目录下的xxx.hex文件,可以看到目标程序描述文件:

图 1-4 hex文件内容
HEX格式文件是可以烧写到单片机中,被单片机执行的一种文件格式[\[3\]](https://baike.baidu.com/item/hex%E6%96%87%E4%BB%B6%E6%A0%BC%E5%BC%8F "hex文件格式")。上图中第1行表示接下来的地址的高16位是0x0800;第2行`:10`开头表示本行实际数据大小是0x10(16)个字节,`0000`表示本行数据的低16位地址,结合第1行,第2行的实际地址为`0x08000000`,`00`表示本行后面的是数据,`A804002001010008E10C0008550C0008`对应了地址0x08000000~0x0800000F共16个字节大小的数据内容,最后的`BC`表示本行的校验和;后面各行含义和第2行一样。
### 多字节存储模式
存储器每个地址上存放一个字节,对于多字节存储有小端模式(Little-Endian)和大端模式(Big-Endian)[\[4\]](https://baike.baidu.com/item/%E5%A4%A7%E5%B0%8F%E7%AB%AF%E6%A8%A1%E5%BC%8F "大小端模式"),本研究涉及的单片机除51单片机以外均为小端模式。所谓小端模式,是指高位字节存在高地址、低位字节存在低地址,例如程序中定义的`uint32_t g_RW_data = 0x789ABCDE;`,根据map文件标号⑪可知它存放的起始地址(加载地址)是0x080013a0,打开.hex文件,可以看到地址0x080013a0存的是0xDE、地址0x080013a3存的是0x78,这与定义`uint8_t g_RW_data[4] = { 0xDE, 0xBC, 0x9A, 0x78 };`存放方式相同。

图 1-5 hex文件 - RW Data
### 程序启动流程
单片机集成了CPU、存储器、定时器等功能。编译器生成的程序是机器码,CPU执行程序的过程就是不断的从存储器取指令和数据、译码、执行的过程。对于本例,由硬件配置了单片机复位后CPU从FLASH地址0x08000000启动。打开启动文件“startup_xxx.s”可以看到有一段:
代码段 1-3
```armasm
__Vectors DCD __initial_sp ; Top of Stack
DCD Reset_Handler ; Reset Handler
DCD NMI_Handler ; NMI Handler
DCD HardFault_Handler ; Hard Fault Handler
DCD MemManage_Handler ; MPU Fault Handler
DCD BusFault_Handler ; Bus Fault Handler
DCD UsageFault_Handler ; Usage Fault Handler
DCD 0 ; Reserved
DCD 0 ; Reserved
DCD 0 ; Reserved
DCD 0 ; Reserved
DCD SVC_Handler ; SVCall Handler
DCD DebugMon_Handler ; Debug Monitor Handler
DCD 0 ; Reserved
DCD PendSV_Handler ; PendSV Handler
DCD SysTick_Handler ; SysTick Handler
; External Interrupts
DCD WWDG_IRQHandler ; Window Watchdog
DCD PVD_IRQHandler ; PVD through EXTI Line detect
DCD TAMPER_IRQHandler ; Tamper
DCD RTC_IRQHandler ; RTC
DCD FLASH_IRQHandler ; FLASH
DCD RCC_IRQHandler ; RCC
DCD EXTI0_IRQHandler ; EXTI Line 0
DCD EXTI1_IRQHandler ; EXTI Line 1
DCD EXTI2_IRQHandler ; EXTI Line 2
DCD EXTI3_IRQHandler ; EXTI Line 3
DCD EXTI4_IRQHandler ; EXTI Line 4
DCD DMA1_Channel1_IRQHandler ; DMA1 Channel 1
DCD DMA1_Channel2_IRQHandler ; DMA1 Channel 2
DCD DMA1_Channel3_IRQHandler ; DMA1 Channel 3
DCD DMA1_Channel4_IRQHandler ; DMA1 Channel 4
DCD DMA1_Channel5_IRQHandler ; DMA1 Channel 5
DCD DMA1_Channel6_IRQHandler ; DMA1 Channel 6
DCD DMA1_Channel7_IRQHandler ; DMA1 Channel 7
DCD ADC1_2_IRQHandler ; ADC1_2
DCD USB_HP_CAN1_TX_IRQHandler ; USB High Priority or CAN1 TX
DCD USB_LP_CAN1_RX0_IRQHandler ; USB Low Priority or CAN1 RX0
DCD CAN1_RX1_IRQHandler ; CAN1 RX1
DCD CAN1_SCE_IRQHandler ; CAN1 SCE
DCD EXTI9_5_IRQHandler ; EXTI Line 9..5
DCD TIM1_BRK_IRQHandler ; TIM1 Break
DCD TIM1_UP_IRQHandler ; TIM1 Update
DCD TIM1_TRG_COM_IRQHandler ; TIM1 Trigger and Commutation
DCD TIM1_CC_IRQHandler ; TIM1 Capture Compare
DCD TIM2_IRQHandler ; TIM2
DCD TIM3_IRQHandler ; TIM3
DCD TIM4_IRQHandler ; TIM4
DCD I2C1_EV_IRQHandler ; I2C1 Event
DCD I2C1_ER_IRQHandler ; I2C1 Error
DCD I2C2_EV_IRQHandler ; I2C2 Event
DCD I2C2_ER_IRQHandler ; I2C2 Error
DCD SPI1_IRQHandler ; SPI1
DCD SPI2_IRQHandler ; SPI2
DCD USART1_IRQHandler ; USART1
DCD USART2_IRQHandler ; USART2
DCD USART3_IRQHandler ; USART3
DCD EXTI15_10_IRQHandler ; EXTI Line 15..10
DCD RTC_Alarm_IRQHandler ; RTC Alarm through EXTI Line
DCD USBWakeUp_IRQHandler ; USB Wakeup from suspend
__Vectors_End
```
汇编指令DCD用于定义一个32位的数据。根据ARM Cortex-M架构,芯片启动时会读取启动地址的前4个字节(32位)并设置它为RAM的初始栈顶地址。根据map文件,本示例程序的栈顶地址是0x200004a8(标号⑰),它存放的基地址是FLASH基地址0x08000000,回看hex文件第2行`:10000000A804002001010008E10C0008550C0008BC`其中对应0x08000000~0x08000003的是`A8040020`,以小端模式的视角去看就是32位的0x200004a8。紧接着的是N个中断向量表,所谓中断向量表,就是一个个中断函数所在地址的列表。此型号单片机有15个系统内部中断(由ARM Cortex-M3架构规定)和43个外部中断(由单片机厂商设计),加上1个栈顶地址,占用的大小是`(1 + 15 + 43) × 4 = 236 (0xec)`,这与代码段1-2的标号③对应。其中,中断向量表的第一个是复位中断向量Reset_Handler,参见代码段1-2的标号⑯,它的值是0x08000101,这与hex文件第一行中的`01010008`对应,这个值代表了Reset_Handler函数的实际地址,是整个程序的入口地址。Reset_Handler函数也定义在启动文件里:
代码段 1-4
```armasm
; Reset handler
Reset_Handler PROC
EXPORT Reset_Handler [WEAK]
IMPORT __main
IMPORT SystemInit
LDR R0, =SystemInit
BLX R0
LDR R0, =__main
BX R0
ENDP
```
它调用了两个函数:`SystemInit`系统时钟初始化函数,负责配置单片机运行的主频,定义在“system_xxx.c”文件中;初始化相关环境的内部__main函数(比如初始化printf等函数库),这个函数由编译器实现,它最后调用了main函数。
在代码里定义的变量是可读写的,这决定了它们要存放在RAM。仔细观察代码段1-2的标号⑪⑫,注意到它们的加载地址位于FLASH,而执行地址位于RAM。程序只下载到了FLASH里,而RAM的数据是断电不保存的,那么代码中有初始值的全局变量(RW Data)的初始值是怎么得到的呢?
是内部__main函数中的__scatterload_copy函数从FLASH拷贝到RAM的,见代码段1-2的标号⑦的函数,这个函数在源码中看不到,但可以在MDK单击调试(快捷键F5),进入反汇编串口输入地址或函数名查看汇编代码:

图 1-6 反汇编窗口

图 1-7 跳转到指定地址的反汇编代码

图 1-8 __scatterload_copy函数的汇编代码
参考ARM汇编指令,分析该汇编代码:
代码段 1-5
```armasm
B 0x08001074 ; 0x0800106C: 跳转到0x08001074地址去执行
LDM r0!,{r3} ; 0x0800106E: 将r0寄存器值的地址的内容加载到r3寄存器中,然后把r0加4
SUBS r2,r2,#4 ; 0x08001070: r2减4
STM r1!,{r3} ; 0x08001072: 将r3寄存器的值存储到r1寄存器值的地址中,然后把r1加4
CMP r2,#0x00 ; 0x08001074: 比较r2寄存器与0
BNE 0x0800106E ; 0x08001076: 不相等则跳转到0x0800106E地址去执行
BX lr ; 0x08001078: 程序返回
```
> ARM汇编调用子程序时,当参数小于4个时,调用子程序前将参数按顺序存入`r0~r3`寄存器中;使用这4个寄存器不必恢复和保存它的值[\[5\]](https://blog.csdn.net/fivedoumi/article/details/50446493 "ARM汇编子程序参数传递")。
编译器编译时会得到RW Data段的大小,RAM和ROM的地址则在`MKD - Options - Target`或链接脚本中设置。可以猜测,汇编代码在调用`__scatterload_copy`函数前将FLASH中的RW Data的基地址赋给了r0、RAM中的RW Data基地址赋给r1、RW Data的大小赋给了r2,如此调用该函数即可将RW Data从FLASH加载到RAM中。
假如用C语言表达,大概是:
代码段 1-6
```c
#include
#define _data_start 0x20000000
#define _data_load_start 0x08001394
#define _data_size 28
memcpy(_data_start, _data_load_start, _data_size);
```
其中,`memcpy`函数的作用是将源地址(第二个参数)起的N个字节(第三个参数)的值拷贝到目标地址(第一个参数),`_data_start`、`_data_load_start`、`_data_size`分别代表RAM中RW Data的基地址、FLASH中的RW Data基地址、RW Data的大小,它们的值由编译器生成,对于本例它们的实际值分别是0x20000000、0x08001394、28。
注意到代码段1-2的标号⑬⑭没有加载地址,根据代码的逻辑,ZI Data、Heap、STACK段的数据在使用时直接引用RAM的指定地址就可以,确实没有必要从FLASH中加载。根据C语言规则,未赋值的全局变量的初始值应为0,__main函数里还有一个__scatterload_zeroinit函数,它负责将RAM中的ZI Data段清零,同理,如果用C语言表达:
代码段 1-7
```c
#include
#define _bss_start 0x20000018
#define _bss_size 144
memset(_bss_start, 0, _bss_size);
```
`memset`函数的作用是将某个地址(第一个参数)起的N个字节(第三个参数)置为第二个参数的值。
程序运行有两条线路:
- main线路:由Reset_Handler函数启动,最终运行到main函数里的死循环;
- 中断:打开了对应中断开关并且触发相应中断后,CPU将暂停main,从中断向量表中读取对应中断向量(实际中断函数的地址),然后执行对应的中断函数,最后回到main继续执行。例如硬件错误的中断向量固定在启动地址的第3个字节(从0数起),偏移地址0x0C,即0x0800000C,见代码段1-2的标号⑤HardFault_Handler函数存在0x08000c54,这与hex文件第2行`:10000000A804002001010008E10C0008550C0008BC`中的`550C0008`对应,不过这两者的地址之间相差了1,这是因为这是Thumb Code模式[\[6\]](https://baike.baidu.com/item/Thumb/63166 "ARM体系结构Thumb指令")。

图 1-9 程序运行线路
### 程序下载
将程序下载到单片机上的常用方式有三种:
- 使用Jlink、DAP等调试器下载:调试通过单片机上的JTAG或SW引脚,连接单片机内部集成的调试单元,调试单元的功能之一是修改单片机上的FLASH内容;
- ISP(In-System Programming),在系统内编程:例如STC、CH32V103xx单片机,在芯片刚启动时,单片机厂商内部固化的系统程序检测UART、SPI、IIC等接口上是否有下载程序的指令,如果有就按照指定的数据协议操作程序存储器,否则跳转到用户程序区运行;
- IAP,在应用程序内编程:提供修改FLASH的程序接口,允许在用户程序内自由修改程序。
使用CubeMX软件新建工程时在`Docs & Resources`一栏的编程手册(Programming manual)

图 1-10 CubeMX软件 - 编程手册
可以看到STM32F10xxx系列中的 FLASH module organization (medium-density devices):

图 1-11 STM32F10xxx系列中等密度的FLASH模块组织
这里的Page即为扇区,这说明这个型号的单片机每个扇区的大小都是1KB。
打开MDK的`Options - Debug - Settings - FLASH Download`查看下载算法,默认选项是:
- 只擦除程序覆盖的扇区(Erase Sectors),因为普通情况下单片机不会使用到其它扇区;
- 编程(Program);
- 编程后验证(Verify),编程后从FLASH读出内容与需写入的内容相对比是否一致,这个选项不是必要的。

图 1-12 FLASH下载算法
由于擦除ROM的最小单位是一个扇区,因此擦除某个扇区时选择该扇区上的任一个地址的效果是一样的,一般选择该扇区的基地址。编程和读出的最小操作单位一般可以是1/2/4/8个字节。
前面编译生成了5040字节即4.9KB的程序,根据ROM存储特点,下载过程至少需要操作5KB大小的FLASH空间,它的下载过程应是:
1. 擦除FLASH上的0x08000000至0x0x080013ff共5KB的空间。对于该型号,这个操作的单位是一个扇区1KB,将擦除`0x08000000`、`0x08000400`、`0x08000800`、`0x08000C00`和`0x08001000`五个地址;
2. 将总共5040字节的程序编程到FLASH;
3. 从FLASH读出数据验证是否已正确写入。
使用Jlink或其它调试器连接单片机,单击MDK的程序下载按钮,观察状态栏验证:

图 1-13 FLASH擦除

图 1-14 FLASH编程

图 1-15 FLASH验证
--------------------------------------------------------------------------------------------------
## 2. 设计方案
本设计以笔记本电脑或手机软件为上位机,基于蓝牙或WiFi的无线通信,以智能车上的单片机为下位机。

图 2-1 通信框图
### 2.1 基本思路
IAP技术使得在程序中可以自由地修改程序,“自由”意味着可以随时擦除、编程、读取FLASH中的内容。但并不能随意地修改,不正确地修改会导致程序无法正常运行;程序不应该修改该程序自身所在的扇区,因为CPU是存储器中逐一取指令并运行的,修改FLASH的过程中需要擦除扇区从而导致正在运行的程序被擦除。
编译器生成的hex文件是文本模式的,比二进制模式占用的大得多,例如上一章的hex文件,实际程序占用的空间是4.9KB,但hex描述文件占用了14KB,如果上位机直接将hex文件发给单片机,单片机需要从hex文件解析出各个目标地址所对应的内容,比较复杂;由于单片机内部集成的RAM通常比ROM小得多,而接收的内容是需要先缓存在RAM里的,因而上位机不能将整个程序一次性下发到单片机。
为了实现IAP升级一般把程序存储器分成两个区域:
- APP区,这块区域运行真正的用户程序区;
- IAP区,即手机或电脑中常说的BootLoader(程序启动引导区),这个区域的程序负责更新APP区的程序,更新完成后跳转到APP区运行。
总流程设计为:
(1) 单片机平时运行在APP区。升级时,上位机不断发送“准备升级”指令,单片机收到后从APP区跳转到IAP区,然后应答上位机;
(2) 上位机发送“擦除”、“编程”等指令,单片机的IAP区程序调用该单片机的IAP接口,实现修改APP区的程序;
(3) 单片机在IAP区完成升级后,跳转到APP区运行更新后的程序。
根据ROM存储器的特点,调试器或ISP的下载算法是先擦除待修改的全部扇区,然后分多段编程FLASH。实际上,可以只擦除内容有变化的扇区并重新编程那些扇区,从而加快升级进程。本设计对于FLASH上扇区大小一样的单片机,上位机对目标程序每一个有变化的扇区,发送“擦除并编程扇区”指令包,该指令包包含了目标扇区地址和整个目标扇区的数据内容。
### 2.2 程序分区
以STC8A8K64S4A12/STC8H8K64U单片机为例,采用IAP技术,将64KB程序存储区人为划分为两块,IAP区初步规划大小为16KB。正常情况下单片机运行APP区程序,当APP区程序需要更新时跳转到IAP区、更新完成后跳回APP区运行。存储器分区有以下两种布局方案:
(1) APP区在储存器首端,IAP区在末端;

图 2-2 分区方案(1)
(2) IAP区在储存器首端,APP区在末端。

图 2-2 分区方案(2)
无论是IAP区还是APP区程序,从FLASH拷贝RW Data和清零ZI Data的操作编译器已经完成。
方案(1)的好处在于APP区代码不需要作任何改动;由于发生中断后单片机硬件是从存储器地址首端的中断向量表去寻找对应的中断函数的,因此方案(2)需要在APP区程序运行时将中断向量映射到APP区,实现起来较为复杂。
然而在设计实验过程中发现,使用方案(1)如果在IAP区更新APP区程序的过程中,出现传输中断等异常导致APP区程序未完整编程,复位单片机后程序将运行错乱,即俗称的“跑飞”,并且也无法再跳转到IAP区,这是因为单片机硬件复位后总是从存储器首端地址开始运行程序。这就要求方案(1)每次运行在IAP区时,必须保证编程APP区是准确无误的才能跳回APP区。
综上所述,选择分区布局方案(2),因此,针对各型号的单片机做中断重映射是本设计的重难点之一。
### 2.3 通信协议
上位机和下位机需要自定义一个通信协议,规定各个字节数据的含义,例如以文本模式还是hex模式表示数值、哪些字节代码目标地址、哪些字节代表扇区数据内容等等。
升级程序时上位机先发送请求升级程序的指令,下位机跳转到IAP区然后应答;接着上位机以一个扇区为单位发送程序扇区数据包,下位机收到后擦除该扇区并编程新程序然后应答上位机。本文将上位机发出的数据包称为指令包,下位机回应给上位机的数据包称为应答包。
除特殊说明外,多字节的部分是小端模式。数值以hex模式表示。
#### 指令包、应答包格式
表 2-1 数据包格式
| Byte(s) | 作用 | 说明 |
| :------ | :------ | :------ |
| 0-1 | 帧头 | 固定,指令包`5A A5`;应答包Byte0、Byte1分别为`A5 5A` |
| 2-3 | 包长 | 本次整个数据包的长度 |
| 4-5 | 检验和 | 从Byte6算起,各个字节的算术和 |
| 6 | 指令码 | 见[指令码](#指令码) |
| 7 | 参数 | 作为指令包时,为Byte6的异或值;作为应答包时,为[状态码](#状态码) |
| 8- | 数据内容 | 编程目标扇区基地址、扇区数据等 |
#### 指令码
表 2-2 指令码
| 数值 | 意义 |
| :--- | :--------------- |
| 0x01 | 准备升级 |
| 0x02 | 擦除并编程扇区 |
| 0x03 | 擦除一个扇区 |
| 0x04 | 擦除指定区域 |
| 0x05 | 编程指定区域 |
其中,大多数型号单片机只使用“准备升级”和“擦除并编程扇区”指令,指令0x03\~0x05只在扇区大小不一的单片机上使用。对于“擦除并编程扇区”指令包,8\~11字节为“擦除并编程”的扇区的基地址,12\~15字节暂时不用,16\~N字节为该扇区的数据内容(不足扇区大小的部分以0xFF填充)。
#### 状态码
表 2-3 状态码
| 数值 | 意义 |
| :--- | :--------------------- |
| 0x00 | 成功 |
| 0x01 | 失败 |
| 0x02 | 设备正忙 |
| 0x03 | 编程数据校验失败 |
| 0x04 | 未知指令 |
| 0x05 | 操作地址非法或未对齐扇区 |
| 0x06 | 数据包校验错误 |
| 0x07 | 擦除目标扇区失败 |
| 0x08 | 数据包大小异常 |
| 0x09 | 编程目标扇区失败 |
#### 协议示例
表 2-4 通信协议示例
| 功能 | 指令包 | 操作结果为成功的应答包 |
| :---------------------- | :---------------------------------------------------------------------------------- | :------------------- |
| 上位机提示下位机准备升级 | 5A A5 08 00 FF 00 01 FE | A5 5A 08 00 01 00 01 00 |
| 擦除并编程扇区0x08004000 | 5A A5 xx xx xx xx 02 FD 00 40 00 08 xx xx xx xx ...(...为扇区数据,字节数为扇区大小) | A5 5A 08 00 02 00 02 00 |
注:表中的指令包、应答包左数第一个字节为Byte0,以HEX形式表示。
协议设计说明:
- 帧头用于过滤掉无关数据,例如WiFi模块的打印信息;
- 包长便于单片机接收时使用一个计数解析数据包而无需额外的缓冲区,接收达到该长度就调用数据包处理函数;
- 检验和用于校验物理传输是否正确;
- FLASH编程时调用的接口函数可能需要将连续的8个字节转换为64位数值,单片机读取N字节时转换为16/32/64为数值时,访问N字节对齐的地址的数据更快[\[7\]](https://blog.csdn.net/lgouc/article/details/8235471 "为什么要内存对齐"),而访问不对齐的地址可能产生硬件错误。那么如果定义一个8字节对齐数据包缓冲区,数据内容的32位操作地址Byte8\~11是4字节对齐的,扇区数据Byte16\~N是8字节对齐的。
--------------------------------------------------------------------------------------------------
## 3 硬件系统设计
本设计基于无线通信。作为上位机,现在的笔记本电脑和手机绝大多数都有蓝牙和WIFI,不需要额外的硬件;作为下位机,智能车上的单片机需要外接蓝牙模块或WIFI模块,模块和单片机之间通过串口通信。因此,只需要在原有智能车设计的电路板上引出单片机的一对串口和模块供电接口(3.3~5.0V)。
- 蓝牙模块
使用蓝牙从机模块,一般来说,使用时只需要接其中的4个引脚:VCC、GND接3.6\~6V直流电源,RXD、TXD分别接单片机某一对串口的TXD、RXD引脚,在电路板上引出4PIN的排母即可。

图 3-1 蓝牙模块
蓝牙从机模块指示灯快闪时表示未被连接,这时可以通过串口根据AT指令协议配置它的名称、连接密码、波特率等等;当模块被其它蓝牙主机设备连接时,指示灯慢闪或常亮,这时蓝牙模块处于透传模式。
如下图所示,实线箭头表示导线连接,虚线箭头表示空中传输。所谓透传,是指两个蓝牙设备在已连接的情况下,从单片机串口TXD发出给蓝牙模块的数据,蓝牙模块内部将数据发到手机或电脑上的蓝牙,进而被手机或电脑的控制器接收,最终显示在软件界面上;同理,从手机或电脑发出的数据,最终发到单片机的RXD端而被接收。

图 3-2 蓝牙透传逻辑图
- WiFi模块
使用ESP8266串口WIFI模块系列下的ESP-01S模块,该模块支持WIFI站点STA+热点AP模式,支持IEEE802.11b、IEEE802.11g、IEEE802.11no无线标准,支持TCP、UDP、HTTP、FTP网络协议,可二次开发。

图 3-3 WiFi模块
电路板设计同蓝牙模块,只是PCB封装略有不同。
- 电路板设计
使用立创EDA软件绘制电路板。本示例中使用逐飞科技的MM32F3277核心板,在原有智能车电路的基础上增加了串口WIFI模块和蓝牙模块的接口,分别连接了该单片机的UART4的C10、C11引脚和UART8的D0、D1引脚。

图 3-4 原理图
将原理图转PCB(Printed circuit board,印制电路板),布局各元件并布线,得到PCB设计图。

图 3-5 PCB
焊接电路板的各元件并连接各模块,车模最终实物如下图。

图 3-6 实物图
--------------------------------------------------------------------------------------------------
## 4 IAP程序设计
IAP程序需要建立两个代码工程:
(1) APP区工程,收到“准备升级”指令包时跳转到IAP区。根据设计方案,APP区的任务是在接收到`5A A5 08 00 FF 00 01 FE`(HEX)后复位程序(相当于跳转到了IAP区)。
(2) IAP区工程,接收升级数据包并更新APP区程序;更新完后跳转到APP区运行。
新建“iap.c”源文件实现IAP升级的各个操作,IAP区和APP区都使用这个源文件,在IAP工程内定义宏`IN_IAP_AREA`以区别于APP工程。为了适配各个型号的单片机,将程序分成2层,函数调用关系如下图:

图 4-1 IAP程序框架
(1) IAP数据层,在图中为矩形边框。IAP数据层代码适用于全部型号,负责接收、处理升级指令包并应答上位机,函数以“iap_”开头:
代码段 4-1
```c
/**
* @brief IAP操作结果
*/
typedef enum iap_status
{
iap_ok = 0, /**< 成功 */
iap_error = 1, /**< 错误 */
iap_busy = 2, /**< 设备正忙 */
iap_verify_data_error = 3, /**< 编程后数据校验错误 */
iap_unKnowCmd_error = 4, /**< 未知指令 */
iap_check_addr_error = 5, /**< 操作地址非法或未对齐扇区 */
iap_check_sum_error = 6, /**< 数据包校验错误 */
iap_erase_sector_error = 7, /**< 擦除目标扇区失败 */
iap_package_size_error = 8, /**< 数据包大小异常或扇区大小选择错误 */
iap_program_sector_error = 9 /**< 编程目标扇区失败 */
} iap_status_t;
/** @brief IAP数据包相关变量 */
uint16_t iap_packetLen;
uint16_t iap_readIndex;
/* ① */
uint8_t __ALIGNED(8) iap_packet[IAP_BUFFER_SIZE];
enum cmd_iap
{
cmd_iap_start = 0x01, /**< 准备升级 */
cmd_iap_eraseProgramSector = 0x02, /**< 擦除并编程扇区 */
cmd_iap_eraseSector = 0x03, /**< 擦除一个扇区 */
cmd_iap_eraseRegion = 0x04, /**< 擦除指定区域 */
cmd_iap_pragramRegion = 0x05 /**< 编程指定区域 */
};
/** @brief 指令包帧头 */
static const uint8_t IAP_CMD_HEADERS[2] = {0x5A, 0xA5};
/**
* @brief IAP功能初始化
*
*/
void iap_init(void)
{
/* ② */
iap_readIndex = 0;
iap_hw_init();
}
/**
* @brief 发送应答包,回应处理结果
*
* @param status 指令包处理结果
*/
void iap_replyPacket(iap_status_t status)
{
uint16_t checksum = iap_packet[6] + status;
iap_putchar(0xA5);
iap_putchar(0x5A);
iap_putchar(0x08);
iap_putchar(0x00);
iap_putchar((uint8_t)checksum);
iap_putchar((uint8_t)(checksum >> 8));
iap_putchar(iap_packet[6]);
iap_putchar(status);
}
/**
* @brief 处理IAP指令包
*
* @return 操作结果
*/
iap_status_t iap_handlePacket(void)
{
uint16_t i, checksum = 0;
for (i = 6; i < iap_packetLen; i++)
{
checksum += iap_packet[i];
}
if (iap_packet[4] + (iap_packet[5] << 8) != checksum)
{
return iap_check_sum_error;
}
if (iap_packet[6] == cmd_iap_start)
{
/* ③ */
#ifdef IN_IAP_AREA
return iap_ok;
#else
iap_hw_reset();
#endif
}
else if (iap_packet[6] == cmd_iap_eraseProgramSector)
{
uint32_t addr = iap_packet[8] + (iap_packet[9] << 8) + (iap_packet[10] << 16) + (iap_packet[11] << 24);
return iap_hw_eraseProgramSector(addr, iap_packet + 12);
}
return iap_unKnowCmd_error;
}
/**
* @brief 读取IAP指令包
*
* @param ch 每次收到的一个字节
*/
void iap_readPacket(uint8_t ch)
{
iap_packet[iap_readIndex++] = ch;
if (iap_readIndex <= 2)
{
/* ④ */
if (ch != IAP_CMD_HEADERS[iap_readIndex - 1])
{
iap_readIndex = 0;
}
}
else if (iap_readIndex == 4)
{
/* ⑤ */
iap_packetLen = (iap_packet[3] << 8) + iap_packet[2];
}
else if (iap_readIndex == iap_packetLen)
{
/* ⑥ */
iap_replyPacket(iap_handlePacket());
iap_readIndex = 0;
}
if (iap_readIndex == 8 && iap_packetLen > IAP_BUFFER_SIZE)
{
/* ⑦ */
iap_replyPacket(iap_package_size_error);
iap_readIndex = 0;
}
}
```
使用变量`iap_readIndex`计数,它的值代表指令包读取索引——已接收到指令包中的有效字节数,约定从0计起。根据指令包协议,指令包读取(`iap_readPacket`函数)解析:
(1) 接收帧头(标号④):如果接收到的连续两个字节为`5A A5`则进入下一步,否则将计数变量置零。
(2) 接收指令包大小(标号⑤):接收到4个有效字节时,`Byte2 + Byte3*16`是该指令包的有效个数,将它保存在全局变量`iap_packetLen`。
(3) 接收完整的指令包(标号⑥):继续接收,直至计数变量`iap_readIndex`和指令包字节数`iap_packetLen`相等。
(4) 处理指令包(`iap_handlePacket`函数):验证校验和,然后根据指令码调用对应函数实现IAP升级。
对注释标号`/* 标号 */`作说明:
- 标号①:定义一个指令包缓存数组,`IAP_BUFFER_SIZE`的大小是`具体单片机的扇区大小+16`,由硬件层定义;`__ALIGNED(8)`表示把数组`iap_packet`放在RAM中8字节对齐的地址上
- 标号②:对于51单片机,RAM的data区只有128字节[\[8\]](https://blog.csdn.net/zy1049677338/article/details/56012239 "51单片机的data,idata,xdata,pdata的详解"),不足以保存一个指令包,本设计涉及的STC8系列的51单片机还有扩展的1\~8KB的xdata,可以通过设置将变量放在xdata区中,在不修改启动文件`STARTUP.A51`的情况下,xdata区的数据在单片机复位启动时不一定会被初始化为0;对于其它单片机,无初始值的全局变量启动时会被清零,这一句代码不是必要的。

图 4-2 设置51单片机的变量存放在XDATA区
- 标号③:收到“准备升级”指令包时,如果单片机处于APP区,就复位芯片,相当于跳转到了IAP区;如果已经在IAP区了,就发送应答包,指示上位机开始发送FLASH编程指令包。
- 标号⑦:指令包的字节数超出了指令包数组的大小,返回`指令包大小错误`码,提示上位机可能选错了扇区大小。
IAP数据层中,需要外部文件调用的函数接口有两个:调用`iap_init()`以启用IAP功能;`iap_readPacket()`函数的作用是根据协议获取指令包,需要在IAP串口(主函数轮询或串口中断)接收到一个字节时以该字节为参数调用。
(2) IAP硬件层,针对具体型号的单片机编写,负责实现擦除/编程/读取程序存储器、发送字符到IAP端口、复位芯片,函数以“iap_hw_”开头,在图中表示为实现圆角边框。
单片机复位启动后首先运行在IAP区,执行IAP硬件初始化`iap_hw_init()`函数后进入循环,等待升级指令包(`iap_hw_readPacket()`函数),使用一个变量计算等待的时长,只在收到升级指令包时才清零该变量,如果计数超过指定时长(0.5\~2s)则跳转到APP区运行,具体代码见各型号硬件层代码。
### 4.1 STC8系列
本节程序适用于宏晶科技STC8系列支持全片IAP(整块FLASH均可通过EEPROM接口编程)的单片机,例如智能车比赛推荐使用的两个型号STC8H8K64U、STC8A8K64S4A12,它们均是8KB RAM、64KB FLASH的全片IAP单片机。参考官方手册[\[9\]](https://www.stcmcudata.com/STC8F-DATASHEET/STC8H.pdf "STC8H系列手册")的`IAP/EEPROM/DATA-FLASH`章节可知,对于全片IAP单片机,在使用STC-ISP软件下载时设置用户EEPROM大小为FLASH大小可以使EEPROM操作的地址和FLASH的地址对应。

图 4-3 ISP下载时设置用户EEPROM大小
#### IAP 硬件层
STC8系列单片机兼容8051指令集,扇区大小为512字节,FLASH地址为0\~FLASH大小,复位后从地址0x0000开始运行。本例程划分IAP区程序空间为4KB(地址范围:0x0000\~0x1000),以串口1的P30和P31引脚作为IAP通信端口,使用Keil C51软件、基于逐飞科技STC8H开源代码库[\[10\]](https://gitee.com/seekfree/STC8H8K64_Library "逐飞科技STC8H8K64开源库")(以下简称逐飞库)开发,依照IAP程序框架图实现各硬件层函数:
代码段 4-2
```c
#include "iap.h"
#include "headfile.h"
/** @brief IAP端口 */
#define IAP_UART UART_1
#define IAP_UART_BAUD 115200
#define IAP_UART_TX_PIN UART1_TX_P31
#define IAP_UART_RX_PIN UART1_RX_P30
#define IAP_UART_TIM TIM_2
/** @brief FLASH扇区大小 */
#define IAP_SECTOR_SIZE 512
/** @brief APP区末端地址 */
#define APP_AREA_END_ADDR 0xFFFF
/** @brief IAP缓冲区大小 */
#define IAP_BUFFER_SIZE (IAP_SECTOR_SIZE + 16)
/** @brief APP区起始地址 */
const uint16_t APP_AREA_START_ADDR = 0x1000;
/** @brief IAP相关变量 */
uint8_t iap_packet[IAP_BUFFER_SIZE];
uint16_t iap_packetLen;
uint16_t iap_readIndex;
/* IAP硬件层 --------------------------------------------------------------------*/
uint8_t iap_recv_char;
uint32_t iap_wait_count = 0;
/**
* @brief 复位程序(跳转到IAP区)
*/
void iap_hw_reset(void)
{
IAP_CONTR = 0x60;
}
/**
* @brief 擦除并编程一个扇区
*
* @param addr 目标扇区基地址
* @param sectorData 扇区数据源地址
*
* @return iap_status_t 操作结果
*/
iap_status_t iap_hw_eraseProgramSector(uint32_t addr, uint8_t *sectorData)
{
/* 超时计数清零 */
iap_wait_count = 0;
/* 校验操作地址 */
if (addr < APP_AREA_START_ADDR || addr > APP_AREA_END_ADDR || addr % IAP_SECTOR_SIZE)
{
return iap_check_addr_error;
}
/* 擦除扇区 */
iap_erase_page(addr);
/* ① 校验擦除结果 */
if (IAP_CONTR & 0x10)
{
return iap_erase_sector_error;
}
/* 编程扇区 */
iap_write_bytes(addr, sectorData, IAP_SECTOR_SIZE);
return iap_ok;
}
/**
* @brief 读取一个扇区
*
* @param addr 目标扇区基地址
* @param sectorData 扇区数据存放目标地址
*
* @return iap_status_t 操作结果
*/
iap_status_t iap_hw_readSector(uint32_t addr, uint8_t *sectorData)
{
if (addr % IAP_SECTOR_SIZE)
{
return iap_check_addr_error;
}
iap_read_bytes(addr, sectorData, IAP_SECTOR_SIZE);
return iap_ok;
}
/**
* @brief 从IAP端口发送一个字节
*
* @param ch 待发送字节
*/
void iap_hw_putchar(uint8_t ch)
{
SBUF = ch;
while (TI == 0);
TI = 0;
}
/**
* @brief 等待数据,超时无数据包则跳转到APP区
*/
void iap_hw_readPacket(void)
{
/* ③ 关闭中断,以轮询方式接收串口数据 */
ES = 0;
while (1)
{
if (RI)
{
RI = 0;
iap_readPacket(SBUF);
}
else
{
/* 超时计数,可适当调整判断阈值 */
delay_us(10);
if (++iap_wait_count == 40000)
{
/* ② */
((void(*)(void))APP_AREA_START_ADDR)();
}
}
}
}
/**
* @brief IAP端口初始化
*/
void iap_hw_uart_init(void)
{
uart_init(IAP_UART, IAP_UART_RX_PIN, IAP_UART_TX_PIN, IAP_UART_BAUD, IAP_UART_TIM);
}
/**
* @brief IAP硬件初始化: 串口初始化
*/
void iap_hw_init(void)
{
IAP_CONTR |= 1 << 7;
iap_set_tps();
iap_hw_uart_init();
#ifdef IN_IAP_AREA
iap_hw_readPacket();
#endif
}
```
由于命名冲突,这里把逐飞库函数的`iap_init`函数重命名为`iap_init1`,并将相关代码移到本例的IAP硬件初始化`iap_hw_init`函数内。对EEPROM/FLASH的操作,调用逐飞库`zf_eeprom`的接口,并在擦除后读取`IAP_OCNTR`寄存器来判断是否有错误发生,该寄存器最高位为1说明操作地址指向了非法地址——例如操作地址超出了前面ISP下载时设置的用户EEPROM大小,见标号①。
标号②:先将APP区起始地址`APP_AREA_START_ADDR`(0x1000)转换为返回值为`void`、参数为`void`的函数指针,然后调用函数,即跳转到APP区基地址。
#### 重映射中断
参考STC8官方手册`中断系统`章节的`中断列表`部分:

图 4-4 STCH系列中断列表(节选)
为了分析51单片机的中断,新建一个Keil C51工程并编写一个测试程序:
代码段4-3
```c
#include
/**
* @brief 串口初始化
*/
void UartInit(void) //115200bps@12.000MHz
{
SCON = 0x50; //8位数据,可变波特率
AUXR |= 0x01; //串口1选择定时器2为波特率发生器
AUXR |= 0x04; //定时器2时钟为Fosc,即1T
T2L = 0xE6; //设定定时初值
T2H = 0xFF; //设定定时初值
AUXR |= 0x10; //启动定时器2
ES = 1; //打开串口发送/接收中断
}
/**
* @brief 定时器初始化
*/
void Timer0Init(void) //10微秒@12.000MHz
{
AUXR |= 0x80; //定时器时钟1T模式
TMOD &= 0xF0; //设置定时器模式
TL0 = 0x88; //设置定时初值
TH0 = 0xFF; //设置定时初值
TF0 = 0; //清除TF0标志
TR0 = 1; //定时器0开始计时
ET0 = 1; //打开定时器中断
}
/**
* @brief 串口1中断处理函数
*/
void UART1_IRQHandler() interrupt 4
{
if (RI)
{
RI = 0;
// ...
}
if (TI)
{
TI = 0;
// ...
}
}
/**
* @brief 定时器0中断处理函数
*/
void T0_IRQHandler() interrupt 1
{
// ...
}
/**
* @brief 用户程序入口
*/
void main(void)
{
/* 初始化定时器0和串口1并打开中断,然后打开总中断 */
Timer0Init();
UartInit();
EA = 1;
while (1)
{
// ...
}
}
```
编译代码,打开生成的xxx.m51文件(默认在`Listings/`文件夹下),对内存分布部分`LINK MAP OF MODULE`关键的几行作标号:
代码段4-4
```sh
LINK MAP OF MODULE: .\Objects\xxx (MAIN)
TYPE BASE LENGTH RELOCATION SEGMENT NAME
-----------------------------------------------------
* * * * * * * D A T A M E M O R Y * * * * * * *
REG 0000H 0008H ABSOLUTE "REG BANK 0"
DATA 0008H 0020H UNIT ?DT?MAIN
DATA 0028H 0005H UNIT _DATA_GROUP_
IDATA 002DH 0001H UNIT ?STACK
* * * * * * * C O D E M E M O R Y * * * * * * *
① CODE 0000H 0003H ABSOLUTE
CODE 0003H 0008H UNIT ?PR?_UART_PUTCHAR?MAIN
② CODE 000BH 0003H ABSOLUTE
CODE 000EH 0015H UNIT ?PR?UARTINIT?MAIN
③ CODE 0023H 0003H ABSOLUTE
...
④ CODE 022BH 008CH UNIT ?C_C51STARTUP
⑤ CODE 0319H 0046H UNIT ?PR?T0_IRQHANDLER?MAIN
⑥ CODE 035FH 002BH UNIT ?PR?UART1_IRQHANDLER?MAIN
CODE 03B2H 0019H UNIT ?C?LIB_CODE
⑦ CODE 03DFH 0013H UNIT ?PR?TIMER0INIT?MAIN
CODE 040AH 0005H UNIT ?C_INITSEG
```
使用STC-ISP软件打开hex文件(默认在`Objects/`文件夹下):

图 4-5 51单片机hex文件(节选)
根据中断列表,代码段4-3的定时器0中断`interrupt 1`和串口1中断`interrupt 4`程序应被编排在地址`0x000B`和`0x0023`,但是中断函数代码量较大导致放不下,由代码段4-4标号⑤和⑥可知这两个中断函数的实际地址是`0x0319`、`0x035F`,代码段4-4标号②和④表示编译器在地址`0x000B`和`0x0023`处放了绝对地址跳转代码。根据51单片机指令集[\[11\]](http://news.eeworld.com.cn/mcu/2015/0910/article_22119.html "51单片机指令集"),`02 xx xx`指令表示跳转到`02`后两个地址字节内容组成(大端模式)的16位地址,对照hex文件地址`0x000B`和`0x0023`起的3个字节指令码,它们恰好分别表示跳转到地址`0x0319`、`0x035F`。代码段4-4标号①和④表示单片机从地址0x0000启动时首先跳转到了`C_C51STARTUP`函数,这个函数在文件`STARTUP.A51`中,它负责设置栈顶等初始化操作、然后调用main()函数。
单片机的大多数中断都有硬件寄存器的中断请求位/中断标志位,标志位被置位时,CPU请求/尝试先跳转到对应的中断程序然后回到原来的位置继续执行,通常中断程序内需要将对应的中断标志位清除,这是为了处理多个中断被触发的情况,CPU会执行标志位未被清除的中断。
51单片机触发中断后硬件上总是从固定的地址(如图4-4)去执行的。将程序空间分成了IAP区(0x0000\~0x0fff)和APP区(0x1000\~0xffff)后,假设在APP区程序中使用了定时器0中断和串口1中断并且不对中断作其它处理,那么中断被触发后结果可能是:
- 情况一,IAP区程序使用了这两个中断:中断跳转到了IAP区内的中断程序;
- 情况二:IAP区程序没使用这两个中断:对于定时器0中断,相当于什么都没做;对于串口1中断,由于没有程序去清除中断标志位导致CPU一直执行中断,导致其它程序没有机会运行,具体现象是APP区main()函数“卡死”。
无论是哪种情况,都没有执行APP区的中断程序。
综上分析,需要对中断做重映射,使单片机执行APP区的中断程序。由于IAP的任务比较简单,可以不使用中断,并将IAP区的中断跳转到APP区:
(1) 删除IAP区工程逐飞库“isr.c”文件的全部中断;
(2) 在Keil C51软件`Options - Target`修改APP区工程代码存储区地址:

图 4-6 修改代码存储区地址
(3) 在Keil C51软件`Options - C51`修改APP区工程中断向量表基地址为APP区程序起始地址,使编译器分配的中断向量位置整体偏移:

图 4-7 修改中断向量表基地址
(4) 修改APP区工程的“STARTUP.A51”文件的启动函数`C_STARTUP`,将启动地址定位在APP区:
代码段 4-5
```asm
NAME ?C_STARTUP
?C_C51STARTUP SEGMENT CODE
?STACK SEGMENT IDATA
RSEG ?STACK
DS 1
EXTRN CODE (?C_START)
PUBLIC ?C_STARTUP
CSEG AT 0x1000
```
(5) 根据STC8手册的中断列表,将全部中断跳转到APP区偏移后的中断向量,修改IAP区工程启动文件“STARTUP.A51”,添加以下代码:
代码段4-6
```asm
APP_AREA_START_ADDR EQU 1000H
ORG 0003H
LJMP APP_AREA_START_ADDR + 0003H
ORG 000BH
LJMP APP_AREA_START_ADDR + 000BH
ORG 0013H
LJMP APP_AREA_START_ADDR + 0013H
ORG 001BH
LJMP APP_AREA_START_ADDR + 001BH
ORG 0023H
LJMP APP_AREA_START_ADDR + 0023H
ORG 002BH
LJMP APP_AREA_START_ADDR + 002BH
ORG 0033H
LJMP APP_AREA_START_ADDR + 0033H
ORG 003BH
LJMP APP_AREA_START_ADDR + 003BH
ORG 0043H
LJMP APP_AREA_START_ADDR + 0043H
ORG 004BH
LJMP APP_AREA_START_ADDR + 004BH
ORG 0053H
LJMP APP_AREA_START_ADDR + 0053H
ORG 005BH
LJMP APP_AREA_START_ADDR + 005BH
ORG 0063H
LJMP APP_AREA_START_ADDR + 0063H
ORG 0083H
LJMP APP_AREA_START_ADDR + 0083H
ORG 008BH
LJMP APP_AREA_START_ADDR + 008BH
ORG 0093H
LJMP APP_AREA_START_ADDR + 0093H
ORG 009BH
LJMP APP_AREA_START_ADDR + 009BH
ORG 00A3H
LJMP APP_AREA_START_ADDR + 00A3H
ORG 00ABH
LJMP APP_AREA_START_ADDR + 00ABH
ORG 00C3H
LJMP APP_AREA_START_ADDR + 00C3H
ORG 00CBH
LJMP APP_AREA_START_ADDR + 00CBH
ORG 00D3H
LJMP APP_AREA_START_ADDR + 00D3H
ORG 00DDH
LJMP APP_AREA_START_ADDR + 00DDH
ORG 011BH
LJMP APP_AREA_START_ADDR + 011BH
ORG 0123H
LJMP APP_AREA_START_ADDR + 0123H
ORG 012BH
LJMP APP_AREA_START_ADDR + 012BH
ORG 0133H
LJMP APP_AREA_START_ADDR + 0133H
ORG 013BH
LJMP APP_AREA_START_ADDR + 013BH
ORG 0143H
LJMP APP_AREA_START_ADDR + 0143H
ORG 014BH
LJMP APP_AREA_START_ADDR + 014BH
ORG 0153H
LJMP APP_AREA_START_ADDR + 0153H
ORG 015BH
LJMP APP_AREA_START_ADDR + 015BH
ORG 0163H
LJMP APP_AREA_START_ADDR + 0163H
```
其中,`EQU`伪指令用于定义符号,`ORG`关键词用于定位接下来的代码放置的地址,`LJMP`指令表示长跳转(跳转到指定的16位地址)。
对中断做重映射以后,中断被触发后,执行顺序是IAP区中断向量-->APP区中断向量-->APP区中断函数,编译后IAP区程序的开头部分如下图:

图 4-8 中断重映射后的IAP区程序文件
#### 调用IAP
(1) IAP区。在`main`函数调用`iap_init()`:
代码段4-7
```c
#include "headfile.h"
#include "iap.h"
void main()
{
DisableGlobalIRQ(); // 关闭总中断
board_init(); // 初始化内部寄存器
iap_init(); // IAP功能初始化
}
```
(2) APP区。在`main()`函数调用`iap_init()`、在对应的串口中断函数调用`iap_readPacket`函数:
代码段4-8
```c
#include "headfile.h"
#include "iap.h"
void main()
{
DisableGlobalIRQ();
board_init();
iap_init();
// 其它初始化...
EnableGlobalIRQ();
while(1)
{
// 此处编写需要循环执行的代码
}
}
// UART1中断
void UART1_Isr() interrupt 4
{
if(UART1_GET_TX_FLAG)
{
UART1_CLEAR_TX_FLAG;
busy[1] = 0;
}
if(UART1_GET_RX_FLAG)
{
UART1_CLEAR_RX_FLAG;
iap_readPacket(SBUF);
// ...
}
}
```
### 4.2 灵动微电子MM32F3277
MM32F3277内存资源为128KB RAM、512KB FLASH,扇区大小为1KB,内核为ARM Cortex M3。使用MDK软件,基于逐飞库[\[12\]](https://gitee.com/seekfree/MM32F3277_Library "逐飞科技MM32F3277开源库")开发,规划IAP区程序空间大小为16KB(0x4000)。
#### IAP硬件层
代码段 4-9
```c
#include "iap.h"
/** @brief IAP端口 */
#define IAP_UART UART_4
#define IAP_UART_BAUD 921600
#define IAP_UART_TX_PIN UART4_TX_C10
#define IAP_UART_RX_PIN UART4_RX_C11
/** @brief FLASH扇区大小 */
#define IAP_SECTOR_SIZE 1024
/** @brief APP区起始地址 */
#define APP_AREA_START_ADDR 0x08004000
/** @brief IAP缓冲区大小 */
#define IAP_BUFFER_SIZE (IAP_SECTOR_SIZE + 16)
/** @brief IAP相关变量 */
uint16_t iap_packetLen;
uint16_t iap_readIndex;
uint8_t __ALIGNED(8) iap_packet[IAP_BUFFER_SIZE];
/* IAP硬件层 --------------------------------------------------------------------*/
uint8_t iap_recv_char;
uint32_t iap_wait_count = 0;
/**
* @brief 复位程序(跳转到IAP区)
*/
void iap_hw_reset(void)
{
NVIC_SystemReset();
}
/**
* @brief 擦除并编程一个扇区
*
* @param addr 目标扇区基地址
* @param sectorData 扇区数据源地址
*
* @return iap_status_t 操作结果
*/
iap_status_t iap_hw_eraseProgramSector(uint32_t addr, uint8_t *sectorData)
{
uint16_t i;
iap_status_t status = iap_ok;
/* 超时计数清零 */
iap_wait_count = 0;
/* 操作地址须对齐扇区 */
if (addr % IAP_SECTOR_SIZE)
{
return iap_check_addr_error;
}
/* FLASH解锁 */
FLASH_Unlock();
/* 擦除扇区 */
if (FLASH_ErasePage(addr) != FLASH_COMPLETE)
{
status = iap_erase_sector_error;
goto end;
}
/* 以半字(2个字节)为单位,编程一个扇区 */
for (i = 0; i < IAP_SECTOR_SIZE; i += 2)
{
if (FLASH_ProgramHalfWord(addr + i, *(uint16_t *)(sectorData + i)) != FLASH_COMPLETE)
{
status = iap_program_sector_error;
goto end;
}
}
end:
/* FLASH上锁 */
FLASH_Lock();
return status;
}
/**
* @brief 读取一个扇区
*
* @param addr 目标扇区基地址
* @param sectorData 扇区数据存放目标地址
*
* @return iap_status_t 操作结果
*/
iap_status_t iap_hw_readSector(uint32_t addr, uint8_t *sectorData)
{
uint16_t i;
if (addr % IAP_SECTOR_SIZE)
{
return iap_check_addr_error;
}
for (i = 0; i < IAP_SECTOR_SIZE; i += 4)
{
*(uint32_t *)(sectorData + i) = *(uint32_t *)(addr + i);
}
return iap_ok;
}
/**
* @brief 从IAP端口发送一个字节
*
* @param ch 待发送字节
*/
void iap_hw_putchar(uint8_t ch)
{
uart_putchar(IAP_UART, ch);
}
#ifdef IN_IAP_AREA
/**
* @brief 读取IAP数据包,超时无数据包则跳转到APP区运行
*
*/
void iap_hw_readPacket(void)
{
typedef void (*pFunction)(void);
pFunction Jump_To_Application;
uint32_t JumpAddress;
while (1)
{
systick_delay_ms(100);
if (++iap_wait_count == 10)
{
__IO uint32_t initial_sp_addr = *(__IO uint32_t *)APP_AREA_START_ADDR;
/* ① Test user application's Stack Pointer */
if (initial_sp_addr >= 0x20000000 && initial_sp_addr <= 0x20020000)
{
/* ② Jump to user application */
JumpAddress = *(__IO uint32_t *)(APP_AREA_START_ADDR + 4);
Jump_To_Application = (pFunction) JumpAddress;
/* ③ Initialize user application's Stack Pointer */
__set_MSP(initial_sp_addr);
/* ⑤ 使用APP区的中断向量表 */
SCB->VTOR = APP_AREA_START_ADDR;
/* ④ Jump to application */
Jump_To_Application();
}
}
}
}
#endif
/**
* @brief IAP端口初始化
*/
void iap_hw_uart_init(void)
{
/* 初始化对应的串口和引脚,打开中断 */
uart_init(IAP_UART, IAP_UART_BAUD, IAP_UART_TX_PIN, IAP_UART_RX_PIN);
uart_rx_irq(IAP_UART, 1);
}
/**
* @brief IAP硬件初始化
*/
void iap_hw_init(void)
{
iap_hw_uart_init();
#ifdef IN_IAP_AREA
iap_hw_readPacket();
#endif
}
```
对于软件复位,直接调用ARM CMSIS标准函数`NVIC_SystemReset`即可。
对于FLASH编程,调用MM32_HAL库函数,为了防止误操作,FLASH编程需要先对FLASH解锁后才能进行FLASH擦除和编程。`*(uint16_t *)(sectorData + i))`等价于`*sectorData + (*(sectorData + 1) << 8)`:`(sectorData + i)`是IAP数据层接收到的扇区数据的第偶数个字节的指针(`uint8_t *`),`(uint16_t *)`表示把该指针转换为无符号16位指针,再取`*`号表示取16位的指针指向的值,整体来看就是取扇区数据第偶数个地址起的无符号16位数值(小端模式)。
据1.5节所述,编译成功后生成的程序前4个字节以小端模式组成的32位数值是初始栈顶地址,紧接着的32位数值是`Reset_Handler`中断向量(程序的入口地址值),程序被正确写入后,FLASH上的IAP区和APP区程序都应满足这个规则。代码段中,标号①判断APP区的初始栈顶地址是否在该单片机RAM的128KB空间内,如果不在则说明程序编程失败,同时它也可以防止首次下载IAP区程序而未下载APP区程序导致跳转到APP区后产生硬件错误;单片机只在复位启动时自动将启动地址(IAP区)的32位值设为栈顶地址,跳转到APP区之前,标号③手动调用内部函数`__set_MSP`重新设置了栈顶地址(如此一来,IAP区和APP区的变量地址是可以重叠的,无需对RAM分区——正常情况下程序中IAP和APP区的变量是无关的);标号②和④读取APP区`Reset_Handler`的地址值并跳转到该地址。
#### 中断重映射
对于ARM Cortex M3/M4/M7内核的单片机,有一个寄存器`SCB->VTOR`用于控制中断向量表,默认是启动地址(一般从FLASH启动,对于MM32F3277来说是0x08000000)。
在跳转到APP区之前(如代码段标号⑤)或APP区启动后(初始化APP区的中断之前)将`SCB->VTOR`赋值为APP区基地址即可实现中断重映射,使硬件在触发中断后基于该地址的中断向量表去执行中断函数。
#### 使用IAP功能
(1) IAP和APP区工程:将`iap.c`、`iap.h`文件加入工程;在`main()`函数调用`iap_init()`;在对应的串口中断调用`iap_readPacket`函数。
代码段4-10
```c
//
#include "iap.h"
int main(void)
{
board_init(true);
iap_init();
...
}
//
#include "iap.h"
void UART4_IRQHandler(void)
{
if(UART4->ISR & UART_ISR_TX_INTF) // 串口发送缓冲空中断
{
UART4->ICR |= UART_ICR_TXICLR; // 清除中断标志位
}
if(UART4->ISR & UART_ISR_RX_INTF) // 串口接收缓冲中断
{
uint8_t ch;
UART4->ICR |= UART_ICR_RXICLR; // 清除中断标志位
uart_query(IAP_UART, &ch);
iap_readPacket(ch);
}
}
```
(2) 在APP区工程设置`MDK - Options - Target - IROM1`为APP区起始地址:

图 4-9 MDK设置编译存放的FLASH地址
### 4.4 STM32F1/F4/L4
> 下面是原理。[单击此处](https://jswyll.com/note/iap/STM32/ "IAP - STM32")查看使用说明。
#### IAP区
使用HAL库开发,STM32F1/L4实现方法大体同上。
对于STM32F4,使用HAL库擦除扇区时需要将FLASH地址转换为FLASH页码。参考STM32F4xx参考手册[[13]](https://usermanual.wiki/Document/STM32ARMMANUALtrang1875.1008118776.pdf "RM0090")可知,STM32F4内部FLASH最大可以有2MB,其中分为FLASH块一和块二;每块最大为1MB,最大有11个扇区/页;扇区0-3是16KB,扇区4是64KB,扇区5-11是128KB。
FLASH地址转扇区页码代码如下:
```c
/** @brief APP区基地址 */
#define IAP_APP_BASE 0x08004000
/** @brief FLASH扇区的最小大小(STM32F4系列,16KB)*/
#define IAP_SECTOR_MIN_SIZE 0x4000
/** @brief FLASH扇区的最大大小(STM32F4系列,128KB)*/
#define IAP_SECTOR_MAX_SIZE 0x20000
/** @brief FLASH块2基地址(STM32F42x和STM32F43x)*/
#define IAP_BANK_2_BASE 0x08100000
/**
* @brief 获取FLASH地址对应的页码
*
* @param addr FLASH操作地址
*
* @retval 0 - 无需擦除
* @retval >0 - HAL库函数擦除页码
*/
int getSectorNum(int addr)
{
int n;
int start = 0;
/* FLASH块2和块1结构相同 */
if (addr >= IAP_BANK_2_BASE)
{
addr = addr - IAP_BANK_2_BASE + FLASH_BASE;
start = 12;
}
/* 扇区0~3是16KB,扇区4是64KB,扇区5~11是128KB */
n = (addr - FLASH_BASE) / IAP_SECTOR_MIN_SIZE;
if ((addr - FLASH_BASE) % IAP_SECTOR_MIN_SIZE != 0)
return -IAP_ERROR_ADDR;
if (n < 5)
return n + start;
if (addr % IAP_SECTOR_MAX_SIZE)
return -IAP_ERROR_ADDR;
return ((addr - FLASH_BASE) / IAP_SECTOR_MAX_SIZE + 4) + start;
}
```
> 使用上位机升级时选择扇区/编程大小为16384及以下的大小都可以,建议1024。
#### APP区
1. 修改程序起始地址为`IAP_APP_BASE`的值
2. 中断重定向。修改`system_stm32f4xx.c`文件
```c
/* Note: Following vector table addresses must be defined in line with linker
configuration. */
/*!< Uncomment the following line if you need to relocate the vector table
anywhere in Flash or Sram, else the vector table is kept at the automatic
remap of boot address selected */
#include "iap.h"
#define USER_VECT_TAB_ADDRESS
#if defined(USER_VECT_TAB_ADDRESS)
/*!< Uncomment the following line if you need to relocate your vector Table
in Sram else user remap will be done in Flash. */
/* #define VECT_TAB_SRAM */
#if defined(VECT_TAB_SRAM)
#define VECT_TAB_BASE_ADDRESS SRAM_BASE /*!< Vector Table base address field.
This value must be a multiple of 0x200. */
#define VECT_TAB_OFFSET 0x00000000U /*!< Vector Table base offset field.
This value must be a multiple of 0x200. */
#else
#define VECT_TAB_BASE_ADDRESS FLASH_BASE /*!< Vector Table base address field.
This value must be a multiple of 0x200. */
#define VECT_TAB_OFFSET IAP_APP_BASE /*!< Vector Table base offset field.
This value must be a multiple of 0x200. */
#endif /* VECT_TAB_SRAM */
#endif /* USER_VECT_TAB_ADDRESS */
```
3. IAP初始化(处理准备升级指令包)
```c
/**
* @brief The application entry point.
* @retval int
*/
int main(void)
{
...
/* USER CODE BEGIN 2 */
iap_init();
/* USER CODE END 2 */
...
}
```
### 4.3 灵动微电子MM32SPIN27PS
MM32SPIN27PS单片机内存资源为12KB RAM、128KB FLASH,扇区大小为1KB,基于ARM Cortex M0内核。
由于MM32_HAL库通用,IAP硬件层代码同灵动微电子MM32F3277,唯一不同之处在于,Cortex M0内核不支持软件重映射中断,没有SCB->VTOR寄存器。
初步尝试使用ST公司提供的解决方案,如下图[\[14\]](http://m.elecfans.com/article/493999.html "STM32F0产品技术培训_2"):

图 4-10 重映射中断到RAM
在APP区启动时,把APP区的48个用户中断向量表拷贝到RAM,然后设置硬件重映射到RAM去寻找中断。代码如下:
代码段 4-11
```c
__IO uint32_t VectorTable[48] __attribute__((at(0x20000000)));
int main(void)
{
for(i = 0; i < 48; i++)
{
VectorTable[i] = *(__IO uint32_t*)(APP_SPACE_ADDR + (i << 2));
}
RCC_APB2PeriphResetCmd(RCC_APB2Periph_SYSCFG, ENABLE);
SYSCFG_MemoryRemapConfig(SYSCFG_MemoryRemap_SRAM);
}
```
经过测试,该方案对于MM32SPIN27PS无效。重新考虑该问题,划分IAP区大小为16KB时FLASH布局如下图:

图 4-11 FLASH布局
既然(从FLASH启动程序时)硬件上只能从地址0x08000000(`FLASH_BASE`)去寻找中断,那么可以在APP区运行时把APP区的中断向量表拷到0x08000000处,在IAP区运行时再恢复IAP区的中断向量表到0x08000000。48个中断共占用192(0xC0)个字节,但在前面已叙述过,FLASH操作的最小单位是一个扇区(对于MM32SPIN27PS来说是1KB,即1024字节)。
综上所述,最终方案为:第一次IAP区启动时,把IAP区的第一个扇区(0x08000000)拷贝到IAP区空白区域的最后一个扇区(0x08003C00)作为备份,此时程序使用的还是IAP区的中断;APP区启动后,把APP区的中断向量表所在的扇区(0x08004000)拷贝到0x08000000,此后单片机使用的是APP区的中断,即使单片机复位后仍将是;在APP收到程序升级指令后,先将备份的IAP区中断拷回0x08000000,然后执行复位运行到IAP区,这时又使用回IAP区的中断函数。基于MM32F3277的IAP程序,修改代码如下:
```c
/**
* @brief 跳转到IAP区
*/
void iap_hw_reset(void)
{
/* 恢复使用IAP区的中断向量表 */
__disable_irq();
iap_hw_eraseProgramSector(FLASH_BASE, (uint8_t *)(APP_AREA_START_ADDR - IAP_SECTOR_SIZE));
NVIC_SystemReset();
}
/**
* @brief IAP硬件初始化: 串口初始化
*/
void iap_hw_init(void)
{
__disable_irq();
iap_hw_uart_init();
#ifdef IN_IAP_AREA
/* 备份IAP区的中断向量表到IAP区的最后一个扇区 */
iap_hw_eraseProgramSector(APP_AREA_START_ADDR - IAP_SECTOR_SIZE, (uint8_t *)FLASH_BASE);
iap_hw_readPacket();
#else
/* 使用APP区的中断向量表 */
iap_hw_eraseProgramSector(FLASH_BASE, (uint8_t *)APP_AREA_START_ADDR);
#endif
__enable_irq();
}
```
### 4.4 恩智浦RT1064
已完成,待编辑...
### 4.5 英飞凌TC26x
已完成,待编辑...
## 5 电脑端上位机程序设计
[https://gitee.com/jswyll_com/mcu_assistant](https://gitee.com/jswyll_com/mcu_assistant "单片机调试助手")
## 6 手机端上位机程序设计
## 7 测试与改进
实际测试时,由于部分同学APP区编写的智能车处理函数放在中断中(定时器或摄像头的DMA),比UART中断优先级高且耗时长,导致丢失字节,未收到完整的准备升级指令包`5A A5 08 00 FF 00 01 FE`。
后面的字节的到来覆盖了前面的字节,但最后一个字节仍然是存在的,因此在APP区放弃解析指令包,直接以收到最后一个字节`FE`作为复位的条件,伪代码如下:
```c
/**
* @brief APP区IAP端口中断回调函数
*/
void UART_IRQHandler()
{
清除中断标志位;
if (收到的字节 == 0xfe)
{
/* 复位,进行IAP升级 */
iap_hw_reset();
}
}
```
## 未完待续