ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

嵌入式驱动工厂模式:接口+实现+工厂三件套

嵌入式驱动工厂模式:接口+实现+工厂三件套 上一篇聊数组和链表选型有朋友私信问业务代码里到处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_flash或e2prom它只知道有个存储设备能 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。用工厂模式后业务代码一行不用改只在初始化时传不同的StorageType。log_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语言面向对象
返回列表