尧图建网站 尧图建网站 YAOTU WEB BUILD 免费咨询
ARTICLE DETAIL

资讯详情

深耕网站建设与建站编程的一线实战洞察。

网易嵌入式软件工程师笔试复盘:考点拆解与备考策略

网易嵌入式软件工程师笔试复盘:考点拆解与备考策略 2020年网易校招正式批的嵌入式软件工程师笔试我是在九月底的一个晚上做的。当时投的是杭州研究院的嵌入式岗位笔试链接发到邮箱限时两小时一共四类题单选、多选、简答、编程。说实话做之前我刷过不少面经但真正坐到屏幕前还是有一些没想到的坎儿。这篇文章就把我复盘这套笔试的全过程写出来包括考点拆解、做题顺序、踩坑细节以及笔试之后怎么衔接技术面。无论你是准备嵌入式校招还是打算转行嵌入式方向我相信这篇复盘都能给你一个比较清晰的坐标系。1. 先摸清这场笔试的脾气题型结构与考察逻辑网易的嵌入式软件工程师笔试和互联网后台开发岗的笔试有本质区别。后台岗位更偏算法、系统设计而嵌入式岗位的笔试题几乎每一道都在暗示一件事你要真正理解硬件和软件之间的边界。整场笔试给我的感觉是它不是在考你能背多少八股文而是考你面对一个资源受限、实时性敏感的嵌入式系统时能不能做出正确判断。从我复盘的角度看这套笔试的题型大致分成四块单选题、多选题、简答题和编程题。单选题大概十道左右覆盖C语言基础、操作系统概念、数据结构基础多选题是最容易翻车的地方因为嵌入式领域很多结论都是在特定场景下成立选项往往设计成看起来都合理简答题一般两到三道通常围绕某个具体场景展开比如内存优化、中断处理或者低功耗设计编程题一般两道一道偏数据结构和算法一道偏嵌入式场景实现。这里最容易被忽略的是多选题。很多同学笔试前狂刷算法题结果死在多选上。为什么因为嵌入式笔试的多选题非常强调边界条件。举个例子问以下哪些方式可以降低嵌入式系统的功耗选项可能有降低CPU主频、使用DMA传输、增加定时器中断频率、进入低功耗模式。看起来A、B、D都对但如果题目要求结合某个具体场景来选择情况就完全不一样。所以做多选时我会先看题目有没有在XX条件下这种限定语有的话每个选项都要拿这个限定条件去过滤一遍。笔试的整体难度并不是每道题都难而是时间紧、范围宽。两小时看起来不少但如果你在选择题上卡太久后面的编程题大概率写不完。我的策略是单选控制在15分钟内多选控制在20分钟内简答控制在30分钟内给编程题留出至少50分钟。这个时间分配在后面我会详细展开。还有一点嵌入式笔试的编程题有一个和纯软件岗不太一样的考察点它非常在意你写的代码是否考虑硬件约束。比如内存够不够、能不能用递归、栈深度是否可控。如果你在代码里写一个无限制的递归调用哪怕逻辑是对的面试官也大概率会给你打低分因为这在嵌入式环境里是不合格的工程实现。2. C语言考点位运算、指针与内存布局是重灾区C语言在嵌入式笔试里的地位不用多说基本上占整张试卷的30%以上。复盘下来最常考的方向有三个位运算、指针与数组、内存管理。这三个方向不是孤立的它们本质上都在考察同一个能力你能否精确控制数据在内存中的样子。2.1 位运算题嵌入式工程师的基本功位运算基本是必考的考法非常直接。比如给定一个寄存器地址让你实现置位、清零、翻转某些位。这类题看起来简单但很多人在如何避免影响其他位这个细节上翻车。正确的做法是先按位取反掩码再与运算或者用移位构造掩码。#define REG_ADDR 0x40021000 /* 将第5位置1其他位不变 */ *(volatile uint32_t *)REG_ADDR | (1u 5); /* 将第3位清零其他位不变 */ *(volatile uint32_t *)REG_ADDR ~(1u 3); /* 翻转第7位 */ *(volatile uint32_t *)REG_ADDR ^ (1u 7);注意这里有个关键的细节访问寄存器必须用volatile修饰的指针。volatile告诉编译器这个值可能被硬件修改不能做优化缓存。如果笔试题目里要求写寄存器操作你忘了volatile这道题基本就丢了一半分。我在实际笔试时专门在代码注释里写了一句防止编译器优化这样即使代码有细微瑕疵面试官也能看到你懂这个点。更进阶一些的位运算题包括统计一个整数中1的个数、判断一个数是否是2的幂、交换两个变量的值而不使用临时变量。这些题在力扣上都有原题但嵌入式的考法往往披着一层业务外衣比如判断某个事件标志位是否被置位用于中断处理函数中事件的位图管理。2.2 指针、数组和结构体对齐指针和数组的题属于不出意外必考的类型而且考得很细。我印象比较深的一类是sizeof相关的题很多人会在这里栽跟头。void func(char str[100]) { printf(%zu\n, sizeof(str)); /* 输出多少 */ } int main() { char str[100] {0}; char *p str; printf(%zu %zu\n, sizeof(str), sizeof(p)); /* 100 和 864位系统 */ func(str); /* 8因为数组作为函数参数退化为指针 */ return 0; }这类题考察的核心是数组名什么时候退化为指针。数组作为函数参数传递时长度信息丢失这在嵌入式场景下非常致命。比如你写了一个驱动函数想通过参数传入一个缓冲区如果依赖sizeof在函数内部获取缓冲区长度调试的时候会得到完全莫名其妙的结果。结构体对齐也是高频考点而且它直接关系到嵌入式两个核心问题内存布局和通信协议解析。比如下面这个结构体struct msg { uint8_t id; uint32_t len; uint8_t data[5]; };在默认对齐规则下sizeof(struct msg)不是10而是16。因为len要从4字节对齐的地址开始存储id后面会填充3个字节。如果这个结构体是用来解析串口数据帧的你在上位机和下位机之间直接发送结构体两边编译器的对齐规则不一致数据就会错位。这也是为什么嵌入式通信协议里我更推荐用__packed或者#pragma pack(1)强制紧凑对齐再配合字段偏移量宏来访问。2.3 内存管理static、const、volatile的陷阱内存管理这块笔试通常不会直接让你写malloc而是通过一系列概念题考察你是否理解嵌入式内存分区。堆、栈、全局区、代码段、常量区每一块区域的生命周期和用途要分清楚。我复盘时整理过一张表笔试前对照着过了三遍内存区域存放内容生命周期典型风险栈局部变量、函数参数函数执行期间溢出、栈深度不可控堆malloc动态分配手动释放前碎片化、泄漏全局/静态区全局变量、static变量整个程序周期多线程竞争常量区字符串常量、const数据整个程序周期写操作崩溃代码段编译后的指令整个程序周期几乎不涉及static在嵌入式里有很多用法修饰局部变量可以延长生命周期修饰全局变量可以限制作用域修饰函数可以避免外部链接。笔试常考的是static局部变量初始化一次这个特性它其实在嵌入式状态机里非常常用。比如一个按键扫描函数用static变量保存上一次的按键状态就能实现边沿检测而不是电平检测。const和volatile同时修饰一个变量的情况也考过问常量指针和指针常量有什么区别或者为什么中断服务函数里要避免使用非volatile变量。其实答案很简单const表示程序不能修改volatile表示编译器不能优化。两者不冲突。一个extern const变量如果同时被中断和主循环访问就需要加volatile否则主循环里可能因为编译器优化而永远读到一个旧值。3. 操作系统与嵌入式基础从进程调度到中断上下文操作系统基础是笔试的第二大块但考察方式和纯软件岗不同。嵌入式笔试几乎不考LRU缓存实现这类算法而是围绕中断、任务调度、资源竞争这些实时系统概念展开。这里我会把这一块的常考点拆开说。3.1 中断与轮询实时系统的两条路有一类简答题我印象很深列举中断方式和轮询方式处理外部事件的优缺点并说明在什么场景下选择哪种方式。这道题其实是在考你是否理解实时性和CPU开销的权衡。轮询的优点是实现简单代码调试方便适合事件发生频率低、间隔相对规律的场景缺点是CPU需要不断查询状态浪费资源且事件响应延迟取决于查询间隔。中断的优点是响应实时CPU在无事件时可以处理其他任务或进入低功耗缺点是引入优先级、嵌套、共享资源保护等复杂性如果中断处理函数耗时过长会影响系统整体实时性。在嵌入式笔试的简答题里这种对比类题型非常常见。答题时不能只罗列优缺点一定要有场景。比如一个每秒触发一次的温湿度传感器使用轮询即可满足需求一个检测机械故障的急停按钮必须使用中断。有场景支撑的答案和纯背书的答案分数差距是很明显的。3.2 任务调度与优先级反转RTOS相关的考点主要集中在任务状态、调度策略、同步机制。常见考题如优先级反转是什么如何解决优先级反转是指高优先级任务被低优先级任务间接阻塞的现象。经典场景是三个任务A高优先级、B中优先级、C低优先级A和C共享一个互斥锁C持有锁后被B抢占导致A等待C释放锁但C又被B抢占A永远排不上。解决方案有三个层面优先级继承、优先级天花板、禁止中断。笔试里如果问你FreeRTOS中如何开启互斥量而不是二值信号量其实就是在考察优先级继承机制。我自己的经验是这种题要能从问题的本质是共享资源访问出发而不仅仅是背出结论。调度算法方面常见考点是抢占式调度与协作式调度的区别时间片轮转适用于什么场景。嵌入式笔试很喜欢问FreeRTOS、uC/OS这类实时内核的任务状态切换比如任务从运行态到阻塞态可能发生了什么系统调用。这类题的关键是画出状态图就绪、运行、阻塞三种状态的转换条件。虽然标题里禁止用流程图但你在纸上推导写答案时用文字描述逻辑要能闭环。3.3 ARM架构与启动流程嵌入式笔试不可能绕开ARM。常见考点包括Cortex-M3/M4的寄存器组、异常向量表、栈指针初始化、启动文件的作用。有一类填空题或者选择题会问复位后CPU首先执行什么操作答案是从向量表取出复位向量设置SP和PC然后执行启动代码最后调用main。有些同学觉得ARM架构离实际开发很远但其实不是。比如你调试一个程序跑飞的问题如果不知道异常向量表里HardFault_Handler的地址在哪里可能连断点都不知道怎么设。我建议笔试前至少把这三件事弄清楚处理器处于什么模式时响应中断哪个寄存器保存返回地址中断返回指令是哪个。Cortex-M3上用的是EXC_RETURN机制和传统ARM的LR保存方式略有不同很多笔试题就喜欢在这里设陷阱。启动流程的考点还包括栈和堆的大小在哪里配置、启动文件里为什么要先初始化系统时钟、__main和main的区别。一般来说链接脚本会指定内存布局启动代码负责把RW段从Flash拷贝到RAM、把ZI段清零。这些都是笔试可以出简答题的知识点而且直接关系到你能不能理解Flash和RAM都分配了哪些内容。3.4 通信协议UART、I2C、SPI通信协议在笔试中占的比例不低但考得不算深主要是概念和计算。UART部分最常考波特率计算比如已知系统时钟频率和分频系数计算实际波特率误差。I2C部分常考时序和地址格式起始条件、停止条件、7位地址和读写位怎么组成第一个字节。SPI部分常考四种模式即CPOL和CPHA的组合以及主从设备之间MOSI、MISO、SCLK、CS四根线的连接。我抽一个例子说说SPI模式。很多同学记不住四种模式其实只需要记住CPOL决定空闲时时钟电平CPHA决定数据在哪个边沿采样。CPOL0表示空闲低电平CPOL1表示空闲高电平CPHA0表示第一个边沿采样CPHA1表示第二个边沿采样。笔试如果给你一张时序图让你判断是模式几按这个规则去对就能推出来。UART波特率计算也是容易失分的地方因为题目往往不直接给你完整参数。比如外部晶振8MHzPLL倍频到72MHzAPB1总线时钟36MHzUSART2挂载在APB1上要产生115200bps波特率分频系数是多少这种题需要你一条条往下算途中任何一步把总线时钟记错结果就偏了。我的建议是做题时把每一步的时钟值写在草稿纸上不要心算。4. 数据结构与手写代码链表和字符串是编程题的常客编程题部分复盘下来基本集中在链表、字符串、数组和位操作。嵌入式岗位不会考太偏的图论或动态规划但链表题几乎年年有。原因很简单嵌入式里大量数据结构都是用链表实现的比如内核的任务队列、缓冲区管理。而且链表题非常考验代码基本功和边界处理能力。4.1 单链表反转基础但最能看出代码功底单链表反转这道题我笔试前一天还在写结果笔试的时候还真碰上了。它是个经典题但能考察的点很多你是否能处理空链表和只有一个节点的边界情况是否理解头插法是否能把代码写得干净。struct list_node { struct list_node *next; int data; }; struct list_node *reverse_list(struct list_node *head) { struct list_node *prev NULL; struct list_node *cur head; while (cur) { struct list_node *next cur-next; cur-next prev; prev cur; cur next; } return prev; }这个写法大家应该都见过但笔试里真正拉开差距的往往不是能不能写出来而是写得是否严谨。我在代码里用了局部指针next来保存下一个节点避免修改cur-next之后丢失后续节点。另外题目如果要求递归实现你要小心递归反转对长链表不友好有栈溢出风险而嵌入式环境本身对栈深度很敏感。我就在代码注释里说明嵌入式环境优先选择迭代实现这个细节面试官会看到。4.2 环形缓冲区非常典型的嵌入式编程题除了链表环形缓冲区ring buffer也是嵌入式笔试编程题的常客因为它直接对应用层和中断之间的数据传递场景。比如串口中断里收数据主循环里处理数据如果不加锁就必须用环形缓冲区来协调生产者和消费者。#define BUF_SIZE 64 typedef struct { uint8_t buffer[BUF_SIZE]; uint16_t head; /* 写入位置 */ uint16_t tail; /* 读取位置 */ uint16_t count; /* 当前数据量 */ } ring_buf_t; void ring_buf_init(ring_buf_t *rb) { rb-head 0; rb-tail 0; rb-count 0; } int ring_buf_write(ring_buf_t *rb, uint8_t data) { if (rb-count BUF_SIZE) { return -1; /* 缓冲区满 */ } rb-buffer[rb-head] data; rb-head (rb-head 1) % BUF_SIZE; rb-count; return 0; } int ring_buf_read(ring_buf_t *rb, uint8_t *data) { if (rb-count 0) { return -1; /* 缓冲区空 */ } *data rb-buffer[rb-tail]; rb-tail (rb-tail 1) % BUF_SIZE; rb-count--; return 0; }这道题隐藏的两个考察点第一当缓冲区大小为2的幂时取模运算可以用位运算 (BUF_SIZE - 1)替代性能更高第二写操作和读操作可能在单核嵌入式系统中被中断打断所以这类代码通常需要配合关中断或原子操作。如果笔试时间充裕我会在代码里补充一句本实现用于单生产者和单消费者场景在Cortex-M上可通过关中断保护临界区这说明你不仅有数据结构的功底还懂嵌入式代码的落地约束。4.3 字符串和数组题注意复杂度与边界除了链表和环形缓冲区字符串类题目也是编程题的备选项。比如反转字符串中的单词顺序找出字符串中最长不重复子串实现一个简易的atoi函数。这些题在力扣上都很经典但笔试题会被改造成嵌入式场景。比如解析一段从GPS模块接收到的NMEA语句提取经纬度信息本质上就是字符串分割和数值转换。做这类题时我会特别注意两点一是不要轻易引入动态内存分配笔试环境如果限制了malloc用固定大小的临时数组是稳妥选择二是所有字符串处理都要考虑输入非法的情况。比如实现atoi的时候空字符串、正负号、溢出、中间包含非法字符每一种情况都应该在代码里体现。int simple_atoi(const char *str) { if (str NULL) { return 0; } int result 0; int sign 1; while (*str ) { str; } if (*str - || *str ) { sign (*str -) ? -1 : 1; str; } while (*str 0 *str 9) { result result * 10 (*str - 0); str; /* 简单的溢出保护 */ if (result 1000000) { break; } } return result * sign; }这类代码不追求精美但一定要稳。笔试的编程题没有交互调试机会代码是否考虑边界条件、是否有明显漏洞阅卷人一眼就能看出来。我习惯在写完主逻辑后再花一分钟在草稿纸上过一遍NULL指针、空字符串、超大数值、负数每个分支都走一遍。5. 备赛节奏与考场策略从刷题到时间的分配很多同学准备嵌入式笔试时的问题不是不努力而是努力的方向太偏。有人花大量时间刷hard级别的算法题结果笔试里出现的是结构体对齐和UART波特率计算有人只背面试题结果发现笔试编程题要求手写完整代码写了一半就卡住。这里我分享一下我自己当时的准备节奏和考场上的具体操作。5.1 笔试前一个月搭建知识框架我的备考思路是先建框架再填细节。第一周把C语言相关的高频知识点过一遍重点是位运算、指针、内存管理、结构体对齐、static/const/volatile。第二周集中看操作系统和嵌入式基础包括进程线程区别、中断处理、RTOS调度、ARM架构基础、常用通信协议。第三周开始刷编程题重点刷链表、字符串、数组、二叉树这些基础数据结构以及环形缓冲区、寄存器位操作这类嵌入式专属题。第四周做模拟笔试严格限时并且直接在编辑器里手写代码不依赖IDE的自动补全。这里要提醒一点如果你平时写代码依赖IDE的自动补全和编译检查笔试前一定要刻意练习盲写代码。因为是线上笔试虽然环境可能带有基础编辑器但没有编译运行调试的完整闭环。我练习的方法是找一个普通文本编辑器把字体调到舒服的大小然后在里面手写完整的函数写完后再复制到本地IDE里去编译看哪里错了。这个习惯帮我提前暴露了不少低级错误比如变量名拼错、分号漏掉、头文件没包含。5.2 考场上的答题顺序先抢稳的分再啃硬骨头整场笔试两小时我的做题顺序是这样的先把单选题做完因为单选大部分是概念题做起来快接着做简答题因为简答题分值高而且只要你能把逻辑写清楚就有基础分然后做多选题最后集中精力写编程题。为什么把多选放在简答后面因为多选题的迷惑性最强很容易在纠结中消耗大量时间。简答题相反哪怕你只记得一部分知识点也可以按自己的理解把原理说清楚至少能拿到过程分。编程题放在最后是因为它需要完整的思路和安静的时间块如果前面拖太久编程题就废了。编程题如果两道都不会我建议先挑看起来能写出部分代码的那道把框架搭起来能拿一分是一分。千万不要在一道题上死磕写了一半又换题最后的代码往往是残缺的。5.3 遇到不会的题怎么办心态和取舍笔试过程中大概率会遇到一两道完全没思路的题。我的原则是不空着能写多少写多少。选择题可以用排除法多选题如果不能确定只选有把握的选项宁可少选不要错选因为多选部分错选通常不得分。简答题哪怕不会也要把相关概念写在上面比如题目问如何优化嵌入式系统的启动时间你可以从硬件初始化、裁剪驱动、延迟外设初始化这几个角度去答即使不全面也能展示思考方向。编程题遇到完全没思路的就回到最朴素的解法。比如一道统计两个二进制表示中有多少位不同的题最朴素的做法是逐位比较时间复杂度O(n)虽然不够惊艳但至少正确。笔试中完成比最优更重要。5.4 时间分配的具体参照我复盘时给自己列过一个时间预算表笔试时基本是按这个节奏走的题型建议用时注意事项单选题15分钟概念题为主快速判断多选题20分钟警惕特定条件下的选项简答题30分钟分点作答逻辑清晰编程题第一题25分钟先写框架再补边界编程题第二题25分钟留5分钟检查检查5分钟看有没有漏题、低级笔误这个时间表不是死的。如果简答题里有一道完全没思路我会在5分钟内写下所有能联想到的知识点然后果断进入编程题。宁可简答题答得薄一点也不能让编程题空着。6. 笔试之后的复盘把一套题变成一份知识地图笔试结束不是终点尤其在网易这种大厂的校招流程里笔试只是第一关后面还有技术面和HR面。考完当天趁着记忆还新鲜我会立刻做复盘。不是简单对答案而是把整套题目背后的知识点整理成一张查漏补缺的清单。6.1 如何做有效的笔试复盘我的复盘分三步走。第一步回忆并记录每道题涉及的知识点和我的答题状态比如这道题考了结构体对齐我做对了这道题考FreeRTOS优先级反转我答得比较模糊。第二步把模糊的知识点整理成一份专题笔记每个专题配一个最小示例比如优先级反转就用三个任务加一个互斥锁的伪代码来说明。第三步重新做一遍当时没做出来的题如果编程题当时没写完现在就把它完整写一遍并且主动思考如何优化。复盘的价值不在于把题背下来而在于建立考点-知识点-实践场景的映射。比如笔试考了UART波特率计算你就应该顺带把USART的整个时钟树捋一遍笔试考了结构体对齐你就应该打开手头的一个通信协议头文件看看实际工程里是怎么定义数据帧结构的。这样一套笔试复盘下来收获比闷头刷一周题还大。6.2 笔试中暴露的短板如何层层补强通过复盘你会发现自己的短板往往集中在两到三类。我当时的问题集中在ARM架构细节和Linux驱动基础。针对ARM架构我用Cortex-M3参考手册把异常向量表、中断使能流程、任务切换时的寄存器保存过程完整看了一遍。针对Linux驱动我尝试写了一个最简单的字符设备驱动包括open、read、write、release四个接口然后在开发板上加载测试。这里我想特别说一句笔试中如果暴露了Linux驱动的知识盲区不要慌。嵌入式软件工程师面试中驱动基础确实会被问到但通常不会要求你写出完整的platform驱动。更常见的是问你字符设备驱动框架怎么搭设备树的作用是什么ioremap是干什么的。所以笔试后补Linux驱动重点是搭框架和理解流程而不是钻进驱动的细节不可自拔。6.3 笔试答案如何作为面试素材很多人忽略了一点笔试里的编程题和简答题就是你面试时的最佳素材。面试官手里有你的笔试记录他很可能直接拿你笔试写的代码来追问。我当时笔试写了一个环形缓冲区面试时面试官就问如果你的缓冲区大小是64head和tail都是uint8_t会不会有问题这道题其实是在考察无符号整数回绕64是2的6次方uint8_t足够表示0到255当head从63跳到0时用 (BUF_SIZE - 1)是可以正确回绕的但如果缓冲区大小改成100就需要用取模或者把head和tail类型改成uint16_t。所以笔试完不要立刻把题目抛到脑后。每道编程题都再想想如果面试官让你优化复杂度你会怎么做如果面试官把这个数据结构放到多线程场景你要怎么加锁如果面试官要求你禁用动态内存分配你的实现还能不能工作这些追问才是笔试真正想筛选出的能力不是刷题能力而是工程判断力。6.4 从笔试到技术面的衔接节奏网易的校招流程一般是笔试通过后进入技术面。技术面通常会围绕简历上的项目展开但笔试的考点也会被拉出来做延伸。比如简历里写了RTOS项目面试官可能会让你画任务状态转换图简历里写了串口通信协议面试官可能会让你现场设计一个带CRC校验的通信帧格式。这些能力和笔试的考点高度重合只是在面试里考察得更深入、更贴近真实工程。我当时笔试结束后用了大概两三天时间做复盘然后立刻把精力切换到项目梳理上。因为笔试已经把自己知识体系上的漏洞暴露得差不多了接下来的时间更适合打磨项目深度你在项目里到底承担了什么角色遇到过什么难啃的问题解决思路是什么有没有具体的性能数据面试官更想看到的是一个能独立解决问题的人而不是一个会背知识点的考生。回头看这场2020网易嵌入式软件工程师的笔试它其实像一个过滤器不只看你学了多少更看你在有限时间里如何组织知识、分配精力、取舍难题。这种能力在真实的嵌入式开发中一样重要——毕竟产品交付的deadline不会因为某个问题难就不找上门。如果你也准备投嵌入式岗位我的建议是别怕笔试范围广把C语言基础、操作系统概念、通信协议这些主干知识学扎实再配上足量的手写代码练习这套题其实没那么吓人。最后再分享一个我自己受益很深的习惯每次笔试完当晚就把回忆版题目整理成文档标注清楚哪些是会但答错了哪些是完全不会。坚持到第三场笔试你会发现自己的知识漏洞列表越来越短答题的信心也会越来越稳。
返回列表