├── .husky ├── .gitignore └── pre-commit ├── demo ├── .gitignore ├── README.md ├── vite.config.js ├── tsconfig.json ├── src │ ├── format-time.ts │ ├── compile.ts │ ├── intelhex.ts │ ├── utils │ │ └── editor-history.util.ts │ ├── index.css │ ├── task-scheduler.ts │ ├── cpu-performance.ts │ ├── execute.ts │ └── index.ts └── index.html ├── benchmark ├── .gitignore ├── permutations.ts ├── index.ts ├── convert-instructions.ts └── benchmark.ts ├── .editorconfig ├── .gitignore ├── prettier.config.js ├── tsconfig.spec.json ├── src ├── types.ts ├── cpu │ ├── interrupt.ts │ ├── interrupt.spec.ts │ ├── cpu.spec.ts │ └── cpu.ts ├── utils │ ├── test-utils.ts │ └── assembler.spec.ts ├── index.ts └── peripherals │ ├── clock.ts │ ├── clock.spec.ts │ ├── adc.spec.ts │ ├── usi.ts │ ├── watchdog.ts │ ├── eeprom.ts │ ├── spi.ts │ ├── twi.ts │ ├── watchdog.spec.ts │ ├── adc.ts │ ├── spi.spec.ts │ ├── usart.ts │ ├── eeprom.spec.ts │ ├── gpio.spec.ts │ ├── usart.spec.ts │ ├── twi.spec.ts │ └── gpio.ts ├── vitest.config.ts ├── .gitpod.yml ├── .vscode ├── settings.json └── extensions.json ├── tsconfig.json ├── .eslintrc.json ├── .github └── workflows │ └── ci.yml ├── LICENSE ├── package.json ├── CONTRIBUTING.md └── README.md /.husky/.gitignore: -------------------------------------------------------------------------------- 1 | _ 2 | -------------------------------------------------------------------------------- /demo/.gitignore: -------------------------------------------------------------------------------- 1 | dist 2 | build 3 | -------------------------------------------------------------------------------- /benchmark/.gitignore: -------------------------------------------------------------------------------- 1 | instruction-fn.ts 2 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | indent_style = space 5 | indent_size = 2 -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .cache 2 | .history 3 | .idea 4 | node_modules 5 | dist 6 | *.log 7 | -------------------------------------------------------------------------------- /.husky/pre-commit: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | . "$(dirname "$0")/_/husky.sh" 3 | 4 | npx lint-staged 5 | -------------------------------------------------------------------------------- /prettier.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | arrowParens: 'always', 3 | printWidth: 100, 4 | singleQuote: true, 5 | tabWidth: 2, 6 | endOfLine: 'auto', 7 | }; 8 | -------------------------------------------------------------------------------- /tsconfig.spec.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "esModuleInterop": true, 4 | "noEmitOnError": false 5 | }, 6 | "extends": "./tsconfig.json", 7 | "exclude": [] 8 | } 9 | -------------------------------------------------------------------------------- /src/types.ts: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | // Copyright (c) Uri Shaked and contributors 3 | 4 | export type u8 = number; 5 | export type u16 = number; 6 | export type i16 = number; 7 | export type u32 = number; 8 | -------------------------------------------------------------------------------- /vitest.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'vitest/config'; 2 | 3 | export default defineConfig({ 4 | test: { 5 | globals: true, 6 | environment: 'node', 7 | include: ['src/**/*.spec.ts'], 8 | }, 9 | }); 10 | -------------------------------------------------------------------------------- /.gitpod.yml: -------------------------------------------------------------------------------- 1 | tasks: 2 | - init: npm install 3 | command: npm run start 4 | 5 | vscode: 6 | extensions: 7 | - esbenp.prettier-vscode@5.1.3:t532ajsImUSrA9N8Bd7jQw== 8 | - dbaeumer.vscode-eslint@2.1.3:1NRvj3UKNTNwmYjptmUmIw== 9 | 10 | ports: 11 | - port: 1234 12 | onOpen: open-preview 13 | -------------------------------------------------------------------------------- /demo/README.md: -------------------------------------------------------------------------------- 1 | # AVR8js Demo 2 | 3 | AVR8js + Monaco Editor. To run this demo: 4 | 5 | 1. Install dependencies: `npm install` 6 | 2. Start the code: `npm start` 7 | 3. Go to http://localhost:3000/ and start tinkering 8 | 9 | The demo is configured to automatically reload the app whenever you change the code. 10 | -------------------------------------------------------------------------------- /demo/vite.config.js: -------------------------------------------------------------------------------- 1 | import { join } from 'path'; 2 | 3 | /** 4 | * @type {import('vite').UserConfig} 5 | */ 6 | const config = { 7 | base: '', 8 | resolve: { 9 | alias: { avr8js: join(__dirname, '../src') }, 10 | }, 11 | server: { 12 | open: true, 13 | }, 14 | }; 15 | 16 | export default config; 17 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | // Place your settings in this file to overwrite default and user settings. 2 | { 3 | "eslint.validate": [ 4 | "javascript", 5 | "javascriptreact", 6 | { "language": "typescript", "autoFix": true }, 7 | { "language": "typescriptreact", "autoFix": true } 8 | ], 9 | "tslint.enable": false 10 | } 11 | -------------------------------------------------------------------------------- /demo/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig.json", 3 | "compilerOptions": { 4 | "strictNullChecks": false, 5 | "target": "es2018", 6 | "baseUrl": "..", 7 | "rootDir": "..", 8 | "lib": ["dom"], 9 | "paths": { 10 | "avr8js": ["src"] 11 | } 12 | }, 13 | "include": ["fuse.ts", "src/**/*.ts"] 14 | } 15 | -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | // See https://go.microsoft.com/fwlink/?LinkId=827846 to learn about workspace recommendations. 3 | 4 | "recommendations": [ 5 | "editorconfig.editorconfig", 6 | "esbenp.prettier-vscode", 7 | "dbaeumer.vscode-eslint" 8 | ], 9 | 10 | // List of extensions recommended by VS Code that should not be recommended for users of this workspace. 11 | "unwantedRecommendations": [] 12 | } 13 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2015", 4 | "module": "ES2015", 5 | "moduleResolution": "node", 6 | "noImplicitAny": true, 7 | "strictNullChecks": true, 8 | "removeComments": false, 9 | "sourceMap": true, 10 | "declaration": true, 11 | "noEmitOnError": true, 12 | "rootDir": "src", 13 | "outDir": "dist/esm", 14 | "lib": ["es2015"] 15 | }, 16 | "include": ["src/**/*.ts"], 17 | "exclude": ["src/**/*.spec.ts"] 18 | } 19 | -------------------------------------------------------------------------------- /demo/src/format-time.ts: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | // Copyright (c) Uri Shaked and contributors 3 | 4 | function zeroPad(value: number, length: number) { 5 | let sval = value.toString(); 6 | while (sval.length < length) { 7 | sval = '0' + sval; 8 | } 9 | return sval; 10 | } 11 | 12 | export function formatTime(seconds: number) { 13 | const ms = Math.floor(seconds * 1000) % 1000; 14 | const secs = Math.floor(seconds % 60); 15 | const mins = Math.floor(seconds / 60); 16 | return `${zeroPad(mins, 2)}:${zeroPad(secs, 2)}.${zeroPad(ms, 3)}`; 17 | } 18 | -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "parser": "@typescript-eslint/parser", 3 | "parserOptions": { 4 | "ecmaVersion": 2018, 5 | "sourceType": "module" 6 | }, 7 | "plugins": ["@typescript-eslint", "prettier", "json"], 8 | "env": { 9 | "browser": true, 10 | "es6": true, 11 | "node": true 12 | }, 13 | "extends": [ 14 | "eslint:recommended", 15 | "plugin:@typescript-eslint/recommended", 16 | "plugin:prettier/recommended" 17 | ], 18 | "rules": { 19 | "prettier/prettier": "error", 20 | "@typescript-eslint/explicit-function-return-type": 0 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /demo/src/compile.ts: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | // Copyright (c) Uri Shaked and contributors 3 | 4 | const url = 'https://hexi.wokwi.com'; 5 | 6 | export interface HexiResult { 7 | stdout: string; 8 | stderr: string; 9 | hex: string; 10 | } 11 | 12 | export async function buildHex(source: string) { 13 | const resp = await fetch(url + '/build', { 14 | method: 'POST', 15 | mode: 'cors', 16 | cache: 'no-cache', 17 | headers: { 18 | 'Content-Type': 'application/json', 19 | }, 20 | body: JSON.stringify({ sketch: source }), 21 | }); 22 | return (await resp.json()) as HexiResult; 23 | } 24 | -------------------------------------------------------------------------------- /benchmark/permutations.ts: -------------------------------------------------------------------------------- 1 | export function* permutations(pattern: string) { 2 | let totalPerms = 1; 3 | for (const char of pattern) { 4 | if (char !== '0' && char !== '1') { 5 | totalPerms *= 2; 6 | } 7 | } 8 | 9 | for (let permIndex = 0; permIndex < totalPerms; permIndex++) { 10 | let varIndex = 0; 11 | let value = 0; 12 | for (const char of pattern) { 13 | value *= 2; 14 | if (char === '1') { 15 | value++; 16 | } else if (char !== '0') { 17 | value += permIndex & (1 << varIndex) ? 1 : 0; 18 | varIndex++; 19 | } 20 | } 21 | yield value; 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /demo/src/intelhex.ts: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | // Copyright (c) Uri Shaked and contributors 3 | 4 | /** 5 | * Minimal Intel HEX loader 6 | * Part of AVR8js 7 | * 8 | * Copyright (C) 2019, Uri Shaked 9 | */ 10 | 11 | export function loadHex(source: string, target: Uint8Array) { 12 | for (const line of source.split('\n')) { 13 | if (line[0] === ':' && line.substr(7, 2) === '00') { 14 | const bytes = parseInt(line.substr(1, 2), 16); 15 | const addr = parseInt(line.substr(3, 4), 16); 16 | for (let i = 0; i < bytes; i++) { 17 | target[addr + i] = parseInt(line.substr(9 + i * 2, 2), 16); 18 | } 19 | } 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: ci 2 | 3 | on: 4 | - push 5 | - pull_request 6 | 7 | jobs: 8 | build: 9 | runs-on: ubuntu-24.04 10 | strategy: 11 | matrix: 12 | node: ['24', '22', '20', '18'] 13 | name: Node ${{ matrix.node }} CI 14 | steps: 15 | - uses: actions/checkout@v4 16 | 17 | - name: Setup node 18 | uses: actions/setup-node@v4 19 | with: 20 | node-version: ${{ matrix.node }} 21 | 22 | - name: npm install, build, lint, and test 23 | run: | 24 | npm ci 25 | npm run build 26 | npm run lint 27 | npm test 28 | env: 29 | CI: true 30 | -------------------------------------------------------------------------------- /demo/src/utils/editor-history.util.ts: -------------------------------------------------------------------------------- 1 | const AVRJS8_EDITOR_HISTORY = 'AVRJS8_EDITOR_HISTORY'; 2 | 3 | export class EditorHistoryUtil { 4 | static hasLocalStorage = !!window.localStorage; 5 | 6 | static storeSnippet(codeSnippet: string) { 7 | if (!EditorHistoryUtil.hasLocalStorage) { 8 | return; 9 | } 10 | window.localStorage.setItem(AVRJS8_EDITOR_HISTORY, codeSnippet); 11 | } 12 | 13 | static clearSnippet() { 14 | if (!EditorHistoryUtil.hasLocalStorage) { 15 | return; 16 | } 17 | localStorage.removeItem(AVRJS8_EDITOR_HISTORY); 18 | } 19 | 20 | static getValue() { 21 | if (!EditorHistoryUtil.hasLocalStorage) { 22 | return; 23 | } 24 | return localStorage.getItem(AVRJS8_EDITOR_HISTORY); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/cpu/interrupt.ts: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | // Copyright (c) Uri Shaked and contributors 3 | 4 | /** 5 | * AVR-8 Interrupt Handling 6 | * Part of AVR8js 7 | * Reference: http://ww1.microchip.com/downloads/en/devicedoc/atmel-0856-avr-instruction-set-manual.pdf 8 | * 9 | * Copyright (C) 2019, Uri Shaked 10 | */ 11 | 12 | import { CPU } from './cpu'; 13 | 14 | export function avrInterrupt(cpu: CPU, addr: number) { 15 | const sp = cpu.dataView.getUint16(93, true); 16 | cpu.data[sp] = cpu.pc & 0xff; 17 | cpu.data[sp - 1] = (cpu.pc >> 8) & 0xff; 18 | if (cpu.pc22Bits) { 19 | cpu.data[sp - 2] = (cpu.pc >> 16) & 0xff; 20 | } 21 | cpu.dataView.setUint16(93, sp - (cpu.pc22Bits ? 3 : 2), true); 22 | cpu.data[95] &= 0x7f; // clear global interrupt flag 23 | cpu.cycles += 2; 24 | cpu.pc = addr; 25 | } 26 | -------------------------------------------------------------------------------- /demo/src/index.css: -------------------------------------------------------------------------------- 1 | body { 2 | padding: 0 16px; 3 | font-family: 'Roboto', sans-serif; 4 | width: 100%; 5 | box-sizing: border-box; 6 | } 7 | 8 | .app-container { 9 | width: 700px; 10 | max-width: 100%; 11 | } 12 | 13 | .toolbar { 14 | padding: 4px; 15 | display: flex; 16 | background-color: #ddd; 17 | box-sizing: border-box; 18 | width: 100%; 19 | } 20 | 21 | .toolbar > button { 22 | margin-right: 4px; 23 | } 24 | 25 | .spacer { 26 | flex: 1; 27 | } 28 | 29 | .code-editor { 30 | width: 100%; 31 | max-width: 100%; 32 | height: 300px; 33 | box-sizing: border-box; 34 | border: 1px solid grey; 35 | } 36 | 37 | .compiler-output { 38 | width: 100%; 39 | box-sizing: border-box; 40 | padding: 8px 12px; 41 | max-height: 160px; 42 | overflow: auto; 43 | } 44 | 45 | .compiler-output pre { 46 | margin: 0; 47 | white-space: pre-line; 48 | } 49 | 50 | #serial-output-text { 51 | color: blue; 52 | } 53 | -------------------------------------------------------------------------------- /demo/src/task-scheduler.ts: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | // Copyright (c) Uri Shaked and contributors 3 | 4 | export type IMicroTaskCallback = () => void; 5 | 6 | export class MicroTaskScheduler { 7 | private readonly channel = new MessageChannel(); 8 | private executionQueue: Array = []; 9 | private stopped = true; 10 | 11 | start() { 12 | if (this.stopped) { 13 | this.stopped = false; 14 | this.channel.port2.onmessage = this.handleMessage; 15 | } 16 | } 17 | 18 | stop() { 19 | this.stopped = true; 20 | this.executionQueue.splice(0, this.executionQueue.length); 21 | this.channel.port2.onmessage = null; 22 | } 23 | 24 | postTask(fn: IMicroTaskCallback) { 25 | if (!this.stopped) { 26 | this.executionQueue.push(fn); 27 | this.channel.port1.postMessage(null); 28 | } 29 | } 30 | 31 | private handleMessage = () => { 32 | const executeJob = this.executionQueue.shift(); 33 | if (executeJob !== undefined) { 34 | executeJob(); 35 | } 36 | }; 37 | } 38 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2019-2025 Uri Shaked 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /demo/src/cpu-performance.ts: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | // Copyright (c) Uri Shaked and contributors 3 | 4 | import { CPU } from 'avr8js'; 5 | 6 | export class CPUPerformance { 7 | private prevTime = 0; 8 | private prevCycles = 0; 9 | private samples = new Float32Array(64); 10 | private sampleIndex = 0; 11 | 12 | constructor( 13 | private cpu: CPU, 14 | private MHZ: number, 15 | ) {} 16 | 17 | reset() { 18 | this.prevTime = 0; 19 | this.prevCycles = 0; 20 | this.sampleIndex = 0; 21 | } 22 | 23 | update() { 24 | if (this.prevTime) { 25 | const delta = performance.now() - this.prevTime; 26 | const deltaCycles = this.cpu.cycles - this.prevCycles; 27 | const deltaCpuMillis = 1000 * (deltaCycles / this.MHZ); 28 | const factor = deltaCpuMillis / delta; 29 | if (!this.sampleIndex) { 30 | this.samples.fill(factor); 31 | } 32 | this.samples[this.sampleIndex++ % this.samples.length] = factor; 33 | } 34 | this.prevCycles = this.cpu.cycles; 35 | this.prevTime = performance.now(); 36 | const avg = this.samples.reduce((x, y) => x + y) / this.samples.length; 37 | return avg; 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /demo/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | AVR8js LED Demo 8 | 9 | 10 | 11 |

AVR8js LED Demo

12 |
13 |
14 | 15 | 16 |
17 |
18 | 19 | 20 | 21 |
22 |
23 |
24 |
25 |
26 |

27 |         

