1. 需求和架构
1.1 需求
采集温度,如果温度超过上限则通过双路modbus寄存器模块打开风扇,如果低于下限,则关闭风扇。
采集环境光,如果光强过低,低于设定的下限,则打开LED灯
过程中每1秒钟从串口打印出传感的值,以及继电器的状态
1.2 架构
顶层业务模块:读取温度和光强,实时控制风扇和灯光,实时打印传感器数值
采集传感器数据模块:采集温度和光强
控制模块:控制风扇和灯光
顶层业务模块和采集传感器数据模块如何通信?
我们已经学过信号量互斥锁(互斥锁)了,从顶层到传感器称为请求(request),从传感器到顶层称为回应(ack)
采集模块:在任务中等待req,也就是阻塞;等到req后调用驱动程序,释放ack
顶层业务模块:释放req,同时等待ack;
也就是使用双向信号量进行顶层业务和传感器采集的交互
需要定义全局变量------结构体(第一个成员是温度,第二个成员是亮度),采集模块把数据装到结构体中,并且释放ack,顶层业务就可以直接从这个全局变量中获取温度和亮度
顶层业务模块和控制模块如何通信?
同理,也需要定义req和ack,注意这个req和ack是与上面的相互独立的,即是两套req和ack
定义结构体(第一个是风扇,第二个灯光)
如果风扇为1,控制模块等待信号量,顶层业务把控制参数放到结构体来了
控制模块释放ack前,会将两个继电器(控制风扇、控制灯光)开关状态写在结构体里;释放ack后,两个继电器状态就反馈到了顶层业务

考虑到产品芯片停产或BOM更改,我们的嵌入式软件架构应该如何改进
2. 降低耦合,并行开发
2.1 使用 宏
旧芯片AP3216已停产,我想替换新芯片ABC,之前写的函数例如AP3216_Get_Bright,就要换名字ABC_Get_Bright了,一个个修改太麻烦了,我们可以直接定义宏 #define ABC_Get_Bright AP3216_Get_Bright

2.2 使用 包含指针的结构体
我想实现高内聚低耦合?

什么是高内聚低耦合?
用通俗的话解释:
高内聚:一个模块 / 任务只做 "一件事",且把这件事做彻底(模块内部的代码、数据关联性极强);
低耦合:模块 / 任务之间的 "依赖关系" 尽可能弱(不需要知道对方的内部实现,只通过少量接口交互)。
可以用 "公司部门分工" 类比:
高内聚:销售部只负责卖产品,技术部只负责研发,不跨部门干杂活(每个部门职责单一且集中);
低耦合:销售部不需要知道技术部怎么研发产品,只需要通过 "需求文档" 这个接口提需求,不用和技术部的代码(内部工作)深度绑定。
我们要使用传感器,包含哪些基本操作?
①传感器的初始化
②亮度数据的读取
我们可以定义一个结构体,成员是指针,一个指向传感器初始化函数,一个指向亮度数据读取函数

什么是适配层?
适配层(Adapter Layer) 是位于「核心业务逻辑」和「底层依赖(硬件、第三方库、操作系统)」之间的 "中间层",核心作用是 隔离依赖、统一接口 ------ 让上层代码不用关心底层的具体实现,从而实现 "上层逻辑不变,底层可灵活替换"。
用通俗的话解释:适配层就像 "翻译官" 或 "转换器"------ 上层代码说 "通用语言"(统一接口),底层硬件 / 库说 "方言"(各自的专属接口),适配层负责把 "通用语言" 翻译成 "方言",反之亦然。
这样我们更换芯片后,其他程序中调用驱动函数时就完全不用改

驱动注册
法一:让结构体的函数指针依次赋值于传感器接口函数

法二:直接进行结构体赋值

这样就可以更灵活地让适配层指向不同驱动
我们可以定义好几个驱动配置结构体

添加一个.h文件

这样有三个好处
① 底层传感器驱动一般比较难调,别人等你调完就太耽误事了,上层业务人员可以在适配层使用一个假驱动,工程师不需要操心不属于自己职责范围内的事,不属于自己的职责的都可以用mock,这就是解耦带来的好处

