嵌入式秋招必备项目(二):Bootloader
Bootloader介绍
Bootloader是什么?
Bootloader实际上就是在我们应用程序启动前运行的一段代码,主要的作用呢,一是对应用程序进行一些校验工作,防止FALSH坏块导致问题。二是对APP进行更新迭代(都发布的产品总不能一个一个壳子拆了让你烧录),此时配合以太网就能实现远程升级(OTA)
单片机的bootloader相较于Linux的Uboot还是要简单很多,因为单片机的内存是SRAM可以直接寻址访问,比SDRAM要方便。而且也没有太多的外设需要初始化。
bootloader工作原理
为了进一步解释BOOTLoader的原理,首先要了解以下内容
-
bin文件的具体内容
了解Bin里面内容的布局,要从具体的链接脚本开始看
0x0800000就是FLASH的起始地址啦,可以看到RESET部分被放在了生成的bin文件的起始位置,具体的其它文件的排放次序可以查看.map文件LR_IROM1 0x08000000 0x00100000 { ; load region size_region ER_IROM1 0x08000000 0x00100000 { ; load address = execution address *.o (RESET, +First) *(InRoot$$Sections) .ANY (+RO) .ANY (+XO) } /*把变量的地址分配到以0x20000000起始的SRAM范围内,这样程序后面就可以把数据从FLASH中直接搬到SRAM的对应地址就行*/ RW_IRAM1 0x20000000 0x00020000 { ; RW data .ANY (+RW +ZI) /*实际上ARMCC在这里还有一个压缩算法,感兴趣可以去搜搜一搜*/ } }RESET的具体内容就是这个中断向量表啦,可以看到第一个位置存放的是SP指针的值,用来初始化C语言运行的基本环境,第二个值就是Reset_Handler,表示对复位的处理
__Vectors DCD __initial_sp ; Top of Stack DCD Reset_Handler ; Reset Handler DCD NMI_Handler ; NMI Handler DCD HardFault_Handler ; Hard Fault Handler DCD MemManage_Handler ; MPU Fault Handler DCD BusFault_Handler ; Bus Fault Handler DCD UsageFault_Handler ; Usage Fault Handler DCD 0 ; Reserved DCD 0 ; Reserved DCD 0 ; Reserved DCD 0 ; Reserved DCD SVC_Handler ; SVCall Handler DCD DebugMon_Handler ; Debug Monitor Handler DCD 0 ; Reserved DCD PendSV_Handler ; PendSV Handler DCD SysTick_Handler ; SysTick Handler -
STM32启动过程(cortexM内核(cortexA内核不适用))
启动的时候根据BooT0和Boot1的配置, SP指针和PC指针会去不同的地方取值
以FLASH启动为例, 硬件会自动把0x0800000的值赋值给SP指针 把0x08000004的值赋值给PC指针,也就是(Reset_Handler),然后执行Reset_Handler- Reset_Handler
SystemInit:做一些核心外设的初始化,配置中断向量表(VETOR)的位置
__main:主要是一些初始化工作,包括清空ZI段,把RW段搬运到内存中,然后跳转main函数
;Reset handler Reset_Handler PROC EXPORT Reset_Handler [WEAK] IMPORT SystemInit IMPORT __main LDR R0, =0xE000ED88 ; 使能浮点运算 CP10,CP11 LDR R1,[R0] ORR R1,R1,#(0xF << 20) STR R1,[R0] LDR R0, =SystemInit BLX R0 LDR R0, =__main BX R0 ENDP - Reset_Handler
-
编写Bootloader需要注意什么
所以对于Bootloader来说,如果想要跳转到APP,就需要手动模拟这个SP指针赋值和PC指针赋值的工作。但在这个过程中,还要注意以下几点。- 模拟SP指针赋值和PC指针赋值
当APP是在RTOS环境跳转时,RTOS的任务用的是PSP指针,而我们要模拟的是给MSP指针赋值,所以这里要强行切换到MSP指针(__set_CONTROL(0)指令) - 中断向量表的配置
Bootloader有自己的VECTOR,APP也有自己的VECTOR,所以跳转APP后,要切换到APP自己的VECTOR当中,否则就无法正确处理中断 - 跳转前应该给APP提供一个"清洁"的外设环境
因为我们的Bootloader和APP很可能共享一个外设,但是具体的业务逻辑完全不一样。假设我们的Bootloader使用了UART并且跳转前刚好有个中断没处理,跳转到APP后触发了UART中断就完蛋了,所以Bootloader在跳转前最好复位UART外设并清除所有中断标志位,APP自己再重新初始化 - 跳转过程要关中断
因为涉及到了VECTOR的切换,关中断也是为了防止意外。进入APP后在开启就行
大概就是长这样
__set_PRIMASK(1); __set_CONTROL(0); //使用RTOS会使用PSP,要恢复MSP jump2app = (iapfun) * (volatile uint32_t *)((uint32_t)BOOT_ADDRESS + 4); sys_nvic_set_vector_table(FLASH_BASE, BOOT_ADDRESS - FLASH_BASE); sys_msr_msp(*(volatile uint32_t *)BOOT_ADDRESS);// 复位MSP指针 ethernet_deinit(); carama_deinit(); usart_deinit(); SysTick->CTRL = 0X00; //禁止SysTick SysTick->LOAD = 0; SysTick->VAL = 0; HAL_DeInit(); sram_deinit(); /*现在RTOS在SRAM下 最后复位*/ jump2app(); - 模拟SP指针赋值和PC指针赋值
项目的内存布局
因为这个项目里涉及到了AB分区,所以整个STM32f407ZGT6的1MB FLASH被分成了以下区域,在这里一定要注意FLASH可不是按字节操作的而是扇区操作的,所以一定不要跨扇区!!!(APPA绝对不能和APPB的部分内容在一个扇区下)。
这里踩过的坑就是FLASH的写入和擦除了,主要就是对APP_PRAMA_ADDRESS下的操作,因为涉及到APPA的参数信息和APPB的参数信息写入。如果写入APPB的参数新时候不暂时保存APPA的参数信息的话,那APPA的参数信息就全完蛋了(因为所有的参数信息都在这128KB的扇区下)
/*FLASH分区: BOOT + A区 + B区 + 参数区 + WIFI固件区*/
#define BOOT_ADDRESS ((uint32_t )FLASH_BASE)
#define BOOT_MAX_SIZE_BYTES ((uint32_t)0x20000)
#define APP_SECTORA_ADDRESS ((uint32_t )0x08020000)
#define APP_SECTORA_MAX_SIZE_BYTES ((uint32_t)0x40000)
#define APP_SECTORB_ADDRESS ((uint32_t )0x08060000)
#define APP_SECTORB_MAX_SIZE_BYTES ((uint32_t)0x40000)
#define APP_PRAMA_ADDRESS ((uint32_t )0x080A0000)
#define APP_PRAMA_BYTES ((uint32_t)0x20000)
/*数字签名*/
#define APP_DS_ADDRESS APP_PRAMA_ADDRESS
#define APP_DS_BYTES ((uint32_t) 0x800)
#define APP_DS_SECTORA_ADDRESS APP_DS_ADDRESS
#define APP_DS_SECTORA_BYTES ((uint32_t) 0x400)
#define APP_DS_SECTORB_ADDRESS (APP_DS_SECTORA_ADDRESS + APP_DS_SECTORA_BYTES)
#define APP_DS_SECTORB_BYTES ((uint32_t) 0x400)
/*APP信息*/
#define APP_INFO_ADDRESS (APP_DS_ADDRESS + APP_DS_BYTES)
#define APP_INFO_BYTES ((uint32_t) 0x800)
#define APP_INFO_TOTAL_BYTES (APP_DS_BYTES + APP_INFO_BYTES)
#define WIFI_FM_ADDRESS ((uint32_t )0x080C0000)
#define WIFI_FM_BYTES ((uint32_t)0x40000)
/*******************************************************************************
* 物理地址 (Address) 分区名称 (Partition) 大小 (Size) / 说明
*******************************************************************************
* 0x08000000 +-------+-----------------------+
* | | BOOT (Bootloader) | 128 KB
* | | | [存放引导程序]
* 0x08020000 +-------+-----------------------+
* | | APP_SECTORA (A区) | 256 KB
* | | | [主运行程序 A]
* 0x08060000 +-------+-----------------------+
* | | APP_SECTORB (B区) | 256 KB
* | | | [备用/升级程序 B]
* 0x080A0000 +-------+-----------------------+
* | | 参数区 (PRAMA) | 128 KB (总空间)
* | | - APP_DS (数字签名区) | -> 子分区见下方详图
* | | - APP_INFO (APP信息区) |
* | | - (剩余空闲空间) |
* 0x080C0000 +-------+-----------------------+
* | | WIFI_FM (WIFI固件区) | 256 KB
* | | | [存放外部模块固件]
* 0x08100000 +-------+-----------------------+ <--- 假设 Flash 结束点
*******************************************************************************
另外,在这里我还修改了链接脚本,把SRAM也做了进一步的划分。
LR_IROM1 0x08060000 0x00040000 { ; load region size_region
ER_IROM1 0x08060000 0x00040000 { ; load address = execution address
*.o (RESET, +First)
*(InRoot$$Sections)
.ANY (+RO)
.ANY (+XO)
}
; 新增:专门给崩溃信息开辟一个极小的、不初始化的执行域
; 它会自动排在 RAM 的最前面
ER_CRASH 0x20000000 UNINIT 0x00000100 {
*(.bss.NoInit)
}
; 修改:原来的 RW_IRAM1 起始地址往后挪 0x100 (256字节)
; 这样就绝对不会和上面的 ER_CRASH 冲突了
RW_IRAM1 0x20000100 0x0001FF00 {
.ANY (+RW +ZI)
}
}
可以看到在SRAM里面,划分出来一块从不初始化的内存区域,这块区域后续也是很有用的,因为SRAM可不像SDRAM,不需要一直刷新。那只要Boot和APP跳转过程中不掉电,这块内存不就可以用来做沟通的桥梁了吗(当然在这里我还用一块外带的EEPROM做了备份,防止真掉电了>.<(RTC寄存器是不是也可以,都可以试一试))。
那为什么不用FLASH,因为FLASH是按扇区操作的(.<),真的很要命,而且FLASH的擦写寿命这一块
- 其它的疑问
实际上在看完对Boot的介绍后,聪明的你可能就会想,那为什么升级要这么麻烦呢?
APPA接收升级信号–>回到Bootloader做升级–>然后跳转新APP,为什么不直接在APPA里就完成对文件的写入和跳转呢?
我在这里的考虑主要是内存方面,因为FLASH真的太小了…,如果我让APPA和APPB都具备升级文件的功能,就意味着在内存中要有两份升级相关的代码(mbedtls和LWIP协议栈加起来可能有几十KB),而且裸机也没有动态链接库这么一说。
而且如果在APP里面升级APP,万一失误了是不是就变砖了。利用Bootloader也可以更好的做一些校验工作,万一变砖了可以Bootloader里面轮训等待新的升级文件并报警
具体功能实现
Ymodem协议
Ymodem协议介绍可以参考这篇博客:https://blog.csdn.net/huangdenan/article/details/103611081
Ymodem协议非常契合当前的升级模式,因为它是接收方通知发送方自己准备好了(跳转到Bootloader里面了),而且Ymodem协议的通信方式也非常适合通过有限状态机去编程(剩下的相信AI就好了)
- 存在的问题
掉电:A/B分区时不怕掉电的,掉电了校验不通过就会跳转老的分区
断点续传: 因为升级文件可能会非常大,要是每次掉电重新来过就太麻烦了。这里后续也许可以考虑参考http的断点续传功能实现断点续传也不错
RSA数字签名
数字签名介绍可以参考:https://baike.baidu.com/item/%E6%95%B0%E5%AD%97%E7%AD%BE%E5%90%8D/212550
环境搭建可以参考这篇:https://www.cnblogs.com/duapple/p/17915569.html
总体来说,数字签名是我们验证APP完整性的重要手段,防止APP被破坏(蓄意破坏或者是FLASH坏块了)。这部分的代码放在了 /主机代码/enctypt_app 下
整体思路就是在生成数字签名后,把数字签名和APP的信息(版本/大小/名字)整合在一起,固定大小为2KB,然后把这2KB和APP的bin文件合并,当接收到合并的文件后进行数字签名校验查看完整性。
uint8_t verify_app(uint8_t* fw_addr, uint32_t fw_size, uint32_t ds_addr)
{
if ( fw_addr == NULL || ds_addr == 0xffffffff)
return 0;
static char public_key_args_N[] = "BE29E714F51611C4F22A892658DC9A3CCF4DA5BF3F054F5EE9752901AC53BDB97BD6114C318F0C17C3EFFAE6CAB12114B98578295E18E405E1A4C151EF82794D7A9E5D171CE46BB5A5F2447175CC194D656E5A861288BD12E8235E20D932ADC7FCEF4F33342E76F7DBF5EBCA52857183485C01AD6583E773679020259A76AD7C5D13BF31D01987578DC368FDA41FDF80E88D37DA975DA7985548055A7232DD5356DE4AEF0C1BCA602943CD24654976AAC03A0577F3E04FF0BDB3680F35238267D0AA8E612155F97C0D18DED1B5B75690D5F64019BE54238BED42D31D9B742CF80EC20F54D39F582C5B26C451B3E36431B37ECE3DEA85B3BC207ADB98B226D3CD";
static char public_key_args_E[] = "010001";
printf("verify start:0x%x fw_size: %d ds_addr:0x%x\r\n", (uint32_t)fw_addr, fw_size, ds_addr);
int ret = 1;
uint8_t verify_ok = 0;
size_t i;
mbedtls_rsa_context rsa;
unsigned char hash[32];
unsigned char *buf = mymalloc(SRAMEX, APP_DS_BYTES); /*存放数字签名的*/
if (buf == NULL)
{
printf("alloc buf fail\r\n");
return 0;
}
memcpy(buf, (void *)ds_addr, APP_DS_BYTES); /*把数字签名拷贝过来*/
mbedtls_md_context_t ctx;
uint8_t *buffer_2k= mymalloc(SRAMEX, 2048);
if (buffer_2k == NULL)
{
myfree(SRAMEX, buf);
printf("alloc buffer_2k fail\r\n");
return 0;
}
int mod;
printf("verify app......\r\n");
mbedtls_rsa_init( &rsa );
if( ( ret = mbedtls_mpi_read_string( &rsa.MBEDTLS_PRIVATE(N), 16, public_key_args_N ) ) != 0 ||
( ret = mbedtls_mpi_read_string( &rsa.MBEDTLS_PRIVATE(E), 16, public_key_args_E ) ) != 0 )
{
printf( " failed\n ! mbedtls_mpi_read_string returned %d\n\n", ret );
goto exit;
}
rsa.MBEDTLS_PRIVATE(len) = ( mbedtls_mpi_bitlen( &rsa.MBEDTLS_PRIVATE(N) ) + 7 ) >> 3;
/*
* Compute the SHA-256 hash of the input file and
* verify the signature
*/
printf( "\n . Verifying the RSA/SHA-256 signature\r\n" );
mbedtls_md_init(&ctx);
ret = mbedtls_md_setup(&ctx, mbedtls_md_info_from_type(MBEDTLS_MD_SHA256), 0);
if (ret)
{
printf("mbedtls_md_setup error: %d\r\n", ret);
goto exit;
}
if ( (ret = mbedtls_md_starts(&ctx)) )
{
printf("mbedtsl_md_starts error: %d\r\n", ret);
goto exit;
}
mod = fw_size % 2048;
for (i = 0; i < fw_size / 2048; i++)
{
memcpy(buffer_2k, (void *)(fw_addr + 2048 * i), 2048);
// printf("-------------------part [%d](256bytes)--------------------------\r\n", i+1);
// for (int j = 0; j < 256; j++)
// {
// fprintf( stdout, "%02X%s", buffer_2k[j],
// ( j + 1 ) % 16 == 0 ? "\r\n" : " " );
// }
// printf("\r\n---------------------------------------------------------\r\n\n\n");
if ( (ret = mbedtls_md_update(&ctx, buffer_2k, 2048)) )
{
printf("mbedtls_md_update error: %d\r\n", ret);
}
}
if (mod)
{
memcpy(buffer_2k, (void *)(fw_addr + 2048 * i), mod);
// printf("-------------------part [%d]--------------------------\r\n", i+1);
// for (int j = 0; j < mod; j++)
// {
// fprintf( stdout, "%02X%s", buffer_2k[j],
// ( j + 1 ) % 16 == 0 ? "\r\n" : " " );
// }
// printf("\r\n---------------------------------------------------------\r\n\n\n");
if ( (ret = mbedtls_md_update(&ctx, buffer_2k, mod)) )
{
printf("mbedtls_md_update error: %d\r\n", ret);
}
}
if ( (ret = mbedtls_md_finish(&ctx, hash)))
{
printf("mbedtls_md_finish error: %d\r\n", ret);
}
printf("-----------------digital sign------------------------\r\n");
for( i = 0; i < rsa.MBEDTLS_PRIVATE(len); i++ )
printf("%02X%s", buf[i],
( i + 1 ) % 16 == 0 ? "\r\n" : " " );
printf("------------------digital sign-------------------------\r\n\n");
//
// printf("--------------------hash-------------------------\r\n");
// for( i = 0; i < 256; i++ )
// printf("%02X%s", hash[i],
// ( i + 1 ) % 16 == 0 ? "\r\n" : " " );
// printf("--------------------hash---------------------\r\n\n");
if( ( ret = mbedtls_rsa_pkcs1_verify( &rsa, MBEDTLS_MD_SHA256,
32, hash, buf ) ) != 0 )
{
printf( " failed\n ! mbedtls_rsa_pkcs1_verify returned -0x%0x\n\n", (unsigned int) -ret );
goto exit;
}
printf( "\n . OK (the signature is valid)\n\n" );
verify_ok = 1;
exit:
myfree(SRAMEX, buf);
myfree(SRAMEX, buffer_2k);
mbedtls_rsa_free( &rsa );
mbedtls_md_free(&ctx);
return verify_ok;
}
- 存在的问题
- 内存:
mbedtls默认使用malloc和free,所以要给heap_size留出一定空间,我记得用了快8KB
当然,mebedtls也运行你配置为自己的malloc和free,具体来说要使能几个宏,并调用具体的API;未用到编译器自带的内存管理(malloc,free等),设置Heap_Szie为0 Heap_Size EQU 0x00000000 AREA HEAP, NOINIT, READWRITE, ALIGN=3 __heap_base/*定义自己的内存分配器*/ #define MBEDTLS_PLATFORM_MEMORY #define MBEDTLS_PLATFORM_C mbedtls_platform_set_calloc_free(mycallocSRAMEX, my_freeSRAMEX);- 公钥与数字证书
这个还是安全性的考虑,感兴趣可以搜一下
- 内存:
AES加密
AES加密介绍的可以参考:https://blog.csdn.net/qq_28205153/article/details/55798628
这部分的代码主要是aes.c(忘记具体代码从哪copy的了),也只是为了看起来唬人,把Ymodem的协议包在封装完之后,再通过aes加密发送出去,来保证安全性。
AES是对称加密算法,这意味这其密钥必须妥善保管,否则一旦泄露就完蛋了。RSA是非对称加密算法,虽然更加保险,但是本身加密数据和解密需要大量时间和性能。所以就有了RSA数字签名+传输过程AES加密的方案
LWIP协议栈
这部分的代码就是正点原子提供的例程,之前写过一篇关于LWIP协议栈移植的博客:
https://blog.csdn.net/qq_45731845/article/details/145420389?spm=1011.2415.3001.5331
A/B分区
A/B分区的意思就是当我们升级的时候,如果当前活跃的是A分区,那么就把新的APP信息写入到B分区,然后跳转到B分区去跑新的程序,这样的好处就是增加了一道保险,哪怕B分区的程序出bug了,也可以回退到A分区运行。
A/B分区的代码在实现的时候,核心问题有以下几点:
- Bootloader如何知道当前活跃的分区是哪个分区?
- Bootloader怎么知道当前活跃的分区是否有BUG?
- Bootloader怎么知道当前有无升级标志位?
这几个问题要解决,核心就是通过Bootloader和APP共享的外设去记录一些信息,比如给FLASH的某个扇区预留出来专门做这个工作。我这边也是为了学习嘛,开发板上有个基于I2C的EEPROM外设,不用白不用(>.<)。
所以上面几个问题就好回答了 - Bootloader如何知道当前活跃的分区是哪个分区?—由app启动后写入地址A,bootloader只读
- Bootloader怎么知道当前活跃的分区是否有BUG?—由app发生异常后写入地址B,bootloader只读
- Bootloader怎么知道当前有无升级标志位?—由app接受到升级信号后写入地址C,bootloader只读
- APP启动后,写入当前活跃分区,并清除是否有BUG的标志位以及升级标志位
void app_flag_init(void)
{
at24cxx_write_one_byte(APP_ACTIVE_ADDRESS, sectorA); /*在哪里就写入哪个标志位*/
delay_ms(40);
at24cxx_write_one_byte(APP_RUN_ERR_ADDRESS, appNoErr);
delay_ms(40);
at24cxx_write_one_byte(APP_UPDATE_FLAG_ADDRESS, updNoNeed);
delay_ms(40);
}
不过因为写入不是原子性的,所以还是在某些场景下有bug,比如程序卡死在写入at24cxx_write_one_byte(APP_ACTIVE_ADDRESS, sectorA)之后,就算重启之后发现当前活跃分析还是SectorA就完蛋咯。
如何判断是不是出现ERR了,我在这里的做法是吧hardfault_handler重写了,在hardfault_handler里面记录,不过问题是万一hardfault就是写入at24cxx_write_one_byte引起的,就会陷入死循环了。
AI觉得可以看门狗,APP定时喂狗,在Bootloader里判断这次进入Bootloader是不是看门狗超时导致的,可以试试
void rt_hw_hard_fault_exception(struct exception_info *exception_info)
{
/*要是FLASH有额外的 还可以保存一下现场*/
// struct exception_stack_frame *exception_stack = &exception_info->stack_frame.exception_stack_frame;
struct stack_frame *context = &exception_info->stack_frame;
appErrFlag err_flag = appNoErr;
printf(" lr: 0x%08x\r\n", context->exception_stack_frame.lr);
printf("--------------------hard_fault_happen APPA------------------\r\n");
printf("psr: 0x%08x\r\n", context->exception_stack_frame.psr);
printf("r00: 0x%08x\r\n", context->exception_stack_frame.r0);
printf("r01: 0x%08x\r\n", context->exception_stack_frame.r1);
printf("r02: 0x%08x\r\n", context->exception_stack_frame.r2);
printf("r03: 0x%08x\r\n", context->exception_stack_frame.r3);
printf("r04: 0x%08x\r\n", context->r4);
printf("r05: 0x%08x\r\n", context->r5);
printf("r06: 0x%08x\r\n", context->r6);
printf("r07: 0x%08x\r\n", context->r7);
printf("r08: 0x%08x\r\n", context->r8);
printf("r09: 0x%08x\r\n", context->r9);
printf("r10: 0x%08x\r\n", context->r10);
printf("r11: 0x%08x\r\n", context->r11);
printf("r12: 0x%08x\r\n", context->exception_stack_frame.r12);
printf(" lr: 0x%08x\r\n", context->exception_stack_frame.lr);
printf(" pc: 0x%08x\r\n", context->exception_stack_frame.pc);
printf("hard fault happend\r\n");
if ( (exception_info->exc_return & 0x10) == 0)
{
printf("FPU active!\r\n");
}
volatile uint32_t cfsr = SCB->CFSR;
volatile uint32_t hfsr = SCB->HFSR;
volatile uint32_t mmfar = SCB->MMFAR;
volatile uint32_t bfar = SCB->BFAR;
if (cfsr & 0xFF)
{
err_flag |= appMemErr_Bit;
printf("MemManage Fault:\n");
if (cfsr & (1 << 7)) printf(" -> MMARVALID: MMFAR=%08x\n", mmfar);
if (cfsr & (1 << 0)) printf(" -> IACCVIOL\n");
if (cfsr & (1 << 1)) printf(" -> DACCVIOL\n");
}
if (cfsr & 0x00FF00)
{
printf("BusFault:\n");
err_flag |= appBusErr_Bit;
if (cfsr & (1 << 15)) printf(" -> BFARVALID: BFAR=%08x\n", bfar);
if (cfsr & (1 << 8)) printf(" -> IBUSERR\n");
if (cfsr & (1 << 9)) printf(" -> PRECISERR\n");
}
if (cfsr & 0xFF0000)
{
printf("UsageFault:\n");
err_flag |= appUsgFaultErr_Bit;
if (cfsr & (1 << 16)) printf(" -> UNDEFINSTR\n");
if (cfsr & (1 << 17)) printf(" -> INVSTATE\n");
if (cfsr & (1 << 18)) printf(" -> INVPC\n");
if (cfsr & (1 << 19)) printf(" -> NOCP\n");
if (cfsr & (1 << 25)) printf(" -> UNALIGNED\n");
if (cfsr & (1 << 26)) printf(" -> DIVBYZERO\n");
}
at24cxx_write_one_byte(APP_RUN_ERR_ADDRESS, err_flag);
while (1);
}
补充:问过的问题
这些问题的答案大伙可以自行AI解决,印象不是很深刻了,而且完全可以基于这些问题吧代码好好改改,到时候方便和面试官吹逼。
- STM32的启动过程?
- bootloader的代码逻辑(A/B分区是怎么做的, 升级的代码流程是什么样的)
- 怎么保证升级的可靠性与安全性?
- LWIP协议栈的移植?
- Uboot的启动过程(是的Linux的Uboot,面试官觉得我应该懂(但是没答上来))?
- 升级过程中断电怎么办,当前的代码有没有考虑到这些问题?
额…基本没有 - bootlaoder的业务逻辑这么简单,为啥还要上RTOS呢?
之前的bootlaoder确实上RTOS了,后来发现真没必要啊就这点代码上啥RTOS,就改过一版 - 涉及到I2C通信的一些问题
- I2C的通信原理
- I2C总线上通常会有哪些问题,协议上要做哪些容错处理?
- I2C出现设备冲突或者总线挂死要怎么回复?
- 如何减少STM32中bin文件代码的体积(从编译器/代码实现/代码设计)?
更多推荐

所有评论(0)