0%

远程升级实现——STM32+IAP

开发工具: Keil 5
开发芯片: STM32F103RCT6
文档编写工具: Markdown

因为只有一个JTAG的下载器,所以在调试程序的时候想用IAP实现远程修改/升级程序.

1 IAP的基础知识

1.1 STM32的编程方式
  • ISP:In System Programming (在系统中编程),通过芯片专用的串行编程接口对其内部的程序存储器进行擦写。
  • IAP:In Application Programming( 在应用中编程),通过调用特定的bootloader程序,对程序存储器的指定段进行读/写操作,从而实现对目标板的程序的修改。

ISP即我们平时所用的JLINK之类的下载器通过专门的接口来下载程序,IAP是通过调用Bootloader来充当下载器的功能实现更新程序的作用。

1.2 Bootloader

一般程序下载方式:

img

STM32的内部内存(FLASH)地址起始于0x08000000,一般程序由此地址写入,0x08000004开始存放中断向量表。当中断开始时,STM32的内部硬件机制会将PC指针定位到“中断向量表”处,并根据中断源取出对应的中断向量执行中断服务程序。

  1. STM32复位之后,从0x08000004地址取出复位中断向量的地址,并跳转到复位中断服务程序。
  2. 在复位中断服务程序执行完之后,跳转至main函数。main函数执行过程中,如果收到中断请求,STM32将PC指针重新拨回中断向量表处。
  3. 根据中断源进入相应的中断服务程序。
  4. 在执行完中断服务程序之后,程序再次返回main函数中执行。

IAP下程序的运行流程:

img
  1. STM32复位之后,从0x08000004地址取出复位中断向量的地址,并跳转到复位中断服务程序,在运行完复位中断服务程序之后跳转到IAP的main函数;
  2. 在执行完IAP以后,跳转至新写入程序的复位向量表,取出新程序的复位中断向量表的地址,并跳转执行新程序的复位中断服务程序,随后跳转至新程序的main函数,上图中②和③;
  3. 在main函数执行过程中,如果CPU得到一个中断请求,PC指针仍强制跳转到地址0x08000004中断向量表处,而不是新程序的中断向量表;
  4. 程序根据设置的中断向量表偏移量,跳转到对应中断源新的中断服务程序中;
  5. 在执行完中断服务程序后,程序返回main函数继续运行。

2 IAP的实现

2.1 Keil设置

keil中需要准备两个工程,一个是IAP,一个是APP。Bootloader通过ISP方式下载到Flash中,APP则是通过串口将编译生成的bin文件发送下载。

使用的STM32型号为STM32F103RCT6,参数如下表所示:

基本参数
名称 STM32F103RCT6
架构 ARM Cortex-M3
Flash容量 256KB
RAM容量 48K

两个工程将Flash分成两个区域,Bootloader存储的起始地址为0x08000000,分配大小这里设置为0x2000字节;用户APP信息存储从0x08002000处开始,分配空间大小为(0x08040000-0x08002000=0x803E000)。IAP工程文件中使用keil默认设置即可,APP工程中的设置如下图所示。

image-20201010133907154image-20201010133813256

image-20201010165809674 image-20201010171514423
2.2 驱动程序

串口接收:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
//串口接收缓冲区
u8 serial_Buffer[SERIAL_MAX_LENGTH] = {0};
//串口接收数据长度
u16 serial_Buffer_Length = 0;

u8 receiveMode = 0;//接收参数的中断处理模型,为0的时候是命令模式,为1的时候为下载模式
u8 receiveExpectCount = 0;//串口期望接收长度

