面灵AI→

科大讯飞车载嵌入式一面:ARM 特殊寄存器与任务切换

轮次
一面
base
苏州
时间
2026-09
来源
牛客网

《面试题目》

  1. ARM 架构下有哪些特殊寄存器?它们各自的作用是什么(PC、LR 等)?
  2. CAN 使用时都要配置什么?CAN 和 CAN FD 有什么区别?
  3. SRAM 的内存分布是怎样的(数据段、代码段、堆栈)?
  4. FreeRTOS 的任务切换原理是什么?
  5. 串口怎么接收不定长数据?
  6. 你常用的编译工具有哪些?
  7. 自我介绍 + 项目介绍:项目用了哪些通信协议、哪些软硬件,你具体做了什么?

《参考解析》

ARM 的特殊寄存器:分内核看,别混着答

Cortex-M 系列:R0~R12 通用寄存器,R13 是栈指针 SP(处理模式用 MSP、线程模式可用 PSP),R14 是链接寄存器 LR,R15 是程序计数器 PC,再加一个 xPSR 组合状态寄存器(APSR 存条件标志位,IPSR 存当前异常号,EPSR 存 Thumb 状态与 IT 块状态)。另外三个屏蔽寄存器 PRIMASK、FAULTMASK、BASEPRI 决定哪些优先级的中断能被屏蔽,CONTROL 决定用哪个栈、是否特权、FPU 上下文是否激活 —— 这几个是写 RTOS 移植层时天天打交道的。

Cortex-A 系列:同样是 R0~R15,但状态在 CPSR/SPSR 里(模式位 M[4:0]、中断屏蔽 I/F、Thumb 位 T、条件标志),另有协处理器系统寄存器,比如 SCTLR(MMU、Cache 总开关)、TTBR0/TTBR1(页表基址)、VBAR(异常向量基址)、MPIDR(当前核 ID)、TPIDRPRW(每核私有数据指针)。写多核启动代码时 VBAR 和 MPIDR 一定会用到。

问到 PC / LR 时要答出细节:PC 读出的是「当前指令地址 + 流水线偏移」(Thumb 下通常是 +4),LR 在 BL/BLX 时保存返回地址;而在异常入口,硬件会把 LR 写成 EXC_RETURN 魔法值(判断用的是 MSP 还是 PSP、是否带 FPU 上下文就靠它的第 2、4 位),异常返回用 BX LR 触发硬件恢复现场 —— 这正是任务切换能只保存一半寄存器的原因。

CAN 的配置项与 CAN FD 的差异

经典 CAN 初始化要配的东西比想象的多:GPIO 复用与重映射、外设时钟、位时序(BRP 分频、TS1/TS2 段长、SJW 同步跳转宽度,采样点一般落在 75%~87.5%)、波特率(tq × (1 + TS1 + TS2))、工作模式(正常 / 环回自测 / 静默监听 / 睡眠)、验收滤波器(ID + 掩码或列表模式,决定哪些帧进 FIFO)、发送邮箱的优先级与自动重传开关、接收 FIFO 与中断(或回调)、以及总线错误计数 TEC/REC 与错误被动、总线关闭(Bus-Off)的自动恢复策略。硬件之外,两端 120Ω 终端电阻、总线拓扑与线长也要提,不然波形反射会让偶发错误帧变多。

CAN FD 的差别集中在三点:① 数据段速率可变,帧头里的 BRS 位做速率切换,仲裁段仍限 1 Mbps,数据段按 ISO 11898-1:2015 最高 5 Mbps(非 ISO 的早期版本有 8 Mbps);② 单帧数据从 8 字节扩到 64 字节,DLC 用 9~15 编码 12/16/20/24/32/48/64;③ 帧格式与校验加强,新增 FDF、BRS、ESI 位,CRC 从 15 位扩到 17/21 位并加了填充位计数校验,同时取消了远程帧。代价是需要 CAN FD 收发器、对网络环路延迟要求更严,采样点也得重新算。

SRAM 的内存分布:一段一段说清楚

  • .text(代码)、.rodata(只读常量)、中断向量表在 Flash 里;
  • .data:已初始化的全局/静态变量,初值存在 Flash,启动代码在进 main 前从 _sidata 拷贝到 RAM 的 _sdata;
  • .bss:未初始化或初值为 0 的全局/静态变量,启动时从 _sbss 到 _ebss 清零,不占 Flash 空间;
  • 堆:从 __heap_base 向上生长,malloc/free 管理;
  • 栈:从链接脚本给的 _estack 向下生长,处理模式/中断用 MSP,跑 RTOS 时任务用 PSP。

配套要能说出验证手段:arm-none-eabi-size 看 text/data/bss 三段大小,翻 map 文件确认各段实际占用和栈顶地址,用链接脚本自定义段(.ccmram、.ramfunc 把中断服务函数搬到 RAM 跑以降低延迟),以及 noinit 段用来存复位原因、看门狗标志这类掉电不该丢的数据。

FreeRTOS 任务切换原理

时基来自 SysTick,每来一个 tick,xTaskIncrementTick() 判断有没有任务需要唤醒;一旦发现更高优先级任务就绪,就置位 PendSV(SCB->ICSR |= PENDSVSET)。PendSV 被特意设成最低优先级,保证它一定在所有中断退出之后才执行,避免在中断上下文里切栈。

真正干活的是 xPortPendSVHandler:先把当前任务的 R4R11 手工压入它自己的任务栈(R0R3、R12、LR、PC、xPSR 在异常入口已由硬件自动入栈),把 PSP 写回 TCB 的栈顶指针字段;然后调用 vTaskSwitchContext() 从就绪链表里挑出最高优先级任务(内部用 CLZ 指令快速定位最高非空优先级),更新 pxCurrentTCB;最后从新任务的栈里恢复 R4~R11、设置 PSP,异常返回时硬件自动弹出剩下的寄存器,任务就换过去了。第一个任务由 SVC 启动。能顺带说出「为什么用 PendSV 而不是直接在 SysTick 里切」「临界区用 BASEPRI 而不是直接关全局中断」这两点,就比只背流程高一档。

串口接收不定长数据的几种做法

  • DMA + 空闲中断(最常用):DMA 在后台收,USART 的 IDLE 中断在总线空闲一个字符时间后触发,中断里用 总长度 - DMA->NDTR 算出本帧长度,处理完重置 DMA 计数。注意 IDLE 标志要先读 SR 再读 DR 才能清掉,还要处理 ORE 溢出标志。
  • 单字节中断 + 定时器超时:每收到一个字节就重置超时定时器,超过 3.5 个字符时间没有新字节就判定帧结束(Modbus RTU 就是这么定的),适合低速、小数据量。
  • 协议自带边界:帧头帧尾、长度字段或固定长度,靠状态机解析,最可靠但需要自定义协议。
  • 工程上通常再套一层环形缓冲:中断/DMA 只负责往里写,解析任务在外面消费,避免在中断里做重活。

常用编译工具

工具链层面是 arm-none-eabi-gcc/g++(或 Keil MDK 的 armcc / IAR 的 iccarm)、make / CMake / Ninja 组织构建,arm-none-eabi-objdump、objcopy(生成 bin/hex)、size、nm、readelf 看产物,gdb + OpenOCD / J-Link 做在线调试,addr2line 把地址翻回源码行。再加一句构建脚本或 CI 里怎么编排的,回答就完整了。