FastLED 3.10.9
Loading...
Searching...
No Matches
AutoResearchEsp8266.h
Go to the documentation of this file.
1#pragma once
2//
3// examples/AutoResearch/AutoResearchEsp8266.h
4//
5// ESP8266 driver benches for the low-memory AutoResearch surface.
6//
7// Every bench takes a single `const fl::json&` parameter and reads its fields
8// positionally out of the JSON array the host sends, so
9// `ci.rpc_client.RpcClient.send(name, args=[...])` drives it directly: that
10// client frames one JSON `params` value (params:[args]), and it cannot express a
11// bind of arity 0 or 2+. The `[a, b, c]` lines below are that array.
12//
13// uartWs2812Loopback: [nLeds, rxPin] -> json
14// Encodes a known pattern with the UART WS2812 driver's encoder, streams it
15// out of UART1 (GPIO2) and samples rxPin through GPIO_IN while the hardware
16// shifts it out, so the CPU is free to poll. Needs a jumper GPIO2 -> rxPin:
17// GPIO_IN does not see GPIO2's pad once UART1 owns it (measured: the line
18// reads low whether TX idles high or low). The decoded LED bytes and the
19// measured T0H/T1H/bit-period are returned for the host to judge.
20//
21// uartWs2812Show: [nLeds] -> json
22// Runs the same driver through FastLED.addLeds + FastLED.show().
23//
24// clocklessShow / apa102Show: [nLeds, frames] -> json
25// Run the default bit-banged WS2812 controller (GPIO4) and the APA102
26// controller (data GPIO13, clock GPIO14) through FastLED.show() and report
27// min/max frame time. A frame near twice the floor means the clockless
28// driver aborted and retried after an interrupt overran its timing window.
29//
30// apa102ShowMhz: [nLeds, mhz] -> json (20 frames)
31// APA102 on native HSPI (GPIO13/14) at 6/10/20/40/80 MHz. The driver
32// programs SPI1CLK in init() only, so each rate gets its own controller and
33// the bench re-inits before timing. Reports achievable_hz (decoded from the
34// SPI1CLK the driver just wrote), expected_us from it, and the measured
35// min_us; `success` is the frame time landing in
36// [expected_us, expected_us + 400 us]. The 400 us covers the ~250 us
37// per-show cost plus jitter, so this band only rejects a bus running at half
38// the programmed rate where the wire time is large next to that cost (6 MHz
39// here: 762 us of wire time vs a 781 us bound). At 20 MHz and above the wire
40// time shrinks under the slack and the band cannot see a halved bus -- there
41// `achievable_hz` (read back from SPI1CLK) is the evidence that the divider
42// took effect, not `success`.
43//
44// apa102ShowBitbang: [nLeds, frames] -> json
45// The same chipset on GPIO4/GPIO16, i.e. the generic software SPI fallback,
46// for a same-run comparison against apa102Show. generic_bitbang is false in a
47// FASTLED_ALL_PINS_HARDWARE_SPI build, where that pair resolves to the legacy
48// Arduino-SPI shim and is therefore not a bit-bang baseline.
49//
50// The benches name the HSPI pair as FL_PIN_SPI_DATA_1 / FL_PIN_SPI_CLOCK_1, the
51// same key the native-HSPI specialization uses, so D1/NodeMCU pin orders
52// (7/5) select the same GPIO13/14 pads instead of quietly falling back to the
53// bit-bang. The sense-pin arguments are always raw GPIO numbers.
54//
55// apa102HspiLoopback: [khz, mosiPin, sckPin] -> json
56// Pin-level HSPI check at 25 or 50 kHz. GPIO_IN does not see GPIO13/14 while
57// HSPI owns the pads, so jumper D7 (GPIO13, MOSI) to mosiPin and D5 (GPIO14,
58// SCK) to sckPin (12 and 5 are D6 and D1; both arguments are required). The
59// SCK ISR samples MOSI
60// on every rising edge (mode 0) and the 96 decoded bytes must match. Run
61// wifiOff first: the ISR budget is one SCK period, and MAC interrupts eat it.
62//
63// apa102HspiWatch: [sensePin, nBytes, nField] -> json
64// Starts a 25 kHz HSPI transfer by hand and polls sensePin through GPI for the
65// whole of it, so it counts the edges on the wire without relying on an ISR
66// (the driver's kick() only watches SPIBUSY, which runs for the programmed
67// time whether or not the pad is driven). nField >= 0 re-splits the same
68// total divider across CLKCNT_N/CLKDIV_PRE before the transfer, which is how
69// a zero CLKCNT_L was isolated; -1 runs whatever the driver installed.
70//
71// apa102HspiJumperCheck: [sensePin, txPin] -> json
72// Drives one HSPI output pad (txPin: 13 = MOSI/D7, 14 = SCK/D5) as a plain
73// GPIO and reports whether sensePin both follows the level and counts the
74// edges, which separates a missing jumper from a dead ISR; run it once per
75// signal before the loopback.
76//
77// All three accept sense pins only from 0, 5, 12 and 15 (the rest are the
78// console, the UART bench, the clockless controller's pad, the flash pads, or
79// the HSPI pads pinMode would unmux), and the loopback rejects two equal pins.
80//
81
82#include "platforms/esp/is_esp.h" // ok platform headers // ok platform specific - ESP8266 bench gate
83
84#if defined(FL_IS_ESP8266) // ok platform specific - ESP8266-only bench file
85
86#include "FastLED.h"
88#include "fl/stl/json.h"
89#include "fl/stl/stdint.h"
90#include "fl/stl/string.h"
91#include "fl/wdt/watchdog.h"
92#include "platforms/esp/8266/fastled_esp8266_uart.h" // ok platform headers // ok platform specific - ESP8266 UART driver under test
93
94// IWYU pragma: begin_keep
95#include <esp8266_peri.h> // ok platform specific - GPI, ESP8266_CLOCK for raw loopback sampling
96#include <user_interface.h> // ok platform specific - system_update_cpu_freq, wifi_* for bench setup
97#include "platforms/esp/8266/i2s_ws2812_codec.h" // ok platform headers - codec + samplesToEdges
98#include "platforms/esp/8266/spi_hspi_clock.h" // ok platform headers - HSPI clock divider
99#include <core_esp8266_i2s.h> // ok platform specific - I2S TX/RX registers for the loopback bench
100#include <cont.h> // ok platform specific - cont_get_free_stack (loop-stack headroom)
101// IWYU pragma: end_keep
102
103namespace autoresearch {
104namespace esp8266 {
105
106constexpr int kMaxLoopbackLeds = 10; // 120 UART bytes fit the 128-byte FIFO
107constexpr int kMaxEdges = kMaxLoopbackLeds * 24 * 2 + 4;
108
109// Always inlined: the IRAM capture loop must not call back into flash.
110__attribute__((always_inline)) inline fl::u32 cycles() {
111 fl::u32 c;
112 __asm__ __volatile__("rsr %0,ccount" : "=a"(c)); // ok platform specific - cycle-accurate edge timestamps; no core cycle counter
113 return c;
114}
115
116inline fl::u8 patternByte(int i) {
117 static const fl::u8 kSeed[] = {0x00, 0xFF, 0xAA, 0x55, 0x0F, 0xF0};
118 if (i < static_cast<int>(sizeof(kSeed))) {
119 return kSeed[i];
120 }
121 return static_cast<fl::u8>(i * 37 + 0x5A);
122}
123
124struct EdgeCapture {
125 fl::u32 edges[kMaxEdges];
126 int count;
127 bool firstLevel;
128};
129
130inline EdgeCapture& edgeCapture() {
132}
133
134// Streams `encoded` out of UART1 while timestamping every rxPin edge. Runs
135// from IRAM: from flash, the cold instruction cache on the first call after
136// boot stalled the loop past the first pulses (measured: 4 bits lost).
137// A FIFO write stalls the CPU for ~115 ns. Filling all 120 bytes up front let
138// the UART shift out 11 bits before sampling began, and writing once per
139// sample missed short T0H pulses. So prime two bytes, then write one just
140// after each falling edge, where the line stays low for >= 300 ns. Two edges
141// fall per UART byte, so the FIFO stays ahead of the shifter.
142FL_NO_INLINE static FL_IRAM void captureEdges(
143 const fl::u8* encoded, int nEncoded, fl::u32 mask, fl::u32 idleCycles,
144 EdgeCapture& cap) {
145 bool level = (GPI & mask) != 0; // ok platform specific - raw GPIO sampling / UART1 FIFO feed for the pin-level loopback
146 cap.firstLevel = level;
147 USF(fl::esp8266_uart::kUart) = encoded[0]; // ok platform specific - raw GPIO sampling / UART1 FIFO feed for the pin-level loopback
148 USF(fl::esp8266_uart::kUart) = encoded[1]; // ok platform specific - raw GPIO sampling / UART1 FIFO feed for the pin-level loopback
149 int fed = 2;
150 fl::u32 lastEdge = cycles();
151 while (cap.count < kMaxEdges) {
152 const bool now = (GPI & mask) != 0; // ok platform specific - raw GPIO sampling / UART1 FIFO feed for the pin-level loopback
153 const fl::u32 t = cycles();
154 if (now != level) {
155 cap.edges[cap.count++] = t;
156 level = now;
157 lastEdge = t;
158 if (!now && fed < nEncoded) {
159 USF(fl::esp8266_uart::kUart) = encoded[fed++]; // ok platform specific - raw GPIO sampling / UART1 FIFO feed for the pin-level loopback
160 }
161 } else if (t - lastEdge > idleCycles) {
162 break;
163 }
164 }
165}
166
167inline bool isUsableRxPin(int pin) {
168 // GPIO1/3 carry the RPC console, 6-11 the flash, 2 is the TX under test.
169 return pin == 0 || pin == 4 || pin == 5 || (pin >= 12 && pin <= 15);
170}
171
172inline fl::json uartWs2812Loopback(int nLeds, int rxPin) {
173 if (!isUsableRxPin(rxPin)) {
174 fl::json r = fl::json::object();
175 r.set("success", false);
176 r.set("error", "rxPin must be one of 0,4,5,12,13,14,15");
177 return r;
178 }
179 if (nLeds < 1) nLeds = 1;
180 if (nLeds > kMaxLoopbackLeds) nLeds = kMaxLoopbackLeds;
181 const int nBytes = nLeds * 3;
182
183 fl::u8 encoded[kMaxLoopbackLeds * 12];
184 for (int i = 0; i < nBytes; ++i) {
185 fl::esp8266_uart::encodeByte(patternByte(i), &encoded[i * 4]);
186 }
187
188 pinMode(rxPin, INPUT);
189 fl::esp8266_uart::begin(FASTLED_ESP8266_UART_BAUD);
190 fl::delayMicroseconds(100); // line settles low (latch)
191
192 const fl::u8 prevMhz = system_get_cpu_freq(); // ok platform specific - ESP8266 bring-up bench measures raw SoC behavior; no core API exposes it
193 system_update_cpu_freq(160); // ok platform specific - ESP8266 bring-up bench measures raw SoC behavior; no core API exposes it
194 const fl::u32 cyclesPerUs = 160;
195 const fl::u32 idleCycles = 20 * cyclesPerUs; // 20 us of silence ends it
196
197 EdgeCapture& cap = edgeCapture();
198 cap.count = 0;
199 const fl::u32 mask = 1u << rxPin;
200
201 const fl::u32 savedPs = xt_rsil(15); // ok platform specific - masks all IRQs for cycle-exact sampling
202 captureEdges(encoded, nLeds * 12, mask, idleCycles, cap);
203 xt_wsr_ps(savedPs); // ok platform specific - restores IRQ level from xt_rsil
204 system_update_cpu_freq(prevMhz); // ok platform specific - ESP8266 bring-up bench measures raw SoC behavior; no core API exposes it
205
206 // Pair rising/falling edges into high pulses; classify by width.
207 fl::u32 t0hMin = 0xFFFFFFFF, t0hMax = 0, t1hMin = 0xFFFFFFFF, t1hMax = 0;
208 fl::u32 periodMin = 0xFFFFFFFF, periodMax = 0;
209 const fl::u32 splitCycles = (625 * cyclesPerUs) / 1000; // between 312 and 937 ns
210 fl::u8 decoded[kMaxLoopbackLeds * 3] = {};
211 int bits = 0;
212 const int start = cap.firstLevel ? 1 : 0; // first edge must be rising
213 for (int e = start; e + 1 < cap.count && bits < nBytes * 8; e += 2) {
214 const fl::u32 high = cap.edges[e + 1] - cap.edges[e];
215 const bool one = high > splitCycles;
216 if (one) {
217 decoded[bits / 8] |= static_cast<fl::u8>(0x80 >> (bits % 8));
218 if (high < t1hMin) t1hMin = high;
219 if (high > t1hMax) t1hMax = high;
220 } else {
221 if (high < t0hMin) t0hMin = high;
222 if (high > t0hMax) t0hMax = high;
223 }
224 if (e + 2 < cap.count) {
225 const fl::u32 period = cap.edges[e + 2] - cap.edges[e];
226 if (period < periodMin) periodMin = period;
227 if (period > periodMax) periodMax = period;
228 }
229 ++bits;
230 }
231
232 int byteErrors = 0;
233 int firstBad = -1;
234 for (int i = 0; i < nBytes; ++i) {
235 if (decoded[i] != patternByte(i)) {
236 ++byteErrors;
237 if (firstBad < 0) firstBad = i;
238 }
239 }
240
241 auto ns = [cyclesPerUs](fl::u32 c) -> fl::i64 {
242 return c == 0xFFFFFFFF ? -1 : static_cast<fl::i64>(c) * 1000 / cyclesPerUs;
243 };
244 fl::json r = fl::json::object();
245 r.set("success", bits == nBytes * 8 && byteErrors == 0);
246 r.set("leds", static_cast<fl::i64>(nLeds));
247 r.set("rx_pin", static_cast<fl::i64>(rxPin));
248 r.set("edges", static_cast<fl::i64>(cap.count));
249 r.set("bits", static_cast<fl::i64>(bits));
250 r.set("byte_errors", static_cast<fl::i64>(byteErrors));
251 r.set("first_bad_byte", static_cast<fl::i64>(firstBad));
252 if (firstBad >= 0) {
253 r.set("expected", static_cast<fl::i64>(patternByte(firstBad)));
254 r.set("actual", static_cast<fl::i64>(decoded[firstBad]));
255 }
256 r.set("idle_low_before", !cap.firstLevel);
257 r.set("t0h_ns_min", ns(t0hMin));
258 r.set("t0h_ns_max", ns(t0hMax));
259 r.set("t1h_ns_min", ns(t1hMin));
260 r.set("t1h_ns_max", ns(t1hMax));
261 r.set("period_ns_min", ns(periodMin));
262 r.set("period_ns_max", ns(periodMax));
263 return r;
264}
265
266// Drive the same driver through the public API (FastLED.addLeds + show) so
267// the controller path runs on silicon; the pad capture above checks the
268// waveform that path emits.
269struct ShowState {
270 CRGB leds[64];
271 fl::UARTController_ESP8266<GRB> controller;
272 bool added = false;
273};
274
275inline ShowState& showState() {
277}
278
279inline fl::json uartWs2812Show(int nLeds) {
280 if (nLeds < 1) nLeds = 1;
281 if (nLeds > 64) nLeds = 64;
282 ShowState& st = showState();
283 CRGB* leds = st.leds;
284 if (!st.added) {
285 FastLED.addLeds(&st.controller, leds, 64);
286 st.added = true;
287 }
288 st.controller.setLeds(fl::span<CRGB>(leds, nLeds));
289 for (int i = 0; i < nLeds; ++i) {
290 leds[i] = CRGB(patternByte(i * 3), patternByte(i * 3 + 1), patternByte(i * 3 + 2));
291 }
292 const fl::u32 t0 = micros();
293 st.controller.showLeds(255);
294 const fl::u32 elapsed = micros() - t0;
295 fl::json r = fl::json::object();
296 r.set("success", true);
297 r.set("leds", static_cast<fl::i64>(nLeds));
298 r.set("show_us", static_cast<fl::i64>(elapsed));
299 // 30 us of wire time per LED plus the latch is the floor.
300 r.set("expected_min_us", static_cast<fl::i64>(nLeds * 30 + FASTLED_ESP8266_UART_RESET_US));
301 return r;
302}
303
304constexpr int kClocklessPin = FL_PIN_CLOCKLESS_1;
305// The platform's declared hardware-SPI pair in the active pin order -- the same
306// key the native-HSPI specialization uses -- so the bench cannot silently
307// measure the generic bit-bang under D1/NodeMCU numbering. The *RealPin
308// constants are the fixed GPIO pads behind them, which is what pinMode() and
309// GPI take.
310constexpr int kApaDataPin = FL_PIN_SPI_DATA_1;
311constexpr int kApaClockPin = FL_PIN_SPI_CLOCK_1;
312constexpr int kApaDataRealPin = fl::FastPin<FL_PIN_SPI_DATA_1>::kRealPin;
313constexpr int kApaClockRealPin = fl::FastPin<FL_PIN_SPI_CLOCK_1>::kRealPin;
314
315struct DriverShowState {
316 CRGB leds[64];
317 CLEDController* clockless = nullptr;
318 CLEDController* apa102 = nullptr;
319};
320
321inline DriverShowState& driverShowState() {
323}
324
325// Show one controller for `frames` frames and report frame-time spread.
326//
327// `controller->showLeds()` and not FastLED.show(): each bench times its own
328// driver, so registering several controllers on overlapping pads across a
329// session (apa102ShowMhz adds one per rate, and the bit-bang baseline shares
330// GPIO4 with clocklessShow) cannot make two drivers write the same pin in one
331// frame, and FastLED's global refresh-rate clamp never enters these numbers.
332inline fl::json timeFrames(CLEDController* controller, int nLeds, int frames,
333 fl::i64 floorUs, fl::i64* minUsOut = nullptr) {
334 DriverShowState& st = driverShowState();
335 if (nLeds < 1) nLeds = 1;
336 if (nLeds > 64) nLeds = 64;
337 if (frames < 1) frames = 1;
338 if (frames > 200) frames = 200;
339 controller->setLeds(fl::span<CRGB>(st.leds, nLeds));
340 fl::u32 minUs = 0xFFFFFFFF, maxUs = 0;
341 const fl::u32 frames0 = fl::_frame_cnt, retries0 = fl::_retry_cnt;
342 int truncated = 0; // finished faster than the wire allows: frame dropped
343 int firstTruncated = -1;
344 for (int f = 0; f < frames; ++f) {
345 for (int i = 0; i < nLeds; ++i) {
346 st.leds[i] = CHSV(static_cast<fl::u8>(f * 7 + i * 13), 255, 255);
347 }
348 const fl::u32 t0 = micros();
349 controller->showLeds(255);
350 const fl::u32 us = micros() - t0;
351 if (us < minUs) minUs = us;
352 if (us > maxUs) maxUs = us;
353 if (static_cast<fl::i64>(us) < floorUs) {
354 if (firstTruncated < 0) firstTruncated = f;
355 ++truncated;
356 }
357 }
358 if (minUsOut != nullptr) {
359 *minUsOut = static_cast<fl::i64>(minUs);
360 }
361 fl::json r = fl::json::object();
362 r.set("success", truncated == 0);
363 r.set("truncated", static_cast<fl::i64>(truncated));
364 r.set("first_truncated_frame", static_cast<fl::i64>(firstTruncated));
365 r.set("driver_frames_ok", static_cast<fl::i64>(fl::_frame_cnt - frames0));
366 r.set("driver_retries", static_cast<fl::i64>(fl::_retry_cnt - retries0));
367 r.set("uptime_ms", static_cast<fl::i64>(millis()));
368 r.set("wifi_opmode", static_cast<fl::i64>(wifi_get_opmode())); // ok platform specific - ESP8266 bring-up bench measures raw SoC behavior; no core API exposes it
369 r.set("leds", static_cast<fl::i64>(nLeds));
370 r.set("frames", static_cast<fl::i64>(frames));
371 r.set("min_us", static_cast<fl::i64>(minUs));
372 r.set("max_us", static_cast<fl::i64>(maxUs));
373 r.set("floor_us", floorUs);
374 return r;
375}
376
377inline fl::json clocklessShow(int nLeds, int frames) {
378 DriverShowState& st = driverShowState();
379 if (!st.clockless) {
380 st.clockless = &FastLED.addLeds<WS2812B, kClocklessPin, GRB>(st.leds, 64);
381 }
382 const int n = nLeds < 1 ? 1 : (nLeds > 64 ? 64 : nLeds);
383 return timeFrames(st.clockless, n, frames, static_cast<fl::i64>(n) * 30);
384}
385
386// The chipset's own frame-length formula, so the expectation cannot drift from
387// the length the controller actually emits.
388inline fl::i64 apa102WireBytes(int nLeds) {
389 // APA102Controller is declared at global scope (legacy naming).
390 return static_cast<fl::i64>(
392 static_cast<fl::size>(nLeds)));
393}
394
395// Frame time against the wire time the programmed divider implies. A frame also
396// carries FastLED's fixed per-show cost (~250 us on this board, identical on the
397// bit-bang path) plus up to ~120 us of scheduling jitter, so the pass band is
398// the wire time plus that slack. It is needed because these benches pass
399// floorUs = 0: timeFrames can never see a truncated frame here, so its own
400// `success` is unconditional.
401//
402// 400 us still rejects a bus running at half the programmed rate: at 6 MHz that
403// would take 762 us of wire time, i.e. 1012 us per frame against a 781 us bound.
404// A single divider step (~7% here) is inside this band by construction -- the
405// band exists to catch a rate that never took effect, not to fine-tune it.
406constexpr fl::i64 kApa102FrameSlackUs = 400;
407
408// The divider the driver actually programmed, decoded from SPI1CLK:
409// f_spi = 80 MHz / ((CLKDIV_PRE + 1) * (CLKCNT_N + 1))
410// Reading the register back is deliberate -- mirroring the driver's private
411// clock() arithmetic would keep compiling (and keep reporting) after the driver
412// changed, and expected_us would then describe a divider never programmed.
413inline fl::u32 hspiAppliedHz() {
414 const fl::u32 reg = SPI1CLK; // ok platform specific - reads back the divider ESP8266HSPIOutput::init() just wrote
415 if (reg & fl::esp8266::kHspiEquSysClk) { // ok platform specific - SPI1CLK bit 31; no core API reports the bus clock
416 return fl::esp8266::kHspiSourceHz; // ok platform specific - SPI1's fixed 80 MHz source; no core API exposes it
417 }
418 const fl::u32 pre = ((reg >> 18) & 0x1FFFUL) + 1;
419 const fl::u32 n = ((reg >> 12) & 0x3FUL) + 1;
420 return fl::esp8266::kHspiSourceHz / (pre * n); // ok platform specific - the same fixed-source decode; no core API exposes it
421}
422
423inline fl::json apa102RateReport(fl::json r, fl::i64 minUs, int nLeds, int mhz,
424 fl::u32 hz) {
425 const fl::i64 bytes = apa102WireBytes(nLeds);
426 const fl::i64 bits = bytes * 8;
427 const fl::i64 expected = hz ? (bits * 1000000 + hz - 1) / static_cast<fl::i64>(hz) : -1;
428 r.set("success", expected > 0 && minUs >= expected &&
429 minUs <= expected + kApa102FrameSlackUs);
430 r.set("mhz", static_cast<fl::i64>(mhz));
431 r.set("requested_hz", static_cast<fl::i64>(mhz) * 1000000);
432 r.set("achievable_hz", static_cast<fl::i64>(hz));
433 r.set("wire_bytes_est", bytes);
434 r.set("expected_us", expected);
435 // The 80 MHz setting is the system-clock bypass, and the bypass is a GPMUX
436 // mux bit as much as SPI1CLK bit 31. Report both, so a hardware-SPI build
437 // at 80 MHz can show the driver paired them and a divider build can show it
438 // did not touch the mux.
439 const fl::u32 reg = SPI1CLK; // ok platform specific - reads back the clock-source selection init() just wrote
440 r.set("reg_equ", static_cast<fl::i64>((reg >> 31) & 1UL));
441 r.set("gpmux_equ", static_cast<fl::i64>((GPMUX >> 9) & 1UL)); // ok platform specific - the bypass mux bit; no core API reports it
442 return r;
443}
444
445// The chipset default: DATA_RATE_MHZ(6) (fl/chipsets/apa102.h).
446inline fl::json apa102Show(int nLeds, int frames) {
447 DriverShowState& st = driverShowState();
448 if (!st.apa102) {
449 st.apa102 = &FastLED.addLeds<APA102, kApaDataPin, kApaClockPin, BGR>(st.leds, 64);
450 }
451 // init() is the only writer of SPI1CLK, and it re-installs the pad mux:
452 // another bench may have left the bus at its own divider or taken the pads
453 // back as plain GPIO, so re-init rather than trust the addLeds-time setup.
454 st.apa102->init();
455 const fl::u32 hz = hspiAppliedHz();
456 const int n = nLeds < 1 ? 1 : (nLeds > 64 ? 64 : nLeds);
457 // Two statements, not one: reading minUs in the same call that fills it in
458 // leaves the order unspecified.
459 fl::i64 minUs = 0;
460 fl::json r = timeFrames(st.apa102, n, frames, 0, &minUs);
461 return apa102RateReport(r, minUs, n, 6, hz);
462}
463
464inline fl::json hspiPinError(const char* what) {
465 fl::json e = fl::json::object();
466 e.set("success", false);
467 e.set("error", what);
468 return e;
469}
470
471// The same chipset on a pin pair HSPI cannot reach: the generic software SPI
472// bit-bang this platform used for every APA102 before the native path. D2/D0,
473// not the loopback sense pins, so a fitted jumper never has two outputs on it.
474//
475// 4/16 are only GPIO4/GPIO16 in raw pin order, and D1/NodeMCU numbering has no
476// FastPin<16> at all, so the baseline is defined for the default order only.
477#if defined(FASTLED_ESP8266_RAW_PIN_ORDER) // ok platform specific - keys 4/16 are GPIO4/16 only in raw order; no API maps a real pad back to a key
478constexpr int kBitbangDataPin = 4;
479constexpr int kBitbangClockPin = 16;
480#endif
481
482inline fl::json apa102ShowBitbang(int nLeds, int frames) {
483#if !defined(FASTLED_ESP8266_RAW_PIN_ORDER) // ok platform specific - those keys exist only in raw order; no core API can express the pair portably
484 (void)nLeds;
485 (void)frames;
486 return hspiPinError("the bit-bang baseline is only defined in raw pin order");
487#else
488 DriverShowState& st = driverShowState();
489 static CLEDController* c = nullptr;
490 if (c == nullptr) {
491 c = &FastLED.addLeds<APA102, kBitbangDataPin, kBitbangClockPin, BGR>(st.leds, 64);
492 }
493 // Another bench may have taken these pads as inputs (uartWs2812Loopback's
494 // rxPin accepts 4), and addLeds() only initialises once.
495 c->init();
496 const int n = nLeds < 1 ? 1 : (nLeds > 64 ? 64 : nLeds);
497 fl::json r = timeFrames(c, n, frames, 0);
498 r.set("data_pin", static_cast<fl::i64>(kBitbangDataPin));
499 r.set("clock_pin", static_cast<fl::i64>(kBitbangClockPin));
500 r.set("wire_bytes_est", apa102WireBytes(n));
501 // This pair is only the generic bit-bang in the default build. Under
502 // FASTLED_ALL_PINS_HARDWARE_SPI every non-HSPI pair goes to the legacy
503 // Arduino-SPI shim, which drives GPIO13/14 -- so there the timing here is
504 // that shim's, not a software bit-bang, and it is not a baseline.
505#if defined(FASTLED_ALL_PINS_HARDWARE_SPI) // ok platform specific - this build resolves the pair to the Arduino-SPI shim; no API reports the backend
506 r.set("generic_bitbang", false);
507#else
508 r.set("generic_bitbang", true);
509#endif
510 return r;
511#endif // FASTLED_ESP8266_RAW_PIN_ORDER
512}
513
514// One APA102 controller per clock rate. SPI1CLK is written by init() alone, so
515// a controller added at a different rate would otherwise keep the previous
516// divider; the bench re-inits to make each rate real.
517template <int MHZ>
518inline CLEDController* apa102At(CRGB* leds) {
519 static CLEDController* c =
520 &FastLED.addLeds<APA102, kApaDataPin, kApaClockPin, BGR, DATA_RATE_MHZ(MHZ)>(leds, 64);
521 return c;
522}
523
524inline fl::json apa102ShowMhz(int nLeds, int frames, int mhz) {
525 DriverShowState& st = driverShowState();
526 CLEDController* c = nullptr;
527 switch (mhz) {
528 case 6: c = apa102At<6>(st.leds); break;
529 case 10: c = apa102At<10>(st.leds); break;
530 case 20: c = apa102At<20>(st.leds); break;
531 case 40: c = apa102At<40>(st.leds); break;
532 case 80: c = apa102At<80>(st.leds); break;
533 default: {
534 fl::json e = fl::json::object();
535 e.set("success", false);
536 e.set("error", "mhz must be 6, 10, 20, 40 or 80");
537 return e;
538 }
539 }
540 const int n = nLeds < 1 ? 1 : (nLeds > 64 ? 64 : nLeds);
541 c->init();
542 // After init(): the divider under test is the one in the register.
543 const fl::u32 hz = hspiAppliedHz();
544 fl::i64 minUs = 0;
545 fl::json r = timeFrames(c, n, frames, 0, &minUs);
546 r = apa102RateReport(r, minUs, n, mhz, hz);
547 r.set("cpu_mhz", static_cast<fl::i64>(system_get_cpu_freq())); // ok platform specific - the bench reports the raw SoC clock; no core API exposes it
548 return r;
549}
550
551// Pin-level HSPI loopback. GPIO_IN does not see GPIO13/14 once HSPI owns the
552// pads, so read MOSI and SCK back through jumpers: SCK (D5, GPIO14) to sckPin
553// and MOSI (D7, GPIO13) to mosiPin. The ISR has to
554// sample the data, because the driver's flush() blocks on the wire. Sense pins
555// are arguments, so a board where those pads are hard to reach can use any two
556// of the pins hspiSensePinOk() allows.
557constexpr int kHspiLoopBytes = 96;
558
559struct HspiLoopbackState {
560 fl::u8 got[kHspiLoopBytes];
561 // Staged here rather than on the handler's stack: this file already split
562 // its bind functions to stay inside 4 KB of loop stack, and the pattern is
563 // fully derivable, so the bytes cost nothing in .bss.
564 fl::u8 tx[kHspiLoopBytes];
565 int bits;
566 int rising;
567 int falling;
568 fl::u32 mosiMask;
569 fl::u32 sckMask;
570};
571
572inline HspiLoopbackState& hspiLoopbackState() {
574}
575
576// The ISR runs on every SCK edge, so it reads a cached pointer rather than
577// paying the singleton lookup, and the accessor must inline so the ISR never
578// fetches from flash mid-transfer.
579__attribute__((always_inline)) inline HspiLoopbackState*& hspiLoopbackActive() {
580 static HspiLoopbackState* s = nullptr;
581 return s;
582}
583
584inline fl::u8 hspiPattern(int i) { return static_cast<fl::u8>(i * 37 + 11); }
585
586// Samples MOSI on every SCK edge and keeps the rising ones (mode 0), so it
587// must stay short: at the bench's clock the ISR gets a few microseconds.
588FL_NO_INLINE static FL_IRAM void hspiSckSample() {
589 HspiLoopbackState& s = *hspiLoopbackActive();
590 const fl::u32 gpi = GPI; // ok platform specific - raw pad sampling for the HSPI pin-level loopback
591 if (gpi & s.sckMask) {
592 ++s.rising;
593 if (s.bits < kHspiLoopBytes * 8) {
594 if (gpi & s.mosiMask) {
595 s.got[s.bits >> 3] |= static_cast<fl::u8>(0x80 >> (s.bits & 7));
596 }
597 ++s.bits;
598 }
599 } else {
600 ++s.falling;
601 }
602}
603
604// The ISR has to finish inside one bit period, so the loopback runs far below
605// the driver's normal clock: at 40 us per bit even the core's generic ISR
606// dispatch has slack.
607template <int KHZ>
608inline fl::json apa102HspiLoopbackAt(int mosiPin, int sckPin) {
609 HspiLoopbackState& s = hspiLoopbackState();
610 hspiLoopbackActive() = &s; // cache for the ISR (constant-initialized static)
611 s.mosiMask = 1u << mosiPin;
612 s.sckMask = 1u << sckPin;
613 s.bits = 0;
614 s.rising = 0;
615 s.falling = 0;
616 for (int i = 0; i < kHspiLoopBytes; ++i) {
617 s.got[i] = 0;
618 s.tx[i] = hspiPattern(i);
619 }
620
621 fl::SPIOutput<kApaDataPin, kApaClockPin, DATA_RATE_KHZ(KHZ)> spi;
622 spi.init();
623 // Snapshot the pad mux the driver just installed. If GPF14 is not the BUS
624 // function, the peripheral shifts the frame internally and nothing reaches
625 // the wire -- which the frame time alone cannot tell you, because the shift
626 // takes exactly as long either way.
627 const fl::u32 gpf13 = GPF13; // ok platform specific - reads back the pad function just installed; no core API reports it
628 const fl::u32 gpf14 = GPF14; // ok platform specific - reads back the pad function just installed; no core API reports it
629 const fl::u32 gpe = GPE; // ok platform specific - reads back the GPIO output enables; no core API reports them
630 pinMode(mosiPin, INPUT);
631 pinMode(sckPin, INPUT);
632 spi.select();
633 const fl::u32 t0 = micros();
634 attachInterrupt(digitalPinToInterrupt(sckPin), hspiSckSample, CHANGE);
635 spi.writeBytes(s.tx, kHspiLoopBytes);
636 const fl::u32 elapsed = micros() - t0;
637 // writeBytes blocks in kick() until SPIBUSY clears, but stay attached
638 // through endTransaction() so no tail edge can be missed.
639 spi.endTransaction();
640 const int risingAfter = s.rising;
641 const int fallingAfter = s.falling;
642 const int bitsAfter = s.bits;
643
644 int badBytes = 0, firstBad = -1;
645 fl::u8 firstBadGot = 0;
646 for (int i = 0; i < kHspiLoopBytes; ++i) {
647 if (s.got[i] != hspiPattern(i)) {
648 if (firstBad < 0) {
649 firstBad = i;
650 // Read now: the probe below drives SCK through this same ISR,
651 // and when the transfer was short it keeps filling got[].
652 firstBadGot = s.got[i];
653 }
654 ++badBytes;
655 }
656 }
657
658 // Was that ISR live for the whole transfer? Drive the SCK pad from the CPU
659 // now, with the same ISR still attached: if these edges are counted but the
660 // transfer's were not, the attach held and the peripheral never drove the
661 // pad -- as opposed to a dead ISR or a missing jumper. Runs after the
662 // measurements so its edges cannot leak into bits/got[].
663 pinMode(kApaClockRealPin, OUTPUT);
664 for (int i = 0; i < 3; ++i) {
665 digitalWrite(kApaClockRealPin, LOW);
667 digitalWrite(kApaClockRealPin, HIGH);
669 }
670 const int probeEdges = s.rising - risingAfter;
671 detachInterrupt(digitalPinToInterrupt(sckPin));
672 spi.stop();
673
674 fl::json r = fl::json::object();
675 r.set("success", bitsAfter == kHspiLoopBytes * 8 && badBytes == 0);
676 r.set("bytes", static_cast<fl::i64>(kHspiLoopBytes));
677 r.set("bits", static_cast<fl::i64>(bitsAfter));
678 r.set("sck_rising_edges", static_cast<fl::i64>(risingAfter));
679 r.set("sck_falling_edges", static_cast<fl::i64>(fallingAfter));
680 // The pad mux the driver installed, and whether the ISR was still live when
681 // the CPU drove the pad: together these separate "no clock reached the pad"
682 // from "no jumper" and from "dead ISR".
683 r.set("gpf13", static_cast<fl::i64>(gpf13));
684 r.set("gpf14", static_cast<fl::i64>(gpf14));
685 r.set("gpe", static_cast<fl::i64>(gpe));
686 r.set("probe_edges", static_cast<fl::i64>(probeEdges));
687 r.set("isr_live_after_transfer", probeEdges == 3);
688 r.set("bad_bytes", static_cast<fl::i64>(badBytes));
689 r.set("first_bad", static_cast<fl::i64>(firstBad));
690 if (firstBad >= 0) {
691 r.set("first_bad_got", static_cast<fl::i64>(firstBadGot));
692 r.set("first_bad_want", static_cast<fl::i64>(hspiPattern(firstBad)));
693 }
694 r.set("khz", static_cast<fl::i64>(KHZ));
695 r.set("elapsed_us", static_cast<fl::i64>(elapsed));
696 // The ISR budget is one SCK period, and the radio's MAC interrupts eat it:
697 // a run with the radio up mis-times edges and reads as a wiring failure.
698 r.set("wifi_opmode", static_cast<fl::i64>(wifi_get_opmode())); // ok platform specific - ESP8266 bring-up bench measures raw SoC behavior; no core API exposes it
699 return r;
700}
701
702// A sense pin has to be one of the pins that stay usable while the RPC console
703// and the bus under test are busy: 1 and 3 are UART0 (the console), 2 is the
704// UART bench's TX pad, 4 carries clocklessShow's WS2812 output, 6-11 are the
705// flash pads (pinMode on them resets the board), and 13/14 are the HSPI pads the
706// transfer is driving -- pinMode there would undo the bus mux. GPIO16 is absent from GPI, so it needs GP16I. `other`
707// excludes the pin already used for the other signal, so a self-loop cannot
708// pass with no jumper fitted.
709inline bool hspiSensePinOk(int pin, int other) {
710 const bool usable = pin == 0 || pin == 5 || pin == 12 || pin == 15; // ok platform specific - the ESP8266 pads left usable on a NodeMCU; no core API reports which real pads the active pin order leaves free
711 return usable && pin != other;
712}
713
714// Drives an HSPI output pad as a plain GPIO and both counts interrupts and
715// samples the level on sensePin, so "no jumper fitted" (level never follows, no
716// edges) is told apart from "the HSPI path never drove the pad" (level follows,
717// no edges) and from a dead ISR. Only meaningful with that jumper fitted.
718//
719// txPin is the pad to drive (13 = MOSI/D7, 14 = SCK/D5); sensePin is where the
720// jumper takes it (12 = D6, 5 = D1). Checking both signals separately is what
721// tells a quiet data line from a missing data jumper.
722inline fl::json apa102HspiJumperCheck(int sensePin, int txPin) {
723 if (!hspiSensePinOk(sensePin, -1)) {
724 return hspiPinError("sensePin must be one of 0, 5, 12, 15");
725 }
726 if (txPin != kApaDataRealPin && txPin != kApaClockRealPin) {
727 return hspiPinError("txPin must be 13 (MOSI/D7) or 14 (SCK/D5)");
728 }
729 const int sckPin = sensePin;
730 const int drivePin = txPin;
731 HspiLoopbackState& s = hspiLoopbackState();
732 hspiLoopbackActive() = &s;
733 s.mosiMask = 0; // this check samples SCK only; no stale mask from a loopback
734 s.sckMask = 1u << sckPin;
735 s.bits = 0;
736 s.rising = 0;
737 s.falling = 0;
738 pinMode(sckPin, INPUT);
739 pinMode(drivePin, OUTPUT);
740 // pinMode(OUTPUT) does not clear the output latch, so start from a known
741 // low: a high latch would swallow the first rising edge.
742 digitalWrite(drivePin, LOW);
743 attachInterrupt(digitalPinToInterrupt(sckPin), hspiSckSample, CHANGE);
744 bool seenHigh = false, seenLow = false;
745 for (int i = 0; i < 10; ++i) {
746 digitalWrite(drivePin, HIGH);
748 if (GPI & s.sckMask) { // ok platform specific - raw pad level tells a missing jumper from a dead ISR
749 seenHigh = true;
750 }
751 digitalWrite(drivePin, LOW);
753 if ((GPI & s.sckMask) == 0) { // ok platform specific - raw pad level, as above
754 seenLow = true;
755 }
756 }
757 detachInterrupt(digitalPinToInterrupt(sckPin));
758 // Give the pad back to HSPI, not to the GPIO output register: a later
759 // apa102Show that skipped init() would otherwise clock internally with the
760 // pad held low and still report normal timing.
761 pinMode(drivePin, SPECIAL); // ok platform specific - SPECIAL is the Arduino-core spelling of "restore this pad's BUS function", i.e. give the pad back to HSPI; no core API restores a bus function
762 const bool levelFollows = seenHigh && seenLow;
763 const bool isrCounted = s.rising == 10 && s.falling == 10;
764 fl::json r = fl::json::object();
765 // A floating pin can both follow nothing and (rarely) produce noise edges,
766 // so the level is the authority on whether the jumper is really fitted.
767 r.set("success", levelFollows && isrCounted);
768 r.set("level_follows_tx", levelFollows);
769 r.set("isr_counted_edges", isrCounted);
770 r.set("rising", static_cast<fl::i64>(s.rising));
771 r.set("falling", static_cast<fl::i64>(s.falling));
772 r.set("sense_pin", static_cast<fl::i64>(sensePin));
773 r.set("tx_pin", static_cast<fl::i64>(txPin));
774 return r;
775}
776
777inline fl::json apa102HspiLoopback(int khz, int mosiPin, int sckPin) {
778 if (!hspiSensePinOk(mosiPin, sckPin) || !hspiSensePinOk(sckPin, mosiPin)) {
779 return hspiPinError(
780 "sense pins must be two different pins from 0, 5, 12, 15");
781 }
782 // Rates the driver resolves cleanly with a divider the ISR can keep up
783 // with: at 25 kHz a bit lasts 40 us, so even the core's generic ISR
784 // dispatch has room, and the radio must be off (wifiOff) for the rest.
785 switch (khz) {
786 case 25: return apa102HspiLoopbackAt<25>(mosiPin, sckPin);
787 case 50: return apa102HspiLoopbackAt<50>(mosiPin, sckPin);
788 default: {
789 fl::json e = fl::json::object();
790 e.set("success", false);
791 e.set("error", "khz must be 25 or 50");
792 return e;
793 }
794 }
795}
796
797// Watch the wire from inside the transfer instead of from an ISR.
798//
799// kick() only ever looks at SPIBUSY, so on its own it cannot tell a transfer
800// that drove the pads from one that only shifted internally -- both take the
801// same time. This stages and starts the transfer by hand and polls the sense
802// pin in the wait loop, so a count of zero here means the pad really was quiet
803// (and not merely that no interrupt happened to run).
804inline fl::json apa102HspiWatch(int sckPin, int nBytes, int nField) {
805 if (!hspiSensePinOk(sckPin, -1)) {
806 return hspiPinError("sckPin must be one of 0, 5, 12, 15");
807 }
808 const int n = nBytes < 1 ? 1 : (nBytes > 64 ? 64 : nBytes);
809 fl::SPIOutput<kApaDataPin, kApaClockPin, DATA_RATE_KHZ(25)> spi;
810 spi.init();
811 // nField >= 0 re-splits the same total divider across CLKCNT_N and
812 // CLKDIV_PRE. Before kHspiMinNDivider the driver resolved 25 kHz to
813 // CLKDIV_PRE = 3201 with CLKCNT_N = CLKCNT_L = 0, and CLKCNT_L is the
814 // clock's high phase, so the pad had nothing to show; re-splitting the same
815 // divider with CLKCNT_N = 1 (CLKCNT_L = 1) is how that was confirmed on the
816 // wire. -1 runs whatever the driver installed, which is what a fixed build
817 // should be measured with.
818 const fl::u32 clkBefore = SPI1CLK; // ok platform specific - reads back the divider the driver installed
819 if (nField >= 0) {
820 const fl::u32 pre1 = ((clkBefore >> 18) & 0x1FFFUL) + 1;
821 const fl::u32 n1 = ((clkBefore >> 12) & 0x3FUL) + 1;
822 const fl::u32 divider = pre1 * n1;
823 const fl::u32 nNew = (static_cast<fl::u32>(nField) & 0x3FUL) + 1;
824 const fl::u32 preNew = divider / nNew == 0 ? 1 : divider / nNew;
825 // Same field layout the driver uses, plus a half-period low phase.
826 SPI1CLK = ((nNew / 2) & 0x3FUL) | ((nNew - 1) << 12) | ((preNew - 1) << 18); // ok platform specific - rebuilds SPI1CLK by hand; no core API exposes it
827 }
828 pinMode(sckPin, INPUT);
829 const fl::u32 mask = 1u << sckPin;
830 // Same staging as the driver's flush(): length in SPI1U1, bytes packed
831 // little-endian into the FIFO words.
832 const fl::u32 bits = static_cast<fl::u32>(n) * 8 - 1;
833 const fl::u32 lenMask = (static_cast<fl::u32>(SPIMMOSI) << SPILMOSI) | // ok platform specific - SPI1U field masks/shifts; no core API exposes them
834 (static_cast<fl::u32>(SPIMMISO) << SPILMISO); // ok platform specific - SPI1U field masks/shifts; no core API exposes them
835 while (SPI1CMD & SPIBUSY) {} // ok platform specific - waits out a previous transfer; no core API polls SPI1CMD
836 SPI1U1 = (SPI1U1 & ~lenMask) | (bits << SPILMOSI) | (bits << SPILMISO); // ok platform specific - sets the SPI1 transfer length, as the driver's flush() does
837 volatile fl::u32* fifo = &SPI1W0; // ok platform specific - SPI1 FIFO staging; no core API exposes it
838 for (int i = 0; i < (n + 3) / 4; ++i) {
839 const int b = i * 4;
840 fl::u32 word = static_cast<fl::u32>(hspiPattern(b));
841 if (b + 1 < n) word |= static_cast<fl::u32>(hspiPattern(b + 1)) << 8;
842 if (b + 2 < n) word |= static_cast<fl::u32>(hspiPattern(b + 2)) << 16;
843 if (b + 3 < n) word |= static_cast<fl::u32>(hspiPattern(b + 3)) << 24;
844 fifo[i] = word;
845 }
846 int transitions = 0, highSamples = 0, samples = 0;
847 fl::u32 last = GPI & mask; // ok platform specific - raw pad level while the bus runs; no core API reports it
848 const fl::u32 t0 = micros();
849 // The same barrier the driver's kick() issues: the SPI1U1 length and FIFO
850 // writes above must be visible before the peripheral is told to start, or
851 // the transfer runs with init()'s leftover length and the counts describe
852 // the wrong frame.
853 __sync_synchronize();
854 SPI1CMD |= SPIBUSY; // ok platform specific - starts the hand-staged transfer; no core API starts a raw SPI1 run
855 while (SPI1CMD & SPIBUSY) { // ok platform specific - polls the bus while sampling the pad; no core API exposes SPI1CMD
856 const fl::u32 now = GPI & mask; // ok platform specific - raw pad level while the bus runs
857 ++samples;
858 if (now != last) {
859 ++transitions;
860 last = now;
861 }
862 if (now != 0) {
863 ++highSamples;
864 }
865 }
866 const fl::u32 elapsed = micros() - t0;
867 spi.stop();
868
869 fl::json r = fl::json::object();
870 r.set("success", transitions > 0);
871 r.set("khz", static_cast<fl::i64>(25));
872 r.set("bytes", static_cast<fl::i64>(n));
873 r.set("bits", static_cast<fl::i64>(n) * 8);
874 r.set("sck_pin", static_cast<fl::i64>(sckPin));
875 r.set("transitions", static_cast<fl::i64>(transitions));
876 r.set("high_samples", static_cast<fl::i64>(highSamples));
877 r.set("samples", static_cast<fl::i64>(samples));
878 r.set("elapsed_us", static_cast<fl::i64>(elapsed));
879 // SPI1CLK: [5:0] CLKCNT_L, [11:6] CLKCNT_H, [17:12] CLKCNT_N, [30:18] PRE.
880 // Reported after the override, so these are the fields the transfer ran
881 // with rather than a mirror of what init() left behind.
882 const fl::u32 clkRan = SPI1CLK; // ok platform specific - reads back the divider the transfer actually ran with
883 r.set("clk_cnt_l", static_cast<fl::i64>(clkRan & 0x3FUL));
884 r.set("clk_cnt_h", static_cast<fl::i64>((clkRan >> 6) & 0x3FUL));
885 r.set("clk_cnt_n", static_cast<fl::i64>((clkRan >> 12) & 0x3FUL));
886 r.set("clk_pre", static_cast<fl::i64>((clkRan >> 18) & 0x1FFFUL));
887 r.set("clk_before_pre", static_cast<fl::i64>((clkBefore >> 18) & 0x1FFFUL));
888 r.set("n_field", static_cast<fl::i64>(nField));
889 return r;
890}
891
892// Turn the radio off so its MAC NMIs stop preempting bit-banged output.
893inline fl::json wifiOff() {
894 wifi_set_opmode_current(NULL_MODE); // ok platform specific - ESP8266 bring-up bench measures raw SoC behavior; no core API exposes it
895 fl::json r = fl::json::object();
896 r.set("success", wifi_get_opmode() == NULL_MODE); // ok platform specific - ESP8266 bring-up bench measures raw SoC behavior; no core API exposes it
897 r.set("wifi_opmode", static_cast<fl::i64>(wifi_get_opmode())); // ok platform specific - ESP8266 bring-up bench measures raw SoC behavior; no core API exposes it
898 return r;
899}
900
901// UART1 internal loopback (UCLBE): TX feeds the RX FIFO inside the
902// peripheral, so this proves the encoder, 6N1 framing and 3.2 Mbaud timing
903// without a jumper. It does not see the GPIO2 pad.
904inline fl::json uartWs2812InternalLoopback(fl::u32 baud) {
905 namespace u = fl::esp8266_uart;
906 u::begin(baud ? baud : FASTLED_ESP8266_UART_BAUD);
907 // The loopback taps the line after TX inversion and RX inversion does
908 // not apply to it, so drop TX inversion for the self-test: RX then sees
909 // ordinary 6N1 frames. The inversion itself is one register bit.
910 USC0(u::kUart) &= ~(1u << UCTXI); // ok platform specific - UART1 internal-loopback registers; no core API
911 USC0(u::kUart) |= (1u << UCLBE); // ok platform specific - UART1 internal-loopback registers; no core API
912 USC0(u::kUart) |= (1u << UCRXRST); // ok platform specific - UART1 internal-loopback registers; no core API
913 USC0(u::kUart) &= ~(1u << UCRXRST); // ok platform specific - UART1 internal-loopback registers; no core API
914 const fl::u8 pixel[3] = {0xA5, 0x0F, 0xC3};
915 fl::u8 sent[12];
916 for (int i = 0; i < 3; ++i) {
917 u::encodeByte(pixel[i], &sent[i * 4]);
918 }
919 u::write(sent, sizeof(sent));
920 u::flush();
921 fl::delayMicroseconds(20 + 9000000 / (baud ? baud : FASTLED_ESP8266_UART_BAUD));
922 const fl::u32 got = (USS(u::kUart) >> USRXC) & 0xFF; // ok platform specific - UART1 internal-loopback registers; no core API
923 fl::u32 matched = 0;
924 fl::json rx = fl::json::array();
925 for (fl::u32 i = 0; i < got && i < sizeof(sent); ++i) {
926 const fl::u8 c = static_cast<fl::u8>(USF(u::kUart) & 0x3F); // ok platform specific - UART1 internal-loopback registers; no core API
927 rx.push_back(static_cast<fl::i64>(c));
928 if (c == (sent[i] & 0x3F)) {
929 ++matched;
930 }
931 }
932 USC0(u::kUart) &= ~(1u << UCLBE); // ok platform specific - UART1 internal-loopback registers; no core API
933 USC0(u::kUart) |= (1u << UCTXI); // ok platform specific - UART1 internal-loopback registers; no core API
934 fl::json r = fl::json::object();
935 r.set("sent", static_cast<fl::i64>(sizeof(sent)));
936 r.set("received", static_cast<fl::i64>(got));
937 r.set("matched", static_cast<fl::i64>(matched));
938 r.set("rx", rx);
939 r.set("success", got == sizeof(sent) && matched == sizeof(sent));
940 return r;
941}
942
943// Drive GPIO2 as a plain GPIO high then low and read rxPin each time, so a
944// missing jumper is told apart from a UART1 pin-mux fault. The next UART call
945// re-runs esp8266_uart::begin(), which hands GPIO2 back to UART1.
946inline fl::json jumperCheck(int rxPin) {
947 fl::json r = fl::json::object();
948 if (!isUsableRxPin(rxPin)) {
949 r.set("success", false);
950 r.set("error", "rxPin must be one of 0,4,5,12,13,14,15");
951 return r;
952 }
953 pinMode(rxPin, INPUT);
954 pinMode(2, OUTPUT);
955 const fl::u32 mask = 1u << rxPin;
956 GPOS = 1u << 2; // ok platform specific - plain-GPIO jumper check on raw GPIO registers
958 const bool seenHigh = (GPI & mask) != 0; // ok platform specific - plain-GPIO jumper check on raw GPIO registers
959 GPOC = 1u << 2; // ok platform specific - plain-GPIO jumper check on raw GPIO registers
961 const bool seenLow = (GPI & mask) == 0; // ok platform specific - plain-GPIO jumper check on raw GPIO registers
962 r.set("rx_pin", static_cast<fl::i64>(rxPin));
963 r.set("rx_high_when_tx_high", seenHigh);
964 r.set("rx_low_when_tx_low", seenLow);
965 r.set("success", seenHigh && seenLow);
966 return r;
967}
968
969// ESP8266 RPC argument reader. Each bench below is bound with one
970// `const fl::json&` parameter -- the JSON array the host passes -- instead of
971// C++ positional parameters, because ci.rpc_client.RpcClient frames every call
972// as a single `params` value (params:[args]): a raw bind of arity 0 or 2+ is
973// unreachable through that client. Returns `fallback` for an absent, short, or
974// non-numeric field.
975inline int rpcArgInt(const fl::json& args, fl::size index, int fallback) {
976 if (args.is_array() && index < args.size() && args[index].is_number()) {
977 const fl::optional<fl::i64> value = args[index].as_int();
978 if (value.has_value()) {
979 return static_cast<int>(value.value());
980 }
981 }
982 return fallback;
983}
984
985// Each bind instantiates sizeable temporaries; split the bindings across
986// non-inlined functions so their frames do not add up on the 4 KB loop
987// stack (one combined frame overflowed it).
988FL_NO_INLINE inline void bindEsp8266WatchdogRpcs(fl::Remote& remote) {
989 // Watchdog bring-up: read why the chip last reset, or stop feeding the
990 // watchdog (while still yielding) so the user timeout must fire.
991 remote.bind("resetCause", [](const fl::json& /*args*/) -> fl::json {
992 auto& wdt = fl::Watchdog::instance();
993 fl::json r = fl::json::object();
994 r.set("cause", fl::string(fl::resetCauseName(wdt.lastResetCause())));
995 r.set("crash_count", static_cast<fl::i64>(wdt.consecutiveCrashCount()));
996 // Loop-stack headroom (4 KB total); the bind/dispatch path runs close.
997 r.set("cont_stack_free", static_cast<fl::i64>(cont_get_free_stack(g_pcont))); // ok platform specific - loop-stack headroom diagnostic
998 return r;
999 });
1000 remote.bind("watchdogStall", [](const fl::json& args) -> fl::json {
1001 const int ms = rpcArgInt(args, 0, -1);
1002 if (ms < 0) {
1003 fl::json e = fl::json::object();
1004 e.set("error", "ms must be >= 0");
1005 return e;
1006 }
1007 const fl::u32 start = fl::millis();
1008 while (fl::millis() - start < static_cast<fl::u32>(ms)) {
1009 fl::delay(10); // yields, so only fl::Watchdog can fire
1010 }
1011 fl::json r = fl::json::object();
1012 r.set("survived_ms", static_cast<fl::i64>(fl::millis() - start));
1013 return r;
1014 });
1015}
1016
1017FL_NO_INLINE inline void bindEsp8266UartRpcs(fl::Remote& remote) {
1018 // rxPin < 0 runs the jumper-free internal loopback instead, with nLeds
1019 // as the rate in kbaud (0 = the driver's 3.2 Mbaud).
1020 remote.bind("uartWs2812Loopback", [](const fl::json& args) -> fl::json {
1021 const int nLeds = rpcArgInt(args, 0, -1);
1022 const int rxPin = rpcArgInt(args, 1, -1);
1023 if (rxPin < 0) {
1024 // kbaud; 0 = default. USD = ESP8266_CLOCK / baud must stay >= 1.
1025 if (nLeds < 0 || nLeds > 80000) {
1026 fl::json e = fl::json::object();
1027 e.set("error", "kbaud must be in [0, 80000]");
1028 return e;
1029 }
1030 return uartWs2812InternalLoopback(static_cast<fl::u32>(nLeds) * 1000u);
1031 }
1032 // nLeds == 0 checks the jumper itself: GPIO2 as plain GPIO.
1033 if (nLeds == 0) {
1034 return jumperCheck(rxPin);
1035 }
1036 return uartWs2812Loopback(nLeds, rxPin);
1037 });
1038 remote.bind("uartWs2812Show", [](const fl::json& args) -> fl::json {
1039 return uartWs2812Show(rpcArgInt(args, 0, 0));
1040 });
1041}
1042
1043FL_NO_INLINE inline void bindEsp8266DriverRpcs(fl::Remote& remote) {
1044 remote.bind("wifiOff", [](const fl::json& /*args*/) -> fl::json { return wifiOff(); });
1045 remote.bind("clocklessShow", [](const fl::json& args) -> fl::json {
1046 return clocklessShow(rpcArgInt(args, 0, 0), rpcArgInt(args, 1, 1));
1047 });
1048 remote.bind("apa102Show", [](const fl::json& args) -> fl::json {
1049 return apa102Show(rpcArgInt(args, 0, 0), rpcArgInt(args, 1, 1));
1050 });
1051}
1052
1053// Own frame: every bind's temporaries get a stack slot in the caller, and one
1054// more bind in bindEsp8266DriverRpcs overflowed the 4 KB loop stack.
1055FL_NO_INLINE inline void bindEsp8266HspiRpcs(fl::Remote& remote) {
1056 remote.bind("apa102ShowMhz", [](const fl::json& args) -> fl::json {
1057 return apa102ShowMhz(rpcArgInt(args, 0, 0), 20, rpcArgInt(args, 1, 6));
1058 });
1059 remote.bind("apa102HspiLoopback", [](const fl::json& args) -> fl::json {
1060 return apa102HspiLoopback(rpcArgInt(args, 0, 0), rpcArgInt(args, 1, -1),
1061 rpcArgInt(args, 2, -1));
1062 });
1063 remote.bind("apa102HspiJumperCheck", [](const fl::json& args) -> fl::json {
1064 return apa102HspiJumperCheck(rpcArgInt(args, 0, -1), rpcArgInt(args, 1, -1));
1065 });
1066 remote.bind("apa102HspiWatch", [](const fl::json& args) -> fl::json {
1067 return apa102HspiWatch(rpcArgInt(args, 0, -1), rpcArgInt(args, 1, 1),
1068 rpcArgInt(args, 2, -1));
1069 });
1070}
1071
1072// A third frame for the same reason: keep every bind's temporaries in a frame
1073// small enough for the 4 KB loop stack.
1074FL_NO_INLINE inline void bindEsp8266HspiBenchRpcs(fl::Remote& remote) {
1075 remote.bind("apa102ShowBitbang", [](const fl::json& args) -> fl::json {
1076 return apa102ShowBitbang(rpcArgInt(args, 0, 0), rpcArgInt(args, 1, 1));
1077 });
1078}
1079
1080// I2S TX -> RX loopback (#4943 engine + #4945 capture): encode a known pattern
1081// with the I2S codec, stream it out of the I2S peripheral, capture it back
1082// through the I2S RX path, decode, and compare -- all in software on-device.
1083FL_NO_INLINE inline void bindEsp8266I2sRpcs(fl::Remote& remote) {
1084 remote.bind("i2sLoopback", [](int nBytes) -> fl::json {
1085 fl::json r = fl::json::object();
1086 if (nBytes < 1 || nBytes > 300) {
1087 r.set("error", "nBytes must be in [1, 300]");
1088 return r;
1089 }
1090 const int n = nBytes;
1091 static fl::u8 tx[300];
1092 for (int i = 0; i < n; ++i) {
1093 tx[i] = static_cast<fl::u8>(i * 53 + 0x1B);
1094 }
1095 // Encode as the I2S engine does: two 16-bit samples per LED byte.
1096 static fl::u16 samples[600];
1097 for (int i = 0; i < n; ++i) {
1098 fl::esp8266::encodeByte(tx[i], &samples[i * 2]);
1099 }
1100 const int nSamples = n * 2;
1101 // TX + RX on the one I2S peripheral. Only ONE side may drive the clocks
1102 // (driving both from the same peripheral conflicts and yields silence).
1103 (void)i2s_rxtxdrive_begin(true, true, true, false);
1104 i2s_set_rate(fl::esp8266::kI2sSlotHz);
1105 i2s_set_bits(16);
1106 for (int i = 0; i < nSamples; ++i) {
1107 (void)i2s_write_sample_nb(samples[i]);
1108 }
1109 static fl::u16 got[600];
1110 int cap = 0;
1111 const fl::u32 deadline = fl::millis() + 20;
1112 while (cap < nSamples && fl::millis() < deadline) {
1113 if (i2s_rx_available() > 0) {
1114 fl::i16 l = 0, rr = 0;
1115 if (i2s_read_sample(&l, &rr, false)) {
1116 // The core packs (right<<16)|left; the engine's 16-bit sample
1117 // may land in either half depending on the loopback wiring.
1118 fl::u16 s = static_cast<fl::u16>(l);
1119 if (s == 0) {
1120 s = static_cast<fl::u16>(rr);
1121 }
1122 got[cap++] = s;
1123 }
1124 }
1125 }
1126 int firstBad = -1;
1127 for (int i = 0; i < n; ++i) {
1128 if (i * 2 + 1 >= cap) { firstBad = i; break; }
1129 if (fl::esp8266::decodeByte(got[i * 2], got[i * 2 + 1]) != tx[i]) {
1130 firstBad = i;
1131 break;
1132 }
1133 }
1134 r.set("success", firstBad < 0);
1135 r.set("sent", static_cast<fl::i64>(n));
1136 r.set("captured_samples", static_cast<fl::i64>(cap));
1137 r.set("first_bad", static_cast<fl::i64>(firstBad));
1138 r.set("s0", static_cast<fl::i64>(samples[0]));
1139 r.set("g0", static_cast<fl::i64>(got[0]));
1140 r.set("g1", static_cast<fl::i64>(got[1]));
1141 r.set("dec0", static_cast<fl::i64>(fl::esp8266::decodeByte(got[0], got[1])));
1142 return r;
1143 });
1144}
1145
1146// HSPI (SPI1) master loopback (#4942): drive bytes out of MOSI (GPIO13) and
1147// read them back from MISO (GPIO12), which the ESP8266 HSPI does through the
1148// SAME register -- after SPI1CMD/SPIBUSY clears, `SPI1W0 & 0xFF` holds the byte
1149// clocked in on MISO. Jumper D7 (GPIO13) -> D6 (GPIO12) and every byte comes
1150// back. This captures the real hardware SPI output byte-exactly.
1151FL_NO_INLINE inline void bindEsp8266SpiRpcs(fl::Remote& remote) {
1152 remote.bind("spiLoopback", [](int nBytes) -> fl::json {
1153 fl::json r = fl::json::object();
1154 if (nBytes < 1 || nBytes > 64) {
1155 r.set("error", "nBytes must be in [1, 64]");
1156 return r;
1157 }
1158 const int n = nBytes;
1159 // Pads: SPECIAL routes each pad to the SPI1 peripheral function. The
1160 // core's SPI.begin() does exactly this for MISO(12)/MOSI(13)/SCK(14);
1161 // omitting MISO leaves it as a plain GPIO, so the duplex receive reads 0
1162 // even when D7->D6 is wired.
1163 pinMode(12, SPECIAL); // MISO -> SPI1 (data in)
1164 pinMode(13, SPECIAL); // MOSI -> SPI1 (data out)
1165 pinMode(14, SPECIAL); // SCK -> SPI1
1166 while (SPI1CMD & SPIBUSY) {
1167 }
1168 SPI1C = 0;
1169 SPI1C1 = 0;
1170 GPMUX &= ~(1UL << 9);
1171 SPI1CLK = fl::esp8266::hspiClockFor(1000000).reg_value; // 1 MHz
1172 SPI1U = SPIUMOSI | SPIUDUPLEX | SPIUSSE;
1173 SPI1U1 = (7UL << SPILMOSI) | (7UL << SPILMISO);
1174
1175 int firstBad = -1;
1176 fl::u8 lastGot = 0;
1177 for (int i = 0; i < n; ++i) {
1178 const fl::u8 b = static_cast<fl::u8>(i * 37 + 0x1B);
1179 while (SPI1CMD & SPIBUSY) {
1180 }
1181 SPI1W0 = b;
1182 SPI1CMD |= SPIBUSY;
1183 while (SPI1CMD & SPIBUSY) {
1184 }
1185 lastGot = static_cast<fl::u8>(SPI1W0 & 0xFF);
1186 if (lastGot != b && firstBad < 0) {
1187 firstBad = i;
1188 }
1189 }
1190 r.set("success", firstBad < 0);
1191 r.set("sent", static_cast<fl::i64>(n));
1192 r.set("first_bad", static_cast<fl::i64>(firstBad));
1193 r.set("last_got", static_cast<fl::i64>(lastGot));
1194 return r;
1195 });
1196}
1197
1198inline void bindEsp8266Benches(fl::Remote& remote) {
1199 bindEsp8266WatchdogRpcs(remote);
1200 bindEsp8266UartRpcs(remote);
1201 bindEsp8266DriverRpcs(remote);
1202 bindEsp8266HspiRpcs(remote);
1203 bindEsp8266HspiBenchRpcs(remote);
1204 bindEsp8266I2sRpcs(remote);
1205 bindEsp8266SpiRpcs(remote);
1206}
1207
1208} // namespace esp8266
1209} // namespace autoresearch
1210
1211#endif // FL_IS_ESP8266
CRGB leds[1]
FL_DISABLE_WARNING_PUSH FL_DISABLE_WARNING_GLOBAL_CONSTRUCTORS CFastLED FastLED
Global LED strip management instance.
@ APA102
APA102 LED chipset.
Definition FastLED.h:299
CLEDController * controller
fl::unique_ptr< fl::Remote > remote
Definition RpcClient.ino:43
static constexpr size_t calculateBytes(size_t num_leds) FL_NO_EXCEPT
Calculate total byte count for APA102 protocol Used for quad-SPI buffer pre-allocation.
Definition apa102.h:271
::CLEDController & addLeds(::CLEDController *pLed, CRGB *data, int nLedsOrOffset, int nLedsIfOffset=0) FL_NO_EXCEPT
Add a CLEDController instance to the world.
CLEDController & setLeds(CRGB *data, int nLeds) FL_NO_EXCEPT
Set the default array of LEDs to be used by this controller.
VIRTUAL_IF_NOT_AVR void showLeds(fl::u8 brightness) FL_NO_EXCEPT
virtual void init() FL_NO_EXCEPT=0
Initialize the LED controller.
T & value() FL_NO_EXCEPT
Definition optional.h:112
bool has_value() const FL_NO_EXCEPT
Definition optional.h:42
static T & instance() FL_NO_EXCEPT
Definition singleton.h:81
static Watchdog & instance() FL_NO_EXCEPT
void set(const fl::string &key, const json &value) FL_NO_EXCEPT
Definition json.h:873
void push_back(const json &value) FL_NO_EXCEPT
Definition json.h:925
static json object() FL_NO_EXCEPT
Definition json.h:864
static json array() FL_NO_EXCEPT
Definition json.h:860
fl::CLEDController CLEDController
constexpr EOrder BGR
Definition eorder.h:22
constexpr EOrder GRB
Definition eorder.h:19
static uint32_t t
Definition Luminova.h:55
#define DATA_RATE_KHZ
Definition fastspi.h:96
#define DATA_RATE_MHZ
Definition fastspi.h:95
fl::hsv8 CHSV
Definition chsv.h:11
fl::CRGB CRGB
Definition crgb.h:25
FastLED's Elegant JSON Library: fl::json
unsigned char u8
Definition stdint.h:131
void delay(u32 ms, bool run_async=true) FL_NO_EXCEPT
Public delay wrapper that keeps bare Arduino delay() preferred after using fl::delay; while still all...
Definition delay.h:98
u32 _retry_cnt
Frame retry counter for ESP8266 clockless drivers (FASTLED_DEBUG_COUNT_FRAME_RETRIES).
fl::u32 millis() FL_NO_EXCEPT
Universal millisecond timer - returns milliseconds since system startup.
Optional< T > optional
Definition optional.h:16
fl::u32 micros() FL_NO_EXCEPT
Universal microsecond timer - returns microseconds since system startup.
fl::i64 i64
Definition s16x16x4.h:222
u32 _frame_cnt
Frame counter for ESP8266 clockless drivers (FASTLED_DEBUG_COUNT_FRAME_RETRIES).
fl::string_view resetCauseName(ResetCause c) FL_NO_EXCEPT
Return a static-lifetime name for c — zero-cost, no allocation.
Definition watchdog.h:61
void digitalWrite(int pin, PinValue val) FL_NO_EXCEPT
Write digital value to pin.
Definition pin.cpp.hpp:51
void pinMode(int pin, PinMode mode) FL_NO_EXCEPT
Set pin mode (input, output, pull-up, pull-down)
Definition pin.cpp.hpp:398
void delayMicroseconds(u32 us) FL_NO_EXCEPT
Delay for a given number of microseconds.
corkscrew_args args
Definition old.h:149
#define FL_NO_INLINE
#define FL_IRAM
Unified cross-platform Watchdog Timer API for FastLED.