一、功能说明

本文记录一个基于 USB + RS485 的通信测试程序。

系统由两部分组成:

  1. 工控板

    • 通过 USB 与上位机串口助手通信;

    • 通过 RS485 与编码器通信。

  2. 编码器

    • 通过 RS485 接收工控板发送的数据;

    • 收到指定命令后返回应答数据。

本次测试的通信逻辑如下:

方向 数据 说明
串口助手 → 工控板 0x1A USB 下发测试命令
工控板 → 编码器 0x1A 工控板通过 RS485 转发命令
编码器 → 工控板 0x55 编码器收到 0x1A 后返回应答
工控板 → 串口助手 0x55 工控板收到编码器应答后通过 USB 返回

也就是说,如果 RS485 通信正常,串口助手向工控板发送 0x1A 后,最终可以收到工控板返回的 0x55


二、整体通信流程

上位机 / 串口助手
        |
        | USB 发送 0x1A
        v
工控板
        |
        | RS485 发送 0x1A
        v
编码器
        |
        | RS485 回复 0x55
        v
工控板
        |
        | USB 返回 0x55
        v
上位机 / 串口助手

三、工控板程序

1. MyProcess.h

该文件主要定义测试命令、编码器应答值、USB 收发缓冲区以及相关函数声明。

#ifndef _MYPROCESS_H
#define _MYPROCESS_H

#include "stdint.h"
#include <string.h>

#define USB_RS485_TEST_CMD      0x1A
#define ENCODER_REPLY_OK        0x55

extern uint8_t process_usb_rxBuf[64];
extern uint8_t process_usb_txBuf[64];

extern volatile uint8_t g_WaitEncoderReply55;
extern volatile uint8_t g_UsbReply55Pending;

void Process_usb_reply(void);
void Encoder_485_SendByte(uint8_t data);

#endif

2. MyProcess.c

该文件实现 USB 命令处理以及 RS485 单字节发送。

当 USB 接收到 0x1A 后,工控板会:

  1. 设置等待编码器回复标志;

  2. 通过 RS485 向编码器发送 0x1A

  3. 清空 USB 接收缓冲区首字节,避免重复处理。

#include "MyProcess.h"
#include "usbd_cdc_if.h"
#include "usart.h"
#include "gpio.h"

uint8_t process_usb_rxBuf[64];
uint8_t process_usb_txBuf[64];

volatile uint8_t g_WaitEncoderReply55 = 0;
volatile uint8_t g_UsbReply55Pending = 0;

void Encoder_485_SendByte(uint8_t data)
{
    HAL_GPIO_WritePin(UART4_RD_GPIO_Port, UART4_RD_Pin, GPIO_PIN_SET); 
    // RS485 发送使能

    __HAL_UART_CLEAR_FLAG(&huart4, UART_FLAG_TC);

    huart4.Instance->DR = data;

    while (__HAL_UART_GET_FLAG(&huart4, UART_FLAG_TXE) == RESET) {}
    while (__HAL_UART_GET_FLAG(&huart4, UART_FLAG_TC) == RESET) {}

    HAL_GPIO_WritePin(UART4_RD_GPIO_Port, UART4_RD_Pin, GPIO_PIN_RESET); 
    // RS485 发送完成,切回接收模式
}

void Process_usb_reply(void)
{
    if (process_usb_rxBuf[0] == USB_RS485_TEST_CMD)
    {
        g_WaitEncoderReply55 = 1;

        Encoder_485_SendByte(USB_RS485_TEST_CMD);

        process_usb_rxBuf[0] = 0;
    }
}

3. USB 接收回调:usbd_cdc_if.c

USB 接收数据后,将数据复制到 process_usb_rxBuf,然后调用 Process_usb_reply() 进行处理。

static int8_t CDC_Receive_FS(uint8_t* Buf, uint32_t *Len)
{
    if (*Len >= 1)
    {
        memset(process_usb_rxBuf, 0, sizeof(process_usb_rxBuf));

        if (*Len > sizeof(process_usb_rxBuf))
        {
            *Len = sizeof(process_usb_rxBuf);
        }

        memcpy(process_usb_rxBuf, Buf, *Len);

        Process_usb_reply();
    }

    USBD_CDC_SetRxBuffer(&hUsbDeviceFS, &Buf[0]);
    USBD_CDC_ReceivePacket(&hUsbDeviceFS);

    return USBD_OK;
}

