news 2026/5/8 17:26:15

嵌入式 C 的单例模式:把“全局唯一”写得更稳

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
嵌入式 C 的单例模式:把“全局唯一”写得更稳
在嵌入式项目里,有些东西天生就只能有一个:看门狗、RTC、系统时钟、调试串口、日志器、系统配置管理器、CRC 模块……这些模块如果随手用全局变量堆起来,早晚会遇到初始化顺序混乱、到处可写难排查、ISR/任务并发冲撞等问题。 单例模式的目标很简单:把“全局唯一”的资源做成“受控的全局”。唯一入口、明确初始化、约束访问,让系统更稳定、可维护。

什么时候用单例更合适

  • 硬件资源物理唯一:WDT、RTC、SysTick、调试 UART 等。

  • 全局服务:日志器、系统配置/参数加载、告警/事件上报、电源管理。

  • 必须集中控制的状态机:系统模式管理、升级流程、故障保护。

    不适用:需要多个实例并行的模块(多路 UART/I2C/SPI 等)。这类优先“管理器 + 多实例”,别硬做成单例。

在 C 里怎么落地

没有类就别纠结“面向对象”,本质是“受控的全局对象 + 受控的访问入口”。两种常见写法:

  • 饿汉式(推荐):静态对象常驻,启动阶段 init() 一次。简单、可预期、无动态内存,最适合嵌入式。
  • 懒汉式:首次使用时再初始化。路径更复杂,需要考虑并发和中断安全,不建议在 ISR 首次调用。
    实践偏好:大多数底层单例用饿汉式,系统早期就完成初始化;上层服务要懒加载时,注意并发保护和边界。

设计要点(一个小模型)

  • 内部状态:static 模块内可见,全局不导出可写句柄。
  • 外部入口:只提供函数或只读接口表(函数指针集合)。
  • 初始化幂等:init() 可多次调用,真正只生效一次。
  • 并发与 ISR:明确哪些 API 可在中断里用;必要时提供 FromISR 版本。

示例一:饿汉式单例(裸机/RTOS 通用)

以“日志器”举例,底层用一个 UART 发送。避免动态内存,init() 幂等,可选关键段保护。

// log.h#ifndefLOG_H#defineLOG_H#include<stdint.h>#include<stddef.h>voidLog_Init(void);// 幂等voidLog_Write(constchar*s);// 线程安全与否看实现voidLog_WriteHex(constvoid*buf,size_tlen);// 可选:对外只暴露只读接口,减少可写全局的暴露typedefstruct{void(*write)(constchar*);void(*writeHex)(constvoid*,size_t);}LogIface;constLogIface*Log_Get(void);#endif
// log.c#include"log.h"// -- 与平台相关的 HAL(示意,根据项目填充) --staticvoidhal_uart_init(void){// 配置波特率、GPIO、多路复用等}staticvoidhal_uart_write_blocking(constchar*s){// 逐字节写 TX 寄存器并等待发送完成}// -- 关键段(裸机/RTOS 适配) --staticinlinevoidcrit_enter(void){// 裸机:__disable_irq(); RTOS:taskENTER_CRITICAL();}staticinlinevoidcrit_exit(void){// 裸机:__enable_irq(); RTOS:taskEXIT_CRITICAL();}// -- 单例本体 --typedefstruct{volatileintinitialized;// 0 未初始化,1 已初始化}Log_t;staticLog_t g_log={0};// 饿汉式:静态存储期对象voidLog_Init(void){if(g_log.initialized)return;crit_enter();if(!g_log.initialized){// 双检降低竞争开销hal_uart_init();g_log.initialized=1;}crit_exit();}voidLog_Write(constchar*s){if(!g_log.initialized){// 团队约定:静默丢弃或自动初始化,择一Log_Init();}hal_uart_write_blocking(s);}voidLog_WriteHex(constvoid*buf,size_tlen){staticconstchar hex[]="0123456789ABCDEF";constuint8_t*p=(constuint8_t*)buf;charout[3]={0};for(size_ti=0;i<len;++i){out[0]=hex[(p[i]>>4)&0xF];out[1]=hex[p[i]&0xF];hal_uart_write_blocking(out);hal_uart_write_blocking(" ");}}staticconstLogIface kIface={.write=Log_Write,.writeHex=Log_WriteHex,};constLogIface*Log_Get(void){Log_Init();return&kIface;}

用法示意:

