diff --git a/.github/workflows/Build.yml b/.github/workflows/Build.yml index 02fa981..d6a5199 100644 --- a/.github/workflows/Build.yml +++ b/.github/workflows/Build.yml @@ -4,6 +4,7 @@ on: push: branches: - main + - siglent pull_request: branches: - main @@ -199,14 +200,20 @@ jobs: Embedded_Firmware: - runs-on: ubuntu-22.04 + runs-on: ubuntu-24.04 steps: - uses: actions/checkout@v4 - name: Install toolchain run: | sudo apt-get update - sudo apt install -y build-essential gcc-arm-none-eabi binutils-arm-none-eabi + sudo apt install -y build-essential binutils-arm-none-eabi + + # Ubuntu 24.04 comes with version 10.3 of the ARM GCC toolchain. For some reason builds with that version result in buggy file reading/writing and unstable USB connections. + # Manually install older version of the toolchain which does not have the problem + wget https://developer.arm.com/-/media/Files/downloads/gnu-rm/9-2020q2/gcc-arm-none-eabi-9-2020-q2-update-x86_64-linux.tar.bz2 + tar -xvjf gcc-arm-none-eabi-9-2020-q2-update-x86_64-linux.tar.bz2 -C /opt + sudo git clone https://github.com/raspberrypi/pico-sdk.git /opt/pico-sdk sudo git -C /opt/pico-sdk checkout 2.1.1 sudo git -C /opt/pico-sdk submodule update --init @@ -229,11 +236,11 @@ jobs: - name: Build application run: | export PICO_SDK_PATH=/opt/pico-sdk + export PATH=/opt/gcc-arm-none-eabi-9-2020-q2-update/bin:$PATH cd Software/LibreCAL mkdir build && cd build cmake .. make -j9 - make -j9 shell: bash - name: Upload diff --git a/Software/LibreCAL/CMakeLists.txt b/Software/LibreCAL/CMakeLists.txt index 8a8e171..05f52b9 100644 --- a/Software/LibreCAL/CMakeLists.txt +++ b/Software/LibreCAL/CMakeLists.txt @@ -33,6 +33,7 @@ add_definitions( -DFW_MAJOR=0 -DFW_MINOR=2 -DFW_PATCH=4 +#-DENABLE_UART ) add_executable(LibreCAL @@ -48,10 +49,12 @@ add_executable(LibreCAL src/USB/msc_disk.cpp src/USB/usb_descriptors.c src/USB/usb.c + src/USB/siglent_tmc.cpp src/fatfs/ff.c src/fatfs/ffsystem.c src/fatfs/ffunicode.c src/fatfs/flashdisk.cpp + src/Log.cpp ) target_include_directories(LibreCAL PUBLIC @@ -62,4 +65,4 @@ target_include_directories(LibreCAL PUBLIC ) pico_add_extra_outputs(LibreCAL) -target_link_libraries(LibreCAL pico_stdlib pico_unique_id hardware_rtc hardware_spi hardware_pwm hardware_adc FreeRTOS tinyusb_device tinyusb_board) +target_link_libraries(LibreCAL pico_stdlib pico_unique_id hardware_rtc hardware_uart hardware_spi hardware_pwm hardware_adc FreeRTOS tinyusb_device tinyusb_board) diff --git a/Software/LibreCAL/src/Log.cpp b/Software/LibreCAL/src/Log.cpp new file mode 100644 index 0000000..67870f4 --- /dev/null +++ b/Software/LibreCAL/src/Log.cpp @@ -0,0 +1,109 @@ +#include "Log.h" + +#ifdef ENABLE_UART + +#include +#include +#include + +#include "hardware/uart.h" +#include "hardware/gpio.h" + +#include "FreeRTOS.h" +#include "task.h" + +extern "C" { + +#define MAX_LINE_LENGTH 256 + +static char fifo[LOG_SENDBUF_LENGTH + MAX_LINE_LENGTH]; +static uint16_t fifo_write, fifo_read; + +#ifdef LOG_USE_MUTEX +#include "FreeRTOS.h" +#include "semphr.h" +static StaticSemaphore_t xMutex; +static SemaphoreHandle_t mutex; +#endif + +#define INC_FIFO_POS(pos, inc) do { pos = (pos + inc) % LOG_SENDBUF_LENGTH; } while(0) + +static uint16_t fifo_space() { + uint16_t used; + if(fifo_write >= fifo_read) { + used = fifo_write - fifo_read; + } else { + used = fifo_write - fifo_read + LOG_SENDBUF_LENGTH; + } + return LOG_SENDBUF_LENGTH - used - 1; +} + +void on_uart_needs_data(void) { + if(fifo_read != fifo_write) { + uart_putc_raw(LOG_UART_ID, fifo[fifo_read]); + INC_FIFO_POS(fifo_read, 1); + } else { + // all done, disable interrupt + uart_set_irq_enables(LOG_UART_ID, false, false); + } +} + +void Log_Init() { + fifo_write = 0; + fifo_read = 0; +#ifdef LOG_USE_MUTEXES + mutex = xSemaphoreCreateMutexStatic(&xMutex); +#endif + + // initialize the UART + uart_init(LOG_UART_ID, 115200); + gpio_set_function(LOG_UART_PIN, UART_FUNCSEL_NUM(LOG_UART_ID, LOG_UART_PIN)); + + uart_set_hw_flow(LOG_UART_ID, false, false); + uart_set_fifo_enabled(LOG_UART_ID, false); + + // Set up the TX interrupt + int UART_IRQ = LOG_UART_ID == uart0 ? UART0_IRQ : UART1_IRQ; + irq_set_exclusive_handler(UART_IRQ, on_uart_needs_data); + irq_set_enabled(UART_IRQ, true); + + // we need to write something here, otherwise the handler is never called later? + uart_puts(LOG_UART_ID, "LibreCAL log start\r\n"); +} + + +void _log_write(const char *module, const char *level, const char *fmt, ...) { + int written = 0; + va_list args; + va_start(args, fmt); +#ifdef LOG_USE_MUTEX + if (!STM::InInterrupt()) { + xSemaphoreTake(mutex, portMAX_DELAY); + } +#endif + written = snprintf(&fifo[fifo_write], MAX_LINE_LENGTH, "%05lu [%6.6s,%s]: ", + xTaskGetTickCount(), module, level); + written += vsnprintf(&fifo[fifo_write + written], MAX_LINE_LENGTH - written, + fmt, args); + written += snprintf(&fifo[fifo_write + written], MAX_LINE_LENGTH - written, + "\r\n"); + + // check if line still fits into ring buffer + if (written > fifo_space()) { + // unable to fit line, skip + return; + } + + int16_t overflow = (fifo_write + written) - LOG_SENDBUF_LENGTH; + if (overflow > 0) { + // printf wrote over the end of the ring buffer -> wrap around + memmove(&fifo[0], &fifo[LOG_SENDBUF_LENGTH], overflow); + } + INC_FIFO_POS(fifo_write, written); + // enable interrupt + uart_set_irq_enables(LOG_UART_ID, false, true); +} +} + +#endif + diff --git a/Software/LibreCAL/src/Log.h b/Software/LibreCAL/src/Log.h new file mode 100644 index 0000000..22cb3b8 --- /dev/null +++ b/Software/LibreCAL/src/Log.h @@ -0,0 +1,73 @@ +#pragma once + +#ifdef __cplusplus +extern "C" { +#endif + +#ifndef ENABLE_UART +// Log output disabled +#define LOG_CRIT(fmt, ...) +#define LOG_ERR(fmt, ...) +#define LOG_WARN(fmt, ...) +#define LOG_INFO(fmt, ...) +#define LOG_DEBUG(fmt, ...) +#else + +#define LOG_UART_ID uart1 +#define LOG_UART_PIN 24 + +#define LOG_SENDBUF_LENGTH 1024 + +#define LOG_LEVEL_DEBUG 4 +#define LOG_LEVEL_INFO 3 +#define LOG_LEVEL_WARN 2 +#define LOG_LEVEL_ERR 1 +#define LOG_LEVEL_CRIT 0 + +#define LOG_LEVEL_DEFAULT LOG_LEVEL_ERR + +#ifndef LOG_LEVEL +#define LOG_LEVEL LOG_LEVEL_DEFAULT +#endif + +#ifndef LOG_MODULE +#define LOG_MODULE "Log" +#endif + +#if LOG_LEVEL >= LOG_LEVEL_CRIT +#define LOG_CRIT(fmt, ...) _log_write(LOG_MODULE, "CRT", fmt, ## __VA_ARGS__) +#else +#define LOG_CRIT(fmt, ...) +#endif +#if LOG_LEVEL >= LOG_LEVEL_ERR +#define LOG_ERR(fmt, ...) _log_write(LOG_MODULE, "ERR", fmt, ## __VA_ARGS__) +#else +#define LOG_ERR(fmt, ...) +#endif +#if LOG_LEVEL >= LOG_LEVEL_WARN +#define LOG_WARN(fmt, ...) _log_write(LOG_MODULE, "WRN", fmt, ## __VA_ARGS__) +#else +#define LOG_WARN(fmt, ...) +#endif +#if LOG_LEVEL >= LOG_LEVEL_INFO +#define LOG_INFO(fmt, ...) _log_write(LOG_MODULE, "INF", fmt, ## __VA_ARGS__) +#else +#define LOG_INFO(fmt, ...) +#endif +#if LOG_LEVEL >= LOG_LEVEL_DEBUG +#define LOG_DEBUG(fmt, ...) _log_write(LOG_MODULE, "DBG", fmt, ## __VA_ARGS__) +#else +#define LOG_DEBUG(fmt, ...) +#endif + +#include + +void Log_Init(); +typedef void (*log_redirect_t)(const char *line, uint16_t length); +void _log_write(const char *module, const char *level, const char *fmt, ...); + +#endif + +#ifdef __cplusplus +} +#endif diff --git a/Software/LibreCAL/src/USB/siglent_tmc.cpp b/Software/LibreCAL/src/USB/siglent_tmc.cpp new file mode 100644 index 0000000..ff10eb4 --- /dev/null +++ b/Software/LibreCAL/src/USB/siglent_tmc.cpp @@ -0,0 +1,294 @@ +/* + * Emulation for a Siglent SEM5000A eCal attached over USBTMC. + * + * Author: Joshua Wise + * Copyright (c) 2025 Accelerated Tech, Inc. + * + * The MIT License (MIT) + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * + */ + +#include +#include /* atoi */ +#include "tusb.h" +#include "bsp/board_api.h" +#include "serial.h" +#include "ff.h" +#include "Switch.hpp" +#include +#include + +#define LOG_LEVEL LOG_LEVEL_DEBUG +#define LOG_MODULE "USB TMC" +#include "Log.h" + +static usbtmc_response_capabilities_t const tud_usbtmc_app_capabilities = { + .USBTMC_status = USBTMC_STATUS_SUCCESS, + .bcdUSBTMC = USBTMC_VERSION, + .bmIntfcCapabilities = { + .listenOnly = 0, + .talkOnly = 0, + .supportsIndicatorPulse = 1 + }, + .bmDevCapabilities = { + .canEndBulkInOnTermChar = 0 + }, +}; + +static size_t ibuf_len; +static uint8_t ibuf[256]; + +static size_t obuf_pos = 0; +static size_t obuf_len = 0; +static uint8_t obuf[1024]; + +static FIL fl_file; +static bool fl_file_open = false; + +extern "C" void tud_usbtmc_open_cb(uint8_t interface_id) { + (void)interface_id; + tud_usbtmc_start_bus_read(); +} + +extern "C" usbtmc_response_capabilities_t const * tud_usbtmc_get_capabilities_cb() { + return &tud_usbtmc_app_capabilities; +} + + +extern "C" bool tud_usbtmc_msg_trigger_cb(usbtmc_msg_generic_t* msg) { + (void)msg; + return true; +} + +extern "C" bool tud_usbtmc_msgBulkOut_start_cb(usbtmc_msg_request_dev_dep_out const * msgHeader) { + ibuf_len = 0; + obuf_pos = 0; + obuf_len = 0; + if (msgHeader->TransferSize > sizeof(ibuf)) { + return false; + } + return true; +} + +extern "C" bool tud_usbtmc_msg_data_cb(void *data, size_t len, bool transfer_complete) { + if (len + ibuf_len < sizeof(ibuf)) { + memcpy(&(ibuf[ibuf_len]), data, len); + ibuf_len += len; + } else { + return false; // buffer overflow! + } + + if (!transfer_complete) { + tud_usbtmc_start_bus_read(); + return true; + } + + /* Ok, we're good, and we've received a message; go parse it. */ + ibuf[ibuf_len] = 0; + LOG_DEBUG("USB TMC: %s", ibuf); + + obuf_len = 0; + obuf_pos = 0; + + /* This SCPI interface supports a very different command set than the main + * SCPI interface does, so we don't even share a parser. */ + + if (!strcasecmp((char *)ibuf, "*IDN?\n")) { + obuf_pos = 0; + obuf_len = snprintf((char *)obuf, sizeof(obuf), "LibreVNA,LibreCAL,%s,%d.%d.%d\n", getSerial(), FW_MAJOR, FW_MINOR, FW_PATCH); + } else if (!strcasecmp((char *)ibuf, "FL:DATA:READ:START\n")) { + /* The FL:DATA: commands convert to filesystem reads and writes. In a + * more serious USB device, we would kick these onto another thread, but + * for this application it's ok to block the USB thread for the small + * reads. + */ + fl_file_open = f_open(&fl_file, "0:siglent/info.dat", FA_OPEN_EXISTING | FA_READ) == FR_OK; + } else if (!strncasecmp((char *)ibuf, "FL:DATA:INDEX ", 14)) { + int idx = atoi((char *)ibuf + 14); + char name[32]; + snprintf(name, sizeof(name), "0:siglent/data%d.zip", idx); + fl_file_open = f_open(&fl_file, name, FA_OPEN_EXISTING | FA_READ) == FR_OK; + } else if (!strncasecmp((char *)ibuf, "FL:DATA:READ? ", 14)) { + size_t req = atoi((char *)ibuf + 14); + + size_t len = tu_min32(sizeof(obuf), req); + size_t rv; + + if (!fl_file_open) { + goto done; + } + + if (f_read(&fl_file, obuf, len, &rv) != FR_OK) { + goto done; + } + + obuf_len = rv; + } else if (!strncasecmp((char *)ibuf, "SL ", 3)) { + /* This is not a very robust parser. */ + char *buf = (char *)ibuf + 3; + const char *cmd = strtok(buf, ","); + if (!cmd) { + goto done; + } + const char *srcport_s = strtok(NULL, ","); + if (!srcport_s) { + goto done; + } + int srcport = atoi(srcport_s) - 1; + if (!strcasecmp(cmd, "OPEN")) { + Switch::SetStandard(srcport, Switch::Standard::Open); + } else if (!strcasecmp(cmd, "SHORT")) { + Switch::SetStandard(srcport, Switch::Standard::Short); + } else if (!strcasecmp(cmd, "LOAD")) { + Switch::SetStandard(srcport, Switch::Standard::Load); + } else if (!strcasecmp(cmd, "THRU") || !strcasecmp(cmd, "ATT")) { + // Since we don't have an attenuator, we fake "SL ATT,n,m" with "THRU" + // by providing the through coefficients for CF_*. + const char *dstport_s = strtok(NULL, ",\n"); + if (!dstport_s) { + goto done; + } + int dstport = atoi(dstport_s) - 1; + Switch::SetThrough(srcport, dstport); + } else { + goto done; + } + + } else if(!strncasecmp((char*) ibuf, "SET:PORT", 8)) { + // SNA5000A sends these commands. There is one mandatory argument + // which is either OPEN, SHORT, LOAD, THRU or ATT. This is then + // followed by a list of ports (by their letter, e.g. 'A' or 'D'). + // All arguments are comma separated. Example command: + // SET:PORT LOAD,A,B + + // assemble the port list + uint8_t ports[4] = {0,0,0,0}; + uint8_t port_cnt = 0; + char *comma = (char*) ibuf; + while(comma = strchr(comma, ',')) { + ports[port_cnt++] = *++comma - 'A'; + } + + // figure out the argument + if(!strncasecmp((char*) &ibuf[9], "THRU", 4) || !strncasecmp((char*) &ibuf[9], "ATT", 4)) { + // special case, this must always have two ports + if(port_cnt != 2) { + LOG_ERR("%d ports given for THRU", port_cnt); + goto done; + } + Switch::SetThrough(ports[0], ports[1]); + goto done; + } + // handle the other standards + auto s = Switch::Standard::None; + if(!strncasecmp((char*) &ibuf[9], "OPEN", 4)) { + s = Switch::Standard::Open; + } else if(!strncasecmp((char*) &ibuf[9], "SHORT", 5)) { + s = Switch::Standard::Short; + } else if(!strncasecmp((char*) &ibuf[9], "LOAD", 4)) { + s = Switch::Standard::Load; + } else { + LOG_ERR("Unknown port standard: %s", &ibuf[9]); + goto done; + } + for(uint8_t i=0;iTransferSize); + if (txlen == 0) { + return true; + } + + tud_usbtmc_transmit_dev_msg_data(obuf + obuf_pos, txlen, (obuf_pos + txlen) == obuf_len, false); + obuf_pos += txlen; + + return true; +} + +extern "C" void usbtmc_app_task_iter(void) { +} + +extern "C" bool tud_usbtmc_initiate_clear_cb(uint8_t *tmcResult) { + *tmcResult = USBTMC_STATUS_SUCCESS; + return true; +} + +extern "C" bool tud_usbtmc_check_clear_cb(usbtmc_get_clear_status_rsp_t *rsp) { + ibuf_len = 0; + obuf_len = 0; + obuf_pos = 0; + rsp->USBTMC_status = USBTMC_STATUS_SUCCESS; + rsp->bmClear.BulkInFifoBytes = 0u; + return true; +} + +extern "C" bool tud_usbtmc_initiate_abort_bulk_in_cb(uint8_t *tmcResult) { + *tmcResult = USBTMC_STATUS_SUCCESS; + return true; +} + +extern "C" bool tud_usbtmc_check_abort_bulk_in_cb(usbtmc_check_abort_bulk_rsp_t *rsp) { + (void)rsp; + tud_usbtmc_start_bus_read(); + return true; +} + +extern "C" bool tud_usbtmc_initiate_abort_bulk_out_cb(uint8_t *tmcResult) { + *tmcResult = USBTMC_STATUS_SUCCESS; + return true; +} + +extern "C" bool tud_usbtmc_check_abort_bulk_out_cb(usbtmc_check_abort_bulk_rsp_t *rsp) { + (void)rsp; + tud_usbtmc_start_bus_read(); + return true; +} + +extern "C" void tud_usbtmc_bulkIn_clearFeature_cb(void) { +} + +extern "C" void tud_usbtmc_bulkOut_clearFeature_cb(void) { + tud_usbtmc_start_bus_read(); +} + +extern "C" bool tud_usbtmc_indicator_pulse_cb(tusb_control_request_t const * msg, uint8_t *tmcResult) { + (void)msg; + // led_indicator_pulse(); + *tmcResult = USBTMC_STATUS_SUCCESS; + return true; +} diff --git a/Software/LibreCAL/src/USB/tusb_config.h b/Software/LibreCAL/src/USB/tusb_config.h index bc6b138..3a1d76a 100644 --- a/Software/LibreCAL/src/USB/tusb_config.h +++ b/Software/LibreCAL/src/USB/tusb_config.h @@ -100,6 +100,9 @@ #define CFG_TUD_MSC 1 #define CFG_TUD_MIDI 0 #define CFG_TUD_VENDOR 1 +#define CFG_TUD_USBTMC 1 +#define CFG_TUD_USBTMC_ENABLE_INT_EP 0 +#define CFG_TUD_USBTMC_ENABLE_488 0 // CDC FIFO size of TX and RX #define CFG_TUD_CDC_RX_BUFSIZE (TUD_OPT_HIGH_SPEED ? 512 : 256) diff --git a/Software/LibreCAL/src/USB/usb.h b/Software/LibreCAL/src/USB/usb.h index 73d1968..ab5e746 100644 --- a/Software/LibreCAL/src/USB/usb.h +++ b/Software/LibreCAL/src/USB/usb.h @@ -17,6 +17,7 @@ typedef enum { typedef void(*usbd_recv_callback_t)(const uint8_t *buf, uint16_t len, usb_interface_t i); void usb_init(usbd_recv_callback_t receive_callback); +void usb_is_siglent(); uint16_t usb_available_buffer(); bool usb_transmit(const uint8_t *data, uint16_t length, uint8_t interface); void usb_log(const char *log, uint16_t length); diff --git a/Software/LibreCAL/src/USB/usb_descriptors.c b/Software/LibreCAL/src/USB/usb_descriptors.c index 4122453..78e319c 100644 --- a/Software/LibreCAL/src/USB/usb_descriptors.c +++ b/Software/LibreCAL/src/USB/usb_descriptors.c @@ -27,6 +27,7 @@ #include #include "tusb.h" #include "serial.h" +#include "main.h" #define USB_PID (0x4000 | _PID_MAP(CDC, 0) | _PID_MAP(MSC, 1) | _PID_MAP(HID, 2) | \ _PID_MAP(MIDI, 3) | _PID_MAP(VENDOR, 4) ) @@ -34,7 +35,7 @@ //--------------------------------------------------------------------+ // Device Descriptors //--------------------------------------------------------------------+ -tusb_desc_device_t const desc_device = +tusb_desc_device_t const desc_device_default = { .bLength = sizeof(tusb_desc_device_t), .bDescriptorType = TUSB_DESC_DEVICE, @@ -58,65 +59,112 @@ tusb_desc_device_t const desc_device = .bNumConfigurations = 0x01 }; +tusb_desc_device_t const desc_device_siglent = +{ + .bLength = sizeof(tusb_desc_device_t), + .bDescriptorType = TUSB_DESC_DEVICE, + .bcdUSB = 0x0210, // at least 2.1 or 3.x for BOS & webUSB + + // We behave like a USBTMC device for this. + .bDeviceClass = 0x00, + .bDeviceSubClass = 0x00, + .bDeviceProtocol = 0x00, + .bMaxPacketSize0 = CFG_TUD_ENDPOINT0_SIZE, + + // Pretend to be a Siglent eCal. + .idVendor = 0xf4ec, + .idProduct = 0x1600, + .bcdDevice = 0x0100, + + .iManufacturer = 0x01, + .iProduct = 0x08, + .iSerialNumber = 0x03, + + .bNumConfigurations = 0x01 +}; + // Invoked when received GET DEVICE DESCRIPTOR // Application return pointer to descriptor uint8_t const * tud_descriptor_device_cb(void) { - return (uint8_t const *) &desc_device; + switch(getUsbMode()) { + case MODE_DEFAULT: return (uint8_t const *) &desc_device_default; + case MODE_SIGLENT: return (uint8_t const *) &desc_device_siglent; + } } //--------------------------------------------------------------------+ // Configuration Descriptor //--------------------------------------------------------------------+ +// Default mode enum { - ITF_NUM_CDC = 0, - ITF_NUM_CDC_DATA, - ITF_NUM_VENDOR, - ITF_NUM_MSC, - ITF_NUM_TOTAL + ITF_DEF_NUM_CDC = 0, + ITF_DEF_NUM_CDC_DATA, + ITF_DEF_NUM_VENDOR, + ITF_DEF_NUM_MSC, + ITF_DEF_NUM_TOTAL }; +// Endpoints for default mode +#define EPNUM_CDC_IN 2 +#define EPNUM_CDC_OUT 2 +#define EPNUM_VENDOR_IN 3 +#define EPNUM_VENDOR_OUT 3 +#define EPNUM_MSC_IN 4 +#define EPNUM_MSC_OUT 4 +#define EPNUM_MSC2_IN 5 +#define EPNUM_MSC2_OUT 5 + +// Siglent mode +enum +{ + ITF_SIGLENT_NUM_CDC = 0, + ITF_SIGLENT_NUM_CDC_DATA, + ITF_SIGLENT_NUM_TMC, + ITF_SIGLENT_NUM_TOTAL +}; +// Endpoints for Siglent mode +#define EPNUM_TMC_IN 3 +#define EPNUM_TMC_OUT 3 + #define CONFIG_TOTAL_LEN (TUD_CONFIG_DESC_LEN + TUD_CDC_DESC_LEN + TUD_VENDOR_DESC_LEN + TUD_MSC_DESC_LEN) -#if CFG_TUSB_MCU == OPT_MCU_LPC175X_6X || CFG_TUSB_MCU == OPT_MCU_LPC177X_8X || CFG_TUSB_MCU == OPT_MCU_LPC40XX - // LPC 17xx and 40xx endpoint type (bulk/interrupt/iso) are fixed by its number - // 0 control, 1 In, 2 Bulk, 3 Iso, 4 In etc ... - #define EPNUM_CDC_IN 2 - #define EPNUM_CDC_OUT 2 - #define EPNUM_VENDOR_IN 5 - #define EPNUM_VENDOR_OUT 5 -#elif CFG_TUSB_MCU == OPT_MCU_SAMG || CFG_TUSB_MCU == OPT_MCU_SAMX7X - // SAMG & SAME70 don't support a same endpoint number with different direction IN and OUT - // e.g EP1 OUT & EP1 IN cannot exist together - #define EPNUM_CDC_IN 2 - #define EPNUM_CDC_OUT 3 - #define EPNUM_VENDOR_IN 4 - #define EPNUM_VENDOR_OUT 5 -#else - #define EPNUM_CDC_IN 2 - #define EPNUM_CDC_OUT 2 - #define EPNUM_VENDOR_IN 3 - #define EPNUM_VENDOR_OUT 3 - #define EPNUM_MSC_IN 4 - #define EPNUM_MSC_OUT 4 - #define EPNUM_MSC2_IN 5 - #define EPNUM_MSC2_OUT 5 -#endif - -uint8_t const desc_configuration[] = +uint8_t const desc_configuration_default[] = { // Config number, interface count, string index, total length, attribute, power in mA - TUD_CONFIG_DESCRIPTOR(1, ITF_NUM_TOTAL, 0, CONFIG_TOTAL_LEN, 0x00, 100), + TUD_CONFIG_DESCRIPTOR(1, ITF_DEF_NUM_TOTAL, 0, CONFIG_TOTAL_LEN, 0x00, 100), // Interface number, string index, EP notification address and size, EP data address (out, in) and size. - TUD_CDC_DESCRIPTOR(ITF_NUM_CDC, 4, 0x81, 8, EPNUM_CDC_OUT, 0x80 | EPNUM_CDC_IN, TUD_OPT_HIGH_SPEED ? 512 : 64), + TUD_CDC_DESCRIPTOR(ITF_DEF_NUM_CDC, 4, 0x81, 8, EPNUM_CDC_OUT, 0x80 | EPNUM_CDC_IN, TUD_OPT_HIGH_SPEED ? 512 : 64), // Interface number, string index, EP Out & IN address, EP size - TUD_VENDOR_DESCRIPTOR(ITF_NUM_VENDOR, 5, EPNUM_VENDOR_OUT, 0x80 | EPNUM_VENDOR_IN, TUD_OPT_HIGH_SPEED ? 512 : 64), + TUD_VENDOR_DESCRIPTOR(ITF_DEF_NUM_VENDOR, 5, EPNUM_VENDOR_OUT, 0x80 | EPNUM_VENDOR_IN, TUD_OPT_HIGH_SPEED ? 512 : 64), // Interface number, string index, EP Out & EP In address, EP size - TUD_MSC_DESCRIPTOR(ITF_NUM_MSC, 6, EPNUM_MSC_OUT, 0x80 | EPNUM_MSC_IN, 64), + TUD_MSC_DESCRIPTOR(ITF_DEF_NUM_MSC, 6, EPNUM_MSC_OUT, 0x80 | EPNUM_MSC_IN, 64), +}; + +/* We don't want a MSC device to confuse the UI and attempt to save files to + * "USB storage", so Siglent mode exposes its TMC interface *instead* of the + * MSC interface. + */ + +#define CONFIG_TOTAL_LEN_SIGLENT (TUD_CONFIG_DESC_LEN + TUD_CDC_DESC_LEN + TUD_USBTMC_IF_DESCRIPTOR_LEN + TUD_USBTMC_BULK_DESCRIPTORS_LEN) + +uint8_t const desc_configuration_siglent[] = +{ + // Config number, interface count, string index, total length, attribute, power in mA + TUD_CONFIG_DESCRIPTOR(1, ITF_SIGLENT_NUM_TOTAL, 0, CONFIG_TOTAL_LEN_SIGLENT, 0x00, 100), + + // Interface number, string index, EP notification address and size, EP data address (out, in) and size. + TUD_CDC_DESCRIPTOR(ITF_SIGLENT_NUM_CDC, 4, 0x81, 8, EPNUM_CDC_OUT, 0x80 | EPNUM_CDC_IN, TUD_OPT_HIGH_SPEED ? 512 : 64), +// +// // Interface number, string index, EP Out & IN address, EP size +// TUD_VENDOR_DESCRIPTOR(ITF_NUM_VENDOR, 5, EPNUM_VENDOR_OUT, 0x80 | EPNUM_VENDOR_IN, TUD_OPT_HIGH_SPEED ? 512 : 64), + + TUD_USBTMC_IF_DESCRIPTOR(ITF_SIGLENT_NUM_TMC, /* _bNumEndpoints = */ 2u, /*_stridx = */ 7u, 0 /* no subclass */), + TUD_USBTMC_BULK_DESCRIPTORS(/* OUT = */ EPNUM_TMC_OUT, /* IN = */ 0x80 | EPNUM_TMC_IN, /* packet size = */ 64), }; // Invoked when received GET CONFIGURATION DESCRIPTOR @@ -125,7 +173,10 @@ uint8_t const desc_configuration[] = uint8_t const * tud_descriptor_configuration_cb(uint8_t index) { (void) index; // for multiple configurations - return desc_configuration; + switch(getUsbMode()) { + case MODE_DEFAULT: return desc_configuration_default; + case MODE_SIGLENT: return desc_configuration_siglent; + } } //--------------------------------------------------------------------+ @@ -175,7 +226,7 @@ uint8_t const desc_ms_os_20[] = U16_TO_U8S_LE(0x0008), U16_TO_U8S_LE(MS_OS_20_SUBSET_HEADER_CONFIGURATION), 0, 0, U16_TO_U8S_LE(MS_OS_20_DESC_LEN-0x0A), // Function Subset header: length, type, first interface, reserved, subset length - U16_TO_U8S_LE(0x0008), U16_TO_U8S_LE(MS_OS_20_SUBSET_HEADER_FUNCTION), ITF_NUM_VENDOR, 0, U16_TO_U8S_LE(MS_OS_20_DESC_LEN-0x0A-0x08), + U16_TO_U8S_LE(0x0008), U16_TO_U8S_LE(MS_OS_20_SUBSET_HEADER_FUNCTION), ITF_DEF_NUM_VENDOR, 0, U16_TO_U8S_LE(MS_OS_20_DESC_LEN-0x0A-0x08), // MS OS 2.0 Compatible ID descriptor: length, type, compatible ID, sub compatible ID U16_TO_U8S_LE(0x0014), U16_TO_U8S_LE(MS_OS_20_FEATURE_COMPATBLE_ID), 'W', 'I', 'N', 'U', 'S', 'B', 0x00, 0x00, @@ -210,9 +261,11 @@ char const* string_desc_arr [] = "LibreCAL CDC", // 4: CDC Interface "LibreCAL Vendor", // 5: Vendor Interface "LibreCAL Storage", // 6: MSC Interface + "LibreCAL Siglent TMC", // 7: USBTMC interface + "LibreCAL (Siglent eCal emulation mode)", // 8: Product ID in eCal emulation mode }; -static uint16_t _desc_str[32]; +static uint16_t _desc_str[64]; // Invoked when received GET STRING DESCRIPTOR request // Application return pointer to descriptor, whose contents must exist long enough for transfer to complete @@ -242,7 +295,7 @@ uint16_t const* tud_descriptor_string_cb(uint8_t index, uint16_t langid) // Cap at max char chr_count = (uint8_t) strlen(str); - if ( chr_count > 31 ) chr_count = 31; + if ( chr_count > 63 ) chr_count = 63; // Convert ASCII string into UTF-16 for(uint8_t i=0; i +#include "Log.h" +#include "main.h" namespace UserInterface { @@ -36,9 +38,18 @@ constexpr uint32_t ledBlinkPeriod = 200; constexpr uint32_t ledBlinkOnTime = 100; void setLED(LED led, bool on) { +#ifdef ENABLE_UART + if(LEDpins[(int) led] == LOG_UART_PIN) { + return; + } +#endif gpio_put(LEDpins[(int) led], !on); } +bool IsFunctionHeld() { + return !gpio_get(Buttonpins[(int)Button::FUNCTION]); +} + void Task(void*) { bool editing = false; uint8_t selectedPort = 0; @@ -90,11 +101,14 @@ void Task(void*) { } // update LEDs + + // wait/ready are solid on when in default mode and blink when in any other mode + bool on = (getUsbMode() == MODE_DEFAULT) || (xTaskGetTickCount() % ledBlinkPeriod > ledBlinkOnTime); if(Heater::IsStable()) { setLED(LED::WAIT, false); - setLED(LED::READY, true); + setLED(LED::READY, on); } else { - setLED(LED::WAIT, true); + setLED(LED::WAIT, on); setLED(LED::READY, false); } if(editing) { @@ -134,6 +148,11 @@ void Task(void*) { void Init() { // Initialize pins for(uint8_t i=0;i +#include +#include #include #include #include @@ -18,6 +20,10 @@ #include "UserInterface.hpp" #include "Heater.hpp" +#define LOG_LEVEL LOG_LEVEL_INFO +#define LOG_MODULE "App" +#include "Log.h" + const char* date = __DATE__; const char* time = __TIME__; @@ -58,6 +64,12 @@ static xTaskHandle handle; Flash flash(spi0, FLASH_CLK_PIN, FLASH_MOSI_PIN, FLASH_MISO_PIN, FLASH_CS_PIN); +static ecal_usb_mode_t mode = MODE_DEFAULT; + +ecal_usb_mode_t getUsbMode() { + return mode; +} + static void usb_rx(const uint8_t *buf, uint16_t len, usb_interface_t i) { if(len > sizeof(usb_buffer)) { // line too long @@ -128,6 +140,21 @@ static void defaultTask(void* ptr) { createInfoFile(); } } + + // Figure out which mode we should be in. The default mode is the normal LibreCAL mode. + // You can always force that mode by pressing the function button when power is applied. + // If that button is not pressed, the LibreCAL may emulate other electronic calibration + // units it the necessary files are available + if(UserInterface::IsFunctionHeld()) { + mode = MODE_DEFAULT; + } else { + // default mode is not forced, check files + if(!f_open(&fil, "0:siglent/info.dat", FA_OPEN_EXISTING | FA_READ)) { + // we have data in Siglent format + mode = MODE_SIGLENT; + f_close(&fil); + } + } usb_init(usb_rx); @@ -140,6 +167,10 @@ static void defaultTask(void* ptr) { } int main(void) { +#ifdef ENABLE_UART + Log_Init(); + LOG_INFO("Start"); +#endif spi_init(spi0, 20000 * 1000); gpio_set_function(FLASH_CLK_PIN, GPIO_FUNC_SPI); gpio_set_function(FLASH_MOSI_PIN, GPIO_FUNC_SPI); diff --git a/Software/LibreCAL/src/main.h b/Software/LibreCAL/src/main.h new file mode 100644 index 0000000..7175b99 --- /dev/null +++ b/Software/LibreCAL/src/main.h @@ -0,0 +1,16 @@ +#pragma once + +#ifdef __cplusplus +extern "C" { +#endif + +typedef enum { + MODE_DEFAULT, + MODE_SIGLENT, +} ecal_usb_mode_t; + +ecal_usb_mode_t getUsbMode(); + +#ifdef __cplusplus +} +#endif diff --git a/Software/Scripts/convert_siglent.py b/Software/Scripts/convert_siglent.py new file mode 100755 index 0000000..9a81a34 --- /dev/null +++ b/Software/Scripts/convert_siglent.py @@ -0,0 +1,138 @@ +#!/usr/bin/env python3 + +""" +Converts LibreCAL Touchstone files to a format that a Siglent VNA can use. + +Usage: + +$ python3 convert_siglent.py /Volumes/LIBRECAL_R /Volumes/LIBRECAL_RW + +(or your paths or drive letters, as appropriate) + +This will create a "siglent" directory in your LIBRECAL_RW volume. Then, +plug in your LibreCAL while holding down the FUNCTION button, which will +activate Siglent eCal compatibility mode, and calibrate your VNA as if you +were using a SEM5000A (or other Siglent eCal). + +Tested on SVA1032X and SNA5000A. +""" + +__author__ = "Joshua Wise" +__copyright__ = "Copyright (c) 2025 Accelerated Tech, Inc." +__license__ = "MIT" + +import io +from pathlib import Path +import sys +import numpy as np +from zipfile import ZipFile, ZIP_DEFLATED +import zipfile +import struct +import hashlib +from time import strftime, gmtime + +if len(sys.argv) != 3: + print(f"usage: {sys.argv[0]} /Volumes/LIBRECAL_R /Volumes/LIBRECAL_RW") + sys.exit(1) + +indir = Path(sys.argv[1]) +outdir = Path(sys.argv[2]) + +def say_open(name, *args): + print(f"reading {name}", file=sys.stderr) + return open(name, *args) + +zipbuf = io.BytesIO() +with ZipFile(zipbuf, "w", compression=ZIP_DEFLATED, compresslevel=9) as zf, \ + zf.open("Factory.csv", "w") as outf_b, \ + io.TextIOWrapper(outf_b) as outf: + with say_open(indir / "info.txt", "r") as inf: + for l in inf.readlines(): + outf.write(f"! {l.strip()}\n") + outf.write("#HZ,A,B,C,D,T_AB,T_AC,T_AD,T_BC,T_BD,T_CD,CF_AB,CF_AC,CF_AD,CF_BC,CF_BD,CF_CD\n") + + freqs = [] # will get populated below + axes = [freqs] + + for port in ["1", "2", "3", "4"]: + for col in "OPEN", "SHORT", "LOAD": + with say_open(indir / f"P{port}_{col}.s1p", "r") as inf: + ax_r, ax_i = [], [] + for l in inf.readlines(): + l = l.strip() + if l.startswith("!") or l.startswith("#"): + continue + freq,r,i = [float(x) for x in l.split(" ")] + freqs.append(freq * 1e9) + ax_r.append(r) + ax_i.append(i) + axes.append(ax_r) + axes.append(ax_i) + freqs = [] # hope they're all the same! + axes.append([0 for _ in axes[0]]) + axes.append([0 for _ in axes[0]]) + + # We don't have an actual confidence check thru standard on LibreCAL, so + # we use the THROUGH standard for that. + for port in ["12", "13", "14", "23", "24", "34"] * 2: + with say_open(indir / f"P{port}_THROUGH.s2p", "r") as inf: + snps = [ [] for _ in range(8) ] + for l in inf.readlines(): + if l.startswith("!") or l.startswith("#"): + continue + snp_line = [float(x) for x in l.split(" ")][1:] + for k,v in enumerate([float(x) for x in l.split(" ")][1:]): + snps[k].append(v) + for l in snps: + axes.append(l) + + # Some early LibreCALs have a truncated P34_THROUGH.s2p file. The + # Siglent format only supports identical number of points for all + # standards. Keep track of shortest parameter list and truncate all + # parameters to that value. + shortest_axis = min(len(ax) for ax in axes) + + for i in range(len(axes)): + axes[i] = axes[i][:shortest_axis] + + ar = np.vstack(axes, dtype=np.float64).transpose() + print(f"compressing {ar.shape[0]} points with {ar.shape[1]} columns") + np.savetxt(outf, ar, delimiter=",") + +ziphash = hashlib.md5(zipbuf.getvalue()).hexdigest() + +caldate = gmtime((indir / "P1_OPEN.s1p").stat().st_ctime) +info = { k: v for k,v in (line.split(": ") for line in open(indir / "info.txt", "r").read().split("\n")) } + +VENDOR = "LibreVNA" +PRODUCT = "LibreCAL" +SERIAL = info['Serial'] +BYTE_0x4E = 4 # Not sure but best guess is that these bytes represent the number of ports on the eCal +BYTE_0x4F = 0 +header = struct.pack("30s16s16s16sBB64s", b"", VENDOR.encode(), PRODUCT.encode(), SERIAL.encode(), BYTE_0x4E, BYTE_0x4F, b"") + +header += f"""Connector:SMA +Module:Factory +Desc:{ziphash} +Frequency:{int(axes[0][0])},{int(axes[0][-1])},{len(axes[0])} +Data:0,{len(zipbuf.getvalue())},{ziphash} +Date:{strftime('%Y-%m-%d', caldate)} +""".encode() + +# There can also be a "Extension: a,b,c,d" (what is that?) after Module:, +# and there can also be other Modules; we don't handle these for now. +# Connector: must always come before Module:. + +header += b"\x00" * (1024 - len(header)) + +(outdir / "siglent").mkdir(exist_ok = True) + +zipname = outdir / "siglent/data0.zip" +print(f"writing {zipname} ({len(zipbuf.getvalue())} bytes)", file=sys.stderr) +with open(zipname, "wb") as f: + f.write(zipbuf.getvalue()) + +datname = outdir / "siglent/info.dat" +print(f"writing {datname}", file=sys.stderr) +with open(datname, "wb") as f: + f.write(header)