diff --git a/ch32fun b/ch32fun index 4679e07..c40a0d5 160000 --- a/ch32fun +++ b/ch32fun @@ -1 +1 @@ -Subproject commit 4679e076f0155817c409500d526422015d21c7d6 +Subproject commit c40a0d52d47a2f64668474f6b61ece6ea87cbedb diff --git a/inc/gpib_defs.h b/inc/gpib_defs.h index 2d1402f..6a1e79e 100644 --- a/inc/gpib_defs.h +++ b/inc/gpib_defs.h @@ -62,6 +62,92 @@ #define GPIB_TAG_BASE 0x40 // Talk Address Group base #define GPIB_SCG_BASE 0x60 // Secondary Command Group base +/* HP3478A */ +// Measurement Function (F) +#define HP3478A_FUNC_DC_VOLTS "F1" +#define HP3478A_FUNC_AC_VOLTS "F2" +#define HP3478A_FUNC_OHMS_2WIRE "F3" +#define HP3478A_FUNC_OHMS_4WIRE "F4" +#define HP3478A_FUNC_DC_CURRENT "F5" +#define HP3478A_FUNC_AC_CURRENT "F6" +#define HP3478A_FUNC_OHMS_EXT "F7" + +// Range (R) +#define HP3478A_RANGE_NEG_2 "R-2" /* 30mV DC */ +#define HP3478A_RANGE_NEG_1 "R-1" /* 300mV (AC/DC) or 300mA (AC/DC) */ +#define HP3478A_RANGE_0 "R0" /* 3V (AC/DC) or 3A (AC/DC) */ +#define HP3478A_RANGE_1 "R1" /* 30V (AC/DC) or 30 Ohm */ +#define HP3478A_RANGE_2 "R2" /* 300V (AC/DC) or 300 Ohm */ +#define HP3478A_RANGE_3 "R3" /* 3K Ohm */ +#define HP3478A_RANGE_4 "R4" /* 30K Ohm */ +#define HP3478A_RANGE_5 "R5" /* 300K Ohm */ +#define HP3478A_RANGE_6 "R6" /* 3M Ohm */ +#define HP3478A_RANGE_7 "R7" /* 30M Ohm */ +#define HP3478A_RANGE_AUTO "RA" /* Autorange */ + +// Display / Integration Time (N) +#define HP3478A_DIGITS_3_5 "N3" /* 3 1/2 digit (Fastest) */ +#define HP3478A_DIGITS_4_5 "N4" /* 4 1/2 digit (1 PLC integration) */ +#define HP3478A_DIGITS_5_5 "N5" /* 5 1/2 digit (Best noise rejection) */ + +// Trigger (T) +#define HP3478A_TRIG_INTERNAL "T1" /* Internal trigger */ +#define HP3478A_TRIG_EXTERNAL "T2" /* External trigger pulse */ +#define HP3478A_TRIG_SINGLE "T3" /* Single trigger (Software/GET) */ +#define HP3478A_TRIG_HOLD "T4" /* Trigger Hold (Idle) */ +#define HP3478A_TRIG_FAST "T5" /* Fast Trigger (No settling delay) */ + +// Autozero (Z) +#define HP3478A_AUTOZERO_OFF "Z0" +#define HP3478A_AUTOZERO_ON "Z1" + +// Write to Display (D) +#define HP3478A_DISP_NORMAL "D1" /* Return to normal measurement display */ +#define HP3478A_DISP_TEXT "D2" /* Prefix: Follow with up to 12 chars */ +#define HP3478A_DISP_TEXT_FAST \ + "D3" /* Like D2, but stops updating (faster read rate) */ + +// Preset Commands (H) +#define HP3478A_PRESET_HOME "H0" +#define HP3478A_MEAS_DC_VOLTS "H1" +#define HP3478A_MEAS_AC_VOLTS "H2" +#define HP3478A_MEAS_OHMS_2WIRE "H3" +#define HP3478A_MEAS_OHMS_4WIRE "H4" +#define HP3478A_MEAS_DC_CURRENT "H5" +#define HP3478A_MEAS_AC_CURRENT "H6" +#define HP3478A_MEAS_OHMS_EXT "H7" + +#define HP3478A_CMD_STATUS_BYTE "B" + +// Serial Poll Register (K) +#define HP3478A_CMD_SRQ_CLEAR "K" + +// Error Register (E) +#define HP3478A_CMD_ERR_READ "E" + +// Front/Rear Switch (S) +#define HP3478A_CMD_SWITCH_READ "S" + +// SRQ Mask (M) +#define HP3478A_CMD_SRQ_MASK_PFX "M" + +#define HP3478A_MASK_DATA_READY 0x01 /* Bit 0: Reading available to bus */ +#define HP3478A_MASK_UNUSED_B1 0x02 /* Bit 1: Not used */ +#define HP3478A_MASK_SYNTAX_ERR 0x04 /* Bit 2: Syntax error */ +#define HP3478A_MASK_HARDWARE_ERR 0x08 /* Bit 3: Hardware error */ +#define HP3478A_MASK_KEYBOARD_SRQ \ + 0x10 /* Bit 4: Front panel 'SRQ' key pressed */ +#define HP3478A_MASK_CAL_FAIL 0x20 /* Bit 5: Calibration procedure failed */ +#define HP3478A_MASK_ZERO_B6 0x40 /* Bit 6: Always zero */ +#define HP3478A_MASK_POWER_ON 0x80 /* Bit 7: PON Switched/Device Clear */ + +// "M20" -> SRQ on Button Only +#define HP3478A_CMD_MASK_BTN_ONLY "M20" +// "M21" -> SRQ on Button OR Data Ready +#define HP3478A_CMD_MASK_BTN_DATA "M21" +// "M00" -> Clear Mask (Disable SRQ) +#define HP3478A_CMD_MASK_CLEAR "M00" + #define GPIB_ASSERT(pin) funDigitalWrite(pin, 0) #define GPIB_RELEASE(pin) funDigitalWrite(pin, 1) #define GPIB_READ(pin) funDigitalRead(pin) diff --git a/inc/usb_config.h b/inc/usb_config.h index 350697a..8dbbd8a 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 0x0100 +#define FUSB_USB_REV 0x0110 #define FUSB_STR_MANUFACTURER u"Open Source GPIB" #define FUSB_STR_PRODUCT u"HP3478A Internal Adapter" -#define FUSB_STR_SERIAL u"3478A-USB-001" +#define FUSB_STR_SERIAL u"3478A-USB-110" //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 94c42fe..9053110 100644 --- a/main.c +++ b/main.c @@ -11,7 +11,7 @@ #include "i2c_bitbang.h" #include "systick.h" -#define FW_VERSION "1.0.0" +#define FW_VERSION "1.1.0" #define MY_ADDR 0 #define DEFAULT_DMM_ADDR 18 // the HP3478A addr @@ -24,95 +24,212 @@ // Timing Config #define USB_DEBOUNCE_CONNECT_MS 50 #define USB_DEBOUNCE_DISCONNECT_MS 200 + #define ENV_SENSOR_READ_INTERVAL_MS 1000 #define GPIB_TIMEOUT_MS 100 -#define MENU_HOVER_COMMIT_MS 2400 -// time between Serial Polls in Passthrough mode -#define POLL_INTERVAL_MS 100 -// dead time after SRQ mask to the DMM -// polling it too soon after recovery causes a timeout -#define DMM_RECOVERY_DELAY_MS 1000 -// PT1000 Constants -#define RTD_A 3.9083e-3 -#define RTD_B -5.775e-7 -#define RTD_R0 1000.0 +// 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 200 // Button press dead-time -// HP3478A Specific Commands -#define HP_CMD_RESET_DISPLAY "D1" -#define HP_CMD_TEXT_DISPLAY "D3" -#define HP_CMD_MASK_BTN_ONLY "M20" // SRQ on Front Panel Button -#define HP_CMD_MASK_BTN_DATA "M21" // SRQ on Data Ready + Button +// Polling +#define POLL_INTERVAL_MS 100 // 10Hz polling when in Passthrough +#define DMM_RECOVERY_DELAY_MS 1000 // Backoff if DMM vanishes -#define CONT_THRESHOLD_OHMS 10.0f -#define HP_OVERLOAD_VAL 9.0e9f // Threshold for +9.99990E+9 -#define REL_STABLE_SAMPLES 3 // samples to wait before locking NULL +// Diode sound +#define DIODE_TH_SHORT 0.050f // Volts (below this = SHORT) +#define DIODE_TH_OPEN 2.500f // 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-3f +#define RTD_B -5.775e-7f +#define RTD_R0 1000.0f + +// Thermistor +// TODO: different ranges? 5k, 10k etc.? +// This is a 5k NTC!!! kinda useless here +#define THERM_A 0.001286f +#define THERM_B 0.00023595f +#define THERM_C 0.0000000941f + +// Thermocouple +#define CJC_FALLBACK_TEMP 22.0f // 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.0f +#define TYPE_K_SCALE 24390.24f // 1 / 41uV +// dBm +#define DBM_REF_Z 50.0f +// Stats +#define STATS_CYCLE_TIME_MS 3000 // time per screen (Live -> Avg -> Min...) +#define STATS_INIT_MIN_VAL 1.0e9f +#define STATS_INIT_MAX_VAL -1.0e9f + +// HP3478A +#define HP_DISP_LEN 13 // 13 chars +#define CONT_THRESHOLD_OHMS 10.0f // continuity beep threshold +#define HP_OVERLOAD_VAL 9.0e9f // HP sends +9.9999E+9 on overload +#define REL_STABLE_SAMPLES 3 // filter depth for Relative NULL 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_CONT, // Continuity Mode active - MODE_FEAT_XOHM // Extended Ohms active + MODE_FEAT_DIODE, // Diode test mode + MODE_FEAT_XOHM, // Extended Ohms active + MODE_FEAT_STATS // avg/min/max } work_mode_t; -static const char* MENU_NAMES[] = {"REL", "CONT", "TEMP", "XOHM", "EXIT"}; - typedef enum { MENU_REL = 0, - MENU_CONT, MENU_TEMP, + MENU_DBM, + MENU_CONT, + MENU_DIODE, MENU_XOHM, + MENU_STATS, MENU_EXIT, MENU_MAX_ITEMS } menu_item_t; +typedef struct { + const char* name; + float a; + float b; + float c; +} thermistor_def_t; + +// UI Strings +static const char* MENU_NAMES[] = {"REL", "TEMP", "DBM", "CONT", + "DIODE", "XOHM", "STATS", "EXIT"}; + +static const char* SENSOR_NAMES[] = {"PT1000", "THERM", "TYPE K"}; + +static const char* WIRE_NAMES[] = {"2-WIRE", "4-WIRE"}; + +// Sub-menu states +typedef enum { + SUBMENU_NONE = 0, + SUBMENU_TEMP_SENS, // step 1: sensor Type + SUBMENU_TEMP_WIRE // step 2: wire mode (skipped for Type K) +} 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; typedef enum { CONT_SHORT = 0, CONT_OPEN } cont_state_t; +typedef enum { + DIODE_STATE_OPEN = 0, // probes open + DIODE_STATE_CHECKING, // voltage @ diode range, checking stability + DIODE_STATE_SHORT, // voltage too low, silent + DIODE_STATE_PLAYING, // is a diode, chirping + DIODE_STATE_DONE // chirped, latched silent +} diode_state_t; + +typedef union { + struct { + float offset; + uint8_t stable_count; + } rel; + + struct { + float min; + float max; + float sum; + uint32_t count; + uint32_t disp_timer; + uint8_t view_mode; + } stats; + + struct { + float r1; + uint8_t calibrated; + } xohm; + + struct { + uint32_t disp_timer; + int last_state; + } cont; + + struct { + uint8_t connected; // touchy? + uint32_t chirp_start; // when we began the touchy + } diode; + + // menu state (only used when in MODE_MENU) + struct { + uint32_t timer; + uint8_t layer; // submenu_state_t + uint8_t sub_pos; // position in submenu + } menu; + +} mode_data_t; + typedef struct { - // USB conn state - int usb_online; // debounced connection state - int usb_raw_prev; // previous raw state - uint32_t usb_ts; // ts for debounce logic + // Hardware State Flags + uint8_t usb_online : 1; + uint8_t usb_raw_prev : 1; + uint8_t env_sensor_present : 1; + uint8_t auto_read : 1; + uint8_t dmm_online : 1; + uint8_t has_saved_state : 1; + uint8_t reserved : 2; - // env sensor - int env_sensor_present; - aht20_data current_env; + // Addresses + uint8_t target_addr; + uint8_t dmm_addr; + + // Timers + uint32_t usb_ts; uint32_t env_last_read; + uint32_t last_poll_time; + uint32_t poll_interval; - // GPIB - uint8_t target_addr; // active target - uint8_t dmm_addr; // specifically the HP3478A - int auto_read; - - // local firmware + // Logic work_mode_t current_mode; - menu_item_t menu_pos; - uint32_t menu_timer; - uint32_t next_poll_time; - int dmm_online; // 0 = offline, 1 = online + int menu_pos; // high-level menu position (REL, TEMP etc.) - // feature stuff - float rel_offset; - uint8_t rel_stable_count; // counter for relative mode stabilization - // XOHM variables - float xohm_r1; // Internal 10M reference value - uint8_t xohm_calibrated; // Flag: 0 = Measuring R1, 1 = Measuring Rx + // Config Selection + temp_sensor_t temp_sensor; + wire_mode_t temp_wire_mode; - // continuity - int cont_last_state; // To dedup display updates - uint32_t cont_disp_timer; // To refresh display occasionally + // Environmental Data + aht20_data current_env; + + // DMM Restore + uint8_t saved_state_bytes[5]; + + // Display shadow buffer + char last_disp_sent[HP_DISP_LEN + 1]; + + // Unionized Mode Data + mode_data_t data; } app_state_t; static app_state_t app = {.dmm_addr = DEFAULT_DMM_ADDR, - .target_addr = DEFAULT_DMM_ADDR}; + .target_addr = DEFAULT_DMM_ADDR, + .poll_interval = POLL_INTERVAL_MS}; // Buffers static char cmd_buffer[128]; static char resp_buffer[256]; static char tmp_buffer[128]; -static char disp_buffer[13]; +static char disp_buffer[HP_DISP_LEN + 1]; +// Size: "D3"(2) + Text(12) + "\n"(1) + Null(1) = 16 bytes +static char disp_cmd_buffer[2 + HP_DISP_LEN + 1 + 1]; // USB Ring Buffer #define USB_RX_BUF_SIZE 512 @@ -148,11 +265,11 @@ static char* skip_spaces(char* str) { void fmt_float(char* buf, size_t size, float val, int precision) { if (val != val) { - snprintf(buf, size, "NaN"); + snprintf(buf, size, "NAN"); return; } if (val > 3.4e38f) { - snprintf(buf, size, "Inf"); + snprintf(buf, size, "INF"); return; } @@ -190,22 +307,61 @@ void fmt_float(char* buf, size_t size, float val, int precision) { } } -void format_resistance(char* buffer, size_t buf_len, float val) { - memset(buffer, 0, buf_len); +// [NUMBER] + [SPACES] + [UNIT] +// assumes buffer is at least HP_DISP_LEN + 1b +void format_aligned_display(char* buffer, float value, int precision, + const char* suffix) { + char num_str[16]; + // fmt number to string + fmt_float(num_str, sizeof(num_str), value, precision); + + int num_len = strlen(num_str); + int suf_len = strlen(suffix); + + // reset buffer to spaces + memset(buffer, ' ', HP_DISP_LEN); + buffer[HP_DISP_LEN] = '\0'; + + // write number (left aligned) + // clamp to max display length + int copy_len = (num_len > HP_DISP_LEN) ? HP_DISP_LEN : num_len; + memcpy(buffer, num_str, copy_len); + + // write suffix (right aligned) + int suf_start = HP_DISP_LEN - suf_len; + if (suf_start < 0) suf_start = 0; + + memcpy(&buffer[suf_start], suffix, suf_len); +} + +void format_resistance(char* buffer, size_t buf_len, float val) { + if (buf_len < HP_DISP_LEN + 1) return; + + float scaled_val; + const char* suffix; + int prec; + + // determine logic if (val >= 1e9f) { - fmt_float(buffer, buf_len, val / 1e9f, 3); - strcat(buffer, " G"); + scaled_val = val / 1e9f; + suffix = " G"; + prec = 4; // "1.1234 G" } else if (val >= 1e6f) { - fmt_float(buffer, buf_len, val / 1e6f, 4); - strcat(buffer, " M"); + scaled_val = val / 1e6f; + suffix = " M"; + prec = 4; // "10.1234 M" } else if (val >= 1e3f) { - fmt_float(buffer, buf_len, val / 1e3f, 3); - strcat(buffer, " K"); + scaled_val = val / 1e3f; + suffix = " K"; + prec = 4; // "100.1234 K" } else { - fmt_float(buffer, buf_len, val, 2); - strcat(buffer, " OHM"); + scaled_val = val; + suffix = " OHM"; + prec = 2; // "100.55 OHM" } + + format_aligned_display(buffer, scaled_val, prec, suffix); } float parse_float(const char* s) { @@ -478,7 +634,7 @@ err: // Bus management -// Assert Interface Clear (IFC) - The "Big Reset Button" +// Assert Interface Clear (IFC) void gpib_interface_clear(void) { GPIB_ASSERT(PIN_IFC); Delay_Ms(1); // IEEE-488 requires >100us @@ -696,6 +852,36 @@ int gpib_receive(uint8_t addr, char* buf, int max_len) { return count; } +// does not stop on newline and does not NULL terminate +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; + uint8_t byte; + + // run until we have the expected count + while (count < expected_len) { + if (gpib_read_byte(&byte, &eoi) < 0) break; // hw timeout + + buf[count++] = byte; + + // only stop on EOI. Ignore '\n' because this is binary data. + if (eoi) break; + } + + // ensure listeners are open before asserting ATN + GPIB_RELEASE(PIN_NDAC); + GPIB_RELEASE(PIN_NRFD); + + // cleanup: ATN -> UNT + GPIB_ASSERT(PIN_ATN); + gpib_write_byte(GPIB_CMD_UNT, 0); + GPIB_RELEASE(PIN_ATN); + + return count; +} + // write then read (for "?" commands) int gpib_query(uint8_t addr, const char* cmd, char* buf, int max_len) { if (gpib_send(addr, cmd) != 0) return -1; @@ -982,63 +1168,280 @@ static void handle_env_sensor(void) { // Helper to write text to HP3478A Display // CMD: D2 = Full alphanumeric message void dmm_display(const char* text) { - snprintf(resp_buffer, 64, "%s%s\n", HP_CMD_TEXT_DISPLAY, text); - gpib_send(app.dmm_addr, resp_buffer); + if (strncmp(app.last_disp_sent, text, HP_DISP_LEN) == 0) { + return; // text hasn't changed + } + + // upd cache + strncpy(app.last_disp_sent, text, HP_DISP_LEN); + app.last_disp_sent[HP_DISP_LEN] = 0; + + // copy command + strcpy(disp_cmd_buffer, HP3478A_DISP_TEXT_FAST); + // append text + strncat(disp_cmd_buffer, text, HP_DISP_LEN); + // newline require for D2 and D3 + strcat(disp_cmd_buffer, "\n"); + gpib_send(app.dmm_addr, disp_cmd_buffer); } static inline void dmm_display_normal(void) { - gpib_send(app.dmm_addr, HP_CMD_RESET_DISPLAY); + gpib_send(app.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) { + gpib_interface_clear(); + gpib_send(app.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; +} + +void restore_dmm_state(void) { + if (!app.has_saved_state) { + gpib_send(app.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); + return; + } + + uint8_t b1 = app.saved_state_bytes[0]; + uint8_t b2 = app.saved_state_bytes[1]; + + // ptrs to string literals + const char* cmd_func; + const char* cmd_range; + const char* cmd_dig; + const char* cmd_az; + + switch ((b1 >> 5) & 0x07) { + case 2: + cmd_func = HP3478A_FUNC_AC_VOLTS; + break; + case 3: + cmd_func = HP3478A_FUNC_OHMS_2WIRE; + break; + case 4: + cmd_func = HP3478A_FUNC_OHMS_4WIRE; + break; + case 5: + cmd_func = HP3478A_FUNC_DC_CURRENT; + break; + case 6: + cmd_func = HP3478A_FUNC_AC_CURRENT; + break; + case 7: + cmd_func = HP3478A_FUNC_OHMS_EXT; + break; + default: + cmd_func = HP3478A_FUNC_DC_VOLTS; + break; + } + + // Decode Range (Bits 4-2) + AutoRange (Byte 2 Bit 1) + if (b2 & 0x02) { + cmd_range = HP3478A_RANGE_AUTO; + } else { + // Octal 1=R-2 ... Octal 7=R4 + switch ((b1 >> 2) & 0x07) { + case 1: + cmd_range = HP3478A_RANGE_NEG_2; + break; + case 2: + cmd_range = HP3478A_RANGE_NEG_1; + break; + case 3: + cmd_range = HP3478A_RANGE_0; + break; + case 4: + cmd_range = HP3478A_RANGE_1; + break; + case 5: + cmd_range = HP3478A_RANGE_2; + break; + case 6: + cmd_range = HP3478A_RANGE_3; + break; + case 7: + cmd_range = HP3478A_RANGE_4; + break; + default: + cmd_range = HP3478A_RANGE_0; + break; + } + } + + // Decode Digits (Bits 1-0) + switch (b1 & 0x03) { + case 2: + cmd_dig = HP3478A_DIGITS_4_5; + break; + case 3: + cmd_dig = HP3478A_DIGITS_3_5; + break; + default: + cmd_dig = HP3478A_DIGITS_5_5; + break; + } + + // Decode AutoZero (Byte 2 Bit 2) + cmd_az = (b2 & 0x04) ? HP3478A_AUTOZERO_ON : HP3478A_AUTOZERO_OFF; + + // "D1 Fx Rx Nx Zx T1 M20" + strcpy(cmd_buffer, HP3478A_DISP_NORMAL); + strcat(cmd_buffer, cmd_func); + strcat(cmd_buffer, cmd_range); + strcat(cmd_buffer, cmd_dig); + strcat(cmd_buffer, cmd_az); + strcat(cmd_buffer, HP3478A_TRIG_INTERNAL HP3478A_CMD_MASK_BTN_ONLY); + + gpib_send(app.dmm_addr, cmd_buffer); } void exit_to_passthrough(void) { - dmm_display_normal(); // "D1" - buzzer_set(0); // mute - - gpib_send(app.dmm_addr, "H1 T1 N5" HP_CMD_MASK_BTN_ONLY); - Delay_Ms(50); + buzzer_set(0); // mute + restore_dmm_state(); gpib_go_to_local(app.dmm_addr); app.current_mode = MODE_PASSTHROUGH; + app.data.menu.layer = SUBMENU_NONE; + app.menu_pos = 0; + app.last_poll_time = millis(); + + Delay_Ms(MENU_DEBOUNCE_MS); } void enter_feature_mode(menu_item_t item) { + // force display refresh + app.last_disp_sent[0] = '\0'; + gpib_remote_enable(1); // assert REN Delay_Ms(20); switch (item) { case MENU_REL: - // T1: Internal Trigger, M21: Data Ready + SRQ Button - gpib_send(app.dmm_addr, "F3 N5 T1" HP_CMD_MASK_BTN_DATA); + gpib_send(app.dmm_addr, + HP3478A_FUNC_OHMS_2WIRE HP3478A_DIGITS_5_5 HP3478A_TRIG_INTERNAL + HP3478A_CMD_MASK_BTN_DATA HP3478A_CMD_SRQ_CLEAR); app.current_mode = MODE_FEAT_REL; - app.rel_offset = 0.0f; + app.data.rel.offset = 0.0f; dmm_display("REL MODE"); break; + case MENU_DBM: + // F1=DCV, A1=AutoRange (Critical for dBm), N5=5.5d + gpib_send(app.dmm_addr, + HP3478A_MEAS_AC_VOLTS HP3478A_TRIG_INTERNAL + HP3478A_CMD_MASK_BTN_DATA HP3478A_CMD_SRQ_CLEAR); + app.current_mode = MODE_FEAT_DBM; + dmm_display("DBM 50 OHM"); + break; + case MENU_TEMP: - // F3: 2W Ohm, M21: Data + Button Mask - gpib_send(app.dmm_addr, "F3 R3 N4 Z1 T1 " HP_CMD_MASK_BTN_DATA); app.current_mode = MODE_FEAT_TEMP; - dmm_display("TEMP PT1000"); + if (app.temp_sensor == SENS_TYPE_K) { + // Thermocouple Setup: + // F1: DC Volts + // R-2: 30mV Range + // Z1: AutoZero On + gpib_send(app.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); + dmm_display("TEMP TYPE-K"); + } else { + // Resistor Setup (PT1000/Therm): + // F3=2W / F4=4W + // R4=30kOhm Range + if (app.temp_wire_mode == WIRE_2W) { + gpib_send(app.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, + 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); + } + + gpib_send(app.dmm_addr, tmp_buffer); + + if (app.temp_sensor == SENS_PT1000) + dmm_display("TEMP PT1000"); + else + dmm_display("TEMP THERM"); + } break; case MENU_CONT: // F3: 2W Ohm, R0: 30 Ohm Range, N3: 3.5 Digits (fastest ADC), M21 - gpib_send(app.dmm_addr, "F3 R1 N3 Z0 T1 " HP_CMD_MASK_BTN_DATA); + gpib_send(app.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); app.current_mode = MODE_FEAT_CONT; - app.cont_last_state = -1; - app.cont_disp_timer = millis(); + app.data.cont.last_state = -1; + app.data.cont.disp_timer = millis(); dmm_display("CONT MODE"); break; + case MENU_DIODE: + // F3: 2-Wire Ohms + // R3: 3 kOhm Range (1mA Current Through Unknown) + // N4: 4.5 Digits + // Z0: Auto-Zero OFF + gpib_send(app.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); + + app.current_mode = MODE_FEAT_DIODE; + + app.data.diode.connected = 0; + app.data.diode.chirp_start = millis() - DIODE_CHIRP_MS - 1; + + dmm_display("DIODE TEST"); + break; + case MENU_XOHM: // H7: High Impedance / Extended Ohm Mode // The DMM puts internal 10M in parallel with input - gpib_send(app.dmm_addr, "H7 N5 T1 " HP_CMD_MASK_BTN_DATA); - app.xohm_r1 = 0.0f; - app.xohm_calibrated = 0; + gpib_send(app.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.0f; + app.data.xohm.calibrated = 0; app.current_mode = MODE_FEAT_XOHM; dmm_display("XOHM 10M REF"); break; + case MENU_STATS: + // F1: DC Volts + // A1: Auto Range + // N5: 5.5 Digit + // Z1: Auto Zero ON + // T1: Internal Trigger + gpib_send(app.dmm_addr, + 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); + + app.current_mode = MODE_FEAT_STATS; + + // Initialize Stats + app.data.stats.min = STATS_INIT_MIN_VAL; // start impossible high + app.data.stats.max = STATS_INIT_MAX_VAL; // start impossible low + app.data.stats.sum = 0.0f; + app.data.stats.count = 0; + app.data.stats.view_mode = 0; + app.data.stats.disp_timer = millis(); + + dmm_display("STATS INIT"); + break; + case MENU_EXIT: default: exit_to_passthrough(); @@ -1047,13 +1450,18 @@ void enter_feature_mode(menu_item_t item) { } void enter_menu_mode(void) { + // force display refresh + app.last_disp_sent[0] = '\0'; + + save_dmm_state(); + uint32_t now = millis(); app.current_mode = MODE_MENU; app.menu_pos = MENU_REL; - app.menu_timer = now; + app.data.menu.timer = now; dmm_display("M: REL"); - gpib_send(app.dmm_addr, HP_CMD_MASK_BTN_ONLY); + gpib_send(app.dmm_addr, HP3478A_CMD_MASK_BTN_ONLY HP3478A_CMD_SRQ_CLEAR); Delay_Ms(200); } @@ -1062,14 +1470,15 @@ void handle_feature_logic(void) { gpib_serial_poll(app.dmm_addr, &stb); // exit button (SRQ) - if (stb & 0x10) { + if (stb & HP3478A_MASK_KEYBOARD_SRQ) { exit_to_passthrough(); return; } // data ready (Bit 0) - if (!(stb & 0x01)) return; + if (!(stb & HP3478A_MASK_DATA_READY)) return; + // read measurement int len = gpib_receive(app.dmm_addr, resp_buffer, sizeof(resp_buffer)); if (len < 0) { // timeout or error @@ -1081,26 +1490,25 @@ void handle_feature_logic(void) { } float val = parse_float(resp_buffer); - // overload (HP 3478A sends +9.99990E+9 for OL) int is_overload = (val > HP_OVERLOAD_VAL); // RELATIVE MODE if (app.current_mode == MODE_FEAT_REL) { - if (app.rel_offset == 0.0f) { + if (app.data.rel.offset == 0.0f) { // waiting to capture the NULL value if (is_overload) { - app.rel_stable_count = 0; // reset counter if probes are open + app.data.rel.stable_count = 0; // reset counter if probes are open dmm_display("O.VLD"); } else { // valid reading - app.rel_stable_count++; + app.data.rel.stable_count++; - if (app.rel_stable_count >= REL_STABLE_SAMPLES) { - app.rel_offset = val; + if (app.data.rel.stable_count >= REL_STABLE_SAMPLES) { + app.data.rel.offset = val; dmm_display("NULL SET"); tone(3000, 50); - app.rel_stable_count = 0; + app.data.rel.stable_count = 0; } else { dmm_display("LOCKING..."); } @@ -1110,33 +1518,110 @@ void handle_feature_logic(void) { if (is_overload) { dmm_display("O.VLD"); } else { - float diff = val - app.rel_offset; + float diff = val - app.data.rel.offset; fmt_float(disp_buffer, sizeof(disp_buffer), diff, 4); - - if (strlen(disp_buffer) < 11) strcat(disp_buffer, " D"); + // Display: "1.2345 D" + format_aligned_display(disp_buffer, diff, 4, " D"); dmm_display(disp_buffer); } } } - - // TEMP MODE - else if (app.current_mode == MODE_FEAT_TEMP) { - if (val > 4000.0f || val < 10.0f) { - dmm_display("OPEN / ERR"); + // dBm MODE + else if (app.current_mode == MODE_FEAT_DBM) { + if (is_overload) { + dmm_display("O.VLD"); } else { - float c = RTD_R0 - val; - float b = RTD_R0 * RTD_A; - float a = RTD_R0 * RTD_B; - float disc = (b * b) - (4 * a * c); + // P(mW) = (V^2 / 50) * 1000 = V^2 * 20 + float p_mw = (val * val * 20.0f); - if (disc >= 0) { - float temp = (-b + sqrtf(disc)) / (2 * a); - fmt_float(disp_buffer, sizeof(disp_buffer), temp, 1); - strcat(disp_buffer, " C"); + if (p_mw < 1e-9f) { + // Align -INF to look consistent + // Display: "-INF DBM" + memset(disp_buffer, ' ', 12); + disp_buffer[12] = '\0'; + memcpy(disp_buffer, "-INF", 4); + memcpy(&disp_buffer[8], " DBM", 4); + dmm_display(disp_buffer); + } else { + float dbm = 10.0f * log10f(p_mw); + // Display: "-14.20 DBM" + format_aligned_display(disp_buffer, dbm, 2, " DBM"); dmm_display(disp_buffer); } } } + // TEMP MODE + else if (app.current_mode == MODE_FEAT_TEMP) { + if (is_overload && app.temp_sensor != SENS_TYPE_K) { + dmm_display("OPEN / ERR"); + } else { + float temp_c = 0.0f; + const char* unit_str = " C"; + + // 1. TYPE K THERMOCOUPLE + if (app.temp_sensor == SENS_TYPE_K) { + // 30mV range safety check (Floating input > 50mV) + if (fabsf(val) > 0.05f) { + dmm_display("CHECK PROBE"); + return; + } else { + float t_amb; + + // Cold Junction Compensation (CJC) + if (app.env_sensor_present) { + // Convert int(2450) to 24.50 + t_amb = (float)app.current_env.temp_c_x100 / 100.0f; + t_amb -= CJC_SELF_HEATING_OFFSET; + unit_str = " C (K)"; + + } else { + t_amb = 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); + } + } + // 2. PT1000 RTD (Callendar-Van Dusen) + else if (app.temp_sensor == SENS_PT1000) { + if (val < 10.0f) { + dmm_display("SHORT"); + } else { + float c = RTD_R0 - val; + float b = RTD_R0 * RTD_A; + float a = RTD_R0 * RTD_B; + float disc = (b * b) - (4 * a * c); + + if (disc >= 0) + temp_c = (-b + sqrtf(disc)) / (2 * a); + else { + dmm_display("RANGE ERR"); + return; + } + } + } + // 3. THERMISTOR (Steinhart-Hart) + else { + if (val < 10.0f) { + dmm_display("SHORT"); + } else { + // protect against log(0) + float r = (val < 1.0f) ? 1.0f : val; + float lr = logf(r); + float lr3 = lr * lr * lr; + float inv_t = THERM_A + (THERM_B * lr) + (THERM_C * lr3); + + temp_c = (1.0f / inv_t) - 273.15f; + } + } + // Display: "24.5 C" (or "24.5 C (K)") + format_aligned_display(disp_buffer, temp_c, 1, unit_str); + dmm_display(disp_buffer); + } + } // CONT MODE else if (app.current_mode == MODE_FEAT_CONT) { int is_short = (!is_overload && val < CONT_THRESHOLD_OHMS); @@ -1151,14 +1636,15 @@ void handle_feature_logic(void) { // display logic uint32_t now = millis(); // force update if state changed or timeout - if ((is_short != app.cont_last_state) || - (now - app.cont_disp_timer > 200)) { + if ((is_short != app.data.cont.last_state) || + (now - app.data.cont.disp_timer > 200)) { if (is_overload) { dmm_display("OPEN"); } else { if (val < 1000.0f) { - fmt_float(disp_buffer, sizeof(disp_buffer), val, 1); - strcat(disp_buffer, " OHM"); + // Normalize low ohms to look like standard ohms mode + // "10.5 OHM" + format_aligned_display(disp_buffer, val, 1, " OHM"); } else { // shouldn't happen in 300 range :) format_resistance(disp_buffer, sizeof(disp_buffer), val); @@ -1166,18 +1652,81 @@ void handle_feature_logic(void) { dmm_display(disp_buffer); } - app.cont_last_state = is_short; - app.cont_disp_timer = now; + app.data.cont.last_state = is_short; + app.data.cont.disp_timer = now; } } + + else if (app.current_mode == MODE_FEAT_DIODE) { + uint32_t now = millis(); + float voltage = is_overload ? 9.9f : (val / 1000.0f); + uint8_t is_valid_signal = + (voltage > DIODE_TH_SHORT && voltage < DIODE_TH_OPEN); + + if (voltage < DIODE_TH_OPEN) { + format_aligned_display(disp_buffer, voltage, 4, "VDC"); + dmm_display(disp_buffer); + } else { + dmm_display("OPEN"); + } + + diode_state_t next_state = app.data.diode.connected; + uint8_t request_buzzer = 0; + + switch (app.data.diode.connected) { + case DIODE_STATE_OPEN: + case DIODE_STATE_SHORT: + if (is_valid_signal) { + next_state = DIODE_STATE_CHECKING; + app.data.diode.chirp_start = now; + } else { + // strictly either open or short + next_state = + (voltage >= 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) { + next_state = DIODE_STATE_PLAYING; + app.data.diode.chirp_start = now; + request_buzzer = 1; + } + break; + + case DIODE_STATE_PLAYING: + request_buzzer = 1; + if ((now - app.data.diode.chirp_start) >= DIODE_CHIRP_MS) { + next_state = DIODE_STATE_DONE; + } + break; + + case DIODE_STATE_DONE: + // latch until signal is clearly lost + if (!is_valid_signal) { + next_state = DIODE_STATE_SHORT; + } + break; + + default: + next_state = DIODE_STATE_OPEN; + break; + } + + app.data.diode.connected = next_state; + buzzer_set(request_buzzer ? 2500 : 0); + } + // XOHM MODE else if (app.current_mode == MODE_FEAT_XOHM) { // cal phase, measure the internal 10M resistor - if (app.xohm_calibrated == 0) { + if (app.data.xohm.calibrated == 0) { // need the probes to be open. Internal R is ~10M if (val > 8.0e6f && val < 12.0e6f) { - app.xohm_r1 = val; // Store R1 - app.xohm_calibrated = 1; + app.data.xohm.r1 = val; // Store R1 + app.data.xohm.calibrated = 1; tone(3000, 100); dmm_display("READY"); Delay_Ms(500); @@ -1189,10 +1738,10 @@ void handle_feature_logic(void) { // R1 = xohm_ref (Internal) // R2 = val (Measured Parallel) else { - if (is_overload || val >= (app.xohm_r1 - 1000.0f)) { + if (is_overload || val >= (app.data.xohm.r1 - 1000.0f)) { dmm_display("OPEN"); } else { - float r1 = app.xohm_r1; + float r1 = app.data.xohm.r1; float r2 = val; float rx = (r1 * r2) / (r1 - r2); @@ -1200,12 +1749,85 @@ void handle_feature_logic(void) { dmm_display(disp_buffer); } } + } else if (app.current_mode == MODE_FEAT_STATS) { + if (is_overload) { + dmm_display("O.VLD"); + } else { + // accumulate math + if (val < app.data.stats.min) app.data.stats.min = val; + if (val > app.data.stats.max) app.data.stats.max = val; + app.data.stats.sum += (double)val; + app.data.stats.count++; + + // rotate display + uint32_t now = millis(); + if (now - app.data.stats.disp_timer > 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; + } + + // render + char prefix[5]; // 3-char prefix + space + float val_to_show = 0.0f; + + switch (app.data.stats.view_mode) { + case 0: // Live Value + strcpy(prefix, ""); + val_to_show = val; + break; + case 1: // Average + strcpy(prefix, "AVG "); + val_to_show = app.data.stats.sum / app.data.stats.count; + break; + case 2: // Minimum + strcpy(prefix, "MIN "); + val_to_show = app.data.stats.min; + break; + case 3: // Maximum + strcpy(prefix, "MAX "); + val_to_show = app.data.stats.max; + break; + default: + strcpy(prefix, "ERR "); + val_to_show = val; + app.data.stats.view_mode = 0; + break; + } + + int offset = snprintf(disp_buffer, sizeof(disp_buffer), "%s", prefix); + if (offset >= 0 && offset < (int)sizeof(disp_buffer)) { + fmt_float(disp_buffer + offset, sizeof(disp_buffer) - offset, + val_to_show, 4); + } + + dmm_display(disp_buffer); + } } } +// gens the base string (e.g., "M: TEMP", "S: TYPE K") into disp_buffer +void prepare_menu_base_string(void) { + const char* prefix = ""; + const char* name = "???"; + + if (app.data.menu.layer == SUBMENU_NONE) { + prefix = "M: "; + if (app.menu_pos < MENU_MAX_ITEMS) name = MENU_NAMES[app.menu_pos]; + } else if (app.data.menu.layer == SUBMENU_TEMP_SENS) { + prefix = "S: "; + if (app.menu_pos < SENS_MAX_ITEMS) name = SENSOR_NAMES[app.menu_pos]; + } else if (app.data.menu.layer == SUBMENU_TEMP_WIRE) { + prefix = "T: "; + if (app.menu_pos < WIRE_MAX_ITEMS) name = WIRE_NAMES[app.menu_pos]; + } + + snprintf(disp_buffer, sizeof(disp_buffer), "%s%s", prefix, name); +} + void handle_menu_navigation(void) { uint32_t now = millis(); - uint32_t elapsed = now - app.menu_timer; + uint32_t elapsed = now - app.data.menu.timer; // nav: check SRQ (next item) if (gpib_check_srq()) { @@ -1213,76 +1835,91 @@ void handle_menu_navigation(void) { gpib_serial_poll(app.dmm_addr, &stb); // only 4b (front panel button SRQ) - if (stb & 0x10) { - app.menu_timer = now; // reset the "hover" timer + if (stb & HP3478A_MASK_KEYBOARD_SRQ) { + if (elapsed < MENU_DEBOUNCE_MS) { + // we polled, so STB is clear on DMM. Just return. + return; + } + + app.data.menu.timer = now; // reset the "hover" timer app.menu_pos++; - if (app.menu_pos >= MENU_MAX_ITEMS) app.menu_pos = 0; + 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; - char* s = "M: ???"; - switch (app.menu_pos) { - case MENU_REL: - s = "M: REL"; - break; - case MENU_CONT: - s = "M: CONT"; - break; - case MENU_TEMP: - s = "M: TEMP"; - break; - case MENU_XOHM: - s = "M: XOHM"; - break; - case MENU_EXIT: - s = "M: EXIT"; - break; - default: - break; - } - dmm_display(s); + if (app.menu_pos >= max_items) app.menu_pos = 0; + + // update display immediately + prepare_menu_base_string(); + dmm_display(disp_buffer); // re-arm SRQ - gpib_send(app.dmm_addr, HP_CMD_MASK_BTN_ONLY); - Delay_Ms(200); + gpib_send(app.dmm_addr, HP3478A_CMD_MASK_BTN_ONLY HP3478A_CMD_SRQ_CLEAR); + Delay_Ms(MENU_DEBOUNCE_MS); return; } } - // visual cd - static int last_dot_count = -1; - int dot_count = elapsed / 800; - if (dot_count > 3) dot_count = 3; + prepare_menu_base_string(); - // only update display if the dots changed - if (dot_count != last_dot_count) { - const char* dots = ""; - if (dot_count == 1) - dots = "."; - else if (dot_count == 2) - dots = ".."; - else if (dot_count == 3) - dots = "..."; + // 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 (dots > 3) dots = 3; - snprintf(disp_buffer, sizeof(disp_buffer), "M: %s%s", - MENU_NAMES[app.menu_pos], dots); - - dmm_display(disp_buffer); - last_dot_count = dot_count; + for (int i = 0; i < dots; i++) strcat(disp_buffer, "."); } + dmm_display(disp_buffer); - // reset dot tracker if cycled - if (elapsed < 100) last_dot_count = 0; + if (elapsed > MENU_COMMIT_DELAY_MS) { + // L0: main menu + if (app.data.menu.layer == SUBMENU_NONE) { + if (app.menu_pos == MENU_TEMP) { + // sensor select + app.data.menu.layer = SUBMENU_TEMP_SENS; + app.menu_pos = 0; // default to first sensor + app.data.menu.timer = now; - // commit selection - if (elapsed > 2400) { - if (app.menu_pos == MENU_EXIT) { - // printf("[MENU] Hover EXIT\n"); - exit_to_passthrough(); - } else { - // printf("[MENU] Hover Select Item %d\n", app.menu_pos); + prepare_menu_base_string(); + dmm_display(disp_buffer); + return; + } + // enter standard modes enter_feature_mode(app.menu_pos); + return; + } + + // L1: sensor select + else if (app.data.menu.layer == SUBMENU_TEMP_SENS) { + app.temp_sensor = (temp_sensor_t)app.menu_pos; + + if (app.temp_sensor == SENS_TYPE_K) { + // Type K is voltage based so skip wire select + enter_feature_mode(MENU_TEMP); + } else { + // wire select (for resistive) + app.data.menu.layer = SUBMENU_TEMP_WIRE; + app.menu_pos = 0; // default to 2W + app.data.menu.timer = now; + + prepare_menu_base_string(); + dmm_display(disp_buffer); + } + return; + } + + // L2: wire select + else if (app.data.menu.layer == SUBMENU_TEMP_WIRE) { + app.temp_wire_mode = (wire_mode_t)app.menu_pos; + enter_feature_mode(MENU_TEMP); + return; } - return; } } @@ -1291,42 +1928,48 @@ void app_loop(void) { // Passthrough if (app.current_mode == MODE_PASSTHROUGH) { - if (now <= app.next_poll_time) { - return; - } + 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); - // DMM offline - detect recovery if (poll_result != 0) { + // poll failed (Timeout/NACK) if (app.dmm_online) { - // printf("DMM Lost\n"); - gpib_interface_clear(); + // printf("DMM Lost connection.\n"); app.dmm_online = 0; + gpib_interface_clear(); } - app.next_poll_time = now + POLL_INTERVAL_MS; + // slow down polling when offline + app.poll_interval = DMM_RECOVERY_DELAY_MS; return; } - // DMM online - detect recovery transition + // got a valid response, check if this is a recovery if (!app.dmm_online) { - // printf("DMM Recovered. Re-initializing...\n"); - gpib_send(app.dmm_addr, HP_CMD_MASK_BTN_ONLY "K"); - gpib_go_to_local(app.dmm_addr); + // printf("DMM Recovered.\n"); 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(4000, 50); - // do it in the next clean poll - app.next_poll_time = now + DMM_RECOVERY_DELAY_MS; + app.poll_interval = POLL_INTERVAL_MS; // restore fast polling return; } - // check for button press - if (stb & 0x10) { + // valid and online, check buttons + if (stb & HP3478A_MASK_KEYBOARD_SRQ) { enter_menu_mode(); } - app.next_poll_time = now + POLL_INTERVAL_MS; return; } @@ -1353,13 +1996,13 @@ void app_loop(void) { } // check exit button first (priority) - if (stb & 0x10) { + if (stb & HP3478A_MASK_KEYBOARD_SRQ) { exit_to_passthrough(); return; } // handle measurement data ready - if (stb & 0x01) { + if (stb & HP3478A_MASK_DATA_READY) { handle_feature_logic(); } } @@ -1384,11 +2027,13 @@ static void cmd_help(void) { " ++ren <0|1> Remote Enable Line control\r\n" " ++ifc Interface Clear (Bus Reset)\r\n" " ++ver Firmware Version\r\n" - " ++stat Show configuration\r\n" + " ++conf Show configuration\r\n" " ++rst System Reboot\r\n" "\r\n" "HP3478A Internal Commands:\r\n" - " ++cont, ++temp, ++rel, ++xohm, ++norm\r\n" + " ++cont, ++temp, ++rel, ++xohm, ++dbm\r\n" + " ++diode, ++math (Min/Max/Avg)\r\n" + " ++norm Exit Special Mode\r\n" " ++disp Text message on LCD (Max 12)\r\n" " ++env [temp|hum] Internal Sensor (Default: csv)\r\n" "\r\n" @@ -1539,7 +2184,7 @@ static void process_command(void) { gpib_go_to_local(app.target_addr); usb_send_text("OK\r\n"); } - // HP3478A + // HP3478A Internal else if (starts_with_nocase(p_cmd, "cont")) enter_feature_mode(MENU_CONT); else if (starts_with_nocase(p_cmd, "temp")) @@ -1548,6 +2193,12 @@ static void process_command(void) { enter_feature_mode(MENU_REL); else if (starts_with_nocase(p_cmd, "xohm")) enter_feature_mode(MENU_XOHM); + else if (starts_with_nocase(p_cmd, "dbm")) + enter_feature_mode(MENU_DBM); + else if (starts_with_nocase(p_cmd, "diode")) + enter_feature_mode(MENU_DIODE); + else if (starts_with_nocase(p_cmd, "math")) + enter_feature_mode(MENU_STATS); else if (starts_with_nocase(p_cmd, "norm")) exit_to_passthrough(); else if (starts_with_nocase(p_cmd, "disp")) { @@ -1670,18 +2321,16 @@ int main() { // app state app.current_mode = MODE_PASSTHROUGH; - app.target_addr = 18; - app.dmm_addr = app.target_addr; - app.xohm_r1 = 0.0f; app.usb_online = 0; app.usb_raw_prev = USB_HW_IS_ACTIVE(); app.usb_ts = millis(); + app.last_poll_time = millis(); while (1) { uint8_t status_byte; if (gpib_serial_poll(app.dmm_addr, &status_byte) == 0) { // printf("Device Found (Stb: 0x%02X)\n", status_byte); - gpib_send(app.dmm_addr, HP_CMD_MASK_BTN_ONLY "K"); + gpib_send(app.dmm_addr, HP3478A_CMD_MASK_BTN_ONLY HP3478A_CMD_SRQ_CLEAR); gpib_go_to_local(app.dmm_addr); break; }