#include #include #include #include #include #include "aht20.h" #include "ch32fun.h" #include "fsusb.h" #include "gpib_defs.h" #include "i2c_bitbang.h" #include "systick.h" #define FW_VERSION "1.0.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 #define MENU_HOVER_COMMIT_MS 2400 // time between Serial Polls in Passthrough mode #define POLL_INTERVAL_MS 100 // dead time after SRQ mask to the DMM // polling it too soon after recovery causes a timeout #define DMM_RECOVERY_DELAY_MS 1000 // PT1000 Constants #define RTD_A 3.9083e-3 #define RTD_B -5.775e-7 #define RTD_R0 1000.0 // HP3478A Specific Commands #define HP_CMD_RESET_DISPLAY "D1" #define HP_CMD_TEXT_DISPLAY "D3" #define HP_CMD_MASK_BTN_ONLY "M20" // SRQ on Front Panel Button #define HP_CMD_MASK_BTN_DATA "M21" // SRQ on Data Ready + Button #define CONT_THRESHOLD_OHMS 10.0f #define HP_OVERLOAD_VAL 9.0e9f // Threshold for +9.99990E+9 #define REL_STABLE_SAMPLES 3 // samples to wait before locking NULL 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_CONT, // Continuity Mode active MODE_FEAT_XOHM // Extended Ohms active } work_mode_t; static const char* MENU_NAMES[] = {"REL", "CONT", "TEMP", "XOHM", "EXIT"}; typedef enum { MENU_REL = 0, MENU_CONT, MENU_TEMP, MENU_XOHM, MENU_EXIT, MENU_MAX_ITEMS } menu_item_t; typedef enum { CONT_SHORT = 0, CONT_OPEN } cont_state_t; typedef struct { // USB conn state int usb_online; // debounced connection state int usb_raw_prev; // previous raw state uint32_t usb_ts; // ts for debounce logic // env sensor int env_sensor_present; aht20_data current_env; uint32_t env_last_read; // GPIB uint8_t target_addr; // active target uint8_t dmm_addr; // specifically the HP3478A int auto_read; // local firmware work_mode_t current_mode; menu_item_t menu_pos; uint32_t menu_timer; uint32_t next_poll_time; int dmm_online; // 0 = offline, 1 = online // feature stuff float rel_offset; uint8_t rel_stable_count; // counter for relative mode stabilization // XOHM variables float xohm_r1; // Internal 10M reference value uint8_t xohm_calibrated; // Flag: 0 = Measuring R1, 1 = Measuring Rx // continuity int cont_last_state; // To dedup display updates uint32_t cont_disp_timer; // To refresh display occasionally } app_state_t; static app_state_t app = {.dmm_addr = DEFAULT_DMM_ADDR, .target_addr = DEFAULT_DMM_ADDR}; // Buffers static char cmd_buffer[128]; static char resp_buffer[256]; static char tmp_buffer[128]; static char disp_buffer[13]; // 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; } } void format_resistance(char* buffer, size_t buf_len, float val) { memset(buffer, 0, buf_len); if (val >= 1e9f) { fmt_float(buffer, buf_len, val / 1e9f, 3); strcat(buffer, " G"); } else if (val >= 1e6f) { fmt_float(buffer, buf_len, val / 1e6f, 4); strcat(buffer, " M"); } else if (val >= 1e3f) { fmt_float(buffer, buf_len, val / 1e3f, 3); strcat(buffer, " K"); } else { fmt_float(buffer, buf_len, val, 2); strcat(buffer, " OHM"); } } 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) - The "Big Reset Button" 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; } // 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) { snprintf(resp_buffer, 64, "%s%s\n", HP_CMD_TEXT_DISPLAY, text); gpib_send(app.dmm_addr, resp_buffer); } static inline void dmm_display_normal(void) { gpib_send(app.dmm_addr, HP_CMD_RESET_DISPLAY); } void exit_to_passthrough(void) { dmm_display_normal(); // "D1" buzzer_set(0); // mute gpib_send(app.dmm_addr, "H1 T1 N5" HP_CMD_MASK_BTN_ONLY); Delay_Ms(50); gpib_go_to_local(app.dmm_addr); app.current_mode = MODE_PASSTHROUGH; } void enter_feature_mode(menu_item_t item) { gpib_remote_enable(1); // assert REN Delay_Ms(20); switch (item) { case MENU_REL: // T1: Internal Trigger, M21: Data Ready + SRQ Button gpib_send(app.dmm_addr, "F3 N5 T1" HP_CMD_MASK_BTN_DATA); app.current_mode = MODE_FEAT_REL; app.rel_offset = 0.0f; dmm_display("REL MODE"); break; case MENU_TEMP: // F3: 2W Ohm, M21: Data + Button Mask gpib_send(app.dmm_addr, "F3 R3 N4 Z1 T1 " HP_CMD_MASK_BTN_DATA); app.current_mode = MODE_FEAT_TEMP; dmm_display("TEMP PT1000"); break; case MENU_CONT: // F3: 2W Ohm, R0: 30 Ohm Range, N3: 3.5 Digits (fastest ADC), M21 gpib_send(app.dmm_addr, "F3 R1 N3 Z0 T1 " HP_CMD_MASK_BTN_DATA); app.current_mode = MODE_FEAT_CONT; app.cont_last_state = -1; app.cont_disp_timer = millis(); dmm_display("CONT MODE"); break; case MENU_XOHM: // H7: High Impedance / Extended Ohm Mode // The DMM puts internal 10M in parallel with input gpib_send(app.dmm_addr, "H7 N5 T1 " HP_CMD_MASK_BTN_DATA); app.xohm_r1 = 0.0f; app.xohm_calibrated = 0; app.current_mode = MODE_FEAT_XOHM; dmm_display("XOHM 10M REF"); break; case MENU_EXIT: default: exit_to_passthrough(); break; } } void enter_menu_mode(void) { uint32_t now = millis(); app.current_mode = MODE_MENU; app.menu_pos = MENU_REL; app.menu_timer = now; dmm_display("M: REL"); gpib_send(app.dmm_addr, HP_CMD_MASK_BTN_ONLY); Delay_Ms(200); } void handle_feature_logic(void) { uint8_t stb = 0; gpib_serial_poll(app.dmm_addr, &stb); // exit button (SRQ) if (stb & 0x10) { exit_to_passthrough(); return; } // data ready (Bit 0) if (!(stb & 0x01)) return; 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.rel_offset == 0.0f) { // waiting to capture the NULL value if (is_overload) { app.rel_stable_count = 0; // reset counter if probes are open dmm_display("O.VLD"); } else { // valid reading app.rel_stable_count++; if (app.rel_stable_count >= REL_STABLE_SAMPLES) { app.rel_offset = val; dmm_display("NULL SET"); tone(3000, 50); app.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.rel_offset; fmt_float(disp_buffer, sizeof(disp_buffer), diff, 4); if (strlen(disp_buffer) < 11) strcat(disp_buffer, " D"); dmm_display(disp_buffer); } } } // TEMP MODE else if (app.current_mode == MODE_FEAT_TEMP) { if (val > 4000.0f || val < 10.0f) { dmm_display("OPEN / ERR"); } 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) { float temp = (-b + sqrtf(disc)) / (2 * a); fmt_float(disp_buffer, sizeof(disp_buffer), temp, 1); strcat(disp_buffer, " C"); 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.cont_last_state) || (now - app.cont_disp_timer > 200)) { if (is_overload) { dmm_display("OPEN"); } else { if (val < 1000.0f) { fmt_float(disp_buffer, sizeof(disp_buffer), val, 1); strcat(disp_buffer, " OHM"); } else { // shouldn't happen in 300 range :) format_resistance(disp_buffer, sizeof(disp_buffer), val); } dmm_display(disp_buffer); } app.cont_last_state = is_short; app.cont_disp_timer = now; } } // XOHM MODE else if (app.current_mode == MODE_FEAT_XOHM) { // cal phase, measure the internal 10M resistor if (app.xohm_calibrated == 0) { // need the probes to be open. Internal R is ~10M if (val > 8.0e6f && val < 12.0e6f) { app.xohm_r1 = val; // Store R1 app.xohm_calibrated = 1; 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.xohm_r1 - 1000.0f)) { dmm_display("OPEN"); } else { float r1 = app.xohm_r1; float r2 = val; float rx = (r1 * r2) / (r1 - r2); format_resistance(disp_buffer, sizeof(disp_buffer), rx); dmm_display(disp_buffer); } } } } void handle_menu_navigation(void) { uint32_t now = millis(); uint32_t elapsed = now - app.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 & 0x10) { app.menu_timer = now; // reset the "hover" timer app.menu_pos++; if (app.menu_pos >= MENU_MAX_ITEMS) app.menu_pos = 0; char* s = "M: ???"; switch (app.menu_pos) { case MENU_REL: s = "M: REL"; break; case MENU_CONT: s = "M: CONT"; break; case MENU_TEMP: s = "M: TEMP"; break; case MENU_XOHM: s = "M: XOHM"; break; case MENU_EXIT: s = "M: EXIT"; break; default: break; } dmm_display(s); // re-arm SRQ gpib_send(app.dmm_addr, HP_CMD_MASK_BTN_ONLY); Delay_Ms(200); return; } } // visual cd static int last_dot_count = -1; int dot_count = elapsed / 800; if (dot_count > 3) dot_count = 3; // only update display if the dots changed if (dot_count != last_dot_count) { const char* dots = ""; if (dot_count == 1) dots = "."; else if (dot_count == 2) dots = ".."; else if (dot_count == 3) dots = "..."; snprintf(disp_buffer, sizeof(disp_buffer), "M: %s%s", MENU_NAMES[app.menu_pos], dots); dmm_display(disp_buffer); last_dot_count = dot_count; } // reset dot tracker if cycled if (elapsed < 100) last_dot_count = 0; // commit selection if (elapsed > 2400) { if (app.menu_pos == MENU_EXIT) { // printf("[MENU] Hover EXIT\n"); exit_to_passthrough(); } else { // printf("[MENU] Hover Select Item %d\n", app.menu_pos); enter_feature_mode(app.menu_pos); } return; } } void app_loop(void) { uint32_t now = millis(); // Passthrough if (app.current_mode == MODE_PASSTHROUGH) { if (now <= app.next_poll_time) { return; } uint8_t stb = 0; int poll_result = gpib_serial_poll(app.dmm_addr, &stb); // DMM offline - detect recovery if (poll_result != 0) { if (app.dmm_online) { // printf("DMM Lost\n"); gpib_interface_clear(); app.dmm_online = 0; } app.next_poll_time = now + POLL_INTERVAL_MS; return; } // DMM online - detect recovery transition if (!app.dmm_online) { // printf("DMM Recovered. Re-initializing...\n"); gpib_send(app.dmm_addr, HP_CMD_MASK_BTN_ONLY "K"); gpib_go_to_local(app.dmm_addr); app.dmm_online = 1; tone(4000, 50); // do it in the next clean poll app.next_poll_time = now + DMM_RECOVERY_DELAY_MS; return; } // check for button press if (stb & 0x10) { enter_menu_mode(); } app.next_poll_time = now + POLL_INTERVAL_MS; 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 & 0x10) { exit_to_passthrough(); return; } // handle measurement data ready if (stb & 0x01) { 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 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 Write data 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, ++norm\r\n" " ++disp 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 '? ' 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 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, "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.target_addr = 18; app.dmm_addr = app.target_addr; app.xohm_r1 = 0.0f; app.usb_online = 0; app.usb_raw_prev = USB_HW_IS_ACTIVE(); app.usb_ts = millis(); while (1) { uint8_t status_byte; if (gpib_serial_poll(app.dmm_addr, &status_byte) == 0) { // printf("Device Found (Stb: 0x%02X)\n", status_byte); gpib_send(app.dmm_addr, HP_CMD_MASK_BTN_ONLY "K"); gpib_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(); } } }