3. 嵌入式系统开发流程:需求分析、硬件设计、软件设计、测试与验证

做嵌入式开发这么多年,我见过太多项目栽在同一个坑里——流程混乱。很多人觉得嵌入式嘛,不就是写写单片机程序,焊几根线就完事了?

说实话,早期我也是这么干的。结果呢?硬件改了三版,软件重写了两次,测试阶段发现根本跑不起来。嗯,从那以后,我老老实实按流程走。

这一章,我就把压箱底的四阶段开发法掰开揉碎了讲给你听。

核心观点:嵌入式开发不是写代码,而是系统工程。需求→硬件→软件→测试,每一步都决定产品生死。

3.1 需求分析——别急着动手,先想清楚做什么

我见过最离谱的项目,客户说"做个智能家居网关",团队直接开干。三个月后交付,客户说"我要的是能控制窗帘的,你这连红外接口都没有"。

为什么会这样?需求没对齐。

我个人习惯把需求拆成三层:

需求层级 关注点 典型问题
功能需求 系统要做什么 采集哪些传感器?控制哪些执行器?
性能需求 做到什么程度 采样率多少?响应时间多快?功耗多低?
约束条件 不能做什么 成本上限?尺寸限制?使用环境温度?

我的经验:需求文档一定要写验收标准。比如"温度采集精度±0.5℃",而不是"温度要准"。否则测试阶段扯皮能扯到天荒地老。

需求分析阶段,我建议你画一张系统上下文图,把系统跟外界的所有交互画清楚。下面这张图就是我当时做工业控制器时画的模板:

嵌入式系统上下文图(需求分析阶段) 嵌入式系统 (目标硬件平台) 传感器 温度/压力/IMU 执行器 电机/阀门/继电器 人机界面 LCD/按键/触屏 上位机 PC/云平台/手机 需求分析关键 明确每个外部实体与系统的交互接口 数据流向、控制流向、电气特性

3.2 硬件设计——选型定方案,打好地基

需求明确了,接下来就是硬件设计。这一步我踩过的坑最多。

你想想看,软件写错了改一行代码就行,硬件画错了——重新打板,两周时间+几千块钱打水漂。

硬件设计我一般分四步走:

  1. 芯片选型:MCU主频、Flash/RAM大小、外设接口数量、功耗等级
  2. 原理图设计:电源树、时钟树、复位电路、外设接口电路
  3. PCB布局布线:信号完整性、电源完整性、EMC考虑
  4. BOM清单与打样:物料采购周期、替代料方案

我曾经踩过的坑:选了一颗很新的MCU,性能强价格低,结果采购周期12周。项目赶着量产,硬生生等了三个月。后来我学乖了——选型时一定要确认供货周期和替代料。

这里我列一个常用的MCU选型对比表,帮你快速定位:

需求场景 推荐架构 典型型号 Flash/RAM 关键外设
超低功耗传感节点 Cortex-M0+ STM32L0系列 64KB/8KB ADC、I2C、SPI
工业控制 Cortex-M4 STM32F4系列 512KB/128KB CAN、UART、定时器
人机交互界面 Cortex-M7 i.MX RT系列 2MB/512KB LCD控制器、SDRAM接口
实时控制 Cortex-R5 TMS570系列 1MB/256KB PWM、QEP、ADC

3.3 软件设计——分层架构,模块化开发

硬件板子回来了,软件就该上场了。但别急着写代码,先搭软件架构。

我见过最乱的代码——所有功能写在一个main.c里,全局变量满天飞,中断服务函数里调printf。这种代码,调试起来能让人崩溃。

我个人习惯用三层架构:

  • 硬件抽象层(HAL):封装MCU寄存器操作,提供统一API。比如 GPIO_WritePin()、UART_SendByte()
  • 中间件层:RTOS、文件系统、协议栈。这部分尽量用成熟的,别自己造轮子
  • 应用层:业务逻辑。这部分才是你的核心竞争力

举个例子,一个简单的温度采集任务,代码结构应该是这样的:

// ========== 硬件抽象层 ==========
// hal_adc.c
uint16_t HAL_ADC_ReadChannel(uint8_t ch) {
    // 操作ADC寄存器,返回原始值
    ADC->CR2 |= ADC_CR2_SWSTART;
    while(!(ADC->SR & ADC_SR_EOC));
    return ADC->DR;
}

// ========== 中间件层 ==========
// os_task.c
void OS_TaskCreate(void (*task)(void*), void* arg, uint32_t stack_size) {
    // 调用RTOS API创建任务
    xTaskCreate(task, "task", stack_size, arg, 1, NULL);
}

// ========== 应用层 ==========
// app_temp.c
void TempTask(void* arg) {
    uint16_t raw;
    float temp;
    while(1) {
        raw = HAL_ADC_ReadChannel(0);      // 读ADC
        temp = (raw * 3.3f / 4096) * 100;  // 转温度
        printf("Temp: %.2f°C\n", temp);
        vTaskDelay(pdMS_TO_TICKS(1000));   // 1秒采集一次
    }
}

我的建议:应用层代码不要直接操作寄存器。哪怕只是读一个GPIO,也要通过HAL层。这样换MCU时,只需要改HAL层,应用层代码几乎不用动。

3.4 测试与验证——别等出问题了再测

测试这件事,我吃过太多亏了。

早期做项目,都是"写完代码再测试"。结果呢?硬件问题、软件问题、时序问题搅在一起,根本分不清是谁的锅。

后来我改成边开发边测试,效率反而更高。

测试分几个层级:

测试层级 测试内容 常用工具
单元测试 单个函数/模块功能 CUnit、Unity、CMock
集成测试 模块间接口、数据流 逻辑分析仪、示波器
系统测试 完整功能、性能、功耗 万用表、功率计、环境试验箱
验收测试 是否满足需求文档 测试用例清单、客户现场验证

我曾经翻过的车:系统测试时发现,电机一启动,ADC采集值就跳变。查了三天,发现是电机驱动的大电流在PCB上造成了地弹,影响了ADC参考电压。后来在布局时把模拟地和数字地分开,问题解决。所以——硬件测试一定要带上实际负载。

最后,我把整个开发流程串成一张流程图,方便你对照执行:

嵌入式系统开发流程总图 需求分析 功能/性能/约束 硬件设计 选型/原理图/PCB 软件设计 架构/编码/调试 测试与验证 单元/集成/系统 关键原则 ① 需求驱动:每个阶段都要回溯需求,避免做无用功 ② 并行开发:硬件设计完成后,软件和硬件测试可以并行 ③ 迭代反馈:测试发现的问题,要反馈到需求或设计阶段修正 ④ 文档同步:每个阶段都要输出文档,别等最后补 实线:正向流程 | 虚线:反馈/并行流程

嗯,这套流程我用了快十年。不能说百分百不出问题,但至少能保证——出了问题你知道问题出在哪。