TMC2209 アナログデバイセズのモータードライバー StallGuard/CoolStep お試し。
これまで DRV8835, DRV8825 や A4988 といった Stepper Driver は触ったことがあるのですが、これらは単純に DIR/STEP を入力して回す単純なものでした。その代わり、電流を決定するにその設定の Vref を調整すると言った手間が生じます。
最近(でもないのかもしれませんが) 3D プリンタ界隈では "センサーレスホーミング" と言った機能があるらしく、どうやら可動域の限度をリミットスイッチではなく物理的に生じる負荷抵抗をモータドライバが検知して既存のリミットスイッチを不要とする機能があるようです。
へぇ、面白そう
と思いつつ、色々調べてみると、一般的にはストール検知(stall detection)と呼ばれる機能でここ数年各メーカのモータードライバの一部が持つ機能のようでした。手持ちのモータードライバを漁ってみると、丁度 Analog Devices の TMC2209 を使った MKS TMC2209 v2.0 のモジュールがあり、これがセンサーレスホーミングに対応していたので ESP32 で制御してストール検知(StallGuard)を試してみました。
はじめに
TMC2209 には StallGuard/CoolStep と呼ばれる機能があります。3D プリンターはこの機能を使ってセンサーレスホーミングを実現しています。
StallGuard/CoolStep を利用するには UART を用いて TMC2209 を制御する必要があります。同時に UART 経由でモーターへの電流を設定することが可能で、これまでの Vref への可変抵抗を用いた調整が不要になります。
また、今回調べた範囲では UART を用いて回転方向と速度を設定してモーターを回転させることは出来るものの、ステップ数で管理した回転の制御というのは出来ないようでした。
なので
- モーターの電流、 StallGuard/CoolStep の設定や利用は UART
- モーターの回転の方向や速度、回転位置(ステップ数)管理は DIR/STEP
として制御するものとしました。
制御側のマイコンには ESP32 を用いていますが Arduino Framework を使っているので ESP32 に限らず利用するライブラリの互換範囲で動作すると思います。
ハードウェア
使用部品
- NodeMCU-32S
- MKS TMC2209 v2.0 ブレッドボードに挿すのに邪魔だったので DIAG/INDEX に取り付けられていたピンは外しました。
- NEMA17 Stepper Motor (BJ42D22-23V01) 3Dプリンタの余り部品/スペック不明、抵抗値を測ったところ 3Ω程度でした
- 1kΩ抵抗(UART TX/RX 分離用)
- ブレッドボード/配線部材
今回モーター駆動の電源は USB の 5V を使っています。(お勧めはません。自己責任で)
配線
TMC2209 の UART は一つの端子で TX/RX を兼ねています。マイコン側の UART は TX/RX それぞれあり、マイコン側の TX は 1kΩの抵抗を挟んで TMC2209 と繋ぐ必要があります。これをしないと、マイコンからデータは送信できるが TMC2209 からのデータが受信できないというトラブルになります。

MKS TMC2209 v2.0 のモジュールは2つの UART ピンがありますが、回路図を確認すると TMC2209 の UART から 0Ωの抵抗(R8)を挟んで繋がれており、別途 1kΩの抵抗が必要になります。(R8 が 0Ωなのは実装ミスなんじゃなかろうか) TMC2209 を使った別のモジュール、例えば FYSETC の Silent2209 の v2.1 以降のモジュールは 1kΩの抵抗を介した UART PIN が提供されており、そのまま繋ぐことが出来ます。利用するモジュールのマニュアルや回路図を確認の上配線してください。
TMC2209 の UART を含む信号レベルは VCC_IO (モジュールでは VDD)の入力で決定します。ESP32 の信号レベルは 3.3v なので NodeMCU の 3.3v 出力を繋ぎます。
ENBLE/DIR/STEP はこれまで一般的なモータドライバ同様、GPIO のピンに接続します。
DIAG は StallGuard の機能による出力となり、マイコン側の GPIO 入力としてピンに接続します。
接続
| NodeMCU32S | MKS TMC2209 V2.0 |
|---|---|
| HW Serial2 RX GPIO16 | PDN-UART |
| HW Serial2 TX GPIO17 | PDN-UART(1kΩ抵抗経由) |
| GPIO18 | DIR |
| GPIO19 | STEP |
| GPIO21 | ENABLE |
| GPIO23 | DIAG |
| 3.3v | VDD (VCC_IO) |
| VIN(5v) | VM (VS) |
| GND | GND |
実体配線


ソフトウェア
開発環境は vscode/devcontainer + PlatformIO + Arduino Framework としています。
利用ライブラリ
- TMCStepper UART 経由で TMC2209 を設定するライブラリです。
- FastAccelStepper STEP/DIR/ENABLE を用いてモーターの回転と位置を制御/管理するライブラリです。
以上2つのライブラリを利用します。 platformio.ini は
[env:nodemcu-32s] platform = espressif32 board = nodemcu-32s monitor_speed = 115200 framework = arduino lib_deps = SPI gin66/FastAccelStepper @ ^0.30.11 teemuatlut/TMCStepper @ ^0.7.3
となりました。
プログラミング
TMCStepper の TMC2209 のドライバのコンストラクターの引数は、Serial Port, RSense, Address になります。
TMC2209 で設定した Serial2 を baudrate を指定して初期化し、TMC2209 ドライバを開始、基本的な設定を行います。
- toff を設定し TMC2209 内部のソフトウェアドライバを開始します。
- rms_current を用いてモーター電流を設定します。(今回は 900mA)
- microsteps を用いてマイクロステップを設定します。(今回は 4 を設定し 1/4 としています)
CoolStep/StallGuard を設定します。
CoolStep は負荷に対して電流を制御する機能です。CoolStep に対する設定は負荷に対する上限と下限の閾値を指定します。設定の上限を超える負荷を検知すると設定されたモーターへの供給電流の値とその 1/2 の範囲でモータへ供給する電流を増やし、負荷が下限より下がると電流を減らします。これによって消費電力を下げモータの無駄な発熱等の回避が可能になります。
StallGuard は負荷の限界を設定し、負荷がその設定より上回ると DIAG ピンに出力を行います。
それぞれの動作に対する負荷は TMC2209 が生成する SG_RESULT と呼ばれる値と比較されます。SG_RESULT は 負荷が大きくなると値が小さくなります。- CoolStep が動作する最小速度閾値である 20bit の TCOOLTHRS を設定します。値はパルス期間のクロック数なので値が大きいほど速度は遅いという事になります。今回は動作確認なので設定可能な下限(最大値)である 0xFFFFF を設定します。
- CoolStep がモーターに供給する電流を可能であれば上げ始める負荷の閾値を設定します。閾値は semin で指定されます。semin で指定される N の N * 32 の値を負荷の値(SG_RESULT)と比較され、負荷の値が閾値より小さい(負荷が大きい)と電流を増やそうとします。
- CoolStep がモーターに供給する電流を可能であれば下げ始める負荷の閾値を設定します。閾値は semax で指定されます。semax で指定される M の (M + N(semin) + 1) * 32 の値を負荷の値(SG_RESULT)と比較され、負荷の値が閾値より大きい(負荷が小さい)と電流を減らそうとします。
- StallGuard が DIAG ピンに出力する閾値を SGTHRS で設定します。SHTHRS に与える値は閾値の半分(/ 2)の数値になります。
FastAccelSteooer を設定しモータを動作させます。
- STEP/DIR/ENABLE ピンを FastAccelStepper にアサインします。
- 自動で ENABLE ピンを制御させるために setAutoEnable を true にします。
- モーターを回転させる最大速度を setSpeedInHz を用いて周波数で設定します。
- モーダーの速度を変化させる加速度を setAcceleration を用いて設定します。
- move を用いて動作ステップ数を指定しモーターを動作させます。
出力ループ
CoolStep と StallGuard の動作を確認するため
を出力します。出力フォーマットは vscode の Teleplot を用いてグラフ化する事として対応させます。
実装
#include <Arduino.h> #include "TMCStepper.h" #include "FastAccelStepper.h" #define dirPinStepper 18 #define stepPinStepper 19 #define enablePinStepper 21 #define diagPin 23 FastAccelStepperEngine engine = FastAccelStepperEngine(); FastAccelStepper *stepper = NULL; TMC2209Stepper driver(&Serial2, 0.11f, 0); // RSense 0.11Ω, UART Address 0 void setup() { pinMode(diagPin, INPUT); Serial.begin(115200); constexpr uint8_t us = 1 << 2; Serial2.begin(115200); // HW UART Serial2 (RX = GPIO16, TX = GPIO17) driver.begin(); driver.toff(5); // Enables driver in software driver.rms_current(900); // motor current mA driver.microsteps(us); driver.TCOOLTHRS(0xFFFFF); driver.semin(5); driver.semax(2); driver.SGTHRS(100); engine.init(); stepper = engine.stepperConnectToPin(stepPinStepper); if (stepper) { stepper->setDirectionPin(dirPinStepper); stepper->setEnablePin(enablePinStepper); stepper->setAutoEnable(true); stepper->setSpeedInHz(250 * us); stepper->setAcceleration(25 * us); stepper->move(4096 * us ) ; } } void loop() { delay(100); Serial.printf(">SG_RESULT:%d\n", driver.SG_RESULT()); Serial.printf(">motor current(mA):%d\n", driver.cs2rms(driver.cs_actual())); Serial.printf(">Speed(Hz):%ld\n", stepper->getCurrentSpeedInMilliHz() / 1000); Serial.printf(">DIAG Pin:%d\n", digitalRead(diagPin) * 100); Serial.printf(">CoolStep lower threshold:%d\n", 5 * 32); Serial.printf(">CoolStep upper threshold:%d\n", (5 + 2 + 1) * 32); Serial.printf(">StallGuard threshold:%d\n", 100 * 2); }
実行結果
まずそのまま実行し Telepolot を用いて出力からグラフを描画します。

