首页 > 其他分享 >STM32CubeMX+usart+DMA+Modbus从机

STM32CubeMX+usart+DMA+Modbus从机

时间:2024-09-25 12:47:57浏览次数:11  
标签:DMA HAL UART 串口 Modbus 从机 xUSART1

前言


单片机型号STM32f103zet6
使用USART1串口来实现DMA收发
使用DMA1通道5,来传输USART1接收的数据
使用DMA1通道4,来传输USART1发送的数据
实现了Modbus 从机协议(03和06功能)
1.准备工作

modbus poll和modbus slave模拟软件下载

链接:https://pan.baidu.com/s/1cX8HC-rm3gsMmHIIvSGo6g?pwd=bai8
提取码:bai8

使用方法参考这位博主的

【工具使用】Modbus Poll软件使用详解-CSDN博客

一.知识简介

1.USART

1).基础知识点在这里插入图片描述

USART(Universal Synchronous and Asynchronous Serial Receiver and Transmitter,通用同步和异步串行接收和发送器)是一种常用于微控制器和计算机之间的串行通信协议。它支持全双工通信,即同时进行数据的发送和接收。USART通常用于嵌入式系统中,因为它可以提供稳定和可靠的数据传输。

以下是USART的一些基本知识点:

  1. 同步与异步通信
  • 异步通信:数据传输不依赖于时钟信号,每个字符的开始和结束由起始位和停止位标识。
    • 同步通信:数据传输依赖于时钟信号,可以更高效地传输数据。
  1. 数据格式
  • 起始位:标识数据传输的开始。
    • 数据位:传输实际的数据。常见的数据位长度有8位或9位。
    • 奇偶校验位(可选):用于错误检测。
    • 停止位:标识数据传输的结束,可以是一个或两个位。
  1. 波特率
  • 波特率是数据传输的速率,通常以比特每秒(bps)来表示。波特率越高,数据传输速度越快。
    • 两个要通信的设备的波特率一定要设置相同,我们常见的波特率是 4800、 9600、115200 等。
  1. 帧结构
  • USART通信的一帧数据包括起始位、数据位、可选的奇偶校验位和停止位。
  1. 通信模式
  • 点对点:两个设备之间的直接通信。
    • 多点:多个设备通过总线连接,可以进行广播或选择性通信。
  1. 硬件接口
  • USART通常通过TX(发送)和RX(接收)引脚进行数据传输。
2).各USART所使用的引脚接口

STM32f103上有3 个USART,其他系列请查看芯片手册在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

2.DMA

1).基础知识点

DMA(Direct Memory Access,直接内存访问)是一种允许某些硬件子系统在不占用CPU的情况下,直接访问系统内存的技术。在串口通信中,DMA可以提高数据传输的效率,因为它允许数据在串口和内存之间直接传输,而不需要CPU的介入。

以下是DMA串口通信的一些基本知识点:

  1. DMA控制器
    • DMA控制器是负责管理DMA传输的硬件组件。它控制数据从源地址到目标地址的传输。
  2. 数据传输
    • 在串口通信中,DMA可以自动从串口接收器(USART)接收数据,并将其存储到内存中,或者从内存中读取数据并发送到串口发送器。
  3. 中断
    • 尽管DMA允许数据传输不需要CPU的直接参与,但通常在传输完成后,DMA控制器会通过中断通知CPU。
  4. 缓冲区
    • DMA通常使用一个或多个缓冲区来存储接收或发送的数据。这些缓冲区可以是环形缓冲区,以支持连续的数据流。
  5. 传输速率
    • DMA可以支持高速数据传输,因为它不受CPU速度的限制。
  6. 错误处理
    • DMA控制器通常具备错误检测功能,如传输超时、数据错误等,并可以在检测到错误时通过中断通知CPU。
  7. 配置
    • DMA的配置包括设置源和目标地址、传输大小、传输方向(接收或发送)、传输速率等。
  8. 优先级
    • 在多通道DMA系统中,可以设置不同的传输优先级,以确保关键任务的数据传输优先进行。
2).MDA框图

在这里插入图片描述

在这里插入图片描述

图中,我们标记了3处位置,起作用分别是:

