基于STM32+串口空闲+DMA+Modbus_RTU从机的实验

一:MODBUS简介

modbus是一种通讯协议,是最常用的串口协议,标准的数据帧格式如下

Modbus数据帧格式

地址域:

第一个字节,每个从机都有具有唯一的地址码,并且响应回送均以各自的地址码开始。主机发送的地址码表明将发送到的从机地址,而从机发送的地址码表明回送的从机地址,地址码为0时是广播模式

功能码:

通讯传送的第二个字节。作为主机请求发送,通过功能码告诉从机执行什么动作。作为从机响应,从机发送的功能码与从主机发送来的功能码一样,并表明从机已响应主机进行操作。如果从机发送的功能码的最高位为1(比如功能码大与此同时127),则表明从机没有响应操作或发送出错。

数据:

数据区是根据不同的功能码而不同。数据区可以是实际数值、设置点、主机发送给从机或从机发送给主机的地址。

差错校验:

使用 CRC码,它是二字节的错误检测码。

部分功能码:

功能码功能码意义
0X03读取功能码
0X10多字节写入功能码

 本次实验使用这两个功能码

功能码详解

主设备发送读取0x03指令 

0xXX0x030xXXXX0xXXXX0xXXXX
设备地址指令码从设备起始地址读取个数校验

从设备回复读取0x03指令

0xXX0x030xXX0xXXXX*n0xXXXX
设备地址指令码返回的字节个数返回的数据校验

主设备发送多个写入0x10指令

0xXX0x100xXXXX0xXXXX0xXX0xXXXX*n0xXXXX
设备地址指令码从设备起始地址写入几个寄存器数写入个数写入个数校验

从设备回复写入0x10指令

0xXX0x100xXX0xXXXX0xXXXX
设备地址指令码从设备起始地址修改数量校验

二:串口

本次实验使用串口DMA和串口空闲中断和串口发送完成中断.配合DMA完成主机的接受和从机的相应.使用的是STM32F103RCT6单片机,串口一的DMA通道如下图表,串口发送使用的是DMA通道4,接受使用通道5

串口空闲中断需要注意的是,产生中断后,由软件清除该位,可以定义一个变量去清楚,如:uint8_t clearIDLE;clearIDLE = USART1->SR;clearIDLE = USART1->DR,记住顺序不能反先读SR再读DR

三:DMA

直接存储器存取(DMA)用来提供在外设和存储器之间或者存储器和存储器之间的高速数据传
输。无须CPU干预,数据可以通过DMA快速地移动,这就节省了CPU的资源来做其他操作.可编程的数据传输数目:最大为65535.

主机发送数据后,由串口DMA接受,接受到数据后,用于解析Modbus协议,根据解析后的协议做出相关功能,如:读写.并且接受完毕后则打开DMA发送,从机发送拼接好的数据帧,发送完毕后这关闭发送DMA

四:代码部分

头文件部分:

#ifndef MODBUS_H
#define MODBUS_H
#include "stm32f10x.h"                  // Device header

typedef struct{
    uint8_t dev_addr;          //从机头码
    uint8_t len;               //接受数据长度

    uint8_t receive_data[256]; //接受的数据数组
    uint8_t receive_flag;      //接受使能

    uint8_t send_data[256];    //发送数据数组
    uint8_t send_flag;         //发送使能

    uint16_t rcrc;             //接受到的crc
    uint16_t crc;              //计算接受到的crc
}Modbus_t;

void Serial_Init(void);
void SerialDMA_Init(void);

void Clean_DMA(void);

void Serial_DMA_Send(uint8_t *arr,uint8_t len);

void Modbus_Init(Modbus_t *modbusdata);
uint16_t Modbus_CRC16(uint8_t *ch,uint8_t len);
void Modbus_EventLoop(Modbus_t *modbusdata);
void Modbus_Read(Modbus_t *modbusdata);
void Modbus_Write(Modbus_t *modbusdata);

void Modbus_CRCRERR(Modbus_t *modbusdata);
void Modbus_ADDRERR(Modbus_t *modbusdata);

#endif

串口和DMA部分:

Modbus_t mod    //定义一个全局变量用于储存Modbus数据

//以下数组用于储存模拟的从机数据
uint16_t Reg[] = {0x6677,0x8899,0x99ff,0xaabb,0xbbcc,0xccdd,0xabcd,0xdcef,0xffff,0x7fff,
                  0x6789,0xdddd,0xeeee,0xfefe,0xaaaa,0xbbbb,0xcccc,0x1010,0x1111,0x011f};

