搜索
bottom↓
回复: 53

一种串口DMA收发不定长数据的方法

  [复制链接]

出0入0汤圆

发表于 2019-4-27 21:17:32 | 显示全部楼层 |阅读模式
以前发过利用DMA接收不定长数据的方法,最近把发送也调试好了,这个程序应用多年,一直很好用,适应各种设备,现把程序再整合一下发上来。

主要原理:
1、串口接收:在内存中开启一个缓冲区,DMA指针指向缓冲区首地址,开启接收,并定时判断是否有数据接收到,如果接收到,则继续等待,如果无数据超过规定时间,则认为本帧数据结束,将数据拷贝至其他部位,同时进行下次接收;
2、串口发送:DMA工作与Normal模式,需要发送时,设置DMA指针到发送数组首地址,DMA长度设置为要发送的数据长度,启动DMA后即可开始发送。

优点:
1、无需任何中断,只需要定时调用串口接收函数;
2、收发占用CPU时间极少,经测试,接收和发送函数平时占用时间大约3-10us(48M主频)
3、串口超时时间随意设定,适应多种应用

下面是程序(本程序是在STM32F373上调试的,其他CPU只要改下寄存器名即可使用):

首先是USART.h
#ifndef __USART_H
#define __USART_H

#define        UART2_TimeoutComp 5  //串口超时时间2*5=10ms

#define        USART_BufferSize2 100  //接收缓冲区大小

#define SRC_USART2_RDR (&(USART2->RDR)) //串口接收寄存器
#define SRC_USART2_TDR (&(USART2->TDR)) //串口发送寄存器

extern u8 USART2_Data[USART_BufferSize2];

void USART_Configuration(void);
void USART_Write(u8* dat,u16 len);
u16 USART_Read(void);

#endif

接下来是USART.c

#include "stm32_includes.h"
#include "USART.h"

//串口接收DMA
DMA_InitTypeDef DMA_InitStructure_Rx;

//串口发送DMA
DMA_InitTypeDef DMA_InitStructure_Tx;

//串口2接收到的数据
u8 USART2_Data[USART_BufferSize2];

//串口2超时
u8 UART2_Timeout;

//串口2接收缓存
u8 uart2_data_temp[USART_BufferSize2];

//串口2上次接收指针位置
u16 uart2_Counter_Last=0;

