返回列表 发新帖我要提问本帖赏金: 130.00元(功能说明)

[STM32F1] 【每周分享】使用STM32的USB功能在Bootloader中虚拟U盘实现拖拽固件升级

[复制链接]
 楼主| yuyy1989 发表于 2025-6-14 15:35 | 显示全部楼层 |阅读模式
<
#申请原创# @21小跑堂
单片机产品开发时可以使用JLink、DAPLink等烧录器进行固件下载和调试,当产品开发完成量产后如果需要升级固件一般都会通过串口、CAN等配合Bootloader进行升级,除了在单片机端实现一个Bootloader外还需要实现一个按照既定通讯协议发送固件的程序,如果使用的单片机支持USB则有更方便的升级方式,接下来实现一个虚拟成U盘拖拽固件升级的Bootloader
这里用到的是一个将USB引出的STM32F103C8T6的核心板
屏幕截图 2025-06-14 133008.png
打开STM32CubeMX新建一个STM32F103的工程,配置USB
屏幕截图 2025-06-14 133243.png
屏幕截图 2025-06-14 133344.png
定义Bootloder大小,实现APP跳转方法,如果不勾选USB MicroLIB需要再定义大一点
屏幕截图 2025-06-14 135123.png
  1. #define BOOT_SIZE 0x4000
  2. #define APP_ADDR (FLASH_BASE + BOOT_SIZE)
  3. typedef  void (*pFunction)(void);
  4. void BL_StartAPP(void)   
  5. {
  6.     pFunction start_application;
  7.     uint32_t app_address;

  8.     __disable_irq();
  9.     app_address = *(__IO uint32_t*) (APP_ADDR + 4);
  10.     start_application = (pFunction) app_address;
  11.     __set_MSP(*(__IO uint32_t*) app_address);
  12.     start_application();
  13. }
Bootloder大小可以完成Bootloader后再调整,可以通过编译出的Hex文件计算,例如下面这个
屏幕截图 2025-06-14 130335.png
关于Hex文件的格式可以在网上搜索,最后的地址是0x3FE0,最后一条的数据长度是0C,所以Bootloder大小为0x3FEC,定义为0x4000就够了
使用PB9来控制是否进入Bootloader,当PB9为高电平时跳转到APP,在GPIO初始化后添加
  1. if(LL_GPIO_IsInputPinSet(KEY_GPIO_Port,KEY_Pin))
  2. {
  3.         LL_mDelay(100);
  4.         if(LL_GPIO_IsInputPinSet(KEY_GPIO_Port,KEY_Pin))
  5.         {
  6.                 LL_GPIO_SetOutputPin(LED_GPIO_Port,LED_Pin);
  7.                 BL_StartAPP();
  8.         }
  9. }
  10. LL_GPIO_ResetOutputPin(LED_GPIO_Port,LED_Pin);if(LL_GPIO_IsInputPinSet(KEY_GPIO_Port,KEY_Pin))
  11. {
  12.         LL_mDelay(100);
  13.         if(LL_GPIO_IsInputPinSet(KEY_GPIO_Port,KEY_Pin))
  14.         {
  15.                 LL_GPIO_SetOutputPin(LED_GPIO_Port,LED_Pin);
  16.                 BL_StartAPP();
  17.         }
  18. }