/*
*串口初始化 波特率 奇偶位 流控位 数据位等
*/
void Serial_Init(void)
{
	RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE);
    RCC_APB2PeriphClockCmd(RCC_APB2Periph_USART1, ENABLE);

    GPIO_InitTypeDef serialgpio = {
        .GPIO_Mode = GPIO_Mode_AF_PP,
        .GPIO_Pin = GPIO_Pin_9,
        .GPIO_Speed = GPIO_Speed_50MHz,
    };
    GPIO_Init(GPIOA,&serialgpio);

    serialgpio.GPIO_Mode = GPIO_Mode_IN_FLOATING;
    serialgpio.GPIO_Pin = GPIO_Pin_10;
    GPIO_Init(GPIOA,&serialgpio);

    USART_InitTypeDef serial = {
        .USART_BaudRate = 115200,
        .USART_HardwareFlowControl = USART_HardwareFlowControl_None,
        .USART_Mode = USART_Mode_Rx | USART_Mode_Tx,
        .USART_Parity = USART_Parity_No,
        .USART_StopBits = USART_StopBits_1,
        .USART_WordLength = USART_WordLength_8b,
    };
    USART_Init(USART1,&serial);
    //串口空闲中断使能
    USART_ITConfig(USART1,USART_IT_IDLE,ENABLE);
    //串口发送完成使能
    USART_ITConfig(USART1,USART_IT_TC,ENABLE);
    USART_Cmd(USART1,ENABLE);

    //串口中断配置
    NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2);
    NVIC_InitTypeDef serialnvic = {
        .NVIC_IRQChannelPreemptionPriority = 1,
        .NVIC_IRQChannelSubPriority = 1,
        .NVIC_IRQChannelCmd = ENABLE,
        .NVIC_IRQChannel = USART1_IRQn,
    };
    NVIC_Init(&serialnvic);

}

/*
* 串口DMA配置 
* 外设地址 外设数据长度 外设地址是否自增
* 内存地址 内存地址长度 内存地址是否自增
* DMA缓冲区大小 DMA优先级 DMA模式 DMA数据传输方向 DMA是否开启内存到内存
*/
void SerialDMA_Init(void)
{
    RCC_AHBPeriphClockCmd(RCC_AHBPeriph_DMA1,ENABLE);

    DMA_DeInit(DMA1_Channel5);    
    DMA_InitTypeDef rxdma = {
        .DMA_PeripheralBaseAddr = USART1_BASE + 0x04, //串口外设地址 与可以直接写0x40013804或(u32)&USART1->DR
        .DMA_PeripheralDataSize = DMA_PeripheralDataSize_Byte, //外设数据长度 8位
        .DMA_PeripheralInc = DMA_PeripheralInc_Disable, //外设地址不自增
        .DMA_MemoryBaseAddr = (u32)&mod.receive_data, //内存地址
        .DMA_MemoryDataSize = DMA_MemoryDataSize_Byte, //内存数据长度 8位
        .DMA_MemoryInc = DMA_MemoryInc_Enable, //内存地址自增
        .DMA_BufferSize = 256,                //dma缓冲区大小
        .DMA_Priority = DMA_Priority_High,    //优先级高
        .DMA_Mode = DMA_Mode_Normal,          //不开启循环模式
        .DMA_DIR = DMA_DIR_PeripheralSRC,     //数据方向为 外设-->内存 及USART1 --> mod.receive_data
        .DMA_M2M = DMA_M2M_Disable,        //关闭内存到内存
    };
    DMA_Init(DMA1_Channel5,&rxdma);

    DMA_DeInit(DMA1_Channel4);
    DMA_InitTypeDef txdma = {
        .DMA_PeripheralBaseAddr = USART1_BASE + 0x04,
        .DMA_PeripheralDataSize = DMA_PeripheralDataSize_Byte,
        .DMA_PeripheralInc = DMA_PeripheralInc_Disable,
        .DMA_MemoryBaseAddr = (u32)&mod.send_data,
        .DMA_MemoryDataSize = DMA_MemoryDataSize_Byte,
        .DMA_MemoryInc = DMA_MemoryInc_Enable,
        .DMA_BufferSize = 0,                    //dma缓冲区为0,是因为发送数据由主机指令来动态分配
        .DMA_Priority = DMA_Priority_High,
        .DMA_Mode = DMA_Mode_Normal,
        .DMA_DIR = DMA_DIR_PeripheralDST,    //数据方向 因为是发送 所以为 内存到外设 及 mod.send_data --> USART1
        .DMA_M2M = DMA_M2M_Disable,
    };
    DMA_Init(DMA1_Channel4,&txdma);

    USART_DMACmd(USART1,USART_DMAReq_Rx | USART_DMAReq_Tx,ENABLE); //开启串口DMA数据发送和接受

    DMA_Cmd(DMA1_Channel4,DISABLE); //关闭DMA发送 
    DMA_Cmd(DMA1_Channel5,ENABLE);    //开启DMA接受
}

 Modbus部分:

