娇小w搡bbbb搡bbb,《第一次の人妻》,中国成熟妇女毛茸茸,边啃奶头边躁狠狠躁视频免费观看

歷史上的今天

今天是:2024年09月08日(星期日)

2020年09月08日 | AVR單片機教程——UART進階

發(fā)布者:等放假的Lwj 來源: eefocus關鍵字:AVR  單片機  UART進階 手機看文章 掃描二維碼
隨時隨地手機看文章

在第一期中,我們已經(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ù)都是字符。在字符流的假設下,如果需要可以斷開的輸出,可以通過用

主站蜘蛛池模板: 综艺| 龙游县| 乐陵市| 石景山区| 黔江区| 寿光市| 丰镇市| 大悟县| 定日县| 土默特右旗| 东丰县| 崇左市| 龙游县| 岳阳县| 长葛市| 鄂托克旗| 乌海市| 巴彦淖尔市| 金秀| 芦山县| 房产| 兴化市| 五莲县| 观塘区| 石林| 正蓝旗| 宿州市| 奉化市| 逊克县| 喀喇沁旗| 拉萨市| 澄迈县| 云南省| 齐齐哈尔市| 嫩江县| 荥经县| 鄂托克旗| 泸定县| 长垣县| 噶尔县| 蕲春县|