グラフは時間経過を X 軸として Y 軸はそれぞれ
- 黄色: Speed(Hz), モーターの回転速度としての STEP に与えられるパルスの周波数
- 緑色: motor current(mA), CoolStep によって制御されるモーターへの供給電流(mA)
- 橙色: SG_RESULT, TMC2209 で検出される負荷の値
- 青色: CoolStep upper threshold, semax で設定される CoolStep の負荷上限閾値(=256 固定値)
- 赤色: StallGuard threshold, semin で設定される DIAG ピンへの出力を制御する StallGuard の閾値(=200 固定値)
- 黄緑色: CoolStep lower threshold, semin で設定される CoolStep の負荷下限閾値(=160 固定値)
- 紫色: Diag Pin, StallGuard で制御される DIAG ピン出力
になります。
CoolStep
橙色の SG_RESULT と、青色の CoolStep 上限閾値、黄緑色の CoolStep 下限閾値、それらに対する結果として緑色のモーターへの供給電流を見ます。
- モーターが停止状態から動作を開始し加速を始め SG_RESULT が CoolStep 下限閾値未満の高負荷時は設定された供給電流値まで増加させています。
- 加速が続きモーターがある程度の速度になり負荷の値である SG_RESULT が CoolStep 上限閾値以上の低負荷になるとモーターへの供給電流が下がり始め、供給電流の下限値である 1/2 まで下がります。
- 動作が続いて指定された動作ステップ数に近づき、減速が開始されると再び負荷が上がり始め SG_RESULT の値が下がり始めます。
- 減速が続き、負荷である SG_RESULT の値がが CoolStep 下限閾値未満の高負荷状態となり再び供給電流を増加させます。
といった、加速と減速の負荷の変動とその負荷の各閾値に対する供給電流の制御という CoolStep の挙動が確認できます。
StallGuard
橙色の SG_RESULT と、赤色の StallGuard 閾値、それらに対する結果として 紫色の DIAG ピン出力を見ます。
動作は非常に単純で、負荷の値である SG_RESULT が閾値未満の高負荷状態の時、DIAG ピンに出力されるというだけの挙動になります。グラフもその挙動を示しています。
外部から負荷を与えた時の動作
次に加速/減速の負荷ではなく、等速動作中に外部から負荷を与えた時の挙動を確認してみます。値は 0.1 秒単位で取得しているので瞬間的に下回る/上回るといった状態の観測にやや難がありますが‥

- 負荷を与えると SG_RESULT の値が下がります。
- SG_RESULT の値がCoolStep の負荷上限閾値以上の低負荷になると供給電流は設定電流値の半分まで減り続けます
- SG_RESULT の値がStallGuard の負荷閾値未満の高負荷になると DIAG ピンに出力されます
- SG_RESULT の値がCoolStep の負荷上限閾値以上の低負荷になると供給電流は設定電流値の半分まで減り続けます
という StallGuard/CoolStep の挙動が確認できます。
秋葉原(秋月電子)で部品を揃えてラジオを作ろう
令和のラジオづくりを体験してみようと思いやってみました。
CH32V003 RISC-V マイコン
- J4M6 8ピン SOP パッケージ
- F4P6 20ピン SSOP パッケージ
それぞれを DIP 変換基板へ実装し大きさ比較。

開発環境
vscode + PlatformIO
Visual Studio Code の PlatformIO のプラグインをインストールし、PlatformIO の設定から ch32v のプラットフォーム拡張をインストールします。また、 WCH-LinkE へのパーミッションを udev の設定で調整します。一通りの設定手順は PlatformIO/CH32V のドキュメントに記述されています。
wlink
wlink は Rust で実装された WCH-LinkE を用いて CH32V を操作する cli tool です。WCH-LinkUtility が Windows のみなので、特に Linux で Delay_Ms(1000); はその代わりとして重宝する tool です。
プログラミング
折角のはじめての CH32V なので Arduino 互換ではなく NoneOS を使ってみました。まとまったドキュメントが見つけられませんでしたが、UART Serial の Hello World からはじめて I2C を操作するところまで動きました。 プログラミングと動作確認は 20ピンの F4P6 で行っています。J4M6 はピン数が足りずデバッグに手間取ったりするので F4P6 で一通り動作させてから J4M6 に載せると言った手順が良いと思います。
Hello World
#include <ch32v00x.h> #include <debug.h> int main(void) { NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2); SystemCoreClockUpdate(); Delay_Init(); USART_Printf_Init(115200); while(1) { printf("Hello world\n"); Delay_Ms(1000); } }
I2C
データシートを見ると CH32V003 は PC1/SDA, PC2/SCL となっているので GPIOC の Pin1, Pin2 にクロックを設定し I2C1 を有効にします。
class i2c { I2C_TypeDef* interface; public: i2c(I2C_TypeDef *i2c, uint32_t clock); void write(uint8_t addr, const uint8_t* data, size_t length); void read(uint8_t addr, uint8_t reg, uint8_t* data, size_t length); }; i2c::i2c(I2C_TypeDef *i2c = I2C1, uint32_t clock = 400000) : interface(i2c) { RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOC | RCC_APB2Periph_AFIO, ENABLE); RCC_APB1PeriphClockCmd(RCC_APB1Periph_I2C1, ENABLE); GPIO_InitTypeDef GPIO_InitStructure = { .GPIO_Pin = GPIO_Pin_1 | GPIO_Pin_2, .GPIO_Speed = GPIO_Speed_50MHz, .GPIO_Mode = GPIO_Mode_AF_OD, }; GPIO_Init(GPIOC, &GPIO_InitStructure); I2C_InitTypeDef I2C_InitStructure = { .I2C_ClockSpeed = clock, .I2C_Mode = I2C_Mode_I2C, .I2C_DutyCycle = I2C_DutyCycle_2, .I2C_OwnAddress1 = 0x00, .I2C_Ack = I2C_Ack_Enable, .I2C_AcknowledgedAddress = I2C_AcknowledgedAddress_7bit, }; I2C_Init(i2c, &I2C_InitStructure); I2C_Cmd(i2c, ENABLE); I2C_AcknowledgeConfig(i2c, ENABLE); } void i2c::write(uint8_t addr, const uint8_t* data, size_t length) { addr <<= 1; while( I2C_GetFlagStatus(interface, I2C_FLAG_BUSY ) != RESET ); I2C_GenerateSTART(interface, ENABLE); while( !I2C_CheckEvent( interface, I2C_EVENT_MASTER_MODE_SELECT ) ); I2C_Send7bitAddress( interface, addr, I2C_Direction_Transmitter); while( !I2C_CheckEvent( interface, I2C_EVENT_MASTER_TRANSMITTER_MODE_SELECTED ) ); I2C_SendData( interface, *data); while( !I2C_CheckEvent( interface, I2C_EVENT_MASTER_BYTE_TRANSMITTED ) ); for(auto i = 1; i < length; i++) { I2C_SendData( interface, *(data + i)); while( !I2C_CheckEvent( interface, I2C_EVENT_MASTER_BYTE_TRANSMITTED ) ); } I2C_GenerateSTOP( interface, ENABLE); return; } void i2c::read(uint8_t addr, uint8_t reg, uint8_t* data, size_t length) { addr <<= 1; while( I2C_GetFlagStatus(interface, I2C_FLAG_BUSY ) != RESET); I2C_GenerateSTART(interface, ENABLE); while( !I2C_CheckEvent(interface, I2C_EVENT_MASTER_MODE_SELECT )); I2C_Send7bitAddress(interface, addr, I2C_Direction_Transmitter); while( !I2C_CheckEvent(interface, I2C_EVENT_MASTER_TRANSMITTER_MODE_SELECTED )); I2C_SendData(interface, reg); while( !I2C_CheckEvent(interface, I2C_EVENT_MASTER_BYTE_TRANSMITTED )); I2C_GenerateSTART(interface, ENABLE); while( !I2C_CheckEvent(interface, I2C_EVENT_MASTER_MODE_SELECT )); I2C_Send7bitAddress(interface, addr, I2C_Direction_Receiver); while( !I2C_CheckEvent(interface, I2C_EVENT_MASTER_RECEIVER_MODE_SELECTED)); for(auto i = 1; i < length; i++) { while( !I2C_CheckEvent(interface, I2C_EVENT_MASTER_BYTE_RECEIVED)); *(data + i)= I2C_ReceiveData(interface); } I2C_GenerateSTOP(interface, ENABLE); return; }
KTMicro ソフトウェアラジオ IC KT0913, (KT0936M), KT0937-D8
今回3種類のソフトウェアラジオの IC を購入しました。すべて KTMicro 社製で KT0913, KT0936M, KY0937-D8 の三種です。
秋月で販売されている KT0913 と KT0937-D8 は同じ SSOP16 のパッケージなので見た目区別つきません。