/*
* modbus初始化
*/
void Modbus_Init(Modbus_t *modbusdata)
{
    modbusdata->dev_addr = 0x02; //从机地址为0x02
    modbusdata->receive_flag = 0;
    modbusdata->len = 0;
    modbusdata->send_flag = 0;

    Serial_Init();
    SerialDMA_Init();
}
/*
*CRC校验
*/
uint16_t Modbus_CRC16(uint8_t *ch,uint8_t len)
{
	uint16_t crc = 0xffff,code = 0xa001;
	char i,j;
	for(i = 0;i< len;i++){
		crc ^= ch[i]; 
		for(j = 0;j < 8;j++){
			if(crc&1){
				crc>>=1;
				crc^=code;
			}else{
				crc>>=1;
			}
		}
	}	
	return crc;  
}
/*
*modbus事件轮询
*/
void Modbus_EventLoop(Modbus_t *modbusdata)
{
    if(modbusdata->receive_flag == 0) //判断是否接受完成
        return;
    if(modbusdata->receive_data[0] == modbusdata->dev_addr) //判断头码
    {
        modbusdata->rcrc = modbusdata->receive_data[modbusdata->len-1];
        modbusdata->rcrc<<=8;
        modbusdata->rcrc |= modbusdata->receive_data[modbusdata->len-2];
        modbusdata->crc = Modbus_CRC16(modbusdata->receive_data,modbusdata->len-2);

        if(modbusdata->crc == modbusdata->rcrc) //判断CRC
        {
            switch (modbusdata->receive_data[1]) //判断功能码
            {
            case 0x03:Modbus_Read(modbusdata); //功能码事件
                break;
            case 0x10:Modbus_Write(modbusdata); //功能码事件
                break;
            default:
                break;
            }
        }
        else
        {
            Modbus_CRCRERR(modbusdata); //CRC错误帧
        }
    }
    else
    {
        Modbus_ADDRERR(modbusdata); //头码错误帧
    }
    modbusdata->receive_flag = 0;
    modbusdata->send_flag = 0;
}

/*
*清除接收通道,让DMA从头开始计数,如果不清除,每次接收完成之后为了使下次的接收是从内存中的下标0
*存储,需要重新写入要传输的数据数量,否则下次直接接着上次传输的位置开始接收存储,发送时也一样
*/
void Clean_DMA(void)
{
    DMA_Cmd(DMA1_Channel5,DISABLE);
    DMA_SetCurrDataCounter(DMA1_Channel5,sizeof(mod.receive_data));
    DMA_Cmd(DMA1_Channel5,ENABLE);
}

/*
*发送缓冲区数据
*/
void Serial_DMA_Send(uint8_t *arr,uint8_t len)
{
    if(len == 0)
        return;
    while(DMA_GetCurrDataCounter(DMA1_Channel4));
    DMA_Cmd(DMA1_Channel4,DISABLE);
    DMA_SetCurrDataCounter(DMA1_Channel4,len);
    DMA_Cmd(DMA1_Channel4,ENABLE);    
}


//以下为功能码事件函数 和 错误码函数
void Modbus_Read(Modbus_t *modbusdata)
{
    uint8_t i,j;
    uint16_t crc,start_addr,read_len;

    start_addr = modbusdata->receive_data[2];
    start_addr <<=8;
    start_addr |= modbusdata->receive_data[3];

    read_len = modbusdata->receive_data[4];
    read_len <<=8;
    read_len |= modbusdata->receive_data[5];

    i=0;
    modbusdata->send_data[i++] = modbusdata->dev_addr;
    modbusdata->send_data[i++] = 0x03;
    modbusdata->send_data[i++] = (read_len * 2)%256;
    for(j=0;j<read_len;j++)
    {
        modbusdata->send_data[i++] = Reg[start_addr+j]>>8;
        modbusdata->send_data[i++] = Reg[start_addr+j];
    }
    crc = Modbus_CRC16(modbusdata->send_data,i);
    modbusdata->send_data[i++] = crc>>8;
    modbusdata->send_data[i++] = crc;

    Serial_DMA_Send(modbusdata->send_data,i);
}

