LECTURE 8 I/O ポートの制御

○ はじめに

ここまでの内容では、デバイスドライバと言ってもソフトウェアで完結してしまう内容ばかりでした。ここでは、実際にハードウェアを制御してみることで、デバイスドライバらしい内容にしてみたいと思います。

具体的には、PC に付いている ( 最近の PC では無いかもしれませんが。。。 ) パラレルポート ( プリンタポート ) に、自作の簡単な電子回路を接続します。そして、それを制御するためのデバイスドライバを作成します。

CPU から周辺機器を制御する場合、通常のメモリ空間に I/O ポートがマッピングされておりメモリとデータをやりとりする感覚で制御する方法と、入出力専用のアドレス空間に対して専用の命令でデータをやりとりする方法があります。後者の場合、入出力専用のアドレス空間を I/O アドレスと呼び、パラレルポートの制御は後者の方法で行います。

このページの内容は自作の電子回路を繋ぐという性質上、失敗するとパラレルポートやマザーボードが損傷する可能性があります。大丈夫な様に書いているつもりではいますが、筆者は責任を負えませんので、自己責任をとれない状況では行わないでください。また、最悪の場合でも損傷しては困る様な PC で行うのも止めた方が良いかもしれません。

○ パラレルポート ( プリンタポート )

PC のパラレルポート ( プリンタポート ) について、まず説明します。ただし、プリンタとのやりとりを行うための説明ではなく、単純に I/O 制御を行う為に必要な事項のみ説明します。

パラレルポートにアクセスするための I/O アドレスは BIOS の設定等によって変化するのですが、通常 0x378 から始まる 3 バイトのアドレス空間にマッピングされていると思います。この開始アドレスのことをベースアドレスと呼びます。

以降、本 Web ではパラレルポートのベースアドレスは 0x378 であるとして説明しますが、この 0x378 という値はお使いの環境によって変わりますので、注意してください。

--- パラレルポートのピン配置図 ---

パラレルポートのピン配置図

上のピン配置図では、関連するピンをグループ化してます ( 長方形で囲んである )。各グループの詳細は以下の通りです。

  • データポート ( ピン 2 - 9 )

    入出力用のポートです。I/O アドレスは ( ベースアドレス + 0 ) です。 この 1 バイト値におけるピンとビットのマッピングは以下の通りです。

    ビット番号 7 6 5 4 3 2 1 0
    ピン番号 9 8 7 6 5 4 3 2
    ビット値が 1 の時の電圧 High High High High High High High High

  • ステータスポート ( ピン 10 - 13、15 )

    読み込み専用のポートです。I/O アドレスは ( ベースアドレス + 1 ) です。 この 1 バイト値におけるピンとビットのマッピングは以下の通りです。

    ビット番号 7 6 5 4 3 2 1 0
    ピン番号 11 10 12 13 15 - - -
    名称 -Busy -Acknowledge (-ACK) Paper End (PE) Select (SLCT) -Error -IRQ Status 予約 予約
    ビット値が 1 の時の電圧 Low High High High High - - -

    PC オープン・アーキテクチャー推進協議会 のテクニカルリファレンスによると、ビット2 の -IRQ Status は 0 にセットされると割り込みが保留になる様です。また、コントロールポートのビット 4 を 1 として割り込みを有効にした場合ステータスポートのビット 6 が Low から High に変わる際に割り込みが発生しますが、この時にステータスポートのビット 2 -IRQ Status が 0 になる様です。1 にセットしないと以後の割り込みが保留されてしまいます。1 にセットするためにはステータスポートの読み出しを行う必要があります。

    ↑2008/08/11 各ビットの名称と割り込み保留について追記しました。割り込み保留について、木原さんどうもありがとうございました。

  • コントロールポート ( ピン 1、14、16 - 17 )

    出力専用の制御用ポートです。I/O アドレスは ( ベースアドレス + 2 ) です。 ここの 1 バイト値におけるピンとビットのマッピングは以下の通りです。

    ビット番号 7 6 5 4 3 2 1 0
    ピン番号 - - - - 17 16 14 1
    名称 予約 予約 Direction IRQ EN SLCT IN -INIT AUTO FD XT STROBE
    特記事項 - - - ここを 1 に設定すると、10 ピン(-ACK) が Low から High に変化した時に割り込みが発生する - - - -
    ビット値が 1 の時の電圧 - - - - Low High Low Low

    ↑2008/08/11 各ビットの名称と、特記事項について追記しました。

  • GND ( ピン 18 - 25 )

    GND に繋がれています。