3種類の IC の違いは対応する周波数帯域やマイコンからのコントロール、もしくはマイコン無しに構成可能とする設計と言った違いがあります。例えば少なくとも KT0936M はマイコンを必要とせずにラジオとして機能させる事を前提とした設計で逆にマイコンから制御するにはやや面倒になっています。逆に KT0913 や KT0937-D8 はマイコンと I2C で繋いで制御する事を前提とした設計になっているようです。
| FM/AM | SW | LW | MCU制御 | |
|---|---|---|---|---|
| KT0913 | ○ | ☓ | ☓ | ○ |
| KT0936M | ○ | ○ | ○ | ☓| |
| KT0937-D8 | ○ | ○ | ☓ | △ |
- FM 32MHz 〜110MHz / AM(MW) 500KHz 〜 1750KHz
- SW 1.75MHz 〜 32MHz
- LW 150KHz〜520KHz
今回は CV32V003 から制御するので KT0936M は使いません。 KT0913 と KT0937-D8 は双方 I2C でマイコンから制御可能ですが、比較して KT0913 の方が制御が簡単な印象です。例えば、KT0913 は I2C で周波数を設定してその周波数に変更が可能ですが、KT0937-D8 は周波数を設定することは出来るもののその周波数に変更することが I2C だけでは出来ませんでした。(方法はあるのかもしれませんが私には見つけられませんでした) clock は内蔵されていないようでそれぞれいくつかの周波数の clock が使えるようですが、32.768KHz のクリスタルが共通して使えるようです。
KT0913
ブレッドボードに配線
KT0913 と CH32V003 以外に音を鳴らすまでに少なくとも部品は KT0913 への外部クロックと音を鳴らすための何かになります。外部クロックは 32.768KHz のクリスタルと負荷としてのコンデンサ(データシートは23pFとありましたが、手持ちに 22pF があったのでそれを使いました)、音声の出力はマイクロスピーカーとしました。
加えて手持ちのパーツから、クリスタルは足が細かったのでブレッドボードに挿しづらく丸ピンソケットにはんだ付けしています。スピーカーは端子台を介するようにしています。また、ノイズがちょっとひどかったのでデカップリングに 0.1uF のコンデンサでバイパスしています。
アンテナは FM のみ壊れたジャンパー線を挿しています。AM アンテナはここでは繋いでいませんが必要に応じてバーアンテナやコイルを付け足します。
I2C での制御を前提としているので CH と VOL には何も繋いでいません。

I2C で制御
I2C アドレスは 0x35、レジスタに対する Read/Write は2バイト単位で行います。(ただし何故か Read で3バイト返してきているように見えます。なにかミスしているのかな?…)
32.768KHz のクリスタルを使っているとして、電源 ON の default から指定する周波数の FM を受信するには
- STATUSA(0x12)を読み XTAL_OK(15bit) が 1 (Ready)になるまで待ちます。
- VOLUME(0x04)のDMUTE(13bit) を 1 (Mute disable) に設定します。
- TUNE(0x03)のFMCHAN(0〜11bit)を受信する周波数を 50,000 で割った値、 FMTUNE(15bit 目)を 1 (FM Tune Enable)に設定します。例えば、NHK-FM 東京の82.5MHz であれば (82500(KHz) / 50(KHz)) | 0x8000(FM Tune) = 0x8672 となります。

write to 0x35 ack data: 0x12 read to 0x35 ack data: 0xDF 0x68 0x06 write to 0x35 ack data: 0x04 0xE0 0x80 write to 0x35 ack data: 0x03 0x86 0x72
これ で指定した周波数の放送を受信し音が鳴りました。
KT0937-D8
もしかしたらあるのかもしれませんが、KT0937-D8 を I2C の制御だけで周波数を設定する方法を私は見つけることが出来ませんでした。ですので CH のピンから周波数を設定するようにしています。 CH ピンから周波数を設定するには 10kΩ 可変抵抗を繋いでアナログで入力する Dial Mode

とスイッチで制御する Key Mode

があり今回は Key Mode を使っています。
ブレッドボードに配線
部品は KT0913 で使ったものに加えて Key Mode での制御での2つのスイッチとしてDIP化したレバースイッチを利用しています。また、KT0913 にあった ENABLE が無い代わりに DVDD (デジタル電源入力)があります(意味として大きく変わらないように思います)。

I2C で制御
I2C アドレスは 0x35、レジスタに対する Read/Write は1バイト単位で行います。 前述したように CH ピンからの制御を行うので、 KT0913 と異なりその制御パラメータを設定する必要があります。
32.768KHz のクリスタルを使っているとして、電源 ON の default から指定する周波数の FM を受信するには
- GPIOCFG2(0x51)の CH_PIN (0〜1bit)を 01 (Key Mode)に設定します。
- PLLCFG0(0x04)の SYS_CFGOK(7bit)を 1 (Ready)に設定します。
- G38KCFG0(0x1B)を読み POWERON_FINISH(1bit) が 1 (Finish)になるまで待ちます。
- LOW_CHAN0(0x0098) と LOW_CHAN1(0x0099) に周波数範囲の下限値を 50,000 で割った値を設定します。例えば 76MHz であれば 76000(KHz) / 50(KHz) = 0x5F0 となり、 LOW_CHAN0(0x0098) に 0x05、 LOW_CHAN1(0x0099)に 0xF0 を設定します。
- FMCHAN0(0x0088) と FMCHAN1(0x0089) に周波数範囲の上限値を 50,000 で割った値を設定します。例えば 95Mhz であれば 95000(kHz) / 50(kHz) = 0x76C となります。この時、FMCHAN0(0x0088)の CHANGE_BAND(7 bit) を 1 (Change band) に、AM_FM(6 bit)を 0 (FM) に設定します。つまり、FMCHAN0(0x0088) は0x07 | 0x80(Change band) & 0xBF(FM) = 0x87、FMCHAN1(0x0089) は 0x6C となります。
- CHAN_NUM0(0x009A) と CHAN_NUM1(0x009B) にチャンネル数を設定します。チャンネル数は周波数の上限(FM_CHAN)と下限(LOW)CHAN)の範囲をFM_SPACE(Default で 100KHz)で割った値に1を加えて設定します。例えば上限が 95MHz 、下限が 76MHz とすると (95,000 - 76,000) / 100 + 1 = 0xBF となり、CHAN_NUM0(0x009A) に 0x00、CHAN_NUM1(0x009B) に 0xBF を設定します。

write to 0x35 ack data: 0x51 0x01 write to 0x35 ack data: 0x04 0x80 write to 0x35 ack data: 0x1B read to 0x35 ack data: 0x84 0x00 write to 0x35 ack data: 0x98 0x05 write to 0x35 ack data: 0x99 0xF0 write to 0x35 ack data: 0x89 0x6C write to 0x35 ack data: 0x88 0x87 write to 0x35 ack data: 0x9A 0x00 write to 0x35 ack data: 0x9B 0xBE
これで受信が開始されレバーボタンを操作することにより 100kHz 単位で周波数を増減し設定する事が出来ます。設定された周波数は STATUS6(0x00E4) と STATUS7(0x00E5) から読み取り (STATUS6 << 8) | STATUS7 で算出出来ます。
8ピン CH32V003 J4M6
おまけですが、8ピンの CH32V003 J4M6 で動かしてみます。と言ってもピンは I2C しか使っておらずプログラムはほとんど同じ、気をつけるのは デバッグなどで UART を使っている場合、プログラムを書き込む SWIO と衝突する事程度です。必要であれば書き換え書き込めば F4P6 同様動作すると思います。配線するととても小さくまとまり音がなると個人的にちょっと感動してしまいました。

