STM32开发(六)STM32F103 通信 —— RS485 Modbus通信编程详解

news2024/12/29 10:44:52

文章目录

    • 一、基础知识点
    • 二、开发环境
    • 三、STM32CubeMX相关配置
      • 1、STM32CubeMX基本配置
      • 2、STM32CubeMX RS485 相关配置
    • 四、Vscode代码讲解
    • 五、结果演示以及报文解析


一、基础知识点

了解 RS485 Modbus协议技术 。本实验是基于STM32F103开发 实现 通过RS-485实现modbus协议。

准备好了吗?开始我的show time。


二、开发环境

1、硬件开发准备
主控:STM32F103ZET6
RS485收发器:SP3485P
在这里插入图片描述

2、软件开发准备
软件开发使用虚拟机 + VScode + STM32Cube 开发STM32,在虚拟机中直接完成编译下载。
该部分可参考:软件开发环境构建


三、STM32CubeMX相关配置

1、STM32CubeMX基本配置

本实验基于CubeMX详解构建基本框架 进行开发。

2、STM32CubeMX RS485 相关配置

(1)发送接收控制脚配置(GPIO配置)
在这里插入图片描述

在这里插入图片描述
gpio输出电平: 低(控制引脚默认低电平,芯片处于读状态)
gpio模式: 推挽输出
gpio上下拉设置: 不上下拉
gpio输出速度: 低速
gpio命名: RS485_DE_nRE (与硬件标识一致,便于代码编写)

(2)串口UART3配置
在这里插入图片描述

根据硬件引脚连接,RS485芯片连接UART3通信

在这里插入图片描述
基本配置: 实验波特率采用9600、数据位8bit、无奇偶校验、停止位1bit
数据方向: 接收发送
在这里插入图片描述
DMA配置: Add添加发送和接收的DMA,DMA参数保持默认状态

(3)中断配置
在这里插入图片描述
实验中接收数据采用空闲触发;发送数据采用DMA发送触发后发送完成中断
UART3总中断(USART3 global interrupt)必须打开(为了发送完成中断实现)
UART_RX (DMA1 channel3 global interrupt) DMA接收中断不打开,取消对钩(这里对钩无法改变,后续解决)
UART_TX (DMA1 channel2 global interrupt) DMA发送中断打开。

在这里插入图片描述
进行NVIC中断等级配置(0等级最高)
上述讲到无法取消DMA接收中断,原因是选中了强制DMA中断(右上角蓝色框,取消对钩就ok)


四、Vscode代码讲解

1、初始化相关中断

#ifdef STM32_F407_RS485_Modbus
    printf("----DWB   此程序通过RS-485实现modbus协议----\r\n");

    __HAL_UART_ENABLE_IT(&huart3, UART_IT_IDLE);        // 使能串口3空闲中断
    HAL_UART_Receive_DMA(&huart3, UART3.pucRec_Buffer, UART3_Rec_LENGTH);
#endif

2、RS485 结构体 以及函数实现

typedef struct 
{
  uint8_t* pucSend_Buffer;     //发送缓存指针  
  uint8_t* pucRec_Buffer;     //接收缓存指针 

  void (*SendArray)(uint8_t*, uint16_t);    //串口发送数组
  void (*SendString)(uint8_t*);             //串口发送字符串

  void (*RS485_Set_SendMode)(void);  //RS-485接口设置为发送模式
  void (*RS485_Set_RecMode)(void);   //RS-485接口设置为接收模式
 
  /* data */
} UART_t;

// 串口发数组
static void SendArray(uint8_t* p_Arr,uint16_t LEN) 
{
  UART3.RS485_Set_SendMode(); 
  HAL_UART_Transmit_DMA(&huart3,p_Arr,LEN);
}

// RS485接口设置发送模式
static void RS485_Set_SendMode()
{
  HAL_GPIO_WritePin(RS485_DE_nRE_GPIO_Port, RS485_DE_nRE_Pin,,GPIO_PIN_SET);
}

