From b9dfd552a62a46449532d49adc0773589076c808 Mon Sep 17 00:00:00 2001 From: Uri Shaked Date: Thu, 21 Nov 2019 19:40:02 +0200 Subject: feat: add blink demo --- demo/.gitignore | 2 + demo/src/compile.ts | 20 +++++++++ demo/src/execute.ts | 38 ++++++++++++++++++ demo/src/format-time.ts | 14 +++++++ demo/src/index.css | 48 ++++++++++++++++++++++ demo/src/index.html | 28 +++++++++++++ demo/src/index.ts | 105 ++++++++++++++++++++++++++++++++++++++++++++++++ demo/src/intelhex.ts | 18 +++++++++ demo/src/led.ts | 102 ++++++++++++++++++++++++++++++++++++++++++++++ demo/tsconfig.json | 12 ++++++ 10 files changed, 387 insertions(+) create mode 100644 demo/.gitignore create mode 100644 demo/src/compile.ts create mode 100644 demo/src/execute.ts create mode 100644 demo/src/format-time.ts create mode 100644 demo/src/index.css create mode 100644 demo/src/index.html create mode 100644 demo/src/index.ts create mode 100644 demo/src/intelhex.ts create mode 100644 demo/src/led.ts create mode 100644 demo/tsconfig.json (limited to 'demo') diff --git a/demo/.gitignore b/demo/.gitignore new file mode 100644 index 0000000..1b823a2 --- /dev/null +++ b/demo/.gitignore @@ -0,0 +1,2 @@ +dist +build diff --git a/demo/src/compile.ts b/demo/src/compile.ts new file mode 100644 index 0000000..4fca6c6 --- /dev/null +++ b/demo/src/compile.ts @@ -0,0 +1,20 @@ +const url = 'https://wokwi-hexi-73miufol2q-uc.a.run.app'; + +export interface IHexiResult { + stdout: string; + stderr: string; + hex: string; +} + +export async function buildHex(source: string) { + const resp = await fetch(url + '/build', { + method: 'POST', + mode: 'cors', + cache: 'no-cache', + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify({ sketch: source }) + }); + return (await resp.json()) as IHexiResult; +} diff --git a/demo/src/execute.ts b/demo/src/execute.ts new file mode 100644 index 0000000..96a0411 --- /dev/null +++ b/demo/src/execute.ts @@ -0,0 +1,38 @@ +import { avrInstruction, AVRTimer, CPU, timer0Config } from 'avr8js'; +import { loadHex } from './intelhex'; + +// ATmega328p params +const FLASH = 0x8000; + +export class AVRRunner { + readonly program = new Uint16Array(FLASH); + readonly cpu: CPU; + readonly timer: AVRTimer; + + private stopped = false; + + constructor(hex: string) { + loadHex(hex, new Uint8Array(this.program.buffer)); + this.cpu = new CPU(this.program); + this.timer = new AVRTimer(this.cpu, timer0Config); + } + + async execute(callback: (cpu: CPU) => void) { + this.stopped = false; + for (;;) { + avrInstruction(this.cpu); + this.timer.tick(); + if (this.cpu.cycles % 50000 === 0) { + callback(this.cpu); + await new Promise((resolve) => setTimeout(resolve, 0)); + if (this.stopped) { + break; + } + } + } + } + + stop() { + this.stopped = true; + } +} diff --git a/demo/src/format-time.ts b/demo/src/format-time.ts new file mode 100644 index 0000000..a82b3b0 --- /dev/null +++ b/demo/src/format-time.ts @@ -0,0 +1,14 @@ +function zeroPad(value: number, length: number) { + let sval = value.toString(); + while (sval.length < length) { + sval = '0' + sval; + } + return sval; +} + +export function formatTime(seconds: number) { + const ms = Math.floor(seconds * 1000) % 1000; + const secs = Math.floor(seconds % 60); + const mins = Math.floor(seconds / 60); + return `${zeroPad(mins, 2)}:${zeroPad(secs, 2)}.${zeroPad(ms, 3)}`; +} diff --git a/demo/src/index.css b/demo/src/index.css new file mode 100644 index 0000000..a3fa8b8 --- /dev/null +++ b/demo/src/index.css @@ -0,0 +1,48 @@ +body { + padding: 0 16px; + font-family: 'Roboto', sans-serif; + width: 100%; + box-sizing: border-box; +} + +.app-container { + width: 500px; + max-width: 100%; +} + +.toolbar { + padding: 4px; + display: flex; + background-color: #ddd; + box-sizing: border-box; + width: 100%; +} + +.toolbar > button { + margin-right: 4px; +} + +.spacer { + flex: 1; +} + +.code-editor { + width: 100%; + max-width: 100%; + height: 300px; + box-sizing: border-box; + border: 1px solid grey; +} + +.compiler-output { + width: 500px; + box-sizing: border-box; + padding: 8px 12px; + max-height: 120px; + overflow: auto; +} + +.compiler-output pre { + margin: 0; + white-space: pre-line; +} diff --git a/demo/src/index.html b/demo/src/index.html new file mode 100644 index 0000000..935bcff --- /dev/null +++ b/demo/src/index.html @@ -0,0 +1,28 @@ + + + + + + + AVR8js LED Demo + + + +

AVR8js LED Demo

+
+
+
+ + +
+
+
+
+
+