なお、定格電圧 / 電流については、 PC オープン・アーキテクチャー推進協議会 のテクニカルリファレンスを引用すると

  • データポート / ステータスポート
    項目名
    Sink Current ( Maximum ) 12mA
    Source Current ( Maximum ) 2mA
    High-level Output Voltage ( Minimum ) 2.4V dc
    Low-level Output Voltage ( Maximum ) 0.5V dc

  • コントロールポート
    項目名
    Sink Current ( Maximum ) 10mA
    Source Current ( Maximum ) 0.2mA
    High-level Output Voltage ( Minimum ) 5.0V dc minus pullup
    Low-level Output Voltage ( Maximum ) 0.5V dc

となっているみたいです。Sink Current は、パラレルポートのピン電圧が Low で外部回路端子の電圧が High の場合にパラレルポートに流れ込む電流で、Source Current は逆にパラレルポートのピン電圧が High で外部回路端子の電圧が Low の場合にパラレルポートから流れ出す電流のことです。

○ 自作電子回路の紹介

本ページで使用する、自作電子回路の回路図および画像を以下に掲載します。

--- 回路図 ---

回路図

上の回路図は、 水魚堂の回路図エディタ を使って書いてみました。

プッシュボタンスイッチの接続については、チャタリング ( スイッチの ON / OFF 時に信号が安定せず、短時間に ON / OFF を繰り返した様な信号になってしまうこと ) 対策を考慮していないため、プッシュボタン操作時の動作は不安定かもしれませんが、御了承ください。

回路図中の抵抗値を選んだ理由について、簡単に書いておきます。R1 - R3 は、LED に流れる電流値を制限するために LED に直列に繋ぐための抵抗です。LED はある値以上の電流を流し続けると壊れてしまうため、抵抗を繋げることで強制的にそれ以下の電流にしてあげます。筆者が使った LED は最大定格電流が 30mA でしたので、余裕を見て 10mA 以下にすることにします。

LED が点灯すると 2V 前後電圧が降下するので、抵抗にかかる電圧は最大で約

  5V ( パラレルポートの High 電圧 ) - 2 = 3V

程度となるので、オームの法則から抵抗値は約

  R ( 抵抗 ) = E ( 電圧 ) / I ( 電流 ) = 3V / 0.01A = 300 Ω

となります。

。。。と LED だけ考えるとそうなるのですが、パラレルポートの定格を考えるとそうはいきません。前述の定格値によると、Source Current が 2mA となっています。上記の回路は、パラレルポートのピンが High の場合に LED に電流が流れる回路なので、この Source Current を守る必要があります。よって、電流の目標値を 2mA 以下に設定しなおします。

2mA という値ですが、PC オープン・アーキテクチャー推進協議会 のテクニカルリファレンスに従うと 2mA ですが、いろんな仕様によって値が違うみたいですし、実際のハードウェアによっても異なると思います。お使いのハードウェアの定格を調べて計算しなおすことをお薦めします。

  R ( 抵抗 ) = E ( 電圧 ) / I ( 電流 ) = 3V / 0.002A = 1.5KΩ

となるので、これより大きい抵抗であれば電流が 2mA 以下になることが分かります。で回路を考えた時にカタログを見てたら、2.2KΩ という抵抗値を発見したのでその値を採用しました。この場合の電流は約

  I ( 電流 ) = E ( 電圧 ) / R ( 抵抗 ) = 3V / 2.2KΩ = 1.4mA

となり、余裕があることが分かります。

ちなみに、LED は流れる電流によって明るさが変わります。ので、10mA と 1.4mA では結構明るさが異なり、暗くなってしまいます。筆者は最初何も考えずに 10mA 流れる様に回路を作って繋げたのですが、とても明るかったです。そのため、回路を作りなおして繋げた時は少し寂しかった。。。しかし、パラレルポートは壊れること無く無事に動いているみたいです^^;