// RS485接口设置接收模式
static void RS485_Set_RecMode()
{
  HAL_GPIO_WritePin(RS485_DE_nRE_GPIO_Port, RS485_DE_nRE_Pin,,GPIO_PIN_RESET);
}

3、RS485 Modbus发送
重构接收回调函数(整个DMA发送过程后面有讲解)

void HAL_UART_TxCpltCallback(UART_HandleTypeDef *huart)
{
  /* Prevent unused argument(s) compilation warning */
  if(huart->Instance == huart3.Instance)
  {
    UART3.RS485_Set_RecMode();
  }
}

4、RS485 Modbus接收
接收使用空闲中断 ,在串口总中断中添加空闲中断检测。

void USART3_IRQHandler(void)
{
  /* USER CODE BEGIN USART3_IRQn 0 */
  if(__HAL_UART_GET_FLAG(&huart3, UART_FLAG_IDLE))      // 判断空闲中断标志位
  {
    __HAL_UART_CLEAR_IDLEFLAG(&huart3);                 // 1、清除中断标志位
    HAL_UART_IdleCallback(&huart3);                     // 2、空闲中断回调函数
  }
  /* USER CODE END USART3_IRQn 0 */
  HAL_UART_IRQHandler(&huart3);
  /* USER CODE BEGIN USART3_IRQn 1 */

  /* USER CODE END USART3_IRQn 1 */
}

在 Drivers/STM32F1xx_HAL_Driver/Inc/stm32f1xx_hal_uart.h 文件中回调函数并没有串口空闲中断回调函数
在这里插入图片描述
重构空闲中断回调函数

void HAL_UART_IdleCallback(UART_HandleTypeDef *huart)
{
  if(huart->Instance == huart3.Instance)
  {
    Modbus.Protocol_Analysis(&UART3);                                        // 接收数据解析
    HAL_UART_Receive_DMA(&huart3, UART3.pucRec_Buffer, UART3_Rec_LENGTH);    // 重新开启接收DMA(在数据解析中会暂时关闭接收DMA)
  }
}

5、Modbus 收发数据详解

(1)Modbus结构体

typedef struct 
{
  uint16_t addr;

  void (*Protocol_Analysis)(UART_t*);  

} Modbus_t;

(2)Modbus接收数据整体框架

#define UART_Order_Index 8
#define FunctionCode_Read_Register 0x03
#define FunctionCode_Write_Register 0x06
#define UART3_Send_LENGTH  20
#define UART3_Rec_LENGTH   20

static void Protocol_Analysis(UART_t* UART)
{
  UART_t* const COM_UART = UART;
  uint8_t i = 0, Index = 0;

  // 1、关闭接收
  HAL_UART_AbortReceive(&huart3);

  // 2、整理接收数据
  for(i=0; i<UART3_Rec_LENGTH; i++)
  {
    if(Index == 0)
    {
        if(*(COM_UART->pucRec_Buffer+i) != Modbus.addr)
        continue;
    }

    *(COM_UART->pucRec_Buffer + Index) = *(COM_UART->pucRec_Buffer + i);
    // 取7字节
    if(Index == UART_Order_Index) 
      break;        
     
    Index++;
  }

    // 4、校验码
    CRC_16.CRC_Value = CRC_16.CRC_Check(COM_UART->pucRec_Buffer, 6);
    CRC_16.CRC_H = (u_int8_t)(CRC_16.CRC_Value >> 8);
    CRC_16.CRC_L = (u_int8_t)CRC_16.CRC_Value;

    if(((*(COM_UART->pucRec_Buffer+6) == CRC_16.CRC_L) && (*(COM_UART->pucRec_Buffer+7) == CRC_16.CRC_H))
                                                                                                ||
       ((*(COM_UART->pucRec_Buffer+6) == CRC_16.CRC_H) && (*(COM_UART->pucRec_Buffer+7) == CRC_16.CRC_L)))
    {
            //校验地址
            if((*(COM_UART->pucRec_Buffer+0)) == Modbus.addr)
            {
            // 5、数据处理
                if((*(COM_UART->pucRec_Buffer+1)) == FunctionCode_Read_Register)
                {
                    Modbus_Read_Register(COM_UART);
                }
                else if((*(COM_UART->pucRec_Buffer+1)) == FunctionCode_Write_Register)
                {
                    Modbus_Wrtie_Register(COM_UART);
                }   
            }
        }
    
    //清缓存
    for(i=0;i<UART3_Rec_LENGTH;i++)
    {
        *(COM_UART->pucRec_Buffer+i) = 0x00;
    }
}

