From dc5194f1c8ab467e938865b1ea30a76e22a8c802 Mon Sep 17 00:00:00 2001 From: kuwoyuki Date: Sun, 30 Nov 2025 04:47:14 +0600 Subject: [PATCH] first commit --- .gitignore | 61 ++ .gitmodules | 3 + .vscode/c_cpp_properties.json | 16 + .vscode/settings.json | 116 +++ Makefile | 32 + README.md | 0 aht20.c | 151 +++ ch32fun | 1 + funconfig.h | 9 + inc/aht20.h | 40 + inc/gpib_defs.h | 91 ++ inc/i2c_bitbang.h | 157 +++ inc/systick.h | 19 + inc/usb_config.h | 180 ++++ main.c | 1702 +++++++++++++++++++++++++++++++++ systick.c | 21 + 16 files changed, 2599 insertions(+) create mode 100644 .gitignore create mode 100644 .gitmodules create mode 100644 .vscode/c_cpp_properties.json create mode 100644 .vscode/settings.json create mode 100644 Makefile create mode 100644 README.md create mode 100644 aht20.c create mode 160000 ch32fun create mode 100644 funconfig.h create mode 100644 inc/aht20.h create mode 100644 inc/gpib_defs.h create mode 100644 inc/i2c_bitbang.h create mode 100644 inc/systick.h create mode 100644 inc/usb_config.h create mode 100644 main.c create mode 100644 systick.c diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..f494111 --- /dev/null +++ b/.gitignore @@ -0,0 +1,61 @@ +# Prerequisites +*.d + +# Object files +*.o +*.ko +*.obj +*.elf + +# Linker output +*.ilk +*.map +*.exp + +# Precompiled Headers +*.gch +*.pch + +# Libraries +*.lib +*.a +*.la +*.lo + +# Shared objects (inc. Windows DLLs) +*.dll +*.so +*.so.* +*.dylib + +# Executables +*.exe +*.out +*.app +*.i*86 +*.x86_64 +*.hex + +# Debug files +*.dSYM/ +*.su +*.idb +*.pdb + +# Kernel Module Compile Results +*.mod* +*.cmd +.tmp_versions/ +modules.order +Module.symvers +Mkfile.old +dkms.conf + +# debug information files +*.dwo + +# z +main.lst +main_ext.bin +main.bin +bin/ diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 0000000..bea043a --- /dev/null +++ b/.gitmodules @@ -0,0 +1,3 @@ +[submodule "ch32fun"] + path = ch32fun + url = https://github.com/cnlohr/ch32fun.git diff --git a/.vscode/c_cpp_properties.json b/.vscode/c_cpp_properties.json new file mode 100644 index 0000000..07c08df --- /dev/null +++ b/.vscode/c_cpp_properties.json @@ -0,0 +1,16 @@ +{ + "configurations": [ + { + "name": "Linux", + "includePath": [ + "${workspaceFolder}/**" + ], + "defines": [], + "compilerPath": "/usr/bin/clang", + "cStandard": "c17", + "cppStandard": "c++17", + "intelliSenseMode": "linux-clang-x64" + } + ], + "version": 4 +} \ No newline at end of file diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..4b82f36 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,116 @@ +{ + "files.associations": { + "*.rmd": "markdown", + "sys.h": "c", + "cstdio": "c", + "__verbose_abort": "c", + "*.inc": "c", + "ch32fun.h": "c", + "stdio.h": "c", + "systick.h": "c", + "def.h": "c", + "ch32v20xhw.h": "c", + "future": "c", + "compare": "c", + "cstdint": "c", + "snmp.h": "c", + "etharp.h": "c", + "dhcp.h": "c", + "netif.h": "c", + "ch32v307gigabit.h": "c", + "stats.h": "c", + "bitset": "c", + "forward_list": "c", + "functional": "c", + "format": "c", + "fstream": "c", + "istream": "c", + "ranges": "c", + "streambuf": "c", + "variant": "c", + "__hash_table": "c", + "__tree": "c", + "deque": "c", + "iterator": "c", + "list": "c", + "locale": "c", + "regex": "c", + "vector": "c", + "memory_resource": "c", + "__config": "c", + "string": "c", + "atomic": "c", + "__bit_reference": "c", + "err.h": "c", + "httpd.h": "c", + "random": "c", + "limits": "c", + "tuple": "c", + "init.h": "c", + "hw_i2c.h": "c", + "chrono": "c", + "stop_token": "c", + "__locale": "c", + "stdint.h": "c", + "ch32v208_eth.h": "c", + "bit": "c", + "any": "c", + "array": "c", + "hash_map": "c", + "strstream": "c", + "charconv": "c", + "cmath": "c", + "codecvt": "c", + "complex": "c", + "concepts": "c", + "condition_variable": "c", + "coroutine": "c", + "cstddef": "c", + "unordered_map": "c", + "unordered_set": "c", + "exception": "c", + "memory": "c", + "numeric": "c", + "optional": "c", + "ratio": "c", + "string_view": "c", + "system_error": "c", + "type_traits": "c", + "algorithm": "c", + "iomanip": "c", + "mutex": "c", + "ostream": "c", + "semaphore": "c", + "shared_mutex": "c", + "span": "c", + "stacktrace": "c", + "text_encoding": "c", + "thread": "c", + "typeindex": "c", + "typeinfo": "c", + "utility": "c", + "valarray": "c", + "__assert": "c", + "__split_buffer": "c", + "ios": "c", + "map": "c", + "new": "c", + "queue": "c", + "set": "c", + "stack": "c", + "stdexcept": "c", + "__node_handle": "c", + "execution": "c", + "numbers": "c", + "print": "c", + "ha_mqtt.h": "c", + "ethernetif.h": "c", + "tcp_bench.h": "c", + "cstring": "c", + "sfhip.h": "c", + "string.h": "c", + "math.h": "c", + "cctype": "c" + }, + "cmake.sourceDirectory": "/home/mira/src/embedded/ch32v208_sens/lwip" +} \ No newline at end of file diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..772d624 --- /dev/null +++ b/Makefile @@ -0,0 +1,32 @@ +TARGET ?= main +TARGET_MCU ?= CH32V203 +TARGET_MCU_PACKAGE ?= CH32V203C8T6 + +CH32V003FUN ?= ./ch32fun/ch32fun +MINICHLINK ?= ./ch32fun/minichlink + +PREFIX ?= riscv64-elf +NEWLIB ?= /usr/riscv64-elf/include/ + +INCLUDE_DIRS += \ + -I./inc + +PROJECT_C_FILES := $(filter-out ./main.c, $(wildcard ./*.c)) +LIB_C_FILES := + +ADDITIONAL_C_FILES := \ + $(PROJECT_C_FILES) \ + $(LIB_C_FILES) + +include $(CH32V003FUN)/ch32fun.mk + +CFLAGS := $(filter-out -nostdlib,$(CFLAGS)) +LDFLAGS := $(filter-out -nostdlib,$(LDFLAGS)) +NANO_FLAGS := -nostartfiles --specs=nano.specs --specs=nosys.specs -lc -lm + +CFLAGS += -Wall -Wextra $(INCLUDE_DIRS) $(NANO_FLAGS) + +all: flash +flash: cv_flash +clean: cv_clean +.PHONY: all flash clean diff --git a/README.md b/README.md new file mode 100644 index 0000000..e69de29 diff --git a/aht20.c b/aht20.c new file mode 100644 index 0000000..512b945 --- /dev/null +++ b/aht20.c @@ -0,0 +1,151 @@ +#include "aht20.h" + +#include + +#include "ch32fun.h" +#include "i2c_bitbang.h" + +// Polynomial: x^8 + x^5 + x^4 + 1 (0x31) +static uint8_t aht20_calc_crc(const uint8_t* data, size_t len) { + uint8_t crc = 0xFF; + + for (size_t i = 0; i < len; i++) { + crc ^= data[i]; + for (uint8_t j = 0; j < 8; j++) { + if (crc & 0x80) { + crc = (crc << 1) ^ 0x31; + } else { + crc = (crc << 1); + } + } + } + return crc; +} + +static int aht20_get_status(uint8_t* status) { + i2c_start(); + if (i2c_write_byte((AHT20_ADDR << 1) | 0) != 0) { + i2c_stop(); + return AHT20_ERR_I2C; + } + i2c_write_byte(AHT20_CMD_STATUS); + i2c_stop(); + + i2c_start(); + if (i2c_write_byte((AHT20_ADDR << 1) | 1) != 0) { + i2c_stop(); + return AHT20_ERR_I2C; + } + *status = i2c_read_byte(1); // NACK to end read + i2c_stop(); + + return AHT20_OK; +} + +uint8_t aht20_init(void) { + uint8_t status; + + // "Before reading the temperature and humidity value, get a byte of status + // word by sending 0x71" + if (aht20_get_status(&status) != AHT20_OK) { + return AHT20_ERR_INIT; + } + + // "If the status word and 0x18 are not equal to 0x18..." (Bit 3 CAL, Bit 4 + // Retain) + if ((status & STATUS_CAL_BIT) == 0) { + // cal + i2c_start(); + if (i2c_write_byte((AHT20_ADDR << 1) | 0) != 0) { + i2c_stop(); + return AHT20_ERR_INIT; + } + i2c_write_byte(AHT20_CMD_CALIBRATE); + i2c_write_byte(0x08); + i2c_write_byte(0x00); + i2c_stop(); + Delay_Ms(10); // "Wait 10ms to send the 0xAC command" + } + + return AHT20_OK; +} + +static inline void aht20_parse_data(uint8_t* buf, aht20_data* result) { + // extract 20-bit raw values + // Humidity: Byte 1, Byte 2, Byte 3 (high 4 bits) + uint32_t raw_hum = ((uint32_t)buf[1] << 12) | ((uint32_t)buf[2] << 4) | + ((uint32_t)buf[3] >> 4); + + // Temperature: Byte 3 (low 4 bits), Byte 4, Byte 5 + uint32_t raw_temp = ((uint32_t)(buf[3] & 0x0F) << 16) | + ((uint32_t)buf[4] << 8) | (uint32_t)buf[5]; + + // Humidity + // Target: %RH * 100 + // Factor: 10000 / 2^20 == 625 / 2^16 + // Shift: >> 16 + // Rounding: + (Divisor / 2) = + 32768 + uint32_t hum = (raw_hum * 625) + 32768; + result->hum_p_x100 = hum >> 16; + + // Temperature + // Target: DegC * 100 + // Range Factor: 20000 / 2^20 == 625 / 2^15 + // Shift: >> 15 + // Rounding: + (Divisor / 2) = + 16384 + uint32_t temp = (raw_temp * 625) + 16384; + result->temp_c_x100 = (int32_t)(temp >> 15) - 5000; +} + +uint8_t aht20_read(aht20_data* out_data) { + i2c_start(); + if (i2c_write_byte((AHT20_ADDR << 1) | 0) != 0) { + i2c_stop(); + return AHT20_ERR_I2C; + } + i2c_write_byte(AHT20_CMD_TRIGGER); + i2c_write_byte(0x33); + i2c_write_byte(0x00); + i2c_stop(); + + // wait for measurement (datasheet says 80ms) + Delay_Ms(80); + + uint8_t buf[7]; // status + 5 data + CRC + uint8_t retry = 3; + + while (retry--) { + i2c_start(); + if (i2c_write_byte((AHT20_ADDR << 1) | 1) != 0) { + i2c_stop(); + return AHT20_ERR_I2C; + } + + buf[0] = i2c_read_byte(0); // ACK + + // check busy bit + if ((buf[0] & STATUS_BUSY_BIT) == 0) { + for (int i = 1; i < 6; i++) { + buf[i] = i2c_read_byte(0); // ACK + } + buf[6] = i2c_read_byte(1); // NACK (last byte) + i2c_stop(); + break; + } + + // still busy + i2c_read_byte(1); // NACK to end this + i2c_stop(); + + Delay_Ms(10); + if (retry == 0) return AHT20_ERR_BUSY; + } + + if (aht20_calc_crc(buf, 6) != buf[6]) { + return AHT20_ERR_CRC; + } + + aht20_parse_data(buf, out_data); + + return AHT20_OK; +} diff --git a/ch32fun b/ch32fun new file mode 160000 index 0000000..4679e07 --- /dev/null +++ b/ch32fun @@ -0,0 +1 @@ +Subproject commit 4679e076f0155817c409500d526422015d21c7d6 diff --git a/funconfig.h b/funconfig.h new file mode 100644 index 0000000..85b879e --- /dev/null +++ b/funconfig.h @@ -0,0 +1,9 @@ +#ifndef _FUNCONFIG_H +#define _FUNCONFIG_H + +// #define FUNCONF_USE_HSE 1 +// #define FUNCONF_SYSTEM_CORE_CLOCK 120000000 +// #define FUNCONF_PLL_MULTIPLIER 15 +#define FUNCONF_SYSTICK_USE_HCLK 1 + +#endif diff --git a/inc/aht20.h b/inc/aht20.h new file mode 100644 index 0000000..d4d1599 --- /dev/null +++ b/inc/aht20.h @@ -0,0 +1,40 @@ +#ifndef AHT20_H +#define AHT20_H + +#include + +#define AHT20_ADDR 0x38 // AHT default i2c address +#define AHT20_CMD_CALIBRATE 0xBE // Calibration command +#define AHT20_CMD_TRIGGER 0xAC // Trigger reading command +#define AHT20_CMD_SOFTRESET 0xBA // Soft reset command +#define AHT20_CMD_STATUS 0x71 +#define AHT20_STATUS_BUSY 0x80 // Status bit for busy +#define AHT20_STATUS_CALIBRATED 0x08 // Status bit for calibrated + +#define AHT20_OK 0x00 +#define AHT20_ERR_INIT 0x01 // Initialization or Calibration failed +#define AHT20_ERR_BUSY 0x02 // Sensor measurement timeout +#define AHT20_ERR_CRC 0x03 // Data corruption detected +#define AHT20_ERR_I2C 0x04 // Generic I2C Bus error (NACK) + +#define STATUS_BUSY_BIT (1 << 7) +#define STATUS_CAL_BIT (1 << 3) + +typedef struct { + uint32_t hum_p_x100; // %RH * 100 + int32_t temp_c_x100; // DegC * 100 +} aht20_data; + +/** + * Init sensor + * Checks status 0x71 and performs calibration (0xBE) if required + */ +uint8_t aht20_init(void); + +/** + * Trigger measurement, wait >80ms, poll busy bit, and read data + * Checks CRC8 + */ +uint8_t aht20_read(aht20_data* out_data); + +#endif // AHT20_H diff --git a/inc/gpib_defs.h b/inc/gpib_defs.h new file mode 100644 index 0000000..2d1402f --- /dev/null +++ b/inc/gpib_defs.h @@ -0,0 +1,91 @@ +#ifndef GPIB_DEFS_H +#define GPIB_DEFS_H + +#include "ch32fun.h" + +// #define GPIB_DEBUG 1 + +// Control Lines (Active LOW) +#define PIN_EOI PB3 +#define PIN_REN PB11 +#define PIN_ATN PA8 +#define PIN_SRQ PA9 +#define PIN_IFC PA10 +#define PIN_NDAC PA11 // Handshake: Not Data Accepted +#define PIN_NRFD PA12 // Handshake: Not Ready For Data +#define PIN_DAV PA15 // Handshake: Data Valid + +// GPIB Commands + +// enum cmd_byte { +// GTL = 0x1, /* go to local */ +// SDC = 0x4, /* selected device clear */ +// PP_CONFIG = 0x5, +// GET = 0x8, /* group execute trigger */ +// TCT = 0x9, /* take control */ +// LLO = 0x11, /* local lockout */ +// DCL = 0x14, /* device clear */ +// PPU = 0x15, /* parallel poll unconfigure */ +// SPE = 0x18, /* serial poll enable */ +// SPD = 0x19, /* serial poll disable */ +// CFE = 0x1f, /* configure enable */ +// LAD = 0x20, /* value to be 'ored' in to obtain listen address */ +// UNL = 0x3F, /* unlisten */ +// TAD = 0x40, /* value to be 'ored' in to obtain talk address */ +// UNT = 0x5F, /* untalk */ +// SAD = 0x60, /* my secondary address (base) */ +// PPE = 0x60, /* parallel poll enable (base) */ +// PPD = 0x70 /* parallel poll disable */ +// }; + +#define GPIB_CMD_GTL 0x01 /* go to local */ +#define GPIB_CMD_SDC 0x04 /* selected device clear */ +#define GPIB_CMD_PP_CONFIG 0x05 /* parallel poll configure */ +#define GPIB_CMD_GET 0x08 /* group execute trigger */ +#define GPIB_CMD_TCT 0x09 /* take control */ +#define GPIB_CMD_LLO 0x11 /* local lockout */ +#define GPIB_CMD_DCL 0x14 /* device clear */ +#define GPIB_CMD_PPU 0x15 /* parallel poll unconfigure */ +#define GPIB_CMD_SPE 0x18 /* serial poll enable */ +#define GPIB_CMD_SPD 0x19 /* serial poll disable */ +#define GPIB_CMD_CFE 0x1F /* configure enable */ +#define GPIB_CMD_LAD 0x20 /* listen address (OR with addr) */ +#define GPIB_CMD_UNL 0x3F /* unlisten */ +#define GPIB_CMD_TAD 0x40 /* talk address (OR with addr) */ +#define GPIB_CMD_UNT 0x5F /* untalk */ +#define GPIB_CMD_SAD 0x60 /* secondary address base */ +#define GPIB_CMD_PPE 0x60 /* parallel poll enable */ +#define GPIB_CMD_PPD 0x70 /* parallel poll disable */ + +// Address Groups +#define GPIB_LAG_BASE 0x20 // Listen Address Group base +#define GPIB_TAG_BASE 0x40 // Talk Address Group base +#define GPIB_SCG_BASE 0x60 // Secondary Command Group base + +#define GPIB_ASSERT(pin) funDigitalWrite(pin, 0) +#define GPIB_RELEASE(pin) funDigitalWrite(pin, 1) +#define GPIB_READ(pin) funDigitalRead(pin) + +// Data Lines (DIO1-DIO8) +#define PIN_DIO1 PB9 +#define PIN_DIO2 PB8 +#define PIN_DIO3 PB5 +#define PIN_DIO4 PB4 +#define PIN_DIO5 PB15 +#define PIN_DIO6 PB14 +#define PIN_DIO7 PB13 +#define PIN_DIO8 PB12 + +#define MASK_DIO1 (1U << 9) +#define MASK_DIO2 (1U << 8) +#define MASK_DIO3 (1U << 5) +#define MASK_DIO4 (1U << 4) +#define MASK_DIO5 (1U << 15) +#define MASK_DIO6 (1U << 14) +#define MASK_DIO7 (1U << 13) +#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 diff --git a/inc/i2c_bitbang.h b/inc/i2c_bitbang.h new file mode 100644 index 0000000..f93ae0e --- /dev/null +++ b/inc/i2c_bitbang.h @@ -0,0 +1,157 @@ +#ifndef _I2C_BITBANG_H +#define _I2C_BITBANG_H + +#include +#include + +#include "ch32fun.h" + +#ifndef I2C_SDA_PIN +#define I2C_SDA_PIN PA1 +#endif + +#ifndef I2C_SCL_PIN +#define I2C_SCL_PIN PA0 +#endif + +// decrease to 1 or 0 for Fast Mode (400kHz) +#ifndef I2C_DELAY_US +#define I2C_DELAY_US 3 +#endif + +#define I2C_ACK 0 +#define I2C_NACK 1 + +#define SCL_HIGH() funDigitalWrite(I2C_SCL_PIN, 1) +#define SCL_LOW() funDigitalWrite(I2C_SCL_PIN, 0) +#define SDA_HIGH() funDigitalWrite(I2C_SDA_PIN, 1) +#define SDA_LOW() funDigitalWrite(I2C_SDA_PIN, 0) +#define SDA_READ() funDigitalRead(I2C_SDA_PIN) + +static inline void i2c_delay(void) { Delay_Us(I2C_DELAY_US); } + +static inline void i2c_init(void) { + funGpioInitAll(); + + funPinMode(I2C_SDA_PIN, GPIO_CFGLR_OUT_10Mhz_OD); + funPinMode(I2C_SCL_PIN, GPIO_CFGLR_OUT_10Mhz_OD); + + // idle bus state + SDA_HIGH(); + SCL_HIGH(); +} + +static inline void i2c_start(void) { + SDA_HIGH(); + SCL_HIGH(); + i2c_delay(); + + SDA_LOW(); + i2c_delay(); + SCL_LOW(); +} + +static inline void i2c_stop(void) { + SDA_LOW(); + i2c_delay(); + SCL_HIGH(); + i2c_delay(); + + SDA_HIGH(); + i2c_delay(); +} + +// returns 0 (I2C_ACK) if acknowledged, 1 (I2C_NACK) if not +static inline uint8_t i2c_write_byte(uint8_t byte) { + for (uint8_t i = 0; i < 8; i++) { + if (byte & 0x80) { + SDA_HIGH(); + } else { + SDA_LOW(); + } + + byte <<= 1; + i2c_delay(); + SCL_HIGH(); + i2c_delay(); + SCL_LOW(); + i2c_delay(); + } + + // ACK/NACK processing + SDA_HIGH(); // release SDA line for slave + i2c_delay(); + SCL_HIGH(); + i2c_delay(); + + uint8_t ack = SDA_READ(); + + SCL_LOW(); + i2c_delay(); + + return ack; +} + +// I2C_ACK (0) to request more data, I2C_NACK (1) to end tx +static inline uint8_t i2c_read_byte(uint8_t ack_control) { + uint8_t byte = 0; + + SDA_HIGH(); + + for (uint8_t i = 0; i < 8; i++) { + byte <<= 1; + i2c_delay(); + SCL_HIGH(); // slave writes data here + i2c_delay(); + + if (SDA_READ()) { + byte |= 1; + } + + SCL_LOW(); + } + + // send ACK/NACK to slave + if (ack_control == I2C_ACK) { + SDA_LOW(); + } else { + SDA_HIGH(); + } + + i2c_delay(); + SCL_HIGH(); + i2c_delay(); + SCL_LOW(); + + SDA_HIGH(); // release bus + i2c_delay(); + + return byte; +} + +static inline void i2c_scan(void) { + printf("--- I2C Scan Start ---\n"); + uint8_t found = 0; + + for (uint8_t addr = 0x08; addr < 0x78; addr++) { + i2c_start(); + + // try to write to the address + uint8_t ack = i2c_write_byte((addr << 1) | 0); + + i2c_stop(); + + if (ack == I2C_ACK) { + printf("Device found at 0x%02X\n", addr); + found++; + Delay_Ms(1); + } + } + + if (found == 0) { + printf("No I2C devices found.\n"); + } + printf("--- I2C Scan End ---\n"); +} + +#endif // _I2C_BITBANG_H \ No newline at end of file diff --git a/inc/systick.h b/inc/systick.h new file mode 100644 index 0000000..eb1d9b8 --- /dev/null +++ b/inc/systick.h @@ -0,0 +1,19 @@ +#ifndef SYSTICK_H +#define SYSTICK_H + +#include + +#include "ch32fun.h" +#include "ch32v20xhw.h" + +#define SYSTICK_ONE_MILLISECOND ((uint32_t)FUNCONF_SYSTEM_CORE_CLOCK / 1000) +#define SYSTICK_ONE_MICROSECOND ((uint32_t)FUNCONF_SYSTEM_CORE_CLOCK / 1000000) + +extern volatile uint32_t systick_millis; + +#define millis() (systick_millis) +#define micros() (SysTick->CNT / SYSTICK_ONE_MICROSECOND) + +void systick_init(void); + +#endif // SYSTICK_H diff --git a/inc/usb_config.h b/inc/usb_config.h new file mode 100644 index 0000000..350697a --- /dev/null +++ b/inc/usb_config.h @@ -0,0 +1,180 @@ +#ifndef _USB_CONFIG_H +#define _USB_CONFIG_H + +#include "funconfig.h" +#include "ch32fun.h" + +#define FUSB_CONFIG_EPS 4 // Include EP0 in this count +#define FUSB_EP1_MODE 1 // TX (IN) +#define FUSB_EP2_MODE -1 // RX (OUT) +#define FUSB_EP3_MODE 1 // TX (IN) +#define FUSB_SUPPORTS_SLEEP 0 +#define FUSB_HID_INTERFACES 0 +#define FUSB_CURSED_TURBO_DMA 0 // Hacky, but seems fine, shaves 2.5us off filling 64-byte buffers. +#define FUSB_HID_USER_REPORTS 0 +#define FUSB_IO_PROFILE 0 +#define FUSB_USE_HPE FUNCONF_ENABLE_HPE +#define FUSB_USER_HANDLERS 1 +#define FUSB_USE_DMA7_COPY 0 +#define FUSB_VDD_5V FUNCONF_USE_5V_VDD + +#include "usb_defines.h" + +#define FUSB_USB_VID 0x1209 +#define FUSB_USB_PID 0x3478 +#define FUSB_USB_REV 0x0100 +#define FUSB_STR_MANUFACTURER u"Open Source GPIB" +#define FUSB_STR_PRODUCT u"HP3478A Internal Adapter" +#define FUSB_STR_SERIAL u"3478A-USB-001" + +//Taken from http://www.usbmadesimple.co.uk/ums_ms_desc_dev.htm +static const uint8_t device_descriptor[] = { + 18, //bLength - Length of this descriptor + 1, //bDescriptorType - Type (Device) + 0x10, 0x01, //bcdUSB - The highest USB spec version this device supports (USB1.1) + 0x02, //bDeviceClass - Device Class + 0x0, //bDeviceSubClass - Device Subclass + 0x0, //bDeviceProtocol - Device Protocol (000 = use config descriptor) + 64, //bMaxPacketSize - Max packet size for EP0 + (uint8_t)(FUSB_USB_VID), (uint8_t)(FUSB_USB_VID >> 8), //idVendor - ID Vendor + (uint8_t)(FUSB_USB_PID), (uint8_t)(FUSB_USB_PID >> 8), //idProduct - ID Product + (uint8_t)(FUSB_USB_REV), (uint8_t)(FUSB_USB_REV >> 8), //bcdDevice - Device Release Number + 1, //iManufacturer - Index of Manufacturer string + 2, //iProduct - Index of Product string + 3, //iSerialNumber - Index of Serial string + 1, //bNumConfigurations - Max number of configurations (if more then 1, you can switch between them) +}; + +/* Configuration Descriptor Set */ +static const uint8_t config_descriptor[ ] = +{ + 0x09, // bLength + 0x02, // bDescriptorType (Configuration) + 0x43, 0x00, // wTotalLength 67 + 0x02, // bNumInterfaces 2 + 0x01, // bConfigurationValue + 0x00, // iConfiguration (String Index) + 0x80, // bmAttributes + 0x32, // bMaxPower 100mA + + 0x09, // bLength + 0x04, // bDescriptorType - Interface + 0x00, // bInterfaceNumber - 0 + 0x00, // bAlternateSetting + 0x01, // bNumEndpoints - 1 + 0x02, // bInterfaceClass - CDC + 0x02, // bInterfaceSubClass - Abstract Control Model (Table 4 in CDC120.pdf) + 0x01, // bInterfaceProtocol - AT Commands: V.250 etc (Table 5) + 0x00, // iInterface (String Index) + + // Setting up CDC interface (Table 18) + 0x05, // bLength + 0x24, // bDescriptorType - CS_INTERFACE (Table 12) + 0x00, // bDescriptorSubType - Header Functional Descriptor (Table 13) + 0x10, 0x01, // bcdCDC - USB version - USB1.1 + // Call Management Functional Descriptor + 0x05, // bLength + 0x24, // bDescriptorType - CS_INTERFACE + 0x01, // bDescriptorSubType - Call Management Functional Descriptor (Table 13) + 0x00, // bmCapabilities: (Table 3 in PSTN120.pdf) + // Bit 0 — Device handles call management itself: + // 1 = device handles call management (e.g. call setup, termination, etc.) + // 0 = host handles it + // Bit 1 — Device can send/receive call management information over a Data Class interface: + // 1 = can use the Data Class interface for call management + // 0 = must use the Communication Class interface + 0x01, // bDataInterface - Indicates that multiplexed commands are handled via data interface 01h (same value as used in the UNION Functional Descriptor) + // Abstract Control Management Functional Descriptor + 0x04, // bLength + 0x24, // bDescriptorType - CS_INTERFACE + 0x02, // bDescriptorSubType - Abstract Control Management Functional Descriptor (Table 13) + 0x02, // bmCapabilities - Device supports the request combination of Set_Line_Coding, Set_Control_Line_State, Get_Line_Coding, and the notification Serial_State (Table 4 in PSTN120.pdf) + // Union Descriptor Functional Descriptor + 0x05, // bLength + 0x24, // bDescriptorType - CS_INTERFACE + 0x06, // bDescriptorSubType - Union Descriptor Functional Descriptor (Table 13) + 0x00, // bControlInterface (Interface number of the control (Communications Class) interface) + 0x01, // bSubordinateInterface0 (Interface number of the subordinate (Data Class) interface) + // Setting up EP1 for CDC config interface + 0x07, // bLength + 0x05, // bDescriptorType (Endpoint) + 0x81, // bEndpointAddress (IN/D2H) + 0x03, // bmAttributes (Interrupt) + 0x40, 0x00, // wMaxPacketSize 64 + 0x01, // bInterval 1 (unit depends on device speed) + + // Transmission interface with two bulk endpoints + 0x09, // bLength + 0x04, // bDescriptorType (Interface) + 0x01, // bInterfaceNumber 1 + 0x00, // bAlternateSetting + 0x02, // bNumEndpoints 2 + 0x0A, // bInterfaceClass + 0x00, // bInterfaceSubClass + 0x00, // bInterfaceProtocol - Transparent + 0x00, // iInterface (String Index) + // EP2 - device to host + 0x07, // bLength + 0x05, // bDescriptorType (Endpoint) + 0x02, // bEndpointAddress (OUT/H2D) + 0x02, // bmAttributes (Bulk) + 0x40, 0x00, // wMaxPacketSize 64 + 0x00, // bInterval 0 (unit depends on device speed) + // EP3 - host to device + 0x07, // bLength + 0x05, // bDescriptorType (Endpoint) + 0x83, // bEndpointAddress (IN/D2H) + 0x02, // bmAttributes (Bulk) + 0x40, 0x00, // wMaxPacketSize 64 + 0x00, // bInterval 0 (unit depends on device speed) + + // 67 bytes +}; + +struct usb_string_descriptor_struct { + uint8_t bLength; + uint8_t bDescriptorType; + uint16_t wString[]; +}; +static const struct usb_string_descriptor_struct language __attribute__((section(".rodata"))) = { + 4, + 3, + {0x0409} // Language ID - English US (look in USB_LANGIDs) +}; +static const struct usb_string_descriptor_struct string1 __attribute__((section(".rodata"))) = { + sizeof(FUSB_STR_MANUFACTURER), + 3, // bDescriptorType - String Descriptor (0x03) + FUSB_STR_MANUFACTURER +}; +static const struct usb_string_descriptor_struct string2 __attribute__((section(".rodata"))) = { + sizeof(FUSB_STR_PRODUCT), + 3, + FUSB_STR_PRODUCT +}; +static const struct usb_string_descriptor_struct string3 __attribute__((section(".rodata"))) = { + sizeof(FUSB_STR_SERIAL), + 3, + FUSB_STR_SERIAL +}; + +// This table defines which descriptor data is sent for each specific +// request from the host (in wValue and wIndex). +static const struct descriptor_list_struct { + uint32_t lIndexValue; // (uint16_t)Index of a descriptor in config or Language ID for string descriptors | (uint8_t)Descriptor type | (uint8_t)Type of string descriptor + const uint8_t *addr; + uint8_t length; +} descriptor_list[] = { + {0x00000100, device_descriptor, sizeof(device_descriptor)}, + {0x00000200, config_descriptor, sizeof(config_descriptor)}, + // {0x00002100, config_descriptor + 18, 9 }, // Not sure why, this seems to be useful for Windows + Android. + + {0x00000300, (const uint8_t *)&language, 4}, + {0x04090301, (const uint8_t *)&string1, string1.bLength}, + {0x04090302, (const uint8_t *)&string2, string2.bLength}, + {0x04090303, (const uint8_t *)&string3, string3.bLength} +}; +#define DESCRIPTOR_LIST_ENTRIES ((sizeof(descriptor_list))/(sizeof(struct descriptor_list_struct)) ) + + +#endif + diff --git a/main.c b/main.c new file mode 100644 index 0000000..94c42fe --- /dev/null +++ b/main.c @@ -0,0 +1,1702 @@ +#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(); + } + } +} \ No newline at end of file diff --git a/systick.c b/systick.c new file mode 100644 index 0000000..08d8dc2 --- /dev/null +++ b/systick.c @@ -0,0 +1,21 @@ +#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++; +}