C语言开发标准(微控制器)

C语言开发标准(微控制器)

Written By Tomy Stark.
E-mail: ro7enkranz@qq.com
Ver 1.0.0

Note:

  • 该标准仅限微控制器平台(RTOS or Bare Metal),不完全适合Linux程序的开发标准。
  • 本标准会不定期迭代完善
  • 转载请注明本文出处链接、作者
  1. 嵌入式开发中,务必注意不同硬件体系架构之间数据类型的差异

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    /* 为保证健壮性和可移植性,一般情况下,一律使用以下类型来定义变量 */
    /* 无符号型 */
    uint8_t
    uint16_t
    uint32_t
    /* 有符号型 */
    int8_t
    int16_t
    int32_t

    /* 而不使用(尽量少用)以下类型来定义变量 */
    /* 无符号型 */
    unsigned char
    unsigned short
    unsigned int
    /* 有符号型 */
    signed char
    signed short
    signed int
  2. 关于注释:

    针对代码的注释,采用单行注释:

    1
    //

    或多行注释(当需要注释的代码量较多时):

    1
    2
    3
    #if 0

    #endif

    针对对某些功能、注意事项、函数等的描述性文字,采用多行注释:

    1
    2
    3
    4
    5
    6
    7
    8
    /* comment */

    或在内容较多需要换行时:

    /*
    当前时钟频率: SYSCLK = HCLK = PCLK2(APB2) = 96 MHz
    因为 TIM_ClockDivision = TIM_CKD_DIV1,所以 CLK_Freq 为 PCLK2 的一分频,所以 CLK_Freq = 96 MHz
    */
  3. 关于缩进:一律采用“空格”缩进,缩进 4 个空格。

  4. 宏函数和内联函数,一般情况下不建议使用宏函数,而更建议使用内联函数,易于调试同时兼具性能。同时,在使用内联函数时需要注意函数体不能太过庞大复杂,否则仍旧会被编译器作为普通函数处理,例如,当对另一个函数进行别名封装时,可以使用内联函数:

    1
    2
    3
    4
    static inline status_t PINS_DRV_Deinit(uint32_t pinCount, const pin_settings_config_t config[])
    {
    return PINS_DRV_Init(pinCount, config);
    }
  5. 针对“”的一些操作需要确保正确,因为宏不易调试,正常情况下,应以让代码更易于阅读和维护,同时又兼顾性能的原则来使用宏,因为花了很多时间想出来的“高效又高深的宏”同样可能需要让别人甚至自己花很多时间去理解,这样无形之中就增加了软件维护成本,特别在项目交接或者不同项目之间代码复用时,矛盾尤为突出,故编程时应在高大上和可维护性之间找到一个平衡点,做出合适的取舍。

  6. 对于规律性的常量型定义,例如有相关性的从n开始递增的多个常量数据定义,建议使用enum枚举型而尽量不用宏定义,因为宏定义不易于调试,而枚举类型易于调试。

  7. 尽量不要在条件判断语句中使用赋值语句,例如

    1
    2
    3
    4
    if (a = b)
    {
    return true;
    }

    如果有意为之,务必加上注释进行必要说明,明确此写法用意。

  8. 合理结合使用“联合体 union”和“结构体 struct”和“位域”,对于特定情形下,可以精简代码量,减少内存占用,提高代码可重用性。例如基于某个通讯外设(I2C、SPI……)上发送的数据帧,包含不同类型的数据情况下,可用此法,但是必须注意字节对齐问题

  9. 同样是 字节对齐问题,为了保证字节对齐,同时更节省内存空间,提高MCU执行效率(缩短程序执行时间),结构体中的数据类型请按从小到大的顺序依次排列 (8位->16位->32位->单精度浮点->双精度浮点)。例如以下代码片段:

    1
    2
    3
    4
    5
    6
    7
    typedef struct {
    uint8_t DMA_TC_Flag; /* DMA 传输完成标志 */
    uint8_t FilterComplete_Flag; /* ADC 滤波完成标志 */
    uint16_t SamplingValue[ADC1_DMA_ARRAY_SIZE]; /* ADC 转换值 */
    uint16_t FilterValue; /* ADC 二阶滤波值 */
    float Voltage; /* ADC 电压值 */
    } ADC1_VarGroup_TypeDef;

    另外,如若要通过指针访问结构体,则需要注意字节对齐,可用以下方法自定义对齐:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    #pragma pack (push, 1) /* 作用:是指把原来对齐方式设置压栈,并设新的对齐方式设置为1个字节对齐 */
    typedef struct {
    uint8_t DMA_TC_Flag; /* DMA 传输完成标志 */
    uint8_t FilterComplete_Flag; /* ADC 滤波完成标志 */
    uint16_t SamplingValue[ADC1_DMA_ARRAY_SIZE]; /* ADC 转换值 */
    uint16_t FilterValue; /* ADC 二阶滤波值 */
    float Voltage; /* ADC 电压值 */
    } ADC1_VarGroup_TypeDef;
    #pragma pack(pop) /* 作用:恢复对齐状态 */
  10. 函数、变量、宏、结构体、联合体等的命名注意规范,缩写采用公认缩写,使名称易于阅读,不使用自造的冷门缩写。命名标准如下示例:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    void I2C_DataTransmit(uint8_t *pBuffer, uint8_t NumOfBytes);

    typedef enum {
    LEVEL_LOW = 0,
    LEVEL_HIGH = 1,
    } GPIO_LevelState_TypeDef;

    status_t PINS_DRV_Config(GPIO_TypeDef* GPIOx, uint16_t GPIO_Pin, GPIO_LevelState_TypeDef GPIO_State);

    uint8_t *g_pBuffer = NULL; /* 定义一个全局指针 */

    #define FPGA_PWR_EN 1U

    typedef struct {
    uint8_t Hall_W; /* 霍尔 */
    uint8_t Hall_V; /* 霍尔 */
    uint8_t Hall_U; /* 霍尔 */
    uint8_t Break_In; /* 刹车输入 */
    uint8_t FR_In; /* 正反转 */
    uint8_t EN_In; /* 电机使能 */
    } GPIO_PinLevel_TypeDef;
  11. 注意某些标准库函数以及其它自己编写的不可重入函数的使用,例如 printf(); 函数,开发调试时可适当于中断程序中使用,但在项目的Release阶段,务必检查中断中有无使用类似不可重入的不安全函数。

  12. 对于编译器优化,务必慎重使用。若不清楚编译器会对哪些函数进行优化,尽量不要使用,以避免优化过后的代码的实际执行情况与自己的真实想法背道而驰,另外,优化级别越高,越不利于调试。

  13. 对于一些全局变量,若只于单个c文件内使用,则使用static关键字修饰。

  14. 对于全局变量,一般情况下只允许一个修改者,可依据实际情况存在多个读取者。若有多个修改者,则务必给全局变量加上 互斥信号量(互斥锁) 进行保护。

  15. 对寄存器的操作,写操作尽量附带掩码(Mask)进行操作。在同一个函数中,对寄存器进行读操作时,先赋值给中间变量,再由中间变量进行之后的条件判断、赋值等操作,切忌直接使用寄存器进行复杂操作,特别是对于输入模式的引脚所对应的一系列寄存器,因为这些寄存器的值可能随时会因为外部电路上电平的变化而发生跳变,如果跳变时正好在使用该寄存器进行一系列连续的操作,则最终可能会出现意想不到的问题,与真实想法背道而驰,严重时会导致崩溃,进入HardFault等错误中断。

  16. 慎用递归,特别针对资源紧缺的平台。

  17. 慎用goto。

  18. 对于频繁变化的变量,尤其是在中断中变化的变量,在定义时务必加上volatile关键字修饰。

  19. 在没有硬件浮点单元(FPU)的MCU上进行开发,尽量少用或者不用浮点运算。

  20. 在中断中不要进行浮点运算(特别是触发频率较高的中断中),会导致效率低下!

  21. 注意字节对齐,例如4字节对齐、8字节对齐。

  22. 尽量不要使用除法运算,使用乘法、位移来替代除法运算。

  23. 定义位域时,注意不同硬件平台的大小端问题。

  24. 定义结构体时,注意字节对齐!按数据类型从小到大的顺序依次排列,可使内存占用更少,同时字节对齐,降低指针操作结构体成员时指向非法地址的可能性。

  25. 对于变量、函数、宏、枚举等的命名,若存在多个针对某一特定实例对象的定义,可把不同的文字置于名称末尾,方便IDE中编辑时的 自动联想 功能(Eclipse系的IDE使用 Alt + / 进行联想),例如:

    变量:

    1
    2
    uint8_t *pBuffer_Tx;
    uint8_t *pBuffer_Rx;

    函数:

    1
    2
    void LPI2C_Drv_Init(void);
    void LPI2C_Drv_Deinit(void);

    宏:

    1
    2
    #define FPGA_PWR_ON 1U
    #define FPGA_PWR_OFF 0U

    枚举:

    1
    2
    3
    4
    5
    6
    enum BEEP_RATE {
    BEEP_RATE_10MS = 10U,
    BEEP_RATE_100MS = 100U,
    BEEP_RATE_1S = 1000U,
    BEEP_RATE_5S = 5000U,
    };
  26. 理解逻辑移位算术移位的区别。

    逻辑左移 = 算数左移

    逻辑右移就是不考虑符号位,右移一位,左边补零即可。算术右移需要考虑符号位,右移一位,若符号位为1,就在左边补1;否则,就补0。所以算术右移也可以进行有符号位的除法,右移 N 位就等于除以2的n次方。

    例如,8位二进制数 11001101 分别右移一位。

    逻辑右移 算术右移
    [0]1100110 [1]1100110
  27. 条件判断语句、运算表达式、宏定义中,每一小部分都用括号显式包含起来,例如宏定义:

    1
    #define PWR_CHECK_OK ((BAT_CHECK_OK) && (ACC_CHECK_OK))
  28. 尽量显式初始化全局变量,未初始化的全局变量是“弱符号”,有潜在风险。举例:当使用一些外部静态库或动态库时,若库中存在同名且初始化的全局变量,则自己的全局变量会被覆盖,会造成莫明其妙的问题且难以追踪。

  29. 弱符号妙用之一:在项目的Release和Debug阶段,通过弱函数和强函数,可将Debug阶段的函数覆盖,以使其在Release阶段不产生效用。例如printf函数。

  30. 尽量只让一个函数实现一个单独的功能,而不是一个函数实现多个功能。

  31. 尽量控制单个函数的行数,一般情况下不要超过80行。

  32. 考虑代码的正确性、健壮性、可靠性、效率、易用性、可读性(可理解性)、可扩展性、可复
    用性、兼容性、可移植性

  33. 编译后生成bin文件:

    1
    C:\Keil_v5\ARM\ARMCC\bin\fromelf.exe --bin -o .\KeilProjFile.bin .\Objects\*.axf
  • 经验:

    1. 循环体要纯粹,尽量避免循环
    2. 循环嵌套时,次数多的安排在内层
    3. 循环中避免使用链表
    4. 循环体展开
  • 优化思路:

    1. 编译器优化开关,直接打开
    2. 利用外部profile工具,查找性能瓶颈,按照28法则进行优化。同时关注如何提高系统整体命中率
    3. 架构重构,使用不同的实现方法达到相同的目的 —— 优化幅度巨大
    4. 最优先考虑算法上的优化 —— 可观
    5. 源码级优化(通用手法)—— 一到两倍
    6. 根据特定平台进行汇编改写,使用高级指令 —— 提升10%~30%,很少使用
  • 通用源码优化手段:

    1. 避免浮点/除法运算
    2. 乘法改移位
    3. 开根号/求对数,使用快速简化运算
    4. 循环展开
    5. 循环内部小循环套大循环
    6. 循环体内避免出现判断语句
    7. 减少函数调用,对于纯粹的,调用频繁的函数进行内联
    8. 函数定义不能超过4个参数,如做不到请用结构体将参数打包,传入结构体指针
  • 链接:azillionmonkeys.com

    1. 把循环体里的判断干掉
    2. 对关键的变量用register(可能没用)
    3. else可以想办法去掉
    4. 定义二维数组时,外层一定要是2的倍数。int arr[3][23]改为int arr[3][32];
    5. 优化循环:for改为do…while, 并且要递减(效率最高的一种循环模式)
    6. 尽量避免赋值时的隐式类型转换(保证数据类型一致)
    7. 局部的函数定义为static
    8. ……

评论