Modbus_Read_Register函数数据解析(协议数据:地址码+功能码+数据长度(字节)+发送数据+CRC)连续读取从设备寄存器值返回给主设备。

static void Modbus_Read_Register(UART_t* UART)
{
  UART_t* const COM_UART = UART;
    
    //校验地址
    if((*(COM_UART->pucRec_Buffer+2) == 0x9C) && (*(COM_UART->pucRec_Buffer+3) == 0x41))
    {
        回应数据
        //地址码
        *(COM_UART->pucSend_Buffer+0)  = Modbus.addr;
        //功能码
        *(COM_UART->pucSend_Buffer+1)  = FunctionCode_Read_Register;
        //数据长度(字节)
        *(COM_UART->pucSend_Buffer+2)  = 2;
        //发送数据
        // deep status
        *(COM_UART->pucSend_Buffer+3)  = 0;
        *(COM_UART->pucSend_Buffer+4) = Deep.Read_Deep();
        *(COM_UART->pucSend_Buffer+5)  = 0;
        *(COM_UART->pucSend_Buffer+6) = 0x66;
        
        //插入CRC
        CRC_16.CRC_Value = CRC_16.CRC_Check(COM_UART->pucSend_Buffer,7); //计算CRC值
        CRC_16.CRC_H     = (uint8_t)(CRC_16.CRC_Value >> 8);
        CRC_16.CRC_L     = (uint8_t)CRC_16.CRC_Value;
        
        *(COM_UART->pucSend_Buffer+7) = CRC_16.CRC_L;
        *(COM_UART->pucSend_Buffer+8) = CRC_16.CRC_H;

        //发送数据
        UART3.SendArray(COM_UART->pucSend_Buffer,9);
    }
}

Modbus_Wrtie_Register函数数据解析。从主设备获取控制从设备外设的数值,解析后控制外设。

static void Modbus_Wrtie_Register(UART_t* UART)
{
    UART_t* const COM_UART = UART;
    uint8_t i=0;
    
    //回应数据
    for(i=0;i<8;i++)
    {
        *(COM_UART->pucSend_Buffer+i) = *(COM_UART->pucRec_Buffer+i);
    }
    //发送数据
    UART3.SendArray(COM_UART->pucSend_Buffer,8);

    //解析数据,控制外设
    if((*(COM_UART->pucRec_Buffer+2) == 0x9C) && (*(COM_UART->pucRec_Buffer+3) == 0x42))
    {
        if(*(COM_UART->pucRec_Buffer+5) == Deep_Status_ON )
            Deep.Deep_Enable();
        else
            Deep.Deep_Disable();
    }
}

为什么要使能DMA发送完成中断才会触发UART的发送完成中断?
答案就在代码里,带大家解析一遍相关代码:

// 调用HAL_UART_Transmit_DMA函数实现DMA发送
HAL_UART_Transmit_DMA
    -> huart->hdmatx->XferCpltCallback = UART_DMATransmitCplt;   // 设置发送完成回调函数