4. UART4 接收中断:stm32f4xx_it.c

工控板通过 UART4 接收编码器返回的数据。

如果当前正在等待编码器回复,并且收到的数据为 0x55,则设置 USB 回复标志位。

void UART4_IRQHandler(void)
{
    uint8_t rxData = 0;

    if ((UART4->SR & UART_FLAG_RXNE) != RESET)
    {
        rxData = UART4->DR;

        if (g_WaitEncoderReply55 == 1)
        {
            if (rxData == ENCODER_REPLY_OK)
            {
                g_WaitEncoderReply55 = 0;
                g_UsbReply55Pending = 1; 
                // 主循环通过 USB 回复 0x55
            }
        }
    }
}

5. main.c

主循环中不断检测 g_UsbReply55Pending

当该标志被置位后,说明工控板已经成功收到编码器返回的 0x55,于是通过 USB 将 0x55 回复给串口助手。

#include "main.h"
#include "usart.h"
#include "usb_device.h"
#include "gpio.h"
#include "MyProcess.h"

void SystemClock_Config(void);
static void NVIC_Configuration(void);

int main(void)
{
    HAL_Init();
    SystemClock_Config();

    MX_GPIO_Init();
    MX_UART4_Init();
    MX_USB_DEVICE_Init();

    __HAL_UART_ENABLE_IT(&huart4, UART_IT_RXNE); 
    // 使能 UART 接收中断

    NVIC_Configuration();

    while (1)
    {
        if (g_UsbReply55Pending)
        {
            g_UsbReply55Pending = 0;

            process_usb_txBuf[0] = ENCODER_REPLY_OK;
            CDC_Transmit_FS(process_usb_txBuf, 1);
        }
    }
}

四、编码器程序

1. main.h

该文件定义 RS485 收发控制宏、测试命令、回复数据、DMA 通道以及相关函数声明。

#ifndef __MAIN_H__
#define __MAIN_H__

#ifdef __cplusplus
extern "C" {
#endif

#include "n32l40x.h"
#include "n32l40x_gpio.h"
#include "n32l40x_dma.h"
#include <stdint.h>
#include <stdio.h>

#define RS485_ENABLE_TX()   GPIO_SetBits(GPIOB, GPIO_PIN_6)
#define RS485_ENABLE_RX()   GPIO_ResetBits(GPIOB, GPIO_PIN_6)

#define USB_RS485_TEST_CMD      0x1A
#define USB_RS485_TEST_REPLY    0x55

#define UART5_TX_DMA_CH        DMA_CH3
#define UART5_TX_DMA_FLAG      DMA_FLAG_TC3
#define UART5_TX_DMA_REMAP     DMA_REMAP_UART5_TX
#define UART5_TX_DMA_IRQn      DMA_Channel3_IRQn

#define UART5_TX_DMA_CLEAR_FLAGS \
        (DMA_FLAG_GL3 | DMA_FLAG_TC3 | DMA_FLAG_HT3 | DMA_FLAG_TE3)

extern volatile uint8_t rs485_dma_tx_busy;

void Delay(uint32_t count);

void RCC_Configuration(void);
void GPIO_Configuration(void);
void USART_Configuration(void);
void NVIC_Configuration(void);
void DMA_Configuration(void);

void RS485_SendBuffer_DMA(uint8_t *buf, uint16_t len);

#ifdef __cplusplus
}
#endif

#endif

2. 编码器 main.c

编码器初始化 GPIO、UART5、DMA 和中断。

RS485 默认处于接收模式。

#include "main.h"
#include "n32l40x_gpio.h"
#include "n32l40x_usart.h"
#include "n32l40x_rcc.h"
#include "misc.h"

volatile uint8_t rs485_dma_tx_busy = 0;

void Delay(uint32_t count)
{
    while (count--) __NOP();
}

void RCC_Configuration(void)
{
    RCC_EnableAPB2PeriphClk(RCC_APB2_PERIPH_AFIO, ENABLE);
    RCC_EnableAPB2PeriphClk(RCC_APB2_PERIPH_GPIOB, ENABLE);
    RCC_EnableAPB2PeriphClk(RCC_APB2_PERIPH_UART5, ENABLE);
    RCC_EnableAHBPeriphClk(RCC_AHB_PERIPH_DMA, ENABLE);
}

