Arduino Pico (earlephillhower) で過剰なシリアル通信によるフリーズを防ぐ

Mido のアバター画像

Mido

ArduinoRaspberry Pi

3分

686字

困ったこと

Arduino IDEからRaspberry Pi Pico (無印 / 2) を触るためのArduino Picoというものがある。

このとき、USB 2.0の限界スループット1に近い多くのデータを送受信しているとまれにRaspberry Pi Pico側がフリーズしてしまうことがあった。

この不具合は双方向の送受信をしている場合2に限って発生し、Pico SDKのWatchdogを有効化しても改善されず、USBケーブルを抜き差しするまで復帰しない。

原因

根本原因は次のIssueで報告されていた。

Arduino Picoは内部のUSBスタック実装にTinyUSBを使用しており、USBタスクの割り込み処理でMutexが競合状態になることによるデッドロックが原因だった。
単なるハングではなくスピンロックに持ち込むハングのため、ウォッチドッグが反応しない。

tusb_config.hのキューサイズを上げることで発生頻度を下げることはできるが、根本的な解決には至らなかった。

対処方

次のパッチをTinyUSBのソースコードに適用する。

--- a/src/tusb.c
+++ b/src/tusb.c
@@ -118,7 +118,7 @@ bool tu_edpt_claim(tu_edpt_state_t* ep_state, osal_mutex_t mutex)
// pre-check to help reducing mutex lock
TU_VERIFY((ep_state->busy == 0) && (ep_state->claimed == 0));
- (void) osal_mutex_lock(mutex, OSAL_TIMEOUT_WAIT_FOREVER);
+ (void) osal_mutex_lock(mutex, OSAL_TIMEOUT_NORMAL); //OSAL_TIMEOUT_WAIT_FOREVER);
// can only claim the endpoint if it is not busy and not claimed yet.
bool const available = (ep_state->busy == 0) && (ep_state->claimed == 0);

Mutexに一定期間のタイムアウトを設けることで、競合状態でも処理を続行できるようにしている。

Arduino Picoの場合は、デフォルトのTinyUSB実装がプリコンパイルされたライブラリ (libpico.aに同梱) を用いているため、簡単にはこの方法を適用できない。
代わりに、ソースコードからTinyUSBを都度コンパイルする実装であるAdafruit TinyUSBをUSBスタックとして扱うようにして対処する。

PlatformIOならば$HOME/.platformio/packages/framework-arduinopico/libraries/Adafruit_TinyUSB_ArduinoにあるTinyUSBのソースコードを上書きすればよい。

私の場合は次のようなシェルスクリプトを書いて、楽にパッチを当てられるようにしている。

#!/usr/bin/env bash
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
PATCH_SET=(
"$HOME/.platformio/packages/framework-arduinopico/libraries/Adafruit_TinyUSB_Arduino"
"$SCRIPT_DIR/fix-tusb-deadlock.patch" # 上記のdiffを保存したファイル (スクリプトと同じ場所に配置)
)
apply_patch() {
local root_path="$1"
local patch_file="$2"
if [ ! -d "$root_path" ]; then
echo "Error: Directory $root_path does not exist."
exit 1
fi
if [ ! -f "$patch_file" ]; then
echo "Error: Patch file $patch_file does not exist."
exit 1
fi
echo -n "Applying $(basename "$patch_file") to $root_path..."
if git -C "$root_path" apply --check "$patch_file" &>/dev/null; then
git -C "$root_path" apply "$patch_file"
echo " done."
else
echo " already applied or cannot be applied."
fi
}
main() {
local total_patches=${#PATCH_SET[@]}
for ((i=0; i<total_patches; i+=2)); do
local root_path="${PATCH_SET[i]}"
local patch_file="${PATCH_SET[i+1]}"
apply_patch "$root_path" "$patch_file"
done
}
main

最後に、platformio.inibuild_flagsに次のフラグを追加して、Adafruit TinyUSBを使用するようにする。

build_flags =
...
'-DUSE_TINYUSB'

まとめ

この原因にたどり着くまで相当な時間がかかった。特に副作用は見られていないので、早くこの修正がTinyUSBに取り込まれれば良いのだが。

脚注

  1. およそ480Mbps。 ↩︎

  2. 処理内容にかかわらず、単純にエコーするだけの通信でも問題が発生した。 ↩︎