☰
嵌入式面试高频题第6弹:SPI vs I2C对比、Flash读写原理、Bootloader设计,这3个搞不定等于白学
2026/10/27 13:33:17 网站建设 项目流程

嵌入式面试必考的三大通信+存储+启动知识,从原理到代码到面试话术一网打尽,附追问预判和回答模板。

面试官:“SPI和I2C有什么区别?什么时候用哪个?”

你:“SPI速度快,I2C线少…”

面试官:“还有呢?”

你:“呃…就这些?”

面试官:(微笑,在简历上画了个圈)

别慌,这是90%的人都会挂的追问。SPI、I2C、Flash、Bootloader这四个东西,面试问到的概率极高——因为它们代表了嵌入式开发的三个核心能力:通信、存储、启动。

这篇文章帮你把这三个知识点讲透,附带面试追问预判和回答模板。

💬 你面试被问过这几个问题吗?评论区说说你被问懵的瞬间!


1. SPI vs I2C 对比

问题:SPI和I2C有什么区别?什么时候用哪个?

回答

先看对比表:

对比项SPII2C
线数4根(MOSI/MISO/SCK/CS)2根(SDA/SCL)
速度最高几十MHz标准100kHz,快速400kHz,高速3.4MHz
全双工✅ 支持❌ 半双工
寻址靠片选CS,一对一7位/10位地址,一对多
时钟主设备产生主设备产生
协议复杂度简单,无应答复杂,有ACK/NACK
距离板级,<30cm板级,<1m

一句话总结:SPI是"专线电话",速度快但占线多;I2C是"总线电话",线少但要排队。

什么时候用哪个?

┌─────────────────────────────────────────────────────────┐ │ 用SPI的场景: │ │ • 需要高速传输(屏幕刷新、SD卡读写) │ │ • 设备少,不需要省引脚 │ │ • 对时序要求严格(ADC、DAC) │ ├─────────────────────────────────────────────────────────┤ │ 用I2C的场景: │ │ • 引脚紧张(传感器、EEPROM、OLED) │ │ • 设备多,需要总线挂载多个设备 │ │ • 速度要求不高(配置寄存器、读取状态) │ └─────────────────────────────────────────────────────────┘

面试官可能的追问

Q:SPI的四种模式(CPOL/CPHA)是什么?

CPOL决定时钟空闲电平,CPHA决定数据采样时刻:

  • Mode 0:CPOL=0, CPHA=0 → 空闲低电平,第一个边沿采样
  • Mode 1:CPOL=0, CPHA=1 → 空闲低电平,第二个边沿采样
  • Mode 2:CPOL=1, CPHA=0 → 空闲高电平,第一个边沿采样
  • Mode 3:CPOL=1, CPHA=1 → 空闲高电平,第二个边沿采样

实际用的时候看设备数据手册,大多数设备用Mode 0或Mode 3。

Q:I2C的ACK/NACK机制是什么?

发送方发完一个字节后,释放SDA线(拉高)。接收方在第9个时钟周期把SDA拉低表示ACK(收到了),保持高电平表示NACK(没收到或不想收)。

应用场景:主设备读取从设备数据时,最后一个字节发NACK表示"读完了,不用再发了"。

Q:I2C总线挂了多个设备,怎么排查通信失败?

  1. 用逻辑分析仪抓波形:看SDA/SCL的电平是否正常
  2. 检查地址:用I2C扫描程序(遍历0x01~0x7F,看谁ACK)
  3. 检查上拉电阻:I2C是开漏输出,必须有上拉电阻(4.7kΩ常用)
  4. 检查时序:快速模式400kHz,设备跟不上会NACK

代码示例:I2C扫描程序

/* 扫描I2C总线上所有设备 */voidi2c_scan(I2C_HandleTypeDef*hi2c){printf("I2C设备扫描开始...\r\n");uint8_tcount=0;for(uint8_taddr=1;addr<127;addr++){/* 尝试发送地址,看有没有ACK */if(HAL_I2C_IsDeviceReady(hi2c,addr<<1,1,10)==HAL_OK){printf(" 发现设备: 0x%02X\r\n",addr);count++;}}if(count==0){printf(" 未发现任何设备!检查接线和上拉电阻\r\n");}else{printf(" 共发现 %d 个设备\r\n",count);}}

2. Flash读写原理

问题:STM32的Flash怎么读写?有什么注意事项?

回答

