aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--src/cpu/cpu.ts4
-rw-r--r--src/peripherals/gpio.ts64
-rw-r--r--src/peripherals/timer.spec.ts158
-rw-r--r--src/peripherals/timer.ts173
4 files changed, 342 insertions, 57 deletions
diff --git a/src/cpu/cpu.ts b/src/cpu/cpu.ts
index 5663a1c..ed88e71 100644
--- a/src/cpu/cpu.ts
+++ b/src/cpu/cpu.ts
@@ -44,6 +44,7 @@ export type CPUMemoryReadHook = (addr: u16) => u8;
export interface CPUMemoryReadHooks {
[key: number]: CPUMemoryReadHook;
}
+
export class CPU implements ICPU {
readonly data: Uint8Array = new Uint8Array(this.sramBytes + registerSpace);
readonly data16 = new Uint16Array(this.data.buffer);
@@ -53,6 +54,9 @@ export class CPU implements ICPU {
readonly writeHooks: CPUMemoryHooks = [];
readonly pc22Bits = this.progBytes.length > 0x20000;
+ // This lets the Timer Compare output override GPIO pins:
+ readonly gpioTimerHooks: CPUMemoryHooks = [];
+
pc = 0;
cycles = 0;
diff --git a/src/peripherals/gpio.ts b/src/peripherals/gpio.ts
index e3c8eaa..cb21669 100644
--- a/src/peripherals/gpio.ts
+++ b/src/peripherals/gpio.ts
@@ -90,23 +90,34 @@ export enum PinState {
InputPullUp,
}
+/* This mechanism allows timers to override specific GPIO pins */
+export enum PinOverrideMode {
+ None,
+ Enable,
+ Set,
+ Clear,
+ Toggle,
+}
+
export class AVRIOPort {
private listeners: GPIOListener[] = [];
private pinValue: u8 = 0;
+ private overrideMask: u8 = 0xff;
+ private overrideValue: u8;
+ private lastValue: u8 = 0;
+ private lastDdr: u8 = 0;
constructor(private cpu: CPU, private portConfig: AVRPortConfig) {
- cpu.writeHooks[portConfig.DDR] = (value, oldValue) => {
+ cpu.writeHooks[portConfig.DDR] = (value: u8) => {
const portValue = cpu.data[portConfig.PORT];
this.updatePinRegister(portValue, value);
- this.writeGpio(value & portValue, oldValue & oldValue);
+ this.writeGpio(portValue, value);
};
- cpu.writeHooks[portConfig.PORT] = (value: u8, oldValue: u8) => {
+ cpu.writeHooks[portConfig.PORT] = (value: u8) => {
const ddrMask = cpu.data[portConfig.DDR];
cpu.data[portConfig.PORT] = value;
- value &= ddrMask;
- cpu.data[portConfig.PIN] = (cpu.data[portConfig.PIN] & ~ddrMask) | value;
this.updatePinRegister(value, ddrMask);
- this.writeGpio(value, oldValue & ddrMask);
+ this.writeGpio(value, ddrMask);
return true;
};
cpu.writeHooks[portConfig.PIN] = (value: u8) => {
@@ -116,9 +127,34 @@ export class AVRIOPort {
const portValue = oldPortValue ^ value;
cpu.data[portConfig.PORT] = portValue;
cpu.data[portConfig.PIN] = (cpu.data[portConfig.PIN] & ~ddrMask) | (portValue & ddrMask);
- this.writeGpio(portValue & ddrMask, oldPortValue & ddrMask);
+ this.writeGpio(portValue, ddrMask);
return true;
};
+ // The following hook is used by the timer compare output to override GPIO pins:
+ cpu.gpioTimerHooks[portConfig.PORT] = (pin: u8, mode: PinOverrideMode) => {
+ const pinMask = 1 << pin;
+ if (mode == PinOverrideMode.None) {
+ this.overrideMask |= pinMask;
+ } else {
+ this.overrideMask &= ~pinMask;
+ switch (mode) {
+ case PinOverrideMode.Enable:
+ this.overrideValue &= ~pinMask;
+ this.overrideValue |= cpu.data[portConfig.PORT] & pinMask;
+ break;
+ case PinOverrideMode.Set:
+ this.overrideValue |= pinMask;
+ break;
+ case PinOverrideMode.Clear:
+ this.overrideValue &= ~pinMask;
+ break;
+ case PinOverrideMode.Toggle:
+ this.overrideValue ^= pinMask;
+ break;
+ }
+ }
+ this.writeGpio(cpu.data[portConfig.PORT], cpu.data[portConfig.DDR]);
+ };
}
addListener(listener: GPIOListener) {
@@ -142,7 +178,7 @@ export class AVRIOPort {
const port = this.cpu.data[this.portConfig.PORT];
const bitMask = 1 << index;
if (ddr & bitMask) {
- return port & bitMask ? PinState.High : PinState.Low;
+ return this.lastValue & bitMask ? PinState.High : PinState.Low;
} else {
return port & bitMask ? PinState.InputPullUp : PinState.Input;
}
@@ -165,9 +201,15 @@ export class AVRIOPort {
this.cpu.data[this.portConfig.PIN] = (this.pinValue & ~ddr) | (port & ddr);
}
- private writeGpio(value: u8, oldValue: u8) {
- for (const listener of this.listeners) {
- listener(value, oldValue);
+ private writeGpio(value: u8, ddr: u8) {
+ const newValue = ((value & this.overrideMask) | this.overrideValue) & ddr;
+ const prevValue = this.lastValue;
+ if (newValue !== prevValue || ddr !== this.lastDdr) {
+ this.lastValue = newValue;
+ this.lastDdr = ddr;
+ for (const listener of this.listeners) {
+ listener(newValue, prevValue);
+ }
}
}
}
diff --git a/src/peripherals/timer.spec.ts b/src/peripherals/timer.spec.ts
index 8733f73..f4b0c69 100644
--- a/src/peripherals/timer.spec.ts
+++ b/src/peripherals/timer.spec.ts
@@ -2,6 +2,7 @@ import { CPU } from '../cpu/cpu';
import { avrInstruction } from '../cpu/instruction';
import { assemble } from '../utils/assembler';
import { AVRTimer, timer0Config, timer1Config, timer2Config } from './timer';
+import { PinOverrideMode } from './gpio';
describe('timer', () => {
let cpu: CPU;
@@ -57,20 +58,6 @@ describe('timer', () => {
expect(cpu.data[0x35]).toEqual(1); // TOV bit in TIFR
});
- it('should set TOV if timer overflows in PWM Phase Correct mode', () => {
- const timer = new AVRTimer(cpu, timer0Config);
- cpu.writeData(0x46, 0xff); // TCNT0 <- 0xff
- timer.tick();
- cpu.writeData(0x47, 0x7f); // OCRA <- 0x7f
- cpu.writeData(0x44, 0x1); // WGM0 <- 1 (PWM, Phase Correct)
- cpu.data[0x45] = 0x1; // TCCR0B.CS <- 1
- cpu.cycles = 1;
- timer.tick();
- const tcnt = cpu.readData(0x46);
- expect(tcnt).toEqual(0); // TCNT should be 0
- expect(cpu.data[0x35]).toEqual(1); // TOV bit in TIFR
- });
-
it('should set TOV if timer overflows in FAST PWM mode', () => {
const timer = new AVRTimer(cpu, timer0Config);
cpu.writeData(0x46, 0xff); // TCNT0 <- 0xff
@@ -273,6 +260,108 @@ describe('timer', () => {
expect(cpu.data[1]).toEqual(2); // r1 should equal 2
});
+ describe('Phase-correct PWM mode', () => {
+ it('should count up to TOP, down to 0, and then set TOV flag', () => {
+ const program = [
+ // Set waveform generation mode (WGM) to PWM, Phase Correct, top OCR0A
+ 'LDI r16, 0x1', // TCCR0A = 1 << WGM00;
+ 'OUT 0x24, r16',
+ 'LDI r16, 0x9', // TCCR0B = (1 << WGM02) | (1 << CS00);
+ 'OUT 0x25, r16',
+ 'LDI r16, 0x3', // OCR0A = 0x3;
+ 'OUT 0x27, r16',
+ 'LDI r16, 0x2', // TCNT0 = 0x2;
+ 'OUT 0x26, r16',
+ ];
+ const nops = [
+ 'NOP', // TCNT0 will be 3
+ 'NOP', // TCNT0 will be 2
+ 'NOP', // TCNT0 will be 1
+ 'NOP', // TCNT0 will be 0
+ 'NOP', // TCNT0 will be 1 (end of test)
+ ];
+ loadProgram(...program, ...nops);
+ const timer = new AVRTimer(cpu, timer0Config);
+
+ for (let i = 0; i < program.length; i++) {
+ avrInstruction(cpu);
+ timer.tick();
+ }
+ expect(cpu.readData(0x46)).toEqual(2); // TCNT should be 2
+
+ avrInstruction(cpu);
+ timer.tick();
+ expect(cpu.readData(0x46)).toEqual(3); // TCNT should be 3
+
+ avrInstruction(cpu);
+ timer.tick();
+ expect(cpu.readData(0x46)).toEqual(2); // TCNT should be 2
+
+ avrInstruction(cpu);
+ timer.tick();
+ expect(cpu.readData(0x46)).toEqual(1); // TCNT should be 1
+ expect(cpu.data[0x35] & 0x1).toEqual(0); // TIFR should have TOV bit clear
+
+ avrInstruction(cpu);
+ timer.tick();
+ expect(cpu.readData(0x46)).toEqual(0); // TCNT should be 0
+ expect(cpu.data[0x35] & 0x1).toEqual(1); // TIFR should have TOV bit set
+
+ avrInstruction(cpu);
+ timer.tick();
+ expect(cpu.readData(0x46)).toEqual(1); // TCNT should be 1
+ });
+
+ it('should clear OC0A when TCNT0=OCR0A and counting up', () => {
+ const program = [
+ // Set waveform generation mode (WGM) to PWM, Phase Correct
+ 'LDI r16, 0x81', // TCCR0A = (1 << COM0A1) || (1 << WGM01);
+ 'OUT 0x24, r16',
+ 'LDI r16, 0x1', // TCCR0B = (1 << CS00);
+ 'OUT 0x25, r16',
+ 'LDI r16, 0xfe', // OCR0A = 0xfe;
+ 'OUT 0x27, r16',
+ 'LDI r16, 0xfd', // TCNT0 = 0xfd;
+ 'OUT 0x26, r16',
+ ];
+ const nops = [
+ 'NOP', // TCNT0 will be 0xfe
+ 'NOP', // TCNT0 will be 0xff
+ 'NOP', // TCNT0 will be 0xfe again (end of test)
+ ];
+ loadProgram(...program, ...nops);
+ const timer = new AVRTimer(cpu, timer0Config);
+
+ // Listen to Port D's internal callback
+ const gpioCallback = jest.fn();
+ cpu.gpioTimerHooks[0x2b] = gpioCallback;
+
+ for (let i = 0; i < program.length; i++) {
+ avrInstruction(cpu);
+ timer.tick();
+ }
+ expect(cpu.readData(0x46)).toEqual(0xfd); // TCNT0 should be 0xfd
+ expect(gpioCallback).toHaveBeenCalledWith(6, PinOverrideMode.Enable, 0x2b);
+ gpioCallback.mockClear();
+
+ avrInstruction(cpu);
+ timer.tick();
+ expect(cpu.readData(0x46)).toEqual(0xfe); // TCNT should be 0xfe
+ expect(gpioCallback).toHaveBeenCalledWith(6, PinOverrideMode.Clear, 0x2b);
+ gpioCallback.mockClear();
+
+ avrInstruction(cpu);
+ timer.tick();
+ expect(cpu.readData(0x46)).toEqual(0xff); // TCNT should be 0xff
+ expect(gpioCallback).not.toHaveBeenCalled();
+
+ avrInstruction(cpu);
+ timer.tick();
+ expect(cpu.readData(0x46)).toEqual(0xfe); // TCNT should be 0xfe
+ expect(gpioCallback).toHaveBeenCalledWith(6, PinOverrideMode.Set, 0x2b);
+ });
+ });
+
describe('16 bit timers', () => {
it('should increment 16-bit TCNT by 1', () => {
const timer = new AVRTimer(cpu, timer1Config);
@@ -365,5 +454,46 @@ describe('timer', () => {
const timerLow = cpu.readData(0x84);
expect((timerHigh << 8) | timerLow).toEqual(0xff00);
});
+
+ it('should toggle OC1B on Compare Match', () => {
+ const program = [
+ // Set waveform generation mode (WGM) to Normal, top 0xFFFF
+ 'LDI r16, 0x10', // TCCR1A = (1 << COM1B0);
+ 'STS 0x80, r16',
+ 'LDI r16, 0x1', // TCCR1B = (1 << CS00);
+ 'STS 0x81, r16',
+ 'LDI r16, 0x0', // OCR1BH = 0x0;
+ 'STS 0x8B, r16',
+ 'LDI r16, 0x4a', // OCR1BL = 0x4a;
+ 'STS 0x8A, r16',
+ 'LDI r16, 0x0', // TCNT1H = 0x0;
+ 'STS 0x85, r16',
+ 'LDI r16, 0x49', // TCNT1L = 0x49;
+ 'STS 0x84, r16',
+ ];
+ const nops = [
+ 'NOP', // TCNT1 will be 0x49
+ 'NOP', // TCNT1 will be 0x4a
+ ];
+ loadProgram(...program, ...nops);
+ const timer = new AVRTimer(cpu, timer1Config);
+
+ // Listen to Port B's internal callback
+ const gpioCallback = jest.fn();
+ cpu.gpioTimerHooks[0x25] = gpioCallback;
+
+ for (let i = 0; i < program.length; i++) {
+ avrInstruction(cpu);
+ timer.tick();
+ }
+ expect(cpu.readData(0x84)).toEqual(0x49); // TCNT1 should be 0x49
+ expect(gpioCallback).toHaveBeenCalledWith(2, PinOverrideMode.Enable, 0x25);
+ gpioCallback.mockClear();
+
+ avrInstruction(cpu);
+ timer.tick();
+ expect(cpu.readData(0x84)).toEqual(0x4a); // TCNT1 should be 0x4a
+ expect(gpioCallback).toHaveBeenCalledWith(2, PinOverrideMode.Toggle, 0x25);
+ });
});
});
diff --git a/src/peripherals/timer.ts b/src/peripherals/timer.ts
index 4e75fb1..4293a49 100644
--- a/src/peripherals/timer.ts
+++ b/src/peripherals/timer.ts
@@ -8,6 +8,7 @@
import { CPU } from '../cpu/cpu';
import { avrInterrupt } from '../cpu/interrupt';
+import { portBConfig, portDConfig, PinOverrideMode } from './gpio';
const timer01Dividers = {
0: 0,
@@ -61,6 +62,12 @@ interface AVRTimerConfig {
TIMSK: u8;
dividers: TimerDividers;
+
+ // Output compare pins
+ compPortA: u16;
+ compPinA: u8;
+ compPortB: u16;
+ compPinB: u8;
}
export const timer0Config: AVRTimerConfig = {
@@ -79,6 +86,10 @@ export const timer0Config: AVRTimerConfig = {
TCCRC: 0, // not available
TIMSK: 0x6e,
dividers: timer01Dividers,
+ compPortA: portDConfig.PORT,
+ compPinA: 6,
+ compPortB: portDConfig.PORT,
+ compPinB: 5,
};
export const timer1Config: AVRTimerConfig = {
@@ -97,6 +108,10 @@ export const timer1Config: AVRTimerConfig = {
TCCRC: 0x82,
TIMSK: 0x6f,
dividers: timer01Dividers,
+ compPortA: portBConfig.PORT,
+ compPinA: 1,
+ compPortB: portBConfig.PORT,
+ compPinB: 2,
};
export const timer2Config: AVRTimerConfig = {
@@ -124,6 +139,10 @@ export const timer2Config: AVRTimerConfig = {
6: 256,
7: 1024,
},
+ compPortA: portBConfig.PORT,
+ compPinA: 3,
+ compPortB: portDConfig.PORT,
+ compPinB: 3,
};
/* All the following types and constants are related to WGM (Waveform Generation Mode) bits: */
@@ -185,14 +204,36 @@ const wgmModes16Bit: WGMConfig[] = [
/*15*/ [TimerMode.FastPWM, TopOCRA, OCRUpdateMode.Bottom, TOVUpdateMode.Top],
];
+type CompBitsValue = 0 | 1 | 2 | 3;
+
+function compToOverride(comp: CompBitsValue) {
+ switch (comp) {
+ case 1:
+ return PinOverrideMode.Toggle;
+ case 2:
+ return PinOverrideMode.Clear;
+ case 3:
+ return PinOverrideMode.Set;
+ default:
+ return PinOverrideMode.Enable;
+ }
+}
+
export class AVRTimer {
private lastCycle = 0;
private ocrA: u16 = 0;
private ocrB: u16 = 0;
+ private icr: u16 = 0; // only for 16-bit timers
private timerMode: TimerMode;
private topValue: TimerTopValue;
private tcnt: u16 = 0;
+ private compA: CompBitsValue;
+ private compB: CompBitsValue;
private tcntUpdated = false;
+ private countingUp = true;
+
+ // This is the temporary register used to access 16-bit registers (section 16.3 of the datasheet)
+ private highByteTemp: u8 = 0;
constructor(private cpu: CPU, private config: AVRTimerConfig) {
this.updateWGMConfig();
@@ -205,20 +246,36 @@ export class AVRTimer {
};
this.cpu.writeHooks[config.TCNT] = (value: u8) => {
- const highByte = this.config.bits === 16 ? this.cpu.data[config.TCNT + 1] : 0;
- this.tcnt = (highByte << 8) | value;
+ this.tcnt = (this.highByteTemp << 8) | value;
this.tcntUpdated = true;
this.timerUpdated();
};
- this.registerHook(config.OCRA, (value: u16) => {
+ this.cpu.writeHooks[config.OCRA] = (value: u8) => {
// TODO implement buffering when timer running in PWM mode
- this.ocrA = value;
- });
- this.registerHook(config.OCRB, (value: u16) => {
- this.ocrB = value;
- });
+ this.ocrA = (this.highByteTemp << 8) | value;
+ };
+ this.cpu.writeHooks[config.OCRB] = (value: u8) => {
+ // TODO implement buffering when timer running in PWM mode
+ this.ocrB = (this.highByteTemp << 8) | value;
+ };
+ this.cpu.writeHooks[config.ICR] = (value: u8) => {
+ this.icr = (this.highByteTemp << 8) | value;
+ };
+ if (this.config.bits === 16) {
+ const updateTempRegister = (value: u8) => {
+ this.highByteTemp = value;
+ };
+ this.cpu.writeHooks[config.TCNT + 1] = updateTempRegister;
+ this.cpu.writeHooks[config.OCRA + 1] = updateTempRegister;
+ this.cpu.writeHooks[config.OCRB + 1] = updateTempRegister;
+ this.cpu.writeHooks[config.ICR + 1] = updateTempRegister;
+ }
cpu.writeHooks[config.TCCRA] = (value) => {
this.cpu.data[config.TCCRA] = value;
+ this.compA = ((value >> 6) & 0x3) as CompBitsValue;
+ this.updateCompA(this.compA ? PinOverrideMode.Enable : PinOverrideMode.None);
+ this.compB = ((value >> 4) & 0x3) as CompBitsValue;
+ this.updateCompB(this.compB ? PinOverrideMode.Enable : PinOverrideMode.None);
this.updateWGMConfig();
return true;
};
@@ -255,11 +312,6 @@ export class AVRTimer {
return this.cpu.data[this.config.TIMSK];
}
- get ICR() {
- // Only available for 16-bit timers
- return (this.cpu.data[this.config.ICR + 1] << 8) | this.cpu.data[this.config.ICR];
- }
-
get CS() {
return (this.TCCRB & 0x7) as 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7;
}
@@ -274,21 +326,12 @@ export class AVRTimer {
case TopOCRA:
return this.ocrA;
case TopICR:
- return this.ICR;
+ return this.icr;
default:
return this.topValue;
}
}
- private registerHook(address: number, hook: (value: u16) => void) {
- if (this.config.bits === 16) {
- this.cpu.writeHooks[address] = (value: u8) => hook((this.cpu.data[address + 1] << 8) | value);
- this.cpu.writeHooks[address + 1] = (value: u8) => hook((value << 8) | this.cpu.data[address]);
- } else {
- this.cpu.writeHooks[address] = hook;
- }
- }
-
private updateWGMConfig() {
const wgmModes = this.config.bits === 16 ? wgmModes16Bit : wgmModes8Bit;
const [timerMode, topValue] = wgmModes[this.WGM];
@@ -303,20 +346,18 @@ export class AVRTimer {
const counterDelta = Math.floor(delta / divider);
this.lastCycle += counterDelta * divider;
const val = this.tcnt;
- const newVal = (val + counterDelta) % (this.TOP + 1);
+ const { timerMode } = this;
+ const phasePwm =
+ timerMode === TimerMode.PWMPhaseCorrect || timerMode === TimerMode.PWMPhaseFrequencyCorrect;
+ const newVal = phasePwm
+ ? this.phasePwmCount(val, counterDelta)
+ : (val + counterDelta) % (this.TOP + 1);
// A CPU write overrides (has priority over) all counter clear or count operations.
if (!this.tcntUpdated) {
this.tcnt = newVal;
this.timerUpdated();
}
- const { timerMode } = this;
- if (
- (timerMode === TimerMode.Normal ||
- timerMode === TimerMode.PWMPhaseCorrect ||
- timerMode === TimerMode.PWMPhaseFrequencyCorrect ||
- timerMode === TimerMode.FastPWM) &&
- val > newVal
- ) {
+ if ((timerMode === TimerMode.Normal || timerMode === TimerMode.FastPWM) && val > newVal) {
this.TIFR |= TOV;
}
}
@@ -337,8 +378,28 @@ export class AVRTimer {
}
}
+ private phasePwmCount(value: u16, delta: u8) {
+ while (delta > 0) {
+ if (this.countingUp) {
+ value++;
+ if (value === this.TOP && !this.tcntUpdated) {
+ this.countingUp = false;
+ }
+ } else {
+ value--;
+ if (!value && !this.tcntUpdated) {
+ this.countingUp = true;
+ this.TIFR |= TOV;
+ }
+ }
+ delta--;
+ }
+ return value;
+ }
+
private timerUpdated() {
const value = this.tcnt;
+
if (this.ocrA && value === this.ocrA) {
this.TIFR |= OCFA;
if (this.timerMode === TimerMode.CTC) {
@@ -346,9 +407,57 @@ export class AVRTimer {
this.tcnt = 0;
this.TIFR |= TOV;
}
+ if (this.compA) {
+ this.updateCompPin(this.compA, 'A');
+ }
}
if (this.ocrB && value === this.ocrB) {
this.TIFR |= OCFB;
+ if (this.compB) {
+ this.updateCompPin(this.compB, 'B');
+ }
+ }
+ }
+
+ private updateCompPin(compValue: CompBitsValue, pinName: 'A' | 'B') {
+ let newValue: PinOverrideMode = PinOverrideMode.None;
+ const inverted = compValue === 3;
+ const isSet = this.countingUp === inverted;
+ switch (this.timerMode) {
+ case TimerMode.Normal:
+ case TimerMode.CTC:
+ case TimerMode.FastPWM:
+ newValue = compToOverride(compValue);
+ break;
+
+ case TimerMode.PWMPhaseCorrect:
+ case TimerMode.PWMPhaseFrequencyCorrect:
+ newValue = isSet ? PinOverrideMode.Set : PinOverrideMode.Clear;
+ break;
+ }
+
+ if (newValue !== PinOverrideMode.None) {
+ if (pinName === 'A') {
+ this.updateCompA(newValue);
+ } else {
+ this.updateCompB(newValue);
+ }
+ }
+ }
+
+ private updateCompA(value: PinOverrideMode) {
+ const { compPortA, compPinA } = this.config;
+ const hook = this.cpu.gpioTimerHooks[compPortA];
+ if (hook) {
+ hook(compPinA, value, compPortA);
+ }
+ }
+
+ private updateCompB(value: PinOverrideMode) {
+ const { compPortB, compPinB } = this.config;
+ const hook = this.cpu.gpioTimerHooks[compPortB];
+ if (hook) {
+ hook(compPinB, value, compPortB);
}
}
}
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.