ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

嵌入式学习day20复盘:从C语言到Linux开发的关键认知与避坑

嵌入式学习day20复盘:从C语言到Linux开发的关键认知与避坑 1. 写在day20这个节点刚好适合停下来复盘一次我给自己定的嵌入式学习计划今天正好走到第20天。说句实话20天放在整个嵌入式知识体系里连入门都算不上但这个时间点特别适合停下来做一次阶段复盘——因为刚好过了最初那种什么都想学、什么都看不明白的混沌期又还没到真正能独立做项目的成熟期认知刚刚开始成形踩过的坑也都还热乎。先说说这20天我在学什么。一开始当然是补C语言基础紧接着是数据结构、计算机组成原理的快速复习然后进入Linux操作系统基础、Shell命令最近这几天开始碰嵌入式Linux应用编程也就是文件IO、进程线程、网络通信这块。整个学习过程中热搜词里那些嵌入式内核源码嵌入式Linux驱动开发嵌入式八股文嵌入式面试题嵌入式开发板网口配置之类的内容我几乎每天都会刷到也从里面筛出了不少真正值得看的资料。这篇复盘我不打算写成普通的学习日志流水账而是想把这20天里最重要的几个认知转变、技术细节和踩坑经历整理出来。尤其是那些面试题背后到底在考什么、超级大循环到事件驱动这个架构升级意味着什么、开发板实操中那些文档里不会写的坑——这些东西如果你也在自学嵌入式大概率会遇到同样的问题。这篇内容适合正在规划嵌入式学习路线的人、刚入行一两年的嵌入式软件工程师以及那些准备转行做嵌入式开发但还没迈出第一步的朋友。2. 学C语言和写嵌入式C是两码事day20这天我才敢这么说如果说这20天里我最大的观念转变是什么那一定是大学里学的C语言和嵌入式开发要用的C语言虽然语法一模一样但思维方式完全是两套东西。2.1 从两道最经典的面试题说起嵌入式面试题里有两道题出现频率极高一道是用宏定义写出求一个数第n位的值另一道是请说明volatile关键字的作用。看起来都是C语言基础题但面试官想知道的东西完全不局限于语法。先看第一道#define GET_BIT(x, n) (((x) (n)) 0x01)这道题考的是位运算。嵌入式开发中寄存器的每一位都可能对应一个硬件开关——某个bit控制GPIO的输出电平某个bit决定串口的波特率分频系数某个bit使能或禁止中断。如果你只会用x % 2或者x / 2的数学思路去处理效率低不说写出来的代码在硬件工程师眼里也完全不合格。再看volatile。在嵌入式里一个变量可能被硬件外设修改比如串口接收寄存器的状态位、可能被中断服务函数修改如中断计数器、可能被多个线程修改。如果编译器把它优化到寄存器里缓存读到的就是旧值。关键词volatile就是告诉编译器这个变量每次都要从内存去读不许给我缓存。这里面还牵出一个经典考点volatile能不能解决线程安全问题答案是能保证可见性但不能保证原子性所以要配合原子操作或互斥锁。2.2 指针、内存与硬件的距离感大学C语言课程里学指针最多就是用int *p a然后*p 10换个地方改个变量值。可到了嵌入式里指针的威力才真正释放出来。比如操作一个GPIO寄存器典型写法是#define GPIOB_BASE 0x40010C00 #define GPIOB_CRL (*(volatile unsigned long *)(GPIOB_BASE 0x00)) #define GPIOB_BSRR (*(volatile unsigned long *)(GPIOB_BASE 0x10))第一眼看到这种地址强转的语法时我是懵的。(volatile unsigned long *)把一个整数地址强转成指针前面的*再对这个地址解引用这样你就能直接往这个内存地址写入数据而这个地址恰好就映射到了GPIO的配置寄存器上。每一个引脚的方向、速率、电平全都靠向这些固定地址读写数据来完成。这就是为什么嵌入式面试必考指针和内存。一个不理解内存布局的人写不出能跟硬件对话的代码。理解这一点之后我再去看那些嵌入式内核源码、启动代码、寄存器映射文件的时候至少不会像看天书一样了。2.3 我学习方法的自我纠正前十天左右我还在用大学里应付考试的方式刷C语言题目看题、写答案、对结果。后来发现这种做法效率极低。嵌入式C语言的标准不是答案对而是编译之后会发生什么。比如const修饰指针的四种组合书上看定义很简单但实际写代码的时候要清楚这个指针指向的数据能不能被改、指针自身能不能被改、硬件寄存器为什么一般不用const修饰反而要用volatile。再比如结构体对齐的问题大学里从没讲过但在嵌入式里这直接关系到内存占用和数据传输。一个结构体如果没安排好字段顺序可能多占好几字节内存在MCU上这可能就是浪费了一小半RAM。我当时实测了一个包含char、int、char三个字段的结构体sizeof结果不是6也不一定是12要看编译器的默认对齐方式——这种跑一跑就知道的学习方式比死记硬背管用太多。我在实际学习中的体会是嵌入式方向学C语言不要满足于在编译器里能跑一定要养成两个习惯一是编译的时候看汇编输出哪怕只看关键几行二是写代码时时刻问自己这个变量在内存里长什么样、会被谁修改。这样坚持下来后面看内核源码、写驱动才不会寸步难行。3. 从超级大循环到事件驱动嵌入式架构升级的分水岭热搜词里有句话我印象很深从超级大循环到事件驱动嵌入式架构升级的分水岭。这个说法非常精准它是每个嵌入式开发者从入门到进阶必须跨过的一道坎。3.1 超级大循环不是错但要清楚它的边界很多嵌入式入门教程的第一课都是点亮LED然后很自然地写出这样的代码while (1) { read_sensor_data(); // 读传感器 process_key_input(); // 处理按键 update_display(); // 刷新显示 send_uart_data(); // 串口发送 }这就是经典的超级大循环Super Loop也叫前后台系统主循环是后台中断是前台。它的优点很明显逻辑简单、执行顺序确定、容易调试特别适合逻辑简单的产品比如一个温湿度计、一个遥控器。但它的缺点同样致命所有任务是顺序执行的任何一个任务阻塞后面所有任务都会卡住。比如read_sensor_data()里如果用了阻塞式I2C读取等待传感器回数据可能要几毫秒那这期间按键就失灵了、显示也不刷新了。如果某个任务的执行时间不稳定整个系统的实时性就是一句空话。3.2 事件驱动与状态机程序结构上的思维转变跨过超级循环这一关就要理解事件驱动的编程思想。核心概念是程序不是在顺序做事而是在等待事件、响应事件。事件可以来自外部中断、定时器溢出、消息队列、网络数据包程序收到事件后再根据当前状态决定做什么反应。一个很经典的例子就是按键消抖。用超级循环实现的话最简单粗暴的方式是检测到按键电平变化后delay(20)再检测这种做法在等待期间整个系统什么都不干非常浪费。但用状态机加定时器实现就完全不同了enum KEY_STATE { KEY_STATE_IDLE, // 空闲 KEY_STATE_WAIT_STABLE, // 消抖等待中 KEY_STATE_PRESSED, // 确认按下 }; void key_scan(void) { static enum KEY_STATE state KEY_STATE_IDLE; uint8_t level read_key_level(); // 读引脚电平 switch (state) { case KEY_STATE_IDLE: if (level KEY_DOWN) { state KEY_STATE_WAIT_STABLE; start_timer(20); // 启动20ms定时器不阻塞 } break; case KEY_STATE_WAIT_STABLE: if (timer_expired() level KEY_DOWN) { state KEY_STATE_PRESSED; notify_key_event(KEY_EVENT_CLICK); } else if (level KEY_UP) { state KEY_STATE_IDLE; } break; case KEY_STATE_PRESSED: if (level KEY_UP) { state KEY_STATE_IDLE; } break; default: break; } }这种写法下系统在等待消抖的20ms里还能干别的活按键事件也能注册回调函数把逻辑解耦得很彻底。这就是从顺序执行到事件驱动最直观的转变。3.3 从裸机到RTOSday20之后要面对的路当项目复杂度再往上走比如需要同时处理多个传感器、通信协议栈、UI刷新、数据存储裸机加定时器轮询的方式就开始捉襟见肘了。这时候就需要引入RTOS实时操作系统比如FreeRTOS、RT-Thread、μC/OS。RTOS的本质是把超级大循环拆成一个个独立任务由内核调度器决定哪个任务先跑、跑多久。任务之间用信号量、消息队列、互斥量通信。学习RTOS的核心不是会用API而是理解任务状态切换、优先级调度、临界区保护这些概念。我在day18的时候尝试着把一个温湿度传感器的例子改成FreeRTOS多任务版本当时那个CAN通信和LCD显示互相干扰的问题光是排查共享资源冲突就花了一个下午——这种坑真的只有踩过才知道。4. 嵌入式Linux绕不开的三板斧文件IO、进程线程与网络我目前的进度正好卡在嵌入式Linux应用开发这一块。不得不说嵌入式Linux和裸机开发完全是两个世界其中最核心的就是文件IO、进程线程和网络编程。4.1 文件IO理解一切皆文件的真正含义Linux下有一条哲学叫一切皆文件按键是文件、LED是文件、串口是文件、网络套接字也是文件。理解这个设计嵌入式开发会豁然开朗。以读取一个GPIO按键为例在嵌入式Linux下可以直接操作sysfsint fd open(/sys/class/gpio/gpio12/value, O_RDONLY); if (fd 0) { perror(open gpio failed); return -1; } char buf[2]; read(fd, buf, sizeof(buf)); close(fd);这里的open/read/close就是最基础的文件IO。但要注意一个关键区别标准C库的fopen/fread自带缓冲写入的数据可能还在内存缓冲里没落到设备上而系统调用open/read/write不带缓冲直接触及内核。在实际开发中带缓冲的scanf表现异常fgets读到的是旧值这类问题八成是缓存没有刷新的问题。用fflush或者改用系统调用就能解决。另外还有个很实用的概念是阻塞与非阻塞。默认情况下read是阻塞的如果设备没有数据进程就会挂在那儿一直等。开发中经常需要设置非阻塞int flags fcntl(fd, F_GETFL); fcntl(fd, F_SETFL, flags | O_NONBLOCK);加上O_NONBLOCK后再read如果没有数据会立即返回-1并设置errno为EAGAIN这样你就可以去干别的事。这背后就是事件驱动架构在应用层的体现。4.2 多进程与多线程嵌入式项目的基本组织方式当应用逻辑复杂之后单线程的阻塞式模型就不够用了。嵌入式Linux应用最常见的两种手段是fork进程和pthread线程。进程间通信IPC的方式非常多管道、FIFO、共享内存、消息队列、信号量、Socket。刚接触可能觉得头大但实际项目里用到的就那么几个。我的经验是优先学信号量和共享内存一个解决互斥同步一个解决大数据量传输效率再学一下Socket因为跨设备通信最后还是落到网络。比如一个常见的嵌入式设备架构主控进程负责采集传感器数据通过共享内存把数据传给UI进程显示同时通过网络模块把数据上报到服务器。两个进程之间还需要一个信号量来防止同时读写同一块内存。这种结构用线程也一样能实现只是要注意线程间共享全局变量时的竞态问题。这里顺便说一个热搜词里的问题嵌入式Linux中如何检查Qt应用程序内存泄露。我之前也在一个小的Qt界面上测试过最简单的方式是加上Qt自带的内存管理机制同时用valgrind的memcheck工具跑一遍数值在持续增长就说明有泄漏。在嵌入式设备上资源紧张内存泄漏是一个绝对不能忽视的问题所以从一开始就要养成习惯。4.3 开发板联网的坑网口配置与WiFi断线重连热搜词里有两组很接地气的问题——飞凌嵌入式开发板打开第二个网口和嵌入式wifi断线重连怎么弄。这两个问题我在实践过程中恰好都遇到过值得单独说说。开发板上的第二个网口首先要看硬件是否真的引出了第二路网卡芯片。然后在内核设备树dts文件里检查对应的以太网控制器节点是否被使能确认PHY芯片的型号和驱动是否匹配。系统起来之后用ifconfig -a看有没有生成eth1接口没有的话多半是设备树没有使能或者PHY的中断/复位引脚没配置对。有了接口之后还要在/etc/network/interfaces里配置IP或者用DHCP然后重启网络服务。WiFi断线重连这个问题更折磨人。嵌入式设备的网络环境往往不稳定一个成熟的产品必须要有自愈能力。最简单的思路是写一个守护进程监控网络状态int check_net_status(void) { int ret system(ping -c 1 -W 2 www.example.com /dev/null 21); return (ret 0) ? 1 : 0; }但这种做法在嵌入式环境有争议因为ping依赖外部网络外网断了并不代表本地局域网故障。更稳妥的做法是直接检查网关或者AP的连通性同时监听wpa_supplicant的事件触发断线事件后尝试重新连接。我自己在V3S板子上跑过每隔5秒检查一次WiFi关联状态如果掉线就重新执行wpa_supplicant的关联流程试下来大概能在2秒内恢复比ping外网方案靠谱得多。5. 调试与工具链20天里踩坑最多的地方恰恰不是写代码说实话这20天里让我最抓狂的不是语法错误也不是算法不会而是程序莫名其妙不工作的排查过程。嵌入式开发的调试和普通软件开发差别太大了。5.1 忘密码与变砖嵌入式Linux的救回之路热搜词里有嵌入式linux忘了密码和嵌入式linux下忘了密码怎么办。这个坑真的典型。我有一次在开发板上改了root用户密码第二天再看笔记发现密码记错了怎么都登不进去当时急得就差重刷系统了。最后找到的办法是通过串口进入U-Boot在U-Boot里传入init/bin/sh的内核启动参数让系统跳过init直接进shell然后mount根文件系统、用passwd重设密码、重启。如果U-Boot也没有留出修改参数的窗口最彻底的办法就是重新烧写整个系统镜像做好备份的话也就几分钟的事。这个经历给我最大的教训是两件事第一开发板的用户名、密码、IP地址这些关键信息要专门记在笔记里别随手写在便签上第二学嵌入式Linux一定要掌握系统启动的完整流程——从U-Boot到内核到根文件系统到init进程每一步能做什么、报错怎么看这比多学几个调试工具都实用。5.2 从print大法到系统性调试刚开始调试程序我是典型的printf党写一个判断就加一行printf程序跑起来满屏输出然后眼巴巴在那翻日志。后来发现这样效率太低了尤其是多线程的问题printf本身还带锁有时候反而掩盖了竞态。系统性调试至少要掌握三个工具gdb断点、单步、查看变量、查看寄存器。嵌入式环境里如果目标板资源太少可以用gdbserver远程调试。strace跟踪系统调用。程序卡住了打不开文件网络连不上strace一下就能看到程序到底停在哪一步系统调用上以及返回的错误码是什么。valgrind查内存泄漏和非法访问。之前测Qt程序内存泄漏就是靠它。举个实际例子有一次我写的程序在开发板上运行几分钟就僵死top看CPU占用正常free看内存也没崩溃。后来用strace挂上去发现程序莫名其妙卡在了一个write系统调用上而且迟迟没有返回。一查才发现串口设备默认是阻塞方式而串口另一头没接设备发送缓冲区满了就卡住。用O_NONBLOCK或者调整串口驱动参数就解决了。如果只用print大法这种问题可能排查一整天都不一定找到根因。5.3 日志规范没有日志的嵌入式程序等于裸奔调试经验告诉我嵌入式程序一定要有规范的日志系统不是随便printf了事。至少要分级别ERROR、WARN、INFO、DEBUG并且支持运行时动态调整级别。这样实际部署时只打印ERROR级别定位问题时再打开DEBUG。我现在的做法是封装一个简单的日志模块支持时间戳、所在文件与行号输出到串口的同时可选写入文件。这样即使程序在客户现场出了问题也能把日志导出来分析。另外日志输出要加锁防止多线程打印乱序日志缓冲区要设上限防止写日志本身把内存或Flash耗尽。这些都是文档里不会写、但实际项目里一定会遇到的细节。6. 嵌入式笔试面试到底在考什么day20反推回来的信息热搜词里嵌入式面试题、八股文、面经出现的频率非常高说明很多正在学和已经入行的人都在关注同样的事情。我在day20这个节点回头看反而对面试有了更清晰的理解。6.1 八股背后的底层逻辑嵌入式面试题无论怎么变考察的核心就那么几块C语言功底、操作系统原理、单片机/ARM体系结构、通信协议UART、I2C、SPI、CAN、Linux基础、项目经验。每一块面试题背后都指向一个真实的工作场景。比如问static关键字的作用看起来是语法题实际是在考你是否理解变量的生命周期和作用域这关系到嵌入式系统内存管理。问进程和线程的区别考的是你能否在多任务环境里选择合适的并发模型。问I2C通信时序考的是你能否看懂芯片数据手册、能否解决实际通信异常问题。所以备考方法不是背题而是用题目反推知识体系。我现在的做法是看到一个面试题先不看答案自己从原理层面推导一遍再去看答案。比如嵌入式cmp指令的判断标志位这种问题看起来是汇编指令但本质是ARM处理器的状态寄存器工作机制理解了CPSR里NZCV标志位怎么根据ALU运算结果变化所有类似题目就都通了。6.2 项目经验怎么准备哪怕是小项目也要有完整闭环面试官最看重的是项目经验而很多自学的人最缺的也是这个。事实上即使是一个小项目只要把它拆透、做透、讲透在面试中就能有说服力。我的建议是从一个完整的感知-处理-输出系统做起。比如做一个基于ESP32或STM32的WiFi环境监测站传感器采集温湿度通过I2C/SPI读取MCU处理后经WiFi上报到MQTT服务器手机/PC端订阅查看数据。这个项目覆盖了GPIO、定时器、中断、通信协议、网络协议栈、RTOS等多个知识点每一块都能展开讲。做项目时一定要把技术细节记录下来比如传感器初始化时序怎么配置的I2C通信出现NACK是怎么排查的功耗优化做了哪些处理数据上报失败重试策略怎么设计的面试官问你遇到的最大困难是什么这些实际记录就是最好的素材。6.3 资料那么多怎么选才不迷路学嵌入式最不缺的就是资料但资料太多反而容易学乱。我搜到过贺老师讲嵌入式尚硅谷嵌入式等比较知名的系列课程也看了不少嵌入式学习路线图。实话说不必追求把所有资料都看完。我的选资料原则是一个主线一套完整系统的视频课程或教材 一份手册芯片数据手册/开发板手册 一套源码Linux内核源码或一个成熟开源项目 一块开发板实验。主线确定后遇到问题查手册、翻源码、上板验证。这样知识是体系化的而不是碎片化的。尤其要重视读源码的习惯哪怕是读一个简单的开源驱动也比看十篇源码解析博文有用。7. day20之后的路线我打算这样接着往下走既然到了复盘节点给自己定一个相对清晰的后续计划还是很有必要的。这里也把方向性思考分享出来。7.1 分岔路单片机方向和Linux方向怎么选嵌入式学习的常见分岔点是继续深入单片机/RTOS裸机FreeRTOS偏向硬件控制还是转嵌入式Linux方向应用开发或者驱动开发。20天学下来我自己更偏向Linux方向因为嵌入式Linux的应用面更宽岗位数量也更多从智能家居、工业控制到边缘计算都有需求。但这并不意味着单片机方向没有前途事实上很多物联网终端设备、汽车电子、小家电还是以MCU为主而且做单片机开发对硬件底层的理解更深转Linux方向也会很顺手。一个比较稳妥的路线是先精通一块MCU的开发GPIO、中断、定时器、UART/SPI/I2C、RTOS再做几个完整项目然后转向嵌入式Linux应用开发再进阶到内核与驱动。这条路线虽然周期长但每一步的根基都很扎实。我目前相当于在MCU基础没完全打牢的情况下就开始碰Linux所以之后的计划是两条线并进一条继续补MCURTOS的基础一条推进Linux应用编程。7.2 用项目反向检验学习成果接下来的学习我不会再以看完某本书看完某个视频为目标而是以做出什么功能为驱动。比如学文件IO就写一个在开发板上读取温湿度传感器并通过串口上报的程序学网络编程就把数据改成经MQTT上报到服务器学多线程就把数据处理和网络发送拆成两个线程用消息队列通信。检验标准很简单程序在开发板上跑起来数据显示正确断电重启后功能正常日志清晰可查。达到这个标准才算真正理解了这个知识点。7.3 一个可落地的20天学习清单如果一定要给一个可参考的后续清单我目前给自己定的方向是这样的第1-5天精通一块MCU的常用外设重点是中断和定时器做一个按键中断控制LED的小实验第6-10天学习FreeRTOS的任务创建、信号量、消息队列把上一周的小实验改成多任务版本第11-15天嵌入式Linux应用编程文件IO、多线程、网络编程在开发板上完成一个完整的小项目第16-20天实践一个综合性小项目比如WiFi环境监测节点并整理成项目文档为面试做准备当然计划赶不上变化碰到卡壳的时候一个知识点磨上一周也正常不用为了赶进度而牺牲深度。说实话20天对于嵌入式这个领域来说真的太短了。但我越来越觉得学习嵌入式的关键不在于比别人多看了几本书、多敲了几行代码而在于能不能在每一个阶段停下来想清楚我现在学到的东西在真实的产品里是怎么用的我能不能把一个小功能从原理到实现完整地讲给别人听现在回头看当初选嵌入式这条路确实是因为它足够硬核每往前走一步都能看到实实在在的东西在跑。希望我的这份day20复盘能给你的嵌入式学习之路提供一点参考。以上内容就是我在第20天最真实的记录。
返回列表