void Modbus_Write(Modbus_t *modbusdata)
{
    uint8_t i;
    uint16_t crc,start_addr,write_num;

    start_addr = modbusdata->receive_data[2];
    start_addr <<=8;
    start_addr |= modbusdata->receive_data[3];

    write_num = modbusdata->receive_data[4];
    write_num <<=8;
    write_num |= modbusdata->receive_data[5];

    for(i=0;i<write_num;i++)
    {
        Reg[start_addr+i] = modbusdata->receive_data[7+i*2];
        Reg[start_addr+i] <<=8;
        Reg[start_addr+i] |= modbusdata->receive_data[8+i*2];
    }
    modbusdata->send_data[0] = modbusdata->dev_addr;
    modbusdata->send_data[1] = 0x10;
    modbusdata->send_data[2] = start_addr>>8;
    modbusdata->send_data[3] = start_addr;
    modbusdata->send_data[4] = write_num>>8;
    modbusdata->send_data[5] = write_num;
    crc = Modbus_CRC16(modbusdata->send_data,6);
    modbusdata->send_data[6] = crc>>8;
    modbusdata->send_data[7] = crc;

    Serial_DMA_Send(modbusdata->send_data,8);
}

void Modbus_ADDRERR(Modbus_t *modbusdata)
{
    uint16_t crc;

    modbusdata->send_data[0] = 0x81;
    modbusdata->send_data[1] = 0x66;
    modbusdata->send_data[2] = 0x00;
    modbusdata->send_data[3] = 0x02;
    modbusdata->send_data[4] = modbusdata->receive_data[0]>>8;
    modbusdata->send_data[5] = modbusdata->receive_data[0];
    crc = Modbus_CRC16(modbusdata->send_data,6);
    modbusdata->send_data[6] = crc>>8;
    modbusdata->send_data[7] = crc;

    Serial_DMA_Send(modbusdata->send_data,8);
}

void Modbus_CRCRERR(Modbus_t *modbusdata)
{
    uint16_t crc;

    modbusdata->send_data[0] = 0x81;
    modbusdata->send_data[1] = 0x65;
    modbusdata->send_data[2] = modbusdata->rcrc>>8;
    modbusdata->send_data[3] = modbusdata->rcrc;
    modbusdata->send_data[4] = modbusdata->crc>>8;
    modbusdata->send_data[5] = modbusdata->crc;
    crc = Modbus_CRC16(modbusdata->send_data,6);
    modbusdata->send_data[6] = crc>>8;
    modbusdata->send_data[7] = crc;

    Serial_DMA_Send(modbusdata->send_data,8);

}

中断和主函数部分:

//主函数部分
extern Modbus_t mod;

int main(void)
{
	Modbus_Init(&mod);

	while (1)
	{
		Modbus_EventLoop(&mod);
	}
}

void USART1_IRQHandler(void)
{
    uint8_t clear;
    if(USART_GetITStatus(USART1,USART_IT_IDLE) == SET)
    {
        clear = USART1->SR;
        clear = USART1->DR;
        clear = clear;         //防止编译器警告
        
        mod.receive_flag = 1;  //接受完成标准位 置一
        mod.len = sizeof(mod.receive_data) - DMA_GetCurrDataCounter(DMA1_Channel5); //获取接受数据长度
        Clean_DMA();
    }
    if(USART_GetITStatus(USART1,USART_IT_TC) == SET)
    {
        USART_ClearITPendingBit(USART1,USART_IT_TC);
        mod.send_flag = 1;                //发送完成标准位 置一
        DMA_Cmd(DMA1_Channel4,DISABLE);
    }

}

测试结果:

功能码0x03:

