嵌入式开发这条路我走了不少年头,从最早拿着一块51单片机开发板对着寄存器手册发懵,到后来啃ARM裸机、上FreeRTOS、再转到Linux应用和驱动,最后碰边缘AI推理部署,回头再看那些动辄几百集的“全家桶”教程,说实话心情挺复杂的。这类超长合集最大的问题从来不是内容不够,而是信息密度被摊薄了——200集里真正卡住你的可能就20个知识点,但你要花几十个小时从里面捞出来。自学嵌入式开发真正需要的不是“看完”,而是先有一张清晰的地图,知道每个阶段该抓什么、可以暂时放过什么、哪些坑踩一次就够了。这篇东西写给几类人:电子/自动化专业想补软件的同学、计算机专业想下沉到硬件的同学、做上层应用想转底层的工程师,以及纯零基础想进这行的转行者。内容偏实操、偏路线、偏踩坑记录,不追求大而全,只求你别把时间浪费在无效的学习循环里。
1. 自学嵌入式的路线图,先想清楚再动手
1.1 为什么“集数越多”反而越容易迷路
我见过太多人收藏了几百集的课程,硬盘里躺着好几个版本的开发板资料,结果三个月过去还在第一章打转。根子在于嵌入式这门技术本身就是树状结构的:底层是电路与芯片手册,中间是C语言和寄存器操作,上层是操作系统、驱动、应用、算法。线性播放的课程把树状知识拉成了一条直线,你跟着看的时候感觉每一步都懂了,但一旦要自己做一个东西,脑子里根本串不起来。我的建议是反过来——先看整棵树的形状,再挑主干深入。具体的做法是拿一张纸,把“硬件—裸机—RTOS—Linux系统—驱动—应用—AI”这条链画出来,每个节点写三件你现在能说清楚的事和三件说不清的事,说不清的就是你下一阶段要攻的点。这么做还有个好处:你会发现自己其实不需要每条支线都走完,做工业控制的可能一辈子用不上深度学习部署,做智能摄像头的可以把RTOS跳过直接上Linux。
另一个现实问题是知识保鲜期。嵌入式底层的东西十年二十年变化不大,寄存器操作、中断模型、I2C/SPI时序这些是稳定的;但工具链、构建系统、AI部署框架每年都在变。所以你在规划路线时要把时间分配做个区分:底层原理值得慢下来啃透,上层工具用“够用就跑”的心态快速过。我自己的习惯是底层内容花七成时间做笔记和实验,上层工具三成时间动手跑通即可,用到了再回头查文档,别指望一次记住所有编译选项。
1.2 一条能走通的主线:从MCU到Linux再到边缘AI
给你一条我自己验证过、也带过几个朋友走通的路线,分成四个阶段,每个阶段都有明确的产出物。产出物非常重要,它逼着你把零散知识组装成能跑的东西,面试时也是硬通货。
| 阶段 | 核心目标 | 建议产出的项目 | 大致耗时 |
|---|---|---|---|
| 基础期 | C语言 + 单片机外设 | 带OLED显示和按键交互的环境监测小设备 | 2到3个月 |
| 进阶期 | Linux应用 + 工具链 | 多线程网络数据采集程序,跑在开发板上 | 2到3个月 |
| 驱动期 | 内核模块 + 设备树 | 自己写的一个I2C传感器字符设备驱动 | 3到4个月 |
| 拓展期 | 边缘AI部署 | 量化后的图像分类模型在板端跑通推理 | 2到3个月 |
这个时间表是按每天投入两到三小时算的,全职学可以压缩,但别指望一个月通关。基础期选STM32或者国产的GD32、APM32都行,关键是别在选芯片上纠结太久,会点灯、会串口、会跑通一个完整项目比什么都重要。进阶期一定要上真实的Linux开发板,用QEMU模拟也能学一部分,但遇到驱动和硬件交互就走不下去了。
1.3 不同背景的人,起点真的不一样
纯转行的朋友最大的误区是想把模电数电从头学一遍。没必要,至少不是第一优先级。你需要的电路知识是“能看懂开发板原理图里GPIO接了个上拉电阻”“知道三极管当开关用”“明白电压不匹配要加电平转换”,这些边做边补完全来得及。真该花时间的反而是C语言和调试能力。
计算机专业的同学软件基础好,但普遍对硬件有畏难情绪,一看到时序图就头大。我的建议是从“会用的黑盒”开始,先用现成的库把传感器跑起来,建立信心,再回头拆库看寄存器怎么写。硬件逻辑其实比很多人想的简单,大部分外设就是“配置寄存器—等标志位—读写数据”这三步。
电子专业的朋友通常硬件没问题,卡在软件工程能力上:不会用Git、写不出规范的Makefile、代码全塞在一个main.c里。这类人需要补的是工程化习惯,先把代码分层(驱动层、业务层、应用层分开),再学会用版本管理,进步会很快。
2. 打地基:C语言和单片机这块别偷懒
2.1 C语言要学到什么颗粒度才算够
嵌入式里的C和你在培训班学的C不是一回事。语法层面大家都会,真正拉开差距的是对内存和编译行为的理解。下面这些点你必须能讲清楚:指针和数组名的区别到底在哪、函数指针怎么用来做回调、结构体内存对齐怎么算、volatile在什么场景必须加、const修饰指针的三种位置分别是什么意思、static修饰全局变量和局部变量的效果差异、宏定义和inline函数的取舍。
我给你一个真实场景:一个环形缓冲区,用来在中断和主循环之间传递串口数据。这是嵌入式最经典的代码结构之一,写不出来基本可以判定C语言没过关。
typedef struct { uint8_t buf[256]; volatile uint16_t head; volatile uint16_t tail; } ring_t; static inline uint16_t ring_next(uint16_t idx) { return (idx + 1) & 0xFF; // 256 是 2 的幂,用位与代替取模 } int ring_put(ring_t *r, uint8_t data) { uint16_t next = ring_next(r->head); if (next == r->tail) return -1; // 满了 r->buf[r->head] = data; r->head = next; return 0; } int ring_get(ring_t *r, uint8_t *out) { if (r->head == r->tail) return -1; // 空了 *out = r->buf[r->tail]; r->tail = ring_next(r->tail); return 0; }这段代码里每个细节都有讲究。head和tail加volatile是因为它们会被中断服务程序修改,编译器不能把它们缓存到寄存器里。用2的幂次做缓冲长度是为了把取模运算换成位与,这在没有硬件除法器的MCU上能省不少周期。判断“满”用的是牺牲一个元素的做法,避免head和tail相等时满空无法区分。这些经验正规教材里往往一句话带过,但你实际写代码时全是坑。
注意:不要用“先把C语言学完再学单片机”这种思路。C语言是工具,脱离使用场景学不透。正确的节奏是学到指针和结构体就开始点灯,边做边补。
2.2 从点亮一颗LED开始建立硬件直觉
点灯这个梗被玩烂了,但它确实是建立硬件直觉的最佳起点。你要在这件小事里搞明白几件事:GPIO有哪几种输出模式(推挽、开漏)、上拉下拉电阻是干什么的、为什么有的LED是高电平点亮有的是低电平点亮、限流电阻怎么算。计算过程其实简单,假设LED正向压降2V,工作电流10mA,供电3.3V,那么限流电阻就是(3.3-2)/0.01=130欧姆,取标准值150欧姆或220欧姆都行,电流小一点只是暗一些,别超就行。
接下来按顺序攻这些外设:GPIO → 外部中断 → 定时器 → PWM → UART → I2C → SPI → ADC。每攻一个都做一个小实验,UART就做个串口命令行,I2C就驱动一块OLED,SPI就接个Flash读写。这中间有个非常重要的习惯:直接从芯片参考手册里查寄存器,而不是永远用厂商的库函数。
// 以某国产MCU为例,直接操作寄存器点亮LED #define RCC_APB2ENR (*(volatile uint32_t *)0x40021018) #define GPIOB_CRL (*(volatile uint32_t *)0x40010C00) #define GPIOB_ODR (*(volatile uint32_t *)0x40010C0C) void led_init(void) { RCC_APB2ENR |= (1 << 3); // 使能 GPIOB 时钟 GPIOB_CRL &= ~(0xF << 0); // 清 PB0 配置位 GPIOB_CRL |= (0x3 << 0); // PB0 推挽输出 50MHz } void led_on(void) { GPIOB_ODR &= ~(1 << 0); } // 低电平点亮 void led_off(void) { GPIOB_ODR |= (1 << 0); }新手在这段代码上最常犯的错误是忘记使能外设时钟。芯片为了省电,所有外设默认时钟关闭,你不开时钟,寄存器写进去也是石沉大海,现象就是“代码看起来没错但灯就是不亮”。这个坑几乎每个人都踩过,记住一句话:操作任何外设之前先查时钟树,确认总线时钟开了没。
2.3 什么时候该从裸机转到RTOS
裸机写个前后台架构(主循环加中断)能应付大多数中小项目,但一旦出现这些信号就该考虑上RTOS了:任务之间有明确的优先级差异、需要阻塞式等待某个事件、多个任务共享资源需要保护、代码里开始出现大段的状态机。FreeRTOS是最适合入门的,代码量小、文档全、移植方便。
学RTOS别一上来就啃源码,先把它当黑盒用:会创建任务、会用队列传数据、会用信号量做互斥、会用事件组等条件。用熟了再去看调度器怎么切换上下文、优先级怎么继承、内存怎么分配。移植到具体芯片上时,重点看port.c里那几处汇编,理解栈帧保存和恢复的过程,这一块通了,你对“程序在芯片上怎么跑”的理解会上一个台阶。
3. 进阶:Linux环境、工具链和开发工具
3.1 Linux命令和交叉编译工具链必须练到肌肉记忆
从MCU跨到Linux,第一道门槛就是命令行。你至少要对这些操作形成条件反射:文件查找用find配合grep,文本处理用awk和sed,进程查看用ps和top,日志看dmesg,内核模块操作用lsmod、insmod、rmmod,挂载用mount,打包解包用tar,远程登录开发板用ssh。这些不是背下来的,是每天用出来的。我建议你把自己的主力电脑直接换成Linux发行版用几个月,或者至少在虚拟机里把日常开发搬过去,逼自己脱离图形界面。
交叉编译是另一个必须跨过的坎。核心概念就一句话:在x86的电脑上编译出能在ARM开发板上运行的二进制。工具链名字里带着目标架构信息,比如32位ARM硬浮点就是arm-linux-gnueabihf-gcc,64位就是aarch64-linux-gnu-gcc。
# 交叉编译一个简单的应用 arm-linux-gnueabihf-gcc -o hello hello.c \ --sysroot=/opt/toolchain/sysroot \ -I./include -L./lib -lpthread # 查看生成的二进制目标架构,确认没编错 file hello # 输出应类似: ELF 32-bit LSB executable, ARM, EABI5 ... # 拷贝到开发板并运行 scp hello root@192.168.1.100:/tmp/ ssh root@192.168.1.100 "/tmp/hello"这里sysroot参数很关键,它告诉编译器去哪里找目标平台的头文件和库。用错sysroot是最常见的编译错误来源,症状是“头文件找不到”或者“链接时符号未定义”。排查思路是先确认工具链自带的那套库版本和开发板上跑的系统版本是否匹配,glibc版本差异会直接导致程序在板子上跑不起来,报“GLIBC_2.xx not found”。
3.2 VSCode和CLion怎么配出顺手的嵌入式环境
用VSCode做嵌入式开发,插件装对了效率翻倍,装错了就是一堆干扰。我的常用清单是:C/C++扩展(微软官方那个,负责跳转和补全)、CMake Tools、Cortex-Debug(连J-Link或OpenOCD调试)、Makefile Tools、Remote-SSH(直接连开发板或编译服务器写代码)、DeviceTree(写dts时语法高亮)、GitLens。写C++的话再补一个clangd,补全和重构比官方扩展更聪明,但配置稍麻烦,看个人取舍。
VSCode配置的核心是两个json文件。c_cpp_properties.json决定头文件搜索路径,配不好就会出现满屏红波浪线。
{ "configurations": [ { "name": "ARM-Linux", "includePath": [ "${workspaceFolder}/**", "/opt/toolchain/sysroot/usr/include" ], "defines": ["__GNUC__"], "compilerPath": "/opt/toolchain/bin/arm-linux-gnueabihf-gcc", "cStandard": "c11", "cppStandard": "c++17", "intelliSenseMode": "linux-gcc-arm" } ], "version": 4 }launch.json负责调试,用OpenOCD连开发板或仿真器的配置大致长这样:
{ "version": "0.2.0", "configurations": [ { "name": "Cortex Debug (OpenOCD)", "type": "cortex-debug", "request": "launch", "servertype": "openocd", "cwd": "${workspaceFolder}", "executable": "./build/app.elf", "device": "STM32F407VG", "configFiles": ["interface/stlink.cfg", "target/stm32f4x.cfg"] } ] }CLion的优势在C++和CMake工程上,重构和代码分析比VSCode强。做嵌入式Linux应用开发时我会用它的Remote Debug功能:本地写代码,通过SSH把二进制同步到板子上,用gdbserver远程调试。配置路径在Settings的Toolchains里加一个Remote Host,填开发板的IP和登录信息即可。CLion的问题是资源占用高,老机器上会卡,嵌入式驱动开发因为要频繁改内核配置和编译,我反而更倾向纯命令行加VSCode。
提示:不管用哪个IDE,底层的编译流程一定要能手动跑通。IDE只是包装,一旦编译出错而你不知道它背后执行了什么命令,排查就会非常被动。建议至少手动敲一遍gcc、make、cmake的完整流程。
3.3 Makefile、CMake、Buildroot各管什么
刚接触的人容易把这三个东西搞混,我用一句话说明分工:Makefile是构建规则,CMake是生成构建规则的工具,Buildroot是生成整个系统镜像的框架。写单片机和小型Linux应用,手写Makefile就够了,几十行能解决的问题别上CMake。项目规模上来(源文件几十个、有多个库依赖),再换CMake管理。
CROSS := arm-linux-gnueabihf- CC := $(CROSS)gcc CFLAGS := -Wall -O2 -I./include LDFLAGS := -L./lib -lpthread SRCS := $(wildcard src/*.c) OBJS := $(SRCS:.c=.o) TARGET := app $(TARGET): $(OBJS) $(CC) $^ -o $@ $(LDFLAGS) %.o: %.c $(CC) $(CFLAGS) -c $< -o $@ clean: rm -f $(OBJS) $(TARGET)这个Makefile里用了wildcard自动收集源文件、用了模式规则批量编译,是实战里最常用的写法。Buildroot则是在你需要从零做一个完整系统时才用:它把交叉工具链、u-boot、内核、根文件系统打包成一条流水线,你在menuconfig里勾选需要的包,它帮你下载、编译、打包。好处是标准化、可复现,坏处是第一次编译慢,而且要理解它那套包管理的目录结构。做产品或者复杂项目建议直接用Buildroot或Yocto,手搓根文件系统容易漏库。
4. 驱动开发:内核模块、设备树与系统裁剪
4.1 字符设备驱动的最小骨架
Linux驱动入门从字符设备开始,因为它模型最简单:用户态通过open/read/write/ioctl调用,内核里对应file_operations里的一堆函数指针。我建议你手写一遍最简版本,别直接抄现成的,抄的时候你会漏掉错误处理,而错误处理恰恰是驱动代码的精华。
#include <linux/module.h> #include <linux/fs.h> #include <linux/uaccess.h> #include <linux/cdev.h> static dev_t devno; static struct cdev my_cdev; static char kbuf[128]; static ssize_t my_read(struct file *f, char __user *buf, size_t len, loff_t *off) { if (*off >= sizeof(kbuf)) return 0; if (copy_to_user(buf, kbuf + *off, len)) return -EFAULT; *off += len; return len; } static ssize_t my_write(struct file *f, const char __user *buf, size_t len, loff_t *off) { if (len > sizeof(kbuf)) len = sizeof(kbuf); if (copy_from_user(kbuf, buf, len)) return -EFAULT; return len; } static struct file_operations fops = { .owner = THIS_MODULE, .read = my_read, .write = my_write, }; static int __init my_init(void) { alloc_chrdev_region(&devno, 0, 1, "mychar"); cdev_init(&my_cdev, &fops); cdev_add(&my_cdev, devno, 1); pr_info("mychar loaded, major=%d\n", MAJOR(devno)); return 0; } static void __exit my_exit(void) { cdev_del(&my_cdev); unregister_chrdev_region(devno, 1); } module_init(my_init); module_exit(my_exit); MODULE_LICENSE("GPL");几个新手必踩的坑:copy_to_user和copy_from_user绝对不能省,内核空间和用户空间地址不能直接互访;返回错误码要用负值,-EFAULT表示地址错误;module_exit里资源释放的顺序要和申请时相反。编译用内核源码树里的Makefile,靠一个外挂的Makefile指定obj-m,然后用内核的编译系统编译。
obj-m += mychar.o KDIR := /lib/modules/$(shell uname -r)/build all: $(MAKE) -C $(KDIR) M=$(PWD) modules clean: $(MAKE) -C $(KDIR) M=$(PWD) clean注意:开发板上的内核版本和你编译模块用的内核源码版本必须一致,否则加载时会报“invalid module format”。改内核配置后一定要重新编译模块,ABI对不上加载就失败。
4.2 设备树配置到底在配什么
设备树解决的是“板级信息硬编码在内核里”的历史包袱。同一颗芯片可以配到不同板子上,引脚接的什么外设、地址是多少、中断号是多少,这些都写在设备树里,内核启动时解析。dts文件的语法其实不复杂,核心是节点和属性。
&i2c1 { status = "okay"; clock-frequency = <400000>; sht30: sht30@44 { compatible = "sensirion,sht30"; reg = <0x44>; interrupt-parent = <&gpio1>; interrupts = <5 IRQ_TYPE_EDGE_FALLING>; }; };写设备树最常出的问题是compatible字段和驱动里的of_match_table对不上,导致驱动根本不probe。排查方法是在/sys/bus/i2c/devices/下面看设备有没有被枚举出来,如果设备在但驱动没加载,多半是match失败。另一个高频问题是引脚复用(pinctrl)配错,明明是I2C功能却配成了普通GPIO,现象是通信完全没反应。配pinctrl时要对照芯片的数据手册里引脚复用表,把复用功能和电气属性(上下拉、驱动能力)都配全。
4.3 系统裁剪和启动优化怎么做
产品化的时候,一个臃肿的内核和根文件系统是要命的——启动慢、占空间、还可能有安全隐患。裁剪的思路是“按需保留”:内核里关掉用不到的子系统(比如你的板子没有CAN总线、没有蓝牙,就别编进去),根文件系统里去掉调试工具和多余库,用busybox把常用命令精简到最核心的几十个。
启动时间优化有几个立竿见影的手段:把内核压缩方式从gzip换成lzo或lz4,解压更快;关掉启动时的printk输出(loglevel设低),串口打印本身就很耗时;用initcall_debug内核参数找出哪个初始化函数最慢,针对性优化;根文件系统用squashfs这类只读压缩格式配合overlayfs,比ext4挂载快;能并行初始化的驱动尽量并行。测量工具用bootgraph配合ftrace,能画出整个启动过程的时间轴。
# 内核启动参数里加上这些方便定位 # initcall_debug printk.time=1 ignore_loglevel dmesg | grep -E "initcall|probe" | sort -t'@' -k2 -n | tail -20这条命令能把最慢的初始化调用揪出来。我踩过的一个坑是某个不用的显示子系统在启动时花了整整1.5秒做自检,直接关掉就省下来了。裁剪这事没有银弹,就是对着启动日志一个个看,一个个砍。
5. 往上走:嵌入式AI部署与算法调优
5.1 边缘推理框架怎么选
这两年嵌入式AI开发热得不行,但框架选型有讲究,选错了后面全是麻烦。评判标准就三条:目标平台有没有硬件加速支持、算子覆盖够不够、社区是否活跃。
| 框架 | 适用场景 | 硬件加速 | 特点 |
|---|---|---|---|
| TFLite | 移动端、MCU | GPU、NPU、DSP | 生态好,量化工具链成熟 |
| NCNN | 移动端、ARM CPU | NEON、Vulkan | 轻量无依赖,腾讯维护 |
| ONNX Runtime | 通用部署 | 多后端 | 模型兼容性最好 |
| MNN | 移动端 | CPU、GPU、NPU | 阿里维护,性能不错 |
| 厂商NPU工具链 | 特定芯片 | 专用NPU | 性能最高,但绑定平台 |
选型建议:先看你的芯片有没有NPU,有就优先用厂商工具链,性能差距可能是十倍以上;没有NPU就用NCNN或TFLite的CPU后端,把NEON优化利用起来。别一上来追求最先进的技术,能把一个量化后的MobileNet跑通、拿到可用的帧率,就已经超过大多数人了。
顺带说一个很多人关心的问题:嵌入式领域有没有类似PCL(点云库)这样功能强大的开源库?有,但要看领域。视觉方向OpenCV是最通用的,数学计算有Eigen和Ceres Solver,通信协议有lwIP和libmodbus,JSON处理有cJSON和nlohmann/json,日志有spdlog,这些在资源受限的板子上都有裁剪版本。PCL本身也能在ARM Linux上编译,只是依赖比较多(Boost、FLANN、VTK),板子上跑起来吃内存,做点云处理要评估好资源。
5.2 模型量化和算子适配的实操
从PC上的浮点模型到板子上的INT8推理,中间隔着一整套量化流程。核心思路是用一批校准数据统计各层的激活值分布,把浮点范围映射到整数范围。TensorFlow的Post-Training Quantization和ONNX的QDQ格式都是这个路子。要点是校准集必须有代表性,用几十张随便挑的图片去校准,精度掉得会让你怀疑人生。
import tensorflow as tf def representative_dataset(): for img in calib_images: # 建议 100~300 张,覆盖各类场景 yield [tf.cast(img, tf.float32)] converter = tf.lite.TFLiteConverter.from_saved_model("model") converter.optimizations = [tf.lite.Optimize.DEFAULT] converter.representative_dataset = representative_dataset converter.target_spec.supported_ops = [tf.lite.OpsSet.TFLITE_BUILTINS_INT8] converter.inference_input_type = tf.int8 converter.inference_output_type = tf.int8 tflite_model = converter.convert()量化后精度掉太多怎么办?几个方向:换更细粒度的量化(per-channel比per-tensor好)、把敏感层(通常是第一层和最后一层)保留浮点、用QAT(量化感知训练)在训练阶段就模拟量化误差。算子不支持是另一个大坑,某些自定义层或者新出的算子,框架没实现。解法有两个:一是回退到CPU用参考实现,慢但能跑;二是自己写算子,需要懂目标平台的指令集,门槛较高。
5.3 性能调优:先定位瓶颈再动手
性能调优最大的忌讳是凭感觉优化。一定要先用工具定位。CPU占用高就用top和perf看热点函数在哪,内存带宽是瓶颈就用带宽测试工具测,推理慢就分阶段计时看是预处理、推理还是后处理慢。我见过有人花一周优化模型推理,最后发现瓶颈在图像resize和颜色空间转换上。
定位到瓶颈后再上手段:计算密集的用NEON或SIMD指令重写核心循环,内存访问密集的做数据布局优化(比如把NHWC改成NCHW减少访存跳跃),多核平台把不同任务绑到不同CPU核心,用DMA搬运数据释放CPU。功耗敏感的场景还要考虑DVFS调频策略,算力需求低的时候降频省电。优化完必须回归测试精度,有时候为了快改了个累加顺序,浮点误差累积起来精度就崩了。
6. 常见问题与踩坑速查
6.1 学习卡点速查表
下面这些是我这些年遇到最高频的问题,做成表方便你对照排查。
| 现象 | 可能原因 | 处理方向 |
|---|---|---|
| 程序烧进去不跑 | 时钟没使能、复位向量错、启动模式不对 | 查时钟树、确认BOOT引脚、用调试器单步 |
| 串口输出乱码 | 波特率不匹配、晶振频率算错、电平不兼容 | 核对波特率和时钟源,确认TTL电平 |
| 驱动加载报错 | 内核版本不匹配、符号未导出 | 用匹配的内核源码编译,检查EXPORT_SYMBOL |
| 设备树不生效 | compatible不匹配、dts没被编译进dtb | 查/sys/firmware/devicetree、反编译dtb确认 |
| 交叉编译程序跑不了 | glibc版本不匹配、动态库缺失 | 静态链接或用匹配的sysroot,ldd查依赖 |
| 推理结果和PC不一致 | 量化误差、预处理不一致、输入布局错 | 逐层对比中间输出,统一预处理参数 |
6.2 项目怎么选才有说服力
学完知识点一定要做项目,但项目选题有讲究。那种“跟着教程一步步敲、和教程一模一样”的项目,做十个也不如自己从需求出发做一个。好的练手项目应该满足几个条件:有明确的输入输出(比如采集温湿度并web展示)、涉及多个知识点(驱动加应用加网络)、能独立调试到跑通。举几个我做过也推荐别人做的例子:基于I2C传感器的数据采集网关,把数据通过MQTT发到服务器;带简单GUI的环境监测终端,用framebuffer或LVGL做界面;摄像头加轻量模型的人形检测设备,跑在带NPU的板子上。
做项目的过程中刻意练习调试能力:用示波器和逻辑分析仪看时序,用gdb调应用,用ftrace和perf调性能。真正的工程师能力差异,一大半体现在“出了问题能不能快速定位”。
6.3 一些不太好意思写在简历上的实在经验
第一,开发板不要买太多。一块STM32、一块Linux板子,足够你学到能独立做项目的水平。买板的快感会欺骗你,让你以为自己在进步,其实只是消费。第二,数据手册和参考手册是最好的老师,遇到不懂的外设直接翻手册的寄存器章节,比看十篇博客都管用。第三,代码要往GitHub上传,哪怕是练手项目,养成Commit和写README的习惯,这既是技术档案也是求职材料。第四,别怕看英文文档,内核文档、芯片手册、框架官网,一手资料永远比二手解读准确。第五,找人交流,加入技术群、参加线下活动、去开源社区提issue,一个人闷头学容易钻牛角尖,很多时候别人一句话就点破你纠结三天的问题。
我自己走到现在,最大的体会是嵌入学得快慢不取决于智商,取决于反馈循环够不够短。写一行代码能立刻在板子上看到现象,这个循环转得越快,学得越快。所以别在纯理论里泡太久,买块板子,今天就点个灯。