Update epdiy library

- ESP32 Platform from 2025.07.31 to 2025.08.30, Framework (Arduino Core) from v3.1.3.250712 to v3.1.3.250808 and IDF from v5.3.3.250707 to v5.3.3.250801 (#23778)
- Epdiy library from v1.0.0 to v2.0.0
This commit is contained in:
Theo Arends 2025-08-12 16:15:58 +02:00
parent 7ebb747dc3
commit 2deb34e856
104 changed files with 9309 additions and 4956 deletions

View File

@ -9,6 +9,8 @@ All notable changes to this project will be documented in this file.
### Breaking Changed
### Changed
- ESP32 Platform from 2025.07.31 to 2025.08.30, Framework (Arduino Core) from v3.1.3.250712 to v3.1.3.250808 and IDF from v5.3.3.250707 to v5.3.3.250801 (#23778)
- Epdiy library from v1.0.0 to v2.0.0
### Fixed
- Syslog RFC5424 compliance (#23509)

View File

@ -132,8 +132,9 @@ The latter links can be used for OTA upgrades too like ``OtaUrl https://ota.tasm
### Breaking Changed
### Changed
- ESP32 Platform from 2025.05.30 to 2025.07.31, Framework (Arduino Core) from v3.1.3.250504 to v3.1.3.250712 and IDF from v5.3.3.250501 to v5.3.3.250707 [#23685](https://github.com/arendst/Tasmota/issues/23685)
- ESP8266 platform update from 2025.05.00 to 2025.07.00 [#23700](https://github.com/arendst/Tasmota/issues/23700)
- ESP32 Platform from 2025.05.30 to 2025.08.30, Framework (Arduino Core) from v3.1.3.250504 to v3.1.3.250808 and IDF from v5.3.3.250501 to v5.3.3.250801 [#23778](https://github.com/arendst/Tasmota/issues/23778)
- Epdiy library from v1.0.0 to v2.0.0
- OpenTherm library from v0.9.0 to v1.1.5 [#23704](https://github.com/arendst/Tasmota/issues/23704)
- Library names [#23560](https://github.com/arendst/Tasmota/issues/23560)
- CSS uses named colors variables [#23597](https://github.com/arendst/Tasmota/issues/23597)

View File

@ -0,0 +1,11 @@
BasedOnStyle: chromium
IndentWidth: 4
ColumnLimit: 100
AlignAfterOpenBracket: BlockIndent
IncludeBlocks: Preserve
BreakBeforeBinaryOperators: All
Cpp11BracedListStyle: false
AllowAllParametersOfDeclarationOnNextLine: true
BinPackArguments: false
BinPackParameters: false
SortIncludes: false

View File

@ -1,6 +1,9 @@
.pio
.vscode
build/
build.clang
sdkconfig.old
sdkconfig
**/build/
.ccls-cache
doc/source/xml/
@ -16,3 +19,10 @@ fp-info-cache
__pycache__
examples/weather/components
sdkconfig
managed_components/
epaper-breakout-backups/
dependencies.lock
ED*.h
ES*.h
examples/private_*/
*.code-workspace

View File

@ -1,6 +0,0 @@
[submodule "hardware/epaper-breakout/esp32-wrover-kicad"]
path = hardware/epaper-breakout/esp32-wrover-kicad
url = https://github.com/aliafshar/esp32-wrover-kicad
[submodule "hardware/epaper-breakout/tp4056"]
path = hardware/epaper-breakout/tp4056
url = https://github.com/alltheworld/tp4056/

View File

@ -12,8 +12,12 @@ sphinx:
# Optionally build your docs in additional formats such as PDF and ePub
formats: all
build:
os: ubuntu-22.04
tools:
python: "3.11"
# Optionally set the version of Python and requirements required to build your docs
python:
version: 3.7
install:
- requirements: doc/requirements.txt

View File

@ -0,0 +1,165 @@
GNU LESSER GENERAL PUBLIC LICENSE
Version 3, 29 June 2007
Copyright (C) 2007 Free Software Foundation, Inc. <https://fsf.org/>
Everyone is permitted to copy and distribute verbatim copies
of this license document, but changing it is not allowed.
This version of the GNU Lesser General Public License incorporates
the terms and conditions of version 3 of the GNU General Public
License, supplemented by the additional permissions listed below.
0. Additional Definitions.
As used herein, "this License" refers to version 3 of the GNU Lesser
General Public License, and the "GNU GPL" refers to version 3 of the GNU
General Public License.
"The Library" refers to a covered work governed by this License,
other than an Application or a Combined Work as defined below.
An "Application" is any work that makes use of an interface provided
by the Library, but which is not otherwise based on the Library.
Defining a subclass of a class defined by the Library is deemed a mode
of using an interface provided by the Library.
A "Combined Work" is a work produced by combining or linking an
Application with the Library. The particular version of the Library
with which the Combined Work was made is also called the "Linked
Version".
The "Minimal Corresponding Source" for a Combined Work means the
Corresponding Source for the Combined Work, excluding any source code
for portions of the Combined Work that, considered in isolation, are
based on the Application, and not on the Linked Version.
The "Corresponding Application Code" for a Combined Work means the
object code and/or source code for the Application, including any data
and utility programs needed for reproducing the Combined Work from the
Application, but excluding the System Libraries of the Combined Work.
1. Exception to Section 3 of the GNU GPL.
You may convey a covered work under sections 3 and 4 of this License
without being bound by section 3 of the GNU GPL.
2. Conveying Modified Versions.
If you modify a copy of the Library, and, in your modifications, a
facility refers to a function or data to be supplied by an Application
that uses the facility (other than as an argument passed when the
facility is invoked), then you may convey a copy of the modified
version:
a) under this License, provided that you make a good faith effort to
ensure that, in the event an Application does not supply the
function or data, the facility still operates, and performs
whatever part of its purpose remains meaningful, or
b) under the GNU GPL, with none of the additional permissions of
this License applicable to that copy.
3. Object Code Incorporating Material from Library Header Files.
The object code form of an Application may incorporate material from
a header file that is part of the Library. You may convey such object
code under terms of your choice, provided that, if the incorporated
material is not limited to numerical parameters, data structure
layouts and accessors, or small macros, inline functions and templates
(ten or fewer lines in length), you do both of the following:
a) Give prominent notice with each copy of the object code that the
Library is used in it and that the Library and its use are
covered by this License.
b) Accompany the object code with a copy of the GNU GPL and this license
document.
4. Combined Works.
You may convey a Combined Work under terms of your choice that,
taken together, effectively do not restrict modification of the
portions of the Library contained in the Combined Work and reverse
engineering for debugging such modifications, if you also do each of
the following:
a) Give prominent notice with each copy of the Combined Work that
the Library is used in it and that the Library and its use are
covered by this License.
b) Accompany the Combined Work with a copy of the GNU GPL and this license
document.
c) For a Combined Work that displays copyright notices during
execution, include the copyright notice for the Library among
these notices, as well as a reference directing the user to the
copies of the GNU GPL and this license document.
d) Do one of the following:
0) Convey the Minimal Corresponding Source under the terms of this
License, and the Corresponding Application Code in a form
suitable for, and under terms that permit, the user to
recombine or relink the Application with a modified version of
the Linked Version to produce a modified Combined Work, in the
manner specified by section 6 of the GNU GPL for conveying
Corresponding Source.
1) Use a suitable shared library mechanism for linking with the
Library. A suitable mechanism is one that (a) uses at run time
a copy of the Library already present on the user's computer
system, and (b) will operate properly with a modified version
of the Library that is interface-compatible with the Linked
Version.
e) Provide Installation Information, but only if you would otherwise
be required to provide such information under section 6 of the
GNU GPL, and only to the extent that such information is
necessary to install and execute a modified version of the
Combined Work produced by recombining or relinking the
Application with a modified version of the Linked Version. (If
you use option 4d0, the Installation Information must accompany
the Minimal Corresponding Source and Corresponding Application
Code. If you use option 4d1, you must provide the Installation
Information in the manner specified by section 6 of the GNU GPL
for conveying Corresponding Source.)
5. Combined Libraries.
You may place library facilities that are a work based on the
Library side by side in a single library together with other library
facilities that are not Applications and are not covered by this
License, and convey such a combined library under terms of your
choice, if you do both of the following:
a) Accompany the combined library with a copy of the same work based
on the Library, uncombined with any other library facilities,
conveyed under the terms of this License.
b) Give prominent notice with the combined library that part of it
is a work based on the Library, and explaining where to find the
accompanying uncombined form of the same work.
6. Revised Versions of the GNU Lesser General Public License.
The Free Software Foundation may publish revised and/or new versions
of the GNU Lesser General Public License from time to time. Such new
versions will be similar in spirit to the present version, but may
differ in detail to address new problems or concerns.
Each version is given a distinguishing version number. If the
Library as you received it specifies that a certain numbered version
of the GNU Lesser General Public License "or any later version"
applies to it, you have the option of following the terms and
conditions either of that published version or of any later version
published by the Free Software Foundation. If the Library as you
received it does not specify a version number of the GNU Lesser
General Public License, you may choose any version of the GNU Lesser
General Public License ever published by the Free Software Foundation.
If the Library as you received it specifies that a proxy can decide
whether future versions of the GNU Lesser General Public License shall
apply, that proxy's public statement of acceptance of any version is
permanent authorization for you to choose that version for the
Library.

View File

@ -8,27 +8,41 @@ EXPORTED_MODES ?= 1,2,5,16,17
# Generate waveforms in room temperature range
EXPORT_TEMPERATURE_RANGE ?= 15,35
FORMATTED_FILES := $(shell find ./ -regex '.*\.\(c\|cpp\|h\|ino\)$$' \
-not -regex '.*/\(.ccls-cache\|.cache\|waveforms\|\components\|build\)/.*' \
-not -regex '.*/img_.*.h' \
-not -regex '.*/build.*' \
-not -regex '.*/\(firasans_.*.h\|opensans.*.h\|amiri.h\|alexandria.h\|dragon.h\)' \
-not -regex '.*E[DS][0-9]*[A-Za-z]*[0-9].h')
# the default headers that should come with the distribution
default: \
$(patsubst %,src/epd_driver/waveforms/epdiy_%.h,$(SUPPORTRED_DISPLAYS))
$(patsubst %,src/waveforms/epdiy_%.h,$(SUPPORTRED_DISPLAYS))
clean:
rm src/epd_driver/waveforms/epdiy_*.h
rm src/epd_driver/waveforms/eink_*.h
rm src/waveforms/epdiy_*.h
rm src/waveforms/eink_*.h
src/epd_driver/waveforms/epdiy_%.h: src/epd_driver/waveforms/epdiy_%.json
format:
clang-format --style=file -i $(FORMATTED_FILES)
format-check:
clang-format --style=file --dry-run -Werror $(FORMATTED_FILES)
src/waveforms/epdiy_%.h: src/waveforms/epdiy_%.json
python3 scripts/waveform_hdrgen.py \
--export-modes $(EXPORTED_MODES) \
--temperature-range $(EXPORT_TEMPERATURE_RANGE) \
epdiy_$* < $< > $@
src/epd_driver/waveforms/eink_%.h: src/epd_driver/waveforms/eink_%.json
src/waveforms/eink_%.h: src/waveforms/eink_%.json
python3 scripts/waveform_hdrgen.py \
--export-modes $(EXPORTED_MODES) \
--temperature-range $(EXPORT_TEMPERATURE_RANGE) \
eink_$* < $< > $@
src/epd_driver/waveforms/epdiy_%.json:
src/waveforms/epdiy_%.json:
python3 scripts/epdiy_waveform_gen.py $* > $@
.PHONY: default
.PHONY: default format

View File

@ -1,4 +1,4 @@
[![Documentation Status](https://readthedocs.org/projects/epdiy/badge/?version=latest)](https://epdiy.readthedocs.io/en/latest/?badge=latest) [![Matrix](https://img.shields.io/matrix/epdiy-general:matrix.vroland.de?label=Matrix%20Chat)](https://matrix.to/#/!GUXWriqsBKkWyXzsBK:matrix.vroland.de?via=matrix.vroland.de) [![JoinSlack](https://img.shields.io/badge/Join%20us-on%20Slack-blueviolet.svg)](https://join.slack.com/t/epdiy/shared_invite/zt-mw3iat5g-6jRylNrK2g79HSxeznvmPg)
[![Documentation Status](https://readthedocs.org/projects/epdiy/badge/?version=latest)](https://epdiy.readthedocs.io/en/latest/?badge=latest) [![Matrix](https://img.shields.io/matrix/epdiy-general:matrix.vroland.de?label=Matrix%20Chat)](https://matrix.to/#/!GUXWriqsBKkWyXzsBK:matrix.vroland.de?via=matrix.vroland.de) [![JoinSlack](https://img.shields.io/badge/Join%20us-on%20Slack-blueviolet.svg)](https://join.slack.com/t/epdiy/shared_invite/zt-189eo7328-bs94cfB~eXPbLYAD1rKQcg)
EPDiy E-Paper Driver
=======================================
@ -9,35 +9,53 @@ EPDiy is a driver board which talks to affordable E-Paper (or E-Ink) screens, wh
* No power consumption when not updating
* Sunlight-readable
Ready-made DIY modules for this size and with 4bpp (16 Grayscale) color support are currently quite expensive. This project uses Kindle replacement screens, which are available for 20$ (small) / 30$ (large) on ebay!
Ready-made DIY modules for this size and with 4bpp (16 Grayscale) color support are currently quite expensive and / or slow.
The EPDiy controller can drive the bare display modules, e.g. from old e-Readers, which are available for 20$ (small) / 30$ (large) on ebay!
Additionally, since it is based on the ESP32S3 (V7) / ESP32 (V2-V6) microcontroller, it features WiFi and Bluetooth connectivity.
The EPDiy driver board targets multiple E-Paper displays. As the driving method for all matrix-based E-ink displays seems to be more or less the same, only the right connector and timings are needed. The EPDiy PCB v5 features 33pin, 34pin and a 39pin connectors, which allow to drive the following display types: ED097OC4, ED060SC4, ED097TC2, ED060SC7. For the full list of supported displays, refer to the table below.
The EPDiy driver board targets a range of E-Paper displays, as shown in the table below.
As the driving method for all matrix-based E-ink displays is more or less the same, only the right connector and timings are needed.
The current V7 board has three different display connectors, other display will require an adapter board.
Revision 5 of the board is optimized for the use with LiPo batteries, featuring a LiPo charger and ultra-low deep sleep current.
The controller is optimized for the use with LiPo batteries, featuring a LiPo charger and ultra-low deep sleep current.
This project supports a driver for the ESP-IDF and Arduino. For installation instructions, please refer to the [documentation](https://epdiy.readthedocs.io/en/latest/getting_started.html#getting-your-board).
Note that for epdiy V7, update speeds are significantly lower when using the Arduino IDE, because it does not allow to change
the sub-optimal cache configuration.
Get Inspired
------------
The `examples` directory contains some example applications like a weather station or a screen diagnostic test.
If you want to build something more useful though, how about:
- A serial terminal for connecting to a raspberry pi: [video](https://cdn.hackaday.io/files/1681937195969312/terminal_demo.mp4) [repository](https://github.com/vroland/epdiy-terminal)]
- A Music Player Daemon (MPD) dashboard: [repository](https://github.com/vroland/epdiy-mpd)]
- An e-Paper picture frame: [video](https://www.youtube.com/watch?v=r7AcNQsSZUw)
- And more to come!
Building It
-----------
If you want to build a board right now, there are two possible routes:
On the [EPDiy Hardware Page](https://vroland.github.io/epdiy-hardware/), you'll find a list of all boards and variants, adapters, and helpers.
Next to each board, there are manufacturing files (gerbers), Bill of Materials (BoM), part placement files,
and 3D models ready to use!
- Use the new v5 PCB (`hardware/epaper-breakout/gerbers_v5.zip`).
**So far, I only tested a prototype of it. The newest gerbers should work, but are untested!**
**If you have tested them, please let me know!**
The BOM is available at (`hardware/epaper-breakout/BOM.csv`).
Positioning files for SMT assembly are available at (`hardware/epaper-breakout/gerbers/epaper-breakout-top-pos.csv`).
Please double check the part positioning and Rotation with your assembly service!
More information on the order process and where to find parts is in the [documentation](https://epdiy.readthedocs.io/en/latest/getting_started.html#getting-your-board).
![demo image](doc/source/img/hardware_page.png)
Make sure to select the `V5` board revision in `idf.py menuconfig` when building the examples.
For ordering from JLCPCB for example, ordering is as easy as downloading the zipped gerbers, BoM, and placement file
and uploading them. The process is very similar for other manufacturers, check your vendor's documentation for details.
Don't forget to oder adapters if the board doesn't have connectors for your specific display.
- Use the old v4 PCB (`hardware/epaper-breakout/gerbers_v4.zip`). This is a bit more fresh, but should work.
The BOM is available at (`hardware/epaper-breakout/BOM.csv`).
Positioning files for SMT assembly are available at (`hardware/epaper-breakout/gerbers/epaper-breakout-top-pos.csv`).
Please double check the part positioning and Rotation with your assembly service!
The current latest version is epdiy V7, beased on the ESP32S3.
Older versions are also available on the hardware page.
#### Contributing Hardware
Want to contribute your own board variant or adapter?
Check out the [epdiy-hardware repository](https://github.com/vroland/epdiy-hardware) for instructions.
Make sure to select the `V4` board revision in `idf.py menuconfig` when building the examples.
Gettings Started
----------------
@ -48,37 +66,40 @@ Join the Discussion
----------------
- [![Matrix](https://img.shields.io/matrix/epdiy-general:matrix.vroland.de?label=Join%20Matrix)](https://matrix.to/#/!GUXWriqsBKkWyXzsBK:matrix.vroland.de?via=matrix.vroland.de) Matrix Community: +epdiy:matrix.vroland.de
- [![JoinSlack](https://img.shields.io/badge/Join%20us-on%20Slack-blueviolet.svg)](https://join.slack.com/t/epdiy/shared_invite/zt-mw3iat5g-6jRylNrK2g79HSxeznvmPg)
- Slack: See badge
Displays
--------
|Name |Size |Resolution|Compatible|Connector|Pin count|Compatible since pcb version|Notes
| --: | --: | --: | --: | --: | --: | --: | --: |
|ED060SC4|6"|800 x 600|yes, tested|FH26W-39S-0.3SHW(60)|39|v2|
|ED097OC4|9.7"|1200 x 825|yes, tested|XF2M-3315-1A|33|V2|Cheap, inferior contrast
|ED097TC2|9.7"|1200 x 825|yes, tested|XF2M-3315-1A|33|V2|Slightly higher price, better contrast
|ED097OC1|9.7"|1200 x 825|yes (should work)|XF2M-3315-1A|33|V2|Cheap, inferior performance
|ED047TC1|4.7"|960 x 540|yes, tested|40-pin|40|LILYGO 4.7" EPD|Supported only by 4.7" e-paper board by LILYGO
|ED133UT2|13.3"|1600 x 1200|yes, tested|adapter board|39|V2|Adapter Board required, also PENG133D
|ED060XC3|6"|758 x 1024|yes, tested|THD0515-34CL-SN|34|V5|Cheapest, good contrast and resolution
|ED060XD4|6"|758 x 1024|yes, tested|THD0515-34CL-SN|34|V5|
|ED060XC5|6"|758 x 1024|yes (should work as ED060XC3)|THD0515-34CL-SN|34|V5|
|ED060XD6|6"|758 x 1024|yes (should work as ED060XC3)|THD0515-34CL-SN|34|V5|
|ED060XH2|6"|758 x 1024|yes (should work as ED060XC3)|THD0515-34CL-SN|34|V5|
|ED060XC9|6"|758 x 1024|yes (should work as ED060XC3)|THD0515-34CL-SN|34|V5|
|ED060KD1|6"|1072 x 1448|yes (should work as ED060XC3)|THD0515-34CL-SN|34|V5|
|ED060KC1|6"|1072 x 1448|yes (should work as ED060XC3)|THD0515-34CL-SN|34|V5|
|ED060SCF|6"|600 x 800|yes, tested|THD0515-34CL-SN|34|V5|Different flex cable shape
|ED060SCN|6"|600 x 800|yes (should work as ED060XC3)|THD0515-34CL-SN|34|V5|Different flex cable shape
|ED060SCP|6"|600 x 800|yes (should work as ED060XC3)|THD0515-34CL-SN|34|V5|Different flex cable shape
|ED060SC7|6"|600 x 800|yes (should work) |AXT434124|34|v5|
|ED060SCG|6"|600 x 800|yes (should work) |AXT434124|34|v5|
| ED060SCE | 6" | 600 x 800 | yes (should work) | AXT434124 | 34 | v5 |
| ED060SCM | 6" | 600 x 800 | yes (should work) | AXT434124 | 34 | v5 |
| ED060SCT | 6" | 600 x 800 | yes, tested | AXT434124 | 34 | v5 |
| ED060SC4 | 6" | 800 x 600<br>167 PPI | yes, tested | FH26W-39S-0.3SHW(60) | 39 | v2 | |
|ED097OC4|9.7"|1200 x 825<br>150 PPI|yes, tested|XF2M-3315-1A|33|v2|Cheap, inferior contrast
|ED097TC2|9.7"|1200 x 825<br>150 PPI|yes, tested|XF2M-3315-1A|33|v2|Slightly higher price, better contrast
|ED097OC1|9.7"|1200 x 825<br>150 PPI|yes (should work)|XF2M-3315-1A|33|v2|Cheap, inferior performance
|ED047TC1|4.7"|960 x 540<br>234 PPI|yes, tested|40-pin|40|LILYGO 4.7" EPD|Supported only by 4.7" e-paper board by LILYGO
| ED050SC5 | 5" | 600 x 800<br>200 PPI | yes, tested | THD0510-33CL-GF | 33 | v5 |
| ED050SC3 | 5" | 600 x 800<br>200 PPI | yes (should work) | THD0510-33CL-GF | 33 | v5 |
| ED052TC4 | 5.2" | 1280 x 780<br>??? PPI | yes (should work) | WP27D-P050VA3 | 50 | v5 |
| ED133UT2 | 13.3" | 1600 x 1200<br>150 PPI | yes, tested | adapter board | 39 | v2 | Adapter Board required, also PENG133D
| ED060XC3 | 6" | 758 x 1024<br>212 PPI | yes, tested | THD0515-34CL-SN | 34 | v5 | Cheapest, good contrast and resolution
| ED060XD4 | 6" | 758 x 1024<br>212 PPI | yes, tested | THD0515-34CL-SN | 34 | v5 |
| ED060XC5 | 6" | 758 x 1024<br>212 PPI | yes (should work as ED060XC3) | THD0515-34CL-SN | 34 | v5 |
| ED060XD6 | 6" | 758 x 1024<br>212 PPI | yes (should work as ED060XC3) | THD0515-34CL-SN | 34 | v5 |
| ED060XH2 | 6" | 758 x 1024<br>212 PPI | yes (should work as ED060XC3) | THD0515-34CL-SN | 34 | v5 |
| ED060XC9 | 6" | 758 x 1024<br>212 PPI | yes (should work as ED060XC3) | THD0515-34CL-SN | 34 | v5 |
| ED060KD1 | 6" | 1072 x 1448<br>300 PPI | yes (should work as ED060XC3) | THD0515-34CL-SN | 34 | v5 |
| ED060KC1 | 6" | 1072 x 1448<br>300 PPI | yes (should work as ED060XC3) | THD0515-34CL-SN | 34 | v5 |
| ED060SCF | 6" | 600 x 800<br>167 PPI | yes, tested | THD0515-34CL-SN | 34 | v5 | Different flex cable shape
| ED060SCN | 6" | 600 x 800<br>167 PPI | yes (should work as ED060XC3) | THD0515-34CL-SN | 34 | v5 | Different flex cable shape
| ED060SCP | 6" | 600 x 800<br>167 PPI | yes (should work as ED060XC3) | THD0515-34CL-SN | 34 | v5 | Different flex cable shape
| ED060SC7 | 6" | 600 x 800<br>167 PPI | yes (should work) | AXT334124 | 34 | v5 | connector dropped in v6
| ED060SCG | 6" | 600 x 800<br>167 PPI | yes (should work) | AXT334124 | 34 | v5 | connector dropped in v6
| ED060SCE | 6" | 600 x 800<br>167 PPI | yes (should work) | AXT334124 | 34 | v5 | connector dropped in v6
| ED060SCM | 6" | 600 x 800<br>167 PPI | yes (should work) | AXT334124 | 34 | v5 | connector dropped in v6
| ED060SCT | 6" | 600 x 800<br>167 PPI | yes, tested | AXT334124 | 34 | v5 | connector dropped in v6
| ED078KC1 | 7.8" | 1872 x 1404<br>300 PPI | yes, tested | FH12-40S-0.5SH | 40 | v7 | 16 data lines
**Please note that board revision v5 is still in prototype stage!**
Troubleshooting
---------------
@ -88,6 +109,25 @@ The following list is compiled from past experiences and GitHub issues:
* **The existing image fades / darkens when updating a partial screen region.** Make sure the VCOM voltage is [calibrated](https://epdiy.readthedocs.io/en/latest/getting_started.html#calibrate-vcom) for your specific display.
* **The second third of the image is replaced with the last third.** This seems to be a timing issue we could not yet quite figure out the reason for. For a workarround or suggestions please [join the discussion](https://github.com/vroland/epdiy/issues/15).
* **The ESP does not boot correctly when external periperals are connected.** Make sure not to pull GPIO12 high during boot, as it is a strapping pin internal voltage selection (https://github.com/vroland/epdiy/issues/17).
* **The ESP power consumption in deep sleep is too high.** Add `rtc_gpio_isolate(GPIO_NUM_12);` to your solution. See also [Configuring IOs (Deep-sleep Only)](https://docs.espressif.com/projects/esp-idf/en/latest/esp32/api-reference/system/sleep_modes.html?highlight=rtc_gpio_isolate#configuring-ios-deep-sleep-only).
LilyGo Boards
---------------
There are several differences with these boards.
One particular one is the way the LilyGo handles power to the display the official lilygo code has two states.
This is now handled in epdiy in a different way to the lilygo code.
**epd_poweroff()** completely turns the power off to the display and the other peripherals of the lilygo.
The new function **epd_powerdown()** keeps the peripherals on (this allows the touch functions to continue to work).
**epd_poweroff() should allways be called before sleeping the system**
You can still use touch to wake the screen with the following.
In Arduino it works like this.
`epd_poweroff();`
`epd_deinit();`
`esp_sleep_enable_ext1_wakeup(GPIO_SEL_13, ESP_EXT1_WAKEUP_ANY_HIGH);`
`esp_deep_sleep_start();`
More on E-Paper Displays
------------------------
@ -106,8 +146,7 @@ Hackaday Project
For more details, see the project page on Hackaday: https://hackaday.io/project/168193-epdiy-976-e-paper-controller
![demo image](doc/source/img/demo.jpg)
![board front](doc/source/img/board_p1.jpg)
![board back](doc/source/img/board_p2.jpg)
![board front](doc/source/img/v6.jpg)
Licenses
--------
@ -116,6 +155,5 @@ The weather example is Copyright (c) David Bird 2018 (except for minor modificat
The board and schematic are licensed under a <a rel="license" href="https://creativecommons.org/licenses/by-sa/4.0/"><img alt="Creative Commons License" style="border-width:0" src="https://i.creativecommons.org/l/by-sa/4.0/80x15.png" /></a> <a rel="license" href="https://creativecommons.org/licenses/by-sa/4.0/">Creative Commons Attribution-ShareAlike 4.0 International License</a>.
Firmware and remaining examples are licensed under the terms of the GNU GPL version 3.
Firmware and remaining examples are licensed under the terms of the GNU Lesser GPL version 3.
Utilities are licensed under the terms of the MIT license.

View File

@ -1,452 +0,0 @@
############################## Revision 5 Board ##############################
epdiy_v5.name=EPDIY Board Revision 5
epdiy_v5.upload.tool=esp32:esptool_py
epdiy_v5.upload.maximum_size=1310720
epdiy_v5.upload.maximum_data_size=327680
epdiy_v5.upload.wait_for_upload_port=true
epdiy_v5.serial.disableDTR=true
epdiy_v5.serial.disableRTS=true
epdiy_v5.build.mcu=esp32
epdiy_v5.build.core=esp32:esp32
epdiy_v5.build.variant=esp32
epdiy_v5.build.board=ESP32_EPDIY
epdiy_v5.build.f_cpu=240000000L
epdiy_v5.build.flash_size=4MB
epdiy_v5.build.flash_freq=40m
epdiy_v5.build.flash_mode=dio
epdiy_v5.build.boot=dio
epdiy_v5.build.partitions=default
epdiy_v5.build.defines=
epdiy_v5.menu.DisplayType.default=ED097OC4
epdiy_v5.menu.DisplayType.default.build.defines=-DCONFIG_EPD_DISPLAY_TYPE_ED097OC4 -DBOARD_HAS_PSRAM -mfix-esp32-psram-cache-issue -DCONFIG_EPD_BOARD_REVISION_V5
epdiy_v5.menu.DisplayType.ed097oc4_lq=ED097OC4 Low Quality
epdiy_v5.menu.DisplayType.ed097oc4_lq.build.defines=-DCONFIG_EPD_DISPLAY_TYPE_ED097OC4_LQ -DBOARD_HAS_PSRAM -mfix-esp32-psram-cache-issue -DCONFIG_EPD_BOARD_REVISION_V5
epdiy_v5.menu.DisplayType.ed060sc4=ED060SC4
epdiy_v5.menu.DisplayType.ed060sc4.build.defines=-DCONFIG_EPD_DISPLAY_TYPE_ED060SC4 -DBOARD_HAS_PSRAM -mfix-esp32-psram-cache-issue -DCONFIG_EPD_BOARD_REVISION_V5
epdiy_v5.menu.DisplayType.ed097tc2=ED097TC2
epdiy_v5.menu.DisplayType.ed097tc2.build.defines=-DCONFIG_EPD_DISPLAY_TYPE_ED097TC2 -DBOARD_HAS_PSRAM -mfix-esp32-psram-cache-issue -DCONFIG_EPD_BOARD_REVISION_V5
epdiy_v5.menu.DisplayType.ed047tc1=ED047TC1 (LILYGO 4.7 inch)
epdiy_v5.menu.DisplayType.ed047tc1.build.defines=-DCONFIG_EPD_DISPLAY_TYPE_ED047TC1 -DBOARD_HAS_PSRAM -mfix-esp32-psram-cache-issue -DCONFIG_EPD_BOARD_REVISION_V5
epdiy_v5.menu.DisplayType.ed133ut2=ED133UT2
epdiy_v5.menu.DisplayType.ed133ut2.build.defines=-DCONFIG_EPD_DISPLAY_TYPE_ED133UT2 -DBOARD_HAS_PSRAM -mfix-esp32-psram-cache-issue -DCONFIG_EPD_BOARD_REVISION_V5
epdiy_v5.menu.PartitionScheme.default=Default 4MB with spiffs (1.2MB APP/1.5MB SPIFFS)
epdiy_v5.menu.PartitionScheme.default.build.partitions=default
epdiy_v5.menu.PartitionScheme.defaultffat=Default 4MB with ffat (1.2MB APP/1.5MB FATFS)
epdiy_v5.menu.PartitionScheme.defaultffat.build.partitions=default_ffat
epdiy_v5.menu.PartitionScheme.default_8MB=8M Flash (3MB APP/1.5MB FAT)
epdiy_v5.menu.PartitionScheme.default_8MB.build.partitions=default_8MB
epdiy_v5.menu.PartitionScheme.minimal=Minimal (1.3MB APP/700KB SPIFFS)
epdiy_v5.menu.PartitionScheme.minimal.build.partitions=minimal
epdiy_v5.menu.PartitionScheme.no_ota=No OTA (2MB APP/2MB SPIFFS)
epdiy_v5.menu.PartitionScheme.no_ota.build.partitions=no_ota
epdiy_v5.menu.PartitionScheme.no_ota.upload.maximum_size=2097152
epdiy_v5.menu.PartitionScheme.noota_3g=No OTA (1MB APP/3MB SPIFFS)
epdiy_v5.menu.PartitionScheme.noota_3g.build.partitions=noota_3g
epdiy_v5.menu.PartitionScheme.noota_3g.upload.maximum_size=1048576
epdiy_v5.menu.PartitionScheme.noota_ffat=No OTA (2MB APP/2MB FATFS)
epdiy_v5.menu.PartitionScheme.noota_ffat.build.partitions=noota_ffat
epdiy_v5.menu.PartitionScheme.noota_ffat.upload.maximum_size=2097152
epdiy_v5.menu.PartitionScheme.noota_3gffat=No OTA (1MB APP/3MB FATFS)
epdiy_v5.menu.PartitionScheme.noota_3gffat.build.partitions=noota_3gffat
epdiy_v5.menu.PartitionScheme.noota_3gffat.upload.maximum_size=1048576
epdiy_v5.menu.PartitionScheme.huge_app=Huge APP (3MB No OTA/1MB SPIFFS)
epdiy_v5.menu.PartitionScheme.huge_app.build.partitions=huge_app
epdiy_v5.menu.PartitionScheme.huge_app.upload.maximum_size=3145728
epdiy_v5.menu.PartitionScheme.min_spiffs=Minimal SPIFFS (1.9MB APP with OTA/190KB SPIFFS)
epdiy_v5.menu.PartitionScheme.min_spiffs.build.partitions=min_spiffs
epdiy_v5.menu.PartitionScheme.min_spiffs.upload.maximum_size=1966080
epdiy_v5.menu.PartitionScheme.fatflash=16M Flash (2MB APP/12.5MB FAT)
epdiy_v5.menu.PartitionScheme.fatflash.build.partitions=ffat
epdiy_v5.menu.FlashMode.qio=QIO
epdiy_v5.menu.FlashMode.qio.build.flash_mode=dio
epdiy_v5.menu.FlashMode.qio.build.boot=qio
epdiy_v5.menu.FlashMode.dio=DIO
epdiy_v5.menu.FlashMode.dio.build.flash_mode=dio
epdiy_v5.menu.FlashMode.dio.build.boot=dio
epdiy_v5.menu.FlashMode.qout=QOUT
epdiy_v5.menu.FlashMode.qout.build.flash_mode=dout
epdiy_v5.menu.FlashMode.qout.build.boot=qout
epdiy_v5.menu.FlashMode.dout=DOUT
epdiy_v5.menu.FlashMode.dout.build.flash_mode=dout
epdiy_v5.menu.FlashMode.dout.build.boot=dout
epdiy_v5.menu.FlashFreq.80=80MHz
epdiy_v5.menu.FlashFreq.80.build.flash_freq=80m
epdiy_v5.menu.FlashFreq.40=40MHz
epdiy_v5.menu.FlashFreq.40.build.flash_freq=40m
epdiy_v5.menu.UploadSpeed.921600=921600
epdiy_v5.menu.UploadSpeed.921600.upload.speed=921600
epdiy_v5.menu.UploadSpeed.115200=115200
epdiy_v5.menu.UploadSpeed.115200.upload.speed=115200
epdiy_v5.menu.UploadSpeed.256000.windows=256000
epdiy_v5.menu.UploadSpeed.256000.upload.speed=256000
epdiy_v5.menu.UploadSpeed.230400.windows.upload.speed=256000
epdiy_v5.menu.UploadSpeed.230400=230400
epdiy_v5.menu.UploadSpeed.230400.upload.speed=230400
epdiy_v5.menu.UploadSpeed.460800.linux=460800
epdiy_v5.menu.UploadSpeed.460800.macosx=460800
epdiy_v5.menu.UploadSpeed.460800.upload.speed=460800
epdiy_v5.menu.UploadSpeed.512000.windows=512000
epdiy_v5.menu.UploadSpeed.512000.upload.speed=512000
epdiy_v5.menu.DebugLevel.none=None
epdiy_v5.menu.DebugLevel.none.build.code_debug=0
epdiy_v5.menu.DebugLevel.error=Error
epdiy_v5.menu.DebugLevel.error.build.code_debug=1
epdiy_v5.menu.DebugLevel.warn=Warn
epdiy_v5.menu.DebugLevel.warn.build.code_debug=2
epdiy_v5.menu.DebugLevel.info=Info
epdiy_v5.menu.DebugLevel.info.build.code_debug=3
epdiy_v5.menu.DebugLevel.debug=Debug
epdiy_v5.menu.DebugLevel.debug.build.code_debug=4
epdiy_v5.menu.DebugLevel.verbose=Verbose
epdiy_v5.menu.DebugLevel.verbose.build.code_debug=5
##############################################################
menu.DisplayType=DisplayType
epdiy_v2.name=EPDIY Board Revision 2/3
epdiy_v2.upload.tool=esptool_py
epdiy_v2.upload.maximum_size=1310720
epdiy_v2.upload.maximum_data_size=327680
epdiy_v2.upload.wait_for_upload_port=true
epdiy_v2.serial.disableDTR=true
epdiy_v2.serial.disableRTS=true
epdiy_v2.build.mcu=esp32
epdiy_v2.build.core=esp32:esp32
epdiy_v2.build.variant=esp32
epdiy_v2.build.board=ESP32_EPDIY
epdiy_v2.build.f_cpu=240000000L
epdiy_v2.build.flash_size=4MB
epdiy_v2.build.flash_freq=40m
epdiy_v2.build.flash_mode=dio
epdiy_v2.build.boot=dio
epdiy_v2.build.partitions=default
epdiy_v2.menu.DisplayType.default=ED097OC4
epdiy_v2.menu.DisplayType.default.build.defines=-DCONFIG_EPD_DISPLAY_TYPE_ED097OC4 -DBOARD_HAS_PSRAM -mfix-esp32-psram-cache-issue -DCONFIG_EPD_BOARD_REVISION_V2_V3
epdiy_v2.menu.DisplayType.ed097oc4_lq=ED097OC4 Low Quality
epdiy_v2.menu.DisplayType.ed097oc4_lq.build.defines=-DCONFIG_EPD_DISPLAY_TYPE_ED097OC4_LQ -DBOARD_HAS_PSRAM -mfix-esp32-psram-cache-issue -DCONFIG_EPD_BOARD_REVISION_V2_V3
epdiy_v2.menu.DisplayType.ed060sc4=ED060SC4
epdiy_v2.menu.DisplayType.ed060sc4.build.defines=-DCONFIG_EPD_DISPLAY_TYPE_ED060SC4 -DBOARD_HAS_PSRAM -mfix-esp32-psram-cache-issue -DCONFIG_EPD_BOARD_REVISION_V2_V3
epdiy_v2.menu.DisplayType.ed097tc2=ED097TC2
epdiy_v2.menu.DisplayType.ed097tc2.build.defines=-DCONFIG_EPD_DISPLAY_TYPE_ED097TC2 -DBOARD_HAS_PSRAM -mfix-esp32-psram-cache-issue -DCONFIG_EPD_BOARD_REVISION_V2_V3
epdiy_v2.menu.DisplayType.ed047tc1=ED047TC1 (LILYGO 4.7 inch)
epdiy_v2.menu.DisplayType.ed047tc1.build.defines=-DCONFIG_EPD_DISPLAY_TYPE_ED047TC1 -DBOARD_HAS_PSRAM -mfix-esp32-psram-cache-issue -DCONFIG_EPD_BOARD_REVISION_V2_V3
epdiy_v2.menu.DisplayType.ed133ut2=ED133UT2
epdiy_v2.menu.DisplayType.ed133ut2.build.defines=-DCONFIG_EPD_DISPLAY_TYPE_ED133UT2 -DBOARD_HAS_PSRAM -mfix-esp32-psram-cache-issue -DCONFIG_EPD_BOARD_REVISION_V2_V3
epdiy_v2.menu.PartitionScheme.default=Default 4MB with spiffs (1.2MB APP/1.5MB SPIFFS)
epdiy_v2.menu.PartitionScheme.default.build.partitions=default
epdiy_v2.menu.PartitionScheme.defaultffat=Default 4MB with ffat (1.2MB APP/1.5MB FATFS)
epdiy_v2.menu.PartitionScheme.defaultffat.build.partitions=default_ffat
epdiy_v2.menu.PartitionScheme.default_8MB=8M Flash (3MB APP/1.5MB FAT)
epdiy_v2.menu.PartitionScheme.default_8MB.build.partitions=default_8MB
epdiy_v2.menu.PartitionScheme.minimal=Minimal (1.3MB APP/700KB SPIFFS)
epdiy_v2.menu.PartitionScheme.minimal.build.partitions=minimal
epdiy_v2.menu.PartitionScheme.no_ota=No OTA (2MB APP/2MB SPIFFS)
epdiy_v2.menu.PartitionScheme.no_ota.build.partitions=no_ota
epdiy_v2.menu.PartitionScheme.no_ota.upload.maximum_size=2097152
epdiy_v2.menu.PartitionScheme.noota_3g=No OTA (1MB APP/3MB SPIFFS)
epdiy_v2.menu.PartitionScheme.noota_3g.build.partitions=noota_3g
epdiy_v2.menu.PartitionScheme.noota_3g.upload.maximum_size=1048576
epdiy_v2.menu.PartitionScheme.noota_ffat=No OTA (2MB APP/2MB FATFS)
epdiy_v2.menu.PartitionScheme.noota_ffat.build.partitions=noota_ffat
epdiy_v2.menu.PartitionScheme.noota_ffat.upload.maximum_size=2097152
epdiy_v2.menu.PartitionScheme.noota_3gffat=No OTA (1MB APP/3MB FATFS)
epdiy_v2.menu.PartitionScheme.noota_3gffat.build.partitions=noota_3gffat
epdiy_v2.menu.PartitionScheme.noota_3gffat.upload.maximum_size=1048576
epdiy_v2.menu.PartitionScheme.huge_app=Huge APP (3MB No OTA/1MB SPIFFS)
epdiy_v2.menu.PartitionScheme.huge_app.build.partitions=huge_app
epdiy_v2.menu.PartitionScheme.huge_app.upload.maximum_size=3145728
epdiy_v2.menu.PartitionScheme.min_spiffs=Minimal SPIFFS (1.9MB APP with OTA/190KB SPIFFS)
epdiy_v2.menu.PartitionScheme.min_spiffs.build.partitions=min_spiffs
epdiy_v2.menu.PartitionScheme.min_spiffs.upload.maximum_size=1966080
epdiy_v2.menu.PartitionScheme.fatflash=16M Flash (2MB APP/12.5MB FAT)
epdiy_v2.menu.PartitionScheme.fatflash.build.partitions=ffat
epdiy_v2.menu.FlashMode.qio=QIO
epdiy_v2.menu.FlashMode.qio.build.flash_mode=dio
epdiy_v2.menu.FlashMode.qio.build.boot=qio
epdiy_v2.menu.FlashMode.dio=DIO
epdiy_v2.menu.FlashMode.dio.build.flash_mode=dio
epdiy_v2.menu.FlashMode.dio.build.boot=dio
epdiy_v2.menu.FlashMode.qout=QOUT
epdiy_v2.menu.FlashMode.qout.build.flash_mode=dout
epdiy_v2.menu.FlashMode.qout.build.boot=qout
epdiy_v2.menu.FlashMode.dout=DOUT
epdiy_v2.menu.FlashMode.dout.build.flash_mode=dout
epdiy_v2.menu.FlashMode.dout.build.boot=dout
epdiy_v2.menu.FlashFreq.80=80MHz
epdiy_v2.menu.FlashFreq.80.build.flash_freq=80m
epdiy_v2.menu.FlashFreq.40=40MHz
epdiy_v2.menu.FlashFreq.40.build.flash_freq=40m
epdiy_v2.menu.UploadSpeed.921600=921600
epdiy_v2.menu.UploadSpeed.921600.upload.speed=921600
epdiy_v2.menu.UploadSpeed.115200=115200
epdiy_v2.menu.UploadSpeed.115200.upload.speed=115200
epdiy_v2.menu.UploadSpeed.256000.windows=256000
epdiy_v2.menu.UploadSpeed.256000.upload.speed=256000
epdiy_v2.menu.UploadSpeed.230400.windows.upload.speed=256000
epdiy_v2.menu.UploadSpeed.230400=230400
epdiy_v2.menu.UploadSpeed.230400.upload.speed=230400
epdiy_v2.menu.UploadSpeed.460800.linux=460800
epdiy_v2.menu.UploadSpeed.460800.macosx=460800
epdiy_v2.menu.UploadSpeed.460800.upload.speed=460800
epdiy_v2.menu.UploadSpeed.512000.windows=512000
epdiy_v2.menu.UploadSpeed.512000.upload.speed=512000
epdiy_v2.menu.DebugLevel.none=None
epdiy_v2.menu.DebugLevel.none.build.code_debug=0
epdiy_v2.menu.DebugLevel.error=Error
epdiy_v2.menu.DebugLevel.error.build.code_debug=1
epdiy_v2.menu.DebugLevel.warn=Warn
epdiy_v2.menu.DebugLevel.warn.build.code_debug=2
epdiy_v2.menu.DebugLevel.info=Info
epdiy_v2.menu.DebugLevel.info.build.code_debug=3
epdiy_v2.menu.DebugLevel.debug=Debug
epdiy_v2.menu.DebugLevel.debug.build.code_debug=4
epdiy_v2.menu.DebugLevel.verbose=Verbose
epdiy_v2.menu.DebugLevel.verbose.build.code_debug=5
############################## Revision 4 Board ##############################
epdiy_v4.name=EPDIY Board Revision 4
epdiy_v4.upload.tool=esp32:esptool_py
epdiy_v4.upload.maximum_size=1310720
epdiy_v4.upload.maximum_data_size=327680
epdiy_v4.upload.wait_for_upload_port=true
epdiy_v4.serial.disableDTR=true
epdiy_v4.serial.disableRTS=true
epdiy_v4.build.mcu=esp32
epdiy_v4.build.core=esp32:esp32
epdiy_v4.build.variant=esp32
epdiy_v4.build.board=ESP32_EPDIY
epdiy_v4.build.f_cpu=240000000L
epdiy_v4.build.flash_size=4MB
epdiy_v4.build.flash_freq=40m
epdiy_v4.build.flash_mode=dio
epdiy_v4.build.boot=dio
epdiy_v4.build.partitions=default
epdiy_v4.build.defines=
epdiy_v4.menu.DisplayType.default=ED097OC4
epdiy_v4.menu.DisplayType.default.build.defines=-DCONFIG_EPD_DISPLAY_TYPE_ED097OC4 -DBOARD_HAS_PSRAM -mfix-esp32-psram-cache-issue -DCONFIG_EPD_BOARD_REVISION_V4
epdiy_v4.menu.DisplayType.ed097oc4_lq=ED097OC4 Low Quality
epdiy_v4.menu.DisplayType.ed097oc4_lq.build.defines=-DCONFIG_EPD_DISPLAY_TYPE_ED097OC4_LQ -DBOARD_HAS_PSRAM -mfix-esp32-psram-cache-issue -DCONFIG_EPD_BOARD_REVISION_V4
epdiy_v4.menu.DisplayType.ed060sc4=ED060SC4
epdiy_v4.menu.DisplayType.ed060sc4.build.defines=-DCONFIG_EPD_DISPLAY_TYPE_ED060SC4 -DBOARD_HAS_PSRAM -mfix-esp32-psram-cache-issue -DCONFIG_EPD_BOARD_REVISION_V4
epdiy_v4.menu.DisplayType.ed097tc2=ED097TC2
epdiy_v4.menu.DisplayType.ed097tc2.build.defines=-DCONFIG_EPD_DISPLAY_TYPE_ED097TC2 -DBOARD_HAS_PSRAM -mfix-esp32-psram-cache-issue -DCONFIG_EPD_BOARD_REVISION_V4
epdiy_v4.menu.DisplayType.ed047tc1=ED047TC1 (LILYGO 4.7 inch)
epdiy_v4.menu.DisplayType.ed047tc1.build.defines=-DCONFIG_EPD_DISPLAY_TYPE_ED047TC1 -DBOARD_HAS_PSRAM -mfix-esp32-psram-cache-issue -DCONFIG_EPD_BOARD_REVISION_V4
epdiy_v4.menu.DisplayType.ed133ut2=ED133UT2
epdiy_v4.menu.DisplayType.ed133ut2.build.defines=-DCONFIG_EPD_DISPLAY_TYPE_ED133UT2 -DBOARD_HAS_PSRAM -mfix-esp32-psram-cache-issue -DCONFIG_EPD_BOARD_REVISION_V4
epdiy_v4.menu.PartitionScheme.default=Default 4MB with spiffs (1.2MB APP/1.5MB SPIFFS)
epdiy_v4.menu.PartitionScheme.default.build.partitions=default
epdiy_v4.menu.PartitionScheme.defaultffat=Default 4MB with ffat (1.2MB APP/1.5MB FATFS)
epdiy_v4.menu.PartitionScheme.defaultffat.build.partitions=default_ffat
epdiy_v4.menu.PartitionScheme.default_8MB=8M Flash (3MB APP/1.5MB FAT)
epdiy_v4.menu.PartitionScheme.default_8MB.build.partitions=default_8MB
epdiy_v4.menu.PartitionScheme.minimal=Minimal (1.3MB APP/700KB SPIFFS)
epdiy_v4.menu.PartitionScheme.minimal.build.partitions=minimal
epdiy_v4.menu.PartitionScheme.no_ota=No OTA (2MB APP/2MB SPIFFS)
epdiy_v4.menu.PartitionScheme.no_ota.build.partitions=no_ota
epdiy_v4.menu.PartitionScheme.no_ota.upload.maximum_size=2097152
epdiy_v4.menu.PartitionScheme.noota_3g=No OTA (1MB APP/3MB SPIFFS)
epdiy_v4.menu.PartitionScheme.noota_3g.build.partitions=noota_3g
epdiy_v4.menu.PartitionScheme.noota_3g.upload.maximum_size=1048576
epdiy_v4.menu.PartitionScheme.noota_ffat=No OTA (2MB APP/2MB FATFS)
epdiy_v4.menu.PartitionScheme.noota_ffat.build.partitions=noota_ffat
epdiy_v4.menu.PartitionScheme.noota_ffat.upload.maximum_size=2097152
epdiy_v4.menu.PartitionScheme.noota_3gffat=No OTA (1MB APP/3MB FATFS)
epdiy_v4.menu.PartitionScheme.noota_3gffat.build.partitions=noota_3gffat
epdiy_v4.menu.PartitionScheme.noota_3gffat.upload.maximum_size=1048576
epdiy_v4.menu.PartitionScheme.huge_app=Huge APP (3MB No OTA/1MB SPIFFS)
epdiy_v4.menu.PartitionScheme.huge_app.build.partitions=huge_app
epdiy_v4.menu.PartitionScheme.huge_app.upload.maximum_size=3145728
epdiy_v4.menu.PartitionScheme.min_spiffs=Minimal SPIFFS (1.9MB APP with OTA/190KB SPIFFS)
epdiy_v4.menu.PartitionScheme.min_spiffs.build.partitions=min_spiffs
epdiy_v4.menu.PartitionScheme.min_spiffs.upload.maximum_size=1966080
epdiy_v4.menu.PartitionScheme.fatflash=16M Flash (2MB APP/12.5MB FAT)
epdiy_v4.menu.PartitionScheme.fatflash.build.partitions=ffat
epdiy_v4.menu.FlashMode.qio=QIO
epdiy_v4.menu.FlashMode.qio.build.flash_mode=dio
epdiy_v4.menu.FlashMode.qio.build.boot=qio
epdiy_v4.menu.FlashMode.dio=DIO
epdiy_v4.menu.FlashMode.dio.build.flash_mode=dio
epdiy_v4.menu.FlashMode.dio.build.boot=dio
epdiy_v4.menu.FlashMode.qout=QOUT
epdiy_v4.menu.FlashMode.qout.build.flash_mode=dout
epdiy_v4.menu.FlashMode.qout.build.boot=qout
epdiy_v4.menu.FlashMode.dout=DOUT
epdiy_v4.menu.FlashMode.dout.build.flash_mode=dout
epdiy_v4.menu.FlashMode.dout.build.boot=dout
epdiy_v4.menu.FlashFreq.80=80MHz
epdiy_v4.menu.FlashFreq.80.build.flash_freq=80m
epdiy_v4.menu.FlashFreq.40=40MHz
epdiy_v4.menu.FlashFreq.40.build.flash_freq=40m
epdiy_v4.menu.UploadSpeed.921600=921600
epdiy_v4.menu.UploadSpeed.921600.upload.speed=921600
epdiy_v4.menu.UploadSpeed.115200=115200
epdiy_v4.menu.UploadSpeed.115200.upload.speed=115200
epdiy_v4.menu.UploadSpeed.256000.windows=256000
epdiy_v4.menu.UploadSpeed.256000.upload.speed=256000
epdiy_v4.menu.UploadSpeed.230400.windows.upload.speed=256000
epdiy_v4.menu.UploadSpeed.230400=230400
epdiy_v4.menu.UploadSpeed.230400.upload.speed=230400
epdiy_v4.menu.UploadSpeed.460800.linux=460800
epdiy_v4.menu.UploadSpeed.460800.macosx=460800
epdiy_v4.menu.UploadSpeed.460800.upload.speed=460800
epdiy_v4.menu.UploadSpeed.512000.windows=512000
epdiy_v4.menu.UploadSpeed.512000.upload.speed=512000
epdiy_v4.menu.DebugLevel.none=None
epdiy_v4.menu.DebugLevel.none.build.code_debug=0
epdiy_v4.menu.DebugLevel.error=Error
epdiy_v4.menu.DebugLevel.error.build.code_debug=1
epdiy_v4.menu.DebugLevel.warn=Warn
epdiy_v4.menu.DebugLevel.warn.build.code_debug=2
epdiy_v4.menu.DebugLevel.info=Info
epdiy_v4.menu.DebugLevel.info.build.code_debug=3
epdiy_v4.menu.DebugLevel.debug=Debug
epdiy_v4.menu.DebugLevel.debug.build.code_debug=4
epdiy_v4.menu.DebugLevel.verbose=Verbose
epdiy_v4.menu.DebugLevel.verbose.build.code_debug=5
############################## Revision 4 Board ##############################
lilygo_t5_47.name=LILYGO T5-4.7 inch e-paper
lilygo_t5_47.upload.tool=esp32:esptool_py
lilygo_t5_47.upload.maximum_size=1310720
lilygo_t5_47.upload.maximum_data_size=327680
lilygo_t5_47.upload.wait_for_upload_port=true
lilygo_t5_47.serial.disableDTR=true
lilygo_t5_47.serial.disableRTS=true
lilygo_t5_47.build.mcu=esp32
lilygo_t5_47.build.core=esp32:esp32
lilygo_t5_47.build.variant=esp32
lilygo_t5_47.build.board=ESP32_EPDIY
lilygo_t5_47.build.f_cpu=240000000L
lilygo_t5_47.build.flash_size=4MB
lilygo_t5_47.build.flash_freq=40m
lilygo_t5_47.build.flash_mode=dio
lilygo_t5_47.build.boot=dio
lilygo_t5_47.build.partitions=default
lilygo_t5_47.build.defines=-DBOARD_HAS_PSRAM -mfix-esp32-psram-cache-issue -DCONFIG_EPD_BOARD_REVISION_LILYGO_T5_47
lilygo_t5_47.menu.DisplayType.default=ED097OC4
lilygo_t5_47.menu.DisplayType.default.build.defines=-DCONFIG_EPD_DISPLAY_TYPE_ED097OC4 -DBOARD_HAS_PSRAM -mfix-esp32-psram-cache-issue -DCONFIG_EPD_BOARD_REVISION_LILYGO_T5_47
lilygo_t5_47.menu.DisplayType.ed097oc4_lq=ED097OC4 Low Quality
lilygo_t5_47.menu.DisplayType.ed097oc4_lq.build.defines=-DCONFIG_EPD_DISPLAY_TYPE_ED097OC4_LQ -DBOARD_HAS_PSRAM -mfix-esp32-psram-cache-issue -DCONFIG_EPD_BOARD_REVISION_LILYGO_T5_47
lilygo_t5_47.menu.DisplayType.ed060sc4=ED060SC4
lilygo_t5_47.menu.DisplayType.ed060sc4.build.defines=-DCONFIG_EPD_DISPLAY_TYPE_ED060SC4 -DBOARD_HAS_PSRAM -mfix-esp32-psram-cache-issue -DCONFIG_EPD_BOARD_REVISION_LILYGO_T5_47
lilygo_t5_47.menu.DisplayType.ed097tc2=ED097TC2
lilygo_t5_47.menu.DisplayType.ed097tc2.build.defines=-DCONFIG_EPD_DISPLAY_TYPE_ED097TC2 -DBOARD_HAS_PSRAM -mfix-esp32-psram-cache-issue -DCONFIG_EPD_BOARD_REVISION_LILYGO_T5_47
lilygo_t5_47.menu.DisplayType.ed047tc1=ED047TC1 (LILYGO 4.7 inch)
lilygo_t5_47.menu.DisplayType.ed047tc1.build.defines=-DCONFIG_EPD_DISPLAY_TYPE_ED047TC1 -DBOARD_HAS_PSRAM -mfix-esp32-psram-cache-issue -DCONFIG_EPD_BOARD_REVISION_LILYGO_T5_47
lilygo_t5_47.menu.DisplayType.ed133ut2=ED133UT2
lilygo_t5_47.menu.DisplayType.ed133ut2.build.defines=-DCONFIG_EPD_DISPLAY_TYPE_ED133UT2 -DBOARD_HAS_PSRAM -mfix-esp32-psram-cache-issue -DCONFIG_EPD_BOARD_REVISION_LILYGO_T5_47
lilygo_t5_47.menu.PartitionScheme.default=Default 4MB with spiffs (1.2MB APP/1.5MB SPIFFS)
lilygo_t5_47.menu.PartitionScheme.default.build.partitions=default
lilygo_t5_47.menu.PartitionScheme.defaultffat=Default 4MB with ffat (1.2MB APP/1.5MB FATFS)
lilygo_t5_47.menu.PartitionScheme.defaultffat.build.partitions=default_ffat
lilygo_t5_47.menu.PartitionScheme.default_8MB=8M Flash (3MB APP/1.5MB FAT)
lilygo_t5_47.menu.PartitionScheme.default_8MB.build.partitions=default_8MB
lilygo_t5_47.menu.PartitionScheme.minimal=Minimal (1.3MB APP/700KB SPIFFS)
lilygo_t5_47.menu.PartitionScheme.minimal.build.partitions=minimal
lilygo_t5_47.menu.PartitionScheme.no_ota=No OTA (2MB APP/2MB SPIFFS)
lilygo_t5_47.menu.PartitionScheme.no_ota.build.partitions=no_ota
lilygo_t5_47.menu.PartitionScheme.no_ota.upload.maximum_size=2097152
lilygo_t5_47.menu.PartitionScheme.noota_3g=No OTA (1MB APP/3MB SPIFFS)
lilygo_t5_47.menu.PartitionScheme.noota_3g.build.partitions=noota_3g
lilygo_t5_47.menu.PartitionScheme.noota_3g.upload.maximum_size=1048576
lilygo_t5_47.menu.PartitionScheme.noota_ffat=No OTA (2MB APP/2MB FATFS)
lilygo_t5_47.menu.PartitionScheme.noota_ffat.build.partitions=noota_ffat
lilygo_t5_47.menu.PartitionScheme.noota_ffat.upload.maximum_size=2097152
lilygo_t5_47.menu.PartitionScheme.noota_3gffat=No OTA (1MB APP/3MB FATFS)
lilygo_t5_47.menu.PartitionScheme.noota_3gffat.build.partitions=noota_3gffat
lilygo_t5_47.menu.PartitionScheme.noota_3gffat.upload.maximum_size=1048576
lilygo_t5_47.menu.PartitionScheme.huge_app=Huge APP (3MB No OTA/1MB SPIFFS)
lilygo_t5_47.menu.PartitionScheme.huge_app.build.partitions=huge_app
lilygo_t5_47.menu.PartitionScheme.huge_app.upload.maximum_size=3145728
lilygo_t5_47.menu.PartitionScheme.min_spiffs=Minimal SPIFFS (1.9MB APP with OTA/190KB SPIFFS)
lilygo_t5_47.menu.PartitionScheme.min_spiffs.build.partitions=min_spiffs
lilygo_t5_47.menu.PartitionScheme.min_spiffs.upload.maximum_size=1966080
lilygo_t5_47.menu.PartitionScheme.fatflash=16M Flash (2MB APP/12.5MB FAT)
lilygo_t5_47.menu.PartitionScheme.fatflash.build.partitions=ffat
lilygo_t5_47.menu.FlashMode.qio=QIO
lilygo_t5_47.menu.FlashMode.qio.build.flash_mode=dio
lilygo_t5_47.menu.FlashMode.qio.build.boot=qio
lilygo_t5_47.menu.FlashMode.dio=DIO
lilygo_t5_47.menu.FlashMode.dio.build.flash_mode=dio
lilygo_t5_47.menu.FlashMode.dio.build.boot=dio
lilygo_t5_47.menu.FlashMode.qout=QOUT
lilygo_t5_47.menu.FlashMode.qout.build.flash_mode=dout
lilygo_t5_47.menu.FlashMode.qout.build.boot=qout
lilygo_t5_47.menu.FlashMode.dout=DOUT
lilygo_t5_47.menu.FlashMode.dout.build.flash_mode=dout
lilygo_t5_47.menu.FlashMode.dout.build.boot=dout
lilygo_t5_47.menu.FlashFreq.80=80MHz
lilygo_t5_47.menu.FlashFreq.80.build.flash_freq=80m
lilygo_t5_47.menu.FlashFreq.40=40MHz
lilygo_t5_47.menu.FlashFreq.40.build.flash_freq=40m
lilygo_t5_47.menu.UploadSpeed.921600=921600
lilygo_t5_47.menu.UploadSpeed.921600.upload.speed=921600
lilygo_t5_47.menu.UploadSpeed.115200=115200
lilygo_t5_47.menu.UploadSpeed.115200.upload.speed=115200
lilygo_t5_47.menu.UploadSpeed.256000.windows=256000
lilygo_t5_47.menu.UploadSpeed.256000.upload.speed=256000
lilygo_t5_47.menu.UploadSpeed.230400.windows.upload.speed=256000
lilygo_t5_47.menu.UploadSpeed.230400=230400
lilygo_t5_47.menu.UploadSpeed.230400.upload.speed=230400
lilygo_t5_47.menu.UploadSpeed.460800.linux=460800
lilygo_t5_47.menu.UploadSpeed.460800.macosx=460800
lilygo_t5_47.menu.UploadSpeed.460800.upload.speed=460800
lilygo_t5_47.menu.UploadSpeed.512000.windows=512000
lilygo_t5_47.menu.UploadSpeed.512000.upload.speed=512000
lilygo_t5_47.menu.DebugLevel.none=None
lilygo_t5_47.menu.DebugLevel.none.build.code_debug=0
lilygo_t5_47.menu.DebugLevel.error=Error
lilygo_t5_47.menu.DebugLevel.error.build.code_debug=1
lilygo_t5_47.menu.DebugLevel.warn=Warn
lilygo_t5_47.menu.DebugLevel.warn.build.code_debug=2
lilygo_t5_47.menu.DebugLevel.info=Info
lilygo_t5_47.menu.DebugLevel.info.build.code_debug=3
lilygo_t5_47.menu.DebugLevel.debug=Debug
lilygo_t5_47.menu.DebugLevel.debug.build.code_debug=4
lilygo_t5_47.menu.DebugLevel.verbose=Verbose
lilygo_t5_47.menu.DebugLevel.verbose.build.code_debug=5

View File

@ -0,0 +1,4 @@
version: "2.0.0"
description: "Drive parallel e-Paper displays with epdiy-based boards."
url: "https://github.com/vroland/epdiy"
license: LGPL-3.0-or-later

View File

@ -0,0 +1,32 @@
{
"name": "epdiy",
"version": "2.0.0",
"description": "Drive parallel e-Paper displays with epdiy-based boards.",
"keywords": "epd, driver, e-ink",
"repository": {
"type": "git",
"url": "https://github.com/vroland/epdiy.git"
},
"authors": [
{
"name": "Valentin Roland",
"email": "github@vroland.de",
"maintainer": true
}
],
"license": "LGPL-3.0",
"frameworks": [
"arduino",
"espidf"
],
"platforms": "espressif32",
"export": {
"exclude": [
"hardware"
]
},
"build": {
"includeDir": "include",
"srcDir": "src/"
}
}

View File

@ -1,9 +1,9 @@
name=epdiy
version=1.0.0
version=2.0.0
author=Valentin Roland <github@vroland.de>
maintainer=Valentin Roland <github@vroland.de>
sentence=Drive parallel e-Paper displays with epdiy-based boards.
paragraph=See https://github.com/vroland/epdiy for details.
architectures=esp32
architectures=esp32,esp32s3
url=https://github.com/vroland/epdiy
category=Display

View File

@ -0,0 +1,101 @@
## Scripts in this folder are for adding addtional capabilities to epdiy.
## imgconvert.py
#### usage:
python3 imgconvert.py [-h] -i INPUTFILE -n NAME -o OUTPUTFILE [-maxw MAX_WIDTH]
[-maxh MAX_HEIGHT]
**optional arguments:**
* **-h, --help** show this help message and exit
* **-i INPUTFILE**
* **-n NAME**
* **-o OUTPUTFILE**
* **-maxw MAX_WIDTH**
* **-maxh MAX_HEIGHT**
==========================================================
## fontconvert.py
#### usage:
python3 fontconvert.py [-h] [--compress] [--additional-intervals ADDITIONAL_INTERVALS]
[--string STRING]
name size fontstack [fontstack ...]
Generate a header file from a font to be used with epdiy.
**positional arguments:**
* **name** name of the font to be used in epdiy.
* **size** font size to use.
* **fontstack** list of font files, ordered by descending priority. This is not actually implemented as yet. Please just use one file for now.
**optional arguments:**
* **-h**, --help show this help message and exit
* **--compress** compress glyph bitmaps.
* **--additional-intervals** ADDITIONAL_INTERVALS
Additional code point intervals to export as min,max. This argument
can be repeated.
* **--string STRING** A quoted string of all required characters. The intervals are will be made from these characters if they exist in the ttf file. Missing characters will warn about their abscence.
####example:
1. Download a ttf from where you like to a directory. As in: "~/Downloads/any_old_ttf.ttf"
in the download directory
2. Run
`python3 fontconvert.py my_font 30 ~/Downloads/any_old_ttf.ttf --string '/0123456789:;@ABCDEFGH[\]^_`abcdefgh\{|}~¡¢£¤¥¦§¨©ª' > fonts.h`
* you will need to use special escapes for characters like ' or " This is system dependant though.
3. copy fonts.h into your app folder or where ever your app can find it.
4. include it into your project with
`#include fonts.h`
Then use it just like any other font file in epdiy.
**To run this script the freetype module needs to be installed. This can be done with `pip install freetype-py` You will be warned if it is not accessible by the script.**
==========================================================
##waveform_hdrgen.py
####usage:
waveform_hdrgen.py [-h] [--list-modes] [--temperature-range TEMPERATURE_RANGE]
[--export-modes EXPORT_MODES]
name
**positional arguments:**
name name of the waveform object.
**optional arguments:**
* **-h, --help** show this help message and exit
* **--list-modes** list the available modes for tis file.
* **--temperature-range TEMPERATURE_RANGE**
only export waveforms in the temperature range of min,max °C.
* **--export-modes EXPORT_MODES**
comma-separated list of waveform mode IDs to export.

View File

@ -1,29 +1,11 @@
#!python3
import freetype
import zlib
import sys
import re
import math
import argparse
from collections import namedtuple
parser = argparse.ArgumentParser(description="Generate a header file from a font to be used with epdiy.")
parser.add_argument("name", action="store", help="name of the font.")
parser.add_argument("size", type=int, help="font size to use.")
parser.add_argument("fontstack", action="store", nargs='+', help="list of font files, ordered by descending priority.")
parser.add_argument("--compress", dest="compress", action="store_true", help="compress glyph bitmaps.")
parser.add_argument("--additional-intervals", dest="additional_intervals", action="append", help="Additional code point intervals to export as min,max. This argument can be repeated.")
args = parser.parse_args()
GlyphProps = namedtuple("GlyphProps", ["width", "height", "advance_x", "left", "top", "compressed_size", "data_offset", "code_point"])
font_stack = [freetype.Face(f) for f in args.fontstack]
compress = args.compress
size = args.size
font_name = args.name
# inclusive unicode code point intervals
# must not overlap and be in ascending order
# modify intervals here
# however if the "string" command line argument is used these are ignored
intervals = [
(32, 126),
(160, 255),
@ -48,12 +30,107 @@ intervals = [
#(0x1F600, 0x1F680),
]
try:
import freetype
except ImportError as error:
sys.exit("To run this script the freetype module needs to be installed.\nThis can be done using:\npip install freetype-py")
import zlib
import sys
import re
import math
import argparse
from collections import namedtuple
#see https://freetype-py.readthedocs.io/en/latest/ for documentation
parser = argparse.ArgumentParser(description="Generate a header file from a font to be used with epdiy.")
parser.add_argument("name", action="store", help="name of the font.")
parser.add_argument("size", type=int, help="font size to use.")
parser.add_argument("fontstack", action="store", nargs='+', help="list of font files, ordered by descending priority. This is not actually implemented please just use one file for now.")
parser.add_argument("--compress", dest="compress", action="store_true", help="compress glyph bitmaps.")
parser.add_argument("--additional-intervals", dest="additional_intervals", action="append", help="Additional code point intervals to export as min,max. This argument can be repeated.")
parser.add_argument("--string", action="store", help="A string of all required characters. intervals are made up of this" )
args = parser.parse_args()
command_line = ""
prev_arg = ""
for arg in sys.argv:
# ~ if prev_arg == "--string":
# ~ command_line = command_line + " '" + arg +"'"
# ~ else:
command_line = command_line + " " + arg
# ~ prev_arg = arg
# ~ print (command_line)
GlyphProps = namedtuple("GlyphProps", ["width", "height", "advance_x", "left", "top", "compressed_size", "data_offset", "code_point"])
font_stack = [freetype.Face(f) for f in args.fontstack]
font_files = args.fontstack
face_index = 0
font_file = font_files[face_index]
compress = args.compress
size = args.size
font_name = args.name
for face in font_stack:
# shift by 6 bytes, because sizes are given as 6-bit fractions
# the display has about 150 dpi.
face.set_char_size(size << 6, size << 6, 150, 150)
# assign intervals from argument parrameters ie. handle the string arg
if args.string != None:
font_file = font_files[face_index]
string = " " + args.string # always add space to the string it is easily forgotten
chars = sorted(set(string))
#make array of code pointscode_ponts.append(ord(char))
code_points = list()
intervals = [] # empty the intevals array NB. if you want to allways add default characters comment out this line
# go through the sorted characters and make the intervals
for char in chars:
if( face.get_char_index(ord(char)) != 0 ):
# this character is in the font file so add it to the new string.
code_points.append(ord(char))
else:
print("The character ", char, " is not available in ", font_file, file=sys.stderr)
lower = code_points[0]
len_x = len(code_points)
x = 0
while x < len_x:
# ~ print ("loop value x = ", x , file=sys.stderr)
a = code_points[x];
b = a;
if( x < len_x - 1):
b = code_points[x + 1];
if( a == b - 1 ):
# ~ print("sequential", a, b, file=sys.stderr)
if( lower == -1):
lower = a
else:
# ~ print("non sequential", a, b , file=sys.stderr)
if( lower == -1):
# ~ print("single character")
interval = (a , a)
else:
interval = (lower, a)
# ~ print("interval", interval , file=sys.stderr)
intervals.append(interval)
lower = -1
x = x + 1
# base intervals are assigned dditional intervals from arguments
add_ints = []
if args.additional_intervals:
if args.additional_intervals != None:
add_ints = [tuple([int(n, base=0) for n in i.split(",")]) for i in args.additional_intervals]
intervals = sorted(intervals + add_ints)
# ~ print("Intervals are now: ", intervals, file=sys.stderr)
def norm_floor(val):
return int(math.floor(val / (1 << 6)))
@ -73,21 +150,48 @@ total_size = 0
total_packed = 0
all_glyphs = []
# new globals
total_chars = 0
ascender = 0
descender = 100
f_height = 0
def load_glyph(code_point):
global face_index
face_index = 0
while face_index < len(font_stack):
face = font_stack[face_index]
glyph_index = face.get_char_index(code_point)
if glyph_index > 0:
face.load_glyph(glyph_index, freetype.FT_LOAD_RENDER)
#count characters found and find bounds of characters
global ascender
if ascender < face.size.ascender:
ascender = face.size.ascender
global descender
if descender > face.size.descender:
descender = face.size.descender
global f_height
if f_height < face.size.height:
f_height = face.size.height
global total_chars
total_chars += 1
return face
break
face_index += 1
# this needs work
# this needs to be handled better to show failed character and continue not just die a questionable death
# this appears to have been designed to combine several font files
# but that is not clear to the end user and this then looks like a bug
print (f"falling back to font {face_index} for {chr(code_point)}.", file=sys.stderr)
raise ValueError(f"code point {code_point} not found in font stack!")
for i_start, i_end in intervals:
for code_point in range(i_start, i_end + 1):
# handle missing characters in font file
if( face.get_char_index(code_point) == 0 ):
print("Character ", chr(code_point), "(", code_point, ") is not in ", font_file, file=sys.stderr)
continue
face = load_glyph(code_point)
bitmap = face.glyph.bitmap
pixels = []
@ -126,7 +230,8 @@ for i_start, i_end in intervals:
all_glyphs.append((glyph, compressed))
# pipe seems to be a good heuristic for the "real" descender
face = load_glyph(ord('|'))
# face = load_glyph(ord('|'))
# removed as max descender and assender are handled above
glyph_data = []
glyph_props = []
@ -134,23 +239,38 @@ for index, glyph in enumerate(all_glyphs):
props, compressed = glyph
glyph_data.extend([b for b in compressed])
glyph_props.append(props)
print("", file=sys.stderr)
print(f"Original font file {font_file} as {font_name} using {total_chars} characters", file=sys.stderr)
print("total", total_packed, file=sys.stderr)
print("compressed", total_size, file=sys.stderr)
print("#pragma once")
print("#include \"epd_driver.h\"")
print(f"const uint8_t {font_name}Bitmaps[{len(glyph_data)}] = {{")
print("#include \"epdiy.h\"")
# add font file origin and characters at the head of the output file
print("/*")
print ( "Created with")
print(command_line)
print(f"As '{font_name}' with available {total_chars} characters")
for i, g in enumerate(glyph_props):
print (f"{chr(g.code_point)}", end ="" )
print("")
print("*/")
print(f"const uint8_t {font_name}_Bitmaps[{len(glyph_data)}] = {{")
for c in chunks(glyph_data, 16):
print (" " + " ".join(f"0x{b:02X}," for b in c))
print ("};");
print(f"const EpdGlyph {font_name}Glyphs[] = {{")
print ('// GlyphProps[width, height, advance_x, left, top, compressed_size, data_offset, code_point]')
print(f"const EpdGlyph {font_name}_Glyphs[] = {{")
for i, g in enumerate(glyph_props):
print (" { " + ", ".join([f"{a}" for a in list(g[:-1])]),"},", f"// {chr(g.code_point) if g.code_point != 92 else '<backslash>'}")
print (" { " + ", ".join([f"{a}" for a in list(g[:-1])]),"},", f"// '{chr(g.code_point) if g.code_point != 92 else '<backslash>'}'")
print ("};");
print(f"const EpdUnicodeInterval {font_name}Intervals[] = {{")
print(f"const EpdUnicodeInterval {font_name}_Intervals[] = {{")
offset = 0
for i_start, i_end in intervals:
print (f" {{ 0x{i_start:X}, 0x{i_end:X}, 0x{offset:X} }},")
@ -158,12 +278,22 @@ for i_start, i_end in intervals:
print ("};");
print(f"const EpdFont {font_name} = {{")
print(f" {font_name}Bitmaps,")
print(f" {font_name}Glyphs,")
print(f" {font_name}Intervals,")
print(f" {len(intervals)},")
print(f" {1 if compress else 0},")
print(f" {norm_ceil(face.size.height)},")
print(f" {norm_ceil(face.size.ascender)},")
print(f" {norm_floor(face.size.descender)},")
print(f" {font_name}_Bitmaps, // (*bitmap) Glyph bitmap pointer, all concatenated together")
print(f" {font_name}_Glyphs, // glyphs Glyph array")
print(f" {font_name}_Intervals, // intervals Valid unicode intervals for this font")
print(f" {len(intervals)}, // interval_count Number of unicode intervals.intervals")
print(f" {1 if compress else 0}, // compressed Does this font use compressed glyph bitmaps?")
print(f" {norm_ceil(f_height)}, // advance_y Newline distance (y axis)")
print(f" {norm_ceil(ascender)}, // ascender Maximal height of a glyph above the base line")
print(f" {norm_floor(descender)}, // descender Maximal height of a glyph below the base line")
print("};")
print("/*")
print("Included intervals")
for i_start, i_end in intervals:
print (f" ( {i_start}, {i_end}), ie. '{chr(i_start)}' - '{chr(i_end)}'")
print("Included intervals", file=sys.stderr)
for i_start, i_end in intervals:
print (f" ( {i_start}, {i_end}), ie. '{chr(i_start)}' - '{chr(i_end)}'", file=sys.stderr)
print("")
print("*/")

View File

@ -17,7 +17,7 @@ args = parser.parse_args()
im = Image.open(args.inputfile)
# convert to grayscale
im = im.convert(mode='L')
im.thumbnail((args.max_width, args.max_height), Image.ANTIALIAS)
im.thumbnail((args.max_width, args.max_height), Image.LANCZOS)
# Write out the output file.
with open(args.outputfile, 'w') as f:

View File

@ -18,24 +18,36 @@ waveforms = json.load(sys.stdin);
total_size = 0
def phase_to_c(phase):
"""
Convert a 5 bit phase to a 4 bit C LUT.
"""
global total_size
def phase_to_c(phase,bits_per_pixel_c=4):
N1 = len(phase)
N2 = len(phase[0])
N = 2**bits_per_pixel_c
if N1%N != 0:
raise ValueError(f"first dimension of phases is {N1}. Allowed are multiples of {N}")
step1 = int(N1/N)
if N2%N != 0:
raise ValueError(f"second dimension of phases is {N2}. Allowed are multiples of {N}")
step2 = int(N2/N)
targets = []
for t in range(0, 32, 2):
for t in range(0, N1, step1):
chunk = 0
line = []
for f in range(0, 32, 2):
i = 0
for f in range(0, N2, step2):
fr = phase[t][f]
chunk = (chunk << 2) | fr
if f and f % 8 == 6:
i += 1
if i == 4:
i = 0
line.append(chunk)
chunk = 0
targets.append(line)
total_size += len(line)
return targets
@ -61,7 +73,7 @@ if args.temperature_range:
modes = []
mode_filter = list(range(len(waveforms["modes"])))
mode_filter = [wm["mode"] for wm in waveforms["modes"]]
if args.export_modes:
mode_filter = list(map(int, args.export_modes.split(",")))
@ -72,6 +84,8 @@ num_modes = len(mode_filter)
temp_intervals = []
for bounds in waveforms["temperature_ranges"]["range_bounds"]:
if bounds["to"] < tmin or bounds["from"] > tmax:
continue
temp_intervals.append(f"{{ .min = {bounds['from']}, .max = {bounds['to']} }}")
modes = []
@ -84,7 +98,7 @@ for m_index, mode in enumerate(waveforms["modes"]):
ranges = []
for i, r in enumerate(mode["ranges"]):
bounds = waveforms["temperature_ranges"]["range_bounds"][i]
if bounds["from"] < tmin or bounds["from"] > tmax:
if bounds["to"] < tmin or bounds["from"] > tmax:
continue
phases = []

View File

@ -0,0 +1,77 @@
#include "epd_board.h"
#include <esp_log.h>
#include <stddef.h>
#include "epdiy.h"
/**
* The board's display control pin state.
*/
static epd_ctrl_state_t ctrl_state;
/**
* The EPDIY board in use.
*/
const EpdBoardDefinition* epd_board = NULL;
void IRAM_ATTR epd_busy_delay(uint32_t cycles) {
volatile unsigned long counts = XTHAL_GET_CCOUNT() + cycles;
while (XTHAL_GET_CCOUNT() < counts) {
};
}
void epd_set_board(const EpdBoardDefinition* board_definition) {
if (epd_board == NULL) {
epd_board = board_definition;
} else {
ESP_LOGW("epdiy", "EPD board can only be set once!");
}
}
const EpdBoardDefinition* epd_current_board() {
return epd_board;
}
void epd_set_mode(bool state) {
ctrl_state.ep_output_enable = state;
ctrl_state.ep_mode = state;
epd_ctrl_state_t mask = {
.ep_output_enable = true,
.ep_mode = true,
};
epd_board->set_ctrl(&ctrl_state, &mask);
}
epd_ctrl_state_t* epd_ctrl_state() {
return &ctrl_state;
}
void epd_control_reg_init() {
ctrl_state.ep_latch_enable = false;
ctrl_state.ep_output_enable = false;
ctrl_state.ep_sth = true;
ctrl_state.ep_mode = false;
ctrl_state.ep_stv = true;
epd_ctrl_state_t mask = {
.ep_latch_enable = true,
.ep_output_enable = true,
.ep_sth = true,
.ep_mode = true,
.ep_stv = true,
};
epd_board->set_ctrl(&ctrl_state, &mask);
}
void epd_control_reg_deinit() {
ctrl_state.ep_output_enable = false;
ctrl_state.ep_mode = false;
ctrl_state.ep_stv = false;
epd_ctrl_state_t mask = {
.ep_output_enable = true,
.ep_mode = true,
.ep_stv = true,
};
epd_board->set_ctrl(&ctrl_state, &mask);
}

View File

@ -0,0 +1,34 @@
#include "driver/adc.h"
#include "epd_board.h"
#include "esp_adc_cal.h"
#include "esp_log.h"
static const adc1_channel_t channel = ADC1_CHANNEL_7;
static esp_adc_cal_characteristics_t adc_chars;
#define NUMBER_OF_SAMPLES 100
void epd_board_temperature_init_v2() {
esp_adc_cal_value_t val_type
= esp_adc_cal_characterize(ADC_UNIT_1, ADC_ATTEN_DB_6, ADC_WIDTH_BIT_12, 1100, &adc_chars);
if (val_type == ESP_ADC_CAL_VAL_EFUSE_TP) {
ESP_LOGI("epd_temperature", "Characterized using Two Point Value\n");
} else if (val_type == ESP_ADC_CAL_VAL_EFUSE_VREF) {
ESP_LOGI("esp_temperature", "Characterized using eFuse Vref\n");
} else {
ESP_LOGI("esp_temperature", "Characterized using Default Vref\n");
}
adc1_config_width(ADC_WIDTH_BIT_12);
adc1_config_channel_atten(channel, ADC_ATTEN_DB_6);
}
float epd_board_ambient_temperature_v2() {
uint32_t value = 0;
for (int i = 0; i < NUMBER_OF_SAMPLES; i++) {
value += adc1_get_raw(channel);
}
value /= NUMBER_OF_SAMPLES;
// voltage in mV
float voltage = esp_adc_cal_raw_to_voltage(value, &adc_chars);
return (voltage - 500.0) / 10.0;
}

View File

@ -0,0 +1,8 @@
/**
* @file "epd_board_common.h"
* @brief Common board functions shared between boards.
*/
#pragma once
void epd_board_temperature_init_v2();
float epd_board_ambient_temperature_v2();

View File

@ -0,0 +1,227 @@
#include "epd_board.h"
#include <sdkconfig.h>
#include "../output_i2s/i2s_data_bus.h"
#include "../output_i2s/render_i2s.h"
#include "../output_i2s/rmt_pulse.h"
// Make this compile on the S3 to avoid long ifdefs
#ifndef CONFIG_IDF_TARGET_ESP32
#define GPIO_NUM_22 0
#define GPIO_NUM_23 0
#define GPIO_NUM_24 0
#define GPIO_NUM_25 0
#endif
#define CFG_DATA GPIO_NUM_23
#define CFG_CLK GPIO_NUM_18
#define CFG_STR GPIO_NUM_0
#define D7 GPIO_NUM_22
#define D6 GPIO_NUM_21
#define D5 GPIO_NUM_27
#define D4 GPIO_NUM_2
#define D3 GPIO_NUM_19
#define D2 GPIO_NUM_4
#define D1 GPIO_NUM_32
#define D0 GPIO_NUM_33
/* Control Lines */
#define CKV GPIO_NUM_25
#define STH GPIO_NUM_26
#define V4_LATCH_ENABLE GPIO_NUM_15
/* Edges */
#define CKH GPIO_NUM_5
typedef struct {
bool power_disable : 1;
bool pos_power_enable : 1;
bool neg_power_enable : 1;
bool ep_scan_direction : 1;
} epd_config_register_t;
static i2s_bus_config i2s_config = {
.clock = CKH,
.start_pulse = STH,
.data_0 = D0,
.data_1 = D1,
.data_2 = D2,
.data_3 = D3,
.data_4 = D4,
.data_5 = D5,
.data_6 = D6,
.data_7 = D7,
};
static void IRAM_ATTR push_cfg_bit(bool bit) {
gpio_set_level(CFG_CLK, 0);
gpio_set_level(CFG_DATA, bit);
gpio_set_level(CFG_CLK, 1);
}
static epd_config_register_t config_reg;
static void epd_board_init(uint32_t epd_row_width) {
/* Power Control Output/Off */
PIN_FUNC_SELECT(GPIO_PIN_MUX_REG[CFG_DATA], PIN_FUNC_GPIO);
PIN_FUNC_SELECT(GPIO_PIN_MUX_REG[CFG_CLK], PIN_FUNC_GPIO);
PIN_FUNC_SELECT(GPIO_PIN_MUX_REG[CFG_STR], PIN_FUNC_GPIO);
gpio_set_direction(CFG_DATA, GPIO_MODE_OUTPUT);
gpio_set_direction(CFG_CLK, GPIO_MODE_OUTPUT);
gpio_set_direction(CFG_STR, GPIO_MODE_OUTPUT);
fast_gpio_set_lo(CFG_STR);
config_reg.power_disable = true;
config_reg.pos_power_enable = false;
config_reg.neg_power_enable = false;
config_reg.ep_scan_direction = true;
// Setup I2S
// add an offset off dummy bytes to allow for enough timing headroom
i2s_bus_init(&i2s_config, epd_row_width + 32);
rmt_pulse_init(CKV);
}
static void epd_board_set_ctrl(epd_ctrl_state_t* state, const epd_ctrl_state_t* const mask) {
if (state->ep_sth) {
fast_gpio_set_hi(STH);
} else {
fast_gpio_set_lo(STH);
}
if (mask->ep_output_enable || mask->ep_mode || mask->ep_stv || mask->ep_latch_enable) {
fast_gpio_set_lo(CFG_STR);
// push config bits in reverse order
push_cfg_bit(state->ep_output_enable);
push_cfg_bit(state->ep_mode);
push_cfg_bit(config_reg.ep_scan_direction);
push_cfg_bit(state->ep_stv);
push_cfg_bit(config_reg.neg_power_enable);
push_cfg_bit(config_reg.pos_power_enable);
push_cfg_bit(config_reg.power_disable);
push_cfg_bit(state->ep_latch_enable);
fast_gpio_set_hi(CFG_STR);
}
}
static void epd_board_poweron(epd_ctrl_state_t* state) {
i2s_gpio_attach(&i2s_config);
// This was re-purposed as power enable.
config_reg.ep_scan_direction = true;
// POWERON
epd_ctrl_state_t mask = {
// Trigger output to shift register
.ep_stv = true,
};
config_reg.power_disable = false;
epd_board_set_ctrl(state, &mask);
epd_busy_delay(100 * 240);
config_reg.neg_power_enable = true;
epd_board_set_ctrl(state, &mask);
epd_busy_delay(500 * 240);
config_reg.pos_power_enable = true;
epd_board_set_ctrl(state, &mask);
epd_busy_delay(100 * 240);
state->ep_stv = true;
state->ep_sth = true;
mask.ep_sth = true;
epd_board_set_ctrl(state, &mask);
// END POWERON
}
void epd_powerdown_lilygo_t5_47() {
epd_ctrl_state_t* state = epd_ctrl_state();
// This was re-purposed as power enable however it also disables the touch.
// this workaround may still leave power on to epd and as such may cause other
// problems such as grey screen.
epd_ctrl_state_t mask = {
// Trigger output to shift register
.ep_stv = true,
};
config_reg.pos_power_enable = false;
epd_board_set_ctrl(state, &mask);
epd_busy_delay(10 * 240);
config_reg.neg_power_enable = false;
config_reg.pos_power_enable = false;
epd_board_set_ctrl(state, &mask);
epd_busy_delay(100 * 240);
state->ep_stv = false;
mask.ep_stv = true;
state->ep_output_enable = false;
mask.ep_output_enable = true;
state->ep_mode = false;
mask.ep_mode = true;
config_reg.power_disable = true;
epd_board_set_ctrl(state, &mask);
i2s_gpio_detach(&i2s_config);
}
static void epd_board_poweroff_common(epd_ctrl_state_t* state) {
// POWEROFF
epd_ctrl_state_t mask = {
// Trigger output to shift register
.ep_stv = true,
};
config_reg.pos_power_enable = false;
epd_board_set_ctrl(state, &mask);
epd_busy_delay(10 * 240);
config_reg.neg_power_enable = false;
config_reg.pos_power_enable = false;
epd_board_set_ctrl(state, &mask);
epd_busy_delay(100 * 240);
state->ep_stv = false;
mask.ep_stv = true;
state->ep_output_enable = false;
mask.ep_output_enable = true;
state->ep_mode = false;
mask.ep_mode = true;
config_reg.power_disable = true;
epd_board_set_ctrl(state, &mask);
i2s_gpio_detach(&i2s_config);
// END POWEROFF
}
static void epd_board_poweroff(epd_ctrl_state_t* state) {
// This was re-purposed as power enable.
config_reg.ep_scan_direction = false;
epd_board_poweroff_common(state);
}
static void epd_board_poweroff_touch(epd_ctrl_state_t* state) {
// This was re-purposed as power enable.
config_reg.ep_scan_direction = true;
epd_board_poweroff_common(state);
}
const EpdBoardDefinition epd_board_lilygo_t5_47 = {
.init = epd_board_init,
.deinit = NULL,
.set_ctrl = epd_board_set_ctrl,
.poweron = epd_board_poweron,
.poweroff = epd_board_poweroff,
.get_temperature = NULL,
};
const EpdBoardDefinition epd_board_lilygo_t5_47_touch = {
.init = epd_board_init,
.deinit = NULL,
.set_ctrl = epd_board_set_ctrl,
.poweron = epd_board_poweron,
.poweroff = epd_board_poweroff_touch,
.get_temperature = NULL,
.set_vcom = NULL,
};

View File

@ -0,0 +1,172 @@
#include "epd_board.h"
#include <sdkconfig.h>
#include "../output_i2s/i2s_data_bus.h"
#include "../output_i2s/render_i2s.h"
#include "../output_i2s/rmt_pulse.h"
#include "epd_board_common.h"
// Make this compile on the S3 to avoid long ifdefs
#ifndef CONFIG_IDF_TARGET_ESP32
#define GPIO_NUM_22 0
#define GPIO_NUM_23 0
#define GPIO_NUM_24 0
#define GPIO_NUM_25 0
#endif
#define CFG_DATA GPIO_NUM_23
#define CFG_CLK GPIO_NUM_18
#define CFG_STR GPIO_NUM_19
#define D7 GPIO_NUM_22
#define D6 GPIO_NUM_21
#define D5 GPIO_NUM_27
#define D4 GPIO_NUM_2
#define D3 GPIO_NUM_0
#define D2 GPIO_NUM_4
#define D1 GPIO_NUM_32
#define D0 GPIO_NUM_33
/* Control Lines */
#define CKV GPIO_NUM_25
#define STH GPIO_NUM_26
/* Edges */
#define CKH GPIO_NUM_5
typedef struct {
bool power_disable : 1;
bool pos_power_enable : 1;
bool neg_power_enable : 1;
bool ep_scan_direction : 1;
} epd_config_register_t;
static i2s_bus_config i2s_config = {
.clock = CKH,
.start_pulse = STH,
.data_0 = D0,
.data_1 = D1,
.data_2 = D2,
.data_3 = D3,
.data_4 = D4,
.data_5 = D5,
.data_6 = D6,
.data_7 = D7,
};
static void IRAM_ATTR push_cfg_bit(bool bit) {
gpio_set_level(CFG_CLK, 0);
gpio_set_level(CFG_DATA, bit);
gpio_set_level(CFG_CLK, 1);
}
static epd_config_register_t config_reg;
static void epd_board_init(uint32_t epd_row_width) {
/* Power Control Output/Off */
PIN_FUNC_SELECT(GPIO_PIN_MUX_REG[CFG_DATA], PIN_FUNC_GPIO);
PIN_FUNC_SELECT(GPIO_PIN_MUX_REG[CFG_CLK], PIN_FUNC_GPIO);
PIN_FUNC_SELECT(GPIO_PIN_MUX_REG[CFG_STR], PIN_FUNC_GPIO);
gpio_set_direction(CFG_DATA, GPIO_MODE_OUTPUT);
gpio_set_direction(CFG_CLK, GPIO_MODE_OUTPUT);
gpio_set_direction(CFG_STR, GPIO_MODE_OUTPUT);
fast_gpio_set_lo(CFG_STR);
config_reg.power_disable = true;
config_reg.pos_power_enable = false;
config_reg.neg_power_enable = false;
config_reg.ep_scan_direction = true;
// Setup I2S
// add an offset off dummy bytes to allow for enough timing headroom
i2s_bus_init(&i2s_config, epd_row_width + 32);
rmt_pulse_init(CKV);
epd_board_temperature_init_v2();
}
static void epd_board_set_ctrl(epd_ctrl_state_t* state, const epd_ctrl_state_t* const mask) {
if (state->ep_sth) {
fast_gpio_set_hi(STH);
} else {
fast_gpio_set_lo(STH);
}
if (mask->ep_output_enable || mask->ep_mode || mask->ep_stv || mask->ep_latch_enable) {
fast_gpio_set_lo(CFG_STR);
// push config bits in reverse order
push_cfg_bit(state->ep_output_enable);
push_cfg_bit(state->ep_mode);
push_cfg_bit(config_reg.ep_scan_direction);
push_cfg_bit(state->ep_stv);
push_cfg_bit(config_reg.neg_power_enable);
push_cfg_bit(config_reg.pos_power_enable);
push_cfg_bit(config_reg.power_disable);
push_cfg_bit(state->ep_latch_enable);
fast_gpio_set_hi(CFG_STR);
}
}
static void epd_board_poweron(epd_ctrl_state_t* state) {
// POWERON
i2s_gpio_attach(&i2s_config);
epd_ctrl_state_t mask = {
// Trigger output to shift register
.ep_stv = true,
};
config_reg.power_disable = false;
epd_board_set_ctrl(state, &mask);
epd_busy_delay(100 * 240);
config_reg.neg_power_enable = true;
epd_board_set_ctrl(state, &mask);
epd_busy_delay(500 * 240);
config_reg.pos_power_enable = true;
epd_board_set_ctrl(state, &mask);
epd_busy_delay(100 * 240);
state->ep_stv = true;
state->ep_sth = true;
mask.ep_sth = true;
epd_board_set_ctrl(state, &mask);
// END POWERON
}
static void epd_board_poweroff(epd_ctrl_state_t* state) {
// POWEROFF
epd_ctrl_state_t mask = {
// Trigger output to shift register
.ep_stv = true,
};
config_reg.pos_power_enable = false;
epd_board_set_ctrl(state, &mask);
epd_busy_delay(10 * 240);
config_reg.neg_power_enable = false;
config_reg.pos_power_enable = false;
epd_board_set_ctrl(state, &mask);
epd_busy_delay(100 * 240);
state->ep_stv = false;
state->ep_output_enable = false;
mask.ep_output_enable = true;
state->ep_mode = false;
mask.ep_mode = true;
config_reg.power_disable = true;
epd_board_set_ctrl(state, &mask);
i2s_gpio_detach(&i2s_config);
// END POWEROFF
}
const EpdBoardDefinition epd_board_v2_v3 = {
.init = epd_board_init,
.deinit = NULL,
.set_ctrl = epd_board_set_ctrl,
.poweron = epd_board_poweron,
.poweroff = epd_board_poweroff,
.get_temperature = epd_board_ambient_temperature_v2,
.set_vcom = NULL,
};

View File

@ -0,0 +1,194 @@
#include "epd_board.h"
#include <sdkconfig.h>
#include "../output_i2s/i2s_data_bus.h"
#include "../output_i2s/render_i2s.h"
#include "../output_i2s/rmt_pulse.h"
#include "epd_board_common.h"
// Make this compile on the S3 to avoid long ifdefs
#ifndef CONFIG_IDF_TARGET_ESP32
#define GPIO_NUM_22 0
#define GPIO_NUM_23 0
#define GPIO_NUM_24 0
#define GPIO_NUM_25 0
#endif
#define CFG_DATA GPIO_NUM_23
#define CFG_CLK GPIO_NUM_18
#define CFG_STR GPIO_NUM_19
#define D7 GPIO_NUM_22
#define D6 GPIO_NUM_21
#define D5 GPIO_NUM_27
#define D4 GPIO_NUM_2
#define D3 GPIO_NUM_0
#define D2 GPIO_NUM_4
#define D1 GPIO_NUM_32
#define D0 GPIO_NUM_33
/* Control Lines */
#define CKV GPIO_NUM_25
#define STH GPIO_NUM_26
#define V4_LATCH_ENABLE GPIO_NUM_15
/* Edges */
#define CKH GPIO_NUM_5
typedef struct {
bool power_disable : 1;
bool power_enable_vpos : 1;
bool power_enable_vneg : 1;
bool power_enable_gl : 1;
bool power_enable_gh : 1;
} epd_config_register_t;
static i2s_bus_config i2s_config = {
.clock = CKH,
.start_pulse = STH,
.data_0 = D0,
.data_1 = D1,
.data_2 = D2,
.data_3 = D3,
.data_4 = D4,
.data_5 = D5,
.data_6 = D6,
.data_7 = D7,
};
static void IRAM_ATTR push_cfg_bit(bool bit) {
gpio_set_level(CFG_CLK, 0);
gpio_set_level(CFG_DATA, bit);
gpio_set_level(CFG_CLK, 1);
}
static epd_config_register_t config_reg;
static void epd_board_init(uint32_t epd_row_width) {
/* Power Control Output/Off */
PIN_FUNC_SELECT(GPIO_PIN_MUX_REG[CFG_DATA], PIN_FUNC_GPIO);
PIN_FUNC_SELECT(GPIO_PIN_MUX_REG[CFG_CLK], PIN_FUNC_GPIO);
PIN_FUNC_SELECT(GPIO_PIN_MUX_REG[CFG_STR], PIN_FUNC_GPIO);
gpio_set_direction(CFG_DATA, GPIO_MODE_OUTPUT);
gpio_set_direction(CFG_CLK, GPIO_MODE_OUTPUT);
gpio_set_direction(CFG_STR, GPIO_MODE_OUTPUT);
fast_gpio_set_lo(CFG_STR);
config_reg.power_disable = true;
config_reg.power_enable_vpos = false;
config_reg.power_enable_vneg = false;
config_reg.power_enable_gl = false;
config_reg.power_enable_gh = false;
// use latch pin as GPIO
PIN_FUNC_SELECT(GPIO_PIN_MUX_REG[V4_LATCH_ENABLE], PIN_FUNC_GPIO);
ESP_ERROR_CHECK(gpio_set_direction(V4_LATCH_ENABLE, GPIO_MODE_OUTPUT));
gpio_set_level(V4_LATCH_ENABLE, 0);
// Setup I2S
// add an offset off dummy bytes to allow for enough timing headroom
i2s_bus_init(&i2s_config, epd_row_width + 32);
rmt_pulse_init(CKV);
epd_board_temperature_init_v2();
}
static void epd_board_set_ctrl(epd_ctrl_state_t* state, const epd_ctrl_state_t* const mask) {
if (state->ep_sth) {
fast_gpio_set_hi(STH);
} else {
fast_gpio_set_lo(STH);
}
if (mask->ep_output_enable || mask->ep_mode || mask->ep_stv) {
fast_gpio_set_lo(CFG_STR);
// push config bits in reverse order
push_cfg_bit(state->ep_output_enable);
push_cfg_bit(state->ep_mode);
push_cfg_bit(config_reg.power_enable_gh);
push_cfg_bit(state->ep_stv);
push_cfg_bit(config_reg.power_enable_gl);
push_cfg_bit(config_reg.power_enable_vneg);
push_cfg_bit(config_reg.power_enable_vpos);
push_cfg_bit(config_reg.power_disable);
fast_gpio_set_hi(CFG_STR);
fast_gpio_set_lo(CFG_STR);
}
if (state->ep_latch_enable) {
fast_gpio_set_hi(V4_LATCH_ENABLE);
} else {
fast_gpio_set_lo(V4_LATCH_ENABLE);
}
}
static void epd_board_poweron(epd_ctrl_state_t* state) {
// POWERON
i2s_gpio_attach(&i2s_config);
epd_ctrl_state_t mask = {
// Trigger output to shift register
.ep_stv = true,
};
config_reg.power_disable = false;
epd_board_set_ctrl(state, &mask);
epd_busy_delay(100 * 240);
config_reg.power_enable_gl = true;
epd_board_set_ctrl(state, &mask);
epd_busy_delay(500 * 240);
config_reg.power_enable_vneg = true;
epd_board_set_ctrl(state, &mask);
epd_busy_delay(500 * 240);
config_reg.power_enable_gh = true;
epd_board_set_ctrl(state, &mask);
epd_busy_delay(500 * 240);
config_reg.power_enable_vpos = true;
epd_board_set_ctrl(state, &mask);
epd_busy_delay(100 * 240);
state->ep_stv = true;
state->ep_sth = true;
mask.ep_sth = true;
epd_board_set_ctrl(state, &mask);
// END POWERON
}
static void epd_board_poweroff(epd_ctrl_state_t* state) {
// POWEROFF
epd_ctrl_state_t mask = {
// Trigger output to shift register
.ep_stv = true,
};
config_reg.power_enable_gh = false;
config_reg.power_enable_vpos = false;
epd_board_set_ctrl(state, &mask);
epd_busy_delay(10 * 240);
config_reg.power_enable_gl = false;
config_reg.power_enable_vneg = false;
epd_board_set_ctrl(state, &mask);
epd_busy_delay(100 * 240);
state->ep_stv = false;
state->ep_output_enable = false;
mask.ep_output_enable = true;
state->ep_mode = false;
mask.ep_mode = true;
config_reg.power_disable = true;
epd_board_set_ctrl(state, &mask);
i2s_gpio_detach(&i2s_config);
// END POWEROFF
}
const EpdBoardDefinition epd_board_v4 = {
.init = epd_board_init,
.deinit = NULL,
.set_ctrl = epd_board_set_ctrl,
.poweron = epd_board_poweron,
.poweroff = epd_board_poweroff,
.get_temperature = epd_board_ambient_temperature_v2,
.set_vcom = NULL,
};

View File

@ -0,0 +1,196 @@
#include "epd_board.h"
#include <sdkconfig.h>
#include "epd_board_common.h"
#include "../output_i2s/i2s_data_bus.h"
#include "../output_i2s/render_i2s.h"
#include "../output_i2s/rmt_pulse.h"
#include "driver/rtc_io.h"
// Make this compile on the S3 to avoid long ifdefs
#ifndef CONFIG_IDF_TARGET_ESP32
#define GPIO_NUM_22 0
#define GPIO_NUM_23 0
#define GPIO_NUM_24 0
#define GPIO_NUM_25 0
#endif
#define CFG_DATA GPIO_NUM_33
#define CFG_CLK GPIO_NUM_32
#define CFG_STR GPIO_NUM_0
#define D7 GPIO_NUM_23
#define D6 GPIO_NUM_22
#define D5 GPIO_NUM_21
#define D4 GPIO_NUM_19
#define D3 GPIO_NUM_18
#define D2 GPIO_NUM_5
#define D1 GPIO_NUM_4
#define D0 GPIO_NUM_25
/* Control Lines */
#define CKV GPIO_NUM_26
#define STH GPIO_NUM_27
#define V4_LATCH_ENABLE GPIO_NUM_2
/* Edges */
#define CKH GPIO_NUM_15
typedef struct {
bool power_enable : 1;
bool power_enable_vpos : 1;
bool power_enable_vneg : 1;
bool power_enable_gl : 1;
bool power_enable_gh : 1;
} epd_config_register_t;
static epd_config_register_t config_reg;
static i2s_bus_config i2s_config = {
.clock = CKH,
.start_pulse = STH,
.data_0 = D0,
.data_1 = D1,
.data_2 = D2,
.data_3 = D3,
.data_4 = D4,
.data_5 = D5,
.data_6 = D6,
.data_7 = D7,
};
static void IRAM_ATTR push_cfg_bit(bool bit) {
gpio_set_level(CFG_CLK, 0);
gpio_set_level(CFG_DATA, bit);
gpio_set_level(CFG_CLK, 1);
}
static void epd_board_init(uint32_t epd_row_width) {
/* Power Control Output/Off */
PIN_FUNC_SELECT(GPIO_PIN_MUX_REG[CFG_DATA], PIN_FUNC_GPIO);
PIN_FUNC_SELECT(GPIO_PIN_MUX_REG[CFG_CLK], PIN_FUNC_GPIO);
PIN_FUNC_SELECT(GPIO_PIN_MUX_REG[CFG_STR], PIN_FUNC_GPIO);
gpio_set_direction(CFG_DATA, GPIO_MODE_OUTPUT);
gpio_set_direction(CFG_CLK, GPIO_MODE_OUTPUT);
gpio_set_direction(CFG_STR, GPIO_MODE_OUTPUT);
fast_gpio_set_lo(CFG_STR);
config_reg.power_enable = false;
config_reg.power_enable_vpos = false;
config_reg.power_enable_vneg = false;
config_reg.power_enable_gl = false;
config_reg.power_enable_gh = false;
// use latch pin as GPIO
PIN_FUNC_SELECT(GPIO_PIN_MUX_REG[V4_LATCH_ENABLE], PIN_FUNC_GPIO);
ESP_ERROR_CHECK(gpio_set_direction(V4_LATCH_ENABLE, GPIO_MODE_OUTPUT));
gpio_set_level(V4_LATCH_ENABLE, 0);
// Setup I2S
// add an offset off dummy bytes to allow for enough timing headroom
i2s_bus_init(&i2s_config, epd_row_width + 32);
rmt_pulse_init(CKV);
epd_board_temperature_init_v2();
}
static void epd_board_set_ctrl(epd_ctrl_state_t* state, const epd_ctrl_state_t* const mask) {
if (state->ep_sth) {
fast_gpio_set_hi(STH);
} else {
fast_gpio_set_lo(STH);
}
if (mask->ep_output_enable || mask->ep_mode || mask->ep_stv) {
fast_gpio_set_lo(CFG_STR);
// push config bits in reverse order
push_cfg_bit(state->ep_output_enable);
push_cfg_bit(state->ep_mode);
push_cfg_bit(config_reg.power_enable_gh);
push_cfg_bit(state->ep_stv);
push_cfg_bit(config_reg.power_enable_gl);
push_cfg_bit(config_reg.power_enable_vneg);
push_cfg_bit(config_reg.power_enable_vpos);
push_cfg_bit(config_reg.power_enable);
fast_gpio_set_hi(CFG_STR);
fast_gpio_set_lo(CFG_STR);
}
if (state->ep_latch_enable) {
fast_gpio_set_hi(V4_LATCH_ENABLE);
} else {
fast_gpio_set_lo(V4_LATCH_ENABLE);
}
}
static void epd_board_poweron(epd_ctrl_state_t* state) {
// POWERON
i2s_gpio_attach(&i2s_config);
epd_ctrl_state_t mask = {
// Trigger output to shift register
.ep_stv = true,
};
config_reg.power_enable = true;
epd_board_set_ctrl(state, &mask);
epd_busy_delay(100 * 240);
config_reg.power_enable_gl = true;
epd_board_set_ctrl(state, &mask);
epd_busy_delay(500 * 240);
config_reg.power_enable_vneg = true;
epd_board_set_ctrl(state, &mask);
epd_busy_delay(500 * 240);
config_reg.power_enable_gh = true;
epd_board_set_ctrl(state, &mask);
epd_busy_delay(500 * 240);
config_reg.power_enable_vpos = true;
epd_board_set_ctrl(state, &mask);
epd_busy_delay(100 * 240);
state->ep_stv = true;
state->ep_sth = true;
mask.ep_sth = true;
epd_board_set_ctrl(state, &mask);
// END POWERON
}
static void epd_board_poweroff(epd_ctrl_state_t* state) {
// POWEROFF
epd_ctrl_state_t mask = {
// Trigger output to shift register
.ep_stv = true,
};
config_reg.power_enable_gh = false;
config_reg.power_enable_vpos = false;
epd_board_set_ctrl(state, &mask);
epd_busy_delay(10 * 240);
config_reg.power_enable_gl = false;
config_reg.power_enable_vneg = false;
epd_board_set_ctrl(state, &mask);
epd_busy_delay(100 * 240);
state->ep_stv = false;
state->ep_output_enable = false;
mask.ep_output_enable = true;
state->ep_mode = false;
mask.ep_mode = true;
config_reg.power_enable = false;
epd_board_set_ctrl(state, &mask);
i2s_gpio_detach(&i2s_config);
// END POWEROFF
}
const EpdBoardDefinition epd_board_v5 = {
.init = epd_board_init,
.deinit = NULL,
.set_ctrl = epd_board_set_ctrl,
.poweron = epd_board_poweron,
.poweroff = epd_board_poweroff,
.get_temperature = epd_board_ambient_temperature_v2,
.set_vcom = NULL,
};

View File

@ -0,0 +1,359 @@
#include "driver/gpio.h"
#include "epd_board.h"
#include <esp_log.h>
#include <sdkconfig.h>
#include <stdint.h>
#include "../output_i2s/i2s_data_bus.h"
#include "../output_i2s/render_i2s.h"
#include "../output_i2s/rmt_pulse.h"
#include "pca9555.h"
#include "tps65185.h"
// Make this compile on the S3 to avoid long ifdefs
#ifndef CONFIG_IDF_TARGET_ESP32
#define GPIO_NUM_22 0
#define GPIO_NUM_23 0
#define GPIO_NUM_24 0
#define GPIO_NUM_25 0
#endif
#define CFG_SCL GPIO_NUM_33
#define CFG_SDA GPIO_NUM_32
#define CFG_INTR GPIO_NUM_35
#define EPDIY_I2C_PORT I2C_NUM_0
#define CFG_PIN_OE (PCA_PIN_PC10 >> 8)
#define CFG_PIN_MODE (PCA_PIN_PC11 >> 8)
#define CFG_PIN_STV (PCA_PIN_PC12 >> 8)
#define CFG_PIN_PWRUP (PCA_PIN_PC13 >> 8)
#define CFG_PIN_VCOM_CTRL (PCA_PIN_PC14 >> 8)
#define CFG_PIN_WAKEUP (PCA_PIN_PC15 >> 8)
#define CFG_PIN_PWRGOOD (PCA_PIN_PC16 >> 8)
#define CFG_PIN_INT (PCA_PIN_PC17 >> 8)
#define D7 GPIO_NUM_23
#define D6 GPIO_NUM_22
#define D5 GPIO_NUM_21
#define D4 GPIO_NUM_19
#define D3 GPIO_NUM_18
#define D2 GPIO_NUM_5
#define D1 GPIO_NUM_4
#define D0 GPIO_NUM_25
/* Control Lines */
#define CKV GPIO_NUM_26
#define STH GPIO_NUM_27
#define V4_LATCH_ENABLE GPIO_NUM_2
/* Edges */
#define CKH GPIO_NUM_15
typedef struct {
i2c_port_t port;
bool pwrup;
bool vcom_ctrl;
bool wakeup;
bool others[8];
} epd_config_register_t;
static i2s_bus_config i2s_config = {
.clock = CKH,
.start_pulse = STH,
.data_0 = D0,
.data_1 = D1,
.data_2 = D2,
.data_3 = D3,
.data_4 = D4,
.data_5 = D5,
.data_6 = D6,
.data_7 = D7,
};
/** The VCOM voltage to use. */
static int vcom = 1600;
static bool interrupt_done = false;
static void IRAM_ATTR interrupt_handler(void* arg) {
interrupt_done = true;
}
static epd_config_register_t config_reg;
static void epd_board_init(uint32_t epd_row_width) {
gpio_hold_dis(CKH); // free CKH after wakeup
i2c_config_t conf;
conf.mode = I2C_MODE_MASTER;
conf.sda_io_num = CFG_SDA;
conf.scl_io_num = CFG_SCL;
conf.sda_pullup_en = GPIO_PULLUP_ENABLE;
conf.scl_pullup_en = GPIO_PULLUP_ENABLE;
conf.master.clk_speed = 400000;
conf.clk_flags = I2C_SCLK_SRC_FLAG_FOR_NOMAL;
ESP_ERROR_CHECK(i2c_param_config(EPDIY_I2C_PORT, &conf));
ESP_ERROR_CHECK(i2c_driver_install(EPDIY_I2C_PORT, I2C_MODE_MASTER, 0, 0, 0));
config_reg.port = EPDIY_I2C_PORT;
config_reg.pwrup = false;
config_reg.vcom_ctrl = false;
config_reg.wakeup = false;
for (int i = 0; i < 8; i++) {
config_reg.others[i] = false;
}
gpio_set_direction(CFG_INTR, GPIO_MODE_INPUT);
gpio_set_intr_type(CFG_INTR, GPIO_INTR_NEGEDGE);
ESP_ERROR_CHECK(gpio_install_isr_service(ESP_INTR_FLAG_EDGE));
ESP_ERROR_CHECK(gpio_isr_handler_add(CFG_INTR, interrupt_handler, (void*)CFG_INTR));
// set all epdiy lines to output except TPS interrupt + PWR good
ESP_ERROR_CHECK(pca9555_set_config(config_reg.port, CFG_PIN_PWRGOOD | CFG_PIN_INT, 1));
// use latch pin as GPIO
PIN_FUNC_SELECT(GPIO_PIN_MUX_REG[V4_LATCH_ENABLE], PIN_FUNC_GPIO);
ESP_ERROR_CHECK(gpio_set_direction(V4_LATCH_ENABLE, GPIO_MODE_OUTPUT));
gpio_set_level(V4_LATCH_ENABLE, 0);
// Setup I2S
// add an offset off dummy bytes to allow for enough timing headroom
i2s_bus_init(&i2s_config, epd_row_width + 32);
rmt_pulse_init(CKV);
}
static void epd_board_deinit() {
// gpio_reset_pin(CFG_INTR);
// rtc_gpio_isolate(CFG_INTR);
ESP_ERROR_CHECK(pca9555_set_config(
config_reg.port, CFG_PIN_PWRGOOD | CFG_PIN_INT | CFG_PIN_VCOM_CTRL | CFG_PIN_PWRUP, 1
));
int tries = 0;
while (!((pca9555_read_input(config_reg.port, 1) & 0xC0) == 0x80)) {
if (tries >= 500) {
ESP_LOGE("epdiy", "failed to shut down TPS65185!");
break;
}
tries++;
vTaskDelay(1);
printf("%X\n", pca9555_read_input(config_reg.port, 1));
}
// Not sure why we need this delay, but the TPS65185 seems to generate an interrupt after some
// time that needs to be cleared.
vTaskDelay(500);
pca9555_read_input(config_reg.port, 0);
pca9555_read_input(config_reg.port, 1);
i2c_driver_delete(EPDIY_I2C_PORT);
gpio_isr_handler_remove(CFG_INTR);
gpio_uninstall_isr_service();
gpio_reset_pin(CFG_INTR);
gpio_reset_pin(V4_LATCH_ENABLE);
}
static void epd_board_set_ctrl(epd_ctrl_state_t* state, const epd_ctrl_state_t* const mask) {
uint8_t value = 0x00;
if (state->ep_sth) {
fast_gpio_set_hi(STH);
} else {
fast_gpio_set_lo(STH);
}
if (mask->ep_output_enable || mask->ep_mode || mask->ep_stv) {
if (state->ep_output_enable)
value |= CFG_PIN_OE;
if (state->ep_mode)
value |= CFG_PIN_MODE;
if (state->ep_stv)
value |= CFG_PIN_STV;
if (config_reg.pwrup)
value |= CFG_PIN_PWRUP;
if (config_reg.vcom_ctrl)
value |= CFG_PIN_VCOM_CTRL;
if (config_reg.wakeup)
value |= CFG_PIN_WAKEUP;
ESP_ERROR_CHECK(pca9555_set_value(config_reg.port, value, 1));
}
if (state->ep_latch_enable) {
fast_gpio_set_hi(V4_LATCH_ENABLE);
} else {
fast_gpio_set_lo(V4_LATCH_ENABLE);
}
}
static void epd_board_poweron(epd_ctrl_state_t* state) {
i2s_gpio_attach(&i2s_config);
epd_ctrl_state_t mask = {
.ep_stv = true,
};
state->ep_stv = true;
config_reg.wakeup = true;
epd_board_set_ctrl(state, &mask);
config_reg.pwrup = true;
epd_board_set_ctrl(state, &mask);
config_reg.vcom_ctrl = true;
epd_board_set_ctrl(state, &mask);
// give the IC time to powerup and set lines
vTaskDelay(1);
int tries = 0;
while (!(pca9555_read_input(config_reg.port, 1) & CFG_PIN_PWRGOOD)) {
if (tries >= 500) {
ESP_LOGE(
"epdiy",
"Power enable failed! INT status: 0x%X 0x%X",
tps_read_register(config_reg.port, TPS_REG_INT1),
tps_read_register(config_reg.port, TPS_REG_INT2)
);
return;
}
tries++;
vTaskDelay(1);
}
ESP_ERROR_CHECK(tps_write_register(config_reg.port, TPS_REG_ENABLE, 0x3F));
tps_set_vcom(config_reg.port, vcom);
state->ep_sth = true;
mask = (const epd_ctrl_state_t){
.ep_sth = true,
};
epd_board_set_ctrl(state, &mask);
while (!((tps_read_register(config_reg.port, TPS_REG_PG) & 0xFA) == 0xFA)) {
if (tries >= 500) {
ESP_LOGE(
"epdiy",
"Power enable failed! PG status: %X",
tps_read_register(config_reg.port, TPS_REG_PG)
);
return;
}
tries++;
vTaskDelay(1);
}
}
static void epd_board_measure_vcom(epd_ctrl_state_t* state) {
epd_ctrl_state_t mask = {
.ep_output_enable = true,
.ep_mode = true,
.ep_stv = true,
};
state->ep_stv = true;
state->ep_mode = false;
state->ep_output_enable = true;
config_reg.wakeup = true;
epd_board_set_ctrl(state, &mask);
config_reg.pwrup = true;
epd_board_set_ctrl(state, &mask);
// give the IC time to powerup and set lines
vTaskDelay(1);
state->ep_sth = true;
mask = (const epd_ctrl_state_t){
.ep_sth = true,
};
epd_board_set_ctrl(state, &mask);
while (!(pca9555_read_input(config_reg.port, 1) & CFG_PIN_PWRGOOD)) {
}
ESP_LOGI("epdiy", "Power rails enabled");
state->ep_sth = true;
mask = (const epd_ctrl_state_t){
.ep_sth = true,
};
epd_board_set_ctrl(state, &mask);
int tries = 0;
while (!((tps_read_register(config_reg.port, TPS_REG_PG) & 0xFA) == 0xFA)) {
if (tries >= 500) {
ESP_LOGE(
"epdiy",
"Power enable failed! PG status: %X",
tps_read_register(config_reg.port, TPS_REG_PG)
);
return;
}
tries++;
vTaskDelay(1);
}
}
static void epd_board_poweroff(epd_ctrl_state_t* state) {
epd_ctrl_state_t mask = {
.ep_stv = true,
.ep_output_enable = true,
.ep_mode = true,
};
config_reg.vcom_ctrl = false;
config_reg.pwrup = false;
state->ep_stv = false;
state->ep_output_enable = false;
state->ep_mode = false;
epd_board_set_ctrl(state, &mask);
vTaskDelay(1);
config_reg.wakeup = false;
epd_board_set_ctrl(state, &mask);
i2s_gpio_detach(&i2s_config);
}
static float epd_board_ambient_temperature() {
return tps_read_thermistor(EPDIY_I2C_PORT);
}
static esp_err_t gpio_set_direction_v6(int pin, bool make_input) {
static uint8_t direction = 0;
uint8_t mask = ~(1 << pin);
direction = (direction & mask) | (make_input << pin);
return pca9555_set_config(EPDIY_I2C_PORT, direction, 0);
}
/**
* Get the input level of the broken-out GPIO extender port.
*/
static bool gpio_get_level_v6(int pin) {
return (pca9555_read_input(EPDIY_I2C_PORT, 0) >> pin) & 1;
}
/**
* Get the input level of the broken-out GPIO extender port.
*/
static esp_err_t gpio_set_value_v6(int pin, bool value) {
static uint8_t state = 0;
uint8_t mask = ~(1 << pin);
state = (state & mask) | (value << pin);
return pca9555_set_value(EPDIY_I2C_PORT, state, 0);
}
static void set_vcom(int value) {
vcom = value;
}
const EpdBoardDefinition epd_board_v6 = {
.init = epd_board_init,
.deinit = epd_board_deinit,
.set_ctrl = epd_board_set_ctrl,
.poweron = epd_board_poweron,
.poweroff = epd_board_poweroff,
.measure_vcom = epd_board_measure_vcom,
.get_temperature = epd_board_ambient_temperature,
.set_vcom = set_vcom,
.gpio_set_direction = gpio_set_direction_v6,
.gpio_read = gpio_get_level_v6,
.gpio_write = gpio_set_value_v6,
};

View File

@ -0,0 +1,338 @@
#include <stdint.h>
#include "epd_board.h"
#include "epdiy.h"
#include "../output_common/render_method.h"
#include "../output_lcd/lcd_driver.h"
#include "esp_log.h"
#include "pca9555.h"
#include "tps65185.h"
#include <driver/gpio.h>
#include <driver/i2c.h>
#include <sdkconfig.h>
// Make this compile von the ESP32 without ifdefing the whole file
#ifndef CONFIG_IDF_TARGET_ESP32S3
#define GPIO_NUM_40 -1
#define GPIO_NUM_41 -1
#define GPIO_NUM_42 -1
#define GPIO_NUM_43 -1
#define GPIO_NUM_44 -1
#define GPIO_NUM_45 -1
#define GPIO_NUM_46 -1
#define GPIO_NUM_47 -1
#define GPIO_NUM_48 -1
#endif
#define CFG_SCL GPIO_NUM_40
#define CFG_SDA GPIO_NUM_39
#define CFG_INTR GPIO_NUM_38
#define EPDIY_I2C_PORT I2C_NUM_0
#define CFG_PIN_OE (PCA_PIN_PC10 >> 8)
#define CFG_PIN_MODE (PCA_PIN_PC11 >> 8)
#define __CFG_PIN_STV (PCA_PIN_PC12 >> 8)
#define CFG_PIN_PWRUP (PCA_PIN_PC13 >> 8)
#define CFG_PIN_VCOM_CTRL (PCA_PIN_PC14 >> 8)
#define CFG_PIN_WAKEUP (PCA_PIN_PC15 >> 8)
#define CFG_PIN_PWRGOOD (PCA_PIN_PC16 >> 8)
#define CFG_PIN_INT (PCA_PIN_PC17 >> 8)
#define D15 GPIO_NUM_47
#define D14 GPIO_NUM_21
#define D13 GPIO_NUM_14
#define D12 GPIO_NUM_13
#define D11 GPIO_NUM_12
#define D10 GPIO_NUM_11
#define D9 GPIO_NUM_10
#define D8 GPIO_NUM_9
#define D7 GPIO_NUM_8
#define D6 GPIO_NUM_18
#define D5 GPIO_NUM_17
#define D4 GPIO_NUM_16
#define D3 GPIO_NUM_15
#define D2 GPIO_NUM_7
#define D1 GPIO_NUM_6
#define D0 GPIO_NUM_5
/* Control Lines */
#define CKV GPIO_NUM_48
#define STH GPIO_NUM_41
#define LEH GPIO_NUM_42
#define STV GPIO_NUM_45
/* Edges */
#define CKH GPIO_NUM_4
typedef struct {
i2c_port_t port;
bool pwrup;
bool vcom_ctrl;
bool wakeup;
bool others[8];
} epd_config_register_t;
/** The VCOM voltage to use. */
static int vcom = 1600;
static epd_config_register_t config_reg;
static bool interrupt_done = false;
static void IRAM_ATTR interrupt_handler(void* arg) {
interrupt_done = true;
}
static lcd_bus_config_t lcd_config = {
.clock = CKH,
.ckv = CKV,
.leh = LEH,
.start_pulse = STH,
.stv = STV,
.data[0] = D0,
.data[1] = D1,
.data[2] = D2,
.data[3] = D3,
.data[4] = D4,
.data[5] = D5,
.data[6] = D6,
.data[7] = D7,
.data[8] = D8,
.data[9] = D9,
.data[10] = D10,
.data[11] = D11,
.data[12] = D12,
.data[13] = D13,
.data[14] = D14,
.data[15] = D15,
};
static void epd_board_init(uint32_t epd_row_width) {
gpio_hold_dis(CKH); // free CKH after wakeup
i2c_config_t conf;
conf.mode = I2C_MODE_MASTER;
conf.sda_io_num = CFG_SDA;
conf.scl_io_num = CFG_SCL;
conf.sda_pullup_en = GPIO_PULLUP_ENABLE;
conf.scl_pullup_en = GPIO_PULLUP_ENABLE;
conf.master.clk_speed = 100000;
conf.clk_flags = 0;
ESP_ERROR_CHECK(i2c_param_config(EPDIY_I2C_PORT, &conf));
ESP_ERROR_CHECK(i2c_driver_install(EPDIY_I2C_PORT, I2C_MODE_MASTER, 0, 0, 0));
config_reg.port = EPDIY_I2C_PORT;
config_reg.pwrup = false;
config_reg.vcom_ctrl = false;
config_reg.wakeup = false;
for (int i = 0; i < 8; i++) {
config_reg.others[i] = false;
}
gpio_set_direction(CFG_INTR, GPIO_MODE_INPUT);
gpio_set_intr_type(CFG_INTR, GPIO_INTR_NEGEDGE);
ESP_ERROR_CHECK(gpio_install_isr_service(ESP_INTR_FLAG_EDGE));
ESP_ERROR_CHECK(gpio_isr_handler_add(CFG_INTR, interrupt_handler, (void*)CFG_INTR));
// set all epdiy lines to output except TPS interrupt + PWR good
ESP_ERROR_CHECK(pca9555_set_config(config_reg.port, CFG_PIN_PWRGOOD | CFG_PIN_INT, 1));
const EpdDisplay_t* display = epd_get_display();
LcdEpdConfig_t config = {
.pixel_clock = display->bus_speed * 1000 * 1000,
.ckv_high_time = 60,
.line_front_porch = 4,
.le_high_time = 4,
.bus_width = display->bus_width,
.bus = lcd_config,
};
epd_lcd_init(&config, display->width, display->height);
}
static void epd_board_deinit() {
epd_lcd_deinit();
ESP_ERROR_CHECK(pca9555_set_config(
config_reg.port, CFG_PIN_PWRGOOD | CFG_PIN_INT | CFG_PIN_VCOM_CTRL | CFG_PIN_PWRUP, 1
));
int tries = 0;
while (!((pca9555_read_input(config_reg.port, 1) & 0xC0) == 0x80)) {
if (tries >= 50) {
ESP_LOGE("epdiy", "failed to shut down TPS65185!");
break;
}
tries++;
vTaskDelay(1);
}
// Not sure why we need this delay, but the TPS65185 seems to generate an interrupt after some
// time that needs to be cleared.
vTaskDelay(50);
pca9555_read_input(config_reg.port, 0);
pca9555_read_input(config_reg.port, 1);
i2c_driver_delete(EPDIY_I2C_PORT);
gpio_uninstall_isr_service();
}
static void epd_board_set_ctrl(epd_ctrl_state_t* state, const epd_ctrl_state_t* const mask) {
uint8_t value = 0x00;
if (mask->ep_output_enable || mask->ep_mode || mask->ep_stv) {
if (state->ep_output_enable)
value |= CFG_PIN_OE;
if (state->ep_mode)
value |= CFG_PIN_MODE;
// if (state->ep_stv) value |= CFG_PIN_STV;
if (config_reg.pwrup)
value |= CFG_PIN_PWRUP;
if (config_reg.vcom_ctrl)
value |= CFG_PIN_VCOM_CTRL;
if (config_reg.wakeup)
value |= CFG_PIN_WAKEUP;
ESP_ERROR_CHECK(pca9555_set_value(config_reg.port, value, 1));
}
}
static void epd_board_poweron(epd_ctrl_state_t* state) {
epd_ctrl_state_t mask = {
.ep_output_enable = true,
.ep_mode = true,
.ep_stv = true,
};
state->ep_stv = true;
state->ep_mode = false;
state->ep_output_enable = true;
config_reg.wakeup = true;
epd_board_set_ctrl(state, &mask);
config_reg.pwrup = true;
epd_board_set_ctrl(state, &mask);
config_reg.vcom_ctrl = true;
epd_board_set_ctrl(state, &mask);
// give the IC time to powerup and set lines
vTaskDelay(1);
while (!(pca9555_read_input(config_reg.port, 1) & CFG_PIN_PWRGOOD)) {
}
ESP_ERROR_CHECK(tps_write_register(config_reg.port, TPS_REG_ENABLE, 0x3F));
tps_set_vcom(config_reg.port, vcom);
state->ep_sth = true;
mask = (const epd_ctrl_state_t){
.ep_sth = true,
};
epd_board_set_ctrl(state, &mask);
int tries = 0;
while (!((tps_read_register(config_reg.port, TPS_REG_PG) & 0xFA) == 0xFA)) {
if (tries >= 500) {
ESP_LOGE(
"epdiy",
"Power enable failed! PG status: %X",
tps_read_register(config_reg.port, TPS_REG_PG)
);
return;
}
tries++;
vTaskDelay(1);
}
}
static void epd_board_measure_vcom(epd_ctrl_state_t* state) {
epd_ctrl_state_t mask = {
.ep_output_enable = true,
.ep_mode = true,
.ep_stv = true,
};
state->ep_stv = true;
state->ep_mode = false;
state->ep_output_enable = true;
config_reg.wakeup = true;
epd_board_set_ctrl(state, &mask);
config_reg.pwrup = true;
epd_board_set_ctrl(state, &mask);
// give the IC time to powerup and set lines
vTaskDelay(1);
state->ep_sth = true;
mask = (const epd_ctrl_state_t){
.ep_sth = true,
};
epd_board_set_ctrl(state, &mask);
while (!(pca9555_read_input(config_reg.port, 1) & CFG_PIN_PWRGOOD)) {
}
ESP_LOGI("epdiy", "Power rails enabled");
state->ep_sth = true;
mask = (const epd_ctrl_state_t){
.ep_sth = true,
};
epd_board_set_ctrl(state, &mask);
int tries = 0;
while (!((tps_read_register(config_reg.port, TPS_REG_PG) & 0xFA) == 0xFA)) {
if (tries >= 500) {
ESP_LOGE(
"epdiy",
"Power enable failed! PG status: %X",
tps_read_register(config_reg.port, TPS_REG_PG)
);
return;
}
tries++;
vTaskDelay(1);
}
}
static void epd_board_poweroff(epd_ctrl_state_t* state) {
epd_ctrl_state_t mask = {
.ep_stv = true,
.ep_output_enable = true,
.ep_mode = true,
};
config_reg.vcom_ctrl = false;
config_reg.pwrup = false;
state->ep_stv = false;
state->ep_output_enable = false;
state->ep_mode = false;
epd_board_set_ctrl(state, &mask);
vTaskDelay(1);
config_reg.wakeup = false;
epd_board_set_ctrl(state, &mask);
}
static float epd_board_ambient_temperature() {
return 20;
}
static void set_vcom(int value) {
vcom = value;
}
const EpdBoardDefinition epd_board_v7 = {
.init = epd_board_init,
.deinit = epd_board_deinit,
.set_ctrl = epd_board_set_ctrl,
.poweron = epd_board_poweron,
.poweroff = epd_board_poweroff,
.measure_vcom = epd_board_measure_vcom,
.get_temperature = epd_board_ambient_temperature,
.set_vcom = set_vcom,
// unimplemented for now, but shares v6 implementation
.gpio_set_direction = NULL,
.gpio_read = NULL,
.gpio_write = NULL,
};

View File

@ -0,0 +1,288 @@
/**
* @file epd_board_v7_raw.c
* @author Martin Fasani www.fasani.de
* @brief Small v7 board without IO expander targeted only to 8-bit einks
* @date 2025-02-10
*/
#include <stdint.h>
#include "epd_board.h"
#include "epdiy.h"
#include "../output_common/render_method.h"
#include "../output_lcd/lcd_driver.h"
#include "esp_log.h"
#include "tps65185.h"
#include <driver/gpio.h>
#include <driver/i2c.h>
#include <sdkconfig.h>
// Make this compile on the ESP32 without ifdefing the whole file
#ifndef CONFIG_IDF_TARGET_ESP32S3
#define GPIO_NUM_40 -1
#define GPIO_NUM_41 -1
#define GPIO_NUM_42 -1
#define GPIO_NUM_43 -1
#define GPIO_NUM_44 -1
#define GPIO_NUM_45 -1
#define GPIO_NUM_46 -1
#define GPIO_NUM_47 -1
#define GPIO_NUM_48 -1
#endif
#define CFG_SCL GPIO_NUM_40
#define CFG_SDA GPIO_NUM_39
#define EPDIY_I2C_PORT I2C_NUM_0
#define CFG_PIN_OE GPIO_NUM_9
#define CFG_PIN_MODE GPIO_NUM_10
#define __CFG_PIN_STV GPIO_NUM_45
#define CFG_PIN_PWRUP GPIO_NUM_11
#define CFG_PIN_VCOM_CTRL GPIO_NUM_12
#define CFG_PIN_WAKEUP GPIO_NUM_14
#define CFG_PIN_PWRGOOD GPIO_NUM_47
#define CFG_PIN_INT GPIO_NUM_13 // TPS65185 INT
#define D7 GPIO_NUM_8
#define D6 GPIO_NUM_18
#define D5 GPIO_NUM_17
#define D4 GPIO_NUM_16
#define D3 GPIO_NUM_15
#define D2 GPIO_NUM_7
#define D1 GPIO_NUM_6
#define D0 GPIO_NUM_5
/* Control Lines */
#define CKV GPIO_NUM_48
#define STH GPIO_NUM_41
#define LEH GPIO_NUM_42
#define STV GPIO_NUM_45
/* Edges */
#define CKH GPIO_NUM_4
typedef struct {
i2c_port_t port;
bool pwrup;
bool vcom_ctrl;
bool wakeup;
bool others[8];
} epd_config_register_t;
/** The VCOM voltage to use. */
static int vcom = 1900;
static epd_config_register_t config_reg;
static bool interrupt_done = false;
static void IRAM_ATTR interrupt_handler(void* arg) {
interrupt_done = true;
}
static lcd_bus_config_t lcd_config = { .clock = CKH,
.ckv = CKV,
.leh = LEH,
.start_pulse = STH,
.stv = STV,
.data[0] = D0,
.data[1] = D1,
.data[2] = D2,
.data[3] = D3,
.data[4] = D4,
.data[5] = D5,
.data[6] = D6,
.data[7] = D7 };
static void epd_board_init(uint32_t epd_row_width) {
gpio_hold_dis(CKH); // free CKH after wakeup
i2c_config_t conf;
conf.mode = I2C_MODE_MASTER;
conf.sda_io_num = CFG_SDA;
conf.scl_io_num = CFG_SCL;
conf.sda_pullup_en = GPIO_PULLUP_ENABLE;
conf.scl_pullup_en = GPIO_PULLUP_ENABLE;
conf.master.clk_speed = 100000;
conf.clk_flags = 0;
ESP_ERROR_CHECK(i2c_param_config(EPDIY_I2C_PORT, &conf));
ESP_ERROR_CHECK(i2c_driver_install(EPDIY_I2C_PORT, I2C_MODE_MASTER, 0, 0, 0));
config_reg.port = EPDIY_I2C_PORT;
config_reg.pwrup = false;
config_reg.vcom_ctrl = false;
config_reg.wakeup = false;
for (int i = 0; i < 8; i++) {
config_reg.others[i] = false;
}
gpio_set_direction(CFG_PIN_INT, GPIO_MODE_INPUT);
gpio_set_intr_type(CFG_PIN_INT, GPIO_INTR_NEGEDGE);
ESP_ERROR_CHECK(gpio_install_isr_service(ESP_INTR_FLAG_EDGE));
ESP_ERROR_CHECK(gpio_isr_handler_add(CFG_PIN_INT, interrupt_handler, (void*)CFG_PIN_INT));
// set all epdiy lines to output except TPS interrupt + PWR good
gpio_set_direction(CFG_PIN_PWRGOOD, GPIO_MODE_INPUT);
gpio_set_pull_mode(CFG_PIN_PWRGOOD, GPIO_PULLUP_ONLY);
gpio_set_direction(CFG_PIN_WAKEUP, GPIO_MODE_OUTPUT);
gpio_set_direction(CFG_PIN_PWRUP, GPIO_MODE_OUTPUT);
gpio_set_direction(CFG_PIN_VCOM_CTRL, GPIO_MODE_OUTPUT);
gpio_set_direction(CFG_PIN_OE, GPIO_MODE_OUTPUT);
gpio_set_direction(CFG_PIN_MODE, GPIO_MODE_OUTPUT);
const EpdDisplay_t* display = epd_get_display();
if (display->bus_width > 8) {
ESP_LOGE("v7_RAW", "displays > 8 bit width are not supported in this board");
}
LcdEpdConfig_t config = {
.pixel_clock = display->bus_speed * 1000 * 1000,
.ckv_high_time = 60,
.line_front_porch = 4,
.le_high_time = 4,
.bus_width = display->bus_width,
.bus = lcd_config,
};
epd_lcd_init(&config, display->width, display->height);
}
static void epd_board_deinit() {
epd_lcd_deinit();
gpio_set_level(CFG_PIN_PWRUP, false);
// Not sure why we need this delay, but the TPS65185 seems to generate an interrupt after some
// time that needs to be cleared.
vTaskDelay(50);
i2c_driver_delete(EPDIY_I2C_PORT);
gpio_uninstall_isr_service();
}
static void epd_board_set_ctrl(epd_ctrl_state_t* state, const epd_ctrl_state_t* const mask) {
if (state->ep_output_enable) {
gpio_set_level(CFG_PIN_OE, 1);
} else {
gpio_set_level(CFG_PIN_OE, 0);
}
if (state->ep_mode) {
gpio_set_level(CFG_PIN_MODE, 1);
} else {
gpio_set_level(CFG_PIN_MODE, 0);
}
if (config_reg.pwrup && !gpio_get_level(CFG_PIN_PWRUP)) {
gpio_set_level(CFG_PIN_PWRUP, 1);
} else {
gpio_set_level(CFG_PIN_PWRUP, 0);
}
if (config_reg.vcom_ctrl && !gpio_get_level(CFG_PIN_VCOM_CTRL)) {
gpio_set_level(CFG_PIN_VCOM_CTRL, 1);
} else {
gpio_set_level(CFG_PIN_VCOM_CTRL, 0);
}
if (config_reg.wakeup && !gpio_get_level(CFG_PIN_WAKEUP)) {
gpio_set_level(CFG_PIN_WAKEUP, 1);
} else {
gpio_set_level(CFG_PIN_WAKEUP, 0);
}
}
static void epd_board_poweron(epd_ctrl_state_t* state) {
epd_ctrl_state_t mask = {
.ep_output_enable = true,
.ep_mode = true,
.ep_stv = true,
};
state->ep_stv = true;
state->ep_mode = true;
state->ep_output_enable = true;
config_reg.wakeup = true;
epd_board_set_ctrl(state, &mask);
vTaskDelay(pdMS_TO_TICKS(10));
config_reg.pwrup = true;
epd_board_set_ctrl(state, &mask);
vTaskDelay(pdMS_TO_TICKS(10));
config_reg.vcom_ctrl = true;
epd_board_set_ctrl(state, &mask);
// give the IC time to powerup and set lines
vTaskDelay(1);
// Check if PWRs lines are up
int timeout_counter = 0;
const int timeout_limit = 20; // 20 * 100ms = 2 seconds
while (gpio_get_level(CFG_PIN_PWRGOOD) == 0) {
if (timeout_counter >= timeout_limit) {
ESP_LOGE("v7_RAW", "Timeout waiting for PWRGOOD signal");
break;
}
timeout_counter++;
vTaskDelay(pdMS_TO_TICKS(100));
}
ESP_LOGI("v7_RAW", "PWRGOOD OK");
tps_set_vcom(config_reg.port, vcom);
int tries = 0;
while (!((tps_read_register(config_reg.port, TPS_REG_PG) & 0xFA) == 0xFA)) {
if (tries >= 500) {
ESP_LOGE(
"epdiy",
"Power enable failed! PG status: %X",
tps_read_register(config_reg.port, TPS_REG_PG)
);
return;
}
tries++;
vTaskDelay(1);
}
}
static void epd_board_poweroff(epd_ctrl_state_t* state) {
epd_ctrl_state_t mask = {
.ep_stv = true,
.ep_output_enable = true,
.ep_mode = true,
};
config_reg.vcom_ctrl = false;
config_reg.pwrup = false;
state->ep_stv = false;
state->ep_output_enable = false;
state->ep_mode = false;
epd_board_set_ctrl(state, &mask);
vTaskDelay(1);
config_reg.wakeup = false;
epd_board_set_ctrl(state, &mask);
}
static float epd_board_ambient_temperature() {
return tps_read_thermistor(EPDIY_I2C_PORT);
}
static void set_vcom(int value) {
vcom = value;
}
const EpdBoardDefinition epd_board_v7_raw = {
.init = epd_board_init,
.deinit = epd_board_deinit,
.set_ctrl = epd_board_set_ctrl,
.poweron = epd_board_poweron,
.poweroff = epd_board_poweroff,
.get_temperature = epd_board_ambient_temperature,
.set_vcom = set_vcom,
// unimplemented for now, but shares v6 implementation
.gpio_set_direction = NULL,
.gpio_read = NULL,
.gpio_write = NULL,
};

View File

@ -0,0 +1,273 @@
#include <stdint.h>
#include "epd_board.h"
#include "epdiy.h"
#include "../output_common/render_method.h"
#include "../output_lcd/lcd_driver.h"
#include "esp_log.h"
#include "pca9555.h"
#include "tps65185.h"
#include <driver/gpio.h>
#include <driver/i2c.h>
#include <sdkconfig.h>
// Make this compile von the ESP32 without ifdefing the whole file
#ifndef CONFIG_IDF_TARGET_ESP32S3
#define GPIO_NUM_40 -1
#define GPIO_NUM_41 -1
#define GPIO_NUM_42 -1
#define GPIO_NUM_43 -1
#define GPIO_NUM_44 -1
#define GPIO_NUM_45 -1
#define GPIO_NUM_46 -1
#define GPIO_NUM_47 -1
#define GPIO_NUM_48 -1
#endif
#define CFG_SCL GPIO_NUM_40
#define CFG_SDA GPIO_NUM_39
#define CFG_INTR GPIO_NUM_38
#define EPDIY_I2C_PORT I2C_NUM_0
#define CFG_PIN_OE (PCA_PIN_PC10 >> 8)
#define CFG_PIN_MODE (PCA_PIN_PC11 >> 8)
#define __CFG_PIN_STV (PCA_PIN_PC12 >> 8)
#define CFG_PIN_PWRUP (PCA_PIN_PC13 >> 8)
#define CFG_PIN_VCOM_CTRL (PCA_PIN_PC14 >> 8)
#define CFG_PIN_WAKEUP (PCA_PIN_PC15 >> 8)
#define CFG_PIN_PWRGOOD (PCA_PIN_PC16 >> 8)
#define CFG_PIN_INT (PCA_PIN_PC17 >> 8)
#define D7 GPIO_NUM_8
#define D6 GPIO_NUM_18
#define D5 GPIO_NUM_17
#define D4 GPIO_NUM_16
#define D3 GPIO_NUM_15
#define D2 GPIO_NUM_7
#define D1 GPIO_NUM_6
#define D0 GPIO_NUM_5
/* Control Lines */
#define CKV GPIO_NUM_48
#define STH GPIO_NUM_41
#define LEH GPIO_NUM_42
#define STV GPIO_NUM_45
/* Edges */
#define CKH GPIO_NUM_4
typedef struct {
i2c_port_t port;
bool pwrup;
bool vcom_ctrl;
bool wakeup;
bool others[8];
} epd_config_register_t;
/** The VCOM voltage to use. */
static int vcom = 1600;
static epd_config_register_t config_reg;
static bool interrupt_done = false;
static void IRAM_ATTR interrupt_handler(void* arg) {
interrupt_done = true;
}
static lcd_bus_config_t lcd_config = {
.clock = CKH,
.ckv = CKV,
.leh = LEH,
.start_pulse = STH,
.stv = STV,
.data[0] = D0,
.data[1] = D1,
.data[2] = D2,
.data[3] = D3,
.data[4] = D4,
.data[5] = D5,
.data[6] = D6,
.data[7] = D7,
};
static void epd_board_init(uint32_t epd_row_width) {
gpio_hold_dis(CKH); // free CKH after wakeup
i2c_config_t conf;
conf.mode = I2C_MODE_MASTER;
conf.sda_io_num = CFG_SDA;
conf.scl_io_num = CFG_SCL;
conf.sda_pullup_en = GPIO_PULLUP_ENABLE;
conf.scl_pullup_en = GPIO_PULLUP_ENABLE;
conf.master.clk_speed = 100000;
conf.clk_flags = 0;
ESP_ERROR_CHECK(i2c_param_config(EPDIY_I2C_PORT, &conf));
ESP_ERROR_CHECK(i2c_driver_install(EPDIY_I2C_PORT, I2C_MODE_MASTER, 0, 0, 0));
config_reg.port = EPDIY_I2C_PORT;
config_reg.pwrup = false;
config_reg.vcom_ctrl = false;
config_reg.wakeup = false;
for (int i = 0; i < 8; i++) {
config_reg.others[i] = false;
}
gpio_set_direction(CFG_INTR, GPIO_MODE_INPUT);
gpio_set_intr_type(CFG_INTR, GPIO_INTR_NEGEDGE);
ESP_ERROR_CHECK(gpio_install_isr_service(ESP_INTR_FLAG_EDGE));
ESP_ERROR_CHECK(gpio_isr_handler_add(CFG_INTR, interrupt_handler, (void*)CFG_INTR));
// set all epdiy lines to output except TPS interrupt + PWR good
ESP_ERROR_CHECK(pca9555_set_config(config_reg.port, CFG_PIN_PWRGOOD | CFG_PIN_INT, 1));
const EpdDisplay_t* display = epd_get_display();
LcdEpdConfig_t config = {
.pixel_clock = display->bus_speed * 1000 * 1000,
.ckv_high_time = 60,
.line_front_porch = 4,
.le_high_time = 4,
.bus_width = display->bus_width,
.bus = lcd_config,
};
epd_lcd_init(&config, display->width, display->height);
}
static void epd_board_deinit() {
epd_lcd_deinit();
ESP_ERROR_CHECK(pca9555_set_config(
config_reg.port, CFG_PIN_PWRGOOD | CFG_PIN_INT | CFG_PIN_VCOM_CTRL | CFG_PIN_PWRUP, 1
));
int tries = 0;
while (!((pca9555_read_input(config_reg.port, 1) & 0xC0) == 0x80)) {
if (tries >= 50) {
ESP_LOGE("epdiy", "failed to shut down TPS65185!");
break;
}
tries++;
vTaskDelay(1);
}
// Not sure why we need this delay, but the TPS65185 seems to generate an interrupt after some
// time that needs to be cleared.
vTaskDelay(50);
pca9555_read_input(config_reg.port, 0);
pca9555_read_input(config_reg.port, 1);
i2c_driver_delete(EPDIY_I2C_PORT);
gpio_uninstall_isr_service();
}
static void epd_board_set_ctrl(epd_ctrl_state_t* state, const epd_ctrl_state_t* const mask) {
uint8_t value = 0x00;
if (mask->ep_output_enable || mask->ep_mode || mask->ep_stv) {
if (state->ep_output_enable)
value |= CFG_PIN_OE;
if (state->ep_mode)
value |= CFG_PIN_MODE;
// if (state->ep_stv) value |= CFG_PIN_STV;
if (config_reg.pwrup)
value |= CFG_PIN_PWRUP;
if (config_reg.vcom_ctrl)
value |= CFG_PIN_VCOM_CTRL;
if (config_reg.wakeup)
value |= CFG_PIN_WAKEUP;
ESP_ERROR_CHECK(pca9555_set_value(config_reg.port, value, 1));
}
}
static void epd_board_poweron(epd_ctrl_state_t* state) {
epd_ctrl_state_t mask = {
.ep_output_enable = true,
.ep_mode = true,
.ep_stv = true,
};
state->ep_stv = true;
state->ep_mode = false;
state->ep_output_enable = true;
config_reg.wakeup = true;
epd_board_set_ctrl(state, &mask);
config_reg.pwrup = true;
epd_board_set_ctrl(state, &mask);
config_reg.vcom_ctrl = true;
epd_board_set_ctrl(state, &mask);
// give the IC time to powerup and set lines
vTaskDelay(1);
while (!(pca9555_read_input(config_reg.port, 1) & CFG_PIN_PWRGOOD)) {
}
ESP_ERROR_CHECK(tps_write_register(config_reg.port, TPS_REG_ENABLE, 0x3F));
tps_set_vcom(config_reg.port, vcom);
state->ep_sth = true;
mask = (const epd_ctrl_state_t){
.ep_sth = true,
};
epd_board_set_ctrl(state, &mask);
int tries = 0;
while (!((tps_read_register(config_reg.port, TPS_REG_PG) & 0xFA) == 0xFA)) {
if (tries >= 500) {
ESP_LOGE(
"epdiy",
"Power enable failed! PG status: %X",
tps_read_register(config_reg.port, TPS_REG_PG)
);
return;
}
tries++;
vTaskDelay(1);
}
}
static void epd_board_poweroff(epd_ctrl_state_t* state) {
epd_ctrl_state_t mask = {
.ep_stv = true,
.ep_output_enable = true,
.ep_mode = true,
};
config_reg.vcom_ctrl = false;
config_reg.pwrup = false;
state->ep_stv = false;
state->ep_output_enable = false;
state->ep_mode = false;
epd_board_set_ctrl(state, &mask);
vTaskDelay(1);
config_reg.wakeup = false;
epd_board_set_ctrl(state, &mask);
}
static float epd_board_ambient_temperature() {
return 20;
}
static void set_vcom(int value) {
vcom = value;
}
const EpdBoardDefinition lilygo_board_s3 = {
.init = epd_board_init,
.deinit = epd_board_deinit,
.set_ctrl = epd_board_set_ctrl,
.poweron = epd_board_poweron,
.poweroff = epd_board_poweroff,
.get_temperature = epd_board_ambient_temperature,
.set_vcom = set_vcom,
// unimplemented for now, but shares v6 implementation
.gpio_set_direction = NULL,
.gpio_read = NULL,
.gpio_write = NULL,
};

View File

@ -0,0 +1,107 @@
#include "pca9555.h"
#include <driver/i2c.h>
#include <esp_err.h>
#include <esp_log.h>
#include <stdint.h>
#define REG_INPUT_PORT0 0
#define REG_INPUT_PORT1 1
#define REG_OUTPUT_PORT0 2
#define REG_OUTPUT_PORT1 3
#define REG_INVERT_PORT0 4
#define REG_INVERT_PORT1 5
#define REG_CONFIG_PORT0 6
#define REG_CONFIG_PORT1 7
static esp_err_t i2c_master_read_slave(i2c_port_t i2c_num, uint8_t* data_rd, size_t size, int reg) {
if (size == 0) {
return ESP_OK;
}
i2c_cmd_handle_t cmd = i2c_cmd_link_create();
if (cmd == NULL) {
ESP_LOGE("epdiy", "insufficient memory for I2C transaction");
}
i2c_master_start(cmd);
i2c_master_write_byte(cmd, (EPDIY_PCA9555_ADDR << 1) | I2C_MASTER_WRITE, true);
i2c_master_write_byte(cmd, reg, true);
i2c_master_stop(cmd);
esp_err_t ret = i2c_master_cmd_begin(i2c_num, cmd, 1000 / portTICK_PERIOD_MS);
if (ret != ESP_OK) {
return ret;
}
i2c_cmd_link_delete(cmd);
cmd = i2c_cmd_link_create();
if (cmd == NULL) {
ESP_LOGE("epdiy", "insufficient memory for I2C transaction");
}
i2c_master_start(cmd);
i2c_master_write_byte(cmd, (EPDIY_PCA9555_ADDR << 1) | I2C_MASTER_READ, true);
if (size > 1) {
i2c_master_read(cmd, data_rd, size - 1, I2C_MASTER_ACK);
}
i2c_master_read_byte(cmd, data_rd + size - 1, I2C_MASTER_NACK);
i2c_master_stop(cmd);
ret = i2c_master_cmd_begin(i2c_num, cmd, 1000 / portTICK_PERIOD_MS);
if (ret != ESP_OK) {
return ret;
}
i2c_cmd_link_delete(cmd);
return ESP_OK;
}
static esp_err_t i2c_master_write_slave(
i2c_port_t i2c_num, uint8_t ctrl, uint8_t* data_wr, size_t size
) {
i2c_cmd_handle_t cmd = i2c_cmd_link_create();
if (cmd == NULL) {
ESP_LOGE("epdiy", "insufficient memory for I2C transaction");
}
i2c_master_start(cmd);
i2c_master_write_byte(cmd, (EPDIY_PCA9555_ADDR << 1) | I2C_MASTER_WRITE, true);
i2c_master_write_byte(cmd, ctrl, true);
i2c_master_write(cmd, data_wr, size, true);
i2c_master_stop(cmd);
esp_err_t ret = i2c_master_cmd_begin(i2c_num, cmd, 1000 / portTICK_PERIOD_MS);
i2c_cmd_link_delete(cmd);
return ret;
}
static esp_err_t pca9555_write_single(i2c_port_t port, int reg, uint8_t value) {
uint8_t w_data[1] = { value };
return i2c_master_write_slave(port, reg, w_data, sizeof(w_data));
}
esp_err_t pca9555_set_config(i2c_port_t port, uint8_t config_value, int high_port) {
return pca9555_write_single(port, REG_CONFIG_PORT0 + high_port, config_value);
}
esp_err_t pca9555_set_inversion(i2c_port_t port, uint8_t config_value, int high_port) {
return pca9555_write_single(port, REG_INVERT_PORT0 + high_port, config_value);
}
esp_err_t pca9555_set_value(i2c_port_t port, uint8_t config_value, int high_port) {
return pca9555_write_single(port, REG_OUTPUT_PORT0 + high_port, config_value);
}
uint8_t pca9555_read_input(i2c_port_t i2c_port, int high_port) {
esp_err_t err;
uint8_t r_data[1];
err = i2c_master_read_slave(i2c_port, r_data, 1, REG_INPUT_PORT0 + high_port);
if (err != ESP_OK) {
ESP_LOGE("PCA9555", "%s failed", __func__);
return 0;
}
return r_data[0];
}

View File

@ -0,0 +1,35 @@
#ifndef PCA9555_H
#define PCA9555_H
#include <driver/i2c.h>
#include <esp_err.h>
#define PCA_PIN_P00 0x0001
#define PCA_PIN_P01 0x0002
#define PCA_PIN_P02 0x0004
#define PCA_PIN_P03 0x0008
#define PCA_PIN_P04 0x0010
#define PCA_PIN_P05 0x0020
#define PCA_PIN_P06 0x0040
#define PCA_PIN_P07 0x0080
#define PCA_PIN_PC10 0x0100
#define PCA_PIN_PC11 0x0200
#define PCA_PIN_PC12 0x0400
#define PCA_PIN_PC13 0x0800
#define PCA_PIN_PC14 0x1000
#define PCA_PIN_PC15 0x2000
#define PCA_PIN_PC16 0x4000
#define PCA_PIN_PC17 0x8000
#define PCA_PIN_P_ALL 0x00FF
#define PCA_PIN_PC_ALL 0xFF00
#define PCA_PIN_ALL 0xFFFF
#define PCA_PIN_NULL 0x0000
static const int EPDIY_PCA9555_ADDR = 0x20;
uint8_t pca9555_read_input(i2c_port_t port, int high_port);
esp_err_t pca9555_set_value(i2c_port_t port, uint8_t config_value, int high_port);
esp_err_t pca9555_set_inversion(i2c_port_t port, uint8_t config_value, int high_port);
esp_err_t pca9555_set_config(i2c_port_t port, uint8_t config_value, int high_port);
#endif // PCA9555_H

View File

@ -0,0 +1,128 @@
#include "tps65185.h"
#include "pca9555.h"
#include "epd_board.h"
#include "esp_err.h"
#include "esp_log.h"
#include <driver/i2c.h>
#include <stdint.h>
static const int EPDIY_TPS_ADDR = 0x68;
static uint8_t i2c_master_read_slave(i2c_port_t i2c_num, int reg) {
uint8_t r_data[1];
i2c_cmd_handle_t cmd = i2c_cmd_link_create();
i2c_master_start(cmd);
i2c_master_write_byte(cmd, (EPDIY_TPS_ADDR << 1) | I2C_MASTER_WRITE, true);
i2c_master_write_byte(cmd, reg, true);
i2c_master_stop(cmd);
ESP_ERROR_CHECK(i2c_master_cmd_begin(i2c_num, cmd, 1000 / portTICK_PERIOD_MS));
i2c_cmd_link_delete(cmd);
cmd = i2c_cmd_link_create();
i2c_master_start(cmd);
i2c_master_write_byte(cmd, (EPDIY_TPS_ADDR << 1) | I2C_MASTER_READ, true);
/*
if (size > 1) {
i2c_master_read(cmd, data_rd, size - 1, I2C_MASTER_ACK);
}
*/
i2c_master_read_byte(cmd, r_data, I2C_MASTER_NACK);
i2c_master_stop(cmd);
ESP_ERROR_CHECK(i2c_master_cmd_begin(i2c_num, cmd, 1000 / portTICK_PERIOD_MS));
i2c_cmd_link_delete(cmd);
return r_data[0];
}
static esp_err_t i2c_master_write_slave(
i2c_port_t i2c_num, uint8_t ctrl, uint8_t* data_wr, size_t size
) {
i2c_cmd_handle_t cmd = i2c_cmd_link_create();
i2c_master_start(cmd);
i2c_master_write_byte(cmd, (EPDIY_TPS_ADDR << 1) | I2C_MASTER_WRITE, true);
i2c_master_write_byte(cmd, ctrl, true);
i2c_master_write(cmd, data_wr, size, true);
i2c_master_stop(cmd);
esp_err_t ret = i2c_master_cmd_begin(i2c_num, cmd, 1000 / portTICK_PERIOD_MS);
i2c_cmd_link_delete(cmd);
return ret;
}
esp_err_t tps_write_register(i2c_port_t port, int reg, uint8_t value) {
uint8_t w_data[1];
esp_err_t err;
w_data[0] = value;
err = i2c_master_write_slave(port, reg, w_data, 1);
return err;
}
uint8_t tps_read_register(i2c_port_t i2c_num, int reg) {
return i2c_master_read_slave(i2c_num, reg);
}
void tps_set_vcom(i2c_port_t i2c_num, unsigned vcom_mV) {
unsigned val = vcom_mV / 10;
ESP_ERROR_CHECK(tps_write_register(i2c_num, 4, (val & 0x100) >> 8));
ESP_ERROR_CHECK(tps_write_register(i2c_num, 3, val & 0xFF));
}
int8_t tps_read_thermistor(i2c_port_t i2c_num) {
tps_write_register(i2c_num, TPS_REG_TMST1, 0x80);
int tries = 0;
while (true) {
uint8_t val = tps_read_register(i2c_num, TPS_REG_TMST1);
// temperature conversion done
if (val & 0x20) {
break;
}
tries++;
if (tries >= 100) {
ESP_LOGE("epdiy", "thermistor read timeout!");
break;
}
}
return (int8_t)tps_read_register(i2c_num, TPS_REG_TMST_VALUE);
}
void tps_vcom_kickback() {
printf("VCOM Kickback test\n");
// pull the WAKEUP pin and the PWRUP pin high to enable all output rails.
epd_current_board()->measure_vcom(epd_ctrl_state());
// set the HiZ bit in the VCOM2 register (BIT 5) 0x20
// this puts the VCOM pin in a high-impedance state.
// bit 3 & 4 Number of acquisitions that is averaged to a single kick-back V. measurement
tps_write_register(I2C_NUM_0, 4, 0x38);
vTaskDelay(1);
uint8_t int1reg = tps_read_register(I2C_NUM_0, TPS_REG_INT1);
uint8_t vcomreg = tps_read_register(I2C_NUM_0, TPS_REG_VCOM2);
}
void tps_vcom_kickback_start() {
uint8_t int1reg = tps_read_register(I2C_NUM_0, TPS_REG_INT1);
// set the ACQ bit in the VCOM2 register to 1 (BIT 7)
tps_write_register(I2C_NUM_0, TPS_REG_VCOM2, 0xA0);
}
unsigned tps_vcom_kickback_rdy() {
uint8_t int1reg = tps_read_register(I2C_NUM_0, TPS_REG_INT1);
if (int1reg == 0x02) {
uint8_t lsb = tps_read_register(I2C_NUM_0, 3);
uint8_t msb = tps_read_register(I2C_NUM_0, 4);
int u16Value = (lsb | (msb << 8)) & 0x1ff;
ESP_LOGI("vcom", "raw value:%d temperature:%d C", u16Value, tps_read_thermistor(I2C_NUM_0));
return u16Value * 10;
} else {
return 0;
}
}

View File

@ -0,0 +1,56 @@
#ifndef TPS65185_H
#define TPS65185_H
#include <driver/i2c.h>
#define TPS_REG_TMST_VALUE 0x00
#define TPS_REG_ENABLE 0x01
#define TPS_REG_VADJ 0x02
#define TPS_REG_VCOM1 0x03
#define TPS_REG_VCOM2 0x04
#define TPS_REG_INT_EN1 0x05
#define TPS_REG_INT_EN2 0x06
#define TPS_REG_INT1 0x07
#define TPS_REG_INT2 0x08
#define TPS_REG_UPSEQ0 0x09
#define TPS_REG_UPSEQ1 0x0A
#define TPS_REG_DWNSEQ0 0x0B
#define TPS_REG_DWNSEQ1 0x0C
#define TPS_REG_TMST1 0x0D
#define TPS_REG_TMST2 0x0E
#define TPS_REG_PG 0x0F
#define TPS_REG_REVID 0x10
esp_err_t tps_write_register(i2c_port_t port, int reg, uint8_t value);
uint8_t tps_read_register(i2c_port_t i2c_num, int reg);
/**
* Sets the VCOM voltage in positive milivolts: 1600 -> -1.6V
*/
void tps_set_vcom(i2c_port_t i2c_num, unsigned vcom_mV);
/**
* @brief Please read datasheet section 8.3.7.1 Kick-Back Voltage Measurement
* 1 Device enters ACTIVE mode
* 2 All power rails are up except VCOM
* VCOM pin is in HiZ state
*/
void tps_vcom_kickback();
/**
* @brief start VCOM kick-back voltage measurements
*/
void tps_vcom_kickback_start();
/**
* VCOM kick-back ACQC (Acquisition Complete) bit in the INT1 register is set
* @return unsigned: 0 is not read
*/
unsigned tps_vcom_kickback_rdy();
/**
* Read the temperature via the on-board thermistor.
*/
int8_t tps_read_thermistor(i2c_port_t i2c_num);
#endif // TPS65185_H

View File

@ -0,0 +1,5 @@
#include "epd_board_specific.h"
void epd_powerdown() {
epd_powerdown_lilygo_t5_47();
}

View File

@ -1,10 +1,15 @@
#include "epd_driver.h"
#include "epdiy.h"
#include "waveforms/ED047TC2.h"
#include "waveforms/epdiy_ED047TC1.h"
// Note: Alternative Waveform added by Lilygo on Oct 2021, size: 266 Kb (ED047TC1 is 37 Kb, 7 times
// smaller)
#include "waveforms/epdiy_ED047TC2.h"
#include "waveforms/epdiy_ED060SC4.h"
#include "waveforms/epdiy_ED060XC3.h"
#include "waveforms/epdiy_ED060SCT.h"
#include "waveforms/epdiy_ED060XC3.h"
#include "waveforms/epdiy_ED097OC4.h"
#include "waveforms/epdiy_ED097TC2.h"
#include "waveforms/epdiy_ED133UT2.h"
#include "waveforms/epdiy_NULL.h"

View File

@ -0,0 +1,159 @@
#include <xtensa/config/core-isa.h>
#include <xtensa/config/core-matmap.h>
#include "sdkconfig.h"
#ifdef CONFIG_IDF_TARGET_ESP32S3
.text
.align 4
.global epd_interlace_4bpp_line_VE
.type epd_interlace_4bpp_line_VE,@function
// // CRASH AND BURN for debugging
// EE.MOVI.32.A q3, a2, 0
// EE.MOVI.32.A q3, a3, 1
// EE.MOVI.32.A q3, a4, 2
// EE.MOVI.32.A q3, a5, 3
// l8ui a10, a10, 0
// bool interlace_line(
// const uint8_t *to,
// const uint8_t *from,
// uint8_t *col_dirtyness;
// uint8_t *interlaced,
// int fb_width
// )
epd_interlace_4bpp_line_VE:
// to - a2
// from - a3
// interlaced - a4
// col_dirtyness - a5
// fb_width - a6
entry a1, 32
// divide by 32 for loop count
srli a11, a6, 5
movi.n a10, 0xF0F0F0F0;
EE.MOVI.32.Q q6,a10,0
EE.MOVI.32.Q q6,a10,1
EE.MOVI.32.Q q6,a10,2
EE.MOVI.32.Q q6,a10,3
movi.n a10, 0x0F0F0F0F
EE.MOVI.32.Q q7,a10,0
EE.MOVI.32.Q q7,a10,1
EE.MOVI.32.Q q7,a10,2
EE.MOVI.32.Q q7,a10,3
// put 4 into shift amount
movi.n a10, 4
WSR.SAR a10
// "dirtyness" register
EE.ZERO.Q q5
// Instructions sometimes are in an unexpected order
// for best pipeline utilization
loopnez a11, .loop_end_difference
EE.VLD.128.IP q0, a2, 16
EE.VLD.128.IP q1, a3, 16
// load column dirtyness
EE.VLD.128.IP q3, a5, 0
// update dirtyness
EE.XORQ q4, q1, q0
// line dirtyness accumulator
EE.ORQ q5, q5, q4
// column dirtyness
EE.ORQ q3, q3, q4
// store column dirtyness
EE.VST.128.IP q3, a5, 16
// mask out every second value
EE.ANDQ q2, q0, q7
EE.ANDQ q0, q0, q6
EE.ANDQ q3, q1, q7
EE.ANDQ q1, q1, q6
// shift vectors to align
EE.VSL.32 q2, q2
EE.VSR.32 q1, q1
// the right shift sign-extends,
// so we make sure the resulting shift is logical by masking again
EE.ANDQ q1, q1, q7
// Combine "from" and "to" nibble
EE.ORQ q2, q2, q3
EE.ORQ q0, q0, q1
// Zip masked out values together
EE.VZIP.8 q2, q0
// store interlaced buffer data
EE.VST.128.IP q2, a4, 16
EE.VST.128.IP q0, a4, 16
.loop_end_difference:
EE.MOVI.32.A q5, a2, 0
EE.MOVI.32.A q5, a3, 1
EE.MOVI.32.A q5, a4, 2
EE.MOVI.32.A q5, a5, 3
or a2, a2, a3
or a2, a2, a4
or a2, a2, a5
//movi.n a2, 1 // return "true"
// CRASH AND BURN for debugging
//EE.MOVI.32.A q5, a2, 0
//EE.MOVI.32.A q5, a3, 1
//EE.MOVI.32.A q5, a4, 2
//EE.MOVI.32.A q5, a5, 3
//movi.n a10, 0
//l8ui a10, a10, 0
retw.n
.global epd_apply_line_mask_VE
.type epd_apply_line_mask_VE,@function
// void epd_apply_line_mask_VE(
// uint8_t *line,
// const uint8_t *mask,
// int mask_len
// )
epd_apply_line_mask_VE:
// line - a2
// mask - a3
// mask_len - a4
entry a1, 32
// divide by 16 for loop count
srli a4, a4, 4
// Instructions sometimes are in an unexpected order
// for best pipeline utilization
loopnez a4, .loop_end_mask
EE.VLD.128.IP q0, a2, 0
EE.VLD.128.IP q1, a3, 16
EE.ANDQ q0, q0, q1
EE.VST.128.IP q0, a2, 16
.loop_end_mask:
retw.n
#endif

View File

@ -0,0 +1,83 @@
#include "epd_display.h"
const EpdDisplay_t ED060SCT = {
.width = 800,
.height = 600,
.bus_width = 8,
.bus_speed = 20,
.default_waveform = &epdiy_ED060SCT,
.display_type = DISPLAY_TYPE_GENERIC,
};
const EpdDisplay_t ED060XC3 = {
.width = 1024,
.height = 768,
.bus_width = 8,
.bus_speed = 20,
.default_waveform = &epdiy_ED060XC3,
.display_type = DISPLAY_TYPE_GENERIC,
};
const EpdDisplay_t ED097OC4 = {
.width = 1200,
.height = 825,
.bus_width = 8,
.bus_speed = 15,
.default_waveform = &epdiy_ED097OC4,
.display_type = DISPLAY_TYPE_GENERIC,
};
const EpdDisplay_t ED097TC2 = {
.width = 1200,
.height = 825,
.bus_width = 8,
.bus_speed = 22,
.default_waveform = &epdiy_ED097TC2,
.display_type = DISPLAY_TYPE_ED097TC2,
};
const EpdDisplay_t ED133UT2 = {
.width = 1600,
.height = 1200,
.bus_width = 8,
.bus_speed = 20,
.default_waveform = &epdiy_ED097TC2,
.display_type = DISPLAY_TYPE_ED097TC2,
};
const EpdDisplay_t ED047TC1 = {
.width = 960,
.height = 540,
.bus_width = 8,
.bus_speed = 20,
.default_waveform = &epdiy_ED047TC1,
.display_type = DISPLAY_TYPE_GENERIC,
};
const EpdDisplay_t ED047TC2 = {
.width = 960,
.height = 540,
.bus_width = 8,
.bus_speed = 20,
.default_waveform = &epdiy_ED047TC2,
.display_type = DISPLAY_TYPE_GENERIC,
};
const EpdDisplay_t ED078KC1 = {
.width = 1872,
.height = 1404,
.bus_width = 16,
.bus_speed = 11,
.default_waveform = &epdiy_ED047TC2,
.display_type = DISPLAY_TYPE_GENERIC,
};
// Attention is by default horizontal rows mirrored
const EpdDisplay_t ED052TC4 = {
.width = 1280,
.height = 720,
.bus_width = 8,
.bus_speed = 22,
.default_waveform = &epdiy_ED097TC2,
.display_type = DISPLAY_TYPE_ED097TC2,
};

View File

@ -37,7 +37,7 @@
#include <esp_types.h>
#include <xtensa/core-macros.h>
#include <driver/gpio.h>
#include "epd_driver.h"
//#include "epd_driver.h"
#include "epd_highlevel.h"
#define WAVEFORM EPD_BUILTIN_WAVEFORM
@ -58,8 +58,8 @@ Epd47::Epd47(int16_t dwidth, int16_t dheight) : Renderer(dwidth, dheight) {
disp_bpp = 4;
}
int32_t Epd47::Init(void) {
epd_init(EPD_LUT_1K);
int Epd47::Init(void) {
epd_init(&epd_board_lilygo_t5_47, &ED097TC2, EPD_LUT_1K);
hl = epd_hl_init(WAVEFORM);
epd47_buffer = epd_hl_get_framebuffer(&hl);
framebuffer = epd47_buffer;

View File

@ -0,0 +1,133 @@
/**
* @file "epd_board.h"
* @brief Board-definitions provided by epdiy.
*/
#pragma once
#include <stdbool.h>
#include <stdint.h>
#include <esp_err.h>
#include <xtensa/core-macros.h>
/**
* State of display control pins.
*/
typedef struct {
bool ep_latch_enable : 1;
bool ep_output_enable : 1;
bool ep_sth : 1;
bool ep_mode : 1;
bool ep_stv : 1;
} epd_ctrl_state_t;
/**
* Operations available on an epdiy board.
*/
typedef struct {
/**
* Initialize the board.
*/
void (*init)(uint32_t epd_row_width);
/**
* Clean up resources and peripherals used by the board.
*/
void (*deinit)(void);
/**
* Set display line state
*/
void (*set_ctrl)(epd_ctrl_state_t*, const epd_ctrl_state_t* const);
/**
* Enable power to the display.
*/
void (*poweron)(epd_ctrl_state_t*);
/**
* Measure VCOM kick-back. Only in v6 & v7 boards!
*/
void (*measure_vcom)(epd_ctrl_state_t* state);
/**
* Disable power to the display.
*/
void (*poweroff)(epd_ctrl_state_t*);
/**
* Set the display common voltage if supported.
*
* Voltage is set as absolute value in millivolts.
* Although VCOM is negative, this function takes a positive (absolute) value.
*/
void (*set_vcom)(int);
/**
* Get the current temperature if supported by the board.
*/
float (*get_temperature)(void);
/**
* Set GPIO direction of the broken-out GPIO extender port,
* if available.
* Setting `make_input` to `1` corresponds to input, `0` corresponds to output.
*/
esp_err_t (*gpio_set_direction)(int pin, bool make_input);
/**
* Get the input level of a GPIO extender pin, if available.
*/
bool (*gpio_read)(int pin);
/**
* Set the output level of a GPIO extender, if available.
*/
esp_err_t (*gpio_write)(int pin, bool value);
} EpdBoardDefinition;
/**
* Get the current board.
*/
const EpdBoardDefinition* epd_current_board();
/**
* Set the board hardware definition. This must be called before epd_init()
*
* The implementation of this method is in board/epd_board.c.
**/
void epd_set_board(const EpdBoardDefinition* board);
/**
* Get the board's current control register state.
*/
epd_ctrl_state_t* epd_ctrl_state();
/**
* Set the display mode pin.
*/
void epd_set_mode(bool state);
/**
* Initialize the control register
*/
void epd_control_reg_init();
/**
* Put the control register into the state of lowest power consumption.
*/
void epd_control_reg_deinit();
// Built in board definitions
extern const EpdBoardDefinition epd_board_lilygo_t5_47;
extern const EpdBoardDefinition epd_board_lilygo_t5_47_touch;
extern const EpdBoardDefinition lilygo_board_s3;
extern const EpdBoardDefinition epd_board_v2_v3;
extern const EpdBoardDefinition epd_board_v4;
extern const EpdBoardDefinition epd_board_v5;
extern const EpdBoardDefinition epd_board_v6;
extern const EpdBoardDefinition epd_board_v7;
extern const EpdBoardDefinition epd_board_v7_raw;
/**
* Helper for short, precise delays.
*/
void epd_busy_delay(uint32_t cycles);

View File

@ -0,0 +1,32 @@
/**
* @file "epd_board_specific.h"
* @brief Board-specific functions that are only conditionally defined.
*/
#pragma once
#include <stdint.h>
/** This is a Lilygo47 specific function
This is a work around a hardware issue with the Lilygo47 epd_poweroff() turns off the epaper
completely however the hardware of the Lilygo47 is different than the official boards. Which means
that on the Lilygo47 this disables power to the touchscreen.
This is a workaround to allow to disable display power but not the touch screen.
On the Lilygo the epd power flag was re-purposed as power enable
for everything. This is a hardware thing.
\warning This workaround may still leave power on to epd and as such may cause other problems such
as grey screen.
Please use epd_poweroff() and epd_deinit() whenever you sleep the system.
The following code can be used to sleep the lilygo and power down the peripherals and wake the
unit on touch. However is should be noted that the touch controller is not powered and as such the
touch coordinates will not be captured. Arduino specific code: \code{.c} epd_poweroff();
epd_deinit();
esp_sleep_enable_ext1_wakeup(GPIO_SEL_13, ESP_EXT1_WAKEUP_ANY_HIGH);
esp_deep_sleep_start();
\endcode
*/
void epd_powerdown_lilygo_t5_47();
void epd_powerdown() __attribute__((deprecated));

View File

@ -0,0 +1,43 @@
#pragma once
#include <stdint.h>
#include "epd_internals.h"
/**
* Display type as "compatibility classes",
* Grouping displays by workarounds needed.
*/
enum EpdDisplayType {
/// A generic EPD, assume default config.
DISPLAY_TYPE_GENERIC,
/// Fast display where we can get away with low hold times.
DISPLAY_TYPE_ED097TC2,
};
typedef struct {
/// Width of the display in pixels.
int width;
/// Height of the display in pixels.
int height;
/// Width of the data bus in bits.
uint8_t bus_width;
/// Speed of the data bus in MHz, if configurable.
/// (Only used by the LCD based renderer in V7+)
int bus_speed;
/// Default waveform to use.
const EpdWaveform* default_waveform;
/// Display type
enum EpdDisplayType display_type;
} EpdDisplay_t;
extern const EpdDisplay_t ED060SCT;
extern const EpdDisplay_t ED060XC3;
extern const EpdDisplay_t ED097OC4;
extern const EpdDisplay_t ED097TC2;
extern const EpdDisplay_t ED133UT2;
extern const EpdDisplay_t ED047TC1;
extern const EpdDisplay_t ED047TC2;
extern const EpdDisplay_t ED078KC1;
extern const EpdDisplay_t ED052TC4;

View File

@ -1,4 +0,0 @@
#pragma once
// This file is only used in the Arduino IDE
// and just includes the IDF component header.
#include "epd_driver/include/epd_driver.h"

View File

@ -1,16 +0,0 @@
set(app_sources "epd_driver.c"
"render.c"
"display_ops.c"
"font.c"
"lut.c"
"builtin_waveforms.c"
"i2s_data_bus.c"
"rmt_pulse.c"
"highlevel.c"
"epd_temperature.c")
idf_component_register(SRCS ${app_sources} INCLUDE_DIRS "include" REQUIRES esp_adc_cal)
set_source_files_properties("lut.c" PROPERTIES COMPILE_OPTIONS -mno-fix-esp32-psram-cache-issue)

View File

@ -1,52 +0,0 @@
menu "E-Paper Driver"
choice EPD_DRIVER_DISPLAY_TYPE
prompt "Display Type"
default EPD_DISPLAY_TYPE_ED097OC4
help
This option sets the display type to drive.
config EPD_DISPLAY_TYPE_ED097OC4
bool "ED097OC4"
config EPD_DISPLAY_TYPE_ED060SC4
bool "ED060SC4"
config EPD_DISPLAY_TYPE_ED060XC3
bool "ED060XC3"
config EPD_DISPLAY_TYPE_ED060SCT
bool "ED060SCT"
config EPD_DISPLAY_TYPE_ED097TC2
bool "ED097TC2"
config EPD_DISPLAY_TYPE_ED133UT2
bool "ED133UT2"
config EPD_DISPLAY_TYPE_ED047TC1
bool "ED047TC1 (LILYGO 4.7 inch)"
config EPD_DISPLAY_TYPE_ED097OC4_LQ
bool "ED097OC4 Low Quality"
endchoice
choice EPD_DRIVER_BOARD_REVISION
prompt "Board / Board Revision"
default EPD_BOARD_REVISION_V2_V3
help
The board revision to compile for.
config EPD_BOARD_REVISION_LILYGO_T5_47
bool "LILYGO T5-4.7 inch e-paper"
config EPD_BOARD_REVISION_V2_V3
bool "epdiy v2 / v3"
config EPD_BOARD_REVISION_V4
bool "epdiy v4"
config EPD_BOARD_REVISION_V5
bool "epdiy v5"
endchoice
endmenu

View File

@ -1,674 +0,0 @@
GNU GENERAL PUBLIC LICENSE
Version 3, 29 June 2007
Copyright (C) 2007 Free Software Foundation, Inc. <https://fsf.org/>
Everyone is permitted to copy and distribute verbatim copies
of this license document, but changing it is not allowed.
Preamble
The GNU General Public License is a free, copyleft license for
software and other kinds of works.
The licenses for most software and other practical works are designed
to take away your freedom to share and change the works. By contrast,
the GNU General Public License is intended to guarantee your freedom to
share and change all versions of a program--to make sure it remains free
software for all its users. We, the Free Software Foundation, use the
GNU General Public License for most of our software; it applies also to
any other work released this way by its authors. You can apply it to
your programs, too.
When we speak of free software, we are referring to freedom, not
price. Our General Public Licenses are designed to make sure that you
have the freedom to distribute copies of free software (and charge for
them if you wish), that you receive source code or can get it if you
want it, that you can change the software or use pieces of it in new
free programs, and that you know you can do these things.
To protect your rights, we need to prevent others from denying you
these rights or asking you to surrender the rights. Therefore, you have
certain responsibilities if you distribute copies of the software, or if
you modify it: responsibilities to respect the freedom of others.
For example, if you distribute copies of such a program, whether
gratis or for a fee, you must pass on to the recipients the same
freedoms that you received. You must make sure that they, too, receive
or can get the source code. And you must show them these terms so they
know their rights.
Developers that use the GNU GPL protect your rights with two steps:
(1) assert copyright on the software, and (2) offer you this License
giving you legal permission to copy, distribute and/or modify it.
For the developers' and authors' protection, the GPL clearly explains
that there is no warranty for this free software. For both users' and
authors' sake, the GPL requires that modified versions be marked as
changed, so that their problems will not be attributed erroneously to
authors of previous versions.
Some devices are designed to deny users access to install or run
modified versions of the software inside them, although the manufacturer
can do so. This is fundamentally incompatible with the aim of
protecting users' freedom to change the software. The systematic
pattern of such abuse occurs in the area of products for individuals to
use, which is precisely where it is most unacceptable. Therefore, we
have designed this version of the GPL to prohibit the practice for those
products. If such problems arise substantially in other domains, we
stand ready to extend this provision to those domains in future versions
of the GPL, as needed to protect the freedom of users.
Finally, every program is threatened constantly by software patents.
States should not allow patents to restrict development and use of
software on general-purpose computers, but in those that do, we wish to
avoid the special danger that patents applied to a free program could
make it effectively proprietary. To prevent this, the GPL assures that
patents cannot be used to render the program non-free.
The precise terms and conditions for copying, distribution and
modification follow.
TERMS AND CONDITIONS
0. Definitions.
"This License" refers to version 3 of the GNU General Public License.
"Copyright" also means copyright-like laws that apply to other kinds of
works, such as semiconductor masks.
"The Program" refers to any copyrightable work licensed under this
License. Each licensee is addressed as "you". "Licensees" and
"recipients" may be individuals or organizations.
To "modify" a work means to copy from or adapt all or part of the work
in a fashion requiring copyright permission, other than the making of an
exact copy. The resulting work is called a "modified version" of the
earlier work or a work "based on" the earlier work.
A "covered work" means either the unmodified Program or a work based
on the Program.
To "propagate" a work means to do anything with it that, without
permission, would make you directly or secondarily liable for
infringement under applicable copyright law, except executing it on a
computer or modifying a private copy. Propagation includes copying,
distribution (with or without modification), making available to the
public, and in some countries other activities as well.
To "convey" a work means any kind of propagation that enables other
parties to make or receive copies. Mere interaction with a user through
a computer network, with no transfer of a copy, is not conveying.
An interactive user interface displays "Appropriate Legal Notices"
to the extent that it includes a convenient and prominently visible
feature that (1) displays an appropriate copyright notice, and (2)
tells the user that there is no warranty for the work (except to the
extent that warranties are provided), that licensees may convey the
work under this License, and how to view a copy of this License. If
the interface presents a list of user commands or options, such as a
menu, a prominent item in the list meets this criterion.
1. Source Code.
The "source code" for a work means the preferred form of the work
for making modifications to it. "Object code" means any non-source
form of a work.
A "Standard Interface" means an interface that either is an official
standard defined by a recognized standards body, or, in the case of
interfaces specified for a particular programming language, one that
is widely used among developers working in that language.
The "System Libraries" of an executable work include anything, other
than the work as a whole, that (a) is included in the normal form of
packaging a Major Component, but which is not part of that Major
Component, and (b) serves only to enable use of the work with that
Major Component, or to implement a Standard Interface for which an
implementation is available to the public in source code form. A
"Major Component", in this context, means a major essential component
(kernel, window system, and so on) of the specific operating system
(if any) on which the executable work runs, or a compiler used to
produce the work, or an object code interpreter used to run it.
The "Corresponding Source" for a work in object code form means all
the source code needed to generate, install, and (for an executable
work) run the object code and to modify the work, including scripts to
control those activities. However, it does not include the work's
System Libraries, or general-purpose tools or generally available free
programs which are used unmodified in performing those activities but
which are not part of the work. For example, Corresponding Source
includes interface definition files associated with source files for
the work, and the source code for shared libraries and dynamically
linked subprograms that the work is specifically designed to require,
such as by intimate data communication or control flow between those
subprograms and other parts of the work.
The Corresponding Source need not include anything that users
can regenerate automatically from other parts of the Corresponding
Source.
The Corresponding Source for a work in source code form is that
same work.
2. Basic Permissions.
All rights granted under this License are granted for the term of
copyright on the Program, and are irrevocable provided the stated
conditions are met. This License explicitly affirms your unlimited
permission to run the unmodified Program. The output from running a
covered work is covered by this License only if the output, given its
content, constitutes a covered work. This License acknowledges your
rights of fair use or other equivalent, as provided by copyright law.
You may make, run and propagate covered works that you do not
convey, without conditions so long as your license otherwise remains
in force. You may convey covered works to others for the sole purpose
of having them make modifications exclusively for you, or provide you
with facilities for running those works, provided that you comply with
the terms of this License in conveying all material for which you do
not control copyright. Those thus making or running the covered works
for you must do so exclusively on your behalf, under your direction
and control, on terms that prohibit them from making any copies of
your copyrighted material outside their relationship with you.
Conveying under any other circumstances is permitted solely under
the conditions stated below. Sublicensing is not allowed; section 10
makes it unnecessary.
3. Protecting Users' Legal Rights From Anti-Circumvention Law.
No covered work shall be deemed part of an effective technological
measure under any applicable law fulfilling obligations under article
11 of the WIPO copyright treaty adopted on 20 December 1996, or
similar laws prohibiting or restricting circumvention of such
measures.
When you convey a covered work, you waive any legal power to forbid
circumvention of technological measures to the extent such circumvention
is effected by exercising rights under this License with respect to
the covered work, and you disclaim any intention to limit operation or
modification of the work as a means of enforcing, against the work's
users, your or third parties' legal rights to forbid circumvention of
technological measures.
4. Conveying Verbatim Copies.
You may convey verbatim copies of the Program's source code as you
receive it, in any medium, provided that you conspicuously and
appropriately publish on each copy an appropriate copyright notice;
keep intact all notices stating that this License and any
non-permissive terms added in accord with section 7 apply to the code;
keep intact all notices of the absence of any warranty; and give all
recipients a copy of this License along with the Program.
You may charge any price or no price for each copy that you convey,
and you may offer support or warranty protection for a fee.
5. Conveying Modified Source Versions.
You may convey a work based on the Program, or the modifications to
produce it from the Program, in the form of source code under the
terms of section 4, provided that you also meet all of these conditions:
a) The work must carry prominent notices stating that you modified
it, and giving a relevant date.
b) The work must carry prominent notices stating that it is
released under this License and any conditions added under section
7. This requirement modifies the requirement in section 4 to
"keep intact all notices".
c) You must license the entire work, as a whole, under this
License to anyone who comes into possession of a copy. This
License will therefore apply, along with any applicable section 7
additional terms, to the whole of the work, and all its parts,
regardless of how they are packaged. This License gives no
permission to license the work in any other way, but it does not
invalidate such permission if you have separately received it.
d) If the work has interactive user interfaces, each must display
Appropriate Legal Notices; however, if the Program has interactive
interfaces that do not display Appropriate Legal Notices, your
work need not make them do so.
A compilation of a covered work with other separate and independent
works, which are not by their nature extensions of the covered work,
and which are not combined with it such as to form a larger program,
in or on a volume of a storage or distribution medium, is called an
"aggregate" if the compilation and its resulting copyright are not
used to limit the access or legal rights of the compilation's users
beyond what the individual works permit. Inclusion of a covered work
in an aggregate does not cause this License to apply to the other
parts of the aggregate.
6. Conveying Non-Source Forms.
You may convey a covered work in object code form under the terms
of sections 4 and 5, provided that you also convey the
machine-readable Corresponding Source under the terms of this License,
in one of these ways:
a) Convey the object code in, or embodied in, a physical product
(including a physical distribution medium), accompanied by the
Corresponding Source fixed on a durable physical medium
customarily used for software interchange.
b) Convey the object code in, or embodied in, a physical product
(including a physical distribution medium), accompanied by a
written offer, valid for at least three years and valid for as
long as you offer spare parts or customer support for that product
model, to give anyone who possesses the object code either (1) a
copy of the Corresponding Source for all the software in the
product that is covered by this License, on a durable physical
medium customarily used for software interchange, for a price no
more than your reasonable cost of physically performing this
conveying of source, or (2) access to copy the
Corresponding Source from a network server at no charge.
c) Convey individual copies of the object code with a copy of the
written offer to provide the Corresponding Source. This
alternative is allowed only occasionally and noncommercially, and
only if you received the object code with such an offer, in accord
with subsection 6b.
d) Convey the object code by offering access from a designated
place (gratis or for a charge), and offer equivalent access to the
Corresponding Source in the same way through the same place at no
further charge. You need not require recipients to copy the
Corresponding Source along with the object code. If the place to
copy the object code is a network server, the Corresponding Source
may be on a different server (operated by you or a third party)
that supports equivalent copying facilities, provided you maintain
clear directions next to the object code saying where to find the
Corresponding Source. Regardless of what server hosts the
Corresponding Source, you remain obligated to ensure that it is
available for as long as needed to satisfy these requirements.
e) Convey the object code using peer-to-peer transmission, provided
you inform other peers where the object code and Corresponding
Source of the work are being offered to the general public at no
charge under subsection 6d.
A separable portion of the object code, whose source code is excluded
from the Corresponding Source as a System Library, need not be
included in conveying the object code work.
A "User Product" is either (1) a "consumer product", which means any
tangible personal property which is normally used for personal, family,
or household purposes, or (2) anything designed or sold for incorporation
into a dwelling. In determining whether a product is a consumer product,
doubtful cases shall be resolved in favor of coverage. For a particular
product received by a particular user, "normally used" refers to a
typical or common use of that class of product, regardless of the status
of the particular user or of the way in which the particular user
actually uses, or expects or is expected to use, the product. A product
is a consumer product regardless of whether the product has substantial
commercial, industrial or non-consumer uses, unless such uses represent
the only significant mode of use of the product.
"Installation Information" for a User Product means any methods,
procedures, authorization keys, or other information required to install
and execute modified versions of a covered work in that User Product from
a modified version of its Corresponding Source. The information must
suffice to ensure that the continued functioning of the modified object
code is in no case prevented or interfered with solely because
modification has been made.
If you convey an object code work under this section in, or with, or
specifically for use in, a User Product, and the conveying occurs as
part of a transaction in which the right of possession and use of the
User Product is transferred to the recipient in perpetuity or for a
fixed term (regardless of how the transaction is characterized), the
Corresponding Source conveyed under this section must be accompanied
by the Installation Information. But this requirement does not apply
if neither you nor any third party retains the ability to install
modified object code on the User Product (for example, the work has
been installed in ROM).
The requirement to provide Installation Information does not include a
requirement to continue to provide support service, warranty, or updates
for a work that has been modified or installed by the recipient, or for
the User Product in which it has been modified or installed. Access to a
network may be denied when the modification itself materially and
adversely affects the operation of the network or violates the rules and
protocols for communication across the network.
Corresponding Source conveyed, and Installation Information provided,
in accord with this section must be in a format that is publicly
documented (and with an implementation available to the public in
source code form), and must require no special password or key for
unpacking, reading or copying.
7. Additional Terms.
"Additional permissions" are terms that supplement the terms of this
License by making exceptions from one or more of its conditions.
Additional permissions that are applicable to the entire Program shall
be treated as though they were included in this License, to the extent
that they are valid under applicable law. If additional permissions
apply only to part of the Program, that part may be used separately
under those permissions, but the entire Program remains governed by
this License without regard to the additional permissions.
When you convey a copy of a covered work, you may at your option
remove any additional permissions from that copy, or from any part of
it. (Additional permissions may be written to require their own
removal in certain cases when you modify the work.) You may place
additional permissions on material, added by you to a covered work,
for which you have or can give appropriate copyright permission.
Notwithstanding any other provision of this License, for material you
add to a covered work, you may (if authorized by the copyright holders of
that material) supplement the terms of this License with terms:
a) Disclaiming warranty or limiting liability differently from the
terms of sections 15 and 16 of this License; or
b) Requiring preservation of specified reasonable legal notices or
author attributions in that material or in the Appropriate Legal
Notices displayed by works containing it; or
c) Prohibiting misrepresentation of the origin of that material, or
requiring that modified versions of such material be marked in
reasonable ways as different from the original version; or
d) Limiting the use for publicity purposes of names of licensors or
authors of the material; or
e) Declining to grant rights under trademark law for use of some
trade names, trademarks, or service marks; or
f) Requiring indemnification of licensors and authors of that
material by anyone who conveys the material (or modified versions of
it) with contractual assumptions of liability to the recipient, for
any liability that these contractual assumptions directly impose on
those licensors and authors.
All other non-permissive additional terms are considered "further
restrictions" within the meaning of section 10. If the Program as you
received it, or any part of it, contains a notice stating that it is
governed by this License along with a term that is a further
restriction, you may remove that term. If a license document contains
a further restriction but permits relicensing or conveying under this
License, you may add to a covered work material governed by the terms
of that license document, provided that the further restriction does
not survive such relicensing or conveying.
If you add terms to a covered work in accord with this section, you
must place, in the relevant source files, a statement of the
additional terms that apply to those files, or a notice indicating
where to find the applicable terms.
Additional terms, permissive or non-permissive, may be stated in the
form of a separately written license, or stated as exceptions;
the above requirements apply either way.
8. Termination.
You may not propagate or modify a covered work except as expressly
provided under this License. Any attempt otherwise to propagate or
modify it is void, and will automatically terminate your rights under
this License (including any patent licenses granted under the third
paragraph of section 11).
However, if you cease all violation of this License, then your
license from a particular copyright holder is reinstated (a)
provisionally, unless and until the copyright holder explicitly and
finally terminates your license, and (b) permanently, if the copyright
holder fails to notify you of the violation by some reasonable means
prior to 60 days after the cessation.
Moreover, your license from a particular copyright holder is
reinstated permanently if the copyright holder notifies you of the
violation by some reasonable means, this is the first time you have
received notice of violation of this License (for any work) from that
copyright holder, and you cure the violation prior to 30 days after
your receipt of the notice.
Termination of your rights under this section does not terminate the
licenses of parties who have received copies or rights from you under
this License. If your rights have been terminated and not permanently
reinstated, you do not qualify to receive new licenses for the same
material under section 10.
9. Acceptance Not Required for Having Copies.
You are not required to accept this License in order to receive or
run a copy of the Program. Ancillary propagation of a covered work
occurring solely as a consequence of using peer-to-peer transmission
to receive a copy likewise does not require acceptance. However,
nothing other than this License grants you permission to propagate or
modify any covered work. These actions infringe copyright if you do
not accept this License. Therefore, by modifying or propagating a
covered work, you indicate your acceptance of this License to do so.
10. Automatic Licensing of Downstream Recipients.
Each time you convey a covered work, the recipient automatically
receives a license from the original licensors, to run, modify and
propagate that work, subject to this License. You are not responsible
for enforcing compliance by third parties with this License.
An "entity transaction" is a transaction transferring control of an
organization, or substantially all assets of one, or subdividing an
organization, or merging organizations. If propagation of a covered
work results from an entity transaction, each party to that
transaction who receives a copy of the work also receives whatever
licenses to the work the party's predecessor in interest had or could
give under the previous paragraph, plus a right to possession of the
Corresponding Source of the work from the predecessor in interest, if
the predecessor has it or can get it with reasonable efforts.
You may not impose any further restrictions on the exercise of the
rights granted or affirmed under this License. For example, you may
not impose a license fee, royalty, or other charge for exercise of
rights granted under this License, and you may not initiate litigation
(including a cross-claim or counterclaim in a lawsuit) alleging that
any patent claim is infringed by making, using, selling, offering for
sale, or importing the Program or any portion of it.
11. Patents.
A "contributor" is a copyright holder who authorizes use under this
License of the Program or a work on which the Program is based. The
work thus licensed is called the contributor's "contributor version".
A contributor's "essential patent claims" are all patent claims
owned or controlled by the contributor, whether already acquired or
hereafter acquired, that would be infringed by some manner, permitted
by this License, of making, using, or selling its contributor version,
but do not include claims that would be infringed only as a
consequence of further modification of the contributor version. For
purposes of this definition, "control" includes the right to grant
patent sublicenses in a manner consistent with the requirements of
this License.
Each contributor grants you a non-exclusive, worldwide, royalty-free
patent license under the contributor's essential patent claims, to
make, use, sell, offer for sale, import and otherwise run, modify and
propagate the contents of its contributor version.
In the following three paragraphs, a "patent license" is any express
agreement or commitment, however denominated, not to enforce a patent
(such as an express permission to practice a patent or covenant not to
sue for patent infringement). To "grant" such a patent license to a
party means to make such an agreement or commitment not to enforce a
patent against the party.
If you convey a covered work, knowingly relying on a patent license,
and the Corresponding Source of the work is not available for anyone
to copy, free of charge and under the terms of this License, through a
publicly available network server or other readily accessible means,
then you must either (1) cause the Corresponding Source to be so
available, or (2) arrange to deprive yourself of the benefit of the
patent license for this particular work, or (3) arrange, in a manner
consistent with the requirements of this License, to extend the patent
license to downstream recipients. "Knowingly relying" means you have
actual knowledge that, but for the patent license, your conveying the
covered work in a country, or your recipient's use of the covered work
in a country, would infringe one or more identifiable patents in that
country that you have reason to believe are valid.
If, pursuant to or in connection with a single transaction or
arrangement, you convey, or propagate by procuring conveyance of, a
covered work, and grant a patent license to some of the parties
receiving the covered work authorizing them to use, propagate, modify
or convey a specific copy of the covered work, then the patent license
you grant is automatically extended to all recipients of the covered
work and works based on it.
A patent license is "discriminatory" if it does not include within
the scope of its coverage, prohibits the exercise of, or is
conditioned on the non-exercise of one or more of the rights that are
specifically granted under this License. You may not convey a covered
work if you are a party to an arrangement with a third party that is
in the business of distributing software, under which you make payment
to the third party based on the extent of your activity of conveying
the work, and under which the third party grants, to any of the
parties who would receive the covered work from you, a discriminatory
patent license (a) in connection with copies of the covered work
conveyed by you (or copies made from those copies), or (b) primarily
for and in connection with specific products or compilations that
contain the covered work, unless you entered into that arrangement,
or that patent license was granted, prior to 28 March 2007.
Nothing in this License shall be construed as excluding or limiting
any implied license or other defenses to infringement that may
otherwise be available to you under applicable patent law.
12. No Surrender of Others' Freedom.
If conditions are imposed on you (whether by court order, agreement or
otherwise) that contradict the conditions of this License, they do not
excuse you from the conditions of this License. If you cannot convey a
covered work so as to satisfy simultaneously your obligations under this
License and any other pertinent obligations, then as a consequence you may
not convey it at all. For example, if you agree to terms that obligate you
to collect a royalty for further conveying from those to whom you convey
the Program, the only way you could satisfy both those terms and this
License would be to refrain entirely from conveying the Program.
13. Use with the GNU Affero General Public License.
Notwithstanding any other provision of this License, you have
permission to link or combine any covered work with a work licensed
under version 3 of the GNU Affero General Public License into a single
combined work, and to convey the resulting work. The terms of this
License will continue to apply to the part which is the covered work,
but the special requirements of the GNU Affero General Public License,
section 13, concerning interaction through a network will apply to the
combination as such.
14. Revised Versions of this License.
The Free Software Foundation may publish revised and/or new versions of
the GNU General Public License from time to time. Such new versions will
be similar in spirit to the present version, but may differ in detail to
address new problems or concerns.
Each version is given a distinguishing version number. If the
Program specifies that a certain numbered version of the GNU General
Public License "or any later version" applies to it, you have the
option of following the terms and conditions either of that numbered
version or of any later version published by the Free Software
Foundation. If the Program does not specify a version number of the
GNU General Public License, you may choose any version ever published
by the Free Software Foundation.
If the Program specifies that a proxy can decide which future
versions of the GNU General Public License can be used, that proxy's
public statement of acceptance of a version permanently authorizes you
to choose that version for the Program.
Later license versions may give you additional or different
permissions. However, no additional obligations are imposed on any
author or copyright holder as a result of your choosing to follow a
later version.
15. Disclaimer of Warranty.
THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
16. Limitation of Liability.
IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
SUCH DAMAGES.
17. Interpretation of Sections 15 and 16.
If the disclaimer of warranty and limitation of liability provided
above cannot be given local legal effect according to their terms,
reviewing courts shall apply local law that most closely approximates
an absolute waiver of all civil liability in connection with the
Program, unless a warranty or assumption of liability accompanies a
copy of the Program in return for a fee.
END OF TERMS AND CONDITIONS
How to Apply These Terms to Your New Programs
If you develop a new program, and you want it to be of the greatest
possible use to the public, the best way to achieve this is to make it
free software which everyone can redistribute and change under these terms.
To do so, attach the following notices to the program. It is safest
to attach them to the start of each source file to most effectively
state the exclusion of warranty; and each file should have at least
the "copyright" line and a pointer to where the full notice is found.
<one line to give the program's name and a brief idea of what it does.>
Copyright (C) <year> <name of author>
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
Also add information on how to contact you by electronic and paper mail.
If the program does terminal interaction, make it output a short
notice like this when it starts in an interactive mode:
<program> Copyright (C) <year> <name of author>
This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
This is free software, and you are welcome to redistribute it
under certain conditions; type `show c' for details.
The hypothetical commands `show w' and `show c' should show the appropriate
parts of the General Public License. Of course, your program's commands
might be different; for a GUI interface, you would use an "about box".
You should also get your employer (if you work as a programmer) or school,
if any, to sign a "copyright disclaimer" for the program, if necessary.
For more information on this, and how to apply and follow the GNU GPL, see
<https://www.gnu.org/licenses/>.
The GNU General Public License does not permit incorporating your program
into proprietary programs. If your program is a subroutine library, you
may consider it more useful to permit linking proprietary applications with
the library. If this is what you want to do, use the GNU Lesser General
Public License instead of this License. But first, please read
<https://www.gnu.org/licenses/why-not-lgpl.html>.

View File

@ -1,84 +0,0 @@
#include "display_ops.h"
#include <stdint.h>
typedef struct {
bool ep_latch_enable : 1;
bool power_disable : 1;
bool pos_power_enable : 1;
bool neg_power_enable : 1;
bool ep_stv : 1;
bool ep_scan_direction : 1;
bool ep_mode : 1;
bool ep_output_enable : 1;
} epd_config_register_t;
static void config_reg_init(epd_config_register_t *cfg) {
cfg->ep_latch_enable = false;
cfg->power_disable = true;
cfg->pos_power_enable = false;
cfg->neg_power_enable = false;
cfg->ep_stv = true;
cfg->ep_scan_direction = true;
cfg->ep_mode = false;
cfg->ep_output_enable = false;
}
inline static void IRAM_ATTR push_cfg_bit(bool bit);
void IRAM_ATTR busy_delay(uint32_t cycles);
inline static void fast_gpio_set_hi(gpio_num_t gpio_num);
inline static void fast_gpio_set_lo(gpio_num_t gpio_num);
static void IRAM_ATTR push_cfg(const epd_config_register_t *cfg) {
fast_gpio_set_lo(CFG_STR);
// push config bits in reverse order
push_cfg_bit(cfg->ep_output_enable);
push_cfg_bit(cfg->ep_mode);
push_cfg_bit(cfg->ep_scan_direction);
push_cfg_bit(cfg->ep_stv);
push_cfg_bit(cfg->neg_power_enable);
push_cfg_bit(cfg->pos_power_enable);
push_cfg_bit(cfg->power_disable);
push_cfg_bit(cfg->ep_latch_enable);
fast_gpio_set_hi(CFG_STR);
}
static void cfg_poweron(epd_config_register_t *cfg) {
#if defined(CONFIG_EPD_BOARD_REVISION_LILYGO_T5_47)
// This was re-purposed as power enable.
cfg->ep_scan_direction = true;
#endif
// POWERON
cfg->power_disable = false;
push_cfg(cfg);
busy_delay(100 * 240);
cfg->neg_power_enable = true;
push_cfg(cfg);
busy_delay(500 * 240);
cfg->pos_power_enable = true;
push_cfg(cfg);
busy_delay(100 * 240);
cfg->ep_stv = true;
push_cfg(cfg);
fast_gpio_set_hi(STH);
// END POWERON
}
static void cfg_poweroff(epd_config_register_t *cfg) {
#if defined(CONFIG_EPD_BOARD_REVISION_LILYGO_T5_47)
// This was re-purposed as power enable.
cfg->ep_scan_direction = false;
#endif
// POWEROFF
cfg->pos_power_enable = false;
push_cfg(cfg);
busy_delay(10 * 240);
cfg->neg_power_enable = false;
push_cfg(cfg);
busy_delay(100 * 240);
cfg->power_disable = true;
push_cfg(cfg);
// END POWEROFF
}

View File

@ -1,108 +0,0 @@
#include "display_ops.h"
typedef struct {
#if defined(CONFIG_EPD_BOARD_REVISION_V5)
bool power_enable : 1;
#else
bool power_disable : 1;
#endif
bool power_enable_vpos : 1;
bool power_enable_vneg : 1;
bool power_enable_gl : 1;
bool ep_stv : 1;
bool power_enable_gh : 1;
bool ep_mode : 1;
bool ep_output_enable : 1;
} epd_config_register_t;
static void config_reg_init(epd_config_register_t *cfg) {
#if defined(CONFIG_EPD_BOARD_REVISION_V5)
cfg->power_enable = false;
#else
cfg->power_disable = true;
#endif
cfg->power_enable_vpos = false;
cfg->power_enable_vneg = false;
cfg->power_enable_gl = false;
cfg->ep_stv = true;
cfg->power_enable_gh = false;
cfg->ep_mode = false;
cfg->ep_output_enable = false;
}
inline static void IRAM_ATTR push_cfg_bit(bool bit);
void IRAM_ATTR busy_delay(uint32_t cycles);
inline static void fast_gpio_set_hi(gpio_num_t gpio_num);
inline static void fast_gpio_set_lo(gpio_num_t gpio_num);
static void IRAM_ATTR push_cfg(const epd_config_register_t *cfg) {
fast_gpio_set_lo(CFG_STR);
// push config bits in reverse order
push_cfg_bit(cfg->ep_output_enable);
push_cfg_bit(cfg->ep_mode);
push_cfg_bit(cfg->power_enable_gh);
push_cfg_bit(cfg->ep_stv);
push_cfg_bit(cfg->power_enable_gl);
push_cfg_bit(cfg->power_enable_vneg);
push_cfg_bit(cfg->power_enable_vpos);
#if defined(CONFIG_EPD_BOARD_REVISION_V5)
push_cfg_bit(cfg->power_enable);
#else
push_cfg_bit(cfg->power_disable);
#endif
fast_gpio_set_hi(CFG_STR);
fast_gpio_set_lo(CFG_STR);
}
static void cfg_poweron(epd_config_register_t *cfg) {
// POWERON
#if defined(CONFIG_EPD_BOARD_REVISION_V5)
cfg->power_enable = true;
#else
cfg->power_disable = false;
#endif
push_cfg(cfg);
busy_delay(100 * 240);
cfg->power_enable_gl = true;
push_cfg(cfg);
busy_delay(500 * 240);
cfg->power_enable_vneg = true;
push_cfg(cfg);
busy_delay(500 * 240);
cfg->power_enable_gh = true;
push_cfg(cfg);
busy_delay(500 * 240);
cfg->power_enable_vpos = true;
push_cfg(cfg);
busy_delay(100 * 240);
cfg->ep_stv = true;
push_cfg(cfg);
fast_gpio_set_hi(STH);
// END POWERON
}
static void cfg_poweroff(epd_config_register_t *cfg) {
// POWEROFF
cfg->power_enable_gh = false;
cfg->power_enable_vpos = false;
push_cfg(cfg);
busy_delay(10 * 240);
cfg->power_enable_gl = false;
cfg->power_enable_vneg = false;
push_cfg(cfg);
busy_delay(100 * 240);
cfg->ep_stv = false;
cfg->ep_output_enable = false;
cfg->ep_mode = false;
#if defined(CONFIG_EPD_BOARD_REVISION_V5)
cfg->power_enable = false;
#else
cfg->power_disable = true;
#endif
push_cfg(cfg);
// END POWEROFF
}

View File

@ -1,195 +0,0 @@
#include "display_ops.h"
#include "esp_timer.h"
#include "esp_log.h"
#include "i2s_data_bus.h"
#include "rmt_pulse.h"
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "xtensa/core-macros.h"
#if defined(CONFIG_EPD_BOARD_REVISION_V2_V3) || defined(CONFIG_EPD_BOARD_REVISION_LILYGO_T5_47)
#include "config_reg_v2.h"
#else
#if defined(CONFIG_EPD_BOARD_REVISION_V4) || defined(CONFIG_EPD_BOARD_REVISION_V5)
#include "config_reg_v4.h"
#else
#error "unknown revision"
#endif
#endif
static epd_config_register_t config_reg;
/*
* Write bits directly using the registers.
* Won't work for some pins (>= 32).
*/
inline static void fast_gpio_set_hi(gpio_num_t gpio_num) {
GPIO.out_w1ts = (1 << gpio_num);
}
inline static void fast_gpio_set_lo(gpio_num_t gpio_num) {
GPIO.out_w1tc = (1 << gpio_num);
}
void IRAM_ATTR busy_delay(uint32_t cycles) {
volatile unsigned long counts = XTHAL_GET_CCOUNT() + cycles;
while (XTHAL_GET_CCOUNT() < counts) {
};
}
inline static void IRAM_ATTR push_cfg_bit(bool bit) {
gpio_set_level(CFG_CLK, 0);
if (bit) {
gpio_set_level(CFG_DATA, 1);
} else {
gpio_set_level(CFG_DATA, 0);
}
gpio_set_level(CFG_CLK, 1);
}
void epd_base_init(uint32_t epd_row_width) {
config_reg_init(&config_reg);
/* Power Control Output/Off */
PIN_FUNC_SELECT(GPIO_PIN_MUX_REG[CFG_DATA], PIN_FUNC_GPIO);
PIN_FUNC_SELECT(GPIO_PIN_MUX_REG[CFG_CLK], PIN_FUNC_GPIO);
PIN_FUNC_SELECT(GPIO_PIN_MUX_REG[CFG_STR], PIN_FUNC_GPIO);
gpio_set_direction(CFG_DATA, GPIO_MODE_OUTPUT);
gpio_set_direction(CFG_CLK, GPIO_MODE_OUTPUT);
gpio_set_direction(CFG_STR, GPIO_MODE_OUTPUT);
#if defined(CONFIG_EPD_BOARD_REVISION_V4) || defined(CONFIG_EPD_BOARD_REVISION_V5)
// use latch pin as GPIO
PIN_FUNC_SELECT(GPIO_PIN_MUX_REG[V4_LATCH_ENABLE], PIN_FUNC_GPIO);
ESP_ERROR_CHECK(gpio_set_direction(V4_LATCH_ENABLE, GPIO_MODE_OUTPUT));
gpio_set_level(V4_LATCH_ENABLE, 0);
#endif
fast_gpio_set_lo(CFG_STR);
push_cfg(&config_reg);
// Setup I2S
i2s_bus_config i2s_config;
// add an offset off dummy bytes to allow for enough timing headroom
i2s_config.epd_row_width = epd_row_width + 32;
i2s_config.clock = CKH;
i2s_config.start_pulse = STH;
i2s_config.data_0 = D0;
i2s_config.data_1 = D1;
i2s_config.data_2 = D2;
i2s_config.data_3 = D3;
i2s_config.data_4 = D4;
i2s_config.data_5 = D5;
i2s_config.data_6 = D6;
i2s_config.data_7 = D7;
i2s_bus_init(&i2s_config);
rmt_pulse_init(CKV);
}
void epd_poweron() { cfg_poweron(&config_reg); }
void epd_poweroff() { cfg_poweroff(&config_reg); }
void epd_base_deinit(){
epd_poweroff();
i2s_deinit();
}
void epd_start_frame() {
while (i2s_is_busy() || rmt_busy()) {
};
config_reg.ep_mode = true;
push_cfg(&config_reg);
pulse_ckv_us(1, 1, true);
// This is very timing-sensitive!
config_reg.ep_stv = false;
push_cfg(&config_reg);
//busy_delay(240);
pulse_ckv_us(100, 100, false);
config_reg.ep_stv = true;
push_cfg(&config_reg);
//pulse_ckv_us(0, 10, true);
pulse_ckv_us(1, 1, true);
pulse_ckv_us(1, 1, true);
pulse_ckv_us(1, 1, true);
pulse_ckv_us(1, 1, true);
config_reg.ep_output_enable = true;
push_cfg(&config_reg);
}
static inline void latch_row() {
#if defined(CONFIG_EPD_BOARD_REVISION_V2_V3) || defined(CONFIG_EPD_BOARD_REVISION_LILYGO_T5_47)
config_reg.ep_latch_enable = true;
push_cfg(&config_reg);
config_reg.ep_latch_enable = false;
push_cfg(&config_reg);
#else
#if defined(CONFIG_EPD_BOARD_REVISION_V4) || defined(CONFIG_EPD_BOARD_REVISION_V5)
fast_gpio_set_hi(V4_LATCH_ENABLE);
fast_gpio_set_lo(V4_LATCH_ENABLE);
#else
#error "unknown revision"
#endif
#endif
}
void IRAM_ATTR epd_skip() {
#if defined(CONFIG_EPD_DISPLAY_TYPE_ED097TC2) || \
defined(CONFIG_EPD_DISPLAY_TYPE_ED133UT2)
pulse_ckv_ticks(5, 5, false);
#else
// According to the spec, the OC4 maximum CKV frequency is 200kHz.
pulse_ckv_ticks(45, 5, false);
#endif
}
void IRAM_ATTR epd_output_row(uint32_t output_time_dus) {
while (i2s_is_busy() || rmt_busy()) {
};
fast_gpio_set_hi(STH);
latch_row();
#if defined(CONFIG_EPD_DISPLAY_TYPE_ED097TC2) || \
defined(CONFIG_EPD_DISPLAY_TYPE_ED133UT2)
pulse_ckv_ticks(output_time_dus, 1, false);
#else
pulse_ckv_ticks(output_time_dus, 50, false);
#endif
i2s_start_line_output();
i2s_switch_buffer();
}
void epd_end_frame() {
config_reg.ep_stv = false;
push_cfg(&config_reg);
pulse_ckv_us(1, 1, true);
pulse_ckv_us(1, 1, true);
pulse_ckv_us(1, 1, true);
pulse_ckv_us(1, 1, true);
pulse_ckv_us(1, 1, true);
config_reg.ep_mode = false;
push_cfg(&config_reg);
pulse_ckv_us(0, 10, true);
config_reg.ep_output_enable = false;
push_cfg(&config_reg);
pulse_ckv_us(1, 1, true);
pulse_ckv_us(1, 1, true);
pulse_ckv_us(1, 1, true);
}
void IRAM_ATTR epd_switch_buffer() { i2s_switch_buffer(); }
uint8_t IRAM_ATTR *epd_get_current_buffer() {
return (uint8_t *)i2s_get_current_buffer();
};

View File

@ -1,111 +0,0 @@
#pragma once
#include "driver/gpio.h"
#define CONFIG_EPD_BOARD_REVISION_LILYGO_T5_47
#if defined(CONFIG_EPD_BOARD_REVISION_V5)
#define D7 GPIO_NUM_23
#define D6 GPIO_NUM_22
#define D5 GPIO_NUM_21
#define D4 GPIO_NUM_19
#define D3 GPIO_NUM_18
#define D2 GPIO_NUM_5
#define D1 GPIO_NUM_4
#define D0 GPIO_NUM_25
/* Config Reggister Control */
#define CFG_DATA GPIO_NUM_33
#define CFG_CLK GPIO_NUM_32
#define CFG_STR GPIO_NUM_0
/* Control Lines */
#define CKV GPIO_NUM_26
#define STH GPIO_NUM_27
#define V4_LATCH_ENABLE GPIO_NUM_2
/* Edges */
#define CKH GPIO_NUM_15
#else
#define D7 GPIO_NUM_22
#define D6 GPIO_NUM_21
#define D5 GPIO_NUM_27
#define D4 GPIO_NUM_2
#if defined(CONFIG_EPD_BOARD_REVISION_LILYGO_T5_47)
#define D3 GPIO_NUM_19
#else
#define D3 GPIO_NUM_0
#endif
#define D2 GPIO_NUM_4
#define D1 GPIO_NUM_32
#define D0 GPIO_NUM_33
#define CFG_DATA GPIO_NUM_23
#define CFG_CLK GPIO_NUM_18
#if defined(CONFIG_EPD_BOARD_REVISION_LILYGO_T5_47)
#define CFG_STR GPIO_NUM_0
#else
#define CFG_STR GPIO_NUM_19
#endif
/* Control Lines */
#define CKV GPIO_NUM_25
#define STH GPIO_NUM_26
#define V4_LATCH_ENABLE GPIO_NUM_15
/* Edges */
#define CKH GPIO_NUM_5
#endif
void epd_base_init(uint32_t epd_row_width);
void epd_base_deinit();
void epd_poweron();
void epd_poweroff();
/**
* Start a draw cycle.
*/
void epd_start_frame();
/**
* End a draw cycle.
*/
void epd_end_frame();
/**
* Waits until all previously submitted data has been written.
* Then, the following operations are initiated:
*
* - Previously submitted data is latched to the output register.
* - The RMT peripheral is set up to pulse the vertical (gate) driver for
* `output_time_dus` / 10 microseconds.
* - The I2S peripheral starts transmission of the current buffer to
* the source driver.
* - The line buffers are switched.
*
* This sequence of operations allows for pipelining data preparation and
* transfer, reducing total refresh times.
*/
void IRAM_ATTR epd_output_row(uint32_t output_time_dus);
/** Skip a row without writing to it. */
void IRAM_ATTR epd_skip();
/**
* Get the currently writable line buffer.
*/
uint8_t IRAM_ATTR *epd_get_current_buffer();
/**
* Switches front and back line buffer.
* If the switched-to line buffer is currently in use,
* this function blocks until transmission is done.
*/
void IRAM_ATTR epd_switch_buffer();

View File

@ -1,344 +0,0 @@
#include "epd_driver.h"
#include "epd_temperature.h"
#include "esp_assert.h"
#include "esp_heap_caps.h"
#include "esp_log.h"
#include "esp_types.h"
#ifndef _swap_int
#define _swap_int(a, b) \
{ \
int t = a; \
a = b; \
b = t; \
}
#endif
EpdRect epd_full_screen() {
EpdRect area = {.x = 0, .y = 0, .width = EPD_WIDTH, .height = EPD_HEIGHT};
return area;
}
void epd_clear() { epd_clear_area(epd_full_screen()); }
void epd_draw_hline(int x, int y, int length, uint8_t color,
uint8_t *framebuffer) {
for (int i = 0; i < length; i++) {
int xx = x + i;
epd_draw_pixel(xx, y, color, framebuffer);
}
}
void epd_draw_vline(int x, int y, int length, uint8_t color,
uint8_t *framebuffer) {
for (int i = 0; i < length; i++) {
int yy = y + i;
epd_draw_pixel(x, yy, color, framebuffer);
}
}
void epd_draw_pixel(int x, int y, uint8_t color, uint8_t *framebuffer) {
if (x < 0 || x >= EPD_WIDTH) {
return;
}
if (y < 0 || y >= EPD_HEIGHT) {
return;
}
uint8_t *buf_ptr = &framebuffer[y * EPD_WIDTH / 2 + x / 2];
if (x % 2) {
*buf_ptr = (*buf_ptr & 0x0F) | (color & 0xF0);
} else {
*buf_ptr = (*buf_ptr & 0xF0) | (color >> 4);
}
}
void epd_draw_circle(int x0, int y0, int r, uint8_t color,
uint8_t *framebuffer) {
int f = 1 - r;
int ddF_x = 1;
int ddF_y = -2 * r;
int x = 0;
int y = r;
epd_draw_pixel(x0, y0 + r, color, framebuffer);
epd_draw_pixel(x0, y0 - r, color, framebuffer);
epd_draw_pixel(x0 + r, y0, color, framebuffer);
epd_draw_pixel(x0 - r, y0, color, framebuffer);
while (x < y) {
if (f >= 0) {
y--;
ddF_y += 2;
f += ddF_y;
}
x++;
ddF_x += 2;
f += ddF_x;
epd_draw_pixel(x0 + x, y0 + y, color, framebuffer);
epd_draw_pixel(x0 - x, y0 + y, color, framebuffer);
epd_draw_pixel(x0 + x, y0 - y, color, framebuffer);
epd_draw_pixel(x0 - x, y0 - y, color, framebuffer);
epd_draw_pixel(x0 + y, y0 + x, color, framebuffer);
epd_draw_pixel(x0 - y, y0 + x, color, framebuffer);
epd_draw_pixel(x0 + y, y0 - x, color, framebuffer);
epd_draw_pixel(x0 - y, y0 - x, color, framebuffer);
}
}
void epd_fill_circle(int x0, int y0, int r, uint8_t color,
uint8_t *framebuffer) {
epd_draw_vline(x0, y0 - r, 2 * r + 1, color, framebuffer);
epd_fill_circle_helper(x0, y0, r, 3, 0, color, framebuffer);
}
void epd_fill_circle_helper(int x0, int y0, int r, int corners, int delta,
uint8_t color, uint8_t *framebuffer) {
int f = 1 - r;
int ddF_x = 1;
int ddF_y = -2 * r;
int x = 0;
int y = r;
int px = x;
int py = y;
delta++; // Avoid some +1's in the loop
while (x < y) {
if (f >= 0) {
y--;
ddF_y += 2;
f += ddF_y;
}
x++;
ddF_x += 2;
f += ddF_x;
// These checks avoid double-drawing certain lines, important
// for the SSD1306 library which has an INVERT drawing mode.
if (x < (y + 1)) {
if (corners & 1)
epd_draw_vline(x0 + x, y0 - y, 2 * y + delta, color, framebuffer);
if (corners & 2)
epd_draw_vline(x0 - x, y0 - y, 2 * y + delta, color, framebuffer);
}
if (y != py) {
if (corners & 1)
epd_draw_vline(x0 + py, y0 - px, 2 * px + delta, color, framebuffer);
if (corners & 2)
epd_draw_vline(x0 - py, y0 - px, 2 * px + delta, color, framebuffer);
py = y;
}
px = x;
}
}
void epd_draw_rect(EpdRect rect, uint8_t color,
uint8_t *framebuffer) {
int x = rect.x; int y = rect.y; int w = rect.width; int h = rect.height;
epd_draw_hline(x, y, w, color, framebuffer);
epd_draw_hline(x, y + h - 1, w, color, framebuffer);
epd_draw_vline(x, y, h, color, framebuffer);
epd_draw_vline(x + w - 1, y, h, color, framebuffer);
}
void epd_fill_rect(EpdRect rect, uint8_t color, uint8_t *framebuffer) {
int x = rect.x; int y = rect.y; int w = rect.width; int h = rect.height;
for (int i = y; i < y + h; i++) {
epd_draw_hline(x, i, w, color, framebuffer);
}
}
static void epd_write_line(int x0, int y0, int x1, int y1, uint8_t color,
uint8_t *framebuffer) {
int steep = abs(y1 - y0) > abs(x1 - x0);
if (steep) {
_swap_int(x0, y0);
_swap_int(x1, y1);
}
if (x0 > x1) {
_swap_int(x0, x1);
_swap_int(y0, y1);
}
int dx, dy;
dx = x1 - x0;
dy = abs(y1 - y0);
int err = dx / 2;
int ystep;
if (y0 < y1) {
ystep = 1;
} else {
ystep = -1;
}
for (; x0 <= x1; x0++) {
if (steep) {
epd_draw_pixel(y0, x0, color, framebuffer);
} else {
epd_draw_pixel(x0, y0, color, framebuffer);
}
err -= dy;
if (err < 0) {
y0 += ystep;
err += dx;
}
}
}
void epd_draw_line(int x0, int y0, int x1, int y1, uint8_t color,
uint8_t *framebuffer) {
// Update in subclasses if desired!
if (x0 == x1) {
if (y0 > y1)
_swap_int(y0, y1);
epd_draw_vline(x0, y0, y1 - y0 + 1, color, framebuffer);
} else if (y0 == y1) {
if (x0 > x1)
_swap_int(x0, x1);
epd_draw_hline(x0, y0, x1 - x0 + 1, color, framebuffer);
} else {
epd_write_line(x0, y0, x1, y1, color, framebuffer);
}
}
void epd_draw_triangle(int x0, int y0, int x1, int y1, int x2, int y2,
uint8_t color, uint8_t *framebuffer) {
epd_draw_line(x0, y0, x1, y1, color, framebuffer);
epd_draw_line(x1, y1, x2, y2, color, framebuffer);
epd_draw_line(x2, y2, x0, y0, color, framebuffer);
}
void epd_fill_triangle(int x0, int y0, int x1, int y1, int x2, int y2,
uint8_t color, uint8_t *framebuffer) {
int a, b, y, last;
// Sort coordinates by Y order (y2 >= y1 >= y0)
if (y0 > y1) {
_swap_int(y0, y1);
_swap_int(x0, x1);
}
if (y1 > y2) {
_swap_int(y2, y1);
_swap_int(x2, x1);
}
if (y0 > y1) {
_swap_int(y0, y1);
_swap_int(x0, x1);
}
if (y0 == y2) { // Handle awkward all-on-same-line case as its own thing
a = b = x0;
if (x1 < a)
a = x1;
else if (x1 > b)
b = x1;
if (x2 < a)
a = x2;
else if (x2 > b)
b = x2;
epd_draw_hline(a, y0, b - a + 1, color, framebuffer);
return;
}
int dx01 = x1 - x0, dy01 = y1 - y0, dx02 = x2 - x0, dy02 = y2 - y0,
dx12 = x2 - x1, dy12 = y2 - y1;
int32_t sa = 0, sb = 0;
// For upper part of triangle, find scanline crossings for segments
// 0-1 and 0-2. If y1=y2 (flat-bottomed triangle), the scanline y1
// is included here (and second loop will be skipped, avoiding a /0
// error there), otherwise scanline y1 is skipped here and handled
// in the second loop...which also avoids a /0 error here if y0=y1
// (flat-topped triangle).
if (y1 == y2)
last = y1; // Include y1 scanline
else
last = y1 - 1; // Skip it
for (y = y0; y <= last; y++) {
a = x0 + sa / dy01;
b = x0 + sb / dy02;
sa += dx01;
sb += dx02;
/* longhand:
a = x0 + (x1 - x0) * (y - y0) / (y1 - y0);
b = x0 + (x2 - x0) * (y - y0) / (y2 - y0);
*/
if (a > b)
_swap_int(a, b);
epd_draw_hline(a, y, b - a + 1, color, framebuffer);
}
// For lower part of triangle, find scanline crossings for segments
// 0-2 and 1-2. This loop is skipped if y1=y2.
sa = (int32_t)dx12 * (y - y1);
sb = (int32_t)dx02 * (y - y0);
for (; y <= y2; y++) {
a = x1 + sa / dy12;
b = x0 + sb / dy02;
sa += dx12;
sb += dx02;
/* longhand:
a = x1 + (x2 - x1) * (y - y1) / (y2 - y1);
b = x0 + (x2 - x0) * (y - y0) / (y2 - y0);
*/
if (a > b)
_swap_int(a, b);
epd_draw_hline(a, y, b - a + 1, color, framebuffer);
}
}
void epd_copy_to_framebuffer(EpdRect image_area, const uint8_t *image_data,
uint8_t *framebuffer) {
assert(framebuffer != NULL);
for (uint32_t i = 0; i < image_area.width * image_area.height; i++) {
uint32_t value_index = i;
// for images of uneven width,
// consume an additional nibble per row.
if (image_area.width % 2) {
value_index += i / image_area.width;
}
uint8_t val = (value_index % 2) ? (image_data[value_index / 2] & 0xF0) >> 4
: image_data[value_index / 2] & 0x0F;
int xx = image_area.x + i % image_area.width;
if (xx < 0 || xx >= EPD_WIDTH) {
continue;
}
int yy = image_area.y + i / image_area.width;
if (yy < 0 || yy >= EPD_HEIGHT) {
continue;
}
uint8_t *buf_ptr = &framebuffer[yy * EPD_WIDTH / 2 + xx / 2];
if (xx % 2) {
*buf_ptr = (*buf_ptr & 0x0F) | (val << 4);
} else {
*buf_ptr = (*buf_ptr & 0xF0) | val;
}
}
}
enum EpdDrawError epd_draw_image(EpdRect area, const uint8_t *data, const EpdWaveform *waveform) {
int temperature = epd_ambient_temperature();
assert(waveform != NULL);
EpdRect no_crop = {
.x = 0,
.y = 0,
.width = 0,
.height = 0,
};
return epd_draw_base(area, data, no_crop, EPD_MODE_DEFAULT, temperature, NULL, waveform);
}

View File

@ -1,34 +0,0 @@
#include "driver/adc.h"
#include "esp_adc_cal.h"
#include "esp_log.h"
/// Use GPIO 35
static const adc1_channel_t channel = ADC1_CHANNEL_7;
static esp_adc_cal_characteristics_t adc_chars;
#define NUMBER_OF_SAMPLES 100
void epd_temperature_init() {
esp_adc_cal_value_t val_type = esp_adc_cal_characterize(
ADC_UNIT_1, ADC_ATTEN_DB_6, ADC_WIDTH_BIT_12, 1100, &adc_chars);
if (val_type == ESP_ADC_CAL_VAL_EFUSE_TP) {
ESP_LOGI("epd_temperature", "Characterized using Two Point Value\n");
} else if (val_type == ESP_ADC_CAL_VAL_EFUSE_VREF) {
ESP_LOGI("esp_temperature", "Characterized using eFuse Vref\n");
} else {
ESP_LOGI("esp_temperature", "Characterized using Default Vref\n");
}
adc1_config_width(ADC_WIDTH_BIT_12);
adc1_config_channel_atten(channel, ADC_ATTEN_DB_6);
}
float epd_ambient_temperature() {
uint32_t value = 0;
for (int i = 0; i < NUMBER_OF_SAMPLES; i++) {
value += adc1_get_raw(channel);
}
value /= NUMBER_OF_SAMPLES;
// voltage in mV
float voltage = esp_adc_cal_raw_to_voltage(value, &adc_chars);
return (voltage - 500.0) / 10.0;
}

View File

@ -1,11 +0,0 @@
#pragma once
/**
* Initialize the ADC for temperature measurement.
*/
void epd_temperature_init();
/**
* Get the current ambient temperature in °C.
*/
float epd_ambient_temperature();

View File

@ -1,375 +0,0 @@
#include "epd_driver.h"
#include "esp_assert.h"
#include "esp_heap_caps.h"
#include "esp_log.h"
#if ESP_IDF_VERSION < (4, 0, 0) || ARDUINO_ARCH_ESP32
#include "rom/miniz.h"
#else
#include "esp32/rom/miniz.h"
#endif
#include <math.h>
#include <stdio.h>
#include <string.h>
typedef struct {
uint8_t mask; /* char data will be bitwise AND with this */
uint8_t lead; /* start bytes of current char in utf-8 encoded character */
uint32_t beg; /* beginning of codepoint range */
uint32_t end; /* end of codepoint range */
int bits_stored; /* the number of bits from the codepoint that fits in char */
} utf_t;
/*
* UTF-8 decode inspired from rosetta code
* https://rosettacode.org/wiki/UTF-8_encode_and_decode#C
*/
static utf_t *utf[] = {
/* mask lead beg end bits */
[0] = &(utf_t){0b00111111, 0b10000000, 0, 0, 6},
[1] = &(utf_t){0b01111111, 0b00000000, 0000, 0177, 7},
[2] = &(utf_t){0b00011111, 0b11000000, 0200, 03777, 5},
[3] = &(utf_t){0b00001111, 0b11100000, 04000, 0177777, 4},
[4] = &(utf_t){0b00000111, 0b11110000, 0200000, 04177777, 3},
&(utf_t){0},
};
/**
* static decompressor object for compressed fonts.
*/
static tinfl_decompressor decomp;
static inline int min(int x, int y) { return x < y ? x : y; }
static inline int max(int x, int y) { return x > y ? x : y; }
static int utf8_len(const uint8_t ch) {
int len = 0;
for (utf_t **u = utf; *u; ++u) {
if ((ch & ~(*u)->mask) == (*u)->lead) {
break;
}
++len;
}
if (len > 4) { /* Malformed leading byte */
assert("invalid unicode.");
}
return len;
}
static uint32_t next_cp(const uint8_t **string) {
if (**string == 0) {
return 0;
}
int bytes = utf8_len(**string);
const uint8_t *chr = *string;
*string += bytes;
int shift = utf[0]->bits_stored * (bytes - 1);
uint32_t codep = (*chr++ & utf[bytes]->mask) << shift;
for (int i = 1; i < bytes; ++i, ++chr) {
shift -= utf[0]->bits_stored;
codep |= ((const uint8_t)*chr & utf[0]->mask) << shift;
}
return codep;
}
EpdFontProperties epd_font_properties_default() {
EpdFontProperties props = {
.fg_color = 0, .bg_color = 15, .fallback_glyph = 0, .flags = EPD_DRAW_ALIGN_LEFT};
return props;
}
const EpdGlyph* epd_get_glyph(const EpdFont *font, uint32_t code_point) {
const EpdUnicodeInterval *intervals = font->intervals;
for (int i = 0; i < font->interval_count; i++) {
const EpdUnicodeInterval *interval = &intervals[i];
if (code_point >= interval->first && code_point <= interval->last) {
return &font->glyph[interval->offset + (code_point - interval->first)];
}
if (code_point < interval->first) {
return NULL;
}
}
return NULL;
}
static int uncompress(uint8_t *dest, uint32_t uncompressed_size, const uint8_t *source, uint32_t source_size) {
if (uncompressed_size == 0 || dest == NULL || source_size == 0 || source == NULL) {
return -1;
}
tinfl_init(&decomp);
// we know everything will fit into the buffer.
tinfl_status decomp_status = tinfl_decompress(&decomp, source, &source_size, dest, dest, &uncompressed_size, TINFL_FLAG_PARSE_ZLIB_HEADER | TINFL_FLAG_USING_NON_WRAPPING_OUTPUT_BUF);
if (decomp_status != TINFL_STATUS_DONE) {
return decomp_status;
}
return 0;
}
/*!
@brief Draw a single character to a pre-allocated buffer.
*/
static enum EpdDrawError IRAM_ATTR draw_char(const EpdFont *font, uint8_t *buffer,
int *cursor_x, int cursor_y, uint16_t buf_width,
uint16_t buf_height, uint32_t cp,
const EpdFontProperties *props) {
assert(props != NULL);
const EpdGlyph *glyph = epd_get_glyph(font, cp);
if (!glyph) {
glyph = epd_get_glyph(font, props->fallback_glyph);
}
if (!glyph) {
return EPD_DRAW_GLYPH_FALLBACK_FAILED;
}
uint32_t offset = glyph->data_offset;
uint8_t width = glyph->width, height = glyph->height;
int left = glyph->left;
int byte_width = (width / 2 + width % 2);
unsigned long bitmap_size = byte_width * height;
const uint8_t *bitmap = NULL;
if (font->compressed) {
uint8_t* tmp_bitmap = (uint8_t *)malloc(bitmap_size);
if (tmp_bitmap == NULL && bitmap_size) {
ESP_LOGE("font", "malloc failed.");
return EPD_DRAW_FAILED_ALLOC;
}
uncompress(tmp_bitmap, bitmap_size, &font->bitmap[offset],
glyph->compressed_size);
bitmap = tmp_bitmap;
} else {
bitmap = &font->bitmap[offset];
}
uint8_t color_lut[16];
for (int c = 0; c < 16; c++) {
int color_difference = (int)props->fg_color - (int)props->bg_color;
color_lut[c] = max(0, min(15, props->bg_color + c * color_difference / 15));
}
for (int y = 0; y < height; y++) {
int yy = cursor_y - glyph->top + y;
if (yy < 0 || yy >= buf_height) {
continue;
}
int start_pos = *cursor_x + left;
bool byte_complete = start_pos % 2;
int x = max(0, -start_pos);
int max_x = min(start_pos + width, buf_width * 2);
for (int xx = start_pos; xx < max_x; xx++) {
uint32_t buf_pos = yy * buf_width + xx / 2;
uint8_t old = buffer[buf_pos];
uint8_t bm = bitmap[y * byte_width + x / 2];
if ((x & 1) == 0) {
bm = bm & 0xF;
} else {
bm = bm >> 4;
}
if ((xx & 1) == 0) {
buffer[buf_pos] = (old & 0xF0) | color_lut[bm];
} else {
buffer[buf_pos] = (old & 0x0F) | (color_lut[bm] << 4);
}
byte_complete = !byte_complete;
x++;
}
}
if (font->compressed) {
free((uint8_t*)bitmap);
}
*cursor_x += glyph->advance_x;
return EPD_DRAW_SUCCESS;
}
/*!
* @brief Calculate the bounds of a character when drawn at (x, y), move the
* cursor (*x) forward, adjust the given bounds.
*/
static void get_char_bounds(const EpdFont *font, uint32_t cp, int *x, int *y,
int *minx, int *miny, int *maxx, int *maxy,
const EpdFontProperties *props) {
assert(props != NULL);
const EpdGlyph *glyph = epd_get_glyph(font, cp);
if (!glyph) {
glyph = epd_get_glyph(font, props->fallback_glyph);
}
if (!glyph) {
return;
}
int x1 = *x + glyph->left, y1 = *y + glyph->top - glyph->height,
x2 = x1 + glyph->width, y2 = y1 + glyph->height;
// background needs to be taken into account
if (props->flags & EPD_DRAW_BACKGROUND) {
*minx = min(*x, min(*minx, x1));
*maxx = max(max(*x + glyph->advance_x, x2), *maxx);
*miny = min(*y + font->descender, min(*miny, y1));
*maxy = max(*y + font->ascender, max(*maxy, y2));
} else {
if (x1 < *minx)
*minx = x1;
if (y1 < *miny)
*miny = y1;
if (x2 > *maxx)
*maxx = x2;
if (y2 > *maxy)
*maxy = y2;
}
*x += glyph->advance_x;
}
void epd_get_text_bounds(const EpdFont *font, const char *string,
const int *x, const int *y,
int *x1, int *y1, int *w, int *h,
const EpdFontProperties *properties) {
// FIXME: Does not respect alignment!
assert(properties != NULL);
EpdFontProperties props = *properties;
if (*string == '\0') {
*w = 0;
*h = 0;
*y1 = *y;
*x1 = *x;
return;
}
int minx = 100000, miny = 100000, maxx = -1, maxy = -1;
int original_x = *x;
int temp_x = *x;
int temp_y = *y;
uint32_t c;
while ((c = next_cp((const uint8_t **)&string))) {
get_char_bounds(font, c, &temp_x, &temp_y, &minx, &miny, &maxx, &maxy, &props);
}
*x1 = min(original_x, minx);
*w = maxx - *x1;
*y1 = miny;
*h = maxy - miny;
}
static enum EpdDrawError epd_write_line(
const EpdFont *font, const char *string, int *cursor_x,
int *cursor_y, uint8_t *framebuffer,
const EpdFontProperties *properties)
{
assert(framebuffer != NULL);
if (*string == '\0') {
return EPD_DRAW_SUCCESS;
}
assert(properties != NULL);
EpdFontProperties props = *properties;
enum EpdFontFlags alignment_mask = EPD_DRAW_ALIGN_LEFT | EPD_DRAW_ALIGN_RIGHT | EPD_DRAW_ALIGN_CENTER;
enum EpdFontFlags alignment = props.flags & alignment_mask;
// alignments are mutually exclusive!
if ((alignment & (alignment - 1)) != 0) {
return EPD_DRAW_INVALID_FONT_FLAGS;
}
int x1 = 0, y1 = 0, w = 0, h = 0;
int tmp_cur_x = *cursor_x;
int tmp_cur_y = *cursor_y;
epd_get_text_bounds(font, string, &tmp_cur_x, &tmp_cur_y, &x1, &y1, &w, &h, &props);
// no printable characters
if (w < 0 || h < 0) {
return EPD_DRAW_NO_DRAWABLE_CHARACTERS;
}
int baseline_height = *cursor_y - y1;
int buf_width = EPD_WIDTH / 2;
int buf_height = EPD_HEIGHT;
uint8_t* buffer = framebuffer;
int local_cursor_x = *cursor_x;
int local_cursor_y = *cursor_y;
uint32_t c;
int cursor_x_init = local_cursor_x;
int cursor_y_init = local_cursor_y;
switch (alignment) {
case EPD_DRAW_ALIGN_LEFT: {
break;
}
case EPD_DRAW_ALIGN_CENTER: {
local_cursor_x -= w / 2;
break;
}
case EPD_DRAW_ALIGN_RIGHT: {
local_cursor_x -= w;
break;
}
default:
break;
}
uint8_t bg = props.bg_color;
if (props.flags & EPD_DRAW_BACKGROUND) {
for (int l = local_cursor_y - font->ascender;
l < local_cursor_y - font->descender; l++) {
epd_draw_hline(local_cursor_x, l, w, bg << 4, buffer);
}
}
enum EpdDrawError err = EPD_DRAW_SUCCESS;
while ((c = next_cp((const uint8_t **)&string))) {
err |= draw_char(font, buffer, &local_cursor_x, local_cursor_y, buf_width,
buf_height, c, &props);
}
*cursor_x += local_cursor_x - cursor_x_init;
*cursor_y += local_cursor_y - cursor_y_init;
return err;
}
enum EpdDrawError epd_write_default(const EpdFont *font, const char *string, int *cursor_x,
int *cursor_y, uint8_t *framebuffer) {
const EpdFontProperties props = epd_font_properties_default();
return epd_write_string(font, string, cursor_x, cursor_y, framebuffer, &props);
}
enum EpdDrawError epd_write_string(
const EpdFont *font, const char *string, int *cursor_x,
int *cursor_y, uint8_t *framebuffer,
const EpdFontProperties *properties
) {
char *token, *newstring, *tofree;
if (string == NULL) {
ESP_LOGE("font.c", "cannot draw a NULL string!");
return EPD_DRAW_STRING_INVALID;
}
tofree = newstring = strdup(string);
if (newstring == NULL) {
ESP_LOGE("font.c", "cannot allocate string copy!");
return EPD_DRAW_FAILED_ALLOC;
}
enum EpdDrawError err = EPD_DRAW_SUCCESS;
// taken from the strsep manpage
int line_start = *cursor_x;
while ((token = strsep(&newstring, "\n")) != NULL) {
*cursor_x = line_start;
err |= epd_write_line(font, token, cursor_x, cursor_y, framebuffer, properties);
*cursor_y += font->advance_y;
}
free(tofree);
return err;
}

View File

@ -1,102 +0,0 @@
/**
* High-level API implementation for epdiy.
*/
#include "epd_highlevel.h"
#include <assert.h>
#include <esp_types.h>
#include <string.h>
#include <esp_heap_caps.h>
#include <esp_log.h>
static bool already_initialized = 0;
const static int fb_size = EPD_WIDTH / 2 * EPD_HEIGHT;
EpdiyHighlevelState epd_hl_init(const EpdWaveform* waveform) {
assert(!already_initialized);
assert(waveform != NULL);
EpdiyHighlevelState state;
state.front_fb = heap_caps_malloc(fb_size, MALLOC_CAP_SPIRAM);
assert(state.front_fb != NULL);
state.back_fb = heap_caps_malloc(fb_size, MALLOC_CAP_SPIRAM);
assert(state.back_fb != NULL);
state.difference_fb = heap_caps_malloc(2 * fb_size, MALLOC_CAP_SPIRAM);
assert(state.difference_fb != NULL);
state.dirty_lines = malloc(EPD_HEIGHT * sizeof(bool));
assert(state.dirty_lines != NULL);
state.waveform = waveform;
memset(state.front_fb, 0xFF, fb_size);
memset(state.back_fb, 0xFF, fb_size);
already_initialized = true;
return state;
}
uint8_t* epd_hl_get_framebuffer(EpdiyHighlevelState* state) {
assert(state != NULL);
return state->front_fb;
}
enum EpdDrawError epd_hl_update_screen(EpdiyHighlevelState* state, enum EpdDrawMode mode, int temperature) {
return epd_hl_update_area(state, mode, temperature, epd_full_screen());
}
enum EpdDrawError epd_hl_update_area(EpdiyHighlevelState* state, enum EpdDrawMode mode, int temperature, EpdRect area) {
assert(state != NULL);
bool previously_white = false;
bool previously_black = false;
//FIXME: use crop information here, if available
EpdRect diff_area = epd_difference_image_cropped(
state->front_fb,
state->back_fb,
area,
state->difference_fb,
state->dirty_lines,
&previously_white,
&previously_black
);
if (diff_area.height == 0 || diff_area.width == 0) {
return EPD_DRAW_SUCCESS;
}
enum EpdDrawError err;
if (previously_white) {
err = epd_draw_base(epd_full_screen(), state->front_fb, diff_area, MODE_PACKING_2PPB | PREVIOUSLY_WHITE | mode, temperature, state->dirty_lines, state->waveform);
} else if (previously_black) {
err = epd_draw_base(epd_full_screen(), state->front_fb, diff_area, MODE_PACKING_2PPB | PREVIOUSLY_BLACK | mode, temperature, state->dirty_lines, state->waveform);
} else {
err = epd_draw_base(epd_full_screen(), state->difference_fb, diff_area, MODE_PACKING_1PPB_DIFFERENCE | mode, temperature, state->dirty_lines, state->waveform);
}
for (int l=diff_area.y; l < diff_area.y + diff_area.height; l++) {
if (state->dirty_lines[l]) {
memcpy(
state->back_fb + EPD_WIDTH / 2 * l,
state->front_fb + EPD_WIDTH / 2 * l,
EPD_WIDTH / 2
);
}
}
return err;
}
void epd_hl_set_all_white(EpdiyHighlevelState* state) {
assert(state != NULL);
memset(state->front_fb, 0xFF, fb_size);
}
void epd_fullclear(EpdiyHighlevelState* state, int temperature) {
assert(state != NULL);
epd_hl_set_all_white(state);
enum EpdDrawError err = epd_hl_update_screen(state, MODE_GC16, temperature);
assert(err == EPD_DRAW_SUCCESS);
epd_clear();
}

View File

@ -1,262 +0,0 @@
#include "i2s_data_bus.h"
#include "driver/periph_ctrl.h"
#if ESP_IDF_VERSION < (4, 0, 0) || ARDUINO_ARCH_ESP32
#include "rom/lldesc.h"
#else
#include "esp32/rom/lldesc.h"
#endif
#include "esp_heap_caps.h"
#include "soc/i2s_reg.h"
#include "soc/i2s_struct.h"
#include "soc/rtc.h"
/// DMA descriptors for front and back line buffer.
/// We use two buffers, so one can be filled while the other
/// is transmitted.
typedef struct {
volatile lldesc_t *dma_desc_a;
volatile lldesc_t *dma_desc_b;
/// Front and back line buffer.
uint8_t *buf_a;
uint8_t *buf_b;
} i2s_parallel_state_t;
/// Indicates which line buffer is currently back / front.
static int current_buffer = 0;
/// The I2S state instance.
static i2s_parallel_state_t i2s_state;
static intr_handle_t gI2S_intr_handle = NULL;
/// Indicates the device has finished its transmission and is ready again.
static volatile bool output_done = true;
/// The start pulse pin extracted from the configuration for use in the "done"
/// interrupt.
static gpio_num_t start_pulse_pin;
/// Initializes a DMA descriptor.
static void fill_dma_desc(volatile lldesc_t *dmadesc, uint8_t *buf,
i2s_bus_config *cfg) {
dmadesc->size = cfg->epd_row_width / 4;
dmadesc->length = cfg->epd_row_width / 4;
dmadesc->buf = buf;
dmadesc->eof = 1;
dmadesc->sosf = 1;
dmadesc->owner = 1;
dmadesc->qe.stqe_next = 0;
dmadesc->offset = 0;
}
/// Address of the currently front DMA descriptor,
/// which uses only the lower 20bits (according to TRM)
uint32_t dma_desc_addr() {
return (uint32_t)(current_buffer ? i2s_state.dma_desc_a
: i2s_state.dma_desc_b) &
0x000FFFFF;
}
/// Set up a GPIO as output and route it to a signal.
static void gpio_setup_out(int gpio, int sig, bool invert) {
if (gpio == -1)
return;
PIN_FUNC_SELECT(GPIO_PIN_MUX_REG[gpio], PIN_FUNC_GPIO);
gpio_set_direction(gpio, GPIO_MODE_DEF_OUTPUT);
gpio_matrix_out(gpio, sig, invert, false);
}
/// Resets "Start Pulse" signal when the current row output is done.
static void IRAM_ATTR i2s_int_hdl(void *arg) {
i2s_dev_t *dev = &I2S1;
if (dev->int_st.out_done) {
//gpio_set_level(start_pulse_pin, 1);
//gpio_set_level(GPIO_NUM_26, 0);
output_done = true;
}
// Clear the interrupt. Otherwise, the whole device would hang.
dev->int_clr.val = dev->int_raw.val;
}
volatile uint8_t IRAM_ATTR *i2s_get_current_buffer() {
return current_buffer ? i2s_state.dma_desc_a->buf : i2s_state.dma_desc_b->buf;
}
bool IRAM_ATTR i2s_is_busy() {
// DMA and FIFO must be done
return !output_done || !I2S1.state.tx_idle;
}
void IRAM_ATTR i2s_switch_buffer() {
// either device is done transmitting or the switch must be away from the
// buffer currently used by the DMA engine.
while (i2s_is_busy() && dma_desc_addr() != I2S1.out_link.addr) {
};
current_buffer = !current_buffer;
}
void IRAM_ATTR i2s_start_line_output() {
output_done = false;
i2s_dev_t *dev = &I2S1;
dev->conf.tx_start = 0;
dev->conf.tx_reset = 1;
dev->conf.tx_fifo_reset = 1;
dev->conf.rx_fifo_reset = 1;
dev->conf.tx_reset = 0;
dev->conf.tx_fifo_reset = 0;
dev->conf.rx_fifo_reset = 0;
dev->out_link.addr = dma_desc_addr();
dev->out_link.start = 1;
// sth is pulled up through peripheral interrupt
// This is timing-critical!
gpio_set_level(start_pulse_pin, 0);
dev->conf.tx_start = 1;
}
void i2s_bus_init(i2s_bus_config *cfg) {
// TODO: Why?
gpio_num_t I2S_GPIO_BUS[] = {cfg->data_6, cfg->data_7, cfg->data_4,
cfg->data_5, cfg->data_2, cfg->data_3,
cfg->data_0, cfg->data_1};
gpio_set_direction(cfg->start_pulse, GPIO_MODE_OUTPUT);
gpio_set_level(cfg->start_pulse, 1);
// store pin in global variable for use in interrupt.
start_pulse_pin = cfg->start_pulse;
// Use I2S1 with no signal offset (for some reason the offset seems to be
// needed in 16-bit mode, but not in 8 bit mode.
int signal_base = I2S1O_DATA_OUT0_IDX;
// Setup and route GPIOS
for (int x = 0; x < 8; x++) {
gpio_setup_out(I2S_GPIO_BUS[x], signal_base + x, false);
}
// Invert word select signal
gpio_setup_out(cfg->clock, I2S1O_WS_OUT_IDX, true);
periph_module_enable(PERIPH_I2S1_MODULE);
i2s_dev_t *dev = &I2S1;
// Initialize device
dev->conf.tx_reset = 1;
dev->conf.tx_reset = 0;
// Reset DMA
dev->lc_conf.in_rst = 1;
dev->lc_conf.in_rst = 0;
dev->lc_conf.out_rst = 1;
dev->lc_conf.out_rst = 0;
// Setup I2S config. See section 12 of Technical Reference Manual
// Enable LCD mode
dev->conf2.val = 0;
dev->conf2.lcd_en = 1;
// Enable FRAME1-Mode (See technical reference manual)
dev->conf2.lcd_tx_wrx2_en = 1;
dev->conf2.lcd_tx_sdx2_en = 0;
// Set to 8 bit parallel output
dev->sample_rate_conf.val = 0;
dev->sample_rate_conf.tx_bits_mod = 8;
// Half speed of bit clock in LCD mode.
// (Smallest possible divider according to the spec).
dev->sample_rate_conf.tx_bck_div_num = 2;
#if defined(CONFIG_EPD_DISPLAY_TYPE_ED097OC4_LQ)
// Initialize Audio Clock (APLL) for 120 Mhz.
rtc_clk_apll_enable(1, 0, 0, 8, 0);
#else
// Initialize Audio Clock (APLL) for 100 Mhz.
rtc_clk_apll_enable(1, 0, 0, 8, 0);
#endif
// Set Audio Clock Dividers
dev->clkm_conf.val = 0;
dev->clkm_conf.clka_en = 1;
dev->clkm_conf.clkm_div_a = 1;
dev->clkm_conf.clkm_div_b = 0;
// 2 is the smallest possible divider according to the spec.
dev->clkm_conf.clkm_div_num = 2;
// Set up FIFO
dev->fifo_conf.val = 0;
dev->fifo_conf.tx_fifo_mod_force_en = 1;
dev->fifo_conf.tx_fifo_mod = 1;
dev->fifo_conf.tx_data_num = 32;
dev->fifo_conf.dscr_en = 1;
// Stop after transmission complete
dev->conf1.val = 0;
dev->conf1.tx_stop_en = 1;
dev->conf1.tx_pcm_bypass = 1;
// Configure TX channel
dev->conf_chan.val = 0;
dev->conf_chan.tx_chan_mod = 1;
dev->conf.tx_right_first = 1;
dev->timing.val = 0;
// Allocate DMA descriptors
i2s_state.buf_a = heap_caps_malloc(cfg->epd_row_width / 4, MALLOC_CAP_DMA);
i2s_state.buf_b = heap_caps_malloc(cfg->epd_row_width / 4, MALLOC_CAP_DMA);
i2s_state.dma_desc_a = heap_caps_malloc(sizeof(lldesc_t), MALLOC_CAP_DMA);
i2s_state.dma_desc_b = heap_caps_malloc(sizeof(lldesc_t), MALLOC_CAP_DMA);
// and fill them
fill_dma_desc(i2s_state.dma_desc_a, i2s_state.buf_a, cfg);
fill_dma_desc(i2s_state.dma_desc_b, i2s_state.buf_b, cfg);
// enable "done" interrupt
SET_PERI_REG_BITS(I2S_INT_ENA_REG(1), I2S_OUT_DONE_INT_ENA_V, 1,
I2S_OUT_DONE_INT_ENA_S);
// register interrupt
esp_intr_alloc(ETS_I2S1_INTR_SOURCE, 0, i2s_int_hdl, 0, &gI2S_intr_handle);
// Reset FIFO/DMA
dev->lc_conf.in_rst = 1;
dev->lc_conf.out_rst = 1;
dev->lc_conf.ahbm_rst = 1;
dev->lc_conf.ahbm_fifo_rst = 1;
dev->lc_conf.in_rst = 0;
dev->lc_conf.out_rst = 0;
dev->lc_conf.ahbm_rst = 0;
dev->lc_conf.ahbm_fifo_rst = 0;
dev->conf.tx_reset = 1;
dev->conf.tx_fifo_reset = 1;
dev->conf.rx_fifo_reset = 1;
dev->conf.tx_reset = 0;
dev->conf.tx_fifo_reset = 0;
dev->conf.rx_fifo_reset = 0;
// Start dma on front buffer
dev->lc_conf.val =
I2S_OUT_DATA_BURST_EN | I2S_OUTDSCR_BURST_EN | I2S_OUT_DATA_BURST_EN;
dev->out_link.addr = ((uint32_t)(i2s_state.dma_desc_a));
dev->out_link.start = 1;
dev->int_clr.val = dev->int_raw.val;
dev->int_ena.val = 0;
dev->int_ena.out_done = 1;
dev->conf.tx_start = 0;
}
void i2s_deinit() {
esp_intr_free(gI2S_intr_handle);
free(i2s_state.buf_a);
free(i2s_state.buf_b);
free((void *)i2s_state.dma_desc_a);
free((void *)i2s_state.dma_desc_b);
rtc_clk_apll_enable(0, 0, 0, 8, 0);
periph_module_disable(PERIPH_I2S1_MODULE);
}

View File

@ -1,67 +0,0 @@
/**
* Implements a 8bit parallel interface to transmit pixel
* data to the display, based on the I2S peripheral.
*/
#pragma once
#include "driver/gpio.h"
#include "esp_attr.h"
#include <stdint.h>
/**
* I2S bus configuration parameters.
*/
typedef struct {
/// GPIO numbers of the parallel bus pins.
gpio_num_t data_0;
gpio_num_t data_1;
gpio_num_t data_2;
gpio_num_t data_3;
gpio_num_t data_4;
gpio_num_t data_5;
gpio_num_t data_6;
gpio_num_t data_7;
/// Data clock pin.
gpio_num_t clock;
/// "Start Pulse", enabling data input on the slave device (active low)
gpio_num_t start_pulse;
// Width of a display row in pixels.
uint32_t epd_row_width;
} i2s_bus_config;
/**
* Initialize the I2S data bus for communication
* with a 8bit parallel display interface.
*/
void i2s_bus_init(i2s_bus_config *cfg);
/**
* Get the currently writable line buffer.
*/
volatile uint8_t IRAM_ATTR *i2s_get_current_buffer();
/**
* Switches front and back line buffer.
* If the switched-to line buffer is currently in use,
* this function blocks until transmission is done.
*/
void IRAM_ATTR i2s_switch_buffer();
/**
* Start transmission of the current back buffer.
*/
void IRAM_ATTR i2s_start_line_output();
/**
* Returns true if there is an ongoing transmission.
*/
bool IRAM_ATTR i2s_is_busy();
/**
* Give up allocated resources.
*/
void i2s_deinit();

View File

@ -1,471 +0,0 @@
/**
* @file epd_driver.h
* A driver library for drawing to an EPD.
*/
#ifdef __cplusplus
extern "C" {
#endif
#define CONFIG_EPD_DISPLAY_TYPE_ED047TC1
#define CONFIG_EPD_BOARD_REVISION_LILYGO_T5_47
#pragma once
#include "esp_attr.h"
#include "epd_internals.h"
#include <stdbool.h>
#include <stdint.h>
/// An area on the display.
typedef struct {
/// Horizontal position.
int x;
/// Vertical position.
int y;
/// Area / image width, must be positive.
int width;
/// Area / image height, must be positive.
int height;
} EpdRect;
/// Possible failures when drawing.
enum EpdDrawError {
EPD_DRAW_SUCCESS = 0x0,
/// No valid framebuffer packing mode was specified.
EPD_DRAW_INVALID_PACKING_MODE = 0x1,
/// No lookup table implementation for this mode / packing.
EPD_DRAW_LOOKUP_NOT_IMPLEMENTED = 0x2,
/// The string to draw is invalid.
EPD_DRAW_STRING_INVALID = 0x4,
/// The string was not empty, but no characters where drawable.
EPD_DRAW_NO_DRAWABLE_CHARACTERS = 0x8,
/// Allocation failed
EPD_DRAW_FAILED_ALLOC = 0x10,
/// A glyph could not be drawn, and not fallback was present.
EPD_DRAW_GLYPH_FALLBACK_FAILED = 0x20,
/// The specified crop area is invalid.
EPD_DRAW_INVALID_CROP = 0x40,
/// No such mode is available with the current waveform.
EPD_DRAW_MODE_NOT_FOUND = 0x80,
/// The waveform info file contains no applicable temperature range.
EPD_DRAW_NO_PHASES_AVAILABLE = 0x100,
/// An invalid combination of font flags was used.
EPD_DRAW_INVALID_FONT_FLAGS = 0x200,
};
/// Global EPD driver options.
enum EpdInitOptions {
/// Use the default options.
EPD_OPTIONS_DEFAULT = 0,
/// Use a small look-up table of 1024 bytes.
/// The EPD driver will use less space, but performance may be worse.
EPD_LUT_1K = 1,
/// Use a 64K lookup table. (default)
/// Best performance, but permanently occupies a 64k block of internal memory.
EPD_LUT_64K = 2,
/// Use a small feed queue of 8 display lines.
/// This uses less memory, but may impact performance.
EPD_FEED_QUEUE_8 = 4,
/// Use a feed queue of 32 display lines. (default)
/// Best performance, but larger memory footprint.
EPD_FEED_QUEUE_32 = 8,
};
/// The image drawing mode.
enum EpdDrawMode {
/// An init waveform.
/// This is currently unused, use `epd_clear()` instead.
MODE_INIT = 0x0,
/// Direct Update: Go from any color to black for white only.
MODE_DU = 0x1,
/// Go from any grayscale value to another with a flashing update.
MODE_GC16 = 0x2,
/// Faster version of `MODE_GC16`.
/// Not available with default epdiy waveforms.
MODE_GC16_FAST = 0x3,
/// Animation Mode: Fast, monochrom updates.
/// Not available with default epdiy waveforms.
MODE_A2 = 0x4,
/// Go from any grayscale value to another with a non-flashing update.
MODE_GL16 = 0x5,
/// Faster version of `MODE_GL16`.
/// Not available with default epdiy waveforms.
MODE_GL16_FAST = 0x6,
/// A 4-grayscale version of `MODE_DU`.
/// Not available with default epdiy waveforms.
MODE_DU4 = 0x7,
/// Arbitrary transitions for 4 grayscale values.
/// Not available with default epdiy waveforms.
MODE_GL4 = 0xA,
/// Not available with default epdiy waveforms.
MODE_GL16_INV = 0xB,
/// Go from a white screen to arbitrary grayscale, quickly.
/// Exclusively available with epdiy waveforms.
MODE_EPDIY_WHITE_TO_GL16 = 0x10,
/// Go from a black screen to arbitrary grayscale, quickly.
/// Exclusively available with epdiy waveforms.
MODE_EPDIY_BLACK_TO_GL16 = 0x11,
/// Monochrome mode. Only supported with 1bpp buffers.
MODE_EPDIY_MONOCHROME = 0x20,
MODE_UNKNOWN_WAVEFORM = 0x3F,
// Framebuffer packing modes
/// 1 bit-per-pixel framebuffer with 0 = black, 1 = white.
/// MSB is left is the leftmost pixel, LSB the rightmost pixel.
MODE_PACKING_8PPB = 0x40,
/// 4 bit-per pixel framebuffer with 0x0 = black, 0xF = white.
/// The upper nibble corresponds to the left pixel.
/// A byte cannot wrap over multiple rows, images of uneven width
/// must add a padding nibble per line.
MODE_PACKING_2PPB = 0x80,
/// A difference image with one pixel per byte.
/// The upper nibble marks the "from" color,
/// the lower nibble the "to" color.
MODE_PACKING_1PPB_DIFFERENCE = 0x100,
// reserver for 4PPB mode
/// Assert that the display has a uniform color, e.g. after initialization.
/// If `MODE_PACKING_2PPB` is specified, a optimized output calculation can be used.
/// Draw on a white background
PREVIOUSLY_WHITE = 0x200,
/// See `PREVIOUSLY_WHITE`.
/// Draw on a black background
PREVIOUSLY_BLACK = 0x400,
};
/// The default draw mode (non-flashy refresh, whith previously white screen).
#define EPD_MODE_DEFAULT (MODE_GL16 | PREVIOUSLY_WHITE)
/// Font drawing flags
enum EpdFontFlags {
/// Draw a background.
///
/// Take the background into account
/// when calculating the size.
EPD_DRAW_BACKGROUND = 0x1,
/// Left-Align lines
EPD_DRAW_ALIGN_LEFT = 0x2,
/// Right-align lines
EPD_DRAW_ALIGN_RIGHT = 0x4,
/// Center-align lines
EPD_DRAW_ALIGN_CENTER = 0x8,
};
/// Font properties.
typedef struct {
/// Foreground color
uint8_t fg_color : 4;
/// Background color
uint8_t bg_color : 4;
/// Use the glyph for this codepoint for missing glyphs.
uint32_t fallback_glyph;
/// Additional flags, reserved for future use
enum EpdFontFlags flags;
} EpdFontProperties;
/** Initialize the ePaper display */
void epd_init(enum EpdInitOptions options);
/** Deinit the ePaper display */
void epd_deinit();
/** Enable display power supply. */
void epd_poweron();
/** Disable display power supply. */
void epd_poweroff();
/** Clear the whole screen by flashing it. */
void epd_clear();
/**
* Clear an area by flashing it.
*
* @param area: The area to clear.
*/
void epd_clear_area(EpdRect area);
/**
* Clear an area by flashing it.
*
* @param area: The area to clear.
* @param cycles: The number of black-to-white clear cycles.
* @param cycle_time: Length of a cycle. Default: 50 (us).
*/
void epd_clear_area_cycles(EpdRect area, int cycles, int cycle_time);
/**
* @returns Rectancle representing the whole screen area.
*/
EpdRect epd_full_screen();
/**
* Draw a picture to a given framebuffer.
*
* @param image_area: The area to copy to. `width` and `height` of the area
* must correspond to the image dimensions in pixels.
* @param image_data: The image data, as a buffer of 4 bit wide brightness
* values. Pixel data is packed (two pixels per byte). A byte cannot wrap over
* multiple rows, images of uneven width must add a padding nibble per line.
* @param framebuffer: The framebuffer object,
* which must be `EPD_WIDTH / 2 * EPD_HEIGHT` large.
*/
void epd_copy_to_framebuffer(EpdRect image_area, const uint8_t *image_data,
uint8_t *framebuffer);
/**
* Draw a pixel a given framebuffer.
*
* @param x: Horizontal position in pixels.
* @param y: Vertical position in pixels.
* @param color: The gray value of the line (0-255);
* @param framebuffer: The framebuffer to draw to,
*/
void epd_draw_pixel(int x, int y, uint8_t color, uint8_t *framebuffer);
/**
* Draw a horizontal line to a given framebuffer.
*
* @param x: Horizontal start position in pixels.
* @param y: Vertical start position in pixels.
* @param length: Length of the line in pixels.
* @param color: The gray value of the line (0-255);
* @param framebuffer: The framebuffer to draw to,
* which must be `EPD_WIDTH / 2 * EPD_HEIGHT` bytes large.
*/
void epd_draw_hline(int x, int y, int length, uint8_t color,
uint8_t *framebuffer);
/**
* Draw a horizontal line to a given framebuffer.
*
* @param x: Horizontal start position in pixels.
* @param y: Vertical start position in pixels.
* @param length: Length of the line in pixels.
* @param color: The gray value of the line (0-255);
* @param framebuffer: The framebuffer to draw to,
* which must be `EPD_WIDTH / 2 * EPD_HEIGHT` bytes large.
*/
void epd_draw_vline(int x, int y, int length, uint8_t color,
uint8_t *framebuffer);
void epd_fill_circle_helper(int x0, int y0, int r, int corners, int delta,
uint8_t color, uint8_t *framebuffer);
/**
* Draw a circle with given center and radius
*
* @param x: Center-point x coordinate
* @param y: Center-point y coordinate
* @param r: Radius of the circle in pixels
* @param color: The gray value of the line (0-255);
* @param framebuffer: The framebuffer to draw to,
*/
void epd_draw_circle(int x, int y, int r, uint8_t color, uint8_t *framebuffer);
/**
* Draw a circle with fill with given center and radius
*
* @param x: Center-point x coordinate
* @param y: Center-point y coordinate
* @param r: Radius of the circle in pixels
* @param color: The gray value of the line (0-255);
* @param framebuffer: The framebuffer to draw to,
*/
void epd_fill_circle(int x, int y, int r, uint8_t color, uint8_t *framebuffer);
/**
* Draw a rectanle with no fill color
*
* @param rect: The rectangle to draw.
* @param color: The gray value of the line (0-255);
* @param framebuffer: The framebuffer to draw to,
*/
void epd_draw_rect(EpdRect rect, uint8_t color, uint8_t *framebuffer);
/**
* Draw a rectanle with fill color
*
* @param rect: The rectangle to fill.
* @param color: The gray value of the line (0-255);
* @param framebuffer: The framebuffer to draw to,
*/
void epd_fill_rect(EpdRect rect, uint8_t color, uint8_t *framebuffer);
/**
* Draw a line
*
* @param x0 Start point x coordinate
* @param y0 Start point y coordinate
* @param x1 End point x coordinate
* @param y1 End point y coordinate
* @param color: The gray value of the line (0-255);
* @param framebuffer: The framebuffer to draw to,
*/
void epd_draw_line(int x0, int y0, int x1, int y1, uint8_t color,
uint8_t *framebuffer);
/**
* Draw a triangle with no fill color
*
* @param x0 Vertex #0 x coordinate
* @param y0 Vertex #0 y coordinate
* @param x1 Vertex #1 x coordinate
* @param y1 Vertex #1 y coordinate
* @param x2 Vertex #2 x coordinate
* @param y2 Vertex #2 y coordinate
* @param color: The gray value of the line (0-255);
* @param framebuffer: The framebuffer to draw to,
*/
void epd_draw_triangle(int x0, int y0, int x1, int y1, int x2, int y2,
uint8_t color, uint8_t *framebuffer);
/**
* Draw a triangle with color-fill
*
* @param x0 Vertex #0 x coordinate
* @param y0 Vertex #0 y coordinate
* @param x1 Vertex #1 x coordinate
* @param y1 Vertex #1 y coordinate
* @param x2 Vertex #2 x coordinate
* @param y2 Vertex #2 y coordinate
* @param color: The gray value of the line (0-255);
* @param framebuffer: The framebuffer to draw to,
*/
void epd_fill_triangle(int x0, int y0, int x1, int y1, int x2, int y2,
uint8_t color, uint8_t *framebuffer);
/**
* Get the current ambient temperature in °C, if supported by the board.
* Requires the display to be powered on.
*/
float epd_ambient_temperature();
/**
* The default font properties.
*/
EpdFontProperties epd_font_properties_default();
/*!
* Get the text bounds for string, when drawn at (x, y).
* Set font properties to NULL to use the defaults.
*/
void epd_get_text_bounds(const EpdFont *font, const char *string,
const int *x, const int *y,
int *x1, int *y1, int *w, int *h,
const EpdFontProperties *props);
/**
* Write text to the EPD.
*/
enum EpdDrawError epd_write_string(const EpdFont *font, const char *string, int *cursor_x,
int *cursor_y, uint8_t *framebuffer,
const EpdFontProperties *properties);
/**
* Write a (multi-line) string to the EPD.
*/
enum EpdDrawError epd_write_default(const EpdFont *font, const char *string, int *cursor_x,
int *cursor_y, uint8_t *framebuffer);
/**
* Get the font glyph for a unicode code point.
*/
const EpdGlyph* epd_get_glyph(const EpdFont *font, uint32_t code_point);
/**
* Darken / lighten an area for a given time.
*
* @param area: The area to darken / lighten.
* @param time: The time in us to apply voltage to each pixel.
* @param color: 1: lighten, 0: darken.
*/
void epd_push_pixels(EpdRect area, short time, int color);
/**
* Base function for drawing an image on the screen.
* If It is very customizable, and the documentation below should be studied carefully.
* For simple applications, use the epdiy highlevel api in "epd_higlevel.h".
*
* @param area: The area of the screen to draw to.
* This can be imagined as shifting the origin of the frame buffer.
* @param data: A full framebuffer of display data.
* It's structure depends on the chosen `mode`.
* @param crop_to: Only draw a part of the frame buffer.
* Set to `epd_full_screen()` to draw the full buffer.
* @param mode: Specifies the Waveform used, the framebuffer format
* and additional information, like if the display is cleared.
* @param temperature: The temperature of the display in °C.
* Currently, this is unused by the default waveforms at can be
* set to room temperature, e.g. 20-25°C.
* @param drawn_lines: If not NULL, an array of at least the height of the
* image. Every line where the corresponding value in `lines` is `false` will be
* skipped.
* @param waveform: The waveform information to use for drawing.
* If you don't have special waveforms, use `EPD_BUILTIN_WAVEFORM`.
* @returns `EPD_DRAW_SUCCESS` on sucess, a combination of error flags otherwise.
*/
enum EpdDrawError IRAM_ATTR epd_draw_base(EpdRect area,
const uint8_t *data,
EpdRect crop_to,
enum EpdDrawMode mode,
int temperature,
const bool *drawn_lines,
const EpdWaveform *waveform);
/**
* Calculate a `MODE_PACKING_1PPB_DIFFERENCE` difference image
* from two `MODE_PACKING_2PPB` (4 bit-per-pixel) buffers.
* If you're using the epdiy highlevel api, this is handled by the update functions.
*
* @param to: The goal image as 4-bpp (`MODE_PACKING_2PPB`) framebuffer.
* @param from: The previous image as 4-bpp (`MODE_PACKING_2PPB`) framebuffer.
* @param crop_to: Only calculate the difference for a crop of the input framebuffers.
* The `interlaced` will not be modified outside the crop area.
* @param interlaced: The resulting difference image in `MODE_PACKING_1PPB_DIFFERENCE` format.
* @param dirty_lines: An array of at least `EPD_HEIGHT`.
* The positions corresponding to lines where `to` and `from` differ
* are set to `true`, otherwise to `false`.
* @param previously_white: If not NULL, it is set to `true`
* if the considered crop of the `from`-image is completely white.
* @param previously_black: If not NULL, it is set to `true`
* if the considered crop of the `from`-image is completely black.
* @returns The smallest rectangle containing all changed pixels.
*/
EpdRect epd_difference_image_cropped(
const uint8_t* to,
const uint8_t* from,
EpdRect crop_to,
uint8_t* interlaced,
bool* dirty_lines,
bool* previously_white,
bool* previously_black
);
/**
* Simplified version of `epd_difference_image_cropped()`, which considers the
* whole display frame buffer.
*
* See `epd_difference_image_cropped() for details.`
*/
EpdRect epd_difference_image(const uint8_t* to, const uint8_t* from, uint8_t* interlaced, bool* dirty_lines);
#ifdef __cplusplus
}
#endif

View File

@ -1,130 +0,0 @@
/**
* @file "epd_highlevel.h"
* @brief High-level API for drawing to e-paper displays.
*
* The functions in this library provide a simple way to manage updates of e-paper display.
* To use it, follow the steps below:
*
* 1. First, we declare a global object that manages the display state.
*
* EpdiyHighlevelState hl;
*
* 2. Then, the driver and framebuffers must be initialized.
*
* epd_init(EPD_LUT_1K);
* hl = epd_hl_init(EPD_BUILTIN_WAVEFORM);
*
* 3. Now, we can draw to the allocated framebuffer,
* using the draw and text functions defined in `epd_driver.h`.
* This will not yet update the display, but only its representation in memory.
*
* // A reference to the framebuffer
* uint8_t* fb = epd_hl_get_framebuffer(&hl);
*
* // draw a black rectangle
* EpdRect some_rect = {
* .x = 100,
* .y = 100,
* .width = 100,
* .height = 100
* };
* epd_fill_rect(some_rect, 0x0, fb);
*
* // write a message
* int cursor_x = 100;
* int cursor_y = 300;
* epd_write_default(&FiraSans, "Hello, World!", &cursor_x, &cursor_y, fb);
*
* // finally, update the display!
* int temperature = 25;
* epd_poweron();
* EpdDrawError err = epd_hl_update_screen(&hl, MODE_GC16, temperature);
* epd_poweroff();
*
* That's it! For many application, this will be enough.
* For special applications and requirements, have a
* closer look at the `epd_driver.h` header.
*/
#ifdef __cplusplus
extern "C" {
#endif
#include "epd_driver.h"
/// Holds the internal state of the high-level API.
typedef struct {
/// The "front" framebuffer object.
uint8_t* front_fb;
/// The "back" framebuffer object.
uint8_t* back_fb;
/// Buffer for holding the interlaced difference image.
uint8_t* difference_fb;
/// Tainted lines based on the last difference calculation.
bool* dirty_lines;
/// The waveform information to use.
const EpdWaveform* waveform;
} EpdiyHighlevelState;
/**
* Initialize a state object.
* This allocates two framebuffers and an update buffer for
* the display in the external PSRAM.
* In order to keep things simple, a chip reset is triggered if this fails.
*
* @param waveform: The waveform to use for updates.
* If you did not create your own, this will be `EPD_BUILTIN_WAVEFORM`.
* @returns An initialized state object.
*/
EpdiyHighlevelState epd_hl_init(const EpdWaveform* waveform);
/// Get a reference to the front framebuffer.
/// Use this to draw on the framebuffer before updating the screen with `epd_hl_update_screen()`.
uint8_t* epd_hl_get_framebuffer(EpdiyHighlevelState* state);
/**
* Update the EPD screen to match the content of the front frame buffer.
* Prior to this, power to the display must be enabled via `epd_poweron()`
* and should be disabled afterwards if no immediate additional updates follow.
*
* @param state: A reference to the `EpdiyHighlevelState` object used.
* @param mode: The update mode to use.
* Additional mode settings like the framebuffer format or
* previous display state are determined by the driver and must not be supplied here.
* In most cases, one of `MODE_GC16` and `MODE_GL16` should be used.
* @param temperature: Environmental temperature of the display in °C.
* @returns `EPD_DRAW_SUCCESS` on sucess, a combination of error flags otherwise.
*/
enum EpdDrawError epd_hl_update_screen(EpdiyHighlevelState* state, enum EpdDrawMode mode, int temperature);
/**
* Update an area of the screen to match the content of the front framebuffer.
* Supplying a small area to update can speed up the update process.
* Prior to this, power to the display must be enabled via `epd_poweron()`
* and should be disabled afterwards if no immediate additional updates follow.
*
* @param state: A reference to the `EpdiyHighlevelState` object used.
* @param mode: See `epd_hl_update_screen()`.
* @param temperature: Environmental temperature of the display in °C.
* @param area: Area of the screen to update.
* @returns `EPD_DRAW_SUCCESS` on sucess, a combination of error flags otherwise.
*/
enum EpdDrawError epd_hl_update_area(EpdiyHighlevelState* state, enum EpdDrawMode mode, int temperature, EpdRect area);
/**
* Reset the front framebuffer to a white state.
*
* @param state: A reference to the `EpdiyHighlevelState` object used.
*/
void epd_hl_set_all_white(EpdiyHighlevelState* state);
/**
* Bring the display to a fully white state and get rid of any
* remaining artifacts.
*/
void epd_fullclear(EpdiyHighlevelState* state, int temperature);
#ifdef __cplusplus
}
#endif

View File

@ -1,146 +0,0 @@
/**
* @file epd_internals.h
* @brief Internal definitions and auxiliary data types.
*
* Unless you want to extend the library itself (Which you are very welcome to do),
* you will most likely not need to know about this file.
*/
#ifndef EPD_INTERNALS_H
#define EPD_INTERNALS_H
#include <stdbool.h>
#include <stdint.h>
/// minimal draw time in ms for a frame layer,
/// which will allow all particles to set properly.
#ifndef MINIMUM_FRAME_TIME
#define MINIMUM_FRAME_TIME 12
#endif
/// Frame draw time for monochrome mode in 1/10 us.
#define MONOCHROME_FRAME_TIME 120
#if defined(CONFIG_EPD_DISPLAY_TYPE_ED097OC4) || \
defined(CONFIG_EPD_DISPLAY_TYPE_ED097TC2) || \
defined(CONFIG_EPD_DISPLAY_TYPE_ED097OC4_LQ)
/// Width of the display area in pixels.
#define EPD_WIDTH 1200
/// Height of the display area in pixels.
#define EPD_HEIGHT 825
#elif defined(CONFIG_EPD_DISPLAY_TYPE_ED133UT2)
#define EPD_WIDTH 1600
#define EPD_HEIGHT 1200
#elif defined(CONFIG_EPD_DISPLAY_TYPE_ED060SC4)
/// Width of the display area in pixels.
#define EPD_WIDTH 800
/// Height of the display area in pixels.
#define EPD_HEIGHT 600
#elif defined(CONFIG_EPD_DISPLAY_TYPE_ED060SCT)
/// Width of the display area in pixels.
#define EPD_WIDTH 800
/// Height of the display area in pixels.
#define EPD_HEIGHT 600
#elif defined(CONFIG_EPD_DISPLAY_TYPE_ED060XC3)
/// Width of the display area in pixels.
#define EPD_WIDTH 1024
/// Height of the display area in pixels.
#define EPD_HEIGHT 758
#elif defined(CONFIG_EPD_DISPLAY_TYPE_ED047TC1)
/// Width of the display area in pixels.
#define EPD_WIDTH 960
/// Height of the display area in pixels.
#define EPD_HEIGHT 540
#else
#error "no display type defined!"
#endif
typedef struct {
int phases;
const uint8_t* luts;
/// If we have timing information for the individual
/// phases, this is an array of the on-times for each phase.
/// Otherwise, this is NULL.
const int* phase_times;
} EpdWaveformPhases;
typedef struct {
uint8_t type;
uint8_t temp_ranges;
EpdWaveformPhases const **range_data;
} EpdWaveformMode;
typedef struct {
int min;
int max;
} EpdWaveformTempInterval;
typedef struct {
uint8_t num_modes;
uint8_t num_temp_ranges;
EpdWaveformMode const **mode_data;
EpdWaveformTempInterval const *temp_intervals;
} EpdWaveform;
extern const EpdWaveform ed047tc2;
extern const EpdWaveform epdiy_ED060SC4;
extern const EpdWaveform epdiy_ED097OC4;
extern const EpdWaveform epdiy_ED047TC1;
extern const EpdWaveform epdiy_ED097TC2;
extern const EpdWaveform epdiy_ED060XC3;
extern const EpdWaveform epdiy_ED060SCT;
extern const EpdWaveform epdiy_ED133UT2;
#if defined(CONFIG_EPD_DISPLAY_TYPE_ED047TC1)
//#define EPD_BUILTIN_WAVEFORM &epdiy_ED047TC1
#define EPD_BUILTIN_WAVEFORM &ed047tc2
#elif defined(CONFIG_EPD_DISPLAY_TYPE_ED060SC4)
#define EPD_BUILTIN_WAVEFORM &epdiy_ED060SC4
#elif defined(CONFIG_EPD_DISPLAY_TYPE_ED060XC3)
#define EPD_BUILTIN_WAVEFORM &epdiy_ED060XC3
#elif defined(CONFIG_EPD_DISPLAY_TYPE_ED060SCT)
#define EPD_BUILTIN_WAVEFORM &epdiy_ED060SCT
#elif defined(CONFIG_EPD_DISPLAY_TYPE_ED097OC4) || defined(CONFIG_EPD_DISPLAY_TYPE_ED097OC4_LQ)
#define EPD_BUILTIN_WAVEFORM &epdiy_ED097OC4
#elif defined(CONFIG_EPD_DISPLAY_TYPE_ED097TC2)
#define EPD_BUILTIN_WAVEFORM &epdiy_ED097TC2
#elif defined (CONFIG_EPD_DISPLAY_TYPE_ED133UT2)
#define EPD_BUILTIN_WAVEFORM &epdiy_ED133UT2
#endif
/// Font data stored PER GLYPH
typedef struct {
uint8_t width; ///< Bitmap dimensions in pixels
uint8_t height; ///< Bitmap dimensions in pixels
uint8_t advance_x; ///< Distance to advance cursor (x axis)
int16_t left; ///< X dist from cursor pos to UL corner
int16_t top; ///< Y dist from cursor pos to UL corner
uint16_t compressed_size; ///< Size of the zlib-compressed font data.
uint32_t data_offset; ///< Pointer into EpdFont->bitmap
} EpdGlyph;
/// Glyph interval structure
typedef struct {
uint32_t first; ///< The first unicode code point of the interval
uint32_t last; ///< The last unicode code point of the interval
uint32_t offset; ///< Index of the first code point into the glyph array
} EpdUnicodeInterval;
/// Data stored for FONT AS A WHOLE
typedef struct {
const uint8_t *bitmap; ///< Glyph bitmaps, concatenated
const EpdGlyph *glyph; ///< Glyph array
const EpdUnicodeInterval *intervals; ///< Valid unicode intervals for this font
uint32_t interval_count; ///< Number of unicode intervals.
bool compressed; ///< Does this font use compressed glyph bitmaps?
uint8_t advance_y; ///< Newline distance (y axis)
int ascender; ///< Maximal height of a glyph above the base line
int descender; ///< Maximal height of a glyph below the base line
} EpdFont;
#endif // EPD_INTERNALS_H

View File

@ -1,617 +0,0 @@
#include "lut.h"
#include "display_ops.h"
#include "esp_log.h"
#include "freertos/task.h"
#include <string.h>
/*
* Build Lookup tables and translate via LUTs.
* WARNING: These functions must only ever write to internal memory,
* Since we disable the PSRAM workaround here for performance reasons.
*/
/* Python script for generating the 1bpp lookup table:
* for i in range(256):
number = 0;
for b in range(8):
if not (i & (b << 1)):
number |= 1 << (2*b)
print ('0x%04x,'%number)
*/
const static uint32_t lut_1bpp_black[256] = {
0x5555, 0x5554, 0x5551, 0x5550, 0x5545, 0x5544, 0x5541, 0x5540, 0x5515,
0x5514, 0x5511, 0x5510, 0x5505, 0x5504, 0x5501, 0x5500, 0x5455, 0x5454,
0x5451, 0x5450, 0x5445, 0x5444, 0x5441, 0x5440, 0x5415, 0x5414, 0x5411,
0x5410, 0x5405, 0x5404, 0x5401, 0x5400, 0x5155, 0x5154, 0x5151, 0x5150,
0x5145, 0x5144, 0x5141, 0x5140, 0x5115, 0x5114, 0x5111, 0x5110, 0x5105,
0x5104, 0x5101, 0x5100, 0x5055, 0x5054, 0x5051, 0x5050, 0x5045, 0x5044,
0x5041, 0x5040, 0x5015, 0x5014, 0x5011, 0x5010, 0x5005, 0x5004, 0x5001,
0x5000, 0x4555, 0x4554, 0x4551, 0x4550, 0x4545, 0x4544, 0x4541, 0x4540,
0x4515, 0x4514, 0x4511, 0x4510, 0x4505, 0x4504, 0x4501, 0x4500, 0x4455,
0x4454, 0x4451, 0x4450, 0x4445, 0x4444, 0x4441, 0x4440, 0x4415, 0x4414,
0x4411, 0x4410, 0x4405, 0x4404, 0x4401, 0x4400, 0x4155, 0x4154, 0x4151,
0x4150, 0x4145, 0x4144, 0x4141, 0x4140, 0x4115, 0x4114, 0x4111, 0x4110,
0x4105, 0x4104, 0x4101, 0x4100, 0x4055, 0x4054, 0x4051, 0x4050, 0x4045,
0x4044, 0x4041, 0x4040, 0x4015, 0x4014, 0x4011, 0x4010, 0x4005, 0x4004,
0x4001, 0x4000, 0x1555, 0x1554, 0x1551, 0x1550, 0x1545, 0x1544, 0x1541,
0x1540, 0x1515, 0x1514, 0x1511, 0x1510, 0x1505, 0x1504, 0x1501, 0x1500,
0x1455, 0x1454, 0x1451, 0x1450, 0x1445, 0x1444, 0x1441, 0x1440, 0x1415,
0x1414, 0x1411, 0x1410, 0x1405, 0x1404, 0x1401, 0x1400, 0x1155, 0x1154,
0x1151, 0x1150, 0x1145, 0x1144, 0x1141, 0x1140, 0x1115, 0x1114, 0x1111,
0x1110, 0x1105, 0x1104, 0x1101, 0x1100, 0x1055, 0x1054, 0x1051, 0x1050,
0x1045, 0x1044, 0x1041, 0x1040, 0x1015, 0x1014, 0x1011, 0x1010, 0x1005,
0x1004, 0x1001, 0x1000, 0x0555, 0x0554, 0x0551, 0x0550, 0x0545, 0x0544,
0x0541, 0x0540, 0x0515, 0x0514, 0x0511, 0x0510, 0x0505, 0x0504, 0x0501,
0x0500, 0x0455, 0x0454, 0x0451, 0x0450, 0x0445, 0x0444, 0x0441, 0x0440,
0x0415, 0x0414, 0x0411, 0x0410, 0x0405, 0x0404, 0x0401, 0x0400, 0x0155,
0x0154, 0x0151, 0x0150, 0x0145, 0x0144, 0x0141, 0x0140, 0x0115, 0x0114,
0x0111, 0x0110, 0x0105, 0x0104, 0x0101, 0x0100, 0x0055, 0x0054, 0x0051,
0x0050, 0x0045, 0x0044, 0x0041, 0x0040, 0x0015, 0x0014, 0x0011, 0x0010,
0x0005, 0x0004, 0x0001, 0x0000};
// Timestamp when the last frame draw was started.
// This is used to enforce a minimum frame draw time, allowing
// all pixels to set.
static uint64_t last_frame_start = 0;
inline int min(int x, int y) { return x < y ? x : y; }
inline int max(int x, int y) { return x > y ? x : y; }
// status tracker for row skipping
uint32_t skipping;
// output a row to the display.
void IRAM_ATTR write_row(uint32_t output_time_dus) {
epd_output_row(output_time_dus);
skipping = 0;
}
// skip a display row
void IRAM_ATTR skip_row(uint8_t pipeline_finish_time) {
// output previously loaded row, fill buffer with no-ops.
if (skipping < 2) {
memset(epd_get_current_buffer(), 0x00, EPD_LINE_BYTES);
epd_output_row(pipeline_finish_time);
} else {
epd_skip();
}
skipping++;
}
void IRAM_ATTR reorder_line_buffer(uint32_t *line_data) {
for (uint32_t i = 0; i < EPD_LINE_BYTES / 4; i++) {
uint32_t val = *line_data;
*(line_data++) = val >> 16 | ((val & 0x0000FFFF) << 16);
}
}
static void IRAM_ATTR bit_shift_buffer_right(uint8_t *buf, uint32_t len,
int shift) {
uint8_t carry = 0xFF << (8 - shift);
for (uint32_t i = 0; i < len; i++) {
uint8_t val = buf[i];
buf[i] = (val >> shift) | carry;
carry = val << (8 - shift);
}
}
static void IRAM_ATTR nibble_shift_buffer_right(uint8_t *buf, uint32_t len) {
uint8_t carry = 0xF;
for (uint32_t i = 0; i < len; i++) {
uint8_t val = buf[i];
buf[i] = (val << 4) | carry;
carry = (val & 0xF0) >> 4;
}
}
///////////////////////////// Looking up EPD Pixels
//////////////////////////////////
static void IRAM_ATTR calc_epd_input_1bpp(const uint32_t *line_data,
uint8_t *epd_input,
const uint8_t *lut) {
uint32_t *wide_epd_input = (uint32_t *)epd_input;
uint8_t *data_ptr = (uint8_t *)line_data;
uint32_t *lut_32 = (uint32_t *)lut;
// this is reversed for little-endian, but this is later compensated
// through the output peripheral.
for (uint32_t j = 0; j < EPD_WIDTH / 16; j++) {
uint8_t v1 = *(data_ptr++);
uint8_t v2 = *(data_ptr++);
wide_epd_input[j] = (lut_32[v1] << 16) | lut_32[v2];
}
}
static void IRAM_ATTR
calc_epd_input_4bpp_lut_64k(const uint32_t *line_data, uint8_t *epd_input,
const uint8_t *conversion_lut) {
uint32_t *wide_epd_input = (uint32_t *)epd_input;
const uint16_t *line_data_16 = (const uint16_t *)line_data;
// this is reversed for little-endian, but this is later compensated
// through the output peripheral.
for (uint32_t j = 0; j < EPD_WIDTH / 16; j++) {
uint16_t v1 = *(line_data_16++);
uint16_t v2 = *(line_data_16++);
uint16_t v3 = *(line_data_16++);
uint16_t v4 = *(line_data_16++);
uint32_t pixel = conversion_lut[v1] << 16 | conversion_lut[v2] << 24 |
conversion_lut[v3] | conversion_lut[v4] << 8;
wide_epd_input[j] = pixel;
}
}
/**
* Look up 4 pixels of a differential image.
*/
static inline uint8_t
lookup_differential_pixels(uint32_t in, const uint8_t *conversion_lut) {
uint8_t out = conversion_lut[in & 0xFF];
in = in >> 8;
out |= (conversion_lut + 0x100)[in & 0xFF];
in = in >> 8;
out |= (conversion_lut + 0x200)[in & 0xFF];
in = in >> 8;
out |= (conversion_lut + 0x300)[in];
return out;
}
/**
* Calculate EPD input for a difference image with one pixel per byte.
*/
static void IRAM_ATTR calc_epd_input_1ppB(const uint32_t *ld,
uint8_t *epd_input,
const uint8_t *conversion_lut) {
// this is reversed for little-endian, but this is later compensated
// through the output peripheral.
for (uint32_t j = 0; j < EPD_WIDTH / 4; j += 4) {
epd_input[j + 2] = lookup_differential_pixels(*(ld++), conversion_lut);
epd_input[j + 3] = lookup_differential_pixels(*(ld++), conversion_lut);
epd_input[j + 0] = lookup_differential_pixels(*(ld++), conversion_lut);
epd_input[j + 1] = lookup_differential_pixels(*(ld++), conversion_lut);
}
}
/**
* Look up 4 pixels in a 1K LUT with fixed "from" value.
*/
static inline uint8_t lookup_pixels_4bpp_1k(uint16_t in,
const uint8_t *conversion_lut,
uint8_t from) {
uint8_t v;
uint8_t out;
v = ((in << 4) | from);
out = conversion_lut[v & 0xFF];
v = ((in & 0xF0) | from);
out |= (conversion_lut + 0x100)[v & 0xFF];
in = in >> 8;
v = ((in << 4) | from);
out |= (conversion_lut + 0x200)[v & 0xFF];
v = ((in & 0xF0) | from);
out |= (conversion_lut + 0x300)[v];
return out;
}
/**
* Calculate EPD input for a 4bpp buffer, but with a difference image LUT.
* This is used for small-LUT mode.
*/
static void IRAM_ATTR calc_epd_input_4bpp_1k_lut(const uint32_t *ld,
uint8_t *epd_input,
const uint8_t *conversion_lut,
uint8_t from) {
uint16_t *ptr = (uint16_t *)ld;
// this is reversed for little-endian, but this is later compensated
// through the output peripheral.
for (uint32_t j = 0; j < EPD_WIDTH / 4; j += 4) {
epd_input[j + 2] = lookup_pixels_4bpp_1k(*(ptr++), conversion_lut, from);
epd_input[j + 3] = lookup_pixels_4bpp_1k(*(ptr++), conversion_lut, from);
epd_input[j + 0] = lookup_pixels_4bpp_1k(*(ptr++), conversion_lut, from);
epd_input[j + 1] = lookup_pixels_4bpp_1k(*(ptr++), conversion_lut, from);
}
}
static void IRAM_ATTR calc_epd_input_4bpp_1k_lut_white(
const uint32_t *ld, uint8_t *epd_input, const uint8_t *conversion_lut) {
calc_epd_input_4bpp_1k_lut(ld, epd_input, conversion_lut, 0xF);
}
static void IRAM_ATTR calc_epd_input_4bpp_1k_lut_black(
const uint32_t *ld, uint8_t *epd_input, const uint8_t *conversion_lut) {
calc_epd_input_4bpp_1k_lut(ld, epd_input, conversion_lut, 0x0);
}
///////////////////////////// Calculate Lookup Tables
//////////////////////////////////
/**
* Unpack the waveform data into a lookup table, with bit shifted copies.
*/
static void IRAM_ATTR waveform_lut(const EpdWaveform *waveform, uint8_t *lut,
uint8_t mode, int range, int frame) {
const uint8_t *p_lut =
waveform->mode_data[mode]->range_data[range]->luts + (16 * 4 * frame);
for (uint8_t to = 0; to < 16; to++) {
for (uint8_t from_packed = 0; from_packed < 4; from_packed++) {
uint8_t index = (to << 4) | (from_packed * 4);
uint8_t packed = *(p_lut++);
lut[index] = (packed >> 6) & 3;
lut[index + 1] = (packed >> 4) & 3;
lut[index + 2] = (packed >> 2) & 3;
lut[index + 3] = (packed >> 0) & 3;
// printf("%2X%2X%2X%2X (%d)", lut[index], lut[index + 1], lut[index + 2],
// lut[index + 3], index);
}
// printf("\n");
}
uint32_t index = 0x100;
for (uint8_t s = 2; s <= 6; s += 2) {
for (int i = 0; i < 0x100; i++) {
lut[index] = lut[index % 0x100] << s;
index++;
}
}
}
/**
* Build a 16-bit LUT from the waveform if the previous color is
* known, e.g. all white or all black.
* This LUT is use to look up 4 pixels at once, as with the epdiy LUT.
*/
static void IRAM_ATTR waveform_lut_static_from(const EpdWaveform *waveform,
uint8_t *lut, uint8_t from,
uint8_t mode, int range,
int frame) {
const uint8_t *p_lut =
waveform->mode_data[mode]->range_data[range]->luts + (16 * 4 * frame);
/// index into the packed "from" row
uint8_t fi = from >> 2;
/// bit shift amount for the packed "from" row
uint8_t fs = 6 - 2 * (from & 3);
// populate the first 4096 bytes
uint8_t v1 = 0;
uint32_t s1 = 0;
for (uint8_t t2 = 0; t2 < 16; t2++) {
uint8_t v2 = ((p_lut[(t2 << 2) + fi] >> fs) & 0x03) << 4;
uint32_t s2 = t2 << 8;
for (uint8_t t3 = 0; t3 < 16; t3++) {
uint8_t v3 = ((p_lut[(t3 << 2) + fi] >> fs) & 0x03) << 2;
uint32_t s3 = t3 << 4;
for (uint8_t t4 = 0; t4 < 16; t4++) {
uint8_t v4 = ((p_lut[(t4 << 2) + fi] >> fs) & 0x03) << 0;
uint32_t s4 = t4;
lut[s1 | s2 | s3 | s4] = v1 | v2 | v3 | v4;
}
}
}
// now just copy and the first 4096 bytes and add the upper two bits
for (uint8_t t1 = 1; t1 < 16; t1++) {
memcpy(&lut[t1 << 12], lut, 1 << 12);
}
for (int i = 0; i < 16; i++) {
uint32_t v1 = ((p_lut[(i << 2) + fi] >> fs) & 0x03);
uint32_t mask = (v1 << 30) | (v1 << 22) | (v1 << 14) | (v1 << 6);
for (int j = 0; j < 16 * 16 * 16 / 4; j++) {
((uint32_t *)lut)[(i << 10) + j] |= mask;
}
}
}
void IRAM_ATTR provide_out(OutputParams *params) {
while (true) {
// line must be able to hold 2-pixel-per-byte or 1-pixel-per-byte data
uint8_t line[EPD_WIDTH];
memset(line, 255, EPD_WIDTH);
xSemaphoreTake(params->start_smphr, portMAX_DELAY);
EpdRect area = params->area;
const uint8_t *ptr = params->data_ptr;
const bool crop = (params->crop_to.width > 0 && params->crop_to.height > 0);
// number of pixels per byte of input data
int ppB = 0;
int bytes_per_line = 0;
int width_divider = 0;
if (params->mode & MODE_PACKING_1PPB_DIFFERENCE) {
ppB = 1;
bytes_per_line = area.width;
width_divider = 1;
} else if (params->mode & MODE_PACKING_2PPB) {
ppB = 2;
bytes_per_line = area.width / 2 + area.width % 2;
width_divider = 2;
} else if (params->mode & MODE_PACKING_8PPB) {
ppB = 8;
bytes_per_line = (area.width / 8 + (area.width % 8 > 0));
width_divider = 8;
} else {
params->error |= EPD_DRAW_INVALID_PACKING_MODE;
}
int crop_x = (crop ? params->crop_to.x : 0);
int crop_y = (crop ? params->crop_to.y : 0);
int crop_w = (crop ? params->crop_to.width : 0);
int crop_h = (crop ? params->crop_to.height : 0);
// Adjust for negative starting coordinates with optional crop
if (area.x - crop_x < 0) {
ptr += -(area.x - crop_x) / width_divider;
}
if (area.y - crop_y < 0) {
ptr += -(area.y - crop_y) * bytes_per_line;
}
// calculate start and end row with crop
int min_y = area.y + crop_y;
int max_y = min(min_y + (crop ? crop_h : area.height), area.height);
for (int i = 0; i < EPD_HEIGHT; i++) {
if (i < min_y || i >= max_y) {
continue;
}
if (params->drawn_lines != NULL && !params->drawn_lines[i - area.y]) {
ptr += bytes_per_line;
continue;
}
uint32_t *lp = (uint32_t *)line;
bool shifted = false;
if (area.width == EPD_WIDTH && area.x == 0 && !crop && !params->error) {
lp = (uint32_t *)ptr;
ptr += bytes_per_line;
} else if (!params->error) {
uint8_t *buf_start = (uint8_t *)line;
uint32_t line_bytes = bytes_per_line;
int min_x = area.x + crop_x;
if (min_x >= 0) {
buf_start += min_x / width_divider;
} else {
// reduce line_bytes to actually used bytes
// ptr was already adjusted above
line_bytes += min_x / width_divider;
}
line_bytes = min(line_bytes, EPD_WIDTH / width_divider -
(uint32_t)(buf_start - line));
memcpy(buf_start, ptr, line_bytes);
ptr += bytes_per_line;
int cropped_width = (crop ? crop_w : area.width);
/// consider half-byte shifts in two-pixel-per-Byte mode.
if (ppB == 2) {
// mask last nibble for uneven width
if (cropped_width % 2 == 1 &&
min_x / 2 + cropped_width / 2 + 1 < EPD_WIDTH) {
*(buf_start + line_bytes - 1) |= 0xF0;
}
if (area.x % 2 == 1 && !(crop_x % 2 == 1) && min_x < EPD_WIDTH) {
shifted = true;
uint32_t remaining =
(uint32_t)line + EPD_WIDTH / 2 - (uint32_t)buf_start;
uint32_t to_shift = min(line_bytes + 1, remaining);
// shift one nibble to right
nibble_shift_buffer_right(buf_start, to_shift);
}
// consider bit shifts in bit buffers
} else if (ppB == 8) {
// mask last n bits if width is not divisible by 8
if (cropped_width % 8 != 0 && bytes_per_line + 1 < EPD_WIDTH) {
uint8_t mask = 0;
for (int s = 0; s < cropped_width % 8; s++) {
mask = (mask << 1) | 1;
}
*(buf_start + line_bytes - 1) |= ~mask;
}
if (min_x % 8 != 0 && min_x < EPD_WIDTH) {
// shift to right
shifted = true;
uint32_t remaining =
(uint32_t)line + EPD_WIDTH / 8 - (uint32_t)buf_start;
uint32_t to_shift = min(line_bytes + 1, remaining);
bit_shift_buffer_right(buf_start, to_shift, min_x % 8);
}
}
lp = (uint32_t *)line;
}
xQueueSendToBack(*params->output_queue, lp, portMAX_DELAY);
if (shifted) {
memset(line, 255, EPD_WIDTH / width_divider);
}
}
xSemaphoreGive(params->done_smphr);
}
}
/**
* Set all pixels not in [xmin,xmax) to nop in the current line buffer.
*/
static void IRAM_ATTR mask_line_buffer(int xmin, int xmax) {
// lower bound to where byte order is not an issue.
int memset_start = (xmin / 16) * 4;
int memset_end = min(((xmax + 15) / 16) * 4, EPD_LINE_BYTES);
uint8_t *lb = epd_get_current_buffer();
// memset the areas where order is not an issue
memset(lb, 0, memset_start);
memset(lb + memset_end, 0, EPD_LINE_BYTES - memset_end);
const int offset_table[4] = {2, 3, 0, 1};
// mask unused pixels at the start of the output interval
uint8_t line_start_mask = 0xFF << (2 * (xmin % 4));
uint8_t line_end_mask = 0xFF >> (8 - 2 * (xmax % 4));
// number of full bytes to mask
int lower_full_bytes = max(0, (xmin / 4 - memset_start));
int upper_full_bytes = max(0, (memset_end - ((xmax + 3) / 4)));
assert(lower_full_bytes <= 3);
assert(upper_full_bytes <= 3);
assert(memset_end >= 4);
// mask full bytes
for (int i = 0; i < lower_full_bytes; i++) {
lb[memset_start + offset_table[i]] = 0x0;
}
for (int i = 0; i < upper_full_bytes; i++) {
lb[memset_end - 4 + offset_table[3 - i]] = 0x0;
}
// mask partial bytes
if ((memset_start + lower_full_bytes) * 4 < xmin) {
epd_get_current_buffer()[memset_start + offset_table[lower_full_bytes]] &=
line_start_mask;
}
if ((memset_end - upper_full_bytes) * 4 > xmax) {
epd_get_current_buffer()[memset_end - 4 +
offset_table[3 - upper_full_bytes]] &=
line_end_mask;
}
}
void IRAM_ATTR busy_delay(uint32_t cycles);
static enum EpdDrawError calculate_lut(OutputParams *params) {
enum EpdDrawMode mode = params->mode;
enum EpdDrawMode selected_mode = mode & 0x3F;
// two pixel per byte packing with only target color
if (mode & MODE_PACKING_2PPB && mode & PREVIOUSLY_WHITE &&
params->conversion_lut_size == (1 << 16)) {
waveform_lut_static_from(params->waveform, params->conversion_lut, 0x0F,
params->waveform_index, params->waveform_range,
params->frame);
} else if (mode & MODE_PACKING_2PPB && mode & PREVIOUSLY_BLACK &&
params->conversion_lut_size == (1 << 16)) {
waveform_lut_static_from(params->waveform, params->conversion_lut, 0x00,
params->waveform_index, params->waveform_range,
params->frame);
// one pixel per byte with from and to colors
} else if (mode & MODE_PACKING_1PPB_DIFFERENCE ||
(mode & MODE_PACKING_2PPB &&
params->conversion_lut_size == (1 << 10))) {
waveform_lut(params->waveform, params->conversion_lut,
params->waveform_index, params->waveform_range, params->frame);
// 1bit per pixel monochrome with only target color
} else if (mode & MODE_PACKING_8PPB &&
selected_mode == MODE_EPDIY_MONOCHROME) {
// FIXME: Pack into waveform?
if (mode & PREVIOUSLY_WHITE) {
memcpy(params->conversion_lut, lut_1bpp_black, sizeof(lut_1bpp_black));
} else if (mode & PREVIOUSLY_BLACK) {
// FIXME: implement!
// memcpy(params->conversion_lut, lut_1bpp_white, sizeof(lut_1bpp_white));
return EPD_DRAW_LOOKUP_NOT_IMPLEMENTED;
} else {
return EPD_DRAW_LOOKUP_NOT_IMPLEMENTED;
}
// unknown format.
} else {
return EPD_DRAW_LOOKUP_NOT_IMPLEMENTED;
}
return EPD_DRAW_SUCCESS;
}
void IRAM_ATTR feed_display(OutputParams *params) {
while (true) {
xSemaphoreTake(params->start_smphr, portMAX_DELAY);
skipping = 0;
EpdRect area = params->area;
enum EpdDrawMode mode = params->mode;
int frame_time = params->frame_time;
params->error |= calculate_lut(params);
void (*input_calc_func)(const uint32_t *, uint8_t *, const uint8_t *) =
NULL;
if (mode & MODE_PACKING_2PPB) {
if (params->conversion_lut_size == 1024) {
if (mode & PREVIOUSLY_WHITE) {
input_calc_func = &calc_epd_input_4bpp_1k_lut_white;
} else if (mode & PREVIOUSLY_BLACK) {
input_calc_func = &calc_epd_input_4bpp_1k_lut_black;
} else {
params->error |= EPD_DRAW_LOOKUP_NOT_IMPLEMENTED;
}
} else if (params->conversion_lut_size == (1 << 16)) {
input_calc_func = &calc_epd_input_4bpp_lut_64k;
} else {
params->error |= EPD_DRAW_LOOKUP_NOT_IMPLEMENTED;
}
} else if (mode & MODE_PACKING_1PPB_DIFFERENCE) {
input_calc_func = &calc_epd_input_1ppB;
} else if (mode & MODE_PACKING_8PPB) {
input_calc_func = &calc_epd_input_1bpp;
} else {
params->error |= EPD_DRAW_LOOKUP_NOT_IMPLEMENTED;
}
// Adjust min and max row for crop.
const bool crop = (params->crop_to.width > 0 && params->crop_to.height > 0);
int crop_y = (crop ? params->crop_to.y : 0);
int min_y = area.y + crop_y;
int max_y = min(min_y + (crop ? params->crop_to.height : area.height), area.height);
// interval of the output line that is needed
// FIXME: only lookup needed parts
int line_start_x = area.x + (crop ? params->crop_to.x : 0);
int line_end_x = line_start_x + (crop ? params->crop_to.width : area.width);
line_start_x = min(max(line_start_x, 0), EPD_WIDTH);
line_end_x = min(max(line_end_x, 0), EPD_WIDTH);
uint64_t now = esp_timer_get_time();
uint64_t diff = (now - last_frame_start) / 1000;
if (diff < MINIMUM_FRAME_TIME) {
vTaskDelay(MINIMUM_FRAME_TIME - diff);
}
last_frame_start = esp_timer_get_time();
epd_start_frame();
for (int i = 0; i < EPD_HEIGHT; i++) {
if (i < min_y || i >= max_y) {
skip_row(frame_time);
continue;
}
if (params->drawn_lines != NULL && !params->drawn_lines[i - area.y]) {
skip_row(frame_time);
continue;
}
uint8_t output[EPD_WIDTH];
xQueueReceive(*params->output_queue, output, portMAX_DELAY);
if (!params->error) {
(*input_calc_func)((uint32_t *)output, epd_get_current_buffer(),
params->conversion_lut);
if (line_start_x > 0 || line_end_x < EPD_WIDTH) {
mask_line_buffer(line_start_x, line_end_x);
}
}
write_row(frame_time);
}
if (!skipping) {
// Since we "pipeline" row output, we still have to latch out the last
// row.
write_row(frame_time);
}
epd_end_frame();
xSemaphoreGive(params->done_smphr);
}
}

View File

@ -1,54 +0,0 @@
#pragma once
#include "esp_attr.h"
#include <stdint.h>
#include "freertos/FreeRTOS.h"
#include "freertos/semphr.h"
#include "freertos/queue.h"
#include "epd_driver.h"
// number of bytes needed for one line of EPD pixel data.
#define EPD_LINE_BYTES EPD_WIDTH / 4
///////////////////////////// Utils /////////////////////////////////////
/*
* Reorder the output buffer to account for I2S FIFO order.
*/
void IRAM_ATTR reorder_line_buffer(uint32_t *line_data);
typedef struct {
const uint8_t *data_ptr;
EpdRect crop_to;
SemaphoreHandle_t done_smphr;
SemaphoreHandle_t start_smphr;
EpdRect area;
int frame;
/// index of the waveform mode when using vendor waveforms.
/// This is not necessarily the mode number if the waveform header
//only contains a selection of modes!
int waveform_index;
/// waveform range when using vendor waveforms
int waveform_range;
/// Draw time for the current frame in 1/10ths of us.
int frame_time;
const EpdWaveform* waveform;
enum EpdDrawMode mode;
enum EpdDrawError error;
const bool *drawn_lines;
// Queue of input data lines
QueueHandle_t* output_queue;
// Lookup table size.
size_t conversion_lut_size;
// Lookup table space.
uint8_t* conversion_lut;
} OutputParams;
void IRAM_ATTR feed_display(OutputParams *params);
void IRAM_ATTR provide_out(OutputParams *params);
void IRAM_ATTR write_row(uint32_t output_time_dus);
void IRAM_ATTR skip_row(uint8_t pipeline_finish_time);

View File

@ -1,382 +0,0 @@
#include "epd_temperature.h"
#include "display_ops.h"
#include "epd_driver.h"
#include "include/epd_driver.h"
#include "include/epd_internals.h"
#include "lut.h"
#include "driver/rtc_io.h"
#include "esp_types.h"
#include "esp_log.h"
#include "freertos/FreeRTOS.h"
#include "freertos/queue.h"
#include "freertos/semphr.h"
#include "freertos/task.h"
#include "xtensa/core-macros.h"
#include <string.h>
inline int min(int x, int y) { return x < y ? x : y; }
inline int max(int x, int y) { return x > y ? x : y; }
const int clear_cycle_time = 12;
const int DEFAULT_FRAME_TIME = 120;
#define RTOS_ERROR_CHECK(x) \
do { \
esp_err_t __err_rc = (x); \
if (__err_rc != pdPASS) { \
abort(); \
} \
} while (0)
#define CLEAR_BYTE 0B10101010
#define DARK_BYTE 0B01010101
// Queue of input data lines
static QueueHandle_t output_queue;
static OutputParams fetch_params;
static OutputParams feed_params;
// Space to use for the EPD output lookup table, which
// is calculated for each cycle.
static uint8_t* conversion_lut;
void epd_push_pixels(EpdRect area, short time, int color) {
uint8_t row[EPD_LINE_BYTES] = {0};
const uint8_t color_choice[4] = {DARK_BYTE, CLEAR_BYTE, 0x00, 0xFF};
for (uint32_t i = 0; i < area.width; i++) {
uint32_t position = i + area.x % 4;
uint8_t mask = color_choice[color] & (0b00000011 << (2 * (position % 4)));
row[area.x / 4 + position / 4] |= mask;
}
reorder_line_buffer((uint32_t *)row);
epd_start_frame();
for (int i = 0; i < EPD_HEIGHT; i++) {
// before are of interest: skip
if (i < area.y) {
skip_row(time);
// start area of interest: set row data
} else if (i == area.y) {
epd_switch_buffer();
memcpy(epd_get_current_buffer(), row, EPD_LINE_BYTES);
epd_switch_buffer();
memcpy(epd_get_current_buffer(), row, EPD_LINE_BYTES);
write_row(time * 10);
// load nop row if done with area
} else if (i >= area.y + area.height) {
skip_row(time);
// output the same as before
} else {
write_row(time * 10);
}
}
// Since we "pipeline" row output, we still have to latch out the last row.
write_row(time * 10);
epd_end_frame();
}
///////////////////////////// Coordination ///////////////////////////////
/**
* Find the waveform temperature range index for a given temperature in °C.
* If no range in the waveform data fits the given temperature, return the
* closest one.
* Returns -1 if the waveform does not contain any temperature range.
*/
int waveform_temp_range_index(const EpdWaveform* waveform, int temperature) {
int idx = 0;
if (waveform->num_temp_ranges == 0) {
return -1;
}
while (idx < waveform->num_temp_ranges - 1
&& waveform->temp_intervals[idx].min < temperature) {
idx++;
}
return idx;
}
//////////////////////////////// API Procedures //////////////////////////////////
static int get_waveform_index(const EpdWaveform* waveform, enum EpdDrawMode mode) {
for (int i=0; i < waveform->num_modes; i++) {
if (waveform->mode_data[i]->type == (mode & 0x3F)) {
return i;
}
}
return -1;
}
enum EpdDrawError IRAM_ATTR epd_draw_base(EpdRect area,
const uint8_t *data,
EpdRect crop_to,
enum EpdDrawMode mode,
int temperature,
const bool *drawn_lines,
const EpdWaveform *waveform) {
uint8_t line[EPD_WIDTH / 2];
memset(line, 255, EPD_WIDTH / 2);
int waveform_range = waveform_temp_range_index(waveform, temperature);
if (waveform_range < 0) {
return EPD_DRAW_NO_PHASES_AVAILABLE;
}
int waveform_index = 0;
uint8_t frame_count = 0;
const EpdWaveformPhases* waveform_phases = NULL;
// no waveform required for monochrome mode
if (!(mode & MODE_EPDIY_MONOCHROME)) {
waveform_index = get_waveform_index(waveform, mode);
if (waveform_index < 0) {
return EPD_DRAW_MODE_NOT_FOUND;
}
waveform_phases = waveform->mode_data[waveform_index]
->range_data[waveform_range];
// FIXME: error if not present
frame_count = waveform_phases->phases;
} else {
frame_count = 1;
}
if (crop_to.width < 0 || crop_to.height < 0) {
return EPD_DRAW_INVALID_CROP;
}
const bool crop = (crop_to.width > 0 && crop_to.height > 0);
if (crop && (crop_to.width > area.width
|| crop_to.height > area.height
|| crop_to.x > area.width
|| crop_to.y > area.height)) {
return EPD_DRAW_INVALID_CROP;
}
for (uint8_t k = 0; k < frame_count; k++) {
int frame_time = DEFAULT_FRAME_TIME;
if (waveform_phases != NULL && waveform_phases->phase_times != NULL) {
frame_time = waveform_phases->phase_times[k];
}
if (mode & MODE_EPDIY_MONOCHROME) {
frame_time = MONOCHROME_FRAME_TIME;
}
fetch_params.area = area;
// IMPORTANT: This must only ever read from PSRAM,
// Since the PSRAM workaround is disabled for lut.c
fetch_params.data_ptr = data;
fetch_params.crop_to = crop_to;
fetch_params.frame = k;
fetch_params.waveform_range = waveform_range;
fetch_params.waveform_index = waveform_index;
fetch_params.frame_time = frame_time;
fetch_params.mode = mode;
fetch_params.waveform = waveform;
fetch_params.error = EPD_DRAW_SUCCESS;
fetch_params.drawn_lines = drawn_lines;
fetch_params.output_queue = &output_queue;
feed_params.area = area;
feed_params.data_ptr = data;
feed_params.crop_to = crop_to;
feed_params.frame = k;
feed_params.frame_time = frame_time;
feed_params.waveform_range = waveform_range;
feed_params.waveform_index = waveform_index;
feed_params.mode = mode;
feed_params.waveform = waveform;
feed_params.error = EPD_DRAW_SUCCESS;
feed_params.drawn_lines = drawn_lines;
feed_params.output_queue = &output_queue;
xSemaphoreGive(fetch_params.start_smphr);
xSemaphoreGive(feed_params.start_smphr);
xSemaphoreTake(fetch_params.done_smphr, portMAX_DELAY);
xSemaphoreTake(feed_params.done_smphr, portMAX_DELAY);
enum EpdDrawError all_errors = fetch_params.error | feed_params.error;
if (all_errors != EPD_DRAW_SUCCESS) {
return all_errors;
}
}
return EPD_DRAW_SUCCESS;
}
void epd_clear_area(EpdRect area) {
epd_clear_area_cycles(area, 3, clear_cycle_time);
}
void epd_clear_area_cycles(EpdRect area, int cycles, int cycle_time) {
const short white_time = cycle_time;
const short dark_time = cycle_time;
for (int c = 0; c < cycles; c++) {
for (int i = 0; i < 10; i++) {
epd_push_pixels(area, dark_time, 0);
}
for (int i = 0; i < 10; i++) {
epd_push_pixels(area, white_time, 1);
}
}
}
void epd_init(enum EpdInitOptions options) {
epd_base_init(EPD_WIDTH);
epd_temperature_init();
size_t lut_size = 0;
if (options & EPD_LUT_1K) {
lut_size = 1 << 10;
} else if ((options & EPD_LUT_64K) || (options == EPD_OPTIONS_DEFAULT)) {
lut_size = 1 << 16;
} else {
ESP_LOGE("epd", "invalid init options: %d", options);
return;
}
conversion_lut = (uint8_t *)heap_caps_malloc(lut_size, MALLOC_CAP_8BIT | MALLOC_CAP_INTERNAL);
if (conversion_lut == NULL) {
ESP_LOGE("epd", "could not allocate LUT!");
}
assert(conversion_lut != NULL);
fetch_params.conversion_lut = conversion_lut;
fetch_params.conversion_lut_size = lut_size;
feed_params.conversion_lut = conversion_lut;
feed_params.conversion_lut_size = lut_size;
fetch_params.done_smphr = xSemaphoreCreateBinary();
fetch_params.start_smphr = xSemaphoreCreateBinary();
feed_params.done_smphr = xSemaphoreCreateBinary();
feed_params.start_smphr = xSemaphoreCreateBinary();
RTOS_ERROR_CHECK(xTaskCreatePinnedToCore((void (*)(void *))provide_out,
"epd_fetch", (1 << 12), &fetch_params, 5,
NULL, 0));
RTOS_ERROR_CHECK(xTaskCreatePinnedToCore((void (*)(void *))feed_display,
"epd_feed", 1 << 12, &feed_params,
5, NULL, 1));
//conversion_lut = (uint8_t *)heap_caps_malloc(1 << 16, MALLOC_CAP_8BIT);
//assert(conversion_lut != NULL);
int queue_len = 32;
if (options & EPD_FEED_QUEUE_32) {
queue_len = 32;
} else if (options & EPD_FEED_QUEUE_8) {
queue_len = 8;
}
output_queue = xQueueCreate(queue_len, EPD_WIDTH);
}
void epd_deinit() {
// FIXME: deinit processes
#if defined(CONFIG_EPD_BOARD_REVISION_V5)
gpio_reset_pin(CKH);
rtc_gpio_isolate(CKH);
#endif
epd_base_deinit();
}
EpdRect epd_difference_image_base(
const uint8_t* to,
const uint8_t* from,
EpdRect crop_to,
int fb_width,
int fb_height,
uint8_t* interlaced,
bool* dirty_lines,
uint8_t* from_or,
uint8_t* from_and
) {
assert(from_or != NULL);
assert(from_and != NULL);
// OR over all pixels of the "from"-image
*from_or = 0x00;
// AND over all pixels of the "from"-image
*from_and = 0xFF;
uint8_t dirty_cols[EPD_WIDTH] = {0};
int x_end = min(fb_width, crop_to.x + crop_to.width);
int y_end = min(fb_height, crop_to.y + crop_to.height);
for (int y=crop_to.y; y < y_end; y++) {
uint8_t dirty = 0;
for (int x = crop_to.x; x < x_end; x++) {
uint8_t t = *(to + y*fb_width / 2 + x / 2);
t = (x % 2) ? (t >> 4) : (t & 0x0f);
uint8_t f = *(from + y*fb_width / 2+ x / 2);
*from_or |= f;
*from_and &= f;
f = (x % 2) ? (f >> 4) : (f & 0x0f);
dirty |= (t ^ f);
dirty_cols[x] |= (t ^ f);
interlaced[y * fb_width + x] = (t << 4) | f;
}
dirty_lines[y] = dirty > 0;
}
int min_x, min_y, max_x, max_y;
for (min_x = crop_to.x; min_x < x_end; min_x++) {
if (dirty_cols[min_x] != 0) break;
}
for (max_x = x_end - 1; max_x >= crop_to.x; max_x--) {
if (dirty_cols[max_x] != 0) break;
}
for (min_y = crop_to.y; min_y < y_end; min_y++) {
if (dirty_lines[min_y] != 0) break;
}
for (max_y = y_end - 1; max_y >= crop_to.y; max_y--) {
if (dirty_lines[max_y] != 0) break;
}
EpdRect crop_rect = {
.x = min_x,
.y = min_y,
.width = max(max_x - min_x + 1, 0),
.height = max(max_y - min_y + 1, 0),
};
return crop_rect;
}
EpdRect epd_difference_image(
const uint8_t* to,
const uint8_t* from,
uint8_t* interlaced,
bool* dirty_lines
) {
uint8_t from_or = 0;
uint8_t from_and = 0;
return epd_difference_image_base(to, from, epd_full_screen(), EPD_WIDTH, EPD_HEIGHT, interlaced, dirty_lines, &from_or, &from_and);
}
EpdRect epd_difference_image_cropped(
const uint8_t* to,
const uint8_t* from,
EpdRect crop_to,
uint8_t* interlaced,
bool* dirty_lines,
bool* previously_white,
bool* previously_black
) {
uint8_t from_or, from_and;
EpdRect result = epd_difference_image_base(to, from, crop_to, EPD_WIDTH, EPD_HEIGHT, interlaced, dirty_lines, &from_or, &from_and);
if (previously_white != NULL) *previously_white = (from_and == 0xFF);
if (previously_black != NULL) *previously_black = (from_or == 0x00);
return result;
}

View File

@ -1,80 +0,0 @@
#include "rmt_pulse.h"
#include "driver/rmt.h"
#include "esp_system.h"
static intr_handle_t gRMT_intr_handle = NULL;
// the RMT channel configuration object
static rmt_config_t row_rmt_config;
// keep track of wether the current pulse is ongoing
volatile bool rmt_tx_done = true;
/**
* Remote peripheral interrupt. Used to signal when transmission is done.
*/
static void IRAM_ATTR rmt_interrupt_handler(void *arg) {
rmt_tx_done = true;
RMT.int_clr.val = RMT.int_st.val;
}
void rmt_pulse_init(gpio_num_t pin) {
row_rmt_config.rmt_mode = RMT_MODE_TX;
// currently hardcoded: use channel 0
row_rmt_config.channel = RMT_CHANNEL_1;
row_rmt_config.gpio_num = pin;
row_rmt_config.mem_block_num = 2;
// Divide 80MHz APB Clock by 8 -> .1us resolution delay
row_rmt_config.clk_div = 8;
row_rmt_config.tx_config.loop_en = false;
row_rmt_config.tx_config.carrier_en = false;
row_rmt_config.tx_config.carrier_level = RMT_CARRIER_LEVEL_LOW;
row_rmt_config.tx_config.idle_level = RMT_IDLE_LEVEL_LOW;
row_rmt_config.tx_config.idle_output_en = true;
#if ESP_IDF_VERSION < ESP_IDF_VERSION_VAL(4, 2, 0) && ESP_IDF_VERSION > ESP_IDF_VERSION_VAL(4, 0, 2)
#error "This driver is not compatible with IDF version 4.1.\nPlease use 4.0 or >= 4.2!"
#endif
esp_intr_alloc(ETS_RMT_INTR_SOURCE, ESP_INTR_FLAG_LEVEL3,
rmt_interrupt_handler, 0, &gRMT_intr_handle);
rmt_config(&row_rmt_config);
rmt_set_tx_intr_en(row_rmt_config.channel, true);
}
void IRAM_ATTR pulse_ckv_ticks(uint16_t high_time_ticks,
uint16_t low_time_ticks, bool wait) {
while (!rmt_tx_done) {
};
volatile rmt_item32_t *rmt_mem_ptr =
&(RMTMEM.chan[row_rmt_config.channel].data32[0]);
if (high_time_ticks > 0) {
rmt_mem_ptr->level0 = 1;
rmt_mem_ptr->duration0 = high_time_ticks;
rmt_mem_ptr->level1 = 0;
rmt_mem_ptr->duration1 = low_time_ticks;
} else {
rmt_mem_ptr->level0 = 1;
rmt_mem_ptr->duration0 = low_time_ticks;
rmt_mem_ptr->level1 = 0;
rmt_mem_ptr->duration1 = 0;
}
RMTMEM.chan[row_rmt_config.channel].data32[1].val = 0;
rmt_tx_done = false;
RMT.conf_ch[row_rmt_config.channel].conf1.mem_rd_rst = 1;
RMT.conf_ch[row_rmt_config.channel].conf1.mem_owner = RMT_MEM_OWNER_TX;
RMT.conf_ch[row_rmt_config.channel].conf1.tx_start = 1;
while (wait && !rmt_tx_done) {
};
}
void IRAM_ATTR pulse_ckv_us(uint16_t high_time_us, uint16_t low_time_us,
bool wait) {
pulse_ckv_ticks(10 * high_time_us, 10 * low_time_us, wait);
}
bool IRAM_ATTR rmt_busy() { return !rmt_tx_done; }

View File

@ -1,4 +1,157 @@
/**
* @file "epd_highlevel.h"
* @brief High-level API for drawing to e-paper displays.
*
* The functions in this library provide a simple way to manage updates of e-paper display.
* To use it, follow the steps below:
*
* 1. First, we declare a global object that manages the display state.
*
* EpdiyHighlevelState hl;
*
* 2. Then, the driver and framebuffers must be initialized.
*
* epd_init(EPD_LUT_1K);
* hl = epd_hl_init(EPD_BUILTIN_WAVEFORM);
*
* 3. Now, we can draw to the allocated framebuffer,
* using the draw and text functions defined in `epdiy.h`.
* This will not yet update the display, but only its representation in memory.
*
* // A reference to the framebuffer
* uint8_t* fb = epd_hl_get_framebuffer(&hl);
*
* // draw a black rectangle
* EpdRect some_rect = {
* .x = 100,
* .y = 100,
* .width = 100,
* .height = 100
* };
* epd_fill_rect(some_rect, 0x0, fb);
*
* // write a message
* int cursor_x = 100;
* int cursor_y = 300;
* epd_write_default(&FiraSans, "Hello, World!", &cursor_x, &cursor_y, fb);
*
* // finally, update the display!
* int temperature = 25;
* epd_poweron();
* EpdDrawError err = epd_hl_update_screen(&hl, MODE_GC16, temperature);
* epd_poweroff();
*
* That's it! For many application, this will be enough.
* For special applications and requirements, have a
* closer look at the `epdiy.h` header.
*
* Colors
* ======
*
* Since most displays only support 16 colors, we're only using the upper 4 bits (nibble) of a byte
* to detect the color.
*
* char pixel_color = color & 0xF0;
*
* So keep in mind, when passing a color to any function, to always set the upper 4 bits, otherwise
* the color would be black.
*
* Possible colors are `0xF0` (white) through `0x80` (median gray) til `0x00` (black).
*/
#ifdef __cplusplus
extern "C" {
#endif
#pragma once
// This file is only used in the Arduino IDE
// and just includes the IDF component header.
#include "epd_driver/include/epd_highlevel.h"
#include <stdint.h>
#include "epdiy.h"
#define EPD_BUILTIN_WAVEFORM NULL
/// Holds the internal state of the high-level API.
typedef struct {
/// The "front" framebuffer object.
uint8_t* front_fb;
/// The "back" framebuffer object.
uint8_t* back_fb;
/// Buffer for holding the interlaced difference image.
uint8_t* difference_fb;
/// Tainted lines based on the last difference calculation.
bool* dirty_lines;
/// Tainted column nibbles based on the last difference calculation.
uint8_t* dirty_columns;
/// The waveform information to use.
const EpdWaveform* waveform;
} EpdiyHighlevelState;
/**
* Initialize a state object.
* This allocates two framebuffers and an update buffer for
* the display in the external PSRAM.
* In order to keep things simple, a chip reset is triggered if this fails.
*
* @param waveform: The waveform to use for updates.
* If you did not create your own, this will be `EPD_BUILTIN_WAVEFORM`.
* @returns An initialized state object.
*/
EpdiyHighlevelState epd_hl_init(const EpdWaveform* waveform);
/// Get a reference to the front framebuffer.
/// Use this to draw on the framebuffer before updating the screen with `epd_hl_update_screen()`.
uint8_t* epd_hl_get_framebuffer(EpdiyHighlevelState* state);
/**
* Update the EPD screen to match the content of the front frame buffer.
* Prior to this, power to the display must be enabled via `epd_poweron()`
* and should be disabled afterwards if no immediate additional updates follow.
*
* @param state: A reference to the `EpdiyHighlevelState` object used.
* @param mode: The update mode to use.
* Additional mode settings like the framebuffer format or
* previous display state are determined by the driver and must not be supplied here.
* In most cases, one of `MODE_GC16` and `MODE_GL16` should be used.
* @param temperature: Environmental temperature of the display in °C.
* @returns `EPD_DRAW_SUCCESS` on sucess, a combination of error flags otherwise.
*/
enum EpdDrawError epd_hl_update_screen(
EpdiyHighlevelState* state, enum EpdDrawMode mode, int temperature
);
/**
* Update an area of the screen to match the content of the front framebuffer.
* Supplying a small area to update can speed up the update process.
* Prior to this, power to the display must be enabled via `epd_poweron()`
* and should be disabled afterwards if no immediate additional updates follow.
*
* @param state: A reference to the `EpdiyHighlevelState` object used.
* @param mode: See `epd_hl_update_screen()`.
* @param temperature: Environmental temperature of the display in °C.
* @param area: Area of the screen to update.
* @returns `EPD_DRAW_SUCCESS` on sucess, a combination of error flags otherwise.
*/
enum EpdDrawError epd_hl_update_area(
EpdiyHighlevelState* state, enum EpdDrawMode mode, int temperature, EpdRect area
);
/**
* Reset the front framebuffer to a white state.
*
* @param state: A reference to the `EpdiyHighlevelState` object used.
*/
void epd_hl_set_all_white(EpdiyHighlevelState* state);
/**
* Bring the display to a fully white state and get rid of any
* remaining artifacts.
*/
void epd_fullclear(EpdiyHighlevelState* state, int temperature);
/**
* Just changes the used Waveform
*/
void epd_hl_waveform(EpdiyHighlevelState* state, const EpdWaveform* waveform);
#ifdef __cplusplus
}
#endif

View File

@ -0,0 +1,89 @@
/**
* @file epd_internals.h
* @brief Internal definitions and auxiliary data types.
*
* Unless you want to extend the library itself (Which you are very welcome to do),
* you will most likely not need to know about this file.
*/
#ifndef EPD_INTERNALS_H
#define EPD_INTERNALS_H
#include <stdbool.h>
#include <stdint.h>
/// minimal draw time in ms for a frame layer,
/// which will allow all particles to set properly.
#define MINIMUM_FRAME_TIME 12
/// Frame draw time for monochrome mode in 1/10 us.
#define MONOCHROME_FRAME_TIME 120
typedef struct {
int phases;
const uint8_t* luts;
/// If we have timing information for the individual
/// phases, this is an array of the on-times for each phase.
/// Otherwise, this is NULL.
const int* phase_times;
} EpdWaveformPhases;
typedef struct {
uint8_t type;
uint8_t temp_ranges;
EpdWaveformPhases const** range_data;
} EpdWaveformMode;
typedef struct {
int min;
int max;
} EpdWaveformTempInterval;
typedef struct {
uint8_t num_modes;
uint8_t num_temp_ranges;
EpdWaveformMode const** mode_data;
EpdWaveformTempInterval const* temp_intervals;
} EpdWaveform;
extern const EpdWaveform epdiy_ED060SC4;
extern const EpdWaveform epdiy_ED097OC4;
extern const EpdWaveform epdiy_ED047TC1;
extern const EpdWaveform epdiy_ED047TC2;
extern const EpdWaveform epdiy_ED097TC2;
extern const EpdWaveform epdiy_ED060XC3;
extern const EpdWaveform epdiy_ED060SCT;
extern const EpdWaveform epdiy_ED133UT2;
extern const EpdWaveform epdiy_NULL;
/// Font data stored PER GLYPH
typedef struct {
uint16_t width; ///< Bitmap dimensions in pixels
uint16_t height; ///< Bitmap dimensions in pixels
uint16_t advance_x; ///< Distance to advance cursor (x axis)
int16_t left; ///< X dist from cursor pos to UL corner
int16_t top; ///< Y dist from cursor pos to UL corner
uint32_t compressed_size; ///< Size of the zlib-compressed font data.
uint32_t data_offset; ///< Pointer into EpdFont->bitmap
} EpdGlyph;
/// Glyph interval structure
typedef struct {
uint32_t first; ///< The first unicode code point of the interval
uint32_t last; ///< The last unicode code point of the interval
uint32_t offset; ///< Index of the first code point into the glyph array
} EpdUnicodeInterval;
/// Data stored for FONT AS A WHOLE
typedef struct {
const uint8_t* bitmap; ///< Glyph bitmaps, concatenated
const EpdGlyph* glyph; ///< Glyph array
const EpdUnicodeInterval* intervals; ///< Valid unicode intervals for this font
uint32_t interval_count; ///< Number of unicode intervals.
bool compressed; ///< Does this font use compressed glyph bitmaps?
uint16_t advance_y; ///< Newline distance (y axis)
int ascender; ///< Maximal height of a glyph above the base line
int descender; ///< Maximal height of a glyph below the base line
} EpdFont;
#endif // EPD_INTERNALS_H

View File

@ -0,0 +1,545 @@
#include "epdiy.h"
#include "epd_board.h"
#include "epd_display.h"
#include "output_common/render_method.h"
#include "render.h"
#include <esp_assert.h>
#include <esp_heap_caps.h>
#include <esp_log.h>
#include <esp_types.h>
// Simple x and y coordinate
typedef struct {
uint16_t x;
uint16_t y;
} Coord_xy;
static const EpdDisplay_t* display = NULL;
// Display rotation. Can be updated using epd_set_rotation(enum EpdRotation)
static enum EpdRotation display_rotation = EPD_ROT_LANDSCAPE;
#ifndef _swap_int
#define _swap_int(a, b) \
{ \
int t = a; \
a = b; \
b = t; \
}
#endif
EpdRect epd_full_screen() {
EpdRect area = { .x = 0, .y = 0, .width = epd_width(), .height = epd_height() };
return area;
}
void epd_clear() {
epd_clear_area(epd_full_screen());
}
void epd_draw_hline(int x, int y, int length, uint8_t color, uint8_t* framebuffer) {
for (int i = 0; i < length; i++) {
int xx = x + i;
epd_draw_pixel(xx, y, color, framebuffer);
}
}
void epd_draw_vline(int x, int y, int length, uint8_t color, uint8_t* framebuffer) {
for (int i = 0; i < length; i++) {
int yy = y + i;
epd_draw_pixel(x, yy, color, framebuffer);
}
}
Coord_xy _rotate(uint16_t x, uint16_t y) {
switch (display_rotation) {
case EPD_ROT_LANDSCAPE:
break;
case EPD_ROT_PORTRAIT:
_swap_int(x, y);
x = epd_width() - x - 1;
break;
case EPD_ROT_INVERTED_LANDSCAPE:
x = epd_width() - x - 1;
y = epd_height() - y - 1;
break;
case EPD_ROT_INVERTED_PORTRAIT:
_swap_int(x, y);
y = epd_height() - y - 1;
break;
}
Coord_xy coord = { x, y };
return coord;
}
void epd_draw_pixel(int x, int y, uint8_t color, uint8_t* framebuffer) {
// Check rotation and move pixel around if necessary
Coord_xy coord = _rotate(x, y);
x = coord.x;
y = coord.y;
if (x < 0 || x >= epd_width()) {
return;
}
if (y < 0 || y >= epd_height()) {
return;
}
uint8_t* buf_ptr = &framebuffer[y * epd_width() / 2 + x / 2];
if (x % 2) {
*buf_ptr = (*buf_ptr & 0x0F) | (color & 0xF0);
} else {
*buf_ptr = (*buf_ptr & 0xF0) | (color >> 4);
}
}
void epd_draw_circle(int x0, int y0, int r, uint8_t color, uint8_t* framebuffer) {
int f = 1 - r;
int ddF_x = 1;
int ddF_y = -2 * r;
int x = 0;
int y = r;
epd_draw_pixel(x0, y0 + r, color, framebuffer);
epd_draw_pixel(x0, y0 - r, color, framebuffer);
epd_draw_pixel(x0 + r, y0, color, framebuffer);
epd_draw_pixel(x0 - r, y0, color, framebuffer);
while (x < y) {
if (f >= 0) {
y--;
ddF_y += 2;
f += ddF_y;
}
x++;
ddF_x += 2;
f += ddF_x;
epd_draw_pixel(x0 + x, y0 + y, color, framebuffer);
epd_draw_pixel(x0 - x, y0 + y, color, framebuffer);
epd_draw_pixel(x0 + x, y0 - y, color, framebuffer);
epd_draw_pixel(x0 - x, y0 - y, color, framebuffer);
epd_draw_pixel(x0 + y, y0 + x, color, framebuffer);
epd_draw_pixel(x0 - y, y0 + x, color, framebuffer);
epd_draw_pixel(x0 + y, y0 - x, color, framebuffer);
epd_draw_pixel(x0 - y, y0 - x, color, framebuffer);
}
}
void epd_fill_circle(int x0, int y0, int r, uint8_t color, uint8_t* framebuffer) {
epd_draw_vline(x0, y0 - r, 2 * r + 1, color, framebuffer);
epd_fill_circle_helper(x0, y0, r, 3, 0, color, framebuffer);
}
void epd_fill_circle_helper(
int x0, int y0, int r, int corners, int delta, uint8_t color, uint8_t* framebuffer
) {
int f = 1 - r;
int ddF_x = 1;
int ddF_y = -2 * r;
int x = 0;
int y = r;
int px = x;
int py = y;
delta++; // Avoid some +1's in the loop
while (x < y) {
if (f >= 0) {
y--;
ddF_y += 2;
f += ddF_y;
}
x++;
ddF_x += 2;
f += ddF_x;
// These checks avoid double-drawing certain lines, important
// for the SSD1306 library which has an INVERT drawing mode.
if (x < (y + 1)) {
if (corners & 1)
epd_draw_vline(x0 + x, y0 - y, 2 * y + delta, color, framebuffer);
if (corners & 2)
epd_draw_vline(x0 - x, y0 - y, 2 * y + delta, color, framebuffer);
}
if (y != py) {
if (corners & 1)
epd_draw_vline(x0 + py, y0 - px, 2 * px + delta, color, framebuffer);
if (corners & 2)
epd_draw_vline(x0 - py, y0 - px, 2 * px + delta, color, framebuffer);
py = y;
}
px = x;
}
}
void epd_draw_rect(EpdRect rect, uint8_t color, uint8_t* framebuffer) {
int x = rect.x;
int y = rect.y;
int w = rect.width;
int h = rect.height;
epd_draw_hline(x, y, w, color, framebuffer);
epd_draw_hline(x, y + h - 1, w, color, framebuffer);
epd_draw_vline(x, y, h, color, framebuffer);
epd_draw_vline(x + w - 1, y, h, color, framebuffer);
}
void epd_fill_rect(EpdRect rect, uint8_t color, uint8_t* framebuffer) {
int x = rect.x;
int y = rect.y;
int w = rect.width;
int h = rect.height;
for (int i = y; i < y + h; i++) {
epd_draw_hline(x, i, w, color, framebuffer);
}
}
static void epd_write_line(int x0, int y0, int x1, int y1, uint8_t color, uint8_t* framebuffer) {
int steep = abs(y1 - y0) > abs(x1 - x0);
if (steep) {
_swap_int(x0, y0);
_swap_int(x1, y1);
}
if (x0 > x1) {
_swap_int(x0, x1);
_swap_int(y0, y1);
}
int dx, dy;
dx = x1 - x0;
dy = abs(y1 - y0);
int err = dx / 2;
int ystep;
if (y0 < y1) {
ystep = 1;
} else {
ystep = -1;
}
for (; x0 <= x1; x0++) {
if (steep) {
epd_draw_pixel(y0, x0, color, framebuffer);
} else {
epd_draw_pixel(x0, y0, color, framebuffer);
}
err -= dy;
if (err < 0) {
y0 += ystep;
err += dx;
}
}
}
void epd_draw_line(int x0, int y0, int x1, int y1, uint8_t color, uint8_t* framebuffer) {
// Update in subclasses if desired!
if (x0 == x1) {
if (y0 > y1)
_swap_int(y0, y1);
epd_draw_vline(x0, y0, y1 - y0 + 1, color, framebuffer);
} else if (y0 == y1) {
if (x0 > x1)
_swap_int(x0, x1);
epd_draw_hline(x0, y0, x1 - x0 + 1, color, framebuffer);
} else {
epd_write_line(x0, y0, x1, y1, color, framebuffer);
}
}
void epd_draw_triangle(
int x0, int y0, int x1, int y1, int x2, int y2, uint8_t color, uint8_t* framebuffer
) {
epd_draw_line(x0, y0, x1, y1, color, framebuffer);
epd_draw_line(x1, y1, x2, y2, color, framebuffer);
epd_draw_line(x2, y2, x0, y0, color, framebuffer);
}
void epd_fill_triangle(
int x0, int y0, int x1, int y1, int x2, int y2, uint8_t color, uint8_t* framebuffer
) {
int a, b, y, last;
// Sort coordinates by Y order (y2 >= y1 >= y0)
if (y0 > y1) {
_swap_int(y0, y1);
_swap_int(x0, x1);
}
if (y1 > y2) {
_swap_int(y2, y1);
_swap_int(x2, x1);
}
if (y0 > y1) {
_swap_int(y0, y1);
_swap_int(x0, x1);
}
if (y0 == y2) { // Handle awkward all-on-same-line case as its own thing
a = b = x0;
if (x1 < a)
a = x1;
else if (x1 > b)
b = x1;
if (x2 < a)
a = x2;
else if (x2 > b)
b = x2;
epd_draw_hline(a, y0, b - a + 1, color, framebuffer);
return;
}
int dx01 = x1 - x0, dy01 = y1 - y0, dx02 = x2 - x0, dy02 = y2 - y0, dx12 = x2 - x1,
dy12 = y2 - y1;
int32_t sa = 0, sb = 0;
// For upper part of triangle, find scanline crossings for segments
// 0-1 and 0-2. If y1=y2 (flat-bottomed triangle), the scanline y1
// is included here (and second loop will be skipped, avoiding a /0
// error there), otherwise scanline y1 is skipped here and handled
// in the second loop...which also avoids a /0 error here if y0=y1
// (flat-topped triangle).
if (y1 == y2)
last = y1; // Include y1 scanline
else
last = y1 - 1; // Skip it
for (y = y0; y <= last; y++) {
a = x0 + sa / dy01;
b = x0 + sb / dy02;
sa += dx01;
sb += dx02;
/* longhand:
a = x0 + (x1 - x0) * (y - y0) / (y1 - y0);
b = x0 + (x2 - x0) * (y - y0) / (y2 - y0);
*/
if (a > b)
_swap_int(a, b);
epd_draw_hline(a, y, b - a + 1, color, framebuffer);
}
// For lower part of triangle, find scanline crossings for segments
// 0-2 and 1-2. This loop is skipped if y1=y2.
sa = (int32_t)dx12 * (y - y1);
sb = (int32_t)dx02 * (y - y0);
for (; y <= y2; y++) {
a = x1 + sa / dy12;
b = x0 + sb / dy02;
sa += dx12;
sb += dx02;
/* longhand:
a = x1 + (x2 - x1) * (y - y1) / (y2 - y1);
b = x0 + (x2 - x0) * (y - y0) / (y2 - y0);
*/
if (a > b)
_swap_int(a, b);
epd_draw_hline(a, y, b - a + 1, color, framebuffer);
}
}
void epd_copy_to_framebuffer(EpdRect image_area, const uint8_t* image_data, uint8_t* framebuffer) {
assert(framebuffer != NULL);
for (uint32_t i = 0; i < image_area.width * image_area.height; i++) {
uint32_t value_index = i;
// for images of uneven width,
// consume an additional nibble per row.
if (image_area.width % 2) {
value_index += i / image_area.width;
}
uint8_t val = (value_index % 2) ? (image_data[value_index / 2] & 0xF0) >> 4
: image_data[value_index / 2] & 0x0F;
int xx = image_area.x + i % image_area.width;
if (xx < 0 || xx >= epd_width()) {
continue;
}
int yy = image_area.y + i / image_area.width;
if (yy < 0 || yy >= epd_height()) {
continue;
}
uint8_t* buf_ptr = &framebuffer[yy * epd_width() / 2 + xx / 2];
if (xx % 2) {
*buf_ptr = (*buf_ptr & 0x0F) | (val << 4);
} else {
*buf_ptr = (*buf_ptr & 0xF0) | val;
}
}
}
enum EpdDrawError epd_draw_image(EpdRect area, const uint8_t* data, const EpdWaveform* waveform) {
int temperature = epd_ambient_temperature();
assert(waveform != NULL);
EpdRect no_crop = {
.x = 0,
.y = 0,
.width = 0,
.height = 0,
};
return epd_draw_base(area, data, no_crop, EPD_MODE_DEFAULT, temperature, NULL, NULL, waveform);
}
void epd_set_rotation(enum EpdRotation rotation) {
display_rotation = rotation;
}
enum EpdRotation epd_get_rotation() {
return display_rotation;
}
int epd_rotated_display_width() {
int display_width = epd_width();
switch (display_rotation) {
case EPD_ROT_PORTRAIT:
display_width = epd_height();
break;
case EPD_ROT_INVERTED_PORTRAIT:
display_width = epd_height();
break;
case EPD_ROT_INVERTED_LANDSCAPE:
case EPD_ROT_LANDSCAPE:
break;
}
return display_width;
}
int epd_rotated_display_height() {
int display_height = epd_height();
switch (display_rotation) {
case EPD_ROT_PORTRAIT:
display_height = epd_width();
break;
case EPD_ROT_INVERTED_PORTRAIT:
display_height = epd_width();
break;
case EPD_ROT_INVERTED_LANDSCAPE:
case EPD_ROT_LANDSCAPE:
break;
}
return display_height;
}
uint8_t epd_get_pixel(int x, int y, int fb_width, int fb_height, const uint8_t* framebuffer) {
if (x < 0 || x >= fb_width) {
return 0;
}
if (y < 0 || y >= fb_height) {
return 0;
}
int fb_width_bytes = fb_width / 2 + fb_width % 2;
uint8_t buf_val = framebuffer[y * fb_width_bytes + x / 2];
if (x % 2) {
buf_val = (buf_val & 0xF0) >> 4;
} else {
buf_val = (buf_val & 0x0F);
}
// epd_draw_pixel needs a 0 -> 255 value
return buf_val << 4;
}
static void draw_rotated_transparent_image(
EpdRect image_area,
const uint8_t* image_buffer,
uint8_t* framebuffer,
uint8_t* transparent_color
) {
uint16_t x_offset = 0;
uint16_t y_offset = 0;
uint8_t pixel_color;
for (uint16_t y = 0; y < image_area.height; y++) {
for (uint16_t x = 0; x < image_area.width; x++) {
x_offset = image_area.x + x;
y_offset = image_area.y + y;
if (x_offset >= epd_rotated_display_width())
continue;
if (y_offset >= epd_rotated_display_height())
continue;
pixel_color = epd_get_pixel(x, y, image_area.width, image_area.height, image_buffer);
if (transparent_color == NULL || pixel_color != (*transparent_color))
epd_draw_pixel(x_offset, y_offset, pixel_color, framebuffer);
}
}
}
void epd_draw_rotated_transparent_image(
EpdRect image_area, const uint8_t* image_buffer, uint8_t* framebuffer, uint8_t transparent_color
) {
draw_rotated_transparent_image(image_area, image_buffer, framebuffer, &transparent_color);
}
void epd_draw_rotated_image(EpdRect image_area, const uint8_t* image_buffer, uint8_t* framebuffer) {
if (epd_get_rotation() != EPD_ROT_LANDSCAPE) {
draw_rotated_transparent_image(image_area, image_buffer, framebuffer, NULL);
} else {
epd_copy_to_framebuffer(image_area, image_buffer, framebuffer);
}
}
void epd_poweron() {
epd_current_board()->poweron(epd_ctrl_state());
}
void epd_poweroff() {
epd_current_board()->poweroff(epd_ctrl_state());
}
void epd_init(
const EpdBoardDefinition* board, const EpdDisplay_t* disp, enum EpdInitOptions options
) {
display = disp;
epd_set_board(board);
epd_renderer_init(options);
}
void epd_deinit() {
epd_renderer_deinit();
}
float epd_ambient_temperature() {
if (!epd_current_board()) {
ESP_LOGE("epdiy", "Could not read temperature: board not set!");
return 0.0;
}
if (!epd_current_board()->get_temperature) {
ESP_LOGW("epdiy", "No ambient temperature sensor - returning 21C");
return 21.0;
}
return epd_current_board()->get_temperature();
}
void epd_set_vcom(uint16_t vcom) {
if (!epd_current_board()) {
ESP_LOGE("epdiy", "Could not set VCOM: board not set!");
return;
}
if (!epd_current_board()->set_vcom) {
ESP_LOGE("epdiy", "Could not set VCOM: board does not support setting VCOM in software.");
return;
}
return epd_current_board()->set_vcom(vcom);
}
const EpdDisplay_t* epd_get_display() {
assert(display != NULL);
return display;
}
int epd_width() {
return display->width;
}
int epd_height() {
return display->height;
}
void epd_set_lcd_pixel_clock_MHz(int frequency) {
#ifdef RENDER_METHOD_LCD
void epd_lcd_set_pixel_clock_MHz(int frequency);
epd_lcd_set_pixel_clock_MHz(frequency);
#else
ESP_LOGW("epdiy", "called set_lcd_pixel_clock_MHz, but LCD driver is not used!");
#endif
}

View File

@ -0,0 +1,605 @@
/**
* @file epdiy.h
* A driver library for drawing to an EPD.
*/
#include "epd_display.h"
#ifdef __cplusplus
extern "C" {
#endif
#pragma once
#include <esp_attr.h>
#include <stdbool.h>
#include <stdint.h>
#include "epd_internals.h"
/// An area on the display.
typedef struct {
/// Horizontal position.
int x;
/// Vertical position.
int y;
/// Area / image width, must be positive.
int width;
/// Area / image height, must be positive.
int height;
} EpdRect;
/// Global EPD driver options.
enum EpdInitOptions {
/// Use the default options.
EPD_OPTIONS_DEFAULT = 0,
/// Use a small look-up table of 1024 bytes.
/// The EPD driver will use less space, but performance may be worse.
EPD_LUT_1K = 1,
/// Use a 64K lookup table. (default)
/// Best performance, but permanently occupies a 64k block of internal memory.
EPD_LUT_64K = 2,
/// Use a small feed queue of 8 display lines.
/// This uses less memory, but may impact performance.
EPD_FEED_QUEUE_8 = 4,
/// Use a feed queue of 32 display lines. (default)
/// Best performance, but larger memory footprint.
EPD_FEED_QUEUE_32 = 8,
};
/// The image drawing mode.
enum EpdDrawMode {
/// An init waveform.
/// This is currently unused, use `epd_clear()` instead.
MODE_INIT = 0x0,
/// Direct Update: Go from any color to black for white only.
MODE_DU = 0x1,
/// Go from any grayscale value to another with a flashing update.
MODE_GC16 = 0x2,
/// Faster version of `MODE_GC16`.
/// Not available with default epdiy waveforms.
MODE_GC16_FAST = 0x3,
/// Animation Mode: Fast, monochrom updates.
/// Not available with default epdiy waveforms.
MODE_A2 = 0x4,
/// Go from any grayscale value to another with a non-flashing update.
MODE_GL16 = 0x5,
/// Faster version of `MODE_GL16`.
/// Not available with default epdiy waveforms.
MODE_GL16_FAST = 0x6,
/// A 4-grayscale version of `MODE_DU`.
/// Not available with default epdiy waveforms.
MODE_DU4 = 0x7,
/// Arbitrary transitions for 4 grayscale values.
/// Not available with default epdiy waveforms.
MODE_GL4 = 0xA,
/// Not available with default epdiy waveforms.
MODE_GL16_INV = 0xB,
/// Go from a white screen to arbitrary grayscale, quickly.
/// Exclusively available with epdiy waveforms.
MODE_EPDIY_WHITE_TO_GL16 = 0x10,
/// Go from a black screen to arbitrary grayscale, quickly.
/// Exclusively available with epdiy waveforms.
MODE_EPDIY_BLACK_TO_GL16 = 0x11,
/// Monochrome mode. Only supported with 1bpp buffers.
MODE_EPDIY_MONOCHROME = 0x20,
MODE_UNKNOWN_WAVEFORM = 0x3F,
// Framebuffer packing modes
/// 1 bit-per-pixel framebuffer with 0 = black, 1 = white.
/// MSB is left is the leftmost pixel, LSB the rightmost pixel.
MODE_PACKING_8PPB = 0x40,
/// 4 bit-per pixel framebuffer with 0x0 = black, 0xF = white.
/// The upper nibble corresponds to the left pixel.
/// A byte cannot wrap over multiple rows, images of uneven width
/// must add a padding nibble per line.
MODE_PACKING_2PPB = 0x80,
/// A difference image with one pixel per byte.
/// The upper nibble marks the "from" color,
/// the lower nibble the "to" color.
MODE_PACKING_1PPB_DIFFERENCE = 0x100,
// reserver for 4PPB mode
/// Assert that the display has a uniform color, e.g. after initialization.
/// If `MODE_PACKING_2PPB` is specified, a optimized output calculation can be used.
/// Draw on a white background
PREVIOUSLY_WHITE = 0x200,
/// See `PREVIOUSLY_WHITE`.
/// Draw on a black background
PREVIOUSLY_BLACK = 0x400,
/// Enforce NOT using S3 Vector extensions.
/// USed for testing.
MODE_FORCE_NO_PIE = 0x800,
};
/** Display software rotation.
* Sets the rotation for the purposes of the drawing and font functions
* Use epd_set_rotation(EPD_ROT_*) to set it using one of the options below
* Use epd_get_rotation() in case you need to read this value
*/
enum EpdRotation {
EPD_ROT_LANDSCAPE = 0,
EPD_ROT_PORTRAIT = 1,
EPD_ROT_INVERTED_LANDSCAPE = 2,
EPD_ROT_INVERTED_PORTRAIT = 3,
};
/// Possible failures when drawing.
enum EpdDrawError {
EPD_DRAW_SUCCESS = 0x0,
/// No valid framebuffer packing mode was specified.
EPD_DRAW_INVALID_PACKING_MODE = 0x1,
/// No lookup table implementation for this mode / packing.
EPD_DRAW_LOOKUP_NOT_IMPLEMENTED = 0x2,
/// The string to draw is invalid.
EPD_DRAW_STRING_INVALID = 0x4,
/// The string was not empty, but no characters where drawable.
EPD_DRAW_NO_DRAWABLE_CHARACTERS = 0x8,
/// Allocation failed
EPD_DRAW_FAILED_ALLOC = 0x10,
/// A glyph could not be drawn, and not fallback was present.
EPD_DRAW_GLYPH_FALLBACK_FAILED = 0x20,
/// The specified crop area is invalid.
EPD_DRAW_INVALID_CROP = 0x40,
/// No such mode is available with the current waveform.
EPD_DRAW_MODE_NOT_FOUND = 0x80,
/// The waveform info file contains no applicable temperature range.
EPD_DRAW_NO_PHASES_AVAILABLE = 0x100,
/// An invalid combination of font flags was used.
EPD_DRAW_INVALID_FONT_FLAGS = 0x200,
/// The waveform lookup could not keep up with the display output.
///
/// Reduce the display clock speed.
EPD_DRAW_EMPTY_LINE_QUEUE = 0x400,
};
/// The default draw mode (non-flashy refresh, whith previously white screen).
#define EPD_MODE_DEFAULT (MODE_GL16 | PREVIOUSLY_WHITE)
/// Font drawing flags
enum EpdFontFlags {
/// Draw a background.
///
/// Take the background into account
/// when calculating the size.
EPD_DRAW_BACKGROUND = 0x1,
/// Left-Align lines
EPD_DRAW_ALIGN_LEFT = 0x2,
/// Right-align lines
EPD_DRAW_ALIGN_RIGHT = 0x4,
/// Center-align lines
EPD_DRAW_ALIGN_CENTER = 0x8,
};
/// Font properties.
typedef struct {
/// Foreground color
uint8_t fg_color : 4;
/// Background color
uint8_t bg_color : 4;
/// Use the glyph for this codepoint for missing glyphs.
uint32_t fallback_glyph;
/// Additional flags, reserved for future use
enum EpdFontFlags flags;
} EpdFontProperties;
#include "epd_board.h"
#include "epd_board_specific.h"
#include "epd_display.h"
#include "epd_highlevel.h"
/** Initialize the ePaper display */
void epd_init(
const EpdBoardDefinition* board, const EpdDisplay_t* display, enum EpdInitOptions options
);
/**
* Get the configured display.
*/
const EpdDisplay_t* epd_get_display();
/**
* Get the EPD display's witdth.
*/
int epd_width();
/**
* Get the EPD display's height.
*/
int epd_height();
/**
* Set the display common voltage if supported.
*
* Voltage is set as absolute value in millivolts.
* Although VCOM is negative, this function takes a positive (absolute) value.
*/
void epd_set_vcom(uint16_t vcom);
/**
* Get the current ambient temperature in °C,
* if the board has a sensor.
*/
float epd_ambient_temperature();
/** Get the display rotation value */
enum EpdRotation epd_get_rotation();
/** Set the display rotation: Affects the drawing and font functions */
void epd_set_rotation(enum EpdRotation rotation);
/** Get screen width after rotation */
int epd_rotated_display_width();
/** Get screen height after rotation */
int epd_rotated_display_height();
/** Deinit the ePaper display */
void epd_deinit();
/** Enable display power supply. */
void epd_poweron();
/** Disable display power supply. */
void epd_poweroff();
/** Clear the whole screen by flashing it. */
void epd_clear();
/**
* Clear an area by flashing it.
*
* @param area: The area to clear.
*/
void epd_clear_area(EpdRect area);
/**
* Clear an area by flashing it.
*
* @param area: The area to clear.
* @param cycles: The number of black-to-white clear cycles.
* @param cycle_time: Length of a cycle. Default: 50 (us).
*/
void epd_clear_area_cycles(EpdRect area, int cycles, int cycle_time);
/**
* @returns Rectancle representing the whole screen area.
*/
EpdRect epd_full_screen();
/**
* Draw a picture to a given framebuffer.
*
* @param image_area: The area to copy to. `width` and `height` of the area
* must correspond to the image dimensions in pixels.
* @param image_data: The image data, as a buffer of 4 bit wide brightness
* values. Pixel data is packed (two pixels per byte). A byte cannot wrap over
* multiple rows, images of uneven width must add a padding nibble per line.
* @param framebuffer: The framebuffer object,
* which must be `epd_width() / 2 * epd_height()` large.
*/
void epd_copy_to_framebuffer(EpdRect image_area, const uint8_t* image_data, uint8_t* framebuffer);
/**
* Draw a pixel a given framebuffer.
*
* @param x: Horizontal position in pixels.
* @param y: Vertical position in pixels.
* @param color: The gray value of the line (see [Colors](#Colors));
* @param framebuffer: The framebuffer to draw to,
*/
void epd_draw_pixel(int x, int y, uint8_t color, uint8_t* framebuffer);
/**
* Draw a horizontal line to a given framebuffer.
*
* @param x: Horizontal start position in pixels.
* @param y: Vertical start position in pixels.
* @param length: Length of the line in pixels.
* @param color: The gray value of the line (see [Colors](#Colors));
* @param framebuffer: The framebuffer to draw to,
* which must be `epd_width() / 2 * epd_height()` bytes large.
*/
void epd_draw_hline(int x, int y, int length, uint8_t color, uint8_t* framebuffer);
/**
* Draw a horizontal line to a given framebuffer.
*
* @param x: Horizontal start position in pixels.
* @param y: Vertical start position in pixels.
* @param length: Length of the line in pixels.
* @param color: The gray value of the line (see [Colors](#Colors));
* @param framebuffer: The framebuffer to draw to,
* which must be `epd_width() / 2 * epd_height()` bytes large.
*/
void epd_draw_vline(int x, int y, int length, uint8_t color, uint8_t* framebuffer);
void epd_fill_circle_helper(
int x0, int y0, int r, int corners, int delta, uint8_t color, uint8_t* framebuffer
);
/**
* Draw a circle with given center and radius
*
* @param x: Center-point x coordinate
* @param y: Center-point y coordinate
* @param r: Radius of the circle in pixels
* @param color: The gray value of the line (see [Colors](#Colors));
* @param framebuffer: The framebuffer to draw to,
*/
void epd_draw_circle(int x, int y, int r, uint8_t color, uint8_t* framebuffer);
/**
* Draw a circle with fill with given center and radius
*
* @param x: Center-point x coordinate
* @param y: Center-point y coordinate
* @param r: Radius of the circle in pixels
* @param color: The gray value of the line (see [Colors](#Colors));
* @param framebuffer: The framebuffer to draw to,
*/
void epd_fill_circle(int x, int y, int r, uint8_t color, uint8_t* framebuffer);
/**
* Draw a rectanle with no fill color
*
* @param rect: The rectangle to draw.
* @param color: The gray value of the line (see [Colors](#Colors));
* @param framebuffer: The framebuffer to draw to,
*/
void epd_draw_rect(EpdRect rect, uint8_t color, uint8_t* framebuffer);
/**
* Draw a rectanle with fill color
*
* @param rect: The rectangle to fill.
* @param color: The gray value of the line (see [Colors](#Colors));
* @param framebuffer: The framebuffer to draw to,
*/
void epd_fill_rect(EpdRect rect, uint8_t color, uint8_t* framebuffer);
/**
* Draw a line
*
* @param x0 Start point x coordinate
* @param y0 Start point y coordinate
* @param x1 End point x coordinate
* @param y1 End point y coordinate
* @param color: The gray value of the line (see [Colors](#Colors));
* @param framebuffer: The framebuffer to draw to,
*/
void epd_draw_line(int x0, int y0, int x1, int y1, uint8_t color, uint8_t* framebuffer);
/**
* Draw a triangle with no fill color
*
* @param x0 Vertex #0 x coordinate
* @param y0 Vertex #0 y coordinate
* @param x1 Vertex #1 x coordinate
* @param y1 Vertex #1 y coordinate
* @param x2 Vertex #2 x coordinate
* @param y2 Vertex #2 y coordinate
* @param color: The gray value of the line (see [Colors](#Colors));
* @param framebuffer: The framebuffer to draw to,
*/
void epd_draw_triangle(
int x0, int y0, int x1, int y1, int x2, int y2, uint8_t color, uint8_t* framebuffer
);
/**
* Draw a triangle with color-fill
*
* @param x0 Vertex #0 x coordinate
* @param y0 Vertex #0 y coordinate
* @param x1 Vertex #1 x coordinate
* @param y1 Vertex #1 y coordinate
* @param x2 Vertex #2 x coordinate
* @param y2 Vertex #2 y coordinate
* @param color: The gray value of the line (see [Colors](#Colors));
* @param framebuffer: The framebuffer to draw to,
*/
void epd_fill_triangle(
int x0, int y0, int x1, int y1, int x2, int y2, uint8_t color, uint8_t* framebuffer
);
/**
* Get the current ambient temperature in °C, if supported by the board.
* Requires the display to be powered on.
*/
float epd_ambient_temperature();
/**
* The default font properties.
*/
EpdFontProperties epd_font_properties_default();
/*!
* Get the text bounds for string, when drawn at (x, y).
* Set font properties to NULL to use the defaults.
*/
void epd_get_text_bounds(
const EpdFont* font,
const char* string,
const int* x,
const int* y,
int* x1,
int* y1,
int* w,
int* h,
const EpdFontProperties* props
);
/*!
* Returns a rect with the bounds of the text
* @param font : the font used to get the character sizes
* @param string: pointer to c string
* @param x : left most position of rectangle
* @param y : top most point of the rectangle
* @param margin : to be pllied to the width and height
* @returns EpdRect with x and y as per the original and height and width
* adjusted to fit the text with the margin added as well.
*/
EpdRect epd_get_string_rect(
const EpdFont* font,
const char* string,
int x,
int y,
int margin,
const EpdFontProperties* properties
);
/**
* Write text to the EPD.
*/
enum EpdDrawError epd_write_string(
const EpdFont* font,
const char* string,
int* cursor_x,
int* cursor_y,
uint8_t* framebuffer,
const EpdFontProperties* properties
);
/**
* Write a (multi-line) string to the EPD.
*/
enum EpdDrawError epd_write_default(
const EpdFont* font, const char* string, int* cursor_x, int* cursor_y, uint8_t* framebuffer
);
/**
* Get the font glyph for a unicode code point.
*/
const EpdGlyph* epd_get_glyph(const EpdFont* font, uint32_t code_point);
/**
* Darken / lighten an area for a given time.
*
* @param area: The area to darken / lighten.
* @param time: The time in us to apply voltage to each pixel.
* @param color: 1: lighten, 0: darken.
*/
void epd_push_pixels(EpdRect area, short time, int color);
/**
* Base function for drawing an image on the screen.
* If It is very customizable, and the documentation below should be studied carefully.
* For simple applications, use the epdiy highlevel api in "epd_higlevel.h".
*
* @param area: The area of the screen to draw to.
* This can be imagined as shifting the origin of the frame buffer.
* @param data: A full framebuffer of display data.
* It's structure depends on the chosen `mode`.
* @param crop_to: Only draw a part of the frame buffer.
* Set to `epd_full_screen()` to draw the full buffer.
* @param mode: Specifies the Waveform used, the framebuffer format
* and additional information, like if the display is cleared.
* @param temperature: The temperature of the display in °C.
* Currently, this is unused by the default waveforms at can be
* set to room temperature, e.g. 20-25°C.
* @param drawn_lines: If not NULL, an array of at least the height of the
* image. Every line where the corresponding value in `lines` is `false` will be
* skipped.
* @param drawn_columns: If not NULL, an array of at least the width of the
* image / 2, 16-byte aligned.
* The image will only be updated in pixel columns where the corresponding nibbles are
* non-zero.
* @param waveform: The waveform information to use for drawing.
* If you don't have special waveforms, use `EPD_BUILTIN_WAVEFORM`.
* @returns `EPD_DRAW_SUCCESS` on sucess, a combination of error flags otherwise.
*/
enum EpdDrawError epd_draw_base(
EpdRect area,
const uint8_t* data,
EpdRect crop_to,
enum EpdDrawMode mode,
int temperature,
const bool* drawn_lines,
const uint8_t* drawn_columns,
const EpdWaveform* waveform
);
/**
* Calculate a `MODE_PACKING_1PPB_DIFFERENCE` difference image
* from two `MODE_PACKING_2PPB` (4 bit-per-pixel) buffers.
* If you're using the epdiy highlevel api, this is handled by the update functions.
*
* @param to: The goal image as 4-bpp (`MODE_PACKING_2PPB`) framebuffer.
* @param from: The previous image as 4-bpp (`MODE_PACKING_2PPB`) framebuffer.
* @param crop_to: Only calculate the difference for a crop of the input framebuffers.
* The `interlaced` will not be modified outside the crop area.
* @param interlaced: The resulting difference image in `MODE_PACKING_1PPB_DIFFERENCE` format.
* @param dirty_lines: An array of at least `epd_height()`.
* The positions corresponding to lines where `to` and `from` differ
* are set to `true`, otherwise to `false`.
* @param col_dirtyness: An array of at least `epd_width() / 2`.
* If a nibble is set to non-zero, the pixel column is marked as changed, aka "dirty."
* The buffer must be 16 byte aligned.
* @returns The smallest rectangle containing all changed pixels.
*/
EpdRect epd_difference_image_cropped(
const uint8_t* to,
const uint8_t* from,
EpdRect crop_to,
uint8_t* interlaced,
bool* dirty_lines,
uint8_t* col_dirtiness
);
/**
* Simplified version of `epd_difference_image_cropped()`, which considers the
* whole display frame buffer.
*
* See `epd_difference_image_cropped() for details.`
*/
EpdRect epd_difference_image(
const uint8_t* to,
const uint8_t* from,
uint8_t* interlaced,
bool* dirty_lines,
uint8_t* col_dirtiness
);
/**
* Return the pixel color of a 4 bit image array
* x,y coordinates of the image pixel
* fb_width, fb_height dimensions
* @returns uint8_t 0-255 representing the color on given coordinates (as in epd_draw_pixel)
*/
uint8_t epd_get_pixel(int x, int y, int fb_width, int fb_height, const uint8_t* framebuffer);
/**
* Draw an image reading pixel per pixel and being rotation aware (via epd_draw_pixel)
*/
void epd_draw_rotated_image(EpdRect image_area, const uint8_t* image_buffer, uint8_t* framebuffer);
/**
* Draw an image reading pixel per pixel and being rotation aware (via epd_draw_pixel)
* With an optional transparent color (color key transparency)
*/
void epd_draw_rotated_transparent_image(
EpdRect image_area, const uint8_t* image_buffer, uint8_t* framebuffer, uint8_t transparent_color
);
/**
* Override the pixel clock when using the LCD driver for display output (Epdiy V7+).
* This may result in draws failing if it's set too high!
*
* This method can be used to tune your application for maximum refresh speed,
* if you can guarantee the driver can keep up.
*/
void epd_set_lcd_pixel_clock_MHz(int frequency);
#ifdef __cplusplus
}
#endif

View File

@ -0,0 +1,434 @@
#include <esp_assert.h>
#include <esp_heap_caps.h>
#include <esp_idf_version.h>
#include <esp_log.h>
#include "epdiy.h"
#include <miniz.h>
#include <math.h>
#include <stdio.h>
#include <string.h>
typedef struct {
uint8_t mask; /* char data will be bitwise AND with this */
uint8_t lead; /* start bytes of current char in utf-8 encoded character */
uint32_t beg; /* beginning of codepoint range */
uint32_t end; /* end of codepoint range */
int bits_stored; /* the number of bits from the codepoint that fits in char */
} utf_t;
/*
* UTF-8 decode inspired from rosetta code
* https://rosettacode.org/wiki/UTF-8_encode_and_decode#C
*/
static utf_t* utf[] = {
/* mask lead beg end bits */
[0] = &(utf_t){ 0b00111111, 0b10000000, 0, 0, 6 },
[1] = &(utf_t){ 0b01111111, 0b00000000, 0000, 0177, 7 },
[2] = &(utf_t){ 0b00011111, 0b11000000, 0200, 03777, 5 },
[3] = &(utf_t){ 0b00001111, 0b11100000, 04000, 0177777, 4 },
[4] = &(utf_t){ 0b00000111, 0b11110000, 0200000, 04177777, 3 },
&(utf_t){ 0 },
};
static inline int min(int x, int y) {
return x < y ? x : y;
}
static inline int max(int x, int y) {
return x > y ? x : y;
}
static int utf8_len(const uint8_t ch) {
int len = 0;
for (utf_t** u = utf; (*u)->mask; ++u) {
if ((ch & ~(*u)->mask) == (*u)->lead) {
break;
}
++len;
}
if (len > 4) { /* Malformed leading byte */
assert("invalid unicode.");
}
return len;
}
static uint32_t next_cp(const uint8_t** string) {
if (**string == 0) {
return 0;
}
int bytes = utf8_len(**string);
const uint8_t* chr = *string;
*string += bytes;
int shift = utf[0]->bits_stored * (bytes - 1);
uint32_t codep = (*chr++ & utf[bytes]->mask) << shift;
for (int i = 1; i < bytes; ++i, ++chr) {
shift -= utf[0]->bits_stored;
codep |= ((const uint8_t)*chr & utf[0]->mask) << shift;
}
return codep;
}
EpdFontProperties epd_font_properties_default() {
EpdFontProperties props
= { .fg_color = 0, .bg_color = 15, .fallback_glyph = 0, .flags = EPD_DRAW_ALIGN_LEFT };
return props;
}
const EpdGlyph* epd_get_glyph(const EpdFont* font, uint32_t code_point) {
const EpdUnicodeInterval* intervals = font->intervals;
for (int i = 0; i < font->interval_count; i++) {
const EpdUnicodeInterval* interval = &intervals[i];
if (code_point >= interval->first && code_point <= interval->last) {
return &font->glyph[interval->offset + (code_point - interval->first)];
}
if (code_point < interval->first) {
return NULL;
}
}
return NULL;
}
static int uncompress(
uint8_t* dest, size_t uncompressed_size, const uint8_t* source, size_t source_size
) {
tinfl_decompressor* decomp;
if (uncompressed_size == 0 || dest == NULL || source_size == 0 || source == NULL) {
return -1;
}
decomp = malloc(sizeof(tinfl_decompressor));
if (!decomp) {
// Out of memory
return -1;
}
tinfl_init(decomp);
// we know everything will fit into the buffer.
tinfl_status decomp_status = tinfl_decompress(
decomp,
source,
&source_size,
dest,
dest,
&uncompressed_size,
TINFL_FLAG_PARSE_ZLIB_HEADER | TINFL_FLAG_USING_NON_WRAPPING_OUTPUT_BUF
);
free(decomp);
if (decomp_status != TINFL_STATUS_DONE) {
return decomp_status;
}
return 0;
}
/*!
@brief Draw a single character to a pre-allocated buffer.
*/
static enum EpdDrawError IRAM_ATTR draw_char(
const EpdFont* font,
uint8_t* buffer,
int* cursor_x,
int cursor_y,
uint32_t cp,
const EpdFontProperties* props
) {
assert(props != NULL);
const EpdGlyph* glyph = epd_get_glyph(font, cp);
if (!glyph) {
glyph = epd_get_glyph(font, props->fallback_glyph);
}
if (!glyph) {
return EPD_DRAW_GLYPH_FALLBACK_FAILED;
}
uint32_t offset = glyph->data_offset;
uint16_t width = glyph->width, height = glyph->height;
int left = glyph->left;
int byte_width = (width / 2 + width % 2);
unsigned long bitmap_size = byte_width * height;
const uint8_t* bitmap = NULL;
if (bitmap_size > 0 && font->compressed) {
uint8_t* tmp_bitmap = (uint8_t*)malloc(bitmap_size);
if (tmp_bitmap == NULL && bitmap_size) {
ESP_LOGE("font", "malloc failed.");
return EPD_DRAW_FAILED_ALLOC;
}
uncompress(tmp_bitmap, bitmap_size, &font->bitmap[offset], glyph->compressed_size);
bitmap = tmp_bitmap;
} else {
bitmap = &font->bitmap[offset];
}
uint8_t color_lut[16];
for (int c = 0; c < 16; c++) {
int color_difference = (int)props->fg_color - (int)props->bg_color;
color_lut[c] = max(0, min(15, props->bg_color + c * color_difference / 15));
}
bool background_needed = props->flags & EPD_DRAW_BACKGROUND;
for (int y = 0; y < height; y++) {
int yy = cursor_y - glyph->top + y;
int start_pos = *cursor_x + left;
bool byte_complete = start_pos % 2;
int x = max(0, -start_pos);
int max_x = start_pos + width;
uint8_t color;
for (int xx = start_pos; xx < max_x; xx++) {
uint8_t bm = bitmap[y * byte_width + x / 2];
if ((x & 1) == 0) {
bm = bm & 0xF;
} else {
bm = bm >> 4;
}
if (background_needed || bm) {
color = color_lut[bm] << 4;
epd_draw_pixel(xx, yy, color, buffer);
}
byte_complete = !byte_complete;
x++;
}
}
if (bitmap_size > 0 && font->compressed) {
free((uint8_t*)bitmap);
}
*cursor_x += glyph->advance_x;
return EPD_DRAW_SUCCESS;
}
/*!
* @brief Calculate the bounds of a character when drawn at (x, y), move the
* cursor (*x) forward, adjust the given bounds.
*/
static void get_char_bounds(
const EpdFont* font,
uint32_t cp,
int* x,
int* y,
int* minx,
int* miny,
int* maxx,
int* maxy,
const EpdFontProperties* props
) {
assert(props != NULL);
const EpdGlyph* glyph = epd_get_glyph(font, cp);
if (!glyph) {
glyph = epd_get_glyph(font, props->fallback_glyph);
}
if (!glyph) {
return;
}
int x1 = *x + glyph->left, y1 = *y + glyph->top - glyph->height, x2 = x1 + glyph->width,
y2 = y1 + glyph->height;
// background needs to be taken into account
if (props->flags & EPD_DRAW_BACKGROUND) {
*minx = min(*x, min(*minx, x1));
*maxx = max(max(*x + glyph->advance_x, x2), *maxx);
*miny = min(*y + font->descender, min(*miny, y1));
*maxy = max(*y + font->ascender, max(*maxy, y2));
} else {
if (x1 < *minx)
*minx = x1;
if (y1 < *miny)
*miny = y1;
if (x2 > *maxx)
*maxx = x2;
if (y2 > *maxy)
*maxy = y2;
}
*x += glyph->advance_x;
}
EpdRect epd_get_string_rect(
const EpdFont* font,
const char* string,
int x,
int y,
int margin,
const EpdFontProperties* properties
) {
assert(properties != NULL);
EpdFontProperties props = *properties;
props.flags |= EPD_DRAW_BACKGROUND;
EpdRect temp = { .x = x, .y = y, .width = 0, .height = 0 };
if (*string == '\0')
return temp;
int minx = 100000, miny = 100000, maxx = -1, maxy = -1;
int temp_x = x;
int temp_y = y + font->ascender;
// Go through each line and get it's co-ordinates
uint32_t c;
while ((c = next_cp((const uint8_t**)&string))) {
if (c == 0x000A) // newline
{
temp_x = x;
temp_y += font->advance_y;
} else
get_char_bounds(font, c, &temp_x, &temp_y, &minx, &miny, &maxx, &maxy, &props);
}
temp.width = maxx - x + (margin * 2);
temp.height = maxy - miny + (margin * 2);
return temp;
}
void epd_get_text_bounds(
const EpdFont* font,
const char* string,
const int* x,
const int* y,
int* x1,
int* y1,
int* w,
int* h,
const EpdFontProperties* properties
) {
// FIXME: Does not respect alignment!
assert(properties != NULL);
EpdFontProperties props = *properties;
if (*string == '\0') {
*w = 0;
*h = 0;
*y1 = *y;
*x1 = *x;
return;
}
int minx = 100000, miny = 100000, maxx = -1, maxy = -1;
int original_x = *x;
int temp_x = *x;
int temp_y = *y;
uint32_t c;
while ((c = next_cp((const uint8_t**)&string))) {
get_char_bounds(font, c, &temp_x, &temp_y, &minx, &miny, &maxx, &maxy, &props);
}
*x1 = min(original_x, minx);
*w = maxx - *x1;
*y1 = miny;
*h = maxy - miny;
}
static enum EpdDrawError epd_write_line(
const EpdFont* font,
const char* string,
int* cursor_x,
int* cursor_y,
uint8_t* framebuffer,
const EpdFontProperties* properties
) {
assert(framebuffer != NULL);
if (*string == '\0') {
return EPD_DRAW_SUCCESS;
}
assert(properties != NULL);
EpdFontProperties props = *properties;
enum EpdFontFlags alignment_mask
= EPD_DRAW_ALIGN_LEFT | EPD_DRAW_ALIGN_RIGHT | EPD_DRAW_ALIGN_CENTER;
enum EpdFontFlags alignment = props.flags & alignment_mask;
// alignments are mutually exclusive!
if ((alignment & (alignment - 1)) != 0) {
return EPD_DRAW_INVALID_FONT_FLAGS;
}
int x1 = 0, y1 = 0, w = 0, h = 0;
int tmp_cur_x = *cursor_x;
int tmp_cur_y = *cursor_y;
epd_get_text_bounds(font, string, &tmp_cur_x, &tmp_cur_y, &x1, &y1, &w, &h, &props);
// no printable characters
if (w < 0 || h < 0) {
return EPD_DRAW_NO_DRAWABLE_CHARACTERS;
}
uint8_t* buffer = framebuffer;
int local_cursor_x = *cursor_x;
int local_cursor_y = *cursor_y;
uint32_t c;
int cursor_x_init = local_cursor_x;
int cursor_y_init = local_cursor_y;
switch (alignment) {
case EPD_DRAW_ALIGN_LEFT: {
break;
}
case EPD_DRAW_ALIGN_CENTER: {
local_cursor_x -= w / 2;
break;
}
case EPD_DRAW_ALIGN_RIGHT: {
local_cursor_x -= w;
break;
}
default:
break;
}
uint8_t bg = props.bg_color;
if (props.flags & EPD_DRAW_BACKGROUND) {
for (int l = local_cursor_y - font->ascender; l < local_cursor_y - font->descender; l++) {
epd_draw_hline(local_cursor_x, l, w, bg << 4, buffer);
}
}
enum EpdDrawError err = EPD_DRAW_SUCCESS;
while ((c = next_cp((const uint8_t**)&string))) {
err |= draw_char(font, buffer, &local_cursor_x, local_cursor_y, c, &props);
}
*cursor_x += local_cursor_x - cursor_x_init;
*cursor_y += local_cursor_y - cursor_y_init;
return err;
}
enum EpdDrawError epd_write_default(
const EpdFont* font, const char* string, int* cursor_x, int* cursor_y, uint8_t* framebuffer
) {
const EpdFontProperties props = epd_font_properties_default();
return epd_write_string(font, string, cursor_x, cursor_y, framebuffer, &props);
}
enum EpdDrawError epd_write_string(
const EpdFont* font,
const char* string,
int* cursor_x,
int* cursor_y,
uint8_t* framebuffer,
const EpdFontProperties* properties
) {
char *token, *newstring, *tofree;
if (string == NULL) {
ESP_LOGE("font.c", "cannot draw a NULL string!");
return EPD_DRAW_STRING_INVALID;
}
tofree = newstring = strdup(string);
if (newstring == NULL) {
ESP_LOGE("font.c", "cannot allocate string copy!");
return EPD_DRAW_FAILED_ALLOC;
}
enum EpdDrawError err = EPD_DRAW_SUCCESS;
// taken from the strsep manpage
int line_start = *cursor_x;
while ((token = strsep(&newstring, "\n")) != NULL) {
*cursor_x = line_start;
err |= epd_write_line(font, token, cursor_x, cursor_y, framebuffer, properties);
*cursor_y += font->advance_y;
}
free(tofree);
return err;
}

View File

@ -0,0 +1,216 @@
/**
* High-level API implementation for epdiy.
*/
#include <assert.h>
#include <esp_heap_caps.h>
#include <esp_log.h>
#include <esp_timer.h>
#include <esp_types.h>
#include <string.h>
#include "epd_highlevel.h"
#include "epdiy.h"
#ifndef _swap_int
#define _swap_int(a, b) \
{ \
int t = a; \
a = b; \
b = t; \
}
#endif
static bool already_initialized = 0;
EpdiyHighlevelState epd_hl_init(const EpdWaveform* waveform) {
assert(!already_initialized);
if (waveform == NULL) {
waveform = epd_get_display()->default_waveform;
}
int fb_size = epd_width() / 2 * epd_height();
#if !(defined(CONFIG_ESP32_SPIRAM_SUPPORT) || defined(CONFIG_ESP32S3_SPIRAM_SUPPORT))
ESP_LOGW(
"EPDiy", "Please enable PSRAM for the ESP32 (menuconfig→ Component config→ ESP32-specific)"
);
#endif
EpdiyHighlevelState state;
state.back_fb = heap_caps_aligned_alloc(16, fb_size, MALLOC_CAP_SPIRAM);
assert(state.back_fb != NULL);
state.front_fb = heap_caps_aligned_alloc(16, fb_size, MALLOC_CAP_SPIRAM);
assert(state.front_fb != NULL);
state.difference_fb = heap_caps_aligned_alloc(16, 2 * fb_size, MALLOC_CAP_SPIRAM);
assert(state.difference_fb != NULL);
state.dirty_lines = malloc(epd_height() * sizeof(bool));
assert(state.dirty_lines != NULL);
state.dirty_columns
= heap_caps_aligned_alloc(16, epd_width() / 2, MALLOC_CAP_INTERNAL | MALLOC_CAP_8BIT);
assert(state.dirty_columns != NULL);
state.waveform = waveform;
memset(state.front_fb, 0xFF, fb_size);
memset(state.back_fb, 0xFF, fb_size);
already_initialized = true;
return state;
}
uint8_t* epd_hl_get_framebuffer(EpdiyHighlevelState* state) {
assert(state != NULL);
return state->front_fb;
}
enum EpdDrawError epd_hl_update_screen(
EpdiyHighlevelState* state, enum EpdDrawMode mode, int temperature
) {
return epd_hl_update_area(state, mode, temperature, epd_full_screen());
}
EpdRect _inverse_rotated_area(uint16_t x, uint16_t y, uint16_t w, uint16_t h) {
// If partial update uses full screen do not rotate anything
if (!(x == 0 && y == 0 && epd_width() == w && epd_height() == h)) {
// invert the current display rotation
switch (epd_get_rotation()) {
// 0 landscape: Leave it as is
case EPD_ROT_LANDSCAPE:
break;
// 1 90 ° clockwise
case EPD_ROT_PORTRAIT:
_swap_int(x, y);
_swap_int(w, h);
x = epd_width() - x - w;
break;
case EPD_ROT_INVERTED_LANDSCAPE:
// 3 180°
x = epd_width() - x - w;
y = epd_height() - y - h;
break;
case EPD_ROT_INVERTED_PORTRAIT:
// 3 270 °
_swap_int(x, y);
_swap_int(w, h);
y = epd_height() - y - h;
break;
}
}
EpdRect rotated = { x, y, w, h };
return rotated;
}
enum EpdDrawError epd_hl_update_area(
EpdiyHighlevelState* state, enum EpdDrawMode mode, int temperature, EpdRect area
) {
assert(state != NULL);
// Not right to rotate here since this copies part of buffer directly
// Check rotation FIX
EpdRect rotated_area = _inverse_rotated_area(area.x, area.y, area.width, area.height);
area.x = rotated_area.x;
area.y = rotated_area.y;
area.width = rotated_area.width;
area.height = rotated_area.height;
uint32_t ts = esp_timer_get_time() / 1000;
// FIXME: use crop information here, if available
EpdRect diff_area = epd_difference_image_cropped(
state->front_fb,
state->back_fb,
area,
state->difference_fb,
state->dirty_lines,
state->dirty_columns
);
if (diff_area.height == 0 || diff_area.width == 0) {
return EPD_DRAW_SUCCESS;
}
uint32_t t1 = esp_timer_get_time() / 1000;
diff_area.x = 0;
diff_area.y = 0;
diff_area.width = epd_width();
diff_area.height = epd_height();
enum EpdDrawError err = EPD_DRAW_SUCCESS;
err = epd_draw_base(
epd_full_screen(),
state->difference_fb,
diff_area,
MODE_PACKING_1PPB_DIFFERENCE | mode,
temperature,
state->dirty_lines,
state->dirty_columns,
state->waveform
);
uint32_t t2 = esp_timer_get_time() / 1000;
diff_area.x = 0;
diff_area.y = 0;
diff_area.width = epd_width();
diff_area.height = epd_height();
int buf_width = epd_width();
for (int l = diff_area.y; l < diff_area.y + diff_area.height; l++) {
if (state->dirty_lines[l] > 0) {
uint8_t* lfb = state->front_fb + buf_width / 2 * l;
uint8_t* lbb = state->back_fb + buf_width / 2 * l;
int x = diff_area.x;
int x_last = diff_area.x + diff_area.width - 1;
if (x % 2) {
*(lbb + x / 2) = (*(lfb + x / 2) & 0xF0) | (*(lbb + x / 2) & 0x0F);
x += 1;
}
if (!(x_last % 2)) {
*(lbb + x_last / 2) = (*(lfb + x_last / 2) & 0x0F) | (*(lbb + x_last / 2) & 0xF0);
x_last -= 1;
}
memcpy(lbb + (x / 2), lfb + (x / 2), (x_last - x + 1) / 2);
}
}
uint32_t t3 = esp_timer_get_time() / 1000;
ESP_LOGI(
"epdiy",
"diff: %dms, draw: %dms, buffer update: %dms, total: %dms",
t1 - ts,
t2 - t1,
t3 - t2,
t3 - ts
);
return err;
}
void epd_hl_set_all_white(EpdiyHighlevelState* state) {
assert(state != NULL);
int fb_size = epd_width() / 2 * epd_height();
memset(state->front_fb, 0xFF, fb_size);
}
void epd_fullclear(EpdiyHighlevelState* state, int temperature) {
assert(state != NULL);
epd_hl_set_all_white(state);
enum EpdDrawError err = epd_hl_update_screen(state, MODE_GC16, temperature);
assert(err == EPD_DRAW_SUCCESS);
epd_clear();
}
void epd_hl_waveform(EpdiyHighlevelState* state, const EpdWaveform* waveform) {
if (waveform == NULL) {
waveform = epd_get_display()->default_waveform;
}
state->waveform = waveform;
}

View File

@ -0,0 +1,86 @@
#include <assert.h>
#include <esp_attr.h>
#include <esp_heap_caps.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include "line_queue.h"
#include "render_method.h"
static inline int ceil_div(int x, int y) {
return x / y + (x % y != 0);
}
/// Initialize the line queue and allocate memory.
LineQueue_t lq_init(int queue_len, int element_size) {
LineQueue_t queue;
queue.element_size = element_size;
queue.size = queue_len;
queue.current = 0;
queue.last = 0;
int elem_buf_size = ceil_div(element_size, 16) * 16;
queue.bufs = calloc(queue.size, elem_buf_size);
assert(queue.bufs != NULL);
for (int i = 0; i < queue.size; i++) {
queue.bufs[i] = heap_caps_aligned_alloc(16, elem_buf_size, MALLOC_CAP_INTERNAL);
assert(queue.bufs[i] != NULL);
}
return queue;
}
/// Deinitialize the line queue and free memory.
void lq_free(LineQueue_t* queue) {
for (int i = 0; i < queue->size; i++) {
heap_caps_free(queue->bufs[i]);
}
free(queue->bufs);
}
uint8_t* IRAM_ATTR lq_current(LineQueue_t* queue) {
int current = atomic_load_explicit(&queue->current, memory_order_acquire);
int last = atomic_load_explicit(&queue->last, memory_order_acquire);
if ((current + 1) % queue->size == last) {
return NULL;
}
return queue->bufs[current];
}
void IRAM_ATTR lq_commit(LineQueue_t* queue) {
int current = atomic_load_explicit(&queue->current, memory_order_acquire);
if (current == queue->size - 1) {
queue->current = 0;
} else {
atomic_fetch_add(&queue->current, 1);
}
}
int IRAM_ATTR lq_read(LineQueue_t* queue, uint8_t* dst) {
int current = atomic_load_explicit(&queue->current, memory_order_acquire);
int last = atomic_load_explicit(&queue->last, memory_order_acquire);
if (current == last) {
return -1;
}
memcpy(dst, queue->bufs[last], queue->element_size);
if (last == queue->size - 1) {
queue->last = 0;
} else {
atomic_fetch_add(&queue->last, 1);
}
return 0;
}
void IRAM_ATTR lq_reset(LineQueue_t* queue) {
queue->current = 0;
queue->last = 0;
}

View File

@ -0,0 +1,40 @@
#pragma once
#include <stdatomic.h>
#include <stdbool.h>
#include <stddef.h>
#include <stdint.h>
/// Circular line queue with atomic read / write operations
/// and accelerated masking on the output buffer.
typedef struct {
int size;
atomic_int current;
atomic_int last;
uint8_t** bufs;
// size of an element
size_t element_size;
} LineQueue_t;
/// Initialize the line queue and allocate memory.
LineQueue_t lq_init(int queue_len, int element_size);
/// Deinitialize the line queue and free memory.
void lq_free(LineQueue_t* queue);
/// Pointer to the next empty element in the line queue.
///
/// NULL if the queue is currently full.
uint8_t* lq_current(LineQueue_t* queue);
/// Advance the line queue.
void lq_commit(LineQueue_t* queue);
/// Read from the line queue.
///
/// Returns 0 for a successful read to `dst`, -1 for a failed read (empty queue).
int lq_read(LineQueue_t* queue, uint8_t* dst);
/// Reset the queue into an empty state.
/// This operation is *not* atomic!
void lq_reset(LineQueue_t* queue);

View File

@ -0,0 +1,145 @@
#include <xtensa/config/core-isa.h>
#include <xtensa/config/core-matmap.h>
#include "sdkconfig.h"
#ifdef CONFIG_IDF_TARGET_ESP32S3
.text
.align 4
.global calc_epd_input_1ppB_1k_S3_VE_aligned
.type calc_epd_input_1ppB_1k_S3_VE_aligned,@function
// // CRASH AND BURN for debugging
// EE.MOVI.32.A q3, a2, 0
// EE.MOVI.32.A q3, a3, 1
// EE.MOVI.32.A q3, a4, 2
// EE.MOVI.32.A q3, a5, 3
// l8ui a10, a10, 0
// void calc_epd_input_1ppB_1k_S3_VE_aligned(
// const uint32_t *ld,
// uint8_t *epd_input,
// const uint8_t *conversion_lut,
// uint32_t epd_width
//);
calc_epd_input_1ppB_1k_S3_VE_aligned:
// input - a2
// output - a3
// lut - a4
// len - a5
entry a1, 32
// divide by 16 and do one loop lesss,
// because the last loop is special
srli a5, a5, 4
addi.n a5, a5, -1
// bitmasks for bit shift by multiplication
movi.n a10, 0x40001000
EE.MOVI.32.Q q4,a10,0
movi.n a10, 0x04000100
EE.MOVI.32.Q q4,a10,1
movi.n a10, 0x00400010
EE.MOVI.32.Q q4,a10,2
movi a10, 0x00040001
EE.MOVI.32.Q q4,a10,3
EE.ZERO.Q q0
EE.VLD.128.IP q1, a2, 16
// Instructions sometimes are in an unexpected order
// for best pipeline utilization
loopnez a5, .loop_end_lut_lookup
// q1, q0 contain the input bytes, zero-extended to bits bytes
EE.VZIP.8 q1, q0
// load 32-bit LUT results
EE.LDXQ.32 q2, q0, a4, 0, 6
EE.LDXQ.32 q2, q0, a4, 1, 7
EE.LDXQ.32 q2, q0, a4, 2, 4
EE.LDXQ.32 q2, q0, a4, 3, 5
EE.LDXQ.32 q3, q0, a4, 0, 2
EE.LDXQ.32 q3, q0, a4, 1, 3
EE.LDXQ.32 q3, q0, a4, 2, 0
EE.LDXQ.32 q3, q0, a4, 3, 1
EE.ZERO.ACCX
// zip to have 16bit LUT results in q2, q3 zeroes
EE.VUNZIP.16 q2, q3
// combine results with using multiply-add as shift-or
EE.VMULAS.U16.ACCX q2,q4
// load 32-bit LUT results
EE.LDXQ.32 q2, q1, a4, 0, 6
EE.LDXQ.32 q2, q1, a4, 1, 7
EE.LDXQ.32 q2, q1, a4, 2, 4
EE.LDXQ.32 q2, q1, a4, 3, 5
EE.LDXQ.32 q0, q1, a4, 0, 2
EE.LDXQ.32 q0, q1, a4, 1, 3
EE.LDXQ.32 q0, q1, a4, 2, 0
EE.LDXQ.32 q0, q1, a4, 3, 1
// store multiplication result in a6
RUR.ACCX_0 a6
s16i a6, a3, 2
EE.ZERO.ACCX
// zip to have 16bit LUT results in q2, q0 zeroes
EE.VUNZIP.16 q2, q0
// Combine second set of results and load the next data
EE.VMULAS.U16.ACCX.LD.IP q1, a2, 16, q2, q4
// store result in a6
RUR.ACCX_0 a6
s16i a6, a3, 0
addi.n a3, a3, 4
.loop_end_lut_lookup:
// Same as above, but in the last iteration
// we do not load to not access out of bounds.
EE.VZIP.8 q1, q0
EE.LDXQ.32 q2, q0, a4, 0, 6
EE.LDXQ.32 q2, q0, a4, 1, 7
EE.LDXQ.32 q2, q0, a4, 2, 4
EE.LDXQ.32 q2, q0, a4, 3, 5
EE.LDXQ.32 q3, q0, a4, 0, 2
EE.LDXQ.32 q3, q0, a4, 1, 3
EE.LDXQ.32 q3, q0, a4, 2, 0
EE.LDXQ.32 q3, q0, a4, 3, 1
EE.ZERO.ACCX
EE.VUNZIP.16 q2, q3
EE.VMULAS.U16.ACCX q2,q4
EE.LDXQ.32 q2, q1, a4, 0, 6
EE.LDXQ.32 q2, q1, a4, 1, 7
EE.LDXQ.32 q2, q1, a4, 2, 4
EE.LDXQ.32 q2, q1, a4, 3, 5
EE.LDXQ.32 q0, q1, a4, 0, 2
EE.LDXQ.32 q0, q1, a4, 1, 3
EE.LDXQ.32 q0, q1, a4, 2, 0
EE.LDXQ.32 q0, q1, a4, 3, 1
RUR.ACCX_0 a6
s16i a6, a3, 2
EE.ZERO.ACCX
EE.VUNZIP.16 q2, q0
EE.VMULAS.U16.ACCX q2, q4
RUR.ACCX_0 a6
s16i a6, a3, 0
movi.n a2, 0 // return status ESP_OK
retw.n
#endif

View File

@ -0,0 +1,495 @@
#include "lut.h"
#include "epdiy.h"
#include "esp_attr.h"
#include "render_context.h"
#include "render_method.h"
#include <stdint.h>
#include <stdlib.h>
#include <string.h>
#include "esp_timer.h"
/*
* Build Lookup tables and translate via LUTs.
* WARNING: These functions must only ever write to internal memory,
* Since we disable the PSRAM workaround here for performance reasons.
*/
/* Python script for generating the 8ppB, starting at white lookup table:
for i in range(256):
number = 0;
for b in range(8):
if not (i & (1 << b)):
number |= 1 << (2*b)
print ('0x%04x,'%number)
*/
const uint32_t lut_8ppB_start_at_white[256] = {
0x5555, 0x5554, 0x5551, 0x5550, 0x5545, 0x5544, 0x5541, 0x5540, 0x5515, 0x5514, 0x5511, 0x5510,
0x5505, 0x5504, 0x5501, 0x5500, 0x5455, 0x5454, 0x5451, 0x5450, 0x5445, 0x5444, 0x5441, 0x5440,
0x5415, 0x5414, 0x5411, 0x5410, 0x5405, 0x5404, 0x5401, 0x5400, 0x5155, 0x5154, 0x5151, 0x5150,
0x5145, 0x5144, 0x5141, 0x5140, 0x5115, 0x5114, 0x5111, 0x5110, 0x5105, 0x5104, 0x5101, 0x5100,
0x5055, 0x5054, 0x5051, 0x5050, 0x5045, 0x5044, 0x5041, 0x5040, 0x5015, 0x5014, 0x5011, 0x5010,
0x5005, 0x5004, 0x5001, 0x5000, 0x4555, 0x4554, 0x4551, 0x4550, 0x4545, 0x4544, 0x4541, 0x4540,
0x4515, 0x4514, 0x4511, 0x4510, 0x4505, 0x4504, 0x4501, 0x4500, 0x4455, 0x4454, 0x4451, 0x4450,
0x4445, 0x4444, 0x4441, 0x4440, 0x4415, 0x4414, 0x4411, 0x4410, 0x4405, 0x4404, 0x4401, 0x4400,
0x4155, 0x4154, 0x4151, 0x4150, 0x4145, 0x4144, 0x4141, 0x4140, 0x4115, 0x4114, 0x4111, 0x4110,
0x4105, 0x4104, 0x4101, 0x4100, 0x4055, 0x4054, 0x4051, 0x4050, 0x4045, 0x4044, 0x4041, 0x4040,
0x4015, 0x4014, 0x4011, 0x4010, 0x4005, 0x4004, 0x4001, 0x4000, 0x1555, 0x1554, 0x1551, 0x1550,
0x1545, 0x1544, 0x1541, 0x1540, 0x1515, 0x1514, 0x1511, 0x1510, 0x1505, 0x1504, 0x1501, 0x1500,
0x1455, 0x1454, 0x1451, 0x1450, 0x1445, 0x1444, 0x1441, 0x1440, 0x1415, 0x1414, 0x1411, 0x1410,
0x1405, 0x1404, 0x1401, 0x1400, 0x1155, 0x1154, 0x1151, 0x1150, 0x1145, 0x1144, 0x1141, 0x1140,
0x1115, 0x1114, 0x1111, 0x1110, 0x1105, 0x1104, 0x1101, 0x1100, 0x1055, 0x1054, 0x1051, 0x1050,
0x1045, 0x1044, 0x1041, 0x1040, 0x1015, 0x1014, 0x1011, 0x1010, 0x1005, 0x1004, 0x1001, 0x1000,
0x0555, 0x0554, 0x0551, 0x0550, 0x0545, 0x0544, 0x0541, 0x0540, 0x0515, 0x0514, 0x0511, 0x0510,
0x0505, 0x0504, 0x0501, 0x0500, 0x0455, 0x0454, 0x0451, 0x0450, 0x0445, 0x0444, 0x0441, 0x0440,
0x0415, 0x0414, 0x0411, 0x0410, 0x0405, 0x0404, 0x0401, 0x0400, 0x0155, 0x0154, 0x0151, 0x0150,
0x0145, 0x0144, 0x0141, 0x0140, 0x0115, 0x0114, 0x0111, 0x0110, 0x0105, 0x0104, 0x0101, 0x0100,
0x0055, 0x0054, 0x0051, 0x0050, 0x0045, 0x0044, 0x0041, 0x0040, 0x0015, 0x0014, 0x0011, 0x0010,
0x0005, 0x0004, 0x0001, 0x0000
};
/* Python script for generating the 8ppB, starting at black lookup table:
for i in range(256):
number = 0;
for b in range(8):
if (i & (1 << b)):
number |= 2 << (2*b)
print ('0x%04x,'%number)
*/
const uint32_t lut_8ppB_start_at_black[256] = {
0x0000, 0x0002, 0x0008, 0x000a, 0x0020, 0x0022, 0x0028, 0x002a, 0x0080, 0x0082, 0x0088, 0x008a,
0x00a0, 0x00a2, 0x00a8, 0x00aa, 0x0200, 0x0202, 0x0208, 0x020a, 0x0220, 0x0222, 0x0228, 0x022a,
0x0280, 0x0282, 0x0288, 0x028a, 0x02a0, 0x02a2, 0x02a8, 0x02aa, 0x0800, 0x0802, 0x0808, 0x080a,
0x0820, 0x0822, 0x0828, 0x082a, 0x0880, 0x0882, 0x0888, 0x088a, 0x08a0, 0x08a2, 0x08a8, 0x08aa,
0x0a00, 0x0a02, 0x0a08, 0x0a0a, 0x0a20, 0x0a22, 0x0a28, 0x0a2a, 0x0a80, 0x0a82, 0x0a88, 0x0a8a,
0x0aa0, 0x0aa2, 0x0aa8, 0x0aaa, 0x2000, 0x2002, 0x2008, 0x200a, 0x2020, 0x2022, 0x2028, 0x202a,
0x2080, 0x2082, 0x2088, 0x208a, 0x20a0, 0x20a2, 0x20a8, 0x20aa, 0x2200, 0x2202, 0x2208, 0x220a,
0x2220, 0x2222, 0x2228, 0x222a, 0x2280, 0x2282, 0x2288, 0x228a, 0x22a0, 0x22a2, 0x22a8, 0x22aa,
0x2800, 0x2802, 0x2808, 0x280a, 0x2820, 0x2822, 0x2828, 0x282a, 0x2880, 0x2882, 0x2888, 0x288a,
0x28a0, 0x28a2, 0x28a8, 0x28aa, 0x2a00, 0x2a02, 0x2a08, 0x2a0a, 0x2a20, 0x2a22, 0x2a28, 0x2a2a,
0x2a80, 0x2a82, 0x2a88, 0x2a8a, 0x2aa0, 0x2aa2, 0x2aa8, 0x2aaa, 0x8000, 0x8002, 0x8008, 0x800a,
0x8020, 0x8022, 0x8028, 0x802a, 0x8080, 0x8082, 0x8088, 0x808a, 0x80a0, 0x80a2, 0x80a8, 0x80aa,
0x8200, 0x8202, 0x8208, 0x820a, 0x8220, 0x8222, 0x8228, 0x822a, 0x8280, 0x8282, 0x8288, 0x828a,
0x82a0, 0x82a2, 0x82a8, 0x82aa, 0x8800, 0x8802, 0x8808, 0x880a, 0x8820, 0x8822, 0x8828, 0x882a,
0x8880, 0x8882, 0x8888, 0x888a, 0x88a0, 0x88a2, 0x88a8, 0x88aa, 0x8a00, 0x8a02, 0x8a08, 0x8a0a,
0x8a20, 0x8a22, 0x8a28, 0x8a2a, 0x8a80, 0x8a82, 0x8a88, 0x8a8a, 0x8aa0, 0x8aa2, 0x8aa8, 0x8aaa,
0xa000, 0xa002, 0xa008, 0xa00a, 0xa020, 0xa022, 0xa028, 0xa02a, 0xa080, 0xa082, 0xa088, 0xa08a,
0xa0a0, 0xa0a2, 0xa0a8, 0xa0aa, 0xa200, 0xa202, 0xa208, 0xa20a, 0xa220, 0xa222, 0xa228, 0xa22a,
0xa280, 0xa282, 0xa288, 0xa28a, 0xa2a0, 0xa2a2, 0xa2a8, 0xa2aa, 0xa800, 0xa802, 0xa808, 0xa80a,
0xa820, 0xa822, 0xa828, 0xa82a, 0xa880, 0xa882, 0xa888, 0xa88a, 0xa8a0, 0xa8a2, 0xa8a8, 0xa8aa,
0xaa00, 0xaa02, 0xaa08, 0xaa0a, 0xaa20, 0xaa22, 0xaa28, 0xaa2a, 0xaa80, 0xaa82, 0xaa88, 0xaa8a,
0xaaa0, 0xaaa2, 0xaaa8, 0xaaaa,
};
static inline int min(int x, int y) {
return x < y ? x : y;
}
static inline int max(int x, int y) {
return x > y ? x : y;
}
// status tracker for row skipping
uint32_t skipping;
__attribute__((optimize("O3"))) void IRAM_ATTR
reorder_line_buffer(uint32_t* line_data, int buf_len) {
for (uint32_t i = 0; i < buf_len / 4; i++) {
uint32_t val = *line_data;
*(line_data++) = val >> 16 | ((val & 0x0000FFFF) << 16);
}
}
__attribute__((optimize("O3"))) void IRAM_ATTR
bit_shift_buffer_right(uint8_t* buf, uint32_t len, int shift) {
uint8_t carry = 0xFF << (8 - shift);
for (uint32_t i = 0; i < len; i++) {
uint8_t val = buf[i];
buf[i] = (val >> shift) | carry;
carry = val << (8 - shift);
}
}
__attribute__((optimize("O3"))) void IRAM_ATTR
nibble_shift_buffer_right(uint8_t* buf, uint32_t len) {
uint8_t carry = 0xF;
for (uint32_t i = 0; i < len; i++) {
uint8_t val = buf[i];
buf[i] = (val << 4) | carry;
carry = (val & 0xF0) >> 4;
}
}
__attribute__((optimize("O3"))) void IRAM_ATTR calc_epd_input_8ppB(
const uint32_t* line_data, uint8_t* epd_input, const uint8_t* lut, uint32_t epd_width
) {
uint32_t* wide_epd_input = (uint32_t*)epd_input;
uint8_t* data_ptr = (uint8_t*)line_data;
uint32_t* lut_32 = (uint32_t*)lut;
// this is reversed for little-endian, but this is later compensated
// through the output peripheral.
for (int j = 0; j < epd_width / 16; j++) {
uint8_t v1 = *(data_ptr++);
uint8_t v2 = *(data_ptr++);
wide_epd_input[j] = (lut_32[v2] << 16) | lut_32[v1];
}
// Account for missing line end if epd_width is not divisible by 16.
// We assume divisibility by 4.
for (int j = 0; j < (epd_width % 16) / 4; j++) {
uint8_t nibble = *data_ptr;
if (j % 2 == 1) {
nibble = nibble >> 4;
data_ptr++;
} else {
nibble = nibble & 0xF;
}
epd_input[(epd_width / 16) * 4 + j] = lut_32[nibble];
}
}
/**
* Look up 4 pixels of a differential image in a LUT constructed for use with vector extensions.
*/
__attribute__((optimize("O3"))) static inline uint8_t lookup_pixels_in_VE_LUT(
const uint32_t in, const uint8_t* conversion_lut
) {
uint32_t* padded_lut = (uint32_t*)conversion_lut;
uint8_t out = padded_lut[(in >> 24) & 0xFF] << 6;
out |= padded_lut[(in >> 16) & 0xFF] << 4;
out |= padded_lut[(in >> 8) & 0xFF] << 2;
out |= padded_lut[(in >> 0) & 0xFF];
return out;
}
/**
* Lookup accelerated by the S3 Vector Extensions.
* Expects aligned buffers and a length that is divisible by 16.
*/
void IRAM_ATTR calc_epd_input_1ppB_1k_S3_VE_aligned(
const uint32_t* ld, uint8_t* epd_input, const uint8_t* conversion_lut, uint32_t epd_width
);
#ifdef RENDER_METHOD_I2S
void calc_epd_input_1ppB_1k_S3_VE_aligned(
const uint32_t* ld, uint8_t* epd_input, const uint8_t* conversion_lut, uint32_t epd_width
) {
// dummy implementation, should never be called.
abort();
}
#endif
/**
* Lookup accelerated by the S3 Vector Extensions.
* Uses a 1K padded LUT (each entry takes up 32 bits)
*/
void IRAM_ATTR calc_epd_input_1ppB_1k_S3_VE(
const uint32_t* ld, uint8_t* epd_input, const uint8_t* conversion_lut, uint32_t epd_width
) {
// alignment boundaries in pixels
int unaligned_len_front = (16 - (uint32_t)ld % 16) % 16;
int unaligned_len_back = ((uint32_t)ld + epd_width) % 16;
int aligned_len = epd_width - unaligned_len_front - unaligned_len_back;
if (unaligned_len_front) {
for (int i = 0; i < unaligned_len_front / 4; i++) {
(*epd_input++) = lookup_pixels_in_VE_LUT((*ld++), conversion_lut);
}
}
calc_epd_input_1ppB_1k_S3_VE_aligned(ld, epd_input, conversion_lut, aligned_len);
ld += aligned_len / 4;
epd_input += aligned_len / 4;
if (unaligned_len_back) {
for (int i = 0; i < unaligned_len_back / 4; i++) {
(*epd_input++) = lookup_pixels_in_VE_LUT((*ld++), conversion_lut);
}
}
}
/**
* Calculate EPD input for a difference image with one pixel per byte.
*/
__attribute__((optimize("O3"))) void IRAM_ATTR calc_epd_input_1ppB_64k(
const uint32_t* ld, uint8_t* epd_input, const uint8_t* conversion_lut, uint32_t epd_width
) {
const uint16_t* lp = (uint16_t*)ld;
for (uint32_t j = 0; j < epd_width / 4; j++) {
epd_input[j] = (conversion_lut[lp[2 * j + 1]] << 4) | conversion_lut[lp[2 * j]];
}
}
__attribute__((optimize("O3"))) void IRAM_ATTR calc_epd_input_2ppB_lut_64k(
const uint32_t* line_data, uint8_t* epd_input, const uint8_t* conversion_lut, uint32_t epd_width
) {
const uint16_t* line_data_16 = (const uint16_t*)line_data;
for (uint32_t j = 0; j < epd_width / 4; j++) {
epd_input[j] = conversion_lut[*(line_data_16++)];
}
}
/**
* Look up 4 pixels in a 1K LUT with fixed "from" value.
*/
__attribute__((optimize("O3"))) static uint8_t lookup_pixels_2ppB_1k(
uint16_t in, const uint8_t* conversion_lut, uint8_t from
) {
uint8_t v;
uint8_t out;
v = ((in << 4) | from);
out = conversion_lut[v & 0xFF];
v = ((in & 0xF0) | from);
out |= (conversion_lut + 0x100)[v & 0xFF];
in = in >> 8;
v = ((in << 4) | from);
out |= (conversion_lut + 0x200)[v & 0xFF];
v = ((in & 0xF0) | from);
out |= (conversion_lut + 0x300)[v];
return out;
}
/**
* Calculate EPD input for a 2ppB buffer, but with a difference image LUT.
* This is used for small-LUT mode.
*/
__attribute__((optimize("O3"))) void IRAM_ATTR calc_epd_input_2ppB_1k_lut(
const uint32_t* ld,
uint8_t* epd_input,
const uint8_t* conversion_lut,
uint8_t from,
uint32_t epd_width
) {
const uint16_t* line_data_16 = (const uint16_t*)ld;
for (uint32_t j = 0; j < epd_width / 4; j++) {
epd_input[j] = lookup_pixels_2ppB_1k(*(line_data_16++), conversion_lut, from);
};
}
__attribute__((optimize("O3"))) void IRAM_ATTR calc_epd_input_2ppB_1k_lut_white(
const uint32_t* ld, uint8_t* epd_input, const uint8_t* conversion_lut, uint32_t epd_width
) {
calc_epd_input_2ppB_1k_lut(ld, epd_input, conversion_lut, 0xF, epd_width);
}
__attribute__((optimize("O3"))) void IRAM_ATTR calc_epd_input_2ppB_1k_lut_black(
const uint32_t* ld, uint8_t* epd_input, const uint8_t* conversion_lut, uint32_t epd_width
) {
calc_epd_input_2ppB_1k_lut(ld, epd_input, conversion_lut, 0x0, epd_width);
}
///////////////////////////// Calculate Lookup Tables
//////////////////////////////////
/**
* Unpack the waveform data into a lookup table, with bit shifted copies.
*/
__attribute__((optimize("O3"))) static void IRAM_ATTR
build_2ppB_lut_1k(uint8_t* lut, const EpdWaveformPhases* phases, int frame) {
const uint8_t* p_lut = phases->luts + (16 * 4 * frame);
for (uint8_t to = 0; to < 16; to++) {
for (uint8_t from_packed = 0; from_packed < 4; from_packed++) {
uint8_t index = (to << 4) | (from_packed * 4);
uint8_t packed = *(p_lut++);
lut[index] = (packed >> 6) & 3;
lut[index + 1] = (packed >> 4) & 3;
lut[index + 2] = (packed >> 2) & 3;
lut[index + 3] = (packed >> 0) & 3;
// printf("%2X%2X%2X%2X (%d)", lut[index], lut[index + 1], lut[index + 2],
// lut[index + 3], index);
}
// printf("\n");
}
uint32_t index = 0x100;
for (uint8_t s = 2; s <= 6; s += 2) {
for (int i = 0; i < 0x100; i++) {
lut[index] = lut[index % 0x100] << s;
index++;
}
}
}
/**
* Unpack the waveform data into a lookup table,
* 64k to loop up two bytes at once
*/
__attribute__((optimize("O3"))) static void IRAM_ATTR
build_1ppB_lut_64k(uint8_t* lut, const EpdWaveformPhases* phases, int frame) {
const uint8_t* p_lut = phases->luts + (16 * 4 * frame);
for (uint8_t to = 0; to < 16; to++) {
for (uint8_t from_packed = 0; from_packed < 4; from_packed++) {
uint8_t index = (to << 4) | (from_packed * 4);
uint8_t packed = *(p_lut++);
lut[index] = (packed >> 6) & 3;
lut[index + 1] = (packed >> 4) & 3;
lut[index + 2] = (packed >> 2) & 3;
lut[index + 3] = (packed >> 0) & 3;
// printf("%2X%2X%2X%2X (%d)", lut[index], lut[index + 1], lut[index + 2],
// lut[index + 3], index);
}
// printf("\n");
}
for (int outer = 0xFF; outer >= 0; outer--) {
uint32_t outer_result = lut[outer] << 2;
outer_result |= (outer_result << 16);
outer_result |= (outer_result << 8);
uint32_t* lut_section = (uint32_t*)(&lut[outer << 8]);
memcpy(lut_section, lut, 0x100);
for (int i = 0; i < 0x100 / 4; i++) {
lut_section[i] = lut_section[i] | outer_result;
}
}
}
/**
* A 32bit aligned lookup table for lookup using the ESP32-S3 vector extensions.
*/
__attribute__((optimize("O3"))) static void IRAM_ATTR
build_1ppB_lut_S3_VE_1k(uint8_t* lut, const EpdWaveformPhases* phases, int frame) {
uint32_t* lut32 = (uint32_t*)lut;
const uint8_t* p_lut = phases->luts + (16 * 4 * frame);
for (uint8_t to = 0; to < 16; to++) {
for (uint8_t from_packed = 0; from_packed < 4; from_packed++) {
uint8_t index = (to << 4) | (from_packed * 4);
uint8_t packed = *(p_lut++);
lut32[index] = (packed >> 6) & 3;
lut32[index + 1] = (packed >> 4) & 3;
lut32[index + 2] = (packed >> 2) & 3;
lut32[index + 3] = (packed >> 0) & 3;
}
}
}
/**
* Build a 16-bit LUT from the waveform if the previous color is
* known, e.g. all white or all black.
* This LUT is use to look up 4 pixels at once, as with the epdiy LUT.
*/
__attribute__((optimize("O3"))) static void build_2ppB_lut_64k_static_from(
uint8_t* lut, const EpdWaveformPhases* phases, uint8_t from, int frame
) {
const uint8_t* p_lut = phases->luts + (16 * 4 * frame);
/// index into the packed "from" row
uint8_t fi = from >> 2;
/// bit shift amount for the packed "from" row
uint8_t fs = 6 - 2 * (from & 3);
// populate the first 4096 bytes
uint8_t v1 = 0;
uint32_t s1 = 0;
for (uint8_t t2 = 0; t2 < 16; t2++) {
uint8_t v2 = ((p_lut[(t2 << 2) + fi] >> fs) & 0x03) << 4;
uint32_t s2 = t2 << 8;
for (uint8_t t3 = 0; t3 < 16; t3++) {
uint8_t v3 = ((p_lut[(t3 << 2) + fi] >> fs) & 0x03) << 2;
uint32_t s3 = t3 << 4;
for (uint8_t t4 = 0; t4 < 16; t4++) {
uint8_t v4 = ((p_lut[(t4 << 2) + fi] >> fs) & 0x03) << 0;
uint32_t s4 = t4;
lut[s1 | s2 | s3 | s4] = v1 | v2 | v3 | v4;
}
}
}
// now just copy and the first 4096 bytes and add the upper two bits
for (uint8_t t1 = 1; t1 < 16; t1++) {
memcpy(&lut[t1 << 12], lut, 1 << 12);
}
for (int i = 0; i < 16; i++) {
uint32_t v1 = ((p_lut[(i << 2) + fi] >> fs) & 0x03);
uint32_t mask = (v1 << 30) | (v1 << 22) | (v1 << 14) | (v1 << 6);
for (int j = 0; j < 16 * 16 * 16 / 4; j++) {
((uint32_t*)lut)[(i << 10) + j] |= mask;
}
}
}
static void build_2ppB_lut_64k_from_0(uint8_t* lut, const EpdWaveformPhases* phases, int frame) {
build_2ppB_lut_64k_static_from(lut, phases, 0, frame);
}
static void build_2ppB_lut_64k_from_15(uint8_t* lut, const EpdWaveformPhases* phases, int frame) {
build_2ppB_lut_64k_static_from(lut, phases, 0xF, frame);
}
static void build_8ppB_lut_256b_from_white(
uint8_t* lut, const EpdWaveformPhases* phases, int frame
) {
memcpy(lut, lut_8ppB_start_at_white, sizeof(lut_8ppB_start_at_white));
}
static void build_8ppB_lut_256b_from_black(
uint8_t* lut, const EpdWaveformPhases* phases, int frame
) {
memcpy(lut, lut_8ppB_start_at_black, sizeof(lut_8ppB_start_at_black));
}
void IRAM_ATTR epd_apply_line_mask(uint8_t* buf, const uint8_t* mask, int len) {
for (int i = 0; i < len / 4; i++) {
((uint32_t*)buf)[i] &= ((uint32_t*)mask)[i];
}
}
LutFunctionPair find_lut_functions(enum EpdDrawMode mode, uint32_t lut_size) {
LutFunctionPair pair;
pair.build_func = NULL;
pair.lookup_func = NULL;
if (mode & MODE_PACKING_1PPB_DIFFERENCE) {
if (EPD_CURRENT_RENDER_METHOD == RENDER_METHOD_LCD && !(mode & MODE_FORCE_NO_PIE)
&& lut_size >= 1024) {
pair.build_func = &build_1ppB_lut_S3_VE_1k;
pair.lookup_func = &calc_epd_input_1ppB_1k_S3_VE;
return pair;
} else if (lut_size >= 1 << 16) {
pair.build_func = &build_1ppB_lut_64k;
pair.lookup_func = &calc_epd_input_1ppB_64k;
return pair;
}
} else if (mode & MODE_PACKING_2PPB) {
if (lut_size >= 1 << 16) {
if (mode & PREVIOUSLY_WHITE) {
pair.build_func = &build_2ppB_lut_64k_from_15;
pair.lookup_func = &calc_epd_input_2ppB_lut_64k;
return pair;
} else if (mode & PREVIOUSLY_BLACK) {
pair.build_func = &build_2ppB_lut_64k_from_0;
pair.lookup_func = &calc_epd_input_2ppB_lut_64k;
return pair;
}
} else if (lut_size >= 1024) {
if (mode & PREVIOUSLY_WHITE) {
pair.build_func = &build_2ppB_lut_1k;
pair.lookup_func = &calc_epd_input_2ppB_1k_lut_white;
return pair;
} else if (mode & PREVIOUSLY_BLACK) {
pair.build_func = &build_2ppB_lut_1k;
pair.lookup_func = &calc_epd_input_2ppB_1k_lut_black;
return pair;
}
}
} else if (mode & MODE_PACKING_8PPB) {
if (lut_size < sizeof(lut_8ppB_start_at_white)) {
return pair;
}
if (mode & PREVIOUSLY_WHITE) {
pair.build_func = &build_8ppB_lut_256b_from_white;
pair.lookup_func = &calc_epd_input_8ppB;
return pair;
} else if (mode & PREVIOUSLY_BLACK) {
pair.build_func = &build_8ppB_lut_256b_from_black;
pair.lookup_func = &calc_epd_input_8ppB;
return pair;
}
}
return pair;
}

View File

@ -0,0 +1,47 @@
#pragma once
#include <stdint.h>
#include "epdiy.h"
// Make a block of 4 pixels lighter on the EPD.
#define CLEAR_BYTE 0B10101010
// Make a block of 4 pixels darker on the EPD.
#define DARK_BYTE 0B01010101
/**
* Type signature of a framebuffer to display output lookup function.
*/
typedef void (*lut_func_t)(
const uint32_t* line_buffer, uint8_t* epd_input, const uint8_t* lut, uint32_t epd_width
);
/**
* Type signature of a LUT preparation function.
*/
typedef void (*lut_build_func_t)(uint8_t* lut, const EpdWaveformPhases* phases, int frame);
typedef struct {
lut_build_func_t build_func;
lut_func_t lookup_func;
} LutFunctionPair;
/**
* Select the appropriate LUT building and lookup function
* for the selected draw mode and allocated LUT size.
*/
LutFunctionPair find_lut_functions(enum EpdDrawMode mode, uint32_t lut_size);
/*
* Reorder the output buffer to account for I2S FIFO order.
*/
void reorder_line_buffer(uint32_t* line_data, int buf_len);
/**
* Apply a mask to a line buffer.
* `len` must be divisible by 4.
*/
void epd_apply_line_mask(uint8_t* buf, const uint8_t* mask, int len);
// legacy functions
void bit_shift_buffer_right(uint8_t* buf, uint32_t len, int shift);
void nibble_shift_buffer_right(uint8_t* buf, uint32_t len);

View File

@ -0,0 +1,106 @@
#include "render_context.h"
#include <string.h>
#include "esp_log.h"
#include "../epdiy.h"
#include "lut.h"
#include "render_method.h"
/// For waveforms without timing and the I2S diving method,
/// the default hold time for each line is 12us
const static int DEFAULT_FRAME_TIME = 120;
static inline int min(int x, int y) {
return x < y ? x : y;
}
void get_buffer_params(
RenderContext_t* ctx,
int* bytes_per_line,
const uint8_t** start_ptr,
int* min_y,
int* max_y,
int* pixels_per_byte
) {
EpdRect area = ctx->area;
enum EpdDrawMode mode = ctx->mode;
const EpdRect crop_to = ctx->crop_to;
const bool horizontally_cropped = !(crop_to.x == 0 && crop_to.width == area.width);
const bool vertically_cropped = !(crop_to.y == 0 && crop_to.height == area.height);
// number of pixels per byte of input data
int width_divider = 0;
if (mode & MODE_PACKING_1PPB_DIFFERENCE) {
*bytes_per_line = area.width;
width_divider = 1;
} else if (mode & MODE_PACKING_2PPB) {
*bytes_per_line = area.width / 2 + area.width % 2;
width_divider = 2;
} else if (mode & MODE_PACKING_8PPB) {
*bytes_per_line = (area.width / 8 + (area.width % 8 > 0));
width_divider = 8;
} else {
ctx->error |= EPD_DRAW_INVALID_PACKING_MODE;
}
int crop_x = (horizontally_cropped ? crop_to.x : 0);
int crop_y = (vertically_cropped ? crop_to.y : 0);
int crop_h = (vertically_cropped ? crop_to.height : 0);
const uint8_t* ptr_start = ctx->data_ptr;
// Adjust for negative starting coordinates with optional crop
if (area.x - crop_x < 0) {
ptr_start += -(area.x - crop_x) / width_divider;
}
if (area.y - crop_y < 0) {
ptr_start += -(area.y - crop_y) * *bytes_per_line;
}
// calculate start and end row with crop
*min_y = area.y + crop_y;
*max_y = min(*min_y + (vertically_cropped ? crop_h : area.height), area.height);
*start_ptr = ptr_start;
*pixels_per_byte = width_divider;
}
void IRAM_ATTR prepare_context_for_next_frame(RenderContext_t* ctx) {
int frame_time = DEFAULT_FRAME_TIME;
if (ctx->phase_times != NULL) {
frame_time = ctx->phase_times[ctx->current_frame];
}
if (ctx->mode & MODE_EPDIY_MONOCHROME) {
frame_time = MONOCHROME_FRAME_TIME;
}
ctx->frame_time = frame_time;
const EpdWaveformPhases* phases
= ctx->waveform->mode_data[ctx->waveform_index]->range_data[ctx->waveform_range];
assert(ctx->lut_build_func != NULL);
ctx->lut_build_func(ctx->conversion_lut, phases, ctx->current_frame);
ctx->lines_prepared = 0;
ctx->lines_consumed = 0;
}
void epd_populate_line_mask(uint8_t* line_mask, const uint8_t* dirty_columns, int mask_len) {
if (dirty_columns == NULL) {
memset(line_mask, 0xFF, mask_len);
} else {
int pixels = mask_len * 4;
for (int c = 0; c < pixels / 2; c += 2) {
uint8_t mask = 0;
mask |= (dirty_columns[c + 1] & 0xF0) != 0 ? 0xC0 : 0x00;
mask |= (dirty_columns[c + 1] & 0x0F) != 0 ? 0x30 : 0x00;
mask |= (dirty_columns[c] & 0xF0) != 0 ? 0x0C : 0x00;
mask |= (dirty_columns[c] & 0x0F) != 0 ? 0x03 : 0x00;
line_mask[c / 2] = mask;
}
}
}

View File

@ -0,0 +1,110 @@
#pragma once
#include <freertos/FreeRTOS.h>
#include <freertos/semphr.h>
#include <freertos/task.h>
#include <stdatomic.h>
#include <stdint.h>
#include "../epdiy.h"
#include "line_queue.h"
#include "lut.h"
#define NUM_RENDER_THREADS 2
typedef struct {
EpdRect area;
EpdRect crop_to;
const bool* drawn_lines;
const uint8_t* data_ptr;
/// The display width for quick access.
int display_width;
/// The display height for quick access.
int display_height;
/// index of the next line of data to process
atomic_int lines_prepared;
volatile int lines_consumed;
int lines_total;
/// frame currently in the current update cycle
int current_frame;
/// number of frames in the current update cycle
int cycle_frames;
TaskHandle_t feed_tasks[NUM_RENDER_THREADS];
SemaphoreHandle_t feed_done_smphr[NUM_RENDER_THREADS];
SemaphoreHandle_t frame_done;
/// Line buffers for feed tasks
uint8_t* feed_line_buffers[NUM_RENDER_THREADS];
/// index of the waveform mode when using vendor waveforms.
/// This is not necessarily the mode number if the waveform header
// only contains a selection of modes!
int waveform_index;
/// waveform range when using vendor waveforms
int waveform_range;
/// Draw time for the current frame in 1/10ths of us.
int frame_time;
const int* phase_times;
const EpdWaveform* waveform;
enum EpdDrawMode mode;
enum EpdDrawError error;
// Lookup table size.
size_t conversion_lut_size;
// Lookup table space.
uint8_t* conversion_lut;
/// LUT lookup function. Must not be NULL.
lut_func_t lut_lookup_func;
/// LUT building function. Must not be NULL
lut_build_func_t lut_build_func;
/// Queue of lines prepared for output to the display,
/// one for each thread.
LineQueue_t line_queues[NUM_RENDER_THREADS];
uint8_t* line_threads;
// Output line mask
uint8_t* line_mask;
/// track line skipping when working in old i2s mode
int skipping;
/// line buffer when using epd_push_pixels
uint8_t* static_line_buffer;
} RenderContext_t;
/**
* Based on the render context, assign the bytes per line,
* framebuffer start pointer, min and max vertical positions and the pixels per byte.
*/
void get_buffer_params(
RenderContext_t* ctx,
int* bytes_per_line,
const uint8_t** start_ptr,
int* min_y,
int* max_y,
int* pixels_per_byte
);
/**
* Prepare the render context for drawing the next frame.
*
* (Reset counters, etc)
*/
void prepare_context_for_next_frame(RenderContext_t* ctx);
/**
* Populate an output line mask from line dirtyness with two bits per pixel.
* If the dirtyness data is NULL, set the mask to neutral.
*
* don't inline for to ensure availability in tests.
*/
void __attribute__((noinline)) epd_populate_line_mask(
uint8_t* line_mask, const uint8_t* dirty_columns, int mask_len
);

View File

@ -0,0 +1,10 @@
#include "render_method.h"
#include "sdkconfig.h"
#ifdef CONFIG_IDF_TARGET_ESP32
const enum EpdRenderMethod EPD_CURRENT_RENDER_METHOD = RENDER_METHOD_I2S;
#elif defined(CONFIG_IDF_TARGET_ESP32S3)
const enum EpdRenderMethod EPD_CURRENT_RENDER_METHOD = RENDER_METHOD_LCD;
#else
#error "unknown chip, cannot choose render method!"
#endif

View File

@ -0,0 +1,29 @@
#pragma once
#include "sdkconfig.h"
/**
* Rendering Method / Hardware to use.
*/
enum EpdRenderMethod {
/// Use the I2S peripheral on ESP32 chips.
RENDER_METHOD_I2S = 1,
/// Use the CAM/LCD peripheral in ESP32-S3 chips.
RENDER_METHOD_LCD = 2,
};
extern const enum EpdRenderMethod EPD_CURRENT_RENDER_METHOD;
#ifdef CONFIG_IDF_TARGET_ESP32
#define RENDER_METHOD_I2S 1
#elif defined(CONFIG_IDF_TARGET_ESP32S3)
#define RENDER_METHOD_LCD 1
#else
#error "unknown chip, cannot choose render method!"
#endif
#ifdef __clang__
#define IRAM_ATTR
// define this if we're using clangd to make it accept the GCC builtin
void __assert_func(const char* file, int line, const char* func, const char* failedexpr);
#endif

View File

@ -0,0 +1,303 @@
#include "i2s_data_bus.h"
#include "esp_intr_alloc.h"
#include "sdkconfig.h"
// the I2S driver is based on ESP32 registers and won't compile on the S3
#ifdef CONFIG_IDF_TARGET_ESP32
#include "driver/rtc_io.h"
#include "esp_system.h"
#if ESP_IDF_VERSION < (4, 0, 0) || ARDUINO_ARCH_ESP32
#include "rom/gpio.h"
#include "rom/lldesc.h"
#else
#include "esp32/rom/gpio.h"
#include "esp32/rom/lldesc.h"
#endif
#include "esp_heap_caps.h"
#include "soc/i2s_reg.h"
#include "soc/i2s_struct.h"
#include "soc/rtc.h"
#include "esp_system.h" // for ESP_IDF_VERSION_VAL
#include "esp_private/periph_ctrl.h"
/// DMA descriptors for front and back line buffer.
/// We use two buffers, so one can be filled while the other
/// is transmitted.
typedef struct {
volatile lldesc_t* dma_desc_a;
volatile lldesc_t* dma_desc_b;
/// Front and back line buffer.
uint8_t* buf_a;
uint8_t* buf_b;
} i2s_parallel_state_t;
/// Indicates which line buffer is currently back / front.
static int current_buffer = 0;
/// The I2S state instance.
static i2s_parallel_state_t i2s_state;
static intr_handle_t gI2S_intr_handle = NULL;
/// Indicates the device has finished its transmission and is ready again.
static volatile bool output_done = true;
/// The start pulse pin extracted from the configuration for use in the "done"
/// interrupt.
static gpio_num_t start_pulse_pin;
/// Initializes a DMA descriptor.
static void fill_dma_desc(
volatile lldesc_t* dmadesc, uint8_t* buf, uint32_t buf_size, uint32_t buf_length
) {
assert(buf_length % 4 == 0); // Must be word aligned
dmadesc->size = buf_size;
dmadesc->length = buf_length;
dmadesc->buf = buf;
dmadesc->eof = 1;
dmadesc->sosf = 1;
dmadesc->owner = 1;
dmadesc->qe.stqe_next = 0;
dmadesc->offset = 0;
}
/// Address of the currently front DMA descriptor,
/// which uses only the lower 20bits (according to TRM)
uint32_t dma_desc_addr() {
return (uint32_t)(current_buffer ? i2s_state.dma_desc_a : i2s_state.dma_desc_b) & 0x000FFFFF;
}
// Helper function to help align values
static size_t align_up(size_t x, size_t a) {
return (size_t)(x + ((size_t)a - 1)) & ~(size_t)(a - 1);
}
/// Set up a GPIO as output and route it to a signal.
static void gpio_setup_out(int gpio, int sig, bool invert) {
if (gpio == -1)
return;
PIN_FUNC_SELECT(GPIO_PIN_MUX_REG[gpio], PIN_FUNC_GPIO);
gpio_set_direction(gpio, GPIO_MODE_DEF_OUTPUT);
gpio_matrix_out(gpio, sig, invert, false);
}
/// Resets "Start Pulse" signal when the current row output is done.
static void IRAM_ATTR i2s_int_hdl(void* arg) {
i2s_dev_t* dev = &I2S1;
if (dev->int_st.out_done) {
// gpio_set_level(start_pulse_pin, 1);
// gpio_set_level(GPIO_NUM_26, 0);
output_done = true;
}
// Clear the interrupt. Otherwise, the whole device would hang.
dev->int_clr.val = dev->int_raw.val;
}
uint8_t* IRAM_ATTR i2s_get_current_buffer() {
return (uint8_t*)(current_buffer ? i2s_state.dma_desc_a->buf : i2s_state.dma_desc_b->buf);
}
bool IRAM_ATTR i2s_is_busy() {
// DMA and FIFO must be done
return !output_done || !I2S1.state.tx_idle;
}
void IRAM_ATTR i2s_switch_buffer() {
// either device is done transmitting or the switch must be away from the
// buffer currently used by the DMA engine.
while (i2s_is_busy() && dma_desc_addr() != I2S1.out_link.addr) {
};
current_buffer = !current_buffer;
}
void IRAM_ATTR i2s_start_line_output() {
output_done = false;
i2s_dev_t* dev = &I2S1;
dev->conf.tx_start = 0;
dev->conf.tx_reset = 1;
dev->conf.tx_fifo_reset = 1;
dev->conf.rx_fifo_reset = 1;
dev->conf.tx_reset = 0;
dev->conf.tx_fifo_reset = 0;
dev->conf.rx_fifo_reset = 0;
dev->out_link.addr = dma_desc_addr();
dev->out_link.start = 1;
// sth is pulled up through peripheral interrupt
// This is timing-critical!
gpio_set_level(start_pulse_pin, 0);
dev->conf.tx_start = 1;
}
void i2s_gpio_attach(i2s_bus_config* cfg) {
gpio_num_t I2S_GPIO_BUS[] = { cfg->data_6, cfg->data_7, cfg->data_4, cfg->data_5,
cfg->data_2, cfg->data_3, cfg->data_0, cfg->data_1 };
gpio_set_direction(cfg->start_pulse, GPIO_MODE_OUTPUT);
gpio_set_level(cfg->start_pulse, 1);
// Use I2S1 with no signal offset (for some reason the offset seems to be
// needed in 16-bit mode, but not in 8 bit mode.
int signal_base = I2S1O_DATA_OUT0_IDX;
// Setup and route GPIOS
for (int x = 0; x < 8; x++) {
gpio_setup_out(I2S_GPIO_BUS[x], signal_base + x, false);
}
// Free CKH after wakeup
gpio_hold_dis(cfg->clock);
// Invert word select signal
gpio_setup_out(cfg->clock, I2S1O_WS_OUT_IDX, true);
}
void i2s_gpio_detach(i2s_bus_config* cfg) {
gpio_set_direction(cfg->data_0, GPIO_MODE_INPUT);
gpio_set_direction(cfg->data_1, GPIO_MODE_INPUT);
gpio_set_direction(cfg->data_2, GPIO_MODE_INPUT);
gpio_set_direction(cfg->data_3, GPIO_MODE_INPUT);
gpio_set_direction(cfg->data_4, GPIO_MODE_INPUT);
gpio_set_direction(cfg->data_5, GPIO_MODE_INPUT);
gpio_set_direction(cfg->data_6, GPIO_MODE_INPUT);
gpio_set_direction(cfg->data_7, GPIO_MODE_INPUT);
gpio_set_direction(cfg->start_pulse, GPIO_MODE_INPUT);
gpio_set_direction(cfg->clock, GPIO_MODE_INPUT);
gpio_reset_pin(cfg->clock);
if (cfg->clock != 5) {
rtc_gpio_isolate(cfg->clock);
}
}
void i2s_bus_init(i2s_bus_config* cfg, uint32_t epd_row_width) {
i2s_gpio_attach(cfg);
// store pin in global variable for use in interrupt.
start_pulse_pin = cfg->start_pulse;
periph_module_enable(PERIPH_I2S1_MODULE);
i2s_dev_t* dev = &I2S1;
// Initialize device
dev->conf.tx_reset = 1;
dev->conf.tx_reset = 0;
// Reset DMA
dev->lc_conf.in_rst = 1;
dev->lc_conf.in_rst = 0;
dev->lc_conf.out_rst = 1;
dev->lc_conf.out_rst = 0;
// Setup I2S config. See section 12 of Technical Reference Manual
// Enable LCD mode
dev->conf2.val = 0;
dev->conf2.lcd_en = 1;
// Enable FRAME1-Mode (See technical reference manual)
dev->conf2.lcd_tx_wrx2_en = 1;
dev->conf2.lcd_tx_sdx2_en = 0;
// Set to 8 bit parallel output
dev->sample_rate_conf.val = 0;
dev->sample_rate_conf.tx_bits_mod = 8;
// Half speed of bit clock in LCD mode.
// (Smallest possible divider according to the spec).
dev->sample_rate_conf.tx_bck_div_num = 2;
// Initialize Audio Clock (APLL)
rtc_clk_apll_enable(true);
rtc_clk_apll_coeff_set(0, 0, 0, 8);
// Set Audio Clock Dividers
dev->clkm_conf.val = 0;
dev->clkm_conf.clka_en = 1;
dev->clkm_conf.clkm_div_a = 1;
dev->clkm_conf.clkm_div_b = 0;
// 2 is the smallest possible divider according to the spec.
dev->clkm_conf.clkm_div_num = 2;
// Set up FIFO
dev->fifo_conf.val = 0;
dev->fifo_conf.tx_fifo_mod_force_en = 1;
dev->fifo_conf.tx_fifo_mod = 1;
dev->fifo_conf.tx_data_num = 32;
dev->fifo_conf.dscr_en = 1;
// Stop after transmission complete
dev->conf1.val = 0;
dev->conf1.tx_stop_en = 0;
dev->conf1.tx_pcm_bypass = 1;
// Configure TX channel
dev->conf_chan.val = 0;
dev->conf_chan.tx_chan_mod = 1;
dev->conf.tx_right_first = 1;
dev->timing.val = 0;
// Allocate DMA descriptors
const size_t buf_size = align_up(epd_row_width / 4, 4); // Buf size must be word aligned
i2s_state.buf_a = heap_caps_malloc(buf_size, MALLOC_CAP_DMA);
i2s_state.buf_b = heap_caps_malloc(buf_size, MALLOC_CAP_DMA);
i2s_state.dma_desc_a = heap_caps_malloc(sizeof(lldesc_t), MALLOC_CAP_DMA);
i2s_state.dma_desc_b = heap_caps_malloc(sizeof(lldesc_t), MALLOC_CAP_DMA);
// and fill them
fill_dma_desc(i2s_state.dma_desc_a, i2s_state.buf_a, epd_row_width / 4, buf_size);
fill_dma_desc(i2s_state.dma_desc_b, i2s_state.buf_b, epd_row_width / 4, buf_size);
// enable "done" interrupt
SET_PERI_REG_BITS(I2S_INT_ENA_REG(1), I2S_OUT_DONE_INT_ENA_V, 1, I2S_OUT_DONE_INT_ENA_S);
// register interrupt
esp_intr_alloc(ETS_I2S1_INTR_SOURCE, 0, i2s_int_hdl, 0, &gI2S_intr_handle);
// Reset FIFO/DMA
dev->lc_conf.in_rst = 1;
dev->lc_conf.out_rst = 1;
dev->lc_conf.ahbm_rst = 1;
dev->lc_conf.ahbm_fifo_rst = 1;
dev->lc_conf.in_rst = 0;
dev->lc_conf.out_rst = 0;
dev->lc_conf.ahbm_rst = 0;
dev->lc_conf.ahbm_fifo_rst = 0;
dev->conf.tx_reset = 1;
dev->conf.tx_fifo_reset = 1;
dev->conf.rx_fifo_reset = 1;
dev->conf.tx_reset = 0;
dev->conf.tx_fifo_reset = 0;
dev->conf.rx_fifo_reset = 0;
// Start dma on front buffer
dev->lc_conf.val = I2S_OUT_DATA_BURST_EN | I2S_OUTDSCR_BURST_EN | I2S_OUT_DATA_BURST_EN;
dev->out_link.addr = ((uint32_t)(i2s_state.dma_desc_a));
dev->out_link.start = 1;
dev->int_clr.val = dev->int_raw.val;
dev->int_ena.val = 0;
dev->int_ena.out_done = 1;
dev->conf.tx_start = 0;
}
void i2s_bus_deinit() {
esp_intr_disable(gI2S_intr_handle);
esp_intr_free(gI2S_intr_handle);
free(i2s_state.buf_a);
free(i2s_state.buf_b);
free((void*)i2s_state.dma_desc_a);
free((void*)i2s_state.dma_desc_b);
rtc_clk_apll_coeff_set(0, 0, 0, 8);
rtc_clk_apll_enable(true);
periph_module_disable(PERIPH_I2S1_MODULE);
}
#endif

View File

@ -0,0 +1,77 @@
/**
* Implements a 8bit parallel interface to transmit pixel
* data to the display, based on the I2S peripheral.
*/
#pragma once
#include <driver/gpio.h>
#include <esp_attr.h>
#include <soc/gpio_periph.h>
#include <soc/gpio_struct.h>
#include <soc/io_mux_reg.h>
#include <stdint.h>
/**
* I2S bus configuration parameters.
*/
typedef struct {
// GPIO numbers of the parallel bus pins.
gpio_num_t data_0;
gpio_num_t data_1;
gpio_num_t data_2;
gpio_num_t data_3;
gpio_num_t data_4;
gpio_num_t data_5;
gpio_num_t data_6;
gpio_num_t data_7;
// Data clock pin.
gpio_num_t clock;
// "Start Pulse", enabling data input on the slave device (active low)
gpio_num_t start_pulse;
} i2s_bus_config;
/**
* Initialize the I2S data bus for communication
* with a 8bit parallel display interface.
*/
void i2s_bus_init(i2s_bus_config* cfg, uint32_t epd_row_width);
/**
* Attach I2S to gpio's
*/
void i2s_gpio_attach(i2s_bus_config* cfg);
/**
* Detach I2S from gpio's
*/
void i2s_gpio_detach(i2s_bus_config* cfg);
/**
* Get the currently writable line buffer.
*/
uint8_t* i2s_get_current_buffer();
/**
* Switches front and back line buffer.
* If the switched-to line buffer is currently in use,
* this function blocks until transmission is done.
*/
void i2s_switch_buffer();
/**
* Start transmission of the current back buffer.
*/
void i2s_start_line_output();
/**
* Returns true if there is an ongoing transmission.
*/
bool i2s_is_busy();
/**
* Give up allocated resources.
*/
void i2s_bus_deinit();

View File

@ -0,0 +1,402 @@
#include "render_i2s.h"
#include "../output_common/render_method.h"
#ifdef RENDER_METHOD_I2S
#include <stdint.h>
#include <string.h>
#include <esp_log.h>
#include "epd_internals.h"
#include "epdiy.h"
// output a row to the display.
#include "../output_common/lut.h"
#include "../output_common/render_context.h"
#include "i2s_data_bus.h"
#include "rmt_pulse.h"
static const epd_ctrl_state_t NoChangeState = { 0 };
/**
* Waits until all previously submitted data has been written.
* Then, the following operations are initiated:
*
* - Previously submitted data is latched to the output register.
* - The RMT peripheral is set up to pulse the vertical (gate) driver for
* `output_time_dus` / 10 microseconds.
* - The I2S peripheral starts transmission of the current buffer to
* the source driver.
* - The line buffers are switched.
*
* This sequence of operations allows for pipelining data preparation and
* transfer, reducing total refresh times.
*/
static void IRAM_ATTR i2s_output_row(uint32_t output_time_dus) {
while (i2s_is_busy() || rmt_busy()) {
};
const EpdBoardDefinition* epd_board = epd_current_board();
epd_ctrl_state_t* ctrl_state = epd_ctrl_state();
epd_ctrl_state_t mask = NoChangeState;
ctrl_state->ep_sth = true;
ctrl_state->ep_latch_enable = true;
mask.ep_sth = true;
mask.ep_latch_enable = true;
epd_board->set_ctrl(ctrl_state, &mask);
mask = NoChangeState;
ctrl_state->ep_latch_enable = false;
mask.ep_latch_enable = true;
epd_board->set_ctrl(ctrl_state, &mask);
if (epd_get_display()->display_type == DISPLAY_TYPE_ED097TC2) {
pulse_ckv_ticks(output_time_dus, 1, false);
} else {
pulse_ckv_ticks(output_time_dus, 50, false);
}
i2s_start_line_output();
i2s_switch_buffer();
}
/** Line i2s_output_row, but resets skip indicator. */
static void IRAM_ATTR i2s_write_row(RenderContext_t* ctx, uint32_t output_time_dus) {
i2s_output_row(output_time_dus);
ctx->skipping = 0;
}
/** Skip a row without writing to it. */
static void IRAM_ATTR i2s_skip_row(RenderContext_t* ctx, uint8_t pipeline_finish_time) {
int line_bytes = ctx->display_width / 4;
// output previously loaded row, fill buffer with no-ops.
if (ctx->skipping < 2) {
memset((void*)i2s_get_current_buffer(), 0x00, line_bytes);
i2s_output_row(pipeline_finish_time);
} else {
if (epd_get_display()->display_type == DISPLAY_TYPE_ED097TC2) {
pulse_ckv_ticks(5, 5, false);
} else {
// According to the spec, the OC4 maximum CKV frequency is 200kHz.
pulse_ckv_ticks(45, 5, false);
}
}
ctx->skipping++;
}
/**
* Start a draw cycle.
*/
static void i2s_start_frame() {
while (i2s_is_busy() || rmt_busy()) {
};
const EpdBoardDefinition* epd_board = epd_current_board();
epd_ctrl_state_t* ctrl_state = epd_ctrl_state();
epd_ctrl_state_t mask = NoChangeState;
ctrl_state->ep_mode = true;
mask.ep_mode = true;
epd_board->set_ctrl(ctrl_state, &mask);
pulse_ckv_us(1, 1, true);
// This is very timing-sensitive!
mask = NoChangeState;
ctrl_state->ep_stv = false;
mask.ep_stv = true;
epd_board->set_ctrl(ctrl_state, &mask);
// busy_delay(240);
pulse_ckv_us(1000, 100, false);
mask = NoChangeState;
ctrl_state->ep_stv = true;
mask.ep_stv = true;
epd_board->set_ctrl(ctrl_state, &mask);
// pulse_ckv_us(0, 10, true);
pulse_ckv_us(1, 1, true);
pulse_ckv_us(1, 1, true);
pulse_ckv_us(1, 1, true);
pulse_ckv_us(1, 1, true);
mask = NoChangeState;
ctrl_state->ep_output_enable = true;
mask.ep_output_enable = true;
epd_board->set_ctrl(ctrl_state, &mask);
}
/**
* End a draw cycle.
*/
static void i2s_end_frame() {
const EpdBoardDefinition* epd_board = epd_current_board();
epd_ctrl_state_t* ctrl_state = epd_ctrl_state();
epd_ctrl_state_t mask = NoChangeState;
ctrl_state->ep_stv = false;
mask.ep_stv = true;
epd_board->set_ctrl(ctrl_state, &mask);
pulse_ckv_us(1, 1, true);
pulse_ckv_us(1, 1, true);
pulse_ckv_us(1, 1, true);
pulse_ckv_us(1, 1, true);
pulse_ckv_us(1, 1, true);
mask = NoChangeState;
ctrl_state->ep_mode = false;
mask.ep_mode = true;
epd_board->set_ctrl(ctrl_state, &mask);
pulse_ckv_us(0, 10, true);
mask = NoChangeState;
ctrl_state->ep_output_enable = false;
mask.ep_output_enable = true;
epd_board->set_ctrl(ctrl_state, &mask);
pulse_ckv_us(1, 1, true);
pulse_ckv_us(1, 1, true);
pulse_ckv_us(1, 1, true);
}
void i2s_do_update(RenderContext_t* ctx) {
for (uint8_t k = 0; k < ctx->cycle_frames; k++) {
prepare_context_for_next_frame(ctx);
// start both feeder tasks
xTaskNotifyGive(ctx->feed_tasks[!xPortGetCoreID()]);
xTaskNotifyGive(ctx->feed_tasks[xPortGetCoreID()]);
// transmission is started in renderer threads, now wait util it's done
xSemaphoreTake(ctx->frame_done, portMAX_DELAY);
for (int i = 0; i < NUM_RENDER_THREADS; i++) {
xSemaphoreTake(ctx->feed_done_smphr[i], portMAX_DELAY);
}
ctx->current_frame++;
// make the watchdog happy.
if (k % 10 == 0) {
vTaskDelay(0);
}
}
}
void IRAM_ATTR epd_push_pixels_i2s(RenderContext_t* ctx, EpdRect area, short time, int color) {
int line_bytes = ctx->display_width / 4;
uint8_t row[line_bytes];
memset(row, 0, line_bytes);
const uint8_t color_choice[4] = { DARK_BYTE, CLEAR_BYTE, 0x00, 0xFF };
for (uint32_t i = 0; i < area.width; i++) {
uint32_t position = i + area.x % 4;
uint8_t mask = color_choice[color] & (0b00000011 << (2 * (position % 4)));
row[area.x / 4 + position / 4] |= mask;
}
reorder_line_buffer((uint32_t*)row, line_bytes);
i2s_start_frame();
for (int i = 0; i < ctx->display_height; i++) {
// before are of interest: skip
if (i < area.y) {
i2s_skip_row(ctx, time);
// start area of interest: set row data
} else if (i == area.y) {
i2s_switch_buffer();
memcpy((void*)i2s_get_current_buffer(), row, line_bytes);
i2s_switch_buffer();
memcpy((void*)i2s_get_current_buffer(), row, line_bytes);
i2s_write_row(ctx, time * 10);
// load nop row if done with area
} else if (i >= area.y + area.height) {
i2s_skip_row(ctx, time);
// output the same as before
} else {
i2s_write_row(ctx, time * 10);
}
}
// Since we "pipeline" row output, we still have to latch out the last row.
i2s_write_row(ctx, time * 10);
i2s_end_frame();
}
void IRAM_ATTR i2s_output_frame(RenderContext_t* ctx, int thread_id) {
uint8_t* line_buf = ctx->feed_line_buffers[thread_id];
ctx->skipping = 0;
EpdRect area = ctx->area;
int frame_time = ctx->frame_time;
i2s_start_frame();
for (int i = 0; i < ctx->display_height; i++) {
LineQueue_t* lq = &ctx->line_queues[0];
memset(line_buf, 0, ctx->display_width);
while (lq_read(lq, line_buf) < 0) {
};
ctx->lines_consumed += 1;
if (ctx->drawn_lines != NULL && !ctx->drawn_lines[i - area.y]) {
i2s_skip_row(ctx, frame_time);
continue;
}
// lookup pixel actions in the waveform LUT
ctx->lut_lookup_func(
(uint32_t*)line_buf,
(uint8_t*)i2s_get_current_buffer(),
ctx->conversion_lut,
ctx->display_width
);
// apply the line mask
epd_apply_line_mask(i2s_get_current_buffer(), ctx->line_mask, ctx->display_width / 4);
reorder_line_buffer((uint32_t*)i2s_get_current_buffer(), ctx->display_width / 4);
i2s_write_row(ctx, frame_time);
}
if (!ctx->skipping) {
// Since we "pipeline" row output, we still have to latch out the
// last row.
i2s_write_row(ctx, frame_time);
}
i2s_end_frame();
xSemaphoreGive(ctx->feed_done_smphr[thread_id]);
xSemaphoreGive(ctx->frame_done);
}
static inline int min(int x, int y) {
return x < y ? x : y;
}
static inline int max(int x, int y) {
return x > y ? x : y;
}
void IRAM_ATTR i2s_fetch_frame_data(RenderContext_t* ctx, int thread_id) {
uint8_t* input_line = ctx->feed_line_buffers[thread_id];
// line must be able to hold 2-pixel-per-byte or 1-pixel-per-byte data
memset(input_line, 0x00, ctx->display_width);
LineQueue_t* lq = &ctx->line_queues[thread_id];
EpdRect area = ctx->area;
int min_y, max_y, bytes_per_line, pixels_per_byte;
const uint8_t* ptr_start;
get_buffer_params(ctx, &bytes_per_line, &ptr_start, &min_y, &max_y, &pixels_per_byte);
const EpdRect crop_to = ctx->crop_to;
const bool horizontally_cropped = !(crop_to.x == 0 && crop_to.width == area.width);
int crop_x = (horizontally_cropped ? crop_to.x : 0);
int crop_w = (horizontally_cropped ? crop_to.width : 0);
// interval of the output line that is needed
// FIXME: only lookup needed parts
int line_start_x = area.x + (horizontally_cropped ? crop_to.x : 0);
int line_end_x = line_start_x + (horizontally_cropped ? crop_to.width : area.width);
line_start_x = min(max(line_start_x, 0), ctx->display_width);
line_end_x = min(max(line_end_x, 0), ctx->display_width);
int l = 0;
while (l = atomic_fetch_add(&ctx->lines_prepared, 1), l < ctx->display_height) {
// if (thread_id) gpio_set_level(15, 0);
ctx->line_threads[l] = thread_id;
if (l < min_y || l >= max_y
|| (ctx->drawn_lines != NULL && !ctx->drawn_lines[l - area.y])) {
uint8_t* buf = NULL;
while (buf == NULL)
buf = lq_current(lq);
memset(buf, 0x00, lq->element_size);
lq_commit(lq);
continue;
}
uint32_t* lp = (uint32_t*)input_line;
bool shifted = false;
const uint8_t* ptr = ptr_start + bytes_per_line * (l - min_y);
if (area.width == ctx->display_width && area.x == 0 && !ctx->error) {
lp = (uint32_t*)ptr;
} else if (!ctx->error) {
uint8_t* buf_start = (uint8_t*)input_line;
uint32_t line_bytes = bytes_per_line;
int min_x = area.x + crop_x;
if (min_x >= 0) {
buf_start += min_x / pixels_per_byte;
} else {
// reduce line_bytes to actually used bytes
// ptr was already adjusted above
line_bytes += min_x / pixels_per_byte;
}
line_bytes = min(
line_bytes,
ctx->display_width / pixels_per_byte - (uint32_t)(buf_start - input_line)
);
memcpy(buf_start, ptr, line_bytes);
int cropped_width = (horizontally_cropped ? crop_w : area.width);
/// consider half-byte shifts in two-pixel-per-Byte mode.
if (pixels_per_byte == 2) {
// mask last nibble for uneven width
if (cropped_width % 2 == 1
&& min_x / 2 + cropped_width / 2 + 1 < ctx->display_width) {
*(buf_start + line_bytes - 1) |= 0xF0;
}
if (area.x % 2 == 1 && !(crop_x % 2 == 1) && min_x < ctx->display_width) {
shifted = true;
uint32_t remaining
= (uint32_t)input_line + ctx->display_width / 2 - (uint32_t)buf_start;
uint32_t to_shift = min(line_bytes + 1, remaining);
// shift one nibble to right
nibble_shift_buffer_right(buf_start, to_shift);
}
// consider bit shifts in bit buffers
} else if (pixels_per_byte == 8) {
// mask last n bits if width is not divisible by 8
if (cropped_width % 8 != 0 && bytes_per_line + 1 < ctx->display_width) {
uint8_t mask = 0;
for (int s = 0; s < cropped_width % 8; s++) {
mask = (mask << 1) | 1;
}
*(buf_start + line_bytes - 1) |= ~mask;
}
if (min_x % 8 != 0 && min_x < ctx->display_width) {
// shift to right
shifted = true;
uint32_t remaining
= (uint32_t)input_line + ctx->display_width / 8 - (uint32_t)buf_start;
uint32_t to_shift = min(line_bytes + 1, remaining);
bit_shift_buffer_right(buf_start, to_shift, min_x % 8);
}
}
lp = (uint32_t*)input_line;
}
uint8_t* buf = NULL;
while (buf == NULL)
buf = lq_current(lq);
memcpy(buf, lp, lq->element_size);
lq_commit(lq);
if (shifted) {
memset(input_line, 255, ctx->display_width / pixels_per_byte);
}
}
}
void i2s_deinit() {
rmt_pulse_deinit();
i2s_bus_deinit();
}
#endif

View File

@ -0,0 +1,59 @@
#include "epdiy.h"
#include <driver/gpio.h>
#include <esp_assert.h>
#include <esp_system.h>
#include <hal/gpio_ll.h>
#include <soc/gpio_struct.h>
#include "../output_common/render_context.h"
#include "sdkconfig.h"
/**
* Lighten / darken picels using the I2S driving method.
*/
void epd_push_pixels_i2s(RenderContext_t* ctx, EpdRect area, short time, int color);
/**
* Do a full update cycle with a configured context.
*/
void i2s_do_update(RenderContext_t* ctx);
/**
* Worker to fetch framebuffer data and write into a queue for processing.
*/
void i2s_fetch_frame_data(RenderContext_t* ctx, int thread_id);
/**
* Worker to output frame data to the display.
*/
void i2s_output_frame(RenderContext_t* ctx, int thread_id);
/**
* Deinitialize the I2S peripheral for low power consumption.
*/
void i2s_deinit();
/*
* Write bits directly using the registers in the ESP32.
* Won't work for some pins (>= 32).
*/
inline void fast_gpio_set_hi(gpio_num_t gpio_num) {
#ifdef CONFIG_IDF_TARGET_ESP32
gpio_dev_t* device = GPIO_LL_GET_HW(GPIO_PORT_0);
device->out_w1ts = (1 << gpio_num);
#else
// not supportd on non ESP32 chips
assert(false);
#endif
}
inline void fast_gpio_set_lo(gpio_num_t gpio_num) {
#ifdef CONFIG_IDF_TARGET_ESP32
gpio_dev_t* device = GPIO_LL_GET_HW(GPIO_PORT_0);
device->out_w1tc = (1 << gpio_num);
#else
// not supportd on non ESP32 chips
assert(false);
#endif
}

View File

@ -0,0 +1,97 @@
#include "../output_common/render_method.h"
#include "esp_intr_alloc.h"
#ifdef RENDER_METHOD_I2S
#include "driver/rmt.h"
#include "rmt_pulse.h"
#include "soc/rmt_struct.h"
static intr_handle_t gRMT_intr_handle = NULL;
// the RMT channel configuration object
static rmt_config_t row_rmt_config;
// keep track of wether the current pulse is ongoing
volatile bool rmt_tx_done = true;
/**
* Remote peripheral interrupt. Used to signal when transmission is done.
*/
static void IRAM_ATTR rmt_interrupt_handler(void* arg) {
rmt_tx_done = true;
RMT.int_clr.val = RMT.int_st.val;
}
// The extern line is declared in esp-idf/components/driver/deprecated/rmt_legacy.c. It has access
// to RMTMEM through the rmt_private.h header which we can't access outside the sdk. Declare our own
// extern here to properly use the RMTMEM smybol defined in
// components/soc/[target]/ld/[target].peripherals.ld Also typedef the new rmt_mem_t struct to the
// old rmt_block_mem_t struct. Same data fields, different names
typedef rmt_mem_t rmt_block_mem_t;
extern rmt_block_mem_t RMTMEM;
void rmt_pulse_init(gpio_num_t pin) {
row_rmt_config.rmt_mode = RMT_MODE_TX;
// currently hardcoded: use channel 0
row_rmt_config.channel = RMT_CHANNEL_1;
row_rmt_config.gpio_num = pin;
row_rmt_config.mem_block_num = 2;
// Divide 80MHz APB Clock by 8 -> .1us resolution delay
row_rmt_config.clk_div = 8;
row_rmt_config.tx_config.loop_en = false;
row_rmt_config.tx_config.carrier_en = false;
row_rmt_config.tx_config.carrier_level = RMT_CARRIER_LEVEL_LOW;
row_rmt_config.tx_config.idle_level = RMT_IDLE_LEVEL_LOW;
row_rmt_config.tx_config.idle_output_en = true;
esp_intr_alloc(
ETS_RMT_INTR_SOURCE, ESP_INTR_FLAG_LEVEL3, rmt_interrupt_handler, 0, &gRMT_intr_handle
);
rmt_config(&row_rmt_config);
rmt_set_tx_intr_en(row_rmt_config.channel, true);
}
void rmt_pulse_deinit() {
esp_intr_disable(gRMT_intr_handle);
esp_intr_free(gRMT_intr_handle);
}
void IRAM_ATTR pulse_ckv_ticks(uint16_t high_time_ticks, uint16_t low_time_ticks, bool wait) {
while (!rmt_tx_done) {
};
volatile rmt_item32_t* rmt_mem_ptr = &(RMTMEM.chan[row_rmt_config.channel].data32[0]);
if (high_time_ticks > 0) {
rmt_mem_ptr->level0 = 1;
rmt_mem_ptr->duration0 = high_time_ticks;
rmt_mem_ptr->level1 = 0;
rmt_mem_ptr->duration1 = low_time_ticks;
} else {
rmt_mem_ptr->level0 = 1;
rmt_mem_ptr->duration0 = low_time_ticks;
rmt_mem_ptr->level1 = 0;
rmt_mem_ptr->duration1 = 0;
}
RMTMEM.chan[row_rmt_config.channel].data32[1].val = 0;
rmt_tx_done = false;
RMT.conf_ch[row_rmt_config.channel].conf1.mem_rd_rst = 1;
RMT.conf_ch[row_rmt_config.channel].conf1.mem_owner = RMT_MEM_OWNER_TX;
RMT.conf_ch[row_rmt_config.channel].conf1.tx_start = 1;
while (wait && !rmt_tx_done) {
};
}
void IRAM_ATTR pulse_ckv_us(uint16_t high_time_us, uint16_t low_time_us, bool wait) {
pulse_ckv_ticks(10 * high_time_us, 10 * low_time_us, wait);
}
bool IRAM_ATTR rmt_busy() {
return !rmt_tx_done;
}
#endif

View File

@ -3,9 +3,9 @@
*/
#pragma once
#include <stdint.h>
#include "driver/gpio.h"
#include "esp_attr.h"
#include <stdint.h>
/**
* Initializes RMT Channel 0 with a pin for RMT pulsing.
@ -13,6 +13,11 @@
*/
void rmt_pulse_init(gpio_num_t pin);
/**
* Resets the pin and RMT peripheral, frees associated resources.
*/
void rmt_pulse_deinit();
/**
* Outputs a single pulse (high -> low) on the configured pin.
* This function will always wait for a previous call to finish.
@ -21,12 +26,11 @@ void rmt_pulse_init(gpio_num_t pin);
* @param: low_time_us Pulse low time in us.
* @param: wait Block until the pulse is finished.
*/
void IRAM_ATTR pulse_ckv_us(uint16_t high_time_us, uint16_t low_time_us,
bool wait);
void pulse_ckv_us(uint16_t high_time_us, uint16_t low_time_us, bool wait);
/**
* Indicates if the rmt is currently sending a pulse.
*/
bool IRAM_ATTR rmt_busy();
bool rmt_busy();
/**
* Outputs a single pulse (high -> low) on the configured pin.
@ -36,5 +40,4 @@ bool IRAM_ATTR rmt_busy();
* @param: low_time_us Pulse low time in clock ticks.
* @param: wait Block until the pulse is finished.
*/
void IRAM_ATTR pulse_ckv_ticks(uint16_t high_time_us, uint16_t low_time_us,
bool wait);
void pulse_ckv_ticks(uint16_t high_time_us, uint16_t low_time_us, bool wait);

View File

@ -0,0 +1,22 @@
/**
* Backported functions to make the LCD-based driver compile with IDF < 5.0
*/
#define RMT_BASECLK_DEFAULT RMT_BASECLK_APB
typedef int rmt_clock_source_t;
static inline void rmt_ll_enable_periph_clock(rmt_dev_t* dev, bool enable) {
dev->sys_conf.clk_en = enable; // register clock gating
dev->sys_conf.mem_clk_force_on = enable; // memory clock gating
}
static inline void rmt_ll_enable_mem_access_nonfifo(rmt_dev_t* dev, bool enable) {
dev->sys_conf.apb_fifo_mask = enable;
}
__attribute__((always_inline)) static inline void rmt_ll_tx_fix_idle_level(
rmt_dev_t* dev, uint32_t channel, uint8_t level, bool enable
) {
dev->chnconf0[channel].idle_out_en_n = enable;
dev->chnconf0[channel].idle_out_lv_n = level;
}

View File

@ -0,0 +1,706 @@
#include "lcd_driver.h"
#include "epdiy.h"
#include "../output_common/render_method.h"
#include "esp_heap_caps.h"
#include "esp_intr_alloc.h"
#include "hal/gpio_types.h"
#ifdef RENDER_METHOD_LCD
#include <assert.h>
#include <esp_idf_version.h>
#include <esp_log.h>
#include <soc/lcd_periph.h>
#include <soc/rmt_periph.h>
#include <stddef.h>
#include <stdint.h>
#include <stdio.h>
#include <string.h>
#include <driver/rmt_tx.h>
#include <driver/rmt_types.h>
#include <driver/rmt_types_legacy.h>
#include <esp_private/periph_ctrl.h>
#include <hal/rmt_types.h>
#include <soc/clk_tree_defs.h>
#include <driver/gpio.h>
#include <esp_check.h>
#include <esp_err.h>
#include <esp_lcd_panel_ops.h>
#include <esp_lcd_panel_rgb.h>
#include <esp_private/gdma.h>
#include <esp_timer.h>
#include <freertos/FreeRTOS.h>
#include <freertos/semphr.h>
#include <freertos/task.h>
#include <hal/dma_types.h>
#include <hal/gdma_ll.h>
#include <hal/gpio_hal.h>
#include <hal/lcd_hal.h>
#include <hal/lcd_ll.h>
#include <hal/rmt_ll.h>
#include <rom/cache.h>
#include <soc/lcd_periph.h>
#include <soc/rmt_struct.h>
#include "hal/gpio_hal.h"
gpio_hal_context_t hal = { .dev = GPIO_HAL_GET_HW(GPIO_PORT_0) };
#define TAG "epdiy"
#if ESP_IDF_VERSION < ESP_IDF_VERSION_VAL(5, 3, 2)
#define LCD_PERIPH_SIGNALS lcd_periph_signals
#else
#define LCD_PERIPH_SIGNALS lcd_periph_rgb_signals
#endif
static inline int min(int x, int y) {
return x < y ? x : y;
}
static inline int max(int x, int y) {
return x > y ? x : y;
}
#define S3_LCD_PIN_NUM_BK_LIGHT -1
// #define S3_LCD_PIN_NUM_MODE 4
#define LINE_BATCH 1000
#define BOUNCE_BUF_LINES 4
#define RMT_CKV_CHAN RMT_CHANNEL_1
// The extern line is declared in esp-idf/components/driver/deprecated/rmt_legacy.c. It has access
// to RMTMEM through the rmt_private.h header which we can't access outside the sdk. Declare our own
// extern here to properly use the RMTMEM smybol defined in
// components/soc/[target]/ld/[target].peripherals.ld Also typedef the new rmt_mem_t struct to the
// old rmt_block_mem_t struct. Same data fields, different names
typedef rmt_mem_t rmt_block_mem_t;
extern rmt_block_mem_t RMTMEM;
// spinlock for protecting the critical section at frame start
static portMUX_TYPE frame_start_spinlock = portMUX_INITIALIZER_UNLOCKED;
typedef struct {
lcd_hal_context_t hal;
intr_handle_t vsync_intr;
intr_handle_t done_intr;
frame_done_func_t frame_done_cb;
line_cb_func_t line_source_cb;
void* line_cb_payload;
void* frame_cb_payload;
int line_length_us;
int line_cycles;
int lcd_res_h;
LcdEpdConfig_t config;
uint8_t* bounce_buffer[2];
// size of a single bounce buffer
size_t bb_size;
size_t batches;
// Number of DMA descriptors that used to carry the frame buffer
size_t num_dma_nodes;
// DMA channel handle
gdma_channel_handle_t dma_chan;
// DMA descriptors pool
dma_descriptor_t* dma_nodes;
/// The number of bytes in a horizontal display register line.
int line_bytes;
// With 8 bit bus width, we need a dummy cycle before the actual data,
// because the LCD peripheral behaves weirdly.
// Also see:
// https://blog.adafruit.com/2022/06/14/esp32uesday-hacking-the-esp32-s3-lcd-peripheral/
int dummy_bytes;
/// The number of lines of the display
int display_lines;
} s3_lcd_t;
static s3_lcd_t lcd = { 0 };
void IRAM_ATTR epd_lcd_line_source_cb(line_cb_func_t line_source, void* payload) {
lcd.line_source_cb = line_source;
lcd.line_cb_payload = payload;
}
void IRAM_ATTR epd_lcd_frame_done_cb(frame_done_func_t cb, void* payload) {
lcd.frame_done_cb = cb;
lcd.frame_cb_payload = payload;
}
static IRAM_ATTR bool fill_bounce_buffer(uint8_t* buffer) {
bool task_awoken = false;
for (int i = 0; i < BOUNCE_BUF_LINES; i++) {
if (lcd.line_source_cb != NULL) {
// this is strange, with 16 bit need a dummy cycle. But still, the first byte in the
// FIFO is correct. So we only need a true dummy byte in the FIFO in the 8 bit
// configuration.
int buffer_offset = i * (lcd.line_bytes + lcd.dummy_bytes) + (lcd.dummy_bytes % 2);
task_awoken |= lcd.line_source_cb(lcd.line_cb_payload, &buffer[buffer_offset]);
} else {
memset(&buffer[i * lcd.line_bytes], 0x00, lcd.line_bytes);
}
}
return task_awoken;
}
static void start_ckv_cycles(int cycles) {
rmt_ll_tx_enable_loop_count(&RMT, RMT_CKV_CHAN, true);
rmt_ll_tx_enable_loop_autostop(&RMT, RMT_CKV_CHAN, true);
rmt_ll_tx_set_loop_count(&RMT, RMT_CKV_CHAN, cycles);
rmt_ll_tx_reset_pointer(&RMT, RMT_CKV_CHAN);
rmt_ll_tx_start(&RMT, RMT_CKV_CHAN);
}
/**
* Build the RMT signal according to the timing set in the lcd object.
*/
static void ckv_rmt_build_signal() {
int low_time = (lcd.line_length_us * 10 - lcd.config.ckv_high_time);
volatile rmt_item32_t* rmt_mem_ptr = &(RMTMEM.chan[RMT_CKV_CHAN].data32[0]);
rmt_mem_ptr->duration0 = lcd.config.ckv_high_time;
rmt_mem_ptr->level0 = 1;
rmt_mem_ptr->duration1 = low_time;
rmt_mem_ptr->level1 = 0;
rmt_mem_ptr[1].val = 0;
}
/**
* Configure the RMT peripheral for use as the CKV clock.
*/
static void init_ckv_rmt() {
periph_module_reset(PERIPH_RMT_MODULE);
periph_module_enable(PERIPH_RMT_MODULE);
rmt_ll_enable_periph_clock(&RMT, true);
// Divide 80MHz APB Clock by 8 -> .1us resolution delay
// idf >= 5.0 calculates the clock divider differently
rmt_ll_set_group_clock_src(&RMT, RMT_CKV_CHAN, RMT_CLK_SRC_DEFAULT, 1, 0, 0);
rmt_ll_tx_set_channel_clock_div(&RMT, RMT_CKV_CHAN, 8);
rmt_ll_tx_set_mem_blocks(&RMT, RMT_CKV_CHAN, 2);
rmt_ll_enable_mem_access_nonfifo(&RMT, true);
rmt_ll_tx_fix_idle_level(&RMT, RMT_CKV_CHAN, RMT_IDLE_LEVEL_LOW, true);
rmt_ll_tx_enable_carrier_modulation(&RMT, RMT_CKV_CHAN, false);
rmt_ll_tx_enable_loop(&RMT, RMT_CKV_CHAN, true);
gpio_hal_func_sel(&hal, lcd.config.bus.ckv, PIN_FUNC_GPIO);
gpio_set_direction(lcd.config.bus.ckv, GPIO_MODE_OUTPUT);
esp_rom_gpio_connect_out_signal(
lcd.config.bus.ckv, rmt_periph_signals.groups[0].channels[RMT_CKV_CHAN].tx_sig, false, 0
);
ckv_rmt_build_signal();
}
/**
* Reset the CKV RMT configuration.
*/
static void deinit_ckv_rmt() {
periph_module_reset(PERIPH_RMT_MODULE);
periph_module_disable(PERIPH_RMT_MODULE);
gpio_reset_pin(lcd.config.bus.ckv);
}
__attribute__((optimize("O3"))) IRAM_ATTR static void lcd_isr_vsync(void* args) {
bool need_yield = false;
uint32_t intr_status = lcd_ll_get_interrupt_status(lcd.hal.dev);
lcd_ll_clear_interrupt_status(lcd.hal.dev, intr_status);
if (intr_status & LCD_LL_EVENT_VSYNC_END) {
int batches_needed = lcd.display_lines / LINE_BATCH;
if (lcd.batches >= batches_needed) {
lcd_ll_stop(lcd.hal.dev);
if (lcd.frame_done_cb != NULL) {
(*lcd.frame_done_cb)(lcd.frame_cb_payload);
}
} else {
int ckv_cycles = 0;
// last batch
if (lcd.batches == batches_needed - 1) {
lcd_ll_enable_auto_next_frame(lcd.hal.dev, false);
lcd_ll_set_vertical_timing(lcd.hal.dev, 1, 0, lcd.display_lines % LINE_BATCH, 10);
ckv_cycles = lcd.display_lines % LINE_BATCH + 10;
} else {
lcd_ll_set_vertical_timing(lcd.hal.dev, 1, 0, LINE_BATCH, 1);
ckv_cycles = LINE_BATCH + 1;
}
// apparently, this is needed for the new timing to take effect.
lcd_ll_start(lcd.hal.dev);
// skip the LCD front porch line, which is not actual data
esp_rom_delay_us(lcd.line_length_us);
start_ckv_cycles(ckv_cycles);
}
lcd.batches += 1;
}
if (need_yield) {
portYIELD_FROM_ISR();
}
};
// ISR handling bounce buffer refill
static IRAM_ATTR bool lcd_rgb_panel_eof_handler(
gdma_channel_handle_t dma_chan, gdma_event_data_t* event_data, void* user_data
) {
dma_descriptor_t* desc = (dma_descriptor_t*)event_data->tx_eof_desc_addr;
// Figure out which bounce buffer to write to.
// Note: what we receive is the *last* descriptor of this bounce buffer.
int bb = (desc == &lcd.dma_nodes[0]) ? 0 : 1;
return fill_bounce_buffer(lcd.bounce_buffer[bb]);
}
static esp_err_t init_dma_trans_link() {
lcd.dma_nodes[0].dw0.suc_eof = 1;
lcd.dma_nodes[0].dw0.size = lcd.bb_size;
lcd.dma_nodes[0].dw0.length = lcd.bb_size;
lcd.dma_nodes[0].dw0.owner = DMA_DESCRIPTOR_BUFFER_OWNER_CPU;
lcd.dma_nodes[0].buffer = lcd.bounce_buffer[0];
lcd.dma_nodes[1].dw0.suc_eof = 1;
lcd.dma_nodes[1].dw0.size = lcd.bb_size;
lcd.dma_nodes[1].dw0.length = lcd.bb_size;
lcd.dma_nodes[1].dw0.owner = DMA_DESCRIPTOR_BUFFER_OWNER_CPU;
lcd.dma_nodes[1].buffer = lcd.bounce_buffer[1];
// loop end back to start
lcd.dma_nodes[0].next = &lcd.dma_nodes[1];
lcd.dma_nodes[1].next = &lcd.dma_nodes[0];
// alloc DMA channel and connect to LCD peripheral
gdma_channel_alloc_config_t dma_chan_config = {
.direction = GDMA_CHANNEL_DIRECTION_TX,
};
ESP_RETURN_ON_ERROR(
gdma_new_channel(&dma_chan_config, &lcd.dma_chan), TAG, "alloc DMA channel failed"
);
gdma_trigger_t trigger = GDMA_MAKE_TRIGGER(GDMA_TRIG_PERIPH_LCD, 0);
ESP_RETURN_ON_ERROR(gdma_connect(lcd.dma_chan, trigger), TAG, "dma connect error");
gdma_transfer_ability_t ability = {
.psram_trans_align = 64,
.sram_trans_align = 4,
};
ESP_RETURN_ON_ERROR(gdma_set_transfer_ability(lcd.dma_chan, &ability), TAG, "dma setup error");
gdma_tx_event_callbacks_t cbs = {
.on_trans_eof = lcd_rgb_panel_eof_handler,
};
ESP_RETURN_ON_ERROR(
gdma_register_tx_event_callbacks(lcd.dma_chan, &cbs, NULL), TAG, "dma setup error"
);
return ESP_OK;
}
void deinit_dma_trans_link() {
gdma_reset(lcd.dma_chan);
gdma_disconnect(lcd.dma_chan);
gdma_del_channel(lcd.dma_chan);
}
/**
* Configure LCD peripheral and auxiliary GPIOs
*/
static esp_err_t init_bus_gpio() {
const int DATA_LINES[16] = {
lcd.config.bus.data[14], lcd.config.bus.data[15], lcd.config.bus.data[12],
lcd.config.bus.data[13], lcd.config.bus.data[10], lcd.config.bus.data[11],
lcd.config.bus.data[8], lcd.config.bus.data[9], lcd.config.bus.data[6],
lcd.config.bus.data[7], lcd.config.bus.data[4], lcd.config.bus.data[5],
lcd.config.bus.data[2], lcd.config.bus.data[3], lcd.config.bus.data[0],
lcd.config.bus.data[1],
};
// connect peripheral signals via GPIO matrix
for (size_t i = (16 - lcd.config.bus_width); i < 16; i++) {
gpio_hal_func_sel(&hal, DATA_LINES[i], PIN_FUNC_GPIO);
gpio_set_direction(DATA_LINES[i], GPIO_MODE_OUTPUT);
esp_rom_gpio_connect_out_signal(
DATA_LINES[i], LCD_PERIPH_SIGNALS.panels[0].data_sigs[i], false, false
);
}
gpio_hal_func_sel(&hal, lcd.config.bus.leh, PIN_FUNC_GPIO);
gpio_set_direction(lcd.config.bus.leh, GPIO_MODE_OUTPUT);
gpio_hal_func_sel(&hal, lcd.config.bus.clock, PIN_FUNC_GPIO);
gpio_set_direction(lcd.config.bus.clock, GPIO_MODE_OUTPUT);
gpio_hal_func_sel(&hal, lcd.config.bus.start_pulse, PIN_FUNC_GPIO);
gpio_set_direction(lcd.config.bus.start_pulse, GPIO_MODE_OUTPUT);
esp_rom_gpio_connect_out_signal(
lcd.config.bus.leh, LCD_PERIPH_SIGNALS.panels[0].hsync_sig, false, false
);
esp_rom_gpio_connect_out_signal(
lcd.config.bus.clock, LCD_PERIPH_SIGNALS.panels[0].pclk_sig, false, false
);
esp_rom_gpio_connect_out_signal(
lcd.config.bus.start_pulse, LCD_PERIPH_SIGNALS.panels[0].de_sig, false, false
);
gpio_config_t vsync_gpio_conf = {
.mode = GPIO_MODE_OUTPUT,
.pin_bit_mask = 1ull << lcd.config.bus.stv,
};
gpio_config(&vsync_gpio_conf);
gpio_set_level(lcd.config.bus.stv, 1);
return ESP_OK;
}
/**
* Reset bus GPIO pin functions.
*/
static void deinit_bus_gpio() {
for (size_t i = (16 - lcd.config.bus_width); i < 16; i++) {
gpio_reset_pin(lcd.config.bus.data[i]);
}
gpio_reset_pin(lcd.config.bus.leh);
gpio_reset_pin(lcd.config.bus.clock);
gpio_reset_pin(lcd.config.bus.start_pulse);
gpio_reset_pin(lcd.config.bus.stv);
}
/**
* Check if the PSRAM cache is properly configured.
*/
static void check_cache_configuration() {
if (CONFIG_ESP32S3_DATA_CACHE_LINE_SIZE < 64) {
ESP_LOGE(
"epdiy",
"cache line size is set to %d (< 64B)! This will degrade performance, please update "
"this option in menuconfig.",
CONFIG_ESP32S3_DATA_CACHE_LINE_SIZE
);
ESP_LOGE(
"epdiy",
"If you are on arduino, you can't set this option yourself, you'll need to use a lower "
"speed."
);
ESP_LOGE(
"epdiy",
"Reducing the pixel clock from %d MHz to %d MHz for now!",
lcd.config.pixel_clock / 1000 / 1000,
lcd.config.pixel_clock / 1000 / 1000 / 2
);
lcd.config.pixel_clock = lcd.config.pixel_clock / 2;
// fixme: this would be nice, but doesn't work :(
// uint32_t d_autoload = Cache_Suspend_DCache();
/// Cache_Set_DCache_Mode(CACHE_SIZE_FULL, CACHE_4WAYS_ASSOC, CACHE_LINE_SIZE_32B);
// Cache_Invalidate_DCache_All();
// Cache_Resume_DCache(d_autoload);
}
}
/**
* Assign LCD configuration parameters from a given configuration, without allocating memory or
* touching the LCD peripheral config.
*/
static void assign_lcd_parameters_from_config(
const LcdEpdConfig_t* config, int display_width, int display_height
) {
// copy over the configuraiton object
memcpy(&lcd.config, config, sizeof(LcdEpdConfig_t));
// Make sure the bounce buffers divide the display height evenly.
lcd.display_lines = (((display_height + 7) / 8) * 8);
lcd.line_bytes = display_width / 4;
lcd.lcd_res_h = lcd.line_bytes / (lcd.config.bus_width / 8);
// With 8 bit bus width, we need a dummy cycle before the actual data,
// because the LCD peripheral behaves weirdly.
// Also see:
// https://blog.adafruit.com/2022/06/14/esp32uesday-hacking-the-esp32-s3-lcd-peripheral/
lcd.dummy_bytes = lcd.config.bus_width / 8;
// each bounce buffer holds a number of lines with data + dummy bytes each
lcd.bb_size = BOUNCE_BUF_LINES * (lcd.line_bytes + lcd.dummy_bytes);
check_cache_configuration();
ESP_LOGI(TAG, "using resolution %dx%d", lcd.lcd_res_h, lcd.display_lines);
}
/**
* Allocate buffers for LCD driver operation.
*/
static esp_err_t allocate_lcd_buffers() {
uint32_t dma_flags = MALLOC_CAP_INTERNAL | MALLOC_CAP_DMA;
// allocate bounce buffers
for (int i = 0; i < 2; i++) {
lcd.bounce_buffer[i] = heap_caps_aligned_calloc(4, 1, lcd.bb_size, dma_flags);
ESP_RETURN_ON_FALSE(lcd.bounce_buffer[i], ESP_ERR_NO_MEM, TAG, "install interrupt failed");
}
// So far, I haven't seen any displays with > 4096 pixels per line,
// so we only need one DMA node for now.
assert(lcd.bb_size < DMA_DESCRIPTOR_BUFFER_MAX_SIZE);
lcd.dma_nodes = heap_caps_calloc(1, sizeof(dma_descriptor_t) * 2, dma_flags);
ESP_RETURN_ON_FALSE(lcd.dma_nodes, ESP_ERR_NO_MEM, TAG, "no mem for dma nodes");
return ESP_OK;
}
static void free_lcd_buffers() {
for (int i = 0; i < 2; i++) {
uint8_t* buf = lcd.bounce_buffer[i];
if (buf != NULL) {
heap_caps_free(buf);
lcd.bounce_buffer[i] = NULL;
}
}
if (lcd.dma_nodes != NULL) {
heap_caps_free(lcd.dma_nodes);
lcd.dma_nodes = NULL;
}
}
/**
* Initialize the LCD peripheral itself and install interrupts.
*/
static esp_err_t init_lcd_peripheral() {
esp_err_t ret = ESP_OK;
// enable APB to access LCD registers
periph_module_enable(PERIPH_LCD_CAM_MODULE);
periph_module_reset(PERIPH_LCD_CAM_MODULE);
lcd_hal_init(&lcd.hal, 0);
lcd_ll_enable_clock(lcd.hal.dev, true);
lcd_ll_select_clk_src(lcd.hal.dev, LCD_CLK_SRC_PLL240M);
ESP_RETURN_ON_ERROR(ret, TAG, "set source clock failed");
// install interrupt service, (LCD peripheral shares the interrupt source with Camera by
// different mask)
int flags = ESP_INTR_FLAG_IRAM | ESP_INTR_FLAG_INTRDISABLED | ESP_INTR_FLAG_SHARED
| ESP_INTR_FLAG_LOWMED;
int source = LCD_PERIPH_SIGNALS.panels[0].irq_id;
uint32_t status = (uint32_t)lcd_ll_get_interrupt_status_reg(lcd.hal.dev);
ret = esp_intr_alloc_intrstatus(
source, flags, status, LCD_LL_EVENT_VSYNC_END, lcd_isr_vsync, NULL, &lcd.vsync_intr
);
ESP_RETURN_ON_ERROR(ret, TAG, "install interrupt failed");
status = (uint32_t)lcd_ll_get_interrupt_status_reg(lcd.hal.dev);
ret = esp_intr_alloc_intrstatus(
source, flags, status, LCD_LL_EVENT_TRANS_DONE, lcd_isr_vsync, NULL, &lcd.done_intr
);
ESP_RETURN_ON_ERROR(ret, TAG, "install interrupt failed");
lcd_ll_fifo_reset(lcd.hal.dev);
lcd_ll_reset(lcd.hal.dev);
// pixel clock phase and polarity
lcd_ll_set_clock_idle_level(lcd.hal.dev, false);
lcd_ll_set_pixel_clock_edge(lcd.hal.dev, false);
// enable RGB mode and set data width
lcd_ll_enable_rgb_mode(lcd.hal.dev, true);
#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(5, 3, 0)
lcd_ll_set_dma_read_stride(lcd.hal.dev, lcd.config.bus_width);
lcd_ll_set_data_wire_width(lcd.hal.dev, lcd.config.bus_width);
#else
lcd_ll_set_data_width(lcd.hal.dev, lcd.config.bus_width);
#endif
lcd_ll_set_phase_cycles(lcd.hal.dev, 0, (lcd.dummy_bytes > 0), 1); // enable data phase only
lcd_ll_enable_output_hsync_in_porch_region(lcd.hal.dev, false); // enable data phase only
// number of data cycles is controlled by DMA buffer size
lcd_ll_enable_output_always_on(lcd.hal.dev, false);
lcd_ll_set_idle_level(lcd.hal.dev, false, true, true);
// configure blank region timing
// RGB panel always has a front and back blank (porch region)
lcd_ll_set_blank_cycles(lcd.hal.dev, 1, 1);
// output hsync even in porch region?
lcd_ll_enable_output_hsync_in_porch_region(lcd.hal.dev, false);
// send next frame automatically in stream mode
lcd_ll_enable_auto_next_frame(lcd.hal.dev, false);
lcd_ll_enable_interrupt(lcd.hal.dev, LCD_LL_EVENT_VSYNC_END, true);
lcd_ll_enable_interrupt(lcd.hal.dev, LCD_LL_EVENT_TRANS_DONE, true);
// enable intr
esp_intr_enable(lcd.vsync_intr);
esp_intr_enable(lcd.done_intr);
return ret;
}
static void deinit_lcd_peripheral() {
// disable and free interrupts
esp_intr_disable(lcd.vsync_intr);
esp_intr_disable(lcd.done_intr);
esp_intr_free(lcd.vsync_intr);
esp_intr_free(lcd.done_intr);
lcd_ll_fifo_reset(lcd.hal.dev);
lcd_ll_reset(lcd.hal.dev);
periph_module_reset(PERIPH_LCD_CAM_MODULE);
periph_module_disable(PERIPH_LCD_CAM_MODULE);
}
/**
* Configure the LCD driver for epdiy.
*/
void epd_lcd_init(const LcdEpdConfig_t* config, int display_width, int display_height) {
esp_err_t ret = ESP_OK;
assign_lcd_parameters_from_config(config, display_width, display_height);
check_cache_configuration();
ret = allocate_lcd_buffers();
ESP_GOTO_ON_ERROR(ret, err, TAG, "lcd buffer allocation failed");
ret = init_lcd_peripheral();
ESP_GOTO_ON_ERROR(ret, err, TAG, "lcd peripheral init failed");
ret = init_dma_trans_link();
ESP_GOTO_ON_ERROR(ret, err, TAG, "install DMA failed");
ret = init_bus_gpio();
ESP_GOTO_ON_ERROR(ret, err, TAG, "configure GPIO failed");
init_ckv_rmt();
// setup driver state
epd_lcd_set_pixel_clock_MHz(lcd.config.pixel_clock / 1000 / 1000);
epd_lcd_line_source_cb(NULL, NULL);
ESP_LOGI(TAG, "LCD init done.");
return;
err:
ESP_LOGE(TAG, "LCD initialization failed!");
abort();
}
/**
* Deinitializue the LCD driver, i.e., free resources and peripherals.
*/
void epd_lcd_deinit() {
epd_lcd_line_source_cb(NULL, NULL);
deinit_bus_gpio();
deinit_lcd_peripheral();
deinit_dma_trans_link();
free_lcd_buffers();
deinit_ckv_rmt();
ESP_LOGI(TAG, "LCD deinitialized.");
}
void epd_lcd_set_pixel_clock_MHz(int frequency) {
lcd.config.pixel_clock = frequency * 1000 * 1000;
// set pclk
int flags = 0;
#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(5, 3, 0)
hal_utils_clk_div_t clk_div = {};
/**
* There was a change in the parameters of this function in this commit:
* https://github.com/espressif/esp-idf/commit/d39388fe4f4c5bfb0b52df9177307b1688f41016#diff-2df607d77e3f6e350bab8eb31cfd914500ae42744564e1640cec47006cc17a9c
* There are different builds with the same IDF minor version, some with, some without the commit.
* So we try to select the correct one by checking if the flag value is defined.
*/
#ifdef LCD_HAL_PCLK_FLAG_ALLOW_EQUAL_SYSCLK
uint32_t freq
= lcd_hal_cal_pclk_freq(&lcd.hal, 240000000, lcd.config.pixel_clock, flags, &clk_div);
#else
uint32_t freq = lcd_hal_cal_pclk_freq(&lcd.hal, 240000000, lcd.config.pixel_clock, &clk_div);
#endif
lcd_ll_set_group_clock_coeff(
&LCD_CAM, (int)clk_div.integer, (int)clk_div.denominator, (int)clk_div.numerator
);
#else
uint32_t freq = lcd_hal_cal_pclk_freq(&lcd.hal, 240000000, lcd.config.pixel_clock, flags);
#endif
ESP_LOGI(TAG, "pclk freq: %d Hz", freq);
lcd.line_length_us = (lcd.lcd_res_h + lcd.config.le_high_time + lcd.config.line_front_porch - 1)
* 1000000 / lcd.config.pixel_clock
+ 1;
lcd.line_cycles = lcd.line_length_us * lcd.config.pixel_clock / 1000000;
ESP_LOGI(TAG, "line width: %dus, %d cylces", lcd.line_length_us, lcd.line_cycles);
ckv_rmt_build_signal();
}
void IRAM_ATTR epd_lcd_start_frame() {
int initial_lines = min(LINE_BATCH, lcd.display_lines);
// hsync: pulse with, back porch, active width, front porch
int end_line
= lcd.line_cycles - lcd.lcd_res_h - lcd.config.le_high_time - lcd.config.line_front_porch;
lcd_ll_set_horizontal_timing(
lcd.hal.dev,
lcd.config.le_high_time - (lcd.dummy_bytes > 0),
lcd.config.line_front_porch,
// a dummy byte is neeed in 8 bit mode to work around LCD peculiarities
lcd.lcd_res_h + (lcd.dummy_bytes > 0),
end_line
);
lcd_ll_set_vertical_timing(lcd.hal.dev, 1, 0, initial_lines, 1);
// generate the hsync at the very beginning of line
lcd_ll_set_hsync_position(lcd.hal.dev, 1);
// reset FIFO of DMA and LCD, incase there remains old frame data
gdma_reset(lcd.dma_chan);
lcd_ll_stop(lcd.hal.dev);
lcd_ll_fifo_reset(lcd.hal.dev);
lcd_ll_enable_auto_next_frame(lcd.hal.dev, true);
lcd.batches = 0;
fill_bounce_buffer(lcd.bounce_buffer[0]);
fill_bounce_buffer(lcd.bounce_buffer[1]);
// the start of DMA should be prior to the start of LCD engine
gdma_start(lcd.dma_chan, (intptr_t)&lcd.dma_nodes[0]);
// enter a critical section to ensure the frame start timing is correct
taskENTER_CRITICAL(&frame_start_spinlock);
// delay 1us is sufficient for DMA to pass data to LCD FIFO
// in fact, this is only needed when LCD pixel clock is set too high
gpio_set_level(lcd.config.bus.stv, 0);
// esp_rom_delay_us(1);
// for picture clarity, it seems to be important to start CKV at a "good"
// time, seemingly start or towards end of line.
start_ckv_cycles(initial_lines + 5);
esp_rom_delay_us(lcd.line_length_us);
gpio_set_level(lcd.config.bus.stv, 1);
esp_rom_delay_us(lcd.line_length_us);
esp_rom_delay_us(lcd.config.ckv_high_time / 10);
// start LCD engine
lcd_ll_start(lcd.hal.dev);
taskEXIT_CRITICAL(&frame_start_spinlock);
}
#else
/// Dummy implementation to link on the old ESP32
void epd_lcd_init(const LcdEpdConfig_t* config, int display_width, int display_height) {
assert(false);
}
#endif // S3 Target

View File

@ -0,0 +1,50 @@
#pragma once
#include <driver/gpio.h>
#include <stdbool.h>
#include <stddef.h>
#include <stdint.h>
/**
* LCD bus configuration parameters.
*/
typedef struct {
// GPIO numbers of the parallel bus pins.
gpio_num_t data[16];
// horizontal clock pin.
gpio_num_t clock;
// vertical clock pin
gpio_num_t ckv;
// horizontal "Start Pulse", enabling data input on the line shift register
gpio_num_t start_pulse;
// latch enable
gpio_num_t leh;
// vertical start pulse, resetting the vertical line shift register.
gpio_num_t stv;
} lcd_bus_config_t;
/// Configuration structure for the LCD-based Epd driver.
typedef struct {
// high time for CKV in 1/10us.
size_t pixel_clock; // = 12000000
int ckv_high_time; // = 70
int line_front_porch; // = 4
int le_high_time; // = 4
int bus_width; // = 16
lcd_bus_config_t bus;
} LcdEpdConfig_t;
typedef bool (*line_cb_func_t)(void*, uint8_t*);
typedef void (*frame_done_func_t)(void*);
void epd_lcd_init(const LcdEpdConfig_t* config, int display_width, int display_height);
void epd_lcd_deinit();
void epd_lcd_frame_done_cb(frame_done_func_t, void* payload);
void epd_lcd_line_source_cb(line_cb_func_t, void* payload);
void epd_lcd_start_frame();
/**
* Set the LCD pixel clock frequency in MHz.
*/
void epd_lcd_set_pixel_clock_MHz(int frequency);

View File

@ -0,0 +1,246 @@
#include <stdint.h>
#include <string.h>
#include "../output_common/render_method.h"
#ifdef RENDER_METHOD_LCD
#include <esp_log.h>
#include <rom/cache.h>
#include "../epd_internals.h"
#include "../output_common/line_queue.h"
#include "../output_common/lut.h"
#include "../output_common/render_context.h"
#include "epd_board.h"
#include "epdiy.h"
#include "lcd_driver.h"
#include "render_lcd.h"
// declare vector optimized line mask application.
void epd_apply_line_mask_VE(uint8_t* line, const uint8_t* mask, int mask_len);
__attribute__((optimize("O3"))) static bool IRAM_ATTR
retrieve_line_isr(RenderContext_t* ctx, uint8_t* buf) {
if (ctx->lines_consumed >= ctx->lines_total) {
return false;
}
int thread = ctx->line_threads[ctx->lines_consumed];
assert(thread < NUM_RENDER_THREADS);
LineQueue_t* lq = &ctx->line_queues[thread];
BaseType_t awoken = pdFALSE;
if (lq_read(lq, buf) != 0) {
ctx->error |= EPD_DRAW_EMPTY_LINE_QUEUE;
memset(buf, 0x00, ctx->display_width / 4);
}
if (ctx->lines_consumed >= ctx->display_height) {
memset(buf, 0x00, ctx->display_width / 4);
}
ctx->lines_consumed += 1;
return awoken;
}
/// start the next frame in the current update cycle
static void IRAM_ATTR handle_lcd_frame_done(RenderContext_t* ctx) {
epd_lcd_frame_done_cb(NULL, NULL);
epd_lcd_line_source_cb(NULL, NULL);
BaseType_t task_awoken = pdFALSE;
xSemaphoreGiveFromISR(ctx->frame_done, &task_awoken);
portYIELD_FROM_ISR();
}
void lcd_do_update(RenderContext_t* ctx) {
epd_set_mode(1);
for (uint8_t k = 0; k < ctx->cycle_frames; k++) {
epd_lcd_frame_done_cb((frame_done_func_t)handle_lcd_frame_done, ctx);
prepare_context_for_next_frame(ctx);
// start both feeder tasks
xTaskNotifyGive(ctx->feed_tasks[!xPortGetCoreID()]);
xTaskNotifyGive(ctx->feed_tasks[xPortGetCoreID()]);
// transmission is started in renderer threads, now wait util it's done
xSemaphoreTake(ctx->frame_done, portMAX_DELAY);
for (int i = 0; i < NUM_RENDER_THREADS; i++) {
xSemaphoreTake(ctx->feed_done_smphr[i], portMAX_DELAY);
}
ctx->current_frame++;
// make the watchdog happy.
vTaskDelay(0);
}
epd_lcd_line_source_cb(NULL, NULL);
epd_lcd_frame_done_cb(NULL, NULL);
epd_set_mode(0);
}
__attribute__((optimize("O3"))) static bool IRAM_ATTR
push_pixels_isr(RenderContext_t* ctx, uint8_t* buf) {
// Output no-op outside of drawn area
if (ctx->lines_consumed < ctx->area.y) {
memset(buf, 0, ctx->display_width / 4);
} else if (ctx->lines_consumed >= ctx->area.y + ctx->area.height) {
memset(buf, 0, ctx->display_width / 4);
} else {
memcpy(buf, ctx->static_line_buffer, ctx->display_width / 4);
}
ctx->lines_consumed += 1;
return pdFALSE;
}
/**
* Populate the line mask for use in epd_push_pixels.
*/
static void push_pixels_populate_line(RenderContext_t* ctx, int color) {
// Select fill pattern by draw color
int fill_byte = 0;
switch (color) {
case 0:
fill_byte = DARK_BYTE;
break;
case 1:
fill_byte = CLEAR_BYTE;
break;
default:
fill_byte = 0x00;
}
// Compute a line mask based on the drawn area
uint8_t* dirtyness = malloc(ctx->display_width / 2);
assert(dirtyness != NULL);
memset(dirtyness, 0, ctx->display_width / 2);
for (int i = 0; i < ctx->display_width; i++) {
if ((i >= ctx->area.x) && (i < ctx->area.x + ctx->area.width)) {
dirtyness[i / 2] |= i % 2 ? 0xF0 : 0x0F;
}
}
epd_populate_line_mask(ctx->line_mask, dirtyness, ctx->display_width / 4);
// mask the line pattern with the populated mask
memset(ctx->static_line_buffer, fill_byte, ctx->display_width / 4);
epd_apply_line_mask(ctx->static_line_buffer, ctx->line_mask, ctx->display_width / 4);
free(dirtyness);
}
void epd_push_pixels_lcd(RenderContext_t* ctx, short time, int color) {
ctx->current_frame = 0;
ctx->lines_total = ctx->display_height;
ctx->lines_consumed = 0;
ctx->static_line_buffer = malloc(ctx->display_width / 4);
assert(ctx->static_line_buffer != NULL);
push_pixels_populate_line(ctx, color);
epd_lcd_frame_done_cb((frame_done_func_t)handle_lcd_frame_done, ctx);
epd_lcd_line_source_cb((line_cb_func_t)&push_pixels_isr, ctx);
epd_set_mode(1);
epd_lcd_start_frame();
xSemaphoreTake(ctx->frame_done, portMAX_DELAY);
epd_set_mode(0);
free(ctx->static_line_buffer);
ctx->static_line_buffer = NULL;
}
#define int_min(a, b) (((a) < (b)) ? (a) : (b))
__attribute__((optimize("O3"))) void IRAM_ATTR
lcd_calculate_frame(RenderContext_t* ctx, int thread_id) {
assert(ctx->lut_lookup_func != NULL);
uint8_t* input_line = ctx->feed_line_buffers[thread_id];
LineQueue_t* lq = &ctx->line_queues[thread_id];
int l = 0;
// if there is an error, start the frame but don't feed data.
if (ctx->error) {
memset(ctx->line_threads, 0, ctx->lines_total);
epd_lcd_line_source_cb((line_cb_func_t)&retrieve_line_isr, ctx);
epd_lcd_start_frame();
ESP_LOGW("epd_lcd", "draw frame draw initiated, but an error flag is set: %X", ctx->error);
return;
}
// line must be able to hold 2-pixel-per-byte or 1-pixel-per-byte data
memset(input_line, 0x00, ctx->display_width);
EpdRect area = ctx->area;
int min_y, max_y, bytes_per_line, _ppB;
const uint8_t* ptr_start;
get_buffer_params(ctx, &bytes_per_line, &ptr_start, &min_y, &max_y, &_ppB);
assert(area.width == ctx->display_width && area.x == 0 && !ctx->error);
// index of the line that triggers the frame output when processed
int trigger_line = int_min(63, max_y - min_y);
while (l = atomic_fetch_add(&ctx->lines_prepared, 1), l < ctx->lines_total) {
ctx->line_threads[l] = thread_id;
// queue is sufficiently filled to fill both bounce buffers, frame
// can begin
if (l - min_y == trigger_line) {
epd_lcd_line_source_cb((line_cb_func_t)&retrieve_line_isr, ctx);
epd_lcd_start_frame();
}
if (l < min_y || l >= max_y
|| (ctx->drawn_lines != NULL && !ctx->drawn_lines[l - area.y])) {
uint8_t* buf = NULL;
while (buf == NULL) {
// break in case of errors
if (ctx->error & EPD_DRAW_EMPTY_LINE_QUEUE) {
printf("on err 1: %d %d\n", ctx->lines_prepared, ctx->lines_consumed);
lq_reset(lq);
return;
};
buf = lq_current(lq);
}
memset(buf, 0x00, lq->element_size);
lq_commit(lq);
continue;
}
uint32_t* lp = (uint32_t*)input_line;
const uint8_t* ptr = ptr_start + bytes_per_line * (l - min_y);
Cache_Start_DCache_Preload((uint32_t)ptr, ctx->display_width, 0);
lp = (uint32_t*)ptr;
uint8_t* buf = NULL;
while (buf == NULL) {
// break in case of errors
if (ctx->error & EPD_DRAW_EMPTY_LINE_QUEUE) {
lq_reset(lq);
printf("on err 2: %d %d\n", ctx->lines_prepared, ctx->lines_consumed);
return;
};
buf = lq_current(lq);
}
ctx->lut_lookup_func(lp, buf, ctx->conversion_lut, ctx->display_width);
// apply the line mask
epd_apply_line_mask_VE(buf, ctx->line_mask, ctx->display_width / 4);
lq_commit(lq);
}
}
#endif

View File

@ -0,0 +1,19 @@
#pragma once
#include "../output_common/render_context.h"
/**
* Lighten / darken picels using the LCD driving method.
*/
void epd_push_pixels_lcd(RenderContext_t* ctx, short time, int color);
/**
* Do a full update cycle with a configured context.
*/
void lcd_do_update(RenderContext_t* ctx);
/**
* Worker thread for output calculation.
* In LCD mode, both threads do the same thing.
*/
void lcd_calculate_frame(RenderContext_t* ctx, int thread_id);

View File

@ -0,0 +1,526 @@
#include "render.h"
#include "epd_board.h"
#include "epd_internals.h"
#include "epdiy.h"
#include <assert.h>
#include <esp_log.h>
#include <esp_timer.h>
#include <esp_types.h>
#include <freertos/FreeRTOS.h>
#include <freertos/semphr.h>
#include <freertos/task.h>
#include <stdalign.h>
#include <stdbool.h>
#include <stdint.h>
#include <stdio.h>
#include <string.h>
#include "esp_heap_caps.h"
#include "output_common/line_queue.h"
#include "output_common/lut.h"
#include "output_common/render_context.h"
#include "output_common/render_method.h"
#include "output_i2s/render_i2s.h"
#include "output_lcd/render_lcd.h"
static inline int min(int x, int y) {
return x < y ? x : y;
}
static inline int max(int x, int y) {
return x > y ? x : y;
}
const int clear_cycle_time = 12;
#define RTOS_ERROR_CHECK(x) \
do { \
esp_err_t __err_rc = (x); \
if (__err_rc != pdPASS) { \
abort(); \
} \
} while (0)
static RenderContext_t render_context;
void epd_push_pixels(EpdRect area, short time, int color) {
render_context.area = area;
#ifdef RENDER_METHOD_LCD
epd_push_pixels_lcd(&render_context, time, color);
#else
epd_push_pixels_i2s(&render_context, area, time, color);
#endif
}
///////////////////////////// Coordination ///////////////////////////////
/**
* Find the waveform temperature range index for a given temperature in °C.
* If no range in the waveform data fits the given temperature, return the
* closest one.
* Returns -1 if the waveform does not contain any temperature range.
*/
int waveform_temp_range_index(const EpdWaveform* waveform, int temperature) {
int idx = 0;
if (waveform->num_temp_ranges == 0) {
return -1;
}
while (idx < waveform->num_temp_ranges - 1 && waveform->temp_intervals[idx].min < temperature) {
idx++;
}
return idx;
}
static int get_waveform_index(const EpdWaveform* waveform, enum EpdDrawMode mode) {
for (int i = 0; i < waveform->num_modes; i++) {
if (waveform->mode_data[i]->type == (mode & 0x3F)) {
return i;
}
}
return -1;
}
///////////////////////////// API Procedures //////////////////////////////////
/// Rounded up display height for even division into multi-line buffers.
static inline int rounded_display_height() {
return (((epd_height() + 7) / 8) * 8);
}
// FIXME: fix misleading naming:
// area -> buffer dimensions
// crop -> area taken out of buffer
enum EpdDrawError IRAM_ATTR epd_draw_base(
EpdRect area,
const uint8_t* data,
EpdRect crop_to,
enum EpdDrawMode mode,
int temperature,
const bool* drawn_lines,
const uint8_t* drawn_columns,
const EpdWaveform* waveform
) {
if (waveform == NULL) {
return EPD_DRAW_NO_PHASES_AVAILABLE;
}
int waveform_range = waveform_temp_range_index(waveform, temperature);
if (waveform_range < 0) {
return EPD_DRAW_NO_PHASES_AVAILABLE;
}
int waveform_index = 0;
uint8_t frame_count = 0;
const EpdWaveformPhases* waveform_phases = NULL;
// no waveform required for monochrome mode
if (!(mode & MODE_EPDIY_MONOCHROME)) {
waveform_index = get_waveform_index(waveform, mode);
if (waveform_index < 0) {
return EPD_DRAW_MODE_NOT_FOUND;
}
waveform_phases = waveform->mode_data[waveform_index]->range_data[waveform_range];
// FIXME: error if not present
frame_count = waveform_phases->phases;
} else {
frame_count = 1;
}
if (crop_to.width < 0 || crop_to.height < 0) {
return EPD_DRAW_INVALID_CROP;
}
const bool crop = (crop_to.width > 0 && crop_to.height > 0);
if (crop
&& (crop_to.width > area.width || crop_to.height > area.height || crop_to.x > area.width
|| crop_to.y > area.height)) {
return EPD_DRAW_INVALID_CROP;
}
#ifdef RENDER_METHOD_LCD
if (mode & MODE_PACKING_1PPB_DIFFERENCE && render_context.conversion_lut_size > 1 << 10) {
ESP_LOGI(
"epdiy",
"Using optimized vector implementation on the ESP32-S3, only 1k of %d LUT in use!",
render_context.conversion_lut_size
);
}
#endif
LutFunctionPair lut_functions = find_lut_functions(mode, render_context.conversion_lut_size);
if (lut_functions.build_func == NULL || lut_functions.lookup_func == NULL) {
ESP_LOGE("epdiy", "no output lookup method found for your mode and LUT size!");
return EPD_DRAW_LOOKUP_NOT_IMPLEMENTED;
}
render_context.area = area;
render_context.crop_to = crop_to;
render_context.waveform_range = waveform_range;
render_context.waveform_index = waveform_index;
render_context.mode = mode;
render_context.waveform = waveform;
render_context.error = EPD_DRAW_SUCCESS;
render_context.drawn_lines = drawn_lines;
render_context.data_ptr = data;
render_context.lut_build_func = lut_functions.build_func;
render_context.lut_lookup_func = lut_functions.lookup_func;
render_context.lines_prepared = 0;
render_context.lines_consumed = 0;
render_context.lines_total = rounded_display_height();
render_context.current_frame = 0;
render_context.cycle_frames = frame_count;
render_context.phase_times = NULL;
if (waveform_phases != NULL && waveform_phases->phase_times != NULL) {
render_context.phase_times = waveform_phases->phase_times;
}
epd_populate_line_mask(
render_context.line_mask, drawn_columns, render_context.display_width / 4
);
#ifdef RENDER_METHOD_I2S
i2s_do_update(&render_context);
#elif defined(RENDER_METHOD_LCD)
lcd_do_update(&render_context);
#endif
if (render_context.error & EPD_DRAW_EMPTY_LINE_QUEUE) {
ESP_LOGE("epdiy", "line buffer underrun occurred!");
}
if (render_context.error != EPD_DRAW_SUCCESS) {
return render_context.error;
}
return EPD_DRAW_SUCCESS;
}
static void IRAM_ATTR render_thread(void* arg) {
int thread_id = (int)arg;
while (true) {
ulTaskNotifyTake(pdTRUE, portMAX_DELAY);
#ifdef RENDER_METHOD_LCD
lcd_calculate_frame(&render_context, thread_id);
#elif defined(RENDER_METHOD_I2S)
if (thread_id == 0) {
i2s_fetch_frame_data(&render_context, thread_id);
} else {
i2s_output_frame(&render_context, thread_id);
}
#endif
xSemaphoreGive(render_context.feed_done_smphr[thread_id]);
}
}
void epd_clear_area(EpdRect area) {
epd_clear_area_cycles(area, 3, clear_cycle_time);
}
void epd_clear_area_cycles(EpdRect area, int cycles, int cycle_time) {
const short white_time = cycle_time;
const short dark_time = cycle_time;
for (int c = 0; c < cycles; c++) {
for (int i = 0; i < 10; i++) {
epd_push_pixels(area, dark_time, 0);
}
for (int i = 0; i < 10; i++) {
epd_push_pixels(area, white_time, 1);
}
for (int i = 0; i < 2; i++) {
epd_push_pixels(area, white_time, 2);
}
}
}
void epd_renderer_init(enum EpdInitOptions options) {
// Either the board should be set in menuconfig or the epd_set_board() must
// be called before epd_init()
assert((epd_current_board() != NULL));
epd_current_board()->init(epd_width());
epd_control_reg_init();
render_context.display_width = epd_width();
render_context.display_height = epd_height();
size_t lut_size = 0;
if (options & EPD_LUT_1K) {
lut_size = 1 << 10;
} else if (options & EPD_LUT_64K) {
lut_size = 1 << 16;
} else if (options == EPD_OPTIONS_DEFAULT) {
#ifdef RENDER_METHOD_LCD
lut_size = 1 << 10;
#else
lut_size = 1 << 16;
#endif
} else {
ESP_LOGE("epd", "invalid init options: %d", options);
return;
}
ESP_LOGI("epd", "Space used for waveform LUT: %dK", lut_size / 1024);
render_context.conversion_lut
= (uint8_t*)heap_caps_malloc(lut_size, MALLOC_CAP_8BIT | MALLOC_CAP_INTERNAL);
if (render_context.conversion_lut == NULL) {
ESP_LOGE("epd", "could not allocate LUT!");
abort();
}
render_context.conversion_lut_size = lut_size;
render_context.static_line_buffer = NULL;
render_context.frame_done = xSemaphoreCreateBinary();
for (int i = 0; i < NUM_RENDER_THREADS; i++) {
render_context.feed_done_smphr[i] = xSemaphoreCreateBinary();
}
// When using the LCD peripheral, we may need padding lines to
// satisfy the bounce buffer size requirements
render_context.line_threads = (uint8_t*)heap_caps_malloc(
rounded_display_height(), MALLOC_CAP_8BIT | MALLOC_CAP_INTERNAL
);
int queue_len = 32;
if (options & EPD_FEED_QUEUE_32) {
queue_len = 32;
} else if (options & EPD_FEED_QUEUE_8) {
queue_len = 8;
}
if (render_context.conversion_lut == NULL) {
ESP_LOGE("epd", "could not allocate line mask!");
abort();
}
render_context.line_mask
= heap_caps_aligned_alloc(16, epd_width() / 4, MALLOC_CAP_8BIT | MALLOC_CAP_INTERNAL);
assert(render_context.line_mask != NULL);
#ifdef RENDER_METHOD_LCD
size_t queue_elem_size = render_context.display_width / 4;
#elif defined(RENDER_METHOD_I2S)
size_t queue_elem_size = render_context.display_width;
#endif
for (int i = 0; i < NUM_RENDER_THREADS; i++) {
render_context.line_queues[i] = lq_init(queue_len, queue_elem_size);
render_context.feed_line_buffers[i] = (uint8_t*)heap_caps_malloc(
render_context.display_width, MALLOC_CAP_8BIT | MALLOC_CAP_INTERNAL
);
assert(render_context.feed_line_buffers[i] != NULL);
RTOS_ERROR_CHECK(xTaskCreatePinnedToCore(
render_thread,
"epd_prep",
1 << 12,
(void*)i,
configMAX_PRIORITIES - 1,
&render_context.feed_tasks[i],
i
));
}
}
void epd_renderer_deinit() {
const EpdBoardDefinition* epd_board = epd_current_board();
epd_board->poweroff(epd_ctrl_state());
for (int i = 0; i < NUM_RENDER_THREADS; i++) {
vTaskDelete(render_context.feed_tasks[i]);
lq_free(&render_context.line_queues[i]);
heap_caps_free(render_context.feed_line_buffers[i]);
vSemaphoreDelete(render_context.feed_done_smphr[i]);
}
#ifdef RENDER_METHOD_I2S
i2s_deinit();
#endif
epd_control_reg_deinit();
if (epd_board->deinit) {
epd_board->deinit();
}
heap_caps_free(render_context.conversion_lut);
heap_caps_free(render_context.line_threads);
heap_caps_free(render_context.line_mask);
vSemaphoreDelete(render_context.frame_done);
}
#ifdef RENDER_METHOD_LCD
uint32_t epd_interlace_4bpp_line_VE(
const uint8_t* to,
const uint8_t* from,
uint8_t* interlaced,
uint8_t* col_dirtyness,
int fb_width
);
#endif
/**
* Interlaces `len` nibbles from the buffers `to` and `from` into `interlaced`.
* In the process, tracks which nibbles differ in `col_dirtyness`.
* Returns `1` if there are differences, `0` otherwise.
* Does not require special alignment of the buffers beyond 32 bit alignment.
*/
__attribute__((optimize("O3"))) static inline int _interlace_line_unaligned(
const uint8_t* to, const uint8_t* from, uint8_t* interlaced, uint8_t* col_dirtyness, int len
) {
int dirty = 0;
for (int x = 0; x < len; x++) {
uint8_t t = *(to + x / 2);
uint8_t f = *(from + x / 2);
t = (x % 2) ? (t >> 4) : (t & 0x0f);
f = (x % 2) ? (f >> 4) : (f & 0x0f);
col_dirtyness[x / 2] |= (t ^ f) << (4 * (x % 2));
dirty |= (t ^ f);
interlaced[x] = (t << 4) | f;
}
return dirty;
}
/**
* Interlaces the lines at `to`, `from` into `interlaced`.
* returns `1` if there are differences, `0` otherwise.
*/
__attribute__((optimize("O3"))) bool _epd_interlace_line(
const uint8_t* to,
const uint8_t* from,
uint8_t* interlaced,
uint8_t* col_dirtyness,
int fb_width
) {
#ifdef RENDER_METHOD_I2S
return _interlace_line_unaligned(to, from, interlaced, col_dirtyness, fb_width) > 0;
#elif defined(RENDER_METHOD_LCD)
// Use Vector Extensions with the ESP32-S3.
// Both input buffers should have the same alignment w.r.t. 16 bytes,
// as asserted in epd_difference_image_base.
uint32_t dirty = 0;
// alignment boundaries in pixels
int unaligned_len_front_px = ((16 - (uint32_t)to % 16) * 2) % 32;
int unaligned_len_back_px = (((uint32_t)to + fb_width / 2) % 16) * 2;
int unaligned_back_start_px = fb_width - unaligned_len_back_px;
int aligned_len_px = fb_width - unaligned_len_front_px - unaligned_len_back_px;
dirty |= _interlace_line_unaligned(to, from, interlaced, col_dirtyness, unaligned_len_front_px);
dirty |= epd_interlace_4bpp_line_VE(
to + unaligned_len_front_px / 2,
from + unaligned_len_front_px / 2,
interlaced + unaligned_len_front_px,
col_dirtyness + unaligned_len_front_px / 2,
aligned_len_px
);
dirty |= _interlace_line_unaligned(
to + unaligned_back_start_px / 2,
from + unaligned_back_start_px / 2,
interlaced + unaligned_back_start_px,
col_dirtyness + unaligned_back_start_px / 2,
unaligned_len_back_px
);
return dirty;
#endif
}
EpdRect epd_difference_image_base(
const uint8_t* to,
const uint8_t* from,
EpdRect crop_to,
int fb_width,
int fb_height,
uint8_t* interlaced,
bool* dirty_lines,
uint8_t* col_dirtyness
) {
assert(fb_width % 8 == 0);
assert(col_dirtyness != NULL);
// these buffers should be allocated 16 byte aligned
assert((uint32_t)to % 16 == 0);
assert((uint32_t)from % 16 == 0);
assert((uint32_t)col_dirtyness % 16 == 0);
assert((uint32_t)interlaced % 16 == 0);
memset(col_dirtyness, 0, fb_width / 2);
memset(dirty_lines, 0, sizeof(bool) * fb_height);
int x_end = min(fb_width, crop_to.x + crop_to.width);
int y_end = min(fb_height, crop_to.y + crop_to.height);
for (int y = crop_to.y; y < y_end; y++) {
uint32_t offset = y * fb_width / 2;
int dirty = _epd_interlace_line(
to + offset, from + offset, interlaced + offset * 2, col_dirtyness, fb_width
);
dirty_lines[y] = dirty;
}
int min_x, min_y, max_x, max_y;
for (min_x = crop_to.x; min_x < x_end; min_x++) {
uint8_t mask = min_x % 2 ? 0xF0 : 0x0F;
if ((col_dirtyness[min_x / 2] & mask) != 0)
break;
}
for (max_x = x_end - 1; max_x >= crop_to.x; max_x--) {
uint8_t mask = min_x % 2 ? 0xF0 : 0x0F;
if ((col_dirtyness[max_x / 2] & mask) != 0)
break;
}
for (min_y = crop_to.y; min_y < y_end; min_y++) {
if (dirty_lines[min_y] != 0)
break;
}
for (max_y = y_end - 1; max_y >= crop_to.y; max_y--) {
if (dirty_lines[max_y] != 0)
break;
}
EpdRect crop_rect = {
.x = min_x,
.y = min_y,
.width = max(max_x - min_x + 1, 0),
.height = max(max_y - min_y + 1, 0),
};
return crop_rect;
}
EpdRect epd_difference_image(
const uint8_t* to,
const uint8_t* from,
uint8_t* interlaced,
bool* dirty_lines,
uint8_t* col_dirtyness
) {
return epd_difference_image_base(
to,
from,
epd_full_screen(),
epd_width(),
epd_height(),
interlaced,
dirty_lines,
col_dirtyness
);
}
EpdRect epd_difference_image_cropped(
const uint8_t* to,
const uint8_t* from,
EpdRect crop_to,
uint8_t* interlaced,
bool* dirty_lines,
uint8_t* col_dirtyness
) {
EpdRect result = epd_difference_image_base(
to, from, crop_to, epd_width(), epd_height(), interlaced, dirty_lines, col_dirtyness
);
return result;
}

View File

@ -0,0 +1,12 @@
#pragma once
#include "epdiy.h"
/**
* Initialize the EPD renderer and its render context.
*/
void epd_renderer_init(enum EpdInitOptions options);
/**
* Deinitialize the EPD renderer and free up its resources.
*/
void epd_renderer_deinit();

Some files were not shown because too many files have changed in this diff Show More