はまりどころ
WCH-LinkE の動作モード
WCH-LinkE はCH32V に対応する RISC-V モードと対応しない ARM モードという2つの Work Mode があります。WCH-LinkE のボードには赤と青の2つの LED が載っており、ARM モードでは赤青両方が点灯し、RISC-V モードでは赤のみが点灯します。また PC から見える USB の product ID が RISC-V モードでは 0x8010、ARM モードでは 0x8012 となっているようです。
~$ lsusb | grep WCH-Link Bus 001 Device 005: ID 1a86:8010 QinHeng Electronics WCH-Link ~$ lsusb | grep WCH-Link Bus 001 Device 011: ID 1a86:8012 QinHeng Electronics WCH-Link
切り替えは
- 透明ケースを外し Mode ボタンを押す
- wlink を使う (exprimental)
$ wlink mode-switch --rv <WCH-Link#0 libusb device> Bus 001 Device 011 ID 1a86:8012(USB-FS 12 Mbps) (DAP mode) 09:26:33 [WARN] This is an experimental feature, better use the WCH-LinkUtility! 09:26:33 [INFO] Switch mode WCH-LinkDAP 1a86:8012 #0 $
- WCH-LinkUtility を使う (Windows のみ)
のいずれかで出来ます。
WCH-LinkE のバージョン
新規インストールなど、最新の開発環境を用いる場合 WCH-LinkE の version は 2.10 以上でなければなりません。(2.9 以下では書き込みなどでエラーが発生します)
$ wlink status 10:10:32 [INFO] Connected to WCH-Link v2.11(v31) (WCH-LinkE-CH32V305)
メーカーオフィシャルの開発環境の MounRiver Studio を利用してバージョンアップできます。また Windows であれば WCH-LinkUtility でも可能なようです。
8ピン CH32V003 J4M6 の UART TX 衝突問題
開発は 20ピンの F4P6 使いましょうとしている根拠でもあるのですが、データーシートで確認できるように 8 ピンの J4M6 は書き込みやデバッグで使う SWIO と UART TX が共通です。つまり、うかつにUSART_Printf_Init() とかして U(S)ART を Enable にするようなプログラムを書き込むと SWIO が使えなくなりデバッグや書き込みが出来なくなってしまいます。
Windows であれば WCH-LinkUtility で消去できるようです。Linux は(おそらくは Mac も?) wlink を使って書き込んだプログラムを消去することが出来ます。
$ wlink erase --chip CH32V003 --method power-off 20:09:53 [INFO] Connected to WCH-Link v2.11(v31) (WCH-LinkE-CH32V305) 20:09:53 [INFO] Erase chip by PowerOff $
Mango Pi MQ Quad (Allwinner H616) Armbian が boot するまで
AliExpress でセールがやっていたので Mango Pi MQ Quad 買いました*1。
このページの FW&SDK からブートイメージがダウンロードできるという事になっていますが、
という状態でした。なのでちょっと試行錯誤し、結論を言えばちょっと作業は必要ですがブートするまでは出来ました。
Armbian OrangePi Zero2 の boot イメージで試す
まず軽く検索してみると、同じ Allwinner H616 を使った Orange Pi Zero2 のイメージが使えるような書き込みが散見されたので
- Armbian の Orange Pi Zero2 のイメージを入手し適当な Micro SD カードに dd 等で書き込む。
- Mango Pi MQ Quad の UART PIN と PC とシリアルで接続してシリアルターミナルを起動する。
- Armbian を書き込んだ Micro SD を挿して電源を投入する。
(この時点でダウンロードした Armbian は Armbian_23.8.3_Orangepizero2_bookworm_current_6.1.53.img.xz です。)
すると…
$ sudo cu Connected to /dev/ttyUSB0 (speed 115200) U-Boot SPL 2023.07.02-armbian (Sep 19 2023 - 18:26:21 +0000) DRAM:This DRAM setup is currently not supported. resetting ... U-Boot SPL 2023.07.02-armbian (Sep 19 2023 - 18:26:21 +0000) DRAM:This DRAM setup is currently not supported. resetting ...
となり u-boot の段階で DRAM を認識できず reset を繰り返して boot しません。
Orange Pi Zero2 の電源制御は AXP305 というICが使われており、その前提で boot イメージの u-boot は構築されていますが、Mango Pi MQ-QUAD の電源制御は AX313A という IC が使われているというハードウェア構成の違いがあります。 u-boot の OrangePi Zero2 の設定(orangepi_zero2_defconfig)を見ると電源制御の記述は見当たりません。これは H616 であった場合 default で AXP305 が設定されるようになっており、Orange Pi Zero2 はそれで問題ないといった記述になっています。 ですので H616 と AXP313A で構成された Mango Pi MQ Quad に対応した u-boot を作る必要があります。
u-boot 構築
u-boot を作るにはまず ARM Trusted firmware と呼ばれるファームウェアを利用する環境に合わせて作成する必要があります。
H616 に対応したものを作るには PLAT=sun50i_h616 を指定します。
$ git clone https://github.com/ARM-software/arm-trusted-firmware.git $ cd cd arm-trusted-firmware/ $ make CROSS_COMPILE=aarch64-linux-gnu- PLAT=sun50i_h616 DEBUG=1 bl31 $ ls build/sun50i_h616/debug/bl31.bin build/sun50i_h616/debug/bl31.bin $ cd .. $
次に u-boot 本体を構築します。
まず コードを clone し、Orange Pi Zero2 の設定を元に Mango Pi MQ Quad の設定(configs/mangopi_mq_quad_defconfig)を作ります。 MQ Quad の設定の内容は Orange Pi Zero2 の設定に AX313A の設定を追記し SPI_FLASH をコメントアウトします。
MQ Quad の回路図を見ると AXP313A の DCDC1〜3 はそれぞれ GPU, CPU, DRAM に繋がれています。

また、H616 のデータシートの 5.2. Recommended Operating Conditions を見ると

