基于FreeRTOS的多协议融合开发,整合了ESP8266 MQTT通信、Modbus RTU、CAN总线、传感器数据采集(温湿度、电压、电流)等核心功能。

先说明一下:本文基于STM32(以STM32F103为例,其他系列可快速适配),使用HAL库开发,整合了多个常用外设和通信协议,适合用于工业控制、智能监测等场景。

一、项目功能概述

本项目实现了一个多功能、多协议的嵌入式监测与控制终端,核心功能如下:

  • 传感器采集:通过AHT20采集温湿度、INA226采集总线电压/电流、ADC采集电位器电压和CPU温度;
  • 显示功能:通过OLED实时显示采集到的温湿度、电压、电流数据;
  • 通信协议:支持Modbus RTU(从机)、CAN总线(接收数据+SDO监测)、ESP8266 MQTT(上传传感器数据、接收控制指令);
  • 任务管理:基于FreeRTOS实现多任务调度(MQTT任务、CAN任务、Modbus任务),任务优先级合理分配;
  • 控制功能:通过MQTT指令控制LED和蜂鸣器,支持从机地址动态配置、参数掉电保存(AT24C02)。

二、硬件选型(关键外设)

项目硬件搭配简洁,性价比高,适合入门和项目落地,核心外设如下:

外设名称

型号

用途

通信方式

主控芯片

STM32F103C8T6

核心控制,任务调度、协议处理

无(主控)

温湿度传感器

AHT20

采集环境温湿度(精度高)

IIC

电压电流传感器

INA226

采集总线电压、电流

IIC

WiFi模块

ESP8266(ESP-01)

MQTT通信,上传数据、接收指令

UART(USART3)

显示模块

0.96寸OLED(128*64)

实时显示传感器数据

IIC

存储模块

AT24C02

掉电保存从机地址等参数

IIC

通信总线

CAN收发器(TJA1050)

CAN总线数据收发、SDO监测

CAN

其他

LED、蜂鸣器、电位器

指示、报警、模拟量采集

GPIO、ADC

备注:硬件接线需对应代码中的引脚配置(如ESP8266接USART3、AHT20/INA226接IIC引脚),具体可参考代码中HAL库的外设初始化配置。

三、核心代码解析(重点模块)

项目代码分为3个核心文件:main.c(主函数、系统初始化)、app_task.c(任务实现、传感器采集、协议处理)、freertos.c(FreeRTOS任务创建),以下重点解析关键模块,完整代码附在文末。

3.1 系统初始化(main.c)

主函数核心逻辑:初始化MCU外设 → 初始化应用任务 → 启动FreeRTOS调度器,代码简洁,重点关注外设初始化顺序和任务启动时机。

int main(void)
{
  /* 1. 基础初始化:HAL库、系统时钟、外设 */
  HAL_Init();
  SystemClock_Config(); // 系统时钟配置(72MHz)
  MX_GPIO_Init();       // GPIO初始化(LED、蜂鸣器)
  MX_CAN_Init();        // CAN初始化
  MX_USART1_UART_Init();// USART1(调试/透传)
  MX_USART3_UART_Init();// USART3(ESP8266通信)
  MX_ADC1_Init();       // ADC初始化(电位器、CPU温度)
  MX_TIM6_Init();       // 定时器初始化
  MX_TIM7_Init();
  MX_NVIC_Init();       // 中断优先级配置

  /* 2. 应用任务初始化:传感器、OLED、MQTT、Modbus等 */
  Task_AppInit();

  /* 3. 启动FreeRTOS调度器,接管任务管理 */
  osKernelInitialize();
  MX_FREERTOS_Init();   // 创建3个核心任务
  osKernelStart();

  /* 调度器启动后,不会执行到这里 */
  while (1)
  {
  }
}

关键说明:SystemClock_Config函数配置HSE时钟,PLL倍频到72MHz,适配STM32F103的常规配置;Task_AppInit是应用层核心初始化函数,后续重点解析。

3.2 应用任务初始化(Task_AppInit)

该函数在app_task.c中实现,负责初始化所有传感器、OLED、通信协议,配置中断和任务参数,是项目的“启动入口”,核心代码解析如下:

