基于HAL库stm32f103系列 bootloader实现

目录

1bootloader

1.1 什么是bootloader

1.2为什么要在单片机上用bootloader

1.3如何在单片机上实现bootloader

2bootloader的实现

2.1平台

2.2flash

 2.3bootloader实现程序初始化

 2.3.1串口空闲中断DMA不定长度接收

 2.3.2Esp8266透传模式

 2.3.3地址规划

2.3.4 YModem协议

2.3.5 YModem协议flash写入

2.3.4 程序跳转

2.3.5函数主体

2.3.6程序演示

2.3.7调试技巧

 3 后记

1bootloader

1.1 什么是bootloader

bootloader即引导加载程序,通过一个程序引导另外一个程序启动。形象的例子就是就是开机电脑会通过BIOS系统引导windos系统启动。

1.2为什么要在单片机上用bootloader

单片机烧录程序一般是用烧录口烧录,但这样做在实际的工程应用中很不方便。即便是下位机程序也会存在迭代的问题,下位机程序如果每次迭代程序都需要通过烧录口来实现,可以想象一下,发出去的产品,你要一个个去现场迭代程序。如果可以在单片机上用引导程序的方式来更新程序,那么通过上位机即可以实现下位机的迭代。

1.3如何在单片机上实现bootloader

单片机的程序烧录是存储在flash里面的。即便烧录程序的过程就是flash写入的过程,程序实现的过程就是flash读取的过程。接下来bootloader的逻辑就很清晰了,先烧录一段程序,它主要实现两个功能:1.如果需要更新程序就更新程序,既写入flash的内容。2.如果不需要更新程序就引导程序启动,既通过读取flash里面的内容实现程序。这个程序我们称之为bootloader,被引导的程序就称之为app。

2bootloader的实现

2.1平台

下位机平台选用野火指南者开发板,其芯片为stm32f103ve。应用外设:串口1,串口3。分别通过CH340和esp8266同电脑交互。上位机程序用野火串口调试助手(新版,支持协议发送文件,协议为YModem协议)。

2.2flash

flash是一种存储器,即便是掉电了也不会导致内部的信息丢失。单片机的程序就是储存在falsh里面,如图所示,0x08000000-0x0807FFFF,代表的是该芯片(f103ve)flash可用空间。信息在在闪存编辑手册里面也有。f103ve一共有256页flash,每一页可以储存2k的内容。flash在使用前必须要擦除,flash擦除是以页为单位擦除。写入则可以一个字节一个字节的写入。

 2.3bootloader实现程序初始化

要实现bootloader,需要将程序的功能细分。1.如果需要更新程序就更新程序,既写入flash的内容。先实现这一步,1接收到上位机指令需要更新程序,2接收文件内容,3写入flash。首先我们需要串口,YModem协议可以一次性传送128字节或者1024字节的有效信息。我选用串口DMA不定长度接收空闲中断处理,也可以用万能的环形缓冲区。

 2.3.1串口空闲中断DMA不定长度接收

这里主要说一下空闲中断,空闲中断从串口收到第一个字节开始,在一个波特率的时间内没有收到下一个字节,就说明串口属于空闲状态,就会进入空闲中断。熟悉这些的可以直接跳转到2.3.3

#define RX_BUF_LEN 1300//DMA接收数据量长度
uint8_t rx_buf[RX_BUF_LEN];//DMA储存数据缓冲区
void UART_DMA_Init(void)
{
 __HAL_UART_ENABLE_IT(&huart1,UART_IT_IDLE);//使能空闲中断
 HAL_UART_Receive_DMA(&huart1,(uint8_t *)rx_buf,RX_BUF_LEN);//开始不定长度接收
}

空闲中断需要重新理一段代码,HAL初始化中断只会进入接收中断。

/*
名称:串口空闲中断函数
*/
void UART_IDLE_IRQHandler(UART_HandleTypeDef *huart)
{
 if(RESET!=__HAL_UART_GET_FLAG(huart,UART_FLAG_IDLE))
  {
	HAL_UART_RxCpltCallback(huart);//调用中断回调函数
	__HAL_UART_CLEAR_IDLEFLAG(huart);//清除中断位
  }
}

把函数放进中断服务函数里面,就可以实现串口中断。

中断回调函数里面添加这一段代码,当有空闲中断的时候会更新接收到的数据长度,处理对应数据长度接收到的数据长度会清0。

void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart)
{
 if(huart->Instance==USART1)//空闲中断
	 Loader_Receive(); 
 if(huart->Instance==USART3)
	 ESP8266_Receive();
}