/////////////////////////////////////////////////////////发送接收程序/////////////////////////////////////////////////////////////////////
//发送多个字节数据
void USART_Write(u8* dat,u16 len)
{
        DMA_Cmd(DMA1_Channel7, DISABLE); //关闭DMA        

        DMA_InitStructure_Tx.DMA_MemoryBaseAddr = (u32)dat; //目标BUF       
        DMA_InitStructure_Tx.DMA_BufferSize = len; //DMA缓存的大小
       
        DMA_Init(DMA1_Channel7, &DMA_InitStructure_Tx); //根据DMA_InitStruct中指定的参数初始化DMA的通道7寄存器
       
        DMA_Cmd(DMA1_Channel7, ENABLE); //开启DMA                
}
//重置串口接收DMA
void USART_ResetDMA_Rx(void)
{

        DMA_Cmd(DMA1_Channel6, DISABLE); //关闭DMA
        DMA_ClearFlag(DMA1_FLAG_TC6);  //清除标志
        DMA1_Channel6->CNDTR = USART_BufferSize2; //重置DMA指针
        DMA_Cmd(DMA1_Channel6, ENABLE); //开启DMA
        UART2_Timeout=0; //清零超时时间
        uart2_Counter_Last = USART_BufferSize2; //重置上次接收指针

}
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
//DMA初始化       
void USART_DMA_Init(void)
{
        //串口接收DMA初始化
        DMA_DeInit(DMA1_Channel6); //将DMA的通道6寄存器重设为缺省值
       
        DMA_InitStructure_Rx.DMA_DIR = DMA_DIR_PeripheralSRC; //外设作源头//外设是作为数据传输的目的地还是来源
        DMA_InitStructure_Rx.DMA_PeripheralInc = DMA_PeripheralInc_Disable; //外设地址寄存器不递增
        DMA_InitStructure_Rx.DMA_MemoryInc = DMA_MemoryInc_Enable; //内存地址递增
        DMA_InitStructure_Rx.DMA_PeripheralDataSize = DMA_PeripheralDataSize_Byte; //外设字节为单位
        DMA_InitStructure_Rx.DMA_MemoryDataSize = DMA_PeripheralDataSize_Byte; //内存字节为单位
        DMA_InitStructure_Rx.DMA_Mode = DMA_Mode_Circular; //工作在循环模式
        DMA_InitStructure_Rx.DMA_Priority = DMA_Priority_High;
        DMA_InitStructure_Rx.DMA_M2M = DMA_M2M_Disable;

        DMA_InitStructure_Rx.DMA_PeripheralBaseAddr = (u32)SRC_USART2_RDR; //源头寄存器
        DMA_InitStructure_Rx.DMA_MemoryBaseAddr = (u32)uart2_data_temp; //目标BUF        
        DMA_InitStructure_Rx.DMA_BufferSize = USART_BufferSize2; //DMA缓存的大小
        DMA_Init(DMA1_Channel6, &DMA_InitStructure_Rx); //初始化DMA的通道7寄存器
       
        DMA_ITConfig(DMA1_Channel6, DMA_IT_TC, DISABLE); //关闭DMA6传输完成中断
       
        USART_DMACmd(USART2,USART_DMAReq_Rx,ENABLE); //使能USART2的接收DMA请求

        DMA_Cmd(DMA1_Channel6, ENABLE); //正式允许DMA        
       
       
        //串口发送DMA初始化
        DMA_DeInit(DMA1_Channel7); //将DMA的通道7寄存器重设为缺省值
       
        DMA_InitStructure_Tx.DMA_DIR = DMA_DIR_PeripheralDST; //外设作目的地
        DMA_InitStructure_Tx.DMA_PeripheralInc = DMA_PeripheralInc_Disable; //外设地址寄存器不递增
        DMA_InitStructure_Tx.DMA_MemoryInc = DMA_MemoryInc_Enable; //内存地址递增
        DMA_InitStructure_Tx.DMA_PeripheralDataSize = DMA_PeripheralDataSize_Byte; //外设字节为单位
        DMA_InitStructure_Tx.DMA_MemoryDataSize = DMA_PeripheralDataSize_Byte; //内存字节为单位
        DMA_InitStructure_Tx.DMA_Mode = DMA_Mode_Normal; //工作在单次模式       
        DMA_InitStructure_Tx.DMA_Priority = DMA_Priority_High;
        DMA_InitStructure_Tx.DMA_M2M = DMA_M2M_Disable;         

        DMA_InitStructure_Tx.DMA_PeripheralBaseAddr = (u32)SRC_USART2_TDR; //源头BUF
        DMA_InitStructure_Tx.DMA_MemoryBaseAddr = (u32)uart2_data_temp; //目标BUF
        DMA_InitStructure_Tx.DMA_BufferSize = USART_BufferSize2; //DMA缓存的大小
       
        DMA_Init(DMA1_Channel7, &DMA_InitStructure_Tx); //初始化DMA的通道7寄存器
       
        DMA_ITConfig(DMA1_Channel7, DMA_IT_TC, DISABLE); //关闭DMA7传输完成中断
       
        USART_DMACmd(USART2,USART_DMAReq_Tx,ENABLE); //使能USART2的发送DMA请求       
}
//串口初始化       
void USART_Configuration(void)
{
    //串口初始化数据结构定义
        USART_InitTypeDef USART_InitStructure;
       
        USART_InitStructure.USART_WordLength          = USART_WordLength_8b;
        USART_InitStructure.USART_StopBits            = USART_StopBits_1;
        USART_InitStructure.USART_Parity              = USART_Parity_No ;
        USART_InitStructure.USART_HardwareFlowControl = USART_HardwareFlowControl_None;
        USART_InitStructure.USART_Mode                = USART_Mode_Rx | USART_Mode_Tx;

        USART_InitStructure.USART_BaudRate            = 115200;
        USART_Init(USART2, &USART_InitStructure);       
        USART_Cmd(USART2, ENABLE);
       
        USART_DMA_Init();
}

