Přeskočit na obsah

Berry I2C

Jak ve firmware 0.12.11 používat obecné I2C API v Berry Scriptu pro čtení a zápis vlastních zařízení.

Berry I2C zpřístupňuje v kontroleru obecný I2C bus jako objekt io["I2C"].

To znamená, že pro mnoho I2C zařízení už není potřeba přidávat nový hardcoded driver do firmware. Místo toho lze:

  • nadefinovat I2C v Controller Configu
  • v Berry Scriptu zařízení najít, číst a zapisovat jeho registry
  • postavit si vlastní jednoduchý driver přímo v projektu

Typický scénář je například:

  • senzor okolního světla
  • akcelerometr nebo IMU
  • kapacitní touch kontroler
  • jiný I2C periferní čip se známým datasheetem

Berry I2C funguje nad jedním IO typu I2C, které je definované v Controller Configu.

Příklad:

{
"I2C": {
"type": "I2C",
"frequency": 400000,
"sda": 27,
"scl": 14
}
}

Důležité:

  • v aktuální verzi firmware je podporovaný jeden Berry I2C bus na kontroler
  • pokud I2C v configu není, io["I2C"] nebude pro váš script použitelný

Berry Script získá I2C bus takto:

var bus = io["I2C"]

Jakmile máte bus, můžete nad ním volat obecné I2C metody.

Vrátí seznam nalezených 7bit I2C adres.

var bus = io["I2C"]
print("scan:", bus.scan())

Ověří, jestli zařízení na dané adrese odpovídá.

if bus.probe(0x10)
print("device found")
end

Provede čisté I2C čtení o délce len.

Použijte ho ve chvíli, kdy zařízení očekává samotné čtení bez register prefixu.

read(addr, len) samo o sobě nenastavuje adresu registru. Pokud potřebujete nejprve zapsat registr a potom číst, použijte writeRead(...) nebo readReg(...).

Zapíše na danou adresu byte payload a vrátí počet zapsaných bytů.

var tx = bytes().append(0x03).append(0x00).append(0x00)
bus.write(0x10, tx)

Provede transakci typu write-then-read. To je běžný způsob práce s registry u mnoha I2C zařízení.

var data = bus.writeRead(0x10, bytes().append(0x04), 2)

Zjednodušený helper pro čtení registru.

var data = bus.readReg(0x10, 0x04, 2)

Zjednodušený helper pro zápis registru.

data může být:

  • jedno byte číslo
  • objekt bytes
bus.writeReg(0x10, 0x03, 0x00)
bus.writeReg(0x10, 0x00, bytes().append(0x00).append(0x00))

Berry I2C vrstva pracuje schválně jen s bytovým payloadem.

Interpretaci hodnot si řešíte v Berry Scriptu podle datasheetu zařízení:

  • little-endian vs. big-endian
  • signed vs. unsigned
  • převod registrů na fyzikální jednotky
  • vlastní inicializační sekvence

To je záměr. Firmware dává obecný transport a projektový driver si skládáte v Berry.

Tento příklad:

  • zkontroluje, že na adrese 0x10 něco odpovídá
  • přečte 16bit little-endian hodnotu z registru 0x04
  • vypíše ji do logu
var bus = io["I2C"]
if !bus.probe(0x10)
print("I2C device 0x10 not found")
return
end
var raw = bus.readReg(0x10, 0x04, 2)
var value = raw[0] | (raw[1] << 8)
print("raw:", value)

Pro VEML7700 lze v Berry Scriptu udělat i velmi krátký projektový driver.

Následující příklad:

  • ověří přítomnost zařízení
  • nastaví základní konfiguraci
  • jednou za sekundu vypisuje ALS lux hodnotu
