Rust+RaspberryPi Pico+e-Paperを動かす

開発
この記事は約14分で読めます。

消しても表示されるe-Paper

e-Paper

以前液晶やらOLEDを使って画面描画をしてきましたが、Aliexpressで良さげなePaper(eインク)のHAT?を見つけたので買ってみました。

そもそもe-Paperとはなんぞやと思う方も多いかと思いますので簡単に説明。

e-Paper(電子ペーパー, eインク)はスーパーの値札やサイゼのスマホ用QR表示やKindleなどで使われている表示デバイスです。

描画の時間がかかったり、高速に更新ができない欠点はありますが、表示には電力がいらないので電源を落としても表示できる強みがあります。

基本的に白黒ですが白黒赤とか白黒黄色など多色も増えてきましたね。

電子ペーパー(EPD / eペーパー)とは - IT用語辞典 e-Words
電子ペーパー(electronic paper)とは、電気的な原理を用いる表示装置のうち、紙に似た特性を持つものです。「EPD」「eペーパー」とも呼ばれます。本記事では、電子ペーパーについて、意味・定義を初心者の方にも分かりやすく解説します...

買ったもの

Waveshare e-Paper e-inkディスプレイモジュール,podo,296*128,zwart wit,分割,2.9in - AliExpress 7
Smarter Shopping, Better Living! Aliexpress.com

よくフレキケーブルが出ていたり、RaspberryPiシリーズに付けるHATがありますが、今回はRaspberryPiPicoに対応したHATタイプです。

一応コネクタも出ているので通常のRaspberryPi等も繋げられますが、Picoシリーズであれば直接繋げられます。

クリックして2.9inch-e-paper-v2-specification.pdfにアクセス

ドキュメントや画像があるように基盤はV2です。

あまり商品ページを見ずに買ったので買ってから気づきましたが、右上にSPIのモードを切り替えができたりします。

ソースコード

#![no_std]
#![no_main]

use defmt::*;
use defmt_rtt as _;
// use panic_probe as _;
use rp2040_hal::{self as hal, timer::Alarm};
use panic_halt as _;

use hal::{pac, Clock,timer::{Alarm0}};

use embedded_graphics::{
    mono_font::{MonoTextStyle, ascii::FONT_6X10, iso_8859_9::FONT_10X20},
    prelude::*,
    text::{Baseline, Text},
    primitives::{Triangle, PrimitiveStyle, Rectangle, PrimitiveStyleBuilder, Circle},
};
use embedded_hal_bus::spi::ExclusiveDevice;
use epd_waveshare::{epd2in9_v2::*, prelude::*};
use fugit::{RateExtU32, ExtU32};

use core::cell::RefCell;
use core::sync::atomic::{AtomicBool, Ordering};
use cortex_m::asm::wfi;
use cortex_m::interrupt::Mutex;
use crate::pac::interrupt;
use cortex_m_rt::entry;
use panic_halt as _;

use core::fmt::Write;
use heapless::String;

static ALARM0: Mutex<RefCell<option>> = Mutex::new(RefCell::new(None));
static TIMER: Mutex<RefCell<option>> = Mutex::new(RefCell::new(None));
static MINUTE_ELAPSED: AtomicBool = AtomicBool::new(false);


#[link_section = ".boot2"]
#[used]
pub static BOOT2: [u8; 256] = rp2040_boot2::BOOT_LOADER_GENERIC_03H;

const XTAL_FREQ_HZ: u32 = 12_000_000u32;

