Sensors

Cuttlefish has one sensor class that works with every Zephyr sensor driver — over 200 parts, on I2C or SPI. You don’t install a library per sensor, write devicetree fragments, or edit Kconfig. You name the part and the bus, and the build produces the rest.


Reading a Sensor

Make a Sensor from a part token and a bus device, then call fetch() and get():

import { I2C0 } from '@typecad/board';
import { Sensor, SENSOR, CHAN, Time } from '@typecad/hal';

// SHT3X temperature/humidity breakout at I2C address 0x44
const sht3x = new Sensor(SENSOR.sensirion_sht3xd, I2C0.device(0x44));

while (true) {
  sht3x.fetch();                               // take one measurement
  const temp = sht3x.get(CHAN.AMBIENT_TEMP);   // read a channel from it
  const rh = sht3x.get(CHAN.HUMIDITY);         // ...same measurement

  UART0.writeLine(`${temp} C, ${rh} %RH`);
  Time.sleep(2000);
}

fetch() takes the measurement. get() reads one channel from the last fetch. That split is Zephyr’s own — one fetch, many reads — so one measurement can give you several values.

SPI Sensors

The same class takes an SPI bus device. The second argument is the chip-select pin:

import { SPI0, PA4 } from '@typecad/board';
import { Sensor, SENSOR, CHAN } from '@typecad/hal';

// BME280 on SPI, chip select wired to PA4
const bme = new Sensor(SENSOR.bosch_bme280, SPI0.device(PA4));

bme.fetch();
const pressure = bme.get(CHAN.PRESS);   // hectopascals

SPI sensors run at 1 MHz by default, which every part in the catalog tolerates. Pass options as a third argument to run faster or change the SPI mode:

import { SPI0, PA4 } from '@typecad/board';
import { Sensor, SENSOR, CHAN } from '@typecad/hal';

// 10 MHz, SPI mode 3 (CPOL=1, CPHA=1)
const bme = new Sensor(SENSOR.bosch_bme280, SPI0.device(PA4), { spiHz: 10000000, mode: 3 });

The bus argument is checked against the part: hand an SPI-only part an I2C0.device(...) — or an I2C-only part an SPI0.device(...) — and the editor rejects it before you build. Parts that take both buses accept either.

Picking a Part

SENSOR. lists every supported part, with a short description and its buses on hover. The name is the part’s Zephyr compatible string with commas and dashes replaced by underscores: the SHT3XD’s compatible is sensirion,sht3xd, so the token is SENSOR.sensirion_sht3xd. When a Zephyr doc or binding file names a compatible, you can find the token by that rule — no lookup table.

Channels

Channels use Zephyr’s own names, minus the SENSOR_CHAN_ prefix: SENSOR_CHAN_AMBIENT_TEMP in a Zephyr doc means CHAN.AMBIENT_TEMP here. Inside get(...), start typing a channel name and the editor offers CHAN as the first completion — accept it, then pick the member.

Your sensor’s type narrows to the part you constructed. An SHT3XD’s get() accepts CHAN.AMBIENT_TEMP and CHAN.HUMIDITY, and nothing else:

const t = sht3x.get(CHAN.AMBIENT_TEMP);  // fine
const p = sht3x.get(CHAN.PRESS);         // build error — the SHT3XD has no pressure channel

The error names the channels the driver actually serves, so the fix is on the screen.

⚙️ Advanced details — what the build produces

The catalog (SENSOR, CHAN) is generated from Zephyr’s own binding files and driver sources at the pinned Zephyr revision, so it stays in sync with the tree you build against. Each constructed sensor becomes a devicetree child node in the generated board overlay — compatible string, I2C address or chip-select entry — and that node is what turns the Zephyr driver on: Zephyr’s sensor Kconfigs default to enabled when their compatible is present. The only Kconfig written is CONFIG_SENSOR=y, added when the build sees a sensor in your code. Pin conflicts between peripherals you use are reported at build time.

Limits

  • Only I2C and SPI attachments. A part that binds on another bus is rejected with an error naming its real buses.
  • Parts whose binding declares an alert-gpios (the SHT3XD, for example) take an alert option wired to a pin.
  • A handful of parts have no scanned channel list; those accept any channel name and let the driver answer at runtime.