软件设计
Arm Mbed 软件原理:
- 是一致的。
- 直观。
- 简单。
- 可靠。
风格
请参阅 Mbed 风格指南。
组织
Arm Mbed OS 代码库被组织成概念子模块,以限制个人贡献的范围和复杂性。这些模块作为单个 Git 仓库包含在 Mbed OS 代码库中。我们建议外部库使用此模型。
-
模块应在 OS 树中进行逻辑分组。避免使用通用词;有意命名。
-
使用模块名称后跟下划线来为每个源文件添加前缀。这可以防止与不同模块中的其他类似命名文件冲突,例如 nanostack/thread.c 和 drivers/Thread.cpp;并非所有工具链都能够支持具有相同名称的目标文件。
mbed-os/rtos/rtos_thread.cpp mbed-os/rtos/rtos_semaphore.cpp mbed-os/drivers/drivers_analog_in.cpp
-
始终使用路径中的模块目录包含头文件。例如:#include “lwip/lwip-interface.h”,include “drivers/Ticker.h”。将 include 路径限制为模块目录。这允许将来移动模块。
-
作为模块的入口点(来自用户空间),我们建议使用单个头文件。例如:mbed.h,rtos.h。
-
头文件应限制外部包含,以避免间接暴露不相关的 API。头文件不应扩展名称空间。
-
在 C++ 模块中,API 应包含在与模块名称匹配的命名空间中。例如:mbed :: Ticker,rtos :: Thread,netsocket :: Socket。
-
在C模块中,每个非静态函数和类型都应该以模块的名称作为前缀,后跟下划线。例如:mbed_critical_section_enter(),lwip_gethostbyname(host)。
-
Mbed OS 代码库中包含的模块可以在单独的仓库中进行镜像。源回购应该清楚地标识并链接到模块的自述文件。
-
特殊目录应遵循一致的命名约定。
贡献
-
请参阅 Mbed 贡献指南。
-
每个拉取请求应该用于单一目的。
-
代码必须编译每次提交。
-
提交消息应该以子模块名称和冒号为前缀:
lwip: Fixed buffer overrun in rx loop The rx loop did not properly wait for rx semaphore to release causing the buffer to overrun
-
修补程序必须在返回到一个或多个发布分支之前登陆主服务器。
-
功能开发可以在单独的分支中进行,并在完成时将其带到 master。
-
绝不能重写主分支和发布分支。
-
所有贡献者必须签署 CLA。
-
对于传入来源,唯一可接受的许可证是:
- MIT。
- Apache。
- Permissive Binary License。
API 设计
通用模块可以分为两个 API,即前端(或用户 API)和后端(或移植层)。用户 API 描述了库实现的程序员接口。对于 Mbed OS,面向用户的 API 应采用基于 C++ 类的接口,而移植层应采用 C 兼容接口。
API 设计 - 用户 API
- 每个模块都应提供面向对象的 C++ 用户 API。
- 目前的标准严格来说是 C++ 03(便携性)。
- 状态应包含在相关的 C++ 类中。
- 在采用代码库中不存在的语言功能之前,请三思而后行:
- 用 C++ 的优点来思考 C。
- 不要让用户学习新东西。
- 例外和 RTTI 被禁用。
- 由于对系统资源的未知影响而避免使用 STL。
- 首选 C 语言功能而不是 C++ 语言功能:
- 在模糊的重载上使用不同的函数名称。
- 使用
uint8_t read_8(void)
,uint16_t read_16(void)
,uint32_t read_32(void)
。 - 使用
void write_8(uint8_t)
,void write_16(uint16_t)
,void write_32(uint32_t)
。 - 将模板限制为类型和数组大小。
- 根据以下规则使用指针和引用。所有权可能含糊不清的使用适当的文档:
- 使用复制或不可变引用来传递具有值语义的类型。
- 使用指针借用动态多态类的所有权。
- 使用指针转移动态多态类的所有权。明确记录转让的所有权。
- 在引用上使用指针:
- C 用户比较熟悉。
- 所有权在语法上是清楚的。
- 将类组织为两种类型:
- 具有值语义的类型,例如 SocketAddress 和 Callback。
- 内置类型的用户友好替代品。
- 无法扩展(遭受对象切片)。
- 按值传递,不需要内存管理(在栈上处理)。
- 如果可能,传递 const-reference 以减少复制。
- 必须便宜复制。
- 应该很小(<= 16 字节)。这不包括间接引用的内存。
- 动态多态类,如 Ticker 和 EthernetInterface:
- 参与班级层次结构。
- 可以通过接口抽象。
- 可扩展 - 应该有虚拟表。如果可重载,必须包含虚拟析构函数。
- 通过指针传递;内存管理应留给用户。
- 应将复制构造函数和复制赋值运算符声明为私有。如果确实需要复制,我们建议使用虚拟克隆成员函数,以避免切片问题,并指出克隆是一个非常重要的操作。
- 如果一个类包含一个非常大的内存区域(> 64 字节),则更喜欢动态分配该区域;如果用户在栈上实例化类,它可以防止栈溢出。
- 一个类应该有一个责任。例如:UDPSocket vs TCPSocket,Ticker vs Timer。
- 将继承用于 “是一种” 关系。例如:UDPSocket 和 Socket。
- 首选基于模板的多态性的抽象基类,以避免代码大小增加。
- 更喜欢在基于预处理器的条件编译和其他形式的间接调度的 C++ 代码中编写抽象接口。
- 不要在全局范围内声明对象(应用程序应分配全局对象)。在全局范围内声明的对象在链接时很少被编译器垃圾收集。这可能会导致应用程序的大小膨胀。
- 将 get/set 函数与私有成员变量一起使用可以隐藏用户的内部状态。
- 如果无法发出错误信号,请避免可能失败的操作。类构造函数不应该失败。
- 不可恢复的错误(例如中断中的 OOM 和互斥锁)不应返回给用户。
- 应将可恢复的错误(例如 UDP 数据包丢失)通过错误代码传播给用户。
- 小心处理弃用。在下一次主要操作系统修订之前,我们不会删除已弃用的 API。弃用的原因包括:
- 设计模式陷阱导致开发人员编写错误的代码。
- 代码功能不正确。
- 代码不安全(同步)或导致未定义的行为。
API 设计 - 移植层
- 每个模块应提供与 C 兼容的移植层。
- 目前的标准严格来说是 C99(便携性)。
- 移植层不应该假设它是如何被消耗的。
- 状态应该包含在由用户 API 指针传递的结构中。避免全局状态。
- 移植层的设计应允许在实现中尽可能多地采用合理的方差。
- 简洁是美好的。
线程和 IRQ 安全
- 用户 API 应该是线程安全的。
- 如果用户 API 旨在中断安全,则应明确记录。
- 如果用户 API 无法保证线程安全,则应使用警告符号明确记录。在所有 API 中使用一致的表单:“警告:不是线程安全的”。
- 模块的移植层应该设计用于非线程安全的实现。
- 如果在中断上下文中调用回调,则应明确记录负责的 API 并发出警告。在所有 API 中使用一致的表单:“警告:从中断上下文调用”
文档
-
模块中的每个函数和类都应提供 doxygen 注释,记录函数以及每个参数和返回值:
/** Wait until a Mutex becomes available. * * @param millisec timeout value or 0 in case of no time-out. (default: osWaitForever) * @return status code that indicates the execution status of the function. */ osStatus lock(uint32_t millisec=osWaitForever);
-
每个类的头文件的 doxygen 必须包含使用 @code 和 @endcode 的使用示例。
-
每个 API 还应该在类头示例的基础上提供 @code 和 @endcode 部分。
-
如果需要有关方法的更具体信息,则应使用 @note 完成此操作。
-
如果不推荐使用某个方法,则必须使用 @deprecated 标记并包含要替换它的方法的说明。
-
每个模块都应提供一个记录模块的自述文件:
- 自述文件应以一个小段落开头,向用户介绍模块,而无需事先了解。
- README 应包含一个代码示例,说明如何使用该模块。
- 如果模块包含移植层,则 README 应包括移植指令。
- 如果模块包含测试,则 README 应提供测试指令。
-
扩展文档应位于模块的 docs 目录中,并带有模块自述文件中的相应链接。
测试
- 每个模块都应包含一个测试目录,其中包含涵盖模块功能的测试。
- 测试应根据被测试的类别进行组织;每个类大约有一个测试文件。
- 代码库中包含的测试必须与 Mbed OS 测试框架兼容。
- 为了避免回归,每个错误修复都应该包含一个额外的测试用例,用于识别错误并在修复错误之前确定性地失败。
配置
Mbed OS 为应用程序开发提供了强大的配置系统。但是,模块还应该关注在 Mbed 构建系统之外保持可配置性。模块应在简单的头文件中提供记录良好的配置选项。
- 每个模块都应该提供带有配置选项的 module_lib.json(或类似)。
- 每个配置选项都应包含涵盖其目的和对系统的影响的文档。
- 为了帮助移植新目标,每个配置选项应提供合理的默认值(如果未定义配置选项)。
- 配置选项不应更改 API 的行为。
- 首选用户 API 中需要不同功能的多个类。
- 目标和应用程序应该能够覆盖每个配置。
- 在所有平台上,默认的优化选择应该是大小。