c

关键说明:

  • AT24C02用于保存从机地址,避免掉电后地址丢失,初始化时读取,异常时设为默认值1;
  • UART采用中断接收+FIFO缓存(128字节),避免数据丢失,适配ESP8266的MQTT指令和调试信息;
  • CanFestival用于CAN总线的SDO监测,可实时获取OD对象字典中的参数(如温度、蜂鸣器状态);
  • Modbus RTU初始化后,可通过Modbus主机读取传感器数据、控制LED/蜂鸣器。

3.3 FreeRTOS任务调度(freertos.c)

项目创建3个核心任务,基于FreeRTOS的抢占式调度,合理分配优先级,避免任务阻塞,核心代码如下:

void Task_AppInit(void)
{
  // 1. OLED初始化,显示启动信息
  OLED_Init();
  OLED_ShowString8x16(0, 0, "ESP8266");
  OLED_ShowString8x16(0, 1, "READY");
  OLED_ShowString8x16(0, 2, "waitting...");

  // 2. 传感器初始化
  INA226_Init(&ina226);  // 电压电流传感器
  AHT20_Init(&aht20);    // 温湿度传感器
  HAL_ADCEx_Calibration_Start(&hadc1); // ADC校准

  // 3. 存储模块初始化,读取从机地址(掉电保存)
  at24cxx_init();
  at24cxx_read(0, &g_addr, 1);
  if ((g_addr < 1U) || (g_addr > 127U))
  {
    g_addr = 1U; // 地址异常时默认设为1
  }
  usRegHoldingBuf[REG_SLAVE_ADDR] = (USHORT)g_addr;
  deviceInfo_SlaveAddress = g_addr;

  // 4. UART中断接收初始化(USART1/USART3)
  HAL_UART_Receive_IT(&huart1, &rx1_byte, 1);
  HAL_UART_Receive_IT(&huart3, &rx3_byte, 1);

  // 5. MQTT初始化(ESP8266连接WiFi、MQTT服务器)
  mqtt_start();
  printf("%d\r\n", g_addr); // 打印从机地址(调试用)

  // 6. CAN总线、CanFestival初始化(SDO监测)
  HAL_TIM_Base_Start_IT(&htim7);
  CanFestival_Can_Init();
  setNodeId(&TestSlave_Data, g_addr);
  setState(&TestSlave_Data, Initialisation);
  setState(&TestSlave_Data, Operational);

  // 7. 注册OD回调函数(蜂鸣器、从机地址更新)
  (void)RegisterSetODentryCallBack(&TestSlave_Data, 0x2000, 0x09, &OnBuzzerUpdate);
  Buzzer_ApplyFromOD();
  (void)RegisterSetODentryCallBack(&TestSlave_Data, 0x2000, 0x0A, &OnNodeIdUpdate);
  NodeId_ApplyFromOD();

  // 8. Modbus RTU初始化(从机,波特率115200,无校验)
  eMBInit(MB_RTU, g_addr, 0, 115200, MB_PAR_NONE);
  eMBEnable();
  modbus_active = 1;
}

任务优先级说明:

  • MQTT任务设为中等优先级:因为MQTT涉及WiFi通信,需要及时处理接收的指令和上传数据,避免通信超时;
  • CAN任务和Modbus任务设为低优先级:两者均为周期性任务(1秒执行一次),优先级低于MQTT,避免抢占MQTT的CPU资源;
  • 每个任务通过osDelay(1)释放CPU,确保其他任务有机会执行,避免单个任务独占CPU。

3.4 核心功能模块实现

3.4.1 传感器数据采集(ADC_VR_Test + IIC_VR_Test)

通过ADC采集电位器电压和CPU温度,通过IIC采集AHT20温湿度、INA226电压电流,数据处理后更新到OLED和Modbus寄存器,核心代码片段:

