经纬恒润(成都)嵌入式一面 + 二面

一面(50min)

1. 自我介绍

2. 主要问MCU项目

3. 任务设计?优先级?功能?所有问题都要给出原因

4. 你的项目架构是怎么设计的?

5. 任务间的偏移?

任务间偏移指多个周期任务实际启动时刻存在相位差,主要由抢占、调度延迟引起。我们不用每个任务单独延时,而是采用事件组,由同一个定时源触发所有同步任务,避免偏移累积;同时配合时间戳做数据时序补偿,保证视觉和控制时序对齐。

6. 你使用到了基于SPI的外部FLASH,能否描述一下SPI通信的特点?

SPI 是主机控制时钟的同步串行接口,主从架构,四根信号线,支持全双工。依靠片选 CS 选中设备,通过 CPOL、CPHA 配置 4 种时钟模式,通信速率比 I2C 高。缺点是无应答机制,多从机需要多片选引脚。项目中使用 SPI 外接 Flash,用来保存配置和日志,采用 Mode0 通信。

模式 CPOL CPHA 空闲 SCLK 采样边沿 备注
Mode0 0 0 低电平 第一个上升沿 SPI Flash 最常用!
Mode1 0 1 低电平 第二个下降沿 较少用
Mode2 1 0 高电平 第一个下降沿 较少用
Mode3 1 1 高电平 第二个上升沿 部分 OLED、传感器使用

通俗记忆口诀

  • CPOL=0:空闲 SCLK 是低;CPOL=1:空闲 SCLK 是高
  • CPHA=0:第一个边沿采样 ;CPHA=1:第二个边沿采样

7. 为什么你的SPI模式要选择00?

选用 SPI Mode0(CPOL=0,CPHA=0),是因为外部 SPI Flash 芯片上电默认就是 Mode0 时序,无需配置 Flash。它空闲时钟为低,第一个上升沿采样,兼容性好,时序稳定,能保证 MCU 和 Flash 通信采样点对齐,避免读到乱码。

8. SPI有几根线?

9. 你用到了I2C,主要用来驱动什么外设?I2C和SPI的区别?

项目中 I2C 主要用来驱动低速传感器,比如IMU 加速度陀螺仪、温湿度传感器、OLED 显示屏这类设备。这类外设数据量不大,对实时性要求不高,但是引脚资源要省,正好适合 I2C。

I2C 和 SPI 的主要区别:

  1. 信号线:I2C 只需要 SDA、SCL 两根线;SPI 最少需要 SCLK、MOSI、MISO、CS 四根线,外设多的时候 CS 片选会占用大量 IO。
  2. 设备寻址 :I2C 靠从机地址区分多个设备,同一条总线可以挂载多个器件,不需要额外 IO;SPI 依靠独立 CS 片选引脚,新增从机就要多一根 CS。
  3. 通信方式:I2C 半双工,同一时刻只能单向传输;SPI 支持全双工,可以同时收发。
  4. 应答机制:I2C 有内置 ACK 应答,主机可以确认从机是否收到数据;SPI 没有硬件 ACK,主机发出去不知道从机是否正确接收。
  5. 速度:I2C 速率偏低,常用 100K、400K,最高几 MHz;SPI 速度高,几十 MHz 很常见,适合 Flash、高速外设。
  6. 时钟:I2C 时钟由主机产生,但总线有仲裁机制,支持多主机;SPI 是单主机架构,没有总线仲裁。
  7. 时序:I2C 有起始、停止信号;SPI 靠 CS 片选的拉低拉高来标记一次通信。

10. I2C读取是的序列,要求给出整体的顺序?

  1. 发送起始信号 Start:SCL 高时,SDA 由高→低
  2. 发送从机地址 + 写位(R/W=0) :7 位从地址 + 0,表示主机要写寄存器地址到从机
  3. 等待从机 ACK:从机拉低 SDA 应答;收到 NACK 代表设备没响应,直接停止
  4. 发送要读取的寄存器地址:主机把目标寄存器编号发出去
  5. 等待从机 ACK:从机应答寄存器地址
  6. 发送重复起始信号 Repeated Start:不发 Stop!直接再次 Start(重点,很多人忘)
  7. 发送从机地址 + 读位(R/W=1):7 位从地址 + 1,表示接下来主机读取数据
  8. 读取从机返回的数据字节 :主机在 SCL 高电平采样 SDA
    • 如果还要继续读下一个字节 :主机发送 ACK,通知从机继续发数据
    • 如果这是最后一个字节 :主机发送 NACK,告诉从机不再读了
  9. 发送停止信号 Stop:SCL 高时,SDA 由低→高,结束本次 I2C 通信

