嵌入式面试高频题第6弹:SPI vs I2C对比、Flash读写原理、Bootloader设计,这3个搞不定等于白学
嵌入式面试必考的三大通信+存储+启动知识,从原理到代码到面试话术一网打尽,附追问预判和回答模板。
面试官:“SPI和I2C有什么区别?什么时候用哪个?”
你:“SPI速度快,I2C线少…”
面试官:“还有呢?”
你:“呃…就这些?”
面试官:(微笑,在简历上画了个圈)
别慌,这是90%的人都会挂的追问。 SPI、I2C、Flash、Bootloader这四个东西,面试问到的概率极高——因为它们代表了嵌入式开发的三个核心能力:通信、存储、启动。
这篇文章帮你把这三个知识点讲透,附带面试追问预判和回答模板。
💬 你面试被问过这几个问题吗?评论区说说你被问懵的瞬间!
1. SPI vs I2C 对比
问题:SPI和I2C有什么区别?什么时候用哪个?
回答
先看对比表:
| 对比项 | SPI | I2C |
|---|---|---|
| 线数 | 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总线挂了多个设备,怎么排查通信失败?
- 用逻辑分析仪抓波形:看SDA/SCL的电平是否正常
- 检查地址:用I2C扫描程序(遍历0x01~0x7F,看谁ACK)
- 检查上拉电阻:I2C是开漏输出,必须有上拉电阻(4.7kΩ常用)
- 检查时序:快速模式400kHz,设备跟不上会NACK
代码示例:I2C扫描程序
/* 扫描I2C总线上所有设备 */
void i2c_scan(I2C_HandleTypeDef *hi2c) {
printf("I2C设备扫描开始...\r\n");
uint8_t count = 0;
for (uint8_t addr = 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分两种用途:
- 程序存储:存放代码,掉电不丢失
- 数据存储:可以用来存配置参数(代替EEPROM)
关键点:Flash只能写0,不能写1。要写1必须先擦除(整页擦除,变成全1)。
┌─────────────────────────────────────────────────────────┐
│ Flash特性: │
│ • 读:随时可以读,速度很快(CPU直接访问) │
│ • 写:只能把1改成0,不能把0改成1 │
│ • 擦:必须整页擦除(1KB或2KB一页),变成全1 │
│ • 寿命:擦写次数有限(STM32F103约1万次) │
│ • 速度:擦除最慢(几十ms),写较快(几十μs/字) │
└─────────────────────────────────────────────────────────┘
面试官可能的追问
Q:Flash和EEPROM有什么区别?
对比项 Flash EEPROM 擦除单位 整页(1KB/2KB) 单字节 写入速度 较慢 较快 寿命 1万次 100万次 成本 低(集成在MCU里) 高(外挂芯片) 适用场景 存大块数据、参数少改 频繁改写小数据 实际项目中:如果参数改写不频繁(比如设备序列号、校准参数),用Flash省钱;如果要频繁存日志、计数器,用EEPROM或FRAM。
Q:写Flash前为什么要擦除?
因为Flash只能把1改成0。比如原来存的是0xFF(全1),你可以写入0x00;但如果原来是0x00,你写0xFF是写不进去的,必须先擦除变成0xFF。
Q:STM32的Flash读写有什么坑?
- 不能在Flash擦写时执行代码:Flash擦写期间CPU会暂停(除非用双Bank)
- 中断向量表:如果Bootloader占了低地址,要重定位中断向量表
- 对齐要求:STM32F103写入必须半字(16位)对齐
- 掉电保护:写到一半掉电会丢数据,要加备份机制
代码示例:STM32F103 Flash读写
/* Flash写入(HAL库) */
#include "stm32f1xx_hal_flash.h"
/* 写入参数到Flash最后一页 */
#define FLASH_PARAM_ADDR 0x0800FC00 /* Flash最后一页地址(假设64KB Flash) */
HAL_StatusTypeDef flash_write_params(uint16_t *data, uint16_t len) {
HAL_StatusTypeDef status = HAL_OK;
FLASH_EraseInitTypeDef erase;
uint32_t error;
/* 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();
return status;
}
/* 3. 写入数据(半字对齐) */
for (uint16_t i = 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();
return status;
}
/* 读取参数(直接访问地址) */
void flash_read_params(uint16_t *buf, uint16_t len) {
uint16_t *addr = (uint16_t *)FLASH_PARAM_ADDR;
for (uint16_t i = 0; i < len; i++) {
buf[i] = addr[i];
}
}
3. Bootloader原理
问题:什么是Bootloader?怎么设计一个简单的Bootloader?
回答
Bootloader是启动加载器,在应用程序运行之前执行,负责:
- 初始化硬件
- 判断是否需要更新固件
- 如果需要,从串口/USB/网络接收新固件并写入Flash
- 跳转到应用程序执行
┌─────────────────────────────────────────────────────────┐
│ Bootloader工作流程: │
│ │
│ 上电 → Bootloader启动 → 检查更新标志 │
│ ↓ ↓ │
│ 有更新请求 无更新请求 │
│ ↓ ↓ │
│ 接收新固件 跳转到APP │
│ 写入Flash ↓ │
│ 清除标志 APP运行 │
│ 跳转到APP ↓ │
│ │
│ Flash布局: │
│ ┌──────────┬──────────────────────────────────┐ │
│ │Bootloader│ Application │ │
│ │ (16KB) │ (剩余空间) │ │
│ │0x08000000│0x08004000 │ │
│ └──────────┴──────────────────────────────────┘ │
└─────────────────────────────────────────────────────────┘
面试官可能的追问
Q:Bootloader和APP怎么跳转?
- 设置主堆栈指针(MSP)为APP的初始SP值(Flash第一个字)
- 获取APP的复位中断向量(Flash第二个字)
- 跳转到该地址执行
/* 跳转到APP */ void jump_to_app(uint32_t app_addr) { /* 检查APP地址是否有效(栈指针在SRAM范围内) */ if ((*(uint32_t *)app_addr & 0x2FF00000) == 0x20000000) { /* 设置主堆栈指针 */ __set_MSP(*(uint32_t *)app_addr); /* 获取复位中断向量 */ uint32_t jump_addr = *(uint32_t *)(app_addr + 4); /* 跳转 */ ((void (*)(void))jump_addr)(); } }
Q:APP的中断向量表怎么处理?
Bootloader占了Flash低地址,APP的中断向量表偏移了。需要:
- 在APP的main函数开头,重定位中断向量表:
SCB->VTOR = 0x08004000; /* APP起始地址 */- 或者在Bootloader跳转前设置:
SCB->VTOR = APP_ADDR;
Q:怎么判断是否需要进入Bootloader?
常见方法:
- 检查标志位:Flash中存一个标志,APP要更新时写入"需要更新"
- 检查按键:上电时检测某个按键是否按下
- 检查串口命令:上电后等100ms,收到特定命令就进入更新模式
- 检查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主循环 */
int main(void) {
HAL_Init();
SystemClock_Config();
MX_USART1_UART_Init();
/* 检查是否需要进入Bootloader */
uint8_t boot_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循环:接收固件 */
void bootloader_loop(void) {
uint8_t cmd;
while (1) {
if (HAL_UART_Receive(&huart1, &cmd, 1, 100) == HAL_OK) {
switch (cmd) {
case CMD_HANDSHAKE:
send_version();
break;
case CMD_ERASE:
erase_app_area();
break;
case CMD_WRITE:
receive_and_write();
break;
case CMD_VERIFY:
verify_crc();
break;
case CMD_JUMP:
jump_to_app(APP_ADDR);
break;
}
}
}
}
总结:三个知识点怎么串起来理解?
┌───────────┐ ┌───────────┐ ┌───────────┐
│ SPI/I2C │ │ Flash │ │Bootloader │
│ 通信 │ │ 存储 │ │ 启动 │
│ │ │ │ │ │
│ 外设怎么 │ │ 数据怎么 │ │ 程序怎么 │
│ 连接通信 │ │ 持久保存 │ │ 更新升级 │
└─────┬─────┘ └─────┬─────┘ └─────┬─────┘
│ │ │
└───────────────┼───────────────┘
▼
一个完整的嵌入式系统:
能通信(SPI/I2C)+ 能存储(Flash)+ 能升级(Bootloader)
面试回答模板:先说是什么 → 再说为什么需要 → 最后说你在哪里用过。有项目经验加分巨大。
📌 下期预告:嵌入式面试高频题第7弹——RTOS任务调度、中断与任务同步、内存管理
👉 关注我不迷路,持续更新嵌入式面试题 + FreeRTOS笔记
💬 评论区来聊聊:
- SPI和I2C你更常用哪个?遇到过什么通信问题?
- 你用过Flash存参数吗?遇到过什么坑?
- 你做过Bootloader吗?用的什么协议?
每一条评论我都会回复,大家一起进步!
更多推荐


所有评论(0)