前回の記事で QR Clock ver.2 のハードウェアを紹介しました。今回はソフトウェアについて紹介していきます。
マイコンには Raspberry Pi Pico W を使用しています。ピン数が多いことや CPU のスペックがそれなりにあること、 Wi-Fi が使えることなどが選定理由です。
もともと MicroPython でプログラムを書こうと思っていましたが、 QR コード生成処理が重たく MicroPython では間に合わなかったのでC言語を用いて開発することにしました。
なお、プログラムは GitHub 上で公開しているのでこちらも参考にしてください。
目次
C言語での開発環境
C 言語、また今回は使用していませんが C++ 言語での開発には Raspberry Pi 公式が提供している SDK (Software Development Kit) を使用することができます。
ドキュメント等はこちらのページに集まっています。
SDK のインストールは VSCode の「 Raspberry Pi Pico 」というプラグインを利用するのが簡単で、詳細は忘れましたが最初にプロジェクトを作成した際に自動で SDK がインストールされた覚えがあります。

注意点として、 SDK が C ドライブのユーザホームディレクトリ直下にインストールされたのですが、その場合プロジェクトを D ドライブに作るとうまくいきませんでした。
SDK のサイズが結構大きくて再インストールするのは面倒だったため、プロジェクトを C ドライブ配下に作成することで解決しました。 D ドライブを使いたい場合などはインストール場所に注意しましょう。
プロジェクトの作成は拡張機能のメニューの「 New C/C++ Project 」を押すことで開始します。

プロジェクトの設定画面が出るので、プロジェクト名、ボードの種類、作成場所を指定します。
大事な設定項目としては、 USB を標準入出力として使えるようにする「 Console over USB 」にチェックをいれるのと、無線通信のオプションとして「 Polled lwIP 」を指定しました。
もう一つの無線オプションである Background lwIP は試していないので知りません。

これでプロジェクトが作成されました。ビルドや書き込みについては記事の後半で説明します。
リアルタイムクロックIC DS1302
RTC (リアルタイムクロック)に DS1302 (秋月電子 113634)を用いています。データシートを基にプログラムを作成しました。
DS1302 との通信は I2C や SPI ではない独自形式のシリアル通信となっています。通信に必要な線は 3 本で、以下のような形式で読み込み、書き込みを行います。

注目すべき点として以下の点が挙げられます。
- 1 byte目がアドレス、2 byte目がデータ
- 下位ビットから順に送信される
- クロックの立上りで値の取り込みが行われる
アドレスの R/C のビットについては、 1 にすることで 32 byte の RAM が使えますが、今回は特に必要がないので使いません。
この単独の読み書きをするプログラムを次のように作成しました。
通信に使用する GPIO ピンをまとめて構造体で管理することにしています。
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 |
typedef struct { uint pin_clk, pin_dio, pin_ce; } ds1302_t; #define DS1302_DELAY_US 1 void ds1302_delay() { sleep_us(DS1302_DELAY_US); } int read_byte(ds1302_t *dev) { int res = 0; for (int i = 0; i < 8; i++) { res |= (gpio_get(dev->pin_dio) << i); ds1302_delay(); gpio_put(dev->pin_clk, 1); ds1302_delay(); gpio_put(dev->pin_clk, 0); ds1302_delay(); } return res; } void write_byte(ds1302_t *dev, int data) { gpio_set_dir(dev->pin_dio, GPIO_OUT); for (int i = 0; i < 8; i++) { gpio_put(dev->pin_clk, 0); ds1302_delay(); gpio_put(dev->pin_dio, (data >> i) & 1); ds1302_delay(); gpio_put(dev->pin_clk, 1); ds1302_delay(); } gpio_set_dir(dev->pin_dio, GPIO_IN); gpio_put(dev->pin_clk, 0); ds1302_delay(); } void set_reg(ds1302_t *dev, int reg, int data) { gpio_put(dev->pin_ce, 1); ds1302_delay(); write_byte(dev, reg); write_byte(dev, data); gpio_put(dev->pin_ce, 0); ds1302_delay(); } int get_reg(ds1302_t *dev, int reg) { gpio_put(dev->pin_ce, 1); ds1302_delay(); write_byte(dev, reg | 1); //Readはアドレス+1 int res = read_byte(dev); gpio_put(dev->pin_ce, 0); ds1302_delay(); return res; } |
時刻情報は DS1302 のレジスタに BCD (Binary Coded Decimal) 形式で格納されています。これは 10 進数の各桁を 2 進数の 4 桁ずつに対応させる表現です。

