返回文章列表

TinyGo 實戰:建構智慧鎖矩陣鍵盤驅動程式

本文延續智慧安全鎖專案,深入探討如何使用 TinyGo 為 4x4 矩陣鍵盤編寫自訂驅動程式。內容涵蓋驅動程式的完整實作流程,從定義結構體、配置 GPIO 引腳,到實現核心的 GetIndices 與 GetKey 函數。文章特別強調按鍵去抖動與釋放檢測的邏輯,以確保輸入的穩定性與準確性。最終,透過主程式整合並利用序列埠進行測試,展示一個功能完整的鍵盤輸入模組。

物聯網開發 軟體工程

在嵌入式系統開發中,與硬體直接互動的驅動程式是串連軟體邏輯與物理世界的關鍵橋樑。本篇文章將從軟體工程的角度,詳細拆解為矩陣鍵盤建構驅動程式的設計思路與實踐。我們將不僅止於完成功能,更著重於程式碼的結構化與可重用性,將驅動邏輯封裝成獨立的 Go 套件。內容將深入探討矩陣掃描演算法、狀態管理(如按鍵釋放檢測),以及如何透過時間延遲實現簡易但有效的按鍵去抖動(Debouncing)機制。此過程不僅是對 TinyGo 在底層硬體控制能力的展現,也為開發者在面對缺乏現成驅動程式的硬體時,提供了一套系統性的解決方案與開發範本。

智慧安全鎖的建構:TinyGo、鍵盤與伺服馬達的整合(續)

玄貓深信,高科技養成不僅止於理論,更在於實踐與創新。在掌握了LED和按鈕的基礎控制後,本章將引導開發者進入更具挑戰性的領域:建構一個智慧安全鎖。這將結合4x4鍵盤的輸入、伺服馬達的精確控制,並深入探討序列埠通信在除錯中的關鍵作用。最終目標是實現一個能透過密碼解鎖的實用裝置。

監測鍵盤輸入:自訂驅動程式的實踐(續)

編寫鍵盤驅動程式(續)

獲取索引函數 (GetIndices)(續)

玄貓將繼續完善GetIndices函數,加入按鍵去抖動和釋放檢測的邏輯。

// keypad/driver.go
package keypad

import "machine"
import "time"

// Driver 結構體定義 (同上)
type Driver struct {
inputEnabled bool
lastColumn int
lastRow int
columns [4]machine.Pin
rows [4]machine.Pin
mapping [4][4]string
}

// Configure 函數定義 (同上)

// GetIndices 掃描鍵盤並返回被按下按鍵的行和列索引。
// 如果沒有按鍵被按下,則返回 (-1, -1)。
func (keypad *Driver) GetIndices() (int, int) {
// 1. 檢查上次按下的按鍵是否已釋放
// 遍歷所有行,將上次按下的行設定為低電平,檢查上次按下的列是否已恢復高電平
if keypad.lastRow != -1 && keypad.lastColumn != -1 {
rowPin := keypad.rows[keypad.lastRow]
columnPin := keypad.columns[keypad.lastColumn]

rowPin.Low() // 啟用上次按下的行
time.Sleep(time.Microsecond * 10) // 穩定電平

if columnPin.Get() { // 如果上次按下的列引腳恢復高電平,表示按鍵已釋放
keypad.inputEnabled = true // 重新允許輸入
keypad.lastColumn = -1
keypad.lastRow = -1
}
rowPin.High() // 恢復行引腳為高電平
}

// 2. 如果輸入被禁用,則不進行新的掃描,直接返回
if !keypad.inputEnabled {
return -1, -1
}

// 3. 遍歷所有行,進行掃描
for rowIndex := range keypad.rows {
rowPin := keypad.rows[rowIndex]

rowPin.Low() // 將當前行引腳設定為低電平
time.Sleep(time.Microsecond * 10) // 短暫延遲以確保電平穩定

for colIndex := range keypad.columns {
columnPin := keypad.columns[colIndex]

if !columnPin.Get() { // 如果列引腳為低電平,表示按鈕被按下
keypad.inputEnabled = false // 禁用新的輸入,直到按鍵釋放
keypad.lastColumn = colIndex
keypad.lastRow = rowIndex

rowPin.High() // 在返回之前,將當前行引腳恢復到高電平
return rowIndex, colIndex
}
}
rowPin.High() // 掃描完當前行後,將其恢復到高電平
}

// 如果沒有按鍵被按下
return -1, -1
}
  • 按鍵去抖動與釋放檢測
  • 在每次掃描之前,函數會檢查keypad.lastRowkeypad.lastColumn是否指向一個上次被按下的按鍵。
  • 如果存在上次按下的按鍵,它會啟用該行並檢查對應的列引腳是否已恢復高電平。如果恢復,表示按鍵已釋放,keypad.inputEnabled會被設為true,允許新的按鍵輸入。
  • 如果keypad.inputEnabledfalse(表示有按鍵被按下但尚未釋放),則GetIndices會直接返回(-1, -1),避免重複檢測同一個按鍵。
  • 新的按鍵檢測
  • keypad.inputEnabledtrue時,函數才會執行正常的矩陣掃描。
  • 一旦檢測到新的按鍵按下,keypad.inputEnabled會被設為false,並儲存當前按鍵的rowIndexcolIndex