void GPIO_Configuration(void)
{
    GPIO_InitType GPIO_InitStructure;

    GPIO_InitStruct(&GPIO_InitStructure);
    GPIO_InitStructure.Pin            = GPIO_PIN_6;
    GPIO_InitStructure.GPIO_Mode      = GPIO_Mode_Out_PP;
    GPIO_InitStructure.GPIO_Current   = GPIO_DC_12mA;
    GPIO_InitStructure.GPIO_Slew_Rate = GPIO_Slew_Rate_High;
    GPIO_InitPeripheral(GPIOB, &GPIO_InitStructure);

    GPIO_InitStruct(&GPIO_InitStructure);
    GPIO_InitStructure.Pin            = GPIO_PIN_4;
    GPIO_InitStructure.GPIO_Mode      = GPIO_Mode_AF_PP;
    GPIO_InitStructure.GPIO_Alternate = GPIO_AF6_UART5;
    GPIO_InitStructure.GPIO_Current   = GPIO_DC_12mA;
    GPIO_InitStructure.GPIO_Slew_Rate = GPIO_Slew_Rate_High;
    GPIO_InitPeripheral(GPIOB, &GPIO_InitStructure);

    GPIO_InitStruct(&GPIO_InitStructure);
    GPIO_InitStructure.Pin            = GPIO_PIN_5;
    GPIO_InitStructure.GPIO_Mode      = GPIO_Mode_AF_PP;
    GPIO_InitStructure.GPIO_Alternate = GPIO_AF7_UART5;
    GPIO_InitStructure.GPIO_Current   = GPIO_DC_12mA;
    GPIO_InitStructure.GPIO_Slew_Rate = GPIO_Slew_Rate_High;
    GPIO_InitPeripheral(GPIOB, &GPIO_InitStructure);

    RS485_ENABLE_RX();
}

void USART_Configuration(void)
{
    USART_InitType USART_InitStructure;
    USART_StructInit(&USART_InitStructure);

    USART_InitStructure.BaudRate            = 2500000;
    USART_InitStructure.WordLength          = USART_WL_8B;
    USART_InitStructure.StopBits            = USART_STPB_1;
    USART_InitStructure.Parity              = USART_PE_NO;
    USART_InitStructure.Mode                = USART_MODE_RX | USART_MODE_TX;
    USART_InitStructure.HardwareFlowControl = USART_HFCTRL_NONE;

    USART_Init(UART5, &USART_InitStructure);
    USART_ConfigInt(UART5, USART_INT_RXDNE, ENABLE);
    USART_Enable(UART5, ENABLE);
}

void DMA_Configuration(void)
{
    DMA_InitType DMA_InitStructure;

    DMA_DeInit(UART5_TX_DMA_CH);
    DMA_StructInit(&DMA_InitStructure);

    DMA_InitStructure.PeriphAddr     = (uint32_t)(UART5_BASE + 0x04);
    DMA_InitStructure.MemAddr        = (uint32_t)0;
    DMA_InitStructure.Direction      = DMA_DIR_PERIPH_DST;
    DMA_InitStructure.BufSize        = 0;
    DMA_InitStructure.PeriphInc      = DMA_PERIPH_INC_DISABLE;
    DMA_InitStructure.DMA_MemoryInc  = DMA_MEM_INC_ENABLE;
    DMA_InitStructure.PeriphDataSize = DMA_PERIPH_DATA_SIZE_BYTE;
    DMA_InitStructure.MemDataSize    = DMA_MemoryDataSize_Byte;
    DMA_InitStructure.CircularMode   = DMA_MODE_NORMAL;
    DMA_InitStructure.Priority       = DMA_PRIORITY_VERY_HIGH;
    DMA_InitStructure.Mem2Mem        = DMA_M2M_DISABLE;

    DMA_Init(UART5_TX_DMA_CH, &DMA_InitStructure);
    DMA_RequestRemap(UART5_TX_DMA_REMAP, DMA, UART5_TX_DMA_CH, ENABLE);

    DMA_ConfigInt(UART5_TX_DMA_CH, DMA_INT_TXC, ENABLE);
    DMA_ConfigInt(UART5_TX_DMA_CH, DMA_INT_ERR, ENABLE);

    USART_EnableDMA(UART5, USART_DMAREQ_TX, ENABLE);
}

