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() 提交事件。

建置與佈署驅動程式

  1. 建立新的檔案 i2c_rpi3_accel.clinux_5.4_rpi3_drivers 資料夾中。
  2. 修改 Makefile 將 i2c_rpi3_accel.o 新增到 obj-m 中。
  3. 建置並佈署模組到 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-namespinctrl-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_tableid_table 來匹配裝置樹中的 Nunchuk 節點。
  • proberemove 函式分別在裝置被偵測到和移除時被呼叫。

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_sendi2c_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 函式中,分配了私有資料結構和輪詢輸入裝置的記憶體,並進行了相關的初始化。
  • 設定了輸入裝置的事件型別和事件程式碼,以支援按鍵事件。