1. NTC 热敏电阻温度采集实现
通过 ADC2 读取 NTC 的电压信号,再通过热敏电阻的特性公式计算实际温度,并提供 shell 命令(gav/ngt)用于调试读取 ADC 值和温度值。

为什么PC3不会影响R64和RT1的串联分压?
PC3 的作用是「将分压后的模拟电压传递给 ADC 模块」,它的设计确保了 "读取时不破坏原有分压":
PC3 配置为「ADC 模拟输入模式」:STM32 的 ADC 输入引脚在模拟模式下,输入阻抗极高(通常 MΩ 级别);
高阻抗的意义:相当于 PC3 是 "高阻探头",只会 "感知" 采样点的电压,不会从分压回路中 "偷走" 明显电流(根据欧姆定律,电流 = 电压 / 电阻,阻抗极高时电流趋近于 0);
结果:PC3 的接入不会改变 R64 和 RT1 的分压比例(因为没有额外电流分流),所以分压计算时无需考虑 PC3------ 它只是一个 "无干扰的读取通道"。
3.3V → 10KΩ固定电阻 → NTC热敏电阻 → GND
采样点接在10KΩ与NTC之间,通过ADC读取分压电压
1.1 ADC 初始化:MX_ADC2_Init(void)
为什么选择ADC2?
AD1也可以,


