【RTOS】智能家居-驱动调试&适配层开发

智能家居-顶层业务模块https://blog.csdn.net/2301_76153977/article/details/156066941?sharetype=blogdetail&sharerId=156066941&sharerefer=PC&sharesource=2301_76153977&spm=1011.2480.3001.8118

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配置选项的含义?

  1. 核心配置(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。
  1. 通道配置(sConfig
配置项 代码值 含义 为什么这么配置?
Channel ADC_CHANNEL_13 选择采集通道 13 通道 13 对应硬件引脚(如 PB1),是 NTC 传感器的采样引脚,需与硬件接线一致。
Rank ADC_REGULAR_RANK_1 通道优先级:1 多通道采集时,按Rank顺序转换(Rank1 先转,Rank2 后转);单通道场景下 Rank 无意义,仅需设为 1。
SamplingTime ADC_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 的 "量化原理":

  1. 核心前提

ADC 是 "模数转换器",作用是把连续的模拟电压(如 0~3.3V)转换为离散的数字值(二进制);

12 位 ADC 的含义:数字值的范围是 0~2¹²-1 = 0~4095(共 4096 个离散等级);

参考电压(V_REF):ADC 的 "测量标尺",代码中是 3.3V(即 ADC 能测量的电压范围是 0~3.3V)。

  1. 推导过程

假设: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?

  1. 先明确:什么是 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 电阻随温度变化越敏感(相同温度变化下,电阻变化更大)。

  1. 为什么代码中是 3950K?

因为代码适配的 NTC 型号是 "10KΩ@25℃,B 值 3950K"(市面上最常用的通用型 NTC):

这是 NTC 厂商的标准化参数(如常见型号 NTC-MF52A-10K-B3950)

1.5 Shell 命令:gavngt

通过串口输入命令,触发 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 要求。

来自详解AP3216c

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_lightmodbus_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 避免任务阻塞)。

相关推荐
思为无线NiceRF2 天前
UWB 智能门锁系统在现有手机生态下的可行性分析
嵌入式硬件·物联网·智能家居
Q_21932764552 天前
基于STM32的智能家居安防系统设计
网络·stm32·智能家居
飞睿科技2 天前
探讨雷达在智能家居与消费电子领域的应用
人工智能·嵌入式硬件·智能家居·雷达·毫米波雷达
世人万千丶3 天前
鸿蒙跨端框架Flutter学习day 1、变量与基本类型-智能家居监控模型
学习·flutter·ui·智能家居·harmonyos·鸿蒙·鸿蒙系统
得一录4 天前
大模型在智能家居场景下的应用架构
架构·智能家居
WZGL12305 天前
智慧养老方兴未艾,“AI+养老”让银龄老人晚年更美好
大数据·人工智能·物联网·生活·智能家居
清风6666667 天前
基于单片机的多传感器智能云梯逃生控制器设计
单片机·嵌入式硬件·毕业设计·智能家居·课程设计
三佛科技-134163842128 天前
PL3327CE/PL3327CD/CS/CF原边调节恒流/恒压控制离线反激式开关电源芯片 典型应用电路
单片机·嵌入式硬件·物联网·智能家居
三佛科技-134163842128 天前
HN32512非隔离12V300MA~600MA降压控制方案典型应用 电路
单片机·嵌入式硬件·物联网·智能家居·pcb工艺
三佛科技-134163842129 天前
FT32F072xx、FT32F072xB、FT32F072x6/x8基于ARM Cortex-M0内核32位单片机分析
arm开发·单片机·嵌入式硬件·智能家居·pcb工艺