DMA请求 如果外设想要通过DMA来传输数据,必须先给DMA控制器发送DMA请求,DMA收到 请求信号之后,控制器会给外设一个应答信号,当外设应答后且DMA控制器收到应答信号之 后,就会启动DMA的传输,直到传输完毕。 DMA1有7个通道,DMA2有5个通道, 不同的DMA控制器的通道对应着不同的外设请求,这决定了我们在软件编程上该怎么设置。

DMA1
在这里插入图片描述

DMA2
***

通道 DMA具有12个独立可编程的通道,其中DMA1有7个通道,DMA2有5个通道,每 个通道对应不同的外设的DMA请求。虽然每个通道可以接收多个外设的请求,但是同一时 间只能接收一个,不能同时接收多个。

仲裁器 当发生多个DMA通道请求时,就意味着有先后响应处理的顺序问题,这个就由仲裁器 管理。仲裁器管理DMA通道请求分为两个阶段。第一阶段属于软件阶段,可以在DMA_CCRx 寄存器中设置,有4个等级:非常高,高,中和低四个优先级。第二阶段属于硬件阶段,如 果两个或以上的DMA通道请求设置的优先级一样,则他们优先级取决于通道编号,编号越 低优先权越高,比如通道0高于通道1。在大容量产品和互联型产品中,DMA1控制器拥有 高于DMA2控制器的优先级。

3.Modbus

1).基础知识点

Modbus协议是一种串行通信协议,广泛应用于工业环境中,用于电子设备之间的通信。Modbus协议的数据帧格式通常包括以下部分:

  1. 设备地址(Device Address):用于标识从设备的唯一地址,范围是0到247,0通常作为广播地址。
  2. 功能码(Function Code):指示从设备要执行的动作,不同的功能码对应不同的操作,例如读取或写入寄存器。
  3. 数据域(Data Field):包含请求和响应参数的数据,具体内容取决于功能码。
  4. 校验(Checksum):对报文内容执行冗余校验的计算结果,用于确保数据传输的正确性。Modbus RTU通常使用CRC16校验,而Modbus ASCII使用LRC校验。

Modbus RTU数据帧的具体格式如下:

  • 起始位:1位
  • 数据位:8位,首先发送最低有效位
  • 奇偶校验位:1位
  • 停止位:1位

Modbus协议支持多种通信方式,包括串行通信(如RS-232、RS-485)和以太网通信(Modbus TCP)。在串行通信中,Modbus RTU和Modbus ASCII是两种常见的模式。

2).数据帧解析
  1. 设备地址(Device Address):通常为1个字节,用于标识网络上的从设备。主设备(Master)通常不使用地址。
  2. 功能码(Function Code):1个字节,定义了要执行的动作,如读取或写入寄存器。
  3. 数据域(Data Field):1个或多个字节,包含执行功能码所需的数据,例如寄存器地址、要写入的值等。
  4. 校验(Checksum):通常为2个字节的CRC(循环冗余校验),用于检测数据在传输过程中是否出现错误。

Modbus数据帧

设备地址功能码数据校验
1byte1byteNbyte2byte

本历程只实现了03和06功能码

功能码

0x03:读取从机寄存器数据

主机发送

从机设备地址功能码起始地址寄存器个数CRC校验码
0103xx xxxx xxxx xx

从机回复

从机设备地址功能码数据长度数据1数据2数据NCRC校验码
0103xx xxxx xxxx xxxx xxxx xx

注意数据长度=要读取寄存器个数X2

例子:

Tx:主机

Rx:从机回复

在这里插入图片描述

0x06:向一个寄存器写入数据

主机发送

从机设备地址功能码起始地址寄存器个数CRC校验码
0106xx xxxx xxxx xx

从机回复

从机设备地址功能码起始地址寄存器个数CRC校验码
0106xx xxxx xxxx xx

06功能码回复与主机发送相同

例子:

Tx:主机

Rx:从机回复

在这里插入图片描述

3).CRC校验

1、预置一个16位寄存器为0FFFFH(全1),称之为CRC寄存器。

2 、把数据帧中的第一个字节的8位与CRC寄存器中的低字节进行异或运算,结果存回CRC寄存器。

