字节嵌软四场技术面面经
岗位:嵌入式软件开发岗和嵌入式影像开发岗-移动os
四场技术面基本都包含了八股问答+项目宏观问题和技术细节深挖+easy/mid难度手撕(三面无手撕)
一面 45min:
1.实习+项目具体内容了解+追问
2.以上经历包含的具体技术问题提问:
1.以I2C/SPI驱动为例,硬件产生中断到CPU执行中断服务程序的完整流程,Linux内核在其中扮演什么角色?
完整流程(通用硬件流程,单片机 / Linux 通用)
-
外设硬件触发事件I2C 收到数据 / 发送完成、SPI 收发完毕,外设内部置中断标志位,向外输出中断请求信号。
-
中断信号送入中断控制器中断信号传给中断控制器(STM32 是 NVIC,Linux 是 GIC/APIC)。
-
中断控制器校验判断中断使能是否开启、中断优先级、中断掩码,符合条件则向 CPU 发起硬件中断请求。
-
CPU 响应中断CPU 结束当前正在执行指令,保存当前程序计数器 PC、寄存器现场,关闭总中断。
-
查询中断向量表CPU 根据中断号,查找中断向量表,获取对应中断服务程序入口地址。
-
跳转到中断服务函数执行进入 IRQ 中断服务程序:
- 清除外设中断标志
- 读取 I2C/SPI 数据、简单收发
- 仅做快速处理,不做耗时业务
- 中断退出恢复 CPU 之前保存的寄存器现场,打开总中断,回到被打断的代码继续运行。
(中断掩码 = 单独屏蔽某个中断,让它暂时失效)
Linux 内核在里面扮演的角色(重点考点)
-
统一管理硬件中断内核初始化时初始化中断控制器,分配中断号、设置中断优先级、注册中断触发方式(上升沿 / 电平)。
-
注册中断服务函数驱动层调用内核接口
request_irq(),把I2C/SPI 外设中断和自己写的中断处理函数绑定。 -
中断分层处理(核心)
- 顶半部(硬中断):快速执行,只做寄存器读写、清中断、拷贝数据,耗时极短
- 底半部(软中断 / 工作队列 /tasklet):把耗时逻辑延后执行,不阻塞其他硬中断,保证系统实时性
-
中断调度与优先级管控内核统一调度所有设备中断,防止某一个外设中断长期占用 CPU,均衡硬件资源。
-
封装硬件差异屏蔽不同平台、不同芯片 I2C/SPI 寄存器差异,给驱动提供统一标准中断调用接口。
-
中断休眠与唤醒外设中断可唤醒休眠 CPU,内核负责电源管理 + 中断唤醒机制。
-
中断异常处理内核捕获中断死循环、中断超时、非法中断,防止系统卡死崩溃
2.介绍中断上半部和下半部
1.上半部(顶半部)
1. 执行时机
中断立刻触发,马上执行,处于中断上下文
2. 工作内容(只做最快最简事)
- 读取硬件寄存器、清中断标志
- 简单拷贝数据、标记事件
- 快速响应硬件,毫秒 / 微秒级完成
3. 严格限制
- 不能休眠、不能阻塞、不能调用睡眠函数
- 不能占用 CPU 太久
- 禁止复杂运算、IO、延时
4. 实现方式
request_irq() 注册的中断处理函数就是上半部
2.下半部(底半部)
1. 执行时机
上半部干完立刻退出中断,延后空闲时再执行,不属于严格中断上下文
2. 工作内容(处理耗时业务)
- 数据解析、协议解析(I2C/SPI 报文解析)
- 数据上报、日志、内存处理、业务逻辑
- 可休眠、可等待、可调度
3. 优势
不抢占硬中断,保证系统响应速度
4. 四种常用实现方式(优先级从高到低)
- 软中断 softirq:内核高频底层用,速度最快
- tasklet:基于软中断封装,驱动最常用
- 工作队列 workqueue:可休眠,最常用、最稳
- 线程化中断 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.静态链接
编译时把库代码直接打包进可执行文件
优点
- 运行不依赖外部库,在哪都能跑
- 启动速度快,不用加载库
- 无版本兼容问题
缺点
- 程序体积非常大
- 多个程序用同库,重复占内存,浪费空间
- 库更新必须重新编译整个程序
项目涉及技术问题
运行时才加载外部库文件 (.so/.dll)
优点
- 可执行文件体积小
- 共享库,多个程序共用一份,节省内存
- 升级方便:只换库文件,不用重编译程序
缺点
- 运行必须携带对应库,缺库直接跑不了
- 启动稍慢,需要加载链接库
- 存在库版本不兼容问题
10.I2C 和 SPI 的物理层特性
1.I2C 物理层
- 引脚:仅 2 根信号线
- SCL:串行时钟线
- SDA:串行数据线
- 电平:开漏输出,必须外接上拉电阻,默认高电平
- 通信方式:半双工,一根线分时收发
- 拓扑:总线型,一主多从,靠设备 7/10 位地址区分
- 速率:常规 100K、400K,最高几兆
- 硬件:无片选,靠地址寻址
- 距离:短距离低速通信
2.SPI 物理层
- 引脚:最少 4 根
- SCLK:时钟
- MOSI:主机发从机收
- MISO:从机发主机收
- CS/SS:片选
- 电平:推挽输出,无需上拉,驱动能力强
- 通信方式:全双工,收发可同时进行
- 拓扑:一主多从,靠片选引脚选中设备
- 速率:速率极高,几十兆很常见
- 硬件:无设备地址,靠片选区分从设备
- 距离:板内高速近距离通信
11.以 I2C 驱动为例,读取寄存器的流程是?
- 主机发送 START 信号
- 发送 设备地址 + 写方向(0)
- 从机应答 ACK
- 发送 要读取的寄存器地址
- 从机应答 ACK
- 主机发送 重复 START 信号(重启)
- 发送 设备地址 + 读方向(1)
- 从机应答 ACK
- 从机发送 寄存器数据
- 主机应答 NACK(告诉从机:读完了)
- 主机发送 STOP 信号
12.如何确认寄存器的数据已写完且写对?
最常用标准做法:回读校验
- 按照 I2C 正常写寄存器流程,把数据写入目标寄存器
- 写完之后,立刻调用读寄存器函数,把这个寄存器的值读出来
- 拿读出的值 和 刚才写入的值 做对比
- 相等 = 写入成功且正确;不相等 = 写入失败
13.全双工和半双工通信的本质区别是什么?
全双工收发可同时进行,半双工同一时刻只能单向传输数据。
14.SPI 驱动高速外设(如 SD 卡、camera)时出现数据传输错误,可能的原因有哪些?
- SPI 时钟速率超出外设承受范围
- CPOL、CPHA 时序模式配置错误
- 片选 CS 控制时序不规范
- 布线干扰、走线过长导致信号失真
- IO 驱动能力不足、电平不匹配
- 系统中断抢占打乱传输时序
- 外设供电不稳、缺少延时等待
15.二维矩阵原地翻转,追问若扩大为一图像,4000×4000 庞大数据后如何优化?如何实现多线程异步处理?
1. 原地顺时针旋转 90° 核心思路(逆时针将顺序反转)
- 先矩阵转置
- 再逐行逆序,全程不开新数组,原地修改
// 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. 内存优化
- 使用堆内存动态分配,拒绝栈存储大数组
- 采用行优先连续内存排布,减少寻址开销
- 内存池预分配,避免频繁 new/free
- 大图像采用内存映射 mmap,直接操作磁盘缓存,不一次性载入内存
3. 访存顺序优化
遵循空间局部性,优先遍历相邻地址数据,避免跳跃访问。
4. 数据类型压缩
图像像素用uint8_t/uint16_t,不用 int,压缩内存占用。
异步架构流程
- 主线程:读取图像、划分任务块、创建线程池
- 工作线程:领取分块任务,独立完成块内翻转 / 旋转
- 线程同步:用条件变量 / 信号量等待所有子线程完成
- 异步非阻塞:主线程派发任务后可执行其他业务,不阻塞
精简实现思路(伪代码)
- 初始化固定数量线程池
- 计算均分任务区间
- 批量投递分块翻转任务
- 主线程异步等待全部任务完成
- 任务结束回调保存图像
二面
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. 保证线程安全常用手段
- 互斥锁 mutex同一时间只允许一个线程访问共享资源,串行执行。
- 原子操作不可拆分指令,执行中途不会被打断,轻量高效。
- 读写锁读多写少场景,多读并行,写独占,提升效率。
- 条件变量线程等待、唤醒,实现线程有序同步。
- 无共享设计尽量使用局部变量,不共用数据,从根源避免冲突。
5.除了加锁,还有其他解决线程资源竞争的方法吗(同上)
6.I2C 和 SPI 的区别
7.I2C 和 SPI 的时序有哪些不同?举具体例子说明。
I2C 核心时序
- 起始信号SCL 高电平期间,SDA 由高拉低
- 停止信号SCL 高电平期间,SDA 由低拉高
- 数据采样规则SCL 高电平期间读取 SDA 数据SCL 低电平时允许 SDA 电平翻转
- 应答时序主机发完一字节,释放 SDA,从机拉低 SDA 产生 ACK
- 读寄存器固定时序起始→发设备写地址→发寄存器地址→重复起始→发设备读地址→读数据→停止
SPI 核心时序
SPI 靠 CPOL 时钟极性 + CPHA 时钟相位 组合决定时序
- CPOL=0:空闲时钟低电平
- CPOL=1:空闲时钟高电平
- CPHA=0:第一个时钟沿采样数据
- CPHA=1:第二个时钟沿采样数据
- 空闲状态无通信时,时钟电平由 CPOL 决定
- 片选优先通信前先拉低 CS,结束拉高 CS
- 数据收发SCLK 跳变沿同时收发 MOSI、MISO 数据
- 无起始停止信号没有 I2C 那种专用起始、停止时序
8.I2C 通信不通,可能的原因有
查接线→查上拉电阻→查 IO 配置→核对设备地址→降低时钟频率→抓取波形看起始 / 应答信号
后续还有很长时间针对项目具体内容的深挖...
二选一:① 求单向链表的倒数第 K 个节点;② 实现均值滤波。
注意链表的创建和释放
① 求单向链表的倒数第 K 个节点(快慢指针)
② 实现均值滤波()
更多推荐
所有评论(0)