2351 lines
60 KiB
C
2351 lines
60 KiB
C
#include <ctype.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 ENV_SENSOR_READ_INTERVAL_MS 1000
|
|
#define GPIB_TIMEOUT_MS 100
|
|
|
|
// 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
|
|
|
|
// 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.050f // Volts (below this = SHORT)
|
|
#define DIODE_TH_OPEN 2.500f // Volts (above this = OPEN/OL)
|
|
#define DIODE_STABLE_MS 20 // wait X ms for voltage to settle
|
|
#define DIODE_CHIRP_MS 50
|
|
|
|
// PT1000 (DIN 43760 / IEC 751 Standard)
|
|
#define RTD_A 3.9083e-3f
|
|
#define RTD_B -5.775e-7f
|
|
#define RTD_R0 1000.0f
|
|
|
|
// Thermistor
|
|
// TODO: different ranges? 5k, 10k etc.?
|
|
// This is a 5k NTC!!! kinda useless here
|
|
#define THERM_A 0.001286f
|
|
#define THERM_B 0.00023595f
|
|
#define THERM_C 0.0000000941f
|
|
|
|
// Thermocouple
|
|
#define CJC_FALLBACK_TEMP 22.0f // used if !app.env_sensor_present
|
|
// this is just cursed but.. yeah, the PCB is near a transformer
|
|
// ideally, the temp should be measured right at the binding posts..
|
|
#define CJC_SELF_HEATING_OFFSET 4.0f
|
|
#define TYPE_K_SCALE 24390.24f // 1 / 41uV
|
|
// dBm
|
|
#define DBM_REF_Z 50.0f
|
|
// Stats
|
|
#define STATS_CYCLE_TIME_MS 3000 // time per screen (Live -> Avg -> Min...)
|
|
#define STATS_INIT_MIN_VAL 1.0e9f
|
|
#define STATS_INIT_MAX_VAL -1.0e9f
|
|
|
|
// HP3478A
|
|
#define HP_DISP_LEN 13 // 13 chars
|
|
#define CONT_THRESHOLD_OHMS 10.0f // continuity beep threshold
|
|
#define HP_OVERLOAD_VAL 9.0e9f // HP sends +9.9999E+9 on overload
|
|
#define REL_STABLE_SAMPLES 3 // filter depth for Relative NULL
|
|
|
|
typedef enum {
|
|
MODE_PASSTHROUGH = 0, // Standard USB-GPIB bridge
|
|
MODE_MENU, // User is cycling options on DMM display
|
|
MODE_FEAT_REL, // Relative Mode active
|
|
MODE_FEAT_TEMP, // PT1000 Temp Mode active
|
|
MODE_FEAT_DBM, // Power ratio using a 50R impedance as ref
|
|
MODE_FEAT_CONT, // Continuity Mode active
|
|
MODE_FEAT_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 struct {
|
|
const char* name;
|
|
float a;
|
|
float b;
|
|
float c;
|
|
} thermistor_def_t;
|
|
|
|
// UI Strings
|
|
static const char* MENU_NAMES[] = {"REL", "TEMP", "DBM", "CONT",
|
|
"DIODE", "XOHM", "STATS", "EXIT"};
|
|
|
|
static const char* SENSOR_NAMES[] = {"PT1000", "THERM", "TYPE K"};
|
|
|
|
static const char* WIRE_NAMES[] = {"2-WIRE", "4-WIRE"};
|
|
|
|
// Sub-menu states
|
|
typedef enum {
|
|
SUBMENU_NONE = 0,
|
|
SUBMENU_TEMP_SENS, // step 1: sensor Type
|
|
SUBMENU_TEMP_WIRE // step 2: wire mode (skipped for Type K)
|
|
} submenu_state_t;
|
|
|
|
// Sensor Types
|
|
typedef enum {
|
|
SENS_PT1000 = 0,
|
|
SENS_THERMISTOR,
|
|
SENS_TYPE_K,
|
|
SENS_MAX_ITEMS
|
|
} temp_sensor_t;
|
|
typedef enum { WIRE_2W = 0, WIRE_4W, WIRE_MAX_ITEMS } wire_mode_t;
|
|
typedef enum { CONT_SHORT = 0, CONT_OPEN } cont_state_t;
|
|
|
|
typedef enum {
|
|
DIODE_STATE_OPEN = 0, // probes open
|
|
DIODE_STATE_CHECKING, // voltage @ diode range, checking stability
|
|
DIODE_STATE_SHORT, // voltage too low, silent
|
|
DIODE_STATE_PLAYING, // is a diode, chirping
|
|
DIODE_STATE_DONE // chirped, latched silent
|
|
} diode_state_t;
|
|
|
|
typedef union {
|
|
struct {
|
|
float offset;
|
|
uint8_t stable_count;
|
|
} rel;
|
|
|
|
struct {
|
|
float min;
|
|
float max;
|
|
float sum;
|
|
uint32_t count;
|
|
uint32_t disp_timer;
|
|
uint8_t view_mode;
|
|
} stats;
|
|
|
|
struct {
|
|
float r1;
|
|
uint8_t calibrated;
|
|
} xohm;
|
|
|
|
struct {
|
|
uint32_t disp_timer;
|
|
int last_state;
|
|
} cont;
|
|
|
|
struct {
|
|
uint8_t connected; // touchy?
|
|
uint32_t chirp_start; // when we began the touchy
|
|
} diode;
|
|
|
|
// menu state (only used when in MODE_MENU)
|
|
struct {
|
|
uint32_t timer;
|
|
uint8_t layer; // submenu_state_t
|
|
uint8_t sub_pos; // position in submenu
|
|
} menu;
|
|
|
|
} mode_data_t;
|
|
|
|
typedef struct {
|
|
// Hardware State Flags
|
|
uint8_t usb_online : 1;
|
|
uint8_t usb_raw_prev : 1;
|
|
uint8_t env_sensor_present : 1;
|
|
uint8_t auto_read : 1;
|
|
uint8_t dmm_online : 1;
|
|
uint8_t has_saved_state : 1;
|
|
uint8_t reserved : 2;
|
|
|
|
// Addresses
|
|
uint8_t target_addr;
|
|
uint8_t dmm_addr;
|
|
|
|
// Timers
|
|
uint32_t usb_ts;
|
|
uint32_t env_last_read;
|
|
uint32_t last_poll_time;
|
|
uint32_t poll_interval;
|
|
|
|
// 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;
|
|
|
|
// Environmental Data
|
|
aht20_data current_env;
|
|
|
|
// DMM Restore
|
|
uint8_t saved_state_bytes[5];
|
|
|
|
// Display shadow buffer
|
|
char last_disp_sent[HP_DISP_LEN + 1];
|
|
|
|
// Unionized Mode Data
|
|
mode_data_t data;
|
|
} app_state_t;
|
|
|
|
static app_state_t app = {.dmm_addr = DEFAULT_DMM_ADDR,
|
|
.target_addr = DEFAULT_DMM_ADDR,
|
|
.poll_interval = POLL_INTERVAL_MS};
|
|
|
|
// Buffers
|
|
static char cmd_buffer[128];
|
|
static char resp_buffer[256];
|
|
static char tmp_buffer[128];
|
|
static char disp_buffer[HP_DISP_LEN + 1];
|
|
// Size: "D3"(2) + Text(12) + "\n"(1) + Null(1) = 16 bytes
|
|
static char disp_cmd_buffer[2 + HP_DISP_LEN + 1 + 1];
|
|
|
|
// USB Ring Buffer
|
|
#define USB_RX_BUF_SIZE 512
|
|
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;
|
|
}
|
|
|
|
void fmt_float(char* buf, size_t size, float val, int precision) {
|
|
if (val != val) {
|
|
snprintf(buf, size, "NAN");
|
|
return;
|
|
}
|
|
if (val > 3.4e38f) {
|
|
snprintf(buf, size, "INF");
|
|
return;
|
|
}
|
|
|
|
if (val < 0.0f) {
|
|
*buf++ = '-';
|
|
val = -val;
|
|
size--;
|
|
}
|
|
|
|
float rounder = 0.5f;
|
|
for (int i = 0; i < precision; i++) rounder *= 0.1f;
|
|
val += rounder;
|
|
|
|
uint32_t int_part = (uint32_t)val;
|
|
float remainder = val - (float)int_part;
|
|
|
|
int len = snprintf(buf, size, "%lu", int_part);
|
|
if (len < 0 || (size_t)len >= size) return;
|
|
|
|
buf += len;
|
|
size -= len;
|
|
|
|
if (precision > 0 && size > 1) {
|
|
*buf++ = '.';
|
|
size--;
|
|
while (precision-- > 0 && size > 1) {
|
|
remainder *= 10.0f;
|
|
int digit = (int)remainder;
|
|
if (digit > 9) digit = 9;
|
|
*buf++ = '0' + digit;
|
|
remainder -= digit;
|
|
size--;
|
|
}
|
|
*buf = 0;
|
|
}
|
|
}
|
|
|
|
// [NUMBER] + [SPACES] + [UNIT]
|
|
// assumes buffer is at least HP_DISP_LEN + 1b
|
|
void format_aligned_display(char* buffer, float value, int precision,
|
|
const char* suffix) {
|
|
char num_str[16];
|
|
|
|
// fmt number to string
|
|
fmt_float(num_str, sizeof(num_str), value, precision);
|
|
|
|
int num_len = strlen(num_str);
|
|
int suf_len = strlen(suffix);
|
|
|
|
// reset buffer to spaces
|
|
memset(buffer, ' ', HP_DISP_LEN);
|
|
buffer[HP_DISP_LEN] = '\0';
|
|
|
|
// write number (left aligned)
|
|
// clamp to max display length
|
|
int copy_len = (num_len > HP_DISP_LEN) ? HP_DISP_LEN : num_len;
|
|
memcpy(buffer, num_str, copy_len);
|
|
|
|
// write suffix (right aligned)
|
|
int suf_start = HP_DISP_LEN - suf_len;
|
|
if (suf_start < 0) suf_start = 0;
|
|
|
|
memcpy(&buffer[suf_start], suffix, suf_len);
|
|
}
|
|
|
|
void format_resistance(char* buffer, size_t buf_len, float val) {
|
|
if (buf_len < HP_DISP_LEN + 1) return;
|
|
|
|
float scaled_val;
|
|
const char* suffix;
|
|
int prec;
|
|
|
|
// determine logic
|
|
if (val >= 1e9f) {
|
|
scaled_val = val / 1e9f;
|
|
suffix = " G";
|
|
prec = 4; // "1.1234 G"
|
|
} else if (val >= 1e6f) {
|
|
scaled_val = val / 1e6f;
|
|
suffix = " M";
|
|
prec = 4; // "10.1234 M"
|
|
} else if (val >= 1e3f) {
|
|
scaled_val = val / 1e3f;
|
|
suffix = " K";
|
|
prec = 4; // "100.1234 K"
|
|
} else {
|
|
scaled_val = val;
|
|
suffix = " OHM";
|
|
prec = 2; // "100.55 OHM"
|
|
}
|
|
|
|
format_aligned_display(buffer, scaled_val, prec, suffix);
|
|
}
|
|
|
|
float parse_float(const char* s) {
|
|
float res = 0.0f;
|
|
float fact = 1.0f;
|
|
int sign = 1;
|
|
int point_seen = 0;
|
|
|
|
while (*s == ' ') s++;
|
|
|
|
if (*s == '+')
|
|
s++;
|
|
else if (*s == '-') {
|
|
sign = -1;
|
|
s++;
|
|
}
|
|
|
|
// parse mantissa
|
|
while (*s) {
|
|
if (*s == '.') {
|
|
point_seen = 1;
|
|
} else if (*s >= '0' && *s <= '9') {
|
|
if (point_seen) {
|
|
fact /= 10.0f;
|
|
res += (*s - '0') * fact;
|
|
} else {
|
|
res = res * 10.0f + (*s - '0');
|
|
}
|
|
} else if (*s == 'E' || *s == 'e') {
|
|
s++; // skip 'E'
|
|
int exp = atoi(s);
|
|
|
|
// apply exponent
|
|
float power = 1.0f;
|
|
int exp_abs = abs(exp);
|
|
while (exp_abs--) power *= 10.0f;
|
|
|
|
if (exp > 0)
|
|
res *= power;
|
|
else
|
|
res /= power;
|
|
|
|
break;
|
|
} else {
|
|
break;
|
|
}
|
|
s++;
|
|
}
|
|
return res * sign;
|
|
}
|
|
|
|
#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) > 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 (auto-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));
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// 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);
|
|
return;
|
|
}
|
|
|
|
buzzer_set(freq_hz);
|
|
Delay_Ms(duration_ms);
|
|
buzzer_set(0);
|
|
}
|
|
|
|
void play_startup_tune() {
|
|
// "Boot Up"
|
|
tone(1500, 100);
|
|
Delay_Ms(20);
|
|
tone(2500, 100);
|
|
Delay_Ms(20);
|
|
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) {
|
|
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) {
|
|
int len = strlen(str);
|
|
int pos = 0;
|
|
while (pos < len) {
|
|
int chunk = len - pos;
|
|
if (chunk > 64) chunk = 64;
|
|
|
|
USBFS_SendEndpointNEW(3, (uint8_t*)(str + pos), chunk, 1);
|
|
Delay_Us(250); // yikes
|
|
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;
|
|
play_connected_tune();
|
|
} else {
|
|
play_disconnected_tune();
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
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
|
|
// CMD: D2 = Full alphanumeric message
|
|
void dmm_display(const char* text) {
|
|
if (strncmp(app.last_disp_sent, text, HP_DISP_LEN) == 0) {
|
|
return; // text hasn't changed
|
|
}
|
|
|
|
// upd cache
|
|
strncpy(app.last_disp_sent, text, HP_DISP_LEN);
|
|
app.last_disp_sent[HP_DISP_LEN] = 0;
|
|
|
|
// copy command
|
|
strcpy(disp_cmd_buffer, HP3478A_DISP_TEXT_FAST);
|
|
// append text
|
|
strncat(disp_cmd_buffer, text, HP_DISP_LEN);
|
|
// newline require for D2 and D3
|
|
strcat(disp_cmd_buffer, "\n");
|
|
gpib_send(app.dmm_addr, disp_cmd_buffer);
|
|
}
|
|
static inline void dmm_display_normal(void) {
|
|
gpib_send(app.dmm_addr, HP3478A_DISP_NORMAL);
|
|
// invalidate cache (we're giving control back to DMM)
|
|
app.last_disp_sent[0] = '\0';
|
|
}
|
|
|
|
void save_dmm_state(void) {
|
|
gpib_interface_clear();
|
|
gpib_send(app.dmm_addr, HP3478A_CMD_STATUS_BYTE);
|
|
|
|
int len = gpib_receive_binary(app.dmm_addr, (char*)app.saved_state_bytes, 5);
|
|
app.has_saved_state = (len == 5) ? 1 : 0;
|
|
}
|
|
|
|
void restore_dmm_state(void) {
|
|
if (!app.has_saved_state) {
|
|
gpib_send(app.dmm_addr,
|
|
HP3478A_DISP_NORMAL HP3478A_FUNC_DC_VOLTS HP3478A_RANGE_AUTO
|
|
HP3478A_DIGITS_5_5 HP3478A_AUTOZERO_ON HP3478A_TRIG_INTERNAL
|
|
HP3478A_CMD_MASK_BTN_ONLY);
|
|
return;
|
|
}
|
|
|
|
uint8_t b1 = app.saved_state_bytes[0];
|
|
uint8_t b2 = app.saved_state_bytes[1];
|
|
|
|
// ptrs to string literals
|
|
const char* cmd_func;
|
|
const char* cmd_range;
|
|
const char* cmd_dig;
|
|
const char* cmd_az;
|
|
|
|
switch ((b1 >> 5) & 0x07) {
|
|
case 2:
|
|
cmd_func = HP3478A_FUNC_AC_VOLTS;
|
|
break;
|
|
case 3:
|
|
cmd_func = HP3478A_FUNC_OHMS_2WIRE;
|
|
break;
|
|
case 4:
|
|
cmd_func = HP3478A_FUNC_OHMS_4WIRE;
|
|
break;
|
|
case 5:
|
|
cmd_func = HP3478A_FUNC_DC_CURRENT;
|
|
break;
|
|
case 6:
|
|
cmd_func = HP3478A_FUNC_AC_CURRENT;
|
|
break;
|
|
case 7:
|
|
cmd_func = HP3478A_FUNC_OHMS_EXT;
|
|
break;
|
|
default:
|
|
cmd_func = HP3478A_FUNC_DC_VOLTS;
|
|
break;
|
|
}
|
|
|
|
// Decode Range (Bits 4-2) + AutoRange (Byte 2 Bit 1)
|
|
if (b2 & 0x02) {
|
|
cmd_range = HP3478A_RANGE_AUTO;
|
|
} else {
|
|
// Octal 1=R-2 ... Octal 7=R4
|
|
switch ((b1 >> 2) & 0x07) {
|
|
case 1:
|
|
cmd_range = HP3478A_RANGE_NEG_2;
|
|
break;
|
|
case 2:
|
|
cmd_range = HP3478A_RANGE_NEG_1;
|
|
break;
|
|
case 3:
|
|
cmd_range = HP3478A_RANGE_0;
|
|
break;
|
|
case 4:
|
|
cmd_range = HP3478A_RANGE_1;
|
|
break;
|
|
case 5:
|
|
cmd_range = HP3478A_RANGE_2;
|
|
break;
|
|
case 6:
|
|
cmd_range = HP3478A_RANGE_3;
|
|
break;
|
|
case 7:
|
|
cmd_range = HP3478A_RANGE_4;
|
|
break;
|
|
default:
|
|
cmd_range = HP3478A_RANGE_0;
|
|
break;
|
|
}
|
|
}
|
|
|
|
// Decode Digits (Bits 1-0)
|
|
switch (b1 & 0x03) {
|
|
case 2:
|
|
cmd_dig = HP3478A_DIGITS_4_5;
|
|
break;
|
|
case 3:
|
|
cmd_dig = HP3478A_DIGITS_3_5;
|
|
break;
|
|
default:
|
|
cmd_dig = HP3478A_DIGITS_5_5;
|
|
break;
|
|
}
|
|
|
|
// Decode AutoZero (Byte 2 Bit 2)
|
|
cmd_az = (b2 & 0x04) ? HP3478A_AUTOZERO_ON : HP3478A_AUTOZERO_OFF;
|
|
|
|
// "D1 Fx Rx Nx Zx T1 M20"
|
|
strcpy(cmd_buffer, HP3478A_DISP_NORMAL);
|
|
strcat(cmd_buffer, cmd_func);
|
|
strcat(cmd_buffer, cmd_range);
|
|
strcat(cmd_buffer, cmd_dig);
|
|
strcat(cmd_buffer, cmd_az);
|
|
strcat(cmd_buffer, HP3478A_TRIG_INTERNAL HP3478A_CMD_MASK_BTN_ONLY);
|
|
|
|
gpib_send(app.dmm_addr, cmd_buffer);
|
|
}
|
|
|
|
void exit_to_passthrough(void) {
|
|
buzzer_set(0); // mute
|
|
restore_dmm_state();
|
|
gpib_go_to_local(app.dmm_addr);
|
|
|
|
app.current_mode = MODE_PASSTHROUGH;
|
|
app.data.menu.layer = SUBMENU_NONE;
|
|
app.menu_pos = 0;
|
|
app.last_poll_time = millis();
|
|
|
|
Delay_Ms(MENU_DEBOUNCE_MS);
|
|
}
|
|
|
|
void enter_feature_mode(menu_item_t item) {
|
|
// force display refresh
|
|
app.last_disp_sent[0] = '\0';
|
|
|
|
gpib_remote_enable(1); // assert REN
|
|
Delay_Ms(20);
|
|
|
|
switch (item) {
|
|
case MENU_REL:
|
|
gpib_send(app.dmm_addr,
|
|
HP3478A_FUNC_OHMS_2WIRE HP3478A_DIGITS_5_5 HP3478A_TRIG_INTERNAL
|
|
HP3478A_CMD_MASK_BTN_DATA HP3478A_CMD_SRQ_CLEAR);
|
|
app.current_mode = MODE_FEAT_REL;
|
|
app.data.rel.offset = 0.0f;
|
|
dmm_display("REL MODE");
|
|
break;
|
|
|
|
case MENU_DBM:
|
|
// F1=DCV, A1=AutoRange (Critical for dBm), N5=5.5d
|
|
gpib_send(app.dmm_addr,
|
|
HP3478A_MEAS_AC_VOLTS HP3478A_TRIG_INTERNAL
|
|
HP3478A_CMD_MASK_BTN_DATA HP3478A_CMD_SRQ_CLEAR);
|
|
app.current_mode = MODE_FEAT_DBM;
|
|
dmm_display("DBM 50 OHM");
|
|
break;
|
|
|
|
case MENU_TEMP:
|
|
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");
|
|
} else {
|
|
// Resistor Setup (PT1000/Therm):
|
|
// F3=2W / F4=4W
|
|
// R4=30kOhm Range
|
|
if (app.temp_wire_mode == WIRE_2W) {
|
|
gpib_send(app.dmm_addr,
|
|
HP3478A_FUNC_OHMS_2WIRE HP3478A_RANGE_4 HP3478A_DIGITS_5_5
|
|
HP3478A_AUTOZERO_ON HP3478A_TRIG_INTERNAL
|
|
HP3478A_CMD_MASK_BTN_DATA HP3478A_CMD_SRQ_CLEAR);
|
|
} else {
|
|
gpib_send(app.dmm_addr,
|
|
HP3478A_FUNC_OHMS_4WIRE HP3478A_RANGE_4 HP3478A_DIGITS_5_5
|
|
HP3478A_AUTOZERO_ON HP3478A_TRIG_INTERNAL
|
|
HP3478A_CMD_MASK_BTN_DATA HP3478A_CMD_SRQ_CLEAR);
|
|
}
|
|
|
|
gpib_send(app.dmm_addr, tmp_buffer);
|
|
|
|
if (app.temp_sensor == SENS_PT1000)
|
|
dmm_display("TEMP PT1000");
|
|
else
|
|
dmm_display("TEMP THERM");
|
|
}
|
|
break;
|
|
|
|
case MENU_CONT:
|
|
// F3: 2W Ohm, R0: 30 Ohm Range, N3: 3.5 Digits (fastest ADC), M21
|
|
gpib_send(app.dmm_addr,
|
|
HP3478A_FUNC_OHMS_2WIRE HP3478A_RANGE_1 HP3478A_DIGITS_3_5
|
|
HP3478A_AUTOZERO_OFF HP3478A_TRIG_INTERNAL
|
|
HP3478A_CMD_MASK_BTN_DATA HP3478A_CMD_SRQ_CLEAR);
|
|
app.current_mode = MODE_FEAT_CONT;
|
|
app.data.cont.last_state = -1;
|
|
app.data.cont.disp_timer = millis();
|
|
dmm_display("CONT MODE");
|
|
break;
|
|
|
|
case MENU_DIODE:
|
|
// F3: 2-Wire Ohms
|
|
// R3: 3 kOhm Range (1mA Current Through Unknown)
|
|
// N4: 4.5 Digits
|
|
// Z0: Auto-Zero OFF
|
|
gpib_send(app.dmm_addr,
|
|
HP3478A_FUNC_OHMS_2WIRE HP3478A_RANGE_3 HP3478A_DIGITS_4_5
|
|
HP3478A_AUTOZERO_OFF HP3478A_TRIG_INTERNAL
|
|
HP3478A_CMD_MASK_BTN_DATA HP3478A_CMD_SRQ_CLEAR);
|
|
|
|
app.current_mode = MODE_FEAT_DIODE;
|
|
|
|
app.data.diode.connected = 0;
|
|
app.data.diode.chirp_start = millis() - DIODE_CHIRP_MS - 1;
|
|
|
|
dmm_display("DIODE TEST");
|
|
break;
|
|
|
|
case MENU_XOHM:
|
|
// H7: High Impedance / Extended Ohm Mode
|
|
// The DMM puts internal 10M in parallel with input
|
|
gpib_send(app.dmm_addr,
|
|
HP3478A_MEAS_OHMS_EXT HP3478A_DIGITS_5_5 HP3478A_TRIG_INTERNAL
|
|
HP3478A_CMD_MASK_BTN_DATA HP3478A_CMD_SRQ_CLEAR);
|
|
app.data.xohm.r1 = 0.0f;
|
|
app.data.xohm.calibrated = 0;
|
|
app.current_mode = MODE_FEAT_XOHM;
|
|
dmm_display("XOHM 10M REF");
|
|
break;
|
|
|
|
case MENU_STATS:
|
|
// F1: DC Volts
|
|
// A1: Auto Range
|
|
// N5: 5.5 Digit
|
|
// Z1: Auto Zero ON
|
|
// T1: Internal Trigger
|
|
gpib_send(app.dmm_addr,
|
|
HP3478A_FUNC_DC_VOLTS HP3478A_RANGE_AUTO HP3478A_DIGITS_5_5
|
|
HP3478A_AUTOZERO_ON HP3478A_TRIG_INTERNAL
|
|
HP3478A_CMD_MASK_BTN_DATA HP3478A_CMD_SRQ_CLEAR);
|
|
|
|
app.current_mode = MODE_FEAT_STATS;
|
|
|
|
// Initialize Stats
|
|
app.data.stats.min = STATS_INIT_MIN_VAL; // start impossible high
|
|
app.data.stats.max = STATS_INIT_MAX_VAL; // start impossible low
|
|
app.data.stats.sum = 0.0f;
|
|
app.data.stats.count = 0;
|
|
app.data.stats.view_mode = 0;
|
|
app.data.stats.disp_timer = millis();
|
|
|
|
dmm_display("STATS INIT");
|
|
break;
|
|
|
|
case MENU_EXIT:
|
|
default:
|
|
exit_to_passthrough();
|
|
break;
|
|
}
|
|
}
|
|
|
|
void enter_menu_mode(void) {
|
|
// force display refresh
|
|
app.last_disp_sent[0] = '\0';
|
|
|
|
save_dmm_state();
|
|
|
|
uint32_t now = millis();
|
|
app.current_mode = MODE_MENU;
|
|
app.menu_pos = MENU_REL;
|
|
app.data.menu.timer = now;
|
|
|
|
dmm_display("M: REL");
|
|
gpib_send(app.dmm_addr, HP3478A_CMD_MASK_BTN_ONLY HP3478A_CMD_SRQ_CLEAR);
|
|
Delay_Ms(200);
|
|
}
|
|
|
|
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, resp_buffer, sizeof(resp_buffer));
|
|
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;
|
|
}
|
|
|
|
float val = parse_float(resp_buffer);
|
|
// overload (HP 3478A sends +9.99990E+9 for OL)
|
|
int is_overload = (val > HP_OVERLOAD_VAL);
|
|
|
|
// RELATIVE MODE
|
|
if (app.current_mode == MODE_FEAT_REL) {
|
|
if (app.data.rel.offset == 0.0f) {
|
|
// 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");
|
|
} 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");
|
|
tone(3000, 50);
|
|
app.data.rel.stable_count = 0;
|
|
} else {
|
|
dmm_display("LOCKING...");
|
|
}
|
|
}
|
|
} else {
|
|
// offset is already set
|
|
if (is_overload) {
|
|
dmm_display("O.VLD");
|
|
} else {
|
|
float diff = val - app.data.rel.offset;
|
|
fmt_float(disp_buffer, sizeof(disp_buffer), diff, 4);
|
|
// Display: "1.2345 D"
|
|
format_aligned_display(disp_buffer, diff, 4, " D");
|
|
dmm_display(disp_buffer);
|
|
}
|
|
}
|
|
}
|
|
// dBm MODE
|
|
else if (app.current_mode == MODE_FEAT_DBM) {
|
|
if (is_overload) {
|
|
dmm_display("O.VLD");
|
|
} else {
|
|
// P(mW) = (V^2 / 50) * 1000 = V^2 * 20
|
|
float p_mw = (val * val * 20.0f);
|
|
|
|
if (p_mw < 1e-9f) {
|
|
// Align -INF to look consistent
|
|
// Display: "-INF DBM"
|
|
memset(disp_buffer, ' ', 12);
|
|
disp_buffer[12] = '\0';
|
|
memcpy(disp_buffer, "-INF", 4);
|
|
memcpy(&disp_buffer[8], " DBM", 4);
|
|
dmm_display(disp_buffer);
|
|
} else {
|
|
float dbm = 10.0f * log10f(p_mw);
|
|
// Display: "-14.20 DBM"
|
|
format_aligned_display(disp_buffer, dbm, 2, " DBM");
|
|
dmm_display(disp_buffer);
|
|
}
|
|
}
|
|
}
|
|
// TEMP MODE
|
|
else if (app.current_mode == MODE_FEAT_TEMP) {
|
|
if (is_overload && app.temp_sensor != SENS_TYPE_K) {
|
|
dmm_display("OPEN / ERR");
|
|
} else {
|
|
float temp_c = 0.0f;
|
|
const char* unit_str = " C";
|
|
|
|
// 1. TYPE K THERMOCOUPLE
|
|
if (app.temp_sensor == SENS_TYPE_K) {
|
|
// 30mV range safety check (Floating input > 50mV)
|
|
if (fabsf(val) > 0.05f) {
|
|
dmm_display("CHECK PROBE");
|
|
return;
|
|
} else {
|
|
float t_amb;
|
|
|
|
// Cold Junction Compensation (CJC)
|
|
if (app.env_sensor_present) {
|
|
// Convert int(2450) to 24.50
|
|
t_amb = (float)app.current_env.temp_c_x100 / 100.0f;
|
|
t_amb -= CJC_SELF_HEATING_OFFSET;
|
|
unit_str = " C (K)";
|
|
|
|
} else {
|
|
t_amb = CJC_FALLBACK_TEMP;
|
|
unit_str = " C (K*)"; // '*' means fallback CJC used
|
|
}
|
|
|
|
// Type K response is actually NOT linear, this should be a LUT or a
|
|
// polynomial calculation
|
|
// Temp = Ambient + (V_meas * Sensitivity)
|
|
temp_c = t_amb + (val * TYPE_K_SCALE);
|
|
}
|
|
}
|
|
// 2. PT1000 RTD (Callendar-Van Dusen)
|
|
else if (app.temp_sensor == SENS_PT1000) {
|
|
if (val < 10.0f) {
|
|
dmm_display("SHORT");
|
|
} else {
|
|
float c = RTD_R0 - val;
|
|
float b = RTD_R0 * RTD_A;
|
|
float a = RTD_R0 * RTD_B;
|
|
float disc = (b * b) - (4 * a * c);
|
|
|
|
if (disc >= 0)
|
|
temp_c = (-b + sqrtf(disc)) / (2 * a);
|
|
else {
|
|
dmm_display("RANGE ERR");
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
// 3. THERMISTOR (Steinhart-Hart)
|
|
else {
|
|
if (val < 10.0f) {
|
|
dmm_display("SHORT");
|
|
} else {
|
|
// protect against log(0)
|
|
float r = (val < 1.0f) ? 1.0f : val;
|
|
float lr = logf(r);
|
|
float lr3 = lr * lr * lr;
|
|
float inv_t = THERM_A + (THERM_B * lr) + (THERM_C * lr3);
|
|
|
|
temp_c = (1.0f / inv_t) - 273.15f;
|
|
}
|
|
}
|
|
// Display: "24.5 C" (or "24.5 C (K)")
|
|
format_aligned_display(disp_buffer, temp_c, 1, unit_str);
|
|
dmm_display(disp_buffer);
|
|
}
|
|
}
|
|
// CONT MODE
|
|
else if (app.current_mode == MODE_FEAT_CONT) {
|
|
int is_short = (!is_overload && val < CONT_THRESHOLD_OHMS);
|
|
|
|
// beep
|
|
if (is_short) {
|
|
buzzer_set(2000); // 2kHz tone
|
|
} else {
|
|
buzzer_set(0);
|
|
}
|
|
|
|
// display logic
|
|
uint32_t now = millis();
|
|
// force update if state changed or timeout
|
|
if ((is_short != app.data.cont.last_state) ||
|
|
(now - app.data.cont.disp_timer > 200)) {
|
|
if (is_overload) {
|
|
dmm_display("OPEN");
|
|
} else {
|
|
if (val < 1000.0f) {
|
|
// Normalize low ohms to look like standard ohms mode
|
|
// "10.5 OHM"
|
|
format_aligned_display(disp_buffer, val, 1, " OHM");
|
|
} else {
|
|
// shouldn't happen in 300 range :)
|
|
format_resistance(disp_buffer, sizeof(disp_buffer), val);
|
|
}
|
|
dmm_display(disp_buffer);
|
|
}
|
|
|
|
app.data.cont.last_state = is_short;
|
|
app.data.cont.disp_timer = now;
|
|
}
|
|
}
|
|
|
|
else if (app.current_mode == MODE_FEAT_DIODE) {
|
|
uint32_t now = millis();
|
|
float voltage = is_overload ? 9.9f : (val / 1000.0f);
|
|
uint8_t is_valid_signal =
|
|
(voltage > DIODE_TH_SHORT && voltage < DIODE_TH_OPEN);
|
|
|
|
if (voltage < DIODE_TH_OPEN) {
|
|
format_aligned_display(disp_buffer, voltage, 4, "VDC");
|
|
dmm_display(disp_buffer);
|
|
} else {
|
|
dmm_display("OPEN");
|
|
}
|
|
|
|
diode_state_t next_state = app.data.diode.connected;
|
|
uint8_t request_buzzer = 0;
|
|
|
|
switch (app.data.diode.connected) {
|
|
case DIODE_STATE_OPEN:
|
|
case DIODE_STATE_SHORT:
|
|
if (is_valid_signal) {
|
|
next_state = DIODE_STATE_CHECKING;
|
|
app.data.diode.chirp_start = now;
|
|
} else {
|
|
// strictly either open or short
|
|
next_state =
|
|
(voltage >= DIODE_TH_OPEN) ? DIODE_STATE_OPEN : DIODE_STATE_SHORT;
|
|
}
|
|
break;
|
|
|
|
case DIODE_STATE_CHECKING:
|
|
if (!is_valid_signal) {
|
|
next_state = DIODE_STATE_SHORT; // signal lost/glitch
|
|
} else if ((now - app.data.diode.chirp_start) >= DIODE_STABLE_MS) {
|
|
next_state = DIODE_STATE_PLAYING;
|
|
app.data.diode.chirp_start = now;
|
|
request_buzzer = 1;
|
|
}
|
|
break;
|
|
|
|
case DIODE_STATE_PLAYING:
|
|
request_buzzer = 1;
|
|
if ((now - app.data.diode.chirp_start) >= DIODE_CHIRP_MS) {
|
|
next_state = DIODE_STATE_DONE;
|
|
}
|
|
break;
|
|
|
|
case DIODE_STATE_DONE:
|
|
// latch until signal is clearly lost
|
|
if (!is_valid_signal) {
|
|
next_state = DIODE_STATE_SHORT;
|
|
}
|
|
break;
|
|
|
|
default:
|
|
next_state = DIODE_STATE_OPEN;
|
|
break;
|
|
}
|
|
|
|
app.data.diode.connected = next_state;
|
|
buzzer_set(request_buzzer ? 2500 : 0);
|
|
}
|
|
|
|
// XOHM MODE
|
|
else if (app.current_mode == MODE_FEAT_XOHM) {
|
|
// cal phase, measure the internal 10M resistor
|
|
if (app.data.xohm.calibrated == 0) {
|
|
// need the probes to be open. Internal R is ~10M
|
|
if (val > 8.0e6f && val < 12.0e6f) {
|
|
app.data.xohm.r1 = val; // Store R1
|
|
app.data.xohm.calibrated = 1;
|
|
tone(3000, 100);
|
|
dmm_display("READY");
|
|
Delay_Ms(500);
|
|
} else {
|
|
dmm_display("OPEN PROBES");
|
|
}
|
|
}
|
|
// Rx = (R1 * R2) / (R1 - R2)
|
|
// R1 = xohm_ref (Internal)
|
|
// R2 = val (Measured Parallel)
|
|
else {
|
|
if (is_overload || val >= (app.data.xohm.r1 - 1000.0f)) {
|
|
dmm_display("OPEN");
|
|
} else {
|
|
float r1 = app.data.xohm.r1;
|
|
float r2 = val;
|
|
float rx = (r1 * r2) / (r1 - r2);
|
|
|
|
format_resistance(disp_buffer, sizeof(disp_buffer), rx);
|
|
dmm_display(disp_buffer);
|
|
}
|
|
}
|
|
} else if (app.current_mode == MODE_FEAT_STATS) {
|
|
if (is_overload) {
|
|
dmm_display("O.VLD");
|
|
} else {
|
|
// accumulate math
|
|
if (val < app.data.stats.min) app.data.stats.min = val;
|
|
if (val > app.data.stats.max) app.data.stats.max = val;
|
|
app.data.stats.sum += (double)val;
|
|
app.data.stats.count++;
|
|
|
|
// rotate display
|
|
uint32_t now = millis();
|
|
if (now - app.data.stats.disp_timer > STATS_CYCLE_TIME_MS) {
|
|
app.data.stats.view_mode++;
|
|
if (app.data.stats.view_mode > 3) app.data.stats.view_mode = 0;
|
|
app.data.stats.disp_timer = now;
|
|
}
|
|
|
|
// render
|
|
char prefix[5]; // 3-char prefix + space
|
|
float val_to_show = 0.0f;
|
|
|
|
switch (app.data.stats.view_mode) {
|
|
case 0: // Live Value
|
|
strcpy(prefix, "");
|
|
val_to_show = val;
|
|
break;
|
|
case 1: // Average
|
|
strcpy(prefix, "AVG ");
|
|
val_to_show = app.data.stats.sum / app.data.stats.count;
|
|
break;
|
|
case 2: // Minimum
|
|
strcpy(prefix, "MIN ");
|
|
val_to_show = app.data.stats.min;
|
|
break;
|
|
case 3: // Maximum
|
|
strcpy(prefix, "MAX ");
|
|
val_to_show = app.data.stats.max;
|
|
break;
|
|
default:
|
|
strcpy(prefix, "ERR ");
|
|
val_to_show = val;
|
|
app.data.stats.view_mode = 0;
|
|
break;
|
|
}
|
|
|
|
int offset = snprintf(disp_buffer, sizeof(disp_buffer), "%s", prefix);
|
|
if (offset >= 0 && offset < (int)sizeof(disp_buffer)) {
|
|
fmt_float(disp_buffer + offset, sizeof(disp_buffer) - offset,
|
|
val_to_show, 4);
|
|
}
|
|
|
|
dmm_display(disp_buffer);
|
|
}
|
|
}
|
|
}
|
|
|
|
// gens the base string (e.g., "M: TEMP", "S: TYPE K") into disp_buffer
|
|
void prepare_menu_base_string(void) {
|
|
const char* prefix = "";
|
|
const char* name = "???";
|
|
|
|
if (app.data.menu.layer == SUBMENU_NONE) {
|
|
prefix = "M: ";
|
|
if (app.menu_pos < MENU_MAX_ITEMS) name = MENU_NAMES[app.menu_pos];
|
|
} else if (app.data.menu.layer == SUBMENU_TEMP_SENS) {
|
|
prefix = "S: ";
|
|
if (app.menu_pos < SENS_MAX_ITEMS) name = SENSOR_NAMES[app.menu_pos];
|
|
} else if (app.data.menu.layer == SUBMENU_TEMP_WIRE) {
|
|
prefix = "T: ";
|
|
if (app.menu_pos < WIRE_MAX_ITEMS) name = WIRE_NAMES[app.menu_pos];
|
|
}
|
|
|
|
snprintf(disp_buffer, sizeof(disp_buffer), "%s%s", prefix, name);
|
|
}
|
|
|
|
void handle_menu_navigation(void) {
|
|
uint32_t now = millis();
|
|
uint32_t elapsed = now - app.data.menu.timer;
|
|
|
|
// nav: check SRQ (next item)
|
|
if (gpib_check_srq()) {
|
|
uint8_t stb = 0;
|
|
gpib_serial_poll(app.dmm_addr, &stb);
|
|
|
|
// 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);
|
|
|
|
// re-arm SRQ
|
|
gpib_send(app.dmm_addr, HP3478A_CMD_MASK_BTN_ONLY HP3478A_CMD_SRQ_CLEAR);
|
|
Delay_Ms(MENU_DEBOUNCE_MS);
|
|
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(disp_buffer, ".");
|
|
}
|
|
dmm_display(disp_buffer);
|
|
|
|
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(disp_buffer);
|
|
return;
|
|
}
|
|
// enter standard modes
|
|
enter_feature_mode(app.menu_pos);
|
|
return;
|
|
}
|
|
|
|
// L1: sensor select
|
|
else if (app.data.menu.layer == SUBMENU_TEMP_SENS) {
|
|
app.temp_sensor = (temp_sensor_t)app.menu_pos;
|
|
|
|
if (app.temp_sensor == SENS_TYPE_K) {
|
|
// Type K is voltage based so skip wire select
|
|
enter_feature_mode(MENU_TEMP);
|
|
} else {
|
|
// wire select (for resistive)
|
|
app.data.menu.layer = SUBMENU_TEMP_WIRE;
|
|
app.menu_pos = 0; // default to 2W
|
|
app.data.menu.timer = now;
|
|
|
|
prepare_menu_base_string();
|
|
dmm_display(disp_buffer);
|
|
}
|
|
return;
|
|
}
|
|
|
|
// L2: wire select
|
|
else if (app.data.menu.layer == SUBMENU_TEMP_WIRE) {
|
|
app.temp_wire_mode = (wire_mode_t)app.menu_pos;
|
|
enter_feature_mode(MENU_TEMP);
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
|
|
void app_loop(void) {
|
|
uint32_t now = millis();
|
|
|
|
// 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(4000, 50);
|
|
app.poll_interval = POLL_INTERVAL_MS; // restore fast polling
|
|
return;
|
|
}
|
|
|
|
// valid and online, check buttons
|
|
if (stb & HP3478A_MASK_KEYBOARD_SRQ) {
|
|
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(tmp_buffer, sizeof(tmp_buffer),
|
|
"Stat:\r\n"
|
|
" Target Addr: %d\r\n"
|
|
" Internal DMM: %d\r\n"
|
|
" Auto Read: %s\r\n"
|
|
" Current Mode: %d\r\n"
|
|
" FW: " FW_VERSION "\r\n",
|
|
app.target_addr, app.dmm_addr, app.auto_read ? "ON" : "OFF",
|
|
app.current_mode);
|
|
usb_send_text(tmp_buffer);
|
|
}
|
|
|
|
static void process_command(void) {
|
|
if (!get_start_command(cmd_buffer, sizeof(cmd_buffer))) {
|
|
return;
|
|
}
|
|
|
|
int is_query = 0;
|
|
|
|
char* p_cmd = skip_spaces(cmd_buffer);
|
|
int is_cpp_cmd = (strncmp(p_cmd, "++", 2) == 0);
|
|
|
|
if (app.current_mode != MODE_PASSTHROUGH) {
|
|
buzzer_set(0);
|
|
app.current_mode = MODE_PASSTHROUGH;
|
|
dmm_display_normal();
|
|
gpib_remote_enable(1); // ensure REN matches state
|
|
}
|
|
|
|
if (is_cpp_cmd) {
|
|
// move past "++"
|
|
p_cmd += 2;
|
|
|
|
// '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: ADDR
|
|
if (starts_with_nocase(p_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(tmp_buffer, sizeof(tmp_buffer), "%d\r\n", app.target_addr);
|
|
usb_send_text(tmp_buffer);
|
|
}
|
|
}
|
|
// CMD: WRITE
|
|
else if (starts_with_nocase(p_cmd, "write")) {
|
|
// sends the rest of the string (p_args) to GPIB
|
|
if (*p_args) {
|
|
gpib_send(app.target_addr, p_args);
|
|
if (app.auto_read) goto do_read_operation; // jmp to read block
|
|
}
|
|
}
|
|
// CMD: READ
|
|
else if (starts_with_nocase(p_cmd, "read")) {
|
|
goto do_read_operation;
|
|
}
|
|
// CMD: AUTO
|
|
else if (starts_with_nocase(p_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");
|
|
}
|
|
}
|
|
// CMD: TRG
|
|
else if (starts_with_nocase(p_cmd, "trg")) {
|
|
gpib_trigger(app.target_addr);
|
|
usb_send_text("OK\r\n");
|
|
}
|
|
// CMD: STATUS / STAT
|
|
else if (starts_with_nocase(p_cmd, "stat")) {
|
|
cmd_status();
|
|
}
|
|
// CMD: CLR
|
|
else if (starts_with_nocase(p_cmd, "clr")) {
|
|
gpib_device_clear(app.target_addr);
|
|
usb_send_text("OK\r\n");
|
|
}
|
|
// CMD: REN (Remote Enable)
|
|
else if (starts_with_nocase(p_cmd, "ren")) {
|
|
if (*p_args) {
|
|
int state = atoi(p_args);
|
|
gpib_remote_enable(state);
|
|
usb_send_text("OK\r\n");
|
|
} else {
|
|
usb_send_text("usage: ++ren 1|0\r\n");
|
|
}
|
|
}
|
|
// CMD: IFC (Interface Clear)
|
|
else if (starts_with_nocase(p_cmd, "ifc")) {
|
|
gpib_interface_clear();
|
|
usb_send_text("OK\r\n");
|
|
}
|
|
|
|
// CMD: SPOLL (Serial Poll)
|
|
else if (starts_with_nocase(p_cmd, "spoll")) {
|
|
uint8_t poll_addr = app.target_addr;
|
|
|
|
if (*p_args) {
|
|
int arg = atoi(p_args);
|
|
if (arg >= 0 && arg <= 30) poll_addr = arg;
|
|
}
|
|
|
|
uint8_t stb;
|
|
if (gpib_serial_poll(poll_addr, &stb) == 0) {
|
|
// print status byte as dec
|
|
snprintf(tmp_buffer, sizeof(tmp_buffer), "%d\r\n", stb);
|
|
usb_send_text(tmp_buffer);
|
|
} else {
|
|
usb_send_text("ERR: Bus\r\n");
|
|
}
|
|
}
|
|
// CMD: LLO (Local Lockout)
|
|
else if (starts_with_nocase(p_cmd, "llo")) {
|
|
gpib_local_lockout();
|
|
usb_send_text("OK\r\n");
|
|
}
|
|
// CMD: DCL (Universal Clear)
|
|
else if (starts_with_nocase(p_cmd, "dcl")) {
|
|
gpib_universal_clear();
|
|
usb_send_text("OK\r\n");
|
|
}
|
|
// CMD: GTL (Go To Local)
|
|
else if (starts_with_nocase(p_cmd, "gtl")) {
|
|
gpib_go_to_local(app.target_addr);
|
|
usb_send_text("OK\r\n");
|
|
}
|
|
// HP3478A Internal
|
|
else if (starts_with_nocase(p_cmd, "cont"))
|
|
enter_feature_mode(MENU_CONT);
|
|
else if (starts_with_nocase(p_cmd, "temp"))
|
|
enter_feature_mode(MENU_TEMP);
|
|
else if (starts_with_nocase(p_cmd, "rel"))
|
|
enter_feature_mode(MENU_REL);
|
|
else if (starts_with_nocase(p_cmd, "xohm"))
|
|
enter_feature_mode(MENU_XOHM);
|
|
else if (starts_with_nocase(p_cmd, "dbm"))
|
|
enter_feature_mode(MENU_DBM);
|
|
else if (starts_with_nocase(p_cmd, "diode"))
|
|
enter_feature_mode(MENU_DIODE);
|
|
else if (starts_with_nocase(p_cmd, "math"))
|
|
enter_feature_mode(MENU_STATS);
|
|
else if (starts_with_nocase(p_cmd, "norm"))
|
|
exit_to_passthrough();
|
|
else if (starts_with_nocase(p_cmd, "disp")) {
|
|
int i = 0;
|
|
|
|
while (p_args[i] != 0 && i < 12) {
|
|
char c = p_args[i];
|
|
if (c >= 'a' && c <= 'z') {
|
|
c -= 32;
|
|
}
|
|
|
|
disp_buffer[i] = c;
|
|
i++;
|
|
}
|
|
disp_buffer[i] = 0;
|
|
dmm_display(disp_buffer);
|
|
usb_send_text("OK\r\n");
|
|
}
|
|
// SYSTEM
|
|
else if (starts_with_nocase(p_cmd, "loc")) {
|
|
gpib_remote_enable(0);
|
|
usb_send_text("OK\r\n");
|
|
} else if (starts_with_nocase(p_cmd, "rst")) {
|
|
usb_send_text("Rebooting...\r\n");
|
|
Delay_Ms(100);
|
|
NVIC_SystemReset();
|
|
} else if (starts_with_nocase(p_cmd, "ver")) {
|
|
usb_send_text("HP3478A Internal GPIB " FW_VERSION "\r\n");
|
|
} else if (starts_with_nocase(p_cmd, "help") ||
|
|
starts_with_nocase(p_cmd, "?")) {
|
|
cmd_help();
|
|
} else if (starts_with_nocase(p_cmd, "env")) {
|
|
if (!app.env_sensor_present) {
|
|
usb_send_text("ERR: No Sensor\r\n");
|
|
return;
|
|
}
|
|
float t = (float)app.current_env.temp_c_x100 / 100.0f;
|
|
float h = (float)app.current_env.hum_p_x100 / 100.0f;
|
|
char* arg = skip_spaces(p_args);
|
|
char out_buf[32];
|
|
|
|
// temp only
|
|
if (starts_with_nocase(arg, "temp")) {
|
|
fmt_float(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")) {
|
|
fmt_float(out_buf, sizeof(out_buf), h, 2);
|
|
usb_send_text(out_buf);
|
|
usb_send_text("\r\n");
|
|
}
|
|
// CSV format (temp,hum)
|
|
else {
|
|
fmt_float(out_buf, 16, t, 2);
|
|
strcat(out_buf, ",");
|
|
// fmt humidity into a temp buffer and append
|
|
char h_buf[16];
|
|
fmt_float(h_buf, sizeof(h_buf), h, 2);
|
|
strcat(out_buf, h_buf);
|
|
strcat(out_buf, "\r\n");
|
|
|
|
usb_send_text(out_buf);
|
|
}
|
|
} else {
|
|
usb_send_text("ERR: Unknown Command\r\n");
|
|
}
|
|
return; // end of ++
|
|
}
|
|
|
|
// PASSTHROUGH MODE
|
|
// check for query '?' to trigger implicit read
|
|
is_query = (strchr(p_cmd, '?') != NULL);
|
|
|
|
if (gpib_send(app.target_addr, p_cmd) < 0) {
|
|
usb_send_text("ERR: Send Fail\r\n");
|
|
return;
|
|
}
|
|
|
|
// check if we should read back
|
|
if (is_query || app.auto_read) {
|
|
goto do_read_operation;
|
|
}
|
|
return;
|
|
|
|
do_read_operation: {
|
|
int len = gpib_receive(app.target_addr, resp_buffer, sizeof(resp_buffer));
|
|
if (len > 0) {
|
|
usb_send_text(resp_buffer);
|
|
} 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 = 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;
|
|
|
|
while (1) {
|
|
handle_usb_state();
|
|
app_loop();
|
|
handle_env_sensor();
|
|
|
|
if (app.usb_online) {
|
|
process_command();
|
|
}
|
|
}
|
|
} |