困ったこと
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.iniのbuild_flagsに次のフラグを追加して、Adafruit TinyUSBを使用するようにする。
build_flags = ... '-DUSE_TINYUSB'まとめ
この原因にたどり着くまで相当な時間がかかった。特に副作用は見られていないので、早くこの修正がTinyUSBに取り込まれれば良いのだが。
