├── host_probe.py ├── host_iq_fifo_min.py ├── hardware └── PhaseLatchMini │ ├── .DS_Store │ ├── PhaseLatchMini.pdf │ ├── production │ ├── PhaseLatchMini.zip │ ├── backups │ │ └── PhaseLatchMini_2025-10-02_18-44-56.zip │ ├── designators.csv │ ├── bom.csv │ ├── positions.csv │ └── netlist.ipc │ ├── PhaseLatchMini-backups │ ├── PhaseLatchMini-2025-10-01_215908.zip │ ├── PhaseLatchMini-2025-10-01_220539.zip │ ├── PhaseLatchMini-2025-10-02_182646.zip │ ├── PhaseLatchMini-2025-10-02_183654.zip │ ├── PhaseLatchMini-2025-10-02_184449.zip │ ├── PhaseLatchMini-2025-10-02_230442.zip │ └── PhaseLatchMini-2025-10-02_231242.zip │ ├── fabrication-toolkit-options.json │ ├── PhaseLatchMini.kicad_prl │ └── PhaseLatchMini.kicad_pro ├── STM32ADC-CDC.code-workspace ├── Inc ├── usbd_desc.h ├── usbd_cdc_if.h ├── main.h ├── iq_adc.h └── usbd_conf.h ├── src ├── stm32f1xx_it.c ├── usbd_desc.c ├── main.c ├── usbd_cdc_if.c ├── usbd_raw.c ├── usbd_conf.c └── iq_adc.c ├── .vscode └── extensions.json ├── .gitignore ├── test └── README ├── include ├── usbd_raw.h └── README ├── lib └── README ├── platformio.ini ├── host_raw_capture.py ├── host_throughput.py ├── host_tick_monitor.py ├── host_iq_live.py ├── host_diagnostics.py ├── host_test.py ├── README.md └── host_iq_fifo.py /host_probe.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /host_iq_fifo_min.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /hardware/PhaseLatchMini/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AndersBNielsen/PhaseLatchMini/HEAD/hardware/PhaseLatchMini/.DS_Store -------------------------------------------------------------------------------- /hardware/PhaseLatchMini/PhaseLatchMini.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AndersBNielsen/PhaseLatchMini/HEAD/hardware/PhaseLatchMini/PhaseLatchMini.pdf -------------------------------------------------------------------------------- /hardware/PhaseLatchMini/production/PhaseLatchMini.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AndersBNielsen/PhaseLatchMini/HEAD/hardware/PhaseLatchMini/production/PhaseLatchMini.zip -------------------------------------------------------------------------------- /hardware/PhaseLatchMini/production/backups/PhaseLatchMini_2025-10-02_18-44-56.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AndersBNielsen/PhaseLatchMini/HEAD/hardware/PhaseLatchMini/production/backups/PhaseLatchMini_2025-10-02_18-44-56.zip -------------------------------------------------------------------------------- /STM32ADC-CDC.code-workspace: -------------------------------------------------------------------------------- 1 | { 2 | "folders": [ 3 | { 4 | "name": "STM32ADC-CDC", 5 | "path": "." 6 | } 7 | ], 8 | "settings": { 9 | "files.associations": { 10 | "usbd_core.h": "c" 11 | } 12 | } 13 | } -------------------------------------------------------------------------------- /hardware/PhaseLatchMini/PhaseLatchMini-backups/PhaseLatchMini-2025-10-01_215908.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AndersBNielsen/PhaseLatchMini/HEAD/hardware/PhaseLatchMini/PhaseLatchMini-backups/PhaseLatchMini-2025-10-01_215908.zip -------------------------------------------------------------------------------- /hardware/PhaseLatchMini/PhaseLatchMini-backups/PhaseLatchMini-2025-10-01_220539.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AndersBNielsen/PhaseLatchMini/HEAD/hardware/PhaseLatchMini/PhaseLatchMini-backups/PhaseLatchMini-2025-10-01_220539.zip -------------------------------------------------------------------------------- /hardware/PhaseLatchMini/PhaseLatchMini-backups/PhaseLatchMini-2025-10-02_182646.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AndersBNielsen/PhaseLatchMini/HEAD/hardware/PhaseLatchMini/PhaseLatchMini-backups/PhaseLatchMini-2025-10-02_182646.zip -------------------------------------------------------------------------------- /hardware/PhaseLatchMini/PhaseLatchMini-backups/PhaseLatchMini-2025-10-02_183654.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AndersBNielsen/PhaseLatchMini/HEAD/hardware/PhaseLatchMini/PhaseLatchMini-backups/PhaseLatchMini-2025-10-02_183654.zip -------------------------------------------------------------------------------- /hardware/PhaseLatchMini/PhaseLatchMini-backups/PhaseLatchMini-2025-10-02_184449.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AndersBNielsen/PhaseLatchMini/HEAD/hardware/PhaseLatchMini/PhaseLatchMini-backups/PhaseLatchMini-2025-10-02_184449.zip -------------------------------------------------------------------------------- /hardware/PhaseLatchMini/PhaseLatchMini-backups/PhaseLatchMini-2025-10-02_230442.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AndersBNielsen/PhaseLatchMini/HEAD/hardware/PhaseLatchMini/PhaseLatchMini-backups/PhaseLatchMini-2025-10-02_230442.zip -------------------------------------------------------------------------------- /hardware/PhaseLatchMini/PhaseLatchMini-backups/PhaseLatchMini-2025-10-02_231242.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AndersBNielsen/PhaseLatchMini/HEAD/hardware/PhaseLatchMini/PhaseLatchMini-backups/PhaseLatchMini-2025-10-02_231242.zip -------------------------------------------------------------------------------- /hardware/PhaseLatchMini/fabrication-toolkit-options.json: -------------------------------------------------------------------------------- 1 | {"ARCHIVE_NAME": "", "EXTRA_LAYERS": "", "ALL_ACTIVE_LAYERS": false, "EXTEND_EDGE_CUT": false, "ALTERNATIVE_EDGE_CUT": false, "AUTO TRANSLATE": true, "AUTO FILL": true, "EXCLUDE DNP": false} -------------------------------------------------------------------------------- /Inc/usbd_desc.h: -------------------------------------------------------------------------------- 1 | #ifndef __USBD_DESC_H 2 | #define __USBD_DESC_H 3 | #ifdef __cplusplus 4 | extern "C" { 5 | #endif 6 | #include "usbd_def.h" 7 | extern USBD_DescriptorsTypeDef FS_Desc; 8 | #ifdef __cplusplus 9 | } 10 | #endif 11 | #endif 12 | -------------------------------------------------------------------------------- /src/stm32f1xx_it.c: -------------------------------------------------------------------------------- 1 | // Interrupt handlers for STM32F1 (minimal set needed for this project) 2 | #include "stm32f1xx_hal.h" 3 | 4 | // Provide SysTick handler (was missing) so HAL_GetTick advances. 5 | void SysTick_Handler(void) { 6 | HAL_IncTick(); 7 | HAL_SYSTICK_IRQHandler(); 8 | } 9 | -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | // See http://go.microsoft.com/fwlink/?LinkId=827846 3 | // for the documentation about the extensions.json format 4 | "recommendations": [ 5 | "platformio.platformio-ide" 6 | ], 7 | "unwantedRecommendations": [ 8 | "ms-vscode.cpptools-extension-pack" 9 | ] 10 | } 11 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .pio 2 | .vscode/.browse.c_cpp.db* 3 | .vscode/c_cpp_properties.json 4 | .vscode/launch.json 5 | .vscode/ipch 6 | hardware/_autosave* 7 | hardware/#auto_saved_files# 8 | _autosave* 9 | *-bak 10 | *-backups 11 | hardware/fp-info-cache 12 | hardware/PhaseLatchMini/fp-info-cache 13 | .DS_Store 14 | hardware/PhaseLatchMini/~PhaseLatchMini.kicad_sch.lck 15 | start.bin 16 | /__pycache__ 17 | __pycache__/* 18 | /hardware/PhaseLatchMini/production/backups -------------------------------------------------------------------------------- /test/README: -------------------------------------------------------------------------------- 1 | 2 | This directory is intended for PlatformIO Test Runner and project tests. 3 | 4 | Unit Testing is a software testing method by which individual units of 5 | source code, sets of one or more MCU program modules together with associated 6 | control data, usage procedures, and operating procedures, are tested to 7 | determine whether they are fit for use. Unit testing finds problems early 8 | in the development cycle. 9 | 10 | More information about PlatformIO Unit Testing: 11 | - https://docs.platformio.org/en/latest/advanced/unit-testing/index.html 12 | -------------------------------------------------------------------------------- /Inc/usbd_cdc_if.h: -------------------------------------------------------------------------------- 1 | #ifndef __USBD_CDC_IF_H 2 | #define __USBD_CDC_IF_H 3 | #include "usbd_cdc.h" 4 | 5 | #ifndef CDC_IN_EP 6 | #define CDC_IN_EP 0x81U 7 | #endif 8 | 9 | extern USBD_CDC_ItfTypeDef USBD_Interface_fops_FS; 10 | uint8_t CDC_Transmit_FS(uint8_t* Buf, uint16_t Len); 11 | uint8_t CDC_Stream_Active(void); 12 | uint32_t CDC_Stream_Drops(void); 13 | void CDC_Debug_ForceStart(void); 14 | #ifdef MINIMAL_CDC 15 | void CDC_Minimal_Task(void); // periodic small packet sender 16 | void CDC_Background_Poke(void); // new: unconditional background attempt & counters 17 | uint32_t CDC_Debug_TxCompletes(void); 18 | uint32_t CDC_Debug_TxAttempts(void); 19 | #endif 20 | 21 | #endif 22 | -------------------------------------------------------------------------------- /hardware/PhaseLatchMini/production/designators.csv: -------------------------------------------------------------------------------- 1 | C1:1 2 | C10:1 3 | C11:1 4 | C12:1 5 | C13:1 6 | C14:1 7 | C15:1 8 | C16:1 9 | C17:1 10 | C18:1 11 | C19:1 12 | C2:1 13 | C20:1 14 | C21:1 15 | C22:1 16 | C23:1 17 | C3:1 18 | C4:1 19 | C5:1 20 | C6:1 21 | C7:1 22 | C8:1 23 | C9:1 24 | D1:1 25 | FB1:1 26 | J1:1 27 | J2:1 28 | J20:1 29 | J21:1 30 | J3:1 31 | J4:1 32 | J5:1 33 | J6:1 34 | L1:1 35 | L2:1 36 | L3:1 37 | L4:1 38 | L5:1 39 | L6:1 40 | R1:1 41 | R10:1 42 | R11:1 43 | R12:1 44 | R13:1 45 | R14:1 46 | R15:1 47 | R17:1 48 | R18:1 49 | R19:1 50 | R2:1 51 | R20:1 52 | R3:1 53 | R4:1 54 | R5:1 55 | R6:1 56 | R7:1 57 | R8:1 58 | R9:1 59 | SW3:1 60 | U1:1 61 | U5:1 62 | Y1:1 63 | Y2:1 64 | -------------------------------------------------------------------------------- /Inc/main.h: -------------------------------------------------------------------------------- 1 | #ifndef __MAIN_H 2 | #define __MAIN_H 3 | 4 | #include "stm32f1xx_hal.h" 5 | #include 6 | 7 | #define LED_GPIO_PORT GPIOB 8 | #define LED_PIN GPIO_PIN_12 9 | 10 | void Error_Handler(void); 11 | 12 | // Exported ADC ring overflow counter 13 | extern volatile uint32_t adc_drops; 14 | 15 | // IQ chunk descriptor (32-bit words pointer + remaining word count) 16 | typedef struct { uint32_t *ptr; uint32_t words; } iq_chunk_t; 17 | 18 | #if defined(ENABLE_IQ) && (ENABLE_IQ==1) 19 | // Expose ring buffer and indices for ISR burst feeder 20 | extern volatile iq_chunk_t q[8]; 21 | extern volatile uint8_t q_head, q_tail; 22 | // Feed instrumentation counters 23 | extern volatile uint32_t feed_loop_pkts; 24 | extern volatile uint32_t feed_isr_pkts; 25 | extern volatile uint32_t feed_busy_skips; 26 | extern volatile uint32_t feed_chain_max; 27 | extern volatile uint32_t feed_chain_current; 28 | #endif 29 | 30 | #endif 31 | -------------------------------------------------------------------------------- /include/usbd_raw.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | #include "usbd_core.h" 3 | /* Raw vendor bulk class (IN 0x81, OUT 0x01) */ 4 | extern USBD_ClassTypeDef USBD_RAW; 5 | 6 | /* Simple streaming API used by ADC DMA callback to enqueue large buffers 7 | * which are then packetised into 64-byte bulk transfers on EP 0x81. 8 | * Data is sent losslessly in order (bulk guarantees reliability). */ 9 | void usbd_raw_stream_submit(const uint8_t *data, uint32_t length); 10 | void usbd_raw_stream_task(void); /* call in main loop to kick initial transfer */ 11 | 12 | extern volatile uint32_t usbd_raw_stream_queued_bytes; /* cumulative bytes accepted */ 13 | extern volatile uint32_t usbd_raw_stream_drops; /* buffers dropped due to full queue */ 14 | 15 | #ifndef USE_RAW 16 | /* Provide harmless inline stubs if raw mode excluded */ 17 | static inline void usbd_raw_stream_submit(const uint8_t *data, uint32_t length) { (void)data; (void)length; } 18 | static inline void usbd_raw_stream_task(void) {} 19 | #endif 20 | 21 | -------------------------------------------------------------------------------- /Inc/iq_adc.h: -------------------------------------------------------------------------------- 1 | #ifndef IQ_ADC_H 2 | #define IQ_ADC_H 3 | #include 4 | #include "stm32f1xx_hal.h" 5 | 6 | // Target complex sample rate (I/Q pairs per second). Adjust as needed. 7 | // NOTE: Actual timer configuration will search prescaler/period for the closest 8 | // achievable rate to this target given TIM3 input clock and 16-bit limits. 9 | #define IQ_SAMPLE_RATE_HZ 210526U 10 | #define IQ_BUFFER_PAIRS 1536U 11 | #define IQ_DMA_LENGTH (IQ_BUFFER_PAIRS*2U) 12 | 13 | /* Achieved timer/sample rate after PSC/ARR search. Set at runtime in iq_adc.c. 14 | * Exported so host/diagnostic builds can inspect or print it. */ 15 | extern volatile uint32_t iq_achieved_rate; 16 | 17 | typedef void (*iq_callback_t)(uint32_t *data, uint32_t count, uint8_t index); 18 | 19 | void iq_init(iq_callback_t cb); 20 | void iq_start(void); 21 | void iq_stop(void); 22 | 23 | extern volatile uint32_t iq_dma_half_count; 24 | extern volatile uint32_t iq_dma_full_count; 25 | 26 | static inline uint32_t iq_half_count(void) { return iq_dma_half_count; } 27 | static inline uint32_t iq_full_count(void) { return iq_dma_full_count; } 28 | 29 | #endif 30 | -------------------------------------------------------------------------------- /Inc/usbd_conf.h: -------------------------------------------------------------------------------- 1 | #ifndef __USBD_CONF_H 2 | #define __USBD_CONF_H 3 | 4 | #include "stm32f1xx_hal.h" 5 | #include 6 | #include 7 | #include 8 | 9 | #define USBD_MAX_NUM_INTERFACES 2 // CDC ACM needs Comm + Data interfaces 10 | #define USBD_MAX_NUM_CONFIGURATION 1 11 | #define USBD_MAX_STR_DESC_SIZ 64 12 | #define USBD_SUPPORT_USER_STRING 0 13 | #define USBD_SELF_POWERED 1 14 | #define USBD_DEBUG_LEVEL 0 15 | 16 | #define USBD_malloc malloc 17 | #define USBD_free free 18 | #define USBD_memset memset 19 | #define USBD_memcpy memcpy 20 | 21 | #if (USBD_DEBUG_LEVEL > 0) 22 | #define USBD_UsrLog(...) printf(__VA_ARGS__); 23 | #else 24 | #define USBD_UsrLog(...) 25 | #endif 26 | 27 | #if (USBD_DEBUG_LEVEL > 1) 28 | #define USBD_ErrLog(...) printf("ERROR: ");\ 29 | printf(__VA_ARGS__); 30 | #else 31 | #define USBD_ErrLog(...) 32 | #endif 33 | 34 | #if (USBD_DEBUG_LEVEL > 2) 35 | #define USBD_DbgLog(...) printf("DEBUG : ");\ 36 | printf(__VA_ARGS__); 37 | #else 38 | #define USBD_DbgLog(...) 39 | #endif 40 | 41 | #endif 42 | -------------------------------------------------------------------------------- /lib/README: -------------------------------------------------------------------------------- 1 | 2 | This directory is intended for project specific (private) libraries. 3 | PlatformIO will compile them to static libraries and link into the executable file. 4 | 5 | The source code of each library should be placed in a separate directory 6 | ("lib/your_library_name/[Code]"). 7 | 8 | For example, see the structure of the following example libraries `Foo` and `Bar`: 9 | 10 | |--lib 11 | | | 12 | | |--Bar 13 | | | |--docs 14 | | | |--examples 15 | | | |--src 16 | | | |- Bar.c 17 | | | |- Bar.h 18 | | | |- library.json (optional. for custom build options, etc) https://docs.platformio.org/page/librarymanager/config.html 19 | | | 20 | | |--Foo 21 | | | |- Foo.c 22 | | | |- Foo.h 23 | | | 24 | | |- README --> THIS FILE 25 | | 26 | |- platformio.ini 27 | |--src 28 | |- main.c 29 | 30 | Example contents of `src/main.c` using Foo and Bar: 31 | ``` 32 | #include 33 | #include 34 | 35 | int main (void) 36 | { 37 | ... 38 | } 39 | 40 | ``` 41 | 42 | The PlatformIO Library Dependency Finder will find automatically dependent 43 | libraries by scanning project source files. 44 | 45 | More information about PlatformIO Library Dependency Finder 46 | - https://docs.platformio.org/page/librarymanager/ldf.html 47 | -------------------------------------------------------------------------------- /hardware/PhaseLatchMini/production/bom.csv: -------------------------------------------------------------------------------- 1 | Designator,Footprint,Quantity,Value,LCSC Part # 2 | "C1, C10, C11, C16, C23, C8, C9",0402,7,100nF,C307331 3 | "C12, C13, C5, C6",0805,4,1uF,C28323 4 | "C14, C15, C20, C22",0402,4,22nF,C1532 5 | "C17, C18",0603,2,470nF,C1623 6 | "C19, C21",0402,2,220nF,C16772 7 | "C2, C3",0402,2,15pF,C1548 8 | "C4, C7",0402,2,18pF,C1549 9 | D1,0805,1,GREEN,C2297 10 | FB1,0603,1,FerriteBead_Small,C14709 11 | "J1, J3",PinHeader_1x17_P2.54mm_Vertical,2,Conn_01x17_Pin, 12 | J2,PinHeader_1x04_P2.54mm_Vertical,1,NC, 13 | J20,SMA_Samtec_SMA-J-P-H-ST-EM1_EdgeMount,1,I_IN,C5301850 14 | J21,SMA_Samtec_SMA-J-P-H-ST-EM1_EdgeMount,1,Q_IN,C5301850 15 | J4,USB_C_Receptacle_HCTL_HC-TYPE-C-16P-01A,1,USB2.0_16P,C2894897 16 | J5,PinHeader_2x03_P2.54mm_Vertical,1,Boot, 17 | J6,PinHeader_1x02_P2.54mm_Vertical,1,NC, 18 | "L1, L2, L3, L4, L5, L6",0805,6,10uH,C1046 19 | "R1, R2, R4, R7",0402,4,10k,C25744 20 | "R10, R11, R12, R5, R6, R8",0603,6,4.7,C23164 21 | "R13, R14",0402,2,5k1,C25905 22 | "R15, R9",0603,2,10,C22859 23 | "R17, R18, R19, R20",0402,4,NC,C25867 24 | R3,0402,1,1k5,C25867 25 | SW3,SW_SPST_TL3342,1,BTN,C318884 26 | U1,LQFP-48_7x7mm_P0.5mm,1,STM32F103C8Tx,C8734 27 | U5,SOT-23-5,1,MIC5504-3.3YM5,C3021093 28 | Y1,Crystal_SMD_3225-4Pin_3.2x2.5mm,1,8MHz,C5308002 29 | Y2,Crystal_SMD_3215-2Pin_3.2x1.5mm,1,32768Hz,C32346 30 | -------------------------------------------------------------------------------- /include/README: -------------------------------------------------------------------------------- 1 | 2 | This directory is intended for project header files. 3 | 4 | A header file is a file containing C declarations and macro definitions 5 | to be shared between several project source files. You request the use of a 6 | header file in your project source file (C, C++, etc) located in `src` folder 7 | by including it, with the C preprocessing directive `#include'. 8 | 9 | ```src/main.c 10 | 11 | #include "header.h" 12 | 13 | int main (void) 14 | { 15 | ... 16 | } 17 | ``` 18 | 19 | Including a header file produces the same results as copying the header file 20 | into each source file that needs it. Such copying would be time-consuming 21 | and error-prone. With a header file, the related declarations appear 22 | in only one place. If they need to be changed, they can be changed in one 23 | place, and programs that include the header file will automatically use the 24 | new version when next recompiled. The header file eliminates the labor of 25 | finding and changing all the copies as well as the risk that a failure to 26 | find one copy will result in inconsistencies within a program. 27 | 28 | In C, the convention is to give header files names that end with `.h'. 29 | 30 | Read more about using header files in official GCC documentation: 31 | 32 | * Include Syntax 33 | * Include Operation 34 | * Once-Only Headers 35 | * Computed Includes 36 | 37 | https://gcc.gnu.org/onlinedocs/cpp/Header-Files.html 38 | -------------------------------------------------------------------------------- /platformio.ini: -------------------------------------------------------------------------------- 1 | ; PlatformIO Project Configuration File 2 | ; 3 | ; Build options: build flags, source filter 4 | ; Upload options: custom upload port, speed and extra flags 5 | ; Library options: dependencies, extra library storages 6 | ; Advanced options: extra scripting 7 | ; 8 | ; Please visit documentation for the other options and examples 9 | ; https://docs.platformio.org/page/projectconf.html 10 | 11 | [env:genericSTM32F103C8] 12 | platform = ststm32 13 | board = genericSTM32F103C8 14 | framework = stm32cube 15 | build_flags = 16 | -DUSE_HAL_DRIVER 17 | -DSTM32F103xB 18 | -DHSE_VALUE=8000000 19 | -DUSB_DEVICE 20 | -DUSE_USB_FS 21 | -IInc 22 | -DMINIMAL_CDC 23 | -DENABLE_IQ=1 24 | -DUSB_TICK_DIAG=0 25 | -DTHROUGHPUT_BASELINE=1 26 | upload_protocol = stlink 27 | 28 | ; Diagnostic environment: enables tick diag and small packet debug traffic 29 | [env:genericSTM32F103C8_diag] 30 | platform = ststm32 31 | board = genericSTM32F103C8 32 | framework = stm32cube 33 | build_flags = 34 | -DUSE_HAL_DRIVER 35 | -DSTM32F103xB 36 | -DHSE_VALUE=8000000 37 | -DUSB_DEVICE 38 | -DUSE_USB_FS 39 | -IInc 40 | -DMINIMAL_CDC 41 | -DENABLE_IQ=0 42 | -DUSB_TICK_DIAG=1 43 | -DTHROUGHPUT_BASELINE=0 44 | upload_protocol = stlink 45 | 46 | ; Future ADC streaming environment (placeholder) 47 | [env:genericSTM32F103C8_adc] 48 | platform = ststm32 49 | board = genericSTM32F103C8 50 | framework = stm32cube 51 | build_flags = 52 | -DUSE_HAL_DRIVER 53 | -DSTM32F103xB 54 | -DHSE_VALUE=8000000 55 | -DUSB_DEVICE 56 | -DUSE_USB_FS 57 | -IInc 58 | -DMINIMAL_CDC 59 | -DENABLE_IQ=1 60 | -DUSB_TICK_DIAG=0 61 | -DTHROUGHPUT_BASELINE=1 62 | upload_protocol = stlink 63 | 64 | ; ADC smoke test environment (text-only periodic stats, no ramp streaming) 65 | [env:genericSTM32F103C8_adc_smoke] 66 | platform = ststm32 67 | board = genericSTM32F103C8 68 | framework = stm32cube 69 | build_flags = 70 | -DUSE_HAL_DRIVER 71 | -DSTM32F103xB 72 | -DHSE_VALUE=8000000 73 | -DUSB_DEVICE 74 | -DUSE_USB_FS 75 | -IInc 76 | -DMINIMAL_CDC 77 | -DENABLE_IQ=1 78 | -DUSB_TICK_DIAG=0 79 | -DTHROUGHPUT_BASELINE=1 80 | -DADC_SMOKE=1 81 | upload_protocol = stlink 82 | -------------------------------------------------------------------------------- /hardware/PhaseLatchMini/production/positions.csv: -------------------------------------------------------------------------------- 1 | Designator,Mid X,Mid Y,Rotation,Layer 2 | C1,187.537411,-68.748589,45.0,top 3 | C10,177.206589,-70.951411,45.0,top 4 | C11,190.25,-77.06,315.0,top 5 | C12,154.7622,-67.0814,90.0,top 6 | C13,154.7368,-80.1878,270.0,top 7 | C14,154.2024,-64.516,90.0,top 8 | C15,154.181301,-69.682377,90.0,top 9 | C16,149.9336,-72.7964,180.0,top 10 | C17,156.197,-64.494513,180.0,top 11 | C18,156.1722,-83.0,180.0,top 12 | C19,157.8356,-71.5264,270.0,top 13 | C2,193.802,-68.453,270.0,top 14 | C20,154.2034,-82.804,270.0,top 15 | C21,157.8356,-75.8952,90.0,top 16 | C22,149.9616,-74.3204,180.0,top 17 | C23,154.2034,-77.5716,270.0,top 18 | C3,193.802,-71.12,90.0,top 19 | C4,193.195,-74.93,0.0,top 20 | C5,198.3232,-72.0344,0.0,top 21 | C6,196.85,-78.161333,180.0,top 22 | C7,194.691,-72.997,270.0,top 23 | C8,186.860822,-68.028178,45.0,top 24 | C9,183.035,-79.375,0.0,top 25 | D1,161.29,-78.9432,180.0,top 26 | FB1,198.117159,-70.042413,180.0,top 27 | J1,182.879999,-82.55,270.0,top 28 | J2,174.244,-73.66,0.0,top 29 | J20,151.13,-68.041,180.0,top 30 | J21,151.13,-79.220998,180.0,top 31 | J3,182.879998,-64.77,270.0,top 32 | J4,205.74,-73.66,90.0,top 33 | J5,168.91,-73.66,0.0,top 34 | J6,201.93,-67.31,270.0,top 35 | L1,158.3436,-67.0095,270.0,top 36 | L2,156.8704,-80.4035,90.0,top 37 | L3,156.6164,-67.0095,270.0,top 38 | L4,158.6484,-80.3699,90.0,top 39 | L5,156.540998,-69.773002,180.0,top 40 | L6,156.464,-77.6224,180.0,top 41 | R1,189.15,-78.11,135.0,top 42 | R10,159.1686,-83.0,0.0,top 43 | R11,155.9052,-75.7936,180.0,top 44 | R12,152.4508,-74.3204,0.0,top 45 | R13,200.2536,-78.161333,90.0,top 46 | R14,206.0,-66.06,180.0,top 47 | R15,155.434962,-74.336485,180.0,top 48 | R2,172.2,-71.950001,270.0,top 49 | R3,180.487376,-78.887376,315.0,top 50 | R4,159.8168,-68.326,90.0,top 51 | R5,159.195,-64.494513,0.0,top 52 | R6,155.895945,-71.414376,180.0,top 53 | R7,160.1216,-80.820801,90.0,top 54 | R8,152.4386,-72.7964,0.0,top 55 | R9,155.416205,-72.868696,180.0,top 56 | SW3,161.29,-73.66,90.0,top 57 | U1,183.006998,-73.433445,135.0,top 58 | U5,198.18316,-75.122413,90.0,top 59 | Y1,191.35,-69.736,90.0,top 60 | Y2,192.151,-73.279,0.0,top 61 | -------------------------------------------------------------------------------- /hardware/PhaseLatchMini/PhaseLatchMini.kicad_prl: -------------------------------------------------------------------------------- 1 | { 2 | "board": { 3 | "active_layer": 0, 4 | "active_layer_preset": "", 5 | "auto_track_width": true, 6 | "hidden_netclasses": [], 7 | "hidden_nets": [], 8 | "high_contrast_mode": 0, 9 | "net_color_mode": 1, 10 | "opacity": { 11 | "images": 0.6, 12 | "pads": 1.0, 13 | "shapes": 1.0, 14 | "tracks": 1.0, 15 | "vias": 1.0, 16 | "zones": 0.6 17 | }, 18 | "selection_filter": { 19 | "dimensions": true, 20 | "footprints": true, 21 | "graphics": true, 22 | "keepouts": true, 23 | "lockedItems": false, 24 | "otherItems": true, 25 | "pads": true, 26 | "text": true, 27 | "tracks": true, 28 | "vias": true, 29 | "zones": true 30 | }, 31 | "visible_items": [ 32 | "vias", 33 | "footprint_text", 34 | "footprint_anchors", 35 | "ratsnest", 36 | "grid", 37 | "footprints_front", 38 | "footprints_back", 39 | "footprint_values", 40 | "footprint_references", 41 | "tracks", 42 | "drc_errors", 43 | "drawing_sheet", 44 | "bitmaps", 45 | "pads", 46 | "zones", 47 | "drc_warnings", 48 | "drc_exclusions", 49 | "locked_item_shadows", 50 | "conflict_shadows", 51 | "shapes" 52 | ], 53 | "visible_layers": "ffffffff_ffffffff_ffffffff_ffffffff", 54 | "zone_display_mode": 0 55 | }, 56 | "git": { 57 | "repo_type": "", 58 | "repo_username": "", 59 | "ssh_key": "" 60 | }, 61 | "meta": { 62 | "filename": "PhaseLatchMini.kicad_prl", 63 | "version": 5 64 | }, 65 | "net_inspector_panel": { 66 | "col_hidden": [ 67 | false, 68 | false, 69 | false, 70 | false, 71 | false, 72 | false, 73 | false, 74 | false, 75 | false, 76 | false, 77 | false, 78 | false 79 | ], 80 | "col_order": [ 81 | 0, 82 | 1, 83 | 2, 84 | 3, 85 | 4, 86 | 5, 87 | 6, 88 | 7, 89 | 8, 90 | 9, 91 | 10, 92 | 11 93 | ], 94 | "col_widths": [ 95 | 156, 96 | 141, 97 | 103, 98 | 71, 99 | 103, 100 | 103, 101 | 103, 102 | 74, 103 | 103, 104 | 103, 105 | 103, 106 | 0 107 | ], 108 | "custom_group_rules": [], 109 | "expanded_rows": [], 110 | "filter_by_net_name": true, 111 | "filter_by_netclass": true, 112 | "filter_text": "", 113 | "group_by_constraint": false, 114 | "group_by_netclass": false, 115 | "show_unconnected_nets": false, 116 | "show_zero_pad_nets": false, 117 | "sort_ascending": true, 118 | "sorting_column": 0 119 | }, 120 | "open_jobsets": [], 121 | "project": { 122 | "files": [] 123 | }, 124 | "schematic": { 125 | "selection_filter": { 126 | "graphics": true, 127 | "images": true, 128 | "labels": true, 129 | "lockedItems": false, 130 | "otherItems": true, 131 | "pins": true, 132 | "symbols": true, 133 | "text": true, 134 | "wires": true 135 | } 136 | } 137 | } 138 | -------------------------------------------------------------------------------- /src/usbd_desc.c: -------------------------------------------------------------------------------- 1 | #include "usbd_desc.h" 2 | #include "usbd_core.h" 3 | #include "usbd_conf.h" 4 | #include "usbd_cdc.h" 5 | #include "usbd_cdc_if.h" // for CDC_IN_EP 6 | 7 | #define USBD_VID 0x0483 8 | #define USBD_PID 0x5741 /* changed to force driver rebind for CDC */ 9 | #define USBD_LANGID_STRING 1033 10 | // Short strings only (keep minimal) 11 | #define USBD_MANUFACTURER_STRING (uint8_t*)"F103" 12 | #define USBD_PRODUCT_FS_STRING (uint8_t*)"CDC IQ STREAM" 13 | #define USBD_SERIALNUMBER_STRING (uint8_t*)"12345678" 14 | #define USBD_CONFIGURATION_FS_STRING (uint8_t*)"CFG" 15 | #define USBD_INTERFACE_FS_STRING (uint8_t*)"IF" 16 | 17 | static uint8_t * USBD_FS_DeviceDescriptor(USBD_SpeedTypeDef speed, uint16_t *length); 18 | static uint8_t * USBD_FS_LangIDStrDescriptor(USBD_SpeedTypeDef speed, uint16_t *length); 19 | static uint8_t * USBD_FS_ManufacturerStrDescriptor(USBD_SpeedTypeDef speed, uint16_t *length); 20 | static uint8_t * USBD_FS_ProductStrDescriptor(USBD_SpeedTypeDef speed, uint16_t *length); 21 | static uint8_t * USBD_FS_ConfigStrDescriptor(USBD_SpeedTypeDef speed, uint16_t *length); 22 | static uint8_t * USBD_FS_InterfaceStrDescriptor(USBD_SpeedTypeDef speed, uint16_t *length); 23 | static uint8_t * USBD_FS_SerialStrDescriptor(USBD_SpeedTypeDef speed, uint16_t *length); 24 | 25 | USBD_DescriptorsTypeDef FS_Desc = { 26 | USBD_FS_DeviceDescriptor, 27 | USBD_FS_LangIDStrDescriptor, 28 | USBD_FS_ManufacturerStrDescriptor, 29 | USBD_FS_ProductStrDescriptor, 30 | USBD_FS_SerialStrDescriptor, 31 | USBD_FS_ConfigStrDescriptor, 32 | USBD_FS_InterfaceStrDescriptor 33 | }; 34 | 35 | __ALIGN_BEGIN static uint8_t hUSBDDeviceDesc[USB_LEN_DEV_DESC] __ALIGN_END = { 36 | 0x12, USB_DESC_TYPE_DEVICE, 37 | 0x00, 0x02, 38 | 0x00, 0x00, 0x00, 39 | 0x40, 40 | LOBYTE(USBD_VID), HIBYTE(USBD_VID), 41 | LOBYTE(USBD_PID), HIBYTE(USBD_PID), 42 | 0x00, 0x02, 43 | 1, 2, 3, 44 | 1 45 | }; 46 | 47 | __ALIGN_BEGIN static uint8_t USBD_LangIDDesc[USB_LEN_LANGID_STR_DESC] __ALIGN_END = { 48 | USB_LEN_LANGID_STR_DESC, USB_DESC_TYPE_STRING, 49 | LOBYTE(USBD_LANGID_STRING), HIBYTE(USBD_LANGID_STRING) 50 | }; 51 | 52 | static uint8_t* USBD_StringDescriptor(const uint8_t* str, uint16_t* length) { 53 | static uint8_t desc[64]; 54 | uint8_t idx = 0; 55 | if (str) { 56 | uint8_t len = 0; 57 | while (str[len] && len < 31) len++; 58 | desc[idx++] = (len * 2) + 2; 59 | desc[idx++] = USB_DESC_TYPE_STRING; 60 | for (uint8_t i = 0; i < len; i++) { 61 | desc[idx++] = str[i]; 62 | desc[idx++] = 0; 63 | } 64 | } else { 65 | desc[0] = 2; 66 | desc[1] = USB_DESC_TYPE_STRING; 67 | } 68 | *length = desc[0]; 69 | return desc; 70 | } 71 | 72 | static uint8_t * USBD_FS_DeviceDescriptor(USBD_SpeedTypeDef speed, uint16_t *length) { 73 | (void)speed; *length = sizeof(hUSBDDeviceDesc); 74 | // No descriptor debug injection in minimal trimmed build 75 | return hUSBDDeviceDesc; 76 | } 77 | static uint8_t * USBD_FS_LangIDStrDescriptor(USBD_SpeedTypeDef speed, uint16_t *length) { 78 | (void)speed; *length = sizeof(USBD_LangIDDesc); return USBD_LangIDDesc; 79 | } 80 | static uint8_t * USBD_FS_ManufacturerStrDescriptor(USBD_SpeedTypeDef speed, uint16_t *length) { 81 | return USBD_StringDescriptor(USBD_MANUFACTURER_STRING, length); 82 | } 83 | static uint8_t * USBD_FS_ProductStrDescriptor(USBD_SpeedTypeDef speed, uint16_t *length) { 84 | return USBD_StringDescriptor(USBD_PRODUCT_FS_STRING, length); 85 | } 86 | static uint8_t * USBD_FS_ConfigStrDescriptor(USBD_SpeedTypeDef speed, uint16_t *length) { 87 | return USBD_StringDescriptor(USBD_CONFIGURATION_FS_STRING, length); 88 | } 89 | static uint8_t * USBD_FS_InterfaceStrDescriptor(USBD_SpeedTypeDef speed, uint16_t *length) { 90 | return USBD_StringDescriptor(USBD_INTERFACE_FS_STRING, length); 91 | } 92 | static uint8_t * USBD_FS_SerialStrDescriptor(USBD_SpeedTypeDef speed, uint16_t *length) { 93 | return USBD_StringDescriptor(USBD_SERIALNUMBER_STRING, length); 94 | } 95 | -------------------------------------------------------------------------------- /host_raw_capture.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | """ 3 | Simple bulk-IN reader using PyUSB for the raw vendor device. 4 | Finds device (default VID=0x0483 PID=0x5750), claims interface 0, reads endpoint 0x81 and writes raw bytes to a file or stdout. 5 | 6 | Usage: 7 | pip install pyusb 8 | python host_raw_capture.py --outfile samples.iq --seconds 10 9 | 10 | List devices: 11 | python host_raw_capture.py --list [--vid 0x0483] 12 | """ 13 | import argparse 14 | import sys 15 | import time 16 | import usb.core 17 | import usb.util 18 | import signal 19 | 20 | # Suppress BrokenPipe noisy traceback when piping to head/tail 21 | def _silent_broken_pipe_hook(exctype, value, tb): 22 | if exctype is BrokenPipeError: 23 | try: 24 | sys.stderr.flush() 25 | except Exception: 26 | pass 27 | return 28 | # Fall back to default for other exceptions 29 | sys.__excepthook__(exctype, value, tb) 30 | 31 | sys.excepthook = _silent_broken_pipe_hook 32 | 33 | def _handle_sigpipe(signum, frame): 34 | # Immediate quiet exit on SIGPIPE 35 | try: 36 | sys.exit(0) 37 | except SystemExit: 38 | raise 39 | 40 | signal.signal(signal.SIGPIPE, _handle_sigpipe) 41 | 42 | EP_IN = 0x81 43 | 44 | parser = argparse.ArgumentParser() 45 | parser.add_argument('--vid', type=lambda x: int(x,0), default=0x0483, help='Vendor ID (default 0x0483)') 46 | parser.add_argument('--pid', type=lambda x: int(x,0), default=0x5741, help='Product ID (default 0x5750)') 47 | parser.add_argument('--list', action='store_true', help='List matching devices and exit') 48 | parser.add_argument('--outfile', '-o', default=None, help='Write raw bytes to this file (default: stdout)') 49 | parser.add_argument('--seconds', '-s', type=float, default=10.0, help='How long to record (seconds)') 50 | parser.add_argument('--timeout', '-t', type=int, default=1000, help='USB read timeout (ms)') 51 | parser.add_argument('--chunk', '-c', type=int, default=64, help='Read chunk size in bytes') 52 | parser.add_argument('--quiet', '-q', action='store_true', help='Suppress banner and final status (useful for pipelines)') 53 | args = parser.parse_args() 54 | 55 | vid, pid = args.vid, args.pid 56 | 57 | if args.list: 58 | devs = list(usb.core.find(find_all=True, idVendor=vid)) if args.vid else list(usb.core.find(find_all=True)) 59 | if not devs: 60 | print('No devices found for VID filter' if args.vid else 'No USB devices found (unexpected)') 61 | sys.exit(0) 62 | for d in devs: 63 | try: 64 | print('Device: VID=0x%04X PID=0x%04X bDeviceClass=0x%02X' % (d.idVendor, d.idProduct, d.bDeviceClass)) 65 | except Exception as e: 66 | print('Device (error reading attrs):', e) 67 | sys.exit(0) 68 | 69 | dev = usb.core.find(idVendor=vid, idProduct=pid) 70 | if dev is None: 71 | print('Device not found: VID=0x%04X PID=0x%04X' % (vid, pid), file=sys.stderr) 72 | sys.exit(1) 73 | 74 | # Detach kernel driver if necessary 75 | try: 76 | if dev.is_kernel_driver_active(0): 77 | dev.detach_kernel_driver(0) 78 | except Exception: 79 | pass 80 | 81 | dev.set_configuration() 82 | cfg = dev.get_active_configuration() 83 | intf = cfg[(0,0)] 84 | 85 | # Claim interface 0 86 | usb.util.claim_interface(dev, intf.bInterfaceNumber) 87 | 88 | out = sys.stdout.buffer if args.outfile is None else open(args.outfile, 'wb') 89 | end = time.time() + args.seconds 90 | if not args.quiet: 91 | print('Starting capture (VID=0x%04X PID=0x%04X), writing to' % (vid, pid), 'stdout' if args.outfile is None else args.outfile, file=sys.stderr) 92 | try: 93 | while time.time() < end: 94 | try: 95 | data = dev.read(EP_IN, args.chunk, timeout=args.timeout) 96 | out.write(bytes(data)) 97 | try: 98 | out.flush() 99 | except BrokenPipeError: 100 | # Upstream pipe closed (e.g., head exited); stop cleanly 101 | break 102 | except usb.core.USBError as e: 103 | if e.errno == 110 or 'timed out' in str(e).lower(): 104 | continue 105 | else: 106 | print('USBError', e, file=sys.stderr) 107 | break 108 | finally: 109 | try: 110 | usb.util.release_interface(dev, intf.bInterfaceNumber) 111 | except Exception: 112 | pass 113 | if args.outfile is not None: 114 | out.close() 115 | 116 | if not args.quiet: 117 | print('Done', file=sys.stderr) 118 | -------------------------------------------------------------------------------- /host_throughput.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | """ 3 | High-rate bulk-IN throughput measurement tool for the raw vendor class device. 4 | 5 | Measures: 6 | - Sustained bytes/s and samples/s (assuming 32-bit words = I(16)|Q(16)) 7 | - Packet rate (using read chunk size, default 4096 = multiples of 64) 8 | - Detects stalls (no data for --stall seconds) and alignment issues 9 | - Optionally writes raw stream to a file (e.g. for further processing) 10 | 11 | Usage: 12 | pip install pyusb 13 | python host_throughput.py --seconds 5 --chunk 4096 14 | python host_throughput.py --outfile dump.bin --seconds 10 15 | 16 | Notes: 17 | Full-speed USB theoretical max for bulk ~ 1.216 MB/s (19 * 64B * 1000 frames) 18 | Practical with Python user space may be slightly lower (~1.0-1.1 MB/s). 19 | """ 20 | import usb.core, usb.util, time, argparse, sys, statistics 21 | 22 | EP_IN = 0x81 23 | 24 | ap = argparse.ArgumentParser() 25 | ap.add_argument('--vid', type=lambda x:int(x,0), default=0x0483) 26 | ap.add_argument('--pid', type=lambda x:int(x,0), default=0x5741) 27 | ap.add_argument('--seconds', type=float, default=5.0) 28 | ap.add_argument('--chunk', type=int, default=4096, help='Host read size (multiple of 64 recommended)') 29 | ap.add_argument('--timeout', type=int, default=1000, help='ms read timeout') 30 | ap.add_argument('--outfile', '-o') 31 | ap.add_argument('--stall', type=float, default=1.0, help='Stall detection threshold seconds (0=disable)') 32 | ap.add_argument('--progress', action='store_true') 33 | ap.add_argument('--no-store', action='store_true') 34 | args = ap.parse_args() 35 | 36 | dev = usb.core.find(idVendor=args.vid, idProduct=args.pid) 37 | if not dev: 38 | print('Device not found', file=sys.stderr); sys.exit(1) 39 | try: 40 | if dev.is_kernel_driver_active(0): 41 | dev.detach_kernel_driver(0) 42 | except Exception: 43 | pass 44 | 45 | dev.set_configuration() 46 | cfg = dev.get_active_configuration() 47 | intf = cfg[(0,0)] 48 | usb.util.claim_interface(dev, intf.bInterfaceNumber) 49 | 50 | store = bytearray() if (args.outfile and not args.no_store) else None 51 | outf = open(args.outfile,'wb') if args.outfile else None 52 | 53 | start = time.time(); end = start + args.seconds 54 | last_data = start 55 | packet_sizes = [] 56 | bytes_total = 0 57 | packets = 0 58 | last_print = start 59 | PRINT_IVL = 0.25 60 | 61 | try: 62 | while True: 63 | now = time.time() 64 | if now >= end: break 65 | try: 66 | data = dev.read(EP_IN, args.chunk, timeout=args.timeout) 67 | except usb.core.USBError as e: 68 | if 'timed out' in str(e).lower(): 69 | if args.stall and (time.time()-last_data) > args.stall: 70 | print('\n[STALL] No data for %.2fs' % (time.time()-last_data)) 71 | break 72 | continue 73 | else: 74 | print('USBError', e, file=sys.stderr) 75 | break 76 | if not data: 77 | continue 78 | b = bytes(data) 79 | l = len(b) 80 | bytes_total += l 81 | packets += 1 82 | last_data = now 83 | packet_sizes.append(l) 84 | if outf: outf.write(b) 85 | if store is not None: store.extend(b) 86 | if args.progress and (now - last_print) >= PRINT_IVL: 87 | dt = now - start 88 | rate = bytes_total / dt 89 | sys.stdout.write('\r%8.1f KB/s %7.1f kIQ/s pkts=%6d avgPkt=%.0f' % (rate/1024, (rate/4)/1000, packets, (sum(packet_sizes)/len(packet_sizes)) if packet_sizes else 0)) 90 | sys.stdout.flush() 91 | last_print = now 92 | finally: 93 | if outf: outf.close() 94 | try: usb.util.release_interface(dev, intf.bInterfaceNumber) 95 | except Exception: pass 96 | 97 | dt = time.time() - start 98 | if args.progress: 99 | print() 100 | if dt <= 0: dt = 1e-6 101 | rate = bytes_total / dt 102 | print('Duration: %.3fs Bytes: %d Rate: %.1f KB/s (%.1f kIQ/s)' % (dt, bytes_total, rate/1024, (rate/4)/1000)) 103 | if packet_sizes: 104 | print('Packets: %d Mean size: %.1f Median: %.1f Min: %d Max: %d' % (len(packet_sizes), statistics.mean(packet_sizes), statistics.median(packet_sizes), min(packet_sizes), max(packet_sizes))) 105 | 106 | if store is not None and store: 107 | # Alignment check 108 | if (len(store) % 4) != 0: 109 | print('WARNING: total bytes not multiple of 4 (trailing=%d)' % (len(store)%4)) 110 | else: 111 | print('Alignment OK (multiple of 4 bytes)') 112 | -------------------------------------------------------------------------------- /src/main.c: -------------------------------------------------------------------------------- 1 | // Minimal IQ streaming firmware: sends only raw ADC-packed words over CDC, no text. 2 | #include "main.h" 3 | #include "usbd_core.h" 4 | #include "usbd_desc.h" 5 | #include "usbd_cdc.h" 6 | #include "usbd_cdc_if.h" 7 | #include "usbd_def.h" 8 | #if ENABLE_IQ 9 | #include "iq_adc.h" 10 | #endif 11 | #include 12 | 13 | USBD_HandleTypeDef hUsbDeviceFS; 14 | static void SystemClock_Config(void); 15 | static void MX_GPIO_Init(void); 16 | static void MX_USB_DEVICE_Init(void); 17 | static void Quiet_Unused_Pins(void); 18 | 19 | // Shared counters / stubs referenced by other modules (always provided) 20 | volatile uint32_t feed_isr_pkts=0, feed_chain_max=0, feed_chain_current=0; 21 | void dma_led_toggle(void){ HAL_GPIO_TogglePin(LED_GPIO_PORT, LED_PIN); } 22 | #if ENABLE_IQ 23 | volatile iq_chunk_t q[8]; 24 | volatile uint8_t q_head=0, q_tail=0; 25 | volatile uint32_t adc_drops=0; 26 | volatile uint32_t feed_loop_pkts=0, feed_busy_skips=0; 27 | static void iq_cb(uint32_t *data, uint32_t count, uint8_t index){(void)index;uint8_t next=(q_head+1)&7; if(next==q_tail){adc_drops++;return;} q[q_head].ptr=data; q[q_head].words=count; q_head=next;} 28 | #endif 29 | 30 | int main(void){ 31 | HAL_Init(); SystemClock_Config(); Quiet_Unused_Pins(); MX_GPIO_Init(); MX_USB_DEVICE_Init(); 32 | #if ENABLE_IQ 33 | iq_init(iq_cb); 34 | // Defer ADC start until USB configured (word boundary stability). Timeout fallback to avoid deadlock. 35 | uint32_t wait_start = HAL_GetTick(); 36 | while(hUsbDeviceFS.dev_state != USBD_STATE_CONFIGURED) { 37 | if(HAL_GetTick() - wait_start > 2000) break; // 2s fallback 38 | } 39 | iq_start(); 40 | #endif 41 | static uint32_t last_led=0, spin=0; extern volatile uint8_t ep1_busy_flag; while(1){ 42 | uint32_t now=HAL_GetTick(); 43 | if(now==last_led){ if(++spin>500000){ HAL_GPIO_TogglePin(LED_GPIO_PORT,LED_PIN); spin=0; } } 44 | else if(now-last_led>=300){ HAL_GPIO_TogglePin(LED_GPIO_PORT,LED_PIN); last_led=now; spin=0; } 45 | if(hUsbDeviceFS.dev_state==USBD_STATE_CONFIGURED){ 46 | #if ENABLE_IQ 47 | while(ep1_busy_flag==0){ 48 | if(q_tail==q_head) break; // no data 49 | iq_chunk_t c=q[q_tail]; 50 | uint32_t bytes=c.words*4U; if(bytes>64) bytes=64; // 64-byte USB FS packet 51 | ep1_busy_flag=1; 52 | USBD_LL_Transmit(&hUsbDeviceFS, CDC_IN_EP,(uint8_t*)c.ptr, bytes); 53 | c.ptr+=bytes/4U; c.words-=bytes/4U; 54 | if(c.words==0) q_tail=(q_tail+1)&7; else q[q_tail]=c; 55 | feed_loop_pkts++; 56 | break; 57 | } 58 | if(ep1_busy_flag && q_tail!=q_head) feed_busy_skips++; 59 | #endif 60 | } 61 | } 62 | } 63 | 64 | static void SystemClock_Config(void){ 65 | RCC_OscInitTypeDef RCC_OscInitStruct={0}; RCC_ClkInitTypeDef RCC_ClkInitStruct={0}; 66 | RCC_OscInitStruct.OscillatorType=RCC_OSCILLATORTYPE_HSE|RCC_OSCILLATORTYPE_HSI; 67 | RCC_OscInitStruct.HSEState=RCC_HSE_ON; RCC_OscInitStruct.HSEPredivValue=RCC_HSE_PREDIV_DIV1; RCC_OscInitStruct.HSIState=RCC_HSI_ON; 68 | RCC_OscInitStruct.PLL.PLLState=RCC_PLL_ON; RCC_OscInitStruct.PLL.PLLSource=RCC_PLLSOURCE_HSE; RCC_OscInitStruct.PLL.PLLMUL=RCC_PLL_MUL9; 69 | if(HAL_RCC_OscConfig(&RCC_OscInitStruct)!=HAL_OK) Error_Handler(); 70 | RCC_ClkInitStruct.ClockType=RCC_CLOCKTYPE_HCLK|RCC_CLOCKTYPE_SYSCLK|RCC_CLOCKTYPE_PCLK1|RCC_CLOCKTYPE_PCLK2; 71 | RCC_ClkInitStruct.SYSCLKSource=RCC_SYSCLKSOURCE_PLLCLK; RCC_ClkInitStruct.AHBCLKDivider=RCC_SYSCLK_DIV1; RCC_ClkInitStruct.APB1CLKDivider=RCC_HCLK_DIV2; RCC_ClkInitStruct.APB2CLKDivider=RCC_HCLK_DIV1; 72 | if(HAL_RCC_ClockConfig(&RCC_ClkInitStruct, FLASH_LATENCY_2)!=HAL_OK) Error_Handler(); __HAL_RCC_USB_CLK_ENABLE(); 73 | } 74 | 75 | static void MX_GPIO_Init(void){ 76 | __HAL_RCC_GPIOC_CLK_ENABLE(); 77 | GPIO_InitTypeDef GPIO_InitStruct={0}; 78 | GPIO_InitStruct.Pin=LED_PIN; GPIO_InitStruct.Mode=GPIO_MODE_OUTPUT_PP; GPIO_InitStruct.Speed=GPIO_SPEED_FREQ_LOW; HAL_GPIO_Init(LED_GPIO_PORT,&GPIO_InitStruct); 79 | HAL_GPIO_WritePin(LED_GPIO_PORT, LED_PIN, GPIO_PIN_RESET); 80 | } 81 | 82 | static void Quiet_Unused_Pins(void){ /* Optionally configure unused pins as analog (implementation omitted for brevity) */ } 83 | 84 | static void MX_USB_DEVICE_Init(void){ 85 | /* Full-speed device init (speed param = 0). */ 86 | USBD_Init(&hUsbDeviceFS, &FS_Desc, 0); USBD_RegisterClass(&hUsbDeviceFS, &USBD_CDC); USBD_CDC_RegisterInterface(&hUsbDeviceFS, &USBD_Interface_fops_FS); USBD_Start(&hUsbDeviceFS); 87 | } 88 | 89 | void Error_Handler(void){ __disable_irq(); while(1){ HAL_GPIO_TogglePin(LED_GPIO_PORT,LED_PIN); HAL_Delay(100);} } 90 | 91 | #ifdef USE_FULL_ASSERT 92 | void assert_failed(uint8_t *file, uint32_t line){ (void)file; (void)line; } 93 | #endif 94 | -------------------------------------------------------------------------------- /host_tick_monitor.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | """ 3 | USB CDC Tick Monitor 4 | 5 | Purpose: 6 | - Open the STM32 CDC serial port 7 | - Capture and parse lines containing: 8 | EARLYTICK DT= 9 | TICK= 10 | HB ... (optional heartbeat line) 11 | - Compute: 12 | * Whether EARLYTICK delta > 0 (SysTick running early) 13 | * Live delta between successive TICK reports 14 | * Warn if TICK does not advance for a timeout window 15 | - Provide a rolling summary every N seconds. 16 | 17 | Usage: 18 | python host_tick_monitor.py --port /dev/tty.usbmodemXXXX --baud 115200 19 | (Baud is ignored by CDC but kept for compatibility.) 20 | 21 | Exit codes: 22 | 0 success / normal user exit (Ctrl+C) 23 | 2 no data received before timeout 24 | 25 | """ 26 | import argparse, sys, time, re, glob 27 | try: 28 | import serial # pyserial 29 | except ImportError: 30 | print("ERROR: pyserial not installed: pip install pyserial", file=sys.stderr) 31 | sys.exit(1) 32 | 33 | EARLY_RE = re.compile(r"EARLYTICK\s+(\d+)\s+(\d+)\s+DT=(\d+)") 34 | TICK_RE = re.compile(r"TICK=(\d+)") 35 | HB_RE = re.compile(r"HB ") 36 | 37 | 38 | def auto_detect_port(): 39 | cands = glob.glob('/dev/tty.usbmodem*') + glob.glob('/dev/tty.usbserial*') 40 | return cands[0] if cands else None 41 | 42 | 43 | def parse_args(): 44 | p = argparse.ArgumentParser(description="Monitor HAL_GetTick output from device") 45 | p.add_argument('--port', help='Serial port (auto-detect if omitted)') 46 | p.add_argument('--baud', type=int, default=115200) 47 | p.add_argument('--timeout', type=float, default=10.0, help='Overall idle timeout (s)') 48 | p.add_argument('--stuck-secs', type=float, default=3.0, help='Seconds of no tick advance before warning') 49 | p.add_argument('--summary-interval', type=float, default=5.0, help='Seconds between summary lines') 50 | return p.parse_args() 51 | 52 | 53 | def main(): 54 | args = parse_args() 55 | port = args.port or auto_detect_port() 56 | if not port: 57 | print("ERROR: No serial port specified and auto-detect failed", file=sys.stderr) 58 | sys.exit(2) 59 | print(f"Opening {port} ...") 60 | ser = serial.Serial(port, args.baud, timeout=0.2) 61 | 62 | first_data_time = None 63 | last_any_time = time.time() 64 | last_tick_val = None 65 | last_tick_recv_time = None 66 | early_report = None 67 | stuck_reported = False 68 | next_summary = time.time() + args.summary_interval 69 | 70 | try: 71 | while True: 72 | line = ser.readline() 73 | now_host = time.time() 74 | if line: 75 | if first_data_time is None: 76 | first_data_time = now_host 77 | last_any_time = now_host 78 | try: 79 | s = line.decode('utf-8', errors='replace').strip() 80 | except Exception: 81 | continue 82 | if not s: 83 | continue 84 | print(s) 85 | m = EARLY_RE.match(s) 86 | if m: 87 | t0, t1, dt = map(int, m.groups()) 88 | early_report = (t0, t1, dt) 89 | if dt == 0: 90 | print("WARNING: EARLY SysTick delta == 0 (SysTick not incrementing early)") 91 | else: 92 | print(f"INFO: EARLY SysTick delta {dt} (ok)") 93 | m = TICK_RE.match(s) 94 | if m: 95 | tick = int(m.group(1)) 96 | if last_tick_val is not None and tick != last_tick_val: 97 | interval = now_host - last_tick_recv_time if last_tick_recv_time else 0 98 | # Basic estimation of tick frequency if interval reasonable 99 | if interval > 0: 100 | tick_diff = tick - last_tick_val 101 | hz_est = tick_diff / interval 102 | print(f"TICK_ADV tick_diff={tick_diff} interval={interval:.3f}s est={hz_est:.1f}Hz") 103 | stuck_reported = False 104 | last_tick_val = tick 105 | last_tick_recv_time = now_host 106 | # Stuck detection 107 | if last_tick_val is not None and (now_host - last_tick_recv_time) > args.stuck_secs and not stuck_reported: 108 | print(f"WARNING: No TICK advance for {args.stuck_secs} seconds (possible SysTick halt)") 109 | stuck_reported = True 110 | else: 111 | # No line this poll; check idle timeout 112 | if (time.time() - last_any_time) > args.timeout: 113 | print("ERROR: No data within timeout window") 114 | sys.exit(2) 115 | if time.time() >= next_summary: 116 | etxt = "none" 117 | if early_report: 118 | etxt = f"t0={early_report[0]} t1={early_report[1]} dt={early_report[2]}" 119 | ticktxt = f"last_tick={last_tick_val}" if last_tick_val is not None else "no_tick" 120 | print(f"SUMMARY early:{etxt} {ticktxt}") 121 | next_summary = time.time() + args.summary_interval 122 | except KeyboardInterrupt: 123 | print("Exiting on user request") 124 | sys.exit(0) 125 | 126 | if __name__ == '__main__': 127 | main() 128 | -------------------------------------------------------------------------------- /src/usbd_cdc_if.c: -------------------------------------------------------------------------------- 1 | // Minimal CDC test driver only 2 | #include "usbd_cdc_if.h" 3 | #include "usbd_desc.h" 4 | #include "usbd_def.h" 5 | #include "usbd_cdc.h" 6 | #include "main.h" 7 | #include "usbd_conf.h" 8 | #include "stm32f1xx.h" 9 | #include 10 | 11 | extern USBD_HandleTypeDef hUsbDeviceFS; 12 | 13 | static int8_t CDC_Init_FS(void); 14 | static int8_t CDC_DeInit_FS(void); 15 | static int8_t CDC_Control_FS(uint8_t cmd, uint8_t* pbuf, uint16_t length); 16 | static int8_t CDC_Receive_FS(uint8_t* pbuf, uint32_t *Len); 17 | 18 | USBD_CDC_ItfTypeDef USBD_Interface_fops_FS = { 19 | CDC_Init_FS, 20 | CDC_DeInit_FS, 21 | CDC_Control_FS, 22 | CDC_Receive_FS 23 | }; 24 | 25 | static uint8_t UserRxBufferFS[64]; 26 | static uint8_t UserTxBufferFS[64]; 27 | 28 | // Diagnostics counters 29 | static volatile uint32_t tx_attempts=0, tx_busy_returns=0, tx_completes=0, tx_watchdog_clears=0; 30 | static volatile uint32_t tiny_packets_sent=0; 31 | static volatile uint32_t auto_attempts=0; // background scheduled attempts 32 | static volatile uint32_t force_attempts=0; // low-level USBD_LL_Transmit attempts 33 | static volatile uint8_t stream_on = 1; // force streaming by default for debug 34 | static volatile uint8_t dtr_set = 1; // treat as always set (remove gating for debug) 35 | // Fallback class data block (used if pClassData never populated by stack) 36 | static USBD_CDC_HandleTypeDef cdc_fallback; 37 | static uint8_t cdc_fallback_active = 0; 38 | static volatile uint32_t txstate_observed = 0; 39 | static volatile uint32_t direct_attempts = 0; 40 | static volatile uint32_t frame_attempts = 0; 41 | static volatile uint32_t frame_ok = 0; 42 | static volatile uint32_t frame_busy = 0; 43 | static volatile uint32_t loop_heart = 0; // increments each CDC_Minimal_Task invocation 44 | extern volatile uint32_t ll_datain_count; // from usbd_conf.c 45 | static uint32_t last_stat_ms=0; 46 | // Forward decl for manual DataIn hook 47 | void USBD_CDC_DataIn(USBD_HandleTypeDef *pdev, uint8_t epnum); 48 | 49 | // Raw low-level transmit probe (bypasses CDC state machine) for debugging 50 | static void raw_in_ep_probe(void) { 51 | // Disabled: never inject probe bytes into the IN stream. Keep CDC strictly binary. 52 | (void)0; 53 | } 54 | 55 | static int8_t CDC_Init_FS(void) { 56 | USBD_CDC_SetTxBuffer(&hUsbDeviceFS, UserTxBufferFS, 0); 57 | USBD_CDC_SetRxBuffer(&hUsbDeviceFS, UserRxBufferFS); 58 | USBD_CDC_ReceivePacket(&hUsbDeviceFS); 59 | // Initial ASCII banner intentionally removed to avoid any non-binary bytes 60 | // being emitted on the CDC IN endpoint. Keep the stream strictly binary. 61 | // If core didn't allocate class data yet, point it to our static block 62 | if(hUsbDeviceFS.pClassData == NULL) { 63 | memset(&cdc_fallback,0,sizeof(cdc_fallback)); 64 | hUsbDeviceFS.pClassData = &cdc_fallback; 65 | cdc_fallback_active = 1; 66 | } 67 | return (USBD_OK); 68 | } 69 | static int8_t CDC_DeInit_FS(void) { return (USBD_OK); } 70 | static int8_t CDC_Control_FS(uint8_t cmd, uint8_t* pbuf, uint16_t length) { 71 | (void)pbuf; (void)length; (void)cmd; return (USBD_OK); 72 | } 73 | 74 | uint8_t CDC_Transmit_FS(uint8_t* Buf, uint16_t Len) { 75 | USBD_CDC_HandleTypeDef *hcdc = (USBD_CDC_HandleTypeDef*)hUsbDeviceFS.pClassData; 76 | tx_attempts++; 77 | if(!hcdc) { tx_busy_returns++; return USBD_BUSY; } 78 | if(hcdc->TxState) { tx_busy_returns++; return USBD_BUSY; } 79 | USBD_CDC_SetTxBuffer(&hUsbDeviceFS, Buf, Len); 80 | return USBD_CDC_TransmitPacket(&hUsbDeviceFS); 81 | } 82 | 83 | static int8_t CDC_Receive_FS(uint8_t* Buf, uint32_t *Len) { 84 | // Pure streaming mode: ignore all incoming data and emit NO textual responses. 85 | // This guarantees the bulk IN stream remains strictly packed IQ words without ASCII headers. 86 | (void)Buf; (void)Len; USBD_CDC_ReceivePacket(&hUsbDeviceFS); return (USBD_OK); 87 | } 88 | 89 | // Attempt to ensure we catch completion regardless of actual weak symbol naming. 90 | // Some ST stacks use USBD_CDC_DataIn, others route via class callback table. 91 | void USBD_CDC_DataIn(USBD_HandleTypeDef *pdev, uint8_t epnum) { 92 | // Call default core handler if present (not accessible here unless weak linked) 93 | if(pdev->pClassData) { 94 | USBD_CDC_HandleTypeDef *hcdc = (USBD_CDC_HandleTypeDef*)pdev->pClassData; 95 | hcdc->TxState = 0U; 96 | } 97 | tx_completes++; 98 | HAL_GPIO_TogglePin(LED_GPIO_PORT, LED_PIN); 99 | } 100 | 101 | // Helper to let main query tx_completes without tight coupling 102 | uint32_t CDC_Debug_TxCompletes(void){ return tx_completes; } 103 | uint32_t CDC_Debug_TxAttempts(void){ return tx_attempts; } 104 | 105 | // Polling task: aggressive fill with 64B frames; every 500ms emit STAT line 106 | void CDC_Minimal_Task(void) { 107 | // Fully disabled in pure IQ streaming build to avoid ANY non-sample bytes. 108 | return; 109 | } 110 | 111 | // Unconditional background poke: increments auto_attempts every call, tries a direct CDC_Transmit_FS 112 | void CDC_Background_Poke(void) { /* disabled */ } 113 | 114 | // Stubs kept for link compatibility 115 | uint8_t CDC_Stream_Active(void){return 0;} 116 | uint32_t CDC_Stream_Drops(void){return 0;} 117 | void CDC_Debug_ForceStart(void){} 118 | void CDC_Stream_Poll(void){} 119 | void CDC_Debug_SendSerialState(void){} 120 | 121 | 122 | 123 | // Hook invoked from low-level DataIn callback (usbd_conf.c) to ensure tx_completes advances 124 | // even if the normal USBD_CDC_DataIn path is bypassed or not wired. 125 | void cdc_force_tx_complete_hook(void) { tx_completes++; } 126 | 127 | 128 | -------------------------------------------------------------------------------- /src/usbd_raw.c: -------------------------------------------------------------------------------- 1 | /* Raw USB vendor class with simple queued streaming support. 2 | * Provides bulk IN (0x81) and OUT (0x01). A small ring buffer of pending 3 | * application data blocks is packetised into 64-byte USB transfers. 4 | * 5 | * This file is only active when USE_RAW==1. Otherwise it compiles to an 6 | * empty translation unit; stub inline functions live in the header. */ 7 | 8 | #if !(defined(USE_RAW) && (USE_RAW==1)) 9 | /* Raw mode disabled */ 10 | #else 11 | #include "usbd_core.h" 12 | #include "usbd_ctlreq.h" 13 | #include "usbd_def.h" 14 | #include "usbd_raw.h" 15 | #include "usbd_conf.h" 16 | 17 | /* Forward declarations */ 18 | static uint8_t USBD_RAW_Init(USBD_HandleTypeDef *pdev, uint8_t cfgidx); 19 | static uint8_t USBD_RAW_DeInit(USBD_HandleTypeDef *pdev, uint8_t cfgidx); 20 | static uint8_t USBD_RAW_Setup(USBD_HandleTypeDef *pdev, USBD_SetupReqTypedef *req); 21 | static uint8_t USBD_RAW_DataIn(USBD_HandleTypeDef *pdev, uint8_t epnum); 22 | static uint8_t USBD_RAW_DataOut(USBD_HandleTypeDef *pdev, uint8_t epnum); 23 | static uint8_t *USBD_RAW_GetFSCfgDesc(uint16_t *length); 24 | 25 | USBD_ClassTypeDef USBD_RAW = { 26 | USBD_RAW_Init, 27 | USBD_RAW_DeInit, 28 | USBD_RAW_Setup, 29 | NULL, 30 | NULL, 31 | USBD_RAW_DataIn, 32 | USBD_RAW_DataOut, 33 | NULL, 34 | NULL, 35 | NULL, 36 | USBD_RAW_GetFSCfgDesc, 37 | }; 38 | 39 | /* Total length = 9 (config) + 9 (interface) + 7 (EP OUT) + 7 (EP IN) = 32 (0x20) */ 40 | __ALIGN_BEGIN static uint8_t USBD_RAW_CfgDesc[] __ALIGN_END = { 41 | /* Configuration Descriptor */ 42 | 0x09, USB_DESC_TYPE_CONFIGURATION, 0x20, 0x00, 0x01, 0x01, 0x00, 0x80, 0x32, 43 | /* Interface Descriptor (bInterfaceClass=0xFF vendor) */ 44 | 0x09, USB_DESC_TYPE_INTERFACE, 0x00, 0x00, 0x02, 0xFF, 0x00, 0x00, 0x00, 45 | /* Endpoint OUT (Bulk, wMaxPacketSize=64) */ 46 | 0x07, USB_DESC_TYPE_ENDPOINT, 0x01, 0x02, 0x40, 0x00, 0x00, 47 | /* Endpoint IN (Bulk, wMaxPacketSize=64) */ 48 | 0x07, USB_DESC_TYPE_ENDPOINT, 0x81, 0x02, 0x40, 0x00, 0x00, 49 | }; 50 | 51 | static uint8_t raw_out_buf[64]; 52 | 53 | /* Stream queue: holds pointers to buffers supplied by producer (e.g. ADC DMA half/full) */ 54 | typedef struct { const uint8_t *ptr; uint32_t len; uint32_t off; } raw_seg_t; 55 | #define RAW_Q_DEPTH 8 56 | static volatile raw_seg_t raw_q[RAW_Q_DEPTH]; 57 | static volatile uint8_t raw_q_head = 0; /* next write */ 58 | static volatile uint8_t raw_q_tail = 0; /* next send */ 59 | volatile uint32_t usbd_raw_stream_queued_bytes = 0; 60 | volatile uint32_t usbd_raw_stream_drops = 0; 61 | 62 | static inline uint8_t raw_q_next(uint8_t i){ return (uint8_t)((i+1U) & (RAW_Q_DEPTH-1U)); } 63 | static int raw_q_full(void){ return raw_q_next(raw_q_head) == raw_q_tail; } 64 | static int raw_q_empty(void){ return raw_q_head == raw_q_tail; } 65 | 66 | /* Called by application (interrupt context allowed) to enqueue a contiguous buffer. 67 | * Buffer must remain valid until fully transmitted (e.g. DMA circular region). */ 68 | void usbd_raw_stream_submit(const uint8_t *data, uint32_t length) { 69 | if(length==0) return; 70 | uint32_t primask = __get_PRIMASK(); __disable_irq(); 71 | if(raw_q_full()) { usbd_raw_stream_drops++; if(!primask) __enable_irq(); return; } 72 | raw_q[raw_q_head].ptr = data; 73 | raw_q[raw_q_head].len = length; 74 | raw_q[raw_q_head].off = 0; 75 | raw_q_head = raw_q_next(raw_q_head); 76 | usbd_raw_stream_queued_bytes += length; 77 | if(!primask) __enable_irq(); 78 | } 79 | 80 | /* Internal: attempt to start a transfer if endpoint idle */ 81 | static void raw_try_tx(USBD_HandleTypeDef *pdev) { 82 | if(pdev->dev_state != USBD_STATE_CONFIGURED) return; 83 | if(raw_q_empty()) return; 84 | /* Check low-level endpoint busy (Tx FIFO) via core handle state: rely on driver pacing 85 | * We opportunistically call USBD_LL_Transmit; if stack returns USBD_BUSY we abort. */ 86 | const raw_seg_t *seg = (const raw_seg_t*)&raw_q[raw_q_tail]; 87 | uint32_t remaining = seg->len - seg->off; 88 | uint16_t pkt = (remaining > 64U)? 64U : (uint16_t)remaining; 89 | if(pkt==0) return; /* should not happen */ 90 | if(USBD_LL_Transmit(pdev, 0x81, (uint8_t*)(seg->ptr + seg->off), pkt) == USBD_OK) { 91 | /* Advance offset only when DataIn callback fires (to avoid double send on early re-entry) */ 92 | } 93 | } 94 | 95 | /* Public task (call from main loop) to ensure at least one transfer is in flight */ 96 | void usbd_raw_stream_task(void) { 97 | extern USBD_HandleTypeDef hUsbDeviceFS; /* defined in main.c */ 98 | raw_try_tx(&hUsbDeviceFS); 99 | } 100 | 101 | /* Configuration state flag visible to main.c */ 102 | volatile uint8_t usbd_raw_configured = 0; 103 | 104 | static uint8_t USBD_RAW_Init(USBD_HandleTypeDef *pdev, uint8_t cfgidx) { 105 | (void)cfgidx; 106 | /* Mark configured for main loop */ 107 | usbd_raw_configured = 1; 108 | USBD_LL_OpenEP(pdev, 0x01, USBD_EP_TYPE_BULK, 0x40); 109 | USBD_LL_OpenEP(pdev, 0x81, USBD_EP_TYPE_BULK, 0x40); 110 | USBD_LL_PrepareReceive(pdev, 0x01, raw_out_buf, sizeof(raw_out_buf)); 111 | /* Kick any pending queue */ 112 | raw_try_tx(pdev); 113 | return USBD_OK; 114 | } 115 | 116 | static uint8_t USBD_RAW_DeInit(USBD_HandleTypeDef *pdev, uint8_t cfgidx) { 117 | (void)cfgidx; 118 | usbd_raw_configured = 0; 119 | USBD_LL_CloseEP(pdev, 0x01); 120 | USBD_LL_CloseEP(pdev, 0x81); 121 | return USBD_OK; 122 | } 123 | 124 | static uint8_t USBD_RAW_Setup(USBD_HandleTypeDef *pdev, USBD_SetupReqTypedef *req) { 125 | (void)pdev; (void)req; return USBD_OK; 126 | } 127 | 128 | static uint8_t USBD_RAW_DataIn(USBD_HandleTypeDef *pdev, uint8_t epnum) { 129 | if((epnum & 0x7F) == 0x01) { /* EP1 IN complete */ 130 | if(!raw_q_empty()) { 131 | /* Advance segment */ 132 | raw_seg_t *seg = (raw_seg_t*)&raw_q[raw_q_tail]; 133 | uint32_t remaining = seg->len - seg->off; 134 | uint32_t step = (remaining > 64U)? 64U : remaining; 135 | seg->off += step; 136 | remaining -= step; 137 | if(remaining == 0) { 138 | /* Pop */ 139 | raw_q_tail = raw_q_next(raw_q_tail); 140 | } 141 | } 142 | /* Start next if available */ 143 | raw_try_tx(pdev); 144 | } 145 | return USBD_OK; 146 | } 147 | 148 | static uint8_t USBD_RAW_DataOut(USBD_HandleTypeDef *pdev, uint8_t epnum) { 149 | (void)epnum; 150 | USBD_LL_PrepareReceive(pdev, 0x01, raw_out_buf, sizeof(raw_out_buf)); 151 | return USBD_OK; 152 | } 153 | 154 | static uint8_t *USBD_RAW_GetFSCfgDesc(uint16_t *length) { 155 | *length = sizeof(USBD_RAW_CfgDesc); 156 | return USBD_RAW_CfgDesc; 157 | } 158 | 159 | #endif /* USE_RAW active */ 160 | -------------------------------------------------------------------------------- /host_iq_live.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | """ 3 | Live I/Q viewer for the STM32 ADC streaming build. 4 | 5 | Assumptions: 6 | - Device enumerates as USB CDC (/dev/tty.usbmodem*) 7 | - Stream consists primarily of packed 32-bit little-endian words. 8 | - Each 32-bit word encodes I (lower 12 bits) and Q (next 12 bits) as: 9 | bits 11..0 : I sample (0..4095) 10 | bits 23..12 : Q sample (0..4095) 11 | (upper bits 31..24 ignored / may contain sequence or noise) 12 | - No textual STAT lines are mixed in; if present, textual lines are skipped. 13 | 14 | Features: 15 | - Smooth, low-CPU terminal updating with recent samples. 16 | - Optional running min/max tracking per channel. 17 | - Simple ASCII sparkline (coarse) if enabled. 18 | - Graceful handling of partial frames and stray ASCII. 19 | 20 | Usage examples: 21 | python host_iq_live.py # auto-detect port, show I/Q stream 22 | python host_iq_live.py --port /dev/tty.usbmodem14101 --rate 500 23 | python host_iq_live.py --spark --window 64 24 | 25 | Press Ctrl+C to exit. 26 | """ 27 | import argparse, glob, sys, time, math, os, re 28 | 29 | try: 30 | import serial # pyserial 31 | except ImportError: 32 | print("ERROR: pyserial not installed: pip install pyserial", file=sys.stderr) 33 | sys.exit(2) 34 | 35 | TEXT_LINE_RE = re.compile(rb'STAT |\r?\n') # crude detector for text interruptions 36 | 37 | ASCII_THRESHOLD = 0.92 # if >92% bytes in a block are printable, treat as text noise 38 | 39 | SPARK_CHARS = "▁▂▃▄▅▆▇█" 40 | 41 | 42 | def find_port(explicit=None): 43 | if explicit: 44 | return explicit 45 | ports = sorted(glob.glob('/dev/tty.usbmodem*')) 46 | if not ports: 47 | raise SystemExit("No /dev/tty.usbmodem* device found") 48 | return ports[0] 49 | 50 | 51 | def spark(values): 52 | if not values: 53 | return '' 54 | lo = min(values); hi = max(values) 55 | if hi == lo: 56 | return SPARK_CHARS[0] * len(values) 57 | span = hi - lo 58 | out = [] 59 | for v in values: 60 | idx = int((v - lo) / span * (len(SPARK_CHARS)-1) + 1e-9) 61 | if idx < 0: idx = 0 62 | if idx >= len(SPARK_CHARS): idx = len(SPARK_CHARS)-1 63 | out.append(SPARK_CHARS[idx]) 64 | return ''.join(out) 65 | 66 | 67 | def decode_words(buf): 68 | # Return list of (i,q) from a bytes-like object length multiple of 4 69 | out = [] 70 | for off in range(0, len(buf), 4): 71 | w = buf[off] | (buf[off+1] << 8) | (buf[off+2] << 16) | (buf[off+3] << 24) 72 | i = w & 0x0FFF 73 | q = (w >> 12) & 0x0FFF 74 | out.append((i,q)) 75 | return out 76 | 77 | 78 | def main(): 79 | ap = argparse.ArgumentParser(description='Live I/Q 12-bit sample viewer (CDC serial)') 80 | ap.add_argument('--port', help='Explicit serial port path') 81 | ap.add_argument('--baud', type=int, default=115200) # ignored by USB but keeps API uniform 82 | ap.add_argument('--rate', type=float, default=20.0, help='UI refresh rate (Hz)') 83 | ap.add_argument('--window', type=int, default=32, help='Number of recent samples to display') 84 | ap.add_argument('--spark', action='store_true', help='Show ASCII sparkline per channel') 85 | ap.add_argument('--stats', action='store_true', help='Show running min/max/avg') 86 | ap.add_argument('--raw-dump', action='store_true', help='Also print raw hex words (debug)') 87 | ap.add_argument('--timeout', type=float, default=0.01, help='Serial read timeout seconds') 88 | args = ap.parse_args() 89 | 90 | port = find_port(args.port) 91 | ser = serial.Serial(port, args.baud, timeout=args.timeout) 92 | 93 | recent_i = [] 94 | recent_q = [] 95 | min_i = 4095; max_i = 0; sum_i = 0; count_i = 0 96 | min_q = 4095; max_q = 0; sum_q = 0; count_q = 0 97 | 98 | next_ui = time.time() 99 | ui_interval = 1.0 / max(args.rate, 1e-3) 100 | 101 | partial = b'' 102 | 103 | try: 104 | while True: 105 | chunk = ser.read(512) 106 | if chunk: 107 | # Filter out any ASCII STAT lines or stray text: skip if mostly printable 108 | printable = sum(32 <= b < 127 for b in chunk) 109 | if printable / len(chunk) > ASCII_THRESHOLD and b'STAT ' in chunk: 110 | continue 111 | partial += chunk 112 | # Align to 4-byte boundary 113 | if len(partial) < 4: 114 | continue 115 | cut = len(partial) - (len(partial) % 4) 116 | block = partial[:cut] 117 | partial = partial[cut:] 118 | words = decode_words(block) 119 | for i,q in words: 120 | recent_i.append(i); recent_q.append(q) 121 | min_i = min(min_i, i); max_i = max(max_i, i); sum_i += i; count_i += 1 122 | min_q = min(min_q, q); max_q = max(max_q, q); sum_q += q; count_q += 1 123 | # Trim windows 124 | if len(recent_i) > args.window: 125 | recent_i = recent_i[-args.window:] 126 | recent_q = recent_q[-args.window:] 127 | if args.raw_dump: 128 | for (i,q) in words: 129 | print(f"{i:03X}:{q:03X}") 130 | now = time.time() 131 | if now >= next_ui: 132 | next_ui = now + ui_interval 133 | # Prepare display 134 | if recent_i: 135 | avg_i = sum_i / max(count_i,1) 136 | avg_q = sum_q / max(count_q,1) 137 | line1 = f"I last[{len(recent_i):02d}]: " + ' '.join(f"{v:03X}" for v in recent_i[-args.window:]) 138 | line2 = f"Q last[{len(recent_q):02d}]: " + ' '.join(f"{v:03X}" for v in recent_q[-args.window:]) 139 | else: 140 | line1 = 'I: (no data)'; line2 = 'Q: (no data)' 141 | lines = [f"Port {port} Win={args.window} Refresh={args.rate}Hz"] 142 | lines += [line1, line2] 143 | if args.spark and recent_i: 144 | lines.append('I spark: ' + spark(recent_i)) 145 | lines.append('Q spark: ' + spark(recent_q)) 146 | if args.stats and count_i: 147 | # Window (recent) averages 148 | wavg_i = sum(recent_i)/len(recent_i) 149 | wavg_q = sum(recent_q)/len(recent_q) 150 | lines.append(f"I min={min_i:03X} max={max_i:03X} avg(run)={avg_i:05.1f} avg(win)={wavg_i:05.1f}") 151 | lines.append(f"Q min={min_q:03X} max={max_q:03X} avg(run)={avg_q:05.1f} avg(win)={wavg_q:05.1f}") 152 | # Clear screen region (simple approach) 153 | sys.stdout.write('\x1b[2J\x1b[H') 154 | sys.stdout.write('\n'.join(lines) + '\n') 155 | sys.stdout.flush() 156 | except KeyboardInterrupt: 157 | pass 158 | finally: 159 | ser.close() 160 | 161 | if __name__ == '__main__': 162 | main() 163 | -------------------------------------------------------------------------------- /host_diagnostics.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | """ 3 | Host diagnostics tool for STM32 CDC / RAW USB streaming. 4 | 5 | Features: 6 | - Auto-detect /dev/tty.usbmodem* port 7 | - Passive capture (banner / heartbeat sniff) 8 | - STATS sampler (two spaced reads + delta parsing) 9 | - Continuous monitor with periodic automatic STATS injection 10 | - Throughput test (byte rate over interval) 11 | - USB descriptor / interface summary (pyusb, optional) 12 | 13 | Usage examples: 14 | python host_diagnostics.py capture --secs 3 15 | python host_diagnostics.py stats 16 | python host_diagnostics.py monitor --stats-interval 1.0 17 | python host_diagnostics.py throughput --secs 5 18 | python host_diagnostics.py usbinfo 19 | 20 | Exits non‑zero on fatal errors (no port, etc.). 21 | """ 22 | import argparse, glob, sys, time, re, json 23 | 24 | try: 25 | import serial # pyserial 26 | except ImportError: 27 | print("ERROR: pyserial not installed: pip install pyserial", file=sys.stderr) 28 | sys.exit(2) 29 | 30 | def find_port(explicit=None): 31 | if explicit: 32 | return explicit 33 | ports = sorted(glob.glob('/dev/tty.usbmodem*')) 34 | if not ports: 35 | raise SystemExit("No /dev/tty.usbmodem* device found") 36 | return ports[0] 37 | 38 | STAT_RE = re.compile(r'STAT\s+TA=(\d+)\s+TB=(\d+)\s+TC=(\d+)\s+WD=(\d+)\s+P=(\d+).*?LH=(\d+)\s+E1=(\d+)\s+EB=(\d+)(?:\s+FBF=(\d+))?') 39 | 40 | FIELDS = ['TA','TB','TC','WD','P','LH','E1','EB','FBF'] 41 | 42 | def parse_stat(line): 43 | m = STAT_RE.search(line) 44 | if not m: 45 | return None 46 | groups = list(m.groups()) 47 | if groups[-1] is None: 48 | groups[-1] = '0' # default FBF if absent 49 | return {k:int(v) for k,v in zip(FIELDS, groups)} 50 | 51 | def cmd_capture(args): 52 | port = find_port(args.port) 53 | ser = serial.Serial(port, args.baud, timeout=0.05) 54 | end = time.time()+args.secs 55 | buf=[] 56 | while time.time()0: hints.append('Endpoint IN completions occurring (hardware active).') 91 | else: hints.append('No EP1 completions detected (E1 static).') 92 | if delta['TC']>0: hints.append('CDC completion hook firing (TC advancing).') 93 | else: hints.append('CDC completion hook NOT firing (TC static).') 94 | if delta['TA']==0: hints.append('No new CDC transmit attempts (TA static).') 95 | if delta['LH']==0: hints.append('Loop heart static - task not running or blocked.') 96 | if s2.get('FBF',0)==1: hints.append('Fallback class data in use.') 97 | print("Hints:") 98 | for h in hints: print(" -", h) 99 | 100 | 101 | def cmd_monitor(args): 102 | port = find_port(args.port) 103 | ser = serial.Serial(port, args.baud, timeout=0.05) 104 | next_stats = time.time()+args.stats_interval if args.stats_interval>0 else None 105 | try: 106 | while True: 107 | d=ser.read(512) 108 | if d: 109 | sys.stdout.write(d.decode(errors='ignore')) 110 | sys.stdout.flush() 111 | if next_stats and time.time()>=next_stats: 112 | ser.write(b'STATS\n') 113 | time.sleep(0.12) 114 | resp=ser.read(800).decode(errors='ignore') 115 | print(resp.strip()) 116 | next_stats=time.time()+args.stats_interval 117 | except KeyboardInterrupt: 118 | pass 119 | finally: 120 | ser.close() 121 | 122 | 123 | def cmd_throughput(args): 124 | port = find_port(args.port) 125 | ser = serial.Serial(port, args.baud, timeout=0) 126 | start=time.time(); end=start+args.secs 127 | count=0 128 | while time.time()Instance==USB) { 11 | __HAL_RCC_USB_CLK_ENABLE(); 12 | // USB uses PA11 (DM) PA12 (DP) - default alt config after reset is fine 13 | HAL_NVIC_SetPriority(USB_LP_CAN1_RX0_IRQn, 3, 0); 14 | HAL_NVIC_EnableIRQ(USB_LP_CAN1_RX0_IRQn); 15 | } 16 | } 17 | 18 | void HAL_PCD_MspDeInit(PCD_HandleTypeDef* pcdHandle) { 19 | if(pcdHandle->Instance==USB) { 20 | __HAL_RCC_USB_CLK_DISABLE(); 21 | HAL_NVIC_DisableIRQ(USB_LP_CAN1_RX0_IRQn); 22 | } 23 | } 24 | 25 | void USB_LP_CAN1_RX0_IRQHandler(void) { 26 | HAL_PCD_IRQHandler(&hpcd_USB_FS); 27 | } 28 | 29 | volatile uint32_t ll_datain_count = 0; // counts HAL_PCD_DataInStageCallback for any IN EP 30 | volatile uint32_t ep1_in_irqs = 0; // counts EP1 IN completions specifically 31 | volatile uint8_t ep1_busy_flag = 0; // cleared on EP1 IN complete 32 | #include "main.h" // for LED pin (if defined) 33 | 34 | USBD_StatusTypeDef USBD_LL_Init(USBD_HandleTypeDef *pdev) { 35 | hpcd_USB_FS.Instance = USB; 36 | hpcd_USB_FS.Init.dev_endpoints = 8; 37 | hpcd_USB_FS.Init.speed = PCD_SPEED_FULL; 38 | hpcd_USB_FS.Init.ep0_mps = 0x40; // restore EP0 MPS 64 bytes (standard for FS) 39 | hpcd_USB_FS.Init.phy_itface = PCD_PHY_EMBEDDED; 40 | hpcd_USB_FS.pData = pdev; 41 | pdev->pData = &hpcd_USB_FS; 42 | if (HAL_PCD_Init(&hpcd_USB_FS) != HAL_OK) { 43 | return USBD_FAIL; 44 | } 45 | // BTABLE at 0x00, start endpoint buffers at 0x40 on 32-byte boundaries 46 | // EP0 OUT (0x00) RX, EP0 IN (0x80) TX, EP1 OUT (0x01), EP1 IN (0x81), EP2 IN (0x82) 47 | HAL_PCDEx_PMAConfig(&hpcd_USB_FS, 0x00, PCD_SNG_BUF, 0x40); // EP0 OUT 48 | HAL_PCDEx_PMAConfig(&hpcd_USB_FS, 0x80, PCD_SNG_BUF, 0x80); // EP0 IN 49 | HAL_PCDEx_PMAConfig(&hpcd_USB_FS, 0x01, PCD_SNG_BUF, 0xC0); // CDC OUT 50 | HAL_PCDEx_PMAConfig(&hpcd_USB_FS, 0x81, PCD_SNG_BUF, 0x100); // CDC IN (default) 51 | HAL_PCDEx_PMAConfig(&hpcd_USB_FS, 0x82, PCD_SNG_BUF, 0x140); // CDC CMD (INT IN) 52 | return USBD_OK; 53 | } 54 | 55 | USBD_StatusTypeDef USBD_LL_DeInit(USBD_HandleTypeDef *pdev) { 56 | HAL_PCD_DeInit(pdev->pData); 57 | return USBD_OK; 58 | } 59 | 60 | USBD_StatusTypeDef USBD_LL_Start(USBD_HandleTypeDef *pdev) { 61 | HAL_PCD_Start(pdev->pData); 62 | return USBD_OK; 63 | } 64 | 65 | USBD_StatusTypeDef USBD_LL_Stop(USBD_HandleTypeDef *pdev) { 66 | HAL_PCD_Stop(pdev->pData); 67 | return USBD_OK; 68 | } 69 | 70 | USBD_StatusTypeDef USBD_LL_OpenEP(USBD_HandleTypeDef *pdev, uint8_t ep_addr, uint8_t ep_type, uint16_t ep_mps) { 71 | HAL_PCD_EP_Open(pdev->pData, ep_addr, ep_mps, ep_type); 72 | return USBD_OK; 73 | } 74 | 75 | USBD_StatusTypeDef USBD_LL_CloseEP(USBD_HandleTypeDef *pdev, uint8_t ep_addr) { 76 | HAL_PCD_EP_Close(pdev->pData, ep_addr); 77 | return USBD_OK; 78 | } 79 | 80 | USBD_StatusTypeDef USBD_LL_FlushEP(USBD_HandleTypeDef *pdev, uint8_t ep_addr) { 81 | HAL_PCD_EP_Flush(pdev->pData, ep_addr); 82 | return USBD_OK; 83 | } 84 | 85 | USBD_StatusTypeDef USBD_LL_StallEP(USBD_HandleTypeDef *pdev, uint8_t ep_addr) { 86 | HAL_PCD_EP_SetStall(pdev->pData, ep_addr); 87 | return USBD_OK; 88 | } 89 | 90 | USBD_StatusTypeDef USBD_LL_ClearStallEP(USBD_HandleTypeDef *pdev, uint8_t ep_addr) { 91 | HAL_PCD_EP_ClrStall(pdev->pData, ep_addr); 92 | return USBD_OK; 93 | } 94 | 95 | uint8_t USBD_LL_IsStallEP(USBD_HandleTypeDef *pdev, uint8_t ep_addr) { 96 | PCD_HandleTypeDef *hpcd = pdev->pData; 97 | if ((ep_addr & 0x80) == 0x80) 98 | return hpcd->IN_ep[ep_addr & 0x7F].is_stall; 99 | else 100 | return hpcd->OUT_ep[ep_addr & 0x7F].is_stall; 101 | } 102 | 103 | USBD_StatusTypeDef USBD_LL_SetUSBAddress(USBD_HandleTypeDef *pdev, uint8_t dev_addr) { 104 | HAL_PCD_SetAddress(pdev->pData, dev_addr); 105 | return USBD_OK; 106 | } 107 | 108 | 109 | uint32_t USBD_LL_GetRxDataSize(USBD_HandleTypeDef *pdev, uint8_t ep_addr) { 110 | return HAL_PCD_EP_GetRxCount(pdev->pData, ep_addr); 111 | } 112 | 113 | void HAL_PCD_SetupStageCallback(PCD_HandleTypeDef *hpcd) { 114 | USBD_LL_SetupStage(hpcd->pData, (uint8_t *)hpcd->Setup); 115 | } 116 | void HAL_PCD_DataOutStageCallback(PCD_HandleTypeDef *hpcd, uint8_t epnum) { 117 | USBD_LL_DataOutStage(hpcd->pData, epnum, hpcd->OUT_ep[epnum].xfer_buff); 118 | } 119 | void HAL_PCD_DataInStageCallback(PCD_HandleTypeDef *hpcd, uint8_t epnum) { 120 | if(epnum == 1) { 121 | ll_datain_count++; ep1_in_irqs++; ep1_busy_flag = 0; 122 | // Force-clear CDC TxState and increment tx_completes if class data present 123 | USBD_HandleTypeDef *pdev = (USBD_HandleTypeDef*)hpcd->pData; 124 | if(pdev && pdev->pClassData) { 125 | USBD_CDC_HandleTypeDef *hcdc = (USBD_CDC_HandleTypeDef*)pdev->pClassData; 126 | if(hcdc->TxState) hcdc->TxState = 0; // unblock pipeline 127 | extern void cdc_force_tx_complete_hook(void); 128 | cdc_force_tx_complete_hook(); 129 | /* auto_ping / small-status packets permanently removed to ensure the IN 130 | bulk stream remains strictly binary under all build configs. */ 131 | // Burst feeder for IQ streaming: immediately schedule next IQ packet if available 132 | #if defined(ENABLE_IQ) && (ENABLE_IQ==1) 133 | extern volatile uint8_t ep1_busy_flag; // already cleared 134 | extern volatile iq_chunk_t q[8]; 135 | extern volatile uint8_t q_head; extern volatile uint8_t q_tail; 136 | extern USBD_HandleTypeDef hUsbDeviceFS; 137 | extern volatile uint32_t feed_isr_pkts; extern volatile uint32_t feed_chain_max; 138 | extern volatile uint32_t feed_chain_current; 139 | // Limit chain depth per ISR to avoid starving main loop. 140 | // Increased from 12 to 16 to permit larger burst drains while still bounded. 141 | uint32_t chain = 0; const uint32_t chain_limit = 16; 142 | while(ep1_busy_flag==0 && q_tail != q_head && chain < chain_limit) { 143 | iq_chunk_t chunk = q[q_tail]; 144 | uint32_t send_bytes = chunk.words * 4U; if(send_bytes > 64) send_bytes = 64; 145 | ep1_busy_flag = 1; 146 | USBD_LL_Transmit(&hUsbDeviceFS, 0x81, (uint8_t*)chunk.ptr, send_bytes); 147 | chunk.ptr += send_bytes / 4U; chunk.words -= send_bytes / 4U; 148 | if(chunk.words == 0) { q_tail = (q_tail + 1) & 7; } else { q[q_tail] = chunk; } 149 | feed_isr_pkts++; chain++; 150 | } 151 | feed_chain_current = chain; 152 | if(chain > feed_chain_max) feed_chain_max = chain; 153 | #endif 154 | } 155 | } 156 | USBD_LL_DataInStage(hpcd->pData, epnum, hpcd->IN_ep[epnum].xfer_buff); 157 | } 158 | void HAL_PCD_SOFCallback(PCD_HandleTypeDef *hpcd) { 159 | USBD_LL_SOF(hpcd->pData); 160 | } 161 | void HAL_PCD_ResetCallback(PCD_HandleTypeDef *hpcd) { 162 | USBD_SpeedTypeDef speed = USBD_SPEED_FULL; USBD_LL_Reset(hpcd->pData); USBD_LL_SetSpeed(hpcd->pData, speed); 163 | } 164 | void HAL_PCD_SuspendCallback(PCD_HandleTypeDef *hpcd) { USBD_LL_Suspend(hpcd->pData); } 165 | void HAL_PCD_ResumeCallback(PCD_HandleTypeDef *hpcd) { USBD_LL_Resume(hpcd->pData); } 166 | void HAL_PCD_ConnectCallback(PCD_HandleTypeDef *hpcd) { USBD_LL_DevConnected(hpcd->pData); } 167 | void HAL_PCD_DisconnectCallback(PCD_HandleTypeDef *hpcd) { USBD_LL_DevDisconnected(hpcd->pData); } 168 | 169 | // Static alloc not required; using default malloc/free macros from usbd_conf.h 170 | 171 | // Low-level transmit and receive wrappers required by USB Device Core 172 | USBD_StatusTypeDef USBD_LL_Transmit(USBD_HandleTypeDef *pdev, uint8_t ep_addr, uint8_t *pbuf, uint16_t size) { 173 | HAL_PCD_EP_Transmit(pdev->pData, ep_addr, pbuf, size); 174 | return USBD_OK; 175 | } 176 | 177 | USBD_StatusTypeDef USBD_LL_PrepareReceive(USBD_HandleTypeDef *pdev, uint8_t ep_addr, uint8_t *pbuf, uint16_t size) { 178 | HAL_PCD_EP_Receive(pdev->pData, ep_addr, pbuf, size); 179 | return USBD_OK; 180 | } 181 | -------------------------------------------------------------------------------- /host_test.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | """ 3 | Simple Mac host test for STM32F103 CDC IQ streamer. 4 | 5 | Features: 6 | - Auto-detect /dev/tty.usbmodem* (or provide --port) 7 | - Sends START command 8 | - Reads IQ 32-bit words (I=lower16, Q=upper16 unsigned) and converts to signed centered values 9 | - Prints basic stats and optionally writes raw complex float32 to a file for GNU Radio / GQRX 10 | 11 | Usage examples: 12 | python3 host_test.py --seconds 5 --out iq.f32 13 | python3 host_test.py --port /dev/tty.usbmodem123456 --seconds 2 14 | 15 | To visualize quickly with numpy/matplotlib: 16 | python3 - <<'EOF' 17 | import numpy as np 18 | x = np.fromfile('iq.f32', dtype=np.float32).view(np.complex64) 19 | print('Samples:', x.size) 20 | EOF 21 | """ 22 | import sys, time, struct, glob, argparse, os, signal 23 | 24 | try: 25 | import serial 26 | except ImportError: 27 | print("pyserial needed: pip install pyserial") 28 | sys.exit(1) 29 | 30 | parser = argparse.ArgumentParser(description="STM32 CDC IQ capture utility with graceful interrupt handling") 31 | parser.add_argument('--port', help='Serial port (auto if omitted)') 32 | parser.add_argument('--baud', type=int, default=115200, help='Ignored by CDC but required by pyserial') 33 | parser.add_argument('--seconds', type=float, default=3.0, help='Capture duration (ignored if --max-bytes or --pairs used)') 34 | parser.add_argument('--out', help='Optional output file (float32 interleaved I,Q)') 35 | parser.add_argument('--max-bytes', type=int, default=0, help='Stop after this many payload bytes (0=disabled)') 36 | parser.add_argument('--pairs', type=int, default=0, help='Stop after this many IQ pairs (0=disabled)') 37 | parser.add_argument('--progress', action='store_true', help='Show live progress bar / stats') 38 | parser.add_argument('--quiet', action='store_true', help='Suppress periodic rate prints') 39 | parser.add_argument('--read-timeout', type=float, default=0.2, help='Serial read timeout seconds (default 0.2, smaller = more responsive Ctrl+C)') 40 | parser.add_argument('--idle-exit', type=float, default=0.0, help='Exit if no data received for this many seconds (0=disabled)') 41 | parser.add_argument('--no-store', action='store_true', help='Do not store raw bytes (reduces RAM). Stats limited; no file output.') 42 | parser.add_argument('--raw-dump', help='Optional raw binary output file (32-bit words) written streaming (even with --no-store)') 43 | parser.add_argument('--diagnose', action='store_true', help='Diagnostic mode: probe command endings, poll STATS, print early bytes') 44 | parser.add_argument('--raw-hex', nargs='?', const=64, type=int, default=0, 45 | help='Print hex dump of first N bytes (default 64 if value omitted)') 46 | parser.add_argument('--show-ascii', action='store_true', help='Show printable ASCII of incoming bytes during capture') 47 | args = parser.parse_args() 48 | 49 | if not args.port: 50 | cands = glob.glob('/dev/tty.usbmodem*') + glob.glob('/dev/tty.usbserial*') 51 | if not cands: 52 | print('No USB CDC ports found') 53 | sys.exit(1) 54 | args.port = cands[0] 55 | 56 | print(f'Using port: {args.port}') 57 | 58 | ser = serial.Serial(args.port, args.baud, timeout=args.read_timeout) 59 | # Give device time after opening 60 | time.sleep(0.5) 61 | 62 | # Flush any banner 63 | ser.reset_input_buffer() 64 | 65 | def send(cmd, ending=b'\r'): 66 | ser.write(cmd + ending) 67 | ser.flush() 68 | 69 | if args.diagnose: 70 | print('[DIAG] Probing line endings for STATS...') 71 | for ending in (b'\r', b'\n', b'\r\n'): 72 | send(b'STATS', ending) 73 | time.sleep(0.05) 74 | print('[DIAG] Sending START with CR, LF, CRLF') 75 | for ending in (b'\r', b'\n', b'\r\n'): 76 | send(b'START', ending) 77 | time.sleep(0.05) 78 | else: 79 | send(b'START') 80 | start_time = time.time() 81 | raw_bytes = bytearray() if not args.no_store else None 82 | raw_count = 0 # total bytes seen (even if not stored) 83 | 84 | TARGET_END = start_time + args.seconds 85 | max_bytes = args.max_bytes if args.max_bytes > 0 else None 86 | target_pairs = args.pairs if args.pairs > 0 else None 87 | 88 | def format_rate(bytes_so_far, t0): 89 | dt = max(1e-6, time.time()-t0) 90 | bps = bytes_so_far/dt 91 | return f"{bps/1024:.1f} KB/s ({(bps/4)/1000:.1f} kIQ/s)" 92 | 93 | last_update = 0.0 94 | update_interval = 0.25 95 | 96 | raw_out_f = None 97 | if args.raw_dump: 98 | raw_out_f = open(args.raw_dump, 'wb') 99 | 100 | last_data_time = time.time() 101 | interrupted = False 102 | hex_remaining = args.raw_hex 103 | 104 | def handle_sigint(signum, frame): 105 | global interrupted 106 | interrupted = True 107 | 108 | signal.signal(signal.SIGINT, handle_sigint) 109 | 110 | try: 111 | while True: 112 | current_len = raw_count if args.no_store else len(raw_bytes) 113 | if max_bytes and current_len >= max_bytes: 114 | break 115 | if target_pairs and (current_len//4) >= target_pairs: 116 | break 117 | if not max_bytes and not target_pairs and time.time() >= TARGET_END: 118 | break 119 | if interrupted: 120 | if args.progress: 121 | print("\n[INT] Ctrl+C received, stopping...") 122 | break 123 | chunk = ser.read(1024) 124 | if chunk: 125 | last_data_time = time.time() 126 | raw_count += len(chunk) 127 | if hex_remaining > 0: 128 | display = chunk[:hex_remaining] 129 | print('\n[HEX]', ' '.join(f'{b:02X}' for b in display)) 130 | hex_remaining -= len(display) 131 | if args.show_ascii: 132 | printable = ''.join(chr(b) if 32 <= b < 127 else '.' for b in chunk) 133 | print(f'\n[ASC] {printable}') 134 | if raw_out_f: 135 | raw_out_f.write(chunk) 136 | if raw_bytes is not None: 137 | raw_bytes.extend(chunk) 138 | if args.diagnose and (raw_count < 256) and (b'STATS' in chunk or b'OK' in chunk or b'?' in chunk): 139 | print('[DIAG] Echo/Response fragment:', chunk) 140 | elif args.idle_exit > 0 and (time.time() - last_data_time) > args.idle_exit: 141 | if args.progress: 142 | print("\n[IDLE] No data, idle timeout reached") 143 | break 144 | if args.progress and (time.time()-last_update) > update_interval: 145 | pairs = (raw_count if args.no_store else len(raw_bytes))//4 146 | goal_desc = [] 147 | cur_bytes = raw_count if args.no_store else len(raw_bytes) 148 | if max_bytes: goal_desc.append(f"{cur_bytes}/{max_bytes}B") 149 | if target_pairs: goal_desc.append(f"{pairs}/{target_pairs}pr") 150 | if not goal_desc: goal_desc.append(f"{pairs}pr") 151 | rate = format_rate(cur_bytes, start_time) 152 | print("\r[CAP] " + ' '.join(goal_desc) + " " + rate + ' ', end='', flush=True) 153 | # Loop continues 154 | finally: 155 | # Always attempt to stop the device 156 | try: 157 | ser.write(b'STOP\r') 158 | ser.flush() 159 | except Exception: 160 | pass 161 | if raw_out_f: 162 | raw_out_f.close() 163 | 164 | if args.progress: 165 | print() # newline after progress / interrupt lines 166 | 167 | total_bytes = raw_count if args.no_store else len(raw_bytes) 168 | print(f'Received {total_bytes} bytes raw in {time.time()-start_time:.2f}s ({format_rate(total_bytes, start_time)})') 169 | 170 | # Data should be multiples of 4 171 | if raw_bytes is None: 172 | if args.out: 173 | print('Cannot write float32 output with --no-store enabled') 174 | print('Streaming mode (no-store) finished; skipped detailed stats.') 175 | sys.exit(0) 176 | 177 | usable = len(raw_bytes) - (len(raw_bytes) % 4) 178 | if usable != len(raw_bytes): 179 | print(f'Trimming {len(raw_bytes)-usable} trailing bytes to align to 32-bit words') 180 | raw_bytes = raw_bytes[:usable] 181 | 182 | word_count = usable // 4 183 | print(f'Words: {word_count} Pairs: {word_count}') 184 | 185 | if word_count == 0: 186 | sys.exit(0) 187 | 188 | # Unpack little-endian 32-bit words (each holds two 16-bit lanes with 12-bit right-aligned samples) 189 | words = struct.unpack('<' + 'I'*word_count, raw_bytes) 190 | I = [] # centered float-ready integers (scaled later) 191 | Q = [] 192 | for w in words: 193 | lane_i = w & 0xFFFF # lower half-word 194 | lane_q = (w >> 16) & 0xFFFF # upper half-word 195 | raw12_i = lane_i & 0x0FFF 196 | raw12_q = lane_q & 0x0FFF 197 | # Center at 2048 (12-bit midscale) giving signed range roughly [-2048,+2047] 198 | I.append(raw12_i - 2048) 199 | Q.append(raw12_q - 2048) 200 | 201 | # Optional write float32 IQ interleaved (normalized to [-1,+1)) 202 | if args.out: 203 | import array 204 | scale = 2048.0 # 12-bit midscale 205 | floats = array.array('f') 206 | for a,b in zip(I,Q): 207 | floats.append(a/scale) 208 | floats.append(b/scale) 209 | with open(args.out, 'wb') as f: 210 | floats.tofile(f) 211 | print(f'Wrote {len(I)} IQ pairs to {args.out} (12-bit centered scaling)') 212 | 213 | # Basic stats 214 | import math 215 | mean_i = sum(I)/len(I) 216 | mean_q = sum(Q)/len(Q) 217 | var_i = sum((x-mean_i)**2 for x in I)/len(I) 218 | var_q = sum((x-mean_q)**2 for x in Q)/len(Q) 219 | print(f'I mean={mean_i:.1f} rms={math.sqrt(var_i):.1f} Q mean={mean_q:.1f} rms={math.sqrt(var_q):.1f}') 220 | 221 | print('Done') 222 | 223 | # Notes: 224 | # - Press Ctrl+C once for graceful stop (sends STOP). Press twice quickly to force if shell still busy. 225 | # - Use --read-timeout smaller for more responsiveness, or --idle-exit to auto-stop when stream dies. 226 | # - Use --no-store + --raw-dump for long captures without large RAM usage. 227 | -------------------------------------------------------------------------------- /src/iq_adc.c: -------------------------------------------------------------------------------- 1 | #include "iq_adc.h" 2 | #include "main.h" 3 | #if defined(ENABLE_IQ) && (ENABLE_IQ==1) 4 | #include "usbd_core.h" 5 | #include "usbd_def.h" 6 | #include "usbd_cdc.h" 7 | #endif 8 | 9 | /* Exported runtime-achieved sample rate (I/Q pairs per second). Set in timer_trigger_init(). 10 | * Declared in Inc/iq_adc.h as extern. */ 11 | volatile uint32_t iq_achieved_rate = 0; 12 | 13 | /* Buffer: each 32-bit entry holds I (lower 16) and Q (upper 16) */ 14 | static volatile uint32_t iq_buffer[IQ_DMA_LENGTH]; 15 | volatile uint32_t iq_dma_half_count = 0; 16 | volatile uint32_t iq_dma_full_count = 0; 17 | volatile uint32_t iq_last_words[4] = {0,0,0,0}; // snapshot for smoke test 18 | static iq_callback_t user_cb = 0; 19 | 20 | static ADC_HandleTypeDef hadc1; // master 21 | static ADC_HandleTypeDef hadc2; // slave 22 | static DMA_HandleTypeDef hdma_adc1; 23 | static TIM_HandleTypeDef htim3; 24 | 25 | static void adc_gpio_init(void) { 26 | __HAL_RCC_GPIOA_CLK_ENABLE(); 27 | GPIO_InitTypeDef GPIO_InitStruct = {0}; 28 | GPIO_InitStruct.Pin = GPIO_PIN_0 | GPIO_PIN_1; // PA0, PA1 29 | GPIO_InitStruct.Mode = GPIO_MODE_ANALOG; 30 | GPIO_InitStruct.Pull = GPIO_NOPULL; 31 | HAL_GPIO_Init(GPIOA, &GPIO_InitStruct); 32 | } 33 | 34 | static volatile uint32_t iq_achieved_rate_local = 0; // internal holder 35 | static void timer_trigger_init(void) { 36 | __HAL_RCC_TIM3_CLK_ENABLE(); 37 | uint32_t timer_clk = HAL_RCC_GetPCLK1Freq(); 38 | // If APB1 prescaler !=1 timer clock doubles (TIMx clocking on APB1 domain quirk) 39 | if(((RCC->CFGR & RCC_CFGR_PPRE1)>>8) > 0) timer_clk *= 2U; 40 | 41 | uint32_t target = IQ_SAMPLE_RATE_HZ; // desired samples per second 42 | 43 | // Strategy: iterate possible prescalers to find an ARR within 16-bit and 44 | // frequency closest to target. Limit search to keep it quick. 45 | uint32_t best_psc = 0, best_arr = 0, best_diff = 0xFFFFFFFF, best_rate = 0; 46 | for(uint32_t psc = 0; psc < 0xFFFF; ++psc) { 47 | uint32_t tclk_div = timer_clk / (psc + 1U); 48 | if(tclk_div < target) break; // further prescalers only lower tclk_div 49 | uint32_t arr = (tclk_div / target); 50 | if(arr == 0) continue; 51 | if(arr > 0) arr -= 1U; // ARR is zero-based 52 | if(arr > 0xFFFF) continue; 53 | uint32_t achieved = tclk_div / (arr + 1U); 54 | uint32_t diff = (achieved > target) ? (achieved - target) : (target - achieved); 55 | if(diff < best_diff) { 56 | best_diff = diff; best_psc = psc; best_arr = arr; best_rate = achieved; 57 | if(diff == 0) break; 58 | } 59 | } 60 | // Fallback: if not found (shouldn't happen) default to 1MHz base logic 61 | if(best_diff == 0xFFFFFFFF) { 62 | best_psc = (timer_clk / 1000000U) - 1U; 63 | best_arr = (1000000U / target) - 1U; 64 | best_rate = (timer_clk / (best_psc + 1U)) / (best_arr + 1U); 65 | } 66 | 67 | htim3.Instance = TIM3; 68 | htim3.Init.Prescaler = best_psc; 69 | htim3.Init.CounterMode = TIM_COUNTERMODE_UP; 70 | htim3.Init.Period = best_arr; 71 | htim3.Init.ClockDivision = TIM_CLOCKDIVISION_DIV1; 72 | htim3.Init.AutoReloadPreload = TIM_AUTORELOAD_PRELOAD_DISABLE; 73 | HAL_TIM_Base_Init(&htim3); 74 | 75 | // TRGO on update 76 | TIM_MasterConfigTypeDef mcfg = {0}; 77 | mcfg.MasterOutputTrigger = TIM_TRGO_UPDATE; 78 | mcfg.MasterSlaveMode = TIM_MASTERSLAVEMODE_DISABLE; 79 | HAL_TIMEx_MasterConfigSynchronization(&htim3, &mcfg); 80 | 81 | // Optionally store or log achieved rate for diagnostics (user can watch in debugger) 82 | iq_achieved_rate_local = best_rate; 83 | /* Export to header-visible global so debugger or diagnostic inspection can 84 | * read the actual achieved timer/sample rate without sending any CDC text. */ 85 | iq_achieved_rate = best_rate; 86 | } 87 | 88 | static void adc_dual_init(void) { 89 | __HAL_RCC_ADC1_CLK_ENABLE(); 90 | __HAL_RCC_ADC2_CLK_ENABLE(); 91 | 92 | // ADC clock prescaler: PCLK2 divided by 6 -> <=14MHz 93 | __HAL_RCC_AFIO_CLK_ENABLE(); 94 | MODIFY_REG(RCC->CFGR, RCC_CFGR_ADCPRE, RCC_CFGR_ADCPRE_DIV6); 95 | 96 | hadc1.Instance = ADC1; 97 | hadc1.Init.DataAlign = ADC_DATAALIGN_RIGHT; 98 | hadc1.Init.ScanConvMode = ADC_SCAN_DISABLE; 99 | hadc1.Init.ContinuousConvMode = DISABLE; 100 | hadc1.Init.DiscontinuousConvMode = DISABLE; 101 | hadc1.Init.NbrOfDiscConversion = 0; 102 | hadc1.Init.ExternalTrigConv = ADC_EXTERNALTRIGCONV_T3_TRGO; 103 | hadc1.Init.NbrOfConversion = 1; 104 | // F1 HAL ADC_InitTypeDef does not contain Mode; dual mode is configured via registers directly 105 | HAL_ADC_Init(&hadc1); 106 | 107 | hadc2.Instance = ADC2; 108 | hadc2.Init = hadc1.Init; 109 | HAL_ADC_Init(&hadc2); 110 | 111 | // Configure channels 112 | ADC_ChannelConfTypeDef sConfig = {0}; 113 | sConfig.Rank = ADC_REGULAR_RANK_1; 114 | sConfig.SamplingTime = ADC_SAMPLETIME_28CYCLES_5; 115 | sConfig.Channel = ADC_CHANNEL_0; // PA0 116 | HAL_ADC_ConfigChannel(&hadc1, &sConfig); 117 | sConfig.Channel = ADC_CHANNEL_1; // PA1 118 | HAL_ADC_ConfigChannel(&hadc2, &sConfig); 119 | 120 | // Enable dual regular simultaneous mode: set DUAL[4:0]=00100 in ADC1->CR1 121 | MODIFY_REG(ADC1->CR1, 0x1F << 16, 0x4 << 16); 122 | 123 | // Calibration sequence 124 | HAL_ADCEx_Calibration_Start(&hadc1); 125 | HAL_ADCEx_Calibration_Start(&hadc2); 126 | } 127 | 128 | static void dma_init(void) { 129 | __HAL_RCC_DMA1_CLK_ENABLE(); 130 | hdma_adc1.Instance = DMA1_Channel1; 131 | hdma_adc1.Init.Direction = DMA_PERIPH_TO_MEMORY; 132 | hdma_adc1.Init.PeriphInc = DMA_PINC_DISABLE; 133 | hdma_adc1.Init.MemInc = DMA_MINC_ENABLE; 134 | hdma_adc1.Init.PeriphDataAlignment = DMA_PDATAALIGN_WORD; // dual-mode packs into 32-bit 135 | hdma_adc1.Init.MemDataAlignment = DMA_MDATAALIGN_WORD; 136 | hdma_adc1.Init.Mode = DMA_CIRCULAR; 137 | hdma_adc1.Init.Priority = DMA_PRIORITY_HIGH; 138 | HAL_DMA_Init(&hdma_adc1); 139 | __HAL_LINKDMA(&hadc1, DMA_Handle, hdma_adc1); 140 | 141 | HAL_NVIC_SetPriority(DMA1_Channel1_IRQn, 5, 0); 142 | HAL_NVIC_EnableIRQ(DMA1_Channel1_IRQn); 143 | } 144 | 145 | void iq_init(iq_callback_t cb) { 146 | user_cb = cb; 147 | adc_gpio_init(); 148 | timer_trigger_init(); 149 | adc_dual_init(); 150 | dma_init(); 151 | } 152 | 153 | void iq_start(void) { 154 | // Enable DMA interrupts for half/transfer complete 155 | __HAL_DMA_ENABLE_IT(&hdma_adc1, DMA_IT_HT); 156 | __HAL_DMA_ENABLE_IT(&hdma_adc1, DMA_IT_TC); 157 | // Ensure DMA bit enabled in ADC1 (F1 requires explicit set) 158 | ADC1->CR2 |= ADC_CR2_DMA; 159 | 160 | // Start slave then master 161 | HAL_ADC_Start(&hadc2); 162 | HAL_ADC_Start(&hadc1); // ensure master actually enabled before DMA start 163 | 164 | // Try HAL helper; if it fails to configure CNDTR (remains 0) we fallback 165 | (void)HAL_ADCEx_MultiModeStart_DMA(&hadc1, (uint32_t*)iq_buffer, IQ_DMA_LENGTH); 166 | 167 | if (DMA1_Channel1->CNDTR == 0) { 168 | // Manual DMA setup fallback for dual regular simultaneous mode 169 | DMA1_Channel1->CCR &= ~DMA_CCR_EN; 170 | DMA1_Channel1->CPAR = (uint32_t)&ADC1->DR; 171 | DMA1_Channel1->CMAR = (uint32_t)iq_buffer; 172 | DMA1_Channel1->CNDTR = IQ_DMA_LENGTH; // number of 32-bit words 173 | // Configure: memory increment, peripheral/mem size 32-bit, circular, high priority 174 | DMA1_Channel1->CCR = DMA_CCR_MINC | DMA_CCR_MSIZE_1 | DMA_CCR_PSIZE_1 | DMA_CCR_CIRC | DMA_CCR_PL_1; 175 | // Enable half/transfer complete interrupts 176 | DMA1_Channel1->CCR |= DMA_CCR_HTIE | DMA_CCR_TCIE; 177 | DMA1_Channel1->CCR |= DMA_CCR_EN; 178 | } 179 | 180 | // Start timer trigger after DMA armed 181 | HAL_TIM_Base_Start(&htim3); 182 | 183 | #if defined(ADC_SMOKE) && (ADC_SMOKE==1) 184 | // In smoke mode also issue a software start in case external trigger path misconfigured 185 | // Enable external trigger if not already 186 | ADC1->CR2 |= ADC_CR2_EXTTRIG; 187 | // Also try a SWSTART to kick first conversion; dual regular simultaneous: start master is sufficient 188 | ADC1->CR2 |= ADC_CR2_SWSTART; 189 | #endif 190 | } 191 | 192 | void iq_stop(void) { 193 | HAL_TIM_Base_Stop(&htim3); 194 | HAL_ADC_Stop(&hadc1); 195 | HAL_ADC_Stop(&hadc2); 196 | HAL_DMA_Abort(&hdma_adc1); 197 | } 198 | 199 | void DMA1_Channel1_IRQHandler(void) { 200 | if(__HAL_DMA_GET_FLAG(&hdma_adc1, DMA_FLAG_HT1)) { 201 | __HAL_DMA_CLEAR_FLAG(&hdma_adc1, DMA_FLAG_HT1); 202 | iq_dma_half_count++; 203 | iq_last_words[0] = iq_buffer[0]; 204 | iq_last_words[1] = iq_buffer[1]; 205 | // Toggle LED to indicate DMA half complete 206 | extern void dma_led_toggle(void); dma_led_toggle(); 207 | if(user_cb) user_cb((uint32_t*)iq_buffer, IQ_DMA_LENGTH/2U, 0); 208 | // DMA kick: schedule first packet immediately if USB idle 209 | #if defined(ENABLE_IQ) && (ENABLE_IQ==1) 210 | extern volatile uint8_t ep1_busy_flag; extern USBD_HandleTypeDef hUsbDeviceFS; 211 | extern volatile iq_chunk_t q[8]; extern volatile uint8_t q_tail, q_head; 212 | extern volatile uint32_t feed_isr_pkts; // reuse ISR counter 213 | static volatile uint32_t dma_kick_count = 0; // local static for reference (not exposed yet) 214 | if(hUsbDeviceFS.dev_state == USBD_STATE_CONFIGURED && ep1_busy_flag==0 && q_tail != q_head) { 215 | iq_chunk_t chunk = q[q_tail]; 216 | uint32_t send_bytes = chunk.words * 4U; if(send_bytes > 64) send_bytes = 64; 217 | ep1_busy_flag = 1; 218 | USBD_LL_Transmit(&hUsbDeviceFS, 0x81, (uint8_t*)chunk.ptr, send_bytes); 219 | chunk.ptr += send_bytes/4U; chunk.words -= send_bytes/4U; 220 | if(chunk.words==0) { q_tail = (q_tail + 1) & 7; } else { q[q_tail] = chunk; } 221 | feed_isr_pkts++; dma_kick_count++; 222 | } 223 | #endif 224 | } 225 | if(__HAL_DMA_GET_FLAG(&hdma_adc1, DMA_FLAG_TC1)) { 226 | __HAL_DMA_CLEAR_FLAG(&hdma_adc1, DMA_FLAG_TC1); 227 | iq_dma_full_count++; 228 | iq_last_words[2] = iq_buffer[IQ_DMA_LENGTH/2U]; 229 | iq_last_words[3] = iq_buffer[IQ_DMA_LENGTH/2U + 1U]; 230 | // Toggle LED to indicate DMA full complete 231 | extern void dma_led_toggle(void); dma_led_toggle(); 232 | if(user_cb) user_cb((uint32_t*)&iq_buffer[IQ_DMA_LENGTH/2U], IQ_DMA_LENGTH/2U, 1); 233 | // DMA kick for second half 234 | #if defined(ENABLE_IQ) && (ENABLE_IQ==1) 235 | extern volatile uint8_t ep1_busy_flag; extern USBD_HandleTypeDef hUsbDeviceFS; 236 | extern volatile iq_chunk_t q[8]; extern volatile uint8_t q_tail, q_head; 237 | extern volatile uint32_t feed_isr_pkts; 238 | static volatile uint32_t dma_kick_count2 = 0; 239 | if(hUsbDeviceFS.dev_state == USBD_STATE_CONFIGURED && ep1_busy_flag==0 && q_tail != q_head) { 240 | iq_chunk_t chunk = q[q_tail]; 241 | uint32_t send_bytes = chunk.words * 4U; if(send_bytes > 64) send_bytes = 64; 242 | ep1_busy_flag = 1; 243 | USBD_LL_Transmit(&hUsbDeviceFS, 0x81, (uint8_t*)chunk.ptr, send_bytes); 244 | chunk.ptr += send_bytes/4U; chunk.words -= send_bytes/4U; 245 | if(chunk.words==0) { q_tail = (q_tail + 1) & 7; } else { q[q_tail] = chunk; } 246 | feed_isr_pkts++; dma_kick_count2++; 247 | } 248 | #endif 249 | } 250 | } 251 | -------------------------------------------------------------------------------- /hardware/PhaseLatchMini/PhaseLatchMini.kicad_pro: -------------------------------------------------------------------------------- 1 | { 2 | "board": { 3 | "3dviewports": [], 4 | "design_settings": { 5 | "defaults": { 6 | "apply_defaults_to_fp_fields": false, 7 | "apply_defaults_to_fp_shapes": false, 8 | "apply_defaults_to_fp_text": false, 9 | "board_outline_line_width": 0.05, 10 | "copper_line_width": 0.2, 11 | "copper_text_italic": false, 12 | "copper_text_size_h": 1.5, 13 | "copper_text_size_v": 1.5, 14 | "copper_text_thickness": 0.3, 15 | "copper_text_upright": false, 16 | "courtyard_line_width": 0.05, 17 | "dimension_precision": 4, 18 | "dimension_units": 3, 19 | "dimensions": { 20 | "arrow_length": 1270000, 21 | "extension_offset": 500000, 22 | "keep_text_aligned": true, 23 | "suppress_zeroes": true, 24 | "text_position": 0, 25 | "units_format": 0 26 | }, 27 | "fab_line_width": 0.1, 28 | "fab_text_italic": false, 29 | "fab_text_size_h": 1.0, 30 | "fab_text_size_v": 1.0, 31 | "fab_text_thickness": 0.15, 32 | "fab_text_upright": false, 33 | "other_line_width": 0.1, 34 | "other_text_italic": false, 35 | "other_text_size_h": 1.0, 36 | "other_text_size_v": 1.0, 37 | "other_text_thickness": 0.15, 38 | "other_text_upright": false, 39 | "pads": { 40 | "drill": 0.8, 41 | "height": 1.27, 42 | "width": 2.54 43 | }, 44 | "silk_line_width": 0.1, 45 | "silk_text_italic": false, 46 | "silk_text_size_h": 1.0, 47 | "silk_text_size_v": 1.0, 48 | "silk_text_thickness": 0.1, 49 | "silk_text_upright": false, 50 | "zones": { 51 | "min_clearance": 0.5 52 | } 53 | }, 54 | "diff_pair_dimensions": [ 55 | { 56 | "gap": 0.0, 57 | "via_gap": 0.0, 58 | "width": 0.0 59 | }, 60 | { 61 | "gap": 0.15, 62 | "via_gap": 0.15, 63 | "width": 0.1 64 | } 65 | ], 66 | "drc_exclusions": [], 67 | "meta": { 68 | "version": 2 69 | }, 70 | "rule_severities": { 71 | "annular_width": "error", 72 | "clearance": "error", 73 | "connection_width": "warning", 74 | "copper_edge_clearance": "error", 75 | "copper_sliver": "warning", 76 | "courtyards_overlap": "error", 77 | "creepage": "error", 78 | "diff_pair_gap_out_of_range": "error", 79 | "diff_pair_uncoupled_length_too_long": "error", 80 | "drill_out_of_range": "error", 81 | "duplicate_footprints": "warning", 82 | "extra_footprint": "warning", 83 | "footprint": "error", 84 | "footprint_filters_mismatch": "ignore", 85 | "footprint_symbol_mismatch": "warning", 86 | "footprint_type_mismatch": "ignore", 87 | "hole_clearance": "error", 88 | "hole_to_hole": "warning", 89 | "holes_co_located": "warning", 90 | "invalid_outline": "error", 91 | "isolated_copper": "warning", 92 | "item_on_disabled_layer": "error", 93 | "items_not_allowed": "error", 94 | "length_out_of_range": "error", 95 | "lib_footprint_issues": "warning", 96 | "lib_footprint_mismatch": "warning", 97 | "malformed_courtyard": "error", 98 | "microvia_drill_out_of_range": "error", 99 | "mirrored_text_on_front_layer": "warning", 100 | "missing_courtyard": "ignore", 101 | "missing_footprint": "warning", 102 | "net_conflict": "warning", 103 | "nonmirrored_text_on_back_layer": "warning", 104 | "npth_inside_courtyard": "ignore", 105 | "padstack": "warning", 106 | "pth_inside_courtyard": "ignore", 107 | "shorting_items": "error", 108 | "silk_edge_clearance": "warning", 109 | "silk_over_copper": "warning", 110 | "silk_overlap": "warning", 111 | "skew_out_of_range": "error", 112 | "solder_mask_bridge": "error", 113 | "starved_thermal": "error", 114 | "text_height": "warning", 115 | "text_on_edge_cuts": "error", 116 | "text_thickness": "warning", 117 | "through_hole_pad_without_hole": "error", 118 | "too_many_vias": "error", 119 | "track_angle": "error", 120 | "track_dangling": "warning", 121 | "track_segment_length": "error", 122 | "track_width": "error", 123 | "tracks_crossing": "error", 124 | "unconnected_items": "error", 125 | "unresolved_variable": "error", 126 | "via_dangling": "warning", 127 | "zones_intersect": "error" 128 | }, 129 | "rules": { 130 | "max_error": 0.005, 131 | "min_clearance": 0.0, 132 | "min_connection": 0.0, 133 | "min_copper_edge_clearance": 0.5, 134 | "min_groove_width": 0.0, 135 | "min_hole_clearance": 0.25, 136 | "min_hole_to_hole": 0.25, 137 | "min_microvia_diameter": 0.2, 138 | "min_microvia_drill": 0.1, 139 | "min_resolved_spokes": 2, 140 | "min_silk_clearance": 0.0, 141 | "min_text_height": 0.8, 142 | "min_text_thickness": 0.08, 143 | "min_through_hole_diameter": 0.3, 144 | "min_track_width": 0.0, 145 | "min_via_annular_width": 0.1, 146 | "min_via_diameter": 0.5, 147 | "solder_mask_to_copper_clearance": 0.005, 148 | "use_height_for_length_calcs": true 149 | }, 150 | "teardrop_options": [ 151 | { 152 | "td_onpthpad": true, 153 | "td_onroundshapesonly": false, 154 | "td_onsmdpad": true, 155 | "td_ontrackend": false, 156 | "td_onvia": true 157 | } 158 | ], 159 | "teardrop_parameters": [ 160 | { 161 | "td_allow_use_two_tracks": true, 162 | "td_curve_segcount": 0, 163 | "td_height_ratio": 1.0, 164 | "td_length_ratio": 0.5, 165 | "td_maxheight": 2.0, 166 | "td_maxlen": 1.0, 167 | "td_on_pad_in_zone": false, 168 | "td_target_name": "td_round_shape", 169 | "td_width_to_size_filter_ratio": 0.9 170 | }, 171 | { 172 | "td_allow_use_two_tracks": true, 173 | "td_curve_segcount": 0, 174 | "td_height_ratio": 1.0, 175 | "td_length_ratio": 0.5, 176 | "td_maxheight": 2.0, 177 | "td_maxlen": 1.0, 178 | "td_on_pad_in_zone": false, 179 | "td_target_name": "td_rect_shape", 180 | "td_width_to_size_filter_ratio": 0.9 181 | }, 182 | { 183 | "td_allow_use_two_tracks": true, 184 | "td_curve_segcount": 0, 185 | "td_height_ratio": 1.0, 186 | "td_length_ratio": 0.5, 187 | "td_maxheight": 2.0, 188 | "td_maxlen": 1.0, 189 | "td_on_pad_in_zone": false, 190 | "td_target_name": "td_track_end", 191 | "td_width_to_size_filter_ratio": 0.9 192 | } 193 | ], 194 | "track_widths": [ 195 | 0.0, 196 | 0.1, 197 | 0.15, 198 | 0.2, 199 | 0.5 200 | ], 201 | "tuning_pattern_settings": { 202 | "diff_pair_defaults": { 203 | "corner_radius_percentage": 80, 204 | "corner_style": 1, 205 | "max_amplitude": 1.0, 206 | "min_amplitude": 0.2, 207 | "single_sided": false, 208 | "spacing": 1.0 209 | }, 210 | "diff_pair_skew_defaults": { 211 | "corner_radius_percentage": 80, 212 | "corner_style": 1, 213 | "max_amplitude": 1.0, 214 | "min_amplitude": 0.2, 215 | "single_sided": false, 216 | "spacing": 0.6 217 | }, 218 | "single_track_defaults": { 219 | "corner_radius_percentage": 80, 220 | "corner_style": 1, 221 | "max_amplitude": 1.0, 222 | "min_amplitude": 0.2, 223 | "single_sided": false, 224 | "spacing": 0.6 225 | } 226 | }, 227 | "via_dimensions": [ 228 | { 229 | "diameter": 0.0, 230 | "drill": 0.0 231 | } 232 | ], 233 | "zones_allow_external_fillets": false 234 | }, 235 | "ipc2581": { 236 | "dist": "", 237 | "distpn": "", 238 | "internal_id": "", 239 | "mfg": "", 240 | "mpn": "" 241 | }, 242 | "layer_pairs": [], 243 | "layer_presets": [], 244 | "viewports": [] 245 | }, 246 | "boards": [], 247 | "cvpcb": { 248 | "equivalence_files": [] 249 | }, 250 | "erc": { 251 | "erc_exclusions": [], 252 | "meta": { 253 | "version": 0 254 | }, 255 | "pin_map": [ 256 | [ 257 | 0, 258 | 0, 259 | 0, 260 | 0, 261 | 0, 262 | 0, 263 | 1, 264 | 0, 265 | 0, 266 | 0, 267 | 0, 268 | 2 269 | ], 270 | [ 271 | 0, 272 | 2, 273 | 0, 274 | 1, 275 | 0, 276 | 0, 277 | 1, 278 | 0, 279 | 2, 280 | 2, 281 | 2, 282 | 2 283 | ], 284 | [ 285 | 0, 286 | 0, 287 | 0, 288 | 0, 289 | 0, 290 | 0, 291 | 1, 292 | 0, 293 | 1, 294 | 0, 295 | 1, 296 | 2 297 | ], 298 | [ 299 | 0, 300 | 1, 301 | 0, 302 | 0, 303 | 0, 304 | 0, 305 | 1, 306 | 1, 307 | 2, 308 | 1, 309 | 1, 310 | 2 311 | ], 312 | [ 313 | 0, 314 | 0, 315 | 0, 316 | 0, 317 | 0, 318 | 0, 319 | 1, 320 | 0, 321 | 0, 322 | 0, 323 | 0, 324 | 2 325 | ], 326 | [ 327 | 0, 328 | 0, 329 | 0, 330 | 0, 331 | 0, 332 | 0, 333 | 0, 334 | 0, 335 | 0, 336 | 0, 337 | 0, 338 | 2 339 | ], 340 | [ 341 | 1, 342 | 1, 343 | 1, 344 | 1, 345 | 1, 346 | 0, 347 | 1, 348 | 1, 349 | 1, 350 | 1, 351 | 1, 352 | 2 353 | ], 354 | [ 355 | 0, 356 | 0, 357 | 0, 358 | 1, 359 | 0, 360 | 0, 361 | 1, 362 | 0, 363 | 0, 364 | 0, 365 | 0, 366 | 2 367 | ], 368 | [ 369 | 0, 370 | 2, 371 | 1, 372 | 2, 373 | 0, 374 | 0, 375 | 1, 376 | 0, 377 | 2, 378 | 2, 379 | 2, 380 | 2 381 | ], 382 | [ 383 | 0, 384 | 2, 385 | 0, 386 | 1, 387 | 0, 388 | 0, 389 | 1, 390 | 0, 391 | 2, 392 | 0, 393 | 0, 394 | 2 395 | ], 396 | [ 397 | 0, 398 | 2, 399 | 1, 400 | 1, 401 | 0, 402 | 0, 403 | 1, 404 | 0, 405 | 2, 406 | 0, 407 | 0, 408 | 2 409 | ], 410 | [ 411 | 2, 412 | 2, 413 | 2, 414 | 2, 415 | 2, 416 | 2, 417 | 2, 418 | 2, 419 | 2, 420 | 2, 421 | 2, 422 | 2 423 | ] 424 | ], 425 | "rule_severities": { 426 | "bus_definition_conflict": "error", 427 | "bus_entry_needed": "error", 428 | "bus_to_bus_conflict": "error", 429 | "bus_to_net_conflict": "error", 430 | "different_unit_footprint": "error", 431 | "different_unit_net": "error", 432 | "duplicate_reference": "error", 433 | "duplicate_sheet_names": "error", 434 | "endpoint_off_grid": "warning", 435 | "extra_units": "error", 436 | "footprint_filter": "ignore", 437 | "footprint_link_issues": "warning", 438 | "four_way_junction": "ignore", 439 | "global_label_dangling": "warning", 440 | "hier_label_mismatch": "error", 441 | "label_dangling": "error", 442 | "label_multiple_wires": "warning", 443 | "lib_symbol_issues": "warning", 444 | "lib_symbol_mismatch": "warning", 445 | "missing_bidi_pin": "warning", 446 | "missing_input_pin": "warning", 447 | "missing_power_pin": "error", 448 | "missing_unit": "warning", 449 | "multiple_net_names": "warning", 450 | "net_not_bus_member": "warning", 451 | "no_connect_connected": "warning", 452 | "no_connect_dangling": "warning", 453 | "pin_not_connected": "error", 454 | "pin_not_driven": "error", 455 | "pin_to_pin": "warning", 456 | "power_pin_not_driven": "error", 457 | "same_local_global_label": "warning", 458 | "similar_label_and_power": "warning", 459 | "similar_labels": "warning", 460 | "similar_power": "warning", 461 | "simulation_model_issue": "ignore", 462 | "single_global_label": "ignore", 463 | "unannotated": "error", 464 | "unconnected_wire_endpoint": "warning", 465 | "undefined_netclass": "error", 466 | "unit_value_mismatch": "error", 467 | "unresolved_variable": "error", 468 | "wire_dangling": "error" 469 | } 470 | }, 471 | "libraries": { 472 | "pinned_footprint_libs": [], 473 | "pinned_symbol_libs": [] 474 | }, 475 | "meta": { 476 | "filename": "PhaseLatchMini.kicad_pro", 477 | "version": 3 478 | }, 479 | "net_settings": { 480 | "classes": [ 481 | { 482 | "bus_width": 12, 483 | "clearance": 0.2, 484 | "diff_pair_gap": 0.15, 485 | "diff_pair_via_gap": 0.25, 486 | "diff_pair_width": 0.1, 487 | "line_style": 0, 488 | "microvia_diameter": 0.3, 489 | "microvia_drill": 0.1, 490 | "name": "Default", 491 | "pcb_color": "rgba(0, 0, 0, 0.000)", 492 | "priority": 2147483647, 493 | "schematic_color": "rgba(0, 0, 0, 0.000)", 494 | "track_width": 0.2, 495 | "via_diameter": 0.6, 496 | "via_drill": 0.3, 497 | "wire_width": 6 498 | } 499 | ], 500 | "meta": { 501 | "version": 4 502 | }, 503 | "net_colors": null, 504 | "netclass_assignments": null, 505 | "netclass_patterns": [] 506 | }, 507 | "pcbnew": { 508 | "last_paths": { 509 | "gencad": "", 510 | "idf": "", 511 | "netlist": "", 512 | "plot": "", 513 | "pos_files": "", 514 | "specctra_dsn": "", 515 | "step": "", 516 | "svg": "", 517 | "vrml": "" 518 | }, 519 | "page_layout_descr_file": "" 520 | }, 521 | "schematic": { 522 | "annotate_start_num": 0, 523 | "bom_export_filename": "${PROJECTNAME}.csv", 524 | "bom_fmt_presets": [], 525 | "bom_fmt_settings": { 526 | "field_delimiter": ",", 527 | "keep_line_breaks": false, 528 | "keep_tabs": false, 529 | "name": "CSV", 530 | "ref_delimiter": ",", 531 | "ref_range_delimiter": "", 532 | "string_delimiter": "\"" 533 | }, 534 | "bom_presets": [], 535 | "bom_settings": { 536 | "exclude_dnp": false, 537 | "fields_ordered": [ 538 | { 539 | "group_by": false, 540 | "label": "Reference", 541 | "name": "Reference", 542 | "show": true 543 | }, 544 | { 545 | "group_by": false, 546 | "label": "Qty", 547 | "name": "${QUANTITY}", 548 | "show": true 549 | }, 550 | { 551 | "group_by": true, 552 | "label": "Value", 553 | "name": "Value", 554 | "show": true 555 | }, 556 | { 557 | "group_by": true, 558 | "label": "DNP", 559 | "name": "${DNP}", 560 | "show": true 561 | }, 562 | { 563 | "group_by": true, 564 | "label": "Exclude from BOM", 565 | "name": "${EXCLUDE_FROM_BOM}", 566 | "show": true 567 | }, 568 | { 569 | "group_by": true, 570 | "label": "Exclude from Board", 571 | "name": "${EXCLUDE_FROM_BOARD}", 572 | "show": true 573 | }, 574 | { 575 | "group_by": true, 576 | "label": "Footprint", 577 | "name": "Footprint", 578 | "show": true 579 | }, 580 | { 581 | "group_by": false, 582 | "label": "Datasheet", 583 | "name": "Datasheet", 584 | "show": true 585 | }, 586 | { 587 | "group_by": false, 588 | "label": "LCSC", 589 | "name": "LCSC", 590 | "show": true 591 | }, 592 | { 593 | "group_by": false, 594 | "label": "Description", 595 | "name": "Description", 596 | "show": false 597 | }, 598 | { 599 | "group_by": false, 600 | "label": "#", 601 | "name": "${ITEM_NUMBER}", 602 | "show": false 603 | }, 604 | { 605 | "group_by": false, 606 | "label": "Sim.Pins", 607 | "name": "Sim.Pins", 608 | "show": false 609 | } 610 | ], 611 | "filter_string": "", 612 | "group_symbols": true, 613 | "include_excluded_from_bom": true, 614 | "name": "", 615 | "sort_asc": true, 616 | "sort_field": "Reference" 617 | }, 618 | "connection_grid_size": 50.0, 619 | "drawing": { 620 | "dashed_lines_dash_length_ratio": 12.0, 621 | "dashed_lines_gap_length_ratio": 3.0, 622 | "default_line_thickness": 6.0, 623 | "default_text_size": 50.0, 624 | "field_names": [], 625 | "intersheets_ref_own_page": false, 626 | "intersheets_ref_prefix": "", 627 | "intersheets_ref_short": false, 628 | "intersheets_ref_show": false, 629 | "intersheets_ref_suffix": "", 630 | "junction_size_choice": 3, 631 | "label_size_ratio": 0.375, 632 | "operating_point_overlay_i_precision": 3, 633 | "operating_point_overlay_i_range": "~A", 634 | "operating_point_overlay_v_precision": 3, 635 | "operating_point_overlay_v_range": "~V", 636 | "overbar_offset_ratio": 1.23, 637 | "pin_symbol_size": 25.0, 638 | "text_offset_ratio": 0.15 639 | }, 640 | "legacy_lib_dir": "", 641 | "legacy_lib_list": [], 642 | "meta": { 643 | "version": 1 644 | }, 645 | "net_format_name": "", 646 | "ngspice": { 647 | "fix_include_paths": true, 648 | "meta": { 649 | "version": 0 650 | }, 651 | "model_mode": 4, 652 | "workbook_filename": "" 653 | }, 654 | "page_layout_descr_file": "", 655 | "plot_directory": "", 656 | "space_save_all_events": true, 657 | "spice_current_sheet_as_root": false, 658 | "spice_external_command": "spice \"%I\"", 659 | "spice_model_current_sheet_as_root": true, 660 | "spice_save_all_currents": false, 661 | "spice_save_all_dissipations": false, 662 | "spice_save_all_voltages": false, 663 | "subpart_first_id": 65, 664 | "subpart_id_separator": 0 665 | }, 666 | "sheets": [ 667 | [ 668 | "26096f25-a87e-4f6d-a99b-624957e9ad57", 669 | "Root" 670 | ] 671 | ], 672 | "text_variables": {} 673 | } 674 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # PhaseLatch Mini – STM32F103 Dual-ADC IQ USB Streamer 2 | 3 | > Combined hardware + firmware + host tooling for a compact dual‑ADC I/Q capture platform. 4 | 5 | ## Overview 6 | 7 | High-rate continuous streaming of interleaved dual ADC (I/Q) samples over USB Full‑Speed (FS) using only the built‑in CDC class on an STM32F103C8 ("Blue Pill" style) board. Companion Python host tools provide live visualization, FIFO bridging, raw capture, diagnostics, and throughput benchmarking. 8 | 9 | > Status: Actively optimized. Current configured complex sample rate target: **210.5 k I/Q samples/sec** (`IQ_SAMPLE_RATE_HZ` = 210526) with sustained USB payload throughput >500 KiB/s. Timer PSC/ARR are selected at runtime by a search routine for the closest achievable rate; observed effective rate can be within a small delta of the target. ADC sampling time was reduced (now `ADC_SAMPLETIME_28CYCLES_5`) to reach this rate while maintaining conversion stability. 10 | 11 | > Origin / Intended Use: Initially developed as a lightweight streaming engine for the [PhaseLoom](https://github.com/AndersBNielsen/PhaseLoom) project, but fully usable with any dual (I/Q) analog front end producing baseband signals on two STM32F1 ADC channels. 12 | Note: Phaseloom should be modified for best performance. Replace 22kOhm feedback caps with matched 47kOhm resistors and replace the output stage 50 ohms with 0 ohm. 13 | 14 | https://www.youtube.com/watch?v=UEAtSE1PV44 15 | 16 | --- 17 | ## Hardware: PhaseLatch Mini 18 | A 4‑layer purple PCB (Blue Pill footprint inspired) integrating two SMA input ports and on‑board ~100 kHz low‑pass filtering (~210 kHz complex baseband bandwidth). Designed around the STM32F103C8 in dual regular simultaneous ADC mode (ADC1 + ADC2) to stream interleaved I/Q samples over USB Full-Speed. 19 | 20 | PhaseLatch Mini 21 | 22 | ### Key Features 23 | - MCU: STM32F103C8 (72 MHz) LQFP‑48 24 | - Dual simultaneous ADC (12‑bit each) packed into 32‑bit words (I lower 12 bits, Q upper 12 bits) 25 | - 2 × edge‑mount SMA (J20 = I_IN, J21 = Q_IN) 26 | - Integrated passive input filtering (inductor + capacitor network targeting ~100 kHz LPF per channel) 27 | - USB‑C connector (USB2.0 FS, CDC class currently; raw/bulk class option planned) 28 | - On‑board 8 MHz + 32.768 kHz crystals for stable system and RTC timing 29 | - Ferrite bead + local bulk/decoupling for cleaner ADC rails 30 | - Unused GPIOs forced to analog mode early for noise reduction (see `Quiet_Unused_Pins` in `main.c`) 31 | - Headers for expansion / SWD / boot configuration 32 | 33 | ### Production Assets 34 | All fabrication outputs live in `hardware/PhaseLatchMini/production`: 35 | - `PhaseLatchMini.zip` – Gerber/drill bundle ready for PCB fab 36 | - `bom.csv` – Bill of Materials (with LCSC part numbers for quick JLCPCB sourcing) 37 | - `designators.csv` – Component reference list 38 | - `positions.csv` – Pick‑and‑place XY + rotation for SMT assembly 39 | - `netlist.ipc` – IPC netlist export 40 | 41 | Direct link (relative): `hardware/PhaseLatchMini/production/PhaseLatchMini.zip` 42 | 43 | ### BOM Highlights 44 | Representative components (from `bom.csv`): 45 | - Decoupling: multiple 100 nF 0402 caps (C307331) near MCU and filter sections 46 | - Filter / bulk caps: 22 nF, 220 nF, 470 nF mix shaping input response 47 | - Inductors: six 10 µH (0805) parts forming LC sections for each channel’s low‑pass filtering / supply isolation 48 | - USB-C receptacle: 16‑pin (HCTL HC-TYPE-C-16P-01A) with 5k1 CC resistors for proper orientation detection 49 | - Ferrite bead (FB1) for USB / supply noise suppression 50 | - Voltage regulator: MIC5504-3.3 (SOT‑23‑5) providing clean 3V3 51 | - Crystals: 8 MHz main, 32.768 kHz low‑speed (RTC / optional precise timing) 52 | - SMA edge connectors: matched pair for I and Q inputs 53 | 54 | ### Assembly Notes 55 | - Clean flux residues near high‑impedance analog nodes (C1/C8 cluster) to keep leakage low. 56 | - If performing hand assembly, solder the SMA edge connectors first to anchor board alignment, then USB, then fine‑pitch MCU. 57 | 58 | ### Filter Tuning 59 | Current passive network targets ~100 kHz corner. For alternative bandwidths: 60 | - Raise corner: decrease shunt capacitance (e.g., swap 470 nF → 220 nF or 22 nF depending on desired slope) or reduce inductance value. 61 | - Lower corner: increase shunt capacitance or employ higher inductance (space permitting). Recalculate fc ≈ 1/(2π√(L·C_eq)). 62 | - Mitigate source‑resistor induced tilt: include its resistance in design equations; keep initial shunt capacitors modest (≤10 nF) if a 50 Ω series is retained upstream. 63 | 64 | ## Firmware 65 | 66 | - Dual ADC synchronous sampling packed as 32-bit little-endian words (I 12-bit + Q 12-bit inside two 16-bit lanes) 67 | - Target sample rate `IQ_SAMPLE_RATE_HZ` (currently 210526) with dynamic TIM3 prescaler/period search (see `timer_trigger_init()` in `iq_adc.c`) 68 | - ADC sampling time set to `ADC_SAMPLETIME_28CYCLES_5` for higher throughput (previous higher value limited rate) 69 | - Circular DMA with half / full transfer interrupts 70 | - Lock-free ring queue feeding USB transmit path 71 | - ISR burst chaining (current chain limit 16 packets per IN completion) to minimize USB idle gaps 72 | - Immediate DMA IRQ packet scheduling ("kick") to prime first packet of each half-buffer 73 | - Optional diagnostic/stat packets suppressed in throughput builds 74 | - Host tools for diagnostics, throughput, raw capture, FIFO bridging, live view 75 | 76 | ### Changing the Sample Rate 77 | 1. Edit `Inc/iq_adc.h` and set `#define IQ_SAMPLE_RATE_HZ ` (I/Q pairs per second). 78 | 2. Ensure the new rate is feasible: TIM3 must be able to realize it given 16-bit ARR and prescaler; the search code picks the closest achievable value. 79 | 3. If pushing higher rates: 80 | - Consider reducing ADC sampling time (`ADC_SAMPLETIME_x`) further, but watch noise/performance. 81 | - Monitor USB throughput counters (`FEED` command) for increased busy skips. 82 | 4. Rebuild and flash. Optionally measure actual achieved rate by counting `iq_dma_half_count` + `iq_dma_full_count` over a timed host interval. 83 | 84 | ### Verifying Achieved Rate 85 | - After START, issue the `A` command periodically; note increments of half/full counts. 86 | - Effective samples/sec ≈ ((delta_half + delta_full) * half_buffer_samples) / time_interval. 87 | - Or capture a known duration with `host_test.py` and compute rows * rate. 88 | 89 | --- 90 | ## Repository Layout 91 | 92 | ``` 93 | platformio.ini # Build environments & macro flags 94 | src/ # Firmware sources (USB, ADC, main logic) 95 | Inc/ / include/ # Public headers 96 | host_*.py # Python host utilities 97 | lib/ test/ # (Placeholders / future use) 98 | ``` 99 | 100 | Key firmware files: 101 | - `src/main.c` – System init, main loop USB feeder (fallback scheduling & instrumentation) 102 | - `src/iq_adc.c` / `Inc/iq_adc.h` – ADC+DMA+Timer configuration, DMA IRQ scheduling kick 103 | - `src/usbd_conf.c` – Low-level USB callbacks, DataIn burst chaining logic 104 | - `src/usbd_cdc_if.c` – CDC interface, command parser (A/F/STATS) & data write API 105 | - `src/usbd_raw.c` (future / optional) – Raw class scaffolding (if transitioning from CDC for additional margin) 106 | 107 | Host scripts (root directory): 108 | - `host_diagnostics.py` – Passive capture, STATS delta decode, periodic monitor, USB descriptor info 109 | - `host_throughput.py` – PyUSB bulk endpoint benchmark (raw vendor class / future); shows packet stats 110 | - `host_raw_capture.py` – Robust raw USB (PyUSB or CDC) capture with quiet mode & BrokenPipe safety 111 | - `host_iq_fifo.py` – Serial CDC → named FIFO adapter (u8 or cf32) for GQRX / GNU Radio 112 | - `host_iq_live.py` – Terminal live IQ stats + sparklines / averages 113 | - `host_test.py` – Simple START/STOP based capture, optional float32 output & basic stats 114 | - `host_probe.py` – (Currently empty placeholder for future probing utilities) 115 | 116 | --- 117 | ## Build & Flash (PlatformIO) 118 | 119 | Prerequisites: 120 | - VS Code + PlatformIO extension (or `pio` CLI) 121 | 122 | Clone & open project, then build one of the defined environments: 123 | 124 | ### PlatformIO Quickstart (Basics) 125 | 126 | If you're new to PlatformIO, these are the minimal steps to get a firmware onto the board. 127 | 128 | #### Option A: VS Code GUI 129 | 1. Install the "PlatformIO IDE" extension in VS Code. 130 | 2. Open this project folder (where `platformio.ini` resides). 131 | 3. Bottom status bar: pick the desired environment (e.g. `adc`). 132 | 4. Click the checkmark (Build). Wait for success. 133 | 5. Connect the board via USB (ensure it enumerates) and press the right arrow (Upload). If first flash fails, press/hold BOOT (if available) or use a serial/USB boot jumper sequence depending on your Blue Pill variant. 134 | 6. Use the plug icon (Monitor) or run a host script to interact. 135 | 136 | #### Option B: CLI 137 | Install PlatformIO Core (Python 3.11+ recommended): 138 | ```bash 139 | pip install --upgrade platformio 140 | ``` 141 | From the project root: 142 | ```bash 143 | # List environments defined in platformio.ini 144 | pio run --list-targets 145 | 146 | # Build the adc environment 147 | pio run -e adc 148 | 149 | # Upload (adjust -e if choosing another env) 150 | pio run -e adc -t upload 151 | 152 | # Open a serial monitor (Ctrl+C to exit) 153 | pio device monitor -b 115200 154 | ``` 155 | If you see permission errors on macOS/Linux, you may need to adjust USB device permissions or add your user to the appropriate group (e.g. `dialout` on some Linux distros). 156 | 157 | #### Common Upload Notes 158 | - Some Blue Pill clones require setting/removing BOOT0 jumpers for the built-in ROM bootloader; after initial flash with a USB‑TTL adapter or ST-Link, subsequent USB CDC updates normally proceed automatically. 159 | - For ST-Link users, ensure drivers and udev rules (Linux) are installed. 160 | - If the device does not reset into the new firmware, press the hardware RESET button. 161 | 162 | #### Serial Device Identification 163 | On macOS you'll typically see `/dev/tty.usbmodem*`. On Linux, it may appear as `/dev/ttyACM0`. Use `pio device list` to enumerate. 164 | 165 | Environments (see `platformio.ini`): 166 | - `env:baseline` – Minimal USB baseline with reduced diagnostics (throughput focus pattern) 167 | - `env:diag` – Heavier diagnostics enabled (ASCII stat chatter) 168 | - `env:adc` – Active dual-ADC streaming (ENABLE_IQ + throughput suppressions) 169 | - `env:adc_smoke` – Lower-intensity validation (extra ADC status prints) 170 | 171 | Typical flow (using `env:adc`): 172 | 1. Select `adc` environment in PlatformIO 173 | 2. Build (should report flash usage ~30%, RAM ~55%) 174 | 3. Upload (DFU/serial depending on board configuration) 175 | 4. Open a serial monitor or run a host script (e.g. `python host_test.py --progress`) 176 | 177 | If enumeration stalls: power cycle board or press reset. Heartbeat LED indicates main loop alive; rapid fallback pattern indicates SysTick/USB early issues. 178 | 179 | --- 180 | ## Runtime Commands (CDC) 181 | 182 | Send as ASCII (line ending flexible, CR/LF accepted): 183 | - `START` – Begin IQ streaming (if not already running) 184 | - `STOP` – Halt streaming (host_test.py sends on exit) 185 | - `A` – Print `ADCSTAT` line with ADC & DMA counters 186 | - `F` – Print `FEED` line (USB feeder instrumentation) 187 | - `STATS` – Legacy status snapshot (may be suppressed in throughput builds) 188 | 189 | --- 190 | ## Data Format 191 | 192 | Each IQ sample pair is one 32-bit little-endian word composed of two 16‑bit containers produced by the STM32F1 dual regular simultaneous ADC mode: 193 | ``` 194 | Bits 0..11 : I (ADC1 12-bit result, right-aligned) 195 | Bits 12..15 : Unused (always 0) <-- upper 4 bits of lower 16-bit half-word 196 | Bits 16..27 : Q (ADC2 12-bit result, right-aligned) 197 | Bits 28..31 : Unused (always 0) <-- upper 4 bits of upper 16-bit half-word 198 | ``` 199 | 200 | So effectively: 201 | ``` 202 | uint16_t I_lane = word & 0xFFFF; // lower half-word 203 | uint16_t Q_lane = (word >> 16) & 0xFFFF; // upper half-word 204 | uint16_t I_raw12 = I_lane & 0x0FFF; // mask to 12 bits 205 | uint16_t Q_raw12 = Q_lane & 0x0FFF; 206 | ``` 207 | 208 | The current `host_test.py` treats each 16-bit lane as a full-range unsigned sample and subtracts 32768. That overstates magnitude and uses the high (always zero) 4 bits. For correct 12‑bit centered scaling use: 209 | ``` 210 | float I = ((int)I_raw12 - 2048) / 2048.0f; // ≈ [-1.0, +1.0) 211 | float Q = ((int)Q_raw12 - 2048) / 2048.0f; 212 | ``` 213 | If you need signed 16-bit containers for downstream tooling, you can expand by left-shifting 4 (to occupy high bits) or replicate into 16-bit signed with: 214 | ``` 215 | int16_t I_s16 = ((int16_t)(I_raw12 ^ 0x800) - 0x800) << 4; // optional dynamic range padding 216 | ``` 217 | But most host paths should just mask & center at 2048. 218 | 219 | --- 220 | ## ADC / Timing Configuration 221 | 222 | - Timer configuration is dynamic: `timer_trigger_init()` iterates prescaler values to find an ARR producing a rate closest to `IQ_SAMPLE_RATE_HZ`. 223 | - Current target: 210526 samples/sec (complex pairs). Achieved rate is printed only via manual inspection (no direct text output yet); you can instrument `(best_rate)` variable in debugger if needed. 224 | - ADC sampling time currently `ADC_SAMPLETIME_28CYCLES_5`; lowering further increases throughput but may degrade SNR and settling for higher source impedances. 225 | 226 | Potential Adjustments: 227 | - Increase `IQ_SAMPLE_RATE_HZ` (watch USB bandwidth & chain limit utilization). 228 | - Change ADC sampling time for performance vs accuracy trade-off. 229 | - Raise `chain_limit` (in `usbd_conf.c`) beyond 16 if the queue frequently stalls with residual samples and main loop fallback is minimal. 230 | 231 | --- 232 | ## USB Transfer Strategy & Optimizations 233 | - Chain limit now 16 (see `usbd_conf.c`); README previously referenced 12—updated. 234 | - DMA IRQ kick ensures first packet of each half-buffer is scheduled promptly. 235 | - Remaining headroom: adopt packed 3-byte I12|Q12 mode (roadmap) or vendor RAW class for further rate increases. 236 | 237 | --- 238 | ## Instrumentation & Monitoring 239 | 240 | Example FEED output (command `F`): 241 | ``` 242 | FEED loop=12345 isr=67890 busy=12 chain_max=11 243 | ``` 244 | Where: 245 | - `loop` – Packets scheduled from main loop (fallback) 246 | - `isr` – Packets scheduled from ISR path (burst + DMA kick) 247 | - `busy` – Attempts skipped because endpoint currently active 248 | - `chain_max` – Highest contiguous packets chained in a single IN completion 249 | 250 | ADCSTAT example (`A`): 251 | ``` 252 | ADCSTAT half=1024 full=1024 drops=0 253 | ``` 254 | (Exact field names may vary—check live output.) 255 | 256 | Use `host_diagnostics.py stats` for legacy STAT delta interpretation, if enabled in build. 257 | 258 | --- 259 | ## Host Utilities 260 | 261 | | Script | Purpose | 262 | |--------|---------| 263 | | `host_test.py` | Simple START capture, optional float32 output, stats, graceful Ctrl+C | 264 | | `host_iq_live.py` | Real-time terminal IQ level display, window averages, sparkline | 265 | | `host_iq_fifo.py` | Serial → FIFO converter (u8 or cf32) for GQRX / fifo consumers | 266 | | `host_raw_capture.py` | Raw byte dump with quiet & BrokenPipe-safe pipeline support | 267 | | `host_diagnostics.py` | STATS deltas, passive capture, USB descriptor info | 268 | | `host_throughput.py` | High-rate PyUSB bulk benchmark (raw class future) | 269 | | `host_probe.py` | Placeholder for future probing tools | 270 | 271 | ### GQRX Integration (via FIFO) 272 | - Set GQRX sample rate to match `IQ_SAMPLE_RATE_HZ` (e.g. 210526) or the nearest supported value. 273 | 1. Run FIFO bridge: 274 | ``` 275 | python3 host_iq_fifo.py --fifo /tmp/iq_cf32.iq 276 | ``` 277 | 2. In GQRX, set input to "UDP / File" or external source capable of reading named FIFO (Linux/macOS). Point to `/tmp/iq_cf32.fifo` with sample rate = `210526`. 278 | 3. Adjust gain/AGC in GQRX; confirm spectrum updates. 279 | 280 | ### Live View 281 | ``` 282 | python host_iq_live.py --port /dev/tty.usbmodemXYZ --window 512 --spark --interval 0.2 283 | ``` 284 | Shows min/max, window & running averages, and a small ASCII sparkline. 285 | 286 | ### Raw Capture Example 287 | ``` 288 | python host_raw_capture.py --out stream.bin --secs 5 --quiet 289 | ``` 290 | Then post-process with custom scripts or convert to complex floats. 291 | 292 | --- 293 | ## Performance Measurement 294 | 295 | Quick rate check (CDC path): 296 | ``` 297 | python host_test.py --seconds 3 --progress 298 | ``` 299 | Or FEED counters before & after a timed interval to estimate packet dispatch accumulation. 300 | 301 | PyUSB high-rate test (when using/adding raw vendor class): 302 | ``` 303 | python host_throughput.py --seconds 5 --progress 304 | ``` 305 | 306 | Interpretation updates: 307 | - Bytes/sec ÷ 4 = complex samples/sec (current raw 32-bit mode). 308 | - For future 3-byte packed mode bytes/sec ÷ 3 will apply. 309 | 310 | --- 311 | ## Troubleshooting 312 | 313 | | Symptom | Suggestions | 314 | |---------|-------------| 315 | | No `/dev/tty.usbmodem*` device | Replug USB, check cable, ensure board enumerates (dmesg / system log) | 316 | | Streaming stalls after START | Check FEED counters (isr incrementing?). Confirm no flood of diagnostics enabled. | 317 | | Throughput lower than expected | Ensure running `env:adc` (not `diag`), verify chain_max near limit, reduce host-side latency (no heavy printing) | 318 | | Mis-scaled IQ values | Mask to 12 bits (0x0FFF) before centering; ensure not interpreting high unused bits | 319 | | Broken pipe in pipeline capture | Use `host_raw_capture.py --quiet` (handles SIGPIPE and flush safety) | 320 | 321 | --- 322 | ## Roadmap / Future Ideas 323 | - Maintain or push beyond 210 kS/s (USB packing & endpoint tuning) 324 | - Optional tighter 24-bit (3-byte) packing (I12|Q12) – reduces bandwidth ~25% 325 | - Vendor RAW class endpoint for marginal gains over CDC ACM 326 | - Lightweight CRC / sequence tags for host-side drop detection 327 | - Optional AGC / scaling in firmware (convert to centered 16-bit signed) 328 | - Continuous integration tests for host scripts 329 | - IQ gain/phase auto-calibration 330 | - Expanded PhaseLatch Mini variants (higher‑resolution MCUs, external HS USB) 331 | 332 | ## License 333 | 334 | (Choose and add a SPDX license header & file as appropriate, e.g. MIT or Apache-2.0.) 335 | 336 | --- 337 | ## Attribution / Notes 338 | 339 | Developed as an incremental exploration of practical FS USB throughput & latency reduction techniques on resource-constrained MCUs while streaming synchronous dual-ADC data. 340 | 341 | Contributions / suggestions welcome. 342 | 343 | ## Getting a PCB 344 | 345 | ## Production & Manufacturing 346 | - The `hardware/production/` folder includes all needed files. 347 | 348 | This project is kindly sponsored by JLCPCB. They offer cheap, professional looking PCBs and super fast delivery. 349 | 350 | Step 1: Get the gerber file zip package from the /hardware folder 351 | 352 | Step 2: Upload to JLCPCB [https://jlcpcb.com/?from=Anders_N](https://jlcpcb.com/?from=Anders_N) 353 | 354 | Upload 355 | 356 | Step 3: Pick your color, surface finish and order. 357 | 358 | Select settings 359 | 360 | Save your choice 361 | 362 | 363 | You can use these affiliate links to get a board for $2 and also get $54 worth of New User Coupons at: https://jlcpcb.com/?from=Anders_N 364 | 365 | And in case you also want to order a 3D-printed case you can use this link. 366 | How to Get a $7 3D Printing Coupon: [https://3d.jlcpcb.com/?from=Anders3DP](https://jlc3dp.com/?from=Anders_N) 367 | 368 | 369 | -------------------------------------------------------------------------------- /hardware/PhaseLatchMini/production/netlist.ipc: -------------------------------------------------------------------------------- 1 | P CODE 00 2 | P UNITS CUST 0 3 | P arrayDim N 4 | 317GND VIA MD0118PA00X+081398Y-027854X0236Y0000R000S3 5 | 317GND VIA MD0118PA00X+080906Y-028150X0236Y0000R000S3 6 | 317GND VIA MD0118PA00X+080315Y-028740X0236Y0000R000S3 7 | 317GND VIA MD0118PA00X+080512Y-029331X0236Y0000R000S3 8 | 317GND VIA MD0118PA00X+081102Y-029891X0236Y0000R000S3 9 | 317/PA0 VIA MD0118PA00X+063287Y-026772X0236Y0000R000S3 10 | 317/PA0 VIA MD0118PA00X+063583Y-026476X0236Y0000R000S3 11 | 317/PA1 VIA MD0118PA00X+065207Y-027018X0236Y0000R000S3 12 | 317/PA1 VIA MD0118PA00X+064911Y-027313X0236Y0000R000S3 13 | 317GND VIA MD0118PA00X+058851Y-028970X0236Y0000R000S3 14 | 317GND VIA MD0118PA00X+060350Y-026290X0236Y0000R000S3 15 | 317GND VIA MD0118PA00X+060330Y-025230X0236Y0000R000S3 16 | 317GND VIA MD0118PA00X+060340Y-031630X0236Y0000R000S3 17 | 317GND VIA MD0118PA00X+060290Y-030660X0236Y0000R000S3 18 | 317+3V3 VIA MD0118PA00X+063350Y-032220X0236Y0000R000S3 19 | 317GND VIA MD0118PA00X+070400Y-031200X0236Y0000R000S3 20 | 317GND VIA MD0118PA00X+072047Y-026756X0236Y0000R000S3 21 | 317GND VIA MD0118PA00X+064331Y-028980X0236Y0000R000S3 22 | 317GND VIA MD0118PA00X+063248Y-029925X0236Y0000R000S3 23 | 317GND VIA MD0118PA00X+063681Y-027657X0236Y0000R000S3 24 | 317GND VIA MD0118PA00X+063400Y-025928X0236Y0000R000S3 25 | 317GND VIA MD0118PA00X+063050Y-026200X0236Y0000R000S3 26 | 317GND VIA MD0118PA00X+075315Y-029433X0236Y0000R000S3 27 | 317GND VIA MD0118PA00X+069213Y-027465X0236Y0000R000S3 28 | 317GND VIA MD0118PA00X+067638Y-027307X0236Y0000R000S3 29 | 317GND VIA MD0118PA00X+066909Y-027307X0236Y0000R000S3 30 | 317GND VIA MD0118PA00X+065945Y-027307X0236Y0000R000S3 31 | 317GND VIA MD0118PA00X+065374Y-027740X0236Y0000R000S3 32 | 317GND VIA MD0118PA00X+064803Y-028488X0236Y0000R000S3 33 | 317GND VIA MD0118PA00X+063744Y-028118X0236Y0000R000S3 34 | 317GND VIA MD0118PA00X+063410Y-028453X0236Y0000R000S3 35 | 317GND VIA MD0118PA00X+077598Y-026362X0236Y0000R000S3 36 | 317GND VIA MD0118PA00X+076476Y-026008X0236Y0000R000S3 37 | 317GND VIA MD0118PA00X+072953Y-026067X0236Y0000R000S3 38 | 317GND VIA MD0118PA00X+072106Y-026067X0236Y0000R000S3 39 | 317GND VIA MD0118PA00X+071417Y-026165X0236Y0000R000S3 40 | 317GND VIA MD0118PA00X+070689Y-026146X0236Y0000R000S3 41 | 317GND VIA MD0118PA00X+068307Y-026165X0236Y0000R000S3 42 | 317GND VIA MD0118PA00X+067303Y-026185X0236Y0000R000S3 43 | 317GND VIA MD0118PA00X+066339Y-026205X0236Y0000R000S3 44 | 317GND VIA MD0118PA00X+065157Y-026087X0236Y0000R000S3 45 | 317GND VIA MD0118PA00X+063287Y-027740X0236Y0000R000S3 46 | 317GND VIA MD0118PA00X+062360Y-029100X0236Y0000R000S3 47 | 317+3V3 VIA MD0118PA00X+078400Y-030780X0236Y0000R000S3 48 | 317GND VIA MD0118PA00X+078025Y-029984X0236Y0000R000S3 49 | 317GND VIA MD0118PA00X+076040Y-030420X0236Y0000R000S3 50 | 317GND VIA MD0118PA00X+076680Y-031040X0236Y0000R000S3 51 | 317GND VIA MD0118PA00X+075700Y-030960X0236Y0000R000S3 52 | 317GND VIA MD0118PA00X+076060Y-031340X0236Y0000R000S3 53 | 317GND VIA MD0118PA00X+077700Y-031843X0236Y0000R000S3 54 | 317GND VIA MD0118PA00X+080680Y-031560X0236Y0000R000S3 55 | 317GND VIA MD0118PA00X+081000Y-031260X0236Y0000R000S3 56 | 317GND VIA MD0118PA00X+079900Y-031252X0236Y0000R000S3 57 | 317GND VIA MD0118PA00X+077550Y-031250X0236Y0000R000S3 58 | 317GND VIA MD0118PA00X+078400Y-031843X0236Y0000R000S3 59 | 317GND VIA MD0118PA00X+079500Y-031843X0236Y0000R000S3 60 | 317GND VIA MD0118PA00X+062959Y-028903X0236Y0000R000S3 61 | 317GND VIA MD0118PA00X+064000Y-029600X0236Y0000R000S3 62 | 317GND VIA MD0118PA00X+067400Y-031000X0236Y0000R000S3 63 | 317GND VIA MD0118PA00X+065200Y-031200X0236Y0000R000S3 64 | 317GND VIA MD0118PA00X+076600Y-029350X0236Y0000R000S3 65 | 317GND VIA MD0118PA00X+071600Y-028250X0236Y0000R000S3 66 | 317GND VIA MD0118PA00X+074200Y-026550X0236Y0000R000S3 67 | 317GND VIA MD0118PA00X+075354Y-030752X0236Y0000R000S3 68 | 317GND VIA MD0118PA00X+072300Y-031550X0236Y0000R000S3 69 | 317GND VIA MD0118PA00X+074600Y-027800X0236Y0000R000S3 70 | 317GND VIA MD0118PA00X+076650Y-027350X0236Y0000R000S3 71 | 317GND VIA MD0118PA00X+072600Y-029600X0236Y0000R000S3 72 | 317GND VIA MD0118PA00X+071992Y-030480X0236Y0000R000S3 73 | 317GND VIA MD0118PA00X+072953Y-028272X0236Y0000R000S3 74 | 317GND VIA MD0118PA00X+071200Y-029450X0236Y0000R000S3 75 | 317GND VIA MD0118PA00X+072550Y-027950X0236Y0000R000S3 76 | 317GND VIA MD0118PA00X+078112Y-028000X0236Y0000R000S3 77 | 317GND VIA MD0118PA00X+073150Y-029350X0236Y0000R000S3 78 | 317+3V3 VIA MD0118PA00X+072300Y-027750X0236Y0000R000S3 79 | 317+3V3 VIA MD0118PA00X+071600Y-031350X0236Y0000R000S3 80 | 317+3V3 VIA MD0118PA00X+074400Y-028800X0236Y0000R000S3 81 | 317+3V3 VIA MD0118PA00X+074941Y-030811X0236Y0000R000S3 82 | 317+3V3 VIA MD0118PA00X+062700Y-026350X0236Y0000R000S3 83 | 317+3V3 VIA MD0118PA00X+069213Y-027976X0236Y0000R000S3 84 | 317/PA13 VIA MD0118PA00X+071800Y-030150X0236Y0000R000S3 85 | 317/PA14 VIA MD0118PA00X+072461Y-029965X0236Y0000R000S3 86 | 317/PB8 VIA MD0118PA00X+073940Y-030125X0236Y0000R000S3 87 | 317/PB8 VIA MD0118PA00X+077332Y-030048X0236Y0000R000S3 88 | 317/PB9 VIA MD0118PA00X+074272Y-030083X0236Y0000R000S3 89 | 317/PB9 VIA MD0118PA00X+076594Y-030102X0236Y0000R000S3 90 | 317/BOOT0 VIA MD0118PA00X+075256Y-031382X0236Y0000R000S3 91 | 317/PA0 VIA MD0118PA00X+073051Y-026851X0236Y0000R000S3 92 | 317/PA1 VIA MD0118PA00X+072050Y-027500X0236Y0000R000S3 93 | 317/PA0 VIA MD0118PA00X+061850Y-028530X0236Y0000R000S3 94 | 317/PA1 VIA MD0118PA00X+061880Y-029420X0236Y0000R000S3 95 | 327+3V3 R20 -2 A01X+064498Y-026732X0213Y0252R180S2 96 | 327/PA1 R20 -1 A01X+064899Y-026732X0213Y0252R180S2 97 | 327+3V3 R19 -2 A01X+064149Y-026727X0213Y0252R000S2 98 | 327/PA0 R19 -1 A01X+063747Y-026727X0213Y0252R000S2 99 | 327/PA0 R18 -2 A01X+063562Y-027123X0213Y0252R180S2 100 | 327GND R18 -1 A01X+063964Y-027123X0213Y0252R180S2 101 | 327/PA1 R17 -2 A01X+064717Y-027121X0213Y0252R000S2 102 | 327GND R17 -1 A01X+064315Y-027121X0213Y0252R000S2 103 | 327/PA1 R15 -1 A01X+061520Y-029266X0315Y0374R180S2 104 | 327NET-(L6-PAD2) R15 -2 A01X+060870Y-029266X0315Y0374R180S2 105 | 327NET-(C22-PAD1) R12 -1 A01X+059695Y-029260X0315Y0374R000S2 106 | 327NET-(L6-PAD2) R12 -2 A01X+060345Y-029260X0315Y0374R000S2 107 | 327NET-(C21-PAD1) R11 -1 A01X+061705Y-029840X0315Y0374R180S2 108 | 327NET-(L4-PAD2) R11 -2 A01X+061055Y-029840X0315Y0374R180S2 109 | 327NET-(C18-PAD1) R10 -1 A01X+062340Y-032677X0315Y0374R000S2 110 | 327NET-(L2-PAD2) R10 -2 A01X+062990Y-032677X0315Y0374R000S2 111 | 327/PA0 R9 -1 A01X+061512Y-028688X0315Y0374R180S2 112 | 327NET-(L5-PAD2) R9 -2 A01X+060863Y-028688X0315Y0374R180S2 113 | 327NET-(C15-PAD1) R8 -1 A01X+059690Y-028660X0315Y0374R000S2 114 | 327NET-(L5-PAD2) R8 -2 A01X+060340Y-028660X0315Y0374R000S2 115 | 327NET-(C19-PAD1) R6 -1 A01X+061701Y-028116X0315Y0374R180S2 116 | 327NET-(L1-PAD2) R6 -2 A01X+061052Y-028116X0315Y0374R180S2 117 | 327NET-(C14-PAD1) R5 -1 A01X+062350Y-025392X0315Y0374R000S2 118 | 327NET-(L1-PAD1) R5 -2 A01X+063000Y-025392X0315Y0374R000S2 119 | 327NET-(L6-PAD2) L6 -2 A01X+061182Y-030560X0344Y0472R180S2 120 | 327NET-(L4-PAD2) L6 -1 A01X+062018Y-030560X0344Y0472R180S2 121 | 327NET-(L1-PAD2) L5 -1 A01X+062049Y-027470X0344Y0472R180S2 122 | 327NET-(L5-PAD2) L5 -2 A01X+061212Y-027470X0344Y0472R180S2 123 | 327NET-(C22-PAD1) C23 -1 A01X+060710Y-030351X0220Y0244R090S2 124 | 327GND C23 -2 A01X+060710Y-030729X0220Y0244R090S2 125 | 327NET-(C22-PAD1) C22 -1 A01X+059229Y-029260X0220Y0244R180S2 126 | 327GND C22 -2 A01X+058851Y-029260X0220Y0244R180S2 127 | 327NET-(C21-PAD1) C21 -1 A01X+062140Y-030069X0220Y0244R270S2 128 | 327GND C21 -2 A01X+062140Y-029691X0220Y0244R270S2 129 | 327NET-(C18-PAD1) C18 -1 A01X+061790Y-032677X0354Y0374R180S2 130 | 327GND C18 -2 A01X+061180Y-032677X0354Y0374R180S2 131 | 327NET-(C14-PAD1) C17 -1 A01X+061800Y-025392X0354Y0374R180S2 132 | 327GND C17 -2 A01X+061190Y-025392X0354Y0374R180S2 133 | 327NET-(C15-PAD1) C16 -1 A01X+059218Y-028660X0220Y0244R180S2 134 | 327GND C16 -2 A01X+058840Y-028660X0220Y0244R180S2 135 | 327NET-(C15-PAD1) C15 -1 A01X+060701Y-027623X0220Y0244R270S2 136 | 327GND C15 -2 A01X+060701Y-027245X0220Y0244R270S2 137 | 327NET-(C14-PAD1) C14 -1 A01X+060710Y-025589X0220Y0244R270S2 138 | 327GND C14 -2 A01X+060710Y-025211X0220Y0244R270S2 139 | 327/~{NRST} C1 -1 A01X+073700Y-027200X0220Y0244R315S2 140 | 327GND C1 -2 A01X+073967Y-026933X0220Y0244R315S2 141 | 327+3V3 C8 -1 A01X+073434Y-026916X0220Y0244R315S2 142 | 327GND C8 -2 A01X+073701Y-026649X0220Y0244R315S2 143 | 327+3V3 C9 -1 A01X+071872Y-031250X0220Y0244R000S2 144 | 327GND C9 -2 A01X+072250Y-031250X0220Y0244R000S2 145 | 327+3V3 C10 -1 A01X+069633Y-028067X0220Y0244R315S2 146 | 327GND C10 -2 A01X+069900Y-027800X0220Y0244R315S2 147 | 327+3V3 C11 -1 A01X+074768Y-030205X0220Y0244R045S2 148 | 327GND C11 -2 A01X+075035Y-030472X0220Y0244R045S2 149 | 327NET-(C19-PAD1) C19 -1 A01X+062140Y-027971X0220Y0244R090S2 150 | 327GND C19 -2 A01X+062140Y-028349X0220Y0244R090S2 151 | 327NET-(C18-PAD1) C20 -1 A01X+060710Y-032411X0220Y0244R090S2 152 | 327GND C20 -2 A01X+060710Y-032789X0220Y0244R090S2 153 | 327GND J4 -A1 A01X+079526Y-030260X0236Y0512R270S2 154 | 327/VBUS J4 -A4 A01X+079526Y-029945X0236Y0512R270S2 155 | 327NET-(J4-CC1) J4 -A5 A01X+079526Y-029492X0118Y0512R270S2 156 | 327/D+ J4 -A6 A01X+079526Y-029098X0118Y0512R270S2 157 | 327/D- J4 -A7 A01X+079526Y-028902X0118Y0512R270S2 158 | 327J4-SBU1-PADA8) J4 -A8 A01X+079526Y-028508X0118Y0512R270S2 159 | 327/VBUS J4 -A9 A01X+079526Y-028055X0236Y0512R270S2 160 | 327GND J4 -A12 A01X+079526Y-027740X0236Y0512R270S2 161 | 327GND J4 -B1 A01X+079526Y-027740X0236Y0512R270S2 162 | 327/VBUS J4 -B4 A01X+079526Y-028055X0236Y0512R270S2 163 | 327NET-(J4-CC2) J4 -B5 A01X+079526Y-028311X0118Y0512R270S2 164 | 327/D+ J4 -B6 A01X+079526Y-028705X0118Y0512R270S2 165 | 327/D- J4 -B7 A01X+079526Y-029295X0118Y0512R270S2 166 | 327J4-SBU2-PADB8) J4 -B8 A01X+079526Y-029689X0118Y0512R270S2 167 | 327/VBUS J4 -B9 A01X+079526Y-029945X0236Y0512R270S2 168 | 327GND J4 -B12 A01X+079526Y-030260X0236Y0512R270S2 169 | 317GND J4 -S1 D0236PA00X+079778Y-030701X0394Y0827R270S0 170 | 317GND J4 -S1 D0236PA00X+081423Y-030701X0394Y0630R270S0 171 | 317GND J4 -S1 D0236PA00X+079778Y-027299X0394Y0827R270S0 172 | 317GND J4 -S1 D0236PA00X+081423Y-027299X0394Y0630R270S0 173 | 327NET-(L1-PAD1) L1 -1 A01X+062340Y-025963X0344Y0472R090S2 174 | 327NET-(L1-PAD2) L1 -2 A01X+062340Y-026800X0344Y0472R090S2 175 | 327NET-(C13-PAD2) L2 -1 A01X+061760Y-032073X0344Y0472R270S2 176 | 327NET-(L2-PAD2) L2 -2 A01X+061760Y-031237X0344Y0472R270S2 177 | 327NET-(C12-PAD2) L3 -1 A01X+061660Y-025963X0344Y0472R090S2 178 | 327NET-(L1-PAD1) L3 -2 A01X+061660Y-026800X0344Y0472R090S2 179 | 327NET-(L2-PAD2) L4 -1 A01X+062460Y-032060X0344Y0472R270S2 180 | 327NET-(L4-PAD2) L4 -2 A01X+062460Y-031223X0344Y0472R270S2 181 | 327NET-(J21-IN) C13 -1 A01X+060920Y-031196X0394Y0571R090S2 182 | 327NET-(C13-PAD2) C13 -2 A01X+060920Y-031944X0394Y0571R090S2 183 | 327NET-(J20-IN) C12 -1 A01X+060930Y-026784X0394Y0571R270S2 184 | 327NET-(C12-PAD2) C12 -2 A01X+060930Y-026036X0394Y0571R270S2 185 | 327/~{NRST} R4 -1 A01X+062920Y-027101X0213Y0252R270S2 186 | 327+3V3 R4 -2 A01X+062920Y-026699X0213Y0252R270S2 187 | 327NET-(U1-PC15) C7 -1 A01X+076650Y-028550X0220Y0244R090S2 188 | 327GND C7 -2 A01X+076650Y-028928X0220Y0244R090S2 189 | 327NET-(U1-PD0) C3 -1 A01X+076300Y-028189X0220Y0244R270S2 190 | 327GND C3 -2 A01X+076300Y-027811X0220Y0244R270S2 191 | 317GND J3 -1 D0394PA00X+080000Y-025500X0669Y0669R090S0 192 | 317/PB8 J3 -2 D0394PA00X+079000Y-025500X0669Y0000R090S0 193 | 317/PB9 J3 -3 D0394PA00X+078000Y-025500X0669Y0000R090S0 194 | 317/PC13 J3 -4 D0394PA00X+077000Y-025500X0669Y0000R090S0 195 | 317/~{NRST} J3 -5 D0394PA00X+076000Y-025500X0669Y0000R090S0 196 | 317/PA0 J3 -6 D0394PA00X+075000Y-025500X0669Y0000R090S0 197 | 317/PA1 J3 -7 D0394PA00X+074000Y-025500X0669Y0000R090S0 198 | 317/PA2 J3 -8 D0394PA00X+073000Y-025500X0669Y0000R090S0 199 | 317/PA3 J3 -9 D0394PA00X+072000Y-025500X0669Y0000R090S0 200 | 317/PA4 J3 -10 D0394PA00X+071000Y-025500X0669Y0000R090S0 201 | 317/PA5 J3 -11 D0394PA00X+070000Y-025500X0669Y0000R090S0 202 | 317/PA6 J3 -12 D0394PA00X+069000Y-025500X0669Y0000R090S0 203 | 317/PA7 J3 -13 D0394PA00X+068000Y-025500X0669Y0000R090S0 204 | 317/PB0 J3 -14 D0394PA00X+067000Y-025500X0669Y0000R090S0 205 | 317/PB1 J3 -15 D0394PA00X+066000Y-025500X0669Y0000R090S0 206 | 317/PB10 J3 -16 D0394PA00X+065000Y-025500X0669Y0000R090S0 207 | 317/PB11 J3 -17 D0394PA00X+064000Y-025500X0669Y0000R090S0 208 | 327NET-(J4-CC2) R14 -1 A01X+081303Y-026008X0213Y0252R180S2 209 | 327GND R14 -2 A01X+080902Y-026008X0213Y0252R180S2 210 | 327NET-(J20-IN) J20 -1 A01X+059500Y-026788X0500Y1260R090S2 211 | 327GND J20 -2 A01X+059598Y-027900X0531Y1457R090S2 212 | 327GND J20 -2 A04X+059598Y-027900X0531Y1457R090S1 213 | 327GND J20 -2 A01X+059598Y-025676X0531Y1457R090S2 214 | 327GND J20 -2 A04X+059598Y-025676X0531Y1457R090S1 215 | 327/PB2 R2 -2 A01X+067795Y-028528X0213Y0252R090S2 216 | 327/BOOT1 R2 -1 A01X+067795Y-028126X0213Y0252R090S2 217 | 317GND J6 -1 D0394PA00X+080000Y-026500X0669Y0669R090S0 218 | 317+5V J6 -2 D0394PA00X+079000Y-026500X0669Y0000R090S0 219 | 327+3V3 C6 -1 A01X+077874Y-030772X0394Y0571R180S2 220 | 327GND C6 -2 A01X+077126Y-030772X0394Y0571R180S2 221 | 327NET-(U1-PD0) Y1 -1 A01X+075669Y-027888X0551Y0472R270S2 222 | 327GND Y1 -2 A01X+075669Y-027022X0551Y0472R270S2 223 | 327NET-(U1-PD1) Y1 -3 A01X+075000Y-027022X0551Y0472R270S2 224 | 327GND Y1 -4 A01X+075000Y-027888X0551Y0472R270S2 225 | 327NET-(J4-CC1) R13 -1 A01X+078840Y-030973X0213Y0252R270S2 226 | 327GND R13 -2 A01X+078840Y-030571X0213Y0252R270S2 227 | 327NET-(U1-PC14) C4 -1 A01X+075872Y-029500X0220Y0244R000S2 228 | 327GND C4 -2 A01X+076250Y-029500X0220Y0244R000S2 229 | 327NET-(D1-A) R7 -2 A01X+063040Y-031618X0213Y0252R270S2 230 | 327+3V3 R7 -1 A01X+063040Y-032020X0213Y0252R270S2 231 | 317GND J1 -1 D0394PA00X+080000Y-032500X0669Y0669R090S0 232 | 317+3V3 J1 -2 D0394PA00X+079000Y-032500X0669Y0000R090S0 233 | 317/PB7 J1 -3 D0394PA00X+078000Y-032500X0669Y0000R090S0 234 | 317/PB6 J1 -4 D0394PA00X+077000Y-032500X0669Y0000R090S0 235 | 317/PB5 J1 -5 D0394PA00X+076000Y-032500X0669Y0000R090S0 236 | 317/PB4 J1 -6 D0394PA00X+075000Y-032500X0669Y0000R090S0 237 | 317/PB3 J1 -7 D0394PA00X+074000Y-032500X0669Y0000R090S0 238 | 317/PA15 J1 -8 D0394PA00X+073000Y-032500X0669Y0000R090S0 239 | 317/D+ J1 -9 D0394PA00X+072000Y-032500X0669Y0000R090S0 240 | 317/D- J1 -10 D0394PA00X+071000Y-032500X0669Y0000R090S0 241 | 317/PA10 J1 -11 D0394PA00X+070000Y-032500X0669Y0000R090S0 242 | 317/PA9 J1 -12 D0394PA00X+069000Y-032500X0669Y0000R090S0 243 | 317/PA8 J1 -13 D0394PA00X+068000Y-032500X0669Y0000R090S0 244 | 317/PB15 J1 -14 D0394PA00X+067000Y-032500X0669Y0000R090S0 245 | 317/PB14 J1 -15 D0394PA00X+066000Y-032500X0669Y0000R090S0 246 | 317/PB13 J1 -16 D0394PA00X+065000Y-032500X0669Y0000R090S0 247 | 317/PB12 J1 -17 D0394PA00X+064000Y-032500X0669Y0000R090S0 248 | 317+3V3 J5 -1 D0394PA00X+066000Y-028000X0669Y0669R000S0 249 | 317+3V3 J5 -2 D0394PA00X+067000Y-028000X0669Y0000R000S0 250 | 317/BOOT0 J5 -3 D0394PA00X+066000Y-029000X0669Y0000R000S0 251 | 317/BOOT1 J5 -4 D0394PA00X+067000Y-029000X0669Y0000R000S0 252 | 317GND J5 -5 D0394PA00X+066000Y-030000X0669Y0000R000S0 253 | 317GND J5 -6 D0394PA00X+067000Y-030000X0669Y0000R000S0 254 | 327+3V3 U1 -1 A01X+073974Y-028518X0581Y0118R135S2 255 | 327/PC13 U1 -2 A01X+073835Y-028378X0581Y0118R135S2 256 | 327NET-(U1-PC14) U1 -3 A01X+073696Y-028239X0581Y0118R135S2 257 | 327NET-(U1-PC15) U1 -4 A01X+073557Y-028100X0581Y0118R135S2 258 | 327NET-(U1-PD0) U1 -5 A01X+073418Y-027961X0581Y0118R135S2 259 | 327NET-(U1-PD1) U1 -6 A01X+073278Y-027822X0581Y0118R135S2 260 | 327/~{NRST} U1 -7 A01X+073139Y-027682X0581Y0118R135S2 261 | 327GND U1 -8 A01X+073000Y-027543X0581Y0118R135S2 262 | 327+3V3 U1 -9 A01X+072861Y-027404X0581Y0118R135S2 263 | 327/PA0 U1 -10 A01X+072722Y-027265X0581Y0118R135S2 264 | 327/PA1 U1 -11 A01X+072582Y-027126X0581Y0118R135S2 265 | 327/PA2 U1 -12 A01X+072443Y-026986X0581Y0118R135S2 266 | 327/PA3 U1 -13 A01X+071657Y-026986X0118Y0581R135S2 267 | 327/PA4 U1 -14 A01X+071518Y-027126X0118Y0581R135S2 268 | 327/PA5 U1 -15 A01X+071378Y-027265X0118Y0581R135S2 269 | 327/PA6 U1 -16 A01X+071239Y-027404X0118Y0581R135S2 270 | 327/PA7 U1 -17 A01X+071100Y-027543X0118Y0581R135S2 271 | 327/PB0 U1 -18 A01X+070961Y-027682X0118Y0581R135S2 272 | 327/PB1 U1 -19 A01X+070822Y-027822X0118Y0581R135S2 273 | 327/PB2 U1 -20 A01X+070682Y-027961X0118Y0581R135S2 274 | 327/PB10 U1 -21 A01X+070543Y-028100X0118Y0581R135S2 275 | 327/PB11 U1 -22 A01X+070404Y-028239X0118Y0581R135S2 276 | 327GND U1 -23 A01X+070265Y-028378X0118Y0581R135S2 277 | 327+3V3 U1 -24 A01X+070126Y-028518X0118Y0581R135S2 278 | 327/PB12 U1 -25 A01X+070126Y-029304X0581Y0118R135S2 279 | 327/PB13 U1 -26 A01X+070265Y-029443X0581Y0118R135S2 280 | 327/PB14 U1 -27 A01X+070404Y-029582X0581Y0118R135S2 281 | 327/PB15 U1 -28 A01X+070543Y-029722X0581Y0118R135S2 282 | 327/PA8 U1 -29 A01X+070682Y-029861X0581Y0118R135S2 283 | 327/PA9 U1 -30 A01X+070822Y-030000X0581Y0118R135S2 284 | 327/PA10 U1 -31 A01X+070961Y-030139X0581Y0118R135S2 285 | 327/D- U1 -32 A01X+071100Y-030278X0581Y0118R135S2 286 | 327/D+ U1 -33 A01X+071239Y-030418X0581Y0118R135S2 287 | 327/PA13 U1 -34 A01X+071378Y-030557X0581Y0118R135S2 288 | 327GND U1 -35 A01X+071518Y-030696X0581Y0118R135S2 289 | 327+3V3 U1 -36 A01X+071657Y-030835X0581Y0118R135S2 290 | 327/PA14 U1 -37 A01X+072443Y-030835X0118Y0581R135S2 291 | 327/PA15 U1 -38 A01X+072582Y-030696X0118Y0581R135S2 292 | 327/PB3 U1 -39 A01X+072722Y-030557X0118Y0581R135S2 293 | 327/PB4 U1 -40 A01X+072861Y-030418X0118Y0581R135S2 294 | 327/PB5 U1 -41 A01X+073000Y-030278X0118Y0581R135S2 295 | 327/PB6 U1 -42 A01X+073139Y-030139X0118Y0581R135S2 296 | 327/PB7 U1 -43 A01X+073278Y-030000X0118Y0581R135S2 297 | 327NET-(U1-BOOT0) U1 -44 A01X+073418Y-029861X0118Y0581R135S2 298 | 327/PB8 U1 -45 A01X+073557Y-029722X0118Y0581R135S2 299 | 327/PB9 U1 -46 A01X+073696Y-029582X0118Y0581R135S2 300 | 327GND U1 -47 A01X+073835Y-029443X0118Y0581R135S2 301 | 327+3V3 U1 -48 A01X+073974Y-029304X0118Y0581R135S2 302 | 327/VBUS FB1 -1 A01X+078309Y-027576X0344Y0374R180S2 303 | 327+5V FB1 -2 A01X+077689Y-027576X0344Y0374R180S2 304 | 327NET-(D1-A) D1 -2 A01X+063131Y-031080X0384Y0551R180S2 305 | 327/PB12 D1 -1 A01X+063869Y-031080X0384Y0551R180S2 306 | 327GND SW3 -2 A01X+064248Y-027760X0669Y0394R270S2 307 | 327GND SW3 -2 A01X+064248Y-030240X0669Y0394R270S2 308 | 327/~{NRST} SW3 -1 A01X+062752Y-027760X0669Y0394R270S2 309 | 327/~{NRST} SW3 -1 A01X+062752Y-030240X0669Y0394R270S2 310 | 317GND J2 -1 D0394PA00X+068600Y-027500X0669Y0669R000S0 311 | 317/PA14 J2 -2 D0394PA00X+068600Y-028500X0669Y0000R000S0 312 | 317/PA13 J2 -3 D0394PA00X+068600Y-029500X0669Y0000R000S0 313 | 317+3V3 J2 -4 D0394PA00X+068600Y-030500X0669Y0000R000S0 314 | 327/D+ R3 -1 A01X+070916Y-030916X0213Y0252R045S2 315 | 327+3V3 R3 -2 A01X+071200Y-031200X0213Y0252R045S2 316 | 327+5V U5 -1 A01X+078399Y-029128X0522Y0236R090S2 317 | 327GND U5 -2 A01X+078025Y-029128X0522Y0236R090S2 318 | 327+5V U5 -3 A01X+077651Y-029128X0522Y0236R090S2 319 | 327D-(U5-NC-PAD4) U5 -4 A01X+077651Y-030024X0522Y0236R090S2 320 | 327+3V3 U5 -5 A01X+078399Y-030024X0522Y0236R090S2 321 | 327/BOOT0 R1 -1 A01X+074610Y-030894X0213Y0252R225S2 322 | 327NET-(U1-BOOT0) R1 -2 A01X+074327Y-030610X0213Y0252R225S2 323 | 327NET-(J21-IN) J21 -1 A01X+059500Y-031189X0500Y1260R090S2 324 | 327GND J21 -2 A01X+059598Y-032302X0531Y1457R090S2 325 | 327GND J21 -2 A04X+059598Y-032302X0531Y1457R090S1 326 | 327GND J21 -2 A01X+059598Y-030077X0531Y1457R090S2 327 | 327GND J21 -2 A04X+059598Y-030077X0531Y1457R090S1 328 | 327NET-(U1-PC15) Y2 -1 A01X+076142Y-028850X0394Y0709R000S2 329 | 327NET-(U1-PC14) Y2 -2 A01X+075158Y-028850X0394Y0709R000S2 330 | 327NET-(U1-PD1) C2 -1 A01X+076300Y-026761X0220Y0244R090S2 331 | 327GND C2 -2 A01X+076300Y-027139X0220Y0244R090S2 332 | 327+5V C5 -1 A01X+077706Y-028360X0394Y0571R000S2 333 | 327GND C5 -2 A01X+078454Y-028360X0394Y0571R000S2 334 | 999 335 | -------------------------------------------------------------------------------- /host_iq_fifo.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | """ 3 | Bridge STM32 dual ADC (F103) CDC/raw streamed 32-bit packed IQ (ADC1+ADC2) into a FIFO as 4 | interleaved complex samples for GQRX (File I/Q input) or other SDR tools. 5 | 6 | Packing (STM32F1 dual regular simultaneous): 7 | Bits 0..11 : ADC1 result -> I (12-bit right-aligned) 8 | Bits 12..15 : zero padding 9 | Bits 16..27 : ADC2 result -> Q (12-bit right-aligned) 10 | Bits 28..31 : zero padding 11 | 12 | Earlier versions mis-decodeds Q at bit 12; fixed to use bit 16. 13 | 14 | Output format: 15 | cf32 : float32 complex (recommended for GQRX) written as contiguous pairs 16 | 17 | Recommended for GQRX now: use --format cf32. In GQRX "File" I/Q input dialog select: 18 | Format: Complex float 32 (if available) OR use external pipe via gnuradio/Soapy for cf32. 19 | If your GQRX build lacks native cf32 file mode, use an external converter to feed cf32 into GQRX. 20 | 21 | Typical run for GQRX (220 kHz stream, prebuffer to avoid underrun): 22 | python host_iq_fifo.py --format cf32 --fifo /tmp/iq_cf32.iq --rate 220000 --prebuf 65536 23 | 24 | Then in GQRX: 25 | 1. Set input device to "File" and point to /tmp/iq_cf32.iq 26 | 2. Sample rate: 220000 (or downsample inside GQRX DSP chain) 27 | 3. Disable Loop so it treats growth as live 28 | 4. Start after prebuffer completes 29 | 30 | Use --swap-iq-output if downstream expects Q,I ordering. 31 | Use --force-phase 0 in the new pure stream (no stray text) — automatic heuristics can be bypassed. 32 | 33 | Ctrl+C to stop; script reports throughput and drop metrics. 34 | """ 35 | import argparse, glob, os, sys, time, math, statistics, stat, fcntl, struct, select, tty, termios 36 | from collections import deque 37 | 38 | try: 39 | import serial 40 | except ImportError: 41 | print('ERROR: pyserial required (pip install pyserial)', file=sys.stderr) 42 | sys.exit(2) 43 | 44 | DEFAULT_RATE = 140000 # complex samples/second (for info only) 45 | 46 | ASCII_SKIP_THRESHOLD = 0.90 # skip chunk if mostly printable text and contains 'STAT' 47 | 48 | 49 | def find_port(explicit=None): 50 | if explicit: 51 | return explicit 52 | ports = sorted(glob.glob('/dev/tty.usbmodem*')) 53 | if not ports: 54 | raise SystemExit('No /dev/tty.usbmodem* device found') 55 | return ports[0] 56 | 57 | def ensure_fifo(path): 58 | if os.path.exists(path): 59 | st_mode = os.stat(path).st_mode 60 | if not stat.S_ISFIFO(st_mode): 61 | raise SystemExit(f'Path {path} exists and is not a FIFO') 62 | else: 63 | os.mkfifo(path) 64 | 65 | 66 | def decode_block(buf, out_container, *, zero_i=False, zero_q=False, invert_q=False, phase_correction_deg=0.0): 67 | """Decode packed 32-bit IQ words into cf32 float32 pairs (little-endian dual12). 68 | 69 | Parameters: 70 | buf : bytes-like, length multiple of 4 71 | out_container: list to which packed ' FIFO 8-bit I/Q bridge for GQRX') 157 | ap.add_argument('--port', help='Explicit serial port path') 158 | ap.add_argument('--baud', type=int, default=115200) 159 | ap.add_argument('--fifo', default='iq_fifo.iq', help='FIFO path (created if absent)') 160 | ap.add_argument('--capture', help='Optional file to also dump FIFO output (cf32)') 161 | ap.add_argument('--timeout', type=float, default=0.01, help='Serial read timeout seconds') 162 | ap.add_argument('--prebuf', type=int, default=0, help='Bytes to accumulate before writing to FIFO (startup cushion)') 163 | ap.add_argument('--chunk', type=int, default=4096, help='Serial read chunk size') 164 | ap.add_argument('--skip-stat', action='store_true', help='(Legacy) Skip printable ASCII chunks containing STAT (OFF by default; may cause misalignment if binary matches)') 165 | ap.add_argument('--format', choices=['cf32'], default='cf32', help='Output sample format for FIFO (cf32 only)') 166 | ap.add_argument('--zero-i', action='store_true', help='Force I channel to zero (debug)') 167 | ap.add_argument('--zero-q', action='store_true', help='Force Q channel to zero (debug)') 168 | ap.add_argument('--analyze', type=int, metavar='N', help='Capture first N 32-bit words, analyze, then continue streaming') 169 | ap.add_argument('--dump-words', action='store_true', help='With --analyze, also print the raw 32-bit words in hex') 170 | ap.add_argument('--dump-tail', type=int, metavar='N', help='Keep last N 32-bit raw words and print them on exit (helps inspect end of stream / after corruption)') 171 | ap.add_argument('--raw-capture', metavar='FILE', help='Capture ALL raw incoming bytes (pre-decoding) to FILE for offline inspection (CAUTION: grows indefinitely)') 172 | # map mode forced to 'dual12' (STM32F1 dual regular simultaneous layout) 173 | # Alignment/auto-realign options removed to keep the host deterministic 174 | # Initial alignment is assumed correct; manual numeric phase discard (0..3) 175 | ap.add_argument('--log-phase', type=int, metavar='N', help='Every N samples, log current block start offset (diagnostic)') 176 | # bias-phase0 removed (legacy tie-break behavior removed) 177 | # (spectral-align and spectral-align-test options removed) 178 | ap.add_argument('--exit-after-analyze', action='store_true', help='Perform analysis block then continue streaming unless --force-exit-after-analyze also given') 179 | ap.add_argument('--force-exit-after-analyze', action='store_true', help='With --exit-after-analyze, actually exit after analysis (legacy behavior)') 180 | ap.add_argument('--analyze-before-fifo', action='store_true', help='Perform --analyze / --dump-words before opening FIFO (avoids waiting for reader)') 181 | # IRR/image-rejection related options removed per request (measure_irr, irr-*, tone-*) 182 | ap.add_argument('--probe-layout', type=int, metavar='N', help='Capture N words and display multiple interpretations (endianness, lane positions)') 183 | # Removed legacy/diagnostic options: swap-lanes, big-endian-words, invert-q, 184 | # swap-iq-output, dc-remove/dc-alpha, image-cancel related options, 185 | # periodic-sync-block and related IRR-after-cancel options. 186 | # alpha-neighbors (image-cancel stability) removed 187 | ap.add_argument('--dump-first-hex', type=int, metavar='N', help='Dump first N raw serial bytes in hex (before alignment) and keep streaming') 188 | ap.add_argument('--diag', action='store_true', help='Enable runtime diagnostics: log serial chunk sizes, timing gaps, small-packet counts (helps debug ticking/underrun)') 189 | ap.add_argument('--diag-only', action='store_true', help='Diagnostics-only: do not open FIFO or write decoded output (isolates serial timing from FIFO backpressure)') 190 | ap.add_argument('--measure-skew', action='store_true', help='Continuously measure I/Q skew at a specified tone frequency and print (deg, ns)') 191 | ap.add_argument('--tone-hz', type=float, default=7000.0, help='Tone frequency in Hz used for skew measurement (default 7000)') 192 | ap.add_argument('--fs', type=float, default=180000.0, help='Sampling rate in Hz (default 180000). Adjust to your board/sample rate') 193 | ap.add_argument('--skew-interval', type=float, default=1.0, help='Minimum seconds between skew reports (default 1.0s)') 194 | ap.add_argument('--skew-correction-deg', type=float, default=0.0, help='Apply a fixed phase correction (degrees) to complex samples before writing FIFO (positive rotates I+jQ by +deg)') 195 | ap.add_argument('--skew-correction-ns', type=float, default=-2000.0, help='Apply a fractional delay (ns) to Q channel (positive delays Q)') 196 | ap.add_argument('--skew-step-ns', type=float, default=1.0, help='Step size in ns when adjusting skew interactively (default 1.0 ns)') 197 | ap.add_argument('--trace-phase', action='store_true', help='Log block-start byte offset (mod 4) and report changes (diagnostic)') 198 | ap.add_argument('--lock-start-phase', action='store_true', help='Lock initial byte phase after startup -- ignore later phase-change requests') 199 | ap.add_argument('--initial-discard', type=int, default=3, help='Discard N leading bytes from stream at startup (useful to adjust byte-phase). Values are taken modulo 4.') 200 | # periodic alignment check removed (no periodic alignment diagnostics) 201 | # (strip-sync-frame removed: firmware no longer inserts periodic sync frames) 202 | args = ap.parse_args() 203 | if args.initial_discard: 204 | print(f"[phase] initial-discard requested: {int(args.initial_discard) % 4} byte(s)") 205 | # Removed legacy CLI flags and auto-realignment handling to keep the host 206 | # stream simple and deterministic. The host now assumes correct framing 207 | # and only provides manual numeric phase discard and diagnostic tracing. 208 | 209 | # (spectral-align-test removed) 210 | # Convenience: allow --dump-words alone; default to analyzing first 64 words 211 | if args.dump_words and not args.analyze: 212 | args.analyze = 64 213 | print(f"[info] --dump-words specified without --analyze; defaulting to analyze first {args.analyze} words", file=sys.stderr) 214 | # If user asked only for probe pre-FIFO, auto-set analyze length 215 | if args.probe_layout and args.analyze_before_fifo and not args.analyze: 216 | args.analyze = args.probe_layout 217 | print(f"[info] --probe-layout implies --analyze {args.analyze} pre-FIFO", file=sys.stderr) 218 | 219 | port = find_port(args.port) 220 | ser = serial.Serial(port, args.baud, timeout=args.timeout) 221 | if args.dump_first_hex: 222 | pre_hex = ser.read(args.dump_first_hex) 223 | print(f"[dump-first-hex] {len(pre_hex)} bytes: {' '.join(f'{b:02X}' for b in pre_hex)}") 224 | # Reinject these bytes into initial alignment path 225 | globals()['_prefetched_initial_bytes'] = pre_hex 226 | 227 | # Early analysis path BEFORE FIFO open if requested 228 | if args.analyze and args.analyze_before_fifo: 229 | target_bytes = args.analyze * 4 230 | buf = bytearray() 231 | print(f"[pre-analyze] Capturing {args.analyze} words ({target_bytes} bytes) before FIFO open...") 232 | # IRR pre-analyze checks removed 233 | while len(buf) < target_bytes: 234 | chunk = ser.read(args.chunk) 235 | if '_prefetched_initial_bytes' in globals(): 236 | chunk = globals().pop('_prefetched_initial_bytes') + chunk 237 | if not chunk: 238 | continue 239 | buf.extend(chunk) 240 | # (spectral alignment removed) no phase_shift applied here 241 | phase_shift = 0 242 | # Ensure we still have full target_bytes after shift; if not, pull more bytes or pad failure. 243 | if phase_shift: 244 | # Attempt to read extra bytes to compensate for discarded leading bytes 245 | while len(buf) < target_bytes + phase_shift: 246 | chunk = ser.read(args.chunk) 247 | if '_prefetched_initial_bytes' in globals(): 248 | chunk = globals().pop('_prefetched_initial_bytes') + chunk 249 | if not chunk: 250 | break 251 | buf.extend(chunk) 252 | aligned_full_len = target_bytes 253 | if len(buf) - phase_shift < target_bytes: 254 | aligned_full_len = len(buf) - phase_shift - (len(buf) - phase_shift) % 4 255 | print(f"[pre-analyze] Warning: insufficient bytes after shift; using {aligned_full_len//4} words instead of {args.analyze}.") 256 | aligned = buf[phase_shift:phase_shift+aligned_full_len] 257 | actual_words = len(aligned)//4 258 | if actual_words != args.analyze: 259 | print(f"[pre-analyze] Adjusted analyze word count: {actual_words}") 260 | words = [] 261 | for off in range(0, actual_words*4, 4): 262 | # Always interpret incoming stream as little-endian 32-bit words 263 | w = aligned[off] | (aligned[off+1] << 8) | (aligned[off+2] << 16) | (aligned[off+3] << 24) 264 | words.append(w) 265 | if args.dump_words: 266 | print("RAW WORDS:") 267 | for i,w in enumerate(words): 268 | print(f" {i:04d}: 0x{w:08X}") 269 | # analyze_samples removed: alignment/metric diagnostics disabled 270 | # IRR pre-analyze measurement removed 271 | if args.probe_layout: 272 | probe_layout(words[:args.probe_layout]) 273 | if args.exit_after_analyze and args.force_exit_after_analyze: 274 | print("[pre-analyze] Force-exit requested after analysis.") 275 | ser.close() 276 | return 277 | # continue streaming -> fall through to FIFO setup 278 | 279 | fifo_fd = None 280 | fifo_path = args.fifo 281 | if not args.diag_only: 282 | fifo_path = args.fifo 283 | ensure_fifo(fifo_path) 284 | print(f"FIFO ready: {fifo_path} (waiting for reader when opened) format={args.format}") 285 | 286 | # Open FIFO for write only; on macOS need reader first or open blocks. 287 | while fifo_fd is None: 288 | try: 289 | fifo_fd = os.open(fifo_path, os.O_WRONLY | os.O_NONBLOCK) 290 | except OSError: 291 | if args.analyze_before_fifo and args.analyze and args.exit_after_analyze and args.force_exit_after_analyze: 292 | # Already analyzed and user wants to exit; break early 293 | print("[info] Analysis complete and force-exit requested; skipping FIFO wait.") 294 | ser.close() 295 | return 296 | print("Waiting for reader (start GQRX File I/Q)...") 297 | time.sleep(0.5) 298 | # Switch to blocking 299 | fl = fcntl.fcntl(fifo_fd, fcntl.F_GETFL) 300 | fcntl.fcntl(fifo_fd, fcntl.F_SETFL, fl & ~os.O_NONBLOCK) 301 | 302 | capture_fh = open(args.capture,'wb') if args.capture else None 303 | raw_capture_fh = open(args.raw_capture,'wb') if args.raw_capture else None 304 | tail_words = deque(maxlen=args.dump_tail) if args.dump_tail else None 305 | 306 | partial = b'' 307 | total_samples = 0 308 | total_bytes_out = 0 309 | start_time = time.time() 310 | prebuf_store = bytearray() 311 | 312 | analyzed = False 313 | analyze_words = [] 314 | # Diagnostics state (enabled with --diag) 315 | last_chunk_time = None 316 | gap_threshold = 0.01 # seconds; gaps larger than this are suspicious (10 ms) 317 | diag_last_summary = time.time() 318 | diag_chunk_count = 0 319 | diag_small_chunk_count = 0 320 | diag_total_bytes = 0 321 | # Runtime phase control 322 | bytes_processed = 0 # total bytes consumed from stream (for phase arithmetic) 323 | pending_discard = int(args.initial_discard) % 4 # bytes to discard to achieve requested phase (0..3) 324 | current_phase = 0 325 | term_orig = None 326 | term_fd = None 327 | term_has_cb = False 328 | # (DC removal and image-cancel features removed — stream is passed through) 329 | # initial alignment state removed 330 | # Skew measurement state 331 | last_skew_time = 0.0 332 | # Trace/lock diagnostics 333 | locked_phase = None 334 | last_traced_phase = None 335 | # Track last reported skew correction so interactive changes are visible 336 | last_reported_skew_correction = None 337 | # Q-history buffer for fractional-delay skew correction 338 | q_history = deque() 339 | max_q_history = 8192 340 | # (Periodic sync-frame removal state removed; firmware no longer injects frames) 341 | 342 | # Alignment/phase-metric functions removed: host will assume phase0 framing and not auto-realign. 343 | 344 | 345 | 346 | try: 347 | # If running in an interactive terminal, enable cbreak mode to capture single key presses 348 | try: 349 | if sys.stdin.isatty(): 350 | term_fd = sys.stdin.fileno() 351 | term_orig = termios.tcgetattr(term_fd) 352 | tty.setcbreak(term_fd) 353 | term_has_cb = True 354 | print('[info] Phase control: press 0/1/2/3 to set byte-phase while running') 355 | except Exception: 356 | term_has_cb = False 357 | 358 | while True: 359 | chunk = ser.read(args.chunk) 360 | if '_prefetched_initial_bytes' in globals(): 361 | chunk = globals().pop('_prefetched_initial_bytes') + chunk 362 | now = time.time() 363 | if not chunk: 364 | # no data returned in this read; treat as a short gap 365 | if args.diag: 366 | if last_chunk_time is not None: 367 | gap = now - last_chunk_time 368 | if gap > gap_threshold: 369 | print(f"[diag] NO-CHUNK gap: {gap*1000:.1f} ms since last chunk") 370 | continue 371 | # diagnostics: timestamp, chunk size, detect gap 372 | if args.diag: 373 | if last_chunk_time is not None: 374 | gap = now - last_chunk_time 375 | if gap > gap_threshold: 376 | print(f"[diag] GAP: {gap*1000:.1f} ms between serial chunks (len={len(chunk)})") 377 | last_chunk_time = now 378 | diag_chunk_count += 1 379 | diag_total_bytes += len(chunk) 380 | if len(chunk) < 32: 381 | diag_small_chunk_count += 1 382 | # per-second summary 383 | if now - diag_last_summary >= 1.0: 384 | avg_bps = diag_total_bytes / max(1.0, now - diag_last_summary) 385 | print(f"[diag-summary] chunks={diag_chunk_count} small_chunks={diag_small_chunk_count} bytes_last_sec={diag_total_bytes} avg_bps~{avg_bps:.0f}") 386 | # reset per-second counters 387 | diag_chunk_count = 0 388 | diag_small_chunk_count = 0 389 | diag_total_bytes = 0 390 | diag_last_summary = now 391 | # (Periodic sync-frame stripping removed; firmware no longer injects frames) 392 | if args.skip_stat: 393 | printable = sum(32 <= b < 127 for b in chunk) 394 | if printable/len(chunk) > ASCII_SKIP_THRESHOLD and b'STAT' in chunk: 395 | continue 396 | if raw_capture_fh: 397 | raw_capture_fh.write(chunk) 398 | 399 | # Check for user keypresses (non-blocking). If user pressed 0..3, schedule phase change. 400 | if term_has_cb: 401 | try: 402 | r,_,_ = select.select([sys.stdin], [], [], 0) 403 | if r: 404 | # read up to 3 bytes to capture arrow key sequences like '\x1b[A' (up) or '\x1b[B' (down) 405 | chs = sys.stdin.read(3) 406 | if not chs: 407 | continue 408 | # numeric phase control (0..3) 409 | for ch in chs: 410 | if ch in ('0','1','2','3'): 411 | desired = int(ch) 412 | if locked_phase is not None: 413 | print(f"[phase] Locked to {locked_phase}; ignoring requested phase change to {desired}") 414 | else: 415 | need = (desired - (bytes_processed % 4)) % 4 416 | pending_discard = need 417 | print(f"[phase] Requested phase={desired} (will discard {need} byte(s) when available)") 418 | break 419 | # arrow keys: ESC [ C = right, ESC [ D = left 420 | # Right arrow increases fractional skew (ns), Left arrow decreases it. 421 | if chs.startswith('\x1b[C') or chs.startswith('\x1b\x1b[C'): 422 | args.skew_correction_ns = float(args.skew_correction_ns) + float(args.skew_step_ns) 423 | print(f"[skew] increased delay -> {args.skew_correction_ns:.3f} ns") 424 | elif chs.startswith('\x1b[D') or chs.startswith('\x1b\x1b[D'): 425 | args.skew_correction_ns = float(args.skew_correction_ns) - float(args.skew_step_ns) 426 | print(f"[skew] decreased delay -> {args.skew_correction_ns:.3f} ns") 427 | # also allow + and - keys for adjustment (phase rotation) 428 | if '+' in chs: 429 | args.skew_correction_deg = float(args.skew_correction_deg) + 1.0 430 | print(f"[skew] increased correction -> {args.skew_correction_deg:.3f} deg") 431 | if '-' in chs: 432 | args.skew_correction_deg = float(args.skew_correction_deg) - 1.0 433 | print(f"[skew] decreased correction -> {args.skew_correction_deg:.3f} deg") 434 | # (Left/Right arrows now control skew ns; '['/']' keys removed) 435 | except Exception: 436 | # if stdin not selectable, ignore 437 | pass 438 | 439 | # (Periodic sync-block detection removed) 440 | 441 | # Initial alignment and auto-realign logic removed — assume stream is phase-0 and aligned. 442 | # Always append incoming chunk directly to partial (no auto-shifting performed). 443 | partial += chunk 444 | 445 | # If a phase-change discard is pending, try to apply it now (if enough bytes buffered) 446 | if pending_discard: 447 | if len(partial) >= pending_discard: 448 | # Drop the requested number of bytes from partial to align framing 449 | partial = partial[pending_discard:] 450 | bytes_processed += pending_discard 451 | current_phase = (current_phase + pending_discard) % 4 452 | print(f"[phase] Applied discard of {pending_discard} byte(s). New byte offset mod4 = {bytes_processed % 4}") 453 | pending_discard = 0 454 | else: 455 | # wait for more bytes to arrive 456 | continue 457 | 458 | # (Initial alignment logic moved earlier; this block intentionally removed) 459 | 460 | # auto-realign behavior is disabled; no initial alignment performed 461 | # auto-realign removed: host will not attempt to auto-shift stream 462 | # Trace and optional lock of start-phase (before we cut full words) 463 | start_phase = bytes_processed % 4 464 | if args.trace_phase: 465 | if start_phase != last_traced_phase: 466 | print(f"[trace] block_start_offset_mod4={start_phase} bytes_processed={bytes_processed} partial_len={len(partial)}") 467 | last_traced_phase = start_phase 468 | if args.lock_start_phase and locked_phase is None: 469 | locked_phase = start_phase 470 | print(f"[phase] Locked start-phase to {locked_phase} (will ignore later phase change requests)") 471 | 472 | if len(partial) < 4: 473 | continue 474 | cut = len(partial) - (len(partial) % 4) 475 | block = partial[:cut] 476 | partial = partial[cut:] 477 | bytes_processed += cut 478 | # Optional analysis path (first N words) 479 | if args.analyze and not analyzed: 480 | for off in range(0, len(block), 4): 481 | if len(analyze_words) < args.analyze: 482 | # Interpret as little-endian 32-bit words 483 | w = block[off] | (block[off+1] << 8) | (block[off+2] << 16) | (block[off+3] << 24) 484 | analyze_words.append(w) 485 | if len(analyze_words) >= args.analyze: 486 | if args.dump_words: 487 | print("RAW WORDS:") 488 | for i,w in enumerate(analyze_words): 489 | print(f" {i:04d}: 0x{w:08X}") 490 | # analyze_samples removed: alignment/metric diagnostics disabled 491 | if args.probe_layout: 492 | probe_layout(analyze_words[:args.probe_layout]) 493 | analyzed = True 494 | if args.exit_after_analyze and args.force_exit_after_analyze: 495 | print('[stream-analyze] Force-exit requested after analysis inside streaming path. Exiting.') 496 | break 497 | # Tail capture path (last N words across entire session) 498 | if tail_words is not None: 499 | for off in range(0, len(block), 4): 500 | w = block[off] | (block[off+1] << 8) | (block[off+2] << 16) | (block[off+3] << 24) 501 | tail_words.append(w) 502 | # (Skew measurement moved to after correction so reported values reflect 503 | # the residual skew after rotation/fractional-delay have been applied.) 504 | # Only cf32 output supported 505 | # If the user requested a fractional-delay skew correction (ns), decode into 506 | # float lists and apply the fractional delay to Q before packing. 507 | if abs(float(args.skew_correction_ns)) > 0.0: 508 | # Report changes in skew-correction value 509 | if last_reported_skew_correction is None or float(args.skew_correction_ns) != float(last_reported_skew_correction): 510 | print(f"[skew] applying fractional delay {float(args.skew_correction_ns):.3f} ns") 511 | last_reported_skew_correction = float(args.skew_correction_ns) 512 | I_list, Q_list = decode_block_to_lists(block, zero_i=args.zero_i, zero_q=args.zero_q, invert_q=False, phase_correction_deg=args.skew_correction_deg) 513 | outbuf = apply_fractional_delay_and_pack(I_list, Q_list, float(args.skew_correction_ns), args.fs, q_history, max_q_history) 514 | else: 515 | pieces = [] 516 | # Report skew-correction degrees when it changes so user can observe effect 517 | if last_reported_skew_correction is None or float(args.skew_correction_deg) != float(last_reported_skew_correction): 518 | print(f"[skew] applying rotation {float(args.skew_correction_deg):.3f} deg") 519 | last_reported_skew_correction = float(args.skew_correction_deg) 520 | decode_block(block, pieces, zero_i=args.zero_i, zero_q=args.zero_q, invert_q=False, phase_correction_deg=args.skew_correction_deg) 521 | # Pass-through stream: no DC removal or image-cancel processing 522 | outbuf = b''.join(pieces) 523 | # Measure skew AFTER any correction has been applied so printed values 524 | # reflect the residual skew seen by the downstream (cf32) stream. 525 | if args.measure_skew: 526 | now = time.time() 527 | if now - last_skew_time >= args.skew_interval: 528 | res = measure_skew_from_cf32(outbuf, args.fs, args.tone_hz) 529 | if res is not None: 530 | deg, ns, magI, magQ = res 531 | print(f"[skew] tone={args.tone_hz}Hz phase_diff={deg:.3f} deg skew={ns:.1f} ns magI={magI:.3f} magQ={magQ:.3f}") 532 | last_skew_time = now 533 | 534 | total_samples += len(outbuf)//8 # 8 bytes per complex sample 535 | if not args.diag_only: 536 | if args.prebuf and len(prebuf_store) < args.prebuf: 537 | prebuf_store.extend(outbuf) 538 | if len(prebuf_store) >= args.prebuf: 539 | try: 540 | os.write(fifo_fd, prebuf_store) 541 | except BrokenPipeError: 542 | print("FIFO reader closed during prebuffer write; exiting.") 543 | break 544 | total_bytes_out += len(prebuf_store) 545 | prebuf_store.clear() 546 | continue 547 | try: 548 | os.write(fifo_fd, outbuf) 549 | except BrokenPipeError: 550 | print("FIFO reader closed; exiting.") 551 | break 552 | total_bytes_out += len(outbuf) 553 | if capture_fh: 554 | capture_fh.write(outbuf) 555 | # Periodic alignment check removed 556 | # Periodic progress every ~2 seconds 557 | if total_samples and (total_samples % (DEFAULT_RATE*2) == 0): 558 | elapsed = time.time() - start_time 559 | rate = total_samples/elapsed if elapsed>0 else 0.0 560 | print(f"Samples={total_samples} OutBytes={total_bytes_out} Rate={rate:.1f} sps ({rate/1000:.1f} ksps) fmt={args.format}") 561 | # IRR streaming capture removed 562 | except KeyboardInterrupt: 563 | pass 564 | finally: 565 | ser.close() 566 | if capture_fh: 567 | capture_fh.close() 568 | if raw_capture_fh: 569 | raw_capture_fh.close() 570 | # Restore terminal state if we enabled cbreak/raw mode 571 | try: 572 | if term_has_cb and term_orig is not None and term_fd is not None: 573 | termios.tcsetattr(term_fd, termios.TCSADRAIN, term_orig) 574 | except Exception: 575 | pass 576 | # Only close FIFO if we actually opened one (diag-only skips opening) 577 | try: 578 | if fifo_fd is not None: 579 | os.close(fifo_fd) 580 | except Exception: 581 | pass 582 | elapsed = time.time()-start_time 583 | if elapsed>0: 584 | print(f"Final: samples={total_samples} avgRate={total_samples/elapsed:.1f} sps") 585 | # sync-frame stripping removed 586 | if tail_words is not None and len(tail_words): 587 | print(f"TAIL RAW WORDS (last {len(tail_words)}):") 588 | base_index = total_samples - len(tail_words) 589 | for i,w in enumerate(tail_words): 590 | print(f" {base_index + i:08d}: 0x{w:08X}") 591 | 592 | # validate_alignment removed: alignment validation/auto-realign is disabled in the simplified host 593 | 594 | # IRR / image-rejection helpers removed per request 595 | 596 | def probe_layout(words, *, swap_lanes=False, big_endian=False): 597 | """Print a brief layout interpretation of first <=16 words.""" 598 | count = min(len(words), 16) 599 | print(f"[probe-layout] showing {count} words swap_lanes={swap_lanes}") 600 | for i in range(count): 601 | w = words[i] 602 | lo16 = w & 0xFFFF 603 | hi16 = (w >> 16) & 0xFFFF 604 | i12 = w & 0x0FFF 605 | q12 = (w >> 16) & 0x0FFF 606 | if swap_lanes: 607 | i_out, q_out = q12, i12 608 | else: 609 | i_out, q_out = i12, q12 610 | print(f" {i:02d}: w=0x{w:08X} lo16=0x{lo16:04X} hi16=0x{hi16:04X} I12={i_out:03X} Q12={q_out:03X}") 611 | 612 | 613 | def decode_block_to_lists(buf, *, zero_i=False, zero_q=False, invert_q=False, phase_correction_deg=0.0): 614 | """Decode packed 32-bit words into two Python lists of floats (I_list, Q_list). 615 | 616 | This mirrors decode_block but returns numeric lists instead of packed bytes so we can 617 | apply fractional-delay skew correction to Q before packing. 618 | """ 619 | I = [] 620 | Q = [] 621 | scale = 2048.0 622 | center = 2048 623 | if phase_correction_deg: 624 | rad = phase_correction_deg * (math.pi/180.0) 625 | rot_cos = math.cos(rad) 626 | rot_sin = math.sin(rad) 627 | else: 628 | rot_cos = 1.0; rot_sin = 0.0 629 | off = 0 630 | for off in range(0, len(buf), 4): 631 | b0 = buf[off]; b1 = buf[off+1]; b2 = buf[off+2]; b3 = buf[off+3] 632 | i16 = b0 | (b1 << 8) 633 | q16 = b2 | (b3 << 8) 634 | i_raw = i16 & 0x0FFF 635 | q_raw = q16 & 0x0FFF 636 | if zero_i: i_raw = center 637 | if zero_q: q_raw = center 638 | if invert_q and not zero_q: 639 | q_raw = (center*2 - q_raw) 640 | fi = (i_raw - center) / scale 641 | fq = (q_raw - center) / scale 642 | if rot_sin != 0.0: 643 | ri = fi * rot_cos - fq * rot_sin 644 | rq = fi * rot_sin + fq * rot_cos 645 | else: 646 | ri = fi; rq = fq 647 | I.append(ri) 648 | Q.append(rq) 649 | return I, Q 650 | 651 | 652 | def apply_fractional_delay_and_pack(I, Q, skew_ns, fs, q_history, max_q_history): 653 | """Apply fractional delay (ns) to Q channel and pack interleaved cf32 bytes. 654 | 655 | Uses linear interpolation across combined q_history + Q to produce delayed Q samples. 656 | Positive skew_ns delays Q (i.e., Q_out[n] = Q_in[n - d]). 657 | """ 658 | if not Q: 659 | return b'' 660 | # If no skew requested, pack directly 661 | if abs(skew_ns) < 1e-12: 662 | pack = struct.pack 663 | out = bytearray() 664 | for ri, rq in zip(I, Q): 665 | out.extend(pack(' max_q_history: 669 | q_history.popleft() 670 | return bytes(out) 671 | 672 | # compute fractional sample delay 673 | d = float(skew_ns) * float(fs) / 1e9 674 | combined_q = list(q_history) + Q 675 | N_hist = len(q_history) 676 | NQ = len(Q) 677 | out = bytearray() 678 | pack = struct.pack 679 | for n in range(NQ): 680 | src = n + N_hist - d 681 | k = int(math.floor(src)) 682 | frac = src - k 683 | if k < 0: 684 | k = 0; frac = 0.0 685 | if k+1 >= len(combined_q): 686 | qk = combined_q[-1] 687 | qk1 = combined_q[-1] 688 | else: 689 | qk = combined_q[k] 690 | qk1 = combined_q[k+1] 691 | q_out = (1.0 - frac) * qk + frac * qk1 692 | out.extend(pack(' max_q_history: 697 | q_history.popleft() 698 | return bytes(out) 699 | 700 | 701 | def measure_skew_from_raw(block, fs, f0): 702 | """Return (phase_diff_deg, skew_ns, magI, magQ) measured from raw 32-bit packed IQ words in block. 703 | 704 | block: bytes with length multiple of 4 (little-endian 32-bit words) 705 | fs: sampling rate (Hz) 706 | f0: tone frequency (Hz) 707 | Method: accumulate C = sum_n x[n] * exp(-j*2*pi*f0*n/fs) for I and Q separately. 708 | """ 709 | import math 710 | N = len(block) // 4 711 | if N <= 0: 712 | return None 713 | delta = 2.0 * math.pi * f0 / fs 714 | # rot = exp(-j*delta) 715 | rot_r = math.cos(delta) 716 | rot_i = -math.sin(delta) 717 | pr = 1.0; pi = 0.0 718 | CI_r = CI_i = CQ_r = CQ_i = 0.0 719 | center = 2048 720 | scale = 2048.0 721 | off = 0 722 | for n in range(N): 723 | b0 = block[off]; b1 = block[off+1]; b2 = block[off+2]; b3 = block[off+3] 724 | off += 4 725 | i16 = b0 | (b1 << 8) 726 | q16 = b2 | (b3 << 8) 727 | i_raw = i16 & 0x0FFF 728 | q_raw = q16 & 0x0FFF 729 | fi = (i_raw - center) / scale 730 | fq = (q_raw - center) / scale 731 | CI_r += fi * pr; CI_i += fi * pi 732 | CQ_r += fq * pr; CQ_i += fq * pi 733 | # rotate phasor by rot (pr + j pi) *= rot_r + j rot_i 734 | tmp_pr = pr * rot_r - pi * rot_i 735 | tmp_pi = pr * rot_i + pi * rot_r 736 | pr, pi = tmp_pr, tmp_pi 737 | 738 | angleI = math.atan2(CI_i, CI_r) 739 | angleQ = math.atan2(CQ_i, CQ_r) 740 | phase_diff = angleQ - angleI 741 | # normalize to [-pi, pi] 742 | while phase_diff <= -math.pi: phase_diff += 2*math.pi 743 | while phase_diff > math.pi: phase_diff -= 2*math.pi 744 | # skew in seconds 745 | skew_s = phase_diff / (2.0 * math.pi * f0) if f0 != 0 else 0.0 746 | skew_ns = skew_s * 1e9 747 | magI = math.hypot(CI_r, CI_i) 748 | magQ = math.hypot(CQ_r, CQ_i) 749 | return (phase_diff * 180.0 / math.pi, skew_ns, magI, magQ) 750 | 751 | 752 | def measure_skew_from_cf32(cf32_bytes, fs, f0): 753 | """Measure skew from cf32 bytes (interleaved ). 754 | 755 | Returns (phase_diff_deg, skew_ns, magI, magQ) or None on error. 756 | """ 757 | import struct, math 758 | if not cf32_bytes: 759 | return None 760 | N = len(cf32_bytes) // 8 761 | if N <= 0: 762 | return None 763 | delta = 2.0 * math.pi * f0 / fs 764 | rot_r = math.cos(delta) 765 | rot_i = -math.sin(delta) 766 | pr = 1.0; pi = 0.0 767 | CI_r = CI_i = CQ_r = CQ_i = 0.0 768 | off = 0 769 | for n in range(N): 770 | # unpack I (float32) then Q (float32) 771 | ri = struct.unpack_from(' math.pi: phase_diff -= 2*math.pi 786 | skew_s = phase_diff / (2.0 * math.pi * f0) if f0 != 0 else 0.0 787 | skew_ns = skew_s * 1e9 788 | magI = math.hypot(CI_r, CI_i) 789 | magQ = math.hypot(CQ_r, CQ_i) 790 | return (phase_diff * 180.0 / math.pi, skew_ns, magI, magQ) 791 | 792 | # Ensure script executes main() when run directly. 793 | if __name__ == '__main__': 794 | main() 795 | --------------------------------------------------------------------------------