乐鑫 IoT 开发框架风格指南
关于本指南
本风格指南旨在鼓励遵循 ESP-IDF 中通用的编码规范。
风格指南包含一系列规范准则,旨在编写出可读性高、可维护性强且稳定可靠的代码。编写与代码库编码风格统一的代码,能够提升代码的阅读与理解效率;统一空格、换行格式规范,可避免后续代码修改时产生大量晦涩难懂的差异内容;沿用通用的模块架构模式,规范各类语言特性的使用方式,能够方便其他开发人员理清代码的运行逻辑。
本指南尽可能简化各项规则,因此无法覆盖全部使用场景。部分场景下,需要为代码的可读性、可维护性与健壮性适当放宽规则要求。
若需修改 ESP-IDF 所引入的第三方代码,请遵循该第三方项目自身的编码规范,同时提交具备实用价值的修改内容,以便并入上游开源项目。
C 语言代码格式化
变量与函数命名
仅在单个源文件中使用的变量与函数,需声明为
static。公共名称(非静态变量、非静态函数)需添加对应组件或单元的专属前缀,规避命名冲突,例如
esp_vfs_register()、esp_console_run()。乐鑫相关的专属名称可统一选用esp_作为前缀,但同一组件内其他名称的前缀规则需保持一致。为了便于识别,静态变量应以
s_为前缀。例如,static bool s_invert。避免不必要的缩写(例如将
data简写为dat),除非不缩写会导致名称过长。
缩进
每个缩进层级使用四个空格。不要使用制表符进行缩进。配置编辑器,确保在每次按下制表键时输出四个空格。
垂直间距
函数之间空一行。不要在函数的开头或结尾处空行。
void function1()
{
do_one_thing();
do_another_thing();
// 错误,请勿在此处使用空行
}
// 请在此处使用空行
void function2()
{
// 错误,请勿在此处使用空行
int var = 0;
while (var < SOME_CONSTANT) {
do_stuff(&var);
}
}
只要不严重影响可读性,单行最大长度可接受 120 个字符。
水平空格
始终在条件和循环关键字后加一个空格:
if (condition) { // 正确
// ...
}
switch (n) { // 正确
case 0:
// ...
}
for(int i = 0; i < CONST; ++i) { // 错误
// ...
}
在二元运算符前后加一个空格,一元运算符则不需要空格。乘法和除法运算符前后可省略空格:
const int y = y0 + (x - x0) * (y1 - y0) / (x1 - x0); // 正确
const int y = y0 + (x - x0)*(y1 - y0)/(x1 - x0); // 也可以
int y_cur = -y; // 正确
++y_cur;
const int y = y0+(x-x0)*(y1-y0)/(x1-x0); // 错误
. 和 -> 运算符前后不需要加空格。
有时,在代码行内添加横向空格能够提升代码的可读性。例如,你可以通过添加空格来对齐函数参数:
esp_rom_gpio_connect_in_signal(PIN_CAM_D6, I2S0I_DATA_IN14_IDX, false);
esp_rom_gpio_connect_in_signal(PIN_CAM_D7, I2S0I_DATA_IN15_IDX, false);
esp_rom_gpio_connect_in_signal(PIN_CAM_HREF, I2S0I_H_ENABLE_IDX, false);
esp_rom_gpio_connect_in_signal(PIN_CAM_PCLK, I2S0I_DATA_IN15_IDX, false);
但需要注意的是,如果有人新增一行代码,并将更长的标识符作为首个参数(例如 PIN_CAM_VSYNC),该标识符将无法适配原有格式。因此需要重新调整其他代码行的对齐格式,给本次代码提交引入大量无实际意义的修改内容。
因此,请谨慎使用横向对齐,尤其是在后期可能会向列表中添加新行的情况下。
切勿使用制表符进行横向对齐。
切勿在行尾添加额外的空白字符。
大括号
函数定义需将大括号放在单独的一行:
// 正确:
void function(int arg)
{
}
// 错误:
void function(int arg) {
}
在函数内部,左大括号需与条件语句、循环语句放在同一行:
if (condition) {
do_one();
} else if (other_condition) {
do_two();
}
注释
使用 // 进行单行注释;多行注释既可使用 // 逐行注释,也可使用 /* */ 注释块。
尽管与格式化无关,但以下是一些关于如何有效使用注释的注意事项。
不要使用单行注释来禁用功能:
void init_something()
{
setup_dma();
// load_resources(); // 读者会疑惑为什么此处被注释。
start_timer();
}
如果不再需要某些代码,请将其完全删除。这些代码之后可以随时在相关文件的 git 历史记录中查到。如果出于某些临时原因禁用了部分函数调用,并且打算之后将其恢复,请在相邻行添加说明:
void init_something()
{
setup_dma();
// TODO: 此处应加载资源,但加载器尚未完全集成。
// load_resources();
start_timer();
}
上述规则同样适用于
#if 0 ... #endif块。如果不再需要代码块,请将其完全删除。否则,请添加注释以说明禁用该代码块的原因。不要使用#if 0 ... #endif或注释来存放未来可能用到的代码片段。不要添加有关作者和更改日期的琐碎注释。可以直接使用 Git 来查找修改人及修改内容等相关信息。例如,类似下文的注释不仅没添加任何有用的信息,还会使代码变得杂乱无章:
void init_something()
{
setup_dma();
// XXX 于 2016-09-01 添加
init_dma_list();
fill_dma_item(0);
// XXX 结束添加
start_timer();
}
行结束符
提交内容只能包含以 LF(Unix 风格)为行结束符的文件。
Windows 用户可以通过设置 core.autocrlf 参数,让 Git 在本地检出文件时使用 Windows 风格的 CRLF 行结束符,提交代码时自动转换为 LF 行结束符。GitHub 官方提供了相关配置的 说明文档。
如果你的分支中意外存在一些提交引入了换行符 (LF) 结尾格式,你可以在 MSYS2 或类 Unix 终端中执行以下命令将其转换为 Unix 格式(需提前切换至 IDF 工作目录,并确保当前已切换到正确的分支):
git rebase --exec 'git diff-tree --no-commit-id --name-only -r HEAD | xargs dos2unix && git commit -a --amend --no-edit --allow-empty' master
请注意,上述示例命令是基于 master 分支执行变基操作;若需基于其他分支执行变基,请修改命令末尾对应的分支名称。
如果仅需要修改单次提交,可以先运行 dos2unix FILENAME,再运行 git commit --amend 更新。
代码格式化
ESP-IDF 使用 Astyle 来格式化源代码。配置存储在 tools/ci/astyle-rules.yml 文件中。
所有组件都默认排除在格式化检查之外。可以将组件从 components_not_formatted_temporary 列表中删除,从而启用对该组件的格式化检查,然后运行:
pre-commit run --files <path_to_files> astyle_py
或者,也可以手动运行 astyle_py。通过 pip install astyle_py==VERSION 安装该工具,确保安装的版本与 .pre-commit-config.yaml 文件中指定的版本相同,然后运行:
astyle_py --rules=$IDF_PATH/tools/ci/astyle-rules.yml <path-to-file>
类型定义
应使用 蛇形命名法 (snake_case),并以 _t 后缀结尾:
typedef int signed_32_bit_t;
枚举
枚举应通过 typedef 定义,并使用命名空间:
typedef enum
{
MODULE_FOO_ONE,
MODULE_FOO_TWO,
MODULE_FOO_THREE
} module_foo_t;
断言
标准 C 语言的 assert() 函数定义于头文件 assert.h 中,该函数用于检查源代码中理应成立的条件。在默认配置下,若断言条件返回 false 或 0,程序会调用 abort() 函数并触发 严重错误。
assert() 只用于检测那些无法修复的错误,这些错误因严重的内部逻辑漏洞或数据损坏而产生,导致程序无法继续运行。对于可修复的错误(如因无效外部输入而产生的错误),应当返回错误值,相关说明可参考文档:错误处理。
备注
当断言一个类型为 esp_err_t 的值等于 ESP_OK 时,应使用 用于不可恢复错误的宏 而不是 assert()。
可以将 ESP-IDF 项目配置为禁用断言(详见 CONFIG_COMPILER_OPTIMIZATION_ASSERTION_LEVEL)。此时在 assert() 语句中调用的函数不应有副作用。
当断言功能被禁用时,还需要采用特定的技术手段来避免出现“变量定义但未使用”的警告,这种警告通常由以下代码模式引起:
int res = do_something();
assert(res == 0);
一旦 assert 被优化掉,将不再使用 res 值,编译器会对此发出警告。但即使禁用了断言,仍必须调用 do_something() 函数。
当变量在单个语句中声明并初始化时,最好在新的一行将其转换为 void。编译器将不再发出警告,且变量仍可在最终的二进制文件中被优化删除:
int res = do_something();
assert(res == 0);
(void)res;
如果变量是单独声明的,例如该变量被用于多个断言,则可以使用 GCC 属性 __attribute__((unused)) 来声明它。编译器将不再发出任何有关未使用变量的警告,且变量仍可被优化删除:
int res __attribute__((unused));
res = do_something();
assert(res == 0);
res = do_something_else();
assert(res != 0);
头文件保护
所有公共头文件应具有预处理器保护。推荐使用 pragma:
#pragma once
最好不要用以下模式:
#ifndef FILE_NAME_H
#define FILE_NAME_H
...
#endif // FILE_NAME_H
除了保护宏,所有 C 语言头文件应具有 extern "C" 保护,使头文件能够在 C++ 代码中使用。头文件结构应遵循以下顺序:先是 pragma once,再是 #include 语句,然后是 extern "C" 保护:
#pragma once
#include <stdint.h>
#ifdef __cplusplus
extern "C" {
#endif
/* declarations go here */
#ifdef __cplusplus
}
#endif
include 语句
编写 #include 语句时,需保持以下顺序:
C 标准库头文件。
其他 POSIX 标准头文件及其常见扩展(如
sys/queue.h)。常见的 IDF 头文件(
esp_log.h、esp_system.h、esp_timer.h以及esp_sleep.h等)。其他组件的头文件,如 FreeRTOS。
当前组件的公共头文件。
私有头文件。
对于 C 标准库头文件和其他 POSIX 头文件,请使用尖括号 (#include <stdio.h>)。
对于所有其他头文件,请使用双引号 (#include "esp_log.h")。
C++ 代码格式化
前文提到的 C 语言代码格式化规则同样适用于 C++ 代码格式化。如果这些规则不足以满足需求,可遵循以下补充规则。
文件命名
C++ 头文件的扩展名为 .hpp。C++ 源文件的扩展名为 .cpp。后者可帮助编译器将 C++ 源文件与 C 源文件区分开来。
命名
类和结构体 名称应使用首字母大写的
驼峰命名法(CamelCase)。成员变量和方法应使用蛇形命名法(snake_case)。若驼峰命名法会严重降低可读性(例如命名GPIOOutput时),可允许使用下划线_(例如GPIO_Output)增强可读性。命名空间 应使用小写的
蛇形命名法。模板 应在函数声明的上一行进行指定。
面向对象编程 (OOP) 中的接口命名不应使用后缀
...Interface。采用该命名方式,后续无论是从普通类中抽取接口,还是将接口改造为普通类,都能顺畅完成调整,不会引发破坏性变更。
类成员顺序
按照优先顺序:
依次声明公共成员、受保护成员、私有成员;若某一访问权限下没有成员,则省略该部分的声明。
先定义构造函数与析构函数,再定义成员函数,最后声明成员变量。
例如:
class ForExample {
public:
// 首先是构造函数,接着是默认构造函数,然后是析构函数
ForExample(double example_factor_arg);
ForExample();
~ForExample();
// 剩余的公共方法
set_example_factor(double example_factor_arg);
// 公共成员变量
uint32_t public_data_member;
private:
// 首先是私有方法
void internal_method();
// 然后是私有成员变量
double example_factor;
};
空格
在命名空间内部不要缩进。
public、protected与private标签的缩进应与相应class标签的缩进保持一致。
简单示例
// spaceship.h 文件
#ifndef SPACESHIP_H_
#define SPACESHIP_H_
#include <cstdlib>
namespace spaceships {
class SpaceShip {
public:
SpaceShip(size_t crew);
size_t get_crew_size() const;
private:
const size_t crew;
};
class SpaceShuttle : public SpaceShip {
public:
SpaceShuttle();
};
class Sojuz : public SpaceShip {
public:
Sojuz();
};
template <typename T>
class CargoShip {
public:
CargoShip(const T &cargo);
private:
T cargo;
};
} // 命名空间 spaceships
#endif // SPACESHIP_H_
// spaceship.cpp 文件
#include "spaceship.h"
namespace spaceships {
// 如果仅在初始化列表中初始化值
// 则可以将大括号放置在同一行
SpaceShip::SpaceShip(size_t crew) : crew(crew) { }
size_t SpaceShip::get_crew_size() const
{
return crew;
}
SpaceShuttle::SpaceShuttle() : SpaceShip(7)
{
// 进一步初始化
}
Sojuz::Sojuz() : SpaceShip(3)
{
// 进一步初始化
}
template <typename T>
CargoShip<T>::CargoShip(const T &cargo) : cargo(cargo) { }
} // 命名空间 spaceships
CMake 代码风格
使用四个空格缩进。
每行最多包含 120 个字符。需要换行时,应尽量兼顾可读性,例如将关键字与参数组放置在单独一行中。
在
endforeach()、endif()等可选括号中不要添加任何内容。使用小写 (
with_underscores) 来命名指令、函数和宏。对于局部变量,使用小写 (
with_underscores)。对于全局变量,使用大写 (
WITH_UNDERSCORES)。其他方面遵循 cmake-lint 项目的默认设置。
Python 代码风格
ESP-IDF 的大部分工具,即 idf.py 及其子命令、构建系统辅助脚本、tools 目录下的脚本,均使用 Python 编写。为保证贡献内容的可移植性,新增的 Python 代码应能在 ESP-IDF 支持的所有 Python 版本上运行,即 快速入门 中所述的最低支持版本,到最新发布的 Python 版本。请避免依赖此版本范围内尚不普遍支持的语法或标准库特性。
代码检查与格式化
Python 代码由 Ruff 进行检查和格式化,并由 mypy 进行类型检查,相关配置位于 ruff.toml 和 .mypy.ini。无需记住这些规则,仅需在提交前安装 pre-commit 钩子 (安装一次即可),此后每次提交时就会自动运行这些检查工具,确保改动风格与代码库整体保持一致。
依赖项
Python 依赖项在 tools/requirements 目录下的 requirements 文件中声明。列出软件包名称即可,请勿在这些文件中固定版本号。版本约束由位于 ESP-IDF 仓库之外的独立约束 (constraint) 文件单独维护,因此在 requirements 文件中写死版本号通常是错误的做法。
复用共享代码 (esp-pylib)
ESP-IDF 将 esp-pylib 作为核心依赖项一同提供。该库整合了乐鑫各类 Python 工具共用的实用工具集,能够保障不同工具的行为保持统一。在新增或迭代修改 Python 工具的过程中,应优先复用该库中的各类辅助代码,而非自行实现。例如,须使用此库内置的共享日志记录器输出日志信息,而不要自行调用 print() 函数;同时,复用库中通用的错误处理、命令行构建模块,不要重新实现。如需了解该库支持的全部功能及具体使用方式,请参阅 esp-pylib README。
使用 EditorConfig 配置项目代码风格
EditorConfig 可以帮助开发者在不同的编辑器和集成开发环境 (IDE) 之间定义和维护一致的代码风格。EditorConfig 项目包含定义代码风格的文件格式,以及一些文本编辑器插件,可以帮助编辑器读取文件格式并遵循定义风格。EditorConfig 文件易于阅读,并且与版本控制系统配合良好。
详情请浏览 EditorConfig 网站。
第三方组件代码风格
ESP-IDF 集成了许多第三方组件,这些组件可能具有不同的代码风格。
FreeRTOS
有关FreeRTOS 采用的代码风格,请参阅 FreeRTOS 风格指南。FreeRTOS 源代码的格式化是通过 Uncrustify 自动完成的,因此 ESP-IDF FreeRTOS 组件中存有 FreeRTOS 代码风格的 Uncrustify 配置 (uncrustify.cfg) 副本 。
如果修改了 FreeRTOS 源文件,可以通过以下步骤重新格式化更新后的文件:
确保系统安装了 Uncrustify (v0.69.0)。
在更新后的 FreeRTOS 源文件上运行以下命令(其中
source.c是需要格式化的源文件的路径)。
uncrustify -c $IDF_PATH/components/freertos/FreeRTOS-Kernel/uncrustify.cfg --replace source.c --no-backup
代码文档
请参阅 编写文档。
结构体
待编写。
语言特性
待编写。