基于USART+自定义协议+DMA+STM32F407VE+CubeMX+FreeRTOS

1 CubeMX配置设置

1.1 RCC 时钟配置

在这里插入图片描述
在这里插入图片描述

1.2 SYS 配置

在这里插入图片描述
用到了FreeRTOS,采用定时器的方式。

1.3 USART1 配置

在这里插入图片描述
添加DMA;
在这里插入图片描述
在这里插入图片描述开启中断;
在这里插入图片描述

1.4 FREERTOS 配置

在这里插入图片描述
Configuration全部默认;

1.5 GPIO 配置

串口使用RS485的通讯方式:PA9(USART1_TX)、PA10(USART_RX)、PC8(RS485_EN)
在这里插入图片描述
在这里插入图片描述
配置完成生成代码;

2 串口代码 UART + DMA + IDLE 模式

前置代码说明,下面内容会涉及:

#define MODBUS_RX_MAX_LEN 32

typedef struct 
{
    //作为从机时使用
    uint8_t  myadd;                            //本设备从机地址
    uint8_t  rcbuf[MODBUS_RX_MAX_LEN];         //modbus接受缓冲区
    uint8_t  recount;                          //modbus端口接收到的数据个数
    uint8_t  reflag;                           //modbus一帧数据接受完成标志位
    uint8_t  sendbuf[MODBUS_RX_MAX_LEN];       //modbus接发送缓冲区
}MODBUS;

协议由MODBUS的基础上进行的自定义更改,命名保留了modbus,可以更改其它命名;

2.1 usart.h

#define RS485DIR_TX     HAL_GPIO_WritePin(RS485_EN_GPIO_Port, RS485_EN_Pin, GPIO_PIN_SET);      
#define RS485DIR_RX     HAL_GPIO_WritePin(RS485_EN_GPIO_Port, RS485_EN_Pin, GPIO_PIN_RESET);  

2.2 usart.c

MX_USART1_UART_Init中只需要添加两行代码

__HAL_UART_ENABLE_IT(&huart1, UART_IT_IDLE);  // 开启IDLE中断
HAL_UART_Receive_DMA(&huart1, modbus.rcbuf, MODBUS_RX_MAX_LEN); // 启动DMA接收

作用解释:
__HAL_UART_ENABLE_IT(&huart1, UART_IT_IDLE)
启用 UART 空闲中断(IDLE),当串口在一段时间内未接收到新数据时会触发此中断,常用于判断一帧数据是否接收完毕。
HAL_UART_Receive_DMA
启动 DMA 接收机制,将接收到的数据直接写入 modbus.rcbuf 缓冲区,提高数据传输效率,降低 CPU 占用率。

2.3 stm32f4xx_it.c

在串口1的中断里添加以下代码
在这里插入图片描述
代码块

void USART1_IRQHandler(void)
{
  /* USER CODE BEGIN USART1_IRQn 0 */

  /* USER CODE END USART1_IRQn 0 */
  HAL_UART_IRQHandler(&huart1);
  /* USER CODE BEGIN USART1_IRQn 1 */

    if(__HAL_UART_GET_FLAG(&huart1, UART_FLAG_IDLE))// 检测是否是 IDLE 中断
    {
        __HAL_UART_CLEAR_IDLEFLAG(&huart1);// 清除 IDLE 中断标志

        HAL_UART_DMAStop(&huart1);// 停止当前 DMA 接收
        
        // 计算接收到的数据长度 = 总长度 - 剩余未传输的长度
        uint16_t len = MODBUS_RX_MAX_LEN - __HAL_DMA_GET_COUNTER(huart1.hdmarx);
        modbus.recount = len;   // 保存接收长度
        modbus.reflag = 1;      // 设置“接收完成”标志
        
        // 重新启动 DMA 接收,准备接收下一帧
        HAL_UART_Receive_DMA(&huart1, modbus.rcbuf, MODBUS_RX_MAX_LEN);
    }
  /* USER CODE END USART1_IRQn 1 */
}

