STM32F407单片机上开发的Modbus RTU 双主站源程序 1. 两个串口同时作为Modbus RTU主站,可同时读取两组Modbus RTU从站数据 1. 基于STM32F407ZET6开发板,采用USART1和USART2作为Modbus RTU通信串口 2. USART1口测试连接几个Modbus RTU从站,可以正常读取从站的数据 3. USART2口测试连接几个Modbus RTU从站,可以正常读取从站的数据 4. 基于正点原子的STM32F407开发板测试正常,其他测试板请自行调试 5. 仅提供源代码,测试说明文件,不提供硬件电路板等

来整点硬核的实战分享。最近在STM32F407上搞了个双主站Modbus RTU项目,两个串口同时当主站干活,实测能稳定读写两组从站设备。先上开发环境:正点原子F407ZET6开发板,CubeMX生成工程框架,HAL库加持。这里直接上干货,说说实现的关键点。

STM32F407单片机上开发的Modbus RTU 双主站源程序 1. 两个串口同时作为Modbus RTU主站,可同时读取两组Modbus RTU从站数据 1. 基于STM32F407ZET6开发板,采用USART1和USART2作为Modbus RTU通信串口 2. USART1口测试连接几个Modbus RTU从站,可以正常读取从站的数据 3. USART2口测试连接几个Modbus RTU从站,可以正常读取从站的数据 4. 基于正点原子的STM32F407开发板测试正常,其他测试板请自行调试 5. 仅提供源代码,测试说明文件,不提供硬件电路板等

先看硬件配置部分,USART1和USART2都用上了。硬件流控制没开,毕竟大部分RTU设备不带这个。GPIO配置注意复用功能:

// USART1配置:PA9-TX  PA10-RX
GPIO_InitStruct.Pin = GPIO_PIN_9|GPIO_PIN_10;
GPIO_InitStruct.Mode = GPIO_MODE_AF_PP;
GPIO_InitStruct.Pull = GPIO_PULLUP;
GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_HIGH;
GPIO_InitStruct.Alternate = GPIO_AF7_USART1;

// USART2同理配置PA2-TX PA3-RX

双主站核心在于状态机切换,这里用两个结构体分别管理两个通道:

typedef struct {
    uint8_t txBuffer[256];
    uint8_t rxBuffer[256];
    uint16_t timeout;
    MODBUS_STATE state;
} ModbusMaster;

ModbusMaster master1, master2;  // 两个主站实例

重点来了——定时器中断处理超时。开个基本定时器,1ms中断一次,处理两个通道的超时计数:

void TIM2_IRQHandler(void) {
    if (__HAL_TIM_GET_FLAG(&htim2, TIM_FLAG_UPDATE)) {
        // 主站1超时处理
        if(master1.timeout > 0 && (--master1.timeout == 0)) {
            handle_timeout(&master1);
        }
        // 主站2同理
        if(master2.timeout > 0 && (--master2.timeout == 0)) {
            handle_timeout(&master2);
        }
        __HAL_TIM_CLEAR_FLAG(&htim2, TIM_FLAG_UPDATE);
    }
}

发送请求函数需要注意切换收发状态。以读取保持寄存器为例:

void modbus_read_holding(ModbusMaster *master, UART_HandleTypeDef *huart, 
                        uint8_t slaveID, uint16_t regAddr, uint16_t regNum) {
    // 构造Modbus帧
    master->txBuffer[0] = slaveID;
    master->txBuffer[1] = 0x03;  // 功能码
    master->txBuffer[2] = regAddr >> 8;
    master->txBuffer[3] = regAddr & 0xFF;
    ... // 填充数据
    
    // 启动发送
    HAL_UART_Transmit_IT(huart, master->txBuffer, 8); 
    master->state = WAIT_RESPONSE;
    master->timeout = 1000;  // 设置1秒超时
}

接收处理用DMA+空闲中断组合拳,这个套路实测能有效处理不定长数据。两个串口各自配置DMA:

// 启动接收
HAL_UARTEx_ReceiveToIdle_DMA(huart, rxBuf, BUF_SIZE);
__HAL_DMA_DISABLE_IT(huart->hdmarx, DMA_IT_HT); // 关闭半传输中断

最后在main循环里搞个非阻塞调度,两个主站交替干活:

while(1) {
    // 主站1状态机
    switch(master1.state) {
        case IDLE:
            modbus_read_holding(&master1, &huart1, 0x01, 0x0000, 2);
            break;
        case WAIT_RESPONSE:
            // 由中断处理
            break;
        // ...其他状态
    }
    
    // 主站2同理,可执行不同操作
    switch(master2.state) {
        case IDLE:
            modbus_write_coil(&master2, &huart2, 0x02, 0x0001, 1);
            break;
        // ... 
    }
    
    HAL_Delay(50); // 适当延时防止CPU跑飞
}

实测中发现几个坑点:1. 两个串口的DMA通道别冲突;2. 超时时间要根据实际从站响应调整;3. 485方向控制引脚切换要留够时间余量。代码里用宏定义控制收发切换:

#define RS485_DIR_TX()  HAL_GPIO_WritePin(GPIOE, GPIO_PIN_3, GPIO_PIN_SET)
#define RS485_DIR_RX()  HAL_GPIO_WritePin(GPIOE, GPIO_PIN_3, GPIO_PIN_RESET)

// 发送前切TX,发送完成中断切回RX
void HAL_UART_TxCpltCallback(UART_HandleTypeDef *huart) {
    if(huart == &huart1) RS485_DIR_RX();
    // ...
}

最后说下测试情况:USART1接温湿度传感器,USART2接继电器模块,同时跑读取和写入操作,20小时压力测试无丢包。代码里留了调试接口,把printf重定向到串口3,方便实时看状态:

// 重定向printf
int __io_putchar(int ch) {
    HAL_UART_Transmit(&huart3, (uint8_t*)&ch, 1, 10);
    return ch;
}

源码已打包,注意不同开发板需调整引脚配置。遇到从站响应慢的情况,适当调大超时阈值和帧间隔时间(3.5字符时间用定时器精确实现)。双主站同时操作时,建议错开请求发送时间避免总线冲突。

Logo

智能硬件社区聚焦AI智能硬件技术生态,汇聚嵌入式AI、物联网硬件开发者,打造交流分享平台,同步全国赛事资讯、开展 OPC 核心人才招募,助力技术落地与开发者成长。

更多推荐