var bus = io["I2C"]
var ok = false
var last = -1000
try
if bus != nil && bus.probe(0x10)
# ALS_CONF = 0x0000 => gain 1x, integration 100 ms
bus.writeReg(0x10, 0x00, bytes().append(0x00).append(0x00))
# Power saving off
bus.writeReg(0x10, 0x03, 0x00)
ok = true
end
except ..
ok = false
end
Plugin(def()
if !ok || controller.millis() - last < 1000
return
end
last = controller.millis()
try
var raw = bus.readReg(0x10, 0x04, 2)
var als = raw[0] | (raw[1] << 8)
var lux = als * 0.0576
print("VEML7700 lux:", lux)
except ..
print("VEML7700 read failed")
end
end)

Níže jsou příklady ve stylu, který se v projektech Spectoda osvědčuje:

  • komentáře vysvětlují, co se děje
  • samotný kód zůstává krátký a úsporný
  • názvy proměnných jsou záměrně stručné, aby script zbytečně nezabíral RAM

Tento plugin:

  • ověří VEML7700 na adrese 0x10
  • nastaví základní konfiguraci
  • jednou za sekundu vypisuje lux hodnotu
var b = io["I2C"]
var ok = false
var last = -1000
# ALS_CONF = 0x0000 => gain 1x, integration 100 ms.
# PSM = 0x00 => normal mode.
try
if b != nil && b.probe(0x10)
b.writeReg(0x10, 0x00, bytes().append(0x00).append(0x00))
b.writeReg(0x10, 0x03, 0x00)
ok = true
end
except ..
ok = false
end
Plugin(def()
if !ok || controller.millis() - last < 1000
return
end
last = controller.millis()
try
var d = b.readReg(0x10, 0x04, 2)
var raw = d[0] | (d[1] << 8)
var lux = raw * 0.0576
print("VEML7700 lux:", lux)
except ..
print("VEML7700 read failed")
end
end)

VEML7700: Daylight plugin jako drop-in replacement

Section titled “VEML7700: Daylight plugin jako drop-in replacement”

Tento příklad je kompaktní náhrada za starší plugin, který používal firmware-specific helpery.

Chování zůstává stejné:

  • čte stejné NUMBER hodnoty v milli-lux jako původní helper
  • publikuje DAYLI
  • při chybě emituje E_INI nebo E_REA
  • upravuje brigh podle cílové lux hodnoty
def Daylight(S)
import controller, math
var id = S["id"]
var target = S["target"]
var toggl = EVS("toggl", id)
var brigh = EVS("brigh", id)
var L_ena = EVS("L_ena", id)
# Původní helper používal pevně gain 1x a integration 100 ms.
# Pokud to změníte, musí zůstat odpovídající i přepočet raw -> milli-lux.
var als_conf = 0x0000
var mlux_per_count_x10 = 576
var bus = nil
var ok = false
var err_init = EVT("DAYLI", "E_INI", id, LABEL)
var err_read = EVT("DAYLI", "E_REA", id, LABEL)
# Přímá náhrada za staré __vmin("I2C", 1.0, 100):
# jeden fixní init senzoru bez auto-range.
try
bus = io["I2C"]
if bus != nil
var cfg = bytes()
cfg.append(als_conf & 0xff)
cfg.append((als_conf >> 8) & 0xff)
bus.writeReg(0x10, 0x00, cfg)
bus.writeReg(0x10, 0x03, 0x00)
ok = true
end
except ..
ok = false
end
if !ok
err_init()
end
# Přímá náhrada za staré __vmre():
# čte ALS register a vrací NUMBER v milli-lux.
var rd = def()
if !ok
return nil
end
try
var d = bus.readReg(0x10, 0x04, 2)
var raw = d[0] | (d[1] << 8)
return int((raw * mlux_per_count_x10) / 10)
except ..
return nil
end
end
EVS("DAYLI", id).cb = def(v)
if v.is(NULL)
var lux = rd()
if lux == nil
err_read()
else
EVS.emit("DAYLI", lux, id, NUMBER)
end
return
end
end
var last = controller.millis()
return Plugin(def()
var now = controller.millis()
if now - last < 1000 || !L_ena || !toggl
return
end
last = now
var lux = rd()
if lux == nil
err_read()
return
end
var dis = math.abs(lux - target)
if dis == 0 || lux == 0
return
end
var dif = real(dis) / real(lux)
if dif < 0.025 || dis < 10
return
end
var b0 = brigh.get(PERCENTAGE)
if lux < target
if dif > 0.1
b0 += 5
else
b0 += 1
end
else
if dif > 0.1
b0 -= 5
else
b0 -= 1
end
end
if b0 < 0
b0 = 0
elif b0 > 100
b0 = 100
end
brigh.set(b0, PERCENTAGE)
end)
end