static void UART_DMATransmitCplt(DMA_HandleTypeDef *hdma)
{
  UART_HandleTypeDef *huart = (UART_HandleTypeDef *)((DMA_HandleTypeDef *)hdma)->Parent;
  /* DMA Normal mode*/
  if ((hdma->Instance->CCR & DMA_CCR_CIRC) == 0U)
  {
    huart->TxXferCount = 0x00U;

    /* Disable the DMA transfer for transmit request by setting the DMAT bit
       in the UART CR3 register */
    CLEAR_BIT(huart->Instance->CR3, USART_CR3_DMAT);

    /* Enable the UART Transmit Complete Interrupt */
    SET_BIT(huart->Instance->CR1, USART_CR1_TCIE);            // 当DMA发送完成后,会使能串口发送完成中断

  }
  /* DMA Circular mode */
  else
  {
#if (USE_HAL_UART_REGISTER_CALLBACKS == 1)
    /*Call registered Tx complete callback*/
    huart->TxCpltCallback(huart);
#else
    /*Call legacy weak Tx complete callback*/
    HAL_UART_TxCpltCallback(huart);
#endif /* USE_HAL_UART_REGISTER_CALLBACKS */
  }
}

当DMA发送完成后,会使能串口发送完成中断。配置打开UART3中断总开关。

HAL_UART_IRQHandler
    -> if (((isrflags & USART_SR_TC) != RESET) && ((cr1its & USART_CR1_TCIE) != RESET))
    -> UART_EndTransmit_IT(huart);
        -> HAL_UART_TxCpltCallback(huart);      // 回调函数为弱函数,可重构

五、结果演示以及报文解析

实验测试使用USB转RS485工具。从设备板子上A B接口连接USB转RS485工具上对应A B接口。主设备为PC端安装的MThings进行Modbus收发数据测试。有兴趣的小伙伴可以体验下MTings官网
在这里插入图片描述
发送数据报文解析
在这里插入图片描述

[2023-03-05 13:13:03-802]COM34-发送:01 06 9c 42 00 01 c6 4e
[2023-03-05 13:13:03-827]COM34-接收:01 06 9c 42 00 01 c6 4e
0x01:主机要查询的从设备地址
0x06:功能码 修改写操作
0x9c 0x42:寄存器地址0x9c42转十进制地址为40,002
0x00 0x01:写入地址的数值为0x01 (控制从设备蜂鸣器打开)
0xc6 0x4e:CRC校验码

[2023-03-05 13:13:04-980]COM34-发送:01 06 9c 42 00 00 07 8e
[2023-03-05 13:13:05-012]COM34-接收:01 06 9c 42 00 00 07 8e
0x01:主机要查询的从设备地址
0x06:功能码 修改写操作
0x9c 0x42:寄存器地址0x9c42转十进制地址为40,002
0x00 0x01:写入地址的数值为0x00 (控制从设备蜂鸣器关闭)
0xc6 0x4e:CRC校验码

接收数据报文解析
在这里插入图片描述

[2023-03-05 13:41:54-954]COM34-发送:01 06 9c 42 00 01 c6 4e
[2023-03-05 13:41:54-977]COM34-接收:01 06 9c 42 00 01 c6 4e
0x01:从设备地址
0x06:功能码 修改写操作
0x9c 0x42:寄存器地址0x9c42转十进制地址为40,002
0x00 0x01:写入地址的数值为0x00 (控制从设备蜂鸣器关闭)
0xc6 0x4e:CRC校验码

[2023-03-05 13:41:56-289]COM34-发送:01 03 9c 41 00 02 ba 4f
0x01:主机要查询的从设备地址
0x03:功能码 查询读操作
0x9c 0x42:寄存器地址0x9c41转十进制地址为40,001
0x00 0x02:读取两个数据(一个数据2字节)
0xba 0x4f:CRC校验码

[2023-03-05 13:41:56-320]COM34-接收:01 03 02 00 01 00 66 d9 a3
0x01:告诉主机自己从设备地址
0x03:功能码 读操作
0x00 0x01:读出第一个数据为0x01,当前蜂鸣器打开状态
0x00 0x66:读取第二个数据为0x66(该值是本猿在代码中写死的值,后续功能会结合本章节modbus功能通信,敬请期待)
0xd9 0xa3:CRC校验码

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/389732.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

DJ1-1 计算机网络和因特网

