第2章:系统架构设计——硬件架构选型、软件分层设计、数据流与事件流设计
好,咱们进入第二章。上一章我们把心电监护仪的需求理清楚了,现在该动真格的了——搭架子。
系统架构设计这事儿,说白了就是回答三个问题:用什么硬件跑?软件怎么分层?数据怎么流动? 这三个问题想明白了,后面写代码、调板子才不会乱成一锅粥。
我个人习惯,做嵌入式系统设计,先画一张大图。把硬件框、软件层、数据流向全画在一张纸上。嗯,这张图就是咱们这章的骨架。
2.1 硬件架构选型
先聊硬件。心电监护仪对硬件的要求其实挺明确的:
- 要能实时采集心电信号(ADC得够快)
- 要能处理算法(CPU不能太弱)
- 要能显示波形(屏幕驱动得跟上)
- 要能联网报警(Wi-Fi/以太网得有)
- 最关键的是——绝对不能死机
我在项目中遇到过选型翻车的情况。有一回选了颗性价比很高的MCU,结果跑完心电滤波算法后,CPU占用率飙到95%,连刷屏都卡。后来硬着头皮换了颗带硬件浮点运算的芯片,才把问题解决。
所以我的建议是:主控芯片至少预留30%的性能余量。别卡着极限选型,那是给自己挖坑。
下面是我个人比较推荐的一套硬件架构:
| 模块 | 推荐选型 | 说明 |
|---|---|---|
| 主控MCU | STM32H743 / NXP i.MX RT1050 | Cortex-M7内核,带FPU,主频400MHz+ |
| 心电前端 | ADS1292R / AD8232 | 集成右腿驱动、导联脱落检测 |
| 显示 | 7寸LCD (800x480) + RGB接口 | 刷新率至少30fps,波形才不卡顿 |
| 存储 | W25Q64 (8MB Flash) + MicroSD | 事件日志存SD卡,固件放Flash |
| 通信 | ESP32-S3 (Wi-Fi) + W5500 (以太网) | 双通道冗余,断网自动切换 |
| 电源管理 | TPS63070 + 电池充电管理 | 支持电池/市电无缝切换 |
⚠️ 避坑指南: 我曾经在选心电前端时贪便宜用了某国产ADC,结果共模抑制比不够,工频干扰滤都滤不掉。后来老老实实换回TI的ADS1292R,问题一次解决。心电信号是微伏级别的,前端芯片的钱不能省。
2.2 软件分层设计
硬件选好了,接下来是软件。你想想看,一个心电监护仪的软件有多复杂?
- 底层要驱动ADC、LCD、SD卡
- 中间层要跑滤波算法、心率计算、心律失常检测
- 上层要处理用户交互、报警逻辑、事件记录
如果不分层,全揉在一个main.c里,那维护起来就是噩梦。我见过一个项目,整个工程就三个文件:main.c、interrupt.c、utils.c。后来加需求,改一个地方崩三个功能,最后只能重写。
所以我一直坚持四层架构:
┌─────────────────────────────────────┐
│ 应用层 (App Layer) │
│ 用户界面、报警管理、事件记录、网络服务 │
├─────────────────────────────────────┤
│ 业务层 (Service Layer) │
│ 心率计算、心律失常检测、数据缓存管理 │
├─────────────────────────────────────┤
│ 驱动抽象层 (HAL) │
│ ADC驱动、LCD驱动、SD卡驱动、Wi-Fi驱动 │
├─────────────────────────────────────┤
│ 硬件层 (Hardware) │
│ MCU、心电前端、LCD、SD卡、Wi-Fi模块 │
└─────────────────────────────────────┘
每一层只跟相邻层打交道。应用层不直接操作寄存器,驱动层不关心业务逻辑。这样改一层,其他层基本不用动。
举个例子:
假设我们要把LCD从7寸换成5寸,分辨率变了。在分层架构下,我只需要改驱动抽象层里的LCD驱动,把初始化参数和绘图函数改一下。应用层里的波形绘制代码完全不用动——它调用的还是LCD_DrawLine()、LCD_FillRect()这些接口。
💡 我的小技巧: 驱动抽象层的接口命名,我习惯用 HAL_模块名_功能() 的格式。比如 HAL_ADC_Start()、HAL_LCD_DisplayWave()。这样一看就知道是哪个模块、干什么用的。团队协作时,这个命名规范能省不少沟通成本。
2.3 数据流与事件流设计
架构搭好了,数据怎么流?事件怎么传?这是整个系统能不能跑稳的关键。
先看数据流。心电数据从采集到显示,经过的路径是这样的:
- 采集:心电前端芯片以250Hz~500Hz的采样率,把模拟信号转成数字信号
- 预处理:MCU通过SPI/DMA读取数据,做50Hz工频陷波、基线漂移滤波
- 分析:滤波后的数据送入心率计算模块,同时做心律失常检测
- 缓存:原始数据和计算结果存入环形缓冲区,供显示和事件记录使用
- 显示:UI模块每隔40ms从缓冲区取数据,刷新波形
- 存储:异常事件触发时,将前后各5秒的数据打包,写入SD卡
这里有个坑,我踩过:数据流不能有阻塞。采集线程如果等显示线程画完图才继续采,那采样率就稳不住。我的做法是:采集用DMA+双缓冲,采集线程只管往缓冲区写,显示线程只管从缓冲区读,互不干扰。
再看事件流。事件流处理的是「发生了什么事」,比如:
- 用户按了报警静音键
- 心率超过120bpm
- 导联脱落
- SD卡写满
这些事件怎么传递?我推荐用事件队列。任何模块产生事件,就往队列里丢。事件处理线程从队列里取事件,分发给对应的处理函数。
事件结构体长这样:
typedef struct {
uint32_t event_id; // 事件ID,比如 EVT_HR_ALARM
uint32_t timestamp; // 事件发生时间戳
uint8_t priority; // 优先级:0紧急,1高,2普通
uint8_t source; // 来源模块:0采集,1分析,2UI
union {
uint32_t value; // 数值型数据,比如心率值
char str[32]; // 字符串型数据,比如错误描述
} data;
} Event_t;
🔑 核心原则: 事件处理必须快。我曾经在事件处理函数里写了个SD卡写入操作,结果卡了200ms,导致UI刷新丢帧。后来把所有耗时操作都丢到任务队列里异步处理,事件处理函数只做「记录+通知」,不做「执行」。
最后说说事件记录。心电监护仪有个硬性要求:异常事件必须记录,且不能丢失。我的方案是:
- 事件发生时,先写入环形缓冲区(内存里,速度快)
- 同时启动一个延迟写入任务,500ms后将事件写入SD卡
- 如果SD卡写入失败(比如卡被拔出),事件保留在环形缓冲区,每隔1秒重试一次
- 环形缓冲区满了怎么办?覆盖最旧的事件。但紧急事件(如心脏骤停)不能被覆盖,单独开一个保护区
这个方案我用了好几年,没出过丢事件的问题。说白了就是:内存做缓冲,SD卡做持久化,重试机制兜底。
嗯,第二章就到这儿。硬件选型、软件分层、数据流事件流,这三个东西搭好了,后面的开发就是往这个架子里填代码。下一章我们开始写真正的代码——心电数据采集与预处理。