From 904ade3700b8be15dbb05aaeb4fd2263d93ba0dd Mon Sep 17 00:00:00 2001 From: erikkaashoek Date: Mon, 2 Nov 2026 14:19:56 +0100 Subject: [PATCH 1/8] Unique USB serial number from the MCU ID (issue #85) All units reported the ChibiOS kernel version "400" as USB serial number, so Windows could not tell two tinySAs apart: with two of them on the same VID/PID only one enumerated, and a unit could get another unit's COM port. The serial number is now 12 hex digits made from the MCU unique ID the way the STM32F303 DFU bootloader makes its serial number (UID0 + UID2, then the upper half of UID2): a tinySA Ultra has the same serial as COM port and in DFU mode. It is filled at every USB reset, before the host asks for it. Windows assigns every unit a new COM port once. Tested: ZS407 enumerates as 1781:0A44\205D346A2039 (its DFU serial), the tinySA BASIC as 0483:5740\2056325F2039, both work side by side. BASIC about +80 B flash, +26 B RAM. --- usbcfg.c | 85 +++++++++++++------------------------------------------- 1 file changed, 20 insertions(+), 65 deletions(-) diff --git a/usbcfg.c b/usbcfg.c index 928a4ec..7333816 100644 --- a/usbcfg.c +++ b/usbcfg.c @@ -248,74 +248,28 @@ static const uint8_t vcom_string2[] = { }; #endif -#ifdef TINYSA4 /* - * Serial Number string. VERSION = 'tinySA4_v1.3-nnn-gxxxxxxx' - * 01234567890123456789012 - * skip last two 'xx' char due to 'tinySA4_v1.3-n-gxxxxxxx' + * Serial Number string: 12 hex digits made from the MCU unique ID the way the STM32F303 DFU + * bootloader makes its serial number (UID0 + UID2, then the upper half of UID2), so every tinySA + * has its own. It was the kernel version "400" on all of them: Windows could not tell two units + * apart and gave one unit's COM port to the other. Filled at every USB reset, before it is requested. */ -static const uint8_t vcom_string3[] = +static uint8_t vcom_string3[2 + 2 * 12]; // the upper bytes of the UTF-16 characters stay 0 + +static void fill_serial_string(void) { -#if 1 - USB_DESC_BYTE(8), /* bLength. */ - USB_DESC_BYTE(USB_DESCRIPTOR_STRING), /* bDescriptorType. */ - '0' + CH_KERNEL_MAJOR, 0, - '0' + CH_KERNEL_MINOR, 0, - '0' + CH_KERNEL_PATCH, 0 -#else - USB_DESC_BYTE(32), /* bLength. */ - USB_DESC_BYTE(USB_DESCRIPTOR_STRING), /* bDescriptorType. */ - VERSION[8], 0, /* 'v' */ - VERSION[9], 0, /* '1' */ - VERSION[10], 0, /* '.' */ - VERSION[11], 0, /* '3' */ - VERSION[12], 0, /* '-' */ - VERSION[13], 0, /* 'n' */ - VERSION[14], 0, /* 'n' */ - VERSION[15], 0, /* 'n' */ - VERSION[16], 0, /* '-' */ - VERSION[17], 0, /* 'g' */ - VERSION[18], 0, /* 'x' */ - VERSION[19], 0, /* 'x' */ - VERSION[20], 0, /* 'x' */ - VERSION[21], 0, /* 'x' */ - VERSION[22], 0, /* 'x' */ -#endif -}; -#else -/* - * Serial Number string. VERSION = 'tinySA_v1.3-nnn-gxxxxxxx' - * 0123456789012345678901 - * skip last two 'xx' char due to 'tinySA_v1.3-n-gxxxxxxx' - */ -static const uint8_t vcom_string3[] = { -#if 1 - USB_DESC_BYTE(8), /* bLength. */ - USB_DESC_BYTE(USB_DESCRIPTOR_STRING), /* bDescriptorType. */ - '0' + CH_KERNEL_MAJOR, 0, - '0' + CH_KERNEL_MINOR, 0, - '0' + CH_KERNEL_PATCH, 0 -#else - USB_DESC_BYTE(32), /* bLength. */ - USB_DESC_BYTE(USB_DESCRIPTOR_STRING), /* bDescriptorType. */ - VERSION[7], 0, /* 'v' */ - VERSION[8], 0, /* '1' */ - VERSION[9], 0, /* '.' */ - VERSION[10], 0, /* '3' */ - VERSION[11], 0, /* '-' */ - VERSION[12], 0, /* 'n' */ - VERSION[13], 0, /* 'n' */ - VERSION[14], 0, /* 'n' */ - VERSION[15], 0, /* '-' */ - VERSION[16], 0, /* 'g' */ - VERSION[17], 0, /* 'x' */ - VERSION[18], 0, /* 'x' */ - VERSION[19], 0, /* 'x' */ - VERSION[20], 0, /* 'x' */ - VERSION[21], 0, /* 'x' */ -#endif -}; -#endif + const uint32_t *uid = (const uint32_t *)UID_BASE; + uint32_t v = uid[0] + uid[2]; + vcom_string3[0] = sizeof vcom_string3; + vcom_string3[1] = USB_DESCRIPTOR_STRING; + for (int i = 2; i < (int)sizeof vcom_string3; i += 2) { + if (i == 2 + 2 * 8) // 8 digits done: then the upper half of UID2 + v = uid[2]; + uint8_t c = v >> 28; + v <<= 4; + vcom_string3[i] = c + (c < 10 ? '0' : 'A' - 10); + } +} /* * Strings wrappers array. @@ -406,6 +360,7 @@ static void usb_event(USBDriver *usbp, usbevent_t event) { switch (event) { case USB_EVENT_RESET: + fill_serial_string(); #ifdef __USE_USB_MSC__ { /* usb_lld_reset() has just allocated the EP0 buffers.*/ From 36059896ab8761fe328732173883d0cbf0ee802e Mon Sep 17 00:00:00 2001 From: erikkaashoek Date: Mon, 2 Nov 2026 14:19:56 +0100 Subject: [PATCH 2/8] Keep the battery display when paused from the console (issue #56) draw_cal_status() clears the whole left panel, the battery included. The battery is redrawn when a sweep completes, which does not happen while paused, so after the "pause" command (it draws the status panel directly) the battery and voltage stayed blank. draw_cal_status() now also requests the battery redraw (not in output mode, which shows no battery). Tested on a ZS407 and a tinySA BASIC: battery and voltage shown after "pause". +8 B. --- ui.c | 1 + 1 file changed, 1 insertion(+) diff --git a/ui.c b/ui.c index a294d1e..7912c1f 100644 --- a/ui.c +++ b/ui.c @@ -6495,6 +6495,7 @@ redraw_cal_status: #endif return; } + redraw_request |= REDRAW_BATTERY; // the fill erased it, while paused no completed sweep redraws it // if (current_menu_is_form() && !in_selftest) // return; From ae14b072b5e4546d036a4983523335ab24404723 Mon Sep 17 00:00:00 2001 From: erikkaashoek Date: Mon, 2 Nov 2026 14:19:57 +0100 Subject: [PATCH 3/8] ext_gain: show the current value (issue #171) "ext_gain" without an argument now prints the current external gain after the usage line, as "attenuate" and "rbw" do. Tested on a ZS407 and a tinySA BASIC: 0.0, 12.5 after "ext_gain 12.5". +16 B. --- sa_cmd.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sa_cmd.c b/sa_cmd.c index b4c7ce3..c09c070 100644 --- a/sa_cmd.c +++ b/sa_cmd.c @@ -398,7 +398,7 @@ VNA_SHELL_FUNCTION(cmd_sweeptime) VNA_SHELL_FUNCTION(cmd_ext_gain) { if (argc != 1 || argv[0][0] == '?') { - usage_printf("ext_gain -100.0..+100.0\r\n"); + usage_printf("ext_gain -100.0..+100.0\r\n%.1f\r\n", setting.external_gain); return; } float o = my_atof(argv[0]); From c051d52ad8b835a8fa9537578d99d2bb645012f4 Mon Sep 17 00:00:00 2001 From: erikkaashoek Date: Mon, 2 Nov 2026 14:19:57 +0100 Subject: [PATCH 4/8] Preset loaded from SD: use its RBW (issue #97) Loading a preset from SD did not call update_rbw() as caldata_recall() does, so the RBW menu showed the preset's RBW while the sweep kept using the previous one. Tested on a ZS407: preset stored with RBW 30 kHz, RBW set to 300 kHz, preset loaded from SD: "rbw" and the status panel show 30 kHz. tinySA Ultra only, +4 B. --- vna_browser.c | 1 + 1 file changed, 1 insertion(+) diff --git a/vna_browser.c b/vna_browser.c index df9c56e..4d40e44 100644 --- a/vna_browser.c +++ b/vna_browser.c @@ -334,6 +334,7 @@ static char cmd_buffer[256+128]; set_scale(setting.scale); set_reflevel(setting.reflevel); set_level_meter_or_waterfall(); + update_rbw(); // as caldata_recall(), else the RBW of the loaded preset is not used // Remember where this preset came from so storing it back can default to it plot_printf(sd_preset_path, sizeof(sd_preset_path), "%s", fno.fname); } From 5a4ccbd9108524656e46dcff1cfa19fd119dd29d Mon Sep 17 00:00:00 2001 From: erikkaashoek Date: Mon, 2 Nov 2026 14:19:57 +0100 Subject: [PATCH 5/8] Band names in .bnd files (issue #136) BANDS->SD stored start, end and level only, so the band names were lost. The name is now written as a quoted fourth field (older firmware ignores it) and loaded when present (files of older firmware keep the current names). A line needs start, end and level: the level was read from lines with only two fields as well. Tested on a ZS407: band named FMRADIO stored as 0, 900000000, 0.000000000, "FMRADIO" renamed, loaded from SD: FMRADIO again. tinySA Ultra only. --- ui.c | 3 +-- vna_browser.c | 4 +++- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/ui.c b/ui.c index 7912c1f..41b2fea 100644 --- a/ui.c +++ b/ui.c @@ -8449,8 +8449,7 @@ static void sa_save_file(uint8_t format) { char *buf = (char *)spi_buffer; buf += plot_printf(buf, 100, "%U, ", setting.bands[i].start); buf += plot_printf(buf, 100, "%U, ", setting.bands[i].end); - buf += plot_printf(buf, 100, "%f ", setting.bands[i].level); - buf += plot_printf(buf, 100, "\r\n"); + buf += plot_printf(buf, 100, "%f, \"%s\"\r\n", setting.bands[i].level, setting.bands[i].name); // name: older firmware ignores it res = f_write(fs_file, (char *)spi_buffer, buf - (char *)spi_buffer, &size); } } diff --git a/vna_browser.c b/vna_browser.c index 4d40e44..f991d1d 100644 --- a/vna_browser.c +++ b/vna_browser.c @@ -177,11 +177,13 @@ finish2: line[j] = 0; j = 0; char *args[16]; int nargs = parse_line(line, args, 16); // Parse line to 16 args - if (nargs < 2 || args[0][0] == '#' || args[0][0] == '!') continue; // No data or comment or settings + if (nargs < 3 || args[0][0] == '#' || args[0][0] == '!') continue; // No data or comment or settings if (count >= BANDS_MAX) {error = "Format err"; goto finish3;} setting.bands[count].start = my_atoui(args[0]);// Get frequency setting.bands[count].end = my_atoui(args[1]);// Get frequency setting.bands[count].level = my_atof(args[2]); // Get frequency + if (nargs > 3) // Name, files of older firmware have none + plot_printf(setting.bands[count].name, BAND_NAME_SIZE, "%s", args[3]); setting.bands[count].enabled = true; // Get frequency count++; } From 515c2d19cf0ba1742bb5aafeee9d9f5a135999e7 Mon Sep 17 00:00:00 2001 From: erikkaashoek Date: Mon, 2 Nov 2026 14:19:57 +0100 Subject: [PATCH 6/8] Self test: cable hint in the test list (issue #114) When test 3 found no signal the hint was drawn directly on the screen and the next redraw of the test list overwrote it (only "Did Y" remained). The waiting line of the test list now shows "Cable connected? Touch screen to continue" instead of "Touch screen to continue", so it is redrawn with the list. Test 3 itself already shows the signal level fail. Tested: on a ZS407 test 3 forced to no signal through the debugger, the hint stays visible; the tinySA BASIC self test passes all tests and ends with "Touch screen to continue". BASIC net about -4 B flash, +4 B RAM. --- sa_core.c | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/sa_core.c b/sa_core.c index 0b54688..a38f77c 100644 --- a/sa_core.c +++ b/sa_core.c @@ -6476,6 +6476,7 @@ static const char *const test_text [4] = static const char *(test_fail_cause [TEST_COUNT]); static int test_status[TEST_COUNT]; static int show_test_info = FALSE; +static const char *test_hint; // last line of the test list while waiting for a touch static volatile int test_wait = false; static float test_value; @@ -6584,7 +6585,7 @@ void cell_draw_test_info(int x0, int y0) cell_printf(xpos, ypos, FONT_s"Self test status:"); } else if (test_case[i].kind == TC_END) { if (test_wait) - cell_printf(xpos, ypos, FONT_s"Touch screen to continue"); + cell_printf(xpos, ypos, FONT_s"%s", test_hint); continue; } else { cell_printf(xpos, ypos, FONT_s"Test %d: %s%s", i+1, test_fail_cause[i], test_text[test_status[i]] ); @@ -7153,6 +7154,7 @@ void selftest(int test) test_fail_cause[i] = ""; } show_test_info = TRUE; + test_hint = "Touch screen to continue"; test_step=0; test_step = setting.test_argument; if (test_step != 0) { @@ -7170,9 +7172,7 @@ void selftest(int test) if (test_step == 2) { if (peakLevel < -60) { test_step = TEST_END; - ili9341_set_foreground(LCD_BRIGHT_COLOR_RED); - ili9341_drawstring_7x13("Signal level too low", 30, 140); - ili9341_drawstring_7x13("Did you connect high and low ports with cable?", 0, 210); + test_hint = "Cable connected? Touch screen to continue"; // drawn in the list, a direct draw was overwritten goto resume2; } From 933d510a23ed6c63dc2e3c783585a9bd3660d565 Mon Sep 17 00:00:00 2001 From: erikkaashoek Date: Mon, 2 Nov 2026 14:19:57 +0100 Subject: [PATCH 7/8] Version screen: copyright 2026, aligned thread table (issue #93) The copyright year is 2026. The thread table of the tinySA Ultra did not line up: the font is proportional and a space is narrower than a digit or letter, so padding with spaces moved the columns (e.g. "idle", priority 1). The numbers are now zero padded, the name follows them and the state is drawn at a fixed position. Tested on a ZS407. tinySA Ultra +16 B, BASIC unchanged. --- main.c | 2 +- ui.c | 8 +++++--- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/main.c b/main.c index fc8cd3b..f4cbcfb 100644 --- a/main.c +++ b/main.c @@ -96,7 +96,7 @@ uint16_t redraw_request = 0; // contains REDRAW_XXX flags // Version text, displayed in Config->Version menu, also send by info command const char * const info_about[]={ BOARD_NAME, - "2019-2024 Copyright @Erik Kaashoek", + "2019-2026 Copyright @Erik Kaashoek", "2016-2020 Copyright @edy555", "SW licensed under GPL. See: https://github.com/erikkaashoek/tinySA", "Version: " VERSION, diff --git a/ui.c b/ui.c index 41b2fea..e38ee8b 100644 --- a/ui.c +++ b/ui.c @@ -707,10 +707,12 @@ extern const char *states[]; #else uint32_t stklimit = 0U; #endif - lcd_printf(x, y+=bFONT_STR_HEIGHT, "%08x|%08x|%08x|%08x|%4u|%4u|%9s|%12s", + // The font is proportional (a space is narrower than a digit), padding with spaces does not align: + // zero padded numbers, the name after them, the state at a fixed position + lcd_printf(x, y+=bFONT_STR_HEIGHT, "%08x|%08x|%08x|%08x|%u|%03u|%s", stklimit, (uint32_t)tp->ctx.sp, max_stack_use, (uint32_t)tp, - (uint32_t)tp->refs - 1, (uint32_t)tp->prio, states[tp->state], - tp->name == NULL ? "" : tp->name); + (uint32_t)tp->refs - 1, (uint32_t)tp->prio, tp->name == NULL ? "" : tp->name); + lcd_printf(x + 330, y, "%s", states[tp->state]); tp = chRegNextThread(tp); } while (tp != NULL); #endif From 736e5d24842da4e31a903bba1c96734ec080bef4 Mon Sep 17 00:00:00 2001 From: erikkaashoek Date: Mon, 2 Nov 2026 14:20:13 +0100 Subject: [PATCH 8/8] Changelog: V1.4284 P1 issues #85 #56 #171 #97 #136 #114 #93 --- changelog.txt | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/changelog.txt b/changelog.txt index e364dc2..f3c0d0b 100644 --- a/changelog.txt +++ b/changelog.txt @@ -1,3 +1,12 @@ +V1.4284 2026-11-02 For all tinySA models +- Every tinySA has its own USB serial number (from the MCU unique ID, on the tinySA Ultra the same as its DFU serial number), so Windows can tell several tinySAs apart. Windows assigns a new COM port once +- The battery stays visible when the sweep is paused with the "pause" console command +- "ext_gain" without a value shows the current external gain +- A preset loaded from SD now also uses its RBW (Ultra only) +- Band names are stored in .bnd files and loaded from them (Ultra only) +- Self test: when there is no signal, "Cable connected?" is shown in the test list instead of a hint that was overwritten +- Version screen: copyright 2026, aligned thread table (Ultra only) + V1.4273 2026-11-02 For all tinySA Ultra models - The tinySA Ultra has its own USB IDs: VID 0x1781 PID 0x0A44 (was 0x0483 0x5740, the ST virtual COM port). Windows assigns a new COM port once, programs that look for the tinySA by its USB IDs need an update (tinySA-App accepts both)