Skip to content

Commit 9b39981

Browse files
committed
feat: GPIO peripheral implementation
Add new AVRIOPort class, implements GPIO output logic
1 parent c800f1c commit 9b39981

File tree

6 files changed

+146
-10
lines changed

6 files changed

+146
-10
lines changed

demo/src/execute.ts

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,13 @@
1-
import { avrInstruction, AVRTimer, CPU, timer0Config } from 'avr8js';
1+
import {
2+
avrInstruction,
3+
AVRTimer,
4+
CPU,
5+
timer0Config,
6+
AVRIOPort,
7+
portBConfig,
8+
portCConfig,
9+
portDConfig
10+
} from 'avr8js';
211
import { loadHex } from './intelhex';
312

413
// ATmega328p params
@@ -8,13 +17,19 @@ export class AVRRunner {
817
readonly program = new Uint16Array(FLASH);
918
readonly cpu: CPU;
1019
readonly timer: AVRTimer;
20+
readonly portB: AVRIOPort;
21+
readonly portC: AVRIOPort;
22+
readonly portD: AVRIOPort;
1123

1224
private stopped = false;
1325

1426
constructor(hex: string) {
1527
loadHex(hex, new Uint8Array(this.program.buffer));
1628
this.cpu = new CPU(this.program);
1729
this.timer = new AVRTimer(this.cpu, timer0Config);
30+
this.portB = new AVRIOPort(this.cpu, portBConfig);
31+
this.portC = new AVRIOPort(this.cpu, portCConfig);
32+
this.portD = new AVRIOPort(this.cpu, portDConfig);
1833
}
1934

2035
async execute(callback: (cpu: CPU) => void) {

demo/src/index.ts

Lines changed: 4 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { buildHex } from './compile';
2-
import './index.css';
32
import { AVRRunner } from './execute';
43
import { formatTime } from './format-time';
4+
import './index.css';
55
import { LED } from './led';
66

77
let editor: any;
@@ -55,16 +55,13 @@ function executeProgram(hex: string) {
5555
runner = new AVRRunner(hex);
5656
const MHZ = 16000000;
5757

58-
// Hook to PORTB output
59-
runner.cpu.writeHooks[0x25] = (value: number) => {
60-
const DDRB = runner.cpu.data[0x24];
61-
value &= DDRB;
58+
// Hook to PORTB register
59+
runner.portB.addListener((value) => {
6260
const D12bit = 1 << 4;
6361
const D13bit = 1 << 5;
6462
led12.value = value & D12bit ? true : false;
6563
led13.value = value & D13bit ? true : false;
66-
};
67-
64+
});
6865
runner.execute((cpu) => {
6966
const time = formatTime(cpu.cycles / MHZ);
7067
statusLabel.textContent = 'Simulation time: ' + time;

src/cpu.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ export interface ICPU {
1919
writeData(addr: u16, value: u8): void;
2020
}
2121

22-
export type ICPUMemoryHook = (value: u8, oldValue: u8, addr: u16) => void;
22+
export type ICPUMemoryHook = (value: u8, oldValue: u8, addr: u16) => boolean | void;
2323
export interface ICPUMemoryHooks {
2424
[key: number]: ICPUMemoryHook;
2525
}
@@ -43,7 +43,9 @@ export class CPU implements ICPU {
4343
writeData(addr: number, value: number) {
4444
const hook = this.writeHooks[addr];
4545
if (hook) {
46-
hook(value, this.data[addr], addr);
46+
if (hook(value, this.data[addr], addr)) {
47+
return;
48+
}
4749
}
4850
this.data[addr] = value;
4951
}

src/gpio.spec.ts

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
import { CPU } from './cpu';
2+
import { AVRIOPort, portBConfig } from './gpio';
3+
4+
describe('GPIO', () => {
5+
it('should invoke the listeners when the port is written to', () => {
6+
const cpu = new CPU(new Uint16Array(1024));
7+
const port = new AVRIOPort(cpu, portBConfig);
8+
const listener = jest.fn();
9+
port.addListener(listener);
10+
cpu.writeData(0x24, 0x0f); // DDRB <- 0x0f
11+
cpu.writeData(0x25, 0x55); // PORTB <- 0x55
12+
expect(listener).toHaveBeenCalledWith(0x05, 0);
13+
expect(cpu.data[0x23]).toEqual(0x5); // PINB should return port value
14+
});
15+
16+
it('should toggle the pin when writing to the PIN register', () => {
17+
const cpu = new CPU(new Uint16Array(1024));
18+
const port = new AVRIOPort(cpu, portBConfig);
19+
const listener = jest.fn();
20+
port.addListener(listener);
21+
cpu.writeData(0x24, 0x0f); // DDRB <- 0x0f
22+
cpu.writeData(0x25, 0x55); // PORTB <- 0x55
23+
cpu.writeData(0x23, 0x01); // PINB <- 0x0f
24+
expect(listener).toHaveBeenCalledWith(0x04, 0x5);
25+
expect(cpu.data[0x23]).toEqual(0x4); // PINB should return port value
26+
});
27+
28+
describe('removeListener', () => {
29+
it('should remove the given listener', () => {
30+
const cpu = new CPU(new Uint16Array(1024));
31+
const port = new AVRIOPort(cpu, portBConfig);
32+
const listener = jest.fn();
33+
port.addListener(listener);
34+
cpu.writeData(0x24, 0x0f); // DDRB <- 0x0f
35+
port.removeListener(listener);
36+
cpu.writeData(0x25, 0x99); // PORTB <- 0x99
37+
expect(listener).not.toHaveBeenCalled();
38+
});
39+
});
40+
});

src/gpio.ts

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
/**
2+
* AVR-8 GPIO Port implementation
3+
* Part of AVR8js
4+
* Reference: http://ww1.microchip.com/downloads/en/DeviceDoc/ATmega48A-PA-88A-PA-168A-PA-328-P-DS-DS40002061A.pdf
5+
*
6+
* Copyright (C) 2019, Uri Shaked
7+
*/
8+
import { CPU } from './cpu';
9+
import { u8 } from './types';
10+
11+
export interface AVRPortConfig {
12+
// Register addresses
13+
PIN: u8;
14+
DDR: u8;
15+
PORT: u8;
16+
}
17+
18+
export type GPIOListener = (value: u8, oldValue: u8) => void;
19+
20+
export const portBConfig: AVRPortConfig = {
21+
PIN: 0x23,
22+
DDR: 0x24,
23+
PORT: 0x25
24+
};
25+
26+
export const portCConfig: AVRPortConfig = {
27+
PIN: 0x26,
28+
DDR: 0x27,
29+
PORT: 0x28
30+
};
31+
32+
export const portDConfig: AVRPortConfig = {
33+
PIN: 0x29,
34+
DDR: 0x2a,
35+
PORT: 0x2b
36+
};
37+
38+
export class AVRIOPort {
39+
private listeners: GPIOListener[] = [];
40+
41+
constructor(cpu: CPU, portConfig: AVRPortConfig) {
42+
cpu.writeHooks[portConfig.PORT] = (value: u8, oldValue: u8) => {
43+
const ddrMask = cpu.data[portConfig.DDR];
44+
value &= ddrMask;
45+
cpu.data[portConfig.PIN] = (cpu.data[portConfig.PIN] & ~ddrMask) | value;
46+
this.writeGpio(value, oldValue & ddrMask);
47+
// TODO: activate pullups if configured as an input pin
48+
};
49+
cpu.writeHooks[portConfig.PIN] = (value: u8) => {
50+
// Writing to 1 PIN toggles PORT bits
51+
const oldPortValue = cpu.data[portConfig.PORT];
52+
const ddrMask = cpu.data[portConfig.DDR];
53+
const portValue = oldPortValue ^ value;
54+
cpu.data[portConfig.PORT] = portValue;
55+
cpu.data[portConfig.PIN] = (cpu.data[portConfig.PIN] & ~ddrMask) | (portValue & ddrMask);
56+
this.writeGpio(portValue & ddrMask, oldPortValue & ddrMask);
57+
return true;
58+
};
59+
}
60+
61+
addListener(listener: GPIOListener) {
62+
this.listeners.push(listener);
63+
}
64+
65+
removeListener(listener: GPIOListener) {
66+
this.listeners = this.listeners.filter((l) => l !== listener);
67+
}
68+
69+
private writeGpio(value: u8, oldValue: u8) {
70+
for (const listener of this.listeners) {
71+
listener(value, oldValue);
72+
}
73+
}
74+
}

src/index.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,3 +2,11 @@ export { CPU, ICPU, ICPUMemoryHook, ICPUMemoryHooks } from './cpu';
22
export { avrInstruction } from './instruction';
33
export { avrInterrupt } from './interrupt';
44
export { AVRTimer, timer0Config, timer1Config, timer2Config } from './timer';
5+
export {
6+
AVRIOPort,
7+
GPIOListener,
8+
AVRPortConfig,
9+
portBConfig,
10+
portCConfig,
11+
portDConfig
12+
} from './gpio';

0 commit comments

Comments
 (0)