作用解释:
USART1_IRQHandler 中我们处理 UART 的空闲中断,用于识别一帧 Modbus 数据的接收完成。此方式无需在每接收一个字节时触发中断,结合 DMA 可以大幅降低 CPU 占用率,适合实时性要求高的场合。

为什么采用 UART + DMA + IDLE 模式
在初期的串口通信实现中,我们曾使用传统的串口中断方式逐字节接收数据(即每接收一个字节触发一次中断)。然而,在实际运行中发现:

  • 当主站发送的数据量较大、发送频率较高时,
  • 串口接收中断频繁触发,导致 MCU 负担加重,
  • 在某些情况下甚至会出现程序跑飞或串口彻底失效的问题(不再进入中断,数据丢失)。

为了解决这一问题,我们引入了 DMA(直接内存访问)机制 来接管接收过程,配合 UART 的 空闲中断(IDLE) 来判断一帧数据的结束,从而大大提高了通信的稳定性和效率。

此种方式既避免了频繁中断的性能瓶颈,也简化了数据帧的接收判断逻辑,适合用于 Modbus 等典型的一帧一处理通信模式。

3 基于MODBUS自定义协议代码

3.1 rs485.h

为实现 基于MODBUS自定义协议RS-485 总线下的从机通信,本模块定义了两个核心结构体:HOLDREG 和 MODBUS,用于数据映射与串口接收处理。

#define MODBUS_RX_MAX_LEN 32

定义最大接收缓冲长度为 32 字节,适用于典型的 Modbus 帧长度(可根据协议指令与寄存器数量调整)。

typedef struct
{
    uint16_t HoldRegAddr;       //地址    
    uint16_t HoldRegData;       //数据
    uint16_t safeLevel;         //读写权限
}HOLDREG;

HoldRegAddr:保持寄存器的地址,对应主站读写的目标地址;
HoldRegData:该寄存器当前值;
safeLevel:权限控制字段,可自定义标记为只读(RO)、可读写(RW)等安全等级。

typedef struct 
{
    //作为从机时使用
    uint8_t  myadd;                            //本设备从机地址
    uint8_t  rcbuf[MODBUS_RX_MAX_LEN];         //modbus接受缓冲区
    uint8_t  recount;                          //modbus端口接收到的数据个数
    uint8_t  reflag;                           //modbus一帧数据接受完成标志位
    uint8_t  sendbuf[MODBUS_RX_MAX_LEN];       //modbus接发送缓冲区
}MODBUS;

extern MODBUS modbus;
extern HOLDREG Reg[];

myadd:本设备的从机地址(Modbus RTU 协议中 1~247);
rcbuf[]:DMA 接收缓存区;
recount:记录实际接收的数据长度;
reflag:当接收到完整的一帧数据后置 1,主循环/任务中处理;
sendbuf[]:准备发送的应答数据缓存。

3.2 rs485.c

本项目基于标准 Modbus RTU 协议进行改造,实现了从机设备在 RS-485 总线下的数据接收、校验、写入和回复等功能。

全局变量定义

MODBUS modbus;                      // 接收处理核心结构体
uint8_t modbus_slave_addr = 0x40;   // 本设备的从机地址
uint8_t modbus_Tx_buff[32];         // 数据回复缓冲区

寄存器映射区初始化

HOLDREG Reg[]= {{0x0000, 0x00, 0},                    
                {0x0001, 0x00, 0},                    
                {0x0002, 0x00, 0},                    
                {0x0003, 0x00, 0},                    
                {0x0004, 0x00, 0},                    
                {0x0005, 0x00, 0},                    
                {0x0006, 0x00, 0},                    
                {0x0007, 0x00, 0},                    
                {0x0008, 0x00, 0},
                {0x0009, 0x00, 0},
                {0x000A, 0x00, 0},
                {0x000B, 0x00, 0}
};

系统预定义了一组保持寄存器(HOLDREG),每个寄存器包含地址、值、权限字段。主站通过功能码可以写入这些寄存器值。

从机发送函数