獲取按鍵函數 (GetKey)

GetKey函數將利用GetIndices返回的行/列索引,從mapping中查找並返回對應的按鍵字元。

// keypad/driver.go
package keypad

import "machine"
import "time"

// Driver 結構體定義 (同上)
type Driver struct {
inputEnabled bool
lastColumn int
lastRow int
columns [4]machine.Pin
rows [4]machine.Pin
mapping [4][4]string
}

// Configure 函數定義 (同上)
// GetIndices 函數定義 (同上)

// GetKey 獲取被按下的按鍵字元。
// 如果沒有按鍵被按下,則返回空字串。
func (keypad *Driver) GetKey() string {
row, column := keypad.GetIndices() // 呼叫 GetIndices 獲取按鍵索引

if row == -1 && column == -1 {
return "" // 如果沒有按鍵被按下,返回空字串
}

// 根據索引從 mapping 中獲取對應的按鍵字元
return keypad.mapping[row][column]
}

GetKey函數提供了一個更高級的抽象,讓使用者無需關心底層的掃描邏輯,只需呼叫此函數即可獲取按下的按鍵。

主函數 (main)

現在,玄貓將在main.go中整合這個鍵盤驅動程式,並測試其功能。

// Chapter03/keypad-driver/main.go
package main

import (
"machine"
"time"
"Chapter03/keypad-driver/keypad" // 引入自訂的 keypad 套件
)

func main() {
// 1. 初始化 keypadDevice 實例
var keypadDevice keypad.Driver

// 2. 配置鍵盤引腳
// 根據電路連接圖,將 Arduino 的引腳傳遞給 Configure 函數
// 行引腳: D3, D4, D5, D6 (K_R0-K_R3)
// 列引腳: D7, D8, D9, D10 (K_C0-K_C3)
keypadDevice.Configure(
machine.D3, machine.D4, machine.D5, machine.D6, // 行引腳
machine.D7, machine.D8, machine.D9, machine.D10, // 列引腳
)

// 3. 無限循環,檢查按鍵並列印
for {
key := keypadDevice.GetKey() // 獲取按下的按鍵

if key != "" {
println("按鈕: ", key) // 如果有按鍵按下,列印到序列埠
}
time.Sleep(50 * time.Millisecond) // 短暫延遲,避免過度掃描和佔用CPU
}
}
  • 初始化驅動程式:創建keypad.Driver的一個實例keypadDevice
  • 配置引腳:呼叫keypadDevice.Configure,傳入Arduino與鍵盤連接的實際GPIO引腳。
  • 循環檢測:在無限循環中,不斷呼叫keypadDevice.GetKey()來獲取按鍵。如果返回的不是空字串,則表示有按鍵被按下,並將其列印到序列埠。time.Sleep用於控制掃描頻率。

燒錄與測試: 將程式燒錄到Arduino Uno:

tinygo flash --target=arduino Chapter03/keypad-driver/main.go

燒錄完成後,打開PuTTY並監控序列埠輸出。當按下鍵盤上的按鈕時,PuTTY終端將顯示對應的按鍵字元。

@startuml
!define DISABLE_LINK
!define PLANTUML_FORMAT svg
!theme _none_

skinparam dpi auto
skinparam shadowing false
skinparam linetype ortho
skinparam roundcorner 5
skinparam defaultFontName "Microsoft JhengHei UI"
skinparam defaultFontSize 16
skinparam minClassWidth 100

