目錄
FPGA模擬PS/2鍵盤
- ———— VerilogHDL + SpinalHDL
衆所周知,PS/2是一種很常見的鍵盤鼠標接口,很多開發板上都有,不論是單片機還是FPGA,基本例程都少不了PS/2控制器。但是,絕大部分代碼都是作爲PS/2主機來讀取鍵盤鼠標發送的數據,很少有作爲鍵盤鼠標設備來跟電腦通信的。Arduino倒是有幾個PS/2鍵盤的庫,只是難以移植,而用FPGA實現PS/2設備的方案,一個字都搜不到!因此作本文以記錄筆者實現FPGA模擬PS/2鍵盤的全過程,希望能對大家有所幫助。
1. PS/2協議簡介
1.1 首先,重要的話說三遍
-
PS/2接口不支持熱插拔!!!
-
PS/2接口不支持熱插拔!!!
-
PS/2接口不支持熱插拔!!!
1.2 參考資料
《PS2技術參考》(Adam Chapweske著)是教科書級的參考資料,一定要潛心拜讀。文章較長,一次性讀完肯定記不住,最好先大致瀏覽,待遇到問題時反覆查閱。
1.3 “協議棧”
爲便於理解,在此冒用現代網絡技術的概念。
1.3.1 物理層
通常電腦上是母的,鍵盤和鼠標是公的,鍵盤是紫色,鼠標是綠色。時鐘線和數據線都是5V,兼容3.3V CMOS。時鐘信號總是由設備產生,時鐘週期一般取40us。
1.3.2 數據鏈路層
與常用的串口類似,不管是上行還是下行,每幀都包含以下內容:
1個起始位 | 總爲0 |
8個數據位(payload) | 低位(LSB)在前 |
1個校驗位 | 奇校驗 |
1個停止位 | 總爲1 |
此外,在主機到設備的傳輸中,最後還有一個應答位。
記住這些,很重要。
1.3.3 傳輸層
這一層將低層與高層解耦合,將發送與接收的細節封裝起來,爲應用層打基礎。
需要注意的問題有幀之間的延時、發送與接收間的干擾、與高層的總線時序等。
1.3.4 會話層
鍵盤與鼠標在本層分道揚鑣,本文僅討論鍵盤,不涉及鼠標。
要讓電腦(host)識別到你寫的鍵盤(device)是個艱鉅的任務。電腦會在開機時發送一連串指令,每條指令都要得到正確的響應,稍有不慎就會被當做無效設備而抑制通信,嚴重時甚至使電腦無法正常啓動!
因爲會話是在開機時建立的,所以開機後再插入鍵盤是無效的,正所謂 “不支持熱插拔”。
1.3.5 應用層
成功被主機識別後,就可以愉快地發送掃描碼了。🚀當鍵盤檢測到有鍵按下時發送通碼,有鍵擡起時發送斷碼。斷碼即在通碼前加了一字節“F0h”。並不是每個鍵的通碼都只有一字節,有的鍵Prt Sc甚至沒有斷碼。
2. Arduino開源庫移植測試
先用單片機把協議學明白了才能用FPGA實現
Arduino有很多模擬PS/2鍵盤的庫,隨便找一個能用的移植到正點原子的mini板上(因爲筆者手頭上只有這塊單片機開發板帶PS/2接口),點擊下載完整工程。
2.1 GPIO配置——物理層
首先一定要看清楚這個引腳耐壓有沒有5V!⚡️因爲主機上帶5V上拉,所以開發板有沒有上拉電阻都無所謂,配置爲開漏輸出即可——STM32F1系列的GPIO配置成開漏輸出時也是可以直接讀IDR的。
2.2 觀察總線波形——數據鏈路層
串行總線的本質是波形圖
判斷鍵盤有沒有被成功識別最簡單的辦法就是發送一個鍵看電腦有沒有反應,但這個過程很難用FPGA實現(尤其是還有bug的時候),所以調試時一般用示波器觀察,正好雙通道。用公對公PS/2線連接開發板與電腦,時鐘線與數據線對應的IO已由排針引出,反覆開關機觀察示波器有以下發現:
- 總線空閒時均爲高電平,但時鐘線會週期性地被主機拉低
- 若未正確應答主機的指令,時鐘線會被永久性拉低
- 通常主機接收完一幀的瞬間會立即拉低時鐘來抑制通信
- 主機可能不釋放時鐘就開始發送
- 主機發送時,數據在時鐘的下降沿轉變
- BIOS初始化時有密集通信,Windows初始化時只發送LED
2.3 記錄初始化過程——會話層
用串口輸出PS/2總線上傳輸的每一字節:
initing...
sending aa
sent
init ok!!!
received 0xf5
sending fa
sent
received 0xff
sending fa
sent
sending aa
sent
received 0xed
sending fa
sent
received 0x2
sending fa
sent
received 0xf5
sending fa
sent
received 0xf4
sending fa
sent
received 0xf5
sending fa
sent
received 0xf4
sending fa
sent
received 0xed
sending fa
sent
received 0x2
sending fa
sent
received 0xf5
sending fa
sent
received 0xf4
sending fa
sent
received 0xf5
sending fa
sent
received 0xf4
sending fa
sent
received 0xed
sending fa
sent
received 0x2
sending fa
sent
received 0xff
sending fa
sent
sending aa
sent
received 0xf3
sending fa
sent
received 0x0
sending fa
sent
received 0xed
sending fa
sent
received 0x0
sending fa
sent
received 0xed
sending fa
sent
received 0x0
sending fa
sent
received 0xf3
sending fa
sent
received 0x8
sending fa
sent
received 0xf3
sending fa
sent
received 0x20
sending fa
sent
received 0xed
sending fa
sent
received 0x2
sending fa
sent
received 0xf3
sending fa
sent
received 0x20
sending fa
sent
received 0xed
sending fa
sent
received 0x0
sending fa
sent
received 0xed
sending fa
sent
received 0x2
sending fa
sent
3. 時序邏輯基礎——Verilog描述數據鏈路層
看波形寫電路是做數字邏輯設計最基本的能力之一。PS/2的時序比較特殊的地方在於,它的數據線不能與時鐘線同時變化,因此整個模塊的時鐘域是總線時鐘頻率的4倍。此外,PS2_CLK
與PS2_DAT
是連接到片外的三態線,發送電路與接收電路分別並聯於其上。
3.1 發送電路
發送電路用來實現設備到主機的通訊過程。
3.1.1 時序圖
發送時序如此簡單,卻並不容易實現,因爲規定從時鐘脈衝的上升沿到一個數據轉變的時間至少要有5微秒,數據變化到時鐘脈衝的下降沿的時間至少要有5微秒並且不大於25微秒 。時鐘頻率可取10 ~ 16.7 kHz,亦即每個時鐘脈衝的寬度在30 ~ 50微秒。
具體細節詳見《參考》,此處不再贅述。
筆者採用一個4進制計數器來產生1 bit信號計數到00和10時翻轉時鐘線,計數到11時改變數據線,以此保證兩根線不會同時改變。時鐘信號的脈寬取40微秒,因此計數器的時鐘就要有10微秒的脈寬,稱之爲clock_quarter
。
3.1.2 原理圖
除去三態總線,整個模塊包括4個輸入信號與3個輸出信號。
clock_quarter
作爲整個電路的時鐘,quarter代表其四分頻後爲總線時鐘頻率。start
的上升沿開始發送ready
代表空閒abort
爲高代表主機抑制(inhibit)了傳輸finish
在發送完成後產生一節拍脈衝
3.1.3 驗證
完整代碼已開源。用示波器觀察輸出波形:
顯然,黃色是數據線,藍色是時鐘線,兩根線的邊沿是正好交錯開的,滿足協議的要求。檢查脈寬正常就大功告成了。
3.2 接收電路
跟發送電路有異曲同工之妙,主要區別在payload的移位寄存器。
3.2.1 時序圖
注意,host是不產生時鐘信號的,但具有優先控制權,這正是PS/2總線的巧妙之處。PS/2總線與I2C總線都是一根時鐘一根數據、開漏、分主從;但I2C可以一主對多從而PS/2只能一對一;PS/2的從機可以隨時發送數據而I2C不行。
3.2.2 原理圖
與發送模塊相比唯一的區別就是faild
代表校驗錯誤或沒有停止位。至於爲什麼叫buffer,是因爲數據會一直緩存到下一次接收。
3.2.3 狀態圖
每個狀態都有自環是在等待bit_cnt
數到11b,代表總線傳輸了1bit。START
回到IDLE
是因爲沒有等到主機拉低數據線(起始位)。
3.2.4 驗證
先上源碼。調試時可以接一個uart模塊,輸出總線上讀到的數據。下面是兩臺不同的電腦發送時的實際波形。
可以看出,主機都是在時鐘下降沿轉變數據線的,這與上行的時序完全不同。注意下圖中的低電平部分,主機拉低的時候比從機拉低的時候要更低一些、過沖更大,這應該與參考點的選取有關,調試時可利用這一特徵來查錯。
3.3 代碼解讀
發送電路與接收電路分別在兩個module中,但存在大量的相似代碼。
3.3.1 位計數器
reg[1:0] bit_cnt;
是兩個電路的“引擎”。
always @(posedge clock_quarter)
if (curr_state != IDLE && curr_state != START)
bit_cnt <= bit_cnt + 1;
else
bit_cnt <= 0;
它直接生成總線時鐘信號assign PS2_CLK = (^bit_cnt) ? 1'b0 : 1'bz;
wire bit_finish = bit_cnt == 2'b11;
也是非常重要的信號,它貫穿整個電路。
3.3.2 數據
發送和接收payload都是狀態機裏的一個狀態,此處需要一個計數器:
// count 8 bit when sending data
always @ (posedge clock_quarter)
if (curr_state != DATA)
byte_cnt <= 0;
else if (bit_finish)
byte_cnt <= byte_cnt + 1;
else
byte_cnt <= byte_cnt;
下面就不同了,接收時使用串入並出移位寄存器:
always @(posedge clock_quarter, posedge reset)
if (reset)
buffer <= 8'h0;
else if (curr_state == START)
buffer <= 8'h0;
else if (curr_state == DATA && bit_finish) // latch PS2_DAT at posedge PS2_CLK
buffer <= {dat_sync, buffer[7:1]}; // LSB first, right shift
else
buffer <= buffer;
發送時,因爲校驗位在payload後面,移位了就沒了,所以筆者選擇用MUX實現:
always @ * // asserting PS2_DAT before negedge PS2_CLK
if (!tx_en)
PS2_DAT = 1'bz;
else if (curr_state == START)
PS2_DAT = 1'b0;
else if (curr_state == DATA)
PS2_DAT = buffer[byte_cnt] ? 1'bz : 1'b0;
else if (curr_state == PARITY)
PS2_DAT = (^buffer) ? 1'b0 : 1'bz;
else
PS2_DAT = 1'bz;
3.3.3 ready / finish
ready都是一樣的assign ready = curr_state == IDLE;
finish有點不同,發送時是這樣的assign finish = curr_state == STOP && bit_finish;
,接收時是assign finish = rx_timeout || (bit_finish && curr_state == ACK);
,其實也就是多了個timeout,用於等待主機產生起始位。
事實上,finish完了馬上就是ready。
3.3.4 failed / abort
發送失敗叫abort:
assign abort = !tx_delay && bit_finish && !PS2_CLK;
接收失敗叫failed:
assign faild = rx_timeout || (bit_finish && (
(curr_state == PARITY && (^buffer) != dat_sync) |
(curr_state == STOP && !dat_sync)));
前文之述備矣。
4. 封裝與耦合——傳輸層
傳輸層承上啓下,通過空閒、發送、接收三個狀態,將下層的發送電路與接收電路耦合起來,並對上層封裝所有細節,使上層得以專注於總線上傳輸的數據,此之謂傳輸層。
此外,爲了上層使用SpinalHDL描述,需要將本層電路寫成blackbox🙈
4.1 原理圖
從上層往下看,只有如下幾個信號:
信號 | 類型 |
---|---|
clock_quarter / reset | ClockDomain |
tx | slave Stream |
rx | master Flow |
tx_failed | output reg |
PS2_CLK / PS2_DAT | inout tri |
龐大的電路被封裝成一個小黑盒子(下圖紅線即連接到片外的PS/2總線):
SpinalHDL裏的BlackBox:
在SpinalHDL中例化模塊非常方便:
val bus = new ps2_bus(80)
bus.PS2_CLK <> PS2_CLK; bus.PS2_DAT <> PS2_DAT
bus.tx.valid := False
bus.tx.payload := B(0)
4.2 對仗工整,強行押韻
強迫症爲了對齊,強行讓read對send,沒想到還押韻了呢,整個狀態轉移渾然天成🎉🎉🎉
4.3 實際波形
依然是兩臺不同的電腦,兩圖都是在設備發送FAh後電腦立即拉低時鐘線。
上圖是在短暫釋放後緊接着又請求發送,而下圖根本就沒釋放,直接進入BUS_READ狀態。上圖還說明了一個問題,當主機發送數據時,若設備不產生時鐘,則主機會等待相當長一段時間。而下圖可以看出,主機發送結束的瞬間(讀到ACK位)就會拉低時鐘線。此外,主機幾乎可以在任何時候拉低時鐘線來中止傳輸,這些都是需要考慮的細節。
觀察這兩圖可以看出,上行幀和下行幀都是從數據線的下降沿開始的,區別在於上行幀開始時時鐘爲高,而下行幀反之。熟練掌握看波形讀數據的能力對後面會話層的調試有很大幫助。
4.4 爲什麼發送電路不會干擾接收電路
筆者在寫代碼之前就在考慮這個問題,直到稀裏糊塗地寫完了之後還是沒明白過來——這麼寫咋就能用了呢?作爲“能用就行”的👷工程師,筆者一直沒有去深究,但現在要寫文檔了,總得有個嚴謹的證明,以彰大國工匠精神。
那麼我們自頂向下看。首先在ps2_bus模塊中,總線是直接連進發送電路和接收電路的,本層沒有任何drive,說明這裏不會產生干擾。
但是有個叫clk_sync
的寄存器,這是檢測時鐘線有沒有被主機拉低的,那麼就有這種可能——設備發送過程中這裏檢測到低電平,誤判爲電腦的發送請求。但是我們的接收電路只有在其start信號的上升沿纔會開始產生時鐘信號從主機讀取,而start信號的定義是wire start_rx = curr_state == BUS_READ && rx_idel;
也就是上圖中叫“comb~1”的與門。那麼問題來了,什麼時候會進入BUS_READ狀態呢?現在翻上去看狀態轉移部分,發現只有在BUS_IDLE狀態纔有可能轉移過去,這樣一來就保證了發送過程中不可能觸發接收動作。同理,在接收時也絕對不會發送,就這麼簡單。
5. 翻譯僞代碼——Spinal描述會話層
話說筆者當初用Verilog寫本層的時候耗時近半個月才調通💩,後來學了Spinal,幾個小時搞定,上板一次成功。
✅SpinalHDL,用了都說好~
5.1 是什麼
電腦開機時會發送一系列指令來檢測這個PS/2接口上是否插入了一個鍵盤,稱之爲“會話層”可能並不恰當,此處僅借用這一概念便於理解。
除了FEh與EEh外,對於電腦的每個指令,設備都必須迴應FAh。如果設備正發送多字節指令時被主機打斷,那麼設備應清空發送緩衝區並優先處理主機發送的指令。一旦開機時識別到了鍵盤,進入系統後每次Caps Lock、Num Lock、Scr Lock變化時主機都會立即發送相應指令告知鍵盤哪個LED應該點亮。關於指令的具體含義詳見《參考》,實際的過程已在2.3節給出。需要注意的是,上電後設備應不停地發送AAh,直到發送成功爲止(因爲剛啓動時總線電平不穩定)。
5.2 爲什麼
5.2.1 先說結論
爲什麼要用SpinalHDL?因爲香。
5.2.2 再說理由
可以略微誇張地說,Spinal在各方面全面碾壓Verilog,如果要舉例子那真的是多如牛毛,不如先上手試試,誰用誰知道。
就拿本層來說,筆者起初用Verilog寫,不清楚應有幾個狀態,來來回回改了好多次,每次改狀態都要
- 在localparam處修改定義
- 修改state寄存器位數
- 修改狀態轉移的always塊
但Spinal只需要
- 定義新狀態,開始寫…
5.3 怎樣做
首先,用中文描述一下這個過程
- 空閒狀態
- 總線 發送=否
- 初始化狀態(入口)
- 總線 發送=是
- 總線 發送 數據=8‘hAA
- 當(總線 未發送失敗 且 正在發送)
- 轉到 空閒狀態
- 接收狀態
- 總線 發送=否
- 應答狀態
- 當 正在發送
- 如果 總線 接收 數據
- 是0xFF:轉到 初始化狀態
- 是0xED:轉到 接收狀態
- 是0xF3:轉到 接收狀態
- 其他:轉到 空閒狀態
- 如果 總線 接收 數據
- 總線 發送=是
- 總線 發送 數據=8’hFA
- 當 正在發送
- (每個狀態)總是
- 當 總線 接收 有效
- 轉到 應答狀態
- 當 總線 接收 有效
接下來,翻譯翻譯,什麼叫SpinalHDL!
中文 | 英文 |
---|---|
狀態 | State |
總線 | bus |
發送 | tx |
接收 | rx |
是 | is |
當 | when |
轉到 | goto |
入口 | EntryPoint |
好了,如果中文描述能看懂,翻譯就是查表,那麼下面的代碼誰也沒有理由看不懂!
不要懷疑,我們就是在寫硬件~
看一下它生成的電路圖吧。
- 用了spinal,誰還想回到三段式呢?
6. TODO:應用層
《參考》中提到,Intel 8042有4個8位寄存器,分別是輸入緩衝區、輸出緩衝區、狀態寄存器和控制寄存器。筆者計劃將會話層完善封裝後掛載到AvalonMM總線,在nios裏編寫應用層軟件。欲知後事如何,且看下回分解……