② 运行过程中动态切换驱动,通过shell命令或config配置文件,使适配层灵活地指向某一套驱动

③ 同一系列不同型号产品,我们可以在程序中预留多套驱动,在flash上放置配置文件,我们读取配置数据,就可以动态挂载到某一驱动上,至于挂载到哪一套驱动上我们可以通过定义宏来决定

其实实际上不一定用宏,.bin文件烧录到不同型号产品中,电路板上增加几个电阻,上拉是高电平,不上拉是低电平。通过电平组合就可以知道挂载到哪个驱动上了

我们为什么要在顶层业务模块和采集驱动之间加一个总采集任务?
在顶层业务模块和采集驱动之间加「总采集任务」,核心是 解耦、统一调度、隔离风险 ------ 把分散的传感器 / 外设采集逻辑集中管理,避免业务直接操作驱动导致的混乱,尤其适合嵌入式多任务场景(如你的 FreeRTOS 项目)。
3. 顶层模块
前提:先明确需求
在写代码前,先定清楚核心需求:
- 基于 FreeRTOS,创建一个 "环境控制业务任务";
- 采集两个核心数据:温度(temprature)、亮度(bright);
- 按阈值控制设备:
- 温度<20℃ → 关风扇;
- 温度>30℃ → 开风扇;
- 亮度<50 → 开灯光;
- 数据要通过
mmc模块管理(存储 / 读取); - 输出日志(方便调试,知道任务运行状态、数据和控制结果)。
3.1 基础框架busines.c
第一步:新建文件,先搭 "基础框架"(busines.c)
全局变量:因为任务循环里要反复读写 "采集数据" 和 "控制状态",定义成全局变量(整个文件都能访问)最方便;
mmc_ctrl_stru是mmc模块的 "数据管家",后面要靠它读取 / 保存数据,所以先定义两个(一个管采集数据,一个管控制状态,分工明确)。
cpp
// 1. 包含头文件:需要什么工具就包含什么
// FreeRTOS核心(任务、延时、pdPASS等宏)
#include "cmsis_os.h"
// 业务相关的结构体、阈值定义(后面会写.h文件)
#include "business.h"
// 数据管理模块(mmc_ctrl_init、mmc_ctrl_require)
#include "mmc.h"
// 通用工具(可能有常用宏、类型定义)
#include "common.h"
// 日志模块(log_d/log_i/log_w等日志接口)
#define LOG_TAG "BUSI_MODULE" // 日志标签:区分日志来源
#include "elog.h"
// 2. 定义全局变量:整个任务都要用的数据容器+管理对象
// 采集数据容器:存温度、亮度
busi_sample_stru busi_sample_values;
// 控制状态容器:存风扇、灯光开关状态
busi_switch_stru busi_switch_values;
// 数据管理对象(mmc模块的"管家"):分别管理上面两个容器
mmc_ctrl_stru busi_sample_mmc_ctrl; // 管"采集数据"
mmc_ctrl_stru busi_switch_mmc_ctrl; // 管"控制状态数据"
第二步:定义任务入口函数(FreeRTOS 任务的 "大门")
FreeRTOS 的任务必须是 "特定格式" 的函数 ------void 任务名(void *p),这是系统规定的(不这么写系统识别不了)。任务函数里要做两件事:
- 初始化(任务启动后只做一次:比如初始化mmc管家);
- 无限循环(任务的核心业务:采集→判断→控制,一直重复)。
cpp
// 3. 任务入口函数:FreeRTOS调度后会执行这个函数
void busi_process_task_entry(void *p)
{
// 3.1 任务启动日志:告诉我们"任务已经跑起来了"(调试用)
log_d("busi_process_task is running ...");
// 3.2 初始化mmc数据管家:给"管家"分配工作
// 第一个管家:管理采集数据(busi_sample_values)
mmc_ctrl_init(
&busi_sample_mmc_ctrl, // 要初始化的管家(取地址,才能修改它)
&busi_sample_values, // 管家要管理的数据容器
sizeof(busi_sample_values), // 数据容器的大小(管家知道要管多少字节)
"busi_sample_values" // 数据名称(调试时能区分,可选但建议写)
);
// 第二个管家:管理控制状态数据(busi_switch_values)
mmc_ctrl_init(
&busi_switch_mmc_ctrl,
&busi_switch_values,
sizeof(busi_switch_values),
"busi_switch_values"
);
// 3.3 无限循环:业务核心(一直跑,除非任务被删除)
while(1)
{
// 后续的"采集→判断→控制"都写在这里
// 先留空,下一步填内容
}
}
任务函数格式:void *p是任务参数(我们这个任务用不到,所以后面传 NULL);
日志log_d:调试用的 "心跳日志",任务启动后串口会输出D/BUSI_MODULE: busi_process_task is running ...,确认任务没崩;
mmc_ctrl_init:初始化 "数据管家"------ 相当于告诉管家 "你要管哪个数据、数据多大",不初始化的话,后面调用mmc_ctrl_require会失败(管家不知道该管谁)。
第三步:填充循环业务(采集数据→打印日志)
循环里的第一步是 "获取数据"------ 没有数据就没法控制设备。所以先通过mmc管家读取采集数据,读取成功后打印出来(方便调试,知道采集到的数值是否正常)。
cpp
// 3.3.1 读取采集数据(温度、亮度):调用mmc管家的"读取接口"
// mmc_ctrl_require:请求读取数据,超时时间MMC_DELAY_TIME(比如100ms)
// 返回pdPASS表示读取成功(FreeRTOS的宏,代表"成功")
if(mmc_ctrl_require(&busi_sample_mmc_ctrl, MMC_DELAY_TIME) == pdPASS)
{
// 读取成功:打印采集到的数据(日志级别用log_i,信息级,比调试级醒目)
log_i(
"temprature : %d , bright : %d , rsvd0 : %d , rsvd1 : %d , rsvd2 : %d , rsvd3 : %d ",
busi_sample_values.temprature, // 温度
busi_sample_values.bright, // 亮度
busi_sample_values.reserved[0], // 预留字段(暂时不用,打印出来备用)
busi_sample_values.reserved[1],
busi_sample_values.reserved[2],
busi_sample_values.reserved[3]
);
// 后面还要加"阈值判断→控制设备",先留空
}
else
{
// 读取失败:打印日志提示(方便排查问题,比如传感器没接好)
log_i("sample values requre FAIL!");
}
// 3.3.2 任务延时:每次循环休息1秒(vTaskDelay是FreeRTOS的延时函数)
// 目的:不占用所有CPU资源,给其他任务留时间运行
vTaskDelay(1000);
mmc_ctrl_require:mmc模块的 "读取数据" 接口,第一个参数是 "要读取哪个管家管理的数据",第二个是超时时间(超过这个时间没读到就返回失败);
log_i:信息级日志,串口输出时会带I/BUSI_MODULE,一眼能看到采集到的具体数据(比如 "temprature : 25 , bright : 60 ...");
else分支:必须加!如果传感器没接好、mmc模块异常,能通过日志知道 "数据读失败",而不是误以为任务没运行;
vTaskDelay(1000):延时 1 秒(1000ms),不然任务会疯狂循环读取数据,CPU 占用率 100%,其他任务(比如日志、串口)会卡死。
第四步:添加 "阈值判断→设备控制" 逻辑
读取到数据后,就要按需求做判断 ------ 温度和亮度分别和阈值对比,然后修改 "控制状态容器"(busi_switch_values)里的风扇、灯光状态。
cpp
// 3.3.3 温度阈值判断→控制风扇
// 条件1:温度<20℃(BUSI_TEMPRATURE_DOWN_THRESHOLD=20)→ 关风扇
if(busi_sample_values.temprature < BUSI_TEMPRATURE_DOWN_THRESHOLD)
{
// 打印警告日志:温度太低
log_w("temprature is too LOW!!");
// 设置风扇状态为"关"(FAN_CTRL_CLOSE_STATUS=0)
busi_switch_values.fan_status = FAN_CTRL_CLOSE_STATUS;
}
// 条件2:温度>30℃(BUSI_TEMPRATURE_UP_THRESHOLD=30)→ 开风扇
if(busi_sample_values.temprature > BUSI_TEMPRATURE_UP_THRESHOLD)
{
log_w("temprature is too HIGH!!");
// 设置风扇状态为"开"(FAN_CTRL_OPEN_STATUS=1)
busi_switch_values.fan_status = FAN_CTRL_OPEN_STATUS;
}
// 3.3.4 亮度阈值判断→控制灯光
// 条件:亮度<50(BUSI_BRIGHT_DOWN_THRESHOLD=50)→ 开灯光
if(busi_sample_values.bright < BUSI_BRIGHT_DOWN_THRESHOLD)
{
log_w("bright is too LOW!!");
// 设置灯光状态为"开"(LIGHT_CTRL_OPEN_STATUS=1)
busi_switch_values.light_status = LIGHT_CTRL_OPEN_STATUS;
}
log_w:警告级日志(带W/BUSI_MODULE),比信息级更醒目,提醒 "温度 / 亮度超标";
状态值:FAN_CTRL_CLOSE_STATUS这些是宏定义(后面会在.h 文件里写),用宏而不是直接写 0/1,是为了 "改值方便"------ 比如以后想把 "风扇开" 改成 2,只改宏定义就行,不用改业务代码。
第五步:保存控制状态(通过 mmc 模块)
修改完风扇 / 灯光状态后,要把这个 "控制状态" 保存到mmc模块(比如存储到 Flash,或发送给执行器),不然下次循环状态会丢失。所以最后要调用mmc_ctrl_require保存控制状态,同时打印状态日志。
写代码(填到 "亮度判断" 之后):
cpp
// 3.3.5 保存控制状态(风扇、灯光状态)
if(mmc_ctrl_require(&busi_switch_mmc_ctrl, MMC_DELAY_TIME) == pdPASS)
{
// 保存成功:打印控制状态(让我们知道风扇/灯光当前是开是关)
log_d("switch requre SUCCESS!");
log_i(
"fan_status : %d , light_status : %d , rsvd0 : %d , rsvd1 : %d , rsvd2 : %d , rsvd3 : %d ",
busi_switch_values.fan_status, // 风扇状态
busi_switch_values.light_status,// 灯光状态
busi_switch_values.reserved[0], // 预留字段
busi_switch_values.reserved[1],
busi_switch_values.reserved[2],
busi_switch_values.reserved[3]
);
}
else
{
// 保存失败:打印错误日志(排查问题)
log_e("switch requre FAIL!");
}
保存控制状态和读取采集数据用的是同一个mmc_ctrl_require接口 ------mmc模块内部会根据 "管家管理的数据类型" 判断是 "读取" 还是 "保存"(不用我们管,调用就行);
log_e:错误级日志(带E/BUSI_MODULE),红色醒目,方便快速发现 "控制状态保存失败" 的问题。
第五步和第三步重复?
不是重复:第三步是 "读数据"(输入),第五步是 "写状态"(输出),是 "输入→处理→输出" 的完整业务流程;
同一个函数不同作用:mmc_ctrl_require 是 "通用接口",具体做读还是写,由它管理的 "数据容器类型" 决定;记住:只要是 "先拿数据、再做控制、最后保存控制结果",就必须分这两步写,少一步都不行(比如只读不写,设备不会真的执行风扇 / 灯光控制;只写不读,控制没有依据)。
第六步:补写.h 文件(business.h)
cpp
#ifndef _BUSINESS_H_
#define _BUSINESS_H_
// 包含FreeRTOS核心头文件:提供任务、延时等OS相关功能支持
#include "cmsis_os.h"
// 包含mmc模块头文件:使用mmc_ctrl_stru结构体和相关数据管理接口
#include "mmc.h"
// 采集数据结构体:存储从传感器获取的核心数据
typedef struct
{
int temprature; // 温度数据(单位:℃,根据实际传感器精度调整)
int bright; // 亮度数据(单位:自定义,如光敏电阻AD值)
int reserved[4]; // 预留字段(4个int型):后续扩展新采集数据(如湿度、气压)时使用
// 预留字段可避免修改结构体大小导致的存储/通信兼容问题
} busi_sample_stru;
// 控制状态结构体:存储设备(风扇、灯光)的控制状态
typedef struct
{
int fan_status; // 风扇控制状态(建议:0=关闭,1=开启,可扩展2=自动模式)
int light_status; // 灯光控制状态(建议:0=关闭,1=开启,可扩展2=调光模式)
int reserved[4]; // 预留字段(4个int型):后续扩展新控制设备(如空调、蜂鸣器)时使用
} busi_switch_stru;
// 声明mmc数据管理对象(全局变量):供其他文件访问(如main.c创建任务时可能用到)
// busi_sample_mmc_ctrl:管理采集数据(busi_sample_stru类型)的mmc控制器
extern mmc_ctrl_stru busi_sample_mmc_ctrl;
// busi_switch_mmc_ctrl:管理控制状态(busi_switch_stru类型)的mmc控制器
extern mmc_ctrl_stru busi_switch_mmc_ctrl;
// 声明全局数据容器:供其他文件读写(如驱动层写采集数据、业务层读数据判断)
extern busi_sample_stru busi_sample_values; // 采集数据存储容器
extern busi_switch_stru busi_switch_values; // 控制状态存储容器
// 声明业务处理任务入口函数:FreeRTOS任务创建时需要调用该函数名
// 任务功能:读取采集数据→阈值判断→控制设备状态→日志输出
extern void busi_process_task_entry(void *p);
// 业务控制阈值宏定义(用括号包裹,避免宏替换时的运算优先级问题)
#define BUSI_TEMPRATURE_DOWN_THRESHOLD (20) // 温度下限阈值:≤20℃时触发风扇关闭
#define BUSI_TEMPRATURE_UP_THRESHOLD (30) // 温度上限阈值:≥30℃时触发风扇开启
#define BUSI_BRIGHT_DOWN_THRESHOLD (2) // 亮度下限阈值:≤2时触发灯光开启(数值小,适配低精度传感器)
#endif
为什么宏的值要带括号?
宏的值带括号,核心是为了 避免宏替换时的 "运算优先级错乱" ------宏本质是 "纯文本替换" (不是函数调用),没有括号保护的话,遇到复杂表达式会出逻辑错误,而且这种错误很难排查。
第七步:在 main 函数里创建任务(让任务跑起来)
写好任务函数后,必须在main函数里调用xTaskCreate创建任务,FreeRTOS 调度器启动后才会执行这个任务。
cpp
int main(void)
{
// 之前的初始化代码(HAL_Init、时钟配置、GPIO初始化、串口初始化、日志初始化等)
// ...(省略,你的原有代码里已经有了)...
// 创建"环境控制业务任务"
xTaskCreate(
busi_process_task_entry, // 任务入口函数(我们写的这个)
"busi_process_task", // 任务名称(调试用,比如FreeRTOS可视化工具里能看到)
512, // 任务栈大小(字节:512足够这个简单任务,复杂任务可以加大到1024)
NULL, // 任务参数(我们这个任务用不到,传NULL)
5, // 任务优先级(0~31:比日志任务、串口任务低一点,避免抢占)
NULL // 任务句柄(不用则传NULL,想后续删除任务可以保存句柄)
);
// 启动FreeRTOS调度器(任务开始运行)
vTaskStartScheduler();
// 调度器启动失败才会走到这里(正常不会执行)
while (1)
{
}
}
xTaskCreate是 FreeRTOS 创建任务的核心函数,参数顺序不能乱(函数名→任务名→栈大小→参数→优先级→句柄);
栈大小:512 字节对于这个任务完全足够(日志打印、数据处理都不复杂),栈太小会导致任务崩溃(hardfault);
优先级:设为 5(假设日志任务优先级是 6),避免业务任务抢占日志任务,导致日志输出不完整。
3.2 高内聚
如何提炼出事物的共性,然后对它们进行封装?
模块和模块的交互是一个基本的功能,会频繁使用,我们应该把这个做成一个公用的函数,这就是高内聚的思想