Tento příklad ukazuje, jak přes Berry I2C připojit MPR121.

Příklad:

  • používá výchozí adresu 0x5A
  • nastaví touch a release threshold pro všech 12 elektrod
  • čte touched mask z registrů 0x00 a 0x01
  • na náběžné hraně dotyku na zvolené elektrodě přepne toggl

Pokud máte pin ADDR zapojený jinak, může být adresa místo 0x5A také 0x5B, 0x5C nebo 0x5D.

Prakticky je dobré vědět:

  • MPR121 vrací stav dotyků jako 12bit masku
  • bit 0 odpovídá elektrodě 0, bit 3 elektrodě 3
  • pro ovládání přes dotyk je vhodné reagovat jen na náběžnou hranu, ne na celý čas držení dotyku
import controller
class I2CDevice
var bus, addr
def init(bus, addr)
self.bus = bus
self.addr = addr
end
def probe()
return self.bus != nil && self.bus.probe(self.addr)
end
def read_reg(reg, len)
return self.bus.readReg(self.addr, reg, len)
end
def write_reg(reg, data)
return self.bus.writeReg(self.addr, reg, data)
end
def read_u16le(reg)
var data = self.read_reg(reg, 2)
return data[0] | (data[1] << 8)
end
end
class MPR121 : I2CDevice
def init(bus, addr)
super(self).init(bus, addr == nil ? 0x5a : addr)
end
def begin(touch_threshold, release_threshold)
if !self.probe()
raise "runtime_error", "MPR121 not found on I2C bus"
end
# Soft reset a stop mód před konfigurací.
self.write_reg(0x80, 0x63)
self.write_reg(0x5e, 0x00)
self.set_thresholds(
touch_threshold == nil ? 12 : touch_threshold,
release_threshold == nil ? 6 : release_threshold,
)
# Základní doporučené filtrování a autoconfig.
self.write_reg(0x2b, 0x01)
self.write_reg(0x2c, 0x01)
self.write_reg(0x2d, 0x00)
self.write_reg(0x2e, 0x00)
self.write_reg(0x5d, 0x04)
# Enable 12 electrodes, run mode.
self.write_reg(0x5e, 0x8c)
return self
end
def set_thresholds(touch_threshold, release_threshold)
for electrode : 0 .. 11
self.write_reg(0x41 + electrode * 2, touch_threshold & 0xff)
self.write_reg(0x42 + electrode * 2, release_threshold & 0xff)
end
end
def touched_mask()
return self.read_u16le(0x00) & 0x0fff
end
def is_touched(electrode)
if electrode < 0 || electrode > 11
raise "value_error", "electrode must be between 0 and 11"
end
return ((self.touched_mask() >> electrode) & 0x01) == 1
end
end
controller.ups = 100
var touch_electrode = 3
var sensor = nil
var last_mask = 0
var last_poll = -1000
var toggl = EVS("toggl")
var mprok = EVS("mprok")
var mperr = EVS("mperr")
var mpmsk = EVS("mpmsk")
def ensure_sensor()
if sensor != nil
return sensor
end
sensor = MPR121(io["I2C"], 0x5a).begin(12, 6)
last_mask = sensor.touched_mask()
mprok.set(true, BOOLEAN)
mperr.set(0, NUMBER)
if !toggl.is(BOOLEAN)
toggl.set(false, BOOLEAN)
end
return sensor
end
Plugin(def()
if controller.millis() - last_poll < 20
return
end
last_poll = controller.millis()
try
var dev = ensure_sensor()
var mask = dev.touched_mask()
var rising = mask & (~last_mask)
mpmsk.set(mask, NUMBER)
if (rising & (1 << touch_electrode)) != 0
toggl.set(!toggl.get(BOOLEAN), BOOLEAN)
end
last_mask = mask
mperr.set(0, NUMBER)
except ..
sensor = nil
mprok.set(false, BOOLEAN)
mperr.set(1, NUMBER)
end
end)

