岗位:嵌入式软件开发岗和嵌入式影像开发岗-移动os
   
四场技术面基本都包含了八股问答+项目宏观问题和技术细节深挖+easy/mid难度手撕(三面无手撕)
    
一面 45min:


1.实习+项目具体内容了解+追问


2.以上经历包含的具体技术问题提问:


1.以I2C/SPI驱动为例,硬件产生中断到CPU执行中断服务程序的完整流程,Linux内核在其中扮演什么角色?

完整流程(通用硬件流程,单片机 / Linux 通用)

  1. 外设硬件触发事件I2C 收到数据 / 发送完成、SPI 收发完毕,外设内部置中断标志位,向外输出中断请求信号。

  2. 中断信号送入中断控制器中断信号传给中断控制器(STM32 是 NVIC,Linux 是 GIC/APIC)。

  3. 中断控制器校验判断中断使能是否开启、中断优先级、中断掩码,符合条件则向 CPU 发起硬件中断请求。

  4. CPU 响应中断CPU 结束当前正在执行指令,保存当前程序计数器 PC、寄存器现场,关闭总中断。

  5. 查询中断向量表CPU 根据中断号,查找中断向量表,获取对应中断服务程序入口地址。

  6. 跳转到中断服务函数执行进入 IRQ 中断服务程序:

  • 清除外设中断标志
  • 读取 I2C/SPI 数据、简单收发
  • 仅做快速处理,不做耗时业务
  1. 中断退出恢复 CPU 之前保存的寄存器现场,打开总中断,回到被打断的代码继续运行。

(中断掩码 = 单独屏蔽某个中断,让它暂时失效)

Linux 内核在里面扮演的角色(重点考点)

  1. 统一管理硬件中断内核初始化时初始化中断控制器,分配中断号、设置中断优先级、注册中断触发方式(上升沿 / 电平)。

  2. 注册中断服务函数驱动层调用内核接口 request_irq(),把I2C/SPI 外设中断和自己写的中断处理函数绑定。

  3. 中断分层处理(核心)

  • 顶半部(硬中断):快速执行,只做寄存器读写、清中断、拷贝数据,耗时极短
  • 底半部(软中断 / 工作队列 /tasklet):把耗时逻辑延后执行,不阻塞其他硬中断,保证系统实时性
  1. 中断调度与优先级管控内核统一调度所有设备中断,防止某一个外设中断长期占用 CPU,均衡硬件资源。

  2. 封装硬件差异屏蔽不同平台、不同芯片 I2C/SPI 寄存器差异,给驱动提供统一标准中断调用接口。

  3. 中断休眠与唤醒外设中断可唤醒休眠 CPU,内核负责电源管理 + 中断唤醒机制。

  4. 中断异常处理内核捕获中断死循环、中断超时、非法中断,防止系统卡死崩溃

2.介绍中断上半部和下半部

1.上半部(顶半部)

1. 执行时机

中断立刻触发,马上执行,处于中断上下文

2. 工作内容(只做最快最简事)

  1. 读取硬件寄存器、清中断标志
  2. 简单拷贝数据、标记事件
  3. 快速响应硬件,毫秒 / 微秒级完成

3. 严格限制

  • 不能休眠、不能阻塞、不能调用睡眠函数
  • 不能占用 CPU 太久
  • 禁止复杂运算、IO、延时

4. 实现方式

request_irq() 注册的中断处理函数就是上半部

2.下半部(底半部)

1. 执行时机

上半部干完立刻退出中断,延后空闲时再执行,不属于严格中断上下文

2. 工作内容(处理耗时业务)

  1. 数据解析、协议解析(I2C/SPI 报文解析)
  2. 数据上报、日志、内存处理、业务逻辑
  3. 可休眠、可等待、可调度

3. 优势

不抢占硬中断,保证系统响应速度

4. 四种常用实现方式(优先级从高到低)

  1. 软中断 softirq:内核高频底层用,速度最快
  2. tasklet:基于软中断封装,驱动最常用
  3. 工作队列 workqueue:可休眠,最常用、最稳
  4. 线程化中断 irq thread:内核线程执行,兼容性最好

  • 上半部:禁止睡眠、禁止阻塞、禁止调度
  • 下半部只有 工作队列、irq 线程 能阻塞休眠,tasklet 不行


