第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 数据流与事件流设计

架构搭好了,数据怎么流?事件怎么传?这是整个系统能不能跑稳的关键。

先看数据流。心电数据从采集到显示,经过的路径是这样的:

  1. 采集:心电前端芯片以250Hz~500Hz的采样率,把模拟信号转成数字信号
  2. 预处理:MCU通过SPI/DMA读取数据,做50Hz工频陷波、基线漂移滤波
  3. 分析:滤波后的数据送入心率计算模块,同时做心律失常检测
  4. 缓存:原始数据和计算结果存入环形缓冲区,供显示和事件记录使用
  5. 显示:UI模块每隔40ms从缓冲区取数据,刷新波形
  6. 存储:异常事件触发时,将前后各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卡做持久化,重试机制兜底

嗯,第二章就到这儿。硬件选型、软件分层、数据流事件流,这三个东西搭好了,后面的开发就是往这个架子里填代码。下一章我们开始写真正的代码——心电数据采集与预处理。