Tento plugin je praktický hlavně tehdy, když:

  • potřebujete z kapacitního touch padu udělat jednoduché tlačítko
  • chcete si nejdřív ověřit, že I2C komunikace funguje, než budete dělat složitější logiku
  • potřebujete si dočasně vystavit diagnostiku přes mprok, mperr a mpmsk

Tento plugin ukazuje, jak přes Berry I2C připojit LSM6DS3TR.

Příklad:

  • používá výchozí I2C adresu 0x6A
  • ověří WHO_AM_I
  • zapne akcelerometr a gyroskop na 104 Hz
  • průběžně vypisuje akceleraci v g a gyroskop v dps

Pokud máte pin SA0 zapojený jinak, může být potřeba změnit adresu na 0x6B.

var b = io["I2C"]
var ok = false
var last = -1000
# Convert two bytes into signed int16.
var s16 = def(lo, hi)
var v = lo | (hi << 8)
if v >= 0x8000
v -= 0x10000
end
return v
end
# Default address is 0x6A when SA0 is low.
# WHO_AM_I should return 0x6A.
# CTRL3_C = 0x44 enables BDU and register auto-increment.
# CTRL1_XL = 0x40 enables accelerometer at 104 Hz, ±2 g.
# CTRL2_G = 0x40 enables gyroscope at 104 Hz, ±245 dps.
try
if b != nil && b.probe(0x6A)
if b.readReg(0x6A, 0x0F, 1)[0] == 0x6A
b.writeReg(0x6A, 0x12, 0x44)
b.writeReg(0x6A, 0x10, 0x40)
b.writeReg(0x6A, 0x11, 0x40)
ok = true
end
end
except ..
ok = false
end
Plugin(def()
if !ok || controller.millis() - last < 500
return
end
last = controller.millis()
try
# Read gyro XYZ and accel XYZ in one burst.
var d = b.readReg(0x6A, 0x22, 12)
var gx = s16(d[0], d[1]) * 0.00875
var gy = s16(d[2], d[3]) * 0.00875
var gz = s16(d[4], d[5]) * 0.00875
var ax = s16(d[6], d[7]) * 0.000061
var ay = s16(d[8], d[9]) * 0.000061
var az = s16(d[10], d[11]) * 0.000061
print("LSM6DS3TR acc[g]:", ax, ay, az, "gyro[dps]:", gx, gy, gz)
except ..
print("LSM6DS3TR read failed")
end
end)

Když připojujete nové I2C zařízení, osvědčený postup je:

  1. Nejprve ověřit scan() a probe(addr).
  2. Potom ručně přečíst nebo zapsat první registry přes readReg(...) a writeReg(...).
  3. Až potom si kolem toho obalit malý Berry driver pro konkrétní zařízení.

Prakticky to znamená:

  • obecný I2C transport řeší firmware
  • device-specific protokol řeší váš Berry Script
  • změna zařízení často nevyžaduje změnu C++ firmware

To je vhodné hlavně pro projekty, kde potřebujete rychle doplnit podporu konkrétního senzoru nebo periferního čipu.

Ve většině případů pracujte s registry, takže dává smysl:

  • readReg(...)
  • writeReg(...)
  • případně writeRead(...)

Samotné read(...) použijte tehdy, když zařízení opravdu očekává čisté čtení bez register adresy nebo když přesně víte, jak se daný čip v této situaci chová.