記事一覧

LinuxのUSBドライバを書いてみよう ~RTL2832uでLチカ~

この記事は MCC Advent Calendar 2017 - Adventar の9日目の記事です。
2連続ですね.SyntaxHighlighterの環境をある程度整備したので,プログラミング関係の記事を書きたくなった.

RTL2832uというICを搭載したワンセグチューナ用の,USBドライバを書いてみましょう.
今回は,プログラミングが簡単なLinux上で開発します(Windowsの開発環境は複雑すぎるので...).

以下のような流れで説明します.

  • RTL2832Uとは

  • LinuxのUSBドライバ


    • Hello worldドライバを書く

    • USBのドライバへ改造


  • LEDの点滅の実験

RTL2832uとは


RTL2832uはこのようなワンセグチューナに実装されたICチップです.
チューナの外観
写真を見るとわかるとおり,上にLEDがついています.電波を受信しているときに点灯するものです.今回は,これを制御するドライバを書きます.

ドライバ


ドライバのプログラムは,簡単に説明すると次のようなものです.

  • デバイスとデータをやりとりをする

  • 基本的にはデータのコピー処理の塊

  • カーネルモードで動く

  • C言語で書く


ドライバはデバイスとのデータをやりとりするためのものですから,当然コピー処理が主体となります.
そのため,小難しいアルゴリズムを書くことはありません.

さらに,ドライバはカーネルモードで動きます.これは,ハードウェアに直接アクセスしてデータをやりとりできる,高位の権限を持ちます.ただし,「普通の」プログラムと違い,printfやscanfなどは使えず,カーネルモード専用の関数を使わないといけません.
 例えば,printfのようにメッセージを出したい場合はカーネルモード専用の関数であるprintkを使わないといけません.しかも,これは画面にメッセージを出すわけではありません.ターミナルでdmesg叩くことでのみ,メッセージを確認できるものとなっています.

また,今回はLinuxで開発しますが,その際,以下をインストールします.

  • kernel-devel

  • kernel-headers


apt-getでインストール:

$ sudo apt-get install kernel-devel
$ sudo apt-get install kernel-headers

また,LinuxにはすでにRTL系のドライバが入ってることが多いです(lsmodを打ってみるとrtl~なドライバが見えたりする).その状態では,今回作るRTL2832u用のドライバが動きませんので,無効にしないといけません.
こちらの環境

$ uname -a
Linux VBox 4.4.0-36-generic #55-Ubuntu SMP Thu Aug 11 18:00:59 UTC 2016 i686 i686 i686 GNU/Linux
$ lsb_release -a
No LSB modules are available.
Distributor ID: Ubuntu
Description: Ubuntu 16.04.1 LTS
Release: 16.04
Codename: xenial

でも入っていたので,以下を別ディレクトリにうつして無効化しました.

/lib/modules/4.4.0-36-generic/kernel/drivers/media/usb/dvb-usb-v2/dvb-usb-rtl28xxu.ko
/lib/modules/4.4.0-36-generic/kernel/drivers/media/dvb-frontends/rtl2832.ko


Hello worldドライバを書く


まずは,基本的なHello worldを書きます.

// HelloDriver.c
#include <linux/module.h>
#include <linux/kernel.h>

int Init(void) {
// dmesg用のprintfのようなもの
printk(KERN_ALERT "[+] HelloDriver: Loaded.\n");
return 0;
}

void Exit(void) {
printk(KERN_ALERT "[+] HelloDriver: Unloaded.\n");
}

module_init(Init); // 初期化関数を設定(Initでなくても良い)
module_exit(Exit); // 終了関数を設定
MODULE_DESCRIPTION("HelloDriver"); // ドライバ
MODULE_AUTHOR("Um6ra1"); // 作者名(なまえをいれてください)
MODULE_LICENSE("Dual BSD/GPL"); // ライセンス表示


さらに,コンパイル用のMakefileを用意します(ファイル名の先頭は大文字です!!).

# Makefile
KERNEL_HEADERS = /lib/modules/$(shell uname -r)/build

obj-m := HelloDriver.o
ccflags-y := -std=gnu99 -Wno-declaration-after-statement