void NVIC_Configuration(void)
{
    NVIC_InitType NVIC_InitStructure;

    NVIC_PriorityGroupConfig(NVIC_PriorityGroup_4);

    NVIC_InitStructure.NVIC_IRQChannel                   = UART5_IRQn;
    NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 0;
    NVIC_InitStructure.NVIC_IRQChannelSubPriority        = 0;
    NVIC_InitStructure.NVIC_IRQChannelCmd                = ENABLE;
    NVIC_Init(&NVIC_InitStructure);

    NVIC_InitStructure.NVIC_IRQChannel                   = UART5_TX_DMA_IRQn;
    NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 1;
    NVIC_InitStructure.NVIC_IRQChannelSubPriority        = 0;
    NVIC_InitStructure.NVIC_IRQChannelCmd                = ENABLE;
    NVIC_Init(&NVIC_InitStructure);
}

int main(void)
{
    RCC_Configuration();
    GPIO_Configuration();
    USART_Configuration();
    DMA_Configuration();
    NVIC_Configuration();

    RS485_ENABLE_RX();

    while (1)
    {
    }
}

3. UART5 接收中断:n32l40x_it.c

编码器通过 UART5 接收 RS485 数据。

当接收到 0x1A 后,立即切换 RS485 为发送模式,发送 0x55,等待发送完成后再切换回接收模式。

#include "main.h"
#include "n32l40x_it.h"
#include "n32l40x_usart.h"
#include "n32l40x_dma.h"

void UART5_IRQHandler(void)
{
    uint32_t sts = UART5->STS;

    // 接收中断处理
    if ((sts & USART_FLAG_RXDNE) != 0)
    {
        uint8_t cmd = (uint8_t)(UART5->DAT);

        // 如果 DMA 正在发送,则忽略本次接收
        if (rs485_dma_tx_busy)
        {
            return;
        }

        // 收到 0x1A 后回复 0x55
        if (cmd == USB_RS485_TEST_CMD)
        {
            RS485_ENABLE_TX();

            USART_ClrFlag(UART5, USART_FLAG_TXC);

            USART_SendData(UART5, USB_RS485_TEST_REPLY);

            while (USART_GetFlagStatus(UART5, USART_FLAG_TXC) == RESET)
            {
            }

            RS485_ENABLE_RX();

            return;
        }
    }

    // 发送完成中断 TXC
    if (rs485_dma_tx_busy && (sts & USART_FLAG_TXC) != 0)
    {
        USART_ConfigInt(UART5, USART_INT_TXC, DISABLE);

        RS485_ENABLE_RX();

        rs485_dma_tx_busy = 0;

        USART_ConfigInt(UART5, USART_INT_RXDNE, ENABLE);
        USART_ClrFlag(UART5, USART_FLAG_TXC);

        return;
    }

    // 溢出处理
    if ((sts & USART_FLAG_OREF) != 0)
    {
        (void)UART5->STS;
        (void)UART5->DAT;
    }
}

4. DMA 通道 3 中断

当前测试中,编码器接收到 0x1A 后是直接通过 UART 发送 0x55,但程序中保留了 DMA 发送完成中断处理,方便后续扩展多字节数据发送。

void DMA_Channel3_IRQHandler(void)
{
    if (DMA_GetIntStatus(DMA_INT_TXC3, DMA) != RESET)
    {
        DMA_ClrIntPendingBit(DMA_INT_TXC3 | DMA_INT_GLB3, DMA);

        while (!(UART5->STS & USART_FLAG_TXC))
        {
        }

        DMA_EnableChannel(UART5_TX_DMA_CH, DISABLE);
        USART_EnableDMA(UART5, USART_DMAREQ_TX, DISABLE);

        RS485_ENABLE_RX();

        rs485_dma_tx_busy = 0;

        USART_ConfigInt(UART5, USART_INT_RXDNE, ENABLE);

        return;
    }

    if (DMA_GetIntStatus(DMA_INT_ERR3, DMA) != RESET)
    {
        DMA_ClrIntPendingBit(DMA_INT_ERR3 | DMA_INT_GLB3, DMA);

        DMA_EnableChannel(UART5_TX_DMA_CH, DISABLE);
        USART_EnableDMA(UART5, USART_DMAREQ_TX, DISABLE);

        RS485_ENABLE_RX();

        rs485_dma_tx_busy = 0;

        USART_ConfigInt(UART5, USART_INT_RXDNE, ENABLE);
    }
}

