在系列教程的最后一篇中,我將向你推薦3個可以深造的方向:C++、事件驅動、RTOS。掌握這些技術可以幫助你更快、更好地開發更大的項目。
本文涉及到許多概念性的內容,如果你有不同意見,歡迎討論。
關于高層
這一篇教程叫作“走向高層”。什么是高層?
我認為,如果寥寥幾行代碼就能實現一個復雜功能,或者一行代碼可以對應到幾百句匯編,那么你就站在高層。高層與底層是相對的概念,沒有絕對的界限。
站得高,看得遠,這同樣適用于編程,我們要走向高層。高層是對底層的封裝,是對現實的抽象,高層相比于底層更加貼近應用。站在高層,你可以看到很多底層看不到的東西,主要有編程工具和思路。合理利用工具,可以簡化代碼,降低工作量;用合適的思路編程,更可以事半功倍。
但是,掌握高層并不意味著忽視甚至鄙視底層,高層建立在底層基礎之上。其一,有些高層出現的詭異現象可以追溯到底層,這樣的debug任務只有通曉底層與高層的開發者才能勝任;其二,為了讓高層實現復雜功能的同時獲得可接受的運行效率,底層必須設計地更加精致,這就對底層提出了更高的要求。
相信你經過一期和二期的教程,已經相當熟悉AVR編程的底層了。跟我一起走上高層吧!
C++
C++繼承自C,兼有低級語言和高級語言的特性。C++代碼可以被編譯為匯編語句,這決定了它的高效;C++支持過程式、面向對象、泛型等編程范式,還有龐大的標準庫,這決定了它的高級。所有C++代碼都可以轉換成C代碼,使用C++絕非必要,但是不能僅憑這一點而否定C++——因為同理,C也是沒有必要的,你為什么還要學C呢?我們使用C++,就是要發揮它的高級。
在幾十年的發展過程中,C++委員會制定了多個標準,主要的有C++98和C++11,C++11添加了很多新特性,既能簡化程序又能提升性能。本文寫于下一個主要標準C++20即將發布之際,主流編譯器對C++11的支持已經很完整,所以我的建議是,如果想學C++,按照C++11標準來學。
在AVR平臺上寫C++比較特殊,在于工具鏈沒有提供C++標準庫。如果你想用標準庫,無論是IO設施還是容器算法,要么委曲求全,用網上能找到的不完整的實現,要么自己寫。這是AVR很勸退C++的一點,對此我的建議是,不要把AVR作為你學習C++的平臺,盡管這一節講的是C++給AVR單片機開發帶來的益處。用AVR來操練C++倒是有很多意想不到的好處。
說起AVR與C++,還不得不提起Arduino,這個平臺從AVR起家,一直使用C++語言,是AVR平臺上C++的最大甚至唯一的應用。我想,如果你能讀到這里,你對Arduino肯定不會陌生,方便易用是它的核心賣點之一。事實上,C++這門語言為它提供了不少幫助,而C++的威力還不止于此。我將以范式為線索,介紹C++的高級之處。
在面向對象的世界中,對象和消息是主角,語句是創建對象和規定消息傳遞方式的工具。定義對象需要用類,定義消息需要類中的函數,安排類之間的關系需要繼承和虛函數,這些是功能上的配角,編程邏輯上的主角。
代碼中的對象是現實中的對象的抽象,由于單片機往往是跟現實世界打交道,這一點比較容易理解。比如,開發板上的每一個外設,包括LED、按鍵等,都是對象。
#include #define F_CPU 25000000 #include class Led { public: Led(uint8_t index) { mask = 1 << (4 + index); DDRC |= mask; } void on() { PORTC |= mask; } void off() { PORTC &= ~mask; } private: uint8_t mask; }; Led red(0), yellow(1), green(2), blue(3); int main() { while (1) { red.on(); blue.off(); _delay_ms(500); red.off(); blue.on(); _delay_ms(500); } } 在這個程序中,我定義了類Led,它有3個函數:構造函數Led(uint8_t),設置mask并在硬件上初始化LED;on和off,分別開和關LED。然后創建了4個全局變量,分別代表4個LED。最后在main中使用,red.on()使紅燈亮,是不是很形象呢?除了形象,你也許還注意到,我沒有顯式調用含有DDRC的那個函數,實際上它在main之前創建全局變量的時候被調用,自動地初始化硬件。我打賭你之前一定忘記過初始化,而C++幫你解決了這個問題。對象的構造、復制、移動、銷毀,以及內存分配,都可以交給C++的語法和標準庫來處理。 上面這個程序實際上是基于對象范式的,它體現了抽象和封裝。面向對象則更進一步,體現了繼承和多態。繼承是類與類之間的關系,如果類D繼承自類B,那么D類型的對象就包含B類型對象所包含的一切內容,類B稱為類D的基類,類D稱為類B的派生類。多態是指相同的語句表現出不同的行為,換言之不同行為可以有統一的接口,可分為編譯期多態和運行時多態。運行時多態體現為虛函數:基類定義虛函數,派生類們實現虛函數,通過基類調用時,實際屬于不同派生類的對象表現出不同的行為。很難理解吧,我們通過實例來看: #include class Shape { public: virtual void draw() const = 0; }; class Line : public Shape { public: Line(uint8_t x1, uint8_t y1, uint8_t x2, uint8_t y2) : x1(x1), y1(y1), x2(x2), y2(y2) { } virtual void draw() const override { // Bresenham line... } private: uint8_t x1, y1, x2, y2; }; class Circle : public Shape { public: Circle(uint8_t x, uint8_t y, uint8_t r) : x(x), y(y), r(r) { } virtual void draw() const override { // Bresenham circle... } private: uint8_t x, y, r; }; int main(void) { Line line(0, 0, 127, 63); Circle circle(64, 32, 16); Shape* shapes[2]; shapes[0] = &line; shapes[1] = &circle; for (uint8_t i = 0; i != sizeof(shapes) / sizeof(*shapes); ++i) shapes[i]->draw(); while (1) ; } override是C++11引入的關鍵字,你需要在項目屬性->Toolchain->AVR/GNU C++ Compiler->Miscellaneous->Other flags中寫-std=c++11,以啟用C++11標準。 這個程序涉及3個類: 基類Shape定義了虛函數draw,派生類Line和Circle提供了不同的實現。main創建了Line和Circle類各一個對象,把指針放到Shape*類型的數組shapes中,然后通過指針調用draw函數,結果是Line::draw和Circle::draw分別被調用一次。要注意,只有通過指針或引用調用虛函數才能多態。引用用類型后的&表示,功能與指針類似,但不用寫取地址和解引用符號,形式上更加簡潔一點。 正如你所見,面向對象范式可以幫助你構建起現實中的對象之間的關系。在大型程序中,類與類、對象與對象、類與對象之間有復雜的聯系,形成了各種設計模式。 C++和C一樣可以自定義數據結構,但是不像C一樣用宏來定義ADT,也不用void*來抹去類型,而是引入了模板,可以把元素類型作為模板參數定義類和函數,一個模板類或模板函數可以實例化成很多個類或函數。比如我們在UART一講中寫過的隊列,在C++中可以這么寫: #include template class Queue { public: Queue() = default; Queue(const Queue&) = delete; Queue& operator=(const Queue&) = delete; Queue(Queue&&) = delete; Queue& operator=(Queue&&) = delete; void push(const T& element) { data[tail] = element; tail = increase(tail); } void pop() { head = increase(head); } bool empty() const { return head == tail; } bool full() const { return increase(tail) == head; } const T& peek() const { return data[head]; } private: T data[S]; size_t head = 0; size_t tail = 0; static size_t increase(size_t value) { if (++value == S) value = 0; return value; } }; 順便把UART那些函數改寫一下: #include #include #include #include #include #define F_CPU 25000000 #include class Uart { public: Uart() { 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 print(char c) { bool full = true; while (1) { ATOMIC_BLOCK(ATOMIC_FORCEON) { if (!queue.full()) full = false; } if (!full) break; // if full, wait until buffer is not full } ATOMIC_BLOCK(ATOMIC_FORCEON) { if (queue.empty()) UCSR0B |= 1 << UDRIE0; queue.push(c); } } void print(int i) { char str[10]; itoa(i, str, 10); print(str); } void print(const char* s) { for (; *s; ++s) print(*s); } void println() { print('n'); } template void println(const T& t) { print(t); println(); } void _interrupt() { UDR0 = queue.peek(); queue.pop(); if (queue.empty()) UCSR0B &= ~(1 << UDRIE0); } private: Queue } uart; ISR(USART0_UDRE_vect) { uart._interrupt(); } int main() { for (char c = ' '; c <= '~'; ++c) uart.print(c); uart.println(); for (int16_t i = 0; ; ++i) { uart.println(i); _delay_ms(500); } } 有什么好處呢?第一段代碼把原來為char編寫的64字節隊列緩沖區改寫成任意類型、任意長度的隊列類模板Queue,以類型T與大小S為模板參數。Uart類中創建了Queue 第二段代碼是把C代碼轉換成了基于對象風格的C++代碼,這個剛剛介紹過了,亮點在于函數print和println。print這一個函數名字對應參數為char、int和const char*的三個版本,這三個函數是重載函數。如果你給print函數傳一個參數,編譯器會幫你匹配到最合適的一個重載。在沒有模板的情況下,你可以把重載看作是一個人性化的功能,畢竟你是知道實際上哪個函數會被調用的,如果你給函數名字加個表示參數類型的后綴,你也可以不使用重載,只不過這件事現在可以交給編譯器了。 講到函數重載,不得不順便提一下運算符重載。私以為,運算符重載是C++最美麗的特性之一。在Java中你可以把兩個字符串用+號連接,C++也可以;C++還允許你賦予運算符以意義,即為原本語義上不成立的運算符編寫代碼,它們往往與形象或數學上的意義相契合,比如用operator<<輸出,再比如寫有理數Rational類然后重載四則運算和比較運算符,而Java中只能覆寫Object類的equals方法。況且直觀上,operator==是比equals更加原生的寫法——==是運算符,屬于語言核心,而equals是函數名字,屬于標準庫。總之,函數重載省去了你為相同功能的函數起不同名字的麻煩,運算符重載則更加直接,連函數名字都省了。 回到正題,在有模板的情況下,重載不再僅僅是人性化功能,而是與泛型緊密結合了。println是一個函數模板,盡管你知道它只能接受char、int和const char*類型的參數,但是寫成模板一可以省事,二允許添加額外的print重載而不改變println。如果給print加上后綴,你能在println中知道要調用哪個嗎?這也是一種多態,編譯期多態。 不過,如果你給println傳了一個錯誤的類型,編譯器給的錯誤信息會很難看,這是C++中模板錯誤的通病。如果不跳票的話,C++20引入的concept會解決這個問題。 Uart還可以再優化一下,讓它繼承自基類Print,其中定義了virtual print(char),其他函數照抄,Uart中只需覆寫這一個print(char)函數,其他print和println都可以刪去,而客戶依然可以使用它們。這是因為,雖然println定義在Print中,但println中調用的print(char)是虛函數,還是會回到Uart::print(char)上來。同樣地還可以有Oled類繼承自Print類,同樣覆寫自己的print(char)。如果一個函數debug需要打印,它應該接受Print&類型的參數,傳入Uart和Oled的實例可以分別實現在串口和OLED屏上打印。
上一篇:ATmega328P定時器詳解
下一篇:AVR單片機教程——示波器
推薦閱讀
史海拾趣
AZ Displays深知品質是企業生存的根本。因此,公司始終將質量控制放在首位,從原材料采購到生產工藝的每一個環節都進行嚴格把控。通過嚴格的質量管理體系和高效的供應鏈管理,AZ Displays的產品質量得到了客戶的高度認可,逐漸在行業內建立了卓越的聲譽。
隨著Eclipse的不斷發展壯大,ECLIPSE公司也面臨著越來越多的挑戰和風險。其中最大的挑戰之一是保持Eclipse的開放性和靈活性,同時確保其穩定性和安全性。為此,ECLIPSE公司采取了一系列措施,包括加強代碼審核、引入安全漏洞獎勵計劃等。此外,公司還積極應對來自競爭對手的挑戰和市場變化,不斷調整和優化自身的戰略和業務模式。
Chino-Excel公司深知人才是企業發展的核心競爭力。因此,公司注重人才培養和團隊建設,為員工提供廣闊的成長空間和良好的職業發展平臺。公司建立了完善的人才培養和激勵機制,吸引和留住了一批批優秀的技術人才和管理人才。這些人才為公司的發展注入了源源不斷的動力,推動了公司的持續創新和進步。
這些故事雖然是虛構的,但它們反映了電子行業中許多公司可能經歷的一些共同挑戰和機遇。這些故事強調了技術創新、全球化戰略、品牌建設、環保理念以及人才培養在電子行業公司發展中的重要性。請注意,這些故事僅為示例,并不代表任何實際公司的具體情況。
在1902年,Joseph C. Belden在美國芝加哥創立了Belden公司,標志著這家電線電纜公司的誕生。創立初期,公司便專注于電線電纜的研發和生產,憑借其卓越的產品質量和可靠的性能,逐漸在市場上嶄露頭角。隨著業務的發展,Belden逐漸擴大了生產規模,并增設了多個生產基地,以滿足不斷增長的市場需求。
隨著技術的不斷進步和市場需求的增長,芯朋微電子逐步將產品線拓展至標準電源和工業驅動領域。2013年,公司推出工控功率芯片產品,這些芯片廣泛應用于電機、基站、智能電表等行業領域,進一步擴大了公司的市場份額。
隨著電子技術的快速發展,線纜行業也在不斷革新。Cables To Go公司緊跟技術潮流,不斷引進新技術、新工藝和新材料,推動產品升級換代。公司還與多所高校和科研機構建立了合作關系,共同研發新型線纜產品和技術。這些技術革新不僅提升了產品的性能和品質,還為公司的可持續發展注入了新的動力。
請注意,以上故事都是基于假設和虛構的,不代表Cables To Go公司的實際發展情況。如需了解該公司的真實發展故事,建議查閱相關新聞報道或公司官方資料。
void checkRing_inti() { check0=2; count0=20; } uchar check_ring() { uchar flag; flag=ring; &nbs ...… 查看全部問答∨ |
|
這是一個穩壓電路,最終輸出1.7V(R7上端為+),R6為電源輸入端。 二極管LM285-1穩壓1.235V. 我理解輸出電壓通過R7,R8分壓傳至比較器,來控制V3,形成負反饋, 但不知道V3是工作在放大狀態,還是工作在開關狀態, 發射機正徧,集電極反偏,應該是 ...… 查看全部問答∨ |
|
S3C2440會自動加載Nand前4K內容,倘若Nand的第1塊被標記為壞塊,是不是不會自動跳轉加載第2塊,因為我找到S3C2440只有NCON0、GPG13、GPG14、GPG15可以告知Page大小,而沒有告知Block大小的引腳。 所以我猜測S3C2440啟動前不進行壞塊檢測?。?! 另 ...… 查看全部問答∨ |
昨天使用定時器做了一個PWM功能,現在用定時器做個計數功能,就是計數方波的個數。這個功能在叫外部事件計數器。就是對外部的方波進行計數。一下就是這個功能的一些說明: 計數器可以用16位或者8位定時器進行計數,實際 ...… 查看全部問答∨ |
我是通過XDS200燒寫器使用Uniflash3.4對F2812進行燒寫完全沒問題。但是換成CCS6就不行了,在進入debug模式的時候會跳出這樣一個對話框: 無視這個提示之后,照樣可以Connect接上,但是出現讀入程序失敗的提示。 … 查看全部問答∨ |
Meeting notice2016國際光電子與微電子技術及應用研討會 會議通知 Meeting notice2016國際光電子與微電子技術及應用研討會 International Conference on Optoelectronics and Microelectronics Technology and Application 時間:2016年10月10-12日地點:上海光大會展中心 O ...… 查看全部問答∨ |