過去在使用市面上巨集鍵盤與滑鼠時,必須依賴原廠提供的專用軟件才能更新設定,
經過逆向工程剖析後,在下發現這些裝置在本質上都是透過 序列通訊 (Serial Communication) 的方式,來覆寫內部的設定檔與巨集資料。
明白通訊機制後,在下便自行製作了一套自訂的更新工具,擺脫了對原廠軟件的依賴,
在下打算將這套序列更新方法,應用到具備原生 USB HID 功能的微控制器上,製作出與市場上相似的巨集鍵盤,而且是不受限於原廠軟體的自訂巨集裝置。
Hardcode 方法
#include <Keyboard.h>
#define PIN 2
bool previousStatus = false;
void setup() {
pinMode(PIN, INPUT_PULLUP);
Keyboard.begin();
}
void loop() {
bool currentStatus = !digitalRead(PIN);
if (!previousStatus && currentStatus) { // press
previousStatus = currentStatus;
Keyboard.press(0x8C); // press key 'a'
} else if (previousStatus && currentStatus) { // hold
} else if (previousStatus && !currentStatus) { // release
previousStatus = currentStatus;
Keyboard.release(0x8C); // release key 'a'
} else if (!previousStatus && !currentStatus) { // idle
}
}
一般在開發此類自訂輸入裝置時,為了快速開發,都會選擇將按鍵對應與指令直接 硬編碼 (Hardcode) 在韌體原始碼中。
這種做法雖然簡單,但需要調整按鍵配置時,都必須重新編譯程式並再次燒錄韌體。
對於不具備相關知識及技術的基礎使用者來說,通常都無法自行修改設定。
RAM 方法
如果能夠讓按鍵與巨集的設定資料無需重新燒錄韌體就能修改,便可以提升裝置的功能性及易用性,讓不熟悉編程的使用者也能輕鬆自訂。
最直接的方法就是透過 序列通訊 (Serial Communication) 與 宿主 (Host) 與 微控制器 之間的雙向溝通。
宿主只需使用 序列通訊工具 ,就能向 微控制器 發送設定資料,達到不燒錄韌體修改按鍵設定的目的。
#include <Keyboard.h>
#define PIN 2
byte key = 0x8C;
bool previousStatus = false;
void setup() {
Serial.begin(115200);
pinMode(PIN, INPUT_PULLUP);
Keyboard.begin();
}
void loop() {
if (Serial.available() > 0) {
// update key from byte-0 of serial data
key = Serial.read();
// clear buffer
while (Serial.available() > 0 && Serial.read() != 0x00) {
}
}
bool currentStatus = !digitalRead(PIN);
if (!previousStatus && currentStatus) { // press
previousStatus = currentStatus;
Keyboard.press(key);
} else if (previousStatus && currentStatus) { // hold
} else if (previousStatus && !currentStatus) { // release
previousStatus = currentStatus;
Keyboard.release(key);
} else if (!previousStatus && !currentStatus) { // idle
}
}
宣告 全域變數 (Global Variables) 來儲存按鍵設定。
在 loop 中,除了持續掃描實體按鈕的按鍵狀態外,同時監聽 序列埠 (Serial Port) ,
當接收到來自宿主的序列資料,便會立即更新全域變數中的按鍵設定,達到即時修改效果。
使用終端機直接發送位元組資料來驗證功能,輸入:
使用指令操作更新
printf "\x8C" >"/dev/ttyACM0"
這種方式能在不重新燒錄韌體的情況下,即時修改微控制器內部的按鍵設定。
使用圖像介面更新
雖然以終端機發送指令已能成功更新按鍵設定,但操作上仍要求使用者具備一定知識與序列通訊概念,對一般使用者並不方便。
在下嘗試製作一個具備 圖像介面 (GUI) 的工具,將 序列通訊與資料 封裝,讓完全沒有程式或硬體背景的使用者,也能透過圖像介面輕鬆修改按鍵設定。
// web-serial.js
class WebSerial {
#port;
constructor() {
}
async connect(baudrate) {
try {
this.#port = await navigator.serial.requestPort();
await this.#port.open({
"baudRate": baudrate
});
} catch (error) {
throw error;
}
}
async sendData(data) {
try {
let writer = this.#port.writable.getWriter();
await writer.write(new Uint8Array(data));
writer.releaseLock();
} catch (error) {
throw error;
}
}
async retrieveData() {
try {
let reader = this.#port.readable.getReader();
let data = await reader.read();
reader.releaseLock();
return [...new Uint8Array(data.value)];
} catch (error) {
throw error;
}
}
async disconnect() {
try {
this.#port.close();
} catch (error) {
throw error;
}
}
}
<!-- web-serial-test.html -->
<!DOCTYPE html>
<html lang="en">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8"/>
<title></title>
<script src="web-serial.js"></script>
<script>
async function updateSerial(data) {
let webSerial = new WebSerial();
await webSerial.connect(115200);
await webSerial.sendData([data]);
await webSerial.disconnect();
}
</script>
</head>
<body>
<select id="key">
<option value="140">Keyboard a</option>
<option value="141">Keyboard b</option>
<option value="142">Keyboard c</option>
</select>
<button onclick="updateSerial(document.getElementById('key').value);">Update</button>
</body>
</html>
在下使用先前自行開發的 WebSerial 類別,透過 HTML5 原生的 Web Serial API 與微控制器進行通訊。
EEPROM 方法
由於按鍵設定是儲存在微控制器 使用 隨機存取記憶體中 (Random Access Memory, RAM) ,
每次裝置重新開機後,設定資料就會被 初始化 , 導致無法保留關機前的設定狀態。
為了解決這個問題,在下使用 微控制器 內建的 電子抹除式可複寫唯讀記憶體 (Electrically-Erasable Programmable Read-Only Memory, EEPROM) 來長期儲存設定資料。
當網頁端透過 Web Serial API 傳送新的按鍵設定資料時,微控制器 會將接收到的資料寫入 EEPROM 中。
如此一來,即使裝置重新開機,也能從 EEPROM 中讀取關機前所儲存的設定資料,達到保存設定的效果。
#include <EEPROM.h>
#include <Keyboard.h>
#define PIN 2
bool previousStatus = false;
void setup() {
Serial.begin(115200);
pinMode(PIN, INPUT_PULLUP);
EEPROM.begin();
Keyboard.begin();
}
void loop() {
if (Serial.available() > 0) {
// update EEPROM-0 from byte-0 of serial data
EEPROM.update(0, Serial.read());
// clear buffer
while (Serial.available() > 0 && Serial.read() != 0x00) {
}
}
bool currentStatus = !digitalRead(PIN);
if (!previousStatus && currentStatus) { // press
previousStatus = currentStatus;
Keyboard.press(EEPROM.read(0));
} else if (previousStatus && currentStatus) { // hold
} else if (previousStatus && !currentStatus) { // release
previousStatus = currentStatus;
Keyboard.release(EEPROM.read(0));
} else if (!previousStatus && !currentStatus) { // idle
}
}
由於 EEPROM.write() 必定會執行寫入動作,頻繁使用會加速 EEPROM 的損耗,因此在下改用 EEPROM.update() 。
EEPROM.update() 的優點在於會先檢查目前儲存的資料與將會寫入的資料是否相同,只在資料不同時才會執行寫入,相同時則不進行任何操作。
這樣不僅能大幅減少不必要的寫入次數,有效延長 EEPROM 的壽命,同時在不需要寫入時也能節省時間。
雖然每次呼叫 EEPROM.update() 會多花一點時間檢查,但在大多數應用環境中,長期來看都是非常值得的做法。
RAM 及 EEPROM 方法
雖然 EEPROM 能夠保存資料,但其讀寫速度遠比 RAM 慢。
如果程式需要頻繁讀取資料,持續存取 EEPROM 會明顯影響整體運作效能;
反之,RAM 雖然讀寫速度極快,但斷電後資料就會全部消失,無法保留資料。
為了同時保存資料與執行效能,在下決定將 RAM 與 EEPROM 兩者價點結合使用。
- 正常運作時,所有按鍵設定一律從 RAM 中的全域變數讀取,確保反應快速。
- 當設定資料有更新時,同時將資料寫入 RAM 與 EEPROM。
- 裝置每次開機初始化時,則先從 EEPROM 將最後儲存的設定資料複製到 RAM 中。
#include <EEPROM.h>
#include <Keyboard.h>
#define PIN 2
byte key;
bool previousStatus = false;
void setup() {
pinMode(PIN, INPUT_PULLUP);
EEPROM.begin();
Keyboard.begin();
key = EEPROM.read(0);
}
void loop() {
if (Serial.available() > 0) {
// update key from byte-0 of serial data
EEPROM.update(0, data = Serial.read());
// clear buffer
while (Serial.available() > 0 && Serial.read() != 0x00) {
}
}
bool currentStatus = !digitalRead(PIN);
if (!previousStatus && currentStatus) { // press
previousStatus = currentStatus;
Keyboard.press(key);
} else if (previousStatus && currentStatus) { // hold
} else if (previousStatus && !currentStatus) { // release
previousStatus = currentStatus;
Keyboard.release(key);
} else if (!previousStatus && !currentStatus) { // idle
}
}
這種做法既能讓設定資料在斷電後不會遺失,又能維持高速的讀取效能。
雖然每次更新設定時需要多寫入一次 EEPROM,但搭配 EEPROM.update() 只在資料實際變化時才執行寫入,
能有效減少不必要的寫入次數,在壽命與效能之間取得較好的平衡。
補充資料
Firefox 目前對 Web Serial API 的支援仍不夠完善。
雖然在 Firefox 中呼叫 Web Serial API 時,會正常彈出選擇序列裝置的視窗,並可讓使用者選擇目標裝置後點擊 允許,
但實際上經常出現連接失敗或無法正常傳輸資料的情況,穩定性較差。
因此在下建議使用基於 Chromium 的 網頁瀏覽器 來發送序列資料,會有較好的相容性與穩定表現。
EEPROM 的資料寫入次數是 有限制 ,一般來說平均大約只有 10萬次 。
雖然在正常合理的使用情境下,很難達到 10萬次 的寫入,但在下仍然建議要盡量節省寫入次數,以延長 EEPROM 的使用壽命。
值得注意的是,EEPROM 的寫入次數限制是每個 地址 (Address) 獨立計算的。
因此即使某一個地址的寫入次數達到上限而損壞,也不會影響其他地址的正常讀寫。
當某個地址無法再寫入時,通常會出現以下兩種情況:
- 永久維持最後一次資料。
- 內部電晶體損壞,所有位元變成高電壓狀態,數值永久維持 255(0b11111111)。
總結
這個專案的主要目標是模仿市面的巨集裝置的設定方式,其核心在於如何讓微控制器能夠接收並即時更新設定資料。
目前在下僅先處理最基本的序列通訊,只接收並處理 第0位元組 的資料,因此暫時不需要複雜的資料結構與解析。
不過,若未來想要擴展更多進階功能,例如:
- 輸入整串文字
- 同時觸發多個按鍵
- 串聯多個操作
就必須定義清晰且完整的資料結構,讓微控制器能根據接收到的結構化資料,精準觸發預期的效果。


沒有留言 :
張貼留言