STM32实现MODEBUS RTU从机与上位机通信
测试板卡: 正点原子MINISTM32(STM32F103RB)
实现思路: 位机向STM32发送连续数据,STM32串口中断一直接收,期间使用定时器控制接收时间,如果在3.5个时间字符时间内没有接收到任何数据,那么定时器就判定为一帧数据接收完毕,然后根据modbus协议处理接收到的数据就可以了。
MODBUS RTU 方式的收发都需要3.5个字符的等待时间,这个时间可以通过定时器控制,两个字符之间的间隔时间按照9600的波特率算,1s可以发9600/8=1200字节,1个字节发送的时间就是1/1200≈833微秒,3.5个字符时间就是1/1200*3.5≈2917微秒,这里我为了方便直接用了5ms,如果你测试不行的话,调小定时器的超时时间。
代码下载地址:https://download.csdn.net/download/qq153471503/12036032
贴一下modbus的命令码表:
贴一下主要的代码片段:串口接收中断回调函数:
- 串口1为调试串口
- 串口3作为modbus的收发数据端口
void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart)
{
if (huart->Instance == DEBUG_UART_HANDLER.Instance)
{
if ((DEBUG_UART_RX_STA & 0x8000) == 0) // 接收未完毕
{
if (DEBUG_UART_RX_STA & 0x4000) // 已经接收到了回车符
{
if (DEBUG_UART_RX_BYTE == '\n') DEBUG_UART_RX_STA |= 0x8000; // 本次接收到的是换行符,标记接收到了换行符
else DEBUG_UART_RX_STA = 0; // 本次接收到的不是换行符,视为接收错误,状态置为初始
}
else
{
if (DEBUG_UART_RX_BYTE == '\r') DEBUG_UART_RX_STA |= 0x4000; // 本次接收到的是回车符,标记接收到了回车符
else
{
DEBUG_UART_RX_BUF[DEBUG_UART_RX_STA & 0x3FFF] = DEBUG_UART_RX_BYTE; // 将本次接收到的数据保存到接收缓存中
if (DEBUG_UART_RX_STA++ >= DEBUG_UART_RX_LEN) DEBUG_UART_RX_STA = 0; // 连续接收到的数据长度高于最大接收长度,则视为接收错误
}
}
}
HAL_UART_Receive_IT(huart, &DEBUG_UART_RX_BYTE, 1); // 再次使能接收中断
}
else if (huart->Instance == huart3.Instance)
{
if (MODBUS_RX_STA < MODBUS_RX_LEN)
{
__HAL_TIM_SET_COUNTER(&htim4, 0); // 清除5ms定时器的计数值
if (MODBUS_RX_STA == 0) TIM4_Set(TIM4_ENABLE); // 如果接收为初始状态,则启动定时器
MODBUS_RX_BUF[MODBUS_RX_STA++] = MODBUS_RX_BYTE; // 接收数据
}
else
{
MODBUS_RX_STA |= 0x8000; // 超长,标记接收结束
}
HAL_UART_Receive_IT(huart, &MODBUS_RX_BYTE, 1); // 再次使能接收中断
}
}
MODBUS RTU数据解析,我只实现了功能码3,其他功能码请根据协议自行进行补充:
// 保持寄存器
#define REG_HOLD_SIZE 10UL
uint16_t REG_HOLD[REG_HOLD_SIZE] = {0, 1, 2, 3, 4, 5, 6, 7, 8, 9};
// 互换两个变量的值
void swap(unsigned char* byte1, unsigned char* byte2)
{
*byte1 ^= *byte2;
*byte2 ^= *byte1;
*byte1 ^= *byte2;
}
inline void modbus_slave_write(const void* buf, uint32_t len)
{
HAL_UART_Transmit(&huart3, (uint8_t *)buf, len, 1000);
}
/**
* 串口三数据接收处理
*/
void MODBUS_RecvHandler(void)
{
// 无数据收到
if ((MODBUS_RX_STA & 0x8000) == 0)
return;
// 收到的数据有误
if (MODBUS_RX_BUF[0] != SLAVE_ADDRESS)
goto __exit;
// 选择相应功能码
switch (MODBUS_RX_BUF[1])
{
case CMD3:
{
int i = 0;
int nread = 0;
int offset = 0;
int num = 0;
// CRC校验
if (usMBCRC16(MODBUS_RX_BUF, 6) != *((uint16_t *)(MODBUS_RX_BUF + 6)))
goto __exit;
// 大端转小端
swap(MODBUS_RX_BUF + 2, MODBUS_RX_BUF + 3);
swap(MODBUS_RX_BUF + 4, MODBUS_RX_BUF + 5);
// 得到保持寄存器偏移地址
offset = *((uint16_t *)(MODBUS_RX_BUF + 2));
// 偏移地址错误
if (offset >= REG_HOLD_SIZE) goto __exit;
// 得到要读取的保持寄存器个数
num = *((uint16_t *)(MODBUS_RX_BUF + 4));
// 计算实际可读的保持寄存器个数
nread = REG_HOLD_SIZE - offset >= num ? num : REG_HOLD_SIZE - offset;
// 实际能够被读取的保持寄存器数量错误
if (nread <= 0) goto __exit;
// 填充响应数据的字节长度
MODBUS_RX_BUF[2] = nread * sizeof(uint16_t);
// 填充保持寄存器的值
for (i = 0; i != nread; i++)
{
*((uint16_t *)(MODBUS_RX_BUF + 3 + i * 2)) = swap_uint16(REG_HOLD[offset + i]);
}
// 计算CRC
*((uint16_t *)(MODBUS_RX_BUF + 3 + nread * 2)) = usMBCRC16(MODBUS_RX_BUF, 3 + nread * 2);
// 发送给主机
modbus_slave_write(MODBUS_RX_BUF, 3 + nread * 2 + 2);
// 等待3.5个字符的时间
HAL_Delay(5);
// 改变保持寄存器的值
for (i = 0; i != REG_HOLD_SIZE; i++)
{
REG_HOLD[i]++;
}
// 退出
goto __exit;
}
default:
goto __exit;
}
__exit:
MODBUS_RX_STA = 0; // 清空接收
return;
}
在主函数中轮训调用modbus的接收处理函数:
单片机串口3我设置为9600的波特率,无流控、无奇偶校验、8bit数据、1bit停止位,上位机打开modbus poll软件,串口设置与单片机相同,Steup->Read/Write Definition,然后从机地址为1,功能码为3,偏移地址0,读取10个保持寄存器的值:
然后收到单片机的modbus rtu的回复:
ends。。。