SNSへはこちら

ESP8266でベアメタル(2) - Lチカやってみよう(レジスタ手打ちもあるよ)

若干難儀したLチカです。

予め断っておきたいのですが、現状このモジュールの構成とか、SPI Flash の書き込み方法(?) とか、色々とまだ把握できていません。ドキュメンテーションが結構散り散りになっていますので、取り敢えず動かして、そこから理解するという方針が良いかなあと思います。

使用する環境

前回構築したものをベースに、今回は NonOS SDK という SDK を用います。本音としては SDK なんて使いたくないのですが、取り敢えず動かなければ何も始まらないと思いこちらを使用します。そう言えばもう1つの SDK として、RTOS を用いたのがありますが、アレはレイヤが高すぎて吐き気がしてきます。

それでは適当なディレクトリに NonOS SDK のリポジトリをクローンしておきましょう。

$ git clone https://github.com/espressif/ESP8266_NONOS_SDK

そして、そのリポジトリルートに cd します。更に examples/peripheral_test をルートにコピーしておきましょう。

$ cd ESP8266_NONOS_SDK
$ cp -r examples/peripheral_test .

このように、examples ディレクトリに入っているプロジェクトは SDK のルートにコピーしてから使用する必要があります。さもないと make コマンドが上手く走りません。

参考にしたもの

  • Building examples in ESP8266 non-OS SDK v.2.0.0
    • 上のビルド方法に関して。
  • ESP8266 Non-OS SDK API Reference
    • SDK の API について。どこにどの内容を書けばいいか(ある程度)説明されています。
  • ESP8266 Technical Reference
    • GPIO レジスタのレジスタマップが載っています。
  • esp8266_memory_map [ESP8266 Support WIKI]
    • 非公式とは言え、各種リファレンスに載っていないペリフェラルのベースアドレスが記載されています。レジスタ手打ちに大いに役立ちそう。

ソースコード

ではLチカコードを記述します。peripheral_test 内の user/user_main.c上部に #include "driver/hw_timer.h" と追記してから、ファイル下部にある user_init という関数を一旦消して、以下のように追記します。

void timer_isr(void) {
    static int state = 0;
    if( state ) {
        gpio_output_set(0, BIT4, BIT4, 0); // clear
        state ^= 1;
    }else{
        gpio_output_set(BIT4, 0, BIT4, 0); // set
        state ^= 1;
    }

}

void ICACHE_FLASH_ATTR
user_init(void)
{
    hw_timer_init(FRC1_SOURCE, 1);
    hw_timer_set_func(timer_isr);
    hw_timer_arm(1000 * 1000);
}

内容としては、ハードウェアタイマーと呼ばれるタイマに 1000 * 1000 usec (== 1000msec) 周期で割り込みを入れます。コールバック関数に timer_isr (命名としてこれはおかしい気がしますが)を渡すことで、割り込み発生時にこの関数が内部で呼ばれます。

gpio_output_set に関しては API リファレンスを見ろと言いたいですが、コメントのようにイディオムになっています。BIT4 はビットマップですので、複数ピンを操作したい場合は OR すればいいと思います。多分。

通常の(?)Lチカのようにディレイタイマを噛ませて無限ループとしても良いのでしょうが、ウォッチドッグタイマがオーバーフローを起こしてしまうため、今回は避けました。まあ定期的に feed すれば良いのだがね。

ビルド

それでは行きましょう。make を叩いても良いのですが、デフォルトで構成されている Makefile はコマンドを打つ際に多数の引数を与えなければいけないようなので、それは面倒です。幸いにも gen_misc.sh というシェルスクリプトが用意されているので、それを呼んで対話的に構成していきましょう。

その前に - 2つの構成

この SDK では、ユーザープログラムを構成するのに2種類のビルド方法が存在するようです。

詳しくは分かりませんが、FTOA と Non-FTOA です。
FTOA は OTA アップデートを目的とした構成らしく、デフォルトのブートローダーによって起動し、アプデ時に書き換えを行う構成のようです(間違っていたら教えてください)。
Non-FTOA とは以上のようなことはなく、ある意味ベタ書きでそのままファームウェアを書き込む構成だと理解しています。

今回はよりレイヤの低いものを用いたいので、Non-FTOA の構成で行きます。

ビルドスクリプトの実行

では、Non-FTOA でビルドから書き込みまでやっていきましょう。
長いので、一部省略してログを貼っておきます。

$ ./gen_misc.sh                                                                                       [master]
gen_misc.sh version 20150511

Please follow below steps(1-5) to generate specific bin(s):
STEP 1: choose boot version(0=boot_v1.1, 1=boot_v1.2+, 2=none)
enter(0/1/2, default 2):
2
boot mode: none