void modbus_send_data(uint8_t *buff, uint8_t len)
{
    RS485DIR_TX;
    HAL_Delay(1);
    HAL_UART_Transmit_DMA(&huart1, buff, len);
    HAL_Delay(1);
    RS485DIR_RX;
}

RS485DIR_TX/RS485DIR_RX:控制 485 总线方向,GPIO:PC8
使用 DMA 非阻塞发送数据,提高性能;
发完数据后及时切换为接收模式。

modbus_service:帧接收 + 校验 + 功能码解析

void modbus_service(void)
{
    if(modbus.reflag == 0) return;

    CRC_check_result = CRC16_XMODEM_T(&modbus.rcbuf[0], modbus.recount-3);
    data_CRC_value = ((modbus.rcbuf[modbus.recount-3]<<8)) | modbus.rcbuf[modbus.recount-2];
    frame_end = modbus.rcbuf[modbus.rcbuf[2] + 5];

    if (CRC_check_result == data_CRC_value && frame_end == 0xEE)
    {
        if (modbus.rcbuf[1] == modbus_slave_addr)
        {
            modbus_16_function(); // 功能码 16:写多个寄存器
        }
    }

    modbus.recount = 0;
    modbus.reflag = 0;
}

数据格式

字节 含义
0 帧头
1 从机地址
2 数据长度
3~n 有效数据
n+1 CRC 高位
n+2 CRC 低位
n+3 帧尾 0xEE

功能码16:写多个保持寄存器

void modbus_16_function(void)
{
    uint16_t register_num = modbus.rcbuf[2];

    for(uint16_t i = 0; i < register_num; i++)
    {
        Reg[i].HoldRegData = modbus.rcbuf[3+i];  // 写入数据
    }

    // 回复帧
    modbus_Tx_buff[0] = modbus.rcbuf[0];  // 帧头
    modbus_Tx_buff[1] = modbus.rcbuf[1];  // 从机地址
    modbus_Tx_buff[2] = modbus.rcbuf[2];  // 长度

    uint16_t crc = CRC16_XMODEM_T(modbus_Tx_buff, 3);
    modbus_Tx_buff[3] = (crc >> 8) & 0xFF;
    modbus_Tx_buff[4] = crc & 0xFF;
    modbus_Tx_buff[5] = modbus.rcbuf[register_num + 3 + 2]; // 原始帧尾

    modbus_send_data(modbus_Tx_buff, 6);
}

注意 有效数据是8位一个字节的数据,要想16位2个字节,可按需扩展rcbuf[3] << 8 | rcbuf[4]

CRC16_XMODEM //CRC校验方式

4 FreeRTOS代码

由于使用的是CubeMX生成,用起来不是很方便,所以只是生成部分代码内容,接下来要进行修改自动生成的初始代码;

4.1 freertos.c

/* USER CODE BEGIN Header */
/**
  ******************************************************************************
  * File Name          : freertos.c
  * Description        : Code for freertos applications
  ******************************************************************************
  * @attention
  *
  * Copyright (c) 2025 STMicroelectronics.
  * All rights reserved.
  *
  * This software is licensed under terms that can be found in the LICENSE file
  * in the root directory of this software component.
  * If no LICENSE file comes with this software, it is provided AS-IS.
  *
  ******************************************************************************
  */
/* USER CODE END Header */

/* Includes ------------------------------------------------------------------*/
#include "FreeRTOS.h"
#include "task.h"
#include "main.h"
#include "cmsis_os.h"

/* Private includes ----------------------------------------------------------*/
/* USER CODE BEGIN Includes */
#include "rs485.h"
/* USER CODE END Includes */

/* Private typedef -----------------------------------------------------------*/
/* USER CODE BEGIN PTD */
/* START_TASK 任务 配置
 * 包括: 任务句柄 任务优先级 堆栈大小 创建任务
 */
#define START_TASK_PRIO         1           /* 任务优先级 */
#define START_TASK_STACK_SIZE   128         /* 任务堆栈大小 */
TaskHandle_t    start_task_handler;         /* 任务句柄 */
void start_task( void * pvParameters );     /* 任务函数 */