//串口中断处理
static void SerialRecv(u8 ch)
{
if(receiveMode == 0)
{
if((serial_Buffer_Length&0x8000) == 0x8000)//已经接收完成,系统还没处理
{
serial_Buffer_Length |= 0x8000;//退出
}
else if((serial_Buffer_Length&0x4000) == 0x4000)//接收到回车还没接收到换行
{
if(ch == '\n')serial_Buffer_Length |= 0x8000;
else
{
//一帧接受失败
serial_Buffer_Length = 0;
}
}
else
{
if((serial_Buffer_Length&0xff) < SERIAL_MAX_LENGTH)
{
if(ch == '\r')serial_Buffer_Length |= 0x4000;
else
{
serial_Buffer[(serial_Buffer_Length&0xff)] = ch;
serial_Buffer_Length++;
}
}
else
{
//一帧接受失败
serial_Buffer_Length = 0;
}
}
}
else
{
//下载模式,只控制字符串的量,数据的第一位是该数据包的长度,接收到这么多长度,接收完成位置一
//注意,在这种模式下,清除serial_Buffer_Length之前应当清除receiveExpectCount的值
if(receiveExpectCount == 0)//期望下载为0,第一个数就是期望下载数
{
receiveExpectCount = ch;
}
else
{
if((serial_Buffer_Length&0x8000) == 0x8000)//已经接收完成,系统还没处理,此时不接收数据
{
serial_Buffer_Length |= 0x8000;//退出
}
else
{
serial_Buffer[(serial_Buffer_Length&0xff)] = ch;//接收数据并保存
serial_Buffer_Length++;
if((serial_Buffer_Length&0xff) == receiveExpectCount)//接收到了期望长度的数据
{
serial_Buffer_Length |= 0x8000;//一包接收完成标志
}
}
}

}
}

void USART1_IRQHandler(void)
{
u8 ch = 0;
if(USART_GetITStatus(USART1, USART_IT_RXNE) == SET)//检查中断发生
{
ch = (u8)USART_ReceiveData(USART1);
USART_ClearITPendingBit(USART1, USART_IT_RXNE); //清除中断
// Debug_Serial_Send_Char(ch); //将收到的数据发送出去
SerialRecv(ch); //处理中断数据
}
}

IAP程序

IAP程序包括iap_down(下载程序,片机接收来自于上位机的数据),iap_jump_app(IAP跳转到APP的跳转指令),iap_over(指示IAP完成,将系统缓冲区清空),iap_set_flag(检测到该标志时跳转到APP程序中),iap_clear_flag(清除APP标志,让IAP不再自动跳转到APP中),app_jump_iap(app跳转到iap的跳转指令)。

IAP_set_flag

1
2
3
4
5
6
7
8
9
10
11
12
13
//设置app固化配置
void iap_set_flag(void)
{
Test_Write(APP_CONFIG_ADDR,APP_CONFIG_SET_VALUE);
printf("固化成功\r\n");
}

//WriteAddr:起始地址
//WriteData:要写入的数据
void Test_Write(u32 WriteAddr,u16 WriteData)
{
STMFLASH_Write(WriteAddr,&WriteData,1);//写入一个字
}

在keil中我们设置0x08000000-0x08003000来存放iap代码,并将0x08001FFC作为存放app固化标志的地方,在宏定义中设置各个变量的地址:

1
2
3
4
5
#define APP_CONFIG_ADDR     0X08001FFC //配置地址

#define APP_CONFIG_SET_VALUE 0X5555 //设置值

#define APP_CONFIG_CLEAR_VALUE 0XFFFF //清零值

iap_claer_flag

清除标志的方式与写入标志的方式同理,在 APP_CONFIG_ADDR 这个地址写入清零值。

1
2
3
4
5
6
7
//清除app固化配置
void iap_clear_flag(void)
{
Test_Write(APP_CONFIG_ADDR,APP_CONFIG_CLEAR_VALUE);

printf("清除成功\r\n");
}

iap_jump_app

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
typedef  void (*iapfun)(void);				//定义一个函数类型的参数.
iapfun jump2app;

//设置栈顶地址
//addr:栈顶地址

__asm void MSR_MSP(u32 addr)
{
MSR MSP, r0 //set Main Stack value
BX r14
}

//跳转到app区域运行
void iap_jump_app(void)
{
iap_load_app(FLASH_APP1_ADDR);//跳转到app的复位向量地址
}

//跳转到应用程序段
//appxaddr:用户代码起始地址.
void iap_load_app(u32 appxaddr)
{
if(((*(vu32*)appxaddr)&0x2FFE0000)==0x20000000) //检查栈顶地址是否合法.0x20000000是sram的起始地址,也是程序的栈顶地址
{
printf("ok\r\n");
Delay_Ms(10);
jump2app=(iapfun)*(vu32*)(appxaddr+4); //用户代码区第二个字为程序开始地址(复位地址)
MSR_MSP(*(vu32*)appxaddr); //初始化APP堆栈指针(用户代码区的第一个字用于存放栈顶地址)
jump2app(); //跳转到APP.
}
else
{
printf("program in flash is error\r\n");
}
}

