如何使用UART+DMA的方式进行数据的传输?

接口/总线/驱动

1120人已加入

描述

UART是做嵌入式产品非常重要的一个模块,它可以作为shell来进行软件调试,也可以简单的打印日志或错误信息,还可以用作数据通讯,比如工业总线,电力规约等都会用到。针对MCU而言,它可能是除GPIO外最常用的模块,但在使用过程中,有一些细节常常会被忽略而导致产品不稳定甚至死机,今天我们就聊聊UART## UART的连接

UART(Universal Asynchronous Receiver/Transmitter)是一种异步通讯,其中通讯的双方主要通过TX, RX交叉连接,有些MCU还支持硬件流控,那就又包含RTS和CTS这两个信号。支持硬件流控的MCU在驱动RS485收发器的时候,可以使用RTS作为收发使能的控制信号,这样软件工程师在写驱动的时候,就不需要控制GPIO来切换发送与接收,而且发送过程中,当数据写入D寄存器后,不需要等待移位的完成,就可以直接去处理其他任务

RS485收发器

下图来自Kinetis KV4参考手册

RS485收发器

UART的参数

UART主要的参数如下表所示

RS485收发器

与I2C/SPI不同,UART在通讯过程中是没有同步时钟的,所以需要用本地时钟采用对方发送的数据,这样就有一个容错的问题,也就是当时钟偏差多大后,通讯将无法建立。Kinetis KV参考手册中描述了计算方法:(RT cycles表示UART模块的系统时钟,每个UART bit都是由16倍的采样完成,并且在RT8,9,10这三个点采样)

针对时钟正偏的情况(时钟比数据快),在此情况下至少要保证最采样STOP bit 的RT8, RT9, RT10落在STOP电平(而不是倒数第一个字节)才能保证采样数据不出错

RS485收发器

那允许的误差就是7 RT cycles/总的RT cycles,而总的RT cycles与数据的长度有关,针对1bit起始+8bit数据,带入公式可以得出总的RT cycles = (9 x 16 + 10),容错率为 7 / (9 x 16 + 10) = 4.54%。针对1bit起始+8bit数据+1bit校验,容错率为7/(10x16 + 10) = 4.12%

针对时钟负偏的情况(时钟比数据慢),临界区应该是RT8, RT9, RT10采在MSB的末尾,这样就会多占用6个RT cycles,个人觉得这个图有点问题,应该把RT8, 9, 10放在STOP的末尾,RT11~16放到IDLE or NEXT FRAME,不过手册里计算公式是没有问题的:

RS485收发器

允许的误差就是-6 RT cycles/总的RT cycles,针对1bit起始+8bit数据,带入公式可以得出容错率 = -6/(10x16) = -3.9%,1bit起始+8bit数据+1bit校验容错率 = -6/(11x16) = -3.53%

所以如果想要稳定的UART通讯,一定要保证UART的时钟源正偏不超过4.12%,负偏不超过3.53%,如果设备要在全温范围内工作,建议还是使用外部晶体作为UART的时钟源或者检查下内部时钟是否能满足这个要求,下图是KL17内部时钟的相关参数,还需要说明一点,整个计算过程是认为对端设备时钟无误差,实际应用中应该保留一定的降额

RS485收发器

UART的使用

MCU芯片厂商往往都会提供UART的相关示例,通常有3中模式,针对这三种不同的模式,用户可以根据自身的需求来进行选择:

RS485收发器

UART的示例

今天我们就以RT1170为例,接收下如何使用UART+DMA的方式进行数据的传输。首先我们Clone一个UART with DMA的工程,原始工程在MCUXpresso SDK目录boardsevkmimxrt1170driver_exampleslpuartedma_transfer中,我们先看下原始代码:

//初始化时钟,Pin
BOARD_ConfigMPU();
BOARD_InitPins();
BOARD_BootClockRUN();

/* Initialize the LPUART. */
/*
 * lpuartConfig.baudRate_Bps = 115200U;
 * lpuartConfig.parityMode = kLPUART_ParityDisabled;
 * lpuartConfig.stopBitCount = kLPUART_OneStopBit;
 * lpuartConfig.txFifoWatermark = 0;
 * lpuartConfig.rxFifoWatermark = 0;
 * lpuartConfig.enableTx = false;
 * lpuartConfig.enableRx = false;
 */
