ok, let's stop

This commit is contained in:
2025-12-01 21:46:39 +06:00
parent 63f6bb5b04
commit abe722f88c
2 changed files with 295 additions and 204 deletions

View File

@@ -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"
}

496
main.c
View File

@@ -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 <ctype.h>
#include <float.h>
#include <math.h>
@@ -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();