程序解释:

  1. 检查栈顶地址

    1
    if(((*(vu32*)appxaddr)&0x2FFE0000)==0x20000000)

    在实际的程序中,设置APP的起始地址为0x08003000,即appxaddr==0x08003000,而 *(vu32 *)appxaddr即取0x08003000-0x08003003这4个字节的值,因为APP中设置中断向量表放置在0x08003000开始的位置,中断向量表中第一个放的则是栈顶地址的值。通过判断栈顶地址值是否正确(是否在0x2000 0000 - 0x 2000 2000之间) 来判断是否应用程序已经下载了,因为应用程序的启动文件会初始化化栈空间,如果栈顶值对了,说明启动文件的初始化执行了应用程也已经下载了。

  2. 程序开始地址

    1
    jump2app=(iapfun)*(vu32*)(appxaddr+4);		//用户代码区第二个字为程序开始地址(复位地址)		

    (appxaddr+4)即0x08003004,这个地址放的时中断向量表的第二项“复位地址”

    1
    typedef  void (*iapfun)(void)

    一般,typedef int a,是给整型定义一个别名 a ;而 void (* iapfun)(void) 是声明一个函数指针,加上 typedef 之后 iapfun 只不过是类型 void(*)void 的一个别名。所以,此时的jump2app已经指向了复位函数所在的地址 Reset_Handler(中断向量表的第二项),跳转到main函数。下图为STM32启动文件 startup_stm32f10x_hd.s 中的代码解释:

    image-20201012144259212

    ST公司都提供了现成的直接可用的启动文件,程序开发人员可以直接引用启动文件后直接进行C应用程序的开发。这样能大大减小开发人员从其它微控制器平台跳转至STM32平台,也降低了适应STM32微控制器的难度。相对于ARM上一代的主流ARM7/ARM9内核架构,新一代Cortex内核架构的启动方式有了比较大的变化。ARM7/ARM9内核的控制器在复位后,CPU会从存储空间的绝对地址0x000000取出第一条指令执行复位中断服务程序的方式启动,即固定了复位后的起始地址为0x000000PC = 0x000000)同时中断向量表的位置并不是固定的。而Cortex-M3内核则正好相反,有3种情况*:
    1、 通过boot引脚设置可以将中断向量表定位于SRAM区,即起始地址为0x2000000,同时复位后PC指针位于0x2000000处;
    2、 通过
    boot引脚设置可以将中断向量表定位于FLASH区,即起始地址为 0x8000000,同时复位后PC指针位于0x8000000处;
    3、 通过
    boot引脚设置可以将中断向量表定位于内置Bootloader区,本文不对这种情况做论述;
    而Cortex-M3内核规定,起始地址必须存放堆顶指针,而第二个地址则必须存放复位中断入口向量地址,这样在
    Cortex-M3内核复位后,会自动从起始地址的下一个32*位空间取出复位中断入口向量,跳转执行复位中断服务程序。

    STM32 IAP 在线升级详解——CSDN

    关于Stm32的IAP详细和应用——CSDN

app_jump_iap

1
2
3
4
5
void app_jump_iap()
{
SCB->VTOR = FLASH_BASE; //设置中断向量偏移量
NVIC_SystemReset(); //复位函数,软件重启
}

​ 其中,NVIC_SystemReset()这个函数在新版的STM32的官方固件库文件 core_cm3.h 中1720行处:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
/* ##################################    Reset function  ############################################ */
/**
* @brief Initiate a system reset request.
* 系统复位
* Initiate a system reset request to reset the MCU
*/
static __INLINE void NVIC_SystemReset(void)
{
SCB->AIRCR = ((0x5FA << SCB_AIRCR_VECTKEY_Pos) |
(SCB->AIRCR & SCB_AIRCR_PRIGROUP_Msk) |
SCB_AIRCR_SYSRESETREQ_Msk); /* Keep priority group unchanged */ /*保持优先级组不变*/
__DSB(); /* Ensure completion of memory access */ /*确保完成内存访问*/
while(1); /* wait until reset */ /*等待重启*/
}

如果使用的是正点原子的老教程,版本文件老旧 core_cm3.h文件可能没有更新,则没有这个函数

