在广州干嵌入式这些年,最大感受就是:这个行业很少有一夜爆红的技术,更多时候是靠一个个稳定的模块、可靠的驱动、能扛住产线测试的产品堆出来的。很多应届生或者刚转行的朋友问我,嵌入式到底该怎么学、怎么做项目、怎么避坑。这篇文章不打算写成一本嵌入式教科书,而是按照真实项目的推进顺序,把环境搭建、软件架构演进、一个可以落地的采集项目、单元测试、常见排错和成长路线整理成一份完整笔记。文章偏实战,代码会尽量完整给出,你拿到后可以照着改、照着跑。
1. 为什么写这篇“广州嵌入式生存笔记”
1.1 广州嵌软岗位的特征
广州的嵌入式岗位和北京、深圳有些不一样。这里没有那么多互联网大厂的大规模后端岗位,更多是家电、车载电子、工业控制、物联网终端、智能硬件公司。这些公司对嵌入式软件工程师的要求往往不只是“会写代码”,还要求你懂一点硬件电路、会看原理图、能用万用表排查问题,甚至要自己焊板子调试。
所以广州做嵌入式,更多时候拼的是综合能力。同样的岗位要求里,北京可能更看重算法和架构,广州这边更看重你能不能把一块板子稳定跑起来,能不能控制成本,能不能在产线出问题时快速定位是硬件问题还是软件问题。
另外一个特点是:广州很多项目是“嵌入式 Linux + MCU”混合开发。一个产品里往往有一颗 MCU 负责实时控制,一颗 MPU 跑 Linux 系统负责网络、显示、协议栈。这就意味着,嵌入式软件工程师不能只懂裸机开发,还得懂 Linux 系统编程、驱动基础、交叉编译,甚至要懂一点设备树。
1.2 嵌入式开发的核心知识边界
嵌入式开发和普通后端开发的最大区别,在于它贴着硬件工作。
普通应用开发关心的是业务逻辑,而嵌入式开发除了业务逻辑之外,还要关心:CPU 怎么启动、时钟怎么配置、外设寄存器怎么操作、中断怎么响应、内存够不够、功耗高不高、EMC 过不过得了。
一般可以把嵌入式开发的知识边界分成四块:
- 硬件基础:电阻电容、三极管、运放、ADC、PWM、I2C、SPI、UART 这些基本外设原理。
- 底层软件:芯片启动、寄存器配置、中断服务函数、定时器、驱动框架。
- 系统软件:RTOS 任务调度、Linux 应用开发、内核驱动、设备树、文件系统。
- 工程能力:调试工具、测试方法、版本管理、产线对接、文档维护。
很多人一开始容易陷在“写代码”里面,忽略硬件和系统层面的理解。实际上在广州的嵌入式岗位里,能同时打通硬件、MCU、Linux 三层的人,薪酬和发展空间会明显更好。
1.3 文章能帮你解决什么问题
本文会围绕一条真实感很强的技术路线展开:
- 搭建嵌入式开发环境,包括交叉编译工具链和调试工具;
- 梳理嵌入式软件架构从超级大循环到事件驱动的演进过程;
- 用嵌入式 Linux 平台做一个 NTC 温度采集与串口上报的小项目,代码完整给出;
- 介绍嵌入式单元测试和工程化思路;
- 整理高频问题排查表和广州嵌入式岗位的学习成长建议。
如果你是刚接触嵌入式,可以按顺序阅读;如果你已经有项目经验,可以直接跳到第4章和第6章看代码和排错。
2. 环境准备与工具链
2.1 硬件与开发板选择
做嵌入式开发离不开开发板。国内工程师常用的板子大概几类:
- MCU 类:STM32、GD32、ESP32、瑞萨等,适合裸机、RTOS、物联网终端。
- Linux 类:飞凌、迅为、正点原子、友善之臂等厂商的 i.MX6ULL、RK3568、全志 H3 等核心板,适合嵌入式 Linux 项目。
- 高性能类:RK3588、树莓派、Jetson 系列,适合嵌入式 AI、边缘计算场景。
很多入门朋友在选板子上会纠结很久。我的建议是:如果做 MCU 方向,STM32F103 或 ESP32 是经典选择;如果做嵌入式 Linux 方向,i.MX6ULL 或 RK3568 这类核心板更贴近工业产品形态。
我在广州碰到很多做工业网关、数据采集盒的项目,普遍用飞凌或迅为的核心板加自研底板。核心板已经把 CPU、DDR、eMMC 做好了,底板上画电源、网口、串口、ADC 采集电路,这样开发周期会压缩很多。
2.2 交叉编译工具链安装
嵌入式 Linux 上的程序基本不能在开发板上直接编译,原因是开发板的 CPU 资源和存储空间有限,所以需要在 PC 上用交叉编译工具链生成目标板能运行的可执行文件。
以 Ubuntu 环境为例,安装 ARM 32 位工具链的命令大致如下:
# 更新软件源 sudo apt update # 安装 ARM 32 位交叉编译工具链 sudo apt install gcc-arm-linux-gnueabihf # 安装完检查版本 arm-linux-gnueabihf-gcc --version如果你的平台是 ARM64,也就是 aarch64 架构,则需要安装:
sudo apt install gcc-aarch64-linux-gnu你可能会想:为什么选交叉编译而不是直接在板子上编译?
原因很简单:
- 开发板 CPU 性能弱,编译大项目慢;
- 交叉编译环境更接近正式构建产物的方式;
- 依赖的库、头文件、sysroot 都可以在 PC 上统一管理。
在实际项目里,我们通常会使用厂商提供的 SDK 工具链,而不是 Ubuntu 自带的通用工具链。因为厂商 SDK 里带了匹配的内核头文件、库文件、GDB、调试工具,可以避免版本不匹配问题。所以上面的 apt 安装命令只适合快速体验,真实项目建议直接用开发板厂商提供的光盘或网盘工具链。
2.3 常用调试工具
嵌入式调试不能只靠 printf,尤其遇到崩溃、内存泄漏、驱动异常时,需要工具链支撑。
我整理了一个常用工具清单,都是实际项目里高频用到的:
| 工具 | 用途 |
|---|---|
| minicom / picocom | 串口调试终端 |
| OpenOCD | MCU 调试和烧录 |
| GDB + gdbserver | 远程断点调试 |
| readelf / objdump | ELF 文件分析 |
| strace | 系统调用跟踪 |
| /proc 文件系统 | 内核和进程状态查看 |
| valgrind | 内存泄漏检测 |
| tcpdump | 网络问题定位 |
其中串口工具是嵌入式开发使用频率最高的。Linux 下用 picocom 会比较稳定:
sudo picocom -b 115200 /dev/ttyUSB0板上程序跑起来后,通过串口输出的日志基本是你第一手判断依据,所以代码里的日志系统一定要设计好。
2.4 工程组织方式
嵌入式项目建议一开始就按清晰目录结构组织,否则代码量上来之后会非常痛苦。
一个典型的嵌入式 Linux 应用工程目录可能是这样的:
project/ ├── CMakeLists.txt ├── Makefile ├── build/ ├── include/ │ ├── adc.h │ ├── ntc.h │ └── uart.h ├── src/ │ ├── main.c │ ├── adc.c │ ├── ntc.c │ └── uart.c ├── tests/ │ ├── test_ntc.c │ └── test_ntc.sh └── docs/把硬件操作、业务逻辑、测试代码分开,是后面做单元测试和持续集成的基础。这一步不能省。
3. 嵌入式软件架构:从超级大循环到事件驱动
很多嵌入式新手接触的第一个项目就是这样写代码:
while (1) { read_sensor(); process_data(); delay(100); }这种写法在简单场景下没有问题,但一旦外设变多、实时性要求变高、状态复杂起来,代码会越来越难维护。这里梳理一下嵌入式软件架构的几个阶段,这也是嵌入式面试中常出现的“分水岭”话题。
3.1 超级大循环:入门时最常用的框架
超级大循环又称前后台系统,主循环里不断轮询各个任务,中断负责标记事件。
#include <stdio.h> volatile unsigned int g_flag = 0; void timer_isr(void) { g_flag = 1; } int main(void) { while (1) { if (g_flag) { g_flag = 0; read_sensor(); process_data(); } // 其他非实时任务 led_toggle(); uart_send_pending(); } return 0; }优点很明显:结构简单,内存占用低,适合逻辑固定的小型系统。缺点也很明显:所有任务串行执行,一个任务阻塞,其他任务全部卡住;延时会浪费 CPU;任务多了以后,优先级和响应时间很难控制。
3.2 中断 + 状态机
当系统有按键、通讯协议、多步操作时,单纯靠延时轮询就会出问题。比如按键消抖、串口接收不定长数据,不可能在主循环里一直等待。
这时候很多工程师会引入状态机设计。在中断里只做最小处理,主循环根据状态机的状态决定下一步操作。
typedef enum { STATE_IDLE, STATE_MEASURE, STATE_SEND, } app_state_t; app_state_t state = STATE_IDLE; while (1) { switch (state) { case STATE_IDLE: if (button_pressed()) { state = STATE_MEASURE; } break; case STATE_MEASURE: start_adc(); state = STATE_SEND; break; case STATE_SEND: uart_send(result); state = STATE_IDLE; break; } }状态机让代码逻辑更清晰,也能更好处理“等待外部事件”的场景。但状态多了以后,状态迁移关系容易变得混乱,需要配合状态表来管理。
3.3 RTOS 与任务划分
当系统需要同时处理多个实时性要求不同的任务时,裸机架构会比较吃力。这时候引入 RTOS(实时操作系统),比如 FreeRTOS、RT-Thread、Zephyr。
RTOS 的核心思路是:把不同的功能拆成独立任务,每个任务有独立优先级,通过信号量、消息队列、互斥锁来通信。
void sensor_task(void *arg) { for (;;) { int value = adc_read(); xQueueSend(adc_queue, &value, pdMS_TO_TICKS(100)); vTaskDelay(pdMS_TO_TICKS(500)); } } void uart_task(void *arg) { for (;;) { int value; if (xQueueReceive(adc_queue, &value, portMAX_DELAY)) { uart_send_int(value); } } }RTOS 让任务隔离更彻底,一个任务的 bug 不至于瞬间拖垮整个系统,但也引入了优先级反转、死锁、堆栈溢出等新问题。所以使用 RTOS 时,堆栈大小、消息队列深度、优先级安排都需要认真设计。
3.4 事件驱动架构
在更大的嵌入式 Linux 或者复杂物联网终端里,任务之间交互频繁,很多团队会选择事件驱动架构:所有事情都通过事件发布与订阅来解耦。
typedef struct { uint32_t type; union { int int_value; float float_value; char string_value[64]; } data; } event_t; void event_post(event_t *evt); void event_subscribe(uint32_t type, event_callback_t cb); // 业务模块只需要订阅关心的事件 event_subscribe(EVENT_ADC_READY, on_adc_ready); event_subscribe(EVENT_UART_RECEIVED, on_uart_received);事件驱动架构的优势是模块间依赖少,新增功能时不需要改原有调用链。缺点是事件流不直观,出现 bug 时靠日志定位需要经验。
3.5 架构选型建议
| 维度 | 超级大循环 | 状态机 | RTOS | 事件驱动 |
|---|---|---|---|---|
| 代码量 | 小 | 中 | 中 | 较大 |
| 实时性 | 弱 | 中 | 强 | 中 |
| 可扩展性 | 差 | 中 | 较好 | 好 |
| 调试难度 | 容易 | 中等 | 较难 | 较难 |
| 适用场景 | 简单外设 | 协议处理 | 复杂实时系统 | 物联网终端 |
实际项目里它们不是互斥的。很多产品是裸机状态机 + RTOS 消息队列混用;嵌入式 Linux 应用则普遍采用事件驱动 + 多线程模型。你面试时如果能把这个演进过程讲清楚,再结合一两个实际例子,会比背八股文效果好很多。
4. 实战:嵌入式 Linux 下 NTC 温度采集与串口上报
4.1 需求与整体流程
很多工业数据采集盒都需要采集外部温度。温度传感器方案有很多,但在成本敏感的项目里,NTC(负温度系数热敏电阻)是最常用的方案之一。
NTC 的特点是:温度升高,电阻值下降。外部电路一般用电阻分压,把电阻变化转换成电压变化,再通过 ADC 采样得到数字值,最后在软件里反算出温度。
整体流程如下:
NTC电阻变化 → 分压电路 → ADC采样 → 原始值转换电压 → 计算NTC电阻 → B值公式计算温度 → 串口输出下面我用一个嵌入式 Linux 平台的小项目为例,演示整个链路。硬件方面假设开发板带有 ADC 接口,Linux 系统可以通过 sysfs 读取 ADC 原始值;串口采用标准 POSIX 编程。
4.2 ADC 与 NTC 硬件理解
先画一个最简单的分压电路:
电源(3.3V) ── R(固定电阻) ──┬── ADC采样点 │ NTC │ GND固定电阻 R 和 NTC 串联分压。当温度变化时,NTC 阻值变化,ADC 采样点的电压也随之变化。
根据分压公式:
V_adc = V_ref * R_ntc / (R + R_ntc)反过来可以求出 NTC 当前阻值:
R_ntc = R * V_adc / (V_ref - V_adc)其中,ADC 原始值和电压的关系是线性的:
V_adc = V_ref * raw / max_raw假设 ADC 是 12 位,则 max_raw = 4095;如果是 10 位,则 max_raw = 1023。实际值要根据芯片 datasheet 和内核配置确认。
得到 R_ntc 之后,通过 NTC 的 B 值公式计算温度:
T = 1 / (1/T0 + ln(R_ntc/R0) / B) - 273.15其中:
- T0 = 298.15K,对应 25 摄氏度;
- R0 是 25 摄氏度时的 NTC 标称阻值;
- B 是 NTC 的 B 值参数,常见有 3435K、3950K 等;
- T 的单位是 K,最后要减 273.15 转成摄氏度。
4.3 核心代码实现
先看 ADC 读取模块,采用 sysfs 接口。不同平台 ADC 节点路径不同,常见的是类似/sys/class/hwmon/hwmon0/device/in_voltage0_raw。实际使用时需要通过find或cat /sys/bus/iio/devices/确认节点路径。
文件路径:src/adc.c
#include <stdio.h> #include <stdlib.h> #include <string.h> #include <fcntl.h> #include <unistd.h> #include <errno.h> #include "adc.h" #define ADC_SYSFS_PATH "/sys/class/hwmon/hwmon0/device/in_voltage0_raw" int adc_read_raw(void) { int fd = -1; char buf[64] = {0}; int raw = -1; ssize_t n; fd = open(ADC_SYSFS_PATH, O_RDONLY); if (fd < 0) { perror("open adc sysfs failed"); return -1; } n = read(fd, buf, sizeof(buf) - 1); if (n < 0) { perror("read adc sysfs failed"); close(fd); return -1; } buf[n] = '\0'; raw = atoi(buf); close(fd); return raw; }NTC 温度计算模块,把计算公式独立封装,方便本地测试。
文件路径:src/ntc.c
#include <math.h> #include "ntc.h" #define NTC_R0 10000.0f // 25℃时 NTC 标称电阻 10K #define NTC_B 3950.0f // NTC B 值 #define T0 298.15f // 25℃ 对应的开尔文温度 #define VREF 3.3f // ADC 参考电压 #define MAX_RAW 4095.0f // ADC 满量程,12位 #define FIXED_R 10000.0f // 分压电路中的固定电阻 static float adc_raw_to_voltage(int raw) { return VREF * raw / MAX_RAW; } static float voltage_to_ntc_resistance(float voltage) { if (voltage <= 0.0f || voltage >= VREF) { return -1.0f; } return FIXED_R * voltage / (VREF - voltage); } static float ntc_resistance_to_temp(float resistance) { if (resistance <= 0.0f) { return -1.0f; } float t = 1.0f / (1.0f / T0 + logf(resistance / NTC_R0) / NTC_B); return t - 273.15f; } float ntc_get_temperature_from_raw(int raw) { float voltage = adc_raw_to_voltage(raw); float resistance = voltage_to_ntc_resistance(voltage); if (resistance < 0.0f) { return -1.0f; } return ntc_resistance_to_temp(resistance); }串口发送模块,这里只给出核心发送部分。假设串口已经通过外部程序配置好波特率,应用层用标准 open/write 即可。
文件路径:src/uart.c
#include <stdio.h> #include <fcntl.h> #include <unistd.h> #include <string.h> #include "uart.h" #define UART_DEV "/dev/ttyS1" int uart_send_temperature(float temp) { int fd = -1; char buf[64] = {0}; int len; fd = open(UART_DEV, O_RDWR | O_NOCTTY | O_NONBLOCK); if (fd < 0) { perror("open uart failed"); return -1; } len = snprintf(buf, sizeof(buf), "temp=%.2f C\n", temp); if (write(fd, buf, len) < 0) { perror("write uart failed"); close(fd); return -1; } close(fd); return 0; }主循环代码:
文件路径:src/main.c
#include <stdio.h> #include <unistd.h> #include "adc.h" #include "ntc.h" #include "uart.h" int main(void) { for (;;) { int raw = adc_read_raw(); if (raw < 0) { fprintf(stderr, "read adc failed\n"); sleep(1); continue; } float temp = ntc_get_temperature_from_raw(raw); if (temp < -100.0f) { fprintf(stderr, "calculate temp failed, raw=%d\n", raw); sleep(1); continue; } uart_send_temperature(temp); printf("raw=%d, temp=%.2f C\n", raw, temp); sleep(2); } return 0; }以上代码是实际项目中很好用的一个基础框架,硬件抽象后的函数可以被单元测试直接调用,这是后面工程化的关键。
4.4 Makefile 构建
交叉编译的 Makefile 可以这样写:
CROSS_COMPILE ?= arm-linux-gnueabihf- CC := $(CROSS_COMPILE)gcc CFLAGS := -Wall -O2 LDFLAGS := -lm SRC := src/main.c src/adc.c src/ntc.c src/uart.c OBJ := $(SRC:.c=.o) TARGET := ntc_demo all: $(TARGET) $(TARGET): $(OBJ) $(CC) $(CFLAGS) -o $@ $^ $(LDFLAGS) %.o: %.c $(CC) $(CFLAGS) -c -o $@ $< clean: rm -f $(OBJ) $(TARGET)-lm很重要,因为 ntc.c 里用了logf,数学函数需要链接 math 库。这一点很多新手会踩坑,编译器找不到logf时往往是一脸懵。
4.5 运行与验证
在 PC 上交叉编译:
make clean make编译成功后会生成可执行文件ntc_demo。把可执行文件拷贝到开发板,可以通过 NFS、TFTP 或者 U 盘。假设通过 scp 拷贝:
scp ntc_demo root@192.168.1.100:/root/在开发板上给执行权限并运行:
chmod +x /root/ntc_demo /root/ntc_demo预期输出类似:
raw=2048, temp=25.03 C raw=2100, temp=23.45 C raw=2200, temp=20.87 C同时,接好串口的话,串口终端会每隔 2 秒收到一行:
temp=25.03 C4.6 结果说明与注意事项
如果你的 ADC 原始值出现剧烈跳变,建议在软件里做多次采样滤波。最简单的做法是连续读 5 次取平均值。
int adc_read_raw_average(int times) { int sum = 0; int raw; int i; for (i = 0; i < times; i++) { raw = adc_read_raw(); if (raw < 0) { continue; } sum += raw; } return sum / times; }滤波代码虽然简单,但对稳定性的提升非常明显,尤其是在工业现场有电机、变频器等干扰源的场景下。
另外要注意的是,NTC 阻值和 B 值参数不同,计算公式里的NTC_R0、NTC_B、FIXED_R都要按实际硬件修改。一个常见误区是:把 10K NTC 的 R0 设置成了 100K,导致计算温度整体偏移十几度。这类问题只靠程序检查不出来,需要拿标准温度计核对。
5. 工程化与单元测试:不只是把程序跑起来
5.1 嵌入式单元测试的意义
很多嵌入式工程师在开发时习惯“烧录板子 -> 看现象 -> 改代码”,这种方式在小项目里没问题,但项目大到一定规模后,回归测试成本会非常高。
一个很小的改动,比如修改了 NTC 的计算公式,如果不做单元测试,你可能要重新烧录、重新接串口、重新等待温度稳定。而单元测试可以直接在 PC 上跑,几秒钟就能验证计算公式是否正确。
嵌入式单元测试的核心思想是:把不依赖真实硬件的代码抽离出来,用模拟数据或桩函数进行测试。像ntc.c这种计算逻辑,完全可以在 x86 PC 上编译测试。
5.2 简单测试框架示例
如果不想引入复杂框架,可以先写一个轻量测试程序。下面是测试ntc.c的一个简单示例:
文件路径:tests/test_ntc.c
#include <stdio.h> #include <math.h> #include "ntc.h" static int test_count = 0; static int pass_count = 0; #define CHECK(cond) \ do { \ test_count++; \ if (cond) { \ pass_count++; \ printf("PASS: %s\n", #cond); \ } else { \ printf("FAIL: %s\n", #cond); \ } \ } while (0) int main(void) { float temp; // 常温下 NTC 阻值接近 R0,温度接近 25℃ // 这里用原始值模拟分压中点 temp = ntc_get_temperature_from_raw(2047); CHECK(fabs(temp - 25.0f) < 3.0f); // 温度升高,NTC 阻值下降 temp = ntc_get_temperature_from_raw(2500); CHECK(temp > 25.0f); // 温度降低,NTC 阻值上升 temp = ntc_get_temperature_from_raw(1500); CHECK(temp < 25.0f); printf("total=%d, pass=%d, fail=%d\n", test_count, pass_count, test_count - pass_count); return pass_count == test_count ? 0 : 1; }编译测试程序时不需要交叉编译,直接用 gcc:
gcc -o test_ntc tests/test_ntc.c src/ntc.c -lm ./test_ntc运行结果:
PASS: fabs(temp - 25.0f) < 3.0f PASS: temp > 25.0f PASS: temp < 25.0f total=3, pass=3, fail=0这种轻量测试很适合计算逻辑、协议解析、状态机等模块。如果你需要更完整框架,可以了解 Unity、Ceedling、CMock 组合,它们能实现 mock 硬件依赖、自动生成测试用例等功能。Unity 是嵌入式领域使用非常广泛的单元测试框架,值得花时间学习。
5.3 结合 CI 的工程建议
单元测试要让团队收益最大化,必须和持续集成结合起来。
我在实际项目中的做法是:在 GitLab CI 或 Jenkins 里配置一个编译任务,x86 环境编译所有与硬件无关的代码并跑单元测试;交叉编译任务单独的 job,负责生成板卡运行镜像。
示例 CI 流水线思路:
代码提交 → 静态检查 → x86 单元测试 → 交叉编译 → 打包固件 → 通知测试部门这样硬件无关的逻辑在提交代码时就能被验证,而硬件相关的问题留给后续板卡测试。整个产品开发周期会缩短很多。
广州很多嵌入式团队还没有引入 CI,因为搭建初期会花一些时间,但一旦项目进入维护期,收益会非常明显。尤其是人员流动后,新接手的人改代码,单元测试能给他提供最基本的安全感。
6. 常见问题与排查思路
嵌入式开发中,绝大多数时间其实是在跟问题和异常打交道。下面这份排查表来自我多年踩坑的经验,先给一个总览:
| 问题现象 | 常见原因 | 解决思路 |
|---|---|---|
| 交叉编译报错找不到头文件 | 工具链 sysroot 不正确 | 检查工具链安装路径,确认内核头文件版本 |
| 板子启动后串口无输出 | boot 参数不对、波特率不匹配 | 确认串口引脚、波特率、启动介质 |
| 串口输出乱码 | 波特率错误、地线没接好 | 核对波特率,检查供电和共地 |
| ADC 原始值跳变 | 滤波不够、电源纹波、参考电压不稳 | 软件均值滤波,检查硬件电路 |
| NTC 温度偏差很大 | B 值、R0 参数不对 | 对比规格书,用标准温度计校准 |
| Linux 程序段错误 | 空指针、越界访问 | gdb 定位,打开 AddressSanitizer |
| Qt 应用内存泄漏 | QObject 父子对象未正确管理 | 结合 valgrind 排查,检查 new/delete |
| 开发板第二个网口不工作 | 设备树未配置、phy 地址不对 | 检查设备树网络节点、PHY 驱动 |
| 内核模块加载失败 | 内核版本不匹配、依赖未加载 | dmesg 查看日志,检查模块依赖 |
| 写入寄存器没效果 | 时钟未使能、寄存器地址错误 | 阅读芯片手册,确认操作时序 |
下面展开几个高频问题。
6.1 交叉编译找不到头文件
错误现象:
fatal error: pthread.h: No such file or directory可能原因:
- 用错了工具链,比如用 aarch64 工具链编译 ARM 32 位代码;
- sysroot 路径没有设置正确;
- 缺少对应的库文件安装包。
排查步骤:
- 先确认板子 CPU 架构:
uname -m; - 检查编译器默认搜索路径:
arm-linux-gnueabihf-gcc -print-sysroot; - 确认头文件是否在 sysroot 里;
- 如果工具链自带 sysroot 不完整,需要重新安装配套 SDK。
这类问题最有效的解决方法是直接用厂商 SDK 里的环境脚本,一般会通过环境变量设置好CROSS_COMPILE、CFLAGS、SYSROOT,避免手动配置出错。
6.2 ADC 读数乱跳
ADC 读数跳动在工业现场尤其常见。可能原因有:
- 没有滤波,单次采样受噪声干扰;
- ADC 参考电压不稳定;
- NTC 引脚走线过长,受到数字信号干扰;
- 电源纹波过大。
解决思路是硬件和软件同时处理。软件上先做多次采样取均值或滑动滤波,硬件上确保 ADC 采样引脚有符合 datasheet 要求的滤波电容。如果加了滤波后仍然跳动明显,可能需要用示波器看波形,确认是不是参考电压本身出了问题。
6.3 嵌入式 Linux 程序内存泄漏
嵌入式 Linux 平台内存资源有限,内存泄漏是极其头痛的问题。如果是 Qt 应用程序,排查思路一般如下:
- 优先检查
new出来的对象是否设置了正确的父对象; - 使用
valgrind检查内存泄漏,但 valgrind 在板子上跑速度很慢,建议先在 x86 环境用同样代码测试; - 使用 AddressSanitizer 重新编译程序,能更快定位越界和泄漏;
- 对于 Qt 应用,注意
QByteArray、QString的隐式共享和拷贝问题。
我见过不少项目内存泄漏的根因,其实是定时器里不断new对象,又忘记释放,或者connect的 lambda 捕获了this后对象生命周期管理不当。这些问题靠“看代码”很难发现,必须借助工具。
6.4 开发板打开第二个网口失败
很多核心板默认只有一个网口在工作,第二个网口需要配置设备树和内核驱动。排查时先确认:
- 硬件上第二个网口的 PHY 芯片是否正常供电;
- 设备树里是否正确声明了第二个网口节点;
- PHY 地址是否和实际电路一致;
- 内核是否编译了对应 PHY 驱动。
在开发板上可以先看核心板厂商提供的其他 dts 文件作为对比。不同厂商对网口的命名可能不一样,有的叫ethernet1,有的叫fec2,需要结合内核源码确认。
7. 广州嵌入式工程师的成长建议
7.1 学习路线
针对广州嵌入式岗位的实际情况,我给出一条相对务实的学习路线:
- 第一阶段:掌握 C 语言和基础电路。C 语言重点掌握指针、结构体、内存管理、回调函数;电路部分重点掌握欧姆定律、分压电路、二极管、三极管、运放基本用法。
- 第二阶段:入手一款 MCU,比如 STM32,学习 GPIO、UART、I2C、SPI、定时器、ADC、中断。能独立完成一个小项目,比如温湿度采集、电机控制、智能小车。
- 第三阶段:学习 FreeRTOS 或 RT-Thread,理解任务、队列、信号量、互斥锁。尝试把一个裸机项目改造成 RTOS 项目。
- 第四阶段:进入嵌入式 Linux,学习 Linux 系统编程、文件 IO、多线程、网络编程。这一步是通往高薪岗位的关键。
- 第五阶段:学习 Linux 驱动开发,了解字符设备驱动、设备树、中断底半部、内核模块编译。
- 第六阶段:根据自己的兴趣选择方向,比如汽车电子、工业控制、物联网、嵌入式 AI。
这条路线如果每周投入 10 小时,大概需要 1 到 1.5 年可以完成前四阶段。不要贪多求快,嵌入式领域基础不牢,后面返工代价非常高。
7.2 面试与“八股”
广州不少公司在嵌入式面试时还是会问一些“八股”题,比如:
volatile关键字的作用是什么?static修饰函数和变量的区别?- 中断服务函数里能调用
printf吗? - 什么是大小端,如何判断?
- 内存对齐是什么?结构体里为什么会有内存空洞?
- 嵌入式系统中任务通信有哪些方式?
- 简述 NTC 和热电偶、DS18B20 等传感器的区别?
这里特别说一下 NTC 和传感器的区别。NTC 本质上是一个热敏电阻,本身不产生数字信号,必须通过电阻分压加 ADC 采样才能得到温度,精度和一致性取决于 B 值、R0 和电路设计。而 DS18B20 是单总线数字温度传感器,内部集成了 ADC 和校准逻辑,可以直接输出数字温度,使用更方便但成本更高。传感器是统称,NTC 只是其中一类元件。
面试时遇到这类问题,不要只背结论,尽量把底层原理和项目经验结合起来。面试官更看重的是你为什么这么用、有没有踩过坑。
7.3 行业方向
广州的嵌入式就业方向比较广,我观察下来主要分几类:
- 汽车电子:车身控制器、VCU、BMS、车载网关,对功能安全要求高。
- 工业控制:PLC、数据采集、工业网关、运动控制,对稳定性要求高。
- 家电与消费电子:变频空调、小家电、扫地机器人,对成本敏感。
- 物联网:智能门锁、烟感、水表、传感器终端,关注低功耗和通信协议。
- 嵌入式 AI:边缘计算盒子、机器视觉、语音识别终端,需要掌握 NPU 部署和模型量化。
近几年“嵌入式 AI”热度上升很快,甚至有人尝试把大模型部署到嵌入式板卡上。这个方向确实有前景,但前提还是先打好嵌入式基础,否则跑模型时遇到的底层问题会非常难排查。
7.4 必备习惯
最后结合这些年经验,分享几个嵌入式工程师真正拉开差距的习惯:
- 看芯片手册而不是只看例程。很多问题,比如 GPIO 开漏和推挽的区别、ADC 采样时间设置,都写在 datasheet 里。
- 学会读内核源码。嵌入式 Linux 项目避不开对内核源码的理解,尤其是设备树和驱动,只看博客很难深入。
- 日志规范从第一天做起。日志里要带时间戳、模块名、关键数值,不要到现场排查时才后悔。
- 重视备份和版本管理。嵌入式工程环境复杂,一个能恢复的构建环境比写一天代码更宝贵。
- 养成成本意识。做产品时,一个电阻、一颗芯片的选择,可能决定产品的利润空间。这一点广州的嵌入式岗位特别看重。
- 跨部门沟通要耐心。嵌入式项目经常需要和硬件、结构、测试、产线打交道,技术方案不要只站在软件视角考虑。
在广州做嵌入式,大多数时候不是做惊天动地的功能,而是把一个模块、一个驱动、一个产品做到稳定可靠。就像本文里面的 NTC 温度采集,看起来只是读 ADC、算公式、发串口,但真正做成产品后,要考虑精度、校准、滤波、成本、产线测试,任何一个环节偷懒都会在批量阶段暴露问题。希望这篇文章能帮你少走一些弯路,尤其是刚进入嵌入式行业、或者正在广州找嵌入式方向的读者,能对真实工作内容有一个比较清醒的认识。如果你想继续深入,可以从嵌入式 Linux 驱动方向入手,那又是一片非常广阔且值得投入的领域。