aboutsummaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
authorUri Shaked2021-09-10 21:05:21 +0300
committerGitHub2021-09-10 21:05:21 +0300
commitec1346b8a9766f95ff8aae7a3e6abf61307a7d7c (patch)
tree95973ae705dffcb28930bbd4d189f21d8627d9cb /src
parentfeat(watchdog): implement watchdog timer #106 (diff)
downloadavr8js-ec1346b8a9766f95ff8aae7a3e6abf61307a7d7c.tar.gz
avr8js-ec1346b8a9766f95ff8aae7a3e6abf61307a7d7c.tar.bz2
avr8js-ec1346b8a9766f95ff8aae7a3e6abf61307a7d7c.zip
feat(adc): ADC peripheral #13
Diffstat (limited to 'src')
-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;
+ }
+}