all:
$(MAKE) -C $(KERNEL_HEADERS) M=$(PWD) modules

clean:
$(MAKE) -C $(KERNEL_HEADERS) M=$(PWD) clean

できたらmakeを打ちます.

$ make

ここでlsを打ってみると,色々ファイルができています.

$ ls
HelloDriver.c HelloDriver.mod.c HelloDriver.o modules.order
HelloDriver.ko HelloDriver.mod.o Makefile Module.symvers

この内,ドライバ本体は.koファイルです.これをinsmodコマンドで実行します.

$ sudo insmod HelloDriver.ko

しかし,何か画面に表示されるわけでもなく,実行されたかよくわかりません.
そこで,dmesgコマンドを打ってみます.

$ dmesg
...
[11521.111731] [+] HelloDriver: Loaded.

最後の方に,Init関数でprintkに書いたメッセージが出ていると思います.これで実行できたことを確認できました.

ロードが確認できたので,ドライバを終了します.

sudo rmmod HelloDriver

一応dmesgしてみると,アンロードされていることも確認できます.

$ dmesg
...
[12892.977632] [+] HelloDriver: Unloaded.


USBドライバへの改造


先程のHello worldをUSBドライバへ改造しましょう.そこでまず,コード量を減らすため,予め型やマクロを定義したヘッダファイル(Typedefs.h)を用意します(すみません,一部使っていないものが含まれています).


// Typedefs.h
#pragma once

typedef unsigned long ul;
typedef unsigned int uint;
typedef struct usb_interface UsbInterface;
typedef struct usb_device_id UsbDeviceID;
typedef struct usb_device UsbDevice;
typedef struct usb_anchor UsbAnchor;
typedef struct usb_host_interface UsbHostInterface;
typedef struct usb_endpoint_descriptor UsbEndpointDescriptor;
typedef struct usb_class_driver UsbClassDriver;
typedef struct usb_ctrlrequest UsbCtrlRequest;
typedef struct semaphore Semaphore;
typedef struct urb Urb;
typedef struct kref Kref;
typedef struct mutex Mutex;
typedef struct completion Completion;
typedef struct inode Inode;
typedef struct file File;
typedef struct file_operations FileOperations;
typedef int (*PfIoctl)(Inode *pInode, File *pFile, uint cmd, ul arg);

#define DRIVER_DESC "LedCtrl" // ドライバの名前
// printf
#define DMESG_INFO(fmt, ...) printk(KERN_INFO "[+] %s: %s: " fmt, DRIVER_DESC, __func__, ## __VA_ARGS__)
#define DMESG_ERR(fmt, ...) printk(KERN_ERR "[!] %s: %s: " fmt, DRIVER_DESC, __func__, ## __VA_ARGS__)


次に,プログラムの本体です.

// LedCtrl.c

#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/usb.h>
#include <linux/slab.h> // kmalloc
#include "Typedefs.h"

// ベンダID,プロダクトID(lsusbなどで調べる)
#define VENDOR_ID 0x0bda
#define PRODUCT_ID 0x2832

#define MINOR_BASE 192 // 最初のマイナー番号(適当)

// 関数プロトタイプ
int Probe(UsbInterface *ip, const UsbDeviceID *pID);
void Disconnect(UsbInterface *ip);
ssize_t Write(File *fp, const s8 *pUser, size_t count, loff_t *pPos);
int Open(Inode *ip, File *fp);
int Release(Inode *ip, File *fp);

// ドライバ独自の構造体(名前やメンバはユーザが決める)
typedef struct {
UsbDevice *pDev; 
UsbInterface *ip;
u8 bulkInEndpointAddr; // エンドポイントのアドレス
Kref kref;
} LedCtrl;

struct usb_device_id entries[] = {
{USB_DEVICE(VENDOR_ID, PRODUCT_ID)}, // ベンダ,プロダクトIDを設定
{}
};
MODULE_DEVICE_TABLE(usb, entries);
struct usb_driver usbDriver = {
.name = "Rtl 2832u Usb 1Seg Driver", // ドライバの説明
.id_table = entries,
.probe = Probe, // 最初に呼ばれる関数
.disconnect = Disconnect, // 制御対象のデバイスが抜かれたら呼ばれる関数
};