次に R4 についてですが、この抵抗はプルアップ抵抗という役目を持っています。パラレルポートの 10 番ピン ( コネクタの 5 番ピン ) にプッシュボタンスイッチを接続しているのですが、それだけだとスイッチが OFF の場合に入力電圧が不安定になってしまいます ( どこにも繋がっていないから )。そのため、パラレルポートの5 番ピン ( コネクタの 4 番ピン ) を( 作成するプログラムによって ) 常時 High にしておきそこに繋げておくことで、確実にスイッチ OFF の場合は High、ON の場合は LOW とすることができます。この時、直結すると膨大な電流が流れるため、適当な抵抗を入れる必要があります。電圧を引き上げる抵抗という意味で、プルアップ抵抗と呼ばれます。

必要な抵抗値は

  R ( 抵抗 ) = E ( 電圧 ) / I ( 電流 ) = 5V / 0.002A = 2.5KΩ

以上となります。回路では 10KΩ を採用しましたが、電流が小さすぎるかもしれません。電流が小さすぎると、ノイズに弱かったり High を保つ為の電流に満たない可能性があります。もしかしたら 5KΩぐらいの方がいいかもしれませんが、筆者も自信があまりありません^^;ちなみに実験時は問題なく動いてました。

--- 電子回路 ---

電子回路

これが筆者が作成した電子回路の写真です。抵抗の数が多い ( R1 - R3 に 2 個ずつ使っている ) ですが、これは 1KΩ の抵抗を直列に繋いで無理矢理 2KΩ にしているからです。

--- LED 発光中 ---

LED 発光中

サンプルプログラムを使って、LED を点灯させた状態です。今回のサンプルプログラムでは、デバイスファイルに書き込みを行う度に一番上の LED が点灯 / 消灯を繰り返し、真中の LED が 1 秒毎に点灯 / 消灯を繰り返します。また、デバイスファイルから読み込みを行うと、その時のプッシュボタンスイッチの状態が分かります。

○ サンプルプログラム

パラレルポートに接続した自作電子回路を制御するためのサンプルプログラム hw_io.hhw_io.chw_main.c です。 はじめに のページから、 全てのサンプルプログラムを 固めた tar ボールをダウンロードすることもできます。

gcc -DMODULE -D__KERNEL__ -O -c hw_io.c
gcc -DMODULE -D__KERNEL__ -O -c hw_main.c
ld -r -o hw.o hw_main.o hw_io.o

としてコンパイル/リンクを行います。

insmod で正常に組み込まれると、真中の LED が 1 秒間隔で点灯 / 消灯を繰り返します。 /dev/chardev に echo 等で書き込みを行う ( 書き込む内容は何でも良い ) と、その度に点灯 / 消灯を交互に繰り返します。また、cat 等で dev/chardev から読み込みを行うと、プッシュボタンが押されている場合は

[Switch] pressed.

とコンソールに表示され、押されていない場合は

[Switch] NOT pressed.

と表示されます。

なお、insmod 時に

hw.o : init_module : Device or resource busy

の様に表示される場合は、parport ドライバ等によって既にパラレルポートが使用されています。その場合は、起動時に parport=0 というカーネルオプションを渡して parport ドライバを無効にしたりする等して、パラレルポートを未使用状態にしてから実行してください。

--- hw_io.h ---

    1    #ifndef ___hw_io_h_included_
    2    #define ___hw_io_h_included_
    3    
    4    
    5    #define IOPORT_BASE     0x378
    6    #define IOPORT_RANGE    3
    7    
    8    #define IOPORT_DATA     ( IOPORT_BASE + 0 )
    9    #define IOPORT_STATUS   ( IOPORT_BASE + 1 )
   10    #define IOPORT_CONTROL  ( IOPORT_BASE + 2 )
   11    
   12    #define DATA_LED_1      0
   13    #define DATA_LED_2      1
   14    #define DATA_LED_3      2
   15    #define DATA_PULLUP     3
   16    #define STATUS_ACK      6
   17    
   18    
   19    void
   20    io_write_or( unsigned short port, unsigned int value );
   21    
   22    void
   23    io_write_xor( unsigned short port, unsigned int value );
   24    
   25    void
   26    io_write_set( unsigned short port, unsigned int value );
   27    
   28    int
   29    io_is_button_pressed();
   30    
   31    
   32    #endif /* ___hw_io_h_included_ */
   33    
   34    /* End of hw_io.h */