// ADC采集(电位器、CPU温度)
static void ADC_VR_Test(void)
{
  uint32_t adc_pa1 = 0;
  uint32_t adc_temp = 0;
  ADC_ChannelConfTypeDef sConfig = {0};
  // 采集电位器(PA1)
  sConfig.Channel = ADC_CHANNEL_1;
  (void)HAL_ADC_ConfigChannel(&hadc1, &sConfig);
  for (uint8_t i = 0; i < 10; i++) // 10次采样平均,提高精度
  {
    HAL_ADC_Start(&hadc1);
    if (HAL_ADC_PollForConversion(&hadc1, 100) == HAL_OK)
    {
      adc_pa1 += HAL_ADC_GetValue(&hadc1);
    }
    HAL_ADC_Stop(&hadc1);
  }
  adc_pa1 /= 10U;
  val = (adc_pa1 * 3.3f / 4095.0f); // 转换为电压值(0-3.3V)

  // 采集CPU温度
  sConfig.Channel = ADC_CHANNEL_TEMPSENSOR;
  (void)HAL_ADC_ConfigChannel(&hadc1, &sConfig);
  // 同样10次采样平均...
  // 温度计算(参考STM32手册公式)
  int64_t vsense_uv = ((int64_t)adc_temp * 3300000LL) / 4095LL;
  int32_t cpu_temp_deci_c = (int32_t)((((vsense_uv - 1430000LL) * 10LL) / 4300LL) + 250LL);
  cpu_temp_abs = (cpu_temp_deci_c < 0) ? -cpu_temp_deci_c : cpu_temp_deci_c;
}

// IIC传感器采集 + OLED显示
static void IIC_VR_Test(void)
{
  // 读取AHT20温湿度、INA226电压电流
  AHT20_Read(&aht20, &temp_deci_c, &humi_deci_pct);
  INA226_ReadBusVoltage_mV(&ina226, &bus_mV);
  INA226_ReadCurrent_mA(&ina226, &cur_mA);

  // 格式化数据,显示到OLED
  sprintf(line, "T:%c%ld.%ldC", (temp_deci_c < 0) ? '-' : '+',
          (long)(abs(temp_deci_c)/10), (long)(abs(temp_deci_c)%10));
  OLED_ShowLineDiff8x16(0, line);
  sprintf(line, "V:%lu.%02luV", bus_mV/1000U, (bus_mV%1000U)/10U);
  OLED_ShowLineDiff8x16(1, line);
  // 湿度、电流显示...

  // 更新设备信息结构体(供Modbus、CAN使用)
  deviceInfo_TP = temp_deci_c;
  deviceInfo_RH = humi_deci_pct;
  deviceInfo_VL = bus_mV;
  deviceInfo_CU = cur_mA;
}

3.4.2 MQTT通信(ESP8266)

通过ESP8266连接WiFi和MQTT服务器(使用emqx公共服务器,无需自己搭建),实现“接收控制指令”和“定时上传传感器数据”,核心逻辑:

  1. mqtt_start():初始化ESP8266,连接WiFi(需修改WiFi名称和密码),连接MQTT服务器,订阅控制主题;
  1. mqtt_senddata():每2秒上传一次传感器数据(温湿度、电压、电流)到MQTT发布主题;
  1. Process_ESP8266_Command():解析ESP8266接收的MQTT指令(如LED_ON、BEEP_OFF、temp查询),执行对应操作。

关键注意:代码中WiFi名称(MIFI-070180-2.4G)和密码(12345678)需替换为自己的WiFi信息,MQTT主题可根据需求修改。

3.4.3 Modbus RTU通信

项目作为Modbus从机,寄存器地址定义在app_task.h中,支持主机读取传感器数据、控制LED/蜂鸣器,核心逻辑:

  • read_value():将传感器采集到的数据更新到Modbus保持寄存器(usRegHoldingBuf);
  • eMBPoll():Modbus从机轮询函数,在Modbus任务中周期性执行,处理主机的读写请求;
  • 寄存器地址映射:REG_TEMPERATURE(0x0003)对应温度、REG_VOLTAGE(0x0005)对应电压,可直接通过Modbus主机访问。
/*
 * FreeModbus Libary: BARE Port
 * Copyright (C) 2006 Christian Walter <wolti@sil.at>
 *
 * This library is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Lesser General Public
 * License as published by the Free Software Foundation; either
 * version 2.1 of the License, or (at your option) any later version.
 *
 * This library is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 * Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public
 * License along with this library; if not, write to the Free Software
 * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA
 *
 * File: $Id$
 */

