打印
[应用方案]

基于APM32F407的MODBUS RTU 从设备实现

[复制链接]
17|0
手机看帖
扫描二维码
随时随地手机跟帖
跳转到指定楼层
楼主
a976209770|  楼主 | 2024-11-28 16:54 | 只看该作者 回帖奖励 |倒序浏览 |阅读模式
简介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 从设备。如果需要扩展其他功能码,框架可以快速适配。

使用特权

评论回复
发新帖 我要提问
您需要登录后才可以回帖 登录 | 注册

本版积分规则

23

主题

26

帖子

0

粉丝