✅ 一句话概括:起始 → 从地址写 → 寄存器地址 → 重复起始 → 从地址读 → 读数据 + ACK/NACK → 停止

精简背诵版(15 秒)

I2C 读寄存器整体顺序:先发送起始信号,发送从机写地址 + 寄存器地址,收到 ACK 后发送重复起始信号;再发送从机读地址,读取数据。非最后一字节主机发 ACK,最后一字节主机发 NACK,最后发送停止信号结束传输。

1. 起始信号 Start

SCL 保持高电平;SDA 高→低

SCL 高的时候,SDA 下降沿 = Start

2. 发送从机写地址(7bit+W=0),共 8bit

循环 8 次(每 1bit):

  1. SCL 拉低 → 主机把 bit 放到 SDA
  2. SCL 拉高 → 从机采样 SDA
  3. SCL 拉低,准备下一位 8bit 发完。

3. 等待从机 ACK(第 9 个时钟)

  1. SCL 拉低 → 主机释放 SDA(SDA 变为高)
  2. SCL 拉高 → 从机拉低 SDA,表示 ACK;SDA 保持高就是 NACK
  3. SCL 拉低

4. 发送寄存器地址(8bit)

和上面发送字节一样: SCL 低,主机置 SDA;SCL 高,从机采样;循环 8bit。

5. 等待从机 ACK(第 9 时钟)

同上,SCL 拉高时从机拉低 SDA 做 ACK。

6. 重复起始 Repeated Start(重点!不发 Stop)

SCL 保持高;SDA 高→低,和普通 Start 时序完全一样。

区别:前面没有 Stop,寄存器指针保留。

7. 发送从机读地址(7bit+R=1),8bit

主机发送 bit:SCL 低置 SDA,SCL 高采样,循环 8bit。

8. 从机 ACK

SCL 拉高,从机拉低 SDA 应答。

9. 读取数据字节(8bit,从机发,主机收)

循环 8 次:

  1. SCL 拉低 → 主机释放 SDA,由从机控制 SDA 电平
  2. SCL 拉高 → 主机采样 SDA 上的数据 bit
  3. SCL 拉低,准备下一位

10. 主机回复 ACK / NACK(第 9 时钟)

✅ 读数据阶段,ACK/NACK 由主机发出!

  1. SCL 拉低 → 主机控制 SDA:
    • 要继续读:主机拉低 SDA = ACK
    • 最后 1 字节:主机释放 SDA,SDA 变高 = NACK
  2. SCL 拉高,从机采样这个 ACK/NACK
  3. SCL 拉低

如果连续读多字节:读完一个字节 + 主机 ACK,继续回到【读数据字节】步骤;读到最后一个字节,主机发 NACK。

11. 停止信号 Stop

SCL 保持高电平;SDA 低→高

SCL 高时 SDA 上升沿 = Stop,总线回到空闲:SCL=1,SDA=1

面试口述精简版

空闲状态 SCL、SDA 都是高。

Start:SCL 高,SDA 由高变低。

发送字节时:SCL 拉低,主机放置 SDA 数据;SCL 拉高,从机采样;8 位结束后第 9 个时钟,从机拉低 SDA 给出 ACK。

发送完寄存器地址后,在不发送 Stop 的情况下,SCL 保持高,SDA 高到低产生重复起始信号。 接着发送读地址,收到 ACK 后开始读数据:SCL 拉低,从机驱动 SDA;SCL 拉高,主机采样。

读完一个字节,第 9 个时钟由主机控制 SDA,发 ACK 继续读,NACK 代表结束读取。

最后 Stop 信号:SCL 保持高,SDA 由低变高,总线回到空闲高电平。

11. I2C有相同的从设备,如何进行区别?

I2C 依靠从机地址识别外设。若设备地址相同,会发生地址冲突。解决方案:第一,如果外设带有 A0/A1 地址引脚,通过硬件高低电平修改设备地址;第二,使用 I2C 多路开关 TCA9548,分时选通不同通道上的设备;第三,分到 MCU 不同的 I2C 硬件总线,物理隔离。

12. 你用到了DMA,DMA主要用来干什么?

DMA 是直接存储器访问,由硬件完成数据搬运,不需要 CPU 逐字节拷贝。CPU 只需预先配置源地址、目的地址和传输长度,启动 DMA 后就可以执行其他任务,传输完成产生中断通知 CPU。项目中用 DMA 配合 SPI 做 Flash 大批量读写,减少 CPU 占用,降低中断频率。

