chobishiba

アトムニャン開発日誌

PicoRubyのWeb terminalやRuntimeGemsがうまく動くようになったらPicoRubyアトムニャンのレシピにブログに書いて人にすすめて歩こうと思ってるのにちょっとまだかかりそうで、かといって初めての人にいきなりビルドから入ってもらうのなんとなくためらう。しかし公開の圧も感じるのでここに書いてくことにする。普段ESP32でPicoRuby動かしてるひとになら通じるだろう。

用意するもの

インストール

Setting Up a Development Environment の手順通り入れていくが以下の点に注意が必要

コード

何にも作らず標準で入っているSPI使っただけでもメモリキチキチに調整すればまばたき表示くらいはできる。

require "spi"

dc  = GPIO.new(33, GPIO::OUT)
rst = GPIO.new(34, GPIO::OUT)

bl = GPIO.new(16, GPIO::OUT)
bl.write(1)

rst.write(1); sleep_ms 10
rst.write(0); sleep_ms 10
rst.write(1); sleep_ms 120

spi = SPI.new(
  unit: :ESP32_SPI2_HOST,
  frequency: 40_000_000,
  sck_pin: 17,
  copi_pin: 21,
  cipo_pin: -1,
  cs_pin: 15,
  mode: 0
)

dc.write(0); spi.write(0xFE)
dc.write(0); spi.write(0xEF)
dc.write(0); spi.write(0x3A)
dc.write(1); spi.write(0x05)
dc.write(0); spi.write(0x36)
dc.write(1); spi.write(0xC8)
dc.write(0); spi.write(0x11)
sleep_ms 120
dc.write(0); spi.write(0x21)
dc.write(0); spi.write(0x29)

# Fill screen white
dc.write(0); spi.write(0x2A)
dc.write(1); spi.write(0x00, 0x02, 0x00, 0x81)
dc.write(0); spi.write(0x2B)
dc.write(1); spi.write(0x00, 0x01, 0x00, 0x80)
dc.write(0); spi.write(0x2C)
dc.write(1)
buf = "\xFF\xFF" * 128
i = 0
while i < 128
  spi.write(buf)
  i += 1
end

# Mouth
dc.write(0); spi.write(0x2A)
dc.write(1); spi.write(0x00, 0x2E, 0x00, 0x56)
dc.write(0); spi.write(0x2B)
dc.write(1); spi.write(0x00, 0x4C, 0x00, 0x4F)
dc.write(0); spi.write(0x2C)
dc.write(1)
buf = "\x00\x00" * 41
i = 0
while i < 4
  spi.write(buf)
  i += 1
end

# Blink loop
op = 1
c = 0
while true
  col = 0x1D
  while col <= 0x5D
    dc.write(0); spi.write(0x2A)
    dc.write(1); spi.write(0x00, col, 0x00, col + 10)
    dc.write(0); spi.write(0x2B)
    dc.write(1); spi.write(0x00, 0x36, 0x00, 0x40)
    dc.write(0); spi.write(0x2C)
    dc.write(1)
    dy = -5
    while dy <= 5
      dx = -5
      while dx <= 5
        b = 0
        if op == 1
          b = 1 if dx * dx + dy * dy <= 25
        else
          b = 1 if dy == 0
        end
        if b == 1
          spi.write("\x00\x00")
        else
          spi.write("\xFF\xFF")
        end
        dx += 1
      end
      dy += 1
    end
    col += 0x40
  end
  if op == 1
    c += 1
    sleep_ms(5000 + c * 7 % 5 * 1000)
    op = 0
  else
    sleep_ms 150
    op = 1
  end
end

でもそれだと厳しいのでmrbgemを作ったがちょっと微妙なので作り直している。(picoruby-lcdと、スタックチャンにも応用できるようなでかいネームスペースの汎用型のものを考えてみたがやってみてはじめていまいちがわかった)

picoruby-lcd部分(まずはRuby実装だけで)

require "gpio"
require "spi"

class LCD
  attr_reader :width, :height

  def initialize(controller, spi:, dc:, rst:, bl:)
    @spi = spi
    @dc = GPIO.new(dc, GPIO::OUT)
    @rst = GPIO.new(rst, GPIO::OUT)
    @bl = GPIO.new(bl, GPIO::OUT)

    # Reset
    @rst.write(1); sleep_ms 10
    @rst.write(0); sleep_ms 10
    @rst.write(1); sleep_ms 120

    case controller
    when :gc9107
      init_gc9107
    else
      raise ArgumentError, "Unknown LCD controller: #{controller}"
    end

    @bl.write(1)
  end

  def cmd(byte)
    @dc.write(0)
    @spi.write(byte)
  end

  def data(*bytes)
    @dc.write(1)
    @spi.write(*bytes)
  end

  def fill(color)
    fill_rect(0, 0, @width, @height, color)
  end

  def fill_rect(x, y, w, h, color)
    set_window(x, y, w, h)
    pixel = ((color >> 8) & 0xFF).chr + (color & 0xFF).chr
    buf = pixel * w
    @dc.write(1)
    i = 0
    while i < h
      @spi.write(buf)
      i += 1
    end
  end

  def blit(x, y, w, h, bitmap, fg, bg)
    set_window(x, y, w, h)
    fg_hi = (fg >> 8) & 0xFF
    fg_lo = fg & 0xFF
    bg_hi = (bg >> 8) & 0xFF
    bg_lo = bg & 0xFF
    @dc.write(1)
    i = 0
    total = w * h
    while i < total
      if (bitmap.getbyte(i >> 3) >> (7 - (i & 7))) & 1 == 1
        @spi.write(fg_hi, fg_lo)
      else
        @spi.write(bg_hi, bg_lo)
      end
      i += 1
    end
  end

  def set_window(x, y, w, h)
    x1 = x + @offset_x
    x2 = x1 + w - 1
    y1 = y + @offset_y
    y2 = y1 + h - 1
    cmd(0x2A)
    data((x1 >> 8) & 0xFF, x1 & 0xFF, (x2 >> 8) & 0xFF, x2 & 0xFF)
    cmd(0x2B)
    data((y1 >> 8) & 0xFF, y1 & 0xFF, (y2 >> 8) & 0xFF, y2 & 0xFF)
    cmd(0x2C)
  end

  def init_gc9107
    @width = 128
    @height = 128
    @offset_x = 2
    @offset_y = 1

    cmd(0xFE)               # Inter Register Enable 1
    cmd(0xEF)               # Inter Register Enable 2
    cmd(0x3A); data(0x05)   # Pixel format: RGB565
    cmd(0x36); data(0xC8)   # Memory access control
    cmd(0x11)               # Sleep out
    sleep_ms 120
    cmd(0x21)               # Display inversion on
    cmd(0x29)               # Display on
  end
