【Cortex-M架构】MCU代码段跳转、运行多段代码,Flash程序更新的实现方式之一

【Cortex-M架构】MCU代码段跳转、运行多段代码,Flash程序更新的实现方式之一

代码跳转

一般来说 MCU上电时 会判断BOOT引脚状态 从而控制运行代码的位置(如BootLeader、用户代码等)
若从用户代码启动 则默认从Flash的地址0位开始运行(0x0800 0000)
在运行用户代码后 可以通过软件再进行代码段的跳转 从而实现运行其他代码段或进入BootLeader烧录模式等等

有了代码跳转方式 就可以实现同时运行多段代码(如将Flash分为三个区域 分别运行引导、烧录、用户代码)
从而可以在软件上实现Flash程序更新

通过程序跳转前需要做的事:
1、禁止所有外设时钟
2、禁止使用PLL
3、禁止所有中断
4、清除所有中断挂起标志
以下以STM32为例

跳转到BootLeader

以下参考:
STM32F407用USB和串口烧写程序

#define DISABLE_INT()	__set_PRIMASK(1)
#define ENABLE_INT()	__set_PRIMASK(0)

static void JumpToBootloader(void)
{
    
    
	/*除了使用boot引脚控制运行系统bootloader,也可以使用程序跳转,跳转前需要注意以下问题
	1、禁止所有外设时钟
	2、禁止使用PLL
	3、禁止所有中断
	4、清除所有中断挂起标志
	以上操作执行完毕后,直接使用跳转指令,跳转到System Memory地址即可。这个地址可以在数据手册中找到
	
	如果觉得关闭外设时钟等操作太复杂,也可以通过另外一种方式来实现上述操作,即:软件复位
	芯片复位后,外设时钟、中断等默认都是关闭的,只要在初始化前完成跳转即可。我们知道,stm32
	在运行main.c函数之前会先执行system_stm32f4xx.c中的SystemInit函数,在HAL库中,该函数只是配置了中断向量,
	因此可以直接在main.c函数的开始添加跳转代码。具体流程如下
	1、需要升级时,首先在flash某个地址将一个标志置1  (没有测试这种方法)
	2、产生软件复位
	3、判断标志位为1,需要升级,标志位清零,执行跳转程序。
	4、判断标志位为0,直接运行程序 DFU*/
	uint32_t i=0;
	void (*SysMemBootJump)(void); /* 声明一个函数指针*/
	__IO uint32_t BootAddr =  0x1FFF0000;//0x1FFF0000; /* STM32F4 的系统 BootLoader 地址 */
	
	/* 关闭全局中断 */
	DISABLE_INT();//CPU_IntDis();  
	
	/* 关闭滴答定时器,复位到默认值 */
	SysTick->CTRL = 0;
	SysTick->LOAD = 0;
	SysTick->VAL = 0;
	
	/* 设置所有时钟到默认状态,使用 HSI 时钟 */
	HAL_RCC_DeInit();
	
	/* 关闭所有中断,清除所有中断挂起标志 */
	for (i = 0; i < 8; i++)
	{
    
    
		NVIC->ICER[i]=0xFFFFFFFF;
		NVIC->ICPR[i]=0xFFFFFFFF;
	}
	
	/* 使能全局中断 */
	ENABLE_INT();//CPU_Init();
	
	/* 设置重映射到系统 Flash */
	__HAL_SYSCFG_REMAPMEMORY_SYSTEMFLASH();
	
	/* 跳转到系统 BootLoader,首地址是 MSP,地址+4 是复位中断服务程序地址 */
	SysMemBootJump = (void (*)(void)) (*((uint32_t *) (BootAddr + 4)));
	
	/* 设置主堆栈指针 */
	__set_MSP(*(uint32_t *)BootAddr);
	
	/*在 RTOS 工程,这条语句很重要,设置为特权级模式,使用 MSP 指针 */
	__set_CONTROL(0);
	
	/* 跳转到系统 BootLoader */
	SysMemBootJump();
	
//	/* 跳转成功的话,不会执行到这里,用户可以在这里添加代码 */
//	while (1)
//	{
    
    
//		
//	}
}

跳转后 即启动BootLeader 然后可以通过串口烧录代码

可以看到 通过调试器信息 运行跳转后 就运行到了BootLeader区域地址
在这里插入图片描述

跳转到Flash其他位置