uint16_t rx_buf_len=0;//接收到的数据长度
void Loader_Receive(void)
{
 uint8_t tmp[1];
 HAL_UART_Receive(&huart1,tmp,1,1);//读取一次DR寄存器,防止重复进入中断
 USART1->DR=0;//串口数据缓冲区清0
 rx_buf_len=RX_BUF_LEN-__HAL_DMA_GET_COUNTER(&hdma_usart1_rx);//获取接收数据长度
 HAL_UART_AbortReceive(&huart1);//关闭DMA传送
 HAL_UART_Receive_DMA(&huart1,(uint8_t *)rx_buf,RX_BUF_LEN);//开启一次新的DMA
}

 2.3.2Esp8266透传模式

通过Esp8266连接wifi透传模式,可以用野火串口调试助手实现对两个串口的交互。这里简单的说下初始化。当然用如果用可以同时交互两个串口,完全用不到这里。放下代码。具体初始化内容为TA测试->设置用户模式>-连接wifi->连接服务器->设置透传模式->使能透传模式。同样用DMA串口不定长度接收

/*
AT+CIPSEND使能透传模式
*/
uint8_t Esp8266_ENtransmit_single(void)
{
 Usart1_SendString((uint8_t*)"正在使能透传模式...\r\n");	
 Usart3_SendString((uint8_t*)"AT+CIPSEND\r\n");
 HAL_Delay(500);	
 if(wifi_rx_buf[16]!=0x4F || wifi_rx_buf[17]!=0x4B)//OK判定失败
 return 1;
 else 
 Usart1_SendString((uint8_t*)"使能透传模式成功...\r\n"); 	
 return 0;	
}
/*
AT+CIPMODE=1,设置透传模式,在此模式下输入即输出
*/
uint8_t Esp8266_Transmit_single(void)
{
 Usart1_SendString((uint8_t*)"正在设置透传模式...\r\n");	
 Usart3_SendString((uint8_t*)"AT+CIPMODE=1\r\n");	
 HAL_Delay(500);	
 if(wifi_rx_buf[18]!=0x4F || wifi_rx_buf[19]!=0x4B)//OK判定失败
 return 1;
 else 
 Usart1_SendString((uint8_t*)"透传模式设置成功\r\n"); 	
 return 0;
}
/*
AT+CIPSTART=" 连接模式","服务器地址","服务器端口"。连接到指定服务器
*/
uint8_t Esp8266_server_single(void)
{
 Usart1_SendString((uint8_t*)"正在通过AT+CIPSTART连接服务器...\r\n");	
 Usart3_SendString((uint8_t*)"AT+CIPSTART=\"TCP\",\"192.168.10.116\",8000\r\n");	
 HAL_Delay(500);	
 if(wifi_rx_buf[11]!=0x4F || wifi_rx_buf[12]!=0x4B)//OK判定失败
 return 1;
 else 
 Usart1_SendString((uint8_t*)"连接server成功\r\n"); 	
 return 0; 	
}
/*
设置AT+CWJAP="用户","密码"
*/
uint8_t Esp8266_wifi_single(void)
{
 Usart1_SendString((uint8_t*)"正在连接wifi...\r\n");	
 Usart3_SendString((uint8_t*)"AT+CWJAP=\"SBYF\",\"88888888\"\r\n");
 HAL_Delay(2500);//延时等待设备连接	
 if(wifi_rx_buf[3]!=0x4F || wifi_rx_buf[4]!=0x4B)//OK判定失败
 return 1;
 else 
 Usart1_SendString((uint8_t*)"连接WIFI成功\r\n"); 	
 return 0; 
}
/*
设置:AT+CWMODE=1,可以作为用户连接路由器
*/
uint8_t Esp8266_Mode_single(void)
{
 Usart1_SendString((uint8_t*)"设置esp用户模式:AT+CWMODE=1...\r\n");
 Usart3_SendString((uint8_t*)"AT+CWMODE=1\r\n");
 HAL_Delay(1000);
 if(wifi_rx_buf[17]!=0x4F || wifi_rx_buf[18]!=0x4B)//OK判定失败
 return 1;
 else 
 Usart1_SendString((uint8_t*)"设置AT+CWMODE=1成功\r\n"); 
 return 0;
}
/*
TA测试
*/
uint8_t Esp8266_TA_single(void)
{
 Usart1_SendString((uint8_t*)"正在进行TA测试...\r\n");	
 Usart3_SendString((uint8_t*)"AT\r\n");
 HAL_Delay(500); 
 if(wifi_rx_buf[1]!=0x41 || wifi_rx_buf[2]!=0x54)//帧头不对(AT)
 return 1; 
 else 
 Usart1_SendString((uint8_t*)"AT测试通过\r\n");
 return 0;
}

 2.3.3地址规划