3.I2C/SPI使用DMA机制吗?

  • SPI 通信速率高、数据连续传输场景多,普遍配合 DMA 实现高速无 CPU 搬运收发。
  • I2C 时序复杂、存在应答位与起始停止信号,虽硬件支持 DMA,但工程中极少使用,多采用中断或轮询方式。
  • DMA 作用:减轻 CPU 负担,数据由外设直接与内存互相搬运。
  • 轮询:CPU 主动不断查询外设状态实现收发,简单低效,占用 CPU 资源高。
  • 中断:外设完成收发后触发中断通知 CPU,CPU 平时可执行其他任务,效率适中,频繁数据易造成中断拥挤。
  • DMA:无需 CPU 介入,由 DMA 控制器完成外设与内存间数据搬运,CPU 仅负责初始化与收尾,大数据高速传输最优。
  • I2C:只用轮询 / 中断,基本不用 DMA
  • UART 串口:少量用中断,大量必用 DMA
  • SPI:高速大批量优先 DMA
  • ADC 采集:连续采样必开 DMA

4.I2C设备没有应答,你会从哪些方面排查问题?

  • 测 SDA/SCL 电平,空闲必须高电平
  • 降低 I2C 波特率测试
  • 核对从机设备地址
  • 检查上拉电阻
  • 复位 I2C 总线解除死锁
  • 改用模拟 I2C 测试,排除硬件外设故障
  • 更换从机芯片排除硬件损坏

5.用过逻辑分析仪吗?主要用来做什么?

  • 示波器:看模拟电压、波形幅度、噪声
  • 逻辑分析仪:只看数字高低电平、时序逻辑、总线协议

主要抓取数字通信时序波形,解析 I2C、SPI、UART 等总线数据,排查时序错误、通信无应答、数据丢包等软硬件问题,快速定位协议与引脚时序故障。


6.C语言中volatile关键字的作用,一般用在什么场景?

  • 告诉编译器:该变量值随时可能被外部改变,不要优化
  • 每次使用强制重读内存,不使用缓存值
  • 保证变量实时性
  • 寄存器地址(单片机 / 嵌入式最常用),外设寄存器、IO 口、定时器、中断寄存器值随时变,必须加 volatile
  • 中断服务函数里用到的全局变量,主函数轮询判断中断标记,不加会被优化死循环
  • 多线程 / 多进程共享全局变量,防止线程间读取到旧缓存数据
  • 内存映射硬件地址

7.Linux和RTOS的差异,从多个维度对比。

Linux 与 RTOS 多维度对比(面试满分版)

1. 核心定位

  • RTOS:实时操作系统,优先保证任务响应时延,硬实时 / 软实时,专为嵌入式控制设计
  • Linux:通用分时操作系统,优先均衡调度、功能完备,默认非实时,可打补丁实现软实时

2. 实时性

  • RTOS:硬实时,中断 / 任务响应时间微秒级,时延确定、可预判
  • Linux:原生非实时,调度时延波动大;加装PREEMPT-RT补丁为软实时,毫秒级,时延不确定

3. 调度机制

  • RTOS:抢占式优先级调度,高优先级任务随时抢占低优先级,调度简单
  • Linux:CFS 完全公平调度为主,兼顾优先级,侧重公平分配 CPU 资源

4. 资源占用

  • RTOS:极小,内核几 KB~ 几十 KB,低配 MCU / 单片机就能跑
  • Linux:体积大,需 MB 级内存、大容量 Flash,必须ARM 多核 / 高端主控

5. 功能生态

  • RTOS:精简,只含任务、信号量、队列、互斥锁、基础驱动,生态弱
  • Linux:完备网络、文件系统、GUI、协议栈、开源软件海量,生态极强

6. 开发难度

  • RTOS:轻量简单,上手快,侧重底层控制逻辑
  • Linux:架构复杂,驱动、内核、应用分层,学习成本高

7. 外设驱动

  • RTOS:精简驱动,自行编写适配多,适配快
  • Linux:海量成熟通用驱动,即插即用,定制修改麻烦

8. 适用场景

RTOS 适用

工业控制、机器人运动控制、电机调速、无人机飞控、仪器仪表、智能家居硬件、自动驾驶底层执行层要求:定时精准、快速响应、低资源

Linux 适用

网关、工控屏、边缘计算、音视频处理、网络设备、上位机、APP 交互、数据上传、AI 视觉要求:联网、界面、大数据、复杂应用

9. 稳定性与功耗

  • RTOS:架构简单,死机少,功耗极低,适合电池设备
  • Linux:进程多易卡顿,后台进程多,功耗偏高

10. 任务通信方式

  • RTOS:信号量、消息队列、事件组、互斥量、任务通知
  • Linux:管道、socket、信号、共享内存、消息队列、进程间通信


8.结构体内存大小计算(32位系统)
  结构体指针地址偏移计算
  大小端模式判断代码实现

