| 原层级名称 | 文件夹名 | 规范说明 |
|---|---|---|
| 硬件驱动层 | hardware_drivers | 全语义命名,明确存放硬件驱动代码,与软件代码完全隔离 |
| 功能模块层 | function_modules | 明确存放纯功能组件,与业务代码完全隔离,边界清晰 |
| 应用接口层 | application_interface | 明确存放应用层接口定义与封装,统一全项目接口规范 |
| 业务逻辑层 | business_logic | 明确存放业务逻辑代码,与底层技术完全解耦 |
| 应用层 | application | 全语义命名,明确存放顶层应用、用户交互代码 |
| 层级 | 餐厅角色 | 核心职责 | 只关心什么? | 不关心什么? |
|---|---|---|---|---|
| 应用层 | 前台 / 服务员 | 1. 接待客人(接收用户按键 / 触摸)2. 给客人看菜单 / 上菜(OLED 显示数据)3. 把客人点的单递给后厨 | 用户怎么看、怎么操作 | 菜怎么做、后厨流程是什么 |
| 业务逻辑层 | 后厨 / 厨师长 | 1. 按菜单做菜(5 秒采集、判断 35℃)2. 控制出菜流程(上报云平台)3. 处理异常(记录日志) | 业务规则是什么、流程怎么走 | 用盘子装还是用碗装、客人长什么样 |
1. 业务逻辑层(business):纯规则,无 UI
**这个文件夹里的代码,绝对不出现 "OLED"、"LCD"、"按键" 这些字眼!**它只默默在后台干活,通过 "函数返回值" 或 "全局变量 / 消息队列" 把结果告诉上层。
// business_logic.c (业务逻辑层核心文件)
// 【业务规则1】:处理温湿度采集与报警
void Business_ProcessTempHumi(void) {
// 1. 调用下层API拿数据(不关心数据怎么来的)
float temp = API_GetTemperature();
float humi = API_GetHumidity();
// 2. 业务规则判断:超过35℃?(不关心报警是响铃还是屏幕显示)
if (temp > g_tempThreshold) {
g_isAlarmActive = true; // 置报警标志位
Business_LogError("Temperature too high!"); // 记录日志
} else {
g_isAlarmActive = false;
}
// 3. 业务规则:上报云平台(不关心上报成功后界面要不要提示)
if (g_timeToReport) {
API_MqttPublish(temp, humi);
}
}
// 【业务规则2】:设置温度阈值(只关心规则,不关心是按键设置还是APP设置)
void Business_SetTempThreshold(float newThreshold) {
if (newThreshold > 0 && newThreshold < 100) { // 业务合法性校验
g_tempThreshold = newThreshold;
Business_LogInfo("Threshold updated.");
}
}
2. 应用层(app):纯 UI / 交互,无规则
这个文件夹里的代码,绝对不出现 "35℃"、"5 秒"、"MQTT" 这些业务字眼! 它只负责两件事:画界面 和响应用户按键。
// app_ui.c (应用层核心文件)
// 【应用层职责1】:刷新OLED显示(只关心怎么画,不关心数据哪来的、规则是什么)
void App_RefreshScreen(void) {
// 1. 从业务逻辑层"拿状态"(但不参与判断)
float showTemp = g_currentTemp; // 直接拿变量
float showHumi = g_currentHumi;
bool showAlarm = g_isAlarmActive;
int wifiStatus = API_GetWifiStatus(); // 也可以调用API拿
// 2. 纯UI绘制(这里全是画线、画字的代码,没有任何业务逻辑)
OLED_Clear();
OLED_ShowString(0, 0, "Temp: ");
OLED_ShowFloat(0, 1, showTemp); // 显示温度,不管它是不是超过35度
if (showAlarm) { // 只是根据标志位决定"要不要画个感叹号",不做判断
OLED_ShowString(100, 0, "!ALARM!");
}
// 显示WiFi图标
if (wifiStatus == WIFI_CONNECTED) {
OLED_DrawWifiIcon(80, 0);
}
}
// 【应用层职责2】:处理按键(只关心用户按了什么,不关心业务怎么处理)
void App_KeyHandler(void) {
if (Key_Scan() == KEY_PLUS) {
// 用户按了加号,我只负责把"加阈值"的请求传给业务层
// 绝对不在这里写 if (threshold > 35) 这种业务判断!
float newVal = g_tempThreshold + 0.5;
Business_SetTempThreshold(newVal); // 甩锅给业务逻辑层
}
if (Key_Scan() == KEY_OK) {
// 用户按了确定,手动触发上报
// 我只负责触发,不关心上报的具体协议和流程
Business_TriggerManualReport();
}
}
三、终极判断标准:做个 "换屏测试"
想知道自己分得对不对?想象一下:
如果我明天把 OLED 屏换成了 LCD 屏,或者甚至把屏幕拆了换成手机 APP 控制,我需要改哪层的代码?
- 需要改的 :应用层(因为显示方式变了,交互方式变了)。
- 一行都不用改的 :业务逻辑层(因为 "5 秒采集、35℃报警" 这个核心规则没变)。
这就是分层的最大意义:解耦! 以后产品经理说 "把报警阈值改成 40℃",你只需要改 business 里的一行代码;说 "把界面颜色改成红色",你只需要改 app 里的绘图代码,两者互不干扰。
| 层级 | 角色定位 | 核心职责 | 关键词 |
|---|---|---|---|
| 功能模块层 | 工具箱里的工具 | 提供纯技术能力(比如 "一把螺丝刀"、"一个计算器"),它不知道 "业务" 是什么,只知道 "我能拧螺丝"、"我能算数学题"。 | 纯技术、无业务、可复用 |
| 应用接口层 | 工具箱的统一把手 | 把工具包装一下,给上层一个简单的操作入口。它告诉上层:"你不用管螺丝刀怎么造的,按这个按钮就能拧螺丝"。 | 封装、统一、屏蔽细节 |
二、实战例子 1:MQTT 云平台上报
我们要实现 "把温湿度数据上报到阿里云" 这个需求。
1. 功能模块层(function_modules):纯 MQTT 协议工具
这里的代码完全不知道 "温湿度" 是什么,它只知道 "MQTT 协议怎么连、怎么发消息"。这是一个可以放到任何项目里用的通用模块。
// function_modules/mqtt_protocol.c (功能模块层:纯MQTT工具)
// 【纯技术功能】:连接MQTT服务器(不关心连的是阿里云还是腾讯云)
int MQTT_Connect(char *broker_ip, int port, char *client_id) {
// 这里全是底层的Socket连接、MQTT握手报文、Keepalive设置
// 没有任何业务逻辑,就是纯协议实现
return socket_status;
}
// 【纯技术功能】:发布消息(不关心发的是温湿度还是开关状态)
int MQTT_Publish(char *topic, char *payload, int qos) {
// 这里只负责把payload字符串通过MQTT发出去
// 它甚至不认识payload里的内容
return mqtt_result;
}
2. 应用接口层(application_interface):封装成业务接口
这里的代码把上面的通用工具,包装成了业务逻辑层能用的 "专属接口"。它屏蔽了 "MQTT Topic 是什么"、"QoS 是 0 还是 1" 这些技术细节。
// application_interface/cloud_api.c (应用接口层:统一封装)
// 【统一接口】:上报温湿度(给业务逻辑层用的)
void API_ReportTempHumi(float temp, float humi) {
// 1. 把业务数据(温湿度)转换成技术格式(JSON字符串)
char json_payload[128];
sprintf(json_payload, "{\"temp\":%.1f, \"humi\":%.1f}", temp, humi);
// 2. 调用功能模块层的"纯工具",但屏蔽了技术细节
// 业务层不需要知道Topic是 "/device/sensor/data",也不需要知道QoS是1
MQTT_Publish("/device/sensor/data", json_payload, 1);
}
// 【统一接口】:初始化云连接(业务层不用管IP和端口)
void API_CloudInit(void) {
// 直接封装好连接阿里云的固定参数
MQTT_Connect("aliyun.com", 1883, "dev_001");
}
三、实战例子 2:温湿度数据滤波
传感器读出来的数据通常有毛刺,我们需要滤波。
1. 功能模块层(function_modules):纯算法工具
这里的代码不知道 "温度" 是什么,它只知道 "给我一组数组,我能算出平均值"。这是一个数学工具。
// function_modules/filter.c (功能模块层:纯算法工具)
// 【纯技术功能】:滑动均值滤波(不关心输入的是温度还是电压)
float Filter_MovingAverage(float *data_buf, int buf_size) {
float sum = 0;
for (int i = 0; i < buf_size; i++) {
sum += data_buf[i];
}
return sum / buf_size; // 纯数学计算,没有业务
}
2. 应用接口层(application_interface):封装成业务接口
这里的代码把算法和传感器驱动捏合在一起,给上层一个 "一键拿干净数据" 的接口。
// application_interface/sensor_api.c (应用接口层:统一封装)
// 定义一个全局缓冲区(应用接口层负责管理数据缓存)
static float temp_buf[10];
// 【统一接口】:获取"干净的"温度(业务层的最爱)
float API_GetCleanTemperature(void) {
// 1. 调用底层驱动(硬件驱动层),读一个"脏数据"
float raw_temp = DRV_SHT30_ReadRawTemp();
// 2. 把脏数据放进缓冲区(应用接口层负责维护这个流程)
for (int i = 0; i < 9; i++) temp_buf[i] = temp_buf[i+1];
temp_buf[9] = raw_temp;
// 3. 调用功能模块层的"纯算法工具"滤波
// 业务层不需要知道缓冲区是10个还是20个,也不需要知道用的是均值滤波
return Filter_MovingAverage(temp_buf, 10);
}
好的!我们用 **"一条完整的温湿度数据双向流转链路",把五个层级像串糖葫芦一样串起来,结合 大白话定位 **、"吃什么 / 产什么" 、"绝对禁区"和项目实例,保证你彻底通透。
整体链路预览
我们先看两条核心数据流,建立全局认知:
- 上行链路(硬件→用户):传感器采集数据 → 最终显示在 OLED 屏上
- 下行链路(用户→硬件):用户按下按键设置阈值 → 最终保存到硬件 Flash
层级 1:硬件驱动层(hardware_drivers)
🎯 核心定位
系统的 **"手脚与神经末梢"**,唯一直接触碰物理硬件的层级。它是软件与硬件之间的 "翻译官":把硬件的电信号翻译成软件能读的数字,把软件的指令翻译成硬件的动作。
- 类比:餐厅里直接对接农场的「采购员 / 食材分拣员」,只负责把地里的菜(电信号)运回后厨,不管菜是用来做鱼香肉丝还是宫保鸡丁。
📥 吃什么(输入)
硬件寄存器地址、GPIO 引脚号、SPI/I2C 时序命令、原始电信号。
📤 产什么(输出)
硬件原始数据(如 SHT30 的 ADC 值)、硬件执行状态(如 "GPIO 置高成功")。
🚫 绝对禁区
绝对不能出现:任何业务逻辑(如 "超过 35℃")、任何 UI 交互(如 "OLED 显示文字")、任何算法(如 "滤波")。
💡 项目实例
DRV_SHT30_ReadRawADC():直接读 SHT30 传感器的 16 位原始 ADC 值(不做任何计算)DRV_OLED_WriteSpiCmd(uint8_t cmd):通过 SPI 向 OLED 屏写命令字节(不管命令是用来清屏还是画点)DRV_Flash_Write(uint32_t addr, uint8_t *data):向 Flash 指定地址写数据(不管存的是阈值还是日志)
层级 2:功能模块层(function_modules)
🎯 核心定位
系统的 **"纯技术工具箱"**,里面全是 "通用武器"。它完全不知道你的项目是 "温湿度计" 还是 "智能门锁",只知道 "我能算数学题"、"我能打包 JSON"、"我能管理文件"。
- 类比:餐厅里的「厨具供应商」,只卖菜刀、砧板、烤箱,不管餐厅做川菜还是粤菜。
📥 吃什么(输入)
纯技术参数(如一个 float 数组、一个字符串、一个整数)。
📤 产什么(输出)
处理后的纯技术结果(如平均值、排序后的数组、MQTT 连接状态码)。
🚫 绝对禁区
绝对不能出现:任何业务名词(如 "温湿度"、"报警")、任何硬件寄存器操作、任何 UI 相关代码。
💡 项目实例
Filter_MovingAverage(float *buf, uint8_t len):滑动均值滤波算法(输入是数组,输出是平均值,不知道这是温度数据)MQTT_Connect(char *ip, uint16_t port):纯 MQTT 协议连接(不知道连接的是阿里云还是自己家的服务器)Json_PackFloat(char *key, float val):JSON 打包工具(不知道打包的是温度还是电压)
层级 3:应用接口层(application_interface)
🎯 核心定位
系统的 **"中间翻译官 + 打包员"**,承上启下的关键枢纽。它把下层 "零散的工具" 和 " raw 的数据" 打包成上层能用的 "套餐",彻底屏蔽所有技术细节,让上层专心写业务。
- 类比:餐厅里的「配菜师」,把采购员的菜(驱动数据)洗好切好,把供应商的厨具(功能模块)准备好,递给厨师长(业务层)说:"料都备好了,您直接炒就行。"
📥 吃什么(输入)
上层的简单指令(如 "给我一个干净的温度")。
📤 产什么(输出)
封装好的业务级数据 / 接口(如滤波后的温度值、一键上报函数)。
🚫 绝对禁区
绝对不能出现:UI 交互(如 "按键扫描")、业务规则判断(如 "超过 35℃报警")。
💡 项目实例
API_GetCleanTemp(void):整合「驱动层读 ADC」+「模块层滤波」,直接返回给上层一个能用的温度值API_ReportTempHumi(float t, float h):整合「模块层 JSON 打包」+「模块层 MQTT 发送」,上层只需传两个数,不用管 Topic 是什么API_SaveThreshold(float val):整合「驱动层 Flash 操作」,上层不用管 Flash 地址是多少
层级 4:业务逻辑层(business_logic)
🎯 核心定位
系统的 **"大脑 / 厨师长",产品灵魂所在。它负责所有 业务规则 **、流程编排 、决策判断。它只关心 "产品需求是什么",完全不关心 "硬件是 SPI 还是 I2C"、"屏幕是 OLED 还是 LCD"。
- 类比:餐厅里的「厨师长」,看着菜单(产品需求)指挥:"5 分钟后出菜"、"这个菜太咸了重做"、"把这个外卖送出去"。
📥 吃什么(输入)
应用接口层的封装数据(如干净的温度、WiFi 连接状态)。
📤 产什么(输出)
业务状态(如报警标志位、日志记录)、对下层的指令(如 "上报数据"、"保存阈值")。
🚫 绝对禁区
绝对不能出现:硬件操作(如 "写 SPI 寄存器")、UI 绘制(如 "画 OLED 像素")。
💡 项目实例
Business_Run5sCycle(void):核心业务循环 ------ 每 5 秒调用 API 拿温度 → 判断是否超过 35℃ → 置报警标志 → 调用 API 上报云平台 → 调用 API 记日志Business_SetTempThreshold(float newVal):业务规则校验 ------ 判断 newVal 是否在 0-100℃之间 → 合法则调用 API 保存,不合法则调用 API 记错误日志
层级 5:应用层(application)
🎯 核心定位
系统的 **"脸面 / 服务员"**,唯一直接对接用户的层级。它只负责两件事:把数据展示给用户看 、接收用户的操作指令。它是用户唯一能感知到的部分。
- 类比:餐厅里的「服务员」,给客人递菜单(OLED 显示)、记录客人点的菜(按键扫描)、把做好的菜端上桌(更新界面)。
📥 吃什么(输入)
用户操作(如按键按下、触摸屏点击)、业务层的状态(如报警标志位、当前温度值)。
📤 产什么(输出)
UI 界面(如 OLED 显示的温度数字、报警图标)、对业务层的指令(如 "把阈值加 0.5℃")。
🚫 绝对禁区
绝对不能做:业务规则判断(如 "阈值是否合法")、直接操作硬件寄存器。
💡 项目实例
App_RefreshOledScreen(void):UI 刷新 ------ 从业务层拿温度 / 报警标志 → 调用 OLED 绘图函数 → 画温度数字 → 如果报警则画个红色感叹号App_KeyScanHandler(void):按键处理 ------ 扫描按键 → 如果是 "+" 键,则调用业务层的Business_SetTempThreshold()函数 → 如果是 "OK" 键,则调用业务层的Business_TriggerManualReport()函数
终极复盘:完整走一遍数据流
上行(传感器→OLED 显示):
- 硬件驱动层 :SHT30 硬件产生电信号 →
DRV_SHT30_ReadRawADC()读出 16 位原始值 - 功能模块层 :
Filter_MovingAverage()对原始值数组滤波 - 应用接口层 :
API_GetCleanTemp()整合上述两步,返回干净温度 - 业务逻辑层 :
Business_Run5sCycle()拿到温度 → 判断是否超过 35℃ → 置位g_AlarmFlag - 应用层 :
App_RefreshOledScreen()拿到温度和g_AlarmFlag→ 画在 OLED 屏上
下行(按键→保存阈值):
- 应用层 :用户按下 "+" 键 →
App_KeyScanHandler()捕获按键 - 业务逻辑层 :调用
Business_SetTempThreshold(35.5)→ 校验 35.5 是否合法 - 应用接口层 :调用
API_SaveThreshold(35.5) - 硬件驱动层 :
DRV_Flash_Write()把 35.5 写入 Flash 硬件保存
场景一:设备上电全流程初始化(冷启动)
覆盖层级 :全 5 层,从底层硬件到顶层界面。数据流方向:自下而上(硬件→应用)。
- 硬件驱动层:依次初始化系统时钟、GPIO 引脚复用、SHT30 传感器、OLED 显示屏、Flash 存储、WiFi 射频模块。
- 功能模块层:初始化滑动滤波数据缓冲区、MQTT 协议栈状态机、日志环形缓冲区。
- 应用接口层:调用下层初始化函数,封装成统一的 "系统启动" 接口,完成硬件与模块的联动自检。
- 业务逻辑层:从接口层读取 Flash 中保存的历史温度阈值,初始化业务状态机(清零报警标志、启动 5 秒定时器、设置默认上报周期)。
- 应用层:初始化 UI 界面框架,OLED 屏显示开机 Logo,等待 2 秒后自动切换到主监控界面。
场景二:用户按键手动触发数据上报(人机交互闭环)
覆盖层级 :应用层→业务层→接口层→模块层→驱动层→(云平台)→反向反馈。数据流方向:下行(用户→硬件)+ 上行(硬件→云→反馈)。
- 应用层:用户按下 "OK" 按键,UI 按键扫描函数捕获到 "手动上报" 指令。
- 业务逻辑层:收到指令,立即标记 "立即上报" 状态,暂停 5 秒定时逻辑,优先执行手动流程。
- 应用接口层:调用 "获取干净温湿度" 接口,同时调用 "云平台上报" 接口。
- 功能模块层:滤波模块处理数据,JSON 模块打包数据,MQTT 模块封装网络报文。
- 硬件驱动层:WiFi 硬件通过射频将报文发送至云平台。
- 业务逻辑层:收到驱动层的 "发送成功" 回调,记录一条 "手动上报成功" 的日志。
- 应用层:OLED 屏主界面短暂弹出 "上报成功" 的提示框,3 秒后自动消失。
场景三:温度超阈值触发硬件报警(联动控制)
覆盖层级 :驱动层→模块层→接口层→业务层→接口层→驱动层(蜂鸣器)+ 应用层。数据流方向:上行采集 + 下行控制 + 界面反馈。
- 硬件驱动层:SHT30 传感器硬件采集环境数据,通过 I2C 接口读出 16 位原始 ADC 值。
- 功能模块层:滑动均值滤波模块对最近 10 次原始值进行滤波计算,去除毛刺。
- 应用接口层:整合驱动与模块,直接返回一个滤波后的标准浮点数温度值。
- 业务逻辑层:5 秒定时周期到达,读取温度值,与阈值比较,判断超过 35℃,立即置位 "系统报警" 标志位。
- 应用接口层:收到业务层指令,调用 "报警控制" 接口。
- 硬件驱动层:蜂鸣器对应的 GPIO 引脚被置高电平,蜂鸣器开始发出 "滴滴" 声。
- 应用层:UI 刷新时检测到报警标志,OLED 屏背景色变为红色,中央显示 "高温报警!" 字样。
场景四:WiFi 连接状态实时监控与显示(状态同步)
覆盖层级 :驱动层→模块层→接口层→业务层→应用层。数据流方向:自下而上(硬件状态→用户界面)。
- 硬件驱动层:WiFi 硬件底层检测到连接状态变化(如路由器断开重连),产生一个硬件中断信号。
- 功能模块层:WiFi 管理模块在中断中更新内部连接状态变量(连接中 / 已连接 / 断开)。
- 应用接口层:封装 "获取 WiFi 连接状态" 接口,上层只需调用,无需关心 WiFi 底层是如何扫描和握手的。
- 业务逻辑层:每秒轮询一次 WiFi 状态。如果状态变为 "断开",则停止数据上报,改为 "离线缓存模式";如果变为 "已连接",则恢复上报并补发缓存数据。
- 应用层:每次 UI 刷新(每秒 1 次)时,通过接口层读取当前状态,在 OLED 屏右上角绘制对应的图标(满格 WiFi / 空格 WiFi / 加载中旋转图标)。
场景五:业务异常自动记录日志(数据持久化)
覆盖层级 :业务层→接口层→模块层→驱动层→反向提示应用层。数据流方向:自上而下(业务逻辑→硬件存储)。
- 业务逻辑层:在尝试读取传感器数据时,连续 3 次收到接口层返回的 "读取失败" 错误码,判定为 "传感器硬件故障"。
- 业务逻辑层:生成一条异常事件,包含故障代码、发生时间戳。
- 应用接口层:调用 "记录系统日志" 接口,传入故障信息。
- 功能模块层:日志工具模块将故障信息格式化为标准字符串,并计算数据校验和。
- 硬件驱动层:将格式化后的日志字符串写入 Flash 硬件的指定日志存储扇区。
- 业务逻辑层:确认写入成功后,置位 "有未读异常日志" 的状态标志。
- 应用层:下次 UI 刷新时,检测到该标志,在 OLED 屏右下角显示一个闪烁的小 "警告" 图标,提示用户查看。