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/(10x16 + 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/(10x16) = -3.9%,1bit起始+8bit数据+1bit校验容错率 = -6/(11x16) = -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端开始串口调试助手并设置自动发送数据,帧间隔最好都修改下
- 通过判断发送与接收的数据个数以判断是否有丢包或者死机的情况。
评论
查看更多