【Raspberry Pi Pico】microSDに保存した画像をDMA転送でLCDに表示する

raspberry pi pico
oplus_32

パーツ

  • ラズパイピコ
  • microSDカードリーダーモジュール
  • LCDモジュール(Pico-LCD 1.14)

SDK・ライブラリ・ドライバ

ファイル構成

pimoroniいっぱい入ってるからどんどん間引いて最小構成にした。

↓コピー元とかも書いた

プロジェクトフォルダ
├ common(pimoroni-pico/commonフォルダ)
├ drivers
│└ st7789(pimoroni/drivers/st7789)
├ libraries
│├ bitmap_fonts(pimoroni-pico/libraries/bitmap_fonts)
│├ hershey_fonts(pimoroni-pico/libraries/hershey_fonts)
│├ no-OS-FatFS-SD-SPI-RPi-Pico(carlk3/no-OS-FatFS-SD-SPI-RPi-Pico)
│├ pico_display(pimoroni-pico/libraries/pico_display)
│└ pico_graphics(pimoroni-pico/libraries/pico_graphics)
├ pico-sdk
CMakeLists.txt
main.cpp
main.hpp

ソース

画像ファイル(240*135且つRGB565のbin形式)が「all_frames_240.bin」というファイル名でmicroSDに保存してある前提でのソース

CMakeLists.txt

cmake_minimum_required(VERSION 3.13)

# pico-sdkのパスを変数に格納
set(PICO_SDK_PATH "${CMAKE_CURRENT_LIST_DIR}/pico-sdk")

# PICO SDKの初期化に必要なスクリプトを読み込む
# 環境変数ではなく、上で定義した変数を使って include する
include(${PICO_SDK_PATH}/pico_sdk_init.cmake)

# プロジェクトの名前と使用言語を指定
# cppを使う場合はCXXを指定する
project(atx_c_cpp C CXX ASM)
set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED ON)

# Pico SDKの初期化して使えるようにする
pico_sdk_init()

# 実行ファイルとしてビルドするソースファイルを指定
add_executable(${PROJECT_NAME}
    main.cpp
)

# USBシリアル通信を有効にする
# 1は有効、0は無効
pico_enable_stdio_usb(${PROJECT_NAME} 1)
pico_enable_stdio_uart(${PROJECT_NAME} 0)

add_subdirectory(common)
add_subdirectory(drivers/st7789)
add_subdirectory(libraries/bitmap_fonts)
add_subdirectory(libraries/pico_graphics)
add_subdirectory(libraries/pico_display)
add_subdirectory(libraries/no-OS-FatFS-SD-SPI-RPi-Pico/FatFs_SPI)

# 必要なライブラリを指定
target_link_libraries(${PROJECT_NAME} PUBLIC
    pico_stdlib      # 標準ライブラリ(stdio、sleepなど)
    FatFs_SPI     # SPI制御用ハードウェアライブラリ
    hardware_spi
    hardware_pwm
    hardware_dma
    pico_display
    pico_graphics
    pimoroni_bus
    bitmap_fonts
    st7789
)

# ビルド成果物の出力先を指定
pico_add_extra_outputs(${PROJECT_NAME})

main.hpp

#include <stdio.h>
#include <string.h>
#include "pico/stdlib.h"
#include "hardware/dma.h"
#include "hardware/spi.h"
#include "pico/stdlib.h"
#include "pico/binary_info.h"
#include "sd_card.h"
#include "ff.h"
#include "pico_display.hpp"
#include "st7789.hpp"
#include "pico_graphics.hpp"

using namespace pimoroni;

#define BUFFER_SIZE (PicoDisplay::WIDTH * PicoDisplay::HEIGHT * 2) // RGB565 = 2 bytes per pixel
uint8_t image_buffer[BUFFER_SIZE];

#define LCD_SPI spi1

void write_to_lcd(uint8_t *data, size_t len);
void write_command(uint8_t cmd);
void write_data(uint8_t *data, size_t len);
void set_window(uint16_t x0, uint16_t y0, uint16_t x1, uint16_t y1);
void lcd_init();
void setup_dma();

main.cpp

#include "main.hpp"

int dma_ch;
dma_channel_config config;