となっているので AXP313A の電源ポートの電圧を回路図とデーターシートに合わせて指定します。
$ git clone git://git.denx.de/u-boot.git $ cd u-boot/configs $ cp orangepi_zero2_defconfig mangopi_mq_quad_defconfig $ vi mangopi_mq_quad_defconfig $ diff orangepi_zero2_defconfig mangopi_mq_quad_defconfig 19c19 < CONFIG_SPI_FLASH_MACRONIX=y --- > # CONFIG_SPI_FLASH_MACRONIX=y 25a26,30 > > CONFIG_AXP313_POWER=y > CONFIG_AXP_DCDC1_VOLT=900 > CONFIG_AXP_DCDC2_VOLT=900 > CONFIG_AXP_DCDC3_VOLT=1500 $ cd .. $
次に作成した設定を適用して u-boot 本体を作成し
$ make CROSS_COMPILE=aarch64-linux-gnu- BL31=../arm-trusted-firmware/build/sun50i_h616/debug/bl31.bin mangopi_mq_quad_defconfig $ make CROSS_COMPILE=aarch64-linux-gnu- BL31=../arm-trusted-firmware/build/sun50i_h616/debug/bl31.bin
Armbian が書き込まれている Micro SD に作成した u-boot を dd で書き込んで (of= で指定する出力先は個別の環境に合わせて下さい。)
$ sudo dd if=./u-boot/u-boot-sunxi-with-spl.bin of=/dev/sdxx bs=8K seek=1 102+1 records in 102+1 records out 841285 bytes (841 kB, 822 KiB) copied, 0.000470996 s, 1.8 GB/s
新しい u-boot を書き込んだ Micro SD を Mango Pi MQ Quad に差し込んで boot し、シリアルコンソールで出力を見ます。
$ sudo cu
Connected to /dev/ttyUSB0 (speed 115200)
U-Boot SPL 2024.01-rc3-00009-g9e53e45292 (Nov 24 2023 - 13:13:58 +0000)
DRAM: 1024 MiB
Trying to boot from MMC1
NOTICE: BL31: v2.9.0 (debug):v2.10-rc0-29-gccd8c0230
NOTICE: BL31: Built : 17:55:38, Nov 21 2023
NOTICE: BL31: Detected Allwinner H616 SoC (1823)
NOTICE: BL31: Found U-Boot DTB at 0x4a0b23f8, model: OrangePi Zero2
INFO: ARM GICv2 driver initialized
INFO: Configuring SPC Controller
INFO: PMIC: Probing AXP305 on RSB
ERROR: RSB: set run-time address: 0x10003
INFO: Could not init RSB: -65539
INFO: BL31: Platform setup done
INFO: BL31: Initializing runtime services
INFO: BL31: cortex_a53: CPU workaround for erratum 855873 was applied
INFO: BL31: cortex_a53: CPU workaround for erratum 1530924 was applied
INFO: PSCI: Suspend is unavailable
INFO: BL31: Preparing for EL3 exit to normal world
INFO: Entry point address = 0x4a000000
INFO: SPSR = 0x3c9
INFO: Changed devicetree.
U-Boot 2024.01-rc3-00009-g9e53e45292 (Nov 24 2023 - 13:13:58 +0000) Allwinner Technology
CPU: Allwinner H616 (SUN50I)
Model: OrangePi Zero2
DRAM: 1 GiB
Core: 54 devices, 22 uclasses, devicetree: separate
WDT: Not starting watchdog@30090a0
MMC: mmc@4020000: 0
Loading Environment from FAT... Unable to use mmc 0:1...
In: serial@5000000
Out: serial@5000000
Err: serial@5000000
Allwinner mUSB OTG (Peripheral)
Net: Could not get PHY for ethernet@5020000: addr 1
using musb-hdrc, OUT ep1out IN ep1in STATUS ep2in
MAC de:ad:be:ef:00:01
HOST MAC de:ad:be:ef:00:00
RNDIS ready
eth1: usb_ether
starting USB...
Bus usb@5200000: USB EHCI 1.00
Bus usb@5200400: USB OHCI 1.0
scanning bus usb@5200000 for devices... 1 USB Device(s) found
scanning bus usb@5200400 for devices... 1 USB Device(s) found
scanning usb for storage devices... 0 Storage Device(s) found
Hit any key to stop autoboot: 0
switch to partitions #0, OK
mmc0 is current device
Scanning mmc 0:1...
Found U-Boot script /boot/boot.scr
3259 bytes read in 3 ms (1 MiB/s)
## Executing script at 4fc00000
U-boot loaded from SD
Boot script loaded from mmc
131 bytes read in 2 ms (63.5 KiB/s)
25388 bytes read in 7 ms (3.5 MiB/s)
Working FDT set to 4fa00000
Failed to load '/boot/dtb/allwinner/overlay/-fixup.scr'
11788479 bytes read in 489 ms (23 MiB/s)
22390792 bytes read in 927 ms (23 MiB/s)
Moving Image from 0x40080000 to 0x40200000, end=417d0000
## Loading init Ramdisk from Legacy Image at 4ff00000 ...
Image Name: uInitrd
Image Type: AArch64 Linux RAMDisk Image (gzip compressed)
Data Size: 11788415 Bytes = 11.2 MiB
Load Address: 00000000
Entry Point: 00000000
Verifying Checksum ... OK
## Flattened Device Tree blob at 4fa00000
Booting using the fdt blob at 0x4fa00000
Working FDT set to 4fa00000
Loading Ramdisk to 494c1000, end 49fff07f ... OK
Loading Device Tree to 0000000049452000, end 00000000494c0fff ... OK
Working FDT set to 49452000
Starting kernel ...
u-boot は起動して kernel の起動まで進みました。 が、Starting kernel ... のままいつまで待っても先に進みません。おそらくは u-boot の問題は解決したが、 AXP313A に変わっている事をまだ kernel は知らないためと思われます。 ですので次に kernel に構成を伝えるため MQ Quad に対応したデバイスツリーを作成し kernel に渡るようにします。
デバイスツリーの対応
MQ Quad に対応したデバイスツリーを色々検索してみると過去 u-boot にパッチが送られているのを見つけました。
これを取り込むと sun50i-h616-mangopi-mq-quad という名前でデバイスツリーが構成/生成されます。
なので先ほど作成した MQ Quad の u-boot の設定で Orange Pi Zero2 の構成(sun50i-h616-orangepi-zero2)のデバイスツリーの指定を MQ Quad の指定に変更します。
$ diff configs/mangopi_mq_quad_defconfig configs/mangopi_mq_quad_defconfig.old 3,4c3 < # CONFIG_DEFAULT_DEVICE_TREE="sun50i-h616-orangepi-zero2" < CONFIG_DEFAULT_DEVICE_TREE="sun50i-h616-mangopi-mq-quad" --- > CONFIG_DEFAULT_DEVICE_TREE="sun50i-h616-orangepi-zero2"
変更したら以前と同様に設定を反映し make します。
$ make CROSS_COMPILE=aarch64-linux-gnu- BL31=../arm-trusted-firmware/build/sun50i_h616/debug/bl31.bin mangopi_mq_quad_defconfig $ make CROSS_COMPILE=aarch64-linux-gnu- BL31=../arm-trusted-firmware/build/sun50i_h616/debug/bl31.bin
make すると前回同様に u-boot の実体と、新たに MQ Quad に対応したデバイスツリーファイルが作成されます。 なので、Micro SD に u-boot を書き込み
$ sudo dd if=u-boot-sunxi-with-spl.bin of=/dev/sdxx bs=8K seek=1 102+1 records in 102+1 records out 841285 bytes (841 kB, 822 KiB) copied, 0.000470996 s, 1.8 GB/s
Micro SD をマウントし、Armbian の /boot/dtb/allwinner/ に新たに出来た arch/arm/dts/sun50i-h616-mangopi-mq-quad.dtb をコピーします。
$ sudo cp arch/arm/dts/sun50i-h616-mangopi-mq-quad.dtb $MICROSD/armbi_root/boot/dtb/allwinner/
Micro SD をアンマウントして MQ Quad に差し込み電源を投入しシリアルコンソールを眺めると
U-Boot SPL 2024.01-rc3-00009-g9e53e45292 (Nov 24 2023 - 13:43:20 +0000)
DRAM: 1024 MiB
Trying to boot from MMC1
NOTICE: BL31: v2.9.0 (debug):v2.10-rc0-29-gccd8c0230
NOTICE: BL31: Built : 17:55:38, Nov 21 2023
NOTICE: BL31: Detected Allwinner H616 SoC (1823)
NOTICE: BL31: Found U-Boot DTB at 0x4a0b23f8, model: MangoPi MQ-Quad
INFO: ARM GICv2 driver initialized
INFO: Configuring SPC Controller
INFO: PMIC: Probing AXP305 on RSB
ERROR: RSB: set run-time address: 0x10003
INFO: Could not init RSB: -65539
INFO: BL31: Platform setup done
INFO: BL31: Initializing runtime services
INFO: BL31: cortex_a53: CPU workaround for erratum 855873 was applied
INFO: BL31: cortex_a53: CPU workaround for erratum 1530924 was applied
INFO: PSCI: Suspend is unavailable
INFO: BL31: Preparing for EL3 exit to normal world
INFO: Entry point address = 0x4a000000
INFO: SPSR = 0x3c9
INFO: Changed devicetree.
U-Boot 2024.01-rc3-00009-g9e53e45292 (Nov 24 2023 - 13:43:20 +0000) Allwinner Technology
CPU: Allwinner H616 (SUN50I)
Model: MangoPi MQ-Quad
DRAM: 1 GiB
Core: 57 devices, 22 uclasses, devicetree: separate
WDT: Not starting watchdog@30090a0
MMC: mmc@4020000: 0, mmc@4021000: 1
Loading Environment from FAT... Unable to use mmc 0:1...
In: serial@5000000
Out: serial@5000000
Err: serial@5000000
Allwinner mUSB OTG (Peripheral)
Net: using musb-hdrc, OUT ep1out IN ep1in STATUS ep2in
MAC de:ad:be:ef:00:01
HOST MAC de:ad:be:ef:00:00
RNDIS ready
eth0: usb_ether
starting USB...
Bus usb@5200000: USB EHCI 1.00
Bus usb@5200400: USB OHCI 1.0
scanning bus usb@5200000 for devices... 1 USB Device(s) found
scanning bus usb@5200400 for devices... 1 USB Device(s) found
scanning usb for storage devices... 0 Storage Device(s) found
Hit any key to stop autoboot: 0
switch to partitions #0, OK
mmc0 is current device
Scanning mmc 0:1...
Found U-Boot script /boot/boot.scr
3259 bytes read in 3 ms (1 MiB/s)
## Executing script at 4fc00000
U-boot loaded from SD
Boot script loaded from mmc
180 bytes read in 2 ms (87.9 KiB/s)
18984 bytes read in 5 ms (3.6 MiB/s)
Working FDT set to 4fa00000
Failed to load '/boot/dtb/allwinner/overlay/-fixup.scr'
11790322 bytes read in 489 ms (23 MiB/s)
22390792 bytes read in 927 ms (23 MiB/s)
Moving Image from 0x40080000 to 0x40200000, end=417d0000
## Loading init Ramdisk from Legacy Image at 4ff00000 ...
Image Name: uInitrd
Image Type: AArch64 Linux RAMDisk Image (gzip compressed)
Data Size: 11790258 Bytes = 11.2 MiB
Load Address: 00000000
Entry Point: 00000000
Verifying Checksum ... OK
## Flattened Device Tree blob at 4fa00000
Booting using the fdt blob at 0x4fa00000
Working FDT set to 4fa00000
Loading Ramdisk to 494c1000, end 49fff7b2 ... OK
Loading Device Tree to 0000000049454000, end 00000000494c0fff ... OK
Working FDT set to 49454000
Starting kernel ...
Loading, please wait...
Starting systemd-udevd version 252.17-1~deb12u1
Begin: Loading essential drivers ... done.
Begin: Running /scripts/init-premount ... done.
Begin: Mounting root file system ... Begin: Running /scripts/local-top ... done.
Begin: Running /scripts/local-premount ... Scanning for Btrfs filesystems
done.
Begin: Will now check root file system ... fsck from util-linux 2.38.1
[/sbin/fsck.ext4 (1) -- /dev/mmcblk0p1] fsck.ext4 -a -C0 /dev/mmcblk0p1
armbi_root: clean, 45354/960992 files, 464626/3849216 blocks
done.
done.
Begin: Running /scripts/local-bottom ... done.
Begin: Running /scripts/init-bottom ... done.
Welcome to Armbian 23.8.3 bookworm!
[ OK ] Created slice system-getty.slice - Slice /system/getty.
[ OK ] Created slice system-modpr…lice - Slice /system/modprobe.
[ OK ] Created slice system-seria… - Slice /system/serial-getty.
[ OK ] Created slice user.slice - User and Session Slice.
[ OK ] Started systemd-ask-passwo…quests to Console Directory Watch.
[ OK ] Started systemd-ask-passwo… Requests to Wall Directory Watch.
[ OK ] Set up automount proc-sys-…rmats File System Automount Point.
[ OK ] Reached target cryptsetup.…get - Local Encrypted Volumes.
[ OK ] Reached target integrityse…Local Integrity Protected Volumes.
[ OK ] Reached target paths.target - Path Units.
[ OK ] Reached target slices.target - Slice Units.
[ OK ] Reached target swap.target - Swaps.
[ OK ] Reached target time-set.target - System Time Set.
kernel が起動し Armbian が動作します。😀
*1:今となっては MQ Quad 買うなら H618 の Orange Pi Zero2W買ったほうが色々楽だと思います
ASIAIR Plus (Raspberry Pi CM4 モデル) ハック
ASIAIR Plus (Raspberry Pi CM4 モデル) の OS と言ったソフトウェア的な中身を入れ替える目的で行ったハックのメモです。
注意:
- リバースエンジニアリングの内容を含みます。この内容によって生じた如何なる不利益も責任を持ちません。
- 個人で入手した ASIAIR Plus の Raspberry Pi CM4 (32GB)モデルに限定しています。最近の Plus である Rockchip 3568 のモデルや mini や PRO といった別のモデルでは確認していません。また、CM4 のモデルでも個体差があるかもしれません。
以降、ASIAIR Plus の Raspberry Pi CM4 モデルを ASIAIRと略します。
ASIAIR の内蔵 eMMC のマウント
- PC と ASIAIR を USB ケーブルで接続。ASIAIR 側は PC と書かれている USB-C に繋ぐ。この時 ASIAIR は電源 OFF の状態とすること
- PC 側で rpiboot を起動
- ASIAIR をリセットボタンを押しながら電源 ON
- PC 側で ASIAIR の eMMC 全体がストレージとして認識されます。
$ rpiboot RPIBOOT: build-date Jan 31 2022 version 0~20220315+git6fa2ec0+nowin-0ubuntu1 Waiting for BCM2835/6/7/2711... Loading embedded: bootcode4.bin Sending bootcode.bin Successful read 4 bytes Waiting for BCM2835/6/7/2711... Loading embedded: bootcode4.bin Second stage boot server Loading embedded: start4.elf File read: start4.elf Second stage boot server done $
dmesg
[ 9851.637490] usb 1-4: new high-speed USB device number 29 using xhci_hcd [ 9851.785809] usb 1-4: config index 0 descriptor too short (expected 55, got 32) [ 9851.786210] usb 1-4: New USB device found, idVendor=0a5c, idProduct=2711, bcdDevice= 0.00 [ 9851.786224] usb 1-4: New USB device strings: Mfr=1, Product=2, SerialNumber=3 [ 9851.786231] usb 1-4: Product: BCM2711 Boot [ 9851.786237] usb 1-4: Manufacturer: Broadcom [ 9851.786243] usb 1-4: SerialNumber: xxxxxxxx [ 9853.893651] usb 1-4: USB disconnect, device number 29 [ 9861.661604] usb 1-4: new high-speed USB device number 30 using xhci_hcd [ 9861.810106] usb 1-4: New USB device found, idVendor=0a5c, idProduct=2711, bcdDevice= 0.00 [ 9861.810120] usb 1-4: New USB device strings: Mfr=1, Product=2, SerialNumber=4 [ 9861.810128] usb 1-4: Product: BCM2711 Boot [ 9861.810133] usb 1-4: Manufacturer: Broadcom [ 9861.810139] usb 1-4: SerialNumber: xxxxxxxx [ 9863.152929] usb 1-4: USB disconnect, device number 30 [ 9864.805622] usb 1-4: new high-speed USB device number 31 using xhci_hcd [ 9864.954546] usb 1-4: New USB device found, idVendor=0a5c, idProduct=0001, bcdDevice= 0.01 [ 9864.954561] usb 1-4: New USB device strings: Mfr=2, Product=1, SerialNumber=3 [ 9864.954568] usb 1-4: Product: Compute Module [ 9864.954574] usb 1-4: Manufacturer: Raspberry Pi [ 9864.954579] usb 1-4: SerialNumber: xxxxxxxx [ 9864.956891] usb-storage 1-4:1.0: USB Mass Storage device detected [ 9864.957483] scsi host7: usb-storage 1-4:1.0 [ 9865.966237] scsi 7:0:0:0: Direct-Access RPi-MSD- 0001 PQ: 0 ANSI: 2 [ 9865.967003] sd 7:0:0:0: Attached scsi generic sg7 type 0 [ 9865.967706] sd 7:0:0:0: [sdg] 61071360 512-byte logical blocks: (31.3 GB/29.1 GiB) [ 9865.968132] sd 7:0:0:0: [sdg] Write Protect is off [ 9865.968140] sd 7:0:0:0: [sdg] Mode Sense: 0f 00 00 00 [ 9865.968304] sd 7:0:0:0: [sdg] Write cache: enabled, read cache: enabled, doesn't support DPO or FUA
要するに ASIAIR は CM4 の nRPI_BOOT の PIN が Reset ボタンと繋がれているようです。
ここまで来れば Raspberry Pi4 のモデルだった ASIAIR Pro の MicroSD と同じです。例えば ROOTFS/root/.ssh/authorized_keys を置いて root で ssh と言ったことも可能になります。
ASIAIR のシステムを入れ替える
ZWO 縛りがある ASIAIR のソフトウェアに個人的に用は無いので。