1.32位系统跟64位系统

  • long:
    • 32 位:4 字节
    • 64 位:8 字节
  • 指针 *:
    • 32 位:4 字节
    • 64 位:8 字节

2.32位系统跟64位系统区别

  • 32 位:地址 4 字节,指针全 4 字节
  • 64 位:地址 8 字节,指针全 8 字节

 3.大端小端

  0x12345678

   地址从低到高:

  • 大端:12 34 56 78
  • 小端:78 56 34 12

4.32 位系统 结构体内存大小计算(4字节对齐)

#include <stdio.h>

struct Test
{
    char ch;
    int num;
    short sh;
};

int main(void)
{
    printf("结构体大小:%zu\n", sizeof(struct Test));
    return 0;
}
  • ch:偏移 0,占 1 字节,补 3 字节空洞
  • num:偏移 4,占 4 字节
  • sh:偏移 8,占 2 字节,末尾补 2 字节对齐 4总大小 = 12

(代码里面为啥放struct test,因为他是类型,int a是变量)

2.结构体成员地址偏移 代码实现

#include <stdio.h>
#include <stddef.h>   // 必须头文件

struct Test
{
    char ch;
    int num;
    short sh;
};

int main(void)
{
    // 1. 库函数直接获取偏移量(面试常用)
    printf("ch偏移:%zu\n", offsetof(struct Test, ch));
    printf("num偏移:%zu\n", offsetof(struct Test, num));
    printf("sh偏移:%zu\n", offsetof(struct Test, sh));

    // 2. 手动指针计算偏移
    struct Test obj;
    char *base = (char *)&obj;
    size_t off_num = (char *)&obj.num - base;
    printf("手动计算num偏移:%zu\n", off_num);

    return 0;
}

3.大小端判断 标准笔试代码

#include <stdio.h>

// 共用体共享同一块内存
union Endian
{
    unsigned int data;
    unsigned char buf[4];
};

// 返回1小端,0大端
int check_endian(void)
{
    union Endian u;
    u.data = 0x12345678;
    // 低地址存低字节0x78 = 小端
    return u.buf[0] == 0x78;
}

int main(void)
{
    if(check_endian())
        printf("小端模式\n");
    else
        printf("大端模式\n");
    return 0;
}


转岗影像方向后的一面 40min:


1.Linux 操作系统与嵌入式实时操作系统(如 FreeRTOS)有哪些区别?(前面已回答)


2.用户态调用 malloc 分配内存,从用户态到内核态的实现链路或流程是什么?

  • 应用调用malloc,优先使用用户态空闲堆内存
  • 内存不足时,glibc 调用brk/mmap系统调用
  • 通过异常指令陷入内核,完成用户态切内核态
  • 内核分配物理内存、建立页表虚拟映射
  • 系统调用返回,切回用户态
  • 库函数二次管理内存,最终返回用户程序可用地址

3.内核态分配的内存,用户态如何使用?

内核内存默认用户态不可访问,权限隔离;建立虚拟地址映射,把内核内存映射到用户进程虚拟地址空间。


4.malloc 分配的内存是在堆上还是栈上?

malloc 开辟堆内存,指针放栈,内容放堆,必须手动 free


5.C++ 中函数传值有几种方式?

  • 值传递:拷贝副本,不改原值
  • 指针传递:传地址,改原值
  • 引用传递:起别名,改原值,C++ 首选

指针传递

// 函数定义
void swap(int *x, int *y)
{
    int t = *x;
    *x = *y;
    *y = t;
}

// 调用
int a=1,b=2;
swap(&a, &b);   // 传变量地址

引用传递

// 函数定义
void swap(int &x, int &y)
{
    int t = x;
    x = y;
    y = t;
}

// 调用
int a=1,b=2;
swap(a,b);  // 调用写法和传值一样,内部直接改原值


6.const int* p和 int *const p有什么区别?

1. const int *p

常量指针 → 指向的值不能改,指针本身能改

2. int *const p

指针常量 → 指针地址不能改,指向的值能改

7.什么是内存泄露?

堆内存申请后没有手动释放,程序丢失访问地址,造成内存一直占用无法回收,就是内存泄漏。


8.内存泄露会导致什么后果?

内存越用越少,程序卡顿变慢,最终内存耗尽崩溃死机。


9.静态链接和动态链接分别有什么优缺点?

1.静态链接

编译时把库代码直接打包进可执行文件

优点

  1. 运行不依赖外部库,在哪都能跑
  2. 启动速度快,不用加载库
  3. 无版本兼容问题