目录 一、计算机网络 二、Interent 1. Internet 的介绍 2. Internet 的具体构成 3. Internet 提供的服务 4. Internet 的通信控制 一、计算机网络 定义&#xff1a;是指两台以上具有独立操作系统的计算机通过某些介质连接成的相互共享软硬件资源的集合体。 计算机网络向…

Python机器学习17——极限学习机(ELM)

本系列基本不讲数学原理&#xff0c;只从代码角度去让读者们利用最简洁的Python代码实现机器学习方法。 背景&#xff1a; 极限学习机(ELM)也是学术界常用的一种机器学习算法&#xff0c;严格来说它应该属于神经网络&#xff0c;应该属于深度学习栏目&#xff0c;但是我这里把它…

C/C++开发,无可避免的多线程(篇四).线程与函数的奇妙碰撞

一、函数、函数指针及函数对象 1.1 函数 函数&#xff08;function&#xff09;是把一个语句序列&#xff08;函数体, function body&#xff09;关联到一个名字和零或更多个函数形参&#xff08;function parameter&#xff09;的列表的 C 实体&#xff0c;可以通过返回或者抛…

MongoDB分片教程

一、概述分片是一种将数据分布在多个 机器。MongoDB使用分片来支持具有非常大数据的部署 集和高吞吐量操作。具有大型数据集或高吞吐量应用程序的数据库系统可以 挑战单个服务器的容量。例如&#xff0c;高查询率可以 耗尽服务器的 CPU 容量。工作集大小大于 系统的 RAM 会给磁…

初学者的第一个Linux驱动

软件环境&#xff1a;Ubuntu20.04 Linux内核源码&#xff1a;3.4.39 硬件环境&#xff1a;GEC6818 什么是驱动&#xff1f;简单来说就是让硬件工作起来的程序代码。 Linux驱动模块加载有两种方式&#xff1a; 1、把写好的驱动代码直接编译进内核。 2、把写好的驱动代码编…

Linux24 -- tcp相关概念、多个客户端链接服务端代码

一、tcp相关概念 tcp协议特点&#xff1a;面向连接的、可靠的、流式服务 建立链接&#xff1a;三次握手&#xff0c;发送 SYN 断开链接&#xff1b;四次挥手&#xff0c;发送 FIN tcp、udp都同属于传输层&#xff0c;在网络层使用ip协议&#xff0c;都要将数据交给IP协议&am…

零拷贝技术-内核源码剖析

在网络编程中&#xff0c;如果我们想要提供文件传输的功能&#xff0c;最简单的方法就是用read将数据从磁盘上的文件中读取出来&#xff0c;再将其用write写入到socket中&#xff0c;通过网络协议发送给客户端。ssize_t read(int fd, void *buf, size_t count); ssize_t write(…

学习记录---latent code 潜在编码

文章目录参考文献1. 什么是潜在编码&#xff1f;2.什么是潜在空间&#xff1f;3.同类潜在编码的相似性4.潜在编码的应用4.1 Antoencoders4.2 Generative models5.结论个人学习总结&#xff0c;持续更新中……参考文献 [1] 快速理解深度学习中的latent code潜在编码 1. 什么是…

[一篇读懂]C语言十一讲:单链表的删除和单链表真题实战

[一篇读懂]C语言十一讲&#xff1a;单链表的删除和单链表真题实战1. 与408关联解析及本节内容介绍1 本节内容介绍2. 单链表的删除操作实战3. 单链表真题解读与解题设计1 题目解读2 解题设计第一阶段&#xff1a;双指针找中间结点第二阶段&#xff1a;原地逆置第三阶段&#xff…

ubuntu16.04 python代码自启动和可执行文件自启动

1 python代码自启动 参考 https://blog.csdn.net/qq_38288618/article/details/104096606 准备好python文件 test.py import time c1 while 1:time.sleep(1)cc1print(c)运行 sudo chmod 777 test.py python3 test.py准备run.sh 文件 #!/bin/bash gnome-terminal -x bash -…

【Spring6】IoC容器之基于XML管理Bean