STEP 2: choose bin generate(0=eagle.flash.bin+eagle.irom0text.bin, 1=user1.bin, 2=user2.bin)
enter (0/1/2, default 0):
0
generate bin: eagle.flash.bin+eagle.irom0text.bin

STEP 3: choose spi speed(0=20MHz, 1=26.7MHz, 2=40MHz, 3=80MHz)
enter (0/1/2/3, default 2):
2
spi speed: 40 MHz

STEP 4: choose spi mode(0=QIO, 1=QOUT, 2=DIO, 3=DOUT)
enter (0/1/2/3, default 0):
0
spi mode: QIO

STEP 5: choose spi size and map
    0= 512KB( 256KB+ 256KB)
    2=1024KB( 512KB+ 512KB)
    3=2048KB( 512KB+ 512KB)
    4=4096KB( 512KB+ 512KB)
    5=2048KB(1024KB+1024KB)
    6=4096KB(1024KB+1024KB)
    7=4096KB(2048KB+2048KB) not support ,just for compatible with nodeMCU board
    8=8192KB(1024KB+1024KB)
    9=16384KB(1024KB+1024KB)
enter (0/2/3/4/5/6/7/8/9, default 0):
6
spi size: 4096KB
spi ota map:  1024KB + 1024KB


start...
(略)
!!!
No boot needed.
Generate eagle.flash.bin and eagle.irom0text.bin successully in folder bin.
eagle.flash.bin-------->0x00000
eagle.irom0text.bin---->0x10000
!!!

これでビルド出来ました。メッセージ下部でアドレスが出ていますね。

書き込み

出来たバイナリは SDK ルートの bin に存在します。書き込みは上の2つのバイナリに加えて、元からこのディレクトリに入っていた一部ファイルを使います。

$ esptool.py write_flash 0x0 ../bin/eagle.flash.bin 0x3FC000 ../bin/esp_init_data_default_v08.bin 0x3FE000 ../bin/blank.bin 0x10000 ../bin/eagle.irom0text.bin

これで1秒ごとにチカチカしていたら成功!

レジスタを叩いてみよう

この SDK、パーティションテーブルとかいう物がよく分からなかったり、main 関数はユーザーが触れられなかったりしますが、それは置いておいてひとまずレジスタ手打ちまで行きたいと思います。
目標はスタートアップコードから自作。

見るべき資料は ESP8266 Technical Referenceesp8266_memory_map [ESP8266 Support WIKI]。ここでレジスタマップを見ながらコードを書いてみます。

LED を点灯させる

GPIO5 == High の時のみに GPIO4 につないでいる LED を点滅させるプログラムを、簡易的に user_init 内に記述してみます。取り敢えず中身をすべて消して、打ち込んでみたいと思います。

#define GPIO_BASE 0x60000300
#define GPIO_OUT_OFFSET 0
#define GPIO_ENABLE_OFFSET 3
#define GPIO_IN_OFFSET 6

#define GPIO(reg) (*(volatile unsigned int *)(GPIO_BASE + 4 * GPIO_##reg##_OFFSET))

void ICACHE_FLASH_ATTR
user_init(void)
{
    GPIO(ENABLE) = 1 << 4;
    while(1) {
        system_soft_wdt_feed();
        if( !(GPIO(IN) & (1 << 5)) ) continue;
        GPIO(OUT) ^= 1 << 4;
        for(volatile unsigned int i = 0; i < 1000000; i++);
    }
}

動作としては以下のようです。

  • GPIO5 == High のときは GPIO4 につながった LED が高速点滅
  • GPIO5 == Low のときは、GPIO4 の LED 点滅が休止

GPIO_ENABLE では n ビット目を立てると GPIOn のピン機能が OUT になります。立てておらず、かつ別レジスタでペリフェラル機能として設定されていないピンは IN になるようです。GPIO_OUT は普通にわかりますよね。

system_soft_wdt_feed() 使ってるじゃねえかゴルァ!って言う方。すみません。ここの解決法がわからないです。
ただ、どうやらこの SoC にはウォッチドッグタイマが搭載されていない疑惑あります。各種リファレンスを見てもそのような記述は見つかりませんし(PDF 中で watchdog, wdt 等で検索してもヒットしない)、そもそもこの関数名に soft と付いているのは怪しいですね。多分内部で定期的にグローバル変数をインクリメントしていって、一定値になったらリセットをかけるように仕向けているんでしょう。
なので、SDK の独自実装だと僕は思います。現状はこの詳細がわからないので関数で逃げておきます。

ひとまず、SDK 上という甘えの中ですが、レジスタ手打ちによるLチカが実装できてよかったです!!