/* ----------------------- Modbus includes ----------------------------------*/
#include "mb.h"
#include "mbport.h"
#include "mbutils.h"
#include "app_task.h"

/* ----------------------- Variables ----------------------------------------*/
static eMBEventType eQueuedEvent;   /* 事件队列中待处理的事件类型 */
static BOOL     xEventInQueue;      /* 事件队列非空标志: TRUE-有待处理事件, FALSE-无事件 */

/* ----------------------- Register buffers ---------------------------------*/
#define HOLDING_REG_SIZE    0x0100  /* 保持寄存器数量: 256个 (地址范围 0x0000-0x00FF) */
#define DISCRETE_SIZE       0x0010  /* 离散量数量: 16个 (地址范围 0x0000-0x000F) */

/* 保持寄存器缓冲区 - 用于Modbus功能码03(读)/06/16(写) */
USHORT usRegHoldingBuf[HOLDING_REG_SIZE];

/* 线圈缓冲区 - 用于Modbus功能码01(读)/05/15(写),按位存储节省空间 */
static UCHAR ucRegCoilsBuf[(DISCRETE_SIZE + 7) / 8];

/* 离散输入缓冲区 - 用于Modbus功能码02(读),按位存储节省空间 */
static UCHAR ucRegDiscreteBuf[(DISCRETE_SIZE + 7) / 8];

extern uint8_t g_addr;    /* 从站设备地址,支持运行时修改 */

/* ----------------------- 事件管理接口 --------------------------------------*/

/**
 * @brief   初始化Modbus事件队列
 * @return  TRUE-初始化成功, FALSE-初始化失败
 * @note    在eMBInit()或eMBTCPInit()之前调用
 */
BOOL xMBPortEventInit(void)
{
    xEventInQueue = FALSE;
    return TRUE;
}

/**
 * @brief   向事件队列投递一个Modbus事件
 * @param   eEvent - 待投递的事件类型
 * @return  TRUE-投递成功, FALSE-投递失败
 * @note    此函数通常在串口接收中断或定时器中断中调用
 */
BOOL xMBPortEventPost(eMBEventType eEvent)
{
    xEventInQueue = TRUE;
    eQueuedEvent = eEvent;
    return TRUE;
}

/**
 * @brief   从事件队列获取一个Modbus事件
 * @param   eEvent - 输出参数,返回获取到的事件类型
 * @return  TRUE-成功获取事件, FALSE-队列为空
 * @note    此函数在主循环中调用,用于轮询处理Modbus事件
 */
BOOL xMBPortEventGet(eMBEventType *eEvent)
{
    BOOL xEventHappened = FALSE;

    if (xEventInQueue)
    {
        *eEvent = eQueuedEvent;
        xEventInQueue = FALSE;
        xEventHappened = TRUE;
    }
    return xEventHappened;
}

/* ----------------------- 保持寄存器回调函数 ---------------------------------*/

/**
 * @brief   保持寄存器访问回调函数
 * @param   pucRegBuffer - 数据缓冲区指针
 *          - 读操作: 输出参数,填充要返回的数据
 *          - 写操作: 输入参数,包含要写入的数据
 * @param   usAddress    - 寄存器起始地址(Modbus协议地址,从1开始)
 * @param   usNRegs      - 寄存器数量
 * @param   eMode        - 操作模式: MB_REG_READ(读取) 或 MB_REG_WRITE(写入)
 * @return  MB_ENOERR - 操作成功
 *          MB_ENOREG - 寄存器地址超出范围
 * @note    支持的Modbus功能码:
 *          - 03: 读保持寄存器
 *          - 06: 写单个保持寄存器
 *          - 16: 写多个保持寄存器
 * @par     特殊寄存器功能映射:
 *          - REG_LED0:     控制LED0 (PB5,低电平点亮)
 *          - REG_LED1:     控制LED1 (PE5,低电平点亮)
 *          - REG_BEEP:     控制蜂鸣器 (PB8,高电平鸣叫)
 *          - REG_SLAVE_ADDR: 从站地址 (写入后自动重启Modbus协议栈)
 */