3、容器&#xff1a;IoC IoC 是 Inversion of Control 的简写&#xff0c;译为“控制反转”&#xff0c;它不是一门技术&#xff0c;而是一种设计思想&#xff0c;是一个重要的面向对象编程法则&#xff0c;能够指导我们如何设计出松耦合、更优良的程序。 Spring 通过 IoC 容…

C语言学习笔记——指针(初阶)

前言 指针可以说是C语言基础语法中最难的理解的知识之一&#xff0c;很多新手&#xff08;包括我&#xff09;刚接触指针时都觉得很难。在我之前发布的笔记中都穿插运用了指针&#xff0c;但是我一直没有专门出一期指针的笔记&#xff0c;这是因为我确实还有些细节至今还不太清…

STM32之关门狗

看门狗介绍在由单片机构成的微型计算机系统中&#xff0c;由于单片机的工作常常会受到来自外界电磁场的干扰&#xff0c;造成程序的跑飞&#xff0c;而陷入死循环&#xff0c;程序的正常运行被打断&#xff0c;由单片机控制的系统无法继续工作&#xff0c;会造成整个系统的陷入…

vue3+rust个人博客建站日记5-所有界面

没有数据的前端&#xff0c;是没有灵魂的。明明标题是vue3 rust &#xff0c;但日记撰写至今&#xff0c;似乎只有第一篇提及了Rust&#xff0c;这可不行。是时候一股作气&#xff0c;完成大部分页面绘制工作了&#xff01; 最后再说一次&#xff0c;时间要加速了。 ——普奇神…

EPICS S7nodave手册

第一章&#xff1a;介绍 本手册分为6章(不算次介绍部分)。第一章介绍s7nodave用于EPICS的设备支持的概念和特新。第二章描述启动一个使用s7nodave的IOC项目所需要的几步。第三章描述s7nodave支持的IOC shell命令。之后&#xff0c;第四章解释s7nodave支持的各种记录类型。最后…

【算法】期末复盘,酒店住宿问题——勿向思想僵化前进

文章目录前言题目描述卡在哪里代码&#xff08;C&#xff09;前言 省流&#xff1a;一个人也可以住双人间&#xff0c;如果便宜的话。 害&#xff01;尚正值青春年华&#xff0c;黄金岁月&#xff0c;小脑瓜子就已经不灵光咯。好在我在考试的最后一分钟还是成功通过了这题&am…

Jetpack Compose 中的 CompositionLocal

要在可组合函数之间共享数据时&#xff0c;可以通过参数传递显式地调用&#xff0c;这通常是最简单和最好的方式。 但随着参数越来越多&#xff0c;组件也越来越多&#xff0c;并且有些数据还需要保持私有性&#xff0c;这时这种方式就会显得很繁琐臃肿&#xff0c;难以维护。…

vscode插件推荐

文章目录前言一、vscode插件推荐&#xff1f;1、 Chinese (Simplified) (简体中文) Language Pack for Visual Studio Code2、Auto Close Tag3、Auto Import3、Error Lens4、vscode-icons5、ES7 React/Redux/React-Native snippets6、GitLens — Git supercharged7、JavaScript…

【FPGA】Verilog:时序电路应用 | 序列发生器 | 序列检测器

前言&#xff1a;本章内容主要是演示Vivado下利用Verilog语言进行电路设计、仿真、综合和下载 示例&#xff1a;序列发生器与序列检测器 ​ 功能特性&#xff1a; 采用 Xilinx Artix-7 XC7A35T芯片 配置方式&#xff1a;USB-JTAG/SPI Flash 高达100MHz 的内部时钟速度 存储器…

车道线检测CondLaneNet论文和源码解读

CondLaneNet: a Top-to-down Lane Detection Framework Based on Conditional Convolution Paper&#xff1a;https://arxiv.org/pdf/2105.05003.pdf code&#xff1a;GitHub - aliyun/conditional-lane-detection 论文解读&#xff1a; 一、摘要 这项工作作为车道线检测任…