直接連接埠操作實例
隨著程式設計越來越為走向大眾化,一些很基本卻很有價值的程式碼可能被擱置一旁,或完全被人們遺忘,例如直接連接埠操作 (DPM)。有些程式員認為,這類命令很難讀懂,可能會讓新手一頭霧水,因此絕對不可與更多人熟悉的程式結構混用。但為什麼不使用所有可用的工具來保持程式碼簡潔高效呢?
例如,每一個 8 位元微控制器的各個連接埠都需要三個 8 位元暫存器來控制連接埠活動。這些暫存器分別是用於輸入/輸出方向的 DDRx;用於引腳邏輯位準控制的 PORTx;以及可保存連接埠目前引腳狀態的 PINx。為了詳細說明,讓我們來看看連接埠 C。在設定期間,程式員可能要將連接埠 C 的引腳設定為輸出,且初始狀態為低位準 (LOW);通常撰寫一個簡潔的「迴路用」程式,會需要 3 或 4 行重複程式碼,從引腳零到引腳七,總共執行 32 行程式碼。若在迴路中使用程式庫函數 (如 pinMode() 與 digitalWrite()),則會引發更多「幕後」活動,導致大量執行額外的程式碼。
最終,連接埠 C 的八個方向暫存器位元設為高位準 (HIGH),連接埠 C (PORTC) 的八個邏輯位準暫存器位元設為低位準 (LOW)。使用以下直接連接埠暫存器命令,將位元設定為 HIGH 或 LOW,也可完成相同的操作:
DDRC = 0xFF; //Set port C pins as OUTPUTS (in binary DDRD = 0b11111111;)
PORTC = 0x00; //Set port C pins LOW (in binary PORTC = 0b00000000;)
如何將連接埠 A 設成輸入呢?
DDRA = 0x00; //Set port A pins as INPUTS (in binary DDRA = 0b00000000;)
最後,將連接埠 D 設定成混合式輸入與輸出:
DDRD = 0x0F; //Set the port D upper four bits as INPUTS and the lower as OUTPUTS (in binary DDRD = 0b00001111);
使用十六進位值 (如 0xFF) 可增加一些有意思的見解,有助於瞭解暫存器位元數值的分配。「0x」是十六進位指示符,第一個數字「F」代表 8 位元暫存器的高四位,而第二個數字「F」則代表低四位。
將位元設為 HIGH 或 LOW 的所有可能組合,都可由十六進位或二進位表示法 (數字) 來表示。使用二進位表示法會更一目了然,因為在撰寫程式碼時可以看到所有八個位元。如果編譯器有支援,就可接受任一種表示法;不過,十六進位法較簡短,看起來也更酷。
請注意:在 C/C++ 語言中,使用二進位表示法並非通用標準。
讓我們更進一步
在設置中配置連接埠 A 和 C 後,只需在主迴路中額外添加幾行利用 DPM 的程式碼,即可讀取數位裝置並操作其他數位裝置。實際上,兩個由數位搖桿控制其行程極限開關的步進馬達,只需用最少量的暫存器命令和查找表,即可輕鬆完成編程工作。此場景的硬體設置如圖 1 所示。
圖 1:搖桿/步進馬達硬體設置,其中的微控制器連接埠 A 和 C 已醒目提示。(圖片來源:DigiKey)
硬體:
一個數位搖桿將其四個常開型 (NO) 觸點連接到 VCC,同時連接到連接埠 A 的引腳 0 到引腳 3。微控制器的輸出會下拉為低位準。當觸點關閉時,連接埠引腳會變為高位準。這些觸點代表「上」(UP)、「下」(DOWN)、「左」(LEFT)、「右」(RIGHT) 控制項目,且已配置為允許兩個鄰近觸點同時啟動,因此能產生八種可能的開關輸出組合。第九個輸出代表「全部停止」(ALL STOP),此狀況會在搖桿置中且所有觸點都在開啟狀態下發生。
四個極限開關將其 NO 觸點接地,同時連接到連接埠 A 的剩餘引腳,這對應搖桿的 UP、DOWN、LEFT、RIGHT 配置。微控制器的開關輸出會拉至高位準;當觸點關閉時,引腳會變成低位準。
步進馬達驅動器板件會切換三個控制引腳的高低位準,以利進行步進、方向及保持功能,進而產生機械性動作。在此例中,連接埠 C 的所有八個位元,皆專門用來操作兩個驅動器,即便有兩個引腳未使用。
編程:
為了產生正確的驅動器輸出,可使用查找表將四個搖桿位元轉換成八個驅動器位元。主迴路僅需一行程式碼即可調用「get_output()」函數,並將 PINA 暫存器的內容傳遞給此函數。從函數回傳的數值會直接寫入 PORTC 暫存器:
PORTC = get_output(PINA);
此函數會存取查找表「lookup_output[ ]」,然後回傳索引值。不過,此函數還會執行其他涉及極限開關的操作。「lookup_output[ ]」方括弧中涵蓋的查找表索引值,會由移位和遮蔽表達式表示。產生的索引變數,是輸入暫存器高 4 位 (極限開關值) 和低四位 (搖桿值),進行按位 AND 運算後的結果。如果任何極限開關觸點處於關閉狀態,且高四位的任何一個位元具有零值,對應的低位元就會被清除。
請注意:由於四個位元可表示 16 種唯一的位元組合,因此 7 個未使用的查找表索引位置會轉換成 0x00,以避免發生錯誤。
複製uint8_t get_output(uint8_t porta_val) { return lookup_output[(porta_val >> 4) & (porta_val & 0x0F)]; }
範例:
當搖桿在 UP 與 RIGHT 位置,且所有極限開關均為開啟時,會將二進位 PINA 暫存器值 0b11111001 (或十六進位的 0xF9) 傳至此函數。此函數會使用 0b00001111 (0x0F) 進行按位 AND 運算,將高四位歸零,產生 0b00001001 (0x09) 作為查找表索引值。使用另一個按位 AND 運算,將此結果與偏移的極限開關狀態值 0b00001111 (0x0F) 進行比較,會使先前值保持不變,並回傳 0b00001001 (0x09) 作為最後的索引值,此值會指向查找表中的 0b00100001 (0x21)。這就是步進馬達驅動器對 UP 與 RIGHT 位置的程式轉換。請參閱圖 2。
圖 2:當搖桿在 UP 與 RIGHT 位置時,微控制器對連接埠 A 與連接埠 C 輸入的解讀。(圖片來源:DigiKey)
如果到達 UP 極限開關,就會產生零位元,則移位值會是 0b00000111 (0x07) 而不是 0b00001111 (0x0F),這會否決對應的 UP 搖桿值,導致最後索引值成為 0b00000001 (0x01),而不是 0b00001001 (0x09)。0b00000001 (0x01) 的轉換值為查找表中的 0x26。這步進馬達驅動器對單純 RIGHT 位置的程式轉換。請參閱圖 3。
圖 3:當 UP 極限開關跳脫,且搖桿在 UP 與 RIGHT 位置時,微控制器對連接埠 A 與連接埠 C 輸入的解讀。(圖片來源:DigiKey)
結論
無論程式員是否採用 DPM 作為設定、讀取或寫入連接埠資料的可行工具,大幅減少編程量都是很好的動力。若使用標準函式庫函數進行相同的操作,不僅需要更多的程式碼,而且可能會佔用大量可用的程式記憶體。以下提供的程式碼在基準功能測試中僅使用 ATMEGA328P 微控制器不到 1% 的記憶體。針對程式碼提供適當的註解是瞭解程式碼當中 DPM 功能的關鍵,且有助於各種程度的程式員更輕鬆進行除錯。
DigiKey 硬體範例:
步進馬達 - https://www.digikey.com/short/pdnfp4
步進控制器 - https://www.digikey.com/short/pdnf4r
搖桿 - https://www.digikey.com/short/pdnf57
極限開關 - https://www.digikey.com/short/pdnfwm
範例程式碼:
複製const uint8_t lookup_output[16] = { 0x09, //Index 0 All Stop.Apply hold current 0x26, // Index 1 Right 0x34, // Index 2 Left 0x00, // Index 3 Unused 0x36, // Index 4 Down 0x0C, // Index 5 Down/Right 0x31, // Index 6 Down/Left 0x00, // Index 7 Unused 0x24, // Index 8 Up 0x21, // Index 9 Up/Right 0x0E, // Index 10 Up/Left 0x00, // Index 11 Unused 0x00, // Index 12 Unused 0x00, // Index 13 Unused 0x00, // Index 14 Unused 0x00 // Index 15 Unused }; void setup() { // Set all bits in port A direction register as INPUTs; // Limits (up, down, left, right) Joystick (up, down, left, right) DDRA = 0x00; // Set all bits of port C direction register as OUTPUTs; // Motor control (Not Used, Mot_1, Dir_1, En_1, Not Used, Mot_2, Dir_2, En_2 DDRC = 0xFF; } void loop() { //Send the port A values to the function.Write the return value to port C. PORTC = get_output(PINA); } /***** Input Value Translation Function *******/ uint8_t get_output(uint8_t porta_val) { // Compare the limit switch and joystick values.Retrieve and return the translated value.return lookup_output[(porta_val >> 4) & (porta_val & 0x0F)]; }

Have questions or comments? Continue the conversation on TechForum, Digi-Key's online community and technical resource.
Visit TechForum