From abe722f88cbcbd3f28ee58f10430189414e838ba Mon Sep 17 00:00:00 2001 From: kuwoyuki Date: Mon, 1 Dec 2025 21:46:39 +0600 Subject: [PATCH] ok, let's stop --- .vscode/settings.json | 3 +- main.c | 496 +++++++++++++++++++++++++----------------- 2 files changed, 295 insertions(+), 204 deletions(-) diff --git a/.vscode/settings.json b/.vscode/settings.json index e3d255f..59967d8 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -111,7 +111,8 @@ "string.h": "c", "math.h": "c", "cctype": "c", - "stdlib.h": "c" + "stdlib.h": "c", + "float.h": "c" }, "cmake.sourceDirectory": "/home/mira/src/embedded/ch32v208_sens/lwip" } \ No newline at end of file diff --git a/main.c b/main.c index 1b4eb5d..169661f 100644 --- a/main.c +++ b/main.c @@ -1,3 +1,17 @@ +/* + * HP3478A internal USB-GPIB adapter & extension + * + * DESCRIPTION: + * This firmware acts as a bridge between USB-CDC and GPIB while also working + * in standalone mode inside HP3478A and MITM-ing GPIB and adding features. + * + * TODO: + * - 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 @@ -35,6 +49,7 @@ #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 +#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 @@ -51,13 +66,6 @@ #define RTD_B -5.775e-7 #define RTD_R0 1000.0 -// Thermistor -// TODO: different ranges? 5k, 10k etc.? -// This is a 5k NTC! kinda useless here -#define THERM_A 0.001286 -#define THERM_B 0.00023595 -#define THERM_C 0.0000000941 - // 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 @@ -181,6 +189,32 @@ typedef enum { MENU_MAX_ITEMS } menu_item_t; +typedef enum { + // Generic + NTC_10K_3950 = 0, // Generic china 3950 + NTC_10K_3435, // alt 10k + NTC_50K_3950, // alt 50k + NTC_100K_3950, // alt 100k + + // YSI 44000 + NTC_YSI_2252, // YSI 44004 (Mix B) + NTC_YSI_3K, // YSI 44005 (Mix B) + NTC_YSI_5K, // YSI 44007 (Mix B) + NTC_YSI_10K_A, // YSI 44006 (Mix H) + NTC_YSI_10K_B, // YSI 44016 (Mix B) + NTC_YSI_30K, // YSI 44008 (Mix H) + NTC_YSI_100K, // YSI 44011 (Mix H) + NTC_YSI_1MEG, // YSI 44015 (Mix H) + + NTC_MAX_ITEMS +} ntc_preset_t; + +typedef struct { + double r0; // R @ 25C + double beta; // beta coefficient (typ. 3000-4500) + const char* name; // display name +} ntc_def_t; + // UI Strings static const char* MENU_NAMES[] = {"REL", "TEMP", "DBM", "CONT", "DIODE", "XOHM", "STATS", "EXIT"}; @@ -189,11 +223,30 @@ 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 2: wire mode (skipped for Type K) + 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 @@ -209,7 +262,6 @@ 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; @@ -271,7 +323,8 @@ typedef struct { uint8_t auto_read : 1; uint8_t dmm_online : 1; uint8_t has_saved_state : 1; - uint8_t reserved : 2; + uint8_t beep_active : 1; + uint8_t reserved : 1; // Addresses uint8_t target_addr; @@ -283,6 +336,9 @@ typedef struct { 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; // Logic work_mode_t current_mode; @@ -291,6 +347,7 @@ typedef struct { // Config Selection temp_sensor_t temp_sensor; wire_mode_t temp_wire_mode; + ntc_preset_t temp_ntc_preset; // Environmental Data aht20_data current_env; @@ -298,7 +355,7 @@ typedef struct { // DMM Restore uint8_t saved_state_bytes[5]; - // Display shadow buffer + // Display shadow buffer (cache) char last_disp_sent[HP_DISP_BUF_SIZE]; // Unionized Mode Data @@ -310,19 +367,28 @@ static app_state_t app = {.dmm_addr = DEFAULT_DMM_ADDR, .poll_interval = POLL_INTERVAL_MS, .gpib_timeout_ms = DEFAULT_GPIB_TIMEOUT_MS}; -// Buffers -// TODO: buffer usage is very inconsistent -// cmd and resp can probably be a union, and what do we even use tmp_buffer -// for?.. -static char cmd_buffer[128]; -static char resp_buffer[256]; -static char tmp_buffer[128]; -// Max theoretic bytes for 12 visual chars is 24 -static char disp_buffer[HP_DISP_BUF_SIZE]; -// Display Command Buffer -// Size: "D3"(2) + Text(HP_DISP_BUF_SIZE) + "\n"(1) + Null(1) -// "D3" + "1.2....2." + "\n" + \0 -static char disp_cmd_buffer[2 + HP_DISP_BUF_SIZE + 2]; +typedef union { + // command processing + struct { + char line_buf[128]; + char fmt_buf[128]; + } cmd; + + // general io ctx + struct { + // for gpib + char raw_data[256]; + } io; + + // display ctx + struct { + char full_cmd[64]; + } disp; + + uint8_t raw[256]; +} app_scratchpad_t; + +static app_scratchpad_t scratch; // USB Ring Buffer #define USB_RX_BUF_SIZE 512 @@ -385,9 +451,9 @@ void double_to_str(char* buf, size_t buf_size, double val, int prec) { val = -val; } - // 3. Multiplier (Integer Powers of 10) + // multiplier if (prec < 0) prec = 0; - if (prec > 9) prec = 9; // Limit precision to prevent overflow + if (prec > 9) prec = 9; // limit precision unsigned long long multiplier = 1; for (int i = 0; i < prec; i++) multiplier *= 10; @@ -395,7 +461,7 @@ void double_to_str(char* buf, size_t buf_size, double val, int prec) { // scale and round val = (val * (double)multiplier) + 0.5; - // 5. split integer and fractional parts + // 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; @@ -1271,7 +1337,6 @@ void TIM2_IRQHandler(void) { } } -// TODO: maybne don't sleep inside it void tone(unsigned int freq_hz, unsigned int duration_ms) { if (freq_hz == 0) { Delay_Ms(duration_ms); @@ -1283,29 +1348,22 @@ void tone(unsigned int freq_hz, unsigned int duration_ms) { buzzer_set(0); } +void tone_nb(uint16_t freq, uint32_t duration_ms) { + buzzer_set(freq); + app.beep_start_ts = millis(); + app.beep_duration = duration_ms; + app.beep_active = 1; +} + +// "Boot Up" void play_startup_tune() { - // "Boot Up" tone(1500, 100); - Delay_Ms(20); + Delay_Ms(25); tone(2500, 100); - Delay_Ms(20); + Delay_Ms(25); tone(4000, 100); } -void play_connected_tune() { - // "Device Attached" - tone(3000, 100); - tone(4000, 100); -} - -void play_disconnected_tune() { - // "Device Removed" - tone(4000, 100); - tone(3000, 100); -} - -void beep(int ms) { tone(2500, ms); } - // ------------------------------------ int HandleSetupCustom(struct _USBState* ctx, int setup_code) { @@ -1437,12 +1495,7 @@ static void handle_usb_state(void) { if (app.usb_online != raw_status) { app.usb_online = raw_status; - if (app.usb_online) { - usb_rx_tail = usb_rx_head = 0; - play_connected_tune(); - } else { - play_disconnected_tune(); - } + if (app.usb_online) usb_rx_tail = usb_rx_head = 0; } } } @@ -1472,12 +1525,12 @@ void dmm_display(const char* text, const char* mode) { // update shadow buf strncpy(app.last_disp_sent, text, HP_DISP_BUF_SIZE - 1); - app.last_disp_sent[HP_DISP_BUF_SIZE - 1] = '\0'; // "D[23]" + Text + "\n" - snprintf(disp_cmd_buffer, sizeof(disp_cmd_buffer), "%s%s\n", mode, + snprintf(scratch.disp.full_cmd, sizeof(scratch.disp.full_cmd), "%s%s\n", mode, app.last_disp_sent); + // send it - gpib_send(app.dmm_addr, disp_cmd_buffer); + gpib_send(app.dmm_addr, scratch.disp.full_cmd); } static inline void dmm_display_normal(void) { gpib_send(app.dmm_addr, HP3478A_DISP_NORMAL); @@ -1507,33 +1560,34 @@ void restore_dmm_state(void) { decode_dmm_state_bytes(app.saved_state_bytes, &state); // "D1 Fx Rx Nx Zx" + "T1 M20" - build_restoration_string(cmd_buffer, &state); - strcat(cmd_buffer, HP3478A_TRIG_INTERNAL HP3478A_CMD_MASK_BTN_ONLY); + 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, cmd_buffer); + gpib_send(app.dmm_addr, scratch.cmd.line_buf); } void exit_to_passthrough(void) { - buzzer_set(0); // mute + buzzer_set(0); 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(); + app.beep_active = 0; - Delay_Ms(MENU_DEBOUNCE_MS); + uint32_t now = millis(); + app.last_poll_time = now; + app.ignore_input_start_ts = now; } void enter_feature_mode(menu_item_t item) { // force display refresh app.last_disp_sent[0] = '\0'; // clean buffer for cmds - cmd_buffer[0] = '\0'; + scratch.cmd.line_buf[0] = '\0'; - gpib_remote_enable(1); // assert REN - Delay_Ms(20); + 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; @@ -1542,16 +1596,17 @@ void enter_feature_mode(menu_item_t item) { case MENU_REL: if (app.has_saved_state) { decode_dmm_state_bytes(app.saved_state_bytes, &saved_cfg); - build_restoration_string(cmd_buffer, &saved_cfg); - strcat(cmd_buffer, HP3478A_TRIG_INTERNAL HP3478A_CMD_MASK_BTN_DATA - HP3478A_CMD_SRQ_CLEAR); + 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); } - gpib_send(app.dmm_addr, cmd_buffer); + gpib_send(app.dmm_addr, scratch.cmd.line_buf); app.current_mode = MODE_FEAT_REL; app.data.rel.offset = 0.0; @@ -1579,8 +1634,20 @@ void enter_feature_mode(menu_item_t item) { HP3478A_AUTOZERO_ON HP3478A_TRIG_INTERNAL HP3478A_CMD_MASK_BTN_DATA HP3478A_CMD_SRQ_CLEAR); dmm_display("TEMP TYPE-K", HP3478A_DISP_TEXT_FAST); + } 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, + 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); + + char buf[13]; + snprintf(buf, sizeof(buf), "NTC %s", + NTC_DEFS[app.temp_ntc_preset].name); + dmm_display(buf, HP3478A_DISP_TEXT_FAST); } else { - // Resistor Setup (PT1000/Therm): + // Resistor Setup (PT1000): // F3=2W / F4=4W // R4=30kOhm Range if (app.temp_wire_mode == WIRE_2W) { @@ -1597,8 +1664,8 @@ void enter_feature_mode(menu_item_t item) { if (app.temp_sensor == SENS_PT1000) dmm_display("TEMP PT1000", HP3478A_DISP_TEXT_FAST); - else - dmm_display("TEMP THERM", HP3478A_DISP_TEXT_FAST); + // else + // dmm_display("TEMP THERM", HP3478A_DISP_TEXT_FAST); } break; @@ -1645,20 +1712,21 @@ void enter_feature_mode(menu_item_t item) { case MENU_STATS: if (app.has_saved_state) { decode_dmm_state_bytes(app.saved_state_bytes, &saved_cfg); - build_restoration_string(cmd_buffer, &saved_cfg); - strcat(cmd_buffer, HP3478A_TRIG_INTERNAL HP3478A_CMD_MASK_BTN_DATA - HP3478A_CMD_SRQ_CLEAR); + 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(cmd_buffer, + 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, cmd_buffer); + gpib_send(app.dmm_addr, scratch.cmd.line_buf); app.current_mode = MODE_FEAT_STATS; @@ -1692,7 +1760,6 @@ void enter_menu_mode(void) { dmm_display("M: REL", HP3478A_DISP_TEXT_FAST); gpib_send(app.dmm_addr, HP3478A_CMD_MASK_BTN_ONLY HP3478A_CMD_SRQ_CLEAR); - Delay_Ms(200); } void handle_feature_logic(void) { @@ -1709,7 +1776,8 @@ void handle_feature_logic(void) { if (!(stb & HP3478A_MASK_DATA_READY)) return; // read measurement - int len = gpib_receive(app.dmm_addr, resp_buffer, sizeof(resp_buffer)); + int len = gpib_receive(app.dmm_addr, scratch.io.raw_data, + sizeof(scratch.io.raw_data)); if (len < 0) { // timeout or error // printf("Read Timeout in Feature\n"); @@ -1719,7 +1787,7 @@ void handle_feature_logic(void) { return; } - double val = parse_double(resp_buffer); + 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; @@ -1738,7 +1806,7 @@ void handle_feature_logic(void) { if (app.data.rel.stable_count >= REL_STABLE_SAMPLES) { app.data.rel.offset = val; dmm_display("NULL SET", HP3478A_DISP_TEXT_FAST); - tone(3000, 50); + tone_nb(3000, 50); app.data.rel.stable_count = 0; } else { dmm_display("LOCKING...", HP3478A_DISP_TEXT_FAST); @@ -1754,9 +1822,9 @@ void handle_feature_logic(void) { // prepend 'D' snprintf(eff_unit, sizeof(eff_unit), "D%s", app.data.rel.unit); - format_metric_value(disp_buffer, sizeof(disp_buffer), diff, eff_unit, - 1); - dmm_display(disp_buffer, HP3478A_DISP_TEXT); + format_metric_value(scratch.disp.full_cmd, + sizeof(scratch.disp.full_cmd), diff, eff_unit, 1); + dmm_display(scratch.disp.full_cmd, HP3478A_DISP_TEXT); } } } @@ -1766,20 +1834,21 @@ void handle_feature_logic(void) { dmm_display("O.VLD", HP3478A_DISP_TEXT_FAST); } else { // P(mW) = (V^2 / 50) * 1000 = V^2 * 20 - double p_mw = (val * val * 20.0f); + double p_mw = (val * val * 20.0); - if (p_mw < 1e-9f) { + if (p_mw < 1e-9) { // Align -INF to look consistent // Display: "-INF DBM" - memset(disp_buffer, ' ', HP_DISP_LEN); - disp_buffer[HP_DISP_LEN] = '\0'; - memcpy(disp_buffer, "-INF", 4); - memcpy(&disp_buffer[8], " DBM", 4); - dmm_display(disp_buffer, HP3478A_DISP_TEXT_FAST); + memset(scratch.disp.full_cmd, ' ', HP_DISP_LEN); + scratch.disp.full_cmd[HP_DISP_LEN] = '\0'; + memcpy(scratch.disp.full_cmd, "-INF", 4); + memcpy(&scratch.disp.full_cmd[8], " DBM", 4); + dmm_display(scratch.disp.full_cmd, HP3478A_DISP_TEXT_FAST); } else { double dbm = 10.0 * log10(p_mw); - format_metric_value(disp_buffer, sizeof(disp_buffer), dbm, "DBM", 00); - dmm_display(disp_buffer, HP3478A_DISP_TEXT); + format_metric_value(scratch.disp.full_cmd, + sizeof(scratch.disp.full_cmd), dbm, "DBM", 00); + dmm_display(scratch.disp.full_cmd, HP3478A_DISP_TEXT); } } } @@ -1791,7 +1860,7 @@ void handle_feature_logic(void) { double temp_c = 0.0; const char* unit_str = " C"; - // 1. TYPE K THERMOCOUPLE + // TYPE K THERMOCOUPLE if (app.temp_sensor == SENS_TYPE_K) { // 30mV range safety check (Floating input > 50mV) if (fabs(val) > 0.050) { @@ -1817,9 +1886,9 @@ void handle_feature_logic(void) { temp_c = t_amb + (val * TYPE_K_SCALE); } } - // 2. PT1000 RTD (Callendar-Van Dusen) + // PT1000 RTD (Callendar-Van Dusen) else if (app.temp_sensor == SENS_PT1000) { - if (val < 10.0f) { + if (val < 10.0) { dmm_display("SHORT", HP3478A_DISP_TEXT_FAST); return; } else { @@ -1829,32 +1898,44 @@ void handle_feature_logic(void) { double disc = (b * b) - (4 * a * c); if (disc >= 0) - temp_c = (-b + sqrtf(disc)) / (2 * a); + temp_c = (-b + sqrt(disc)) / (2 * a); else { dmm_display("RANGE ERR", HP3478A_DISP_TEXT_FAST); return; } } } - // 3. THERMISTOR (Steinhart-Hart) + // THERMISTOR (simplified Steinhart-Hart) else { - if (val < 10.0f) { + if (val < 10.0) { dmm_display("SHORT", HP3478A_DISP_TEXT_FAST); return; - } else { - // log(0) ? :) - double r = (val < 1.0f) ? 1.0f : val; - double lr = log(r); - double lr3 = lr * lr * lr; - double inv_t = THERM_A + (THERM_B * lr) + (THERM_C * lr3); - - temp_c = (1.0f / inv_t) - 273.15f; } + if (val > 4000000.0) { + dmm_display("OPEN", HP3478A_DISP_TEXT_FAST); + return; + } + + double r_meas = val; + // constants from our preset + double r0 = NTC_DEFS[app.temp_ntc_preset].r0; + double beta = NTC_DEFS[app.temp_ntc_preset].beta; + const double t0_k = 298.15; // 25.0C in Kelvin + + // 1/T = 1/T0 + (1/B * ln(R/R0)) + double ln_ratio = log(r_meas / r0); + double inv_t = (1.0 / t0_k) + ((1.0 / beta) * ln_ratio); + + // convert Kelvin to Celsius + temp_c = (1.0 / inv_t) - 273.15; + + unit_str = "C NTC"; } + // Display: "24.5 C" (or "24.5 C (K)") - format_metric_value(disp_buffer, sizeof(disp_buffer), temp_c, unit_str, - 0); - dmm_display(disp_buffer, HP3478A_DISP_TEXT); + 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); } } // CONT MODE @@ -1871,27 +1952,28 @@ void handle_feature_logic(void) { if (is_overload) { dmm_display("OPEN", HP3478A_DISP_TEXT_FAST); } else { - format_metric_value(disp_buffer, sizeof(disp_buffer), val, "OHM", 1); - dmm_display(disp_buffer, HP3478A_DISP_TEXT); + format_metric_value(scratch.disp.full_cmd, + sizeof(scratch.disp.full_cmd), val, "OHM", 1); + dmm_display(scratch.disp.full_cmd, HP3478A_DISP_TEXT); } } } else if (app.current_mode == MODE_FEAT_DIODE) { uint32_t now = millis(); - float voltage = is_overload ? 9.9f : (val / 1000.0f); + double voltage = is_overload ? 9.9 : (val / 1000.0); uint8_t is_valid_signal = (voltage > DIODE_TH_SHORT && voltage < DIODE_TH_OPEN); if (voltage < DIODE_TH_OPEN) { - format_metric_value(disp_buffer, sizeof(disp_buffer), voltage, "VDC", 1); - dmm_display(disp_buffer, HP3478A_DISP_TEXT); + format_metric_value(scratch.disp.full_cmd, sizeof(scratch.disp.full_cmd), + voltage, "VDC", 1); + dmm_display(scratch.disp.full_cmd, HP3478A_DISP_TEXT); } else { dmm_display("OPEN", HP3478A_DISP_TEXT); } diode_state_t next_state = app.data.diode.connected; - uint8_t request_buzzer = 0; switch (app.data.diode.connected) { case DIODE_STATE_OPEN: @@ -1910,15 +1992,8 @@ void handle_feature_logic(void) { 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) { + // has been stable long enough + tone_nb(2500, DIODE_CHIRP_MS); next_state = DIODE_STATE_DONE; } break; @@ -1936,7 +2011,6 @@ void handle_feature_logic(void) { } app.data.diode.connected = next_state; - buzzer_set(request_buzzer ? 2500 : 0); } // XOHM MODE @@ -1947,9 +2021,7 @@ void handle_feature_logic(void) { if (val > 8.0e6 && val < 12.0e6) { app.data.xohm.r1 = val; // Store R1 app.data.xohm.calibrated = 1; - tone(3000, 100); - dmm_display("READY", HP3478A_DISP_TEXT_FAST); - Delay_Ms(500); + tone_nb(3000, 100); } else { dmm_display("OPEN PROBES", HP3478A_DISP_TEXT_FAST); } @@ -1958,15 +2030,16 @@ void handle_feature_logic(void) { // R1 = xohm_ref (Internal) // R2 = val (Measured Parallel) else { - if (is_overload || val >= (app.data.xohm.r1 - 1000.0f)) { + if (is_overload || val >= (app.data.xohm.r1 - 1000.0)) { dmm_display("OPEN", HP3478A_DISP_TEXT_FAST); } else { double r1 = app.data.xohm.r1; double r2 = val; double rx = (r1 * r2) / (r1 - r2); - format_metric_value(disp_buffer, sizeof(disp_buffer), rx, "OHM", 1); - dmm_display(disp_buffer, HP3478A_DISP_TEXT); + format_metric_value(scratch.disp.full_cmd, + sizeof(scratch.disp.full_cmd), rx, "OHM", 1); + dmm_display(scratch.disp.full_cmd, HP3478A_DISP_TEXT); } } } else if (app.current_mode == MODE_FEAT_STATS) { @@ -2015,7 +2088,8 @@ void handle_feature_logic(void) { // render if (app.data.stats.view_mode == 0) { // live mode - format_metric_value(disp_buffer, sizeof(disp_buffer), val_to_show, + format_metric_value(scratch.disp.full_cmd, + sizeof(scratch.disp.full_cmd), val_to_show, app.data.stats.unit, 1); } else { // stats mode: prefix (4) + number (8) @@ -2038,17 +2112,18 @@ void handle_feature_logic(void) { double_to_str(num_buf, sizeof(num_buf), scaled, 4); // combine: prefix + number + suffix - snprintf(disp_buffer, sizeof(disp_buffer), "%s%s%c ", prefix_str, - num_buf, suffix ? suffix : ' '); + snprintf(scratch.disp.full_cmd, sizeof(scratch.disp.full_cmd), + "%s%s%c ", prefix_str, num_buf, suffix ? suffix : ' '); } // send it - dmm_display(disp_buffer, HP3478A_DISP_TEXT); + dmm_display(scratch.disp.full_cmd, HP3478A_DISP_TEXT); } } } -// gens the base string (e.g., "M: TEMP", "S: TYPE K") into disp_buffer +// gens the base string (e.g., "M: TEMP", "S: TYPE K") into +// scratch.disp.full_cmd void prepare_menu_base_string(void) { const char* prefix = ""; const char* name = "???"; @@ -2062,48 +2137,53 @@ void prepare_menu_base_string(void) { } else if (app.data.menu.layer == SUBMENU_TEMP_WIRE) { prefix = "T: "; if (app.menu_pos < WIRE_MAX_ITEMS) name = WIRE_NAMES[app.menu_pos]; + } else if (app.data.menu.layer == SUBMENU_TEMP_NTC) { + prefix = "N: "; + if (app.menu_pos < NTC_MAX_ITEMS) name = NTC_DEFS[app.menu_pos].name; } - snprintf(disp_buffer, sizeof(disp_buffer), "%s%s", prefix, name); + snprintf(scratch.disp.full_cmd, sizeof(scratch.disp.full_cmd), "%s%s", prefix, + name); } void handle_menu_navigation(void) { uint32_t now = millis(); uint32_t elapsed = now - app.data.menu.timer; - // nav: check SRQ (next item) - if (gpib_check_srq()) { - uint8_t stb = 0; - gpib_serial_poll(app.dmm_addr, &stb); + if (elapsed > MENU_DEBOUNCE_MS) { + // only poll GPIB if physical line is asserted + if (gpib_check_srq()) { + uint8_t stb = 0; + gpib_serial_poll(app.dmm_addr, &stb); + + // 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; + + // update display + prepare_menu_base_string(); + dmm_display(scratch.disp.full_cmd, HP3478A_DISP_TEXT_FAST); + + // re-arm SRQ + gpib_send(app.dmm_addr, + HP3478A_CMD_MASK_BTN_ONLY HP3478A_CMD_SRQ_CLEAR); - // only 4b (front panel button SRQ) - 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++; - - 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; - - if (app.menu_pos >= max_items) app.menu_pos = 0; - - // update display immediately - prepare_menu_base_string(); - dmm_display(disp_buffer, HP3478A_DISP_TEXT_FAST); - - // re-arm SRQ - gpib_send(app.dmm_addr, HP3478A_CMD_MASK_BTN_ONLY HP3478A_CMD_SRQ_CLEAR); - Delay_Ms(MENU_DEBOUNCE_MS); - return; } } @@ -2115,9 +2195,9 @@ void handle_menu_navigation(void) { int dots = dot_time / MENU_DOT_INTERVAL_MS; if (dots > 3) dots = 3; - for (int i = 0; i < dots; i++) strcat(disp_buffer, "."); + for (int i = 0; i < dots; i++) strcat(scratch.disp.full_cmd, "."); } - dmm_display(disp_buffer, HP3478A_DISP_TEXT_FAST); + dmm_display(scratch.disp.full_cmd, HP3478A_DISP_TEXT_FAST); if (elapsed > MENU_COMMIT_DELAY_MS) { // L0: main menu @@ -2129,7 +2209,7 @@ void handle_menu_navigation(void) { app.data.menu.timer = now; prepare_menu_base_string(); - dmm_display(disp_buffer, HP3478A_DISP_TEXT_FAST); + dmm_display(scratch.disp.full_cmd, HP3478A_DISP_TEXT_FAST); return; } // enter standard modes @@ -2144,6 +2224,12 @@ void handle_menu_navigation(void) { if (app.temp_sensor == SENS_TYPE_K) { // Type K is voltage based so skip wire select enter_feature_mode(MENU_TEMP); + } else if (app.temp_sensor == SENS_THERMISTOR) { + app.data.menu.layer = SUBMENU_TEMP_NTC; + app.menu_pos = 0; // default to 10K + app.data.menu.timer = now; + prepare_menu_base_string(); + dmm_display(scratch.disp.full_cmd, HP3478A_DISP_TEXT_FAST); } else { // wire select (for resistive) app.data.menu.layer = SUBMENU_TEMP_WIRE; @@ -2151,16 +2237,20 @@ void handle_menu_navigation(void) { app.data.menu.timer = now; prepare_menu_base_string(); - dmm_display(disp_buffer, HP3478A_DISP_TEXT_FAST); + dmm_display(scratch.disp.full_cmd, HP3478A_DISP_TEXT_FAST); } return; } - - // L2: wire select + // L2a: 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; + } // L2b: NTC select + else if (app.data.menu.layer == SUBMENU_TEMP_NTC) { + app.temp_ntc_preset = (ntc_preset_t)app.menu_pos; + enter_feature_mode(MENU_TEMP); + return; } } } @@ -2168,6 +2258,13 @@ 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(); @@ -2202,13 +2299,14 @@ void app_loop(void) { 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); + 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) { + if (stb & HP3478A_MASK_KEYBOARD_SRQ && + (now - app.ignore_input_start_ts) > MENU_LOCKOUT_MS) { enter_menu_mode(); } @@ -2288,7 +2386,7 @@ static void cmd_help(void) { } static void cmd_status(void) { - snprintf(tmp_buffer, sizeof(tmp_buffer), + snprintf(scratch.cmd.fmt_buf, sizeof(scratch.cmd.fmt_buf), "Stat:\r\n" " Target Addr: %d\r\n" " Internal DMM: %d\r\n" @@ -2298,15 +2396,15 @@ static void cmd_status(void) { " FW: " FW_VERSION "\r\n", app.target_addr, app.dmm_addr, app.gpib_timeout_ms, app.auto_read ? "ON" : "OFF", app.current_mode); - usb_send_text(tmp_buffer); + usb_send_text(scratch.cmd.fmt_buf); } static void process_command(void) { - if (!get_start_command(cmd_buffer, sizeof(cmd_buffer))) { + if (!get_start_command(scratch.cmd.line_buf, sizeof(scratch.cmd.line_buf))) { return; } - char* p_cmd = skip_spaces(cmd_buffer); + char* p_cmd = skip_spaces(scratch.cmd.line_buf); int is_cpp_cmd = (strncmp(p_cmd, "++", 2) == 0); int is_query = (strchr(p_cmd, '?') != NULL); @@ -2333,8 +2431,9 @@ static void process_command(void) { usb_send_text("ERR: Invalid Addr\r\n"); } else { // if no arg provided, show current - snprintf(tmp_buffer, sizeof(tmp_buffer), "%d\r\n", app.target_addr); - usb_send_text(tmp_buffer); + snprintf(scratch.cmd.fmt_buf, sizeof(scratch.cmd.fmt_buf), "%d\r\n", + app.target_addr); + usb_send_text(scratch.cmd.fmt_buf); } break; @@ -2381,9 +2480,9 @@ static void process_command(void) { usb_send_text("ERR: Range 1-60000\r\n"); } } else { - snprintf(tmp_buffer, sizeof(tmp_buffer), "%lu\r\n", + snprintf(scratch.cmd.fmt_buf, sizeof(scratch.cmd.fmt_buf), "%lu\r\n", app.gpib_timeout_ms); - usb_send_text(tmp_buffer); + usb_send_text(scratch.cmd.fmt_buf); } break; case CMD_TRG: @@ -2427,8 +2526,9 @@ static void process_command(void) { uint8_t poll_addr = (*p_args) ? atoi(p_args) : app.target_addr; uint8_t stb; if (gpib_serial_poll(poll_addr, &stb) == 0) { - snprintf(tmp_buffer, sizeof(tmp_buffer), "%d\r\n", stb); - usb_send_text(tmp_buffer); + snprintf(scratch.cmd.fmt_buf, sizeof(scratch.cmd.fmt_buf), "%d\r\n", + stb); + usb_send_text(scratch.cmd.fmt_buf); } else usb_send_text("ERR: Bus\r\n"); break; @@ -2447,10 +2547,10 @@ static void process_command(void) { while (p_args[i] != 0 && i < HP_DISP_LEN) { char c = p_args[i]; if (c >= 'a' && c <= 'z') c -= 32; - disp_buffer[i++] = c; + scratch.disp.full_cmd[i++] = c; } - disp_buffer[i] = 0; - dmm_display(disp_buffer, HP3478A_DISP_TEXT_FAST); + scratch.disp.full_cmd[i] = 0; + dmm_display(scratch.disp.full_cmd, HP3478A_DISP_TEXT_FAST); usb_send_text("OK\r\n"); break; } @@ -2538,9 +2638,10 @@ static void process_command(void) { return; do_read_operation: { - int len = gpib_receive(app.target_addr, resp_buffer, sizeof(resp_buffer)); + int len = gpib_receive(app.target_addr, scratch.io.raw_data, + sizeof(scratch.io.raw_data)); if (len > 0) { - usb_send_text(resp_buffer); + usb_send_text(scratch.io.raw_data); } else if (is_cpp_cmd || is_query) { usb_send_text("ERR: Read Timeout\r\n"); } @@ -2574,20 +2675,9 @@ int main() { 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, HP3478A_CMD_MASK_BTN_ONLY HP3478A_CMD_SRQ_CLEAR); - gpib_go_to_local(app.dmm_addr); - break; - } - Delay_Ms(100); - } - - app.dmm_online = 1; + app.last_poll_time = 0; + app.ignore_input_start_ts = millis() - 2000; + app.dmm_online = 0; while (1) { handle_usb_state();