ADC 初始化函数,配置 ADC2 为单通道、软件触发、单次转换模式:
cpp
static void MX_ADC2_Init(void)
{
ADC_ChannelConfTypeDef sConfig = {0};
// ADC2核心配置
hadc2.Instance = ADC2; // 外设实例为ADC2
hadc2.Init.ScanConvMode = ADC_SCAN_DISABLE; // 关闭扫描模式(仅单通道)
hadc2.Init.ContinuousConvMode = DISABLE; // 关闭连续转换(单次触发单次转换)
hadc2.Init.DiscontinuousConvMode = DISABLE; // 关闭间断转换
hadc2.Init.ExternalTrigConv = ADC_SOFTWARE_START; // 软件触发(手动调用HAL_ADC_Start启动)
hadc2.Init.DataAlign = ADC_DATAALIGN_RIGHT; // 数据右对齐(12位ADC,结果范围0~4095)
hadc2.Init.NbrOfConversion = 1; // 转换通道数:1个
if (HAL_ADC_Init(&hadc2) != HAL_OK) Error_Handler(); // 初始化ADC外设
// 通道配置(ADC2_CH13)
sConfig.Channel = ADC_CHANNEL_13; // 选择通道13
sConfig.Rank = ADC_REGULAR_RANK_1; // 通道优先级:1(唯一通道)
sConfig.SamplingTime = ADC_SAMPLETIME_239CYCLES_5; // 采样时间:239.5个ADC时钟周期
if (HAL_ADC_ConfigChannel(&hadc2, &sConfig) != HAL_OK) Error_Handler(); // 配置通道
}
采样时间(239.5 周期):采样时间越长,ADC 读数越稳定,但采集速度越慢;239.5 周期适合对稳定性要求较高的场景(如温度采集);
12 位 ADC:分辨率为 12 位,所以 ADC 值范围是 0~4095(对应电压 0~3.3V)。
ADC配置选项的含义?
- 核心配置(
hadc2.Init)
配置项 代码值 含义 为什么这么配置? ScanConvMode ADC_SCAN_DISABLE 关闭 "扫描模式" 扫描模式用于 多通道采集(如同时采集 ADC_CH13、CH14),会按通道顺序依次转换;本场景仅采集 NTC 一个通道,关闭扫描模式更高效。 ContinuousConvMode DISABLE 关闭 "连续转换模式" 连续模式:ADC 启动后会一直重复转换(如采集→结果→采集→结果...),适合高频实时采样;本场景是 "按需采集"(通过 shell 命令触发),关闭后仅在调用HAL_ADC_Start时执行一次转换,节省 CPU 和功耗。 DiscontinuousConvMode DISABLE 关闭 "间断转换模式" 间断模式是扫描模式的补充(多通道采集时,分批次转换部分通道),单通道场景下无需启用。 ExternalTrigConv ADC_SOFTWARE_START 软件触发转换 触发方式分 "硬件触发"(如定时器溢出、外部 GPIO 电平)和 "软件触发"(通过HAL_ADC_Start函数);本场景是手动通过 shell 命令触发,所以用软件触发更灵活。 DataAlign ADC_DATAALIGN_RIGHT 数据右对齐 - 右对齐:结果存放在 16 位寄存器的低 12 位(如 0x0000~0x0FFF),直接读取后无需移位,计算方便; - 左对齐:结果存放在高 12 位(如 0x0000~0xFFF0),需右移 4 位才能得到真实值,冗余 12 位 ADC 的转换结果是 12 位二进制数(范围 0~4095): NbrOfConversion 1 转换通道数:1 个 仅采集 NTC 对应的 ADC_CH13 一个通道,所以通道数设为 1。
- 通道配置(
sConfig)
配置项 代码值 含义 为什么这么配置? ChannelADC_CHANNEL_13选择采集通道 13 通道 13 对应硬件引脚(如 PB1),是 NTC 传感器的采样引脚,需与硬件接线一致。 RankADC_REGULAR_RANK_1通道优先级:1 多通道采集时,按 Rank顺序转换(Rank1 先转,Rank2 后转);单通道场景下 Rank 无意义,仅需设为 1。SamplingTimeADC_SAMPLETIME_239CYCLES_5采样时间:239.5 个 ADC 时钟周期 ADC 采集的核心是 "充电时间":ADC 内部有一个电容,采样时需要通过引脚给电容充电到输入电压值,充电时间越长,采样越稳定(抗噪声能力强);NTC 是缓慢变化的温度信号,无需高速采样,239.5 周期兼顾稳定性和效率(若采样时间太短,会因电容未充满导致读数不准)。
1.2 NTC 初始化:NTC_Init(void)
对外提供的初始化接口,本质是调用 ADC2 的初始化函数:
cpp
int NTC_Init(void)
{
MX_ADC2_Init(); // 初始化ADC2
return 0;
}
1.3 ADC 值读取:Get_ADC_Value(void)
单次 ADC 采集函数,返回 12 位 ADC 原始值(0~4095):
cpp
uint32_t Get_ADC_Value(void)
{
HAL_ADC_Start(&hadc2); // 启动ADC转换
// 等待转换完成,超时时间100ms(超时返回HAL_TIMEOUT,此处未处理)
HAL_ADC_PollForConversion(&hadc2, 100);
return HAL_ADC_GetValue(&hadc2); // 读取转换结果(0~4095)
}
流程:启动转换 → 阻塞等待转换完成 → 读取结果;
潜在问题:未检查HAL_ADC_PollForConversion的返回值,若超时(如 ADC 硬件异常),会返回错误的 ADC 值。
1.4 温度计算:NTC_Get_Temprature(void)
核心函数,通过 ADC 值→电压→NTC 电阻→温度的流程,计算实际温度:
cpp
int NTC_Get_Temprature(void)
{
int adc_value;
float vol; // NTC分压电压(V)
float res; // NTC当前电阻(Ω)
float Tc; // 计算出的摄氏温度(℃)
adc_value = Get_ADC_Value(); // 读取ADC原始值
vol = (adc_value * 3.3) / 4096; // ADC值→电压(12位ADC,3.3V参考电压)
log_d("voltage is %f", vol); // 打印分压电压
res = (10000 * vol) / (3.3 - vol); // 分压公式计算NTC电阻
log_d("res is %f", res); // 打印NTC电阻
// 热敏电阻B值公式计算温度(B=3950K)
Tc = (3950.0 / (log(res/10000) + (3950.0/298.15))) - 273.15;
return (int)(Tc * 1000); // 返回温度×1000(避免浮点数精度损失,如25.5℃返回25500)
}
电压 = (ADC值 / 4095) × 参考电压(3.3V)怎么推的?
这个公式的本质是 "将 ADC 的数字量(离散值)映射为模拟量(连续电压)",推导过程基于 ADC 的 "量化原理":
- 核心前提
ADC 是 "模数转换器",作用是把连续的模拟电压(如 0~3.3V)转换为离散的数字值(二进制);
12 位 ADC 的含义:数字值的范围是 0~2¹²-1 = 0~4095(共 4096 个离散等级);
参考电压(V_REF):ADC 的 "测量标尺",代码中是 3.3V(即 ADC 能测量的电压范围是 0~3.3V)。
- 推导过程
假设:ADC 的数字值为D,对应的模拟电压为V,参考电压为V_REF;
12 位 ADC 有 4096 个等级,每个等级对应的 "电压间隔"(量化步长)为:
量化步长 = 参考电压 / 总等级数 = V_REF / 4095(注意:不是 4096!因为数字值从 0 开始,0 对应 0V,4095 对应 V_REF);
例:参考电压 3.3V 时,量化步长 = 3.3V / 4095 ≈ 0.806mV(即数字值每变化 1,对应电压变化约 0.806mV);
因此,数字值D对应的电压V为:
V = D × 量化步长 = D × (V_REF / 4095);
代入代码中的 3.3V 参考电压,得到:
V = (ADC值 / 4095) × 3.3V。
ADC 值→电压:
12 位 ADC 的量化公式:电压 = (ADC值 / 4095) × 参考电压(3.3V);
电压→NTC 电阻:
串联分压电路(固定电阻 R0=10KΩ):
固定电阻两端电压:V_R0 = 3.3V - V_NTC;
串联电路电流相等:V_NTC / R_NTC = V_R0 / R0;
推导得:R_NTC = (R0 × V_NTC) / (3.3V - V_NTC);
电阻→温度(B 值公式):
NTC 热敏电阻的 B 值公式(假设 25℃时标称电阻 R25=10KΩ,B=3950K):
1/T = 1/T25 + (1/B) × ln(R/R25)
T:绝对温度(K),T25=25℃+273.15=298.15K;
R:NTC 当前电阻,R25=10KΩ;
推导得摄氏温度:Tc = (B / (ln(R/R25) + B/T25)) - 273.15。
为什么B=3950K?
- 先明确:什么是 NTC 的 B 值?
NTC(负温度系数热敏电阻)的电阻随温度升高而减小,B 值是描述这种关系的参数,定义为:
B = T1×T2 / (T2-T1) × ln(R1/R2)
T1、T2:绝对温度(K),通常取 T1=25℃(298.15K)、T2=100℃(373.15K);
R1、R2:NTC 在 T1、T2 温度下的电阻值(如 R1=10KΩ@25℃,R2=3.43KΩ@100℃);
B 值越大,NTC 电阻随温度变化越敏感(相同温度变化下,电阻变化更大)。
- 为什么代码中是 3950K?
因为代码适配的 NTC 型号是 "10KΩ@25℃,B 值 3950K"(市面上最常用的通用型 NTC):
这是 NTC 厂商的标准化参数(如常见型号 NTC-MF52A-10K-B3950)
1.5 Shell 命令:gav和ngt
通过串口输入命令,触发 ADC 值或温度读取,方便调试:
gav:读取 ADC 原始值(Get_ADC_Value);ngt:读取 NTC 温度(NTC_Get_Temprature),并将返回的 "温度 ×1000" 转换为浮点数打印;ZNS_CMD_EXPORT:自定义 shell 命令注册宏,将命令导出到 shell 系统,支持用户通过串口调用。
cpp
void gav(int argc,char **argv)
{
log_d("gav command is running...");
log_d("adc value is %d",Get_ADC_Value());
}
ZNS_CMD_EXPORT(gav,get adc value)
void ngt(int argc,char **argv)
{
log_d("ngt command is running ....");
log_d("ntc temprature is %f",((float)NTC_Get_Temprature())/1000.0);
}
ZNS_CMD_EXPORT(ngt,ntc get temprature)
2. AP3216C 传感器(ALS 环境光检测 + PS 接近检测 + IR 红外检测)的 I2C 驱动实现
通过 I2C 总线初始化传感器、读取环境光强度(ALS)数据
宏定义