我们需要把app程序放在我们指定的地方,为flash写入做准备,这里我规划bootloader有16kb的空间,即8页flash。那么app程序就得从第9页flash写入,计算app写入地址为0x08004000。这个程序,0x08000000—0x08004000为bootloader的地址规划,后面的flash为app的地址规划。

2.3.4 YModem协议

YModem协议可以一次性传递128或者1024的字节长度数据。这里简单说下野火串口调试助手的YModem协议。

上位机点击发送文件,进入等待状态,等待下位机响应。下位机响应0x43,说明可以开始数据传递,每次接受完一帧数据下位机响应0x06,应答,等待上位机发送下一帧数据。上位机发送完以后会发送0x04,说明文件传输结束。

 

需要完整的了解,还要去看野火的协议详解。然后我们就可以写一个app文件,生成bin文件,开始文件传输的实验。bin文件一般来说都会大于1k,所以我们用到的是模式2。

我们需要建立一个结构体来为我们写flash做一些准备。

typedef struct
{
 uint8_t data_serial;//数据序号
 uint8_t data_mode;//数据模式
 uint16_t Erase_space;//擦除空间
 uint16_t Occupy_space;	//占用空间
 uint32_t flash_addr; //flash写入地址
}
Loader_agreement;

数据序号,对应位1,说明是第几组数据。数据模式对应位0,说明是128长度还是1024长度。擦除空间,准备好写入flash的空间在使用flash以前应该先进行页擦除。占用空间,已经写入flash的空间,通过这两个判断是否需要擦除更多的flash。flash写入地址,数据的写入地址。数据长度最长是1029。建立1030长度的DMA储存缓冲区完全够用。设置flash写入地址为0x08004000,。至此初始化部分就完成了。C转为hex就是0x43。

初始化部分,System_Init为提示信息。

  esp8266_init();
  UART_DMA_Init();
  System_Init(); 
void System_Init(void)
{
 HAL_TIM_Base_Start_IT(&htim2);
 Usart1_SendString((uint8_t*)"进入系统引导界面...\r\n");
 Usart1_SendString((uint8_t*)"输入C开始系统装载...\r\n");
 Usart1_SendString((uint8_t*)"10s后进入系统引导...\r\n");
}

app文件,主要作用就是开中断打印1s一次的测试。

  MX_GPIO_Init();
  MX_DMA_Init();
  MX_USART1_UART_Init();
  MX_USART3_UART_Init();
  MX_TIM2_Init();
  /* USER CODE BEGIN 2 */
  USARTx_SendString(&huart1,(uint8_t *)"启动成功\r\n");
  HAL_TIM_Base_Start_IT(&htim2);
while(1)
{

}

uint16_t count=1000;
void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim)
{
 if(htim->Instance==TIM2)//1ms中断
  {
	count--;
   if(count<=1)
	{
	 USARTx_SendString(&huart1,(uint8_t*)"中断测试\r\n");
	 count=1000;
	}	
  }
}

2.3.5 YModem协议flash写入

写入的过程,就是下载程序更新程序的过程。协议解析代码段,写的不严谨,但是勉强能用。

uint8_t i;	
uint32_t data[0xFF+1];
uint8_t *ptr=rx_buf;
void Loader_write_flash(void)
{
if(rx_buf_len>0)
 {
  rx_buf_len=0;		
  if(ptr[0]==0x04)
  {	  
   Usart3_SendString((uint8_t*)"程序写入结束\r\n");
   return;
  }	  
	Loader.data_mode=ptr[0];//协议模式
	Loader.data_serial=ptr[1];//协议序号
	 if(Loader.data_serial>0)
	 {
     if(Loader.data_mode==0x01)
			i=0x1F;
		 else if(Loader.data_mode==0x02)
          i=0xFF;
		if(i==0xFF)
		 {	
         uint32_t tmp;			 
        for(uint16_t count=0;count<i+1;count++)
        {
         tmp=0; 			  
			tmp+=ptr[4*count+3]<<0;
			tmp+=ptr[4*count+4]<<8;
			tmp+=ptr[4*count+5]<<16;
			tmp+=ptr[4*count+6]<<24;
			data[count]=tmp;  
		  }
		  if(Loader.Occupy_space==Loader.Erase_space)//没有使用的空间就需要擦除
		  {			  
		   FLASH_Erase(Loader.flash_addr);
		   Loader.Erase_space+=0x800;//擦除以后增加1页flash(2K)空间
		  }
		  FLASH_Write(Loader.flash_addr,(uint32_t *)&data,0xFF+1);
		  Loader.Occupy_space+=0x400;//写入1次写入1k
		  Loader.flash_addr+=0x400;//每次写入1k数据,首地址增加1k。
	    } 
	   }
	   uint8_t tmp=0x06;//应答一次	
      HAL_UART_Transmit(&huart1,(uint8_t *)&tmp,1,1000);	
		Usart3_SendString((uint8_t*)"完成一次应答\r\n");
	}	
}

