Skip to content

Hardware Control (GPIO / I2C / RMT / RTC)

APIs for operating hardware peripherals from Ruby.

Note

For pin layouts and electrical specifications, see Hardware. To check whether a pin is already in use by another function, use FmrbHw.pin_status.

Pin numbers differ between the machines

The GROVE pins are not the same on Retro and Modern, so an app that hardcodes a number runs on one machine only. Branch on the board instead:

ruby sda, scl = case FmrbConst::BOARD when "tab5", "naryav4" then [53, 54] # Tab5 GROVE else [47, 48] # narya-board GROVE 2 end

Some pins are held by the system and cannot be acquired at all — see Hardware for the reserved list on each machine.

GPIO

Digital input/output.

Constructor

GPIO.new(pin, flags, alt_function = 0)
Argument Purpose
pin GPIO number
flags Direction and pull configuration (can be OR-combined)
alt_function Alternate function number (normally 0)

flags Constants

Constant Meaning
GPIO::IN Input
GPIO::OUT Output
GPIO::HIGH_Z High impedance
GPIO::PULL_UP Pull-up resistor enabled
GPIO::PULL_DOWN Pull-down resistor enabled
GPIO::OPEN_DRAIN Open drain output
GPIO::ALT Alternate function

Combine with OR:

btn = GPIO.new(10, GPIO::IN | GPIO::PULL_UP)
led = GPIO.new(11, GPIO::OUT)

Instance Methods

Method Purpose
read Read the level (0 / 1)
write(val) Output 0 or 1
high? Returns true if the level is High
low? Returns true if the level is Low

Class Methods (Direct Operation Without Creating an Instance)

Method Purpose
GPIO.read_at(pin) Read level
GPIO.write_at(pin, val) Output level
GPIO.high_at?(pin) / GPIO.low_at?(pin) Level check
GPIO.set_dir_at(pin, dir) Change direction only
GPIO.pull_up_at(pin) / GPIO.pull_down_at(pin) Enable pull resistor
GPIO.open_drain_at(pin) Open drain
GPIO.set_function_at(pin, alt) Alternate function

Example: Toggle LED with a Button

class LedToggle < FmrbApp
  BTN_PIN = 10
  LED_PIN = 11

  def on_create
    @btn = GPIO.new(BTN_PIN, GPIO::IN | GPIO::PULL_UP)
    @led = GPIO.new(LED_PIN, GPIO::OUT)
    @led.write(0)
    @prev = 1
  end

  def on_update
    cur = @btn.read
    if @prev == 1 && cur == 0   # Falling edge
      @led.write(@led.read == 1 ? 0 : 1)
    end
    @prev = cur
    20
  end
end

LedToggle.new.start

I2C

Communicates with devices via the I2C bus.

Constructor

I2C.new(unit:, frequency: 100_000, sda_pin: -1, scl_pin: -1, timeout: 500)
Argument Purpose
unit: Bus identifier. e.g. "ESP32_I2C0", "ESP32_I2C1"
frequency: Clock frequency (Hz). Default 100kHz
sda_pin: / scl_pin: -1 uses the default pins
timeout: Timeout in ms

Methods

Method Purpose
read(addr_7bit, length, timeout: @timeout, *write_data) Read (write_data triggers a write first, then read without STOP)
write(addr_7bit, *data, timeout: @timeout) Write. Accepts Integer / Array<Integer> / String
scan(timeout: @timeout) Scan addresses 0x08 to 0x77 and return a list of responding devices
close Release the bus

Example: Scanning I2C Devices

i2c = I2C.new(unit: "ESP32_I2C0", frequency: 400_000)
addrs = i2c.scan
Log.info("found: #{addrs.map { |a| a.to_s(16) }.join(', ')}")

Example: Register Read/Write

i2c = I2C.new(unit: "ESP32_I2C0")
# Read 4 bytes starting from register 0x10
data = i2c.read(0x32, 4, 0x10)   # Arguments after the 2nd are write_data
i2c.write(0x32, 0x10, 0xAB)      # Write 0xAB to register 0x10

