窓処理と計算方法の見直し(CMSIS-DSP)

現実には窓関数を使うから、窓をかけるのと計算方法(元のソースは最後の出力配列作成時のq15の扱い方が変)だったので処理方法を見直して、対数表示でも違和感ないように見直し

<コードの波形部分以外の全部>

/* See https://m0agx.eu/practical-fft-on-microcontrollers-using-cmsis-dsp.html */

#include <math.h>
#include <ltstdio.h>

#include "arm_math.h"
#include "pico/stdlib.h"
#include "pico/time.h"

unsigned char __697hz_raw[] = {// データ部分は省略};

unsigned int __697hz_raw_len = 512;

#define FFT_SIZE 256

// バッファ宣言
q15_t input_signal[FFT_SIZE];   // 入力(実数信号)
q15_t fft_output[FFT_SIZE * 2]; // 出力(複素数 interleaved)
q15_t mag_squared[FFT_SIZE];    // パワースペクトル(Q13形式)

void perform_fft_and_power_spectrum(arm_rfft_instance_q15 *instance, q15_t *input, q15_t *output, q15_t *power_spectrum)
{
    arm_rfft_q15(instance, input, output);
    arm_cmplx_mag_squared_q15(output, power_spectrum, FFT_SIZE);
}

int main()
{
    stdio_init_all();
    sleep_ms(1000);

    q15_t *input = (q15_t *)__697hz_raw;
    q15_t windowed_input[FFT_SIZE];

    float scaling_factor = 1.0f / 8192.0; // Q13 → float
    float hann_correction = 1.0f / 0.5f;  // ハニング窓で約0.5倍になる補正

    // ハニング窓
    for (int n = 0; n < FFT_SIZE; n++) { float hann = 0.5f * (1.0f - cosf(2.0f * M_PI * n / (FFT_SIZE - 1))); q15_t hann_q15 = (q15_t)(hann * 32767.0f); int32_t val = (int32_t)input[n] * hann_q15; windowed_input[n] = (q15_t)(val >> 15);
    }

    arm_rfft_instance_q15 fft_instance;
    arm_status status = arm_rfft_init_q15(&fft_instance, FFT_SIZE, 0, 1);
    printf("FFT init %d\n", status);

    uint32_t start_time = time_us_32();

    perform_fft_and_power_spectrum(&fft_instance, windowed_input, fft_output, mag_squared);

    uint32_t end_time = time_us_32();

    printf("Execution time: %.2f us per FFT\n", (float)(end_time - start_time));

    for (uint32_t j = 0; j <= FFT_SIZE / 2; j++)
    {
        float mag = mag_squared[j] * scaling_factor * hann_correction;
        float db = 10.0f * log10f(mag + 1e-12f); // 0対策のオフセット付き
        printf("Bin %3u: %.2f dB\n", j, db);
    }

    printf("\n");
    __BKPT(1);
}

<結果>

窓はHanningが適切っぽいし、フレーム数も256で簡易表示なら速度的にも速いから良さそうだ

ADCはノイジーだけど内蔵の12 bitsでやってみるのがPoCには適切だろうと思う

 

admin

ラズピコ2でのFFT実行時間(CMSIS-DSP)

以下の記事のCMSIS-DSPの実行時間を測定してみた

https://isehara-3lv.sakura.ne.jp/blog/2025/06/04/cmsis-dspをラズピコ2で使う/

該当部分だけのコードは、

    uint32_t start_time = time_us_32();
    for (int i = 0; i < ITERATIONS; ++i) {
        arm_rfft_q15(&fft_instance, input, output);
    }
    uint32_t end_time = time_us_32();

    printf("\nExecution time: %.2f us per FFT\n", 
          (float)(end_time - start_time) / ITERATIONS);

ITERATIONSを2以上にすると結果が変になる

つまり、inputがワークとして使われて破壊されてしまうようだ、一回だけ実行させた時の計算時間は、

Execution time: 392.00 us per FFT/

この数値、

https://github.com/jptrainor/cmsis-sandbox

にある、

fft execution time (us)
               32     64    128    256
 clean_q15    259    331    511    869

の256フレーム時の869μsと比較すると半分以下だから、それだけ高速化されているということ

但し、リンク先の情報でもわかるようにデータによって計算時間は変わるようで、clean(つまり単純正弦波)とnoizy(ノイズ重畳)ではかなり時間差ありますが、今回は正弦波だから、この869μsと比較するのが妥当

ともかくも256フレームでこの程度の実行時間であれば、小型のLCD(320*240)に表示する前提ならば問題ないレベルでしょう

 

admin

pico-sdkでADCからのDMA転送とADC精度

VScodeのラズピコエクステンション(Raspberry Pi Pico)にいくつかのサンプルコードがあるのでその中から内蔵ADCからDMA転送するコードがあるので、ついでにADCの精度も見てみた

<環境>

M4 MacBook, VScode、ラズピコエクステンション(Raspberry Pi Pico)使用

<変更箇所>

・結果を4bitシフトで8bitにしてるのを12bit獲得する

・ADC読み出し値の最大値と平均値を求める処理追加(GPIO26はGND接続)

・USBシリアルを使えるようにCMakeに追加(但し動作がイマイチ不安定、予想外の時に出力されて、期待した時は出力されない、なのでデバッガに期待)

・コンパイラが最適化すると見れないローカル変数があるので、変数が見れるようにDebugモードにするようにCMakeで設定

<接続>

こんな感じでGNDに落とす、

 

<結果>

予想通り、LSB側の3ビットぐらいはノイズに埋もれてる

 

<全体のコード>

core1側で三角波作って外部のDACで波形作成して、ADCに入力することを想定しているね

DMA処理自体はレジスタ操作とかは不要で、pico-sdkに関数が用意されている

/**
 * Copyright (c) 2021 Raspberry Pi (Trading) Ltd.
 *
 * SPDX-License-Identifier: BSD-3-Clause
 */
//
// ADC values are 4bits shifted, so the max value is 255
//
// -> modify to handle full scale 12bits(2025/6/4)
// it seems noisy, LSB 3bits are inaccrate
// 
#include <stdio.h>
#include "pico/stdlib.h"
// For ADC input:
#include "hardware/adc.h"
#include "hardware/dma.h"
// For resistor DAC output:
#include "pico/multicore.h"
#include "hardware/pio.h"
#include "resistor_dac.pio.h"

// This example uses the DMA to capture many samples from the ADC.
//
// - We are putting the ADC in free-running capture mode at 0.5 Msps
//
// - A DMA channel will be attached to the ADC sample FIFO
//
// - Configure the ADC to right-shift samples to 8 bits of significance, so we
//   can DMA into a byte buffer
//
// This could be extended to use the ADC's round robin feature to sample two
// channels concurrently at 0.25 Msps each.
//
// It would be nice to have some analog samples to measure! This example also
// drives waves out through a 5-bit resistor DAC, as found on the reference
// VGA board. If you have that board, you can take an M-F jumper wire from
// GPIO 26 to the Green pin on the VGA connector (top row, next-but-rightmost
// hole). Or you can ignore that part of the code and connect your own signal
// to the ADC input.

// Channel 0 is GPIO26
#define CAPTURE_CHANNEL 0
#define CAPTURE_DEPTH 1000

uint16_t capture_buf[CAPTURE_DEPTH];


void calc_max_and_avg(const uint16_t *buf, size_t len, uint16_t *max_val, double *avg_val) {
    uint16_t max = 0;
    uint32_t sum = 0;
    for (size_t i = 0; i < len; ++i) { if (buf[i] > max) max = buf[i];
        sum += buf[i];
    }
    *max_val = max;
    *avg_val = (double)sum / len;
}


void core1_main();

int main() {
    stdio_init_all();

    // Send core 1 off to start driving the "DAC" whilst we configure the ADC.
    multicore_launch_core1(core1_main);

    // Init GPIO for analogue use: hi-Z, no pulls, disable digital input buffer.
    adc_gpio_init(26 + CAPTURE_CHANNEL);

    adc_init();
    adc_select_input(CAPTURE_CHANNEL);
    adc_fifo_setup(
        true,    // Write each completed conversion to the sample FIFO
        true,    // Enable DMA data request (DREQ)
        1,       // DREQ (and IRQ) asserted when at least 1 sample present
        false,   // We won't see the ERR bit because of 8 bit reads; disable.
        false    // not Shift each sample to 8 bits when pushing to FIFO
    );

    // Divisor of 0 -> full speed. Free-running capture with the divider is
    // equivalent to pressing the ADC_CS_START_ONCE button once per `div + 1`
    // cycles (div not necessarily an integer). Each conversion takes 96
    // cycles, so in general you want a divider of 0 (hold down the button
    // continuously) or > 95 (take samples less frequently than 96 cycle
    // intervals). This is all timed by the 48 MHz ADC clock.
    adc_set_clkdiv(0);

    printf("Arming DMA\n");
    sleep_ms(1000);
    // Set up the DMA to start transferring data as soon as it appears in FIFO
    uint dma_chan = dma_claim_unused_channel(true);
    dma_channel_config cfg = dma_channel_get_default_config(dma_chan);

    // Reading from constant address, writing to incrementing byte addresses
    channel_config_set_transfer_data_size(&cfg, DMA_SIZE_16);
    channel_config_set_read_increment(&cfg, false);
    channel_config_set_write_increment(&cfg, true);

    // Pace transfers based on availability of ADC samples
    channel_config_set_dreq(&cfg, DREQ_ADC);

    dma_channel_configure(dma_chan, &cfg,
        capture_buf,    // dst
        &adc_hw->fifo,  // src
        CAPTURE_DEPTH,  // transfer count
        true            // start immediately
    );

    printf("Starting capture\n");
    adc_run(true);

    // Once DMA finishes, stop any new conversions from starting, and clean up
    // the FIFO in case the ADC was still mid-conversion.
    dma_channel_wait_for_finish_blocking(dma_chan);
    printf("Capture finished\n");
    adc_run(false);
    adc_fifo_drain();   // max four samples

    // Print samples to stdout so you can display them in pyplot, excel, matlab
    for (int i = 0; i < CAPTURE_DEPTH; ++i) {
        printf("%u, ", capture_buf[i]);
        if (i % 10 == 9)
            printf("\n");
    }

    uint16_t max_value;
    double average;

    calc_max_and_avg(capture_buf, CAPTURE_DEPTH, &max_value, &average);

    printf("Max: %u\n", max_value);
    printf("Average: %f\n", average);
}

// ----------------------------------------------------------------------------
// Code for driving the "DAC" output for us to measure

// Core 1 is just going to sit and drive samples out continuously. PIO provides
// consistent sample frequency.

#define OUTPUT_FREQ_KHZ 5
#define SAMPLE_WIDTH 5
// This is the green channel on the VGA board
#define DAC_PIN_BASE 6

void core1_main() {
    PIO pio = pio0;
    uint sm = pio_claim_unused_sm(pio0, true);
    uint offset = pio_add_program(pio0, &resistor_dac_5bit_program);
    resistor_dac_5bit_program_init(pio0, sm, offset,
        OUTPUT_FREQ_KHZ * 1000 * 2 * (1 << SAMPLE_WIDTH), DAC_PIN_BASE);
    while (true) {
        // Triangle wave
        for (int i = 0; i < (1 << SAMPLE_WIDTH); ++i)
            pio_sm_put_blocking(pio, sm, i);
        for (int i = 0; i < (1 << SAMPLE_WIDTH); ++i)
            pio_sm_put_blocking(pio, sm, (1 << SAMPLE_WIDTH) - 1 - i);
    }
}

 

admin

CMSIS-DSPをラズピコ2で使う

ラズピコ2の特徴の一つである、FPUとGPUを活用するのにちょうど良さそうなのがARM系プロセッサ向けに用意されているCMSIS-DSP、ラズピコのハードにも最適化されているので、例えばArduinoライブラリよりもFFT処理が高速化できるし、MNISTで使っている事例もあります

ラズピコに比較しておよそ5倍程度の高速化というような情報もあります

以下、VScodeでラズピコ2の環境を用意したので、コードからCMSIS-DSPを使えるようにします

https://isehara-3lv.sakura.ne.jp/blog/2025/05/27/ラズピコ2の開発環境vscode-raspberry-pi-pico/

<実行環境>

ラズピコ2W + デバッグボード、M4 MacBookVScode + Raspberry pi pico extension

参考になったのは、やはりRaspberryPI財団のページで、

https://forums.raspberrypi.com/viewtopic.php?t=365053

手順を述べると、

① ラズピコにカスタマイズされたスタティックライブラリを作成する

作成されるのは、libCMSISDSP.aという名称になります

② main.cからは①で作成したスタティックライブラリと、CMSIS関連ヘッダファイル群を呼び出せるようにinclucdeする

1-1 CMSIS-DSPのライブラリを作成するためのディレクトリ構成

ビルドの例は以下の通り、

mkdir build
cd build
cmake ..
make -j4

1-2 サンプルプログラムとCMSIS-DSPライブラリの組み込み

https://forums.raspberrypi.com/viewtopic.php?t=365053

にあるFFTを実行するサンプルプログラムをそのまま使用しました

“arm_math.h”がCMSIS-DSPを使用するための宣言です

VScodeで作成されたCMakeファイルに、以下の🔴部分の3行を追加します

これでRASPBERRY PI PICO PROJECTのCompile ProjectでUF2ファイルが作成されて、Flushすればラズピコ上で動作開始します

③ 実行結果

USBシリアルのポートは二つ見えますが、常時見えているのがデバッガー側、ラズピコ2側は瞬間でしか見えないので要注意、というかデバッガ使わないと見ることはできないと思う

コンソールに印字された結果(256個出てきますが、re/imの組み合わせなので実質128個、それの二乗平均しています)をコピーしてリニア軸で図にすると、

さらに、対数軸で見てみると、

それらしいグラフになっています

 

admin

ラズピコ2の開発環境(VScode + Raspberry Pi Pico)

Arduino IDEはもっさり感もありますが、機能も不足していて本格的に使おうとするとイマイチ

じゃVScode + PlatformIOどうなのと言うと、ラズパイ財団の関係からラズピコは公式サポートなし、と言うわけなのでラズパイ財団おすすめのVScodeの拡張機能(Raspberry Pi Pico)を使う、名前そのままですがVScodeの拡張機能で出てくるのでそれをインスト

プロジェクト管理画面から、作成、ビルド、書き込み、デバッグができます

定番のHello worldコードがデフォルトで作成(一部変更しています)されるので、

#include 
#include "pico/stdlib.h"


int main()
{
    stdio_init_all();
    int a = 10;
    while (true) {
        printf("Hello, world! a = %d\n", a);
        sleep_ms(1000);
        a += 1;
    }
}

ビルドしてラズピコ2にデバッガ経由で転送、

シリアル出力の有効化は、

% ls /dev/tty.*
/dev/tty.Bluetooth-Incoming-Port	/dev/tty.usbmodem1202
/dev/tty.debug-console			/dev/tty.usbmodem1301

usbmodemが二個見えてますが、どちらか一方はデバッガー

二つしかないから、交互に選択してみると、

USB-Serialでコンソールに出力されています

デバッガは、

こんな感じで普通に動作、変数aはコンパイラの最適化でprintf文中で使わないと落とされます、デバッガ機能確認用に定義したがprintfで使わないとデバッガで見えない

 

admi

ラズピコ2 W

ラズピコ2もサポートが整ってきたと思うので購入(ピンなしでdebug端子含めて後付け)、ラズピコに比較すると例えばIoTなどのセキュアアプリケーション構築には向きそうです

<セットアップ>

Arduino IDE(2.3.5)での設定、w/ debug probe

と、

既存のrp2040用のコードをコンパイル(そのままコンパイル可能)してアップロードすると、

デバッガーも使えました

<性能比較>

HUB75のLEDアレイのスキャンタイム(ラズピコがほぼ14ms:リフレッシュサイクルでおよそ70Hzでラズピコ2が10ms切るぐらい)はラズピコでのRustの実行速度とほぼ同じだから、ハードの進化は偉大なり

使い道は今のところ未定、例えばTelloのコントローラーとしてはコマンド制御はともかくも、動画のストリーミング処理にはメモリもCPU能力も足りないし、当然ライブラリもない

 

admin

HUB75 LEDアレイ表示にテキストのイメージ化機能も追加

https://isehara-3lv.sakura.ne.jp/blog/2025/01/23/hub75-ledアレイの表示画像を任意に変更できるようにし/

作成されてるイメージのアップロードだけではなく、テキスト入力してそれをイメージ化して送信するように機能追加、フォントはMacでは、

fontPath:=“/System/Library/Fonts/ヒラギノ角ゴシック W3.ttc”
を使っていますが、ラズパイではヘッドレスでフォントもインストールされてないからそこから始めたないとダメ

コードのGitHubへのリンクに変更はありません

<ブラウザ画面>

<表示されたイメージ>

LEDアレイでの表示ポジションや縮尺、倍率などの改良の余地はありますが、機能的にはこれでほぼ完成形

 

admin

 

 

 

HUB75 LEDアレイの表示画像を任意に変更できるようにした

固定した画像ファイルを選択するだけではイマイチだから、webサーバー(ラズパイzero)にブラウザ経由で任意の画像をアップロードして、サーバー側で画像の圧縮処理、rgb抽出を行いTCPでラズピコに送信して表示させるようにした

構成はwebアプリをgolangで作成し、ラズピコ側のコードも対応して変更、途中でデータロスはできないからUDPはあり得なくてTCPは前提

コード生成はPerplexityが無償でもgolangでは使えるコードが出てくるので活用、多分コードの生産性からいったら倍以上にはなる

<web application code>

https://github.com/chateight/golang/tree/master/image

<raspberry pi pico code>

https://github.com/chateight/hub75_led_array_image_upload/tree/main

ラズパイzeroでの実行速度は150KBぐらいのイメージファイルでLEDアレイに表示されるまで2秒ちょっとといったところ(Wi-Fi転送は過去の経験で1MB/secは出るからほぼイメージ処理の時間)だけど実用的には問題ないレベル

ラズピコ側でTCP通信するのにどうやるのかを検索してもイマイチだったので、それもPerplexityからの回答で、主要なコードを抜き出すとこんな感じ、

void setup(){
  IPAddress staticIP(192, 168, 1, 200); 
  IPAddress gateway(192, 168, 1, 1);
  IPAddress subnet(255, 255, 255, 0);

  WiFi.config(staticIP, gateway, subnet);

  WiFi.begin(ssid, password);
  while (WiFi.status() != WL_CONNECTED) {
    delay(500);
    Serial.println("Connecting to WiFi...");
  }
  server.begin();
}

void loop() {
  client = server.accept(); // TCP start
  // if network is active
  if (client) {
    while (client.connected()) {
      if (client.available()) {
        // receive binary data
        int bytesRead = 0;
        unsigned long startTime = millis();
        
        // Read all available data within a 500ms window
        while (client.available() && (millis() - startTime < 500) && bytesRead < MAX_BUFFER) { 
      receivedData[bytesRead] = client.read(); 
      bytesRead++; 
     }
        
        // response to the server
        const char* response = "Resp from Pico W!";
        client.write(response, strlen(response));
      }
  }

今更ながらコード作成でもLLMはもはや必需品で、用途ごとに要求されるものが違うからそれは人間社会と同じかもしれない

 

admin

 

 

Baker link. 起動手順についての補足

Baker link. ENVは今少し不安定(Dockerの対応するコンテナを削除しないと動かない時がある)のように見えるので、手順を補足説明

<環境>

M1 MacBook Air sequoia

cargo/rustc 1.82.0

Rancher Desktop 1.16.0

<手順>

基本は、

https://baker-link.org/Blink/Baker_link_Env_page.html

ですが、途中いくつか鬼門あるので手順を記載

① historyから作成済みならば、作成済みプロジェクトの履歴からクリックして開く

② 「コンテナで再度開く」を選択

ここでコンテナにファイルが存在しない状態、つまりVScodeでソースコードが開かない時があって、その場合にはDockerのコンテナ(dockerという名前)を削除して最初からやるとうまくいく、imageはbaker-link-envという名前でこれが残っていればコンテナ展開されるだけ、このイメージがDockerに存在していなければBaker link. ENVがダウンロードするんだろう

③ probe-rs見つからないと言われる -> install/failどちらも無視

④ F5でデバッガ起動

⑤ 「cargo buildを見つけられません」のポップアップ -> 「このままデバッグ」を選択(すでに作成済みなので)

もし現在のソースファイルでビルドされていなければ、bash(Docker コンソール)でビルドする(qemu-system-aarch64使っているのでそこそこ遅い)

F5を押したタイミングでラズピコにDocker上のビルドされたバイナリを転送するようです

⑥ デバッグボタンが現れる

 

⑦ デバッグボタン列の一番左の青ボタンでデバッグが起動する

この時にはdfmtコンソール現れてデバッグ開始できる状態になってます

コンソールで前回の出力と連続しているけれども、INF0 Program start!がソースの22行目に埋め込まれてるinfo!(“Program start!”);からの出力でデバッグ開始の確認になります

 

admin

Baker link.で作成済みのプロジェクトを再度開くとき

作成したBaker link.のプロジェクトを再度開く時にどうするのかなと思って、再度作成をしてしまうとすでに作成されたプロジェクトに階層構造でつながって新たに作成されます

 

 

Baker link. Envのhistoryボタンをクリックすると過去に作成されたプロジェクトが出てくるのでそこをクリックすれば良いだけです、新規に階層構造で作成されたプロジェクトはFinderで削除実施

画像は削除した履歴(三階層の二番目と三番目)を指定しているのでLog出力で見つからないと言われてます

 

admin