/* TASK1 任务 用来通信
 * 包括: 任务句柄 任务优先级 堆栈大小 创建任务
 */
#define TASK1_PRIO         2                /* 任务优先级 */
#define TASK1_STACK_SIZE   128              /* 任务堆栈大小 */
TaskHandle_t    task1_handler;              /* 任务句柄 */
void task1( void * pvParameters );          /* 任务函数 */

/* TASK2 任务 LED_RUN
 * 包括: 任务句柄 任务优先级 堆栈大小 创建任务
 */
#define TASK2_PRIO         3                /* 任务优先级 */
#define TASK2_STACK_SIZE   128              /* 任务堆栈大小 */
TaskHandle_t    task2_handler;              /* 任务句柄 */
void task2( void * pvParameters );          /* 任务函数 */

/* USER CODE END PTD */

/* Private define ------------------------------------------------------------*/
/* USER CODE BEGIN PD */

/* USER CODE END PD */

/* Private macro -------------------------------------------------------------*/
/* USER CODE BEGIN PM */

/* USER CODE END PM */

/* Private variables ---------------------------------------------------------*/
/* USER CODE BEGIN Variables */
#if 0
/* USER CODE END Variables */
osThreadId defaultTaskHandle;

/* Private function prototypes -----------------------------------------------*/
/* USER CODE BEGIN FunctionPrototypes */
// 在 void StartDefaultTask(void const * argument); 下一行添加 #endif
/* USER CODE END FunctionPrototypes */

void StartDefaultTask(void const * argument);
#endif
void MX_FREERTOS_Init(void); /* (MISRA C 2004 rule 8.1) */

/* GetIdleTaskMemory prototype (linked to static allocation support) */
void vApplicationGetIdleTaskMemory( StaticTask_t **ppxIdleTaskTCBBuffer, StackType_t **ppxIdleTaskStackBuffer, uint32_t *pulIdleTaskStackSize );

/* USER CODE BEGIN GET_IDLE_TASK_MEMORY */
static StaticTask_t xIdleTaskTCBBuffer;
static StackType_t xIdleStack[configMINIMAL_STACK_SIZE];

void vApplicationGetIdleTaskMemory( StaticTask_t **ppxIdleTaskTCBBuffer, StackType_t **ppxIdleTaskStackBuffer, uint32_t *pulIdleTaskStackSize )
{
  *ppxIdleTaskTCBBuffer = &xIdleTaskTCBBuffer;
  *ppxIdleTaskStackBuffer = &xIdleStack[0];
  *pulIdleTaskStackSize = configMINIMAL_STACK_SIZE;
  /* place for user code */
}
/* USER CODE END GET_IDLE_TASK_MEMORY */

/**
  * @brief  FreeRTOS initialization
  * @param  None
  * @retval None
  */
void MX_FREERTOS_Init(void) {
  /* USER CODE BEGIN Init */

  /* USER CODE END Init */

  /* USER CODE BEGIN RTOS_MUTEX */
  /* add mutexes, ... */
  /* USER CODE END RTOS_MUTEX */

  /* USER CODE BEGIN RTOS_SEMAPHORES */
  /* add semaphores, ... */
  /* USER CODE END RTOS_SEMAPHORES */

  /* USER CODE BEGIN RTOS_TIMERS */
  /* start timers, add new ones, ... */
  /* USER CODE END RTOS_TIMERS */

  /* USER CODE BEGIN RTOS_QUEUES */
  /* add queues, ... */
#if 0
  /* USER CODE END RTOS_QUEUES */

  /* Create the thread(s) */
  /* definition and creation of defaultTask */
  osThreadDef(defaultTask, StartDefaultTask, osPriorityNormal, 0, 128);
  defaultTaskHandle = osThreadCreate(osThread(defaultTask), NULL);

  /* USER CODE BEGIN RTOS_THREADS */
  /* add threads, ... */
#endif
    xTaskCreate((TaskFunction_t )   start_task,
        (char *                 )   "start_task",
        (configSTACK_DEPTH_TYPE )   START_TASK_STACK_SIZE,
        (void *                 )   NULL,
        (UBaseType_t            )   START_TASK_PRIO,
        (TaskHandle_t *         )   &start_task_handler );
  /* USER CODE END RTOS_THREADS */

}