USB3
ASIAIR の USB3 は PCIe 経由で接続されている Renesas の μPD720201 が内蔵されていますが Raspberry Pi OS で配布されている default kernel では動作しません。
$ lspci 00:00.0 PCI bridge: Broadcom Inc. and subsidiaries BCM2711 PCIe Bridge (rev 20) 01:00.0 USB controller: Renesas Technology Corp. uPD720201 USB 3.0 Host Controller (rev 03) $ lsusb Bus 001 Device 001: ID 1d6b:0002 Linux Foundation 2.0 root hub $
なので kernel の設定を変更して
CONFIG_USB_XHCI_PCI_RENESAS=m
再構築し、firmware ファイルを /lib/firmware/renesas_usb_fw.mem に置いて reboot します。
[ 6.514335] xhci-hcd fe9c0000.xhci: new USB bus registered, assigned bus number 1 [ 6.514603] xhci-hcd fe9c0000.xhci: USB3 root hub has no ports [ 6.514621] xhci-hcd fe9c0000.xhci: hcc params 0x0220fe65 hci version 0x110 quirks 0x0000000000010010 [ 6.514703] xhci-hcd fe9c0000.xhci: irq 34, io mem 0xfe9c0000 ...
$ lsmod |grep xhci xhci_pci 24576 0 xhci_pci_renesas 16384 1 xhci_pci $ lsusb Bus 001 Device 001: ID 1d6b:0002 Linux Foundation 2.0 root hub Bus 003 Device 001: ID 1d6b:0003 Linux Foundation 3.0 root hub Bus 002 Device 002: ID 05e3:0761 Genesys Logic, Inc. Genesys Mass Storage Device Bus 002 Device 001: ID 1d6b:0002 Linux Foundation 2.0 root hub $
無事認識しました
12V 電源制御周り
ON/OFF は GPIO 18,12,13,26 で制御、電流電圧センサーは I2C に繋がれています。
$ i2cdetect -l
i2c-0 i2c i2c-22-mux (chan_id 0) I2C adapter
i2c-1 i2c bcm2835 (i2c@7e804000) I2C adapter
i2c-10 i2c i2c-22-mux (chan_id 1) I2C adapter
i2c-20 i2c fef04500.i2c I2C adapter
i2c-21 i2c fef09500.i2c I2C adapter
i2c-22 i2c bcm2835 (i2c@7e205000) I2C adapter
$ i2cdetect -y 10
0 1 2 3 4 5 6 7 8 9 a b c d e f
00: -- -- -- -- -- -- -- --
10: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- --
20: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- --
30: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- --
40: -- -- -- -- -- -- -- -- 48 49 -- 4b -- -- -- --
50: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- --
60: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- --
70: -- -- -- -- -- -- -- --
ekos/kstars の indi-3rdparty にある indi-asi-power で値を参照、制御できます。

