# Plan: Teensy 4.1 Bring-up (Blink → Serial → Display → Touch → SD) > **Status: Phase 0 + SD + TFT + touch all verified on hardware (2026-07-26).** > - **Toolchain** installed (`teensy:avr` 1.62.0 + udev rules), blink sketch > confirmed (red LED @ 1 Hz), HelloSerial confirmed (`teensy41 alive #N` over > `/dev/ttyACM0` once per second). CDC-ACM transport path verified. > - **SD card** test sketch ([`firmware/teensy41/sd_test/sd_test.ino`](../firmware/teensy41/sd_test/sd_test.ino)) > mounted a 32 GB FAT32 card via `SD.begin(BUILTIN_SDCARD)` (4-bit SDMMC), > listed the root directory, read the first 512 bytes of `bcm2710-rpi-2-b.dtb`. > exFAT re-test pending the 1 TB SDXC card. > - **TFT display** test sketch ([`firmware/teensy41/tft_test/tft_test.ino`](../firmware/teensy41/tft_test/tft_test.ino)) > confirmed on hardware: **landscape 480×320**, fill edge-to-edge, text > readable, colors correct. Uses `ST7796_t3` library (hardware SPI0). > - **Touch** calibration sketch ([`firmware/teensy41/touch_cal/touch_cal.ino`](../firmware/teensy41/touch_cal/touch_cal.ino)) > confirmed on hardware: 4-corner tap calibration → live dot-draw tracks the > stylus across the full 480×320 screen. > **Calibration constants:** `x_min=207, x_max=1909, y_min=168, y_max=1798, > invert_x=1, invert_y=1` (with axis swap: raw Y → screen X, raw X → screen Y). > > **Hard-won lessons (read these before porting the display/touch code):** > > 1. **Landscape, not portrait.** The target orientation is **landscape > 480×320** (wider than tall). Use `tft.init(320, 480)` + `setRotation(1)` > to get landscape with zero offsets. Do NOT use `init(480, 320)` — that > hits the library's else-branch which computes a negative `_colstart` and > offsets the image off the panel. > 2. **`tft.width()` / `tft.height()` are broken.** The `ST7796_t3` accessors > always return the unrotated 480×320 regardless of `setRotation()`. Use the > constants `SCREEN_W=480` / `SCREEN_H=320` for all drawing coordinates, > never `tft.width()`/`tft.height()`. > 3. **Pin 10 is SPI0 CS0 — do not `pinMode` it.** Calling `pinMode(10, INPUT)` > (e.g. for `t_irq`) corrupts the SPI0 engine and reverts the display to > the wrong orientation. `t_irq` is on **pin 2** (see > [`firmware/teensy41/WIRING.md`](../firmware/teensy41/WIRING.md)). > 4. **XPT2046 axes are swapped in landscape rotation 1.** Raw Y maps to > screen X, raw X maps to screen Y. The calibration struct stores > `x_min/x_max` as the range of raw Y, `y_min/y_max` as the range of raw X, > and `raw_to_screen()` swaps accordingly. > 5. **`drawPixel()` mispositions near edges.** Use `fillRect(x, y, 1, n, c)` / > `fillRect(x, y, n, 1, c)` for crosshairs and thin lines instead of > `drawPixel()` — `fillRect` is reliable across the full screen. > > **Next:** exFAT re-test on the 1 TB SDXC card (when it arrives), then start > porting the CYD display/touch/UI code per > [`plans/teensy41_signer_port.md`](teensy41_signer_port.md) Phase 2+. Phase 0 of [`plans/teensy41_signer_port.md`](teensy41_signer_port.md). Goal: prove the toolchain, the board, and the USB CDC transport path before porting any signer code. Toolchain: **Arduino CLI + Teensyduino** (matches the primary path in the port plan). ## 0. On the LED you see at power-on The Teensy 4.1 ships from PJRC with a factory **blink sketch** on the onboard red LED (pin 13). Expected behavior at first plug-in: - **Red LED near USB connector blinks ~1 Hz** → factory blink program is running → board is healthy. - **Steady red LED** → a previous sketch left it on, or the board is halted. - **Steady orange/green LED** → that is the *power* LED, always on when the board has power. Not the user LED. - **No LED activity at all, and `lsusb` shows `16c0:0478 ... Teensy Halfkay Bootloader`** → the application flash was erased and the board is sitting in the Halfkay bootloader. This is healthy and is the easiest state to upload from: `arduino-cli upload` (via Teensy Loader) pushes a sketch to Halfkay directly, no Program-button press needed. There is no `/dev/ttyACMx` in this state because Halfkay enumerates as HID, not CDC — `arduino-cli board list` may show nothing, and that is expected. A steady power LED plus a blinking red user LED = everything is fine. A Halfkay enumeration with no LED = erased flash, ready to receive a sketch. ## 1. Host toolchain 1. Install `arduino-cli` (e.g. `pip install arduino-cli` or download the binary). 2. Add the Teensyduino core: ```bash arduino-cli config init arduino-cli config set board_manager.additional_urls https://www.pjrc.com/teensy/package_teensy_index.json arduino-cli core update-index arduino-cli core install teensy:avr ``` 3. Verify the FQBN is available: ```bash arduino-cli board listall teensy: | grep teensy41 ``` **Exit criterion:** `teensy:avr:teensy41` appears in the list. > Note: the `teensy:avr` package from PJRC's index includes the Teensy Loader and > compiler toolchain (arm-none-eabi-gcc). On Linux, udev rules are required for the > bootloader device — see PJRC's [Linux udev rules](https://www.pjrc.com/teensy/td_download.html). > Without them, `arduino-cli upload` will fail with a permission error on `/dev/ttyACMx`. ## 2. Detect the board 1. Plug the Teensy 4.1 into USB (use the onboard USB port, not the USB host header). 2. List Arduino ports: ```bash arduino-cli board list ``` **Exit criterion:** a port shows up as `/dev/ttyACMx` (Linux), `COMx` (Windows), or `/dev/cu.usbmodem*` (macOS) with protocol `serial` and board `teensy:avr:teensy41`. 3. If no port appears, press the **Program button** on the Teensy once — that forces the bootloader to enumerate. (The factory blink sketch does not expose a CDC port by default, so the port may only show up after the first upload of a sketch that calls `Serial.begin()`.) ## 3. Blink sketch (first upload) Create [`firmware/teensy41/teensy41_blink.ino`](../firmware/teensy41/teensy41_blink.ino): ```cpp // Teensy 4.1 bring-up: blink the onboard red LED on pin 13 at 1 Hz. void setup() { pinMode(LED_BUILTIN, OUTPUT); // LED_BUILTIN == 13 on Teensy 4.1 } void loop() { digitalWrite(LED_BUILTIN, HIGH); delay(500); digitalWrite(LED_BUILTIN, LOW); delay(500); } ``` Compile + upload: ```bash arduino-cli compile --fqbn teensy:avr:teensy41 firmware/teensy41 arduino-cli upload -p /dev/ttyACM0 --fqbn teensy:avr:teensy41 firmware/teensy41 ``` If upload fails with "no Teensy found", press the Program button on the board to force the bootloader, then retry. **Exit criterion:** the red LED toggles at exactly 1 Hz (slower than the factory blink, which is ~5 Hz, so the change is obvious). ## 4. HelloSerial (validate the CDC-ACM transport path) The signer's USB transport ([`plans/teensy41_signer_port.md`](teensy41_signer_port.md) §USB transport) is TinyUSB CDC-ACM + 4-byte length-prefix framing. Before writing the framer, confirm that a plain `Serial.print` over USB CDC works end-to-end. Create [`firmware/teensy41/teensy41_hello_serial.ino`](../firmware/teensy41/teensy41_hello_serial.ino): ```cpp // Teensy 4.1 bring-up: print a counter over USB CDC every second. void setup() { Serial.begin(115200); // USB CDC on Teensy 4.1 pinMode(LED_BUILTIN, OUTPUT); } void loop() { static uint32_t n = 0; digitalWrite(LED_BUILTIN, HIGH); Serial.print("teensy41 alive #"); Serial.println(n++); delay(500); digitalWrite(LED_BUILTIN, LOW); delay(500); } ``` Compile + upload, then open a monitor: ```bash arduino-cli monitor -p /dev/ttyACM0 -c baudrate=115200 # or: stty -F /dev/ttyACM0 115200 raw -echo && cat /dev/ttyACM0 ``` **Exit criterion:** `teensy41 alive #0`, `#1`, `#2`… prints once per second, and the red LED blinks in lockstep. This proves the CDC-ACM path we will reuse for the signer's length-prefix framing. ## 5. Next milestones (covered by separate plans) Once blink + serial are confirmed, the rest of Phase 1 from [`plans/teensy41_signer_port.md`](teensy41_signer_port.md) proceeds: 1. **ST7796S 480×320 display** — port [`firmware/cyd_esp32_2432s028/main/ili9341.c`](../firmware/cyd_esp32_2432s028/main/ili9341.c) with the ST7796S init sequence from [`firmware/teensy41/ST7796S_Datasheet.pdf`](../firmware/teensy41/ST7796S_Datasheet.pdf) and the Elecrow wiki page. Wiring per the port plan: TFT SPI on pins 11/12/13, CS=10, DC=9, RESET=8, BL=22. Exit: fill screen + draw text. 2. **XPT2046 touch** — port [`firmware/cyd_esp32_2432s028/main/touch.c`](../firmware/cyd_esp32_2432s028/main/touch.c) with 480×320 constants. T_CS=7, T_IRQ=6. Exit: read touch coords, map to pixels. 3. **SD card + exFAT** — install SdFat via Arduino Library Manager, mount a 1 TB SDXC card via `sd.begin(SdioConfig(FIFO_SDIO))`. Exit: write + read a test file. ## 6. Status tracking Update [`firmware/teensy41/README.md`](../firmware/teensy41/README.md) from "Planned — not yet implemented" to "In progress — Phase 0 bring-up" once the blink sketch is confirmed running on the board.