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 を設定するライブラリです。

github.com

  • FastAccelStepper STEP/DIR/ENABLE を用いてモーターの回転と位置を制御/管理するライブラリです。

github.com

以上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

となりました。

プログラミング

  1. TMCStepper の TMC2209 のドライバコンストラクターの引数は、Serial Port, RSense, Address になります。

    • Serial Port はマイコン側のシリアルポート(今回はハードウェアシリアルとして Serial2)を指定します。
    • RSense は BRA/BRB ピンから GND の間に挟まれるモジュールに実装されている抵抗(sense resister)の値を指定します。MKS TMC2209 V2.0 は回路図から 0.11Ω(0R11)が読み取れるので、0.11f を指定します。
    • UART に繋がれる TMC2209 のアドレスを指定します。今回は一つしか接続せず特に設定を行っていないので 0 となります。(MS1/MS2 ピンの設定で決まります)
  2. TMC2209 で設定した Serial2 を baudrate を指定して初期化し、TMC2209 ドライバを開始、基本的な設定を行います。

    • toff を設定し TMC2209 内部のソフトウェアドライバを開始します。
    • rms_current を用いてモーター電流を設定します。(今回は 900mA)
    • microsteps を用いてマイクロステップを設定します。(今回は 4 を設定し 1/4 としています)
  3. 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)の数値になります。
  4. FastAccelSteooer を設定しモータを動作させます。

    • STEP/DIR/ENABLE ピンを FastAccelStepper にアサインします。
    • 自動で ENABLE ピンを制御させるために setAutoEnable を true にします。
    • モーターを回転させる最大速度を setSpeedInHz を用いて周波数で設定します。
    • モーダーの速度を変化させる加速度を setAcceleration を用いて設定します。
    • move を用いて動作ステップ数を指定しモーターを動作させます。
  5. 出力ループ
    CoolStep と StallGuard の動作を確認するため

    • 負荷の値(SG_RESULT)
    • モーターに供給される電流の値(cs_actual で得られる値を cs2rms で変換した値)
    • StallGuard で制御される DIAG ピンの出力
    • semin/semax/SGTHR で設定された各閾値
      • semin = 5: 5 * 32 = 160 (CoolStep 下限閾値)
      • semax = 2: (5 + 2 + 1) * 32 = 256 (CoolStep 上限閾値)
      • SGTHRS = 100: 100 * 2 = 200 (StallGuard 閾値)
    • FastAccelStepper で設定されるモーター速度(周波数 Hz)

を出力します。出力フォーマットは vscode の Teleplot を用いてグラフ化する事として対応させます。

marketplace.visualstudio.com

実装

#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 下限閾値、それらに対する結果として色のモーターへの供給電流を見ます。

  1. モーターが停止状態から動作を開始し加速を始め SG_RESULT が CoolStep 下限閾値未満の高負荷時は設定された供給電流値まで増加させています。
  2. 加速が続きモーターがある程度の速度になり負荷の値である SG_RESULT が CoolStep 上限閾値以上の低負荷になるとモーターへの供給電流が下がり始め、供給電流の下限値である 1/2 まで下がります。
  3. 動作が続いて指定された動作ステップ数に近づき、減速が開始されると再び負荷が上がり始め SG_RESULT の値が下がり始めます。
  4. 減速が続き、負荷である 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 パッケージ

akizukidenshi.com

  • F4P6 20ピン SSOP パッケージ

akizukidenshi.com

それぞれを DIP 変換基板へ実装し大きさ比較。

akizukidenshi.com

www.wch-ic.com

開発環境

vscode + PlatformIO

Visual Studio Code の PlatformIO のプラグインをインストールし、PlatformIO の設定から ch32v のプラットフォーム拡張をインストールします。また、 WCH-LinkE へのパーミッションを udev の設定で調整します。一通りの設定手順は PlatformIO/CH32V のドキュメントに記述されています。

pio-ch32v.readthedocs.io

wlink は Rust で実装された WCH-LinkE を用いて CH32V を操作する cli tool です。WCH-LinkUtility が Windows のみなので、特に Linux で Delay_Ms(1000); はその代わりとして重宝する tool です。

github.com

プログラミング

折角のはじめての 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 の三種です。

akizukidenshi.com

秋月で販売されている 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 を受信するには

  1. STATUSA(0x12)を読み XTAL_OK(15bit) が 1 (Ready)になるまで待ちます。
  2. VOLUME(0x04)のDMUTE(13bit) を 1 (Mute disable) に設定します。
  3. 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 を受信するには

  1. GPIOCFG2(0x51)の CH_PIN (0〜1bit)を 01 (Key Mode)に設定します。
  2. PLLCFG0(0x04)の SYS_CFGOK(7bit)を 1 (Ready)に設定します。
  3. G38KCFG0(0x1B)を読み POWERON_FINISH(1bit) が 1 (Finish)になるまで待ちます。
  4. LOW_CHAN0(0x0098) と LOW_CHAN1(0x0099) に周波数範囲の下限値を 50,000 で割った値を設定します。例えば 76MHz であれば 76000(KHz) / 50(KHz) = 0x5F0 となり、 LOW_CHAN0(0x0098) に 0x05、 LOW_CHAN1(0x0099)に 0xF0 を設定します。
  5. 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 となります。
  6. 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 以下では書き込みなどでエラーが発生します)

github.com

$ 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

mangopi.org

このページの FW&SDK からブートイメージがダウンロードできるという事になっていますが、

  • 直接ダウンロードできるイメージは Debian/Android 共にブートしない
  • 百度経由のだとイメージではなくダウンロードツールらしきものをインストールさせようとする(したくない)

という状態でした。なのでちょっと試行錯誤し、結論を言えばちょっと作業は必要ですがブートするまでは出来ました。

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 と呼ばれるファームウェアを利用する環境に合わせて作成する必要があります。

github.com

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 本体を構築します。

source.denx.de

まず コードを 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 にパッチが送られているのを見つけました。

lore.kernel.org

これを取り込むと 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 ボタンと繋がれているようです。

www.raspberrypi.com

ここまで来れば 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)に投げた内容の書き直しです。

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" を指定しています。 WindowsMac では OBS の仮想カメラ機能を利用して pyvirtualcam を利用することができるようです。

OrangePi3B OpenCL Driver Install Memo

基本的に RK3588 を対象にしたこちらを参考に

clehaxze.tw

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)に投げたものの書き直しになります。

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]]]

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