eMBErrorCode eMBRegHoldingCB(UCHAR *pucRegBuffer, USHORT usAddress,
                              USHORT usNRegs, eMBRegisterMode eMode)
{
    eMBErrorCode eStatus = MB_ENOERR;
    USHORT iRegIndex;

    /* 地址转换: FreeModbus传入的usAddress从1开始,转换为从0开始便于数组索引 */
    if (usAddress >= 1)
    {
        usAddress--;
    }
    else
    {
        return MB_ENOREG;    /* 无效地址: Modbus地址最小为1 */
    }

    /* 地址边界检查: 确保访问范围不超出保持寄存器缓冲区(0-255) */
    if ((usAddress + usNRegs) <= HOLDING_REG_SIZE)
    {
        if (eMode == MB_REG_WRITE)  /* 写操作: 处理功能码06(写单个)和16(写多个) */
        {
            for (iRegIndex = 0; iRegIndex < usNRegs; iRegIndex++)
            {
                /* Modbus协议采用大端格式: 高字节在前,低字节在后 */
                USHORT usRegValue = (pucRegBuffer[iRegIndex * 2] << 8) |
                                     pucRegBuffer[iRegIndex * 2 + 1];

                /* 写入保持寄存器缓冲区 */
                usRegHoldingBuf[usAddress + iRegIndex] = usRegValue;

                /* 特殊寄存器映射 - 控制硬件外设 */
                if ((usAddress + iRegIndex) == REG_LED0)
                {
                    /* LED0控制: 最低位为1时关闭(高电平),为0时点亮(低电平) */
                    HAL_GPIO_WritePin(GPIOB, GPIO_PIN_5,
                                      (usRegValue & 0x01) ? GPIO_PIN_RESET : GPIO_PIN_SET);
                }
                else if ((usAddress + iRegIndex) == REG_LED1)
                {
                    /* LED1控制: 最低位为1时关闭(高电平),为0时点亮(低电平) */
                    HAL_GPIO_WritePin(GPIOE, GPIO_PIN_5,
                                      (usRegValue & 0x01) ? GPIO_PIN_RESET : GPIO_PIN_SET);
                }
                else if ((usAddress + iRegIndex) == REG_BEEP)
                {
                    /* 蜂鸣器控制: 最低位为1时鸣叫(高电平有效),为0时关闭 */
                    HAL_GPIO_WritePin(GPIOB, GPIO_PIN_8,
                                      (usRegValue & 0x01) ? GPIO_PIN_SET : GPIO_PIN_RESET);
                }
                else if ((usAddress + iRegIndex) == REG_SLAVE_ADDR)
                {
                    /* 从站地址修改: 支持地址范围1-247 (Modbus标准有效地址) */
                    if (usRegValue >= 1 && usRegValue <= 247)
                    {
                        g_addr = usRegValue;

                        /* 保存地址到EEPROM,确保掉电不丢失 */
                        at24cxx_write(0, &g_addr, 1);

                        /* 重启Modbus协议栈以应用新地址 */
                        eMBDisable();                       /* 禁用当前协议栈 */
                        eMBInit(MB_RTU, g_addr, 0, 115200, MB_PAR_NONE);  /* 重新初始化 */
                        eMBEnable();                        /* 使能新协议栈 */
                    }
                }
            }
        }
        else  /* 读操作: 处理功能码03(读保持寄存器) */
        {
            for (iRegIndex = 0; iRegIndex < usNRegs; iRegIndex++)
            {
                USHORT usRegValue = usRegHoldingBuf[usAddress + iRegIndex];

                /* 以大端格式填充响应缓冲区: 高字节在前,低字节在后 */
                pucRegBuffer[iRegIndex * 2] = (UCHAR)(usRegValue >> 8);      /* 高字节 */
                pucRegBuffer[iRegIndex * 2 + 1] = (UCHAR)(usRegValue & 0xFF); /* 低字节 */
            }
        }
    }
    else
    {
        eStatus = MB_ENOREG;  /* 地址越界: 访问了不存在的寄存器区域 */
    }

    return eStatus;
}

