From c0623fccd78cff44470522ed2ae5fab6e8c7429b Mon Sep 17 00:00:00 2001 From: Peter M Date: Sun, 24 May 2026 11:30:16 +0200 Subject: [PATCH 1/6] epaper: add descriptor-driven panel API Move e-paper panel configuration into Erlang descriptors and teach the C driver to parse descriptor terms, execute compact command bytecode, manage LUT slots, and enforce descriptor geometry. Add descriptor constructors for the supported 2.9 inch and 2.13 inch SSD1680/JD79656 panels, including full, fast, partial, and 4-gray refresh paths. Apply logical view-to-native rotation when packing native frames so rotated descriptors render correctly. Split the Erlang package into a public facade, reusable descriptor builders, bytecode helpers, and a built-in panel registry so user projects can prototype custom panel descriptors out of tree. Document the descriptor flow, custom panel workflow, and Erlang-side validation coverage for orientation and descriptor shape. Signed-off-by: Peter M --- CMakeLists.txt | 1 + README.Md | 33 + atomgl_epaper/.gitignore | 9 + atomgl_epaper/ADD_PANEL.md | 171 +++ atomgl_epaper/CUSTOM_PANEL.md | 234 +++ atomgl_epaper/LICENSE | 202 +++ atomgl_epaper/README.md | 75 + atomgl_epaper/rebar.config | 16 + atomgl_epaper/src/atomgl_epaper.app.src | 13 + atomgl_epaper/src/atomgl_epaper.erl | 23 + .../src/atomgl_epaper_descriptor.erl | 189 +++ atomgl_epaper/src/atomgl_epaper_panels.erl | 408 +++++ atomgl_epaper/src/atomgl_epaper_program.erl | 76 + atomgl_epaper/test/atomgl_epaper_tests.erl | 77 + display_driver.c | 6 + docs/display-drivers.md | 120 ++ epaper_color.c | 97 +- epaper_color.h | 4 + epaper_commands.c | 34 +- epaper_commands.h | 90 +- epaper_display_driver.c | 1347 ++++++++++++++++- epaper_draw.c | 134 +- epaper_draw.h | 4 + epaper_program.c | 121 ++ epaper_program.h | 85 ++ tests/items/test_items.c | 14 +- 26 files changed, 3492 insertions(+), 91 deletions(-) create mode 100644 atomgl_epaper/.gitignore create mode 100644 atomgl_epaper/ADD_PANEL.md create mode 100644 atomgl_epaper/CUSTOM_PANEL.md create mode 100644 atomgl_epaper/LICENSE create mode 100644 atomgl_epaper/README.md create mode 100644 atomgl_epaper/rebar.config create mode 100644 atomgl_epaper/src/atomgl_epaper.app.src create mode 100644 atomgl_epaper/src/atomgl_epaper.erl create mode 100644 atomgl_epaper/src/atomgl_epaper_descriptor.erl create mode 100644 atomgl_epaper/src/atomgl_epaper_panels.erl create mode 100644 atomgl_epaper/src/atomgl_epaper_program.erl create mode 100644 atomgl_epaper/test/atomgl_epaper_tests.erl create mode 100644 epaper_program.c create mode 100644 epaper_program.h diff --git a/CMakeLists.txt b/CMakeLists.txt index 5fe14cb..685d15b 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -38,6 +38,7 @@ idf_component_register(SRCS "epaper_color.c" "epaper_commands.c" "epaper_draw.c" + "epaper_program.c" "mono_draw.c" "epaper_display_driver.c" "font_data.c" diff --git a/README.Md b/README.Md index f129067..b9cc1b5 100644 --- a/README.Md +++ b/README.Md @@ -93,6 +93,39 @@ software dithering See also [display drivers](docs/display-drivers.md) documentation page for additional information. +### E-paper panel descriptors + +Ready-made e-paper panel descriptors are provided as a separate, +dependency-free Erlang/OTP library in [`atomgl_epaper/`](atomgl_epaper/). It +can be consumed as a sparse / `subdir` dependency directly from this +repository — see [`atomgl_epaper/README.md`](atomgl_epaper/README.md) for +details. + +For Erlang (`rebar3`): + +```erlang +%% rebar.config +{deps, [ + {atomgl_epaper, + {git, "https://github.com/atomvm/atomgl.git", {branch, "main"}}, + [{subdir, "atomgl_epaper"}]} +]}. +``` + +For Elixir (Mix), add the dependency to `mix.exs`: + +```elixir +# mix.exs +defp deps do + [ + {:atomgl_epaper, + github: "atomvm/atomgl", + branch: "main", + sparse: "atomgl_epaper"} + ] +end +``` + ## Platform Support AtomGL is currently compatible with **ESP32** microcontrollers. Linux with SDL is also supported for diff --git a/atomgl_epaper/.gitignore b/atomgl_epaper/.gitignore new file mode 100644 index 0000000..27e2e34 --- /dev/null +++ b/atomgl_epaper/.gitignore @@ -0,0 +1,9 @@ +# SPDX-License-Identifier: Apache-2.0 +# SPDX-FileCopyrightText: 2026 Peter M + +_build/ +.rebar3/ +rebar.lock +ebin/ +*.beam +.eunit/ diff --git a/atomgl_epaper/ADD_PANEL.md b/atomgl_epaper/ADD_PANEL.md new file mode 100644 index 0000000..eb0f918 --- /dev/null +++ b/atomgl_epaper/ADD_PANEL.md @@ -0,0 +1,171 @@ + + + +# Adding E-Paper Panels + +This guide is for humans and LLM agents adding a new E-Paper panel descriptor to +AtomGL. The descriptor library now lives in this `atomgl_epaper` package; the +low-level C display driver still lives in the parent AtomGL component. Keep +panel additions small, traceable, and license-clean. + +## Source Policy + +Prefer primary, permissively licensed sources: + +- Vendor datasheets, product specs, and command tables. +- Vendor driver examples with compatible licenses, such as + [waveshareteam/e-Paper](https://github.com/waveshareteam/e-Paper). +- Existing descriptors in `atomgl_epaper/src/atomgl_epaper_panels.erl` and driver + helpers in the parent AtomGL tree. + +The Waveshare repository is a useful starting point for many panel init +sequences, refresh commands, LUT payloads, reset timing, BUSY polarity, and +native dimensions. When copying bytes or command sequences from permissively +licensed source, preserve the relevant attribution or license notice near the +copied data. + +Treat GPL code as reference only: + +- [bitbank2/bb_epaper](https://github.com/bitbank2/bb_epaper) and similar GPL + projects are useful for identifying controller families, panel aliases, and + behavioral clues. +- Do not copy GPL code, LUT tables, command byte arrays, comments, names chosen + only by that project, or implementation structure into AtomGL. +- If GPL code helped orient the work, describe it as corroborating reference + material in the commit or PR notes, not as the source for copied content. + +If the license of a source is unclear, do not copy from it. Use it only to form +questions, then verify details from a primary source or hardware testing. + +## Before Editing + +Collect these facts for the target panel: + +- Panel marketing name and likely compatible lookup strings. +- Native width and height, not just the desired display orientation. +- Controller IC or controller family. If uncertain, name the family rather than + inventing a precise controller. +- BUSY idle level, reset pulse timing, and whether GPIO pullups are needed. +- RAM data commands for old/current planes or color planes. +- Frame layout: row-major or column-major, MSB or LSB first. +- Pixel polarity: whether `1` means white or black. +- Refresh modes: full, fast, partial, 4-gray, color, sleep. +- LUT format and whether extra register bytes accompany the LUT payload. +- Any controller-specific plane mapping, palette, or ghosting constraints. + +Use `rg` to search this package, the parent driver, and source trees for similar +panels first. Match the existing descriptor style instead of inventing a new +shape. + +Important local paths: + +- `atomgl_epaper/src/atomgl_epaper.erl` - public `panel/1`, `panel/2`, and + descriptor validation facade. +- `atomgl_epaper/src/atomgl_epaper_panels.erl` - panel lookup strings, + controller command sequences, and LUTs. +- `atomgl_epaper/src/atomgl_epaper_descriptor.erl` - descriptor builders, + orientation handling, timing defaults, and validation. +- `atomgl_epaper/src/atomgl_epaper_program.erl` - bytecode helpers for + commands, delays, BUSY waits, plane insertion, and LUT insertion. +- `atomgl_epaper/test/atomgl_epaper_tests.erl` - descriptor and validation + tests. +- `atomgl_epaper/README.md` - package usage and public package notes. +- `atomgl_epaper/CUSTOM_PANEL.md` - userland descriptor prototyping guide. +- `epaper_commands.h` and `epaper_display_driver.c` - parent AtomGL C driver + files, needed only when controller behavior changes. + +## Implementation Checklist + +1. Decide whether this is descriptor-only + + Most panels should be descriptor-only changes in + `atomgl_epaper/src/atomgl_epaper_panels.erl`. Reuse an existing controller + when the command set and driver behavior match. + + Add a new controller only when parsing, LUT insertion, plane writes, palette + mapping, refresh flow, or BUSY behavior needs distinct C-side handling. + + Controller changes usually touch: + + - `epaper_commands.h` + - `epaper_display_driver.c` + +2. Add a panel descriptor + + Panel lookup clauses live in `atomgl_epaper/src/atomgl_epaper_panels.erl`. + Add: + + - A primary lookup string, usually vendor-qualified. + - Optional aliases for mode defaults, such as `-fast`, `-partial`, or + `-4gray`. + - Native geometry, palette size, refresh modes, and default refresh mode. + - Init, refresh, partial, 4-gray, LUT, and sleep programs as needed. + - Helper functions only when they reduce duplication or make the sequence + easier to audit. + +3. Keep driver behavior generic + + The C driver should know about controller behavior, not product names. Avoid + hardcoding a specific panel lookup string in the driver. Product-specific + command values should usually live in the descriptor bytecode. + +4. Document the lookup strings + + Update package docs when a new public lookup string is added. If the parent + AtomGL docs include a supported-panel list, update that too. + +5. Add descriptor tests + + Add focused EUnit coverage in `atomgl_epaper/test/atomgl_epaper_tests.erl` + for: + + - Controller atom. + - Native and view dimensions. + - Refresh mode list and default mode aliases. + - LUT byte size or palette size when relevant. + +## Verification + +Run the lightweight descriptor checks: + +```sh +cd atomgl_epaper +rebar3 eunit +rebar3 xref +cd .. +git diff --check +``` + +For descriptor-only changes, `rebar3 eunit` is the main package test. For +controller or C driver changes, also run the broader parent AtomGL tests +available in your environment. If hardware is available, verify: + +- Full refresh clears and draws correctly. +- Fast or partial refresh updates the expected window without stale-plane + artifacts. +- Sleep and wake work repeatedly. +- 4-gray or color palettes map white, gray/color levels, and black correctly. +- Timeout behavior matches the panel BUSY polarity. + +Record which hardware revision was tested. Some Waveshare panels changed +controller behavior across V1/V2/V3 or after a production date. + +## LLM Agent Rules + +When an LLM agent adds a panel: + +- State which source files or datasheets were used and their licenses. +- Use GPL sources only as reference; never copy bytes, arrays, or comments from + them. +- Prefer Waveshare or other permissive vendor sources for command bytes and LUTs + when available. +- Do not claim an exact controller when the evidence only says `UC81xx`, + `SSD16xx`, or another family name. +- Keep the change in a separate commit for each controller or panel family. +- Keep descriptor tests in `atomgl_epaper/test`, not inline in the descriptor + module. +- Do not refactor unrelated display code while adding a panel. +- Run the verification commands above and report failures honestly. + +When in doubt, leave a narrowly worded note in the descriptor name or docs, such +as `UC81xx-compatible`, instead of encoding a guessed controller identity. diff --git a/atomgl_epaper/CUSTOM_PANEL.md b/atomgl_epaper/CUSTOM_PANEL.md new file mode 100644 index 0000000..4044ba5 --- /dev/null +++ b/atomgl_epaper/CUSTOM_PANEL.md @@ -0,0 +1,234 @@ + + + +# Custom E-Paper Panels + +You can prototype a panel descriptor in your own Erlang or Elixir project +without adding a lookup string to AtomGL. Use `atomgl_epaper_descriptor` to +build and validate the descriptor, and use `atomgl_epaper_program` to build the +bytecode programs consumed by the C display driver. + +Once the descriptor works on hardware, it can be copied into +`atomgl_epaper/src/atomgl_epaper_panels.erl` with a public lookup string and a +focused descriptor test. + +## Example: Waveshare 1.54 V2 200x200 + +The example below is a userland descriptor module for the Waveshare 1.54 inch +V2 200x200 monochrome panel. The command sequence and waveform bytes are +adapted from Waveshare's MIT-licensed `EPD_1in54_V2` driver: + +- `Arduino_R4/src/e-Paper/EPD_1in54_V2.cpp` +- `Arduino_R4/src/e-Paper/EPD_1in54_V2.h` + +The AtomGL controller atom is `ssd16xx`; this covers SSD1680-compatible +program-driven monochrome panels. + +```erlang +%% SPDX-License-Identifier: MIT +%% SPDX-SnippetCopyrightText: Waveshare team + +-module(my_epaper_200x200). + +-export([ + descriptor/0, + descriptor/1 +]). + +-import(atomgl_epaper_program, [ + program/1, + cmd/1, + cmd/2, + wait_busy/2, + reset/3, + insert_plane/1, + insert_lut/1, + insert_prev_frame/1, + capture_frame/0, + mark_prev_valid/0, + delay_ms/1 +]). + +-define(LUT_FULL, 0). +-define(LUT_PARTIAL, 1). + +descriptor() -> + descriptor(#{}). + +descriptor(Opts) -> + atomgl_epaper_descriptor:ssd16xx_panel( + "Waveshare 1.54 V2 200x200 e-paper SSD16xx-compatible", + maps:merge(#{ + native_width => 200, + native_height => 200, + refresh_modes => [full, partial], + default_refresh => full, + init => init(), + full => full_refresh(), + partial => partial_refresh(), + sleep => cmd(16#10, <<16#01>>), + lut_full => wf_full_1in54(), + lut_partial => wf_partial_1in54() + }, Opts)). + +init() -> + program([ + reset(200, 2, 200), + wait_busy(0, 5000), + cmd(16#12), + wait_busy(0, 5000), + cmd(16#01, <<16#C7, 16#00, 16#01>>), + cmd(16#11, <<16#01>>), + full_window(), + cmd(16#3C, <<16#01>>), + cmd(16#18, <<16#80>>), + cmd(16#22, <<16#B1>>), + cmd(16#20), + set_ram_cursor(), + wait_busy(0, 5000), + insert_lut(?LUT_FULL) + ]). + +full_refresh() -> + program([ + insert_lut(?LUT_FULL), + set_ram_cursor(), + cmd(16#24), + capture_frame(), + insert_plane(0), + delay_ms(2), + set_ram_cursor(), + cmd(16#26), + insert_prev_frame(0), + cmd(16#22, <<16#C7>>), + cmd(16#20), + wait_busy(0, 5000), + mark_prev_valid() + ]). + +partial_refresh() -> + program([ + insert_lut(?LUT_PARTIAL), + cmd(16#37, <<16#00, 16#00, 16#00, 16#00, 16#00, + 16#40, 16#00, 16#00, 16#00, 16#00>>), + cmd(16#3C, <<16#80>>), + cmd(16#22, <<16#C0>>), + cmd(16#20), + wait_busy(0, 5000), + set_ram_cursor(), + cmd(16#26), + insert_prev_frame(0), + delay_ms(2), + set_ram_cursor(), + cmd(16#24), + capture_frame(), + insert_plane(0), + cmd(16#22, <<16#CF>>), + cmd(16#20), + wait_busy(0, 5000), + mark_prev_valid() + ]). + +full_window() -> + [ + cmd(16#44, <<16#00, 16#18>>), + cmd(16#45, <<16#C7, 16#00, 16#00, 16#00>>) + ]. + +set_ram_cursor() -> + [ + cmd(16#4E, <<16#00>>), + cmd(16#4F, <<16#C7, 16#00>>) + ]. + +wf_full_1in54() -> + << + 16#80, 16#48, 16#40, 16#00, 16#00, 16#00, + 16#00, 16#00, 16#00, 16#00, 16#00, 16#00, + 16#40, 16#48, 16#80, 16#00, 16#00, 16#00, + 16#00, 16#00, 16#00, 16#00, 16#00, 16#00, + 16#80, 16#48, 16#40, 16#00, 16#00, 16#00, + 16#00, 16#00, 16#00, 16#00, 16#00, 16#00, + 16#40, 16#48, 16#80, 16#00, 16#00, 16#00, + 16#00, 16#00, 16#00, 16#00, 16#00, 16#00, + 16#00, 16#00, 16#00, 16#00, 16#00, 16#00, + 16#00, 16#00, 16#00, 16#00, 16#00, 16#00, + 16#0A, 16#00, 16#00, 16#00, 16#00, 16#00, + 16#00, 16#08, 16#01, 16#00, 16#08, 16#01, + 16#00, 16#02, 16#0A, 16#00, 16#00, 16#00, + 16#00, 16#00, 16#00, 16#00, 16#00, 16#00, + 16#00, 16#00, 16#00, 16#00, 16#00, 16#00, + 16#00, 16#00, 16#00, 16#00, 16#00, 16#00, + 16#00, 16#00, 16#00, 16#00, 16#00, 16#00, + 16#00, 16#00, 16#00, 16#00, 16#00, 16#00, + 16#00, 16#00, 16#00, 16#00, 16#00, 16#00, + 16#00, 16#00, 16#00, 16#00, 16#00, 16#00, + 16#00, 16#00, 16#00, 16#00, 16#00, 16#00, + 16#00, 16#00, 16#00, 16#00, 16#00, 16#00, + 16#00, 16#00, 16#00, 16#00, 16#00, 16#00, + 16#00, 16#00, 16#00, 16#00, 16#00, 16#00, + 16#22, 16#22, 16#22, 16#22, 16#22, 16#22, + 16#00, 16#00, 16#00, 16#22, 16#17, 16#41, + 16#00, 16#32, 16#20 + >>. + +wf_partial_1in54() -> + << + 16#00, 16#40, 16#00, 16#00, 16#00, 16#00, + 16#00, 16#00, 16#00, 16#00, 16#00, 16#00, + 16#80, 16#80, 16#00, 16#00, 16#00, 16#00, + 16#00, 16#00, 16#00, 16#00, 16#00, 16#00, + 16#40, 16#40, 16#00, 16#00, 16#00, 16#00, + 16#00, 16#00, 16#00, 16#00, 16#00, 16#00, + 16#00, 16#80, 16#00, 16#00, 16#00, 16#00, + 16#00, 16#00, 16#00, 16#00, 16#00, 16#00, + 16#00, 16#00, 16#00, 16#00, 16#00, 16#00, + 16#00, 16#00, 16#00, 16#00, 16#00, 16#00, + 16#0F, 16#00, 16#00, 16#00, 16#00, 16#00, + 16#00, 16#01, 16#01, 16#00, 16#00, 16#00, + 16#00, 16#00, 16#00, 16#00, 16#00, 16#00, + 16#00, 16#00, 16#00, 16#00, 16#00, 16#00, + 16#00, 16#00, 16#00, 16#00, 16#00, 16#00, + 16#00, 16#00, 16#00, 16#00, 16#00, 16#00, + 16#00, 16#00, 16#00, 16#00, 16#00, 16#00, + 16#00, 16#00, 16#00, 16#00, 16#00, 16#00, + 16#00, 16#00, 16#00, 16#00, 16#00, 16#00, + 16#00, 16#00, 16#00, 16#00, 16#00, 16#00, + 16#00, 16#00, 16#00, 16#00, 16#00, 16#00, + 16#00, 16#00, 16#00, 16#00, 16#00, 16#00, + 16#00, 16#00, 16#00, 16#00, 16#00, 16#00, + 16#00, 16#00, 16#00, 16#00, 16#00, 16#00, + 16#22, 16#22, 16#22, 16#22, 16#22, 16#22, + 16#00, 16#00, 16#00, 16#02, 16#17, 16#41, + 16#B0, 16#32, 16#28 + >>. +``` + +Use the descriptor exactly like a built-in one: + +```erlang +{ok, Descriptor} = my_epaper_200x200:descriptor(), + +Port = open_port({spawn, "display"}, [ + {descriptor, Descriptor}, + {spi_host, spi2}, + {cs, 5}, + {dc, 17}, + {reset, 16}, + {busy, 4} +]). +``` + +The GPIO numbers above are placeholders. Use the pins from your board wiring. + +## Moving A Working Panel Into AtomGL + +After hardware testing, move the descriptor into +`atomgl_epaper/src/atomgl_epaper_panels.erl`: + +- Add a primary lookup string, such as `"waveshare,epd1in54_V2"`. +- Keep aliases or mode-default variants close to the primary clause. +- Keep copied vendor LUTs under an SPDX snippet notice. +- Add descriptor tests for controller, dimensions, refresh modes, and LUT byte + sizes. +- Update `README.md` and `docs/display-drivers.md` with the new lookup string. diff --git a/atomgl_epaper/LICENSE b/atomgl_epaper/LICENSE new file mode 100644 index 0000000..d645695 --- /dev/null +++ b/atomgl_epaper/LICENSE @@ -0,0 +1,202 @@ + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/atomgl_epaper/README.md b/atomgl_epaper/README.md new file mode 100644 index 0000000..4a34b43 --- /dev/null +++ b/atomgl_epaper/README.md @@ -0,0 +1,75 @@ + + +# atomgl_epaper + +A small Erlang/OTP library that provides ready-made panel descriptors and a +bytecode builder for e-paper displays driven by [AtomGL](../README.Md). + +The main public entry point is `atomgl_epaper`, whose `panel/1` and +`panel/2` functions return a validated descriptor (`{ok, Descriptor}`) that +can be passed straight to the AtomGL e-paper display driver. + +For custom out-of-tree panels, `atomgl_epaper_descriptor` and +`atomgl_epaper_program` can be used directly from your own project. See +[`CUSTOM_PANEL.md`](CUSTOM_PANEL.md). + +## Usage + +```erlang +{ok, Descriptor} = atomgl_epaper:panel("waveshare,epd2in9_V2"), +%% Descriptor can now be used to open the AtomGL display port. +``` + +An optional options map may override defaults such as `orientation`, +`rotation`, `view_width`, `view_height`, `spi_clock_hz`, etc.: + +```erlang +{ok, Descriptor} = + atomgl_epaper:panel("waveshare,epd2in9_V2", + #{orientation => portrait}). +``` + +## Adding this library to your project + +This library lives inside the [AtomGL](https://github.com/atomvm/atomgl) +mono-repo. Both `rebar3` and `mix` can pull just this subdirectory. + +### rebar3 + +```erlang +%% rebar.config +{deps, [ + {atomgl_epaper, + {git, "https://github.com/atomvm/atomgl.git", {branch, "main"}}, + [{subdir, "atomgl_epaper"}]} +]}. +``` + +### Elixir (Mix) + +```elixir +# mix.exs +defp deps do + [ + {:atomgl_epaper, + github: "atomvm/atomgl", + branch: "main", + sparse: "atomgl_epaper"} + ] +end +``` + +## Tests + +```bash +cd atomgl_epaper +rebar3 eunit +``` + +## License + +Apache-2.0. See [`LICENSE`](LICENSE). diff --git a/atomgl_epaper/rebar.config b/atomgl_epaper/rebar.config new file mode 100644 index 0000000..637c005 --- /dev/null +++ b/atomgl_epaper/rebar.config @@ -0,0 +1,16 @@ +%% SPDX-License-Identifier: Apache-2.0 +%% SPDX-FileCopyrightText: 2026 Peter M + +{erl_opts, [debug_info]}. + +{deps, []}. + +{eunit_opts, [verbose]}. + +{xref_checks, [ + undefined_function_calls, + undefined_functions, + locals_not_used, + deprecated_function_calls, + deprecated_functions +]}. diff --git a/atomgl_epaper/src/atomgl_epaper.app.src b/atomgl_epaper/src/atomgl_epaper.app.src new file mode 100644 index 0000000..3ee48a7 --- /dev/null +++ b/atomgl_epaper/src/atomgl_epaper.app.src @@ -0,0 +1,13 @@ +%% SPDX-License-Identifier: Apache-2.0 +%% SPDX-FileCopyrightText: 2026 Peter M + +{application, atomgl_epaper, [ + {description, "AtomGL e-paper panel descriptors and bytecode builders"}, + {vsn, "0.1.0"}, + {registered, []}, + {applications, [kernel, stdlib]}, + {env, []}, + {modules, []}, + {licenses, ["Apache-2.0"]}, + {links, [{"GitHub", "https://github.com/atomvm/atomgl"}]} +]}. diff --git a/atomgl_epaper/src/atomgl_epaper.erl b/atomgl_epaper/src/atomgl_epaper.erl new file mode 100644 index 0000000..901202c --- /dev/null +++ b/atomgl_epaper/src/atomgl_epaper.erl @@ -0,0 +1,23 @@ +%% SPDX-License-Identifier: Apache-2.0 +%% SPDX-FileCopyrightText: 2026 Peter M + +-module(atomgl_epaper). + +-export([ + panel/1, + panel/2, + validate_descriptor/1 +]). + +panel(Compatible) when is_binary(Compatible) -> + panel(binary_to_list(Compatible), #{}); +panel(Compatible) -> + panel(Compatible, #{}). + +panel(Compatible, Opts) when is_binary(Compatible) -> + panel(binary_to_list(Compatible), Opts); +panel(Compatible, Opts) -> + atomgl_epaper_panels:panel(Compatible, Opts). + +validate_descriptor(Desc) -> + atomgl_epaper_descriptor:validate_descriptor(Desc). diff --git a/atomgl_epaper/src/atomgl_epaper_descriptor.erl b/atomgl_epaper/src/atomgl_epaper_descriptor.erl new file mode 100644 index 0000000..9419cb6 --- /dev/null +++ b/atomgl_epaper/src/atomgl_epaper_descriptor.erl @@ -0,0 +1,189 @@ +%% SPDX-License-Identifier: Apache-2.0 +%% SPDX-FileCopyrightText: 2026 Peter M + +-module(atomgl_epaper_descriptor). + +-export([ + ssd16xx_panel/2, + jd79656_panel/2, + validate_descriptor/1 +]). + +ssd16xx_panel(Name, Config) -> + build_panel(ssd16xx, Name, Config). + +jd79656_panel(Name, Config) -> + build_panel(jd79656, Name, Config). + +build_panel(Controller, Name, Config) -> + NativeW = maps:get(native_width, Config), + NativeH = maps:get(native_height, Config), + Orientation = maps:get(orientation, Config, landscape), + {ViewW, ViewH, Rotation} = case Orientation of + landscape -> {NativeH, NativeW, 90}; + landscape_left -> {NativeH, NativeW, 90}; + landscape_right -> {NativeH, NativeW, 270}; + portrait -> {NativeW, NativeH, 0}; + portrait_flipped -> {NativeW, NativeH, 180}; + _ -> error({bad_orientation, Orientation}) + end, + + DefaultRefresh = maps:get(default_refresh, Config, full), + RefreshModes = maps:get(refresh_modes, Config, [full]), + PaletteSize = maps:get(palette_size, Config, 2), + + Luts = [ + {lut_full, maps:get(lut_full, Config, <<>>)}, + {lut_partial, maps:get(lut_partial, Config, <<>>)}, + {lut_4gray, maps:get(lut_4gray, Config, <<>>)}, + {lut_fast, maps:get(lut_fast, Config, <<>>)} + ], + + Programs = mode_programs(Config), + Timing = timing(Config), + GhostingConfig = maps:get(ghosting, Config, #{}), + Ghosting = [ + {max_fast_refreshes, maps:get(max_fast_refreshes, GhostingConfig, maps:get(max_fast_refreshes, Config, 10))}, + {reseed_on_timeout, maps:get(reseed_on_timeout, GhostingConfig, maps:get(reseed_on_timeout, Config, true))} + ], + + {Layout, Polarity} = case Controller of + ssd16xx -> {row_msb, white_1}; + jd79656 -> {column_msb, black_1}; + uc8175 -> {row_msb, white_1} + end, + + Descriptor = [ + {descriptor_version, 2}, + {name, Name}, + {controller, Controller}, + {native_width, NativeW}, + {native_height, NativeH}, + {view_width, ViewW}, + {view_height, ViewH}, + {rotation, Rotation}, + {spi_clock_hz, maps:get(spi_clock_hz, Config, 4000000)}, + {busy_idle_level, maps:get(busy_idle_level, Config, 0)}, + {use_gpio_pullups, maps:get(use_gpio_pullups, Config, false)}, + {frame_layout, Layout}, + {polarity, Polarity}, + {refresh_modes, RefreshModes}, + {default_refresh, DefaultRefresh}, + {palette_size, PaletteSize}, + {programs, Programs}, + {timing, Timing}, + {ghosting, Ghosting} + ] ++ [L || L = {_, Bin} <- Luts, Bin =/= <<>>], + + validate_descriptor(Descriptor). + +mode_programs(Config) -> + P0 = [], + P1 = case maps:find(init, Config) of + {ok, Init} -> [{init, Init} | P0]; + error -> P0 + end, + P2 = case maps:find(full, Config) of + {ok, Full} -> [{full, Full} | P1]; + error -> P1 + end, + P3 = case maps:find(fast, Config) of + {ok, Fast} -> [{fast, Fast} | P2]; + error -> P2 + end, + P4 = case maps:find(partial, Config) of + {ok, Partial} -> [{partial, Partial} | P3]; + error -> P3 + end, + P5 = case maps:find(sleep, Config) of + {ok, Sleep} -> [{sleep, Sleep} | P4]; + error -> P4 + end, + P6 = case maps:find('4gray', Config) of + {ok, FourGray} -> [{'4gray', FourGray} | P5]; + error -> P5 + end, + P6. + +timing(Config) -> + [ + {full_expected_ms, maps:get(full_expected_ms, Config, 2000)}, + {fast_expected_ms, maps:get(fast_expected_ms, Config, 500)}, + {poll_interval_ms, maps:get(poll_interval_ms, Config, 50)}, + {timeout_ms, maps:get(timeout_ms, Config, 5000)} + ]. + +validate_descriptor(Desc) -> + Version = get_value(descriptor_version, Desc), + true = (Version == 2), + + NativeW = get_value(native_width, Desc), + NativeH = get_value(native_height, Desc), + ViewW = get_value(view_width, Desc), + ViewH = get_value(view_height, Desc), + Rotation = get_value(rotation, Desc), + + %% Width/height consistency check + case Rotation of + 0 -> + true = (ViewW == NativeW), + true = (ViewH == NativeH); + 180 -> + true = (ViewW == NativeW), + true = (ViewH == NativeH); + 90 -> + true = (ViewW == NativeH), + true = (ViewH == NativeW); + 270 -> + true = (ViewW == NativeH), + true = (ViewH == NativeW); + _ -> + error({bad_rotation, Rotation}) + end, + + Controller = get_value(controller, Desc), + true = lists:member(Controller, [ssd16xx, jd79656, uc8175, acep7]), + + Layout = get_value(frame_layout, Desc), + true = case Controller of + ssd16xx -> lists:member(Layout, [row_msb, row_lsb]); + uc8175 -> lists:member(Layout, [row_msb, row_lsb]); + jd79656 -> lists:member(Layout, [column_msb, column_lsb]); + _ -> true + end, + + DefaultRefresh = get_value(default_refresh, Desc), + RefreshModes = get_value(refresh_modes, Desc), + true = lists:member(DefaultRefresh, RefreshModes), + + Programs = get_value(programs, Desc), + true = is_defined(init, Programs), + + %% Required programs for declared modes + lists:foreach(fun(Mode) -> + true = is_defined(Mode, Programs) + end, RefreshModes), + + %% Bytecode payload length limits + lists:foreach(fun({_K, V}) -> + true = (byte_size(V) =< 4096) + end, Programs), + + {ok, Desc}. + +get_value(Key, List) -> + get_value(Key, List, undefined). + +get_value(Key, [{Key, Val} | _], _Default) -> + Val; +get_value(Key, [_ | Tail], Default) -> + get_value(Key, Tail, Default); +get_value(_Key, [], Default) -> + Default. + +is_defined(Key, [{Key, _} | _]) -> + true; +is_defined(Key, [_ | Tail]) -> + is_defined(Key, Tail); +is_defined(_Key, []) -> + false. diff --git a/atomgl_epaper/src/atomgl_epaper_panels.erl b/atomgl_epaper/src/atomgl_epaper_panels.erl new file mode 100644 index 0000000..73c5296 --- /dev/null +++ b/atomgl_epaper/src/atomgl_epaper_panels.erl @@ -0,0 +1,408 @@ +%% SPDX-License-Identifier: Apache-2.0 +%% SPDX-FileCopyrightText: 2026 Peter M + +-module(atomgl_epaper_panels). + +-export([ + panel/2 +]). + +-import(atomgl_epaper_descriptor, [ + jd79656_panel/2, + ssd16xx_panel/2 +]). + +-import(atomgl_epaper_program, [ + program/1, + cmd/1, + cmd/2, + wait_busy/2, + reset/3, + insert_plane/1, + insert_lut/1, + insert_prev_frame/1, + capture_frame/0, + mark_prev_valid/0, + label_delay/2, + delay_ms/1 +]). + +-define(LUT_FULL, 0). +-define(LUT_PARTIAL, 1). +-define(LUT_4GRAY, 2). +-define(LUT_FAST, 3). + +panel("waveshare,epaper-2in9", Opts) -> + ssd16xx_panel("Waveshare 2.9\" e-paper SSD1680 v1", maps:merge(#{ + native_width => 128, + native_height => 296, + refresh_modes => [full], + default_refresh => full, + init => ssd1680_2in9_init(), + full => ssd1680_2in9_refresh(), + sleep => cmd(16#10, <<16#01>>) + }, Opts)); +panel("waveshare,epd2in9_V2", Opts) -> + ssd16xx_panel("Waveshare epd2in9_V2 2.9\" e-paper SSD1680 v1", maps:merge(#{ + native_width => 128, + native_height => 296, + palette_size => 4, + refresh_modes => [full, fast, partial, '4gray'], + default_refresh => full, + init => epd2in9_v2_init(none), + full => epd2in9_v2_refresh(?LUT_FULL, 16#C7), + fast => epd2in9_v2_refresh(?LUT_FAST, 16#C7), + partial => epd2in9_v2_partial_refresh(), + '4gray' => epd2in9_v2_4gray_refresh(), + sleep => cmd(16#10, <<16#01>>), + lut_full => epd2in9_v2_default_lut(), + lut_fast => epd2in9_v2_fast_lut(), + lut_partial => epd2in9_v2_partial_lut(), + lut_4gray => epd2in9_v2_4gray_lut() + }, Opts)); +panel("waveshare,epd2in9_V2-fast", Opts) -> + panel("waveshare,epd2in9_V2", maps:merge(#{default_refresh => fast}, Opts)); +panel("waveshare,epd2in9_V2-partial", Opts) -> + panel("waveshare,epd2in9_V2", maps:merge(#{default_refresh => partial}, Opts)); +panel("waveshare,epd2in9_V2-4gray", Opts) -> + panel("waveshare,epd2in9_V2", maps:merge(#{default_refresh => '4gray'}, Opts)); +panel("dke,depg0290bns800f6", Opts) -> + ssd16xx_panel("DKE DEPG0290BNS800F6 2.9\" e-paper SSD1680 v1", maps:merge(#{ + native_width => 128, + native_height => 296, + refresh_modes => [full], + default_refresh => full, + init => ssd1680_2in9_init(), + full => ssd1680_2in9_refresh(), + sleep => cmd(16#10, <<16#01>>) + }, Opts)); +panel("waveshare,epaper-2in13", Opts) -> + panel("waveshare,epd2in13_V4", Opts); +panel("waveshare,epd2in13_V4", Opts) -> + ssd16xx_panel("Waveshare epd2in13_V4 2.13\" e-paper SSD1680 v1", maps:merge(#{ + native_width => 122, + native_height => 250, + refresh_modes => [full, fast, partial], + default_refresh => full, + init => epd2in13_v4_init(), + full => epd2in13_v4_refresh(16#F7), + fast => epd2in13_v4_refresh(16#C7), + partial => epd2in13_v4_partial_refresh(), + sleep => cmd(16#10, <<16#01>>) + }, Opts)); +panel("waveshare,epd2in13_V4-fast", Opts) -> + panel("waveshare,epd2in13_V4", maps:merge(#{default_refresh => fast}, Opts)); +panel("waveshare,epd2in13_V4-partial", Opts) -> + panel("waveshare,epd2in13_V4", maps:merge(#{default_refresh => partial}, Opts)); +panel("heltec,lcmen2r13efc1", Opts) -> + jd79656_panel("Heltec LCMEN2R13EFC1 2.13\" e-paper JD79656 v1", maps:merge(#{ + native_width => 122, + native_height => 250, + refresh_modes => [full], + default_refresh => full, + init => heltec_lcmen2r13efc1_init(), + full => heltec_lcmen2r13efc1_refresh(), + sleep => program([cmd(16#02), wait_busy(1, 5000)]) + }, Opts)); +panel("heltec,icmen2r13efc1", Opts) -> + panel("heltec,lcmen2r13efc1", Opts); +panel("heltec,ht-vme213", Opts) -> + panel("heltec,lcmen2r13efc1", Opts); +panel(_, _Opts) -> + error. + +ssd1680_2in9_init() -> + program([ + reset(10, 10, 100), + wait_busy(0, 5000), + cmd(16#12), + wait_busy(0, 5000), + label_delay(0, 20), + cmd(16#3C, <<16#05>>), + cmd(16#2C, <<16#36>>), + cmd(16#03, <<16#17>>), + cmd(16#04, <<16#41, 16#00, 16#32>>), + cmd(16#11, <<16#03>>), + cmd(16#44, <<16#01, 16#10>>), + cmd(16#45, <<16#00, 16#00, 16#27, 16#01>>), + set_ram_cursor(1), + cmd(16#01, <<16#27, 16#01, 16#00>>) + ]). + +ssd1680_2in9_refresh() -> + program([ + set_ram_cursor(1), + cmd(16#24), + capture_frame(), + insert_plane(0), + delay_ms(2), + set_ram_cursor(1), + cmd(16#26), + insert_plane(1), + cmd(16#22, <<16#F4>>), + cmd(16#20), + wait_busy(0, 5000), + mark_prev_valid() + ]). + +epd2in13_v4_init() -> + program([ + reset(20, 2, 20), + wait_busy(0, 5000), + cmd(16#12), + wait_busy(0, 5000), + cmd(16#01, <<16#F9, 16#00, 16#00>>), + cmd(16#11, <<16#03>>), + epd2in13_v4_window(), + epd2in13_v4_cursor(), + cmd(16#3C, <<16#05>>), + cmd(16#21, <<16#00, 16#80>>), + cmd(16#18, <<16#80>>), + wait_busy(0, 5000) + ]). + +epd2in13_v4_refresh(UpdateMode) -> + program([ + epd2in13_v4_cursor(), + cmd(16#24), + capture_frame(), + insert_plane(0), + epd2in13_v4_cursor(), + cmd(16#26), + insert_prev_frame(0), + cmd(16#22, <>), + cmd(16#20), + wait_busy(0, 5000), + mark_prev_valid() + ]). + +epd2in13_v4_partial_refresh() -> + program([ + reset(0, 1, 0), + cmd(16#3C, <<16#80>>), + cmd(16#01, <<16#F9, 16#00, 16#00>>), + cmd(16#11, <<16#03>>), + epd2in13_v4_window(), + epd2in13_v4_cursor(), + cmd(16#24), + capture_frame(), + insert_plane(0), + cmd(16#22, <<16#FF>>), + cmd(16#20), + wait_busy(0, 5000), + mark_prev_valid() + ]). + +epd2in13_v4_window() -> + [cmd(16#44, <<16#00, 16#0F>>), cmd(16#45, <<16#00, 16#00, 16#F9, 16#00>>)]. + +epd2in13_v4_cursor() -> + [cmd(16#4E, <<16#00>>), cmd(16#4F, <<16#00, 16#00>>)]. + +%% SPDX-SnippetBegin +%% SPDX-SnippetCopyrightText: Copyright (c) 2019 Heltec Automation +%% SPDX-License-Identifier: MIT +heltec_lcmen2r13efc1_init() -> + program([ + reset(100, 100, 100), + wait_busy(1, 5000), + cmd(16#12), + wait_busy(1, 5000), + cmd(16#4D, <<16#55, 16#00, 16#00>>), + cmd(16#A9, <<16#25, 16#00, 16#00>>), + cmd(16#F3, <<16#0A, 16#00, 16#00>>), + cmd(16#44, <<16#01, 16#0F>>), + cmd(16#45, <<16#F9, 16#00, 16#00, 16#00>>), + cmd(16#3C, <<16#01>>), + cmd(16#18, <<16#80>>), + cmd(16#4E, <<16#01>>), + cmd(16#4F, <<16#F9, 16#00>>), + wait_busy(1, 5000) + ]). + +heltec_lcmen2r13efc1_refresh() -> + program([ + cmd(16#13), + insert_plane(0), + cmd(16#12), + cmd(16#04), + wait_busy(1, 5000), + delay_ms(10), + cmd(16#12), + delay_ms(10), + wait_busy(1, 5000), + cmd(16#02), + wait_busy(1, 5000) + ]). +%% SPDX-SnippetEnd + +epd2in9_v2_init(BorderWaveform) -> + epd2in9_v2_init(BorderWaveform, 0). + +epd2in9_v2_init(BorderWaveform, RamXOffset) -> + RamXEnd = RamXOffset + 15, + Base = [ + reset(10, 10, 100), + wait_busy(0, 5000), + cmd(16#12), + wait_busy(0, 5000), + label_delay(0, 20), + cmd(16#01, <<16#27, 16#01, 16#00>>), + cmd(16#11, <<16#03>>), + cmd(16#44, <>), + cmd(16#45, <<16#00, 16#00, 16#27, 16#01>>), + cmd(16#4E, <>), + cmd(16#4F, <<16#00, 16#00>>), + cmd(16#21, <<16#00, 16#80>>) + ], + program(Base ++ border_waveform(BorderWaveform)). + +epd2in9_v2_refresh(LutSlot, UpdateMode) -> + program([ + insert_lut(LutSlot), + set_ram_cursor(), + cmd(16#24), + capture_frame(), + insert_plane(0), + delay_ms(2), + set_ram_cursor(), + cmd(16#26), + insert_prev_frame(0), + cmd(16#22, <>), + cmd(16#20), + wait_busy(0, 5000), + mark_prev_valid() + ]). + +epd2in9_v2_partial_refresh() -> + program([ + epd2in9_v2_partial_prelude(), + cmd(16#11, <<16#03>>), + cmd(16#44, <<16#00, 16#0F>>), + cmd(16#45, <<16#00, 16#00, 16#27, 16#01>>), + set_ram_cursor(), + cmd(16#26), + insert_prev_frame(0), + delay_ms(2), + set_ram_cursor(), + cmd(16#24), + capture_frame(), + insert_plane(0), + cmd(16#22, <<16#0F>>), + cmd(16#20), + wait_busy(0, 5000), + mark_prev_valid() + ]). + +epd2in9_v2_partial_prelude() -> + [ + reset(0, 1, 2), + insert_lut(?LUT_PARTIAL), + cmd(16#37, <<16#00, 16#00, 16#00, 16#00, 16#00, 16#40, 16#00, 16#00, 16#00, 16#00>>), + cmd(16#3C, <<16#80>>), + cmd(16#22, <<16#C0>>), + cmd(16#20), + wait_busy(0, 5000) + ]. + +epd2in9_v2_4gray_refresh() -> + program([ + insert_lut(?LUT_4GRAY), + set_ram_cursor(), + cmd(16#24), + insert_plane(0), + delay_ms(2), + set_ram_cursor(), + cmd(16#26), + insert_plane(1), + cmd(16#22, <<16#C7>>), + cmd(16#20), + wait_busy(0, 5000) + ]). + +border_waveform(none) -> + []; +border_waveform(Waveform) -> + [cmd(16#3C, <>)]. + +set_ram_cursor() -> + set_ram_cursor(0). + +set_ram_cursor(RamXOffset) -> + [cmd(16#4E, <>), cmd(16#4F, <<16#00, 16#00>>)]. + +%% SPDX-SnippetBegin +%% SPDX-SnippetCopyrightText: Waveshare team +%% SPDX-License-Identifier: MIT +epd2in9_v2_partial_lut() -> + << + 16#00, 16#40, 16#00, 16#00, 16#00, 16#00, 16#00, 16#00, 16#00, 16#00, 16#00, 16#00, + 16#80, 16#80, 16#00, 16#00, 16#00, 16#00, 16#00, 16#00, 16#00, 16#00, 16#00, 16#00, + 16#40, 16#40, 16#00, 16#00, 16#00, 16#00, 16#00, 16#00, 16#00, 16#00, 16#00, 16#00, + 16#00, 16#80, 16#00, 16#00, 16#00, 16#00, 16#00, 16#00, 16#00, 16#00, 16#00, 16#00, + 16#00, 16#00, 16#00, 16#00, 16#00, 16#00, 16#00, 16#00, 16#00, 16#00, 16#00, 16#00, + 16#0A, 16#00, 16#00, 16#00, 16#00, 16#00, 16#01, 16#01, 16#00, 16#00, 16#00, 16#00, + 16#00, 16#00, 16#01, 16#00, 16#00, 16#00, 16#00, 16#00, 16#00, 16#00, 16#00, 16#00, + 16#00, 16#00, 16#00, 16#00, 16#00, 16#00, 16#00, 16#00, 16#00, 16#00, 16#00, 16#00, + 16#00, 16#00, 16#00, 16#00, 16#00, 16#00, 16#00, 16#00, 16#00, 16#00, 16#00, 16#00, + 16#00, 16#00, 16#00, 16#00, 16#00, 16#00, 16#00, 16#00, 16#00, 16#00, 16#00, 16#00, + 16#00, 16#00, 16#00, 16#00, 16#00, 16#00, 16#00, 16#00, 16#00, 16#00, 16#00, 16#00, + 16#00, 16#00, 16#00, 16#00, 16#00, 16#00, 16#00, 16#00, 16#00, 16#00, 16#00, 16#00, + 16#22, 16#22, 16#22, 16#22, 16#22, 16#22, 16#00, 16#00, 16#00, 16#22, 16#17, 16#41, + 16#B0, 16#32, 16#36 + >>. + +epd2in9_v2_default_lut() -> + << + 16#80, 16#66, 16#00, 16#00, 16#00, 16#00, 16#00, 16#00, 16#40, 16#00, 16#00, 16#00, + 16#10, 16#66, 16#00, 16#00, 16#00, 16#00, 16#00, 16#00, 16#20, 16#00, 16#00, 16#00, + 16#80, 16#66, 16#00, 16#00, 16#00, 16#00, 16#00, 16#00, 16#40, 16#00, 16#00, 16#00, + 16#10, 16#66, 16#00, 16#00, 16#00, 16#00, 16#00, 16#00, 16#20, 16#00, 16#00, 16#00, + 16#00, 16#00, 16#00, 16#00, 16#00, 16#00, 16#00, 16#00, 16#00, 16#00, 16#00, 16#00, + 16#14, 16#08, 16#00, 16#00, 16#00, 16#00, 16#02, 16#0A, 16#0A, 16#00, 16#0A, 16#0A, + 16#00, 16#01, 16#00, 16#00, 16#00, 16#00, 16#00, 16#00, 16#00, 16#00, 16#00, 16#00, + 16#00, 16#00, 16#00, 16#00, 16#00, 16#00, 16#00, 16#00, 16#00, 16#00, 16#00, 16#00, + 16#00, 16#00, 16#00, 16#00, 16#00, 16#00, 16#00, 16#00, 16#00, 16#00, 16#00, 16#00, + 16#00, 16#00, 16#00, 16#00, 16#00, 16#00, 16#00, 16#00, 16#14, 16#08, 16#00, 16#01, + 16#00, 16#00, 16#01, 16#00, 16#00, 16#00, 16#00, 16#00, 16#00, 16#01, 16#00, 16#00, + 16#00, 16#00, 16#00, 16#00, 16#00, 16#00, 16#00, 16#00, 16#00, 16#00, 16#00, 16#00, + 16#44, 16#44, 16#44, 16#44, 16#44, 16#44, 16#00, 16#00, 16#00, 16#22, 16#17, 16#41, + 16#00, 16#32, 16#36 + >>. + +epd2in9_v2_4gray_lut() -> + << + 16#00, 16#60, 16#00, 16#00, 16#00, 16#00, 16#00, 16#00, 16#00, 16#00, 16#00, 16#00, + 16#20, 16#60, 16#10, 16#00, 16#00, 16#00, 16#00, 16#00, 16#00, 16#00, 16#00, 16#00, + 16#28, 16#60, 16#14, 16#00, 16#00, 16#00, 16#00, 16#00, 16#00, 16#00, 16#00, 16#00, + 16#2A, 16#60, 16#15, 16#00, 16#00, 16#00, 16#00, 16#00, 16#00, 16#00, 16#00, 16#00, + 16#00, 16#90, 16#00, 16#00, 16#00, 16#00, 16#00, 16#00, 16#00, 16#00, 16#00, 16#00, + 16#00, 16#02, 16#00, 16#05, 16#14, 16#00, 16#00, 16#1E, 16#1E, 16#00, 16#00, 16#00, + 16#00, 16#01, 16#00, 16#02, 16#00, 16#05, 16#14, 16#00, 16#00, 16#00, 16#00, 16#00, + 16#00, 16#00, 16#00, 16#00, 16#00, 16#00, 16#00, 16#00, 16#00, 16#00, 16#00, 16#00, + 16#00, 16#00, 16#00, 16#00, 16#00, 16#00, 16#00, 16#00, 16#00, 16#00, 16#00, 16#00, + 16#00, 16#00, 16#00, 16#00, 16#00, 16#00, 16#00, 16#00, 16#00, 16#00, 16#00, 16#00, + 16#00, 16#00, 16#00, 16#00, 16#00, 16#00, 16#00, 16#00, 16#00, 16#00, 16#00, 16#00, + 16#00, 16#00, 16#00, 16#00, 16#00, 16#00, 16#00, 16#00, 16#00, 16#00, 16#00, 16#00, + 16#24, 16#22, 16#22, 16#22, 16#23, 16#32, 16#00, 16#00, 16#00, 16#22, 16#17, 16#41, + 16#AE, 16#32, 16#28 + >>. + +epd2in9_v2_fast_lut() -> + << + 16#90, 16#00, 16#00, 16#00, 16#00, 16#00, 16#00, 16#00, 16#00, 16#00, 16#00, 16#00, + 16#60, 16#00, 16#00, 16#00, 16#00, 16#00, 16#00, 16#00, 16#00, 16#00, 16#00, 16#00, + 16#90, 16#00, 16#00, 16#00, 16#00, 16#00, 16#00, 16#00, 16#00, 16#00, 16#00, 16#00, + 16#60, 16#00, 16#00, 16#00, 16#00, 16#00, 16#00, 16#00, 16#00, 16#00, 16#00, 16#00, + 16#00, 16#00, 16#00, 16#00, 16#00, 16#00, 16#00, 16#00, 16#00, 16#00, 16#00, 16#00, + 16#19, 16#19, 16#00, 16#00, 16#00, 16#00, 16#00, 16#00, 16#00, 16#00, 16#00, 16#00, + 16#00, 16#00, 16#00, 16#00, 16#00, 16#00, 16#00, 16#00, 16#00, 16#00, 16#00, 16#00, + 16#00, 16#00, 16#00, 16#00, 16#00, 16#00, 16#00, 16#00, 16#00, 16#00, 16#00, 16#00, + 16#00, 16#00, 16#00, 16#00, 16#00, 16#00, 16#00, 16#00, 16#00, 16#00, 16#00, 16#00, + 16#00, 16#00, 16#00, 16#00, 16#00, 16#00, 16#00, 16#00, 16#00, 16#00, 16#00, 16#00, + 16#00, 16#00, 16#00, 16#00, 16#00, 16#00, 16#00, 16#00, 16#00, 16#00, 16#00, 16#00, + 16#00, 16#00, 16#00, 16#00, 16#00, 16#00, 16#00, 16#00, 16#00, 16#00, 16#00, 16#00, + 16#24, 16#42, 16#22, 16#22, 16#23, 16#32, 16#00, 16#00, 16#00, 16#22, 16#17, 16#41, + 16#AE, 16#32, 16#38 + >>. +%% SPDX-SnippetEnd diff --git a/atomgl_epaper/src/atomgl_epaper_program.erl b/atomgl_epaper/src/atomgl_epaper_program.erl new file mode 100644 index 0000000..45d8e68 --- /dev/null +++ b/atomgl_epaper/src/atomgl_epaper_program.erl @@ -0,0 +1,76 @@ +%% SPDX-License-Identifier: Apache-2.0 +%% SPDX-FileCopyrightText: 2026 Peter M + +-module(atomgl_epaper_program). + +-export([ + program/1, + cmd/1, + cmd/2, + wait_busy/2, + reset/3, + insert_plane/1, + insert_lut/1, + insert_prev_frame/1, + capture_frame/0, + mark_prev_valid/0, + label_delay/2, + delay_ms/1 +]). + +-define(DELAY, 16#80). +-define(META, 16#40). +-define(LEN_MASK, 16#3F). + +-define(OP_WAIT_BUSY, 16#01). +-define(OP_RESET, 16#02). +-define(OP_INSERT_PLANE, 16#03). +-define(OP_INSERT_LUT, 16#04). +-define(OP_INSERT_PREV_FRAME, 16#05). +-define(OP_CAPTURE_FRAME, 16#06). +-define(OP_MARK_PREV_VALID, 16#07). +-define(OP_LABEL, 16#10). + +program(Parts) -> + iolist_to_binary(Parts). + +cmd(Cmd) -> + cmd(Cmd, <<>>). + +cmd(Cmd, Data) when is_list(Data) -> + cmd(Cmd, list_to_binary(Data)); +cmd(Cmd, Data) when byte_size(Data) =< ?LEN_MASK -> + <>. + +wait_busy(Level, TimeoutMs) -> + meta(?OP_WAIT_BUSY, <>). + +reset(HighMs, LowMs, SettleMs) -> + meta(?OP_RESET, <>). + +insert_plane(PlaneId) -> + meta(?OP_INSERT_PLANE, <>). + +insert_lut(SlotId) -> + meta(?OP_INSERT_LUT, <>). + +insert_prev_frame(PlaneId) -> + meta(?OP_INSERT_PREV_FRAME, <>). + +capture_frame() -> + meta(?OP_CAPTURE_FRAME, <<>>). + +mark_prev_valid() -> + meta(?OP_MARK_PREV_VALID, <<>>). + +label_delay(LabelId, DelayMs) -> + meta_delay(?OP_LABEL, <>, DelayMs). + +delay_ms(DelayMs) -> + label_delay(0, DelayMs). + +meta(Opcode, Operands) when byte_size(Operands) =< ?LEN_MASK -> + <>. + +meta_delay(Opcode, Operands, DelayMs) when byte_size(Operands) =< ?LEN_MASK -> + <>. diff --git a/atomgl_epaper/test/atomgl_epaper_tests.erl b/atomgl_epaper/test/atomgl_epaper_tests.erl new file mode 100644 index 0000000..b94e981 --- /dev/null +++ b/atomgl_epaper/test/atomgl_epaper_tests.erl @@ -0,0 +1,77 @@ +%% SPDX-License-Identifier: Apache-2.0 +%% SPDX-FileCopyrightText: 2026 Peter M + +-module(atomgl_epaper_tests). + +-include_lib("eunit/include/eunit.hrl"). + +-import(atomgl_epaper, [panel/1, panel/2, validate_descriptor/1]). + +get_value(Key, List) -> + proplists:get_value(Key, List). + +orientation_geometry_test_() -> + [ + ?_test(begin + {ok, Desc} = panel("waveshare,epd2in9_V2"), + ?assertEqual(296, get_value(view_width, Desc)), + ?assertEqual(128, get_value(view_height, Desc)), + ?assertEqual(90, get_value(rotation, Desc)) + end), + ?_test(begin + {ok, Desc} = panel("waveshare,epd2in9_V2", #{orientation => landscape_left}), + ?assertEqual(296, get_value(view_width, Desc)), + ?assertEqual(128, get_value(view_height, Desc)), + ?assertEqual(90, get_value(rotation, Desc)) + end), + ?_test(begin + {ok, Desc} = panel("waveshare,epd2in9_V2", #{orientation => landscape_right}), + ?assertEqual(296, get_value(view_width, Desc)), + ?assertEqual(128, get_value(view_height, Desc)), + ?assertEqual(270, get_value(rotation, Desc)) + end), + ?_test(begin + {ok, Desc} = panel("waveshare,epd2in9_V2", #{orientation => portrait}), + ?assertEqual(128, get_value(view_width, Desc)), + ?assertEqual(296, get_value(view_height, Desc)), + ?assertEqual(0, get_value(rotation, Desc)) + end), + ?_test(begin + {ok, Desc} = panel("waveshare,epd2in9_V2", #{orientation => portrait_flipped}), + ?assertEqual(128, get_value(view_width, Desc)), + ?assertEqual(296, get_value(view_height, Desc)), + ?assertEqual(180, get_value(rotation, Desc)) + end) + ]. + +invalid_rotation_descriptor_test() -> + Desc = [ + {descriptor_version, 2}, + {controller, ssd16xx}, + {native_width, 128}, + {native_height, 296}, + {view_width, 128}, + {view_height, 296}, + {rotation, 45}, + {frame_layout, row_msb}, + {refresh_modes, [full]}, + {default_refresh, full}, + {programs, [{init, <<>>}, {full, <<>>}]} + ], + ?assertError({bad_rotation, 45}, validate_descriptor(Desc)). + +invalid_rotated_view_geometry_test() -> + Desc = [ + {descriptor_version, 2}, + {controller, ssd16xx}, + {native_width, 128}, + {native_height, 296}, + {view_width, 128}, + {view_height, 296}, + {rotation, 90}, + {frame_layout, row_msb}, + {refresh_modes, [full]}, + {default_refresh, full}, + {programs, [{init, <<>>}, {full, <<>>}]} + ], + ?assertError({badmatch, false}, validate_descriptor(Desc)). diff --git a/display_driver.c b/display_driver.c index dce82db..d9a3d5e 100644 --- a/display_driver.c +++ b/display_driver.c @@ -37,6 +37,12 @@ Context *oled_display_create_port(GlobalContext *global, term opts); Context *display_create_port(GlobalContext *global, term opts) { term compat_atom = globalcontext_make_atom(global, ATOM_STR("\xA", "compatible")); + term descriptor_atom = globalcontext_make_atom(global, ATOM_STR("\xA", "descriptor")); + + term descriptor_term = interop_proplist_get_value(opts, descriptor_atom); + if (descriptor_term != term_nil()) { + return epaper_display_create_port(global, opts); + } term compat_value_term = interop_proplist_get_value(opts, compat_atom); if (compat_value_term == term_nil()) { diff --git a/docs/display-drivers.md b/docs/display-drivers.md index f48c3cb..9442cf3 100644 --- a/docs/display-drivers.md +++ b/docs/display-drivers.md @@ -302,6 +302,126 @@ epaper_opts = [ ] ``` +### E-Paper Display API + +AtomGL provides a data-driven descriptor framework to drive monochrome and 4-gray E-Paper displays. Compatible panel configurations are represented as bytecode descriptors, meaning the same driver binary can adapt to different controllers, native geometry vs. user-facing orientation, byte/bit layouts, and waveform program payloads. + +#### Panel Constructors + +To obtain a panel descriptor, call `atomgl_epaper:panel/1` or `atomgl_epaper:panel/2`. + +```elixir +# Default panel configuration (landscape) +{:ok, descriptor} = :atomgl_epaper.panel("waveshare,epd2in9_V2") + +# Custom panel configuration with options +{:ok, descriptor_portrait} = :atomgl_epaper.panel("waveshare,epd2in9_V2", %{ + orientation: :portrait, + default_refresh: :partial +}) +``` + +Supported compatibility lookup strings for panels: +- **SSD1680 2.9"**: `"waveshare,epaper-2in9"`, `"waveshare,epd2in9_V2"`, `"waveshare,epd2in9_V2-fast"`, `"waveshare,epd2in9_V2-partial"`, `"waveshare,epd2in9_V2-4gray"`, `"dke,depg0290bns800f6"` +- **SSD1680 2.13"**: `"waveshare,epaper-2in13"`, `"waveshare,epd2in13_V4"`, `"waveshare,epd2in13_V4-fast"`, `"waveshare,epd2in13_V4-partial"` +- **JD79656 2.13"**: `"heltec,lcmen2r13efc1"`, `"heltec,icmen2r13efc1"`, `"heltec,ht-vme213"` + +##### Constructor Options + +When calling `panel/2`, you can customize: +- `orientation` - `:landscape`, `:landscape_left`, `:landscape_right`, `:portrait`, or `:portrait_flipped` (automatically maps native coordinates to view width/height and sets rotation). `:landscape` is a compatibility alias for `:landscape_left`. +- `default_refresh` - The refresh mode used when no frame option is specified. +- `refresh_modes` - List of allowed refresh modes (e.g., `[:full, :fast, :partial]`). +- `ghosting` - Map with ghosting settings: + - `max_fast_refreshes` - Maximum successive fast updates before a full refresh is forced. + - `reseed_on_timeout` - Reseed previous/current planes with full refresh after BUSY timeouts. +- `timing` - Map to adjust driver timeout parameters (`full_expected_ms`, `fast_expected_ms`, `poll_interval_ms`, `timeout_ms`). + +#### E-Paper Descriptor Schema + +A completed descriptor is a keyword list with the following shape: + +```elixir +[ + descriptor_version: 2, + name: "Waveshare epd2in9_V2 2.9\" e-paper SSD1680 v1", + controller: :ssd16xx, # :ssd16xx, :jd79656, :uc8175, or :acep7 + native_width: 128, + native_height: 296, + view_width: 296, + view_height: 128, + rotation: 90, + spi_clock_hz: 4000000, + busy_idle_level: 0, + use_gpio_pullups: false, + frame_layout: :row_msb, # :row_msb, :row_lsb, :column_msb, :column_lsb + polarity: :white_1, # :white_1 or :black_1 + refresh_modes: [:full, :fast, :partial, :4gray], + default_refresh: :full, + programs: %{ + init: <<...>>, # Init bytecode + full: <<...>>, # Full refresh waveform/cmd sequence + fast: <<...>>, # Fast refresh sequence (optional) + partial: <<...>>, # Partial refresh sequence (optional) + sleep: <<...>>, # Sleep command sequence (optional) + "4gray": <<...>> # 4-gray refresh sequence (optional) + }, + timing: %{ + full_expected_ms: 2000, + fast_expected_ms: 500, + poll_interval_ms: 50, + timeout_ms: 5000 + }, + ghosting: %{ + max_fast_refreshes: 10, + reseed_on_timeout: true + }, + lut_full: <<...>>, # Waveform LUT (optional) + lut_partial: <<...>>, # Waveform LUT (optional) + lut_4gray: <<...>>, # Waveform LUT (optional) + lut_fast: <<...>> # Waveform LUT (optional) +] +``` + +#### Ghosting Policy + +E-Paper controllers are highly susceptible to image burn-in and visual ghosting. AtomGL implements a conservative driver-level ghosting policy: +1. **Reseeding**: When the previous frame state is unknown or invalid, a partial/fast update is promoted to a full refresh to seed the controller's current/previous memory. +2. **Promotion Count**: After `max_fast_refreshes` successive fast/partial updates, the next update is automatically promoted to a full refresh to clean the screen. +3. **BUSY Timeout**: If the controller fails to finish in time (timing out on wait), the driver invalidates the previous frame and schedules a full refresh on the next update. + +#### Opening the Port + +Configure the SPI display port with the descriptor returned by the constructor: + +```elixir +epaper_opts = [ + spi_host: spi_host, + compatible: "waveshare,epd2in9_V2", + descriptor: descriptor, + cs: 22, + dc: 21, + reset: 18, + busy: 19 +] + +display = :erlang.open_port({:spawn, "display"}, epaper_opts) +``` + +#### Runtime Refresh Options + +When sending updates via the port, you can dynamically choose the refresh mode per frame (rather than being locked to one mode at port creation time): + +```elixir +# Standard/Default update +:port.call(display, {:update, items}, 5000) + +# Request a fast refresh mode for this frame +:port.call(display, {:update, items, [refresh: :fast]}, 5000) +``` + +If the requested refresh mode is not listed in the descriptor's `refresh_modes`, the driver defaults back to `default_refresh`. Only modes defined and present in the descriptor are permitted. + ## Custom Initialization Sequences Many displays require specific initialization sequences with carefully tuned values for voltages, diff --git a/epaper_color.c b/epaper_color.c index ddd27b7..e6f5667 100644 --- a/epaper_color.c +++ b/epaper_color.c @@ -20,6 +20,7 @@ #include "epaper_color.h" +#include #include #include #include @@ -44,27 +45,34 @@ const uint8_t epaper_gdep073e01_palette[7][3] = { { 0x12, 0x5F, 0x20 } }; +// App-facing SSD1680 4-gray palette. The plane writer maps these palette +// indices onto the SSD1680/Waveshare two-plane gray classes. +const uint8_t epaper_ssd1680_4gray_palette[4][3] = { + { 0xFF, 0xFF, 0xFF }, + { 0xAA, 0xAA, 0xAA }, + { 0x55, 0x55, 0x55 }, + { 0x00, 0x00, 0x00 } +}; + static inline float square(float p) { return p * p; } -uint8_t epaper_dither_acep7(int x, int y, uint8_t r, uint8_t g, uint8_t b, - const uint8_t palette[][3], int palette_size) +static int clamp_u8(int v) { - const uint8_t m[4][4] = { - { 0, 8, 2, 10 }, - { 12, 4, 14, 6 }, - { 3, 11, 1, 9 }, - { 15, 7, 13, 5 } - }; - - // following r parameters have been found using standard deviation - // that gives a decent result - int r1 = r + roundf(92.0 * ((float) m[x % 4][y % 4] * 0.0625 - 0.5)); - int g1 = g + roundf(85.0 * ((float) m[x % 4][y % 4] * 0.0625 - 0.5)); - int b1 = b + roundf(65.0 * ((float) m[x % 4][y % 4] * 0.0625 - 0.5)); + if (v < 0) { + return 0; + } + if (v > 255) { + return 255; + } + return v; +} +static uint8_t nearest_palette_index(int r, int g, int b, + const uint8_t palette[][3], int palette_size) +{ float min = INT_MAX; int min_index = 0; @@ -74,9 +82,9 @@ uint8_t epaper_dither_acep7(int x, int y, uint8_t r, uint8_t g, uint8_t b, int b2 = palette[i][2]; #ifdef NO_WEIGHTS - float d = square((r2 - r1)) + square((g2 - g1)) + square((b2 - b1)); + float d = square((r2 - r)) + square((g2 - g)) + square((b2 - b)); #else - float d = square((r2 - r1) * 0.30) + square((g2 - g1) * 0.59) + square((b2 - b1) * 0.11); + float d = square((r2 - r) * 0.30) + square((g2 - g) * 0.59) + square((b2 - b) * 0.11); #endif if (d < min) { @@ -87,3 +95,60 @@ uint8_t epaper_dither_acep7(int x, int y, uint8_t r, uint8_t g, uint8_t b, return min_index; } + +static bool exact_palette_index(uint8_t r, uint8_t g, uint8_t b, + const uint8_t palette[][3], int palette_size, uint8_t *out) +{ + for (int i = 0; i < palette_size; i++) { + if (palette[i][0] == r && palette[i][1] == g && palette[i][2] == b) { + *out = i; + return true; + } + } + return false; +} + +uint8_t epaper_dither_acep7(int x, int y, uint8_t r, uint8_t g, uint8_t b, + const uint8_t palette[][3], int palette_size) +{ + uint8_t exact_index; + if (exact_palette_index(r, g, b, palette, palette_size, &exact_index)) { + return exact_index; + } + + const uint8_t m[4][4] = { + { 0, 8, 2, 10 }, + { 12, 4, 14, 6 }, + { 3, 11, 1, 9 }, + { 15, 7, 13, 5 } + }; + + // following r parameters have been found using standard deviation + // that gives a decent result + int r1 = r + roundf(92.0 * ((float) m[x % 4][y % 4] * 0.0625 - 0.5)); + int g1 = g + roundf(85.0 * ((float) m[x % 4][y % 4] * 0.0625 - 0.5)); + int b1 = b + roundf(65.0 * ((float) m[x % 4][y % 4] * 0.0625 - 0.5)); + + return nearest_palette_index(r1, g1, b1, palette, palette_size); +} + +uint8_t epaper_dither_4gray(int x, int y, uint8_t r, uint8_t g, uint8_t b, + const uint8_t palette[][3], int palette_size) +{ + uint8_t exact_index; + if (exact_palette_index(r, g, b, palette, palette_size, &exact_index)) { + return exact_index; + } + + const uint8_t m[4][4] = { + { 0, 8, 2, 10 }, + { 12, 4, 14, 6 }, + { 3, 11, 1, 9 }, + { 15, 7, 13, 5 } + }; + + int yval = ((r << 1) + r + (g << 2) + b) >> 3; + yval = clamp_u8(yval + ((2 * m[x % 4][y % 4]) - 15) * 3); + + return 3 - ((yval * 3 + 127) / 255); +} diff --git a/epaper_color.h b/epaper_color.h index 57c24a9..a528bcd 100644 --- a/epaper_color.h +++ b/epaper_color.h @@ -25,8 +25,12 @@ extern const uint8_t epaper_acep_palette[7][3]; extern const uint8_t epaper_gdep073e01_palette[7][3]; +extern const uint8_t epaper_ssd1680_4gray_palette[4][3]; uint8_t epaper_dither_acep7(int x, int y, uint8_t r, uint8_t g, uint8_t b, const uint8_t palette[][3], int palette_size); +uint8_t epaper_dither_4gray(int x, int y, uint8_t r, uint8_t g, uint8_t b, + const uint8_t palette[][3], int palette_size); + #endif diff --git a/epaper_commands.c b/epaper_commands.c index 9a67aa2..8195cd7 100644 --- a/epaper_commands.c +++ b/epaper_commands.c @@ -34,6 +34,11 @@ static void wait_busy_high(int busy_gpio) } } +static TickType_t delay_ms_to_ticks(uint32_t delay_ms) +{ + return (delay_ms + portTICK_PERIOD_MS - 1) / portTICK_PERIOD_MS; +} + void epaper_execute_init_seq(struct SPIDCBus *bus, int busy_gpio, const uint8_t *seq, size_t seq_len, bool wait_busy_between_cmds) { @@ -47,7 +52,10 @@ void epaper_execute_init_seq(struct SPIDCBus *bus, int busy_gpio, seq += len; if (flags_len & EPAPER_INIT_SEQ_DELAY) { - vTaskDelay(*seq++ / portTICK_PERIOD_MS); + uint8_t delay_ms = *seq++; + if (delay_ms > 0) { + vTaskDelay(delay_ms_to_ticks(delay_ms)); + } } if (wait_busy_between_cmds) { @@ -129,14 +137,25 @@ const struct EPaperDesc epaper_desc_acep7c = { .name = "Waveshare 5.65\" ACeP 7-color", .native_width = 600, .native_height = 448, + .view_width = 600, + .view_height = 448, + .rotation = 0, .spi_clock_hz = 1000000, .palette = epaper_acep_palette, .palette_size = 7, + .controller = EPAPER_CONTROLLER_RAW, + .layout = { + .byte_order = EPAPER_BYTE_ORDER_ROW_MAJOR, + .bit_order = EPAPER_BIT_ORDER_MSB_LEFT, + .polarity = EPAPER_POLARITY_WHITE_IS_1 + }, + .command_target = EPAPER_COMMAND_TARGET_COLOR_PLANE, .init_seq = epaper_init_seq_acep7c, .init_seq_len = sizeof(epaper_init_seq_acep7c), .init_wait_busy_between_cmds = false, + .use_gpio_pullups = true, .frame_preamble_seq = epaper_preamble_acep7c, .frame_preamble_seq_len = sizeof(epaper_preamble_acep7c), @@ -144,6 +163,7 @@ const struct EPaperDesc epaper_desc_acep7c = { .refresh_has_data = false, .refresh_data_byte = 0x00, .post_power_off_busy_level = 0, + .busy_idle_level = 1, .periodic_refresh_interval = 5, }; @@ -152,14 +172,25 @@ const struct EPaperDesc epaper_desc_gdep073e01 = { .name = "Good Display GDEP073E01 7.3\" 7-color", .native_width = 800, .native_height = 480, + .view_width = 800, + .view_height = 480, + .rotation = 0, .spi_clock_hz = 4000000, .palette = epaper_gdep073e01_palette, .palette_size = 7, + .controller = EPAPER_CONTROLLER_RAW, + .layout = { + .byte_order = EPAPER_BYTE_ORDER_ROW_MAJOR, + .bit_order = EPAPER_BIT_ORDER_MSB_LEFT, + .polarity = EPAPER_POLARITY_WHITE_IS_1 + }, + .command_target = EPAPER_COMMAND_TARGET_COLOR_PLANE, .init_seq = epaper_init_seq_gdep073e01, .init_seq_len = sizeof(epaper_init_seq_gdep073e01), .init_wait_busy_between_cmds = true, + .use_gpio_pullups = true, .frame_preamble_seq = NULL, .frame_preamble_seq_len = 0, @@ -167,6 +198,7 @@ const struct EPaperDesc epaper_desc_gdep073e01 = { .refresh_has_data = true, .refresh_data_byte = 0x00, .post_power_off_busy_level = 1, + .busy_idle_level = 1, .periodic_refresh_interval = 0, }; diff --git a/epaper_commands.h b/epaper_commands.h index c90faf0..c06f5e7 100644 --- a/epaper_commands.h +++ b/epaper_commands.h @@ -25,6 +25,7 @@ #include #include +#include "epaper_program.h" #include "spi_dc_driver.h" // --- Init sequence byte-array format --- @@ -62,27 +63,82 @@ extern const size_t epaper_init_seq_gdep073e01_len; // --- Per-panel descriptor --- // +enum EPaperController +{ + EPAPER_CONTROLLER_ACEP7, + EPAPER_CONTROLLER_SSD16XX, + EPAPER_CONTROLLER_JD79656, + EPAPER_CONTROLLER_UC8175 +}; + +enum EPaperByteOrder +{ + EPAPER_BYTE_ORDER_ROW_MAJOR, + EPAPER_BYTE_ORDER_COLUMN_MAJOR +}; + +enum EPaperBitOrder +{ + EPAPER_BIT_ORDER_MSB_LEFT, + EPAPER_BIT_ORDER_LSB_LEFT +}; + +enum EPaperPolarity +{ + EPAPER_POLARITY_WHITE_IS_1, + EPAPER_POLARITY_BLACK_IS_1 +}; + +enum EPaperCommandTarget +{ + EPAPER_COMMAND_TARGET_CURRENT_PLANE, + EPAPER_COMMAND_TARGET_PREVIOUS_PLANE, + EPAPER_COMMAND_TARGET_COLOR_PLANE +}; + +enum EPaperRefreshMode +{ + EPAPER_REFRESH_FULL, + EPAPER_REFRESH_FAST, + EPAPER_REFRESH_PARTIAL, + EPAPER_REFRESH_4GRAY +}; + +struct EPaperFrameLayout +{ + enum EPaperByteOrder byte_order; + enum EPaperBitOrder bit_order; + enum EPaperPolarity polarity; +}; + // Captures every panel-specific knob so a single unified driver can -// drive multiple controllers by compatible-string dispatch. The -// struct carries no function pointers: the current variation across -// ACeP 5.65" and GDEP073E01 is entirely data. +// drive multiple controllers by compatible-string dispatch. struct EPaperDesc { const char *name; int native_width; int native_height; + int view_width; + int view_height; + int rotation; int spi_clock_hz; // Color palette (RGB triplets) and its entry count. const uint8_t (*palette)[3]; int palette_size; + enum EPaperController controller; + struct EPaperFrameLayout layout; + enum EPaperCommandTarget command_target; + // One-time init sequence (format documented above). const uint8_t *init_seq; size_t init_seq_len; bool init_wait_busy_between_cmds; + bool use_gpio_pullups; + // Optional per-frame preamble sent before DTM (0x10) on every // do_update and clear_screen. NULL when unused. Uses the same // byte-array format as init_seq and is executed without inter- @@ -97,10 +153,38 @@ struct EPaperDesc uint8_t refresh_data_byte; int post_power_off_busy_level; + // BUSY pin level when the controller is idle / ready to receive + // commands. Used after reset (in display_spi_init) before + // executing any init sequence. Most Waveshare/Good Display PSR- + // style controllers idle BUSY high (1); SSD1680 idles BUSY low + // (0). Get this wrong and the post-reset busy wait deadlocks. + int busy_idle_level; + // Periodic full-screen white-out. Zero = disabled. N > 0 means // "call clear_screen(7) once every N do_update() invocations" to // prevent ghosting on panels that need it (ACeP 5.65"). int periodic_refresh_interval; + + // Data-driven descriptor programs. + uint8_t descriptor_version; + struct EPaperProgram init; + struct EPaperProgram program_full; + struct EPaperProgram program_fast; + struct EPaperProgram program_partial; + struct EPaperProgram program_4gray; + struct EPaperProgram sleep; + struct EPaperProgram wake; + struct EPaperLut lut_slots[EPAPER_MAX_LUT_SLOTS]; + + // Timing & ghosting fields + int full_expected_ms; + int fast_expected_ms; + int poll_interval_ms; + int timeout_ms; + int max_fast_refreshes; + bool reseed_on_timeout; + + enum EPaperRefreshMode default_refresh; }; extern const struct EPaperDesc epaper_desc_acep7c; diff --git a/epaper_display_driver.c b/epaper_display_driver.c index 12f9152..8eb94a9 100644 --- a/epaper_display_driver.c +++ b/epaper_display_driver.c @@ -20,10 +20,15 @@ #include #include +#include #include +#include +#include #include #include +#include +#include #include #include @@ -43,8 +48,10 @@ #include "epaper_color.h" #include "epaper_commands.h" #include "epaper_draw.h" +#include "epaper_program.h" #include "epaper_screen.h" #include "image_helpers.h" +#include "mono_draw.h" #include "spi_dc_driver.h" #include "spi_display.h" @@ -53,6 +60,19 @@ static const char *TAG = "epaper_display_driver"; +#define SSD1680_LUT_PAYLOAD_LEN 153 +#define SSD1680_LUT_WITH_REGS_LEN 159 +#define EPAPER_TERM_PROGRAM_COUNT 7 + +struct EPaperState +{ + bool prev_valid; + int fast_refresh_count; + enum EPaperRefreshMode last_refresh_mode; + uint64_t last_refresh_ms; + bool needs_reseed; +}; + static void clear_screen(Context *ctx, int color); struct EpaperDriver @@ -63,12 +83,22 @@ struct EpaperDriver int reset_gpio; const struct EPaperDesc *desc; + struct EPaperDesc term_desc; + char *term_desc_name; + uint8_t *term_program_bytes[EPAPER_TERM_PROGRAM_COUNT]; + uint8_t *term_lut_bytes[EPAPER_MAX_LUT_SLOTS]; + struct EpaperScreen screen; + struct MonoScreen mono_screen; + + uint8_t *prev_frame; + size_t prev_frame_len; + + struct EPaperState state; Context *ctx; int count_to_refresh; - uint64_t last_refresh; struct DisplayTaskArgs display_args; }; @@ -84,6 +114,28 @@ static const struct { { "good-display/gdep073e01", &epaper_desc_gdep073e01 }, }; +static const struct EPaperDesc epaper_desc_term_default = { + .name = "Erlang e-paper descriptor", + .native_width = 128, + .native_height = 296, + .view_width = 128, + .view_height = 296, + .rotation = 0, + .spi_clock_hz = 4000000, + .palette = NULL, + .palette_size = 0, + .controller = EPAPER_CONTROLLER_SSD16XX, + .layout = { + .byte_order = EPAPER_BYTE_ORDER_ROW_MAJOR, + .bit_order = EPAPER_BIT_ORDER_MSB_LEFT, + .polarity = EPAPER_POLARITY_WHITE_IS_1 + }, + .command_target = EPAPER_COMMAND_TARGET_CURRENT_PLANE, + .use_gpio_pullups = false, + .busy_idle_level = 0, + .descriptor_version = 2, +}; + static const struct EPaperDesc *epaper_desc_for_compatible(const char *compat) { for (size_t i = 0; i < sizeof(epaper_compat_table) / sizeof(epaper_compat_table[0]); i++) { @@ -94,7 +146,17 @@ static const struct EPaperDesc *epaper_desc_for_compatible(const char *compat) return NULL; } +static bool epaper_desc_requires_program(const struct EPaperDesc *desc) +{ + return desc->controller != EPAPER_CONTROLLER_ACEP7; +} + static void display_init_using_list(struct EpaperDriver *driver, term init_list); +static bool epaper_parse_descriptor_override(struct EpaperDriver *driver, + term opts, Context *ctx); +static void epaper_free_init_failed_driver(struct EpaperDriver *driver, + bool spi_device_added); +static bool epaper_parse_refresh_mode(term val, Context *ctx, enum EPaperRefreshMode *out); static void display_reset(struct EpaperDriver *driver) { @@ -103,6 +165,18 @@ static void display_reset(struct EpaperDriver *driver) gpio_set_level(driver->reset_gpio, 1); } +static void epaper_delay_ms(uint32_t delay_ms) +{ + if (delay_ms == 0) { + return; + } + if (delay_ms < portTICK_PERIOD_MS) { + esp_rom_delay_us(delay_ms * 1000); + return; + } + vTaskDelay((delay_ms + portTICK_PERIOD_MS - 1) / portTICK_PERIOD_MS); +} + static void wait_busy_level(struct EpaperDriver *driver, int level) { while (gpio_get_level(driver->busy_gpio) != level) { @@ -110,6 +184,75 @@ static void wait_busy_level(struct EpaperDriver *driver, int level) } } +// Wait for BUSY pin to go to target level. Returns true on +// success, false on timeout. On timeout, invalidates the cached +// partial-refresh previous frame so the next partial update re-seeds +// via a full base refresh. +static bool wait_busy_timeout(struct EpaperDriver *driver, int level, + uint16_t timeout_ms) +{ + TickType_t start = xTaskGetTickCount(); + TickType_t timeout = pdMS_TO_TICKS(timeout_ms); + + while (gpio_get_level(driver->busy_gpio) != level) { + if ((xTaskGetTickCount() - start) > timeout) { + if (driver->state.prev_valid) { + ESP_LOGW(TAG, + "BUSY wait for level %d timed out for %s; invalidating partial prev frame.", + level, driver->desc->name); + } else { + ESP_LOGW(TAG, "BUSY wait for level %d timed out for %s.", + level, driver->desc->name); + } + driver->state.prev_valid = false; + if (driver->desc->reseed_on_timeout) { + driver->state.needs_reseed = true; + } + return false; + } + epaper_delay_ms(10); + } + return true; +} + +static bool wait_busy_low_timeout(struct EpaperDriver *driver) +{ + int timeout = (driver->desc->timeout_ms > 0) ? driver->desc->timeout_ms : 5000; + return wait_busy_timeout(driver, 0, timeout); +} + +static bool epaper_write_lut_bytes(struct EpaperDriver *driver, + const uint8_t *lut, size_t lut_len) +{ + if (lut == NULL || lut_len == 0) { + return false; + } + + if (lut_len < SSD1680_LUT_PAYLOAD_LEN) { + ESP_LOGE(TAG, "SSD1680 LUT is too short: %u bytes.", (unsigned) lut_len); + return false; + } + + spi_dc_write_cmd_data(&driver->bus, 0x32, lut, SSD1680_LUT_PAYLOAD_LEN); + wait_busy_low_timeout(driver); + + if (lut_len < SSD1680_LUT_WITH_REGS_LEN) { + return true; + } + + spi_dc_write_command(&driver->bus, 0x3F); // WRITE_EOPQ + spi_dc_write_data(&driver->bus, lut[153]); + spi_dc_write_command(&driver->bus, 0x03); // GATE_VOLTAGE + spi_dc_write_data(&driver->bus, lut[154]); + spi_dc_write_command(&driver->bus, 0x04); // SOURCE_VOLTAGE + spi_dc_write_data(&driver->bus, lut[155]); + spi_dc_write_data(&driver->bus, lut[156]); + spi_dc_write_data(&driver->bus, lut[157]); + spi_dc_write_command(&driver->bus, 0x2C); // WRITE_VCOM + spi_dc_write_data(&driver->bus, lut[158]); + return true; +} + static void wait_some_time(Context *ctx) { struct EpaperDriver *driver = EPAPER_DRIVER_FROM_CTX(ctx); @@ -117,11 +260,11 @@ static void wait_some_time(Context *ctx) struct timeval tv; gettimeofday(&tv, NULL); uint64_t now = tv.tv_sec * 1000LL + (tv.tv_usec / 1000LL); - uint64_t delta = now - driver->last_refresh; + uint64_t delta = now - driver->state.last_refresh_ms; if (delta < 2000) { // Wait 2 seconds before allowing a new refresh; undocumented but // empirically required or the panel drops updates. - vTaskDelay((2000 - delta) / portTICK_PERIOD_MS); + epaper_delay_ms(2000 - delta); } } @@ -131,7 +274,7 @@ static void update_last_refresh_ts(Context *ctx) struct timeval tv; gettimeofday(&tv, NULL); - driver->last_refresh = tv.tv_sec * 1000LL + (tv.tv_usec / 1000LL); + driver->state.last_refresh_ms = tv.tv_sec * 1000LL + (tv.tv_usec / 1000LL); } static void maybe_refresh(Context *ctx) @@ -177,10 +320,610 @@ static void send_post_frame_refresh(struct EpaperDriver *driver) wait_busy_level(driver, driver->desc->post_power_off_busy_level); } -static void do_update(Context *ctx, term display_list) +static int view_mono_line_bytes(struct EpaperDriver *driver) +{ + return (driver->mono_screen.w + 7) / 8; +} + +static int native_row_bytes(struct EpaperDriver *driver) +{ + return (driver->desc->native_width + 7) / 8; +} + +static int native_column_bytes(struct EpaperDriver *driver) +{ + return (driver->desc->native_height + 7) / 8; +} + +static int native_frame_chunk_bytes(struct EpaperDriver *driver) +{ + if (driver->desc->layout.byte_order == EPAPER_BYTE_ORDER_COLUMN_MAJOR) { + return native_column_bytes(driver); + } + return native_row_bytes(driver); +} + +static int native_frame_chunk_count(struct EpaperDriver *driver) +{ + if (driver->desc->layout.byte_order == EPAPER_BYTE_ORDER_COLUMN_MAJOR) { + return driver->desc->native_width; + } + return driver->desc->native_height; +} + +static size_t mono_frame_bytes(struct EpaperDriver *driver) +{ + if (driver->desc->layout.byte_order == EPAPER_BYTE_ORDER_COLUMN_MAJOR) { + return (size_t) driver->desc->native_width * native_column_bytes(driver); + } + return (size_t) native_row_bytes(driver) * driver->desc->native_height; +} + +static bool epaper_view_to_native(const struct EPaperDesc *desc, + int vx, int vy, int *nx, int *ny) +{ + switch (desc->rotation) { + case 0: + *nx = vx; + *ny = vy; + break; + case 90: + *nx = vy; + *ny = desc->native_height - 1 - vx; + break; + case 180: + *nx = desc->native_width - 1 - vx; + *ny = desc->native_height - 1 - vy; + break; + case 270: + *nx = desc->native_width - 1 - vy; + *ny = vx; + break; + default: + return false; + } + + return *nx >= 0 && *nx < desc->native_width + && *ny >= 0 && *ny < desc->native_height; +} + +static void render_mono_line_raw(struct EpaperDriver *driver, + uint8_t *buf, int ypos, BaseDisplayItem *items, int items_len) +{ + int plane_bytes = view_mono_line_bytes(driver); + memset(buf, 0xFF, plane_bytes); + + BaseDisplayItem *row = display_items_row(items, items_len, ypos); + int xpos = 0; + while (xpos < driver->mono_screen.w) { + int drawn_pixels = mono_draw_x(&driver->mono_screen, buf, xpos, ypos, row); + xpos += drawn_pixels; + } +} + +static bool ensure_prev_frame(struct EpaperDriver *driver) +{ + size_t frame_len = mono_frame_bytes(driver); + if (driver->prev_frame != NULL + && driver->prev_frame_len == frame_len) { + return true; + } + + free(driver->prev_frame); + driver->prev_frame = heap_caps_malloc(frame_len, MALLOC_CAP_DMA); + driver->prev_frame_len = frame_len; + driver->state.prev_valid = false; + + if (UNLIKELY(!driver->prev_frame)) { + driver->prev_frame_len = 0; + ESP_LOGE(TAG, "Failed to allocate e-paper previous-frame buffer."); + return false; + } + + return true; +} + +static void write_mono_frame(struct EpaperDriver *driver, + const uint8_t *frame) +{ + bool transaction_in_progress = false; + int chunk_bytes = native_frame_chunk_bytes(driver); + int chunk_count = native_frame_chunk_count(driver); + + spi_device_acquire_bus(driver->bus.spi_disp.handle, portMAX_DELAY); + + for (int chunk = 0; chunk < chunk_count; chunk++) { + if (transaction_in_progress) { + spi_transaction_t *trans = NULL; + spi_device_get_trans_result(driver->bus.spi_disp.handle, &trans, portMAX_DELAY); + } + + spi_display_dma_write(&driver->bus.spi_disp, chunk_bytes, + frame + ((size_t) chunk * chunk_bytes)); + transaction_in_progress = true; + } + + if (transaction_in_progress) { + spi_transaction_t *trans = NULL; + spi_device_get_trans_result(driver->bus.spi_disp.handle, &trans, portMAX_DELAY); + } + + spi_device_release_bus(driver->bus.spi_disp.handle); +} + +static void set_native_frame_bit(struct EpaperDriver *driver, uint8_t *frame, + int nx, int ny, bool bit_value) +{ + size_t byte_index; + int bit_pos; + + if (driver->desc->layout.byte_order == EPAPER_BYTE_ORDER_COLUMN_MAJOR) { + byte_index = (size_t) nx * native_column_bytes(driver) + (ny / 8); + bit_pos = ny % 8; + } else { + byte_index = (size_t) ny * native_row_bytes(driver) + (nx / 8); + bit_pos = nx % 8; + } + + if (driver->desc->layout.bit_order == EPAPER_BIT_ORDER_MSB_LEFT) { + bit_pos = 7 - bit_pos; + } + + uint8_t mask = (uint8_t) (1U << bit_pos); + if (bit_value) { + frame[byte_index] |= mask; + } else { + frame[byte_index] &= (uint8_t) ~mask; + } +} + +static void set_native_mono_pixel(struct EpaperDriver *driver, uint8_t *frame, + int nx, int ny, bool white) +{ + bool bit_value = white; + if (driver->desc->layout.polarity == EPAPER_POLARITY_BLACK_IS_1) { + bit_value = !white; + } + set_native_frame_bit(driver, frame, nx, ny, bit_value); +} + +static bool build_native_mono_frame(struct EpaperDriver *driver, uint8_t *frame, + uint8_t *line_buf, BaseDisplayItem *items, int items_len, bool draw_bw_plane) +{ + size_t frame_len = mono_frame_bytes(driver); + memset(frame, driver->desc->layout.polarity == EPAPER_POLARITY_WHITE_IS_1 ? 0xFF : 0x00, + frame_len); + + for (int vy = 0; vy < driver->mono_screen.h; vy++) { + if (draw_bw_plane) { + render_mono_line_raw(driver, line_buf, vy, items, items_len); + } else { + memset(line_buf, 0xFF, view_mono_line_bytes(driver)); + } + + for (int vx = 0; vx < driver->mono_screen.w; vx++) { + bool white = ((line_buf[vx / 8] >> (vx % 8)) & 0x01) != 0; + int nx; + int ny; + if (!epaper_view_to_native(driver->desc, vx, vy, &nx, &ny)) { + return false; + } + set_native_mono_pixel(driver, frame, nx, ny, white); + } + } + + return true; +} + +static bool write_mono_plane(struct EpaperDriver *driver, uint8_t *frame_buf, + uint8_t *line_buf, + BaseDisplayItem *items, int items_len, bool draw_bw_plane, + uint8_t *capture_frame) +{ + if (!build_native_mono_frame(driver, frame_buf, line_buf, + items, items_len, draw_bw_plane)) { + ESP_LOGE(TAG, "Failed to transform logical e-paper frame into native RAM."); + return false; + } + + if (capture_frame != NULL) { + memcpy(capture_frame, frame_buf, mono_frame_bytes(driver)); + } + + write_mono_frame(driver, frame_buf); + return true; +} + +static uint8_t get_epaper_packed_pixel(uint8_t *line_buf, int xpos) +{ + uint8_t packed = line_buf[xpos / 2]; + if ((xpos & 1) == 0) { + return packed >> 4; + } + return packed & 0x0F; +} + +static bool build_native_4gray_plane(struct EpaperDriver *driver, + uint8_t *gray_buf, uint8_t *plane_buf, BaseDisplayItem *items, + int items_len, int plane_bit) +{ + int gray_bytes = (driver->mono_screen.w + 1) / 2; + memset(plane_buf, 0x00, mono_frame_bytes(driver)); + + for (int vy = 0; vy < driver->mono_screen.h; vy++) { + memset(gray_buf, 0x00, gray_bytes); + BaseDisplayItem *row = display_items_row(items, items_len, vy); + int xpos = 0; + while (xpos < driver->mono_screen.w) { + int drawn_pixels = epaper_draw_4gray_x(&driver->screen, gray_buf, xpos, vy, row); + xpos += drawn_pixels; + } + + for (xpos = 0; xpos < driver->mono_screen.w; xpos++) { + uint8_t gray = get_epaper_packed_pixel(gray_buf, xpos); + if (gray & (1 << plane_bit)) { + // Palette index bits select the SSD1680 two-plane gray class: + // 0=white, 1=light gray, 2=dark gray, 3=black. + int nx; + int ny; + if (!epaper_view_to_native(driver->desc, xpos, vy, &nx, &ny)) { + return false; + } + set_native_frame_bit(driver, plane_buf, nx, ny, true); + } + } + } + + return true; +} + +static bool write_4gray_plane(struct EpaperDriver *driver, + uint8_t *gray_buf, uint8_t *plane_buf, BaseDisplayItem *items, + int items_len, int plane_bit) +{ + if (!build_native_4gray_plane(driver, gray_buf, plane_buf, + items, items_len, plane_bit)) { + ESP_LOGE(TAG, "Failed to transform logical 4-gray e-paper frame into native RAM."); + return false; + } + + write_mono_frame(driver, plane_buf); + return true; +} + +struct EPaperProgramRun +{ + uint8_t *plane_buf; + uint8_t *line_buf; + uint8_t *gray_buf; + BaseDisplayItem *items; + int items_len; + bool capture_next_plane; + bool is_4gray; +}; + +struct EPaperProgramDriverContext +{ + struct EpaperDriver *driver; + struct EPaperProgramRun *run; +}; + +static void epaper_program_command(void *ctx, uint8_t command, + const uint8_t *data, size_t len) +{ + struct EPaperProgramDriverContext *program_ctx = ctx; + spi_dc_write_cmd_data(&program_ctx->driver->bus, command, data, len); +} + +static void epaper_program_delay_ms(void *ctx, uint8_t delay_ms) +{ + (void) ctx; + epaper_delay_ms(delay_ms); +} + +static void epaper_program_reset(void *ctx, uint8_t high_ms, + uint8_t low_ms, uint8_t settle_ms) +{ + struct EPaperProgramDriverContext *program_ctx = ctx; + struct EpaperDriver *driver = program_ctx->driver; + gpio_set_level(driver->reset_gpio, 1); + epaper_delay_ms(high_ms); + gpio_set_level(driver->reset_gpio, 0); + epaper_delay_ms(low_ms); + gpio_set_level(driver->reset_gpio, 1); + epaper_delay_ms(settle_ms); +} + +static bool epaper_program_wait_busy(void *ctx, uint8_t level, + uint16_t timeout_ms) +{ + struct EPaperProgramDriverContext *program_ctx = ctx; + return wait_busy_timeout(program_ctx->driver, level, timeout_ms); +} + +static bool epaper_program_insert_plane(void *ctx, uint8_t plane_id) +{ + struct EPaperProgramDriverContext *program_ctx = ctx; + struct EpaperDriver *driver = program_ctx->driver; + struct EPaperProgramRun *run = program_ctx->run; + + if (run == NULL || run->plane_buf == NULL) { + ESP_LOGE(TAG, "INSERT_PLANE used without render state."); + return false; + } + + uint8_t *capture_frame = NULL; + if (run->capture_next_plane) { + if (!ensure_prev_frame(driver)) { + return false; + } + capture_frame = driver->prev_frame; + run->capture_next_plane = false; + } + + switch (plane_id) { + case 0: + if (driver->desc->controller == EPAPER_CONTROLLER_SSD16XX && run->is_4gray) { + if (run->gray_buf == NULL) { + return false; + } + return write_4gray_plane(driver, run->gray_buf, run->plane_buf, + run->items, run->items_len, 0); + } + if (run->line_buf == NULL) { + return false; + } + return write_mono_plane(driver, run->plane_buf, run->line_buf, + run->items, run->items_len, true, capture_frame); + case 1: + if (driver->desc->controller == EPAPER_CONTROLLER_SSD16XX && run->is_4gray) { + if (run->gray_buf == NULL) { + return false; + } + return write_4gray_plane(driver, run->gray_buf, run->plane_buf, + run->items, run->items_len, 1); + } + if (run->line_buf == NULL) { + return false; + } + return write_mono_plane(driver, run->plane_buf, run->line_buf, + run->items, run->items_len, false, capture_frame); + default: + ESP_LOGE(TAG, "Unsupported INSERT_PLANE id %d.", plane_id); + return false; + } +} + +static bool epaper_program_insert_lut(void *ctx, uint8_t slot_id) +{ + if (slot_id >= EPAPER_MAX_LUT_SLOTS) { + return false; + } + struct EPaperProgramDriverContext *program_ctx = ctx; + struct EpaperDriver *driver = program_ctx->driver; + if (driver->desc->lut_slots[slot_id].bytes == NULL + || driver->desc->lut_slots[slot_id].len == 0) { + ESP_LOGE(TAG, "INSERT_LUT references missing LUT slot %d.", slot_id); + return false; + } + return epaper_write_lut_bytes(driver, + driver->desc->lut_slots[slot_id].bytes, + driver->desc->lut_slots[slot_id].len); +} + +static bool epaper_program_insert_prev_frame(void *ctx, uint8_t plane_id) +{ + (void) plane_id; + struct EPaperProgramDriverContext *program_ctx = ctx; + struct EpaperDriver *driver = program_ctx->driver; + if (!ensure_prev_frame(driver)) { + return false; + } + write_mono_frame(driver, driver->prev_frame); + return true; +} + +static bool epaper_program_capture_frame(void *ctx) +{ + struct EPaperProgramDriverContext *program_ctx = ctx; + if (program_ctx->run == NULL) { + return false; + } + program_ctx->run->capture_next_plane = true; + return true; +} + +static bool epaper_program_mark_prev_valid(void *ctx) +{ + struct EPaperProgramDriverContext *program_ctx = ctx; + program_ctx->driver->state.prev_valid = true; + return true; +} + +static bool epaper_run_driver_program(struct EpaperDriver *driver, + const struct EPaperProgram *program, struct EPaperProgramRun *run) +{ + struct EPaperProgramDriverContext program_ctx = { + .driver = driver, + .run = run + }; + const struct EPaperProgramOps ops = { + .ctx = &program_ctx, + .command = epaper_program_command, + .delay_ms = epaper_program_delay_ms, + .reset = epaper_program_reset, + .wait_busy = epaper_program_wait_busy, + .insert_plane = epaper_program_insert_plane, + .insert_lut = epaper_program_insert_lut, + .insert_prev_frame = epaper_program_insert_prev_frame, + .capture_frame = epaper_program_capture_frame, + .mark_prev_valid = epaper_program_mark_prev_valid + }; + return epaper_run_program(program, &ops); +} + +static const struct EPaperProgram *epaper_resolve_refresh_program(struct EpaperDriver *driver, + enum EPaperRefreshMode requested_mode, enum EPaperRefreshMode *resolved_mode) +{ + enum EPaperRefreshMode mode = requested_mode; + + // Promote differential refreshes to full when the previous-frame state is not usable. + bool differential_mode = mode == EPAPER_REFRESH_FAST || mode == EPAPER_REFRESH_PARTIAL; + if (differential_mode) { + if (!driver->state.prev_valid || driver->state.needs_reseed) { + ESP_LOGI(TAG, "Promoting refresh to FULL: prev_valid=%d, needs_reseed=%d", + driver->state.prev_valid, driver->state.needs_reseed); + mode = EPAPER_REFRESH_FULL; + } else if (driver->state.last_refresh_mode != EPAPER_REFRESH_FULL + && driver->state.last_refresh_mode != mode) { + ESP_LOGI(TAG, "Promoting refresh to FULL: mode transition from %d to %d", + driver->state.last_refresh_mode, mode); + mode = EPAPER_REFRESH_FULL; + } else if (driver->desc->max_fast_refreshes > 0 + && driver->state.fast_refresh_count >= driver->desc->max_fast_refreshes) { + ESP_LOGI(TAG, "Promoting refresh to FULL: fast refresh count %d reached limit %d", + driver->state.fast_refresh_count, driver->desc->max_fast_refreshes); + mode = EPAPER_REFRESH_FULL; + } + } + + ESP_LOGI(TAG, "Resolved refresh program: requested=%d, last=%d, resolved=%d", + requested_mode, driver->state.last_refresh_mode, mode); + + *resolved_mode = mode; + + switch (mode) { + case EPAPER_REFRESH_FULL: + return &driver->desc->program_full; + case EPAPER_REFRESH_FAST: + return &driver->desc->program_fast; + case EPAPER_REFRESH_PARTIAL: + return &driver->desc->program_partial; + case EPAPER_REFRESH_4GRAY: + return &driver->desc->program_4gray; + default: + return &driver->desc->program_full; + } +} + +static void do_update_descriptor_mono(struct EpaperDriver *driver, + BaseDisplayItem *items, int items_len, enum EPaperRefreshMode requested_mode) +{ + enum EPaperRefreshMode resolved_mode; + const struct EPaperProgram *program = epaper_resolve_refresh_program(driver, requested_mode, &resolved_mode); + + if (program->bytes == NULL) { + ESP_LOGE(TAG, "E-paper descriptor for %s has no refresh program for mode %d.", + driver->desc->name, resolved_mode); + return; + } + + const size_t frame_len = mono_frame_bytes(driver); + const int line_bytes = view_mono_line_bytes(driver); + uint8_t *frame_buf = heap_caps_malloc(frame_len, MALLOC_CAP_DMA); + uint8_t *line_buf = heap_caps_malloc(line_bytes, MALLOC_CAP_DMA); + if (UNLIKELY(!frame_buf || !line_buf)) { + fprintf(stderr, "do_update: failed to alloc e-paper frame buffers\n"); + free(frame_buf); + free(line_buf); + return; + } + + struct EPaperProgramRun run = { + .plane_buf = frame_buf, + .line_buf = line_buf, + .gray_buf = NULL, + .items = items, + .items_len = items_len, + .capture_next_plane = false, + .is_4gray = false + }; + + if (!epaper_run_driver_program(driver, program, &run)) { + ESP_LOGW(TAG, "E-paper refresh program failed for %s.", driver->desc->name); + } else { + driver->state.last_refresh_mode = resolved_mode; + if (resolved_mode == EPAPER_REFRESH_FULL || resolved_mode == EPAPER_REFRESH_4GRAY) { + driver->state.fast_refresh_count = 0; + driver->state.needs_reseed = false; + } else { + driver->state.fast_refresh_count++; + } + } + + free(frame_buf); + free(line_buf); +} + +static void do_update_descriptor_4gray(struct EpaperDriver *driver, + BaseDisplayItem *items, int items_len, enum EPaperRefreshMode requested_mode) +{ + enum EPaperRefreshMode resolved_mode; + const struct EPaperProgram *program = epaper_resolve_refresh_program(driver, requested_mode, &resolved_mode); + + if (program->bytes == NULL) { + ESP_LOGE(TAG, "E-paper descriptor for %s has no 4-gray refresh program.", + driver->desc->name); + return; + } + + const int gray_bytes = (driver->mono_screen.w + 1) / 2; + const size_t frame_len = mono_frame_bytes(driver); + const int line_bytes = view_mono_line_bytes(driver); + bool is_4gray = (resolved_mode == EPAPER_REFRESH_4GRAY); + uint8_t *gray_buf = NULL; + if (is_4gray) { + gray_buf = heap_caps_malloc(gray_bytes, MALLOC_CAP_DMA); + if (UNLIKELY(!gray_buf)) { + fprintf(stderr, "do_update: failed to alloc 4-gray buffer\n"); + return; + } + } + uint8_t *plane_buf = heap_caps_malloc(frame_len, MALLOC_CAP_DMA); + uint8_t *line_buf = heap_caps_malloc(line_bytes, MALLOC_CAP_DMA); + if (UNLIKELY(!plane_buf || !line_buf)) { + fprintf(stderr, "do_update: failed to alloc plane buffers\n"); + free(gray_buf); + free(plane_buf); + free(line_buf); + return; + } + + struct EPaperProgramRun run = { + .plane_buf = plane_buf, + .line_buf = line_buf, + .gray_buf = gray_buf, + .items = items, + .items_len = items_len, + .capture_next_plane = false, + .is_4gray = is_4gray + }; + + if (!epaper_run_driver_program(driver, program, &run)) { + ESP_LOGW(TAG, "E-paper 4-gray refresh program failed for %s.", + driver->desc->name); + } else { + driver->state.last_refresh_mode = resolved_mode; + if (resolved_mode == EPAPER_REFRESH_FULL || resolved_mode == EPAPER_REFRESH_4GRAY) { + driver->state.fast_refresh_count = 0; + driver->state.needs_reseed = false; + } else { + driver->state.fast_refresh_count++; + } + } + + free(gray_buf); + free(plane_buf); + free(line_buf); +} + +static void do_update(Context *ctx, term display_list, term update_opts) { - maybe_refresh(ctx); - wait_some_time(ctx); + struct EpaperDriver *driver = EPAPER_DRIVER_FROM_CTX(ctx); + if (driver->desc->controller == EPAPER_CONTROLLER_ACEP7) { + maybe_refresh(ctx); + wait_some_time(ctx); + } BaseDisplayItem *items; size_t len; @@ -188,15 +931,39 @@ static void do_update(Context *ctx, term display_list) return; } - struct EpaperDriver *driver = EPAPER_DRIVER_FROM_CTX(ctx); - int screen_width = driver->screen.w; - int screen_height = driver->screen.h; + // Resolve update refresh mode + enum EPaperRefreshMode refresh_mode = driver->desc->default_refresh; + if (update_opts != term_nil()) { + term mode_term = interop_kv_get_value_default(update_opts, ATOM_STR("\x7", "refresh"), term_nil(), ctx->global); + if (mode_term != term_nil()) { + enum EPaperRefreshMode parsed_mode; + if (epaper_parse_refresh_mode(mode_term, ctx, &parsed_mode)) { + refresh_mode = parsed_mode; + } else { + ESP_LOGE(TAG, "Invalid refresh mode option specified in update."); + } + } + } + + if (driver->desc->controller != EPAPER_CONTROLLER_ACEP7) { + if (driver->desc->palette_size == 4) { + do_update_descriptor_4gray(driver, items, len, refresh_mode); + } else { + do_update_descriptor_mono(driver, items, len, refresh_mode); + } + display_items_delete(items, len); + update_last_refresh_ts(ctx); + return; + } send_frame_preamble(driver); // DTM — data transfer to panel memory. spi_dc_write_command(&driver->bus, 0x10); + int screen_width = driver->screen.w; + int screen_height = driver->screen.h; + uint8_t *buf = heap_caps_malloc(screen_width / 2, MALLOC_CAP_DMA); if (UNLIKELY(!buf)) { fprintf(stderr, "do_update: failed to alloc buf\n"); @@ -254,13 +1021,27 @@ static void process_message(Message *message, Context *ctx) if (UNLIKELY(!term_is_tuple(req) || term_get_tuple_arity(req) < 1)) { AVM_ABORT(); } + int req_arity = term_get_tuple_arity(req); term cmd = term_get_tuple_element(req, 0); if (cmd == context_make_atom(ctx, "\x6" "update")) { - term display_list = term_get_tuple_element(req, 1); - do_update(ctx, display_list); + if (req_arity < 2) { + ESP_LOGE(TAG, "Invalid update request: expected {update, Items} or {update, Items, Opts}."); + } else { + term display_list = term_get_tuple_element(req, 1); + term update_opts = term_nil(); + if (req_arity >= 3) { + update_opts = term_get_tuple_element(req, 2); + } + do_update(ctx, display_list, update_opts); + } + + // Reply already sent at enqueue time by + // try_pre_ack_render_cmd in display_task.c. Sending another + // reply here would leak a stray `{Ref, ok}' into the caller's + // mailbox after port:call has already returned. return; } else if (cmd == globalcontext_make_atom(ctx->global, "\xA" "load_image")) { @@ -329,15 +1110,465 @@ static void clear_screen(Context *ctx, int color) send_post_frame_refresh(driver); } +static bool epaper_term_bool(term value, bool *out) +{ + if (value == TRUE_ATOM) { + *out = true; + return true; + } + if (value == FALSE_ATOM) { + *out = false; + return true; + } + return false; +} + +static bool epaper_parse_int_field(term container, Context *ctx, + AtomString key, int *out) +{ + term value = interop_kv_get_value_default( + container, key, term_nil(), ctx->global); + if (value == term_nil()) { + return true; + } + if (term_is_integer(value)) { + *out = term_to_int(value); + return true; + } + return false; +} + +static bool epaper_parse_bool_field(term container, Context *ctx, + AtomString key, bool *out) +{ + term value = interop_kv_get_value_default( + container, key, term_nil(), ctx->global); + if (value == term_nil()) { + return true; + } + bool parsed; + if (epaper_term_bool(value, &parsed)) { + *out = parsed; + return true; + } + return false; +} + +static bool epaper_copy_binary_field(term container, Context *ctx, + AtomString key, uint8_t **owned_bytes, const uint8_t **bytes, size_t *len) +{ + term value = interop_kv_get_value_default( + container, key, term_nil(), ctx->global); + if (value == term_nil()) { + *owned_bytes = NULL; + *bytes = NULL; + *len = 0; + return true; + } + + if (!term_is_binary(value)) { + return false; + } + + size_t value_len = term_binary_size(value); + uint8_t *copy = malloc(value_len == 0 ? 1 : value_len); + if (copy == NULL) { + return false; + } + + memcpy(copy, term_binary_data(value), value_len); + *owned_bytes = copy; + *bytes = copy; + *len = value_len; + return true; +} + +static void epaper_free_descriptor_override(struct EpaperDriver *driver) +{ + free(driver->term_desc_name); + driver->term_desc_name = NULL; + + for (int i = 0; i < EPAPER_TERM_PROGRAM_COUNT; i++) { + free(driver->term_program_bytes[i]); + driver->term_program_bytes[i] = NULL; + } + + for (int i = 0; i < EPAPER_MAX_LUT_SLOTS; i++) { + free(driver->term_lut_bytes[i]); + driver->term_lut_bytes[i] = NULL; + } +} + +static void epaper_free_init_failed_driver(struct EpaperDriver *driver, + bool spi_device_added) +{ + if (driver == NULL) { + return; + } + + if (driver->ctx != NULL && driver->ctx->platform_data == &driver->display_args) { + driver->ctx->platform_data = NULL; + } + + if (driver->display_args.messages_queue != NULL) { + vQueueDelete(driver->display_args.messages_queue); + driver->display_args.messages_queue = NULL; + } + + if (spi_device_added && driver->bus.spi_disp.handle != NULL) { + spi_bus_remove_device(driver->bus.spi_disp.handle); + driver->bus.spi_disp.handle = NULL; + } + + free(driver->prev_frame); + driver->prev_frame = NULL; + epaper_free_descriptor_override(driver); + free(driver); +} + +static bool epaper_parse_controller(term val, Context *ctx, enum EPaperController *out) +{ + if (val == context_make_atom(ctx, ATOM_STR("\x7", "ssd16xx"))) { + *out = EPAPER_CONTROLLER_SSD16XX; + return true; + } + if (val == context_make_atom(ctx, ATOM_STR("\x7", "jd79656"))) { + *out = EPAPER_CONTROLLER_JD79656; + return true; + } + if (val == context_make_atom(ctx, ATOM_STR("\x6", "uc8175"))) { + *out = EPAPER_CONTROLLER_UC8175; + return true; + } + if (val == context_make_atom(ctx, ATOM_STR("\x5", "acep7"))) { + *out = EPAPER_CONTROLLER_ACEP7; + return true; + } + return false; +} + +static bool epaper_parse_layout(term val, Context *ctx, struct EPaperFrameLayout *out) +{ + if (val == context_make_atom(ctx, ATOM_STR("\x7", "row_msb"))) { + out->byte_order = EPAPER_BYTE_ORDER_ROW_MAJOR; + out->bit_order = EPAPER_BIT_ORDER_MSB_LEFT; + return true; + } + if (val == context_make_atom(ctx, ATOM_STR("\x7", "row_lsb"))) { + out->byte_order = EPAPER_BYTE_ORDER_ROW_MAJOR; + out->bit_order = EPAPER_BIT_ORDER_LSB_LEFT; + return true; + } + if (val == context_make_atom(ctx, ATOM_STR("\xA", "column_msb"))) { + out->byte_order = EPAPER_BYTE_ORDER_COLUMN_MAJOR; + out->bit_order = EPAPER_BIT_ORDER_MSB_LEFT; + return true; + } + if (val == context_make_atom(ctx, ATOM_STR("\xA", "column_lsb"))) { + out->byte_order = EPAPER_BYTE_ORDER_COLUMN_MAJOR; + out->bit_order = EPAPER_BIT_ORDER_LSB_LEFT; + return true; + } + return false; +} + +static bool epaper_parse_polarity(term val, Context *ctx, enum EPaperPolarity *out) +{ + if (val == context_make_atom(ctx, ATOM_STR("\x7", "white_1"))) { + *out = EPAPER_POLARITY_WHITE_IS_1; + return true; + } + if (val == context_make_atom(ctx, ATOM_STR("\x7", "black_1"))) { + *out = EPAPER_POLARITY_BLACK_IS_1; + return true; + } + return false; +} + +static bool epaper_parse_refresh_mode(term val, Context *ctx, enum EPaperRefreshMode *out) +{ + if (val == context_make_atom(ctx, ATOM_STR("\x4", "full"))) { + *out = EPAPER_REFRESH_FULL; + return true; + } + if (val == context_make_atom(ctx, ATOM_STR("\x4", "fast"))) { + *out = EPAPER_REFRESH_FAST; + return true; + } + if (val == context_make_atom(ctx, ATOM_STR("\x7", "partial"))) { + *out = EPAPER_REFRESH_PARTIAL; + return true; + } + if (val == context_make_atom(ctx, ATOM_STR("\x5", "4gray"))) { + *out = EPAPER_REFRESH_4GRAY; + return true; + } + return false; +} + +static bool epaper_validate_descriptor_geometry(const struct EPaperDesc *desc) +{ + if (desc->native_width <= 0 || desc->native_height <= 0 + || desc->view_width <= 0 || desc->view_height <= 0) { + ESP_LOGE(TAG, "Invalid e-paper geometry: native=%dx%d view=%dx%d.", + desc->native_width, desc->native_height, + desc->view_width, desc->view_height); + return false; + } + + switch (desc->rotation) { + case 0: + case 180: + if (desc->view_width == desc->native_width + && desc->view_height == desc->native_height) { + return true; + } + break; + case 90: + case 270: + if (desc->view_width == desc->native_height + && desc->view_height == desc->native_width) { + return true; + } + break; + default: + ESP_LOGE(TAG, "Invalid e-paper rotation %d; expected 0, 90, 180, or 270.", + desc->rotation); + return false; + } + + ESP_LOGE(TAG, "Invalid e-paper geometry for rotation %d: native=%dx%d view=%dx%d.", + desc->rotation, desc->native_width, desc->native_height, + desc->view_width, desc->view_height); + return false; +} + +static bool epaper_parse_descriptor_override(struct EpaperDriver *driver, + term opts, Context *ctx) +{ + term descriptor = interop_kv_get_value_default( + opts, ATOM_STR("\xA", "descriptor"), term_nil(), ctx->global); + if (descriptor == term_nil()) { + return true; + } + if (!term_is_nonempty_list(descriptor)) { + ESP_LOGE(TAG, "Invalid e-paper descriptor: expected keyword list."); + return false; + } + + memset(driver->term_program_bytes, 0, sizeof(driver->term_program_bytes)); + memset(driver->term_lut_bytes, 0, sizeof(driver->term_lut_bytes)); + driver->term_desc_name = NULL; + driver->term_desc = *driver->desc; + + // Check version first + int descriptor_version = 0; + if (!epaper_parse_int_field(descriptor, ctx, + ATOM_STR("\x12", "descriptor_version"), + &descriptor_version)) { + ESP_LOGE(TAG, "Invalid e-paper descriptor_version."); + return false; + } + driver->term_desc.descriptor_version = descriptor_version; + if (driver->term_desc.descriptor_version != 2) { + ESP_LOGE(TAG, "Unsupported e-paper descriptor version %d. Only version 2 is supported.", + driver->term_desc.descriptor_version); + return false; + } + + term name = interop_kv_get_value_default( + descriptor, ATOM_STR("\x4", "name"), term_nil(), ctx->global); + if (name != term_nil()) { + int name_ok; + char *parsed_name = interop_term_to_string(name, &name_ok); + if (name_ok && parsed_name != NULL) { + driver->term_desc_name = parsed_name; + driver->term_desc.name = parsed_name; + } else { + free(parsed_name); + } + } + + // Geometry + if (!epaper_parse_int_field(descriptor, ctx, ATOM_STR("\xC", "native_width"), + &driver->term_desc.native_width) + || !epaper_parse_int_field(descriptor, ctx, ATOM_STR("\xD", "native_height"), + &driver->term_desc.native_height) + || !epaper_parse_int_field(descriptor, ctx, ATOM_STR("\xA", "view_width"), + &driver->term_desc.view_width) + || !epaper_parse_int_field(descriptor, ctx, ATOM_STR("\xB", "view_height"), + &driver->term_desc.view_height) + || !epaper_parse_int_field(descriptor, ctx, ATOM_STR("\x8", "rotation"), + &driver->term_desc.rotation) + || !epaper_parse_int_field(descriptor, ctx, ATOM_STR("\xC", "spi_clock_hz"), + &driver->term_desc.spi_clock_hz) + || !epaper_parse_int_field(descriptor, ctx, ATOM_STR("\xF", "busy_idle_level"), + &driver->term_desc.busy_idle_level)) { + ESP_LOGE(TAG, "Invalid integer field in e-paper descriptor."); + return false; + } + + if (!epaper_parse_bool_field(descriptor, ctx, ATOM_STR("\x10", "use_gpio_pullups"), + &driver->term_desc.use_gpio_pullups)) { + ESP_LOGE(TAG, "Invalid e-paper descriptor use_gpio_pullups."); + return false; + } + + if (!epaper_parse_int_field(descriptor, ctx, ATOM_STR("\xC", "palette_size"), + &driver->term_desc.palette_size)) { + ESP_LOGE(TAG, "Invalid e-paper descriptor palette_size."); + return false; + } + + // Parse Controller + term controller_term = interop_kv_get_value_default(descriptor, ATOM_STR("\xA", "controller"), term_nil(), ctx->global); + if (controller_term != term_nil()) { + enum EPaperController ctrl; + if (!epaper_parse_controller(controller_term, ctx, &ctrl)) { + ESP_LOGE(TAG, "Invalid controller in descriptor."); + return false; + } + driver->term_desc.controller = ctrl; + } + + // Parse Frame Layout + term layout_term = interop_kv_get_value_default(descriptor, ATOM_STR("\xC", "frame_layout"), term_nil(), ctx->global); + if (layout_term != term_nil()) { + struct EPaperFrameLayout layout; + if (!epaper_parse_layout(layout_term, ctx, &layout)) { + ESP_LOGE(TAG, "Invalid frame_layout in descriptor."); + return false; + } + driver->term_desc.layout = layout; + } + + // Parse Polarity + term polarity_term = interop_kv_get_value_default(descriptor, ATOM_STR("\x8", "polarity"), term_nil(), ctx->global); + if (polarity_term != term_nil()) { + enum EPaperPolarity polarity; + if (!epaper_parse_polarity(polarity_term, ctx, &polarity)) { + ESP_LOGE(TAG, "Invalid polarity in descriptor."); + return false; + } + driver->term_desc.layout.polarity = polarity; + } + + // Parse Default Refresh + term default_refresh_term = interop_kv_get_value_default(descriptor, ATOM_STR("\xF", "default_refresh"), term_nil(), ctx->global); + if (default_refresh_term != term_nil()) { + enum EPaperRefreshMode default_refresh; + if (!epaper_parse_refresh_mode(default_refresh_term, ctx, &default_refresh)) { + ESP_LOGE(TAG, "Invalid default_refresh mode in descriptor."); + return false; + } + driver->term_desc.default_refresh = default_refresh; + } + + // Parse Programs Map + term programs_map = interop_kv_get_value_default(descriptor, ATOM_STR("\x8", "programs"), term_nil(), ctx->global); + if (programs_map != term_nil()) { + if (!epaper_copy_binary_field(programs_map, ctx, ATOM_STR("\x4", "init"), + &driver->term_program_bytes[0], + &driver->term_desc.init.bytes, &driver->term_desc.init.len) + || !epaper_copy_binary_field(programs_map, ctx, ATOM_STR("\x4", "full"), + &driver->term_program_bytes[1], + &driver->term_desc.program_full.bytes, &driver->term_desc.program_full.len) + || !epaper_copy_binary_field(programs_map, ctx, ATOM_STR("\x4", "fast"), + &driver->term_program_bytes[2], + &driver->term_desc.program_fast.bytes, &driver->term_desc.program_fast.len) + || !epaper_copy_binary_field(programs_map, ctx, ATOM_STR("\x7", "partial"), + &driver->term_program_bytes[3], + &driver->term_desc.program_partial.bytes, &driver->term_desc.program_partial.len) + || !epaper_copy_binary_field(programs_map, ctx, ATOM_STR("\x5", "4gray"), + &driver->term_program_bytes[4], + &driver->term_desc.program_4gray.bytes, &driver->term_desc.program_4gray.len) + || !epaper_copy_binary_field(programs_map, ctx, ATOM_STR("\x5", "sleep"), + &driver->term_program_bytes[5], + &driver->term_desc.sleep.bytes, &driver->term_desc.sleep.len) + || !epaper_copy_binary_field(programs_map, ctx, ATOM_STR("\x4", "wake"), + &driver->term_program_bytes[6], + &driver->term_desc.wake.bytes, &driver->term_desc.wake.len)) { + ESP_LOGE(TAG, "Invalid e-paper descriptor program binary."); + return false; + } + } + + // Parse Timing Map + term timing_map = interop_kv_get_value_default(descriptor, ATOM_STR("\x6", "timing"), term_nil(), ctx->global); + if (timing_map != term_nil()) { + if (!epaper_parse_int_field(timing_map, ctx, ATOM_STR("\x10", "full_expected_ms"), &driver->term_desc.full_expected_ms) + || !epaper_parse_int_field(timing_map, ctx, ATOM_STR("\x10", "fast_expected_ms"), &driver->term_desc.fast_expected_ms) + || !epaper_parse_int_field(timing_map, ctx, ATOM_STR("\x10", "poll_interval_ms"), &driver->term_desc.poll_interval_ms) + || !epaper_parse_int_field(timing_map, ctx, ATOM_STR("\n", "timeout_ms"), &driver->term_desc.timeout_ms)) { + ESP_LOGE(TAG, "Invalid field in e-paper descriptor timing."); + return false; + } + } + + // Parse Ghosting Map + term ghosting_map = interop_kv_get_value_default(descriptor, ATOM_STR("\x8", "ghosting"), term_nil(), ctx->global); + if (ghosting_map != term_nil()) { + if (!epaper_parse_int_field(ghosting_map, ctx, ATOM_STR("\x12", "max_fast_refreshes"), &driver->term_desc.max_fast_refreshes) + || !epaper_parse_bool_field(ghosting_map, ctx, ATOM_STR("\x11", "reseed_on_timeout"), &driver->term_desc.reseed_on_timeout)) { + ESP_LOGE(TAG, "Invalid field in e-paper descriptor ghosting."); + return false; + } + } + + // Parse LUTs + if (!epaper_copy_binary_field(descriptor, ctx, ATOM_STR("\x8", "lut_full"), + &driver->term_lut_bytes[EPAPER_LUT_SLOT_FULL], + &driver->term_desc.lut_slots[EPAPER_LUT_SLOT_FULL].bytes, + &driver->term_desc.lut_slots[EPAPER_LUT_SLOT_FULL].len) + || !epaper_copy_binary_field(descriptor, ctx, ATOM_STR("\xB", "lut_partial"), + &driver->term_lut_bytes[EPAPER_LUT_SLOT_PARTIAL], + &driver->term_desc.lut_slots[EPAPER_LUT_SLOT_PARTIAL].bytes, + &driver->term_desc.lut_slots[EPAPER_LUT_SLOT_PARTIAL].len) + || !epaper_copy_binary_field(descriptor, ctx, ATOM_STR("\x9", "lut_4gray"), + &driver->term_lut_bytes[EPAPER_LUT_SLOT_4GRAY], + &driver->term_desc.lut_slots[EPAPER_LUT_SLOT_4GRAY].bytes, + &driver->term_desc.lut_slots[EPAPER_LUT_SLOT_4GRAY].len) + || !epaper_copy_binary_field(descriptor, ctx, ATOM_STR("\x8", "lut_fast"), + &driver->term_lut_bytes[EPAPER_LUT_SLOT_FAST], + &driver->term_desc.lut_slots[EPAPER_LUT_SLOT_FAST].bytes, + &driver->term_desc.lut_slots[EPAPER_LUT_SLOT_FAST].len)) { + ESP_LOGE(TAG, "Invalid e-paper descriptor LUT binary."); + return false; + } + + if (driver->term_desc.palette_size <= 0) { + driver->term_desc.palette_size = 2; + } + + // Setup 4-gray palette if relevant + if (driver->term_desc.controller == EPAPER_CONTROLLER_SSD16XX && driver->term_desc.palette_size == 4) { + driver->term_desc.palette = epaper_ssd1680_4gray_palette; + } + + if (!epaper_validate_descriptor_geometry(&driver->term_desc)) { + return false; + } + + driver->desc = &driver->term_desc; + ESP_LOGI(TAG, "Using Erlang e-paper descriptor: %s", driver->desc->name); + return true; +} + static void display_spi_init(Context *ctx, term opts) { - // Resolve compatible string -> per-panel descriptor. + term descriptor_term = interop_kv_get_value_default( + opts, ATOM_STR("\xA", "descriptor"), term_nil(), ctx->global); + + // Without an explicit descriptor, resolve compatible string to one + // of the remaining C-side table-driven displays. term compat_term = interop_kv_get_value_default( opts, ATOM_STR("\xA", "compatible"), term_nil(), ctx->global); int str_ok; char *compat_string = interop_term_to_string(compat_term, &str_ok); const struct EPaperDesc *desc = NULL; - if (str_ok && compat_string) { + if (descriptor_term != term_nil()) { + desc = &epaper_desc_term_default; + } else if (str_ok && compat_string) { desc = epaper_desc_for_compatible(compat_string); } if (!desc) { @@ -348,17 +1579,45 @@ static void display_spi_init(Context *ctx, term opts) } free(compat_string); - struct EpaperDriver *driver = malloc(sizeof(struct EpaperDriver)); - // TODO check here + struct EpaperDriver *driver = calloc(1, sizeof(struct EpaperDriver)); + if (UNLIKELY(!driver)) { + ESP_LOGE(TAG, "Failed init: unable to allocate driver state."); + return; + } driver->desc = desc; driver->ctx = ctx; - driver->screen.w = desc->native_width; - driver->screen.h = desc->native_height; + if (!epaper_parse_descriptor_override(driver, opts, ctx)) { + ESP_LOGE(TAG, "Failed init: invalid descriptor override."); + epaper_free_init_failed_driver(driver, false); + return; + } + desc = driver->desc; + if (!epaper_validate_descriptor_geometry(desc)) { + ESP_LOGE(TAG, "Failed init: invalid descriptor geometry for '%s'.", desc->name); + epaper_free_init_failed_driver(driver, false); + return; + } + if (epaper_desc_requires_program(desc) && desc->program_full.bytes == NULL) { + ESP_LOGE(TAG, + "Failed init: SSD1680 compatible '%s' requires a descriptor refresh program.", + desc->name); + epaper_free_init_failed_driver(driver, false); + return; + } + + driver->screen.w = desc->view_width; + driver->screen.h = desc->view_height; driver->screen.palette = desc->palette; driver->screen.palette_size = desc->palette_size; - + driver->mono_screen.w = desc->view_width; + driver->mono_screen.h = desc->view_height; driver->display_args.messages_queue = xQueueCreate(32, sizeof(Message *)); + if (driver->display_args.messages_queue == NULL) { + ESP_LOGE(TAG, "Failed init: unable to allocate display queue."); + epaper_free_init_failed_driver(driver, false); + return; + } driver->display_args.process_message_fn = process_message; driver->display_args.ctx = ctx; ctx->platform_data = &driver->display_args; @@ -366,35 +1625,52 @@ static void display_spi_init(Context *ctx, term opts) struct SPIDisplayConfig spi_config; spi_display_init_config(&spi_config); spi_config.clock_speed_hz = desc->spi_clock_hz; - spi_display_parse_config(&spi_config, opts, ctx->global); + if (!spi_display_parse_config(&spi_config, opts, ctx->global)) { + ESP_LOGE(TAG, "Failed init: invalid SPI display configuration."); + epaper_free_init_failed_driver(driver, false); + return; + } spi_display_init(&driver->bus.spi_disp, &spi_config); + bool spi_device_added = true; bool ok = display_common_gpio_from_opts(opts, ATOM_STR("\x4", "busy"), &driver->busy_gpio, ctx->global); ok = ok && display_common_gpio_from_opts(opts, ATOM_STR("\x2", "dc"), &driver->bus.dc_gpio, ctx->global); ok = ok && display_common_gpio_from_opts(opts, ATOM_STR("\x5", "reset"), &driver->reset_gpio, ctx->global); if (UNLIKELY(!ok)) { ESP_LOGE(TAG, "Failed init: invalid display GPIOs."); + epaper_free_init_failed_driver(driver, spi_device_added); return; } gpio_set_direction(driver->reset_gpio, GPIO_MODE_OUTPUT); gpio_set_level(driver->reset_gpio, 1); gpio_set_direction(driver->bus.dc_gpio, GPIO_MODE_OUTPUT); - gpio_set_pull_mode(driver->bus.dc_gpio, GPIO_PULLUP_ENABLE); gpio_set_direction(driver->busy_gpio, GPIO_MODE_INPUT); - gpio_set_pull_mode(driver->busy_gpio, GPIO_PULLUP_ENABLE); - gpio_set_level(driver->bus.dc_gpio, 0); - - display_reset(driver); + if (desc->use_gpio_pullups) { + gpio_set_pull_mode(driver->bus.dc_gpio, GPIO_PULLUP_ENABLE); + gpio_set_pull_mode(driver->busy_gpio, GPIO_PULLUP_ENABLE); + gpio_set_level(driver->bus.dc_gpio, 0); + } - wait_busy_level(driver, 1); + driver->state.prev_valid = false; + driver->state.fast_refresh_count = 0; + driver->state.needs_reseed = false; // Init sequence: init_list opt overrides the descriptor default. term init_list = interop_kv_get_value_default( opts, ATOM_STR("\x9", "init_list"), term_nil(), ctx->global); if (init_list != term_nil()) { + display_reset(driver); + wait_busy_level(driver, desc->busy_idle_level); display_init_using_list(driver, init_list); - } else { + } else if (desc->init.bytes != NULL) { + if (!epaper_run_driver_program(driver, &desc->init, NULL)) { + ESP_LOGW(TAG, "E-paper descriptor init program failed for %s.", + desc->name); + } + } else if (desc->init_seq != NULL) { + display_reset(driver); + wait_busy_level(driver, desc->busy_idle_level); epaper_execute_init_seq(&driver->bus, driver->busy_gpio, desc->init_seq, desc->init_seq_len, desc->init_wait_busy_between_cmds); @@ -414,7 +1690,12 @@ static void display_spi_init(Context *ctx, term opts) while (1) ; #else - xTaskCreate(display_task_process_messages, "display", 10000, &driver->display_args, 1, NULL); + if (xTaskCreate(display_task_process_messages, "display", 10000, + &driver->display_args, 1, NULL) != pdPASS) { + ESP_LOGE(TAG, "Failed init: unable to start display task."); + epaper_free_init_failed_driver(driver, spi_device_added); + return; + } #endif } @@ -426,13 +1707,7 @@ Context *epaper_display_create_port(GlobalContext *global, term opts) return ctx; } -// Erlang-side init override: accepts a list of -// {CmdByte :: 0..255, Binary :: binary()} -// {sleep_ms, Ms :: 0..255} -// {wait_busy_level, Level :: 0 | 1} -// tuples and applies them in order. Mirrors dcs_lcd's display_init_using_list -// with an added wait_busy_level clause, since e-paper controllers typically -// require BUSY-pin polling between commands that DCS LCDs do not. +// Erlang-side init override static void display_init_using_list(struct EpaperDriver *driver, term init_list) { term t = init_list; @@ -447,7 +1722,7 @@ static void display_init_using_list(struct EpaperDriver *driver, term init_list) spi_dc_write_cmd_data(&driver->bus, cmd, data, term_binary_size(data_term)); } else if ((cmd_term == context_make_atom(driver->ctx, ATOM_STR("\x8", "sleep_ms"))) && term_is_integer(data_term)) { - vTaskDelay(term_to_int(data_term) / portTICK_PERIOD_MS); + epaper_delay_ms(term_to_int(data_term)); } else if ((cmd_term == context_make_atom(driver->ctx, ATOM_STR("\xF", "wait_busy_level"))) && term_is_integer(data_term)) { wait_busy_level(driver, term_to_int(data_term)); diff --git a/epaper_draw.c b/epaper_draw.c index ef46e80..e4547c7 100644 --- a/epaper_draw.c +++ b/epaper_draw.c @@ -32,6 +32,17 @@ #include "epaper_screen.h" #include "font_data.h" +static uint8_t epaper_dither_screen(const struct EpaperScreen *screen, + int x, int y, uint8_t r, uint8_t g, uint8_t b, bool four_gray) +{ + if (four_gray) { + return epaper_dither_4gray(x, y, r, g, b, + screen->palette, screen->palette_size); + } + return epaper_dither_acep7(x, y, r, g, b, + screen->palette, screen->palette_size); +} + void epaper_draw_pixel_x(const struct EpaperScreen *screen, uint8_t *line_buf, int xpos, uint8_t c) { @@ -47,9 +58,9 @@ void epaper_draw_pixel_x(const struct EpaperScreen *screen, } } -int epaper_draw_image_x(const struct EpaperScreen *screen, +static int epaper_draw_image_dither_x(const struct EpaperScreen *screen, uint8_t *line_buf, int xpos, int ypos, int max_line_len, - BaseDisplayItem *item) + BaseDisplayItem *item, bool four_gray) { int x = item->x; int y = item->y; @@ -88,13 +99,13 @@ int epaper_draw_image_x(const struct EpaperScreen *screen, uint8_t g = (img_pixel >> 16) & 0xFF; uint8_t b = (img_pixel >> 8) & 0xFF; - uint8_t c = epaper_dither_acep7(xpos + drawn_pixels, ypos, r, g, b, - screen->palette, screen->palette_size); + uint8_t c = epaper_dither_screen(screen, + xpos + drawn_pixels, ypos, r, g, b, four_gray); epaper_draw_pixel_x(screen, line_buf, xpos + drawn_pixels, c); } else if (visible_bg) { - uint8_t c = epaper_dither_acep7(xpos + drawn_pixels, ypos, bgcolor_r, bgcolor_g, bgcolor_b, - screen->palette, screen->palette_size); + uint8_t c = epaper_dither_screen(screen, + xpos + drawn_pixels, ypos, bgcolor_r, bgcolor_g, bgcolor_b, four_gray); epaper_draw_pixel_x(screen, line_buf, xpos + drawn_pixels, c); } else if (drawn_pixels > 0) { @@ -113,9 +124,9 @@ int epaper_draw_image_x(const struct EpaperScreen *screen, return drawn_pixels; } -int epaper_draw_rect_x(const struct EpaperScreen *screen, +static int epaper_draw_rect_dither_x(const struct EpaperScreen *screen, uint8_t *line_buf, int xpos, int ypos, int max_line_len, - BaseDisplayItem *item) + BaseDisplayItem *item, bool four_gray) { int x = item->x; int width = item->width; @@ -131,8 +142,8 @@ int epaper_draw_rect_x(const struct EpaperScreen *screen, } for (int j = xpos - x; j < width; j++) { - uint8_t c = epaper_dither_acep7(xpos + drawn_pixels, ypos, r, g, b, - screen->palette, screen->palette_size); + uint8_t c = epaper_dither_screen(screen, + xpos + drawn_pixels, ypos, r, g, b, four_gray); epaper_draw_pixel_x(screen, line_buf, xpos + drawn_pixels, c); drawn_pixels++; } @@ -140,10 +151,10 @@ int epaper_draw_rect_x(const struct EpaperScreen *screen, return drawn_pixels; } -int epaper_draw_shape_x(const struct EpaperScreen *screen, +static int epaper_draw_shape_dither_x(const struct EpaperScreen *screen, uint8_t *line_buf, int xpos, int ypos, int max_line_len, BaseDisplayItem *item, - int *outside_run) + int *outside_run, bool four_gray) { if (display_items_shape_outside_run(item, xpos, ypos, outside_run)) { return 0; @@ -158,12 +169,12 @@ int epaper_draw_shape_x(const struct EpaperScreen *screen, if (run > max_line_len) { run = max_line_len; } - return epaper_draw_rect_x(screen, line_buf, xpos, ypos, run, item); + return epaper_draw_rect_dither_x(screen, line_buf, xpos, ypos, run, item, four_gray); } -int epaper_draw_text_x(const struct EpaperScreen *screen, +static int epaper_draw_text_dither_x(const struct EpaperScreen *screen, uint8_t *line_buf, int xpos, int ypos, int max_line_len, - BaseDisplayItem *item) + BaseDisplayItem *item, bool four_gray) { int x = item->x; int y = item->y; @@ -215,13 +226,13 @@ int epaper_draw_text_x(const struct EpaperScreen *screen, } if (opaque) { - uint8_t c = epaper_dither_acep7(xpos + drawn_pixels, ypos, fgcolor_r, fgcolor_g, fgcolor_b, - screen->palette, screen->palette_size); + uint8_t c = epaper_dither_screen(screen, + xpos + drawn_pixels, ypos, fgcolor_r, fgcolor_g, fgcolor_b, four_gray); epaper_draw_pixel_x(screen, line_buf, xpos + drawn_pixels, c); } else if (visible_bg) { - uint8_t c = epaper_dither_acep7(xpos + drawn_pixels, ypos, bgcolor_r, bgcolor_g, bgcolor_b, - screen->palette, screen->palette_size); + uint8_t c = epaper_dither_screen(screen, + xpos + drawn_pixels, ypos, bgcolor_r, bgcolor_g, bgcolor_b, four_gray); epaper_draw_pixel_x(screen, line_buf, xpos + drawn_pixels, c); } else if (drawn_pixels > 0) { @@ -239,9 +250,9 @@ int epaper_draw_text_x(const struct EpaperScreen *screen, return drawn_pixels; } -int epaper_draw_scaled_cropped_img_x(const struct EpaperScreen *screen, +static int epaper_draw_scaled_cropped_img_dither_x(const struct EpaperScreen *screen, uint8_t *line_buf, int xpos, int ypos, int max_line_len, - BaseDisplayItem *item) + BaseDisplayItem *item, bool four_gray) { int x = item->x; int y = item->y; @@ -282,15 +293,15 @@ int epaper_draw_scaled_cropped_img_x(const struct EpaperScreen *screen, uint8_t g = (img_pixel >> 16) & 0xFF; uint8_t b = (img_pixel >> 8) & 0xFF; for (int k = 0; k < run; k++) { - uint8_t c = epaper_dither_acep7(xpos + drawn_pixels, ypos, r, g, b, - screen->palette, screen->palette_size); + uint8_t c = epaper_dither_screen(screen, + xpos + drawn_pixels, ypos, r, g, b, four_gray); epaper_draw_pixel_x(screen, line_buf, xpos + drawn_pixels, c); drawn_pixels++; } } else if (visible_bg) { for (int k = 0; k < run; k++) { - uint8_t c = epaper_dither_acep7(xpos + drawn_pixels, ypos, bgcolor_r, bgcolor_g, bgcolor_b, - screen->palette, screen->palette_size); + uint8_t c = epaper_dither_screen(screen, + xpos + drawn_pixels, ypos, bgcolor_r, bgcolor_g, bgcolor_b, four_gray); epaper_draw_pixel_x(screen, line_buf, xpos + drawn_pixels, c); drawn_pixels++; } @@ -312,9 +323,9 @@ int epaper_draw_scaled_cropped_img_x(const struct EpaperScreen *screen, return drawn_pixels; } -int epaper_draw_x(const struct EpaperScreen *screen, +static int epaper_draw_dither_x(const struct EpaperScreen *screen, uint8_t *line_buf, int xpos, int ypos, - BaseDisplayItem *row) + BaseDisplayItem *row, bool four_gray) { int line_len = screen->w - xpos; int transparent_run = INT_MAX; @@ -337,23 +348,28 @@ int epaper_draw_x(const struct EpaperScreen *screen, int drawn_pixels = 0; switch (item->primitive) { case PrimitiveImage: - drawn_pixels = epaper_draw_image_x(screen, line_buf, xpos, ypos, max_line_len, item); + drawn_pixels = epaper_draw_image_dither_x(screen, line_buf, xpos, ypos, + max_line_len, item, four_gray); break; case PrimitiveScaledCroppedImage: - drawn_pixels = epaper_draw_scaled_cropped_img_x(screen, line_buf, xpos, ypos, max_line_len, item); + drawn_pixels = epaper_draw_scaled_cropped_img_dither_x(screen, line_buf, xpos, ypos, + max_line_len, item, four_gray); break; case PrimitiveRect: - drawn_pixels = epaper_draw_rect_x(screen, line_buf, xpos, ypos, max_line_len, item); + drawn_pixels = epaper_draw_rect_dither_x(screen, line_buf, xpos, ypos, + max_line_len, item, four_gray); break; case PrimitiveText: - drawn_pixels = epaper_draw_text_x(screen, line_buf, xpos, ypos, max_line_len, item); + drawn_pixels = epaper_draw_text_dither_x(screen, line_buf, xpos, ypos, + max_line_len, item, four_gray); break; case PrimitiveShape: - drawn_pixels = epaper_draw_shape_x(screen, line_buf, xpos, ypos, max_line_len, item, &run); + drawn_pixels = epaper_draw_shape_dither_x(screen, line_buf, xpos, ypos, + max_line_len, item, &run, four_gray); break; default: { @@ -376,3 +392,57 @@ int epaper_draw_x(const struct EpaperScreen *screen, return 1; } + +int epaper_draw_x(const struct EpaperScreen *screen, + uint8_t *line_buf, int xpos, int ypos, + BaseDisplayItem *row) +{ + return epaper_draw_dither_x(screen, line_buf, xpos, ypos, row, false); +} + +int epaper_draw_4gray_x(const struct EpaperScreen *screen, + uint8_t *line_buf, int xpos, int ypos, + BaseDisplayItem *row) +{ + return epaper_draw_dither_x(screen, line_buf, xpos, ypos, row, true); +} + +int epaper_draw_image_x(const struct EpaperScreen *screen, + uint8_t *line_buf, int xpos, int ypos, int max_line_len, + BaseDisplayItem *item) +{ + return epaper_draw_image_dither_x(screen, line_buf, xpos, ypos, + max_line_len, item, false); +} + +int epaper_draw_rect_x(const struct EpaperScreen *screen, + uint8_t *line_buf, int xpos, int ypos, int max_line_len, + BaseDisplayItem *item) +{ + return epaper_draw_rect_dither_x(screen, line_buf, xpos, ypos, + max_line_len, item, false); +} + +int epaper_draw_shape_x(const struct EpaperScreen *screen, + uint8_t *line_buf, int xpos, int ypos, int max_line_len, + BaseDisplayItem *item, int *outside_run) +{ + return epaper_draw_shape_dither_x(screen, line_buf, xpos, ypos, + max_line_len, item, outside_run, false); +} + +int epaper_draw_text_x(const struct EpaperScreen *screen, + uint8_t *line_buf, int xpos, int ypos, int max_line_len, + BaseDisplayItem *item) +{ + return epaper_draw_text_dither_x(screen, line_buf, xpos, ypos, + max_line_len, item, false); +} + +int epaper_draw_scaled_cropped_img_x(const struct EpaperScreen *screen, + uint8_t *line_buf, int xpos, int ypos, int max_line_len, + BaseDisplayItem *item) +{ + return epaper_draw_scaled_cropped_img_dither_x(screen, line_buf, xpos, ypos, + max_line_len, item, false); +} diff --git a/epaper_draw.h b/epaper_draw.h index 5b9918c..97133ce 100644 --- a/epaper_draw.h +++ b/epaper_draw.h @@ -51,4 +51,8 @@ int epaper_draw_x(const struct EpaperScreen *screen, uint8_t *line_buf, int xpos, int ypos, BaseDisplayItem *row); +int epaper_draw_4gray_x(const struct EpaperScreen *screen, + uint8_t *line_buf, int xpos, int ypos, + BaseDisplayItem *row); + #endif diff --git a/epaper_program.c b/epaper_program.c new file mode 100644 index 0000000..3bfa95c --- /dev/null +++ b/epaper_program.c @@ -0,0 +1,121 @@ +/* + * This file is part of AtomGL. + * + * Copyright 2026 AtomGL contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * SPDX-License-Identifier: Apache-2.0 + */ + +#include "epaper_program.h" + +static bool epaper_run_meta_opcode(const struct EPaperProgramOps *ops, + uint8_t opcode, const uint8_t *operands, uint8_t len) +{ + switch (opcode) { + case EPAPER_PROGRAM_WAIT_BUSY: + if (len != 3 || ops->wait_busy == NULL) { + return false; + } + return ops->wait_busy(ops->ctx, operands[0], + (uint16_t) operands[1] | ((uint16_t) operands[2] << 8)); + + case EPAPER_PROGRAM_RESET: + if (len != 3 || ops->reset == NULL) { + return false; + } + ops->reset(ops->ctx, operands[0], operands[1], operands[2]); + return true; + + case EPAPER_PROGRAM_INSERT_PLANE: + if (len != 1 || ops->insert_plane == NULL) { + return false; + } + return ops->insert_plane(ops->ctx, operands[0]); + + case EPAPER_PROGRAM_INSERT_LUT: + if (len != 1 || ops->insert_lut == NULL) { + return false; + } + return ops->insert_lut(ops->ctx, operands[0]); + + case EPAPER_PROGRAM_INSERT_PREV_FRAME: + if (len != 1 || ops->insert_prev_frame == NULL) { + return false; + } + return ops->insert_prev_frame(ops->ctx, operands[0]); + + case EPAPER_PROGRAM_CAPTURE_FRAME: + if (len != 0 || ops->capture_frame == NULL) { + return false; + } + return ops->capture_frame(ops->ctx); + + case EPAPER_PROGRAM_MARK_PREV_VALID: + if (len != 0 || ops->mark_prev_valid == NULL) { + return false; + } + return ops->mark_prev_valid(ops->ctx); + + case EPAPER_PROGRAM_LABEL: + return len == 1; + + default: + return false; + } +} + +bool epaper_run_program(const struct EPaperProgram *program, + const struct EPaperProgramOps *ops) +{ + if (program == NULL || program->bytes == NULL) { + return true; + } + if (ops == NULL || ops->command == NULL) { + return false; + } + + const uint8_t *pc = program->bytes; + const uint8_t *end = program->bytes + program->len; + while (pc < end) { + if ((size_t) (end - pc) < 2) { + return false; + } + + uint8_t opcode = *pc++; + uint8_t flags_len = *pc++; + uint8_t len = flags_len & EPAPER_PROGRAM_LEN_MASK; + if ((size_t) (end - pc) < len) { + return false; + } + + if (flags_len & EPAPER_PROGRAM_META) { + if (!epaper_run_meta_opcode(ops, opcode, pc, len)) { + return false; + } + } else { + ops->command(ops->ctx, opcode, pc, len); + } + pc += len; + + if (flags_len & EPAPER_PROGRAM_DELAY) { + if (pc >= end || ops->delay_ms == NULL) { + return false; + } + ops->delay_ms(ops->ctx, *pc++); + } + } + + return true; +} diff --git a/epaper_program.h b/epaper_program.h new file mode 100644 index 0000000..28f35fd --- /dev/null +++ b/epaper_program.h @@ -0,0 +1,85 @@ +/* + * This file is part of AtomGL. + * + * Copyright 2026 AtomGL contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * SPDX-License-Identifier: Apache-2.0 + */ + +#ifndef _EPAPER_PROGRAM_H_ +#define _EPAPER_PROGRAM_H_ + +#include +#include +#include + +#define EPAPER_MAX_LUT_SLOTS 4 + +#define EPAPER_PROGRAM_DELAY 0x80 +#define EPAPER_PROGRAM_META 0x40 +#define EPAPER_PROGRAM_LEN_MASK 0x3F + +enum EPaperProgramOpcode +{ + EPAPER_PROGRAM_WAIT_BUSY = 0x01, + EPAPER_PROGRAM_RESET = 0x02, + EPAPER_PROGRAM_INSERT_PLANE = 0x03, + EPAPER_PROGRAM_INSERT_LUT = 0x04, + EPAPER_PROGRAM_INSERT_PREV_FRAME = 0x05, + EPAPER_PROGRAM_CAPTURE_FRAME = 0x06, + EPAPER_PROGRAM_MARK_PREV_VALID = 0x07, + EPAPER_PROGRAM_LABEL = 0x10 +}; + +enum EPaperLutSlot +{ + EPAPER_LUT_SLOT_FULL = 0, + EPAPER_LUT_SLOT_PARTIAL = 1, + EPAPER_LUT_SLOT_4GRAY = 2, + EPAPER_LUT_SLOT_FAST = 3 +}; + +struct EPaperProgram +{ + const uint8_t *bytes; + size_t len; +}; + +struct EPaperLut +{ + const uint8_t *bytes; + size_t len; +}; + +struct EPaperProgramOps +{ + void *ctx; + + void (*command)(void *ctx, uint8_t command, const uint8_t *data, size_t len); + void (*delay_ms)(void *ctx, uint8_t delay_ms); + void (*reset)(void *ctx, uint8_t high_ms, uint8_t low_ms, uint8_t settle_ms); + + bool (*wait_busy)(void *ctx, uint8_t level, uint16_t timeout_ms); + bool (*insert_plane)(void *ctx, uint8_t plane_id); + bool (*insert_lut)(void *ctx, uint8_t slot_id); + bool (*insert_prev_frame)(void *ctx, uint8_t plane_id); + bool (*capture_frame)(void *ctx); + bool (*mark_prev_valid)(void *ctx); +}; + +bool epaper_run_program(const struct EPaperProgram *program, + const struct EPaperProgramOps *ops); + +#endif diff --git a/tests/items/test_items.c b/tests/items/test_items.c index 46d08d2..565a0e2 100644 --- a/tests/items/test_items.c +++ b/tests/items/test_items.c @@ -1233,10 +1233,11 @@ typedef enum { RendererDcsLcd, RendererMono, - RendererEpaper + RendererEpaper, + RendererEpaper4gray } renderer_t; -static const char *const renderer_names[] = { "dcs_lcd", "mono", "epaper" }; +static const char *const renderer_names[] = { "dcs_lcd", "mono", "epaper", "epaper_4gray" }; static int renderer_line_bytes(renderer_t renderer) { @@ -1244,6 +1245,7 @@ static int renderer_line_bytes(renderer_t renderer) case RendererMono: return SCREEN_W / 8; case RendererEpaper: + case RendererEpaper4gray: return SCREEN_W / 2; default: return SCREEN_W * 2; @@ -1261,6 +1263,12 @@ static int renderer_draw_x(renderer_t renderer, int w, uint16_t *line, int xpos, struct EpaperScreen screen = { .w = w, .h = SCREEN_H, .palette = epaper_acep_palette, .palette_size = 7 }; return epaper_draw_x(&screen, (uint8_t *) line, xpos, ypos, row); } + case RendererEpaper4gray: { + struct EpaperScreen screen = { + .w = w, .h = SCREEN_H, .palette = epaper_ssd1680_4gray_palette, .palette_size = 4 + }; + return epaper_draw_4gray_x(&screen, (uint8_t *) line, xpos, ypos, row); + } default: { struct DCSLCDScreen screen; memset(&screen, 0, sizeof(screen)); @@ -1410,7 +1418,7 @@ static void test_runs_match_pixels(void) } bool same = true; - for (renderer_t renderer = RendererDcsLcd; renderer <= RendererEpaper && same; renderer++) { + for (renderer_t renderer = RendererDcsLcd; renderer <= RendererEpaper4gray && same; renderer++) { render_items(renderer, items, len, expected, true); render_items(renderer, items, len, frame, false); same = memcmp(expected, frame, renderer_line_bytes(renderer) * SCREEN_H) == 0; From c6052f29be1e72362d28b8a463cb60615f203a6d Mon Sep 17 00:00:00 2001 From: Peter M Date: Sun, 24 May 2026 11:30:55 +0200 Subject: [PATCH 2/6] epaper: migrate built-in panels to descriptors Move ACeP 7-color panels to Erlang descriptors and add UC8151 and UC8276 panel support with controller-specific LUT, frame, and refresh handling. Remove the old compatible-string path and baked-in ACeP C descriptors. Update the panel documentation and descriptor tests. Signed-off-by: Peter M --- README.Md | 4 +- .../src/atomgl_epaper_descriptor.erl | 90 ++++- atomgl_epaper/src/atomgl_epaper_panels.erl | 334 +++++++++++++++++- atomgl_epaper/src/atomgl_epaper_program.erl | 4 + atomgl_epaper/test/atomgl_epaper_tests.erl | 73 ++++ display_driver.c | 5 +- docs/display-drivers.md | 41 ++- epaper_commands.c | 143 -------- epaper_commands.h | 23 +- epaper_display_driver.c | 208 ++++++++--- 10 files changed, 682 insertions(+), 243 deletions(-) diff --git a/README.Md b/README.Md index b9cc1b5..106cb50 100644 --- a/README.Md +++ b/README.Md @@ -83,7 +83,9 @@ that would otherwise be impossible to support with traditional frame buffer appr * `ilitek,ili9341` / `ilitek,ili9342c`: ILI9341 / ILI9342C - 240x320, 16-bit colors * `ilitek,ili9486`: ILI9486 - 320x480, 16-bit colors (RGB565) * `ilitek,ili9488`: ILI9488 - 320x480, 18-bit colors (RGB666 over SPI; 3 bytes/pixel transfer) -* `waveshare,5in65-acep-7c`: Waveshare 7-color 5.65" ACeP display module - 600x480, 7 colors + +* `waveshare,5in65-acep-7c`: Waveshare 7-color 5.65" ACeP display module - 600x448, 7 colors + +software dithering +* `good-display/gdep073e01`: Good Display 7.3" 7-color e-paper module - 800x480, 7 colors + software dithering * `sharp,memory-lcd`: Sharp Memory LCDs - 400x240, 1-bit monochrome * `solomon-systech,ssd1306`: Solomon Systech SSD1306 - 128x64, 1-bit monochrome diff --git a/atomgl_epaper/src/atomgl_epaper_descriptor.erl b/atomgl_epaper/src/atomgl_epaper_descriptor.erl index 9419cb6..7b2cb0d 100644 --- a/atomgl_epaper/src/atomgl_epaper_descriptor.erl +++ b/atomgl_epaper/src/atomgl_epaper_descriptor.erl @@ -6,6 +6,9 @@ -export([ ssd16xx_panel/2, jd79656_panel/2, + uc8151_panel/2, + uc8276_panel/2, + acep7_panel/2, validate_descriptor/1 ]). @@ -15,6 +18,50 @@ ssd16xx_panel(Name, Config) -> jd79656_panel(Name, Config) -> build_panel(jd79656, Name, Config). +uc8151_panel(Name, Config) -> + build_panel(uc8151, Name, Config). + +uc8276_panel(Name, Config) -> + build_panel(uc8276, Name, Config). + +acep7_panel(Name, Config) -> + NativeW = maps:get(native_width, Config), + NativeH = maps:get(native_height, Config), + Rotation = maps:get(rotation, Config, 0), + ViewW = maps:get(view_width, Config, NativeW), + ViewH = maps:get(view_height, Config, NativeH), + + Descriptor = [ + {descriptor_version, 2}, + {name, Name}, + {controller, acep7}, + {native_width, NativeW}, + {native_height, NativeH}, + {view_width, ViewW}, + {view_height, ViewH}, + {rotation, Rotation}, + {spi_clock_hz, maps:get(spi_clock_hz, Config, 4000000)}, + {busy_idle_level, maps:get(busy_idle_level, Config, 1)}, + {use_gpio_pullups, maps:get(use_gpio_pullups, Config, true)}, + {frame_layout, maps:get(frame_layout, Config, row_msb)}, + {polarity, maps:get(polarity, Config, white_1)}, + {refresh_modes, maps:get(refresh_modes, Config, [full])}, + {default_refresh, maps:get(default_refresh, Config, full)}, + {palette, maps:get(palette, Config)}, + {palette_size, maps:get(palette_size, Config, 7)}, + {init_seq, maps:get(init_seq, Config)}, + {init_wait_busy_between_cmds, maps:get(init_wait_busy_between_cmds, Config, false)}, + {refresh_has_data, maps:get(refresh_has_data, Config, false)}, + {refresh_data_byte, maps:get(refresh_data_byte, Config, 0)}, + {post_power_off_busy_level, maps:get(post_power_off_busy_level, Config, 0)}, + {periodic_refresh_interval, maps:get(periodic_refresh_interval, Config, 0)} + ] ++ case maps:find(frame_preamble_seq, Config) of + {ok, FramePreambleSeq} -> [{frame_preamble_seq, FramePreambleSeq}]; + error -> [] + end, + + validate_descriptor(Descriptor). + build_panel(Controller, Name, Config) -> NativeW = maps:get(native_width, Config), NativeH = maps:get(native_height, Config), @@ -50,6 +97,8 @@ build_panel(Controller, Name, Config) -> {Layout, Polarity} = case Controller of ssd16xx -> {row_msb, white_1}; jd79656 -> {column_msb, black_1}; + uc8151 -> {row_msb, white_1}; + uc8276 -> {row_msb, white_1}; uc8175 -> {row_msb, white_1} end, @@ -142,11 +191,13 @@ validate_descriptor(Desc) -> end, Controller = get_value(controller, Desc), - true = lists:member(Controller, [ssd16xx, jd79656, uc8175, acep7]), + true = lists:member(Controller, [ssd16xx, jd79656, uc8151, uc8276, uc8175, acep7]), Layout = get_value(frame_layout, Desc), true = case Controller of ssd16xx -> lists:member(Layout, [row_msb, row_lsb]); + uc8151 -> lists:member(Layout, [row_msb, row_lsb]); + uc8276 -> lists:member(Layout, [row_msb, row_lsb]); uc8175 -> lists:member(Layout, [row_msb, row_lsb]); jd79656 -> lists:member(Layout, [column_msb, column_lsb]); _ -> true @@ -156,18 +207,31 @@ validate_descriptor(Desc) -> RefreshModes = get_value(refresh_modes, Desc), true = lists:member(DefaultRefresh, RefreshModes), - Programs = get_value(programs, Desc), - true = is_defined(init, Programs), - - %% Required programs for declared modes - lists:foreach(fun(Mode) -> - true = is_defined(Mode, Programs) - end, RefreshModes), - - %% Bytecode payload length limits - lists:foreach(fun({_K, V}) -> - true = (byte_size(V) =< 4096) - end, Programs), + case Controller of + acep7 -> + true = (Rotation == 0), + Palette = get_value(palette, Desc), + true = lists:member(Palette, [acep7, acep7c, gdep073e01]), + InitSeq = get_value(init_seq, Desc), + true = is_binary(InitSeq), + true = (byte_size(InitSeq) =< 4096), + FramePreambleSeq = get_value(frame_preamble_seq, Desc, <<>>), + true = is_binary(FramePreambleSeq), + true = (byte_size(FramePreambleSeq) =< 4096); + _ -> + Programs = get_value(programs, Desc), + true = is_defined(init, Programs), + + %% Required programs for declared modes + lists:foreach(fun(Mode) -> + true = is_defined(Mode, Programs) + end, RefreshModes), + + %% Bytecode payload length limits + lists:foreach(fun({_K, V}) -> + true = (byte_size(V) =< 4096) + end, Programs) + end, {ok, Desc}. diff --git a/atomgl_epaper/src/atomgl_epaper_panels.erl b/atomgl_epaper/src/atomgl_epaper_panels.erl index 73c5296..530977b 100644 --- a/atomgl_epaper/src/atomgl_epaper_panels.erl +++ b/atomgl_epaper/src/atomgl_epaper_panels.erl @@ -8,14 +8,18 @@ ]). -import(atomgl_epaper_descriptor, [ + acep7_panel/2, jd79656_panel/2, - ssd16xx_panel/2 + ssd16xx_panel/2, + uc8151_panel/2, + uc8276_panel/2 ]). -import(atomgl_epaper_program, [ program/1, cmd/1, cmd/2, + cmd_delay/3, wait_busy/2, reset/3, insert_plane/1, @@ -33,14 +37,19 @@ -define(LUT_FAST, 3). panel("waveshare,epaper-2in9", Opts) -> - ssd16xx_panel("Waveshare 2.9\" e-paper SSD1680 v1", maps:merge(#{ + panel("waveshare,epd2in9", Opts); +panel("waveshare,epd2in9", Opts) -> + uc8151_panel("Waveshare epd2in9 2.9\" e-paper UC8151 v1", maps:merge(#{ native_width => 128, native_height => 296, - refresh_modes => [full], + refresh_modes => [full, partial], default_refresh => full, - init => ssd1680_2in9_init(), - full => ssd1680_2in9_refresh(), - sleep => cmd(16#10, <<16#01>>) + init => uc8151_init(296, 16#03, ?LUT_FULL), + full => uc8151_refresh(128, 296, ?LUT_FULL), + partial => uc8151_refresh(128, 296, ?LUT_PARTIAL), + sleep => cmd(16#10, <<16#01>>), + lut_full => uc8151_2in9_full_lut(), + lut_partial => uc8151_2in9_partial_lut() }, Opts)); panel("waveshare,epd2in9_V2", Opts) -> ssd16xx_panel("Waveshare epd2in9_V2 2.9\" e-paper SSD1680 v1", maps:merge(#{ @@ -78,6 +87,19 @@ panel("dke,depg0290bns800f6", Opts) -> }, Opts)); panel("waveshare,epaper-2in13", Opts) -> panel("waveshare,epd2in13_V4", Opts); +panel("waveshare,epd2in13", Opts) -> + uc8151_panel("Waveshare epd2in13 2.13\" e-paper UC8151 v1", maps:merge(#{ + native_width => 122, + native_height => 250, + refresh_modes => [full, partial], + default_refresh => full, + init => uc8151_init(250, 16#63, ?LUT_FULL), + full => uc8151_refresh(122, 250, ?LUT_FULL), + partial => uc8151_refresh(122, 250, ?LUT_PARTIAL), + sleep => cmd(16#10, <<16#01>>), + lut_full => uc8151_2in13_full_lut(), + lut_partial => uc8151_2in13_partial_lut() + }, Opts)); panel("waveshare,epd2in13_V4", Opts) -> ssd16xx_panel("Waveshare epd2in13_V4 2.13\" e-paper SSD1680 v1", maps:merge(#{ native_width => 122, @@ -94,6 +116,27 @@ panel("waveshare,epd2in13_V4-fast", Opts) -> panel("waveshare,epd2in13_V4", maps:merge(#{default_refresh => fast}, Opts)); panel("waveshare,epd2in13_V4-partial", Opts) -> panel("waveshare,epd2in13_V4", maps:merge(#{default_refresh => partial}, Opts)); +panel("waveshare,epd4in2_V2", Opts) -> + uc8276_panel("Waveshare epd4in2_V2 4.2\" e-paper UC8276 v1", maps:merge(#{ + native_width => 400, + native_height => 300, + palette_size => 4, + refresh_modes => [full, fast, partial, '4gray'], + default_refresh => full, + init => uc8276_init(), + full => uc8276_refresh(uc8276_mono_prelude(), 16#F7), + fast => uc8276_refresh(uc8276_fast_prelude(16#6E), 16#C7), + partial => uc8276_partial_refresh(), + '4gray' => uc8276_4gray_refresh(), + sleep => cmd(16#10, <<16#01>>), + lut_4gray => uc8276_4gray_lut() + }, Opts)); +panel("waveshare,epd4in2_V2-fast", Opts) -> + panel("waveshare,epd4in2_V2", maps:merge(#{default_refresh => fast}, Opts)); +panel("waveshare,epd4in2_V2-partial", Opts) -> + panel("waveshare,epd4in2_V2", maps:merge(#{default_refresh => partial}, Opts)); +panel("waveshare,epd4in2_V2-4gray", Opts) -> + panel("waveshare,epd4in2_V2", maps:merge(#{default_refresh => '4gray'}, Opts)); panel("heltec,lcmen2r13efc1", Opts) -> jd79656_panel("Heltec LCMEN2R13EFC1 2.13\" e-paper JD79656 v1", maps:merge(#{ native_width => 122, @@ -108,9 +151,177 @@ panel("heltec,icmen2r13efc1", Opts) -> panel("heltec,lcmen2r13efc1", Opts); panel("heltec,ht-vme213", Opts) -> panel("heltec,lcmen2r13efc1", Opts); +panel("waveshare,5in65-acep-7c", Opts) -> + acep7_panel("Waveshare 5.65\" ACeP 7-color", maps:merge(#{ + native_width => 600, + native_height => 448, + spi_clock_hz => 1000000, + busy_idle_level => 1, + use_gpio_pullups => true, + palette => acep7, + init_seq => acep7c_init_seq(), + init_wait_busy_between_cmds => false, + frame_preamble_seq => acep7c_frame_preamble_seq(), + refresh_has_data => false, + refresh_data_byte => 0, + post_power_off_busy_level => 0, + periodic_refresh_interval => 5 + }, Opts)); +panel("good-display/gdep073e01", Opts) -> + acep7_panel("Good Display GDEP073E01 7.3\" 7-color", maps:merge(#{ + native_width => 800, + native_height => 480, + spi_clock_hz => 4000000, + busy_idle_level => 1, + use_gpio_pullups => true, + palette => gdep073e01, + init_seq => gdep073e01_init_seq(), + init_wait_busy_between_cmds => true, + refresh_has_data => true, + refresh_data_byte => 0, + post_power_off_busy_level => 1, + periodic_refresh_interval => 0 + }, Opts)); panel(_, _Opts) -> error. +uc8151_init(NativeHeight, BorderWaveform, LutSlot) -> + DriverOutput = NativeHeight - 1, + Border = case BorderWaveform of + none -> []; + _ -> [cmd(16#3C, <>)] + end, + program([ + reset(200, 2, 200), + cmd(16#01, <>), + cmd(16#0C, <<16#D7, 16#D6, 16#9D>>), + cmd(16#2C, <<16#A8>>), + cmd(16#3A, <<16#1A>>), + cmd(16#3B, <<16#08>>), + Border, + cmd(16#11, <<16#03>>), + insert_lut(LutSlot) + ]). + +uc8151_refresh(NativeWidth, NativeHeight, LutSlot) -> + program([ + insert_lut(LutSlot), + uc8151_window(NativeWidth, NativeHeight), + uc8151_cursor(), + cmd(16#24), + capture_frame(), + insert_plane(0), + cmd(16#22, <<16#C4>>), + cmd(16#20), + cmd(16#FF), + delay_ms(100), + wait_busy(0, 5000), + mark_prev_valid() + ]). + +uc8151_window(NativeWidth, NativeHeight) -> + XEnd = NativeWidth - 1, + YEnd = NativeHeight - 1, + [ + cmd(16#44, <<16#00, ((XEnd bsr 3) band 16#FF)>>), + cmd(16#45, <<16#00, 16#00, YEnd:16/little-unsigned-integer>>) + ]. + +uc8151_cursor() -> + [ + cmd(16#4E, <<16#00>>), + cmd(16#4F, <<16#00, 16#00>>) + ]. + +uc8276_init() -> + program([ + reset(100, 2, 100), + wait_busy(0, 5000), + cmd(16#12), + wait_busy(0, 5000), + uc8276_mono_prelude(), + wait_busy(0, 5000) + ]). + +uc8276_mono_prelude() -> + [ + cmd(16#21, <<16#40, 16#00>>), + cmd(16#3C, <<16#05>>), + cmd(16#11, <<16#03>>), + uc8276_window(), + uc8276_cursor() + ]. + +uc8276_fast_prelude(Temperature) -> + [ + cmd(16#21, <<16#40, 16#00>>), + cmd(16#3C, <<16#05>>), + cmd(16#1A, <>), + cmd(16#22, <<16#91>>), + cmd(16#20), + wait_busy(0, 5000), + cmd(16#11, <<16#03>>), + uc8276_window(), + uc8276_cursor() + ]. + +uc8276_refresh(Prelude, UpdateMode) -> + program([ + Prelude, + cmd(16#24), + capture_frame(), + insert_plane(0), + cmd(16#26), + insert_prev_frame(0), + cmd(16#22, <>), + cmd(16#20), + wait_busy(0, 5000), + mark_prev_valid() + ]). + +uc8276_partial_refresh() -> + program([ + cmd(16#3C, <<16#80>>), + cmd(16#21, <<16#00, 16#00>>), + cmd(16#3C, <<16#80>>), + uc8276_window(), + uc8276_cursor(), + cmd(16#24), + capture_frame(), + insert_plane(0), + cmd(16#22, <<16#FF>>), + cmd(16#20), + wait_busy(0, 5000), + mark_prev_valid() + ]). + +uc8276_4gray_refresh() -> + program([ + reset(100, 2, 100), + cmd(16#12), + wait_busy(0, 5000), + cmd(16#21, <<16#00, 16#00>>), + cmd(16#3C, <<16#03>>), + cmd(16#0C, <<16#8B, 16#9C, 16#A4, 16#0F>>), + insert_lut(?LUT_4GRAY), + cmd(16#11, <<16#03>>), + uc8276_window(), + uc8276_cursor(), + cmd(16#24), + insert_plane(0), + cmd(16#26), + insert_plane(1), + cmd(16#22, <<16#CF>>), + cmd(16#20), + wait_busy(0, 5000) + ]). + +uc8276_window() -> + [cmd(16#44, <<16#00, 16#31>>), cmd(16#45, <<16#00, 16#00, 16#2B, 16#01>>)]. + +uc8276_cursor() -> + [cmd(16#4E, <<16#00>>), cmd(16#4F, <<16#00, 16#00>>)]. + ssd1680_2in9_init() -> program([ reset(10, 10, 100), @@ -236,6 +447,45 @@ heltec_lcmen2r13efc1_refresh() -> ]). %% SPDX-SnippetEnd +acep7c_init_seq() -> + program([ + cmd(16#00, <<16#EF, 16#08>>), + cmd(16#01, <<16#37, 16#00, 16#23, 16#23>>), + cmd(16#03, <<16#00>>), + cmd(16#06, <<16#C7, 16#C7, 16#1D>>), + cmd(16#30, <<16#3C>>), + cmd(16#40, <<16#00>>), + cmd(16#50, <<16#3F>>), + cmd(16#60, <<16#22>>), + cmd(16#61, <<16#02, 16#58, 16#01, 16#C0>>), + cmd(16#E3, <<16#AA>>), + cmd_delay(16#82, <<16#80>>, 100), + cmd(16#50, <<16#37>>) + ]). + +acep7c_frame_preamble_seq() -> + program([ + cmd(16#61, <<16#02, 16#58, 16#01, 16#C0>>) + ]). + +gdep073e01_init_seq() -> + program([ + cmd(16#AA, <<16#49, 16#55, 16#20, 16#08, 16#09, 16#18>>), + cmd(16#01, <<16#3F>>), + cmd(16#00, <<16#5F, 16#69>>), + cmd(16#03, <<16#00, 16#54, 16#00, 16#44>>), + cmd(16#05, <<16#40, 16#1F, 16#1F, 16#2C>>), + cmd(16#06, <<16#6F, 16#1F, 16#17, 16#49>>), + cmd(16#08, <<16#6F, 16#1F, 16#1F, 16#22>>), + cmd(16#30, <<16#00>>), + cmd(16#50, <<16#3F>>), + cmd(16#60, <<16#02, 16#00>>), + cmd(16#61, <<16#03, 16#20, 16#01, 16#E0>>), + cmd(16#84, <<16#01>>), + cmd(16#E3, <<16#2F>>), + cmd(16#04) + ]). + epd2in9_v2_init(BorderWaveform) -> epd2in9_v2_init(BorderWaveform, 0). @@ -334,6 +584,78 @@ set_ram_cursor(RamXOffset) -> %% SPDX-SnippetBegin %% SPDX-SnippetCopyrightText: Waveshare team %% SPDX-License-Identifier: MIT +uc8151_2in9_full_lut() -> + << + 16#50, 16#AA, 16#55, 16#AA, 16#11, 16#00, + 16#00, 16#00, 16#00, 16#00, 16#00, 16#00, + 16#00, 16#00, 16#00, 16#00, 16#00, 16#00, + 16#00, 16#00, 16#FF, 16#FF, 16#1F, 16#00, + 16#00, 16#00, 16#00, 16#00, 16#00, 16#00 + >>. + +uc8151_2in9_partial_lut() -> + << + 16#10, 16#18, 16#18, 16#08, 16#18, 16#18, + 16#08, 16#00, 16#00, 16#00, 16#00, 16#00, + 16#00, 16#00, 16#00, 16#00, 16#00, 16#00, + 16#00, 16#00, 16#13, 16#14, 16#44, 16#12, + 16#00, 16#00, 16#00, 16#00, 16#00, 16#00 + >>. + +uc8151_2in13_full_lut() -> + << + 16#22, 16#55, 16#AA, 16#55, 16#AA, 16#55, 16#AA, 16#11, + 16#00, 16#00, 16#00, 16#00, 16#00, 16#00, 16#00, 16#00, + 16#1E, 16#1E, 16#1E, 16#1E, 16#1E, 16#1E, 16#1E, 16#1E, + 16#01, 16#00, 16#00, 16#00, 16#00, 16#00 + >>. + +uc8151_2in13_partial_lut() -> + << + 16#18, 16#00, 16#00, 16#00, 16#00, 16#00, 16#00, 16#00, + 16#00, 16#00, 16#00, 16#00, 16#00, 16#00, 16#00, 16#00, + 16#0F, 16#01, 16#00, 16#00, 16#00, 16#00, 16#00, 16#00, + 16#00, 16#00, 16#00, 16#00, 16#00, 16#00 + >>. + +uc8276_4gray_lut() -> + << + 16#01, 16#0A, 16#1B, 16#0F, 16#03, 16#01, 16#01, + 16#05, 16#0A, 16#01, 16#0A, 16#01, 16#01, 16#01, + 16#05, 16#08, 16#03, 16#02, 16#04, 16#01, 16#01, + 16#01, 16#04, 16#04, 16#02, 16#00, 16#01, 16#01, + 16#01, 16#00, 16#00, 16#00, 16#00, 16#01, 16#01, + 16#01, 16#00, 16#00, 16#00, 16#00, 16#01, 16#01, + 16#01, 16#0A, 16#1B, 16#0F, 16#03, 16#01, 16#01, + 16#05, 16#4A, 16#01, 16#8A, 16#01, 16#01, 16#01, + 16#05, 16#48, 16#03, 16#82, 16#84, 16#01, 16#01, + 16#01, 16#84, 16#84, 16#82, 16#00, 16#01, 16#01, + 16#01, 16#00, 16#00, 16#00, 16#00, 16#01, 16#01, + 16#01, 16#00, 16#00, 16#00, 16#00, 16#01, 16#01, + 16#01, 16#0A, 16#1B, 16#8F, 16#03, 16#01, 16#01, + 16#05, 16#4A, 16#01, 16#8A, 16#01, 16#01, 16#01, + 16#05, 16#48, 16#83, 16#82, 16#04, 16#01, 16#01, + 16#01, 16#04, 16#04, 16#02, 16#00, 16#01, 16#01, + 16#01, 16#00, 16#00, 16#00, 16#00, 16#01, 16#01, + 16#01, 16#00, 16#00, 16#00, 16#00, 16#01, 16#01, + 16#01, 16#8A, 16#1B, 16#8F, 16#03, 16#01, 16#01, + 16#05, 16#4A, 16#01, 16#8A, 16#01, 16#01, 16#01, + 16#05, 16#48, 16#83, 16#02, 16#04, 16#01, 16#01, + 16#01, 16#04, 16#04, 16#02, 16#00, 16#01, 16#01, + 16#01, 16#00, 16#00, 16#00, 16#00, 16#01, 16#01, + 16#01, 16#00, 16#00, 16#00, 16#00, 16#01, 16#01, + 16#01, 16#8A, 16#9B, 16#8F, 16#03, 16#01, 16#01, + 16#05, 16#4A, 16#01, 16#8A, 16#01, 16#01, 16#01, + 16#05, 16#48, 16#03, 16#42, 16#04, 16#01, 16#01, + 16#01, 16#04, 16#04, 16#42, 16#00, 16#01, 16#01, + 16#01, 16#00, 16#00, 16#00, 16#00, 16#01, 16#01, + 16#01, 16#00, 16#00, 16#00, 16#00, 16#01, 16#01, + 16#00, 16#00, 16#00, 16#00, 16#00, 16#00, 16#00, + 16#00, 16#00, 16#00, 16#00, 16#00, 16#00, 16#00, + 16#02, 16#00, 16#00, 16#07, 16#17, 16#41, 16#A8, + 16#32, 16#30 + >>. + epd2in9_v2_partial_lut() -> << 16#00, 16#40, 16#00, 16#00, 16#00, 16#00, 16#00, 16#00, 16#00, 16#00, 16#00, 16#00, diff --git a/atomgl_epaper/src/atomgl_epaper_program.erl b/atomgl_epaper/src/atomgl_epaper_program.erl index 45d8e68..c6a9545 100644 --- a/atomgl_epaper/src/atomgl_epaper_program.erl +++ b/atomgl_epaper/src/atomgl_epaper_program.erl @@ -7,6 +7,7 @@ program/1, cmd/1, cmd/2, + cmd_delay/3, wait_busy/2, reset/3, insert_plane/1, @@ -42,6 +43,9 @@ cmd(Cmd, Data) when is_list(Data) -> cmd(Cmd, Data) when byte_size(Data) =< ?LEN_MASK -> <>. +cmd_delay(Cmd, Data, DelayMs) when byte_size(Data) =< ?LEN_MASK -> + <>. + wait_busy(Level, TimeoutMs) -> meta(?OP_WAIT_BUSY, <>). diff --git a/atomgl_epaper/test/atomgl_epaper_tests.erl b/atomgl_epaper/test/atomgl_epaper_tests.erl index b94e981..9d0866c 100644 --- a/atomgl_epaper/test/atomgl_epaper_tests.erl +++ b/atomgl_epaper/test/atomgl_epaper_tests.erl @@ -75,3 +75,76 @@ invalid_rotated_view_geometry_test() -> {programs, [{init, <<>>}, {full, <<>>}]} ], ?assertError({badmatch, false}, validate_descriptor(Desc)). + +uc8151_panel_descriptor_test_() -> + [ + ?_test(begin + {ok, Desc} = panel("waveshare,epd2in9"), + ?assertEqual(uc8151, get_value(controller, Desc)), + ?assertEqual(128, get_value(native_width, Desc)), + ?assertEqual(296, get_value(native_height, Desc)), + ?assertEqual([full, partial], get_value(refresh_modes, Desc)), + ?assertEqual(30, byte_size(get_value(lut_full, Desc))), + ?assertEqual(30, byte_size(get_value(lut_partial, Desc))) + end), + ?_test(begin + {ok, Desc} = panel("waveshare,epd2in13"), + ?assertEqual(uc8151, get_value(controller, Desc)), + ?assertEqual(122, get_value(native_width, Desc)), + ?assertEqual(250, get_value(native_height, Desc)), + ?assertEqual(250, get_value(view_width, Desc)), + ?assertEqual(122, get_value(view_height, Desc)), + ?assertEqual(30, byte_size(get_value(lut_full, Desc))), + ?assertEqual(30, byte_size(get_value(lut_partial, Desc))) + end) + ]. + +uc8276_panel_descriptor_test_() -> + [ + ?_test(begin + {ok, Desc} = panel("waveshare,epd4in2_V2"), + ?assertEqual(uc8276, get_value(controller, Desc)), + ?assertEqual(400, get_value(native_width, Desc)), + ?assertEqual(300, get_value(native_height, Desc)), + ?assertEqual(300, get_value(view_width, Desc)), + ?assertEqual(400, get_value(view_height, Desc)), + ?assertEqual([full, fast, partial, '4gray'], get_value(refresh_modes, Desc)), + ?assertEqual(233, byte_size(get_value(lut_4gray, Desc))) + end), + ?_test(begin + {ok, Desc} = panel("waveshare,epd4in2_V2-4gray"), + ?assertEqual(uc8276, get_value(controller, Desc)), + ?assertEqual('4gray', get_value(default_refresh, Desc)) + end) + ]. + +acep7_panel_descriptor_test_() -> + [ + ?_test(begin + {ok, Desc} = panel("waveshare,5in65-acep-7c"), + ?assertEqual(acep7, get_value(controller, Desc)), + ?assertEqual(600, get_value(native_width, Desc)), + ?assertEqual(448, get_value(native_height, Desc)), + ?assertEqual(acep7, get_value(palette, Desc)), + ?assertEqual(46, byte_size(get_value(init_seq, Desc))), + ?assertEqual(6, byte_size(get_value(frame_preamble_seq, Desc))), + ?assertEqual(false, get_value(refresh_has_data, Desc)), + ?assertEqual(5, get_value(periodic_refresh_interval, Desc)) + end), + ?_test(begin + {ok, Desc} = panel("good-display/gdep073e01"), + ?assertEqual(acep7, get_value(controller, Desc)), + ?assertEqual(800, get_value(native_width, Desc)), + ?assertEqual(480, get_value(native_height, Desc)), + ?assertEqual(gdep073e01, get_value(palette, Desc)), + ?assertEqual(63, byte_size(get_value(init_seq, Desc))), + ?assertEqual(true, get_value(init_wait_busy_between_cmds, Desc)), + ?assertEqual(true, get_value(refresh_has_data, Desc)), + ?assertEqual(1, get_value(post_power_off_busy_level, Desc)) + end), + ?_test(begin + ?assertError({badmatch, false}, + panel("waveshare,5in65-acep-7c", + #{rotation => 90, view_width => 448, view_height => 600})) + end) + ]. diff --git a/display_driver.c b/display_driver.c index d9a3d5e..3e64d33 100644 --- a/display_driver.c +++ b/display_driver.c @@ -57,10 +57,7 @@ Context *display_create_port(GlobalContext *global, term opts) } Context *ctx = NULL; - if (!strcmp(compat_string, "waveshare,5in65-acep-7c") - || !strcmp(compat_string, "good-display/gdep073e01")) { - ctx = epaper_display_create_port(global, opts); - } else if (!strcmp(compat_string, "sharp,memory-lcd")) { + if (!strcmp(compat_string, "sharp,memory-lcd")) { ctx = memory_lcd_display_create_port(global, opts); } else if (!strcmp(compat_string, "ilitek,ili9341") || !strcmp(compat_string, "ilitek,ili9342c") diff --git a/docs/display-drivers.md b/docs/display-drivers.md index 9442cf3..0e629a7 100644 --- a/docs/display-drivers.md +++ b/docs/display-drivers.md @@ -11,7 +11,7 @@ This document describes how to configure and use the various display drivers sup To use a display with AtomGL, you need: 1. A communication interface (either SPI or I²C) that must be opened and configured -2. A display driver selected by providing a `compatible` string that matches your display model +2. A display driver selected by providing either a `compatible` string or an E-Paper `descriptor` The display driver will handle all the low-level communication and rendering, while you provide display lists describing what should be shown. @@ -272,17 +272,15 @@ sharp_lcd_opts = [ ] ``` -### Waveshare 5.65" ACeP 7-Color (waveshare,5in65-acep-7c) +### ACeP 7-Color E-Paper -600×480 7-color E-Paper display. Driver has software dithering support. - -**Compatible string:** `"waveshare,5in65-acep-7c"` +ACeP 7-color E-Paper displays use Erlang descriptors generated by +`atomgl_epaper`. Driver support includes software dithering. | Option | Type | Description | Default | |--------|------|-------------|---------| | `spi_host` | term | SPI host reference | Required | -| `width` | integer | Display width in pixels | 600 | -| `height` | integer | Display height in pixels | 480 | +| `descriptor` | keyword list | Descriptor returned by `atomgl_epaper` | Required | | `cs` | integer | Chip select GPIO pin | Required | | `dc` | integer | Data/Command GPIO pin | Required | | `reset` | integer | Reset GPIO pin | Required | @@ -292,9 +290,11 @@ sharp_lcd_opts = [ **Example:** ```elixir +{:ok, descriptor} = :atomgl_epaper.panel("waveshare,5in65-acep-7c") + epaper_opts = [ spi_host: spi_host, - compatible: "waveshare,5in65-acep-7c", + descriptor: descriptor, cs: 22, dc: 21, reset: 18, @@ -304,7 +304,7 @@ epaper_opts = [ ### E-Paper Display API -AtomGL provides a data-driven descriptor framework to drive monochrome and 4-gray E-Paper displays. Compatible panel configurations are represented as bytecode descriptors, meaning the same driver binary can adapt to different controllers, native geometry vs. user-facing orientation, byte/bit layouts, and waveform program payloads. +AtomGL provides a data-driven descriptor framework to drive monochrome, 4-gray, and ACeP 7-color E-Paper displays. Panel configurations are represented as bytecode descriptors, meaning the same driver binary can adapt to different controllers, native geometry vs. user-facing orientation, byte/bit layouts, and waveform program payloads. #### Panel Constructors @@ -321,10 +321,13 @@ To obtain a panel descriptor, call `atomgl_epaper:panel/1` or `atomgl_epaper:pan }) ``` -Supported compatibility lookup strings for panels: -- **SSD1680 2.9"**: `"waveshare,epaper-2in9"`, `"waveshare,epd2in9_V2"`, `"waveshare,epd2in9_V2-fast"`, `"waveshare,epd2in9_V2-partial"`, `"waveshare,epd2in9_V2-4gray"`, `"dke,depg0290bns800f6"` +Supported panel lookup strings: +- **UC8151 2.9" / 2.13"**: `"waveshare,epaper-2in9"`, `"waveshare,epd2in9"`, `"waveshare,epd2in13"` +- **UC8276 4.2"**: `"waveshare,epd4in2_V2"`, `"waveshare,epd4in2_V2-fast"`, `"waveshare,epd4in2_V2-partial"`, `"waveshare,epd4in2_V2-4gray"` +- **SSD1680 2.9"**: `"waveshare,epd2in9_V2"`, `"waveshare,epd2in9_V2-fast"`, `"waveshare,epd2in9_V2-partial"`, `"waveshare,epd2in9_V2-4gray"`, `"dke,depg0290bns800f6"` - **SSD1680 2.13"**: `"waveshare,epaper-2in13"`, `"waveshare,epd2in13_V4"`, `"waveshare,epd2in13_V4-fast"`, `"waveshare,epd2in13_V4-partial"` - **JD79656 2.13"**: `"heltec,lcmen2r13efc1"`, `"heltec,icmen2r13efc1"`, `"heltec,ht-vme213"` +- **ACeP 7-color**: `"waveshare,5in65-acep-7c"`, `"good-display/gdep073e01"` ##### Constructor Options @@ -337,6 +340,8 @@ When calling `panel/2`, you can customize: - `reseed_on_timeout` - Reseed previous/current planes with full refresh after BUSY timeouts. - `timing` - Map to adjust driver timeout parameters (`full_expected_ms`, `fast_expected_ms`, `poll_interval_ms`, `timeout_ms`). +ACeP 7-color panel constructors currently support native orientation only. + #### E-Paper Descriptor Schema A completed descriptor is a keyword list with the following shape: @@ -345,7 +350,7 @@ A completed descriptor is a keyword list with the following shape: [ descriptor_version: 2, name: "Waveshare epd2in9_V2 2.9\" e-paper SSD1680 v1", - controller: :ssd16xx, # :ssd16xx, :jd79656, :uc8175, or :acep7 + controller: :ssd16xx, # :ssd16xx, :jd79656, :uc8151, :uc8276, :uc8175, or :acep7 native_width: 128, native_height: 296, view_width: 296, @@ -358,6 +363,8 @@ A completed descriptor is a keyword list with the following shape: polarity: :white_1, # :white_1 or :black_1 refresh_modes: [:full, :fast, :partial, :4gray], default_refresh: :full, + palette: :acep7, # ACeP color descriptors use :acep7, :acep7c, or :gdep073e01 + palette_size: 7, programs: %{ init: <<...>>, # Init bytecode full: <<...>>, # Full refresh waveform/cmd sequence @@ -366,6 +373,13 @@ A completed descriptor is a keyword list with the following shape: sleep: <<...>>, # Sleep command sequence (optional) "4gray": <<...>> # 4-gray refresh sequence (optional) }, + init_seq: <<...>>, # ACeP color init sequence (optional for program descriptors) + init_wait_busy_between_cmds: false, # ACeP color init behavior + frame_preamble_seq: <<...>>, # ACeP color per-frame preamble (optional) + refresh_has_data: false, # ACeP color refresh command behavior + refresh_data_byte: 0, + post_power_off_busy_level: 0, + periodic_refresh_interval: 0, timing: %{ full_expected_ms: 2000, fast_expected_ms: 500, @@ -383,6 +397,8 @@ A completed descriptor is a keyword list with the following shape: ] ``` +Program-driven monochrome and 4-gray descriptors use the `programs` and LUT fields. ACeP 7-color descriptors use the palette and ACeP sequence fields (`init_seq`, `frame_preamble_seq`, and refresh settings) instead. + #### Ghosting Policy E-Paper controllers are highly susceptible to image burn-in and visual ghosting. AtomGL implements a conservative driver-level ghosting policy: @@ -397,7 +413,6 @@ Configure the SPI display port with the descriptor returned by the constructor: ```elixir epaper_opts = [ spi_host: spi_host, - compatible: "waveshare,epd2in9_V2", descriptor: descriptor, cs: 22, dc: 21, diff --git a/epaper_commands.c b/epaper_commands.c index 8195cd7..3bfb25d 100644 --- a/epaper_commands.c +++ b/epaper_commands.c @@ -25,8 +25,6 @@ #include -#include "epaper_color.h" - static void wait_busy_high(int busy_gpio) { while (gpio_get_level(busy_gpio) != 1) { @@ -63,144 +61,3 @@ void epaper_execute_init_seq(struct SPIDCBus *bus, int busy_gpio, } } } - -// --- Built-in init sequences --- -// -// Transcribed mechanically from the original display_spi_init() -// bodies in 5in65_acep_7c_display_driver.c and -// gdep073e01_display_driver.c. _Static_assert on sizeof guards -// against miscounted data bytes. - -// clang-format off - -// Waveshare 5.65" ACeP 7-color. No BUSY polling between commands; a -// single 100 ms delay between VDCS (0x82) and the second CDI (0x50) -// replicates the vTaskDelay(10) at tick rate 100 Hz in the original -// driver. -const uint8_t epaper_init_seq_acep7c[] = { - 0x00, 2, 0xEF, 0x08, // PSR - 0x01, 4, 0x37, 0x00, 0x23, 0x23, // PWRR - 0x03, 1, 0x00, // POFS - 0x06, 3, 0xC7, 0xC7, 0x1D, // BTST - 0x30, 1, 0x3C, // PLL - 0x40, 1, 0x00, // TSE - 0x50, 1, 0x3F, // CDI - 0x60, 1, 0x22, // TCON - 0x61, 4, 0x02, 0x58, 0x01, 0xC0, // TRES (600x448) - 0xE3, 1, 0xAA, // PWS - 0x82, EPAPER_INIT_SEQ_DELAY | 1, 0x80, 100, // VDCS + 100 ms - 0x50, 1, 0x37, // CDI (second) -}; -_Static_assert(sizeof(epaper_init_seq_acep7c) == 46, - "epaper_init_seq_acep7c: miscounted bytes"); -const size_t epaper_init_seq_acep7c_len = sizeof(epaper_init_seq_acep7c); - -// Good Display GDEP073E01 7.3" 7-color. BUSY polling interleaved -// after each command by the executor (wait_busy_between_cmds = true); -// no per-command delay bytes are required. The leading 0xAA entry is -// an undocumented vendor preamble preserved verbatim. -const uint8_t epaper_init_seq_gdep073e01[] = { - 0xAA, 6, 0x49, 0x55, 0x20, 0x08, 0x09, 0x18, // vendor preamble - 0x01, 1, 0x3F, // PWRR - 0x00, 2, 0x5F, 0x69, // PSR - 0x03, 4, 0x00, 0x54, 0x00, 0x44, // POFS - 0x05, 4, 0x40, 0x1F, 0x1F, 0x2C, // BTST1 - 0x06, 4, 0x6F, 0x1F, 0x17, 0x49, // BTST2 - 0x08, 4, 0x6F, 0x1F, 0x1F, 0x22, // BTST3 - 0x30, 1, 0x00, // PLL - 0x50, 1, 0x3F, // CDI - 0x60, 2, 0x02, 0x00, // TCON - 0x61, 4, 0x03, 0x20, 0x01, 0xE0, // TRES (800x480) - 0x84, 1, 0x01, // T_VDCS - 0xE3, 1, 0x2F, // PWS - 0x04, 0, // PON -}; -_Static_assert(sizeof(epaper_init_seq_gdep073e01) == 63, - "epaper_init_seq_gdep073e01: miscounted bytes"); -const size_t epaper_init_seq_gdep073e01_len = sizeof(epaper_init_seq_gdep073e01); - -// --- Per-frame preambles --- -// -// ACeP 5.65" retransmits the resolution command (0x61 + 600x448) -// before every DTM; GoodDisplay's GDEP073E01 does not. The preamble -// uses the same byte-array format as init_seq. - -static const uint8_t epaper_preamble_acep7c[] = { - 0x61, 4, 0x02, 0x58, 0x01, 0xC0, // TRES (600x448) -}; -_Static_assert(sizeof(epaper_preamble_acep7c) == 6, - "epaper_preamble_acep7c: miscounted bytes"); - -// --- Per-panel descriptors --- - -const struct EPaperDesc epaper_desc_acep7c = { - .name = "Waveshare 5.65\" ACeP 7-color", - .native_width = 600, - .native_height = 448, - .view_width = 600, - .view_height = 448, - .rotation = 0, - .spi_clock_hz = 1000000, - - .palette = epaper_acep_palette, - .palette_size = 7, - .controller = EPAPER_CONTROLLER_RAW, - .layout = { - .byte_order = EPAPER_BYTE_ORDER_ROW_MAJOR, - .bit_order = EPAPER_BIT_ORDER_MSB_LEFT, - .polarity = EPAPER_POLARITY_WHITE_IS_1 - }, - .command_target = EPAPER_COMMAND_TARGET_COLOR_PLANE, - - .init_seq = epaper_init_seq_acep7c, - .init_seq_len = sizeof(epaper_init_seq_acep7c), - .init_wait_busy_between_cmds = false, - .use_gpio_pullups = true, - - .frame_preamble_seq = epaper_preamble_acep7c, - .frame_preamble_seq_len = sizeof(epaper_preamble_acep7c), - - .refresh_has_data = false, - .refresh_data_byte = 0x00, - .post_power_off_busy_level = 0, - .busy_idle_level = 1, - - .periodic_refresh_interval = 5, -}; - -const struct EPaperDesc epaper_desc_gdep073e01 = { - .name = "Good Display GDEP073E01 7.3\" 7-color", - .native_width = 800, - .native_height = 480, - .view_width = 800, - .view_height = 480, - .rotation = 0, - .spi_clock_hz = 4000000, - - .palette = epaper_gdep073e01_palette, - .palette_size = 7, - .controller = EPAPER_CONTROLLER_RAW, - .layout = { - .byte_order = EPAPER_BYTE_ORDER_ROW_MAJOR, - .bit_order = EPAPER_BIT_ORDER_MSB_LEFT, - .polarity = EPAPER_POLARITY_WHITE_IS_1 - }, - .command_target = EPAPER_COMMAND_TARGET_COLOR_PLANE, - - .init_seq = epaper_init_seq_gdep073e01, - .init_seq_len = sizeof(epaper_init_seq_gdep073e01), - .init_wait_busy_between_cmds = true, - .use_gpio_pullups = true, - - .frame_preamble_seq = NULL, - .frame_preamble_seq_len = 0, - - .refresh_has_data = true, - .refresh_data_byte = 0x00, - .post_power_off_busy_level = 1, - .busy_idle_level = 1, - - .periodic_refresh_interval = 0, -}; - -// clang-format on diff --git a/epaper_commands.h b/epaper_commands.h index c06f5e7..f400176 100644 --- a/epaper_commands.h +++ b/epaper_commands.h @@ -53,14 +53,6 @@ void epaper_execute_init_seq(struct SPIDCBus *bus, int busy_gpio, const uint8_t *seq, size_t seq_len, bool wait_busy_between_cmds); -// Built-in init sequences. Each array is paired with a size_t -// constant giving its length; callers pass both to -// epaper_execute_init_seq(). -extern const uint8_t epaper_init_seq_acep7c[]; -extern const size_t epaper_init_seq_acep7c_len; -extern const uint8_t epaper_init_seq_gdep073e01[]; -extern const size_t epaper_init_seq_gdep073e01_len; - // --- Per-panel descriptor --- // enum EPaperController @@ -68,6 +60,8 @@ enum EPaperController EPAPER_CONTROLLER_ACEP7, EPAPER_CONTROLLER_SSD16XX, EPAPER_CONTROLLER_JD79656, + EPAPER_CONTROLLER_UC8151, + EPAPER_CONTROLLER_UC8276, EPAPER_CONTROLLER_UC8175 }; @@ -89,13 +83,6 @@ enum EPaperPolarity EPAPER_POLARITY_BLACK_IS_1 }; -enum EPaperCommandTarget -{ - EPAPER_COMMAND_TARGET_CURRENT_PLANE, - EPAPER_COMMAND_TARGET_PREVIOUS_PLANE, - EPAPER_COMMAND_TARGET_COLOR_PLANE -}; - enum EPaperRefreshMode { EPAPER_REFRESH_FULL, @@ -112,7 +99,7 @@ struct EPaperFrameLayout }; // Captures every panel-specific knob so a single unified driver can -// drive multiple controllers by compatible-string dispatch. +// drive multiple controllers by descriptor dispatch. struct EPaperDesc { @@ -130,7 +117,6 @@ struct EPaperDesc enum EPaperController controller; struct EPaperFrameLayout layout; - enum EPaperCommandTarget command_target; // One-time init sequence (format documented above). const uint8_t *init_seq; @@ -187,7 +173,4 @@ struct EPaperDesc enum EPaperRefreshMode default_refresh; }; -extern const struct EPaperDesc epaper_desc_acep7c; -extern const struct EPaperDesc epaper_desc_gdep073e01; - #endif diff --git a/epaper_display_driver.c b/epaper_display_driver.c index 8eb94a9..d3d2ddc 100644 --- a/epaper_display_driver.c +++ b/epaper_display_driver.c @@ -27,6 +27,7 @@ #include #include +#include #include #include #include @@ -62,6 +63,9 @@ static const char *TAG = "epaper_display_driver"; #define SSD1680_LUT_PAYLOAD_LEN 153 #define SSD1680_LUT_WITH_REGS_LEN 159 +#define UC8151_LUT_PAYLOAD_LEN 30 +#define UC8276_LUT_PAYLOAD_LEN 227 +#define UC8276_LUT_WITH_REGS_LEN 233 #define EPAPER_TERM_PROGRAM_COUNT 7 struct EPaperState @@ -85,6 +89,8 @@ struct EpaperDriver const struct EPaperDesc *desc; struct EPaperDesc term_desc; char *term_desc_name; + uint8_t *term_init_seq_bytes; + uint8_t *term_frame_preamble_seq_bytes; uint8_t *term_program_bytes[EPAPER_TERM_PROGRAM_COUNT]; uint8_t *term_lut_bytes[EPAPER_MAX_LUT_SLOTS]; @@ -106,14 +112,6 @@ struct EpaperDriver #define EPAPER_DRIVER_FROM_CTX(ctx) \ CONTAINER_OF((struct DisplayTaskArgs *) (ctx)->platform_data, struct EpaperDriver, display_args) -static const struct { - const char *compat; - const struct EPaperDesc *desc; -} epaper_compat_table[] = { - { "waveshare,5in65-acep-7c", &epaper_desc_acep7c }, - { "good-display/gdep073e01", &epaper_desc_gdep073e01 }, -}; - static const struct EPaperDesc epaper_desc_term_default = { .name = "Erlang e-paper descriptor", .native_width = 128, @@ -130,22 +128,11 @@ static const struct EPaperDesc epaper_desc_term_default = { .bit_order = EPAPER_BIT_ORDER_MSB_LEFT, .polarity = EPAPER_POLARITY_WHITE_IS_1 }, - .command_target = EPAPER_COMMAND_TARGET_CURRENT_PLANE, .use_gpio_pullups = false, .busy_idle_level = 0, .descriptor_version = 2, }; -static const struct EPaperDesc *epaper_desc_for_compatible(const char *compat) -{ - for (size_t i = 0; i < sizeof(epaper_compat_table) / sizeof(epaper_compat_table[0]); i++) { - if (!strcmp(compat, epaper_compat_table[i].compat)) { - return epaper_compat_table[i].desc; - } - } - return NULL; -} - static bool epaper_desc_requires_program(const struct EPaperDesc *desc) { return desc->controller != EPAPER_CONTROLLER_ACEP7; @@ -157,6 +144,8 @@ static bool epaper_parse_descriptor_override(struct EpaperDriver *driver, static void epaper_free_init_failed_driver(struct EpaperDriver *driver, bool spi_device_added); static bool epaper_parse_refresh_mode(term val, Context *ctx, enum EPaperRefreshMode *out); +static bool epaper_parse_palette(term val, Context *ctx, + const uint8_t (**palette)[3], int *palette_size); static void display_reset(struct EpaperDriver *driver) { @@ -228,6 +217,45 @@ static bool epaper_write_lut_bytes(struct EpaperDriver *driver, return false; } + if (driver->desc->controller == EPAPER_CONTROLLER_UC8151) { + if (lut_len < UC8151_LUT_PAYLOAD_LEN) { + ESP_LOGE(TAG, "UC8151 LUT is too short: %u bytes.", (unsigned) lut_len); + return false; + } + spi_dc_write_cmd_data(&driver->bus, 0x32, lut, UC8151_LUT_PAYLOAD_LEN); + return true; + } + + if (driver->desc->controller == EPAPER_CONTROLLER_UC8276) { + if (lut_len < UC8276_LUT_PAYLOAD_LEN) { + ESP_LOGE(TAG, "UC8276 LUT is too short: %u bytes.", (unsigned) lut_len); + return false; + } + spi_dc_write_cmd_data(&driver->bus, 0x32, lut, UC8276_LUT_PAYLOAD_LEN); + + if (lut_len < UC8276_LUT_WITH_REGS_LEN) { + return true; + } + + spi_dc_write_command(&driver->bus, 0x3F); + spi_dc_write_data(&driver->bus, lut[227]); + spi_dc_write_command(&driver->bus, 0x03); + spi_dc_write_data(&driver->bus, lut[228]); + spi_dc_write_command(&driver->bus, 0x04); + spi_dc_write_data(&driver->bus, lut[229]); + spi_dc_write_data(&driver->bus, lut[230]); + spi_dc_write_data(&driver->bus, lut[231]); + spi_dc_write_command(&driver->bus, 0x2C); + spi_dc_write_data(&driver->bus, lut[232]); + return true; + } + + if (driver->desc->controller != EPAPER_CONTROLLER_SSD16XX) { + ESP_LOGE(TAG, "Controller %d does not support LUT insertion.", + driver->desc->controller); + return false; + } + if (lut_len < SSD1680_LUT_PAYLOAD_LEN) { ESP_LOGE(TAG, "SSD1680 LUT is too short: %u bytes.", (unsigned) lut_len); return false; @@ -561,9 +589,15 @@ static bool build_native_4gray_plane(struct EpaperDriver *driver, for (xpos = 0; xpos < driver->mono_screen.w; xpos++) { uint8_t gray = get_epaper_packed_pixel(gray_buf, xpos); - if (gray & (1 << plane_bit)) { - // Palette index bits select the SSD1680 two-plane gray class: - // 0=white, 1=light gray, 2=dark gray, 3=black. + bool bit_value; + if (driver->desc->controller == EPAPER_CONTROLLER_UC8276) { + // UC8276 4-gray expects inverted two-bit gray planes: + // plane 0 carries the high bit, plane 1 carries the low bit. + bit_value = (gray & (plane_bit == 0 ? 0x02 : 0x01)) == 0; + } else { + bit_value = (gray & (1 << plane_bit)) != 0; + } + if (bit_value) { int nx; int ny; if (!epaper_view_to_native(driver->desc, xpos, vy, &nx, &ny)) { @@ -663,7 +697,9 @@ static bool epaper_program_insert_plane(void *ctx, uint8_t plane_id) switch (plane_id) { case 0: - if (driver->desc->controller == EPAPER_CONTROLLER_SSD16XX && run->is_4gray) { + if ((driver->desc->controller == EPAPER_CONTROLLER_SSD16XX + || driver->desc->controller == EPAPER_CONTROLLER_UC8276) + && run->is_4gray) { if (run->gray_buf == NULL) { return false; } @@ -676,7 +712,9 @@ static bool epaper_program_insert_plane(void *ctx, uint8_t plane_id) return write_mono_plane(driver, run->plane_buf, run->line_buf, run->items, run->items_len, true, capture_frame); case 1: - if (driver->desc->controller == EPAPER_CONTROLLER_SSD16XX && run->is_4gray) { + if ((driver->desc->controller == EPAPER_CONTROLLER_SSD16XX + || driver->desc->controller == EPAPER_CONTROLLER_UC8276) + && run->is_4gray) { if (run->gray_buf == NULL) { return false; } @@ -1187,6 +1225,10 @@ static void epaper_free_descriptor_override(struct EpaperDriver *driver) { free(driver->term_desc_name); driver->term_desc_name = NULL; + free(driver->term_init_seq_bytes); + driver->term_init_seq_bytes = NULL; + free(driver->term_frame_preamble_seq_bytes); + driver->term_frame_preamble_seq_bytes = NULL; for (int i = 0; i < EPAPER_TERM_PROGRAM_COUNT; i++) { free(driver->term_program_bytes[i]); @@ -1236,6 +1278,14 @@ static bool epaper_parse_controller(term val, Context *ctx, enum EPaperControlle *out = EPAPER_CONTROLLER_JD79656; return true; } + if (val == context_make_atom(ctx, ATOM_STR("\x6", "uc8151"))) { + *out = EPAPER_CONTROLLER_UC8151; + return true; + } + if (val == context_make_atom(ctx, ATOM_STR("\x6", "uc8276"))) { + *out = EPAPER_CONTROLLER_UC8276; + return true; + } if (val == context_make_atom(ctx, ATOM_STR("\x6", "uc8175"))) { *out = EPAPER_CONTROLLER_UC8175; return true; @@ -1306,6 +1356,23 @@ static bool epaper_parse_refresh_mode(term val, Context *ctx, enum EPaperRefresh return false; } +static bool epaper_parse_palette(term val, Context *ctx, + const uint8_t (**palette)[3], int *palette_size) +{ + if (val == context_make_atom(ctx, ATOM_STR("\x5", "acep7")) + || val == context_make_atom(ctx, ATOM_STR("\x6", "acep7c"))) { + *palette = epaper_acep_palette; + *palette_size = 7; + return true; + } + if (val == context_make_atom(ctx, ATOM_STR("\xA", "gdep073e01"))) { + *palette = epaper_gdep073e01_palette; + *palette_size = 7; + return true; + } + return false; +} + static bool epaper_validate_descriptor_geometry(const struct EPaperDesc *desc) { if (desc->native_width <= 0 || desc->native_height <= 0 @@ -1316,6 +1383,17 @@ static bool epaper_validate_descriptor_geometry(const struct EPaperDesc *desc) return false; } + if (desc->controller == EPAPER_CONTROLLER_ACEP7 + && (desc->rotation != 0 + || desc->view_width != desc->native_width + || desc->view_height != desc->native_height)) { + ESP_LOGE(TAG, + "ACeP e-paper descriptors only support native orientation: native=%dx%d view=%dx%d rotation=%d.", + desc->native_width, desc->native_height, + desc->view_width, desc->view_height, desc->rotation); + return false; + } + switch (desc->rotation) { case 0: case 180: @@ -1358,6 +1436,8 @@ static bool epaper_parse_descriptor_override(struct EpaperDriver *driver, memset(driver->term_program_bytes, 0, sizeof(driver->term_program_bytes)); memset(driver->term_lut_bytes, 0, sizeof(driver->term_lut_bytes)); + driver->term_init_seq_bytes = NULL; + driver->term_frame_preamble_seq_bytes = NULL; driver->term_desc_name = NULL; driver->term_desc = *driver->desc; @@ -1420,6 +1500,19 @@ static bool epaper_parse_descriptor_override(struct EpaperDriver *driver, return false; } + term palette_term = interop_kv_get_value_default( + descriptor, ATOM_STR("\x7", "palette"), term_nil(), ctx->global); + if (palette_term != term_nil()) { + const uint8_t (*palette)[3]; + int palette_size; + if (!epaper_parse_palette(palette_term, ctx, &palette, &palette_size)) { + ESP_LOGE(TAG, "Invalid palette in e-paper descriptor."); + return false; + } + driver->term_desc.palette = palette; + driver->term_desc.palette_size = palette_size; + } + // Parse Controller term controller_term = interop_kv_get_value_default(descriptor, ATOM_STR("\xA", "controller"), term_nil(), ctx->global); if (controller_term != term_nil()) { @@ -1493,6 +1586,36 @@ static bool epaper_parse_descriptor_override(struct EpaperDriver *driver, } } + if (!epaper_copy_binary_field(descriptor, ctx, ATOM_STR("\x8", "init_seq"), + &driver->term_init_seq_bytes, + &driver->term_desc.init_seq, &driver->term_desc.init_seq_len) + || !epaper_copy_binary_field(descriptor, ctx, ATOM_STR("\x12", "frame_preamble_seq"), + &driver->term_frame_preamble_seq_bytes, + &driver->term_desc.frame_preamble_seq, &driver->term_desc.frame_preamble_seq_len)) { + ESP_LOGE(TAG, "Invalid e-paper ACeP sequence binary."); + return false; + } + + int refresh_data_byte = driver->term_desc.refresh_data_byte; + if (!epaper_parse_bool_field(descriptor, ctx, ATOM_STR("\x1B", "init_wait_busy_between_cmds"), + &driver->term_desc.init_wait_busy_between_cmds) + || !epaper_parse_bool_field(descriptor, ctx, ATOM_STR("\x10", "refresh_has_data"), + &driver->term_desc.refresh_has_data) + || !epaper_parse_int_field(descriptor, ctx, ATOM_STR("\x11", "refresh_data_byte"), + &refresh_data_byte) + || !epaper_parse_int_field(descriptor, ctx, ATOM_STR("\x19", "post_power_off_busy_level"), + &driver->term_desc.post_power_off_busy_level) + || !epaper_parse_int_field(descriptor, ctx, ATOM_STR("\x19", "periodic_refresh_interval"), + &driver->term_desc.periodic_refresh_interval)) { + ESP_LOGE(TAG, "Invalid ACeP e-paper descriptor field."); + return false; + } + if (refresh_data_byte < 0 || refresh_data_byte > UINT8_MAX) { + ESP_LOGE(TAG, "Invalid refresh_data_byte in e-paper descriptor."); + return false; + } + driver->term_desc.refresh_data_byte = (uint8_t) refresh_data_byte; + // Parse Timing Map term timing_map = interop_kv_get_value_default(descriptor, ATOM_STR("\x6", "timing"), term_nil(), ctx->global); if (timing_map != term_nil()) { @@ -1541,9 +1664,21 @@ static bool epaper_parse_descriptor_override(struct EpaperDriver *driver, } // Setup 4-gray palette if relevant - if (driver->term_desc.controller == EPAPER_CONTROLLER_SSD16XX && driver->term_desc.palette_size == 4) { + if ((driver->term_desc.controller == EPAPER_CONTROLLER_SSD16XX + || driver->term_desc.controller == EPAPER_CONTROLLER_UC8276) + && driver->term_desc.palette_size == 4) { driver->term_desc.palette = epaper_ssd1680_4gray_palette; } + if (driver->term_desc.controller == EPAPER_CONTROLLER_ACEP7 && driver->term_desc.palette == NULL) { + ESP_LOGE(TAG, "ACeP e-paper descriptor requires a known color palette."); + return false; + } + if (driver->term_desc.controller == EPAPER_CONTROLLER_ACEP7 + && driver->term_desc.init.bytes == NULL + && driver->term_desc.init_seq == NULL) { + ESP_LOGE(TAG, "ACeP e-paper descriptor requires an init program or init_seq."); + return false; + } if (!epaper_validate_descriptor_geometry(&driver->term_desc)) { return false; @@ -1559,25 +1694,12 @@ static void display_spi_init(Context *ctx, term opts) term descriptor_term = interop_kv_get_value_default( opts, ATOM_STR("\xA", "descriptor"), term_nil(), ctx->global); - // Without an explicit descriptor, resolve compatible string to one - // of the remaining C-side table-driven displays. - term compat_term = interop_kv_get_value_default( - opts, ATOM_STR("\xA", "compatible"), term_nil(), ctx->global); - int str_ok; - char *compat_string = interop_term_to_string(compat_term, &str_ok); - const struct EPaperDesc *desc = NULL; - if (descriptor_term != term_nil()) { - desc = &epaper_desc_term_default; - } else if (str_ok && compat_string) { - desc = epaper_desc_for_compatible(compat_string); - } - if (!desc) { - ESP_LOGE(TAG, "Failed init: unknown or missing compatible '%s'.", - compat_string ? compat_string : "(null)"); - free(compat_string); + if (descriptor_term == term_nil()) { + ESP_LOGE(TAG, "Failed init: missing e-paper descriptor."); return; } - free(compat_string); + + const struct EPaperDesc *desc = &epaper_desc_term_default; struct EpaperDriver *driver = calloc(1, sizeof(struct EpaperDriver)); if (UNLIKELY(!driver)) { @@ -1600,7 +1722,7 @@ static void display_spi_init(Context *ctx, term opts) } if (epaper_desc_requires_program(desc) && desc->program_full.bytes == NULL) { ESP_LOGE(TAG, - "Failed init: SSD1680 compatible '%s' requires a descriptor refresh program.", + "Failed init: e-paper descriptor '%s' requires a full refresh program.", desc->name); epaper_free_init_failed_driver(driver, false); return; From 690d795dab7ba30e002de7f8f5fe1300919e9df1 Mon Sep 17 00:00:00 2001 From: Peter M Date: Mon, 25 May 2026 17:30:14 +0200 Subject: [PATCH 3/6] epaper: add sleep and refresh lifecycle controls Introduce version 3 descriptors with named low-power modes, wake policies, and after-wake refresh handling. Add sleep, standby, and deep-sleep programs for supported panels and preserve host previous-frame state when permitted. Validate advertised refresh modes and expose info, capabilities, and wait_idle calls. Document the lifecycle API and add descriptor coverage. Signed-off-by: Peter M --- atomgl_epaper/ADD_PANEL.md | 8 +- atomgl_epaper/CUSTOM_PANEL.md | 13 +- .../src/atomgl_epaper_descriptor.erl | 66 +- atomgl_epaper/src/atomgl_epaper_panels.erl | 78 +- atomgl_epaper/test/atomgl_epaper_tests.erl | 104 ++- docs/display-drivers.md | 63 +- docs/epaper-low-power-specs.md | 64 ++ epaper_commands.h | 53 +- epaper_display_driver.c | 801 ++++++++++++++++-- 9 files changed, 1155 insertions(+), 95 deletions(-) create mode 100644 docs/epaper-low-power-specs.md diff --git a/atomgl_epaper/ADD_PANEL.md b/atomgl_epaper/ADD_PANEL.md index eb0f918..e557bda 100644 --- a/atomgl_epaper/ADD_PANEL.md +++ b/atomgl_epaper/ADD_PANEL.md @@ -49,7 +49,8 @@ Collect these facts for the target panel: - RAM data commands for old/current planes or color planes. - Frame layout: row-major or column-major, MSB or LSB first. - Pixel polarity: whether `1` means white or black. -- Refresh modes: full, fast, partial, 4-gray, color, sleep. +- Refresh modes: full, fast, partial, 4-gray, and color; sleep modes are + described separately via `sleep_modes`. - LUT format and whether extra register bytes accompany the LUT payload. - Any controller-specific plane mapping, palette, or ghosting constraints. @@ -99,7 +100,7 @@ Important local paths: - Optional aliases for mode defaults, such as `-fast`, `-partial`, or `-4gray`. - Native geometry, palette size, refresh modes, and default refresh mode. - - Init, refresh, partial, 4-gray, LUT, and sleep programs as needed. + - Init, refresh, partial, 4-gray, LUT, and `sleep_modes` programs as needed. - Helper functions only when they reduce duplication or make the sequence easier to audit. @@ -143,7 +144,8 @@ available in your environment. If hardware is available, verify: - Full refresh clears and draws correctly. - Fast or partial refresh updates the expected window without stale-plane artifacts. -- Sleep and wake work repeatedly. +- Standby/sleep/deep-sleep and wake work repeatedly for every exposed + `sleep_modes` entry. - 4-gray or color palettes map white, gray/color levels, and black correctly. - Timeout behavior matches the panel BUSY polarity. diff --git a/atomgl_epaper/CUSTOM_PANEL.md b/atomgl_epaper/CUSTOM_PANEL.md index 4044ba5..30bda17 100644 --- a/atomgl_epaper/CUSTOM_PANEL.md +++ b/atomgl_epaper/CUSTOM_PANEL.md @@ -66,11 +66,22 @@ descriptor(Opts) -> init => init(), full => full_refresh(), partial => partial_refresh(), - sleep => cmd(16#10, <<16#01>>), + sleep_modes => [ + {sleep, [ + {enter, cmd(16#10, <<16#01>>)}, + {wake, reset_init}, + {controller_ram, unknown}, + {host_prev_frame, preserve}, + {after_wake_refresh, allow_if_program_reseeds} + ]} + ], lut_full => wf_full_1in54(), lut_partial => wf_partial_1in54() }, Opts)). +%% `allow_if_program_reseeds` allows fast/partial after wake only when the +%% selected refresh program includes insert_prev_frame/1. + init() -> program([ reset(200, 2, 200), diff --git a/atomgl_epaper/src/atomgl_epaper_descriptor.erl b/atomgl_epaper/src/atomgl_epaper_descriptor.erl index 7b2cb0d..b404be1 100644 --- a/atomgl_epaper/src/atomgl_epaper_descriptor.erl +++ b/atomgl_epaper/src/atomgl_epaper_descriptor.erl @@ -3,6 +3,8 @@ -module(atomgl_epaper_descriptor). +-define(MAX_SLEEP_MODES, 8). + -export([ ssd16xx_panel/2, jd79656_panel/2, @@ -32,7 +34,7 @@ acep7_panel(Name, Config) -> ViewH = maps:get(view_height, Config, NativeH), Descriptor = [ - {descriptor_version, 2}, + {descriptor_version, 3}, {name, Name}, {controller, acep7}, {native_width, NativeW}, @@ -54,7 +56,8 @@ acep7_panel(Name, Config) -> {refresh_has_data, maps:get(refresh_has_data, Config, false)}, {refresh_data_byte, maps:get(refresh_data_byte, Config, 0)}, {post_power_off_busy_level, maps:get(post_power_off_busy_level, Config, 0)}, - {periodic_refresh_interval, maps:get(periodic_refresh_interval, Config, 0)} + {periodic_refresh_interval, maps:get(periodic_refresh_interval, Config, 0)}, + {sleep_modes, maps:get(sleep_modes, Config, [])} ] ++ case maps:find(frame_preamble_seq, Config) of {ok, FramePreambleSeq} -> [{frame_preamble_seq, FramePreambleSeq}]; error -> [] @@ -103,7 +106,7 @@ build_panel(Controller, Name, Config) -> end, Descriptor = [ - {descriptor_version, 2}, + {descriptor_version, 3}, {name, Name}, {controller, Controller}, {native_width, NativeW}, @@ -120,6 +123,7 @@ build_panel(Controller, Name, Config) -> {default_refresh, DefaultRefresh}, {palette_size, PaletteSize}, {programs, Programs}, + {sleep_modes, maps:get(sleep_modes, Config, [])}, {timing, Timing}, {ghosting, Ghosting} ] ++ [L || L = {_, Bin} <- Luts, Bin =/= <<>>], @@ -144,15 +148,11 @@ mode_programs(Config) -> {ok, Partial} -> [{partial, Partial} | P3]; error -> P3 end, - P5 = case maps:find(sleep, Config) of - {ok, Sleep} -> [{sleep, Sleep} | P4]; + P5 = case maps:find('4gray', Config) of + {ok, FourGray} -> [{'4gray', FourGray} | P4]; error -> P4 end, - P6 = case maps:find('4gray', Config) of - {ok, FourGray} -> [{'4gray', FourGray} | P5]; - error -> P5 - end, - P6. + P5. timing(Config) -> [ @@ -164,7 +164,7 @@ timing(Config) -> validate_descriptor(Desc) -> Version = get_value(descriptor_version, Desc), - true = (Version == 2), + true = (Version == 3), NativeW = get_value(native_width, Desc), NativeH = get_value(native_height, Desc), @@ -205,7 +205,9 @@ validate_descriptor(Desc) -> DefaultRefresh = get_value(default_refresh, Desc), RefreshModes = get_value(refresh_modes, Desc), + validate_refresh_modes(RefreshModes), true = lists:member(DefaultRefresh, RefreshModes), + validate_sleep_modes(get_value(sleep_modes, Desc, [])), case Controller of acep7 -> @@ -235,6 +237,48 @@ validate_descriptor(Desc) -> {ok, Desc}. +validate_refresh_modes(RefreshModes) when is_list(RefreshModes) -> + true = (RefreshModes =/= []), + true = lists:member(full, RefreshModes), + true = (length(RefreshModes) == length(lists:usort(RefreshModes))), + lists:foreach(fun(Mode) -> + true = lists:member(Mode, [full, fast, partial, '4gray']) + end, RefreshModes); +validate_refresh_modes(_) -> + error(bad_refresh_modes). + +validate_sleep_modes(SleepModes) when is_list(SleepModes) -> + true = (length(SleepModes) =< ?MAX_SLEEP_MODES), + Modes = lists:map(fun + ({Mode, _Props}) when is_atom(Mode) -> Mode; + (_) -> error(bad_sleep_mode) + end, SleepModes), + true = (length(Modes) == length(lists:usort(Modes))), + lists:foreach(fun validate_sleep_mode/1, SleepModes); +validate_sleep_modes(_) -> + error(bad_sleep_modes). + +validate_sleep_mode({Mode, Props}) when is_atom(Mode), is_list(Props) -> + Enter = get_value(enter, Props), + true = is_binary(Enter), + true = (byte_size(Enter) =< 4096), + Wake = get_value(wake, Props, reset_init), + true = (is_binary(Wake) orelse lists:member(Wake, [init, reset_init])), + case Wake of + Bin when is_binary(Bin) -> + true = (byte_size(Bin) =< 4096); + _ -> + ok + end, + true = lists:member(get_value(controller_ram, Props, unknown), + [unknown, retained, lost]), + true = lists:member(get_value(host_prev_frame, Props, invalidate), + [preserve, invalidate]), + true = lists:member(get_value(after_wake_refresh, Props, full), + [allow, full, allow_if_program_reseeds]); +validate_sleep_mode(_) -> + error(bad_sleep_mode). + get_value(Key, List) -> get_value(Key, List, undefined). diff --git a/atomgl_epaper/src/atomgl_epaper_panels.erl b/atomgl_epaper/src/atomgl_epaper_panels.erl index 530977b..167efd7 100644 --- a/atomgl_epaper/src/atomgl_epaper_panels.erl +++ b/atomgl_epaper/src/atomgl_epaper_panels.erl @@ -47,7 +47,7 @@ panel("waveshare,epd2in9", Opts) -> init => uc8151_init(296, 16#03, ?LUT_FULL), full => uc8151_refresh(128, 296, ?LUT_FULL), partial => uc8151_refresh(128, 296, ?LUT_PARTIAL), - sleep => cmd(16#10, <<16#01>>), + sleep_modes => controller_sleep_modes(sleep_program(0)), lut_full => uc8151_2in9_full_lut(), lut_partial => uc8151_2in9_partial_lut() }, Opts)); @@ -63,7 +63,7 @@ panel("waveshare,epd2in9_V2", Opts) -> fast => epd2in9_v2_refresh(?LUT_FAST, 16#C7), partial => epd2in9_v2_partial_refresh(), '4gray' => epd2in9_v2_4gray_refresh(), - sleep => cmd(16#10, <<16#01>>), + sleep_modes => ssd16xx_sleep_modes(100), lut_full => epd2in9_v2_default_lut(), lut_fast => epd2in9_v2_fast_lut(), lut_partial => epd2in9_v2_partial_lut(), @@ -83,7 +83,7 @@ panel("dke,depg0290bns800f6", Opts) -> default_refresh => full, init => ssd1680_2in9_init(), full => ssd1680_2in9_refresh(), - sleep => cmd(16#10, <<16#01>>) + sleep_modes => ssd16xx_sleep_modes(0) }, Opts)); panel("waveshare,epaper-2in13", Opts) -> panel("waveshare,epd2in13_V4", Opts); @@ -96,7 +96,7 @@ panel("waveshare,epd2in13", Opts) -> init => uc8151_init(250, 16#63, ?LUT_FULL), full => uc8151_refresh(122, 250, ?LUT_FULL), partial => uc8151_refresh(122, 250, ?LUT_PARTIAL), - sleep => cmd(16#10, <<16#01>>), + sleep_modes => controller_sleep_modes(sleep_program(0)), lut_full => uc8151_2in13_full_lut(), lut_partial => uc8151_2in13_partial_lut() }, Opts)); @@ -110,7 +110,7 @@ panel("waveshare,epd2in13_V4", Opts) -> full => epd2in13_v4_refresh(16#F7), fast => epd2in13_v4_refresh(16#C7), partial => epd2in13_v4_partial_refresh(), - sleep => cmd(16#10, <<16#01>>) + sleep_modes => ssd16xx_sleep_modes(100) }, Opts)); panel("waveshare,epd2in13_V4-fast", Opts) -> panel("waveshare,epd2in13_V4", maps:merge(#{default_refresh => fast}, Opts)); @@ -128,7 +128,7 @@ panel("waveshare,epd4in2_V2", Opts) -> fast => uc8276_refresh(uc8276_fast_prelude(16#6E), 16#C7), partial => uc8276_partial_refresh(), '4gray' => uc8276_4gray_refresh(), - sleep => cmd(16#10, <<16#01>>), + sleep_modes => controller_sleep_modes(sleep_program(200)), lut_4gray => uc8276_4gray_lut() }, Opts)); panel("waveshare,epd4in2_V2-fast", Opts) -> @@ -145,7 +145,7 @@ panel("heltec,lcmen2r13efc1", Opts) -> default_refresh => full, init => heltec_lcmen2r13efc1_init(), full => heltec_lcmen2r13efc1_refresh(), - sleep => program([cmd(16#02), wait_busy(1, 5000)]) + sleep_modes => standby_sleep_modes(program([cmd(16#02), wait_busy(1, 5000)])) }, Opts)); panel("heltec,icmen2r13efc1", Opts) -> panel("heltec,lcmen2r13efc1", Opts); @@ -165,7 +165,8 @@ panel("waveshare,5in65-acep-7c", Opts) -> refresh_has_data => false, refresh_data_byte => 0, post_power_off_busy_level => 0, - periodic_refresh_interval => 5 + periodic_refresh_interval => 5, + sleep_modes => acep7_sleep_modes(0, none) }, Opts)); panel("good-display/gdep073e01", Opts) -> acep7_panel("Good Display GDEP073E01 7.3\" 7-color", maps:merge(#{ @@ -180,11 +181,70 @@ panel("good-display/gdep073e01", Opts) -> refresh_has_data => true, refresh_data_byte => 0, post_power_off_busy_level => 1, - periodic_refresh_interval => 0 + periodic_refresh_interval => 0, + sleep_modes => acep7_sleep_modes(1, <<16#00>>) }, Opts)); panel(_, _Opts) -> error. +ssd16xx_sleep_modes(DelayMs) -> + [ + {sleep, sleep_mode(sleep_program(DelayMs), reset_init, + retained, preserve, allow_if_program_reseeds)}, + {deep_sleep, sleep_mode(deep_sleep_program(DelayMs), reset_init, + lost, preserve, allow_if_program_reseeds)} + ]. + +controller_sleep_modes(EnterProgram) -> + [ + {sleep, sleep_mode(EnterProgram, reset_init, + unknown, preserve, allow_if_program_reseeds)} + ]. + +standby_sleep_modes(EnterProgram) -> + [ + {standby, sleep_mode(EnterProgram, reset_init, + unknown, invalidate, full)} + ]. + +acep7_sleep_modes(BusyLevel, PowerOffData) -> + [ + {standby, sleep_mode(acep7_power_off_program(BusyLevel, PowerOffData), + init, unknown, invalidate, full)}, + {deep_sleep, sleep_mode(acep7_deep_sleep_program(BusyLevel, PowerOffData), + reset_init, lost, invalidate, full)} + ]. + +sleep_mode(Enter, Wake, ControllerRam, HostPrevFrame, AfterWakeRefresh) -> + [ + {enter, Enter}, + {wake, Wake}, + {controller_ram, ControllerRam}, + {host_prev_frame, HostPrevFrame}, + {after_wake_refresh, AfterWakeRefresh} + ]. + +sleep_program(0) -> + cmd(16#10, <<16#01>>); +sleep_program(DelayMs) -> + program([cmd(16#10, <<16#01>>), delay_ms(DelayMs)]). + +deep_sleep_program(0) -> + cmd(16#10, <<16#03>>); +deep_sleep_program(DelayMs) -> + program([cmd(16#10, <<16#03>>), delay_ms(DelayMs)]). + +acep7_power_off_program(BusyLevel, PowerOffData) -> + program(acep7_power_off_steps(BusyLevel, PowerOffData)). + +acep7_deep_sleep_program(BusyLevel, PowerOffData) -> + program([acep7_power_off_steps(BusyLevel, PowerOffData), cmd(16#07, <<16#A5>>)]). + +acep7_power_off_steps(BusyLevel, none) -> + [cmd(16#02), wait_busy(BusyLevel, 5000)]; +acep7_power_off_steps(BusyLevel, PowerOffData) -> + [cmd(16#02, PowerOffData), wait_busy(BusyLevel, 5000)]. + uc8151_init(NativeHeight, BorderWaveform, LutSlot) -> DriverOutput = NativeHeight - 1, Border = case BorderWaveform of diff --git a/atomgl_epaper/test/atomgl_epaper_tests.erl b/atomgl_epaper/test/atomgl_epaper_tests.erl index 9d0866c..f22b384 100644 --- a/atomgl_epaper/test/atomgl_epaper_tests.erl +++ b/atomgl_epaper/test/atomgl_epaper_tests.erl @@ -46,7 +46,7 @@ orientation_geometry_test_() -> invalid_rotation_descriptor_test() -> Desc = [ - {descriptor_version, 2}, + {descriptor_version, 3}, {controller, ssd16xx}, {native_width, 128}, {native_height, 296}, @@ -62,7 +62,7 @@ invalid_rotation_descriptor_test() -> invalid_rotated_view_geometry_test() -> Desc = [ - {descriptor_version, 2}, + {descriptor_version, 3}, {controller, ssd16xx}, {native_width, 128}, {native_height, 296}, @@ -76,6 +76,34 @@ invalid_rotated_view_geometry_test() -> ], ?assertError({badmatch, false}, validate_descriptor(Desc)). +invalid_refresh_modes_descriptor_test_() -> + BaseDesc = [ + {descriptor_version, 3}, + {controller, ssd16xx}, + {native_width, 128}, + {native_height, 296}, + {view_width, 128}, + {view_height, 296}, + {rotation, 0}, + {frame_layout, row_msb}, + {default_refresh, full}, + {programs, [{init, <<>>}, {full, <<>>}, {partial, <<>>}]} + ], + [ + ?_test(begin + ?assertError({badmatch, false}, + validate_descriptor([{refresh_modes, [full, partial, partial]} | BaseDesc])) + end), + ?_test(begin + ?assertError({badmatch, false}, + validate_descriptor([{refresh_modes, [partial]} | BaseDesc])) + end), + ?_test(begin + ?assertError({badmatch, false}, + validate_descriptor([{refresh_modes, [full, turbo]} | BaseDesc])) + end) + ]. + uc8151_panel_descriptor_test_() -> [ ?_test(begin @@ -118,6 +146,78 @@ uc8276_panel_descriptor_test_() -> end) ]. +sleep_modes_descriptor_test_() -> + [ + ?_test(begin + {ok, Desc} = panel("waveshare,epd2in9_V2"), + ?assertEqual(3, get_value(descriptor_version, Desc)), + SleepModes = get_value(sleep_modes, Desc), + Sleep = get_value(sleep, SleepModes), + DeepSleep = get_value(deep_sleep, SleepModes), + ?assertEqual(<<16#10, 1, 16#01, 16#10, 16#C1, 0, 100>>, + get_value(enter, Sleep)), + ?assertEqual(retained, get_value(controller_ram, Sleep)), + ?assertEqual(preserve, get_value(host_prev_frame, Sleep)), + ?assertEqual(allow_if_program_reseeds, get_value(after_wake_refresh, Sleep)), + ?assertEqual(<<16#10, 1, 16#03, 16#10, 16#C1, 0, 100>>, + get_value(enter, DeepSleep)), + ?assertEqual(lost, get_value(controller_ram, DeepSleep)) + end), + ?_test(begin + {ok, Desc} = panel("waveshare,epd4in2_V2"), + SleepModes = get_value(sleep_modes, Desc), + Sleep = get_value(sleep, SleepModes), + ?assertEqual(<<16#10, 1, 16#01, 16#10, 16#C1, 0, 200>>, + get_value(enter, Sleep)) + end), + ?_test(begin + {ok, Desc} = panel("good-display/gdep073e01"), + SleepModes = get_value(sleep_modes, Desc), + DeepSleep = get_value(deep_sleep, SleepModes), + ?assertEqual( + <<16#02, 1, 16#00, 16#01, 16#43, 1, 136, 19, 16#07, 1, 16#A5>>, + get_value(enter, DeepSleep)), + ?assertEqual(reset_init, get_value(wake, DeepSleep)), + ?assertEqual(full, get_value(after_wake_refresh, DeepSleep)) + end), + ?_test(begin + CustomModes = [ + {lab, [ + {enter, <<1, 2, 3>>}, + {wake, init}, + {controller_ram, unknown}, + {host_prev_frame, invalidate}, + {after_wake_refresh, full} + ]} + ], + {ok, Desc} = panel("waveshare,epd2in9_V2", #{sleep_modes => CustomModes}), + ?assertEqual(CustomModes, get_value(sleep_modes, Desc)) + end), + ?_test(begin + DuplicateModes = [ + {sleep, [{enter, <<1>>}]}, + {sleep, [{enter, <<2>>}]} + ], + ?assertError({badmatch, false}, + panel("waveshare,epd2in9_V2", #{sleep_modes => DuplicateModes})) + end), + ?_test(begin + TooManyModes = [ + {mode1, [{enter, <<1>>}]}, + {mode2, [{enter, <<2>>}]}, + {mode3, [{enter, <<3>>}]}, + {mode4, [{enter, <<4>>}]}, + {mode5, [{enter, <<5>>}]}, + {mode6, [{enter, <<6>>}]}, + {mode7, [{enter, <<7>>}]}, + {mode8, [{enter, <<8>>}]}, + {mode9, [{enter, <<9>>}]} + ], + ?assertError({badmatch, false}, + panel("waveshare,epd2in9_V2", #{sleep_modes => TooManyModes})) + end) + ]. + acep7_panel_descriptor_test_() -> [ ?_test(begin diff --git a/docs/display-drivers.md b/docs/display-drivers.md index 0e629a7..3ede38a 100644 --- a/docs/display-drivers.md +++ b/docs/display-drivers.md @@ -335,6 +335,7 @@ When calling `panel/2`, you can customize: - `orientation` - `:landscape`, `:landscape_left`, `:landscape_right`, `:portrait`, or `:portrait_flipped` (automatically maps native coordinates to view width/height and sets rotation). `:landscape` is a compatibility alias for `:landscape_left`. - `default_refresh` - The refresh mode used when no frame option is specified. - `refresh_modes` - List of allowed refresh modes (e.g., `[:full, :fast, :partial]`). +- `sleep_modes` - Override the panel's low-power mode descriptors for experiments or panel-specific tuning. - `ghosting` - Map with ghosting settings: - `max_fast_refreshes` - Maximum successive fast updates before a full refresh is forced. - `reseed_on_timeout` - Reseed previous/current planes with full refresh after BUSY timeouts. @@ -348,7 +349,7 @@ A completed descriptor is a keyword list with the following shape: ```elixir [ - descriptor_version: 2, + descriptor_version: 3, name: "Waveshare epd2in9_V2 2.9\" e-paper SSD1680 v1", controller: :ssd16xx, # :ssd16xx, :jd79656, :uc8151, :uc8276, :uc8175, or :acep7 native_width: 128, @@ -370,9 +371,24 @@ A completed descriptor is a keyword list with the following shape: full: <<...>>, # Full refresh waveform/cmd sequence fast: <<...>>, # Fast refresh sequence (optional) partial: <<...>>, # Partial refresh sequence (optional) - sleep: <<...>>, # Sleep command sequence (optional) "4gray": <<...>> # 4-gray refresh sequence (optional) }, + sleep_modes: [ + sleep: [ + enter: <<...>>, # Bytecode program to enter the mode + wake: :reset_init, # :init, :reset_init, or custom bytecode + controller_ram: :retained, # :retained, :lost, or :unknown + host_prev_frame: :preserve, # :preserve or :invalidate + after_wake_refresh: :allow_if_program_reseeds + ], + deep_sleep: [ + enter: <<...>>, + wake: :reset_init, + controller_ram: :lost, + host_prev_frame: :preserve, + after_wake_refresh: :allow_if_program_reseeds + ] + ], init_seq: <<...>>, # ACeP color init sequence (optional for program descriptors) init_wait_busy_between_cmds: false, # ACeP color init behavior frame_preamble_seq: <<...>>, # ACeP color per-frame preamble (optional) @@ -397,7 +413,7 @@ A completed descriptor is a keyword list with the following shape: ] ``` -Program-driven monochrome and 4-gray descriptors use the `programs` and LUT fields. ACeP 7-color descriptors use the palette and ACeP sequence fields (`init_seq`, `frame_preamble_seq`, and refresh settings) instead. +Program-driven monochrome and 4-gray descriptors use the `programs` and LUT fields. ACeP 7-color descriptors use the palette and ACeP sequence fields (`init_seq`, `frame_preamble_seq`, and refresh settings) instead. Both styles can expose `sleep_modes`. #### Ghosting Policy @@ -437,6 +453,47 @@ When sending updates via the port, you can dynamically choose the refresh mode p If the requested refresh mode is not listed in the descriptor's `refresh_modes`, the driver defaults back to `default_refresh`. Only modes defined and present in the descriptor are permitted. +For slow displays, use `wait_idle` after an asynchronous `update` when the caller needs a queue barrier. It returns after earlier retained display messages have been processed, with `:ok` when the most recent executed update completed successfully and `:error` if the driver detected a failure. + +```elixir +:port.call(display, {:update, items, [refresh: :partial]}, 5000) +:port.call(display, {:wait_idle}, 30_000) +``` + +The e-paper port also exposes descriptor capabilities at runtime: + +```elixir +:port.call(display, {:info}, 5000) +# [ +# controller: :ssd16xx, +# width: 296, +# height: 128, +# refresh_modes: [:full, :fast, :partial, :"4gray"], +# default_refresh: :full, +# sleep_modes: [:sleep, :deep_sleep], +# asleep: false +# ] +``` + +#### Low Power Sleep + +Descriptors can expose up to 8 unique named low-power modes under `sleep_modes`. The built-in names are: + +- `:standby` - controller power-off/standby when the panel supports it. +- `:sleep` - controller sleep mode intended to retain more internal state. +- `:deep_sleep` - lowest-power controller mode; wake generally requires hardware reset. + +Use the mode explicitly: + +```elixir +:port.call(display, {:sleep, :standby}, 5000) +:port.call(display, {:sleep, :sleep}, 5000) +:port.call(display, {:sleep, :deep_sleep}, 5000) +:port.call(display, {:wake}, 5000) +``` + +If an update arrives while the controller is asleep, the driver wakes it first. `host_prev_frame` controls whether AtomGL keeps its own previous-frame buffer; `after_wake_refresh` controls whether a fast/partial update is allowed, forced to full, or allowed only when the selected bytecode program writes the saved previous frame back to controller RAM. + ## Custom Initialization Sequences Many displays require specific initialization sequences with carefully tuned values for voltages, diff --git a/docs/epaper-low-power-specs.md b/docs/epaper-low-power-specs.md new file mode 100644 index 0000000..59522ed --- /dev/null +++ b/docs/epaper-low-power-specs.md @@ -0,0 +1,64 @@ + + + +# E-paper Low Power Spec Notes + +Internal working notes for mapping panel data sheets and vendor drivers onto +AtomGL `standby`, `sleep`, `deep_sleep`, and `wake` behavior. + +## Summary Table + +| AtomGL panel key | Panel / controller | Source | Power-off / standby | Sleep | Deep sleep | Wake requirement | Partial after wake | +| --- | --- | --- | --- | --- | --- | --- | --- | +| `waveshare,epd2in9_V2` | 2.9 inch SSD1680 | SSD1680 command table / Waveshare command use | Not exposed separately | `0x10,0x01` | `0x10,0x03` | Reset + init | Fast/partial allowed only when the bytecode reseeds controller previous RAM | +| `dke,depg0290bns800f6` | 2.9 inch SSD1680 | DEPG0290BNS800F6 controller sheet | Not exposed separately | `0x10,0x01` | `0x10,0x03` | Reset + init | No differential mode in AtomGL descriptor | +| `waveshare,epd2in13_V4` | 2.13 inch SSD1680-style | Waveshare command use | Not exposed separately | `0x10,0x01` | `0x10,0x03` | Reset + init | Fast/partial allowed only when the bytecode reseeds controller previous RAM | +| `waveshare,epd2in9`, `waveshare,epd2in13` | UC8151-era mono panels | Existing AtomGL command table | Not exposed separately | `0x10,0x01` | Not exposed by default | Reset + init | Partial promoted unless the selected program reseeds previous RAM | +| `waveshare,epd4in2_V2` | UC8276 4.2 inch | Existing AtomGL command table | Not exposed separately | `0x10,0x01` | Not exposed by default | Reset + init | Fast/partial allowed only when the bytecode reseeds controller previous RAM | +| `heltec,lcmen2r13efc1` | JD79656 2.13 inch | Existing AtomGL command table | `0x02`, wait BUSY high | Not exposed by default | Not exposed by default | Reset + init | No differential mode in AtomGL descriptor | +| `waveshare,5in65-acep-7c` | 5.65 inch ACeP 7-color | Waveshare command use / existing AtomGL refresh flow | `0x02`, wait BUSY low | Not exposed by default | `0x02`, wait, `0x07,0xA5` | Hardware reset + init | No partial mode in AtomGL descriptor | +| `good-display/gdep073e01` | 7.3 inch Spectra 6, 800x480, GDEP073E01-style | `Waveshare_7.3inch-e-Paper-(E)-user-manual.pdf` | `0x02,0x00`; BUSY low while active, high when idle | Not documented | `0x02,0x00`, wait, `0x07,0xA5` | Hardware reset + init | No partial mode in AtomGL descriptor | + +## `good-display/gdep073e01` + +Local spec sheet: + +- `/Users/petermm/Downloads/Waveshare_7.3inch-e-Paper-(E)-user-manual.pdf` + +Extracted from the command table: + +| Function | Command | Data | Notes | +| --- | --- | --- | --- | +| Power OFF | `0x02` | `0x00` | Driver enters power-off/standby state. | +| Power ON | `0x04` | none | Used before refresh. | +| Deep Sleep | `0x07` | `0xA5` | Check code; exiting deep sleep requires HWRESET. | +| Data Start Transmission | `0x10` | frame bytes | Writes display RAM. | +| Data Refresh | `0x12` | `0x01` in spec | Local Waveshare sample code sends `0x00`; AtomGL currently follows sample code. | + +Power/current table notes: + +- The sheet includes rows for module standby current and module deep sleep current, + but the extracted table does not provide numeric values. +- The sheet advertises low-current deep sleep and on-chip display RAM, but does not + describe a separate RAM-retaining sleep mode analogous to SSD1680 `0x10,0x01`. + +Cross-checks: + +- Waveshare Arduino/STM32/C drivers for `EPD_7IN3E_Sleep()` send: + `0x02, 0x00`, wait busy, then `0x07, 0xA5`. +- Waveshare Python `epd7in3e.py` sleep sends only `0x07, 0xA5`. +- `bb_epaper` sends data refresh as `0x12, 0x00` for UC81xx 4-gray, + 4-color, and 7-color panels, including Spectra 6. +- `bb_epaper` currently treats `BBEP_7COLOR` sleep as power-off only and does not + send the `0x07,0xA5` deep-sleep command in its 7-color path. +- AtomGL currently does power-off after each ACEP7/Spectra 6 refresh: + `0x04`, `0x12`, optional refresh data, `0x02`. + +Implementation implication: + +- For this panel family, `standby` means the already-supported power-off state + (`0x02,0x00`) when exposed as an explicit command. +- `deep_sleep` should mean `0x02,0x00`, wait for idle, then `0x07,0xA5`. +- `wake` from deep sleep must use hardware reset and full init. +- There is no basis in this sheet for preserving controller RAM across + `deep_sleep`. diff --git a/epaper_commands.h b/epaper_commands.h index f400176..385ebaa 100644 --- a/epaper_commands.h +++ b/epaper_commands.h @@ -88,7 +88,42 @@ enum EPaperRefreshMode EPAPER_REFRESH_FULL, EPAPER_REFRESH_FAST, EPAPER_REFRESH_PARTIAL, - EPAPER_REFRESH_4GRAY + EPAPER_REFRESH_4GRAY, + EPAPER_REFRESH_MODE_COUNT +}; + +#define EPAPER_REFRESH_MODE_FULL (1U << EPAPER_REFRESH_FULL) +#define EPAPER_REFRESH_MODE_FAST (1U << EPAPER_REFRESH_FAST) +#define EPAPER_REFRESH_MODE_PARTIAL (1U << EPAPER_REFRESH_PARTIAL) +#define EPAPER_REFRESH_MODE_4GRAY (1U << EPAPER_REFRESH_4GRAY) + +#define EPAPER_MAX_SLEEP_MODES 8 + +enum EPaperControllerRamPolicy +{ + EPAPER_CONTROLLER_RAM_UNKNOWN, + EPAPER_CONTROLLER_RAM_RETAINED, + EPAPER_CONTROLLER_RAM_LOST +}; + +enum EPaperHostPrevFramePolicy +{ + EPAPER_HOST_PREV_FRAME_INVALIDATE, + EPAPER_HOST_PREV_FRAME_PRESERVE +}; + +enum EPaperAfterWakeRefreshPolicy +{ + EPAPER_AFTER_WAKE_REFRESH_FULL, + EPAPER_AFTER_WAKE_REFRESH_ALLOW, + EPAPER_AFTER_WAKE_REFRESH_ALLOW_IF_PROGRAM_RESEEDS +}; + +enum EPaperWakePolicy +{ + EPAPER_WAKE_INIT, + EPAPER_WAKE_RESET_INIT, + EPAPER_WAKE_PROGRAM }; struct EPaperFrameLayout @@ -98,6 +133,17 @@ struct EPaperFrameLayout enum EPaperPolarity polarity; }; +struct EPaperSleepMode +{ + uint32_t atom_index; + struct EPaperProgram enter; + struct EPaperProgram wake; + enum EPaperWakePolicy wake_policy; + enum EPaperControllerRamPolicy controller_ram; + enum EPaperHostPrevFramePolicy host_prev_frame; + enum EPaperAfterWakeRefreshPolicy after_wake_refresh; +}; + // Captures every panel-specific knob so a single unified driver can // drive multiple controllers by descriptor dispatch. @@ -158,8 +204,9 @@ struct EPaperDesc struct EPaperProgram program_fast; struct EPaperProgram program_partial; struct EPaperProgram program_4gray; - struct EPaperProgram sleep; - struct EPaperProgram wake; + struct EPaperSleepMode sleep_modes[EPAPER_MAX_SLEEP_MODES]; + int sleep_mode_count; + uint8_t refresh_mode_mask; struct EPaperLut lut_slots[EPAPER_MAX_LUT_SLOTS]; // Timing & ghosting fields diff --git a/epaper_display_driver.c b/epaper_display_driver.c index d3d2ddc..d1cda1f 100644 --- a/epaper_display_driver.c +++ b/epaper_display_driver.c @@ -66,7 +66,7 @@ static const char *TAG = "epaper_display_driver"; #define UC8151_LUT_PAYLOAD_LEN 30 #define UC8276_LUT_PAYLOAD_LEN 227 #define UC8276_LUT_WITH_REGS_LEN 233 -#define EPAPER_TERM_PROGRAM_COUNT 7 +#define EPAPER_TERM_PROGRAM_COUNT 5 struct EPaperState { @@ -75,9 +75,16 @@ struct EPaperState enum EPaperRefreshMode last_refresh_mode; uint64_t last_refresh_ms; bool needs_reseed; + bool last_op_ok; + bool asleep; + const struct EPaperSleepMode *asleep_mode; + bool after_wake_pending; + bool after_wake_can_reseed_prev; + enum EPaperAfterWakeRefreshPolicy after_wake_refresh; }; -static void clear_screen(Context *ctx, int color); +static void clear_screen_internal(Context *ctx, int color, + bool clear_after_wake_pending); struct EpaperDriver { @@ -92,6 +99,8 @@ struct EpaperDriver uint8_t *term_init_seq_bytes; uint8_t *term_frame_preamble_seq_bytes; uint8_t *term_program_bytes[EPAPER_TERM_PROGRAM_COUNT]; + uint8_t *term_sleep_enter_bytes[EPAPER_MAX_SLEEP_MODES]; + uint8_t *term_sleep_wake_bytes[EPAPER_MAX_SLEEP_MODES]; uint8_t *term_lut_bytes[EPAPER_MAX_LUT_SLOTS]; struct EpaperScreen screen; @@ -130,7 +139,8 @@ static const struct EPaperDesc epaper_desc_term_default = { }, .use_gpio_pullups = false, .busy_idle_level = 0, - .descriptor_version = 2, + .refresh_mode_mask = EPAPER_REFRESH_MODE_FULL, + .descriptor_version = 3, }; static bool epaper_desc_requires_program(const struct EPaperDesc *desc) @@ -315,7 +325,7 @@ static void maybe_refresh(Context *ctx) driver->count_to_refresh--; if (driver->count_to_refresh <= 0) { // 7 is the panel's white entry on both current palettes. - clear_screen(ctx, 7); + clear_screen_internal(ctx, 7, false); update_last_refresh_ts(ctx); driver->count_to_refresh = driver->desc->periodic_refresh_interval; } @@ -800,15 +810,269 @@ static bool epaper_run_driver_program(struct EpaperDriver *driver, return epaper_run_program(program, &ops); } +static void epaper_forget_previous_frame(struct EpaperDriver *driver) +{ + driver->state.prev_valid = false; + driver->state.fast_refresh_count = 0; + driver->state.needs_reseed = false; +} + +static void epaper_clear_after_wake_policy(struct EpaperDriver *driver) +{ + driver->state.after_wake_pending = false; + driver->state.after_wake_can_reseed_prev = false; + driver->state.after_wake_refresh = EPAPER_AFTER_WAKE_REFRESH_ALLOW; +} + +static void epaper_reset_refresh_history(struct EpaperDriver *driver) +{ + driver->state.last_refresh_mode = EPAPER_REFRESH_FULL; + driver->state.fast_refresh_count = 0; + driver->state.needs_reseed = false; +} + +static const struct EPaperSleepMode *epaper_find_sleep_mode(struct EpaperDriver *driver, + term mode_term) +{ + if (!term_is_atom(mode_term)) { + return NULL; + } + uint32_t atom_index = (uint32_t) term_to_atom_index(mode_term); + for (int i = 0; i < driver->desc->sleep_mode_count; i++) { + const struct EPaperSleepMode *mode = &driver->desc->sleep_modes[i]; + if (mode->atom_index == atom_index) { + return mode; + } + } + return NULL; +} + +static bool epaper_run_init_sequence(struct EpaperDriver *driver, bool reset_first) +{ + if (reset_first) { + display_reset(driver); + wait_busy_level(driver, driver->desc->busy_idle_level); + } + if (driver->desc->init.bytes != NULL) { + return epaper_run_driver_program(driver, &driver->desc->init, NULL); + } + if (driver->desc->init_seq != NULL) { + epaper_execute_init_seq(&driver->bus, driver->busy_gpio, + driver->desc->init_seq, driver->desc->init_seq_len, + driver->desc->init_wait_busy_between_cmds); + return true; + } + + if (!reset_first) { + display_reset(driver); + wait_busy_level(driver, driver->desc->busy_idle_level); + } + return true; +} + +static bool epaper_run_wake_sequence(struct EpaperDriver *driver, + const struct EPaperSleepMode *mode) +{ + if (mode != NULL && mode->wake_policy == EPAPER_WAKE_PROGRAM) { + return epaper_run_driver_program(driver, &mode->wake, NULL); + } + if (mode != NULL && mode->wake_policy == EPAPER_WAKE_INIT) { + return epaper_run_init_sequence(driver, false); + } + return epaper_run_init_sequence(driver, true); +} + +static bool epaper_wake_if_asleep(struct EpaperDriver *driver) +{ + if (!driver->state.asleep) { + return true; + } + + const struct EPaperSleepMode *mode = driver->state.asleep_mode; + if (!epaper_run_wake_sequence(driver, mode)) { + ESP_LOGW(TAG, "E-paper wake sequence failed for %s.", driver->desc->name); + return false; + } + + driver->state.asleep = false; + driver->state.asleep_mode = NULL; + bool prev_valid_before_wake = driver->state.prev_valid; + bool host_prev_preserved = mode != NULL + && mode->host_prev_frame == EPAPER_HOST_PREV_FRAME_PRESERVE; + bool controller_ram_retained = mode != NULL + && mode->controller_ram == EPAPER_CONTROLLER_RAM_RETAINED; + if (!(host_prev_preserved && controller_ram_retained)) { + driver->state.prev_valid = false; + } + epaper_reset_refresh_history(driver); + driver->state.after_wake_pending = true; + driver->state.after_wake_can_reseed_prev = + host_prev_preserved && prev_valid_before_wake; + driver->state.after_wake_refresh = mode != NULL + ? mode->after_wake_refresh + : EPAPER_AFTER_WAKE_REFRESH_FULL; + return true; +} + +static bool do_sleep(Context *ctx, term mode_term) +{ + struct EpaperDriver *driver = EPAPER_DRIVER_FROM_CTX(ctx); + + if (driver->state.asleep) { + return true; + } + + const struct EPaperSleepMode *mode = epaper_find_sleep_mode(driver, mode_term); + if (mode == NULL) { + ESP_LOGI(TAG, "E-paper descriptor for %s has no requested sleep mode.", + driver->desc->name); + return false; + } + + if (!epaper_run_driver_program(driver, &mode->enter, NULL)) { + ESP_LOGW(TAG, "E-paper sleep program failed for %s.", driver->desc->name); + return false; + } + + driver->state.asleep = true; + driver->state.asleep_mode = mode; + epaper_clear_after_wake_policy(driver); + if (mode->host_prev_frame == EPAPER_HOST_PREV_FRAME_INVALIDATE) { + epaper_forget_previous_frame(driver); + } + return true; +} + +static bool do_wake(Context *ctx) +{ + struct EpaperDriver *driver = EPAPER_DRIVER_FROM_CTX(ctx); + + if (driver->state.asleep) { + return epaper_wake_if_asleep(driver); + } + return true; +} + +static const struct EPaperProgram *epaper_program_for_refresh_mode( + struct EpaperDriver *driver, enum EPaperRefreshMode mode) +{ + switch (mode) { + case EPAPER_REFRESH_FULL: + return &driver->desc->program_full; + case EPAPER_REFRESH_FAST: + return &driver->desc->program_fast; + case EPAPER_REFRESH_PARTIAL: + return &driver->desc->program_partial; + case EPAPER_REFRESH_4GRAY: + return &driver->desc->program_4gray; + default: + return &driver->desc->program_full; + } +} + +static uint8_t epaper_refresh_mode_bit(enum EPaperRefreshMode mode) +{ + return (uint8_t) (1U << mode); +} + +static bool epaper_refresh_mode_allowed(const struct EPaperDesc *desc, + enum EPaperRefreshMode mode) +{ + return (desc->refresh_mode_mask & epaper_refresh_mode_bit(mode)) != 0; +} + +static term epaper_refresh_mode_atom(Context *ctx, enum EPaperRefreshMode mode) +{ + switch (mode) { + case EPAPER_REFRESH_FULL: + return context_make_atom(ctx, ATOM_STR("\x4", "full")); + case EPAPER_REFRESH_FAST: + return context_make_atom(ctx, ATOM_STR("\x4", "fast")); + case EPAPER_REFRESH_PARTIAL: + return context_make_atom(ctx, ATOM_STR("\x7", "partial")); + case EPAPER_REFRESH_4GRAY: + return context_make_atom(ctx, ATOM_STR("\x5", "4gray")); + default: + return context_make_atom(ctx, ATOM_STR("\x7", "unknown")); + } +} + +static term epaper_controller_atom(Context *ctx, enum EPaperController controller) +{ + switch (controller) { + case EPAPER_CONTROLLER_SSD16XX: + return context_make_atom(ctx, ATOM_STR("\x7", "ssd16xx")); + case EPAPER_CONTROLLER_JD79656: + return context_make_atom(ctx, ATOM_STR("\x7", "jd79656")); + case EPAPER_CONTROLLER_UC8151: + return context_make_atom(ctx, ATOM_STR("\x6", "uc8151")); + case EPAPER_CONTROLLER_UC8276: + return context_make_atom(ctx, ATOM_STR("\x6", "uc8276")); + case EPAPER_CONTROLLER_UC8175: + return context_make_atom(ctx, ATOM_STR("\x6", "uc8175")); + case EPAPER_CONTROLLER_ACEP7: + return context_make_atom(ctx, ATOM_STR("\x5", "acep7")); + default: + return context_make_atom(ctx, ATOM_STR("\x7", "unknown")); + } +} + +static bool epaper_program_inserts_prev_frame(const struct EPaperProgram *program) +{ + if (program == NULL || program->bytes == NULL) { + return false; + } + + size_t pos = 0; + while (pos + 2 <= program->len) { + uint8_t opcode = program->bytes[pos++]; + uint8_t flags_len = program->bytes[pos++]; + size_t data_len = flags_len & EPAPER_PROGRAM_LEN_MASK; + bool is_meta = (flags_len & EPAPER_PROGRAM_META) != 0; + bool has_delay = (flags_len & EPAPER_PROGRAM_DELAY) != 0; + size_t next = pos + data_len + (has_delay ? 1 : 0); + if (next > program->len) { + return false; + } + if (is_meta && opcode == EPAPER_PROGRAM_INSERT_PREV_FRAME) { + return true; + } + pos = next; + } + + return false; +} + +static void epaper_mark_refresh_success(struct EpaperDriver *driver, + enum EPaperRefreshMode resolved_mode) +{ + epaper_clear_after_wake_policy(driver); + driver->state.last_refresh_mode = resolved_mode; + if (resolved_mode == EPAPER_REFRESH_FULL || resolved_mode == EPAPER_REFRESH_4GRAY) { + driver->state.fast_refresh_count = 0; + driver->state.needs_reseed = false; + } else { + driver->state.fast_refresh_count++; + } +} + static const struct EPaperProgram *epaper_resolve_refresh_program(struct EpaperDriver *driver, enum EPaperRefreshMode requested_mode, enum EPaperRefreshMode *resolved_mode) { enum EPaperRefreshMode mode = requested_mode; + const struct EPaperProgram *after_wake_program = epaper_program_for_refresh_mode(driver, mode); + bool after_wake_reseed_allowed = + driver->state.after_wake_pending + && driver->state.after_wake_refresh + == EPAPER_AFTER_WAKE_REFRESH_ALLOW_IF_PROGRAM_RESEEDS + && driver->state.after_wake_can_reseed_prev + && epaper_program_inserts_prev_frame(after_wake_program); // Promote differential refreshes to full when the previous-frame state is not usable. bool differential_mode = mode == EPAPER_REFRESH_FAST || mode == EPAPER_REFRESH_PARTIAL; if (differential_mode) { - if (!driver->state.prev_valid || driver->state.needs_reseed) { + if ((!driver->state.prev_valid && !after_wake_reseed_allowed) + || driver->state.needs_reseed) { ESP_LOGI(TAG, "Promoting refresh to FULL: prev_valid=%d, needs_reseed=%d", driver->state.prev_valid, driver->state.needs_reseed); mode = EPAPER_REFRESH_FULL; @@ -825,26 +1089,28 @@ static const struct EPaperProgram *epaper_resolve_refresh_program(struct EpaperD } } + if ((mode == EPAPER_REFRESH_FAST || mode == EPAPER_REFRESH_PARTIAL) + && driver->state.after_wake_pending) { + if (driver->state.after_wake_refresh == EPAPER_AFTER_WAKE_REFRESH_FULL) { + ESP_LOGI(TAG, "Promoting refresh to FULL: after-wake policy requires full refresh."); + mode = EPAPER_REFRESH_FULL; + } else if (driver->state.after_wake_refresh + == EPAPER_AFTER_WAKE_REFRESH_ALLOW_IF_PROGRAM_RESEEDS + && !after_wake_reseed_allowed) { + ESP_LOGI(TAG, + "Promoting refresh to FULL: after-wake program does not reseed previous frame."); + mode = EPAPER_REFRESH_FULL; + } + } + ESP_LOGI(TAG, "Resolved refresh program: requested=%d, last=%d, resolved=%d", requested_mode, driver->state.last_refresh_mode, mode); *resolved_mode = mode; - - switch (mode) { - case EPAPER_REFRESH_FULL: - return &driver->desc->program_full; - case EPAPER_REFRESH_FAST: - return &driver->desc->program_fast; - case EPAPER_REFRESH_PARTIAL: - return &driver->desc->program_partial; - case EPAPER_REFRESH_4GRAY: - return &driver->desc->program_4gray; - default: - return &driver->desc->program_full; - } + return epaper_program_for_refresh_mode(driver, mode); } -static void do_update_descriptor_mono(struct EpaperDriver *driver, +static bool do_update_descriptor_mono(struct EpaperDriver *driver, BaseDisplayItem *items, int items_len, enum EPaperRefreshMode requested_mode) { enum EPaperRefreshMode resolved_mode; @@ -853,7 +1119,7 @@ static void do_update_descriptor_mono(struct EpaperDriver *driver, if (program->bytes == NULL) { ESP_LOGE(TAG, "E-paper descriptor for %s has no refresh program for mode %d.", driver->desc->name, resolved_mode); - return; + return false; } const size_t frame_len = mono_frame_bytes(driver); @@ -864,7 +1130,7 @@ static void do_update_descriptor_mono(struct EpaperDriver *driver, fprintf(stderr, "do_update: failed to alloc e-paper frame buffers\n"); free(frame_buf); free(line_buf); - return; + return false; } struct EPaperProgramRun run = { @@ -877,23 +1143,19 @@ static void do_update_descriptor_mono(struct EpaperDriver *driver, .is_4gray = false }; - if (!epaper_run_driver_program(driver, program, &run)) { + bool ok = epaper_run_driver_program(driver, program, &run); + if (!ok) { ESP_LOGW(TAG, "E-paper refresh program failed for %s.", driver->desc->name); } else { - driver->state.last_refresh_mode = resolved_mode; - if (resolved_mode == EPAPER_REFRESH_FULL || resolved_mode == EPAPER_REFRESH_4GRAY) { - driver->state.fast_refresh_count = 0; - driver->state.needs_reseed = false; - } else { - driver->state.fast_refresh_count++; - } + epaper_mark_refresh_success(driver, resolved_mode); } free(frame_buf); free(line_buf); + return ok; } -static void do_update_descriptor_4gray(struct EpaperDriver *driver, +static bool do_update_descriptor_4gray(struct EpaperDriver *driver, BaseDisplayItem *items, int items_len, enum EPaperRefreshMode requested_mode) { enum EPaperRefreshMode resolved_mode; @@ -902,7 +1164,7 @@ static void do_update_descriptor_4gray(struct EpaperDriver *driver, if (program->bytes == NULL) { ESP_LOGE(TAG, "E-paper descriptor for %s has no 4-gray refresh program.", driver->desc->name); - return; + return false; } const int gray_bytes = (driver->mono_screen.w + 1) / 2; @@ -914,7 +1176,7 @@ static void do_update_descriptor_4gray(struct EpaperDriver *driver, gray_buf = heap_caps_malloc(gray_bytes, MALLOC_CAP_DMA); if (UNLIKELY(!gray_buf)) { fprintf(stderr, "do_update: failed to alloc 4-gray buffer\n"); - return; + return false; } } uint8_t *plane_buf = heap_caps_malloc(frame_len, MALLOC_CAP_DMA); @@ -924,7 +1186,7 @@ static void do_update_descriptor_4gray(struct EpaperDriver *driver, free(gray_buf); free(plane_buf); free(line_buf); - return; + return false; } struct EPaperProgramRun run = { @@ -937,27 +1199,27 @@ static void do_update_descriptor_4gray(struct EpaperDriver *driver, .is_4gray = is_4gray }; - if (!epaper_run_driver_program(driver, program, &run)) { + bool ok = epaper_run_driver_program(driver, program, &run); + if (!ok) { ESP_LOGW(TAG, "E-paper 4-gray refresh program failed for %s.", driver->desc->name); } else { - driver->state.last_refresh_mode = resolved_mode; - if (resolved_mode == EPAPER_REFRESH_FULL || resolved_mode == EPAPER_REFRESH_4GRAY) { - driver->state.fast_refresh_count = 0; - driver->state.needs_reseed = false; - } else { - driver->state.fast_refresh_count++; - } + epaper_mark_refresh_success(driver, resolved_mode); } free(gray_buf); free(plane_buf); free(line_buf); + return ok; } -static void do_update(Context *ctx, term display_list, term update_opts) +static bool do_update(Context *ctx, term display_list, term update_opts) { struct EpaperDriver *driver = EPAPER_DRIVER_FROM_CTX(ctx); + if (!epaper_wake_if_asleep(driver)) { + return false; + } + if (driver->desc->controller == EPAPER_CONTROLLER_ACEP7) { maybe_refresh(ctx); wait_some_time(ctx); @@ -966,7 +1228,7 @@ static void do_update(Context *ctx, term display_list, term update_opts) BaseDisplayItem *items; size_t len; if (UNLIKELY(display_items_new_list(display_list, &items, &len, ctx) != DisplayItemsOk)) { - return; + return false; } // Resolve update refresh mode @@ -976,7 +1238,13 @@ static void do_update(Context *ctx, term display_list, term update_opts) if (mode_term != term_nil()) { enum EPaperRefreshMode parsed_mode; if (epaper_parse_refresh_mode(mode_term, ctx, &parsed_mode)) { - refresh_mode = parsed_mode; + if (epaper_refresh_mode_allowed(driver->desc, parsed_mode)) { + refresh_mode = parsed_mode; + } else { + ESP_LOGW(TAG, + "Refresh mode is not declared for %s; using default.", + driver->desc->name); + } } else { ESP_LOGE(TAG, "Invalid refresh mode option specified in update."); } @@ -984,14 +1252,17 @@ static void do_update(Context *ctx, term display_list, term update_opts) } if (driver->desc->controller != EPAPER_CONTROLLER_ACEP7) { + bool ok; if (driver->desc->palette_size == 4) { - do_update_descriptor_4gray(driver, items, len, refresh_mode); + ok = do_update_descriptor_4gray(driver, items, len, refresh_mode); } else { - do_update_descriptor_mono(driver, items, len, refresh_mode); + ok = do_update_descriptor_mono(driver, items, len, refresh_mode); } display_items_delete(items, len); - update_last_refresh_ts(ctx); - return; + if (ok) { + update_last_refresh_ts(ctx); + } + return ok; } send_frame_preamble(driver); @@ -1006,7 +1277,7 @@ static void do_update(Context *ctx, term display_list, term update_opts) if (UNLIKELY(!buf)) { fprintf(stderr, "do_update: failed to alloc buf\n"); display_items_delete(items, len); - return; + return false; } memset(buf, 0x11, screen_width / 2); @@ -1041,10 +1312,76 @@ static void do_update(Context *ctx, term display_list, term update_opts) free(buf); send_post_frame_refresh(driver); + epaper_clear_after_wake_policy(driver); display_items_delete(items, len); update_last_refresh_ts(ctx); + return true; +} + +static term epaper_info_kv(Context *ctx, Heap *heap, AtomString key, + term value, term tail) +{ + term item = term_alloc_tuple(2, heap); + term_put_tuple_element(item, 0, context_make_atom(ctx, key)); + term_put_tuple_element(item, 1, value); + return term_list_prepend(item, tail, heap); +} + +static term epaper_refresh_modes_term(Context *ctx, Heap *heap, + const struct EPaperDesc *desc) +{ + term modes = term_nil(); + for (int mode = EPAPER_REFRESH_MODE_COUNT - 1; mode >= 0; mode--) { + if (epaper_refresh_mode_allowed(desc, (enum EPaperRefreshMode) mode)) { + modes = term_list_prepend( + epaper_refresh_mode_atom(ctx, (enum EPaperRefreshMode) mode), + modes, heap); + } + } + return modes; +} + +static term epaper_sleep_modes_term(Heap *heap, const struct EPaperDesc *desc) +{ + term modes = term_nil(); + for (int i = desc->sleep_mode_count - 1; i >= 0; i--) { + modes = term_list_prepend( + term_from_atom_index((atom_index_t) desc->sleep_modes[i].atom_index), + modes, heap); + } + return modes; +} + +static term epaper_build_info(Context *ctx, struct EpaperDriver *driver, + Heap *heap) +{ + const struct EPaperDesc *desc = driver->desc; + term info = term_nil(); + info = epaper_info_kv(ctx, heap, ATOM_STR("\x6", "asleep"), + driver->state.asleep ? TRUE_ATOM : FALSE_ATOM, info); + info = epaper_info_kv(ctx, heap, ATOM_STR("\xB", "sleep_modes"), + epaper_sleep_modes_term(heap, desc), info); + info = epaper_info_kv(ctx, heap, ATOM_STR("\xF", "default_refresh"), + epaper_refresh_mode_atom(ctx, desc->default_refresh), info); + info = epaper_info_kv(ctx, heap, ATOM_STR("\xD", "refresh_modes"), + epaper_refresh_modes_term(ctx, heap, desc), info); + info = epaper_info_kv(ctx, heap, ATOM_STR("\xC", "palette_size"), + term_from_int(desc->palette_size), info); + info = epaper_info_kv(ctx, heap, ATOM_STR("\x8", "rotation"), + term_from_int(desc->rotation), info); + info = epaper_info_kv(ctx, heap, ATOM_STR("\xD", "native_height"), + term_from_int(desc->native_height), info); + info = epaper_info_kv(ctx, heap, ATOM_STR("\xC", "native_width"), + term_from_int(desc->native_width), info); + info = epaper_info_kv(ctx, heap, ATOM_STR("\x6", "height"), + term_from_int(desc->view_height), info); + info = epaper_info_kv(ctx, heap, ATOM_STR("\x5", "width"), + term_from_int(desc->view_width), info); + info = epaper_info_kv(ctx, heap, ATOM_STR("\xA", "controller"), + epaper_controller_atom(ctx, desc->controller), info); + return info; } static void process_message(Message *message, Context *ctx) @@ -1061,10 +1398,13 @@ static void process_message(Message *message, Context *ctx) } int req_arity = term_get_tuple_arity(req); term cmd = term_get_tuple_element(req, 0); + term reply_status = OK_ATOM; + struct EpaperDriver *driver = EPAPER_DRIVER_FROM_CTX(ctx); if (cmd == context_make_atom(ctx, "\x6" "update")) { + bool update_ok = false; if (req_arity < 2) { ESP_LOGE(TAG, "Invalid update request: expected {update, Items} or {update, Items, Opts}."); } else { @@ -1073,8 +1413,9 @@ static void process_message(Message *message, Context *ctx) if (req_arity >= 3) { update_opts = term_get_tuple_element(req, 2); } - do_update(ctx, display_list, update_opts); + update_ok = do_update(ctx, display_list, update_opts); } + driver->state.last_op_ok = update_ok; // Reply already sent at enqueue time by // try_pre_ack_render_cmd in display_task.c. Sending another @@ -1082,6 +1423,37 @@ static void process_message(Message *message, Context *ctx) // mailbox after port:call has already returned. return; + } else if (cmd == context_make_atom(ctx, "\x5" "sleep")) { + if (req_arity != 2) { + ESP_LOGE(TAG, "Invalid sleep request: expected {sleep, Mode}."); + reply_status = ERROR_ATOM; + } else { + reply_status = do_sleep(ctx, term_get_tuple_element(req, 1)) + ? OK_ATOM + : ERROR_ATOM; + } + + } else if (cmd == context_make_atom(ctx, "\x4" "wake")) { + reply_status = do_wake(ctx) ? OK_ATOM : ERROR_ATOM; + + } else if (cmd == context_make_atom(ctx, "\x9" "wait_idle")) { + reply_status = driver->state.last_op_ok ? OK_ATOM : ERROR_ATOM; + + } else if (cmd == context_make_atom(ctx, "\x4" "info") + || cmd == context_make_atom(ctx, "\xC" "capabilities")) { + BEGIN_WITH_STACK_HEAP( + TUPLE_SIZE(2) + REF_SIZE + + 11 * (TUPLE_SIZE(2) + CONS_SIZE) + + (EPAPER_REFRESH_MODE_COUNT + EPAPER_MAX_SLEEP_MODES) * CONS_SIZE + + 8, + heap); + term return_tuple = term_alloc_tuple(2, &heap); + term_put_tuple_element(return_tuple, 0, gen_message.ref); + term_put_tuple_element(return_tuple, 1, epaper_build_info(ctx, driver, &heap)); + display_message_send(gen_message.pid, return_tuple, ctx->global); + END_WITH_STACK_HEAP(heap, ctx->global); + return; + } else if (cmd == globalcontext_make_atom(ctx->global, "\xA" "load_image")) { handle_load_image(req, gen_message.ref, gen_message.pid, ctx); return; @@ -1097,15 +1469,20 @@ static void process_message(Message *message, Context *ctx) BEGIN_WITH_STACK_HEAP(TUPLE_SIZE(2) + REF_SIZE, heap); term return_tuple = term_alloc_tuple(2, &heap); term_put_tuple_element(return_tuple, 0, gen_message.ref); - term_put_tuple_element(return_tuple, 1, OK_ATOM); + term_put_tuple_element(return_tuple, 1, reply_status); display_message_send(gen_message.pid, return_tuple, ctx->global); END_WITH_STACK_HEAP(heap, ctx->global); } -static void clear_screen(Context *ctx, int color) +static void clear_screen_internal(Context *ctx, int color, + bool clear_after_wake_pending) { struct EpaperDriver *driver = EPAPER_DRIVER_FROM_CTX(ctx); + if (!epaper_wake_if_asleep(driver)) { + return; + } + int screen_width = driver->screen.w; int screen_height = driver->screen.h; @@ -1146,6 +1523,9 @@ static void clear_screen(Context *ctx, int color) free(buf); send_post_frame_refresh(driver); + if (clear_after_wake_pending) { + epaper_clear_after_wake_policy(driver); + } } static bool epaper_term_bool(term value, bool *out) @@ -1221,6 +1601,26 @@ static bool epaper_copy_binary_field(term container, Context *ctx, return true; } +static bool epaper_copy_binary_term(term value, + uint8_t **owned_bytes, const uint8_t **bytes, size_t *len) +{ + if (!term_is_binary(value)) { + return false; + } + + size_t value_len = term_binary_size(value); + uint8_t *copy = malloc(value_len == 0 ? 1 : value_len); + if (copy == NULL) { + return false; + } + + memcpy(copy, term_binary_data(value), value_len); + *owned_bytes = copy; + *bytes = copy; + *len = value_len; + return true; +} + static void epaper_free_descriptor_override(struct EpaperDriver *driver) { free(driver->term_desc_name); @@ -1235,6 +1635,13 @@ static void epaper_free_descriptor_override(struct EpaperDriver *driver) driver->term_program_bytes[i] = NULL; } + for (int i = 0; i < EPAPER_MAX_SLEEP_MODES; i++) { + free(driver->term_sleep_enter_bytes[i]); + driver->term_sleep_enter_bytes[i] = NULL; + free(driver->term_sleep_wake_bytes[i]); + driver->term_sleep_wake_bytes[i] = NULL; + } + for (int i = 0; i < EPAPER_MAX_LUT_SLOTS; i++) { free(driver->term_lut_bytes[i]); driver->term_lut_bytes[i] = NULL; @@ -1356,6 +1763,85 @@ static bool epaper_parse_refresh_mode(term val, Context *ctx, enum EPaperRefresh return false; } +static bool epaper_parse_refresh_modes(term modes, Context *ctx, uint8_t *out) +{ + if (!term_is_nonempty_list(modes)) { + return false; + } + + uint8_t mask = 0; + term t = modes; + while (term_is_nonempty_list(t)) { + enum EPaperRefreshMode mode; + if (!epaper_parse_refresh_mode(term_get_list_head(t), ctx, &mode)) { + return false; + } + uint8_t bit = epaper_refresh_mode_bit(mode); + if ((mask & bit) != 0) { + return false; + } + mask |= bit; + t = term_get_list_tail(t); + } + + if (t != term_nil() || (mask & EPAPER_REFRESH_MODE_FULL) == 0) { + return false; + } + + *out = mask; + return true; +} + +static bool epaper_parse_controller_ram_policy(term val, Context *ctx, + enum EPaperControllerRamPolicy *out) +{ + if (val == context_make_atom(ctx, ATOM_STR("\x7", "unknown"))) { + *out = EPAPER_CONTROLLER_RAM_UNKNOWN; + return true; + } + if (val == context_make_atom(ctx, ATOM_STR("\x8", "retained"))) { + *out = EPAPER_CONTROLLER_RAM_RETAINED; + return true; + } + if (val == context_make_atom(ctx, ATOM_STR("\x4", "lost"))) { + *out = EPAPER_CONTROLLER_RAM_LOST; + return true; + } + return false; +} + +static bool epaper_parse_host_prev_frame_policy(term val, Context *ctx, + enum EPaperHostPrevFramePolicy *out) +{ + if (val == context_make_atom(ctx, ATOM_STR("\x8", "preserve"))) { + *out = EPAPER_HOST_PREV_FRAME_PRESERVE; + return true; + } + if (val == context_make_atom(ctx, ATOM_STR("\xA", "invalidate"))) { + *out = EPAPER_HOST_PREV_FRAME_INVALIDATE; + return true; + } + return false; +} + +static bool epaper_parse_after_wake_refresh_policy(term val, Context *ctx, + enum EPaperAfterWakeRefreshPolicy *out) +{ + if (val == context_make_atom(ctx, ATOM_STR("\x5", "allow"))) { + *out = EPAPER_AFTER_WAKE_REFRESH_ALLOW; + return true; + } + if (val == context_make_atom(ctx, ATOM_STR("\x4", "full"))) { + *out = EPAPER_AFTER_WAKE_REFRESH_FULL; + return true; + } + if (val == context_make_atom(ctx, ATOM_STR("\x18", "allow_if_program_reseeds"))) { + *out = EPAPER_AFTER_WAKE_REFRESH_ALLOW_IF_PROGRAM_RESEEDS; + return true; + } + return false; +} + static bool epaper_parse_palette(term val, Context *ctx, const uint8_t (**palette)[3], int *palette_size) { @@ -1373,6 +1859,170 @@ static bool epaper_parse_palette(term val, Context *ctx, return false; } +static bool epaper_parse_sleep_modes(struct EpaperDriver *driver, + term descriptor, Context *ctx) +{ + term sleep_modes = interop_kv_get_value_default( + descriptor, ATOM_STR("\xB", "sleep_modes"), term_nil(), ctx->global); + if (sleep_modes == term_nil()) { + driver->term_desc.sleep_mode_count = 0; + return true; + } + + int mode_count = 0; + term t = sleep_modes; + while (term_is_nonempty_list(t)) { + if (mode_count >= EPAPER_MAX_SLEEP_MODES) { + ESP_LOGE(TAG, "Too many e-paper sleep modes; maximum is %d.", + EPAPER_MAX_SLEEP_MODES); + return false; + } + + term entry = term_get_list_head(t); + if (!term_is_tuple(entry) || term_get_tuple_arity(entry) != 2) { + ESP_LOGE(TAG, "Invalid e-paper sleep mode entry."); + return false; + } + + term mode_atom = term_get_tuple_element(entry, 0); + term props = term_get_tuple_element(entry, 1); + if (!term_is_atom(mode_atom) + || (props != term_nil() && !term_is_nonempty_list(props))) { + ESP_LOGE(TAG, "Invalid e-paper sleep mode name or properties."); + return false; + } + uint32_t mode_atom_index = (uint32_t) term_to_atom_index(mode_atom); + for (int i = 0; i < mode_count; i++) { + if (driver->term_desc.sleep_modes[i].atom_index == mode_atom_index) { + ESP_LOGE(TAG, "Duplicate e-paper sleep mode."); + return false; + } + } + + struct EPaperSleepMode *mode = &driver->term_desc.sleep_modes[mode_count]; + memset(mode, 0, sizeof(*mode)); + mode->atom_index = mode_atom_index; + mode->wake_policy = EPAPER_WAKE_RESET_INIT; + mode->controller_ram = EPAPER_CONTROLLER_RAM_UNKNOWN; + mode->host_prev_frame = EPAPER_HOST_PREV_FRAME_INVALIDATE; + mode->after_wake_refresh = EPAPER_AFTER_WAKE_REFRESH_FULL; + + term enter = interop_kv_get_value_default( + props, ATOM_STR("\x5", "enter"), term_nil(), ctx->global); + if (enter == term_nil() + || !epaper_copy_binary_term(enter, + &driver->term_sleep_enter_bytes[mode_count], + &mode->enter.bytes, &mode->enter.len)) { + ESP_LOGE(TAG, "Invalid or missing e-paper sleep mode enter program."); + return false; + } + + term wake = interop_kv_get_value_default( + props, ATOM_STR("\x4", "wake"), term_nil(), ctx->global); + if (wake != term_nil()) { + if (term_is_binary(wake)) { + if (!epaper_copy_binary_term(wake, + &driver->term_sleep_wake_bytes[mode_count], + &mode->wake.bytes, &mode->wake.len)) { + ESP_LOGE(TAG, "Invalid e-paper sleep mode wake program."); + return false; + } + mode->wake_policy = EPAPER_WAKE_PROGRAM; + } else if (wake == context_make_atom(ctx, ATOM_STR("\x4", "init"))) { + mode->wake_policy = EPAPER_WAKE_INIT; + } else if (wake == context_make_atom(ctx, ATOM_STR("\xA", "reset_init"))) { + mode->wake_policy = EPAPER_WAKE_RESET_INIT; + } else { + ESP_LOGE(TAG, "Invalid e-paper sleep mode wake policy."); + return false; + } + } + + term controller_ram = interop_kv_get_value_default( + props, ATOM_STR("\xE", "controller_ram"), term_nil(), ctx->global); + if (controller_ram != term_nil() + && !epaper_parse_controller_ram_policy(controller_ram, ctx, &mode->controller_ram)) { + ESP_LOGE(TAG, "Invalid e-paper sleep mode controller_ram policy."); + return false; + } + + term host_prev_frame = interop_kv_get_value_default( + props, ATOM_STR("\xF", "host_prev_frame"), term_nil(), ctx->global); + if (host_prev_frame != term_nil() + && !epaper_parse_host_prev_frame_policy(host_prev_frame, ctx, + &mode->host_prev_frame)) { + ESP_LOGE(TAG, "Invalid e-paper sleep mode host_prev_frame policy."); + return false; + } + + term after_wake_refresh = interop_kv_get_value_default( + props, ATOM_STR("\x12", "after_wake_refresh"), term_nil(), ctx->global); + if (after_wake_refresh != term_nil() + && !epaper_parse_after_wake_refresh_policy(after_wake_refresh, ctx, + &mode->after_wake_refresh)) { + ESP_LOGE(TAG, "Invalid e-paper sleep mode after_wake_refresh policy."); + return false; + } + + mode_count++; + t = term_get_list_tail(t); + } + + if (t != term_nil()) { + ESP_LOGE(TAG, "Invalid e-paper sleep_modes list."); + return false; + } + + driver->term_desc.sleep_mode_count = mode_count; + return true; +} + +static bool epaper_validate_descriptor_refresh_modes(const struct EPaperDesc *desc) +{ + if ((desc->refresh_mode_mask & epaper_refresh_mode_bit(desc->default_refresh)) == 0) { + ESP_LOGE(TAG, "default_refresh is not listed in refresh_modes."); + return false; + } + + if (desc->controller == EPAPER_CONTROLLER_ACEP7) { + if (desc->refresh_mode_mask != EPAPER_REFRESH_MODE_FULL) { + ESP_LOGE(TAG, "ACeP e-paper descriptors currently support only full refresh."); + return false; + } + return true; + } + + for (int mode = 0; mode < EPAPER_REFRESH_MODE_COUNT; mode++) { + if (!epaper_refresh_mode_allowed(desc, (enum EPaperRefreshMode) mode)) { + continue; + } + const struct EPaperProgram *program; + switch ((enum EPaperRefreshMode) mode) { + case EPAPER_REFRESH_FULL: + program = &desc->program_full; + break; + case EPAPER_REFRESH_FAST: + program = &desc->program_fast; + break; + case EPAPER_REFRESH_PARTIAL: + program = &desc->program_partial; + break; + case EPAPER_REFRESH_4GRAY: + program = &desc->program_4gray; + break; + default: + program = NULL; + break; + } + if (program->bytes == NULL) { + ESP_LOGE(TAG, "refresh_modes declares a mode without a program."); + return false; + } + } + + return true; +} + static bool epaper_validate_descriptor_geometry(const struct EPaperDesc *desc) { if (desc->native_width <= 0 || desc->native_height <= 0 @@ -1435,11 +2085,15 @@ static bool epaper_parse_descriptor_override(struct EpaperDriver *driver, } memset(driver->term_program_bytes, 0, sizeof(driver->term_program_bytes)); + memset(driver->term_sleep_enter_bytes, 0, sizeof(driver->term_sleep_enter_bytes)); + memset(driver->term_sleep_wake_bytes, 0, sizeof(driver->term_sleep_wake_bytes)); memset(driver->term_lut_bytes, 0, sizeof(driver->term_lut_bytes)); driver->term_init_seq_bytes = NULL; driver->term_frame_preamble_seq_bytes = NULL; driver->term_desc_name = NULL; driver->term_desc = *driver->desc; + memset(driver->term_desc.sleep_modes, 0, sizeof(driver->term_desc.sleep_modes)); + driver->term_desc.sleep_mode_count = 0; // Check version first int descriptor_version = 0; @@ -1450,8 +2104,8 @@ static bool epaper_parse_descriptor_override(struct EpaperDriver *driver, return false; } driver->term_desc.descriptor_version = descriptor_version; - if (driver->term_desc.descriptor_version != 2) { - ESP_LOGE(TAG, "Unsupported e-paper descriptor version %d. Only version 2 is supported.", + if (driver->term_desc.descriptor_version != 3) { + ESP_LOGE(TAG, "Unsupported e-paper descriptor version %d. Only version 3 is supported.", driver->term_desc.descriptor_version); return false; } @@ -1546,6 +2200,16 @@ static bool epaper_parse_descriptor_override(struct EpaperDriver *driver, driver->term_desc.layout.polarity = polarity; } + // Parse Refresh Modes + term refresh_modes_term = interop_kv_get_value_default( + descriptor, ATOM_STR("\xD", "refresh_modes"), term_nil(), ctx->global); + if (refresh_modes_term == term_nil() + || !epaper_parse_refresh_modes(refresh_modes_term, ctx, + &driver->term_desc.refresh_mode_mask)) { + ESP_LOGE(TAG, "Invalid refresh_modes in descriptor."); + return false; + } + // Parse Default Refresh term default_refresh_term = interop_kv_get_value_default(descriptor, ATOM_STR("\xF", "default_refresh"), term_nil(), ctx->global); if (default_refresh_term != term_nil()) { @@ -1556,6 +2220,11 @@ static bool epaper_parse_descriptor_override(struct EpaperDriver *driver, } driver->term_desc.default_refresh = default_refresh; } + if (!epaper_refresh_mode_allowed(&driver->term_desc, + driver->term_desc.default_refresh)) { + ESP_LOGE(TAG, "default_refresh is not listed in refresh_modes."); + return false; + } // Parse Programs Map term programs_map = interop_kv_get_value_default(descriptor, ATOM_STR("\x8", "programs"), term_nil(), ctx->global); @@ -1574,18 +2243,20 @@ static bool epaper_parse_descriptor_override(struct EpaperDriver *driver, &driver->term_desc.program_partial.bytes, &driver->term_desc.program_partial.len) || !epaper_copy_binary_field(programs_map, ctx, ATOM_STR("\x5", "4gray"), &driver->term_program_bytes[4], - &driver->term_desc.program_4gray.bytes, &driver->term_desc.program_4gray.len) - || !epaper_copy_binary_field(programs_map, ctx, ATOM_STR("\x5", "sleep"), - &driver->term_program_bytes[5], - &driver->term_desc.sleep.bytes, &driver->term_desc.sleep.len) - || !epaper_copy_binary_field(programs_map, ctx, ATOM_STR("\x4", "wake"), - &driver->term_program_bytes[6], - &driver->term_desc.wake.bytes, &driver->term_desc.wake.len)) { + &driver->term_desc.program_4gray.bytes, &driver->term_desc.program_4gray.len)) { ESP_LOGE(TAG, "Invalid e-paper descriptor program binary."); return false; } } + if (!epaper_validate_descriptor_refresh_modes(&driver->term_desc)) { + return false; + } + + if (!epaper_parse_sleep_modes(driver, descriptor, ctx)) { + return false; + } + if (!epaper_copy_binary_field(descriptor, ctx, ATOM_STR("\x8", "init_seq"), &driver->term_init_seq_bytes, &driver->term_desc.init_seq, &driver->term_desc.init_seq_len) @@ -1777,6 +2448,10 @@ static void display_spi_init(Context *ctx, term opts) driver->state.prev_valid = false; driver->state.fast_refresh_count = 0; driver->state.needs_reseed = false; + driver->state.last_op_ok = true; + driver->state.asleep = false; + driver->state.asleep_mode = NULL; + epaper_clear_after_wake_policy(driver); // Init sequence: init_list opt overrides the descriptor default. term init_list = interop_kv_get_value_default( @@ -1804,10 +2479,10 @@ static void display_spi_init(Context *ctx, term opts) #if SELF_TEST for (int i = 0; i < 8; i++) { fprintf(stderr, "color: %i\n", i); - clear_screen(ctx, i); + clear_screen_internal(ctx, i, true); vTaskDelay(30000 / portTICK_PERIOD_MS); } - clear_screen(ctx, 1); + clear_screen_internal(ctx, 1, true); while (1) ; From ec12a76e40562fc91b2ea391e8b309e9e4f51a1d Mon Sep 17 00:00:00 2001 From: Peter M Date: Sun, 31 May 2026 14:39:02 +0200 Subject: [PATCH 4/6] epaper: harden descriptor validation and refresh state Validate descriptor scalars, geometry, frame-size arithmetic, programs, sleep modes, LUTs, and init failure paths at the Erlang and C boundaries. Separate ACeP raw init sequences from interpreted command bytecode. Send cached previous frames before capturing new ones and commit cache validity only after successful programs. Preserve optical refresh history across sleep/wake and invalidate monochrome caches before gray updates. Correct Heltec BUSY polarity, bound wake waits, and use wide timeout/tick arithmetic. Validate retained refresh programs independently of enabled capabilities. Add focused Erlang descriptor regression coverage. Signed-off-by: Peter M --- atomgl_epaper/CUSTOM_PANEL.md | 10 + .../src/atomgl_epaper_descriptor.erl | 377 +++++++++++++++--- atomgl_epaper/src/atomgl_epaper_panels.erl | 123 ++++-- atomgl_epaper/src/atomgl_epaper_program.erl | 21 + atomgl_epaper/test/atomgl_epaper_tests.erl | 236 ++++++++++- docs/display-drivers.md | 26 +- epaper_commands.c | 55 ++- epaper_commands.h | 5 +- epaper_display_driver.c | 360 +++++++++++++---- epaper_program.c | 82 ++++ epaper_program.h | 3 + 11 files changed, 1104 insertions(+), 194 deletions(-) diff --git a/atomgl_epaper/CUSTOM_PANEL.md b/atomgl_epaper/CUSTOM_PANEL.md index 30bda17..8a8e20d 100644 --- a/atomgl_epaper/CUSTOM_PANEL.md +++ b/atomgl_epaper/CUSTOM_PANEL.md @@ -8,6 +8,11 @@ without adding a lookup string to AtomGL. Use `atomgl_epaper_descriptor` to build and validate the descriptor, and use `atomgl_epaper_program` to build the bytecode programs consumed by the C display driver. +Descriptors are keyword/proplists all the way down. The validator checks scalar +ranges, controller/layout compatibility, bytecode structure, sleep mode +programs, and ACeP raw init sequences so malformed custom panels fail before the +display port opens. + Once the descriptor works on hardware, it can be copied into `atomgl_epaper/src/atomgl_epaper_panels.erl` with a public lookup string and a focused descriptor test. @@ -232,6 +237,11 @@ Port = open_port({spawn, "display"}, [ The GPIO numbers above are placeholders. Use the pins from your board wiring. +For ACeP color panels, build `init_seq` and `frame_preamble_seq` with +`atomgl_epaper_program:init_seq/1`, `init_cmd/1,2`, and `init_cmd_delay/3`. +Those raw command sequences are separate from interpreted refresh/sleep +bytecode and do not support `wait_busy/2`, `reset/3`, or render operations. + ## Moving A Working Panel Into AtomGL After hardware testing, move the descriptor into diff --git a/atomgl_epaper/src/atomgl_epaper_descriptor.erl b/atomgl_epaper/src/atomgl_epaper_descriptor.erl index b404be1..28316f8 100644 --- a/atomgl_epaper/src/atomgl_epaper_descriptor.erl +++ b/atomgl_epaper/src/atomgl_epaper_descriptor.erl @@ -4,6 +4,24 @@ -module(atomgl_epaper_descriptor). -define(MAX_SLEEP_MODES, 8). +-define(MAX_DESCRIPTOR_BINARY_LEN, 4096). +-define(MAX_INT, 16#7FFFFFFF). +-define(MAX_DIMENSION, (?MAX_INT - 7)). +-define(MAX_FRAME_BYTES, 16#FFFFFFFF). +-define(PROGRAM_DELAY, 16#80). +-define(PROGRAM_META, 16#40). +-define(PROGRAM_LEN_MASK, 16#3F). +-define(INIT_SEQ_DELAY, 16#80). +-define(INIT_SEQ_LEN_MASK, 16#7F). + +-define(OP_WAIT_BUSY, 16#01). +-define(OP_RESET, 16#02). +-define(OP_INSERT_PLANE, 16#03). +-define(OP_INSERT_LUT, 16#04). +-define(OP_INSERT_PREV_FRAME, 16#05). +-define(OP_CAPTURE_FRAME, 16#06). +-define(OP_MARK_PREV_VALID, 16#07). +-define(OP_LABEL, 16#10). -export([ ssd16xx_panel/2, @@ -68,15 +86,11 @@ acep7_panel(Name, Config) -> build_panel(Controller, Name, Config) -> NativeW = maps:get(native_width, Config), NativeH = maps:get(native_height, Config), - Orientation = maps:get(orientation, Config, landscape), - {ViewW, ViewH, Rotation} = case Orientation of - landscape -> {NativeH, NativeW, 90}; - landscape_left -> {NativeH, NativeW, 90}; - landscape_right -> {NativeH, NativeW, 270}; - portrait -> {NativeW, NativeH, 0}; - portrait_flipped -> {NativeW, NativeH, 180}; - _ -> error({bad_orientation, Orientation}) - end, + OrientationRotation = orientation_rotation(maps:get(orientation, Config, landscape)), + Rotation = maps:get(rotation, Config, OrientationRotation), + {DefaultViewW, DefaultViewH} = default_view_geometry(NativeW, NativeH, Rotation), + ViewW = maps:get(view_width, Config, DefaultViewW), + ViewH = maps:get(view_height, Config, DefaultViewH), DefaultRefresh = maps:get(default_refresh, Config, full), RefreshModes = maps:get(refresh_modes, Config, [full]), @@ -130,6 +144,28 @@ build_panel(Controller, Name, Config) -> validate_descriptor(Descriptor). +orientation_rotation(landscape) -> + 90; +orientation_rotation(landscape_left) -> + 90; +orientation_rotation(landscape_right) -> + 270; +orientation_rotation(portrait) -> + 0; +orientation_rotation(portrait_flipped) -> + 180; +orientation_rotation(Orientation) -> + error({bad_orientation, Orientation}). + +default_view_geometry(NativeW, NativeH, Rotation) -> + case Rotation of + 0 -> {NativeW, NativeH}; + 180 -> {NativeW, NativeH}; + 90 -> {NativeH, NativeW}; + 270 -> {NativeH, NativeW}; + _ -> {NativeW, NativeH} + end. + mode_programs(Config) -> P0 = [], P1 = case maps:find(init, Config) of @@ -155,129 +191,352 @@ mode_programs(Config) -> P5. timing(Config) -> + TimingConfig = maps:get(timing, Config, #{}), [ - {full_expected_ms, maps:get(full_expected_ms, Config, 2000)}, - {fast_expected_ms, maps:get(fast_expected_ms, Config, 500)}, - {poll_interval_ms, maps:get(poll_interval_ms, Config, 50)}, - {timeout_ms, maps:get(timeout_ms, Config, 5000)} + {full_expected_ms, config_value(full_expected_ms, TimingConfig, Config, 2000)}, + {fast_expected_ms, config_value(fast_expected_ms, TimingConfig, Config, 500)}, + {poll_interval_ms, config_value(poll_interval_ms, TimingConfig, Config, 50)}, + {timeout_ms, config_value(timeout_ms, TimingConfig, Config, 5000)} ]. -validate_descriptor(Desc) -> +config_value(Key, Nested, Config, Default) when is_map(Nested) -> + maps:get(Key, Nested, maps:get(Key, Config, Default)); +config_value(Key, _Nested, Config, Default) -> + maps:get(Key, Config, Default). + +validate_descriptor(Desc) when is_list(Desc) -> Version = get_value(descriptor_version, Desc), - true = (Version == 3), + validate_int(descriptor_version, Version, 3, 3), NativeW = get_value(native_width, Desc), NativeH = get_value(native_height, Desc), ViewW = get_value(view_width, Desc), ViewH = get_value(view_height, Desc), Rotation = get_value(rotation, Desc), + validate_int(native_width, NativeW, 1, ?MAX_DIMENSION), + validate_int(native_height, NativeH, 1, ?MAX_DIMENSION), + validate_int(view_width, ViewW, 1, ?MAX_DIMENSION), + validate_int(view_height, ViewH, 1, ?MAX_DIMENSION), + validate_int(rotation, Rotation, 0, 270), - %% Width/height consistency check case Rotation of 0 -> - true = (ViewW == NativeW), - true = (ViewH == NativeH); + require_true(view_width, ViewW == NativeW, bad_geometry), + require_true(view_height, ViewH == NativeH, bad_geometry); 180 -> - true = (ViewW == NativeW), - true = (ViewH == NativeH); + require_true(view_width, ViewW == NativeW, bad_geometry), + require_true(view_height, ViewH == NativeH, bad_geometry); 90 -> - true = (ViewW == NativeH), - true = (ViewH == NativeW); + require_true(view_width, ViewW == NativeH, bad_geometry), + require_true(view_height, ViewH == NativeW, bad_geometry); 270 -> - true = (ViewW == NativeH), - true = (ViewH == NativeW); + require_true(view_width, ViewW == NativeH, bad_geometry), + require_true(view_height, ViewH == NativeW, bad_geometry); _ -> error({bad_rotation, Rotation}) end, Controller = get_value(controller, Desc), - true = lists:member(Controller, [ssd16xx, jd79656, uc8151, uc8276, uc8175, acep7]), + validate_member(controller, Controller, [ssd16xx, jd79656, uc8151, uc8276, uc8175, acep7]), Layout = get_value(frame_layout, Desc), - true = case Controller of + require_true(frame_layout, case Controller of ssd16xx -> lists:member(Layout, [row_msb, row_lsb]); uc8151 -> lists:member(Layout, [row_msb, row_lsb]); uc8276 -> lists:member(Layout, [row_msb, row_lsb]); uc8175 -> lists:member(Layout, [row_msb, row_lsb]); jd79656 -> lists:member(Layout, [column_msb, column_lsb]); _ -> true + end, incompatible_with_controller), + case Controller of + acep7 -> ok; + _ -> + FrameBytes = case Layout of + column_msb -> NativeW * ((NativeH + 7) div 8); + column_lsb -> NativeW * ((NativeH + 7) div 8); + _ -> ((NativeW + 7) div 8) * NativeH + end, + require_true(frame_layout, FrameBytes =< ?MAX_FRAME_BYTES, + frame_size_overflow) end, + validate_member(polarity, get_value(polarity, Desc), [white_1, black_1]), + validate_int(spi_clock_hz, get_value(spi_clock_hz, Desc), 1, 80000000), + validate_member(busy_idle_level, get_value(busy_idle_level, Desc), [0, 1]), + validate_bool(use_gpio_pullups, get_value(use_gpio_pullups, Desc)), DefaultRefresh = get_value(default_refresh, Desc), RefreshModes = get_value(refresh_modes, Desc), validate_refresh_modes(RefreshModes), - true = lists:member(DefaultRefresh, RefreshModes), + validate_member(default_refresh, DefaultRefresh, RefreshModes), validate_sleep_modes(get_value(sleep_modes, Desc, [])), + PaletteSize = get_value(palette_size, Desc), + validate_member(palette_size, PaletteSize, [2, 4, 7]), + validate_timing(get_value(timing, Desc, [])), + validate_ghosting(get_value(ghosting, Desc, [])), case Controller of acep7 -> - true = (Rotation == 0), + require_true(rotation, Rotation == 0, acep7_native_orientation_only), + require_true(native_width, (NativeW rem 2) == 0, acep7_width_must_be_even), + require_true(view_width, (ViewW rem 2) == 0, acep7_width_must_be_even), + validate_member(palette_size, PaletteSize, [7]), Palette = get_value(palette, Desc), - true = lists:member(Palette, [acep7, acep7c, gdep073e01]), + validate_member(palette, Palette, [acep7, acep7c, gdep073e01]), InitSeq = get_value(init_seq, Desc), - true = is_binary(InitSeq), - true = (byte_size(InitSeq) =< 4096), + validate_required_init_seq(init_seq, InitSeq), FramePreambleSeq = get_value(frame_preamble_seq, Desc, <<>>), - true = is_binary(FramePreambleSeq), - true = (byte_size(FramePreambleSeq) =< 4096); + validate_init_seq(frame_preamble_seq, FramePreambleSeq), + validate_bool(init_wait_busy_between_cmds, get_value(init_wait_busy_between_cmds, Desc)), + validate_bool(refresh_has_data, get_value(refresh_has_data, Desc)), + validate_int(refresh_data_byte, get_value(refresh_data_byte, Desc), 0, 255), + validate_member(post_power_off_busy_level, + get_value(post_power_off_busy_level, Desc), [0, 1]), + validate_int(periodic_refresh_interval, + get_value(periodic_refresh_interval, Desc), 0, ?MAX_INT), + validate_member(refresh_modes, RefreshModes, [[full]]); _ -> + require_true(palette_size, PaletteSize =/= 7, acep7_only), + require_true(palette_size, + PaletteSize =/= 4 orelse lists:member(Controller, [ssd16xx, uc8276]), + unsupported_4gray_controller), + require_true(palette_size, + not lists:member('4gray', RefreshModes) orelse PaletteSize == 4, + required_for_4gray), Programs = get_value(programs, Desc), - true = is_defined(init, Programs), + validate_kv_list(programs, Programs), + require_true(programs, is_defined(init, Programs), missing_init), - %% Required programs for declared modes lists:foreach(fun(Mode) -> - true = is_defined(Mode, Programs) + require_true(programs, is_defined(Mode, Programs), + {missing_refresh_program, Mode}) end, RefreshModes), - %% Bytecode payload length limits - lists:foreach(fun({_K, V}) -> - true = (byte_size(V) =< 4096) - end, Programs) + validate_required_program(init, get_value(init, Programs), false), + lists:foreach(fun(Mode) -> + validate_required_program(Mode, get_value(Mode, Programs), true) + end, RefreshModes), + lists:foreach(fun({Mode, Program}) -> + validate_program(Mode, Program, + lists:member(Mode, [full, fast, partial, '4gray'])) + end, Programs), + validate_optional_binary(lut_full, get_value(lut_full, Desc, <<>>)), + validate_optional_binary(lut_partial, get_value(lut_partial, Desc, <<>>)), + validate_optional_binary(lut_4gray, get_value(lut_4gray, Desc, <<>>)), + validate_optional_binary(lut_fast, get_value(lut_fast, Desc, <<>>)) end, - {ok, Desc}. + {ok, Desc}; +validate_descriptor(_) -> + error({bad_descriptor, expected_keyword_list}). + +bad_field(Key, Reason) -> + error({bad_field, Key, Reason}). + +require_true(_Key, true, _Reason) -> + ok; +require_true(Key, false, Reason) -> + bad_field(Key, Reason). + +validate_int(Key, Value, Min, Max) when is_integer(Value), Value >= Min -> + case Max of + infinity -> ok; + _ when Value =< Max -> ok; + _ -> bad_field(Key, {out_of_range, Min, Max}) + end; +validate_int(Key, _Value, Min, Max) -> + bad_field(Key, {expected_integer, Min, Max}). + +validate_bool(_Key, Value) when is_boolean(Value) -> + ok; +validate_bool(Key, _Value) -> + bad_field(Key, expected_boolean). + +validate_member(_Key, Value, Allowed) -> + case lists:member(Value, Allowed) of + true -> ok; + false -> bad_field(_Key, {expected_one_of, Allowed}) + end. + +validate_kv_list(Key, Value) when is_list(Value) -> + lists:foreach(fun + ({K, _V}) when is_atom(K) -> ok; + (_) -> bad_field(Key, expected_keyword_list) + end, Value); +validate_kv_list(Key, _Value) -> + bad_field(Key, expected_keyword_list). validate_refresh_modes(RefreshModes) when is_list(RefreshModes) -> - true = (RefreshModes =/= []), - true = lists:member(full, RefreshModes), - true = (length(RefreshModes) == length(lists:usort(RefreshModes))), + require_true(refresh_modes, RefreshModes =/= [], empty), + require_true(refresh_modes, lists:member(full, RefreshModes), missing_full), + require_true(refresh_modes, + length(RefreshModes) == length(lists:usort(RefreshModes)), duplicate), lists:foreach(fun(Mode) -> - true = lists:member(Mode, [full, fast, partial, '4gray']) + validate_member(refresh_modes, Mode, [full, fast, partial, '4gray']) end, RefreshModes); validate_refresh_modes(_) -> - error(bad_refresh_modes). + bad_field(refresh_modes, expected_list). + +validate_timing(Timing) -> + validate_kv_list(timing, Timing), + validate_int(full_expected_ms, get_value(full_expected_ms, Timing, 2000), 0, ?MAX_INT), + validate_int(fast_expected_ms, get_value(fast_expected_ms, Timing, 500), 0, ?MAX_INT), + validate_int(poll_interval_ms, get_value(poll_interval_ms, Timing, 50), 1, ?MAX_INT), + validate_int(timeout_ms, get_value(timeout_ms, Timing, 5000), 1, ?MAX_INT). + +validate_ghosting(Ghosting) -> + validate_kv_list(ghosting, Ghosting), + validate_int(max_fast_refreshes, get_value(max_fast_refreshes, Ghosting, 10), 0, ?MAX_INT), + validate_bool(reseed_on_timeout, get_value(reseed_on_timeout, Ghosting, true)). + +validate_optional_binary(_Key, <<>>) -> + ok; +validate_optional_binary(_Key, Bin) when is_binary(Bin), byte_size(Bin) =< ?MAX_DESCRIPTOR_BINARY_LEN -> + ok; +validate_optional_binary(Key, _Value) -> + bad_field(Key, expected_binary). + +validate_program(Key, Bin, AllowRenderOps) when is_binary(Bin), + byte_size(Bin) =< ?MAX_DESCRIPTOR_BINARY_LEN -> + validate_program_bytes(Key, Bin, AllowRenderOps), + validate_program_flow(Key, Bin, false, false); +validate_program(Key, _Value, _AllowRenderOps) -> + bad_field(Key, expected_program_binary). + +validate_required_program(Key, <<>>, _AllowRenderOps) -> + bad_field(Key, empty_program); +validate_required_program(Key, Bin, AllowRenderOps) -> + validate_program(Key, Bin, AllowRenderOps). + +validate_program_bytes(_Key, <<>>, _AllowRenderOps) -> + ok; +validate_program_bytes(Key, <>, AllowRenderOps) -> + Len = FlagsLen band ?PROGRAM_LEN_MASK, + HasDelay = (FlagsLen band ?PROGRAM_DELAY) =/= 0, + IsMeta = (FlagsLen band ?PROGRAM_META) =/= 0, + case Rest of + <> -> + case IsMeta of + true -> validate_meta(Key, Opcode, byte_size(Data), AllowRenderOps); + false -> ok + end, + case {HasDelay, Tail} of + {true, <<_Delay, Next/binary>>} -> + validate_program_bytes(Key, Next, AllowRenderOps); + {true, _} -> + bad_field(Key, truncated_delay); + {false, _} -> + validate_program_bytes(Key, Tail, AllowRenderOps) + end; + _ -> + bad_field(Key, truncated_record) + end; +validate_program_bytes(Key, _Bad, _AllowRenderOps) -> + bad_field(Key, truncated_record). + +validate_program_flow(Key, <<>>, true, _CaptureCompleted) -> + bad_field(Key, capture_without_plane); +validate_program_flow(_Key, <<>>, false, _CaptureCompleted) -> + ok; +validate_program_flow(Key, <>, + CapturePending, CaptureCompleted) -> + Len = FlagsLen band ?PROGRAM_LEN_MASK, + HasDelay = (FlagsLen band ?PROGRAM_DELAY) =/= 0, + IsMeta = (FlagsLen band ?PROGRAM_META) =/= 0, + <<_Data:Len/binary, Tail0/binary>> = Rest, + Tail = case HasDelay of + true -> + <<_Delay, Next/binary>> = Tail0, + Next; + false -> + Tail0 + end, + {NextPending, NextCompleted} = case {IsMeta, Opcode, CapturePending} of + {true, ?OP_CAPTURE_FRAME, true} -> + bad_field(Key, capture_already_pending); + {true, ?OP_CAPTURE_FRAME, false} -> + {true, CaptureCompleted}; + {true, ?OP_INSERT_PLANE, true} -> + {false, true}; + {true, ?OP_MARK_PREV_VALID, _} when not CaptureCompleted -> + bad_field(Key, mark_without_capture); + _ -> + {CapturePending, CaptureCompleted} + end, + validate_program_flow(Key, Tail, NextPending, NextCompleted). + +validate_meta(_Key, ?OP_WAIT_BUSY, 3, _AllowRenderOps) -> ok; +validate_meta(_Key, ?OP_RESET, 3, _AllowRenderOps) -> ok; +validate_meta(_Key, ?OP_INSERT_LUT, 1, _AllowRenderOps) -> ok; +validate_meta(_Key, ?OP_LABEL, 1, _AllowRenderOps) -> ok; +validate_meta(_Key, ?OP_INSERT_PLANE, 1, true) -> ok; +validate_meta(_Key, ?OP_INSERT_PREV_FRAME, 1, true) -> ok; +validate_meta(_Key, ?OP_CAPTURE_FRAME, 0, true) -> ok; +validate_meta(_Key, ?OP_MARK_PREV_VALID, 0, true) -> ok; +validate_meta(Key, Opcode, Len, _AllowRenderOps) -> + bad_field(Key, {bad_meta_opcode, Opcode, Len}). + +validate_init_seq(Key, Bin) when is_binary(Bin), + byte_size(Bin) =< ?MAX_DESCRIPTOR_BINARY_LEN -> + validate_init_seq_bytes(Key, Bin); +validate_init_seq(Key, _Value) -> + bad_field(Key, expected_init_sequence_binary). + +validate_required_init_seq(Key, <<>>) -> + bad_field(Key, empty_sequence); +validate_required_init_seq(Key, Bin) -> + validate_init_seq(Key, Bin). + +validate_init_seq_bytes(_Key, <<>>) -> + ok; +validate_init_seq_bytes(Key, <<_Cmd, FlagsLen, Rest/binary>>) -> + Len = FlagsLen band ?INIT_SEQ_LEN_MASK, + HasDelay = (FlagsLen band ?INIT_SEQ_DELAY) =/= 0, + case Rest of + <<_Data:Len/binary, Tail/binary>> -> + case {HasDelay, Tail} of + {true, <<_Delay, Next/binary>>} -> + validate_init_seq_bytes(Key, Next); + {true, _} -> + bad_field(Key, truncated_delay); + {false, _} -> + validate_init_seq_bytes(Key, Tail) + end; + _ -> + bad_field(Key, truncated_record) + end; +validate_init_seq_bytes(Key, _Bad) -> + bad_field(Key, truncated_record). validate_sleep_modes(SleepModes) when is_list(SleepModes) -> - true = (length(SleepModes) =< ?MAX_SLEEP_MODES), + require_true(sleep_modes, length(SleepModes) =< ?MAX_SLEEP_MODES, too_many), Modes = lists:map(fun ({Mode, _Props}) when is_atom(Mode) -> Mode; - (_) -> error(bad_sleep_mode) + (_) -> bad_field(sleep_modes, bad_sleep_mode) end, SleepModes), - true = (length(Modes) == length(lists:usort(Modes))), + require_true(sleep_modes, length(Modes) == length(lists:usort(Modes)), duplicate), lists:foreach(fun validate_sleep_mode/1, SleepModes); validate_sleep_modes(_) -> - error(bad_sleep_modes). + bad_field(sleep_modes, expected_list). validate_sleep_mode({Mode, Props}) when is_atom(Mode), is_list(Props) -> + validate_kv_list(Mode, Props), Enter = get_value(enter, Props), - true = is_binary(Enter), - true = (byte_size(Enter) =< 4096), + validate_required_program(Mode, Enter, false), Wake = get_value(wake, Props, reset_init), - true = (is_binary(Wake) orelse lists:member(Wake, [init, reset_init])), case Wake of Bin when is_binary(Bin) -> - true = (byte_size(Bin) =< 4096); + validate_required_program(Mode, Bin, false); _ -> - ok + validate_member(Mode, Wake, [init, reset_init]) end, - true = lists:member(get_value(controller_ram, Props, unknown), + validate_member(Mode, get_value(controller_ram, Props, unknown), [unknown, retained, lost]), - true = lists:member(get_value(host_prev_frame, Props, invalidate), + validate_member(Mode, get_value(host_prev_frame, Props, invalidate), [preserve, invalidate]), - true = lists:member(get_value(after_wake_refresh, Props, full), + validate_member(Mode, get_value(after_wake_refresh, Props, full), [allow, full, allow_if_program_reseeds]); validate_sleep_mode(_) -> - error(bad_sleep_mode). + bad_field(sleep_modes, bad_sleep_mode). get_value(Key, List) -> get_value(Key, List, undefined). diff --git a/atomgl_epaper/src/atomgl_epaper_panels.erl b/atomgl_epaper/src/atomgl_epaper_panels.erl index 167efd7..3ff2eb0 100644 --- a/atomgl_epaper/src/atomgl_epaper_panels.erl +++ b/atomgl_epaper/src/atomgl_epaper_panels.erl @@ -17,9 +17,12 @@ -import(atomgl_epaper_program, [ program/1, + init_seq/1, cmd/1, cmd/2, - cmd_delay/3, + init_cmd/1, + init_cmd/2, + init_cmd_delay/3, wait_busy/2, reset/3, insert_plane/1, @@ -60,7 +63,7 @@ panel("waveshare,epd2in9_V2", Opts) -> default_refresh => full, init => epd2in9_v2_init(none), full => epd2in9_v2_refresh(?LUT_FULL, 16#C7), - fast => epd2in9_v2_refresh(?LUT_FAST, 16#C7), + fast => epd2in9_v2_fast_refresh(), partial => epd2in9_v2_partial_refresh(), '4gray' => epd2in9_v2_4gray_refresh(), sleep_modes => ssd16xx_sleep_modes(100), @@ -108,7 +111,7 @@ panel("waveshare,epd2in13_V4", Opts) -> default_refresh => full, init => epd2in13_v4_init(), full => epd2in13_v4_refresh(16#F7), - fast => epd2in13_v4_refresh(16#C7), + fast => epd2in13_v4_fast_refresh(), partial => epd2in13_v4_partial_refresh(), sleep_modes => ssd16xx_sleep_modes(100) }, Opts)); @@ -125,7 +128,7 @@ panel("waveshare,epd4in2_V2", Opts) -> default_refresh => full, init => uc8276_init(), full => uc8276_refresh(uc8276_mono_prelude(), 16#F7), - fast => uc8276_refresh(uc8276_fast_prelude(16#6E), 16#C7), + fast => uc8276_fast_refresh(16#6E), partial => uc8276_partial_refresh(), '4gray' => uc8276_4gray_refresh(), sleep_modes => controller_sleep_modes(sleep_program(200)), @@ -141,6 +144,7 @@ panel("heltec,lcmen2r13efc1", Opts) -> jd79656_panel("Heltec LCMEN2R13EFC1 2.13\" e-paper JD79656 v1", maps:merge(#{ native_width => 122, native_height => 250, + busy_idle_level => 1, refresh_modes => [full], default_refresh => full, init => heltec_lcmen2r13efc1_init(), @@ -184,8 +188,8 @@ panel("good-display/gdep073e01", Opts) -> periodic_refresh_interval => 0, sleep_modes => acep7_sleep_modes(1, <<16#00>>) }, Opts)); -panel(_, _Opts) -> - error. +panel(Compatible, _Opts) -> + {error, {unsupported_panel, Compatible}}. ssd16xx_sleep_modes(DelayMs) -> [ @@ -339,6 +343,21 @@ uc8276_refresh(Prelude, UpdateMode) -> mark_prev_valid() ]). +uc8276_fast_refresh(Temperature) -> + program([ + uc8276_fast_prelude(Temperature), + cmd(16#26), + insert_prev_frame(0), + uc8276_cursor(), + cmd(16#24), + capture_frame(), + insert_plane(0), + cmd(16#22, <<16#C7>>), + cmd(16#20), + wait_busy(0, 5000), + mark_prev_valid() + ]). + uc8276_partial_refresh() -> program([ cmd(16#3C, <<16#80>>), @@ -447,6 +466,21 @@ epd2in13_v4_refresh(UpdateMode) -> mark_prev_valid() ]). +epd2in13_v4_fast_refresh() -> + program([ + epd2in13_v4_cursor(), + cmd(16#26), + insert_prev_frame(0), + epd2in13_v4_cursor(), + cmd(16#24), + capture_frame(), + insert_plane(0), + cmd(16#22, <<16#C7>>), + cmd(16#20), + wait_busy(0, 5000), + mark_prev_valid() + ]). + epd2in13_v4_partial_refresh() -> program([ reset(0, 1, 0), @@ -508,42 +542,42 @@ heltec_lcmen2r13efc1_refresh() -> %% SPDX-SnippetEnd acep7c_init_seq() -> - program([ - cmd(16#00, <<16#EF, 16#08>>), - cmd(16#01, <<16#37, 16#00, 16#23, 16#23>>), - cmd(16#03, <<16#00>>), - cmd(16#06, <<16#C7, 16#C7, 16#1D>>), - cmd(16#30, <<16#3C>>), - cmd(16#40, <<16#00>>), - cmd(16#50, <<16#3F>>), - cmd(16#60, <<16#22>>), - cmd(16#61, <<16#02, 16#58, 16#01, 16#C0>>), - cmd(16#E3, <<16#AA>>), - cmd_delay(16#82, <<16#80>>, 100), - cmd(16#50, <<16#37>>) + init_seq([ + init_cmd(16#00, <<16#EF, 16#08>>), + init_cmd(16#01, <<16#37, 16#00, 16#23, 16#23>>), + init_cmd(16#03, <<16#00>>), + init_cmd(16#06, <<16#C7, 16#C7, 16#1D>>), + init_cmd(16#30, <<16#3C>>), + init_cmd(16#40, <<16#00>>), + init_cmd(16#50, <<16#3F>>), + init_cmd(16#60, <<16#22>>), + init_cmd(16#61, <<16#02, 16#58, 16#01, 16#C0>>), + init_cmd(16#E3, <<16#AA>>), + init_cmd_delay(16#82, <<16#80>>, 100), + init_cmd(16#50, <<16#37>>) ]). acep7c_frame_preamble_seq() -> - program([ - cmd(16#61, <<16#02, 16#58, 16#01, 16#C0>>) + init_seq([ + init_cmd(16#61, <<16#02, 16#58, 16#01, 16#C0>>) ]). gdep073e01_init_seq() -> - program([ - cmd(16#AA, <<16#49, 16#55, 16#20, 16#08, 16#09, 16#18>>), - cmd(16#01, <<16#3F>>), - cmd(16#00, <<16#5F, 16#69>>), - cmd(16#03, <<16#00, 16#54, 16#00, 16#44>>), - cmd(16#05, <<16#40, 16#1F, 16#1F, 16#2C>>), - cmd(16#06, <<16#6F, 16#1F, 16#17, 16#49>>), - cmd(16#08, <<16#6F, 16#1F, 16#1F, 16#22>>), - cmd(16#30, <<16#00>>), - cmd(16#50, <<16#3F>>), - cmd(16#60, <<16#02, 16#00>>), - cmd(16#61, <<16#03, 16#20, 16#01, 16#E0>>), - cmd(16#84, <<16#01>>), - cmd(16#E3, <<16#2F>>), - cmd(16#04) + init_seq([ + init_cmd(16#AA, <<16#49, 16#55, 16#20, 16#08, 16#09, 16#18>>), + init_cmd(16#01, <<16#3F>>), + init_cmd(16#00, <<16#5F, 16#69>>), + init_cmd(16#03, <<16#00, 16#54, 16#00, 16#44>>), + init_cmd(16#05, <<16#40, 16#1F, 16#1F, 16#2C>>), + init_cmd(16#06, <<16#6F, 16#1F, 16#17, 16#49>>), + init_cmd(16#08, <<16#6F, 16#1F, 16#1F, 16#22>>), + init_cmd(16#30, <<16#00>>), + init_cmd(16#50, <<16#3F>>), + init_cmd(16#60, <<16#02, 16#00>>), + init_cmd(16#61, <<16#03, 16#20, 16#01, 16#E0>>), + init_cmd(16#84, <<16#01>>), + init_cmd(16#E3, <<16#2F>>), + init_cmd(16#04) ]). epd2in9_v2_init(BorderWaveform) -> @@ -584,6 +618,23 @@ epd2in9_v2_refresh(LutSlot, UpdateMode) -> mark_prev_valid() ]). +epd2in9_v2_fast_refresh() -> + program([ + insert_lut(?LUT_FAST), + set_ram_cursor(), + cmd(16#26), + insert_prev_frame(0), + delay_ms(2), + set_ram_cursor(), + cmd(16#24), + capture_frame(), + insert_plane(0), + cmd(16#22, <<16#C7>>), + cmd(16#20), + wait_busy(0, 5000), + mark_prev_valid() + ]). + epd2in9_v2_partial_refresh() -> program([ epd2in9_v2_partial_prelude(), diff --git a/atomgl_epaper/src/atomgl_epaper_program.erl b/atomgl_epaper/src/atomgl_epaper_program.erl index c6a9545..496c033 100644 --- a/atomgl_epaper/src/atomgl_epaper_program.erl +++ b/atomgl_epaper/src/atomgl_epaper_program.erl @@ -5,9 +5,13 @@ -export([ program/1, + init_seq/1, cmd/1, cmd/2, cmd_delay/3, + init_cmd/1, + init_cmd/2, + init_cmd_delay/3, wait_busy/2, reset/3, insert_plane/1, @@ -22,6 +26,7 @@ -define(DELAY, 16#80). -define(META, 16#40). -define(LEN_MASK, 16#3F). +-define(INIT_LEN_MASK, 16#7F). -define(OP_WAIT_BUSY, 16#01). -define(OP_RESET, 16#02). @@ -35,6 +40,9 @@ program(Parts) -> iolist_to_binary(Parts). +init_seq(Parts) -> + iolist_to_binary(Parts). + cmd(Cmd) -> cmd(Cmd, <<>>). @@ -46,6 +54,19 @@ cmd(Cmd, Data) when byte_size(Data) =< ?LEN_MASK -> cmd_delay(Cmd, Data, DelayMs) when byte_size(Data) =< ?LEN_MASK -> <>. +init_cmd(Cmd) -> + init_cmd(Cmd, <<>>). + +init_cmd(Cmd, Data) when is_list(Data) -> + init_cmd(Cmd, list_to_binary(Data)); +init_cmd(Cmd, Data) when byte_size(Data) =< ?INIT_LEN_MASK -> + <>. + +init_cmd_delay(Cmd, Data, DelayMs) when is_list(Data) -> + init_cmd_delay(Cmd, list_to_binary(Data), DelayMs); +init_cmd_delay(Cmd, Data, DelayMs) when byte_size(Data) =< ?INIT_LEN_MASK -> + <>. + wait_busy(Level, TimeoutMs) -> meta(?OP_WAIT_BUSY, <>). diff --git a/atomgl_epaper/test/atomgl_epaper_tests.erl b/atomgl_epaper/test/atomgl_epaper_tests.erl index f22b384..85c5c98 100644 --- a/atomgl_epaper/test/atomgl_epaper_tests.erl +++ b/atomgl_epaper/test/atomgl_epaper_tests.erl @@ -7,6 +7,13 @@ -import(atomgl_epaper, [panel/1, panel/2, validate_descriptor/1]). +-define(PROGRAM_DELAY, 16#80). +-define(PROGRAM_META, 16#40). +-define(PROGRAM_LEN_MASK, 16#3F). +-define(OP_INSERT_PLANE, 16#03). +-define(OP_INSERT_PREV_FRAME, 16#05). +-define(OP_CAPTURE_FRAME, 16#06). + get_value(Key, List) -> proplists:get_value(Key, List). @@ -41,6 +48,19 @@ orientation_geometry_test_() -> ?assertEqual(128, get_value(view_width, Desc)), ?assertEqual(296, get_value(view_height, Desc)), ?assertEqual(180, get_value(rotation, Desc)) + end), + ?_test(begin + {ok, Desc} = panel("waveshare,epd2in9_V2", #{rotation => 0}), + ?assertEqual(128, get_value(view_width, Desc)), + ?assertEqual(296, get_value(view_height, Desc)), + ?assertEqual(0, get_value(rotation, Desc)) + end), + ?_test(begin + {ok, Desc} = panel("waveshare,epd2in9_V2", + #{rotation => 0, view_width => 128, view_height => 296}), + ?assertEqual(128, get_value(view_width, Desc)), + ?assertEqual(296, get_value(view_height, Desc)), + ?assertEqual(0, get_value(rotation, Desc)) end) ]. @@ -74,7 +94,51 @@ invalid_rotated_view_geometry_test() -> {default_refresh, full}, {programs, [{init, <<>>}, {full, <<>>}]} ], - ?assertError({badmatch, false}, validate_descriptor(Desc)). + ?assertError({bad_field, view_width, bad_geometry}, validate_descriptor(Desc)). + +frame_size_limits_test_() -> + Config = #{rotation => 90, init => <<0, 0>>, full => <<0, 0>>}, + [ + ?_test(begin + {ok, Row} = atomgl_epaper_descriptor:ssd16xx_panel("row boundary", + Config#{native_width => 32768, native_height => 1048575}), + ?assertEqual(1048575, get_value(view_width, Row)), + {ok, Column} = atomgl_epaper_descriptor:jd79656_panel("column boundary", + Config#{native_width => 1048575, native_height => 32768}), + ?assertEqual(1048575, get_value(view_height, Column)) + end), + ?_test(begin + %% 3 bytes per row times 1431655765 rows is exactly UINT32_MAX. + {ok, _} = atomgl_epaper_descriptor:ssd16xx_panel("exact frame boundary", + Config#{native_width => 24, native_height => 1431655765}) + end), + ?_assertError({bad_field, frame_layout, frame_size_overflow}, + atomgl_epaper_descriptor:ssd16xx_panel("one row beyond frame boundary", + Config#{native_width => 24, native_height => 1431655766})), + ?_assertError({bad_field, frame_layout, frame_size_overflow}, + atomgl_epaper_descriptor:ssd16xx_panel("row overflow", + Config#{native_width => 32768, native_height => 1048576})), + ?_assertError({bad_field, frame_layout, frame_size_overflow}, + atomgl_epaper_descriptor:jd79656_panel("column overflow", + Config#{native_width => 1048576, native_height => 32768})), + %% Floor division would incorrectly accept these non-byte-aligned frames. + ?_assertError({bad_field, frame_layout, frame_size_overflow}, + atomgl_epaper_descriptor:ssd16xx_panel("row rounding", + Config#{native_width => 32769, native_height => 1048575})), + ?_assertError({bad_field, frame_layout, frame_size_overflow}, + atomgl_epaper_descriptor:jd79656_panel("column rounding", + Config#{native_width => 1048575, native_height => 32769})), + ?_test(begin + {ok, _} = atomgl_epaper_descriptor:ssd16xx_panel("dimension boundary", + Config#{native_width => 2147483640, native_height => 1}) + end), + ?_assertError({bad_field, native_width, {out_of_range, 1, 2147483640}}, + atomgl_epaper_descriptor:ssd16xx_panel("dimension rounding overflow", + Config#{native_width => 2147483641, native_height => 1})), + ?_assertError({bad_field, native_height, {out_of_range, 1, 2147483640}}, + atomgl_epaper_descriptor:jd79656_panel("dimension narrowing overflow", + Config#{native_width => 1, native_height => 4294967297})) + ]. invalid_refresh_modes_descriptor_test_() -> BaseDesc = [ @@ -86,24 +150,122 @@ invalid_refresh_modes_descriptor_test_() -> {view_height, 296}, {rotation, 0}, {frame_layout, row_msb}, + {polarity, white_1}, + {spi_clock_hz, 4000000}, + {busy_idle_level, 0}, + {use_gpio_pullups, false}, + {palette_size, 2}, {default_refresh, full}, - {programs, [{init, <<>>}, {full, <<>>}, {partial, <<>>}]} + {programs, [{init, <<0, 0>>}, {full, <<0, 0>>}, {partial, <<0, 0>>}]}, + {timing, []}, + {ghosting, []} ], [ ?_test(begin - ?assertError({badmatch, false}, + ?assertError({bad_field, refresh_modes, duplicate}, validate_descriptor([{refresh_modes, [full, partial, partial]} | BaseDesc])) end), ?_test(begin - ?assertError({badmatch, false}, + ?assertError({bad_field, refresh_modes, missing_full}, validate_descriptor([{refresh_modes, [partial]} | BaseDesc])) end), ?_test(begin - ?assertError({badmatch, false}, + ?assertError({bad_field, refresh_modes, {expected_one_of, [full, fast, partial, '4gray']}}, validate_descriptor([{refresh_modes, [full, turbo]} | BaseDesc])) end) ]. +empty_required_program_test_() -> + BaseDesc = [ + {descriptor_version, 3}, + {controller, ssd16xx}, + {native_width, 128}, + {native_height, 296}, + {view_width, 128}, + {view_height, 296}, + {rotation, 0}, + {frame_layout, row_msb}, + {polarity, white_1}, + {spi_clock_hz, 4000000}, + {busy_idle_level, 0}, + {use_gpio_pullups, false}, + {palette_size, 2}, + {refresh_modes, [full]}, + {default_refresh, full}, + {programs, [{init, <<0, 0>>}, {full, <<0, 0>>}]}, + {timing, []}, + {ghosting, []} + ], + [ + ?_assertError({bad_field, init, empty_program}, + validate_descriptor([{programs, [{init, <<>>}, {full, <<0, 0>>}]} | BaseDesc])), + ?_assertError({bad_field, full, empty_program}, + validate_descriptor([{programs, [{init, <<0, 0>>}, {full, <<>>}]} | BaseDesc])), + ?_assertError({bad_field, sleep, empty_program}, + validate_descriptor([{sleep_modes, [{sleep, [{enter, <<>>}]}]} | BaseDesc])), + ?_assertError({bad_field, sleep, empty_program}, + validate_descriptor([{sleep_modes, [{sleep, [ + {enter, <<0, 0>>}, {wake, <<>>} + ]}]} | BaseDesc])), + ?_assertError({bad_field, init_seq, empty_sequence}, + panel("waveshare,5in65-acep-7c", #{init_seq => <<>>})) + ]. + +invalid_previous_frame_flow_test_() -> + {ok, Descriptor} = panel("waveshare,epd2in9_V2"), + Programs = get_value(programs, Descriptor), + [ + ?_assertError({bad_field, full, mark_without_capture}, + validate_descriptor(replace_value(programs, + replace_value(full, <<16#07, 16#40>>, Programs), Descriptor))), + ?_assertError({bad_field, full, capture_without_plane}, + validate_descriptor(replace_value(programs, + replace_value(full, <<16#06, 16#40>>, Programs), Descriptor))) + ]. + +fast_previous_frame_order_test_() -> + [ + ?_test(assert_previous_frame_order("waveshare,epd2in9_V2")), + ?_test(assert_previous_frame_order("waveshare,epd2in13_V4")), + ?_test(assert_previous_frame_order("waveshare,epd4in2_V2")) + ]. + +restricted_refresh_modes_test() -> + {ok, Desc} = panel("waveshare,epd2in9_V2", #{refresh_modes => [full]}), + ?assertEqual([full], get_value(refresh_modes, Desc)), + ?assertEqual(full, get_value(default_refresh, Desc)), + Programs = get_value(programs, Desc), + ?assert(is_binary(get_value('4gray', Programs))), + ?assertError({bad_field, fast, truncated_record}, + validate_descriptor(replace_value(programs, + replace_value(fast, <<0>>, Programs), Desc))), + ?assertError({bad_field, init, {bad_meta_opcode, 3, 1}}, + validate_descriptor(replace_value(programs, + replace_value(init, <<3, 16#41, 0>>, Programs), Desc))). + +heltec_busy_polarity_test() -> + {ok, Desc} = panel("heltec,lcmen2r13efc1"), + ?assertEqual(1, get_value(busy_idle_level, Desc)), + Standby = get_value(standby, get_value(sleep_modes, Desc)), + ?assertEqual(reset_init, get_value(wake, Standby)), + {ok, LowBusyDesc} = panel("waveshare,epd2in9_V2"), + ?assertEqual(0, get_value(busy_idle_level, LowBusyDesc)). + +timing_options_test() -> + {ok, Desc} = panel("waveshare,epd2in9_V2", + #{timing => #{timeout_ms => 1234, poll_interval_ms => 25}}), + Timing = get_value(timing, Desc), + ?assertEqual(1234, get_value(timeout_ms, Timing)), + ?assertEqual(25, get_value(poll_interval_ms, Timing)). + +integer_field_limits_test() -> + {ok, Desc} = panel("waveshare,epd2in9_V2", #{timeout_ms => 70000}), + ?assertEqual(70000, get_value(timeout_ms, get_value(timing, Desc))), + ?assertError({bad_field, timeout_ms, {out_of_range, 1, 2147483647}}, + panel("waveshare,epd2in9_V2", #{timeout_ms => 2147483648})), + ?assertError({bad_field, max_fast_refreshes, {out_of_range, 0, 2147483647}}, + panel("waveshare,epd2in9_V2", #{max_fast_refreshes => 4294967296})). + uc8151_panel_descriptor_test_() -> [ ?_test(begin @@ -183,7 +345,7 @@ sleep_modes_descriptor_test_() -> ?_test(begin CustomModes = [ {lab, [ - {enter, <<1, 2, 3>>}, + {enter, <<1, 2, 2, 3>>}, {wake, init}, {controller_ram, unknown}, {host_prev_frame, invalidate}, @@ -198,7 +360,7 @@ sleep_modes_descriptor_test_() -> {sleep, [{enter, <<1>>}]}, {sleep, [{enter, <<2>>}]} ], - ?assertError({badmatch, false}, + ?assertError({bad_field, sleep_modes, duplicate}, panel("waveshare,epd2in9_V2", #{sleep_modes => DuplicateModes})) end), ?_test(begin @@ -213,7 +375,7 @@ sleep_modes_descriptor_test_() -> {mode8, [{enter, <<8>>}]}, {mode9, [{enter, <<9>>}]} ], - ?assertError({badmatch, false}, + ?assertError({bad_field, sleep_modes, too_many}, panel("waveshare,epd2in9_V2", #{sleep_modes => TooManyModes})) end) ]. @@ -243,8 +405,64 @@ acep7_panel_descriptor_test_() -> ?assertEqual(1, get_value(post_power_off_busy_level, Desc)) end), ?_test(begin - ?assertError({badmatch, false}, + ?assertError({bad_field, rotation, acep7_native_orientation_only}, panel("waveshare,5in65-acep-7c", #{rotation => 90, view_width => 448, view_height => 600})) + end), + ?_test(begin + ?assertError({bad_field, init_seq, truncated_record}, + panel("waveshare,5in65-acep-7c", #{init_seq => <<0>>})) + end), + ?_test(begin + ?assertError({bad_field, native_width, acep7_width_must_be_even}, + panel("waveshare,5in65-acep-7c", #{native_width => 601})) end) ]. + +unknown_panel_test() -> + ?assertEqual({error, {unsupported_panel, "missing,panel"}}, + panel("missing,panel")). + +assert_previous_frame_order(Compatible) -> + {ok, Descriptor} = panel(Compatible), + Programs = get_value(programs, Descriptor), + FastOps = program_meta_opcodes(get_value(fast, Programs)), + ?assert(opcode_index(?OP_INSERT_PREV_FRAME, FastOps) + < opcode_index(?OP_CAPTURE_FRAME, FastOps)), + ?assert(opcode_index(?OP_CAPTURE_FRAME, FastOps) + < opcode_index(?OP_INSERT_PLANE, FastOps)), + FullOps = program_meta_opcodes(get_value(full, Programs)), + ?assert(opcode_index(?OP_CAPTURE_FRAME, FullOps) + < opcode_index(?OP_INSERT_PLANE, FullOps)), + ?assert(opcode_index(?OP_INSERT_PLANE, FullOps) + < opcode_index(?OP_INSERT_PREV_FRAME, FullOps)). + +program_meta_opcodes(<<>>) -> + []; +program_meta_opcodes(<>) -> + Len = FlagsLen band ?PROGRAM_LEN_MASK, + HasDelay = (FlagsLen band ?PROGRAM_DELAY) =/= 0, + IsMeta = (FlagsLen band ?PROGRAM_META) =/= 0, + <<_Data:Len/binary, Tail0/binary>> = Rest, + Tail = case HasDelay of + true -> + <<_Delay, Next/binary>> = Tail0, + Next; + false -> + Tail0 + end, + case IsMeta of + true -> [Opcode | program_meta_opcodes(Tail)]; + false -> program_meta_opcodes(Tail) + end. + +opcode_index(Opcode, Opcodes) -> + opcode_index(Opcode, Opcodes, 1). + +opcode_index(Opcode, [Opcode | _], Index) -> + Index; +opcode_index(Opcode, [_ | Rest], Index) -> + opcode_index(Opcode, Rest, Index + 1). + +replace_value(Key, Value, Proplist) -> + [{Key, Value} | proplists:delete(Key, Proplist)]. diff --git a/docs/display-drivers.md b/docs/display-drivers.md index 3ede38a..ca6d89a 100644 --- a/docs/display-drivers.md +++ b/docs/display-drivers.md @@ -366,13 +366,13 @@ A completed descriptor is a keyword list with the following shape: default_refresh: :full, palette: :acep7, # ACeP color descriptors use :acep7, :acep7c, or :gdep073e01 palette_size: 7, - programs: %{ + programs: [ init: <<...>>, # Init bytecode full: <<...>>, # Full refresh waveform/cmd sequence fast: <<...>>, # Fast refresh sequence (optional) partial: <<...>>, # Partial refresh sequence (optional) "4gray": <<...>> # 4-gray refresh sequence (optional) - }, + ], sleep_modes: [ sleep: [ enter: <<...>>, # Bytecode program to enter the mode @@ -389,23 +389,23 @@ A completed descriptor is a keyword list with the following shape: after_wake_refresh: :allow_if_program_reseeds ] ], - init_seq: <<...>>, # ACeP color init sequence (optional for program descriptors) + init_seq: <<...>>, # ACeP raw init sequence init_wait_busy_between_cmds: false, # ACeP color init behavior frame_preamble_seq: <<...>>, # ACeP color per-frame preamble (optional) refresh_has_data: false, # ACeP color refresh command behavior refresh_data_byte: 0, post_power_off_busy_level: 0, periodic_refresh_interval: 0, - timing: %{ + timing: [ full_expected_ms: 2000, fast_expected_ms: 500, poll_interval_ms: 50, timeout_ms: 5000 - }, - ghosting: %{ + ], + ghosting: [ max_fast_refreshes: 10, reseed_on_timeout: true - }, + ], lut_full: <<...>>, # Waveform LUT (optional) lut_partial: <<...>>, # Waveform LUT (optional) lut_4gray: <<...>>, # Waveform LUT (optional) @@ -413,7 +413,9 @@ A completed descriptor is a keyword list with the following shape: ] ``` -Program-driven monochrome and 4-gray descriptors use the `programs` and LUT fields. ACeP 7-color descriptors use the palette and ACeP sequence fields (`init_seq`, `frame_preamble_seq`, and refresh settings) instead. Both styles can expose `sleep_modes`. +Program-driven monochrome and 4-gray descriptors use the `programs` and LUT fields. ACeP 7-color descriptors use the palette and ACeP raw sequence fields (`init_seq`, `frame_preamble_seq`, and refresh settings) instead. Both styles can expose `sleep_modes`. Descriptor nested fields are keyword lists/proplists; `panel/2` constructor options are maps. + +Use `atomgl_epaper_program:program/1` with `cmd`, `wait_busy`, `reset`, and render op helpers for interpreted descriptor bytecode. Use `init_seq/1`, `init_cmd/1,2`, and `init_cmd_delay/3` only for ACeP raw init and frame preamble sequences; raw sequences do not support bytecode meta operations. #### Ghosting Policy @@ -453,14 +455,14 @@ When sending updates via the port, you can dynamically choose the refresh mode p If the requested refresh mode is not listed in the descriptor's `refresh_modes`, the driver defaults back to `default_refresh`. Only modes defined and present in the descriptor are permitted. -For slow displays, use `wait_idle` after an asynchronous `update` when the caller needs a queue barrier. It returns after earlier retained display messages have been processed, with `:ok` when the most recent executed update completed successfully and `:error` if the driver detected a failure. +For slow displays, use `wait_idle` after an asynchronous `update` when the caller needs a queue barrier. It returns after earlier retained display messages have been processed, with `:ok` when the most recent executed update completed successfully and `:error` if the driver detected a failure. If the display queue overflows, older pre-acked updates may be dropped before execution, so `wait_idle` reports the last update that actually ran. ```elixir :port.call(display, {:update, items, [refresh: :partial]}, 5000) :port.call(display, {:wait_idle}, 30_000) ``` -The e-paper port also exposes descriptor capabilities at runtime: +The e-paper port also exposes descriptor capabilities at runtime via `{:info}` or the `{:capabilities}` alias: ```elixir :port.call(display, {:info}, 5000) @@ -468,6 +470,10 @@ The e-paper port also exposes descriptor capabilities at runtime: # controller: :ssd16xx, # width: 296, # height: 128, +# native_width: 128, +# native_height: 296, +# rotation: 90, +# palette_size: 4, # refresh_modes: [:full, :fast, :partial, :"4gray"], # default_refresh: :full, # sleep_modes: [:sleep, :deep_sleep], diff --git a/epaper_commands.c b/epaper_commands.c index 3bfb25d..469a765 100644 --- a/epaper_commands.c +++ b/epaper_commands.c @@ -37,20 +37,57 @@ static TickType_t delay_ms_to_ticks(uint32_t delay_ms) return (delay_ms + portTICK_PERIOD_MS - 1) / portTICK_PERIOD_MS; } -void epaper_execute_init_seq(struct SPIDCBus *bus, int busy_gpio, +bool epaper_validate_init_seq(const uint8_t *seq, size_t seq_len) +{ + if (seq == NULL) { + return seq_len == 0; + } + if (seq_len > EPAPER_MAX_DESCRIPTOR_BINARY_LEN) { + return false; + } + + size_t pos = 0; + while (pos < seq_len) { + if (seq_len - pos < 2) { + return false; + } + pos++; + uint8_t flags_len = seq[pos++]; + uint8_t len = flags_len & 0x7F; + + if (seq_len - pos < len) { + return false; + } + pos += len; + + if (flags_len & EPAPER_INIT_SEQ_DELAY) { + if (seq_len - pos < 1) { + return false; + } + pos++; + } + } + return true; +} + +bool epaper_execute_init_seq(struct SPIDCBus *bus, int busy_gpio, const uint8_t *seq, size_t seq_len, bool wait_busy_between_cmds) { - const uint8_t *end = seq + seq_len; - while (seq < end) { - uint8_t cmd = *seq++; - uint8_t flags_len = *seq++; + if (!epaper_validate_init_seq(seq, seq_len)) { + return false; + } + + size_t pos = 0; + while (pos < seq_len) { + uint8_t cmd = seq[pos++]; + uint8_t flags_len = seq[pos++]; uint8_t len = flags_len & 0x7F; - spi_dc_write_cmd_data(bus, cmd, seq, len); - seq += len; + spi_dc_write_cmd_data(bus, cmd, seq + pos, len); + pos += len; if (flags_len & EPAPER_INIT_SEQ_DELAY) { - uint8_t delay_ms = *seq++; + uint8_t delay_ms = seq[pos++]; if (delay_ms > 0) { vTaskDelay(delay_ms_to_ticks(delay_ms)); } @@ -60,4 +97,6 @@ void epaper_execute_init_seq(struct SPIDCBus *bus, int busy_gpio, wait_busy_high(busy_gpio); } } + + return true; } diff --git a/epaper_commands.h b/epaper_commands.h index 385ebaa..2926f86 100644 --- a/epaper_commands.h +++ b/epaper_commands.h @@ -43,14 +43,15 @@ // 0x00, which is PSR on most Waveshare / GoodDisplay controllers. #define EPAPER_INIT_SEQ_DELAY 0x80 - // Execute an init sequence over an SPI+DC bus. seq/seq_len describe // a byte array in the format documented above. When // wait_busy_between_cmds is true, a busy-high poll is inserted after // each command (mirroring the Good Display / Waveshare convention of // "command completes when BUSY rises"). busy_gpio is ignored when // the flag is false. -void epaper_execute_init_seq(struct SPIDCBus *bus, int busy_gpio, +bool epaper_validate_init_seq(const uint8_t *seq, size_t seq_len); + +bool epaper_execute_init_seq(struct SPIDCBus *bus, int busy_gpio, const uint8_t *seq, size_t seq_len, bool wait_busy_between_cmds); // --- Per-panel descriptor --- diff --git a/epaper_display_driver.c b/epaper_display_driver.c index d1cda1f..05d8088 100644 --- a/epaper_display_driver.c +++ b/epaper_display_driver.c @@ -26,6 +26,7 @@ #include #include +#include #include #include #include @@ -38,6 +39,7 @@ #include #include #include +#include #include #include @@ -67,6 +69,7 @@ static const char *TAG = "epaper_display_driver"; #define UC8276_LUT_PAYLOAD_LEN 227 #define UC8276_LUT_WITH_REGS_LEN 233 #define EPAPER_TERM_PROGRAM_COUNT 5 +#define EPAPER_MAX_SPI_CLOCK_HZ 80000000 struct EPaperState { @@ -130,7 +133,7 @@ static const struct EPaperDesc epaper_desc_term_default = { .rotation = 0, .spi_clock_hz = 4000000, .palette = NULL, - .palette_size = 0, + .palette_size = 2, .controller = EPAPER_CONTROLLER_SSD16XX, .layout = { .byte_order = EPAPER_BYTE_ORDER_ROW_MAJOR, @@ -141,6 +144,13 @@ static const struct EPaperDesc epaper_desc_term_default = { .busy_idle_level = 0, .refresh_mode_mask = EPAPER_REFRESH_MODE_FULL, .descriptor_version = 3, + .full_expected_ms = 2000, + .fast_expected_ms = 500, + .poll_interval_ms = 50, + .timeout_ms = 5000, + .max_fast_refreshes = 10, + .reseed_on_timeout = true, + .default_refresh = EPAPER_REFRESH_FULL, }; static bool epaper_desc_requires_program(const struct EPaperDesc *desc) @@ -188,13 +198,14 @@ static void wait_busy_level(struct EpaperDriver *driver, int level) // partial-refresh previous frame so the next partial update re-seeds // via a full base refresh. static bool wait_busy_timeout(struct EpaperDriver *driver, int level, - uint16_t timeout_ms) + uint32_t timeout_ms) { TickType_t start = xTaskGetTickCount(); - TickType_t timeout = pdMS_TO_TICKS(timeout_ms); + // Descriptor timeouts can exceed 16 bits; avoid overflow in tick conversion. + TickType_t timeout = (TickType_t) (((uint64_t) timeout_ms * configTICK_RATE_HZ + 999) / 1000); while (gpio_get_level(driver->busy_gpio) != level) { - if ((xTaskGetTickCount() - start) > timeout) { + if ((TickType_t) (xTaskGetTickCount() - start) >= timeout) { if (driver->state.prev_valid) { ESP_LOGW(TAG, "BUSY wait for level %d timed out for %s; invalidating partial prev frame.", @@ -272,7 +283,9 @@ static bool epaper_write_lut_bytes(struct EpaperDriver *driver, } spi_dc_write_cmd_data(&driver->bus, 0x32, lut, SSD1680_LUT_PAYLOAD_LEN); - wait_busy_low_timeout(driver); + if (!wait_busy_low_timeout(driver)) { + return false; + } if (lut_len < SSD1680_LUT_WITH_REGS_LEN) { return true; @@ -331,13 +344,14 @@ static void maybe_refresh(Context *ctx) } } -static void send_frame_preamble(struct EpaperDriver *driver) +static bool send_frame_preamble(struct EpaperDriver *driver) { if (driver->desc->frame_preamble_seq != NULL) { - epaper_execute_init_seq(&driver->bus, driver->busy_gpio, + return epaper_execute_init_seq(&driver->bus, driver->busy_gpio, driver->desc->frame_preamble_seq, driver->desc->frame_preamble_seq_len, false); } + return true; } static void send_post_frame_refresh(struct EpaperDriver *driver) @@ -643,6 +657,8 @@ struct EPaperProgramRun BaseDisplayItem *items; int items_len; bool capture_next_plane; + bool capture_succeeded; + bool mark_prev_valid_requested; bool is_4gray; }; @@ -698,6 +714,10 @@ static bool epaper_program_insert_plane(void *ctx, uint8_t plane_id) uint8_t *capture_frame = NULL; if (run->capture_next_plane) { + if (run->is_4gray) { + ESP_LOGE(TAG, "CAPTURE_FRAME is not supported for 4-gray frames."); + return false; + } if (!ensure_prev_frame(driver)) { return false; } @@ -719,8 +739,15 @@ static bool epaper_program_insert_plane(void *ctx, uint8_t plane_id) if (run->line_buf == NULL) { return false; } - return write_mono_plane(driver, run->plane_buf, run->line_buf, - run->items, run->items_len, true, capture_frame); + if (!write_mono_plane(driver, run->plane_buf, run->line_buf, + run->items, run->items_len, true, capture_frame)) { + return false; + } + if (capture_frame != NULL) { + run->capture_succeeded = true; + driver->state.prev_valid = false; + } + return true; case 1: if ((driver->desc->controller == EPAPER_CONTROLLER_SSD16XX || driver->desc->controller == EPAPER_CONTROLLER_UC8276) @@ -734,8 +761,15 @@ static bool epaper_program_insert_plane(void *ctx, uint8_t plane_id) if (run->line_buf == NULL) { return false; } - return write_mono_plane(driver, run->plane_buf, run->line_buf, - run->items, run->items_len, false, capture_frame); + if (!write_mono_plane(driver, run->plane_buf, run->line_buf, + run->items, run->items_len, false, capture_frame)) { + return false; + } + if (capture_frame != NULL) { + run->capture_succeeded = true; + driver->state.prev_valid = false; + } + return true; default: ESP_LOGE(TAG, "Unsupported INSERT_PLANE id %d.", plane_id); return false; @@ -761,10 +795,16 @@ static bool epaper_program_insert_lut(void *ctx, uint8_t slot_id) static bool epaper_program_insert_prev_frame(void *ctx, uint8_t plane_id) { - (void) plane_id; struct EPaperProgramDriverContext *program_ctx = ctx; struct EpaperDriver *driver = program_ctx->driver; - if (!ensure_prev_frame(driver)) { + struct EPaperProgramRun *run = program_ctx->run; + bool cached_frame_available = driver->state.prev_valid + || driver->state.after_wake_can_reseed_prev + || (run != NULL && run->capture_succeeded); + if (plane_id != 0 || !cached_frame_available + || driver->prev_frame == NULL + || driver->prev_frame_len != mono_frame_bytes(driver)) { + ESP_LOGE(TAG, "INSERT_PREV_FRAME used without a valid plane 0 cache."); return false; } write_mono_frame(driver, driver->prev_frame); @@ -784,7 +824,11 @@ static bool epaper_program_capture_frame(void *ctx) static bool epaper_program_mark_prev_valid(void *ctx) { struct EPaperProgramDriverContext *program_ctx = ctx; - program_ctx->driver->state.prev_valid = true; + if (program_ctx->run == NULL || !program_ctx->run->capture_succeeded) { + ESP_LOGE(TAG, "MARK_PREV_VALID used without a successful capture."); + return false; + } + program_ctx->run->mark_prev_valid_requested = true; return true; } @@ -807,14 +851,16 @@ static bool epaper_run_driver_program(struct EpaperDriver *driver, .capture_frame = epaper_program_capture_frame, .mark_prev_valid = epaper_program_mark_prev_valid }; - return epaper_run_program(program, &ops); -} - -static void epaper_forget_previous_frame(struct EpaperDriver *driver) -{ - driver->state.prev_valid = false; - driver->state.fast_refresh_count = 0; - driver->state.needs_reseed = false; + bool ok = epaper_run_program(program, &ops); + if (run != NULL) { + if (run->capture_next_plane) { + ok = false; + } + if (run->capture_succeeded) { + driver->state.prev_valid = ok && run->mark_prev_valid_requested; + } + } + return ok; } static void epaper_clear_after_wake_policy(struct EpaperDriver *driver) @@ -824,13 +870,6 @@ static void epaper_clear_after_wake_policy(struct EpaperDriver *driver) driver->state.after_wake_refresh = EPAPER_AFTER_WAKE_REFRESH_ALLOW; } -static void epaper_reset_refresh_history(struct EpaperDriver *driver) -{ - driver->state.last_refresh_mode = EPAPER_REFRESH_FULL; - driver->state.fast_refresh_count = 0; - driver->state.needs_reseed = false; -} - static const struct EPaperSleepMode *epaper_find_sleep_mode(struct EpaperDriver *driver, term mode_term) { @@ -851,21 +890,26 @@ static bool epaper_run_init_sequence(struct EpaperDriver *driver, bool reset_fir { if (reset_first) { display_reset(driver); - wait_busy_level(driver, driver->desc->busy_idle_level); + if (!wait_busy_timeout(driver, driver->desc->busy_idle_level, + driver->desc->timeout_ms)) { + return false; + } } if (driver->desc->init.bytes != NULL) { return epaper_run_driver_program(driver, &driver->desc->init, NULL); } if (driver->desc->init_seq != NULL) { - epaper_execute_init_seq(&driver->bus, driver->busy_gpio, + return epaper_execute_init_seq(&driver->bus, driver->busy_gpio, driver->desc->init_seq, driver->desc->init_seq_len, driver->desc->init_wait_busy_between_cmds); - return true; } if (!reset_first) { display_reset(driver); - wait_busy_level(driver, driver->desc->busy_idle_level); + if (!wait_busy_timeout(driver, driver->desc->busy_idle_level, + driver->desc->timeout_ms)) { + return false; + } } return true; } @@ -904,7 +948,7 @@ static bool epaper_wake_if_asleep(struct EpaperDriver *driver) if (!(host_prev_preserved && controller_ram_retained)) { driver->state.prev_valid = false; } - epaper_reset_refresh_history(driver); + // Reset/init does not constitute a full optical refresh. driver->state.after_wake_pending = true; driver->state.after_wake_can_reseed_prev = host_prev_preserved && prev_valid_before_wake; @@ -938,7 +982,7 @@ static bool do_sleep(Context *ctx, term mode_term) driver->state.asleep_mode = mode; epaper_clear_after_wake_policy(driver); if (mode->host_prev_frame == EPAPER_HOST_PREV_FRAME_INVALIDATE) { - epaper_forget_previous_frame(driver); + driver->state.prev_valid = false; } return true; } @@ -1140,6 +1184,8 @@ static bool do_update_descriptor_mono(struct EpaperDriver *driver, .items = items, .items_len = items_len, .capture_next_plane = false, + .capture_succeeded = false, + .mark_prev_valid_requested = false, .is_4gray = false }; @@ -1196,9 +1242,18 @@ static bool do_update_descriptor_4gray(struct EpaperDriver *driver, .items = items, .items_len = items_len, .capture_next_plane = false, + .capture_succeeded = false, + .mark_prev_valid_requested = false, .is_4gray = is_4gray }; + if (is_4gray) { + // Neither gray plane represents a cached monochrome optical frame. + // Invalidate before execution, since a failed program may still refresh. + driver->state.prev_valid = false; + driver->state.after_wake_can_reseed_prev = false; + } + bool ok = epaper_run_driver_program(driver, program, &run); if (!ok) { ESP_LOGW(TAG, "E-paper 4-gray refresh program failed for %s.", @@ -1265,7 +1320,10 @@ static bool do_update(Context *ctx, term display_list, term update_opts) return ok; } - send_frame_preamble(driver); + if (!send_frame_preamble(driver)) { + display_items_delete(items, len); + return false; + } // DTM — data transfer to panel memory. spi_dc_write_command(&driver->bus, 0x10); @@ -1486,7 +1544,9 @@ static void clear_screen_internal(Context *ctx, int color, int screen_width = driver->screen.w; int screen_height = driver->screen.h; - send_frame_preamble(driver); + if (!send_frame_preamble(driver)) { + return; + } spi_dc_write_command(&driver->bus, 0x10); @@ -1550,7 +1610,11 @@ static bool epaper_parse_int_field(term container, Context *ctx, return true; } if (term_is_integer(value)) { - *out = term_to_int(value); + avm_int_t parsed = term_to_int(value); + if (parsed < INT_MIN || parsed > INT_MAX) { + return false; + } + *out = (int) parsed; return true; } return false; @@ -1589,6 +1653,9 @@ static bool epaper_copy_binary_field(term container, Context *ctx, } size_t value_len = term_binary_size(value); + if (value_len > EPAPER_MAX_DESCRIPTOR_BINARY_LEN) { + return false; + } uint8_t *copy = malloc(value_len == 0 ? 1 : value_len); if (copy == NULL) { return false; @@ -1609,6 +1676,9 @@ static bool epaper_copy_binary_term(term value, } size_t value_len = term_binary_size(value); + if (value_len > EPAPER_MAX_DESCRIPTOR_BINARY_LEN) { + return false; + } uint8_t *copy = malloc(value_len == 0 ? 1 : value_len); if (copy == NULL) { return false; @@ -2014,7 +2084,7 @@ static bool epaper_validate_descriptor_refresh_modes(const struct EPaperDesc *de program = NULL; break; } - if (program->bytes == NULL) { + if (program->bytes == NULL || program->len == 0) { ESP_LOGE(TAG, "refresh_modes declares a mode without a program."); return false; } @@ -2023,6 +2093,43 @@ static bool epaper_validate_descriptor_refresh_modes(const struct EPaperDesc *de return true; } +static bool epaper_validate_descriptor_programs(const struct EPaperDesc *desc) +{ + if (!epaper_validate_program(&desc->init, false) + || !epaper_validate_program(&desc->program_full, true) + || !epaper_validate_program(&desc->program_fast, true) + || !epaper_validate_program(&desc->program_partial, true) + || !epaper_validate_program(&desc->program_4gray, true)) { + ESP_LOGE(TAG, "Invalid e-paper descriptor bytecode program."); + return false; + } + + for (int i = 0; i < desc->sleep_mode_count; i++) { + const struct EPaperSleepMode *mode = &desc->sleep_modes[i]; + if (mode->enter.bytes == NULL || mode->enter.len == 0 + || !epaper_validate_program(&mode->enter, false) + || (mode->wake_policy == EPAPER_WAKE_PROGRAM + && (mode->wake.bytes == NULL || mode->wake.len == 0 + || !epaper_validate_program(&mode->wake, false)))) { + ESP_LOGE(TAG, "Invalid e-paper sleep mode bytecode program."); + return false; + } + } + + return true; +} + +static bool epaper_validate_descriptor_sequences(const struct EPaperDesc *desc) +{ + if (!epaper_validate_init_seq(desc->init_seq, desc->init_seq_len) + || !epaper_validate_init_seq(desc->frame_preamble_seq, + desc->frame_preamble_seq_len)) { + ESP_LOGE(TAG, "Invalid e-paper ACeP raw command sequence."); + return false; + } + return true; +} + static bool epaper_validate_descriptor_geometry(const struct EPaperDesc *desc) { if (desc->native_width <= 0 || desc->native_height <= 0 @@ -2033,6 +2140,33 @@ static bool epaper_validate_descriptor_geometry(const struct EPaperDesc *desc) return false; } + if (desc->native_width > INT_MAX - 7 || desc->native_height > INT_MAX - 7 + || desc->view_width > INT_MAX - 7 || desc->view_height > INT_MAX - 7) { + ESP_LOGE(TAG, "E-paper dimensions exceed safe rounding limits."); + return false; + } + if (desc->controller != EPAPER_CONTROLLER_ACEP7) { + bool column_major = desc->layout.byte_order == EPAPER_BYTE_ORDER_COLUMN_MAJOR; + size_t chunk_bytes = column_major + ? ((size_t) desc->native_height + 7) / 8 + : ((size_t) desc->native_width + 7) / 8; + size_t chunk_count = column_major + ? (size_t) desc->native_width + : (size_t) desc->native_height; + // Keep the ESP32 allocation contract even when validated on a 64-bit host. + if (chunk_count > UINT32_MAX / chunk_bytes) { + ESP_LOGE(TAG, "E-paper frame size exceeds 32-bit allocation limits."); + return false; + } + } + + if (desc->controller == EPAPER_CONTROLLER_ACEP7 + && ((desc->native_width & 1) != 0 || (desc->view_width & 1) != 0)) { + ESP_LOGE(TAG, "ACeP e-paper width must be even: native=%d view=%d.", + desc->native_width, desc->view_width); + return false; + } + if (desc->controller == EPAPER_CONTROLLER_ACEP7 && (desc->rotation != 0 || desc->view_width != desc->native_width @@ -2071,6 +2205,92 @@ static bool epaper_validate_descriptor_geometry(const struct EPaperDesc *desc) return false; } +static bool epaper_validate_descriptor_layout(const struct EPaperDesc *desc) +{ + switch (desc->controller) { + case EPAPER_CONTROLLER_JD79656: + return desc->layout.byte_order == EPAPER_BYTE_ORDER_COLUMN_MAJOR; + case EPAPER_CONTROLLER_SSD16XX: + case EPAPER_CONTROLLER_UC8151: + case EPAPER_CONTROLLER_UC8276: + case EPAPER_CONTROLLER_UC8175: + return desc->layout.byte_order == EPAPER_BYTE_ORDER_ROW_MAJOR; + case EPAPER_CONTROLLER_ACEP7: + return true; + default: + return false; + } +} + +static bool epaper_validate_descriptor_semantics(const struct EPaperDesc *desc) +{ + if (!epaper_validate_descriptor_geometry(desc) + || !epaper_validate_descriptor_refresh_modes(desc) + || !epaper_validate_descriptor_programs(desc) + || !epaper_validate_descriptor_sequences(desc)) { + return false; + } + + if (!epaper_validate_descriptor_layout(desc)) { + ESP_LOGE(TAG, "Invalid frame_layout for e-paper controller."); + return false; + } + if (desc->spi_clock_hz <= 0 || desc->spi_clock_hz > EPAPER_MAX_SPI_CLOCK_HZ) { + ESP_LOGE(TAG, "Invalid e-paper spi_clock_hz: %d.", desc->spi_clock_hz); + return false; + } + if ((desc->busy_idle_level != 0 && desc->busy_idle_level != 1) + || (desc->post_power_off_busy_level != 0 + && desc->post_power_off_busy_level != 1)) { + ESP_LOGE(TAG, "Invalid e-paper BUSY level in descriptor."); + return false; + } + if (desc->palette_size != 2 && desc->palette_size != 4 + && desc->palette_size != 7) { + ESP_LOGE(TAG, "Invalid e-paper palette_size: %d.", desc->palette_size); + return false; + } + if (desc->controller == EPAPER_CONTROLLER_ACEP7) { + if (desc->palette == NULL || desc->palette_size != 7) { + ESP_LOGE(TAG, "ACeP e-paper descriptor requires a known 7-color palette."); + return false; + } + if ((desc->init.bytes == NULL || desc->init.len == 0) + && (desc->init_seq == NULL || desc->init_seq_len == 0)) { + ESP_LOGE(TAG, "ACeP e-paper descriptor requires an init program or init_seq."); + return false; + } + } else { + if (desc->init.bytes == NULL || desc->init.len == 0) { + ESP_LOGE(TAG, "E-paper descriptor requires an init program."); + return false; + } + if (desc->palette_size == 7) { + ESP_LOGE(TAG, "7-color palette_size is only valid for ACeP descriptors."); + return false; + } + if (desc->palette_size == 4 + && desc->controller != EPAPER_CONTROLLER_SSD16XX + && desc->controller != EPAPER_CONTROLLER_UC8276) { + ESP_LOGE(TAG, "4-gray palette_size is not supported by this controller."); + return false; + } + } + if (epaper_refresh_mode_allowed(desc, EPAPER_REFRESH_4GRAY) + && desc->palette_size != 4) { + ESP_LOGE(TAG, "4gray refresh mode requires palette_size 4."); + return false; + } + if (desc->full_expected_ms < 0 || desc->fast_expected_ms < 0 + || desc->poll_interval_ms <= 0 || desc->timeout_ms <= 0 + || desc->max_fast_refreshes < 0 + || desc->periodic_refresh_interval < 0) { + ESP_LOGE(TAG, "Invalid e-paper timing or ghosting value."); + return false; + } + return true; +} + static bool epaper_parse_descriptor_override(struct EpaperDriver *driver, term opts, Context *ctx) { @@ -2249,10 +2469,6 @@ static bool epaper_parse_descriptor_override(struct EpaperDriver *driver, } } - if (!epaper_validate_descriptor_refresh_modes(&driver->term_desc)) { - return false; - } - if (!epaper_parse_sleep_modes(driver, descriptor, ctx)) { return false; } @@ -2340,18 +2556,7 @@ static bool epaper_parse_descriptor_override(struct EpaperDriver *driver, && driver->term_desc.palette_size == 4) { driver->term_desc.palette = epaper_ssd1680_4gray_palette; } - if (driver->term_desc.controller == EPAPER_CONTROLLER_ACEP7 && driver->term_desc.palette == NULL) { - ESP_LOGE(TAG, "ACeP e-paper descriptor requires a known color palette."); - return false; - } - if (driver->term_desc.controller == EPAPER_CONTROLLER_ACEP7 - && driver->term_desc.init.bytes == NULL - && driver->term_desc.init_seq == NULL) { - ESP_LOGE(TAG, "ACeP e-paper descriptor requires an init program or init_seq."); - return false; - } - - if (!epaper_validate_descriptor_geometry(&driver->term_desc)) { + if (!epaper_validate_descriptor_semantics(&driver->term_desc)) { return false; } @@ -2360,14 +2565,14 @@ static bool epaper_parse_descriptor_override(struct EpaperDriver *driver, return true; } -static void display_spi_init(Context *ctx, term opts) +static bool display_spi_init(Context *ctx, term opts) { term descriptor_term = interop_kv_get_value_default( opts, ATOM_STR("\xA", "descriptor"), term_nil(), ctx->global); if (descriptor_term == term_nil()) { ESP_LOGE(TAG, "Failed init: missing e-paper descriptor."); - return; + return false; } const struct EPaperDesc *desc = &epaper_desc_term_default; @@ -2375,7 +2580,7 @@ static void display_spi_init(Context *ctx, term opts) struct EpaperDriver *driver = calloc(1, sizeof(struct EpaperDriver)); if (UNLIKELY(!driver)) { ESP_LOGE(TAG, "Failed init: unable to allocate driver state."); - return; + return false; } driver->desc = desc; @@ -2383,20 +2588,20 @@ static void display_spi_init(Context *ctx, term opts) if (!epaper_parse_descriptor_override(driver, opts, ctx)) { ESP_LOGE(TAG, "Failed init: invalid descriptor override."); epaper_free_init_failed_driver(driver, false); - return; + return false; } desc = driver->desc; if (!epaper_validate_descriptor_geometry(desc)) { ESP_LOGE(TAG, "Failed init: invalid descriptor geometry for '%s'.", desc->name); epaper_free_init_failed_driver(driver, false); - return; + return false; } if (epaper_desc_requires_program(desc) && desc->program_full.bytes == NULL) { ESP_LOGE(TAG, "Failed init: e-paper descriptor '%s' requires a full refresh program.", desc->name); epaper_free_init_failed_driver(driver, false); - return; + return false; } driver->screen.w = desc->view_width; @@ -2409,7 +2614,7 @@ static void display_spi_init(Context *ctx, term opts) if (driver->display_args.messages_queue == NULL) { ESP_LOGE(TAG, "Failed init: unable to allocate display queue."); epaper_free_init_failed_driver(driver, false); - return; + return false; } driver->display_args.process_message_fn = process_message; driver->display_args.ctx = ctx; @@ -2421,7 +2626,7 @@ static void display_spi_init(Context *ctx, term opts) if (!spi_display_parse_config(&spi_config, opts, ctx->global)) { ESP_LOGE(TAG, "Failed init: invalid SPI display configuration."); epaper_free_init_failed_driver(driver, false); - return; + return false; } spi_display_init(&driver->bus.spi_disp, &spi_config); bool spi_device_added = true; @@ -2432,7 +2637,7 @@ static void display_spi_init(Context *ctx, term opts) if (UNLIKELY(!ok)) { ESP_LOGE(TAG, "Failed init: invalid display GPIOs."); epaper_free_init_failed_driver(driver, spi_device_added); - return; + return false; } gpio_set_direction(driver->reset_gpio, GPIO_MODE_OUTPUT); @@ -2462,15 +2667,22 @@ static void display_spi_init(Context *ctx, term opts) display_init_using_list(driver, init_list); } else if (desc->init.bytes != NULL) { if (!epaper_run_driver_program(driver, &desc->init, NULL)) { - ESP_LOGW(TAG, "E-paper descriptor init program failed for %s.", + ESP_LOGE(TAG, "E-paper descriptor init program failed for %s.", desc->name); + epaper_free_init_failed_driver(driver, spi_device_added); + return false; } } else if (desc->init_seq != NULL) { display_reset(driver); wait_busy_level(driver, desc->busy_idle_level); - epaper_execute_init_seq(&driver->bus, driver->busy_gpio, - desc->init_seq, desc->init_seq_len, - desc->init_wait_busy_between_cmds); + if (!epaper_execute_init_seq(&driver->bus, driver->busy_gpio, + desc->init_seq, desc->init_seq_len, + desc->init_wait_busy_between_cmds)) { + ESP_LOGE(TAG, "E-paper descriptor init_seq failed for %s.", + desc->name); + epaper_free_init_failed_driver(driver, spi_device_added); + return false; + } } update_last_refresh_ts(ctx); @@ -2491,16 +2703,24 @@ static void display_spi_init(Context *ctx, term opts) &driver->display_args, 1, NULL) != pdPASS) { ESP_LOGE(TAG, "Failed init: unable to start display task."); epaper_free_init_failed_driver(driver, spi_device_added); - return; + return false; } #endif + + return true; } Context *epaper_display_create_port(GlobalContext *global, term opts) { Context *ctx = context_new(global); + if (ctx == NULL) { + return NULL; + } ctx->native_handler = display_task_consume_mailbox; - display_spi_init(ctx, opts); + if (!display_spi_init(ctx, opts)) { + scheduler_terminate(ctx); + return NULL; + } return ctx; } diff --git a/epaper_program.c b/epaper_program.c index 3bfa95c..da6c759 100644 --- a/epaper_program.c +++ b/epaper_program.c @@ -76,6 +76,88 @@ static bool epaper_run_meta_opcode(const struct EPaperProgramOps *ops, } } +static bool epaper_validate_meta_opcode(uint8_t opcode, uint8_t len, + bool allow_render_ops) +{ + switch (opcode) { + case EPAPER_PROGRAM_WAIT_BUSY: + case EPAPER_PROGRAM_RESET: + return len == 3; + + case EPAPER_PROGRAM_INSERT_LUT: + return len == 1; + + case EPAPER_PROGRAM_INSERT_PLANE: + case EPAPER_PROGRAM_INSERT_PREV_FRAME: + return allow_render_ops && len == 1; + + case EPAPER_PROGRAM_CAPTURE_FRAME: + case EPAPER_PROGRAM_MARK_PREV_VALID: + return allow_render_ops && len == 0; + + case EPAPER_PROGRAM_LABEL: + return len == 1; + + default: + return false; + } +} + +bool epaper_validate_program(const struct EPaperProgram *program, + bool allow_render_ops) +{ + if (program == NULL || program->bytes == NULL) { + return true; + } + if (program->len > EPAPER_MAX_DESCRIPTOR_BINARY_LEN) { + return false; + } + + const uint8_t *pc = program->bytes; + const uint8_t *end = program->bytes + program->len; + bool capture_pending = false; + bool capture_completed = false; + while (pc < end) { + if ((size_t) (end - pc) < 2) { + return false; + } + + uint8_t opcode = *pc++; + uint8_t flags_len = *pc++; + uint8_t len = flags_len & EPAPER_PROGRAM_LEN_MASK; + if ((size_t) (end - pc) < len) { + return false; + } + + if (flags_len & EPAPER_PROGRAM_META) { + if (!epaper_validate_meta_opcode(opcode, len, allow_render_ops)) { + return false; + } + if (opcode == EPAPER_PROGRAM_CAPTURE_FRAME) { + if (capture_pending) { + return false; + } + capture_pending = true; + } else if (opcode == EPAPER_PROGRAM_INSERT_PLANE && capture_pending) { + capture_pending = false; + capture_completed = true; + } else if (opcode == EPAPER_PROGRAM_MARK_PREV_VALID && !capture_completed) { + return false; + } + } + pc += len; + + if (flags_len & EPAPER_PROGRAM_DELAY) { + if (pc >= end) { + return false; + } + pc++; + } + } + + return !capture_pending; +} + bool epaper_run_program(const struct EPaperProgram *program, const struct EPaperProgramOps *ops) { diff --git a/epaper_program.h b/epaper_program.h index 28f35fd..0ed7ab4 100644 --- a/epaper_program.h +++ b/epaper_program.h @@ -30,6 +30,7 @@ #define EPAPER_PROGRAM_DELAY 0x80 #define EPAPER_PROGRAM_META 0x40 #define EPAPER_PROGRAM_LEN_MASK 0x3F +#define EPAPER_MAX_DESCRIPTOR_BINARY_LEN 4096 enum EPaperProgramOpcode { @@ -81,5 +82,7 @@ struct EPaperProgramOps bool epaper_run_program(const struct EPaperProgram *program, const struct EPaperProgramOps *ops); +bool epaper_validate_program(const struct EPaperProgram *program, + bool allow_render_ops); #endif From 172404edc5ef941f341d9eb685c858c0f9417cac Mon Sep 17 00:00:00 2001 From: Peter M Date: Mon, 12 Oct 2026 16:44:20 +0200 Subject: [PATCH 5/6] epaper: add optional board-level panel power rail Ports can now describe a switchable panel supply: `panel_power` (GPIO), `panel_power_active` (:high by default, :low for inverted rails) and `panel_power_delay_ms` (settle time before the controller is addressed, 100 ms by default). The rail is raised before reset/init and before every wake sequence, and it is dropped only for sleep modes that declare `controller_ram: :lost`: the descriptor has already given up the controller RAM in that mode, so deep_sleep becomes the lowest power state a board with a switchable rail can reach. Retained and unknown modes keep the rail up, so the switch cannot contradict a descriptor that promises retained state. A port that fails to open leaves the rail off, and ports without the option behave exactly as before (no GPIO is driven; GPIO 0 is never touched by an unconfigured rail). Verified with the extracted-function host harness extended with rail coverage (mocked hardware, ASan/UBSan, 100 Hz and 1000 Hz ticks) and a syntax/type check against ESP-IDF 5.5.5 plus AtomVM headers. Signed-off-by: Peter M --- docs/display-drivers.md | 32 +++++++++++ docs/epaper-low-power-specs.md | 16 ++++++ epaper_display_driver.c | 102 +++++++++++++++++++++++++++++++++ 3 files changed, 150 insertions(+) diff --git a/docs/display-drivers.md b/docs/display-drivers.md index ca6d89a..8305c1a 100644 --- a/docs/display-drivers.md +++ b/docs/display-drivers.md @@ -441,6 +441,38 @@ epaper_opts = [ display = :erlang.open_port({:spawn, "display"}, epaper_opts) ``` +#### Panel Power Switch + +Boards that switch the panel's supply (Heltec's Vext rail, Waveshare HAT power +pins and similar) can hand the controlling GPIO to the port: + +| Option | Type | Description | Default | +|--------|------|-------------|---------| +| `panel_power` | integer | GPIO that switches the panel supply | Optional; without it the rail is assumed to be always on | +| `panel_power_active` | atom | `:high` or `:low` active level | `:high` | +| `panel_power_delay_ms` | integer | Settle time after the rail comes up, before reset/init is sent | 100 | + +The driver raises the rail before it resets or initializes the controller, and +again before any wake sequence, then drops it when a sleep mode declares +`controller_ram: :lost` — the descriptor has already given up on the +controller's RAM in that mode, so `{:sleep, :deep_sleep}` becomes the lowest +power state the board can reach. Modes that declare `:retained` or `:unknown` +keep the rail up, so the switch can never contradict a descriptor that promises +retained state. + +```elixir +# Heltec Vision Master E290: the panel is fed from the Vext rail (GPIO 18). +epaper_opts = [ + spi_host: spi_host, + descriptor: descriptor, + cs: 3, + dc: 4, + reset: 5, + busy: 6, + panel_power: 18 +] +``` + #### Runtime Refresh Options When sending updates via the port, you can dynamically choose the refresh mode per frame (rather than being locked to one mode at port creation time): diff --git a/docs/epaper-low-power-specs.md b/docs/epaper-low-power-specs.md index 59522ed..4629574 100644 --- a/docs/epaper-low-power-specs.md +++ b/docs/epaper-low-power-specs.md @@ -19,6 +19,22 @@ AtomGL `standby`, `sleep`, `deep_sleep`, and `wake` behavior. | `waveshare,5in65-acep-7c` | 5.65 inch ACeP 7-color | Waveshare command use / existing AtomGL refresh flow | `0x02`, wait BUSY low | Not exposed by default | `0x02`, wait, `0x07,0xA5` | Hardware reset + init | No partial mode in AtomGL descriptor | | `good-display/gdep073e01` | 7.3 inch Spectra 6, 800x480, GDEP073E01-style | `Waveshare_7.3inch-e-Paper-(E)-user-manual.pdf` | `0x02,0x00`; BUSY low while active, high when idle | Not documented | `0x02,0x00`, wait, `0x07,0xA5` | Hardware reset + init | No partial mode in AtomGL descriptor | +## Board power rails + +The table above describes what each controller can do on its own. Boards that +also switch the panel's supply (Heltec's Vext rail, Waveshare HAT power pins) +report that GPIO to the port as `panel_power`, with `panel_power_active` +(`:high`/`:low`) and `panel_power_delay_ms` (settle time, 100 ms default). + +- The rail is raised before reset/init and before every wake sequence, so the + controller is always powered before it is addressed. +- The rail is dropped when a sleep mode declares `controller_ram: :lost`, since + the descriptor already gives up the controller RAM in that mode. Combined, + `deep_sleep` + `panel_power` is the lowest power state the board offers. +- `:retained` and `:unknown` modes keep the rail up, so the switch never + contradicts a descriptor that promises retained state. +- Ports without `panel_power` behave exactly as before (rail assumed on). + ## `good-display/gdep073e01` Local spec sheet: diff --git a/epaper_display_driver.c b/epaper_display_driver.c index 05d8088..e92bb91 100644 --- a/epaper_display_driver.c +++ b/epaper_display_driver.c @@ -96,6 +96,13 @@ struct EpaperDriver int busy_gpio; int reset_gpio; + // Optional board-level panel power switch (e.g. Heltec Vext, Waveshare HAT + // power pins): -1 when the panel is permanently powered. + int panel_power_gpio; + bool panel_power_active_high; + bool panel_power_on; + uint32_t panel_power_delay_ms; + const struct EPaperDesc *desc; struct EPaperDesc term_desc; char *term_desc_name; @@ -186,6 +193,78 @@ static void epaper_delay_ms(uint32_t delay_ms) vTaskDelay((delay_ms + portTICK_PERIOD_MS - 1) / portTICK_PERIOD_MS); } +// Board-level panel power switch. Ports may pass `panel_power` (GPIO number), +// `panel_power_active` (:high by default, :low for inverted switches) and +// `panel_power_delay_ms` (settle time before the controller is touched, 100 ms +// by default). Without the option the panel is assumed to be always powered, +// which is what the desktop/bench panels do. +#define EPAPER_PANEL_POWER_DEFAULT_DELAY_MS 100 + +static bool epaper_parse_panel_power(struct EpaperDriver *driver, term opts, Context *ctx) +{ + GlobalContext *global = ctx->global; + + term gpio_term = interop_kv_get_value_default( + opts, ATOM_STR("\xB", "panel_power"), term_invalid_term(), global); + if (gpio_term == term_invalid_term()) { + return true; + } + if (!term_is_integer(gpio_term)) { + ESP_LOGE(TAG, "Invalid panel_power option: expected a GPIO number."); + return false; + } + driver->panel_power_gpio = (int) term_to_int(gpio_term); + driver->panel_power_active_high = true; + driver->panel_power_delay_ms = EPAPER_PANEL_POWER_DEFAULT_DELAY_MS; + + term active_term = interop_kv_get_value_default( + opts, ATOM_STR("\x12", "panel_power_active"), term_invalid_term(), global); + if (active_term != term_invalid_term()) { + if (active_term == context_make_atom(ctx, ATOM_STR("\x4", "high"))) { + driver->panel_power_active_high = true; + } else if (active_term == context_make_atom(ctx, ATOM_STR("\x3", "low"))) { + driver->panel_power_active_high = false; + } else { + ESP_LOGE(TAG, "Invalid panel_power_active option: expected high or low."); + return false; + } + } + + term delay_term = interop_kv_get_value_default( + opts, ATOM_STR("\x14", "panel_power_delay_ms"), term_invalid_term(), global); + if (delay_term != term_invalid_term()) { + avm_int_t delay_ms = term_is_integer(delay_term) ? term_to_int(delay_term) : -1; + if (delay_ms < 0) { + ESP_LOGE(TAG, "Invalid panel_power_delay_ms option: expected milliseconds >= 0."); + return false; + } + driver->panel_power_delay_ms = (uint32_t) delay_ms; + } + + ESP_LOGI(TAG, "Panel power switch: GPIO%d, %s, %ums settle delay.", + driver->panel_power_gpio, driver->panel_power_active_high ? "active high" : "active low", + (unsigned) driver->panel_power_delay_ms); + return true; +} + +// Raises or drops the panel rail. Idempotent, so callers may assert power on +// every path that is about to talk to the controller. +static void epaper_set_panel_power(struct EpaperDriver *driver, bool on) +{ + if (driver->panel_power_gpio < 0 || on == driver->panel_power_on) { + return; + } + + int on_level = driver->panel_power_active_high ? 1 : 0; + driver->panel_power_on = on; + gpio_set_direction(driver->panel_power_gpio, GPIO_MODE_OUTPUT); + gpio_set_level(driver->panel_power_gpio, on ? on_level : !on_level); + if (on) { + // The controller must be powered before it accepts reset and commands. + epaper_delay_ms(driver->panel_power_delay_ms); + } +} + static void wait_busy_level(struct EpaperDriver *driver, int level) { while (gpio_get_level(driver->busy_gpio) != level) { @@ -933,6 +1012,9 @@ static bool epaper_wake_if_asleep(struct EpaperDriver *driver) } const struct EPaperSleepMode *mode = driver->state.asleep_mode; + // A sleep mode with lost controller RAM also drops the panel rail, so it + // has to be raised (and allowed to settle) before the wake sequence runs. + epaper_set_panel_power(driver, true); if (!epaper_run_wake_sequence(driver, mode)) { ESP_LOGW(TAG, "E-paper wake sequence failed for %s.", driver->desc->name); return false; @@ -984,6 +1066,11 @@ static bool do_sleep(Context *ctx, term mode_term) if (mode->host_prev_frame == EPAPER_HOST_PREV_FRAME_INVALIDATE) { driver->state.prev_valid = false; } + if (mode->controller_ram == EPAPER_CONTROLLER_RAM_LOST) { + // The descriptor says the controller keeps nothing across this mode, so + // the panel rail can stay down until something wakes it again. + epaper_set_panel_power(driver, false); + } return true; } @@ -1742,6 +1829,9 @@ static void epaper_free_init_failed_driver(struct EpaperDriver *driver, free(driver->prev_frame); driver->prev_frame = NULL; epaper_free_descriptor_override(driver); + // Nobody can talk to the panel through this port any more, so leave the + // rail off instead of powering a controller that is not being driven. + epaper_set_panel_power(driver, false); free(driver); } @@ -2582,6 +2672,9 @@ static bool display_spi_init(Context *ctx, term opts) ESP_LOGE(TAG, "Failed init: unable to allocate driver state."); return false; } + // No panel power switch unless the port asks for one: GPIO 0 must never be + // driven by an unconfigured rail. + driver->panel_power_gpio = -1; driver->desc = desc; driver->ctx = ctx; @@ -2650,6 +2743,15 @@ static bool display_spi_init(Context *ctx, term opts) gpio_set_level(driver->bus.dc_gpio, 0); } + // Raise the panel rail (when the board has one) before reset or any + // controller command leaves the port. + if (!epaper_parse_panel_power(driver, opts, ctx)) { + ESP_LOGE(TAG, "Failed init: invalid panel power configuration."); + epaper_free_init_failed_driver(driver, spi_device_added); + return false; + } + epaper_set_panel_power(driver, true); + driver->state.prev_valid = false; driver->state.fast_refresh_count = 0; driver->state.needs_reseed = false; From 17c564d428814aefe8dbc3f7d3faf5c15cc4bf09 Mon Sep 17 00:00:00 2001 From: Peter M Date: Thu, 22 Oct 2026 23:49:12 +0200 Subject: [PATCH 6/6] epaper: handle panel power GPIO failures Signed-off-by: Peter M --- epaper_display_driver.c | 40 ++++++++++++++++++++++++++++++++-------- 1 file changed, 32 insertions(+), 8 deletions(-) diff --git a/epaper_display_driver.c b/epaper_display_driver.c index e92bb91..affaa94 100644 --- a/epaper_display_driver.c +++ b/epaper_display_driver.c @@ -213,7 +213,12 @@ static bool epaper_parse_panel_power(struct EpaperDriver *driver, term opts, Con ESP_LOGE(TAG, "Invalid panel_power option: expected a GPIO number."); return false; } - driver->panel_power_gpio = (int) term_to_int(gpio_term); + avm_int_t gpio = term_to_int(gpio_term); + if (gpio < 0 || gpio > INT_MAX || !GPIO_IS_VALID_OUTPUT_GPIO((int) gpio)) { + ESP_LOGE(TAG, "Invalid panel_power option: expected an output-capable GPIO."); + return false; + } + driver->panel_power_gpio = (int) gpio; driver->panel_power_active_high = true; driver->panel_power_delay_ms = EPAPER_PANEL_POWER_DEFAULT_DELAY_MS; @@ -249,20 +254,25 @@ static bool epaper_parse_panel_power(struct EpaperDriver *driver, term opts, Con // Raises or drops the panel rail. Idempotent, so callers may assert power on // every path that is about to talk to the controller. -static void epaper_set_panel_power(struct EpaperDriver *driver, bool on) +static bool epaper_set_panel_power(struct EpaperDriver *driver, bool on) { if (driver->panel_power_gpio < 0 || on == driver->panel_power_on) { - return; + return true; } int on_level = driver->panel_power_active_high ? 1 : 0; + if (gpio_set_direction(driver->panel_power_gpio, GPIO_MODE_OUTPUT) != ESP_OK + || gpio_set_level(driver->panel_power_gpio, on ? on_level : !on_level) != ESP_OK) { + ESP_LOGE(TAG, "Failed to switch panel power GPIO%d %s.", + driver->panel_power_gpio, on ? "on" : "off"); + return false; + } driver->panel_power_on = on; - gpio_set_direction(driver->panel_power_gpio, GPIO_MODE_OUTPUT); - gpio_set_level(driver->panel_power_gpio, on ? on_level : !on_level); if (on) { // The controller must be powered before it accepts reset and commands. epaper_delay_ms(driver->panel_power_delay_ms); } + return true; } static void wait_busy_level(struct EpaperDriver *driver, int level) @@ -1014,9 +1024,15 @@ static bool epaper_wake_if_asleep(struct EpaperDriver *driver) const struct EPaperSleepMode *mode = driver->state.asleep_mode; // A sleep mode with lost controller RAM also drops the panel rail, so it // has to be raised (and allowed to settle) before the wake sequence runs. - epaper_set_panel_power(driver, true); + if (!epaper_set_panel_power(driver, true)) { + return false; + } if (!epaper_run_wake_sequence(driver, mode)) { ESP_LOGW(TAG, "E-paper wake sequence failed for %s.", driver->desc->name); + // Keep the port asleep and restore the rail-off state of lost-RAM modes. + if (mode != NULL && mode->controller_ram == EPAPER_CONTROLLER_RAM_LOST) { + epaper_set_panel_power(driver, false); + } return false; } @@ -1045,6 +1061,11 @@ static bool do_sleep(Context *ctx, term mode_term) struct EpaperDriver *driver = EPAPER_DRIVER_FROM_CTX(ctx); if (driver->state.asleep) { + // Retry power-off if a previous sleep or failed wake could not drop it. + if (driver->state.asleep_mode != NULL + && driver->state.asleep_mode->controller_ram == EPAPER_CONTROLLER_RAM_LOST) { + return epaper_set_panel_power(driver, false); + } return true; } @@ -1069,7 +1090,7 @@ static bool do_sleep(Context *ctx, term mode_term) if (mode->controller_ram == EPAPER_CONTROLLER_RAM_LOST) { // The descriptor says the controller keeps nothing across this mode, so // the panel rail can stay down until something wakes it again. - epaper_set_panel_power(driver, false); + return epaper_set_panel_power(driver, false); } return true; } @@ -2750,7 +2771,10 @@ static bool display_spi_init(Context *ctx, term opts) epaper_free_init_failed_driver(driver, spi_device_added); return false; } - epaper_set_panel_power(driver, true); + if (!epaper_set_panel_power(driver, true)) { + epaper_free_init_failed_driver(driver, spi_device_added); + return false; + } driver->state.prev_valid = false; driver->state.fast_refresh_count = 0;