end

mpu6886のmrbgemも使って傾けたら笑う、振ったらぐるぐるした目みたいなのが一応mrbにコンパイルしなくてもギリいける。

require "spi"
require "lcd"
require "i2c"
require "mpu6886"

spi = SPI.new(unit: :ESP32_SPI2_HOST, frequency: 40_000_000,
              sck_pin: 17, copi_pin: 21, cipo_pin: -1, cs_pin: 15)
lcd = LCD.new(:gc9107, spi: spi, dc: 33, rst: 34, bl: 16)
lcd.fill(0xFFFF)
i2c = I2C.new(unit: :ESP32_I2C0, frequency: 100_000,
              sda_pin: 38, scl_pin: 39)
mpu = MPU6886.new(i2c)

EO = "\x04\x07\xF1\xFF\x3F\xE7\xFD\xFF\xDF\xF3\xFE\x7F\xC7\xF0\x10\x00"
EC = "\x00\x00\x00\x00\x00\x00\x01\xFF\xC0\x00\x00\x00\x00\x00\x00\x00"
EE = "\x00\x00\x00\x38\x1F\xC7\xFC\xFF\xBF\xFF\xFF\x00\x00\x00\x00\x00"
ES = "\x00\x00\x00\x00\x00\x00\x00\x00\x01\xE0\x00\x7F\xE0\x07\xCF\x80\x78\x0E\x03\x00\x38\x38\x00\xC1\x87\x87\x0C\x3E\x18\x60\x38\xC3\x01\xC6\x1C\x0E\x30\x70\x61\x83\xEF\x0C\x0F\xF0\x00\x1E\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00"
EH = "\x63\x7B\xFF\xFF\xF7\xF1\xF8\x70\x10"

lcd.fill(0xFFFF)
lcd.blit(27, 53, 11, 11, EO, 0x0000, 0xFFFF)
lcd.blit(91, 53, 11, 11, EO, 0x0000, 0xFFFF)
lcd.fill_rect(44, 75, 41, 4, 0x0000)
sp = 0
c = 0
t = 50
while true
  a = mpu.acceleration
  if sp == 0
    c += 1
    if c == t
      t = c + 50 + c * 7 % 5 * 10
      lcd.blit(27, 53, 11, 11, EC, 0x0000, 0xFFFF)
      lcd.blit(91, 53, 11, 11, EC, 0x0000, 0xFFFF)
      sleep_ms 150
      lcd.blit(27, 53, 11, 11, EO, 0x0000, 0xFFFF)
      lcd.blit(91, 53, 11, 11, EO, 0x0000, 0xFFFF)
    end
    ts = a[:x] * a[:x] + a[:y] * a[:y] + a[:z] * a[:z]
    if ts > 2.5
      sp = 40
      lcd.blit(22, 48, 21, 21, ES, 0x0000, 0xFFFF)
      lcd.blit(86, 48, 21, 21, ES, 0x0000, 0xFFFF)
      lcd.fill_rect(44, 73, 41, 8, 0xFFFF)
      lcd.fill_rect(52, 73, 25, 8, 0x0000)
    elsif ts < 1.5 && a[:z] > 0.7
      sp = -20
      lcd.blit(27, 53, 11, 11, EE, 0x0000, 0xFFFF)
      lcd.blit(91, 53, 11, 11, EE, 0x0000, 0xFFFF)
      lcd.fill_rect(44, 73, 41, 8, 0xFFFF)
      lcd.fill_rect(52, 73, 25, 8, 0x0000)
      lcd.blit(106, 30, 9, 8, EH, 0x0000, 0xFFFF)
    end
  else
    if sp > 0
      sp -= 1
    else
      sp += 1
    end
    if sp == 0
      lcd.fill(0xFFFF)
      lcd.blit(27, 53, 11, 11, EO, 0x0000, 0xFFFF)
      lcd.blit(91, 53, 11, 11, EO, 0x0000, 0xFFFF)
      lcd.fill_rect(44, 75, 41, 4, 0x0000)
    end
  end
  sleep_ms 100
end

最初からこうではないけどちょっとずつ試しながらあれやこれやとやってるとコードが長くなっていき、節約のため短くしようとしたらこうなった。 一度作ってみるとああしたい、こうしたいが出てくるのでまた磨いていく。動けば一緒と言ってしまえばそうなんだけど、気に入るまで手を入れられるのが個人開発のいいところ。

とはいえ動くのは嬉しいからいろんなところ連れ回してる。