/* USER CODE BEGIN Header_StartDefaultTask */
#if 0
/**
  * @brief  Function implementing the defaultTask thread.
  * @param  argument: Not used
  * @retval None
  */
/* USER CODE END Header_StartDefaultTask */
void StartDefaultTask(void const * argument)
{
  /* USER CODE BEGIN StartDefaultTask */
  /* Infinite loop */
  for(;;)
  {
    osDelay(1);
  }
  /* USER CODE END StartDefaultTask */
}

/* Private application code --------------------------------------------------*/
/* USER CODE BEGIN Application */
#endif
void start_task( void * pvParameters )
{
    taskENTER_CRITICAL();               /* 进入临界区 */

    xTaskCreate((TaskFunction_t         )   task1,
                (char *                 )   "task1",
                (configSTACK_DEPTH_TYPE )   TASK1_STACK_SIZE,
                (void *                 )   NULL,
                (UBaseType_t            )   TASK1_PRIO,
                (TaskHandle_t *         )   &task1_handler );
                
    xTaskCreate((TaskFunction_t         )   task2,
                (char *                 )   "task2",
                (configSTACK_DEPTH_TYPE )   TASK2_STACK_SIZE,
                (void *                 )   NULL,
                (UBaseType_t            )   TASK2_PRIO,
                (TaskHandle_t *         )   &task2_handler );
    vTaskDelete(start_task_handler);
    taskEXIT_CRITICAL();                /* 退出临界区 */
}

void task1( void * pvParameters )
{
    while(1)
    {
        modbus_service();     //从机
        vTaskDelay(1000);
    }
}

void task2( void * pvParameters )
{
    while(1)
    {
        HAL_GPIO_TogglePin(LED_RUN_GPIO_Port,LED_RUN_Pin);
        vTaskDelay(1000);
    }
}


/* USER CODE END Application */

注意 每次使用CubeMX生成代码 都要在void StartDefaultTask(void const * argument); 下一行添加 #endif

5 通信测试

5.1 串口调试助手–串口设置

在这里插入图片描述

5.2 接收设置

在这里插入图片描述

5.3 发送设置

在这里图片描述

5.3.1 附加位设置

在这里插入图片描述

5.4 数据日志

在这里插入图片描述
根据 数据格式 分析:

5.4.1 主机发送数据

帧头 0xAA、从机地址 0x40、数据长度 0x05、有效数据 0x68 0x63 0x67 0x69 0xDC、CRC高位 0xF5、CRC低位 0x3F、帧尾 0xEE。

5.4.1从机返还数据

帧头 0xAA、从机地址 0x40、数据长度 0x05、CRC高位 0x27、CRC低位 0x34、帧尾 0xEE。

6 Keil5 调试

使用串口调试助手发送数据,在keil中查看modbus.rcbuf接收缓存
在这里插入图片描述
接收到的数据正常

将有效数据保存到Reg.HoldRegData查看
在这里插入图片描述
正常保存

附录

crc.c

