この記事はDeNA 24 新卒 Advent Calendar 2023の21日目の記事です。
同期のみなさんが書かれた他の記事もぜひぜひチェックしてみてください!
vimとかマイコンとか、個人的に興味がある内容もたくさんで面白いです……!!!
昨今のマイコン開発
本記事でのマイコンは産業用途でなく、ホビー用途のマイコン(ESP32、ラズピコなど)を示します。
昨今のマイコン開発ではWiFi機能が当たり前に入りつつ、価格も10ドル以下で技適もばっちりなど、誰でも手が出しやすくなってきたように感じます。
最近ではArduino UnoにR4が登場したことでAVR 8bitを脱し、Arduino UnoシリーズもArm 32bitの仲間入りを果たしたり、WiFi搭載モデルが登場したりしています。
安価なマイコンではESP32シリーズが活躍しており、ESP32C3はRISC-Vを採用、ESP32S3ではXtensaとRISC-Vを使い分けるなど新しいアーキテクチャも登場しています。
ファイルシステムやセキュアブート、マルチタスクなど、PC向けOSなのか!?というレベルでいろいろなことができます。
記事本編にあたって
今回はマイコン本体ではなく、マイコンをプログラムする側に着目して、様々なマイコンプログラムが可能な言語を触ってみます!
本記事で使用した書き込みホストはArch Linuxですが、Linux系のOSであれば概ね同様で、ポートの名前は異なりますがMacOSでも概ね同様に環境構築可能です!
また本記事で使用するマイコンとしてM5Stack Core2を選びました。
(今回使用するどの言語でも動作が確認できるためです。)
ここからはC/C++ (PlatformIO), TinyGo, MicroPythonの3つについて「言語の概要→個人の所感→環境構築→動作確認」を行った後、紹介しきれなかった言語の説明を行い、最後に簡単な速度比較と全体の感想を述べる流れになります。
C/C++ (PlatformIO)
概要
Cでの開発は昔からある手法で、ホビー用途ではかつてArduinoIDEで一世を風靡しました。しかしArduioIDEの独特の使い心地やライブラリも含めて専用ツールでの書き込みが必要でした。(最近のバージョンではIDEがモダンになりましたが……)
最近ではPlatformIOというオールインワンツールが(個人の所感ですが)人気を博しています。
複数マイコン対応ができ、ライブラリ管理もできて、モダンな開発体験ができます。
対応言語はC/C++限定のツールです。
対応マイコン
今回挙げる中で最強の多さです。スクロールするだけで日が暮れる量です。
所感
絵文字は
😍→ポジティブな個人の感想
😢→ネガティブな個人の感想
としています。
😍
- 豊富なマイコン向けライブラリが存在!
- 複数マイコン(マルチボード)対応が容易
- 数多くのマイコンに対応
- C/C++でありながら、ライブラリの依存をiniファイルに書けるので他環境での再現性が高い
- マイコン個別のコンパイラや書込ツールのインストールが不要で全部自動で解決
- platformioが入ればどこでも動く←圧倒的に良い!!!!
😢
- (個人の力不足だが)C/C++での記述が大変(特に文字列操作など)
- 複数マイコン(マルチボード)に対して概ね同じコードが使えるが、内部の依存が暗黙的に違うことがあり、予期しない問題に遭遇しがち
環境構築
インストール
VSCodeの拡張機能で入れる方法とCLIツールを用いる方法の2種類あります。
前者はただクリックで入れるだけという恐ろしい簡単さで、後者もpacmanやhomebrewなどで簡単に入れられます。
↓VSCode拡張機能
↓CLI版
あくまで個人的な好みですが、CLI版のほうがサクサク動いて使い勝手も良いので、CLI版について取り上げます。最新のものは公式ドキュメントからご確認ください。
PlatformIOのインストールに関して、
HomeBrewなら
brew install platformio
Arch Linxuなら
sudo pacman -S platformio
でインストールできます。
platformIOのコマンドはpio
で始まります。
プロジェクト作成
initでプロジェクトを作ることができます。
ボードを指定できる-b
でM5 Stack Core2
を指定します。(複数指定も可能で、マルチターゲットなプロジェクトにもできます。)
mkdir with_pio && cd with_pio
pio project init -b "m5stack-core2"
ディレクトリが作られます。src/にプログラムを書いていきます。
tree .
.
├── include
│ └── README
├── lib
│ └── README
├── platformio.ini
├── src
└── test
└── README
5 directories, 4 files
マイコンやライブラリの指定はplatformio.iniに記述されます。
コマンド経由でも手動でも加筆修正可能です。
[env:m5stack-core2]
platform = espressif32
board = m5stack-core2
framework = arduino
初期の指定はframework = arduino
になっていますが、M5系のマイコンだとframework = espidf
も指定できたりします。
pioではpkg
コマンドでボードの追加もマイコンの追加もできて、Gitにおけるcheckout
コマンドみたいな汎用性があります。
ライブラリ探しはWebの公式registoryでの検索が便利です。(コマンドからの検索もできますが、こちらのほうがexamplesなどタブで見れてより便利です)
プログラム作成
今回はframework=arduino
なので、Arduinoのようなお作法でプログラムを書けば動きます。
ただしArduino IDEのように標準で色々ライブラリが入っているわけではないので、必要なライブラリは先述のpkg
コマンドでインストールする必要があります。
今回、M5Stack Core2のディスプレイに文字を描画したく、そのために必要なm5stack/M5Core2ライブラリをインストールします。
pio pkg install --library "m5stack/M5Core2@^0.1.8"
これで入ります(iniファイルに直書きでも可能です)。
src/にmain.cppを作成して下記プログラムを書き込みます。
M5Stack Core2のディスプレイにアドベントカレンダーのタイトル、作動中のCPUのクロック周波数、アドベントカレンダーリンクのQRコードを描画するプログラムです。
またシリアル通信検証のためにシリアル通信でRunning
を出し続けるようにもしています。
#include <Arduino.h>
#include <M5Core2.h>
void setup()
{
M5.begin();
M5.Lcd.fillScreen(GREEN);
M5.Lcd.setCursor(10, 10);
M5.Lcd.setTextColor(RED);
M5.Lcd.setTextSize(3);
M5.Lcd.println("DeNA\n New graduate\n Advent Calendar\n 2023\n");
M5.Lcd.setTextColor(BLACK);
M5.Lcd.setTextSize(2);
M5.Lcd.print("CPU Frequency:");
M5.Lcd.print(getCpuFrequencyMhz());
M5.Lcd.println("MHz");
M5.Lcd.qrcode("https://qiita.com/advent-calendar/2023/dena-24-newgrad", 228, 160, 90, 9);
}
void loop()
{
Serial.println("Running");
delay(3000);
}
-
M5.Lcd
でM5StackマイコンのLCD機能を呼び出しています。 -
getCpuFrequencyMhz()
はM5StackでなくESP32が提供する関数で、CPUのクロック周波数を取得できます(setCpuFrequencyMhz()
を使えばクロック周波数を変えることもできます) -
delay
はArduinoの関数で指定したミリ秒待つものです
といった具合にいろいろなレイヤのものが混ざり合っていて怖さがありますね……
他にもESP32のArduinoはFreeRTOSが入っているので、xTaskCreatePinnedToCore()
でFrreRTOSのタスクを実行することも可能で、それが#include
の追加なしに呼ぶことができて、牧歌的な空間になっています。
書き込み
書き込みはrun
コマンドを使用します。コンパイラや書き込みツールは自動で適切なものが選ばれます。
platformio.iniで指定すれば他のツールを明示的に選ぶこともできます。
pio run -t upload -e m5stack-core2
-
-e
の指定で書き込み環境を指定します。 -
-t upload
の指定でビルドだけでなくマイコンへの書き込みまで一度に可能です。 - ポートの指定は
--upload-port /dev/ttyUSB0
のように手動でも記述できますが、自動で識別してくれるので省略します。
これだけで書き込みまでできるはまさに革命です。
ありがちな自分で書き込みツールをmakeでビルドして……とかそういう手間が一切ありません。
動作検証
描画できてます!
(M5Stack Core2はバッテリー内臓なので外しても使えます。)
シリアル通信はdevice monitor
コマンドで確認できます。
M5Stack Core2をUSBで繋いで見てみます。
pio device monitor -b 115200
-
-b
で速度を指定できます。 - runと同じくポート選択できますが、記述を省略して自動選択にしています。
--- Terminal on /dev/ttyUSB0 | 115200 8-N-1
--- Available filters and text transformations: colorize, debug, default, direct, esp32_exception_decoder, hexlify, log2file, nocontrol, printable, send_on_enter, time
--- More details at https://bit.ly/pio-monitor-filters
--- Quit: Ctrl+C | Menu: Ctrl+T | Help: Ctrl+T followed by Ctrl+H
Running
Running
Running
Running
Running
Running
Running
Running
src/main.c
のloop
関数内で記述したRunningが垂れ流されていることがわかります。
PlatformIoはパッケージマネージャ、ビルド、書き込み、シリアルモニタがすべて揃って書き込みできるオールインワンツールです!!!
TinyGo
概要
TinyGoはGoのマイコン向けサブセットです。
マイコンにも依りますが、goroutineを使った開発すらマイコンでできちゃいます。
豆知識:Go 1.21でwebassemblyコンパイルがGo本体にもサポートされましたが、それまではTinyGoがGo→webassemblyへのコンパイル手段としても選ばれていました。
モダン言語のマイコン向け言語の多くがVMタイプなのに対して、TinyGoではLLVMを用いてビルドされ、マイコンに書き込まれます。
内部的にはLLVMを使っているため、新規マイコンの対応が早いのも特徴です。LLVM IRの勝利ですね。
対応マイコン
対応マイコンは先述の通り比較的多いです。
M5Stamp C3など最近出たマイコンにも対応しています。
またTinyGoでは新しいマイコンの追加を比較的容易にできるようです。(LLVMがサポートしていれば!!)
一方で、対応機能は一部のみです。(GPIOは使えるけど、WiFiモジュールは使えないなど)
例えばM5Stack Core2では下記のようにWiFiやBluetoothだけでなくI2CやPWMも未対応です……
Interface | Hardware Supported | TinyGo Support |
---|---|---|
GPIO | YES | YES |
UART | YES | YES |
SPI | YES | YES |
I2C | YES | Not yet |
ADC | YES | Not yet |
PWM | YES | Not yet |
USBDevice | NO | NO |
WiFi | YES | Not Yet |
Bluetooth | YES | Not yet |
所感
😍
- Go系のカンファレンスでもTinyGo制作事例が登場するなど、日本での利用事例も存在
- マイコンの差異が抽象化できる関数や定数になっていて可読性が高い(例えばマイコン搭載のLEDは
machine.LED
でピン番号を取得できる) - 環境構築が比較的容易
- LLVMでコンパイルできるマイコンターゲットであれば対応が早い
- 公式ドキュメントに対応マイコン、対応機能が分かりやすくまとまっている。
😢
- 対応マイコンは多いが、対応機能が一部のみ
- ライブラリのドキュメントが乏しい
デメリットは少ないですが、少ないデメリットが致命的で、使い方によっては厳しい側面もあります。
一方でディスプレやWiFi機能が不要な自作キーボードのプログラムなどでは利用事例もあり、言語は適材適所だなと感じさせられます。
環境構築
最新の方法は公式ドキュメントからご確認ください。
インストール
TinyGo自体は、HomeBrewなら
brew tap tinygo-org/tools
brew install tinygo
Arch Linxuなら
sudo pacman -S tinygo
でインストールできます。
追加でGoの環境も必要なので、rtxなどを用いて用意しておきましょう。
プロジェクト作成
プロジェクトの作成は普通のGoと同じです。
mkdir with_tinygo && cd with_tinygo
go mod init with_tinygo
tree .
.
└── go.mod
1 directory, 1 file
プログラム作成
Lチカをしつつ、シリアル通信でLEDの状態を表示するプログラムを作成してみます。
package main
import (
"machine"
"time"
)
func main() {
led := machine.GPIO23
led.Configure(machine.PinConfig{Mode: machine.PinOutput})
for {
println("High")
led.High()
time.Sleep(time.Second)
println("Low")
led.Low()
time.Sleep(time.Second)
}
}
-
machine.GPIO23
のようにしてGPIOにアクセスできます。 - マイコン固有のものに関しては、マイコンごとのページに定義が書いてあります。 例: M5Stack Core2
-
println
などでシリアル通信に出力を流せます
書き込み
書き込みはflash
コマンドを用います。
tinygo flash -target=m5stack-core2 main.go
動作検証
GPIO28とGNDにLEDを指すと……(本来は抵抗を挟むべきです。良い子は真似してはいけません)
(Gifでフレームレートを落としていますが実際は)1秒毎に点滅しています。
アドベントカレンダーらしくクリスマス要素も生まれてよかったです。
TinyGo自体にはシリアルモニタが付属していないので、自身で用意する必要があります。
(コメントにて@sago35さんよりご指摘いただいたとおり)TinyGoにはシリアルモニタも付属しています!
tinygo monitor --target m5stack-core2
↓(ポートは自動で認識してくれます)
Connected to /dev/ttyUSB0. Press Ctrl-C to exit.
Low
High
Low
High
Low
(無限に続く……)
GoでGPIOを用いてLチカしつつ、シリアル通信での出力を確認することができました!
MicroPython
概要
MicroPythonはPythonのマイコン向けサブセットです。
言語自体機能も豊富ですが、マイコン特化の機能もデフォルトで多く提供されており、例えばファイルシステムやネットワークアクセスなどが簡単に使用できます。
言語自体の機能についてより詳しく知りたい方はInterface 2023年3月号のMicroPython教科書が勉強になります。
またMicroPythonのESP32クイックリファレンスを見るだけでもマイコン固有の機能の対応の多さに驚かされます。
低レイヤの機能も使えつつ、高度な機能にもアクセスできる強みがあります。
M5Stack社が提供するGUIでマイコンプログラムができるUIFlowは内部でMicroPythonを用いています。
対応マイコン
インタプリタのダウンロードページから対応しているマイコンを確認できます。
この中にあるVendor:M5 Stack
にはATOMシリーズしか掲載がないですが、M5Stack公式ツールのUIFlowを用いれば、M5Stack社の多くのマイコンでMicroPython(をラップしたGUIプログラミングツール)が使えます。
マイコンごとにピン配置なども異なるため、マイコンの機能を適切に使うにはマイコンにあったインタプリタを用意する必要があります。
M5Stack公式のMicroPython導入ドキュメントにもUIFlow経由でのMicroPythonが紹介されています。
(M5Stamp C3Uなどは逆にUIFlowが対応しておらずMicroPython公式のインタプリタがあるので、MicroPython公式側のインタプリタを用いるのが良さそうです。
マイコンごとに環境構築の手法が異なり少し厄介です。)
いずれにしてもTinyGoやCとのそれと異なり、基本的にランタイムとプログラムを別個で書き込むことになります。
所感
😍
- Pythonの多くの機能が舞い込んでも使える
- マイコン固有の機能も一部対応している(M5Stack Core2のディスプレイやWiFiモジュールが使える)
- 対応マイコンが多い
- 低レイヤな機能にもアクセスできる
- マイコンにインタプリタを書き込んでしまえば、コンパイル不要で簡単にプログラムを試せる
😢
- C/C++と比べると速度が芳しくない
- 環境構築手段が複数あり、それぞれ特徴があり選定が難しく、書き込みまでの工数も多い
- インタプリタ、書込ツール、プログラムが別れているので初期構築に手間がかかる
- マイコンに書き込んで実行するまで動作確認できない(インタプリタ型なのでやむを得ない)
環境構築
インストール
M5Stack Core2の機能を最大限活かすのであればUIFlowですが、本記事ではオリジナルなMicroPythonで比較を行いため、MicroPython公式のesptool
を用いた手法を取り上げます。
公式のインストール手順に従います。
pip install esptool
まずマイコンのデータを消し去ります。
esptool.py --port /dev/ttyUSB0 erase_flash
先述の通りM5Stack向けはないので、ESP32 / WROOM で代用します。(M5 Stack Core2のSoCはESP32-D0WDQ6-V3ですが、一般的にこの世代はESP32と略されているようです)。
FirmeWareから最新の.binファイルをダウンロードして、書き込みます。
cd ~/ダウンロード
esptool.py --chip esp32 --port /dev/ttyUSB0 --baud 460800 write_flash -z 0x1000 ESP32_GENERIC-20231005-v1.21.0.bin
これでMicroPythonランタイムがマイコンに導入されました。
試しにシリアル通信経由でプログラムを入力して実行してみます。
任意のツールが利用可能ですが、今回も環境構築の手間を省くためにPlatformIOのツールを用います。
(MicroPythonのデフォルトのbaud rateは115200)
pio device monitor -b 115200
最初は何も出ませんが、キーボードのEnterを押すと>>>
が出てきます。
ここにMicroPythonのコードを入力すると実行できます。
試しにこちらを入力してみます。
import machine
machine.freq()
↓
現在のM5Stack Core2のクロック周波数が確認できました!
設定を行えばネットワーク経由でプログラムを書き込んだり、先ほどのようにシリアル通信でも書き込みができます。
ですが度々コピーするのは面倒なので、ファイルをFTP的にPCからマイコンに移動できるツールを導入します。
ファイルをPCから転送するツールもいくつかありますが、簡単にインストールできるrshellを今回は使用します。
pip install rshell
後ほど書き込み時に使用します。
プロジェクト作成
PlatformIOやTinyGoのような初期化コマンドはないので、ディレクトリを作ってPythonファイルを作成します。
mkdir with_micropython && cd with_micropython
プログラム作成
折角なのでMicroPythonのWLAN機能を使って天気情報を取得してみます。
(ssid,passwordは環境に応じて変えてください。)
import network
import urequests
import json
print("DeNA New graduate Advent Calendar 2023")
ssid = "SSID"
password = "PASSWORD"
url = "https://api.open-meteo.com/v1/forecast?latitude=35.6895&longitude=139.6917&hourly=temperature_2m&start_date=2023-12-24&end_date=2023-12-25"
# WiFiに接続
def connect_wifi():
station = network.WLAN(network.STA_IF)
station.active(True)
station.connect(ssid, password)
while not station.isconnected():
pass
print("接続OK")
# APIで温度取得
def get_temperature_change():
response = urequests.get(url)
data = response.json()
# 時間ごとの温度を表示
for entry in data["hourly"]["time"]:
index = data["hourly"]["time"].index(entry)
temperature = data["hourly"]["temperature_2m"][index]
print("時間: {}, 温度: {}°C".format(entry, temperature))
response.close()
connect_wifi()
get_temperature_change()
APIはOpen-Metroを用いて、Tokyoのクリスマスイブからクリスマスの温度を取得してみます。
ArduinoでもArduinoJson経由でJSONを扱えますが、デフォルトでここまでできるのはさすがモダンな言語です。
(良くも悪くも)型を意識する必要すらありません。
書き込み
下記コマンドで、マイコン上にSSHするかのような状態になります。
rshell -p /dev/ttyUSB0
(catやlsなども使えます。)
それでは、先ほど作成したmain.goをマイコン上にコピーします。
MicroPythonのVM上では/pyboard
がMicroPython実行時のカレントディレクトリ的な扱いになっています。
MicroPythonではmain.pyがエントリーポイントになって実行されるので、名前はそのままで大丈夫です。
cp main.py /pyboard
実行終了後はCtrl+dでrshellから抜けて大丈夫です。
動作検証
例によってPlatformIOのシリアルモニタにて、シリアル通信を行います。
pio device monitor -b 115200
でシリアル通信を確立させた後、M5Stack Core2のリロードボタンを押して再起動させることで、先程書き込んだmain.pyを実行させます。
画像の通りAPIを叩いた結果がシリアル通信に出力されました。
今年の東京のクリスマスイブとクリスマスの温度変化が確認できています。
寒いですね……
ということで、MicroPython経由でESP32でネットワークアクセスをして情報を取得することができました!
他
mruby
mrubyは我らが日本を代表する、まつもとゆきひろさんが作られたRubyの組み込みシステム向けバージョンです。
またこれをさらに改善し、低性能なマイコンでも動作するようにしたmrubycというのもあります。
これも日本が開発の中心となっており、九州工業大学としまねソフト研究開発センターが共同開発で作っておられます。
今回掲載しようと試みましたが、環境構築に大苦戦し、再現性のある記事が書けなかったため断念しました。
mrubycではPlatformIOを用いた手法もあるようですが、mrubyもMicroPython同様、マイコンごとに環境構築の手法が異なり少し厄介です。
Rust
RISC-Vマイコンの登場とLLVMの活躍もあり、Rustでもマイコン開発ができるようになってきました。部分的ですが……
Interface 2023年5月号の特集にはstdなRustでM5Stamp C3Uを用いた開発手法が取り上げられています。私も読んでみて、かなり詳しく取り上げられていると感じましたので、Rustでマイコン開発に興味がある方はご覧になってみてください。
nightlyが必要だったりと多少手間はかかりますが、stdなRustでマイコン開発ができるのはとても魅力的です。
JavaScript
JavaScriptのマイコン向けエンジンはEspruinoやJerryScriptなど複数存在します。
今回検証や環境構築の解説は省きますが、あまりにも様々な環境で動くJavaScriptには脱帽せざるを得ません。
Python
紹介したMicroPythonとは異なるPythonのマイコン向けサブセットもいくつかあり、GitHubでのスター数ではMicroPythonが首位であるものの、教育用途のためのCircuitPythonや、Python2を8bitマイコンでも使えるようにしたPyMiteなどあるようです。
人間が海を見たがるように、プログラム言語もマイコンに回帰するのではないでしょうか。←?
速度を比較してみる
最後に定番の素数判定プログラムで測定してみます。
それぞれ2147483647が素数かを判定するのにかかった時間で、同じアルゴリズムを使用しています。
コンパイラオプションなどはデフォルトのままで、環境構築と書き込みはこの記事の手法、マイコンはM5Stack Core2を用いています。
使用したコード
package main
import (
"math"
"time"
)
func isPrime(num int) bool {
if num <= 1 {
return false
}
for i := 2; float64(i) <= math.Sqrt(float64(num)); i++ {
if num%i == 0 {
return false
}
}
return true
}
func main() {
start := time.Now()
result := isPrime(2147483647)
end := time.Now()
println("result: ", result)
println("time(micro): ", end.Sub(start)/1000)
}
#include <Arduino.h>
bool is_prime(int num)
{
if (num <= 1)
{
return false;
}
for (int i = 2; i <= sqrt(num); i++)
{
if (num % i == 0)
return false;
}
return true;
}
void setup()
{
Serial.begin(115200);
while (!Serial)
{
delay(100);
}
int start_time = micros();
bool result = is_prime(2147483647);
int end_time = micros();
Serial.print("result: ");
if (result)
{
Serial.println("prime");
}
else
{
Serial.println("not prime");
}
Serial.print("time(micro): ");
Serial.println(end_time - start_time);
}
void loop()
{
Serial.println("done");
delay(100000);
}
import math
import utime
def is_prime(num):
if num <= 1:
return False
for i in range(2, int(math.sqrt(num)) + 1):
if num % i == 0:
return False
return True
start = utime.ticks_us()
result = is_prime(2147483647)
end = utime.ticks_us()
print("result: ", result)
print("time(micro): ", utime.ticks_diff(end, start))
結果は下記のとおりです。
言語 | 実行時間の平均 (μs) |
---|---|
TinyGo v0.30.0 | 5659 |
C/C++ (PlatformIO v6.1.11) | 200937 |
MicroPython v1.21.0 | 912474 |
グラフからも分かる通り、MicroPythonがもっとも遅く、TinyGoが最速となりました。
MicroPythonはインタプリタなので遅いのは妥当な結果ですが、TinyGoがC/C++(PlatformIO)より早いというのは意外です。
コメントにて@fujitanozomuさんよりご指摘いただいたように,C/C++が異常に遅い原因についてはコードに問題があります.
一見普通に実装したように見えてアセンブラレベルではcall命令が多用されており,またsqrtfでなくsqrtを用いるなどオーバーヘッドが多くありました.
最終的に
bool is_prime(int num)
{
+ int num_sqrt = sqrtf(num);
- for (int i = 2; i <= sqrt(num); i++)
+ for (int i = 2; i <= num_sqrt; i++)
{
if (num % i == 0)
return false;
}
return true;
と変更することで,3625μsという最速の実行が可能となりました.
パフォーマンス改善は奥が深い……
所感
マイコンの開発手法はたくさん増えましたが、かなり混沌としている印象を受けました。
同じ言語でもマイコン向け言語が複数あったり、同一のマイコン向け言語でもマイコンにより書き込み方が違うなど様々です。
一方でPlatformIOを用いることでC/C++でも快適に開発ができるなど、単に言語の書き心地だけでなく環境全体が重要だと感じさせられました。
おわり
卒業研究が非常にまずい時期ですが、アドベントカレンダーをうまく利用して卒業研究とも関連のある分野を深めることができて一石二鳥になりました!
やはり締切は人類最大の発明ですね……
ちょっと宣伝です。日常的にブログを趣味でやっていて、記事はblog.usuyuki.netに書いています!
今回はQiitaのアドベントカレンダーなので、Qiitaに書いてみました!
最後までご覧いただきありがとうございました🙇🙇🙇