pyvirtualcam を使って流星検出しながらのライブ配信
こちらも過去に X (旧Twitter)に投げた内容の書き直しです。
動画ライブでリアルタイムで流星検出するにはというお題。ライブとかよくわからないけれど、とりあえず動画に検出した流星にマーク付けて pyvirtualcam とかで仮想カメラに出力し、その仮想カメラを OBS と言ったライブで用いるソフトウエアの入力に回すと出来ます。というか出来ました。 pic.twitter.com/Zfk1Lin0cv
— tentaisan (@tentaisan1) 2022年12月22日
AtomCam といったリモートの動画像ストリームを入力として流星検出処理を行い pyvirtualcam を利用して仮想カメラに出力します。 これによって、流星検出フィルタ処理を行った結果を仮想カメラの出力となり、その仮想カメラの出力を OBS といった動画配信ツールの入力として扱う事ができます。 今回は検出処理のハフ変換で得られる直線成分の二点を用いて OpenCV の rectangle で四角形を描画し、流星をくくるような描画を行っています。

実装は先日の流星検出を実装を連続するストリームに対して動作するように書き換え、結果を pyvirtualcam の Camera コンテキストに送っているだけです。 入力される動画像を buffer に溜めながら差分比較明合成画像を作成し、buffer に fps の数の画像、つまり 1秒間の画像が溜まったら差分比較明合成画像に対して直線成分検出を行い、検出されれば四角形のマーカーを描画してカメラに送ると言った処理になります。
import pyvirtualcam import cv2 capture = cv2.VideoCapture("rtsp://atomcam.local:8554/video0_unicast") width = int(capture.get(cv2.CAP_PROP_FRAME_WIDTH)) height = int(capture.get(cv2.CAP_PROP_FRAME_HEIGHT)) fps = int(capture.get(cv2.CAP_PROP_FPS)) camera = pyvirtualcam.Camera(width, height, fps, fmt=pyvirtualcam.PixelFormat.BGR, device="/dev/video2") s, cache = capture.read() lighten = cv2.UMat(height, width, type=cv2.CV_8UC3, s=(0)) buffer = [] while s: s, frame = capture.read() if s: buffer.append(frame) difference = cv2.subtract(frame, cache) lighten = cv2.max(lighten, difference) cache = frame if len(buffer) > fps: g = cv2.GaussianBlur(lighten, (5, 5), 0) c = cv2.Canny(g, 100, 200, 3) r = cv2.HoughLinesP(c, 1, 3.141592653/180, 25, minLineLength=30, maxLineGap=5).get() for f in buffer: if r is not None: for p in r: p = p[0] cv2.rectangle(f, pt1=(p[0], p[1]), pt2=(p[2], p[3]), color=(0, 255, 0)) camera.send(f) camera.sleep_until_next_frame() lighten = cv2.UMat(height, width, type=cv2.CV_8UC3, s=(0)) buffer.clear() camera.close() capture.release()
ここでは Linux 環境で v4l2loopback を利用しています。ですので pyvirtualcam.Camera の引数に device="/dev/video2" を指定しています。
Windows や Mac では OBS の仮想カメラ機能を利用して pyvirtualcam を利用することができるようです。
OrangePi3B OpenCL Driver Install Memo
基本的に RK3588 を対象にしたこちらを参考に
libmali を RK356x の bifrost-G52 のものに置き換える
$ /usr/lib $ sudo wget https://github.com/JeffyCN/mirrors/raw/libmali/lib/aarch64-linux-gnu/libmali-bifrost-g52-g2p0-wayland-gbm.so $ cd /lib/firmware/ $ sudo wget https://github.com/JeffyCN/mirrors/raw/libmali/firmware/g610/mali_csffw.bin $ sudo apt install mesa-opencl-icd clinfo $ sudo mkdir -p /etc/OpenCL/vendors $ echo "/usr/lib/libmali-bifrost-g52-g2p0-wayland-gbm.so" | sudo tee /etc/OpenCL/vendors/mali.icd $ sudo apt install libxcb-dri2-0 libxcb-dri3-0 libwayland-client0 libwayland-server0 libx11-xcb1
$ clinfo Number of platforms 2 Platform Name ARM Platform Platform Vendor ARM Platform Version OpenCL 2.1 v1.g2p0-01eac0.a728b64d6b9d8cadf7f54eb4e167984c Platform Profile FULL_PROFILE Platform Extensions cl_khr_global_int32_base_atomics cl_khr_global_int32_extended_atomics cl_khr_local_int32_base_atomics cl_khr_local_int32_extended_atomics cl_khr_byte_addressable_store cl_khr_3d_image_writes cl_khr_int64_base_atomics cl_khr_int64_extended_atomics cl_khr_fp16 cl_khr_icd cl_khr_egl_image cl_khr_image2d_from_buffer cl_khr_depth_images cl_khr_subgroups cl_khr_il_program cl_khr_priority_hints cl_khr_create_command_queue cl_khr_spirv_no_integer_wrap_decoration cl_khr_extended_versioning cl_khr_device_uuid cl_arm_core_id cl_arm_printf cl_arm_thread_limit_hint cl_arm_non_uniform_work_group_size cl_arm_import_memory cl_arm_import_memory_dma_buf cl_arm_import_memory_host cl_arm_integer_dot_product_int8 cl_arm_job_slot_selection cl_arm_scheduling_controls Platform Extensions function suffix ARM Platform Host timer resolution 1ns Platform Name Clover Platform Vendor Mesa Platform Version OpenCL 1.1 Mesa 23.0.4-0ubuntu1~22.04.1 Platform Profile FULL_PROFILE Platform Extensions cl_khr_icd Platform Extensions function suffix MESA Platform Name ARM Platform Number of devices 1 arm_release_ver of this libmali is 'g2p0-01eac0', rk_so_ver is '10'. Device Name Mali-G52 r1p0 Device Vendor ARM Device Vendor ID 0x74021000 Device Version OpenCL 2.1 v1.g2p0-01eac0.a728b64d6b9d8cadf7f54eb4e167984c Device UUID 00100274-0100-0000-0000-000000000000 Driver UUID fc0b0b03-27c5-81b2-311e-8307e8823e37 Valid Device LUID No Device LUID 0000-000000000000 Device Node Mask 0 Device Numeric Version 0x801000 (2.1.0) Driver Version 2.1 Device OpenCL C Version OpenCL C 2.0 v1.g2p0-01eac0.a728b64d6b9d8cadf7f54eb4e167984c Device Type GPU Device Profile FULL_PROFILE Device Available Yes Compiler Available Yes Linker Available Yes Max compute units 1 Available core IDs 0 Available job slots (ARM) 1-2 Max clock frequency 800MHz Device Partition (core) Max number of sub-devices 0 Supported partition types None Supported affinity domains (n/a) Max work item dimensions 3 Max work item sizes 384x384x384 Max work group size 384 Preferred work group size multiple (kernel) 8 Max sub-groups per work group 48 Preferred / native vector sizes char 16 / 4 short 8 / 2 int 4 / 1 long 2 / 1 half 8 / 2 (cl_khr_fp16) float 4 / 1 double 0 / 0 (n/a) Half-precision Floating-point support (cl_khr_fp16) Denormals Yes Infinity and NANs Yes Round to nearest Yes Round to zero Yes Round to infinity Yes IEEE754-2008 fused multiply-add Yes Support is emulated in software No Single-precision Floating-point support (core) Denormals Yes Infinity and NANs Yes Round to nearest Yes Round to zero Yes Round to infinity Yes IEEE754-2008 fused multiply-add Yes Support is emulated in software No Correctly-rounded divide and sqrt operations No Double-precision Floating-point support (n/a) Address bits 64, Little-Endian Global memory size 4294967296 (4GiB) Error Correction support No Max memory allocation 1073741824 (1024MiB) Unified memory for Host and Device Yes Shared Virtual Memory (SVM) capabilities (core) Coarse-grained buffer sharing Yes Fine-grained buffer sharing No Fine-grained system sharing No Atomics No Minimum alignment for any data type 128 bytes Alignment of base address 1024 bits (128 bytes) Preferred alignment for atomics SVM 0 bytes Global 0 bytes Local 0 bytes Max size for global variable 65536 (64KiB) Preferred total size of global vars 0 Global Memory cache type Read/Write Global Memory cache size 131072 (128KiB) Global Memory cache line size 64 bytes Image support Yes Max number of samplers per kernel 16 Max size for 1D images from buffer 65536 pixels Max 1D or 2D image array size 2048 images Base address alignment for 2D image buffers 32 bytes Pitch alignment for 2D image buffers 64 pixels Max 2D image size 65536x65536 pixels Max 3D image size 65536x65536x65536 pixels Max number of read image args 128 Max number of write image args 64 Max number of read/write image args 64 Max number of pipe args 16 Max active pipe reservations 1 Max pipe packet size 1024 Local memory type Global Local memory size 32768 (32KiB) Max number of constant args 128 Max constant buffer size 1073741824 (1024MiB) Max size of kernel argument 1024 Queue properties (on host) Out-of-order execution Yes Profiling Yes Queue properties (on device) Out-of-order execution Yes Profiling Yes Preferred size 2097152 (2MiB) Max size 16777216 (16MiB) Max queues on device 1 Max events on device 1024 Prefer user sync for interop No Profiling timer resolution 1000ns Execution capabilities Run OpenCL kernels Yes Run native kernels No Sub-group independent forward progress Yes IL version SPIR-V_1.0 ILs with version SPIR-V 0x400000 (1.0.0) printf() buffer size 1048576 (1024KiB) Built-in kernels (n/a) Built-in kernels with version (n/a) Device Extensions cl_khr_global_int32_base_atomics cl_khr_global_int32_extended_atomics cl_khr_local_int32_base_atomics cl_khr_local_int32_extended_atomics cl_khr_byte_addressable_store cl_khr_3d_image_writes cl_khr_int64_base_atomics cl_khr_int64_extended_atomics cl_khr_fp16 cl_khr_icd cl_khr_egl_image cl_khr_image2d_from_buffer cl_khr_depth_images cl_khr_subgroups cl_khr_il_program cl_khr_priority_hints cl_khr_create_command_queue cl_khr_spirv_no_integer_wrap_decoration cl_khr_extended_versioning cl_khr_device_uuid cl_arm_core_id cl_arm_printf cl_arm_thread_limit_hint cl_arm_non_uniform_work_group_size cl_arm_import_memory cl_arm_import_memory_dma_buf cl_arm_import_memory_host cl_arm_integer_dot_product_int8 cl_arm_job_slot_selection cl_arm_scheduling_controls Device Extensions with Version cl_khr_global_int32_base_atomics 0x400000 (1.0.0) cl_khr_global_int32_extended_atomics 0x400000 (1.0.0) cl_khr_local_int32_base_atomics 0x400000 (1.0.0) cl_khr_local_int32_extended_atomics 0x400000 (1.0.0) cl_khr_byte_addressable_store 0x400000 (1.0.0) cl_khr_3d_image_writes 0x400000 (1.0.0) cl_khr_int64_base_atomics 0x400000 (1.0.0) cl_khr_int64_extended_atomics 0x400000 (1.0.0) cl_khr_fp16 0x400000 (1.0.0) cl_khr_icd 0x400000 (1.0.0) cl_khr_egl_image 0x400000 (1.0.0) cl_khr_image2d_from_buffer 0x400000 (1.0.0) cl_khr_depth_images 0x400000 (1.0.0) cl_khr_subgroups 0x400000 (1.0.0) cl_khr_il_program 0x400000 (1.0.0) cl_khr_priority_hints 0x400000 (1.0.0) cl_khr_create_command_queue 0x400000 (1.0.0) cl_khr_spirv_no_integer_wrap_decoration 0x400000 (1.0.0) cl_khr_extended_versioning 0x400000 (1.0.0) cl_khr_device_uuid 0x400000 (1.0.0) cl_arm_core_id 0x400000 (1.0.0) cl_arm_printf 0x400000 (1.0.0) cl_arm_thread_limit_hint 0x400000 (1.0.0) cl_arm_non_uniform_work_group_size 0x400000 (1.0.0) cl_arm_import_memory 0x400000 (1.0.0) cl_arm_import_memory_dma_buf 0x400000 (1.0.0) cl_arm_import_memory_host 0x400000 (1.0.0) cl_arm_integer_dot_product_int8 0x400000 (1.0.0) cl_arm_job_slot_selection 0x400000 (1.0.0) cl_arm_scheduling_controls 0x1000 (0.1.0) Platform Name Clover Number of devices 0 NULL platform behavior clGetPlatformInfo(NULL, CL_PLATFORM_NAME, ...) ARM Platform clGetDeviceIDs(NULL, CL_DEVICE_TYPE_ALL, ...) Success [ARM] clCreateContext(NULL, ...) [default] Success [ARM] clCreateContext(NULL, ...) [other] clCreateContextFromType(NULL, CL_DEVICE_TYPE_DEFAULT) Success (1) Platform Name ARM Platform Device Name Mali-G52 r1p0 clCreateContextFromType(NULL, CL_DEVICE_TYPE_CPU) No devices found in platform clCreateContextFromType(NULL, CL_DEVICE_TYPE_GPU) Success (1) Platform Name ARM Platform Device Name Mali-G52 r1p0 clCreateContextFromType(NULL, CL_DEVICE_TYPE_ACCELERATOR) No devices found in platform clCreateContextFromType(NULL, CL_DEVICE_TYPE_CUSTOM) No devices found in platform clCreateContextFromType(NULL, CL_DEVICE_TYPE_ALL) Success (1) Platform Name ARM Platform Device Name Mali-G52 r1p0 ICD loader properties ICD loader Name OpenCL ICD Loader ICD loader Vendor OCL Icd free software ICD loader Version 2.2.14 ICD loader Profile OpenCL 3.0 $
夜空の動画像に対する流星検出アルゴリズム
Atomcam をはじめ夜空の動画ファイルから流星を検出するアルゴリズムのメモです。以前 X (旧 Twitter)に投げたものの書き直しになります。
移動中の手慰みに個人的にちょろっと頼まれていた流星検出ロジックの解説。基本、@KinHase さんの meteor-detect と同じ(はず)ものを最小単位で書き直したもの。 pic.twitter.com/DKwonL6vem
— tentaisan (@tentaisan1) 2022年11月22日
Atomcam 周辺のコミュニティで広く使われているmeteor-detectをはじめ、indi-allskyといった実装を調べましたが、だいたい同じアルゴリズムを利用しているようです。
はてなに動画ファイルが上げられないので、流星を含む AtomCam2 で撮影された1分間の対象動画像を構成するの比較明合成の画像を示します。