/* ----------------------- 输入寄存器回调函数 ---------------------------------*/

/**
 * @brief   输入寄存器读回调函数
 * @param   pucRegBuffer - 输出参数,返回要填充的数据缓冲区
 * @param   usAddress    - 寄存器起始地址(Modbus协议地址,从1开始)
 * @param   usNRegs      - 寄存器数量
 * @return  MB_ENOERR - 操作成功
 *          MB_ENOREG - 寄存器地址超出范围
 * @note    支持的Modbus功能码: 04(读输入寄存器)
 * @warning 当前为示例实现,全部返回0,实际应用需根据硬件修改
 *          典型应用: ADC采集值、传感器数据、设备状态等只读数据
 */
eMBErrorCode eMBRegInputCB(UCHAR *pucRegBuffer, USHORT usAddress, USHORT usNRegs)
{
    eMBErrorCode eStatus = MB_ENOERR;

    /* 地址转换: Modbus地址从1开始 -> 数组索引从0开始 */
    if (usAddress >= 1)
    {
        usAddress--;
    }
    else
    {
        return MB_ENOREG;
    }

    /* 地址边界检查: 输入寄存器范围 0x0000-0x000F (共16个寄存器) */
    if ((usAddress + usNRegs) <= DISCRETE_SIZE)
    {
        for (USHORT i = 0; i < usNRegs; i++)
        {
            /* TODO: 替换为实际数据源,例如:
             * - HAL_ADC_GetValue(&hadc)
             * - 传感器采集的温湿度值
             * - 设备运行状态 */
            pucRegBuffer[i * 2] = 0;     /* 高字节 */
            pucRegBuffer[i * 2 + 1] = 0; /* 低字节 */
        }
    }
    else
    {
        eStatus = MB_ENOREG;  /* 地址越界 */
    }

    return eStatus;
}

/* ----------------------- 线圈回调函数 --------------------------------------*/

/**
 * @brief   线圈访问回调函数
 * @param   pucRegBuffer - 数据缓冲区指针
 *          - 读操作: 输出参数,填充要返回的线圈状态
 *          - 写操作: 输入参数,包含要设置的线圈状态
 * @param   usAddress    - 线圈起始地址(Modbus协议地址,从1开始)
 * @param   usNCoils     - 线圈数量
 * @param   eMode        - 操作模式: MB_REG_READ(读取) 或 MB_REG_WRITE(写入)
 * @return  MB_ENOERR - 操作成功
 *          MB_ENOREG - 地址超出范围
 * @note    支持的Modbus功能码:
 *          - 01: 读线圈
 *          - 05: 写单个线圈
 *          - 15: 写多个线圈
 * @attention 线圈采用位压缩存储,每个字节存储8个线圈状态以提高内存效率
 */
eMBErrorCode eMBRegCoilsCB(UCHAR *pucRegBuffer, USHORT usAddress,
                            USHORT usNCoils, eMBRegisterMode eMode)
{
    eMBErrorCode eStatus = MB_ENOERR;

    /* 地址转换: Modbus地址从1开始 -> 数组索引从0开始 */
    if (usAddress >= 1)
    {
        usAddress--;
    }
    else
    {
        return MB_ENOREG;
    }

    /* 地址边界检查: 线圈范围 0x0000-0x000F (共16个线圈) */
    if ((usAddress + usNCoils) <= DISCRETE_SIZE)
    {
        if (eMode == MB_REG_WRITE)  /* 写操作: 处理功能码05(写单个)和15(写多个) */
        {
            USHORT iBit = 0;
            while (iBit < usNCoils)
            {
                /* 每次处理最多8个线圈(1字节),提高处理效率 */
                UCHAR ucNBits = (UCHAR)((usNCoils - iBit) > 8 ? 8 : (usNCoils - iBit));

                /* 将缓冲区的位数据写入线圈缓冲区 */
                xMBUtilSetBits(ucRegCoilsBuf, (USHORT)(usAddress + iBit),
                               ucNBits, pucRegBuffer[iBit / 8]);

                iBit = (USHORT)(iBit + ucNBits);
            }
        }
        else  /* 读操作: 处理功能码01(读线圈) */
        {
            USHORT iBit = 0;
            while (iBit < usNCoils)
            {
                /* 每次处理最多8个线圈(1字节) */
                UCHAR ucNBits = (UCHAR)((usNCoils - iBit) > 8 ? 8 : (usNCoils - iBit));

                /* 从线圈缓冲区读取位数据并填充到响应缓冲区 */
                pucRegBuffer[iBit / 8] = xMBUtilGetBits(ucRegCoilsBuf,
                                                        (USHORT)(usAddress + iBit), ucNBits);
                iBit = (USHORT)(iBit + ucNBits);
            }
        }
    }
    else
    {
        eStatus = MB_ENOREG;  /* 地址越界 */
    }

    return eStatus;
}

