
OTA BootLoader是什么OTA是指无线通信技术BootLoader是设备开机时最先运行的一段程序负责初始化硬件加载操作系统它本身不具备无线上网的能力也不会主动去下载更新包。那么OTA BootLoader那就是指的是通过无线通信来对设备进行远程升级的一门技术。自定义BootLoader:首先看一下别人的bootloader功能串口挂起等待接收程序数据 把Bin文件写入到对应的flash当中他会识别发送的是Hex文件还是Bin文件如果是Hex文件他会自己转换成Bin文件自定义的与自带的bootloader的区别程序本身存放的位置不同。如果是我们自己写的BootLoader会存放在flash的起始位置0x0800 0000中但是系统内的boot loader是存放在特殊位置0x1FFF F000。这里需要注意当我们自己写boot loader写入flash时会造成位置冲突。这是因为我们的程序代码也要放进flash会冲突。解决方法是将flash分区AB区存放因为我们程序上上电后执行的第一个程序必须是BootLoader程序所以说bootloader必须在flash的起始位置。但是A区的起始位置是哪里呢当我们把bootloader编译好了那么B区的位置就订死了接下来就是A区的起始位置了。假设boot loader使用了4KB4KB的16进制是0x1000 那么A区的起始地址就是0x0800 0000 0x1000 0x0800 1000因为flash起始地址是0x0800 0000。这种方式是紧挨着的如果你想要让B区有空闲位置的话那么就可以给B区一个比较大的位置。比如16KB此时A区的地址就是0x0800 4000.我们每次复位程序都是从flash的起始位置执行的那么我们每一次复位都要执行一次boot loader程序。他通过跳转的方式等boot loader执行完毕后再去跳转到A区。这个跳转相当于复位就是在boot loader的main函数中执行A区的复位中断。如何将A程序写入到特定的flash地址启动时会给AB都分配一个栈栈顶元素 RAM起始地址 栈大小。当我们的stack大小相等时那么A和B的栈顶地址就相等因为他们不会同一时刻来使用这块内存。写入到flash占多少内存用页来算复位中断地址是确定的地址。A程序在编译时一定要自己专门定义自己的起始地址。使用串口接收方式向BootLoader发送程序我们使用串口的话我们需要编写串口接收函数当我们使用普通的串口接收函数HAL_UART_Receive()的话他会有超时等待时间是阻塞式的CPU一直来查看有没有数据发过来并且他会有一个接受长度接受不够这个长度他就会一直超时等待。不适合我们的不定长接受。另一种函数串口的空闲接受HAL_UARTEx_ReceiveToIdle()函数他是空闲中断函数他实际上也是CPU一直查询看有没有数据过来但是他跟上面不同的是他会有一个空闲中断当接收端检测到总线有一个空闲帧时就会返回。否则就等待接受数据直到超时时间结束他也是一种阻塞式的他可以接收不定长数据但是因为他是阻塞式的所以就不适合我们一般使用空闲中断接收函数HAL_UARTEx_ReceiveToIdle_IT()他带有中断不需要CPU一直来查询是否数据到了而是当接收数据寄存器的数据全都移动到接受移位寄存器后会提醒CPU过来搬运数据不占用CPU资源而且当检测到总线上有空闲帧时他会产生空闲中断我们编写这个空闲中断的回调函数就可以了。这里会产生一个小Bug就是当我们的串口初始化结束到调用串口接收函数之前如果有人源源不断的发数据的话就会造成ORE溢出所以我们要先清除掉这些问题调用两个函数//清空掉初始化串口使用之前的所有问题。 __HAL_UART_CLEAR_OREFLAG(huart1); //清楚掉ORE的标志位 __HAL_UART_CLEAR_IDLEFLAG(huart1); //清楚掉IDLE的标志位因为后面用的是空闲的中断所以先清除一下标志位随后再编写我们的回调函数void HAL_UARTEx_RxEventCallback(UART_HandleTypeDef *huart, uint16_t Size) { if(huart-Instance USART1) { //保存接收到的实际长度 uart_rec_len Size; /* 先来进行一个日志打印 */ printf(Buffer:%d, uart_rec_len); // TODO: 将接收到的数据写入flash flash编程后续再说 //使用完数据之后清空随后准备下一次的接收 memset(uart_rec_buff, 0, BOOTLOADER_UART_REC_BUFF_LEN); uart_rec_len 0; HAL_UARTEx_ReceiveToIdle_IT(huart1, uart_rec_buff, BOOTLOADER_UART_REC_BUFF_LEN); /* 然后再打开空闲接收中断 */ } }这就是大致的一个通过串口向BootLoader发送程序的一般流程。写数据到FLASH需要注意Flash默认是加锁的状态需要优先解锁随后使用完成之后再重新加锁。先擦除再写入可以按页擦除或者全部擦除。Flash编程只支持16 32 64位写入默认小端序解锁调用官方的函数即可// 1. 先解锁flash HAL_FLASH_Unlock();先擦除每次写入的数据稳定应该在256字节左右因为串口原因会导致并不是每次都是256如何判断当前一页是需要擦除如果是新的一页那就擦除擦除前是不同的内容擦除后都是FFFF优势第一次擦除后后面就不需要再擦除了当我们第一个256写入后擦除了一整页那么后续的7次就不需要再擦除了另外一种方法我们先用一个缓冲区储存数据当达到2k时把这一页全部擦除再写入2k但是这种方式不适用这是因为我们是一次256发的但是会因为在传输过程中导致的不精确导致他最后跟2k会有一两个字节的偏差。那么他就会一直等待。另外就是我们很少会遇到刚好写入的地址是一页的起始地址的。需要扩大缓冲区或者使用环形缓冲区。flash擦除相关代码// 2. 判断当前写入的地址是否为新的一页 如果是就先擦除一页 // 2.1 遍历需要写入的地址长度为当前接收到的数据长度如果全部的内容都是0xffff则说明已经擦除过了 uint8_t is_erase 0; //先定义擦除变量为0默认不需要擦除。 uint32_t Page_Addr APP_START_ADDR flash_write_offset; //用来记录当前偏移后的地址 for(uint16_t i 0;i uart_rec_len; i ) { //遍历每一个值只要有一个不等于0xffff则说明还没有擦除。需要擦除赋值为1。 uint8_t data *(volatile uint8_t *)(APP_START_ADDR i flash_write_offset); /* 这里的APP_START_ADDR i flash_write_offset 是为了得到当前写入的地址 */ if(data ! 0xffff) { is_erase 1; Page_Addr (Page_Addr i) - (Page_Addr i)%FLASH_PAGE_SIZE; //记录当前页的起始地址 break; } } // 2.2 如果需要擦除则擦除当前页写进main.c中 if(is_erase) { FLASH_EraseInitTypeDef erase_init; erase_init.Banks FLASH_BANK_1; //选择银行1 erase_init.NbPages 1; //擦除页数 erase_init.PageAddress Page_Addr; //设置擦除页的起始地址 erase_init.TypeErase FLASH_TYPEERASE_PAGES; //擦除类型为页 uint32_t * PageError NULL; //定义一个指针用来存储擦除的错误地址 HAL_FLASHEx_Erase(erase_init, PageError); /* flash擦除比较耗费性能 */ }我们首先要遍历一下要写入的地址是否是新的一页遍历长度那就是接收到的字节数如果我们要写入的地址的数据都是0xFFFF那么说明这就是新的一页我们定义一个变量is_erase 作为标志位来判断是否要擦除。我们遍历时如果有数据不等于0xFFFF那就说明这不是新的一页给is_erase赋值为1擦除然后直接break;我们代码中的APP_START_ADDR是A区的起始地址我们在.h文件中已经#define定义过了。还有就是flash_write_offset变量他是作为一个偏移变量用来记录每次写入多少个数据。随后通过uint32_t Page_Addr APP_START_ADDR flash_write_offset;来记录下一次写入的起始地址。当我们遍历到第一个不是0xFFFF的数据后就直接break给is_erase赋值为1.随后我们就要开始擦除了并且还要记录下当前的位置但是我们擦除是按页擦除所以说我们要计算出当前页的起始地址计算过程如下所以说用 (Page_Addr i) - (Page_Addr i)%FLASH_PAGE_SIZE的结果那就是本页的起始地址。随后我们进行擦除我们的擦除函数如下随后我我们清空缓冲区执行下一次接收注意我们的中断要求快进快出但是擦除Flash要消耗几十ms不符合中断快进快出的特点所以我们把擦除的部分放入main中的while循环在中断中只用置is_erase的标志位就可以了。再写入写入时我们需要调用HAL_FLASH_Program();来进行一个写入的操作。函数原型HAL_StatusTypeDef HAL_FLASH_Program(uint32_t TypeProgram, uint32_t Address, uint64_t Data);第一个参数是指的是写入数据的宽度他只能写入半字1字或者双字。我们的stm32那么1字就是32位半字就是16位双字就是64位。第二个参数是写入的目标地址。第三个参数是要写入的数据。代码/* FLASH的写入 使用函数HAL_FLASH_Program(); */ //我们使用16位字节的写入所以我们需要将数据拼接成两字节 // 拼接我们接收到的数据一定是整两个字节的。 for(uint8_t i 0; i uart_rec_len ; i i 2) { uint16_t data16 0; if(i 1 uart_rec_len) //健壮型判断 { data16 uart_rec_buff[i] | (uart_rec_buff[i 1] 8); //小端序低位在前高位在后 } //随后进行写入 HAL_FLASH_Program(FLASH_TYPEPROGRAM_HALFWORD, Page_Addr i, data16); }因为是半字输入。16位就需要把两个字节拼起来。小端序低位在左高位在右代码while (1) { // 2.2 如果需要擦除则擦除当前页 if(is_erase) { HAL_FLASH_Unlock(); FLASH_EraseInitTypeDef erase_init; erase_init.Banks FLASH_BANK_1; //选择银行1 erase_init.NbPages 1; //擦除页数 erase_init.PageAddress Page_Addr; //设置擦除页的起始地址 erase_init.TypeErase FLASH_TYPEERASE_PAGES; //擦除类型为页 uint32_t PageError 0; //定义一个指针用来存储擦除的错误地址 HAL_FLASHEx_Erase(erase_init, PageError); /* flash擦除比较耗费性能 */ is_erase 0; } if(uart_rec_len 0) { /* FLASH的写入 使用函数HAL_FLASH_Program(); */ //我们使用16位字节的写入所以我们需要将数据拼接成两字节 // 拼接。 for(uint8_t i 0; i uart_rec_len ; i i 2) { uint16_t flash_adder Page_Addr i; //用来记录写入的地址 uint16_t data16 0; if(i 1 uart_rec_len) //健壮型判断 { data16 uart_rec_buff[i] | (uart_rec_buff[i 1] 8); //小端序低位在前高位在后 } else { //(i 1) uart_rec_len的情况 data16 uart_rec_buff[i] | (0xff 8); } //随后进行写入 HAL_FLASH_Program(FLASH_TYPEPROGRAM_HALFWORD, flash_adder, data16); } flash_write_offset uart_rec_len; // 疑问为什么擦除时不需要这个偏移量 // 因为我们擦除时只需要记录下哪一擦除但是写入的时候我们需要记下下一次要写入的地址。 HAL_FLASH_Lock(); /*--------------------------------------------------------------------------------*/ //使用完数据之后清空随后准备下一次的接收 memset(uart_rec_buff, 0, BOOTLOADER_UART_REC_BUFF_LEN); uart_rec_len 0; HAL_UARTEx_ReceiveToIdle_IT(huart1, uart_rec_buff, BOOTLOADER_UART_REC_BUFF_LEN); /* 然后再打开空闲接收中断 */ } /* USER CODE END WHILE */ /* USER CODE BEGIN 3 */ }BUG我们在flash中写入数据时我们此时是按照半字写入但是当我们写入奇数个数据比如“”“Hello”最后一个o是跟0xFF或上后发过去的。下一次再发送Hello时他的H会跟上一个o一起但是我们写flash是要求先擦除再写入的。就会发生错误。还有就是我们是不可以将这个0xff空出来的。解决方法如下定义一个last_byte_flag标志位和last_byte位。last_byte_flag用来记录上次是否有多出来的一个last_byte用来记录上次多出来的数据。随后我们通过判断uart_rec_len last_byte_len%2 是否等于0分为两种情况。然后两种情况内部还可以分为上次有剩余和上次没剩余两种情况。interface.c代码#include int_bootloader.h //接受程序的缓冲区 uint8_t uart_rec_buff[BOOTLOADER_UART_REC_BUFF_LEN] {0}; uint16_t uart_rec_len 0; //定义接受到的长度变量 uint16_t uart_rec_full 0; uint32_t flash_write_offset 0; //用来定义偏移量 uint8_t is_erase 0; uint32_t Page_Addr 0; //本次要写入的起始地址 APP_START_ADDR flash_write_offset uint32_t erase_page_addr 0; //需要擦除的页首地址单独存放避免污染 Page_Addr //末尾可能出现的单独的字节。 uint8_t last_byte_flag 0; //是否有单独遗留的字节没有写入 uint8_t last_byte 0; //遗留的单独的字节数据 /** * brief 串口开启接受中断后触发空闲帧时使用的回调函数 * param huart * param Size 接收的字符数 * retval * */ void HAL_UARTEx_RxEventCallback(UART_HandleTypeDef *huart, uint16_t Size) { if(huart-Instance USART1) { //保存接收到的实际长度 uart_rec_len Size; /* 先来进行一个日志打印 */ printf(Buffer:%d\n, uart_rec_len); /*--------------------------------------------------------------------------------*/ // // 2. 判断当前写入的地址是否为新的一页 如果是就先擦除一页 // // 2.1 遍历需要写入的地址长度为当前接收到的数据长度如果全部的内容都是0xffff则说明已经擦除过了 is_erase 0; //先定义擦除变量为0默认不需要擦除。 Page_Addr APP_START_ADDR flash_write_offset; //用来记录当前偏移后的地址写入地址后面不要再改它 printf(Page_Addr:%d\n, Page_Addr); if(uart_rec_len 0) { HAL_FLASH_Unlock(); for(uint16_t i 0;i uart_rec_len; i ) { //遍历每一个值只要有一个不等于0xff说明这个位置已经有数据了没擦干净需要擦除。 uint32_t cur APP_START_ADDR i flash_write_offset; /* 当前检查到的绝对地址 */ uint8_t data *(volatile uint8_t *)cur; if(data ! 0xff) { is_erase 1; /* 注意这里只写 erase_page_addr绝不动 Page_Addr 否则写入地址会被改成页首导致每次都从 0 开始写 */ erase_page_addr cur - cur % FLASH_PAGE_SIZE; //该地址所在页的页首 break; } } } } } /** * brief 串口接收 A程序 * */ void int_bootloader_init(void) { //清空掉初始化串口使用之前的所有问题。 __HAL_UART_CLEAR_OREFLAG(huart1); //清楚掉ORE的标志位 __HAL_UART_CLEAR_IDLEFLAG(huart1); //清楚掉IDLE的标志位因为后面用的是空闲的中断所以先清除一下标志位 //HAL_UARTEX_ReceiveToIdle()他是占用CPU的不如使用带中断的。 //少一个参数超时时间因为他是异步执行的 HAL_UARTEx_ReceiveToIdle_IT(huart1, uart_rec_buff, BOOTLOADER_UART_REC_BUFF_LEN); //他可能会出现一个bug就是程序没有上电时就有人给他源源不断的发数据造成他的RDR寄存器被占满溢出中断但是此时还没上电。 //解决方法在初始化最开始就清空串口的问题。 }main.c代码int main(void) { int_bootloader_init(); while (1) { // 2.2 如果需要擦除则擦除当前页 if(is_erase) { FLASH_EraseInitTypeDef erase_init; erase_init.Banks FLASH_BANK_1; //选择银行1 erase_init.NbPages 1; //擦除页数 erase_init.PageAddress erase_page_addr; //设置擦除页的起始地址独立变量不能用 Page_Addr erase_init.TypeErase FLASH_TYPEERASE_PAGES; //擦除类型为页 uint32_t PageError 0; //定义一个指针用来存储擦除的错误地址 HAL_FLASHEx_Erase(erase_init, PageError); /* flash擦除比较耗费性能 */ is_erase 0; } if(uart_rec_len 0) { uint16_t data16 0; //判断uart_rec_len last_byte_flag) % 2来判断最后有没有多出来一个 if((uart_rec_len last_byte_flag) % 2 0) // 这次刚好写完 { if(last_byte_flag 1) { // 上次有遗留这次没遗留 这次是奇数1 5 Int_flash_write_with_last(); flash_write_offset uart_rec_len 1; //记录偏移 } else { // 上次没有遗留这次也没有遗留 这次是偶数2 4 Int_flash_write_no_last(); flash_write_offset uart_rec_len; //记录偏移 } last_byte_flag 0; } else // 这次写完后还有单独遗留字节 { if(last_byte_flag 1) { // 上次有遗留这次也有遗留 这次是偶数4 6 Int_flash_write_with_last(); flash_write_offset uart_rec_len; //记录偏移 } else { // 这次写完会有遗留字节上次没有遗留字节 Int_flash_write_no_last(); flash_write_offset uart_rec_len - 1; } last_byte_flag 1; last_byte uart_rec_buff[uart_rec_len - 1]; } HAL_FLASH_Lock(); /*--------------------------------------------------------------------------------*/ //使用完数据之后清空随后准备下一次的接收 memset(uart_rec_buff, 0, BOOTLOADER_UART_REC_BUFF_LEN); uart_rec_len 0; HAL_UARTEx_ReceiveToIdle_IT(huart1, uart_rec_buff, BOOTLOADER_UART_REC_BUFF_LEN); /* 然后再打开空闲接收中断 */ } /* USER CODE END WHILE */ /* USER CODE BEGIN 3 */ } }跳转到A程序使用函数指针的方式/** * brief 串口接收 A程序 * */ void int_bootloader_init(void) { //清空掉初始化串口使用之前的所有问题。 __HAL_UART_CLEAR_OREFLAG(huart1); //清楚掉ORE的标志位 __HAL_UART_CLEAR_IDLEFLAG(huart1); //清楚掉IDLE的标志位因为后面用的是空闲的中断所以先清除一下标志位 //HAL_UARTEX_ReceiveToIdle()他是占用CPU的不如使用带中断的。 //少一个参数超时时间因为他是异步执行的 HAL_UARTEx_ReceiveToIdle_IT(huart1, uart_rec_buff, BOOTLOADER_UART_REC_BUFF_LEN); //他可能会出现一个bug就是程序没有上电时就有人给他源源不断的发数据造成他的RDR寄存器被占满溢出中断但是此时还没上电。 //解决方法在初始化最开始就清空串口的问题。 } /** * brief 跳转到应用程序 */ void int_bootloader_jump_to_app(void) { typedef void (* pFunc)(void); // 1.0 校验 //栈顶地址的值 uint32_t app_start_ptr *(volatile uint32_t *)APP_START_ADDR; //复位中断向量的值 uint32_t app_reset_hander *(volatile uint32_t *)(APP_START_ADDR 4); // 1.1 校验栈顶地址 if(app_start_ptr SRAM_BASE_ADDR || app_start_ptr (SRAM_BASE_ADDR SRAM_SIZE)) { printf(Stack adder error!\n); printf(app_start_ptr 0x%08X\n, app_start_ptr); return; } else { printf(Stack adder ok!\n); printf(app_start_ptr 0x%08X\n, app_start_ptr); } // 1.2 校验复位中断地址 if(app_reset_hander APP_START_ADDR app_reset_hander APP_END_ADDR) { printf(Reset adder error!\n); return; } else { printf(Reset adder ok!\n); printf(app_reset_hander 0x%08X\n, app_reset_hander); } // 2.0 注销boot loader程序 NVIC_DisableIRQ(USART1_IRQn); NVIC_DisableIRQ(EXTI9_5_IRQn); // 关闭systick SysTick-CTRL 0; SysTick-LOAD 0; SysTick-VAL 0; // // 2.2 注销HAL库 HAL_DeInit(); // 2.3 设置新的堆栈指针 __set_MSP(app_start_ptr); // 2.4 重定向中断向量表 SCB-VTOR APP_START_ADDR; // 2.5 跳转到应用程序的复位中断向量地址 pFunc Jump_To_App (pFunc)app_reset_hander; Jump_To_App(); }注意一点就是我们此时关闭了HAL的一些中断然后再运行A程序的时候可能会因为一些中断的关闭而运行不起来。所以我们保险期间需要在A程序的开头添加上__disable_irq(); //重定向中断向量表 SCB-VTOR APP_SATRT_ADDR; __enable_irq();有一个小BUG要注意一下就是我的这些处理代码都是在mian.c中写的也包括跳转函数当我们每执行一次后会直接执行这个跳转函数我们的串口是用的空闲中断方式发送时是256字节间隔50ms发送的所以就会导致刚发送256字节就直接跳转所以我添加了一个标志位每次写入后对他1当他的值 50 的时候才会去执行跳转函数.人机交互我们的启动boot loader的时候日志输出给用户因为可能有人发送hello这些不是bin文件的数据就会导致擦错。所以我们有一个开始标记串口发送 Start 或 硬件按下按键。结束串口等待1s/2s没有接受到数据 或 硬件按下按键最后校验程序。