第3章 UART 開發(fā)基礎(chǔ)
3.1 同步傳輸與異步傳輸
3.1.1 概念與示例
使用生活例子來說明什么是同步、異步:
- 同步:朋友打電話說到我家吃飯,我在家里等他們
- 異步:朋友沒有提前打招呼,突然就到我家來了
它們的差別在于:有沒有使用一種方法“實現(xiàn)約好時間”。
在電子產(chǎn)品中, 使用同步傳輸時, 一般涉及兩個信號:
- 時鐘信號:用來通知對方要讀取數(shù)據(jù)了
- 數(shù)據(jù)信號:用來傳輸數(shù)據(jù)
同步傳輸示例如下:
- 時鐘信號:打電話,起約定作用
- 數(shù)據(jù)信號:傳輸數(shù)據(jù)
異步傳輸示例如下:
使用異步信號傳輸數(shù)據(jù)時,雙方遵守相同的約定:
- 起始信號:發(fā)送方可以通知接收方"注意了,我要開始傳輸數(shù)據(jù)了"
- 數(shù)據(jù)的表示: 怎么表示邏輯 1,怎么表示邏輯 0。
以紅外遙控器解碼器為例,它向單片機發(fā)出的數(shù)據(jù)格式如下:
- 起始信號:解碼器發(fā)出一個 9ms 的低電平、 4.5ms 的高電平, 用來同時對方說"開始了"
- 表示一位數(shù)據(jù)
- 邏輯 1:0.56ms 的低電平+1.69ms 的高電平
- 邏輯 0:0.56ms 的低電平+0.56ms 的高電平
- 接收方、發(fā)送方都遵守這樣的約定, 就可以使用一條線傳輸數(shù)據(jù)
3.1.2 差別
同步傳輸 | 異步傳輸 | |
---|---|---|
信號線 | 多:時鐘信號、數(shù)據(jù)信號 | 少:只需要數(shù)據(jù)信號 |
速率 | 可變, 提高時鐘信號頻率即可 | 雙方提前約定 |
抗干擾能力 | 強 | 弱 |
3.2 UART 協(xié)議與操作方法
3.2.1 UART 協(xié)議
通用異步收發(fā)器簡稱 UART,即“Universal Asynchronous Receiver Transmitter”, 它用來傳輸串行數(shù)據(jù):發(fā)送數(shù)據(jù)時,CPU 將并行數(shù)據(jù)寫入 UART,UART 按照一定的格式在一 根電線上串行發(fā)出;接收數(shù)據(jù)時, UART 檢測另一根電線上的信號,將串行數(shù)據(jù)收集放在緩 沖區(qū)中,CPU 即可讀取 UART 獲得這些數(shù)據(jù)。 UART 之間以全雙工方式傳輸數(shù)據(jù),最精簡的連 線方法只有三根電線:TxD 用于發(fā)送數(shù)據(jù), RxD 用于接收數(shù)據(jù),GND 用于給雙方提供參考電 平,連線如圖所示:
UART 使用標(biāo)準(zhǔn)的 TTL/CMOS 邏輯電平(0~5V、0~3.3V、0~2.5V 或 0~1.8V 四種)來表 示數(shù)據(jù),高電平表示 1,低電平表示 0。進行長距離傳輸時,為了增強數(shù)據(jù)的抗干擾能力、 提高傳輸長度, 通常將 TTL/CMOS 邏輯電平轉(zhuǎn)換為 RS-232 邏輯電平, 3~12V 表示 0,-3~- 12V 表示 1。
TxD、RxD 數(shù)據(jù)線以“位”為最小單位傳輸數(shù)據(jù)。幀(frame)由具有完整意義的、不可 分割的若干位組成,它包含開始位、數(shù)據(jù)位、較驗位(需要的話)和停止位。發(fā)送數(shù)據(jù)之前,
UART 之間要約定好數(shù)據(jù)的傳輸速率(即每位所占據(jù)的時間,其倒數(shù)稱為波特率)、數(shù)據(jù)的傳
輸格式(即有多少個數(shù)據(jù)位、是否使用較驗位、是奇較驗還是偶較驗、有多少個停止位)。 數(shù)據(jù)傳輸流程如下:
- 平時數(shù)據(jù)線處于“空閉”狀態(tài)(1 狀態(tài))。
- 當(dāng)要發(fā)送數(shù)據(jù)時,UART 改變 TxD 數(shù)據(jù)線的狀態(tài)(變?yōu)?0 狀態(tài))并維持 1 位的時間──這 樣接收方檢測到開始位后,再等待 1.5 位的時間就開始一位一位地檢測數(shù)據(jù)線的狀態(tài) 得到所傳輸?shù)臄?shù)據(jù)。
- UART 一幀中可以有 5、6、7 或 8 位的數(shù)據(jù),發(fā)送方一位一位地改變數(shù)據(jù)線的狀態(tài)將它 們發(fā)送出去,首先發(fā)送最低位。
- 如果使用較驗功能, UART 在發(fā)送完數(shù)據(jù)位后,還要發(fā)送 1 個較驗位。有兩種較驗方法: 奇較驗、偶較驗──數(shù)據(jù)位連同較驗位中, “1”的數(shù)目等于奇數(shù)或偶數(shù)。
- 最后, 發(fā)送停止位, 數(shù)據(jù)線恢復(fù)到“空閉”狀態(tài)(1 狀態(tài))。停止位的長度有 3 種: 1 位、 1.5 位、 2 位。
下圖演示了 UART 使用 7 個數(shù)據(jù)位、偶較驗、2 個停止位的格式傳輸字符“A ”(二進制 值為 0b01000001)時, TTL/CMOS 邏輯電平、 RS-232 邏輯電平對應(yīng)的波形。
雙方約定了“傳輸一 bit 數(shù)據(jù)的時間”, 就可以算出 1 秒內(nèi)能傳輸多少 bit 數(shù)據(jù), 這 被稱為“比特率”, 又經(jīng)常被稱為“波特率”。兩者有什么關(guān)系?
假設(shè)發(fā)送方 A 能精確控制信號的電壓, 接收方 B 也能精確識別電壓, 雙方如此約定:
電壓范圍 | 表示的兩 bit 數(shù)據(jù) |
---|---|
0~0.7V | 0b00 |
~0.8 1.5V | 0b01 |
1.6~2.3V | 0b10 |
~2.4 3.3V | 0b11 |
那么要傳輸一個字節(jié)的數(shù)據(jù), 比如 0x78,它的二進制數(shù)為 0b01,11,10,00,只需要傳 輸 4 次(假設(shè) 1ms 改變一次電壓, 假設(shè)先傳輸?shù)臀唬?/p>
- 第 1ms,A 設(shè)置電壓為 0V,B 識別出電壓后,認(rèn)為收到了 bit1 為 0、bit0 為 0
- 第 2ms,A 設(shè)置電壓為 1.6V,B 識別出電壓后, 認(rèn)為收到了 bit3 為 1、bit2 為 0
- 第 3ms,A 設(shè)置電壓為 2.4V,B 識別出電壓后, 認(rèn)為收到了 bit5 為 1、bit4 為 1
- 第 4ms,A 設(shè)置電壓為 0.8V,B 識別出電壓后, 認(rèn)為收到了 bit7 為 0、bit6 為 1
只需要 4ms,就傳輸了 4 個狀態(tài),但是傳輸了 8bit 數(shù)據(jù):波特率*2=比特率。
假設(shè)發(fā)送方 A 精確控制信號電壓的能力比較差,只能保證 00.7V、1.83.3V 的電壓比 較穩(wěn)定;接收方 B 識別電壓的能力也不夠精確,只能保證可以識別出 0~0.7V、1.8 3.3V 的 電壓, 于是雙方約定:
電壓范圍 | 表示的 1 bit 數(shù)據(jù) |
---|---|
0~0.7V | 0 |
1.8~3.3V | 1 |
那么要傳輸一個字節(jié)的數(shù)據(jù), 比如 0x78,它的二進制數(shù)為 0b01111000,需要傳輸 8 次 (假設(shè) 1ms 改變一次電壓,假設(shè)先傳輸?shù)臀唬?/p>
- 第 1ms,A 設(shè)置電壓為 0V,B 識別出電壓后,認(rèn)為收到了 bit0 為 0
- 第 2ms,A 設(shè)置電壓為 0V,B 識別出電壓后,認(rèn)為收到了 bit1 為 0
- 第 3ms,A 設(shè)置電壓為 0V,B 識別出電壓后,認(rèn)為收到了 bit2 為 0
- 第 4ms,A 設(shè)置電壓為 3.3V,B 識別出電壓后, 認(rèn)為收到了 bit3 為 1
- 第 5ms,A 設(shè)置電壓為 3.3V,B 識別出電壓后, 認(rèn)為收到了 bit4 為 1
- 第 6ms,A 設(shè)置電壓為 3.3V,B 識別出電壓后, 認(rèn)為收到了 bit5 為 1
- 第 7ms,A 設(shè)置電壓為 3.3V,B 識別出電壓后, 認(rèn)為收到了 bit6 為 1
- 第 8ms,A 設(shè)置電壓為 0V,B 識別出電壓后,認(rèn)為收到了 bit7 為 0
需要 8ms,傳輸 8 個狀態(tài), 傳輸了 8bit 數(shù)據(jù):波特率=比特率。
所以,波特率: 1 秒內(nèi)傳輸信號的狀態(tài)數(shù)(波形數(shù))。比特率: 1 秒內(nèi)傳輸數(shù)據(jù)的 bit 數(shù)。如果一個波形, 能表示 N 個 bit,那么:波特率 * N = 比特率。
3.2.2 STM32H5 UART 硬件結(jié)構(gòu)
3.2.3 RS485 協(xié)議
使用 RS485 協(xié)議傳輸數(shù)據(jù)時, 電路圖如下:
RS485 協(xié)議里,使用 A、B 差分信號線傳輸數(shù)據(jù): 兩線間的電壓差為+(2 至 6)V 表示 邏輯 1,電壓差為-(2 至 6)V 時表示邏輯 0。它是半雙工的傳輸方式:MCU1 要發(fā)送數(shù)據(jù) 時,從 TxD 引腳把數(shù)據(jù)發(fā)送給電平轉(zhuǎn)換芯片 MAX13487EESA,它把 TxD 的信號轉(zhuǎn)換為差分信 號傳遞給另一個電平轉(zhuǎn)換芯片 MAX13487EESA,進而轉(zhuǎn)換為 TTL 電平通過 RO 發(fā)送到 MCU2 的 RxD 引腳。MCU2 要給 MCU1 發(fā)送數(shù)據(jù)的話,必須等待差分信號線處于空閑狀態(tài)。
對于軟件而言, 使用 RS485 跟普通的 UART 沒有區(qū)別。
3.3 UART 編程
3.3.1 硬件連接
按照下圖連線: 調(diào)試、供電、兩個 485 互連。
3.3.2 三種編程方式
結(jié)合 UART 硬件結(jié)構(gòu), 有 3 種編程方法:
- 查詢方式:
- 要發(fā)送數(shù)據(jù)時, 先把數(shù)據(jù)寫入 TDR 寄存器, 然后判斷 TDR 為空再返回。當(dāng)然也可以先 判斷 TDR 為空, 再寫入。
- 要讀取數(shù)據(jù)時, 先判斷 RDR 非空, 再讀取 RDR 得到數(shù)據(jù)。
- 中斷方式:
- 使用中斷方式, 效率更高,并且可以在接收數(shù)據(jù)時避免數(shù)據(jù)丟失。
- 要發(fā)送數(shù)據(jù)時, 使能“TXE”中斷(發(fā)送寄存器空中斷)。在 TXE 中斷處理函數(shù)里, 從 程序的發(fā)送 buffer 里取出一個數(shù)據(jù), 寫入 TDR。等再次發(fā)生 TXE 中斷時, 再從程序的發(fā)送 buffer 里取出下一個數(shù)據(jù)寫入 TDR。
- 對于接收數(shù)據(jù),在一開始就使能“RXNE”中斷(接收寄存器非空) 。這樣,UART 接收 到一個數(shù)據(jù)就會觸發(fā)中斷,在中斷程序里讀取 RDR 得到數(shù)據(jù), 存入程序的接收 buffer。當(dāng) 程序向讀取串口數(shù)據(jù)時, 它直接讀取接收 buffer 即可。
- 這里涉及的“發(fā)送 buffer”、“接收 buffer”,特別適合使用“環(huán)形 buffer ”。
- DMA 方式:
- 使用中斷方式時, 在傳輸、接收數(shù)據(jù)時,會發(fā)生中斷, 還需要 CPU 執(zhí)行中斷處理函數(shù)。 有另外一種方法:DMA(Direct Memory Access), 它可以直接在 2 個設(shè)備之間傳遞數(shù)據(jù),無需 CPU 參與。
框圖如下:
設(shè)置好 DMA(源、目的、地址增減方向、每次讀取數(shù)據(jù)的長度、讀取次數(shù))后,DMA 就 會自動地在 SRAM 和 UART 之間傳遞數(shù)據(jù):
- 發(fā)送時: DMA 從 SRAM 得到數(shù)據(jù), 寫入 UART 的 TDR 寄存器
- 接收時: DMA 從 UART 的 RDR 寄存器得到數(shù)據(jù), 寫到 SRAM 去
- 指定的數(shù)據(jù)傳輸完畢后,觸發(fā) DMA 中斷;在數(shù)據(jù)傳輸過程中,沒有中斷, CPU 無需處理。
函數(shù)如下:
查詢方式 | 中斷方式 | DMA 方式 | |
---|---|---|---|
發(fā)送 | HAL_UART_Transmit | HAL_UART_Transmit_ITHAL_UART_TxCpltCallback | HAL_UART_Transmit_DMAHAL_UART_TxHalfCpltCallback HAL_UART_TxCpltCallback |
接 收 | HAL_UART_Receive | HAL_UART_Receive_ITHAL_UART_RxCpltCallback | HAL_UART_Receive_DMAHAL_UART_RxHalfCpltCallback HAL_UART_RxCpltCallback |
錯誤 | HAL_UART_ErrorCallback | HAL_UART_ErrorCallback |
3.3.3 查詢方式
本 節(jié) 程 序 源 碼 為 “ 3_ 程 序 源 碼 ?1_ 視 頻 配 套 的 源 碼 3-4_UART 編程 ( 查 詢 方 式)uart_poll.7z”。
缺點: 發(fā)送數(shù)據(jù)時要死等發(fā)送完畢,接收數(shù)據(jù)時容易丟失。
3.3.4 中斷方式
本 節(jié) 程 序 源 碼 為 “ 3_ 程 序 源 碼 ?1_ 視 頻 配 套 的 源 碼 3-5_UART 編程 ( 中 斷 方 式)uart_int.7z”。
缺點: 需要是事先調(diào)用接收函數(shù), 才能通過中斷接收數(shù)據(jù), 易丟失。
3.3.5 DMA 方式
本 節(jié) 程 序 源 碼 為 “ 3_ 程序源碼 ?1_ 視 頻 配 套 的 源 碼 3-6_UART 編程 (DMA 方 式)uart_dma.7z”。
本節(jié)講的是傳統(tǒng) DMA 方式,不涉及“idle 中斷”, 它會在后面講解。 缺點: 需要是事先調(diào)用接收函數(shù), 才能通過中斷接收數(shù)據(jù), 易丟失。
3.4 效率最高的 UART 編程方法
3.4.1 IDLE 中斷
IDLE,空閑的定義是:總線上在一個字節(jié)的時間內(nèi)沒有再接收到數(shù)據(jù)。
UART 的 IDLE 中斷何時發(fā)生? RxD 引腳一開始就是空閑的啊, 難道 IDLE 中斷一直產(chǎn)生? 不是的。當(dāng)我們使能 IDLE 中斷后,它并不會立刻產(chǎn)生,而是: 至少收到 1 個數(shù)據(jù)后, 發(fā)現(xiàn) 在一個字節(jié)的時間里,都沒有接收到新數(shù)據(jù),才會產(chǎn)生 IDLE 中斷。
我們使用 DMA 接收數(shù)據(jù)時,確實可以提高 CPU 的效率, 但是“無法預(yù)知要接收多少數(shù) 據(jù)”, 而我們想盡快處理接收到的數(shù)據(jù)。怎么辦? 比如我想讀取 100 字節(jié)的數(shù)據(jù), 但是接 收到 60 字節(jié)后對方就不再發(fā)送數(shù)據(jù)了, 怎么辦? 我們怎么判斷數(shù)據(jù)傳輸中止了? 可以使用 IDLE 中斷。在這種情況下,DMA 傳輸結(jié)束的條件有 3:
- 接收完指定數(shù)量的數(shù)據(jù)了, 比如收到了 100 字節(jié)的數(shù)據(jù)了,HAL_UART_RxCpltCallback 被調(diào)用
- 總線空閑了: HAL_UARTEx_RxEventCallback 被調(diào)用
- 發(fā)生了錯誤: HAL_UART_ErrorCallback 被調(diào)用
使用 IDLE 狀態(tài)來接收的函數(shù)有:
函數(shù) | 回調(diào)函數(shù) | |
---|---|---|
查詢方式 | HAL_UARTEx_ReceiveToIdle | 根據(jù)返回參數(shù) RxLen 判斷是否接收 完畢, 還是因為空閑而返回 |
中斷方式 | HAL_UARTEx_ReceiveToIdle_IT | 完畢: HAL_UART_RxCpltCallback 因為空閑而中止:HAL_UARTEx_RxEventCallback |
DMA 方式 | HAL_UARTEx_ReceiveToIdle_DMA | 傳輸一半:HAL_UART_RxHalfCpltCallback 完畢:HAL_UART_RxCpltCallback因為空閑而中止:HAL_UARTEx_RxEventCallback |
錯誤 | HAL_UART_ErrorCallback |
3.4.2 DMA 發(fā)送/DMA+IDLE 接收
要點有 3:
- 對于發(fā)送:使用“HAL_UART_Transmit_DMA”函數(shù)
- 對于接收:一開始就調(diào)用“HAL_UARTEx_ReceiveToIdle_DMA”啟動接收
- 在回調(diào)函數(shù)“HAL_UART_RxCpltCallback”或“HAL_UARTEx_RxEventCallback”里讀取、 存儲數(shù)據(jù)后,再次調(diào)用“HAL_UARTEx_ReceiveToIdle_DMA”啟動接收
3.5 在 RTOS 里使用 UART
3.5.1 程序框架
本程序的重點在于如何高效地接收數(shù)據(jù):
- 使用 DMA+IDLE 中斷的方式接收數(shù)據(jù),它會把數(shù)據(jù)存入臨時緩沖區(qū);
- 在回調(diào)函數(shù)里:把臨時緩沖器的數(shù)據(jù)寫入隊列,然后再次使能 DMA
- AP 讀取隊列: 如果隊列里沒有數(shù)據(jù)則阻塞。
框架如下:
3.5.2 編寫程序
本 節(jié) 程 序 源 碼 為 “ 3_ 程 序 源 碼 ?1_ 視 頻 配 套 的 源 碼 3-8_ 在 RTOS 里 使 用 UARTuart_rtos.7z”。
3.5.3 面向?qū)ο蠓庋b UART
我們使用多個 UART:UART2、UART4,以初始化為例,有如下函數(shù):
void UART2_Rx_Start(void);
void UART4_Rx_Start(void) ;
對于使用者而言,非常不友好:當(dāng) UART 數(shù)量增多,他需要記住、使用多個函數(shù)名;當(dāng) 更換某個 UART,他需要修改多處代碼。 比如對于如下代碼, 當(dāng)需要更換為 UART4 時, 需要 修改第 1、3 行代碼為 UART4 的函數(shù):
uart2_init(115200, 'N', 8, 1);
char *str = “www.100ask.net”;
uart2_sendp(str, strlen(str), 100);
把 UART 的操作封裝為結(jié)構(gòu)體, 可以解決這個問題。 UART 的操作主要有 3 個函數(shù): 初始 化、發(fā)送數(shù)據(jù)、接收數(shù)據(jù)。那么可以抽象出如下結(jié)構(gòu)體:
struct UART_Device {
char *name;
int (*Init)( struct UART_Device *pDev, int baud, char parity, int data_bit, int stop_bit);
int (*Send)( struct UART_Device *pDev, uint8_t *datas, uint32_t len, int timeout);
int (*RecvByte)( struct UART_Device *pDev, uint8_t *data, int timeout);
};
本節(jié)為 UART2、UART4 分別構(gòu)造一個“struct UART_Device”結(jié)構(gòu)體,比如:
strcut UART_Device g_uart2_dev = {“uart2”, uart2_init, uart2_send, uart2_recvbyte};
strcut UART_Device g_uart4_dev = {“uart4”, uart4_init, uart4_send, uart4_recvbyte};
使用時,示例代碼如下:
struct UART_Device *pDev = &g_uart2_dev;
pDev- >Init(pDev, 115200, 'N', 8, 1);
char *str = “www.100ask.net”;
pDev- >Send(pDev, str, strlen(str), 100);
如果要更換串口,只需要修改第 1 行代碼, 讓它指向 g_uart4_dev 即可:這就是面向 對象編程的優(yōu)點。
本節(jié)代碼為: 本節(jié)程序源碼為“3_程序源碼?1_視頻配套的源碼3-9_面向?qū)ο蠓庋b UARTuart_rtos_all_ok.7z ”
先使用 STM32CubeMX 配置 UART2、UART4,把它們的發(fā)送、接收都使用 DMA。如下圖配 置:
-
嵌入式
+關(guān)注
關(guān)注
5125文章
19438瀏覽量
313073 -
uart
+關(guān)注
關(guān)注
22文章
1251瀏覽量
102870 -
時鐘信號
+關(guān)注
關(guān)注
4文章
464瀏覽量
28980
發(fā)布評論請先 登錄
相關(guān)推薦
DSP嵌入式系統(tǒng)開發(fā)典型案例, 第3章 中低速數(shù)據(jù)采集系統(tǒng)設(shè)計

慕課嵌入式開發(fā)及應(yīng)用(第二章.UART驅(qū)動構(gòu)件的設(shè)計方法)

嵌入式Linux應(yīng)用程序開發(fā)詳解-第3章

【北京迅為】iTOP-RK3568開發(fā)板OpenHarmony系統(tǒng)南向驅(qū)動開發(fā)-第4章 UART基礎(chǔ)知識

評論