req、ack、共享内存这三个变量都是必须的,共享内存如何表达?------首地址、长度


我们新创建mmc.c和mmc.h这两个文件将它们加入到新建的Application/Modules/MMC这个group中来,mmc主要是实现模块与模块之间的通信的一些通用的函数即 module to module communication
第二步:写 mmc.c 开头(包含头文件 + 日志配置)
先包含 mmc.h(用 mmc_ctrl_stru 和接口声明);
要初始化共享内存(用 memset 清 0),所以包含 string.h;
要输出日志(调试用),所以配置 LOG_TAG + 包含 elog.h;
cpp
// 1. 包含依赖头文件:严格对应头文件的依赖和功能需求
#include "mmc.h" // 核心:用里面的 mmc_ctrl_stru 结构体和接口声明
#include "string.h" // 要用 memset 初始化共享内存为0(避免垃圾数据)
#define LOG_TAG "MMC_MODULE" // 日志标签:调试时知道是 mmc 模块的日志
#include "elog.h" // 日志模块:输出调试信息(比如初始化成功、请求成功)
第三步:实现 mmc_ctrl_init 函数(初始化接口)
写mmc_ctrl_init ( 初始化接口)函数,我们要做 3 件事:
-
给结构体里的两个信号量
sem_req/sem_ack分配资源(创建信号量); -
给共享内存相关的成员赋值(
p_shared_mem = p_sm,shared_mem_nbyte = sm_len); -
初始化共享内存为 0(
memset); -
输出日志(告诉调试者 "初始化成功")。
cpp
// 2. 实现初始化接口:
void mmc_ctrl_init(mmc_ctrl_stru *p_mmc_ctrl, void *p_sm, int sm_len, char *sm_name)
{
// 2.1 创建两个二进制信号量(FreeRTOS 信号量,同步核心)
// sem_req:请求信号量(使用者→提供者,比如业务任务要数据时释放)
p_mmc_ctrl->sem_req = xSemaphoreCreateBinary();
// sem_ack:应答信号量(提供者→使用者,比如驱动写好数据后释放)
p_mmc_ctrl->sem_ack = xSemaphoreCreateBinary();
// 2.2 绑定共享内存:把外部传入的内存地址和长度,赋值给结构体成员
p_mmc_ctrl->p_shared_mem = p_sm; // 共享内存的地址(比如 busi_sample_values 的地址)
p_mmc_ctrl->shared_mem_nbyte = sm_len; // 共享内存的长度(比如 sizeof(busi_sample_values))
// 2.3 初始化共享内存为0:避免初始垃圾数据影响数据读取
memset(p_mmc_ctrl->p_shared_mem, 0, p_mmc_ctrl->shared_mem_nbyte);
// 2.4 输出初始化日志:调试用,确认初始化成功
log_d("mmc init success! sem_req&sem_ack created, shared_mem: %s (len: %d bytes)",
sm_name, sm_len);
}
为什么用 xSemaphoreCreateBinary()?因为我们需要 "一对一同步"(使用者发一次请求,提供者回一次应答),二进制信号量(初始状态为 "未释放")最适合;
为什么要绑定共享内存?mmc 模块是 "管家",必须知道 "要管理哪块内存、多大",才能让使用者和提供者通过这块内存交换数据;
为什么 p_mmc_ctrl 是指针?因为我们要修改结构体里的成员(信号量、内存地址),传指针才能直接操作原结构体(传值的话只会修改副本,没用)。
为什么p_mmc_ctrl能访问sem_req
p_mmc_ctrl 的类型是 mmc_ctrl_stru *:它是一个 "指向 mmc_ctrl_stru 结构体的指针"(* 代表指针),指针的本质是 "存储了某个变量的内存地址";
sem_req 是 mmc_ctrl_stru 结构体的成员:你在 mmc.h 里已经明确定义了:
cpptypedef struct { SemaphoreHandle_t sem_req; // sem_req是结构体里的第一个成员 SemaphoreHandle_t sem_ack; void *p_shared_mem; int shared_mem_nbyte; } mmc_ctrl_stru;语法格式:指针变量->结构体成员名
简单说:p_mmc_ctrl 手里拿着 "mmc_ctrl_stru 结构体的地址",而 sem_req 是这个结构体里的 "一个房间"------ 知道了结构体的地址,再按 "房间位置",自然能找到 sem_req。
memset的用法void *memset(void *ptr, int value, size_t num);
三个参数必须记牢,按顺序来:
ptr:要初始化的内存地址(比如结构体变量的地址、数组的地址);
value:要设置的值(通常是 0,注意:虽然是 int 类型,但实际只取低 8 位,所以写 0 就够了);
num:要设置的内存字节数(比如结构体的大小、数组的长度 × 单个元素的大小)。
第四步:实现 mmc_ctrl_require 函数(数据请求接口)
写mmc_ctrl_require (使用者请求数据的接口)函数,比如业务任务要温度数据时调用,核心逻辑是:
- 使用者释放 sem_req 信号量(告诉提供者 "我要数据了");
- 使用者阻塞等待 sem_ack 信号量(最多等 waittime ms,等提供者写好数据);
- 等待成功:返回 pdPASS(告诉使用者 "数据好了,去共享内存拿");
- 等待失败:返回 pdFAIL(比如超时,提供者没及时回应);
- 全程加日志(调试时知道每一步状态)。
cpp
// 3. 实现数据请求接口:完全对应头文件的声明
int mmc_ctrl_require(mmc_ctrl_stru *p_mmc_ctrl, int waittime)
{
// 3.1 释放 sem_req 信号量:给提供者发"请求数据"的消息
xSemaphoreGive(p_mmc_ctrl->sem_req);
log_d("sem_req gived! waiting for sem_ack (max: %d ms)", waittime);
// 3.2 阻塞等待 sem_ack 信号量:等提供者写好数据的应答
if (xSemaphoreTake(p_mmc_ctrl->sem_ack, waittime) == pdTRUE)
{
// 3.3 等待成功:数据已写入共享内存,输出日志+打印内存内容(调试用)
log_d("require success! shared_mem ready");
// 十六进制打印共享内存数据(直观看到数据是否正确)
elog_hexdump("shared_mem content", 16, p_mmc_ctrl->p_shared_mem, p_mmc_ctrl->shared_mem_nbyte);
return pdPASS; // 返回成功(pdPASS 是 FreeRTOS 的宏,对应 1)
}
else
{
// 3.4 等待失败:超时或信号量获取失败,输出错误日志
log_e("require fail! wait sem_ack timeout (%d ms)", waittime);
return pdFAIL; // 返回失败(pdFAIL 是 FreeRTOS 的宏,对应 0)
}
}
为什么先 xSemaphoreGive 再 xSemaphoreTake?这是 "请求 - 应答" 的标准流程:使用者先发请求(释放 sem_req),再等应答(获取 sem_ack);
为什么 waittime 是参数?因为不同场景的超时需求可能不一样(比如业务任务要温度数据等 10ms,要风扇状态等 5ms),传参数更灵活;
为什么用 elog_hexdump?调试时能直观看到共享内存里的原始数据(比如温度 25 对应的十六进制是 0x19),方便排查 "数据有没有写对"。
3.3 统一规范 高效开发
3.3.1 统一名称
新建一个common.h,专门放置关于状态信息的宏,使其他代码中不要出现数字
cpp
#ifndef _COMMON_H_
#define _COMMON_H_
#define FAN_CTRL_CLOSE_STATUS (0)
#define FAN_CTRL_OPEN_STATUS (1)
#define LIGHT_CTRL_CLOSE_STATUS (0)
#define LIGHT_CTRL_OPEN_STATUS (1)
#endif
一个团队要分工合作,我完成了我这部分代码,别人还没完成他们部分的,我如何做测试呢?
------mocktesk
总采集和总控制还没写完,那我们就先写个假的总采集模块和假的总控制模块,这样就可以对我写的业务模块进行测试了
3.3.2 mocktest
新建一个busi_test.c,
实现这两个任务

任务 1:mock_sample_task_entry(传感器数据模拟)
功能:模拟温度、亮度等传感器数据的采集和更新;
cpp
void mock_sample_task_entry(void *p)
{
log_d("mock sample task is running..."); // 任务启动日志
while(1) // FreeRTOS 任务必需死循环(不能返回)
{
log_d("wait for sample_sem_req forever");
// 阻塞等待"请求信号量":初始值为 0,需其他任务通过 xSemaphoreGive 触发
xSemaphoreTake(busi_sample_mmc_ctrl.sem_req, portMAX_DELAY);
// 模拟数据采集:给全局数据结构体赋值(固定值,实际应读硬件寄存器)
busi_sample_values.temprature = 25;
busi_sample_values.bright = 2;
busi_sample_values.reserved[0] = 1;
busi_sample_values.reserved[1] = 2;
busi_sample_values.reserved[2] = 3;
busi_sample_values.reserved[3] = 4;
// 打印更新后的数据(调试用)
log_d("current temp = %d bright = %d", busi_sample_values.temprature, busi_sample_values.bright);
log_d("current reserved = [0]%d [1]%d [2]%d [3]%d",
busi_sample_values.reserved[0], busi_sample_values.reserved[1],
busi_sample_values.reserved[2], busi_sample_values.reserved[3]);
// 发送"响应信号量":通知请求方"数据已更新完成"
xSemaphoreGive(busi_sample_mmc_ctrl.sem_ack);
log_d("release sem ack");
}
}
任务 2:mock_switch_task_entry(设备开关控制模拟)
功能:模拟风扇、灯光的开关状态控制;
运行流程:与 mock_sample_task_entry 完全对称,仅操作的结构体和数据不同:
阻塞等待 busi_switch_mmc_ctrl.sem_req 信号量;
给 busi_switch_values 赋值(风扇、灯光设为 "开启" 状态);
打印状态后,通过 sem_ack 信号量响应请求方。
cpp
oid mock_switch_task_entry(void *p)
{
log_d("mock switch task is running ....");
while(1)
{
log_d("wait for switch_sem_req forever");
xSemaphoreTake(busi_switch_mmc_ctrl.sem_req,portMAX_DELAY);
busi_switch_values.fan_status = FAN_CTRL_OPEN_STATUS;
busi_switch_values.light_status = LIGHT_CTRL_OPEN_STATUS;
busi_switch_values.reserved[0] = 1;
busi_switch_values.reserved[1] = 2;
busi_switch_values.reserved[2] = 3;
busi_switch_values.reserved[3] = 4;
log_d("current fan_status = %d light_status = %d",busi_switch_values.fan_status,busi_switch_values.light_status);
log_d("current reserved = [0]%d [1]%d [2]%d [3]%d",busi_switch_values.reserved[0],busi_switch_values.reserved[1],busi_switch_values.reserved[2],busi_switch_values.reserved[3]);
xSemaphoreGive(busi_switch_mmc_ctrl.sem_ack);
log_d("release sem ack");
}
}
3. Shell 命令:smst(启动任务)
功能:通过串口输入 smst 命令,动态创建并启动两个模拟任务;
关键函数:
xTaskCreate:FreeRTOS 任务创建函数,参数说明:
cpp
void smst(int argc,char **argv)
{
log_d("start mock_sample_task_entry sample task");
xTaskCreate(mock_sample_task_entry,"mock_sample_task",128,(void *)0,7,&mst_handle);
xTaskCreate(mock_switch_task_entry,"mock_switch_task",128,(void *)0,7,&mst_handle);
}
ZNS_CMD_EXPORT(smst,start mock sample task)
ZNS_CMD_EXPORT(smst, ...):自定义 shell 命令注册宏,将 smst 命令导出到 shell 系统,支持用户通过串口调用。
核心逻辑
结合之前的mmc模块和业务任务,整个 "请求 - 应答" 流程是:
-
串口输入
smst→ 启动两个mock任务(提供者); -
业务任务
busi_process_task_entry启动→ 调用mmc_ctrl_require(&busi_sample_mmc_ctrl, ...)→ 释放sem_req信号量; -
mock_sample_task一直等sem_req→ 拿到信号量后,给busi_sample_values写模拟数据→ 释放sem_ack; -
业务任务拿到
sem_ack→ 读busi_sample_values数据→ 按阈值判断→ 调用mmc_ctrl_require(&busi_switch_mmc_ctrl, ...)→ 释放sem_req; -
mock_switch_task一直等sem_req→ 拿到信号量后,确认busi_switch_values状态→ 释放sem_ack; -
业务任务拿到
sem_ack→ 打印控制状态→ 延时 1 秒→ 重复流程。
实验
烧录代码,打开shell

输入smst