7 セグ時計など 1 桁ずつそのまま扱う場合には便利ですが、今回はもっと汎用的な形式で扱いたいので、pico/stdlib.hに含まれるdatetime_t型で読み書きできる関数を用意しました。
RTC の動作設定 (ClockHalt) やトリクルチャージの設定は初回起動時に一度行えばいいのですが、簡単のため時刻設定時に毎回設定するようにしています。
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 |
int bcd_to_int(int data) { return (data / 16) * 10 + (data % 16); } int int_to_bcd(int data) { return (data / 10) * 16 + (data % 10); } void ds1302_set_datetime(ds1302_t *dev, datetime_t datetime) { set_reg(dev, DS1302_REG_WP, 0); // Disable WriteProtect set_reg(dev, DS1302_REG_SECOND, int_to_bcd(datetime.sec)); // 同時に ClockHalt = 0 も設定 set_reg(dev, DS1302_REG_MINUTE, int_to_bcd(datetime.min)); // set_reg(dev, DS1302_REG_HOUR, int_to_bcd(datetime.hour)); // 24 hour モード set_reg(dev, DS1302_REG_DATE, int_to_bcd(datetime.day)); // set_reg(dev, DS1302_REG_MONTH, int_to_bcd(datetime.month)); // set_reg(dev, DS1302_REG_DAY, int_to_bcd(datetime.dotw)); // set_reg(dev, DS1302_REG_YEAR, int_to_bcd(datetime.year % 100)); // set_reg(dev, DS1302_REG_CTRL, 0xA5); // Trickle Charge 1 Diode, 2k ohm } datetime_t ds1302_get_datetime(ds1302_t *dev) { datetime_t res; res.sec = bcd_to_int(get_reg(dev, DS1302_REG_SECOND)); res.min = bcd_to_int(get_reg(dev, DS1302_REG_MINUTE)); res.hour = bcd_to_int(get_reg(dev, DS1302_REG_HOUR)); res.day = bcd_to_int(get_reg(dev, DS1302_REG_DATE)); res.month = bcd_to_int(get_reg(dev, DS1302_REG_MONTH)); res.dotw = bcd_to_int(get_reg(dev, DS1302_REG_DAY)); res.year = 2000 + bcd_to_int(get_reg(dev, DS1302_REG_YEAR)); return res; } |
LEDドライバ TM1640
16 個の 2 色 8×8 マトリクス LED を制御するために、 16 個の LED ドライバ TM1640 (秋月電子 113225)を搭載しています。この IC は本来 16 桁までのカソードコモン 7 セグ LED を制御することができますが、配線を考えると 2 色 8×8 マトリクス LED でも使用することができました。
データシートによれば、 TM1640 との通信は 2 本の線を用いる独自形式のシリアル通信です。

これをプログラムにするのですが、今回は 16 個の TM1640 をまとめて操作する前提のプログラムとなっています。クロック線は全体で共通で、信号線は 16 本個別に制御します。
まずは 16 チャネルまとめて通信の開始、終了をする処理と、同一のデータを全体に送信する関数を紹介します。
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 |
#define TM1640_CHANNELS 16 typedef struct { uint pin_clk; uint pin_dios[TM1640_CHANNELS]; uint brightness; // 明るさ設定値 } tm1640_t; #define TM1640_DELAY_US 10 // 10us delay between clk/dio pulses void tm1640_delay() { sleep_us(TM1640_DELAY_US); } void tm1640_start(const tm1640_t *dev) { for (int i = 0; i < TM1640_CHANNELS; i++) gpio_put(dev->pin_dios[i], 0); tm1640_delay(); gpio_put(dev->pin_clk, 0); tm1640_delay(); } void tm1640_stop(const tm1640_t *dev) { for (int i = 0; i < TM1640_CHANNELS; i++) gpio_put(dev->pin_dios[i], 0); tm1640_delay(); gpio_put(dev->pin_clk, 1); tm1640_delay(); for (int i = 0; i < TM1640_CHANNELS; i++) gpio_put(dev->pin_dios[i], 1); tm1640_delay(); } // 全部に同じデータを送信する void tm1640_write_byte(const tm1640_t *dev, uint8_t data) { for (int i = 0; i < 8; i++) { int bit = (data >> i) & 1; for (int ch = 0; ch < TM1640_CHANNELS; ch++) { gpio_put(dev->pin_dios[ch], bit); } tm1640_delay(); gpio_put(dev->pin_clk, 1); tm1640_delay(); gpio_put(dev->pin_clk, 0); tm1640_delay(); } } |
これを使って、コマンドを全体に送信することができます。コマンドはデータコマンド、アドレスコマンド、表示制御コマンドの 3 種類があるのでそれぞれ簡単に説明します。
まずデータコマンドについて、これはデータ送信時にアドレスが自動でインクリメントされるかどうかを設定できます。
今回の用途では、データを更新するときは特定のアドレスだけでなくて全体を書き換えたいので、 Address auto +1 のモードで使用します。

次にアドレスコマンドについて、これはデータを書き換える際に対象のアドレスを指定するコマンドです。 Address auto +1 モードの場合、連続するデータの書き込み開始アドレスを指定することになります。
今回の用途では、必ず全体を書き換えるので、 0 番地以外を指定することはありません。 0x0F の次は多分 0x00 に戻ると思うので、毎回 16 byte ずつデータを送信していればアドレスコマンドを打たなくても大丈夫かもしれませんが、念のため書き込みの度にアドレスを 0x00 に指定するようにしています。

最後に表示制御コマンドですが、これは LED の明るさと表示の ON/OFF を設定することができます。
これは基本的には初期化処理の中で一度だけ実行すればよいものになります。

データコマンドと表示制御コマンドを打つ関数を紹介します。
|
1 2 3 4 5 6 7 8 9 10 11 12 13 |
void tm1640_write_data_cmd(const tm1640_t *dev) { tm1640_start(dev); tm1640_write_byte(dev, TM1640_CMD1); tm1640_stop(dev); } void tm1640_write_dsp_ctrl(const tm1640_t *dev) { tm1640_start(dev); tm1640_write_byte(dev, TM1640_CMD3 | TM1640_DSP_ON | dev->brightness); tm1640_stop(dev); } |
さて、肝心のデータを個別に送信する処理についてですが、まずはデータの表現方法を考える必要がありました。
QR コードや文字の描画を行うことを考えると、複数の LED マトリクスを並べたディスプレイ全体の各ピクセルごとの色の配列があると便利そうです。ただ TM1640 の制御という抽象度を考えると、もっと低レベルで各 IC が担当する 2x8x8 = 128 bit を 64 bit 整数 2 つで表すことにしました。
データを送信する関数は次のようになっています。 4 重ループの順番が少々複雑ですが、仕方ないかなと思いました。
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 |
// data: 16ch x 2color. Each color 64bit = 8row x 8col. Little endian. void tm1640_write_ints(const tm1640_t *dev, const uint64_t data[16][2]) { tm1640_write_data_cmd(dev); tm1640_start(dev); tm1640_write_byte(dev, TM1640_CMD2); // Start address for (int color = 0; color < 2; color++) { for (int row = 0; row < 8; row++) { for (int col = 0; col < 8; col++) { for (int ch = 0; ch < TM1640_CHANNELS; ch++) { int bit = (data[ch][color] >> (row * 8 + col)) & 1; gpio_put(dev->pin_dios[ch], bit); } tm1640_delay(); gpio_put(dev->pin_clk, 1); tm1640_delay(); gpio_put(dev->pin_clk, 0); tm1640_delay(); } } } tm1640_stop(dev); } |
ロータリーエンコーダ
今回作成した QR Clock ver.2 は表示モード設定などの操作にロータリーエンコーダを採用しました。
ロータリーエンコーダについては過去に使い方を解説したことがありますが(過去記事)、今回ははじめ MicroPython で開発しようとしていたこともあり、こちらのライブラリを参考にさせていただきました。
この Python のコードをもとに、プッシュスイッチの機能とソフトウェアによるチャタリング除去機能を実装しました。ロータリーエンコーダ部分についてはチャタリング除去なしでもほとんど問題なく動いていたのですが、プッシュスイッチ部分では必須でした。
デバイスを管理する構造体には、ピン番号の他プッシュや回転の検出を表すフラグ変数と、内部の計算で使うための状態を記憶する変数が含まれています。
rotary_main_loop()を定期的に呼び出すことで GPIO の読み取りとイベント検出を行います。
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 |
typedef struct { uint pin_a, pin_b, pin_p; int hist_len; bool f_push; int f_rotate; int _state; uint64_t _a_hist, _b_hist, _p_hist; int _a_val, _b_val, _p_val; } rotary_t; void rotary_main_loop(rotary_t *dev) { uint64_t mask = dev->hist_len >= 64 ? UINT64_MAX : (1ull << dev->hist_len) - 1; int edge_a = debounce_detect_edge(dev->pin_a, &dev->_a_hist, &dev->_a_val, mask); int edge_b = debounce_detect_edge(dev->pin_b, &dev->_b_hist, &dev->_b_val, mask); int edge_p = debounce_detect_edge(dev->pin_p, &dev->_p_hist, &dev->_p_val, mask); if (edge_a != 0 || edge_b != 0) { int incr = process_rotary_pins(dev); if (incr != 0) dev->f_rotate = incr; } if (edge_p == -1) { dev->f_push = true; } } |
メイン関数では次のような使い方をしています。
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
int main() { ... while (true) { rotary_main_loop(&rotary); // イベント処理 if (rotary.f_push) { rotary.f_push = false; // クリック時の処理 ... } if (rotary.f_rotate != 0) { // 回転時の処理 ... rotary.f_rotate = 0; } ... } } |
QRコード計算
この作品の一番大事なところとも言える QR コードを生成する処理です。前作のマイクロ QR コード時計では QR コード生成処理を手書きしたのですが、今回は開発期間短縮のために既存のライブラリを用いることにしました。
Python用ライブラリ
冒頭にも書いた通り、まずは MicroPython で QR コードを生成しようと考えていました。しかし、 MicroPython 用の QR コードライブラリは見当たらず、通常の Python 用のライブラリとしてこちらを試してみました。
PyPI のページの「ファイルをダウンロード」から .tar.gz ファイルをダウンロードして解凍すると、 Python のソースファイルを入手できました。
通常の Python 環境で動作確認を取ったのち、 MicroPython 環境で動かしてみると、「標準ライブラリのbisectモジュールが存在しない」といったエラーが出ました。
MicroPython は Python の縮小版なのでライブラリが足りないことがあります。今回のケースでは、bisect.pyというファイルを作成して、bisect_left()関数を標準ライブラリと同じ引数の仕様で定義すれば解決しました。
そのほかの修正点として、不要な画像化のプログラムを削除したり、再帰で計算していた多項式の剰余をループで計算するように変更したり、正規表現のreモジュールの一部使えない関数を回避したりといった変更を加えると MicroPython でも動くようになりました。
しかし、 QR コード 1 つを計算するのに 1000 ms 以上の時間がかかったため、毎秒表示を更新するという要件を満たすことができず、 MicroPython は諦めることにしました。
C言語用ライブラリ
C 言語用の QR コード生成ライブラリとして、こちらの GitHub で公開されている libqrencode を使用しました。
リポジトリのトップディレクトリにある.c/.hファイルをダウンロードして、手元のプロジェクトに追加します。この中でメインファイルからインクルードする必要があるのはqrencode.hだけです。
datetime_t型の日付、時刻情報から文字列を組み立てて、それを QR コードにエンコードするプログラムを以下に示します。
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 |
// UTF-8で保存すること. const char *weekday_japanese[7] = {"日", "月", "火", "水", "木", "金", "土"}; void show_qr(datetime_t *dt) { char buf[50]; sprintf(buf, "%d年%d月%d日 (%s) %d時%d分%d秒", dt->year, dt->month, dt->day, weekday_japanese[(dt->dotw) % 7], dt->hour, dt->min, dt->sec); QRcode *qrcode = QRcode_encodeString(buf, 3, QR_ECLEVEL_M, QR_MODE_8, 1); if (!qrcode) { printf("QR encode failed\n"); return; } display_clear_matrix(display_matrix); for (int y = 0; y < qrcode->width; y++) { for (int x = 0; x < qrcode->width; x++) { int idx = y * qrcode->width + x; if (qrcode->data[idx] & 1) { display_matrix[y + 1][x + 1] = ORANGE; } } } ... } |
まずはsprintf()を使って QR コードに埋め込む文字列を作成します。日本語を使うのでファイルの文字コードには気を付けましょう。
次に libqrencode のQRcode_encodeString()を呼び出します。各引数の意味は次のとおりです。
- 第1引数:埋め込む文字列のポインタ。
- 第2引数:QRコードのバージョン(大きさ)。今回は29×29のバージョン3。
- 第3引数:誤り訂正レベル。
- 第4引数:エンコードのモード。数字モードや英数字モードなどもあるが、今回は文字コードをそのまま使う8ビットバイトモード。
- 第5引数:case sensitive. 大文字小文字関係ないなら0。8ビットバイトモードだとおそらく関係ないが1にしておいた。
QR コードが計算できたら、それを LED の状態を管理する 2 次元配列display_matrixに格納しています。qrcode->dataはunsigned charのデータですが、各ビットに意味が割り当てられています。最下位ビットが白黒を表しているので、 1 と論理積を取って最下位ビットを取り出しています。
QR コードを LED に表示させるとこんな感じです。

Wi-FiでNTPサーバーから時刻取得
Raspberry Pi Pico W の魅力の一つは Wi-Fi が使える点です。時計に必要な時刻合わせの機能を NTP を使ってできるようにしました。
プログラムについては、公式のサンプルプログラムの中に NTP を使うものがあったので、それをベースに改変しています。
今回は Wi-Fi は NTP でしか使わないので、ntp_client.cに Wi-Fi 関連のコードもまとめています。作成したntp_get_time()の処理は大雑把にいうと以下の流れになっています。
- ハードコードしておいたSSID、パスワードでWi-Fiに接続。
- DNSでNTPサーバー(pool.ntp.org)のIPアドレスを取得。
- NTPリクエストを送信。
- 帰ってきたデータから時刻(JST)を計算して
datetime_tに変換。
それぞれの処理について少し詳しく見ていきます。
初期化・Wi-Fi接続
まずは↑のステップの前段階として無線機能の初期化処理です。この関数はメイン関数の最初の諸々の初期化処理の中で呼び出しています。
それぞれの詳しい内容は全く把握していませんが、cyw43_arch_init()とcyw43_arch_enable_sta_mode()の 2 つの関数を呼べばよいようです。
|
1 2 3 4 5 6 7 8 |
void ntp_init() { if (cyw43_arch_init()) { printf("failed to cyw43_arch_init\n"); } cyw43_arch_enable_sta_mode(); } |
次に Wi-Fi のアクセスポイントに接続する処理です。この処理は実際に NTP を利用するときにはじめて実行するようにしています。
cyw43_arch_wifi_connect_timeout_ms()に SSID 、パスワード、認証方式、タイムアウト時間を渡します。
|
1 2 3 4 5 6 7 8 9 10 |
bool ntp_get_time(datetime_t *result, uint32_t timeout_ms) { ... if (cyw43_arch_wifi_connect_timeout_ms(WIFI_SSID, WIFI_PASSWORD, CYW43_AUTH_WPA2_AES_PSK, timeout_ms)) { printf("failed to connect Wifi\n"); return false; } ... } |
通信管理構造体
ここから DNS サーバーおよび NTP サーバーと通信していくのですが、通信に関する状態をNTP_Tという一つの構造体にまとめて管理しています。
通信は UDP で行います。udp_new_ip_type()を使って通信を管理する PCB (Protocol Control Block) というものを作るようです。
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
typedef struct NTP_T_ { ip_addr_t ntp_server_address; struct udp_pcb *ntp_pcb; int dns_result, ntp_result; // 0: waiting, 1: success, -1: fail uint32_t unix_time; } NTP_T; bool ntp_get_time(datetime_t *result, uint32_t timeout_ms) { ... NTP_T state; state.dns_result = 0; state.ntp_result = 0; state.ntp_pcb = udp_new_ip_type(IPADDR_TYPE_ANY); if (!state.ntp_pcb) { printf("failed to create pcb\n"); return false; } } |
DNSで名前解決
DNS はドメイン名から IP アドレスに変換するための仕組みです。 NTP サーバーの IP アドレスを取得するために DNS サーバーに問い合わせます。
dns_gethostbyname()に NTP サーバーのドメイン名と取得した IP アドレスの格納場所のアドレス、処理が完了したときに実行されるコールバック関数のアドレス、コールバック関数への引数を渡して実行します。
ネットワーク関係の処理はcyw43_arch_poll()を何度も呼ぶことで進みます。ただし、 CPU をある程度解放する必要があるようで、 while ループの中でsleep_ms()を呼ばないと処理が進まないという罠がありました。
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 |
// Call back with a DNS result static void ntp_dns_found(const char *hostname, const ip_addr_t *ipaddr, void *arg) { NTP_T *state = (NTP_T *)arg; if (ipaddr) { state->ntp_server_address = *ipaddr; printf("ntp address %s\n", ipaddr_ntoa(ipaddr)); state->dns_result = 1; } else { state->dns_result = -1; printf("ntp dns request failed\n"); } } bool ntp_get_time(datetime_t *result, uint32_t timeout_ms) { absolute_time_t deadline = make_timeout_time_ms(timeout_ms); ... cyw43_arch_lwip_begin(); int err = dns_gethostbyname(NTP_SERVER, &state.ntp_server_address, ntp_dns_found, &state); cyw43_arch_lwip_end(); if (err == ERR_INPROGRESS) { // DNS 待機 while (state.dns_result == 0 && absolute_time_diff_us(get_absolute_time(), deadline) > 0) { cyw43_arch_poll(); sleep_ms(1); } } else { printf("invalid DNS response.\n"); goto FAIL; } if (state.dns_result != 1) { printf("failed to get response from DNS.\n"); goto FAIL; } ... } |
NTPリクエスト
次に NTP サーバーに実際にリクエストを送信します。
ntp_request()がリクエストを送信する関数です。udp_sendto()を利用して DNS で取得した NTP サーバーの IP アドレスに対してデータを送信しています。
リクエストの後、 NTP サーバーから時刻データを含む返事が返ってくるはずですが、それを処理するのがntp_recv()関数です。この関数では返事が正しいことをチェックしたうえで NTP 時間から UNIX 時間へと時刻を変換しています。この関数は udp_recv()によってコールバック関数に登録されています。
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 |
// Make an NTP request static void ntp_request(NTP_T *state) { // cyw43_arch_lwip_begin/end should be used around calls into lwIP to ensure correct locking. // You can omit them if you are in a callback from lwIP. Note that when using pico_cyw_arch_poll // these calls are a no-op and can be omitted, but it is a good practice to use them in // case you switch the cyw43_arch type later. cyw43_arch_lwip_begin(); struct pbuf *p = pbuf_alloc(PBUF_TRANSPORT, NTP_MSG_LEN, PBUF_RAM); uint8_t *req = (uint8_t *)p->payload; memset(req, 0, NTP_MSG_LEN); req[0] = 0x1b; udp_sendto(state->ntp_pcb, p, &state->ntp_server_address, NTP_PORT); pbuf_free(p); cyw43_arch_lwip_end(); } // NTP data received static void ntp_recv(void *arg, struct udp_pcb *pcb, struct pbuf *p, const ip_addr_t *addr, u16_t port) { NTP_T *state = (NTP_T *)arg; uint8_t mode = pbuf_get_at(p, 0) & 0x7; uint8_t stratum = pbuf_get_at(p, 1); // Check the result if (ip_addr_cmp(addr, &state->ntp_server_address) && port == NTP_PORT && p->tot_len == NTP_MSG_LEN && mode == 0x4 && stratum != 0) { uint8_t seconds_buf[4] = {0}; pbuf_copy_partial(p, seconds_buf, sizeof(seconds_buf), 40); uint32_t ntp_time = seconds_buf[0] << 24 | seconds_buf[1] << 16 | seconds_buf[2] << 8 | seconds_buf[3]; state->unix_time = ntp_time - NTP_DELTA; state->ntp_result = 1; } else { state->ntp_result = -1; printf("invalid ntp response\n"); } pbuf_free(p); } bool ntp_get_time(datetime_t *result, uint32_t timeout_ms) { ... udp_recv(state.ntp_pcb, ntp_recv, &state); ntp_request(&state); // NTP 待機 while (state.ntp_result == 0 && absolute_time_diff_us(get_absolute_time(), deadline) > 0) { cyw43_arch_poll(); sleep_ms(1); // これがないと動かない! } if (state.ntp_result != 1) { printf("failed to get response from NTP.\n"); goto FAIL; } ... } |
時刻変換・後処理
UNIX 時間を JST に変換した上でdatetime_tのデータに変換します。
最後に udp_remove() でメモリの解放を行って終了します。
またここまでの各段階で処理が失敗したときにgoto FAIL;としていましたが、ntp_get_time()の最後にメモリの解放処理を置いてそこに飛ぶようにしています。
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
bool ntp_get_time(datetime_t *result, uint32_t timeout_ms) { ... time_t jst_time = state.unix_time + 9 * 60 * 60; struct tm *tm = gmtime(&jst_time); result->year = tm->tm_year + 1900; result->month = tm->tm_mon + 1; result->day = tm->tm_mday; result->hour = tm->tm_hour; result->min = tm->tm_min; result->sec = tm->tm_sec; result->dotw = tm->tm_wday; // 0 = Sunday udp_remove(state.ntp_pcb); return true; FAIL: udp_remove(state.ntp_pcb); return false; } |
タイマー割り込み
時刻の更新処理を 1 秒ごとに行うため、 Raspberry Pi Pico W 本体のタイマー割り込みを使用しています。
レジスタをいじったりする必要はなく、一定の周期でコールバック関数を呼ぶように設定する API のadd_repeating_timer_ms()を使用します。
プログラム中では以下のようにフラグをセットするコールバック関数を 1000 ms ごとに呼ぶように設定しています。
第 1 引数は周期を指定しますが、負の場合はコールバック関数の呼び出し間隔として設定し、正の場合はコールバック関数の処理が終わってから次に呼び出されるまでの時間として設定することを意味しています。
第 4 引数のtimerはコールバック関数に渡される構造体ですが、特に使用はしていません。
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
bool f_update = false; // 画面更新フラグ struct repeating_timer timer; bool timer_callback(struct repeating_timer *t) { f_update = true; return true; } void init() { ... add_repeating_timer_ms(-1000, timer_callback, NULL, &timer); } |
LEDディスプレイのデータ管理
ここからはハードウェアの使い方ではなくソフトウェア的な話題になります。
8×8 の赤緑 2 色 LED マトリクスモジュールが 16 個並んで、 32×32 の LED ディスプレイを構成しているのですが、文字や図形を描画したいという視点ではモジュール単位の区切りなどは考えたくありません。
そこで、以下に示すように enum として Color_t を定義し、 Color_t[32][32]としてディスプレイのデータを管理することにしました。
しかし LED ドライバの TM1640 に渡すデータは前述したとおり、 64 bit x 2 色の uint64_t[16][2]としているので、これらの間の変換処理を用意しています。
|
1 2 3 4 5 6 7 |
typedef enum { OFF = 0b00, RED = 0b01, GREEN = 0b10, ORANGE = 0b11, } Color_t; |
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 |
typedef struct { int ch; uint64_t bit; } pos_t; // 32 x 32 のマトリクスから 16 ch x 64 bit のデータへの変換テーブル pos_t pos_table[32][32]; void display_init() { for (int i = 0; i < 32; i++) { for (int j = 0; j < 32; j++) { int ch = (i / 8) + (3 - (j / 8)) * 4; int row = i % 8; int col = 7 - (j % 8); pos_table[i][j] = (pos_t){ch, 1ull << (row * 8 + col)}; } } } void display_convert_matrix_to_array(const Color_t matrix[32][32], uint64_t array[TM1640_CHANNELS][2]) { for (int i = 0; i < TM1640_CHANNELS; i++) { array[i][0] = 0; array[i][1] = 0; } for (int i = 0; i < 32; i++) { for (int j = 0; j < 32; j++) { int ch = pos_table[i][j].ch; uint64_t bit = pos_table[i][j].bit; if (matrix[i][j] & RED) array[ch][0] |= bit; if (matrix[i][j] & GREEN) array[ch][1] |= bit; } } } void display_clear_matrix(Color_t matrix[32][32]) { for (int i = 0; i < 32; i++) { for (int j = 0; j < 32; j++) { matrix[i][j] = OFF; } } } |
|
1 2 |
uint64_t display_array[TM1640_CHANNELS][2]; Color_t display_matrix[32][32]; |
文字描画
メニュー機能およびデジタル時計モードで英数字や記号を描画します。使用する文字種は限られているので、必要な文字だけフォントデータを手作業で作成することにしました。
フォントは隣の文字との余白も含めて 8×6 px のサイズとし、 1 列を 1 byte として各文字 6 byte のデータとしています。使う文字だけ用意しているので、文字とバイト列の組として記録してあります。
LED ディスプレイを 4 行 5 列の文字表示器として扱って、文字列を描画するdisplay_print_string_to_matrix()関数を用意しています。
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 |
typedef struct { char c; uint8_t data[6]; } font_t; const font_t font[] = { {'0', {0b0111110, 0b1000101, 0b1001001, 0b1010001, 0b0111110, 0}}, {'1', {0b0000000, 0b0100001, 0b1111111, 0b0000001, 0b0000000, 0}}, {'2', {0b0100001, 0b1000011, 0b1000101, 0b1001001, 0b0110001, 0}}, {'3', {0b1000010, 0b1000001, 0b1010001, 0b1101001, 0b1000110, 0}}, {'4', {0b0001100, 0b0010100, 0b0100100, 0b1111111, 0b0000100, 0}}, {'5', {0b1110010, 0b1010001, 0b1010001, 0b1010001, 0b1001110, 0}}, {'6', {0b0011110, 0b0101001, 0b1001001, 0b1001001, 0b0000110, 0}}, {'7', {0b1000000, 0b1000111, 0b1001000, 0b1010000, 0b1100000, 0}}, {'8', {0b0110110, 0b1001001, 0b1001001, 0b1001001, 0b0110110, 0}}, {'9', {0b0110000, 0b1001001, 0b1001001, 0b1001010, 0b0111100, 0}}, {'A', {0b0111111, 0b1000100, 0b1000100, 0b1000100, 0b0111111, 0}}, {'D', {0b1111111, 0b1000001, 0b1000001, 0b0100010, 0b0011100, 0}}, {'G', {0b0111110, 0b1000001, 0b1001001, 0b1001001, 0b1101111, 0}}, {'I', {0b0000000, 0b1000001, 0b1111111, 0b1000001, 0b0000000, 0}}, {'N', {0b1111111, 0b0010000, 0b0001000, 0b0000100, 0b1111111, 0}}, {'P', {0b1111111, 0b1001000, 0b1001000, 0b1001000, 0b0110000, 0}}, {'Q', {0b0111110, 0b1000001, 0b1000101, 0b1000010, 0b0111101, 0}}, {'R', {0b1111111, 0b1001000, 0b1001100, 0b1001010, 0b0110001, 0}}, {'T', {0b1000000, 0b1000000, 0b1111111, 0b1000000, 0b1000000, 0}}, {'o', {0b0011100, 0b0100010, 0b0100010, 0b0100010, 0b0011100, 0}}, {'x', {0b0100010, 0b0010100, 0b0001000, 0b0010100, 0b0100010, 0}}, {'>', {0b1000001, 0b0100010, 0b0010100, 0b0001000, 0b0000000, 0}}, {'/', {0b0000010, 0b0000100, 0b0001000, 0b0010000, 0b0100000, 0}}, {'_', {0b0000001, 0b0000001, 0b0000001, 0b0000001, 0b0000001, 0}}, {':', {0b0000000, 0b0000000, 0b0010100, 0b0000000, 0b0000000, 0}}, {'\0', {0, 0, 0, 0, 0, 0}}, }; const uint8_t *get_font(char c) { for (int i = 0;; i++) { if (font[i].c == c || font[i].c == '\0') return font[i].data; } } // line: 0-3, col: 0-4. 指定した位置から配置し自動で改行 void display_print_string_to_matrix(char *s, int line, int col, Color_t color, Color_t matrix[32][32]) { for (int i = 0; s[i] != '\0'; i++) { const uint8_t *f = get_font(s[i]); for (int j = 0; j < 6; j++) { for (int k = 0; k < 8; k++) { if ((f[j] >> (7 - k)) & 1) matrix[line * 8 + k][col * 6 + j + 1] = color; else matrix[line * 8 + k][col * 6 + j + 1] = OFF; } } col++; if (col == 5) { col = 0; line++; } } } |
デジタル時計モードで時刻を表示するとこんな感じです。

アナログ時計描画
アナログ時計モードは事前に準備しておいた背景(文字盤)データと、直線描画処理の組み合わせで実装しました。
背景はこのような 32×32 のデータです。 Python の PIL を利用して適当なプログラムを書いて作成しました。uint32_t[32]としてプログラムに埋め込んであります。

|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 |
// 範囲内であれば色を塗る void draw_pixel(int x, int y, Color_t color, Color_t matrix[32][32]) { if (-15 <= x && x <= 16 && -15 <= y && y <= 16) { matrix[y + 15][x + 15] = color; } } // 線分が通るマスを塗りつぶす void draw_line(float x0, float y0, float x1, float y1, Color_t color, Color_t matrix[32][32]) { float dx = fabsf(x1 - x0); float dy = fabsf(y1 - y0); int steps = (int)fmaxf(dx, dy) * 2 + 1; // 十分な分解能でステップ数決定 for (int i = 0; i <= steps; i++) { float t = (float)i / steps; float x = x0 + (x1 - x0) * t; float y = y0 + (y1 - y0) * t; draw_pixel((int)roundf(x), (int)roundf(y), color, matrix); } } void draw_hand(float r, float theta, Color_t color, Color_t matrix[32][32]) { float x = cosf(theta) * r; float y = sinf(theta) * r; draw_line(0, 0, x, y, color, matrix); } |
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
void show_analog(datetime_t *dt) { draw_background(RED, display_matrix); // 秒針 float t1 = M_PI * 2 * dt->sec / 60 - M_PI_2; draw_hand(12, t1, GREEN, display_matrix); // 長針 float t2 = M_PI * 2 * dt->min / 60 - M_PI_2; draw_hand(10, t2, ORANGE, display_matrix); // 短針 float t3 = M_PI * 2 * (dt->hour % 12 * 5 + dt->min / 12) / 60 - M_PI_2; draw_hand(8, t3, ORANGE, display_matrix); display_convert_matrix_to_array(display_matrix, display_array); tm1640_write_ints(&tm1640, display_array); } |
アナログ時計モードで時刻を表示するとこんな感じです。

メインループ
メインループでやるべき仕事は以下の通りです。
- ロータリーエンコーダの入力処理
- ロータリーエンコーダにクリックや回転のイベントがあった場合、現在の状態に応じた処理
- 1秒ごとの時刻更新フラグが立っている場合、時刻を更新しモードに応じた表示処理
現在の状態による場合分けが多いので少し面倒ではありますが、一つずつ処理を書いていくだけです。
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 |
void init() { stdio_init_all(); for (int i = 0; i < TM1640_CHANNELS; i++) display_array[i][0] = display_array[i][1] = 0; tm1640_init(&tm1640, display_array); display_init(); rotary_init(&rotary); ds1302_init(&ds1302); ntp_init(); add_repeating_timer_ms(-1000, timer_callback, NULL, &timer); } int main() { sleep_ms(500); init(); int cursor = 0; char ntp_status = ' '; mode_t mode = QR; datetime_t dt; while (true) { rotary_main_loop(&rotary); // イベント処理 if (rotary.f_push) { rotary.f_push = false; if (mode == MENU) { if (cursor == 3) { ntp_status = '_'; show_menu(cursor, ntp_status); if (ntp_get_time(&dt, 10 * 1000)) { ntp_status = 'o'; show_menu(cursor, ntp_status); ds1302_set_datetime(&ds1302, dt); } else { ntp_status = 'x'; show_menu(cursor, ntp_status); } } else { mode = cursor; f_update = true; } } else { mode = MENU; ntp_status = ' '; show_menu(cursor, ntp_status); } } if (rotary.f_rotate != 0) { if (mode == MENU) { cursor = (cursor + rotary.f_rotate + 4) % 4; show_menu(cursor, ntp_status); } rotary.f_rotate = 0; } if (f_update && mode != MENU) { f_update = false; dt = ds1302_get_datetime(&ds1302); if (mode == QR) { show_qr(&dt); } else if (mode == DIG) { show_digital(&dt); } else if (mode == ANA) { show_analog(&dt); } } } } |
ビルド・書き込み
最後にプログラムのビルドと書き込みの方法を紹介します。
まずビルドについてですが、プロジェクトを作成した際に生成された CMakeLists.txt を編集します。
ソースファイルが複数に分割されている場合、 add_executable という項目で各 .c ファイルを対象に追加する必要がありました。
また Wi-Fi の SSID やパスワードをプログラム中で#defineする代わりに secrets.cmake というファイルに書いておき、 CMakeList.txt で include して target_compile_definitions という項目でコンパイル時に定義を追加できます。
それ以外の設定項目についてはよく把握していない部分も多いので説明しません。真似したい方はファイルを読んでみてください。
ビルドや書き込みは VS Code の画面右下にある Compile, Run などのボタンを押すか、コマンドパレットから Raspberry Pi Pico: Compile Pico Project などを選択することで実行できます。

書き込みの際は、本体の BOOTSEL ボタンを押しながら PC に接続する必要があることに注意しましょう。
おわりに
QRClock2 のプログラムは様々なプログラムを組み合わせていて、それぞれを雑多に紹介しました。一部分だけでも参考になれば嬉しいです。
冒頭にも書きましたが、プログラムは GitHub で公開しているので参考にどうぞ。
またニコニコにこの作品の動画を投稿しているので、未視聴の方は見ていただけると嬉しいです。





