关于RS485通讯中使用STM32串口以DMA方式发送数据丢失字节的问题

1、开发平台

计算机操作系统:WIN7 64位;

开发环境:Keil MDK 5.14;

MCU:STM32F407ZET6;

STM32F4xx固件库:STM32F4xx_DSP_StdPeriph_Lib_V1.4.0;

串口调试助手;

2、问题描述

    在测试用STM32F4xx芯片的串口USART1以DMA方式进行RS485收发通讯时,出现数据字节丢失的现象,一般丢失1~2个字节。

    出现问题时测试的简单收发机制:使能串口USART1的DMA收发功能,开启了DMA发送完成中断和USART1空闲中断。通过串口调试助手发送N个字节给MCU,当MCU产生USART1空闲中断时,在USART1空闲中断服务程序中将DMA接收到的N个字节数据从接收缓存拷贝到发送缓存,准备好数据后,RS485切换为发送模式,通过启动一次DMA发送,将N个字节数据原样回送到串口调试助手。最后,在DMA发送完成中断服务程序中判断到有DMA发送完成标志TCIF7置位时,立即将RS485再次切换为接收模式。

3、原因分析

        在STM32F4xx英文参考手册(RM0090)中,USART章节的使用DMA发送小节给出了如下时序图:

        由图可见,当DMA将第3个字节Frame 3写到USART数据寄存器USART_DR时,TX线上才刚准备出现第2个字节Frame 2的时序,并且DMA发送完成中断标志在TX线还未出现第2个字节Frame 2时序时就由硬件置1了,所以,如果软件中在DMA发送完成中断服务程序中检测到DMA TCIF标志置1后马上将RS485切换为接收模式,则后面的字节数据将会丢失。

        所以,需要让数据字节不丢失的话,必须让所有字节(包括字节的停止位)在TX线上稳定发送完成后,才能将RS485切换为接收模式。

4、解决方法

        如上图所示,有一个关键点是:当所有字节(包括字节的停止位)在TX线上稳定发送完成后,串口发送完成标志(TC flag)置1。所以,有两个解决方法:

扫描二维码关注公众号,回复: 4311670 查看本文章

      方法一:用DMA发送完成中断,不用USART1发送完成中断。在DMA发送完成中断服务程序中检测到有TCIF7置1时,再等待USART1发送完成标志TC置1,直到USART1发送完成标志TC置1后,清零USART1发送完成标志TC,然后再将RS485切换为接收模式。

      方法二:用USART1发送完成中断,不用DMA发送完成中断。在USART1中断服务程序USART1_IRQHandler()中,检测到有USART1发送完成标志TC置1时,清零USART1发送完成标志TC,并且要清零DMA发送完成标志DMA_FLAG_TCIF7,最好同时清零DMA_FLAG_FEIF7、DMA_FLAG_DMEIF7、DMA_FLAG_TEIF7 、DMA_FLAG_HTIF7,然后再将RS485切换为接收模式。

      方法三:用DMA发送函数中的,发送数据长度进行+2处理,即可解决此问题。(本人项目实际使用方法)

5、参考源代码


方法一:用DMA发送完成中断

/*-------------------------------------------------------------------------------------- 
函数名称:void USART_DMA_SendStart(DMA_Stream_TypeDef *DMA_Streamx, u16 m_u16SendCnt)
函数功能:串口USART1启动一次DMA传输函数  
入口参数:DMA_Stream_TypeDef DMA_Streamx - DMA数据流(DMA1_Stream0~7/DMA2_Stream0~7);
         u16 m_u16SendCnt - 待传输数据字节数
出口参数:无
说    明:无
---------------------------------------------------------------------------------------*/
void USART_DMA_SendStart(DMA_Stream_TypeDef *DMA_Streamx, u16 m_u16SendCnt)  
{    
    u16 l_u16RetryCnt = 0;
    
    DMA_Cmd(DMA_Streamx, DISABLE);                      //关闭DMA传输           
    while ((DMA_GetCmdStatus(DMA_Streamx) != DISABLE) && (l_u16RetryCnt++ < 500));    //等待DMA可配置    
    DMA_SetCurrDataCounter(DMA_Streamx, m_u16SendCnt);  //数据传输量        
    DMA_Cmd(DMA_Streamx, ENABLE);                          //开启DMA传输   
}        
 
