diff options
Diffstat (limited to '')
| -rw-r--r-- | src/peripherals/adc.spec.ts | 137 | ||||
| -rw-r--r-- | src/peripherals/adc.ts | 258 |
2 files changed, 395 insertions, 0 deletions
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; + } +} |
