Linux での開発#
Linux でも Windows / macOS と同じコマンドで全章をビルド・書き込みできます。
違うのはポート名が /dev/ttyACM0 のような形になることと、インストール方法くらいです。
ただし Linux でしか出ないエラーがいくつかあり、そこで止まると原因が 分かりにくいので、実際の出力とあわせてまとめます。あわせて、書籍では扱って いない Python からボードと通信する方法もここに置きます。
注釈
このページの出力例は Arduino UNO R4 WiFi の実機を Ubuntu 24.04 LTS (x86_64) に つないで取ったものです。他のディストリビューションでの違いは「9. Ubuntu 以外」に まとめてあります。
1. 環境をつくる#
端末(ターミナル)を開く#
このページのコマンドは、すべて端末に打ち込みます。Ubuntu では Ctrl + Alt + T で開きます。左下のアプリ一覧から「端末」を探しても同じです。
端末を開いて、次の uv のインストールを打ち込んだところ。入れた直後は
まだ PATH が通っておらず、source してから uv --version が通ります#
ユーザ名@マシン名:~$ のような行(プロンプト)が出れば準備完了です。
$ の右にコマンドを打って Enter を押すと実行されます。以降、行頭の $ は
「ここに打つ」という意味なので、$ 自体は入力しません。
uv を入れる#
uv は Python の環境とパッケージをまとめて 面倒みてくれる道具です。PlatformIO をこれで動かします。
curl -LsSf https://astral.sh/uv/install.sh | sh
インストール先は ~/.local/bin です。PATH は、コマンド名だけで実行できるように
OS が探しに行くフォルダの一覧のことです。入れた直後のシェルはまだ PATH を
知らないので、そのまま uv を打つと command not found になります。
端末を開き直すか、次を実行して PATH に反映してください。
source ~/.bashrc # zsh なら ~/.zshrc
uv --version
source ~/.local/bin/env でも PATH だけ通せます(上の GIF はこちらです)。
このとき Ubuntu は sudo snap install astral-uv を勧めてきますが、従わないで
ください。uv は入っていて、PATH がまだ通っていないだけです。
git が入っていない場合は先に入れてください。
sudo apt install -y git # Ubuntu / Debian
sudo を付けたコマンドは管理者の権限で実行されるので、自分のログインパスワードを聞かれます。
打っている間は画面に何も表示されませんが、そのまま打って Enter を押してください。
サンプルコードを取ってくる#
git clone https://github.com/iory/learning-os-from-arduino.git
cd learning-os-from-arduino/docs/os-on-arduino/code
uv sync
uv sync で PlatformIO と pyserial が入ります。コンパイラは入れなくて
かまいません。 PlatformIO が ~/.platformio へ自前で取ってきます
(初回のビルドで数百 MB ぶん、数分かかります)。
2. ボードをつなぐ前に — udev ルールを入れる#
先にこれをやってください。 udev(USB 機器を挿したときの権限などを決める Linux の仕組み)にルールを足します。 あとで説明する 2 つの問題(権限と ModemManager)が、これ 1 つでまとめて解決します。
curl -fsSL https://raw.githubusercontent.com/platformio/platformio-core/develop/platformio/assets/system/99-platformio-udev.rules \
| sudo tee /etc/udev/rules.d/99-platformio-udev.rules >/dev/null
sudo udevadm control --reload-rules && sudo udevadm trigger
入れたあとは USB を一度抜き挿ししてください。 既につないでいるデバイスには 自動で反映されません。
これは PlatformIO 公式の udev ルールです (公式の説明)。
3. ボードを見つける#
uv run pio device list
/dev/ttyACM0
------------
Hardware ID: USB VID:PID=2341:1002 SER=B43A45B9F1CC LOCATION=1-1:1.1
Description: UNO WiFi R4 CMSIS-DAP - TinyUSB CDC
2341:1002 が Arduino UNO R4 WiFi です。2341 が Arduino のベンダ ID で、
あとで Python からポートを自動検出するときに使います。
/dev/ttyS* がたくさん出ることがありますが、これはマザーボード上の
シリアルポートで、ボードとは関係ありません。ttyACM の行を見てください。
番号は固定ではありません
/dev/ttyACM0 の 0 は「何番目に挿されたか」でしかありません。
抜き挿ししたり、ほかの USB シリアル機器を先につないだりすると、
同じボードが /dev/ttyACM1 になります。書き込みが急に
No such file or directory で失敗したら、まずこれを疑ってください。
いま何番かは、こう調べます。
uv run pio device list # Hardware ID に 2341:1002 と出る行
ls /dev/ttyACM* # 手早く見るだけなら
どれが今挿したボードなのか確実に知りたいときは、カーネルのログを 流しっぱなしにしておいて、その場で挿すのがいちばん早いです。
sudo dmesg --follow # 止めるときは Ctrl-C
この状態でボードを挿すと、その瞬間にこの行が出ます。
cdc_acm 1-1:1.1: ttyACM0: USB ACM device
ttyACM0 の部分が、いま挿したボードの名前です。抜くと USB disconnect が
出るので、抜き挿しすれば番号の対応が確実に分かります。
sudo を付けるのは、Ubuntu では dmesg が root 専用(kernel.dmesg_restrict=1)
だからです。付けずに実行すると
read kernel buffer failed: Operation not permitted になります。
Windows でも同じことが起きます(COM3 が COM5 になる)。
番号に依存しない書き方は「7. Python からボードと通信する」にあります。
4. ビルドして書き込む#
uv run pio run -d 01_boot -t upload
ポートを確認して書き込むところ。実機の Ubuntu 24.04 で流したものです#
Auto-detected: /dev/ttyACM0 のとおり、ふつうはポートを指定しなくても
見つかります。ほかに USB シリアル機器がつながっていて誤検出されるときだけ
--upload-port /dev/ttyACM0 を足してください。
5. シリアルモニタで見る#
uv run pio device monitor -b 115200
第1章は S を 1 文字送ると出力が始まります。モニタが開いたら S を
打ってください。
なぜ S を待たせているのか
ボードは電源が入った瞬間から全速力で走り出します。一方こちらは、書き込みが
終わってからシリアルモニタを開くまでに数秒かかります。第1章のスケッチは
setup() の中で一度だけ印字して終わるので、普通に書くとモニタが開く前に
出力が終わってしまい、何も見えません。しかも UNO R4 WiFi は、シリアル
ポートを開いてもリセットされません(USB 通信はボード上の ESP32-S3 が
受け持っていて、プログラムが動く RA4M1 とは独立しているためです)。
つまりモニタを開き直しても、最初からやり直してはくれません。
そこで「PC 側の準備ができた」ことをボードに伝える必要があります。
S はその合図です。スケッチは
while (Serial.read() != 'S') { delay(1); }
で足踏みし、S が届いてから印字を始めます。だからモニタを開くのが何秒
遅れても、出力を最初から見られます。
Linux ではもう一つ理由があります。ModemManager が書き込み直後にポートを
開きにくるため、while (!Serial) だけでは「ホストが繋がった」と誤って
判定されてしまいます(「6. Linux でつまずくところ」を参照)。
書籍では 01_boot / 01_boot_vector_dump / 応用編の adv3_heap と
adv4_fs がこの形です。ずっと印字し続ける章(第2章など)には要りません。
=== Boot Sequence Check ===
bss_var (should be 0): 0
data_var (should be 12345): 12345
=== Memory Addresses ===
&bss_var: 0x200000C4
&data_var: 0x20000000
VTOR: 0x20007F00
MSP: 0x20007EC8
If you see this, boot sequence completed!
モニタを終了するには Ctrl-C を押します。
6. Linux でつまずくところ#
Permission denied: /dev/ttyACM0#
udev ルールを入れずにボードをつなぐと、こうなります。
$ cat /dev/ttyACM0
cat: /dev/ttyACM0: Permission denied
Python からだと、こう出ます。
PermissionError: [Errno 13] Permission denied: '/dev/ttyACM0'
原因はデバイスファイルの所有グループです。
$ ls -l /dev/ttyACM0
crw-rw---- 1 root dialout 166, 0 Aug 31 01:51 /dev/ttyACM0
# ^^^^^^^ このグループに入っていないと開けない
「2. udev ルール」を入れていれば、この問題は起きません。
PlatformIO のルールが MODE="0666" を設定するので、誰でも開けるように
なります。
$ ls -l /dev/ttyACM0
crw-rw-rw- 1 root dialout 166, 0 Aug 31 02:12 /dev/ttyACM0
udev ルールを使わない場合(Arduino IDE だけを使う場合など)は、
自分を dialout グループに入れてください。
sudo usermod -aG dialout $USER
このあと一度ログアウトして入り直す必要があります。 グループの変更は
ログインし直すまで効きません。いま開いているターミナルだけで先に有効に
したいときは newgrp dialout が使えます。
ModemManager がボードを掴む#
Ubuntu には ModemManager という常駐サービスがいて、USB シリアル機器が
現れると「モバイル回線のモデムかもしれない」と考えて調べにいきます。
Arduino はモデムではないのですが、つないだ直後にポートを開かれるため、
setup() の中でしか出力しないスケッチだと、こちらがモニタを開くより前に
出力が終わってしまいます。
書籍の第1章が S を待つ仕組みになっているのは、これが理由です。
いま自分の環境がどうなっているかは、こう調べます。
$ udevadm info -q property -n /dev/ttyACM0 | grep ID_MM
ID_MM_CANDIDATE=1
ID_MM_CANDIDATE=1 だけが出る場合、ModemManager の対象になっています。
「2. udev ルール」を入れると、こうなります。
$ udevadm info -q property -n /dev/ttyACM0 | grep ID_MM
ID_MM_CANDIDATE=1
ID_MM_DEVICE_IGNORE=1
ID_MM_PORT_IGNORE=1
ID_MM_DEVICE_IGNORE=1 が付けば、ModemManager はこのデバイスに触りません。
なぜ PlatformIO のルールで効くのか
公式ルールの Arduino 向けの行は idProduct を [08][023]* に限っていて、
UNO R4 WiFi の PID 1002 はこの条件に一致しません。
効いているのは、その下にある別の行です。
ATTRS{product}=="*CMSIS-DAP*", MODE="0666", ENV{ID_MM_DEVICE_IGNORE}="1", ENV{ID_MM_PORT_IGNORE}="1"
UNO R4 WiFi は USB の製品名が UNO WiFi R4 CMSIS-DAP なので、
PID ではなく製品名でマッチしています。
ModemManager をまったく使っていない(モバイル回線を使わない)機械なら、 止めてしまう手もあります。ただし udev ルールで足りるので、ふつうは不要です。
sudo systemctl disable --now ModemManager
モニタを開いたままだと書き込めない#
シリアルポートを開けるのは同時に 1 つだけです。モニタを開いたまま upload
すると失敗します。Windows の PermissionError にあたるもので、Linux では
Device or resource busy と出ます。モニタを Ctrl-C で閉じてから
書き込み直してください。
Arduino IDE のシリアルモニタや、自分で書いた Python スクリプトが掴んで いるときも同じです。
そのほか#
brltty。点字ディスプレイ用の常駐で、Ubuntu には既定で入っています。 一部の USB シリアル機器を横取りするため、/dev/ttyACM0が現れてすぐ 消えるようなら疑ってください(systemctl status brlttyで確認できます)。 UNO R4 WiFi では確認されていませんが、USB-シリアル変換器を使う場合は 起こりえますUSB ハブ経由だと書き込みに失敗することがあります。PC 本体に直挿しを 試してください
ボードが応答しなくなったら RESET ボタンを素早く 2 回押すと ブートローダ(書き込みを受け付けるためにマイコンに常駐している小さな プログラム)に入ります
7. Python からボードと通信する#
書籍ではシリアルモニタで見るところまでしか扱いませんが、実際に何か作るときは
PC 側のプログラムからボードと話したくなります。uv sync で入る
pyserial を使います。
最小の例#
第5章(05_shell) を書き込んだ状態で試してください。
シェルが動いているので、ps を送るとタスク表が返ってきます。
import time
import serial
import serial.tools.list_ports
# Arduino の USB ベンダ ID。ポート名を書かずに済む
port = next(p.device for p in serial.tools.list_ports.comports() if p.vid == 0x2341)
print("見つかったポート:", port)
ser = serial.Serial(port, 115200, timeout=0.2)
time.sleep(1.6) # 挿した直後は取りこぼすことがあるので少し待つ
ser.reset_input_buffer()
ser.write(b"ps\n")
time.sleep(1.0)
print(ser.read(4096).decode("utf-8", errors="replace"))
ser.close()
上のコードを docs/os-on-arduino/code フォルダに hello_serial.py という名前で保存してから、実行します。
uv run python hello_serial.py
hello_serial.py を実行したところ#
見つかったポート: /dev/ttyACM0
ps
ID NAME STATE CPU%
-- ---- ----- ----
0 idle READY 100%
1 LED1 BLOCKED 0%
2 LED2 BLOCKED 0%
3 Heartbeat BLOCKED 0%
4 Shell RUNNING 0%
>
打ちながら試す#
送るコマンドを決め打ちにせず、その場で打ちたいときは、読む側を別スレッドに するだけの短いコンソールが書けます。
"""ボードのシェルと対話する、最小のシリアルコンソール。Ctrl-D で終了。"""
import sys
import time
import threading
import serial
import serial.tools.list_ports
port = next(p.device for p in serial.tools.list_ports.comports() if p.vid == 0x2341)
print(f"connected: {port} (Ctrl-D で終了)")
ser = serial.Serial(port, 115200, timeout=0.2)
time.sleep(1.6) # 挿した直後は取りこぼすことがあるので少し待つ
ser.reset_input_buffer()
def reader(): # ボードからの出力を流し続ける
while True:
data = ser.read(4096)
if data:
sys.stdout.write(data.decode("utf-8", errors="replace"))
sys.stdout.flush()
threading.Thread(target=reader, daemon=True).start()
# for line in sys.stdin: は読み溜めするので、対話では readline を使う
for line in iter(sys.stdin.readline, ""): # 打った行をボードへ送る
ser.write(line.encode())
ser.flush()
上のコードを docs/os-on-arduino/code フォルダに serial_console.py という名前で
保存してから、実行します。
uv run python serial_console.py
ps でタスク表を見て、kill 1 で LED1 を止め(SUSPEND に変わります)、
exec 1 で戻すところ。実機の Ubuntu 24.04 で打ったものです#
kill 1 を打つと基板の L LED の点滅が止まり、exec 1 で再開します。
画面の STATE と手元の LED が一致していることを確かめてください。
覚えておくと詰まらない 3 つ#
ポート名を書かない。 上の p.vid == 0x2341 で探す書き方なら、
Linux / macOS / Windows のどれでも同じコードが動きます
(/dev/ttyACM0 / /dev/cu.usbmodem... / COM3 のどれが返るかの違いだけです)。
開いたら少し待つ。 UNO R4 WiFi はポートを開いてもリセットされませんが、
挿した直後は、最初の出力やコマンドを取りこぼすことがあります。上のコードの
time.sleep はそのためです。
ポートは 1 つのプロセスしか開けません。 PlatformIO のモニタや Arduino IDE を閉じてから実行してください。
8. 四脚ロボットを PC から動かす#
第13章まで読み飛ばしてかまいません。
第13章の四脚ロボットは、組み立てと校正を PC 側からやるほうが早いです (値が全部画面に出て、直すたびに焼き直さなくて済みます)。
cd 13_quadruped/host # docs/os-on-arduino/code から
uv run python quad_host.py scan # バスに何個サーボがいるか
uv run python quad_host.py calibrate # zero と sign を測る -> calib.json
uv run python quad_host.py stand # home 姿勢を保持
uv run python quad_host.py teleop # w/s/a/d で歩かせる
ポートを明示したいときは --port /dev/ttyACM0 を足してください。
組み立てと学習の全体像は 歩行を学習させる(強化学習) にあります。
9. Ubuntu 以外#
このページは Ubuntu 24.04 で確認したものです。他のディストリビューションでも 手順はほぼ同じで、違うのはパッケージの入れ方とグループ名だけです。
Ubuntu / Debian |
Fedora |
Arch |
|
|---|---|---|---|
git を入れる |
|
|
|
シリアルのグループ |
|
|
|
uv の導入と PlatformIO のツールチェインは、どのディストリビューションでも
同じです。 コンパイラを自前で用意する必要はありません。
自分の環境でどのグループが要るかは、これで分かります。
$ ls -l /dev/ttyACM0
crw-rw---- 1 root dialout 166, 0 Aug 31 01:51 /dev/ttyACM0
# ^^^^^^^ ここに出るのが、入るべきグループ
udev ルールを入れれば、そもそもグループを気にしなくてよくなります。
10. 全章をまとめて確認する#
同梱のスクリプトで、全章をビルドして実機に書き込み、出力が書籍の記載どおりかを 自動で突き合わせられます。
uv run python scripts/verify_chapters.py
詳しくは 手元のボードで全章をまとめて確認する を見てください。