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端開始串口調試助手并設置自動發送數據,幀間隔最好都修改下
- 通過判斷發送與接收的數據個數以判斷是否有丟包或者死機的情況。
評論
查看更多