同理 跳转函数也可以跳到其他地方
跳转前 同样需要进行关闭中断等操作
如:

static void JumpToApp(uint32_t add)
{
    
    
	uint32_t i=0;
	void (*SysMemBootJump)(void);        /* 声明一个函数指针 */
	__IO uint32_t BootAddr = add; /* 系统的APP地址 */	
	

	/* 关闭全局中断 */
	DISABLE_INT(); 

	/* 关闭滴答定时器,复位到默认值 */
	SysTick->CTRL = 0;
  SysTick->LOAD = 0;
  SysTick->VAL = 0;

	/* 设置所有时钟到默认状态,使用HSI时钟 */
	HAL_RCC_DeInit();

	/* 关闭所有中断,清除所有中断挂起标志*/
	for (i = 0; i < 8; i++)
	{
    
    
		NVIC->ICER[i]=0xFFFFFFFF;
		NVIC->ICPR[i]=0xFFFFFFFF;
	}	

	/* 使能全局中断 */
	ENABLE_INT();

	/* 跳转到系统BootLoader,首地址是MSP,地址+4是复位中断复位程序地址 */
	SysMemBootJump = (void (*)(void)) (*((uint32_t *) (BootAddr + 4)));

	/* 设置主堆栈指针 */
	__set_MSP(*(uint32_t *)BootAddr);
	
	/*在 RTOS 工程,这条语句很重要,设置为特权级模式,使用 MSP 指针 */
	__set_CONTROL(0);

	/* 跳转到系统 BootLoader */
	SysMemBootJump(); 
}

即在add区域执行代码

若是填写0x0800 0000 并在main函数头打断点 可以看到会跳转到本工程的main函数开头

在这里插入图片描述
在这里插入图片描述
并且如果调试时选择跳出函数 可以看到程序会重新运行.s文件的系统初始化
在这里插入图片描述

MCU运行多段代码

为了实现程序串口更新 需要在正常运行时 检查判断条件 并在成立时跳转到BootLeader
那么可以在Flash区域定义两块代码 一块用于引导判断 一块才是APP

一般来说 用户代码起止地址为0x0800 0000
单片机复位后进入用户代码的起始位置也是这个地址
在这里插入图片描述
那么我们可以将引导APP烧录至该地址
相关工程配置默认即可
并在main函数中加入判断 如:

int main(void)
{
    
    
  /* USER CODE BEGIN 1 */

  /* USER CODE END 1 */
  

  /* MCU Configuration--------------------------------------------------------*/

  /* Reset of all peripherals, Initializes the Flash interface and the Systick. */
  HAL_Init();

  /* USER CODE BEGIN Init */

  /* USER CODE END Init */

  /* Configure the system clock */
  SystemClock_Config();

  /* USER CODE BEGIN SysInit */

  /* USER CODE END SysInit */

  /* Initialize all configured peripherals */
  MX_GPIO_Init();
  /* USER CODE BEGIN 2 */

  /* USER CODE END 2 */

  /* Infinite loop */
  /* USER CODE BEGIN WHILE */
  while (1)
  {
    
    
    /* USER CODE END WHILE */
		if(HAL_GPIO_ReadPin(GPIOE,GPIO_PIN_1)==GPIO_PIN_RESET)
		{
    
    
			JumpToBootloader();
		}
		else
		{
    
    
			JumpToApp(0x08004000);
		}
		/*
		if ...
		JumpToApp(2);
		...
		else
		JumpToApp(3);
		...
		*/
    /* USER CODE BEGIN 3 */
  }
  /* USER CODE END 3 */
}

那么就可以实现运行第二块APP代码
但第二块APP要根据跳转的地址进行调整
譬如:
在这里插入图片描述
同时修改中断向量表的位置
在这里插入图片描述
这里需要取消USER_VECT_TAB_ADDRESS的定义注释 然后修改VECT_TAB_OFFSET偏移 位于system_stm32f4xx.c
在这里插入图片描述
编译后 再次烧录时则在地址0x0800 4000开始
Flash断点和烧录其他位置时不会被清空 但要设置好每块代码区域的位置和大小 否则会冲突

其他程序更新烧录方式

同理 引导程序也不一定非要跳转到BootLeader才能烧录
直接多定义几块Flash区域 一样可以实现引导、备份、运行和烧录
烧录也不一定要用串口 无线传输、文件管理等 只要能写入对应的Flash的位置 就能正常运行