/*-------------------------------------------------------------------------------------- 
函数名称:void USART1_DMA_SendNByte(u8 *m_pSendBuf, u16 m_u16SendCnt) 
函数功能:串口USART1以DMA方式发送多字节函数  
入口参数:u8 *m_pSendBuf - 待发送数据缓存, u16 m_u16SendCnt - 待发送数据个数
出口参数:无
说    明:无
---------------------------------------------------------------------------------------*/  
void USART1_DMA_SendNByte(u8 *m_pSendBuf, u16 m_u16SendCnt)  
{    
    memcpy(G_u8Usart1SendBuf, m_pSendBuf, m_u16SendCnt);      
    USART_DMA_SendStart(DMA2_Stream7, m_u16SendCnt); //启动一次DMA传输      
}  
  
/*-------------------------------------------------------------------------------------- 
函数名称:void DMA2_Stream7_IRQHandler(void) 
函数功能:串口USART1以DMA方式发送完成中断服务程序  
入口参数:无
出口参数:无
说    明:无
---------------------------------------------------------------------------------------*/
void DMA2_Stream7_IRQHandler(void)  
{    
    if(DMA_GetFlagStatus(DMA2_Stream7, DMA_FLAG_TCIF7) != RESET)    //DMA发送完成?  
    {   
        DMA_ClearFlag(DMA2_Stream7, DMA_FLAG_TCIF7 | DMA_FLAG_FEIF7 | 
                      DMA_FLAG_DMEIF7 | DMA_FLAG_TEIF7 | DMA_FLAG_HTIF7);     //清除标志位            
        
        while(!USART_GetFlagStatus(USART1, USART_FLAG_TC));    //等待USART1发送完成标志TC置1
        USART_ClearFlag(USART1, USART_FLAG_TC);     //清除发送完成标志
        
        RS485_Recv();        //切换为RS485接收模式        
    }  
}
 
/*-------------------------------------------------------------------------------------- 
函数名称:void USART1_IRQHandler(void)
函数功能:USART串口1中断服务程序  
入口参数:无
出口参数:无
说    明:无
---------------------------------------------------------------------------------------*/
void USART1_IRQHandler(void)  
{  
    u16 l_u16Temp = 0;
    
    if(USART_GetITStatus(USART1, USART_IT_IDLE) != RESET)    //若有空闲中断  
    {  
        DMA_Cmd(DMA2_Stream5, DISABLE); //关闭DMA2_Stream5,防止处理期间有数据 
        
        DMA_ClearFlag(DMA2_Stream5, DMA_FLAG_TCIF5 | DMA_FLAG_FEIF5 | 
                      DMA_FLAG_DMEIF5 | DMA_FLAG_TEIF5 | DMA_FLAG_HTIF5);     //清除标志位        
  
        //清除USART总线空闲中断标志(只要读USART1->SR和USART1->DR即可)
        l_u16Temp = USART1->SR;         
        l_u16Temp = USART1->DR;
          
        G_u16CommRecvLen = USART1_RECV_MAXLEN - DMA_GetCurrDataCounter(DMA2_Stream5);     //求出接收到数据的字节数 
        if(G_u16CommRecvLen <= USART1_RECV_MAXLEN)
        {
            RS485_Send();        //RS485发送模式
            USART1_DMA_SendNByte(G_u8Usart1RecvBuf, G_u16CommRecvLen);    //回送接收到的数据
        }
          
        DMA_SetCurrDataCounter(DMA2_Stream5, USART1_RECV_MAXLEN);  //设置传输数据长度
        DMA_Cmd(DMA2_Stream5, ENABLE);     //使能DMA2_Stream5  
    }     
}


方法二:用USART1发送完成中断


/*-------------------------------------------------------------------------------------- 
函数名称:void USART_DMA_SendStart(DMA_Stream_TypeDef *DMA_Streamx, u16 m_u16SendCnt)
函数功能:串口USART1启动一次DMA传输函数  
入口参数:DMA_Stream_TypeDef DMA_Streamx - DMA数据流(DMA1_Stream0~7/DMA2_Stream0~7);
         u16 m_u16SendCnt - 待传输数据字节数
出口参数:无
说    明:无
---------------------------------------------------------------------------------------*/
void USART_DMA_SendStart(DMA_Stream_TypeDef *DMA_Streamx, u16 m_u16SendCnt)  
{    
    u16 l_u16RetryCnt = 0;
    
    DMA_Cmd(DMA_Streamx, DISABLE);                      //关闭DMA传输           
    while ((DMA_GetCmdStatus(DMA_Streamx) != DISABLE) && (l_u16RetryCnt++ < 500));    //等待DMA可配置    
    DMA_SetCurrDataCounter(DMA_Streamx, m_u16SendCnt);  //数据传输量        
    DMA_Cmd(DMA_Streamx, ENABLE);                          //开启DMA传输   
}        
 