五、关键标志位说明

1. 工控板侧

变量 作用
g_WaitEncoderReply55 工控板是否正在等待编码器回复 0x55
g_UsbReply55Pending 是否需要通过 USB 回复上位机 0x55
process_usb_rxBuf USB 接收缓冲区
process_usb_txBuf USB 发送缓冲区

2. 编码器侧

变量 / 宏 作用
rs485_dma_tx_busy DMA 是否正在发送
RS485_ENABLE_TX() RS485 切换到发送模式
RS485_ENABLE_RX() RS485 切换到接收模式
USB_RS485_TEST_CMD 测试命令 0x1A
USB_RS485_TEST_REPLY 测试回复 0x55

六、测试方法

1. 硬件连接

工控板和编码器之间通过 RS485 连接。

需要保证:

  1. A、B 线连接正确;

  2. 双方共地;

  3. RS485 收发控制引脚工作正常;

  4. 双方串口参数一致。


2. 串口参数

编码器侧 UART5 当前配置如下:

USART_InitStructure.BaudRate   = 2500000;
USART_InitStructure.WordLength = USART_WL_8B;
USART_InitStructure.StopBits   = USART_STPB_1;
USART_InitStructure.Parity     = USART_PE_NO;

因此工控板 UART4 也需要配置为:

波特率:2500000
数据位:8
停止位:1
校验位:None

3. 上位机测试

在串口助手中通过 USB 虚拟串口发送:

1A

如果通信正常,串口助手应收到:

55

七、注意事项

1. RS485 方向控制必须正确

RS485 是半双工通信,发送和接收需要通过方向控制引脚切换。

工控板发送前:

HAL_GPIO_WritePin(UART4_RD_GPIO_Port, UART4_RD_Pin, GPIO_PIN_SET);

工控板发送完成后:

HAL_GPIO_WritePin(UART4_RD_GPIO_Port, UART4_RD_Pin, GPIO_PIN_RESET);

编码器发送前:

RS485_ENABLE_TX();

编码器发送完成后:

RS485_ENABLE_RX();

如果方向切换不正确,可能出现以下问题:

  • 编码器收不到 0x1A

  • 工控板收不到 0x55

  • RS485 总线被一直占用;

  • 串口助手没有任何返回。


2. 双方波特率必须一致

编码器程序中 UART5 波特率为:

2500000

工控板 UART4 的波特率也必须设置为相同值,否则会导致接收数据错误。


3. USB 不建议在中断中直接发送

工控板收到编码器回复后,并没有在 UART4 中断中直接调用 CDC_Transmit_FS(),而是设置标志位:

g_UsbReply55Pending = 1;

然后在主循环中发送 USB 数据:

CDC_Transmit_FS(process_usb_txBuf, 1);

这样做可以避免在 UART 中断中直接操作 USB 带来的时序或阻塞问题。


4. 单字节通信适合测试链路

本程序只测试单字节命令和单字节应答,适合验证:

  • USB 通信是否正常;

  • 工控板 RS485 发送是否正常;

  • 编码器 RS485 接收是否正常;

  • 编码器 RS485 回复是否正常;

  • 工控板 USB 回传是否正常。

后续可扩展多个通信方式,如IIC、SPI、CAN等:

八、总结

本文实现了一个简单的 USB 转 RS485 通信测试流程。

整体逻辑为:

USB 收到 0x1A
      ↓
工控板通过 RS485 发送 0x1A
      ↓
编码器收到 0x1A 后回复 0x55
      ↓
工控板收到 0x55
      ↓
工控板通过 USB 返回 0x55

通过该程序,可以快速判断工控板与编码器之间的 RS485 通信是否正常。

如果串口助手发送 0x1A 后能够收到 0x55,说明 USB 通信、RS485 通信以及编码器应答逻辑均正常。

Logo

智能硬件社区聚焦AI智能硬件技术生态,汇聚嵌入式AI、物联网硬件开发者,打造交流分享平台,同步全国赛事资讯、开展 OPC 核心人才招募,助力技术落地与开发者成长。

更多推荐