+      
+
+ + + + diff --git a/demo/src/index.ts b/demo/src/index.ts new file mode 100644 index 0000000..22e8143 --- /dev/null +++ b/demo/src/index.ts @@ -0,0 +1,105 @@ +import { buildHex } from './compile'; +import './index.css'; +import { AVRRunner } from './execute'; +import { formatTime } from './format-time'; +import { LED } from './led'; + +let editor: any; +const BLINK_CODE = ` +// Green LED connected to LED_BUILTIN, +// Red LED connected to pin 12. Enjoy! + +void setup() { + pinMode(LED_BUILTIN, OUTPUT); +} + +void loop() { + digitalWrite(LED_BUILTIN, HIGH); + delay(500); + digitalWrite(LED_BUILTIN, LOW); + delay(500); +}`.trim(); + +// Load Editor +declare var window: any; +declare var monaco: any; +window.require.config({ + paths: { vs: 'https://cdnjs.cloudflare.com/ajax/libs/monaco-editor/0.18.0/min/vs' } +}); +window.require(['vs/editor/editor.main'], () => { + editor = monaco.editor.create(document.querySelector('.code-editor'), { + value: BLINK_CODE, + language: 'cpp', + minimap: { enabled: false } + }); +}); + +// Set up LEDs +const leds = document.querySelector('.leds'); +const led13 = new LED({ color: 'green', lightColor: '#80ff80' }); +const led12 = new LED({ color: 'red', lightColor: '#ff8080' }); +leds.appendChild(led13.el); +leds.appendChild(led12.el); + +// Set up toolbar +let runner: AVRRunner; + +const runButton = document.querySelector('#run-button'); +runButton.addEventListener('click', compileAndRun); +const stopButton = document.querySelector('#stop-button'); +stopButton.addEventListener('click', stopCode); +const statusLabel = document.querySelector('#status-label'); +const compilerOutputText = document.querySelector('#compiler-output-text'); + +function executeProgram(hex: string) { + runner = new AVRRunner(hex); + const MHZ = 16000000; + + // Hook to PORTB output + runner.cpu.writeHooks[0x25] = (value: number) => { + const DDRB = runner.cpu.data[0x24]; + value &= DDRB; + const D12bit = 1 << 4; + const D13bit = 1 << 5; + led12.value = value & D12bit ? true : false; + led13.value = value & D13bit ? true : false; + }; + + runner.execute((cpu) => { + const time = formatTime(cpu.cycles / MHZ); + statusLabel.textContent = 'Simulation time: ' + time; + }); +} + +async function compileAndRun() { + led12.value = false; + led13.value = false; + + runButton.setAttribute('disabled', '1'); + try { + statusLabel.textContent = 'Compiling...'; + const result = await buildHex(editor.getModel().getValue()); + compilerOutputText.textContent = result.stderr || result.stdout; + if (result.hex) { + compilerOutputText.textContent += '\nProgram running...'; + stopButton.removeAttribute('disabled'); + executeProgram(result.hex); + } else { + runButton.removeAttribute('disabled'); + } + } catch (err) { + runButton.removeAttribute('disabled'); + alert('Failed: ' + err); + } finally { + statusLabel.textContent = ''; + } +} + +function stopCode() { + stopButton.setAttribute('disabled', '1'); + runButton.removeAttribute('disabled'); + if (runner) { + runner.stop(); + runner = null; + } +} diff --git a/demo/src/intelhex.ts b/demo/src/intelhex.ts new file mode 100644 index 0000000..ba5d6c8 --- /dev/null +++ b/demo/src/intelhex.ts @@ -0,0 +1,18 @@ +/** + * Minimal Intel HEX loader + * Part of AVR8js + * + * Copyright (C) 2019, Uri Shaked + */ + +export function loadHex(source: string, target: Uint8Array) { + for (const line of source.split('\n')) { + if (line[0] === ':' && line.substr(7, 2) === '00') { + const bytes = parseInt(line.substr(1, 2), 16); + const addr = parseInt(line.substr(3, 4), 16); + for (let i = 0; i < bytes; i++) { + target[addr + i] = parseInt(line.substr(9 + i * 2, 2), 16); + } + } + } +} diff --git a/demo/src/led.ts b/demo/src/led.ts new file mode 100644 index 0000000..8c6deb4 --- /dev/null +++ b/demo/src/led.ts @@ -0,0 +1,102 @@ +const led = ` + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +`; + +export interface ILEDOptions { + color: string; + lightColor?: string; +} + +const ON_CLASS = 'led-on'; +export class LED { + readonly el = document.createElement('span'); + private readonly lightEl: SVGElement; + constructor({ color, lightColor }: ILEDOptions) { + this.el.innerHTML = led + .replace('{{color}}', color) + .replace('{{lightColor}}', lightColor || color); + this.lightEl = this.el.querySelector('.light'); + this.lightEl.style.display = 'none'; + } + + get value() { + return this.lightEl.style.display !== 'none'; + } + + set value(value: boolean) { + this.lightEl.style.display = value ? '' : 'none'; + } +} diff --git a/demo/tsconfig.json b/demo/tsconfig.json new file mode 100644 index 0000000..2c2940e --- /dev/null +++ b/demo/tsconfig.json @@ -0,0 +1,12 @@ +{ + "extends": "../tsconfig.json", + "compilerOptions": { + "baseUrl": "..", + "rootDir": "..", + "lib": ["dom"], + "paths": { + "avr8js": ["src"] + } + }, + "include": ["fuse.ts", "src/**/*.ts"] +} -- cgit v1.2.3