UART是做嵌入式產(chǎn)品非常重要的一個(gè)模塊,它可以作為shell來進(jìn)行軟件調(diào)試,也可以簡單的打印日志或錯(cuò)誤信息,還可以用作數(shù)據(jù)通訊,比如工業(yè)總線,電力規(guī)約等都會用到。針對MCU而言,它可能是除GPIO外最常用的模塊,但在使用過程中,有一些細(xì)節(jié)常常會被忽略而導(dǎo)致產(chǎn)品不穩(wěn)定甚至死機(jī),今天我們就聊聊UART## UART的連接
UART(Universal Asynchronous Receiver/Transmitter)是一種異步通訊,其中通訊的雙方主要通過TX, RX交叉連接,有些MCU還支持硬件流控,那就又包含RTS和CTS這兩個(gè)信號。支持硬件流控的MCU在驅(qū)動(dòng)RS485收發(fā)器的時(shí)候,可以使用RTS作為收發(fā)使能的控制信號,這樣軟件工程師在寫驅(qū)動(dòng)的時(shí)候,就不需要控制GPIO來切換發(fā)送與接收,而且發(fā)送過程中,當(dāng)數(shù)據(jù)寫入D寄存器后,不需要等待移位的完成,就可以直接去處理其他任務(wù)
下圖來自Kinetis KV4參考手冊
UART的參數(shù)
UART主要的參數(shù)如下表所示
與I2C/SPI不同,UART在通訊過程中是沒有同步時(shí)鐘的,所以需要用本地時(shí)鐘采用對方發(fā)送的數(shù)據(jù),這樣就有一個(gè)容錯(cuò)的問題,也就是當(dāng)時(shí)鐘偏差多大后,通訊將無法建立。Kinetis KV參考手冊中描述了計(jì)算方法:(RT cycles表示UART模塊的系統(tǒng)時(shí)鐘,每個(gè)UART bit都是由16倍的采樣完成,并且在RT8,9,10這三個(gè)點(diǎn)采樣)
針對時(shí)鐘正偏的情況(時(shí)鐘比數(shù)據(jù)快),在此情況下至少要保證最采樣STOP bit 的RT8, RT9, RT10落在STOP電平(而不是倒數(shù)第一個(gè)字節(jié))才能保證采樣數(shù)據(jù)不出錯(cuò)
那允許的誤差就是7 RT cycles/總的RT cycles,而總的RT cycles與數(shù)據(jù)的長度有關(guān),針對1bit起始+8bit數(shù)據(jù),帶入公式可以得出總的RT cycles = (9 x 16 + 10),容錯(cuò)率為 7 / (9 x 16 + 10) = 4.54%。針對1bit起始+8bit數(shù)據(jù)+1bit校驗(yàn),容錯(cuò)率為7/(10x16 + 10) = 4.12%
針對時(shí)鐘負(fù)偏的情況(時(shí)鐘比數(shù)據(jù)慢),臨界區(qū)應(yīng)該是RT8, RT9, RT10采在MSB的末尾,這樣就會多占用6個(gè)RT cycles,個(gè)人覺得這個(gè)圖有點(diǎn)問題,應(yīng)該把RT8, 9, 10放在STOP的末尾,RT11~16放到IDLE or NEXT FRAME,不過手冊里計(jì)算公式是沒有問題的:
允許的誤差就是-6 RT cycles/總的RT cycles,針對1bit起始+8bit數(shù)據(jù),帶入公式可以得出容錯(cuò)率 = -6/(10x16) = -3.9%,1bit起始+8bit數(shù)據(jù)+1bit校驗(yàn)容錯(cuò)率 = -6/(11x16) = -3.53%
所以如果想要穩(wěn)定的UART通訊,一定要保證UART的時(shí)鐘源正偏不超過4.12%,負(fù)偏不超過3.53%,如果設(shè)備要在全溫范圍內(nèi)工作,建議還是使用外部晶體作為UART的時(shí)鐘源或者檢查下內(nèi)部時(shí)鐘是否能滿足這個(gè)要求,下圖是KL17內(nèi)部時(shí)鐘的相關(guān)參數(shù),還需要說明一點(diǎn),整個(gè)計(jì)算過程是認(rèn)為對端設(shè)備時(shí)鐘無誤差,實(shí)際應(yīng)用中應(yīng)該保留一定的降額
UART的使用
MCU芯片廠商往往都會提供UART的相關(guān)示例,通常有3中模式,針對這三種不同的模式,用戶可以根據(jù)自身的需求來進(jìn)行選擇:
UART的示例
今天我們就以RT1170為例,接收下如何使用UART+DMA的方式進(jìn)行數(shù)據(jù)的傳輸。首先我們Clone一個(gè)UART with DMA的工程,原始工程在MCUXpresso SDK目錄boardsevkmimxrt1170driver_exampleslpuartedma_transfer中,我們先看下原始代碼:
//初始化時(shí)鐘,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發(fā)送字符串g_tipString數(shù)組
/* Send g_tipString out. */
xfer.data = g_tipString;
xfer.dataSize = sizeof(g_tipString) - 1;
txOnGoing = true;
LPUART_SendEDMA(DEMO_LPUART, &g_lpuartEdmaHandle, &xfer);
//等待發(fā)送完成
/* Wait send finished */
while (txOnGoing)
{
}
//設(shè)置While(1)發(fā)送/接收的數(shù)據(jù)長度
/* Start to echo. */
sendXfer.data = g_txBuffer;
sendXfer.dataSize = ECHO_BUFFER_LENGTH;
receiveXfer.data = g_rxBuffer;
receiveXfer.dataSize = ECHO_BUFFER_LENGTH;
這段函數(shù)中,使能了UART TX DMA完成中斷以及UART RX DMA中斷,下面是中斷服務(wù)函數(shù):
/* 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;
}
}
進(jìn)入while(1)后,每接收到8個(gè)字節(jié)就會將收到的數(shù)據(jù)發(fā)送回來
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;
}
}
這個(gè)Demo可以展示如何使用DMA來傳輸U(kuò)ART,但是實(shí)際用戶在使用的時(shí)候卻很難使用,問題主要出在接收端。各種通訊協(xié)議很少是有固定字節(jié)長度的,比如Modbus,Profibus-DP。針對不定長數(shù)據(jù)的接收,我們常見有以下幾種做法:
- 使能中斷接收,這樣做軟件處理會比較簡單,但是會占用CPU的中斷資源,每接收1個(gè)字節(jié)都會產(chǎn)生一次中斷。同時(shí)如果系統(tǒng)中需要支持多串口通訊,還可能會出現(xiàn)OR(Receiver Overrun)錯(cuò)誤而導(dǎo)致丟幀
- 使用DMA接收數(shù)據(jù),使能IdleLineInterrupt,并配置空閑字節(jié)長度,在Idle中斷服務(wù)程序中Copy數(shù)據(jù)到用戶空間。以Modbus為例,其要求發(fā)送幀間隔是3.5Char,也就是每幀之間的必須等待超過3.5倍的字節(jié)時(shí)間長度,不同的波特率對應(yīng)不同的等待時(shí)間,我們可以配置空閑長度為4個(gè)字節(jié),這樣MCU如果接收端連續(xù)等待4個(gè)字節(jié)長度時(shí)間都是高電平后產(chǎn)生一個(gè)中斷(RT1170支持從起始位或者停止位開始計(jì)數(shù))
對這個(gè)代碼我們可以做簡單的修改以實(shí)現(xiàn)不定長數(shù)據(jù)的接收:
配置Idle從Stop位開始計(jì)算,空閑等待4個(gè)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在通訊過程中往往會遇到干擾而導(dǎo)致一些錯(cuò)誤或者異常,MCU在獲取異常后會置位一些錯(cuò)誤標(biāo)志,這些標(biāo)志一定要進(jìn)行處理,否則有可能出現(xiàn)接收終止而導(dǎo)致通訊失敗。不同的芯片針對錯(cuò)誤標(biāo)志的清除方法是不同的(有的需要讀D寄存器,有的需要W1C),一定要根據(jù)手冊來處理:
LPUART_EnableInterrupts(DEMO_LPUART, kLPUART_RxOverrunInterruptEnable
| kLPUART_NoiseErrorInterruptEnable | kLPUART_IdleLineInterruptEnable
| kLPUART_FramingErrorInterruptEnable | kLPUART_ParityErrorInterruptEnable);
EnableIRQ(DEMO_UART_IRQn);
在EDMA_CreateHandle函數(shù)中關(guān)閉DMA中斷,因?yàn)橐呀?jīng)開啟串口IDLE中斷,就沒必要再DMA中斷,而且很有可能DMA中斷尚未產(chǎn)生,幀數(shù)據(jù)已經(jīng)接收完畢了
/* 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]);
當(dāng)DMA尚未接收到全部數(shù)據(jù)時(shí),如果幀已經(jīng)結(jié)束,那我們就必須知道當(dāng)前DMA傳輸了多少個(gè)數(shù)據(jù),所以可以編寫一個(gè)函數(shù)來獲取這個(gè)值
static uint32_t GetRingBufferLengthDMA(void)
{
return (RS232_MAX_BUFFER - EDMA_GetRemainingMajorLoopCount(EXAMPLE_LPUART_DMA_BASEADDR, LPUART_RX_DMA_CHANNEL));
}
在UART中斷服務(wù)函數(shù)中Copy數(shù)據(jù)到用戶空間,并做異常處理
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);
//繼續(xù)接收下一幀數(shù)據(jù)
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;
}
封裝接收函數(shù),每次要接收數(shù)據(jù)前,調(diào)用該函數(shù)即可:
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);
封裝發(fā)送函數(shù),每次需要發(fā)送數(shù)據(jù)前,調(diào)用該函數(shù)即可:
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);
}
修改主循環(huán),同樣改為還回模式
while(1)
{
if(g_UartReceivedFlag)
{
uart_sendDMA(g_txBuffer.uCount);
g_UartReceivedFlag = 0; }
}
1.PC端開始串口調(diào)試助手并設(shè)置自動(dòng)發(fā)送數(shù)據(jù),幀間隔最好都修改下
- 通過判斷發(fā)送與接收的數(shù)據(jù)個(gè)數(shù)以判斷是否有丟包或者死機(jī)的情況。
評論
查看更多