3、将CRC寄存器向右移一位,最高位填以0,最低位移出并检测。

4 、如果最低位为0:重复第三步(下一次移位);如果最低位为1:将CRC寄存器与一个预设的固定值(0A001H)进行异或运算。

5、重复第三步和第四步直到8次移位。这样处理完了一个完整的八位。

6 、重复第2步到第5步来处理下一个八位,直到所有的字节处理结束。

7、最终CRC寄存器的值就是CRC的值。

方便大家代码放这了

/* 参数1:数组指针;参数2;长度 */

uint16_t Modbus_CRC16(uint8_t *buffer, uint16_t buffer_length)
{
    uint16_t crc = 0xFFFF;
    for (uint16_t i = 0; i < buffer_length; i++)
    {
        crc ^= (uint16_t)buffer[i];
        for (uint8_t j = 0; j < 8; j++)
        {
            if (crc & 0x0001)
                crc = (crc >> 1) ^ 0xA001;
            else
                crc = crc >> 1;
        }
    }
    crc = (crc >> 8) | (crc << 8);
    return crc;
}

二.创建初始化代码

STM32CubeMX使用方法可以看这位博主的

【工具使用】STM32CubeMX-基础使用篇-CSDN博客

打开STM32CubeMX

1.新建工程,配置USART

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

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

2.配置DMA

在这里插入图片描述

3.GPIO配置

在这里插入图片描述

然后…直接点击 GENERATE CODE 生成代码(就是右上角蓝色的按钮)

注意:要生成MDK文件

三.发送测试

打开文件可以看到STM32CubeMX已经帮我们完成发送部分的底层处理和逻辑。

在这里插入图片描述

在USART串口中我们可以使用3种函数发送数据

