ADC采样值多模块分发:嵌入式C语言事件驱动与观察者模式实践
发布时间:2026/8/31 2:08:00来源:尧图网络
ADC采样值写到全局变量里然后各个模块自己去读——这是嵌入式开发里最常见的做法也是最容易埋雷的做法。功能确实能跑通但一旦模块变多你会发现ADC采集模块跟显示、控制、日志、告警全部耦合在一起改一处牵全身查一个问题要把所有读这个变量的地方翻一遍。这篇文章讲的是嵌入式软件设计模式系列的第 25 节主题非常具体当 ADC 采样值更新时如何让多个接收端同时收到通知。如果用一句话概括核心思路就是“采样一次发布事件各端订阅按需处理”。这套机制的本质是观察者模式Observer Pattern但我不会停留在概念层面而是直接给出可以搬进项目的 C 语言实现并拆解注册、发布、回调、中断上下文等关键细节。读完这篇文章你能解决三类问题多个模块都要用同一个 ADC 采样值代码却越写越乱想解耦采集端和消费端但不知道用什么模式落地不知道在中断里发布事件、调用回调有哪些坑以及怎么绕过。1. 这篇文章真正要解决的问题先还原一个真实场景。你负责一块主控板板上有一个 ADC 通道采集的是电源电压。这个电压值至少有四个地方要用OLED 显示模块要实时刷新电压数值控制逻辑要根据电压做阈值判断比如低于 10.8V 进入保护日志模块要把电压记录到 Flash方便后期分析告警模块要在电压异常时点亮指示灯或者触发蜂鸣器。最直白的写法是定义一个全局变量g_adc_valueADC 转换完成后更新它然后每个模块在自己的任务里去读。代码少跑起来也快但它有几个非常明显的隐患。第一数据竞争问题。如果 ADC 在中断里更新全局变量而显示任务或控制任务在主循环里读它编译器可能会优化掉重读逻辑或者出现读到“一半数据”的情况。第二耦合问题。ADC 采集模块并不关心这个值被谁用了、用了几次但全局变量这种写法要求每个使用方都“知道”谁写了这个值。一旦接收端增加你要在很多文件里增加对全局变量的引用。第三时序问题。不同模块对数据的时效性要求不一样控制模块需要最新值显示模块可能只需要每秒刷新几次日志模块更希望拿到的是“一段时间内的曲线”。全局变量解决不了这种差异化需求。所以真正要解决的问题不是“怎么让四个模块都能读到 ADC 值”而是“怎么让采集端和消费端彻底解耦同时又能保证每个消费端在数据更新时获得通知”。这就引出了观察者模式。2. “ADC 一更新4 端收到”一个 ADC 采样值的四个归宿先说清楚“一更新 4 端收到”的业务含义。在 ADC 场景里生产者和消费者的关系非常典型。ADC 外设是生产者经过初始化、触发、采样、转换完成之后产生一批数据。消费者有很多个它们各自对数据有不同的处理方式但都不想干预采样过程也不希望采样过程反过来依赖它们。我来拆解一下这四个接收端的实际诉求。端一显示模块。它关心的是电压的“可读性”需要把原始 ADC 值换算成实际电压再格式化成字符串送到屏幕。它不要求每个转换周期都刷新反而希望数据稳定一点否则屏幕上数字会一直跳动。端二控制模块。它关心的是电压的“趋势和边界”比如低于阈值立即切换继电器。它对实时性要求最高不能等显示模块处理完再去拿数据。端三日志模块。它关心的是“历史规律”会把采样值写入环形缓冲区定期落盘。它自己会做二次滤波所以不希望收到过于频繁的原始数据。端四告警模块。它关心的是“异常状态”比如连续三次低于阈值才告警避免瞬时抖动误报。这四个模块对同一份 ADC 数据的处理节奏、频率、关注点完全不同。如果用传统的“全局变量 标志位”方案怎么协调你会写出很多if (adc_flag)的代码还要为每个模块单独设计节流逻辑。而用观察者模式采集端只需要做一件事发布“ADC 更新”事件。至于谁订阅、谁收到之后做什么、做了多久都是订阅者自己的事。这就是“一更新 4 端收到”的真正价值一份数据多个消费者各自处理互不阻塞。下面用一张表对比传统方案和观察者模式方案的区别。对比维度全局变量 轮询观察者模式 事件发布采集端与消费端耦合高消费端必须知道全局变量低采集端只发布事件新增一个接收端要改采集端和主循环只需注册一个订阅者数据时效性统一交付无法差异化发布后立即通知订阅者自行决定消费时机中断上下文使用中断只写变量轮询在读需要 volatile中断发布事件订阅回调收到后决定放队列还是直接用代码可读性逻辑散落在各处每个订阅者一个回调文件职责清晰3. 基础概念观察者模式在嵌入式 C 中的落地形态观察者模式的定义很简单定义对象之间的一对多依赖关系当一个对象状态改变时所有依赖它的对象都会得到通知并自动更新。但这个定义放在嵌入式 C 里至少有两个地方需要重新理解。第一这里没有“对象”只有“结构体”和“函数指针”。在 C 语言里实现观察者模式最自然的载体就是函数指针数组。每个订阅者提供一个回调函数事件发布时遍历数组调用所有订阅者的回调。第二嵌入式系统有中断上下文和任务上下文的区别。这在纯 PC 或 Web 开发里几乎不用考虑但在单片机里非常关键。ADC 转换完成回调往往发生在中断上下文在这个上下文里直接调用一堆订阅者的回调稍有不慎就会导致系统崩溃或实时性恶化。所以嵌入式 C 的观察者模式通常可以做两种变形同步发布发布时直接遍历订阅者逐个调用回调。适用于回调代码短小、不阻塞的情况。异步发布发布时把事件投递到队列由专门的事件处理任务在任务上下文中调用回调。适用于消费端代码较重、逻辑较复杂的场景。这两种方式在后面的代码里都会体现。还要注意观察者模式和 RTOS 消息队列的区别。消息队列是“数据复制 任务间通信”观察者模式是“通知 回调”。前者偏重数据传输后者偏重行为分发。实际项目中完全可以配合使用ADC 中断里发布事件订阅者收到事件后把数据拷贝到消息队列发给某个任务。另外观察者模式也有适用边界。如果系统里只有一个消费方引入事件注册和分发是多余的。如果消费方数量经常变化而且彼此逻辑差异很大观察者模式就很合适。4. 环境准备与前置条件核心代码可以跑在任意支持 C99 的编译环境下不绑定具体芯片。不过为了让示例更贴近真实 ADC 开发我会把 ADC 采集部分写成 STM32 HAL 库风格因为这类 API 在工程里最常见网上资料也最全。如果你用的是 NXP S32K、瑞萨 RA、TI MSPM0 或者 AUTOSAR MCAL代码里的事件注册和分发部分可以完全复用只需要把 ADC 回调来源替换成对应平台的驱动。建议的环境如下编译环境GCC / Keil MDK / IAR任选语言标准C99 或以上芯片平台STM32、GD32、NXP S32K、小华、极海等任意带 ADC 的 MCU驱动方式HAL 库 / SDK / MCALAPI 以你的芯片官方驱动为准调试工具串口、调试器、示波器、逻辑分析仪接下来的代码分为两部分event_bus.c/event_bus.h通用事件注册、发布模块不依赖芯片adc_app.c/display_subscriber.c/control_subscriber.c/log_subscriber.c/alarm_subscriber.cADC 采集和四个订阅者演示完整接入流程。代码量不大但足够跑通“一更新 4 端收到”的完整链路。5. 核心流程拆解与完整代码实现整个流程分四步订阅者注册、ADC 采样完成、事件发布、回调分发。下面先给出通用的事件总线模块。5.1 事件总线通用模块创建文件event_bus.h。/** * 文件event_bus.h * 说明轻量级事件注册与发布模块不依赖具体硬件 */ #ifndef EVENT_BUS_H #define EVENT_BUS_H #include stdint.h #include stddef.h /* 事件 ID 定义 */ #define EVENT_ADC_UPDATED 0x01 /* 事件回调函数原型 * data 指向事件携带的数据len 为数据长度 */ typedef void (*event_handler_t)(uint32_t event_id, void *data, size_t len); /* 订阅者注册表项 */ typedef struct { uint8_t active; /* 1 表示有效0 表示空闲 */ uint32_t event_id; /* 订阅的事件 ID */ event_handler_t handler; /* 回调函数指针 */ } subscriber_t; void event_bus_init(void); int event_subscribe(uint32_t event_id, event_handler_t handler); int event_publish(uint32_t event_id, void *data, size_t len); #endif /* EVENT_BUS_H */创建文件event_bus.c。/** * 文件event_bus.c * 说明事件注册表采用静态数组实现避免动态内存分配 */ #include event_bus.h #define MAX_SUBSCRIBERS 8 static subscriber_t s_subscribers[MAX_SUBSCRIBERS]; static int s_subscriber_count 0; void event_bus_init(void) { for (int i 0; i MAX_SUBSCRIBERS; i) { s_subscribers[i].active 0; s_subscribers[i].event_id 0; s_subscribers[i].handler NULL; } s_subscriber_count 0; } int event_subscribe(uint32_t event_id, event_handler_t handler) { if (handler NULL || s_subscriber_count MAX_SUBSCRIBERS) { return -1; } for (int i 0; i s_subscriber_count; i) { if (s_subscribers[i].active s_subscribers[i].event_id event_id s_subscribers[i].handler handler) { return 0; /* 重复注册直接忽略 */ } } s_subscribers[s_subscriber_count].active 1; s_subscribers[s_subscriber_count].event_id event_id; s_subscribers[s_subscriber_count].handler handler; s_subscriber_count; return 0; } int event_publish(uint32_t event_id, void *data, size_t len) { for (int i 0; i s_subscriber_count; i) { if (s_subscribers[i].active s_subscribers[i].event_id event_id) { s_subscribers[i].handler(event_id, data, len); } } return 0; }这里需要注意几个设计选择。注册表使用静态数组最大支持 8 个订阅者。对大多数 MCU 项目足够而且避免了动态内存分配带来的不确定性和碎片问题。event_subscribe做了一次重复注册检查。如果同一个回调函数重复订阅同一个事件第二次调用直接返回 0不增加新表项。这个细节在工程里很重要因为很多启动代码会在多个初始化函数里不小心重复注册同一个消费者。event_publish本身是遍历调用这段代码如果被放到中断里需要确保所有订阅者的回调都足够短、不阻塞、不做复杂计算。否则系统实时性会被拖垮。5.2 定义 ADC 数据结构ADC 采样完成后我们希望把通道号、原始值、换算后的电压值一起传给订阅者。创建一个adc_types.h。/** * 文件adc_types.h * 说明ADC 数据结构定义 */ #ifndef ADC_TYPES_H #define ADC_TYPES_H #include stdint.h typedef struct { uint8_t channel; /* ADC 通道号 */ uint16_t raw_value; /* 原始采样值 */ int32_t millivolt; /* 换算后的电压值单位 mV */ } adc_sample_t; #endif /* ADC_TYPES_H */5.3 四个订阅者实现分别创建四个订阅者文件每个文件只负责一个模块的逻辑。这样代码结构非常清晰ADC 采集端不需要知道谁订阅了订阅者之间也互不干扰。实现显示订阅者文件display_subscriber.c。/** * 文件display_subscriber.c * 说明显示模块订阅者负责刷新 LCD/OLED 上的电压值 */ #include event_bus.h #include adc_types.h /* 显示刷新节流计数 */ static uint16_t s_display_counter 0; static void on_adc_updated(uint32_t event_id, void *data, size_t len) { adc_sample_t *sample (adc_sample_t *)data; size_t expected_len sizeof(adc_sample_t); if (data NULL || len ! expected_len) { return; } /* 每 20 次采样刷新一次显示减少屏幕闪烁 */ s_display_counter; if (s_display_counter 20) { s_display_counter 0; /* 这里调用你的屏幕刷新函数例如 * lcd_show_voltage(sample-channel, sample-millivolt); */ } } void display_subscriber_register(void) { event_subscribe(EVENT_ADC_UPDATED, on_adc_updated); }实现控制订阅者文件control_subscriber.c。/** * 文件control_subscriber.c * 说明控制模块订阅者负责电压阈值判断与继电器控制 */ #include event_bus.h #include adc_types.h #define LOW_VOLTAGE_THRESHOLD_MV 10800 #define HIGH_VOLTAGE_THRESHOLD_MV 14500 static void on_adc_updated(uint32_t event_id, void *data, size_t len) { adc_sample_t *sample (adc_sample_t *)data; size_t expected_len sizeof(adc_sample_t); if (data NULL || len ! expected_len) { return; } if (sample-millivolt LOW_VOLTAGE_THRESHOLD_MV) { /* 欠压保护逻辑例如 * relay_off(RELAY_LOAD); * control_set_state(STATE_UNDER_VOLTAGE); */ } else if (sample-millivolt HIGH_VOLTAGE_THRESHOLD_MV) { /* 过压保护逻辑 */ } else { /* 正常区间按需处理 */ } } void control_subscriber_register(void) { event_subscribe(EVENT_ADC_UPDATED, on_adc_updated); }实现日志订阅者文件log_subscriber.c。/** * 文件log_subscriber.c * 说明日志模块订阅者负责采样数据入环形缓冲和落盘 */ #include event_bus.h #include adc_types.h #define LOG_BUFFER_SIZE 64 static uint16_t s_log_buf[LOG_BUFFER_SIZE]; static uint8_t s_log_index 0; static uint8_t s_log_count 0; static void on_adc_updated(uint32_t event_id, void *data, size_t len) { adc_sample_t *sample (adc_sample_t *)data; size_t expected_len sizeof(adc_sample_t); if (data NULL || len ! expected_len) { return; } s_log_buf[s_log_index] sample-raw_value; s_log_index (s_log_index 1) % LOG_BUFFER_SIZE; if (s_log_count LOG_BUFFER_SIZE) { s_log_count; } } void log_subscriber_register(void) { event_subscribe(EVENT_ADC_UPDATED, on_adc_updated); }实现告警订阅者文件alarm_subscriber.c。/** * 文件alarm_subscriber.c * 说明告警模块订阅者连续多次超限才触发告警避免抖动误报 */ #include event_bus.h #include adc_types.h #define ALARM_THRESHOLD_MV 17500 #define ALARM_CONTINUOUS_NUM 3 static uint8_t s_over_count 0; static void on_adc_updated(uint32_t event_id, void *data, size_t len) { adc_sample_t *sample (adc_sample_t *)data; size_t expected_len sizeof(adc_sample_t); if (data NULL || len ! expected_len) { return; } if (sample-millivolt ALARM_THRESHOLD_MV) { s_over_count; if (s_over_count ALARM_CONTINUOUS_NUM) { /* 触发告警例如 * led_on(LED_RED); * buzzer_on(); */ } } else { s_over_count 0; /* 告警恢复逻辑 */ } } void alarm_subscriber_register(void) { event_subscribe(EVENT_ADC_UPDATED, on_adc_updated); }四个订阅者的代码风格一致先做参数检查再处理各自逻辑。这种“每个订阅者一个文件”的组织方式新增一个接收端几乎不需要改动已有模块。5.4 ADC 采集端发布事件采集端放在一个专门文件adc_app.c里。这里以 STM32 HAL 库为例演示写法但需要说明的是ADC 配置方式和中断回调名称以你使用的芯片 SDK 为准。/** * 文件adc_app.c * 说明ADC 采集与事件发布。 * 以 STM32 HAL 库风格为例其他芯片平台请替换为对应 SDK 的回调。 */ #include event_bus.h #include adc_types.h #define ADC_SAMPLE_NUM 8 static uint16_t s_adc_buf[ADC_SAMPLE_NUM]; /* 由芯片 ADC 转换完成中断调用函数名以实际 SDK 为准 */ void adc_conv_complete_isr(uint32_t channel) { adc_sample_t sample; uint32_t sum 0; for (int i 0; i ADC_SAMPLE_NUM; i) { sum s_adc_buf[i]; } sample.channel (uint8_t)channel; sample.raw_value (uint16_t)(sum / ADC_SAMPLE_NUM); sample.millivolt adc_convert_millivolt(sample.raw_value); /* 发布 ADC 更新事件通知所有订阅者 */ event_publish(EVENT_ADC_UPDATED, sample, sizeof(sample)); } /* 启动 ADC 采样可以放到初始化阶段调用 */ void adc_app_start(void) { /* 启动 ADC DMA 或中断采集例如 * HAL_ADC_Start_DMA(hadc1, (uint32_t *)s_adc_buf, ADC_SAMPLE_NUM); */ }这段代码里adc_convert_millivolt把原始 ADC 值换算成电压换算公式是 ADC 电压 ADC 值 × 参考电压 / 满量程。具体实现依赖芯片的参考电压和 ADC 位数例如 12 位 ADC 满量程是 4095参考电压 3.3V那么关系就写为int32_t adc_convert_millivolt(uint16_t raw) { /* 以 12 位 ADC、参考电压 3300mV 为例 */ return (int32_t)raw * 3300 / 4095; }真实项目里如果使用了分压电路还要在这个函数里补上分压比。这部分是纯计算不依赖芯片可以单独放到adc_calc.c里做单元测试。5.5 主流程组装创建main.c的核心流程把所有模块串起来。/** * 文件main.c * 说明演示“ADC 一更新4 端收到”的主流程 */ #include event_bus.h #include adc_app.h void rtc_display_subscriber_register(void); void control_subscriber_register(void); void log_subscriber_register(void); void alarm_subscriber_register(void); int main(void) { /* 初始化硬件具体函数以平台为准 */ board_init(); /* 初始化事件总线必须在所有订阅者注册之前执行 */ event_bus_init(); /* 注册四个订阅者 */ display_subscriber_register(); control_subscriber_register(); log_subscriber_register(); alarm_subscriber_register(); /* 启动 ADC 采样 */ adc_app_start(); while (1) { /* 主循环任务看门狗、低功耗管理、命令处理等 */ } }如果你使用的是带 RTOS 的系统也可以把订阅者注册放到各个任务的初始化阶段回调里改用消息队列把数据投递给任务这样实时性会更容易控制。6. 运行结果与效果验证把代码编译下载到开发板上需要验证的核心点有三个订阅者注册是否成功、ADC 更新能否触发所有订阅者回调、四个接收端的处理是否互不干扰。建议先用串口日志辅助验证。在on_adc_updated中临时增加日志输出例如static void on_adc_updated(uint32_t event_id, void *data, size_t len) { adc_sample_t *sample (adc_sample_t *)data; printf([LOG] ch%d, raw%d, mv%d\r\n, sample-channel, sample-raw_value, sample-millivolt); }预期串口输出类似下面的效果[LOG] ch0, raw2048, mv1651 [LOG] ch0, raw2050, mv1653 [LOG] ch0, raw2047, mv1650 [LOG] ch0, raw2049, mv1652如果能看到持续输出说明 ADC 采集和事件发布链路已经通了。至于四端各自的逻辑可以通过打点观察显示端屏幕电压值每 20 次采样刷新一次控制端把电压降到阈值以下继电器动作日志端环形缓冲区的数据持续增长告警端让电压连续三次超过阈值红色告警灯亮起。如果运行后没有任何输出第一步应该检查 ADC 是否真的启动成功。拿到调试器在adc_conv_complete_isr里打断点。如果断点没有命中说明问题在 ADC 外设配置和事件总线无关。如果断点命中了但订阅者回调没有执行检查event_bus_init是否在event_subscribe之前调用这是最容易犯的错误。还可以做一个简单的压力测试把 ADC 采样频率调高到 10kHz 以上观察系统是否出现卡顿。如果回调逻辑非常轻量应该不会卡顿如果出现卡顿就说明你仍然在中断里做了太多事情需要引入异步发布方案。7. 常见问题与排查思路观察者模式在嵌入式场景落地时真正容易出问题的地方往往不是模式本身而是 C 语言和中断上下文带来的边界情况。问题现象可能原因排查方式解决方案订阅者回调没有被执行event_bus_init被放在注册后调用注册表被清空在初始化处设置断点检查注册顺序先event_bus_init再event_subscribe同一个回调被调用多次订阅者注册了多次代码里没有去重检查查看订阅者注册表内容注册前检查是否已存在相同 handler显示端数值跳动严重每次采样都刷新显示没有节流查看显示刷新频率加节流计数每隔 N 次刷新一次中断里发布事件导致系统卡顿某个订阅者回调里有阻塞操作用调试器查看中断耗时回调里只做标记或用消息队列异步处理回调里操作了非线程安全的资源多个上下文同时访问同一变量查看回调代码中对全局变量的访问加临界区保护或把数据处理移出回调ADC 数据偶尔出现跳变没有做滤波或 DMA 缓冲区竞争打印原始值和滤波值对比使用中值滤波、滑动平均滤波订阅者数量超过限制注册表数组太小检查注册表是否存在空槽调大MAX_SUBSCRIBERS或改用动态列表其中“中断里发布事件导致系统卡顿”是最值得展开的一条。事件发布本身耗时很短就是一个 for 循环调用几个函数指针。但如果某个订阅者的回调函数里有printf或者 Flash 写入那么中断就会被无限拉长。解决思路是“发布不变、消费异步”回调里只做一件事把数据放进一个轻量的队列或者设置一个标志位真正耗时的处理放在后台任务里。这样发布者的耗时只取决于赋值操作和消费端逻辑完全无关。另外volatile关键字在 ADC 中断更新数据时很重要。当 ADC 的中断回调更新一个共享变量而主循环读取它时建议在变量类型前加volatile避免编译器过度优化导致读不到最新值。在事件总线模块里s_subscribers数组只在初始化阶段写运行阶段只读加上volatile的意义不大但如果采集中断里直接改s_subscriber_count就要注意访问竞争了。8. 最佳实践与工程建议到这里模式能跑通了但“能跑通”和“能上线”之间还有一段距离。下面这些建议来自嵌入式项目工程化的经验。第一订阅者的回调函数要遵守“短小精悍”原则。回调应该只做数据快照、状态置位、投递队列这类轻量操作。真正耗时的数据处理包括屏幕绘制、日志落盘、复杂计算放到任务上下文去做。这条原则在中断上下文发布事件时尤其重要。第二把回调参数里的数据拷贝出来而不是持有指针。上面的例子里发布者用的是栈上变量sample回调执行期间这个变量是有效的。但如果你在回调里把data指针保存下来后面再用就会指向已经失效的栈地址。更安全的做法是在回调开头把adc_sample_t拷贝到自己的缓冲区。第三事件 ID 和数据类型的匹配要建立约定。事件总线的本质是弱类型编译器无法保证EVENT_ADC_UPDATED携带的数据确实是adc_sample_t。建议建立一张事件定义表写明每个事件 ID 对应的数据类型代码评审时逐项核对。如果团队规模大可以在event_bus.h里用宏或注释明确约定。第四考虑用队列做异步投递。当你的消费端是 RTOS 任务时比较推荐“中断发布 队列投递 任务处理”的组合。ADC 中断只做两件事把数据拷贝进队列发出一个事件通知。任务侧阻塞等待事件数据一到就处理。这样既保证了实时性又避免了中断堆积。第五ADC 数值的滤波放到采集端而不是每个订阅者都做一遍。上例子里面采集端做了ADC_SAMPLE_NUM8次平均四个订阅者拿到的就是已经平滑过的数据。如果每个订阅者都自己做各自的滤波策略反而会出现显示端、控制端看到的数据不一致。滤波策略应该尽量统一收敛在采集端。第六警惕回调顺序带来的隐含依赖。事件发布按注册顺序调用订阅者回调你是无法保证“控制模块先于显示模块”的。如果两个订阅者之间存在先后依赖说明它们本身就不该是平行订阅关系应该把这种依赖逻辑放到采集端或者合并成一个订阅者后再分发。第七做好命名规范。我见过不少嵌入式项目事件订阅者回调命名为on_xxx发布接口命名为publish_xxx订阅注册命名为xxx_subscriber_register。这种命名能让你在 3 个月后回看代码时一眼就看清某个函数是干嘛的。团队协作时建议在 README 里列出一张“事件 ID 与订阅者清单”方便新人快速上手。第八考虑把事件总线替换成更完整的消息框架。本文实现的是一个最小可用的事件总线适合大多数 MCU 项目。如果业务继续膨胀出现了几十种事件、几十个订阅者可以考虑引入更完整的消息框架比如以发布订阅为核心的消息分发组件甚至带有 QoS 保证的消息队列。但前提是把基础的事件定义和订阅关系理清楚否则框架只会放大混乱。9. 总结与后续学习方向ADC 采样值的分发问题表面上是数据流动的问题本质上却是架构耦合的问题。从全局变量到观察者模式代码量没有增加太多但模块之间的依赖关系被彻底扭转了采集端不再关心数据被谁使用消费端也不需要知道采样具体怎么发生。这种“发布一次、多方订阅”的思路同样适用于按键事件、传感器数据、网络消息、状态机迁移等场景。这篇文章里的三个关键点建议你收藏事件总线用静态数组实现注册表去重发布时遍历订阅者回调中断上下文发布事件时订阅者回调必须短小耗时操作放到任务中ADC 滤波在采集端做统一处理四个接收端拿到的数据口径一致。下一步可以这样继续深入先把代码移植到你手头的开发板确认串口输出正常然后尝试把“同步发布”改成“异步队列发布”观察系统在 10kHz 采样频率下的表现再往后可以研究一下 AUTOSAR MCAL 里 ADC 驱动的事件通知机制你会发现底层思路和这里讲的事件总线非常一致。最后提醒一句代码里用到的 ADC 回调函数名、DMA 配置方式、寄存器细节最终要以你使用的芯片和 SDK 的实际接口为准。这套事件总线的思路可以跨平台复用但外设层的实现一定要和芯片手册对齐。
网站建设高端定制企业官网