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

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

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

下图来自Kinetis KV4参考手册

UART的参数

UART主要的参数如下表所示

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

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

那允许的误差就是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/(10×16 + 10) = 4.12%

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

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

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

UART的使用

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

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。针对不定长数据的接收,我们常见有以下几种做法:

使能中断接收,这样做软件处理会比较简单,但是会占用CPU的中断资源,每接收1个字节都会产生一次中断。同时如果系统中需要支持多串口通讯,还可能会出现OR(Receiver Overrun)错误而导致丢帧使用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);

在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端开始串口调试助手并设置自动发送数据,帧间隔最好都修改下

通过判断发送与接收的数据个数以判断是否有丢包或者死机的情况。

发表评论