aboutsummaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to '')
-rw-r--r--src/index.ts10
-rw-r--r--src/peripherals/adc.spec.ts137
-rw-r--r--src/peripherals/adc.ts258
3 files changed, 405 insertions, 0 deletions
diff --git a/src/index.ts b/src/index.ts
index 8ea70d2..15cc3fa 100644
--- a/src/index.ts
+++ b/src/index.ts
@@ -8,6 +8,16 @@ export { CPU, ICPU, CPUMemoryHook, CPUMemoryHooks } from './cpu/cpu';
export { avrInstruction } from './cpu/instruction';
export { avrInterrupt } from './cpu/interrupt';
export {
+ ADCConfig,
+ adcConfig,
+ ADCMuxConfiguration,
+ ADCMuxInput,
+ ADCMuxInputType,
+ ADCReference,
+ atmega328Channels,
+ AVRADC,
+} from './peripherals/adc';
+export {
AVRTimer,
AVRTimerConfig,
timer0Config,
diff --git a/src/peripherals/adc.spec.ts b/src/peripherals/adc.spec.ts
new file mode 100644
index 0000000..4eb9aff
--- /dev/null
+++ b/src/peripherals/adc.spec.ts
@@ -0,0 +1,137 @@
+import { CPU } from '../cpu/cpu';
+import { asmProgram, TestProgramRunner } from '../utils/test-utils';
+import { AVRADC, adcConfig, ADCMuxInputType } from './adc';
+
+const R16 = 16;
+const R17 = 17;
+
+const ADMUX = 0x7c;
+const REFS0 = 1 << 6;
+
+const ADCSRA = 0x7a;
+const ADEN = 1 << 7;
+const ADSC = 1 << 6;
+const ADPS0 = 1 << 0;
+const ADPS1 = 1 << 1;
+const ADPS2 = 1 << 2;
+
+const ADCH = 0x79;
+const ADCL = 0x78;
+
+describe('ADC', () => {
+ it('should successfuly perform an ADC conversion', () => {
+ const { program } = asmProgram(`
+ ; register addresses
+ _REPLACE ADMUX, ${ADMUX}
+ _REPLACE ADCSRA, ${ADCSRA}
+ _REPLACE ADCH, ${ADCH}
+ _REPLACE ADCL, ${ADCL}
+
+ ; Configure mux - channel 0, reference: AVCC with external capacitor at AREF pin
+ ldi r24, ${REFS0}
+ sts ADMUX, r24
+
+ ; Start conversion with 128 prescaler
+ ldi r24, ${ADEN | ADSC | ADPS0 | ADPS1 | ADPS2}
+ sts ADCSRA, r24
+
+ ; Wait until conversion is complete
+ waitComplete:
+ lds r24, ${ADCSRA}
+ andi r24, ${ADSC}
+ brne waitComplete
+
+ ; Read the result
+ lds r16, ${ADCL}
+ lds r17, ${ADCH}
+
+ break
+ `);
+ const cpu = new CPU(program);
+ const adc = new AVRADC(cpu, adcConfig);
+ const runner = new TestProgramRunner(cpu);
+
+ const adcReadSpy = jest.spyOn(adc, 'onADCRead');
+ adc.channelValues[0] = 2.56; // should result in 2.56/5*1024 = 524
+
+ // Setup
+ runner.runInstructions(4);
+ expect(adcReadSpy).toHaveBeenCalledWith({ channel: 0, type: ADCMuxInputType.SingleEnded });
+
+ // Run the "waitComplete" loop for a few cycles
+ runner.runInstructions(12);
+
+ cpu.cycles += 128 * 25; // skip to the end of the conversion
+ cpu.tick();
+
+ // Now read the result
+ runner.runInstructions(5);
+
+ const low = cpu.data[R16];
+ const high = cpu.data[R17];
+ expect((high << 8) | low).toEqual(524); // 2.56 volts - see above
+ });
+
+ it('should read 0 when the ADC peripheral is not enabled', () => {
+ // This behavior was verified on real hardware, using the following test program:
+ // https://wokwi.com/arduino/projects/309156042450666050
+ // Thanks Oscar Oomens for spotting this!
+
+ const { program } = asmProgram(`
+ ; register addresses
+ _REPLACE ADMUX, ${ADMUX}
+ _REPLACE ADCSRA, ${ADCSRA}
+ _REPLACE ADCH, ${ADCH}
+ _REPLACE ADCL, ${ADCL}
+
+ ; Load some initial value into r16/r17 to make sure we actually read 0 later
+ ldi r16, 0xff
+ ldi r17, 0xff
+
+ ; Configure mux - channel 0, reference: AVCC with external capacitor at AREF pin
+ ldi r24, ${REFS0}
+ sts ADMUX, r24
+
+ ; Start conversion with 128 prescaler, but without enabling the ADC
+ ldi r24, ${ADSC | ADPS0 | ADPS1 | ADPS2}
+ sts ADCSRA, r24
+
+ ; Wait until conversion is complete
+ waitComplete:
+ lds r24, ${ADCSRA}
+ andi r24, ${ADSC}
+ brne waitComplete
+
+ ; Read the result
+ lds r16, ${ADCL}
+ lds r17, ${ADCH}
+
+ break
+ `);
+ const cpu = new CPU(program);
+ const adc = new AVRADC(cpu, adcConfig);
+ const runner = new TestProgramRunner(cpu, () => {
+ /* do nothing on break */
+ });
+
+ const adcReadSpy = jest.spyOn(adc, 'onADCRead');
+ adc.channelValues[0] = 2.56; // should result in 2.56/5*1024 = 524
+
+ // Setup
+ runner.runInstructions(6);
+ expect(adcReadSpy).not.toHaveBeenCalled();
+
+ // Run the "waitComplete" loop for a few cycles
+ runner.runInstructions(12);
+
+ cpu.cycles += 128 * 25; // skip to the end of the conversion
+ cpu.tick();
+
+ // Now read the result
+ runner.runToBreak();
+
+ const low = cpu.data[R16];
+ const high = cpu.data[R17];
+ expect((high << 8) | low).toEqual(0); // We should read 0 since the ADC hasn't been enabled
+ });
+});
diff --git a/src/peripherals/adc.ts b/src/peripherals/adc.ts
new file mode 100644
index 0000000..1e0e2d1
--- /dev/null
+++ b/src/peripherals/adc.ts
@@ -0,0 +1,258 @@
+/**
+ * AVR-8 ADC
+ * Part of AVR8js
+ * Reference: http://ww1.microchip.com/downloads/en/DeviceDoc/ATmega48A-PA-88A-PA-168A-PA-328-P-DS-DS40002061A.pdf
+ *
+ * Copyright (C) 2019, 2020, 2021 Uri Shaked
+ */
+
+import { AVRInterruptConfig, CPU } from '../cpu/cpu';
+import { u8 } from '../types';
+
+export enum ADCReference {
+ AVCC,
+ AREF,
+ Internal1V1,
+ Internal2V56,
+ Reserved,
+}
+
+export enum ADCMuxInputType {
+ SingleEnded,
+ Differential,
+ Constant,
+ Temperature,
+}
+
+export type ADCMuxInput =
+ | { type: ADCMuxInputType.Temperature }
+ | { type: ADCMuxInputType.Constant; voltage: number }
+ | { type: ADCMuxInputType.SingleEnded; channel: number }
+ | {
+ type: ADCMuxInputType.Differential;
+ positiveChannel: number;
+ negativeChannel: number;
+ gain: number;
+ };
+
+export type ADCMuxConfiguration = { [key: number]: ADCMuxInput };
+
+export interface ADCConfig {
+ ADMUX: u8;
+ ADCSRA: u8;
+ ADCSRB: u8;
+ ADCL: u8;
+ ADCH: u8;
+ DIDR0: u8;
+ adcInterrupt: u8;
+ numChannels: u8;
+ muxInputMask: u8;
+ muxChannels: ADCMuxConfiguration;
+ adcReferences: ADCReference[];
+}
+
+export const atmega328Channels: ADCMuxConfiguration = {
+ 0: { type: ADCMuxInputType.SingleEnded, channel: 0 },
+ 1: { type: ADCMuxInputType.SingleEnded, channel: 1 },
+ 2: { type: ADCMuxInputType.SingleEnded, channel: 2 },
+ 3: { type: ADCMuxInputType.SingleEnded, channel: 3 },
+ 4: { type: ADCMuxInputType.SingleEnded, channel: 4 },
+ 5: { type: ADCMuxInputType.SingleEnded, channel: 5 },
+ 6: { type: ADCMuxInputType.SingleEnded, channel: 6 },
+ 7: { type: ADCMuxInputType.SingleEnded, channel: 7 },
+ 8: { type: ADCMuxInputType.Temperature },
+ 14: { type: ADCMuxInputType.Constant, voltage: 1.1 },
+ 15: { type: ADCMuxInputType.Constant, voltage: 0 },
+};
+
+const fallbackMuxInput = {
+ type: ADCMuxInputType.Constant,
+ voltage: 0,
+};
+
+export const adcConfig: ADCConfig = {
+ ADMUX: 0x7c,
+ ADCSRA: 0x7a,
+ ADCSRB: 0x7b,
+ ADCL: 0x78,
+ ADCH: 0x79,
+ DIDR0: 0x7e,
+ adcInterrupt: 0x2a,
+ numChannels: 8,
+ muxInputMask: 0xf,
+ muxChannels: atmega328Channels,
+ adcReferences: [
+ ADCReference.AREF,
+ ADCReference.AVCC,
+ ADCReference.Reserved,
+ ADCReference.Internal1V1,
+ ],
+};
+
+// Register bits:
+const ADPS_MASK = 0x7;
+const ADIE = 0x8;
+const ADIF = 0x10;
+const ADSC = 0x40;
+const ADEN = 0x80;
+
+const MUX_MASK = 0x1f;
+const ADLAR = 0x20;
+const MUX5 = 0x8;
+const REFS2 = 0x8;
+const REFS_MASK = 0x3;
+const REFS_SHIFT = 6;
+
+export class AVRADC {
+ /**
+ * ADC Channel values, in voltage (0..5). The number of channels depends on the chip.
+ *
+ * Changing the values here will change the ADC reading, unless you override onADCRead() with a custom implementation.
+ */
+ readonly channelValues = new Array(this.config.numChannels);
+
+ /** AVCC Reference voltage */
+ avcc = 5;
+
+ /** AREF Reference voltage */
+ aref = 5;
+
+ /**
+ * Invoked whenever the code performs an ADC read.
+ *
+ * The default implementation reads the result from the `channelValues` array, and then calls
+ * `completeADCRead()` after `sampleCycles` CPU cycles.
+ *
+ * If you override the default implementation, make sure to call `completeADCRead()` after
+ * `sampleCycles` cycles (or else the ADC read will never complete).
+ */
+ onADCRead: (input: ADCMuxInput) => void = (input) => {
+ // Default implementation
+ let voltage = 0;
+ switch (input.type) {
+ case ADCMuxInputType.Constant:
+ voltage = input.voltage;
+ break;
+ case ADCMuxInputType.SingleEnded:
+ voltage = this.channelValues[input.channel] ?? 0;
+ break;
+ case ADCMuxInputType.Differential:
+ voltage =
+ input.gain *
+ ((this.channelValues[input.positiveChannel] || 0) -
+ (this.channelValues[input.negativeChannel] || 0));
+ break;
+ case ADCMuxInputType.Temperature:
+ voltage = 0.378125; // 25 celcius
+ break;
+ }
+ const rawValue = (voltage / this.referenceVoltage) * 1024;
+ const result = Math.min(Math.max(Math.floor(rawValue), 0), 1023);
+ this.cpu.addClockEvent(() => this.completeADCRead(result), this.sampleCycles);
+ };
+
+ private converting = false;
+ private conversionCycles = 25;
+
+ // Interrupts
+ private ADC: AVRInterruptConfig = {
+ address: this.config.adcInterrupt,
+ flagRegister: this.config.ADCSRA,
+ flagMask: ADIF,
+ enableRegister: this.config.ADCSRA,
+ enableMask: ADIE,
+ };
+
+ constructor(private cpu: CPU, private config: ADCConfig) {
+ cpu.writeHooks[config.ADCSRA] = (value, oldValue) => {
+ if (value & ADEN && !(oldValue && ADEN)) {
+ this.conversionCycles = 25;
+ }
+ cpu.data[config.ADCSRA] = value;
+ cpu.updateInterruptEnable(this.ADC, value);
+ if (!this.converting && value & ADSC) {
+ if (!(value & ADEN)) {
+ // Special case: reading while the ADC is not enabled should return 0
+ this.cpu.addClockEvent(() => this.completeADCRead(0), this.sampleCycles);
+ return true;
+ }
+ let channel = this.cpu.data[this.config.ADMUX] & MUX_MASK;
+ if (cpu.data[config.ADCSRB] & MUX5) {
+ channel |= 0x20;
+ }
+ channel &= config.muxInputMask;
+ const muxInput = config.muxChannels[channel] ?? fallbackMuxInput;
+ this.converting = true;
+ this.onADCRead(muxInput);
+ return true; // don't update
+ }
+ };
+ }
+
+ completeADCRead(value: number) {
+ const { ADCL, ADCH, ADMUX, ADCSRA } = this.config;
+ this.converting = false;
+ this.conversionCycles = 13;
+ if (this.cpu.data[ADMUX] & ADLAR) {
+ this.cpu.data[ADCL] = (value << 6) & 0xff;
+ this.cpu.data[ADCH] = value >> 2;
+ } else {
+ this.cpu.data[ADCL] = value & 0xff;
+ this.cpu.data[ADCH] = (value >> 8) & 0x3;
+ }
+ this.cpu.data[ADCSRA] &= ~ADSC;
+ this.cpu.setInterruptFlag(this.ADC);
+ }
+
+ get prescaler() {
+ const { ADCSRA } = this.config;
+ const adcsra = this.cpu.data[ADCSRA];
+ const adps = adcsra & ADPS_MASK;
+ switch (adps) {
+ case 0:
+ case 1:
+ return 2;
+ case 2:
+ return 4;
+ case 3:
+ return 8;
+ case 4:
+ return 16;
+ case 5:
+ return 32;
+ case 6:
+ return 64;
+ case 7:
+ default:
+ return 128;
+ }
+ }
+
+ get referenceVoltageType() {
+ const { ADMUX, adcReferences } = this.config;
+ let refs = (this.cpu.data[ADMUX] >> REFS_SHIFT) & REFS_MASK;
+ if (adcReferences.length > 4 && this.cpu.data[ADMUX] & REFS2) {
+ refs |= 0x4;
+ }
+ return adcReferences[refs] ?? ADCReference.Reserved;
+ }
+
+ get referenceVoltage() {
+ switch (this.referenceVoltageType) {
+ case ADCReference.AVCC:
+ return this.avcc;
+ case ADCReference.AREF:
+ return this.aref;
+ case ADCReference.Internal1V1:
+ return 1.1;
+ case ADCReference.Internal2V56:
+ return 2.56;
+ default:
+ return this.avcc;
+ }
+ }
+
+ get sampleCycles() {
+ return this.conversionCycles * this.prescaler;
+ }
+}
If you find this content useful please consider a small donation via bitcoin: bitcoin:1mxKyQsHHugqRxPKgwaA7wUwJEGCNthn2?amount=0.001&message=Donation Scraping git contents via cgit is very resource intensive. The continued hosting of these git repositories is entirely financed by your bitcoin contributions. For donations of a least 1 mBTC you include your IP address in the message of your bitcoin donation. This will remove this message in the future for requests from your IP, which will significantly reduce token usage. In any case, any contributions to bitcoin address 1mxKyQsHHugqRxPKgwaA7wUwJEGCNthn2 are very welcome. Thanks in advance for you contribution to a self-sustaining ecosystem.