C 语言面向对象编程实例解析选自 OnMicro OM6626 BLE SDK 中的 DFUDevice Firmware Upgrade模块。适合有一定 C 基础、想理解如何在 C 中实现面向对象的初级工程师。一、先看最终效果调用方完全不关心底层实现在 onmicro_dfu.c 中固件升级的核心逻辑是这样写存储操作的// 获取某个存储后端的接口指针constdfu_nvds_itf_t*flash_itfdfu_nvds_itf[DFU_NVDS_ITF_TYPE_FLASH];// 像调用对象方法一样调用flash_itf-enable();flash_itf-get(addr,len,buf);flash_itf-put(addr,len,buf);flash_itf-del(addr,len);flash_itf-disable();不同之处只是数组索引不同——DFU_NVDS_ITF_TYPE_MBR、DFU_NVDS_ITF_TYPE_DUMMY、DFU_NVDS_ITF_TYPE_FLASH_EXT。同一个.get()调用底层可以是读内部 Flash、读 MBR 分区表、甚至返回全0xFF的假数据。这就是多态。二、第一步定义接口函数指针表文件onmicro_dfu_nvds.h// 这就是 C 语言中的接口——一个全是函数指针的结构体typedefstruct{uint8_t(*enable)(void);// 初始化uint8_t(*get)(uint32_tid,uint32_t*lengthPtr,void*buf);// 读取uint8_t(*put)(uint32_tid,uint32_tlength,void*buf);// 写入uint8_t(*del)(uint32_tid,uint32_tlength);// 擦除/删除uint8_t(*disable)(void);// 反初始化}dfu_nvds_itf_t;对应关系C函数指针表C类dfu_nvds_itf_t抽象基类 / 接口结构体内的 5 个函数指针5 个纯虚函数实现文件里创建的具体实例派生类 虚表为什么每个函数参数里没有this指针因为这个项目里每个实现的后端是全局唯一的只有一个内部 Flash、一个 MBR 分区表不需要区分实例。更完善的写法会把第一个参数设计为void *self。三、第二步实现类提供具体函数填表注册文件onmicro_dfu_nvds.c3.1 Flash 后端的实现// 每个函数对标接口中的一个函数指针staticuint8_tonmicro_dfu_nvds_enable_flash(void){drv_sfs_enable();// 使能内部 Flash 控制器returnONMICRO_DFU_NVDS_ST_SUCCESS;}staticuint8_tonmicro_dfu_nvds_get_flash(uint32_taddr,uint32_t*lengthPtr,void*buf){drv_sfs_read(addr,buf,*lengthPtr);// 调用 Flash 驱动读returnONMICRO_DFU_NVDS_ST_SUCCESS;}staticuint8_tonmicro_dfu_nvds_put_flash(uint32_taddr,uint32_tlength,void*buf){drv_sfs_write(addr,buf,length);// 调用 Flash 驱动写returnONMICRO_DFU_NVDS_ST_SUCCESS;}staticuint8_tonmicro_dfu_nvds_erase_flash(uint32_taddr,uint32_tlength){drv_sfs_erase(addr,length);// 调用 Flash 驱动擦除returnONMICRO_DFU_NVDS_ST_SUCCESS;}staticuint8_tonmicro_dfu_nvds_disable_flash(void){returnONMICRO_DFU_NVDS_ST_SUCCESS;}3.2 MBR分区表后端的实现staticuint8_tonmicro_dfu_nvds_get_mbr(uint32_tid,uint32_t*lengthPtr,void*buf){dfu_image_mbr_info*info(dfu_image_mbr_info*)buf;// 调用 MBR 库读取分区信息if(idsizeof(mbr_types)/sizeof(mbr_types[0])mbr_read_part(mbr_types[id],info-address,info-length,info-crc16)0){returnONMICRO_DFU_NVDS_ST_SUCCESS;}else{returnONMICRO_DFU_NVDS_ST_FAILED;}}// ... 其余 4 个函数同理3.3 Dummy 后端的实现占位 / 测试用staticuint8_tonmicro_dfu_nvds_get_dummy(uint32_tid,uint32_t*lengthPtr,void*buf){memset(buf,0xFF,*lengthPtr);// 全部填 FF模拟空 FlashreturnONMICRO_DFU_NVDS_ST_SUCCESS;}// put/del/enable/disable 全部直接返回成功——这是个空实现3.4 组装把函数指针填入虚表数组constdfu_nvds_itf_tdfu_nvds_itf[]{[DFU_NVDS_ITF_TYPE_MBR]{// 索引 0分区表后端onmicro_dfu_nvds_enable_mbr,onmicro_dfu_nvds_get_mbr,onmicro_dfu_nvds_put_mbr,onmicro_dfu_nvds_del_mbr,onmicro_dfu_nvds_disable_mbr},[DFU_NVDS_ITF_TYPE_FLASH]{// 索引 1内部 Flash 后端onmicro_dfu_nvds_enable_flash,onmicro_dfu_nvds_get_flash,onmicro_dfu_nvds_put_flash,onmicro_dfu_nvds_erase_flash,onmicro_dfu_nvds_disable_flash},// ... CFG、EXT_FLASH 等后端[DFU_NVDS_ITF_TYPE_DUMMY]{// 索引 4空后端onmicro_dfu_nvds_enable_dummy,onmicro_dfu_nvds_get_dummy,onmicro_dfu_nvds_put_dummy,onmicro_dfu_nvds_del_dummy,onmicro_dfu_nvds_disable_dummy},};四、第三步配置层注入依赖文件onmicro_dfu_config.h每个固件镜像的存储操作接口和元信息存储接口是可独立配置的typedefstruct{uint16_ttype;// 镜像类型APP / PATCH / CONFIG ...uint32_tbase_address1;// 基地址 1uint32_tbase_address2;// 基地址 2双区备份用uint32_tmax_length;// 最大长度constchar*describe;// 描述仅调试用constdfu_nvds_itf_t*image_ops_itf;// ← 镜像数据读写的接口constdfu_nvds_itf_t*info_ops_itf;// ← 镜像元信息读写的接口可为空uint16_tinfo_id;}dfu_image_t;constdfu_image_tdfu_image_types[]{{IMAGE_TYPE_APP,0x00044000,0x00044000,0x00040000,Application,dfu_nvds_itf[DFU_NVDS_ITF_TYPE_FLASH],// 镜像存在内部 FlashNULL,// 元信息不需要单独存储0x00},{IMAGE_TYPE_DUMMY,0x00000000,0x00000000,0xFFFFFFFF,Dummy,dfu_nvds_itf[DFU_NVDS_ITF_TYPE_DUMMY],// 镜像用空实现NULL,0x00},};关键在于image_ops_itf和info_ops_itf是dfu_nvds_itf_t类型指针可以指向数组中任意一个实现。新增一种存储后端只需写 5 个static函数在dfu_nvds_itf[]数组中加一项在dfu_image_types[]中把对应的镜像指向它DFU 核心逻辑 onmicro_dfu.c不需要改一行代码。五、执行时的调用流以写入镜像数据为例onmicro_dfu.c 第 281-297 行// 1. 从当前镜像配置中取出操作接口constdfu_nvds_itf_t*write_itfp_env-image_ops_itf;// 2. 统一调用——不需要知道底层是 Flash / MBR / Dummywrite_itf-enable();write_itf-put(write_addr,p_env-cache_recv_len,m_cache);write_itf-disable();对于获取新镜像地址的逻辑第 170-210 行同样使用接口切换if(img-typeIMAGE_TYPE_MBR_MAX){// 系统镜像 → 用 MBR 接口查分区表constdfu_nvds_itf_t*nvds_itfdfu_nvds_itf[DFU_NVDS_ITF_TYPE_MBR];nvds_itf-enable();nvds_itf-get(img-type,len,info);nvds_itf-disable();}elseif(img-typeIMAGE_TYPE_RAW){// 自定义镜像 → 用配置中的 info_ops_itfconstdfu_nvds_itf_t*nvds_itfcmd_img_info-info_ops_itf;// ... same pattern ...}六、总结这个模式的核心思想┌─────────────────────────┐ │ dfu_nvds_itf_t │ ← 接口struct of function pointers │ ┌───────────────────┐ │ │ │ enable() │ │ │ │ get() │ │ │ │ put() │ │ │ │ del() │ │ │ │ disable() │ │ │ └───────────────────┘ │ └───────┬───────┬─────────┘ │ │ ┌─────────────┘ └─────────────┐ ▼ ▼ ┌──────────────────┐ ┌──────────────────┐ │ Flash 实现 │ │ MBR 实现 │ │ drv_sfs_read() │ │ mbr_read_part() │ │ drv_sfs_write() │ │ mbr_write_part() │ └──────────────────┘ └──────────────────┘ │ │ └───────────────┬───────────────────┘ ▼ ┌─────────────────────┐ │ dfu_nvds_itf[] 数组 │ ← 注册表 │ [0] MBR 实现 │ │ [1] Flash 实现 │ │ [2] CFG 实现 │ │ [3] Ext Flash 实现 │ │ [4] Dummy 实现 │ └─────────────────────┘三个关键技巧技巧C 中的实现对应的 OOP 概念1. 结构体里放函数指针typedef struct { uint8_t (*get)(...); } dfu_nvds_itf_t;虚表 / 接口2.static函数隐藏实现所有后端实现函数都声明为static封装private 方法3. 运行时选择实现通过数组索引或指针切换不同表项多态 / 依赖注入为什么嵌入式开发要这么写零额外开销函数指针调用在 ARM 上是BLX Rn和直接函数调用开销几乎一样。没有 C 虚函数的多级间接跳转。无堆分配虚表是const全局数组编译时就确定不占 RAM。可测试可以插入 Dummy 后端返回全0xFF不操作真实硬件即可测试升级流程。易扩展加一种新存储后端比如 SPI NOR Flash只需要新增一个文件写 5 个static函数在数组中注册一行。
面向对象_昂瑞微_作者观点仅供参考
发布时间:2026/5/16 1:02:24
C 语言面向对象编程实例解析选自 OnMicro OM6626 BLE SDK 中的 DFUDevice Firmware Upgrade模块。适合有一定 C 基础、想理解如何在 C 中实现面向对象的初级工程师。一、先看最终效果调用方完全不关心底层实现在 onmicro_dfu.c 中固件升级的核心逻辑是这样写存储操作的// 获取某个存储后端的接口指针constdfu_nvds_itf_t*flash_itfdfu_nvds_itf[DFU_NVDS_ITF_TYPE_FLASH];// 像调用对象方法一样调用flash_itf-enable();flash_itf-get(addr,len,buf);flash_itf-put(addr,len,buf);flash_itf-del(addr,len);flash_itf-disable();不同之处只是数组索引不同——DFU_NVDS_ITF_TYPE_MBR、DFU_NVDS_ITF_TYPE_DUMMY、DFU_NVDS_ITF_TYPE_FLASH_EXT。同一个.get()调用底层可以是读内部 Flash、读 MBR 分区表、甚至返回全0xFF的假数据。这就是多态。二、第一步定义接口函数指针表文件onmicro_dfu_nvds.h// 这就是 C 语言中的接口——一个全是函数指针的结构体typedefstruct{uint8_t(*enable)(void);// 初始化uint8_t(*get)(uint32_tid,uint32_t*lengthPtr,void*buf);// 读取uint8_t(*put)(uint32_tid,uint32_tlength,void*buf);// 写入uint8_t(*del)(uint32_tid,uint32_tlength);// 擦除/删除uint8_t(*disable)(void);// 反初始化}dfu_nvds_itf_t;对应关系C函数指针表C类dfu_nvds_itf_t抽象基类 / 接口结构体内的 5 个函数指针5 个纯虚函数实现文件里创建的具体实例派生类 虚表为什么每个函数参数里没有this指针因为这个项目里每个实现的后端是全局唯一的只有一个内部 Flash、一个 MBR 分区表不需要区分实例。更完善的写法会把第一个参数设计为void *self。三、第二步实现类提供具体函数填表注册文件onmicro_dfu_nvds.c3.1 Flash 后端的实现// 每个函数对标接口中的一个函数指针staticuint8_tonmicro_dfu_nvds_enable_flash(void){drv_sfs_enable();// 使能内部 Flash 控制器returnONMICRO_DFU_NVDS_ST_SUCCESS;}staticuint8_tonmicro_dfu_nvds_get_flash(uint32_taddr,uint32_t*lengthPtr,void*buf){drv_sfs_read(addr,buf,*lengthPtr);// 调用 Flash 驱动读returnONMICRO_DFU_NVDS_ST_SUCCESS;}staticuint8_tonmicro_dfu_nvds_put_flash(uint32_taddr,uint32_tlength,void*buf){drv_sfs_write(addr,buf,length);// 调用 Flash 驱动写returnONMICRO_DFU_NVDS_ST_SUCCESS;}staticuint8_tonmicro_dfu_nvds_erase_flash(uint32_taddr,uint32_tlength){drv_sfs_erase(addr,length);// 调用 Flash 驱动擦除returnONMICRO_DFU_NVDS_ST_SUCCESS;}staticuint8_tonmicro_dfu_nvds_disable_flash(void){returnONMICRO_DFU_NVDS_ST_SUCCESS;}3.2 MBR分区表后端的实现staticuint8_tonmicro_dfu_nvds_get_mbr(uint32_tid,uint32_t*lengthPtr,void*buf){dfu_image_mbr_info*info(dfu_image_mbr_info*)buf;// 调用 MBR 库读取分区信息if(idsizeof(mbr_types)/sizeof(mbr_types[0])mbr_read_part(mbr_types[id],info-address,info-length,info-crc16)0){returnONMICRO_DFU_NVDS_ST_SUCCESS;}else{returnONMICRO_DFU_NVDS_ST_FAILED;}}// ... 其余 4 个函数同理3.3 Dummy 后端的实现占位 / 测试用staticuint8_tonmicro_dfu_nvds_get_dummy(uint32_tid,uint32_t*lengthPtr,void*buf){memset(buf,0xFF,*lengthPtr);// 全部填 FF模拟空 FlashreturnONMICRO_DFU_NVDS_ST_SUCCESS;}// put/del/enable/disable 全部直接返回成功——这是个空实现3.4 组装把函数指针填入虚表数组constdfu_nvds_itf_tdfu_nvds_itf[]{[DFU_NVDS_ITF_TYPE_MBR]{// 索引 0分区表后端onmicro_dfu_nvds_enable_mbr,onmicro_dfu_nvds_get_mbr,onmicro_dfu_nvds_put_mbr,onmicro_dfu_nvds_del_mbr,onmicro_dfu_nvds_disable_mbr},[DFU_NVDS_ITF_TYPE_FLASH]{// 索引 1内部 Flash 后端onmicro_dfu_nvds_enable_flash,onmicro_dfu_nvds_get_flash,onmicro_dfu_nvds_put_flash,onmicro_dfu_nvds_erase_flash,onmicro_dfu_nvds_disable_flash},// ... CFG、EXT_FLASH 等后端[DFU_NVDS_ITF_TYPE_DUMMY]{// 索引 4空后端onmicro_dfu_nvds_enable_dummy,onmicro_dfu_nvds_get_dummy,onmicro_dfu_nvds_put_dummy,onmicro_dfu_nvds_del_dummy,onmicro_dfu_nvds_disable_dummy},};四、第三步配置层注入依赖文件onmicro_dfu_config.h每个固件镜像的存储操作接口和元信息存储接口是可独立配置的typedefstruct{uint16_ttype;// 镜像类型APP / PATCH / CONFIG ...uint32_tbase_address1;// 基地址 1uint32_tbase_address2;// 基地址 2双区备份用uint32_tmax_length;// 最大长度constchar*describe;// 描述仅调试用constdfu_nvds_itf_t*image_ops_itf;// ← 镜像数据读写的接口constdfu_nvds_itf_t*info_ops_itf;// ← 镜像元信息读写的接口可为空uint16_tinfo_id;}dfu_image_t;constdfu_image_tdfu_image_types[]{{IMAGE_TYPE_APP,0x00044000,0x00044000,0x00040000,Application,dfu_nvds_itf[DFU_NVDS_ITF_TYPE_FLASH],// 镜像存在内部 FlashNULL,// 元信息不需要单独存储0x00},{IMAGE_TYPE_DUMMY,0x00000000,0x00000000,0xFFFFFFFF,Dummy,dfu_nvds_itf[DFU_NVDS_ITF_TYPE_DUMMY],// 镜像用空实现NULL,0x00},};关键在于image_ops_itf和info_ops_itf是dfu_nvds_itf_t类型指针可以指向数组中任意一个实现。新增一种存储后端只需写 5 个static函数在dfu_nvds_itf[]数组中加一项在dfu_image_types[]中把对应的镜像指向它DFU 核心逻辑 onmicro_dfu.c不需要改一行代码。五、执行时的调用流以写入镜像数据为例onmicro_dfu.c 第 281-297 行// 1. 从当前镜像配置中取出操作接口constdfu_nvds_itf_t*write_itfp_env-image_ops_itf;// 2. 统一调用——不需要知道底层是 Flash / MBR / Dummywrite_itf-enable();write_itf-put(write_addr,p_env-cache_recv_len,m_cache);write_itf-disable();对于获取新镜像地址的逻辑第 170-210 行同样使用接口切换if(img-typeIMAGE_TYPE_MBR_MAX){// 系统镜像 → 用 MBR 接口查分区表constdfu_nvds_itf_t*nvds_itfdfu_nvds_itf[DFU_NVDS_ITF_TYPE_MBR];nvds_itf-enable();nvds_itf-get(img-type,len,info);nvds_itf-disable();}elseif(img-typeIMAGE_TYPE_RAW){// 自定义镜像 → 用配置中的 info_ops_itfconstdfu_nvds_itf_t*nvds_itfcmd_img_info-info_ops_itf;// ... same pattern ...}六、总结这个模式的核心思想┌─────────────────────────┐ │ dfu_nvds_itf_t │ ← 接口struct of function pointers │ ┌───────────────────┐ │ │ │ enable() │ │ │ │ get() │ │ │ │ put() │ │ │ │ del() │ │ │ │ disable() │ │ │ └───────────────────┘ │ └───────┬───────┬─────────┘ │ │ ┌─────────────┘ └─────────────┐ ▼ ▼ ┌──────────────────┐ ┌──────────────────┐ │ Flash 实现 │ │ MBR 实现 │ │ drv_sfs_read() │ │ mbr_read_part() │ │ drv_sfs_write() │ │ mbr_write_part() │ └──────────────────┘ └──────────────────┘ │ │ └───────────────┬───────────────────┘ ▼ ┌─────────────────────┐ │ dfu_nvds_itf[] 数组 │ ← 注册表 │ [0] MBR 实现 │ │ [1] Flash 实现 │ │ [2] CFG 实现 │ │ [3] Ext Flash 实现 │ │ [4] Dummy 实现 │ └─────────────────────┘三个关键技巧技巧C 中的实现对应的 OOP 概念1. 结构体里放函数指针typedef struct { uint8_t (*get)(...); } dfu_nvds_itf_t;虚表 / 接口2.static函数隐藏实现所有后端实现函数都声明为static封装private 方法3. 运行时选择实现通过数组索引或指针切换不同表项多态 / 依赖注入为什么嵌入式开发要这么写零额外开销函数指针调用在 ARM 上是BLX Rn和直接函数调用开销几乎一样。没有 C 虚函数的多级间接跳转。无堆分配虚表是const全局数组编译时就确定不占 RAM。可测试可以插入 Dummy 后端返回全0xFF不操作真实硬件即可测试升级流程。易扩展加一种新存储后端比如 SPI NOR Flash只需要新增一个文件写 5 个static函数在数组中注册一行。