はじめに
前回、ディスプレイに表示するとこまで出来ました。
これを使ってピクセルアート的なものができるのではないかと思い、今回やってみようと思いました。
概要
ピクセルアートの色コードを一つ一つ指定するのは現実的ではないのでGIMPで画像を作成してそれをPythonを使って各ピクセルの色コードを抽出してそれをC言語の多次元配列で定義して前回からのプログラムで使えるようにしました。
実際に作成した動画:YouTube
解説動画;YouTube
Github
Pythonプログラム
from PIL import Image
import os
def convert_to_rgb565(image_path):
"""
指定した画像を読み込み、RGB565形式のデータに変換する関数。
RGB565は16bitで色を表す方式で、赤5bit・緑6bit・青5bitを使う。
"""
# 画像を開いてRGBモードに変換
img = Image.open(image_path).convert('RGB')
width, height = img.size
rgb565_data = []
# 画像の全ピクセルを走査
for y in range(height):
for x in range(width):
r, g, b = img.getpixel((x, y))
# RGB565に変換(ビットマスクで切り捨て、シフトで位置調整)
rgb565 = ((r & 0xF8) << 8) | ((g & 0xFC) << 3) | (b >> 3)
rgb565_data.append(rgb565)
return rgb565_data, width, height
def generate_c_array(frames_dir):
"""
指定フォルダ内のPNG画像を読み込み、C言語の配列として出力する文字列を生成する。
各画像は1フレームとして扱い、アニメーションデータにまとめる。
"""
# フォルダ内の.pngファイルを取得(名前順にソート)
frame_files = sorted([f for f in os.listdir(frames_dir) if f.endswith('.png')])
if not frame_files:
return "// No frames found.\n" # 画像がない場合のメッセージ
# 最初の画像からサイズ情報を取得
data0, w, h = convert_to_rgb565(os.path.join(frames_dir, frame_files[0]))
frame_size = w * h # 1フレームあたりのピクセル数
# C言語用配列の先頭部分(ヘッダ)
c_array = '#include <stdint.h>\n\n'
c_array += f'const uint16_t animation_frames[{len(frame_files)}][{frame_size}] = {{\n'
# 各フレームのデータを配列として出力
for i, fname in enumerate(frame_files):
data, _, _ = convert_to_rgb565(os.path.join(frames_dir, fname))
c_array += f' // Frame {i}\n {{\n '
for j, pixel in enumerate(data):
# 16進数(4桁)で出力
c_array += f'0x{pixel:04X}, '
# 幅ごとに改行して見やすくする
if (j + 1) % w == 0:
c_array += '\n '
c_array += '},\n'
c_array += '};\n' # 配列終了
return c_array
# 使い方例
frames_folder = 'frames' # フレーム画像のフォルダ名(例: frames/frame0.png, frame1.png ...)
c_code = generate_c_array(frames_folder)
# 生成したCコードをファイルに保存
with open('animation_data.c', 'w') as f:
f.write(c_code)
このプログラムを使い、作ったアニメーション画像をC言語の多次元配列に変換しました。
使い方はこのプログラムと同じ階層にframesというファイルを作り、アニメーション画像をframesの中に入れてこのプログラムを実行すると同じ階層にanimation_data.cができます。
※ 出来上がったanimation_data.cの中身のincludeするファイルは自分で書きました。
画面描画
出来上がった多次元配列のプログラムをforループで描画していきます。
#define SCALE 16
#define SCREEN_WIDTH 960
#define SCREEN_HEIGHT 960
#define FRAME_WIDTH 64
#define FRAME_HEIGHT 64
#define FRAME_COUNT 10
void draw_frame_scale(int frame_index) {
for (int y = 0; y < FRAME_HEIGHT; y++) {
for (int x = 0; x < FRAME_WIDTH; x++) {
uint16_t color = animation_frames[frame_index][y * FRAME_WIDTH + x];
for (int dy = 0; dy < SCALE; dy++) {
for (int dx = 0; dx < SCALE; dx++) {
// mailbox_fb_set_pixel(color, x * SCALE + dx, y * SCALE + dy);
int sx = x * SCALE + dx;
int sy = y * SCALE + dy;
if (sx < SCREEN_WIDTH && sy < SCREEN_HEIGHT) {
backbuffer[sy * SCREEN_WIDTH + sx] = color;
}
}
}
}
}
}
描画負荷と私の画力の都合により、今回は64×64の画像を採用しました。表示時は1ピクセルを16倍に拡大し、960×960のサイズで描画しています。
この方法でフレームバッファに直接描画して試してみたのですが、上から下に画像が描画されていくさまが見えてしまい、画像がパッと切り替わらないのでバックバッファに一度描画してそれからフレームバッファに転送するなど色々と試行錯誤しましたが上手くいきませんでした。
DMA転送
ChatGptにも相談してみるとDMA転送という技術があると知りました。
DMA転送とはCPUを介さずに、周辺機器とメモリ間でデータを直接転送する技術です。
本当に改善されるのか半信半疑でしたが、とりあえず試してみました。
// DMA制御ブロック構造体
typedef struct {
uint32_t ti; // Transfer Information
uint32_t source_ad; // Source Address
uint32_t dest_ad; // Destination Address
uint32_t txfr_len; // Transfer Length
uint32_t stride; // 2D stride mode
uint32_t nextconbk; // Next Control Block
uint32_t _reserved[2];
} dma_cb_t;
// DMA制御ブロックの実体(32バイトアライン必須)
dma_cb_t dma_cb __attribute__((aligned(32)));
// DMAを使ってバックバッファを画面にコピーする
void dma_copy_framebuffer(void *source, void *dest, uint32_t size_bytes) {
// DMAチャネルを停止&リセット
IO_WRITE(DMA_CS, (1 << 31));
for (volatile int i = 0; i < 100; i++); // 小さな待機
// DMA制御ブロックの設定
dma_cb.ti = (1 << 26) | (1 << 8) | (1 << 4); // NO WIDE BURSTS | DEST_INC | SRC_INC
dma_cb.source_ad = (uint32_t)(uintptr_t)source; // 転送元アドレス
dma_cb.dest_ad = (uint32_t)(uintptr_t)dest; // 転送先アドレス
dma_cb.txfr_len = size_bytes; // 転送サイズ(バイト)
dma_cb.stride = 0; // 1D転送
dma_cb.nextconbk = 0; // チェーンしない
// 制御ブロックのアドレス設定
IO_WRITE(DMA_CONBLK_AD, (uintptr_t)&dma_cb);
// DMA開始(エラー/終了フラグクリア & 有効化 & スタート)
IO_WRITE(DMA_CS, 0x10880001);
}
DMA関連のメモリマップは0x3F007000からになります。
詳しくはgithubのhwio.hをご覧ください。
プログラム解説
-
dma_cb_t 構造体
DMAコントローラに渡す「制御ブロック」。転送元/先アドレス、サイズ、モードなどを設定します。
Raspberry Pi の DMA は 32バイトアラインされた構造体が必要です。 -
DMAリセット
IO_WRITE(DMA_CS, (1 << 31));
DMA チャネルをリセットしてから設定しないと、前回の転送が残っておかしな動作をします。 -
Transfer Information (ti)
これはDMAの動作を設定するビットフィールドになります。LED点灯の時、指定のビットを1にしてONにしたみたいな感じです。
(1 << 26) → NO WIDE BURSTS(幅の広いバースト転送をしない)
(1 << 8) → DEST_INC(転送先アドレスを自動でインクリメント)
(1 << 4) → SRC_INC(転送元アドレスを自動でインクリメント) -
source_ad / dest_ad
バックバッファとフレームバッファの実アドレス(物理アドレス)を設定します。 -
txfr_len
転送する総バイト数。16bit色なら 横幅 * 高さ * 2 バイト。 -
nextconbk
次の制御ブロック(チェーンDMA用)。今回は単発転送なので 0。 -
DMA_CONBLK_AD に書き込み
制御ブロックの物理アドレスを DMA に渡します。 -
DMA開始
0x10880001 はエラー/完了ビットをクリアしつつ、DMAを有効化&転送開始する設定です。
最後に
今回はベアメタルプログラムでピクセルアートアニメーションを作って見ました。
画像がパッと切り替わるまで苦労しましたができたときはすごく達成感がありました。
今後、入力などにも挑戦できたらなと思っています。