接下来需要虚拟一个文件系统,这里如果对文件系统不熟悉也没关系,可以使用分区工具DiskGenius分出一个最小容量的分区,没有空闲分区的可以在虚拟机里操作,格式化后顺便放进一个提示文件,选中这个分区,记录下这两个数值
屏幕截图 2025-06-13 072615.png
打开扇区编辑,全选然后另存为一个文件
屏幕截图 2025-06-14 131236.png
用winhex打开刚才保存的文件,找到这几个不为0的数据段
屏幕截图 2025-06-12 192126.png
屏幕截图 2025-06-12 192226.png
屏幕截图 2025-06-12 192325.png
屏幕截图 2025-06-12 192854.png
使用winhex的复制为C源码的选项将数据粘贴到代码中,其中DBR中有一大段是启动代码可以忽略,FAT1和FAT2的数据一样
  1. #define BL_FAT_CLUSTER_SIZE             0x200       //簇大小
  2. #define BL_FAT_INDEX_START_ADDR         0x11000     //目录起始地址
  3. #define BL_FAT_INDEX_BIN_START_ADDR     0x11080     //写入的BIN文件起始地址
  4. #define BL_FAT_DATA_START_ADDR          0x15000     //数据起始地址
  5. //以下是一个8M的FAT16分区的数据结构
  6. //DBR 0x00-0x1FF 以55 AA结尾,引导程序代码部分省略
  7. const uint8_t BL_FAT_dbr[70] = {
  8.         0xEB, 0x3C, 0x90, 0x4D, 0x53, 0x44, 0x4F, 0x53, 0x35, 0x2E, 0x30, 0x00, 0x02, 0x01, 0x08, 0x00,
  9.         0x02, 0x00, 0x02, 0x00, 0x40, 0xF8, 0x40, 0x00, 0x3F, 0x00, 0xFF, 0x00, 0x00, 0x08, 0x00, 0x00,
  10.         0x00, 0x00, 0x00, 0x00, 0x80, 0x00, 0x29, 0x23, 0x48, 0x00, 0x00, 0x20, 0x20, 0x20, 0x20, 0x20,
  11.         0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x46, 0x41, 0x54, 0x31, 0x36, 0x20, 0x20, 0x20, 0x59, 0x55,
  12.     0x59, 0x59, 0x31, 0x39, 0x38, 0x39
  13. };
  14. //FAT1 0x1000 FAT2 0x9000
  15. const uint8_t BL_FAT_fat1_fat2[4]=
  16. {
  17.     0xF8,0xFF,0xFF,0xFF
  18. };
  19. //根目录 0x11000
  20. const uint8_t BL_FAT_root_dir[128] = {
  21.         0x53, 0x54, 0x4D, 0x33, 0x32, 0x42, 0x4F, 0x4F, 0x54, 0x20, 0x20, 0x08, 0x00, 0x00, 0x00, 0x00,
  22.         0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x75, 0x95, 0xCC, 0x5A, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
  23.         0x42, 0x68, 0x00, 0x65, 0x00, 0x72, 0x00, 0x65, 0x00, 0x00, 0x00, 0x0F, 0x00, 0x64, 0xFF, 0xFF,
  24.         0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x00, 0x00, 0xFF, 0xFF, 0xFF, 0xFF,
  25.         0x01, 0x70, 0x00, 0x75, 0x00, 0x74, 0x00, 0x20, 0x00, 0x62, 0x00, 0x0F, 0x00, 0x64, 0x69, 0x00,
  26.         0x6E, 0x00, 0x20, 0x00, 0x66, 0x00, 0x69, 0x00, 0x6C, 0x00, 0x00, 0x00, 0x65, 0x00, 0x20, 0x00,
  27.         0x50, 0x55, 0x54, 0x42, 0x49, 0x4E, 0x7E, 0x31, 0x20, 0x20, 0x20, 0x20, 0x00, 0xB3, 0x93, 0x95,
  28.         0xCC, 0x5A, 0xCC, 0x5A, 0x00, 0x00, 0x94, 0x95, 0xCC, 0x5A, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00
  29. };