--- hw_io.c ---

    1    #include <asm/bitops.h>      /* test_and_set_bit, clear_bit */
    2    #include <asm/io.h>          /* outb(), inb() */
    3    #include <asm/system.h>      /* rmb() */
    4    #include "hw_io.h"
    5    
    6    static int lock;
    7    
    8    
    9    /*
   10     * 現在の指定ポートの値と指定値を or した結果を
   11     * 指定ポートに書き込む
   12     */
   13    void
   14    io_write_or( unsigned short port, unsigned int value )
   15    {
   16        unsigned int data;
   17        
   18        if ( test_and_set_bit( 0, ( void* )( &lock ) ) != 0 )
   19            return;
   20    
   21        data  = inb( port );
   22        data |= value;
   23        outb( data, port );
   24    
   25        clear_bit( 0, ( void* )( &lock ) );
   26    }
   27    
   28    
   29    /*
   30     * 現在の指定ポートの値と指定値を xor した結果を
   31     * 指定ポートに書き込む ( 指定ビットの値反転 )
   32     */
   33    void
   34    io_write_xor( unsigned short port, unsigned int value )
   35    {
   36        unsigned int data;
   37    
   38        if ( test_and_set_bit( 0, ( void* )( &lock ) ) != 0 )
   39            return;
   40    
   41        data  = inb( port );
   42        data ^= value;
   43        outb( data, port );
   44    
   45        clear_bit( 0, ( void* )( &lock ) );
   46    }
   47    
   48    
   49    /*
   50     * 指定値を指定ポートに書き込む
   51     */
   52    void
   53    io_write_set( unsigned short port, unsigned int value )
   54    {
   55        if ( test_and_set_bit( 0, ( void* )( &lock ) ) != 0 )
   56            return;
   57    
   58        outb( value, port );
   59    
   60        clear_bit( 0, ( void* )( &lock ) );
   61    }
   62    
   63    
   64    /*
   65     * ステータスポートの値を読み込み、ACK ( 10 番ピン )
   66     * が LOW かどうか調べる。
   67     */
   68    int
   69    io_is_button_pressed()
   70    {
   71        unsigned int data;
   72    
   73        data = inb( IOPORT_STATUS );
   74        rmb();
   75
   76        if ( ! ( data & ( 1 << STATUS_ACK ) ) )
   77            return 1;
   78    
   79        return 0;
   80    }
   81    
   82    
   83    /* End of hw_io.c */