//串口接收读取程序
u16 USART_Read(void)
{
        u16 counter,ret = 0 ;

        //超时时间
        UART2_Timeout++;
       
        //获取接收指针位置
        counter = DMA_GetCurrDataCounter(DMA1_Channel6);
       
        //指针发生变化,说明有数据接收到
        if(counter != uart2_Counter_Last)  //未完成传输
        {
                UART2_Timeout = 0;  //清零超时时间
                uart2_Counter_Last = counter; //保存本次指针位置
        }
        else //指针没有变化,说明没有数据接收到
        {
                if(UART2_Timeout >= UART2_TimeoutComp)  //产生超时
                {
                        if(uart2_Counter_Last < USART_BufferSize2) //有数据接收到
                        {
                                //取得接收到的数据数量
                                ret = USART_BufferSize2 - uart2_Counter_Last;

                                //将接收到的数据复制到接收数组中
                                CopyData(uart2_data_temp,USART2_Data,ret);

                                //重置串口接收DMA,继续下次接收
                                USART_ResetDMA_Rx();                               
                        }
                       
                        //清零超时时间
                        UART2_Timeout=0;
                }
        }

        return ret;
}

接下来是主程序

//--------------------------------------------------------------------------------------------------------------
//数据接收程序
void USART_Task(void)
{               
        len = USART_Read();       
       
        if(len > 0)
        {
                               
                //分析数据
                AnalizeData(USART2_Data, len);       

        }       

        return;
}

int main()
{
        while(1)
        {
                USART_Task();  //调用接收程序
                delay_ms(2);  //2ms调用一次
        }
}


发送数据时,只需要调用发送函数USART_Write即可,数据长度随意。





阿莫论坛20周年了!感谢大家的支持与爱护!!

月入3000的是反美的。收入3万是亲美的。收入30万是移民美国的。收入300万是取得绿卡后回国,教唆那些3000来反美的!

出0入0汤圆

发表于 2019-4-27 23:18:53 来自手机 | 显示全部楼层
谢谢分享!

出0入0汤圆

发表于 2019-4-28 08:31:35 | 显示全部楼层
值得参考,谢谢楼主

出0入0汤圆

发表于 2019-4-28 08:35:18 | 显示全部楼层
dma+空闲中断更好用

出0入0汤圆

发表于 2019-4-28 08:38:29 | 显示全部楼层
串口接收使用dma+空闲中断, 很好用的,

出0入0汤圆

发表于 2019-4-28 09:03:37 | 显示全部楼层
DMA+空闲中断+地址匹配,绝对大杀器。

出0入0汤圆

发表于 2019-4-28 09:04:05 | 显示全部楼层
timer查询DMA长度做超时判断~

出10入12汤圆

发表于 2019-4-28 09:06:55 | 显示全部楼层
串口好办,  SPI不好办

出0入0汤圆

发表于 2019-4-28 09:24:16 | 显示全部楼层
强烈推荐串口空闲中断, 绝对好用

出0入0汤圆

发表于 2019-4-28 09:26:59 | 显示全部楼层
感谢分享。

出0入0汤圆

发表于 2019-4-28 09:28:34 | 显示全部楼层
mark一下,DMA收发不定长。

出0入362汤圆

发表于 2019-4-28 10:20:45 | 显示全部楼层
有现成的idle中断干嘛不用。。

出0入0汤圆

 楼主| 发表于 2019-4-28 12:06:25 | 显示全部楼层