RMT

Drives WS2812B / WS2812 LEDs and infrared remotes via the ESP32 RMT peripheral.

Constructor

RMT.new(pin, t0h_ns:, t0l_ns:, t1h_ns:, t1l_ns:, reset_ns:)

Specifies NRZ encoding timing in nanoseconds. Standard values for WS2812B:

rmt = RMT.new(8,
              t0h_ns: 350,  t0l_ns: 900,
              t1h_ns: 700,  t1l_ns: 600,
              reset_ns: 50_000)

Methods

Method Purpose
write(*params) Output byte sequence. Accepts Integer / Array / String arguments

Example: Lighting WS2812B LEDs

class LedStrip < FmrbApp
  RMT_PIN = 8

  def on_create
    @rmt = RMT.new(RMT_PIN,
                    t0h_ns: 350,  t0l_ns: 900,
                    t1h_ns: 700,  t1l_ns: 600,
                    reset_ns: 50_000)
    set_color(0xFF, 0x00, 0x00)  # Red
  end

  def set_color(r, g, b)
    # WS2812B uses GRB order
    @rmt.write([g, r, b])
  end
end

LedStrip.new.start

A WS2812B matrix sample is available in /app/demo/led_matrix.app.rb.

An 8x8 WS2812B matrix driven from the device, with the source open in the editor

The app draws the same pattern on screen as it sends to the panel, so you can see what it intends to do even with nothing wired up. Note the LED_PIN branch on FmrbConst::BOARD near the top of the source — the GROVE pins differ between the machines.

Real-Time Clock (RX8900 / RX8130)

Both machines carry a battery-backed RTC on the I2C bus, at address 0x32. The part differs:

Machine Part Class
Retro (narya-board) RX8900, on I2C1, shared with GROVE 1 RX8900
Modern (M5Stack Tab5) RX8130, on the internal bus RX8130

The two classes take the same arguments and answer to the same methods, so an app that picks the class by board runs on either:

i2c = I2C.new(unit: "ESP32_I2C0")
rtc = FmrbConst::BOARD == "tab5" ? RX8130.new(i2c) : RX8900.new(i2c)
rtc.sync_system_clock

Methods

Method Return value / purpose
init Initialise the RTC
read_time {year:, month:, day:, hour:, minute:, second:, wday:}
write_time(hash) Write a time
sync_system_clock Set the system clock from the RTC. true on success
vlf? Low-voltage flag. true means the stored time may be lost
temperature Temperature in Celsius (Float). RX8900 only

Example: sync the clock at startup

class ClockSyncApp < FmrbApp
  def on_create
    i2c = I2C.new(unit: "ESP32_I2C0")
    rtc = FmrbConst::BOARD == "tab5" ? RX8130.new(i2c) : RX8900.new(i2c)
    if rtc.vlf?
      Log.warn("RTC battery low; resetting")
      rtc.write_time(year: 2026, month: 1, day: 1,
                     hour: 0, minute: 0, second: 0, wday: 4)
    end
    rtc.sync_system_clock
    now = rtc.read_time
    Log.info("time: #{now[:year]}-#{now[:month]}-#{now[:day]} #{now[:hour]}:#{now[:minute]}")
  end
end

ClockSyncApp.new.start

You usually do not need this

The system syncs from the RTC at boot, and FmrbApp.wallclock / FmrbApp.set_wallclock go through the system clock (POSIX epoch), which also writes back to the RTC. Reach for the driver classes only when you want the chip's own features — the low-voltage flag, or the RX8900's temperature sensor.

Pin Assignment Check

Before using a pin, you can check whether the system is already using it.

unless FmrbHw.pin_available?(10)
  Log.error("Pin 10 already in use: #{FmrbHw.pin_status(10)}")
  return
end

For details, see FmrbHw.