Compare commits

..

9 Commits

Author SHA1 Message Date
3bf60570d1 docs: add images 2025-12-05 01:00:27 +06:00
fb6c3cbb7b feat: add ++dmm_loop cmd
++dmm_loop <0|1> to disable the HP3478A DMM loop, if for some reason the
DMM app loop is interfering with the GPIB bus when used as a USB-GPIB
controller
2025-12-04 18:26:02 +06:00
329a648f9c fix: don't manually re-arm SRQ in menu nav 2025-12-04 16:28:50 +06:00
9fed882f50 feat: autohold, persistent config (ext flash), a little cleanup? maybe 2025-12-04 15:50:27 +06:00
054bc38683 fix: don't pull REN up on startup before DMM is online 2025-12-02 20:39:09 +06:00
ad833867d1 fix: trig hold when entering menu to avoid flickering 2025-12-02 17:59:55 +06:00
fd177c0b4a pic 2025-12-02 03:59:40 +06:00
4c6bbc58fd go faster, add some more prologix cmds, readme 2025-12-02 03:47:31 +06:00
7e6195f967 tmp 2025-12-02 02:03:04 +06:00
14 changed files with 1909 additions and 868 deletions

View File

@@ -112,7 +112,9 @@
"math.h": "c", "math.h": "c",
"cctype": "c", "cctype": "c",
"stdlib.h": "c", "stdlib.h": "c",
"float.h": "c" "float.h": "c",
"stdbool.h": "c",
"initializer_list": "c"
}, },
"cmake.sourceDirectory": "/home/mira/src/embedded/ch32v208_sens/lwip" "cmake.sourceDirectory": "/home/mira/src/embedded/ch32v208_sens/lwip"
} }

137
README.md
View File