我个人觉得,对于复杂的系统,能少用中断最好,一旦中断多了,需要多考虑很多问题,例如优先级,冲突之类的,不如无中断的系统调试简单,问题少。还有很多利用IO模拟某些时序的应用,开中断会影响时序,就需要不断开关中断,麻烦,所以我平时做的系统很少用中断,用这种模式就非常理想,效果很好。

出0入0汤圆

发表于 2019-4-28 12:13:10 | 显示全部楼层
感谢分享,一直用接收中断做的

出0入0汤圆

 楼主| 发表于 2019-4-28 13:20:51 | 显示全部楼层
IDLE中断有一个小缺点,对于103这类没有超时时间设定的CPU,IDLE是固定的1个字节时间作为超时,某些应用,比如GPS,每次传输有多条数据,每条数据之间有一定的间隔,如果用IDLE的话,会多次中断,而某些系统设计时更希望接收到完整的一帧数据后再做处理,这时用IDLE中断就会比较麻烦,这种不用中断的方式,可以设置超时时间,可以确保一帧数据完整。现实中我们的项目GPS超时时间设为100ms,比较理想,一次能接收到非常完整的数据帧,有五六百字节左右,后面统一处理,不用老去占用CPU。

出5入42汤圆

发表于 2019-4-28 19:56:41 | 显示全部楼层
horizon0315 发表于 2019-4-28 13:20
IDLE中断有一个小缺点,对于103这类没有超时时间设定的CPU,IDLE是固定的1个字节时间作为超时,某些应用, ...

确实,有时候使用4G或者2G模块,AT指令打出去,模块响应会回OK,……,参数…… 这些数据不连续,会分着来。

出0入0汤圆

发表于 2019-4-29 10:09:33 | 显示全部楼层
感谢分享

出0入0汤圆

发表于 2019-4-29 10:44:02 | 显示全部楼层
和我使用的方式一样
uint16_t check_dma_rx_ok(void *argument)
{
    static uint8_t chTimeCount;
    static uint16_t hwLastLength;
    volatile uint16_t hwLength = 0;

#define TIMEOUT     2

    hwLength = MB_DMA_RX_SIZE - (uint16_t)g_thusart1.pDmaChannelRx->CNDTR;
    if (hwLength) {
        if (hwLength == hwLastLength) {
            if ((++chTimeCount) >= TIMEOUT) {
                chTimeCount = 0;
                return hwLength;
            }
        } else {
            chTimeCount = 0;
            hwLastLength = hwLength;
        }
    }
    return 0;
}

出0入0汤圆

发表于 2019-4-29 11:47:26 | 显示全部楼层
不错,学习一下

出0入0汤圆

发表于 2019-4-29 12:00:43 | 显示全部楼层
好資料,謝謝分享。

出330入0汤圆

发表于 2019-4-29 12:48:57 来自手机 | 显示全部楼层
这种串口处理方式在某些应用场景下很合适,比如主问从答,双方都可以等,各自忙其他的事,然后抽空看下串口有无数据,数据有无变化等等通讯进展情况,甚至数据已经收发完成,由于系统不是中断机制,而没有及时响应处理。而在某些实时性要求高的场景下有局限性,比如面板操控变频器。

出140入115汤圆

发表于 2019-4-29 23:24:56 来自手机 | 显示全部楼层
优点是不需要中断

出0入0汤圆

发表于 2019-4-29 23:38:02 | 显示全部楼层
我485接收用DMA+IDLE中断+FIFO,发送用DMA+串口完成中断。

出0入0汤圆

发表于 2019-5-3 16:03:34 | 显示全部楼层
专门讨论技术的帖子,赞

出0入0汤圆

发表于 2019-5-5 08:50:39 | 显示全部楼层
谢谢分享。。。。。。。。。dma

出0入42汤圆

发表于 2019-5-9 09:41:49 | 显示全部楼层
当DMA指针改变时处理数据,DMA指针没有变化时不做处理也不需要重置DMA。

出0入0汤圆