野火串口调试第一次送信息为文件名字,长度为128,序列号0,这一条可以忽略,只需要从第二次开始解析就行了。

stm32flash写入支持1一个字一个字的写入,就是32位,同时写入模式为高位在后,低位在前,即一个0x0800400的数据写入flash按照地址读取为0x0040080,所以需要对数据调换顺序。

        for(uint16_t count=0;count<i+1;count++)
        {
         tmp=0; 			  
			tmp+=ptr[4*count+3]<<0;
			tmp+=ptr[4*count+4]<<8;
			tmp+=ptr[4*count+5]<<16;
			tmp+=ptr[4*count+6]<<24;
			data[count]=tmp;  
		  }

根据情况,看是否需要开辟新的新空间(擦除flash),每次写入地址后首地址都应该递增。

		  if(Loader.Occupy_space==Loader.Erase_space)//没有使用的空间就需要擦除
		  {			  
		   FLASH_Erase(Loader.flash_addr);
		   Loader.Erase_space+=0x800;//擦除以后增加1页flash(2K)空间
		  }
		  FLASH_Write(Loader.flash_addr,(uint32_t *)&data,0xFF+1);
		  Loader.Occupy_space+=0x400;//写入1次写入1k
		  Loader.flash_addr+=0x400;//每次写入1k数据,首地址增加1k。
	    } 

至此flash就写完了。如果需要观察协议的写入数据是什么,可以用以下函数实现

void test_task(void)
{
	if(rx_buf_len>0)//串口1收到信息,通过透传模式发送给串口3
	{	
	  HAL_UART_Transmit_DMA(&huart3,(uint8_t *)rx_buf,rx_buf_len);
	   rx_buf_len=0;	
	  uint8_t tmp=0x06;	
     HAL_UART_Transmit(&huart1,(uint8_t *)&tmp,1,1000);			
	}
	if(rx3_buf_len>0)//串口3收到信息,通过透传模式发送给串口1
	{
	  HAL_UART_Transmit_DMA(&huart1,(uint8_t *)wifi_rx_buf,rx3_buf_len);
	  rx3_buf_len=0;	
	}		
}

串口1收到的数据串口3发出来。

2.3.4 程序跳转

如果不需要更新程序,就进入引导进入app。跳转程序借鉴别人的,也参考了很网上很多人的写法。https://www.freesion.com/article/2530714904/https://www.freesion.com/article/2530714904/具体思路:建立函数指针,给函数指针指定地址,初始化堆栈指针,重定向中断向量表,运行函数。代码如下

void System_jumpApp(uint32_t APP_ADDR)
{
 if(((*(volatile uint32_t*)APP_ADDR)&0x2FFE0000)==0x20000000)//是否有数据写入
  {
	void (*SysJumpapp)(void); //声明一个函数指针
	__disable_irq();//关闭所有中断
	HAL_SuspendTick();//关闭定时器
	HAL_RCC_DeInit();//设置默认时钟HSI
	for(uint32_t i=0;i<8;i++)//清除所有中断位
	{
	 NVIC->ICER[i]=0xFFFFFFFF;
	 NVIC->ICPR[i]=0xFFFFFFFF;
	}
	SCB->VTOR=FLASH_BASE|0x4000;//重定向中断向量表
	HAL_ResumeTick();//开启滴答定时器
	__enable_irq();//开启全局中断
	SysJumpapp = (void (*)(void)) (*((uint32_t *) (APP_ADDR + 4)));//中断复位地址
	__set_MSP(*(uint32_t *)APP_ADDR);//设置函数主堆栈指针
	__set_CONTROL(0);//如果使用了RTOS工程,需要用到这条语句,设置为特权级模式,使用MSP指针
	SysJumpapp();//跳转
	}
}