// ファイルオペレーション
FileOperations fops = {
.owner = THIS_MODULE,
.write = Write, // writeシステムコールにより呼ばれる
.open = Open, // openされたら呼ばれる
.release = Release, // 解放関数
};
UsbClassDriver class = {
.name = "usb/LedCtrl%d", // /devでのファイル名
.fops = &fops,
.minor_base = MINOR_BASE,
};

// ---- 関数定義 ----

// 解放関数
void Delete(Kref *pKref) {
#define ToDev(d) container_of(d, LedCtrl, kref)
LedCtrl *pDev = ToDev(pKref);

usb_put_dev(pDev->pDev);
kfree(pDev); // 独自構造体を解放
}

// レジスタから読出し
// RTL2832uでは,valueはアドレス,indexはコマンド
int ReadReg(LedCtrl *pDev, u16 value, u16 index, u8 *pData, int size) {
memset(pData, 0, size); // まずはバッファをクリア

// USBデバイスにコマンドを送る
int ret = usb_control_msg(pDev->pDev,
usb_rcvctrlpipe(pDev->pDev, 0), // Endpoint0とのパイプを形成
0, (USB_DIR_IN | USB_TYPE_VENDOR), // USB側からデータが「IN」してくる
value, index, pData, size, 50*HZ); // アドレス,コマンド,バッファ,サイズ,タイムアウト時間

// 読み出せなかった...
if(ret != size) {
DMESG_ERR("Read=%d\n", ret);
return -1;
}
return 0;
}

// レジスタに書込み
int WriteReg(LedCtrl *pDev, u16 value, u16 index, u8 *pData, int size) {
int written = usb_control_msg(pDev->pDev,
usb_sndctrlpipe(pDev->pDev, 0),
0, (USB_DIR_OUT | USB_TYPE_VENDOR),
value, index, pData, size, 50*HZ);

if(written != size) {
DMESG_ERR("Written=%d\n", written);
return -1;
}
return 0;
}

// LEDの制御(on=0: OFF, =1: ON)
int ControlLED(LedCtrl *pDev, int on) {
u8 gpio;
ReadReg(pDev, 0x3001, 0x0200, &gpio, 1); // GPIOの値を読む
if(on) gpio |= 0x04; // ONなら3bit目を1に
else gpio &= ~0x04; // OFFなら3bit目を0に
WriteReg(pDev, 0x3001, 0x0210, &gpio, 1); // GPIOに書き込み
return 0;
}

// writeシステムコール
// echo -n "ON" > /dev/LedCtrl0 のようにしてアクセス
ssize_t Write(File *fp, const s8 *pUser, size_t count, loff_t *pPos) {
LedCtrl *pDev = fp->private_data;
u32 msg;
copy_from_user(&msg, pUser, 4); // ユーザ空間からデータをコピー
if((u16)msg == 0x4E4F) // "ON"を受取った
ControlLED(pDev, 1); // LEDをONに
else if(msg == 0x0046464F) //"OFF"を受取った
ControlLED(pDev, 0); // LEDをOFFに
return count;
}

// openシステムコール,主に初期化
int Open(Inode *ip, File *fp) {
UsbInterface *pIntf = usb_find_interface(&usbDriver, iminor(ip)); // USBインターフェイスを探索
LedCtrl *pDev= usb_get_intfdata(pIntf); // USBインターフェイスを得る
kref_get(&pDev->kref); // デバイスの参照カウンタを+1してから得る
fp->private_data = (void *)pDev; // 独自構造体を紐付け
return 0;
}

int Release(Inode *ip, File *fp) {
LedCtrl *pDev= fp->private_data;
kref_put(&pDev->kref, Delete); // 参照カウンタを減らし,Delete関数を呼ぶ
return 0;
}