发表于 2019-11-3 09:54:01 | 显示全部楼层
今天看清了楼主头像,是编织袋里面装的八只鸭子^_^,初看以为是个多足机器人O(∩_∩)O哈哈~

出0入0汤圆

发表于 2019-11-3 11:17:26 | 显示全部楼层
waft_wind 发表于 2019-5-9 09:41
当DMA指针改变时处理数据,DMA指针没有变化时不做处理也不需要重置DMA。

感觉楼像是要应对串口阻塞问题,比如“发数方”发送过程阻塞,本机接收就会产生超时,但此时接收不是完整的数据包,复位一下DMA,进入下次接收。

出0入0汤圆

发表于 2019-11-3 11:29:57 | 显示全部楼层
串口在正常应用时,比如定时比较准确的”发送--接收“一般都没问题。但因发送方阻塞后“粘包”连发,主程序处理不当就会产生数据丢失现象。因为阻塞产生IDLE中断,此时解包不是完整的包而放弃数据;“粘包”连发可能又掉漏掉的IDLE中断,解包时又会丢到多余的数据。

出0入4汤圆

发表于 2019-11-3 12:59:02 | 显示全部楼层
技术贴不错,DMA确实好用

出0入0汤圆

发表于 2019-11-3 15:42:41 | 显示全部楼层

感谢分享

出0入45汤圆

发表于 2019-11-5 10:33:02 | 显示全部楼层
谢谢分享!串口DMA收发

出0入0汤圆

发表于 2019-11-5 11:38:45 | 显示全部楼层
两个中断方法可以及时收到完整数据,1是DMA传输完成中断内再等待串口传输完成约2个字节左右时间;2是不用DMA中断直接使用串口空闲中断处理

出0入0汤圆

发表于 2019-11-5 12:00:22 | 显示全部楼层
谢谢分享。。。

出0入0汤圆

 楼主| 发表于 2019-11-6 21:42:00 | 显示全部楼层
zpwkxg 发表于 2019-11-3 09:54
今天看清了楼主头像,是编织袋里面装的八只鸭子^_^,初看以为是个多足机器人O(∩_∩)O哈哈~ ...

当时在一个集市上看到的,装了一堆鸭子在那卖,觉得好玩,就拍下来了

出0入0汤圆

 楼主| 发表于 2019-11-6 21:49:25 | 显示全部楼层
我们公司现在做程序的原则是尽量不使用中断,这样可以避免调试过程中的太多问题。我们有个项目,用的103VC,3.5寸彩屏,50多个页面,加上5个串口数据,矩阵键盘,外加外置EEPROM,FLASH,RTC等,基本上103的资源用完了,一个中断没开,运行非常流畅不卡,按键响应正常,非常好用,以前开过中断,程序处理麻烦,很多重要地方都要关中断处理,后来要求不使用中断,就非常好用了,做了个简易时间片系统,没有任何响应速度慢的问题。

出0入0汤圆

发表于 2019-11-6 22:49:10 | 显示全部楼层
horizon0315 发表于 2019-11-6 21:49
我们公司现在做程序的原则是尽量不使用中断,这样可以避免调试过程中的太多问题。我们有个项目,用的103VC, ...

简易时间片系统的时间是用什么来保证的?一个定时器中断都没有吗?

出0入0汤圆

 楼主| 发表于 2019-11-11 19:40:06 | 显示全部楼层
bad_fpga 发表于 2019-11-6 22:49
简易时间片系统的时间是用什么来保证的?一个定时器中断都没有吗?

用定时器定时,不断查询,但不加中断,这样不会打断任务执行,执行完成后再切换任务,任务执行周期会有一定偏差,但都在接受范围,几十us误差。

出0入4汤圆

发表于 2019-11-11 22:14:59 | 显示全部楼层
串口+输入捕获+输出比较做自己的灵活空闲中断。。。

出0入0汤圆