一般来说 预留一个引导 一个备份 一个APP区域就够了
另外 STM32也有一种通过USB DFU的方式进行烧录的方法 该方法实际上也是将一部分区域作为DFU引导 然后将另一部分作为APP区域进行烧录
实际上 有的MCU集成了DFU在系统ROM里面 可以直接跳转DFU替换BootLeader 跳转后 用USB连接MCU即可看到DFU驱动 即可用DFU的方式烧录程序

【STM32】HAL库USB实现软件升级DFU的功能操作及配置

附录:压缩字符串、大小端格式转换

压缩字符串

首先HART数据格式如下:
在这里插入图片描述
在这里插入图片描述
重点就是浮点数和字符串类型
Latin-1就不说了 基本用不到

浮点数

浮点数里面 如 0x40 80 00 00表示4.0f

在HART协议里面 浮点数是按大端格式发送的 就是高位先发送 低位后发送

发送出来的数组为:40,80,00,00

但在C语言对浮点数的存储中 是按小端格式来存储的 也就是40在高位 00在低位
浮点数:4.0f
地址0x1000对应00
地址0x1001对应00
地址0x1002对应80
地址0x1003对应40

若直接使用memcpy函数 则需要进行大小端转换 否则会存储为:
地址0x1000对应40
地址0x1001对应80
地址0x1002对应00
地址0x1003对应00

大小端转换:

void swap32(void * p)
{
    
    
   uint32_t *ptr=p;
   uint32_t x = *ptr;
   x = (x << 16) | (x >> 16);
   x = ((x & 0x00FF00FF) << 8) | ((x >> 8) & 0x00FF00FF);

   *ptr=x;
}

压缩Packed-ASCII字符串

本质上是将原本的ASCII的最高2位去掉 然后拼接起来 比如空格(0x20)
四个空格拼接后就成了
1000 0010 0000 1000 0010 0000
十六进制:82 08 20
对了一下表 0x20之前的识别不了
也就是只能识别0x20-0x5F的ASCII表
在这里插入图片描述

压缩/解压函数后面再写:

//传入的字符串和数字必须提前声明 且字符串大小至少为str_len 数组大小至少为str_len%4*3 str_len必须为4的倍数
uint8_t Trans_ASCII_to_Pack(uint8_t * str,uint8_t * buf,const uint8_t str_len)
{
    
    
   if(str_len%4)
   {
    
    
      return 0;
   }
	 
   uint8_t i=0;
   memset(buf,0,str_len/4*3);	  
   for(i=0;i<str_len;i++)
   {
    
    
      if(str[i]==0x00)
      {
    
    
         str[i]=0x20;
      }
   }

   for(i=0;i<str_len/4;i++)
   {
    
    
      buf[3*i]=(str[4*i]<<2)|((str[4*i+1]>>4)&0x03);
      buf[3*i+1]=(str[4*i+1]<<4)|((str[4*i+2]>>2)&0x0F);
      buf[3*i+2]=(str[4*i+2]<<6)|(str[4*i+3]&0x3F);
   }

   return 1;
}

//传入的字符串和数字必须提前声明 且字符串大小至少为str_len 数组大小至少为str_len%4*3 str_len必须为4的倍数
uint8_t Trans_Pack_to_ASCII(uint8_t * str,uint8_t * buf,const uint8_t str_len)
{
    
    
   if(str_len%4)
   {
    
    
      return 0;
   }

   uint8_t i=0;

   memset(str,0,str_len);

   for(i=0;i<str_len/4;i++)
   {
    
    
      str[4*i]=(buf[3*i]>>2)&0x3F;
      str[4*i+1]=((buf[3*i]<<4)&0x30)|(buf[3*i+1]>>4);
      str[4*i+2]=((buf[3*i+1]<<2)&0x3C)|(buf[3*i+2]>>6);
      str[4*i+3]=buf[3*i+2]&0x3F;
   }

   return 1;
}


大小端转换

在串口等数据解析中 难免遇到大小端格式问题

什么是大端和小端

所谓的大端模式,就是高位字节排放在内存的低地址端,低位字节排放在内存的高地址端。

所谓的小端模式,就是低位字节排放在内存的低地址端,高位字节排放在内存的高地址端。

简单来说:大端——高尾端,小端——低尾端