--- hw_main.c ---

    1    #include <asm/io.h>          /* outb(), inb() */
    2    #include <asm/uaccess.h>     /* copy_from_user, copy_to_user */
    3    #include <linux/fs.h>        /* inode, file, file_operations, ... */
    4    #include <linux/ioport.h>    /* check_region(), request_region(), release_region() */
    5    #include <linux/kernel.h>    /* printk() */
    6    #include <linux/module.h>
    7    #include <linux/sched.h>     /* task_struct */
    8    #include <linux/timer.h> 
    9    
   10    #include "hw_io.h"
   11    
   12    
   13    MODULE_LICENSE( "GPL" );
   14    
   15    
   16    static int   devmajor = 77;
   17    static char* devname  = "chardev";
   18    static char* msg      = "module [io.o]";
   19    
   20    
   21    /* 100 tick = 1 sec. */
   22    static int timeout = 100; 
   23    MODULE_PARM( timeout, "i" );
   24    
   25    static struct timer_list chardev_timer;
   26    static int timerstop = 0;
   27    
   28    static unsigned int initial_data;
   29    
   30    
   31    static void chardev_timer_handler( unsigned long data );
   32    static void chardev_del_timer();
   33    
   34    
   35    /*
   36     * ログ出力フォーマット用関数
   37     */
   38    static void 
   39    log_format( char* body )
   40    {
   41        if ( current != NULL ) {
   42            printk( KERN_INFO "%s : PID = %-5i cmdline = \"%-10s\" : %s\n", 
   43                msg, current->pid, current->comm, body );
   44        }
   45        else {
   46            printk( KERN_INFO "%s : %s\n", msg, body );
   47        }
   48    }
   49    
   50    
   51    /* 
   52     * タイマー登録処理 
   53     */
   54    static void 
   55    chardev_timer_register( void )
   56    {
   57        init_timer( &chardev_timer );
   58    
   59        chardev_timer.expires  = jiffies + timeout;
   60        chardev_timer.data     = ( unsigned long )jiffies;
   61        chardev_timer.function = chardev_timer_handler;
   62    
   63        add_timer( &chardev_timer );
   64    }
   65    
   66    
   67    /*
   68     * タイマー削除関数
   69     * タイマ停止フラグを設定し、del_timer_sync で既存のタイマー関数を
   70     * 削除する(実行中の場合は終了するまで待つ)。
   71     */
   72    static void
   73    chardev_del_timer()
   74    {
   75        timerstop = 1;
   76        del_timer_sync( &chardev_timer );
   77    }
   78    
   79    
   80    /* 
   81     * タイムアウト処理 
   82     */
   83    static void 
   84    chardev_timer_handler( unsigned long data )
   85    {
   86        io_write_xor( IOPORT_DATA, ( 1 << DATA_LED_2 ) );
   87    
   88        if ( timerstop ) {
   89            printk( KERN_INFO "%s : Timer is not to be restarted.\n", msg );
   90            return;
   91        }
   92    
   93        chardev_timer_register();
   94    }
   95    
   96    
   97    /*
   98     * open()
   99     */
  100    static int 
  101    chardev_open( struct inode* inode, struct file* filp )
  102    {
  103        log_format( "open()  called" );
  104        return 0;
  105    }
  106    
  107    
  108    /*
  109     * release()
  110     */
  111    static int 
  112    chardev_release( struct inode* inode, struct file* filp )
  113    {
  114        log_format( "close() called" );
  115        return 0;
  116    }
  117    
  118    
  119    /*
  120     * read()
  121     */
  122    static ssize_t 
  123    chardev_read( struct file* filp, char* buf, size_t count, loff_t* pos )
  124    {
  125        char status[32];
  126        int copy_len = 0;
  127        int len = 0;
  128    
  129        log_format( "read()  called" );
  130    
  131        if ( *pos == 0 ) {
  132            if ( io_is_button_pressed() )
  133                len = sprintf( status, "[Switch] pressed.\n" );
  134            else
  135                len = sprintf( status, "[Switch] NOT pressed.\n" );
  136        }
  137    
  138        copy_len = ( len > count ) ? count : len;
  139    
  140        if ( copy_to_user( buf, status, copy_len ) ) {
  141            log_format( "copy_to_user failed\n" );
  142            return -EFAULT;
  143        }
  144    
  145        *pos += copy_len;
  146    
  147        return copy_len;
  148    }
  149    
  150    
  151    /*
  152     * write()
  153     */
  154    static ssize_t 
  155    chardev_write( struct file* filp, const char* buf, size_t count, loff_t* pos )
  156    {
  157        log_format( "write() called" );
  158        io_write_xor( IOPORT_DATA, ( 1 << DATA_LED_1 ) );
  159        return count;
  160    }
  161    
  162    
  163    static struct file_operations chardev_fops = 
  164    {
  165        owner   : THIS_MODULE,
  166        read    : chardev_read,
  167        write   : chardev_write,
  168        open    : chardev_open,
  169        release : chardev_release,
  170    };
  171    
  172    
  173    /*
  174     * モジュールの初期処理
  175     * insmod 時に呼ばれる
  176     */
  177    int 
  178    init_module( void )
  179    {
  180        EXPORT_NO_SYMBOLS;
  181    
  182        if ( register_chrdev( devmajor, devname, &chardev_fops ) ) {
  183            log_format( "register_chrdev failed" );
  184            return -EBUSY;
  185        }
  186    
  187    
  188        if ( request_region( IOPORT_BASE, IOPORT_RANGE, devname ) == NULL ) {
  189            log_format( "request_region returned NULL" );
  190            unregister_chrdev( devmajor, devname );
  191            return -EBUSY;
  192        }
  193    
  194        initial_data = inb( IOPORT_DATA );
  195        io_write_or( IOPORT_DATA, ( 1 << DATA_PULLUP ) );
  196        chardev_timer_register();
  197    
  198        log_format( "loaded  into kernel" );
  199        return 0;
  200    }
  201    
  202    
  203    /*
  204     * モジュールの解放処理
  205     * モジュールの参照数が 0 であれば、rmmod 時に呼ばれる
  206     */
  207    void 
  208    cleanup_module( void )
  209    {
  210        chardev_del_timer();
  211        io_write_set( IOPORT_DATA, initial_data );
  212    
  213        release_region( IOPORT_BASE, IOPORT_RANGE );
  214    
  215        if ( unregister_chrdev( devmajor, devname ) ) {
  216            log_format( "unregister_chrdev failed" );
  217            /* 対策無し ... */
  218        }
  219    
  220        log_format( "removed from kernel" );
  221    }
  222    
  223    
  224    /* End of hw_main.c */

