ハードとソフト
hub75eとは?
本題に入る前に,今回使用したマトリクスLEDについてざっくり説明します.
hub75はマトリクスLEDのインターフェースの規格で,hub75eはそれに「e」ピンが追加されたものです.hub75では行の指定が「a,b,c,d」ピンの4bitしかないので32行までしか点灯させられません(注: 16行ではない)が,hub75eなら5bitあるので64行表示できます(注: 32行ではない).
以降はhub75eの説明になりますが,行を指定するビットが一つ減るだけで,本質的にはhub75も同じ感じです.
hub75eでは行を指定して一度に二行ずつ光らせます.例えば5'b00000
のときは0行目と32行目が,5'b00001
のときは1行目と33行目が,,,5'b11111
のときは31行目と63行目が光ります.
各列の色はどう指定するかというと,RGBそれぞれで上側の色と下側の色(5'b00000
のときは0行目の色と32行目の色)を指定するためのピンがあり,それぞれをhigh
またはlow
にします.その後クロックを立ち上げると一列分書き込まれ,また次の列のデータを指定してクロックを立ち上げ,,,を繰り返して64列のデータを指定します.
なお,点灯はRGBそれぞれを光らせる・光らせないの8色しかなく,一度に点灯させられるのも2行だけです.素早く色や行を切り替えてPWM表示・ダイナミック点灯することで,マトリクス全体にいろいろな色が表示されているように見せます.
目標
- PYNQ-Z2のSDカードに画像データをのせよう
- 画像データをPSで読み取ろう
- 読み取った画像データをPLへ送ろう
- 画像データをマトリクスLEDへ送ろう
PYNQ-Z2のSDカードに画像データをのせよう
使用したマトリクスLEDが64x64なので,Pythonを使って正方形の画像を64x64に整形しました.
表示画像はこちら.
上記GIF画像をkana.gif
という名前でダウンロードし,画像一枚ずつに分け,64x64にしました.
import cv2
SIZE = 64
# GIF画像読み込み
cap = cv2.VideoCapture("kana.gif")
# 一枚ずつ分けて保存
i = 0
while (cap.isOpened()):
ret, frame = cap.read()
if ret:
# 正方形にする(縦長画像なので高さを横幅に合わせた)
frame = frame[0:490, :, :]
# 64x64にする
frame = cv2.resize(frame, (SIZE, SIZE))
# 保存
cv2.imwrite(f"kana_img/{i:04d}.png", frame)
i += 1
こうして作成した画像をJupyter Notebook経由でSDカードに保存しました.
画像データをPSで読み取ろう
少し後の話になるのですが,PYNQ-Z2のDMAをPythonでやろうとすると,pynqライブラリのバージョンが古くうまくいきませんでした.アップグレードも上手くいかなかったので潔く(?)C++で挑むことにしました.
PYNQ-Z2にC++のOpenCVをインストールしました.「linux opencv c++」とかで調べたらインストール方法が出てくると思うので,ここでは割愛.
余談ですが,PYNQ-Z2についての情報を調べようとしてもほとんどヒットしません.しかしlinuxでの方法を調べると,PYNQ-Z2に応用できそうな情報がたくさんヒットします.
これで画像データが読み込めるようになりました.実際のプログラムは次の章でやります.
読み取った画像データをPLへ送ろう
次にpynq_apiライブラリをインストールしました.基本的にはGithubのreadme.mdに従えばインストールできます.一つ注意点として,readmeにはリンカ指定は-lpynq_api
と書かれていますが,正しくは-lpynq
です.
PSから送信
PSからPLへはDMAを使ってデータを送ります.ちなみにDMAはその逆,PLからPSへの送信もできますが今回は使いません.
DMAでは一度に送れるデータ量に制限があります.バースト転送することでたくさんのデータを送ります.
ここでは一度に64ビットの情報を送ります.
また,一画素あたり4ビットRGBで表現することにしたので,一画像あたり64(マトリクスLEDの行数)x64(マトリクスLEDの列数)x4(一画素のビット数)x3(RGB)=49152bit=6144Byteあります.一色当たり2048Byteです.
バースト長は256になります.
#include <iostream>
#include <opencv2/opencv.hpp>
#define SUCCESS (0)
#define IMG_READ_FAIL (-1)
#define IMG_SIZE (64)
#define BURST_SIZE (256)
// 一枚の画像情報をPSからPLへ送信する関数
int send_img_data(
PYNQ_AXI_DMA &dma,
PYNQ_SHARED_MEMORY &memory,
unsigned long long *data,
const char *img_path
) {
// 画像の読み込み
cv::Mat img;
try {
img = cv::imread(img_path, -1);
if (img.empty()) throw IMG_READ_FAIL;
}
catch (int e) {
return e;
}
// 画像データをバッファに詰めて送信する
cv::Vec3b pixel;
unsigned char place = 0;
int shift_num = 0;
// RGBの三回ループ
for (int c = 0; c < 3; c++) {
// 初期化(たぶんなくてもいい)
for (int i = 0; i < BURST_SIZE; i++) {
data[i] = 0L;
}
// 二重ループで一画素ずつループ
for (int i = 0; i < IMG_SIZE; i++) {
for (int j = 0; j < IMG_SIZE; j++) {
pixel = img.at<cv::Vec3b>(i, j);
// 一画素分の情報8bitの上位4bitを取得
data[place] |= (((unsigned long long)(pixel[c]) >> 4)) << shift_num;
shift_num += 4;
// 64ビット埋まったら次の要素へ
if (shift_num == 64) {
place++;
shift_num = 0;
}
}
}
// DMAで送信
PYNQ_writeDMA(&dma, &memory, 0, sizeof(long long) * BURST_SIZE);
PYNQ_waitForDMAComplete(&dma, AXI_DMA_WRITE);
}
return SUCCESS;
}
メイン関数はこちら.
ちなみにDMAのアドレスはブロックデザインの下記画像から調べられます.
#include <iostream>
#include <string>
#include <sstream>
#include <chrono>
#include <thread>
#include <opencv2/opencv.hpp>
extern "C" {
#include <pynq_api.h>
}
#include "send_img_data.h"
using namespace std::chrono;
int main(void) {
std::cout << "start" << std::endl;
char bit_path[] = "./bit/ps2pl2hub_nc.bit";
const int ADDR = 0x40400000;
int rtn;
// ビットファイルの準備
PYNQ_loadBitstream(bit_path);
// DMAの準備
PYNQ_SHARED_MEMORY memory;
PYNQ_allocatedSharedMemory(&memory, sizeof(long long) * BURST_SIZE, 1);
PYNQ_AXI_DMA dma;
PYNQ_openDMA(&dma, ADDR);
unsigned long long *data = (unsigned long long *)memory.pointer;
// 4回ループ(GIF画像の繰り返し数)
for (int i = 0; i < 4; i++) {
// 34回ループ(GIF画像の画像数)
for (int j = 0; j < 34; j++) {
// 開始時刻取得
auto start = system_clock::now();
// ファイル名作成
std::stringstream ss;
ss << std::setw(4) << std::setfill('0') << j;
std::string file_name = "./img_kana/" + ss.str() + ".png";
// 画像データを送る
rtn = send_img_data(
dma, memory, data,
file_name.c_str()
);
if (rtn != SUCCESS) {
std::cout << i << " " << j << " error" << std::endl;
break;
}
// 70ms経つまで待つ
while (
duration_cast<milliseconds>(system_clock::now() - start).count() % 1000 < 70
);
}
}
// 真っ黒画像を送る
rtn = send_img_data(
dma, memory, data,
"./img/black.png"
);
if (rtn != SUCCESS) {
std::cout << "black error" << std::endl;
}
else {
std::cout << "black send" << std::endl;
}
// DMAを閉じる
PYNQ_closeDMA(&dma);
PYNQ_freeSharedMemory(&memory);
std::cout << "end" << std::endl;
return 0;
}
ちなみにコンパイルコマンドはこんな感じです.OpenCVとpynq_apiを使うためにかなり長くなっています.
g++ -o kana kana.cpp -I /usr/local/include/opencv4 -l opencv_core -l opencv_objdetect -l opencv_highgui -l opencv_imgproc -l opencv_videoio -l opencv_imgcodecs -lpynq -lcma -lpthread
PLで受信
ここからはHDLになります.
その前に,全体像を張ります.
axi_dmaの設定
右クリック-> Add IP
からAXI Direct Memory Access
をブロックデザインに追加します.
追加直後は以下のような見た目です.
設定画面を開いて,以下のように設定します.
ここで,それぞれの言葉の意味は以下のような感じです.
- Enable Scatter Gather Engine: DMAの複雑な機能が使えるらしいです.よく知りません.「scatter gather」でググると情報が出てきます.
- Enabe Micro DMA: これをオンにすると下のAllow Unallgned Transfersにチェックを入れられなくなります.
- Width of Buffer length Register: バッファの位置を指定するレジスタの幅.バッファが32bitのとき,このレジスタは5bit以上必要.
- Address Width: 送受信するデータのアドレス幅っぽい.
- Enable Read Channel: そのまま,受信チャンネルを有効化するかどうか.ここでいう「受信」はPL目線なので,PS->PL通信を有効にするか.
- Memory Map Data Width: このIPがPSと通信するときのデータ幅.
- Stream Data Width: このIPがPLと通信するときのデータ幅.
- Max Burst Size: そのまま.最大バースト長.
- Enable Write Channel: そのまま.PL->PS通信を有効にするか.
あとはZYNQ
をダブルクリックして設定画面を開いて以下の設定(HP Slave AXI Interface
のところ)をしてAuto ~~
をやれば配線できます.
受信部はaxi_dma_0
で,受け取った画像をimg_memory_wrapper_0
で保存しています.その中身はこちら(Qiitaではコードの色が変わらなかったので拡張子を「.v」としていますが,実際は「.sv」です).
module img_memory_0(
input clk,
input resetn,
input [3:0] btn, // デバッグ用なので関係ない
input [1:0] sw, // デバッグ用なので関係ない
output [7:0] ar, // デバッグ用なので関係ない
// s axis
input [63:0] tdata,
input tlast,
input tvalid,
output reg tready,
input [4:0] row, // 表示する行
output [64 * 4 - 1:0] red_f, // 赤の上側
output [64 * 4 - 1:0] red_s, // 赤の下側
output [64 * 4 - 1:0] green_f,
output [64 * 4 - 1:0] green_s,
output [64 * 4 - 1:0] blue_f,
output [64 * 4 - 1:0] blue_s
);
// 受信状態
typedef enum logic [2:0] {
IDLE,
BLUE,
GREEN,
RED
} state_t;
reg [2:0][255:0][63:0] full_data; // 画像データ(RGB,バースト長,送られてくる1データの三次元配列)
reg [7:0] itr = 8'hff;
state_t state = IDLE;
always @(posedge clk) begin
// 初期化(リセット状態)
if (!resetn) begin
tready <= 1'b0;
for (logic [1:0] i = 0; i < 3; i++) begin
for (logic [8:0] j = 0; j < 256; j++) begin
full_data[i][j] <= 64'b0;
end
end
itr <= 8'hff;
state <= IDLE;
// 通常動作
end else begin
case (state)
// 待機(データが送信されてくるのを待つ)
IDLE: begin
tready <= 1'b0;
if (tvalid) begin
state <= BLUE;
itr <= 8'hff;
end
end
// 青色を取得
BLUE: begin
if (tvalid) begin
tready <= 1'b1;
full_data[0][itr] <= tdata;
if (tlast) begin
state <= GREEN;
itr <= 8'hff;
end else begin
itr <= itr + 8'b1;
end
end else begin
tready <= 1'b0;
end
end
// 緑色を取得
GREEN: begin
if (tvalid) begin
tready <= 1'b1;
full_data[1][itr] <= tdata;
if (tlast) begin
state <= RED;
itr <= 8'hff;
end else begin
itr <= itr + 8'b1;
end
end else begin
tready <= 1'b0;
end
end
// 赤色を取得
RED: begin
if (tvalid) begin
tready <= 1'b1;
full_data[2][itr] <= tdata;
if (tlast) begin
state <= IDLE;
itr <= 8'hff;
end else begin
itr <= itr + 8'b1;
end
end else begin
tready <= 1'b0;
end
end
endcase
end
end
// 指定された行のデータを出力(4要素で一行分のデータになる)
assign blue_f = {full_data[0][{1'b0, row, 2'b11}], full_data[0][{1'b0, row, 2'b10}], full_data[0][{1'b0, row, 2'b01}], full_data[0][{1'b0, row, 2'b00}]};
assign blue_s = {full_data[0][{1'b1, row, 2'b11}], full_data[0][{1'b1, row, 2'b10}], full_data[0][{1'b1, row, 2'b01}], full_data[0][{1'b1, row, 2'b00}]};
assign green_f = {full_data[1][{1'b0, row, 2'b11}], full_data[1][{1'b0, row, 2'b10}], full_data[1][{1'b0, row, 2'b01}], full_data[1][{1'b0, row, 2'b00}]};
assign green_s = {full_data[1][{1'b1, row, 2'b11}], full_data[1][{1'b1, row, 2'b10}], full_data[1][{1'b1, row, 2'b01}], full_data[1][{1'b1, row, 2'b00}]};
assign red_f = {full_data[2][{1'b0, row, 2'b11}], full_data[2][{1'b0, row, 2'b10}], full_data[2][{1'b0, row, 2'b01}], full_data[2][{1'b0, row, 2'b00}]};
assign red_s = {full_data[2][{1'b1, row, 2'b11}], full_data[2][{1'b1, row, 2'b10}], full_data[2][{1'b1, row, 2'b01}], full_data[2][{1'b1, row, 2'b00}]};
// デバッグ用なので関係ない
assign ar = sw != 2'b11 ? full_data[sw][btn][7:0] : 8'b0;
endmodule
画像データをマトリクスLEDへ送ろう
これまでのところで,PYNQ-Z2のSDカードに保存されている画像データをPL上のレジスタに展開できたので,今度はそのデータをマトリクスLEDに送信します.保存されている画像データを絶えず送信し続けます.
例によって,実際の拡張子は「.sv」です.
module hub_driver(
input clk,
input resetn,
// 一行分のデータ(上側)
input [64 * 4 - 1:0] blue_in_f,
input [64 * 4 - 1:0] green_in_f,
input [64 * 4 - 1:0] red_in_f,
// 一行分のデータ(下側)
input [64 * 4 - 1:0] blue_in_s,
input [64 * 4 - 1:0] green_in_s,
input [64 * 4 - 1:0] red_in_s,
// hub75eへ送信するデータ
output reg [4:0] row, // hub75eへ送るとともに,img_memoryモジュールに行を指定している
output reg [1:0] red,
output reg [1:0] green,
output reg [1:0] blue,
output reg lcl, // led clk
output reg lat,
output reg oe
);
typedef enum logic [1:0] {
IDLE,
SEND_ROW,
LAT_HIGH,
LAT_LOW
} state_t;
// hub75eへ送信するデータのクロック周波数
localparam integer FREQ = 8_000_000 /* Hz */;
localparam integer HALF_CYCLE = 100_000_000 / FREQ / 2;
state_t state = IDLE;
reg [31:0] cnt = 32'b1;
reg carry_flag = 1'b0;
reg [5:0] col = 6'b1; // 列
reg [3:0] step = 4'b0; // 明るさを指定するための変数
// それぞれ一行分のデータ
reg [64 * 4 - 1:0] red_color_f = 256'b0;
reg [64 * 4 - 1:0] blue_color_f = 256'b0;
reg [64 * 4 - 1:0] green_color_f = 256'b0;
reg [64 * 4 - 1:0] red_color_s = 256'b0;
reg [64 * 4 - 1:0] blue_color_s = 256'b0;
reg [64 * 4 - 1:0] green_color_s = 256'b0;
always_ff @(posedge clk) begin
if (!resetn) begin
row <= 5'd0;
red <= 2'b0;
green <= 2'b0;
blue <= 2'b0;
lcl <= 1'b0;
lat <= 1'b1;
oe <= 1'b1;
state <= IDLE;
cnt <= 32'b1;
carry_flag <= 1'b0;
col <= 6'b1;
step <= 4'b0;
red_color_f <= 256'b0;
blue_color_f <= 256'b0;
green_color_f <= 256'b0;
red_color_s <= 256'b0;
blue_color_s <= 256'b0;
green_color_s <= 256'b0;
end else begin
case (state)
// 初期化
IDLE: begin
lat <= 1'b0;
oe <= 1'b0;
state <= SEND_ROW;
step <= step + 4'b1;
frame_cnt <= 32'b1;
red_color_f <= red_in_f;
green_color_f <= green_in_f;
blue_color_f <= blue_in_f;
red_color_s <= red_in_s;
green_color_s <= green_in_s;
blue_color_s <= blue_in_s;
end
SEND_ROW: begin
if (cnt == HALF_CYCLE) begin
lcl <= !lcl;
cnt <= 32'b1;
// 立下りのタイミングで送信データを更新する
if (lcl) begin
// 4bitのデータがstepを超えているかどうかで出力をhighにするかどうか決める.
// stepはインクリメントされていくので,明るい画素ほど点灯される時間が長い
red <= {red_color_s[3:0] > step, red_color_f[3:0] > step};
green <= {green_color_s[3:0] > step, green_color_f[3:0] > step};
blue <= {blue_color_s[3:0] > step, blue_color_f[3:0] > step};
// 次の画素データを最下位ビットに移動させる
red_color_f <= {4'b0, red_color_f[64 * 4 - 1:4]};
green_color_f <= {4'b0, green_color_f[64 * 4 - 1:4]};
blue_color_f <= {4'b0, blue_color_f[64 * 4 - 1:4]};
red_color_s <= {4'b0, red_color_s[64 * 4 - 1:4]};
green_color_s <= {4'b0, green_color_s[64 * 4 - 1:4]};
blue_color_s <= {4'b0, blue_color_s[64 * 4 - 1:4]};
{carry_flag, col} <= col + 7'b1;
// 64列送り終えたら状態更新
if (carry_flag) begin
state <= LAT_HIGH;
lat <= 1'b1;
oe <= 1'b1;
end
end
end else begin
cnt <= cnt + 32'b1;
end
end
// 以下,点灯処理と次の行の表示準備
LAT_HIGH: begin
if (cnt == HALF_CYCLE) begin
cnt <= 32'b1;
lat <= 1'b0;
oe <= 1'b0;
state <= LAT_LOW;
red_color_f <= red_in_f;
green_color_f <= green_in_f;
blue_color_f <= blue_in_f;
red_color_s <= red_in_s;
green_color_s <= green_in_s;
blue_color_s <= blue_in_s;
end else begin
cnt <= cnt + 32'b1;
end
end
LAT_LOW: begin
if (cnt == HALF_CYCLE) begin
cnt <= 32'b1;
row <= row + 5'b1;
// 画像一枚分を送り終えたら,stepを増やしもう一度送信
if (row == 5'd31) begin
state <= IDLE;
end else begin
state <= SEND_ROW;
end
end else begin
cnt <= cnt + 32'b1;
end
end
endcase
end
end
endmodule
その他
System Verilogはブロックデザインに追加できないので,Verilog HDLでラッパーを作っています.
module img_memory_wrapper(
input clk,
input resetn,
input [3:0] btn, // デバッグ用なので関係ない
input [1:0] sw, // デバッグ用なので関係ない
output [7:0] ar, // デバッグ用なので関係ない
// s axis
input [63:0] tdata,
input tlast,
input tvalid,
output tready,
input [4:0] row,
output [64 * 4 - 1:0] red_f,
output [64 * 4 - 1:0] red_s,
output [64 * 4 - 1:0] green_f,
output [64 * 4 - 1:0] green_s,
output [64 * 4 - 1:0] blue_f,
output [64 * 4 - 1:0] blue_s
);
img_memory_0 img_memory (
.clk(clk), .resetn(resetn),
.btn(btn), .sw(sw), .ar(ar),
.tdata(tdata), .tlast(tlast), .tvalid(tvalid), .tready(tready),
.row(row + 2), // 次の行のデータが欲しいので,ここで調整
.red_f(red_f), .red_s(red_s),
.green_f(green_f), .green_s(green_s),
.blue_f(blue_f), .blue_s(blue_s)
);
endmodule
module hub_driver_wrapper(
input clk,
input resetn,
input [64 * 4 - 1:0] red_in_f,
input [64 * 4 - 1:0] red_in_s,
input [64 * 4 - 1:0] green_in_f,
input [64 * 4 - 1:0] green_in_s,
input [64 * 4 - 1:0] blue_in_f,
input [64 * 4 - 1:0] blue_in_s,
output [4:0] row,
output [1:0] red,
output [1:0] green,
output [1:0] blue,
output lcl, // led clk
output lat,
output oe,
output gnd
);
assign gnd = 1'b0;
hub_driver hub_driver_0 (
.clk(clk), .resetn(resetn),
.blue_in_f(blue_in_f), .green_in_f(green_in_f), .red_in_f(red_in_f),
.blue_in_s(blue_in_s), .green_in_s(green_in_s), .red_in_s(red_in_s),
.row(row),
.red(red), .green(green), .blue(blue),
.lcl(lcl), .lat(lat), .oe(oe)
);
endmodule
あとXDC.
##PmodA
set_property -dict { PACKAGE_PIN Y18 IOSTANDARD LVCMOS33 } [get_ports { red[0] }]; #IO_L17P_T2_34 Sch=ja_p[1]
set_property -dict { PACKAGE_PIN Y19 IOSTANDARD LVCMOS33 } [get_ports { blue[0] }]; #IO_L17N_T2_34 Sch=ja_n[1]
set_property -dict { PACKAGE_PIN Y16 IOSTANDARD LVCMOS33 } [get_ports { red[1] }]; #IO_L7P_T1_34 Sch=ja_p[2]
set_property -dict { PACKAGE_PIN Y17 IOSTANDARD LVCMOS33 } [get_ports { blue[1] }]; #IO_L7N_T1_34 Sch=ja_n[2]
set_property -dict { PACKAGE_PIN U18 IOSTANDARD LVCMOS33 } [get_ports { green[0] }]; #IO_L12P_T1_MRCC_34 Sch=ja_p[3]
set_property -dict { PACKAGE_PIN U19 IOSTANDARD LVCMOS33 } [get_ports { gnd }]; #IO_L12N_T1_MRCC_34 Sch=ja_n[3]
set_property -dict { PACKAGE_PIN W18 IOSTANDARD LVCMOS33 } [get_ports { green[1] }]; #IO_L22P_T3_34 Sch=ja_p[4]
set_property -dict { PACKAGE_PIN W19 IOSTANDARD LVCMOS33 } [get_ports { row[4] }]; #IO_L22N_T3_34 Sch=ja_n[4]
##PmodB
set_property -dict { PACKAGE_PIN W14 IOSTANDARD LVCMOS33 } [get_ports { row[0] }]; #IO_L8P_T1_34 Sch=jb_p[1]
set_property -dict { PACKAGE_PIN Y14 IOSTANDARD LVCMOS33 } [get_ports { row[2] }]; #IO_L8N_T1_34 Sch=jb_n[1]
set_property -dict { PACKAGE_PIN T11 IOSTANDARD LVCMOS33 } [get_ports { lcl }]; #IO_L1P_T0_34 Sch=jb_p[2]
set_property -dict { PACKAGE_PIN T10 IOSTANDARD LVCMOS33 } [get_ports { oe }]; #IO_L1N_T0_34 Sch=jb_n[2]
set_property -dict { PACKAGE_PIN V16 IOSTANDARD LVCMOS33 } [get_ports { row[1] }]; #IO_L18P_T2_34 Sch=jb_p[3]
set_property -dict { PACKAGE_PIN W16 IOSTANDARD LVCMOS33 } [get_ports { row[3] }]; #IO_L18N_T2_34 Sch=jb_n[3]
set_property -dict { PACKAGE_PIN V12 IOSTANDARD LVCMOS33 } [get_ports { lat }]; #IO_L4P_T0_34 Sch=jb_p[4]
set_property -dict { PACKAGE_PIN W13 IOSTANDARD LVCMOS33 } [get_ports { gnd }]; #IO_L4N_T0_34 Sch=jb_n[4]
完成品
実はランダムでPS->PLのデータ送受信に失敗するのでたまに表示が乱れます.
おそらく回路のどこかに非効率な部分があると思うのですが,心折れたのでこの辺で完成としました.