/* ----------------------- 离散输入回调函数 -----------------------------------*/

/**
 * @brief   离散输入读回调函数
 * @param   pucRegBuffer - 输出参数,返回要填充的离散输入状态
 * @param   usAddress    - 离散输入起始地址(Modbus协议地址,从1开始)
 * @param   usNDiscrete  - 离散输入数量
 * @return  MB_ENOERR - 操作成功
 *          MB_ENOREG - 地址超出范围
 * @note    支持的Modbus功能码: 02(读离散输入)
 * @warning 当前为示例实现,全部返回0,实际应用需根据硬件修改
 *          典型应用: 限位开关状态、按钮状态、传感器触发信号等
 */
eMBErrorCode eMBRegDiscreteCB(UCHAR *pucRegBuffer, USHORT usAddress, USHORT usNDiscrete)
{
    eMBErrorCode eStatus = MB_ENOERR;

    /* 地址转换: Modbus地址从1开始 -> 数组索引从0开始 */
    if (usAddress >= 1)
    {
        usAddress--;
    }
    else
    {
        return MB_ENOREG;
    }

    /* 地址边界检查: 离散输入范围 0x0000-0x000F (共16个离散输入) */
    if ((usAddress + usNDiscrete) <= DISCRETE_SIZE)
    {
        USHORT iBit = 0;
        while (iBit < usNDiscrete)
        {
            /* 每次处理最多8个离散输入(1字节) */
            UCHAR ucNBits = (UCHAR)((usNDiscrete - iBit) > 8 ? 8 : (usNDiscrete - iBit));

            /* TODO: 替换为实际数据源,例如:
             * - HAL_GPIO_ReadPin(限位开关)
             * - 按钮状态
             * - 传感器触发信号
             * 
             * 当前示例从ucRegDiscreteBuf读取(该缓冲区当前全为0) */
            pucRegBuffer[iBit / 8] = xMBUtilGetBits(ucRegDiscreteBuf,
                                                    (USHORT)(usAddress + iBit), ucNBits);
            iBit = (USHORT)(iBit + ucNBits);
        }
    }
    else
    {
        eStatus = MB_ENOREG;  /* 地址越界 */
    }

    return eStatus;
}

四、调试要点(避坑指南)

在多协议融合开发中容易遇到通信失败、数据异常等问题,这里总结几个关键调试点:

  1. 外设接线:确保IIC引脚(SDA、SCL)接对,ESP8266的TX/RX与STM32的USART3 RX/TX交叉连接(ESP_TX → STM32 RX),CAN收发器接线正确(CAN_H、CAN_L);
  1. 中断优先级:UART、CAN中断优先级需高于FreeRTOS的任务优先级,避免中断被任务阻塞,导致数据丢失;
  1. ESP8266调试:可通过USART1串口打印ESP8266的响应信息,排查WiFi连接、MQTT连接失败的问题(如密码错误、服务器地址错误);
  1. 传感器校准:ADC采集需进行校准,INA226需根据实际电路配置分流电阻参数(代码中默认1000Ω,可修改);
  1. 任务栈大小:如果任务执行过程中出现死机,可能是栈大小不足,可适当增大对应任务的栈大小(如MQTT任务栈改为128*5)。

Logo

AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。

更多推荐