@@ -0,0 +1,137 @@
# HP3478A Internal USB-GPIB Extension
An internal extension board for the HP3478A Multimeter. USB-CDC-GPIB bridge (Prologix-style) and an internal feature controller.
Uses WCH CH32V203 (RISC-V) MCU.
It enumerates as a standard serial device (e.g., /dev/ttyACM0 or COMx).
PCB [here](https://git.ayau.me/mira/mhardware/src/branch/master/hp3478a-ch32-ext)
Photos in [img/](./img/) and the [demo video](./img/feats.mp4)
## Menu System
Extended features (Continuity, Temp, etc.) can be accessed using the SRQ button of HP3478A without a PC
1. **Enter Menu:** Press the Front Panel **SRQ**.
2. **Nav:** Press the button again to cycle through modes (`M: REL` -> `M: TEMP` -> `M: DBM`...).
3. **Selection (Hover):** Stop pressing the button.
- The display will animate dots (`...`) after 500ms.
- After ~2.5 seconds of inactivity, the selected mode is activated.
4. **Sub-Menus:** Some modes (like Temp) have sub-menus for sensor type and 2W/4W mode. Same cycle logic to select these.
5. **Exit:** Press the SRQ button to exit back to initial mode (one we entered menu from).
## Features
These can be triggered via the Menu or the serial commands below.
Relative and Statistics depend on the mode you enter the menu from, i.e. if you enter relative from DCV it saves your state and you get relative voltage, from 2W relative 2W etc.
```
++help
HP3478A Internal USB-GPIB 1.2.0
[GPIB Setup]
++addr <0-30> Target Address
++auto <0-2> 0:Off, 1:Read-After-Write, 2:Query-Only
++read_tmo_ms <t> Timeout in ms
++eoi <0|1> Assert hardware EOI on write end
++eos <0-3> Write Term: 0:CRLF, 1:CR, 2:LF, 3:None
++eor <0-7> Read Stop: 0:CRLF ... 7:EOI-Only
++eot_enable <B> Append extra char to read output
++eot_char <dec> The char to append
++dmm_loop <0|1> Toggle HP3478A loop, if it conflicts w/ GPIB bus
[System Configuration]
++config List all configurable parameters
++get <name> Get parameter value
++set <name> <v> Set parameter value
++savecfg Save config to flash
++ver Firmware Version
++rst System Reset
[GPIB Bus Operations]
++read Read from target
++write <data> Write to target
++trg Device Trigger (GET)
++clr Device Clear (SDC)
++dcl Universal Device Clear (DCL)
++ifc Interface Clear (Bus Reset)
++spoll [addr] Serial Poll
++srq Query SRQ Line (0=High/Idle, 1=Low/Active)
++loc Local (Drop REN)
++llo Local Lockout
[Internal HP3478A Features]
++cont, ++hold, ++rel, ++xohm
++dbm, ++diode, ++math, ++norm
++temp <l1> <l2> Temperature sensor mode
++env [temp|hum] Internal AHT20 Sensor
++disp <text> Write text to LCD
```
There's also a configuration which can be saved using `++savecfg` if you want to make it persist (it's saved to v203's "undocumented" flash).
Also see [inc/config.h](./inc/config.h).
```
++config
my_addr: 0
dmm_addr: 18
target_addr: 18
eot_char: 0
eot_enable: 0
eoi_assert: 1
eos_mode: 0
eor_mode: 0
auto_read: 0
gpib_timeout_ms: 1200
poll_interval_ms: 100
env_sensor_read_interval_ms: 1000
dmm_recovery_delay_ms: 1000
usb_debounce_connect_ms: 50
usb_debounce_disconnect_ms: 200
usb_timeout_target_ms: 5
menu_dot_interval_ms: 500
menu_commit_delay_ms: 2400
menu_sublayer_delay_ms: 500
menu_debounce_ms: 100
menu_lockout_ms: 1000
stats_cycle_time_ms: 3000
cont_disp_update_ms: 150
diode_stable_ms: 20
buzzer_chirp_hz: 3000
buzzer_chirp_ms: 75
buzzer_cont_hz: 2500
rtd_a: 0.003908
rtd_b: -0.000001
rtd_r0: 1000.000000
cjc_fallback_temp: 22.000000
cjc_self_heating_offset: 4.000000
type_k_scale: 24390.240000
dbm_ref_z: 50.000000
diode_th_short: 0.050000
diode_th_open: 2.500000
autohold_threshold: 1.000000
autohold_change_req: 2.000000
autohold_min_val: 0.050000
cont_threshold_ohms: 10.000000
autohold_stable_count: 3
rel_stable_count: 3
```
i.e.
```
++set dbm_ref_z 60
++savecfg
```
Now your dBm feature will use 600 ohm ref instead of 50.
## Known Issues
1. I did NOT use the brain when routing the USBLC6. Either have to DNP the IC and jumper the data lines, or rely on the software hack to check USB `SUSPEND` to guess if the host is connected. I do the latter, works but meh. VBUS detection would be nicer.
2. The buzzer is **not on a hardware TIM pin**. It's on a generic GPIO... This means it can't be hardware/timer PWMed. Instead, the firmware is firing a TIM IRQ thousands of times a second just to manually toggle the pin in the ISR..
3. There is a microSD card footprint on the board. The fw does absolutely nothing with it. Data logging?

View File

@@ -5,5 +5,10 @@
// #define FUNCONF_SYSTEM_CORE_CLOCK 120000000 // #define FUNCONF_SYSTEM_CORE_CLOCK 120000000
// #define FUNCONF_PLL_MULTIPLIER 15 // #define FUNCONF_PLL_MULTIPLIER 15
#define FUNCONF_SYSTICK_USE_HCLK 1 #define FUNCONF_SYSTICK_USE_HCLK 1
#define FUNCONF_USE_DEBUGPRINTF 0
#define FUNCONF_USE_UARTPRINTF 0
#define FUNCONF_USE_USBPRINTF 0
#define FUNCONF_NULL_PRINTF 1
// #define GPIB_DEBUG 1
#endif #endif

BIN
img/ch32v203.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 545 KiB

BIN
img/connection_gpib.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 833 KiB

BIN
img/connection_pcb.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 790 KiB

BIN
img/feats.mp4 Normal file

Binary file not shown.

BIN
img/usb_b.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 667 KiB

155
inc/config.h Normal file
View File

@@ -0,0 +1,155 @@
#ifndef CONFIG_H
#define CONFIG_H
#include <stdbool.h>
#include <stdint.h>
#define ERASE_PAGE_SIZE 64
#define CONFIG_MAGIC 0x1234ABCD
#define CONFIG_VERSION 1
#define MY_ADDR 0
#define DEFAULT_DMM_ADDR 18 // the HP3478A addr
// Timing Config
#define USB_DEBOUNCE_CONNECT_MS 50
#define USB_DEBOUNCE_DISCONNECT_MS 200
#define ENV_SENSOR_READ_INTERVAL_MS 1000
#define DEFAULT_GPIB_TIMEOUT_MS 1200
// This kind of ass but.. yeah, I don't want to access systick there
// assume if the PC hasn't read after ~5ms it's stalled
#define USB_TIMEOUT_TARGET_MS 5
#define CYCLES_PER_LOOP 50
#define USB_TIMEOUT_LIMIT \
((FUNCONF_SYSTEM_CORE_CLOCK / 1000 * USB_TIMEOUT_TARGET_MS) / CYCLES_PER_LOOP)
// 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 100 // Button press dead-time
#define MENU_LOCKOUT_MS 200 // How long to ignore SRQ for after exiting menu
// Polling
#define POLL_INTERVAL_MS 100 // 10Hz polling when in Passthrough
#define DMM_RECOVERY_DELAY_MS 1000 // Backoff if DMM vanishes
// Diode sound
#define DIODE_TH_SHORT 0.050 // Volts (below this = SHORT)
#define DIODE_TH_OPEN 2.500 // Volts (above this = OPEN/OL)
#define DIODE_STABLE_MS 20 // wait X ms for voltage to settle
#define DIODE_CHIRP_MS 50
// PT1000 (DIN 43760 / IEC 751 Standard)
#define RTD_A 3.9083e-3
#define RTD_B -5.775e-7
#define RTD_R0 1000.0
// Thermocouple
#define CJC_FALLBACK_TEMP 22.0 // used if !app.env_sensor_present
// this is just cursed but.. yeah, the PCB is near a transformer
// ideally, the temp should be measured right at the binding posts..
#define CJC_SELF_HEATING_OFFSET 4.0
#define TYPE_K_SCALE 24390.24 // 1 / 41uV
// dBm
#define DBM_REF_Z 50.0
// Stats
#define STATS_CYCLE_TIME_MS 3000 // time per screen (Live -> Avg -> Min...)
#define STATS_INIT_MIN_VAL 1.0e9
#define STATS_INIT_MAX_VAL -1.0e9
// Autohold
#define AUTOHOLD_THRESHOLD 1.0 // deviation % allowed for stability check
#define AUTOHOLD_CHANGE_REQ 2.0 // % new value must differ by to trigger upd
// XXX: ideally, we should have different sample requirements for diff ranges
// we don't need as many counts at N5 range as at N3
#define AUTOHOLD_STABLE_COUNT 3 // num samples required
#define AUTOHOLD_MIN_VAL 0.05 // noise floor
#define CONT_DISP_UPDATE_MS 150 // display throttling
#define CONT_THRESHOLD_OHMS 10.0 // continuity beep threshold
#define REL_STABLE_COUNT 3 // filter depth for Relative NULL
#define BUZZER_CHIRP_HZ 3000
#define BUZZER_CHIRP_MS 75
#define BUZZER_CONT_HZ 2500
#define BUZZER_ONLINE_TUNE 0 // todo: add to conf
// #define LOCAL_COMMIT 1
typedef struct {
uint32_t magic;
uint32_t version;
// addressing
uint8_t my_addr; // controller
uint8_t dmm_addr; // hp3478a
uint8_t target_addr; // target
uint8_t eot_char;
uint8_t eot_enable;
uint8_t eoi_assert;
uint8_t eos_mode;
uint8_t eor_mode;
uint8_t auto_read;
uint8_t padding; // align
// timings (ms)
uint32_t gpib_timeout_ms;
uint32_t poll_interval_ms;
uint32_t env_sensor_read_interval_ms;
uint32_t dmm_recovery_delay_ms;
uint32_t usb_debounce_connect_ms;
uint32_t usb_debounce_disconnect_ms;
uint32_t usb_timeout_target_ms;
// timings
uint32_t menu_dot_interval_ms;
uint32_t menu_commit_delay_ms;
uint32_t menu_sublayer_delay_ms;
uint32_t menu_debounce_ms;
uint32_t menu_lockout_ms;
uint32_t stats_cycle_time_ms;
uint32_t cont_disp_update_ms;
uint32_t diode_stable_ms;
// buzzer
uint32_t buzzer_chirp_hz;
uint32_t buzzer_chirp_ms;
uint32_t buzzer_cont_hz;
// math & cal
double rtd_a;
double rtd_b;
double rtd_r0;
double cjc_fallback_temp;
double cjc_self_heating_offset;
double type_k_scale;
double dbm_ref_z;
double diode_th_short;
double diode_th_open;
// thresholds
double autohold_threshold;
double autohold_change_req;
double autohold_min_val;
double cont_threshold_ohms;
// logic counts
uint32_t autohold_stable_count;
uint32_t rel_stable_count;
} fw_config_t;
typedef enum { CFG_TYPE_UINT8, CFG_TYPE_UINT32, CFG_TYPE_DOUBLE } cfg_type_t;
typedef struct {
const char* name;
size_t offset;
cfg_type_t type;
} cfg_field_t;
#endif // CONFIG_H

View File

@@ -3,8 +3,6 @@
#include "ch32fun.h" #include "ch32fun.h"
// #define GPIB_DEBUG 1
// Control Lines (Active LOW) // Control Lines (Active LOW)
#define PIN_EOI PB3 #define PIN_EOI PB3
#define PIN_REN PB11 #define PIN_REN PB11
@@ -147,6 +145,7 @@
#define HP3478A_CMD_MASK_BTN_DATA "M21" #define HP3478A_CMD_MASK_BTN_DATA "M21"
// "M00" -> Clear Mask (Disable SRQ) // "M00" -> Clear Mask (Disable SRQ)
#define HP3478A_CMD_MASK_CLEAR "M00" #define HP3478A_CMD_MASK_CLEAR "M00"
#define HP3478A_CMD_MASK_BTN_SYNERR "M24"
#define GPIB_ASSERT(pin) funDigitalWrite(pin, 0) #define GPIB_ASSERT(pin) funDigitalWrite(pin, 0)
#define GPIB_RELEASE(pin) funDigitalWrite(pin, 1) #define GPIB_RELEASE(pin) funDigitalWrite(pin, 1)
@@ -162,6 +161,32 @@
#define PIN_DIO7 PB13 #define PIN_DIO7 PB13
#define PIN_DIO8 PB12 #define PIN_DIO8 PB12
// Physical Pin mappings on PORT B
#define PIN_POS_D1 9 // PB9
#define PIN_POS_D2 8 // PB8
#define PIN_POS_D3 5 // PB5
#define PIN_POS_D4 4 // PB4
#define PIN_POS_D5 15 // PB15
#define PIN_POS_D6 14 // PB14
#define PIN_POS_D7 13 // PB13
#define PIN_POS_D8 12 // PB12
#define SHIFT_GRP_9 9 // D1 (PB9->0) & D6 (PB14->5)
#define SHIFT_GRP_7 7 // D2 (PB8->1) & D7 (PB13->6)
#define SHIFT_D3 3 // D3 (PB5->2)
#define SHIFT_D4 1 // D4 (PB4->3)
#define SHIFT_D5 11 // D5 (PB15->4)
#define SHIFT_D8 5 // D8 (PB12->7)
// pins that share the same shift amount
// Group A: shift right 9 (PB9->Bit0, PB14->Bit5)
#define MASK_GRP_9 ((1 << 0) | (1 << 5))
// Group B: shift right 7 (PB8->Bit1, PB13->Bit6)
#define MASK_GRP_7 ((1 << 1) | (1 << 6))
#define CALC_PIN_BSHR(val, bit_idx, pin_num) \
((val & (1 << bit_idx)) ? (1U << (pin_num + 16)) : (1U << pin_num))
#define MASK_DIO1 (1U << 9) #define MASK_DIO1 (1U << 9)
#define MASK_DIO2 (1U << 8) #define MASK_DIO2 (1U << 8)
#define MASK_DIO3 (1U << 5) #define MASK_DIO3 (1U << 5)
@@ -171,7 +196,4 @@
#define MASK_DIO7 (1U << 13) #define MASK_DIO7 (1U << 13)
#define MASK_DIO8 (1U << 12) #define MASK_DIO8 (1U << 12)
static const int DIO_PINS[] = {PIN_DIO1, PIN_DIO2, PIN_DIO3, PIN_DIO4,
PIN_DIO5, PIN_DIO6, PIN_DIO7, PIN_DIO8};
#endif #endif

View File

@@ -9,11 +9,7 @@
#define SYSTICK_ONE_MILLISECOND ((uint32_t)FUNCONF_SYSTEM_CORE_CLOCK / 1000) #define SYSTICK_ONE_MILLISECOND ((uint32_t)FUNCONF_SYSTEM_CORE_CLOCK / 1000)
#define SYSTICK_ONE_MICROSECOND ((uint32_t)FUNCONF_SYSTEM_CORE_CLOCK / 1000000) #define SYSTICK_ONE_MICROSECOND ((uint32_t)FUNCONF_SYSTEM_CORE_CLOCK / 1000000)
extern volatile uint32_t systick_millis;
#define millis() (systick_millis) #define millis() (systick_millis)
#define micros() (SysTick->CNT / SYSTICK_ONE_MICROSECOND) #define micros() (SysTick->CNT / SYSTICK_ONE_MICROSECOND)
void systick_init(void);
#endif // SYSTICK_H #endif // SYSTICK_H

View File

@@ -22,10 +22,10 @@
#define FUSB_USB_VID 0x1209 #define FUSB_USB_VID 0x1209
#define FUSB_USB_PID 0x3478 #define FUSB_USB_PID 0x3478
#define FUSB_USB_REV 0x0110 #define FUSB_USB_REV 0x0120
#define FUSB_STR_MANUFACTURER u"Open Source GPIB" #define FUSB_STR_MANUFACTURER u"Open Source GPIB"
#define FUSB_STR_PRODUCT u"HP3478A Internal Adapter" #define FUSB_STR_PRODUCT u"HP3478A Internal Adapter"
#define FUSB_STR_SERIAL u"3478A-USB-110" #define FUSB_STR_SERIAL u"3478A-USB-120"
//Taken from http://www.usbmadesimple.co.uk/ums_ms_desc_dev.htm //Taken from http://www.usbmadesimple.co.uk/ums_ms_desc_dev.htm
static const uint8_t device_descriptor[] = { static const uint8_t device_descriptor[] = {

2415
main.c

File diff suppressed because it is too large Load Diff

View File

@@ -1,21 +0,0 @@
#include "systick.h"
volatile uint32_t systick_millis;
void systick_init(void) {
SysTick->CTLR = 0x0000;
SysTick->CMP = SysTick->CNT + SYSTICK_ONE_MILLISECOND;
systick_millis = 0;
SysTick->CTLR = SYSTICK_CTLR_STE | // Enable Counter
SYSTICK_CTLR_STIE | // Enable Interrupts
SYSTICK_CTLR_STCLK; // Set Clock Source to HCLK/1
NVIC_EnableIRQ(SysTick_IRQn);
}
void SysTick_Handler(void) __attribute__((interrupt));
void SysTick_Handler(void) {
SysTick->CMP = SysTick->CNT + SYSTICK_ONE_MILLISECOND;
SysTick->SR = 0;
systick_millis++;
}