13. DMA的配置流程?

首先开启 DMA 和 SPI 外设时钟;配置 DMA 传输方向、源地址、目的地址、地址增量、数据宽度、传输数量和工作模式;按需开启 DMA 传输完成中断并配置 NVIC;将 SPI 外设请求映射到 DMA 通道;使能 DMA,再打开 SPI 的 DMA 请求;传输完成触发中断通知 CPU。

14. Bootloader是自己做的吗?功能是什么?

Bootloader 是我自己实现的引导程序,上电最先执行。主要做硬件初始化,判断是否需要固件升级;需要升级时接收固件、CRC 校验,擦写 Flash 更新 App;升级成功则跳转到应用 App 运行;固件校验失败不会跳转,防止设备变砖。Flash 做分区,Bootloader 和 App 放在不同 Flash 区域。

15. Bootloader升级用了什么协议?

Bootloader 升级基于串口,使用自研轻量分包协议。数据包包含帧头、包序号、长度、数据、CRC 校验、帧尾。采用停等重传机制,每收到一包回复 ACK/NACK。全部接收完成后做整体固件 CRC 校验,校验通过才烧写 Flash。没有使用 Ymodem,自研协议代码更小,方便移植。

USART 是通用同步异步收发器。它可以工作在异步模式,也就是常说的 UART 串口,收发不需要共享时钟,依靠波特率、起始停止位做帧同步,使用 TX、RX、GND 三根线。我们项目 Bootloader 固件升级就是使用 USART 异步串口传输固件。它也支持同步模式,但项目没有使用。

16. 怎么判断固件下载的完整性?

分两级校验。每一个数据包附带 CRC16,接收时校验单包,出错就重传;所有包接收拼接完成后,计算整个固件的全局 CRC,和上位机下发的全局 CRC 对比。烧入 Flash 后,还会再读 Flash 做一次 CRC 二次校验,校验全部通过,才认为固件完整并允许跳转 App。

17. ADC用来采集什么数据?

ADC 用来采集模拟电压,项目主要采集供电电压做欠压检测,还有模拟传感器、采样电阻的电压;将模拟量转为数字量。使用 DMA 配合 ADC 进行连续采集,降低 CPU 开销。

18. 除了你用的外设?单片机中还有哪些外设?

还有 TIM 定时器、看门狗、DAC、CAN、RTC、USB、硬件 CRC、ETH 以太网、SDIO、片上比较器。定时器可以输出 PWM、输入捕获;看门狗用来防止程序死机复位;RTC 提供实时时间;CAN 用于工业总线通信。

19. Timer定时器的功能?

Timer 定时器可以产生定时中断,实现周期性任务;支持 PWM 输出,用于电机、LED 调光;输入捕获测量脉冲频率;正交编码器接口读取电机编码器;还可以触发 ADC 采样。分为基本、通用、高级定时器,高级定时器带有互补 PWM 和刹车保护。

20. 中断的配置流程?

中断配置分两大块:外设本身的中断使能 + NVIC 内核中断优先级配置,一共 4 个核心步骤:

  1. 开启外设时钟:比如 TIM、USART、ADC,先打开外设时钟,外设才能工作。

  2. 外设内部配置,开启外设对应的中断标志 配置外设功能,然后使能外设的中断。例如:定时器开启更新中断;串口开启接收完成中断;DMA 开启传输完成中断。外设产生事件后,置起中断请求标志位。

  3. 配置 NVIC(嵌套向量中断控制器) NVIC 是内核里面的模块,管理所有中断。

    • 设置抢占优先级、响应优先级;
    • 使能该中断通道。

    抢占优先级高,可以打断低抢占优先级的中断;响应优先级只在多个中断同时到来时决定先后,不能中断嵌套。

  4. 编写中断服务函数(ISR) 在中断服务函数里面,先清除中断标志位(非常关键,不清除会反复进中断),再写对应的业务处理代码。

补充要点: 全局中断开关__enable_irq(),默认上电是打开的;一般不需要手动开启,除非代码里手动关闭过全局中断。

21. 有没有进行RAM划分?

有进行 RAM 划分。编译器自动划分 data、bss、栈、堆。我手动开辟 DMA 专用缓冲区,保证连续内存;划分 Bootloader 与 App 的共享 RAM;修改启动文件配置栈大小,并且通过 map 文件查看 RAM 占用,避免栈溢出。

