# 乐鑫 IoT 开发框架风格指南

[English]

## 关于本指南

本风格指南旨在鼓励遵循 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 配置为在本地检出以 CRLF（Windows 风格）为行结束符的文件，但提交时转换为以 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 进行 rebase，若要基于其他分支进行 rebase，请更改命令结尾的分支名称。

要更新单次提交中的文件行结束符，可以先运行 `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;

### 断言

使用 `assert.h` 中定义的标准 C 函数 `assert()` 来检查源代码中应该为真的条件。在默认配置中，若断言条件返回 `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 项目的默认设置。

## 使用 EditorConfig 配置项目代码风格

EditorConfig 可以帮助开发者在不同的编辑器和集成开发环境 (IDE) 之间定义和维护一致的代码风格。EditorConfig 项目包含定义代码风格的文件格式，以及一些文本编辑器插件，可以帮助编辑器读取文件格式并遵循定义风格。EditorConfig 文件易于阅读，并且与版本控制系统配合良好。

详情请浏览 EditorConfig 网站。

## 第三方组件代码风格

ESP-IDF 集成了许多第三方组件，这些组件可能具有不同的代码风格。

### FreeRTOS

有关FreeRTOS 采用的代码风格，请参阅 FreeRTOS 风格指南。FreeRTOS 源代码的格式化是通过 Uncrustify 自动完成的，因此 ESP-IDF FreeRTOS 组件中存有 FreeRTOS 代码风格的 Uncrustify 配置 (`uncrustify.cfg`) 副本 。

如果修改了 FreeRTOS 源文件，可以通过以下步骤重新格式化更新后的文件：

1. 确保系统安装了 Uncrustify (v0.69.0)。

2. 在更新后的 FreeRTOS 源文件上运行以下命令（其中 `source.c` 是需要格式化的源文件的路径）。

    uncrustify -c $IDF_PATH/components/freertos/FreeRTOS-Kernel/uncrustify.cfg --replace source.c --no-backup

## 代码文档

请参阅此处的指南：编写文档。

## 结构体

待编写。

## 语言特性

待编写。

此文档对您有帮助吗？

- 反馈已收到，谢谢！
- 如果您有其他意见，欢迎填写乐鑫文档反馈表。

- 我们重视您的反馈。
- 您可以填写乐鑫文档反馈表告诉我们如何改进该文档。