前言
单片机型号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的一些基本知识点:
- 同步与异步通信:
- 异步通信:数据传输不依赖于时钟信号,每个字符的开始和结束由起始位和停止位标识。
- 同步通信:数据传输依赖于时钟信号,可以更高效地传输数据。
- 数据格式:
- 起始位:标识数据传输的开始。
- 数据位:传输实际的数据。常见的数据位长度有8位或9位。
- 奇偶校验位(可选):用于错误检测。
- 停止位:标识数据传输的结束,可以是一个或两个位。
- 波特率:
- 波特率是数据传输的速率,通常以比特每秒(bps)来表示。波特率越高,数据传输速度越快。
- 两个要通信的设备的波特率一定要设置相同,我们常见的波特率是 4800、 9600、115200 等。
- 帧结构:
- USART通信的一帧数据包括起始位、数据位、可选的奇偶校验位和停止位。
- 通信模式:
- 点对点:两个设备之间的直接通信。
- 多点:多个设备通过总线连接,可以进行广播或选择性通信。
- 硬件接口:
- USART通常通过TX(发送)和RX(接收)引脚进行数据传输。
2).各USART所使用的引脚接口
STM32f103上有3 个USART,其他系列请查看芯片手册在这里插入图片描述
2.DMA
1).基础知识点
DMA(Direct Memory Access,直接内存访问)是一种允许某些硬件子系统在不占用CPU的情况下,直接访问系统内存的技术。在串口通信中,DMA可以提高数据传输的效率,因为它允许数据在串口和内存之间直接传输,而不需要CPU的介入。
以下是DMA串口通信的一些基本知识点:
- DMA控制器:
- DMA控制器是负责管理DMA传输的硬件组件。它控制数据从源地址到目标地址的传输。
- 数据传输:
- 在串口通信中,DMA可以自动从串口接收器(USART)接收数据,并将其存储到内存中,或者从内存中读取数据并发送到串口发送器。
- 中断:
- 尽管DMA允许数据传输不需要CPU的直接参与,但通常在传输完成后,DMA控制器会通过中断通知CPU。
- 缓冲区:
- DMA通常使用一个或多个缓冲区来存储接收或发送的数据。这些缓冲区可以是环形缓冲区,以支持连续的数据流。
- 传输速率:
- DMA可以支持高速数据传输,因为它不受CPU速度的限制。
- 错误处理:
- DMA控制器通常具备错误检测功能,如传输超时、数据错误等,并可以在检测到错误时通过中断通知CPU。
- 配置:
- DMA的配置包括设置源和目标地址、传输大小、传输方向(接收或发送)、传输速率等。
- 优先级:
- 在多通道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协议的数据帧格式通常包括以下部分:
- 设备地址(Device Address):用于标识从设备的唯一地址,范围是0到247,0通常作为广播地址。
- 功能码(Function Code):指示从设备要执行的动作,不同的功能码对应不同的操作,例如读取或写入寄存器。
- 数据域(Data Field):包含请求和响应参数的数据,具体内容取决于功能码。
- 校验(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).数据帧解析
- 设备地址(Device Address):通常为1个字节,用于标识网络上的从设备。主设备(Master)通常不使用地址。
- 功能码(Function Code):1个字节,定义了要执行的动作,如读取或写入寄存器。
- 数据域(Data Field):1个或多个字节,包含执行功能码所需的数据,例如寄存器地址、要写入的值等。
- 校验(Checksum):通常为2个字节的CRC(循环冗余校验),用于检测数据在传输过程中是否出现错误。
Modbus数据帧
设备地址 | 功能码 | 数据 | 校验 |
---|---|---|---|
1byte | 1byte | Nbyte | 2byte |
本历程只实现了03和06功能码
功能码
0x03:读取从机寄存器数据
主机发送
从机设备地址 | 功能码 | 起始地址 | 寄存器个数 | CRC校验码 |
---|---|---|---|---|
01 | 03 | xx xx | xx xx | xx xx |
从机回复
从机设备地址 | 功能码 | 数据长度 | 数据1 | 数据2 | 数据N | CRC校验码 |
---|---|---|---|---|---|---|
01 | 03 | xx xx | xx xx | xx xx | xx xx | xx xx |
注意:数据长度=要读取寄存器个数X2
例子:
Tx:主机
Rx:从机回复
0x06:向一个寄存器写入数据
主机发送
从机设备地址 | 功能码 | 起始地址 | 寄存器个数 | CRC校验码 |
---|---|---|---|---|
01 | 06 | xx xx | xx xx | xx xx |
从机回复
从机设备地址 | 功能码 | 起始地址 | 寄存器个数 | CRC校验码 |
---|---|---|---|---|
01 | 06 | xx xx | xx xx | xx 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)
参数:串口,接收到的字节个数
重写这个函数,对接收到的数据进行处理
说明:
- 该函数是中断回调函数
- 函数名不能改变,它在CubeMX生成时已经写好了各种函数调用、函数弱定义;注意:不要在同一个源文件中同时声明强定义和弱定义的同名函数,这会导致编译错误
- 不需要清除中断标志位,调用函数前会自己清除
- 只要产生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