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:
parent
7ebb747dc3
commit
2deb34e856
@ -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)
|
||||
|
||||
@ -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)
|
||||
|
||||
11
lib/libesp32_eink/epdiy/.clang-format
Normal file
11
lib/libesp32_eink/epdiy/.clang-format
Normal 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
|
||||
10
lib/libesp32_eink/epdiy/.gitignore
vendored
10
lib/libesp32_eink/epdiy/.gitignore
vendored
@ -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
|
||||
|
||||
6
lib/libesp32_eink/epdiy/.gitmodules
vendored
6
lib/libesp32_eink/epdiy/.gitmodules
vendored
@ -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/
|
||||
@ -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
|
||||
|
||||
165
lib/libesp32_eink/epdiy/LICENSE
Normal file
165
lib/libesp32_eink/epdiy/LICENSE
Normal 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.
|
||||
@ -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
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
[](https://epdiy.readthedocs.io/en/latest/?badge=latest) [](https://matrix.to/#/!GUXWriqsBKkWyXzsBK:matrix.vroland.de?via=matrix.vroland.de) [](https://join.slack.com/t/epdiy/shared_invite/zt-mw3iat5g-6jRylNrK2g79HSxeznvmPg)
|
||||
[](https://epdiy.readthedocs.io/en/latest/?badge=latest) [](https://matrix.to/#/!GUXWriqsBKkWyXzsBK:matrix.vroland.de?via=matrix.vroland.de) [](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).
|
||||

|
||||
|
||||
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
|
||||
----------------
|
||||
|
||||
- [](https://matrix.to/#/!GUXWriqsBKkWyXzsBK:matrix.vroland.de?via=matrix.vroland.de) Matrix Community: +epdiy:matrix.vroland.de
|
||||
- [](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
|
||||
|
||||

|
||||

|
||||

|
||||

|
||||
|
||||
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.
|
||||
|
||||
|
||||
@ -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
|
||||
4
lib/libesp32_eink/epdiy/idf_component.yml
Normal file
4
lib/libesp32_eink/epdiy/idf_component.yml
Normal 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
|
||||
32
lib/libesp32_eink/epdiy/library.json
Normal file
32
lib/libesp32_eink/epdiy/library.json
Normal 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/"
|
||||
}
|
||||
}
|
||||
@ -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
|
||||
|
||||
101
lib/libesp32_eink/epdiy/scripts/README.md
Normal file
101
lib/libesp32_eink/epdiy/scripts/README.md
Normal 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.
|
||||
|
||||
@ -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("*/")
|
||||
|
||||
|
||||
@ -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:
|
||||
|
||||
@ -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 = []
|
||||
|
||||
0
lib/libesp32_eink/epdiy/src/epd_driver/Makefile → lib/libesp32_eink/epdiy/src/Makefile
Executable file → Normal file
0
lib/libesp32_eink/epdiy/src/epd_driver/Makefile → lib/libesp32_eink/epdiy/src/Makefile
Executable file → Normal file
77
lib/libesp32_eink/epdiy/src/board/epd_board.c
Normal file
77
lib/libesp32_eink/epdiy/src/board/epd_board.c
Normal 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);
|
||||
}
|
||||
34
lib/libesp32_eink/epdiy/src/board/epd_board_common.c
Normal file
34
lib/libesp32_eink/epdiy/src/board/epd_board_common.c
Normal 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;
|
||||
}
|
||||
8
lib/libesp32_eink/epdiy/src/board/epd_board_common.h
Normal file
8
lib/libesp32_eink/epdiy/src/board/epd_board_common.h
Normal 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();
|
||||
227
lib/libesp32_eink/epdiy/src/board/epd_board_lilygo_t5_47.c
Normal file
227
lib/libesp32_eink/epdiy/src/board/epd_board_lilygo_t5_47.c
Normal 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,
|
||||
};
|
||||
172
lib/libesp32_eink/epdiy/src/board/epd_board_v2_v3.c
Normal file
172
lib/libesp32_eink/epdiy/src/board/epd_board_v2_v3.c
Normal 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,
|
||||
};
|
||||
194
lib/libesp32_eink/epdiy/src/board/epd_board_v4.c
Normal file
194
lib/libesp32_eink/epdiy/src/board/epd_board_v4.c
Normal 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,
|
||||
};
|
||||
196
lib/libesp32_eink/epdiy/src/board/epd_board_v5.c
Normal file
196
lib/libesp32_eink/epdiy/src/board/epd_board_v5.c
Normal 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,
|
||||
};
|
||||
359
lib/libesp32_eink/epdiy/src/board/epd_board_v6.c
Normal file
359
lib/libesp32_eink/epdiy/src/board/epd_board_v6.c
Normal 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,
|
||||
};
|
||||
338
lib/libesp32_eink/epdiy/src/board/epd_board_v7.c
Normal file
338
lib/libesp32_eink/epdiy/src/board/epd_board_v7.c
Normal 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,
|
||||
};
|
||||
288
lib/libesp32_eink/epdiy/src/board/epd_board_v7_raw.c
Normal file
288
lib/libesp32_eink/epdiy/src/board/epd_board_v7_raw.c
Normal 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,
|
||||
};
|
||||
273
lib/libesp32_eink/epdiy/src/board/lilygo_board_s3.c
Normal file
273
lib/libesp32_eink/epdiy/src/board/lilygo_board_s3.c
Normal 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,
|
||||
};
|
||||
107
lib/libesp32_eink/epdiy/src/board/pca9555.c
Normal file
107
lib/libesp32_eink/epdiy/src/board/pca9555.c
Normal 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];
|
||||
}
|
||||
35
lib/libesp32_eink/epdiy/src/board/pca9555.h
Normal file
35
lib/libesp32_eink/epdiy/src/board/pca9555.h
Normal 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
|
||||
128
lib/libesp32_eink/epdiy/src/board/tps65185.c
Normal file
128
lib/libesp32_eink/epdiy/src/board/tps65185.c
Normal 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;
|
||||
}
|
||||
}
|
||||
56
lib/libesp32_eink/epdiy/src/board/tps65185.h
Normal file
56
lib/libesp32_eink/epdiy/src/board/tps65185.h
Normal 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
|
||||
5
lib/libesp32_eink/epdiy/src/board_specific.c
Normal file
5
lib/libesp32_eink/epdiy/src/board_specific.c
Normal file
@ -0,0 +1,5 @@
|
||||
#include "epd_board_specific.h"
|
||||
|
||||
void epd_powerdown() {
|
||||
epd_powerdown_lilygo_t5_47();
|
||||
}
|
||||
11
lib/libesp32_eink/epdiy/src/epd_driver/builtin_waveforms.c → lib/libesp32_eink/epdiy/src/builtin_waveforms.c
Executable file → Normal file
11
lib/libesp32_eink/epdiy/src/epd_driver/builtin_waveforms.c → lib/libesp32_eink/epdiy/src/builtin_waveforms.c
Executable file → Normal 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"
|
||||
159
lib/libesp32_eink/epdiy/src/diff.S
Normal file
159
lib/libesp32_eink/epdiy/src/diff.S
Normal 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
|
||||
83
lib/libesp32_eink/epdiy/src/displays.c
Normal file
83
lib/libesp32_eink/epdiy/src/displays.c
Normal 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,
|
||||
};
|
||||
@ -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;
|
||||
|
||||
133
lib/libesp32_eink/epdiy/src/epd_board.h
Normal file
133
lib/libesp32_eink/epdiy/src/epd_board.h
Normal 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);
|
||||
32
lib/libesp32_eink/epdiy/src/epd_board_specific.h
Normal file
32
lib/libesp32_eink/epdiy/src/epd_board_specific.h
Normal 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));
|
||||
43
lib/libesp32_eink/epdiy/src/epd_display.h
Normal file
43
lib/libesp32_eink/epdiy/src/epd_display.h
Normal 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;
|
||||
@ -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"
|
||||
@ -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)
|
||||
@ -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
|
||||
@ -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>.
|
||||
@ -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
|
||||
}
|
||||
@ -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
|
||||
}
|
||||
@ -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();
|
||||
};
|
||||
@ -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();
|
||||
@ -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);
|
||||
}
|
||||
@ -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;
|
||||
}
|
||||
@ -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();
|
||||
@ -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;
|
||||
}
|
||||
@ -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();
|
||||
}
|
||||
@ -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);
|
||||
}
|
||||
@ -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();
|
||||
@ -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
|
||||
@ -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
|
||||
@ -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
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
@ -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);
|
||||
@ -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;
|
||||
}
|
||||
@ -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; }
|
||||
@ -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
|
||||
|
||||
89
lib/libesp32_eink/epdiy/src/epd_internals.h
Normal file
89
lib/libesp32_eink/epdiy/src/epd_internals.h
Normal 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
|
||||
545
lib/libesp32_eink/epdiy/src/epdiy.c
Normal file
545
lib/libesp32_eink/epdiy/src/epdiy.c
Normal 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
|
||||
}
|
||||
605
lib/libesp32_eink/epdiy/src/epdiy.h
Normal file
605
lib/libesp32_eink/epdiy/src/epdiy.h
Normal 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
|
||||
434
lib/libesp32_eink/epdiy/src/font.c
Normal file
434
lib/libesp32_eink/epdiy/src/font.c
Normal 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;
|
||||
}
|
||||
0
lib/libesp32_eink/epdiy/src/epd_driver/hacks.cmake → lib/libesp32_eink/epdiy/src/hacks.cmake
Executable file → Normal file
0
lib/libesp32_eink/epdiy/src/epd_driver/hacks.cmake → lib/libesp32_eink/epdiy/src/hacks.cmake
Executable file → Normal file
216
lib/libesp32_eink/epdiy/src/highlevel.c
Normal file
216
lib/libesp32_eink/epdiy/src/highlevel.c
Normal 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;
|
||||
}
|
||||
86
lib/libesp32_eink/epdiy/src/output_common/line_queue.c
Normal file
86
lib/libesp32_eink/epdiy/src/output_common/line_queue.c
Normal 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;
|
||||
}
|
||||
40
lib/libesp32_eink/epdiy/src/output_common/line_queue.h
Normal file
40
lib/libesp32_eink/epdiy/src/output_common/line_queue.h
Normal 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);
|
||||
145
lib/libesp32_eink/epdiy/src/output_common/lut.S
Normal file
145
lib/libesp32_eink/epdiy/src/output_common/lut.S
Normal 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
|
||||
495
lib/libesp32_eink/epdiy/src/output_common/lut.c
Normal file
495
lib/libesp32_eink/epdiy/src/output_common/lut.c
Normal 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;
|
||||
}
|
||||
47
lib/libesp32_eink/epdiy/src/output_common/lut.h
Normal file
47
lib/libesp32_eink/epdiy/src/output_common/lut.h
Normal 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);
|
||||
106
lib/libesp32_eink/epdiy/src/output_common/render_context.c
Normal file
106
lib/libesp32_eink/epdiy/src/output_common/render_context.c
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
110
lib/libesp32_eink/epdiy/src/output_common/render_context.h
Normal file
110
lib/libesp32_eink/epdiy/src/output_common/render_context.h
Normal 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
|
||||
);
|
||||
10
lib/libesp32_eink/epdiy/src/output_common/render_method.c
Normal file
10
lib/libesp32_eink/epdiy/src/output_common/render_method.c
Normal 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
|
||||
29
lib/libesp32_eink/epdiy/src/output_common/render_method.h
Normal file
29
lib/libesp32_eink/epdiy/src/output_common/render_method.h
Normal 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
|
||||
303
lib/libesp32_eink/epdiy/src/output_i2s/i2s_data_bus.c
Normal file
303
lib/libesp32_eink/epdiy/src/output_i2s/i2s_data_bus.c
Normal 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
|
||||
77
lib/libesp32_eink/epdiy/src/output_i2s/i2s_data_bus.h
Normal file
77
lib/libesp32_eink/epdiy/src/output_i2s/i2s_data_bus.h
Normal 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();
|
||||
402
lib/libesp32_eink/epdiy/src/output_i2s/render_i2s.c
Normal file
402
lib/libesp32_eink/epdiy/src/output_i2s/render_i2s.c
Normal 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
|
||||
59
lib/libesp32_eink/epdiy/src/output_i2s/render_i2s.h
Normal file
59
lib/libesp32_eink/epdiy/src/output_i2s/render_i2s.h
Normal 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
|
||||
}
|
||||
97
lib/libesp32_eink/epdiy/src/output_i2s/rmt_pulse.c
Normal file
97
lib/libesp32_eink/epdiy/src/output_i2s/rmt_pulse.c
Normal 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
|
||||
15
lib/libesp32_eink/epdiy/src/epd_driver/rmt_pulse.h → lib/libesp32_eink/epdiy/src/output_i2s/rmt_pulse.h
Executable file → Normal file
15
lib/libesp32_eink/epdiy/src/epd_driver/rmt_pulse.h → lib/libesp32_eink/epdiy/src/output_i2s/rmt_pulse.h
Executable file → Normal 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);
|
||||
22
lib/libesp32_eink/epdiy/src/output_lcd/idf-4-backports.h
Normal file
22
lib/libesp32_eink/epdiy/src/output_lcd/idf-4-backports.h
Normal 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;
|
||||
}
|
||||
706
lib/libesp32_eink/epdiy/src/output_lcd/lcd_driver.c
Normal file
706
lib/libesp32_eink/epdiy/src/output_lcd/lcd_driver.c
Normal 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
|
||||
50
lib/libesp32_eink/epdiy/src/output_lcd/lcd_driver.h
Normal file
50
lib/libesp32_eink/epdiy/src/output_lcd/lcd_driver.h
Normal 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);
|
||||
246
lib/libesp32_eink/epdiy/src/output_lcd/render_lcd.c
Normal file
246
lib/libesp32_eink/epdiy/src/output_lcd/render_lcd.c
Normal 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
|
||||
19
lib/libesp32_eink/epdiy/src/output_lcd/render_lcd.h
Normal file
19
lib/libesp32_eink/epdiy/src/output_lcd/render_lcd.h
Normal 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);
|
||||
526
lib/libesp32_eink/epdiy/src/render.c
Normal file
526
lib/libesp32_eink/epdiy/src/render.c
Normal 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;
|
||||
}
|
||||
12
lib/libesp32_eink/epdiy/src/render.h
Normal file
12
lib/libesp32_eink/epdiy/src/render.h
Normal 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();
|
||||
0
lib/libesp32_eink/epdiy/src/epd_driver/waveforms/epdiy_ED047TC1.h → lib/libesp32_eink/epdiy/src/waveforms/epdiy_ED047TC1.h
Executable file → Normal file
0
lib/libesp32_eink/epdiy/src/epd_driver/waveforms/epdiy_ED047TC1.h → lib/libesp32_eink/epdiy/src/waveforms/epdiy_ED047TC1.h
Executable file → Normal file
File diff suppressed because one or more lines are too long
0
lib/libesp32_eink/epdiy/src/epd_driver/waveforms/epdiy_ED060SC4.h → lib/libesp32_eink/epdiy/src/waveforms/epdiy_ED060SC4.h
Executable file → Normal file
0
lib/libesp32_eink/epdiy/src/epd_driver/waveforms/epdiy_ED060SC4.h → lib/libesp32_eink/epdiy/src/waveforms/epdiy_ED060SC4.h
Executable file → Normal file
0
lib/libesp32_eink/epdiy/src/epd_driver/waveforms/epdiy_ED060SCT.h → lib/libesp32_eink/epdiy/src/waveforms/epdiy_ED060SCT.h
Executable file → Normal file
0
lib/libesp32_eink/epdiy/src/epd_driver/waveforms/epdiy_ED060SCT.h → lib/libesp32_eink/epdiy/src/waveforms/epdiy_ED060SCT.h
Executable file → Normal file
0
lib/libesp32_eink/epdiy/src/epd_driver/waveforms/epdiy_ED060XC3.h → lib/libesp32_eink/epdiy/src/waveforms/epdiy_ED060XC3.h
Executable file → Normal file
0
lib/libesp32_eink/epdiy/src/epd_driver/waveforms/epdiy_ED060XC3.h → lib/libesp32_eink/epdiy/src/waveforms/epdiy_ED060XC3.h
Executable file → Normal file
0
lib/libesp32_eink/epdiy/src/epd_driver/waveforms/epdiy_ED097OC4.h → lib/libesp32_eink/epdiy/src/waveforms/epdiy_ED097OC4.h
Executable file → Normal file
0
lib/libesp32_eink/epdiy/src/epd_driver/waveforms/epdiy_ED097OC4.h → lib/libesp32_eink/epdiy/src/waveforms/epdiy_ED097OC4.h
Executable file → Normal file
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user