cpp
#ifndef _AP3216_H_ // 1. 防止头文件重复包含(预处理守卫)
#define _AP3216_H_
// 2. 定义AP3216C的I2C从机地址(7位地址)
#define AP3216C_I2C_ADDR 0x1E
// 3. 定义AP3216C的关键寄存器地址(寄存器是传感器内部存储配置/数据的单元)
#define AP3216C_REG_SYSCONFIG 0x00 // 系统配置寄存器(复位、工作模式)
#define AP3216C_REG_IRDATA_LO 0x0A // IR红外数据低8位寄存器
#define AP3216C_REG_IRDATA_HI 0x0B // IR红外数据高8位寄存器
#define AP3216C_REG_ALSDATA_LO 0x0C // ALS环境光数据低8位寄存器
#define AP3216C_REG_ALSDATA_HI 0x0D // ALS环境光数据高8位寄存器
#define AP3216C_REG_PSDATA_LO 0x0E // PS接近数据低8位寄存器
#define AP3216C_REG_PSDATA_HI 0x0F // PS接近数据高8位寄存器
// 4. 声明对外提供的函数接口(告诉其他文件:这些函数在别的地方实现了,可以直接调用)
extern int ap3216c_init(void); // 传感器初始化函数
extern int ap3216c_read_als(void); // 读取环境光数据函数
#endif // 预处理守卫结束
防止重复包含:#ifndef AP3216_H #define AP3216_H #endif 是「预处理守卫」。如果多个文件(如main.c和ap3216.c)都#include "ap3216.h",不会导致宏定义(如AP3216C_I2C_ADDR)和函数声明重复,避免编译报错。
统一宏定义:将 I2C 地址、寄存器地址这些 "固定参数" 集中定义,后续需要修改时(如传感器地址变了),只需改头文件的AP3216C_I2C_ADDR,不用在所有调用的地方逐个修改,维护方便。
暴露函数接口:extern 关键字声明函数 "在其他文件中实现"(实际在ap3216.c中),让main.c等文件可以调用ap3216c_init()和ap3216c_read_als(),而不用关心函数内部怎么实现的(模块化设计)。
2.1 I2C 寄存器读写:i2c_read_reg/i2c_write_reg
I2C复习嗷
https://blog.csdn.net/2301_76153977/article/details/154956302?spm=1001.2014.3001.5501
AP3216C 通过 I2C 总线与 MCU 通信,需遵循 "I2C 从机地址→寄存器地址→数据读写" 的通信协议,这两个函数是核心封装。
(1)写寄存器:i2c_write_reg(unsigned char reg_addr, unsigned char value)
功能:向 AP3216C 的指定寄存器写入 1 字节数据(用于配置传感器)
cpp
static int i2c_write_reg(unsigned char reg_addr, unsigned char value)
{
IIC_Start(); // 1. 发送I2C起始信号(SCL高电平时,SDA从高变低)
// 2. 发送I2C从机地址+写命令:(地址<<1) | 0X00(0XFE=0X1E<<1 & 0XFE,确保写位为0)
IIC_Write_Byte((AP3216C_I2C_ADDR<<1)&0XFE);
IIC_Write_Byte(reg_addr); // 3. 发送要写入的寄存器地址(如SYSCONFIG=0X00)
IIC_Write_Byte(value); // 4. 发送要写入的寄存器数据
IIC_Stop(); // 5. 发送I2C停止信号(SCL高电平时,SDA从低变高)
return 0;
}
AP3216C_I2C_ADDR << 1:7 位地址左移 1 位,腾出最低位作为 "读写命令位"
I2C 总线通信时,MCU 发送的第一个字节是 "8 位从机地址 + 读写位":
写操作:最低位 = 0 → 公式 (地址<<1) & 0xFE(确保最低位 0);
读操作:最低位 = 1 → 公式 (地址<<1) | 0x01(确保最低位 1);
I2C 写操作的时序为「Start → 从机地址 + 写 → 寄存器地址 → 数据 → Stop」
(2)读寄存器:i2c_read_reg(unsigned char reg_addr, unsigned char *data)
cpp
static int i2c_read_reg(unsigned char reg_addr, unsigned char *data)
{
IIC_Start(); // 1. 发送起始信号
// 2. 发送从机地址+写命令(先告知传感器要读取的寄存器地址,需用写命令)
IIC_Write_Byte((AP3216C_I2C_ADDR<<1)&0XFE);
IIC_Write_Byte(reg_addr); // 3. 发送要读取的寄存器地址(如ALSDATA_LO=0X0C)
IIC_Start(); // 4. 发送重复起始信号(切换为读操作,无需Stop)
// 5. 发送从机地址+读命令:(地址<<1) | 0X01(0X1F=0X1E<<1 | 0X01,读位为1)
IIC_Write_Byte((AP3216C_I2C_ADDR<<1)|0X01);
*data = IIC_Read_Byte(); // 6. 读取寄存器数据
IIC_NAck(); // 7. 发送非应答(告诉传感器已读完1字节,无需再发数据)
IIC_Stop(); // 8. 发送停止信号
return 0;
}
I2C 读寄存器的时序为「Start → 从机地址 + 写 → 寄存器地址 → 重复 Start → 从机地址 + 读 → 读数据 → NAck → Stop」(核心是 "先写地址,再读数据")。
2.2 传感器初始化:ap3216c_init(void)
功能:配置 AP3216C 进入工作模式(ALS+PS+IR 同时开启),初始化流程遵循传感器 datasheet 要求。