int main() {
    // シリアルポートの初期化
    stdio_init_all();

    // SPIの初期化
    spi_init(LCD_SPI, 40000000); // SPIのクロックを40MHzに設定
    // DMASPIを有効化
    spi_set_format(LCD_SPI, 8, SPI_CPOL_0, SPI_CPHA_0, SPI_MSB_FIRST);

    // // SPIのピン設定
    gpio_init(SPI_DEFAULT_DC);
    gpio_set_dir(SPI_DEFAULT_DC, GPIO_OUT);
    gpio_put(SPI_DEFAULT_DC, 1); // 初期状態:データモード
    // チップセレクトピンをSPI機能に設定
    gpio_init(SPI_BG_FRONT_CS);
    gpio_set_dir(SPI_BG_FRONT_CS, GPIO_OUT);
    gpio_put(SPI_BG_FRONT_CS, 1);

    gpio_set_function(SPI_DEFAULT_SCK, GPIO_FUNC_SPI); // クロックピンをSPI機能に設定
    gpio_set_function(SPI_DEFAULT_MOSI, GPIO_FUNC_SPI); // MOSIピンをSPI機能に設定
    // バックライトピン
    gpio_init(SPI_BG_FRONT_PWM);
    gpio_set_dir(SPI_BG_FRONT_PWM, GPIO_OUT);
    gpio_put(SPI_BG_FRONT_PWM, 1); // バックライトON
    // リセットピンを初期化
    gpio_init(SPI_BG_FRONT_RESET);

    sleep_ms(5000); // シリアルポートの安定化のために5秒待機
    
    FRESULT fr;
    FATFS fs;
    FIL fil;
    int ret;
    uint8_t buf[64800];
    char filename[] = "all_frames_240.bin";

    // SDカードの初期化
    if (!sd_init_driver()) {
        printf("SDカードの初期化に失敗しました。\r\n");
        while (true);
    }

    // ドライブのマウント
    fr = f_mount(&fs, "0:", 1);
    if (fr != FR_OK) {
        printf("ドライブのマウントに失敗しました。エラーコード: %d\r\n", fr);
        while (true);
    }

    // ファイルを読み取り専用で開く
    fr = f_open(&fil, filename, FA_READ);
    if (fr != FR_OK) {
        printf("ファイルのオープンに失敗しました。エラーコード: %d\r\n", fr);
        while (true);
    }

    printf("LCDの初期化を開始します\r\n");
    lcd_init();

    // ファイルからデータを読み取る
    printf("データを読み取ります '%s':\r\n", filename);

    //DMA準備
    setup_dma();
    printf("LCDに画像を描画します\r\n");
    UINT bytesRead;

    fr = f_read(&fil, buf, sizeof(buf), &bytesRead);
    if (fr != FR_OK) {
            printf("ファイルの読み取りに失敗しました。エラーコード: %d\r\n", fr);
            f_close(&fil);
            while (true);
        }
    write_to_lcd((uint8_t *)buf, bytesRead);

    dma_channel_unclaim(dma_ch); // DMAチャンネルを解放

    write_to_lcd((uint8_t *)buf, bytesRead); // 読み取ったデータをLCDに書き込む

    // ファイルを閉じる
    fr = f_close(&fil);
    if (fr != FR_OK) {
        printf("ファイルのクローズに失敗しました。エラーコード: %d\r\n", fr);
        while (true);
    }

    // ドライブのアンマウント
    f_unmount("0:");

    printf("SDカードのテストが完了しました。\r\n");
    while (true) {
        // 無限ループでプログラムを終了しないようにする
        tight_loop_contents();
    }
}

// LCDにコマンドを送信する関数
void write_command(uint8_t cmd) {
    gpio_put(SPI_DEFAULT_DC, 0);
    gpio_put(SPI_BG_FRONT_CS, 0);
    spi_write_blocking(LCD_SPI, &cmd, 1);
    gpio_put(SPI_BG_FRONT_CS, 1);
}

// LCDにデータを送信する関数
void write_data(uint8_t *data, size_t len) {
    gpio_put(SPI_DEFAULT_DC, 1);       // データモード
    gpio_put(SPI_BG_FRONT_CS, 0);       // 通信開始
    spi_write_blocking(LCD_SPI, data, len);
    gpio_put(SPI_BG_FRONT_CS, 1);       // 通信終了
}

