{"id":50509723,"url":"https://github.com/dmatking/esp32-notes","last_synced_at":"2026-06-02T19:01:14.274Z","repository":{"id":349082714,"uuid":"1200993895","full_name":"dmatking/esp32-notes","owner":"dmatking","description":"Practical hardware notes for ESP32 variants — initialization, peripherals, and hard-won lessons","archived":false,"fork":false,"pushed_at":"2026-04-04T05:09:12.000Z","size":5,"stargazers_count":0,"open_issues_count":0,"forks_count":0,"subscribers_count":0,"default_branch":"main","last_synced_at":"2026-04-04T06:35:29.275Z","etag":null,"topics":["embedded","esp-hosted","esp-idf","esp32","esp32-c6","esp32-p4","esp32-s3","hardware-notes","mipi-dsi"],"latest_commit_sha":null,"homepage":null,"language":null,"has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":null,"status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/dmatking.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":null,"code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2026-04-04T04:27:57.000Z","updated_at":"2026-04-04T05:09:16.000Z","dependencies_parsed_at":null,"dependency_job_id":null,"html_url":"https://github.com/dmatking/esp32-notes","commit_stats":null,"previous_names":["dmatking/esp32-notes"],"tags_count":null,"template":false,"template_full_name":null,"purl":"pkg:github/dmatking/esp32-notes","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/dmatking%2Fesp32-notes","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/dmatking%2Fesp32-notes/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/dmatking%2Fesp32-notes/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/dmatking%2Fesp32-notes/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/dmatking","download_url":"https://codeload.github.com/dmatking/esp32-notes/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/dmatking%2Fesp32-notes/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":33833277,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-05-26T15:22:16.424Z","status":"online","status_checked_at":"2026-06-02T02:00:07.132Z","response_time":109,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["embedded","esp-hosted","esp-idf","esp32","esp32-c6","esp32-p4","esp32-s3","hardware-notes","mipi-dsi"],"created_at":"2026-06-02T19:01:13.445Z","updated_at":"2026-06-02T19:01:14.245Z","avatar_url":"https://github.com/dmatking.png","language":null,"funding_links":[],"categories":[],"sub_categories":[],"readme":"# ESP32 Hardware Notes\n\nPractical notes on ESP32 variants — initialization quirks, peripheral setup, and things learned the hard way. Board-specific details (pin assignments, etc.) live in each project's README; this covers chip/subsystem-level knowledge that applies across boards.\n\n---\n\n## ESP32-P4 (with ESP32-C6 Co-processor)\n\nBoards like the Waveshare ESP32-P4-WIFI6-Touch-LCD-4B pair an ESP32-P4 application processor with an ESP32-C6 co-processor that handles WiFi and Bluetooth via SDIO using the `esp_hosted` library.\n\n\u003e **New to the P4+C6 combo?** See [esp32-p4-wifi-starter](https://github.com/dmatking/esp32-p4-wifi-starter) — a minimal, heavily-annotated WiFi example that walks through the architecture and init sequence step by step.\n\n**Silicon revision:** All current P4 hardware is v1.0 or v1.3 — not P4X (v3.x). Any BSP or example that requires v3.x minimum must be adapted. Required config:\n\n```\nCONFIG_ESP32P4_SELECTS_REV_LESS_THAN_V3=y\n```\n\n### Radio Initialization Order\n\nThe initialization sequence is strict — `esp_hosted` must be set up before anything else touches the network stack:\n\n```c\nesp_hosted_init();\nesp_hosted_connect_to_slave();   // establish SDIO link to C6\nnvs_flash_init();\nesp_netif_init();\nesp_event_loop_create_default();\nesp_wifi_init(\u0026cfg);\n// then WiFi config, start, connect\n```\n\nGetting this order wrong produces cryptic failures or hangs during WiFi init.\n\n### SDIO Configuration (P4 ↔ C6)\n\nThe SDIO interface between P4 and C6 is 4-bit mode at 40 MHz. The reset line to the C6 is active HIGH (unlike many reset signals). These settings are consistent across P4 boards using the C6 as a co-processor.\n\n```\nCONFIG_SDIO_CLK=18\nCONFIG_SDIO_CMD=19\nCONFIG_SDIO_D0=14 ... D3=17\nCONFIG_SLAVE_RESET_GPIO=54   (active HIGH)\nCONFIG_SDIO_CLOCK_FREQ_KHZ=40000\nCONFIG_SDIO_BUS_WIDTH_4=y\n```\n\n### Bluetooth (NimBLE over SDIO)\n\nThe P4 has no integrated BT controller — BT runs on the C6 and is accessed via VHCI over SDIO. Key config:\n\n```\nCONFIG_BT_CONTROLLER_DISABLED=y\nCONFIG_BT_NIMBLE_TRANSPORT_UART=n\nCONFIG_ESP_HOSTED_ENABLE_BT_NIMBLE=y\nCONFIG_ESP_HOSTED_NIMBLE_HCI_VHCI=y\n```\n\n**Known issue:** Logitech multi-device keyboards frequently lose sync when switching BT channels. Mitigation: provide a way to force re-pair (e.g., long-press BOOT button triggers disconnect + re-advertising).\n\n### C6 Co-processor Firmware\n\nWaveshare boards have shipped with C6 firmware v0.0.0 which does not support Bluetooth — check the C6 firmware version on first boot, as this may have been updated in later production runs. If the version is too old, flash the correct firmware manually:\n\n```bash\nesptool.py write_flash 0x310000 slave_fw.bin\n```\n\nAfter that, OTA updates to the slave firmware can be handled from the P4 at runtime.\n\n### PSRAM (HEX Mode, 200 MHz)\n\nP4 uses **HEX mode** PSRAM (not OCT). Running at 200 MHz requires experimental feature flag:\n\n```\nCONFIG_SPIRAM_MODE_HEX=y\nCONFIG_SPIRAM_SPEED_200M=y\nCONFIG_IDF_EXPERIMENTAL_FEATURES=y\n```\n\n**Critical — APM-560 errata (P4 v1.0/v1.3, not P4X):** An unauthorized AHB access will block ALL PSRAM and flash transactions until reset. Use 64-byte aligned allocations for anything DMA-touched:\n\n```c\nheap_caps_aligned_calloc(64, count, size, MALLOC_CAP_SPIRAM);\n```\n\n### Cache Sync Before DMA\n\nAny time the CPU writes to a PSRAM buffer that hardware (PPA, DMA) will read, sync the cache first:\n\n```c\nesp_cache_msync(buffer, size, ESP_CACHE_MSYNC_FLAG_DIR_C2M);\n```\n\nSkipping this causes silent corruption or incorrect display output that's hard to diagnose.\n\n### MIPI-DSI Display Init Sequence\n\nFor ST7703-based 720×720 panels, the initialization order matters:\n\n1. Initialize LDO channel 3 at 2.5V for the MIPI PHY\n2. Create DSI bus (2 lanes, 480 Mbps)\n3. Create DBI IO for command channel\n4. Configure DPI panel with video timing (pixel clock, porches)\n5. Reset → init → enable panel\n6. Allocate double render buffers in PSRAM (64-byte aligned)\n7. Set up PPA SRM async client for buffer copy\n\nTypical working DPI timing for 720×720 @ ~60 FPS:\n```\npixel_clock: 38 MHz\nh_back_porch: 50, h_pulse_width: 20, h_front_porch: 50\nv_back_porch: 20, v_pulse_width: 4, v_front_porch: 20\n```\n\n### Double-Buffered Rendering (PPA-Accelerated)\n\nThe P4's PPA (Pixel Processing Accelerator) handles buffer copies asynchronously. The pattern:\n\n- CPU renders into a back buffer\n- PPA copies previous back buffer to framebuffer via DMA\n- Semaphore synchronizes `flush_wait()` with PPA completion callback\n- Cache sync (`C2M`) required before every PPA transfer\n\nThis lets the CPU render the next frame while hardware handles the display copy.\n\n**PPA hardware rotation:** PPA can also rotate frames 90° CW in hardware — useful for landscape-source → portrait-display pipelines (e.g., decode a landscape JPEG, rotate in-place before the display copy).\n\n### Hardware JPEG Decode\n\nThe P4 has an onboard JPEG decoder. Key constraints:\n\n- Frame dimensions must be divisible by 8 (DMA alignment requirement)\n- Decoded output goes directly into a PSRAM buffer — zero copy into the display framebuffer is possible\n- Sustained ~30 fps to a 720×720 MIPI-DSI display is achievable with hardware decode + PPA double-buffer\n\nSoftware JPEG decode (e.g., `tjpgd` on ESP32-S3) tops out around 12 fps at 320×240 for comparison.\n\n### Audio Codecs\n\n**ES8311** (used in webradio and similar boards):\n- I2C address: `0x18`\n- Requires a separate I2S TX channel setup alongside the I2C config\n- Mono codec\n\n**ES8388** (M5Stack Tab5):\n- Stereo codec\n- Use the BSP (`espressif/esp-bsp`) rather than writing a raw driver: `bsp_audio_codec_speaker_init()`, `bsp_audio_codec_microphone_init()`\n\n### GT911 Capacitive Touchscreen\n\n- I2C address: `0x5D`\n- Use a direct I2C driver — not the BSP abstraction\n- Supports tap regions and swipe gestures (up/down/left/right)\n\n### A/V Sync via I2S DMA Counter\n\nWhen combining audio playback with video display, use the I2S DMA sample counter as the ground truth for elapsed time — not `esp_timer_get_time()` or wall clock.\n\n- I2S consumes samples at exactly the configured sample rate (e.g., 16 kHz)\n- Drive video frame requests from this counter: fetch the frame that corresponds to current audio position\n- Sync is \"by construction\" — no drift accumulation is possible\n- If video rendering falls behind, silence appears rather than audio/video desync\n\n### Console / Logging Configuration\n\nThe Waveshare P4 board uses **UART as primary console** with USB JTAG as secondary. The Espressif EV board uses USB_SERIAL_JTAG as primary. Using the EV board sdkconfig on Waveshare hardware produces no application logs — make sure your sdkconfig.defaults sets:\n\n```\nCONFIG_ESP_CONSOLE_UART_DEFAULT=y\nCONFIG_ESP_CONSOLE_SECONDARY_USB_SERIAL_JTAG=y\n```\n\n### Known Errata (P4 v1.0 / v1.3)\n\nThese affect the silicon revision on current boards (not P4X):\n\n| Errata | Description | Workaround |\n|--------|-------------|-----------|\n| APM-560 | Unauthorized AHB access blocks all PSRAM/flash until reset | 64-byte aligned DMA allocations |\n| RMT-176 | RMT idle state bug | Set `RMT_IDLE_OUT_EN_CHn=1` |\n| I2C-308 | I2C slave multi-read FIFO bug | Avoid I2C slave multi-read; use single reads |\n\n---\n\n## ESP32-S3\n\n### PSRAM Mode\n\nUse **octal mode** for boards with octal PSRAM. Quad mode on an octal board produces \"PSRAM chip is not connected\" errors.\n\n```\nCONFIG_SPIRAM_MODE_OCT=y\n```\n\n### Display Notes\n\n**ST7789 via SPI (240×320):**\n- 40 MHz SPI clock works reliably\n- Color inversion is required on most boards: `esp_lcd_panel_invert_color(panel, true)`\n\n**AMOLED via QSPI (LilyGo T4-S3 / RM67162):**\n- The vendor driver dimensions are swapped: `AMOLED_WIDTH=600` is the physical height, `AMOLED_HEIGHT=450` is the physical width. Use `amoled_height()` for column count and `amoled_width()` for row count.\n- The full framebuffer must be pushed in a single call — row-by-row transfers do not work with this display.\n- 36 MHz QSPI, pixel data is byte-swapped RGB565.\n\n### I2C / OLED Performance\n\nSH1107 OLED (128×128) performance at different I2C speeds:\n- 400 kHz → ~17 FPS\n- 1 MHz → ~42 FPS\n- 1 MHz + dirty page tracking → 60-70 FPS achievable\n\nInternal pull-ups are sufficient for short cable runs at 1 MHz.\n\n**Stack warning:** A 2048-byte framebuffer on the stack will overflow default 3584-byte task stacks. Declare it `static` or heap-allocate it.\n\n---\n\n## ESP32-C6\n\n### Standalone (XIAO ESP32-C6)\n\nNo onboard RGB LED — remove any `led_strip` component or it will fail to compile/link.\n\nI2C works reliably at 1 MHz with short cables; internal pull-ups are sufficient for devices like SH1107 at address 0x3C.\n\n---\n\n## WiFi Streaming Patterns\n\n### HTTP Pull vs TCP Push\n\n**HTTP pull** (preferred): The client requests each frame/audio chunk on demand; the server does not push.\n- Tolerates WiFi hiccups gracefully — a missed frame is retried on the next request cycle\n- A/V sync is simpler: lock to the audio counter and request the video frame at that timestamp\n- Used in: esp32-p4-webradio, m5stack-tab5-video-stream\n\n**TCP push**: Server sends frames length-prefixed over a persistent TCP connection.\n- Supports multiple simultaneous clients\n- Hiccup recovery is more complex — a dropped connection requires reconnect and resync\n- Used in: video-stream (legacy ESP32-S3 path)\n\nFor new projects, prefer HTTP pull unless multiple simultaneous viewers are required.\n\n---\n\n## BLE Keyboard Host (NimBLE)\n\nNotes for projects using `esp32-ble-kbd-host` or similar BLE HID host implementations.\n\n### Single Persistent Task\n\nUse one persistent `kbd_main_task` covering scan, connect, and reconnect — do not split these into separate tasks with hand-offs. A multi-task design misses BOOT button presses that arrive during the task transition window.\n\n### Lightweight Reconnect\n\nKeep the device object alive across a disconnect — do not free and recreate it. `esp_ble_hidh_dev_reconnect()` can then skip full GATT rediscovery on reconnect, which is significantly faster than a full pairing cycle.\n\n### Re-pair from Any State\n\n`start_pairing()` must be callable from any state, including during a reconnect retry loop. If it isn't, holding the BOOT button during reconnect will be silently ignored.\n\n### P4 Note\n\nOn ESP32-P4, BLE routes through the ESP32-C6 co-processor via VHCI over the same SDIO link as WiFi. The NimBLE config is the same as noted above in the Bluetooth section.\n\n---\n\n## wolfSSH\n\nNotes for projects using `esp32-wolfssh-client` or wolfSSH directly.\n\n### Session Setup Order\n\nThe setup sequence is strict — steps cannot be reordered:\n\n```\nwolfSSH_Init()          // once at startup, not per-session\nwolfSSH_CTX_new()\nwolfSSH_new()\nconnect (TCP)\nwolfSSH_set_channel_type(SHELL)\nwolfSSH_SendTerminalSize() / PTY request\nwolfSSH_accept()        // completes handshake\n// read/write loop\n```\n\nGetting the order wrong produces cryptic handshake failures.\n\n### Clear Socket Timeouts After Handshake\n\nSet socket timeouts during connect to avoid hanging on a dead server, but **clear them after the handshake completes**. Leaving timeouts active on the session socket causes spurious `EAGAIN` mid-session during legitimate pauses in output.\n\n```c\n// After wolfSSH_accept() succeeds:\nstruct timeval tv = {0, 0};\nsetsockopt(sock, SOL_SOCKET, SO_RCVTIMEO, \u0026tv, sizeof(tv));\nsetsockopt(sock, SOL_SOCKET, SO_SNDTIMEO, \u0026tv, sizeof(tv));\n```\n\n### wolfSSL user_settings.h Override (P4)\n\nThe default wolfSSL `user_settings.h` causes compile errors or silent runtime failures on ESP32-P4. Override it per-project — copy a working `user_settings.h` from `esp32-wolfssh-client` rather than deriving from scratch.\n\n### One Session at a Time\n\nOnly one SSH session can be active at a time. Call `disconnect()` and wait for the disconnect callback before opening a new session, or the next `wolfSSH_accept()` returns `ESP_ERR_INVALID_STATE`.\n\n### Keyboard Input Requires a Queue\n\nwolfSSH's write call blocks. Keyboard input must arrive via a thread-safe queue — do not call `wolfSSH_stream_send()` directly from a GPIO ISR or a different task without a queue in between.\n\n---\n\n## Cross-Variant\n\n### FreeRTOS Tick Rate\n\nThe default 100 Hz tick rate (10 ms resolution) is too coarse for animation. For smooth display loops:\n\n```\nCONFIG_FREERTOS_HZ=1000\n```\n\nThen use:\n```c\nvTaskDelayUntil(\u0026xLastWakeTime, pdMS_TO_TICKS(33)); // ~30 FPS\n```\n\n### CMakeLists.txt Component Dependencies\n\nESP-IDF processes `CMakeLists.txt` twice on first configure, and `CONFIG_*` variables are not available on the first pass. Components like `bt` and `esp_hid` must be in `REQUIRES` unconditionally — source file lists can still be conditional, but the component dependency cannot.\n\n```cmake\n# Wrong — bt won't be linked if CONFIG_BT_ENABLED isn't set on first pass\nif(CONFIG_BT_ENABLED)\n    list(APPEND requires bt)\nendif()\n\n# Right\nidf_component_register(... REQUIRES bt esp_hid ...)\n```\n\n### NVS Init Pattern\n\nAlways handle the version-mismatch case:\n\n```c\nesp_err_t ret = nvs_flash_init();\nif (ret == ESP_ERR_NVS_NO_FREE_PAGES || ret == ESP_ERR_NVS_NEW_VERSION_FOUND) {\n    ESP_ERROR_CHECK(nvs_flash_erase());\n    ret = nvs_flash_init();\n}\nESP_ERROR_CHECK(ret);\n```\n\n### ESP-IDF Version Notes\n\n- **Minimum for P4 + MIPI-DSI support:** v5.3\n- **Tested:** v5.5.1, v5.5.3\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fdmatking%2Fesp32-notes","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fdmatking%2Fesp32-notes","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fdmatking%2Fesp32-notes/lists"}