​ iap和app之间的跳转必须关闭所有中断 ,并且复位NVIC中断寄存器的值,因为跳转函数是用程序指针完成的,但跳转只是强制改变了PC指针的位置,NVIC寄存器的值还是保持着原来main的值,所以一旦发生中断就会指向跳转前的main函数的中断函数入口地址,程序会卡死导致 HardFault。所以最好的方法是使用上述的软件重启的思路,其余的处理方式还有:①跳转之前复位或者关闭所有打开的中断②跳转后在初始化时加入RCC_DeInit();NVIC_DeInit ();等让中断恢复默认值。

iap_down_s

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
#define FLASH_APP1_ADDR		0x08002000  	//第一个应用程序起始地址(存放在FLASH)
//保留的空间为IAP使用

u16 iapbuf[1024] = {0}; //用于缓存数据的数组
u16 receiveDataCur = 0; //当前iapbuffer中已经填充的数据长度,一次填充满了之后写入flash并清零
u32 addrCur = FLASH_APP1_ADDR; //当前系统写入地址,每次写入之后地址增加2048

//开始下载
void iap_down_s(void)
{
u16 i = 0;
u16 temp = 0;
u16 receiveCount;
printf("begin,wait data download\r\n");
receiveMode = 1;//串口进入下载接收数据模式
while(1)
{
//循环接收数据,每次必须要发128个数据下来,如果没有128,说明这是最后一包数据
//接收到一包数据之后,返回一个小数点,发送完成,系统编程完成之后返回一个iap_over
if(serial_Buffer_Length & 0x8000)
{
receiveCount = (u8)(serial_Buffer_Length&0x00ff);
if(receiveCount == 128)//满足一包,填充并查看是否有了1024字节,有了写入闪存
{
for(i = 0; i < receiveCount; i+=2)
{
//数据八位融合为16位
temp = (((u16)serial_Buffer[i+1])<<8) + ((u16)serial_Buffer[i]);
iapbuf[receiveDataCur] = temp;
receiveDataCur++;//完成之后receiveDataCur++;
}
receiveExpectCount = 0;//清除期望接收模式
serial_Buffer_Length = 0;//清除串口满标志
printf(".");//每次接受一次数据打一个点
//此时需要检测receiveDataCur的值,要是放满了,就需要写入
if(receiveDataCur == 1024)
{
//写入flash中
STMFLASH_Write(addrCur,iapbuf,1024);
//printf("\r\nwrite addr %x,length 1024\r\n",addrCur);
addrCur += 2048;//地址+2048
//写完之后receiveDataCur要清零等待下一次传输
receiveDataCur = 0;
}
else //有可能最后一包有128个数据但是最终没有2048个数据,此时扩展一个指令用于完成最后一个的写入
{

}
//还没放满,等待下一次数据过来
}
else //不满足一包,说明数据传送这是最后一包,写入闪存
{
//没有一包也要传送到缓存中
for(i = 0; i < receiveCount; i+=2)
{

//数据八位融合为16位
temp = (((u16)serial_Buffer[i+1])<<8) + ((u16)serial_Buffer[i]);
iapbuf[receiveDataCur] = temp;
receiveDataCur++;//完成之后receiveDataCur++;

}
receiveExpectCount = 0;//清除期望接收模式
serial_Buffer_Length = 0;//清除串口满标志
printf(".");//每次接受一次数据打一个点
//之后就要将这数据写入到闪存中
STMFLASH_Write(addrCur,iapbuf,receiveDataCur);//将最后的一些内容字节写进去.
//printf("\r\nwrite addr %x,length %d\r\n",addrCur,receiveDataCur);
//写完之后要把地址恢复到原来的位置
addrCur = FLASH_APP1_ADDR;
receiveDataCur = 0;
//写完之后要退出下载循环并告诉上位机,已经下载完了
printf("download over\r\n");
//同时,也要退出下载循环模式
receiveMode = 0;
return;
}
}
}
}

代码的核心思想是上位机每次发送128个数据,128个8位数据通过位操作两两融合成16位数据,每个新数据占2个地址,写满2048个addr后写一次flash;当最后一包数据不是128时说明数据发送完成了,将最后的数据烧入flash之后把地址恢复到原来位置addrCur = FLASH_APP1_ADDR,退出下载模式 receiveMode从1置为0;可能会出现的情况在于最后一包的数据也是128个,此时iap_down_s的判断机制仍处于下载模式,针对这种情况定义一个新指令iap_over,上位机侦测到最后一包数据也是128个时补充发送该命令,下位机将缓存写入并退出。

