本文隸屬於AVR單片機教程系列。html
在第一期中,咱們已經開始使用UART來實現單片機開發板與計算機之間的通訊,但只是簡單地講了講一些概念和庫函數的使用。在這一篇教程中,咱們將從硬件與軟件等各方面更深刻地瞭解UART。數組
一直在講的UART實際上是USART組件的一部分,USART比UART多了同步的一部分,但這一部分用得太少(我歷來沒用過),並且缺少實例,因此就略過了。然而,單片機的設計者很機智地把這個雞肋功能昇華了一下,USART組件能夠支持SPI模式。SPI是一種同步串行總線,能夠支持很高的傳輸速率。這個功能使得ATmega324PA支持最多3個SPI通道,其中一個是純SPI,另兩個就是SPI模式下的USART。咱們將在下一講中揭開SPI的神祕面紗。數據結構
回到UART模式下的USART組件。開發板引出的RX
和TX
引腳是屬於USART0組件的,所以使用時如下n
都用0
代替。併發
UART共有5個寄存器:異步
UDRn
是收發數據寄存器,收(RXB
)和發(TXB
)使用不一樣的寄存器,但都經過UDRn
來訪問。向TXB
寫入一個字節,UART就開始發送;RXB
保存接收到的數據,帶有額外一個字節的緩衝(如同下一節要講的緩衝區)。async
UCSRnA
包含UART狀態位,如三個中斷對應的標誌,以及一些不經常使用的設置位。函數
UCSRnB
主要用於使能,包括收發器與三個中斷的使能位,以及9位幀格式相關的位。工具
UCSRnC
是最主要的控制寄存器,能夠配置USART的模式與格式。測試
UBRRnL
和UBRRnH
(能夠經過UBRRn
來訪問這個16位寄存器)用於設定波特率,在異步模式下,\(BAUD = \frac {f_{CPU}} {16(UBRRn + 1)}\)。ui
UART支持三個中斷,分別是接收完成(RX
)、數據寄存器空(UDRE
)、發送完成(TX
)。第一個用於接收,後兩個用於發送,通常使用UDRE
。
RX
中斷容許程序在任什麼時候刻及時地接收並處理總線上發來的數據。沿用「串口接收」一講中的例子:
#include <avr/io.h> #include <avr/interrupt.h> #include <ee1/led.h> int main(void) { led_init(); PORTD |= 1 << 0; // RXD0 pull-up UCSR0B = 1 << RXCIE0 // RX interrupt | 1 << RXEN0 // RX enabled | 1 << TXEN0; // TX enabled UCSR0C = 0b00 << UMSEL00 // asynchronous USART | 0b10 << UPM00 // even parity | 0 << USBS0 // 1 stop bit | 0b11 << UCSZ00; // 8-bit UBRR0L = 40; // 38400bps sei(); while (1) ; } ISR(USART0_RX_vect) { static const char led_char[4] = {'r', 'y', 'g', 'b'}; static uint8_t which = 4; uint8_t byte = UDR0; bool matched = false; for (uint8_t i = 0; i != 4; ++i) if (byte == led_char[i]) { matched = true; which = i; break; } if (!matched && (byte == '0' || byte == '1')) { matched = true; if (which < 4) led_set(which, byte - '0'); which = 4; } if (!matched) which = 4; }
TX
與UDRE
中斷容許程序在總線發送數據同時執行其餘代碼。好比,在打印ASCII表的同時控制LED閃爍。
#include <avr/io.h> #include <avr/interrupt.h> #include <ee1/led.h> #include <ee1/delay.h> int main(void) { led_init(); UCSR0B = 1 << UDRIE0 // UDRE interrupt | 1 << TXEN0; // TX only UCSR0C = 0b00 << UMSEL00 // asynchronous USART | 0b10 << UPM00 // even parity | 0 << USBS0 // 1 stop bit | 0b11 << UCSZ00; // 8-bit UBRR0L = 40; // 38400bps sei(); while (1) { led_on(); delay(500); led_off(); delay(500); } } ISR(USART0_UDRE_vect) { static char c = 0x21; UDR0 = c; if (++c == 0x7F) c = 0x21; }
你看,不用定時器,只需總線中斷與老套的main
結合便可。
值得一提的是UDRE
中斷的設計特別人性化——UDREn
的復位值是1
,程序能夠把全部數據都放在中斷中,控制部分只需開關中斷——而SPI和I²C組件都沒有這個特性。至於它到底帶來多少好處,只有在碼的過程當中體會了。
若是你較真一點,就會以爲上面這個程序很爛:
把硬件驅動(UART配置與中斷)與業務邏輯(要輸出的內容)牢牢地鏈接在一塊兒(專業點講,叫「緊耦合」),不符合可複用性等一系列設計原則;
ASCII表是十分有規律的,而大多數程序的輸出則否則,須要UDRE
中斷之外的代碼來決定要輸出什麼字符串,僅中斷並不能解放常規的輸出。
其實咱們還遇到過其餘問題:
相比25MHz的CPU頻率,UART的38400波特率是很慢的,傳輸一個字節的時間可讓CPU執行幾千條指令,但uart_print_string
等函數的策略都是等待UART把數據發送完成才返回,是阻塞的;
uart_scan_string
等函數要求程序乖乖地等待總線上的數據到來,不能錯過,這使程序不能在等待的同時作其餘事;
以上兩點相結合更讓人尷尬——在發送的同時接收到的數據會被錯過,怎麼還能叫全雙工總線呢?
這輸入和輸出兩方面的問題能夠用一種高度對稱的手段來解決,它就是緩衝區。緩衝區是這樣一種結構,它存放着一串字符,來自於程序的輸出或UART的接收,並能夠按順序取出,用於UART的發送或程序的輸入。顯然,這須要用到中斷:在RX
中斷中,向緩衝區中放入接收到的數據;在UDRE
中斷中,若是緩衝區中有數據,則取出併發送之。
因而,當程序須要輸入時,能夠從緩衝區中取一些字符,並解析成整數等類型,若是緩衝區爲空,則等待輸入,與C語言標準輸入scanf
很相似;當程序須要輸出時,能夠直接把字符串寫到緩衝區中,讓中斷來逐字節發送,而主程序能夠無需等待,直接繼續工做,這種輸出是異步的。這個「異步」與UART總線的「異步」是不一樣的概念。關於阻塞、異步等概念,可參考:怎樣理解阻塞非阻塞與同步異步的區別?
可是如今「緩衝區」還只是一個抽象概念,咱們要把它落實成代碼。如何實現一個緩衝區呢?
咱們先把緩衝區想象成一個管道,有頭和尾兩端,咱們須要從尾部放入球,從頭部取出。這種數據結構稱爲隊列。
隊列能夠用鏈表來實現,好處是隊列的長度沒有限制,除非內存耗盡。可是在咱們的應用場景中,鏈表節點中有效的數據是一個字節,卻還須要兩個字節來存放一個指針,不太划算。而且,malloc
函數是比較耗時的,應避免頻繁調用。
咱們使用一種叫做「循環隊列」的實現。循環隊列是一個數組,保存兩個下標,分別指向頭和尾(因爲我主要寫C++,我習慣用尾後)。循環體如今,假如隊列的大小是64,那麼下標爲63的元素的後一個就是下標爲0的元素。若是把普通數組想象成一個矩形,那麼循環隊列就是一個圓環。
初始時,頭和尾下標相同。向尾部放入一個字節,就是在尾下標處寫數據,並讓尾下標指向下一個元素;取出一個字節,就是讀取頭下標處的數據,並讓頭下標指向下一個元素。當兩個下標相等時,隊列爲空;當尾的後一個等於頭時,隊列滿——但是明明這時只放了63個元素,爲何再也不放一個呢?由於會與隊列空的狀況衝突,沒法分辨,爲了省事,仍是浪費一個字節吧。
下面這段代碼須要你認真閱讀並理解,可是請先忽略volatile
和ATOMIC_BLOCK(ATOMIC_FORCEON)
,當它們不存在就能夠了。你也能夠參考一些循環隊列相關的資料來更好地理解這種結構(原本我想寫的,但這篇已經很長了)。
#include <stdint.h> #include <stdbool.h> #include <avr/io.h> #include <avr/interrupt.h> #include <util/atomic.h> #define UART_TX_BUFFER_SIZE 64 #define UART_TX_BUFFER_MASK (UART_TX_BUFFER_SIZE - 1) volatile char uart_tx_buffer[UART_TX_BUFFER_SIZE]; volatile uint8_t uart_tx_head = 0; volatile uint8_t uart_tx_tail = 0; void uart_init_buffered() { UCSR0B = 0 << UDRIE0 // UDRE interrupt disabled | 1 << TXEN0; // TX only UCSR0C = 0b00 << UMSEL00 // asynchronous USART | 0b10 << UPM00 // even parity | 0 << USBS0 // 1 stop bit | 0b11 << UCSZ00; // 8-bit UBRR0L = 40; // 38400bps } void uart_print_char_buffered(char c) { bool full = true; while (1) { ATOMIC_BLOCK(ATOMIC_FORCEON) { if (((uart_tx_tail + 1) & UART_TX_BUFFER_MASK) // 0->1, ..., 63->0 != uart_tx_head) full = false; } if (!full) break; // if full, wait until buffer is not full } ATOMIC_BLOCK(ATOMIC_FORCEON) { if (uart_tx_head == uart_tx_tail) UCSR0B |= 1 << UDRIE0; uart_tx_buffer[uart_tx_tail] = c; uart_tx_tail = (uart_tx_tail + 1) & UART_TX_BUFFER_MASK; } } ISR(USART0_UDRE_vect) { UDR0 = uart_tx_buffer[uart_tx_head]; uart_tx_head = (uart_tx_head + 1) & UART_TX_BUFFER_MASK; if (uart_tx_head == uart_tx_tail) UCSR0B &= ~(1 << UDRIE0); }
看到這裏我默認你已經理解了循環數組,下面來看這些被忽略的語句。聲明爲volatile
的變量必定會被放在內存中而不是通用寄存器中;ATOMIC_BLOCK
的功能是,後面的大括號中的語句是原子的,在執行時不會被中斷;ATOMIC_FORCEON
會在執行完後把全局中斷打開。
相信你必定對這種代碼感到不適,爲何須要這麼麻煩呢?以if (uart_tx_head == uart_tx_tail)
這一句爲例,這句語句一般由主程序執行。
假設執行到這一句前時uart_tx_head
爲41
,uart_tx_tail
爲42
,即緩衝區中還有1
字節沒有發送。
程序讀取uart_tx_head
,其值爲41
。
在讀取uart_tx_tail
以前,USART0_UDRE_vect
中斷觸發了,在中斷中最後一個字節被髮送,uart_tx_head
被修改成42
,UDRIE0
被寫0
,關掉了這個中斷,隨後中斷退出。
程序讀取uart_tx_tail
,其值爲42
,二者不相等,UDRIE0
不會被寫1
,中斷保持關閉狀態。
緩衝區中被寫了一個字節,uart_tx_tail
變爲43
。緩衝區明明非空,UDRE
中斷卻沒有開,這個字節沒法發送。
這樣分析很累,我寫的時候並無認真分析不加原子操做可能帶來的問題,而是遵循這樣的原則:對於非中斷與中斷的代碼共享的數據,在非中斷代碼中必定要加原子,在中斷代碼中,若是在使用這些數據時全局中斷可能處於打開狀態,則也須要加原子。
如今咱們實現了串口輸出緩衝區,輸入緩衝區的原理相似,留做做業。咱們還須要關注幾個問題:
串口輸出是連續的字符流。「連續」是指不存在發送幾個字節,停頓一下,再繼續發送的狀況;「字符流」是指發送的數據都是字符。在字符流的假設下,若是須要能夠斷開的輸出,能夠經過用\0
標記斷點來實現。可是對於字節流,即數據自己就可能包含\0
的情形下,如何標記斷點呢?做業4在緩衝區的基礎之上增長了這樣的需求。
以上代碼對於在緩衝區滿時插入字符的策略是等待直到緩衝區有空位,雖然必定能等到,保證數據被髮送,但可能須要等待很長時間。好比,在緩衝區滿時發送一個較長的字符串,插入每一字節時都須要等待一個字節被髮送的時間,整體上與同步發送無異。這裏提供幾種方案:用一種結構來標記是否發生了錯誤,以及發生何種錯誤;給發送函數添加返回值,指示是否發送成功;使用動態緩衝區,當緩衝區滿時新開闢一塊空間存放。不過,仍是要根據應用選擇最合適的。
UART是稀缺資源,單片機一共有兩個,我設計的時候用掉一個,要是再加個串口調試,就用完了。可是,利用一個額外的GPIO和開發板左上角的邏輯門資源,咱們能夠把一個UART發送通道擴展成兩個。
這個組合電路有兩個輸入:單片機UART輸出(UART
,簡寫爲U
)和信號選擇(SEL
,簡寫爲S
);兩個輸出:當SEL
爲低電平時有效的通道A(OUTA
,簡寫爲A
)和當SEL
爲高電平時有效的通道B(OUTB
,簡寫爲B
,以上名字都是隨便起的)。這樣,儘管不能在兩個通道同時發送,但至少SEL
能夠控制每一個字節的流向。
回顧UART的幀格式,當信號線上沒有信號的時候,它是保持高電平的,所以對於A通道,當SEL
爲高時,OUTA
老是爲高;當SEL
爲低時,OUTA
電平與UART
相同,能夠獲得\(A = U + S\),+
號表示邏輯或。同理,\(B = U + \overline {S}\),上劃線表示邏輯非。另外,·
號表示邏輯與。
可是開發板上並無或門和非門,只有與非門(|
,C語言中|
表示什麼?)和或非門(↓
),咱們須要把這兩個式子變形一下:
\(A = U + S = \overline { \overline{U} \cdot \overline{S} } = (U \downarrow 0) | (S \downarrow 0)\)
\(B = U + \overline {S} = \overline { \overline{U} \cdot S } = (U \downarrow 0) | S\)
這樣咱們就能夠畫出原理圖:
左邊兩個是或非門,右邊是與非門,分別位於開發板左上角標註NOR
和NAND
處。每一個門有兩個輸入和一個輸出,對應A
、B
、O
三個引腳,A
和B
是能夠對調的。
而後就要根據原理圖搭建電路。也許你對這張並不複雜的電路圖毫無頭緒。的確,麪包板上看似簡單的電路也可能很複雜,不過仍是有規則能夠遵循的:
每一條杜邦線有兩端,是黑色膠殼加上一根針或者沒有針。有針的稱爲「公」,沒有的稱爲「母」(我真沒開車)。
板上的排針鏈接母頭,麪包板鏈接公頭。
杜邦線有3種:公對公、公對母、母對母。
麪包板上,一行5個孔是鏈接起來的。
各個引腳能夠劃分爲若干不相交集合,相同集合內的引腳有導線鏈接,不一樣集合內的引腳沒有引腳鏈接。每一個集合稱爲一個net。
對於只有一個引腳的net,無論它。
對於有兩個引腳的net,選用合適的杜邦線把兩個引腳直接鏈接。
對於有至少3個引腳的net,一般須要藉助麪包板,選用合適的杜邦線把每一個引腳與麪包板上同一行鏈接。
這張圖裏只有SEL
和第一個或非門的輸出這兩個net有3個引腳,所以麪包板上只會有6根線,像這樣:
最後簡單地測試一下,PIN_D
用做SEL
。
#include <ee1/pin.h> #include <ee1/uart.h> #include <ee1/delay.h> int main(void) { pin_mode(PIN_D, OUTPUT); uart_init(UART_TX); for (int16_t i = 0; ; ++i) { pin_write(PIN_D, i & 1); uart_print_int(i); uart_print_line(); delay(500); } }
把兩個通道鏈接到USB轉串口工具上,分別能夠看到奇數和偶數的輸出。
爲何一般使用UDRE而不是TX?何時不能使用UDRE而只能選擇TX?
使用中斷與緩衝區改寫「串口接收」一講中的例程。
如何使用74HC138來擴展UART輸出?
實現一個兩個發送通道共用的緩衝區(注意第1題)。