STM32的Flash分两种用途:

  1. 程序存储:存放代码,掉电不丢失
  2. 数据存储:可以用来存配置参数(代替EEPROM)

关键点:Flash只能写0,不能写1。要写1必须先擦除(整页擦除,变成全1)。

┌─────────────────────────────────────────────────────────┐ │ Flash特性: │ │ • 读:随时可以读,速度很快(CPU直接访问) │ │ • 写:只能把1改成0,不能把0改成1 │ │ • 擦:必须整页擦除(1KB或2KB一页),变成全1 │ │ • 寿命:擦写次数有限(STM32F103约1万次) │ │ • 速度:擦除最慢(几十ms),写较快(几十μs/字) │ └─────────────────────────────────────────────────────────┘

面试官可能的追问

Q:Flash和EEPROM有什么区别?

对比项FlashEEPROM
擦除单位整页(1KB/2KB)单字节
写入速度较慢较快
寿命1万次100万次
成本低(集成在MCU里)高(外挂芯片)
适用场景存大块数据、参数少改频繁改写小数据

实际项目中:如果参数改写不频繁(比如设备序列号、校准参数),用Flash省钱;如果要频繁存日志、计数器,用EEPROM或FRAM。

Q:写Flash前为什么要擦除?

因为Flash只能把1改成0。比如原来存的是0xFF(全1),你可以写入0x00;但如果原来是0x00,你写0xFF是写不进去的,必须先擦除变成0xFF。

Q:STM32的Flash读写有什么坑?

  1. 不能在Flash擦写时执行代码:Flash擦写期间CPU会暂停(除非用双Bank)
  2. 中断向量表:如果Bootloader占了低地址,要重定位中断向量表
  3. 对齐要求:STM32F103写入必须半字(16位)对齐
  4. 掉电保护:写到一半掉电会丢数据,要加备份机制

代码示例:STM32F103 Flash读写

/* Flash写入(HAL库) */#include"stm32f1xx_hal_flash.h"/* 写入参数到Flash最后一页 */#defineFLASH_PARAM_ADDR0x0800FC00/* Flash最后一页地址(假设64KB Flash) */HAL_StatusTypeDefflash_write_params(uint16_t*data,uint16_tlen){HAL_StatusTypeDef status=HAL_OK;FLASH_EraseInitTypeDef erase;uint32_terror;/* 1. 解锁Flash */HAL_FLASH_Unlock();/* 2. 擦除最后一页 */erase.TypeErase=FLASH_TYPEERASE_PAGES;erase.PageAddress=FLASH_PARAM_ADDR;erase.NbPages=1;status=HAL_FLASHEx_Erase(&erase,&error);if(status!=HAL_OK){HAL_FLASH_Lock();returnstatus;}/* 3. 写入数据(半字对齐) */for(uint16_ti=0;i<len;i++){status=HAL_FLASH_Program(FLASH_TYPEPROGRAM_HALFWORD,FLASH_PARAM_ADDR+i*2,data[i]);if(status!=HAL_OK)break;}/* 4. 锁定Flash */HAL_FLASH_Lock();returnstatus;}/* 读取参数(直接访问地址) */voidflash_read_params(uint16_t*buf,uint16_tlen){uint16_t*addr=(uint16_t*)FLASH_PARAM_ADDR;for(uint16_ti=0;i<len;i++){buf[i]=addr[i];}}

3. Bootloader原理

问题:什么是Bootloader?怎么设计一个简单的Bootloader?

回答

Bootloader是启动加载器,在应用程序运行之前执行,负责:

  1. 初始化硬件
  2. 判断是否需要更新固件
  3. 如果需要,从串口/USB/网络接收新固件并写入Flash
  4. 跳转到应用程序执行
┌─────────────────────────────────────────────────────────┐ │ Bootloader工作流程: │ │ │ │ 上电 → Bootloader启动 → 检查更新标志 │ │ ↓ ↓ │ │ 有更新请求 无更新请求 │ │ ↓ ↓ │ │ 接收新固件 跳转到APP │ │ 写入Flash ↓ │ │ 清除标志 APP运行 │ │ 跳转到APP ↓ │ │ │ │ Flash布局: │ │ ┌──────────┬──────────────────────────────────┐ │ │ │Bootloader│ Application │ │ │ │ (16KB) │ (剩余空间) │ │ │ │0x08000000│0x08004000 │ │ │ └──────────┴──────────────────────────────────┘ │ └─────────────────────────────────────────────────────────┘