功能码0x10:

  • 40
    点赞
  • 36
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
### 回答1: Freemodbus(Free Modbus)是一个开源的Modbus通信协议库,它提供了在嵌入式系统中实现Modbus通信协议的功能。DMA(Direct Memory Access)是一种用于数据传输的技术,通过DMA可以在不占用CPU资源的情况下实现高速数据传输。那么关于Freemodbus的DMA接收,可以简单描述如下: 在使用Freemodbus中实现DMA接收的过程中,首先需要配置DMA通道,并将接收数据的缓冲区地址指定给DMA。当设备接收到Modbus通信的数据时,数据会被直接传送到DMA所指定的缓冲区中。由于DMA的特性,数据传输过程中不需要CPU进行干预,可以充分利用CPU的空闲时间进行其他的任务处理。 在DMA接收数据后,我们可以通过相应的回调函数或中断来处理接收到的数据。可以根据需要解析Modbus通信协议的相关字段,以获取所需的数据内容。同时,还可以进一步处理和响应Modbus通信的命令。 使用Freemodbus的DMA接收,可以有效提高数据传输的效率,减轻CPU的负担。同时,由于使用了硬件DMA传输数据,可以减少数据传输的延迟,提高数据接收的可靠性。 需要注意的是,在使用Freemodbus的DMA接收时,需要了解并配置相关的硬件和参数,以确保数据的正确接收和处理。同时,还需要合理设计和布置系统资源,以充分发挥DMA技术的优势。 总而言之,Freemodbus的DMA接收是一种在嵌入式系统中实现高效Modbus通信的方法。通过配置DMA通道,并指定接收缓冲区地址,可以实现无CPU干预的高速数据接收。这种方式可以提高系统的实时性和稳定性,适用于对数据传输效率和可靠性有较高要求的应用场景。 ### 回答2: Freemodbus是一种开源的Modbus通信协议栈,它能够在多种嵌入式系统上实现Modbus通信功能。DMA(直接内存访问)是一种特殊的数据传输方式,它可以实现数据的高效率传输,减轻CPU的负担。 在Freemodbus中使用DMA接收数据,需要首先配置DMA通道的参数。通过设置DMA的源地址、目标地址和数据长度等参数,将接收到的数据传输到指定的缓冲区中。接收到的数据可以是Modbus从设备发送过来的Modbus数据帧。 在数据传输过程中,DMA控制器负责直接从外设读取数据,并将数据存储到指定的缓冲区中,而不需要CPU的介入。这样就能够实现数据的快速传输,提高系统的效率。 在Freemodbus中,通过配置串口的接收中断触发方式,当接收到完整的Modbus数据帧时,会触发DMA传输完成中断。在中断服务程序中,可以对接收到的数据进行处理,例如解析Modbus数据帧中的功能码和数据内容等。 使用DMA接收数据可以提高数据传输的效率,减少CPU的负担。在高速、大数据量的Modbus通信场景中,尤其适用。但需要注意的是,在使用DMA接收数据时,需要合理配置DMA的参数,确保数据传输的准确性和完整性。 总而言之,Freemodbus可以通过配置DMA实现数据的高速接收,提高系统的效率和性能。这为Modbus通信在嵌入式系统中的应用提供了更多的选择和改进的可能性。 ### 回答3: FreeModbus 是一种开源的 Modbus 通信协议栈,用于将数据在 Modbus 从站设备与主站设备之间进行传输和通信。DMA(直接内存访问)是一种数据传输方式,允许在不使用 CPU 的情况下,将数据直接传输到内存中。 对于 FreeModbus,DMA 接收是指在数据传输过程中,使用 DMA 控制器将接收到的数据直接传输到内存中,而无需 CPU 的干预。通过使用 DMA,可以显著提高数据传输的效率和速度,并减轻 CPU 的负担。 在 FreeModbus 中,DMA 接收需要进行一定的配置和设置。首先,需要配置 DMA 控制器以启用接收功能,并指定数据传输的源和目的地。然后,需要设置接收缓冲区的地址,并设置缓冲区的大小,以确保接收到的数据不会超出缓冲区的容量。 一旦 DMA 接收配置完成,当有数据从 Modbus 从站设备传输到主站设备时,DMA 控制器会自动将数据传输到指定的内存地址中。一旦数据传输完成,可以通过检查接收缓冲区中的数据来获取接收到的数据。 使用 DMA 接收可以有效地减少 CPU 在数据传输过程中的参与,提高整体系统的性能和效率。因此,在使用 FreeModbus 进行通信时,使用 DMA 接收是一种值得考虑的方法。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值