|
关注公众号,回复“入门资料”获取单片机入门到高级开挂教程
开发板带你入门,我们带你飞
文 | 无际(微信:2777492857)
全文约6352字,阅读大约需要 10 分钟
你有没有过这样的经历?
刚入职一家公司,兴致勃勃地打开项目代码,准备大干一场,结果:
变量名是 a、b、tmp,函数名是 func1、process,参数列表密密麻麻,不知道传进去的是什么鬼。
宏定义和常量散落在文件的各个角落,想改个参数得全文件搜索。
同一个逻辑,一会儿用 if else,一会儿用 switch case,一会儿又来个 goto。
代码块的缩进,大括号一会儿在行尾,一会儿在新行,风格随机切换。
最要命的是,注释呢?它在哪里?哦,找到了,一行 // 初始化 孤零零地立在那,告诉你这行代码是用来初始化的,而你早就知道它干的是初始化的活,但它具体初始化了啥,为啥这么初始化,背景信息一概没有。
你瞪大了双眼,感觉不是在读代码,而是在破前任的案。心里的骂骂咧咧:这到底是哪位大神的杰作?是想考验我的智商还是耐心?
别装了,我知道你懂。甚至,你可能正在挠头想,我写的代码,在别人眼里会不会也是这样?
以上,我们每个人或多或少都遇到过,或者制造过的。
别小看这些写代码的小习惯、小毛病。它们积累起来,就像雪球一样,越滚越大,最终可能压垮整个项目。
当你的代码难以阅读时,调试效率,维护成本,稳定性等等都原地爆炸。
这玩意,就跟停车一样的,作为老司机,我一向方便别人,就是方便自己。
同样,规范化编程不仅仅是为了让代码看起来漂亮,更是为了提升效率、降低成本、保障质量、促进协作,最终成就更好的产品和更优秀的自己。
通过这篇文章,你将Get到:
?一套行之有效的单片机C语言编程规范指南。
?理解为何这些规范至关重要。
?掌握写出清晰、易读、易维护代码的秘诀。
?学会如何通过代码展现你的专业性,让你的同事在review你的代码时眼前一亮,发自内心地为你点赞!
系好安全带,发车!
1. 命名规范
名字是代码的身份证。好的命名能让你一眼看穿代码的意图,坏的命名则让你抓狂。
1.1 变量名: 使用描述性强的英文单词或缩写,体现变量的用途和类型。采用驼峰命名法或下划线命名法,并在团队内统一。局部变量可以用较短的名称,全局变量和静态变量需要更长的、带有前缀的名称。
?示例:int sensor_value; (下划线) 或 int sensorValue; (驼峰)
?避免:int val;, int tmp;
1.2 常量(宏定义 #define 或 const):全部大写,单词之间用下划线分隔。体现常量的含义。
?示例:#define MAX_BUFFER_SIZE 256, const int MOTOR_SPEED_HIGH = 1000;
?避免:#define SIZE 256, const int highSpeed = 1000;
1.3 函数名: 使用动词或动宾短语,体现函数的功能。采用驼峰命名法或下划线命名法,与变量名风格一致。
?示例:void init_system(void); 或 void initSystem(void);, int read_sensor_data(void); 或 int readSensorData(void);
?避免:void init();, int getData();, void process();
1.4 枚举类型(enum)和结构体类型(struct):使用有意义的名称,通常首字母大写,或加特定前缀(如 e_,s_)。枚举成员全部大写,单词之间用下划线分隔。
?示例:typedef enum { STATE_IDLE, STATE_RUNNING, STATE_ERROR } e_system_state;
?示例:typedef struct { uint16_t voltage; uint16_t current; } s_sensor_data;
很多人可能和我一样,英语不好,如果每个细节都这样做,太耗时间了,能按时完成任务都不错了。
以前你这样说,我非常理解你,因为我也是这样干的,哪有精力管这么多。
不过现在有AI的帮忙,几分钟就搞定了,你可以先写个初步的,然后让AI按照命名规则帮你优化变量名、常量、函数名,效率起飞又规范。
2. 代码排版
整洁的排版能大幅提升代码的可读性。
缩进: 使用空格进行缩进,不建议使用Tab(因为Tab在不同编辑器显示宽度不同)。通常使用4个空格作为一个缩进级别,并在团队内强制统一。
大括号 {}:采用Allman风格,即左大括号 { 放在新的一行,与对应的关键字(如 if, for, while, 函数名)对齐。右大括号 } 也独占一行,与左大括号对齐。
?示例:
efhe1wia2i364040824411.png
空行: 使用空行分隔不同的逻辑块、函数定义、变量声明等,增加代码的层次感和可读性。
空格: 在运算符两侧、逗号后、类型转换后添加空格,例如 a = b + c;, func(arg1, arg2);, (uint8_t)value;。
3. 注释:代码的向导
好的注释不是越多越好,而是恰到好处,能解释清楚代码的“为什么”和“是什么”,而不是简单复述“怎么做”。
?文件头部注释: 包含文件名、作者、创建日期、版本信息、文件功能简述等。
?函数头部注释: 说明函数的功能、参数(名称、含义、取值范围)、返回值(含义、可能的值)、可能的副作用、函数调用时的注意事项等。
?关键代码块注释: 对于复杂的逻辑、巧妙的实现、潜在的陷阱,或者与硬件紧密相关的操作,要添加解释性注释。
?常量和宏注释: 说明其用途和含义。
?避免: 无意义的注释,例如 i++; // i自增1。
?代码示例:
/* * 文件名: sensor_module.h * 作者: 无际单片机编程 * 创建日期: 2025-4-21 * 版本: 1.0 * 描述: 提供与传感器模块交互相关的函数和定义。 * 主要包括传感器初始化、数据读取和状态获取功能。 */#ifndef __SENSOR_MODULE_H__ // 防止头文件被重复包含的宏定义#define __SENSOR_MODULE_H__#include // 包含标准整数类型定义/* * @brief 定义传感器数据状态的枚举类型 */typedef enum{ SENSOR_STATUS_OK = 0, // 传感器工作正常 SENSOR_STATUS_ERROR_INIT, // 传感器初始化失败 SENSOR_STATUS_ERROR_READ // 传感器数据读取失败 // 可以根据实际情况添加更多状态} e_sensor_status;/* * @brief 定义传感器原始数据结构体 */typedef struct{ uint16_t raw_value; // 传感器原始ADC值或其他原始数据 // 根据传感器类型可以添加更多字段} s_sensor_raw_data;/* * @brief 定义处理后的传感器数据结构体 */typedef struct{ float processed_value; // 例如,转换为实际物理量的数值 // 根据需求可以添加更多字段} s_sensor_processed_data;/* * @brief 初始化传感器模块 * @param 无 * @return e_sensor_status: 初始化结果状态 * @retval SENSOR_STATUS_OK: 初始化成功 * @retval SENSOR_STATUS_ERROR_INIT: 初始化失败 */e_sensor_status sensor_init(void);/* * @brief 从传感器读取原始数据 * @param raw_data_ptr: 指向存储原始数据的结构体的指针 * @return e_sensor_status: 数据读取结果状态 * @retval SENSOR_STATUS_OK: 读取成功 * @retval SENSOR_STATUS_ERROR_READ: 读取失败 */e_sensor_status sensor_read_raw_data(s_sensor_raw_data *raw_data_ptr);/* * @brief 处理传感器原始数据,转换为可用数值 * @param raw_data: 传感器原始数据结构体 * @param processed_data_ptr: 指向存储处理后数据的结构体的指针 * @return e_sensor_status: 数据处理结果状态 (通常处理不涉及硬件,总是成功,但仍返回以保持接口一致性) * @retval SENSOR_STATUS_OK: 处理成功 */e_sensor_status sensor_process_data(const s_sensor_raw_data *raw_data, s_sensor_processed_data *processed_data_ptr);/* * @brief 获取当前传感器模块的状态 * @param 无 * @return e_sensor_status: 当前传感器模块的运行状态 */e_sensor_status sensor_get_status(void);#endif // __SENSOR_MODULE_H__
/* * 文件名: sensor_module.c * 作者: 无际单片机编程 * 创建日期: 2025-4-21 * 版本: 1.0 * 描述: 传感器模块的实现文件。包含硬件初始化、数据采集和处理逻辑。 */#include "sensor_module.h" // 包含对应的头文件#include "hardware_abstraction.h" // 假设这是一个抽象硬件操作的头文件#include // 示例需要用到printf,实际单片机可能不需要// 全局变量:存储传感器模块的当前状态,使用g_前缀表示全局static e_sensor_status g_current_sensor_status = SENSOR_STATUS_ERROR_INIT;/* * @brief 初始化传感器模块 * @param 无 * @return e_sensor_status: 初始化结果状态 * @retval SENSOR_STATUS_OK: 初始化成功 * @retval SENSOR_STATUS_ERROR_INIT: 初始化失败 * * @note 该函数会配置传感器相关的GPIO、ADC通道等硬件资源。 * 如果硬件配置失败,则返回错误状态。 */e_sensor_status sensor_init(void){ e_sensor_status status = SENSOR_STATUS_ERROR_INIT; // TODO: 调用硬件抽象层函数进行传感器硬件初始化 // 假设这里调用了HAL库或者底层的驱动函数 if (HAL_Sensor_Init() == HAL_OK) { // 硬件初始化成功后,设置模块状态为OK g_current_sensor_status = SENSOR_STATUS_OK; status = SENSOR_STATUS_OK; // 重要代码块注释:初始化完成后进行一次传感器自检,确保工作正常 if (HAL_Sensor_SelfTest() != HAL_OK) { g_current_sensor_status = SENSOR_STATUS_ERROR_INIT; // 自检失败,更新状态 status = SENSOR_STATUS_ERROR_INIT; // TODO: 记录错误日志或者触发报警 } } else { // 硬件初始化失败 g_current_sensor_status = SENSOR_STATUS_ERROR_INIT; status = SENSOR_STATUS_ERROR_INIT; // TODO: 记录错误日志或者触发报警 } return status;}/* * @brief 从传感器读取原始数据 * @param raw_data_ptr: 指向存储原始数据的结构体的指针 * @return e_sensor_status: 数据读取结果状态 * @retval SENSOR_STATUS_OK: 读取成功 * @retval SENSOR_STATUS_ERROR_READ: 读取失败 * * @note 读取前会检查传感器当前状态。如果状态异常,则不执行读取操作。 * 读取完成后会将读取到的原始值存入指定的结构体。 */e_sensor_status sensor_read_raw_data(s_sensor_raw_data *raw_data_ptr){ // 参数有效性检查,虽然简单的例子可以省略,但在实际项目中很重要 if (raw_data_ptr == NULL) { // TODO: 处理空指针错误,例如断言或者返回错误码 return SENSOR_STATUS_ERROR_READ; // 或者定义一个专门的参数错误状态 } // 关键判断注释:只有当传感器状态正常时才尝试读取数据 if (g_current_sensor_status != SENSOR_STATUS_OK) { // printf("Sensor not in OK state, cannot read.
"); // 调试信息,实际单片机可省略或用Log替代 return SENSOR_STATUS_ERROR_READ; // 传感器状态异常,返回读取失败 } uint16_t adc_value = 0; e_sensor_status status = SENSOR_STATUS_ERROR_READ; // TODO: 调用硬件抽象层函数从ADC读取原始值 // 假设HAL_ADC_Read() 返回ADC值,失败返回特定的错误值或通过返回值检查 if (HAL_ADC_Read(&adc_value) == HAL_OK) // 假设HAL_ADC_Read有返回值指示成功或失败 { raw_data_ptr->raw_value = adc_value; status = SENSOR_STATUS_OK; } else { // 读取硬件失败 g_current_sensor_status = SENSOR_STATUS_ERROR_READ; // 更新模块状态为读取失败 status = SENSOR_STATUS_ERROR_READ; // TODO: 记录错误日志或者触发报警 } return status;}/* * @brief 处理传感器原始数据,转换为可用数值 * @param raw_data: 传感器原始数据结构体 * @param processed_data_ptr: 指向存储处理后数据的结构体的指针 * @return e_sensor_status: 数据处理结果状态 (通常处理不涉及硬件,总是成功,但仍返回以保持接口一致性) * @retval SENSOR_STATUS_OK: 处理成功 * * @note 该函数执行线性转换或其他复杂的算法,将原始ADC值转换为实际物理量。 * 例如,将ADC值转换为电压、温度、湿度等。 */e_sensor_status sensor_process_data(const s_sensor_raw_data *raw_data, s_sensor_processed_data *processed_data_ptr){ // 参数有效性检查 if (raw_data == NULL || processed_data_ptr == NULL) { // TODO: 处理空指针错误 return SENSOR_STATUS_ERROR_READ; // 或定义专门的参数错误状态 } // 关键算法注释:将12位ADC原始值(范围0-4095)转换为电压值(假设参考电压3.3V) // 公式: Voltage = (ADC_Value / 4095.0) * 3.3V const float ADC_MAX_VALUE = 4095.0f; // 使用命名常量代替魔术数字,并添加注释说明含义 const float REFERENCE_VOLTAGE = 3.3f; // 参考电压值 processed_data_ptr->processed_value = ((float)raw_data->raw_value / ADC_MAX_VALUE) * REFERENCE_VOLTAGE; // TODO: 如果需要更复杂的转换(例如,非线性、查表),在此实现并添加详细注释说明算法或公式。 return SENSOR_STATUS_OK; // 数据处理通常不会失败,除非输入参数无效}/* * @brief 获取当前传感器模块的状态 * @param 无 * @return e_sensor_status: 当前传感器模块的运行状态 */e_sensor_status sensor_get_status(void){ return g_current_sensor_status;}// 假设的硬件抽象层函数声明,实际应在 hardware_abstraction.h 中定义/*HAL_OK: 假设的硬件操作成功返回值HAL_ERROR: 假设的硬件操作失败返回值*/typedef int HAL_StatusTypeDef; // 假设HAL状态类型是int#define HAL_OK 0#define HAL_ERROR -1HAL_StatusTypeDef HAL_Sensor_Init(void); // 初始化硬件HAL_StatusTypeDef HAL_Sensor_SelfTest(void); // 传感器自检HAL_StatusTypeDef HAL_ADC_Read(uint16_t *value); // 从ADC读取值// 假设这些函数有简单的实现,以便编译示例HAL_StatusTypeDef HAL_Sensor_Init(void) { /*printf("HAL: Initializing sensor...
");*/ return HAL_OK; }HAL_StatusTypeDef HAL_Sensor_SelfTest(void) { /*printf("HAL: Sensor self-test...
");*/ return HAL_OK; } // 或返回HAL_ERROR模拟失败HAL_StatusTypeDef HAL_ADC_Read(uint16_t *value) { /*printf("HAL: Reading ADC...
");*/ if (value) *value = 2048; return HAL_OK; } // 模拟读取一个值
4. 函数
函数是构建程序的模块。
?单一职责原则: 一个函数只做一件事情,并把它做好。函数功能越单一,就越容易理解、测试和重用。
?控制函数长度: 尽量保持函数简短,理想情况下一个函数不超过一屏幕。过长的函数往往意味着它做了太多事情,可以考虑拆分成更小的函数。
?明确的接口: 函数参数和返回值应清晰地表达输入和输出。避免使用全局变量作为函数的主要输入或输出。
?代码示例:
#include // 包含标准整数类型定义#include // 示例需要用到printf,实际单片机可能需要替换为其他输出方式// 定义一些常量,符合命名规范,用于电压转换计算const uint16_t ADC_MAX_VALUE = 4095; // 假设ADC是12位的,最大值为2^12 - 1const float REFERENCE_VOLTAGE = 3.3f; // 假设ADC的参考电压是3.3V/* * @brief 从传感器硬件读取原始ADC值 * @details 这个函数只负责与底层硬件交互,获取原始的ADC数值。 * 它不知道这个值代表什么物理量,也不进行任何转换。 * @param 无 * @return uint16_t: 读取到的原始ADC值 */uint16_t sensor_read_raw_adc(void){ // TODO: 这里模拟从硬件ADC通道读取原始数据 // 实际代码会调用底层驱动或HAL库函数,例如: // uint16_t raw_value = HAL_ADC_GetValue(ADC_CHANNEL_SENSOR); // 假设读取硬件可能失败,但为了示例简洁,这里省略复杂的错误处理和返回值检查 // 实际应用中需要检查硬件读取函数的返回值并处理错误 uint16_t raw_value = 2048; // 模拟读取到的一个中间值,例如参考电压的一半 return raw_value;}/* * @brief 将原始ADC值转换为实际电压值 * @details 这个函数只负责执行数学计算,将原始ADC数据按照特定公式转换为电压。 * 它不负责读取硬件,也不负责后续的数据使用(如打印)。 * @param raw_adc_value: uint16_t 类型的原始ADC值 * @return float: 转换后的电压值 (单位V) */float sensor_convert_adc_to_voltage(uint16_t raw_adc_value){ // 参数校验:确保输入值不超过ADC最大值,避免潜在的转换问题 if (raw_adc_value > ADC_MAX_VALUE) { // 可以选择记录警告、返回一个特殊值(如NaN)或限制输入 // 为了示例简单,这里直接限制输入值 raw_adc_value = ADC_MAX_VALUE; } // 执行转换计算: 电压 = (原始值 / 最大原始值) * 参考电压 // 使用浮点数进行精确计算 float voltage = ((float)raw_adc_value / ADC_MAX_VALUE) * REFERENCE_VOLTAGE; return voltage;}/* * @brief 完成传感器数据采集、转换并报告的任务流程 * @details 这个函数作为一个协调者,依次调用读取原始数据和转换数据的函数, * 然后将最终结果进行报告(例如打印到控制台)。 * 它不关心底层硬件细节,也不关心具体的转换公式。 * @param 无 * @return 无 */void sensor_process_and_report_voltage(void){ // 步骤1: 调用负责读取原始数据的函数 // sensor_read_raw_adc() 函数隐藏了底层硬件读取的复杂性 uint16_t raw_data = sensor_read_raw_adc(); // 步骤2: 调用负责数据转换的函数 // sensor_convert_adc_to_voltage() 函数隐藏了原始数据到电压的转换公式 float voltage_value = sensor_convert_adc_to_voltage(raw_data); // 步骤3: 报告最终的处理结果 // 在实际单片机应用中,这里可能是将值发送到UART、更新LCD显示、存储到内存等 // 这里使用printf作为示例报告方式 printf("Sensor Raw ADC: %u, Converted Voltage: %.2f V
", raw_data, voltage_value); // TODO: 根据电压值做进一步的逻辑判断或控制输出,例如判断是否超限 // if (voltage_value > VOLTAGE_UPPER_THRESHOLD) { trigger_warning(); }}// 一个简单的main函数来演示调用过程 (在单片机中通常是while(1)主循环调用任务函数)int main(void){ // 在一个典型的单片机应用中,这里会有系统、外设的初始化调用 // 例如:System_Init(); HAL_ADC_Init(); etc. printf("Starting sensor data processing example...
"); // 调用负责整个任务流程的函数 // sensor_process_and_report_voltage() 函数是整个任务的入口 sensor_process_and_report_voltage(); printf("Sensor data processing finished.
"); // 在嵌入式系统中,main函数通常不会返回,而是进入一个无限循环或操作系统调度 // while(1); return 0; // 为了在标准PC环境下编译运行示例,这里保留return 0}
5. 变量:按需分配,谨慎使用全局
?局部变量优先: 优先使用局部变量,它们的生命周期和作用域都在函数内部,更容易管理,也能减少不同函数间的耦合。
?谨慎使用全局变量: 全局变量可能在程序的任何地方被修改,导致难以追踪问题。如果必须使用全局变量,考虑加上特定的前缀(如 g_)或使用 static 限制其作用域在当前文件内。
?变量声明位置: 在函数或代码块的开头声明变量,并在声明时初始化。
6. 消灭一切神秘数字
代码中突然出现的数字(如 if (state == 5), delay_ms(100); 这里的 5 和 100)被称为神秘数字,它们让代码难以理解和修改。
?使用命名常量或宏定义: 用有意义的名称代替魔术数字。
?示例:#define STATE_PROCESSING 5, #define DELAY_SHORT_MS 100
?使用:if (state == STATE_PROCESSING), delay_ms(DELAY_SHORT_MS);
?使用枚举类型: 对于一组相关的整数常量,使用 enum 是更好的选择。
7. 头文件:干净整洁的接口定义
?包含保护(Include Guards): 在每个头文件开头使用 #ifndef, #define, #endif 来防止头文件被重复包含。
?示例:
#ifndef __MY_MODULE_H__#define __MY_MODULE_H__#endif // __MY_MODULE_H__
?只包含必要的头文件: 在 .c 文件中,只包含它需要的头文件。在 .h 文件中,只包含定义了其中声明的类型、常量或函数签名的头文件。避免不必要的依赖。
8. 错误处理与断言
?返回值检查: 对于可能失败的函数调用,检查其返回值,并采取相应的错误处理措施。
?断言(assert): 在开发和调试阶段,使用 assert 来检查不可能发生的情况或函数的前提条件。如果在运行时断言失败,程序会终止,帮助你快速定位问题。在发布版本中通常会关闭断言。
这个在esp32代码库其实有大量的应用,特别是对于一些指针,内存分配等等。
9. 控制结构
?避免过多的嵌套: 尽量减少 if-else 或循环的嵌套层数,超过三层就需要警惕,考虑提取函数。
?谨慎使用 goto: goto 语句容易破坏代码的线性流程,使代码难以理解和维护。只有在少数特定场景(如跳出多重循环、统一的错误出口)下才考虑使用,并务必谨慎。
咱们写的每一行代码,不仅仅是为了实现功能,更是你专业素养的体现。规范的代码,是工程师之间无声的交流,也是留给未来自己最好的礼物。
end
oxqz2in2swf64040824511.jpg
下面是更多无际原创的个人成长经历、行业经验、技术干货。
1.电子工程师是怎样的成长之路?10年5000字总结
2.如何快速看懂别人的代码和思维
3.单片机开发项目全局变量太多怎么管理?
4.C语言开发单片机为什么大多数都采用全局变量的形式?
5.单片机怎么实现模块化编程?实用程度让人发指!
6.c语言回调函数的使用及实际作用详解
7.手把手教你c语言队列实现代码,通俗易懂超详细!
8.c语言指针用法详解,通俗易懂超详细! |
|