// 最初に呼ばれる関数
int Probe(UsbInterface *ip, const UsbDeviceID *pID) {
int errno = -ENOMEM;

LedCtrl *pDev = kmalloc(sizeof(LedCtrl), GFP_KERNEL);
if(!pDev) {
DMESG_ERR("Out of memory.\n");
goto L_Error;
}
memset(pDev, 0, sizeof(*pDev)) ;
kref_init(&pDev->kref); // 参照カウンタを初期化
pDev->pDev = usb_get_dev(interface_to_usbdev(ip)); // USBデバイスがあるか
pDev->ip = ip;

UsbHostInterface *pHostIf = ip->cur_altsetting;
// 0番目のエンドポイントを得る.このチューナはBulk-Inのep0が1つしかない(当然,control endpointもlsusbで表示されないが存在はしている)
UsbEndpointDescriptor *ep = &pHostIf->endpoint[0].desc;
pDev->bulkInEndpointAddr = ep->bEndpointAddress; // エンドポイントのアドレス

usb_set_intfdata(ip, pDev);

errno = usb_register_dev(ip, &class); // USBデバイスを登録
if(errno) {
DMESG_ERR("Not able to get minor for this device.\n");
usb_set_intfdata(ip, NULL);
goto L_Error;
}
dev_info(&ip->dev, "[+] LedCtrl: Attached=%d", ip->minor);
return 0;

L_Error: // エラー
if(pDev) kref_put(&pDev->kref, Delete);
return errno;
}

// USBデバイスを抜くと呼ばれる
void Disconnect(UsbInterface *ip) {
dev_info(&ip->dev, "[+] LedCtrl: (%d) is Disconnected", ip->minor);

LedCtrl *pDev= usb_get_intfdata(ip);
usb_set_intfdata(ip, NULL);

usb_deregister_dev(ip, &class);
kref_put(&pDev->kref, Delete);
}

module_usb_driver(usbDriver);
MODULE_DESCRIPTION(DRIVER_DESC);
MODULE_AUTHOR("Um6ra1");
MODULE_LICENSE("Dual BSD/GPL");

いきなり複雑で申し訳ないのですが,要するに

  • USBの初期化処理

  • USBの終了処理


そして,システムコール

  • write

  • open


に対する動作,加えて初期化や終了処理を定義したことになります.
最初に呼ばれるのはProbe関数で,ここでUSBとドライバの紐付けがなされます.これで,openシステムコールに対してはOpenが,writeシステムコールに対してはWrite関数が反応してくれるようになるわけです.
 Write関数が重要で,ここでユーザ空間とデータをやりとりします.まず,echoなどで送られてきたデータを,copy_from_userによってmsgに格納します.このmsgの値によってLEDのON/OFFを切替えます.
 LEDのON/OFFはUSBのGPIOレジスタに書き込むことで実現されます.実際,GPIOのアドレスは0x3001で,その3bit目がLEDのON/OFFを決定します.具体的には,ReadReg関数でGPIOレジスタを読み出し,3bit目を編集し,WriteRegで書き戻す処理で行っています.また,ReadRegやWriteRegで使われるusb_rcvctrlpipe,usb_sndctrlpipe関数はデバイスとの窓口になっていて,コマンドやデータの受け渡しを行います.これらはEndPoint0というエンドポイントにリクエストを送受または受信します.

LEDの点滅の実験


Lチカをしてみましょう.先程のソースをコンパイルします.

$ make

その後,insmodでインストールします.

$ sudo insmod LedCtrl.ko

ここでdmesgしてみて,

$ dmesg
...
[15807.086305] Rtl 2832u Usb 1Seg Driver 1-2:1.0: [+] Usb1Seg: Attached=0
[15807.086341] usbcore: registered new interface driver Rtl 2832u Usb 1Seg Driver

のようなメッセージが出ればきちんと認識しています.そして,/devをlsで確認すると,

$ ls /dev
...
LedCtrl0

と,LedCtrl0といデバイスファイルが見つかります.これに,echoなどでコマンドを飛ばすわけです.しかし,この状態ではアクセス権限がないので,これを設定してやります.

$ sudo chmod 0777 /dev/LedCtrl0


試しに,LEDをONにするコマンドを送ります.

$ sudo echo -n "ON" > /dev/LedCtrl0

すると,写真のようにチューナのLEDが点灯します.
チューナのLEDが点灯
逆に,

$ sudo echo -n "OFF" > /dev/LedCtrl0

を送ると消灯です.

以上,駆け足での説明となりましたが,これで以上となります.

Comments

Post a comment

Private comment