前两天在研究STM32F103单片机的串口空闲中断时,突然想起来Modbus通信非常适合用空闲中断来处理。先看看Modbus RTU模式下的通信规范。
可以看到Modbus RTU通信模式下,数据的开始和结束是由空闲字符间隔时间来区分的,而STM32F103单片机自带串口空闲模式检测。
在通常情况下Modbus通信一帧数据的检测可以用时间判断,不停的去读取接收数据的长度是否发送变化,如果在一定时间内,接收数据的长度没有发生变化,就认为一帧数据结束完毕。
通过时间来判断一帧数据是否接收完成,会导致单片机不停的进入中断。而STM32F103单片机自带了空闲数据检测功能,这个功能是由单片机内部的硬件完成的,不需要程序去处理,这样单片机的就有机会去执行更多了任务了。为了让单片机在处理Modbus RTU通信时更简单快速,可以用 串口DMA加上空闲中断的方式来实现。
下面直接用代码说明具体的实现过程,首先初始化串口。
- #define UART2_DMA 1 //使用串口2 DMA传输
- #define CHECK_NONE_ONE_STOP 1 //无校验位 1个停止位 1有效 0 无效
- #define CHECK_NONE_TWO_STOP 0 //无校验位 2个停止位 1有效 0 无效
- #define CHECK_EVEN 0 //偶数校验 1有效 0 无效
- #define CHECK_ODD 0 //奇数校验 1有效 0 无效
- void uart2_init( u16 baud )
- {
- GPIO_InitTypeDef GPIO_InitStructure;
- USART_InitTypeDef USART_InitStructure;
- NVIC_InitTypeDef NVIC_InitStructure;
- RCC_APB2PeriphClockCmd( RCC_APB2Periph_GPIOA, ENABLE );
- RCC_APB1PeriphClockCmd( RCC_APB1Periph_USART2, ENABLE );
- GPIO_InitStructure.GPIO_Pin = GPIO_Pin_2;
- GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP; //推挽复用模式
- GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
- GPIO_Init( GPIOA, &GPIO_InitStructure );
- GPIO_InitStructure.GPIO_Pin = GPIO_Pin_3;
- GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IN_FLOATING; //浮空输入模式
- GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
- GPIO_Init( GPIOA, &GPIO_InitStructure );
- NVIC_InitStructure.NVIC_IRQChannel = USART2_IRQn;
- NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 0x02;
- NVIC_InitStructure.NVIC_IRQChannelSubPriority = 0x02;
- NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE;
- NVIC_Init( &NVIC_InitStructure );
- USART_InitStructure.USART_BaudRate = baud;
- USART_InitStructure.USART_WordLength = USART_WordLength_8b;
- USART_InitStructure.USART_StopBits = USART_StopBits_1;
- USART_InitStructure.USART_Parity = USART_Parity_No;
-
- #if(CHECK_EVEN == 1) //如果定义了偶校验 数据位长度要改为9位
- USART_InitStructure.USART_WordLength = USART_WordLength_9b;
- USART_InitStructure.USART_Parity = USART_Parity_Even;
- #endif
- #if(CHECK_ODD == 1) //如果定义了奇校验 数据位长度要改为9位
- USART_InitStructure.USART_WordLength = USART_WordLength_9b;
- USART_InitStructure.USART_Parity = USART_Parity_Odd;
- #endif
- #if(CHECK_NONE_ONE_STOP==1) //停止位为 一位
- USART_InitStructure.USART_StopBits = USART_StopBits_1;
- #endif
- #if(CHECK_NONE_TWO_STOP==1) //停止位为 两位
- USART_InitStructure.USART_StopBits = USART_StopBits_2;
- #endif
-
-
- USART_InitStructure.USART_Mode = USART_Mode_Rx | USART_Mode_Tx;
- USART_InitStructure.USART_HardwareFlowControl = USART_HardwareFlowControl_None;
- USART_Init( USART2, &USART_InitStructure );
-
- #if (UART2_DMA == 1)
- USART_ITConfig( USART2, USART_IT_IDLE, ENABLE ); //使能串口空闲中断
- USART_DMACmd( USART2, USART_DMAReq_Rx, ENABLE ); //使能串口2 DMA接收
- uartDMA_Init(); //初始化 DMA
- #else
- USART_ITConfig( USART2, USART_IT_RXNE, ENABLE ); //使能串口RXNE接收中断
- #endif
- USART_Cmd( USART2, ENABLE ); //使能串口2
-
- //RXNE中断和IDLE中断的区别?
- //当接收到1个字节,就会产生RXNE中断,当接收到一帧数据,就会产生IDLE中断。比如给单片机一次性发送了8个字节,就会产生8次RXNE中断,1次IDLE中断。
- }
复制代码
在Modbus通信时,需要对每个字节的数据进行校验,所以这里使用宏定义来初始化串口数据使用的是那种校验方式。如果需要偶校验就将CHECK_EVEN 设置为1,如果需要奇校验就将CHECK_ODD设置为1,如果不需要校验位,就将这两个都设置为0。在串口初始化的时候,需要开启空闲中断和DMA功能。
下来初始化 串口2的DMA功能
- void uartDMA_Init( void )
- {
- DMA_InitTypeDef DMA_IniStructure;
- RCC_AHBPeriphClockCmd( RCC_AHBPeriph_DMA1, ENABLE ); //使能DMA时钟
- DMA_DeInit( DMA1_Channel6 ); //DMA1通道6对应 USART2_RX
- DMA_IniStructure.DMA_PeripheralBaseAddr = ( u32 )&USART2->DR; //DMA外设usart基地址
- DMA_IniStructure.DMA_MemoryBaseAddr = ( u32 )dma_rec_buff; //DMA内存基地址
- DMA_IniStructure.DMA_DIR = DMA_DIR_PeripheralSRC; //数据传输方向,从外设读取发送到内存
- DMA_IniStructure.DMA_BufferSize = DMA_REC_LEN; //DMA通道的DMA缓存的大小 也就是 DMA一次传输的字节数
- DMA_IniStructure.DMA_PeripheralInc = DMA_PeripheralInc_Disable; //外设地址寄存器不变
- DMA_IniStructure.DMA_MemoryInc = DMA_MemoryInc_Enable; //数据缓冲区地址递增
- DMA_IniStructure.DMA_PeripheralDataSize = DMA_PeripheralDataSize_Byte; //外设以字节为单位搬运
- DMA_IniStructure.DMA_MemoryDataSize = DMA_MemoryDataSize_Byte; //数据缓冲区以字节为单位搬入
- DMA_IniStructure.DMA_Mode = DMA_Mode_Normal; //工作在正常缓存模式
- DMA_IniStructure.DMA_Priority = DMA_Priority_Medium; //DMA通道 x拥有中优先级
- DMA_IniStructure.DMA_M2M = DMA_M2M_Disable; //DMA通道x没有设置为内存到内存传输
- DMA_Init( DMA1_Channel6, &DMA_IniStructure );
- DMA_Cmd( DMA1_Channel6, ENABLE );
- }
复制代码
串口2的接收对应的是DMA的通道6,所以DMA的通道选择通道6,然后将串口2的数据寄存器作为外设基地址,将数组dma_rec_buff作为DMA的内存地址。外设地址不自增,内存地址自增。这样串口2每接收一个字节的数据,DMA就将接收到的数据存入数组dma_rec_buff中,同时指向数组的地址增加一位,这样串口2接收到的数据就会被一直存放在数组dma_rec_buff中。
这里要注意一点,数组的缓存区要大于串口2接收到的最长数据个数,否则如果接收到串口数据比较长,而数组比较小,数组中的数据就会被覆盖,导致接收到的数据错误。
接下来就是串口中断的实现了
- void USART2_IRQHandler( void )
- {
- u8 tem = 0;
- #if (UART2_DMA == 1) //如果使能了 UART2_DMA 则使用串口空闲中断和 DMA功能接收数据
- if( USART_GetITStatus( USART2, USART_IT_IDLE ) != RESET ) //空闲中断 一帧数据发送完成
- {
- USART_ReceiveData( USART2 ); //读取数据注意:这句必须要,否则不能够清除空闲中断标志位。
- DMA_Cmd( DMA1_Channel6, DISABLE ); //关闭 DMA 防止后续数据干扰
- uart2_rec_cnt = DMA_REC_LEN - DMA_GetCurrDataCounter( DMA1_Channel6 ); //DMA接收缓冲区数据长度减去当前 DMA传输通道中剩余单元数量就是已经接收到的数据数量
- copy_data( dma_rec_buff, uart2_rec_cnt ); //备份数据
- receiveOK_flag = 1; //置位数据接收完成标志位
- USART_ClearITPendingBit( USART2, USART_IT_IDLE ); //清除空闲中断标志位
- myDMA_Enable( DMA1_Channel6 ); //重新恢复 DMA等待下一次接收
- }
- #else //如果未使能 UART2_DMA 则通过常规方式接收数据 每接收到一个字节就会进入一次中断
- if( USART_GetITStatus( USART2, USART_IT_RXNE ) != RESET ) //接收中断
- {
- tem = USART_ReceiveData( USART2 );
- USART_SendData( USART2, tem );
- }
- #endif
- }
复制代码
串口进入空闲中断后,说明一帧数据已经接收完成。关闭DMA通道,开始处理数据。获取DMA中接收到的数据长度,然后将数据拷贝到备份数组中,然后置接收完成标志位。开启 DMA进行下一次数据接收。
在主程序中如果检测到了数据接收完成标志时,就开始处理接收到的数据。这样就可以将串口接收数据和处理数据分开,在处理上一笔数据的时候,就可以继续接收下一笔数据。提高了系统的实时性。
这里如果直接使用DMA接收数组去处理数据的话,有可能会出现,旧的一笔数据未处理完成时,DMA接收到了新的一笔数据,然后存储到了 DMA缓存数组中。这样在处理数据过程中,旧的数据就会被新的数据覆盖掉,导致系统异常。如果Modbus的通信速率没有那么快,在下一次数据来临之前,系统完全有时间处理完上一笔数据,那么直接使用DMA的缓存数据也是可以的。
不过上面这种将数据拷贝倒其他数据中处理的方法也是有风险的。比如第一笔数据还没处理完成,第二笔数据已经接收完成,此时进入串口空闲中断,系统依然会将第二笔数据拷贝到备份数组中去。这样备份数组中的数据依然会被覆盖。但是这种情况如果出现的话,就说明接收数据的频率高于系统处理数据的频率,系统本身就存在风险,需要重新设计。
接收数据完成后,就需要在主程序中调用数据处理函数。
- int main(void)
- {
- u8 j = 0;
-
- NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2);
-
- delay_init(); //延时函数初始化
- LED_Init(); //初始化与LED连接的硬件接口
- uart2_init(9600);
- while(1)
- {
- if( receiveOK_flag ) //一帧数据接收完成后开始处理数据
- {
- receiveOK_flag = 0;
- DisposeReceive(); //处理modbus通信
- }
-
- j++;
- if(j > 50)
- {
- j = 0;
- LED = !LED;
- }
- delay_ms(10);
- }
- }
复制代码
最后是Modbus的协议解析
- void DisposeReceive( void )
- {
- u16 CRC16 = 0, CRC16Temp = 0;
- if( data_backup[0] == SlaveID ) //地址等于本机地址 地址范围:1 - 32
- {
- CRC16 = App_Tab_Get_CRC16( data_backup, dataLen_backup - 2 ); //CRC校验 低字节在前 高字节在后 高字节为报文最后一个字节
- CRC16Temp = ( ( u16 )( data_backup[dataLen_backup - 1] << 8 ) | data_backup[dataLen_backup - 2] );
- if( CRC16 != CRC16Temp )
- {
- err = 4; //CRC校验错误
- }
- StartRegAddr = ( u16 )( data_backup[2] << 8 ) | data_backup[3];
- if( StartRegAddr > (HoldRegStartAddr + HoldRegCount - 1) )
- {
- err = 2; //起始地址不在规定范围内 00 - 07 1 - 8号通道
- }
- if( err == 0 )
- {
- switch( data_backup[1] ) //功能码
- {
- case 3: //读多个寄存器
- {
- Modbus_03_Slave();
- break;
- }
- case 6: //写单个寄存器
- {
- Modbus_06_Slave();
- break;
- }
- case 16: //写多个寄存器
- {
- Modbus_16_Slave();
- break;
- }
- default:
- {
- err = 1; //不支持该功能码
- break;
- }
- }
- }
- if( err > 0 )
- {
- SendBuf[0] = data_backup[0];
- SendBuf[1] = data_backup[1] | 0x80;
- SendBuf[2] = err; //发送错误代码
- CRC16Temp = App_Tab_Get_CRC16( SendBuf, 3 ); //计算CRC校验值
- SendBuf[3] = CRC16Temp & 0xFF; //CRC低位
- SendBuf[4] = ( CRC16Temp >> 8 ); //CRC高位
- uart2_Send( SendBuf, 5 );
- err = 0; //发送完数据后清除错误标志
- }
- }
- }
- /*
- 函数功能:读保持寄存器 03
- 主站请求报文: 0x01 0x03 0x0000 0x0001 0x840A 读从0开始的1个保持寄存器
- 从站正常响应报文: 0x01 0x03 0x02 0x09C4 0xBF87 读到的2字节数据为 0x09C4
- */
- void Modbus_03_Slave( void )
- {
- u16 RegNum = 0;
- u16 CRC16Temp = 0;
- u8 i = 0;
- RegNum = ( u16 )( data_backup[4] << 8 ) | data_backup[5]; //获取寄存器数量
- if( ( StartRegAddr + RegNum ) <= (HoldRegStartAddr + HoldRegCount) ) //寄存器地址+寄存器数量 在规定范围内 <=8
- {
- SendBuf[0] = data_backup[0]; //地址
- SendBuf[1] = data_backup[1]; //功能码
- SendBuf[2] = RegNum * 2; //返回字节数量
- for( i = 0; i < RegNum; i++ ) //循环读取保持寄存器内的值
- {
- SendBuf[3 + i * 2] = HoldReg[StartRegAddr * 2 + i * 2];
- SendBuf[4 + i * 2] = HoldReg[StartRegAddr * 2 + i * 2 + 1];
- }
- CRC16Temp = App_Tab_Get_CRC16( SendBuf, RegNum * 2 + 3 ); //获取CRC校验值
- SendBuf[RegNum * 2 + 3] = CRC16Temp & 0xFF; //CRC低位
- SendBuf[RegNum * 2 + 4] = ( CRC16Temp >> 8 ); //CRC高位
- uart2_Send( SendBuf, RegNum * 2 + 5 );
- }
- else
- {
- err = 3; //寄存器数量不在规定范围内
- }
- }
- /*
- 函数功能:写单个保持寄存器 06
- 主站请求报文: 0x01 0x06 0x0000 0x1388 0x849C 写0号寄存器的值为0x1388
- 从站正常响应报文: 0x01 0x06 0x0000 0x1388 0x849C 0号寄存器的值为0x1388
- */
- void Modbus_06_Slave( void )
- {
- u16 RegValue = 0;
- u16 CRC16Temp = 0;
- RegValue = ( u16 )( data_backup[4] << 8 ) | data_backup[5]; //获取寄存器值
- if( RegValue <= HoldMaxValue ) //寄存器值不超过1000
- {
- HoldReg[StartRegAddr * 2] = data_backup[4]; //存储寄存器值
- HoldReg[StartRegAddr * 2 + 1] = data_backup[5];
- SendBuf[0] = data_backup[0]; //地址
- SendBuf[1] = data_backup[1]; //功能码
- SendBuf[2] = data_backup[2]; //地址高位
- SendBuf[3] = data_backup[3]; //地址低位
- SendBuf[4] = data_backup[4]; //值高位
- SendBuf[5] = data_backup[5]; //值低位
- CRC16Temp = App_Tab_Get_CRC16( SendBuf, 6 ); //获取CRC校验值
- SendBuf[6] = CRC16Temp & 0xFF; //CRC低位
- SendBuf[7] = ( CRC16Temp >> 8 ); //CRC高位
- uart2_Send( SendBuf, 8 );
- }
- else
- {
- err = 3; //寄存器数值不在规定范围内
- }
- }
- /*
- 函数功能:写多个连续保持寄存器值 16
- 主站请求报文: 0x01 0x10 0x7540 0x0002 0x04 0x0000 0x2710 0xB731 写从0x7540地址开始的2个保持寄存器值 共4字节
- 从站正常响应报文: 0x01 0x10 0x7540 0x0002 0x5A10 写从0x7540地址开始的2个保持寄存器值
- */
- void Modbus_16_Slave( void )
- {
- u16 RegNum = 0;
- u16 CRC16Temp = 0;
- u8 i = 0;
- RegNum = ( u16 )( data_backup[4] << 8 ) | data_backup[5]; //获取寄存器数量
- if( ( StartRegAddr + RegNum ) <= (HoldRegStartAddr + HoldRegCount) ) //寄存器地址+寄存器数量 在规定范围内 <=8
- {
- for( i = 0; i < RegNum; i++ ) //存储寄存器设置值
- {
- HoldReg[StartRegAddr * 2 + i * 2] = data_backup[i * 2 + 7];
- HoldReg[StartRegAddr * 2 + 1 + i * 2] = data_backup[i * 2 + 8];
- }
- SendBuf[0] = data_backup[0]; //起始地址
- SendBuf[1] = data_backup[1]; //功能码
- SendBuf[2] = data_backup[2]; //地址高位
- SendBuf[3] = data_backup[3]; //地址低位
- SendBuf[4] = data_backup[4]; //寄存器数量高位
- SendBuf[5] = data_backup[5]; //寄存器数量低位
- CRC16Temp = App_Tab_Get_CRC16( SendBuf, 6 ); //获取CRC校验值
- SendBuf[6] = CRC16Temp & 0xFF; //CRC低位
- SendBuf[7] = ( CRC16Temp >> 8 ); //CRC高位
- uart2_Send( SendBuf, 8 );
- }
- else
- {
- err = 3; //寄存器数量不在规定范围内
- }
- }
复制代码
Modbus的执行在这里只实现了最常用的三个。
各个功能码的实现原理在这里就不说了,直接通过代码中和注释就可以看明白了。
下面贴出串口和modbus的完整代码
uart2.h
- #ifndef __UART2_H
- #define __UART2_H
- #include "sys.h"
- #define DMA_REC_LEN 50 //DMA数据接收缓冲区
- void uart2_init(u16 baud);
- void uartDMA_Init( void );
- void myDMA_Enable( DMA_Channel_TypeDef*DMA_CHx );
- void uart2_Send( u8 *buf, u16 len );
- void copy_data( u8 *buf, u16 len );
- #endif
复制代码
uart2.c
- //串口2空闲中断 + DMA数据传输
- #include "uart2.h"
- #define UART2_DMA 1 //使用串口2 DMA传输
- #define CHECK_NONE_ONE_STOP 1 //无校验位 1个停止位 1有效 0 无效
- #define CHECK_NONE_TWO_STOP 0 //无校验位 2个停止位 1有效 0 无效
- #define CHECK_EVEN 0 //偶数校验 1有效 0 无效
- #define CHECK_ODD 0 //奇数校验 1有效 0 无效
- u8 dma_rec_buff[DMA_REC_LEN] = {0};
- u16 uart2_rec_cnt = 0; //串口接收数据长度
- u8 data_backup[DMA_REC_LEN] = {0}; //数据备份
- u16 dataLen_backup = 0; //长度备份
- _Bool receiveOK_flag = 0; //接收完成标志位
- /*
- 空闲中断是什么意思呢?
- 指的是当总线接收数据时,一旦数据流断了,此时总线没有接收传输,处于空闲状态,IDLE就会置1,产生空闲中断;又有数据发送时,IDLE位就会置0;
- 注意:置1之后它不会自动清0,也不会因为状态位是1而一直产生中断,它只有0跳变到1时才会产生,也可以理解为上升沿触发。
- 所以,为确保下次空闲中断正常进行,需要在中断服务函数发送任意数据来清除标志位。
- */
- void uart2_init( u16 baud )
- {
- GPIO_InitTypeDef GPIO_InitStructure;
- USART_InitTypeDef USART_InitStructure;
- NVIC_InitTypeDef NVIC_InitStructure;
- RCC_APB2PeriphClockCmd( RCC_APB2Periph_GPIOA, ENABLE );
- RCC_APB1PeriphClockCmd( RCC_APB1Periph_USART2, ENABLE );
- GPIO_InitStructure.GPIO_Pin = GPIO_Pin_2;
- GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP; //推挽复用模式
- GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
- GPIO_Init( GPIOA, &GPIO_InitStructure );
- GPIO_InitStructure.GPIO_Pin = GPIO_Pin_3;
- GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IN_FLOATING; //浮空输入模式
- GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
- GPIO_Init( GPIOA, &GPIO_InitStructure );
- NVIC_InitStructure.NVIC_IRQChannel = USART2_IRQn;
- NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 0x02;
- NVIC_InitStructure.NVIC_IRQChannelSubPriority = 0x02;
- NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE;
- NVIC_Init( &NVIC_InitStructure );
- USART_InitStructure.USART_BaudRate = baud;
- USART_InitStructure.USART_WordLength = USART_WordLength_8b;
- USART_InitStructure.USART_StopBits = USART_StopBits_1;
- USART_InitStructure.USART_Parity = USART_Parity_No;
-
- #if(CHECK_EVEN == 1) //如果定义了偶校验 数据位长度要改为9位
- USART_InitStructure.USART_WordLength = USART_WordLength_9b;
- USART_InitStructure.USART_Parity = USART_Parity_Even;
- #endif
- #if(CHECK_ODD == 1) //如果定义了奇校验 数据位长度要改为9位
- USART_InitStructure.USART_WordLength = USART_WordLength_9b;
- USART_InitStructure.USART_Parity = USART_Parity_Odd;
- #endif
- #if(CHECK_NONE_ONE_STOP==1) //停止位为 一位
- USART_InitStructure.USART_StopBits = USART_StopBits_1;
- #endif
- #if(CHECK_NONE_TWO_STOP==1) //停止位为 两位
- USART_InitStructure.USART_StopBits = USART_StopBits_2;
- #endif
-
-
- USART_InitStructure.USART_Mode = USART_Mode_Rx | USART_Mode_Tx;
- USART_InitStructure.USART_HardwareFlowControl = USART_HardwareFlowControl_None;
- USART_Init( USART2, &USART_InitStructure );
-
- #if (UART2_DMA == 1)
- USART_ITConfig( USART2, USART_IT_IDLE, ENABLE ); //使能串口空闲中断
- USART_DMACmd( USART2, USART_DMAReq_Rx, ENABLE ); //使能串口2 DMA接收
- uartDMA_Init(); //初始化 DMA
- #else
- USART_ITConfig( USART2, USART_IT_RXNE, ENABLE ); //使能串口RXNE接收中断
- #endif
- USART_Cmd( USART2, ENABLE ); //使能串口2
-
- //RXNE中断和IDLE中断的区别?
- //当接收到1个字节,就会产生RXNE中断,当接收到一帧数据,就会产生IDLE中断。比如给单片机一次性发送了8个字节,就会产生8次RXNE中断,1次IDLE中断。
- }
- void uartDMA_Init( void )
- {
- DMA_InitTypeDef DMA_IniStructure;
- RCC_AHBPeriphClockCmd( RCC_AHBPeriph_DMA1, ENABLE ); //使能DMA时钟
- DMA_DeInit( DMA1_Channel6 ); //DMA1通道6对应 USART2_RX
- DMA_IniStructure.DMA_PeripheralBaseAddr = ( u32 )&USART2->DR; //DMA外设usart基地址
- DMA_IniStructure.DMA_MemoryBaseAddr = ( u32 )dma_rec_buff; //DMA内存基地址
- DMA_IniStructure.DMA_DIR = DMA_DIR_PeripheralSRC; //数据传输方向,从外设读取发送到内存
- DMA_IniStructure.DMA_BufferSize = DMA_REC_LEN; //DMA通道的DMA缓存的大小 也就是 DMA一次传输的字节数
- DMA_IniStructure.DMA_PeripheralInc = DMA_PeripheralInc_Disable; //外设地址寄存器不变
- DMA_IniStructure.DMA_MemoryInc = DMA_MemoryInc_Enable; //数据缓冲区地址递增
- DMA_IniStructure.DMA_PeripheralDataSize = DMA_PeripheralDataSize_Byte; //外设以字节为单位搬运
- DMA_IniStructure.DMA_MemoryDataSize = DMA_MemoryDataSize_Byte; //数据缓冲区以字节为单位搬入
- DMA_IniStructure.DMA_Mode = DMA_Mode_Normal; //工作在正常缓存模式
- DMA_IniStructure.DMA_Priority = DMA_Priority_Medium; //DMA通道 x拥有中优先级
- DMA_IniStructure.DMA_M2M = DMA_M2M_Disable; //DMA通道x没有设置为内存到内存传输
- DMA_Init( DMA1_Channel6, &DMA_IniStructure );
- DMA_Cmd( DMA1_Channel6, ENABLE );
- }
- //重新恢复DMA指针
- void myDMA_Enable( DMA_Channel_TypeDef*DMA_CHx )
- {
- DMA_Cmd( DMA_CHx, DISABLE ); //关闭DMA1所指示的通道
- DMA_SetCurrDataCounter( DMA_CHx, DMA_REC_LEN ); //DMA通道的DMA缓存的大小
- DMA_Cmd( DMA_CHx, ENABLE ); //DMA1所指示的通道
- }
- //发送len个字节
- //buf:发送区首地址
- //len:发送的字节数
- void uart2_Send( u8 *buf, u16 len )
- {
- u16 t;
- for( t = 0; t < len; t++ ) //循环发送数据
- {
- while( USART_GetFlagStatus( USART2, USART_FLAG_TC ) == RESET );
- USART_SendData( USART2, buf[t] );
- }
- while( USART_GetFlagStatus( USART2, USART_FLAG_TC ) == RESET );
- }
- //备份接收到的数据
- void copy_data( u8 *buf, u16 len )
- {
- u16 t;
- dataLen_backup = len; //保存数据长度
- for( t = 0; t < len; t++ )
- {
- data_backup[t] = buf[t]; //备份接收到的数据,防止在处理数据过程中接收到新数据,将旧数据覆盖掉。
- }
- }
- // Modbus RTU 通信协议
- //地址码 功能码 寄存器高位 寄存器低位 数据高位 数据低位 CRC高位 CRC低位
- //01 03 00 00 03 e8 xx xx
- //利用空闲中断接收串口不定长数据
- //串口接收一组数据结束后才会进入中断,在数据发送过程中,已经接收到的数据将会被存入DMA的缓冲区中
- void USART2_IRQHandler( void )
- {
- u8 tem = 0;
- #if (UART2_DMA == 1) //如果使能了 UART2_DMA 则使用串口空闲中断和 DMA功能接收数据
- if( USART_GetITStatus( USART2, USART_IT_IDLE ) != RESET ) //空闲中断 一帧数据发送完成
- {
- USART_ReceiveData( USART2 ); //读取数据注意:这句必须要,否则不能够清除空闲中断标志位。
- DMA_Cmd( DMA1_Channel6, DISABLE ); //关闭 DMA 防止后续数据干扰
- uart2_rec_cnt = DMA_REC_LEN - DMA_GetCurrDataCounter( DMA1_Channel6 ); //DMA接收缓冲区数据长度减去当前 DMA传输通道中剩余单元数量就是已经接收到的数据数量
- //uart2_Send( dma_rec_buff, uart2_rec_cnt ); //发送接收到的数据
- copy_data( dma_rec_buff, uart2_rec_cnt ); //备份数据
- receiveOK_flag = 1; //置位数据接收完成标志位
- USART_ClearITPendingBit( USART2, USART_IT_IDLE ); //清除空闲中断标志位
- myDMA_Enable( DMA1_Channel6 ); //重新恢复 DMA等待下一次接收
- }
- #else //如果未使能 UART2_DMA 则通过常规方式接收数据 每接收到一个字节就会进入一次中断
- if( USART_GetITStatus( USART2, USART_IT_RXNE ) != RESET ) //接收中断
- {
- tem = USART_ReceiveData( USART2 );
- USART_SendData( USART2, tem );
- }
- #endif
- }
复制代码
modbus.h
- #ifndef __MODBUS_H
- #define __MODBUS_H
- #include "sys.h"
- #define SlaveID 0x01 //从机地址
- void DisposeReceive( void );
- void Modbus_03_Slave(void);
- void Modbus_06_Slave(void);
- void Modbus_16_Slave(void);
- #endif
复制代码
modbus.c
- #include "modbus.h"
- #include "crc16.h"
- #include "uart2.h"
- #define HoldRegStartAddr 0x0000 //保持寄存器起始地址
- #define HoldRegCount 8 //保持寄存器数量
- #define HoldMaxValue 1000 //寄存器最大值
- extern u8 data_backup[DMA_REC_LEN]; //modbus接收到数据备份
- extern u16 dataLen_backup; //modbus接收数据长度备份
- u8 SendBuf[DMA_REC_LEN] = {0};
- u16 StartRegAddr = HoldRegStartAddr;
- u8 err = 0; //错误代码
- u8 HoldReg[HoldRegCount*2] = {1, 1, 2, 2, 3, 3, 4, 4, 5, 5, 6, 6, 7, 7, 8, 8}; //存储8路数据值
- /*
- 错误代码说明:
- 0x01 不是支持的功能码
- 0x02 起始地址不在规定范围内
- 0x03 寄存器数量不在规定范围内
- 0x04 数据校验错误
- */
- //处理接收到的数据
- // 接收: [地址][功能码][起始地址高][起始地址低][总寄存器数高][总寄存器数低][CRC低][CRC高]
- void DisposeReceive( void )
- {
- u16 CRC16 = 0, CRC16Temp = 0;
- if( data_backup[0] == SlaveID ) //地址等于本机地址 地址范围:1 - 32
- {
- CRC16 = App_Tab_Get_CRC16( data_backup, dataLen_backup - 2 ); //CRC校验 低字节在前 高字节在后 高字节为报文最后一个字节
- CRC16Temp = ( ( u16 )( data_backup[dataLen_backup - 1] << 8 ) | data_backup[dataLen_backup - 2] );
- if( CRC16 != CRC16Temp )
- {
- err = 4; //CRC校验错误
- }
- StartRegAddr = ( u16 )( data_backup[2] << 8 ) | data_backup[3];
- if( StartRegAddr > (HoldRegStartAddr + HoldRegCount - 1) )
- {
- err = 2; //起始地址不在规定范围内 00 - 07 1 - 8号通道
- }
- if( err == 0 )
- {
- switch( data_backup[1] ) //功能码
- {
- case 3: //读多个寄存器
- {
- Modbus_03_Slave();
- break;
- }
- case 6: //写单个寄存器
- {
- Modbus_06_Slave();
- break;
- }
- case 16: //写多个寄存器
- {
- Modbus_16_Slave();
- break;
- }
- default:
- {
- err = 1; //不支持该功能码
- break;
- }
- }
- }
- if( err > 0 )
- {
- SendBuf[0] = data_backup[0];
- SendBuf[1] = data_backup[1] | 0x80;
- SendBuf[2] = err; //发送错误代码
- CRC16Temp = App_Tab_Get_CRC16( SendBuf, 3 ); //计算CRC校验值
- SendBuf[3] = CRC16Temp & 0xFF; //CRC低位
- SendBuf[4] = ( CRC16Temp >> 8 ); //CRC高位
- uart2_Send( SendBuf, 5 );
- err = 0; //发送完数据后清除错误标志
- }
- }
- }
- /*
- 函数功能:读保持寄存器 03
- 主站请求报文: 0x01 0x03 0x0000 0x0001 0x840A 读从0开始的1个保持寄存器
- 从站正常响应报文: 0x01 0x03 0x02 0x09C4 0xBF87 读到的2字节数据为 0x09C4
- */
- void Modbus_03_Slave( void )
- {
- u16 RegNum = 0;
- u16 CRC16Temp = 0;
- u8 i = 0;
- RegNum = ( u16 )( data_backup[4] << 8 ) | data_backup[5]; //获取寄存器数量
- if( ( StartRegAddr + RegNum ) <= (HoldRegStartAddr + HoldRegCount) ) //寄存器地址+寄存器数量 在规定范围内 <=8
- {
- SendBuf[0] = data_backup[0]; //地址
- SendBuf[1] = data_backup[1]; //功能码
- SendBuf[2] = RegNum * 2; //返回字节数量
- for( i = 0; i < RegNum; i++ ) //循环读取保持寄存器内的值
- {
- SendBuf[3 + i * 2] = HoldReg[StartRegAddr * 2 + i * 2];
- SendBuf[4 + i * 2] = HoldReg[StartRegAddr * 2 + i * 2 + 1];
- }
- CRC16Temp = App_Tab_Get_CRC16( SendBuf, RegNum * 2 + 3 ); //获取CRC校验值
- SendBuf[RegNum * 2 + 3] = CRC16Temp & 0xFF; //CRC低位
- SendBuf[RegNum * 2 + 4] = ( CRC16Temp >> 8 ); //CRC高位
- uart2_Send( SendBuf, RegNum * 2 + 5 );
- }
- else
- {
- err = 3; //寄存器数量不在规定范围内
- }
- }
- /*
- 函数功能:写单个保持寄存器 06
- 主站请求报文: 0x01 0x06 0x0000 0x1388 0x849C 写0号寄存器的值为0x1388
- 从站正常响应报文: 0x01 0x06 0x0000 0x1388 0x849C 0号寄存器的值为0x1388
- */
- void Modbus_06_Slave( void )
- {
- u16 RegValue = 0;
- u16 CRC16Temp = 0;
- RegValue = ( u16 )( data_backup[4] << 8 ) | data_backup[5]; //获取寄存器值
- if( RegValue <= HoldMaxValue ) //寄存器值不超过1000
- {
- HoldReg[StartRegAddr * 2] = data_backup[4]; //存储寄存器值
- HoldReg[StartRegAddr * 2 + 1] = data_backup[5];
- SendBuf[0] = data_backup[0]; //地址
- SendBuf[1] = data_backup[1]; //功能码
- SendBuf[2] = data_backup[2]; //地址高位
- SendBuf[3] = data_backup[3]; //地址低位
- SendBuf[4] = data_backup[4]; //值高位
- SendBuf[5] = data_backup[5]; //值低位
- CRC16Temp = App_Tab_Get_CRC16( SendBuf, 6 ); //获取CRC校验值
- SendBuf[6] = CRC16Temp & 0xFF; //CRC低位
- SendBuf[7] = ( CRC16Temp >> 8 ); //CRC高位
- uart2_Send( SendBuf, 8 );
- }
- else
- {
- err = 3; //寄存器数值不在规定范围内
- }
- }
- /*
- 函数功能:写多个连续保持寄存器值 16
- 主站请求报文: 0x01 0x10 0x7540 0x0002 0x04 0x0000 0x2710 0xB731 写从0x7540地址开始的2个保持寄存器值 共4字节
- 从站正常响应报文: 0x01 0x10 0x7540 0x0002 0x5A10 写从0x7540地址开始的2个保持寄存器值
- */
- void Modbus_16_Slave( void )
- {
- u16 RegNum = 0;
- u16 CRC16Temp = 0;
- u8 i = 0;
- RegNum = ( u16 )( data_backup[4] << 8 ) | data_backup[5]; //获取寄存器数量
- if( ( StartRegAddr + RegNum ) <= (HoldRegStartAddr + HoldRegCount) ) //寄存器地址+寄存器数量 在规定范围内 <=8
- {
- for( i = 0; i < RegNum; i++ ) //存储寄存器设置值
- {
- HoldReg[StartRegAddr * 2 + i * 2] = data_backup[i * 2 + 7];
- HoldReg[StartRegAddr * 2 + 1 + i * 2] = data_backup[i * 2 + 8];
- }
- SendBuf[0] = data_backup[0]; //起始地址
- SendBuf[1] = data_backup[1]; //功能码
- SendBuf[2] = data_backup[2]; //地址高位
- SendBuf[3] = data_backup[3]; //地址低位
- SendBuf[4] = data_backup[4]; //寄存器数量高位
- SendBuf[5] = data_backup[5]; //寄存器数量低位
- CRC16Temp = App_Tab_Get_CRC16( SendBuf, 6 ); //获取CRC校验值
- SendBuf[6] = CRC16Temp & 0xFF; //CRC低位
- SendBuf[7] = ( CRC16Temp >> 8 ); //CRC高位
- uart2_Send( SendBuf, 8 );
- }
- else
- {
- err = 3; //寄存器数量不在规定范围内
- }
- }
复制代码
测试效果
测试功能码 0x03 读一个或者多个保持寄存器值
测试功能码 0x06 写单个保持寄存值
测试功能码 0x10 写多个保持寄存器值
再次读取所有寄存器值
说明寄存器值修改成功。
|