一、什么是单例模式?
1. 定义与核心思想
单例模式(Singleton Pattern)是一种创建型设计模式,核心目标是确保一个类在系统中仅有一个实例,并提供全局访问点。在嵌入式开发中,这一模式常用于管理硬件资源或全局状态,避免因多次初始化或并发操作导致的资源冲突。
2. 嵌入式中的典型特征
- 唯一性:如 UART 串口控制器,多任务同时操作会引发数据混乱。
- 资源节约:避免重复初始化占用额外内存(尤其在 RAM 稀缺的 MCU 中)。
- 线程安全:需通过锁或关中断保护临界区(例如 RTOS 中的任务竞争)。
二、单例模式的作用与应用场景
1. 四大核心作用
- 统一管理硬件外设:如 SPI Flash、ADC 模块的全局访问。
- 减少内存开销:避免重复创建对象(适合资源有限的 MCU)。
- 防止状态不一致:确保配置参数(如校准值)全局唯一。
- 简化跨模块调用:日志系统、任务调度器等无需传递实例。
2. 六大嵌入式应用场景
| 场景 |
示例 |
风险规避 |
| 硬件控制器 |
I2C 总线驱动、PWM 发生器 |
避免多任务操作时序冲突 |
| 系统配置管理 |
设备序列号、网络参数 |
防止配置被意外覆盖 |
| 日志记录器 |
通过 UART 或 Flash 写入日志 |
确保日志文件原子写入 |
| 中断服务管理器 |
统一管理外部中断优先级 |
防止中断嵌套导致死锁 |
| 电源管理模块 |
休眠唤醒策略、低功耗模式 |
统一控制电源状态 |
| 数据缓存池 |
传感器数据环形缓冲区 |
避免多任务读写越界 |
三、手把手用 C语言 实现单例模式
1. 裸机环境实现(关中断保护)
#include <stdint.h>
#include "stm32g0xx_hal.h" // 以STM32G0系列为例
// 单例对象:模拟SPI控制器
typedef struct {
uint32_t spi_speed; // SPI时钟频率
uint8_t tx_buffer[64]; // 发送缓冲区
} SPIController;
static SPIController instance; // 静态实例(编译期分配内存)
static volatile uint8_t initialized = 0; // volatile防止编译器优化
// 获取SPI控制器实例(关中断保护原子操作)
SPIController* get_spi_controller(void){
if (!initialized) {
__disable_irq(); // 关中断
if (!initialized) { // 双重检查锁定
instance.spi_speed = 1000000; // 默认1MHz
HAL_SPI_Init(&hspi1); // 初始化硬件SPI(需提前配置hspi1)
initialized = 1;
}
__enable_irq();
}
return &instance;
}
// 示例:多任务调用SPI发送数据
void task_send_data(void){
SPIController* spi = get_spi_controller();
HAL_SPI_Transmit(&hspi1, spi->tx_buffer, 64, 100); // 发送数据
}
代码解析
- 静态内存分配:
SPIController instance 直接分配在全局区,避免动态内存风险。
- 关中断保护:裸机环境下通过
__disable_irq() 确保初始化原子性。
那么问题来了:为什么初始化一个单例对象需要关中断?因为在裸机环境中,中断服务函数随时可能打断主流程。如果主流程刚走到 initialized 判断之后、赋值之前,突然被中断打断,而中断处理里又调用了 get_spi_controller(),就会造成重复初始化。双重检查配合关中断,正是为了堵上这道缝隙。这也是为什么 initialized 必须声明为 volatile——编译器可不知道这个变量会在中断里被改写。
如果你正在写单片机程序,又不想在裸机上手工管理这些细节,可以到 云栈社区 上找其他开发者聊聊,看看大家在 RTOS 环境下都是怎么封装单例的。
|