接下来实现USB读取方法,将这些数据发送给电脑
  1. void BL_FAT_ReadBlocks512(uint32_t block_addr,uint8_t *data,uint16_t block_len)
  2. {
  3.     uint32_t data_index = block_addr * 512;
  4.     uint32_t data_len = block_len * 512;
  5.     memset(data,0,data_len);
  6.     if(data_index == 0)//DBR
  7.     {
  8.         memcpy(data,BL_FAT_dbr,70);
  9.         data[510] = 0x55;
  10.         data[511] = 0xAA;
  11.     }
  12.     else if(data_index == 0x1000 || data_index == 0x9000) //FAT1 FAT2
  13.     {
  14.         memcpy(data,BL_FAT_fat1_fat2,4);
  15.     }
  16.     else if(data_index == 0x11000)//根目录
  17.     {
  18.         memcpy(data,BL_FAT_root_dir,128);
  19.     }
  20. }void BL_FAT_ReadBlocks512(uint32_t block_addr,uint8_t *data,uint16_t block_len)
  21. {
  22.     uint32_t data_index = block_addr * 512;
  23.     uint32_t data_len = block_len * 512;
  24.     memset(data,0,data_len);
  25.     if(data_index == 0)//DBR
  26.     {
  27.         memcpy(data,BL_FAT_dbr,70);
  28.         data[510] = 0x55;
  29.         data[511] = 0xAA;
  30.     }
  31.     else if(data_index == 0x1000 || data_index == 0x9000) //FAT1 FAT2
  32.     {
  33.         memcpy(data,BL_FAT_fat1_fat2,4);
  34.     }
  35.     else if(data_index == 0x11000)//根目录
  36.     {
  37.         memcpy(data,BL_FAT_root_dir,128);
  38.     }
  39. }
在usbd_storage_if.c中的STORAGE_Read_FS调用这个方法,编译烧录后拉低PB9通过USB连接电脑和核心板,电脑上会出现这个盘符
屏幕截图 2025-06-14 130906.png
打开也能看到提示文件
屏幕截图 2025-06-14 130927.png
接下来实现通过U盘接收固件数据,原本想用固定地址接收的,但是有些系统接入U盘后会写入一些系统文件,这样就占用了原本的地址,虽然并没有成功写入但是可能是系统缓存的原因继续添加文件的话文件数据的起始地址就变了,在0x11000这个地址存储着U盘中文件的索引,数据格式如图
屏幕截图 2025-06-13 074447.png
向U盘添加文件后会在后面顺序添加这个文件的索引,对于长文件名的文件最后也会追加一个短文件名的索引,因此只需要判断最后一个文件索引的扩展名是不是BIN就行了。
虚拟数据已经占用了前4个索引,因此从第5个索引开始查找,找到最后一个索引,判断文件是否为BIN格式,如果是BIN格式,获取到数据的簇偏移号和大小,成功获取到簇偏移号后就能计算出数据的写入地址,然后等待USB写入数据并将固件的数据写入内部FLASH
  1. void BL_FAT_WriteBlocks512(uint32_t block_addr,uint8_t *data,uint16_t block_len)
  2. {
  3.     uint32_t data_index = block_addr * 512;
  4.     uint32_t data_len = block_len * 512;
  5.     uint16_t sum = 0;
  6.     if(data_index < BL_FAT_INDEX_START_ADDR)
  7.         return;
  8.     if(find_bin_data_addr == 0) //还未找到固件起始地址,寻找最后一个不为0的文件索引
  9.     {
  10.         if(data_index < BL_FAT_DATA_START_ADDR)
  11.         {
  12.             if(data_index < BL_FAT_INDEX_BIN_START_ADDR)
  13.             {
  14.                 data_index += 0x80;
  15.                 data += 0x80;
  16.                 data_len -= 0x80;
  17.                 find_bin_data_addr = 0;
  18.                 current_fat_file_addr = BL_FAT_INDEX_BIN_START_ADDR;
  19.                 current_bin_data_addr = 0;
  20.                 current_bin_data_size = 0;
  21.                 current_flash_addr = APP_ADDR;
  22.             }
  23.             if(data_index > current_fat_file_addr + 31)
  24.                 return;
  25.             while(data_len > 0)
  26.             {
  27.                 sum = 0;
  28.                 for(uint8_t i = 0;i<32;i++)
  29.                 {
  30.                     sum += data[i];
  31.                 }
  32.                 if(sum == 0)
  33.                 {
  34.                     if(current_bin_data_addr != 0 && current_bin_data_size != 0)
  35.                     {
  36.                         find_bin_data_addr = 1;
  37.                     }
  38.                     break;
  39.                 }
  40.                 else
  41.                 {
  42.                     current_fat_file_addr = data_index;
  43.                     if(data[11] == 0x20 && data[8] == 'B' && data[9] == 'I' && data[10] == 'N' && (data[26] > 1 || data[27] > 0))
  44.                     {
  45.                         current_bin_data_addr = BL_FAT_DATA_START_ADDR + (data[26]+(data[27]<<8)-2)*BL_FAT_CLUSTER_SIZE; //减2才是真正的簇号
  46.                         current_bin_data_size = data[28] + (data[29]<<8) + (data[30]<<16) + (data[31]<<24);
  47.                         if(current_bin_data_size + BOOT_SIZE> (*(uint16_t*)(FLASHSIZE_BASE))*1024) //固件过大
  48.                         {
  49.                             current_bin_data_size = 0;
  50.                             led_delay = 200;
  51.                         }
  52.                     }
  53.                 }
  54.                 data_index += 32;
  55.                 data += 32;
  56.                 data_len -= 32;
  57.             }
  58.         }
  59.     }
  60.     else
  61.     {
  62.         if(data_index < current_bin_data_addr)
  63.             return;
  64.         if(data_len > current_bin_data_size)
  65.         {
  66.             BL_FlashApp(data,current_bin_data_size);//写入FLASH的方法省略,网上例程很多
  67.             current_bin_data_size = 0;
  68.         }
  69.         else
  70.         {
  71.             BL_FlashApp(data,data_len);
  72.             current_bin_data_size -= data_len;
  73.         }
  74.         if(current_bin_data_size == 0)
  75.         {
  76.             find_bin_data_addr = 0;
  77.             led_delay = 1000;
  78.         }
  79.     }
  80. }