// 描画する表域を設定する関数
void set_window(uint16_t x0, uint16_t y0, uint16_t x1, uint16_t y1) {
    uint8_t cmd = 0x2A;
    write_command(cmd);
    uint8_t data[] = { x0 >> 8, x0 & 0xFF, x1 >> 8, x1 & 0xFF };
    write_data(data, 4);

    cmd = 0x2B;
    write_command(cmd);
    uint8_t data2[] = { y0 >> 8, y0 & 0xFF, y1 >> 8, y1 & 0xFF };
    write_data(data2, 4);

    printf("LCD初期化完了");
}

void setup_dma() {
    // DMAチャンネルを取得
    dma_ch = dma_claim_unused_channel(true);
    // DMAチャンネルの設定
    config = dma_channel_get_default_config(dma_ch);
    channel_config_set_transfer_data_size(&config, DMA_SIZE_8); // データサイズを8ビットに設定
    channel_config_set_read_increment(&config, true);  // 読み取りインクリメントを有効にする
    channel_config_set_write_increment(&config, false);  // 書き込みインクリメントを無効にする
    channel_config_set_dreq(&config, spi_get_dreq(LCD_SPI, true));  // DREQを設定
}

void write_to_lcd(uint8_t *data, size_t len) {        
    // 描画開始コマンドを送る
    write_command(0x2C);  // LCDにデータ書き込みの開始を指示

    gpio_put(SPI_DEFAULT_DC, 1); // データモードに設定
    gpio_put(SPI_BG_FRONT_CS, 0); // チップセレクトをアサート

    dma_channel_configure(
        dma_ch,                     // DMAチャンネル
        &config,                    // DMA設定
        &spi_get_hw(LCD_SPI)->dr,   // SPI送信レジスタ
        data,                       // 転送元データ
        len,                        // 転送するデータの長さ
        true                        // DMA転送を開始する
    );

    dma_channel_wait_for_finish_blocking(dma_ch); // DMA転送が完了するまで待機
    while (spi_is_busy(LCD_SPI)) {
        tight_loop_contents(); // SPIがビジー状態でないことを確認
    }
    gpio_put(SPI_BG_FRONT_CS, 1); // チップセレクトをディアサート
}

void lcd_init() {
    // LCDのリセット
    gpio_init(SPI_BG_FRONT_RESET);
    gpio_set_dir(SPI_BG_FRONT_RESET, GPIO_OUT);
    gpio_put(SPI_BG_FRONT_RESET, 0);
    sleep_ms(50);
    gpio_put(SPI_BG_FRONT_RESET, 1);
    sleep_ms(50);

    sleep_ms(100);
    write_command(0x01); // リセット
    sleep_ms(150);

    write_command(0x11); // Sleep Out(低電力モードからの起動)
    sleep_ms(500);

    write_command(0x3A); // Interface Pixel Format(データのカラービットの長さ)
    uint8_t data = 0x55; //16ビットカラーを使用するため、0x55を設定
    write_data(&data, 1);

    // 表示方向制御
    write_command(0x36);
    uint8_t madctl = 0x60; // 方向に応じて変更
    write_data(&madctl, 1);

    write_command(0x21); // Inversion ON(明るさ調整)
    write_command(0x29); // Display ON
    sleep_ms(100);

    spi_get_hw(LCD_SPI)->dmacr = SPI_SSPDMACR_TXDMAE_BITS; // DMAを有効化

    // なぜか描画位置がずれるのでオフセットを設定
    const int X_OFFSET = 40;
    const int Y_OFFSET = 53;
    set_window(X_OFFSET, Y_OFFSET, PicoDisplay::WIDTH - 1 + X_OFFSET, PicoDisplay::HEIGHT - 1 + Y_OFFSET); // 描画領域を設定
}

正直あんまよく理解してないけど奇跡的に動いてる。

結果

おわりに

多分CMakeの設定次第でエラーが出たりすると思うけど、
そこさえ是正できれば動くコードではあるよ

タイトルとURLをコピーしました