intmain(void){// 其他板级初始化...Log_Init();// 明确初始化点Log_Write("boot ok\r\n");constLogIface*log=Log_Get();log->write("run...\r\n");return0;}

要点:

  • g_log 不在头文件暴露,外部只能走 API。
  • 初始化幂等;担心首次路径里关中断,可在系统早期显式 Log_Init()。
  • 裸机关键段保持很短;RTOS 更推荐上电阶段完成初始化。

示例二:懒汉式 + FreeRTOS 串行化发送

多个任务写日志时希望串口访问串行化,可以加互斥量。注意:ISR 不要拿互斥;需提供 FromISR 路径或在 ISR 里仅入队。

// log_rtos.c(片段)#include"FreeRTOS.h"#include"semphr.h"externvoidhal_uart_init(void);externvoidhal_uart_write_blocking(constchar*s);typedefstruct{volatileintinitialized;}Log_t;staticLog_t g_log={0};staticSemaphoreHandle_t s_uartMtx;// 只保护发送,不保护初始化voidLog_Init(void){if(g_log.initialized)return;taskENTER_CRITICAL();if(!g_log.initialized){hal_uart_init();s_uartMtx=xSemaphoreCreateMutex();// 生产环境建议断言或降级处理创建失败g_log.initialized=1;}taskEXIT_CRITICAL();}voidLog_Write(constchar*s){if(!g_log.initialized)Log_Init();if(s_uartMtx){if(xSemaphoreTake(s_uartMtx,portMAX_DELAY)==pdTRUE){hal_uart_write_blocking(s);xSemaphoreGive(s_uartMtx);}}else{hal_uart_write_blocking(s);}}// 如需 ISR 使用:// 1) 提供 Log_WriteFromISR(避免互斥,入队到环形缓冲);// 2) 由后台任务取出串行打印。

并发与中断安全这几件事

  • 初始化尽量别出现在 ISR。需要 ISR 使用的服务,系统早期就初始化好。
  • ISR 不拿互斥。RTOS 场景用 FromISR 入队,任务侧消费。
    ± 裸机关键段要短,不在关键段内做阻塞外设操作。
  • 多个单例的依赖要写清初始化顺序(时钟 -> UART -> 日志器),不要靠“碰运气”。

常见坑与规避

  • 动态内存:尽量避免在底层单例里 malloc,防碎片、可预测。
  • 隐式全局:不要把可写结构体放头文件;对外暴露函数或只读接口表。
  • 可测试性:接口用函数指针表或弱符号,单测时替换 HAL。
  • 可能扩容:若未来可能多路,别把单例写死。做成“管理器 + 默认实例0”,迁移更顺滑。

一个可复用的小模板

// singleton_template.h#ifndefSINGLETON_TEMPLATE_H#defineSINGLETON_TEMPLATE_Htypedefstruct{int(*init)(void);void(*doWork)(intarg);}ServiceIface;voidService_Init(void);constServiceIface*Service_Get(void);#endif
// singleton_template.c#include"singleton_template.h"// 平台相关依赖(示意)staticinthal_dep_init(void){return0;}staticvoidhal_dep_work(intarg){(void)arg;}typedefstruct{volatileintinitialized;}Service_t;staticService_t g_service={0};voidService_Init(void){if(g_service.initialized)return;// 可选:关键段// crit_enter();if(!g_service.initialized){(void)hal_dep_init();g_service.initialized=1;}// crit_exit();}staticvoidService_DoWork(intarg){if(!g_service.initialized)Service_Init();hal_dep_work(arg);}staticconstServiceIface kSvc={.init=Service_Init,.doWork=Service_DoWork,};constServiceIface*Service_Get(void){Service_Init();return&kSvc;}

落地方法:把 HAL 部分换成你的外设/服务实现;把关键段宏替换为项目里的裸机/RTOS 实现即可。

何时不该用单例

  • 需要多个实例(多路外设、多连接、多会话)。
  • 强依赖测试隔离、并发扩展、热插拔等“生命周期复杂”的模块。
  • 业务对象(如每个连接/会话),应有清晰的创建/销毁接口。

小结

  • 单例在嵌入式 C 的价值,是把“全局唯一”变成“可控可测”:唯一入口、明确初始化、约束访问。

  • 优先用饿汉式 + 静态分配;初始化提前到系统早期。

  • 明确线程/中断边界:任务用互斥,ISR 走无锁路径或队列。

  • 预留测试替换点,别把可写全局暴露到头文件。

    把上面的模板拷进项目,替换 HAL 细节,就能快速做出一个“顺手、稳定”的单例模块;后续有多实例需求时,也能平滑升级到“管理器 + 多实例”。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/5/8 17:25:40

SQL约束

数据库基础&#xff1a;SQL 约束 约束&#xff08;Constraint&#xff09;是数据库表设计的核心规则&#xff0c;用于强制保证数据的完整性、准确性和一致性&#xff0c;防止脏数据&#xff08;错误、冗余、矛盾的数据&#xff09;进入数据库。本文详细讲解 MySQL 中五大核心约…

作者头像 李华
网站建设 2026/5/8 17:25:16

Windows终极效率革命:PowerToys完全使用指南

Windows终极效率革命&#xff1a;PowerToys完全使用指南 【免费下载链接】PowerToys Microsoft PowerToys is a collection of utilities that supercharge productivity and customization on Windows 项目地址: https://gitcode.com/GitHub_Trending/po/PowerToys 你是…

作者头像 李华
网站建设 2026/5/8 17:25:16

ComfyUI IPAdapter Plus完整教程:3步掌握AI图像引导生成技术

ComfyUI IPAdapter Plus完整教程&#xff1a;3步掌握AI图像引导生成技术 【免费下载链接】ComfyUI_IPAdapter_plus 项目地址: https://gitcode.com/gh_mirrors/co/ComfyUI_IPAdapter_plus 你是否曾经遇到过这样的问题&#xff1a;用AI生成图像时&#xff0c;无论怎么调…

作者头像 李华
网站建设 2026/5/8 17:24:51

AutoSar FiM模块实战:手把手教你配置诊断事件驱动的功能降级

AutoSar FiM模块实战&#xff1a;从零构建诊断事件驱动的功能降级系统 车窗缓缓上升时突然停止&#xff0c;防夹功能意外触发——这可能是电机温度传感器触发了FiM模块的功能降级机制。在汽车电子系统中&#xff0c;这类看似简单的异常背后&#xff0c;隐藏着Dem与FiM模块精密…

作者头像 李华