○ サンプルプログラムの説明

では、サンプルプログラムについて説明していきます。


--- hw_io.h ---

  • 5 〜 6 行目

    パラレルポートのベースアドレスと I/O アドレス空間の大きさをマクロ 定義しています。前述の様に、パラレルポートのベースアドレスの値は お使いの環境によって異なりますので、環境に合わせて書き換えて下さい。

  • 8 〜 10 行目

    データポート、ステータスポート、コントロールポートそれぞれの先頭 アドレスをマクロ定義しています。

  • 12 〜 16 行目

    サンプルプログラムで使用する入出力ピンの、各ポート値におけるビット 番号をマクロ定義しています。DATA_LED1 は回路図中の一番上の LED、 DATA_LED2 は真中の LED、DATA_LED_3 は一番下の LED を表しています。 また、DATA_PULLUP はプルアップ抵抗接続先のピンを表しており、STATUS_ACK はプッシュボタンの接続先ピンを表しています。

    例えば、DATA_LED1 はデータポートの 0 ビット ( パラレルポートの 2 番ピン ) に接続しているので 0 と定義しています。また、プッシュボタンの接続先は ステータスポートの 6 ビット ( パラレルポートの 10 番ピン ) に接続しているので、 STATUS_ACK を 6 と定義しています。

  • 19 〜 29 行目

    hw_io.c で定義している関数のプロトタイプ宣言です。


--- hw_io.c ---

  • 1 〜 4 行目

    必要なヘッダファイルをインクルードしています。asm/io.h は、I/O ポートとの入出力を行う為に必要な関数のプロトタイプやマクロが定義 されています。asm/bitops.h は、割り込まれずにビット操作を 行う為の関数プロトタイプが定義されています ( ロック処理に使用 )。 また、asm/system.h にはメモリバリア関数 ( 後述 ) が定義されています。

  • 6 行目

    hw_io.c 内で使用するロック変数を定義しています。I/O ポートとの 入出力を行う際に、一度データを読み込んでその値を加工してから書き出す という処理を行っているのですが、その際他のプロセスやタイムアウト ハンドラに割り込まれると都合が悪いので、まずロックを取得してから 処理を行う様にしています。このロック取得のために使う変数です。

  • 9 〜 26 行目 ( io_write_or )

    実行時の指定ポートの値を読み込み、その値と引数のビット or を 計算して、結果を指定ポートに書き込む関数です。1 を設定したいピン に対応したビットを 1 としたマスクを指定することで、指定したピンに 1 を書き込みその他のピンは現状のままにしておきたい場合に使用します。

    test_and_set_bit でロックを取得し、I/O 操作を行った後に clear_bit でロックを解放するという実装になっています。プロセス間の排他のみであれば spin_lock を使う所なのですが、プロセスとは無関係に実行されるタイムアウトハンドラ 内での使用を考えると、1 CPU システムでは spin_lock が無意味であること/ プロセスコンテキストとタイムアウトハンドラ間でデッドロックが発生する 可能性があることから止めました。

    なお、タイムアウトハンドラ内でロックが取得できるまで待つわけにもいかないので、 取得できなかったらその時は諦めて終了するという実装にしました。 きっちり作るのであれば、タイムアウトハンドラとの競合が起きない様な設計にした上で 他の箇所を spin_lock で排他処理するのが良いのかな、と考えている所です。

    test_and_set_bit および clear_bit は、途中で割り込まれることが無い ビット操作用の関数で、引数の型がプラットフォーム依存となります。 ここでは、いくつかのビット操作用の関数の i386 のプロトタイプを示します。

    #include <asm/bitops.h>
    void set_bit( int nr, void* addr );
    void clear_bit( int nr, void* addr );
    int test_and_set_bit( int nr, void* addr );
    int test_and_clear_bit( int nr, void* addr );
    
    

    addr に操作対象データ領域へのポインタを指定し、nr に操作するビット番号を指定します。 各関数の内容は以下の通りです。

    関数名 説明
    set_bit 指定ビットを 1 にする
    clear_bit 指定ビットを 0 にする
    test_and_set_bit 指定ビットを 1 にした上で、以前の値を返す
    test_and_clear_bit 指定ビットを 0 にした上で、以前の値を返す

    I/O ポートとの入出力には、asm/io.h で定義されている関数群 inx / outx を使います。 この関数群はプラットフォーム依存になってしまうため、 プラットフォーム毎に返り値の型や引数の型が異なる場合があります。 ここでは i386 でのプロトタイプを示します。 なお、hw_io.c 内の関数についても、port については i386 の型に合わせています。

    #include <asm/io.h>
    unsigned char inb( unsigned short port );
    void outb( unsigned char data, unsigned short port );
    unsigned short inw( unsigned short port );
    void outw( unsigned short data, unsigned short port );
    unsigned int inl( unsigned short port );
    void outl( unsigned int data, unsigned short port );
    
    

    port に I/O アドレスを指定して inx を実行すると、そのアドレスにマッピング されている範囲のデータが b/w/l に対応した幅で返って来ます。また、 port に I/O アドレスを指定し data に書き込みたい幅のデータを指定して outx を実行すると、そのアドレスに対して指定したデータに対応する電圧が出力されます。

    各関数と扱うデータ幅の関係は以下の通りです。

    関数名 データ幅
    inb / outb 8 ビット
    inw / outw 16 ビット
    inl / outl 32 ビット

  • 29 〜 46 行目 ( io_write_xor )

    実行時の指定ポートの値と、指定値のビット xor を行う点のみ、 io_write_or と異なります。xor を行うことで、引数に指定したマスク中の 1 が設定されているビットに対応するピンのみ、値が反転されます。

  • 49 〜 61 行目 ( io_write_set )

    実行時の指定ポートの値を無視して、引数に指定した通りにピンの値を設定する関数です。

  • 64 〜 80 行目 ( io_is_button_pressed )

    実行時のステータスポートの値を読み込み、6 番ビット ( パラレルポートの 10 番ピン ) の値を返します。パラレルポートの 10 番ピンは 1 の場合に High で 0 の場合に Low となりますので、0 であればプッシュボタンが 押されており、1 の場合はプッシュボタンが押されていないことになります。

    74 行目で rmb() という関数を実行していますが、これは読み取り用の メモリバリア関数です。メモリバリアについては メモリバリア でもう少し詳しく書きますが、ここではステータスポート値の読み込みを 確実に完了させているということだけ記憶しておいてください。