/*-------------------------------------------------------------------------------------- 
函数名称:void USART1_DMA_SendNByte(u8 *m_pSendBuf, u16 m_u16SendCnt) 
函数功能:串口USART1以DMA方式发送多字节函数  
入口参数:u8 *m_pSendBuf - 待发送数据缓存, u16 m_u16SendCnt - 待发送数据个数
出口参数:无
说    明:无
---------------------------------------------------------------------------------------*/  
void USART1_DMA_SendNByte(u8 *m_pSendBuf, u16 m_u16SendCnt)  
{    
    memcpy(G_u8Usart1SendBuf, m_pSendBuf, m_u16SendCnt);      
    USART_DMA_SendStart(DMA2_Stream7, m_u16SendCnt); //启动一次DMA传输      
}
 
/*-------------------------------------------------------------------------------------- 
函数名称:void USART1_IRQHandler(void)
函数功能:USART串口1中断服务程序  
入口参数:无
出口参数:无
说    明:无
---------------------------------------------------------------------------------------*/
void USART1_IRQHandler(void)  
{  
    u16 l_u16Temp = 0;
    
    if(USART_GetITStatus(USART1, USART_IT_IDLE) != RESET)    //若有空闲中断  
    {  
        DMA_Cmd(DMA2_Stream5, DISABLE); //关闭DMA2_Stream5,防止处理期间有数据     
        DMA_ClearFlag(DMA2_Stream5, DMA_FLAG_TCIF5 | DMA_FLAG_FEIF5 | 
                      DMA_FLAG_DMEIF5 | DMA_FLAG_TEIF5 | DMA_FLAG_HTIF5);//清零标志位         
  
        //清除USART总线空闲中断标志(只要读USART1->SR和USART1->DR即可)
        l_u16Temp = USART1->SR;         
        l_u16Temp = USART1->DR;
          
        G_u16CommRecvLen = USART1_RECV_MAXLEN - DMA_GetCurrDataCounter(DMA2_Stream5);     //求出接收到数据的字节数 
        if(G_u16CommRecvLen <= USART1_RECV_MAXLEN)
        {
            RS485_Send();        //RS485发送模式
            USART1_DMA_SendNByte(G_u8Usart1RecvBuf, G_u16CommRecvLen);
        }
          
        DMA_SetCurrDataCounter(DMA2_Stream5, USART1_RECV_MAXLEN);  //设置传输数据长度
        DMA_Cmd(DMA2_Stream5, ENABLE);     //使能DMA2_Stream5  
    } 
 
    if(USART_GetITStatus(USART1, USART_IT_TC) != RESET)    //若有发送完成中断  
    {  
        USART_ClearITPendingBit(USART1, USART_IT_TC);    //清除USART1发送完成中断标志
        DMA_ClearFlag(DMA2_Stream7, DMA_FLAG_TCIF7 | DMA_FLAG_FEIF7 | 
                      DMA_FLAG_DMEIF7 | DMA_FLAG_TEIF7 | DMA_FLAG_HTIF7);//清零标志位
            
        RS485_Recv();        //切换为RS485接收模式
    }    
}

方法三:发送长度➕2

/*****************************调用函数**************************/										if(Frame_CRC(Send_SmartBox_Buffer,Send_SmartBox_Buffer[2]+5)==0)
{
    RS485_EN=1;
    __nop();__nop();__nop();__nop();__nop();__nop();
    __nop();__nop();__nop();__nop();__nop();__nop();
	DMA14_FLAG_Complete = 0;
											                         
    USART1_DMA_SendBytes((char*)Send_SmartBox_Buffer,Send_SmartBox_Buffer[2]+5);
}


/*****************************DMA发送函数**************************/	
/*usart1 通过DMA1.4   pc */
void USART1_DMA_SendBytes(char *bytes, uint16_t counts)
{
	DMA1_Channel4->CNDTR = counts+2;
	DMA1_Channel4->CMAR = (uint32_t)bytes; 
	DMA_Cmd(DMA1_Channel4, ENABLE);
	USART_DMACmd(USART1, USART_DMAReq_Tx, ENABLE);
}


6、声明

   三种方法中,方法一在中断里面等待白白耗费了时间。方法二是个不错的方案。方法三方便快捷,最为简单。
 

猜你喜欢

转载自blog.csdn.net/qq_27747359/article/details/84631039