From 9fed882f5027a8cf92dbfd4122d31acf3553b51d Mon Sep 17 00:00:00 2001 From: kuwoyuki Date: Thu, 4 Dec 2025 02:18:47 +0600 Subject: [PATCH] feat: autohold, persistent config (ext flash), a little cleanup? maybe --- .vscode/settings.json | 4 +- README.md | 147 ++-- funconfig.h | 5 + inc/config.h | 154 ++++ inc/gpib_defs.h | 3 +- inc/systick.h | 4 - inc/usb_config.h | 4 +- main.c | 1754 ++++++++++++++++++++++++++--------------- systick.c | 21 - 9 files changed, 1376 insertions(+), 720 deletions(-) create mode 100644 inc/config.h delete mode 100644 systick.c diff --git a/.vscode/settings.json b/.vscode/settings.json index 59967d8..a9557bf 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -112,7 +112,9 @@ "math.h": "c", "cctype": "c", "stdlib.h": "c", - "float.h": "c" + "float.h": "c", + "stdbool.h": "c", + "initializer_list": "c" }, "cmake.sourceDirectory": "/home/mira/src/embedded/ch32v208_sens/lwip" } \ No newline at end of file diff --git a/README.md b/README.md index b4a96db..847977a 100644 --- a/README.md +++ b/README.md @@ -22,73 +22,112 @@ Extended features (Continuity, Temp, etc.) can be accessed using the SRQ button 4. **Sub-Menus:** Some modes (like Temp) have sub-menus for sensor type and 2W/4W mode. Same cycle logic to select these. 5. **Exit:** Press the SRQ button to exit back to initial mode (one we entered menu from). -## Extended Feature Modes +## Features + +These can be triggered via the Menu or the serial commands below. -These can be triggered via the Menu or the serial commands below. Although sub-menu features probably don't work through GPIB commands. Relative and Statistics depend on the mode you enter the menu from, i.e. if you enter relative from DCV it saves your state and you get relative voltage, from 2W relative 2W etc. -| Feature | Command | Description | -| :-------------- | :-------- | :------------------------------------------------------------------------------------------------------------------------------------------------------------ | -| **Continuity** | `++cont` | Fast, latched continuity beeper (2W Ohm, R0: 30 Ohm Range, N3: 3.5 Digits) | -| **Temperature** | `++temp` | Supports **PT1000**, **Type K** (via shit CJC... because the AHT20 is near the transformer :)), and **Thermistors** (Generic 10k/50k/100k & YSI 44000 series) | -| **Relative** | `++rel` | Set NULL/0 offset | -| **dBm** | `++dbm` | Power measurement calculated against 50Ω reference | -| **Diode** | `++diode` | Diode test with 2.5V open circuit voltage and audible latch | -| **Ext Ohms** | `++xohm` | Measures high resistance (>30MΩ) by calculating against internal 10MΩ parallel resistance | -| **Statistics** | `++math` | Cycles display between Live / Min / Max / Avg | -| **Environment** | `++env` | Reads internal AHT20 sensor (Temp/Humidity) | -| **Display** | `++disp` | write custom string to LCD | -| **Normal** | `++norm` | Resets DMM to standard DC Volts state | +``` +++help -## USB-GPIB Command Reference +HP3478A Internal USB-GPIB 1.2.0 -Commands start with `++`. Any other text is sent directly to the GPIB bus. +[GPIB Setup] + ++addr <0-30> Target Address + ++auto <0-2> 0:Off, 1:Read-After-Write, 2:Query-Only + ++read_tmo_ms Timeout in ms + ++eoi <0|1> Assert hardware EOI on write end + ++eos <0-3> Write Term: 0:CRLF, 1:CR, 2:LF, 3:None + ++eor <0-7> Read Stop: 0:CRLF ... 7:EOI-Only + ++eot_enable Append extra char to read output + ++eot_char The char to append -### Controller Configuration +[System Configuration] + ++config List all configurable parameters + ++get Get parameter value + ++set Set parameter value + ++savecfg Save config to flash + ++ver Firmware Version + ++rst System Reset -| Command | Arguments | Description | -| :--------------- | :-------- | :---------------------------------------------------- | -| `++ver` | | Get Firmware Version | -| `++help` / `++?` | | List available commands | -| `++rst` | | Reset the controller | -| `++addr` | `0-30` | Set target GPIB address (Default: 18) | -| `++tmo` | `ms` | Set USB-to-GPIB read timeout (alias: `++read_tmo_ms`) | +[GPIB Bus Operations] + ++read Read from target + ++write Write to target + ++trg Device Trigger (GET) + ++clr Device Clear (SDC) + ++dcl Universal Device Clear (DCL) + ++ifc Interface Clear (Bus Reset) + ++spoll [addr] Serial Poll + ++srq Query SRQ Line (0=High/Idle, 1=Low/Active) + ++loc Local (Drop REN) + ++llo Local Lockout -there's also `++savecfg` and `++mode` to fake prologix compliance +[Internal HP3478A Features] + ++cont, ++hold, ++rel, ++xohm + ++dbm, ++diode, ++math, ++norm + ++temp Temperature sensor mode + ++env [temp|hum] Internal AHT20 Sensor + ++disp Write text to LCD +``` -### Protocol & Formatting +There's also a configuration which can be saved using `++savecfg` if you want to make it persist (it's saved to v203's "undocumented" flash). +Also see [inc/config.h](./inc/config.h). -| Command | Arguments | Description | -| :------------- | :-------- | :------------------------------------------------------------------- | -| `++auto` | `0-2` | **0**: Off, **1**: Read-after-Write, **2**: Query-Only (ends in `?`) | -| `++eoi` | `0`\|`1` | Assert hardware EOI line on write end | -| `++eos` | `0-3` | Append to Write: 0:CRLF, 1:CR, 2:LF, 3:None | -| `++eor` | `0-7` | Stop Read on: 0:CRLF ... 7:EOI-Only | -| `++eot_enable` | `0`\|`1` | Append character to USB output stream? | -| `++eot_char` | `dec` | The decimal char to append (e.g., 10 for `\n`) | +``` +++config +my_addr: 0 +dmm_addr: 18 +target_addr: 18 +eot_char: 0 +eot_enable: 0 +eoi_assert: 1 +eos_mode: 0 +eor_mode: 0 +auto_read: 0 +gpib_timeout_ms: 1200 +poll_interval_ms: 100 +env_sensor_read_interval_ms: 1000 +dmm_recovery_delay_ms: 1000 +usb_debounce_connect_ms: 50 +usb_debounce_disconnect_ms: 200 +usb_timeout_target_ms: 5 +menu_dot_interval_ms: 500 +menu_commit_delay_ms: 2400 +menu_sublayer_delay_ms: 500 +menu_debounce_ms: 100 +menu_lockout_ms: 1000 +stats_cycle_time_ms: 3000 +cont_disp_update_ms: 150 +diode_stable_ms: 20 +buzzer_chirp_hz: 3000 +buzzer_chirp_ms: 75 +buzzer_cont_hz: 2500 +rtd_a: 0.003908 +rtd_b: -0.000001 +rtd_r0: 1000.000000 +cjc_fallback_temp: 22.000000 +cjc_self_heating_offset: 4.000000 +type_k_scale: 24390.240000 +dbm_ref_z: 50.000000 +diode_th_short: 0.050000 +diode_th_open: 2.500000 +autohold_threshold: 1.000000 +autohold_change_req: 2.000000 +autohold_min_val: 0.050000 +cont_threshold_ohms: 10.000000 +autohold_stable_count: 3 +rel_stable_count: 3 +``` -### Bus Management +i.e. -| Command | Description | -| :-------- | :------------------------------------------------ | -| `++read` | Read data from target immediately | -| `++write` | Write data to target manually | -| `++trg` | Issue Group Execute Trigger (GET) | -| `++clr` | Selected Device Clear (SDC) | -| `++dcl` | Universal Device Clear (resets entire bus) | -| `++spoll` | Serial Poll target (returns Status Byte) | -| `++srq` | Query SRQ Line status (0=Idle, 1=Active) | -| `++stat` | Print controller internal status (Addr, Tmo, etc) | +``` +++set dbm_ref_z 60 +++savecfg +``` -### Lower Level Control - -| Command | Description | -| :------ | :------------------------------------------ | -| `++loc` | Go To Local (Release Remote Lock) | -| `++gtl` | Send "Go To Local" GPIB message | -| `++llo` | Local Lockout (Disable front panel buttons) | -| `++ren` | Toggle Remote Enable line | -| `++ifc` | Interface Clear (Hard Bus Reset) | +Now your dBm feature will use 600 ohm ref instead of 50. ## Known Issues diff --git a/funconfig.h b/funconfig.h index 85b879e..36fee4b 100644 --- a/funconfig.h +++ b/funconfig.h @@ -5,5 +5,10 @@ // #define FUNCONF_SYSTEM_CORE_CLOCK 120000000 // #define FUNCONF_PLL_MULTIPLIER 15 #define FUNCONF_SYSTICK_USE_HCLK 1 +#define FUNCONF_USE_DEBUGPRINTF 0 +#define FUNCONF_USE_UARTPRINTF 0 +#define FUNCONF_USE_USBPRINTF 0 +#define FUNCONF_NULL_PRINTF 1 +// #define GPIB_DEBUG 1 #endif diff --git a/inc/config.h b/inc/config.h new file mode 100644 index 0000000..3f15b21 --- /dev/null +++ b/inc/config.h @@ -0,0 +1,154 @@ +#ifndef CONFIG_H +#define CONFIG_H + +#include +#include + +#define ERASE_PAGE_SIZE 64 +#define CONFIG_MAGIC 0x1234ABCD +#define CONFIG_VERSION 1 + +#define MY_ADDR 0 +#define DEFAULT_DMM_ADDR 18 // the HP3478A addr + +// Timing Config +#define USB_DEBOUNCE_CONNECT_MS 50 +#define USB_DEBOUNCE_DISCONNECT_MS 200 + +#define ENV_SENSOR_READ_INTERVAL_MS 1000 +#define DEFAULT_GPIB_TIMEOUT_MS 1200 + +// This kind of ass but.. yeah, I don't want to access systick there +// assume if the PC hasn't read after ~5ms it's stalled +#define USB_TIMEOUT_TARGET_MS 5 +#define CYCLES_PER_LOOP 50 +#define USB_TIMEOUT_LIMIT \ + ((FUNCONF_SYSTEM_CORE_CLOCK / 1000 * USB_TIMEOUT_TARGET_MS) / CYCLES_PER_LOOP) + +// Menu Animation Timings +#define MENU_DOT_INTERVAL_MS 500 // Speed of "..." addition +#define MENU_COMMIT_DELAY_MS 2400 // Time to hold before entering mode +#define MENU_SUBLAYER_DELAY_MS 500 // Time to "hover" before dots start +#define MENU_DEBOUNCE_MS 100 // Button press dead-time +#define MENU_LOCKOUT_MS 1000 // How long to ignore SRQ for after exiting menu + +// Polling +#define POLL_INTERVAL_MS 100 // 10Hz polling when in Passthrough +#define DMM_RECOVERY_DELAY_MS 1000 // Backoff if DMM vanishes + +// Diode sound +#define DIODE_TH_SHORT 0.050 // Volts (below this = SHORT) +#define DIODE_TH_OPEN 2.500 // Volts (above this = OPEN/OL) +#define DIODE_STABLE_MS 20 // wait X ms for voltage to settle +#define DIODE_CHIRP_MS 50 + +// PT1000 (DIN 43760 / IEC 751 Standard) +#define RTD_A 3.9083e-3 +#define RTD_B -5.775e-7 +#define RTD_R0 1000.0 + +// Thermocouple +#define CJC_FALLBACK_TEMP 22.0 // used if !app.env_sensor_present +// this is just cursed but.. yeah, the PCB is near a transformer +// ideally, the temp should be measured right at the binding posts.. +#define CJC_SELF_HEATING_OFFSET 4.0 +#define TYPE_K_SCALE 24390.24 // 1 / 41uV +// dBm +#define DBM_REF_Z 50.0 +// Stats +#define STATS_CYCLE_TIME_MS 3000 // time per screen (Live -> Avg -> Min...) +#define STATS_INIT_MIN_VAL 1.0e9 +#define STATS_INIT_MAX_VAL -1.0e9 + +// Autohold +#define AUTOHOLD_THRESHOLD 1.0 // deviation % allowed for stability check +#define AUTOHOLD_CHANGE_REQ 2.0 // % new value must differ by to trigger upd +// XXX: ideally, we should have different sample requirements for diff ranges +// we don't need as many counts at N5 range as at N3 +#define AUTOHOLD_STABLE_COUNT 3 // num samples required +#define AUTOHOLD_MIN_VAL 0.05 // noise floor + +#define CONT_DISP_UPDATE_MS 150 // display throttling +#define CONT_THRESHOLD_OHMS 10.0 // continuity beep threshold + +#define REL_STABLE_COUNT 3 // filter depth for Relative NULL + +#define BUZZER_CHIRP_HZ 3000 +#define BUZZER_CHIRP_MS 75 +#define BUZZER_CONT_HZ 2500 + +// #define LOCAL_COMMIT 1 + +typedef struct { + uint32_t magic; + uint32_t version; + + // addressing + uint8_t my_addr; // controller + uint8_t dmm_addr; // hp3478a + uint8_t target_addr; // target + + uint8_t eot_char; + uint8_t eot_enable; + uint8_t eoi_assert; + uint8_t eos_mode; + uint8_t eor_mode; + uint8_t auto_read; + uint8_t padding; // align + + // timings (ms) + uint32_t gpib_timeout_ms; + uint32_t poll_interval_ms; + uint32_t env_sensor_read_interval_ms; + uint32_t dmm_recovery_delay_ms; + uint32_t usb_debounce_connect_ms; + uint32_t usb_debounce_disconnect_ms; + uint32_t usb_timeout_target_ms; + + // timings + uint32_t menu_dot_interval_ms; + uint32_t menu_commit_delay_ms; + uint32_t menu_sublayer_delay_ms; + uint32_t menu_debounce_ms; + uint32_t menu_lockout_ms; + uint32_t stats_cycle_time_ms; + uint32_t cont_disp_update_ms; + uint32_t diode_stable_ms; + + // buzzer + uint32_t buzzer_chirp_hz; + uint32_t buzzer_chirp_ms; + uint32_t buzzer_cont_hz; + + // math & cal + double rtd_a; + double rtd_b; + double rtd_r0; + double cjc_fallback_temp; + double cjc_self_heating_offset; + double type_k_scale; + double dbm_ref_z; + double diode_th_short; + double diode_th_open; + + // thresholds + double autohold_threshold; + double autohold_change_req; + double autohold_min_val; + double cont_threshold_ohms; + + // logic counts + uint32_t autohold_stable_count; + uint32_t rel_stable_count; + +} fw_config_t; + +typedef enum { CFG_TYPE_UINT8, CFG_TYPE_UINT32, CFG_TYPE_DOUBLE } cfg_type_t; + +typedef struct { + const char* name; + size_t offset; + cfg_type_t type; +} cfg_field_t; + +#endif // CONFIG_H \ No newline at end of file diff --git a/inc/gpib_defs.h b/inc/gpib_defs.h index 291c4f7..7bada2f 100644 --- a/inc/gpib_defs.h +++ b/inc/gpib_defs.h @@ -3,8 +3,6 @@ #include "ch32fun.h" -// #define GPIB_DEBUG 1 - // Control Lines (Active LOW) #define PIN_EOI PB3 #define PIN_REN PB11 @@ -147,6 +145,7 @@ #define HP3478A_CMD_MASK_BTN_DATA "M21" // "M00" -> Clear Mask (Disable SRQ) #define HP3478A_CMD_MASK_CLEAR "M00" +#define HP3478A_CMD_MASK_BTN_SYNERR "M24" #define GPIB_ASSERT(pin) funDigitalWrite(pin, 0) #define GPIB_RELEASE(pin) funDigitalWrite(pin, 1) diff --git a/inc/systick.h b/inc/systick.h index eb1d9b8..4830974 100644 --- a/inc/systick.h +++ b/inc/systick.h @@ -9,11 +9,7 @@ #define SYSTICK_ONE_MILLISECOND ((uint32_t)FUNCONF_SYSTEM_CORE_CLOCK / 1000) #define SYSTICK_ONE_MICROSECOND ((uint32_t)FUNCONF_SYSTEM_CORE_CLOCK / 1000000) -extern volatile uint32_t systick_millis; - #define millis() (systick_millis) #define micros() (SysTick->CNT / SYSTICK_ONE_MICROSECOND) -void systick_init(void); - #endif // SYSTICK_H diff --git a/inc/usb_config.h b/inc/usb_config.h index 8dbbd8a..703869b 100644 --- a/inc/usb_config.h +++ b/inc/usb_config.h @@ -22,10 +22,10 @@ #define FUSB_USB_VID 0x1209 #define FUSB_USB_PID 0x3478 -#define FUSB_USB_REV 0x0110 +#define FUSB_USB_REV 0x0120 #define FUSB_STR_MANUFACTURER u"Open Source GPIB" #define FUSB_STR_PRODUCT u"HP3478A Internal Adapter" -#define FUSB_STR_SERIAL u"3478A-USB-110" +#define FUSB_STR_SERIAL u"3478A-USB-120" //Taken from http://www.usbmadesimple.co.uk/ums_ms_desc_dev.htm static const uint8_t device_descriptor[] = { diff --git a/main.c b/main.c index d07e9de..308ee25 100644 --- a/main.c +++ b/main.c @@ -9,102 +9,58 @@ * - Implement a VTable for features, to replace the massive switch * to make consistnet entry and exits * - Break main() up into app_process_usb() and app_process_gpib()? - * - SCPI-compliant command set for the passthrough mode * - Data logging? */ #include #include #include +#include +#include #include #include #include #include "aht20.h" #include "ch32fun.h" +#include "config.h" #include "fsusb.h" #include "gpib_defs.h" #include "i2c_bitbang.h" #include "systick.h" -#define FW_VERSION "1.1.0" - -#define MY_ADDR 0 -#define DEFAULT_DMM_ADDR 18 // the HP3478A addr +#define FW_VERSION "1.2.0" #define PIN_VBUS PB10 #define PIN_BUZZ PC13 +// USB #define USB_HW_IS_ACTIVE() (!((USBFSCTX.USBFS_DevSleepStatus) & 0x02)) - -// Timing Config -#define USB_DEBOUNCE_CONNECT_MS 50 -#define USB_DEBOUNCE_DISCONNECT_MS 200 -#define USB_SEND_TIMEOUT_MS 50 - -#define ENV_SENSOR_READ_INTERVAL_MS 1000 -#define DEFAULT_GPIB_TIMEOUT_MS 1200 - -// This kind of ass but.. yeah, I don't want to access systick there -// assume if the PC hasn't read after ~5ms it's stalled -#define USB_TIMEOUT_TARGET_MS 5 -#define CYCLES_PER_LOOP 50 -#define USB_TIMEOUT_LIMIT \ - ((FUNCONF_SYSTEM_CORE_CLOCK / 1000 * USB_TIMEOUT_TARGET_MS) / CYCLES_PER_LOOP) - -// Menu Animation Timings -#define MENU_DOT_INTERVAL_MS 500 // Speed of "..." addition -#define MENU_COMMIT_DELAY_MS 2400 // Time to hold before entering mode -#define MENU_SUBLAYER_DELAY_MS 500 // Time to "hover" before dots start -#define MENU_DEBOUNCE_MS 100 // Button press dead-time -#define MENU_LOCKOUT_MS 1000 // How long to ignore SRQ for after exiting menu - -// Polling -#define POLL_INTERVAL_MS 100 // 10Hz polling when in Passthrough -#define DMM_RECOVERY_DELAY_MS 2000 // Backoff if DMM vanishes - -// Diode sound -#define DIODE_TH_SHORT 0.050 // Volts (below this = SHORT) -#define DIODE_TH_OPEN 2.500 // Volts (above this = OPEN/OL) -#define DIODE_STABLE_MS 20 // wait X ms for voltage to settle -#define DIODE_CHIRP_MS 50 - -// PT1000 (DIN 43760 / IEC 751 Standard) -#define RTD_A 3.9083e-3 -#define RTD_B -5.775e-7 -#define RTD_R0 1000.0 - -// Thermocouple -#define CJC_FALLBACK_TEMP 22.0 // used if !app.env_sensor_present -// this is just cursed but.. yeah, the PCB is near a transformer -// ideally, the temp should be measured right at the binding posts.. -#define CJC_SELF_HEATING_OFFSET 4.0 -#define TYPE_K_SCALE 24390.24 // 1 / 41uV -// dBm -#define DBM_REF_Z 50.0 -// Stats -#define STATS_CYCLE_TIME_MS 3000 // time per screen (Live -> Avg -> Min...) -#define STATS_INIT_MIN_VAL 1.0e9 -#define STATS_INIT_MAX_VAL -1.0e9 +#define USB_RX_BUF_SIZE 1024 +#define USB_RX_MASK (USB_RX_BUF_SIZE - 1) +#define USB_TX_BUF_SIZE 2048 +#define USB_TX_MASK (USB_TX_BUF_SIZE - 1) // HP3478A #define HP_DISP_LEN 12 // 12 chars // max str length required to fill 12 segments. // Worst case: 12 chars + 12 dots/commas + 1 Null terminator = 25 bytes #define HP_DISP_BUF_SIZE ((HP_DISP_LEN * 2) + 1) -#define CONT_THRESHOLD_OHMS 10.0 // continuity beep threshold -#define CONT_DISP_UPDATE_MS 250 // display throttling -#define DMM_OL_THRESHOLD 9.0e9 // HP sends +9.9999E+9 on overload +#define DMM_OL_THRESHOLD 9.0e9 // HP sends +9.9999E+9 on overload #define DMM_OL_NEG_THRESHOLD -9.0e9 -#define REL_STABLE_SAMPLES 3 // filter depth for Relative NULL +#define CFG_ENTRY(member, dtype) {#member, offsetof(fw_config_t, member), dtype} + +// Command system typedef enum { CMD_UNKNOWN = 0, // Controller config - CMD_VER, // Get Firmware Version - CMD_HELP, // List commands - CMD_SAVECFG, // Save configuration to EEPROM (we just ACK to not break - // compat, cba w/ flash) + CMD_VER, // Get Firmware Version + CMD_HELP, // List commands + CMD_CFG_SET, + CMD_CFG_GET, + CMD_CFG_LIST, + CMD_SAVECFG, // Save configuration CMD_RST, // Reset the controller CMD_ADDR, // Set GPIB primary address CMD_MODE, // Set Controller vs Device mode (we're always the controller) @@ -137,16 +93,17 @@ typedef enum { CMD_IFC, // Interface Clear (Bus Reset) // HP3478A features - CMD_DISP, // Write on display - CMD_MATH, // MIN/MAX/AVG - CMD_ENV, // Temp/Hum sensor - CMD_CONT, // Continuity mode - CMD_TEMP, // Temperature mode - CMD_REL, // Relative mode - CMD_XOHM, // Extended Ohm range - CMD_DBM, // dBm - CMD_DIODE, // Diode test - CMD_NORM // Normal functionality + CMD_DISP, // Write on display + CMD_MATH, // MIN/MAX/AVG + CMD_ENV, // Temp/Hum sensor + CMD_CONT, // Continuity mode + CMD_TEMP, // Temperature mode + CMD_REL, // Relative mode + CMD_XOHM, // Extended Ohm range + CMD_DBM, // dBm + CMD_DIODE, // Diode test + CMD_AUTOHOLD, // Autohold (latch) reading + CMD_NORM // Normal functionality } cmd_id_t; @@ -155,57 +112,7 @@ typedef struct { cmd_id_t id; } cmd_entry_t; -static const cmd_entry_t COMMAND_TABLE[] = { - // Controller config - {"ver", CMD_VER}, - {"help", CMD_HELP}, - {"?", CMD_HELP}, - {"savecfg", CMD_SAVECFG}, - {"rst", CMD_RST}, - {"addr", CMD_ADDR}, // Set target GPIB address - {"mode", CMD_MODE}, // 0=Device, 1=Controller - {"tmo", CMD_TIMEOUT}, // Set read timeout - {"read_tmo_ms", CMD_TIMEOUT}, - - // Protocol/formatting - {"auto", CMD_AUTO}, // Auto-read data after write - {"eoi", CMD_EOI}, // Enable/Disable EOI assert - {"eos", CMD_EOS}, - {"eor", CMD_EOR}, - {"eot_enable", CMD_EOT_ENABLE}, - {"eot_char", CMD_EOT_CHAR}, - - // Common - {"read", CMD_READ}, - {"write", CMD_WRITE}, - {"trg", CMD_TRG}, // Trigger instrument (GET) - {"clr", CMD_CLR}, // Clear device buffer (SDC) - {"stat", CMD_STAT}, // Controller Status - - // GPIB Bus Lines/States - {"dcl", CMD_DCL}, // "Device Clear" (Resets state of ALL devices) - {"spoll", CMD_SPOLL}, // "Serial Poll" (Read status byte) - {"srq", CMD_SRQ}, // Check "Service Request" line status - {"loc", CMD_LOC}, // Return to local (front panel) control - {"gtl", CMD_GTL}, // "Go To Local" message - {"llo", CMD_LLO}, // "Local Lockout" (Disables front panel buttons) - {"ren", CMD_REN}, // "Remote Enable" - {"ifc", CMD_IFC}, // "Interface Clear" (Hard bus reset) - - // HP3478A - {"cont", CMD_CONT}, // Continuity Test - {"diode", CMD_DIODE}, // Diode Test - {"xohm", CMD_XOHM}, // Extended ohm - {"dbm", CMD_DBM}, // dBm - {"temp", CMD_TEMP}, // Temperature - {"rel", CMD_REL}, // Relative (Delta) measurement - {"math", CMD_MATH}, // MIN/MAX/AVG - {"disp", CMD_DISP}, // Set Display Text - {"env", CMD_ENV}, // Temp/Hum sensor - {"norm", CMD_NORM}, // Reset to Normal/DC Volts - - {NULL, CMD_UNKNOWN}}; - +// Modes/config typedef enum { AUTO_OFF = 0, AUTO_ON = 1, // read after every write @@ -217,26 +124,49 @@ typedef enum { MODE_PASSTHROUGH = 0, // Standard USB-GPIB bridge MODE_MENU, // User is cycling options on DMM display MODE_FEAT_REL, // Relative Mode active - MODE_FEAT_TEMP, // PT1000 Temp Mode active - MODE_FEAT_DBM, // Power ratio using a 50R impedance as ref + MODE_FEAT_AUTOHOLD, // Autohold MODE_FEAT_CONT, // Continuity Mode active MODE_FEAT_DIODE, // Diode test mode + MODE_FEAT_TEMP, // PT1000 Temp Mode active + MODE_FEAT_DBM, // Power ratio using a 50R impedance as ref MODE_FEAT_XOHM, // Extended Ohms active - MODE_FEAT_STATS // AVG/MIN/MAX + MODE_FEAT_STATS, // AVG/MIN/MAX } work_mode_t; +// Menu/UI typedef enum { MENU_REL = 0, - MENU_TEMP, - MENU_DBM, + MENU_AUTOHOLD, MENU_CONT, MENU_DIODE, + MENU_TEMP, + MENU_DBM, MENU_XOHM, MENU_STATS, MENU_EXIT, MENU_MAX_ITEMS } menu_item_t; +// Sub-menu states +typedef enum { + SUBMENU_NONE = 0, + SUBMENU_TEMP_SENS, // step 1: sensor Type + SUBMENU_TEMP_WIRE, // step 2a: wire mode (skipped for Type K) + SUBMENU_TEMP_NTC // step 2b: NTC Value (Thermistor only) +} submenu_state_t; + +// Logic + +// Sensor Types +typedef enum { + SENS_PT1000 = 0, + SENS_THERMISTOR, + SENS_TYPE_K, + SENS_MAX_ITEMS +} temp_sensor_t; + +typedef enum { WIRE_2W = 0, WIRE_4W, WIRE_MAX_ITEMS } wire_mode_t; + typedef enum { // Generic NTC_10K_3950 = 0, // Generic china 3950 @@ -263,48 +193,10 @@ typedef struct { const char* name; // display name } ntc_def_t; -// UI Strings -static const char* MENU_NAMES[] = {"REL", "TEMP", "DBM", "CONT", - "DIODE", "XOHM", "STATS", "EXIT"}; +// Logic states -static const char* SENSOR_NAMES[] = {"PT1000", "THERM", "TYPE K"}; - -static const char* WIRE_NAMES[] = {"2-WIRE", "4-WIRE"}; - -// some common NTC defs -static const ntc_def_t NTC_DEFS[] = { - // Generic/China - {10000.0, 3950.0, "10K 3950"}, // black bead) - {10000.0, 3435.0, "10K 3435"}, // euro? - {50000.0, 3950.0, "50K 3950"}, // generic - {100000.0, 3950.0, "100K 3950"}, // generic - // From YSI datasheet - {2252.0, 3891.0, "2.252K YSI"}, // 44004 - {3000.0, 3891.0, "3K YSI"}, // 44005 - {5000.0, 3891.0, "5K YSI"}, // 44007 - {10000.0, 3574.0, "10K YSI A"}, // Mix H (YSI 10k) - {10000.0, 3891.0, "10K YSI B"}, // Mix B (matches 2.2K curve) - {30000.0, 3810.0, "30K YSI"}, // 44008 - {100000.0, 3988.0, "100K YSI"}, // 44011 - {1000000.0, 4582.0, "1MEG YSI"} // 44015 -}; - -// Sub-menu states -typedef enum { - SUBMENU_NONE = 0, - SUBMENU_TEMP_SENS, // step 1: sensor Type - SUBMENU_TEMP_WIRE, // step 2a: wire mode (skipped for Type K) - SUBMENU_TEMP_NTC // step 2b: NTC Value (Thermistor only) -} submenu_state_t; - -// Sensor Types -typedef enum { - SENS_PT1000 = 0, - SENS_THERMISTOR, - SENS_TYPE_K, - SENS_MAX_ITEMS -} temp_sensor_t; -typedef enum { WIRE_2W = 0, WIRE_4W, WIRE_MAX_ITEMS } wire_mode_t; +// GPIB session +typedef enum { SESSION_WRITE, SESSION_READ } session_mode_t; typedef enum { DIODE_STATE_OPEN = 0, // probes open @@ -321,7 +213,17 @@ typedef struct { char unit_str[5]; // "VDC", "OHM", etc. } dmm_decoded_state_t; +// App state + typedef union { + struct { + double latched_val; // val currently frozen on the LCD + double candidate_val; // val we are currently testing for stability + uint8_t stable_count; // how many counts has the candidate been stable for + bool is_populated; // has a val ever been latched? + char unit[5]; + } autohold; + struct { double offset; uint8_t stable_count; @@ -340,12 +242,12 @@ typedef union { struct { double r1; - uint8_t calibrated; + bool calibrated; } xohm; struct { - uint8_t connected; // touchy? - uint32_t chirp_start; // when we began the touchy + diode_state_t connected; // touchy? + uint32_t chirp_start; // when we began the touchy } diode; struct { @@ -370,31 +272,18 @@ typedef struct { uint8_t env_sensor_present : 1; uint8_t dmm_online : 1; uint8_t has_saved_state : 1; - uint8_t beep_active : 1; - uint8_t auto_read : 2; + uint8_t tone_timer_pending : 1; + uint8_t reserved : 2; - // Protocol Config - uint8_t eoi_assert : 1; // ++eoi 0|1 - uint8_t eos_mode : 2; // ++eos 0-3 - uint8_t eot_enable : 1; // ++eot_enable 0|1 - uint8_t eor_mode : 3; // ++eor 0-7 - uint8_t reserved : 1; // padding - - uint32_t gpib_timeout_ms; - - // Addresses - uint8_t target_addr; - uint8_t dmm_addr; - uint8_t eot_char; + uint32_t usb_timeout_cycles; // calculated from sys_cfg.usb_timeout_target_ms // Timers uint32_t usb_ts; uint32_t env_last_read; uint32_t last_poll_time; - uint32_t poll_interval; uint32_t ignore_input_start_ts; - uint32_t beep_start_ts; // buzzer - uint32_t beep_duration; + uint32_t tone_start_ts; // buzzer + uint32_t tone_duration; // Logic work_mode_t current_mode; @@ -439,66 +328,426 @@ typedef union { uint8_t raw[256]; } app_scratchpad_t; -static app_state_t app = { - // config defaults - .target_addr = DEFAULT_DMM_ADDR, +// Consts/LUTs + +static const cmd_entry_t COMMAND_TABLE[] = { + // Controller config + {"ver", CMD_VER}, + {"help", CMD_HELP}, + {"?", CMD_HELP}, + {"savecfg", CMD_SAVECFG}, + {"config_set", CMD_CFG_SET}, // ++config_set name value + {"set", CMD_CFG_SET}, // Alias: ++set name value + {"config_get", CMD_CFG_GET}, // ++config_get name + {"get", CMD_CFG_GET}, // Alias: ++get name + {"config", CMD_CFG_LIST}, // ++config + {"rst", CMD_RST}, + {"addr", CMD_ADDR}, // Set target GPIB address + {"mode", CMD_MODE}, // 0=Device, 1=Controller + {"tmo", CMD_TIMEOUT}, // Set read timeout + {"read_tmo_ms", CMD_TIMEOUT}, + + // Protocol/formatting + {"auto", CMD_AUTO}, // Auto-read data after write + {"eoi", CMD_EOI}, // Enable/Disable EOI assert + {"eos", CMD_EOS}, + {"eor", CMD_EOR}, + {"eot_enable", CMD_EOT_ENABLE}, + {"eot_char", CMD_EOT_CHAR}, + + // Common + {"read", CMD_READ}, + {"write", CMD_WRITE}, + {"trg", CMD_TRG}, // Trigger instrument (GET) + {"clr", CMD_CLR}, // Clear device buffer (SDC) + {"stat", CMD_STAT}, // Controller Status + + // GPIB Bus Lines/States + {"dcl", CMD_DCL}, // "Device Clear" (Resets state of ALL devices) + {"spoll", CMD_SPOLL}, // "Serial Poll" (Read status byte) + {"srq", CMD_SRQ}, // Check "Service Request" line status + {"loc", CMD_LOC}, // Return to local (front panel) control + {"gtl", CMD_GTL}, // "Go To Local" message + {"llo", CMD_LLO}, // "Local Lockout" (Disables front panel buttons) + {"ren", CMD_REN}, // "Remote Enable" + {"ifc", CMD_IFC}, // "Interface Clear" (Hard bus reset) + + // HP3478A + {"cont", CMD_CONT}, // Continuity Test + {"diode", CMD_DIODE}, // Diode Test + {"xohm", CMD_XOHM}, // Extended ohm + {"dbm", CMD_DBM}, // dBm + {"temp", CMD_TEMP}, // Temperature + {"rel", CMD_REL}, // Relative (Delta) measurement + {"math", CMD_MATH}, // MIN/MAX/AVG + {"disp", CMD_DISP}, // Set Display Text + {"env", CMD_ENV}, // Temp/Hum sensor + {"hold", CMD_AUTOHOLD}, // Autohold mode + {"norm", CMD_NORM}, // Reset to Normal/DC Volts + + {NULL, CMD_UNKNOWN}}; + +// UI Strings +static const char* MENU_NAMES[] = {"REL", "AUTOHOLD", "CONT", "DIODE", "TEMP", + "DBM", "XOHM", "STATS", "EXIT"}; + +static const char* SENSOR_NAMES[] = {"PT1000", "THERM", "TYPE K"}; + +static const char* WIRE_NAMES[] = {"2-WIRE", "4-WIRE"}; + +// some common NTC defs +static const ntc_def_t NTC_DEFS[] = { + // Generic/China + {10000.0, 3950.0, "10K 3950"}, // black bead) + {10000.0, 3435.0, "10K 3435"}, // euro? + {50000.0, 3950.0, "50K 3950"}, // generic + {100000.0, 3950.0, "100K 3950"}, // generic + // From YSI datasheet + {2252.0, 3891.0, "2.252K YSI"}, // 44004 + {3000.0, 3891.0, "3K YSI"}, // 44005 + {5000.0, 3891.0, "5K YSI"}, // 44007 + {10000.0, 3574.0, "10K YSI A"}, // Mix H (YSI 10k) + {10000.0, 3891.0, "10K YSI B"}, // Mix B (matches 2.2K curve) + {30000.0, 3810.0, "30K YSI"}, // 44008 + {100000.0, 3988.0, "100K YSI"}, // 44011 + {1000000.0, 4582.0, "1MEG YSI"} // 44015 +}; + +// buzz +static const uint32_t ONLINE_NOTES[] = {3100, 80, 4200, 120}; +// static const uint32_t OFFLINE_NOTES[] = {4200, 80, 3100, 120}; + +static const cfg_field_t CONFIG_MAP[] = { + // addressing + CFG_ENTRY(my_addr, CFG_TYPE_UINT8), + CFG_ENTRY(dmm_addr, CFG_TYPE_UINT8), + CFG_ENTRY(target_addr, CFG_TYPE_UINT8), + + // protocol + CFG_ENTRY(eot_char, CFG_TYPE_UINT8), + CFG_ENTRY(eot_enable, CFG_TYPE_UINT8), + CFG_ENTRY(eoi_assert, CFG_TYPE_UINT8), + CFG_ENTRY(eos_mode, CFG_TYPE_UINT8), + CFG_ENTRY(eor_mode, CFG_TYPE_UINT8), + CFG_ENTRY(auto_read, CFG_TYPE_UINT8), + + // timings (hw) + CFG_ENTRY(gpib_timeout_ms, CFG_TYPE_UINT32), + CFG_ENTRY(poll_interval_ms, CFG_TYPE_UINT32), + CFG_ENTRY(env_sensor_read_interval_ms, CFG_TYPE_UINT32), + CFG_ENTRY(dmm_recovery_delay_ms, CFG_TYPE_UINT32), + CFG_ENTRY(usb_debounce_connect_ms, CFG_TYPE_UINT32), + CFG_ENTRY(usb_debounce_disconnect_ms, CFG_TYPE_UINT32), + CFG_ENTRY(usb_timeout_target_ms, CFG_TYPE_UINT32), + + // timings (ui) + CFG_ENTRY(menu_dot_interval_ms, CFG_TYPE_UINT32), + CFG_ENTRY(menu_commit_delay_ms, CFG_TYPE_UINT32), + CFG_ENTRY(menu_sublayer_delay_ms, CFG_TYPE_UINT32), + CFG_ENTRY(menu_debounce_ms, CFG_TYPE_UINT32), + CFG_ENTRY(menu_lockout_ms, CFG_TYPE_UINT32), + CFG_ENTRY(stats_cycle_time_ms, CFG_TYPE_UINT32), + CFG_ENTRY(cont_disp_update_ms, CFG_TYPE_UINT32), + CFG_ENTRY(diode_stable_ms, CFG_TYPE_UINT32), + + // buzzer + CFG_ENTRY(buzzer_chirp_hz, CFG_TYPE_UINT32), + CFG_ENTRY(buzzer_chirp_ms, CFG_TYPE_UINT32), + CFG_ENTRY(buzzer_cont_hz, CFG_TYPE_UINT32), + + // math/cal + CFG_ENTRY(rtd_a, CFG_TYPE_DOUBLE), + CFG_ENTRY(rtd_b, CFG_TYPE_DOUBLE), + CFG_ENTRY(rtd_r0, CFG_TYPE_DOUBLE), + CFG_ENTRY(cjc_fallback_temp, CFG_TYPE_DOUBLE), + CFG_ENTRY(cjc_self_heating_offset, CFG_TYPE_DOUBLE), + CFG_ENTRY(type_k_scale, CFG_TYPE_DOUBLE), + CFG_ENTRY(dbm_ref_z, CFG_TYPE_DOUBLE), + CFG_ENTRY(diode_th_short, CFG_TYPE_DOUBLE), + CFG_ENTRY(diode_th_open, CFG_TYPE_DOUBLE), + + // thresholds + CFG_ENTRY(autohold_threshold, CFG_TYPE_DOUBLE), + CFG_ENTRY(autohold_change_req, CFG_TYPE_DOUBLE), + CFG_ENTRY(autohold_min_val, CFG_TYPE_DOUBLE), + CFG_ENTRY(cont_threshold_ohms, CFG_TYPE_DOUBLE), + + // logic counts + CFG_ENTRY(autohold_stable_count, CFG_TYPE_UINT32), + CFG_ENTRY(rel_stable_count, CFG_TYPE_UINT32), +}; + +static const size_t CONFIG_MAP_SIZE = sizeof(CONFIG_MAP) / sizeof(cfg_field_t); + +// Globals + +// Flash config +__attribute__((section(".external"))) volatile fw_config_t flash_config = { + .magic = CONFIG_MAGIC, + .version = CONFIG_VERSION, + + .my_addr = MY_ADDR, .dmm_addr = DEFAULT_DMM_ADDR, - .gpib_timeout_ms = DEFAULT_GPIB_TIMEOUT_MS, - .poll_interval = POLL_INTERVAL_MS, + .target_addr = DEFAULT_DMM_ADDR, // prologix std defaults - .auto_read = 0, // ++auto 0 (off) - .eoi_assert = 1, // ++eoi 1 (assert EOI on write end) - .eos_mode = 0, // ++eos 0 (CR+LF appended to writes) - .eor_mode = 0, // ++eor 0 (terminates read on CR+LF) - .eot_enable = 0, // ++eot_enable 0 (off) - .eot_char = 0, // ++eot_char 0 + .auto_read = 0, // ++auto 0 (off) + .eoi_assert = true, // ++eoi 1 (assert EOI on write end) + .eos_mode = 0, // ++eos 0 (CR+LF appended to writes) + .eor_mode = 0, // ++eor 0 (terminates read on CR+LF) + .eot_enable = false, // ++eot_enable 0 (off) + .eot_char = 0, // ++eot_char 0 - .current_mode = MODE_PASSTHROUGH, + .gpib_timeout_ms = DEFAULT_GPIB_TIMEOUT_MS, + .poll_interval_ms = POLL_INTERVAL_MS, + .env_sensor_read_interval_ms = ENV_SENSOR_READ_INTERVAL_MS, + .dmm_recovery_delay_ms = DMM_RECOVERY_DELAY_MS, + .usb_debounce_connect_ms = USB_DEBOUNCE_CONNECT_MS, + .usb_debounce_disconnect_ms = USB_DEBOUNCE_DISCONNECT_MS, + .usb_timeout_target_ms = USB_TIMEOUT_TARGET_MS, - .usb_online = 0, - .dmm_online = 0, - .has_saved_state = 0, - .beep_active = 0}; + .menu_dot_interval_ms = MENU_DOT_INTERVAL_MS, + .menu_commit_delay_ms = MENU_COMMIT_DELAY_MS, + .menu_sublayer_delay_ms = MENU_SUBLAYER_DELAY_MS, + .menu_debounce_ms = MENU_DEBOUNCE_MS, + .menu_lockout_ms = MENU_LOCKOUT_MS, + .stats_cycle_time_ms = STATS_CYCLE_TIME_MS, + .cont_disp_update_ms = CONT_DISP_UPDATE_MS, + + // Physics + .rtd_a = RTD_A, + .rtd_b = RTD_B, + .rtd_r0 = RTD_R0, + .cjc_fallback_temp = CJC_FALLBACK_TEMP, + .cjc_self_heating_offset = CJC_SELF_HEATING_OFFSET, + .type_k_scale = TYPE_K_SCALE, + .dbm_ref_z = DBM_REF_Z, + + // Thresholds + .diode_th_short = DIODE_TH_SHORT, + .diode_th_open = DIODE_TH_OPEN, + .diode_stable_ms = DIODE_STABLE_MS, + + // Buzzer + .buzzer_chirp_hz = BUZZER_CHIRP_HZ, + .buzzer_chirp_ms = BUZZER_CHIRP_MS, + .buzzer_cont_hz = BUZZER_CONT_HZ, + + .autohold_threshold = AUTOHOLD_THRESHOLD, + .autohold_change_req = AUTOHOLD_CHANGE_REQ, + .autohold_min_val = AUTOHOLD_MIN_VAL, + .cont_threshold_ohms = CONT_THRESHOLD_OHMS, + + .autohold_stable_count = AUTOHOLD_STABLE_COUNT, + .rel_stable_count = REL_STABLE_COUNT}; + +static fw_config_t sys_cfg; + +// App state +static app_state_t app = {.current_mode = MODE_PASSTHROUGH, + + .usb_online = false, + .dmm_online = false, + .has_saved_state = false, + .tone_timer_pending = false}; static app_scratchpad_t scratch; -// GPIB session -typedef enum { SESSION_WRITE, SESSION_READ } session_mode_t; +// USB +volatile uint8_t usb_rx_buffer[USB_RX_BUF_SIZE]; +volatile uint32_t usb_rx_head = 0; +volatile uint32_t usb_rx_tail = 0; + +volatile uint8_t usb_tx_buffer[USB_TX_BUF_SIZE]; +volatile uint32_t usb_tx_head = 0; +volatile uint32_t usb_tx_tail = 0; + +static uint8_t cdc_line_coding[7] = {0x00, 0xC2, 0x01, 0x00, 0x00, 0x00, 0x08}; +extern volatile uint8_t usb_debug; // DEBUG + +// Audio state +volatile bool is_buzzer_pulsing = false; +static uint32_t current_buzz_freq = 0; // LUT for GPIB writes static uint32_t gpib_write_lut[256]; -// USB Ring Buffers -#define USB_RX_BUF_SIZE 1024 -#define USB_RX_MASK (USB_RX_BUF_SIZE - 1) +// systick +volatile uint32_t systick_millis; -#define USB_TX_BUF_SIZE 2048 -#define USB_TX_MASK (USB_TX_BUF_SIZE - 1) +static void systick_init(void) { + SysTick->CTLR = 0x0000; + SysTick->CMP = SysTick->CNT + SYSTICK_ONE_MILLISECOND; + systick_millis = 0; + SysTick->CTLR = SYSTICK_CTLR_STE | // Enable Counter + SYSTICK_CTLR_STIE | // Enable Interrupts + SYSTICK_CTLR_STCLK; // Set Clock Source to HCLK/1 -volatile uint8_t usb_rx_buffer[USB_RX_BUF_SIZE]; -volatile uint8_t usb_tx_buffer[USB_TX_BUF_SIZE]; -volatile uint32_t usb_rx_head = 0; -volatile uint32_t usb_tx_head = 0; -volatile uint32_t usb_rx_tail = 0; -volatile uint32_t usb_tx_tail = 0; + NVIC_EnableIRQ(SysTick_IRQn); +} -static uint8_t cdc_line_coding[7] = {0x00, 0xC2, 0x01, 0x00, 0x00, 0x00, 0x08}; +void SysTick_Handler(void) __attribute__((interrupt)); +void SysTick_Handler(void) { + SysTick->CMP = SysTick->CNT + SYSTICK_ONE_MILLISECOND; + SysTick->SR = 0; + systick_millis++; +} -volatile uint8_t buzzer_active = 0; -static uint32_t current_buzz_freq = 0; -extern volatile uint8_t usb_debug; +// Config + +static const cfg_field_t* find_config_field(const char* name) { + for (size_t i = 0; i < CONFIG_MAP_SIZE; i++) { + if (strcasecmp(CONFIG_MAP[i].name, name) == 0) { + return &CONFIG_MAP[i]; + } + } + return NULL; +} + +inline static void config_apply_to_app(void) { + app.usb_timeout_cycles = + ((FUNCONF_SYSTEM_CORE_CLOCK / 1000 * sys_cfg.usb_timeout_target_ms) / + CYCLES_PER_LOOP); +} + +static void config_reset_defaults(void) { + sys_cfg.magic = CONFIG_MAGIC; + sys_cfg.version = CONFIG_VERSION; + + sys_cfg.my_addr = MY_ADDR; + sys_cfg.dmm_addr = DEFAULT_DMM_ADDR; + sys_cfg.target_addr = DEFAULT_DMM_ADDR; + + sys_cfg.auto_read = 0; // ++auto 0 (off) + sys_cfg.eoi_assert = true; // ++eoi 1 (assert EOI on write end) + sys_cfg.eos_mode = 0; // ++eos 0 (CR+LF appended to writes) + sys_cfg.eor_mode = 0; // ++eor 0 (terminates read on CR+LF) + sys_cfg.eot_enable = false; // ++eot_enable 0 (off) + sys_cfg.eot_char = 0; // ++eot_char 0 + + sys_cfg.gpib_timeout_ms = DEFAULT_GPIB_TIMEOUT_MS; + sys_cfg.poll_interval_ms = POLL_INTERVAL_MS; + sys_cfg.env_sensor_read_interval_ms = ENV_SENSOR_READ_INTERVAL_MS; + sys_cfg.dmm_recovery_delay_ms = DMM_RECOVERY_DELAY_MS; + sys_cfg.usb_debounce_connect_ms = USB_DEBOUNCE_CONNECT_MS; + sys_cfg.usb_debounce_disconnect_ms = USB_DEBOUNCE_DISCONNECT_MS; + sys_cfg.usb_timeout_target_ms = USB_TIMEOUT_TARGET_MS; + + sys_cfg.menu_dot_interval_ms = MENU_DOT_INTERVAL_MS; + sys_cfg.menu_commit_delay_ms = MENU_COMMIT_DELAY_MS; + sys_cfg.menu_sublayer_delay_ms = MENU_SUBLAYER_DELAY_MS; + sys_cfg.menu_debounce_ms = MENU_DEBOUNCE_MS; + sys_cfg.menu_lockout_ms = MENU_LOCKOUT_MS; + sys_cfg.stats_cycle_time_ms = STATS_CYCLE_TIME_MS; + sys_cfg.cont_disp_update_ms = CONT_DISP_UPDATE_MS; + // Physics + sys_cfg.rtd_a = RTD_A; + sys_cfg.rtd_b = RTD_B; + sys_cfg.rtd_r0 = RTD_R0; + sys_cfg.cjc_fallback_temp = CJC_FALLBACK_TEMP; + sys_cfg.cjc_self_heating_offset = CJC_SELF_HEATING_OFFSET; + sys_cfg.type_k_scale = TYPE_K_SCALE; + sys_cfg.dbm_ref_z = DBM_REF_Z; + // Thresholds + sys_cfg.diode_th_short = DIODE_TH_SHORT; + sys_cfg.diode_th_open = DIODE_TH_OPEN; + sys_cfg.diode_stable_ms = DIODE_STABLE_MS; + + sys_cfg.autohold_threshold = AUTOHOLD_THRESHOLD; + sys_cfg.autohold_change_req = AUTOHOLD_CHANGE_REQ; + sys_cfg.autohold_min_val = AUTOHOLD_MIN_VAL; + sys_cfg.cont_threshold_ohms = CONT_THRESHOLD_OHMS; + + // Buzzer + sys_cfg.buzzer_chirp_hz = BUZZER_CHIRP_HZ; + sys_cfg.buzzer_chirp_ms = BUZZER_CHIRP_MS; + sys_cfg.buzzer_cont_hz = BUZZER_CONT_HZ; + + sys_cfg.autohold_stable_count = AUTOHOLD_STABLE_COUNT; + sys_cfg.rel_stable_count = REL_STABLE_COUNT; +} + +static void config_save(void) { + uint16_t* source_ptr = (uint16_t*)&sys_cfg; + uint32_t start_addr = (uint32_t)&flash_config; + + int total_bytes = sizeof(fw_config_t); + int total_halfwords = (total_bytes + 1) / 2; + int pages_to_erase = (total_bytes + ERASE_PAGE_SIZE - 1) / ERASE_PAGE_SIZE; + + printf("config: saving %d bytes to %08lx\n", total_bytes, start_addr); + + // unlock flash + if (FLASH->CTLR & CR_LOCK_Set) { + FLASH->KEYR = FLASH_KEY1; + FLASH->KEYR = FLASH_KEY2; + } + + // erase loop + for (int p = 0; p < pages_to_erase; p++) { + uint32_t page_addr = start_addr + (p * ERASE_PAGE_SIZE); + + FLASH->CTLR &= ~CR_PAGE_ER; // clear + FLASH->CTLR |= CR_PAGE_ER; // set page erase + FLASH->ADDR = page_addr; + FLASH->CTLR |= CR_STRT_Set; // start erase + + while (FLASH->STATR & SR_BSY); // wait + + if (FLASH->STATR & SR_WRPRTERR) { + printf("config: erase error (WPR)\n"); + FLASH->CTLR |= CR_LOCK_Set; + return; + } + FLASH->CTLR &= ~CR_PAGE_ER; // clear erase flag + } + + // slower than fast program but we rarely do this, so meh + FLASH->CTLR |= CR_PG_Set; + + for (int i = 0; i < total_halfwords; i++) { + uint32_t write_addr = start_addr + (i * 2); + + *(__IO uint16_t*)write_addr = source_ptr[i]; + + while (FLASH->STATR & SR_BSY); // wait + + if (FLASH->STATR & SR_WRPRTERR || FLASH->STATR & FLASH_STATR_PGERR) { + printf("config: write error @ %08lx\n", write_addr); + break; + } + } + + FLASH->CTLR &= ~CR_PG_Set; // disable PG + FLASH->CTLR |= CR_LOCK_Set; // lock + + printf("config: saved\n"); +} + +static void config_init(void) { + const fw_config_t* src = (const fw_config_t*)&flash_config; + + if (src->magic == CONFIG_MAGIC) { + memcpy(&sys_cfg, src, sizeof(fw_config_t)); + printf("config: loaded from flash\n"); + } else { + printf("config: flash invalid/blank, loading defaults\n"); + config_reset_defaults(); + // autosave defaults? + // config_save(); + } +} // helpers -static int starts_with_nocase(const char* str, const char* prefix) { +static bool starts_with_nocase(const char* str, const char* prefix) { while (*prefix) { if (tolower((unsigned char)*str) != tolower((unsigned char)*prefix)) { - return 0; + return false; } str++; prefix++; } - return 1; + return true; } static char* skip_spaces(char* str) { @@ -521,37 +770,50 @@ static cmd_id_t parse_command_id(const char* cmd) { return CMD_UNKNOWN; } -void double_to_str(char* buf, size_t buf_size, double val, int prec) { +static void double_to_str(char* buf, size_t buf_size, double val, int prec) { if (buf_size == 0) return; buf[0] = '\0'; size_t offset = 0; - // NaN/Inf - if (val != val) { + if (isnan(val)) { if (buf_size > 3) strcpy(buf, "NAN"); return; } - - // negative - if (val < 0.0) { + if (isinf(val)) { + if (buf_size - offset > 3) strcpy(buf + offset, "INF"); + return; + } + if (signbit(val)) { if (offset < buf_size - 1) buf[offset++] = '-'; val = -val; } - // multiplier + if (val > (double)UINT32_MAX) { + if (buf_size > 3) strcpy(buf, "OVFL"); // overflow + return; + } + if (prec < 0) prec = 0; if (prec > 9) prec = 9; // limit precision - unsigned long long multiplier = 1; + // calc multiplier + uint32_t multiplier = 1; for (int i = 0; i < prec; i++) multiplier *= 10; - // scale and round - val = (val * (double)multiplier) + 0.5; + uint32_t int_part = (uint32_t)val; + // fractional component + double remainder = val - (double)int_part; - // split integer and fractional parts - unsigned long long full_scaled = (unsigned long long)val; - unsigned long int_part = (unsigned long)(full_scaled / multiplier); - unsigned long long frac_part = full_scaled % multiplier; + // scale and round the remainder + remainder = (remainder * (double)multiplier) + 0.5; + uint32_t frac_part = (uint32_t)remainder; + + // handle rounding rollover + if (frac_part >= multiplier) { + frac_part = 0; + int_part++; + // if int_part overflows here it wraps to 0 + } // print integer part int res = snprintf(buf + offset, buf_size - offset, "%lu", int_part); @@ -566,10 +828,10 @@ void double_to_str(char* buf, size_t buf_size, double val, int prec) { if (prec > 0 && offset < buf_size - 1) { buf[offset++] = '.'; - unsigned long long divider = multiplier / 10; + uint64_t divider = multiplier / 10; while (divider > 0 && offset < buf_size - 1) { - unsigned int digit = (unsigned int)(frac_part / divider); + uint32_t digit = frac_part / divider; buf[offset++] = (char)('0' + digit); frac_part %= divider; @@ -581,7 +843,7 @@ void double_to_str(char* buf, size_t buf_size, double val, int prec) { } // "Note that period, comma, and semicolon go between characters" -int count_non_visual_chars(const char* s) { +static int count_non_visual_chars(const char* s) { int c = 0; while (*s) { if (*s == '.' || *s == ',' || *s == ';') c++; @@ -590,8 +852,8 @@ int count_non_visual_chars(const char* s) { return c; } -void format_metric_value(char* buffer, size_t buf_len, double val, - const char* unit, int auto_scale) { +static void format_metric_value(char* buffer, size_t buf_len, double val, + const char* unit, int auto_scale) { // must hold 12 visible chars + 1 null. if (buf_len <= HP_DISP_LEN) return; @@ -635,7 +897,7 @@ void format_metric_value(char* buffer, size_t buf_len, double val, } } - int is_neg = (scaled < 0.0); + bool is_neg = (scaled < 0.0); // visual slots needed for non-decimal part (sign + ints + space separator) int separator = (meta_vis_len > 0) ? 1 : 0; int reserved_vis = is_neg + int_digits + separator + meta_vis_len; @@ -698,11 +960,11 @@ void format_metric_value(char* buffer, size_t buf_len, double val, } } -double parse_double(const char* s) { +static double parse_double(const char* s) { double mantissa = 0.0; int exponent = 0; int sign = 1; - int decimal_seen = 0; + bool decimal_seen = false; int decimal_counts = 0; // skip whitespace @@ -724,7 +986,7 @@ double parse_double(const char* s) { decimal_counts++; } } else if (*s == '.') { - decimal_seen = 1; + decimal_seen = true; } else if (*s == 'E' || *s == 'e') { s++; exponent = atoi(s); @@ -750,8 +1012,8 @@ double parse_double(const char* s) { return mantissa * sign; } -void decode_dmm_state_bytes(const uint8_t* state_bytes, - dmm_decoded_state_t* out) { +static void decode_dmm_state_bytes(const uint8_t* state_bytes, + dmm_decoded_state_t* out) { uint8_t b1 = state_bytes[0]; uint8_t b2 = state_bytes[1]; @@ -837,7 +1099,8 @@ void decode_dmm_state_bytes(const uint8_t* state_bytes, } // constructs the base configuration string (F R N Z) from decoded state -void build_restoration_string(char* buffer, const dmm_decoded_state_t* state) { +static void build_restoration_string(char* buffer, + const dmm_decoded_state_t* state) { strcpy(buffer, HP3478A_DISP_NORMAL); strcat(buffer, state->cmd_func); strcat(buffer, state->cmd_range); @@ -845,14 +1108,14 @@ void build_restoration_string(char* buffer, const dmm_decoded_state_t* state) { strcat(buffer, state->cmd_az); } -static int is_query(const char* cmd) { - if (!cmd || !*cmd) return 0; +static bool is_query(const char* cmd) { + if (!cmd || !*cmd) return false; size_t len = strlen(cmd); while (len > 0 && isspace((unsigned char)cmd[len - 1])) { len--; } - if (len > 0 && cmd[len - 1] == '?') return 1; - return 0; + if (len > 0 && cmd[len - 1] == '?') return true; + return false; } #ifdef GPIB_DEBUG @@ -879,9 +1142,11 @@ static void gpib_dump_state(const char* context) { #define gpib_dump_state(x) ((void)0) #endif -void gpib_write_data(uint8_t b) { GPIOB->BSHR = gpib_write_lut[b]; } +inline static void gpib_write_data(uint8_t b) { + GPIOB->BSHR = gpib_write_lut[b]; +} -uint8_t gpib_read_data(void) { +inline static uint8_t gpib_read_data(void) { // read all 16 pins, invert (gpib is active low) uint32_t r = ~(GPIOB->INDR); uint8_t b = 0; @@ -901,7 +1166,7 @@ inline static int gpib_wait_pin(int pin, int expected_state) { uint32_t start = millis(); while (GPIB_READ(pin) != expected_state) { - if ((millis() - start) > app.gpib_timeout_ms) { + if ((millis() - start) > sys_cfg.gpib_timeout_ms) { #ifdef GPIB_DEBUG // Print which specific pin failed char* pin_name = "UNKNOWN"; @@ -922,9 +1187,9 @@ inline static int gpib_wait_pin(int pin, int expected_state) { return 0; } -int gpib_write_byte(uint8_t data, int assert_eoi) { +static int gpib_write_byte(uint8_t data, int assert_eoi) { #ifdef GPIB_DEBUG - printf("[TX] 0x%02X (EOI=%d)... ", data, assert_eoi); + // printf("[TX] 0x%02X (EOI=%d)... ", data, assert_eoi); #endif // wait for listeners to be ready @@ -962,7 +1227,7 @@ int gpib_write_byte(uint8_t data, int assert_eoi) { return 0; } -int gpib_read_byte(uint8_t* data, int* eoi_asserted) { +static int gpib_read_byte(uint8_t* data, bool* eoi_asserted) { // sssert busy state GPIB_ASSERT(PIN_NDAC); // not accepted yet GPIB_ASSERT(PIN_NRFD); // not ready yet @@ -979,6 +1244,9 @@ int gpib_read_byte(uint8_t* data, int* eoi_asserted) { if (gpib_wait_pin(PIN_DAV, 0) < 0) { GPIB_RELEASE(PIN_NDAC); GPIB_RELEASE(PIN_NRFD); +#ifdef GPIB_DEBUG + printf("[GPIB] Read timeout waiting for DAV Low (Talker not ready)\n"); +#endif return -1; // timeout } @@ -996,6 +1264,9 @@ int gpib_read_byte(uint8_t* data, int* eoi_asserted) { // wait for talker to release DAV if (gpib_wait_pin(PIN_DAV, 1) < 0) { GPIB_RELEASE(PIN_NRFD); +#ifdef GPIB_DEBUG + printf("[GPIB] Read timeout waiting for DAV High (Talker stuck)\n"); +#endif return -2; // timeout } @@ -1006,7 +1277,7 @@ int gpib_read_byte(uint8_t* data, int* eoi_asserted) { } // Sets up Talker/Listener for data transfer -int gpib_start_session(uint8_t target_addr, session_mode_t mode) { +static int gpib_start_session(uint8_t target_addr, session_mode_t mode) { GPIB_ASSERT(PIN_ATN); Delay_Us(1); @@ -1041,14 +1312,14 @@ err: // Bus management // Assert Interface Clear (IFC) -void gpib_interface_clear(void) { +static void gpib_interface_clear(void) { GPIB_ASSERT(PIN_IFC); Delay_Us(150); // IEEE-488 requires >100us GPIB_RELEASE(PIN_IFC); } // Control Remote Enable (REN) -void gpib_remote_enable(int enable) { +static void gpib_remote_enable(int enable) { if (enable) { GPIB_ASSERT(PIN_REN); } else { @@ -1057,13 +1328,13 @@ void gpib_remote_enable(int enable) { } // Check SRQ Line (Active Low) -static inline int gpib_check_srq(void) { return !GPIB_READ(PIN_SRQ); } +inline static int gpib_check_srq(void) { return !GPIB_READ(PIN_SRQ); } // Universal Commands (Affects All Devices) // Universal Device Clear (DCL) // Resets logic of ALL devices on the bus -int gpib_universal_clear(void) { +static int gpib_universal_clear(void) { GPIB_ASSERT(PIN_ATN); Delay_Us(1); @@ -1078,7 +1349,7 @@ int gpib_universal_clear(void) { // Local Lockout (LLO) // Disables front panel "Local" buttons on all devices -int gpib_local_lockout(void) { +static int gpib_local_lockout(void) { GPIB_ASSERT(PIN_ATN); Delay_Us(1); @@ -1096,7 +1367,7 @@ int gpib_local_lockout(void) { // Selected Device Clear (SDC) // Resets logic of ONLY the targeted device -int gpib_device_clear(uint8_t addr) { +static int gpib_device_clear(uint8_t addr) { GPIB_ASSERT(PIN_ATN); Delay_Us(1); @@ -1113,7 +1384,7 @@ err: // Group Execute Trigger (GET) // Triggers the device to take a measurement -int gpib_trigger(uint8_t addr) { +static int gpib_trigger(uint8_t addr) { GPIB_ASSERT(PIN_ATN); Delay_Us(1); @@ -1131,7 +1402,7 @@ err: // Go To Local (GTL) // Addresses a specific device and restores Front Panel control // (Keeps REN asserted for other devices on the bus) -int gpib_go_to_local(uint8_t addr) { +static int gpib_go_to_local(uint8_t addr) { GPIB_ASSERT(PIN_ATN); Delay_Us(1); @@ -1148,7 +1419,7 @@ err: // Serial Poll // Reads the Status Byte (STB) from the device -int gpib_serial_poll(uint8_t addr, uint8_t* status) { +static int gpib_serial_poll(uint8_t addr, uint8_t* status) { GPIB_ASSERT(PIN_ATN); Delay_Us(1); @@ -1163,7 +1434,7 @@ int gpib_serial_poll(uint8_t addr, uint8_t* status) { GPIB_ASSERT(PIN_NRFD); // Busy / Not Ready GPIB_RELEASE(PIN_ATN); // Handover to data mode - int eoi; + bool eoi; if (gpib_read_byte(status, &eoi) < 0) { goto err_data; } @@ -1200,18 +1471,18 @@ err: // Data transfer // Send string to device (handles CRLF escape sequences) -int gpib_send(uint8_t addr, const char* str) { +static int gpib_send(uint8_t addr, const char* str) { if (gpib_start_session(addr, SESSION_WRITE) < 0) return -1; int len = strlen(str); // EOS mode determines what we ADD to the string const char* suffix = ""; - if (app.eos_mode == 0) + if (sys_cfg.eos_mode == 0) suffix = "\r\n"; // CRLF - else if (app.eos_mode == 1) + else if (sys_cfg.eos_mode == 1) suffix = "\r"; // CR - else if (app.eos_mode == 2) + else if (sys_cfg.eos_mode == 2) suffix = "\n"; // LF // EOS 3 = none @@ -1223,7 +1494,7 @@ int gpib_send(uint8_t addr, const char* str) { // assert EOI on last byte ONLY if ++eoi 1 (eoi_assert) is set int is_last = (i == total_len - 1); - int trigger_eoi = (app.eoi_assert && is_last); + int trigger_eoi = (sys_cfg.eoi_assert && is_last); if (gpib_write_byte(b, trigger_eoi) < 0) { // error cleanup @@ -1242,11 +1513,11 @@ int gpib_send(uint8_t addr, const char* str) { } // Receive string from device -int gpib_receive(uint8_t addr, char* buf, int max_len) { +static int gpib_receive(uint8_t addr, char* buf, int max_len) { if (gpib_start_session(addr, SESSION_READ) < 0) return -1; int count = 0; - int eoi = 0; + bool eoi = false; uint8_t byte; int effective_max = max_len - 2; @@ -1257,37 +1528,37 @@ int gpib_receive(uint8_t addr, char* buf, int max_len) { // hw EOI always stops the read immediately if (eoi) break; - int match = 0; + bool match = false; char c = (char)byte; // buf[count-1] is 'c'. buf[count-2] is prev. char prev = (count >= 2) ? buf[count - 2] : 0; char prev2 = (count >= 3) ? buf[count - 3] : 0; - switch (app.eor_mode) { + switch (sys_cfg.eor_mode) { case 0: // CR + LF - if (prev == '\r' && c == '\n') match = 1; + if (prev == '\r' && c == '\n') match = true; break; case 1: // CR - if (c == '\r') match = 1; + if (c == '\r') match = true; break; case 2: // LF - if (c == '\n') match = 1; + if (c == '\n') match = true; break; case 3: // none? - match = 0; + match = false; break; case 4: // LF + CR - if (prev == '\n' && c == '\r') match = 1; + if (prev == '\n' && c == '\r') match = true; break; case 5: // ETX (0x03) - if (c == 0x03) match = 1; + if (c == 0x03) match = true; break; case 6: // CR + LF + ETX - if (prev2 == '\r' && prev == '\n' && c == 0x03) match = 1; + if (prev2 == '\r' && prev == '\n' && c == 0x03) match = true; break; case 7: // EOI signal only - match = 0; + match = false; break; } @@ -1295,8 +1566,8 @@ int gpib_receive(uint8_t addr, char* buf, int max_len) { } // append USB EOT char if enabled - if (app.eot_enable) { - buf[count++] = (char)app.eot_char; + if (sys_cfg.eot_enable) { + buf[count++] = (char)sys_cfg.eot_char; } buf[count] = 0; // null terminate @@ -1314,11 +1585,11 @@ int gpib_receive(uint8_t addr, char* buf, int max_len) { } // does not stop on newline and does not NULL terminate -int gpib_receive_binary(uint8_t addr, char* buf, int expected_len) { +static int gpib_receive_binary(uint8_t addr, char* buf, int expected_len) { if (gpib_start_session(addr, SESSION_READ) < 0) return -1; int count = 0; - int eoi = 0; + bool eoi = false; uint8_t byte; // run until we have the expected count @@ -1343,7 +1614,7 @@ int gpib_receive_binary(uint8_t addr, char* buf, int expected_len) { return count; } -void gpib_init(void) { +static void gpib_init(void) { // calculate BSHR for the DIO lines for (int i = 0; i < 256; i++) { gpib_write_lut[i] = @@ -1394,7 +1665,7 @@ void gpib_init(void) { // ------------------------------------ -void buzzer_init(void) { +static void buzzer_init(void) { funPinMode(PIN_BUZZ, GPIO_Speed_50MHz | GPIO_CNF_OUT_PP); funDigitalWrite(PIN_BUZZ, 0); @@ -1408,20 +1679,19 @@ void buzzer_init(void) { TIM2->CTLR1 |= TIM_CEN; } -void buzzer_set(uint32_t freq_hz) { - if (current_buzz_freq == freq_hz) return; - - current_buzz_freq = freq_hz; - +static void buzzer_hw_set(uint32_t freq_hz) { if (freq_hz == 0) { - buzzer_active = 0; + is_buzzer_pulsing = false; return; } - int reload_val = (int)(1000000UL / (2 * freq_hz)); - TIM2->ATRLR = reload_val; - TIM2->CNT = 0; // reset phase only on CHANGE - buzzer_active = 1; + if (current_buzz_freq != freq_hz) { + current_buzz_freq = freq_hz; + int reload_val = (int)(1000000UL / (2 * freq_hz)); + TIM2->ATRLR = reload_val; + TIM2->CNT = 0; // reset phase only on CHANGE + } + is_buzzer_pulsing = true; } void TIM2_IRQHandler(void) __attribute__((interrupt)); @@ -1430,7 +1700,7 @@ void TIM2_IRQHandler(void) { // clr the flag TIM2->INTFR = (int)~TIM_UIF; - if (buzzer_active) { + if (is_buzzer_pulsing) { // Toggle PC13 if (GPIOC->OUTDR & (1 << 13)) { GPIOC->BSHR = (1 << (16 + 13)); // Reset (Low) @@ -1446,34 +1716,28 @@ void TIM2_IRQHandler(void) { } } -void tone(unsigned int freq_hz, unsigned int duration_ms) { - if (freq_hz == 0) { - Delay_Ms(duration_ms); - return; +static void tone_nb(int freq, uint32_t duration_ms) { + buzzer_hw_set(freq); + if (freq > 0) { + app.tone_start_ts = millis(); + app.tone_duration = duration_ms; + app.tone_timer_pending = true; } - - buzzer_set(freq_hz); - Delay_Ms(duration_ms); - buzzer_set(0); } -void tone_nb(int freq, uint32_t duration_ms) { - buzzer_set(freq); - app.beep_start_ts = millis(); - app.beep_duration = duration_ms; - app.beep_active = 1; +void play_tune(const uint32_t* tune) { + // Hardcoded to 4 elements (2 notes) for speed/simplicity + for (int i = 0; i < 4; i += 2) { + buzzer_hw_set(tune[i]); + Delay_Ms(tune[i + 1]); + + buzzer_hw_set(0); + Delay_Ms(30); + } } -// "Boot Up" -void play_startup_tune() { - tone(1500, 100); - Delay_Ms(25); - tone(2500, 100); - Delay_Ms(25); - tone(4000, 100); -} - -// ------------------------------------ +// ------ +// USB int HandleSetupCustom(struct _USBState* ctx, int setup_code) { if (ctx->USBFS_SetupReqType & USB_REQ_TYP_CLASS) { @@ -1514,7 +1778,7 @@ void HandleDataOut(struct _USBState* ctx, int endp, uint8_t* data, int len) { } } -void usb_process_tx(void) { +static void usb_process_tx(void) { if (!app.usb_online) return; // check hw busy (endp 3) @@ -1543,7 +1807,7 @@ void usb_process_tx(void) { usb_tx_tail = (tail + len) & USB_TX_MASK; } -void usb_send_text(const char* str) { +static void usb_send_text(const char* str) { if (!app.usb_online) { // if offline, just reset buffer usb_tx_head = usb_tx_tail = 0; @@ -1557,13 +1821,13 @@ void usb_send_text(const char* str) { if (next == usb_tx_tail) { usb_process_tx(); - uint32_t timeout = USB_TIMEOUT_LIMIT; // ~5ms + uint32_t timeout = app.usb_timeout_cycles; // ~5ms while (next == usb_tx_tail) { // this *should* be set by the ISR, so can exit immediately if (!USB_HW_IS_ACTIVE()) return; if (--timeout == 0) { - return; // give up and drop + return; // give up and drop the packet } usb_process_tx(); @@ -1579,20 +1843,20 @@ void usb_send_text(const char* str) { } // pull a line from ring buffer -int get_start_command(char* dest_buf, int max_len) { +static int get_start_command(char* dest_buf, int max_len) { uint32_t head = usb_rx_head; uint32_t tail = usb_rx_tail; if (head == tail) return 0; int len = 0; - int found_newline = 0; + bool found_newline = false; uint32_t scan = tail; while (scan != head) { char c = usb_rx_buffer[scan]; if (c == '\n' || c == '\r') { - found_newline = 1; + found_newline = true; break; } scan = (scan + 1) & USB_RX_MASK; @@ -1627,8 +1891,6 @@ int get_start_command(char* dest_buf, int max_len) { return 0; } -// ---------------------------------------- - static void handle_usb_state(void) { int raw_status = USB_HW_IS_ACTIVE(); uint32_t now = millis(); @@ -1640,19 +1902,26 @@ static void handle_usb_state(void) { } // debounce with different thresholds for connect/disconnect - uint32_t threshold = - raw_status ? USB_DEBOUNCE_CONNECT_MS : USB_DEBOUNCE_DISCONNECT_MS; + uint32_t threshold = raw_status ? sys_cfg.usb_debounce_connect_ms + : sys_cfg.usb_debounce_disconnect_ms; if ((now - app.usb_ts) > threshold) { // state has been stable long enough if (app.usb_online != raw_status) { app.usb_online = raw_status; - if (app.usb_online) usb_rx_tail = usb_rx_head = 0; + if (app.usb_online) { + printf("[USB] CONNECTED\n"); + usb_rx_tail = usb_rx_head = 0; + } else { + printf("[USB] DISCONNECTED\n"); + } } } } +// ------ + static void handle_env_sensor(void) { if (!app.env_sensor_present) { return; @@ -1660,7 +1929,7 @@ static void handle_env_sensor(void) { uint32_t now = millis(); - if ((now - app.env_last_read) >= ENV_SENSOR_READ_INTERVAL_MS) { + if ((now - app.env_last_read) >= sys_cfg.env_sensor_read_interval_ms) { if (aht20_read(&app.current_env) == AHT20_OK) { app.env_last_read = now; } @@ -1668,7 +1937,7 @@ static void handle_env_sensor(void) { } // helper to write text to HP3478A display -void dmm_display(const char* text, const char* mode) { +static void dmm_display(const char* text, const char* mode) { // cmp vs shadow buf if (strncmp(app.last_disp_sent, text, HP_DISP_BUF_SIZE) == 0) { return; @@ -1683,26 +1952,29 @@ void dmm_display(const char* text, const char* mode) { app.last_disp_sent); // send it - gpib_send(app.dmm_addr, scratch.disp.full_cmd); + gpib_send(sys_cfg.dmm_addr, scratch.disp.full_cmd); } -static inline void dmm_display_normal(void) { - gpib_send(app.dmm_addr, HP3478A_DISP_NORMAL); + +inline static void dmm_display_normal(void) { + gpib_send(sys_cfg.dmm_addr, HP3478A_DISP_NORMAL); // invalidate cache (we're giving control back to DMM) app.last_disp_sent[0] = '\0'; } -void save_dmm_state(void) { +static void save_dmm_state(void) { gpib_interface_clear(); - gpib_send(app.dmm_addr, HP3478A_CMD_STATUS_BYTE); + gpib_send(sys_cfg.dmm_addr, HP3478A_CMD_STATUS_BYTE); - int len = gpib_receive_binary(app.dmm_addr, (char*)app.saved_state_bytes, 5); - app.has_saved_state = (len == 5) ? 1 : 0; + int len = + gpib_receive_binary(sys_cfg.dmm_addr, (char*)app.saved_state_bytes, 5); + app.has_saved_state = (len == 5) ? true : false; } -void restore_dmm_state(void) { +static void restore_dmm_state(void) { if (!app.has_saved_state) { + printf("[STATE] No saved state, applying defaults.\n"); // default fallback if no state saved - gpib_send(app.dmm_addr, + gpib_send(sys_cfg.dmm_addr, HP3478A_DISP_NORMAL HP3478A_FUNC_DC_VOLTS HP3478A_RANGE_AUTO HP3478A_DIGITS_5_5 HP3478A_AUTOZERO_ON HP3478A_TRIG_INTERNAL HP3478A_CMD_MASK_BTN_ONLY); @@ -1712,29 +1984,54 @@ void restore_dmm_state(void) { dmm_decoded_state_t state; decode_dmm_state_bytes(app.saved_state_bytes, &state); + printf("[STATE] Restoring: Func=%s Range=%s Digits=%s\n", state.cmd_func, + state.cmd_range, state.cmd_digits); + // "D1 Fx Rx Nx Zx" + "T1 M20" build_restoration_string(scratch.cmd.line_buf, &state); strcat(scratch.cmd.line_buf, HP3478A_TRIG_INTERNAL HP3478A_CMD_MASK_BTN_ONLY); - gpib_send(app.dmm_addr, scratch.cmd.line_buf); + printf("[STATE] Restore CMD: %s\n", scratch.cmd.line_buf); + gpib_send(sys_cfg.dmm_addr, scratch.cmd.line_buf); } -void exit_to_passthrough(void) { - buzzer_set(0); +static void exit_to_passthrough(void) { + buzzer_hw_set(0); + printf("[APP] Exiting to Passthrough. Restoring DMM...\n"); restore_dmm_state(); - gpib_go_to_local(app.dmm_addr); + gpib_go_to_local(sys_cfg.dmm_addr); app.current_mode = MODE_PASSTHROUGH; app.data.menu.layer = SUBMENU_NONE; app.menu_pos = 0; - app.beep_active = 0; + app.tone_timer_pending = false; uint32_t now = millis(); app.last_poll_time = now; app.ignore_input_start_ts = now; } -void enter_feature_mode(menu_item_t item) { +static int init_restored_state(char* unit_dst) { + if (!app.has_saved_state) { + dmm_display("ERR NO STATE", HP3478A_DISP_TEXT_FAST); + return -1; + } + + dmm_decoded_state_t saved_cfg; + decode_dmm_state_bytes(app.saved_state_bytes, &saved_cfg); + build_restoration_string(scratch.cmd.line_buf, &saved_cfg); + + strcat(scratch.cmd.line_buf, + HP3478A_TRIG_INTERNAL HP3478A_CMD_MASK_BTN_DATA HP3478A_CMD_SRQ_CLEAR); + + gpib_send(sys_cfg.dmm_addr, scratch.cmd.line_buf); + strcpy(unit_dst, saved_cfg.unit_str); + + return 0; +} + +static void enter_feature_mode(menu_item_t item) { + printf("[APP] Enter Feature Mode: %s (%d)\n", MENU_NAMES[item], item); // force display refresh app.last_disp_sent[0] = '\0'; // clean buffer for cmds @@ -1742,33 +2039,44 @@ void enter_feature_mode(menu_item_t item) { gpib_remote_enable(1); // make sure REN is asserted - // we will hold the decoded state here for REL/STATS - dmm_decoded_state_t saved_cfg; - switch (item) { - case MENU_REL: - if (app.has_saved_state) { - decode_dmm_state_bytes(app.saved_state_bytes, &saved_cfg); - build_restoration_string(scratch.cmd.line_buf, &saved_cfg); - strcat(scratch.cmd.line_buf, - HP3478A_TRIG_INTERNAL HP3478A_CMD_MASK_BTN_DATA - HP3478A_CMD_SRQ_CLEAR); - strcpy(app.data.rel.unit, saved_cfg.unit_str); - } else { - // we have no saved state? don't know relative to WHAT - dmm_display("ERR NO STATE", HP3478A_DISP_TEXT_FAST); + case MENU_AUTOHOLD: + // Assuming you track autohold units in the REL struct or dedicated one + if (init_restored_state(app.data.autohold.unit) == 0) { + app.current_mode = MODE_FEAT_AUTOHOLD; + app.data.autohold.stable_count = 0; + app.data.autohold.is_populated = false; + dmm_display("AUTO HOLD", HP3478A_DISP_TEXT_FAST); } + break; - gpib_send(app.dmm_addr, scratch.cmd.line_buf); + case MENU_REL: + if (init_restored_state(app.data.rel.unit) == 0) { + app.current_mode = MODE_FEAT_REL; + app.data.rel.offset = 0.0; + dmm_display("REL MODE", HP3478A_DISP_TEXT_FAST); + } + break; - app.current_mode = MODE_FEAT_REL; - app.data.rel.offset = 0.0; - dmm_display("REL MODE", HP3478A_DISP_TEXT_FAST); + case MENU_STATS: + if (init_restored_state(app.data.stats.unit) == 0) { + app.current_mode = MODE_FEAT_STATS; + + // Initialize Stats + app.data.stats.min = DBL_MAX; // start impossible high + app.data.stats.max = -DBL_MAX; // start impossible low + app.data.stats.sum = 0.0; + app.data.stats.count = 0; + app.data.stats.view_mode = 0; + app.data.stats.disp_timer = millis(); + + dmm_display("STATS INIT", HP3478A_DISP_TEXT_FAST); + } break; case MENU_DBM: // F1=DCV, A1=AutoRange (Critical for dBm), N5=5.5d - gpib_send(app.dmm_addr, + gpib_send(sys_cfg.dmm_addr, HP3478A_MEAS_AC_VOLTS HP3478A_TRIG_INTERNAL HP3478A_CMD_MASK_BTN_DATA HP3478A_CMD_SRQ_CLEAR); app.current_mode = MODE_FEAT_DBM; @@ -1782,7 +2090,7 @@ void enter_feature_mode(menu_item_t item) { // F1: DC Volts // R-2: 30mV Range // Z1: AutoZero On - gpib_send(app.dmm_addr, + gpib_send(sys_cfg.dmm_addr, HP3478A_FUNC_DC_VOLTS HP3478A_RANGE_NEG_2 HP3478A_DIGITS_5_5 HP3478A_AUTOZERO_ON HP3478A_TRIG_INTERNAL HP3478A_CMD_MASK_BTN_DATA HP3478A_CMD_SRQ_CLEAR); @@ -1790,7 +2098,7 @@ void enter_feature_mode(menu_item_t item) { } else if (app.temp_sensor == SENS_THERMISTOR) { // Thermistor: 2-Wire Ohms (Usually standard for NTC) // Range Auto (NTC varies wildy) - gpib_send(app.dmm_addr, + gpib_send(sys_cfg.dmm_addr, HP3478A_FUNC_OHMS_2WIRE HP3478A_RANGE_AUTO HP3478A_DIGITS_5_5 HP3478A_AUTOZERO_ON HP3478A_TRIG_INTERNAL HP3478A_CMD_MASK_BTN_DATA HP3478A_CMD_SRQ_CLEAR); @@ -1804,12 +2112,12 @@ void enter_feature_mode(menu_item_t item) { // F3=2W / F4=4W // R4=30kOhm Range if (app.temp_wire_mode == WIRE_2W) { - gpib_send(app.dmm_addr, + gpib_send(sys_cfg.dmm_addr, HP3478A_FUNC_OHMS_2WIRE HP3478A_RANGE_4 HP3478A_DIGITS_5_5 HP3478A_AUTOZERO_ON HP3478A_TRIG_INTERNAL HP3478A_CMD_MASK_BTN_DATA HP3478A_CMD_SRQ_CLEAR); } else { - gpib_send(app.dmm_addr, + gpib_send(sys_cfg.dmm_addr, HP3478A_FUNC_OHMS_4WIRE HP3478A_RANGE_4 HP3478A_DIGITS_5_5 HP3478A_AUTOZERO_ON HP3478A_TRIG_INTERNAL HP3478A_CMD_MASK_BTN_DATA HP3478A_CMD_SRQ_CLEAR); @@ -1824,7 +2132,7 @@ void enter_feature_mode(menu_item_t item) { case MENU_CONT: // F3: 2W Ohm, R0: 30 Ohm Range, N3: 3.5 Digits (fastest ADC), M21 - gpib_send(app.dmm_addr, + gpib_send(sys_cfg.dmm_addr, HP3478A_FUNC_OHMS_2WIRE HP3478A_RANGE_1 HP3478A_DIGITS_3_5 HP3478A_AUTOZERO_OFF HP3478A_TRIG_INTERNAL HP3478A_CMD_MASK_BTN_DATA HP3478A_CMD_SRQ_CLEAR); @@ -1837,7 +2145,7 @@ void enter_feature_mode(menu_item_t item) { // R3: 3 kOhm Range (1mA Current Through Unknown) // N4: 4.5 Digits // Z0: Auto-Zero OFF - gpib_send(app.dmm_addr, + gpib_send(sys_cfg.dmm_addr, HP3478A_FUNC_OHMS_2WIRE HP3478A_RANGE_3 HP3478A_DIGITS_4_5 HP3478A_AUTOZERO_OFF HP3478A_TRIG_INTERNAL HP3478A_CMD_MASK_BTN_DATA HP3478A_CMD_SRQ_CLEAR); @@ -1845,7 +2153,7 @@ void enter_feature_mode(menu_item_t item) { app.current_mode = MODE_FEAT_DIODE; app.data.diode.connected = 0; - app.data.diode.chirp_start = millis() - DIODE_CHIRP_MS - 1; + app.data.diode.chirp_start = 0; dmm_display("DIODE TEST", HP3478A_DISP_TEXT_FAST); break; @@ -1853,47 +2161,15 @@ void enter_feature_mode(menu_item_t item) { case MENU_XOHM: // H7: High Impedance / Extended Ohm Mode // The DMM puts internal 10M in parallel with input - gpib_send(app.dmm_addr, + gpib_send(sys_cfg.dmm_addr, HP3478A_MEAS_OHMS_EXT HP3478A_DIGITS_5_5 HP3478A_TRIG_INTERNAL HP3478A_CMD_MASK_BTN_DATA HP3478A_CMD_SRQ_CLEAR); app.data.xohm.r1 = 0.0; - app.data.xohm.calibrated = 0; + app.data.xohm.calibrated = false; app.current_mode = MODE_FEAT_XOHM; dmm_display("XOHM 10M REF", HP3478A_DISP_TEXT_FAST); break; - case MENU_STATS: - if (app.has_saved_state) { - decode_dmm_state_bytes(app.saved_state_bytes, &saved_cfg); - build_restoration_string(scratch.cmd.line_buf, &saved_cfg); - strcat(scratch.cmd.line_buf, - HP3478A_TRIG_INTERNAL HP3478A_CMD_MASK_BTN_DATA - HP3478A_CMD_SRQ_CLEAR); - strcpy(app.data.stats.unit, saved_cfg.unit_str); - } else { - // fallback - strcpy(scratch.cmd.line_buf, - HP3478A_FUNC_DC_VOLTS HP3478A_RANGE_AUTO HP3478A_DIGITS_5_5 - HP3478A_AUTOZERO_ON HP3478A_TRIG_INTERNAL - HP3478A_CMD_MASK_BTN_DATA HP3478A_CMD_SRQ_CLEAR); - strcpy(app.data.stats.unit, "VDC"); - } - - gpib_send(app.dmm_addr, scratch.cmd.line_buf); - - app.current_mode = MODE_FEAT_STATS; - - // Initialize Stats - app.data.stats.min = DBL_MAX; // start impossible high - app.data.stats.max = -DBL_MAX; // start impossible low - app.data.stats.sum = 0.0; - app.data.stats.count = 0; - app.data.stats.view_mode = 0; - app.data.stats.disp_timer = millis(); - - dmm_display("STATS INIT", HP3478A_DISP_TEXT_FAST); - break; - case MENU_EXIT: default: exit_to_passthrough(); @@ -1901,26 +2177,26 @@ void enter_feature_mode(menu_item_t item) { } } -void enter_menu_mode(void) { +static void enter_menu_mode(void) { // force display refresh app.last_disp_sent[0] = '\0'; save_dmm_state(); // Trigger Hold (T4) to make sure no new measurements // interrupt our display while we're going through the menu - gpib_send(app.dmm_addr, HP3478A_TRIG_HOLD); + gpib_send(sys_cfg.dmm_addr, HP3478A_TRIG_HOLD); app.current_mode = MODE_MENU; app.menu_pos = MENU_REL; app.data.menu.timer = millis(); dmm_display("M: REL", HP3478A_DISP_TEXT_FAST); - gpib_send(app.dmm_addr, HP3478A_CMD_MASK_BTN_ONLY HP3478A_CMD_SRQ_CLEAR); + gpib_send(sys_cfg.dmm_addr, HP3478A_CMD_MASK_BTN_ONLY HP3478A_CMD_SRQ_CLEAR); } -void handle_feature_logic(void) { +static void handle_feature_logic(void) { uint8_t stb = 0; - gpib_serial_poll(app.dmm_addr, &stb); + gpib_serial_poll(sys_cfg.dmm_addr, &stb); // exit button (SRQ) if (stb & HP3478A_MASK_KEYBOARD_SRQ) { @@ -1932,21 +2208,21 @@ void handle_feature_logic(void) { if (!(stb & HP3478A_MASK_DATA_READY)) return; // read measurement - int len = gpib_receive(app.dmm_addr, scratch.io.raw_data, + int len = gpib_receive(sys_cfg.dmm_addr, scratch.io.raw_data, sizeof(scratch.io.raw_data)); if (len < 0) { // timeout or error - // printf("Read Timeout in Feature\n"); + printf("[FEAT] DMM read timeout in feature. Mode: %d\n", app.current_mode); app.current_mode = MODE_PASSTHROUGH; - app.dmm_online = 0; + app.dmm_online = false; gpib_interface_clear(); return; } double val = parse_double(scratch.io.raw_data); // overload (HP 3478A sends +9.99990E+9 for OL) - int is_overload = 0; - if (val < DMM_OL_NEG_THRESHOLD || val > DMM_OL_THRESHOLD) is_overload = 1; + bool is_overload = false; + if (val < DMM_OL_NEG_THRESHOLD || val > DMM_OL_THRESHOLD) is_overload = true; // RELATIVE MODE if (app.current_mode == MODE_FEAT_REL) { @@ -1959,10 +2235,10 @@ void handle_feature_logic(void) { // valid reading app.data.rel.stable_count++; - if (app.data.rel.stable_count >= REL_STABLE_SAMPLES) { + if (app.data.rel.stable_count >= sys_cfg.rel_stable_count) { app.data.rel.offset = val; dmm_display("NULL SET", HP3478A_DISP_TEXT_FAST); - tone_nb(3000, 50); + tone_nb(sys_cfg.buzzer_chirp_hz, sys_cfg.buzzer_chirp_ms); app.data.rel.stable_count = 0; } else { dmm_display("LOCKING...", HP3478A_DISP_TEXT_FAST); @@ -1983,14 +2259,83 @@ void handle_feature_logic(void) { dmm_display(scratch.disp.full_cmd, HP3478A_DISP_TEXT); } } + } else if (app.current_mode == MODE_FEAT_AUTOHOLD) { + bool is_signal_valid = !is_overload && val > sys_cfg.autohold_min_val; + + if (!is_signal_valid) { + app.data.autohold.stable_count = 0; + + if (!app.data.autohold.is_populated) { + dmm_display("-----", HP3478A_DISP_TEXT_FAST); + } + } else { + // check deviation between current live val and "candidate" + double diff_percent = 0.0; + if (app.data.autohold.stable_count > 0) { + diff_percent = fabs((val - app.data.autohold.candidate_val) / + app.data.autohold.candidate_val) * + 100.0; + } + + if (app.data.autohold.stable_count == 0 || + diff_percent <= sys_cfg.autohold_threshold) { + // reading is within stability window + + // if this is the start of a new seq, set the candidate + if (app.data.autohold.stable_count == 0) { + app.data.autohold.candidate_val = val; + } + + app.data.autohold.stable_count++; + + // latch trigger + if (app.data.autohold.stable_count >= sys_cfg.autohold_stable_count) { + // diff between new candidate and old latched val + double change_from_displayed = 0.0; + if (app.data.autohold.is_populated) { + change_from_displayed = fabs((app.data.autohold.candidate_val - + app.data.autohold.latched_val) / + app.data.autohold.latched_val) * + 100.0; + } + + // only update display (and beep) if: + // 1. we haven't shown anything yet + // 2. the new stable value is (>AUTOHOLD_CHANGE_REQ%) from the old val + if (!app.data.autohold.is_populated || + change_from_displayed > sys_cfg.autohold_change_req) { + // latch it + app.data.autohold.latched_val = app.data.autohold.candidate_val; + app.data.autohold.is_populated = true; + + format_metric_value( + scratch.disp.full_cmd, sizeof(scratch.disp.full_cmd), + app.data.autohold.latched_val, app.data.autohold.unit, 1); + dmm_display(scratch.disp.full_cmd, HP3478A_DISP_TEXT); + + // chirp + tone_nb(sys_cfg.buzzer_chirp_hz, sys_cfg.buzzer_chirp_ms); + } + + // cap counter + app.data.autohold.stable_count = sys_cfg.autohold_stable_count; + } + } else { + // jitter/unstable - reset + app.data.autohold.stable_count = 0; + app.data.autohold.candidate_val = val; + } + } } + // dBm MODE + // TODO: different references else if (app.current_mode == MODE_FEAT_DBM) { if (is_overload) { dmm_display("O.VLD", HP3478A_DISP_TEXT_FAST); } else { // P(mW) = (V^2 / 50) * 1000 = V^2 * 20 - double p_mw = (val * val * 20.0); + double p_mw = (val * val / sys_cfg.dbm_ref_z) * 1000; if (p_mw < 1e-9) { // Align -INF to look consistent @@ -2028,18 +2373,18 @@ void handle_feature_logic(void) { // Cold Junction Compensation (CJC) if (app.env_sensor_present) { t_amb = app.current_env.temp_c_x100 / 100.0; - t_amb -= CJC_SELF_HEATING_OFFSET; + t_amb -= sys_cfg.cjc_self_heating_offset; unit_str = "C (K)"; } else { - t_amb = CJC_FALLBACK_TEMP; + t_amb = sys_cfg.cjc_fallback_temp; unit_str = "C (K*)"; // '*' means fallback CJC used } // Type K response is actually NOT linear, this should be a LUT or a // polynomial calculation // Temp = Ambient + (V_meas * Sensitivity) - temp_c = t_amb + (val * TYPE_K_SCALE); + temp_c = t_amb + (val * sys_cfg.type_k_scale); } } // PT1000 RTD (Callendar-Van Dusen) @@ -2048,9 +2393,9 @@ void handle_feature_logic(void) { dmm_display("SHORT", HP3478A_DISP_TEXT_FAST); return; } else { - double c = RTD_R0 - val; - double b = RTD_R0 * RTD_A; - double a = RTD_R0 * RTD_B; + double c = sys_cfg.rtd_r0 - val; + double b = sys_cfg.rtd_r0 * sys_cfg.rtd_a; + double a = sys_cfg.rtd_r0 * sys_cfg.rtd_b; double disc = (b * b) - (4 * a * c); if (disc >= 0) @@ -2088,7 +2433,6 @@ void handle_feature_logic(void) { unit_str = "C NTC"; } - // Display: "24.5 C" (or "24.5 C (K)") format_metric_value(scratch.disp.full_cmd, sizeof(scratch.disp.full_cmd), temp_c, unit_str, 0); dmm_display(scratch.disp.full_cmd, HP3478A_DISP_TEXT); @@ -2096,13 +2440,13 @@ void handle_feature_logic(void) { } // CONT MODE else if (app.current_mode == MODE_FEAT_CONT) { - int is_short = (!is_overload && val < CONT_THRESHOLD_OHMS); + int is_short = (!is_overload && val < sys_cfg.cont_threshold_ohms); // instant beep - buzzer_set(is_short ? 2500 : 0); + buzzer_hw_set(is_short ? sys_cfg.buzzer_cont_hz : 0); uint32_t now = millis(); - if (now - app.data.cont.last_disp_update > CONT_DISP_UPDATE_MS) { + if (now - app.data.cont.last_disp_update > sys_cfg.cont_disp_update_ms) { app.data.cont.last_disp_update = now; if (is_overload) { @@ -2118,10 +2462,10 @@ void handle_feature_logic(void) { else if (app.current_mode == MODE_FEAT_DIODE) { uint32_t now = millis(); double voltage = is_overload ? 9.9 : (val / 1000.0); - uint8_t is_valid_signal = - (voltage > DIODE_TH_SHORT && voltage < DIODE_TH_OPEN); + bool is_valid_signal = + (voltage > sys_cfg.diode_th_short && voltage < sys_cfg.diode_th_open); - if (voltage < DIODE_TH_OPEN) { + if (voltage < sys_cfg.diode_th_open) { format_metric_value(scratch.disp.full_cmd, sizeof(scratch.disp.full_cmd), voltage, "VDC", 1); dmm_display(scratch.disp.full_cmd, HP3478A_DISP_TEXT); @@ -2139,17 +2483,18 @@ void handle_feature_logic(void) { app.data.diode.chirp_start = now; } else { // strictly either open or short - next_state = - (voltage >= DIODE_TH_OPEN) ? DIODE_STATE_OPEN : DIODE_STATE_SHORT; + next_state = (voltage >= sys_cfg.diode_th_open) ? DIODE_STATE_OPEN + : DIODE_STATE_SHORT; } break; case DIODE_STATE_CHECKING: if (!is_valid_signal) { next_state = DIODE_STATE_SHORT; // signal lost/glitch - } else if ((now - app.data.diode.chirp_start) >= DIODE_STABLE_MS) { + } else if ((now - app.data.diode.chirp_start) >= + sys_cfg.diode_stable_ms) { // has been stable long enough - tone_nb(2500, DIODE_CHIRP_MS); + tone_nb(sys_cfg.buzzer_chirp_hz, sys_cfg.buzzer_chirp_ms); next_state = DIODE_STATE_DONE; } break; @@ -2172,12 +2517,12 @@ void handle_feature_logic(void) { // XOHM MODE else if (app.current_mode == MODE_FEAT_XOHM) { // cal phase, measure the internal 10M resistor - if (app.data.xohm.calibrated == 0) { + if (!app.data.xohm.calibrated) { // need the probes to be open. Internal R is ~10M if (val > 8.0e6 && val < 12.0e6) { app.data.xohm.r1 = val; // Store R1 - app.data.xohm.calibrated = 1; - tone_nb(3000, 100); + app.data.xohm.calibrated = true; + tone_nb(sys_cfg.buzzer_chirp_hz, sys_cfg.buzzer_chirp_ms); } else { dmm_display("OPEN PROBES", HP3478A_DISP_TEXT_FAST); } @@ -2210,7 +2555,7 @@ void handle_feature_logic(void) { // rotate display uint32_t now = millis(); - if (now - app.data.stats.disp_timer > STATS_CYCLE_TIME_MS) { + if (now - app.data.stats.disp_timer > sys_cfg.stats_cycle_time_ms) { app.data.stats.view_mode++; if (app.data.stats.view_mode > 3) app.data.stats.view_mode = 0; app.data.stats.disp_timer = now; @@ -2280,7 +2625,7 @@ void handle_feature_logic(void) { // gens the base string (e.g., "M: TEMP", "S: TYPE K") into // scratch.disp.full_cmd -void prepare_menu_base_string(void) { +static void prepare_menu_base_string(void) { const char* prefix = ""; const char* name = "???"; @@ -2302,7 +2647,14 @@ void prepare_menu_base_string(void) { name); } -void handle_menu_navigation(void) { +inline static int get_menu_max_items(void) { + if (app.data.menu.layer == SUBMENU_TEMP_SENS) return SENS_MAX_ITEMS; + if (app.data.menu.layer == SUBMENU_TEMP_WIRE) return WIRE_MAX_ITEMS; + if (app.data.menu.layer == SUBMENU_TEMP_NTC) return NTC_MAX_ITEMS; + return MENU_MAX_ITEMS; +} + +static void handle_menu_navigation(void) { uint32_t now = millis(); uint32_t elapsed = now - app.data.menu.timer; @@ -2311,32 +2663,21 @@ void handle_menu_navigation(void) { if (gpib_check_srq()) { uint8_t stb = 0; - if (gpib_serial_poll(app.dmm_addr, &stb) != 0) return; + if (gpib_serial_poll(sys_cfg.dmm_addr, &stb) != 0) return; // check if it was the front panel btn if (stb & HP3478A_MASK_KEYBOARD_SRQ) { // reset timer app.data.menu.timer = now; app.menu_pos++; - - int max_items = 0; - if (app.data.menu.layer == SUBMENU_NONE) - max_items = MENU_MAX_ITEMS; - else if (app.data.menu.layer == SUBMENU_TEMP_SENS) - max_items = SENS_MAX_ITEMS; - else if (app.data.menu.layer == SUBMENU_TEMP_WIRE) - max_items = WIRE_MAX_ITEMS; - else if (app.data.menu.layer == SUBMENU_TEMP_NTC) - max_items = NTC_MAX_ITEMS; - - if (app.menu_pos >= max_items) app.menu_pos = 0; + if (app.menu_pos >= get_menu_max_items()) app.menu_pos = 0; // update display prepare_menu_base_string(); dmm_display(scratch.disp.full_cmd, HP3478A_DISP_TEXT_FAST); // re-arm SRQ - gpib_send(app.dmm_addr, + gpib_send(sys_cfg.dmm_addr, HP3478A_CMD_MASK_BTN_ONLY HP3478A_CMD_SRQ_CLEAR); return; @@ -2347,16 +2688,16 @@ void handle_menu_navigation(void) { prepare_menu_base_string(); // only calculate dots if we are past the initial delay - if (elapsed > MENU_SUBLAYER_DELAY_MS) { - uint32_t dot_time = elapsed - MENU_SUBLAYER_DELAY_MS; - int dots = dot_time / MENU_DOT_INTERVAL_MS; + if (elapsed > sys_cfg.menu_sublayer_delay_ms) { + uint32_t dot_time = elapsed - sys_cfg.menu_sublayer_delay_ms; + int dots = dot_time / sys_cfg.menu_dot_interval_ms; if (dots > 3) dots = 3; for (int i = 0; i < dots; i++) strcat(scratch.disp.full_cmd, "."); } dmm_display(scratch.disp.full_cmd, HP3478A_DISP_TEXT_FAST); - if (elapsed > MENU_COMMIT_DELAY_MS) { + if (elapsed > sys_cfg.menu_commit_delay_ms) { // L0: main menu if (app.data.menu.layer == SUBMENU_NONE) { if (app.menu_pos == MENU_TEMP) { @@ -2412,101 +2753,6 @@ void handle_menu_navigation(void) { } } -void app_loop(void) { - uint32_t now = millis(); - - if (app.beep_active) { - if ((now - app.beep_start_ts) >= app.beep_duration) { - buzzer_set(0); - app.beep_active = 0; - } - } - - // Passthrough - if (app.current_mode == MODE_PASSTHROUGH) { - int srq_asserted = gpib_check_srq(); - int time_to_poll = (now - app.last_poll_time) >= app.poll_interval; - - // if disconnected, we only try to reconnect on timer ticks - if (!app.dmm_online && !time_to_poll) return; - // if online, we poll if SRQ is pulled OR timer expires - if (app.dmm_online && !srq_asserted && !time_to_poll) return; - - app.last_poll_time = now; - - uint8_t stb = 0; - // try to talk to DMM - int poll_result = gpib_serial_poll(app.dmm_addr, &stb); - - if (poll_result != 0) { - // poll failed (Timeout/NACK) - if (app.dmm_online) { - // printf("DMM Lost connection.\n"); - app.dmm_online = 0; - } - // slow down polling when offline - app.poll_interval = DMM_RECOVERY_DELAY_MS; - return; - } - - // got a valid response, check if this is a recovery - if (!app.dmm_online) { - // printf("DMM Recovered.\n"); - // only assert REN here when it's online - gpib_remote_enable(1); - gpib_interface_clear(); - - app.dmm_online = 1; - gpib_send(app.dmm_addr, HP3478A_CMD_MASK_BTN_ONLY HP3478A_CMD_SRQ_CLEAR); - gpib_go_to_local(app.dmm_addr); - tone_nb(4000, 50); - app.poll_interval = POLL_INTERVAL_MS; // restore fast polling - return; - } - - // valid and online, check buttons - if (stb & HP3478A_MASK_KEYBOARD_SRQ && - (now - app.ignore_input_start_ts) > MENU_LOCKOUT_MS) { - enter_menu_mode(); - } - - return; - } - - // Nav - if (app.current_mode == MODE_MENU) { - handle_menu_navigation(); - return; - } - - // Features - // early exit if no SRQ - if (!gpib_check_srq()) { - return; - } - - uint8_t stb; - if (gpib_serial_poll(app.dmm_addr, &stb) != 0) { - // DMM crashed during feature mode - // printf("Feature crash: DMM Lost\n"); - app.current_mode = MODE_PASSTHROUGH; - app.dmm_online = 0; - gpib_interface_clear(); - return; - } - - // check exit button first (priority) - if (stb & HP3478A_MASK_KEYBOARD_SRQ) { - exit_to_passthrough(); - return; - } - - // handle measurement data ready - if (stb & HP3478A_MASK_DATA_READY) { - handle_feature_logic(); - } -} - static void cmd_help(void) { static const char* help_text = "\r\nHP3478A Internal USB-GPIB " FW_VERSION @@ -2521,7 +2767,12 @@ static void cmd_help(void) { " ++eor <0-7> Read Stop: 0:CRLF ... 7:EOI-Only\r\n" " ++eot_enable Append extra char to read output\r\n" " ++eot_char The char to append\r\n" - " ++savecfg Save current config\r\n" + "\r\n" + "[System Configuration]\r\n" + " ++config List all configurable parameters\r\n" + " ++get Get parameter value\r\n" + " ++set Set parameter value\r\n" + " ++savecfg Save config to flash\r\n" " ++ver Firmware Version\r\n" " ++rst System Reset\r\n" "\r\n" @@ -2538,8 +2789,9 @@ static void cmd_help(void) { " ++llo Local Lockout\r\n" "\r\n" "[Internal HP3478A Features]\r\n" - " ++cont, ++temp, ++rel, ++xohm, ++dbm\r\n" - " ++diode, ++math, ++norm\r\n" + " ++cont, ++hold, ++rel, ++xohm\r\n" + " ++dbm, ++diode, ++math, ++norm\r\n" + " ++temp Temperature sensor mode\r\n" " ++env [temp|hum] Internal AHT20 Sensor\r\n" " ++disp Write text to LCD\r\n" "\r\n"; @@ -2559,9 +2811,9 @@ static void cmd_status(void) { "EOI : %d\r\n" "EOT : %s (%d)\r\n" "SRQ : %d\r\n", - app.target_addr, app.gpib_timeout_ms, app.auto_read, app.eos_mode, - app.eor_mode, app.eoi_assert, app.eot_enable ? "ON" : "OFF", - app.eot_char, srq); + sys_cfg.target_addr, sys_cfg.gpib_timeout_ms, sys_cfg.auto_read, + sys_cfg.eos_mode, sys_cfg.eor_mode, sys_cfg.eoi_assert, + sys_cfg.eot_enable ? "ON" : "OFF", sys_cfg.eot_char, srq); usb_send_text(scratch.cmd.fmt_buf); } @@ -2572,9 +2824,9 @@ static void process_command(void) { } char* p_cmd = skip_spaces(scratch.cmd.line_buf); - int is_cpp_cmd = (strncmp(p_cmd, "++", 2) == 0); + menu_item_t menu_item = MENU_EXIT; - if (is_cpp_cmd) { + if (strncmp(p_cmd, "++", 2) == 0) { p_cmd += 2; char* p_args = p_cmd; while (*p_args && !isspace((unsigned char)*p_args)) { @@ -2595,13 +2847,13 @@ static void process_command(void) { if (*p_args) { int addr = atoi(p_args); if (addr >= 0 && addr <= 30) { - app.target_addr = addr; + sys_cfg.target_addr = addr; } else usb_send_text("ERR: Invalid Addr\r\n"); } else { // if no arg provided, show current snprintf(scratch.cmd.fmt_buf, sizeof(scratch.cmd.fmt_buf), "%d\r\n", - app.target_addr); + sys_cfg.target_addr); usb_send_text(scratch.cmd.fmt_buf); } break; @@ -2612,11 +2864,11 @@ static void process_command(void) { // Allow 0, 1, 2. Ignore 3 since we don't support it // it's possible but.. effort if (val >= 0 && val <= 2) { - app.auto_read = val; + sys_cfg.auto_read = val; } } else { snprintf(scratch.cmd.fmt_buf, sizeof(scratch.cmd.fmt_buf), "%d\r\n", - app.auto_read); + sys_cfg.auto_read); usb_send_text(scratch.cmd.fmt_buf); } break; @@ -2636,37 +2888,37 @@ static void process_command(void) { case CMD_EOI: if (*p_args) { - app.eoi_assert = (atoi(p_args) ? 1 : 0); + sys_cfg.eoi_assert = (atoi(p_args) ? true : false); } else { - usb_send_text(app.eoi_assert ? "1\r\n" : "0\r\n"); + usb_send_text(sys_cfg.eoi_assert ? "1\r\n" : "0\r\n"); } break; case CMD_EOS: if (*p_args) { int val = atoi(p_args); - if (val >= 0 && val <= 3) app.eos_mode = val; + if (val >= 0 && val <= 3) sys_cfg.eos_mode = val; } else { snprintf(scratch.cmd.fmt_buf, sizeof(scratch.cmd.fmt_buf), "%d\r\n", - app.eos_mode); + sys_cfg.eos_mode); usb_send_text(scratch.cmd.fmt_buf); } break; case CMD_EOT_ENABLE: if (*p_args) { - app.eot_enable = (atoi(p_args) ? 1 : 0); + sys_cfg.eot_enable = (atoi(p_args) ? true : false); } else { - usb_send_text(app.eot_enable ? "1\r\n" : "0\r\n"); + usb_send_text(sys_cfg.eot_enable ? "1\r\n" : "0\r\n"); } break; case CMD_EOT_CHAR: if (*p_args) { - app.eot_char = (uint8_t)atoi(p_args); + sys_cfg.eot_char = (uint8_t)atoi(p_args); } else { snprintf(scratch.cmd.fmt_buf, sizeof(scratch.cmd.fmt_buf), "%d\r\n", - app.eot_char); + sys_cfg.eot_char); usb_send_text(scratch.cmd.fmt_buf); } break; @@ -2675,11 +2927,11 @@ static void process_command(void) { if (*p_args) { int val = atoi(p_args); if (val >= 0 && val <= 7) { - app.eor_mode = val; + sys_cfg.eor_mode = val; } } else { snprintf(scratch.cmd.fmt_buf, sizeof(scratch.cmd.fmt_buf), "%d\r\n", - app.eor_mode); + sys_cfg.eor_mode); usb_send_text(scratch.cmd.fmt_buf); } break; @@ -2699,9 +2951,93 @@ static void process_command(void) { break; case CMD_SAVECFG: - // usb_send_text("OK\r\n"); + config_save(); break; + case CMD_CFG_LIST: { + // dump all config values + for (size_t i = 0; i < CONFIG_MAP_SIZE; i++) { + const cfg_field_t* f = &CONFIG_MAP[i]; + uint8_t* base_addr = (uint8_t*)&sys_cfg; + void* p_val = (void*)(base_addr + f->offset); + + usb_send_text(f->name); + usb_send_text(": "); + + if (f->type == CFG_TYPE_UINT8) { + snprintf(scratch.cmd.fmt_buf, 64, "%u\r\n", *(uint8_t*)p_val); + } else if (f->type == CFG_TYPE_UINT32) { + snprintf(scratch.cmd.fmt_buf, 64, "%lu\r\n", *(uint32_t*)p_val); + } else if (f->type == CFG_TYPE_DOUBLE) { + double_to_str(scratch.cmd.fmt_buf, 64, *(double*)p_val, 6); + strcat(scratch.cmd.fmt_buf, "\r\n"); + } + usb_send_text(scratch.cmd.fmt_buf); + } + break; + } + + case CMD_CFG_GET: { + char* arg_name = p_args; + // terminate arg at next space + char* p_end = strchr(arg_name, ' '); + if (p_end) *p_end = 0; + + const cfg_field_t* f = find_config_field(arg_name); + if (!f) { + usb_send_text("ERR: Param not found\r\n"); + } else { + uint8_t* base_addr = (uint8_t*)&sys_cfg; + void* p_val = (void*)(base_addr + f->offset); + + if (f->type == CFG_TYPE_UINT8) { + snprintf(scratch.cmd.fmt_buf, 64, "%u\r\n", *(uint8_t*)p_val); + } else if (f->type == CFG_TYPE_UINT32) { + snprintf(scratch.cmd.fmt_buf, 64, "%lu\r\n", *(uint32_t*)p_val); + } else if (f->type == CFG_TYPE_DOUBLE) { + double_to_str(scratch.cmd.fmt_buf, 64, *(double*)p_val, + 6); /* precision 6 */ + strcat(scratch.cmd.fmt_buf, "\r\n"); + } + + usb_send_text(scratch.cmd.fmt_buf); + } + break; + } + + case CMD_CFG_SET: { + // syntax: ++set + char* arg_name = p_args; + char* arg_val = strchr(arg_name, ' '); + + if (!arg_val) { + usb_send_text("ERR: Missing value\r\n"); + return; + } + *arg_val = 0; + arg_val++; + arg_val = skip_spaces(arg_val); + + const cfg_field_t* f = find_config_field(arg_name); + if (!f) { + usb_send_text("ERR: Param not found\r\n"); + } else { + uint8_t* base_addr = (uint8_t*)&sys_cfg; + void* p_dest = (void*)(base_addr + f->offset); + + if (f->type == CFG_TYPE_UINT8) { + *(uint8_t*)p_dest = (uint8_t)atoi(arg_val); + } else if (f->type == CFG_TYPE_UINT32) { + *(uint32_t*)p_dest = (uint32_t)strtoul(arg_val, NULL, 10); + } else if (f->type == CFG_TYPE_DOUBLE) { + *(double*)p_dest = parse_double(arg_val); + } + + config_apply_to_app(); + } + break; + } + case CMD_SRQ: usb_send_text(gpib_check_srq() ? "1\r\n" : "0\r\n"); break; @@ -2711,9 +3047,9 @@ static void process_command(void) { goto do_read_operation; case CMD_WRITE: if (*p_args) { - gpib_send(app.target_addr, p_args); + gpib_send(sys_cfg.target_addr, p_args); // if auto is 1 (Read-after-write), read now - if (app.auto_read == 1) goto do_read_operation; + if (sys_cfg.auto_read == 1) goto do_read_operation; } break; @@ -2723,21 +3059,21 @@ static void process_command(void) { int val = atoi(p_args); // min 1ms, max 60s if (val > 0 && val < 60000) { - app.gpib_timeout_ms = val; + sys_cfg.gpib_timeout_ms = val; } else { usb_send_text("ERR: Range 1-60000\r\n"); } } else { snprintf(scratch.cmd.fmt_buf, sizeof(scratch.cmd.fmt_buf), "%lu\r\n", - app.gpib_timeout_ms); + sys_cfg.gpib_timeout_ms); usb_send_text(scratch.cmd.fmt_buf); } break; case CMD_TRG: - gpib_trigger(app.target_addr); + gpib_trigger(sys_cfg.target_addr); break; case CMD_CLR: - gpib_device_clear(app.target_addr); + gpib_device_clear(sys_cfg.target_addr); break; case CMD_DCL: gpib_universal_clear(); @@ -2749,7 +3085,7 @@ static void process_command(void) { gpib_local_lockout(); break; case CMD_GTL: - gpib_go_to_local(app.target_addr); + gpib_go_to_local(sys_cfg.target_addr); break; case CMD_LOC: gpib_remote_enable(0); @@ -2763,7 +3099,7 @@ static void process_command(void) { break; case CMD_SPOLL: { - uint8_t poll_addr = (*p_args) ? atoi(p_args) : app.target_addr; + uint8_t poll_addr = (*p_args) ? atoi(p_args) : sys_cfg.target_addr; uint8_t stb; if (gpib_serial_poll(poll_addr, &stb) == 0) { snprintf(scratch.cmd.fmt_buf, sizeof(scratch.cmd.fmt_buf), "%d\r\n", @@ -2834,31 +3170,70 @@ static void process_command(void) { // feat entry case CMD_CONT: - case CMD_TEMP: - case CMD_REL: - case CMD_XOHM: - case CMD_DBM: - case CMD_DIODE: - case CMD_MATH: { - menu_item_t item = MENU_EXIT; - if (cmd_id == CMD_CONT) - item = MENU_CONT; - else if (cmd_id == CMD_TEMP) - item = MENU_TEMP; - else if (cmd_id == CMD_REL) - item = MENU_REL; - else if (cmd_id == CMD_XOHM) - item = MENU_XOHM; - else if (cmd_id == CMD_DBM) - item = MENU_DBM; - else if (cmd_id == CMD_DIODE) - item = MENU_DIODE; - else if (cmd_id == CMD_MATH) - item = MENU_STATS; + menu_item = MENU_CONT; + break; + case CMD_AUTOHOLD: + menu_item = MENU_AUTOHOLD; + break; + case CMD_TEMP: { + // syntax: +temp [config] + menu_item = MENU_TEMP; - save_dmm_state(); // save before changing - enter_feature_mode(item); - usb_send_text("OK\r\n"); + if (*p_args == 0) { + usb_send_text( + "Syntax: ++temp [config]\r\n" + "0 PT1000\r\n" + "1 Thermistor [0..11]\r\n" + "2 Type K [0=2Wire, 1=4Wire]\r\n"); + return; + } + + char* next; + long l1 = strtol(p_args, &next, 10); + long l2 = strtol(next, NULL, 10); + + if (l1 < 0 || l1 >= SENS_MAX_ITEMS) { + snprintf(scratch.cmd.fmt_buf, sizeof(scratch.cmd.fmt_buf), + "ERR: max %d sensor idx\r\n", SENS_MAX_ITEMS - 1); + usb_send_text(scratch.cmd.fmt_buf); + return; + } + if (l1 == SENS_THERMISTOR) { + if (l2 < 0 || l2 >= NTC_MAX_ITEMS) { + snprintf(scratch.cmd.fmt_buf, sizeof(scratch.cmd.fmt_buf), + "ERR: max %d preset idx\r\n", NTC_MAX_ITEMS - 1); + usb_send_text(scratch.cmd.fmt_buf); + return; + } + app.temp_ntc_preset = (ntc_preset_t)l2; + } else if (l1 == SENS_TYPE_K) { + if (l2 < 0 || l2 >= WIRE_MAX_ITEMS) { + snprintf(scratch.cmd.fmt_buf, sizeof(scratch.cmd.fmt_buf), + "ERR: 0 = 2W, 1 = 4W mode\r\n"); + usb_send_text(scratch.cmd.fmt_buf); + return; + } + app.temp_wire_mode = (wire_mode_t)l2; + } + + app.temp_sensor = (temp_sensor_t)l1; + break; + } + + case CMD_REL: + menu_item = MENU_REL; + break; + case CMD_XOHM: + menu_item = MENU_XOHM; + break; + case CMD_DBM: + menu_item = MENU_DBM; + break; + case CMD_DIODE: + menu_item = MENU_DIODE; + break; + case CMD_MATH: { + menu_item = MENU_STATS; break; } @@ -2866,11 +3241,19 @@ static void process_command(void) { usb_send_text("ERR: Unknown cmd\r\n"); break; } + + // if it was an internal cmd + if (menu_item != MENU_EXIT) { + save_dmm_state(); // save before changing + enter_feature_mode(menu_item); + usb_send_text("OK\r\n"); + } + return; } // passthrough mode (not "++") - if (gpib_send(app.target_addr, p_cmd) < 0) { + if (gpib_send(sys_cfg.target_addr, p_cmd) < 0) { usb_send_text("ERR: Send Fail\r\n"); return; } @@ -2878,14 +3261,14 @@ static void process_command(void) { // 2. Auto-Read Logic // If Auto == 1: Always Read // If Auto == 2: Read only if command ends with '?' - if (app.auto_read == 1 || (app.auto_read == 2 && is_query(p_cmd))) { + if (sys_cfg.auto_read == 1 || (sys_cfg.auto_read == 2 && is_query(p_cmd))) { goto do_read_operation; } return; do_read_operation: { - int len = gpib_receive(app.target_addr, scratch.io.raw_data, + int len = gpib_receive(sys_cfg.target_addr, scratch.io.raw_data, sizeof(scratch.io.raw_data)); if (len > 0) { usb_send_text(scratch.io.raw_data); @@ -2896,28 +3279,127 @@ do_read_operation: { } } +static void app_loop(void) { + uint32_t now = millis(); + + if (app.tone_timer_pending) { + if ((now - app.tone_start_ts) >= app.tone_duration) { + buzzer_hw_set(0); + app.tone_timer_pending = false; + } + } + + // Passthrough + if (app.current_mode == MODE_PASSTHROUGH) { + int srq_asserted = gpib_check_srq(); + int time_to_poll = (now - app.last_poll_time) >= sys_cfg.poll_interval_ms; + + // if disconnected, we only try to reconnect on timer ticks + if (!app.dmm_online && !time_to_poll) return; + // if online, we poll if SRQ is pulled OR timer expires + if (app.dmm_online && !srq_asserted && !time_to_poll) return; + + app.last_poll_time = now; + + uint8_t stb = 0; + // try to talk to DMM + int poll_result = gpib_serial_poll(sys_cfg.dmm_addr, &stb); + + if (poll_result != 0) { + // poll failed (Timeout/NACK) + if (app.dmm_online) { + // printf("DMM Lost connection.\n"); + app.dmm_online = false; + } + // slow down polling when offline + sys_cfg.poll_interval_ms = sys_cfg.dmm_recovery_delay_ms; + return; + } + + // got a valid response, check if this is a recovery + if (!app.dmm_online) { + // printf("DMM Recovered.\n"); + // only assert REN here when it's online + gpib_remote_enable(1); + gpib_interface_clear(); + + app.dmm_online = true; + play_tune(ONLINE_NOTES); + + gpib_send(sys_cfg.dmm_addr, + HP3478A_CMD_MASK_BTN_ONLY HP3478A_CMD_SRQ_CLEAR); + gpib_go_to_local(sys_cfg.dmm_addr); + sys_cfg.poll_interval_ms = + sys_cfg.poll_interval_ms; // restore fast polling + return; + } + + // valid and online, check buttons + if (stb & HP3478A_MASK_KEYBOARD_SRQ && + (now - app.ignore_input_start_ts) > sys_cfg.menu_lockout_ms) { + enter_menu_mode(); + } + + return; + } + + // Nav + if (app.current_mode == MODE_MENU) { + handle_menu_navigation(); + return; + } + + // Features + // early exit if no SRQ + if (!gpib_check_srq()) { + return; + } + + uint8_t stb; + if (gpib_serial_poll(sys_cfg.dmm_addr, &stb) != 0) { + // DMM crashed during feature mode + // printf("Feature crash: DMM Lost\n"); + app.current_mode = MODE_PASSTHROUGH; + app.dmm_online = false; + gpib_interface_clear(); + return; + } + + // check exit button first (priority) + if (stb & HP3478A_MASK_KEYBOARD_SRQ) { + exit_to_passthrough(); + return; + } + + // handle measurement data ready + if (stb & HP3478A_MASK_DATA_READY) { + handle_feature_logic(); + } +} + int main() { SystemInit(); systick_init(); funGpioInitAll(); - // Buzzer setup + config_init(); buzzer_init(); - play_startup_tune(); - - // I2C sensor i2c_init(); - app.env_sensor_present = aht20_init() == AHT20_OK ? 1 : 0; + if (aht20_init() == AHT20_OK) { + app.env_sensor_present = true; + printf("[INIT] AHT20 Sensor OK\n"); + } else { + app.env_sensor_present = false; + printf("[INIT] AHT20 Sensor MISSING\n"); + } - // GPIB controller gpib_init(); - - // USB interface USBFSSetup(); // usb_debug = 1; // app state app.usb_raw_prev = USB_HW_IS_ACTIVE(); + config_apply_to_app(); // init timers uint32_t now = millis(); app.usb_ts = now; @@ -2935,4 +3417,4 @@ int main() { process_command(); } } -} \ No newline at end of file +} diff --git a/systick.c b/systick.c deleted file mode 100644 index 08d8dc2..0000000 --- a/systick.c +++ /dev/null @@ -1,21 +0,0 @@ -#include "systick.h" - -volatile uint32_t systick_millis; - -void systick_init(void) { - SysTick->CTLR = 0x0000; - SysTick->CMP = SysTick->CNT + SYSTICK_ONE_MILLISECOND; - systick_millis = 0; - SysTick->CTLR = SYSTICK_CTLR_STE | // Enable Counter - SYSTICK_CTLR_STIE | // Enable Interrupts - SYSTICK_CTLR_STCLK; // Set Clock Source to HCLK/1 - - NVIC_EnableIRQ(SysTick_IRQn); -} - -void SysTick_Handler(void) __attribute__((interrupt)); -void SysTick_Handler(void) { - SysTick->CMP = SysTick->CNT + SYSTICK_ONE_MILLISECOND; - SysTick->SR = 0; - systick_millis++; -}