//初始化LPUART
LPUART_GetDefaultConfig(&lpuartConfig);
lpuartConfig.baudRate_Bps = BOARD_DEBUG_UART_BAUDRATE;
lpuartConfig.enableTx     = true;
lpuartConfig.enableRx     = true;

LPUART_Init(DEMO_LPUART, &lpuartConfig, DEMO_LPUART_CLK_FREQ);
//初始化DMA
#if defined(FSL_FEATURE_SOC_DMAMUX_COUNT) && FSL_FEATURE_SOC_DMAMUX_COUNT
/* Init DMAMUX */
DMAMUX_Init(EXAMPLE_LPUART_DMAMUX_BASEADDR);
/* Set channel for LPUART */
DMAMUX_SetSource(EXAMPLE_LPUART_DMAMUX_BASEADDR, LPUART_TX_DMA_CHANNEL, LPUART_TX_DMA_REQUEST);
DMAMUX_SetSource(EXAMPLE_LPUART_DMAMUX_BASEADDR, LPUART_RX_DMA_CHANNEL, LPUART_RX_DMA_REQUEST);
DMAMUX_EnableChannel(EXAMPLE_LPUART_DMAMUX_BASEADDR, LPUART_TX_DMA_CHANNEL);
DMAMUX_EnableChannel(EXAMPLE_LPUART_DMAMUX_BASEADDR, LPUART_RX_DMA_CHANNEL);
#endif
/* Init the EDMA module */
EDMA_GetDefaultConfig(&config);
EDMA_Init(EXAMPLE_LPUART_DMA_BASEADDR, &config);
EDMA_CreateHandle(&g_lpuartTxEdmaHandle, EXAMPLE_LPUART_DMA_BASEADDR, LPUART_TX_DMA_CHANNEL);
EDMA_CreateHandle(&g_lpuartRxEdmaHandle, EXAMPLE_LPUART_DMA_BASEADDR, LPUART_RX_DMA_CHANNEL);
#if defined(FSL_FEATURE_EDMA_HAS_CHANNEL_MUX) && FSL_FEATURE_EDMA_HAS_CHANNEL_MUX
EDMA_SetChannelMux(EXAMPLE_LPUART_DMA_BASEADDR, LPUART_TX_DMA_CHANNEL, DEMO_LPUART_TX_EDMA_CHANNEL);
EDMA_SetChannelMux(EXAMPLE_LPUART_DMA_BASEADDR, LPUART_RX_DMA_CHANNEL, DEMO_LPUART_RX_EDMA_CHANNEL);
#endif
/* Create LPUART DMA handle. */
LPUART_TransferCreateHandleEDMA(DEMO_LPUART, &g_lpuartEdmaHandle, LPUART_UserCallback, NULL, &g_lpuartTxEdmaHandle,&g_lpuartRxEdmaHandle);
//通过DMA发送字符串g_tipString数组
/* Send g_tipString out. */
xfer.data     = g_tipString;
xfer.dataSize = sizeof(g_tipString) - 1;
txOnGoing     = true;
LPUART_SendEDMA(DEMO_LPUART, &g_lpuartEdmaHandle, &xfer);
//等待发送完成
/* Wait send finished */
while (txOnGoing)
{
}
//设置While(1)发送/接收的数据长度
/* Start to echo. */
sendXfer.data        = g_txBuffer;
sendXfer.dataSize    = ECHO_BUFFER_LENGTH;
receiveXfer.data     = g_rxBuffer;
receiveXfer.dataSize = ECHO_BUFFER_LENGTH;

这段函数中,使能了UART TX DMA完成中断以及UART RX DMA中断,下面是中断服务函数:

/* LPUART user callback */
void LPUART_UserCallback(LPUART_Type *base, lpuart_edma_handle_t *handle, status_t status, void *userData)
{
    userData = userData;

    if (kStatus_LPUART_TxIdle == status)
    {
        txBufferFull = false;
        txOnGoing    = false;
    }

    if (kStatus_LPUART_RxIdle == status)
    {
        rxBufferEmpty = false;
        rxOnGoing     = false;
    }
}