22. FLASH Driver放在哪里?有没有考虑过将其放置在Bootloader中的不安全性?

Flash 驱动放在 Bootloader 中,App 不含 Flash 擦写代码。我考虑过安全风险:Bootloader 损坏会导致设备变砖。解决方案是分区隔离,在驱动中增加地址校验,禁止擦除 Bootloader 区域;固件烧写前做完整 CRC 校验,避免写入非法固件。

23. 堆栈?

24. 如何判断栈溢出?如何判断栈大小分配是否合适?

栈溢出常见现象:程序随机 HardFault、莫名其妙死机、变量被莫名篡改,问题很难稳定复现。判断栈溢出有静态分析动态运行检测两种方式。

① 静态:查看 map 文件

② 动态方法 1:栈水印法(栈填充标记,项目最常用)

③ 动态方法 2:MCU 硬件栈检测(部分 Cortex-M 内核支持)

④ 调试器在线查看

25. 除了FreeRTOS的api,还有什么判断栈溢出的方法?

26. C语言如何获取绝对地址10000000的值并赋值给变量A?给出表达式?

复制代码
uint32_t A = *(volatile uint32_t *)0x10000000;

逐句解释

  1. 0x10000000:目标绝对地址;
  2. (uint32_t *)0x10000000:把数字强制转换成uint32_t 类型指针
  3. *:解引用,读取这个地址里面存放的值;
  4. volatile 必须加,防止编译器优化,避免编译器把读取直接优化掉(寄存器 / 外设地址访问必备)。

27. const变量和非const变量的区别?

const用来修饰变量,表示只读,编译器在编译阶段检查,不能直接修改。

  1. 存储位置
  • 全局 / 静态const常量:一般存放在 Flash (.rodata 只读段),掉电不丢失,不能写;
  • 普通全局非 const 变量:初始化的在.data,未初始化在.bss,都在 RAM,可以读写;
  • 函数内部局部 const:如果是简单常量,编译器可能直接优化;如果是局部 const 数组,会放在栈上(可以强行指针篡改,但属于未定义行为)。

28. 对变量的某位进行置位操作

29. if(x == 0) 和 if(0 == x) 哪个更好?

if(0 == x) 更好。如果误把 == 写成 =,0 = x属于常量赋值,直接编译报错,提前捕获 bug;而if(x=0)是合法赋值,编译器不报错,会造成隐藏逻辑错误。现代编译器开启警告后也能检测if(x=0)。(速度一样)

30. const和define的区别?

#define 是预处理文本替换,无类型,不占用内存,没有作用域;const 是编译期带类型的常量,存放在 rodata 段,可以取地址,支持类型检查。宏容易产生优先级错误,const 更加安全。

31. 每个项目的代码行?

32. 项目中遇到问题是怎么解决的?

33. 压力大的时候如何缓解?

34. 家庭地址?

35. 六级成绩?

36. 什么事你坚持了很久?

37. 面了几家公司?

38. 薪资要求?

39. 反问环节

二面(40min)

1. 主要讲PPT,中途打断提问

相关推荐
新晨单片机设计2 小时前
S014A-基于STM32单片机计算器(LCD1602显示)【Proteus仿真+Keil程序+原理图】
stm32·单片机·proteus
POMAGTOR2 小时前
【磁吸 POGOPIN 连接器 汽车医疗机器人弹簧顶针】
嵌入式硬件·机器人·汽车
clear sky .3 小时前
内存8字节对齐是什么?
c语言·stm32·嵌入式硬件
辰域电子3 小时前
STM32项目开源:智能安防与燃气监测系统(代码+原理图+仿真)
stm32·单片机·嵌入式硬件·开源·毕业设计·proteus
服务器硬件项目经理3 小时前
第14篇_服务器主板上MOSFET的三大应用
运维·服务器·人工智能·单片机·硬件工程·硬件
SamChan903 小时前
Python批量处理PDF踩坑实录:中文编码、字体内嵌、多栏排版与内存占用
python·单片机·ai·pdf·机器翻译
HUI-4744 小时前
EG1170|SSOP‑10 外置 MOS 200V 异步降压控制器,快充 / 电池充电大电流降压优选
嵌入式硬件·硬件工程
zcmodeltech4 小时前
能源电力沙盘模型控制系统设计——基于STM32与Modbus RTU的“发—输—变—配—用”全链条动态展示方案
数据库·stm32·单片机·嵌入式硬件·能源
高斯的手稿08014 小时前
丽萨单片机固件升级,裁剪hex文件
单片机·嵌入式硬件