面试官可能的追问

Q:Bootloader和APP怎么跳转?

  1. 设置主堆栈指针(MSP)为APP的初始SP值(Flash第一个字)
  2. 获取APP的复位中断向量(Flash第二个字)
  3. 跳转到该地址执行
/* 跳转到APP */voidjump_to_app(uint32_tapp_addr){/* 检查APP地址是否有效(栈指针在SRAM范围内) */if((*(uint32_t*)app_addr&0x2FF00000)==0x20000000){/* 设置主堆栈指针 */__set_MSP(*(uint32_t*)app_addr);/* 获取复位中断向量 */uint32_tjump_addr=*(uint32_t*)(app_addr+4);/* 跳转 */((void(*)(void))jump_addr)();}}

Q:APP的中断向量表怎么处理?

Bootloader占了Flash低地址,APP的中断向量表偏移了。需要:

  1. 在APP的main函数开头,重定位中断向量表:
    SCB->VTOR=0x08004000;/* APP起始地址 */
  2. 或者在Bootloader跳转前设置:
    SCB->VTOR=APP_ADDR;

Q:怎么判断是否需要进入Bootloader?

常见方法:

  1. 检查标志位:Flash中存一个标志,APP要更新时写入"需要更新"
  2. 检查按键:上电时检测某个按键是否按下
  3. 检查串口命令:上电后等100ms,收到特定命令就进入更新模式
  4. 检查APP有效性:Flash APP区域全0xFF说明没有APP,进入Bootloader

Q:串口升级的协议怎么设计?

简单协议示例:

帧头(2字节) + 命令(1字节) + 长度(2字节) + 数据(N字节) + 校验(1字节) 命令定义: 0x01: 握手(PC→MCU,MCU回复版本号) 0x02: 擦除(PC→MCU,MCU擦除APP区域) 0x03: 写入(PC→MCU,发送固件数据) 0x04: 校验(PC→MCU,MCU回复CRC) 0x05: 跳转(PC→MCU,MCU跳转到APP)

代码示例:简单的串口Bootloader框架

/* Bootloader主循环 */intmain(void){HAL_Init();SystemClock_Config();MX_USART1_UART_Init();/* 检查是否需要进入Bootloader */uint8_tboot_flag=*(uint8_t*)BOOT_FLAG_ADDR;if(boot_flag==0xAA||is_app_valid()==0){/* 进入Bootloader模式 */printf("[BOOT] 进入Bootloader模式\r\n");bootloader_loop();}else{/* 跳转到APP */printf("[BOOT] 跳转到APP\r\n");jump_to_app(APP_ADDR);}while(1);}/* Bootloader循环:接收固件 */voidbootloader_loop(void){uint8_tcmd;while(1){if(HAL_UART_Receive(&huart1,&cmd,1,100)==HAL_OK){switch(cmd){caseCMD_HANDSHAKE:send_version();break;caseCMD_ERASE:erase_app_area();break;caseCMD_WRITE:receive_and_write();break;caseCMD_VERIFY:verify_crc();break;caseCMD_JUMP:jump_to_app(APP_ADDR);break;}}}}

总结:三个知识点怎么串起来理解?

┌───────────┐ ┌───────────┐ ┌───────────┐ │ SPI/I2C │ │ Flash │ │Bootloader │ │ 通信 │ │ 存储 │ │ 启动 │ │ │ │ │ │ │ │ 外设怎么 │ │ 数据怎么 │ │ 程序怎么 │ │ 连接通信 │ │ 持久保存 │ │ 更新升级 │ └─────┬─────┘ └─────┬─────┘ └─────┬─────┘ │ │ │ └───────────────┼───────────────┘ ▼ 一个完整的嵌入式系统: 能通信(SPI/I2C)+ 能存储(Flash)+ 能升级(Bootloader)

面试回答模板:先说是什么 → 再说为什么需要 → 最后说你在哪里用过。有项目经验加分巨大。


📌下期预告:嵌入式面试高频题第7弹——RTOS任务调度、中断与任务同步、内存管理

👉 关注我不迷路,持续更新嵌入式面试题 + FreeRTOS笔记

💬 评论区来聊聊:

  • SPI和I2C你更常用哪个?遇到过什么通信问题?
  • 你用过Flash存参数吗?遇到过什么坑?
  • 你做过Bootloader吗?用的什么协议?

每一条评论我都会回复,大家一起进步!

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询