进入while(1)后,每接收到8个字节就会将收到的数据发送回来

while (1)
{
    /* If RX is idle and g_rxBuffer is empty, start to read data to g_rxBuffer. */
    if ((!rxOnGoing) && rxBufferEmpty)
    {
        rxOnGoing = true;
        LPUART_ReceiveEDMA(DEMO_LPUART, &g_lpuartEdmaHandle, &receiveXfer);
    }

    /* If TX is idle and g_txBuffer is full, start to send data. */
    if ((!txOnGoing) && txBufferFull)
    {
        txOnGoing = true;
        LPUART_SendEDMA(DEMO_LPUART, &g_lpuartEdmaHandle, &sendXfer);
    }

    /* If g_txBuffer is empty and g_rxBuffer is full, copy g_rxBuffer to g_txBuffer. */
    if ((!rxBufferEmpty) && (!txBufferFull))
    {
        memcpy(g_txBuffer, g_rxBuffer, ECHO_BUFFER_LENGTH);
        rxBufferEmpty = true;
        txBufferFull  = true;
    }
}

这个Demo可以展示如何使用DMA来传输UART,但是实际用户在使用的时候却很难使用,问题主要出在接收端。各种通讯协议很少是有固定字节长度的,比如Modbus,Profibus-DP。针对不定长数据的接收,我们常见有以下几种做法:

  1. 使能中断接收,这样做软件处理会比较简单,但是会占用CPU的中断资源,每接收1个字节都会产生一次中断。同时如果系统中需要支持多串口通讯,还可能会出现OR(Receiver Overrun)错误而导致丢帧
  2. 使用DMA接收数据,使能IdleLineInterrupt,并配置空闲字节长度,在Idle中断服务程序中Copy数据到用户空间。以Modbus为例,其要求发送帧间隔是3.5Char,也就是每帧之间的必须等待超过3.5倍的字节时间长度,不同的波特率对应不同的等待时间,我们可以配置空闲长度为4个字节,这样MCU如果接收端连续等待4个字节长度时间都是高电平后产生一个中断(RT1170支持从起始位或者停止位开始计数)

对这个代码我们可以做简单的修改以实现不定长数据的接收:

配置Idle从Stop位开始计算,空闲等待4个Char长度

LPUART_GetDefaultConfig(&lpuartConfig);
lpuartConfig.baudRate_Bps = BOARD_DEBUG_UART_BAUDRATE;
lpuartConfig.enableTx     = true;
lpuartConfig.enableRx     = true;

lpuartConfig.rxIdleType = kLPUART_IdleTypeStopBit;
lpuartConfig.rxIdleConfig = kLPUART_IdleCharacter4;

LPUART_Init(DEMO_LPUART, &lpuartConfig, DEMO_LPUART_CLK_FREQ);

使能UART中断,这里需要注意UART在通讯过程中往往会遇到干扰而导致一些错误或者异常,MCU在获取异常后会置位一些错误标志,这些标志一定要进行处理,否则有可能出现接收终止而导致通讯失败。不同的芯片针对错误标志的清除方法是不同的(有的需要读D寄存器,有的需要W1C),一定要根据手册来处理:

LPUART_EnableInterrupts(DEMO_LPUART, kLPUART_RxOverrunInterruptEnable
| kLPUART_NoiseErrorInterruptEnable | kLPUART_IdleLineInterruptEnable
| kLPUART_FramingErrorInterruptEnable | kLPUART_ParityErrorInterruptEnable);
EnableIRQ(DEMO_UART_IRQn);

RS485收发器

在EDMA_CreateHandle函数中关闭DMA中断,因为已经开启串口IDLE中断,就没必要再DMA中断,而且很有可能DMA中断尚未产生,帧数据已经接收完毕了

/* Get the DMA instance number */
edmaInstance = EDMA_GetInstance(base);
channelIndex = (EDMA_GetInstanceOffset(edmaInstance) * (uint32_t)FSL_FEATURE_EDMA_MODULE_CHANNEL) + channel;
s_EDMAHandle[channelIndex] = handle;

/* Enable NVIC interrupt */
//(void)EnableIRQ(s_edmaIRQNumber[edmaInstance][channel]);

