单板控制领域模型设计与实现
现状与问题
BSP团队负责多个项目单板的BSP开发维护,目前共96块板卡之多,而且板卡因为改版,往往还存在多个版本。每块板卡都有相应的单板控制,负责板卡的设备注册、板上芯片初始化、外部中断初始化、EPLD资源管理、BSP回调及IOCMD接口实现等等。
在老的软件架构和开发模式下,新板卡的单板控制开发调试流程一般如下:
- 复制一份老的单板控制模块,解决编译问题,保证编译通过;
- 对照硬件功能调试清单,对复制的代码进行修修补补,进行调试。
这种开发模式下,存在如下问题:
- 各单板单板控制模块各个独立,代码规模跟随单板成线性增长(O(n));
- 调试清单式验收把关松散,缺少软件架构上的保护,存在遗漏功能点、打印错误、单板间实现差异等问题,导致在联调时大量问题才暴露,影响单板联调进度;
- 单板控制代码重复度为80%以上,因为同类板卡往往只是更换了某个芯片;
- 板卡硬件设计的升级、改进、演进是渐进的,但是在软件设计架构上,体现不出、也不支持这种渐进式的演进;
- 大量的重复代码给开发和维护带来一定的挑战,也带来了巨大的人力浪费和软件管理难度。
设计的目标
项目单板主要分为PFU、SFU、MPU三大类,每类单板的硬件框架标准相同,核心功能一致,对外部呈现接口统一。如何在标准化的硬件框架上,构建可扩展性、可重用性、可维护性、可测试性良好的单板控制领域模型是本次设计的重要目标,具体来说,包括:
- 提供统一的模型框架,降低新增单板的单控控制模块开发复杂度;
- 提供统一的EPLD寄存器视图,方便与寄存器手册进行直接映射;
- 提供统一的接口视图;
- 提供统一的单板控制功能测试;
- 友好支持单元测试中硬件仿制等需求;
- 能清晰的体现单板硬件设计上的渐进式演进;
- 能清晰的体现单板改版的差异;
- 消除新增单板控制模块的重复代码。原来每新增一块同类新单板,要新增约15000~20000行代码,新模型的设计目标是控制在1000行以内。
设计与实现
领域建模
领域模型设计,从某个一个角度来看,就是做好差异性管理。这其中包含2个信息:1)什么是核心业务,什么是差异;2)如何管理。
我们根据单板控制领域的核心业务,以及单板间的差异性需求,抽象出3个对象(board、epld、ops),并建立领域模型(如下)。
模型实现中运用的主要技术方法:
1) DSL:构建DSL语法对寄存器进行描述;
2) 继承:C语言不支持继承语法,通过模拟C++ vtable虚函数表指针,实现对象的继承功能;
3) 覆盖: C99中,若数据结构体中的某个字段被重复初始化,最后一次初始化有效。根据这个特点,实现对象的覆盖功能。
ops对象
根据单板控制的领域模型特点,抽象出统一的接口对象:ctrl_operations,并按应用场景分为3类:内部接口、外部接口、ioctl接口,并分别进行模块化设计。ops对象定义示例如下:
/* board_a单板 PCB_VER_1版本 作为 ctrl_operations 的默认实现 */ struct ctrl_operations { /* internal operations */ SWORD32 (*board_init)(struct board *bd); SWORD32 (*board_exit)(struct board *bd); /* 以下省略... */ /* external operations */ /* k_GetCpuFreq wrapper */ WORD32 (*get_cpu_freq)(struct board *bd); /* 以下省略... */ /* ioctl operations */ /*BSP_IOCMD_BRDCTRL_LOCAL_MSMSG_GET*/ WORD32 (*query_local_msMsg) (struct board *bd,T_BSP_BRDCTRL_MSMSG_GET *ptMsMsg); /* 以下省略... */ const struct ctrl_operations *inherits; };
Ops对象差异性的设计需求,通过继承和覆盖来满足。通过继承,满足体现单板硬件设计上的渐进演进关系的设计需求;通过覆盖,满足体现单板与单板、单板改版间的硬件改动的设计需求。
下面的一个实例中可以一目了然看出board_a单板与board_b单板的继承关系和硬件实现差异。
static struct ctrl_operations board_b_ops = { .inherits = &board_a_ops, .rov_wr = bsp_board_b_rov_wr, .vol_modify = bsp_board_b_vol_modify, .get_voltage_current = bsp_board_b_get_voltage_current, .funccard_info = bsp_board_b_funccard_info, };
在上例中可以看出,继承通过inherits字段表达,由finalize接口实现,具体实现细节如下:
SWORD32 finalize(struct ctrl_operations *ops) { const struct ctrl_operations *cur; void **begin = (void **)ops; void **end = (void **)&ops->inherits; void **pp; if (!ops) return -EINVAL; if (!ops->inherits) return 0; for (cur = ops->inherits; cur; cur = cur->inherits) { void **inherit = (void **)cur; for (pp = begin; pp < end; pp++, inherit++) if (!*pp) *pp = *inherit; } for (pp = begin; pp < end; pp++) if (IS_ERR(*pp)) { *pp = NULL; return -EFAULT; } ops->inherits = NULL; return 0; }
epld对象
EPLD对象通过BOOTEPLD_REGISTER、WORKEPLD_REGISTER、CPUEPLD_REGISTER 3个DSL进行描述,其描述语法形式采用硬件提供的寄存器说明手册格式(如下)。
// boot epld registers BOOTEPLD_REGISTER(board_id, 0x000) /* Board ID Reg */ BOOTEPLD_REGISTER(bom_pcb_cpld_ver, 0x002) /* PCB BOM ID Reg */ BOOTEPLD_REGISTER(system_ctrl, 0x004) /* System Control Reg */ BOOTEPLD_REGISTER(flash_rst, 0x010) /* Flash Reset Reg */ BOOTEPLD_REGISTER(rec_rst, 0x040) /* Recored Reset Reg */
DSL对EPLD对象的定义、初始化和测试接口提供统一封装,模型根据不同的应用场景,定义具体EPLD_REGISTER的行为,完成EPLD对象的动态定义、动态初始化和动态测试接口实现。如EPLD对象的动态初始化实现:
#define BOOTEPLD_REGISTER(name, offset) .name = VALID(offset), struct bootepld_reg bootepld_base = { .inherits = NULL, #include "bsp_register_define.h" };
通过继承和覆盖支持不同单板的epld差异性的设计需求,继承实现与ops对象类似,详见ops对象部分,不再赘述。
static struct bootepld_reg board_a_bootepld = { .inherits = (void *)&bootepld_base, };
另外,单元测试中对EPLD硬件进行mock打桩的需求,本模型也提供了很好的支持,实现起来也十分方便。如:
struct bootepld_reg test_bootepld_1 = { .inherits = (void *)&bootepld_base, .bom_pcb_cpld_ver = VALID(0x1beef), }; struct bootepld_reg test_bootepld_2 = { .inherits = (void *)&test_bootepld_1, .bom_pcb_cpld_ver = VALID(0x2beef), .cpu_type = VALID(0xcbeef), };
效果与推广
采用文中所述设计方法,对1块已有单板控制(共18540行)进行重构,3块新增板卡的单板控制进行开发,新增一块单板支持的代码量减少98.6%(从18540行降到254行),代码规模减少82.3%(从74160行降到13101行),平均复杂度减少42%(从4.76降到2.8)。
本设计方法先后在多个团队实践,应用在PFU、SFU、subcard等单板控制模块上,并已大量商用。
本设计方法适用于有相同硬件框架标准的多设备的驱动开发,其中涉及到的技术方法对所有C语言软件模块都可以借鉴参考。