cpp
int ap3216c_init(void)
{
/* 系统配置寄存器(0X00)的模式说明(代码注释已列出):
0X04:SW reset(软件复位)
0X03:ALS and PS+IR functions active(ALS+PS+IR同时开启,持续工作)
*/
i2c_write_reg(AP3216C_REG_SYSCONFIG, 0X04); // 第一步:发送软件复位命令
HAL_Delay(500); // 等待复位完成(传感器复位需要时间, datasheet 要求至少10ms,这里留足500ms)
i2c_write_reg(AP3216C_REG_SYSCONFIG, 0X03); // 第二步:设置为持续工作模式
return 0;
}
传感器上电后可能处于不确定状态,复位可确保寄存器恢复默认值,再配置工作模式更可靠。
工作模式选择:0X03 是 "持续工作模式"(ALS 和 PS+IR 一直采集数据),若需低功耗,可选择 "单次模式"(如 0X07:采集一次后进入低功耗,需再次触发)。
2.3 环境光数据读取:ap3216c_read_als(void)
功能:读取 AP3216C 的 ALS(环境光)测量值(16 位数据),并返回结果。
cpp
int ap3216c_read_als(void)
{
uint16_t als_value = 0; // ALS数据为16位(低8位+高8位)
// ((uint8_t *)(&als_value))+1 → 取als_value的高8位地址(小端模式下,uint16_t的字节顺序是:低字节存低地址,高字节存高地址)
i2c_read_reg(AP3216C_REG_ALSDATA_LO, ((uint8_t *)(&als_value))+1);
// ((uint8_t *)(&als_value))+0 → 取als_value的低8位地址
i2c_read_reg(AP3216C_REG_ALSDATA_HI, ((uint8_t *)(&als_value))+0);
log_i("als_value is %d", als_value); // 打印环境光原始值
return als_value;
}
为什么 "低字节存高地址,高字节存低地址"?
这是由 AP3216C 的数据存储格式(大端序) 和 STM32 的内存存储格式(小端序) 共同决定的:
AP3216C 的 ALS 数据存储:环境光强度数据是 16 位,分为 "低字节(LO)" 和 "高字节(HI)",分别存在两个独立寄存器中;
数据格式为 大端序:高字节是数据的 "高位部分",低字节是 "低位部分"(例:数据 0x3412,高字节 = 0x12,低字节 = 0x34);
STM32 的内存存储(小端序):16 位变量 als_value 在内存中占 2 字节,地址分布为:
低地址(&als_value + 0):存储变量的 "低字节";
高地址(&als_value + 1):存储变量的 "高字节";
数据拼接逻辑:读取 AP3216C 的 "低字节(LO)"→ 存入 als_value 的高地址(+1);
读取 AP3216C 的 "高字节(HI)"→ 存入 als_value 的低地址(+0);
最终拼接结果:als_value = (HI << 8) | LO(例:HI=0x12,LO=0x34 → als_value=0x1234);
3. Modbus RTU 的开关控制
通过 RS485 总线(Modbus RTU 协议)控制灯光(LIGHT)和风扇(FAN)的开关,同时提供 shell 命令调试
核心业务逻辑
代码严格遵循 "模块化设计",整体结构如下:
| 模块 | 核心文件 / 模块 | 功能职责 |
|---|---|---|
| 1. 硬件 | main.c(GPIO / 时钟 / USART1)、bsp_485.c、bsp_TiMbase.c | 初始化 STM32 核心硬件(时钟、GPIO、串口)、RS485 通信接口、定时器(帧同步) |
| 2. 通信协议 | mb_m.h(Modbus 主机库) | 实现 Modbus RTU 主机协议,封装 "读线圈""写线圈" 等核心命令,处理帧打包 / 解析 / CRC 校验 |
| 3. 驱动适配 | SwitchDrvAdaptor.h | 统一灯光 / 风扇的开关接口(硬件无关抽象),支持 Modbus 继电器等多种驱动方式 |
| 4. 任务调度 | FreeRTOS(main.c、SwitchDrvAdaptor.h) | 创建 / 管理 Modbus 轮询、设备监控、灯光 / 风扇控制等任务,通过信号量实现任务间通信 |
| 5. 应用调试 | shell 相关代码 | 提供 Shell 命令(ol/cl/of/cf/modbusread/modbuswrite),支持手动控制和调试 |
核心设计思想:分层解耦,比如后续要把 "Modbus 继电器" 换成 "GPIO 直接控制",只需修改驱动适配层的接口实现,无需改动 Modbus 协议、任务调度等上层代码。
3.1 RS485 硬件驱动( bsp_485.c相关代码)

