@typecad/framework-zephyr 1.0.0-alpha.11 → 1.0.0-alpha.12

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -13,6 +13,7 @@
13
13
 
14
14
  import type { ZephyrChipDescriptor } from '../chips/types.js';
15
15
  import type { ZephyrDisplayProfile } from '../display/profiles.js';
16
+ import { PANEL_CONTROLLER_DEFAULTS, panelControllerFor } from '../display/profiles.js';
16
17
  import type { KconfigUsage } from './kconfig.js';
17
18
 
18
19
  /**
@@ -40,12 +41,24 @@ export interface DisplayWiring {
40
41
  backlightPin?: number;
41
42
  }
42
43
 
43
- /** Touch wiring from cuttlefish.config.ts (irq/resetPin/sda/scl). */
44
+ /** Touch wiring from cuttlefish.config.ts (irq/resetPin/sda/scl; cs for SPI
45
+ * resistive controllers, calibration for the XPT2046 DT binding). */
44
46
  export interface TouchWiring {
47
+ /** Touch controller kind — selects the DT node shape (FT6336U node on I2C0
48
+ * vs XPT2046 node on the display's SPI bus). Default 'ft6336u'. */
49
+ controller?: 'ft6336u' | 'xpt2046';
45
50
  irq?: number;
46
51
  resetPin?: number;
47
52
  sda?: number;
48
53
  scl?: number;
54
+ /** XPT2046 only: SPI CS pin (second cs-gpios entry on the panel's bus). */
55
+ cs?: number;
56
+ /** XPT2046 only: raw ADC calibration — feeds the binding's min-x/max-x/
57
+ * min-y/max-y (required props). Defaults span the full 12-bit range. */
58
+ calibration?: { xMin: number; xMax: number; yMin: number; yMax: number };
59
+ /** XPT2046 only: pen-detect Z1 threshold (binding's z-threshold). Resistive
60
+ * panels need a few hundred 12-bit counts; default 400. */
61
+ minPressure?: number;
49
62
  }
50
63
 