HAL_UART_Transmit(&huart1, uint8_t *pData, uint16_t Num, 超时值);    
HAL_UART_Transmit_IT(&huart1, uint8_t *pData, uint16_t Num);         
HAL_UART_Transmit_DMA(&huart1,![在这里插入图片描述](https://i-blog.csdnimg.cn/direct/eea2297e60e94b33a152d52e44981ae9.png#pic_center)
 uint8_t *pData, uint16_t Num);         

每个函数发送方式都不同

*HAL_UART_Transmit (&huart1, uint8_t pData, uint16_t Num, 超时值);

参数解释:

  • huart:指向一个UART_HandleTypeDef结构体的指针,这个结构体包含了UART配置的所有信息。
  • pData:指向要发送数据的缓冲区的指针。
  • Size:要发送的数据的大小(以字节为单位)。
  • Timeout:发送操作的超时时间(以毫秒为单位)。传递HAL_MAX_DELAY可以等待无限期。

它是一个阻塞函数,它会一直等待直到所有数据都被发送或者超时发生。

每一次发送一个字节,死等,等到下一个字节发送完成才会发下一个字节,不断地重复,但是有一个超时值。

超时值:在这个时间内如果没有发送完成就会直接返回,以防卡死。

超时值设定:1秒 ÷ 波特率 × 字节数 × 10 × 1000ms。

*HAL_UART_Transmit_IT(&huart1, uint8_t pData, uint16_t Num);

参数解释:

  • huart:指向一个 UART_HandleTypeDef 结构体的指针,这个结构体包含了 UART 配置的所有信息。
  • pData:指向要发送数据缓冲区的指针。
  • Size:要发送的数据的大小(以字节为单位)。

使用 HAL_UART_Transmit_IT() 函数时,你需要确保已经开启了 UART 的中断,并且中断服务例程已经正确配置。

HAL_UART_Transmit_IT() 函数是 STM32 HAL 库提供的一个用于使用中断方式发送 UART 数据的函数。这个函数允许你在发送数据时不用阻塞等待,而是在数据发送完毕后通过中断服务例程(ISR)得到通知。

每发送一个字节就会产生一个发送中断,然后会进入CubeMX生成的回调函数,自动填入下一个字节,不断重复。不要在没有确认上一次传输完成的情况下连续调用,这可能导致UART状态混乱。

在调用 HAL_UART_Transmit_IT() 之前,确保UART处于合适的状态。如果UART正忙于之前的传输,函数将返回 HAL_BUSY。可以通过检查 huart->gState 是否为 HAL_UART_STATE_READY 来确定是否可以开始新的传输

while((&huart1)->gState != HAL_UART_STATE_READY);

*HAL_UART_Transmit_DMA(&huart1, uint8_t pData, uint16_t Num);

参数解释:

  • huart:指向一个 UART_HandleTypeDef 结构体的指针,这个结构体包含了 UART 配置的所有信息。
  • pData:指向要发送数据缓冲区的指针。
  • Size:要发送的数据的大小(以字节为单位)

HAL_UART_Transmit_DMA() 函数是 STM32 HAL 库提供的一个用于使用 DMA(直接内存访问)方式发送 UART 数据的函数。使用 DMA 可以减轻 CPU 的负担,因为它允许数据在内存和外设之间直接传输,而不需要 CPU 的介入。

在连续使用 HAL_UART_Transmit_DMA() 函数时,可能会遇到只能发出第一条数据的问题。这是因为 DMA 传输数据到串口这个外设太快了,传输完后程序并不会在该处停留,但是串口发送需要时间。运行到下一条 HAL_UART_Transmit_DMA() 函数的时候,上一条数据还没来得及发完,导致串口处于 BUSY 状态。可以通过检查 UART 的状HAL_UART_STATE_READY 来确保可以开始新的传输或者可以延时时间让第一条发送有足够的时间,

现在我们上机测试

把这段代码放到/* USER CODE BEGIN 2 /与/ USER CODE END 2 */之间

**注意:**用户代码必须放在BEGIN与END之间,不然使用STM32CubeMX再次生成会把不在BEGIN与END之间的代码消除。

//头文件
#include "stdio.h"
#include <string.h>

/* 下面是三种发送方式
*发送方式1:阻塞式发送
*发送方式2:中断式发送
*发送方式3:DMA发送
*/
static char  Example [] = "Hello World!\r";										//定义一个数组
HAL_UART_Transmit (&huart1, (uint8_t*)Example , strlen(Example), 0xFFFF);     	//发送1
HAL_UART_Transmit_IT (&huart1, (uint8_t*)Example , strlen(Example));			//方式2
while((&huart1)->gState != HAL_UART_STATE_READY);								//等待中断式发送完成
HAL_UART_Transmit_DMA (&huart1,(uint8_t*)Example , strlen(Example));			//方式3

打开串口助手

编译,烧录

可以看到串口有显示
在这里插入图片描述

如果没有显式成功,查看串口是否正确,查看有没有打勾keil的Reset and Run

在这里插入图片描述

四.编写DMA接收

先将printf重定向串口USART1,方便后面的测试

#include <stdio.h>//头文件

/* USER CODE BEGIN 0 */
#pragma import(__use_no_semihosting)
struct __FILE
{
    int handle;
};                                                         // 标准库需要的支持函数
 
FILE __stdout;                                             // FILE 在stdio.h文件
void _sys_exit(int x)
{
    x = x;                                                 // 定义_sys_exit()以避免使用半主机模式
}
 
int fputc(int ch, FILE *f)                                 // 重写fputc函数,使printf的输出由UART1实现,  这里使用USART1
{

    HAL_UART_Transmit(&huart1, (uint8_t *)&ch, 1, 0x02);
    return ch;
}
/* USER CODE END 0 */

1.创建结构体,管理接收和发送的数据

在main.h里面添加
在这里插入图片描述

/* USER CODE BEGIN ET */
typedef struct                      /* 声明一个变量,用于管理变量 */
{
    uint16_t receiveNUM;            /* 接收字节数;在中段回调函数里被自动赋值;8>0即为接收到的第一帧 */
    uint8_t  receiveData[512];      /* 接受到的数据 */
    uint8_t  BuffTemp[512];         /* 接收缓存;接收到一帧的时候就会清零并(一帧)复制到ReceivedData[ ] */
} xUSARTx_TypeDef;

/* 声明串口结构体 */
extern xUSARTx_TypeDef xUSART1; /* 声明串口1的结构体 */

/* USER CODE END ET */

然后回到main.c定义串口USART1结构体
在这里插入图片描述

2.开启DMA接自动收

HAL_UARTEx_ReceiveToIdle_DMA(&huart1, xUSART1.BuffTemp, sizeof(xUSART1.BuffTemp));  // 开启DMA空闲中断    

参数:串口、接收缓存区、最大接收字节数

在 main.c的 /* USER CODE BEGIN 2 / 与 / END 2 */ 之间插入。

在这里插入图片描述

调用函数后,硬件就会进入自动接收状态:从RX引脚接收到的数据,会逐个字节顺序存放到指定缓存中,这里我们指定的缓存是:xUSART1.BuffTemp。

开启DMA中断,空闲中断,只要达成下面两个条件之一就会触发:

1.DMA接收的字节数超过了参数最大字节数。

2.USART发生空闲中断,接收引脚超过1字节时间没有新的信号。

但产生中断之后就会调用回调函数

CubeMX生成的代码,已编写好上述两个中断服务函数,还定义了一个它俩最终调用的回调函数。

3.重写空闲中断回调函数

CubeMX定义的回调函数是一个弱函数

我们可以重写回调函数来实现对接收到的数据进行处理

DMA所用到的回调函数:

HAL_UARTEx_RxEventCallback(UART_HandleTypeDef *huart, uint16_t Size)

参数:串口,接收到的字节个数

重写这个函数,对接收到的数据进行处理

说明:

  1. 该函数是中断回调函数
  2. 函数名不能改变,它在CubeMX生成时已经写好了各种函数调用、函数弱定义;注意:不要在同一个源文件中同时声明强定义和弱定义的同名函数,这会导致编译错误
  3. 不需要清除中断标志位,调用函数前会自己清除
  4. 只要产生DMA与空闲中断函数都会调用此函数,以引脚编号作参数(huart == &huart1)
/* USER CODE BEGIN 4 */

void HAL_UARTEx_RxEventCallback(UART_HandleTypeDef *huart, uint16_t Size)
{
    if (huart == &huart1)                                                                    // 判断串口
    {
        __HAL_UNLOCK(huart);                                                                 // 解锁串口状态
 
        xUSART1.receiveNUM  = Size;                                                          // 把接收字节数,存入结构体xUSART1.ReceiveNum
        memset(xUSART1.receiveData, 0, sizeof(xUSART1.receiveData));                         // 清除xUSART1.ReceiveData前一帧接收到的数据
        memcpy(xUSART1.receiveData, xUSART1.BuffTemp, Size);                                 // 把新数据,从临时缓存中,复制到xUSART1.ReceiveData[]
        HAL_UARTEx_ReceiveToIdle_DMA(&huart1, xUSART1.BuffTemp, sizeof(xUSART1.BuffTemp));   // 再次开启DMA空闲中断
    }
}
 
/* USER CODE END 4 */

代码解释

xUSART1.receiveNUM = Size;

进入回调函数后,代码会把接收到的字节数存入xUSART1.receiveNUM中,由此我们可以通过判断receiveNUM>0,就知道接收到新的一帧数据

memset(xUSART1.receiveData, 0, sizeof(xUSART1.receiveData));

void *memset(void *s, int c, size_t n);

参数说明:

  • s:指向要设置的内存块的指针。
  • c:要设置的值,会转换为无符号字符(unsigned char)后赋值给内存块。
  • n:要设置的字节数。

返回值:

  • memset 返回指向内存块 s 的指针。

作用:把xUSART1.receiveData里的内容清除

memcpy(xUSART1.receiveData, xUSART1.BuffTemp, Size);

void *memcpy(void *dest, const void *src, size_t n);

参数说明:

  • dest:指向目标内存地址的指针,数据将被复制到这里。
  • src:指向源内存地址的指针,数据将从这里复制。
  • n:要复制的字节数。

返回值:

  • memcpy 返回指向目标内存块 dest 的指针。

作用:把接收缓冲区(xUSART1.BuffTemp)的数据转移到xUSART1.receiveData

HAL_UARTEx_ReceiveToIdle_DMA(&huart1, xUSART1.BuffTemp, sizeof(xUSART1.BuffTemp));

再次开启DMA空闲中断,进入接收状态。

再次调用的原因是DMA的中断服务函数里会关闭DMA,只会接收一次。

这样就可以让DMA接收不断的工作

在CubeMX配置中,DMA有一个选项 :Mode的circular, 可以让DMA进行连续地的工作,接收完成后,无需在回调函数里再次开启DMA 。但是,目前的CubeMX版本(V6.10),这个参数的选择,会使我们上面的DMA接收与发送,相冲突。那我们二选一好了,自行手工调用。

4.接收测试

在main.c的while函数里,编写代码

  while (1)
  {
    /* USER CODE END WHILE */

    /* USER CODE BEGIN 3 */
        if (xUSART1.receiveNUM)                                                   // 判断字节数
        {
            printf("\r<<<<< USART1 接收到一帧数据 \r");                           // 提示
            printf("ASCII : %s\r", (char *)xUSART1.receiveData);                 // 显示数据,以ASCII方式显示,即以字符串的方式显示
            printf("字节数:%d \r", xUSART1.receiveNUM);                         // 显示字节数
            printf("\r\r");                                                     // 显示换行
            xUSART1.receiveNUM = 0;                                            // 清0接收标记
        }
  }
  /* USER CODE END 3 */

编译,烧录

打开串口助手,配置好串口输出

在这里插入图片描述

五.Modbus协议编写—从机模式

1.创建一个结构体管理变量

创建Modbus.c和Modbus.h文件

在Modbus.h中创建结构体

typedef struct                        /* 声明一个变量,用于管理变量 */
{
    uint8_t address;                  /* 从机基地址 */
    uint8_t send[100];                /* 发送缓冲区 */
    uint8_t receive[100];             /* 接收缓冲区 */
} modbus_TypeDef;

extern modbus_TypeDef Modbus;         /* 声明modbus的结构体 */

在这里插入图片描述

2.编写Modbus.c代码

创建一个数组用作主机读数据和写数据时操作的寄存器

modbus_TypeDef Modbus;    //定义结构体
/* 寄存器数据 */
uint16_t Reg[] = {0x0001,
                  0x0003,
                  0x0000,
                  0x0001,
                  0x0001,
                  0x0001,
                  0x0001,
                  0X0001
                 };

作为从机时的地址

/* modbus初始化*/
void Modbus_Init(void)
{
    Modbus.address = 0x01;          /* 基地址位01 */
}


事件处理函数

(1)判断要读取的寄存器数是否小于定义的寄存器个数

(2)判断函数先判断校验码是否正确

(3)判断基地址是否相同

(4)判断功能码进入对应的函数

void Modbus_processing(void)
{
    uint16_t crc,rccrc,Reglen;                                                             //定义两个获取接收和计算校验码的变量
    crc = Modbus_CRC16(&Modbus.receive[0],xUSART1.receiveNUM-2);                            //获取到接收的数据进行计算
    rccrc = (Modbus.receive[xUSART1.receiveNUM-2]<<8) + Modbus.receive[xUSART1.receiveNUM-1];//获取接收数组的校验位
    Reglen = (Modbus.receive[4]<<8)+Modbus.receive[5];                                     //要读取寄存器个数
    if(Reglen>9)                                                                           //如果要获取的个数超过寄存器数量就不会应答
    {
        return;
    }
    if(crc == rccrc)                                                                       //校验码相同才可以进入
    {
        if(Modbus.receive[0] == Modbus.address)                                            //判断基地址
          {
           switch(Modbus.receive[1])                                                       //判断功能码
           {
               case 3:      Function_03();  break;
               case 6:      Function_06();  break;
               case 16:     break;
           }
          }
          else if(Modbus.receive[0] == 0)//基地址不一样返回空
          {
          
          }
    }
}
功能码0x03 读取对应的寄存器数据

在这里插入图片描述

/* 功能03:主机读取从机 */
void Function_03(void)
{
    uint16_t Regadd, Reglen,crc;
    uint8_t i,j;
    i = 0;
    Regadd = (Modbus.receive[2]<<8)+Modbus.receive[3];//要读取寄存器的首地址
    Reglen = (Modbus.receive[4]<<8)+Modbus.receive[5];//要读取寄存器个数
    //开始打包数据包
    Modbus.send[i++] = Modbus.address;                //ID号
    Modbus.send[i++] = 0x03;                          //功能码
    Modbus.send[i++] = ((Reglen*2)%256);              //返回字节个数
    for(j=0;j<Reglen;j++)
    {
        Modbus.send[i++] = Reg[Regadd+j]>>8;          //寄存器数据的高位;因为寄存器是16位,所以要分成2个8位发送
        Modbus.send[i++] = Reg[Regadd+j]%256;         //寄存器数据的地位
    }
    crc = Modbus_CRC16(Modbus.send,i);                //计算校验码;同理校验码是16位
    Modbus.send[i++] = crc>>8;                        //返回校验码高位
    Modbus.send[i++] = crc%256;                       //返回校验码地位
    //打包好数据开始发送
    HAL_UART_Transmit_DMA (&huart1,(uint8_t*)Modbus.send,i);       // 发送方式DMA

}
功能码0x06 写入对应的寄存器数据

在这里插入图片描述

/* 功能06:主机写入从机 */
void Function_06(void)
{
    uint16_t Regadd,crc,val;                          //地址;验证码;要修改的值;
    uint16_t i;
    i = 0;
    Regadd = (Modbus.receive[2]<<8)+Modbus.receive[3];      //获取要修改的地址
    val = (Modbus.receive[4]<<8)+Modbus.receive[5];         //获取修改的数值
    Reg[Regadd] = val;                                //修改数值
    //开始打包回应包
    Modbus.send[i++] = Modbus.address;                //从机地址
    Modbus.send[i++] = 0x06;                          //功能码
    Modbus.send[i++] = Regadd>>8;                     //返回写入地址
    Modbus.send[i++] = Regadd%256;
    Modbus.send[i++] = val>>8;                        //返回写入数值
    Modbus.send[i++] = val%256;
    crc = Modbus_CRC16(Modbus.send,i);                //校验码
    Modbus.send[i++] = crc>>8;                        //返回校验码高位
    Modbus.send[i++] = crc%256;                       //返回校验码地位
    //数据打包完开始发回
    HAL_UART_Transmit_DMA (&huart1,(uint8_t*)Modbus.send,i);       // 发送方式DMA
}

在modbus.h中声明函数

在这里插入图片描述

在main.c添加函数

#include "modbus.h" //头文件



Modbus_Init(); //初始化代码




//替换掉原来的中断处理函数

void HAL_UARTEx_RxEventCallback(UART_HandleTypeDef *huart, uint16_t Size)
{
    if (huart == &huart1)                                                                    // 判断串口
    {
        __HAL_UNLOCK(huart);                                                                 // 解锁串口状态
 
        xUSART1.receiveNUM  = Size;                                                          // 把接收字节数,存入结构体xUSART1.ReceiveNum
        memset(Modbus.receive, 0, sizeof(Modbus.receive));                         // 清除Modbus.receive前一帧接收到的数据
        memset(xUSART1.receiveData, 0, sizeof(xUSART1.receiveData));                         // 清除xUSART1.ReceiveData前一帧接收到的数据
        memcpy(xUSART1.receiveData, xUSART1.BuffTemp, Size);                                 // 把新数据,从临时缓存中,复制到xUSART1.ReceiveData[]
        memcpy(Modbus.receive, xUSART1.BuffTemp, Size);// 把新数据,从临时缓存中,复制到Modbus.receive
        HAL_UARTEx_ReceiveToIdle_DMA(&huart1, xUSART1.BuffTemp, sizeof(xUSART1.BuffTemp));   // 再次开启DMA空闲中断; 每当接收完指定长度,或者产生空闲中断时,就会来到这个
    }
}

在这里插入图片描述

在这里插入图片描述

注释掉测试USART1的代码 把Modbus处理函数添加在while中

在这里插入图片描述

3.测试Modbus协议

1.功能码0x03

读取指定个数寄存器的数据

这里是查看8个

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

2.功能码0x06

把6写入第5个寄存器中

在这里插入图片描述

在这里插入图片描述

标签:DMA,HAL,UART,串口,Modbus,从机,xUSART1
From: https://blog.csdn.net/2402_84849283/article/details/142520108

相关文章

  • modbus 的 输入和输出
    modbus的输入和输出1.输入线圈输出线圈2.输入寄存器输出寄存器保持寄存器什么是线圈?线圈就是一种只能有0和1(或者开和关)两种状态的一种寄存器俗称线圈寄存器它一般用来表示或者控制设备元器件的开或关的状态在PLC中一般包括两种线圈:输入线圈和输出线......
  • 推荐一款Modbus转OPC UA协议软件
    在很多工业自动化领域OPCUA协议使用非常广泛,而很多PLC或其他控制系统RFID控制机、视觉控制器等采用Modbus协议比较多,这就存在不同系统因为使用不同协议而无法交付,下面我介绍一款软件可以完美解决Modbus和OPCUA协议无法通信问题。该软件是一款国内公司开发的纯国产软件,下载地......
  • modbus设备数据 转 profinet IO项目案例
    目录1案例说明12VFBOX网关工作原理13准备工作24设置网关采集MODBUS从站数据25用PROFINETIO协议转发数据86案例总结101案例说明设置网关采集Modbus设备数据把采集的数据转成profinetIO协议转发给其他系统。2VFBOX网关工作原理VFBOX网关是协议转换网关,是把一......
  • 用Podman搭建LAMP开发环境的容器(六) -- 使用普通用户
    上接:用Podman搭建LAMP开发环境的容器(五)–端口转发现在这个容器还是直接用root用户跑的。虽然说如果只是开发环境的话,而且是一个虚拟机容器,直接用root用户工作应该也问题不大。不过我还是想在一般情况下用普通用户。首先我要改containerfile文件的脚本,来创建一个普通......
  • ModbusRTU通信协议报文剖析
    前言大家好!我是付工。前面给大家介绍了Modbus协议的应用层面。终于有人把Modbus说明白了那么,今天跟大家聊聊关于Modbus协议报文的那些事。一、真实案例前段时间有个粉丝朋友,让我帮他解决一个问题。这个粉丝朋友是负责Modbus主站调试的。项目背景:这是一个船舶的项目,主站是一个......
  • 基于旗芯微FC4150系列的DMA详解
    1.概述DMA的中文名称是直接内存访问,它不需要CPU的参与,实现数据传输的技术(但是也会占用总线带宽,所以有时候使用DMA虽然会降低CPU负载,但提高访问数据速度并不高)。1.1旗芯微FC4150系列芯片DMA特征·所有数据的传输都是从源地址写入到目标地址,源地址和目标地址以及传输大小都是......
  • 591从机库ota
    一.IAP修改点:1:LD文件修改起始地址为0,长度为8k,2:启动文件修改指向地址为库的起始地址48k,即0xC000。3:ota.h中修改各部分起始地址和大小,4.预编译宏中添加库起始地址,5.添加新的mcu.c文件,屏蔽校准函数,目的是节省flash空间。 二:app需修改处:1.LD文件2.启动文件3.预编译......
  • 揭秘 ARMxy 嵌入式控制器的 ModbusTCP 通信协议实战案例
    引言随着工业4.0概念的普及,越来越多的企业开始寻求将传统设备与现代信息技术相融合的方法,以提升生产效率和管理水平。在这个过程中,ModbusTCP作为一种成熟的工业通信协议,因其简单易用、兼容性好而在工业自动化领域得到广泛应用。与此同时,Node-Red作为一种开放源码的可视化编程工具,......
  • 用Podman搭建LAMP开发环境的容器(五) -- 端口转发
    上接:用Podman搭建LAMP开发环境的容器(四)–使用VSCode今天接着折腾前几天的VSCode配置。虽然现在看着好像是差不多了,也可以在VSCode上面编写代码了。但是还有一个很大的问题:容器中Apache服务的端口并没有映射到本地,所以现在用浏览器不能访问我写的页面,也不能访问phpMyAdmi......
  • RS-485通信与Modbus协议概念介绍
    RS485通信1、实际上在RS485之前RS232就已经诞生,但是RS232也有不足:1)接口的信号电平值较高,达到十几伏,容易损坏接口电路的芯片,而且和TTL电平不兼容,因此和单片机电路接起来的话必须加转换电路。2)接口使用的信号线与其他设备形成共地模式的通信,这种共地模式传输容易产生干扰,并且......