RS485 是工业常用的差分通信总线,需通过 "使能引脚(EN)" 切换收发模式,核心函数如下:
(1)初始化函数 :RS485_Init(uint32_t ulBaudRate, uint8_t ucDataBits, uint8_t eParity)
功能:配置 USART2 和 RS485 相关 GPIO,初始化通信参数。

cpp
void RS485_Init(...)
{
// 1. 使能时钟(USART2+GPIOA/B/C,其中RS485_EN可能接PB/PC引脚)
__HAL_RCC_USART2_CLK_ENABLE();
__HAL_RCC_GPIOA_CLK_ENABLE(); // USART2_TX=PA2, RX=PA3
__HAL_RCC_GPIOB_CLK_ENABLE();
__HAL_RCC_GPIOC_CLK_ENABLE();
// 2. 配置RS485_EN引脚(输出模式,控制收发切换)
GPIO_InitStruct.Pin = RS485_EN_Pin; // 需在头文件定义(PC5)
GPIO_InitStruct.Mode = GPIO_MODE_OUTPUT_PP; // 推挽输出
HAL_GPIO_Init(RS485_EN_GPIO_Port, &GPIO_InitStruct);
// 3. 配置USART2引脚(TX=PA2复用输出,RX=PA3输入)
GPIO_InitStruct.Pin = GPIO_PIN_2; // TX
GPIO_InitStruct.Mode = GPIO_MODE_AF_PP; // 复用推挽(UART输出)
HAL_GPIO_Init(GPIOA, &GPIO_InitStruct);
GPIO_InitStruct.Pin = GPIO_PIN_3; // RX
GPIO_InitStruct.Mode = GPIO_MODE_INPUT; // 普通输入
HAL_GPIO_Init(GPIOA, &GPIO_InitStruct);
// 4. 配置UART参数(波特率、数据位、校验位、停止位)
huart2.Instance = USART2;
huart2.Init.BaudRate = ulBaudRate; // 如9600(Modbus常用)
// 数据位+校验位适配:8位数据无校验→8位;8位数据+校验→9位(第9位为校验位)
switch ( ucDataBits )
{
case 8:
if (eParity == 0)
huart2.Init.WordLength = UART_WORDLENGTH_8B;
else
huart2.Init.WordLength = UART_WORDLENGTH_9B;
break;
case 7:
break;
default:
break;
huart2.Init.StopBits = UART_STOPBITS_1; // Modbus默认1位停止位
// 校验位配置(0=无校验,1=奇校验,2=偶校验,Modbus常用无校验)
switch (eParity) {
case 0: huart2.Init.Parity = UART_PARITY_NONE; break;
case 1: huart2.Init.Parity = UART_PARITY_ODD; break;
case 2: huart2.Init.Parity = UART_PARITY_EVEN; break;
}
HAL_UART_Init(&huart2);
// 5. 使能UART接收中断(RXNE:接收数据非空中断)
HAL_NVIC_SetPriority(USART2_IRQn, 1, 0); // 中断优先级1(较高)
HAL_NVIC_EnableIRQ(USART2_IRQn);
__HAL_UART_ENABLE_IT(&huart2, UART_IT_RXNE);
RS485_RX_MODE(); // 初始化为接收模式(EN引脚拉低/拉高,取决于RS485芯片)
}
RS485_TX_MODE()是给PC5置1, RS485_RX_MODE()是给PC5置0

RS485_Init函数被调用时,参数是 RS485_Init(9600, 8, 0)(波特率 9600、8 位数据、无校验),因此这段代码会执行:
ucDataBits=8 + eParity=0 → 字长设为UART_WORDLENGTH_8B;
eParity=0 → 校验模式设为UART_PARITY_NONE;
配合固定的UART_STOPBITS_1(1 位停止位),最终串口配置为 9600-8-N-1(波特率 9600、8 位数据、无校验、1 位停止位)------ 这是 Modbus RTU 的标准配置,确保与从机通信兼容。
(2)发送函数 :RS485_Send_Data(uint8_t *Tx_Buf, uint8_t TxCount)
功能:切换为发送模式,通过 UART 发送数据,发送完成后切回接收模式。
cpp
void RS485_Send_Data(uint8_t *Tx_Buf, uint8_t TxCount)
{
RS485_TX_MODE(); // 切发送模式
HAL_UART_Transmit(&huart2, Tx_Buf, TxCount, 0xffff); // 阻塞发送(超时16ms)
RS485_RX_MODE(); // 切接收模式
}
3.2 Modbus RTU 主机协议(bsp485.c中)
代码使用开源 Modbus 主机库( FreeModbus),核心是线圈读写(Modbus 功能码 0x01 读线圈、0x05 写单个线圈)。
Modbus 是 工业领域最常用的串行通信协议,核心作用是让不同设备(如单片机、传感器、PLC、继电器模块)通过总线(如 RS485)实现数据交互,简单说就是 "工业设备间的'普通话'"------ 不管设备品牌、型号如何,只要支持 Modbus,就能互相 "沟通"。
主机 :主动发起命令的设备(我的 GD32 单片机);
代码中:eMBMasterInit 初始化为主机,eMBMasterReqReadCoils(读线圈)、eMBMasterReqWriteCoil(写线圈)是主机发送的命令;
从机 :被动响应命令的设备(我的 Modbus 继电器模块);每个从机有唯一地址(如我的代码中从机地址 0x01),主机通过地址指定要通信的从机。
Modbus 主机(STM32)通过 RS485 总线,用 RTU 模式和 Modbus 从机(继电器)通信,核心是通过 "读线圈""写线圈" 命令实现开关控制
(1)核心函数说明
| 函数 | 功能描述 |
|---|---|
eMBMasterReqReadCoils |
读线圈状态(功能码 0x01):参数 = 从机地址、线圈起始地址、线圈数量、超时时间 |
eMBMasterReqWriteCoil |
写单个线圈(功能码 0x05):参数 = 从机地址、线圈地址、线圈状态(0 = 断,1 = 通)、超时时间 |

(2)Shell 命令封装:modbusread/modbuswrite
通过 shell 命令手动触发 Modbus 操作,方便调试:
cpp
// 读线圈命令:modbusread 从机地址 线圈起始地址 线圈数量(十进制)
int modbusread(int argc, char **argv)
{
if(argc !=4) { /* 参数错误提示 */ return 0; }
UCHAR ucSndAddr = atoi(argv[1]); // 从机地址(如1)
USHORT usCoilAddr = atoi(argv[2]); // 线圈地址(如0)
USHORT usNCoils = atoi(argv[3]); // 线圈数量(如1)
eMBMasterReqReadCoils(ucSndAddr, usCoilAddr, usNCoils, 100); // 超时100ms
}
// 写线圈命令:modbuswrite 从机地址 线圈地址 线圈状态(0=关,1=开)
int modbuswrite(int argc, char **argv)
{
if(argc !=4) { /* 参数错误提示 */ return 0; }
UCHAR ucSndAddr = atoi(argv[1]);
USHORT usCoilAddr = atoi(argv[2]);
USHORT usCoilData = atoi(argv[3]); // 1=置1(开),0=置0(关)
eMBMasterReqWriteCoil(ucSndAddr, usCoilAddr, usCoilData, 100);
}

示例:在终端输入 modbuswrite 1 0 1 → 控制地址 1 的从机,线圈 0 置 1(对应开关打开)。
3.3 设备驱动适配层与 FreeRTOS 任务(SwitchDrvAdaptor.c相关代码)
核心逻辑是 "硬件无关抽象",把不同驱动方式(如 Modbus 继电器、GPIO)封装成统一的开关接口,上层任务无需关心硬件细节:
- 接口结构体(switch_drv_ops_stru):定义p_switch_open(打开)、p_switch_close(关闭)两个函数指针,作为统一接口;
- 驱动配置:通过条件编译选择驱动方式(如SWITCH_DRV_ADAPTOR_LIGHT_SWITCH_USING_MODBUS_RELAY启用 Modbus 继电器驱动),绑定对应的实现函数(如light_modbus_open调用 Modbus 写线圈命令);
- 状态反馈:预留共享内存和信号量,用于向中间层反馈开关执行结果(如 "灯光已打开")。
(1)驱动接口结构体:switch_drv_ops_stru
定义开关设备的统一操作接口(类似 "函数指针数组"):
cpp
// 假设结构体定义(头文件中)
typedef struct {
void (*p_switch_open)(void); // 打开设备
void (*p_switch_close)(void); // 关闭设备
} switch_drv_ops_stru;
(2)驱动适配配置
通过条件编译选择驱动方式(Modbus 继电器或预留的 "ABC" 驱动):
cpp
// 灯光驱动配置:若定义SWITCH_DRV_ADAPTOR_LIGHT_SWITCH_USING_MODBUS_RELAY,则使用Modbus继电器驱动
switch_drv_ops_stru Switch_LIGHT_Ops =
#ifdef SWITCH_DRV_ADAPTOR_LIGHT_SWITCH_USING_MODBUS_RELAY
SWITCH_LIGHT_DRV_OPS_CONFIG_MODBUS_RELAY ; // 灯光的Modbus驱动实现
#endif
#ifdef SWITCH_DRV_ADAPTOR_LIGHT_SWITCH_USING_ABC
SENSOR_TEMPRATURE_DRV_OPS_CONFIG_ABC ; // 预留其他驱动(如GPIO)
#endif
// 风扇驱动配置(逻辑同上)
switch_drv_ops_stru Switch_FAN_Ops =
#ifdef SWITCH_DRV_ADAPTOR_FAN_SWITCH_USING_MODBUS_RELAY
SWITCH_FAN_DRV_OPS_CONFIG_MODBUS_RELAY ;
#endif
#ifdef SWITCH_DRV_ADAPTOR_FAN_SWITCH_USING_ABC
SENSOR_BRIGHT_DRV_OPS_CONFIG_ABC ;
#endif
(3)FreeRTOS 控制任务
创建两个独立任务(灯光 / 风扇),通过信号量与中间层(MidLayer)通信:
cpp
// 灯光控制任务
void switch_drv_adaptor_light_process_task_entry(void *p)
{
log_i("switch_light_process is running...\r\n");
while(1)
{
log_i("wait for midlayer_switch_light_req");
xSemaphoreTake(midlayer_switch_light_mmc_ctrl.sem_req,portMAX_DELAY); //等待来自中间层的控制请求
if (LIGHT_CTRL_OPEN_STATUS == (((midlayer_switch_light_stru *)(midlayer_switch_light_mmc_ctrl.p_shared_mem))->light_sw))
{
log_i("Open the Light!");
Switch_LIGHT_Ops.p_switch_open();
//((midlayer_switch_light_stru *)(midlayer_switch_light_mmc_ctrl.p_shared_mem))->light_sw = xxx 装填开关状态反馈
}
if (LIGHT_CTRL_CLOSE_STATUS == (((midlayer_switch_light_stru *)(midlayer_switch_light_mmc_ctrl.p_shared_mem))->light_sw))
{
log_i("Close the Light!");
Switch_LIGHT_Ops.p_switch_close();
//((midlayer_switch_light_stru *)(midlayer_switch_light_mmc_ctrl.p_shared_mem))->light_sw = xxx 装填开关状态反馈
}
xSemaphoreGive(midlayer_switch_light_mmc_ctrl.sem_ack); //回应中间层
}
}
通信逻辑:中间层通过 sem_req 信号量触发任务,任务执行后通过 sem_ack 应答,p_shared_mem 是共享内存,传递控制命令和状态反馈;
优势:任务独立,灯光和风扇控制互不干扰,实时性高。
(4)Shell 调试命令(ol/cl/of/cf)
直接调用驱动接口,手动控制开关(调试用):
cpp
void ol(int argc,char **argv) { Switch_LIGHT_Ops.p_switch_open(); } // open light
ZNS_CMD_EXPORT(ol, open light)
void cl(int argc,char **argv) { Switch_LIGHT_Ops.p_switch_close(); } // close light
ZNS_CMD_EXPORT(cl, close light)
void of(int argc,char **argv) { Switch_FAN_Ops.p_switch_open(); } // open fan
ZNS_CMD_EXPORT(of, open fan)
void cf(int argc,char **argv) { Switch_FAN_Ops.p_switch_close(); } // close fan
ZNS_CMD_EXPORT(cf, close fan)
4. Modbus RTU 继电器控制的核心驱动实现(switch_drv.c)
承接了之前的 RS485 硬件驱动和上层应用逻辑,核心作用是:将 "灯光 / 风扇的开关操作" 封装为 Modbus 写线圈命令,同时提供 Modbus 协议初始化、轮询和状态监控任务 ------ 是 "上层开关命令" 与 "底层 Modbus 通信" 之间的桥梁。
| 模块 | 核心函数 / 任务 | 功能职责 |
|---|---|---|
| Modbus 继电器控制接口 | modbus_relay_open_light/close_light、modbus_relay_open_fan/close_fan |
封装 Modbus 写线圈命令,实现灯光 / 风扇的开关控制(对外提供简单接口) |
| Modbus 协议初始化任务 | ModbusMasterPoll_Task |
初始化 Modbus 主机协议栈、使能协议,循环处理 Modbus 收发和解析(协议核心) |
| Modbus 状态监控任务 | ModbusMasterMoniter_Task |
周期读取从机线圈状态,用于监控设备运行情况(如继电器是否真的打开) |
4.1 核心控制接口:灯光 / 风扇的 Modbus 命令封装
实际上参数意义:
eMBMasterErrCode eMBMasterReqWriteCoil(
UCHAR ucSndAddr, // 从机地址
USHORT usCoilAddr, // 线圈地址
USHORT usCoilData, // 线圈状态(0x0000=关,0xFF00=开)
LONG lTimeOut // 超时时间(ms) );
继电器0号线圈接灯,1号线圈接风扇
| 函数名 | 从机地址 | 线圈地址 | 线圈状态 | 超时 | 功能描述 |
|---|---|---|---|---|---|
modbus_relay_open_light |
0x01 | 0x00 | 0xFF00 | 100 | 控制 0x01 从机的 0 号线圈开(灯光打开) |
modbus_relay_close_light |
0x01 | 0x00 | 0x0000 | 100 | 控制 0x01 从机的 0 号线圈关(灯光关闭) |
modbus_relay_open_fan |
0x01 | 0x01 | 0xFF00 | 100 | 控制 0x01 从机的 1 号线圈开(风扇打开) |
modbus_relay_close_fan |
0x01 | 0x01 | 0x0000 | 100 | 控制 0x01 从机的 1 号线圈关(风扇关闭) |
cpp
int modbus_relay_open_light(void)
{
return eMBMasterReqWriteCoil(0x01, 0x00, 0xff00, 100);
}
int modbus_relay_close_light(void)
{
return eMBMasterReqWriteCoil(0x01, 0x00, 0x0000, 100);
}
int modbus_relay_open_fan(void)
{
return eMBMasterReqWriteCoil(0x01, 0x01, 0xff00, 100);
}
int modbus_relay_close_fan(void)
{
return eMBMasterReqWriteCoil(0x01, 0x01, 0x0000, 100);
}
4.2 Modbus 协议核心任务:ModbusMasterPoll_Task
负责 Modbus 主机的初始化和协议处理,是 Modbus 通信的 "心脏":
cpp
void ModbusMasterPoll_Task(void *pvParameters)
{
// 1. Modbus主机初始化
eMBMasterInit(MB_RTU, 0x02, 9600, MB_PAR_NONE);
// 参数解析:
// MB_RTU:Modbus通信模式(RTU模式,适用于RS485总线);
// 0x02:本设备(Modbus主机)的地址(主机地址仅在多主机场景有用,单主机可忽略);
// 9600:RS485通信波特率(必须与从机一致);
// MB_PAR_NONE:无校验(与从机校验方式一致,否则通信失败);
eMBMasterEnable(); // 2. 使能Modbus主机(启动协议栈)
log_i("Modbus init ok\r\n"); // 3. 打印初始化成功日志
while(1)
{
eMBMasterPoll(); // 4. 循环轮询:处理Modbus请求队列、收发数据、解析响应
vTaskDelay(6); // 延迟6ms,平衡CPU占用和通信实时性
}
}
关键:eMBMasterPoll是 FreeModbus 主机库的核心函数,必须周期性调用(延迟不能过长,否则会丢失从机响应)。
4.3 Modbus 状态监控任务:ModbusMasterMoniter_Task
周期性读取从机线圈状态,用于监控设备是否按命令执行(如发送 "打开灯光" 后,确认 0 号线圈是否真的为 "开")。
cpp
void ModbusMasterMoniter_Task(void *pvParameters)
{
while(1)
{
// 读从机线圈:从机地址0x01、起始线圈地址0x0000、读取16个线圈(0x0010)、超时-1(无限等待)
eMBMasterReqReadCoils(0x01, 0x0000, 0x0010, -1);
vTaskDelay(800); // 每800ms读取一次
}
}
参数解析:
从机地址0x01:需与实际 Modbus 从机(如继电器模块)的地址一致;
线圈地址0x0000:从机的第一个线圈地址(对应开关 1);
线圈数量0x0010(16 个):一次读取 16 个线圈的状态;
超时-1:无限等待响应(不建议,改为 100~500ms 避免任务阻塞)。