28 |       
29 |
30 | 31 | 32 | 33 | 34 | -------------------------------------------------------------------------------- /src/cpu/interrupt.spec.ts: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | // Copyright (c) Uri Shaked and contributors 3 | 4 | import { describe, expect, it } from 'vitest'; 5 | import { CPU } from './cpu'; 6 | import { avrInterrupt } from './interrupt'; 7 | 8 | describe('avrInterrupt', () => { 9 | it('should execute interrupt handler', () => { 10 | const cpu = new CPU(new Uint16Array(0x8000)); 11 | cpu.pc = 0x520; 12 | cpu.data[94] = 0; 13 | cpu.data[93] = 0x80; // SP <- 0x80 14 | cpu.data[95] = 0b10000001; // SREG <- I------C 15 | avrInterrupt(cpu, 5); 16 | expect(cpu.cycles).toEqual(2); 17 | expect(cpu.pc).toEqual(5); 18 | expect(cpu.data[93]).toEqual(0x7e); // SP 19 | expect(cpu.data[0x80]).toEqual(0x20); // Return addr low 20 | expect(cpu.data[0x7f]).toEqual(0x5); // Return addr high 21 | expect(cpu.data[95]).toEqual(0b00000001); // SREG: -------C 22 | }); 23 | 24 | it('should push a 3-byte return address when running in 22-bit PC mode (issue #58)', () => { 25 | const cpu = new CPU(new Uint16Array(0x80000)); 26 | expect(cpu.pc22Bits).toEqual(true); 27 | 28 | cpu.pc = 0x10520; 29 | cpu.data[94] = 0; 30 | cpu.data[93] = 0x80; // SP <- 0x80 31 | cpu.data[95] = 0b10000001; // SREG <- I------C 32 | 33 | avrInterrupt(cpu, 5); 34 | expect(cpu.cycles).toEqual(2); 35 | expect(cpu.pc).toEqual(5); 36 | expect(cpu.data[93]).toEqual(0x7d); // SP should decrement by 3 37 | expect(cpu.data[0x80]).toEqual(0x20); // Return addr low 38 | expect(cpu.data[0x7f]).toEqual(0x05); // Return addr high 39 | expect(cpu.data[0x7e]).toEqual(0x1); // Return addr extended 40 | expect(cpu.data[95]).toEqual(0b00000001); // SREG: -------C 41 | }); 42 | }); 43 | -------------------------------------------------------------------------------- /src/utils/test-utils.ts: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | // Copyright (c) Uri Shaked and contributors 3 | 4 | import { CPU } from '../cpu/cpu'; 5 | import { assemble } from './assembler'; 6 | import { avrInstruction } from '../cpu/instruction'; 7 | 8 | const BREAK_OPCODE = 0x9598; 9 | 10 | export function asmProgram(source: string) { 11 | const { bytes, errors, lines, labels } = assemble(source); 12 | if (errors.length) { 13 | throw new Error('Assembly failed: ' + errors); 14 | } 15 | return { program: new Uint16Array(bytes.buffer), lines, instructionCount: lines.length, labels }; 16 | } 17 | 18 | const defaultOnBreak = () => { 19 | throw new Error('BREAK instruction encountered'); 20 | }; 21 | 22 | export class TestProgramRunner { 23 | constructor( 24 | private readonly cpu: CPU, 25 | private readonly onBreak: (cpu: CPU) => void = defaultOnBreak, 26 | ) {} 27 | 28 | runInstructions(count: number) { 29 | const { cpu, onBreak } = this; 30 | for (let i = 0; i < count; i++) { 31 | if (cpu.progMem[cpu.pc] === BREAK_OPCODE) { 32 | onBreak?.(cpu); 33 | } 34 | avrInstruction(cpu); 35 | cpu.tick(); 36 | } 37 | } 38 | 39 | runUntil(predicate: (cpu: CPU) => boolean, maxIterations = 5000) { 40 | const { cpu, onBreak } = this; 41 | for (let i = 0; i < maxIterations; i++) { 42 | if (cpu.progMem[cpu.pc] === BREAK_OPCODE) { 43 | onBreak?.(cpu); 44 | } 45 | if (predicate(cpu)) { 46 | return; 47 | } 48 | avrInstruction(cpu); 49 | cpu.tick(); 50 | } 51 | throw new Error('Test program ran for too long, check your predicate'); 52 | } 53 | 54 | runToBreak() { 55 | this.runUntil((cpu) => cpu.progMem[cpu.pc] === BREAK_OPCODE); 56 | } 57 | 58 | runToAddress(byteAddr: number) { 59 | this.runUntil((cpu) => cpu.pc * 2 === byteAddr); 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | // Copyright (c) Uri Shaked and contributors 3 | 4 | export { CPU } from './cpu/cpu'; 5 | export type { CPUMemoryHook, CPUMemoryHooks } from './cpu/cpu'; 6 | export { avrInstruction } from './cpu/instruction'; 7 | export { avrInterrupt } from './cpu/interrupt'; 8 | export { 9 | adcConfig, 10 | ADCMuxInputType, 11 | ADCReference, 12 | atmega328Channels, 13 | AVRADC, 14 | } from './peripherals/adc'; 15 | export type { ADCConfig, ADCMuxConfiguration, ADCMuxInput } from './peripherals/adc'; 16 | export { AVRClock, clockConfig } from './peripherals/clock'; 17 | export type { AVRClockConfig } from './peripherals/clock'; 18 | export { AVREEPROM, eepromConfig, EEPROMMemoryBackend } from './peripherals/eeprom'; 19 | export type { AVREEPROMConfig, EEPROMBackend } from './peripherals/eeprom'; 20 | export { 21 | AVRIOPort, 22 | INT0, 23 | INT1, 24 | PCINT0, 25 | PCINT1, 26 | PCINT2, 27 | PinState, 28 | portAConfig, 29 | portBConfig, 30 | portCConfig, 31 | portDConfig, 32 | portEConfig, 33 | portFConfig, 34 | portGConfig, 35 | portHConfig, 36 | portJConfig, 37 | portKConfig, 38 | portLConfig, 39 | } from './peripherals/gpio'; 40 | export type { 41 | AVRExternalInterrupt, 42 | AVRPinChangeInterrupt, 43 | AVRPortConfig, 44 | GPIOListener, 45 | } from './peripherals/gpio'; 46 | export { AVRSPI, spiConfig } from './peripherals/spi'; 47 | export type { SPIConfig, SPITransferCallback } from './peripherals/spi'; 48 | export { AVRTimer, timer0Config, timer1Config, timer2Config } from './peripherals/timer'; 49 | export type { AVRTimerConfig } from './peripherals/timer'; 50 | export * from './peripherals/twi'; 51 | export { AVRUSART, usart0Config } from './peripherals/usart'; 52 | export { AVRUSI } from './peripherals/usi'; 53 | export { AVRWatchdog, watchdogConfig } from './peripherals/watchdog'; 54 | export type { WatchdogConfig } from './peripherals/watchdog'; 55 | -------------------------------------------------------------------------------- /demo/src/execute.ts: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | // Copyright (c) Uri Shaked and contributors 3 | 4 | import { 5 | avrInstruction, 6 | AVRTimer, 7 | CPU, 8 | timer0Config, 9 | timer1Config, 10 | timer2Config, 11 | AVRIOPort, 12 | AVRUSART, 13 | portBConfig, 14 | portCConfig, 15 | portDConfig, 16 | usart0Config, 17 | } from 'avr8js'; 18 | import { loadHex } from './intelhex'; 19 | import { MicroTaskScheduler } from './task-scheduler'; 20 | 21 | // ATmega328p params 22 | const FLASH = 0x8000; 23 | 24 | export class AVRRunner { 25 | readonly program = new Uint16Array(FLASH); 26 | readonly cpu: CPU; 27 | readonly timer0: AVRTimer; 28 | readonly timer1: AVRTimer; 29 | readonly timer2: AVRTimer; 30 | readonly portB: AVRIOPort; 31 | readonly portC: AVRIOPort; 32 | readonly portD: AVRIOPort; 33 | readonly usart: AVRUSART; 34 | readonly speed = 16e6; // 16 MHZ 35 | readonly workUnitCycles = 500000; 36 | readonly taskScheduler = new MicroTaskScheduler(); 37 | 38 | constructor(hex: string) { 39 | loadHex(hex, new Uint8Array(this.program.buffer)); 40 | this.cpu = new CPU(this.program); 41 | this.timer0 = new AVRTimer(this.cpu, timer0Config); 42 | this.timer1 = new AVRTimer(this.cpu, timer1Config); 43 | this.timer2 = new AVRTimer(this.cpu, timer2Config); 44 | this.portB = new AVRIOPort(this.cpu, portBConfig); 45 | this.portC = new AVRIOPort(this.cpu, portCConfig); 46 | this.portD = new AVRIOPort(this.cpu, portDConfig); 47 | this.usart = new AVRUSART(this.cpu, usart0Config, this.speed); 48 | this.taskScheduler.start(); 49 | } 50 | 51 | // CPU main loop 52 | execute(callback: (cpu: CPU) => void) { 53 | const cyclesToRun = this.cpu.cycles + this.workUnitCycles; 54 | while (this.cpu.cycles < cyclesToRun) { 55 | avrInstruction(this.cpu); 56 | this.cpu.tick(); 57 | } 58 | 59 | callback(this.cpu); 60 | this.taskScheduler.postTask(() => this.execute(callback)); 61 | } 62 | 63 | stop() { 64 | this.taskScheduler.stop(); 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /benchmark/index.ts: -------------------------------------------------------------------------------- 1 | import { CPU } from '../src/cpu/cpu'; 2 | import { avrInstruction } from '../src/cpu/instruction'; 3 | import { createBenchmark } from './benchmark'; 4 | import { permutations } from './permutations'; 5 | import { instructions, executeInstruction } from './instruction-fn'; 6 | 7 | /* Approach 1: use large Uint16Array with all possible opcodes */ 8 | const instructionArray = new Uint16Array(65536); 9 | for (let i = 0; i < instructions.length; i++) { 10 | const { pattern } = instructions[i]; 11 | for (const opcode of permutations(pattern.replace(/ /g, '').substr(0, 16))) { 12 | if (!instructionArray[opcode]) { 13 | instructionArray[opcode] = i + 1; 14 | } 15 | } 16 | } 17 | 18 | function avrInstructionUintArray(cpu: CPU) { 19 | const opcode = cpu.progMem[cpu.pc]; 20 | executeInstruction(instructionArray[opcode], cpu, opcode); 21 | } 22 | 23 | /* Approach 2: use instMap */ 24 | const instructionMap: { [key: number]: (cpu: CPU, opcode: number) => void } = {}; 25 | for (const { pattern, fn } of instructions) { 26 | for (const opcode of permutations(pattern.replace(/ /g, '').substr(0, 16))) { 27 | if (!instructionMap[opcode]) { 28 | instructionMap[opcode] = fn; 29 | } 30 | } 31 | } 32 | 33 | function avrInstructionObjMap(cpu: CPU) { 34 | const opcode = cpu.progMem[cpu.pc]; 35 | instructionMap[opcode](cpu, opcode); 36 | } 37 | 38 | /* Run the benchmark */ 39 | function run() { 40 | const benchmark = createBenchmark('cpu-benchmark'); 41 | 42 | const cpu = new CPU(new Uint16Array(0x1000)); 43 | cpu.progMem[0] = 0x8088; 44 | const timeA = benchmark('avrInstruction'); 45 | while (timeA()) { 46 | cpu.pc = 0; 47 | avrInstruction(cpu); 48 | } 49 | 50 | const timeB = benchmark('avrInstructionObjMap'); 51 | while (timeB()) { 52 | cpu.pc = 0; 53 | avrInstructionObjMap(cpu); 54 | } 55 | 56 | const timeC = benchmark('avrInstructionUintArray'); 57 | while (timeC()) { 58 | cpu.pc = 0; 59 | avrInstructionUintArray(cpu); 60 | } 61 | 62 | benchmark.report(); 63 | } 64 | 65 | run(); 66 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "avr8js", 3 | "version": "0.20.1", 4 | "main": "dist/cjs/index.js", 5 | "module": "./dist/esm/index.js", 6 | "types": "./dist/esm/index.d.ts", 7 | "author": "Uri Shaked ", 8 | "repository": "https://github.com/wokwi/avr8js", 9 | "license": "MIT", 10 | "description": "Arduino (8-bit AVR) simulator, written in JavaScript and runs in the browser / Node.js", 11 | "keywords": [ 12 | "Arduino", 13 | "Arduino Uno", 14 | "AVR", 15 | "8 bit", 16 | "MCU", 17 | "simulation", 18 | "simulator", 19 | "ATmega", 20 | "ATmega328p", 21 | "microcontroller" 22 | ], 23 | "scripts": { 24 | "build": "rimraf dist && tsc --sourceMap false && tsc -m commonjs --outDir dist/cjs --sourceMap false", 25 | "build:demo": "vite build demo", 26 | "prepare": "husky install && npm run build", 27 | "start": "vite demo", 28 | "lint": "eslint src/**/*.ts demo/**/*.ts", 29 | "test": "npm run lint && vitest run", 30 | "test:watch": "vitest", 31 | "benchmark:prepare": "tsx benchmark/convert-instructions.ts", 32 | "benchmark": "tsx benchmark/index.ts" 33 | }, 34 | "files": [ 35 | "dist" 36 | ], 37 | "devDependencies": { 38 | "@types/node": "^18.0.0", 39 | "@typescript-eslint/eslint-plugin": "^5.48.0", 40 | "@typescript-eslint/parser": "^5.48.0", 41 | "@wokwi/elements": "^0.16.2", 42 | "eslint": "^8.31.0", 43 | "eslint-config-prettier": "^10.0.1", 44 | "eslint-plugin-json": "^3.1.0", 45 | "eslint-plugin-prettier": "^5.1.3", 46 | "husky": "^6.0.0", 47 | "lint-staged": "^9.5.0", 48 | "prettier": "^3.5.0", 49 | "rimraf": "^3.0.2", 50 | "tsx": "^4.19.2", 51 | "typescript": "^4.9.4", 52 | "vite": "^6.0.0", 53 | "vitest": "^3.2.4" 54 | }, 55 | "lint-staged": { 56 | "src/**/*.{ts,tsx}": [ 57 | "eslint --fix", 58 | "prettier --write", 59 | "git add" 60 | ] 61 | }, 62 | "engines": { 63 | "node": ">= 18.0.0", 64 | "npm": ">= 10.0.0" 65 | }, 66 | "alias": { 67 | "avr8js": "./src" 68 | }, 69 | "browserslist": [ 70 | "last 1 Chrome versions" 71 | ] 72 | } 73 | -------------------------------------------------------------------------------- /src/peripherals/clock.ts: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | // Copyright (c) Uri Shaked and contributors 3 | 4 | /** 5 | * AVR8 Clock 6 | * Part of AVR8js 7 | * Reference: http://ww1.microchip.com/downloads/en/DeviceDoc/ATmega48A-PA-88A-PA-168A-PA-328-P-DS-DS40002061A.pdf 8 | * 9 | * Copyright (C) 2020, Uri Shaked 10 | */ 11 | 12 | import { CPU } from '../cpu/cpu'; 13 | import { u32, u8 } from '../types'; 14 | 15 | const CLKPCE = 128; 16 | 17 | export interface AVRClockConfig { 18 | CLKPR: u8; 19 | } 20 | 21 | export const clockConfig: AVRClockConfig = { 22 | CLKPR: 0x61, 23 | }; 24 | 25 | const prescalers = [ 26 | 1, 2, 4, 8, 16, 32, 64, 128, 256, 27 | 28 | // The following values are "reserved" according to the datasheet, so we measured 29 | // with a scope to figure them out (on ATmega328p) 30 | 2, 4, 8, 16, 32, 64, 128, 31 | ]; 32 | 33 | export class AVRClock { 34 | private clockEnabledCycles = 0; 35 | private prescalerValue = 1; 36 | cyclesDelta = 0; 37 | 38 | constructor( 39 | private cpu: CPU, 40 | private baseFreqHz: u32, 41 | private config: AVRClockConfig = clockConfig, 42 | ) { 43 | this.cpu.writeHooks[this.config.CLKPR] = (clkpr) => { 44 | if ((!this.clockEnabledCycles || this.clockEnabledCycles < cpu.cycles) && clkpr === CLKPCE) { 45 | this.clockEnabledCycles = this.cpu.cycles + 4; 46 | } else if (this.clockEnabledCycles && this.clockEnabledCycles >= cpu.cycles) { 47 | this.clockEnabledCycles = 0; 48 | const index = clkpr & 0xf; 49 | const oldPrescaler = this.prescalerValue; 50 | this.prescalerValue = prescalers[index]; 51 | this.cpu.data[this.config.CLKPR] = index; 52 | if (oldPrescaler !== this.prescalerValue) { 53 | this.cyclesDelta = 54 | (cpu.cycles + this.cyclesDelta) * (oldPrescaler / this.prescalerValue) - cpu.cycles; 55 | } 56 | } 57 | 58 | return true; 59 | }; 60 | } 61 | 62 | get frequency() { 63 | return this.baseFreqHz / this.prescalerValue; 64 | } 65 | 66 | get prescaler() { 67 | return this.prescalerValue; 68 | } 69 | 70 | get timeNanos() { 71 | return ((this.cpu.cycles + this.cyclesDelta) / this.frequency) * 1e9; 72 | } 73 | 74 | get timeMicros() { 75 | return ((this.cpu.cycles + this.cyclesDelta) / this.frequency) * 1e6; 76 | } 77 | 78 | get timeMillis() { 79 | return ((this.cpu.cycles + this.cyclesDelta) / this.frequency) * 1e3; 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /benchmark/convert-instructions.ts: -------------------------------------------------------------------------------- 1 | import * as fs from 'fs'; 2 | import * as prettier from 'prettier'; 3 | import prettierOptions from '../prettier.config'; 4 | 5 | const input = fs.readFileSync('src/cpu/instruction.ts', { encoding: 'utf-8' }); 6 | let fnName = ''; 7 | let fnBody = ''; 8 | let currentInstruction = ''; 9 | let pattern = ''; 10 | let output = ` 11 | import { CPU } from '../src/cpu/cpu'; 12 | 13 | function isTwoWordInstruction(opcode: number) { 14 | return ( 15 | /* LDS */ 16 | (opcode & 0xfe0f) === 0x9000 || 17 | /* STS */ 18 | (opcode & 0xfe0f) === 0x9200 || 19 | /* CALL */ 20 | (opcode & 0xfe0e) === 0x940e || 21 | /* JMP */ 22 | (opcode & 0xfe0e) === 0x940c 23 | ); 24 | } 25 | 26 | /* eslint-disable @typescript-eslint/no-unused-vars */ 27 | `; 28 | const patternToFn: Array<[string, string]> = []; 29 | for (const line of input.split('\n')) { 30 | if (line.startsWith(' /* ') && line.includes(', ')) { 31 | currentInstruction = line.trim().split(',')[0].split(' ')[1]; 32 | fnBody = ''; 33 | pattern = line.split(',')[1].split('*')[0]; 34 | console.log(currentInstruction); 35 | fnName = 'inst' + currentInstruction.replace(/[()]/g, ''); 36 | patternToFn.push([pattern.trim(), fnName]); 37 | } else if (line.startsWith(' }')) { 38 | output += ` 39 | export function ${fnName}(cpu: CPU, opcode: number) { 40 | /*${pattern}*/ 41 | ${fnBody} 42 | cpu.cycles++; 43 | if (++cpu.pc >= cpu.progMem.length) { 44 | cpu.pc = 0; 45 | } 46 | } 47 | `; 48 | currentInstruction = ''; 49 | } else if (currentInstruction) { 50 | fnBody += line; 51 | } 52 | } 53 | 54 | let executeInstructionCases = ``; 55 | output += `\nexport const instructions = [`; 56 | let i = 1; 57 | for (const [fnPattern, fn] of patternToFn) { 58 | output += `{pattern: '${fnPattern}', fn: ${fn}, idx: ${i}},`; 59 | executeInstructionCases += `case ${i}: ${fn}(cpu, opcode); break;\n`; 60 | i++; 61 | } 62 | output += ']'; 63 | 64 | output += `\n 65 | export function executeInstruction(idx: number, cpu: CPU, opcode: number) { 66 | switch (idx) { 67 | ${executeInstructionCases} 68 | default: instNOP(cpu, opcode); 69 | } 70 | }`; 71 | 72 | const formattedOutput = prettier.format(output, { 73 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 74 | ...(prettierOptions as any), 75 | parser: 'babel', 76 | }); 77 | 78 | fs.writeFileSync('benchmark/instruction-fn.ts', formattedOutput, { 79 | encoding: 'utf-8', 80 | }); 81 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing to AVR8js 2 | 3 | First of all, thank you for considering contributing to AVR8js! 4 | Please go over these guidelines to ensure that your contribution lands 5 | successfully. 6 | 7 | ## How to Contribute 8 | 9 | Before starting to work on a new feature, please 10 | [file an issue](https://github.com/wokwi/avr8js/issues/new) 11 | to discuss the implementation. 12 | 13 | ## Setting-up Your Environment 14 | 15 | The source code is written in the [TypeScript](https://www.typescriptlang.org/) language, a typed 16 | extension of JavaScript. 17 | 18 | In addition, we use the following tools: 19 | * [prettier](https://prettier.io/) to keep the code pretty 20 | * [eslint](https://eslint.org/) to keep the code consistent and clean 21 | * [editorconfig](https://editorconfig.org/) to keep indentation consistent across different editors 22 | * [jest](https://jestjs.io/) for the unit tests 23 | 24 | If you open this project with [Visual Studio Code](https://code.visualstudio.com/), you will be prompted 25 | to install the [relevant extensions](.vscode/extensions.json) for these tools. 26 | 27 | Finally, we recommend using [Wallaby.js](https://wallabyjs.com/) to run the tests automatically 28 | as you write the code. It should work out of the box with this repo, without any extra configuration. 29 | You can also run the tests manually, from the commandline (see below). 30 | 31 | ## Running the Demo Project 32 | 33 | The demo project allows you to edit Arduino code, compile it, and run it in the simulator. 34 | It also simulates 2 LEDs connected to pins 12 and 13 (PB4 and PB5). To run it, simply execute 35 | 36 | ``` 37 | npm start 38 | ``` 39 | 40 | Then go to http://localhost:1234/ to interact with the project. 41 | 42 | The demo project is packaged using [parcel](https://parceljs.org/) and uses the 43 | [Monaco Editor](https://microsoft.github.io/monaco-editor/) for the interactive 44 | code editor, and the [Wokwi Elements library](https://www.npmjs.com/package/@wokwi/elements) 45 | for displaying the LEDs. 46 | 47 | ## Running The Tests 48 | 49 | Run the tests once: 50 | 51 | ``` 52 | npm test 53 | ``` 54 | 55 | Run the tests of the files you modified since last commit (watch mode): 56 | 57 | ``` 58 | npm run test:watch 59 | ``` 60 | 61 | ## Reference Material 62 | 63 | The following datasheets can be useful when working on new AVR8js features 64 | or fixing existing code: 65 | 66 | * [ATmega48A/PA/88A/PA/168A/PA/328/P Datasheet](http://ww1.microchip.com/downloads/en/DeviceDoc/ATmega48A-PA-88A-PA-168A-PA-328-P-DS-DS40002061A.pdf) 67 | * [The AVR Istruction Set Manual](http://ww1.microchip.com/downloads/en/devicedoc/atmel-0856-avr-instruction-set-manual.pdf) 68 | * [ ATmega640/V-1280/V-1281/V-2560/V-2561/V Datasheet](https://ww1.microchip.com/downloads/en/devicedoc/atmel-2549-8-bit-avr-microcontroller-atmega640-1280-1281-2560-2561_datasheet.pdf) 69 | 70 | ## Coding Guidelines 71 | 72 | Please make sure to follow these guidelines when contributing code: 73 | 74 | 1. You include a relevant test case. Ideally the test case would fail before 75 | your code changes, and pass after implementing the change. 76 | 2. Your commit messages should follow the [conventional commits 77 | standard](https://www.conventionalcommits.org/), e.g.: 78 | `feat(instruction): implement EICALL, EIJMP` 79 | 3. The contributed code has to be compatible with the MIT license. If your 80 | work incoporates some third-party code, please make sure that their 81 | license is compatible and that you credit appropriately. 82 | 83 | Thank you! 84 | -------------------------------------------------------------------------------- /src/cpu/cpu.spec.ts: -------------------------------------------------------------------------------- 1 | import { describe, expect, it } from 'vitest'; 2 | import { AVRClockEventCallback, CPU } from './cpu'; 3 | 4 | type ITestEvent = [number, number]; // Expected cycles, actual cycles 5 | 6 | describe('cpu', () => { 7 | it('should set initial value of SP to the last byte of internal SRAM', () => { 8 | const cpu = new CPU(new Uint16Array(1024), 0x1000); 9 | expect(cpu.SP).toEqual(0x10ff); 10 | }); 11 | 12 | describe('events', () => { 13 | it('should execute queued events after the given number of cycles has passed', () => { 14 | const cpu = new CPU(new Uint16Array(1024), 0x1000); 15 | const events: ITestEvent[] = []; 16 | for (const i of [1, 4, 10]) { 17 | cpu.addClockEvent(() => events.push([i, cpu.cycles]), i); 18 | } 19 | for (let i = 0; i < 10; i++) { 20 | cpu.cycles++; 21 | cpu.tick(); 22 | } 23 | expect(events).toEqual([ 24 | [1, 1], 25 | [4, 4], 26 | [10, 10], 27 | ]); 28 | }); 29 | 30 | it('should correctly sort the events when added in reverse order', () => { 31 | const cpu = new CPU(new Uint16Array(1024), 0x1000); 32 | const events: ITestEvent[] = []; 33 | for (const i of [10, 4, 1]) { 34 | cpu.addClockEvent(() => events.push([i, cpu.cycles]), i); 35 | } 36 | for (let i = 0; i < 10; i++) { 37 | cpu.cycles++; 38 | cpu.tick(); 39 | } 40 | expect(events).toEqual([ 41 | [1, 1], 42 | [4, 4], 43 | [10, 10], 44 | ]); 45 | }); 46 | 47 | describe('updateClockEvent', () => { 48 | it('should update the number of cycles for the given clock event', () => { 49 | const cpu = new CPU(new Uint16Array(1024), 0x1000); 50 | const events: ITestEvent[] = []; 51 | const callbacks: AVRClockEventCallback[] = []; 52 | for (const i of [1, 4, 10]) { 53 | callbacks[i] = cpu.addClockEvent(() => events.push([i, cpu.cycles]), i); 54 | } 55 | cpu.updateClockEvent(callbacks[4], 2); 56 | cpu.updateClockEvent(callbacks[1], 12); 57 | for (let i = 0; i < 14; i++) { 58 | cpu.cycles++; 59 | cpu.tick(); 60 | } 61 | expect(events).toEqual([ 62 | [4, 2], 63 | [10, 10], 64 | [1, 12], 65 | ]); 66 | }); 67 | 68 | describe('clearClockEvent', () => { 69 | it('should remove the given clock event', () => { 70 | const cpu = new CPU(new Uint16Array(1024), 0x1000); 71 | const events: ITestEvent[] = []; 72 | const callbacks: AVRClockEventCallback[] = []; 73 | for (const i of [1, 4, 10]) { 74 | callbacks[i] = cpu.addClockEvent(() => events.push([i, cpu.cycles]), i); 75 | } 76 | cpu.clearClockEvent(callbacks[4]); 77 | for (let i = 0; i < 10; i++) { 78 | cpu.cycles++; 79 | cpu.tick(); 80 | } 81 | expect(events).toEqual([ 82 | [1, 1], 83 | [10, 10], 84 | ]); 85 | }); 86 | 87 | it('should return false if the provided clock event is not scheduled', () => { 88 | const cpu = new CPU(new Uint16Array(1024), 0x1000); 89 | const event4 = cpu.addClockEvent(() => 0, 4); 90 | const event10 = cpu.addClockEvent(() => 0, 10); 91 | cpu.addClockEvent(() => 0, 1); 92 | 93 | // Both event should be successfully removed 94 | expect(cpu.clearClockEvent(event4)).toBe(true); 95 | expect(cpu.clearClockEvent(event10)).toBe(true); 96 | // And now we should get false, as these events have already been removed 97 | expect(cpu.clearClockEvent(event4)).toBe(false); 98 | expect(cpu.clearClockEvent(event10)).toBe(false); 99 | }); 100 | }); 101 | }); 102 | }); 103 | }); 104 | -------------------------------------------------------------------------------- /src/peripherals/clock.spec.ts: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | // Copyright (c) Uri Shaked and contributors 3 | 4 | import { describe, expect, it } from 'vitest'; 5 | import { CPU } from '../cpu/cpu'; 6 | import { AVRClock, clockConfig } from './clock'; 7 | 8 | // Clock Registers 9 | const CLKPC = 0x61; 10 | 11 | // Register bit names 12 | const CLKPCE = 128; 13 | 14 | describe('Clock', () => { 15 | it('should set the prescaler when double-writing CLKPC', () => { 16 | const cpu = new CPU(new Uint16Array(0x1000)); 17 | const clock = new AVRClock(cpu, 16e6, clockConfig); 18 | cpu.writeData(CLKPC, CLKPCE); 19 | cpu.writeData(CLKPC, 3); // Divide by 8 (2^3) 20 | expect(clock.frequency).toEqual(2e6); // 2MHz 21 | expect(cpu.readData(CLKPC)).toEqual(3); 22 | }); 23 | 24 | it('should not update the prescaler if CLKPCE was not set CLKPC', () => { 25 | const cpu = new CPU(new Uint16Array(0x1000)); 26 | const clock = new AVRClock(cpu, 16e6, clockConfig); 27 | cpu.writeData(CLKPC, 3); // Divide by 8 (2^3) 28 | expect(clock.frequency).toEqual(16e6); // still 16MHz 29 | expect(cpu.readData(CLKPC)).toEqual(0); 30 | }); 31 | 32 | it('should not update the prescaler if more than 4 cycles passed since setting CLKPCE', () => { 33 | const cpu = new CPU(new Uint16Array(0x1000)); 34 | const clock = new AVRClock(cpu, 16e6, clockConfig); 35 | cpu.writeData(CLKPC, CLKPCE); 36 | cpu.cycles += 6; 37 | cpu.writeData(CLKPC, 3); // Divide by 8 (2^3) 38 | expect(clock.frequency).toEqual(16e6); // still 16MHz 39 | expect(cpu.readData(CLKPC)).toEqual(0); 40 | }); 41 | 42 | describe('prescaler property', () => { 43 | it('should return the current prescaler value', () => { 44 | const cpu = new CPU(new Uint16Array(0x1000)); 45 | const clock = new AVRClock(cpu, 16e6, clockConfig); 46 | cpu.writeData(CLKPC, CLKPCE); 47 | cpu.writeData(CLKPC, 5); // Divide by 32 (2^5) 48 | cpu.cycles = 16e6; 49 | expect(clock.prescaler).toEqual(32); 50 | }); 51 | }); 52 | 53 | describe('time properties', () => { 54 | it('should return current number of microseconds, derived from base freq + prescaler', () => { 55 | const cpu = new CPU(new Uint16Array(0x1000)); 56 | const clock = new AVRClock(cpu, 16e6, clockConfig); 57 | cpu.writeData(CLKPC, CLKPCE); 58 | cpu.writeData(CLKPC, 2); // Divide by 4 (2^2) 59 | cpu.cycles = 16e6; 60 | expect(clock.timeMillis).toEqual(4000); // 4 seconds 61 | }); 62 | 63 | it('should return current number of milliseconds, derived from base freq + prescaler', () => { 64 | const cpu = new CPU(new Uint16Array(0x1000)); 65 | const clock = new AVRClock(cpu, 16e6, clockConfig); 66 | cpu.writeData(CLKPC, CLKPCE); 67 | cpu.writeData(CLKPC, 2); // Divide by 4 (2^2) 68 | cpu.cycles = 16e6; 69 | expect(clock.timeMicros).toEqual(4e6); // 4 seconds 70 | }); 71 | 72 | it('should return current number of nanoseconds, derived from base freq + prescaler', () => { 73 | const cpu = new CPU(new Uint16Array(0x1000)); 74 | const clock = new AVRClock(cpu, 16e6, clockConfig); 75 | cpu.writeData(CLKPC, CLKPCE); 76 | cpu.writeData(CLKPC, 2); // Divide by 4 (2^2) 77 | cpu.cycles = 16e6; 78 | expect(clock.timeNanos).toEqual(4e9); // 4 seconds 79 | }); 80 | 81 | it('should correctly calculate time when changing the prescale value at runtime', () => { 82 | const cpu = new CPU(new Uint16Array(0x1000)); 83 | const clock = new AVRClock(cpu, 16e6, clockConfig); 84 | cpu.cycles = 16e6; // run 1 second at 16MHz 85 | cpu.writeData(CLKPC, CLKPCE); 86 | cpu.writeData(CLKPC, 2); // Divide by 4 (2^2) 87 | cpu.cycles += 2 * 4e6; // run 2 more seconds at 4MhZ 88 | expect(clock.timeMillis).toEqual(3000); // 3 seconds in total 89 | 90 | cpu.writeData(CLKPC, CLKPCE); 91 | cpu.writeData(CLKPC, 1); // Divide by 2 (2^1) 92 | cpu.cycles += 0.5 * 8e6; // run 0.5 more seconds at 8MhZ 93 | expect(clock.timeMillis).toEqual(3500); // 3.5 seconds in total 94 | }); 95 | }); 96 | }); 97 | -------------------------------------------------------------------------------- /src/peripherals/adc.spec.ts: -------------------------------------------------------------------------------- 1 | import { describe, expect, it, vi } from 'vitest'; 2 | import { CPU } from '../cpu/cpu'; 3 | import { asmProgram, TestProgramRunner } from '../utils/test-utils'; 4 | import { adcConfig, ADCMuxInputType, AVRADC } from './adc'; 5 | 6 | const R16 = 16; 7 | const R17 = 17; 8 | 9 | const ADMUX = 0x7c; 10 | const REFS0 = 1 << 6; 11 | 12 | const ADCSRA = 0x7a; 13 | const ADEN = 1 << 7; 14 | const ADSC = 1 << 6; 15 | const ADPS0 = 1 << 0; 16 | const ADPS1 = 1 << 1; 17 | const ADPS2 = 1 << 2; 18 | 19 | const ADCH = 0x79; 20 | const ADCL = 0x78; 21 | 22 | describe('ADC', () => { 23 | it('should successfuly perform an ADC conversion', () => { 24 | const { program } = asmProgram(` 25 | ; register addresses 26 | _REPLACE ADMUX, ${ADMUX} 27 | _REPLACE ADCSRA, ${ADCSRA} 28 | _REPLACE ADCH, ${ADCH} 29 | _REPLACE ADCL, ${ADCL} 30 | 31 | ; Configure mux - channel 0, reference: AVCC with external capacitor at AREF pin 32 | ldi r24, ${REFS0} 33 | sts ADMUX, r24 34 | 35 | ; Start conversion with 128 prescaler 36 | ldi r24, ${ADEN | ADSC | ADPS0 | ADPS1 | ADPS2} 37 | sts ADCSRA, r24 38 | 39 | ; Wait until conversion is complete 40 | waitComplete: 41 | lds r24, ${ADCSRA} 42 | andi r24, ${ADSC} 43 | brne waitComplete 44 | 45 | ; Read the result 46 | lds r16, ${ADCL} 47 | lds r17, ${ADCH} 48 | 49 | break 50 | `); 51 | const cpu = new CPU(program); 52 | const adc = new AVRADC(cpu, adcConfig); 53 | const runner = new TestProgramRunner(cpu); 54 | 55 | const adcReadSpy = vi.spyOn(adc, 'onADCRead'); 56 | adc.channelValues[0] = 2.56; // should result in 2.56/5*1024 = 524 57 | 58 | // Setup 59 | runner.runInstructions(4); 60 | expect(adcReadSpy).toHaveBeenCalledWith({ channel: 0, type: ADCMuxInputType.SingleEnded }); 61 | 62 | // Run the "waitComplete" loop for a few cycles 63 | runner.runInstructions(12); 64 | 65 | cpu.cycles += 128 * 25; // skip to the end of the conversion 66 | cpu.tick(); 67 | 68 | // Now read the result 69 | runner.runInstructions(5); 70 | 71 | const low = cpu.data[R16]; 72 | const high = cpu.data[R17]; 73 | expect((high << 8) | low).toEqual(524); // 2.56 volts - see above 74 | }); 75 | 76 | it('should read 0 when the ADC peripheral is not enabled', () => { 77 | // This behavior was verified on real hardware, using the following test program: 78 | // https://wokwi.com/arduino/projects/309156042450666050 79 | // Thanks Oscar Oomens for spotting this! 80 | 81 | const { program } = asmProgram(` 82 | ; register addresses 83 | _REPLACE ADMUX, ${ADMUX} 84 | _REPLACE ADCSRA, ${ADCSRA} 85 | _REPLACE ADCH, ${ADCH} 86 | _REPLACE ADCL, ${ADCL} 87 | 88 | ; Load some initial value into r16/r17 to make sure we actually read 0 later 89 | ldi r16, 0xff 90 | ldi r17, 0xff 91 | 92 | ; Configure mux - channel 0, reference: AVCC with external capacitor at AREF pin 93 | ldi r24, ${REFS0} 94 | sts ADMUX, r24 95 | 96 | ; Start conversion with 128 prescaler, but without enabling the ADC 97 | ldi r24, ${ADSC | ADPS0 | ADPS1 | ADPS2} 98 | sts ADCSRA, r24 99 | 100 | ; Wait until conversion is complete 101 | waitComplete: 102 | lds r24, ${ADCSRA} 103 | andi r24, ${ADSC} 104 | brne waitComplete 105 | 106 | ; Read the result 107 | lds r16, ${ADCL} 108 | lds r17, ${ADCH} 109 | 110 | break 111 | `); 112 | const cpu = new CPU(program); 113 | const adc = new AVRADC(cpu, adcConfig); 114 | const runner = new TestProgramRunner(cpu, () => { 115 | /* do nothing on break */ 116 | }); 117 | 118 | const adcReadSpy = vi.spyOn(adc, 'onADCRead'); 119 | adc.channelValues[0] = 2.56; // should result in 2.56/5*1024 = 524 120 | 121 | // Setup 122 | runner.runInstructions(6); 123 | expect(adcReadSpy).not.toHaveBeenCalled(); 124 | 125 | // Run the "waitComplete" loop for a few cycles 126 | runner.runInstructions(12); 127 | 128 | cpu.cycles += 128 * 25; // skip to the end of the conversion 129 | cpu.tick(); 130 | 131 | // Now read the result 132 | runner.runToBreak(); 133 | 134 | const low = cpu.data[R16]; 135 | const high = cpu.data[R17]; 136 | expect((high << 8) | low).toEqual(0); // We should read 0 since the ADC hasn't been enabled 137 | }); 138 | }); 139 | -------------------------------------------------------------------------------- /src/peripherals/usi.ts: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | // Copyright (c) Uri Shaked and contributors 3 | 4 | import { AVRInterruptConfig, CPU } from '../cpu/cpu'; 5 | import { AVRIOPort } from './gpio'; 6 | 7 | const USICR = 0x2d; 8 | const USISR = 0x2e; 9 | const USIDR = 0x2f; 10 | const USIBR = 0x30; 11 | 12 | // USISR bits 13 | const USICNT_MASK = 0xf; 14 | const USIDC = 1 << 4; 15 | const USIPF = 1 << 5; 16 | const USIOIF = 1 << 6; 17 | const USISIF = 1 << 7; 18 | 19 | // USICR bits 20 | const USITC = 1 << 0; 21 | const USICLK = 1 << 1; 22 | const USICS0 = 1 << 2; 23 | const USICS1 = 1 << 3; 24 | const USIWM0 = 1 << 4; 25 | const USIWM1 = 1 << 5; 26 | const USIOIE = 1 << 6; 27 | const USISIE = 1 << 7; 28 | 29 | export class AVRUSI { 30 | // Interrupts 31 | private START: AVRInterruptConfig = { 32 | address: 0xd, 33 | flagRegister: USISR, 34 | flagMask: USISIF, 35 | enableRegister: USICR, 36 | enableMask: USISIE, 37 | }; 38 | 39 | private OVF: AVRInterruptConfig = { 40 | address: 0xe, 41 | flagRegister: USISR, 42 | flagMask: USIOIF, 43 | enableRegister: USICR, 44 | enableMask: USIOIE, 45 | }; 46 | 47 | constructor(cpu: CPU, port: AVRIOPort, portPin: number, dataPin: number, clockPin: number) { 48 | const PIN = portPin; 49 | const PORT = PIN + 2; 50 | port.addListener((value) => { 51 | const twoWire = (cpu.data[USICR] & USIWM1) === USIWM1; 52 | if (twoWire) { 53 | if (value & (1 << clockPin) && !(value & (1 << dataPin))) { 54 | // Start condition detected 55 | cpu.setInterruptFlag(this.START); 56 | } 57 | if (value & (1 << clockPin) && value & (1 << dataPin)) { 58 | // Stop condition detected 59 | cpu.data[USISR] |= USIPF; 60 | } 61 | } 62 | }); 63 | const updateOutput = () => { 64 | const oldValue = cpu.data[PORT]; 65 | const newValue = 66 | cpu.data[USIDR] & 0x80 ? oldValue | (1 << dataPin) : oldValue & ~(1 << dataPin); 67 | cpu.writeHooks[PORT](newValue, oldValue, PORT, 0xff); 68 | if (newValue & 0x80 && !(cpu.data[PIN] & 0x80)) { 69 | cpu.data[USISR] |= USIDC; // Shout output HIGH (pulled-up), but input is LOW 70 | } else { 71 | cpu.data[USISR] &= ~USIDC; 72 | } 73 | }; 74 | const count = () => { 75 | const counter = (cpu.data[USISR] + 1) & USICNT_MASK; 76 | cpu.data[USISR] = (cpu.data[USISR] & ~USICNT_MASK) | counter; 77 | if (!counter) { 78 | cpu.data[USIBR] = cpu.data[USIDR]; 79 | cpu.setInterruptFlag(this.OVF); 80 | } 81 | }; 82 | const shift = (inputValue: number) => { 83 | cpu.data[USIDR] = (cpu.data[USIDR] << 1) | inputValue; 84 | updateOutput(); 85 | }; 86 | cpu.writeHooks[USIDR] = (value: number) => { 87 | cpu.data[USIDR] = value; 88 | updateOutput(); 89 | return true; 90 | }; 91 | cpu.writeHooks[USISR] = (value: number) => { 92 | const writeClearMask = USISIF | USIOIF | USIPF; 93 | cpu.data[USISR] = (cpu.data[USISR] & writeClearMask & ~value) | (value & 0xf); 94 | cpu.clearInterruptByFlag(this.START, value); 95 | cpu.clearInterruptByFlag(this.OVF, value); 96 | return true; 97 | }; 98 | cpu.writeHooks[USICR] = (value: number) => { 99 | cpu.data[USICR] = value & ~(USICLK | USITC); 100 | cpu.updateInterruptEnable(this.START, value); 101 | cpu.updateInterruptEnable(this.OVF, value); 102 | const clockSrc = value & ((USICS1 | USICS0) >> 2); 103 | const mode = value & ((USIWM1 | USIWM0) >> 4); 104 | const usiClk = value & USICLK; 105 | port.openCollector = mode >= 2 ? 1 << dataPin : 0; 106 | const inputValue = cpu.data[PIN] & (1 << dataPin) ? 1 : 0; 107 | if (usiClk && !clockSrc) { 108 | shift(inputValue); 109 | count(); 110 | } 111 | if (value & USITC) { 112 | cpu.writeHooks[PIN](1 << clockPin, cpu.data[PIN], PIN, 0xff); 113 | const newValue = cpu.data[PIN] & (1 << clockPin); 114 | if (usiClk && (clockSrc === 2 || clockSrc === 3)) { 115 | if (clockSrc === 2 && newValue) { 116 | shift(inputValue); 117 | } 118 | if (clockSrc === 3 && !newValue) { 119 | shift(inputValue); 120 | } 121 | count(); 122 | } 123 | return true; 124 | } 125 | }; 126 | } 127 | } 128 | -------------------------------------------------------------------------------- /src/peripherals/watchdog.ts: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | // Copyright (c) Uri Shaked and contributors 3 | 4 | /** 5 | * AVR8 Watchdog Timer 6 | * Part of AVR8js 7 | * Reference: http://ww1.microchip.com/downloads/en/DeviceDoc/ATmega48A-PA-88A-PA-168A-PA-328-P-DS-DS40002061A.pdf 8 | * 9 | * Copyright (C) 2021 Uri Shaked 10 | */ 11 | 12 | import { AVRClock } from '..'; 13 | import { AVRInterruptConfig, CPU } from '../cpu/cpu'; 14 | import { u8 } from '../types'; 15 | 16 | export interface WatchdogConfig { 17 | watchdogInterrupt: u8; 18 | MCUSR: u8; 19 | WDTCSR: u8; 20 | } 21 | 22 | // Register bits: 23 | const MCUSR_WDRF = 0x8; // Watchdog System Reset Flag 24 | 25 | const WDTCSR_WDIF = 0x80; 26 | const WDTCSR_WDIE = 0x40; 27 | const WDTCSR_WDP3 = 0x20; 28 | const WDTCSR_WDCE = 0x10; // Watchdog Change Enable 29 | const WDTCSR_WDE = 0x8; 30 | const WDTCSR_WDP2 = 0x4; 31 | const WDTCSR_WDP1 = 0x2; 32 | const WDTCSR_WDP0 = 0x1; 33 | const WDTCSR_WDP210 = WDTCSR_WDP2 | WDTCSR_WDP1 | WDTCSR_WDP0; 34 | 35 | const WDTCSR_PROTECT_MASK = WDTCSR_WDE | WDTCSR_WDP3 | WDTCSR_WDP210; 36 | 37 | export const watchdogConfig: WatchdogConfig = { 38 | watchdogInterrupt: 0x0c, 39 | MCUSR: 0x54, 40 | WDTCSR: 0x60, 41 | }; 42 | 43 | export class AVRWatchdog { 44 | readonly clockFrequency = 128000; 45 | 46 | /** 47 | * Used to keep track on the last write to WDCE. Once written, the WDE/WDP* bits can be changed. 48 | */ 49 | private changeEnabledCycles = 0; 50 | private watchdogTimeout = 0; 51 | private enabledValue = false; 52 | private scheduled = false; 53 | 54 | // Interrupts 55 | private Watchdog: AVRInterruptConfig = { 56 | address: this.config.watchdogInterrupt, 57 | flagRegister: this.config.WDTCSR, 58 | flagMask: WDTCSR_WDIF, 59 | enableRegister: this.config.WDTCSR, 60 | enableMask: WDTCSR_WDIE, 61 | }; 62 | 63 | constructor( 64 | private cpu: CPU, 65 | private config: WatchdogConfig, 66 | private clock: AVRClock, 67 | ) { 68 | const { WDTCSR } = config; 69 | this.cpu.onWatchdogReset = () => { 70 | this.resetWatchdog(); 71 | }; 72 | cpu.writeHooks[WDTCSR] = (value: u8, oldValue: u8) => { 73 | if (value & WDTCSR_WDCE && value & WDTCSR_WDE) { 74 | this.changeEnabledCycles = this.cpu.cycles + 4; 75 | value = value & ~WDTCSR_PROTECT_MASK; 76 | } else { 77 | if (this.cpu.cycles >= this.changeEnabledCycles) { 78 | value = (value & ~WDTCSR_PROTECT_MASK) | (oldValue & WDTCSR_PROTECT_MASK); 79 | } 80 | this.enabledValue = !!(value & WDTCSR_WDE || value & WDTCSR_WDIE); 81 | this.cpu.data[WDTCSR] = value; 82 | } 83 | 84 | if (this.enabled) { 85 | this.resetWatchdog(); 86 | } 87 | 88 | if (this.enabled && !this.scheduled) { 89 | this.cpu.addClockEvent(this.checkWatchdog, this.watchdogTimeout - this.cpu.cycles); 90 | } 91 | 92 | this.cpu.clearInterruptByFlag(this.Watchdog, value); 93 | return true; 94 | }; 95 | } 96 | 97 | resetWatchdog() { 98 | const cycles = Math.floor((this.clock.frequency / this.clockFrequency) * this.prescaler); 99 | this.watchdogTimeout = this.cpu.cycles + cycles; 100 | } 101 | 102 | checkWatchdog = () => { 103 | if (this.enabled && this.cpu.cycles >= this.watchdogTimeout) { 104 | // Watchdog timed out! 105 | const wdtcsr = this.cpu.data[this.config.WDTCSR]; 106 | if (wdtcsr & WDTCSR_WDIE) { 107 | this.cpu.setInterruptFlag(this.Watchdog); 108 | } 109 | if (wdtcsr & WDTCSR_WDE) { 110 | if (wdtcsr & WDTCSR_WDIE) { 111 | this.cpu.data[this.config.WDTCSR] &= ~WDTCSR_WDIE; 112 | } else { 113 | this.cpu.reset(); 114 | this.scheduled = false; 115 | this.cpu.data[this.config.MCUSR] |= MCUSR_WDRF; 116 | return; 117 | } 118 | } 119 | this.resetWatchdog(); 120 | } 121 | if (this.enabled) { 122 | this.scheduled = true; 123 | this.cpu.addClockEvent(this.checkWatchdog, this.watchdogTimeout - this.cpu.cycles); 124 | } else { 125 | this.scheduled = false; 126 | } 127 | }; 128 | 129 | get enabled() { 130 | return this.enabledValue; 131 | } 132 | 133 | /** 134 | * The base clock frequency is 128KHz. Thus, a prescaler of 2048 gives 16ms timeout. 135 | */ 136 | get prescaler() { 137 | const wdtcsr = this.cpu.data[this.config.WDTCSR]; 138 | const value = ((wdtcsr & WDTCSR_WDP3) >> 2) | (wdtcsr & WDTCSR_WDP210); 139 | return 2048 << value; 140 | } 141 | } 142 | -------------------------------------------------------------------------------- /demo/src/index.ts: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | // Copyright (c) Uri Shaked and contributors 3 | 4 | import '@wokwi/elements'; 5 | import { LEDElement } from '@wokwi/elements'; 6 | import { PinState } from 'avr8js'; 7 | import { buildHex } from './compile'; 8 | import { CPUPerformance } from './cpu-performance'; 9 | import { AVRRunner } from './execute'; 10 | import { formatTime } from './format-time'; 11 | import './index.css'; 12 | import { EditorHistoryUtil } from './utils/editor-history.util'; 13 | 14 | let editor: any; // eslint-disable-line @typescript-eslint/no-explicit-any 15 | const BLINK_CODE = ` 16 | // Green LED connected to LED_BUILTIN, 17 | // Red LED connected to pin 12. Enjoy! 18 | 19 | void setup() { 20 | Serial.begin(115200); 21 | pinMode(LED_BUILTIN, OUTPUT); 22 | } 23 | 24 | void loop() { 25 | Serial.println("Blink"); 26 | digitalWrite(LED_BUILTIN, HIGH); 27 | delay(500); 28 | digitalWrite(LED_BUILTIN, LOW); 29 | delay(500); 30 | }`.trim(); 31 | 32 | // Load Editor 33 | declare const window: any; // eslint-disable-line @typescript-eslint/no-explicit-any 34 | declare const monaco: any; // eslint-disable-line @typescript-eslint/no-explicit-any 35 | window.require.config({ 36 | paths: { vs: 'https://cdnjs.cloudflare.com/ajax/libs/monaco-editor/0.33.0/min/vs' }, 37 | }); 38 | window.require(['vs/editor/editor.main'], () => { 39 | editor = monaco.editor.create(document.querySelector('.code-editor'), { 40 | value: EditorHistoryUtil.getValue() || BLINK_CODE, 41 | language: 'cpp', 42 | minimap: { enabled: false }, 43 | }); 44 | }); 45 | 46 | // Set up LEDs 47 | const led13 = document.querySelector('wokwi-led[color=green]'); 48 | const led12 = document.querySelector('wokwi-led[color=red]'); 49 | 50 | // Set up toolbar 51 | let runner: AVRRunner; 52 | 53 | /* eslint-disable @typescript-eslint/no-use-before-define */ 54 | const runButton = document.querySelector('#run-button'); 55 | runButton.addEventListener('click', compileAndRun); 56 | const stopButton = document.querySelector('#stop-button'); 57 | stopButton.addEventListener('click', stopCode); 58 | const revertButton = document.querySelector('#revert-button'); 59 | revertButton.addEventListener('click', setBlinkSnippet); 60 | const statusLabel = document.querySelector('#status-label'); 61 | const compilerOutputText = document.querySelector('#compiler-output-text'); 62 | const serialOutputText = document.querySelector('#serial-output-text'); 63 | 64 | function executeProgram(hex: string) { 65 | runner = new AVRRunner(hex); 66 | const MHZ = 16000000; 67 | 68 | // Hook to PORTB register 69 | runner.portB.addListener(() => { 70 | led12.value = runner.portB.pinState(4) === PinState.High; 71 | led13.value = runner.portB.pinState(5) === PinState.High; 72 | }); 73 | runner.usart.onByteTransmit = (value) => { 74 | serialOutputText.textContent += String.fromCharCode(value); 75 | }; 76 | const cpuPerf = new CPUPerformance(runner.cpu, MHZ); 77 | runner.execute((cpu) => { 78 | const time = formatTime(cpu.cycles / MHZ); 79 | const speed = (cpuPerf.update() * 100).toFixed(0); 80 | statusLabel.textContent = `Simulation time: ${time} (${speed}%)`; 81 | }); 82 | } 83 | 84 | async function compileAndRun() { 85 | led12.value = false; 86 | led13.value = false; 87 | 88 | storeUserSnippet(); 89 | 90 | runButton.setAttribute('disabled', '1'); 91 | revertButton.setAttribute('disabled', '1'); 92 | 93 | serialOutputText.textContent = ''; 94 | try { 95 | statusLabel.textContent = 'Compiling...'; 96 | const result = await buildHex(editor.getModel().getValue()); 97 | compilerOutputText.textContent = result.stderr || result.stdout; 98 | if (result.hex) { 99 | compilerOutputText.textContent += '\nProgram running...'; 100 | stopButton.removeAttribute('disabled'); 101 | executeProgram(result.hex); 102 | } else { 103 | runButton.removeAttribute('disabled'); 104 | } 105 | } catch (err) { 106 | runButton.removeAttribute('disabled'); 107 | revertButton.removeAttribute('disabled'); 108 | alert('Failed: ' + err); 109 | } finally { 110 | statusLabel.textContent = ''; 111 | } 112 | } 113 | 114 | function storeUserSnippet() { 115 | EditorHistoryUtil.clearSnippet(); 116 | EditorHistoryUtil.storeSnippet(editor.getValue()); 117 | } 118 | 119 | function stopCode() { 120 | stopButton.setAttribute('disabled', '1'); 121 | runButton.removeAttribute('disabled'); 122 | revertButton.removeAttribute('disabled'); 123 | if (runner) { 124 | runner.stop(); 125 | runner = null; 126 | } 127 | } 128 | 129 | function setBlinkSnippet() { 130 | editor.setValue(BLINK_CODE); 131 | EditorHistoryUtil.storeSnippet(editor.getValue()); 132 | } 133 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # AVR8js 2 | 3 | This is a JavaScript library that implementats the AVR 8-bit architecture. 4 | 5 | It's the heart- but not the whole body- of the Arduino simulator at [https://wokwi.com](https://wokwi.com). 6 | 7 | [![Build Status](../../workflows/ci/badge.svg)](https://github.com/wokwi/avr8js/actions/workflows/ci.yml) 8 | [![NPM Version](https://img.shields.io/npm/v/avr8js)](https://www.npmjs.com/package/avr8js) 9 | ![License: MIT](https://img.shields.io/npm/l/avr8js) 10 | ![Types: TypeScript](https://img.shields.io/npm/types/avr8js) 11 | [![Gitpod ready-to-code](https://img.shields.io/badge/Gitpod-ready--to--code-blue?logo=gitpod)](https://gitpod.io/#https://github.com/wokwi/avr8js) 12 | 13 | ## Example Applications Using `AVR8js` 14 | 15 | * [Wokwi Arduino Simulator](https://wokwi.com) - Arduino simulator with a choice of hardware that can be wired up dynamically in your browser! 16 | * [avr8js-electron playground](https://github.com/arcostasi/avr8js-electron-playground) - a Downloadable Electron Arduino simulator app 17 | * [AVR8js-Falstad](https://markmegarry.github.io/AVR8js-Falstad/) - combining the Falstad circuit simulator with AVR8js! 18 | * [The Engineering Physics Department of Dawson College's Arduino Course](https://tawjaw.github.io/Arduino-Robot-Virtual-Lab/) - an introduction to Arduino with a focus on robotics 19 | 20 | ## How to Use This Library 21 | 22 | This library only implements the AVR CPU core. 23 | You have to supply it pre-compiled machine code to run, and implement functional simulations of any external hardware. You will probably also want to add audio/visual representations of external hardware being simulated. 24 | 25 | A rough conceptual diagram: 26 | 27 | ``` 28 | Pre-Compiled machine code --> AVR8js <--> Glue code <--> external hardware functional simulation <--> simulation state display for the user 29 | ``` 30 | You may be interested in exploring the [wokwi-elements](https://github.com/wokwi/wokwi-elements) collection of web-components for visual representations of many common hardware components. (Note: these are visual only elements- you will need to add the appropriate functional simulation and glue code.) 31 | 32 | ### Walkthrough Video Tutorial 33 | 34 | A step-by-step video tutorial showing how to build a simple Arduino simulator using AVR8js and React: 35 | 36 | [![AVR8JS Walkthrough Video](https://i.imgur.com/3meSd1m.png)](https://youtu.be/fArqj-USmjA) 37 | 38 | And a related [blog post](https://blog.wokwi.com/avr8js-simulate-arduino-in-javascript/). 39 | 40 | ### Unofficial examples 41 | 42 | These examples show working examples of using `avr8js` in an application. Many of them also demonstrate how to use the `wokwi-elements` and include working examples of functional simulations of the components, and how to hook them up to `avr8js`. 43 | 44 | * [Minimal Example](https://stackblitz.com/edit/avr8js-minimal?file=main.ts) 45 | * [6 LEDs](https://stackblitz.com/edit/avr8js-6leds?file=index.ts) 46 | * [LED PWM](https://stackblitz.com/edit/avr8js-pwm?file=index.ts) 47 | * [Serial Monitor](https://stackblitz.com/edit/avr8js-serial?file=index.ts) 48 | * [NeoPixel Matrix](https://stackblitz.com/edit/avr8js-ws2812?file=index.ts) 49 | * [Arduino MEGA NeoPixel Matrix](https://stackblitz.com/edit/avr8js-mega-ws2812?file=index.ts) 50 | * [Simon Game](https://stackblitz.com/edit/avr8js-simon-game?file=index.ts) - with pushbuttons and sound 51 | * [XMAS LEDs](https://stackblitz.com/edit/avr8js-xmas-dafna?file=index.ts) 52 | * [Assembly Code](https://stackblitz.com/edit/avr8js-asm?file=index.ts) 53 | * [EEPROM persistence](https://stackblitz.com/edit/avr8js-eeprom-localstorage?file=eeprom-localstorage-backend.ts) 54 | 55 | Note: they are all hosted outside of this repo. 56 | 57 | ## Running the demo project 58 | 59 | The demo project allows you to edit Arduino code, compile it, and run it in the simulator. 60 | It also simulates 2 LEDs connected to pins 12 and 13 (PB4 and PB5). 61 | 62 | To run the demo project, check out this repository, run `npm install` and then `npm start`. 63 | 64 | ## Which chips can be simulated? 65 | 66 | The library focuses on simulating the *ATmega328p*, which is the MCU used by the Arduino Uno. 67 | 68 | However, the code is built in a modular way, and is highly configurable, making it possible 69 | to simulate many chips from the AVR8 family, such as the ATmega2560 and the ATtiny series: 70 | 71 | * [ATtiny85 Simulation](https://avr8js-attiny85.stackblitz.io?file=index.ts) 72 | 73 | Check out [issue 67](https://github.com/wokwi/avr8js/issues/67#issuecomment-728121667) and 74 | [issue 73](https://github.com/wokwi/avr8js/issues/73#issuecomment-743740477) for more information. 75 | 76 | ## Running the tests 77 | 78 | Run the tests once: 79 | 80 | ``` 81 | npm test 82 | ``` 83 | 84 | Run the tests of the files you modified since last commit (watch mode): 85 | 86 | ``` 87 | npm run test:watch 88 | ``` 89 | 90 | For more information, please check the [Contributing Guide](CONTRIBUTING.md). 91 | 92 | ## License 93 | 94 | Copyright (C) 2019-2025 Uri Shaked. The code is released under the terms of the MIT license. 95 | -------------------------------------------------------------------------------- /src/peripherals/eeprom.ts: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | // Copyright (c) Uri Shaked and contributors 3 | 4 | import { AVRInterruptConfig, CPU } from '../cpu/cpu'; 5 | import { u16, u32, u8 } from '../types'; 6 | 7 | export interface EEPROMBackend { 8 | readMemory(addr: u16): u8; 9 | writeMemory(addr: u16, value: u8): void; 10 | eraseMemory(addr: u16): void; 11 | } 12 | 13 | export class EEPROMMemoryBackend implements EEPROMBackend { 14 | readonly memory: Uint8Array; 15 | 16 | constructor(size: u16) { 17 | this.memory = new Uint8Array(size); 18 | this.memory.fill(0xff); 19 | } 20 | 21 | readMemory(addr: u16) { 22 | return this.memory[addr]; 23 | } 24 | 25 | writeMemory(addr: u16, value: u8) { 26 | this.memory[addr] &= value; 27 | } 28 | 29 | eraseMemory(addr: u16) { 30 | this.memory[addr] = 0xff; 31 | } 32 | } 33 | 34 | export interface AVREEPROMConfig { 35 | eepromReadyInterrupt: u8; 36 | 37 | EECR: u8; 38 | EEDR: u8; 39 | EEARL: u8; 40 | EEARH: u8; 41 | 42 | /** The amount of clock cycles erase takes */ 43 | eraseCycles: u32; 44 | /** The amount of clock cycles a write takes */ 45 | writeCycles: u32; 46 | } 47 | 48 | export const eepromConfig: AVREEPROMConfig = { 49 | eepromReadyInterrupt: 0x2c, 50 | EECR: 0x3f, 51 | EEDR: 0x40, 52 | EEARL: 0x41, 53 | EEARH: 0x42, 54 | eraseCycles: 28800, // 1.8ms at 16MHz 55 | writeCycles: 28800, // 1.8ms at 16MHz 56 | }; 57 | 58 | const EERE = 1 << 0; 59 | const EEPE = 1 << 1; 60 | const EEMPE = 1 << 2; 61 | const EERIE = 1 << 3; 62 | const EEPM0 = 1 << 4; 63 | const EEPM1 = 1 << 5; 64 | const EECR_WRITE_MASK = EEPE | EEMPE | EERIE | EEPM0 | EEPM1; 65 | 66 | export class AVREEPROM { 67 | /** 68 | * Used to keep track on the last write to EEMPE. From the datasheet: 69 | * The EEMPE bit determines whether setting EEPE to one causes the EEPROM to be written. 70 | * When EEMPE is set, setting EEPE within four clock cycles will write data to the EEPROM 71 | * at the selected address If EEMPE is zero, setting EEPE will have no effect. 72 | */ 73 | private writeEnabledCycles = 0; 74 | 75 | private writeCompleteCycles = 0; 76 | 77 | // Interrupts 78 | private EER: AVRInterruptConfig = { 79 | address: this.config.eepromReadyInterrupt, 80 | flagRegister: this.config.EECR, 81 | flagMask: EEPE, 82 | enableRegister: this.config.EECR, 83 | enableMask: EERIE, 84 | constant: true, 85 | inverseFlag: true, 86 | }; 87 | 88 | constructor( 89 | private cpu: CPU, 90 | private backend: EEPROMBackend, 91 | private config: AVREEPROMConfig = eepromConfig, 92 | ) { 93 | this.cpu.writeHooks[this.config.EECR] = (eecr) => { 94 | const { EEARH, EEARL, EECR, EEDR } = this.config; 95 | 96 | const addr = (this.cpu.data[EEARH] << 8) | this.cpu.data[EEARL]; 97 | 98 | this.cpu.data[EECR] = (this.cpu.data[EECR] & ~EECR_WRITE_MASK) | (eecr & EECR_WRITE_MASK); 99 | this.cpu.updateInterruptEnable(this.EER, eecr); 100 | 101 | if (eecr & EERE) { 102 | this.cpu.clearInterrupt(this.EER); 103 | } 104 | 105 | if (eecr & EEMPE) { 106 | const eempeCycles = 4; 107 | this.writeEnabledCycles = this.cpu.cycles + eempeCycles; 108 | this.cpu.addClockEvent(() => { 109 | this.cpu.data[EECR] &= ~EEMPE; 110 | }, eempeCycles); 111 | } 112 | 113 | // Read 114 | if (eecr & EERE) { 115 | this.cpu.data[EEDR] = this.backend.readMemory(addr); 116 | // When the EEPROM is read, the CPU is halted for four cycles before the 117 | // next instruction is executed. 118 | this.cpu.cycles += 4; 119 | return true; 120 | } 121 | 122 | // Write 123 | if (eecr & EEPE) { 124 | // If EEMPE is zero, setting EEPE will have no effect. 125 | if (this.cpu.cycles >= this.writeEnabledCycles) { 126 | this.cpu.data[EECR] &= ~EEPE; 127 | return true; 128 | } 129 | // Check for write-in-progress 130 | if (this.cpu.cycles < this.writeCompleteCycles) { 131 | return true; 132 | } 133 | 134 | const eedr = this.cpu.data[EEDR]; 135 | 136 | this.writeCompleteCycles = this.cpu.cycles; 137 | 138 | // Erase 139 | if (!(eecr & EEPM1)) { 140 | this.backend.eraseMemory(addr); 141 | this.writeCompleteCycles += this.config.eraseCycles; 142 | } 143 | // Write 144 | if (!(eecr & EEPM0)) { 145 | this.backend.writeMemory(addr, eedr); 146 | this.writeCompleteCycles += this.config.writeCycles; 147 | } 148 | 149 | this.cpu.data[EECR] |= EEPE; 150 | 151 | this.cpu.addClockEvent(() => { 152 | this.cpu.setInterruptFlag(this.EER); 153 | }, this.writeCompleteCycles - this.cpu.cycles); 154 | 155 | // When EEPE has been set, the CPU is halted for two cycles before the 156 | // next instruction is executed. 157 | this.cpu.cycles += 2; 158 | } 159 | 160 | return true; 161 | }; 162 | } 163 | } 164 | -------------------------------------------------------------------------------- /src/peripherals/spi.ts: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | // Copyright (c) Uri Shaked and contributors 3 | 4 | import { AVRInterruptConfig, CPU } from '../cpu/cpu'; 5 | import { u8 } from '../types'; 6 | 7 | export interface SPIConfig { 8 | spiInterrupt: u8; 9 | 10 | SPCR: u8; 11 | SPSR: u8; 12 | SPDR: u8; 13 | } 14 | 15 | // Register bits: 16 | const SPCR_SPIE = 0x80; // SPI Interrupt Enable 17 | const SPCR_SPE = 0x40; // SPI Enable 18 | const SPCR_DORD = 0x20; // Data Order 19 | const SPCR_MSTR = 0x10; // Master/Slave Select 20 | const SPCR_CPOL = 0x8; // Clock Polarity 21 | const SPCR_CPHA = 0x4; // Clock Phase 22 | const SPCR_SPR1 = 0x2; // SPI Clock Rate Select 1 23 | const SPCR_SPR0 = 0x1; // SPI Clock Rate Select 0 24 | const SPSR_SPR_MASK = SPCR_SPR1 | SPCR_SPR0; 25 | 26 | const SPSR_SPIF = 0x80; // SPI Interrupt Flag 27 | const SPSR_WCOL = 0x40; // Write COLlision Flag 28 | const SPSR_SPI2X = 0x1; // Double SPI Speed Bit 29 | 30 | export const spiConfig: SPIConfig = { 31 | spiInterrupt: 0x22, 32 | SPCR: 0x4c, 33 | SPSR: 0x4d, 34 | SPDR: 0x4e, 35 | }; 36 | 37 | export type SPITransferCallback = (value: u8) => number; 38 | export type SPIByteTransferCallback = (value: u8) => void; 39 | 40 | const bitsPerByte = 8; 41 | 42 | export class AVRSPI { 43 | /** @deprecated Use onByte() instead */ 44 | public onTransfer: SPITransferCallback = () => 0; 45 | 46 | /** 47 | * SPI byte transfer callback. Invoked whenever the user code starts an SPI transaction. 48 | * You can override this with your own SPI handler logic. 49 | * 50 | * The callback receives a argument: the byte sent over the SPI MOSI line. 51 | * It should call `completeTransfer()` within `transferCycles` CPU cycles. 52 | */ 53 | public onByte: SPIByteTransferCallback = (value) => { 54 | const valueIn = this.onTransfer(value); 55 | this.cpu.addClockEvent(() => this.completeTransfer(valueIn), this.transferCycles); 56 | }; 57 | 58 | private transmissionActive = false; 59 | 60 | // Interrupts 61 | private SPI: AVRInterruptConfig = { 62 | address: this.config.spiInterrupt, 63 | flagRegister: this.config.SPSR, 64 | flagMask: SPSR_SPIF, 65 | enableRegister: this.config.SPCR, 66 | enableMask: SPCR_SPIE, 67 | }; 68 | 69 | constructor( 70 | private cpu: CPU, 71 | private config: SPIConfig, 72 | private freqHz: number, 73 | ) { 74 | const { SPCR, SPSR, SPDR } = config; 75 | cpu.writeHooks[SPDR] = (value: u8) => { 76 | if (!(cpu.data[SPCR] & SPCR_SPE)) { 77 | // SPI not enabled, ignore write 78 | return; 79 | } 80 | 81 | // Write collision 82 | if (this.transmissionActive) { 83 | cpu.data[SPSR] |= SPSR_WCOL; 84 | return true; 85 | } 86 | 87 | // Clear write collision / interrupt flags 88 | cpu.data[SPSR] &= ~SPSR_WCOL; 89 | this.cpu.clearInterrupt(this.SPI); 90 | 91 | this.transmissionActive = true; 92 | this.onByte(value); 93 | return true; 94 | }; 95 | cpu.writeHooks[SPCR] = (value: u8) => { 96 | this.cpu.updateInterruptEnable(this.SPI, value); 97 | }; 98 | cpu.writeHooks[SPSR] = (value: u8) => { 99 | this.cpu.data[SPSR] = value; 100 | this.cpu.clearInterruptByFlag(this.SPI, value); 101 | }; 102 | } 103 | 104 | reset() { 105 | this.transmissionActive = false; 106 | } 107 | 108 | /** 109 | * Completes an SPI transaction. Call this method only from the `onByte` callback. 110 | * 111 | * @param receivedByte Byte read from the SPI MISO line. 112 | */ 113 | completeTransfer(receivedByte: number) { 114 | const { SPDR } = this.config; 115 | this.cpu.data[SPDR] = receivedByte; 116 | this.cpu.setInterruptFlag(this.SPI); 117 | this.transmissionActive = false; 118 | } 119 | 120 | get isMaster() { 121 | return this.cpu.data[this.config.SPCR] & SPCR_MSTR ? true : false; 122 | } 123 | 124 | get dataOrder() { 125 | return this.cpu.data[this.config.SPCR] & SPCR_DORD ? 'lsbFirst' : 'msbFirst'; 126 | } 127 | 128 | get spiMode() { 129 | const CPHA = this.cpu.data[this.config.SPCR] & SPCR_CPHA; 130 | const CPOL = this.cpu.data[this.config.SPCR] & SPCR_CPOL; 131 | return ((CPHA ? 2 : 0) | (CPOL ? 1 : 0)) as 0 | 1 | 2 | 3; 132 | } 133 | 134 | /** 135 | * The clock divider is only relevant for Master mode 136 | */ 137 | get clockDivider() { 138 | const base = this.cpu.data[this.config.SPSR] & SPSR_SPI2X ? 2 : 4; 139 | switch (this.cpu.data[this.config.SPCR] & SPSR_SPR_MASK) { 140 | case 0b00: 141 | return base; 142 | 143 | case 0b01: 144 | return base * 4; 145 | 146 | case 0b10: 147 | return base * 16; 148 | 149 | case 0b11: 150 | return base * 32; 151 | } 152 | // We should never get here: 153 | throw new Error('Invalid divider value!'); 154 | } 155 | 156 | /** Number of cycles to complete a single byte SPI transaction */ 157 | get transferCycles() { 158 | return this.clockDivider * bitsPerByte; 159 | } 160 | 161 | /** 162 | * The SPI freqeuncy is only relevant to Master mode. 163 | * In slave mode, the frequency can be as high as F(osc) / 4. 164 | */ 165 | get spiFrequency() { 166 | return this.freqHz / this.clockDivider; 167 | } 168 | } 169 | -------------------------------------------------------------------------------- /benchmark/benchmark.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * MIT License 3 | * 4 | * Copyright (c) 2019 Miško Hevery 5 | * 6 | * Permission is hereby granted, free of charge, to any person obtaining a copy 7 | * of this software and associated documentation files (the "Software"), to deal 8 | * in the Software without restriction, including without limitation the rights 9 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | * copies of the Software, and to permit persons to whom the Software is 11 | * furnished to do so, subject to the following conditions: 12 | * 13 | * The above copyright notice and this permission notice shall be included in all 14 | * copies or substantial portions of the Software. 15 | * 16 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | * SOFTWARE. 23 | * 24 | * Source: https://github.com/mhevery/AngularConnect-2019 25 | */ 26 | 27 | // tslint:disable 28 | 29 | import { performance } from 'perf_hooks'; 30 | 31 | const MIN_SAMPLE_COUNT_NO_IMPROVEMENT = 100; 32 | const MIN_SAMPLE_DURATION = 2; 33 | 34 | const UNITS = ['ms', 'us', 'ns', 'ps']; 35 | export interface Benchmark { 36 | (versionName: string): Profile; 37 | report(fn?: (report: string) => void): void; 38 | } 39 | export interface Profile { 40 | (): boolean; 41 | profileName: string; 42 | bestTime: number; 43 | iterationCount: number; 44 | sampleCount: number; 45 | noImprovementCount: number; 46 | } 47 | 48 | export function createBenchmark(benchmarkName: string): Benchmark { 49 | const profiles: Profile[] = []; 50 | 51 | const benchmark = function Benchmark(profileName: string): Profile { 52 | let iterationCounter: number = 0; 53 | let timestamp: number = 0; 54 | const profile: Profile = function Profile(): boolean { 55 | if (iterationCounter === 0) { 56 | let runAgain = false; 57 | // if we reached the end of the iteration count than we should decide what to do next. 58 | if (timestamp === 0) { 59 | // this is the first time we are executing 60 | iterationCounter = profile.iterationCount; 61 | runAgain = true; 62 | // console.log('profiling', profileName, '...'); 63 | } else { 64 | profile.sampleCount++; 65 | // we came to an end of a sample, compute the time. 66 | const durationMs = performance.now() - timestamp; 67 | const iterationTimeMs = Math.max(durationMs / profile.iterationCount, 0); 68 | if (profile.bestTime > iterationTimeMs) { 69 | profile.bestTime = iterationTimeMs; 70 | profile.noImprovementCount = 0; 71 | runAgain = true; 72 | } else { 73 | runAgain = profile.noImprovementCount++ < MIN_SAMPLE_COUNT_NO_IMPROVEMENT; 74 | } 75 | if (durationMs < MIN_SAMPLE_DURATION) { 76 | // we have not ran for long enough so increase the iteration count. 77 | profile.iterationCount = Math.max( 78 | // As a sanity if duration_ms is 0 just double the count. 79 | profile.iterationCount << 1, 80 | // Otherwise try to guess how many iterations we have to do to get the right time. 81 | Math.round((MIN_SAMPLE_DURATION / durationMs) * profile.iterationCount), 82 | ); 83 | profile.noImprovementCount = 0; 84 | runAgain = true; 85 | } 86 | } 87 | iterationCounter = profile.iterationCount; 88 | timestamp = performance.now(); 89 | return runAgain; 90 | } else { 91 | // this is the common path and it needs te be quick! 92 | iterationCounter--; 93 | return true; 94 | } 95 | } as Profile; 96 | profile.profileName = profileName; 97 | profile.bestTime = Number.MAX_SAFE_INTEGER; 98 | profile.iterationCount = 1; 99 | profile.noImprovementCount = 0; 100 | profile.sampleCount = 0; 101 | profiles.push(profile); 102 | return profile; 103 | } as Benchmark; 104 | 105 | benchmark.report = function (fn?: (report: string) => void) { 106 | const fastest = profiles.reduce((previous: Profile, current: Profile) => { 107 | return previous.bestTime < current.bestTime ? previous : current; 108 | }); 109 | let unitOffset = 0; 110 | let time = fastest.bestTime; 111 | while (time < 1 && time !== 0) { 112 | time = time * 1000; 113 | unitOffset++; 114 | } 115 | const unit: string = UNITS[unitOffset]; 116 | (fn || console.log)( 117 | `Benchmark: ${benchmarkName}\n${profiles 118 | .map((profile: Profile) => { 119 | const time = (profile.bestTime * Math.pow(1000, unitOffset)).toFixed(3); 120 | const percent = (100 - (profile.bestTime / fastest.bestTime) * 100).toFixed(0); 121 | return ' ' + profile.profileName + ': ' + time + ' ' + unit + '(' + percent + '%)'; 122 | }) 123 | .join('\n')}`, 124 | ); 125 | }; 126 | return benchmark; 127 | } 128 | -------------------------------------------------------------------------------- /src/peripherals/twi.ts: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | // Copyright (c) Uri Shaked and contributors 3 | 4 | import { AVRInterruptConfig, CPU } from '../cpu/cpu'; 5 | import { u8 } from '../types'; 6 | 7 | export interface TWIEventHandler { 8 | start(repeated: boolean): void; 9 | 10 | stop(): void; 11 | 12 | connectToSlave(addr: u8, write: boolean): void; 13 | 14 | writeByte(value: u8): void; 15 | 16 | readByte(ack: boolean): void; 17 | } 18 | 19 | export interface TWIConfig { 20 | twiInterrupt: u8; 21 | 22 | TWBR: u8; 23 | TWCR: u8; 24 | TWSR: u8; 25 | TWDR: u8; 26 | TWAR: u8; 27 | TWAMR: u8; 28 | } 29 | 30 | /* eslint-disable @typescript-eslint/no-unused-vars */ 31 | // Register bits: 32 | const TWCR_TWINT = 0x80; // TWI Interrupt Flag 33 | const TWCR_TWEA = 0x40; // TWI Enable Acknowledge Bit 34 | const TWCR_TWSTA = 0x20; // TWI START Condition Bit 35 | const TWCR_TWSTO = 0x10; // TWI STOP Condition Bit 36 | const TWCR_TWWC = 0x8; //TWI Write Collision Flag 37 | const TWCR_TWEN = 0x4; // TWI Enable Bit 38 | const TWCR_TWIE = 0x1; // TWI Interrupt Enable 39 | const TWSR_TWS_MASK = 0xf8; // TWI Status 40 | const TWSR_TWPS1 = 0x2; // TWI Prescaler Bits 41 | const TWSR_TWPS0 = 0x1; // TWI Prescaler Bits 42 | const TWSR_TWPS_MASK = TWSR_TWPS1 | TWSR_TWPS0; // TWI Prescaler mask 43 | const TWAR_TWA_MASK = 0xfe; // TWI (Slave) Address Register 44 | const TWAR_TWGCE = 0x1; // TWI General Call Recognition Enable Bit 45 | 46 | const STATUS_BUS_ERROR = 0x0; 47 | const STATUS_TWI_IDLE = 0xf8; 48 | // Master states 49 | const STATUS_START = 0x08; 50 | const STATUS_REPEATED_START = 0x10; 51 | const STATUS_SLAW_ACK = 0x18; 52 | const STATUS_SLAW_NACK = 0x20; 53 | const STATUS_DATA_SENT_ACK = 0x28; 54 | const STATUS_DATA_SENT_NACK = 0x30; 55 | const STATUS_DATA_LOST_ARBITRATION = 0x38; 56 | const STATUS_SLAR_ACK = 0x40; 57 | const STATUS_SLAR_NACK = 0x48; 58 | const STATUS_DATA_RECEIVED_ACK = 0x50; 59 | const STATUS_DATA_RECEIVED_NACK = 0x58; 60 | // TODO: add slave states 61 | /* eslint-enable @typescript-eslint/no-unused-vars */ 62 | 63 | export const twiConfig: TWIConfig = { 64 | twiInterrupt: 0x30, 65 | TWBR: 0xb8, 66 | TWSR: 0xb9, 67 | TWAR: 0xba, 68 | TWDR: 0xbb, 69 | TWCR: 0xbc, 70 | TWAMR: 0xbd, 71 | }; 72 | 73 | // A simple TWI Event Handler that sends a NACK for all events 74 | export class NoopTWIEventHandler implements TWIEventHandler { 75 | constructor(protected twi: AVRTWI) {} 76 | 77 | start() { 78 | this.twi.completeStart(); 79 | } 80 | 81 | stop() { 82 | this.twi.completeStop(); 83 | } 84 | 85 | connectToSlave() { 86 | this.twi.completeConnect(false); 87 | } 88 | 89 | writeByte() { 90 | this.twi.completeWrite(false); 91 | } 92 | 93 | readByte() { 94 | this.twi.completeRead(0xff); 95 | } 96 | } 97 | 98 | export class AVRTWI { 99 | public eventHandler: TWIEventHandler = new NoopTWIEventHandler(this); 100 | private busy = false; 101 | 102 | // Interrupts 103 | private TWI: AVRInterruptConfig = { 104 | address: this.config.twiInterrupt, 105 | flagRegister: this.config.TWCR, 106 | flagMask: TWCR_TWINT, 107 | enableRegister: this.config.TWCR, 108 | enableMask: TWCR_TWIE, 109 | }; 110 | 111 | constructor( 112 | private cpu: CPU, 113 | private config: TWIConfig, 114 | private freqHz: number, 115 | ) { 116 | this.updateStatus(STATUS_TWI_IDLE); 117 | this.cpu.writeHooks[config.TWCR] = (value) => { 118 | this.cpu.data[config.TWCR] = value; 119 | const clearInt = value & TWCR_TWINT; 120 | this.cpu.clearInterruptByFlag(this.TWI, value); 121 | this.cpu.updateInterruptEnable(this.TWI, value); 122 | const { status } = this; 123 | if (clearInt && value & TWCR_TWEN && !this.busy) { 124 | const twdrValue = this.cpu.data[this.config.TWDR]; 125 | this.cpu.addClockEvent(() => { 126 | if (value & TWCR_TWSTA) { 127 | this.busy = true; 128 | this.eventHandler.start(status !== STATUS_TWI_IDLE); 129 | } else if (value & TWCR_TWSTO) { 130 | this.busy = true; 131 | this.eventHandler.stop(); 132 | } else if (status === STATUS_START || status === STATUS_REPEATED_START) { 133 | this.busy = true; 134 | this.eventHandler.connectToSlave(twdrValue >> 1, twdrValue & 0x1 ? false : true); 135 | } else if (status === STATUS_SLAW_ACK || status === STATUS_DATA_SENT_ACK) { 136 | this.busy = true; 137 | this.eventHandler.writeByte(twdrValue); 138 | } else if (status === STATUS_SLAR_ACK || status === STATUS_DATA_RECEIVED_ACK) { 139 | this.busy = true; 140 | const ack = !!(value & TWCR_TWEA); 141 | this.eventHandler.readByte(ack); 142 | } 143 | }, 0); 144 | return true; 145 | } 146 | }; 147 | } 148 | 149 | get prescaler() { 150 | switch (this.cpu.data[this.config.TWSR] & TWSR_TWPS_MASK) { 151 | case 0: 152 | return 1; 153 | case 1: 154 | return 4; 155 | case 2: 156 | return 16; 157 | case 3: 158 | return 64; 159 | } 160 | // We should never get here: 161 | throw new Error('Invalid prescaler value!'); 162 | } 163 | 164 | get sclFrequency() { 165 | return this.freqHz / (16 + 2 * this.cpu.data[this.config.TWBR] * this.prescaler); 166 | } 167 | 168 | completeStart() { 169 | this.busy = false; 170 | this.updateStatus(this.status === STATUS_TWI_IDLE ? STATUS_START : STATUS_REPEATED_START); 171 | } 172 | 173 | completeStop() { 174 | this.busy = false; 175 | this.cpu.data[this.config.TWCR] &= ~TWCR_TWSTO; 176 | this.updateStatus(STATUS_TWI_IDLE); 177 | } 178 | 179 | completeConnect(ack: boolean) { 180 | this.busy = false; 181 | if (this.cpu.data[this.config.TWDR] & 0x1) { 182 | this.updateStatus(ack ? STATUS_SLAR_ACK : STATUS_SLAR_NACK); 183 | } else { 184 | this.updateStatus(ack ? STATUS_SLAW_ACK : STATUS_SLAW_NACK); 185 | } 186 | } 187 | 188 | completeWrite(ack: boolean) { 189 | this.busy = false; 190 | this.updateStatus(ack ? STATUS_DATA_SENT_ACK : STATUS_DATA_SENT_NACK); 191 | } 192 | 193 | completeRead(value: u8) { 194 | this.busy = false; 195 | const ack = !!(this.cpu.data[this.config.TWCR] & TWCR_TWEA); 196 | this.cpu.data[this.config.TWDR] = value; 197 | this.updateStatus(ack ? STATUS_DATA_RECEIVED_ACK : STATUS_DATA_RECEIVED_NACK); 198 | } 199 | 200 | private get status() { 201 | return this.cpu.data[this.config.TWSR] & TWSR_TWS_MASK; 202 | } 203 | 204 | private updateStatus(value: u8) { 205 | const { TWSR } = this.config; 206 | this.cpu.data[TWSR] = (this.cpu.data[TWSR] & ~TWSR_TWS_MASK) | value; 207 | this.cpu.setInterruptFlag(this.TWI); 208 | } 209 | } 210 | -------------------------------------------------------------------------------- /src/peripherals/watchdog.spec.ts: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | // Copyright (c) Uri Shaked and contributors 3 | 4 | /** 5 | * AVR8 Watchdog Timer Test Suite 6 | * Part of AVR8js 7 | * 8 | * Copyright (C) 2021 Uri Shaked 9 | */ 10 | 11 | import { describe, expect, it } from 'vitest'; 12 | import { AVRClock, clockConfig } from '..'; 13 | import { CPU } from '../cpu/cpu'; 14 | import { asmProgram, TestProgramRunner } from '../utils/test-utils'; 15 | import { AVRWatchdog, watchdogConfig } from './watchdog'; 16 | 17 | const R20 = 20; 18 | 19 | const MCUSR = 0x54; 20 | const WDRF = 1 << 3; 21 | 22 | const WDTCSR = 0x60; 23 | const WDP0 = 1 << 0; 24 | const WDP1 = 1 << 1; 25 | const WDP2 = 1 << 2; 26 | const WDE = 1 << 3; 27 | const WDCE = 1 << 4; 28 | const WDP3 = 1 << 5; 29 | const WDIE = 1 << 6; 30 | 31 | const INT_WDT = 0xc; 32 | 33 | describe('Watchdog', () => { 34 | it('should correctly calculate the prescaler from WDTCSR', () => { 35 | const cpu = new CPU(new Uint16Array(1024)); 36 | const clock = new AVRClock(cpu, 16e6, clockConfig); 37 | const watchdog = new AVRWatchdog(cpu, watchdogConfig, clock); 38 | cpu.writeData(WDTCSR, WDCE | WDE); 39 | cpu.writeData(WDTCSR, 0); 40 | expect(watchdog.prescaler).toEqual(2048); 41 | cpu.writeData(WDTCSR, WDP2 | WDP1 | WDP0); 42 | expect(watchdog.prescaler).toEqual(256 * 1024); 43 | cpu.writeData(WDTCSR, WDP3 | WDP0); 44 | expect(watchdog.prescaler).toEqual(1024 * 1024); 45 | }); 46 | 47 | it('should not change the prescaler unless WDCE is set', () => { 48 | const cpu = new CPU(new Uint16Array(1024)); 49 | const clock = new AVRClock(cpu, 16e6, clockConfig); 50 | const watchdog = new AVRWatchdog(cpu, watchdogConfig, clock); 51 | cpu.writeData(WDTCSR, 0); 52 | expect(watchdog.prescaler).toEqual(2048); 53 | cpu.writeData(WDTCSR, WDP2 | WDP1 | WDP0); 54 | expect(watchdog.prescaler).toEqual(2048); 55 | 56 | cpu.writeData(WDTCSR, WDCE | WDE); 57 | cpu.cycles += 5; // WDCE should expire after 4 cycles 58 | cpu.writeData(WDTCSR, WDP2 | WDP1 | WDP0); 59 | expect(watchdog.prescaler).toEqual(2048); 60 | }); 61 | 62 | it('should reset the CPU when the timer expires', () => { 63 | const { program } = asmProgram(` 64 | ; register addresses 65 | _REPLACE WDTCSR, ${WDTCSR} 66 | 67 | ; Setup watchdog 68 | ldi r16, ${WDE | WDCE} 69 | sts WDTCSR, r16 70 | ldi r16, ${WDE} 71 | sts WDTCSR, r16 72 | 73 | nop 74 | 75 | break 76 | `); 77 | const cpu = new CPU(program); 78 | const clock = new AVRClock(cpu, 16e6, clockConfig); 79 | const watchdog = new AVRWatchdog(cpu, watchdogConfig, clock); 80 | const runner = new TestProgramRunner(cpu); 81 | 82 | // Setup: enable watchdog timer 83 | runner.runInstructions(4); 84 | expect(watchdog.enabled).toBe(true); 85 | 86 | // Now we skip 8ms. Watchdog shouldn't fire, yet 87 | cpu.cycles += 16000 * 8; 88 | runner.runInstructions(1); 89 | 90 | // Now we skip an extra 8ms. Watchdog should fire and reset! 91 | cpu.cycles += 16000 * 8; 92 | cpu.tick(); 93 | expect(cpu.pc).toEqual(0); 94 | expect(cpu.readData(MCUSR)).toEqual(WDRF); 95 | }); 96 | 97 | it('should extend the watchdog timeout when executing a WDR instruction', () => { 98 | const { program } = asmProgram(` 99 | ; register addresses 100 | _REPLACE WDTCSR, ${WDTCSR} 101 | 102 | ; Setup watchdog 103 | ldi r16, ${WDE | WDCE} 104 | sts WDTCSR, r16 105 | ldi r16, ${WDE} 106 | sts WDTCSR, r16 107 | 108 | wdr 109 | nop 110 | 111 | break 112 | `); 113 | const cpu = new CPU(program); 114 | const clock = new AVRClock(cpu, 16e6, clockConfig); 115 | const watchdog = new AVRWatchdog(cpu, watchdogConfig, clock); 116 | const runner = new TestProgramRunner(cpu); 117 | 118 | // Setup: enable watchdog timer 119 | runner.runInstructions(4); 120 | expect(watchdog.enabled).toBe(true); 121 | 122 | // Now we skip 8ms. Watchdog shouldn't fire, yet 123 | cpu.cycles += 16000 * 8; 124 | runner.runInstructions(1); 125 | expect(cpu.pc).not.toEqual(0); 126 | 127 | // Now we skip an extra 8ms. We extended the timeout with WDR, so watchdog won't fire yet 128 | cpu.cycles += 16000 * 8; 129 | runner.runInstructions(1); 130 | expect(cpu.pc).not.toEqual(0); 131 | 132 | // Finally, another 8ms bring us to 16ms since last WDR, and watchdog should fire 133 | cpu.cycles += 16000 * 8; 134 | cpu.tick(); 135 | expect(cpu.pc).toEqual(0); 136 | }); 137 | 138 | it('should fire an interrupt when the watchdog expires and WDIE is set', () => { 139 | const { program } = asmProgram(` 140 | ; register addresses 141 | _REPLACE WDTCSR, ${WDTCSR} 142 | 143 | ; Setup watchdog 144 | ldi r16, ${WDE | WDCE} 145 | sts WDTCSR, r16 146 | ldi r16, ${WDE | WDIE} 147 | sts WDTCSR, r16 148 | 149 | nop 150 | sei 151 | 152 | break 153 | `); 154 | const cpu = new CPU(program); 155 | const clock = new AVRClock(cpu, 16e6, clockConfig); 156 | const watchdog = new AVRWatchdog(cpu, watchdogConfig, clock); 157 | const runner = new TestProgramRunner(cpu); 158 | 159 | // Setup: enable watchdog timer 160 | runner.runInstructions(4); 161 | expect(watchdog.enabled).toBe(true); 162 | 163 | // Now we skip 8ms. Watchdog shouldn't fire, yet 164 | cpu.cycles += 16000 * 8; 165 | runner.runInstructions(1); 166 | 167 | // Now we skip an extra 8ms. Watchdog should fire and jump to the interrupt handler 168 | cpu.cycles += 16000 * 8; 169 | runner.runInstructions(1); 170 | 171 | expect(cpu.pc).toEqual(INT_WDT); 172 | // The watchdog timer should also clean the WDIE bit, so next timeout will reset the MCU. 173 | expect(cpu.readData(WDTCSR) & WDIE).toEqual(0); 174 | }); 175 | 176 | it('should not reset the CPU if the watchdog has been disabled', () => { 177 | const { program } = asmProgram(` 178 | ; register addresses 179 | _REPLACE WDTCSR, ${WDTCSR} 180 | 181 | ; Setup watchdog 182 | ldi r16, ${WDE | WDCE} 183 | sts WDTCSR, r16 184 | ldi r16, ${WDE} 185 | sts WDTCSR, r16 186 | 187 | ; disable watchdog 188 | ldi r16, ${WDE | WDCE} 189 | sts WDTCSR, r16 190 | ldi r16, 0 191 | sts WDTCSR, r16 192 | 193 | ldi r20, 55 194 | 195 | break 196 | `); 197 | const cpu = new CPU(program); 198 | const clock = new AVRClock(cpu, 16e6, clockConfig); 199 | const watchdog = new AVRWatchdog(cpu, watchdogConfig, clock); 200 | const runner = new TestProgramRunner(cpu); 201 | 202 | // Setup: enable watchdog timer 203 | runner.runInstructions(4); 204 | expect(watchdog.enabled).toBe(true); 205 | 206 | // Now we skip 8ms. Watchdog shouldn't fire, yet. We disable it. 207 | cpu.cycles += 16000 * 8; 208 | runner.runInstructions(4); 209 | 210 | // Now we skip an extra 20ms. Watchdog shouldn't reset! 211 | cpu.cycles += 16000 * 20; 212 | runner.runInstructions(1); 213 | expect(cpu.pc).not.toEqual(0); 214 | expect(cpu.data[R20]).toEqual(55); // assert that `ldi r20, 55` ran 215 | }); 216 | }); 217 | -------------------------------------------------------------------------------- /src/peripherals/adc.ts: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | // Copyright (c) Uri Shaked and contributors 3 | 4 | /** 5 | * AVR-8 ADC 6 | * Part of AVR8js 7 | * Reference: http://ww1.microchip.com/downloads/en/DeviceDoc/ATmega48A-PA-88A-PA-168A-PA-328-P-DS-DS40002061A.pdf 8 | * 9 | * Copyright (C) 2019, 2020, 2021 Uri Shaked 10 | */ 11 | 12 | import { AVRInterruptConfig, CPU } from '../cpu/cpu'; 13 | import { u8 } from '../types'; 14 | 15 | export enum ADCReference { 16 | AVCC, 17 | AREF, 18 | Internal1V1, 19 | Internal2V56, 20 | Reserved, 21 | } 22 | 23 | export enum ADCMuxInputType { 24 | SingleEnded, 25 | Differential, 26 | Constant, 27 | Temperature, 28 | } 29 | 30 | export type ADCMuxInput = 31 | | { type: ADCMuxInputType.Temperature } 32 | | { type: ADCMuxInputType.Constant; voltage: number } 33 | | { type: ADCMuxInputType.SingleEnded; channel: number } 34 | | { 35 | type: ADCMuxInputType.Differential; 36 | positiveChannel: number; 37 | negativeChannel: number; 38 | gain: number; 39 | }; 40 | 41 | export type ADCMuxConfiguration = { [key: number]: ADCMuxInput }; 42 | 43 | export interface ADCConfig { 44 | ADMUX: u8; 45 | ADCSRA: u8; 46 | ADCSRB: u8; 47 | ADCL: u8; 48 | ADCH: u8; 49 | DIDR0: u8; 50 | adcInterrupt: u8; 51 | numChannels: u8; 52 | muxInputMask: u8; 53 | muxChannels: ADCMuxConfiguration; 54 | adcReferences: ADCReference[]; 55 | } 56 | 57 | export const atmega328Channels: ADCMuxConfiguration = { 58 | 0: { type: ADCMuxInputType.SingleEnded, channel: 0 }, 59 | 1: { type: ADCMuxInputType.SingleEnded, channel: 1 }, 60 | 2: { type: ADCMuxInputType.SingleEnded, channel: 2 }, 61 | 3: { type: ADCMuxInputType.SingleEnded, channel: 3 }, 62 | 4: { type: ADCMuxInputType.SingleEnded, channel: 4 }, 63 | 5: { type: ADCMuxInputType.SingleEnded, channel: 5 }, 64 | 6: { type: ADCMuxInputType.SingleEnded, channel: 6 }, 65 | 7: { type: ADCMuxInputType.SingleEnded, channel: 7 }, 66 | 8: { type: ADCMuxInputType.Temperature }, 67 | 14: { type: ADCMuxInputType.Constant, voltage: 1.1 }, 68 | 15: { type: ADCMuxInputType.Constant, voltage: 0 }, 69 | }; 70 | 71 | const fallbackMuxInput = { 72 | type: ADCMuxInputType.Constant, 73 | voltage: 0, 74 | }; 75 | 76 | export const adcConfig: ADCConfig = { 77 | ADMUX: 0x7c, 78 | ADCSRA: 0x7a, 79 | ADCSRB: 0x7b, 80 | ADCL: 0x78, 81 | ADCH: 0x79, 82 | DIDR0: 0x7e, 83 | adcInterrupt: 0x2a, 84 | numChannels: 8, 85 | muxInputMask: 0xf, 86 | muxChannels: atmega328Channels, 87 | adcReferences: [ 88 | ADCReference.AREF, 89 | ADCReference.AVCC, 90 | ADCReference.Reserved, 91 | ADCReference.Internal1V1, 92 | ], 93 | }; 94 | 95 | // Register bits: 96 | const ADPS_MASK = 0x7; 97 | const ADIE = 0x8; 98 | const ADIF = 0x10; 99 | const ADSC = 0x40; 100 | const ADEN = 0x80; 101 | 102 | const MUX_MASK = 0x1f; 103 | const ADLAR = 0x20; 104 | const MUX5 = 0x8; 105 | const REFS2 = 0x8; 106 | const REFS_MASK = 0x3; 107 | const REFS_SHIFT = 6; 108 | 109 | export class AVRADC { 110 | /** 111 | * ADC Channel values, in voltage (0..5). The number of channels depends on the chip. 112 | * 113 | * Changing the values here will change the ADC reading, unless you override onADCRead() with a custom implementation. 114 | */ 115 | readonly channelValues = new Array(this.config.numChannels); 116 | 117 | /** AVCC Reference voltage */ 118 | avcc = 5; 119 | 120 | /** AREF Reference voltage */ 121 | aref = 5; 122 | 123 | /** 124 | * Invoked whenever the code performs an ADC read. 125 | * 126 | * The default implementation reads the result from the `channelValues` array, and then calls 127 | * `completeADCRead()` after `sampleCycles` CPU cycles. 128 | * 129 | * If you override the default implementation, make sure to call `completeADCRead()` after 130 | * `sampleCycles` cycles (or else the ADC read will never complete). 131 | */ 132 | onADCRead: (input: ADCMuxInput) => void = (input) => { 133 | // Default implementation 134 | let voltage = 0; 135 | switch (input.type) { 136 | case ADCMuxInputType.Constant: 137 | voltage = input.voltage; 138 | break; 139 | case ADCMuxInputType.SingleEnded: 140 | voltage = this.channelValues[input.channel] ?? 0; 141 | break; 142 | case ADCMuxInputType.Differential: 143 | voltage = 144 | input.gain * 145 | ((this.channelValues[input.positiveChannel] || 0) - 146 | (this.channelValues[input.negativeChannel] || 0)); 147 | break; 148 | case ADCMuxInputType.Temperature: 149 | voltage = 0.378125; // 25 celcius 150 | break; 151 | } 152 | const rawValue = (voltage / this.referenceVoltage) * 1024; 153 | const result = Math.min(Math.max(Math.floor(rawValue), 0), 1023); 154 | this.cpu.addClockEvent(() => this.completeADCRead(result), this.sampleCycles); 155 | }; 156 | 157 | private converting = false; 158 | private conversionCycles = 25; 159 | 160 | // Interrupts 161 | private ADC: AVRInterruptConfig = { 162 | address: this.config.adcInterrupt, 163 | flagRegister: this.config.ADCSRA, 164 | flagMask: ADIF, 165 | enableRegister: this.config.ADCSRA, 166 | enableMask: ADIE, 167 | }; 168 | 169 | constructor( 170 | private cpu: CPU, 171 | private config: ADCConfig, 172 | ) { 173 | cpu.writeHooks[config.ADCSRA] = (value, oldValue) => { 174 | if (value & ADEN && !(oldValue && ADEN)) { 175 | this.conversionCycles = 25; 176 | } 177 | cpu.data[config.ADCSRA] = value; 178 | cpu.updateInterruptEnable(this.ADC, value); 179 | if (!this.converting && value & ADSC) { 180 | if (!(value & ADEN)) { 181 | // Special case: reading while the ADC is not enabled should return 0 182 | this.cpu.addClockEvent(() => this.completeADCRead(0), this.sampleCycles); 183 | return true; 184 | } 185 | let channel = this.cpu.data[this.config.ADMUX] & MUX_MASK; 186 | if (cpu.data[config.ADCSRB] & MUX5) { 187 | channel |= 0x20; 188 | } 189 | channel &= config.muxInputMask; 190 | const muxInput = config.muxChannels[channel] ?? fallbackMuxInput; 191 | this.converting = true; 192 | this.onADCRead(muxInput); 193 | return true; // don't update 194 | } 195 | }; 196 | } 197 | 198 | completeADCRead(value: number) { 199 | const { ADCL, ADCH, ADMUX, ADCSRA } = this.config; 200 | this.converting = false; 201 | this.conversionCycles = 13; 202 | if (this.cpu.data[ADMUX] & ADLAR) { 203 | this.cpu.data[ADCL] = (value << 6) & 0xff; 204 | this.cpu.data[ADCH] = value >> 2; 205 | } else { 206 | this.cpu.data[ADCL] = value & 0xff; 207 | this.cpu.data[ADCH] = (value >> 8) & 0x3; 208 | } 209 | this.cpu.data[ADCSRA] &= ~ADSC; 210 | this.cpu.setInterruptFlag(this.ADC); 211 | } 212 | 213 | get prescaler() { 214 | const { ADCSRA } = this.config; 215 | const adcsra = this.cpu.data[ADCSRA]; 216 | const adps = adcsra & ADPS_MASK; 217 | switch (adps) { 218 | case 0: 219 | case 1: 220 | return 2; 221 | case 2: 222 | return 4; 223 | case 3: 224 | return 8; 225 | case 4: 226 | return 16; 227 | case 5: 228 | return 32; 229 | case 6: 230 | return 64; 231 | case 7: 232 | default: 233 | return 128; 234 | } 235 | } 236 | 237 | get referenceVoltageType() { 238 | const { ADMUX, adcReferences } = this.config; 239 | let refs = (this.cpu.data[ADMUX] >> REFS_SHIFT) & REFS_MASK; 240 | if (adcReferences.length > 4 && this.cpu.data[ADMUX] & REFS2) { 241 | refs |= 0x4; 242 | } 243 | return adcReferences[refs] ?? ADCReference.Reserved; 244 | } 245 | 246 | get referenceVoltage() { 247 | switch (this.referenceVoltageType) { 248 | case ADCReference.AVCC: 249 | return this.avcc; 250 | case ADCReference.AREF: 251 | return this.aref; 252 | case ADCReference.Internal1V1: 253 | return 1.1; 254 | case ADCReference.Internal2V56: 255 | return 2.56; 256 | default: 257 | return this.avcc; 258 | } 259 | } 260 | 261 | get sampleCycles() { 262 | return this.conversionCycles * this.prescaler; 263 | } 264 | } 265 | -------------------------------------------------------------------------------- /src/cpu/cpu.ts: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | // Copyright (c) Uri Shaked and contributors 3 | 4 | /** 5 | * AVR 8 CPU data structures 6 | * Part of AVR8js 7 | * 8 | * Copyright (C) 2019, Uri Shaked 9 | */ 10 | 11 | import { AVRIOPort } from '../peripherals/gpio'; 12 | import { u32, u16, u8, i16 } from '../types'; 13 | import { avrInterrupt } from './interrupt'; 14 | 15 | const registerSpace = 0x100; 16 | const MAX_INTERRUPTS = 128; // Enough for ATMega2560 17 | 18 | export type CPUMemoryHook = (value: u8, oldValue: u8, addr: u16, mask: u8) => boolean | void; 19 | export interface CPUMemoryHooks { 20 | [key: number]: CPUMemoryHook; 21 | } 22 | 23 | export type CPUMemoryReadHook = (addr: u16) => u8; 24 | export interface CPUMemoryReadHooks { 25 | [key: number]: CPUMemoryReadHook; 26 | } 27 | 28 | export interface AVRInterruptConfig { 29 | address: u8; 30 | enableRegister: u16; 31 | enableMask: u8; 32 | flagRegister: u16; 33 | flagMask: u8; 34 | constant?: boolean; 35 | inverseFlag?: boolean; 36 | } 37 | 38 | export type AVRClockEventCallback = () => void; 39 | 40 | interface AVRClockEventEntry { 41 | cycles: number; 42 | callback: AVRClockEventCallback; 43 | next: AVRClockEventEntry | null; 44 | } 45 | 46 | export class CPU { 47 | readonly data: Uint8Array = new Uint8Array(this.sramBytes + registerSpace); 48 | readonly data16 = new Uint16Array(this.data.buffer); 49 | readonly dataView = new DataView(this.data.buffer); 50 | readonly progBytes = new Uint8Array(this.progMem.buffer); 51 | readonly readHooks: CPUMemoryReadHooks = []; 52 | readonly writeHooks: CPUMemoryHooks = []; 53 | private readonly pendingInterrupts: (AVRInterruptConfig | null)[] = new Array(MAX_INTERRUPTS); 54 | private nextClockEvent: AVRClockEventEntry | null = null; 55 | private readonly clockEventPool: AVRClockEventEntry[] = []; // helps avoid garbage collection 56 | 57 | /** 58 | * Whether the program counter (PC) can address 22 bits (the default is 16) 59 | */ 60 | readonly pc22Bits = this.progBytes.length > 0x20000; 61 | 62 | readonly gpioPorts = new Set(); 63 | readonly gpioByPort: AVRIOPort[] = []; 64 | 65 | /** 66 | * This function is called by the WDR instruction. The Watchdog peripheral attaches 67 | * to it to listen for WDR (watchdog reset). 68 | */ 69 | onWatchdogReset = () => { 70 | /* empty by default */ 71 | }; 72 | 73 | /** 74 | * Program counter 75 | */ 76 | pc: u32 = 0; 77 | 78 | /** 79 | * Clock cycle counter 80 | */ 81 | cycles = 0; 82 | 83 | nextInterrupt: i16 = -1; 84 | maxInterrupt: i16 = 0; 85 | 86 | constructor( 87 | public progMem: Uint16Array, 88 | private sramBytes = 8192, 89 | ) { 90 | this.reset(); 91 | } 92 | 93 | reset() { 94 | this.SP = this.data.length - 1; 95 | this.pc = 0; 96 | this.pendingInterrupts.fill(null); 97 | this.nextInterrupt = -1; 98 | this.nextClockEvent = null; 99 | } 100 | 101 | readData(addr: number) { 102 | if (addr >= 32 && this.readHooks[addr]) { 103 | return this.readHooks[addr](addr); 104 | } 105 | return this.data[addr]; 106 | } 107 | 108 | writeData(addr: number, value: number, mask = 0xff) { 109 | const hook = this.writeHooks[addr]; 110 | if (hook) { 111 | if (hook(value, this.data[addr], addr, mask)) { 112 | return; 113 | } 114 | } 115 | this.data[addr] = value; 116 | } 117 | 118 | get SP() { 119 | return this.dataView.getUint16(93, true); 120 | } 121 | 122 | set SP(value: number) { 123 | this.dataView.setUint16(93, value, true); 124 | } 125 | 126 | get SREG() { 127 | return this.data[95]; 128 | } 129 | 130 | get interruptsEnabled() { 131 | return this.SREG & 0x80 ? true : false; 132 | } 133 | 134 | setInterruptFlag(interrupt: AVRInterruptConfig) { 135 | const { flagRegister, flagMask, enableRegister, enableMask } = interrupt; 136 | if (interrupt.inverseFlag) { 137 | this.data[flagRegister] &= ~flagMask; 138 | } else { 139 | this.data[flagRegister] |= flagMask; 140 | } 141 | if (this.data[enableRegister] & enableMask) { 142 | this.queueInterrupt(interrupt); 143 | } 144 | } 145 | 146 | updateInterruptEnable(interrupt: AVRInterruptConfig, registerValue: u8) { 147 | const { enableMask, flagRegister, flagMask, inverseFlag } = interrupt; 148 | if (registerValue & enableMask) { 149 | const bitSet = this.data[flagRegister] & flagMask; 150 | if (inverseFlag ? !bitSet : bitSet) { 151 | this.queueInterrupt(interrupt); 152 | } 153 | } else { 154 | this.clearInterrupt(interrupt, false); 155 | } 156 | } 157 | 158 | queueInterrupt(interrupt: AVRInterruptConfig) { 159 | const { address } = interrupt; 160 | this.pendingInterrupts[address] = interrupt; 161 | if (this.nextInterrupt === -1 || this.nextInterrupt > address) { 162 | this.nextInterrupt = address; 163 | } 164 | if (address > this.maxInterrupt) { 165 | this.maxInterrupt = address; 166 | } 167 | } 168 | 169 | clearInterrupt({ address, flagRegister, flagMask }: AVRInterruptConfig, clearFlag = true) { 170 | if (clearFlag) { 171 | this.data[flagRegister] &= ~flagMask; 172 | } 173 | const { pendingInterrupts, maxInterrupt } = this; 174 | if (!pendingInterrupts[address]) { 175 | return; 176 | } 177 | pendingInterrupts[address] = null; 178 | if (this.nextInterrupt === address) { 179 | this.nextInterrupt = -1; 180 | for (let i = address + 1; i <= maxInterrupt; i++) { 181 | if (pendingInterrupts[i]) { 182 | this.nextInterrupt = i; 183 | break; 184 | } 185 | } 186 | } 187 | } 188 | 189 | clearInterruptByFlag(interrupt: AVRInterruptConfig, registerValue: number) { 190 | const { flagRegister, flagMask } = interrupt; 191 | if (registerValue & flagMask) { 192 | this.data[flagRegister] &= ~flagMask; 193 | this.clearInterrupt(interrupt); 194 | } 195 | } 196 | 197 | addClockEvent(callback: AVRClockEventCallback, cycles: number) { 198 | const { clockEventPool } = this; 199 | cycles = this.cycles + Math.max(1, cycles); 200 | const maybeEntry = clockEventPool.pop(); 201 | const entry: AVRClockEventEntry = maybeEntry ?? { cycles, callback, next: null }; 202 | entry.cycles = cycles; 203 | entry.callback = callback; 204 | let { nextClockEvent: clockEvent } = this; 205 | let lastItem = null; 206 | while (clockEvent && clockEvent.cycles < cycles) { 207 | lastItem = clockEvent; 208 | clockEvent = clockEvent.next; 209 | } 210 | if (lastItem) { 211 | lastItem.next = entry; 212 | entry.next = clockEvent; 213 | } else { 214 | this.nextClockEvent = entry; 215 | entry.next = clockEvent; 216 | } 217 | return callback; 218 | } 219 | 220 | updateClockEvent(callback: AVRClockEventCallback, cycles: number) { 221 | if (this.clearClockEvent(callback)) { 222 | this.addClockEvent(callback, cycles); 223 | return true; 224 | } 225 | return false; 226 | } 227 | 228 | clearClockEvent(callback: AVRClockEventCallback) { 229 | let { nextClockEvent: clockEvent } = this; 230 | if (!clockEvent) { 231 | return false; 232 | } 233 | const { clockEventPool } = this; 234 | let lastItem = null; 235 | while (clockEvent) { 236 | if (clockEvent.callback === callback) { 237 | if (lastItem) { 238 | lastItem.next = clockEvent.next; 239 | } else { 240 | this.nextClockEvent = clockEvent.next; 241 | } 242 | if (clockEventPool.length < 10) { 243 | clockEventPool.push(clockEvent); 244 | } 245 | return true; 246 | } 247 | lastItem = clockEvent; 248 | clockEvent = clockEvent.next; 249 | } 250 | return false; 251 | } 252 | 253 | tick() { 254 | const { nextClockEvent } = this; 255 | if (nextClockEvent && nextClockEvent.cycles <= this.cycles) { 256 | nextClockEvent.callback(); 257 | this.nextClockEvent = nextClockEvent.next; 258 | if (this.clockEventPool.length < 10) { 259 | this.clockEventPool.push(nextClockEvent); 260 | } 261 | } 262 | 263 | const { nextInterrupt } = this; 264 | if (this.interruptsEnabled && nextInterrupt >= 0) { 265 | // eslint-disable-next-line @typescript-eslint/no-non-null-assertion 266 | const interrupt = this.pendingInterrupts[nextInterrupt]!; 267 | avrInterrupt(this, interrupt.address); 268 | if (!interrupt.constant) { 269 | this.clearInterrupt(interrupt); 270 | } 271 | } 272 | } 273 | } 274 | -------------------------------------------------------------------------------- /src/peripherals/spi.spec.ts: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | // Copyright (c) Uri Shaked and contributors 3 | 4 | import { describe, expect, it, vi } from 'vitest'; 5 | import { CPU } from '../cpu/cpu'; 6 | import { asmProgram, TestProgramRunner } from '../utils/test-utils'; 7 | import { AVRSPI, spiConfig } from './spi'; 8 | 9 | const FREQ_16MHZ = 16e6; 10 | 11 | // CPU registers 12 | const R17 = 17; 13 | const SREG = 95; 14 | 15 | // SPI Registers 16 | const SPCR = 0x4c; 17 | const SPSR = 0x4d; 18 | const SPDR = 0x4e; 19 | 20 | // Register bit names 21 | const SPR0 = 1; 22 | const SPR1 = 2; 23 | const CPOL = 4; 24 | const CPHA = 8; 25 | const MSTR = 0x10; 26 | const DORD = 0x20; 27 | const SPE = 0x40; 28 | const SPIE = 0x80; 29 | const WCOL = 0x40; 30 | const SPIF = 0x80; 31 | const SPI2X = 1; 32 | 33 | describe('SPI', () => { 34 | it('should correctly calculate the frequency based on SPCR/SPST values', () => { 35 | const cpu = new CPU(new Uint16Array(1024)); 36 | const spi = new AVRSPI(cpu, spiConfig, FREQ_16MHZ); 37 | 38 | // Values in this test are based on Table 19-5 in the datasheet, page 177: 39 | // http://ww1.microchip.com/downloads/en/DeviceDoc/ATmega48A-PA-88A-PA-168A-PA-328-P-DS-DS40002061A.pdf 40 | 41 | // Standard SPI speed: 42 | cpu.writeData(SPSR, 0); 43 | cpu.writeData(SPCR, 0); 44 | expect(spi.spiFrequency).toEqual(FREQ_16MHZ / 4); 45 | cpu.writeData(SPCR, SPR0); 46 | expect(spi.spiFrequency).toEqual(FREQ_16MHZ / 16); 47 | cpu.writeData(SPCR, SPR1); 48 | expect(spi.spiFrequency).toEqual(FREQ_16MHZ / 64); 49 | cpu.writeData(SPCR, SPR1 | SPR0); 50 | expect(spi.spiFrequency).toEqual(FREQ_16MHZ / 128); 51 | 52 | // Double SPI speed: 53 | cpu.writeData(SPSR, SPI2X); 54 | cpu.writeData(SPCR, 0); 55 | expect(spi.spiFrequency).toEqual(FREQ_16MHZ / 2); 56 | cpu.writeData(SPCR, SPR0); 57 | expect(spi.spiFrequency).toEqual(FREQ_16MHZ / 8); 58 | cpu.writeData(SPCR, SPR1); 59 | expect(spi.spiFrequency).toEqual(FREQ_16MHZ / 32); 60 | cpu.writeData(SPCR, SPR1 | SPR0); 61 | expect(spi.spiFrequency).toEqual(FREQ_16MHZ / 64); 62 | }); 63 | 64 | it('should correctly report the data order (MSB/LSB first), based on SPCR value', () => { 65 | const cpu = new CPU(new Uint16Array(1024)); 66 | const spi = new AVRSPI(cpu, spiConfig, FREQ_16MHZ); 67 | 68 | cpu.writeData(SPCR, 0); 69 | expect(spi.dataOrder).toBe('msbFirst'); 70 | 71 | cpu.writeData(SPCR, DORD); 72 | expect(spi.dataOrder).toBe('lsbFirst'); 73 | }); 74 | 75 | it('should correctly report the SPI mode, based on SPCR value', () => { 76 | const cpu = new CPU(new Uint16Array(1024)); 77 | const spi = new AVRSPI(cpu, spiConfig, FREQ_16MHZ); 78 | 79 | // Values in this test are based on Table 2 in the datasheet, page 174. 80 | cpu.writeData(SPCR, 0); 81 | expect(spi.spiMode).toBe(0); 82 | 83 | cpu.writeData(SPCR, CPHA); 84 | expect(spi.spiMode).toBe(1); 85 | 86 | cpu.writeData(SPCR, CPOL); 87 | expect(spi.spiMode).toBe(2); 88 | 89 | cpu.writeData(SPCR, CPOL | CPHA); 90 | expect(spi.spiMode).toBe(3); 91 | }); 92 | 93 | it('should indicate slave/master operation, based on SPCR value', () => { 94 | const cpu = new CPU(new Uint16Array(1024)); 95 | const spi = new AVRSPI(cpu, spiConfig, FREQ_16MHZ); 96 | 97 | expect(spi.isMaster).toBe(false); 98 | 99 | cpu.writeData(SPCR, MSTR); 100 | expect(spi.isMaster).toBe(true); 101 | }); 102 | 103 | it('should call the `onByteTransfer` callback when initiating an SPI trasfer by writing to SPDR', () => { 104 | const cpu = new CPU(new Uint16Array(1024)); 105 | const spi = new AVRSPI(cpu, spiConfig, FREQ_16MHZ); 106 | spi.onByte = vi.fn(); 107 | 108 | cpu.writeData(SPCR, SPE | MSTR); 109 | cpu.writeData(SPDR, 0x8f); 110 | 111 | expect(spi.onByte).toHaveBeenCalledWith(0x8f); 112 | }); 113 | 114 | it('should ignore SPDR writes when the SPE bit in SPCR is clear', () => { 115 | const cpu = new CPU(new Uint16Array(1024)); 116 | const spi = new AVRSPI(cpu, spiConfig, FREQ_16MHZ); 117 | spi.onByte = vi.fn(); 118 | 119 | cpu.writeData(SPCR, MSTR); 120 | cpu.writeData(SPDR, 0x8f); 121 | 122 | expect(spi.onByte).not.toHaveBeenCalled(); 123 | }); 124 | 125 | it('should transmit a byte successfully (integration)', () => { 126 | // Based on code example from section 19.2 of the datasheet, page 172 127 | const { program } = asmProgram(` 128 | ; register addresses 129 | _REPLACE SPCR, ${SPCR - 0x20} 130 | _REPLACE SPDR, ${SPDR - 0x20} 131 | _REPLACE SPSR, ${SPSR - 0x20} 132 | _REPLACE DDR_SPI, 0x4 ; PORTB 133 | 134 | SPI_MasterInit: 135 | ; Set MOSI and SCK output, all others input 136 | LDI r17, 0x28 137 | OUT DDR_SPI, r17 138 | 139 | ; Enable SPI, Master, set clock rate fck/16 140 | LDI r17, 0x51 ; (1< { 163 | byteReceivedFromAsmCode = value; 164 | cpu.addClockEvent(() => spi.completeTransfer(0x5b), spi.transferCycles); 165 | }; 166 | 167 | const runner = new TestProgramRunner(cpu, () => 0); 168 | runner.runToBreak(); 169 | 170 | // 16 cycles per clock * 8 bits = 128 171 | expect(cpu.cycles).toBeGreaterThanOrEqual(128); 172 | 173 | expect(byteReceivedFromAsmCode).toEqual(0xb8); 174 | expect(cpu.data[R17]).toEqual(0x5b); 175 | }); 176 | 177 | it('should set the WCOL bit in SPSR if writing to SPDR while SPI is already transmitting', () => { 178 | const cpu = new CPU(new Uint16Array(1024)); 179 | new AVRSPI(cpu, spiConfig, FREQ_16MHZ); 180 | 181 | cpu.writeData(SPCR, SPE | MSTR); 182 | cpu.writeData(SPDR, 0x50); 183 | cpu.tick(); 184 | expect(cpu.readData(SPSR) & WCOL).toEqual(0); 185 | 186 | cpu.writeData(SPDR, 0x51); 187 | expect(cpu.readData(SPSR) & WCOL).toEqual(WCOL); 188 | }); 189 | 190 | it('should clear the SPIF bit and fire an interrupt when SPI transfer completes', () => { 191 | const cpu = new CPU(new Uint16Array(1024)); 192 | new AVRSPI(cpu, spiConfig, FREQ_16MHZ); 193 | 194 | cpu.writeData(SPCR, SPE | SPIE | MSTR); 195 | cpu.writeData(SPDR, 0x50); 196 | cpu.data[SREG] = 0x80; // SREG: I------- 197 | 198 | // At this point, write shouldn't be complete yet 199 | cpu.cycles += 10; 200 | cpu.tick(); 201 | expect(cpu.pc).toEqual(0); 202 | 203 | // 100 cycles later, it should (8 bits * 8 cycles per bit = 64). 204 | cpu.cycles += 100; 205 | cpu.tick(); 206 | expect(cpu.data[SPSR] & SPIF).toEqual(0); 207 | expect(cpu.pc).toEqual(0x22); // SPI Ready interrupt 208 | }); 209 | 210 | it('should fire a pending SPI interrupt when SPIE flag is set', () => { 211 | const cpu = new CPU(new Uint16Array(1024)); 212 | new AVRSPI(cpu, spiConfig, FREQ_16MHZ); 213 | 214 | cpu.writeData(SPCR, SPE | MSTR); 215 | cpu.writeData(SPDR, 0x50); 216 | cpu.data[SREG] = 0x80; // SREG: I------- 217 | 218 | // Wait for transfer to complete (8 bits * 8 cycles per bit = 64). 219 | cpu.cycles += 64; 220 | cpu.tick(); 221 | 222 | expect(cpu.data[SPSR] & SPIF).toEqual(SPIF); 223 | expect(cpu.pc).toEqual(0); // Interrupt not taken (yet) 224 | 225 | // Enable the interrupt (SPIE) 226 | cpu.writeData(SPCR, SPE | MSTR | SPIE); 227 | cpu.tick(); 228 | expect(cpu.pc).toEqual(0x22); // SPI Ready interrupt 229 | expect(cpu.data[SPSR] & SPIF).toEqual(0); 230 | }); 231 | 232 | it('should should only update SPDR when tranfer finishes (double buffering)', () => { 233 | const cpu = new CPU(new Uint16Array(1024)); 234 | const spi = new AVRSPI(cpu, spiConfig, FREQ_16MHZ); 235 | spi.onByte = () => { 236 | cpu.addClockEvent(() => spi.completeTransfer(0x88), spi.transferCycles); 237 | }; 238 | 239 | cpu.writeData(SPCR, SPE | MSTR); 240 | cpu.writeData(SPDR, 0x8f); 241 | 242 | cpu.cycles = 10; 243 | cpu.tick(); 244 | expect(cpu.readData(SPDR)).toEqual(0); 245 | 246 | cpu.cycles = 32; // 4 cycles per bit * 8 bits = 32 247 | cpu.tick(); 248 | expect(cpu.readData(SPDR)).toEqual(0x88); 249 | }); 250 | }); 251 | -------------------------------------------------------------------------------- /src/peripherals/usart.ts: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | // Copyright (c) Uri Shaked and contributors 3 | 4 | /** 5 | * AVR-8 USART Peripheral 6 | * Part of AVR8js 7 | * Reference: http://ww1.microchip.com/downloads/en/DeviceDoc/ATmega48A-PA-88A-PA-168A-PA-328-P-DS-DS40002061A.pdf 8 | * 9 | * Copyright (C) 2019, 2020, 2021 Uri Shaked 10 | */ 11 | 12 | import { AVRInterruptConfig, CPU } from '../cpu/cpu'; 13 | import { u8 } from '../types'; 14 | 15 | export interface USARTConfig { 16 | rxCompleteInterrupt: u8; 17 | dataRegisterEmptyInterrupt: u8; 18 | txCompleteInterrupt: u8; 19 | 20 | UCSRA: u8; 21 | UCSRB: u8; 22 | UCSRC: u8; 23 | UBRRL: u8; 24 | UBRRH: u8; 25 | UDR: u8; 26 | } 27 | 28 | export const usart0Config: USARTConfig = { 29 | rxCompleteInterrupt: 0x24, 30 | dataRegisterEmptyInterrupt: 0x26, 31 | txCompleteInterrupt: 0x28, 32 | UCSRA: 0xc0, 33 | UCSRB: 0xc1, 34 | UCSRC: 0xc2, 35 | UBRRL: 0xc4, 36 | UBRRH: 0xc5, 37 | UDR: 0xc6, 38 | }; 39 | 40 | export type USARTTransmitCallback = (value: u8) => void; 41 | export type USARTLineTransmitCallback = (value: string) => void; 42 | export type USARTConfigurationChangeCallback = () => void; 43 | 44 | /* eslint-disable @typescript-eslint/no-unused-vars */ 45 | // Register bits: 46 | const UCSRA_RXC = 0x80; // USART Receive Complete 47 | const UCSRA_TXC = 0x40; // USART Transmit Complete 48 | const UCSRA_UDRE = 0x20; // USART Data Register Empty 49 | const UCSRA_FE = 0x10; // Frame Error 50 | const UCSRA_DOR = 0x8; // Data OverRun 51 | const UCSRA_UPE = 0x4; // USART Parity Error 52 | const UCSRA_U2X = 0x2; // Double the USART Transmission Speed 53 | const UCSRA_MPCM = 0x1; // Multi-processor Communication Mode 54 | const UCSRA_CFG_MASK = UCSRA_U2X; 55 | const UCSRB_RXCIE = 0x80; // RX Complete Interrupt Enable 56 | const UCSRB_TXCIE = 0x40; // TX Complete Interrupt Enable 57 | const UCSRB_UDRIE = 0x20; // USART Data Register Empty Interrupt Enable 58 | const UCSRB_RXEN = 0x10; // Receiver Enable 59 | const UCSRB_TXEN = 0x8; // Transmitter Enable 60 | const UCSRB_UCSZ2 = 0x4; // Character Size 2 61 | const UCSRB_RXB8 = 0x2; // Receive Data Bit 8 62 | const UCSRB_TXB8 = 0x1; // Transmit Data Bit 8 63 | const UCSRB_CFG_MASK = UCSRB_UCSZ2 | UCSRB_RXEN | UCSRB_TXEN; 64 | const UCSRC_UMSEL1 = 0x80; // USART Mode Select 1 65 | const UCSRC_UMSEL0 = 0x40; // USART Mode Select 0 66 | const UCSRC_UPM1 = 0x20; // Parity Mode 1 67 | const UCSRC_UPM0 = 0x10; // Parity Mode 0 68 | const UCSRC_USBS = 0x8; // Stop Bit Select 69 | const UCSRC_UCSZ1 = 0x4; // Character Size 1 70 | const UCSRC_UCSZ0 = 0x2; // Character Size 0 71 | const UCSRC_UCPOL = 0x1; // Clock Polarity 72 | /* eslint-enable @typescript-eslint/no-unused-vars */ 73 | 74 | const rxMasks = { 75 | 5: 0x1f, 76 | 6: 0x3f, 77 | 7: 0x7f, 78 | 8: 0xff, 79 | 9: 0xff, 80 | }; 81 | export class AVRUSART { 82 | public onByteTransmit: USARTTransmitCallback | null = null; 83 | public onLineTransmit: USARTLineTransmitCallback | null = null; 84 | public onRxComplete: (() => void) | null = null; 85 | public onConfigurationChange: USARTConfigurationChangeCallback | null = null; 86 | 87 | private rxBusyValue = false; 88 | private rxByte = 0; 89 | private lineBuffer = ''; 90 | 91 | // Interrupts 92 | private RXC: AVRInterruptConfig = { 93 | address: this.config.rxCompleteInterrupt, 94 | flagRegister: this.config.UCSRA, 95 | flagMask: UCSRA_RXC, 96 | enableRegister: this.config.UCSRB, 97 | enableMask: UCSRB_RXCIE, 98 | constant: true, 99 | }; 100 | private UDRE: AVRInterruptConfig = { 101 | address: this.config.dataRegisterEmptyInterrupt, 102 | flagRegister: this.config.UCSRA, 103 | flagMask: UCSRA_UDRE, 104 | enableRegister: this.config.UCSRB, 105 | enableMask: UCSRB_UDRIE, 106 | }; 107 | private TXC: AVRInterruptConfig = { 108 | address: this.config.txCompleteInterrupt, 109 | flagRegister: this.config.UCSRA, 110 | flagMask: UCSRA_TXC, 111 | enableRegister: this.config.UCSRB, 112 | enableMask: UCSRB_TXCIE, 113 | }; 114 | 115 | constructor( 116 | private cpu: CPU, 117 | private config: USARTConfig, 118 | private freqHz: number, 119 | ) { 120 | this.reset(); 121 | this.cpu.writeHooks[config.UCSRA] = (value, oldValue) => { 122 | cpu.data[config.UCSRA] = value & (UCSRA_MPCM | UCSRA_U2X); 123 | cpu.clearInterruptByFlag(this.TXC, value); 124 | if ((value & UCSRA_CFG_MASK) !== (oldValue & UCSRA_CFG_MASK)) { 125 | this.onConfigurationChange?.(); 126 | } 127 | return true; 128 | }; 129 | this.cpu.writeHooks[config.UCSRB] = (value, oldValue) => { 130 | cpu.updateInterruptEnable(this.RXC, value); 131 | cpu.updateInterruptEnable(this.UDRE, value); 132 | cpu.updateInterruptEnable(this.TXC, value); 133 | if (value & UCSRB_RXEN && oldValue & UCSRB_RXEN) { 134 | cpu.clearInterrupt(this.RXC); 135 | } 136 | if (value & UCSRB_TXEN && !(oldValue & UCSRB_TXEN)) { 137 | // Enabling the transmission - mark UDR as empty 138 | cpu.setInterruptFlag(this.UDRE); 139 | } 140 | cpu.data[config.UCSRB] = value; 141 | if ((value & UCSRB_CFG_MASK) !== (oldValue & UCSRB_CFG_MASK)) { 142 | this.onConfigurationChange?.(); 143 | } 144 | return true; 145 | }; 146 | this.cpu.writeHooks[config.UCSRC] = (value) => { 147 | cpu.data[config.UCSRC] = value; 148 | this.onConfigurationChange?.(); 149 | return true; 150 | }; 151 | this.cpu.readHooks[config.UDR] = () => { 152 | const mask = rxMasks[this.bitsPerChar] ?? 0xff; 153 | const result = this.rxByte & mask; 154 | this.rxByte = 0; 155 | this.cpu.clearInterrupt(this.RXC); 156 | return result; 157 | }; 158 | this.cpu.writeHooks[config.UDR] = (value) => { 159 | if (this.onByteTransmit) { 160 | this.onByteTransmit(value); 161 | } 162 | if (this.onLineTransmit) { 163 | const ch = String.fromCharCode(value); 164 | if (ch === '\n') { 165 | this.onLineTransmit(this.lineBuffer); 166 | this.lineBuffer = ''; 167 | } else { 168 | this.lineBuffer += ch; 169 | } 170 | } 171 | this.cpu.addClockEvent(() => { 172 | cpu.setInterruptFlag(this.UDRE); 173 | cpu.setInterruptFlag(this.TXC); 174 | }, this.cyclesPerChar); 175 | this.cpu.clearInterrupt(this.TXC); 176 | this.cpu.clearInterrupt(this.UDRE); 177 | }; 178 | this.cpu.writeHooks[config.UBRRH] = (value) => { 179 | this.cpu.data[config.UBRRH] = value; 180 | this.onConfigurationChange?.(); 181 | return true; 182 | }; 183 | this.cpu.writeHooks[config.UBRRL] = (value) => { 184 | this.cpu.data[config.UBRRL] = value; 185 | this.onConfigurationChange?.(); 186 | return true; 187 | }; 188 | } 189 | 190 | reset() { 191 | this.cpu.data[this.config.UCSRA] = UCSRA_UDRE; 192 | this.cpu.data[this.config.UCSRB] = 0; 193 | this.cpu.data[this.config.UCSRC] = UCSRC_UCSZ1 | UCSRC_UCSZ0; // default: 8 bits per byte 194 | this.rxBusyValue = false; 195 | this.rxByte = 0; 196 | this.lineBuffer = ''; 197 | } 198 | 199 | get rxBusy() { 200 | return this.rxBusyValue; 201 | } 202 | 203 | writeByte(value: number, immediate = false) { 204 | const { cpu } = this; 205 | if (this.rxBusyValue || !this.rxEnable) { 206 | return false; 207 | } 208 | if (immediate) { 209 | this.rxByte = value; 210 | cpu.setInterruptFlag(this.RXC); 211 | this.onRxComplete?.(); 212 | } else { 213 | this.rxBusyValue = true; 214 | cpu.addClockEvent(() => { 215 | this.rxBusyValue = false; 216 | this.writeByte(value, true); 217 | }, this.cyclesPerChar); 218 | return true; 219 | } 220 | } 221 | 222 | private get cyclesPerChar() { 223 | const symbolsPerChar = 1 + this.bitsPerChar + this.stopBits + (this.parityEnabled ? 1 : 0); 224 | return (this.UBRR + 1) * this.multiplier * symbolsPerChar; 225 | } 226 | 227 | private get UBRR() { 228 | const { UBRRH, UBRRL } = this.config; 229 | return (this.cpu.data[UBRRH] << 8) | this.cpu.data[UBRRL]; 230 | } 231 | 232 | private get multiplier() { 233 | return this.cpu.data[this.config.UCSRA] & UCSRA_U2X ? 8 : 16; 234 | } 235 | 236 | get rxEnable() { 237 | return !!(this.cpu.data[this.config.UCSRB] & UCSRB_RXEN); 238 | } 239 | 240 | get txEnable() { 241 | return !!(this.cpu.data[this.config.UCSRB] & UCSRB_TXEN); 242 | } 243 | 244 | get baudRate() { 245 | return Math.floor(this.freqHz / (this.multiplier * (1 + this.UBRR))); 246 | } 247 | 248 | get bitsPerChar() { 249 | const ucsz = 250 | ((this.cpu.data[this.config.UCSRC] & (UCSRC_UCSZ1 | UCSRC_UCSZ0)) >> 1) | 251 | (this.cpu.data[this.config.UCSRB] & UCSRB_UCSZ2); 252 | switch (ucsz) { 253 | case 0: 254 | return 5; 255 | case 1: 256 | return 6; 257 | case 2: 258 | return 7; 259 | case 3: 260 | return 8; 261 | default: // 4..6 are reserved 262 | case 7: 263 | return 9; 264 | } 265 | } 266 | 267 | get stopBits() { 268 | return this.cpu.data[this.config.UCSRC] & UCSRC_USBS ? 2 : 1; 269 | } 270 | 271 | get parityEnabled() { 272 | return this.cpu.data[this.config.UCSRC] & UCSRC_UPM1 ? true : false; 273 | } 274 | 275 | get parityOdd() { 276 | return this.cpu.data[this.config.UCSRC] & UCSRC_UPM0 ? true : false; 277 | } 278 | } 279 | -------------------------------------------------------------------------------- /src/peripherals/eeprom.spec.ts: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | // Copyright (c) Uri Shaked and contributors 3 | 4 | import { describe, expect, it } from 'vitest'; 5 | import { CPU } from '../cpu/cpu'; 6 | import { asmProgram, TestProgramRunner } from '../utils/test-utils'; 7 | import { AVREEPROM, EEPROMMemoryBackend } from './eeprom'; 8 | 9 | // EEPROM Registers 10 | const EECR = 0x3f; 11 | const EEDR = 0x40; 12 | const EEARL = 0x41; 13 | const EEARH = 0x42; 14 | const SREG = 95; 15 | 16 | // Register bit names 17 | /* eslint-disable @typescript-eslint/no-unused-vars */ 18 | const EERE = 1; 19 | const EEPE = 2; 20 | const EEMPE = 4; 21 | const EERIE = 8; 22 | const EEPM0 = 16; 23 | const EEPM1 = 32; 24 | /* eslint-enable @typescript-eslint/no-unused-vars */ 25 | 26 | describe('EEPROM', () => { 27 | describe('Reading the EEPROM', () => { 28 | it('should return 0xff when reading from an empty location', () => { 29 | const cpu = new CPU(new Uint16Array(0x1000)); 30 | new AVREEPROM(cpu, new EEPROMMemoryBackend(1024)); 31 | cpu.writeData(EEARL, 0); 32 | cpu.writeData(EEARH, 0); 33 | cpu.writeData(EECR, EERE); 34 | cpu.tick(); 35 | expect(cpu.cycles).toEqual(4); 36 | expect(cpu.data[EEDR]).toEqual(0xff); 37 | }); 38 | 39 | it('should return the value stored at the given EEPROM address', () => { 40 | const cpu = new CPU(new Uint16Array(0x1000)); 41 | const eepromBackend = new EEPROMMemoryBackend(1024); 42 | new AVREEPROM(cpu, eepromBackend); 43 | eepromBackend.memory[0x250] = 0x42; 44 | cpu.writeData(EEARL, 0x50); 45 | cpu.writeData(EEARH, 0x2); 46 | cpu.writeData(EECR, EERE); 47 | cpu.tick(); 48 | expect(cpu.data[EEDR]).toEqual(0x42); 49 | }); 50 | }); 51 | 52 | describe('Writing to the EEPROM', () => { 53 | it('should write a byte to the given EEPROM address', () => { 54 | const cpu = new CPU(new Uint16Array(0x1000)); 55 | const eepromBackend = new EEPROMMemoryBackend(1024); 56 | new AVREEPROM(cpu, eepromBackend); 57 | cpu.writeData(EEDR, 0x55); 58 | cpu.writeData(EEARL, 15); 59 | cpu.writeData(EEARH, 0); 60 | cpu.writeData(EECR, EEMPE); 61 | cpu.writeData(EECR, EEPE); 62 | cpu.tick(); 63 | expect(cpu.cycles).toEqual(2); 64 | expect(eepromBackend.memory[15]).toEqual(0x55); 65 | expect(cpu.data[EECR] & EEPE).toEqual(EEPE); 66 | }); 67 | 68 | it('should not erase the memory when writing if EEPM1 is high', () => { 69 | // We subtract 0x20 to translate from RAM address space to I/O register space 70 | const { program, instructionCount } = asmProgram(` 71 | ; register addresses 72 | _REPLACE TWSR, ${EECR - 0x20} 73 | _REPLACE EEARL, ${EEARL - 0x20} 74 | _REPLACE EEDR, ${EEDR - 0x20} 75 | _REPLACE EECR, ${EECR - 0x20} 76 | 77 | LDI r16, 0x55 78 | OUT EEDR, r16 79 | LDI r16, 9 80 | OUT EEARL, r16 81 | SBI EECR, 5 ; EECR |= EEPM1 82 | SBI EECR, 2 ; EECR |= EEMPE 83 | SBI EECR, 1 ; EECR |= EEPE 84 | `); 85 | 86 | const cpu = new CPU(program); 87 | const eepromBackend = new EEPROMMemoryBackend(1024); 88 | new AVREEPROM(cpu, eepromBackend); 89 | eepromBackend.memory[9] = 0x0f; // high four bits are cleared 90 | 91 | const runner = new TestProgramRunner(cpu); 92 | runner.runInstructions(instructionCount); 93 | 94 | // EEPROM was 0x0f, and our program wrote 0x55. 95 | // Since write (without erase) only clears bits, we expect 0x05 now. 96 | expect(eepromBackend.memory[9]).toEqual(0x05); 97 | }); 98 | 99 | it('should clear the EEPE bit and fire an interrupt when write has been completed', () => { 100 | const cpu = new CPU(new Uint16Array(0x1000)); 101 | const eepromBackend = new EEPROMMemoryBackend(1024); 102 | new AVREEPROM(cpu, eepromBackend); 103 | cpu.writeData(EEDR, 0x55); 104 | cpu.writeData(EEARL, 15); 105 | cpu.writeData(EEARH, 0); 106 | cpu.writeData(EECR, EEMPE); 107 | cpu.data[SREG] = 0x80; // SREG: I------- 108 | cpu.writeData(EECR, EEPE | EERIE); 109 | cpu.cycles += 1000; 110 | cpu.tick(); 111 | // At this point, write shouldn't be complete yet 112 | expect(cpu.data[EECR] & EEPE).toEqual(EEPE); 113 | expect(cpu.pc).toEqual(0); 114 | cpu.cycles += 10000000; 115 | // And now, 10 million cycles later, it should. 116 | cpu.tick(); 117 | expect(eepromBackend.memory[15]).toEqual(0x55); 118 | expect(cpu.data[EECR] & EEPE).toEqual(0); 119 | expect(cpu.pc).toEqual(0x2c); // EEPROM Ready interrupt 120 | }); 121 | 122 | it('should clear the fire an interrupt when there is a pending interrupt and the interrupt flag is enabled (issue #110)', () => { 123 | const cpu = new CPU(new Uint16Array(0x1000)); 124 | const eepromBackend = new EEPROMMemoryBackend(1024); 125 | new AVREEPROM(cpu, eepromBackend); 126 | cpu.writeData(EEDR, 0x55); 127 | cpu.writeData(EEARL, 15); 128 | cpu.writeData(EEARH, 0); 129 | cpu.writeData(EECR, EEMPE); 130 | cpu.data[SREG] = 0x80; // SREG: I------- 131 | cpu.writeData(EECR, EEPE); 132 | cpu.cycles += 1000; 133 | cpu.tick(); 134 | // At this point, write shouldn't be complete yet 135 | expect(cpu.data[EECR] & EEPE).toEqual(EEPE); 136 | expect(cpu.pc).toEqual(0); 137 | cpu.cycles += 10000000; 138 | // And now, 10 million cycles later, it should. 139 | cpu.tick(); 140 | expect(eepromBackend.memory[15]).toEqual(0x55); 141 | expect(cpu.data[EECR] & EEPE).toEqual(0); 142 | cpu.writeData(EECR, EERIE); 143 | cpu.tick(); 144 | expect(cpu.pc).toEqual(0x2c); // EEPROM Ready interrupt 145 | }); 146 | 147 | it('should skip the write if EEMPE is clear', () => { 148 | const cpu = new CPU(new Uint16Array(0x1000)); 149 | const eepromBackend = new EEPROMMemoryBackend(1024); 150 | new AVREEPROM(cpu, eepromBackend); 151 | cpu.writeData(EEDR, 0x55); 152 | cpu.writeData(EEARL, 15); 153 | cpu.writeData(EEARH, 0); 154 | cpu.writeData(EECR, EEMPE); 155 | cpu.cycles = 8; // waiting for more than 4 cycles should clear EEMPE 156 | cpu.tick(); 157 | cpu.writeData(EECR, EEPE); 158 | cpu.tick(); 159 | // Ensure that nothing was written, and EEPE bit is clear 160 | expect(cpu.cycles).toEqual(8); 161 | expect(eepromBackend.memory[15]).toEqual(0xff); 162 | expect(cpu.data[EECR] & EEPE).toEqual(0); 163 | }); 164 | 165 | it('should skip the write if another write is already in progress', () => { 166 | const cpu = new CPU(new Uint16Array(0x1000)); 167 | const eepromBackend = new EEPROMMemoryBackend(1024); 168 | new AVREEPROM(cpu, eepromBackend); 169 | 170 | // Write 0x55 to address 15 171 | cpu.writeData(EEDR, 0x55); 172 | cpu.writeData(EEARL, 15); 173 | cpu.writeData(EEARH, 0); 174 | cpu.writeData(EECR, EEMPE); 175 | cpu.writeData(EECR, EEPE); 176 | cpu.tick(); 177 | expect(cpu.cycles).toEqual(2); 178 | 179 | // Write 0x66 to address 16 (first write is still in progress) 180 | cpu.writeData(EEDR, 0x66); 181 | cpu.writeData(EEARL, 16); 182 | cpu.writeData(EEARH, 0); 183 | cpu.writeData(EECR, EEMPE); 184 | cpu.writeData(EECR, EEPE); 185 | cpu.tick(); 186 | 187 | // Ensure that second write didn't happen 188 | expect(cpu.cycles).toEqual(2); 189 | expect(eepromBackend.memory[15]).toEqual(0x55); 190 | expect(eepromBackend.memory[16]).toEqual(0xff); 191 | }); 192 | 193 | it('should write two bytes sucessfully', () => { 194 | const cpu = new CPU(new Uint16Array(0x1000)); 195 | const eepromBackend = new EEPROMMemoryBackend(1024); 196 | new AVREEPROM(cpu, eepromBackend); 197 | 198 | // Write 0x55 to address 15 199 | cpu.writeData(EEDR, 0x55); 200 | cpu.writeData(EEARL, 15); 201 | cpu.writeData(EEARH, 0); 202 | cpu.writeData(EECR, EEMPE); 203 | cpu.writeData(EECR, EEPE); 204 | cpu.tick(); 205 | expect(cpu.cycles).toEqual(2); 206 | 207 | // wait long enough time for the first write to finish 208 | cpu.cycles += 10000000; 209 | cpu.tick(); 210 | 211 | // Write 0x66 to address 16 212 | cpu.writeData(EEDR, 0x66); 213 | cpu.writeData(EEARL, 16); 214 | cpu.writeData(EEARH, 0); 215 | cpu.writeData(EECR, EEMPE); 216 | cpu.writeData(EECR, EEPE); 217 | cpu.tick(); 218 | 219 | // Ensure both writes took place 220 | expect(cpu.cycles).toEqual(10000004); 221 | expect(eepromBackend.memory[15]).toEqual(0x55); 222 | expect(eepromBackend.memory[16]).toEqual(0x66); 223 | }); 224 | }); 225 | 226 | describe('EEPROM erase', () => { 227 | it('should only erase the memory when EEPM0 is high', () => { 228 | // We subtract 0x20 to translate from RAM address space to I/O register space 229 | const { program, instructionCount } = asmProgram(` 230 | ; register addresses 231 | _REPLACE EEARL, ${EEARL - 0x20} 232 | _REPLACE EEDR, ${EEDR - 0x20} 233 | _REPLACE EECR, ${EECR - 0x20} 234 | 235 | LDI r16, 0x55 236 | OUT EEDR, r16 237 | LDI r16, 9 238 | OUT EEARL, r16 239 | SBI EECR, 4 ; EECR |= EEPM0 240 | SBI EECR, 2 ; EECR |= EEMPE 241 | SBI EECR, 1 ; EECR |= EEPE 242 | `); 243 | 244 | const cpu = new CPU(program); 245 | const eepromBackend = new EEPROMMemoryBackend(1024); 246 | new AVREEPROM(cpu, eepromBackend); 247 | eepromBackend.memory[9] = 0x22; 248 | 249 | const runner = new TestProgramRunner(cpu); 250 | runner.runInstructions(instructionCount); 251 | 252 | expect(eepromBackend.memory[9]).toEqual(0xff); 253 | }); 254 | }); 255 | }); 256 | -------------------------------------------------------------------------------- /src/utils/assembler.spec.ts: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | // Copyright (c) Uri Shaked and contributors 3 | 4 | import { describe, expect, it } from 'vitest'; 5 | import { assemble } from './assembler'; 6 | 7 | function bytes(hex: string) { 8 | const result = new Uint8Array(hex.length / 2); 9 | for (let i = 0; i < hex.length; i += 2) { 10 | result[i / 2] = parseInt(hex.substr(i, 2), 16); 11 | } 12 | return result; 13 | } 14 | 15 | describe('AVR assembler', () => { 16 | it('should assemble ADD instruction', () => { 17 | expect(assemble('ADD r16, r11')).toEqual({ 18 | bytes: bytes('0b0d'), 19 | errors: [], 20 | lines: [{ byteOffset: 0, bytes: '0d0b', line: 1, text: 'ADD r16, r11' }], 21 | labels: {}, 22 | }); 23 | }); 24 | 25 | it('should support labels', () => { 26 | expect(assemble('loop: JMP loop').bytes).toEqual(bytes('0c940000')); 27 | }); 28 | 29 | it('should support mutli-line programs', () => { 30 | const input = ` 31 | start: 32 | LDI r16, 15 33 | EOR r16, r0 34 | BREQ start 35 | `; 36 | expect(assemble(input).bytes).toEqual(bytes('0fe00025e9f3')); 37 | }); 38 | 39 | it('should successfully assemble an empty program', () => { 40 | expect(assemble('')).toEqual({ 41 | bytes: new Uint8Array(0), 42 | errors: [], 43 | lines: [], 44 | labels: {}, 45 | }); 46 | }); 47 | 48 | it('should return an empty byte array in case of program error', () => { 49 | expect(assemble('LDI r15, 20')).toEqual({ 50 | bytes: new Uint8Array(0), 51 | errors: ['Line 0: Rd out of range: 16<>31'], 52 | lines: [], 53 | labels: {}, 54 | }); 55 | }); 56 | 57 | it('should correctly assemble `ADC r0, r1`', () => { 58 | expect(assemble('ADC r0, r1').bytes).toEqual(bytes('011c')); 59 | }); 60 | 61 | it('should correctly assemble `BCLR 2`', () => { 62 | expect(assemble('BCLR 2').bytes).toEqual(bytes('a894')); 63 | }); 64 | 65 | it('should correctly assemble `BLD r4, 7`', () => { 66 | expect(assemble('BLD r4, 7').bytes).toEqual(bytes('47f8')); 67 | }); 68 | 69 | it('should correctly assemble `BRBC 0, +8`', () => { 70 | expect(assemble('BRBC 0, +8').bytes).toEqual(bytes('20f4')); 71 | }); 72 | 73 | it('should correctly assemble `BRBS 3, 92`', () => { 74 | expect(assemble('BRBS 3, 92').bytes).toEqual(bytes('73f1')); 75 | }); 76 | 77 | it('should correctly assemble `BRBS 3, -4`', () => { 78 | expect(assemble('BRBS 3, -4').bytes).toEqual(bytes('f3f3')); 79 | }); 80 | 81 | it('should correctly assemble BREQ with forward label target', () => { 82 | expect(assemble('BREQ next \n next:').bytes).toEqual(bytes('01f0')); 83 | }); 84 | 85 | it('should correctly assemble BRNE with forward label target', () => { 86 | expect(assemble('BRNE next \n next:').bytes).toEqual(bytes('01f4')); 87 | }); 88 | 89 | it('should correctly assemble `CBI 0xc, 5`', () => { 90 | expect(assemble('CBI 0xc, 5').bytes).toEqual(bytes('6598')); 91 | }); 92 | 93 | it('should correctly assemble `CALL 0xb8`', () => { 94 | expect(assemble('CALL 0xb8').bytes).toEqual(bytes('0e945c00')); 95 | }); 96 | 97 | it('should correctly assemble `CPC r27, r18`', () => { 98 | expect(assemble('CPC r27, r18').bytes).toEqual(bytes('b207')); 99 | }); 100 | 101 | it('should correctly assemble `CPC r24, r1`', () => { 102 | expect(assemble('CPC r24, r1').bytes).toEqual(bytes('8105')); 103 | }); 104 | 105 | it('should correctly assemble `CPI r26, 0x9`', () => { 106 | expect(assemble('CPI r26, 0x9').bytes).toEqual(bytes('a930')); 107 | }); 108 | 109 | it('should correctly assemble `CPSE r2, r3`', () => { 110 | expect(assemble('CPSE r2, r3`').bytes).toEqual(bytes('2310')); 111 | }); 112 | 113 | it('should correctly assemble `ICALL`', () => { 114 | expect(assemble('ICALL').bytes).toEqual(bytes('0995')); 115 | }); 116 | 117 | it('should correctly assemble `IJMP`', () => { 118 | expect(assemble('IJMP').bytes).toEqual(bytes('0994')); 119 | }); 120 | 121 | it('should correctly assemble `IN r5, 0xb`', () => { 122 | expect(assemble('IN r5, 0xb').bytes).toEqual(bytes('5bb0')); 123 | }); 124 | 125 | it('should correctly assemble `INC r5`', () => { 126 | expect(assemble('INC r5').bytes).toEqual(bytes('5394')); 127 | }); 128 | 129 | it('should correctly assemble `JMP 0xb8`', () => { 130 | expect(assemble('JMP 0xb8').bytes).toEqual(bytes('0c945c00')); 131 | }); 132 | 133 | it('should correctly assemble `LAC r19`', () => { 134 | expect(assemble('LAC Z, r19').bytes).toEqual(bytes('3693')); 135 | }); 136 | 137 | it('should correctly assemble `LAS Z, r17`', () => { 138 | expect(assemble('LAS Z, r17').bytes).toEqual(bytes('1593')); 139 | }); 140 | 141 | it('should correctly assemble `LAT Z, r0`', () => { 142 | expect(assemble('LAT Z, r0').bytes).toEqual(bytes('0792')); 143 | }); 144 | 145 | it('should correctly assemble `LDI r28, 0xff`', () => { 146 | expect(assemble('LDI r28, 0xff').bytes).toEqual(bytes('cfef')); 147 | }); 148 | 149 | it('should correctly assemble `LDS r5, 0x150`', () => { 150 | expect(assemble('LDS r5, 0x150').bytes).toEqual(bytes('50905001')); 151 | }); 152 | 153 | it('should correctly assemble `LD r1, X`', () => { 154 | expect(assemble('LD r1, X').bytes).toEqual(bytes('1c90')); 155 | }); 156 | 157 | it('should correctly assemble `LD r17, X+`', () => { 158 | expect(assemble('LD r17, X+').bytes).toEqual(bytes('1d91')); 159 | }); 160 | 161 | it('should correctly assemble `LD r1, -X`', () => { 162 | expect(assemble('LD r1, -X').bytes).toEqual(bytes('1e90')); 163 | }); 164 | 165 | it('should correctly assemble `LD r8, Y`', () => { 166 | expect(assemble('LD r8, Y').bytes).toEqual(bytes('8880')); 167 | }); 168 | 169 | it('should correctly assemble `LD r3, Y+`', () => { 170 | expect(assemble('LD r3, Y+').bytes).toEqual(bytes('3990')); 171 | }); 172 | 173 | it('should correctly assemble `LD r0, -Y`', () => { 174 | expect(assemble('LD r0, -Y').bytes).toEqual(bytes('0a90')); 175 | }); 176 | 177 | it('should correctly assemble `LDD r4, Y+2`', () => { 178 | expect(assemble('LDD r4, Y+2').bytes).toEqual(bytes('4a80')); 179 | }); 180 | 181 | it('should correctly assemble `LD r5, Z`', () => { 182 | expect(assemble('LD r5, Z').bytes).toEqual(bytes('5080')); 183 | }); 184 | 185 | it('should correctly assemble `LD r7, Z+`', () => { 186 | expect(assemble('LD r7, Z+').bytes).toEqual(bytes('7190')); 187 | }); 188 | 189 | it('should correctly assemble `LD r0, -Z`', () => { 190 | expect(assemble('LD r0, -Z').bytes).toEqual(bytes('0290')); 191 | }); 192 | 193 | it('should correctly assemble `LDD r15, Z+31`', () => { 194 | expect(assemble('LDD r15, Z+31').bytes).toEqual(bytes('f78c')); 195 | }); 196 | 197 | it('should correctly assemble `LPM`', () => { 198 | expect(assemble('LPM').bytes).toEqual(bytes('c895')); 199 | }); 200 | 201 | it('should correctly assemble `LPM r2, Z`', () => { 202 | expect(assemble('LPM r2, Z').bytes).toEqual(bytes('2490')); 203 | }); 204 | 205 | it('should correctly assemble `LPM r1, Z+`', () => { 206 | expect(assemble('LPM r1, Z+').bytes).toEqual(bytes('1590')); 207 | }); 208 | 209 | it('should correctly assemble `LSR r7`', () => { 210 | expect(assemble('LSR r7').bytes).toEqual(bytes('7694')); 211 | }); 212 | 213 | it('should correctly assemble `MOV r7, r8`', () => { 214 | expect(assemble('MOV r7, r8').bytes).toEqual(bytes('782c')); 215 | }); 216 | 217 | it('should correctly assemble `MOVW r26, r22`', () => { 218 | expect(assemble('MOVW r26, r22').bytes).toEqual(bytes('db01')); 219 | }); 220 | 221 | it('should correctly assemble `MUL r5, r6`', () => { 222 | expect(assemble('MUL r5, r6').bytes).toEqual(bytes('569c')); 223 | }); 224 | 225 | it('should correctly assemble `MULS r18, r19`', () => { 226 | expect(assemble('MULS r18, r19').bytes).toEqual(bytes('2302')); 227 | }); 228 | 229 | it('should correctly assemble `MULSU r16, r17`', () => { 230 | expect(assemble('MULSU r16, r17').bytes).toEqual(bytes('0103')); 231 | }); 232 | 233 | it('should correctly assemble `NEG r20`', () => { 234 | expect(assemble('NEG r20').bytes).toEqual(bytes('4195')); 235 | }); 236 | 237 | it('should correctly assemble `NOP`', () => { 238 | expect(assemble('NOP').bytes).toEqual(bytes('0000')); 239 | }); 240 | 241 | it('should correctly assemble `OR r5, r2`', () => { 242 | expect(assemble('OR r5, r2').bytes).toEqual(bytes('5228')); 243 | }); 244 | 245 | it('should correctly assemble `ORI r22, 0x81`', () => { 246 | expect(assemble('ORI r22, 0x81').bytes).toEqual(bytes('6168')); 247 | }); 248 | 249 | it('should correctly assemble `OUT 0x3f, r1`', () => { 250 | expect(assemble('OUT 0x3f, r1').bytes).toEqual(bytes('1fbe')); 251 | }); 252 | 253 | it('should correctly assemble `POP r26`', () => { 254 | expect(assemble('POP r26').bytes).toEqual(bytes('af91')); 255 | }); 256 | 257 | it('should correctly assemble `PUSH r11`', () => { 258 | expect(assemble('PUSH r11').bytes).toEqual(bytes('bf92')); 259 | }); 260 | 261 | it('should correctly assemble `RCALL +6`', () => { 262 | expect(assemble('RCALL +6').bytes).toEqual(bytes('03d0')); 263 | }); 264 | 265 | it('should correctly assemble `RCALL -4`', () => { 266 | expect(assemble('RCALL -4').bytes).toEqual(bytes('fedf')); 267 | }); 268 | 269 | it('should correctly assemble `RET`', () => { 270 | expect(assemble('RET').bytes).toEqual(bytes('0895')); 271 | }); 272 | 273 | it('should correctly assemble `RETI`', () => { 274 | expect(assemble('RETI').bytes).toEqual(bytes('1895')); 275 | }); 276 | 277 | it('should correctly assemble `RJMP 2`', () => { 278 | expect(assemble('RJMP 2').bytes).toEqual(bytes('01c0')); 279 | }); 280 | 281 | it('should correctly assemble `ROR r0`', () => { 282 | expect(assemble('ROR r0').bytes).toEqual(bytes('0794')); 283 | }); 284 | 285 | it('should correctly assemble `SBCI r23, 3`', () => { 286 | expect(assemble('SBCI r23, 3').bytes).toEqual(bytes('7340')); 287 | }); 288 | 289 | it('should correctly assemble `SBI 0x0c, 5`', () => { 290 | expect(assemble('SBI 0x0c, 5').bytes).toEqual(bytes('659a')); 291 | }); 292 | 293 | it('should correctly assemble `SBIS 0x0c, 5`', () => { 294 | expect(assemble('SBIS 0x0c, 5').bytes).toEqual(bytes('659b')); 295 | }); 296 | 297 | it('should correctly assemble `SBIW r28, 2`', () => { 298 | expect(assemble('SBIW r28, 2').bytes).toEqual(bytes('2297')); 299 | }); 300 | 301 | it('should correctly assemble `SLEEP`', () => { 302 | expect(assemble('SLEEP').bytes).toEqual(bytes('8895')); 303 | }); 304 | 305 | it('should correctly assemble `SPM`', () => { 306 | expect(assemble('SPM').bytes).toEqual(bytes('e895')); 307 | }); 308 | 309 | it('should correctly assemble `SPM Z+`', () => { 310 | expect(assemble('SPM Z+').bytes).toEqual(bytes('f895')); 311 | }); 312 | 313 | it('should correctly assemble `STS 0x151, r31`', () => { 314 | expect(assemble('STS 0x151, r31').bytes).toEqual(bytes('f0935101')); 315 | }); 316 | 317 | it('should correctly assemble `ST X, r1`', () => { 318 | expect(assemble('ST X, r1').bytes).toEqual(bytes('1c92')); 319 | }); 320 | 321 | it('should correctly assemble `ST X+, r1`', () => { 322 | expect(assemble('ST X+, r1').bytes).toEqual(bytes('1d92')); 323 | }); 324 | 325 | it('should correctly assemble `ST -X, r17`', () => { 326 | expect(assemble('ST -X, r17').bytes).toEqual(bytes('1e93')); 327 | }); 328 | 329 | it('should correctly assemble `ST Y, r2`', () => { 330 | expect(assemble('ST Y, r2').bytes).toEqual(bytes('2882')); 331 | }); 332 | 333 | it('should correctly assemble `ST Y+, r1`', () => { 334 | expect(assemble('ST Y+, r1').bytes).toEqual(bytes('1992')); 335 | }); 336 | 337 | it('should correctly assemble `ST -Y, r1`', () => { 338 | expect(assemble('ST -Y, r1').bytes).toEqual(bytes('1a92')); 339 | }); 340 | 341 | it('should correctly assemble `STD Y+17, r0`', () => { 342 | expect(assemble('STD Y+17, r0').bytes).toEqual(bytes('098a')); 343 | }); 344 | 345 | it('should correctly assemble `ST Z, r16`', () => { 346 | expect(assemble('ST Z, r16').bytes).toEqual(bytes('0083')); 347 | }); 348 | 349 | it('should correctly assemble `ST Z+, r0`', () => { 350 | expect(assemble('ST Z+, r0').bytes).toEqual(bytes('0192')); 351 | }); 352 | 353 | it('should correctly assemble `ST -Z, r16`', () => { 354 | expect(assemble('ST -Z, r16').bytes).toEqual(bytes('0293')); 355 | }); 356 | 357 | it('should correctly assemble `STD Z+1, r0`', () => { 358 | expect(assemble('STD Z+1, r0').bytes).toEqual(bytes('0182')); 359 | }); 360 | 361 | it('should correctly assemble `SWAP r1`', () => { 362 | expect(assemble('SWAP r1').bytes).toEqual(bytes('1294')); 363 | }); 364 | 365 | it('should correctly assemble `XCH Z, r21`', () => { 366 | expect(assemble('XCH Z, r21').bytes).toEqual(bytes('5493')); 367 | }); 368 | }); 369 | -------------------------------------------------------------------------------- /src/peripherals/gpio.spec.ts: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | // Copyright (c) Uri Shaked and contributors 3 | 4 | import { CPU } from '../cpu/cpu'; 5 | import { asmProgram, TestProgramRunner } from '../utils/test-utils'; 6 | import { AVRIOPort, portBConfig, PinState, portDConfig, PinOverrideMode } from './gpio'; 7 | import { describe, it, expect, vi } from 'vitest'; 8 | 9 | // CPU registers 10 | const SREG = 95; 11 | 12 | // GPIO registers 13 | const PINB = 0x23; 14 | const DDRB = 0x24; 15 | const PORTB = 0x25; 16 | const PIND = 0x29; 17 | const DDRD = 0x2a; 18 | const PORTD = 0x2b; 19 | const EIFR = 0x3c; 20 | const EIMSK = 0x3d; 21 | const PCICR = 0x68; 22 | const EICRA = 0x69; 23 | const PCIFR = 0x3b; 24 | const PCMSK0 = 0x6b; 25 | 26 | // Register bit names 27 | const INT0 = 0; 28 | const ISC00 = 0; 29 | const ISC01 = 1; 30 | const PCIE0 = 0; 31 | const PCINT3 = 3; 32 | 33 | // Pin names 34 | const PB0 = 0; 35 | const PB1 = 1; 36 | const PB3 = 3; 37 | const PB4 = 4; 38 | const PD2 = 2; 39 | 40 | // Interrupt vector addresses 41 | const PC_INT_INT0 = 2; 42 | const PC_INT_PCINT0 = 6; 43 | 44 | describe('GPIO', () => { 45 | it('should invoke the listeners when the port is written to', () => { 46 | const cpu = new CPU(new Uint16Array(1024)); 47 | const port = new AVRIOPort(cpu, portBConfig); 48 | const listener = vi.fn(); 49 | cpu.writeData(DDRB, 0x0f); 50 | port.addListener(listener); 51 | cpu.writeData(PORTB, 0x55); 52 | expect(listener).toHaveBeenCalledWith(0x55, 0); 53 | expect(cpu.data[0x23]).toEqual(0x5); // PINB should return port value 54 | }); 55 | 56 | it('should invoke the listeners when DDR changes (issue #28)', () => { 57 | const cpu = new CPU(new Uint16Array(1024)); 58 | const port = new AVRIOPort(cpu, portBConfig); 59 | const listener = vi.fn(); 60 | cpu.writeData(PORTB, 0x55); 61 | port.addListener(listener); 62 | cpu.writeData(DDRB, 0xf0); 63 | expect(listener).toHaveBeenCalledWith(0x55, 0x55); 64 | }); 65 | 66 | it('should invoke the listeners when pullup register enabled (issue #62)', () => { 67 | const cpu = new CPU(new Uint16Array(1024)); 68 | const port = new AVRIOPort(cpu, portBConfig); 69 | const listener = vi.fn(); 70 | port.addListener(listener); 71 | cpu.writeData(PORTB, 0x55); 72 | expect(listener).toHaveBeenCalledWith(0x55, 0); 73 | }); 74 | 75 | it('should toggle the pin when writing to the PIN register', () => { 76 | const cpu = new CPU(new Uint16Array(1024)); 77 | const port = new AVRIOPort(cpu, portBConfig); 78 | const listener = vi.fn(); 79 | port.addListener(listener); 80 | cpu.writeData(DDRB, 0x0f); 81 | cpu.writeData(PORTB, 0x55); 82 | cpu.writeData(PINB, 0x01); 83 | expect(listener).toHaveBeenCalledWith(0x54, 0x55); 84 | expect(cpu.data[PINB]).toEqual(0x4); // PINB should return port value 85 | }); 86 | 87 | it('should only affect one pin when writing to PIN using SBI (issue #103)', () => { 88 | const { program } = asmProgram(` 89 | ; register addresses 90 | _REPLACE DDRD, ${DDRD - 0x20} 91 | _REPLACE PIND, ${PIND - 0x20} 92 | _REPLACE PORTD, ${PORTD - 0x20} 93 | 94 | ; Setup 95 | ldi r24, 0x48 96 | out DDRD, r24 97 | out PORTD, r24 98 | 99 | ; Now toggle pin 6 with SBI 100 | sbi PIND, 6 101 | 102 | break 103 | `); 104 | const cpu = new CPU(program); 105 | const portD = new AVRIOPort(cpu, portDConfig); 106 | const runner = new TestProgramRunner(cpu); 107 | 108 | const listener = vi.fn(); 109 | portD.addListener(listener); 110 | 111 | // Setup: pins 6, 3 are output, set to HIGH 112 | runner.runInstructions(3); 113 | expect(listener).toHaveBeenCalledWith(0x48, 0x0); 114 | expect(cpu.data[PORTD]).toEqual(0x48); 115 | listener.mockReset(); 116 | 117 | // Now we toggle pin 6 118 | runner.runInstructions(1); 119 | expect(listener).toHaveBeenCalledWith(0x08, 0x48); 120 | expect(cpu.data[PORTD]).toEqual(0x8); 121 | }); 122 | 123 | it('should update the PIN register on output compare (OCR) match (issue #102)', () => { 124 | const cpu = new CPU(new Uint16Array(1024)); 125 | const port = new AVRIOPort(cpu, portBConfig); 126 | cpu.writeData(DDRB, 1 << 1); 127 | port.timerOverridePin(1, PinOverrideMode.Set); 128 | expect(port.pinState(1)).toBe(PinState.High); 129 | expect(cpu.data[PINB]).toBe(1 << 1); 130 | port.timerOverridePin(1, PinOverrideMode.Clear); 131 | expect(port.pinState(1)).toBe(PinState.Low); 132 | expect(cpu.data[PINB]).toBe(0); 133 | }); 134 | 135 | describe('removeListener', () => { 136 | it('should remove the given listener', () => { 137 | const cpu = new CPU(new Uint16Array(1024)); 138 | const port = new AVRIOPort(cpu, portBConfig); 139 | const listener = vi.fn(); 140 | port.addListener(listener); 141 | cpu.writeData(DDRB, 0x0f); 142 | port.removeListener(listener); 143 | cpu.writeData(PORTB, 0x99); 144 | expect(listener).toHaveBeenCalledTimes(1); 145 | }); 146 | }); 147 | 148 | describe('pinState', () => { 149 | it('should return PinState.High when the pin set to output and HIGH', () => { 150 | const cpu = new CPU(new Uint16Array(1024)); 151 | const port = new AVRIOPort(cpu, portBConfig); 152 | cpu.writeData(DDRB, 0x1); 153 | cpu.writeData(PORTB, 0x1); 154 | expect(port.pinState(PB0)).toEqual(PinState.High); 155 | }); 156 | 157 | it('should return PinState.Low when the pin set to output and LOW', () => { 158 | const cpu = new CPU(new Uint16Array(1024)); 159 | const port = new AVRIOPort(cpu, portBConfig); 160 | cpu.writeData(DDRB, 0x8); 161 | cpu.writeData(PORTB, 0xf7); 162 | expect(port.pinState(PB3)).toEqual(PinState.Low); 163 | }); 164 | 165 | it('should return PinState.Input by default (reset state)', () => { 166 | const cpu = new CPU(new Uint16Array(1024)); 167 | const port = new AVRIOPort(cpu, portBConfig); 168 | expect(port.pinState(PB1)).toEqual(PinState.Input); 169 | }); 170 | 171 | it('should return PinState.InputPullUp when the pin is set to input with pullup', () => { 172 | const cpu = new CPU(new Uint16Array(1024)); 173 | const port = new AVRIOPort(cpu, portBConfig); 174 | cpu.writeData(DDRB, 0); 175 | cpu.writeData(PORTB, 0x2); 176 | expect(port.pinState(PB1)).toEqual(PinState.InputPullUp); 177 | }); 178 | 179 | it('should reflect the current port state when called inside a listener', () => { 180 | // Related issue: https://github.com/wokwi/avr8js/issues/9 181 | const cpu = new CPU(new Uint16Array(1024)); 182 | const port = new AVRIOPort(cpu, portBConfig); 183 | const listener = vi.fn(() => { 184 | expect(port.pinState(PB0)).toBe(PinState.High); 185 | }); 186 | expect(port.pinState(PB0)).toBe(PinState.Input); 187 | cpu.writeData(DDRB, 0x01); 188 | port.addListener(listener); 189 | cpu.writeData(PORTB, 0x01); 190 | expect(listener).toHaveBeenCalled(); 191 | }); 192 | 193 | it('should reflect the current port state when called inside a listener after DDR change', () => { 194 | // Related issue: https://github.com/wokwi/avr8js/issues/47 195 | const cpu = new CPU(new Uint16Array(1024)); 196 | const port = new AVRIOPort(cpu, portBConfig); 197 | const listener = vi.fn(() => { 198 | expect(port.pinState(PB0)).toBe(PinState.Low); 199 | }); 200 | expect(port.pinState(PB0)).toBe(PinState.Input); 201 | port.addListener(listener); 202 | cpu.writeData(DDRB, 0x01); 203 | expect(listener).toHaveBeenCalled(); 204 | }); 205 | }); 206 | 207 | describe('setPin', () => { 208 | it('should set the value of the given pin', () => { 209 | const cpu = new CPU(new Uint16Array(1024)); 210 | const port = new AVRIOPort(cpu, portBConfig); 211 | cpu.writeData(DDRB, 0); 212 | port.setPin(PB4, true); 213 | expect(cpu.data[0x23]).toEqual(0x10); 214 | port.setPin(PB4, false); 215 | expect(cpu.data[0x23]).toEqual(0x0); 216 | }); 217 | 218 | it('should only update PIN register when pin in Input mode', () => { 219 | const cpu = new CPU(new Uint16Array(1024)); 220 | const port = new AVRIOPort(cpu, portBConfig); 221 | cpu.writeData(DDRB, 0x10); 222 | cpu.writeData(PORTB, 0x0); 223 | port.setPin(PB4, true); 224 | expect(cpu.data[PINB]).toEqual(0x0); 225 | cpu.writeData(DDRB, 0x0); 226 | expect(cpu.data[PINB]).toEqual(0x10); 227 | }); 228 | }); 229 | 230 | describe('External interrupt', () => { 231 | it('should generate INT0 interrupt on rising edge', () => { 232 | const cpu = new CPU(new Uint16Array(1024)); 233 | const port = new AVRIOPort(cpu, portDConfig); 234 | cpu.writeData(EIMSK, 1 << INT0); 235 | cpu.writeData(EICRA, (1 << ISC01) | (1 << ISC00)); 236 | 237 | expect(cpu.data[EIFR]).toEqual(0); 238 | port.setPin(PD2, true); 239 | expect(cpu.data[EIFR]).toEqual(1 << INT0); 240 | 241 | cpu.data[SREG] = 0x80; // SREG: I------- (enable interrupts) 242 | cpu.tick(); 243 | expect(cpu.pc).toEqual(PC_INT_INT0); 244 | expect(cpu.cycles).toEqual(2); 245 | expect(cpu.data[EIFR]).toEqual(0); 246 | 247 | port.setPin(PD2, false); 248 | expect(cpu.data[EIFR]).toEqual(0); 249 | }); 250 | 251 | it('should generate INT0 interrupt on falling edge', () => { 252 | const cpu = new CPU(new Uint16Array(1024)); 253 | const port = new AVRIOPort(cpu, portDConfig); 254 | cpu.writeData(EIMSK, 1 << INT0); 255 | cpu.writeData(EICRA, 1 << ISC01); 256 | 257 | expect(cpu.data[EIFR]).toEqual(0); 258 | port.setPin(PD2, true); 259 | expect(cpu.data[EIFR]).toEqual(0); 260 | port.setPin(PD2, false); 261 | expect(cpu.data[EIFR]).toEqual(1 << INT0); 262 | 263 | cpu.data[SREG] = 0x80; // SREG: I------- (enable interrupts) 264 | cpu.tick(); 265 | expect(cpu.pc).toEqual(PC_INT_INT0); 266 | expect(cpu.cycles).toEqual(2); 267 | expect(cpu.data[EIFR]).toEqual(0); 268 | }); 269 | 270 | it('should generate INT0 interrupt on level change', () => { 271 | const cpu = new CPU(new Uint16Array(1024)); 272 | const port = new AVRIOPort(cpu, portDConfig); 273 | cpu.writeData(EIMSK, 1 << INT0); 274 | cpu.writeData(EICRA, 1 << ISC00); 275 | 276 | expect(cpu.data[EIFR]).toEqual(0); 277 | port.setPin(PD2, true); 278 | expect(cpu.data[EIFR]).toEqual(1 << INT0); 279 | cpu.writeData(EIFR, 1 << INT0); 280 | expect(cpu.data[EIFR]).toEqual(0); 281 | port.setPin(PD2, false); 282 | expect(cpu.data[EIFR]).toEqual(1 << INT0); 283 | }); 284 | 285 | it('should a sticky INT0 interrupt while the pin level is low', () => { 286 | const cpu = new CPU(new Uint16Array(1024)); 287 | const port = new AVRIOPort(cpu, portDConfig); 288 | cpu.writeData(EIMSK, 1 << INT0); 289 | cpu.writeData(EICRA, 0); 290 | expect(cpu.data[EIFR]).toEqual(0); 291 | 292 | port.setPin(PD2, true); 293 | expect(cpu.data[EIFR]).toEqual(0); 294 | 295 | port.setPin(PD2, false); 296 | expect(cpu.data[EIFR]).toEqual(1 << INT0); 297 | 298 | // This is a sticky interrupt, verify we can't clear the flag: 299 | cpu.writeData(EIFR, 1 << INT0); 300 | expect(cpu.data[EIFR]).toEqual(1 << INT0); 301 | 302 | cpu.data[SREG] = 0x80; // SREG: I------- (enable interrupts) 303 | cpu.tick(); 304 | expect(cpu.pc).toEqual(PC_INT_INT0); 305 | expect(cpu.cycles).toEqual(2); 306 | 307 | // Flag shouldn't be cleared, as the interrupt is sticky 308 | expect(cpu.data[EIFR]).toEqual(1 << INT0); 309 | 310 | // But it will be cleared as soon as the pin goes high. 311 | port.setPin(PD2, true); 312 | expect(cpu.data[EIFR]).toEqual(0); 313 | }); 314 | }); 315 | 316 | describe('Pin change interrupts (PCINT)', () => { 317 | it('should generate a pin change interrupt when PB3 (PCINT3) goes high', () => { 318 | const cpu = new CPU(new Uint16Array(1024)); 319 | const port = new AVRIOPort(cpu, portBConfig); 320 | cpu.writeData(PCICR, 1 << PCIE0); 321 | cpu.writeData(PCMSK0, 1 << PCINT3); 322 | 323 | port.setPin(PB3, true); 324 | expect(cpu.data[PCIFR]).toEqual(1 << PCIE0); 325 | 326 | cpu.data[SREG] = 0x80; // SREG: I------- 327 | cpu.tick(); 328 | expect(cpu.pc).toEqual(PC_INT_PCINT0); 329 | expect(cpu.cycles).toEqual(2); 330 | expect(cpu.data[PCIFR]).toEqual(0); 331 | }); 332 | 333 | it('should generate a pin change interrupt when PB3 (PCINT3) goes low', () => { 334 | const cpu = new CPU(new Uint16Array(1024)); 335 | const port = new AVRIOPort(cpu, portBConfig); 336 | 337 | port.setPin(PB3, true); 338 | cpu.writeData(PCICR, 1 << PCIE0); 339 | cpu.writeData(PCMSK0, 1 << PCINT3); 340 | expect(cpu.data[PCIFR]).toEqual(0); 341 | 342 | port.setPin(PB3, false); 343 | expect(cpu.data[PCIFR]).toEqual(1 << PCIE0); 344 | 345 | cpu.data[SREG] = 0x80; // SREG: I------- 346 | cpu.tick(); 347 | expect(cpu.pc).toEqual(PC_INT_PCINT0); 348 | expect(cpu.cycles).toEqual(2); 349 | expect(cpu.data[PCIFR]).toEqual(0); 350 | }); 351 | 352 | it('should clear the interrupt flag when writing to PCIFR', () => { 353 | const cpu = new CPU(new Uint16Array(1024)); 354 | const port = new AVRIOPort(cpu, portBConfig); 355 | cpu.writeData(PCICR, 1 << PCIE0); 356 | cpu.writeData(PCMSK0, 1 << PCINT3); 357 | 358 | port.setPin(PB3, true); 359 | expect(cpu.data[PCIFR]).toEqual(1 << PCIE0); 360 | 361 | cpu.writeData(PCIFR, 1 << PCIE0); 362 | expect(cpu.data[PCIFR]).toEqual(0); 363 | }); 364 | }); 365 | }); 366 | -------------------------------------------------------------------------------- /src/peripherals/usart.spec.ts: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | // Copyright (c) Uri Shaked and contributors 3 | 4 | import { describe, expect, it, vi } from 'vitest'; 5 | import { CPU } from '../cpu/cpu'; 6 | import { AVRUSART, usart0Config } from './usart'; 7 | 8 | const FREQ_16MHZ = 16e6; 9 | const FREQ_11_0529MHZ = 11059200; 10 | 11 | // CPU registers 12 | const SREG = 95; 13 | 14 | // USART0 Registers 15 | const UCSR0A = 0xc0; 16 | const UCSR0B = 0xc1; 17 | const UCSR0C = 0xc2; 18 | const UBRR0L = 0xc4; 19 | const UBRR0H = 0xc5; 20 | const UDR0 = 0xc6; 21 | 22 | // Register bit names 23 | const U2X0 = 2; 24 | const TXEN = 8; 25 | const RXEN = 16; 26 | const UDRIE = 0x20; 27 | const TXCIE = 0x40; 28 | const RXC = 0x80; 29 | const TXC = 0x40; 30 | const UDRE = 0x20; 31 | const USBS = 0x08; 32 | const UPM0 = 0x10; 33 | const UPM1 = 0x20; 34 | 35 | // Interrupt address 36 | const PC_INT_UDRE = 0x26; 37 | const PC_INT_TXC = 0x28; 38 | const UCSZ0 = 2; 39 | const UCSZ1 = 4; 40 | const UCSZ2 = 4; 41 | 42 | describe('USART', () => { 43 | it('should correctly calculate the baudRate from UBRR', () => { 44 | const cpu = new CPU(new Uint16Array(1024)); 45 | const usart = new AVRUSART(cpu, usart0Config, FREQ_11_0529MHZ); 46 | cpu.writeData(UBRR0H, 0); 47 | cpu.writeData(UBRR0L, 5); 48 | expect(usart.baudRate).toEqual(115200); 49 | }); 50 | 51 | it('should correctly calculate the baudRate from UBRR in double-speed mode', () => { 52 | const cpu = new CPU(new Uint16Array(1024)); 53 | const usart = new AVRUSART(cpu, usart0Config, FREQ_16MHZ); 54 | cpu.writeData(UBRR0H, 3); 55 | cpu.writeData(UBRR0L, 64); 56 | cpu.writeData(UCSR0A, U2X0); 57 | expect(usart.baudRate).toEqual(2400); 58 | }); 59 | 60 | it('should call onConfigurationChange when the baudRate changes', () => { 61 | const cpu = new CPU(new Uint16Array(1024)); 62 | const usart = new AVRUSART(cpu, usart0Config, FREQ_16MHZ); 63 | const onConfigurationChange = vi.fn(); 64 | usart.onConfigurationChange = onConfigurationChange; 65 | 66 | cpu.writeData(UBRR0H, 0); 67 | expect(onConfigurationChange).toHaveBeenCalled(); 68 | 69 | onConfigurationChange.mockClear(); 70 | cpu.writeData(UBRR0L, 5); 71 | expect(onConfigurationChange).toHaveBeenCalled(); 72 | 73 | onConfigurationChange.mockClear(); 74 | cpu.writeData(UCSR0A, U2X0); 75 | expect(onConfigurationChange).toHaveBeenCalled(); 76 | }); 77 | 78 | describe('bitsPerChar', () => { 79 | it('should return 5-bits per byte when UCSZ = 0', () => { 80 | const cpu = new CPU(new Uint16Array(1024)); 81 | const usart = new AVRUSART(cpu, usart0Config, FREQ_16MHZ); 82 | cpu.writeData(UCSR0C, 0); 83 | expect(usart.bitsPerChar).toEqual(5); 84 | }); 85 | 86 | it('should return 6-bits per byte when UCSZ = 1', () => { 87 | const cpu = new CPU(new Uint16Array(1024)); 88 | const usart = new AVRUSART(cpu, usart0Config, FREQ_16MHZ); 89 | cpu.writeData(UCSR0C, UCSZ0); 90 | expect(usart.bitsPerChar).toEqual(6); 91 | }); 92 | 93 | it('should return 7-bits per byte when UCSZ = 2', () => { 94 | const cpu = new CPU(new Uint16Array(1024)); 95 | const usart = new AVRUSART(cpu, usart0Config, FREQ_16MHZ); 96 | cpu.writeData(UCSR0C, UCSZ1); 97 | expect(usart.bitsPerChar).toEqual(7); 98 | }); 99 | 100 | it('should return 8-bits per byte when UCSZ = 3', () => { 101 | const cpu = new CPU(new Uint16Array(1024)); 102 | const usart = new AVRUSART(cpu, usart0Config, FREQ_16MHZ); 103 | cpu.writeData(UCSR0C, UCSZ0 | UCSZ1); 104 | expect(usart.bitsPerChar).toEqual(8); 105 | }); 106 | 107 | it('should return 9-bits per byte when UCSZ = 7', () => { 108 | const cpu = new CPU(new Uint16Array(1024)); 109 | const usart = new AVRUSART(cpu, usart0Config, FREQ_16MHZ); 110 | cpu.writeData(UCSR0C, UCSZ0 | UCSZ1); 111 | cpu.writeData(UCSR0B, UCSZ2); 112 | expect(usart.bitsPerChar).toEqual(9); 113 | }); 114 | 115 | it('should call onConfigurationChange when bitsPerChar change', () => { 116 | const cpu = new CPU(new Uint16Array(1024)); 117 | const usart = new AVRUSART(cpu, usart0Config, FREQ_16MHZ); 118 | const onConfigurationChange = vi.fn(); 119 | usart.onConfigurationChange = onConfigurationChange; 120 | 121 | cpu.writeData(UCSR0C, UCSZ0 | UCSZ1); 122 | expect(onConfigurationChange).toHaveBeenCalled(); 123 | 124 | onConfigurationChange.mockClear(); 125 | cpu.writeData(UCSR0B, UCSZ2); 126 | expect(onConfigurationChange).toHaveBeenCalled(); 127 | 128 | onConfigurationChange.mockClear(); 129 | cpu.writeData(UCSR0B, UCSZ2); 130 | expect(onConfigurationChange).not.toHaveBeenCalled(); 131 | }); 132 | }); 133 | 134 | describe('stopBits', () => { 135 | it('should return 1 when USBS = 0', () => { 136 | const cpu = new CPU(new Uint16Array(1024)); 137 | const usart = new AVRUSART(cpu, usart0Config, FREQ_16MHZ); 138 | expect(usart.stopBits).toEqual(1); 139 | }); 140 | 141 | it('should return 2 when USBS = 1', () => { 142 | const cpu = new CPU(new Uint16Array(1024)); 143 | const usart = new AVRUSART(cpu, usart0Config, FREQ_16MHZ); 144 | cpu.writeData(UCSR0C, USBS); 145 | expect(usart.stopBits).toEqual(2); 146 | }); 147 | }); 148 | 149 | describe('parityEnabled', () => { 150 | it('should return false when UPM1 = 0', () => { 151 | const cpu = new CPU(new Uint16Array(1024)); 152 | const usart = new AVRUSART(cpu, usart0Config, FREQ_16MHZ); 153 | expect(usart.parityEnabled).toEqual(false); 154 | }); 155 | 156 | it('should return true when UPM1 = 1', () => { 157 | const cpu = new CPU(new Uint16Array(1024)); 158 | const usart = new AVRUSART(cpu, usart0Config, FREQ_16MHZ); 159 | cpu.writeData(UCSR0C, UPM1); 160 | expect(usart.parityEnabled).toEqual(true); 161 | }); 162 | }); 163 | 164 | describe('parityOdd', () => { 165 | it('should return false when UPM0 = 0', () => { 166 | const cpu = new CPU(new Uint16Array(1024)); 167 | const usart = new AVRUSART(cpu, usart0Config, FREQ_16MHZ); 168 | expect(usart.parityOdd).toEqual(false); 169 | }); 170 | 171 | it('should return true when UPM0 = 1', () => { 172 | const cpu = new CPU(new Uint16Array(1024)); 173 | const usart = new AVRUSART(cpu, usart0Config, FREQ_16MHZ); 174 | cpu.writeData(UCSR0C, UPM0); 175 | expect(usart.parityOdd).toEqual(true); 176 | }); 177 | }); 178 | 179 | it('should invoke onByteTransmit when UDR0 is written to', () => { 180 | const cpu = new CPU(new Uint16Array(1024)); 181 | const usart = new AVRUSART(cpu, usart0Config, FREQ_16MHZ); 182 | usart.onByteTransmit = vi.fn(); 183 | cpu.writeData(UCSR0B, TXEN); 184 | cpu.writeData(UDR0, 0x61); 185 | expect(usart.onByteTransmit).toHaveBeenCalledWith(0x61); 186 | }); 187 | 188 | describe('txEnable/rxEnable', () => { 189 | it('txEnable should equal true when the transitter is enabled', () => { 190 | const cpu = new CPU(new Uint16Array(1024)); 191 | const usart = new AVRUSART(cpu, usart0Config, FREQ_16MHZ); 192 | usart.onByteTransmit = vi.fn(); 193 | expect(usart.txEnable).toEqual(false); 194 | cpu.writeData(UCSR0B, TXEN); 195 | expect(usart.txEnable).toEqual(true); 196 | }); 197 | 198 | it('rxEnable should equal true when the transitter is enabled', () => { 199 | const cpu = new CPU(new Uint16Array(1024)); 200 | const usart = new AVRUSART(cpu, usart0Config, FREQ_16MHZ); 201 | usart.onByteTransmit = vi.fn(); 202 | expect(usart.rxEnable).toEqual(false); 203 | cpu.writeData(UCSR0B, RXEN); 204 | expect(usart.rxEnable).toEqual(true); 205 | }); 206 | }); 207 | 208 | describe('tick()', () => { 209 | it('should trigger data register empty interrupt if UDRE is set', () => { 210 | const cpu = new CPU(new Uint16Array(1024)); 211 | new AVRUSART(cpu, usart0Config, FREQ_16MHZ); 212 | cpu.writeData(UCSR0B, UDRIE | TXEN); 213 | cpu.data[SREG] = 0x80; // SREG: I------- 214 | cpu.tick(); 215 | expect(cpu.pc).toEqual(PC_INT_UDRE); 216 | expect(cpu.cycles).toEqual(2); 217 | expect(cpu.data[UCSR0A] & UDRE).toEqual(0); 218 | }); 219 | 220 | it('should trigger data TX Complete interrupt if TXCIE is set', () => { 221 | const cpu = new CPU(new Uint16Array(1024)); 222 | new AVRUSART(cpu, usart0Config, FREQ_16MHZ); 223 | cpu.writeData(UCSR0B, TXCIE | TXEN); 224 | cpu.writeData(UDR0, 0x61); 225 | cpu.data[SREG] = 0x80; // SREG: I------- 226 | cpu.cycles = 1e6; 227 | cpu.tick(); 228 | expect(cpu.pc).toEqual(PC_INT_TXC); 229 | expect(cpu.cycles).toEqual(1e6 + 2); 230 | expect(cpu.data[UCSR0A] & TXC).toEqual(0); 231 | }); 232 | 233 | it('should not trigger data TX Complete interrupt if UDR was not written to', () => { 234 | const cpu = new CPU(new Uint16Array(1024)); 235 | new AVRUSART(cpu, usart0Config, FREQ_16MHZ); 236 | cpu.writeData(UCSR0B, TXCIE | TXEN); 237 | cpu.data[SREG] = 0x80; // SREG: I------- 238 | cpu.tick(); 239 | expect(cpu.pc).toEqual(0); 240 | expect(cpu.cycles).toEqual(0); 241 | }); 242 | 243 | it('should not trigger any interrupt if interrupts are disabled', () => { 244 | const cpu = new CPU(new Uint16Array(1024)); 245 | new AVRUSART(cpu, usart0Config, FREQ_16MHZ); 246 | cpu.writeData(UCSR0B, UDRIE | TXEN); 247 | cpu.writeData(UDR0, 0x61); 248 | cpu.data[SREG] = 0; // SREG: 0 (disable interrupts) 249 | cpu.cycles = 1e6; 250 | cpu.tick(); 251 | expect(cpu.pc).toEqual(0); 252 | expect(cpu.cycles).toEqual(1e6); 253 | expect(cpu.data[UCSR0A]).toEqual(TXC | UDRE); 254 | }); 255 | }); 256 | 257 | describe('onLineTransmit', () => { 258 | it('should call onLineTransmit with the current line buffer after every newline', () => { 259 | const cpu = new CPU(new Uint16Array(1024)); 260 | const usart = new AVRUSART(cpu, usart0Config, FREQ_16MHZ); 261 | usart.onLineTransmit = vi.fn(); 262 | cpu.writeData(UCSR0B, TXEN); 263 | cpu.writeData(UDR0, 0x48); // 'H' 264 | cpu.writeData(UDR0, 0x65); // 'e' 265 | cpu.writeData(UDR0, 0x6c); // 'l' 266 | cpu.writeData(UDR0, 0x6c); // 'l' 267 | cpu.writeData(UDR0, 0x6f); // 'o' 268 | cpu.writeData(UDR0, 0xa); // '\n' 269 | expect(usart.onLineTransmit).toHaveBeenCalledWith('Hello'); 270 | }); 271 | 272 | it('should not call onLineTransmit if no newline was received', () => { 273 | const cpu = new CPU(new Uint16Array(1024)); 274 | const usart = new AVRUSART(cpu, usart0Config, FREQ_16MHZ); 275 | usart.onLineTransmit = vi.fn(); 276 | cpu.writeData(UCSR0B, TXEN); 277 | cpu.writeData(UDR0, 0x48); // 'H' 278 | cpu.writeData(UDR0, 0x69); // 'i' 279 | expect(usart.onLineTransmit).not.toHaveBeenCalled(); 280 | }); 281 | 282 | it('should clear the line buffer after each call to onLineTransmit', () => { 283 | const cpu = new CPU(new Uint16Array(1024)); 284 | const usart = new AVRUSART(cpu, usart0Config, FREQ_16MHZ); 285 | usart.onLineTransmit = vi.fn(); 286 | cpu.writeData(UCSR0B, TXEN); 287 | cpu.writeData(UDR0, 0x48); // 'H' 288 | cpu.writeData(UDR0, 0x69); // 'i' 289 | cpu.writeData(UDR0, 0xa); // '\n' 290 | cpu.writeData(UDR0, 0x74); // 't' 291 | cpu.writeData(UDR0, 0x68); // 'h' 292 | cpu.writeData(UDR0, 0x65); // 'e' 293 | cpu.writeData(UDR0, 0x72); // 'r' 294 | cpu.writeData(UDR0, 0x65); // 'e' 295 | cpu.writeData(UDR0, 0xa); // '\n' 296 | expect(usart.onLineTransmit).toHaveBeenCalledWith('there'); 297 | }); 298 | }); 299 | 300 | describe('writeByte', () => { 301 | it('should return false if called when RX is busy', () => { 302 | const cpu = new CPU(new Uint16Array(1024)); 303 | const usart = new AVRUSART(cpu, usart0Config, FREQ_16MHZ); 304 | cpu.writeData(UCSR0B, RXEN); 305 | cpu.writeData(UBRR0L, 103); // baud: 9600 306 | expect(usart.writeByte(10)).toEqual(true); 307 | expect(usart.writeByte(10)).toEqual(false); 308 | cpu.tick(); 309 | expect(usart.writeByte(10)).toEqual(false); 310 | }); 311 | }); 312 | 313 | describe('Integration tests', () => { 314 | it('should set the TXC bit after ~1.04mS when baud rate set to 9600', () => { 315 | const cpu = new CPU(new Uint16Array(1024)); 316 | new AVRUSART(cpu, usart0Config, FREQ_16MHZ); 317 | cpu.writeData(UCSR0B, TXEN); 318 | cpu.writeData(UBRR0L, 103); // baud: 9600 319 | cpu.writeData(UDR0, 0x48); // 'H' 320 | cpu.cycles += 16000; // 1ms 321 | cpu.tick(); 322 | expect(cpu.data[UCSR0A] & TXC).toEqual(0); 323 | cpu.cycles += 800; // 0.05ms 324 | cpu.tick(); 325 | expect(cpu.data[UCSR0A] & TXC).toEqual(TXC); 326 | }); 327 | 328 | it('should be ready to recieve the next byte after ~1.04ms when baudrate set to 9600', () => { 329 | const cpu = new CPU(new Uint16Array(1024)); 330 | const usart = new AVRUSART(cpu, usart0Config, FREQ_16MHZ); 331 | const rxCompleteCallback = vi.fn(); 332 | usart.onRxComplete = rxCompleteCallback; 333 | cpu.writeData(UCSR0B, RXEN); 334 | cpu.writeData(UBRR0L, 103); // baud: 9600 335 | expect(usart.writeByte(0x42)).toBe(true); 336 | cpu.cycles += 16000; // 1ms 337 | cpu.tick(); 338 | expect(cpu.data[UCSR0A] & RXC).toEqual(0); // byte not received yet 339 | expect(usart.rxBusy).toBe(true); 340 | expect(rxCompleteCallback).not.toHaveBeenCalled(); 341 | cpu.cycles += 800; // 0.05ms 342 | cpu.tick(); 343 | expect(cpu.data[UCSR0A] & RXC).toEqual(RXC); 344 | expect(usart.rxBusy).toBe(false); 345 | expect(rxCompleteCallback).toHaveBeenCalled(); 346 | expect(cpu.readData(UDR0)).toEqual(0x42); 347 | expect(cpu.readData(UDR0)).toEqual(0); 348 | }); 349 | }); 350 | }); 351 | -------------------------------------------------------------------------------- /src/peripherals/twi.spec.ts: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | // Copyright (c) Uri Shaked and contributors 3 | 4 | import { describe, expect, it, vi } from 'vitest'; 5 | import { CPU } from '../cpu/cpu'; 6 | import { asmProgram, TestProgramRunner } from '../utils/test-utils'; 7 | import { AVRTWI, twiConfig } from './twi'; 8 | 9 | const FREQ_16MHZ = 16e6; 10 | 11 | // CPU registers 12 | const R16 = 16; 13 | const R17 = 17; 14 | const SREG = 95; 15 | 16 | // TWI Registers 17 | const TWBR = 0xb8; 18 | const TWSR = 0xb9; 19 | const TWDR = 0xbb; 20 | const TWCR = 0xbc; 21 | 22 | // Register bit names 23 | const TWIE = 1; 24 | const TWEN = 4; 25 | const TWSTO = 0x10; 26 | const TWSTA = 0x20; 27 | const TWEA = 0x40; 28 | const TWINT = 0x80; 29 | 30 | const onTestBreak = (cpu: CPU) => { 31 | console.log(cpu.data[TWCR].toString(16)); 32 | console.log(cpu.data[R16]); 33 | }; 34 | 35 | describe('TWI', () => { 36 | it('should correctly calculate the sclFrequency from TWBR', () => { 37 | const cpu = new CPU(new Uint16Array(1024)); 38 | const twi = new AVRTWI(cpu, twiConfig, FREQ_16MHZ); 39 | cpu.writeData(TWBR, 0x48); 40 | cpu.writeData(TWSR, 0); // prescaler: 1 41 | expect(twi.sclFrequency).toEqual(100000); 42 | }); 43 | 44 | it('should take the prescaler into consideration when calculating sclFrequency', () => { 45 | const cpu = new CPU(new Uint16Array(1024)); 46 | const twi = new AVRTWI(cpu, twiConfig, FREQ_16MHZ); 47 | cpu.writeData(TWBR, 0x03); 48 | cpu.writeData(TWSR, 0x01); // prescaler: 4 49 | expect(twi.sclFrequency).toEqual(400000); 50 | }); 51 | 52 | it('should trigger data an interrupt if TWINT is set', () => { 53 | const cpu = new CPU(new Uint16Array(1024)); 54 | const twi = new AVRTWI(cpu, twiConfig, FREQ_16MHZ); 55 | cpu.writeData(TWCR, TWIE); 56 | cpu.data[SREG] = 0x80; // SREG: I------- 57 | twi.completeStart(); // This will set the TWINT flag 58 | cpu.tick(); 59 | expect(cpu.pc).toEqual(0x30); // 2-wire Serial Interface Vector 60 | expect(cpu.cycles).toEqual(2); 61 | expect(cpu.data[TWCR] & TWINT).toEqual(0); 62 | }); 63 | 64 | describe('Master mode', () => { 65 | it('should call the startEvent handler when TWSTA bit is written 1', () => { 66 | const cpu = new CPU(new Uint16Array(1024)); 67 | const twi = new AVRTWI(cpu, twiConfig, FREQ_16MHZ); 68 | vi.spyOn(twi.eventHandler, 'start'); 69 | cpu.writeData(TWCR, TWINT | TWSTA | TWEN); 70 | cpu.cycles++; 71 | cpu.tick(); 72 | expect(twi.eventHandler.start).toHaveBeenCalledWith(false); 73 | }); 74 | 75 | it('should connect successfully in case of repeated start (issue #91)', () => { 76 | const cpu = new CPU(new Uint16Array(1024)); 77 | const twi = new AVRTWI(cpu, twiConfig, FREQ_16MHZ); 78 | 79 | // Start condition 80 | cpu.writeData(TWCR, TWINT | TWSTA | TWEN); 81 | cpu.cycles++; 82 | cpu.tick(); 83 | 84 | // Repeated start 85 | vi.spyOn(twi.eventHandler, 'start'); 86 | cpu.writeData(TWCR, TWINT | TWSTA | TWEN); 87 | cpu.cycles++; 88 | cpu.tick(); 89 | expect(twi.eventHandler.start).toHaveBeenCalledWith(true); 90 | 91 | // Now try to connect... 92 | vi.spyOn(twi.eventHandler, 'connectToSlave'); 93 | cpu.writeData(TWDR, 0x80); // Address 0x40, write mode 94 | cpu.writeData(TWCR, TWINT | TWEN); 95 | cpu.cycles++; 96 | cpu.tick(); 97 | expect(twi.eventHandler.connectToSlave).toHaveBeenCalledWith(0x40, true); 98 | }); 99 | 100 | it('should successfully transmit a byte to a slave', () => { 101 | // based on the example in page 225 of the datasheet: 102 | // https://ww1.microchip.com/downloads/en/DeviceDoc/ATmega48A-PA-88A-PA-168A-PA-328-P-DS-DS40002061A.pdf 103 | const { program } = asmProgram(` 104 | ; register addresses 105 | _REPLACE TWSR, ${TWSR} 106 | _REPLACE TWDR, ${TWDR} 107 | _REPLACE TWCR, ${TWCR} 108 | 109 | ; TWCR bits 110 | _REPLACE TWEN, ${TWEN} 111 | _REPLACE TWSTO, ${TWSTO} 112 | _REPLACE TWSTA, ${TWSTA} 113 | _REPLACE TWINT, ${TWINT} 114 | 115 | ; TWSR states 116 | _REPLACE START, 0x8 ; TWI start 117 | _REPLACE MT_SLA_ACK, 0x18 ; Slave Adresss ACK has been received 118 | _REPLACE MT_DATA_ACK, 0x28 ; Data ACK has been received 119 | 120 | ; Send start condition 121 | ldi r16, TWEN 122 | sbr r16, TWSTA 123 | sbr r16, TWINT 124 | sts TWCR, r16 125 | 126 | ; Wait for TWINT Flag set. This indicates that the START condition has been transmitted 127 | call wait_for_twint 128 | 129 | ; Check value of TWI Status Register. Mask prescaler bits. If status different from START go to ERROR 130 | lds r16, TWSR 131 | andi r16, 0xf8 132 | cpi r16, START 133 | brne error 134 | 135 | ; Load SLA_W into TWDR Register. Clear TWINT bit in TWCR to start transmission of address 136 | ; 0x44 = Address 0x22, write mode (R/W bit clear) 137 | _REPLACE SLA_W, 0x44 138 | ldi r16, SLA_W 139 | sts TWDR, r16 140 | ldi r16, TWINT 141 | sbr r16, TWEN 142 | sts TWCR, r16 143 | 144 | ; Wait for TWINT Flag set. This indicates that the SLA+W has been transmitted, and ACK/NACK has been received. 145 | call wait_for_twint 146 | 147 | ; Check value of TWI Status Register. Mask prescaler bits. If status different from MT_SLA_ACK go to ERROR 148 | lds r16, TWSR 149 | andi r16, 0xf8 150 | cpi r16, MT_SLA_ACK 151 | brne error 152 | 153 | ; Load DATA into TWDR Register. Clear TWINT bit in TWCR to start transmission of data 154 | _replace DATA, 0x55 155 | ldi r16, DATA 156 | sts TWDR, r16 157 | ldi r16, TWINT 158 | sbr r16, TWEN 159 | sts TWCR, r16 160 | 161 | ; Wait for TWINT Flag set. This indicates that the DATA has been transmitted, and ACK/NACK has been received 162 | call wait_for_twint 163 | 164 | ; Check value of TWI Status Register. Mask prescaler bits. If status different from MT_DATA_ACK go to ERROR 165 | lds r16, TWSR 166 | andi r16, 0xf8 167 | cpi r16, MT_DATA_ACK 168 | brne error 169 | 170 | ; Transmit STOP condition 171 | ldi r16, TWINT 172 | sbr r16, TWEN 173 | sbr r16, TWSTO 174 | sts TWCR, r16 175 | 176 | ; Wait for TWINT Flag set. This indicates that the STOP condition has been sent 177 | call wait_for_twint 178 | 179 | ; Check value of TWI Status Register. The masked value should be 0xf8 once done 180 | lds r16, TWSR 181 | andi r16, 0xf8 182 | cpi r16, 0xf8 183 | brne error 184 | 185 | ; Indicate success by loading 0x42 into r17 186 | ldi r17, 0x42 187 | 188 | loop: 189 | jmp loop 190 | 191 | ; Busy-waits for the TWINT flag to be set 192 | wait_for_twint: 193 | lds r16, TWCR 194 | andi r16, TWINT 195 | breq wait_for_twint 196 | ret 197 | 198 | ; In case of an error, toggle a breakpoint 199 | error: 200 | break 201 | `); 202 | const cpu = new CPU(program); 203 | const twi = new AVRTWI(cpu, twiConfig, FREQ_16MHZ); 204 | const runner = new TestProgramRunner(cpu, onTestBreak); 205 | twi.eventHandler = { 206 | start: vi.fn(), 207 | stop: vi.fn(), 208 | connectToSlave: vi.fn(), 209 | writeByte: vi.fn(), 210 | readByte: vi.fn(), 211 | }; 212 | 213 | // Step 1: wait for start condition 214 | runner.runInstructions(4); 215 | expect(twi.eventHandler.start).toHaveBeenCalledWith(false); 216 | 217 | runner.runInstructions(16); 218 | twi.completeStart(); 219 | 220 | // Step 2: wait for slave connect in write mode 221 | runner.runInstructions(16); 222 | expect(twi.eventHandler.connectToSlave).toHaveBeenCalledWith(0x22, true); 223 | 224 | runner.runInstructions(16); 225 | twi.completeConnect(true); 226 | 227 | // Step 3: wait for first data byte 228 | runner.runInstructions(16); 229 | expect(twi.eventHandler.writeByte).toHaveBeenCalledWith(0x55); 230 | 231 | runner.runInstructions(16); 232 | twi.completeWrite(true); 233 | 234 | // Step 4: wait for stop condition 235 | runner.runInstructions(16); 236 | expect(twi.eventHandler.stop).toHaveBeenCalled(); 237 | 238 | runner.runInstructions(16); 239 | twi.completeStop(); 240 | 241 | // Step 5: wait for the assembly code to indicate success by settings r17 to 0x42 242 | runner.runInstructions(16); 243 | expect(cpu.data[R17]).toEqual(0x42); 244 | }); 245 | 246 | it('should successfully receive a byte from a slave', () => { 247 | const { program } = asmProgram(` 248 | ; register addresses 249 | _REPLACE TWSR, ${TWSR} 250 | _REPLACE TWDR, ${TWDR} 251 | _REPLACE TWCR, ${TWCR} 252 | 253 | ; TWCR bits 254 | _REPLACE TWEN, ${TWEN} 255 | _REPLACE TWSTO, ${TWSTO} 256 | _REPLACE TWSTA, ${TWSTA} 257 | _REPLACE TWEA, ${TWEA} 258 | _REPLACE TWINT, ${TWINT} 259 | 260 | ; TWSR states 261 | _REPLACE START, 0x8 ; TWI start 262 | _REPLACE MT_SLAR_ACK, 0x40 ; Slave Adresss ACK has been received 263 | _REPLACE MT_DATA_RECV, 0x50 ; Data has been received 264 | _REPLACE MT_DATA_RECV_NACK, 0x58 ; Data has been received, NACK has been returned 265 | 266 | ; Send start condition 267 | ldi r16, TWEN 268 | sbr r16, TWSTA 269 | sbr r16, TWINT 270 | sts TWCR, r16 271 | 272 | ; Wait for TWINT Flag set. This indicates that the START condition has been transmitted 273 | call wait_for_twint 274 | 275 | ; Check value of TWI Status Register. Mask prescaler bits. If status different from START go to ERROR 276 | lds r16, TWSR 277 | andi r16, 0xf8 278 | ldi r18, START 279 | cpse r16, r18 280 | jmp error ; only jump if r16 != r18 (START) 281 | 282 | ; Load SLA_R into TWDR Register. Clear TWINT bit in TWCR to start transmission of address 283 | ; 0xa1 = Address 0x50, read mode (R/W bit set) 284 | _REPLACE SLA_R, 0xa1 285 | ldi r16, SLA_R 286 | sts TWDR, r16 287 | ldi r16, TWINT 288 | sbr r16, TWEN 289 | sts TWCR, r16 290 | 291 | ; Wait for TWINT Flag set. This indicates that the SLA+W has been transmitted, and ACK/NACK has been received. 292 | call wait_for_twint 293 | 294 | ; Check value of TWI Status Register. Mask prescaler bits. If status different from MT_SLA_ACK go to ERROR 295 | lds r16, TWSR 296 | andi r16, 0xf8 297 | cpi r16, MT_SLAR_ACK 298 | brne error 299 | 300 | ; Clear TWINT bit in TWCR to receive the next byte, set TWEA to send ACK 301 | ldi r16, TWINT 302 | sbr r16, TWEA 303 | sbr r16, TWEN 304 | sts TWCR, r16 305 | 306 | ; Wait for TWINT Flag set. This indicates that the DATA has been received, and ACK has been transmitted 307 | call wait_for_twint 308 | 309 | ; Check value of TWI Status Register. Mask prescaler bits. If status different from MT_DATA_RECV go to ERROR 310 | lds r16, TWSR 311 | andi r16, 0xf8 312 | cpi r16, MT_DATA_RECV 313 | brne error 314 | 315 | ; Validate that we recieved the desired data - first byte should be 0x66 316 | lds r16, TWDR 317 | cpi r16, 0x66 318 | brne error 319 | 320 | ; Clear TWINT bit in TWCR to receive the next byte, this time we don't ACK 321 | ldi r16, TWINT 322 | sbr r16, TWEN 323 | sts TWCR, r16 324 | 325 | ; Wait for TWINT Flag set. This indicates that the DATA has been received, and NACK has been transmitted 326 | call wait_for_twint 327 | 328 | ; Check value of TWI Status Register. Mask prescaler bits. If status different from MT_DATA_RECV_NACK go to ERROR 329 | lds r16, TWSR 330 | andi r16, 0xf8 331 | cpi r16, MT_DATA_RECV_NACK 332 | brne error 333 | 334 | ; Validate that we recieved the desired data - second byte should be 0x77 335 | lds r16, TWDR 336 | cpi r16, 0x77 337 | brne error 338 | 339 | ; Transmit STOP condition 340 | ldi r16, TWINT 341 | sbr r16, TWEN 342 | sbr r16, TWSTO 343 | sts TWCR, r16 344 | 345 | ; Wait for TWINT Flag set. This indicates that the STOP condition has been sent 346 | call wait_for_twint 347 | 348 | ; Check value of TWI Status Register. The masked value should be 0xf8 once done 349 | lds r16, TWSR 350 | andi r16, 0xf8 351 | cpi r16, 0xf8 352 | brne error 353 | 354 | ; Indicate success by loading 0x42 into r17 355 | ldi r17, 0x42 356 | 357 | loop: 358 | jmp loop 359 | 360 | ; Busy-waits for the TWINT flag to be set 361 | wait_for_twint: 362 | lds r16, TWCR 363 | andi r16, TWINT 364 | breq wait_for_twint 365 | ret 366 | 367 | ; In case of an error, toggle a breakpoint 368 | error: 369 | break 370 | `); 371 | const cpu = new CPU(program); 372 | const twi = new AVRTWI(cpu, twiConfig, FREQ_16MHZ); 373 | const runner = new TestProgramRunner(cpu, onTestBreak); 374 | twi.eventHandler = { 375 | start: vi.fn(), 376 | stop: vi.fn(), 377 | connectToSlave: vi.fn(), 378 | writeByte: vi.fn(), 379 | readByte: vi.fn(), 380 | }; 381 | 382 | // Step 1: wait for start condition 383 | runner.runInstructions(4); 384 | expect(twi.eventHandler.start).toHaveBeenCalledWith(false); 385 | 386 | runner.runInstructions(16); 387 | twi.completeStart(); 388 | 389 | // Step 2: wait for slave connect in read mode 390 | runner.runInstructions(16); 391 | expect(twi.eventHandler.connectToSlave).toHaveBeenCalledWith(0x50, false); 392 | 393 | runner.runInstructions(16); 394 | twi.completeConnect(true); 395 | 396 | // Step 3: send the first byte to the master, expect ack 397 | runner.runInstructions(16); 398 | expect(twi.eventHandler.readByte).toHaveBeenCalledWith(true); 399 | 400 | runner.runInstructions(16); 401 | twi.completeRead(0x66); 402 | 403 | // Step 4: send the first byte to the master, expect nack 404 | runner.runInstructions(16); 405 | expect(twi.eventHandler.readByte).toHaveBeenCalledWith(false); 406 | 407 | runner.runInstructions(16); 408 | twi.completeRead(0x77); 409 | 410 | // Step 5: wait for stop condition 411 | runner.runInstructions(24); 412 | expect(twi.eventHandler.stop).toHaveBeenCalled(); 413 | 414 | runner.runInstructions(16); 415 | twi.completeStop(); 416 | 417 | // Step 6: wait for the assembly code to indicate success by settings r17 to 0x42 418 | runner.runInstructions(16); 419 | expect(cpu.data[R17]).toEqual(0x42); 420 | }); 421 | }); 422 | }); 423 | -------------------------------------------------------------------------------- /src/peripherals/gpio.ts: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | // Copyright (c) Uri Shaked and contributors 3 | 4 | /** 5 | * AVR-8 GPIO Port implementation 6 | * Part of AVR8js 7 | * Reference: http://ww1.microchip.com/downloads/en/DeviceDoc/ATmega48A-PA-88A-PA-168A-PA-328-P-DS-DS40002061A.pdf 8 | * 9 | * Copyright (C) 2019-2023 Uri Shaked 10 | */ 11 | 12 | import { AVRInterruptConfig, CPU } from '../cpu/cpu'; 13 | import { u8 } from '../types'; 14 | 15 | export interface AVRExternalInterrupt { 16 | /** either EICRA or EICRB, depending on which register holds the ISCx0/ISCx1 bits for this interrupt */ 17 | EICR: u8; 18 | EIMSK: u8; 19 | EIFR: u8; 20 | 21 | /* Offset of the ISCx0/ISCx1 bits in the EICRx register */ 22 | iscOffset: u8; 23 | 24 | /** Bit index in the EIMSK / EIFR registers */ 25 | index: u8; // 0..7 26 | 27 | /** Interrupt vector index */ 28 | interrupt: u8; 29 | } 30 | 31 | export interface AVRPinChangeInterrupt { 32 | PCIE: u8; // bit index in PCICR/PCIFR 33 | PCICR: u8; 34 | PCIFR: u8; 35 | PCMSK: u8; 36 | pinChangeInterrupt: u8; 37 | mask: u8; 38 | offset: u8; 39 | } 40 | 41 | export interface AVRPortConfig { 42 | // Register addresses 43 | PIN: u8; 44 | DDR: u8; 45 | PORT: u8; 46 | 47 | // Interrupt settings 48 | pinChange?: AVRPinChangeInterrupt; 49 | externalInterrupts: (AVRExternalInterrupt | null)[]; 50 | } 51 | 52 | export const INT0: AVRExternalInterrupt = { 53 | EICR: 0x69, 54 | EIMSK: 0x3d, 55 | EIFR: 0x3c, 56 | index: 0, 57 | iscOffset: 0, 58 | interrupt: 2, 59 | }; 60 | 61 | export const INT1: AVRExternalInterrupt = { 62 | EICR: 0x69, 63 | EIMSK: 0x3d, 64 | EIFR: 0x3c, 65 | index: 1, 66 | iscOffset: 2, 67 | interrupt: 4, 68 | }; 69 | 70 | export const PCINT0 = { 71 | PCIE: 0, 72 | PCICR: 0x68, 73 | PCIFR: 0x3b, 74 | PCMSK: 0x6b, 75 | pinChangeInterrupt: 6, 76 | mask: 0xff, 77 | offset: 0, 78 | }; 79 | 80 | export const PCINT1 = { 81 | PCIE: 1, 82 | PCICR: 0x68, 83 | PCIFR: 0x3b, 84 | PCMSK: 0x6c, 85 | pinChangeInterrupt: 8, 86 | mask: 0xff, 87 | offset: 0, 88 | }; 89 | 90 | export const PCINT2 = { 91 | PCIE: 2, 92 | PCICR: 0x68, 93 | PCIFR: 0x3b, 94 | PCMSK: 0x6d, 95 | pinChangeInterrupt: 10, 96 | mask: 0xff, 97 | offset: 0, 98 | }; 99 | 100 | export type GPIOListener = (value: u8, oldValue: u8) => void; 101 | export type ExternalClockListener = (pinValue: boolean) => void; 102 | 103 | export const portAConfig: AVRPortConfig = { 104 | PIN: 0x20, 105 | DDR: 0x21, 106 | PORT: 0x22, 107 | externalInterrupts: [], 108 | }; 109 | 110 | export const portBConfig: AVRPortConfig = { 111 | PIN: 0x23, 112 | DDR: 0x24, 113 | PORT: 0x25, 114 | 115 | // Interrupt settings 116 | pinChange: PCINT0, 117 | externalInterrupts: [], 118 | }; 119 | 120 | export const portCConfig: AVRPortConfig = { 121 | PIN: 0x26, 122 | DDR: 0x27, 123 | PORT: 0x28, 124 | 125 | // Interrupt settings 126 | pinChange: PCINT1, 127 | externalInterrupts: [], 128 | }; 129 | 130 | export const portDConfig: AVRPortConfig = { 131 | PIN: 0x29, 132 | DDR: 0x2a, 133 | PORT: 0x2b, 134 | 135 | // Interrupt settings 136 | pinChange: PCINT2, 137 | externalInterrupts: [null, null, INT0, INT1], 138 | }; 139 | 140 | export const portEConfig: AVRPortConfig = { 141 | PIN: 0x2c, 142 | DDR: 0x2d, 143 | PORT: 0x2e, 144 | externalInterrupts: [], 145 | }; 146 | 147 | export const portFConfig: AVRPortConfig = { 148 | PIN: 0x2f, 149 | DDR: 0x30, 150 | PORT: 0x31, 151 | externalInterrupts: [], 152 | }; 153 | 154 | export const portGConfig: AVRPortConfig = { 155 | PIN: 0x32, 156 | DDR: 0x33, 157 | PORT: 0x34, 158 | externalInterrupts: [], 159 | }; 160 | 161 | export const portHConfig: AVRPortConfig = { 162 | PIN: 0x100, 163 | DDR: 0x101, 164 | PORT: 0x102, 165 | externalInterrupts: [], 166 | }; 167 | 168 | export const portJConfig: AVRPortConfig = { 169 | PIN: 0x103, 170 | DDR: 0x104, 171 | PORT: 0x105, 172 | externalInterrupts: [], 173 | }; 174 | 175 | export const portKConfig: AVRPortConfig = { 176 | PIN: 0x106, 177 | DDR: 0x107, 178 | PORT: 0x108, 179 | externalInterrupts: [], 180 | }; 181 | 182 | export const portLConfig: AVRPortConfig = { 183 | PIN: 0x109, 184 | DDR: 0x10a, 185 | PORT: 0x10b, 186 | externalInterrupts: [], 187 | }; 188 | 189 | export enum PinState { 190 | Low, 191 | High, 192 | Input, 193 | InputPullUp, 194 | } 195 | 196 | /* This mechanism allows timers to override specific GPIO pins */ 197 | export enum PinOverrideMode { 198 | None, 199 | Enable, 200 | Set, 201 | Clear, 202 | Toggle, 203 | } 204 | 205 | enum InterruptMode { 206 | LowLevel, 207 | Change, 208 | FallingEdge, 209 | RisingEdge, 210 | } 211 | 212 | export class AVRIOPort { 213 | readonly externalClockListeners: (ExternalClockListener | null)[] = []; 214 | 215 | private readonly externalInts: (AVRInterruptConfig | null)[]; 216 | private readonly PCINT: AVRInterruptConfig | null; 217 | private listeners: GPIOListener[] = []; 218 | private pinValue: u8 = 0; 219 | private overrideMask: u8 = 0xff; 220 | private overrideValue: u8 = 0; 221 | private lastValue: u8 = 0; 222 | private lastDdr: u8 = 0; 223 | private lastPin: u8 = 0; 224 | openCollector: u8 = 0; 225 | 226 | constructor( 227 | private cpu: CPU, 228 | readonly portConfig: Readonly, 229 | ) { 230 | cpu.gpioPorts.add(this); 231 | cpu.gpioByPort[portConfig.PORT] = this; 232 | 233 | cpu.writeHooks[portConfig.DDR] = (value: u8) => { 234 | const portValue = cpu.data[portConfig.PORT]; 235 | cpu.data[portConfig.DDR] = value; 236 | this.writeGpio(portValue, value); 237 | this.updatePinRegister(value); 238 | return true; 239 | }; 240 | cpu.writeHooks[portConfig.PORT] = (value: u8) => { 241 | const ddrMask = cpu.data[portConfig.DDR]; 242 | cpu.data[portConfig.PORT] = value; 243 | this.writeGpio(value, ddrMask); 244 | this.updatePinRegister(ddrMask); 245 | return true; 246 | }; 247 | cpu.writeHooks[portConfig.PIN] = (value: u8, oldValue, addr, mask) => { 248 | // Writing to 1 PIN toggles PORT bits 249 | const oldPortValue = cpu.data[portConfig.PORT]; 250 | const ddrMask = cpu.data[portConfig.DDR]; 251 | const portValue = oldPortValue ^ (value & mask); 252 | cpu.data[portConfig.PORT] = portValue; 253 | this.writeGpio(portValue, ddrMask); 254 | this.updatePinRegister(ddrMask); 255 | return true; 256 | }; 257 | 258 | // External interrupts 259 | const { externalInterrupts } = portConfig; 260 | this.externalInts = externalInterrupts.map((externalConfig) => 261 | externalConfig 262 | ? { 263 | address: externalConfig.interrupt, 264 | flagRegister: externalConfig.EIFR, 265 | flagMask: 1 << externalConfig.index, 266 | enableRegister: externalConfig.EIMSK, 267 | enableMask: 1 << externalConfig.index, 268 | } 269 | : null, 270 | ); 271 | const EICR = new Set(externalInterrupts.map((item) => item?.EICR)); 272 | for (const EICRx of EICR) { 273 | this.attachInterruptHook(EICRx || 0); 274 | } 275 | const EIMSK = externalInterrupts.find((item) => item && item.EIMSK)?.EIMSK ?? 0; 276 | this.attachInterruptHook(EIMSK, 'mask'); 277 | const EIFR = externalInterrupts.find((item) => item && item.EIFR)?.EIFR ?? 0; 278 | this.attachInterruptHook(EIFR, 'flag'); 279 | 280 | // Pin change interrupts 281 | const { pinChange } = portConfig; 282 | this.PCINT = pinChange 283 | ? { 284 | address: pinChange.pinChangeInterrupt, 285 | flagRegister: pinChange.PCIFR, 286 | flagMask: 1 << pinChange.PCIE, 287 | enableRegister: pinChange.PCICR, 288 | enableMask: 1 << pinChange.PCIE, 289 | } 290 | : null; 291 | if (pinChange) { 292 | const { PCIFR, PCMSK } = pinChange; 293 | cpu.writeHooks[PCIFR] = (value) => { 294 | for (const gpio of this.cpu.gpioPorts) { 295 | const { PCINT } = gpio; 296 | if (PCINT) { 297 | cpu.clearInterruptByFlag(PCINT, value); 298 | } 299 | } 300 | return true; 301 | }; 302 | cpu.writeHooks[PCMSK] = (value) => { 303 | cpu.data[PCMSK] = value; 304 | for (const gpio of this.cpu.gpioPorts) { 305 | const { PCINT } = gpio; 306 | if (PCINT) { 307 | cpu.updateInterruptEnable(PCINT, value); 308 | } 309 | } 310 | return true; 311 | }; 312 | } 313 | } 314 | 315 | addListener(listener: GPIOListener) { 316 | this.listeners.push(listener); 317 | } 318 | 319 | removeListener(listener: GPIOListener) { 320 | this.listeners = this.listeners.filter((l) => l !== listener); 321 | } 322 | 323 | /** 324 | * Get the state of a given GPIO pin 325 | * 326 | * @param index Pin index to return from 0 to 7 327 | * @returns PinState.Low or PinState.High if the pin is set to output, PinState.Input if the pin is set 328 | * to input, and PinState.InputPullUp if the pin is set to input and the internal pull-up resistor has 329 | * been enabled. 330 | */ 331 | pinState(index: number) { 332 | const ddr = this.cpu.data[this.portConfig.DDR]; 333 | const port = this.cpu.data[this.portConfig.PORT]; 334 | const bitMask = 1 << index; 335 | const openState = port & bitMask ? PinState.InputPullUp : PinState.Input; 336 | const highValue = this.openCollector & bitMask ? openState : PinState.High; 337 | if (ddr & bitMask) { 338 | return this.lastValue & bitMask ? highValue : PinState.Low; 339 | } else { 340 | return openState; 341 | } 342 | } 343 | 344 | /** 345 | * Sets the input value for the given pin. This is the value that 346 | * will be returned when reading from the PIN register. 347 | */ 348 | setPin(index: number, value: boolean) { 349 | const bitMask = 1 << index; 350 | this.pinValue &= ~bitMask; 351 | if (value) { 352 | this.pinValue |= bitMask; 353 | } 354 | this.updatePinRegister(this.cpu.data[this.portConfig.DDR]); 355 | } 356 | 357 | /** 358 | * Internal method - do not call this directly! 359 | * Used by the timer compare output units to override GPIO pins. 360 | */ 361 | timerOverridePin(pin: u8, mode: PinOverrideMode) { 362 | const { cpu, portConfig } = this; 363 | const pinMask = 1 << pin; 364 | if (mode === PinOverrideMode.None) { 365 | this.overrideMask |= pinMask; 366 | this.overrideValue &= ~pinMask; 367 | } else { 368 | this.overrideMask &= ~pinMask; 369 | switch (mode) { 370 | case PinOverrideMode.Enable: 371 | this.overrideValue &= ~pinMask; 372 | this.overrideValue |= cpu.data[portConfig.PORT] & pinMask; 373 | break; 374 | case PinOverrideMode.Set: 375 | this.overrideValue |= pinMask; 376 | break; 377 | case PinOverrideMode.Clear: 378 | this.overrideValue &= ~pinMask; 379 | break; 380 | case PinOverrideMode.Toggle: 381 | this.overrideValue ^= pinMask; 382 | break; 383 | } 384 | } 385 | const ddrMask = cpu.data[portConfig.DDR]; 386 | this.writeGpio(cpu.data[portConfig.PORT], ddrMask); 387 | this.updatePinRegister(ddrMask); 388 | } 389 | 390 | private updatePinRegister(ddr: u8) { 391 | const newPin = (this.pinValue & ~ddr) | (this.lastValue & ddr); 392 | this.cpu.data[this.portConfig.PIN] = newPin; 393 | if (this.lastPin !== newPin) { 394 | for (let index = 0; index < 8; index++) { 395 | if ((newPin & (1 << index)) !== (this.lastPin & (1 << index))) { 396 | const value = !!(newPin & (1 << index)); 397 | this.toggleInterrupt(index, value); 398 | this.externalClockListeners[index]?.(value); 399 | } 400 | } 401 | this.lastPin = newPin; 402 | } 403 | } 404 | 405 | private toggleInterrupt(pin: u8, risingEdge: boolean) { 406 | const { cpu, portConfig, externalInts, PCINT } = this; 407 | const { externalInterrupts, pinChange } = portConfig; 408 | const externalConfig = externalInterrupts[pin]; 409 | const external = externalInts[pin]; 410 | if (external && externalConfig) { 411 | const { EIMSK, index, EICR, iscOffset } = externalConfig; 412 | if (cpu.data[EIMSK] & (1 << index)) { 413 | const configuration = (cpu.data[EICR] >> iscOffset) & 0x3; 414 | let generateInterrupt = false; 415 | external.constant = false; 416 | switch (configuration) { 417 | case InterruptMode.LowLevel: 418 | generateInterrupt = !risingEdge; 419 | external.constant = true; 420 | break; 421 | case InterruptMode.Change: 422 | generateInterrupt = true; 423 | break; 424 | case InterruptMode.FallingEdge: 425 | generateInterrupt = !risingEdge; 426 | break; 427 | case InterruptMode.RisingEdge: 428 | generateInterrupt = risingEdge; 429 | break; 430 | } 431 | if (generateInterrupt) { 432 | cpu.setInterruptFlag(external); 433 | } else if (external.constant) { 434 | cpu.clearInterrupt(external, true); 435 | } 436 | } 437 | } 438 | 439 | if (pinChange && PCINT && pinChange.mask & (1 << pin)) { 440 | const { PCMSK } = pinChange; 441 | if (cpu.data[PCMSK] & (1 << (pin + pinChange.offset))) { 442 | cpu.setInterruptFlag(PCINT); 443 | } 444 | } 445 | } 446 | 447 | private attachInterruptHook(register: number, registerType: 'flag' | 'mask' | 'other' = 'other') { 448 | if (!register) { 449 | return; 450 | } 451 | 452 | const { cpu } = this; 453 | 454 | cpu.writeHooks[register] = (value: u8) => { 455 | if (registerType !== 'flag') { 456 | cpu.data[register] = value; 457 | } 458 | for (const gpio of cpu.gpioPorts) { 459 | for (const external of gpio.externalInts) { 460 | if (external && registerType === 'mask') { 461 | cpu.updateInterruptEnable(external, value); 462 | } 463 | if (external && !external.constant && registerType === 'flag') { 464 | cpu.clearInterruptByFlag(external, value); 465 | } 466 | } 467 | 468 | gpio.checkExternalInterrupts(); 469 | } 470 | 471 | return true; 472 | }; 473 | } 474 | 475 | private checkExternalInterrupts() { 476 | const { cpu } = this; 477 | const { externalInterrupts } = this.portConfig; 478 | for (let pin = 0; pin < 8; pin++) { 479 | const external = externalInterrupts[pin]; 480 | if (!external) { 481 | continue; 482 | } 483 | const pinValue = !!(this.lastPin & (1 << pin)); 484 | const { EIFR, EIMSK, index, EICR, iscOffset, interrupt } = external; 485 | if (!(cpu.data[EIMSK] & (1 << index)) || pinValue) { 486 | continue; 487 | } 488 | const configuration = (cpu.data[EICR] >> iscOffset) & 0x3; 489 | if (configuration === InterruptMode.LowLevel) { 490 | cpu.queueInterrupt({ 491 | address: interrupt, 492 | flagRegister: EIFR, 493 | flagMask: 1 << index, 494 | enableRegister: EIMSK, 495 | enableMask: 1 << index, 496 | constant: true, 497 | }); 498 | } 499 | } 500 | } 501 | 502 | private writeGpio(value: u8, ddr: u8) { 503 | const newValue = (((value & this.overrideMask) | this.overrideValue) & ddr) | (value & ~ddr); 504 | const prevValue = this.lastValue; 505 | if (newValue !== prevValue || ddr !== this.lastDdr) { 506 | this.lastValue = newValue; 507 | this.lastDdr = ddr; 508 | for (const listener of this.listeners) { 509 | listener(newValue, prevValue); 510 | } 511 | } 512 | } 513 | } 514 | --------------------------------------------------------------------------------