STM32F407单片机上实现的Modbus RTU双主站源程序
最近在STM32F407上搞了个双主站Modbus RTU项目,两个串口同时当主站干活,实测能稳定读写两组从站设备。先看硬件配置部分,USART1和USART2都用上了。2. USART1口测试连接几个Modbus RTU从站,可以正常读取从站的数据。3. USART2口测试连接几个Modbus RTU从站,可以正常读取从站的数据。2. USART1口测试连接几个Modbus RTU从站,可以正常
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字符时间用定时器精确实现)。双主站同时操作时,建议错开请求发送时间避免总线冲突。


更多推荐



所有评论(0)