General Purpose I/O。RasPiに限らず、最近のマイコンは機能を減らすことなくピンの数を抑える工夫がある。
それ以外にも、チップの外につけるパーツを減らせるようになっている。
参照したドキュメントはSoC-Peripherals.pdfで、6節にGPIOの説明がある。
GPIOなソースコードはこちら: https://github.com/mgwsuzuki/v6pi/tree/master/base
ピンの機能設定
SoCに内蔵される機能は多いほど応用が広がるわけだが、それはつまり入力または出力のピン数の増加を意味する。
とは言っても、チップの外に出せるピンの数は有限であり、またユーザがすべての機能を使うわけでもないので、機能ごとに専用のピンを割り当てるのは非効率である。
そんなわけでRasPiでは、1つのピンに対して複数の機能をマルチプレクスし、ユーザにそれを選択してもらうようになっている。
vp_gpio.hでは以下のようなコードでピンの機能選択ができる。
// function select
typedef enum {
VP_GPIO_FSEL_INPUT = 0,
VP_GPIO_FSEL_OUTPUT = 1,
VP_GPIO_FSEL_ALT0 = 4,
VP_GPIO_FSEL_ALT1 = 5,
VP_GPIO_FSEL_ALT2 = 6,
VP_GPIO_FSEL_ALT3 = 7,
VP_GPIO_FSEL_ALT4 = 3,
VP_GPIO_FSEL_ALT5 = 2
} vp_gpio_fsel_t;
// 指定された番号のGPIOピンのfunctionをセットする
void vp_gpio_set_fsel(i32_t portnum, vp_gpio_fsel_t fsel);
vp_gpio_fsel_tの意味はSoC-Peripherals.pdfをみれば分かると思う。
出力ピンのレベル指定
GPIOを出力ピンとしたとき、出力したい値を設定するために以下の2つの関数を用意した。
// 指定された番号のGPIOピンの出力を1にする
void vp_gpio_set_output(i32_t portnum);
// 指定された番号のGPIOピンの出力を0にする
void vp_gpio_clear_output(i32_t portnum);
これはGPSETnまたはGPCLRnレジスタを叩くだけの関数である。
上記のように、RasPiではピンの出力を1にするか0にするかの2つしか持っていないようだが、マイコンによっては設定したい値そのものを指定できるレジスタや、ピン出力を反転するためのレジスタを持つものもある。
特に設定したい値そのものを指定できるレジスタは必須と思っていたけど、RasPiはそうではないようだ。ということで、それが出来る関数を用意してみた。
// 指定された番号のGPIOピンのレベルをセットする
void vp_gpio_set_level(i32_t portnum, u32_t level);
入力ピンの状態を知る
GPIOを入力ピンとしたとき、ピンの状態を知るために以下の関数を用意した。
// 指定された番号のGPIOピンのレベルを返す
u32_t vp_gpio_get_level(i32_t portnum);
指定されたピンがHなら1、Lなら0を返す。
プルアップとプルダウン
RasPiでは、各ピンごとにプルアップ、プルダウン、プルアップ/ダウンなしを指定できる。ただ、その指定方法は変わっていて、
- GPPUDにモードを指定し
- 150clk待って
- GPPUDCLKnの指定したいピンに対応するビットに1を書き込み
- 150clk待って
- GPPUDCLKnの指定したいピンに対応するビットに0を書き込む
という手順になっている。一見複雑に見えるが、LSI設計している人間にとってその狙いと回路構成が目に浮かぶのである。
150clk待つというのは正確には150clk以上待てばいいはずで、実装もそうしている。
ここで忘れていけないのは、最後のステップのGPPUDCLKnに0を書き込むところである。
GPPUDに書き込んだ値を記憶させるのに重要なことは、GPPUDCLKnを0から1に変化させることである。いわゆる立ち上がりエッジと言われるものだが、これを発生させるためにはGPPUDCLKnに1を書き込む前にその値が0になっていないといけない。
もし0を書き込むのを忘れて、別のピンのプルアップ/ダウンを設定しようとして1を書き込んでも、1から1への変化(というか変化していない)じゃ立ち上がりエッジは発生しない。
その結果、正しく設定できないということになる。
vp_gpio.hでは以下のコード及び関数でプルアップ/プルダウンを指定できる。
// pull-up/down
typedef enum {
VP_GPIO_PUD_OFF = 0,
VP_GPIO_PUD_DOWN = 1,
VP_GPIO_PUD_UP = 2
} vp_gpio_pud_t;
// 指定された番号のGPIOピンのpull-up/downをセットする
void vp_gpio_set_pud(i32_t portnum, vp_gpio_pud_t pud);
プルアップ/プルダウンの使い分け
そもそもプルアップとかプルダウンってなんのために使うのか、というのを理解しないといけない。
よくデジタル回路では0を示すのに0V、1を示すのに3.3Vとか2.5Vの電圧をかける、みたいに言われる。
がしかし、デジタル回路といってももとはアナログな振る舞いに支配されるわけで、そこには0でも1でもない状態が存在する。
1つの信号線を誰かが0Vや3.3Vに駆動していればいいのだが、実は誰も駆動していない、という状態が普通にある。例えば、USBなどのコネクタに何も挿さっていない場合を考えてみるとよい。
そのような、だれも駆動していない信号を入力したら、それは0になるのか、1になるのか?
答えは「分からない」とか「0や1にふらつく」である。
この「ふらつく」というのがかなり厄介で、例えばUARTなら1->0の変化でデータが到着したと判断するので、ふらつきはすなわち誤受信になる。割り込み信号ならふらつくたびに意図しない割り込みが発生する。
これは困るので、それを回避するためにプルアップ/プルダウンをする。
プルアップにしたら、誰も信号を駆動していないときに1になり、プルダウンにしたら、誰も信号を駆動していないときに0になる。もし誰かが信号を駆動したらプルアップ/プルダウンの指定に関係なく、駆動されたレベルになる。
つまり、主に入力ピンのための機能だと思えばよい。
出力ピンなら自らが0か1に駆動するから基本的にいらない。が、自ら「駆動しない」という使い方をするなら別だが、ちょっと割愛。
プルアップにするかプルダウンにするかは、そのピンの機能によりけりであり、誤動作しないように設定すればよい。