举个例子,比如数字 0x12 34 56 78在内存中的表示形式为:

1)大端模式:

低地址 -----------------> 高地址

0x12 | 0x34 | 0x56 | 0x78

2)小端模式:

低地址 ------------------> 高地址

0x78 | 0x56 | 0x34 | 0x12

可见,大端模式和字符串的存储模式类似。

数据传输中的大小端

比如地址位、起止位一般都是大端格式
如:
起始位:0x520A
则发送的buf应为{0x52,0x0A}

而数据位一般是小端格式(单字节无大小端之分)
如:
一个16位的数据发送出来为{0x52,0x0A}
则对应的uint16_t类型数为: 0x0A52

而对于浮点数4.0f 转为32位应是:
40 80 00 00

以大端存储来说 发送出来的buf就是依次发送 40 80 00 00

以小端存储来说 则发送 00 00 80 40

由于memcpy等函数 是按字节地址进行复制 其复制的格式为小端格式 所以当数据为小端存储时 不用进行大小端转换
如:

uint32_t dat=0;
uint8_t buf[]={
    
    0x00,0x00,0x80,0x40};
   memcpy(&dat,buf,4);
   float f=0.0f;
   f=*((float*)&dat); //地址强转
   printf("%f",f);

或更优解:

   uint8_t buf[]={
    
    0x00,0x00,0x80,0x40};   
   float f=0.0f;
   memcpy(&f,buf,4);

而对于大端存储的数据(如HART协议数据 全为大端格式) 其复制的格式仍然为小端格式 所以当数据为小端存储时 要进行大小端转换
如:

uint32_t dat=0;
uint8_t buf[]={
    
    0x40,0x80,0x00,0x00};
   memcpy(&dat,buf,4);
   float f=0.0f;
   swap32(&dat); //大小端转换
   f=*((float*)&dat); //地址强转
   printf("%f",f);

或:

uint8_t buf[]={
    
    0x40,0x80,0x00,0x00};
   memcpy(&dat,buf,4);
   float f=0.0f;
   swap32(&f); //大小端转换
   printf("%f",f);

或更优解:

uint32_t dat=0;
uint8_t buf[]={
    
    0x40,0x80,0x00,0x00};
   float f=0.0f;
   dat=(buf[0]<<24)|(buf[0]<<16)|(buf[0]<<8)|(buf[0]<<0)
   f=*((float*)&dat);

总结

固 若数据为小端格式 则可以直接用memcpy函数进行转换 否则通过移位的方式再进行地址强转

对于多位数据 比如同时传两个浮点数 则可以定义结构体之后进行memcpy复制(数据为小端格式)

对于小端数据 直接用memcpy写入即可 若是浮点数 也不用再进行强转

对于大端数据 如果不嫌麻烦 或想使代码更加简洁(但执行效率会降低) 也可以先用memcpy写入结构体之后再调用大小端转换函数 但这里需要注意的是 结构体必须全为无符号整型 浮点型只能在大小端转换写入之后再次强转 若结构体内采用浮点型 则需要强转两次

所以对于大端数据 推荐通过移位的方式来进行赋值 然后再进行个别数的强转 再往通用结构体进行写入

多个不同变量大小的结构体 要主要字节对齐的问题
可以用#pragma pack(1) 使其对齐为1
但会影响效率

大小端转换函数

直接通过对地址的操作来实现 传入的变量为32位的变量
中间变量ptr是传入变量的地址

void swap16(void * p)
{
    
    
   uint16_t *ptr=p;
   uint16_t x = *ptr;
   x = (x << 8) | (x >> 8);

   *ptr=x;
}

void swap32(void * p)
{
    
    
   uint32_t *ptr=p;
   uint32_t x = *ptr;
   x = (x << 16) | (x >> 16);
   x = ((x & 0x00FF00FF) << 8) | ((x >> 8) & 0x00FF00FF);

   *ptr=x;
}

void swap64(void * p)
{
    
    
   uint64_t *ptr=p;
   uint64_t x = *ptr;
   x = (x << 32) | (x >> 32);
   x = ((x & 0x0000FFFF0000FFFF) << 16) | ((x >> 16) & 0x0000FFFF0000FFFF);
   x = ((x & 0x00FF00FF00FF00FF) << 8) | ((x >> 8) & 0x00FF00FF00FF00FF);

   *ptr=x;
}