https://isehara-3lv.sakura.ne.jp/blog/2025/01/23/hub75-ledアレイの表示画像を任意に変更できるようにし/
作成されてるイメージのアップロードだけではなく、テキスト入力してそれをイメージ化して送信するように機能追加、フォントはMacでは、
コードのGitHubへのリンクに変更はありません
<ブラウザ画面>
<表示されたイメージ>
LEDアレイでの表示ポジションや縮尺、倍率などの改良の余地はありますが、機能的にはこれでほぼ完成形
admin
la vie libre
https://isehara-3lv.sakura.ne.jp/blog/2025/01/23/hub75-ledアレイの表示画像を任意に変更できるようにし/
作成されてるイメージのアップロードだけではなく、テキスト入力してそれをイメージ化して送信するように機能追加、フォントはMacでは、
コードのGitHubへのリンクに変更はありません
<ブラウザ画面>
<表示されたイメージ>
LEDアレイでの表示ポジションや縮尺、倍率などの改良の余地はありますが、機能的にはこれでほぼ完成形
admin
固定した画像ファイルを選択するだけではイマイチだから、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. 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.がdebug-probe(ラズパイ財団純正)でも動くという記述あったので使ってみた
<確認環境>
・M1 Mac Sequia 15.2
・debug-probe
<Baker link. tutorial>
https://baker-link.org/introduction.html
tutorial実行で必要な開発環境としては、
① Docker Desktop(suse版):Rancherなので実際はDockerが起動される
② VScode
③ probe-rs
開発コンソールには、Baker link.Envをインストールして、そこから起動できます
<つかえたところ>
なぜかVScode起動後の『コンテナでサイド開く』クリックでprobe-rs見つからないと言われ(ちゃんとインストしてるのに)たので再度コンソールからインスト、以下はVScode ポップアップメニューのインストールを選んだ後のメッセージ、サイドのインストでも同じメッセージ出たけど無視して進めたら通った
Installation failed. Go to https://probe.rs to check out the setup and troubleshooting instructions. baker link
probe-rsのインストコマンド、
% curl --proto '=https' --tlsv1.2 -LsSf https://github.com/probe-rs/probe-rs/releases/latest/download/probe-rs-tools-installer.sh | sh
F5起動(デバッグスタート)すると実行ファイル見つからないと言われる、ならばとDockerのコンソールからコマンドラインでビルドしてやるとF5でデバッガ起動して普通に使えるようになりました
ハードはHUB75 LEDアレイ駆動時そのままの接続、
コードエディタはVScodeそのまま、新規にウインドを開きます
現在までに種々のデバッグ環境が公開されてますが、Dockerを使う(クロス環境のCross + Podmanに似ている)のが一番筋が良さそうだからこれが今のデフォルト
P.S. リソースは大量消費しているようだけど宿命
admin
基本の表示機能の作成から、
https://isehara-3lv.sakura.ne.jp/blog/2024/12/16/hub75パネルを動かしてみた/
① 画像イメージ情報を3パターン用意
② LED駆動用にUSBの口が二つあるACアダプタ(二つで3Aの容量)用意してラズピコのケーブルも接続できるようにして物としてのまとまりをよくする
USB – DCプラグケーブル購入して、ジャックは見繕って加工してつないだ
③ M5Stackをコントローラーにして3パターンの画像切り替えと表示オフ機能を持たせた
ラズピコとM5Stackのコードと、LEDアレイとラズピコ接続回路図は以下のリンクから、
https://github.com/chateight/hub75_led_array_drive
Arduino言語ではこれでほぼ終わりの予定
admin
Arduino言語との速度比較のために、Rustでループ処理部分をベンチマークしてみた
プラットホームはembassyを使って、そのソースコードに埋め込み、表示パターンは階調表示のあるテストパターン、routine.rsは表示データの初期化を行うだけですが
コードは二つに分割
<main.rs>
//
// % cargo run --bin raspico
#![no_std]
#![no_main]
use embassy_executor::Spawner;
use embassy_rp::gpio::{Level, Output};
use {defmt_rtt as _, panic_probe as _};
mod routine;
use routine::DispData;
#[embassy_executor::main]
async fn main(_spawner: Spawner) {
let p = embassy_rp::init(Default::default());
let mut r1 = Output::new(p.PIN_2, Level::High);
let mut r2 = Output::new(p.PIN_5, Level::High);
let mut g1 = Output::new(p.PIN_3, Level::High);
let mut g2 = Output::new(p.PIN_8, Level::High);
let mut b1 = Output::new(p.PIN_4, Level::High);
let mut b2 = Output::new(p.PIN_9, Level::High);
let mut a = Output::new(p.PIN_10, Level::Low);
let mut b = Output::new(p.PIN_16, Level::Low);
let mut c = Output::new(p.PIN_18, Level::Low);
let mut d = Output::new(p.PIN_20, Level::Low);
let _e = Output::new(p.PIN_22, Level::Low);
let mut clk = Output::new(p.PIN_11, Level::Low);
let mut lat = Output::new(p.PIN_12, Level::High);
let mut oe = Output::new(p.PIN_13, Level::High);
let a_array: [u8; 16] = [0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1];
let b_array: [u8; 16] = [0, 0, 1, 1, 0, 0, 1, 1, 0, 0, 1, 1, 0, 0, 1, 1];
let c_array: [u8; 16] = [0, 0, 0, 0, 1, 1, 1, 1, 0, 0, 0, 0, 1, 1, 1, 1];
let d_array: [u8; 16] = [0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1];
let mut disp_data = DispData {
_array64: [0; 64],
array4d: [[[[0; 64]; 32]; 3]; 2],
};
let new_disp_data = disp_data.new();
loop {
for i in 0..16 {
// row number
if a_array[i] == 0 {
a.set_low();
} else {
a.set_high();
};
if b_array[i] == 0 {
b.set_low();
} else {
b.set_high();
};
if c_array[i] == 0 {
c.set_low();
} else {
c.set_high();
};
if d_array[i] == 0 {
d.set_low();
} else {
d.set_high();
};
for b in 0..15 {
// shading
lat.set_high();
for k in 0..64 {
// column data
clk.set_low();
//
if new_disp_data.array4d[0][2][i][k] > b {
r1.set_high();
} else {
r1.set_low();
}
if new_disp_data.array4d[0][2][16 + i][k] > b {
r2.set_high();
} else {
r2.set_low();
}
if new_disp_data.array4d[0][1][i][k] > b {
g1.set_high();
} else {
g1.set_low();
}
if new_disp_data.array4d[0][1][16 + i][k] > b {
g2.set_high();
} else {
g2.set_low();
}
if new_disp_data.array4d[0][0][i][k] > b {
b1.set_high();
} else {
b1.set_low();
}
if new_disp_data.array4d[0][0][16 + i][k] > b {
b2.set_high();
} else {
b2.set_low();
}
clk.set_high();
}
oe.set_high();
lat.set_low();
oe.set_low();
}
}
}
}
<routine.rs>
表示データの作成をするだけ、
static ARRAY64: [u8; 64] = [
0, 1, 1, 2, 3, 5, 8, 13, 13, 8, 5, 3, 2, 1, 1, 0, 0, 1, 1, 2, 3, 5, 8, 13, 13, 8, 5, 3, 2, 1,
1, 0, 0, 1, 1, 2, 3, 5, 8, 13, 13, 8, 5, 3, 2, 1, 1, 0, 0, 1, 1, 2, 3, 5, 8, 13, 13, 8, 5, 3,
2, 1, 1, 0,
];
pub struct DispData {
pub _array64: [u8; 64],
pub array4d: [[[[u8; 64]; 32]; 3]; 2],
}
impl DispData {
pub fn new(&mut self) -> Self {
let mut dis_data = DispData {
_array64: ARRAY64,
array4d: [[[[0; 64]; 32]; 3]; 2],
};
for j in 0..3 {
for i in 0..32 {
dis_data.array4d[0][j][i] = ARRAY64;
}
}
dis_data
}
}
<実行速度>
およそ10ms(Arduino言語ではおよそ14msだから3割程度高速化)、繰り返し(メモリ読み出しとgpio書き込み)回数は一回のリフレッシュループで、
64*3*16*32 = 98,304(16行*3色*パネル上下*16階調)ということで一回のr/g/bデータの書き込みでおよそ1μs費やしている勘定で、クロック数ではおよそ130クロックとなります、階調処理での条件分岐が処理時間では大きそうですね
写真は行アドレス信号のDなので一周期がリフレッシュ周期
結果だけ見るとRustの優位性はそれほど無いように見えるかもしれないけど、メモリ管理やコンパイラの出来の良さ、複数タスク管理をasync/awaitで簡単に実現できるから他の言語で実現(Arduino言語ならRTOS使うことになるけれども、async/awaitのほうが軽量だと)するよりもアドバンテージはあると思う
P.S. 2024/12/24
この程度(3割)の改善ならば、pico 2を使えば簡単に逆転できそうだよね、消費電力とか別にすればね、ハードの進化は偉大なりか
admin
オーダーしていたパーツが届いたので動かしてみた
HUB75のフラットケーブルをつなぐためにユニバーサルボードでラズピコとコネクタの配線、GPIOとの対応付はAmazonで売り切れになっている接続ボードのコネクションリストと同じにした、何かの時に役立つかもしれないから
最初はテストパターン表示
フィボナッチ数列の13までの昇順と降順でちょうど16になるからそれを4回繰り返したデータを用意して表示させてみた、リフレッシュサイクルは60フレーム/s以上(akafuji表示状態で測定すると1000/14 =71.429 フレーム/s)だから人の目には綺麗に見える
次にイメージデータを表示させてみる、データは「赤富士」のフリー素材を持ってきて最終的にb/g/rの[3][32][64]配列に落とし込む、実行はPython使って最終的にはコンソールに出力された以下のcolor_code変数をプリントしたものをArduino IDEに切り貼り
#
# to make r/g/b array from shrinked bmp image
#
from PIL import Image
import numpy as np
import cv2
# resize the image file
image = Image.open("akafuji.png")
resized_image = image.resize((64, 32))
resized_image.save("resized.bmp")
# get b/g/r value
image = cv2.imread('resized.bmp')
color_code = [[[0 for i in range(64)] for j in range(32)] for k in range(3)]
for x in range(64):
for y in range(32):
pixel = image[y, x]
color_code[0][y][x] = image[y, x, 0]//16
color_code[1][y][x] = image[y, x, 1]//16
color_code[2][y][x] = image[y, x, 2]//16
print(color_code)
元の64*32のbmpファイルは、
こんな感じ、
これをLEDで表示させると、
ゴミが出てます、データそのものは0になっているから、なんかしらの要因でそうなってますね
ちなみに、Arduino IDEのコードの核の部分は以下の通り
<main処理>
void setup1(){
// set the gpio pins direction
pinMode(r1, OUTPUT);
pinMode(r2, OUTPUT);
pinMode(g1, OUTPUT);
pinMode(g2, OUTPUT);
pinMode(b1, OUTPUT);
pinMode(b2, OUTPUT);
pinMode(clk, OUTPUT);
pinMode(lat, OUTPUT);
pinMode(oe, OUTPUT);
pinMode(a, OUTPUT);
pinMode(b, OUTPUT);
pinMode(c, OUTPUT);
pinMode(d, OUTPUT);
pinMode(e, OUTPUT);
gpio_put(oe, HIGH);
gpio_put(lat, LOW);
gpio_put(clk, LOW);
}
void loop1(){
// message check from core0
//
while (rp2040.fifo.available()>0){
// bool rp2040.fifo.pop_nb(uint32_t *dest)
message = rp2040.fifo.pop();
}
// led display refreah loop
//
for (row_c = 0; row_c < 16; row_c++){
// select row address
gpio_put(oe, HIGH);
gpio_put(a, a_sel[row_c]);
gpio_put(b, b_sel[row_c]);
gpio_put(c, c_sel[row_c]);
gpio_put(d, d_sel[row_c]);
gpio_put(e, e_sel[row_c]);
// display row data with shade control
for (byte j = 0; j < 15; j++){
row_set(j);
gpio_put(oe, HIGH);
clock_func(lat);
gpio_put(oe, LOW);
}
}
//mov_data(1, 2);
}
<common.h>
行選択信号のeは64*32では使わないので常時LOW
// gpio pin assignment for HUB75
#define r1 (2)
#define r2 (5)
#define g1 (3)
#define g2 (8)
#define b1 (4)
#define b2 (9)
#define clk (11)
#define lat (12)
#define oe (13)
#define a (10)
#define b (16)
#define c (18)
#define d (20)
#define e (22)
// row select matrix
byte row_c = 0; // row counter
byte a_sel[32] = {0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1};
byte b_sel[32] = {0, 0, 1, 1, 0, 0, 1, 1, 0, 0, 1, 1, 0, 0, 1, 1, 0, 0, 1, 1, 0, 0, 1, 1, 0, 0, 1, 1, 0, 0, 1, 1};
byte c_sel[32] = {0, 0, 0, 0, 1, 1, 1, 1, 0, 0, 0, 0, 1, 1, 1, 1, 0, 0, 0, 0, 1, 1, 1, 1, 0, 0, 0, 0, 1, 1, 1, 1};
byte d_sel[32] = {0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1};
byte e_sel[32] = {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1};
<functions>
clock_func()でgpis_put()が一回だけだとパルスが細すぎてLEDパネルでうまくラッチできないから3回書き込みでパルス幅確保
// to make clock pulse
void clock_func(byte port){
//digitalWrite(0, HIGH);
gpio_put(port, 1); // in case of wite once, data wouldn't be captured correctly
gpio_put(port, 1);
gpio_put(port, 1);
//digitalWrite(0, LOW);
gpio_put(port, 0);
}
// row data buffer write
void row_set(byte shade){
for (byte i = 0; i < 64; i++){
// akafuji color order b/g/r
// red
if (akafuji[2][row_c][i] > shade){
gpio_put(r1, 1);
}
else{
gpio_put(r1, 0);
}
if (akafuji[2][row_c + 16][i] > shade){
gpio_put(r2, 1);
}
else{
gpio_put(r2, 0);
}
// green
if (akafuji[1][row_c][i] > shade){
gpio_put(g1, 1);
}
else{
gpio_put(g1, 0);
}
if (akafuji[1][row_c + 16][i] > shade){
gpio_put(g2, 1);
}
else{
gpio_put(g2, 0);
}
//blue
if (akafuji[0][row_c][i] > shade){
gpio_put(b1, 1);
}
else{
gpio_put(b1, 0);
}
if (akafuji[0][row_c + 16][i] > shade){
gpio_put(b2, 1);
}
else{
gpio_put(b2, 0);
}
// write to row buffer
clock_func(clk);
}
}
文字も自分でフォント作るよりも、既存のフォントで文字作ってそれをイメージ化してbmpデータとするのが現実的に思える
P.S.
いきなりですが、ゴミ対策はアドレス選択変更前に表示オフを追加すれば良い、つまりrow選択アドレス変える時には表示を一旦オフにしろという原則通り、
// select row address
gpio_put(oe, HIGH);
iPhoneでlive撮影して、平均化してしまうと輝度低下して冴えない写真になるのは致し方なしだね
admin
Arduinoはコンパイル時にcppに落とされてからコンパイルされるので、基本はコンパイラ言語ですが、学習用にかなり冗長な作りになっているようなので、実行速度が問題になるケースも多そうです
というわけで簡単にベンチマーク
元はLEDブリンクのソースに計測用のコードを追加しています
<mainのinoファイル>
ただし時間測定時には、loop1()のwhile処理(loop0()からのメッセージ受信)はコメントアウト
#include "common.h"
// constants won't change. Used here to set a pin number:
const int ledPin = LED_BUILTIN; // the number of the LED pin
// Variables will change:
int ledState = LOW; // ledState used to set the LED
// Generally, you should use "unsigned long" for variables that hold time
// The value will quickly become too large for an int to store
unsigned long previousMillis = 0; // will store last time LED was updated
// constants won't change:
const long interval = 500; // interval at which to blink (milliseconds)
uint32_t message_s = 0;
uint32_t message = 999;
void setup() {
// set the digital pin as output:
pinMode(ledPin, OUTPUT);
}
void loop() {
// send message to core1
rp2040.fifo.push(message_s);
message_s += 1;
unsigned long currentMillis = millis();
if (currentMillis - previousMillis >= interval) {
// save the last time you blinked the LED
previousMillis = currentMillis;
// if the LED is off turn it on and vice-versa:
if (ledState == LOW) {
ledState = HIGH;
} else {
ledState = LOW;
}
// set the LED with the ledState of the variable:
digitalWrite(ledPin, ledState);
}
}
void setup1(){
pinMode(0, OUTPUT);
}
void loop1(){
while (rp2040.fifo.available()>0){
message = rp2040.fifo.pop();
message *= 10;
}
for (int i = 0; i < 8; i++){
for (int j = 0; j < 8; j++){ if (disp_data[i][j] > 7){
dest_data[i][j] = disp_data[i][j];
}
else{
dest_data[i][j] = 0;
}
}
}
clock_func();
}
<common.hファイル:byte型の二次元配列を2個確保しています>
データの中身に取り立てて意味はありません
// display data
byte disp_data[8][8] = {
{0,1,1,2,3,5,8,13},
{13,8,5,3,2,1,1,0},
{0,1,1,2,3,5,8,13},
{13,8,5,3,2,1,1,0},
{0,1,1,2,3,5,8,13},
{13,8,5,3,2,1,1,0},
{0,1,1,2,3,5,8,13},
{13,8,5,3,2,1,1,0}
};
byte dest_data[8][8] = {
};
<clock.inoファイル>
gpioへの書き込みが極端に遅いので、c++の関数を呼び出せるということなので切り替えた(digitalWrite() -> gpio_put())、パルス幅確保のためにハイレベルを5回書き込んでます
void clock_func(){
//digitalWrite(0, HIGH);
gpio_put(0, 1);
gpio_put(0, 1);
gpio_put(0, 1);
gpio_put(0, 1);
gpio_put(0, 1);
//digitalWrite(0, LOW);
gpio_put(0, 0);
}
実行速度の測定結果
測定はgpio0のポイントをオシロのプローブであたって時間測定
・digitalWrite()使用時
・gpio_put()使用時
考察
ループの外でGPIO書き込みを行っているのに、全体の処理時間を3割程度長くしてしまうArduinoの標準書き込み関数は時間がクリティカルな場所では使えない、配列の処理に関してはcに比較したら遅いだろうけれども許容範囲か、ただしコードの記述には工夫が必要(配列の要素を一度変数に入れてから処理とかはやめた方が良い
やりたいことはLEDアレイ(HUB75で64*32)の点灯をコア1で行うようにしたいから、16階調処理だと一行のループ処理をおよそ69μs以内(50Hz以下になると人の目に見えるから、1000000/(60*15*16) =69.444μs )ぐらいで実行必要だからターゲットはそこだけど、なんとかなりそうな感触
admin
ラズパイzeroでもコアが一個しかないのに、picoには二個あります、その分クロック周波数は1GHz vs 133MHzですが、
で、マルチコアを使ったアプリとしてHUB75インターフェースのLEDパネルを駆動させるのが取っ付きやすそうに思う
なぜなら、LEDパネルは擬似的な点灯(だから静止画や動画撮影ではまともに見えない)で時間を保証(数十μsオーダー)して点滅させないといけないので、通信や表示データを用意する余裕はないだろうから、表示制御にはコア一個を割り振り、もう一個のコアでは通信と表示データを用意させる処理を実行させる
無論メモリは両方のコアで共通だから、完全に負荷分散ができる訳でもないけどシングルコアよりはかなり余裕があるはず、上はメモリやペリフェラル含めた全体図、下はコア部分だけに着目したブロック図でAHB-Liteがコアの分離とアクセス制御をやっているように見える
データの受け渡しは、表示データの完全性の保証は必要ないからFIFOは制御用のデータ受け渡しに使い、表示データは共通のRAMエリアで2面持たせて、切り替え使用するようにすればいいだろう
admin