/*
* ============================================================================
* QCW DRSSTC Generator v4.0 with ILI9341 Display
* ============================================================================
*
* Target: Arduino Nano (ATmega328P @ 16 MHz) + ILI9341 320x240 TFT
*
* Hardware config:
* - 325V bus, no doubler
* - 4x bus caps = 13200 uF
* - CFDA bridge (IPW65R080CFDA)
* - Buck stage: FF200R12KS4 + C4D40120D + 250uH + 20-40uF MKP
* - Open-loop control (Davekni approach)
* - ILI9341 display via hardware SPI
*
* Pin Assignments:
* D2 - S1 FIRE / ARM
* D3 - S2 AUTO/MANUAL toggle
* D4 - S3 MODE select
* D5 - Interrupter -> fiber LED #1
* D6 - S4 EMERGENCY STOP
* D7 - TFT_DC (Data/Command)
* D8 - TFT_RST (Reset)
* D9 - Buck PWM -> fiber LED #2 (Timer1 OC1A 40 kHz)
* D10 - TFT_CS (Chip Select)
* D11 - SPI MOSI (TFT MOSI)
* D12 - SPI MISO (TFT MISO, optional)
* D13 - SPI SCK (TFT SCK)
* D0/D1 - HW Serial PC debug
*
* A0 - PW Pulse Width (5-25 ms)
* A1 - Bps Burst Per Second (0.5-15 Hz)
* A2 - Ampl Ramp Peak Duty (0-95%)
* A3 - Wick Start Offset (0-75% start duty)
* A4 - Ampsin Sine Mod Amplitude (0=off, makes ramp wavy)
* A5 - Fsin Sine Mod Frequency (10 Hz - 5 kHz)
* A6 - Tscale Time Scale (0.5-2x pulse multiplier)
* A7 - Uscale Voltage Scale (0.3-1x peak duty limiter)
*
* Required Libraries (install via Arduino Library Manager):
* - Adafruit GFX Library
* - Adafruit ILI9341
*
* Display wiring (ILI9341 module):
* VCC -> Arduino 5V (or 3.3V if 5V-tolerant module)
* GND -> Arduino GND
* CS -> Arduino D10
* RESET -> Arduino D8
* DC -> Arduino D7
* SDI/MOSI -> Arduino D11
* SCK -> Arduino D13
* LED -> Arduino 3.3V (or 100 ohm resistor)
* SDO/MISO -> Arduino D12 (or leave disconnected)
*
* IMPORTANT: Display updates ONLY between pulses, NEVER during pulse,
* to avoid timing disruption.
*
* ============================================================================
*/
#include <avr/wdt.h>
#include <avr/interrupt.h>
#include <avr/io.h>
#include <avr/pgmspace.h>
#include <SPI.h>
#include <Adafruit_GFX.h>
#include <Adafruit_ILI9341.h>
// ============================================================================
// PIN DEFINITIONS
// ============================================================================
#define PIN_BTN_FIRE 2
#define PIN_BTN_AUTO 3
#define PIN_BTN_MODE 4
#define PIN_INTERRUPTER 5
#define PIN_BTN_ESTOP 6
#define TFT_DC 7
#define TFT_RST 8
#define PIN_PWM_RAMP 9
#define TFT_CS 10
#define PIN_POT_PW A0
#define PIN_POT_BPS A1
#define PIN_POT_AMPL A2
#define PIN_POT_WICK A3
#define PIN_POT_AMPSIN A4
#define PIN_POT_FSIN A5
#define PIN_POT_TSCALE A6
#define PIN_POT_USCALE A7
// ============================================================================
// DISPLAY OBJECT
// ============================================================================
Adafruit_ILI9341 tft = Adafruit_ILI9341(TFT_CS, TFT_DC, TFT_RST);
// Screen dimensions (landscape orientation)
#define SCR_W 320
#define SCR_H 240
// Layout regions
#define HEADER_H 18
#define PARAMS_Y (HEADER_H + 2)
#define PARAMS_H 56 // 2 rows instead of 3 (parameters compacted)
#define RAMP_Y (PARAMS_Y + PARAMS_H + 2)
#define RAMP_H 140 // увеличено с 110 для лучшей visibility
#define FOOTER_Y (RAMP_Y + RAMP_H + 2)
#define FOOTER_H (SCR_H - FOOTER_Y)
// Colors (RGB565)
#define COL_BG ILI9341_BLACK
#define COL_HEADER 0x2104
#define COL_TEXT ILI9341_WHITE
#define COL_LABEL ILI9341_CYAN
#define COL_VAL ILI9341_WHITE
#define COL_RAMP_BG 0x0841
#define COL_RAMP_GRID 0x2945
#define COL_RAMP ILI9341_GREEN
#define COL_RAMP_LIN 0x4208
#define COL_ARMED ILI9341_RED
#define COL_DISARM ILI9341_GREEN
#define COL_AUTO ILI9341_ORANGE
#define COL_MANUAL ILI9341_CYAN
#define COL_PULSE ILI9341_YELLOW
// ============================================================================
// CONSTANTS
// ============================================================================
#define PWM_TOP 400
#define MAX_DUTY 380
#define MIN_DUTY 0
#define PULSE_WIDTH_MIN_MS 5
#define PULSE_WIDTH_MAX_MS 25
#define BPS_MIN_X10 5
#define BPS_MAX_X10 150
#define ALPHA_X1000_DEFAULT 210
// Pulse shape: fall phase fraction
// 12 = 12% of pulse is fall (88% rise), gives sharp but smooth cutoff
// Range typical 8-20 (lower = sharper fall, higher = more gradual)
#define FALL_FRACTION_X100 12
#define WICK_MAX_DUTY 300
#define AMPSIN_MAX 50 // Reduced from 100 — less aggressive sine modulation
#define FSIN_MIN_HZ 10
#define FSIN_MAX_HZ 2000 // Reduced from 5000 — slower waves, more visible
#define TSCALE_MIN_X100 0 // Curve magnitude: 0 = max sag, 100 = linear, 200 = max bulge
#define TSCALE_MAX_X100 200
#define USCALE_MIN_X100 10 // Curve POSITION: 10 = peak в начале rise (10%)
#define USCALE_MAX_X100 90 // 50 = середина (default), 90 = в конце (90%)
// Reference PW для horizontal display scaling
#define PW_REFERENCE_MS PULSE_WIDTH_MAX_MS // 25ms = full screen width
#define DEBOUNCE_MS 50
#define WATCHDOG_TIMEOUT WDTO_2S
#define DISPLAY_UPDATE_MS 250 // Update display every 250 ms between pulses
// ============================================================================
// OPERATION MODES
// ============================================================================
enum OperationMode {
MODE_QCW_STANDARD = 0,
MODE_QCW_SINE_MOD = 1,
MODE_PHASE1_INTERRUPT = 2,
MODE_PHASE1_5_PDM = 3,
MODE_CW_QCW = 4, // Continuous-wave QCW: back-to-back sawtooth ramps
MODE_COUNT = 5
};
const char MODE_0[] PROGMEM = "QCW STD";
const char MODE_1[] PROGMEM = "QCW SIN";
const char MODE_2[] PROGMEM = "PH1 INT";
const char MODE_3[] PROGMEM = "PH1 PDM";
const char MODE_4[] PROGMEM = "CW QCW";
const char* const MODE_NAMES[] PROGMEM = {MODE_0, MODE_1, MODE_2, MODE_3, MODE_4};
// ============================================================================
// STATE
// ============================================================================
volatile uint16_t pulse_width_ms = 10;
volatile uint16_t bps_x10 = 20;
volatile uint16_t peak_duty = MAX_DUTY / 2;
volatile uint16_t wick_offset = 0;
volatile uint16_t ampsin = 0;
volatile uint16_t fsin_hz = 100;
volatile uint16_t tscale_x100 = 100;
volatile uint16_t uscale_x100 = 100;
volatile uint16_t alpha_x1000 = ALPHA_X1000_DEFAULT;
volatile bool fire_armed = false;
volatile bool auto_mode = false;
volatile bool manual_pulse_pending = false;
volatile uint8_t current_mode = MODE_QCW_STANDARD;
volatile bool estop_triggered = false;
struct ButtonState {
bool last_state;
bool stable_state;
uint32_t last_change_ms;
};
ButtonState btn_fire = {HIGH, HIGH, 0};
ButtonState btn_auto = {HIGH, HIGH, 0};
ButtonState btn_mode = {HIGH, HIGH, 0};
ButtonState btn_estop = {HIGH, HIGH, 0};
uint32_t last_pulse_ms = 0;
uint32_t last_pot_update_ms = 0;
uint32_t last_display_ms = 0;
uint32_t estop_clear_ms = 0;
uint32_t pulse_counter = 0;
// CW QCW mode state
bool cw_running = false;
uint32_t cw_start_us = 0;
uint32_t last_ramp_position = 0;
// Last drawn values (for diff-based redraw to reduce flicker)
struct DrawnState {
uint16_t pulse_width_ms;
uint16_t bps_x10;
uint8_t ampl_pct;
uint8_t wick_pct;
uint8_t ampsin_pct;
uint16_t fsin_hz;
uint16_t tscale_x100;
uint16_t uscale_x100;
uint16_t alpha_x1000;
bool armed;
bool auto_mode;
uint8_t op_mode;
uint32_t pulse_count;
bool valid;
};
DrawnState drawn = {0,0,0,0,0,0,0,0,0,false,false,0,0,false};
// Sine table for audio modulation
const int8_t sine_table[256] PROGMEM = {
0, 3, 6, 9, 12, 15, 18, 21, 24, 27, 30, 33, 36, 39, 42, 45,
48, 51, 54, 57, 59, 62, 65, 67, 70, 73, 75, 78, 80, 82, 85, 87,
89, 91, 94, 96, 98, 100, 102, 103, 105, 107, 108, 110, 111, 113, 114, 116,
117, 118, 119, 120, 121, 122, 123, 123, 124, 125, 125, 126, 126, 126, 126, 126,
127, 126, 126, 126, 126, 126, 125, 125, 124, 123, 123, 122, 121, 120, 119, 118,
117, 116, 114, 113, 111, 110, 108, 107, 105, 103, 102, 100, 98, 96, 94, 91,
89, 87, 85, 82, 80, 78, 75, 73, 70, 67, 65, 62, 59, 57, 54, 51,
48, 45, 42, 39, 36, 33, 30, 27, 24, 21, 18, 15, 12, 9, 6, 3,
0, -3, -6, -9, -12, -15, -18, -21, -24, -27, -30, -33, -36, -39, -42, -45,
-48, -51, -54, -57, -59, -62, -65, -67, -70, -73, -75, -78, -80, -82, -85, -87,
-89, -91, -94, -96, -98, -100, -102, -103, -105, -107, -108, -110, -111, -113, -114, -116,
-117, -118, -119, -120, -121, -122, -123, -123, -124, -125, -125, -126, -126, -126, -126, -126,
-127, -126, -126, -126, -126, -126, -125, -125, -124, -123, -123, -122, -121, -120, -119, -118,
-117, -116, -114, -113, -111, -110, -108, -107, -105, -103, -102, -100, -98, -96, -94, -91,
-89, -87, -85, -82, -80, -78, -75, -73, -70, -67, -65, -62, -59, -57, -54, -51,
-48, -45, -42, -39, -36, -33, -30, -27, -24, -21, -18, -15, -12, -9, -6, -3
};
// ============================================================================
// FORWARD DECLARATIONS
// ============================================================================
void setup_timer1_pwm();
void emergency_stop();
void update_pots();
void handle_buttons();
bool check_button_press(uint8_t pin, ButtonState& state);
int32_t calculate_duty(uint32_t t_elapsed_us, uint32_t pulse_us, uint8_t mode);
void fire_pulse();
uint16_t read_pot_oversampled(uint8_t pin);
int32_t calc_curve_deviation(int32_t rise_t, int32_t ramp_range);
void display_init();
void draw_layout();
void draw_header();
void draw_parameters();
void draw_ramp_shape();
void draw_footer();
int calc_duty_for_display(int t_norm);
// ============================================================================
// SETUP
// ============================================================================
void setup() {
wdt_disable();
// Configure pins
pinMode(PIN_BTN_FIRE, INPUT_PULLUP);
pinMode(PIN_BTN_AUTO, INPUT_PULLUP);
pinMode(PIN_BTN_MODE, INPUT_PULLUP);
pinMode(PIN_BTN_ESTOP, INPUT_PULLUP);
pinMode(PIN_INTERRUPTER, OUTPUT);
pinMode(PIN_PWM_RAMP, OUTPUT);
digitalWrite(PIN_INTERRUPTER, LOW);
digitalWrite(PIN_PWM_RAMP, LOW);
setup_timer1_pwm();
Serial.begin(115200);
Serial.println(F("QCW Generator v4.0 starting..."));
// CRITICAL: Initialize button states by reading actual pin values
// This prevents false "press" detection on boot if pins read LOW
// (which would happen with modules that pull pins LOW)
delay(50); // Let pullups stabilize
btn_fire.last_state = digitalRead(PIN_BTN_FIRE);
btn_fire.stable_state = btn_fire.last_state;
btn_auto.last_state = digitalRead(PIN_BTN_AUTO);
btn_auto.stable_state = btn_auto.last_state;
btn_mode.last_state = digitalRead(PIN_BTN_MODE);
btn_mode.stable_state = btn_mode.last_state;
btn_estop.last_state = digitalRead(PIN_BTN_ESTOP);
btn_estop.stable_state = btn_estop.last_state;
// Diagnostic: show button states at boot
// All should read HIGH (1) if INPUT_PULLUP works correctly
Serial.print(F("Button states at boot: "));
Serial.print(F("FIRE=")); Serial.print(btn_fire.last_state);
Serial.print(F(" AUTO=")); Serial.print(btn_auto.last_state);
Serial.print(F(" MODE=")); Serial.print(btn_mode.last_state);
Serial.print(F(" ESTOP=")); Serial.println(btn_estop.last_state);
Serial.println(F("(All should be 1 if not pressed)"));
Serial.println(F("If 0 = wiring/module issue or stuck button"));
// Initialize display
display_init();
update_pots();
Serial.println(F("Ready."));
wdt_enable(WATCHDOG_TIMEOUT);
}
// ============================================================================
// TIMER1 PWM SETUP
// ============================================================================
void setup_timer1_pwm() {
TCCR1A = (1 << COM1A1) | (1 << WGM11);
TCCR1B = (1 << WGM13) | (1 << WGM12) | (1 << CS10);
ICR1 = PWM_TOP;
OCR1A = 0;
}
inline void set_buck_duty(uint16_t duty) {
if (duty > MAX_DUTY) duty = MAX_DUTY;
OCR1A = duty;
}
// ============================================================================
// DISPLAY INIT
// ============================================================================
void display_init() {
tft.begin();
tft.setRotation(1); // Landscape 320x240
tft.fillScreen(COL_BG);
// Splash screen
tft.setTextColor(ILI9341_CYAN);
tft.setTextSize(3);
tft.setCursor(60, 80);
tft.print(F("QCW DRSSTC"));
tft.setTextSize(2);
tft.setCursor(110, 130);
tft.print(F("v4.0"));
tft.setTextSize(1);
tft.setTextColor(ILI9341_DARKGREY);
tft.setCursor(85, 170);
tft.print(F("Initializing..."));
delay(1000);
draw_layout();
}
// ============================================================================
// LAYOUT (static elements drawn once)
// ============================================================================
void draw_layout() {
tft.fillScreen(COL_BG);
// Header bar
tft.fillRect(0, 0, SCR_W, HEADER_H, COL_HEADER);
tft.setTextSize(2);
tft.setTextColor(COL_TEXT, COL_HEADER);
tft.setCursor(4, 3);
tft.print(F("QCW"));
// Ramp area border
tft.drawRect(0, RAMP_Y - 1, SCR_W, RAMP_H + 2, COL_RAMP_GRID);
tft.fillRect(1, RAMP_Y, SCR_W - 2, RAMP_H, COL_RAMP_BG);
// Draw grid
for (int i = 1; i < 4; i++) {
int y = RAMP_Y + (RAMP_H * i) / 4;
for (int x = 4; x < SCR_W - 4; x += 8) {
tft.drawPixel(x, y, COL_RAMP_GRID);
}
}
for (int i = 1; i < 4; i++) {
int x = (SCR_W * i) / 4;
for (int y = RAMP_Y + 4; y < RAMP_Y + RAMP_H - 4; y += 8) {
tft.drawPixel(x, y, COL_RAMP_GRID);
}
}
// Ramp labels
tft.setTextSize(1);
tft.setTextColor(COL_RAMP, COL_RAMP_BG);
tft.setCursor(4, RAMP_Y + 2);
tft.print(F("Duty"));
tft.setTextColor(COL_RAMP_GRID, COL_RAMP_BG);
tft.setCursor(SCR_W - 26, RAMP_Y + RAMP_H - 9);
tft.print(F("time"));
drawn.valid = false;
}
// ============================================================================
// HEADER (ARM / AUTO / MODE indicators)
// ============================================================================
void draw_header() {
// Clear right side of header
tft.fillRect(60, 0, SCR_W - 60, HEADER_H, COL_HEADER);
tft.setTextSize(2);
// ARM indicator
tft.setTextColor(fire_armed ? COL_ARMED : COL_DISARM, COL_HEADER);
tft.setCursor(70, 3);
tft.print(fire_armed ? F("ARM") : F("OFF"));
// AUTO/MANUAL
if (fire_armed) {
tft.setTextColor(auto_mode ? COL_AUTO : COL_MANUAL, COL_HEADER);
tft.setCursor(125, 3);
tft.print(auto_mode ? F("AUT") : F("MAN"));
}
// Op mode
tft.setTextColor(COL_TEXT, COL_HEADER);
tft.setTextSize(1);
tft.setCursor(190, 6);
// Print mode name from PROGMEM
char mode_buf[10];
strcpy_P(mode_buf, (PGM_P)pgm_read_word(&MODE_NAMES[current_mode]));
tft.print(mode_buf);
// E-STOP indicator
if (estop_triggered) {
tft.setTextSize(2);
tft.setTextColor(ILI9341_RED, COL_HEADER);
tft.setCursor(260, 3);
tft.print(F("STOP"));
}
}
// ============================================================================
// PARAMETERS (8 pots + alpha)
// ============================================================================
void draw_param_value(int x, int y, const __FlashStringHelper* label,
const char* value) {
tft.setTextSize(1);
tft.setTextColor(COL_LABEL, COL_BG);
tft.setCursor(x, y);
tft.print(label);
tft.setTextColor(COL_VAL, COL_BG);
tft.setCursor(x + 36, y);
// Clear the value area first to avoid leftover digits
tft.fillRect(x + 36, y, 60, 8, COL_BG);
tft.setCursor(x + 36, y);
tft.print(value);
}
void draw_parameters() {
char buf[12];
int col1 = 4;
int col2 = 110;
int col3 = 216;
int row_h = 14; // reduced from 16 (params area compacted)
int y = PARAMS_Y + 1;
// Row 1: PW, BPS, AMPL
snprintf(buf, sizeof(buf), "%dms", pulse_width_ms);
draw_param_value(col1, y, F("PW:"), buf);
snprintf(buf, sizeof(buf), "%d.%dHz", bps_x10 / 10, bps_x10 % 10);
draw_param_value(col2, y, F("BPS:"), buf);
uint8_t ampl_pct = (peak_duty * 100UL) / PWM_TOP;
snprintf(buf, sizeof(buf), "%d%%", ampl_pct);
draw_param_value(col3, y, F("AMPL:"), buf);
// Row 2: WICK, AMPSIN, FSIN
y += row_h;
uint8_t wick_pct = (wick_offset * 100UL) / PWM_TOP;
snprintf(buf, sizeof(buf), "%d%%", wick_pct);
draw_param_value(col1, y, F("WICK:"), buf);
uint8_t ampsin_pct = (ampsin * 100UL) / AMPSIN_MAX;
snprintf(buf, sizeof(buf), "%d%%", ampsin_pct);
draw_param_value(col2, y, F("ASIN:"), buf);
snprintf(buf, sizeof(buf), "%dHz", fsin_hz);
draw_param_value(col3, y, F("FSIN:"), buf);
// Row 3: TSCALE, USCALE, ALPHA
y += row_h;
snprintf(buf, sizeof(buf), "%d.%02dx", tscale_x100 / 100, tscale_x100 % 100);
draw_param_value(col1, y, F("CURV:"), buf);
snprintf(buf, sizeof(buf), "%d%%", uscale_x100);
draw_param_value(col2, y, F("CPOS:"), buf);
snprintf(buf, sizeof(buf), "%d", alpha_x1000);
draw_param_value(col3, y, F("ALPH:"), buf);
}
// ============================================================================
// RAMP SHAPE VISUALIZATION
// ============================================================================
// Calculate duty value for display purposes (mirrors fire_pulse logic)
int calc_duty_for_display(int t_norm, bool with_compensation) {
const int32_t RISE_END_X10000 = 10000 - ((int32_t)FALL_FRACTION_X100 * 100);
int peak_duty_eff = (int)peak_duty;
int wick_off = wick_offset;
int ramp_range = peak_duty_eff - wick_off;
if (ramp_range < 0) ramp_range = 0;
int duty = 0;
if ((int32_t)t_norm < RISE_END_X10000) {
// Rise phase: chord + curve deviation
int32_t rise_t = ((int32_t)t_norm * 10000L) / RISE_END_X10000;
int32_t chord = wick_off + (rise_t * ramp_range) / 10000;
int32_t deviation = calc_curve_deviation(rise_t, ramp_range);
int32_t duty_target = chord + deviation;
duty = duty_target;
if (with_compensation) {
int32_t comp_step1 = (duty_target * rise_t) / 10000L;
int32_t duty_comp = (comp_step1 * (int32_t)alpha_x1000) / 1000L;
duty += duty_comp;
}
} else {
// Fall phase: linear decay from peak to 0
int32_t fall_position = (int32_t)t_norm - RISE_END_X10000;
int32_t fall_total = 10000L - RISE_END_X10000;
int32_t fall_t = (fall_position * 10000L) / fall_total;
int32_t peak_total = peak_duty_eff;
if (with_compensation) {
int32_t comp_at_peak = (peak_duty_eff * (int32_t)alpha_x1000) / 1000L;
peak_total += comp_at_peak;
if (peak_total > MAX_DUTY) peak_total = MAX_DUTY;
}
duty = peak_total - (peak_total * fall_t) / 10000L;
}
// Sine modulation с envelope (matches calculate_duty behavior)
if (ampsin > 0 && pulse_width_ms > 0
&& (int32_t)t_norm < RISE_END_X10000) {
uint32_t cycles_x100 = ((uint32_t)fsin_hz * pulse_width_ms) / 10;
uint32_t phase_idx = ((uint32_t)t_norm * cycles_x100) / 100 * 256 / 10000;
uint8_t sine_idx = phase_idx & 0xFF;
int8_t sine_val = pgm_read_byte(&sine_table[sine_idx]);
uint8_t env_idx = ((uint32_t)t_norm * 128UL) / 10000;
if (env_idx > 127) env_idx = 127;
int8_t env_val = (int8_t)pgm_read_byte(&sine_table[env_idx]);
int sine_offset = ((int32_t)sine_val * (int32_t)ampsin * env_val) / (127L * 127L);
duty += sine_offset;
}
if (duty < 0) duty = 0;
if (duty > MAX_DUTY) duty = MAX_DUTY;
return duty;
}
void draw_ramp_shape() {
// Clear ramp area
tft.fillRect(1, RAMP_Y, SCR_W - 2, RAMP_H, COL_RAMP_BG);
// Redraw grid
for (int i = 1; i < 4; i++) {
int y = RAMP_Y + (RAMP_H * i) / 4;
for (int x = 4; x < SCR_W - 4; x += 8) {
tft.drawPixel(x, y, COL_RAMP_GRID);
}
}
for (int i = 1; i < 4; i++) {
int x = (SCR_W * i) / 4;
for (int y = RAMP_Y + 4; y < RAMP_Y + RAMP_H - 4; y += 8) {
tft.drawPixel(x, y, COL_RAMP_GRID);
}
}
// Labels
tft.setTextSize(1);
tft.setTextColor(COL_RAMP, COL_RAMP_BG);
tft.setCursor(4, RAMP_Y + 2);
tft.print(F("Duty"));
tft.setTextColor(COL_RAMP_GRID, COL_RAMP_BG);
tft.setCursor(SCR_W - 26, RAMP_Y + RAMP_H - 9);
tft.print(F("time"));
// Draw both curves
// Step every 4 pixels to speed up (display is slow Nano SPI)
int prev_y_lin = RAMP_Y + RAMP_H - 1;
int prev_y_comp = RAMP_Y + RAMP_H - 1;
int prev_x = 1;
int step = 4; // Skip pixels for faster drawing
// In CW mode show 3 sawtooth cycles across display width
// In other modes show single pulse shape с PW horizontal scaling
bool is_cw_mode = (current_mode == MODE_CW_QCW);
int cycles_shown = is_cw_mode ? 3 : 1;
// PW horizontal scaling: pulse occupies portion of width proportional к PW
// PW = PW_REFERENCE_MS (25ms) -> full width
// PW = 5ms -> 20% width
int pulse_width_pixels = is_cw_mode ? (SCR_W - 2) :
(((int32_t)pulse_width_ms * (SCR_W - 2)) / PW_REFERENCE_MS);
if (pulse_width_pixels < 4) pulse_width_pixels = 4;
if (pulse_width_pixels > (SCR_W - 2)) pulse_width_pixels = SCR_W - 2;
for (int x = 1; x < SCR_W - 1; x += step) {
int t_norm;
bool in_pulse;
if (is_cw_mode) {
int t_pixel_norm = ((x - 1) * 10000L) / (SCR_W - 2);
t_norm = (t_pixel_norm * cycles_shown) % 10000;
in_pulse = true;
} else {
// Normal mode: pulse occupies only pulse_width_pixels pixels
int pixel_in_pulse = x - 1;
if (pixel_in_pulse < pulse_width_pixels) {
t_norm = ((int32_t)pixel_in_pulse * 10000L) / pulse_width_pixels;
in_pulse = true;
} else {
t_norm = 0;
in_pulse = false; // past pulse end - flat at 0
}
}
int duty_lin, duty_comp;
if (!in_pulse) {
duty_lin = 0;
duty_comp = 0;
} else if (is_cw_mode) {
// For CW: use full ramp (no fall phase)
// Compute как rise phase only с 0..10000 mapping
int peak_eff = (int)peak_duty;
int ramp_range = peak_eff - (int)wick_offset;
if (ramp_range < 0) ramp_range = 0;
int chord = wick_offset + ((int32_t)t_norm * ramp_range) / 10000;
int dev = calc_curve_deviation(t_norm, ramp_range);
int duty_target = chord + dev;
// Sine modulation с envelope для display
if (ampsin > 0) {
uint32_t cycles_x100 = ((uint32_t)fsin_hz * pulse_width_ms) / 10;
uint32_t phase_idx = ((uint32_t)t_norm * cycles_x100) / 100 * 256 / 10000;
uint8_t sine_idx = phase_idx & 0xFF;
int8_t sine_val = (int8_t)pgm_read_byte(&sine_table[sine_idx]);
uint8_t env_idx = ((uint32_t)t_norm * 128UL) / 10000;
if (env_idx > 127) env_idx = 127;
int8_t env_val = (int8_t)pgm_read_byte(&sine_table[env_idx]);
int sine_offset = ((int32_t)sine_val * (int32_t)ampsin * env_val) / (127L * 127L);
duty_target += sine_offset;
}
duty_lin = duty_target;
if (duty_lin < 0) duty_lin = 0;
if (duty_lin > MAX_DUTY) duty_lin = MAX_DUTY;
// With compensation
int32_t comp_step1 = ((int32_t)duty_target * t_norm) / 10000L;
int32_t comp = (comp_step1 * (int32_t)alpha_x1000) / 1000L;
duty_comp = duty_target + comp;
if (duty_comp < 0) duty_comp = 0;
if (duty_comp > MAX_DUTY) duty_comp = MAX_DUTY;
} else {
// Normal mode - use standard display calc (with rise + fall)
duty_lin = calc_duty_for_display(t_norm, false);
duty_comp = calc_duty_for_display(t_norm, true);
}
int y_lin = RAMP_Y + RAMP_H - 1 - ((long)duty_lin * (RAMP_H - 4)) / MAX_DUTY;
int y_comp = RAMP_Y + RAMP_H - 1 - ((long)duty_comp * (RAMP_H - 4)) / MAX_DUTY;
// Don't draw line при wraparound (avoid visual artifact в CW mode)
if (x > 1) {
bool wrapped = false;
if (is_cw_mode) {
int prev_t = ((prev_x - 1) * 10000L) / (SCR_W - 2);
int prev_t_in_cycle = (prev_t * cycles_shown) % 10000;
if (t_norm < prev_t_in_cycle) wrapped = true;
}
if (!wrapped) {
tft.drawLine(prev_x, prev_y_lin, x, y_lin, COL_RAMP_LIN);
tft.drawLine(prev_x, prev_y_comp, x, y_comp, COL_RAMP);
}
}
prev_x = x;
prev_y_lin = y_lin;
prev_y_comp = y_comp;
}
}
// ============================================================================
// FOOTER (pulse count)
// ============================================================================
void draw_footer() {
tft.fillRect(0, FOOTER_Y, SCR_W, FOOTER_H, COL_BG);
tft.setTextSize(2);
tft.setTextColor(COL_TEXT, COL_BG);
tft.setCursor(4, FOOTER_Y + 2);
tft.print(F("Pulses:"));
tft.setTextColor(COL_PULSE, COL_BG);
tft.print(pulse_counter);
}
// ============================================================================
// DIFF-BASED REDRAW
// ============================================================================
bool params_changed() {
return (pulse_width_ms != drawn.pulse_width_ms) ||
(bps_x10 != drawn.bps_x10) ||
((peak_duty * 100UL) / PWM_TOP != drawn.ampl_pct) ||
((wick_offset * 100UL) / PWM_TOP != drawn.wick_pct) ||
((ampsin * 100UL) / AMPSIN_MAX != drawn.ampsin_pct) ||
(fsin_hz != drawn.fsin_hz) ||
(tscale_x100 != drawn.tscale_x100) ||
(uscale_x100 != drawn.uscale_x100) ||
(alpha_x1000 != drawn.alpha_x1000);
}
bool ramp_changed() {
return ((peak_duty * 100UL) / PWM_TOP != drawn.ampl_pct) ||
((wick_offset * 100UL) / PWM_TOP != drawn.wick_pct) ||
((ampsin * 100UL) / AMPSIN_MAX != drawn.ampsin_pct) ||
(fsin_hz != drawn.fsin_hz) ||
(uscale_x100 != drawn.uscale_x100) ||
(tscale_x100 != drawn.tscale_x100) || // curve pot affects ramp shape
(alpha_x1000 != drawn.alpha_x1000) ||
(current_mode != drawn.op_mode) ||
(pulse_width_ms != drawn.pulse_width_ms);
}
bool header_changed() {
return (fire_armed != drawn.armed) ||
(auto_mode != drawn.auto_mode) ||
(current_mode != drawn.op_mode);
}
bool footer_changed() {
return (pulse_counter != drawn.pulse_count);
}
void update_display() {
// Only redraw what changed (reduces flicker, saves SPI time)
bool need_header = header_changed() || !drawn.valid;
bool need_params = params_changed() || !drawn.valid;
bool need_ramp = ramp_changed() || !drawn.valid;
bool need_footer = footer_changed() || !drawn.valid;
if (need_header) draw_header();
if (need_params) draw_parameters();
if (need_ramp) draw_ramp_shape();
if (need_footer) draw_footer();
// Save drawn state
drawn.pulse_width_ms = pulse_width_ms;
drawn.bps_x10 = bps_x10;
drawn.ampl_pct = (peak_duty * 100UL) / PWM_TOP;
drawn.wick_pct = (wick_offset * 100UL) / PWM_TOP;
drawn.ampsin_pct = (ampsin * 100UL) / AMPSIN_MAX;
drawn.fsin_hz = fsin_hz;
drawn.tscale_x100 = tscale_x100;
drawn.uscale_x100 = uscale_x100;
drawn.alpha_x1000 = alpha_x1000;
drawn.armed = fire_armed;
drawn.auto_mode = auto_mode;
drawn.op_mode = current_mode;
drawn.pulse_count = pulse_counter;
drawn.valid = true;
}
// ============================================================================
// POT READING
// ============================================================================
uint16_t read_pot_oversampled(uint8_t pin) {
uint32_t sum = 0;
for (uint8_t i = 0; i < 4; i++) {
sum += analogRead(pin);
delayMicroseconds(100);
}
return sum / 4;
}
void update_pots() {
uint16_t pw_raw = read_pot_oversampled(PIN_POT_PW);
uint16_t bps_raw = read_pot_oversampled(PIN_POT_BPS);
uint16_t ampl_raw = read_pot_oversampled(PIN_POT_AMPL);
uint16_t wick_raw = read_pot_oversampled(PIN_POT_WICK);
uint16_t ampsin_raw = read_pot_oversampled(PIN_POT_AMPSIN);
uint16_t fsin_raw = read_pot_oversampled(PIN_POT_FSIN);
uint16_t tscale_raw = read_pot_oversampled(PIN_POT_TSCALE);
uint16_t uscale_raw = read_pot_oversampled(PIN_POT_USCALE);
pulse_width_ms = map(pw_raw, 0, 1023, PULSE_WIDTH_MIN_MS, PULSE_WIDTH_MAX_MS);
bps_x10 = map(bps_raw, 0, 1023, BPS_MIN_X10, BPS_MAX_X10);
peak_duty = map(ampl_raw, 0, 1023, 0, MAX_DUTY);
wick_offset = map(wick_raw, 0, 1023, 0, WICK_MAX_DUTY);
ampsin = map(ampsin_raw, 0, 1023, 0, AMPSIN_MAX);
fsin_hz = map(fsin_raw, 0, 1023, FSIN_MIN_HZ, FSIN_MAX_HZ);
tscale_x100 = map(tscale_raw, 0, 1023, TSCALE_MIN_X100, TSCALE_MAX_X100);
uscale_x100 = map(uscale_raw, 0, 1023, USCALE_MIN_X100, USCALE_MAX_X100);
}
// ============================================================================
// BUTTONS
// ============================================================================
bool check_button_press(uint8_t pin, ButtonState& state) {
bool current_state = digitalRead(pin);
uint32_t now = millis();
if (current_state != state.last_state) {
state.last_change_ms = now;
state.last_state = current_state;
}
bool press_detected = false;
if ((now - state.last_change_ms) >= DEBOUNCE_MS) {
if (current_state != state.stable_state) {
// Only detect FALLING edge (HIGH -> LOW transition) as press
// С stable_state goes HIGH first, then LOW = press
// С если stable_state was LOW from boot, no false trigger
bool was_high = state.stable_state;
state.stable_state = current_state;
if (was_high && current_state == LOW) {
press_detected = true;
}
}
}
return press_detected;
}
void handle_buttons() {
if (check_button_press(PIN_BTN_ESTOP, btn_estop)) {
emergency_stop();
estop_triggered = true;
fire_armed = false;
auto_mode = false;
manual_pulse_pending = false;
Serial.println(F("ESTOP"));
return;
}
if (estop_triggered) {
if (digitalRead(PIN_BTN_FIRE) == HIGH &&
digitalRead(PIN_BTN_AUTO) == HIGH &&
digitalRead(PIN_BTN_MODE) == HIGH &&
digitalRead(PIN_BTN_ESTOP) == HIGH) {
if (estop_clear_ms == 0) {
estop_clear_ms = millis();
}
if ((millis() - estop_clear_ms) >= 3000) {
estop_triggered = false;
estop_clear_ms = 0;
Serial.println(F("Cleared"));
}
} else {
estop_clear_ms = 0;
}
return;
}
if (check_button_press(PIN_BTN_FIRE, btn_fire)) {
if (!fire_armed) {
fire_armed = true;
Serial.println(F("ARMED"));
} else if (!auto_mode) {
manual_pulse_pending = true;
Serial.println(F("FIRE"));
} else {
fire_armed = false;
auto_mode = false;
emergency_stop();
Serial.println(F("DISARMED"));
}
}
if (check_button_press(PIN_BTN_AUTO, btn_auto)) {
if (fire_armed) {
auto_mode = !auto_mode;
if (!auto_mode) {
emergency_stop();
fire_armed = true;
}
}
}
if (check_button_press(PIN_BTN_MODE, btn_mode)) {
current_mode = (current_mode + 1) % MODE_COUNT;
}
}
// ============================================================================
// EMERGENCY STOP
// ============================================================================
void emergency_stop() {
OCR1A = 0;
digitalWrite(PIN_INTERRUPTER, LOW);
digitalWrite(PIN_PWM_RAMP, LOW);
cw_running = false; // Stop CW continuous mode
}
// ============================================================================
// CURVE TRANSFORMATION (piecewise linear with movable breakpoint)
// ============================================================================
//
// Two straight-line segments meeting at a single breakpoint.
//
// tscale_x100 = curve MAGNITUDE: breakpoint Y position relative к chord
// 0 = breakpoint MAX BELOW chord (steep ending, shallow start)
// 100 = breakpoint ON chord (pure linear, no break visible)
// 200 = breakpoint MAX ABOVE chord (steep start, shallow ending)
//
// uscale_x100 = breakpoint X POSITION (where в the rise breakpoint sits)
// 10 = breakpoint NEAR START (short first segment, long second)
// 50 = breakpoint MIDDLE (default)
// 90 = breakpoint NEAR END (long first segment, short second)
//
// Math:
// bp_x = uscale_x100 / 100 × rise_length (X position 0..1)
// bp_y_on_chord = wick + (peak-wick) × bp_x (Y if on chord)
// bp_y_shift = (tscale_x100 - 100) / 100 × ramp_range × 0.5
// ^ max ±50% of ramp range
// bp_y = bp_y_on_chord + bp_y_shift
//
// For t < bp_x: linear от (0, wick) к (bp_x, bp_y)
// For t > bp_x: linear от (bp_x, bp_y) к (rise_end, peak)
//
// Returns deviation from chord at given rise_t.
int32_t calc_curve_deviation(int32_t rise_t, int32_t ramp_range) {
if (tscale_x100 == 100) return 0; // Pure linear, no breakpoint shift
// Breakpoint X position (0..10000)
int32_t bp_x = ((int32_t)uscale_x100 * 10000L) / 100L;
if (bp_x < 100) bp_x = 100;
if (bp_x > 9900) bp_x = 9900;
// Breakpoint Y shift relative к chord (signed)
// tscale_x100 - 100 ranges -100..+100
// Max shift = ramp_range / 2 (so breakpoint can move ±50% of ramp range)
int32_t curve_factor = (int32_t)tscale_x100 - 100; // -100..+100
int32_t bp_y_shift = (ramp_range * curve_factor) / 200L; // ±ramp_range/2 max
// Where chord is at bp_x:
int32_t chord_at_bp = (bp_x * ramp_range) / 10000L;
// Actual breakpoint Y (relative к wick start):
int32_t bp_y = chord_at_bp + bp_y_shift;
// Compute piecewise linear value at rise_t (relative к wick)
int32_t actual_y;
if (rise_t < bp_x) {
// Segment 1: linear от (0, 0) к (bp_x, bp_y) [Y values relative к wick]
actual_y = (rise_t * bp_y) / bp_x;
} else {
// Segment 2: linear от (bp_x, bp_y) к (10000, ramp_range)
int32_t seg2_t = rise_t - bp_x;
int32_t seg2_total = 10000L - bp_x;
int32_t seg2_dy = ramp_range - bp_y;
actual_y = bp_y + (seg2_t * seg2_dy) / seg2_total;
}
// Chord value at rise_t (relative к wick):
int32_t chord_y = (rise_t * ramp_range) / 10000L;
// Deviation = actual - chord (what caller adds к chord)
return actual_y - chord_y;
}
// ============================================================================
// DUTY CALCULATION (during pulse)
// ============================================================================
int32_t calculate_duty(uint32_t t_elapsed_us, uint32_t pulse_us, uint8_t mode) {
if (t_elapsed_us >= pulse_us) return 0;
uint32_t t_norm = (t_elapsed_us * 10000UL) / pulse_us;
int32_t duty = 0;
switch (mode) {
case MODE_QCW_STANDARD:
case MODE_QCW_SINE_MOD: {
// Split pulse into rise phase (RISE_FRACTION) and fall phase (rest)
// FALL_FRACTION_X100 = 12 means 12% fall, 88% rise
//
// Rise: t_norm 0 to RISE_END_X10000 (= 10000 - FALL_FRACTION_X100*100)
// Fall: t_norm RISE_END_X10000 to 10000
const int32_t RISE_END_X10000 = 10000 - ((int32_t)FALL_FRACTION_X100 * 100);
// For FALL=12%: RISE_END = 8800 (rise 0-8800, fall 8800-10000)
int32_t effective_peak = (int32_t)peak_duty; // uscale теперь curve POSITION, не limit
int32_t ramp_range = effective_peak - (int32_t)wick_offset;
if (ramp_range < 0) ramp_range = 0;
if ((int32_t)t_norm < RISE_END_X10000) {
// RISE PHASE: chord (linear от wick к peak) + curve deviation
// Map t_norm 0..RISE_END to rise_t 0..10000
int32_t rise_t = ((int32_t)t_norm * 10000L) / RISE_END_X10000;
// CHORD: linear baseline от wick_offset к effective_peak
int32_t chord = (int32_t)wick_offset +
(rise_t * ramp_range) / 10000;
// CURVE DEVIATION: sinusoidal bulge/sag around chord
// Preserves endpoints (deviation = 0 при rise_t=0 и rise_t=10000)
int32_t deviation = calc_curve_deviation(rise_t, ramp_range);
int32_t duty_target = chord + deviation;
// Bus drop compensation (V_bus падает linearly с real time)
int32_t comp_step1 = (duty_target * rise_t) / 10000L;
int32_t duty_comp = (comp_step1 * (int32_t)alpha_x1000) / 1000L;
duty = duty_target + duty_comp;
} else {
// FALL PHASE: linear ramp down from peak to 0
int32_t fall_position = (int32_t)t_norm - RISE_END_X10000;
int32_t fall_total = 10000L - RISE_END_X10000;
int32_t fall_t = (fall_position * 10000L) / fall_total;
int32_t peak_at_end_rise = effective_peak;
int32_t comp_at_peak = (peak_at_end_rise * (int32_t)alpha_x1000) / 1000L;
int32_t peak_total = peak_at_end_rise + comp_at_peak;
if (peak_total > MAX_DUTY) peak_total = MAX_DUTY;
duty = peak_total - (peak_total * fall_t) / 10000L;
}
// Sine modulation: active whenever ampsin > 0
// Envelope smoothing: fade in start, fade out end (reduces sharp transitions)
if (ampsin > 0) {
uint32_t sine_phase = (t_elapsed_us * (uint32_t)fsin_hz) / 3906UL;
uint8_t sine_idx = sine_phase & 0xFF;
int8_t sine_val = (int8_t)pgm_read_byte(&sine_table[sine_idx]);
// Envelope: smooth fade in/out (using same sine_table к get bell shape)
// t_norm 0..10000 -> envelope_idx 0..127 (half-period 0..π)
uint8_t env_idx = (t_norm * 128UL) / 10000;
if (env_idx > 127) env_idx = 127;
int8_t env_val = (int8_t)pgm_read_byte(&sine_table[env_idx]);
// env_val: 0..127..0 across pulse (bell shape)
int32_t sine_offset = ((int32_t)sine_val * (int32_t)ampsin * env_val) / (127L * 127L);
duty += sine_offset;
}
break;
}
case MODE_PHASE1_INTERRUPT:
case MODE_PHASE1_5_PDM: {
duty = (int32_t)peak_duty;
break;
}
}
if (duty < 0) duty = 0;
if (duty > MAX_DUTY) duty = MAX_DUTY;
return duty;
}
// ============================================================================
// CW QCW MODE — continuous sawtooth ramps
// ============================================================================
//
// В CW mode interrupter держится HIGH continuously, buck PWM генерирует
// continuous sawtooth: rise -> wrap -> rise (НЕТ fall к 0 как в normal QCW).
//
// Уникально для CW mode:
// - pulse_width_ms становится period of sawtooth (5-25ms)
// - interrupter НЕ toggles между ramps
// - residual primary current + hot ionized channel позволяют shorter ramps
// - effective BPS = 1000 / pulse_width_ms (e.g. 67 Hz при 15ms)
//
// Caller отвечает за HIGH interrupter и timing.
int32_t calculate_cw_duty(uint32_t elapsed_in_ramp_us, uint32_t ramp_us) {
if (ramp_us == 0) return 0;
if (elapsed_in_ramp_us >= ramp_us) {
elapsed_in_ramp_us = elapsed_in_ramp_us % ramp_us;
}
// Normalize: t_norm = 0..10000 across full ramp
uint32_t t_norm = (elapsed_in_ramp_us * 10000UL) / ramp_us;
int32_t effective_peak = (int32_t)peak_duty;
int32_t ramp_range = effective_peak - (int32_t)wick_offset;
if (ramp_range < 0) ramp_range = 0;
// No fall phase - rise spans entire ramp period
// Chord + curve deviation
int32_t rise_t = (int32_t)t_norm;
int32_t chord = (int32_t)wick_offset + (rise_t * ramp_range) / 10000;
int32_t deviation = calc_curve_deviation(rise_t, ramp_range);
int32_t duty_target = chord + deviation;
// Bus drop compensation
int32_t comp_step1 = (duty_target * rise_t) / 10000L;
int32_t duty_comp = (comp_step1 * (int32_t)alpha_x1000) / 1000L;
int32_t duty = duty_target + duty_comp;
// Sine modulation
// Sine modulation с envelope (matches calculate_duty behavior)
if (ampsin > 0) {
uint32_t sine_phase = (elapsed_in_ramp_us * (uint32_t)fsin_hz) / 3906UL;
uint8_t sine_idx = sine_phase & 0xFF;
int8_t sine_val = (int8_t)pgm_read_byte(&sine_table[sine_idx]);
uint8_t env_idx = (t_norm * 128UL) / 10000;
if (env_idx > 127) env_idx = 127;
int8_t env_val = (int8_t)pgm_read_byte(&sine_table[env_idx]);
int32_t sine_offset = ((int32_t)sine_val * (int32_t)ampsin * env_val) / (127L * 127L);
duty += sine_offset;
}
if (duty < 0) duty = 0;
if (duty > MAX_DUTY) duty = MAX_DUTY;
return duty;
}
void start_cw_mode() {
if (cw_running) return;
cw_running = true;
cw_start_us = micros();
last_ramp_position = 0;
digitalWrite(PIN_INTERRUPTER, HIGH);
}
void stop_cw_mode() {
if (!cw_running) return;
cw_running = false;
set_buck_duty(0);
digitalWrite(PIN_INTERRUPTER, LOW);
}
// Called every main loop iteration when в CW mode + armed
// Updates buck PWM continuously based on current position in ramp cycle
void handle_cw_mode() {
uint32_t ramp_us = (uint32_t)pulse_width_ms * 1000UL;
if (ramp_us < 1000) ramp_us = 1000; // Safety minimum 1ms
uint32_t now_us = micros();
uint32_t elapsed_total = now_us - cw_start_us;
uint32_t elapsed_in_ramp = elapsed_total % ramp_us;
// Detect ramp wraparound (counts complete sawtooth cycles)
if (elapsed_in_ramp < last_ramp_position) {
pulse_counter++;
}
last_ramp_position = elapsed_in_ramp;
int32_t duty = calculate_cw_duty(elapsed_in_ramp, ramp_us);
set_buck_duty((uint16_t)duty);
}
// ============================================================================
// FIRE PULSE (NO display updates here - timing critical!)
// ============================================================================
void fire_pulse() {
digitalWrite(PIN_INTERRUPTER, HIGH);
uint32_t base_pulse_us;
switch (current_mode) {
case MODE_PHASE1_INTERRUPT:
base_pulse_us = 200;
break;
case MODE_PHASE1_5_PDM:
base_pulse_us = (uint32_t)pulse_width_ms * 200UL;
break;
default:
base_pulse_us = (uint32_t)pulse_width_ms * 1000UL;
break;
}
// pulse duration determined by PW pot only
// (tscale_x100 теперь controls ramp curve shape, не time)
uint32_t pulse_us = base_pulse_us;
uint32_t start_us = micros();
uint32_t elapsed_us = 0;
while (elapsed_us < pulse_us) {
elapsed_us = micros() - start_us;
int32_t duty = calculate_duty(elapsed_us, pulse_us, current_mode);
set_buck_duty((uint16_t)duty);
wdt_reset();
if (digitalRead(PIN_BTN_ESTOP) == LOW) {
break;
}
}
set_buck_duty(0);
digitalWrite(PIN_INTERRUPTER, LOW);
pulse_counter++;
}
// ============================================================================
// MAIN LOOP
// ============================================================================
void loop() {
wdt_reset();
uint32_t now_ms = millis();
// Button handling (always fast, never during pulse)
handle_buttons();
// Pot reading periodic
if ((now_ms - last_pot_update_ms) >= 50) {
update_pots();
last_pot_update_ms = now_ms;
}
// Display update (only between pulses)
// Check that pulse not happening now
bool pulse_in_progress = (OCR1A != 0);
if (!pulse_in_progress && (now_ms - last_display_ms) >= DISPLAY_UPDATE_MS) {
update_display();
last_display_ms = millis(); // Re-read after slow SPI draws
}
// Stop CW mode если: disarmed, estop, или сменился mode
if (cw_running &&
(!fire_armed || estop_triggered || current_mode != MODE_CW_QCW)) {
stop_cw_mode();
}
// Fire logic
if (!fire_armed || estop_triggered) {
if (OCR1A != 0) emergency_stop();
return;
}
// CW QCW mode: continuous sawtooth ramps (works only в AUTO mode)
if (current_mode == MODE_CW_QCW) {
if (auto_mode) {
if (!cw_running) {
start_cw_mode();
}
handle_cw_mode();
} else {
// MANUAL не имеет смысла для CW - stop if running
if (cw_running) stop_cw_mode();
}
return;
}
if (!auto_mode) {
if (manual_pulse_pending) {
manual_pulse_pending = false;
fire_pulse();
}
return;
}
uint32_t cycle_ms = 10000UL / bps_x10;
if ((now_ms - last_pulse_ms) >= cycle_ms) {
fire_pulse();
last_pulse_ms = now_ms;
}
}