iap_over_s

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
//最后一包有128个数据但是最终没有2048个数据
//收到这个指令检测receiveDataCur和addrCur的值,
//完成最后的写入

void iap_over_s(void)
{
//这个时候,依然在串口下载模式
if(receiveDataCur != 0)
{
STMFLASH_Write(addrCur,iapbuf,receiveDataCur);//将最后的一些内容字节写进去.
printf("write addr %x,length %d",addrCur,receiveDataCur);
addrCur = FLASH_APP1_ADDR;
receiveDataCur = 0;
//同时,也要退出下载模式
receiveMode = 0;
}
printf("最后一包发送完成\r\n");
}

Flash擦写

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
//读取指定地址的半字(16位数据)
//faddr:读地址(此地址必须为2的倍数!!)
//返回值:对应数据.
u16 STMFLASH_ReadHalfWord(u32 faddr)
{
return *(vu16*)faddr;
}


#if STM32_FLASH_WREN //如果使能了写
//不检查的写入
//WriteAddr:起始地址
//pBuffer:数据指针
//NumToWrite:半字(16位)数
void STMFLASH_Write_NoCheck(u32 WriteAddr,u16 *pBuffer,u16 NumToWrite)
{
u16 i;
for(i=0;i<NumToWrite;i++)
{
FLASH_ProgramHalfWord(WriteAddr,pBuffer[i]);
WriteAddr+=2;//地址增加2.
}
}

//从指定地址开始写入指定长度的数据
//WriteAddr:起始地址(此地址必须为2的倍数!!)
//pBuffer:数据指针
//NumToWrite:半字(16位)数(就是要写入的16位数据的个数.)
#if STM32_FLASH_SIZE<256
#define STM_SECTOR_SIZE 1024 //字节
#else
#define STM_SECTOR_SIZE 2048
#endif


u16 STMFLASH_BUF[STM_SECTOR_SIZE/2];//最多是2K字节
void STMFLASH_Write(u32 WriteAddr,u16 *pBuffer,u16 NumToWrite)
{
u32 secpos; //扇区地址
u16 secoff; //扇区内偏移地址(16位字计算)
u16 secremain; //扇区内剩余地址(16位字计算)
u16 i;
u32 offaddr; //去掉0X08000000后的地址
if(WriteAddr<STM32_FLASH_BASE||(WriteAddr>=(STM32_FLASH_BASE+1024*STM32_FLASH_SIZE)))return;//非法地址
FLASH_Unlock(); //解锁
offaddr=WriteAddr-STM32_FLASH_BASE; //实际偏移地址.
secpos=offaddr/STM_SECTOR_SIZE; //扇区地址 0~127 for STM32F103RBT6
secoff=(offaddr%STM_SECTOR_SIZE)/2; //在扇区内的偏移(2个字节为基本单位.)
secremain=STM_SECTOR_SIZE/2-secoff; //扇区剩余空间大小
if(NumToWrite<=secremain)secremain=NumToWrite;//不大于该扇区范围
while(1)
{
STMFLASH_Read(secpos*STM_SECTOR_SIZE+STM32_FLASH_BASE,STMFLASH_BUF,STM_SECTOR_SIZE/2);//读出整个扇区的内容
for(i=0;i<secremain;i++)//校验数据
{
if(STMFLASH_BUF[secoff+i]!=0XFFFF)break;//需要擦除
}
if(i<secremain)//需要擦除
{
FLASH_ErasePage(secpos*STM_SECTOR_SIZE+STM32_FLASH_BASE);//擦除这个扇区
for(i=0;i<secremain;i++)//复制
{
STMFLASH_BUF[i+secoff]=pBuffer[i];
}
STMFLASH_Write_NoCheck(secpos*STM_SECTOR_SIZE+STM32_FLASH_BASE,STMFLASH_BUF,STM_SECTOR_SIZE/2);//写入整个扇区
}else STMFLASH_Write_NoCheck(WriteAddr,pBuffer,secremain);//写已经擦除了的,直接写入扇区剩余区间.
if(NumToWrite==secremain)break;//写入结束了
else//写入未结束
{
secpos++; //扇区地址增1
secoff=0; //偏移位置为0
pBuffer+=secremain; //指针偏移
WriteAddr+=secremain; //写地址偏移
NumToWrite-=secremain; //字节(16位)数递减
if(NumToWrite>(STM_SECTOR_SIZE/2))secremain=STM_SECTOR_SIZE/2;//下一个扇区还是写不完
else secremain=NumToWrite;//下一个扇区可以写完了
}
};
FLASH_Lock();//上锁
}
#endif