--- hw_main.c ---

  • 1 〜 10 行目

    必要なヘッダファイルをインクルードしています。

  • 21 〜 23 行目

    真中の LED を点滅させるためのタイマー間隔の初期値を、1 秒としています。

  • 28 行目

    モジュール組み込み時のデータポートの値を保存するための変数を宣言しています。 サンプルプログラムではデータポートへの書き込みを行います。そのため、init_module においてデータポートの値を保存しておき、cleanup_module でその値をデータポート に書き戻すことで、初期状態に戻しています。

  • 80 〜 94 行目 ( chardev_timer_handler )

    タイムアウトハンドラでは io_write_xor( IOPORT_DATA, ( 1 << DATA_LED_2 ) ) を実行することで、DATA_LED_2 ( 真中の LED ) のピン値を反転しています。 タイマーは初期値の場合 1 秒間隔で実行されますので、1 秒毎に真中の LED が点灯 / 消灯を繰り返すことになります。

  • 119 〜 148 行目 ( chardev_read )

    131 〜 136 行目において、io_is_button_pressed を実行してプッシュボタン の状態を読み込み、状態に応じてユーザプロセスに返す文字列をバッファに 格納しています。if ( *pos == 0 ) の判定を行っているのは、*pos が 0 の場合、 つまり連続した read の最初の実行時のみデータを返す様にするためです。 この判定をせずに毎回文字列を返してしまうと、ユーザプロセスが EOF を判定 できないために延々と read を実行することになってしまいます。

  • 151 〜 160 行目 ( chardev_write )

    io_write_xor( IOPORT_DATA, ( 1 << DATA_LED_1 ) ) を実行することで、 DATA_LED_1 ( 一番上の LED ) のピン値を反転しています。 つまり、write を実行する度に一番上の LED が点灯 / 消灯を繰り返すことになります。 なお、書き込まれたデータは無視しているため、何を書き込んでも構いません。

  • 173 〜 200 行目 ( init_module )

    今回は複数のファイルでカーネルモジュールを作成しているため、180 行目で EXPORT_NO_SYMBOLS マクロを実行しています。EXPORT_NO_SYMBOLS マクロについては LECTURE 1 カーネルモジュールとしてデバイスドライバを作る を参照してください。

    カーネル内から I/O リソースを使用する場合は、request_region を使って 使用権を取得しなければいけません。

    #include <linux/ioport.h>
    struct resource* request_region( unsigned long start, unsigned long len, char* name );
    
    

    188 〜 192 行目で request_region を実行し、IOPORT_BASE から始まる IOPORT_RANGE バイトのアドレス空間の使用を予約しています。使用権の取得に失敗した場合は NULL が返って来るため、その時はキャラクタデバイス登録を削除して終了しています。

    194 行目でデータポートの値を保存しています。 その後、io_write_or( IOPORT_DATA, ( 1 << DATA_PULLUP ) ) を実行することで、 プッシュボタンの接続先のピンを High にしています。

  • 203 〜 221 行目

    タイマーを停止した後、io_write_set を実行してデータポートを 初期状態に戻しています。その後、release_region を実行して I/O リソース を解放しています。

    #include <linux/ioport.h>
    void release_region( unsigned long start, unsigned long len );
    
    

    I/O リソースの使用が終わったら、release_region を使って必ず解放してください。