发表于 2019-11-11 23:25:51 | 显示全部楼层
horizon0315 发表于 2019-11-11 19:40
用定时器定时,不断查询,但不加中断,这样不会打断任务执行,执行完成后再切换任务,任务执行周期会有一 ...

其实上OS写起来更方便些,我已经习惯了有rtos配合他的动态内存、队列这些,自己裸写太费劲了。

出0入0汤圆

发表于 2019-11-11 23:28:21 | 显示全部楼层
horizon0315 发表于 2019-11-11 19:40
用定时器定时,不断查询,但不加中断,这样不会打断任务执行,执行完成后再切换任务,任务执行周期会有一 ...

查询的效率肯定差些的,你这里面没有高要求的东西所以可以用。中断 DMA这些硬件性的东西合理利用才能发挥芯片的潜力。

出1310入193汤圆

发表于 2019-11-11 23:35:45 | 显示全部楼层
2:不用DMA中断直接使用串口空闲中断处理   DMA没有这个 来得妥当

出0入0汤圆

 楼主| 发表于 2019-11-12 15:24:01 | 显示全部楼层
我用的场合里面内容太多,几万行代码,出问题非常难找,所以屏蔽了所有中断,这样任务不会轻易打断,只要安排好时序,相对问题就少,多人协同开发,不会互相影响

出0入0汤圆

发表于 2019-11-13 08:21:33 | 显示全部楼层
又一收发不定长数据的方法!学习了

出0入0汤圆

发表于 2019-11-13 08:50:19 | 显示全部楼层
一直用dma+idle中断

出20入0汤圆

发表于 2020-3-3 15:30:32 | 显示全部楼层
不错,学习了,我试一下

出0入0汤圆

发表于 2020-3-12 09:40:50 | 显示全部楼层
谢谢分享!

出100入101汤圆

发表于 2021-6-24 19:58:25 | 显示全部楼层
不错,学习

出0入0汤圆

发表于 2021-6-27 17:46:13 | 显示全部楼层
接收:DMA+空闲中断,空闲中断接收时用定时器中断+FIFO解决所有问题

出100入101汤圆

发表于 2021-6-27 19:17:40 | 显示全部楼层
shijy1977 发表于 2021-6-27 17:46
接收:DMA+空闲中断,空闲中断接收时用定时器中断+FIFO解决所有问题

很多mcu不支持空闲中断的

出0入0汤圆

发表于 2021-6-29 01:00:59 | 显示全部楼层
谢谢分享,不错

出0入0汤圆

发表于 2021-6-29 12:43:23 来自手机 | 显示全部楼层
谢谢分享,不错

出0入0汤圆

发表于 2021-7-1 21:08:03 | 显示全部楼层
谢谢分享,正准备写这样的程序。

出0入0汤圆

发表于 2022-10-23 18:00:02 | 显示全部楼层
感谢分享,正在做HMI和单片机modbus rtu通信,觉得比空闲中断好用。
回帖提示: 反政府言论将被立即封锁ID 在按“提交”前,请自问一下:我这样表达会给举报吗,会给自己惹麻烦吗? 另外:尽量不要使用Mark、顶等没有意义的回复。不得大量使用大字体和彩色字。【本论坛不允许直接上传手机拍摄图片,浪费大家下载带宽和论坛服务器空间,请压缩后(图片小于1兆)才上传。压缩方法可以在微信里面发给自己(不要勾选“原图),然后下载,就能得到压缩后的图片】。另外,手机版只能上传图片,要上传附件需要切换到电脑版(不需要使用电脑,手机上切换到电脑版就行,页面底部)。
您需要登录后才可以回帖 登录 | 注册

本版积分规则

手机版|Archiver|amobbs.com 阿莫电子技术论坛 ( 粤ICP备2022115958号, 版权所有:东莞阿莫电子贸易商行 创办于2004年 (公安交互式论坛备案:44190002001997 ) )

GMT+8, 2024-4-26 09:06

© Since 2004 www.amobbs.com, 原www.ourdev.cn, 原www.ouravr.com

快速回复 返回顶部 返回列表