我也说一下STM32下串口DMA的任意长度接收发送的思路

2019-07-20 17:41发布

我也不算是真正的单片机开发人员,只是项目需要,单片机程序也一直在优化,优化最多的串口通信,尽可能快的传输大数据,以前直接用中断的方式,稳定,但传输稍大的数据时,需要的时间就久了,特别是需要传输扫描枪扫到的图片时,就会等待很久。后来改成了DMA,也只有DMA是最快了。好了,进入正题

1.百度,是最好的老师,可以查到很多前辈积累的经验和方法,我也是参考了他们的
2.DMA任意字符的接收,很多是使用串口的USART_IT_IDLE空闲中断,没错, 我也使用它了。
3.还有使用DMA双缓冲区,当时研究时发现双缓冲转换不是很方便,就没用了。
4.使用DMA的DMA_IT_TC中断,当接收满时就触发中断,这个我也需要用到
5.有前辈使用DMA与环形缓冲区结合,这个方法很好,不过我有点看不懂,对于手上的项目改动太大,就没继续深研
6.我使用了环型缓冲区,可以做到数据与业务分离,很方便
7.我上了RTX系统,多任务,很方便,也比较危险,所以线程同步要做好

好了,我要说下我的思路
我使用的单片机是STM32F205,RTX系统,关于串口和DMA配置忽略吧,百度都能查到

接收数据:
前面说到要用到的两个中断,串口的空闲和DMA的接收完成中断,两个相结合后就可以实现任意长度了(说是任意长度,其实接收的长度当然要小于分配的环型缓冲区)。我使用了环型缓冲区用来接收数据,这样就可以做到业务逻辑的分离了,接收的只管接收,处理的只管处理

