简介MODBUS RTU 是一种广泛应用于工业自动化的串行通信协议。本技术贴基于 APM32F407 开发板,通过 UART 外设实现了 MODBUS RTU 的从设备功能,包括数据收发、帧解析、CRC 校验,以及功能码的处理,支持读取线圈、写单个线圈和诊断命令等功能。
1. MODBUS RTU 数据帧结构
MODBUS RTU 通信帧由以下部分组成:
地址域 | 1 字节 | 从设备地址,范围为 1-247,地址 0 表示广播。 |
功能码 | 1 字节 | 定义从设备的具体操作,例如读写寄存器、读写线圈等。 |
数据域 | N 字节 | 根据功能码定义,包含具体命令参数或返回数据。 |
CRC 校验 | 2 字节 | 使用 CRC16 校验数据完整性,生成多项式为 0xA001。 |
帧示例
以下为读取线圈状态的请求帧示例:
地址 (0x01) | 功能码 (0x01) | 起始地址高 (0x00) | 起始地址低 (0x00) | 线圈数量高 (0x00) | 线圈数量低 (0x08) | CRC 高 | CRC 低
2. 实现功能概述
本次实现的功能包括:
- UART 初始化:支持 MODBUS RTU 的波特率、校验位和停止位配置。
- 数据收发:使用中断方式高效接收数据,支持完整的数据发送和接收。
- CRC 校验:验证通信数据完整性,避免数据传输错误。
- 帧解析与功能码处理:
- 读取线圈状态(功能码 0x01)。
- 写单个线圈(功能码 0x05)。
- 诊断命令(功能码 0x08)。
3. 代码实现
3.1 串口初始化
在 bsp_usart.c 文件中添加串口初始化函数,配置 UART 满足 MODBUS 通信需求。
#include "bsp_usart.h"
#include "apm32f4xx_usart.h"
#include "apm32f4xx_gpio.h"
#include "apm32f4xx_rcm.h"
void USART_MODBUS_Init(void)
{
// 启用 USART 和 GPIO 时钟
RCM_EnableAPB2PeriphClock(RCM_APB2_PERIPH_USART1);
RCM_EnableAHB1PeriphClock(RCM_AHB1_PERIPH_GPIOA);
// 配置 GPIO PA9 (TX) 和 PA10 (RX)
GPIO_Config_T gpioConfig;
gpioConfig.pin = GPIO_PIN_9 | GPIO_PIN_10;
gpioConfig.mode = GPIO_MODE_AF;
gpioConfig.outtype = GPIO_OUT_TYPE_PP;
gpioConfig.pupd = GPIO_PUPD_NOPULL;
gpioConfig.speed = GPIO_SPEED_50MHz;
GPIO_Config(GPIOA, &gpioConfig);
GPIO_ConfigPinAF(GPIOA, GPIO_PIN_SOURCE_9, GPIO_AF_USART1);
GPIO_ConfigPinAF(GPIOA, GPIO_PIN_SOURCE_10, GPIO_AF_USART1);
// 配置 USART
USART_Config_T usartConfig;
usartConfig.baudRate = 9600; // 波特率
usartConfig.wordLength = USART_WORD_LEN_8B; // 数据长度
usartConfig.stopBits = USART_STOP_BIT_1; // 停止位
usartConfig.parity = USART_PARITY_EVEN; // 校验位
usartConfig.mode = USART_MODE_TX_RX; // 收发模式
USART_Config(USART1, &usartConfig);
// 启用 USART
USART_Enable(USART1);
}
3.2 数据收发
接收缓冲与中断
通过中断方式接收数据,缓冲区存储接收到的帧数据。
#define MODBUS_BUFFER_SIZE 256
static uint8_t modbusRxBuffer[MODBUS_BUFFER_SIZE];
static uint16_t modbusRxIndex = 0;
void USART1_IRQHandler(void)
{
if (USART_ReadStatusFlag(USART1, USART_FLAG_RXNE))
{
// 接收数据存入缓冲区
modbusRxBuffer[modbusRxIndex++] = USART_RxData(USART1);
if (modbusRxIndex >= MODBUS_BUFFER_SIZE)
{
modbusRxIndex = 0; // 防止溢出
}
}
}
数据发送
实现完整数据帧发送功能。
void USART_MODBUS_Send(uint8_t *data, uint16_t length)
{
for (uint16_t i = 0; i < length; i++)
{
USART_TxData(USART1, data); // 发送数据
while (!USART_ReadStatusFlag(USART1, USART_FLAG_TXE)); // 等待发送完成
}
}
3.3 CRC 校验
MODBUS RTU 使用 CRC16 校验确保数据完整性。
CRC 校验代码
uint16_t MODBUS_CRC16(uint8_t *data, uint16_t length)
{
uint16_t crc = 0xFFFF;
for (uint16_t i = 0; i < length; i++)
{
crc ^= data;
for (uint8_t j = 0; j < 8; j++)
{
if (crc & 0x0001)
crc = (crc >> 1) ^ 0xA001;
else
crc >>= 1;
}
}
return crc;
}
3.4 帧解析与功能码处理
帧解析函数
解析接收到的 MODBUS 数据帧,根据功能码调用相应的处理逻辑。
void MODBUS_ProcessFrame(uint8_t *frame, uint16_t length)
{
if (length < 4) return; // 最小帧长度:地址 + 功能码 + CRC
uint16_t crcReceived = (frame[length - 2] | (frame[length - 1] << 8));
uint16_t crcCalculated = MODBUS_CRC16(frame, length - 2);
if (crcReceived != crcCalculated)
{
printf("CRC Error\n");
return;
}
uint8_t functionCode = frame[1];
switch (functionCode)
{
case 0x01: // 读取线圈状态
MODBUS_ReadCoils(frame, length);
break;
case 0x05: // 写单个线圈
MODBUS_WriteSingleCoil(frame, length);
break;
case 0x08: // 诊断命令
MODBUS_Diagnostics(frame, length);
break;
default:
printf("Unsupported Function Code\n");
break;
}
}
读取线圈状态
#define MAX_COILS 128
static uint8_t coilStatus[MAX_COILS] = {0}; // 模拟线圈状态数组
void MODBUS_ReadCoils(uint8_t *frame, uint16_t length)
{
uint16_t startAddress = (frame[2] << 8) | frame[3];
uint16_t quantity = (frame[4] << 8) | frame[5];
uint8_t byteCount = (quantity + 7) / 8;
uint8_t response[256];
response[0] = frame[0];
response[1] = 0x01;
response[2] = byteCount;
for (uint16_t i = 0; i < quantity; i++)
{
uint16_t byteIndex = i / 8;
uint16_t bitIndex = i % 8;
if (coilStatus[startAddress + i])
{
response[3 + byteIndex] |= (1 << bitIndex);
}
else
{
response[3 + byteIndex] &= ~(1 << bitIndex);
}
}
uint16_t crc = MODBUS_CRC16(response, 3 + byteCount);
response[3 + byteCount] = crc & 0xFF;
response[4 + byteCount] = (crc >> 8) & 0xFF;
USART_MODBUS_Send(response, 5 + byteCount);
}
写单个线圈
void MODBUS_WriteSingleCoil(uint8_t *frame, uint16_t length)
{
uint16_t address = (frame[2] << 8) | frame[3];
uint16_t value = (frame[4] << 8) | frame[5];
if (value == 0xFF00)
{
coilStatus[address] = 1;
}
else if (value == 0x0000)
{
coilStatus[address] = 0;
}
USART_MODBUS_Send(frame, length); // 回显请求帧
}
诊断命令
void MODBUS_Diagnostics(uint8_t *frame, uint16_t length)
{
USART_MODBUS_Send(frame, length); // 回显请求帧
}
4. 主程序整合
int main(void)
{
USART_MODBUS_Init();
while (1)
{
if (modbusRxIndex > 0)
{
MODBUS_ProcessFrame(modbusRxBuffer, modbusRxIndex);
modbusRxIndex = 0;
}
}
}
5. 技术总结
支持功能- 读取线圈状态(功能码 0x01)
- 写单个线圈(功能码 0x05)
- 诊断命令(功能码 0x08)
代码亮点
- 实现了完整的 MODBUS 数据帧收发和 CRC 校验。
- 支持多种功能码,扩展性强。
- 中断机制提高了通信效率。
通过本贴的完整实现,可以构建一个标准的 MODBUS 从设备。如果需要扩展其他功能码,框架可以快速适配。
|