以上で hw_io.h、hw_io.c、hw_main.c の解説は終わりです。環境がある方は、ぜひ回路作って繋げて動かしてみて下さい!ソフトウェアだけで終わるプログラミングとはまた違った面白さが得られると思いますよー。

○ メモリバリア

プログラム実行時には CPU とメモリあるいは外部 I/O 機器とのやりとりが発生しますが、コンパイラによる最適化や CPU による最適化等によって、これらの命令順序が意図せず変更されてしまうことがあります。

例えば互いに干渉しないとコンパイラや CPU が判断した読み書き命令列について、一気にキャッシュメモリに読み込みを行った上で書き込み命令を実行する、等です( この間、予測に基づいて予め別のメモリ内容までも読み込んだりする場合もあります )。

しかし、別プロセスとの待ち合わせ等を行う必要があったり、I/O 機器に対して書き込みを完了した上で読み込まなければ意味が無いピン状態を読んだりする場合には、コンパイラや CPU では判断を間違えてしまう可能性があります。

この様な場合に、命令の実行順を保証しようとするためにメモリバリアがあります。メモリバリアを置くことで、コンパイラや CPU に対してその時点までのメモリアクセスや I/O アクセスを完了させることができます。

メモリバリアには、コンパイラに対して指示するためのものと、ハードウェアに対して指示するものがあります。

--- コンパイラに対するメモリバリア ---

#include <linux/kernel.h>
void barrier( void );

--- ハードウェアに対するメモリバリア ---

#include <asm/system.h>
void rmb( void );
void wmb( void );
void mb( void );

rmb は読み出し順序に関するメモリバリアで、wmb は書き込み順序に関するメモリバリアです。mb は読み書き両方に対するメモリバリアとなります。

なお、スピンロックやアトミック操作 ( 割り込まれることが無く操作を完了できる処理、サンプルプログラムでいうと test_and_set_bit / clear_bit 等 ) はメモリバリアと同様の働きをする様です( この辺りを詳しく知りたい方は、それらの関数のインラインアセンブラによる実装と CPU のマニュアルを見比べながら調べてみて下さい。筆者も Intel Processor について調べておおまかには分かりましたが、実装できる程には分かってないので書けません m(__)m )。

サンプルプログラムの場合、hw_io.c のほとんどの関数において、I/O 操作がアトミック操作の間にあります。よって、clear_bit より前の操作は関数終了時には完了していることが保証されています。内部の inb / outb 順序については、読み込んだ値を加工して書き込んでいるため間違えないだろう。と思って何もしてません。。。

なお、io_is_button_pressed() において rmb() を使っていますが、このアドレスに対しては io_is_button_pressed において同じ変数への読み込みしか行っていないため、判断を誤って並び替え等が行われない様にと念を押してる感じです。どう並び替えられるのか見当が付かないので、一応入れてみています。