首先重定向中断向量表,不然不能在app里面正确开启中断,比如我程序app程序偏移0x4000的地址,那么我中断向量表也要偏移0x4000。在做这些之前,为了保险起见,应该把能关的都关了。然后设置中断复位地址和主堆栈指针。程序刚开始启动,在中断和堆栈建立完成以后会进入中断复位程序,然后进入main函数。最后跳转就行了。

2.3.5函数主体

通过cubemx初始化,如果串口要使用DMA,应该先开启DMA外设,再去开串口,保证DMA的时钟在串口之前开启,不然会出问题。

uint8_t count_s;
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();
  MX_DMA_Init();
  MX_USART1_UART_Init();
  MX_USART3_UART_Init();
  MX_TIM2_Init();
  /* USER CODE BEGIN 2 */
  esp8266_init();
  UART_DMA_Init();
  System_Init(); 
  /* USER CODE END 2 */

  /* Infinite loop */

  /* USER CODE BEGIN WHILE */
  while (1)
  {
      if(count_ms>999)
    {
	  count_ms=0;
	  count_s++;
	  printf("计时%d秒\r\n",count_s);
	  if(count_s>=10)
	  {	  
	   HAL_TIM_Base_Stop_IT(&htim2);
		printf("跳转APP\r\n");  
		System_jumpApp(Loader.flash_addr);  
	  } 
	 }		  
	 if(rx3_buf_len>0)//串口3收到信息,通过透传模式发送给串口1,以此实现手动发送信息
	 { 
	  HAL_UART_Transmit_DMA(&huart1,(uint8_t *)wifi_rx_buf,rx3_buf_len);
     rx3_buf_len=0;		  
	 if(wifi_rx_buf[0]==0x43)
	  {	  
      Usart3_SendString((uint8_t*)"开始执行程序写入\r\n");	
      HAL_TIM_Base_Stop_IT(&htim2);		  
     } 
	 }
   Loader_write_flash();
  /* USER CODE END WHILE */
	
  /* USER CODE BEGIN 3 */

 }
  /* USER CODE END 3 */
}

2.3.6程序演示

程序写入

 串口3回复接收信息。

 复位后不操作等待进入引导

 说明跳转成功,重定向中断向量表成功。

2.3.7调试技巧

1:当不确定是协议写入问题还是程序跳转问题的时候,可以改变烧录地址验证问题

设置IROM1偏移,就是flash写入的偏移。

2:生成bin文件需要设置Run #1,要通过kile的 fromelf工具,生成bin文件。 

 3 后记

程序现在只是,也算是把bootloader调试通了,能跑就行了,本身的可以移植性,可操作性,风险验证性并不高,bug也有很多,还需很多调试修正,写这文章最主要的目的是为了记录笔记,记录学习过程。

  • 1
    点赞
  • 51
    收藏
    觉得还不错? 一键收藏
  • 3
    评论
基于HAL库STM32F4 Bootloader是一种自定义引导程序,用于STM32F4 MCU。它通常由两部分组成,即Bootloader和应用程序(App)。 Bootloader是启动程序的一部分,位于用户的Flash区域的前部。它在芯片启动后首先运行,并负责进行硬件的初始化。初始化完成后,Bootloader会跳转到对应的应用程序。 使用HAL库可以方便地开发STM32F4 BootloaderHAL库是一种硬件抽象层,提供了许多功能和API,使开发者能够更轻松地访问和控制硬件资源。通过HAL库,开发者可以编写自定义的Bootloader代码,实现芯片的初始化和应用程序的跳转。 总结起来,基于HAL库STM32F4 Bootloader是一种自定义引导程序,用于进行硬件初始化并跳转到应用程序。<span class="em">1</span><span class="em">2</span><span class="em">3</span> #### 引用[.reference_title] - *1* [stm32f446_custom_bootloader:基于STM32CUBE HAL的STM32F446 MCU的自定义引导程序](https://download.csdn.net/download/weixin_42136837/15649570)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v93^chatsearchT3_2"}}] [.reference_item style="max-width: 50%"] - *2* *3* [[笔记]STM32基于HAL编写Bootloader+App程序结构](https://blog.csdn.net/qq_33591039/article/details/121562204)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v93^chatsearchT3_2"}}] [.reference_item style="max-width: 50%"] [ .reference_list ]
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值