背景:公司有一個PS2鍵盤驅動的項目,沒有控制器,需要模拟PS2協定,檢測按鍵并通過input子系統将按鍵時間上報
一、準備:
1、PS2協定:
PS2有兩個控制線,時鐘線和資料線。當按鍵按下或擡起,裝置會向主機發送鍵碼。時鐘由裝置産生,主機需要在下降沿去資料線采集一位資料。按鍵碼由多個11位資料組成:0 (起始)+ 8位(資料)+1/0(奇偶校驗位)+1(結束位)。資料位是先發的低位!
PS2接口是以前台式電腦後邊留的圓形接口,6個引腳,主要用VCC,GND,clk,data。與AT(5腳)一樣
2、鍵碼:
PS2鍵碼大緻分為兩類:
第1類按鍵 通碼為一個位元組,斷碼為0xF0+通碼形式。如A鍵,其通碼為0x1C;斷碼為0xF0 0x1C。
第2類按鍵 通碼為兩位元組0xE0+0xXX形式,斷碼為0xE0+0xF0+0xXX形式。如Right Ctrl鍵,其通碼為0xE0 0x14;斷碼為0xE0 0xF0 0x14。
通碼為按下,段碼為擡起
3、input 子系統下的PS2鍵盤驅動(因為沒有驅動器,沒有使用這個架構):
1,Linux X86 PS/2 鍵盤驅動架構流程(以下均已Intel 8042鍵盤控制器為例):
1.1 裝置初始化注冊流程:
鍵盤控制器硬體驅動(i8042.c) -> 序列槽驅動核心(serio.c) -> 序列槽驅動(atkbd.c)
-> 輸入驅動核心(input.c) -> 輸入事件驅動程式(keyboard.c) -> 虛拟控制台驅動子產品;
注冊流程:
1.1.1,鍵盤控制器硬體驅動(i8042.c):
1)、首先鍵盤硬體驅動程式讀寫鍵盤硬體控制器寄存器,配置寄存器并擷取相控制器硬體資訊(PS/2鍵盤控制器一共有三個寄存器加一個指令端口,指令數量也不多,下載下傳位址:https://download.csdn.net/download/a372048518/9835565)
注意:Intel8042鍵盤控制器内部已經實作了PS/2時序協定,硬體驅動程式隻需要讀寫相關寄存器即可;
2)、将8042鍵盤控制器作為平台裝置(struct platform_device)挂接在平台總線(struct bus_type)上,并将擷取到的硬體資訊及相關軟體資料結構資訊儲存在平台裝置執行個體中;
3)、将8042鍵盤硬體控制器驅動(struct platform_driver)注冊到平台總線上,平台總線比對平台裝置和平台驅動,并調用平台驅動的probe方法,在probe方法中配置設定并初始化序列槽資料結構執行個體(struct serio),将平台裝置資訊放入序列槽資料結構執行個體中;最後将序列槽結構執行個體注冊進入序列槽驅動核心(serio_register_port);
1.1.2,序列槽驅動核心(serio.c)
序列槽驅動核心負責比對序列槽和序列槽驅動,比對之後,調用序列槽驅動的atkbd_connect方法;
1.1.2,PS/2鍵盤序列槽驅動(atkbd.c)
将序列槽驅動(struct serio_driver)注冊到序列槽核心并比對到序列槽裝置之後,調用序列槽驅動的atkbd_connect方法,在atkbd_connect方法中配置設定并初始化輸入裝置(struct input_dev),最後将輸入裝置注冊到輸入子系統核心;
1.1.3, 輸入驅動核心(input.c)
輸入子系統請移步:輸入子系統介紹https://blog.csdn.net/a372048518/article/details/71055147
1.1.4,輸入事件驅動程式(keyboard.c)
鍵盤輸入事件驅動程式負責處理鍵盤輸入裝置上報的鍵盤輸入事件資訊,并将資訊傳遞給虛拟控制台子產品;
1.2 裝置中斷流程:
8042硬體中斷(i8042_interrupt) -> 序列槽核心中斷(serio_interrupt) -> 序列槽驅動中斷(atkbd_interrupt) -> 輸入子系統核心中斷(input_event) -> 輸入時間驅動程式中斷(kbd_event)
4、GPIO模拟PS2思路:
1)、一個CLK引腳設為下降沿觸發中斷模式,當有中斷産生是,讀取資料線的值,連續讀取11個為一組資料,儲存在數組裡,調用鍵碼處理函數,将其中1~8位取出、位移,得到一個8位數。
2)、根據項目的鍵盤共分為12個1類碼和4個2類碼,置兩個碼值的标志位,區分按下和擡起某個按鍵,調用input_report_key()函數,将按鍵事件上報。
3)、驅動将裝置初始化為input裝置,這樣應用層就可以使用input機制擷取事件了
4)、中斷處理鍵碼,當有錯誤碼産生後,會一直錯誤,為避免,需要加一個定時器機制,置零計數
二、實作:
1、input裝置節點的初始化
dev = input_allocate_device();
dev->name = "my_ps2_key";
dev->phys = "ps2";
dev->uniq = "20180523";
dev->id.bustype = BUS_I8042;
dev->id.vendor = ID_PRODUCT;
dev->id.version = ID_VENDOR;
set_bit(EV_SYN, dev->evbit); //設定為同步事件,這個宏可以在input.h中找到
set_bit(EV_KEY, dev->evbit); //因為是按鍵,是以要設定成按鍵事件
set_bit(KEY_A, dev->keybit); //設定這個按鍵表示為KEY_A這個鍵,到時用來上報
。。。
ret = input_register_device(dev); //注冊input裝置
2、GPIO中斷設定
#define PS2_CLK_PIN 5
irq_num1 = gpio_to_irq(PS2_CLK_PIN); //申請中斷号,并注冊中斷,下降沿觸發
err = request_irq(irq_num1, irq_fuction, IRQF_TRIGGER_FALLING, "tiny4412_key1", dev);
3、定時器設定
#define OVER_TIME 1*HZ //逾時時間1秒
struct timer_list timer; //定時器句柄
init_timer(&timer); //初始化定時器timer_list的一些變量
timer.expires = jiffies + OVER_TIME;
timer.function = timer_function;
add_timer(&timer); //将定時器加入到timer清單中,等待處理
mod_timer(&timer,jiffies + OVER_TIME); //激活timer,需要在add後激活才有意義
void timer_function(unsigned long arg) //定時器中斷處理函數
{
countter = 0; //置零資料接收位數技術
mod_timer(&timer,jiffies + OVER_TIME); //再次激活定時器
}
4、GPIO中斷采集資料
static irqreturn_t irq_fuction(int irq, void *dev_id)
{
int bit = 0;
struct input_dev *keydev;
keydev = dev_id; //上報鍵值
bit = gpio_get_value(PS2_DTAT_PIN);
keyValueBuf[countter++] = bit; //儲存在緩沖區,取gpio1_12的值
if(countter == 1){
mod_timer(&timer,jiffies + OVER_TIME);
}
if(countter >= 11) {
//接受完一個鍵碼了
key_member_del(dev_id); //資料處理函數,判斷鍵碼,上報不同鍵碼及按下擡起狀态
countter = 0;
}
return IRQ_HANDLED;
}
5、鍵碼處理函數
代碼較多,腳複雜,占篇幅大,語言表達:定義兩個鍵碼的兩個标志位,初始化為0,當被調用此函數的時候,先判斷這兩個标志位是否都為零,如果都為零,則表示第一次按下某個按鍵。對于第一類碼,這個就是按鍵的區分碼,則判斷是否為12個按鍵碼中的一個,如果是,将其上報為按下,并将标志位置2,擡起的兩個資料,将其減為0,下次有按鍵按下,重複過程。當标志為1時,此次是擡起的區分碼,可以用于上報擡起事件。
對于第二類碼,第一個資料為0xE0,沒法區分鍵值,将标志位置為4,當進入函數時,二類碼标志位為4,則說明此次資料為按鍵的區分碼,判斷是4個按鍵之一,就将其上報為按下,并将标志--,當擡起執行完,标志位正好為0,重複過程。當标志為1時,此次是擡起的區分碼,可以用于上報擡起事件。
如果兩個标志位不全為0,則表示不是按下的第一個碼,區分哪個不為0,對标志位做區分,實作上述過程。
三、細節:
1、鍵盤是需求方提供的,需要用示波器一次采集出每個按鍵按下和擡起的所有碼
A: 1C F0 1C 第一類鍵碼
上: E0 74 E0 F0 74 第二類鍵碼
2、根據應用層所使用的鍵碼(input或PS2),上報對應的鍵值,上報之前可能需要轉換一下
3、所有使用到的按鍵值,在初始化input_dev 的時候都需要set_bit到裝置中,否則不能上報
四、備注:
PS2的驅動及應用程式測試檔案,有需要的可以私聊,剛開始寫部落格,還不知道怎麼上傳