当DMA尚未接收到全部数据时,如果帧已经结束,那我们就必须知道当前DMA传输了多少个数据,所以可以编写一个函数来获取这个值

static uint32_t GetRingBufferLengthDMA(void)
{
    return (RS232_MAX_BUFFER - EDMA_GetRemainingMajorLoopCount(EXAMPLE_LPUART_DMA_BASEADDR, LPUART_RX_DMA_CHANNEL));
}

在UART中断服务函数中Copy数据到用户空间,并做异常处理

void LPUART_RX_ISR()
{
    uint32_t status = 0;

    status = LPUART_GetStatusFlags(DEMO_LPUART);

    if ((kLPUART_IdleLineFlag) & status)
    {
	LPUART_ClearStatusFlags(DEMO_LPUART, kLPUART_IdleLineFlag);
	g_rxBuffer.uCount = GetRingBufferLengthDMA();
	g_txBuffer.uCount = g_rxBuffer.uCount;
	memcpy((void *)&g_txBuffer.byData, (void *)&g_rxBuffer.byData, g_rxBuffer.uCount);
        //继续接收下一帧数据
	LPUART_TransferAbortReceiveEDMA(DEMO_LPUART, &g_lpuartEdmaHandle);
	memset((void *)&g_rxBuffer, 0, sizeof(g_rxBuffer));
	receiveXfer.data  = &g_rxBuffer.byData[0];
	receiveXfer.dataSize = RS232_MAX_BUFFER;
		
	LPUART_ReceiveEDMA(DEMO_LPUART, &g_lpuartEdmaHandle, &receiveXfer);
	g_UartReceivedFlag = 1;

    }

    if ((kLPUART_LinBreakInterruptEnable|kLPUART_RxOverrunInterruptEnable
	| kLPUART_NoiseErrorInterruptEnable | kLPUART_FramingErrorInterruptEnable 
	| kLPUART_ParityErrorInterruptEnable) & status)
    {
	LPUART_ClearStatusFlags(DEMO_LPUART, kLPUART_LinBreakInterruptEnable
	|kLPUART_RxOverrunInterruptEnable | kLPUART_NoiseErrorInterruptEnable
	| kLPUART_FramingErrorInterruptEnable | kLPUART_ParityErrorInterruptEnable);
    }
		
    SDK_ISR_EXIT_BARRIER;
}

封装接收函数,每次要接收数据前,调用该函数即可:

LPUART_TransferAbortReceiveEDMA(DEMO_LPUART, &g_lpuartEdmaHandle);
memset((void *)&g_rxBuffer, 0, sizeof(g_rxBuffer));
receiveXfer.data  = &g_rxBuffer.byData[0];
receiveXfer.dataSize = RS232_MAX_BUFFER;
		
LPUART_ReceiveEDMA(DEMO_LPUART, &g_lpuartEdmaHandle, &receiveXfer);

封装发送函数,每次需要发送数据前,调用该函数即可:

void uart_sendDMA(uint32_t len)
{
    /* Send g_tipString out. */
    sendXfer.data     = &g_txBuffer.byData[0];
    if(len < RS232_MAX_BUFFER)
    {
	sendXfer.dataSize = len;
    }
    else
    {
	sendXfer.dataSize = RS232_MAX_BUFFER;
    }
    g_lpuartEdmaHandle.txState = kStatus_LPUART_TxIdle;
    LPUART_SendEDMA(DEMO_LPUART, &g_lpuartEdmaHandle, &sendXfer);

}

修改主循环,同样改为还回模式

while(1)
 {
if(g_UartReceivedFlag)
    {
	uart_sendDMA(g_txBuffer.uCount);
	g_UartReceivedFlag = 0;    }
 }

1.PC端开始串口调试助手并设置自动发送数据,帧间隔最好都修改下

RS485收发器

  1. 通过判断发送与接收的数据个数以判断是否有丢包或者死机的情况。
打开APP阅读更多精彩内容
声明:本文内容及配图由入驻作者撰写或者入驻合作网站授权转载。文章观点仅代表作者本人,不代表电子发烧友网立场。文章及其配图仅供工程师学习之用,如有内容侵权或者其他违规问题,请联系本站处理。 举报投诉

全部0条评论

快来发表一下你的评论吧 !

×
20
完善资料,
赚取积分