前言
日常开发中常见糟糕工程问题:
1、引脚编号直接写在驱动函数内:GPIO_SetBits(GPIOA,GPIO_Pin_0);
2、驱动、业务逻辑混杂在同一个文件,裸机 / RTOS 项目难以复用;
3、头文件重复包含、全局变量随意 extern,编译警告层出不穷;
4、更换 PCB 硬件引脚,需要全局搜索修改,极易漏改引发 bug。
模块化的核心目标:硬件层与应用层分离,一处修改、全局生效,驱动跨项目复用。本文落地一套经过多个工控、环境监测项目验证的代码分层规范。
一、工程文件夹分层架构(推荐目录结构)
Project
├─ User // 应用层:业务逻辑、任务、指令解析
├─ Driver // 驱动层:外设底层驱动(oled、uart、sensor)
├─ Hardware // 硬件抽象层【重点】:引脚、时钟、硬件宏定义
├─ Core // 内核:启动文件、系统时钟、中断
├─ Middleware // 中间件:FreeRTOS、Modbus、协议栈
└─ System // 系统工具:延时、打印、数据转换工具函数
分层规则:
1、Hardware 层:只存放硬件相关定义,不实现业务函数;
2、Driver 驱动只能调用 Hardware 提供的宏,禁止直接写 GPIO 引脚;
3、User 应用层不能直接操作寄存器 / GPIO,只能调用 Driver 提供 API;
三层隔离:应用层 → 驱动层 → 硬件抽象层
二、头文件核心规范:防止重复包含,理清.h/.c 职责
2.1 基础准则
.c:存放变量、函数实现;禁止放置函数实现、大数组在.h
.h:函数声明、宏定义、结构体、类型声明
所有头文件必须增加头文件保护宏,杜绝重复包含报错
错误示范:
// bad.h 错误写法
void Led_TurnOn(void)
{
GPIO_SetBits(GPIOA,GPIO_Pin_0);
}
标准规范写法:
#ifndef __LED_H
#define __LED_H
void Led_Init(void);
void Led_TurnOn(void);
void Led_TurnOff(void);
#endif
2.2 extern 使用禁忌
不要在头文件定义全局变量,仅做声明:
// .h
extern uint8_t g_sensor_data;
// .c
uint8_t g_sensor_data = 0;
三、硬件引脚集中宏管理(核心方案)
建立 hardware_gpio.h,所有硬件引脚统一管理,整个项目只在此文件修改引脚。
更换 PCB 硬件,只修改此文件,驱动文件一行不动。
hardware_gpio.h
#ifndef __HARDWARE_GPIO_H
#define __HARDWARE_GPIO_H
#include “stm32f10x.h”
/******************** LED硬件定义 ********************/
#define LED_RCC RCC_APB2Periph_GPIOA
#define LED_PORT GPIOA
#define LED_PIN GPIO_Pin_0
/******************** 继电器硬件定义 ********************/
#define RELAY_RCC RCC_APB2Periph_GPIOB
#define RELAY_PORT GPIOB
#define RELAY_PIN GPIO_Pin_5
/******************** 串口引脚 ********************/
#define USART1_TX_RCC RCC_APB2Periph_GPIOA
#define USART1_TX_PORT GPIOA
#define USART1_TX_PIN GPIO_Pin_9
#define USART1_RX_RCC RCC_APB2Periph_GPIOA
#define USART1_RX_PORT GPIOA
#define USART1_RX_PIN GPIO_Pin_10
#endif
驱动层 led.c(只引用硬件宏,无任何固定引脚)
#include “led.h”
#include “hardware_gpio.h”
void Led_Init(void)
{
GPIO_InitTypeDef GPIO_InitStruct;
RCC_APB2PeriphClockCmd(LED_RCC, ENABLE);
GPIO_InitStruct.GPIO_Pin = LED_PIN;
GPIO_InitStruct.GPIO_Mode = GPIO_Mode_Out_PP;
GPIO_InitStruct.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(LED_PORT, &GPIO_InitStruct);
Led_TurnOff();
}
void Led_TurnOn(void)
{
GPIO_SetBits(LED_PORT, LED_PIN);
}
void Led_TurnOff(void)
{
GPIO_ResetBits(LED_PORT, LED_PIN);
}
led.h
#ifndef __LED_H
#define __LED_H
void Led_Init(void);
void Led_TurnOn(void);
void Led_TurnOff(void);
#endif
四、进阶:参数宏统一管理(波特率、传感器地址)
新建 hardware_config.h,存放硬件参数,和引脚文件分离:
#ifndef __HARDWARE_CONFIG_H
#define __HARDWARE_CONFIG_H
#define USART1_BAUDRATE 9600
#define SHT30_I2C_ADDR 0x44
#define OLED_I2C_SPEED 400000
#endif
串口初始化、传感器驱动直接引用宏,调试参数不用到处查找。
五、HAL 库适配方案(通用架构,无缝迁移)
如果项目使用 HAL 库,架构思路完全一致,仅宏定义格式调整:
// hardware_gpio.h HAL示例
#define LED_GPIO_Port GPIOA
#define LED_Pin GPIO_PIN_0
驱动函数使用 HAL 库 API 调用宏,一套架构同时兼容标准库、HAL 库项目。
六、模块化架构带来的工程优势(项目实战总结)
硬件移植成本极低:改版 PCB 只修改 hardware 层文件;
驱动代码可跨项目复制:oled、传感器驱动不需要改动;
代码可读性强,新人接手快速定位硬件相关配置;
便于版本管理:硬件变更只改动少量文件,git 对比清晰;
适配 RTOS 大型项目,驱动与业务完全解耦。
七、开发常见踩坑汇总
❌ 禁止在.c 以外文件写硬件引脚常量;
❌ 不要直接复制驱动文件后修改内部引脚,破坏可移植性;
❌ 头文件循环包含:A.h 包含 B.h,B.h 又包含 A.h;
✅ 解决方案:尽量减少跨头文件相互引用,在.c 引入所需头;
❌ 宏命名混乱,统一规范:硬件宏全大写,模块名_功能名;
❌ 驱动内部直接调用 main.c 全局变量,应当提供函数接口交互。
八、完整调用示例(main.c)
#include “led.h”
int main(void)
{
Led_Init();
while(1)
{
Led_TurnOn();
delay_ms(500);
Led_TurnOff();
delay_ms(500);
}
}
结语
模块化不是形式,是长期项目维护的刚需。小型裸机项目可能感受不到优势,但环境监测、无线采集、多传感器综合项目中,规范架构能极大减少调试 bug。这套架构可以直接套用在我专栏前面的 LoRa、Modbus、WiFi 监测仪、FreeRTOS 所有工程,统一代码风格。
网硕互联帮助中心



评论前必须登录!
注册