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

一面(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,中途打断提问

相关推荐
木白CPP3 小时前
SELinux 介绍和基本使用
嵌入式硬件·安卓·安全架构
女神下凡4 小时前
芯参谋(30):EMMC eMCP 软件设计规范
arm开发·单片机·嵌入式硬件·设计规范
xingzhemengyou16 小时前
STM32F427将函数设定在固定位置
stm32·单片机·嵌入式硬件
SKH.6 小时前
ARM-uart
arm开发·单片机
@MMiL7 小时前
为什么-200 mod 4000 等于3800
单片机
殷忆枫10 小时前
基于ESP32 的局域网全双工对讲系统
单片机·嵌入式硬件·esp32·对讲系统
szxinmai主板定制专家11 小时前
工业视觉新思路:FPGA图像预处理+RK NPU推理,实现高速缺陷检测
人工智能·嵌入式硬件·fpga开发·rk3576+codesys
硅基手札11 小时前
ROS2 全面解析 - 从架构到源码
单片机·架构·机器人
Molesidy14 小时前
【Embedded Development】【高级MCU篇】基于野火STM32H750XBH6_Pro开发板的高级MCU开发学习之1点亮led
stm32·单片机·学习
FPGA小徐14 小时前
【硬件实战】立创 EDA 绘制 TF/SD 卡模块电路|SPI 模式原理图 + 阻抗匹配 + PCB 布局布线全教程
单片机·嵌入式硬件·fpga开发