189 lines
6.6 KiB
JavaScript
189 lines
6.6 KiB
JavaScript
|
|
/**
|
|||
|
|
* avr_runner.js
|
|||
|
|
* -------------
|
|||
|
|
* Node.js ATmega328P (Arduino Uno) emulator using avr8js.
|
|||
|
|
*
|
|||
|
|
* Loads a compiled Intel HEX firmware file and emulates the CPU at
|
|||
|
|
* 16 MHz. The USART (Serial) peripheral is bridged to a TCP socket
|
|||
|
|
* so the Python broker can connect and exchange bytes.
|
|||
|
|
*
|
|||
|
|
* Usage:
|
|||
|
|
* node avr_runner.js <hex_file> [broker_host] [broker_port]
|
|||
|
|
*
|
|||
|
|
* The script acts as a TCP CLIENT. It connects (with retries) to the
|
|||
|
|
* Python broker which acts as the server for the Arduino side.
|
|||
|
|
*
|
|||
|
|
* Data flow:
|
|||
|
|
* Pi --> broker:5556 --> avr_runner --> usart.writeByte() --> Arduino RX
|
|||
|
|
* Arduino TX --> usart.onByteTransmit --> broker:5556 --> Pi
|
|||
|
|
*/
|
|||
|
|
|
|||
|
|
'use strict';
|
|||
|
|
|
|||
|
|
const fs = require('fs');
|
|||
|
|
const net = require('net');
|
|||
|
|
const path = require('path');
|
|||
|
|
|
|||
|
|
// ── Load avr8js from local wokwi-libs ────────────────────────────────────────
|
|||
|
|
const AVR8JS_CJS = path.resolve(
|
|||
|
|
__dirname, '..', '..', 'wokwi-libs', 'avr8js', 'dist', 'cjs', 'index.js'
|
|||
|
|
);
|
|||
|
|
|
|||
|
|
let avr8js;
|
|||
|
|
try {
|
|||
|
|
avr8js = require(AVR8JS_CJS);
|
|||
|
|
} catch (e) {
|
|||
|
|
process.stderr.write(`[avr_runner] FATAL: cannot load avr8js from:\n ${AVR8JS_CJS}\n ${e.message}\n`);
|
|||
|
|
process.exit(1);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
const {
|
|||
|
|
CPU,
|
|||
|
|
avrInstruction,
|
|||
|
|
AVRUSART, usart0Config,
|
|||
|
|
AVRTimer, timer0Config, timer1Config, timer2Config,
|
|||
|
|
} = avr8js;
|
|||
|
|
|
|||
|
|
// ── CLI arguments ─────────────────────────────────────────────────────────────
|
|||
|
|
const [,, hexFile, brokerHost = '127.0.0.1', brokerPort = '5556'] = process.argv;
|
|||
|
|
|
|||
|
|
if (!hexFile) {
|
|||
|
|
process.stderr.write('Usage: node avr_runner.js <hex_file> [broker_host] [broker_port]\n');
|
|||
|
|
process.exit(1);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
if (!fs.existsSync(hexFile)) {
|
|||
|
|
process.stderr.write(`[avr_runner] ERROR: hex file not found: ${hexFile}\n`);
|
|||
|
|
process.exit(1);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// ── Intel HEX parser ──────────────────────────────────────────────────────────
|
|||
|
|
function parseIntelHex(content) {
|
|||
|
|
// ATmega328P has 32 KB flash → 0x8000 bytes
|
|||
|
|
const flash = new Uint8Array(0x8000);
|
|||
|
|
|
|||
|
|
for (const rawLine of content.split('\n')) {
|
|||
|
|
const line = rawLine.trim();
|
|||
|
|
if (!line.startsWith(':') || line.length < 11) continue;
|
|||
|
|
|
|||
|
|
const bytes = Buffer.from(line.slice(1), 'hex');
|
|||
|
|
const byteCount = bytes[0];
|
|||
|
|
const addr = (bytes[1] << 8) | bytes[2];
|
|||
|
|
const recordType = bytes[3];
|
|||
|
|
|
|||
|
|
if (recordType === 0x00) { // Data record
|
|||
|
|
for (let i = 0; i < byteCount; i++) {
|
|||
|
|
if (addr + i < flash.length) {
|
|||
|
|
flash[addr + i] = bytes[4 + i];
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
// recordType 0x01 = EOF — nothing to do
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// AVR instructions are 16-bit little-endian words
|
|||
|
|
return new Uint16Array(flash.buffer);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// ── Build the CPU ─────────────────────────────────────────────────────────────
|
|||
|
|
const CLOCK_HZ = 16_000_000;
|
|||
|
|
|
|||
|
|
const hexContent = fs.readFileSync(hexFile, 'utf8');
|
|||
|
|
const program = parseIntelHex(hexContent);
|
|||
|
|
const cpu = new CPU(program);
|
|||
|
|
|
|||
|
|
// Timers are needed for delay() / millis() inside the Arduino sketch
|
|||
|
|
const timers = [
|
|||
|
|
new AVRTimer(cpu, timer0Config),
|
|||
|
|
new AVRTimer(cpu, timer1Config),
|
|||
|
|
new AVRTimer(cpu, timer2Config),
|
|||
|
|
];
|
|||
|
|
|
|||
|
|
const usart = new AVRUSART(cpu, usart0Config, CLOCK_HZ);
|
|||
|
|
|
|||
|
|
// ── TCP bridge state ───────────────────────────────────────────────────────────
|
|||
|
|
let socket = null;
|
|||
|
|
let txBacklog = []; // bytes queued before TCP connects
|
|||
|
|
|
|||
|
|
// Arduino → Pi: forward transmitted bytes
|
|||
|
|
usart.onByteTransmit = (byte) => {
|
|||
|
|
const ch = String.fromCharCode(byte);
|
|||
|
|
process.stdout.write(`[AVR->Pi] ${ch === '\n' ? '\\n\n' : ch}`);
|
|||
|
|
|
|||
|
|
if (socket && !socket.destroyed) {
|
|||
|
|
socket.write(Buffer.from([byte]));
|
|||
|
|
} else {
|
|||
|
|
txBacklog.push(byte); // buffer until connected
|
|||
|
|
}
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
// ── Simulation loop ───────────────────────────────────────────────────────────
|
|||
|
|
// Run ~160 000 instructions per Node.js event-loop tick ≈ 10 ms simulated time.
|
|||
|
|
// setImmediate() yields after each batch so I/O callbacks can fire.
|
|||
|
|
const BATCH = 160_000;
|
|||
|
|
|
|||
|
|
function runBatch() {
|
|||
|
|
for (let i = 0; i < BATCH; i++) {
|
|||
|
|
avrInstruction(cpu);
|
|||
|
|
cpu.tick();
|
|||
|
|
}
|
|||
|
|
setImmediate(runBatch);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// ── TCP connection to broker ──────────────────────────────────────────────────
|
|||
|
|
let retryCount = 0;
|
|||
|
|
const MAX_RETRIES = 40; // 40 × 500 ms = 20 s
|
|||
|
|
|
|||
|
|
function connectToBroker() {
|
|||
|
|
if (retryCount >= MAX_RETRIES) {
|
|||
|
|
process.stderr.write('[avr_runner] ERROR: could not connect to broker after max retries\n');
|
|||
|
|
process.exit(1);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
const s = new net.Socket();
|
|||
|
|
|
|||
|
|
s.connect(parseInt(brokerPort, 10), brokerHost, () => {
|
|||
|
|
retryCount = 0;
|
|||
|
|
socket = s;
|
|||
|
|
process.stdout.write(`[avr_runner] Connected to broker ${brokerHost}:${brokerPort}\n`);
|
|||
|
|
|
|||
|
|
// Flush bytes queued before connection
|
|||
|
|
if (txBacklog.length > 0) {
|
|||
|
|
s.write(Buffer.from(txBacklog));
|
|||
|
|
txBacklog = [];
|
|||
|
|
}
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
// Pi → Arduino: feed received bytes into USART RX
|
|||
|
|
s.on('data', (chunk) => {
|
|||
|
|
for (const byte of chunk) {
|
|||
|
|
const ch = String.fromCharCode(byte);
|
|||
|
|
process.stdout.write(`[Pi->AVR] ${ch === '\n' ? '\\n\n' : ch}`);
|
|||
|
|
// writeByte(value, immediate=true) bypasses baud-rate timing
|
|||
|
|
usart.writeByte(byte, true);
|
|||
|
|
}
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
s.on('close', () => {
|
|||
|
|
process.stdout.write('[avr_runner] Broker connection closed\n');
|
|||
|
|
socket = null;
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
s.on('error', (err) => {
|
|||
|
|
if (err.code === 'ECONNREFUSED') {
|
|||
|
|
retryCount++;
|
|||
|
|
process.stdout.write(`[avr_runner] Broker not ready, retry ${retryCount}/${MAX_RETRIES} ...\n`);
|
|||
|
|
setTimeout(connectToBroker, 500);
|
|||
|
|
} else {
|
|||
|
|
process.stderr.write(`[avr_runner] Socket error: ${err.message}\n`);
|
|||
|
|
}
|
|||
|
|
});
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// ── Start ─────────────────────────────────────────────────────────────────────
|
|||
|
|
process.stdout.write(`[avr_runner] Loaded: ${path.basename(hexFile)}\n`);
|
|||
|
|
process.stdout.write(`[avr_runner] ATmega328P @ ${CLOCK_HZ / 1e6} MHz — simulation starting\n`);
|
|||
|
|
|
|||
|
|
connectToBroker();
|
|||
|
|
runBatch();
|