Linux 輸入子系統是處理來自各種輸入裝置事件的核心框架。本文介紹如何開發根據 I2C 的加速器和 Nunchuk 驅動程式,並將其註冊到輸入子系統。首先,以 ADXL345 加速器為例,說明 I2C 裝置的初始化、設定測量模式、註冊輸入裝置、設定事件型別與程式碼,以及輪詢函式的實作。接著,延伸至 Nunchuk 控制器的整合,闡述硬體連線方式與軟體實作重點。最後,詳細說明如何使用裝置樹描述 Nunchuk 裝置,包含 I2C 控制器設定、時脈頻率、裝置地址等,並深入解析 I2C 驅動程式設定、通訊實作,以及 Input 框架的整合與事件處理。
Linux 核心輸入子系統驅動程式開發
前言
Linux 的輸入子系統(Input Subsystem)是用於處理來自各種輸入裝置(如鍵盤、滑鼠、觸控式螢幕、加速器等)的事件的核心框架。本文將介紹如何開發一個根據 I2C 的加速器驅動程式,並將其註冊到輸入子系統中。
硬體與軟體需求
- ADXL345 加速器模組
- Raspberry Pi 3 或其他支援的硬體平台
- Linux 5.4 核心原始碼
驅動程式開發步驟
1. 初始化 I2C 裝置支援
首先,需要在驅動程式中加入對 ADXL345 加速器的支援。定義裝置的相容性與 I2C 裝置 ID。
static const struct of_device_id ioaccel_dt_ids[] = {
{ .compatible = "arrow,adxl345", },
{ }
};
MODULE_DEVICE_TABLE(of, ioaccel_dt_ids);
static const struct i2c_device_id i2c_ids[] = {
{ "adxl345", 0 },
{ }
};
MODULE_DEVICE_TABLE(i2c, i2c_ids);
2. 設定加速器進入測量模式
使用 SMBus 函式存取加速器的暫存器,使其進入測量模式。
#define POWER_CTL 0x2D
#define PCTL_MEASURE (1 << 3)
#define OUT_X_MSB 0x33
/* 進入測量模式 */
i2c_smbus_write_byte_data(client, POWER_CTL, PCTL_MEASURE);
3. 將裝置註冊到輸入子系統
建立一個私有資料結構 struct ioaccel_dev,用於管理物理裝置與邏輯裝置之間的指標。
struct ioaccel_dev {
struct i2c_client *i2c_client;
struct input_polled_dev *polled_input;
};
4. 初始化輪詢輸入裝置
在 ioaccel_probe() 函式中,分配並初始化 input_polled_dev 結構。
ioaccel->polled_input = devm_input_allocate_polled_device(&client->dev);
ioaccel->i2c_client = client;
ioaccel->polled_input->private = ioaccel;
ioaccel->polled_input->poll_interval = 50;
ioaccel->polled_input->poll = ioaccel_poll;
5. 設定事件型別與程式碼
設定裝置支援的事件型別與程式碼。
set_bit(EV_KEY, ioaccel->polled_input->input->evbit);
set_bit(KEY_1, ioaccel->polled_input->input->keybit);
6. 註冊輪詢輸入裝置
在 ioaccel_probe() 中註冊裝置,在 ioaccel_remove() 中取消註冊。
input_register_polled_device(ioaccel->polled_input);
input_unregister_polled_device(ioaccel->polled_input);
7. 輪詢函式實作
實作 ioaccel_poll() 函式,用於讀取加速器的值並產生輸入事件。
static void ioaccel_poll(struct input_polled_dev *pl_dev)
{
struct ioaccel_dev *ioaccel = pl_dev->private;
int val = 0;
val = i2c_smbus_read_byte_data(ioaccel->i2c_client, OUT_X_MSB);
if ((val > 0xc0) && (val < 0xff)) {
input_event(ioaccel->polled_input->input, EV_KEY, KEY_1, 1);
} else {
input_event(ioaccel->polled_input->input, EV_KEY, KEY_1, 0);
}
input_sync(ioaccel->polled_input->input);
}
#### 內容解密:
此函式會每隔一段時間(由 poll_interval 設定)被呼叫,用於讀取 ADXL345 加速器的 OUT_X_MSB 暫存器的值。根據讀取值決定是否產生 KEY_1 的按下或放開事件,並透過 input_sync() 提交事件。
建置與佈署驅動程式
- 建立新的檔案
i2c_rpi3_accel.c在linux_5.4_rpi3_drivers資料夾中。 - 修改 Makefile 將
i2c_rpi3_accel.o新增到obj-m中。 - 建置並佈署模組到 Raspberry Pi。
~/linux_5.4_rpi3_drivers$ make
~/linux_5.4_rpi3_drivers$ make deploy
輸入子系統驅動程式開發:加速規與Nunchuk控制
加速規輸入子系統驅動開發
在Linux核心中,輸入子系統(Input Subsystem)提供了一個統一的介面,用於處理來自不同輸入裝置的事件,如鍵盤、滑鼠、觸控螢幕等。本章節將探討如何開發一個輸入子系統驅動程式,以控制加速規(Accelerometer)裝置,並將其註冊為輸入裝置。
程式碼解析
以下是i2c_rpi3_accel.c檔案的程式碼片段,用於實作加速規輸入子系統驅動:
#include <linux/module.h>
#include <linux/fs.h>
#include <linux/i2c.h>
#include <linux/input-polldev.h>
struct ioaccel_dev {
struct i2c_client *i2c_client;
struct input_polled_dev *polled_input;
};
#define POWER_CTL 0x2D
#define PCTL_MEASURE (1 << 3)
#define OUT_X_MSB 0x33
static void ioaccel_poll(struct input_polled_dev *pl_dev)
{
struct ioaccel_dev *ioaccel = pl_dev->private;
int val = 0;
val = i2c_smbus_read_byte_data(ioaccel->i2c_client, OUT_X_MSB);
if ((val > 0xc0) && (val < 0xff)) {
input_event(ioaccel->polled_input->input, EV_KEY, KEY_1, 1);
} else {
input_event(ioaccel->polled_input->input, EV_KEY, KEY_1, 0);
}
input_sync(ioaccel->polled_input->input);
}
static int ioaccel_probe(struct i2c_client *client, const struct i2c_device_id *id)
{
// ...
}
內容解密:
ioaccel_poll函式是輪詢函式,用於讀取加速規的資料並產生輸入事件。i2c_smbus_read_byte_data函式用於讀取加速規的OUT_X_MSB暫存器值。- 根據讀取的值,若落在特定範圍內,則產生
EV_KEY事件,並將KEY_1的值設為1或0。 input_sync函式用於同步輸入事件。
程式碼流程圖
@startuml
skinparam backgroundColor #FEFEFE
skinparam componentStyle rectangle
title Linux核心輸入子系統驅動程式開發
package "Linux Shell 操作" {
package "檔案操作" {
component [ls/cd/pwd] as nav
component [cp/mv/rm] as file
component [chmod/chown] as perm
}
package "文字處理" {
component [grep] as grep
component [sed] as sed
component [awk] as awk
component [cut/sort/uniq] as text
}
package "系統管理" {
component [ps/top/htop] as process
component [systemctl] as service
component [cron] as cron
}
package "管線與重導向" {
component [| 管線] as pipe
component [> >> 輸出] as redirect
component [$() 命令替換] as subst
}
}
nav --> file : 檔案管理
file --> perm : 權限設定
grep --> sed : 過濾處理
sed --> awk : 欄位處理
pipe --> redirect : 串接命令
process --> service : 服務管理
note right of pipe
命令1 | 命令2
前者輸出作為後者輸入
end note
@enduml此圖示說明瞭ioaccel_poll函式的邏輯流程。
Nunchuk輸入子系統驅動開發
Nunchuk是Wii遊戲機的控制器,包含3軸加速規、X-Y搖桿和兩個按鈕。透過I2C通訊,可以控制Nunchuk裝置。
硬體連線
將Raspberry Pi的I2C腳位連線到Nunchuk的UEXT接頭:
- 連線Raspberry Pi SCL到UEXT SCL(Pin 5)
- 連線Raspberry Pi SDA到UEXT SDA(Pin 6)
- 連線Raspberry Pi 3.3V到UEXT 3.3V(Pin 1)
- 連線Raspberry Pi GND到UEXT GND(Pin 2)
軟體實作
開發Nunchuk輸入子系統驅動程式,需要實作I2C通訊和輸入事件處理。
LAB 10.2:Nunchuk 裝置樹描述與驅動程式實作
在本文中,我們將探討如何使用裝置樹(Device Tree)描述 Nunchuk 裝置,並實作其驅動程式。首先,我們會分析裝置樹的組態,接著介紹驅動程式的主要程式碼段落,包括 I2C 驅動程式的設定和 Input 框架驅動程式的設定。
裝置樹描述
首先,開啟 bcm2710-rpi-3-b.dts 檔案,找到 i2c1 控制器主節點。在 i2c1 節點中,有一個 pinctrl-0 屬性指向 i2c1_pins 鎖存組態節點,該節點組態了 i2c1 控制器的引腳為 I2C 模式。i2c1_pins 鎖存組態節點定義在 bcm2710-rpi-3-b.dts 檔案中的 gpio 節點屬性內。
&i2c1 {
pinctrl-names = "default";
pinctrl-0 = <&i2c1_pins>;
clock-frequency = <100000>;
status = "okay";
nunchuk: nunchuk@52 {
compatible = "nunchuk";
reg = <0x52>;
};
};
內容解密:
&i2c1表示對i2c1節點的參考。pinctrl-names和pinctrl-0用於設定引腳組態。clock-frequency = <100000>設定 I2C 匯流排的時脈頻率為 100 kHz。status = "okay"啟用i2c1控制器。nunchuk: nunchuk@52是新增的子節點,描述 Nunchuk 裝置。compatible = "nunchuk"與驅動程式中的of_device_id結構相匹配。reg = <0x52>指定 Nunchuk 裝置的 I2C 地址。
Nunchuk 控制器驅動程式描述
Nunchuk 驅動程式主要分為兩個部分:I2C 驅動程式設定和 Input 框架驅動程式設定。
I2C 驅動程式設定
#include <linux/i2c.h>
static struct i2c_driver nunchuk_driver = {
.driver = {
.name = "nunchuk",
.owner = THIS_MODULE,
.of_match_table = nunchuk_of_match,
},
.probe = nunchuk_probe,
.remove = nunchuk_remove,
.id_table = nunchuk_id,
};
module_i2c_driver(nunchuk_driver);
static const struct of_device_id nunchuk_of_match[] = {
{ .compatible = "nunchuk"},
{}
};
MODULE_DEVICE_TABLE(of, nunchuk_of_match);
static const struct i2c_device_id nunchuk_id[] = {
{"nunchuk", 0},
{}
};
MODULE_DEVICE_TABLE(i2c, nunchuk_id);
內容解密:
- 定義了一個
i2c_driver結構體nunchuk_driver,並註冊到 I2C 匯流排。 - 使用
of_match_table和id_table來匹配裝置樹中的 Nunchuk 節點。 probe和remove函式分別在裝置被偵測到和移除時被呼叫。
I2C 通訊實作
static int nunchuk_read_registers(struct i2c_client *client, u8 *buf, int buf_size)
{
int status;
mdelay(10);
buf[0] = 0x00;
status = i2c_master_send(client, buf, 1);
if (status >= 0 && status != 1)
return -EIO;
if (status < 0)
return status;
mdelay(10);
status = i2c_master_recv(client, buf, buf_size);
if (status >= 0 && status != buf_size)
return -EIO;
if (status < 0)
return status;
return 0;
}
內容解密:
- 使用
i2c_master_send和i2c_master_recv函式與 Nunchuk 裝置進行 I2C 通訊。 - 首先傳送一個位元組
0x00以請求資料,然後接收 Nunchuk 傳送的資料。
Input 框架驅動程式設定
#include <linux/input.h>
#include <linux/input-polldev.h>
struct nunchuk_dev {
struct input_polled_dev *polled_input;
struct i2c_client *client;
};
static int nunchuk_probe(struct i2c_client *client, const struct i2c_device_id *id)
{
struct nunchuk_dev *nunchuk;
struct input_polled_dev *polled_device;
nunchuk = devm_kzalloc(&client->dev, sizeof(*nunchuk), GFP_KERNEL);
polled_device = devm_input_allocate_polled_device(&client->dev);
nunchuk->client = client;
polled_device->private = nunchuk;
polled_device->poll_interval = 50;
polled_device->poll = nunchuk_poll;
polled_device->input->dev.parent = &client->dev;
polled_device->input->name = "WII Nunchuk";
polled_device->input->id.bustype = BUS_I2C;
set_bit(EV_KEY, polled_device->input->evbit);
set_bit(BTN_C, polled_device->input->keybit);
set_bit(BTN_Z, polled_device->input->keybit);
// ...
}
內容解密:
- 定義了一個私有資料結構
nunchuk_dev,包含了一個input_polled_dev指標和一個i2c_client指標。 - 在
probe函式中,分配了私有資料結構和輪詢輸入裝置的記憶體,並進行了相關的初始化。 - 設定了輸入裝置的事件型別和事件程式碼,以支援按鍵事件。