缺点

  1. 程序体积非常大
  2. 多个程序用同库,重复占内存,浪费空间
  3. 库更新必须重新编译整个程序

项目涉及技术问题

运行时才加载外部库文件 (.so/.dll)

优点

  1. 可执行文件体积小
  2. 共享库,多个程序共用一份,节省内存
  3. 升级方便:只换库文件,不用重编译程序

缺点

  1. 运行必须携带对应库,缺库直接跑不了
  2. 启动稍慢,需要加载链接库
  3. 存在库版本不兼容问题

10.I2C 和 SPI 的物理层特性

1.I2C 物理层

  1. 引脚:仅 2 根信号线
    • SCL:串行时钟线
    • SDA:串行数据线
  2. 电平:开漏输出,必须外接上拉电阻,默认高电平
  3. 通信方式:半双工,一根线分时收发
  4. 拓扑:总线型,一主多从,靠设备 7/10 位地址区分
  5. 速率:常规 100K、400K,最高几兆
  6. 硬件:无片选,靠地址寻址
  7. 距离:短距离低速通信

2.SPI 物理层

  1. 引脚:最少 4 根
    • SCLK:时钟
    • MOSI:主机发从机收
    • MISO:从机发主机收
    • CS/SS:片选
  2. 电平:推挽输出,无需上拉,驱动能力强
  3. 通信方式:全双工,收发可同时进行
  4. 拓扑:一主多从,靠片选引脚选中设备
  5. 速率:速率极高,几十兆很常见
  6. 硬件:无设备地址,靠片选区分从设备
  7. 距离:板内高速近距离通信

11.以 I2C 驱动为例,读取寄存器的流程是?

  • 主机发送 START 信号
  • 发送 设备地址 + 写方向(0)
    • 从机应答 ACK
  • 发送 要读取的寄存器地址
    • 从机应答 ACK
  • 主机发送 重复 START 信号(重启)
  • 发送 设备地址 + 读方向(1)
    • 从机应答 ACK
  • 从机发送 寄存器数据
    • 主机应答 NACK(告诉从机:读完了)
  • 主机发送 STOP 信号


12.如何确认寄存器的数据已写完且写对?

最常用标准做法:回读校验

  1. 按照 I2C 正常写寄存器流程,把数据写入目标寄存器
  2. 写完之后,立刻调用读寄存器函数,把这个寄存器的值读出来
  3. 拿读出的值 和 刚才写入的值 做对比
  4. 相等 = 写入成功且正确;不相等 = 写入失败

13.全双工和半双工通信的本质区别是什么?

全双工收发可同时进行,半双工同一时刻只能单向传输数据。


14.SPI 驱动高速外设(如 SD 卡、camera)时出现数据传输错误,可能的原因有哪些?

  • SPI 时钟速率超出外设承受范围
  • CPOL、CPHA 时序模式配置错误
  • 片选 CS 控制时序不规范
  • 布线干扰、走线过长导致信号失真
  • IO 驱动能力不足、电平不匹配
  • 系统中断抢占打乱传输时序
  • 外设供电不稳、缺少延时等待

15.二维矩阵原地翻转,追问若扩大为一图像,4000×4000 庞大数据后如何优化?如何实现多线程异步处理?
 

1. 原地顺时针旋转 90° 核心思路(逆时针将顺序反转)

  1. 先矩阵转置
  2. 再逐行逆序,全程不开新数组,原地修改
// n*n方阵原地旋转
void rotate(vector<vector<int>>& mat) {
    int n = mat.size();
    // 1.转置
    for(int i=0;i<n;i++)
        for(int j=i+1;j<n;j++)
            swap(mat[i][j],mat[j][i]);
    // 2.每行反转
    for(int i=0;i<n;i++)
        reverse(mat[i].begin(),mat[i].end());
}

1.分块处理(核心优化)

  • 不一次性操作整张 4000*4000 图像
  • 划分为64×64 / 128×128 小块逐块翻转旋转
  • 符合 CPU高速缓存行机制,大幅提升读写速度

2. 内存优化

  1. 使用堆内存动态分配,拒绝栈存储大数组
  2. 采用行优先连续内存排布,减少寻址开销
  3. 内存池预分配,避免频繁 new/free
  4. 大图像采用内存映射 mmap,直接操作磁盘缓存,不一次性载入内存

3. 访存顺序优化

遵循空间局部性,优先遍历相邻地址数据,避免跳跃访问。

4. 数据类型压缩

图像像素用uint8_t/uint16_t,不用 int,压缩内存占用。

