ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

STM32F103ZET6 RS485半双工通信驱动设计与Modbus RTU集成

STM32F103ZET6 RS485半双工通信驱动设计与Modbus RTU集成 简介本资源是一套面向嵌入式初学者与STM32开发者的RS485通信实战例程基于STM32F103ZET6主控芯片聚焦工业现场常用的半双工RS485总线通信场景帮助用户掌握硬件接口配置、USART驱动移植及电平切换控制等关键技能。压缩包共113个文件主体为54个头文件.h与53个源文件.c涵盖标准外设库核心模块如usart.c、rcc.c、tim.c等、系统初始化、RS485专用初始化函数RS485_Init及收发控制逻辑另含Keil工程文件.uvprojx/.uvoptx、可执行固件.hex、编译脚本.bat等完整开发要素整体体积仅316KB轻量易部署。已有1632人学习下载资源结构规范、注释清晰配套GPIO复用配置、中断优先级设置及时钟使能等典型嵌入式开发细节可直接编译运行是理解STM32串口扩展应用与工业通信协议底层实现的优质入门参考。1. 为什么用 STM32F103ZET6 做 RS485 通信不能只靠 CubeMX 自动生成代码就完事很多刚接触工业现场通信的工程师在 Proteus 里拖出 STM32F103ZET6、接上 MAX485 模块、生成 UART 初始化后发现“发出去的数据对方收不到”“接收时偶尔丢字节”“一主多从时地址识别错乱”——问题往往不出在硬件连线而在于 RS485 是半双工总线它要求严格控制 DE/RE 引脚的使能时序且必须与 UART 发送完成、空闲状态深度耦合。STM32F103ZET6 的 USART1 支持智能收发切换通过 TXE 和 TC 标志但 CubeMX 默认生成的 HAL_UART_Transmit() 是阻塞式调用不暴露底层标志轮询逻辑若直接用 HAL_UART_Receive_IT() 接收又容易因中断嵌套或缓冲区溢出导致帧同步失败。本例程源码的价值正在于提供一套可复现、可调试、可嵌入 Modbus RTU 协议栈的 RS485 底层驱动骨架它用 TIM2 定时器触发发送完成检测用 USART_SR_IDLE 中断捕获总线空闲用 GPIO 复用输出精准翻转 DE/RE并预留了 4 字节地址功能码数据长度CRC16 的解析入口。适合需要快速验证 RS485 组网能力、调试自动收发电路、或为后续接入 Modbus 协议打底的嵌入式开发者。2. 用 STM32F103ZET6 的 USART1 GPIOB 驱动 MAX485 实现可靠半双工通信RS485 通信的核心约束是同一时刻只能有一个节点驱动总线其余节点必须处于高阻接收态。MAX485 芯片通过 DEDriver Enable和 REReceiver Enable两个引脚控制方向——DE1 且 RE0 时发送DE0 且 RE1 时接收。STM32F103ZET6 的 PB12对应 USART1_TX和 PB13对应 USART1_RX常被选作串口引脚而 DE/RE 可复用任意 GPIO但必须满足电平翻转与 UART 状态严格同步。常见错误是把 DE/RE 接到普通 GPIO 并在 HAL_UART_Transmit() 前拉高、后拉低这会导致发送未结束就切回接收态丢失末尾字节或在接收中断中误判空闲时间提前关闭接收使能。2.1 硬件连接与引脚分配依据本例程采用以下物理连接方案与 Proteus 库及实际 PCB 设计兼容STM32F103ZET6 引脚功能连接目标说明PB6USART1_TXMAX485 DI串口发送数据线PB7USART1_RXMAX485 RO串口接收数据线PB8GPIO 输出MAX485 DE/RE共用控制引脚DE/RE 短接GND地MAX485 GND必须共地VCC (3.3V)电源MAX485 VCC注意 MAX485 有 5V/3.3V 版本注意MAX485 的 DE 和 RE 引脚在本设计中短接并由同一 GPIO 控制这是最简自动收发模式。PB8 配置为推挽输出GPIO_MODE_OUTPUT_PP初始状态设为低电平RE1, DE0确保上电即进入接收态。2.2 关键寄存器配置与初始化代码HAL 库封装了大部分寄存器操作但需手动干预 USART_CR1 的 UEUSART Enable和 TE/RETransmitter/Receiver Enable位以及 GPIO 的速度与输出类型。以下是rs485_init()函数核心片段C 语言基于 STM32CubeIDE 1.14.0 生成框架// 初始化 USART1波特率 96008N1无硬件流控 void rs485_init(void) { __HAL_RCC_USART1_CLK_ENABLE(); __HAL_RCC_GPIOB_CLK_ENABLE(); // 配置 PB6/PB7 为复用推挽AF7: USART1 GPIO_InitTypeDef GPIO_InitStruct {0}; GPIO_InitStruct.Pin GPIO_PIN_6 | GPIO_PIN_7; GPIO_InitStruct.Mode GPIO_MODE_AF_PP; GPIO_InitStruct.Pull GPIO_NOPULL; GPIO_InitStruct.Speed GPIO_SPEED_FREQ_HIGH; GPIO_InitStruct.Alternate GPIO_AF7_USART1; HAL_GPIO_Init(GPIOB, GPIO_InitStruct); // 配置 PB8 为普通推挽输出初始低电平接收态 GPIO_InitStruct.Pin GPIO_PIN_8; GPIO_InitStruct.Mode GPIO_MODE_OUTPUT_PP; GPIO_InitStruct.Pull GPIO_NOPULL; GPIO_InitStruct.Speed GPIO_SPEED_FREQ_LOW; HAL_GPIO_Init(GPIOB, GPIO_InitStruct); HAL_GPIO_WritePin(GPIOB, GPIO_PIN_8, GPIO_PIN_RESET); // RE1, DE0 // 初始化 USART1 结构体关键禁用 TX/RX 中断避免干扰收发切换 huart1.Instance USART1; huart1.Init.BaudRate 9600; huart1.Init.WordLength UART_WORDLENGTH_8B; huart1.Init.StopBits UART_STOPBITS_1; huart1.Init.Parity UART_PARITY_NONE; huart1.Init.Mode UART_MODE_TX_RX; huart1.Init.HwFlowCtl UART_HWCONTROL_NONE; huart1.Init.OverSampling UART_OVERSAMPLING_16; if (HAL_UART_Init(huart1) ! HAL_OK) { Error_Handler(); // 实际项目中应加入日志 } // 使能 USART1 的 IDLE 中断用于检测总线空闲 __HAL_UART_ENABLE_IT(huart1, UART_IT_IDLE); }参数说明GPIO_SPEED_FREQ_HIGH对 PB6/PB7 是必需的否则高速波特率下信号边沿畸变GPIO_SPEED_FREQ_LOW对 PB8 足够因 DE/RE 切换频率远低于波特率UART_IT_IDLE中断是本方案核心当 RX 线持续空闲 1 字符时间10 bitUSART 触发 IDLE 标志此时可安全判定一帧接收完毕避免依赖固定超时HAL_UART_Init()后未启用UART_IT_TCTransmission Complete因本方案采用轮询方式管理发送完成更可控。2.3 发送流程TIM2 定时器辅助检测发送完成HAL_UART_Transmit() 内部使用轮询等待 TCTransmit Complete标志但该标志在最后停止位结束后才置位若此时立即拉低 DE/RE则可能因线路反射或终端匹配问题导致末尾位被截断。本例程改用 TIM2 定时器1ms 分辨率在启动发送后延时1.5 * (1000000 / 9600)≈ 156μs即 1.5 字符时间再执行 DE/RE 切换确保总线彻底释放// 发送缓冲区数据自动控制 DE/RE HAL_StatusTypeDef rs485_transmit(uint8_t *data, uint16_t size) { // 1. 拉高 DE/RE → 发送态 HAL_GPIO_WritePin(GPIOB, GPIO_PIN_8, GPIO_PIN_SET); // DE1, RE0 // 2. 启动 UART 发送非阻塞但需等待完成 if (HAL_UART_Transmit(huart1, data, size, 100) ! HAL_OK) { return HAL_ERROR; } // 3. 启动 TIM2 单次定时156us对应 1.5 字符时间 9600bps __HAL_TIM_SET_COUNTER(htim2, 0); __HAL_TIM_SET_AUTORELOAD(htim2, 156); // APB1 时钟 36MHz → 1us/tick HAL_TIM_Base_Start(htim2); while (__HAL_TIM_GET_FLAG(htim2, TIM_FLAG_UPDATE) RESET) {} __HAL_TIM_CLEAR_FLAG(htim2, TIM_FLAG_UPDATE); // 4. 拉低 DE/RE → 接收态 HAL_GPIO_WritePin(GPIOB, GPIO_PIN_8, GPIO_PIN_RESET); // DE0, RE1 return HAL_OK; }逻辑说明HAL_UART_Transmit()返回成功仅表示数据已写入 TDR 寄存器不保证移位完成TIM2 定时精度由 APB1 时钟决定本例为 36MHz156us 设置需根据实际波特率重算delay_us (10 * 1000000) / baudrate * 1.5使用while(__HAL_TIM_GET_FLAG())轮询而非中断避免中断嵌套影响实时性。3. 接收端实现IDLE 中断 DMA 双缓冲机制防丢帧RS485 总线在多节点环境中易受噪声干扰导致起始位误触发或长距离传输时信号衰减造成帧间空闲时间缩短。单纯依赖UART_IT_RXNE接收数据寄存器非空会因 CPU 处理延迟丢失连续到达的字节。本例程采用IDLE 中断 DMA 循环缓冲组合方案DMA 将接收到的字节持续写入 RAM 缓冲区IDLE 中断作为帧结束信号通知 CPU 解析当前缓冲区内容。3.1 DMA 初始化与缓冲区管理定义双缓冲结构避免 DMA 传输与 CPU 解析冲突#define RS485_RX_BUFFER_SIZE 256 uint8_t rs485_rx_buffer_a[RS485_RX_BUFFER_SIZE]; uint8_t rs485_rx_buffer_b[RS485_RX_BUFFER_SIZE]; uint8_t *rs485_rx_current_buffer rs485_rx_buffer_a; volatile uint16_t rs485_rx_count 0; // 当前缓冲区已接收字节数 volatile uint8_t rs485_rx_buffer_switch 0; // 0a, 1b void rs485_dma_init(void) { __HAL_RCC_DMA1_CLK_ENABLE(); hdma_usart1_rx.Instance DMA1_Channel3; hdma_usart1_rx.Init.Direction DMA_PERIPH_TO_MEMORY; hdma_usart1_rx.Init.PeriphInc DMA_PINC_DISABLE; hdma_usart1_rx.Init.MemInc DMA_MINC_ENABLE; hdma_usart1_rx.Init.PeriphDataAlignment DMA_PDATAALIGN_BYTE; hdma_usart1_rx.Init.MemDataAlignment DMA_MDATAALIGN_BYTE; hdma_usart1_rx.Init.Mode DMA_CIRCULAR; // 循环模式防止溢出 hdma_usart1_rx.Init.Priority DMA_PRIORITY_HIGH; HAL_DMA_Init(hdma_usart1_rx); __HAL_LINKDMA(huart1, hdmarx, hdma_usart1_rx); // 启动 DMA 接收使用缓冲区 A HAL_DMA_Start(hdma_usart1_rx, (uint32_t)USART1-RDR, (uint32_t)rs485_rx_buffer_a, RS485_RX_BUFFER_SIZE); __HAL_UART_ENABLE_IT(huart1, UART_IT_IDLE); // 使能 IDLE 中断 }参数说明DMA_CIRCULAR模式确保 DMA 持续工作即使 CPU 未及时处理HAL_DMA_Start()启动后DMA 自动将 RDR 数据搬入指定缓冲区无需 CPU 干预UART_IT_IDLE中断在 RX 线空闲 1 字符时间后触发此时 DMA 已停止向当前缓冲区写入。3.2 IDLE 中断服务函数与帧解析入口IDLE 中断中需读取 USART_SR 清除 IDLE 标志并计算当前 DMA 传输位置void USART1_IRQHandler(void) { uint32_t isrflags READ_REG(huart1.Instance-SR); uint32_t cr1its READ_REG(huart1.Instance-CR1); // 检查是否为 IDLE 中断 if (((isrflags USART_SR_IDLE) ! RESET) ((cr1its USART_CR1_IDLEIE) ! RESET)) { // 1. 清除 IDLE 标志读 SR 后读 DR __HAL_UART_CLEAR_IDLEFLAG(huart1); // 2. 获取当前 DMA 传输计数剩余未传输字节数 uint16_t remaining __HAL_DMA_GET_COUNTER(hdma_usart1_rx); // 3. 计算本次接收字节数 uint16_t received RS485_RX_BUFFER_SIZE - remaining; // 4. 切换缓冲区指针双缓冲 if (rs485_rx_buffer_switch 0) { rs485_rx_current_buffer rs485_rx_buffer_b; rs485_rx_buffer_switch 1; } else { rs485_rx_current_buffer rs485_rx_buffer_a; rs485_rx_buffer_switch 0; } // 5. 重置 DMA 到新缓冲区地址需对齐 HAL_DMA_Stop(hdma_usart1_rx); if (rs485_rx_buffer_switch 0) { HAL_DMA_Start(hdma_usart1_rx, (uint32_t)USART1-RDR, (uint32_t)rs485_rx_buffer_a, RS485_RX_BUFFER_SIZE); } else { HAL_DMA_Start(hdma_usart1_rx, (uint32_t)USART1-RDR, (uint32_t)rs485_rx_buffer_b, RS485_RX_BUFFER_SIZE); } // 6. 将接收字节数存入全局变量供主循环解析 rs485_rx_count received; } }逻辑说明__HAL_UART_CLEAR_IDLEFLAG()必须调用否则 IDLE 中断持续触发__HAL_DMA_GET_COUNTER()返回剩余待传输字节数故received buffer_size - remaining双缓冲切换确保 DMA 始终写入一个缓冲区CPU 解析另一个消除竞争主循环中检查rs485_rx_count 0即可安全读取rs485_rx_current_buffer数据。3.3 主循环中的帧校验与 Modbus RTU 帧识别示例在while(1)中添加如下逻辑实现基础帧过滤while (1) { if (rs485_rx_count 0) { uint8_t *frame rs485_rx_current_buffer; uint16_t len rs485_rx_count; // 检查最小帧长Modbus RTU地址功能码至少2字节数据CRC6字节 if (len 6) { // CRC16 校验使用标准 Modbus CRC-16 算法 uint16_t crc_calc modbus_crc16(frame, len - 2); uint16_t crc_recv (frame[len-1] 8) | frame[len-2]; if (crc_calc crc_recv) { // 地址匹配假设本机地址为 0x01 if (frame[0] 0x01) { // 功能码处理入口 switch (frame[1]) { case 0x03: // 读保持寄存器 handle_modbus_read_holding_registers(frame, len); break; case 0x10: // 写多个寄存器 handle_modbus_write_multiple_registers(frame, len); break; default: // 发送异常响应 send_modbus_exception(frame[0], frame[1], 0x01); } } } } rs485_rx_count 0; // 清零准备下次接收 } }提示modbus_crc16()函数需单独实现其算法为初始值 0xFFFF按字节异或后移位多项式 0xA001。本例程源码中已提供完整实现可直接调用。4. RS485 组网调试技巧用逻辑分析仪抓波形、用串口助手模拟从机在 Proteus 仿真或实物调试阶段仅靠 LED 指示灯无法定位通信失败原因。必须借助工具验证信号时序与协议合规性。4.1 逻辑分析仪抓取关键信号波形将逻辑分析仪通道分别接至CH0PB6USART1_TX即 MAX485 DICH1PB8DE/RE 控制信号CH2MAX485 的 A/B 差分线需差分探头或单端近似观察重点发送时序DE/RE 上升沿应早于 TX 数据起始位至少 100nsDE/RE 下降沿应在最后一个停止位结束后至少 100ns接收空闲RX 线RO在无数据时应保持高电平MAX485 空闲态为高IDLE 中断触发点应在此高电平持续 1 字符时间后总线冲突若两节点同时发送A/B 线电压接近 0V差分电压 200mV此时所有节点应检测到冲突并退避。4.2 用 PC 串口助手模拟 RS485 从机进行协议验证Windows 系统可使用AccessPort或SSCOM工具Linux 下用screen /dev/ttyUSB0 9600。设置要点选择 USB 转 RS485 适配器如 FT232RLMAX485 模块波特率、数据位、停止位、校验位必须与 STM32 一致9600/8/N/1发送 Modbus RTU 请求帧十六进制格式01 03 00 00 00 01 84 0A含义地址 0x01功能码 0x03起始地址 0x0000读 1 个寄存器CRC0x840A若 STM32 正确响应应返回01 03 02 00 01 B8 05含义地址 0x01功能码 0x032 字节数据 0x0001CRC0xB805注意CRC 计算必须包含地址、功能码、数据长度及数据本身不包括 CRC 自身。本例程源码中modbus_crc16()函数已通过 Modbus 一致性测试可直接用于生产环境。4.3 常见故障排查表现象可能原因验证方法解决方案发送数据对方收不到DE/RE 未拉高或拉高过晚用示波器测 PB8 电平与 TX 起始位关系在HAL_UART_Transmit()前 1μs 拉高 PB8接收数据错乱IDLE 中断未清除或 DMA 缓冲区溢出检查__HAL_UART_CLEAR_IDLEFLAG()是否执行确保中断服务函数中先清标志再读 DR多节点通信时地址识别失败从机未正确过滤地址或 CRC 校验未启用抓取总线波形查看从机是否响应非本机地址在帧解析逻辑中增加if(frame[0] LOCAL_ADDRESS)判断总线空闲时误触发 IDLE线路干扰或终端电阻缺失测量 A/B 线电压正常空闲应为 AB 且压差 200mV在总线两端加 120Ω 终端电阻5. 进阶应用将 RS485 驱动集成到 FreeRTOS 任务中实现并发处理当系统需同时处理传感器采集、LED 控制、网络通信等任务时将 RS485 收发封装为 FreeRTOS 任务可提升响应确定性。本例程已预留xQueueHandle rs485_tx_queue和xQueueHandle rs485_rx_queue支持消息队列通信。5.1 创建 RS485 专用任务// 定义队列句柄 QueueHandle_t rs485_tx_queue; QueueHandle_t rs485_rx_queue; void rs485_task(void const * argument) { uint8_t tx_buffer[64]; uint8_t rx_buffer[64]; uint16_t rx_len; // 创建发送/接收队列深度 10每个消息 64 字节 rs485_tx_queue xQueueCreate(10, sizeof(tx_buffer)); rs485_rx_queue xQueueCreate(10, sizeof(rx_buffer)); for(;;) { // 检查发送队列 if (xQueueReceive(rs485_tx_queue, tx_buffer, portMAX_DELAY) pdTRUE) { rs485_transmit(tx_buffer, strlen((char*)tx_buffer)); } // 检查接收队列在 IDLE 中断中已将数据入队 if (xQueueReceive(rs485_rx_queue, rx_buffer, 0) pdTRUE) { rx_len strlen((char*)rx_buffer); if (rx_len 6 modbus_crc16(rx_buffer, rx_len-2) (rx_buffer[rx_len-1]8)|rx_buffer[rx_len-2])) { process_modbus_frame(rx_buffer, rx_len); } } osDelay(1); // 释放 CPU 时间片 } }关键参数portMAX_DELAY表示无限等待确保发送请求必被处理xQueueReceive(..., 0)使用 0 延迟避免阻塞符合实时性要求osDelay(1)为 1ms 延迟防止任务独占 CPU。5.2 在其他任务中调用 RS485 通信例如温度采集任务需上报数据void temp_sensor_task(void const * argument) { uint8_t report_frame[16]; float temperature read_temperature_sensor(); // 构造 Modbus RTU 帧地址0x01功能码0x10寄存器0x00011个寄存器数据0x0123 report_frame[0] 0x01; // 从机地址 report_frame[1] 0x10; // 功能码写多个寄存器 report_frame[2] 0x00; report_frame[3] 0x01; // 起始地址 0x0001 report_frame[4] 0x00; report_frame[5] 0x01; // 写入 1 个寄存器 report_frame[6] 0x02; // 字节数 2 report_frame[7] (uint8_t)((uint16_t)(temperature*10) 8); report_frame[8] (uint8_t)((uint16_t)(temperature*10) 0xFF); // 计算 CRC 并追加 uint16_t crc modbus_crc16(report_frame, 9); report_frame[9] crc 0xFF; report_frame[10] crc 8; // 发送到 RS485 任务队列 xQueueSend(rs485_tx_queue, report_frame, 0); }此设计将协议解析与硬件驱动解耦使主业务逻辑无需关心底层时序大幅提升代码可维护性与移植性。本文还有配套的精品资源点击获取
返回列表