|
| 1 | +mAIkroscope Galvo Interface Board – **Extended Hardware & Firmware Documentation** |
| 2 | +Rev A – 26 May 2025 |
| 3 | + |
| 4 | + |
| 5 | + |
| 6 | +### 1 Purpose & Key Features |
| 7 | + |
| 8 | +* Dual-axis galvo driver interface for UC2 microscopes. |
| 9 | +* Seeed XIAO ESP32-S3 MCU handles scan synthesis and trigger generation. |
| 10 | +* On-board 12-bit SPI DAC (MCP4822) creates ±10 V differential XY signals for low-cost Chinese galvo drivers. |
| 11 | +* Three independent 50 Ω trigger outputs (pixel, line, frame). |
| 12 | +* CAN 2.0B bus for remote control via UC2-CAN protocol. |
| 13 | +* Integrated ±12 V/3.3 V/5 V analog rails, 2 A output budget for external driver modules.  |
| 14 | + |
| 15 | +This is a scan using the Flimlabs.com software, their detector and their pulsed lasers. This is a 512x512 pixel^2 area |
| 16 | + |
| 17 | + |
| 18 | + |
| 19 | + |
| 20 | + |
| 21 | + |
| 22 | +### 2 System Architecture |
| 23 | + |
| 24 | +``` |
| 25 | +ESP32-S3 ↔ SPI ↔ MCP4822 → LM324 diff amps → SMA L± / R± |
| 26 | + │ │ ├─ pixel/line/frame triggers |
| 27 | + │ │ └─ U.FL laser blanking |
| 28 | + └─ TWAI CAN bus → SN65HVD230 → JST-XH backbone |
| 29 | +``` |
| 30 | + |
| 31 | +* **Two-phase DAC update:** write 12-bit sample into DAC register (CS low), then pulse **LDAC** to latch both channels simultaneously; saves one SPI transaction per point.  |
| 32 | + |
| 33 | + |
| 34 | + |
| 35 | + |
| 36 | +### 3 Electrical Specifications |
| 37 | + |
| 38 | +| Parameter | Value | Comment | | |
| 39 | +| - | -- | | - | |
| 40 | +| Backbone input | 12 V ±5 % | via J1001-1 | | |
| 41 | +| Board self-consumption | ≈ 50 mA | excludes external galvo drivers | | |
| 42 | +| Galvo supply pass-thru | ±12 V, 2 A max | J1004/J1007/J1008 | | |
| 43 | +| DAC resolution / range | 12 bit, 0 – 3.3V at 4096 steps | converted to ±10 V diff | | |
| 44 | +| Output bandwidth | ≈ 25 kHz (-3 dB) | limited by LM324 stage | | |
| 45 | +| Trigger level | 3.3 V CMOS, 50 Ω back-terminated | SMA J1009-J1011 | | |
| 46 | +| CAN bus | ISO11898-2, 1 Mbit/s default | 120 Ω terminator selectable (JP2001) | | |
| 47 | + |
| 48 | + |
| 49 | + |
| 50 | + |
| 51 | +### 4 Connectors & Pinouts (summary) |
| 52 | + |
| 53 | +* **Power + CAN (J1001, JST-XH-4):** GND, +12 V, CAN\_H - CAN\_L |
| 54 | +* **Galvo power out (J1004 big Molex 3.96 mm, J1007/J1008 KF2510):** ±12 V, GND – pin order matches driver boards. |
| 55 | +* **Differential XY (J1005/J1006, SMA-3):** L+, L-, GND and R+, R-, GND. |
| 56 | +* **Triggers (SMA-2):** Pixel (J1009), Line (J1010), Frame (J1011). |
| 57 | +* **Laser blanking (J10001, U.FL):** single-ended 50 Ω. |
| 58 | +* **XIAO headers (J1002/J1003):** break out all ESP32-S3 I/O; see GPIO map in §7.  |
| 59 | + |
| 60 | + |
| 61 | + |
| 62 | +### 5 Analog Signal Chain |
| 63 | + |
| 64 | +1. **MCP4822-E/SN** generates 0-3.3 V (12-bit). |
| 65 | +2. **Gain/offset network (LM324)** maps 0 V→0 V, ±2.048 V→±10 V. |
| 66 | +3. Second LM324 stage inverts each leg, giving true differential L± and R±. |
| 67 | +4. Outputs drive ≥ 20 mA into the 10 kΩ inputs of typical galvo amps. |
| 68 | +5. LC filters on ±12 VA rails set 50 Hz cutoff to suppress switching noise.  |
| 69 | + |
| 70 | + |
| 71 | + |
| 72 | +### 6 Power Subsystem |
| 73 | + |
| 74 | +* **TPS5430 buck:** 12 V → 3.3 V @ 0.8 A for MCU/CAN. |
| 75 | +* **TPS5430 inverting buck-boost:** 12 V → −12 V @ 1.5 A for op-amps. |
| 76 | +* **78L05 linear:** 5 V A for DAC/reference. |
| 77 | +* All rails filtered by π-LC networks; testpoints TP400x provided for bring-up.  |
| 78 | + |
| 79 | + |
| 80 | + |
| 81 | +### 7 Firmware Overview |
| 82 | + |
| 83 | +```cpp |
| 84 | +// Default pin mapping (Arduino style) on XIAO systems |
| 85 | +#define PIN_DAC_CS 8 // J1003-10 / GPIO8 |
| 86 | +#define PIN_DAC_SCK 7 // J1003-9 / GPIO7 |
| 87 | +#define PIN_DAC_SDI 9 // J1003-11 / GPIO9 |
| 88 | +#define PIN_DAC_LDAC 6 // J1002-6 / GPIO6 |
| 89 | +#define PIN_TRIG_PIXEL 2 // GPIO2 |
| 90 | +#define PIN_TRIG_LINE 3 // GPIO3 |
| 91 | +#define PIN_TRIG_FRAME 4 // GPIO4 |
| 92 | +#define PIN_CAN_TX 5 // GPIO5 |
| 93 | +#define PIN_CAN_RX 44 // GPIO44 |
| 94 | +#define PIN_LASER 43 // GPIO43 |
| 95 | +#define PIN_PUSHBUTTON 1 // GPIO1 (enable internal pull-up) |
| 96 | +``` |
| 97 | +
|
| 98 | +* **Loop structure:** |
| 99 | +
|
| 100 | + ```text |
| 101 | + for each Y line: |
| 102 | + for each X pixel: |
| 103 | + write X,Y to DAC; assert TRIG_PIXEL |
| 104 | + assert TRIG_LINE |
| 105 | + assert TRIG_FRAME |
| 106 | + ``` |
| 107 | +* **Coordinate depth:** 12 bit full-scale → map image size (e.g. 512 × 512) by `coord = (pixel * 4095) / (size-1)`. |
| 108 | +* **PlatformIO project:** clone `openUC2/galvo-interface-fw`, select `env:xiao_esp32s3`, build & upload via USB-C. |
| 109 | +* **UC2-CAN commands:** |
| 110 | +*Coming soon* |
| 111 | + |
| 112 | + |
| 113 | + |
| 114 | +### 8 Typical Operating Procedure |
| 115 | + |
| 116 | +1. Feed 12 V from microscope backbone into J1001-1 (CAN connecotr); connect CAN (optional), you can supply 12v/GND via the JST 4 Pin sconnector. |
| 117 | +2. Connect each galvo driver board with 3-pin power and SMA signal leads. |
| 118 | +3. Flash firmware; set parameters in firmware for min/max pixel and stepsize as well as pixel dwell time |
| 119 | +4. Monitor pixel trigger on oscilloscope |
| 120 | +5. Tune galvo PID on driver boards for minimal overshoot |
| 121 | + |
| 122 | +### 9 iBOM |
| 123 | + |
| 124 | +<iframe width="100%" style={{"aspect-ratio": "16 / 9"}} src="https://openuc2.github.io/kicad/ibom-galvo.html" title="iBOM" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" referrerpolicy="strict-origin-when-cross-origin" allowfullscreen></iframe> |
| 125 | + |
| 126 | + |
| 127 | + |
| 128 | +### 10 Firmware Architecture (openUC2-LaserScanner) |
| 129 | + |
| 130 | +You can find the files here https://github.com/openUC2/openUC2-LaserScanner/blob/main/src/main.cpp |
| 131 | + |
| 132 | +The UC2-ESP Firmware also has the CAN-enabled version of it in its code base here: https://github.com/youseetoo/uc2-esp32/blob/main/main/src/scanner/GalvoController.cpp |
| 133 | + |
| 134 | +:::warn |
| 135 | +Sometimes the XIAO cannot be flashed immediately, for this you have to press boot and reset while firmware is flashed - or erase the firmware first using this tool: https://espressif.github.io/esptool-js/ |
| 136 | +::: |
| 137 | + |
| 138 | + |
| 139 | +| Layer | Key files | Function | |
| 140 | +| ----------------------- | ------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | |
| 141 | +| **Application** | `src/main.cpp` | Instantiates `SPIRenderer`, updates parameters in a `while(1)` loop, and calls `start()`; disables the watchdog and raises log level to warn for GPIO. ([GitHub][1]) | |
| 142 | +| **Render Engine** | `src/SPIRenderer.{h,cpp}` | Converts image geometry into DAC samples, handles SPI transfers, and asserts pixel/line/frame triggers with single–cycle GPIO writes. Implements `setParameters()`, `draw()`, `start()`. ([GitHub][2]) | |
| 143 | +| **Board-select macros** | same header | `IS_XIAO`, `IS_XIAO_UC2GALVOBOARD` choose the correct pin map at compile time. ([GitHub][3]) | |
| 144 | +| **Build system** | `platformio.ini` | Two build environments: **esp32dev** (generic) and **UC2\_3\_Xiao** (Seeed XIAO ESP32-S3). Both use ESP-IDF with Arduino component; UART at 921 kbit s⁻¹. ([GitHub][4]) | |
| 145 | + |
| 146 | +#### 11.1 File structure |
| 147 | + |
| 148 | +``` |
| 149 | +openUC2-LaserScanner |
| 150 | +├── src/ # C++ sources |
| 151 | +│ ├── main.cpp # app_main() entry point |
| 152 | +│ ├── SPIRenderer.cpp # raster engine |
| 153 | +│ └── SPIRenderer.h |
| 154 | +├── include/ # (optional) extra headers |
| 155 | +├── lib/ # third-party libs, e.g. UC2-CAN |
| 156 | +├── python/ # helper scripts (ILDA, LUT generation) |
| 157 | +├── KICAD/ # electrical design |
| 158 | +├── platformio.ini # build targets |
| 159 | +└── sdkconfig.* # pre-tuned IDF configs |
| 160 | +``` |
| 161 | + |
| 162 | +#### 11.2 Build flow (PlatformIO) |
| 163 | + |
| 164 | +1. Select **UC2\_3\_Xiao** in *platformio.ini*. |
| 165 | +2. `pio run` → ESP-IDF CMake → compile + link with Arduino component. |
| 166 | +3. `pio run -t upload` flashes over USB-CDC (921 kbit s⁻¹). |
| 167 | +4. Partition scheme `custom_partition_esp32s3.csv` reserves PSRAM cache and 1 MiB for OTA. |
| 168 | + |
| 169 | +#### 11.3 Runtime tasks |
| 170 | + |
| 171 | +| Task | Core affinity | Duty | |
| 172 | +| ----------------------------- | ------------- | ---------------------------------------------------------------------------- | |
| 173 | +| **mainTask (app\_main)** | core 0 | parameter sweep, calls renderer, yields 10 ms to scheduler. | |
| 174 | +| **IDF idle0/1** | core 0/1 | housekeeping (watchdog disabled for main). | |
| 175 | +| Additional tasks (*optional*) | — | CAN listener, WebSocket, or CLI can be added; use `xTaskCreatePinnedToCore`. | |
| 176 | + |
| 177 | +The code presently runs everything in the foreground; heavy processing (e.g. CAN command parsing) should be off-loaded to a FreeRTOS task to keep the raster loop deterministic. |
| 178 | + |
| 179 | +#### 11.4 SPIRenderer workflow |
| 180 | + |
| 181 | +```text |
| 182 | +constructor() |
| 183 | + ├─ configure GPIO for laser, LDAC, triggers |
| 184 | + ├─ initialise SPI3 @20 MHz (XIAO) or HSPI (generic) |
| 185 | + └─ pre-compute nX, nY from (X_MIN…X_MAX)/STEP |
| 186 | +
|
| 187 | +start() |
| 188 | + └─ draw() |
| 189 | +
|
| 190 | +draw() |
| 191 | + for each frame |
| 192 | + assert all triggers high ← frame start |
| 193 | + for X = X_MIN…X_MAX |
| 194 | + for Y = Y_MIN…Y_MAX |
| 195 | + clear triggers |
| 196 | + hold LDAC low |
| 197 | + SPI out 16-bit word to DAC-A (X) |
| 198 | + SPI out 16-bit word to DAC-B (Y) |
| 199 | + release LDAC ← both axes latch simultaneously |
| 200 | + set pixel trigger, delay tPixelDwelltime µs |
| 201 | + issue line trigger each X-loop |
| 202 | + clear all triggers ← frame end |
| 203 | +``` |
| 204 | + |
| 205 | +* **Timing** – `esp_rom_delay_us()` delivers sub-µs waits; replace with a hardware timer ISR for dwell times <5 µs. |
| 206 | +* **Throughput** – one SPI transaction per axis, LDAC latched once per pixel → 2 × 16 bits @ 20 MHz ≈ 1.6 µs transfer; 512×512 raster ≈ 0.42 s per frame exclusive of galvo settling. |
| 207 | +* **Triggers** – 50 Ω back-termination; rising edge marks *start* of integration for FLIM/PMT cards. |
| 208 | + |
| 209 | +#### 11.5 Customisation hooks |
| 210 | + |
| 211 | +| Use case | Modification | |
| 212 | +| ------------------------ | ----------------------------------------------------------------------------------------------------------------------------------- | |
| 213 | +| **Different image size** | call `setParameters()` with new limits/step; renderer recomputes nX/nY without reallocating. | |
| 214 | +| **External control** | add a CAN listener task that writes into a shared parameters struct guarded by a mutex; call `renderer->start()` from that task. | |
| 215 | +| **Scan patterns** | replace the two nested `for` loops with a lookup table of `(x,y)` pairs (e.g. spiral, Lissajous). | |
| 216 | +| **Higher speed** | use queued (`spi_device_queue_trans`) DMA transfers and toggle LDAC from SPI post-trans callback to overlap SPI and galvo settling. | |
| 217 | + |
| 218 | +#### 11.6 Pin map (compile-time) |
| 219 | + |
| 220 | +| Signal | GPIO (UC2-Galvo) | GPIO (bare XIAO) | |
| 221 | +| --------------- | ---------------- | ---------------- | |
| 222 | +| **MOSI** | 9 | 7 | |
| 223 | +| **SCK** | 7 | 8 | |
| 224 | +| **CS** | 8 | 9 | |
| 225 | +| **LDAC** | 6 | 6 | |
| 226 | +| **TRIG PIXEL** | 2 | 2 | |
| 227 | +| **TRIG LINE** | 3 | 3 | |
| 228 | +| **TRIG FRAME** | 4 | 4 | |
| 229 | +| **LASER blank** | 43 | 43 | |
| 230 | + |
| 231 | +Defined in `SPIRenderer.h`; switch boards via `-DIS_XIAO_UC2GALVOBOARD` or `-DIS_XIAO` build flag. ([GitHub][3]) |
| 232 | + |
| 233 | +#### 11.7 Extending the firmware |
| 234 | + |
| 235 | +* **Finite-state machine** – wrap the renderer in a state class (IDLE, SCANNING, PAUSED). |
| 236 | +* **Streaming points** – replace the nested loops by a ring buffer filled by CAN/UART for arbitrary point lists. |
| 237 | +* **Persistent settings** – store last used scan parameters to NVS (`nvs_flash.h`) and recall on boot. |
| 238 | +* **Web OTA** – enable `esp_https_ota` component and reserve second app partition (already present in `custom_partition_esp32s3.csv`). |
| 239 | + |
| 240 | +--- |
| 241 | + |
| 242 | +[1]: https://raw.githubusercontent.com/openUC2/openUC2-LaserScanner/main/src/main.cpp "raw.githubusercontent.com" |
| 243 | +[2]: https://raw.githubusercontent.com/openUC2/openUC2-LaserScanner/main/src/SPIRenderer.cpp "raw.githubusercontent.com" |
| 244 | +[3]: https://raw.githubusercontent.com/openUC2/openUC2-LaserScanner/main/src/SPIRenderer.h "raw.githubusercontent.com" |
| 245 | +[4]: https://raw.githubusercontent.com/openUC2/openUC2-LaserScanner/main/platformio.ini "raw.githubusercontent.com" |
0 commit comments