package "keypad-driver" {
component "main.go" as Main
package "keypad" {
component "driver.go" as Driver
class "Driver Struct" as DriverStruct {
+ inputEnabled: bool
+ lastColumn: int
+ lastRow: int
+ columns: [4]machine.Pin
+ rows: [4]machine.Pin
+ mapping: [4][4]string
}
DriverStruct -- "Configure()"
DriverStruct -- "GetIndices()"
DriverStruct -- "GetKey()"
}
}

Main --> Driver : 引入 keypad 套件
Main --> DriverStruct : 實例化 Driver
Main --> "Configure()" : 初始化引腳與映射
Main --> "GetKey()" : 循環獲取按鍵

"GetIndices()" --> DriverStruct : 讀取/更新狀態
"GetKey()" --> "GetIndices()" : 呼叫獲取索引

actor "使用者 (按下按鍵)" as User
User --> Driver : 物理按鍵輸入

Driver --> "序列埠 (PuTTY)" as SerialPort : 輸出按鍵訊息

@enduml

看圖說話:

此圖示展示了鍵盤驅動程式的軟體架構和執行流程。keypad-driver專案包含main.go主程式和keypad套件中的driver.godriver.go定義了Driver結構體及其方法,包括Configure(初始化鍵盤引腳和按鍵映射)、GetIndices(掃描鍵盤並返回按鍵的行/列索引,處理去抖動和釋放檢測)和GetKey(利用GetIndices返回的索引獲取實際按鍵字元)。main.go負責實例化Driver,呼叫Configure進行初始化,然後在一個無限循環中不斷呼叫GetKey來檢測按鍵輸入,並將結果輸出到序列埠。使用者透過物理按鍵輸入與驅動程式互動,最終在PuTTY等序列埠監控工具上看到按鍵訊息。

玄貓已經成功編寫了自己的鍵盤驅動程式,這是一個重要的里程碑!這項技能不僅限於鍵盤,未來在面對其他TinyGo尚未支援的硬體時,玄貓也能夠自行開發驅動程式。

尋找TinyGo驅動程式

TinyGo社群持續在擴展其支援的硬體驅動程式。當前,TinyGo支援多種設備,並且這個數量還在不斷增長。玄貓可以在TinyGo官方的驅動程式倉庫中找到現有的驅動程式。

貢獻驅動程式給TinyGo

TinyGo社群非常歡迎貢獻。如果玄貓開發了一個新的設備驅動程式並希望將其貢獻給TinyGo,可以遵循以下步驟:

  1. 開啟一個議題 (Issue):在TinyGo驅動程式的GitHub倉庫中開啟一個新的Issue,解釋玄貓想要添加什麼設備的驅動程式,以及計劃如何實現它。
  2. Fork倉庫:將TinyGo驅動程式的GitHub倉庫Fork到玄貓自己的GitHub帳戶。
  3. 創建新分支:基於dev分支創建一個新的分支來開發驅動程式。
  4. 創建拉取請求 (Pull Request):完成開發後,創建一個拉取請求,將玄貓的分支合併到TinyGo驅動程式倉庫的dev分支。

玄貓在與TinyGo社群互動的經驗非常積極,社群成員通常非常樂於助人且有禮貌。這是一個學習和貢獻的好機會。

解構這項從零建構驅動程式的實踐路徑可以發現,其核心價值遠不止於完成一個功能模組,而是從技術「使用者」躍升為「創造者」的關鍵轉折。相較於直接取用現成驅動程式所帶來的即時效率,親手處理底層的按鍵去抖動與釋放檢測,再將其封裝為如 GetKey 般的高階抽象,這種「垂直整合」的開發經驗,能淬鍊出難以替代的系統性思考與問題解決能力。它將孤立的編碼技巧,轉化為面對任何未知硬體挑戰時,都能從容應對的實戰策略。

隨著物聯網裝置日益多元且碎片化,這種回歸第一原理、自主解決整合缺口的能力,將成為區分資深專家與一般工程師的關鍵指標。我們預見,未來高價值的技術領導者,將是那些能夠在現有生態系資源不足時,主動為團隊「鋪路」的開創者。

玄貓認為,這條從理論驗證到實踐創新的修養路徑,是技術人員建立深度自信與核心競爭力的必經之途,其長期投資回報遠勝於對短期開發效率的片面追求。