异步架构流程

  1. 主线程:读取图像、划分任务块、创建线程池
  2. 工作线程:领取分块任务,独立完成块内翻转 / 旋转
  3. 线程同步:用条件变量 / 信号量等待所有子线程完成
  4. 异步非阻塞:主线程派发任务后可执行其他业务,不阻塞

精简实现思路(伪代码)

  1. 初始化固定数量线程池
  2. 计算均分任务区间
  3. 批量投递分块翻转任务
  4. 主线程异步等待全部任务完成
  5. 任务结束回调保存图像


二面


1.new/delete 与 malloc/free 的区别

new/delete 是 C++ 专属,会自动调用构造 / 析构、自动算大小、返回类型指针、失败抛异常;malloc/free 是库函数,只做内存分配,需手动算大小、强转、返回 NULL,不调用构造析构。


2.new 一个指针对象后,用另一个指针指向该对象,再用 delete 释放这个指针,会有什么问题?

  • 多个指针指向同一块堆内存,任意一个 delete 都会直接释放内存
  • 释放后所有指向该内存的指针全部失效
  • C++ 没有引用计数,不会自动判断还有没有指针在用
  • 会变成野指针

3.哈希表的原理、解决哈希冲突的方法

通过哈希函数,把关键字 key映射成数组下标,直接定位存储位置,实现近似 O (1) 增删改查。

什么是哈希冲突

不同 key 经过哈希函数,算出同一个数组下标,位置重叠,就是冲突。

1. 开放定址法(最常用)

冲突就往后找空位存放

  • 线性探测:往后逐个找空位置
  • 二次探测:跳跃式查找,减少扎堆缺点:容易堆积聚集、删除麻烦

2. 链地址法(拉链法,Java HashMap 用这个)

数组每个位置挂一条链表

  • 下标冲突,直接追加到链表尾部
  • 查询先找下标,再遍历链表优点:实现简单、冲突处理高效、适合大数据


4.对线程安全的理解。

1. 什么是线程安全

多线程同时访问共享资源,程序运行结果和单线程执行结果一致,不会出现数据错乱、脏数据、异常崩溃,就是线程安全。

2. 不安全原因

多个线程同时读写同一全局变量 / 静态变量 / 堆共享数据,抢占资源、指令交错,导致数据出错。

3. 保证线程安全常用手段

  1. 互斥锁 mutex同一时间只允许一个线程访问共享资源,串行执行。
  2. 原子操作不可拆分指令,执行中途不会被打断,轻量高效。
  3. 读写锁读多写少场景,多读并行,写独占,提升效率。
  4. 条件变量线程等待、唤醒,实现线程有序同步。
  5. 无共享设计尽量使用局部变量,不共用数据,从根源避免冲突。

5.除了加锁,还有其他解决线程资源竞争的方法吗(同上)


6.I2C 和 SPI 的区别


7.I2C 和 SPI 的时序有哪些不同?举具体例子说明。

I2C 核心时序

  1. 起始信号SCL 高电平期间,SDA 由高拉低
  2. 停止信号SCL 高电平期间,SDA 由低拉高
  3. 数据采样规则SCL 高电平期间读取 SDA 数据SCL 低电平时允许 SDA 电平翻转
  4. 应答时序主机发完一字节,释放 SDA,从机拉低 SDA 产生 ACK
  5. 读寄存器固定时序起始→发设备写地址→发寄存器地址→重复起始→发设备读地址→读数据→停止

SPI 核心时序

SPI 靠 CPOL 时钟极性 + CPHA 时钟相位 组合决定时序

  • CPOL=0:空闲时钟低电平
  • CPOL=1:空闲时钟高电平
  • CPHA=0:第一个时钟沿采样数据
  • CPHA=1:第二个时钟沿采样数据
  1. 空闲状态无通信时,时钟电平由 CPOL 决定
  2. 片选优先通信前先拉低 CS,结束拉高 CS
  3. 数据收发SCLK 跳变沿同时收发 MOSI、MISO 数据
  4. 无起始停止信号没有 I2C 那种专用起始、停止时序


8.I2C 通信不通,可能的原因有

查接线→查上拉电阻→查 IO 配置→核对设备地址→降低时钟频率→抓取波形看起始 / 应答信号


后续还有很长时间针对项目具体内容的深挖...


二选一:① 求单向链表的倒数第 K 个节点;② 实现均值滤波。
注意链表的创建和释放
    
① 求单向链表的倒数第 K 个节点(快慢指针)

② 实现均值滤波()

Logo

北京人形旗下天工造物具身智能开源社区,聚焦具身天工与慧思开物两大平台

更多推荐