嵌入式驱动工厂模式:接口+实现+工厂三件套
2026/8/13 13:08:31 网站建设 项目流程

上一篇聊数组和链表选型,有朋友私信问:业务代码里到处spi_flash_read()spi_flash_write(),换个存储介质就要改一大片,这种耦合怎么破?这问题太典型了,答案是工厂模式,把"创建驱动"和"使用驱动"分开。

工厂模式在应用软件开发里是老面孔,但嵌入式里很多人没用过,觉得"我就一个驱动,搞什么工厂"。真到产品线一铺开,SPI Flash、SDIO Flash、EEPROM 三种存储要兼容,就傻眼了。这篇把嵌入式驱动工厂模式讲透,给个能直接套的模板。

先看痛点:业务代码直接依赖具体驱动

我刚工作时写过一个数据记录模块,长这样:

void log_save(uint32_t addr, const uint8_t *data, uint32_t len) { spi_flash_write(addr, data, len); /* 直接调 SPI Flash 驱动 */ } void log_read(uint32_t addr, uint8_t *buf, uint32_t len) { spi_flash_read(addr, buf, len); }

当时觉得没问题。后来产品要出高低配两个版本:高配用 SPI Flash,低配用 EEPROM。我一改就傻了,log_save/log_read里全是spi_flash_xxx,每个调用点都要改成e2prom_xxx,要么加if (high_version)分支,代码瞬间丑陋。更要命的是,还有个 SDIO Flash 的中配版本在路上。

这就是业务代码直接依赖具体驱动的代价:换硬件就改业务代码,加一种硬件就加一坨分支。业务逻辑(什么时候存、存哪里)和驱动细节(怎么写 Flash)搅在一起,改一个动全身。

解法:抽象一个驱动接口

工厂模式的第一步不是写工厂,是抽象接口。把"存储设备能干什么"抽象出来,业务代码只依赖接口,不依赖具体驱动。

/* 存储设备接口:抽象出所有存储设备共有的操作 */ typedef struct { int (*init)(void); int (*read)(uint32_t addr, uint8_t *buf, uint32_t len); int (*write)(uint32_t addr, const uint8_t *data, uint32_t len); int (*erase)(uint32_t addr, uint32_t len); const char *name; } StorageDevice;

这个结构体就是 C 语言里"接口"的标准写法,一组函数指针。它定义了"存储设备"这个抽象类型该有什么行为,但不关心具体是 SPI Flash 还是 EEPROM。

业务代码改成依赖这个抽象接口:

static const StorageDevice *g_storage = NULL; void log_save(uint32_t addr, const uint8_t *data, uint32_t len) { g_storage->write(addr, data, len); /* 调接口,不调具体驱动 */ } void log_read(uint32_t addr, uint8_t *buf, uint32_t len) { g_storage->read(addr, buf, len); }

注意g_storage->write这一行,业务代码不再认识spi_flashe2prom,它只知道"有个存储设备,能 write"。具体是哪种设备,运行时再定。这就是"依赖抽象,不依赖具体"。

具体驱动实现接口

每种具体驱动实现这个接口,把自己填进函数指针表:

/* SPI Flash 驱动实现 */ static int spi_flash_init(void) { /* 初始化 SPI Flash */ return 0; } static int spi_flash_read(uint32_t addr, uint8_t *buf, uint32_t len) { /* ... */ return 0; } static int spi_flash_write(uint32_t addr, const uint8_t *data, uint32_t len) { /* ... */ return 0; } static int spi_flash_erase(uint32_t addr, uint32_t len) { /* ... */ return 0; } const StorageDevice spi_flash_dev = { .init = spi_flash_init, .read = spi_flash_read, .write = spi_flash_write, .erase = spi_flash_erase, .name = "spi_flash", }; /* EEPROM 驱动实现 */ static int e2prom_init(void) { /* 初始化 EEPROM */ return 0; } static int e2prom_read(uint32_t addr, uint8_t *buf, uint32_t len) { /* ... */ return 0; } static int e2prom_write(uint32_t addr, const uint8_t *data, uint32_t len) { /* ... */ return 0; } static int e2prom_erase(uint32_t addr, uint32_t len) { /* ... */ return 0; } const StorageDevice e2prom_dev = { .init = e2prom_init, .read = e2prom_read, .write = e2prom_write, .erase = e2prom_erase, .name = "e2prom", };

每个驱动就是一个StorageDevice实例,把自己的函数填进去。这其实就是 C 语言的面向对象,结构体当类,函数指针当方法,实例当对象。上一篇讲链表管 GUI 时用过类似手法,这里是同一个思路在不同场景的应用。