至此Bootloader部分就完成了,接下来实现APP部分,新建一个工程,在工程设置中修改起始地址和长度
屏幕截图 2025-06-14 141910.png
在C/C++标签添加USER_VECT_TAB_ADDRESS到末尾
屏幕截图 2025-06-14 142002.png
升级使用的是BIN文件,在User选项卡中添加fromelf --bin -o ".\@L\@L.bin" "#L"就能在编译后生成bin文件了
屏幕截图 2025-06-14 142123.png
打开system_stm32f1xx.c修改偏移量
屏幕截图 2025-06-14 142259.png
之后就能像平常一样进行编程了,编译出的bin文件就可以拖动到Bootloader的U盘中实现升级了,效果如下

在此基础上还可以增加文件校验加密传输防止降级等功能,有兴趣的可以自行尝试



打赏榜单

lvxinjia123 打赏了 30.00 元 2025-08-02
理由:试了一下真的可以

21小跑堂 打赏了 100.00 元 2025-06-19
理由:恭喜通过原创审核!期待您更多的原创作品~

评论

通过配置STM32F103的USB功能和Bootloader,实现拖拽固件升级代码。实现的结果较好,但是内容略显单薄,期待更加充实的优质原创。  发表于 2025-6-19 14:05
goyhuan 发表于 2025-6-21 10:41 来自手机 | 显示全部楼层
很具体
kissdb 发表于 2025-6-30 11:48 | 显示全部楼层
这个模拟出来的U盘,手机可以识别吗
 楼主| yuyy1989 发表于 2025-7-1 09:39 | 显示全部楼层
kissdb 发表于 2025-6-30 11:48
这个模拟出来的U盘,手机可以识别吗

试了一下 可以识别但是没法升级
您需要登录后才可以回帖 登录 | 注册

本版积分规则

认证:同飞软件研发工程师
简介:制冷系统单片机软件开发,使用PID控制温度

161

主题

815

帖子

10

粉丝
快速回复 在线客服 返回列表 返回顶部