STM32 中实现 Modbus RTU
在 STM32 中实现 Modbus RTU 通信时,数据接收和处理是由底层驱动和协议栈共同完成的。让我详细解释这个过程:
一、通信实现的核心机制
STM32 接收 RS485 数据的过程涉及三个关键组件:
- 硬件层:RS485 转换器将差分信号转换为 UART 的 TTL 电平
- UART 驱动:STM32 的 UART 外设接收数据并触发中断
- Modbus 协议栈:解析接收到的数据并执行相应操作
二、数据接收的具体流程
1. 初始化阶段
// 主函数中的初始化代码
HAL_Init();
SystemClock_Config();
MX_GPIO_Init();
MX_USART1_UART_Init(); // 配置UART1为9600-8N1
MX_ADC1_Init();
/* 初始化Modbus RTU从机 */
eMBInit(MB_RTU, 0x01, 0, 9600, MB_PAR_NONE);
eMBEnable();
- MX_USART1_UART_Init() 配置 UART1 的波特率、数据位、停止位等参数
- eMBInit() 初始化 Modbus 协议栈,设置从机地址、通信参数
2. 数据接收机制
当 RS485 总线上有数据传输时:
- UART 接收数据:RS485 转换器将差分信号转换为 TTL 电平,STM32 的 UART 接收数据
- 触发接收中断:每个字节接收完成后触发 UART_RX 中断
- 数据存入缓冲区:中断服务程序将接收到的字节存入 Modbus 协议栈的接收缓冲区
在 FreeModbus 库中,这通常由portserial.c文件中的中断服务程序实现:
// 典型的UART接收中断服务程序
void USARTx_IRQHandler(void)
{
if(USART_GetITStatus(USARTx, USART_IT_RXNE) != RESET)
{
/* 读取接收到的字节 */
ucByte = (unsigned char)USART_ReceiveData(USARTx);
/* 将字节传递给Modbus协议栈 */
if(xMBPortSerialPutByte((CHAR)ucByte) != TRUE)
{
/* 处理缓冲区溢出 */
}
/* 清除中断标志 */
USART_ClearITPendingBit(USARTx, USART_IT_RXNE);
}
}
3. Modbus 帧检测
Modbus RTU 协议规定:
- 两个连续字节之间的空闲时间超过 3.5 个字符时间,表示一帧结束
- 例如,在 9600 波特率下,3.5 个字符时间约为 3.65ms
协议栈通过定时器检测这个空闲时间:
// 伪代码:检测帧结束
if (idle_time > 3.5 * char_time) {
frame_complete = true;
process_modbus_frame();
}
4. 协议解析与处理
在主循环中,eMBPoll()函数不断检查是否有完整的 Modbus 帧:
while (1) {
/* 采集和更新数据 */
HAL_ADC_Start(&hadc1);
uint16_t adc_value = HAL_ADC_GetValue(&hadc1);
// ... 更新寄存器 ...
/* 处理Modbus请求 */
eMBPoll();
HAL_Delay(10);
}
eMBPoll()的核心逻辑:
eMBErrorCode eMBPoll( void )
{
eMBErrorCode eStatus = MB_ENOERR;
eMBEventType eEvent;
/* 检查是否接收到完整帧 */
if( xMBPortEventGet( &eEvent ) == TRUE )
{
switch ( eEvent )
{
case EV_FRAME_RECEIVED:
/* 处理接收到的请求 */
eStatus = eMBProcessRxFrame();
break;
case EV_EXECUTE_COMPLETE:
/* 发送响应 */
eStatus = eMBSendTxFrame();
break;
default:
break;
}
}
return eStatus;
}
5. 回调函数处理请求
当协议栈解析出有效的 Modbus 请求后,会调用相应的回调函数:
/* Modbus回调函数 - 处理读输入寄存器请求 */
eMBErrorCode eMBRegInputCB( UCHAR * pucRegBuffer, USHORT usAddress, USHORT usNRegs ) {
// 检查地址范围
if ((usAddress >= REG_INPUT_START) && (usAddress + usNRegs <= REG_INPUT_START + REG_INPUT_NREGS)) {
// 将寄存器数据复制到响应缓冲区
iRegIndex = (int)(usAddress - REG_INPUT_START);
while (usNRegs > 0) {
*pucRegBuffer++ = (UCHAR)(usRegInputBuf[iRegIndex] >> 8);
*pucRegBuffer++ = (UCHAR)(usRegInputBuf[iRegIndex] & 0xFF);
iRegIndex++;
usNRegs--;
}
return MB_ENOERR;
} else {
return MB_ENOREG; // 无效地址
}
}
三、RS485 方向控制的实现
在你提供的代码中,RS485 方向控制是隐式实现的。在 FreeModbus 库中,通常通过以下方式控制:
// 在portserial.c中实现
static void vMBPortSerialEnable( BOOL xRxEnable, BOOL xTxEnable )
{
if( xRxEnable )
{
/* 使能接收,关闭发送 */
HAL_GPIO_WritePin(DE_RE_GPIO_Port, DE_RE_Pin, GPIO_PIN_RESET);
}
else if( xTxEnable )
{
/* 使能发送,关闭接收 */
HAL_GPIO_WritePin(DE_RE_GPIO_Port, DE_RE_Pin, GPIO_PIN_SET);
}
}
这个函数会在以下时机被调用:
- 接收到完整帧后,切换到接收模式
- 准备发送响应前,切换到发送模式
- 发送完成后,切换回接收模式
四、完整的通信流程示意图
上位机发送请求 STM32响应
┌─────────────────┐ ┌─────────────────┐
│ 发送Modbus请求 │───RS485───► │ UART接收数据 │
│ (功能码04) │ │ 存入接收缓冲区 │
└─────────────────┘ └─────────┬───────┘
│
▼
┌─────────────────┐
│ 检测帧结束 │
│ (3.5字符空闲) │
└─────────────────┘
│
▼
┌─────────────────┐
│ 解析Modbus帧 │
│ (校验、地址、 │
│ 功能码检查) │
└─────────────────┘
│
▼
┌─────────────────┐
│ 调用回调函数 │
│ (读取寄存器数据) │
└─────────────────┘
│
▼
┌─────────────────┐
│ 构建响应帧 │
│ (CRC校验) │
└─────────────────┘
│
▼
┌─────────────────┐
│ 切换到发送模式 │
│ (DE/RE=1) │
└─────────────────┘
│
▼
┌─────────────────┐
│ 通过UART发送 │
│ 响应数据 │
└─────────────────┘
│
▼
┌─────────────────┐
│ 切换回接收模式 │
│ (DE/RE=0) │
└─────────────────┘
五、关键总结
- 数据接收:通过 UART 中断自动接收,存入协议栈缓冲区
- 帧检测:通过空闲时间检测 Modbus 帧边界
- 协议解析:检查从机地址、功能码、CRC 校验
- 回调处理:根据请求类型调用相应回调函数
- 响应发送:构建响应帧,控制 RS485 方向,发送数据
在你的代码中,这些细节都由 FreeModbus 库处理,你只需要关注数据采集和寄存器回调函数的实现。