工厂函数:根据参数返回不同实例

接口和实现都有了,谁来决定用哪个?这就是工厂函数的活,根据参数,返回对应的驱动实例:

typedef enum { STORAGE_SPI_FLASH, STORAGE_SDIO_FLASH, STORAGE_E2PROM, } StorageType; const StorageDevice *storage_factory(StorageType type) { switch (type) { case STORAGE_SPI_FLASH: return &spi_flash_dev; case STORAGE_SDIO_FLASH: return &sdio_flash_dev; case STORAGE_E2PROM: return &e2prom_dev; default: return NULL; } }

业务代码初始化时调一次工厂,拿到驱动实例,之后就用这个实例:

int log_init(StorageType type) { g_storage = storage_factory(type); if (g_storage == NULL) return -1; return g_storage->init(); }

现在回到开头的痛点:高配 SPI Flash、低配 EEPROM、中配 SDIO Flash。用工厂模式后,业务代码一行不用改,只在初始化时传不同的StorageTypelog_init(STORAGE_SPI_FLASH)跑高配,log_init(STORAGE_E2PROM)跑低配。换硬件、加硬件,全在工厂函数一处搞定。

三个优势,一个比一个值钱

工厂模式在嵌入式驱动里的优势,我列三条,一条比一条值钱。

第一,业务代码不依赖具体驱动。这是基本盘。业务逻辑和驱动细节解耦,换硬件不动业务代码。这一条就值回票价,产品线一铺开,维护成本断崖式下降。

第二,新增驱动类型只改两处。加一种新存储(比如 NAND Flash),只要写一个nand_flash_dev实例实现接口,再在工厂函数加一个case。业务代码、其他驱动,一行不用动。这符合开闭原则,对扩展开放,对修改封闭。在多人协作的项目里,这个特性让新增驱动不会误伤老代码。

第三,便于单元测试 Mock 替换。这条很多人没意识到。业务代码依赖的是StorageDevice接口,测试时可以塞一个 Mock 实现:

/* 测试用 Mock 驱动 */ static int mock_read(uint32_t addr, uint8_t *buf, uint32_t len) { memcpy(buf, &mock_mem[addr], len); /* 从内存读,不碰真硬件 */ return 0; } const StorageDevice mock_dev = { .read = mock_read, /* ... */ }; void test_log_save(void) { g_storage = &mock_dev; /* 替换成 Mock */ log_save(0x100, test_data, 4); assert(memcmp(&mock_mem[0x100], test_data, 4) == 0); }

不用接真硬件就能测业务逻辑。嵌入式单元测试一直是个老大难(硬件依赖重、跑得慢),工厂模式加 Mock 是破局的关键之一。我见过不少团队嵌入式代码零测试,理由是"没法跑硬件",其实业务逻辑层完全可以用 Mock 测起来,只是架构上没留接口。

落地建议

第一,接口别设计太大。只放真正共有的操作,别为了统一硬塞。比如 EEPROM 没有"擦除"概念(按字节写),接口里硬加erase就得让 EEPROM 实现个空函数,污染抽象。这种差异要么接口分两层,要么用可选函数指针(NULL 表示不支持,调用前判断)。

第二,工厂函数用 switch 没问题,别被"switch 是坏味道"洗脑。嵌入式里驱动类型有限、编译期基本确定,switch 直白高效。真要动态注册(插件式),再用链表注册表,那是过度设计。

第三,实例用静态全局变量(const StorageDevice spi_flash_dev),不要每次工厂调用都 malloc。驱动实例是编译期就确定的单例,静态分配即可,省去内存管理,也避免碎片。

第四,工厂可以分层。storage_factory选存储类型,sensor_factory选传感器类型,display_factory选显示类型。每类硬件一个工厂,别一个工厂管所有。

这套模式不挑 MCU,不挑 RTOS,核心就是"接口 + 实现 + 工厂"三件套。它真正解决的是"硬件多样性和业务逻辑稳定性之间的矛盾",业务逻辑要稳,硬件要灵活换,工厂模式用一层抽象把它们隔开。在产品线多、硬件版本多的项目里,这是必修课。

下一篇讲应用层感知底层变化的三种姿势:轮询、回调、观察者,把驱动和应用之间的另一条线也理清。

有用的话点个在看,让更多被驱动耦合折磨的嵌入式工程师看到。


标签:嵌入式 工厂模式 驱动设计 C语言面向对象

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询