//从指定地址开始读出指定长度的数据
//ReadAddr:起始地址
//pBuffer:数据指针
//NumToWrite:半字(16位)数
void STMFLASH_Read(u32 ReadAddr,u16 *pBuffer,u16 NumToRead)
{
u16 i;
for(i=0;i<NumToRead;i++)
{
pBuffer[i]=STMFLASH_ReadHalfWord(ReadAddr);//读取2个字节.
ReadAddr+=2;//偏移2个字节.
}
}

用户回调函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
void Help_Proc_Func(void)
{
printf("DzyLink shell v1.0\r\n");
printf("modify by Dingzy\r\n");
printf("2020/10/12 21:44\r\n");
}

void List_Proc_Func(void)
{
u8 i = 0;
printf("command num is %d\r\n",COMMAND_NUM);
for(i = 0; i < COMMAND_NUM; i++)
{
printf("%d : %s\r\n",i,commandStringList[i]);
}
printf("*****************************************************\r\n");
}

CommandScan扫描命令函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
//扫描命令字符串,并调用相应处理函数
void CommandScan(void)
{
u8 commandLength1;
u8 commandLength2;
u8 i = 0,j = 0;
//数据满
if((serial_Buffer_Length & 0x8000) == 0x8000)
{
//检测命令不是全为空格
if(Command_Is_Vailed())
{
Command_Copy();//copy命令字符串等待处理
//去除命令头上的空白
Command_Remove_Space_Head();
//去除命令尾巴上的空格
Command_Remove_Space_End();
//去除中间的重复空格
Command_Remove_Space_Inner();
commandLength1 = Command_Find_Space_Postion(1);//获取长度
if(commandLength1 == 0)commandLength1 = commandStringLength;//当第二个空格获取返回0的时候,说明没有参数,纯命令,所以没有空格
for(i = 0; i < COMMAND_NUM; i++)
{
commandLength2 = StringGetLength(commandStringList[i]);
if(commandLength1 == commandLength2)
{
//长度相同,比对每个字符
for(j = 0; j < commandLength1; j++)
{
if(commandStringBuffer[j] == commandStringList[i][j])continue;
else break;
}
if(j == commandLength1)//比对成功
{
//调用函数
Command_Proc_Func_Table[i]();
return;
}
}
else
{
//直接长度不同,不需要比对了
continue;
}
}
if(i == COMMAND_NUM)
{
//没找到对应命令
printf("not find command\r\n");
}

}
else
{
printf("command can't all space\r\n");
serial_Buffer_Length = 0;
}

}
}

//回调函数数组定义,比对成功之后按Command_table中的命令组依次执行
Command_Proc_Func Command_Proc_Func_Table[] =
{
Help_Proc_Func, //打印版本、作者和时间信息
List_Proc_Func, //打印命令num
iap_down_s, //iap下载程序
iap_jump_app, //iap跳转到app
iap_over_s, //最后一包128数据写入
iap_set_flag, //设置app固化标志
iap_clear_flag //清除app固化标志
};

main函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
int main(void)
{
NVIC_Group_Init(); //系统默认中断分组
Debug_Serial_Init(115200);
Delay_Init();
Command_Init(100); //100ms
while(1)
{
if(STMFLASH_ReadHalfWord(APP_CONFIG_ADDR) == 0x5555)
{
//直接跳转到APP
iap_jump_app_s();
}
CommandScan(); //扫描命令处理
}
}
2.3 APP程序