fn draw_base(display: &mut Display2in9) {
    info!("Drawing rectangle...");
    // 四角形
    let box_style = PrimitiveStyleBuilder::new().stroke_color(Color::Black).stroke_width(2).fill_color(Color::Black).build();
    Rectangle::new(Point::new(20, 40), Size::new(60, 50))
        .into_styled(box_style)
        .draw( display)
        .unwrap();

    info!("Drawing triangle...");
    // トライアングル
    Triangle::new(Point::new(50, 100), Point::new(50, 150), Point::new(100, 100))
        .into_styled(PrimitiveStyle::with_stroke(Color::Black, 2))
        .draw( display)
        .unwrap();

    info!("Drawing circle 1...");
    // 円
    let circle_style = PrimitiveStyleBuilder::new().stroke_color(Color::Black).stroke_width(2).fill_color(Color::Black).build();
    Circle::new(Point::new(50, 200), 50)
        .into_styled(circle_style)
        .draw( display)
        .unwrap();

    info!("Drawing circle 2...");
    Circle::new(Point::new(65, 215), 30)
        .into_styled(PrimitiveStyle::with_stroke(Color::White, 3))
        .draw( display)
        .unwrap();
    
    info!("draw_base completed");
}

fn draw_all(display: &mut Display2in9, count: &mut i32) {
    
    // 画面をクリア(白)
    display.clear(Color::White).unwrap();
    
    draw_base(display);
    
    // 「Hello」テキストを描画(黒)
    let text_style = MonoTextStyle::new(&FONT_6X10, Color::Black);
    let text = draw_text_and_counter("Hello ", count);
    Text::with_baseline(text.as_str(), Point::new(10, 20), text_style, Baseline::Top)
        .draw( display)
        .unwrap();

    // 「World」テキストを描画(白)
    let text_style2 = MonoTextStyle::new(&FONT_10X20, Color::White);
    Text::with_baseline("World", Point::new(20, 40), text_style2, Baseline::Top)
        .draw( display)
        .unwrap();
}

fn draw_text_and_counter(value: &str, counter: &mut i32) -> String<64> {
    // 任意のテキストとカウンターを組み合わせて文字列を作成
    let mut s: String<64> = String::new();

    core::write!(&mut s, "{}", value).ok();
    core::write!(&mut s, "counter: {}", counter).ok();

    s
}

