Repository navigation
JTAG Implementation Evolution From 2.25Mbps to 9Mbps
The glossy-msp430 project supports two distinct JTAG implementations, each optimized for different hardware constraints:
| Implementation | Hardware Target | Speed | Clock Source | Release | Use Case |
|---|---|---|---|---|---|
| GeneratorSTLinkPWM | ST-Link V2 Clone | 2.25 Mbps | Timer PWM | v1.0 | Low-cost accessibility; educational use |
| DtrigJtag | BluePill/F103 | 9 Mbps | SPI + Timer Toggle | v2.0+ | Performance debugging; production use |
This document traces the architectural evolution, explains why each design made different trade-offs, and when to use each variant.
┌────────────────────────────────────────────────────────────────────────┐
│ JTAG IMPLEMENTATION COMPARISON │
├─────────────────────┬──────────────────────┬────────────────────────────┤
│ Aspect │ GeneratorSTLinkPWM │ DtrigJtag │
├─────────────────────┼──────────────────────┼────────────────────────────┤
│ Hardware Target │ ST-Link V2 Clone │ BluePill / STM32F103 │
│ Speed │ 2.25 Mbps │ 9 Mbps (9× faster!) │
│ Cost (Hardware) │ $3-5 │ $15-20 │
│ Clock Source │ TIM1 PWM (50% duty) │ SPI + TIM1 Toggle Mode │
│ Data Transfer │ DMA to GPIO BSRR │ SPI DMA (byte-per-cycle) │
│ JTDI/JTDO Control │ GPIO BSRR writes │ SPI peripheral (native) │
│ TMS Control │ GPIO BSRR writes │ TIM1_CH2N toggle (0-DMA) │
│ DMA Channels Used │ 3 (read/write/tms) │ 2 (SPI RX/TX only) │
│ Port Layout │ Split (PA/PB) │ Single (PA only) │
│ Synchronization │ Timer repetition ctr │ Critical section + CNT │
│ Speed-Grades │ 1 (fixed) │ 5 (0.5-9 Mbps) │
│ TCLK Support │ Via SPI bit-stream │ Via SPI burst + PWM muting │
│ Overhead │ High DMA arbitration │ Low (SPI native protocol) │
│ Temperature Stable │ ±0.5% (PLL) │ ±0.1% (SPI baud ratio) │
└─────────────────────┴──────────────────────┴────────────────────────────┘
ST-Link V2 Clone Constraints:
- GPIO pins split across Port A (PA6/PA7) and Port B (PB13/PB14)
- No SPI peripheral easily available without internal surgery
- Maximum 2.25 MHz due to 3-channel DMA arbitration overhead
- Physical layout forces multi-port coordination
BluePill Advantages:
- All JTAG signals on Port A (PA5/PA6/PA7)
- SPI1 peripheral native to pins PA5/PA6/PA7
- Flexible timer channels for TMS modulation
- Single-port DMA enables 9 MHz operation
GeneratorSTLinkPWM (ST-Link V2):
"Given rigid hardware constraints, maximize speed with autonomous hardware waveforms rather than CPU bit-banging."
- Accepted lower speed (2.25 Mbps) for cost reduction and accessibility
- Prioritized robustness (deterministic waveforms) over throughput
DtrigJtag (BluePill):
"Leverage SPI efficiency and timer versatility to achieve native protocol speeds."
- Exploits flexible GPIO routing and native peripherals
- Prioritizes performance (9 Mbps) for production debugging
The Problem: Split GPIO ports + no SPI + need precision clock
The Solution: Automated waveform generation via PWM + multi-DMA
┌──────────────────────────────────────────────────────────┐
│ STM32F103 (ST-Link V2 Clone) │
├──────────────────────────────────────────────────────────┤
│ │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ TIM1 (Master Timer, 8-count cycle) │ │
│ │ • PWM on CH1N (PB13) → JTCK (50% duty) │ │
│ │ • CCR2 (count 5) → triggers DMA for JTMS writes │ │
│ │ • CCR3 (count 1) → triggers DMA for JTDI writes │ │
│ │ • CCR4 (count 0) → triggers DMA for JTDO reads │ │
│ └──────────┬───────────────────────────────────────┘ │
│ │ │
│ ┌──────────┴──────────────────────────────────────┐ │
│ │ DMA Arbitration (3 channels, different addresses) │
│ │ │
│ │ DMA1_Ch2: Port A IDR → Memory (JTDO read) │
│ │ DMA1_Ch3: Memory → Port A BSRR (JTDI write) │
│ │ DMA1_Ch4: Memory → Port B BSRR (JTMS write) │
│ │ │
│ │ ⚠ Problem: Each channel = ~115 ns overhead │
│ │ 3 channels × 8-count = 444 ns cycle │
│ │ Margin: ~99 ns (safe but tight) │
│ └──────────┬──────────────────────────────────────┘ │
│ │ │
│ ┌──────────▼──────────────────────────────────────┐ │
│ │ GPIO Ports (Split across A and B) │ │
│ │ PA6/PA7 (JTDO/JTDI) ─→ Target JTAG │ │
│ │ PB13/PB14 (JTCK/JTMS) ─→ │ │
│ └──────────┬──────────────────────────────────────┘ │
│ │ │
└─────────────┼──────────────────────────────────────────┘
│
┌────▼─────┐
│ MSP430 │
│ Target │
└───────────┘
Key Bottleneck: DMA arbitration across 3 separate peripheral addresses
- Each DMA request: ~115 ns (setup + arbitrate + transfer)
- 3 channels per cycle × 115 ns = 345 ns minimum
- At 2.25 MHz, cycle = 444 ns → only 99 ns margin ✓ but limited
The Insight: SPI handles data transfer natively; timer toggle mode handles TMS for free
The Solution: SPI + Timer Toggle Mode + Critical-Section Synchronization
┌──────────────────────────────────────────────────────────┐
│ STM32F103 (BluePill, Flexible GPIO) │
├──────────────────────────────────────────────────────────┤
│ │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ SPI1 (Data Transfer, byte-per-8-cycles) │ │
│ │ • SCK = PA5 (JTCK) — SPI clock (8 cycles/byte) │ │
│ │ • MOSI = PA7 (JTDI) — TX via DMA1_Ch3 │ │
│ │ • MISO = PA6 (JTDO) — RX via DMA1_Ch2 │ │
│ │ │ │
│ │ DMA Cost: Only 2 channels (RX/TX) │ │
│ │ Each byte: 8 cycles @ APB2 / N baud │ │
│ └──────┬───────────────────────────────────────────┘ │
│ │ │
│ ┌──────┴────────────────────────────────────────────┐ │
│ │ TIM1 (TMS Generator, Toggle Mode, zero-DMA) │ │
│ │ • CH2N = PB14 (JTMS) — toggle output │ │
│ │ • CH3 DMA-trigger = CCR2 reload (one DMA shot) │ │
│ │ • CH4 DMA-trigger = CCR2 reload (one DMA shot) │ │
│ │ │ │
│ │ Toggle Pattern (Toggle mode): │ │
│ │ Entry: CCR2 = count N → toggle → TMS LOW │ │
│ │ Exit: CCR2 = count M → toggle → TMS HIGH │ │
│ │ Return: CCR2 = count Z → toggle → TMS LOW │ │
│ │ │ │
│ │ DMA Cost: Only 2 CCR2 reloads (not per-bit!) │ │
│ └──────┬────────────────────────────────────────────┘ │
│ │ │
│ ┌──────┴────────────────────────────────────────────┐ │
│ │ Critical Section: Atomic SPI + TIM1 Launch │ │
│ │ │ │
│ │ void Start(..., uint16_t cnt_offset) { │ │
│ │ { │ │
│ │ CriticalSection lock; ← Disable interrupts │ │
│ │ SpiTxDma::Enable(); ← Start SPI DMA │ │
│ │ CycleTimer::CounterResume(); ← Resume TIM1 │ │
│ │ } ← Re-enable interrupts │ │
│ │ } │ │
│ │ │ │
│ │ Result: SPI and TIM1 phase-locked from bit 0 │ │
│ │ cnt_offset pre-loads TIM1 CNT for TMS timing │ │
│ └──────┬────────────────────────────────────────────┘ │
│ │ │
│ ┌──────▼────────────────────────────────────────────┐ │
│ │ GPIO Port A (All JTAG signals on same port!) │ │
│ │ PA5 (SPI SCK) ─→ JTCK │ │
│ │ PA6 (SPI MISO) ─→ JTDO │ │
│ │ PA7 (SPI MOSI) ─→ JTDI │ │
│ │ │ │
│ │ GPIO Port B │ │
│ │ PB14 (TIM1_CH2N) ─→ JTMS │ │
│ └──────┬────────────────────────────────────────────┘ │
│ │ │
└─────────┼──────────────────────────────────────────────┘
│
┌────▼─────┐
│ MSP430 │
│ Target │
└───────────┘
Key Innovation:
- SPI handles data transfer (byte per 8 cycles, protocol-native)
- TIM1 toggle mode provides TMS pulses for FREE (no per-bit DMA cost)
- Only 2 DMA channels needed (SPI RX/TX) → 4× less arbitration
- Critical section ensures first bit alignment → cnt_offset tuning per grade
Result: Can sustain 9 Mbps safely ✓
At 2.25 MHz (GeneratorSTLinkPWM):
Cycle time: 444 ns (8-count × 55.5 ns/count)
Timeline:
Count 0: DmaRead (Port A IDR) ─ 115 ns
Count 1: DmaWrite (Port A BSRR) ─ 115 ns (gap: 55 ns)
Count 2: DmaTms (Port B BSRR) ─ 115 ns (gap: 55 ns)
Count 3-7: PWM high/low, wait for next cycle
Total margin: ~99 ns → Safe ✓ (but no room to increase speed)
At 9 Mbps (DtrigJtag, per byte):
Cycle time: 111 ns (8-count × 13.9 ns/count @ 72 MHz with N=8 prescaler)
Timeline (Single-port SPI):
SPI TX DMA: Start transfer (DMA native, single address)
TIM1 Toggle: Hardware generates TMS transitions
No per-bit DMA arbitration!
Total margin: Excellent ✓ (can add speed grades safely)
Key Insight:
- GeneratorSTLinkPWM: Each DMA request = separate peripheral address = full setup/arbitrate/transfer cycle
- DtrigJtag: SPI peripheral handles clock/data natively; toggle mode needs zero DMA
GeneratorSTLinkPWM (Timer repetition counter):
// Set exact cycle count via RCR (repetition counter)
BeatTimer::SetupRepetition(kTimerPeriod_, 1); // repeat once
// Timer runs autonomously for exact bit countDtrigJtag (Critical section + CNT preload):
// Lock interrupts, launch SPI and TIM1 atomically
{
CriticalSection lock;
SpiTxDma::Enable(); // Start SPI first byte
CycleTimer::CounterResume(); // Resume TIM1 with preset CNT
}
// cnt_offset = LA-calibrated phase shift per speed grade- Problem identified: ST-Link V2 clone lacks SPI but has GPIO and timer
- Initial attempt: Naive bit-banging (unreliable, jittery)
- Solution: GeneratorSTLinkPWM template (Timer PWM + 3-channel DMA)
- Result: 2.25 Mbps, stable, deterministic
- Release: v1.0
- Observation: BluePill has flexible GPIO + native SPI on PA5/PA6/PA7
- Strategy: Use SPI for data (byte per 8 cycles) + timer for TMS
- Challenge: TMS timing must align with SPI bit edges
- Solution: Critical section synchronization + cnt_offset tuning
- Result: 9 Mbps, 4× faster than ST-Link V2
- Release: v1.5+
- Insight: TIM1 toggle mode generates TMS pulses with zero DMA cost
- Improvement: Removed 3rd DMA channel (TMS control)
- Validation: Speed grades (0.5, 1.125, 2.25, 4.5, 9 Mbps) with per-grade tuning
- Addition: TCLK high-frequency support (~450 kHz) via SPI burst
- Release: v2.0+
| Aspect | GeneratorSTLinkPWM | DtrigJtag |
|---|---|---|
| Clock Source | TIM1 PWM (GPIO output) | SPI SCK (peripheral) |
| Frequency Control | Timer prescaler | SPI baud rate (BR field) |
| Duty Cycle | Fixed 50% (4 high, 4 low) | Fixed 50% (SPI native) |
| Accuracy | ±0.5% (PLL drift) | ±0.1% (baud ratio) |
| Jitter | ±1 timer count (~55 ns @ 2.25 MHz) | <1 ns (SPI native) |
| Adjustability | Single fixed speed | 5 speed grades at runtime |
| Aspect | GeneratorSTLinkPWM | DtrigJtag |
|---|---|---|
| JTDI/JTDO Method | DMA to GPIO BSRR | SPI MOSI/MISO via DMA |
| Unit of Transfer | Per-DMA (variable) | Per-byte (8 cycles/byte) |
| DMA Channels | 2 (RX, TX) | 2 (RX, TX) |
| Arbitration Overhead | None (not used for JTDI) | Minimal (SPI protocol) |
| Throughput | Limited by timer cycle | Limited by SPI baud |
| Byte Alignment | 8-count cycle per bit | Native (8 counts/byte) |
| Aspect | GeneratorSTLinkPWM | DtrigJtag |
|---|---|---|
| TMS Method | DMA writes to GPIO BSRR | TIM1_CH2N toggle mode |
| DMA Channels for TMS | 1 dedicated | 0 (toggle is hardware) |
| Pulses per Frame | Multiple (every state change) | 3 (entry, exit, RTI) |
| Control Method | Per-bit DMA updates | Two CCR2 reload DMA shots |
| Timing Precision | Timer-based (counts) | Toggle mode (exact) |
| CPU Overhead | Render TMS sequence each frame | Render TMS bit pattern once |
Key Difference: DtrigJtag's toggle mode requires only 2 DMA writes for entire TMS sequence, vs GeneratorSTLinkPWM's multi-write approach.
GeneratorSTLinkPWM:
Port A: PA6 (JTDO), PA7 (JTDI)
Port B: PB13 (JTCK), PB14 (JTMS)
↑ Separate DMA writes needed
DtrigJtag:
Port A: PA5 (SPI SCK=JTCK), PA6 (SPI MISO=JTDO), PA7 (SPI MOSI=JTDI)
↑ Single peripheral, atomic SPI protocol
Port B: PB14 (TIM1_CH2N=JTMS) — no DMA
GeneratorSTLinkPWM (ST-Link V2):
Firmware.shared/
drivers/
JtagDev.tim.cpp ← Main implementation
util/
WaveJtag.h ← GeneratorSTLinkPWM template
target.stlinv2/
platform.h ← Pin definitions
DtrigJtag (BluePill):
Firmware.shared/
drivers/
JtagDev.dtrig.cpp ← Main implementation
util/
DtrigJtag.h ← DtrigJtag template
target.bluepill/
platform.h ← Pin definitions
DtrigJtag supports 5 speed grades tuned via cnt_offset parameter:
// From JtagDev.dtrig.cpp
static constexpr uint16_t kDtrigCntOffset_1 = 2; // 0.5625 MHz (slowest, safest)
static constexpr uint16_t kDtrigCntOffset_2 = 2; // 1.125 MHz
static constexpr uint16_t kDtrigCntOffset_3 = 2; // 2.25 MHz
static constexpr uint16_t kDtrigCntOffset_4 = 4; // 4.5 MHz
static constexpr uint16_t kDtrigCntOffset_5 = 4; // 9 MHz (fastest)
// Each grade calibrated with logic analyzer to align TMS toggles
// with SPI bit edges within the 8-count cycleGeneratorSTLinkPWM: Fixed 2.25 MHz (no speed grades)
| Implementation | Method | Frequency |
|---|---|---|
| GeneratorSTLinkPWM | Via SPI bit-stream (kJtmsWave pattern) | ~450 kHz |
| DtrigJtag | SPI burst at reduced baud + GPIO muting | ~450 kHz |
Both achieve ~450 kHz for MSP430 flash operations (requires <500 kHz per spec).
Scenario: Reading 256 bytes from target flash
Operation: 16-bit reads × 128 = 2048 JTAG clock cycles total
GeneratorSTLinkPWM @ 2.25 MHz:
Time = 2048 cycles / 2.25 MHz = 911 µs
Frame overhead: ~50 µs (20 frames × ~2.5 µs setup)
Total: ~961 µs
DtrigJtag @ 9 Mbps:
Time = 2048 cycles / 9 MHz = 228 µs
Frame overhead: ~20 µs (20 frames × ~1 µs setup)
Total: ~248 µs
Speedup: 961 / 248 ≈ 3.87× (vs theoretical 4×)
Reason: Frame overhead amortized over multiple transfers
Real Impact:
- Small reads (1-4 bytes): ~10-50 µs difference (imperceptible)
- Large transfers (>256 bytes): dtrig clearly faster
- Typical debug session: Mix of operations, overall 2-3× faster
GeneratorSTLinkPWM:
- Theoretical max: 2.25 Mbps (2,250,000 bits/sec)
- Practical max: ~1.8 Mbps (accounting for frame gaps, bus arbitration)
DtrigJtag:
- Theoretical max: 9 Mbps (9,000,000 bits/sec @ 72 MHz / 8 prescaler)
- Practical max: ~8.5 Mbps (accounting for frame gaps, DMA setup)
Use GeneratorSTLinkPWM (2.25 Mbps, ST-Link V2) if:
- ✓ Hardware: ST-Link V2 clone (constrained GPIO, no SPI)
- ✓ Budget: $3-5 required
- ✓ Use case: Educational, accessibility, proof-of-concept
- ✓ Speed acceptable: ≤2.25 Mbps is enough
- ✓ Availability: Local hobbyist shop has stock
Use DtrigJtag (9 Mbps, BluePill) if:
- ✓ Hardware: STM32F103 BluePill or equivalent
- ✓ Performance: Need ≥4 Mbps
- ✓ Use case: Production debugging, development
- ✓ Features: Speed-grade tuning, TCLK support required
- ✓ Flexibility: Can use 5 different speed grades
| Hardware | Implementation | Speed | Notes |
|---|---|---|---|
| ST-Link V2 Clone | GeneratorSTLinkPWM | 2.25 Mbps | Fixed; only option |
| BluePill (STM32F103) | DtrigJtag | 9 Mbps | 5 speed grades |
| Nucleo-L432KC | DtrigJtag | 9 Mbps | Limited testing |
| Nucleo-G431KB | DtrigJtag | 9 Mbps | Limited testing |
| Custom F1/F3 board | DtrigJtag (if PA5/PA6/PA7 available) | 9 Mbps | Board-dependent |
| Lesson | Implication |
|---|---|
| Hardware constraints drive design | ST-Link V2's split GPIO → timer/DMA; BluePill's SPI → dtrig |
| DMA arbitration is expensive | Each peripheral address = ~115 ns overhead; limits speed |
| SPI is underrated for JTAG | Native protocol efficiency beats bit-banging by 4× |
| Timer toggle mode is free | TIM1_CH2N toggle costs zero DMA budget |
| Synchronization matters | Critical section + cnt_offset = phase-locked precision |
| Speed grades = robustness | Tuning per frequency proves margins exist |
-
GeneratorSTLinkPWM: Template-heavy, demonstrates C++17
constexprtricks - DtrigJtag: More complex (critical sections, timing tuning), but clearer intent
- Both use bmt abstractions exclusively (no raw register access)
Potential optimizations:
- Extend DtrigJtag to G4/L4 families (higher clock = higher Mbps possible)
- Explore H7 dual-DMA for ST-Link V2 → eliminate arbitration, reach 3-4 Mbps
- Add variable speed-grade support to GeneratorSTLinkPWM (minor work)
The evolution from 2.25 Mbps (GeneratorSTLinkPWM) to 9 Mbps (DtrigJtag) illustrates how hardware constraints and design choices shape performance:
- ST-Link V2: Rigid GPIO + no SPI → timer/DMA workaround → 2.25 Mbps
- BluePill: Flexible GPIO + native SPI → SPI + timer toggle → 9 Mbps
Each implementation is optimal for its target hardware and use case. The project supports both because different users have different priorities: cost vs. performance, accessibility vs. production-grade debugging.
- Repurposing-ST-Link-V2-Clones-for-MSP430-JTAG-Debugging.md — Detailed GeneratorSTLinkPWM design
- Firmware.shared/util/DtrigJtag.h — DtrigJtag template documentation
- Firmware.shared/drivers/JtagDev.dtrig.cpp — DtrigJtag implementation & speed grades
- Firmware.shared/util/TimDmaWave.h — Timer/DMA utilities (supports both approaches)
- CODE_GUIDELINES.md — C++17 template and bmt abstraction standards