51
64
  export function generateOverlay(
@@ -96,14 +109,23 @@ export function generateOverlay(
96
109
  // controller with the panel's compatible string + dimensions. The pin
97
110
  // wiring (cs/dc/rst) uses ESP32 GPIO defaults from the demo config; a
98
111
  // real board overlay would carry its own binding.
99
- emitDisplayNode(lines, display, wiring);
112
+ // An XPT2046 on the same bus needs its CS as the second cs-gpios entry,
113
+ // so thread it into the display block (DT assignment replaces the whole
114
+ // property — both entries must be written together).
115
+ emitDisplayNode(
116
+ lines,
117
+ display,
118
+ wiring,
119
+ touch?.controller === 'xpt2046' ? (touch?.cs ?? DEFAULT_XPT2046_CS) : undefined,
120
+ );
100
121
  }
101
- // FT6336U touch on I2C defined when the program uses touch (the UI touch
102
- // adapter references DT_NODELABEL(ft6336u)). Same rationale: the bare devkit
103
- // has no such node. Gated on usesTouch (not usesI2c/chip.i2c) so it emits
104
- // even when the chip descriptor doesn't declare I2C controllers (ESP32).
122
+ // Touch — FT6336U capacitive on I2C (references DT_NODELABEL(ft6336u)) or
123
+ // XPT2046 resistive on the display's SPI bus (references
124
+ // DT_NODELABEL(xpt2046)). Same rationale as the display node: the bare
125
+ // devkit has no such node. Gated on usesTouch (not usesI2c/chip.i2c) so it
126
+ // emits even when the chip descriptor doesn't declare bus controllers.
105
127
  if (usage.usesTouch) {
106
- emitTouchNode(lines, touch);
128
+ emitTouchNode(lines, touch, display);
107
129
  }
108
130
 
109
131
  // Preferences (ZMS settings backend): point the settings subsystem at the
@@ -138,11 +160,26 @@ export function generateOverlay(
138
160
  }
139
161
 
140
162
  /**
141
- * Emit a display DT node definition. The node is attached to spi2 (the ESP32's
142
- * first user SPI controller) via a MIPI DBI SPI bridge. Pin wiring comes from
143
- * the display config (cs/dc/rst); defaults match the demo-st wiring if absent.
163
+ * Emit a display DT node definition. The node is attached to the profile's SPI
164
+ * controller (default spi2, the ESP32's first user SPI controller) via a MIPI
165
+ * DBI SPI bridge. Pin wiring comes from the display config (cs/dc/rst);
166
+ * defaults match the demo-st wiring if absent. The compatible string + node
167
+ * props come from the profile's panel controller (st7796s carries the required
168
+ * pgc/ngc gamma + madctl; ili9341's binding defaults everything else).
169
+ *
170
+ * spiTouchCs: when an XPT2046 SPI touch controller shares the bus, its CS is
171
+ * appended as the second cs-gpios entry (the touch node uses reg = <1>) — DT
172
+ * property assignment replaces, so both entries must be written together.
144
173
  */
145
- function emitDisplayNode(lines: string[], display: ZephyrDisplayProfile, wiring?: DisplayWiring): void {
174
+ function emitDisplayNode(
175
+ lines: string[],
176
+ display: ZephyrDisplayProfile,
177
+ wiring?: DisplayWiring,
178
+ spiTouchCs?: number,
179
+ ): void {
180
+ const bus = display.busLabel ?? 'spi2';
181
+ const controller = panelControllerFor(display);
182
+ const compatible = display.dtCompatible ?? PANEL_CONTROLLER_DEFAULTS[controller].dtCompatible;
146
183
  const dc = wiring?.dc ?? 17;
147
184
  const rst = wiring?.rst ?? 16;
148
185
  const cs = wiring?.cs ?? 5;
@@ -163,7 +200,9 @@ function emitDisplayNode(lines: string[], display: ZephyrDisplayProfile, wiring?
163
200
  // macros are used instead of the named SPIM2_*_GPIOxx tokens because the
164
201
  // bindings header omits GPIOs 22-25 from those lists.
165
202
  if (sck !== undefined && mosi !== undefined) {
166
- lines.push('&spim2_default {');
203
+ // 'spi2' → pinctrl group 'spim2_default' (ESP32 SPI-master naming).
204
+ const pinctrlGroup = bus.replace(/^spi(\d)$/, 'spim$1') + '_default';
205
+ lines.push(`&${pinctrlGroup} {`);
167
206
  lines.push(' group1 {');
168
207
  lines.push(` pinmux = <ESP32_PINMUX(${miso ?? 19}, ESP_FSPIQ_IN, ESP_NOSIG)>,`);
169
208
  lines.push(` <ESP32_PINMUX(${sck}, ESP_NOSIG, ESP_FSPICLK_OUT)>,`);
@@ -180,9 +219,9 @@ function emitDisplayNode(lines: string[], display: ZephyrDisplayProfile, wiring?
180
219
  lines.push(' status = "okay";');
181
220
  lines.push('};');
182
221
  lines.push('');
183
- lines.push('&spi2 {');
222
+ lines.push(`&${bus} {`);
184
223
  lines.push(' status = "okay";');
185
- lines.push(` cs-gpios = <&${gpioController(cs)} ${cs} GPIO_ACTIVE_LOW>;`);
224
+ lines.push(` cs-gpios = <&${gpioController(cs)} ${cs} GPIO_ACTIVE_LOW>${spiTouchCs !== undefined ? `, <&${gpioController(spiTouchCs)} ${spiTouchCs} GPIO_ACTIVE_LOW>` : ''};`);
186
225
  // Enable GDMA for the SPI2 host. The ESP32 SPI driver uses DMA only when
187
226
  // dma-enabled is set AND dmas wires tx/rx channels to the GDMA controller;
188
227
  // without it, transfers run PIO through the 64-byte FIFO (~4MHz effective at
@@ -198,25 +237,36 @@ function emitDisplayNode(lines: string[], display: ZephyrDisplayProfile, wiring?
198
237
  lines.push('/ {');
199
238
  lines.push(' mipi_dbi: mipi-dbi {');
200
239
  lines.push(' compatible = "zephyr,mipi-dbi-spi";');
201
- lines.push(' spi-dev = <&spi2>;');
240
+ lines.push(` spi-dev = <&${bus}>;`);
202
241
  lines.push(` dc-gpios = <&${gpioController(dc)} ${dc} GPIO_ACTIVE_HIGH>;`);
203
242
  lines.push(` reset-gpios = <&${gpioController(rst)} ${rst} GPIO_ACTIVE_LOW>;`);
204
243
  lines.push(' write-only;');
205
244
  lines.push(' #address-cells = <1>;');
206
245
  lines.push(' #size-cells = <0>;');
207
246
  lines.push(` ${display.dtLabel}: display@0 {`);
208
- lines.push(' compatible = "sitronix,st7796s";');
247
+ lines.push(` compatible = "${compatible}";`);
209
248
  lines.push(' reg = <0>;');
210
- lines.push(` mipi-max-frequency = <${freq}>;`);
211
- lines.push(' mipi-mode = "MIPI_DBI_MODE_SPI_4WIRE";');
249
+ lines.push(` mipi-max-frequency = <${freq}>;`);
250
+ lines.push(' mipi-mode = "MIPI_DBI_MODE_SPI_4WIRE";');
251
+ // Required by the lcd-controller binding (Zephyr 4.x): 0 = RGB565,
252
+ // matching upstream ILI9341 boards (esp_wrover_kit) and the C++
253
+ // runtime, which drives these SPI TFTs as RGB565.
254
+ lines.push(' pixel-format = <0>;');
212
255
  lines.push(` width = <${nativeW}>;`);
213
256
  lines.push(` height = <${nativeH}>;`);
214
- // MADCTL: rotation 1 (landscape, MV=1) + BGR bit, matching the adapter's
215
- // direct-drive init (0x28). The DT copy keeps the stock driver's init
216
- // consistent if it is ever exercised.
217
- lines.push(' madctl = <0x28>;');
218
- lines.push(' pgc = [f0 09 0b 06 04 2e 46 46 39 13 15 12 15 12];');
219
- lines.push(' ngc = [f0 09 0b 06 04 2e 46 46 39 13 15 12 15 12];');
257
+ if (controller === 'st7796s') {
258
+ // MADCTL: rotation 1 (landscape, MV=1) + BGR bit, matching the adapter's
259
+ // direct-drive init (0x28). The DT copy keeps the stock driver's init
260
+ // consistent if it is ever exercised.
261
+ lines.push(' madctl = <0x28>;');
262
+ lines.push(' pgc = [f0 09 0b 06 04 2e 46 46 39 13 15 12 15 12];');
263
+ lines.push(' ngc = [f0 09 0b 06 04 2e 46 46 39 13 15 12 15 12];');
264
+ } else {
265
+ // ILI9341: the ilitek,ili9341 binding carries defaults for every register
266
+ // (gamma, power, porch) and expresses orientation via `rotation` (degrees)
267
+ // instead of a raw MADCTL — no panel-specific props are required.
268
+ lines.push(` rotation = <${display.rotation ?? 0}>;`);
269
+ }
220
270
  lines.push(' };');
221
271
  lines.push(' };');
222
272
  lines.push('};');
@@ -243,13 +293,39 @@ function emitDisplayNode(lines: string[], display: ZephyrDisplayProfile, wiring?
243
293
  }
244
294
  }
245
295
 
296
+ /** Default XPT2046 CS/IRQ pins (ESP32-S3 GPIOs clear of the demo-st display
297
+ * wiring: 5/17/16/15 and the remuxed SPI pins). Config values override. */
298
+ const DEFAULT_XPT2046_CS = 6;
299
+ const DEFAULT_XPT2046_IRQ = 7;
300
+
246
301
  /**
247
- * Emit an FT6336U touch DT node on the first I2C controller. The node address
248
- * is the FT6336U default (0x38). The UI touch adapter references
249
- * DT_NODELABEL(ft6336u). Pin wiring (irq/resetPin/sda/scl) comes from the
250
- * touch config; defaults match the demo-st wiring if absent.
302
+ * Emit the touch DT node for the configured controller.
303
+ *
304
+ * FT6336U (capacitive, I2C): node on i2c0 at the FT6336U default address
305
+ * (0x38); the C++ touch adapter reads it via i2c_write_read_dt.
306
+ *
307
+ * XPT2046 (resistive, SPI): node on the display's SPI bus as CS index 1. The
308
+ * in-tree xptek,xpt2046 binding (drivers/input) is register-matched for the
309
+ * raw SPI access — CONFIG_INPUT stays off, so the in-tree input driver does
310
+ * not build and the adapter owns the chip (same pattern as FT6336U reusing
311
+ * the ft5336 binding). The binding requires int-gpios, touchscreen-size-*,
312
+ * and min/max calibration props, so defaults are filled for anything the
313
+ * config omits.
251
314
  */
252
- function emitTouchNode(lines: string[], touch?: TouchWiring): void {
315
+ function emitTouchNode(
316
+ lines: string[],
317
+ touch: TouchWiring | undefined,
318
+ display: ZephyrDisplayProfile | undefined,
319
+ ): void {
320
+ if (touch?.controller === 'xpt2046') {
321
+ emitXpt2046Node(lines, touch, display);
322
+ return;
323
+ }
324
+ emitFt6336uNode(lines, touch);
325
+ }
326
+
327
+ /** FT6336U capacitive touch node on the first I2C controller. */
328
+ function emitFt6336uNode(lines: string[], touch?: TouchWiring): void {
253
329
  const irq = touch?.irq ?? 15;
254
330
  const resetPin = touch?.resetPin;
255
331
  const sda = touch?.sda;
@@ -296,3 +372,52 @@ function emitTouchNode(lines: string[], touch?: TouchWiring): void {
296
372
  lines.push('};');
297
373
  lines.push('');
298
374
  }
375
+
376
+ /** XPT2046 resistive touch node on the display's SPI bus (CS index 1). */
377
+ function emitXpt2046Node(
378
+ lines: string[],
379
+ touch: TouchWiring,
380
+ display: ZephyrDisplayProfile | undefined,
381
+ ): void {
382
+ const bus = display?.busLabel ?? 'spi2';
383
+ const cs = touch.cs ?? DEFAULT_XPT2046_CS;
384
+ const irq = touch.irq ?? DEFAULT_XPT2046_IRQ;
385
+ const cal = touch.calibration;
386
+ // ESP32-S3 GPIOs 0-31 are on gpio0, 32-48 on gpio1.
387
+ const gpioController = (pin: number) => pin <= 31 ? 'gpio0' : 'gpio1';
388
+ const zThreshold = touch.minPressure ?? 400;
389
+ // touchscreen-size-* describe the panel the touch layer sits on (the display
390
+ // profile's effective size); a touch-only build falls back to the 12-bit
391
+ // full-scale range so the binding's required props still resolve.
392
+ const sizeX = display?.width ?? cal?.xMax ?? 320;
393
+ const sizeY = display?.height ?? cal?.yMax ?? 240;
394
+ // The display block already wrote cs-gpios with both entries (its CS at
395
+ // index 0, the touch CS at index 1). When there is no display block, enable
396
+ // the bus here with the touch CS as the only entry.
397
+ if (!display) {
398
+ lines.push(`&${bus} {`);
399
+ lines.push(' status = "okay";');
400
+ lines.push(` cs-gpios = <&${gpioController(cs)} ${cs} GPIO_ACTIVE_LOW>;`);
401
+ lines.push('};');
402
+ lines.push('');
403
+ }
404
+ lines.push(`&${bus} {`);
405
+ lines.push(' xpt2046: xpt2046@1 {');
406
+ lines.push(' compatible = "xptek,xpt2046";');
407
+ lines.push(' reg = <1>;');
408
+ // The XPT2046 datasheet max SPI clock is 2.5MHz — the panel bus may run at
409
+ // 80MHz, but this node's spi-max-frequency gates only its own transactions
410
+ // (the adapter's SPI_DT_SPEC picks it up).
411
+ lines.push(' spi-max-frequency = <2500000>;');
412
+ lines.push(` int-gpios = <&${gpioController(irq)} ${irq} GPIO_ACTIVE_LOW>;`);
413
+ lines.push(` touchscreen-size-x = <${sizeX}>;`);
414
+ lines.push(` touchscreen-size-y = <${sizeY}>;`);
415
+ lines.push(` min-x = <${cal?.xMin ?? 0}>;`);
416
+ lines.push(` max-x = <${cal?.xMax ?? 4095}>;`);
417
+ lines.push(` min-y = <${cal?.yMin ?? 0}>;`);
418
+ lines.push(` max-y = <${cal?.yMax ?? 4095}>;`);
419
+ lines.push(` z-threshold = <${zThreshold}>;`);
420
+ lines.push(' };');
421
+ lines.push('};');
422
+ lines.push('');
423
+ }
@@ -289,8 +289,13 @@ export default defineFrameworkManifest({
289
289
  },
290
290
  display: {
291
291
  supported: true,
292
- partialCoverage: false,
293
- unsupportedReason: undefined,
292
+ partialCoverage: true,
293
+ // Partial: mono profiles (ssd1306-zephyr) drive display.* ops via the
294
+ // direct GFX runtime only — no CuttlefishGFX UI rendering path. The
295
+ // ILI9341 UI adapter shares the ST7796S direct-drive transport with a
296
+ // per-controller init table (16-bit RGB565 wire format); hardware-tuned
297
+ // on ST7796S only. E-ink panels are out of scope at this time.
298
+ unsupportedReason: 'Mono panels (ssd1306) are direct-op only (no UI rendering); ili9341 UI path is ported but not yet hardware-verified; e-ink is out of scope at this time.',
294
299
  drivers: ['ili9341-zephyr', 'st7796-zephyr', 'ssd1306-zephyr'],
295
300
  colorFormat: 'rgb565',
296
301
  ops: {
@@ -499,6 +504,7 @@ export default defineFrameworkManifest({
499
504
  polyfills: {
500
505
  emitted: [
501
506
  { id: 'cuttlefish_halt', domain: 'standard', notes: 'Mapped to a k_msleep halt loop (exceptions disabled)' },
507
+ { id: 'wiring_compat', domain: 'standard', notes: 'HIGH/LOW/digitalRead/etc. macros routing Wiring tokens (referenced unconditionally by the UI runtime header) to the __tc_gpio_* helpers' },
502
508
  { id: 'string_methods', domain: 'embedded', notes: 'STL-free __tc_* string helpers (const char*, inline ASCII case conv, <cstring> only)' },
503
509
  { id: 'static_array', domain: 'embedded', notes: 'STL-free __tc_StaticArray<T,N> wrapper for no-<vector> mutated/struct array literals' },
504
510
  { id: 'timer_methods', domain: 'embedded', notes: 'k_timer + k_work pool (system workqueue); callbacks run in thread context' },
@@ -509,7 +515,7 @@ export default defineFrameworkManifest({
509
515
 
510
516
  toolchain: {
511
517
  backend: 'west',
512
- operations: { prepare: true, compile: true, upload: true, monitor: true },
518
+ operations: { prepare: true, compile: true, upload: true, monitor: true, debug: true },
513
519
  },
514
520
 
515
521
  libraryResolution: {
@@ -393,7 +393,9 @@ export function lowerBle(op: HALOpIR): { code?: string; expression?: string } {
393
393
  case 'ble.on_read':
394
394
  // Store the typed read handler as void*; __tc_ble_attr_read casts it back
395
395
  // to the right signature based on the char's type field. reinterpret_cast
396
- // (not a C-style cast) keeps this AUTOSAR-compliant under --autosar=strict.
396
+ // (not a C-style cast) avoids M5-0-7, but M5-0-10 still flags it — the
397
+ // whole type-erased table is covered by a knownPatterns deviation on
398
+ // that rule (see rules.ts, "BLE type-erased callback table").
397
399
  return { code: `__tc_ble.on_read[__tc_ble.current_char] = reinterpret_cast<void*>(${s(o.handler)});` };
398
400
  case 'ble.on_write':
399
401
  return { code: `__tc_ble.on_write[__tc_ble.current_char] = (${s(o.handler)});` };
package/src/strategy.ts CHANGED
@@ -157,9 +157,10 @@ export class ZephyrStrategy implements PlatformStrategy {
157
157
  // program-analysis usesStdString detector doesn't see types generated by
158
158
  // the BLE lowering layer — so without forcing <string> here, any BLE server
159
159
  // with a Utf8 characteristic fails to compile ('std::string does not name a
160
- // type'). Uses <string>, not <string.h>: the latter is the C flat-string
161
- // header (already included for the shim's strncpy/strcmp).
162
- if (uses('usesBle')) inc.push('<stdlib.h>', '<string.h>', '<string>', '<zephyr/bluetooth/bluetooth.h>', '<zephyr/bluetooth/conn.h>', '<zephyr/bluetooth/gatt.h>', '<zephyr/bluetooth/uuid.h>');
160
+ // type'). <cstdlib>/<cstring> (not <stdlib.h>/<string.h>) back the shim's
161
+ // strtol/strcmp/strncpy the same AUTOSAR-compliant spelling the HTTP,
162
+ // MQTT, and Preferences paths below already use.
163
+ if (uses('usesBle')) inc.push('<cstdlib>', '<cstring>', '<string>', '<zephyr/bluetooth/bluetooth.h>', '<zephyr/bluetooth/conn.h>', '<zephyr/bluetooth/gatt.h>', '<zephyr/bluetooth/uuid.h>');
163
164
  // Display: the analyzer's usesDisplay flag (set by display.* hal-ops) drives
164
165
  // this include. When ctx.analysis is absent (capability query), uses()
165
166
  // defaults to true so a real build never strips it.
@@ -407,21 +408,29 @@ export class ZephyrStrategy implements PlatformStrategy {
407
408
  '}',
408
409
  );
409
410
 
410
- // Safety shims: when the program uses @typecad/safety, provide __tc_gpio_read
411
- // / __tc_gpio_write backed by the raw controller (a best-effort read that
412
- // does not depend on a pin having a DT spec). __tc_delay_us uses k_busy_wait.
411
+ // GPIO read shim: the wiring_compat polyfill routes the UI runtime
412
+ // header's unconditional digitalRead() poll (init-press-input.ts) to
413
+ // __tc_gpio_read, so the definition must NOT be gated on @typecad/safety.
414
+ // The signature is `int` to match wiring_compat's forward declaration —
415
+ // a uint32_t definition alongside it would leave the declared int
416
+ // overload undefined (int wins overload resolution for small integer
417
+ // arguments).
413
418
  //
414
- // The pin is a RUNTIME value here (safety's voter passes whatever pin it
415
- // was handed), so the controller cannot be baked in as a single DT_NODELABEL
416
- // on a multi-controller SoC (ESP32-S3: pins 0–31 → gpio0, 32–48 → gpio1).
417
- // Emit a tiny __tc_gpio_dev(pin) dispatcher that resolves the owning
418
- // controller's device per pin; single-controller SoCs collapse it to a
419
- // one-liner. Each DT_NODELABEL is still compile-time-resolved per branch, so
420
- // it is always statically valid.
419
+ // The pin is a RUNTIME value here (the UI pin-watch table and safety's
420
+ // voter pass whatever pin they were handed), so the controller cannot be
421
+ // baked in as a single DT_NODELABEL on a multi-controller SoC (ESP32-S3:
422
+ // pins 0–31 → gpio0, 32–48 → gpio1). Emit a tiny __tc_gpio_dev(pin)
423
+ // dispatcher that resolves the owning controller's device per pin;
424
+ // single-controller SoCs collapse it to a one-liner. Each DT_NODELABEL is
425
+ // still compile-time-resolved per branch, so it is always statically valid.
426
+ lines.push(...emitGpioDevDispatcher(chip));
427
+ lines.push(
428
+ 'inline int __tc_gpio_read(int pin) { return gpio_pin_get_raw(__tc_gpio_dev(static_cast<uint32_t>(pin)), static_cast<gpio_pin_t>(pin)); }',
429
+ );
430
+ // __tc_gpio_write / __tc_delay_us are only referenced via @typecad/safety
431
+ // lowering, so they stay gated on it.
421
432
  if (program && programUsesSafety(program)) {
422
- lines.push(...emitGpioDevDispatcher(chip));
423
433
  lines.push(
424
- 'inline int __tc_gpio_read(uint32_t pin) { return gpio_pin_get_raw(__tc_gpio_dev(pin), pin); }',
425
434
  'inline void __tc_gpio_write(uint32_t pin, uint32_t value) { gpio_pin_set_raw(__tc_gpio_dev(pin), pin, value); }',
426
435
  '#ifndef __TC_DELAY_US_DEFINED',
427
436
  '#define __TC_DELAY_US_DEFINED',
@@ -860,6 +869,62 @@ export class ZephyrStrategy implements PlatformStrategy {
860
869
  return '';
861
870
  }
862
871
 
872
+ // ── Interrupt safety ─────────────────────────────────────────────────────
873
+ // Zephyr ISRs run above thread context: anything that sleeps (k_msleep),
874
+ // pends, or takes a driver lock is illegal there (asserted by the kernel in
875
+ // debug builds; corrupts scheduler state otherwise). The names below are the
876
+ // IR-level callees cuttlefish's interrupt-analysis pass matches (the same
877
+ // keys ArduinoStrategy uses; timing.delay/delay_microseconds hal-ops are
878
+ // mapped back to the bare names by the analyzer itself).
879
+ isrUnsafeOperations(): Map<string, { reason: string; severity: 'warning' | 'info' }> {
880
+ return new Map<string, { reason: string; severity: 'warning' | 'info' }>([
881
+ ['delay', {
882
+ reason: 'delay() lowers to k_msleep(), which sleeps the calling thread — illegal in Zephyr interrupt context (submit a k_work item or arm a k_timer instead)',
883
+ severity: 'warning',
884
+ }],
885
+ ['delayMicroseconds', {
886
+ reason: 'delayMicroseconds() busy-waits the CPU for the full delay, stalling every lower-priority interrupt and the scheduler for its duration',
887
+ severity: 'warning',
888
+ }],
889
+ ['console.log', {
890
+ reason: 'console output lowers to printk(), which is ISR-legal but slow and lock-protected (CONFIG_PRINTK_SYNC) — it adds jitter to every interrupt behind it',
891
+ severity: 'info',
892
+ }],
893
+ ['console.error', {
894
+ reason: 'console output lowers to printk(), which is ISR-legal but slow and lock-protected (CONFIG_PRINTK_SYNC) — it adds jitter to every interrupt behind it',
895
+ severity: 'info',
896
+ }],
897
+ ['console.warn', {
898
+ reason: 'console output lowers to printk(), which is ISR-legal but slow and lock-protected (CONFIG_PRINTK_SYNC) — it adds jitter to every interrupt behind it',
899
+ severity: 'info',
900
+ }],
901
+ ['I2C0', {
902
+ reason: 'I2C transactions may sleep (driver locking + clock stretching) and are not callable from Zephyr interrupt context',
903
+ severity: 'warning',
904
+ }],
905
+ ['I2C1', {
906
+ reason: 'I2C transactions may sleep (driver locking + clock stretching) and are not callable from Zephyr interrupt context',
907
+ severity: 'warning',
908
+ }],
909
+ ['SPI0', {
910
+ reason: 'SPI transfers take driver locks and may wait on DMA completion — not safe in Zephyr interrupt context',
911
+ severity: 'warning',
912
+ }],
913
+ ['SPI1', {
914
+ reason: 'SPI transfers take driver locks and may wait on DMA completion — not safe in Zephyr interrupt context',
915
+ severity: 'warning',
916
+ }],
917
+ ['UART0', {
918
+ reason: 'UART output via uart_poll_out blocks until the TX FIFO has room — a full FIFO stalls the ISR',
919
+ severity: 'info',
920
+ }],
921
+ ['UART1', {
922
+ reason: 'UART output via uart_poll_out blocks until the TX FIFO has room — a full FIFO stalls the ISR',
923
+ severity: 'info',
924
+ }],
925
+ ]);
926
+ }
927
+
863
928
  ambientTypeDeclarations(): string[] {
864
929
  // Preferences is the only HAL surface the framework lowers that is used as
865
930
  // a bare global (the HAL Preferences class is exported, but the canonical
@@ -1218,7 +1283,11 @@ struct __tc_StaticArray {
1218
1283
  id: 'async_runtime',
1219
1284
  domain: 'embedded',
1220
1285
  requiredIncludes: [],
1221
- forwardDeclarations: [],
1286
+ // Polyfill definitions emit before shimLines, but the runtime's
1287
+ // timer bodies call millis() (defined in shimLines) — declare it
1288
+ // first so the polyfill compiles even for programs whose source
1289
+ // has no explicit timing call.
1290
+ forwardDeclarations: ['unsigned long millis();'],
1222
1291
  helperStructs: [generateStaticAsyncRuntime(8, this.getAsyncRuntimeConfig().waitForPinEdge)],
1223
1292
  helperFunctions: [],
1224
1293
  shimMacros: [],
@@ -1325,11 +1394,12 @@ struct __tc_StaticArray {
1325
1394
 
1326
1395
  // ── Strategy-owned display/touch adapter seam ────────────────────────────
1327
1396
  // Zephyr owns its display + touch adapters: the UI display adapter bridges
1328
- // the in-tree CuttlefishGFX class to Zephyr's display_write() API (see
1329
- // src/display/ui-adapter.ts), and the FT6336U touch adapter drives the I2C
1330
- // controller via Zephyr's i2c API (src/display/touch-adapter.ts). Both live
1331
- // in this package so cuttlefish carries no Zephyr/Wiring-specific display or
1332
- // touch knowledge. Mirrors ArduinoStrategy's provides*/resolve* pattern.
1397
+ // the in-tree CuttlefishGFX class to the panel (per-controller init + wire
1398
+ // format, see src/display/ui-adapter.ts), and the touch adapters drive the
1399
+ // FT6336U (I2C capacitive) and XPT2046 (SPI resistive) controllers via
1400
+ // Zephyr's bus APIs (src/display/touch-adapter.ts). Both live in this
1401
+ // package so cuttlefish carries no Zephyr/Wiring-specific display or touch
1402
+ // knowledge. Mirrors ArduinoStrategy's provides*/resolve* pattern.
1333
1403
 
1334
1404
  providesDisplayAdapter(): boolean { return true; }
1335
1405
 
@@ -206,12 +206,17 @@ export const Toolchain = {
206
206
  // for either driver. Thread a non-default profile here only if a future
207
207
  // board carries a display node under a different nodelabel.
208
208
  const displayProfile = usesDisplay ? DEFAULT_ZEPHYR_DISPLAY_PROFILE : undefined;
209
+ // Touch controller kind comes from which DT nodelabel the emitted adapter
210
+ // references (FT6336U on I2C, XPT2046 on the display's SPI bus).
211
+ const usesTouch = uses('ft6336u') || uses('touch_');
212
+ const usesXpt = uses('xpt2046');
209
213
  const overlay = generateOverlay(chip, {
210
214
  usesI2c: uses('i2c_'),
211
215
  usesSpi: uses('spi_'),
212
216
  usesUart: uses('uart_'),
213
217
  usesDisplay,
214
- usesTouch: uses('ft6336u') || uses('touch_'),
218
+ usesTouch: usesTouch || usesXpt,
219
+ touchController: usesXpt ? 'xpt2046' : 'ft6336u',
215
220
  }, displayProfile);
216
221
  const overlayDir = join(projectRoot, 'boards');
217
222
  mkdirSync(overlayDir, { recursive: true });
@@ -303,23 +308,49 @@ export const Toolchain = {
303
308
  backlightPin: typeof dispCfg.backlightPin === 'number' ? dispCfg.backlightPin : undefined,
304
309
  }
305
310
  : undefined;
306
- // Extract touch pin wiring (irq/resetPin/sda/scl) from the config
307
- // display.touch section so the DT overlay wires the I2C bus + touch node.
311
+ // Extract touch pin wiring from the config display.touch section so the
312
+ // DT overlay wires the bus + touch node. I2C (FT6336U) carries
313
+ // irq/resetPin/sda/scl; SPI (XPT2046) carries irq/cs + the calibration
314
+ // range the xptek,xpt2046 binding requires.
308
315
  const touchCfg = dispCfg?.touch as Record<string, unknown> | undefined;
309
- const touchWiring: TouchWiring | undefined = touchCfg
316
+ const isXpt = touchCfg?.library === 'XPT2046_Touchscreen';
317
+ const touchCal = touchCfg?.calibration as
318
+ { xMin?: unknown; xMax?: unknown; yMin?: unknown; yMax?: unknown } | undefined;
319
+ const num = (v: unknown): number | undefined => (typeof v === 'number' ? v : undefined);
320
+ let touchWiring: TouchWiring | undefined = touchCfg
310
321
  ? {
311
- irq: typeof touchCfg.irq === 'number' ? touchCfg.irq : undefined,
312
- resetPin: typeof touchCfg.resetPin === 'number' ? touchCfg.resetPin : undefined,
313
- sda: typeof touchCfg.sda === 'number' ? touchCfg.sda : undefined,
314
- scl: typeof touchCfg.scl === 'number' ? touchCfg.scl : undefined,
322
+ controller: isXpt ? 'xpt2046' : 'ft6336u',
323
+ irq: num(touchCfg.irq),
324
+ resetPin: num(touchCfg.resetPin),
325
+ sda: num(touchCfg.sda),
326
+ scl: num(touchCfg.scl),
327
+ cs: num(touchCfg.cs),
328
+ calibration: touchCal
329
+ ? {
330
+ xMin: num(touchCal.xMin) ?? 0,
331
+ xMax: num(touchCal.xMax) ?? 4095,
332
+ yMin: num(touchCal.yMin) ?? 0,
333
+ yMax: num(touchCal.yMax) ?? 4095,
334
+ }
335
+ : undefined,
336
+ minPressure: num(touchCfg.minPressure),
315
337
  }
316
338
  : undefined;
339
+ // Touch controller kind for Kconfig (bus driver selection) and the DT
340
+ // node shape: from the config when available, else from the DT nodelabel
341
+ // the emitted adapter references. Forced onto touchWiring so a source
342
+ // scan match without a config section still emits the right node.
343
+ const usesXpt = isXpt || uses('xpt2046');
344
+ if (usesXpt) {
345
+ touchWiring = { controller: 'xpt2046', ...(touchWiring ?? {}) };
346
+ }
317
347
  const overlay = generateOverlay(chip, {
318
348
  usesI2c: uses('i2c_'),
319
349
  usesSpi: uses('spi_'),
320
350
  usesUart: uses('uart_'),
321
351
  usesDisplay,
322
- usesTouch: uses('ft6336u') || uses('touch_'),
352
+ usesTouch: uses('ft6336u') || uses('touch_') || usesXpt,
353
+ touchController: usesXpt ? 'xpt2046' : 'ft6336u',
323
354
  psram: o.psram,
324
355
  }, displayProfile, wiring, touchWiring);
325
356
  const overlayDir = join(projectRoot, 'boards');
@@ -130,8 +130,10 @@ export function scaffoldZephyrProject(projectRoot: string, debug = false, userKc
130
130
  '',
131
131
  'project(zephyr_app)',
132
132
  '',
133
- '# Collect cuttlefish-emitted sources.',
134
- 'file(GLOB app_sources src/*.cpp src/*.c)',
133
+ '# Collect cuttlefish-emitted sources. CONFIGURE_DEPENDS makes CMake re-',
134
+ '# check the glob when the source set changes (e.g. the transpiler removes',
135
+ '# a stale entry), instead of linking a file list from the last configure.',
136
+ 'file(GLOB app_sources CONFIGURE_DEPENDS src/*.cpp src/*.c)',
135
137
  '',
136
138
  'target_sources(app PRIVATE ${app_sources})',
137
139
  // When PSRAM is configured, define BOARD_HAS_PSRAM so the UI runtime's
@@ -97,9 +97,12 @@ export function isZephyrBase(dir: string): boolean {
97
97
  // ── Strategy 1: `west` on PATH ──────────────────────────────────────────────
98
98
 
99
99
  export function discoverFromPath(): WestInstall | null {
100
+ // shell only on Windows (where.exe resolution through cmd) — an args array
101
+ // with shell: true triggers Node's DEP0190 deprecation warning on Linux/
102
+ // macOS, where `which` is a plain executable that needs no shell.
100
103
  const which = spawnSync(IS_WIN ? 'where' : 'which', ['west'], {
101
104
  encoding: 'utf8',
102
- shell: true,
105
+ shell: IS_WIN,
103
106
  windowsHide: true,
104
107
  });
105
108
  if (which.status !== 0) return null;