Raspberry Pi(ラズペリーパイ)を使い、組み込みソフトウェアを理解する入門シリーズ第4回。
ラズパイにLEDを繋ぎ、C言語で書いたプログラムでLEDを点滅させる、通称「Lチカ」を実現してみました。
今回は、いよいよ組み込みソフトウェアの肝となるデバイス制御部の作り方についての説明です。
タイトルで「誰でもわかる」と謳っていますが、第1回から順に読んで頂く事が前提となります。必ず仕組みがわかるようになります。
www.initial-jj.com
www.initial-jj.com
www.initial-jj.com
前回はC言語のソースコードを公開しました。今回はそのコードの解説を行います。今回の解説で、どのようにハードウェア制御してるのかが理解できるようになります。
組み込みソフトウェア開発に興味のある方は、ラズパイを手にして遊んでみて下さい。
ソースコード
/* file name : main.c */ #include <bcm_host.h> /* required to use bcm_host_get_peripheral_address() */ #include <fcntl.h> /* required to use open(), close(), usleep() */ #include <sys/mman.h> /* required to use mmap(), munmap() */ #define BLOCK_SIZE (4096) /* マッピングするメモリサイズ(4KByte) */ #define GPIO_OFFSET (0x00200000) /* see p.91 of dataseet */ #define GPFSEL0 (0x00) /* GPFSEL0 Address下位1byte */ #define GPSET0 (0x1c) /* GPFSET0 Address下位1byte */ #define GPCLR0 (0x28) /* GPFCLR0 Address下位1byte */ int main( void ) { volatile unsigned int *gpio; void *map; int fd; int adr_gpio_base; int i = 0; /*==========================================================*/ /* GPIO制御レジスタ・アドレスを取得し、map変数に代入する */ /*==========================================================*/ fd = open( "/dev/mem", (O_RDWR | O_SYNC) ); /* open()エラー処理割愛 */ ; /*------------------------------------------------------------------------------------------*/ /* refer to */ /* https://www.raspberrypi.org/documentation/hardware/raspberrypi/peripheral_addresses.md*/ /* */ /* ペリフェラル物理アドレスがラズパイ世代で異なる為、互換性を持たせる為に */ /* bcm_host_get_peripheral_address()でプログラム実行マシンの上の物理アドレスを取得する */ /*------------------------------------------------------------------------------------------*/ adr_gpio_base = bcm_host_get_peripheral_address(); /* mapにペリフェラル物理アドレスをマッピング */ map = mmap( NULL, BLOCK_SIZE, PROT_WRITE, MAP_SHARED, fd, (adr_gpio_base + GPIO_OFFSET) ); /* mmap()エラー処理割愛 */ ; close( fd ); /*==========================================================*/ /* GPIO 2pin 初期設定 */ /*==========================================================*/ gpio = (unsigned int *)map; gpio[GPFSEL0/4] |= 0x00000040; gpio[GPCLR0/4] |= 0x00000002; usleep( 500000 ); /*==========================================================*/ /* LED点滅処理 */ /*==========================================================*/ while (i < 5 ){ gpio[GPSET0/4] |= 0x00000002; usleep( 250000 ); gpio[GPCLR0/4] |= 0x00000002; usleep( 250000 ); i++; } gpio[GPFSEL0/4] &= 0xfffffffb; /* INPUTに戻す(無くても良い処理) */ munmap( map, BLOCK_SIZE ); return 0; }
プログラムの実行方法
$ gcc main.c -I/opt/vc/include -L/opt/vc/lib -lbcm_host $ sudo ./a.out
コンピューターのデータ(メモリ)の単位について
ハードウェア仕様書を読むにあたり、最低限知っておくべき知識があります。その1つが、コンピューターで扱う数の単位です。
最小単位とn進法
メモリの最小単位は「bit(ビット)」と言います。デジタルですから「0」か「1」のどちらかの値を取ります。0と1の2値で数値表現する方法を2進法と言います。
日常生活で使っているのは10進数と言います。
10進数は0~9までの10個の数字を使い、+1してこの10個で表現できない値となる時に桁が増えて「10」と表現します。
2進数は0~1の2個の数字を使い、+1してこの2個で表現できない値となる時に桁が増えて「10」と表現します。
他に、16進数も使います。これは0~Fの16個の文字を使います。
10進数を、2進数と16進数に変換した対応表を書いたので参考にして下さい。
数値が16進数である場合、それ明示する為に、先頭に”0x”と書いたり末尾に”H”と書きます。
データ(メモリ)の単位
最小単位はbitですが、長さ、重さの単位と同じく値が多くなると別の単位を使います。
8Bitを1Byte(バイト)と表現します。
1,024Byteは1K(キロ)Byteになります。1KBと書く事もあります。
一般的にキロはx1,000倍を表す単位ですが、コンピューターではx1,024倍なので間違わないようにしましょう。
GPIOの仕様を読み解く
ペリフェラルの配置アドレスを調べる
ARMのGPIO制御仕様(データシート)はラズパイ公式サイトから入手できます。
https://www.raspberrypi.org/documentation/hardware/raspberrypi/bcm2835/BCM2835-ARM-Peripherals.pdf
(Raspberry Pi Documentation より)
pdfを別ウィンドウで開いて、データシートを見ながら説明をご覧頂くと、わかりやすいと思います。むしろ、データシートはしっかりと読まないといけません。
p.89からGPIOの説明が書かれていますが、まずはp.5のPeripherals(ペリフェラル)を見てみましょう。
ペリフェラルとは周辺機器(周辺機能) の事で、ARMに内蔵されているハードウェア機能(通信、GPIO他)の事を表しています。
p.5にはメモリ空間のどこにI/O Peripheralsが割り当てられているかが書かれています。このような図をメモリ・マップと言います。
これにより、Physical Addressは20000000H から配置されている事がわかります。
しかし、Linux OSは仮想アドレス空間でソフトウェアを動作させる仕組みを持っており、物理アドレスを直接アクセスする事はできません。
メモリ保護機能が無いシステムであれば、メモリ・マップに書かれているアドレスに直接アクセスが可能で、ハードウェア制御ができます。
では、どうすれば良いか?
答えは、https://www.raspberrypi.org の一番下の”Documentation”を辿っていくと、https://www.raspberrypi.org/documentation/hardware/raspberrypi/peripheral_addresses.md という情報を得られます。
Pi Zero、Pi Zero Wと第1世代のラズパイは0x20000000にマッピングされていますが、Pi 2、Pi 3では0x3F000000にマッピングされていると書かれています。
また、実際にマッピングされているアドレスを取得する関数が用意されており、それがbcm_host_get_peripheral_address() だと言う事もわかります。
今回書いたソースコード(34行目)は、全世代のラズパイで動くように、ペリフェラルのマッピング・アドレスをこの関数で取得しています。この関数を処理し終わった時に得られる値(戻り値と言う)に、実際に割り当てられているアドレスが入っています。
ソースコードでは、adr_gpio_base という変数にアドレスを格納しています。
なお、このページの最後には、C言語でbcm_hostライブラリを組み込む時のコンパイル・オプション設定も書かれていますので、どうやってbcm_hostをリンクすれば良いか?という疑問も解決します。
GPIO仕様を理解する
次に、p.89のGPIO仕様を読み解きます。
p.90から制御レジスタ一覧表が記載されています。
レジスタとは、メモリの1種で、CPU内に存在しており、ここに1や0を書き込む事で、決められた動作をします。
レジスタは、演算途中の値を保持する汎用レジスタと呼ばれるもの等色々ありますが、その詳細は割愛します。
「Address」は無視です。上述の通り、プログラムがアクセスするアドレスは別アドレスにマッピングされているからです。
但し、今回使うPi 2では0x3F000000からマッピングされていますが、0x7E200000番地に配置されているレジスタを制御する場合は、0x3F000000をベースアドレスとして、+200000番地のオフセットが必要になります。即ち、0x3F200000番地から任意のアドレスにアクセスする事になります。
「Field Name」はレジスタ名称が書かれています。そのレジスタが持っている機能を略して名前にするのが一般的です。
「Size」はレジスタのサイズです。単位が書かれていませんが、bitです。
これは、ページ先頭の「All accesses are assumed to be 32-bit.」という説明文で明示されています。
話が戻りますが、32bitという事は別の表現で4Byteと言っても同じです。(32bit = 8bit(= 1Byte) x 4)
ここで1番大事なのは、レジスタのサイズを把握しておく事です。4Byteレジスタである事を忘れないで下さい。
もう1つ疑問があると思います。「どのレジスタを制御すれば、やりたい制御ができるのか?」です。
残念ながら、これはレジスタの説明を読んで理解するしかありません。
今回はGPIO 2番ピンを使っているので、該当するレジスタは
- GPFSEL0:0xXXXXXX00
- GPSET0:0xXXXXXX1C
- GPCLR0:0xXXXXXX28
です。データシートの該当レジスタの説明を読んでみて下さい。
ソースコードの説明
1~3行目(#include)
#include で使用するライブラリを「使いますよ」と宣言しています。
どの関数を使用する為にincludeしているかは、コード内のコメントを参照して下さい。
C言語では、/* */に挟まれた文字列はコメントとして扱われ、プログラム実行に影響を与えません。
5~9行目(#define)
数字や計算式を文字列に置換する役割があります。コースコード内のあちこちに数字を書くと、
- 可読性が低下
- 保守性が低下
と良いことは1つもないので、意味のある文字列に置換して使います。
12~73行目(main()関数)
処理全体で、main関数と呼びます。
関数名を”main”とすると、その関数が最初に実行されます。
これはシステムによって異なります。
システムエンジニアがソフトウェア構成を検討する際に違った名前をエントリー・ポイントにする事もありますが、gccでLinuxソフトを書く場合はmain関数だと憶えておけば良いです。
23~42行目(open()関数)
GPIO制御レジスタへアクセスする為の細工です。
上述の通り、レジスタが配置されたアドレスにアクセスする事が禁じられているシステム上で動かすプログラムなので、間接的にレジスタへアクセスできるようにしています。
具体的には、open()関数で”/dev/mem”をオープンし、mmap()関数で実際に割り当てられているペリフェラルアドレス空間とリンクさせています。
これで、map変数をリード/ライトすれば、実際のペリフェラルを制御できるようになります。
なお、open()やmmap()関数の実行が失敗(エラー)が発生した場合に、どう振る舞うかを決めて実装しなければなりませんが、今回は必要最低限のソースコードとした為、処理を割愛しています。
市場に出回っている製品に実装されているプログラムは、このようなエラー発生時の処理が多量に入っており、頭を悩ますポイントです。
51行目(GPFSEL0)
2番ピンを出力設定にしています。
データシートのp.91~92に「Table 6-2-GPIO Alternate function select register 0」という表があり、このFSEL2の値を変更しています。
FSEL9に説明が書かれていますが、FSEL2も同じです。
このレジスタは上位2bit以外、全て3bitずつGPIOの制御をするレジスタになっています。
FSEL2は8-6bit目に配置されており、この3bitを”001b”にすれば、「GPIO Pin 2 is an output」となる事がわかります。
なぜ0x00000040を代入しているのか、わかりやすくする為に表を書いたので参考にして下さい。
もう1つ重要なポイントがあります。それはmap変数のアクセス方法です。
C言語を知っている方前提の話となりますが、map変数はポインタ型で宣言しています。
void型にしていますが、このレジスタは「32bitアクセスするよう」説明書きがあったので、int型(4Byte単位アクセス)である必要があります。
このmapポインタを配列として使っているので、配列の添字が+1するだけで、アクセスするアドレスは+4になります。
しかし、GPIOレジスタの#define定義は可読性重視の為、実際のアドレス下位1byteとしています。
このdefine定義を添え字として使用しているので、アドレッシングは狙うアドレスの x4倍になります。
その為、添字内で /4 する事で辻褄を合わせています。
他に良い実装方法を思いついていましたが、今回は読みやすさ重視で、このような実装としました。
53行目(GPCLR0)
まず、GPIO 2pinを出力LOレベルにしてLEDを消灯させています。
54行目(usleep())
システムコールを使用しました。
引数の単位は1usで、与えられた時間sleep(=処理を止める)させています。
59行目(while)
{}内の処理を5回繰り返します。
60行目(GPSET0)
GPIO 2pinを出力HIレベルにして、LEDを点灯させています。
61行目(usleep)
250ms、LED点灯状態保持
63行目(GPCLR0)
GPIO 2pinを出力LOレベルにして、LEDを消灯させています。
64行目(usleep)
250ms、LED消灯状態保持
以上が、ソースコードの解説となります。
ソースコードの左に行数を付記したかったのですが、CSSを大幅に改修する必要があったので、付記できませんでした。
その内リライトしたいと思います。
なお、WiringPiというライブラリを導入すれば、python同様にお手軽にやりたい事を実現できます。
ですが、今回の趣旨は「どうやってデバイス制御をしているか?」の手引き書ですので、極力ライブラリを使わずにソースコードを書きました。
組み込みソフトウェアを作るのは、それなりの見識が無いと難しいとは思いますが、この分野に挑戦してみたいという方が増えるきっかけになれば幸いです。
最終的には「自分で調べる努力と熱意」が必要となりますが、今はWebサイトを使えば英文も簡単に翻訳できるので敷居は低くなっていると思います。是非、自分の手で挑戦してみて下さい。
ご覧いただき、ありがとうございました。
ランキングに参加しています。ポチッと押して去って頂けると、次回に続く。
コメント
+200000の正体が分からず1時間調べてここに辿り着きました。
謎が解けました
ありがとうございます。
toppo さん
ご訪問頂きありがとうございました。
お役に立てて光栄です!