news 2026/2/26 10:24:01

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

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
[STM32F1] 【每周分享】使用STM32的USB功能在Bootloader中虚拟U盘实现拖拽固件升级

单片机产品开发时可以使用JLink、DAPLink等烧录器进行固件下载和调试,当产品开发完成量产后如果需要升级固件一般都会通过串口、CAN等配合Bootloader进行升级,除了在单片机端实现一个Bootloader外还需要实现一个按照既定通讯协议发送固件的程序,如果使用的单片机支持USB则有更方便的升级方式,接下来实现一个虚拟成U盘拖拽固件升级的Bootloader
这里用到的是一个将USB引出的STM32F103C8T6的核心板


打开STM32CubeMX新建一个STM32F103的工程,配置USB



定义Bootloder大小,实现APP跳转方法,如果不勾选USB MicroLIB需要再定义大一点

复制

  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文件计算,例如下面这个


关于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分出一个最小容量的分区,没有空闲分区的可以在虚拟机里操作,格式化后顺便放进一个提示文件,选中这个分区,记录下这两个数值


打开扇区编辑,全选然后另存为一个文件


用winhex打开刚才保存的文件,找到这几个不为0的数据段





使用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连接电脑和核心板,电脑上会出现这个盘符


打开也能看到提示文件


接下来实现通过U盘接收固件数据,原本想用固定地址接收的,但是有些系统接入U盘后会写入一些系统文件,这样就占用了原本的地址,虽然并没有成功写入但是可能是系统缓存的原因继续添加文件的话文件数据的起始地址就变了,在0x11000这个地址存储着U盘中文件的索引,数据格式如图


向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部分,新建一个工程,在工程设置中修改起始地址和长度


在C/C++标签添加USER_VECT_TAB_ADDRESS到末尾


升级使用的是BIN文件,在User选项卡中添加fromelf --bin -o ".\@L\@L.bin" "#L"就能在编译后生成bin文件了


打开system_stm32f1xx.c修改偏移量


之后就能像平常一样进行编程了,编译出的bin文件就可以拖动到Bootloader的U盘中实现升级了,效果如下

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


---------------------
作者:yuyy1989
链接:https://bbs.21ic.com/icview-3461600-1-1.html
来源:21ic.com
此文章已获得原创/原创奖标签,著作权归21ic所有,任何人未经允许禁止转载。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/2/17 22:26:46

基于Thinkphp和Laravel框架的竞赛管理系统vue

目录具体实现截图项目开发技术介绍PHP核心代码部分展示系统结论源码获取/同行可拿货,招校园代理具体实现截图 本系统&#xff08;程序源码数据库调试部署讲解&#xff09;带文档1万字以上 同行可拿货,招校园代理 基于Thinkphp和Laravel框架的竞赛管理系统vue 项目开发技…

作者头像 李华
网站建设 2026/2/16 2:31:40

one-hot编码

我来详细介绍一下 one-hot 编码&#xff08;独热编码&#xff09;。什么是 One-Hot 编码&#xff1f;One-Hot 编码是一种将分类变量转换为二进制向量的技术&#xff0c;其中每个类别都表示为一个二进制向量&#xff0c;只有一个元素为1&#xff08;"热"&#xff09;&…

作者头像 李华
网站建设 2026/2/21 3:00:48

Agilent安捷伦8564EC-40g频谱分析仪

射频江湖的“老炮儿”&#xff1a;安捷伦8564EC&#xff0c;为何至今仍是传奇&#xff1f;156/2558//3328在射频工程师的实验室里&#xff0c;如果看到一个笨重但依然锃亮的“大铁盒子”&#xff0c;前面板布满实体按键和旋钮&#xff0c;屏幕或许有些发黄&#xff0c;但显示的…

作者头像 李华
网站建设 2026/2/21 5:31:40

提示工程架构师必读:Agentic AI技术生态标准化与开源社区发展报告

提示工程架构师必读:Agentic AI技术生态标准化与开源社区发展报告 引言:Agentic AI的“野蛮生长”与架构师的痛点 1. 从“工具化AI”到“Agentic AI”:一场范式革命 2023年以来,Agentic AI(智能体AI)成为AI领域最热门的方向之一。与传统“被动执行指令”的AI(如ChatG…

作者头像 李华
网站建设 2026/2/22 4:43:54

Pascal VOC数据集划分的致命陷阱与最佳实践:为什么99%的开发者都该以JPEGImages图片文件夹为基准,而不是Annotations XML?

划分基准推荐度优点缺点/风险适用场景JPEGImages&#xff08;图片&#xff09;★★★★★源头操作&#xff0c;保证每张有效样本必有图&#xff1b;通过相同 ID 加载标注天然同步&#xff1b;无图无标注可立即发现并清理&#xff1b;主流框架均以图片列表为准需额外检查标注文件…

作者头像 李华