Files
hp3478a_ext/main.c
2025-12-01 21:46:39 +06:00

2698 lines
72 KiB
C

/*
* 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>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include "aht20.h"
#include "ch32fun.h"
#include "fsusb.h"
#include "gpib_defs.h"
#include "i2c_bitbang.h"
#include "systick.h"
#define FW_VERSION "1.1.0"
#define MY_ADDR 0
#define DEFAULT_DMM_ADDR 18 // the HP3478A addr
#define PIN_VBUS PB10
#define PIN_BUZZ PC13
#define USB_HW_IS_ACTIVE() (!((USBFSCTX.USBFS_DevSleepStatus) & 0x02))
// Timing Config
#define USB_DEBOUNCE_CONNECT_MS 50
#define USB_DEBOUNCE_DISCONNECT_MS 200
#define USB_SEND_TIMEOUT_MS 50
#define ENV_SENSOR_READ_INTERVAL_MS 1000
#define DEFAULT_GPIB_TIMEOUT_MS 3000
// 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
#define MENU_LOCKOUT_MS 1000 // How long to ignore SRQ for after exiting menu
// Polling
#define POLL_INTERVAL_MS 100 // 10Hz polling when in Passthrough
#define DMM_RECOVERY_DELAY_MS 1000 // Backoff if DMM vanishes
// Diode sound
#define DIODE_TH_SHORT 0.050 // Volts (below this = SHORT)
#define DIODE_TH_OPEN 2.500 // Volts (above this = OPEN/OL)
#define DIODE_STABLE_MS 20 // wait X ms for voltage to settle
#define DIODE_CHIRP_MS 50
// PT1000 (DIN 43760 / IEC 751 Standard)
#define RTD_A 3.9083e-3
#define RTD_B -5.775e-7
#define RTD_R0 1000.0
// Thermocouple
#define CJC_FALLBACK_TEMP 22.0 // used if !app.env_sensor_present
// this is just cursed but.. yeah, the PCB is near a transformer
// ideally, the temp should be measured right at the binding posts..
#define CJC_SELF_HEATING_OFFSET 4.0
#define TYPE_K_SCALE 24390.24 // 1 / 41uV
// dBm
#define DBM_REF_Z 50.0
// Stats
#define STATS_CYCLE_TIME_MS 3000 // time per screen (Live -> Avg -> Min...)
#define STATS_INIT_MIN_VAL 1.0e9
#define STATS_INIT_MAX_VAL -1.0e9
// HP3478A
#define HP_DISP_LEN 12 // 12 chars
// max str length required to fill 12 segments.
// Worst case: 12 chars + 12 dots/commas + 1 Null terminator = 25 bytes
#define HP_DISP_BUF_SIZE ((HP_DISP_LEN * 2) + 1)
#define CONT_THRESHOLD_OHMS 10.0 // continuity beep threshold
#define CONT_DISP_UPDATE_MS 250 // display throttling
#define DMM_OL_THRESHOLD 9.0e9 // HP sends +9.9999E+9 on overload
#define DMM_OL_NEG_THRESHOLD -9.0e9
#define REL_STABLE_SAMPLES 3 // filter depth for Relative NULL
typedef enum {
CMD_UNKNOWN = 0,
// Prologix / Standard GPIB
CMD_ADDR,
CMD_AUTO,
CMD_READ,
CMD_WRITE,
CMD_TRG,
CMD_CLR,
CMD_DCL,
CMD_SPOLL,
CMD_LOC,
CMD_GTL,
CMD_LLO,
CMD_REN,
CMD_IFC,
CMD_STAT,
CMD_RST,
CMD_VER,
CMD_HELP,
// HP3478A Features
CMD_CONT,
CMD_TEMP,
CMD_REL,
CMD_XOHM,
CMD_DBM,
CMD_DIODE,
CMD_MATH,
CMD_NORM,
CMD_DISP,
CMD_ENV,
CMD_TIMEOUT
} cmd_id_t;
typedef struct {
const char* name;
cmd_id_t id;
} cmd_entry_t;
static const cmd_entry_t COMMAND_TABLE[] = {
// common
{"read", CMD_READ},
{"write", CMD_WRITE},
{"addr", CMD_ADDR},
{"trg", CMD_TRG},
{"auto", CMD_AUTO},
{"stat", CMD_STAT},
// feats
{"cont", CMD_CONT},
{"temp", CMD_TEMP},
{"rel", CMD_REL},
{"xohm", CMD_XOHM},
{"dbm", CMD_DBM},
{"diode", CMD_DIODE},
{"math", CMD_MATH},
{"norm", CMD_NORM},
{"disp", CMD_DISP},
{"env", CMD_ENV},
// GPIB mgmt
{"read_tmo_ms", CMD_TIMEOUT},
{"tmo", CMD_TIMEOUT},
{"clr", CMD_CLR},
{"dcl", CMD_DCL},
{"spoll", CMD_SPOLL},
{"loc", CMD_LOC},
{"gtl", CMD_GTL},
{"llo", CMD_LLO},
{"ren", CMD_REN},
{"ifc", CMD_IFC},
{"rst", CMD_RST},
{"ver", CMD_VER},
{"help", CMD_HELP},
{"?", CMD_HELP},
{NULL, CMD_UNKNOWN}};
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_DIODE, // Diode test mode
MODE_FEAT_XOHM, // Extended Ohms active
MODE_FEAT_STATS // avg/min/max
} work_mode_t;
typedef enum {
MENU_REL = 0,
MENU_TEMP,
MENU_DBM,
MENU_CONT,
MENU_DIODE,
MENU_XOHM,
MENU_STATS,
MENU_EXIT,
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"};
static const char* SENSOR_NAMES[] = {"PT1000", "THERM", "TYPE K"};
static const char* WIRE_NAMES[] = {"2-WIRE", "4-WIRE"};
// some common NTC defs
static const ntc_def_t NTC_DEFS[] = {
// Generic/China
{10000.0, 3950.0, "10K 3950"}, // black bead)
{10000.0, 3435.0, "10K 3435"}, // euro?
{50000.0, 3950.0, "50K 3950"}, // generic
{100000.0, 3950.0, "100K 3950"}, // generic
// From YSI datasheet
{2252.0, 3891.0, "2.252K YSI"}, // 44004
{3000.0, 3891.0, "3K YSI"}, // 44005
{5000.0, 3891.0, "5K YSI"}, // 44007
{10000.0, 3574.0, "10K YSI A"}, // Mix H (YSI 10k)
{10000.0, 3891.0, "10K YSI B"}, // Mix B (matches 2.2K curve)
{30000.0, 3810.0, "30K YSI"}, // 44008
{100000.0, 3988.0, "100K YSI"}, // 44011
{1000000.0, 4582.0, "1MEG YSI"} // 44015
};
// Sub-menu states
typedef enum {
SUBMENU_NONE = 0,
SUBMENU_TEMP_SENS, // step 1: sensor Type
SUBMENU_TEMP_WIRE, // step 2a: wire mode (skipped for Type K)
SUBMENU_TEMP_NTC // step 2b: NTC Value (Thermistor only)
} submenu_state_t;
// Sensor Types
typedef enum {
SENS_PT1000 = 0,
SENS_THERMISTOR,
SENS_TYPE_K,
SENS_MAX_ITEMS
} temp_sensor_t;
typedef enum { WIRE_2W = 0, WIRE_4W, WIRE_MAX_ITEMS } wire_mode_t;
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_DONE // chirped, latched silent
} diode_state_t;
typedef struct {
const char* cmd_func;
const char* cmd_range;
const char* cmd_digits;
const char* cmd_az;
char unit_str[5]; // "VDC", "OHM", etc.
} dmm_decoded_state_t;
typedef union {
struct {
double offset;
uint8_t stable_count;
char unit[5];
} rel;
struct {
double min;
double max;
double sum;
uint32_t count;
uint32_t disp_timer;
uint8_t view_mode;
char unit[5];
} stats;
struct {
double r1;
uint8_t calibrated;
} xohm;
struct {
uint8_t connected; // touchy?
uint32_t chirp_start; // when we began the touchy
} diode;
struct {
// we're using very fast ADC mode for cont, so throttle disp update
// although.. ideally this should be done globally somehow
uint32_t last_disp_update;
} cont;
// 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 {
// 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 beep_active : 1;
uint8_t reserved : 1;
// Addresses
uint8_t target_addr;
uint8_t dmm_addr;
uint32_t gpib_timeout_ms;
// Timers
uint32_t usb_ts;
uint32_t env_last_read;
uint32_t last_poll_time;
uint32_t poll_interval;
uint32_t ignore_input_start_ts;
uint32_t beep_start_ts; // buzzer
uint32_t beep_duration;
// Logic
work_mode_t current_mode;
int menu_pos; // high-level menu position (REL, TEMP etc.)
// 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;
// DMM Restore
uint8_t saved_state_bytes[5];
// Display shadow buffer (cache)
char last_disp_sent[HP_DISP_BUF_SIZE];
// 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,
.poll_interval = POLL_INTERVAL_MS,
.gpib_timeout_ms = DEFAULT_GPIB_TIMEOUT_MS};
typedef union {
// MODE A: Command Processing Context
// Used when we are inside process_command()
struct {
char line_buf[128]; // Replaces: scratch.cmd.line_buf
char fmt_buf[128]; // Replaces: scratch.cmd.fmt_buf (For "Stat:", "OK",
// etc.)
} cmd;
// MODE B: General IO Context
// Used when reading from GPIB or calculating Features
struct {
// Replaces: scratch.io.raw_data
// Used for reading GPIB data ("+1.234E-3") before parsing
char raw_data[256];
} io;
// MODE C: Display Context
// Used when formatting text for the HP3478A
struct {
// Replaces: scratch.disp.full_cmd AND scratch.disp.full_cmd
// We will generate the final command directly here
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
volatile uint8_t usb_rx_buffer[USB_RX_BUF_SIZE];
volatile uint16_t usb_rx_head = 0;
volatile uint16_t usb_rx_tail = 0;
static uint8_t cdc_line_coding[7] = {0x00, 0xC2, 0x01, 0x00, 0x00, 0x00, 0x08};
volatile uint8_t buzzer_active = 0;
static uint32_t current_buzz_freq = 0;
extern volatile uint8_t usb_debug;
// helpers
static int starts_with_nocase(const char* str, const char* prefix) {
while (*prefix) {
if (tolower((unsigned char)*str) != tolower((unsigned char)*prefix)) {
return 0;
}
str++;
prefix++;
}
return 1;
}
static char* skip_spaces(char* str) {
while (*str && isspace((unsigned char)*str)) {
str++;
}
return str;
}
static cmd_id_t parse_command_id(const char* cmd) {
for (int i = 0; COMMAND_TABLE[i].name != NULL; i++) {
if (starts_with_nocase(cmd, COMMAND_TABLE[i].name)) {
int len = strlen(COMMAND_TABLE[i].name);
char next = cmd[len];
if (next == 0 || isspace((unsigned char)next)) {
return COMMAND_TABLE[i].id;
}
}
}
return CMD_UNKNOWN;
}
void double_to_str(char* buf, size_t buf_size, double val, int prec) {
if (buf_size == 0) return;
buf[0] = '\0';
size_t offset = 0;
// NaN/Inf
if (val != val) {
if (buf_size > 3) strcpy(buf, "NAN");
return;
}
// negative
if (val < 0.0) {
if (offset < buf_size - 1) buf[offset++] = '-';
val = -val;
}
// multiplier
if (prec < 0) prec = 0;
if (prec > 9) prec = 9; // limit precision
unsigned long long multiplier = 1;
for (int i = 0; i < prec; i++) multiplier *= 10;
// scale and round
val = (val * (double)multiplier) + 0.5;
// 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;
// print integer part
int res = snprintf(buf + offset, buf_size - offset, "%lu", int_part);
if (res < 0 || (size_t)res >= buf_size - offset) {
buf[buf_size - 1] = '\0';
return;
}
offset += (size_t)res;
// print decimal point and fraction
if (prec > 0 && offset < buf_size - 1) {
buf[offset++] = '.';
unsigned long long divider = multiplier / 10;
while (divider > 0 && offset < buf_size - 1) {
unsigned int digit = (unsigned int)(frac_part / divider);
buf[offset++] = (char)('0' + digit);
frac_part %= divider;
divider /= 10;
}
}
buf[offset] = '\0';
}
// "Note that period, comma, and semicolon go between characters"
int count_non_visual_chars(const char* s) {
int c = 0;
while (*s) {
if (*s == '.' || *s == ',' || *s == ';') c++;
s++;
}
return c;
}
void format_metric_value(char* buffer, size_t buf_len, double val,
const char* unit, int auto_scale) {
// must hold 12 visible chars + 1 null.
if (buf_len <= HP_DISP_LEN) return;
memset(buffer, ' ', HP_DISP_LEN);
buffer[HP_DISP_LEN] = '\0';
// scale
double scaled = val;
double abs_val = fabs(val);
char suffix = 0;
if (auto_scale) {
if (abs_val >= 1.0e9) {
scaled *= 1.0e-9;
suffix = 'G';
} else if (abs_val >= 1.0e6) {
scaled *= 1.0e-6;
suffix = 'M';
} else if (abs_val >= 1.0e3) {
scaled *= 1.0e-3;
suffix = 'K';
}
}
double abs_s = fabs(scaled);
// unit + suffix
size_t unit_len = strlen(unit);
size_t suffix_len = (suffix != 0) ? 1 : 0;
size_t meta_vis_len = unit_len + suffix_len;
if (meta_vis_len > (HP_DISP_LEN - 3)) {
// truncate unit?
}
int int_digits = 1;
if (abs_s >= 1.0) {
double t = abs_s;
while (t >= 10.0) {
t /= 10.0;
int_digits++;
}
}
int is_neg = (scaled < 0.0);
// visual slots needed for non-decimal part (sign + ints + space separator)
int separator = (meta_vis_len > 0) ? 1 : 0;
int reserved_vis = is_neg + int_digits + separator + meta_vis_len;
// calculate allowed decimals (visual)
int allowed_prec = HP_DISP_LEN - reserved_vis;
if (allowed_prec < 0) allowed_prec = 0;
int desired_prec = 5;
if (abs_s >= 10000.0)
desired_prec = 1;
else if (abs_s >= 1000.0)
desired_prec = 2;
else if (abs_s >= 100.0)
desired_prec = 3;
else if (abs_s >= 10.0)
desired_prec = 4;
int final_prec = (desired_prec < allowed_prec) ? desired_prec : allowed_prec;
// render num
char num_buf[32];
double_to_str(num_buf, sizeof(num_buf), scaled, final_prec);
// calc padding
int num_bytes = strlen(num_buf);
int num_dots = count_non_visual_chars(num_buf);
int num_vis_len = num_bytes - num_dots;
// total visual slots used so far
int total_vis_used = num_vis_len + meta_vis_len;
// to push unit to the right edge
int spaces_needed = HP_DISP_LEN - total_vis_used;
if (spaces_needed < 1 && meta_vis_len > 0)
spaces_needed = 1; // enforce 1 space if possible?
if (total_vis_used + spaces_needed > HP_DISP_LEN) {
// crunch: if overflow (very large number), reduce spaces
spaces_needed = HP_DISP_LEN - total_vis_used;
if (spaces_needed < 0) spaces_needed = 0;
}
// [number] [spaces] [suffix] [unit]
// num
strcpy(buffer, num_buf);
// spaces
int current_len = strlen(buffer);
for (int i = 0; i < spaces_needed; i++) {
buffer[current_len++] = ' ';
}
buffer[current_len] = '\0';
// suffix
if (suffix) {
buffer[current_len++] = suffix;
buffer[current_len] = '\0';
}
// unit
if (unit_len > 0) {
strcat(buffer, unit);
}
}
double parse_double(const char* s) {
double mantissa = 0.0;
int exponent = 0;
int sign = 1;
int decimal_seen = 0;
int decimal_counts = 0;
// skip whitespace
while (*s == ' ') s++;
// handle sign
if (*s == '+') {
s++;
} else if (*s == '-') {
sign = -1;
s++;
}
// parse Mantissa as pure int
while (*s) {
if (*s >= '0' && *s <= '9') {
mantissa = (mantissa * 10.0) + (*s - '0');
if (decimal_seen) {
decimal_counts++;
}
} else if (*s == '.') {
decimal_seen = 1;
} else if (*s == 'E' || *s == 'e') {
s++;
exponent = atoi(s);
break;
} else {
break;
}
s++;
}
exponent -= decimal_counts;
double power = 1.0;
int e_abs = abs(exponent);
while (e_abs-- > 0) power *= 10.0;
if (exponent > 0) {
mantissa *= power;
} else {
mantissa /= power;
}
return mantissa * sign;
}
void decode_dmm_state_bytes(const uint8_t* state_bytes,
dmm_decoded_state_t* out) {
uint8_t b1 = state_bytes[0];
uint8_t b2 = state_bytes[1];
// decode function & unit
switch ((b1 >> 5) & 0x07) {
case 2:
out->cmd_func = HP3478A_FUNC_AC_VOLTS;
strcpy(out->unit_str, "VAC");
break;
case 3:
out->cmd_func = HP3478A_FUNC_OHMS_2WIRE;
strcpy(out->unit_str, "OHM");
break;
case 4:
out->cmd_func = HP3478A_FUNC_OHMS_4WIRE;
strcpy(out->unit_str, "OHM");
break;
case 5:
out->cmd_func = HP3478A_FUNC_DC_CURRENT;
strcpy(out->unit_str, "ADC");
break;
case 6:
out->cmd_func = HP3478A_FUNC_AC_CURRENT;
strcpy(out->unit_str, "AAC");
break;
case 7:
out->cmd_func = HP3478A_FUNC_OHMS_EXT;
strcpy(out->unit_str, "OHM");
break;
default: // case 1 or anything else
out->cmd_func = HP3478A_FUNC_DC_VOLTS;
strcpy(out->unit_str, "VDC");
break;
}
// decode range (bits 4-2) + autorange (byte 2 bit 1)
if (b2 & 0x02) {
out->cmd_range = HP3478A_RANGE_AUTO;
} else {
switch ((b1 >> 2) & 0x07) {
case 1:
out->cmd_range = HP3478A_RANGE_NEG_2;
break;
case 2:
out->cmd_range = HP3478A_RANGE_NEG_1;
break;
case 3:
out->cmd_range = HP3478A_RANGE_0;
break;
case 4:
out->cmd_range = HP3478A_RANGE_1;
break;
case 5:
out->cmd_range = HP3478A_RANGE_2;
break;
case 6:
out->cmd_range = HP3478A_RANGE_3;
break;
case 7:
out->cmd_range = HP3478A_RANGE_4;
break;
default:
out->cmd_range = HP3478A_RANGE_0;
break;
}
}
// decode digits (bits 1-0)
switch (b1 & 0x03) {
case 2:
out->cmd_digits = HP3478A_DIGITS_4_5;
break;
case 3:
out->cmd_digits = HP3478A_DIGITS_3_5;
break;
default:
out->cmd_digits = HP3478A_DIGITS_5_5;
break;
}
// decode autozero (byte 2 bit 2)
out->cmd_az = (b2 & 0x04) ? HP3478A_AUTOZERO_ON : HP3478A_AUTOZERO_OFF;
}
// constructs the base configuration string (F R N Z) from decoded state
void build_restoration_string(char* buffer, const dmm_decoded_state_t* state) {
strcpy(buffer, HP3478A_DISP_NORMAL);
strcat(buffer, state->cmd_func);
strcat(buffer, state->cmd_range);
strcat(buffer, state->cmd_digits);
strcat(buffer, state->cmd_az);
}
#ifdef GPIB_DEBUG
static void gpib_dump_state(const char* context) {
uint8_t d = 0;
if (!GPIB_READ(PIN_DIO1)) d |= 0x01;
if (!GPIB_READ(PIN_DIO2)) d |= 0x02;
if (!GPIB_READ(PIN_DIO3)) d |= 0x04;
if (!GPIB_READ(PIN_DIO4)) d |= 0x08;
if (!GPIB_READ(PIN_DIO5)) d |= 0x10;
if (!GPIB_READ(PIN_DIO6)) d |= 0x20;
if (!GPIB_READ(PIN_DIO7)) d |= 0x40;
if (!GPIB_READ(PIN_DIO8)) d |= 0x80;
printf("\n[GPIB DUMP] %s\n", context);
printf(" M: ATN=%d IFC=%d REN=%d EOI=%d | S: SRQ=%d\n", GPIB_READ(PIN_ATN),
GPIB_READ(PIN_IFC), GPIB_READ(PIN_REN), GPIB_READ(PIN_EOI),
GPIB_READ(PIN_SRQ));
printf(" H: DAV=%d NRFD=%d NDAC=%d\n", GPIB_READ(PIN_DAV),
GPIB_READ(PIN_NRFD), GPIB_READ(PIN_NDAC));
printf(" D: 0x%02X\n", d);
}
#else
#define gpib_dump_state(x) ((void)0)
#endif
// low level
void gpib_write_data(uint8_t b) {
uint32_t bshr = 0;
if (b & 0x01)
bshr |= (MASK_DIO1 << 16);
else
bshr |= MASK_DIO1;
if (b & 0x02)
bshr |= (MASK_DIO2 << 16);
else
bshr |= MASK_DIO2;
if (b & 0x04)
bshr |= (MASK_DIO3 << 16);
else
bshr |= MASK_DIO3;
if (b & 0x08)
bshr |= (MASK_DIO4 << 16);
else
bshr |= MASK_DIO4;
if (b & 0x10)
bshr |= (MASK_DIO5 << 16);
else
bshr |= MASK_DIO5;
if (b & 0x20)
bshr |= (MASK_DIO6 << 16);
else
bshr |= MASK_DIO6;
if (b & 0x40)
bshr |= (MASK_DIO7 << 16);
else
bshr |= MASK_DIO7;
if (b & 0x80)
bshr |= (MASK_DIO8 << 16);
else
bshr |= MASK_DIO8;
GPIOB->BSHR = bshr;
}
uint8_t gpib_read_data(void) {
uint32_t r = ~(GPIOB->INDR); // active low
uint8_t b = 0;
if (r & MASK_DIO1) b |= 0x01;
if (r & MASK_DIO2) b |= 0x02;
if (r & MASK_DIO3) b |= 0x04;
if (r & MASK_DIO4) b |= 0x08;
if (r & MASK_DIO5) b |= 0x10;
if (r & MASK_DIO6) b |= 0x20;
if (r & MASK_DIO7) b |= 0x40;
if (r & MASK_DIO8) b |= 0x80;
return b;
}
static int gpib_wait_pin(int pin, int expected_state) {
uint32_t start = millis();
while (GPIB_READ(pin) != expected_state) {
if ((millis() - start) > app.gpib_timeout_ms) {
#ifdef GPIB_DEBUG
// Print which specific pin failed
char* pin_name = "UNKNOWN";
if (pin == PIN_NRFD)
pin_name = "NRFD";
else if (pin == PIN_NDAC)
pin_name = "NDAC";
else if (pin == PIN_DAV)
pin_name = "DAV";
printf("[GPIB ERR] Timeout waiting for %s to be %d\n", pin_name,
expected_state);
gpib_dump_state("TIMEOUT STATE");
#endif
return -1;
}
}
return 0;
}
int gpib_write_byte(uint8_t data, int assert_eoi) {
#ifdef GPIB_DEBUG
printf("[TX] 0x%02X (EOI=%d)... ", data, assert_eoi);
#endif
// wait for listeners to be ready
if (gpib_wait_pin(PIN_NRFD, 1) < 0) {
return -1;
}
gpib_write_data(data);
// assert EOI if this is the last byte
if (assert_eoi) {
GPIB_ASSERT(PIN_EOI);
}
Delay_Us(1); // T1
GPIB_ASSERT(PIN_DAV);
// wait for listeners ack
if (gpib_wait_pin(PIN_NDAC, 1) < 0) {
GPIB_RELEASE(PIN_DAV);
GPIB_RELEASE(PIN_EOI);
#ifdef GPIB_DEBUG
printf("NDAC stuck LOW, (device didn't accept)\n");
#endif
return -2;
}
GPIB_RELEASE(PIN_DAV);
GPIB_RELEASE(PIN_EOI);
// float bus
gpib_write_data(0x00);
return 0;
}
int gpib_read_byte(uint8_t* data, int* eoi_asserted) {
// sssert busy state
GPIB_ASSERT(PIN_NDAC); // not accepted yet
GPIB_ASSERT(PIN_NRFD); // not ready yet
// float data lines
gpib_write_data(0x00);
// Delay_Us(2);
// signal ready for data
GPIB_RELEASE(PIN_NRFD);
// wait for talker to assert DAV
if (gpib_wait_pin(PIN_DAV, 0) < 0) {
GPIB_RELEASE(PIN_NDAC);
GPIB_RELEASE(PIN_NRFD);
return -1; // timeout
}
Delay_Us(1); // T2
// read data and EOI status
*data = gpib_read_data();
*eoi_asserted = (GPIB_READ(PIN_EOI) == 0); // active LOW
// signal not ready (processing data)
GPIB_ASSERT(PIN_NRFD);
// signal data accepted
GPIB_RELEASE(PIN_NDAC);
// wait for talker to release DAV
if (gpib_wait_pin(PIN_DAV, 1) < 0) {
GPIB_RELEASE(PIN_NRFD);
return -2; // timeout
}
// prepare for next byte
GPIB_ASSERT(PIN_NDAC);
return 0;
}
typedef enum { SESSION_WRITE, SESSION_READ } session_mode_t;
// Sets up Talker/Listener for data transfer
int gpib_start_session(uint8_t target_addr, session_mode_t mode) {
GPIB_ASSERT(PIN_ATN);
Delay_Us(20);
// Unlisten everyone first to clear bus state
if (gpib_write_byte(GPIB_CMD_UNL, 0) < 0) {
GPIB_RELEASE(PIN_ATN);
return -1;
}
uint8_t talker = (mode == SESSION_WRITE) ? MY_ADDR : target_addr;
uint8_t listener = (mode == SESSION_WRITE) ? target_addr : MY_ADDR;
// Untalk, Set Talker, Set Listener
if (gpib_write_byte(GPIB_CMD_UNT, 0) < 0) goto err;
if (gpib_write_byte(GPIB_CMD_TAD | talker, 0) < 0) goto err;
if (gpib_write_byte(GPIB_CMD_LAD | listener, 0) < 0) goto err;
Delay_Us(10);
GPIB_RELEASE(PIN_ATN); // Switch to Data Mode
Delay_Us(10);
return 0;
err:
GPIB_RELEASE(PIN_ATN);
return -1;
}
// Bus management
// Assert Interface Clear (IFC)
void gpib_interface_clear(void) {
GPIB_ASSERT(PIN_IFC);
Delay_Ms(1); // IEEE-488 requires >100us
GPIB_RELEASE(PIN_IFC);
Delay_Ms(1);
}
// Control Remote Enable (REN)
void gpib_remote_enable(int enable) {
if (enable) {
GPIB_ASSERT(PIN_REN);
} else {
GPIB_RELEASE(PIN_REN);
}
}
// Check SRQ Line (Active Low)
int gpib_check_srq(void) { return !GPIB_READ(PIN_SRQ); }
// Universal Commands (Affects All Devices)
// Universal Device Clear (DCL)
// Resets logic of ALL devices on the bus
int gpib_universal_clear(void) {
GPIB_ASSERT(PIN_ATN);
Delay_Us(20);
if (gpib_write_byte(GPIB_CMD_DCL, 0) < 0) {
GPIB_RELEASE(PIN_ATN);
return -1;
}
Delay_Us(10);
GPIB_RELEASE(PIN_ATN);
return 0;
}
// Local Lockout (LLO)
// Disables front panel "Local" buttons on all devices
int gpib_local_lockout(void) {
GPIB_ASSERT(PIN_ATN);
Delay_Us(20);
// LLO is universal, no addressing needed
if (gpib_write_byte(GPIB_CMD_LLO, 0) < 0) {
GPIB_RELEASE(PIN_ATN);
return -1;
}
Delay_Us(10);
GPIB_RELEASE(PIN_ATN);
return 0;
}
// Addressed cmds
// Selected Device Clear (SDC)
// Resets logic of ONLY the targeted device
int gpib_device_clear(uint8_t addr) {
GPIB_ASSERT(PIN_ATN);
Delay_Us(20);
if (gpib_write_byte(GPIB_CMD_UNL, 0) < 0) goto err;
if (gpib_write_byte(GPIB_CMD_LAD | addr, 0) < 0) goto err;
if (gpib_write_byte(GPIB_CMD_SDC, 0) < 0) goto err;
GPIB_RELEASE(PIN_ATN);
return 0;
err:
GPIB_RELEASE(PIN_ATN);
return -1;
}
// Group Execute Trigger (GET)
// Triggers the device to take a measurement
int gpib_trigger(uint8_t addr) {
GPIB_ASSERT(PIN_ATN);
Delay_Us(20);
if (gpib_write_byte(GPIB_CMD_UNL, 0) < 0) goto err;
if (gpib_write_byte(GPIB_CMD_LAD | addr, 0) < 0) goto err;
if (gpib_write_byte(GPIB_CMD_GET, 0) < 0) goto err;
GPIB_RELEASE(PIN_ATN);
return 0;
err:
GPIB_RELEASE(PIN_ATN);
return -1;
}
// Go To Local (GTL)
// Addresses a specific device and restores Front Panel control
// (Keeps REN asserted for other devices on the bus)
int gpib_go_to_local(uint8_t addr) {
GPIB_ASSERT(PIN_ATN);
Delay_Us(20);
if (gpib_write_byte(GPIB_CMD_UNL, 0) < 0) goto err;
if (gpib_write_byte(GPIB_CMD_LAD | addr, 0) < 0) goto err;
if (gpib_write_byte(GPIB_CMD_GTL, 0) < 0) goto err;
GPIB_RELEASE(PIN_ATN);
return 0;
err:
GPIB_RELEASE(PIN_ATN);
return -1;
}
// Serial Poll
// Reads the Status Byte (STB) from the device
int gpib_serial_poll(uint8_t addr, uint8_t* status) {
GPIB_ASSERT(PIN_ATN);
Delay_Us(20);
// setupo seq: UNL -> SPE -> LAD(Me) -> TAD(Target)
if (gpib_write_byte(GPIB_CMD_UNL, 0) < 0) goto err;
if (gpib_write_byte(GPIB_CMD_SPE, 0) < 0) goto err;
if (gpib_write_byte(GPIB_CMD_LAD | MY_ADDR, 0) < 0) goto err;
if (gpib_write_byte(GPIB_CMD_TAD | addr, 0) < 0) goto err;
// drop ATN to read data
GPIB_RELEASE(PIN_ATN);
Delay_Us(5);
int eoi;
int res = gpib_read_byte(status, &eoi);
// handshake complete, clean up lines
GPIB_RELEASE(PIN_NRFD);
GPIB_RELEASE(PIN_NDAC);
// end seq: ATN -> SPD -> UNT
GPIB_ASSERT(PIN_ATN);
Delay_Us(5);
gpib_write_byte(GPIB_CMD_SPD, 0); // disable spoll
gpib_write_byte(GPIB_CMD_UNT, 0); // untalk
GPIB_RELEASE(PIN_ATN);
return res;
err:
GPIB_RELEASE(PIN_ATN);
return -1;
}
// Data transfer
// Send string to device (handles CRLF escape sequences)
int gpib_send(uint8_t addr, const char* str) {
if (gpib_start_session(addr, SESSION_WRITE) < 0) return -1;
int len = strlen(str);
for (int i = 0; i < len; i++) {
uint8_t b = str[i];
int skip = 0;
// escape sequence handling (\n, \r)
if (b == '\\' && i < len - 1) {
if (str[i + 1] == 'n') {
b = 0x0A;
skip = 1;
} else if (str[i + 1] == 'r') {
b = 0x0D;
skip = 1;
}
}
// tag the last byte with EOI
int is_last = (i == len - 1) || (skip && i == len - 2);
if (gpib_write_byte(b, is_last) < 0) {
// error during write, try to clean up bus
GPIB_ASSERT(PIN_ATN);
gpib_write_byte(GPIB_CMD_UNL, 0);
GPIB_RELEASE(PIN_ATN);
return -1;
}
if (skip) i++;
}
// normal cleanup
GPIB_ASSERT(PIN_ATN);
gpib_write_byte(GPIB_CMD_UNL, 0);
GPIB_RELEASE(PIN_ATN);
return 0;
}
// Receive string from device
int gpib_receive(uint8_t addr, char* buf, int max_len) {
if (gpib_start_session(addr, SESSION_READ) < 0) return -1;
int count = 0;
int eoi = 0;
uint8_t byte;
while (count < max_len - 1) {
if (gpib_read_byte(&byte, &eoi) < 0) break;
buf[count++] = (char)byte;
// stop on EOI or LF
if (eoi || byte == '\n') break;
}
buf[count] = 0; // null terminate
// 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;
}
// 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;
Delay_Ms(2); // give device time to process
return gpib_receive(addr, buf, max_len);
}
void gpib_init(void) {
// configure control lines as open-drain outputs
funPinMode(PIN_EOI, GPIO_Speed_50MHz | GPIO_CNF_OUT_OD);
funPinMode(PIN_REN, GPIO_Speed_50MHz | GPIO_CNF_OUT_OD);
funPinMode(PIN_ATN, GPIO_Speed_50MHz | GPIO_CNF_OUT_OD);
funPinMode(PIN_IFC, GPIO_Speed_50MHz | GPIO_CNF_OUT_OD);
funPinMode(PIN_DAV, GPIO_Speed_50MHz | GPIO_CNF_OUT_OD);
funPinMode(PIN_NDAC, GPIO_Speed_50MHz | GPIO_CNF_OUT_OD);
funPinMode(PIN_NRFD, GPIO_Speed_50MHz | GPIO_CNF_OUT_OD);
// SRQ is input with pull-up
funPinMode(PIN_SRQ, GPIO_CNF_IN_PUPD);
funDigitalWrite(PIN_SRQ, 1);
// release all control lines to idle (HIGH)
GPIB_RELEASE(PIN_EOI);
GPIB_RELEASE(PIN_REN);
GPIB_RELEASE(PIN_ATN);
GPIB_RELEASE(PIN_IFC);
GPIB_RELEASE(PIN_DAV);
GPIB_RELEASE(PIN_NDAC);
GPIB_RELEASE(PIN_NRFD);
// data lines
funPinMode(PIN_DIO1, GPIO_Speed_50MHz | GPIO_CNF_OUT_OD);
funPinMode(PIN_DIO2, GPIO_Speed_50MHz | GPIO_CNF_OUT_OD);
funPinMode(PIN_DIO3, GPIO_Speed_50MHz | GPIO_CNF_OUT_OD);
funPinMode(PIN_DIO4, GPIO_Speed_50MHz | GPIO_CNF_OUT_OD);
funPinMode(PIN_DIO5, GPIO_Speed_50MHz | GPIO_CNF_OUT_OD);
funPinMode(PIN_DIO6, GPIO_Speed_50MHz | GPIO_CNF_OUT_OD);
funPinMode(PIN_DIO7, GPIO_Speed_50MHz | GPIO_CNF_OUT_OD);
funPinMode(PIN_DIO8, GPIO_Speed_50MHz | GPIO_CNF_OUT_OD);
// float data lines (release to HIGH)
gpib_write_data(0x00);
#ifdef GPIB_DEBUG
printf("[GPIB] Asserting IFC...\n");
#endif
gpib_interface_clear();
#ifdef GPIB_DEBUG
gpib_dump_state("INIT DONE");
// if no device is connected: NRFD/NDAC/DAV should all be 1
// if device is connected: NRFD/NDAC might be 0
#endif
}
// ------------------------------------
void buzzer_init(void) {
funPinMode(PIN_BUZZ, GPIO_Speed_50MHz | GPIO_CNF_OUT_PP);
funDigitalWrite(PIN_BUZZ, 0);
RCC->APB1PCENR |= RCC_TIM2EN;
TIM2->PSC = (FUNCONF_SYSTEM_CORE_CLOCK / 1000000) - 1;
TIM2->ATRLR = 250;
TIM2->DMAINTENR |= TIM_UIE;
NVIC_EnableIRQ(TIM2_IRQn);
TIM2->CTLR1 |= TIM_CEN;
}
void buzzer_set(uint32_t freq_hz) {
if (current_buzz_freq == freq_hz) return;
current_buzz_freq = freq_hz;
if (freq_hz == 0) {
buzzer_active = 0;
return;
}
uint16_t reload_val = (uint16_t)(1000000UL / (2 * freq_hz));
TIM2->ATRLR = reload_val;
TIM2->CNT = 0; // reset phase only on CHANGE
buzzer_active = 1;
}
void TIM2_IRQHandler(void) __attribute__((interrupt));
void TIM2_IRQHandler(void) {
if (TIM2->INTFR & TIM_UIF) {
// clr the flag
TIM2->INTFR = (uint16_t)~TIM_UIF;
if (buzzer_active) {
// Toggle PC13
if (GPIOC->OUTDR & (1 << 13)) {
GPIOC->BSHR = (1 << (16 + 13)); // Reset (Low)
} else {
GPIOC->BSHR = (1 << 13); // Set (High)
}
} else {
// ensure low when inactive
if (GPIOC->OUTDR & (1 << 13)) {
GPIOC->BSHR = (1 << (16 + 13));
}
}
}
}
void tone(unsigned int freq_hz, unsigned int duration_ms) {
if (freq_hz == 0) {
Delay_Ms(duration_ms);
return;
}
buzzer_set(freq_hz);
Delay_Ms(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() {
tone(1500, 100);
Delay_Ms(25);
tone(2500, 100);
Delay_Ms(25);
tone(4000, 100);
}
// ------------------------------------
int HandleSetupCustom(struct _USBState* ctx, int setup_code) {
if (ctx->USBFS_SetupReqType & USB_REQ_TYP_CLASS) {
switch (setup_code) {
case 0x21: // CDC_GET_LINE_CODING
ctx->pCtrlPayloadPtr = cdc_line_coding;
return 7;
case 0x20: // CDC_SET_LINE_CODING
case 0x22: // CDC_SET_CONTROL_LINE_STATE
return 0;
}
}
return -1;
}
int HandleInRequest(struct _USBState* ctx __attribute__((unused)),
int endp __attribute__((unused)),
uint8_t* data __attribute__((unused)),
int len __attribute__((unused))) {
return 0;
}
void HandleDataOut(struct _USBState* ctx, int endp, uint8_t* data, int len) {
if (endp == 0) {
ctx->USBFS_SetupReqLen = 0;
} else if (endp == 2) {
// Copy to Ring Buffer
for (int i = 0; i < len; i++) {
uint16_t next_head = (usb_rx_head + 1) % USB_RX_BUF_SIZE;
if (next_head != usb_rx_tail) {
usb_rx_buffer[usb_rx_head] = data[i];
usb_rx_head = next_head;
}
}
}
}
static void usb_send_text(const char* str) {
if (!app.usb_online) return;
int len = strlen(str);
int pos = 0;
while (pos < len) {
int chunk = len - pos;
if (chunk > 64) chunk = 64;
uint32_t start_time = millis();
int sent = 0;
while ((millis() - start_time) < USB_SEND_TIMEOUT_MS) {
int result = USBFS_SendEndpointNEW(3, (uint8_t*)(str + pos), chunk, 1);
if (result == 0) {
sent = 1;
break;
}
}
if (!sent) {
return;
}
pos += chunk;
}
}
// pull a line from ring buffer
int get_start_command(char* dest_buf, int max_len) {
if (usb_rx_head == usb_rx_tail) return 0;
uint16_t temp_tail = usb_rx_tail;
int len = 0;
int found_newline = 0;
// Peek for newline
while (temp_tail != usb_rx_head) {
char c = usb_rx_buffer[temp_tail];
if (c == '\n' || c == '\r') {
found_newline = 1;
break;
}
temp_tail = (temp_tail + 1) % USB_RX_BUF_SIZE;
len++;
if (len >= max_len - 1) break;
}
if (found_newline) {
// copy out
for (int i = 0; i < len; i++) {
dest_buf[i] = usb_rx_buffer[usb_rx_tail];
usb_rx_tail = (usb_rx_tail + 1) % USB_RX_BUF_SIZE;
}
dest_buf[len] = 0;
// eat newline chars
while (usb_rx_tail != usb_rx_head) {
char c = usb_rx_buffer[usb_rx_tail];
if (c == '\r' || c == '\n') {
usb_rx_tail = (usb_rx_tail + 1) % USB_RX_BUF_SIZE;
} else {
break;
}
}
return len;
}
return 0;
}
// ----------------------------------------
static void handle_usb_state(void) {
int raw_status = USB_HW_IS_ACTIVE();
uint32_t now = millis();
// edge detection
if (raw_status != app.usb_raw_prev) {
app.usb_ts = now;
app.usb_raw_prev = raw_status;
}
// debounce with different thresholds for connect/disconnect
uint32_t threshold =
raw_status ? USB_DEBOUNCE_CONNECT_MS : USB_DEBOUNCE_DISCONNECT_MS;
if ((now - app.usb_ts) > threshold) {
// state has been stable long enough
if (app.usb_online != raw_status) {
app.usb_online = raw_status;
if (app.usb_online) usb_rx_tail = usb_rx_head = 0;
}
}
}
static void handle_env_sensor(void) {
if (!app.env_sensor_present) {
return;
}
uint32_t now = millis();
if ((now - app.env_last_read) >= ENV_SENSOR_READ_INTERVAL_MS) {
if (aht20_read(&app.current_env) == AHT20_OK) {
app.env_last_read = now;
}
}
}
// helper to write text to HP3478A display
void dmm_display(const char* text, const char* mode) {
// cmp vs shadow buf
if (strncmp(app.last_disp_sent, text, HP_DISP_BUF_SIZE) == 0) {
return;
}
// printf("Updating Display: %s\n", text);
// update shadow buf
strncpy(app.last_disp_sent, text, HP_DISP_BUF_SIZE - 1);
// "D[23]" + Text + "\n"
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, scratch.disp.full_cmd);
}
static inline void dmm_display_normal(void) {
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) {
// default fallback if no state saved
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;
}
dmm_decoded_state_t state;
decode_dmm_state_bytes(app.saved_state_bytes, &state);
// "D1 Fx Rx Nx Zx" + "T1 M20"
build_restoration_string(scratch.cmd.line_buf, &state);
strcat(scratch.cmd.line_buf, HP3478A_TRIG_INTERNAL HP3478A_CMD_MASK_BTN_ONLY);
gpib_send(app.dmm_addr, scratch.cmd.line_buf);
}
void exit_to_passthrough(void) {
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.beep_active = 0;
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
scratch.cmd.line_buf[0] = '\0';
gpib_remote_enable(1); // make sure REN is asserted
// we will hold the decoded state here for REL/STATS
dmm_decoded_state_t saved_cfg;
switch (item) {
case MENU_REL:
if (app.has_saved_state) {
decode_dmm_state_bytes(app.saved_state_bytes, &saved_cfg);
build_restoration_string(scratch.cmd.line_buf, &saved_cfg);
strcat(scratch.cmd.line_buf,
HP3478A_TRIG_INTERNAL HP3478A_CMD_MASK_BTN_DATA
HP3478A_CMD_SRQ_CLEAR);
strcpy(app.data.rel.unit, saved_cfg.unit_str);
} else {
// we have no saved state? don't know relative to WHAT
dmm_display("ERR NO STATE", HP3478A_DISP_TEXT_FAST);
}
gpib_send(app.dmm_addr, scratch.cmd.line_buf);
app.current_mode = MODE_FEAT_REL;
app.data.rel.offset = 0.0;
dmm_display("REL MODE", HP3478A_DISP_TEXT_FAST);
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", HP3478A_DISP_TEXT_FAST);
break;
case MENU_TEMP:
app.current_mode = MODE_FEAT_TEMP;
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", 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):
// 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);
}
if (app.temp_sensor == SENS_PT1000)
dmm_display("TEMP PT1000", HP3478A_DISP_TEXT_FAST);
// else
// dmm_display("TEMP THERM", HP3478A_DISP_TEXT_FAST);
}
break;
case MENU_CONT:
// F3: 2W Ohm, R0: 30 Ohm Range, N3: 3.5 Digits (fastest ADC), M21
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;
dmm_display("CONT MODE", HP3478A_DISP_TEXT_FAST);
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", HP3478A_DISP_TEXT_FAST);
break;
case MENU_XOHM:
// H7: High Impedance / Extended Ohm Mode
// The DMM puts internal 10M in parallel with input
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.0;
app.data.xohm.calibrated = 0;
app.current_mode = MODE_FEAT_XOHM;
dmm_display("XOHM 10M REF", HP3478A_DISP_TEXT_FAST);
break;
case MENU_STATS:
if (app.has_saved_state) {
decode_dmm_state_bytes(app.saved_state_bytes, &saved_cfg);
build_restoration_string(scratch.cmd.line_buf, &saved_cfg);
strcat(scratch.cmd.line_buf,
HP3478A_TRIG_INTERNAL HP3478A_CMD_MASK_BTN_DATA
HP3478A_CMD_SRQ_CLEAR);
strcpy(app.data.stats.unit, saved_cfg.unit_str);
} else {
// fallback
strcpy(scratch.cmd.line_buf,
HP3478A_FUNC_DC_VOLTS HP3478A_RANGE_AUTO HP3478A_DIGITS_5_5
HP3478A_AUTOZERO_ON HP3478A_TRIG_INTERNAL
HP3478A_CMD_MASK_BTN_DATA HP3478A_CMD_SRQ_CLEAR);
strcpy(app.data.stats.unit, "VDC");
}
gpib_send(app.dmm_addr, scratch.cmd.line_buf);
app.current_mode = MODE_FEAT_STATS;
// Initialize Stats
app.data.stats.min = DBL_MAX; // start impossible high
app.data.stats.max = -DBL_MAX; // start impossible low
app.data.stats.sum = 0.0;
app.data.stats.count = 0;
app.data.stats.view_mode = 0;
app.data.stats.disp_timer = millis();
dmm_display("STATS INIT", HP3478A_DISP_TEXT_FAST);
break;
case MENU_EXIT:
default:
exit_to_passthrough();
break;
}
}
void enter_menu_mode(void) {
// force display refresh
app.last_disp_sent[0] = '\0';
save_dmm_state();
app.current_mode = MODE_MENU;
app.menu_pos = MENU_REL;
app.data.menu.timer = millis();
dmm_display("M: REL", HP3478A_DISP_TEXT_FAST);
gpib_send(app.dmm_addr, HP3478A_CMD_MASK_BTN_ONLY HP3478A_CMD_SRQ_CLEAR);
}
void handle_feature_logic(void) {
uint8_t stb = 0;
gpib_serial_poll(app.dmm_addr, &stb);
// exit button (SRQ)
if (stb & HP3478A_MASK_KEYBOARD_SRQ) {
exit_to_passthrough();
return;
}
// data ready (Bit 0)
if (!(stb & HP3478A_MASK_DATA_READY)) return;
// read measurement
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");
app.current_mode = MODE_PASSTHROUGH;
app.dmm_online = 0;
gpib_interface_clear();
return;
}
double val = parse_double(scratch.io.raw_data);
// overload (HP 3478A sends +9.99990E+9 for OL)
int is_overload = 0;
if (val < DMM_OL_NEG_THRESHOLD || val > DMM_OL_THRESHOLD) is_overload = 1;
// RELATIVE MODE
if (app.current_mode == MODE_FEAT_REL) {
if (app.data.rel.offset == 0.0) {
// waiting to capture the NULL value
if (is_overload) {
app.data.rel.stable_count = 0; // reset counter if probes are open
dmm_display("O.VLD", HP3478A_DISP_TEXT_FAST);
} else {
// valid reading
app.data.rel.stable_count++;
if (app.data.rel.stable_count >= REL_STABLE_SAMPLES) {
app.data.rel.offset = val;
dmm_display("NULL SET", HP3478A_DISP_TEXT_FAST);
tone_nb(3000, 50);
app.data.rel.stable_count = 0;
} else {
dmm_display("LOCKING...", HP3478A_DISP_TEXT_FAST);
}
}
} else {
// offset is already set
if (is_overload) {
dmm_display("O.VLD", HP3478A_DISP_TEXT_FAST);
} else {
double diff = val - app.data.rel.offset;
char eff_unit[5];
// prepend 'D'
snprintf(eff_unit, sizeof(eff_unit), "D%s", app.data.rel.unit);
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);
}
}
}
// dBm MODE
else if (app.current_mode == MODE_FEAT_DBM) {
if (is_overload) {
dmm_display("O.VLD", HP3478A_DISP_TEXT_FAST);
} else {
// P(mW) = (V^2 / 50) * 1000 = V^2 * 20
double p_mw = (val * val * 20.0);
if (p_mw < 1e-9) {
// Align -INF to look consistent
// Display: "-INF DBM"
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(scratch.disp.full_cmd,
sizeof(scratch.disp.full_cmd), dbm, "DBM", 00);
dmm_display(scratch.disp.full_cmd, HP3478A_DISP_TEXT);
}
}
}
// TEMP MODE
else if (app.current_mode == MODE_FEAT_TEMP) {
if (is_overload && app.temp_sensor != SENS_TYPE_K) {
dmm_display("OPEN / ERR", HP3478A_DISP_TEXT_FAST);
} else {
double temp_c = 0.0;
const char* unit_str = " C";
// TYPE K THERMOCOUPLE
if (app.temp_sensor == SENS_TYPE_K) {
// 30mV range safety check (Floating input > 50mV)
if (fabs(val) > 0.050) {
dmm_display("CHECK PROBE", HP3478A_DISP_TEXT_FAST);
return;
} else {
double t_amb;
// Cold Junction Compensation (CJC)
if (app.env_sensor_present) {
t_amb = app.current_env.temp_c_x100 / 100.0;
t_amb -= CJC_SELF_HEATING_OFFSET;
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);
}
}
// PT1000 RTD (Callendar-Van Dusen)
else if (app.temp_sensor == SENS_PT1000) {
if (val < 10.0) {
dmm_display("SHORT", HP3478A_DISP_TEXT_FAST);
return;
} else {
double c = RTD_R0 - val;
double b = RTD_R0 * RTD_A;
double a = RTD_R0 * RTD_B;
double disc = (b * b) - (4 * a * c);
if (disc >= 0)
temp_c = (-b + sqrt(disc)) / (2 * a);
else {
dmm_display("RANGE ERR", HP3478A_DISP_TEXT_FAST);
return;
}
}
}
// THERMISTOR (simplified Steinhart-Hart)
else {
if (val < 10.0) {
dmm_display("SHORT", HP3478A_DISP_TEXT_FAST);
return;
}
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(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
else if (app.current_mode == MODE_FEAT_CONT) {
int is_short = (!is_overload && val < CONT_THRESHOLD_OHMS);
// instant beep
buzzer_set(is_short ? 2500 : 0);
uint32_t now = millis();
if (now - app.data.cont.last_disp_update > CONT_DISP_UPDATE_MS) {
app.data.cont.last_disp_update = now;
if (is_overload) {
dmm_display("OPEN", HP3478A_DISP_TEXT_FAST);
} else {
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();
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(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;
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) {
// has been stable long enough
tone_nb(2500, 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;
}
// XOHM MODE
else if (app.current_mode == MODE_FEAT_XOHM) {
// cal phase, measure the internal 10M resistor
if (app.data.xohm.calibrated == 0) {
// need the probes to be open. Internal R is ~10M
if (val > 8.0e6 && val < 12.0e6) {
app.data.xohm.r1 = val; // Store R1
app.data.xohm.calibrated = 1;
tone_nb(3000, 100);
} else {
dmm_display("OPEN PROBES", HP3478A_DISP_TEXT_FAST);
}
}
// Rx = (R1 * R2) / (R1 - R2)
// R1 = xohm_ref (Internal)
// R2 = val (Measured Parallel)
else {
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(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) {
if (is_overload) {
dmm_display("O.VLD", HP3478A_DISP_TEXT_FAST);
} 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 += 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
const char* prefix_str = "";
double val_to_show = val;
switch (app.data.stats.view_mode) {
case 0: // Live
val_to_show = val;
break;
case 1: // Avg
prefix_str = "AVG ";
val_to_show = app.data.stats.sum / app.data.stats.count;
break;
case 2: // Min
prefix_str = "MIN ";
val_to_show = app.data.stats.min;
break;
case 3: // Max
prefix_str = "MAX ";
val_to_show = app.data.stats.max;
break;
default:
app.data.stats.view_mode = 0;
break;
}
// render
if (app.data.stats.view_mode == 0) {
// live mode
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)
double abs_v = fabs(val_to_show);
double scaled = val_to_show;
char suffix = 0;
if (abs_v >= 1.0e9) {
scaled *= 1.0e-9;
suffix = 'G';
} else if (abs_v >= 1.0e6) {
scaled *= 1.0e-6;
suffix = 'M';
} else if (abs_v >= 1.0e3) {
scaled *= 1.0e-3;
suffix = 'K';
}
char num_buf[16];
double_to_str(num_buf, sizeof(num_buf), scaled, 4);
// combine: prefix + number + suffix
snprintf(scratch.disp.full_cmd, sizeof(scratch.disp.full_cmd),
"%s%s%c ", prefix_str, num_buf, suffix ? suffix : ' ');
}
// send it
dmm_display(scratch.disp.full_cmd, HP3478A_DISP_TEXT);
}
}
}
// 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 = "???";
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];
} 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(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;
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);
return;
}
}
}
prepare_menu_base_string();
// only calculate dots if we are past the initial delay
if (elapsed > MENU_SUBLAYER_DELAY_MS) {
uint32_t dot_time = elapsed - MENU_SUBLAYER_DELAY_MS;
int dots = dot_time / MENU_DOT_INTERVAL_MS;
if (dots > 3) dots = 3;
for (int i = 0; i < dots; i++) strcat(scratch.disp.full_cmd, ".");
}
dmm_display(scratch.disp.full_cmd, HP3478A_DISP_TEXT_FAST);
if (elapsed > MENU_COMMIT_DELAY_MS) {
// 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;
prepare_menu_base_string();
dmm_display(scratch.disp.full_cmd, HP3478A_DISP_TEXT_FAST);
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 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;
app.menu_pos = 0; // default to 2W
app.data.menu.timer = now;
prepare_menu_base_string();
dmm_display(scratch.disp.full_cmd, HP3478A_DISP_TEXT_FAST);
}
return;
}
// 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;
}
}
}
void app_loop(void) {
uint32_t now = millis();
if (app.beep_active) {
if ((now - app.beep_start_ts) >= app.beep_duration) {
buzzer_set(0);
app.beep_active = 0;
}
}
// Passthrough
if (app.current_mode == MODE_PASSTHROUGH) {
int srq_asserted = gpib_check_srq();
int time_to_poll = (now - app.last_poll_time) >= app.poll_interval;
// if disconnected, we only try to reconnect on timer ticks
if (!app.dmm_online && !time_to_poll) return;
// if online, we poll if SRQ is pulled OR timer expires
if (app.dmm_online && !srq_asserted && !time_to_poll) return;
app.last_poll_time = now;
uint8_t stb = 0;
// try to talk to DMM
int poll_result = gpib_serial_poll(app.dmm_addr, &stb);
if (poll_result != 0) {
// poll failed (Timeout/NACK)
if (app.dmm_online) {
// printf("DMM Lost connection.\n");
app.dmm_online = 0;
gpib_interface_clear();
}
// slow down polling when offline
app.poll_interval = DMM_RECOVERY_DELAY_MS;
return;
}
// got a valid response, check if this is a recovery
if (!app.dmm_online) {
// printf("DMM Recovered.\n");
app.dmm_online = 1;
gpib_send(app.dmm_addr, HP3478A_CMD_MASK_BTN_ONLY HP3478A_CMD_SRQ_CLEAR);
gpib_go_to_local(app.dmm_addr);
tone_nb(4000, 50);
app.poll_interval = POLL_INTERVAL_MS; // restore fast polling
return;
}
// valid and online, check buttons
if (stb & HP3478A_MASK_KEYBOARD_SRQ &&
(now - app.ignore_input_start_ts) > MENU_LOCKOUT_MS) {
enter_menu_mode();
}
return;
}
// Nav
if (app.current_mode == MODE_MENU) {
handle_menu_navigation();
return;
}
// Features
// early exit if no SRQ
if (!gpib_check_srq()) {
return;
}
uint8_t stb;
if (gpib_serial_poll(app.dmm_addr, &stb) != 0) {
// DMM crashed during feature mode
// printf("Feature crash: DMM Lost\n");
app.current_mode = MODE_PASSTHROUGH;
app.dmm_online = 0;
gpib_interface_clear();
return;
}
// check exit button first (priority)
if (stb & HP3478A_MASK_KEYBOARD_SRQ) {
exit_to_passthrough();
return;
}
// handle measurement data ready
if (stb & HP3478A_MASK_DATA_READY) {
handle_feature_logic();
}
}
static void cmd_help(void) {
static const char* help_text =
"\r\n=== HP3478A Internal USB-GPIB v" FW_VERSION
" ===\r\n"
"\r\n"
"Prologix-style Commands:\r\n"
" ++addr <N> Set Target GPIB Address (0-30)\r\n"
" ++auto <0|1> 0=Off, 1=Read-After-Write\r\n"
" ++read Read data from current target\r\n"
" ++write <D> Write data <D> to target\r\n"
" ++trg Trigger (GET) - Target\r\n"
" ++clr Device Clear (SDC) - Target\r\n"
" ++dcl Device Clear (DCL) - All Devices\r\n"
" ++spoll [A] Serial Poll (Target or Addr A)\r\n"
" ++loc Local Mode (Drop REN Line)\r\n"
" ++gtl Go To Local (GTL) - Target Only\r\n"
" ++llo Local Lockout (Disable front panels)\r\n"
" ++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"
" ++rst System Reboot\r\n"
"\r\n"
"HP3478A Internal Commands:\r\n"
" ++cont, ++temp, ++rel, ++xohm, ++dbm\r\n"
" ++diode, ++math (Min/Max/Avg)\r\n"
" ++norm Exit Special Mode\r\n"
" ++disp <msg> Text message on LCD (Max 12)\r\n"
" ++env [temp|hum] Internal Sensor (Default: csv)\r\n"
"\r\n"
"Usage:\r\n"
" Commands starting with ++ are executed locally.\r\n"
" All other data is sent to the target GPIB device.\r\n"
" Input '?' or '? <cmd>' for this help.\r\n";
usb_send_text(help_text);
}
static void cmd_status(void) {
snprintf(scratch.cmd.fmt_buf, sizeof(scratch.cmd.fmt_buf),
"Stat:\r\n"
" Target Addr: %d\r\n"
" Internal DMM: %d\r\n"
" Timeout: %lu ms\r\n"
" Auto Read: %s\r\n"
" Current Mode: %d\r\n"
" 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(scratch.cmd.fmt_buf);
}
static void process_command(void) {
if (!get_start_command(scratch.cmd.line_buf, sizeof(scratch.cmd.line_buf))) {
return;
}
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);
if (is_cpp_cmd) {
p_cmd += 2; // skip "++"
// 'p_args' will point to the first non-space char after the command word
char* p_args = p_cmd;
while (*p_args && !isspace((unsigned char)*p_args))
p_args++; // find end of word
p_args = skip_spaces(p_args); // find start of args
cmd_id_t cmd_id = parse_command_id(p_cmd);
switch (cmd_id) {
// config
case CMD_ADDR:
if (*p_args) {
int addr = atoi(p_args);
if (addr >= 0 && addr <= 30) {
app.target_addr = addr;
usb_send_text("OK\r\n");
} else
usb_send_text("ERR: Invalid Addr\r\n");
} else {
// if no arg provided, show current
snprintf(scratch.cmd.fmt_buf, sizeof(scratch.cmd.fmt_buf), "%d\r\n",
app.target_addr);
usb_send_text(scratch.cmd.fmt_buf);
}
break;
case CMD_AUTO:
if (*p_args) {
app.auto_read = atoi(p_args) ? 1 : 0;
usb_send_text("OK\r\n");
} else
usb_send_text(app.auto_read ? "1\r\n" : "0\r\n");
break;
case CMD_VER:
usb_send_text("HP3478A Internal USB-GPIB " FW_VERSION "\r\n");
break;
case CMD_STAT:
cmd_status();
break;
case CMD_HELP:
cmd_help();
break;
case CMD_RST:
NVIC_SystemReset();
break;
// data
case CMD_READ:
goto do_read_operation;
case CMD_WRITE:
if (*p_args) {
gpib_send(app.target_addr, p_args);
if (app.auto_read) goto do_read_operation;
}
break;
// GPIB Bus control
case CMD_TIMEOUT:
if (*p_args) {
int val = atoi(p_args);
// min 1ms, max 60s
if (val > 0 && val < 60000) {
app.gpib_timeout_ms = val;
usb_send_text("OK\r\n");
} else {
usb_send_text("ERR: Range 1-60000\r\n");
}
} else {
snprintf(scratch.cmd.fmt_buf, sizeof(scratch.cmd.fmt_buf), "%lu\r\n",
app.gpib_timeout_ms);
usb_send_text(scratch.cmd.fmt_buf);
}
break;
case CMD_TRG:
gpib_trigger(app.target_addr);
usb_send_text("OK\r\n");
break;
case CMD_CLR:
gpib_device_clear(app.target_addr);
usb_send_text("OK\r\n");
break;
case CMD_DCL:
gpib_universal_clear();
usb_send_text("OK\r\n");
break;
case CMD_IFC:
gpib_interface_clear();
usb_send_text("OK\r\n");
break;
case CMD_LLO:
gpib_local_lockout();
usb_send_text("OK\r\n");
break;
case CMD_GTL:
gpib_go_to_local(app.target_addr);
usb_send_text("OK\r\n");
break;
case CMD_LOC:
gpib_remote_enable(0);
usb_send_text("OK\r\n");
break;
case CMD_REN:
if (*p_args) {
gpib_remote_enable(atoi(p_args));
usb_send_text("OK\r\n");
} else
usb_send_text("ERR: Usage ++ren 1|0\r\n");
break;
case CMD_SPOLL: {
uint8_t poll_addr = (*p_args) ? atoi(p_args) : app.target_addr;
uint8_t stb;
if (gpib_serial_poll(poll_addr, &stb) == 0) {
snprintf(scratch.cmd.fmt_buf, sizeof(scratch.cmd.fmt_buf), "%d\r\n",
stb);
usb_send_text(scratch.cmd.fmt_buf);
} else
usb_send_text("ERR: Bus\r\n");
break;
}
// HP3478A Internal Features
case CMD_NORM:
exit_to_passthrough();
// ensure REN is back up for normal use
gpib_remote_enable(1);
usb_send_text("OK\r\n");
break;
case CMD_DISP: {
int i = 0;
while (p_args[i] != 0 && i < HP_DISP_LEN) {
char c = p_args[i];
if (c >= 'a' && c <= 'z') c -= 32;
scratch.disp.full_cmd[i++] = c;
}
scratch.disp.full_cmd[i] = 0;
dmm_display(scratch.disp.full_cmd, HP3478A_DISP_TEXT_FAST);
usb_send_text("OK\r\n");
break;
}
case CMD_ENV: {
if (!app.env_sensor_present) {
usb_send_text("ERR: No Sensor\r\n");
return;
}
double t = app.current_env.temp_c_x100 / 100.0;
double h = app.current_env.hum_p_x100 / 100.0;
char* arg = skip_spaces(p_args);
char out_buf[32];
// temp only
if (starts_with_nocase(arg, "temp")) {
double_to_str(out_buf, sizeof(out_buf), t, 2);
usb_send_text(out_buf);
usb_send_text("\r\n");
}
// hum only
else if (starts_with_nocase(arg, "hum")) {
double_to_str(out_buf, sizeof(out_buf), h, 2);
usb_send_text(out_buf);
usb_send_text("\r\n");
}
// CSV format (temp,hum)
else {
double_to_str(out_buf, 16, t, 2);
strcat(out_buf, ",");
// fmt humidity into a temp buffer and append
char h_buf[16];
double_to_str(h_buf, sizeof(h_buf), h, 2);
strcat(out_buf, h_buf);
strcat(out_buf, "\r\n");
usb_send_text(out_buf);
}
break;
}
// feat entry
case CMD_CONT:
case CMD_TEMP:
case CMD_REL:
case CMD_XOHM:
case CMD_DBM:
case CMD_DIODE:
case CMD_MATH: {
menu_item_t item = MENU_EXIT;
if (cmd_id == CMD_CONT)
item = MENU_CONT;
else if (cmd_id == CMD_TEMP)
item = MENU_TEMP;
else if (cmd_id == CMD_REL)
item = MENU_REL;
else if (cmd_id == CMD_XOHM)
item = MENU_XOHM;
else if (cmd_id == CMD_DBM)
item = MENU_DBM;
else if (cmd_id == CMD_DIODE)
item = MENU_DIODE;
else if (cmd_id == CMD_MATH)
item = MENU_STATS;
save_dmm_state(); // save before changing
enter_feature_mode(item);
usb_send_text("OK\r\n");
break;
}
default:
usb_send_text("ERR: Unknown cmd\r\n");
break;
}
return;
}
// passthrough mode (not "++")
if (gpib_send(app.target_addr, p_cmd) < 0) {
usb_send_text("ERR: Send Fail\r\n");
return;
}
if (is_query || app.auto_read) goto do_read_operation;
return;
do_read_operation: {
int len = gpib_receive(app.target_addr, scratch.io.raw_data,
sizeof(scratch.io.raw_data));
if (len > 0) {
usb_send_text(scratch.io.raw_data);
} else if (is_cpp_cmd || is_query) {
usb_send_text("ERR: Read Timeout\r\n");
}
}
}
int main() {
SystemInit();
systick_init();
funGpioInitAll();
// Buzzer setup
buzzer_init();
// I2C sensor
i2c_init();
app.env_sensor_present = aht20_init() == AHT20_OK ? 1 : 0;
// GPIB controller
gpib_init();
gpib_remote_enable(1);
// USB interface
USBFSSetup();
// usb_debug = 1;
play_startup_tune();
// app state
app.current_mode = MODE_PASSTHROUGH;
app.usb_online = 0;
app.usb_raw_prev = USB_HW_IS_ACTIVE();
app.usb_ts = millis();
app.last_poll_time = 0;
app.ignore_input_start_ts = millis() - 2000;
app.dmm_online = 0;
while (1) {
handle_usb_state();
app_loop();
handle_env_sensor();
if (app.usb_online) {
process_command();
}
}
}