unsigned short CRC16_XMODEM_T(unsigned char *ptr, unsigned int len)
{
    const unsigned int crc_table[256] = {
        0x0000, 0x1021, 0x2042, 0x3063, 0x4084, 0x50a5, 0x60c6, 0x70e7,
        0x8108, 0x9129, 0xa14a, 0xb16b, 0xc18c, 0xd1ad, 0xe1ce, 0xf1ef,
        0x1231, 0x0210, 0x3273, 0x2252, 0x52b5, 0x4294, 0x72f7, 0x62d6,
        0x9339, 0x8318, 0xb37b, 0xa35a, 0xd3bd, 0xc39c, 0xf3ff, 0xe3de,
        0x2462, 0x3443, 0x0420, 0x1401, 0x64e6, 0x74c7, 0x44a4, 0x5485,
        0xa56a, 0xb54b, 0x8528, 0x9509, 0xe5ee, 0xf5cf, 0xc5ac, 0xd58d,
        0x3653, 0x2672, 0x1611, 0x0630, 0x76d7, 0x66f6, 0x5695, 0x46b4,
        0xb75b, 0xa77a, 0x9719, 0x8738, 0xf7df, 0xe7fe, 0xd79d, 0xc7bc,
        0x48c4, 0x58e5, 0x6886, 0x78a7, 0x0840, 0x1861, 0x2802, 0x3823,
        0xc9cc, 0xd9ed, 0xe98e, 0xf9af, 0x8948, 0x9969, 0xa90a, 0xb92b,
        0x5af5, 0x4ad4, 0x7ab7, 0x6a96, 0x1a71, 0x0a50, 0x3a33, 0x2a12,
        0xdbfd, 0xcbdc, 0xfbbf, 0xeb9e, 0x9b79, 0x8b58, 0xbb3b, 0xab1a,
        0x6ca6, 0x7c87, 0x4ce4, 0x5cc5, 0x2c22, 0x3c03, 0x0c60, 0x1c41,
        0xedae, 0xfd8f, 0xcdec, 0xddcd, 0xad2a, 0xbd0b, 0x8d68, 0x9d49,
        0x7e97, 0x6eb6, 0x5ed5, 0x4ef4, 0x3e13, 0x2e32, 0x1e51, 0x0e70,
        0xff9f, 0xefbe, 0xdfdd, 0xcffc, 0xbf1b, 0xaf3a, 0x9f59, 0x8f78,
        0x9188, 0x81a9, 0xb1ca, 0xa1eb, 0xd10c, 0xc12d, 0xf14e, 0xe16f,
        0x1080, 0x00a1, 0x30c2, 0x20e3, 0x5004, 0x4025, 0x7046, 0x6067,
        0x83b9, 0x9398, 0xa3fb, 0xb3da, 0xc33d, 0xd31c, 0xe37f, 0xf35e,
        0x02b1, 0x1290, 0x22f3, 0x32d2, 0x4235, 0x5214, 0x6277, 0x7256,
        0xb5ea, 0xa5cb, 0x95a8, 0x8589, 0xf56e, 0xe54f, 0xd52c, 0xc50d,
        0x34e2, 0x24c3, 0x14a0, 0x0481, 0x7466, 0x6447, 0x5424, 0x4405,
        0xa7db, 0xb7fa, 0x8799, 0x97b8, 0xe75f, 0xf77e, 0xc71d, 0xd73c,
        0x26d3, 0x36f2, 0x0691, 0x16b0, 0x6657, 0x7676, 0x4615, 0x5634,
        0xd94c, 0xc96d, 0xf90e, 0xe92f, 0x99c8, 0x89e9, 0xb98a, 0xa9ab,
        0x5844, 0x4865, 0x7806, 0x6827, 0x18c0, 0x08e1, 0x3882, 0x28a3,
        0xcb7d, 0xdb5c, 0xeb3f, 0xfb1e, 0x8bf9, 0x9bd8, 0xabbb, 0xbb9a,
        0x4a75, 0x5a54, 0x6a37, 0x7a16, 0x0af1, 0x1ad0, 0x2ab3, 0x3a92,
        0xfd2e, 0xed0f, 0xdd6c, 0xcd4d, 0xbdaa, 0xad8b, 0x9de8, 0x8dc9,
        0x7c26, 0x6c07, 0x5c64, 0x4c45, 0x3ca2, 0x2c83, 0x1ce0, 0x0cc1,
        0xef1f, 0xff3e, 0xcf5d, 0xdf7c, 0xaf9b, 0xbfba, 0x8fd9, 0x9ff8,
        0x6e17, 0x7e36, 0x4e55, 0x5e74, 0x2e93, 0x3eb2, 0x0ed1, 0x1ef0
    };

    unsigned short crc = 0x0000;
    
    while(len--) 
    {
        crc = (crc << 8) ^ crc_table[(crc >> 8 ^ *ptr++) & 0xff];
    }
    
    return(crc);
}

END

Logo

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

更多推荐