|
| 1 | +# Trigger Oscilloscope on Mazduino boards |
| 2 | + |
| 3 | +Implementation notes for bringing rusEFI's Trigger Oscilloscope up on Mazduino |
| 4 | +hardware. Nothing here is implemented yet — this is a design record of what the |
| 5 | +upstream feature does, what it costs, and what a board revision would have to |
| 6 | +provide. |
| 7 | + |
| 8 | +Everything below was read out of `ext/rusefi/firmware` at the submodule revision |
| 9 | +in this tree. File and line references are to that copy. |
| 10 | + |
| 11 | +--- |
| 12 | + |
| 13 | +## 1. What it is, and why the existing loggers are not it |
| 14 | + |
| 15 | +rusEFI ships three trigger diagnostics. They are usually confused with each |
| 16 | +other, so the distinction matters when deciding whether this is worth the board |
| 17 | +change: |
| 18 | + |
| 19 | +| Logger | TS name | What it records | Needs | |
| 20 | +|---|---|---|---| |
| 21 | +| Primary tooth | "Primary Tooth Time Logger" | one timestamp per rising edge | nothing extra | |
| 22 | +| Composite | "Composite Logger" | a timestamp per *edge*, plus level/sync/TDC flags | nothing extra | |
| 23 | +| **Trigger scope** | "Trigger Oscilloscope" | the **raw analog voltage** on the trigger line, sampled continuously | an ADC-capable input | |
| 24 | + |
| 25 | +The first two are digital: they tell you *when* the ECU's own comparator decided |
| 26 | +an edge happened. If the comparator is misbehaving, they tell you nothing about |
| 27 | +why — a VR signal that is too weak to cross the threshold at cranking, ringing |
| 28 | +on a long unshielded run, or a Hall sensor with a slow rise time all look the |
| 29 | +same from a digital logger: a missing or spurious tooth. |
| 30 | + |
| 31 | +The trigger scope samples the conditioned analog waveform, so you see amplitude, |
| 32 | +shape and noise. That is what makes it worth the hardware, and also why it |
| 33 | +cannot be retrofitted in firmware alone. |
| 34 | + |
| 35 | +--- |
| 36 | + |
| 37 | +## 2. Why it does not work on Mazduino today |
| 38 | + |
| 39 | +`trigger_scope.cpp` is wrapped entirely in `#ifdef TRIGGER_SCOPE` |
| 40 | +(`firmware/console/binary/trigger_scope.cpp:7`). The define comes from the |
| 41 | +board's own makefile — on AlphaX 4chan, `board.mk:24`: |
| 42 | + |
| 43 | +```make |
| 44 | +DDEFS += -DTRIGGER_SCOPE |
| 45 | +``` |
| 46 | + |
| 47 | +No Mazduino board defines it, and none has a `trigger_scope_config.h`. So |
| 48 | +`initTriggerScope()` is never compiled in, and the `TS_TRIGGER_SCOPE_ENABLE` |
| 49 | +case inside `TS_SET_LOGGER_SWITCH` is compiled out too — the firmware answers |
| 50 | +the start command with a NAK. |
| 51 | + |
| 52 | +**The generated INI is misleading here.** `tunerstudio.template.ini` emits the |
| 53 | +`loggerDef = triggerScope, "Trigger Oscilloscope", csv` block unconditionally, |
| 54 | +so every Mazduino INI advertises a logger the firmware cannot run. TunerStudio |
| 55 | +will list it and fail. Consider suppressing that block for boards without the |
| 56 | +define when this is picked up. |
| 57 | + |
| 58 | +--- |
| 59 | + |
| 60 | +## 3. Protocol, for whoever writes the app side |
| 61 | + |
| 62 | +The scope rides the same `TS_SET_LOGGER_SWITCH` ('l', `0x6C`) command as the |
| 63 | +tooth and composite loggers, with a different sub-command byte: |
| 64 | + |
| 65 | +| Action | Bytes | Constant | |
| 66 | +|---|---|---| |
| 67 | +| Start | `6C 04` | `TS_TRIGGER_SCOPE_ENABLE` | |
| 68 | +| Stop | `6C 05` | `TS_TRIGGER_SCOPE_DISABLE` | |
| 69 | +| Read | `6C 06` | `TS_TRIGGER_SCOPE_READ` | |
| 70 | + |
| 71 | +Wrapped in the usual TS CRC frame. `dataReadyCondition` is the |
| 72 | +`triggerScopeReady` output channel; reading before it is set answers |
| 73 | +`TS_RESPONSE_OUT_OF_RANGE` (`0x84`). |
| 74 | + |
| 75 | +The response payload is a flat array of 2-byte records, no header or footer: |
| 76 | + |
| 77 | +``` |
| 78 | +recordDef = 0, 0, 2 |
| 79 | +recordField = channel1, <name>, bit 0, 8 bits, scale 6.6/255, "v" |
| 80 | +recordField = channel2, <name>, bit 8, 8 bits, scale 6.6/255, "v" |
| 81 | +``` |
| 82 | + |
| 83 | +So byte 0 is channel 1, byte 1 is channel 2, interleaved for the whole buffer. |
| 84 | +Samples are **8-bit** (the ADC runs in 8-bit mode, `ADC_CR1_RES_1`), and the |
| 85 | +buffer is `BIG_BUFFER_SIZE` = 8192 bytes → 4096 sample pairs per read. |
| 86 | + |
| 87 | +The channel names come from the board's `prepend.txt`, e.g. AlphaX 4chan: |
| 88 | + |
| 89 | +```c |
| 90 | +#define TS_TRIGGER_SCOPE_CHANNEL_1_NAME "C2/C3 Crank VR" |
| 91 | +#define TS_TRIGGER_SCOPE_CHANNEL_2_NAME "E5/E6 Cam VR" |
| 92 | +``` |
| 93 | +
|
| 94 | +--- |
| 95 | +
|
| 96 | +## 4. The hardware requirement |
| 97 | +
|
| 98 | +### 4.1 The scale factor tells you the divider |
| 99 | +
|
| 100 | +The INI scales a sample by `6.6 / 255`. An 8-bit full-scale reading is 255, |
| 101 | +which the ECU reports as 6.6 V, while the pin itself can only see VREF = 3.3 V. |
| 102 | +That is a **2:1 divider in front of the ADC pin**: 6.6 V at the input becomes |
| 103 | +3.3 V at the pin. |
| 104 | +
|
| 105 | +If a Mazduino revision uses a different divider, the `6.6` in the INI has to |
| 106 | +change to match, or every reading is wrong by that ratio. It is a per-board |
| 107 | +constant baked into the generated INI, so it belongs with the board's other |
| 108 | +scaling values. |
| 109 | +
|
| 110 | +### 4.2 What has to reach the pin |
| 111 | +
|
| 112 | +The scope taps the signal **after** protection and conditioning but **before** |
| 113 | +(or in parallel with) the digital comparator. Wanted: |
| 114 | +
|
| 115 | +- Divider to put the working range inside 0–3.3 V at the pin (2:1 for the 6.6 V |
| 116 | + scale above). |
| 117 | +- Clamp diodes to 3.3 V and GND. A VR sensor at high RPM swings far beyond this |
| 118 | + and will otherwise destroy the pin. |
| 119 | +- Series resistor between the clamp and the pin to limit clamp current. |
| 120 | +- Keep the existing conditioning path intact — the scope is a passive observer, |
| 121 | + the decoder still runs off the comparator output. |
| 122 | +
|
| 123 | +A VR sensor is bipolar and swings negative. The AlphaX inputs are named |
| 124 | +`IN_RES*`, which on Hellen boards are biased analog inputs, so the negative half |
| 125 | +of the swing is lifted into range by the bias network. A Mazduino tap would need |
| 126 | +the same treatment: bias the signal to roughly mid-rail so both halves of the VR |
| 127 | +waveform are visible, otherwise the negative half clips to 0 V and the trace is |
| 128 | +useless for judging symmetry. |
| 129 | +
|
| 130 | +### 4.3 The pin must be on the ADC |
| 131 | +
|
| 132 | +On `mazduino-core` the trigger input is `Gpio::C6` |
| 133 | +(`boards/mazduino-core/board_configuration.cpp:89`). **PC6 has no ADC channel on |
| 134 | +STM32F4** — it is a digital-only pin. So this cannot be done by re-declaring an |
| 135 | +existing pin; the conditioned signal has to be routed to a spare ADC-capable pin |
| 136 | +as a second net. That is a board respin, not a firmware change. |
| 137 | +
|
| 138 | +ADC-capable pins on the STM32F4 in use: PA0–PA7, PB0–PB1, PC0–PC5, and PF3–PF10 |
| 139 | +on the 144-pin parts. |
| 140 | +
|
| 141 | +--- |
| 142 | +
|
| 143 | +## 5. The blocker: ADC3 is already spoken for |
| 144 | +
|
| 145 | +This is the part that decides the design, and it is specific to Mazduino. |
| 146 | +
|
| 147 | +Upstream puts the scope on ADC3: |
| 148 | +
|
| 149 | +```c |
| 150 | +#define TRIGGER_SCOPE_ADC ADCD3 // trigger_scope_config.h |
| 151 | +``` |
| 152 | + |
| 153 | +Mazduino puts **software knock** on ADC3 (`boards/mazduino-core/knock_config.h`): |
| 154 | + |
| 155 | +```c |
| 156 | +#define KNOCK_ADC ADCD3 |
| 157 | +#define KNOCK_PIN_CH1 Gpio::F4 // ADC_CHANNEL_IN14 |
| 158 | +#define KNOCK_PIN_CH2 Gpio::F5 // ADC_CHANNEL_IN15 |
| 159 | +``` |
| 160 | +
|
| 161 | +Upstream already treats these as mutually exclusive — `startSampling()` and |
| 162 | +`initTriggerScope()` both bail out when `enableSoftwareKnock` is set, with the |
| 163 | +comment *"Trigger scope and knock currently mutually exclusive"*. They also |
| 164 | +compete for the same 8 KB scratch area: `BigBufferUser` has separate |
| 165 | +`TriggerScope` and `KnockSpectrogram` entries, and only one holder at a time. |
| 166 | +
|
| 167 | +Three ways out, in increasing order of effort: |
| 168 | +
|
| 169 | +1. **Accept the exclusivity.** Scope works only with software knock disabled. |
| 170 | + Cheapest, and matches upstream. Knock is not something you need while |
| 171 | + diagnosing a trigger that will not sync, so in practice the conflict is |
| 172 | + mild — but it must be surfaced in the UI, or the feature will read as broken |
| 173 | + to anyone with knock enabled. |
| 174 | +2. **Put the scope on a different ADC unit** (ADC1/ADC2), if the chosen pin's |
| 175 | + channel is reachable from it and the DMA stream is free. Needs care: ADC1 is |
| 176 | + busy with the normal sensor scan. Check `STM32_ADC_USE_ADC*` in `mcuconf.h` |
| 177 | + and the DMA stream map before committing. |
| 178 | +3. **Share ADC3 by time-slicing.** Not worth it — both features want continuous |
| 179 | + DMA into a large buffer. |
| 180 | +
|
| 181 | +Recommendation: option 1 for a first cut, and pick the scope pin on ADC3 |
| 182 | +(PF3–PF10 range, next to the existing knock pins) so no ADC rework is needed. |
| 183 | +
|
| 184 | +--- |
| 185 | +
|
| 186 | +## 6. Sample rate and capture window |
| 187 | +
|
| 188 | +From `trigger_scope_config.h`, `TRIGGER_SCOPE_SAMPLE_TIME = ADC_SAMPLE_144`, in |
| 189 | +8-bit mode, with two channels in the scan group. |
| 190 | +
|
| 191 | +On STM32F4, one conversion takes `SMP + resolution` ADC clocks, where the |
| 192 | +8-bit resolution costs 8 cycles. So per channel pair: |
| 193 | +
|
| 194 | +``` |
| 195 | +cycles_per_scan = 2 * (144 + 8) = 304 |
| 196 | +f_scan = f_adc / 304 |
| 197 | +``` |
| 198 | + |
| 199 | +`f_adc` is `PCLK2 / STM32_ADC_ADCPRE`. With PCLK2 = 84 MHz and a /4 prescaler |
| 200 | +that is 21 MHz, giving roughly: |
| 201 | + |
| 202 | +``` |
| 203 | +f_scan ≈ 21e6 / 304 ≈ 69 kSa/s per channel |
| 204 | +window = 4096 scans / 69 kSa/s ≈ 59 ms |
| 205 | +``` |
| 206 | + |
| 207 | +**Verify `STM32_ADC_ADCPRE` for the target board before quoting these numbers** — |
| 208 | +they move directly with the prescaler. The knock config in this repo derives its |
| 209 | +rate the same way and is a good template: |
| 210 | + |
| 211 | +```c |
| 212 | +#define KNOCK_SAMPLE_RATE (STM32_PCLK2 / (4 * (84 + 12))) |
| 213 | +``` |
| 214 | +
|
| 215 | +~59 ms is about 3 crank revolutions at 3000 rpm, or half a revolution at |
| 216 | +cranking speed. Enough to see the gap and judge waveform shape; not enough for a |
| 217 | +long-term noise hunt. `triggerScopeGetBuffer()` re-arms the next capture 10 ms |
| 218 | +after each read, so a continuous read loop gives a repeating snapshot, not a |
| 219 | +gapless recording. |
| 220 | +
|
| 221 | +--- |
| 222 | +
|
| 223 | +## 7. Firmware checklist |
| 224 | +
|
| 225 | +Once the hardware exists: |
| 226 | +
|
| 227 | +1. `boards/<board>/trigger_scope_config.h`: |
| 228 | + ```c |
| 229 | + #define TRIGGER_SCOPE_ADC ADCD3 |
| 230 | + #define TRIGGER_SCOPE_SAMPLE_TIME ADC_SAMPLE_144 |
| 231 | + #define TRIGGER_SCOPE_PIN_CH1 Gpio::Fx |
| 232 | + #define TRIGGER_SCOPE_ADC_CH1 ADC_CHANNEL_INxx |
| 233 | + #define TRIGGER_SCOPE_HAS_CH2 true // cam channel, optional |
| 234 | + #define TRIGGER_SCOPE_PIN_CH2 Gpio::Fy |
| 235 | + #define TRIGGER_SCOPE_ADC_CH2 ADC_CHANNEL_INyy |
| 236 | + ``` |
| 237 | +2. `boards/<board>/board.mk`: `DDEFS += -DTRIGGER_SCOPE` |
| 238 | +3. `boards/<board>/prepend.txt`: set `TS_TRIGGER_SCOPE_CHANNEL_1_NAME` and |
| 239 | + `..._2_NAME` to the real connector pin labels — these are what the user sees |
| 240 | + as the trace legend. |
| 241 | +4. If the divider is not 2:1, change the `6.6` scale in the INI template for |
| 242 | + this board. |
| 243 | +5. Decide and document the knock interaction. `enableSoftwareKnock` silently |
| 244 | + disables the scope today; if that stays, the INI should gate the logger on |
| 245 | + it so TunerStudio does not offer a dead button. |
| 246 | + |
| 247 | +Note `adcConvGroupCh1` hardcodes two channels in `ADC_SQR3`. A single-channel |
| 248 | +board still works but wastes half the buffer on a dead channel — worth a small |
| 249 | +patch upstream-side if only one channel is fitted. |
| 250 | + |
| 251 | +--- |
| 252 | + |
| 253 | +## 8. App side (MTuneX) |
| 254 | + |
| 255 | +Not implemented, and deliberately so: adding a button that NAKs on every board |
| 256 | +currently shipped would only confuse people. When a board gains the hardware: |
| 257 | + |
| 258 | +- The commands are three fixed 2-byte payloads (section 3), so `TsProtocol` |
| 259 | + needs one more builder alongside the tooth/composite ones. |
| 260 | +- Gate the menu entry on the ECU actually supporting it. The INI's `loggerDef` |
| 261 | + is not a reliable signal today (section 2); until that is fixed, gate on the |
| 262 | + board name or on a probe that tolerates a NAK. |
| 263 | +- The existing Tooth Logger screen already has a mode selector and a raw-data |
| 264 | + table; a scope trace is a third mode rather than a new screen. |
| 265 | + |
| 266 | +--- |
| 267 | + |
| 268 | +## 9. Speeduino |
| 269 | + |
| 270 | +No equivalent. Speeduino has the tooth logger and three composite variants, all |
| 271 | +digital edge logging off the same interrupt. There is no ADC path on the trigger |
| 272 | +input and no scope command in its serial protocol. |
0 commit comments