完善资料让更多小伙伴认识你,还能领取20积分哦, 立即完善>
扫一扫,分享给好友
最近在琢磨单片机在线更新程序的事情,查资料查到在STM32上实现一个bootloader比较简单,废话不多说,动手尝试一下。
0、项目目标 为F103C8编写一个bootloader工程,占用flash地址为:0x08000000~0x08001FFF,共8KB。这个bootloader能够从0x08002000处运行代码。(后期可能会对bootloader进行升级,增加从某处接收固件的功能) 1、准备硬件 硬件用的是淘宝上随处可见的F103C8T6核心板,便宜,外设简单,用来做这个测试最好不过了。唯一的缺点就是FLASH有点小,才64KB(对于平时工作用的16位机来说,64KB好像也挺大了嚯)。核心板上通常带有一颗LED,用来指示状态或者调试也应该足够了。此外还需要准备一个STLINK烧录调试器,用来烧写程序和调试。 2、创建工程 本来打算用Stm32cubeIDE做的,但是在IDE上实在是没找到能够修改二进制文件起始地址的地方。因此使用stm32cubeMX生成MDK的工程,然后使用MDK进行编译。(吐槽一下MDK的编译HAL的速度,真的很慢)。 工程配置非常简单,使能外部时钟、配置LED引脚为输出。如下图: 配置完成后直接生成工程即可。 3、编写代码 3.1、测试工程 先写个简单的程序测试一下生成的工程能否正常工作。在main函数的while循环里添加如下代码。功能非常简单,就是LED每隔1秒钟亮灭。 /* USER CODE BEGIN WHILE */ while (1) { /* USER CODE END WHILE */ /* USER CODE BEGIN 3 */ HAL_GPIO_TogglePin(LED_GPIO_Port, LED_Pin); HAL_Delay(1000); } /* USER CODE END 3 */ 观察核心板上的LED状态,LED开始闪烁,说明工程暂时没有问题。接着开始进入正题。 3.2、理论知识 在正式写代码之前,先补充一点关于CM3启动的知识。(水平有限,不太专业,大概理解一下好了) 3.2.1、单片机上电后,从0x08000000地址运行程序 当单片机上电时,内核会检查BOOT引脚的情况,从而引导从哪个存储器启动。这里我们只讨论从主闪存存储器启动的情况,也就是上电后,从0x08000000地址运行程序。 观察MDK默认的编译配置可发现,我们写程序编译出来的二进制文件,是存放在0x08000000起始的闪存内。如下图: 实际情况是这样的吗?我们可以查看编译后的Bin文件来验证一下(这里需要调用MDK的hex转bin插件,上网搜一下就行)。用UltraEdit打开刚才编写编译的Bin文件(节选),如下图所示: 接着我们打开MDK,通过STLINK进入调试状态,从View->Memory Windows->Memory 窗口查看0x08000000地址的数据(节选)。如下图所示: 两张图的数据完全相同,至此,可以验证上面的说法:上电后,单片机从0x08000000处开始启动用户程序。 3.2.2、单片机如何运行到main()函数 要弄懂单片机如何运行到main()函数的问题,实际上就是探讨单片机是如何启动的问题。我们可以用刚刚LED闪烁的工程来分析。我们生成上述工程的反汇编文件,结合工程里的startup_stm32f103xb.s文件,一起分析启动流程。 首先生成反汇编文件。在MDK的配置选项里面可以开启,添加插件后再编译一次即可得到*.asm反汇编文件。(上文所述的生成Bin文件也可以在这里配置)具体配置如下图所示,添加两句语句即可。 重新编译,打开反汇编文件。同时打开startup_stm32f103xb.s一起分析。 先看中断向量表部分。如下图所示,左边为startup_stm32f103xb.s文件,右边为反汇编文件。 … …(漂亮的省略号) … 左边第61行到122行,即从标号Vectors 到 Vectors_End的区域称为中断向量表。(PS:实际上0x08000000这个地址存放的20000140不是中断向量,而是栈顶地址,这里不知道为什么要用Vectors 来标记)。 如果不太了解底层的话,可能不清楚中断向量表有什么用。这里简单解释一下(水平有限,不一定对)。中断向量表里面保存着中断向量,说到底就是某个内存区域里面存放着一个地址(函数指针)。当某个中断触发时,单片机可以根据中断号,来定位到这个内存区域,进而得到这个内存区域中存放的函数指针,然后通过这个指针跳转到对应中断服务函数里面。 举个例子。上图左边第62行表示0x08000004到0x08000007这个内存区域存放着Reset_Handler函数的函数指针0x08000101。当复位中断(Reset)触发时,单片机会从0x08000004~0x08000007这个内存区域取出一个函数指针0x08000101,然后将PC指针跳转到0x08000101,从而完成一次中断处理。如果要问,为啥子单片机会知道从这个地址取处函数指针?这得问ST公司了,他们就是这样做的233333。 了解完中断向量表,接下来可以继续探讨启动流程了。 如上文所述,当单片机上电后,会触发复位中断,单片机就从0x08000004~0x08000007这个内存区域中取出地址0x08000101,然后将PC指针跳转到这个地址去继续执行程序。顺理成章地,我们就可以去看0x08000101这个地址开始的内存区域中都存放了那些代码。 直接查看startup_stm32f103xb.s中Reset_Handler标号所在位置的源代码。当然如果头比较铁,也可以从反汇编文件中定位0x08000101地址的反汇编代码。源代码的阅读性当然比反汇编代码的好,所以我就不头铁了。源代码如下图,在startup_stm32f103xb.s的129行开始: 129~132行不需要理会,是伪代码,只做一些标记或解释作用,实际上编译后不产生机器代码。 133:将SystemInit标号代表的内存地址赋值给R0。如果想深究SystemInit标号代表的内存地址到底是多少,可以看反汇编文件。这里就不展开了。 134:跳转到R0,也就是跳转到SystemInit。 我们先不看SystemInit,我们先把剩下的两行代码看完。135到136的形式跟133到135基本相同,就是跳转到__main。 到此为止,我们知道了一件事情。单片机上电后,运行Reset_Handler。而Reset_Handler主要是运行了SystemInit和__main。(这里有个前提,那就是SystemInit运行结束后返回了。实际上就是返回了)。 那接下来探讨SystemInit干了些什么。那问题发生了,在startup_stm32f103xb.s中找不到SystemInit。我们再仔细观察前面的代码(此时我们都是列文虎克,哈哈啊哈),发现132行是不是有个IMPORT SystemInit?这行代码的意思是:导出SystemInit标号。也就是外部可以使用startup_stm32f103xb.s文件的SystemInit标号。那行,我们找找其他文件。全局搜索一下,发现在system_stm32f1xx.c文件里面。(谢天谢地,终于到C语言了。为啥子Typora不支持插入asm代码呢,截图老费劲了)。SystemInit是个函数,如下所示: /** * @brief Setup the microcontroller system * Initialize the Embedded Flash Interface, the PLL and update the * SystemCoreClock variable. * @note This function should be used only after reset. * @param None * @retval None */ void SystemInit (void) { #if defined(STM32F100xE) || defined(STM32F101xE) || defined(STM32F101xG) || defined(STM32F103xE) || defined(STM32F103xG) #ifdef DATA_IN_ExtSRAM SystemInit_ExtMemCtl(); #endif /* DATA_IN_ExtSRAM */ #endif /* Configure the Vector Table location -------------------------------------*/ #if defined(USER_VECT_TAB_ADDRESS) SCB->VTOR = VECT_TAB_BASE_ADDRESS | VECT_TAB_OFFSET; /* Vector Table Relocation in Internal SRAM. */ #endif /* USER_VECT_TAB_ADDRESS */ } 可以看到,是一大串的宏定义。实际的代码只有下面两行。 SystemInit_ExtMemCtl(); SCB->VTOR = VECT_TAB_BASE_ADDRESS | VECT_TAB_OFFSET; 而且上面的宏定义都不成立!!(扶额叹气),所以仅剩的这两行代码亦不会运行。也就是说,SystemInit啥也没干,进去就出来了。 继续分析__main。同样地,先找一下这个标号在哪里。发现,没找到!!!嗨呀,咋没找到咧。查看反汇编文件,发现的确没有。上网查询一下,网上说这是C库文件,主要是配置C语言运行的环境,然后跳转到C语言的main()函数。这个据说跟编译器有关,是编译器添加的代码。 一般到这也就算了,毕竟我们也分析到运行main()函数了。但是啊,__main()到底做了什么事情难道不好奇吗?(此处配音,千反田:我很好奇)。那我们就再分析一下反汇编文件。无论这部分代码是由什么东西添加的,最终经过编译后,一定是在反汇编文件中有体现的,毕竟单片机最终跑的就是二进制文件。 首先找到Reset_Handler的反汇编代码,如下,留意红框部分: 红框部分就是__main标号代表的地址,0x080000ed。那我们接着找0x080000ed。这里有一个小问题,上文我们知道Reset_Handler=0x08000101,但是实际上Reset_Handler在0x08000100。所以这时候我们不应该找0x080000ed,而是找0x080000ec。 找到0x080000ec处代码如下: 这段代码中间穿插了比较多注释类型的文本,看着有一点点乱。其实仔细看,还是比较好看懂的。 首先是第118行,这是将__lit_00000000赋值给sp指针。 __lit_00000000在134行有所描述,再往下找,找到139行,发现这个值等于20000410。对中断向量表还有点印象的话就会发现,这个值跟中断向量表的第一个向量的值是一样的。这里可以理解为:编译器将中断向量表的第一个向量复制到了0x080000fc,并且在之后将其赋值给了sp指针。 接着来到121行,0x080000f0,这里是跳转到__scatterload去执行一些加载数据段的东西,这里不展开。 接着是129行和130行。129行是将当前PC指针指向的值+0,然后赋值给r0,接着130跳转到r0。这里可能会稍微迷惑一下,129行指向的值不是一条指令吗?怎么能加0后赋值到PC?其实不是的,仔细看129行后面的注释,实际上在执行129行时,PC指针并不是129行的0x080000f4,而是0x080000f8。这其实是流水线架构的问题,取指->译指->运行是同时进行的,也就是说,取值(PC变化)要比当前运行的地址要快2个指令。因此运行129行(0x080000f4)的指令时,PC指针已经取指到132行(0x080000f8)了。所以130行代码其实是跳转到0x08000aa1。 接下来我们跳转到0x08000aa1看一下是什么,如下图: 到这里就是main()函数了,也就是main()函数的第一行代码HAL_Init(); 至此,单片机从启动到运行main()函数的过程就分析完成了。 好的,那就先总结一下单片机启动都做了什么。
3.2.3、编写Bootloader需要做什么 我们编写bootloader的目标就是让单片机上电后,能够从0x08002000启动。现在我们都知道了,单片机上电默认是从0x08000000启动的,那怎么跳转到0x08002000呢?这就需要我们手动进行跳转了。 手动进行跳转,实际上就是模拟一次上电的过程。在真实上电时,程序从0x08000000开始跑。在我们完成bootloader的工作后,可以为单片机准备一个类似刚上电的环境,然后将PC指针指向APP程序的起始地址即可。 综上所述,编写bootloader进行程序跳转,需要以下几步:
3.3、编写程序跳转函数 有了以上的知识,接下来就可以开始编写程序了。(这里的程序参考了https://www.cnblogs.com/jiuliblog-2016/p/11411887.html这位大佬的博文) 1、中断向量表偏移。 SCB->VTOR = app_addr; //app_addr为新程序的起始地址 1 2、设置SP指针 __asm void MSR_MSP(uint32_t addr) { MSR MSP, r0; BX r14; } 3、跳转函数的完整代码 typedef void (*APP_FUNC)(); //函数指针类型定义 /* * 设置SP指针函数 */ __asm void MSR_MSP(uint32_t addr) { MSR MSP, r0; BX r14; } /* * 跳转到APP程序函数 */ void run_app(uint32_t app_addr) { uint32_t reset_addr = 0; APP_FUNC jump2app; /* 栈顶地址是否合法(这里sram大小为8k) */ if(((*(uint32_t *)app_addr)&0x2FFFE000) == 0x20000000) { /* 跳转之前关闭相应的中断 */ NVIC_DisableIRQ(SysTick_IRQn); /* 中断向量表偏移 */ SCB->VTOR = app_addr; /* 设置栈指针 */ MSR_MSP(app_addr); /* 获取复位地址 */ reset_addr = *(uint32_t *)(app_addr+4); /* 跳转到APP地址 */ jump2app = ( APP_FUNC )reset_addr; jump2app(); } else { //printf("APP Not Found!n"); } } 这样跳转函数就写完啦。只要在合适的时候调用run_app()函数,单片机就会去执行APP程序。至于接收固件,或者自行刷写固件等功能,就等以后再完善了。 4、验证 工程写好了,当然要验证一下能不能跑啦。 4.1、简单验证 最简单的验证就是:写一个LED闪烁的程序,闪烁的速度与Bootloader的闪烁速度不相同。然后将APP工程的flash起始地址设置为:0x08002000,ROM大小设置为0x0000D000即可,如下图所示: 设置好,编译程序,用MDK自带的烧录工具或者STM32 ST-LINK Utility工具都可以把hex文件烧录到0x08002000。值得注意的是,烧录时不要勾选擦除整片flash,不然会把bootloader程序擦除掉。 都烧录完毕后,应该就能看到核心板上的LED按照APP程序设定的频率闪烁了。 4.2、完整验证 上述的简单验证过于简单,就算没有设置中断向量表偏移都能跑(因为APP和Bootloader的Systick中断代码一样)。 为了验证中断是否能够正常工作那就写一个带中断的工程吧。简单一点,使能TIM3,再TIM3更新中断里反转LED。仍旧使用stm32cubeMX生成MDK工程,配置如图。对了,注意一点,记得在SYS子菜单下使能SW调试。之前忘记使能,烧写老麻烦了。 生成工程,在stm32f1xx_it.c中的定时器3中断服务函数添加如下代码: /** * @brief This function handles TIM3 global interrupt. */ void TIM3_IRQHandler(void) { /* USER CODE BEGIN TIM3_IRQn 0 */ static unsigned int sui1sCnt = 0; sui1sCnt++; if(sui1sCnt >= 1000) { sui1sCnt = 0; HAL_GPIO_TogglePin(LED_GPIO_Port, LED_Pin); } /* USER CODE END TIM3_IRQn 0 */ HAL_TIM_IRQHandler(&htim3); /* USER CODE BEGIN TIM3_IRQn 1 */ /* USER CODE END TIM3_IRQn 1 */ } 值得注意的是,需要把定时器3初始化为1ms更新。 接下来在main()函数中使能定时器3基本计时(带中断),调用以下函数即可: HAL_TIM_Base_Start_IT(&htim3); 这里可以先不改flash起始地址,先把程序烧写进单片机看运行正不正常。正常的话,修改flash起始地址为0x08002000,然后重新烧写Bootloader和APP,复位,LED1秒闪烁一次,验证完成。 当然,如果有好奇小宝宝如果想知道,如果没有偏移中断向量表的话,这个程序还能不能跑。那就把偏移中断向量表的代码注释掉,再编译烧写试试看。(剧透:跑不了了哦,而且程序会跑乱~) |
|
|
|
只有小组成员才能发言,加入小组>>
调试STM32H750的FMC总线读写PSRAM遇到的问题求解?
1874 浏览 1 评论
X-NUCLEO-IHM08M1板文档中输出电流为15Arms,15Arms是怎么得出来的呢?
1658 浏览 1 评论
1143 浏览 2 评论
STM32F030F4 HSI时钟温度测试过不去是怎么回事?
759 浏览 2 评论
ST25R3916能否对ISO15693的标签芯片进行分区域写密码?
1720 浏览 2 评论
1963浏览 9评论
STM32仿真器是选择ST-LINK还是选择J-LINK?各有什么优势啊?
789浏览 4评论
STM32F0_TIM2输出pwm2后OLED变暗或者系统重启是怎么回事?
612浏览 3评论
628浏览 3评论
stm32cubemx生成mdk-arm v4项目文件无法打开是什么原因导致的?
590浏览 3评论
小黑屋| 手机版| Archiver| 电子发烧友 ( 湘ICP备2023018690号 )
GMT+8, 2025-1-11 20:17 , Processed in 0.924238 second(s), Total 77, Slave 61 queries .
Powered by 电子发烧友网
© 2015 bbs.elecfans.com
关注我们的微信
下载发烧友APP
电子发烧友观察
版权所有 © 湖南华秋数字科技有限公司
电子发烧友 (电路图) 湘公网安备 43011202000918 号 电信与信息服务业务经营许可证:合字B2-20210191 工商网监 湘ICP备2023018690号