app使用最简单的蜂鸣器实验(代码来自正点原子),硬件部分将蜂鸣器的I/O口连接至PB8,代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
#include "sys.h"	
#include "delay.h"
#include "led.h"
#include "beep.h"
//ALIENTEK战舰STM32开发板实验2
//蜂鸣器实验
//技术支持:www.openedv.com
//广州市星翼电子科技有限公司
int main(void)
{
delay_init(); //延时函数初始化
LED_Init(); //初始化与LED连接的硬件接口
BEEP_Init(); //初始化蜂鸣器端口
while(1)
{
LED0=0;
BEEP=0;
delay_ms(300);//延时300ms
LED0=1;
BEEP=1;
delay_ms(300);//延时300ms
}
}
蜂鸣器

3 通信协议

挖坑待填…

4 Q&A

4.1 报错
  • ..\..\Libraries\CMSIS\stm32f10x.h(298): error: #67: expected a "}"
    ADC1_2_IRQn = 18, /*!< ADC1 and ADC2 global Interrupt */
    ..\..\Libraries\CMSIS\stm32f10x.h(472): warning: #12-D: parsing restarts here after previous syntax error} IRQn_Type;
    ..\..\User\main.c: 1 warning, 1 error
    "..\..\User\main.c" - 1 Error(s), 1 Warning(s).
    
    1
    2
    3
    4
    5
    6

    **解决方法:**在C/C++选项卡里,把STM3210X_HD从**Prepocessor symbol define** 里面删掉。在老版本的官方STM32F10x.h文件里,是`...&&!defined(STM32F10X_HD) && ...` 原来是有括号的,不做标识符来处理,而新版的直接说明了出来.

    - ```
    ..\driver\debugSerial.c(14): error: #260-D: explicit type is missing ("int" assumed)
    _sys_exit(int x)
    **问题原因:**_sys_exit(int x) 这个函数没有返回类型,产生这个的原因是因为用了C99的库,C99和C89的区别详见https://www.cnblogs.com/ys77/p/11541827.html **解决方法:**添加 void 不报错,编译通过。
    1
    void  _sys_exit(int x)
4.2 IAP跳转执行的问题

在栈顶地址验证通过之后,Flash进行了擦除-拷贝-跳转执行的操作,问题在于跳转执行之后,Bootloader又将其引导回了流程一开始的阶段,两次擦除和拷贝之后,栈顶地址发生了改变,程序无法运行,如图所示:

解决方法:跳转程序没有正常执行,三种问题可能会导致APP跳转失败。

  1. 设置了APP标志后,APP能够跳转到IAP中,但IAP马上又会跳转回APP,永远不能等待下载;

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    //设置app固化配置
    void iap_set_flag_s(void)
    {
    Test_Write(APP_CONFIG_ADDR,APP_CONFIG_SET_VALUE);
    printf("ok\r\n");
    }

    //清除app固化配置
    void iap_clear_flag(void)
    {
    Test_Write(APP_CONFIG_ADDR,APP_CONFIG_CLEAR_VALUE);

    printf("ok\r\n");
    }

    先清除APP标志,然后再跳转到IAP程序中,标志就不会影响IAP的下载流程了。

  2. APP的工程中,不仅是在[Target]中要设置Flash的起始地址和SIze,在Jlink的[Flash Download]中也需要设置芯片的起始地址和Size,如图所示:

    image-20201010133813256 image-20201010133907154
  3. 中断向量表的设置。在IAP中不需要考虑中断向量表,IAP的默认程序就是从0x8000000位置开始的,但是APP代码的起始位置必须从IAP程序之后的地址开始,因此必须重新设置中断向量表。在system_stm32f10x.c中又一个system_init的函数,该函数的作用为启动时调用配置系统时钟,该函数的最后为:

    1
    2
    3
    4
    5
    #ifdef VECT_TAB_SRAM
    SCB->VTOR = SRAM_BASE | VECT_TAB_OFFSET; /* Vector Table Relocation in Internal SRAM. */
    #else
    SCB->VTOR = FLASH_BASE | VECT_TAB_OFFSET; /* Vector Table Relocation in Internal FLASH. */
    #endif

    其中VECT_TAB_OFFSET就是需要修改的偏移量,也就是APP程序的起始地址偏移,这个设置必须与IAP同步,我们设置为2000。该值的宏就需要修改,在128行的位置,将0x0修改为0x2000(与2中设置同步)。

    1
    2
    #define VECT_TAB_OFFSET  0x2000 /*!< Vector Table base offset field. 
    This value must be a multiple of 0x200. */
两种颜色的功德箱(逃