diff --git a/software/TimeFrame_V4_0_full.ino b/software/TimeFrame_V4_0_full.ino new file mode 100644 index 0000000..771ffc2 --- /dev/null +++ b/software/TimeFrame_V4_0_full.ino @@ -0,0 +1,796 @@ +// TimeFrame v4.0 +// Copyright (C) 2016 Cubic-Print +// +// Original source code here: http://www.github.com/cubic-print/timeframe +// Paul Hutchison modifications: https://github.com/paulh-rnd/timeframe +// +// This a complete re-write of the sketch with the following changes +// (Chris Satterlee, November 2019): +// +// - More comments to help reader understand how it works +// - More use of named constants for readability +// - More use of functions for readability +// - Better constant and variable names +// - Unnecessary code removed +// - On-board LED heartbeat is now always on, but code rewritten to +// not interfere with timing +// - Implementation of Paul Hutchison's improvements: +// - Glowing LED for pushbutton (OPTIONAL) +// - Frame starts in standby mode +// - Additional analog input to control electromagnet duty +// (OPTIONAL) +// - Adjustments to ranges +// - NOTE: Paul removed "Magnet Off" (LED only) mode. This code +// brings it back (but it may be turned off with a +// constant) +// - NEW FEATURE: frequency control +// +// Note that this sketch is compatible with with both the original +// hardware design and with either or both of the hardware modifications +// added by Paul Hutchison (pushbutton LED and electromagnet strength +// knob.) However, the constants HW_HAS_PUSHBUTTON_LED and +// HW_HAS_MAG_STRENGTH_KNOB must be set appropriately below. +// +// The frequency control feature uses the same knob (potentiometer) as +// the LED brightness. The ability to vary the frequency allows the user +// to find the resonant frequency of the object(s). Frequency control +// mode is entered by "double clicking" the pushbutton when it is in the +// standby mode. The LED brightness value is captured on entry to the +// frequency control mode, and the knob is temporarily re-purposed to +// control the frequency. The speed control knob and the electromagnet +// strength knob work as normal while in frequency control +// mode. Frequency control mode is exited and normal slow motion mode is +// entered when the pushbutton is pressed. At this transition, the +// frequency value is captured and continues to be used while the frame +// is powered on. Additionally, the value is written to EEPROM. This +// value is read the next time the frame is powered on and is used until +// the user enters frequency control mode again to change it. +// +// In frequency control mode, the pushbutton LED blinks rapidly. On +// hardware that does not implement the pushbutton LED, the only way to +// tell that it is in frequency control mode is to go ahead and turn the +// knob that normally controls the brightness and see if it instead is +// controlling the frequency. +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// GNU General Public License terms: . +#include + +// Hardware feature constants +const bool HW_HAS_PUSHBUTTON_LED = true; +const bool HW_HAS_MAG_STRENGTH_KNOB = true; +// Software feature constants +const bool INCLUDE_MAGNET_OFF_MODE = true; +// Debug constants +const bool DEBUG_MODE = false; +const bool DEBUG_PRINT_OCRS = false; // Only if DEBUG_MODE +const bool DEBUG_PRINT_TARGET_VALUES = false; // Only if DEBUG_MODE +const uint16_t DEBUG_MS = 500; // Set to 0 for no delay +const uint16_t DEBUG_DECIMAL_PLACES = 2; +const uint32_t SERIAL_BAUD = 9600; +// ATmega328P constants +const float ANALOG_READ_MAX = 1023.0; +const float SYS_CLK_HZ = 16000000.0; // 16 Mhz +// Mode constants +const uint16_t POWER_ON = 0; +const uint16_t STANDBY = 1; +const uint16_t SLOW_MOTION = 2; +const uint16_t DISTORTED_REALITY = 3; +const uint16_t MAGNET_OFF = 4; +const uint16_t FREQ_CONTROL = 5; +// Pin constants +const uint8_t LED_DUTY_PIN = A0; // Brightness +const uint8_t FREQ_DELTA_PIN = A1; // Slow motion speed +const uint8_t MAG_DUTY_PIN = A2; // Magnet strength +const uint8_t FREQ_VALUE_PIN = A0; // Magnet frequency (shared with LED duty) +const uint16_t OC0A_PIN = 6; // Used for PUSHBUTTON_INPUT_PIN +const uint16_t OC0B_PIN = 5; +const uint16_t OC1A_PIN = 9; +const uint16_t OC1B_PIN = 10; +const uint16_t OC2A_PIN = 11; +const uint16_t OC2B_PIN = 3; +const uint16_t PUSHBUTTON_LED_PIN = OC0B_PIN; // Timer 0 (8-bit) +const uint16_t LED_PIN = OC1B_PIN; // Timer 1 (16-bit) +const uint16_t MAG_PIN = OC2B_PIN; // Timer 2 (8-bit) +const uint16_t PUSHBUTTON_INPUT_PIN = 6; +const uint16_t ONBOARD_LED_PIN = 13; +// Pushbutton LED constants +const float PUSHBUTTON_LED_STANDBY_PERIOD_MS = 3000.0; +const float PUSHBUTTON_LED_FREQ_CONTROL_PERIOD_MS = 200.0; +const float PUSHBUTTON_LED_MAX_DUTY = 100.0; // 255 = 100% +const float PUSHBUTTON_LED_OFF_PCT = 10.0; // < 100 +const float PUSHBUTTON_LED_DIM_RATIO = 4.0; +const uint16_t PUSHBUTTON_PRESSED = HIGH; +const uint16_t PUSHBUTTON_DEBOUNCE_MS = 100; +const uint16_t PUSHBUTTON_POLL_MS = 1; +const uint16_t PUSHBUTTON_DOUBLE_CLICK_MS = 300; +// Prescaler constants +const uint16_t TIMER1_PRESCALER = 8; // LED PWM +const uint16_t TIMER2_PRESCALER = 1024; // Mag PWM +// EEPROM constants +const float EEPROM_VALID_ADDRESS = 0; +const float EEPROM_VALID_VALUE = 112233.01; // Magic number @ addr 0 +const float EEPROM_FREQ_ADDRESS = 4; +// Frequency and duty tuning constants +const float BASE_FREQ_HZ = 77.0; +const float FREQ_RANGE_HZ = 14.0; // Total range, centered around BASE_FREQ_HZ +const float MIN_FREQ_DELTA_HZ = 0.5; // Negative OK! +const float MAX_FREQ_DELTA_HZ = 2.0; // Negative OK, but must be > MIN +const float MIN_LED_DUTY_PCT = 0.0; +const float MAX_LED_DUTY_PCT = 20.0; +const float MIN_MAG_DUTY_PCT = 5.0; +const float MAX_MAG_DUTY_PCT = 25.0; +const float DEFAULT_MAG_DUTY_PCT = 7.0; // If no mag strength knob +// Heartbeat constants +const uint32_t HEARTBEAT_PERIOD_MS = 2000; +const uint32_t HEARTBEAT_T1_MS = 300; // On until T1 +const uint32_t HEARTBEAT_T2_MS = 600; // Off until T2 +const uint32_t HEARTBEAT_T3_MS = 800; // On until T3, then off +// Derived constants +const float PB_MAX_DUTY_DIVISOR = 2.0 - (PUSHBUTTON_LED_OFF_PCT / 100.0) * 2.0; +const float MIN_FREQ_HZ = BASE_FREQ_HZ - (FREQ_RANGE_HZ/2.0); +const float MAX_FREQ_HZ = BASE_FREQ_HZ + (FREQ_RANGE_HZ/2.0); +const float TIMER1_TICK_HZ = SYS_CLK_HZ / TIMER1_PRESCALER; +const float TIMER2_TICK_HZ = SYS_CLK_HZ / TIMER2_PRESCALER; +const float MIN_POSSIBLE_LED_HZ = TIMER1_TICK_HZ / (1L << 16); +const float MAX_POSSIBLE_LED_HZ = TIMER1_TICK_HZ / 2; +const float MIN_POSSIBLE_MAG_HZ = TIMER2_TICK_HZ / (1L << 8); +const float MAX_POSSIBLE_MAG_HZ = TIMER2_TICK_HZ / 2; + +// Global variables +float mag_freq_hz; +float led_freq_hz; +float led_freq_delta_hz; // Slow motion speed (positive: LED freq is > magnet) +float led_duty_pct; // Brightness +float mag_duty_pct; // Electromagnet strength +uint32_t mode_start_time; +uint16_t mode = POWER_ON; +uint16_t prev_mode = POWER_ON; + +void setup() { + // Start serial + Serial.begin(SERIAL_BAUD); + + // Set pin modes + set_pin_modes(); + + // Initialize mode timer + capture_mode_start_time(); + + // Set initial magnet frequency variable + set_mag_freq(); + + // POWER_ON mode -> STANDBY mode + mode = STANDBY; +} + +void loop() { + // Per-mode actions + switch (mode) { + + case STANDBY: + if (mode_changed()) { + mag_and_led_off(); + } + update_pushbutton_led(); // Slow throb + break; + + case SLOW_MOTION: + if (mode_changed()) { + update_pushbutton_led(); // solid on (dim) + mag_and_led_on(); + if (prev_mode == FREQ_CONTROL) + write_freq_to_eeprom(); + } + break; + + case MAGNET_OFF: + if (mode_changed()) + mag_off(); + break; + + case FREQ_CONTROL: + update_pushbutton_led(); // Fast throb + set_mag_freq(); + break; + + default: + break; + } + + // Get knob settings + get_knob_settings(); + + // Set LED frequency based on magnet frequency and knob + set_led_freq(); + + // Update Output Compare Registers + update_ocrs(); + + // Check pushbutton and change mode if required + prev_mode = mode; + mode = next_mode(mode); + + // Print debug info, if enabled + debug_monitor(); + + // Update onboard LED heartbeat value + heartbeat(); +} + +void set_pin_modes() { + // Outputs + if (HW_HAS_PUSHBUTTON_LED) + pinMode(PUSHBUTTON_LED_PIN, OUTPUT); + pinMode(LED_PIN, OUTPUT); + pinMode(MAG_PIN, OUTPUT); + pinMode(ONBOARD_LED_PIN, OUTPUT); + if (DEBUG_MODE) { + pinMode(OC1A_PIN, OUTPUT); // 1/2 LED frequency, 50% duty + pinMode(OC2A_PIN, OUTPUT); // 1/2 magnet frequency, 50% duty + } + // Inputs + pinMode(PUSHBUTTON_INPUT_PIN, INPUT); + pinMode(LED_DUTY_PIN, INPUT); // Aka FREQ_VALUE_PIN + pinMode(FREQ_DELTA_PIN, INPUT); + if (HW_HAS_MAG_STRENGTH_KNOB) + pinMode(MAG_DUTY_PIN, INPUT); +} + +void capture_mode_start_time() { + mode_start_time = millis(); +} + +void set_mag_freq() { + // Set the mag_freq_hz global variable. This depends on the mode. In + // POWER_ON mode the mag_freq_hz value is read from EEPROM (if + // valid). In FREQ_CONTROL mode, it is based on the frequency control + // knob (which is the same as the brightness knob.) In other modes, + // the value doesn't change. + if (mode == POWER_ON) { + mag_freq_hz = get_initial_mag_frequency(); + } else if (mode == FREQ_CONTROL) { + mag_freq_hz = value_in_range(MIN_FREQ_HZ, MAX_FREQ_HZ, + knob_value(FREQ_VALUE_PIN)); + } + // Else keep current value + + // Constrain to limits + mag_freq_hz = get_limited_mag_hz(mag_freq_hz); +} + +void set_led_freq() { + // The LED frequency is a delta from the magnet frequency. Since the + // magnet PWM runs on the 8-bit Timer1, its actual frequency can be + // different from the target frequency by a significant amount + // relative to the delta, so we want to use the actual magnet + // frequency. That is based on the OCR2A value. Note that since the + // OCRs are updated after the call to this function in loop(), it will + // not get properly updated until the subsequent loop() iteration. + float actual_mag_freq_hz = calculate_actual_hz(2,TIMER2_PRESCALER); + // Normal: LED freq > mag freq by delta (which can be negative) + led_freq_hz = actual_mag_freq_hz + led_freq_delta_hz; + if (mode == DISTORTED_REALITY) + // Base frequency doubled + led_freq_hz = (actual_mag_freq_hz * 2) + led_freq_delta_hz; + + // Constrain to limits + led_freq_hz = get_limited_led_hz(led_freq_hz); +} + +float get_initial_mag_frequency() { + // Read the EEPROM to get the initial frequency. If the EEPROM has not + // been programmed with the frequency, use BASE_FREQ_HZ. + uint16_t eeprom_valid_count; + float eeprom_value; + // Check that address 0 has "magic" value + EEPROM.get(EEPROM_VALID_ADDRESS, eeprom_value); + if (eeprom_value == EEPROM_VALID_VALUE) { + EEPROM.get(EEPROM_FREQ_ADDRESS, eeprom_value); + if (DEBUG_MODE) { + Serial.print("Initial magnet frequency (from EEPROM): "); + Serial.println(eeprom_value); + } + return (eeprom_value); + } else { + // If EEPROM is not programmed, use BASE_FREQ_HZ + Serial.print("Initial magnet frequency (BASE_FREQ_HZ): "); + Serial.println(BASE_FREQ_HZ); + return (BASE_FREQ_HZ); + } +} + +void write_freq_to_eeprom() { + // Write magic number to address 0 and magnet frequency to address 4 + EEPROM.put(EEPROM_VALID_ADDRESS, EEPROM_VALID_VALUE); + EEPROM.put(EEPROM_FREQ_ADDRESS, mag_freq_hz); + if (DEBUG_MODE) { + Serial.print("Saved magnet frequency to EEPROM: "); + Serial.println(mag_freq_hz); + } +} + +float knob_value(uint8_t pin) { + // Returns a value between 0.0 (knob turned max counterclockwise) and + // 1.0 (knob turned max clockwise). + // + // The potentiometer resistance is zero when the knob is turned all + // the way counterclockwise. This results in a voltage of +5V at the + // analog input pin and analogRead() returns a value of 1023. The + // potentiometer resistance is at its max when the knob is turned all + // the way clockwise. This results in a voltage of zero at the analog + // input pin and analogRead() returns a value of 0. This is opposite + // from what we want. + // + // This function flips the sense, and scales the value so that the + // value returned increases from zero to one as the knob is turned + // clockwise. + return ((ANALOG_READ_MAX - analogRead(pin)) / ANALOG_READ_MAX); +} + +float value_in_range(float min_value, float max_value, float knob_value) { + // This function uses the knob value and translates it to a value + // between the specified minimum and maximum values. + return (min_value + ((max_value - min_value) * knob_value)); +} + +void get_knob_settings() { + set_led_freq_delta(); + set_led_duty_pct(); + set_mag_duty_pct(); +} + +void set_led_freq_delta() { + // Set the led_freq_delta_hz global variable based on the slow motion + // speed knob position + led_freq_delta_hz = value_in_range(MIN_FREQ_DELTA_HZ, MAX_FREQ_DELTA_HZ, + knob_value(FREQ_DELTA_PIN)); +} + +void set_led_duty_pct() { + // In frequency control mode, the LED brightness knob is "borrowed" + // for adjusting the frequency, so the current led_duty_pct value is + // maintained. However, once slow motion mode is re-entered, the LED + // duty will be set based on where the knob was left from the + // frequency adjustment. The user will just have to readjust the + // brightness at that point. + if (mode != FREQ_CONTROL) { + // Set the led_duty_pct global variable based on the brightness knob + // position + led_duty_pct = value_in_range(MIN_LED_DUTY_PCT, MAX_LED_DUTY_PCT, + knob_value(LED_DUTY_PIN)); + } +} + +void set_mag_duty_pct() { + if (HW_HAS_MAG_STRENGTH_KNOB) { + // Set the mag_duty_pct global variable based on the magnet strength + // knob position + mag_duty_pct = value_in_range(MIN_MAG_DUTY_PCT, MAX_MAG_DUTY_PCT, + knob_value(MAG_DUTY_PIN)); + } else { + // No knob: use default + mag_duty_pct = DEFAULT_MAG_DUTY_PCT; + } +} + +uint16_t next_mode(uint16_t current_mode) { + uint16_t next_mode = STANDBY; + uint32_t current_mode_time; + + // POWER_ON mode immediately transitions to STANDBY + if (current_mode == POWER_ON) + return STANDBY; + + // Get current mode time. Note that this is the time between the + // previous pushbutton -release- and the current pushbutton -press-. + current_mode_time = get_current_mode_time(); + + // If button is not pressed or if this is a release bounce, keep + // current mode + if (!pushbutton_pressed() || (current_mode_time < PUSHBUTTON_DEBOUNCE_MS)) + return current_mode; + + // Button is pressed. Wait until it is released + wait_for_pushbutton_release(); + + // Determine next mode + switch (current_mode) { + case STANDBY: + next_mode = SLOW_MOTION; + break; + case SLOW_MOTION: + next_mode = DISTORTED_REALITY; // Normal case + if (current_mode_time < PUSHBUTTON_DOUBLE_CLICK_MS) + // If we've been in slow motion mode for a very short amount of + // time, this is the "double click" that overrides the normal case + // and puts us into frequency control mode. + next_mode = FREQ_CONTROL; + break; + case DISTORTED_REALITY: + next_mode = STANDBY; + if (INCLUDE_MAGNET_OFF_MODE) + next_mode = MAGNET_OFF; + break; + case MAGNET_OFF: + next_mode = STANDBY; + break; + case FREQ_CONTROL: + next_mode = SLOW_MOTION; + break; + } + capture_mode_start_time(); + return next_mode; +} + +bool mode_changed() { + return (mode != prev_mode); +} + +bool pushbutton_pressed() { + if (digitalRead(PUSHBUTTON_INPUT_PIN) == PUSHBUTTON_PRESSED) + return true; + // Otherwise, it's not pressed - don't read it again! + return false; +} + +void wait_for_pushbutton_release() { + delay(PUSHBUTTON_DEBOUNCE_MS); + while (pushbutton_pressed()) + delay(PUSHBUTTON_POLL_MS); +} + +uint32_t get_current_mode_time() { + uint32_t current_time = millis(); + if (mode_start_time > current_time) { + // Timer wrapped (happens once every 50 days). No need to get fancy, + // just return current time. + return current_time; + } + // Normal case + return (current_time - mode_start_time); +} + +void update_pushbutton_led() { + uint16_t duty; + if (!HW_HAS_PUSHBUTTON_LED) + return; + switch (mode) { + case STANDBY: + duty = get_pushbutton_led_duty(PUSHBUTTON_LED_STANDBY_PERIOD_MS); + break; + case SLOW_MOTION: + case DISTORTED_REALITY: + duty = PUSHBUTTON_LED_MAX_DUTY / PUSHBUTTON_LED_DIM_RATIO; + break; + case FREQ_CONTROL: + duty = get_pushbutton_led_duty(PUSHBUTTON_LED_FREQ_CONTROL_PERIOD_MS); + break; + } + analogWrite(PUSHBUTTON_LED_PIN, duty); +} + +uint16_t get_pushbutton_led_duty(float period_ms) { + // In STANDBY mode the pushbutton LED "throbs" (for lack of a better + // word). This is accomplished by cyclically modifying the duty cycle + // of the PWM pin that drives it. + // + // The pushbutton LED uses "simple PWM", i.e. analogWrite(). The + // "value" parameter for analogWrite() determines the duty cycle and + // is an integer between 0 (always off) to 255 (always on). + // + // This function is passed a period (interval between throbs) in + // milliseconds. It uses that, along with the current time in + // milliseconds to determine the current duty cycle value for + // analogWrite(). The throbbing is sinusoidal. Complicating the math a + // bit is support for having the LED be off for some percentage of + // each period (PUSHBUTTON_LED_OFF_PCT). + // + // This function is also used for the much faster throbbing in + // FREQ_CONTROL mode. Simple blinking would have been fine for that, + // but it was easier to just use this same function with a shorter + // period. + float radians = 2.0 * PI * millis() / period_ms; + int16_t led_duty = ((PUSHBUTTON_LED_MAX_DUTY / PB_MAX_DUTY_DIVISOR) * + ((PB_MAX_DUTY_DIVISOR - 1.0) + sin(radians))); + led_duty = (led_duty < 0) ? 0 : led_duty; // Off when negative + return led_duty; +} + +void update_ocrs() { + // The ATmega328P Output Compare Registers (OCRs) determine the + // frequency and duty cycle of the PWM signal. With the TCCRs + // programmed as they are by led_on() and mag_on(), this works as + // follows: + // + // OCRnA is programmed with the TOP value and OCRnB is programmed with + // the compare value. + // + // The TOP value of the timer is the value at which it will wrap back + // to BOTTOM (i.e. zero). This determines the frequency (but not duty + // cycle) of the PWM output; the larger the TOP value, the slower the + // PWM output frequency. The timer runs at the chip clock frequency + // (16 Mhz) divided by the prescaler (programmed in the TCCRnB + // register). Setting the TOP value to 1 results in a frequency that + // is equal to half the timer frequency (e.g. 1 MHz for a prescaler of + // 8, 7.8125 kHz fpr a prescaler of 1024). The maximum TOP value for + // the 8-bit timers (Timer0 and Timer2) is 255, and the maximum TOP + // value for the 16-bit Timer1 is 65535. + // + // The PWM output is HIGH when the timer equals 0 and continues to be + // HIGH up to and including the cycle when the timer equals the + // compare value. The PWM output is low when the timer value is + // greater than the compare value. This determines the duty cycle. + update_led_ocrs(); + update_mag_ocrs(); +} +void update_led_ocrs() { + uint32_t top_value; + uint32_t compare_value; + top_value = get_top_value(led_freq_hz, TIMER1_PRESCALER); + compare_value = get_compare_value(led_duty_pct, top_value); + OCR1A = top_value; + OCR1B = compare_value; +} + +void update_mag_ocrs() { + uint32_t top_value; + uint32_t compare_value; + top_value = get_top_value(mag_freq_hz, TIMER2_PRESCALER); + compare_value = get_compare_value(mag_duty_pct, top_value); + OCR2A = top_value; + OCR2B = compare_value; +} + +uint32_t get_top_value(float hz_tgt, uint16_t prescaler) { + // Get the TOP value (OCRnA) that will achieve a PWM frequency closest + // to the target + float top_value_float; + uint32_t top_value; + top_value_float = ((SYS_CLK_HZ / prescaler) / hz_tgt) - 1.0; + top_value = top_value_float + 0.5; // rounds to integer + return top_value; +} + +uint32_t get_compare_value(float duty_pct_tgt, uint32_t top_value) { + // Get the compare value (OCRnB) that will achieve a duty cycle + // closest to the target + uint32_t compare_value_float; + uint32_t compare_value; + compare_value_float = ((duty_pct_tgt / 100) * (top_value + 1)) - 1.0; + compare_value = compare_value_float + 0.5; // rounds to integer + return compare_value; +} + +float calculate_actual_hz(uint16_t timer, uint16_t prescaler) { + uint32_t timer_top_value; + switch (timer) { + case 0: + timer_top_value = OCR0A; + break; + case 1: + timer_top_value = OCR1A; + break; + case 2: + timer_top_value = OCR2A; + break; + } + return (SYS_CLK_HZ / prescaler / (timer_top_value + 1)); +} + +float calculate_actual_duty_pct(uint16_t timer) { + uint32_t timer_top_value; + uint32_t timer_compare_value; + switch (timer) { + case 0: + timer_top_value = OCR0A; + timer_compare_value = OCR0B; + break; + case 1: + timer_top_value = OCR1A; + timer_compare_value = OCR1B; + break; + case 2: + timer_top_value = OCR2A; + timer_compare_value = OCR2B; + break; + } + return ((float(timer_compare_value) + 1) / (timer_top_value + 1) * 100); +} + +float get_limited_mag_hz(float mag_freq_hz) { + if (mag_freq_hz < MIN_POSSIBLE_MAG_HZ) { + if (DEBUG_MODE) { + Serial.print("Magnet frequency "); + Serial.print(mag_freq_hz); + Serial.print(" Hz is too low, using "); + Serial.println(MIN_POSSIBLE_MAG_HZ); + } + return MIN_POSSIBLE_MAG_HZ; + } + if (mag_freq_hz > MAX_POSSIBLE_MAG_HZ) { + if (DEBUG_MODE) { + Serial.print("Magnet frequency "); + Serial.print(mag_freq_hz); + Serial.print(" Hz is too high, using "); + Serial.println(MAX_POSSIBLE_MAG_HZ); + } + return MAX_POSSIBLE_MAG_HZ; + } + return mag_freq_hz; +} + +float get_limited_led_hz(float led_freq_hz) { + if (led_freq_hz < MIN_POSSIBLE_LED_HZ) { + if (DEBUG_MODE) { + Serial.print("LED frequency "); + Serial.print(led_freq_hz); + Serial.print(" Hz is too low, using "); + Serial.println(MIN_POSSIBLE_LED_HZ); + } + return MIN_POSSIBLE_LED_HZ; + } + if (led_freq_hz > MAX_POSSIBLE_LED_HZ) { + if (DEBUG_MODE) { + Serial.print("LED frequency "); + Serial.print(led_freq_hz); + Serial.print(" Hz is too high, using "); + Serial.println(MAX_POSSIBLE_LED_HZ); + } + return MAX_POSSIBLE_LED_HZ; + } + return led_freq_hz; +} + +void mag_and_led_on() { + mag_on(); + led_on(); +} + +void mag_and_led_off() { + mag_off(); + led_off(); +} + +void led_on() { + // Write TCCR1A and TCCR1B registers with: + // COM1A = b01 (OC1A pin toggles on compare match) + // COM1B = b10 (OC1B pin cleared on compare match and set at 0) + // WGM = b1111 (waveform generation mode 15) + // CS = b010 (prescaler = 8) + // ATmega328P Datasheet: + // Table 16-2 (COM), Table 16-4 (WGM), Table 16-5 (CS) + TCCR1A = _BV(COM1A0) | _BV(COM1B1) | _BV(WGM11) | _BV(WGM10); + TCCR1B = _BV(WGM13) | _BV(WGM12) | _BV(CS11); +} + +void led_off() { + // WGM = b0000 (Waveform generation mode 0) + TCCR1A = _BV(COM1A0) | _BV(COM1B1); + TCCR1B = _BV(CS11); +} + +void mag_on() { + // Write TCCR2A and TCCR2B registers with: + // COM2A = b01 (OC2A pin toggles on compare match) + // COM2B = b10 (OC2B pin cleared on compare match and set at 0) + // WGM = b111 (waveform generation mode 7) + // CS = b111 (prescaler = 1024) + // ATmega328P Datasheet: + // Tables 18-3,18-6 (COM), Table 18-8 (WGM), Table 18-9 (CS) + TCCR2A = _BV(COM2A0) | _BV(COM2B1) | _BV(WGM21) | _BV(WGM20); + TCCR2B = _BV(WGM22) | _BV(CS22) | _BV(CS21) | _BV(CS20); +} + +void mag_off() { + // WGM = b000 (Waveform generation mode 0) + TCCR2A = _BV(COM2A0) | _BV(COM2B1); + TCCR2B = _BV(CS22) | _BV(CS21)| _BV(CS20); +} + +void debug_monitor() { + // Print the actual frequency, duty and delta values (based on + // OCRs). Optionally print the OCR values themselves and the target + // frequency, duty and delta values. + if (DEBUG_MODE) { + if (millis() % (DEBUG_MS + 1) != 0) + // Rate limit debug to once per DEBUG_MS milliseconds. + return; + float actual_led_freq_hz = calculate_actual_hz(1,TIMER1_PRESCALER); + float actual_mag_freq_hz = calculate_actual_hz(2,TIMER2_PRESCALER); + float actual_freq_delta_hz = actual_led_freq_hz - actual_mag_freq_hz; + print_mode(); + Serial.print(" Mode,"); + Serial.print(" LED: "); + Serial.print(actual_led_freq_hz, DEBUG_DECIMAL_PLACES); + Serial.print(" Hz, "); + Serial.print(calculate_actual_duty_pct(1), DEBUG_DECIMAL_PLACES); + Serial.print("% duty"); + Serial.print(" Mag: "); + Serial.print(actual_mag_freq_hz, DEBUG_DECIMAL_PLACES); + Serial.print(" Hz, "); + Serial.print(calculate_actual_duty_pct(2), DEBUG_DECIMAL_PLACES); + Serial.print("% duty"); + Serial.print(" Delta: "); + Serial.print(actual_freq_delta_hz, DEBUG_DECIMAL_PLACES); + Serial.print(" Hz"); + if (DEBUG_PRINT_OCRS) { + Serial.print(" OCR1A: "); + Serial.print(OCR1A); + Serial.print(" OCR1B: "); + Serial.print(OCR1B); + Serial.print(" OCR2A: "); + Serial.print(OCR2A); + Serial.print(" OCR2B: "); + Serial.print(OCR2B); + } + Serial.println(""); + if (DEBUG_PRINT_TARGET_VALUES) { + print_target_values(); + } + } +} + +void print_target_values() { + // These are the "ideal" values for frequency, duty and delta before + // the rounding effects of the 8-bit and 16-bit OCRs. + Serial.print(" Targets: "); + Serial.print(" LED: "); + Serial.print(led_freq_hz, DEBUG_DECIMAL_PLACES); + Serial.print(" Hz, "); + Serial.print(led_duty_pct, DEBUG_DECIMAL_PLACES); + Serial.print("% duty"); + Serial.print(" Mag: "); + Serial.print(mag_freq_hz, DEBUG_DECIMAL_PLACES); + Serial.print(" Hz, "); + Serial.print(mag_duty_pct, DEBUG_DECIMAL_PLACES); + Serial.print("% duty"); + Serial.print(" Delta: "); + Serial.print(led_freq_delta_hz, DEBUG_DECIMAL_PLACES); + Serial.println(" Hz"); +} + +void print_mode() { + // Prints mode name (no trailing whitespace or carriage return) + if (mode == POWER_ON) + Serial.print(" POWER_ON"); + else if (mode == STANDBY) + Serial.print(" STANDBY"); + else if (mode == SLOW_MOTION) + Serial.print(" SLOW_MOTION"); + else if (mode == DISTORTED_REALITY) + Serial.print("DISTORTED_REALITY"); + else if (mode == MAGNET_OFF) + Serial.print(" MAGNET_OFF"); + else if (mode == FREQ_CONTROL) + Serial.print(" FREQ_CONTROL"); + else + Serial.print(" UNKNOWN"); +} + +void heartbeat() { + // Non-blocking, i.e. no calls to delay() + uint32_t ms_in_period = millis() % HEARTBEAT_PERIOD_MS; + if (ms_in_period < HEARTBEAT_T1_MS) { + // HIGH until T1 + digitalWrite(ONBOARD_LED_PIN, HIGH); // Thump + } else if (ms_in_period < HEARTBEAT_T2_MS) { + // LOW until T2 + digitalWrite(ONBOARD_LED_PIN, LOW); + } else if (ms_in_period < HEARTBEAT_T3_MS) { + // HIGH until T3 + digitalWrite(ONBOARD_LED_PIN, HIGH); // Thump + } else { + // LOW the rest of the period + digitalWrite(ONBOARD_LED_PIN, LOW); + } +}