#[entry]
fn main() -> ! {
    info!("Program start!");
    let mut pac = pac::Peripherals::take().unwrap();

    let mut watchdog = hal::Watchdog::new(pac.WATCHDOG);

    let clocks = hal::clocks::init_clocks_and_plls(
        XTAL_FREQ_HZ,
        pac.XOSC,
        pac.CLOCKS,
        pac.PLL_SYS,
        pac.PLL_USB,
        &mut pac.RESETS,
        &mut watchdog,
    )
    .ok()
    .unwrap();

    let mut timer = rp2040_hal::Timer::new(pac.TIMER, &mut pac.RESETS, &clocks);
    
    // アラームを設定(180秒後)
    let mut alarm0 = timer.alarm_0().unwrap();
    alarm0.schedule(180u32.secs()).unwrap();
    alarm0.enable_interrupt();

    // タイマーとアラームをグローバルに保存
    cortex_m::interrupt::free(|cs| {
        TIMER.borrow(cs).replace(Some(timer));
        ALARM0.borrow(cs).replace(Some(alarm0));
    });

    unsafe {
        pac::NVIC::unmask(pac::Interrupt::TIMER_IRQ_0);
    }

    let sio = hal::Sio::new(pac.SIO);
    let pins = hal::gpio::Pins::new(
        pac.IO_BANK0,
        pac.PADS_BANK0,
        sio.gpio_bank0,
        &mut pac.RESETS,
    );

    // ピンアサイン(要求仕様に準拠)
    let sck = pins.gpio10.into_function::();  // CLK -> GP10
    let mosi = pins.gpio11.into_function::(); // DIN -> GP11
    let cs = pins.gpio9.into_push_pull_output();                      // CS  -> GP9
    let dc = pins.gpio8.into_push_pull_output();                      // DC  -> GP8
    let rst = pins.gpio12.into_push_pull_output();                     // RST -> GP12
    let busy_in = pins.gpio13.into_pull_up_input();                    // BUSY-> GP13

    // SPI初期化(GP10/GP11はSPI1用のピンなのでSPI1を使用)
    // MODE_0、16MHz(e-Paperの推奨周波数)
    let spi_bus: hal::Spi<_, _, _, 8> = hal::Spi::new(pac.SPI1, (mosi, sck)).init(
        &mut pac.RESETS,
        clocks.peripheral_clock.freq(),
        16u32.MHz(),
        embedded_hal::spi::MODE_0,
    );
    
    // CSピンを含むSPIデバイスを作成(epd-waveshareがSpiDeviceトレイトを要求)
    // new_no_delayを使用してtimerの借用問題を回避
    let mut spi = ExclusiveDevice::new_no_delay(spi_bus, cs).unwrap();

    // EPD初期化(RST/DC/BUSYの扱いはドライバ内で自動処理)
    let mut epd = cortex_m::interrupt::free(|cs| {
        let mut timer_borrow = TIMER.borrow(cs).borrow_mut();
        let timer_ref = timer_borrow.as_mut().unwrap();
        Epd2in9::new(&mut spi, busy_in, dc, rst, timer_ref, None).unwrap()
    });
    
    // ディスプレイバッファ初期化(296x128)
    let mut display = Display2in9::default();
    
    let mut count = 0;  
    
    loop {
        count += 1;
        info!("Drawing all...");

        // EPDをウェイクアップ(スリープモードから復帰)
        cortex_m::interrupt::free(|cs| {
            let mut timer_borrow = TIMER.borrow(cs).borrow_mut();
            let timer_ref = timer_borrow.as_mut().unwrap();
            let _ = epd.wake_up(&mut spi, timer_ref);
        });

        info!("Drawing base...");
        draw_all(&mut display, &mut count);
        // フレーム更新と表示
        cortex_m::interrupt::free(|cs| {
            let mut timer_borrow = TIMER.borrow(cs).borrow_mut();
            let timer_ref = timer_borrow.as_mut().unwrap();
            epd.update_frame(&mut spi, &display.buffer(), timer_ref).unwrap();
            epd.display_frame(&mut spi, timer_ref).unwrap();
            // スリープモードに入る(省電力)
            epd.sleep(&mut spi, timer_ref).unwrap();
        });
        
        info!("Display updated successfully!");

        info!("Sleeping for 180 seconds...");
        while !MINUTE_ELAPSED.load(Ordering::Acquire) {
            wfi();
        }
        MINUTE_ELAPSED.store(false, Ordering::Release);
        info!("Waking up from sleep...");
    }
}

#[allow(non_snake_case)]
#[cortex_m_rt::interrupt]
fn TIMER_IRQ_0() {
    cortex_m::interrupt::free(|cs| {
        if let Some(alarm0) = ALARM0.borrow(cs).borrow_mut().as_mut() {
            alarm0.clear_interrupt();
            // 180秒後に再スケジュール
            let _ = alarm0.schedule(180u32.secs());
        }
    });
    
    MINUTE_ELAPSED.store(true, Ordering::Release);
}

高速に描画できないので180秒ごとするためタイマーのアラームを利用しています。

draw_allなどはembedded_graphicsで描画しているだけなのでそこまで難しくありません。

no_std環境なので文字列と数字の結合ができないのでdraw_text_and_counterでheaplessを使って結合しています。

今回使用したデバイスは2.9インチの白黒v2なのでepd2in9_v2をインポートしています。違うモデルなら書き換えてください。

ちなみに180秒としているのはドキュメントによると100万回の画面更新を想定しているようで、3分ごとだと100万回で6年弱ぐらい持ちそうです。

いつものGitHub

GitHub - zinntikumugai/raspberrypi-pico-e-paper_waveshare_rs
Contribute to zinntikumugai/raspberrypi-pico-e-paper_waveshare_rs development by creating an account on GitHub.

動画

参考

クリックして2.9inch-e-paper-v2-specification.pdfにアクセス

epd_waveshare - Rust
A simple Driver for the Waveshare E-Ink Displays via SPI

 

 

コメント