SNSへはこちら

ESP8266でベアメタル(8) - PWM(その1)

お次は PWM 機能です。今回の記事は全て NonOS SDK API Reference を参考にしています。

ESP8266におけるPWM

結論から言います。このマイコンには PWM ペリフェラルは存在しません。この PWM は SDK でソフトウェア的に実装されている機能であり、決してペリフェラルでは無いということに注意すべきです。

実装内容は恐らく以下のようです。

  • ハードウェアタイマー(以下、HW タイマ 等々...)で実装されている
  • ピン機能自体は GPIO
  • 初期化時・設定変更時の Period, Duty によって、HW タイマ内部で構成したコールバック関数を動作させる
    • 内部にカウンタを持っていて、それによって動作を変える
    • カウンタが設定値以下で Low 出力、それ以上で High 出力

このような実装構成を推測するに至ったのは、SDK Reference 中のこの一文です。

PWM APIs can not be called when APIs in hw_timer.c are in use, because they use the same hardware timer.

当初は「PWM ペリフェラルでさえも無いのか」と若干落胆しましたが、まあそういうものでしょう。あと、やっぱり関数の実装が無いのでどういう挙動になっているのかイマイチ分かりません。ライブラリのソースコードは読むもの!いいですね?

使い方

やっぱりマニュアルがカス実例がないとわかりにくいですので、なんとなく試行錯誤した結果わかったことを載せます。日本語情報マジでないんだよね。Arduino IDE?そんなの知りませんね...

例によって peripheral_test プロジェクトをベースに考えます。

初期化

pwm_init 関数で初期化をします。この時、PWM のチャネルなるものを定義でき、どうやら最大8チャンネルまで PWM として GPIO ピンを登録できるようです。ちなみにこの「最大8チャンネルまで」というのは SDK のヘッダファイルを読んだ結果です。ちゃんと記載してくれよまじで。

初期化は以下のように行います。例えば IO4 のみを PWM として登録したい場合。

uint32 io_info[][3] = {
    {PERIPHS_IO_MUX_GPIO4_U, FUNC_GPIO4, 4},
};
uint32 duty = 0;
pwm_init(1000, &duty, 1, io_info);

ここで「いや、io_info って構造体じゃねーのかよ」と思った貴方。僕と同じ考えです

それはさておき、まず io_info です。これは PWM 機能として登録するピンを記述する配列です。IOx を PWM 化したいのだったら、素直に {PERIPHS_IO_MUX_GPIOx_U, FUNC_GPIOx, x} って書けばいいです。その他特殊ピンの扱いは使用するマクロが異なりますが、そんなの使うほうが悪い(暴言)ということで、一般の GPIO のみの解説とします。もしそのようなギリギリを攻める構成にしたい場合は、是非是非ヘッダーファイルを読んでください。

複数ピンをまとめて登録したい場合は、3つの要素を並べて配列にしてください。登録した要素番号順にチャンネルとピンとの対応が取られます。
この時、関数の引数として記述した1を登録チャンネル数に書き換えることをお忘れなく。

続いて duty ですが、これは初期状態の duty を規定します。シンプルですね。ただ、後述するようにこの duty は duty のようであって duty ではないので、注意が必要です。

最後に、関数に渡した第1引数ですが、こちらは PWM 波形1周期の時間となります。単位は usec。この例だと 1msec で1周期になります。

dutyについて

まずは以下に 100% の duty で PWM 波形を出させる方法を示します。

pwm_set_duty(22222, 0); // What's 22222 ??
pwm_start();

数字はさておき、以下のような記述です。

  • pwm_set_duty でチャネル0の duty を22222に
  • pwm_start で設定を適用

設定を適用するのに start とはなんか違くないか?とも思いますが(update とすべきだとは思う)、以上のような記述で duty を変更できます。

で、この duty が duty ではない問題です。
通常ですと duty は 0〜100 もしくは 0〜1 で規定されますが、どうやら違うらしいです。リファレンスでは duty cycle と記述されていますが、(duty*45)/ (period*1000) で規定されているらしいですよ。謎の式出た。また、duty の最大値は period * 1000 /45 らしいです。

恐らく PWM をソフトウェア的に動かしている関係で CPU クロックと命令の兼ね合いがあるんでしょうが、この辺もライブラリとしてラップしてほしかったです...

実際に計算してみましょう。duty の最大値。
今回は period = 1000 なので、この値は 22,222.222... となります。なるほど、これが先程の値ですね。ということはこれを 100% として後は割合計算をすれば LED の調光はできそうです。
よく見ると、duty cycle の式はこの計算式の逆数の duty 倍になっているのもお分かりいただけると思います。

LEDボヤァのソースコード

では以上を踏まえまして、とりあえずのコードを載せておきます。いい感じにLEDボヤァすると思いますよ。

その前に、#include "pwm.h" をしておいてください。パーティションテーブルとかの部分は省略して記述します。

static os_timer_t os_timer;
static void os_timer_cb(void *arg) { // {{{
    static uint32 count = 0;
    count += 22222 * 10 / 100; // incr by 10%
    if( count >= 22222 ) count = 0;
    pwm_set_duty(count, 0);
    pwm_start();
} // }}}

void user_init(void) {
    uart_init(BIT_RATE_115200, BIT_RATE_115200); // For debug output

    uint32 io_info[][3] = {
        {PERIPHS_IO_MUX_GPIO4_U, FUNC_GPIO4, 4},
    };
    uint32 duty = 0;
    pwm_init(1000, &duty, 1, io_info);
    os_timer_disarm(&os_timer);
    os_timer_setfn(&os_timer, (os_timer_func_t *)os_timer_cb, NULL);
    os_timer_arm(&os_timer, 100, 1); // 100msec
}

ビルド前にプロジェクトルートの Makefile に -lpwm を追加します。1秒に1回くらいの周期でLEDボヤァしていたら成功です。

よくわからないことがあるんですよ。色々と試したその実験結果等を書こうと思ったのですが、長いので次回に続きます

今年の記事投稿はこれが最後と思います。皆さん、この状況下ですがそれぞれが工夫された良いお年を。