//串口1中断处理
void USART1_IRQHandler(void)
{
    uint8_t ID = 0;                //串口序号
    uint16_t Size = 0;


    if ( USART_GetITStatus( USART1, USART_IT_IDLE ) != RESET )           //串口空闲中断 用于DMA       

    {
        USART_ClearITPendingBit( USART1, USART_IT_IDLE );  //清除空闲中断
        USART_ReceiveData( USART1);  //读DR,只有读过一次,才能真正清除标志
        USART_ClearFlag( USART1, USART_FLAG_IDLE );  //读SR其实就是清除标志
        DMA_Cmd(DMA2_Stream2, DISABLE);  //先停止DMA才行设置缓冲区大小
        
        Size = DMA_USART_RecieveBuffer_Len - DMA_GetCurrDataCounter( DMA2_Stream2 );                        //获得DMA当前收到的字节数,因为DMA中断只有接收长度满时才会触发,所以要把剩余数据读出来 DMA2 Stream2-Channel4
        if(Size > 0)  //如果有剩余数据
            FIFO_PutBuff( &USART1_FIFO_Receive, DMA_USART1_Recieve_Buff, Size );            //将收到的DMA缓冲复制进串口输入FIFO  FIFO_PutBuff()函数是环型缓冲区写入Buff的用到的,USART1_FIFO_Receive 环型缓冲区结构,  DMA_USART1_Recieve_Buff DMA接收缓冲区
        USART_DMA_Status[ID].USART_IS_IDLE = 1;  //自定义标识 串口空闲标识
        USART_DMA_Status[ID].DMA_ReceiveOK = 1;  //自定义标识 DMA接收完成标识

        DMA_SetCurrDataCounter( DMA2_Stream2, DMA_USART_RecieveBuffer_Len );   //重新设置DMA的读取缓冲区长度   DMA_USART1_Recieve_Buff[DMA_USART_RecieveBuffer_Len]
        DMA_Cmd(DMA2_Stream2, ENABLE);  //开启DMA       

    }


//DMA中断处理
void DMA2_Stream2_IRQHandler(void)
{
    uint8_t ID = 0;  //串口序号

    if( DMA_GetITStatus(DMA2_Stream2, DMA_IT_TCIF2) != RESET )
    {
        DMA_ClearITPendingBit(DMA2_Stream2, DMA_IT_TCIF2);

        //重新设置DMA坊取缓冲区长度时会触发该中断,所以当有该标志时,不处理该条中断。
       //在实际使用中,发现经常接收的数据有出入,跟踪后发现在串口中断USART_IT_IDLE重新设置DMA缓冲长度时,会触发该中断,所以加入了自定义标识USART_DMA_Status[ID].USART_IS_IDLE,为1时,说明已经空闲了,不再将数据写入环型缓冲区
        if( USART_DMA_Status[ID].USART_IS_IDLE != 1 )
        {
              FIFO_PutBuff( &USART1_FIFO_Receive, DMA_USART1_Recieve_Buff, DMA_USART_RecieveBuffer_Len );    //将DMA缓冲区数据写入环型缓冲区
              USART_DMA_Status[ID].DMA_ReceiveOK = 1;    //DMA接收完成
        }
               
        USART_DMA_Status[ID].USART_IS_IDLE = 0;    //重置串口空闲中断
       
    }

}

业务逻辑处理:
//项目在通信中,使用了定长命令头协议方式,这样能更好的处理数据,可以知道这个命令是做什么的,以及这个命令将要接收的数据的长度

typedef struct
{
       
        uint8_t        Cmd;                                                        //命令标识
        uint8_t        CmdType;                                        //命令类型
        uint32_t        DataLen;                                        //发送数据长度,用于下次发送的数据长度
        uint32_t        Param1;                                                //参数1
        uint32_t        Param2;                                                //参数2
        uint32_t        Param3;                                                //参数3
        uint32_t        Param4;                                                //参数4
        uint8_t         LRC;                                                        //校验码 除最后一位LRC码外所有字节的XOR运算
       
} TCMDHeader;                                        //串口 命令头 结构体



void Query_USART1(void)

        uint16_t BuffSize        = FIFO_GetSize( &USART1_FIFO_Receive );                        //串口1缓冲区可读字节数

        if( BuffSize > 0 )                 //如果串口1有数据
        {
                ReadSize = FIFO_ReadByte( &USART1_FIFO_Receive, (uint8_t *) &USART_Receive_CMD, sizeof( TCMDHeader ), 50 );                //读取命令头 超时50ms



                if( ReadSize != sizeof( TCMDHeader ) )                 //如果读到的大小与待读大小不一致,则退出
                {
                        return;
                }

                if( Check_LRC( (uint8_t *) &USART_Receive_CMD, ReadSize) )                        //命令头LRC校验成功
                {

                                //这里通过USART_Receive_CMD命令来区分命令的类型和处理
                               //......
                }
        }




以上就是接收的部分,我的项目中两块单片机使用串口进行大数据传输,单片机1把采集的图片通过串口发送到单片机2上,单片机2首先接收命令头,通过命令头知道下次待接收的数据大小,这样单片机1只管发数据,单片机2处理命令头后循环处理环型缓冲的数据,边接收边通过网络接口透传到上位机上,直到数据接收完成。单片机1的发送也是通过DMA方式发送,当然也是任意长度的发送。

下面我就说说DMA如何发送任意长度

发送数据:
思路是这样的:发送也有一个环型缓冲区,发送的时候把数据填入缓冲区中,直到填满,如果未开启DMA发送中断,则开启,开启后,调用函数只管往缓冲区填数据,DMA的发送完成中断会判断缓冲区是否为空,空则发送完成,不空,则继续调用DMA发送

//DMA的底层发送函数,每调用一次发送一个包 SendFIFO是发送的环型缓冲区,结构与接收的一致
//DMAy_Streamx DMA通道号
//因为函数都是做成通用性的,不同的参数使用不同的串口和DMA流
void USART_DMA_Send( TFIFO* SendFIFO, DMA_Stream_TypeDef* DMAy_Streamx )
{
       
        uint16_t BuffSize = 0;
        uint16_t ReadSize = 0;
       
        int ID = USART_Get_DMA_USARTIndex_FromStreamX( DMAy_Streamx );        //通过DMAy_Streamx获得串口序列号 0:USART1 1:USART2...
        uint8_t* Buffer = USART_Get_DMA_Buffer_FromStreamX( DMAy_Streamx );    //获得DMA的发送缓冲区  如:if( DMAStreamX == DMA2_Stream7 )        return DMA_USART1_Send_Buff;        //USART1 TX
       
        if( SendFIFO == NULL || DMAy_Streamx == NULL || ID == -1 || Buffer == NULL ) return;
       
        //DMA发送标识,如果DMA未开启发送,则开启DMA发送。
       //第一次调用发送时 USART_DMA_Status[ID].DMA_IsSending = 0, 开启DMA发送后 USART_DMA_Status[ID].DMA_IsSending = 1 ,之后的发送将不在此处理剩余的数据,而是在发送完成中断中继续发送,如果发送后再置USART_DMA_Status[ID].DMA_IsSending = 0
        if( USART_DMA_Status[ID].DMA_IsSending == 0 )
        {

                        BuffSize = FIFO_GetSize( SendFIFO );    //获得发送缓冲区大小
       
                        if(  BuffSize == 0 ) return;    //为空则退出       
       
                        if( BuffSize > DMA_USART_SendBuffer_Len ) BuffSize = DMA_USART_SendBuffer_Len;    //如果发送大小大于DMA发送缓冲,则大小取DMA发送缓冲区大小
                       
                        if( FIFO_GetBuff( SendFIFO, BuffSize, Buffer, &ReadSize) != 1 ) return;            /复制发送数据到DMA缓冲区中  DMA_USART_Send_Buff[ID]:串口数对应的DMA缓冲区

                        USART_DMA_Status[ID].DMA_IsSending = 1;    //设置发送标识为1

                        DMA_Cmd( DMAy_Streamx, DISABLE );    //停止发送
                        DMA_SetCurrDataCounter( DMAy_Streamx, ReadSize );    //设置DMA发送缓冲区的大小为待发送大小       
                        DMA_Cmd( DMAy_Streamx, ENABLE );    //开启发送
               
        }
       
}


//任意字符发送,主要是使用该函数发送,该函数主要是向发送环型缓冲区填入数据,并触发开启第一次DMA发送,之后的发送将在发送中断完成后发送
uint16_t USART_DMA_PostBuff( USART_TypeDef* USARTx, uint8_t *Buff, uint16_t BuffSize )
{
       
        uint16_t Free = 0;
        uint16_t SendSize = 0;
       
        TFIFO* SendFIFO = USART_GetSendFIFOName( USARTx );    //获得串口对应的FIFO环型缓冲区
        DMA_Stream_TypeDef* DMAy_Streamx = USART_Get_DMAStreamX(USARTx, 1);    //取得对应串口的DMA流通道 0:为RX 1:为TX
       
        if( SendFIFO == NULL || DMAy_Streamx == NULL )        return SendSize;
       
        //进入循环发送,直到所有的数据都填入环型缓冲区为止
        while( SendSize < BuffSize )
        {
                        Free = FIFO_GetFree( SendFIFO );    //获得串口发送环型缓冲区空闲大小
               
                        if( Free > 0 )    //如果有空闲
                        {
               
                                if( Free > BuffSize )    //如果空闲大小大于待发送大小,则直接把待发送数据压入发送缓冲区
                                {
                                        FIFO_PutBuff( SendFIFO, Buff, BuffSize);    //压入发送缓冲区
                                        SendSize = BuffSize;    //发送数据大小
                                       
                                        USART_DMA_Send( SendFIFO, DMAy_Streamx );    //发送DMA数据
                                }
                                else
                                {
                                       
                                        if( BuffSize - SendSize < Free ) Free = BuffSize - SendSize;
                                       
                                        FIFO_PutBuff( SendFIFO, (uint8_t *) (Buff + SendSize), Free);        //压入发送缓冲区
                                        SendSize += Free;
                                       
                                        USART_DMA_Send( SendFIFO, DMAy_Streamx );     //发送DMA数据

                                }
                               
                        }

                        Delay_ms(1);    //延时,多任务下让出时间片

        }
       
        return SendSize;
       
}



/*
*函数名称:DMA2_Stream7_IRQHandler
*函数功能:USART1-TX DMA2 Stream7-Channel4 响应中断
*发送完成后触发该中断,并继续发送未完成的数据
*/
void DMA2_Stream7_IRQHandler(void)
{
       
        uint8_t ID = 0;                //串口序号
       
        uint16_t BuffSize = 0;
        uint16_t ReadSize = 0;       
       
        if( DMA_GetITStatus(DMA2_Stream7, DMA_IT_TCIF7) != RESET )
        {
               
                DMA_ClearITPendingBit(DMA2_Stream7, DMA_IT_TCIF7);    //清除中断标志
               
               
                BuffSize = FIFO_GetSize( &USART1_FIFO_Send );    //获得发送缓冲区大小
               
                if( BuffSize > 0 )
                {
                       
                        if( BuffSize > DMA_USART_SendBuffer_Len ) BuffSize = DMA_USART_SendBuffer_Len;        //如果发送大小大于DMA发送缓冲,则大小取DMA发送缓冲区大小
                       
                        if( FIFO_GetBuff( &USART1_FIFO_Send, BuffSize, DMA_USART1_Send_Buff, &ReadSize) != 1 )            //如果发送环型缓冲区已经为空,则退出发送,否则继续发送
                        {
                                USART_DMA_Status[ID].DMA_SendOK = 1;    //发送完成
                                USART_DMA_Status[ID].DMA_IsSending = 0;    //重置发送标识,下次发送需要开启DMA发送
                               
                                return;
                        }
               
                        DMA_Cmd( DMA2_Stream7, DISABLE );    //停止发送
                        DMA_SetCurrDataCounter( DMA2_Stream7, ReadSize );    //设置DMA发送缓冲区的大小为待发送大小       
                        DMA_Cmd( DMA2_Stream7, ENABLE );

                        USART_DMA_Status[ID].DMA_SendOK = 0;    //未发送完成
                        USART_DMA_Status[ID].DMA_IsSending = 1;    //正在发送
                       
                }
                else
                {
                        USART_DMA_Status[ID].DMA_SendOK = 1;    //发送完成
                        USART_DMA_Status[ID].DMA_IsSending = 0;    //已发送完成
                }
               

        }
       
}





终于发完了,并整理了备注,程序可能比较复杂,该方法也稳定运行了一段时间,暂时没发现问题


友情提示: 此问题已得到解决,问题已经关闭,关闭后问题禁止继续编辑,回答。