RaspberryPiで
デバイスドライバを作ってみた。
byk‑onishi
自己紹介
名前:大西和貴( @_k_onishi_ )
出身:京都
所属:SAKURAInternetInc.
アプリケーションエンジニア
(Go(Goji),Typescript(React.js),PHP,LXC,Etc...)
趣味:
CPU,Kernel,仮想化,CTF(Pwn),マルウェア
読書、ピアノ、ギター、カラオケ
概要
対象となるチップセットのメモリマップの概要
及びキャラクタデバイスドライバの実装。
環境
PaspberryPi3ModelB
$ sudo cat /etc/os-release
PRETTY_NAME="Raspbian GNU/Linux 9 (stretch)"
NAME="Raspbian GNU/Linux"
VERSION_ID="9"
VERSION="9 (stretch)"
ID=raspbian
ID_LIKE=debian
HOME_URL="http://www.raspbian.org/"
SUPPORT_URL="http://www.raspbian.org/RaspbianForums"
BUG_REPORT_URL="http://www.raspbian.org/RaspbianBugs"
$ sudo uname -a
Linux raspberrypi 4.9.41-v7+ #1023 SMP Tue Aug 8 16:00:15 BST 2
Chipset
まずデータシートからチップセットを調べる。
Processer
BroadcomBCM2387chipset.
1.2GHzQuad‑CoreARMCortex‑A53(64Bit)
引用:https://www.terraelectronica.ru/pdf/show?
pdf_file=%2Fds%2Fpdf%2FT%2FTechicRP3.pdf
上記から BCM2387 であることがわかる。
BCM2387
https://web.stanford.edu/class/cs140e/docs/BCM2837‑ARM‑
Peripherals.pdf
ペリフェラルレジスタ
ペリフェラルデバイスのレジスタは、ペリフェラル(プログラマブ
ル・カウンタや割り込み制御、シリアル通信ポートなどのハードウ
ェア)の動作を設定したり、動作状況を読み出したりするためのレ
ジスタである。
引用:https://ja.wikipedia.org/wiki/レジスタ_(コンピュータ)#ペリ
フェラルデバイスのレジスタ
データシートを見ると以下のような記述があった。
Peripherals(atphysicaladdress0x3F000000on)aremapped
intothekernelvirtualaddress
spacestartingataddress0xF2000000.
引用:https://web.stanford.edu/class/cs140e/docs/BCM2837‑
ARM‑Peripherals.pdfp6
上記からペリフェラルレジスタが 0x3F000000 からマッピングされてい
ることがわかる。
ドキュメントから bcm_host_get_peripheral_address() でペリフェ
ラルレジスタのアドレスを取得できることがわかったので実際に以下の
コードで確かめる。
https://www.raspberrypi.org/documentation/hardware/raspberrypi/peri
pheral_addresses.md
$ cat samples/get_peripheral_address.c
#include <stdio.h>
#include <bcm_host.h>
int main(int argc, char* argv[]) {
printf("0x%08Xn", bcm_host_get_peripheral_address());
return 0;
}
$ gcc samples/get_peripheral_address.c -I/opt/vc/include -L/opt
$ ./myfile
0x3F000000
/proc/iomem からもメモリマップが確認できる。
$ sudo cat /proc/iomem
00000000-3b3fffff : System RAM
00008000-00afffff : Kernel code
00c00000-00d3da63 : Kernel data
3f006000-3f006fff : dwc_otg
3f007000-3f007eff : /soc/dma@7e007000
3f00b840-3f00b84e : /soc/vchiq
3f00b880-3f00b8bf : /soc/mailbox@7e00b880
3f101000-3f102fff : /soc/cprman@7e101000
3f200000-3f2000b3 : /soc/gpio@7e200000
3f201000-3f201fff : /soc/serial@7e201000
3f201000-3f201fff : /soc/serial@7e201000
3f202000-3f2020ff : /soc/sdhost@7e202000
3f215000-3f215007 : /soc/aux@0x7e215000
3f300000-3f3000ff : /soc/mmc@7e300000
3f980000-3f98ffff : dwc_otg
GPIO制御用レジスタ
TheGPIOhas41registers.Allaccessesareassumedtobe32‑bit.
https://web.stanford.edu/class/cs140e/docs/BCM2837‑ARM‑
Peripherals.pdfp90
BCM2387のデータシートp90にGPIOレジスタのマッピング表が記載さ
れている。
Address Field
Name Description Size Read/Write
0x7E200000 GPFSEL0 GPIOFunction
Select0 32 R/W
0x7E200004 GPFSEL1 GPIOFunction
Select1 32 R/W
0x7E200008 GPFSEL2 GPIOFunction
Select2 32 R/W
0x7E20000C GPFSEL3 GPIOFunction
Select3 32 R/W
0x7E200010 GPFSEL4 GPIOFunction
Select4 32 R/W
0x7E200014 GPFSEL5 GPIOFunction
Select5 32 R/W
Address Field
Name Description Size Read/Write
0x7E20001C GPSET0 GPIOPinOutput
Set0 32 W
0x7E200020 GPSET1 GPIOPinOutput
Set1 32 W
0x7E200024 ‑ Reserved ‑ ‑
0x7E200028 GPCLR0 GPIOPinOutput
Clear0 32 W
0x7E20002C GPCLR1 GPIOPinOutput
Clear1 32 W
0x7E200030 ‑ Reserved ‑ ‑
0x7E200034 GPLEV0 GPIOPinLevel0 32 R
0x7E200038 GPLEV1 GPIOPinLevel1 32 R
GPIO"FunctionSelect"Registers(GPFSEL0~
GPFSEL5)
Thefunctionselectregistersareusedtodefinetheoperationof
thegeneral‑purposeI/O
pins
https://web.stanford.edu/class/cs140e/docs/BCM2837‑ARM‑
Peripherals.pdfp91
Bit(s) FieldName Description Type Reset
29‑27 FSEL9
FSEL9‑FunctionSelect9
000=GPIOPin9isaninput
001=GPIOPin9isanoutput
R/W 0
26‑24 FSEL8 FSEL8‑FunctionSelect8 R/W 0
23‑21 FSEL7 FSEL7‑FunctionSelect7 R/W 0
20‑18 FSEL6 FSEL6‑FunctionSelect6 R/W 0
17‑15 FSEL5 FSEL5‑FunctionSelect5 R/W 0
Bit(s) FieldName Description Type Reset
14‑12 FSEL4 FSEL4‑FunctionSelect4 R/W 0
11‑9 FSEL3 FSEL3‑FunctionSelect3 R/W 0
8‑6 FSEL2 FSEL2‑FunctionSelect2 R/W 0
5‑3 FSEL1 FSEL1‑FunctionSelect1 R/W 0
2‑0 FSEL0 FSEL0‑FunctionSelect0 R/W 0
3bitずつが各ピンに対応しており、同じ要領で GPFSEL5 まで続き54ピ
ン全てに対応する。
GPIOPinOutputSetRegisters(GPSET0~GPSET1)
TheoutputsetregistersareusedtosetaGPIOpin.
https://web.stanford.edu/class/cs140e/docs/BCM2837‑ARM‑
Peripherals.pdfp95
アウトプット用のレジスタで各ビットが各ピンに対応している。
GPSET0 を例に対応表をいかに示す。
Bit(s) FieldName Description Type Reset
31‑0 SETn(n=0..31) 0=Noeffect
1=SetGPIOpinn R/W 0
同じ要領で GPSET1 を用いて54ピンまで対応している。
GPIOPinOutputClearRegisters(GPCLR0~
GPCLR1)
Theoutputclearregisters)areusedtoclearaGPIOpin.
https://web.stanford.edu/class/cs140e/docs/BCM2837‑ARM‑
Peripherals.pdfp95
これはアウトプットのクリア用レジスタで先ほどと同様に各ビットが各
ピンに対応している。
GPCLR0 を例に対応表をいかに示す。
Bit(s) FieldName Description Type Reset
31‑0 CLRn(n=0..31) 0=Noeffect
1=ClearGPIOpinn R/W 0
同じ要領で GPCLR1 を用いて54ピンまで対応している。
GPIOPinLevelRegisters(GPLEV0~GPLEV1)
Thepinlevelregistersreturntheactualvalueofthepin.
https://web.stanford.edu/class/cs140e/docs/BCM2837‑ARM‑
Peripherals.pdfp96
ピンの値を取得する用のレジスタで先ほどと同様に各ビットが各ピンに
対応している。
GPLEV0 を例に対応表をいかに示す。
Bit(s) FieldName Description Type Reset
31‑0 LEVn
(n=0..31)
0=GPIOpinnis
low
1=GPIOpinnis
high R/W
同じ要領で GPLEV1 を用いて54ピンまで対応している。
実装に関して
/dev/mem
mmap()
PAGE_SIZE
/dev/mem
memはコンピュータのメインメモリーイメージのキャラクターデ
バイスファイル(characterdevicefile)である。
https://linuxjm.osdn.jp/html/LDP_man‑pages/man4/mem.4.html
mmap()
ファイルやデバイスをメモリーにマップ/アンマップする。
https://linuxjm.osdn.jp/html/LDP_man‑pages/man2/mmap.2.html
当該関数を用いて先ほどのデバイスファイルをプロセスのメモリにマッ
ピングしアクセスを行う。
PAGE_SIZE
仮想メモリを扱う単位であるページのサイズを取得する。これは
mmap() 関数でマッピングするメモリのサイズに用いる(ページサイズ
でマッピングするのが定石らしい)
$ getconf PAGE_SIZE
4096
実装
https://github.com/k‑onishi/gpio‑lib
キャラクタデバイス作成(カーネルモジュール)
システムコールに対応する関数の作成
メジャー番号とマイナー番号の取得
キャラクタデバイス構造体の初期化
ドライバの登録
デバイスのクラス登録
デバイスファイルの作成
システムコール対応の関数テーブル作成
struct file_operations my_file_ops = {
.owner = THIS_MODULE,
.open = my_open,
.release = my_close,
.read = my_read,
.write = my_write,
.unlocked_ioctl = my_ioctl, /* 64 bits */
.compat_ioctl = my_ioctl, /* 32 bits */
};
open 及び release で物理アドレスと仮想アドレスのマッピングを行
い、 ioctl で操作対象のピンの変更、そして実際の読み書きは read 及
び write で行う。
file_operations は実装できるシステムコールに対応した処理を保持
する構造体で以下のように定義されている。
struct file_operations {
struct module *owner;
loff_t (*llseek) (struct file *, loff_t, int);
ssize_t (*read) (struct file *, char __user *, size_t
ssize_t (*aio_read) (struct kiocb *, char __user *,
ssize_t (*write) (struct file *, const char __user *,
ssize_t (*aio_write) (struct kiocb *, const char __user
int (*readdir) (struct file *, void *, filldir_t);
unsigned int (*poll) (struct file *, struct poll_table_
int (*ioctl) (struct inode *, struct file *, unsigned
long (*unlocked_ioctl) (struct file *, unsigned int
long (*compat_ioctl) (struct file *, unsigned int,
int (*mmap) (struct file *, struct vm_area_struct *);
int (*open) (struct inode *, struct file *);
int (*flush) (struct file *);
int (*release) (struct inode *, struct file *);
int (*fsync) (struct file *, struct dentry *, int datas
int (*aio_fsync) (struct kiocb *, int datasync);
int (*fasync) (int, struct file *, int);
int (*lock) (struct file *, int, struct file_lock *);
ssize_t (*readv) (struct file *, const struct iovec *,
ssize_t (*writev) (struct file *, const struct iovec *,
ssize_t (*sendfile) (struct file *, loff_t *, size_t
ssize_t (*sendpage) (struct file *, struct page *,
unsigned long (*get_unmapped_area)(struct file *,
int (*check_flags)(int);
int (*dir_notify)(struct file *filp, unsigned long
int (*flock) (struct file *, int, struct file_lock *);
};
メジャー番号とマイナー番号の取得
以下では alloc_chrdev_region 関数でメジャー番号の取得を行い、
dev_t 型の変数から MAJOR マクロを用いてメジャー番号を取得してい
る。
dev_t dev;
alloc_ret = alloc_chrdev_region(&dev, MINOR_NUMBER_START, NUMBE
if (alloc_ret != 0) {
printk(KERN_ERR "failed to alloc_chrdev_region()n");
return -1;
}
major_number = MAJOR(dev);
dev_t は以下のように定義されており unsigned int であることがわ
かる。そしてその変数から決められた上位12ビットをメジャー番号とし
ている。
typedef __u32 __kernel_dev_t;
typedef __kernel_dev_t dev_t;
// include/linux/kdev_t.h
#define MINORBITS 20
#define MINORMASK ((1U << MINORBITS) - 1)
#define MAJOR(dev) ((unsigned int) ((dev) >> MINORBITS))
#define MINOR(dev) ((unsigned int) ((dev) & MINORMASK))
#define MKDEV(ma,mi) (((ma) << MINORBITS) | (mi))
alloc_chrdev_region() は以下のように定義されており
__register_chrdev_region の戻り値からメジャー番号とマイナー番
号を設定している。
int alloc_chrdev_region(dev_t *dev, unsigned baseminor, unsigne
const char *name)
{
struct char_device_struct *cd;
cd = __register_chrdev_region(0, baseminor, count, name
if (IS_ERR(cd))
return PTR_ERR(cd);
*dev = MKDEV(cd->major, cd->baseminor);
return 0;
}
キャラクタデバイス構造体の初期化
/* initialize cdev and function table */
cdev_init(&my_char_dev, &my_file_ops);
my_char_dev.owner = THIS_MODULE;
cdev_init() は以下のように定義されておりキャラクタデバイス構造
体のopsメンバに引数であるシステムコールハンドラ関数のテーブルを
設定している。
void cdev_init(struct cdev *cdev, struct file_operations *fops)
{
memset(cdev, 0, sizeof *cdev);
INIT_LIST_HEAD(&cdev->list);
cdev->kobj.ktype = &ktype_cdev_default;
kobject_init(&cdev->kobj);
cdev->ops = fops;
}
cdev は以下のように定義されており、先ほど作成したシステムコール
テーブルを保持するメンバが存在する。
struct cdev {
struct kobject kobj;
struct module *owner;
struct file_operations *ops;
struct list_head list;
dev_t dev;
unsigned int count;
};
ドライバの登録
デバイスドライバの登録は cdev_add() で行う。キャラクタデバイス構
造体やデバイス番号を保持している変数、当該デバイスに割り当てるマ
イナー番号の数などの設定を行う。
cdev_err = cdev_add(&my_char_dev, dev, NUMBER_MINOR_NUMBER);
if (cdev_err != 0) {
printk(KERN_ERR "failed to cdev_add()n");
unregister_chrdev_region(dev, NUMBER_MINOR_NUMBER);
return -1;
}
デバイスのクラス登録
class_create() でクラス登録を行う。当該処理
で /sys/class/my_device/ が作成される。
my_char_dev_class = class_create(THIS_MODULE, DEVICE_NAME);
if (IS_ERR(my_char_dev_class)) {
printk(KERN_ERR "class_create()n");
cdev_del(&my_char_dev);
unregister_chrdev_region(dev, NUMBER_MINOR_NUMBER);
return -1;
}
デバイスファイルの作成
device_create() で実際に指定のメジャー番号及びマイナー番号のデ
バイスを登録する。(ex. sys/class/my_device/my_device )
device_create(my_char_dev_class, NULL, MKDEV(major_number,
実装に関して
module_init()&module_exit()
PAGE_SIZE
ioremap_nocache()
get_user()&put_user();
module_init()&module_exit()
モジュールのロード及びアンロード時に呼び出される関数を登録。
先ほどの説明した一連の処理は module_init の引数となっている関数
内で行う。
module_init(my_init);
module_exit(my_exit);
PAGE_SIZE
カーネルが仮想アドレス空間においてメモリをチャンクとして扱う際の
サイズ。
// include/asm-i386/page.h
#define PAGE_SHIFT 12
#define PAGE_SIZE (1UL << PAGE_SHIFT)
ioremap_nocache()
カーネル空間に物理アドレスのマッピングを行う。
base_address = (int)ioremap_nocache(
GPIO_ADDRESS, PAGE_SIZE);
return 0;
get_user()&put_user()
ユーザ空間<‑>カーネル空間でデータのコピーを行う。
static ssize_t my_write(struct file* file, const char __user* b
{
int mode;
get_user(mode, &buff[0]);
:
(省略)
:
static ssize_t my_read(struct file* file,
char __user* buff,
size_t count, loff_t *pos)
{
int num_reg = (pin_number / NUM_PIN_EACH_REG);
int offset = (pin_number % NUM_PIN_EACH_REG);
int addr = base_address + GPLEV0_OFFSET
+ (REG_GAP * num_reg);
unsigned int reg_value = MEMORY(addr);
int value = ((reg_value >> offset) & 1UL);
put_user(value + '0', &buff[0]);
return count;
}
実装
https://github.com/k‑onishi/gpio‑driver

ラズパイでデバイスドライバを作ってみた。