Peripheral Mocking
Create stand-ins for the real chips and sensors your firmware talks to, so you can test how your code reads and writes them — without any physical parts. These stand-ins (called mock devices) let you decide exactly what each chip “replies,” so you can rehearse every situation your firmware might meet, on your computer.
Quick glossary for this page: I2C and SPI are two common buses chips use to talk to each other. MOSI/MISO are the SPI data lines (Master-Out/Slave-In and Master-In/Slave-Out). MSB means “most significant byte” — the leading byte of a multi-byte value. A NACK is a “not acknowledged” reply (the chip saying it didn’t accept the message). A JEDEC ID is a chip-identification response some memory chips return when asked.
I2C Mock Devices
ISimI2CDevice Interface
Implement this interface to mock an I2C peripheral:
import type { ISimI2CDevice } from '@typecad/simulator';
const mockBME280: ISimI2CDevice = {
read(register: number, count: number): number[] {
// Return fake sensor data based on register
if (register === 0xFA) return [0x7E, 0x02, 0x40]; // Temperature MSB
if (register === 0xFD) return [0x6B, 0x00, 0x20]; // Humidity MSB
if (register === 0xD0) return [0x60]; // Chip ID
return new Array(count).fill(0);
},
write(register: number, data: number[]): void {
// Handle configuration writes
if (register === 0xF4) {
console.log(`Ctrl meas: mode=${data[0]}`);
}
},
};| Method | Parameters | Returns | Description |
|---|---|---|---|
read | register: number, count: number | number[] | Handle a register read, return byte array of length count |
write | register: number, data: number[] | void | Handle a register write |
Attaching to a Simulated Bus
import { createSimBoard } from '@typecad/simulator';
const board = createSimBoard({ boardType: 'arduino-uno', i2cBusCount: 1 });
const i2c = board.i2c(0);
// Attach mock BME280 at address 0x76
i2c.attachDevice(0x76, mockBME280);
// Detach when done
i2c.detachDevice(0x76);Using the Bus with a Mock
Firmware code that uses I2C device accessors will automatically hit the mock:
i2c.begin();
i2c.setClock(100000);
const device = i2c.device(0x76);
const chipId = device.readByte(0xD0); // → 0x60 (from mock)
device.writeByte(0xF4, 0x27); // → logged by mock
const tempData = device.readBytes(0xFA, 3); // → [0x7E, 0x02, 0x40]Operation Logging
Every I2C read and write is logged for test assertions:
const log = i2c.getLog();
for (const entry of log) {
console.log(`${entry.operation} @ 0x${entry.address.toString(16)} reg=0x${entry.register.toString(16)}`);
if (entry.data) console.log(` data: ${entry.data}`);
}
// Clear log between test cases
i2c.clearLog();I2COperationLog fields:
| Field | Type | Description |
|-------|------|-------------|
| operation | 'read' \| 'write' | Operation type |
| address | number | I2C device address |
| register | number | Register address |
| data | number[] | Data bytes (writes) |
| timestamp | number | Monotonic timestamp |
Error Handling
If firmware accesses an address with no mock device, the bus fires an error:
i2c.onError((status, address, operation) => {
console.log(`I2C error: ${status} on ${address} during ${operation}`);
});
const unknown = i2c.device(0x42);
unknown.readByte(0x00); // Fires NACK error — no device at 0x42SPI Mock Devices
ISimSPIDevice Interface
Implement this interface to mock an SPI peripheral:
import type { ISimSPIDevice } from '@typecad/simulator';
const mockFlash: ISimSPIDevice = {
// Required: handle full-duplex transfers
transfer(mosiData: number[]): number[] {
if (mosiData[0] === 0x9F) {
return [0xEF, 0x40, 0x18, 0x00]; // JEDEC ID response
}
if (mosiData[0] === 0x03) {
// Read command: return fake data
return [0x00, ...new Array(mosiData.length - 1).fill(0x42)];
}
return new Array(mosiData.length).fill(0xFF);
},
// Optional: handle register writes
write(register: number, data: number[]): void {
console.log(`SPI write reg 0x${register.toString(16)}: [${data}]`);
},
// Optional: handle register reads
readRegister(register: number, count: number): number[] {
return new Array(count).fill(0x42);
},
};| Method | Required | Description |
|---|---|---|
transfer(mosiData) | Yes | Full-duplex: receive MOSI, return MISO |
write(register, data) | No | Falls back to transfer() if not provided |
readRegister(register, count) | No | Falls back to transfer() with dummy bytes |
Attaching to a Simulated SPI Bus
SPI devices are keyed by chip-select pin. Use a simulated digital pin as the CS so the mock device keys cleanly against its pin number:
import { createSimBoard } from '@typecad/simulator';
const board = createSimBoard({ boardType: 'arduino-uno', spiBusCount: 1 });
const spi = board.spi(0);
// Chip-select on pin 10
const cs = board.digital(10).asOutput();
// Attach mock flash memory on CS pin 10
spi.attachDevice(cs, mockFlash);
// Get a device accessor scoped to the CS pin
const flash = spi.device(cs);
const jedecId = flash.transfer([0x9F, 0x00, 0x00, 0x00]);
// → [0xEF, 0x40, 0x18, 0x00]SPI Device Accessor Methods
| Method | Description |
|---|---|
transfer(data) | Full-duplex transfer through mock device |
write(data) | Write-only; uses mock’s write() or falls back to transfer() |
read(count) | Read N bytes by sending dummy zeros |
writeRegister(reg, data) | Write to a register address |
readRegister(reg, count) | Read from a register address |
Operation Logging
const log = spi.getLog();
for (const entry of log) {
console.log(`${entry.operation} reg=${entry.register ?? '-'} data=${entry.data}`);
}
spi.clearLog();SPIOperationLog fields:
| Field | Type | Description |
|-------|------|-------------|
| operation | 'read' \| 'write' \| 'transfer' | Operation type |
| register | number | Register address (if applicable) |
| count | number | Byte count requested |
| data | number[] | MOSI data sent |
| response | number[] | MISO data returned |
| timestamp | number | Monotonic timestamp |
Testing Patterns
Verify Initialization Sequence
let initialized = false;
const mockSensor: ISimI2CDevice = {
read() { return []; },
write(reg, data) {
// BME280 reset command
if (reg === 0xE0 && data[0] === 0xB6) initialized = true;
},
};
// After running firmware init code
describe('Sensor init')
.it('sends reset command')
.expect(initialized ? 1 : 0).toBeTruthy();
done();Verify Register Read Sequence
const mockSensor: ISimI2CDevice = {
read(reg) {
if (reg === 0xD0) return [0x60]; // Chip ID
return [0];
},
write() {},
};
// Run firmware that reads chip ID
// ...
const log = i2c.getLog();
describe('Chip ID read')
.it('reads register 0xD0')
.expect(log.length).toBeGreaterThanOrEqual(1)
.it('reads correct chip ID')
.expect(log[0].register).toBe(0xD0);
done();Simulate Sensor Fault
const faultySensor: ISimI2CDevice = {
read() { return []; }, // Empty read = bus error / no device
write() {},
};Verify SPI JEDEC ID
describe('Flash JEDEC ID')
.it('reads correct manufacturer')
.expect(jedecId[0]).toBe(0xEF)
.it('reads correct memory type')
.expect(jedecId[1]).toBe(0x40);
done();On This Page