Skip to main content

CAN node emulation and conformance

The AT1032S joins a CAN bus as a full node, with its 120 Ω termination switched on in software and acceptance filters applied in hardware. That lets it do two jobs at once: check that the board under test answers requests correctly, and stand in for the ECUs that are not on the bench, so a control board runs as though the whole machine were around it.

The AT1032S joins the CAN bus on DA24 and DA25 as a full node. It sends requests and checks the replies from the control board, and transmits the periodic frames of an ECU that is not on the bench, so the board runs as if the whole machine were present.

One node on the bus, playing tester and missing-ECU at the same time.

Wiring

AT1032SBus
DA24 CANHCAN high
DA25 CANLCAN low

The test sequence

import { AT1000 } from '@ikalogic/at1000';

// ---- The bus and the frames we care about ---------------------------------
const BAUD_RATE = 500000;
const TERMINATION = true; // switch the 120 ohm on when we are at the bus end

const REQUEST_ID = 0x120; // what we ask the board
const REQUEST_DATA = [0x01, 0x00]; // a "report status" command on this bus
const REPLY_ID = 0x121; // what the board must answer with
const REPLY_BYTES = 4; // how long a good answer is
const REPLY_TIMEOUT_MS = 200;

// Frames a missing ECU would normally put on the bus.
const EMULATED = [
{ id: 0x300, data: [0x01, 0x00, 0x00, 0x00], period_ms: 100, name: 'sensor ECU' },
{ id: 0x310, data: [0x00, 0x80], period_ms: 200, name: 'actuator ECU' },
];
const RUN_SECONDS = 10;
// ---------------------------------------------------------------------------

const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
const hex = (n) => '0x' + n.toString(16).toUpperCase();

const devices = await AT1000.findDevices();
const tester = await AT1000.open(devices[0]);
await tester.reset();

const can = tester.com.can(0);

// Accept only the reply id. The mask clears the bits that must match exactly.
await can.configure({
enabled: true,
baud_rate: BAUD_RATE,
termination_resistors: TERMINATION,
rx_filter: { mask: 0x7FF, id: REPLY_ID, extended_id: false },
});
await can.start_rx();

// ---- 1. Request and reply -------------------------------------------------
await can.rx(); // drop anything already buffered
await can.tx({ id: REQUEST_ID, data: REQUEST_DATA });

let reply = null;
const deadline = Date.now() + REPLY_TIMEOUT_MS;
while (Date.now() < deadline && reply === null) {
const frames = await can.rx();
reply = frames.find((frame) => frame.id === REPLY_ID) ?? null;
if (reply === null) await sleep(10);
}

if (reply === null) {
console.log(`FAIL: no reply on ${hex(REPLY_ID)} within ${REPLY_TIMEOUT_MS} ms`);
} else {
console.log(`Reply ${hex(reply.id)}: ${reply.data.map((b) => hex(b)).join(' ')}`);
console.log(reply.data.length === REPLY_BYTES
? 'Reply length ok'
: `FAIL: ${reply.data.length} bytes`);
}

// ---- 2. Stand in for the ECUs that are not on the bench --------------------
const names = EMULATED.map((e) => e.name).join(' and ');
console.log(`\nEmulating ${names} for ${RUN_SECONDS} s.`);

const started = Date.now();
const lastSent = EMULATED.map(() => 0);

while (Date.now() - started < RUN_SECONDS * 1000) {
const now = Date.now();
for (let i = 0; i < EMULATED.length; i++) {
if (now - lastSent[i] >= EMULATED[i].period_ms) {
await can.tx({ id: EMULATED[i].id, data: EMULATED[i].data });
lastSent[i] = now;
}
}

// Anything the board sends back while we play the missing ECUs.
for (const frame of await can.rx()) {
console.log(` board sent ${hex(frame.id)}: ` +
`${frame.data.map((b) => hex(b)).join(' ')}`);
}

await sleep(10);
}

await can.stop_rx();
await can.disable();

await tester.reset();

Adapting it to your bus

  • BAUD_RATE covers 10 kbit/s to 1 Mbit/s, and TERMINATION switches the 120 Ω on when the AT1032S sits at the end of the bus.
  • rx_filter decides what reaches your script. Widen the mask to take a range of ids, or set the mask to 0 to receive everything on the bus.
  • EMULATED is the list of frames the AT1032S puts on the bus on behalf of hardware that is not present. Add one entry per missing node.
  • The ids and payloads above stand in for whatever your bus actually speaks. Replace REQUEST_DATA, REPLY_BYTES and the EMULATED frames with your own. A protocol layered on top of CAN, such as UDS or CANopen, adds its own framing rules on top of the raw frames shown here.
  • For a worked account of this approach on a real installation, see reproducing a factory CANopen and Modbus network on the bench.

API used on this page