在第一期中,我們已經(jīng)開始使用UART來實現(xiàn)單片機開發(fā)板與計算機之間的通信,但只是簡單地講了講一些概念和庫函數(shù)的使用。在這一篇教程中,我們將從硬件與軟件等各方面更深入地了解UART。
USART組件
一直在講的UART其實是USART組件的一部分,USART比UART多了同步的一部分,但這一部分用得太少(我從來沒用過),而且缺乏實例,所以就略過了。然而,單片機的設計者很機智地把這個雞肋功能升華了一下,USART組件可以支持SPI模式。SPI是一種同步串行總線,可以支持很高的傳輸速率。這個功能使得ATmega324PA支持最多3個SPI通道,其中一個是純SPI,另兩個就是SPI模式下的USART。我們將在下一講中揭開SPI的神秘面紗。
回到UART模式下的USART組件。開發(fā)板引出的RX和TX引腳是屬于USART0組件的,因此使用時以下n都用0代替。
UART共有5個寄存器:
UDRn是收發(fā)數(shù)據(jù)寄存器,收(RXB)和發(fā)(TXB)使用不同的寄存器,但都通過UDRn來訪問。向TXB寫入一個字節(jié),UART就開始發(fā)送;RXB保存接收到的數(shù)據(jù),帶有額外一個字節(jié)的緩沖(如同下一節(jié)要講的緩沖區(qū))。
UCSRnA包含UART狀態(tài)位,如三個中斷對應的標志,以及一些不常用的設置位。
UCSRnB主要用于使能,包括收發(fā)器與三個中斷的使能位,以及9位幀格式相關的位。
UCSRnC是最主要的控制寄存器,可以配置USART的模式與格式。
UBRRnL和UBRRnH(可以通過UBRRn來訪問這個16位寄存器)用于設定波特率,在異步模式下,BAUD=fCPU16(UBRRn+1)。
UART支持三個中斷,分別是接收完成(RX)、數(shù)據(jù)寄存器空(UDRE)、發(fā)送完成(TX)。第一個用于接收,后兩個用于發(fā)送,一般使用UDRE。
RX中斷允許程序在任何時刻及時地接收并處理總線上發(fā)來的數(shù)據(jù)。沿用串口接收一講中的例子:
#include #include #include 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中斷允許程序在總線發(fā)送數(shù)據(jù)同時執(zhí)行其他代碼。比如,在打印ASCII表的同時控制LED閃爍。 #include #include #include #include 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,程序可以把所有數(shù)據(jù)都放在中斷中,控制部分只需開關中斷——而SPI和I2C組件都沒有這個特性。至于它到底帶來多少好處,只有在碼的過程中體會了。 緩沖區(qū) 如果你較真一點,就會覺得上面這個程序很爛: 把硬件驅動(UART配置與中斷)與業(yè)務邏輯(要輸出的內容)緊緊地連接在一起(專業(yè)點講,叫“緊耦合”),不符合可復用性等一系列設計原則; ASCII表是十分有規(guī)律的,而大多數(shù)程序的輸出則不然,需要UDRE中斷以外的代碼來決定要輸出什么字符串,僅中斷并不能解放常規(guī)的輸出。 其實我們還遇到過其他問題: 相比25MHz的CPU頻率,UART的38400波特率是很慢的,傳輸一個字節(jié)的時間可以讓CPU執(zhí)行幾千條指令,但uart_print_string等函數(shù)的策略都是等待UART把數(shù)據(jù)發(fā)送完成才返回,是阻塞的; uart_scan_string等函數(shù)要求程序乖乖地等待總線上的數(shù)據(jù)到來,不能錯過,這使程序不能在等待的同時做其他事; 以上兩點相結合更讓人尷尬——在發(fā)送的同時接收到的數(shù)據(jù)會被錯過,怎么還能叫全雙工總線呢? 這輸入和輸出兩方面的問題可以用一種高度對稱的手段來解決,它就是緩沖區(qū)。緩沖區(qū)是這樣一種結構,它存放著一串字符,來自于程序的輸出或UART的接收,并可以按順序取出,用于UART的發(fā)送或程序的輸入。顯然,這需要用到中斷:在RX中斷中,向緩沖區(qū)中放入接收到的數(shù)據(jù);在UDRE中斷中,如果緩沖區(qū)中有數(shù)據(jù),則取出并發(fā)送之。 于是,當程序需要輸入時,可以從緩沖區(qū)中取一些字符,并解析成整數(shù)等類型,如果緩沖區(qū)為空,則等待輸入,與C語言標準輸入scanf很類似;當程序需要輸出時,可以直接把字符串寫到緩沖區(qū)中,讓中斷來逐字節(jié)發(fā)送,而主程序可以無需等待,直接繼續(xù)工作,這種輸出是異步的。這個“異步”與UART總線的“異步”是不同的概念。關于阻塞、異步等概念,可參考:怎樣理解阻塞非阻塞與同步異步的區(qū)別? 但是現(xiàn)在“緩沖區(qū)”還只是一個抽象概念,我們要把它落實成代碼。如何實現(xiàn)一個緩沖區(qū)呢? 我們先把緩沖區(qū)想象成一個管道,有頭和尾兩端,我們需要從尾部放入球,從頭部取出。這種數(shù)據(jù)結構稱為隊列。 隊列可以用鏈表來實現(xiàn),好處是隊列的長度沒有限制,除非內存耗盡。但是在我們的應用場景中,鏈表節(jié)點中有效的數(shù)據(jù)是一個字節(jié),卻還需要兩個字節(jié)來存放一個指針,不太劃算。并且,malloc函數(shù)是比較耗時的,應避免頻繁調用。 我們使用一種叫作“循環(huán)隊列”的實現(xiàn)。循環(huán)隊列是一個數(shù)組,保存兩個下標,分別指向頭和尾(由于我主要寫C++,我習慣用尾后)。循環(huán)體現(xiàn)在,假如隊列的大小是64,那么下標為63的元素的后一個就是下標為0的元素。如果把普通數(shù)組想象成一個矩形,那么循環(huán)隊列就是一個圓環(huán)。 初始時,頭和尾下標相同。向尾部放入一個字節(jié),就是在尾下標處寫數(shù)據(jù),并讓尾下標指向下一個元素;取出一個字節(jié),就是讀取頭下標處的數(shù)據(jù),并讓頭下標指向下一個元素。當兩個下標相等時,隊列為空;當尾的后一個等于頭時,隊列滿——可是明明這時只放了63個元素,為什么不再放一個呢?因為會與隊列空的情況沖突,無法分辨,為了省事,還是浪費一個字節(jié)吧。 下面這段代碼需要你認真閱讀并理解,但是請先忽略volatile和ATOMIC_BLOCK(ATOMIC_FORCEON),當它們不存在就可以了。你也可以參考一些循環(huán)隊列相關的資料來更好地理解這種結構(本來我想寫的,但這篇已經(jīng)很長了)。 #include #include #include #include #include #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); } 看到這里我默認你已經(jīng)理解了循環(huán)數(shù)組,下面來看這些被忽略的語句。聲明為volatile的變量一定會被放在內存中而不是通用寄存器中;ATOMIC_BLOCK的功能是,后面的大括號中的語句是原子的,在執(zhí)行時不會被中斷;ATOMIC_FORCEON會在執(zhí)行完后把全局中斷打開。 相信你一定對這種代碼感到不適,為什么需要這么麻煩呢?以if (uart_tx_head == uart_tx_tail)這一句為例,這句語句通常由主程序執(zhí)行。 假設執(zhí)行到這一句前時uart_tx_head為41,uart_tx_tail為42,即緩沖區(qū)中還有1字節(jié)沒有發(fā)送。 程序讀取uart_tx_head,其值為41。 在讀取uart_tx_tail之前,USART0_UDRE_vect中斷觸發(fā)了,在中斷中最后一個字節(jié)被發(fā)送,uart_tx_head被修改為42,UDRIE0被寫0,關掉了這個中斷,隨后中斷退出。 程序讀取uart_tx_tail,其值為42,兩者不相等,UDRIE0不會被寫1,中斷保持關閉狀態(tài)。 緩沖區(qū)中被寫了一個字節(jié),uart_tx_tail變?yōu)?3。緩沖區(qū)明明非空,UDRE中斷卻沒有開,這個字節(jié)無法發(fā)送。 這樣分析很累,我寫的時候并沒有認真分析不加原子操作可能帶來的問題,而是遵循這樣的原則:對于非中斷與中斷的代碼共享的數(shù)據(jù),在非中斷代碼中一定要加原子,在中斷代碼中,如果在使用這些數(shù)據(jù)時全局中斷可能處于打開狀態(tài),則也需要加原子。 現(xiàn)在我們實現(xiàn)了串口輸出緩沖區(qū),輸入緩沖區(qū)的原理類似,留作作業(yè)。我們還需要關注幾個問題: 串口輸出是連續(xù)的字符流。“連續(xù)”是指不存在發(fā)送幾個字節(jié),停頓一下,再繼續(xù)發(fā)送的情況;“字符流”是指發(fā)送的數(shù)據(jù)都是字符。在字符流的假設下,如果需要可以斷開的輸出,可以通過用
主站蜘蛛池模板:
综艺|
龙游县|
乐陵市|
石景山区|
黔江区|
寿光市|
丰镇市|
大悟县|
定日县|
土默特右旗|
东丰县|
崇左市|
龙游县|
岳阳县|
长葛市|
鄂托克旗|
乌海市|
巴彦淖尔市|
金秀|
芦山县|
房产|
兴化市|
五莲县|
观塘区|
石林|
正蓝旗|
宿州市|
奉化市|
逊克县|
喀喇沁旗|
拉萨市|
澄迈县|
云南省|
齐齐哈尔市|
嫩江县|
荥经县|
鄂托克旗|
泸定县|
长垣县|
噶尔县|
蕲春县|