動画像を対象に可能な限り短くまとめた流星を検出するプログラム全体を示します。
import cv2 capture = cv2.VideoCapture("http://atomcam.local/sdcard/record/20231023/21/22.mp4") r, cache = capture.read() lighten = cv2.UMat(1080, 1920, type=cv2.CV_8UC3, s=(0)) while r: r, frame = capture.read() if r: difference = cv2.subtract(frame, cache) lighten = cv2.max(lighten, difference) cache = frame capture.release() g = cv2.GaussianBlur(lighten, (5, 5), 0) c = cv2.Canny(g, 100, 200, 3) r = cv2.HoughLinesP(c, 1, 3.141592653/180, 25, minLineLength=30, maxLineGap=5) if r.get() is not None: print("detect")
この動画像は動画像の中の 9 フレームに流星が現れています。

この流星をプログラムで検出してみます。
画像の差分による動体抽出
まず、動画像の連続するフレームに対しそのフレームの一つ前のフレームとの差分を計算します。
difference = cv2.subtract(frame, cache)
cache = frame
すると、止まっているつまりは前のフレームと同じ値を持つピクセルは0となり、動いているもののみが値として現れます。

連続する動体の時間軸合成
次に一定の時間の動作とするために複数のフレームの差分画像を比較明合成し、その時間間隔の動作としての画像を作成します。
lighten = cv2.max(lighten, difference)
これにより、流星の場合直線の動きとしての特性が明確になります。

画像からの流星(直線成分)検出
ここまでの処理で動画像の連続する複数の画像から単一の画像が作成されました。以降はこの単一画像から流星と思われる直線成分を検出する処理になります。

ガウシアンフィルターを用いて画像をぼかし、ノイズを落とします。
g = cv2.GaussianBlur(lighten, (5, 5), 0)

Canny法を用いてエッジ検出を行います。
c = cv2.Canny(g, 100, 200, 3)

ハフ変換を用いて直線成分を検出します。
r = cv2.HoughLinesP(c, 1, 3.141592653/180, 25, minLineLength=30, maxLineGap=5)
これによって直線成分が検出されれば、[x1, y1, x2, y2] という直線成分の二点の情報をひとまとまりとして、検出した直線成分の数だけ値が返ります。 この場合は2つの直線成分が検出されています。 直線成分が検出されなければ None が返ります。
[[[1737 655 1787 640]] [[1734 652 1788 636]]]

直線成分が検出されればそのフレームの集合に流星が含まれていると判断されます。

