├── .gitignore ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── asm_thumb ├── README.md ├── __init__.py ├── fractal.py └── monobitmap.py ├── fractal.py ├── main.py ├── monobitmap.py ├── photos └── m4-epaper27-julia.jpg ├── third_party ├── __init__.py └── waveshare │ ├── LICENSE │ ├── __init__.py │ ├── color_epd1in54.py │ ├── color_epd2in13.py │ ├── epd2in13.py │ ├── epd2in7.py │ ├── epd2in9.py │ └── epdif.py └── update-device.sh /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | *.egg-info/ 24 | .installed.cfg 25 | *.egg 26 | MANIFEST 27 | 28 | # PyInstaller 29 | # Usually these files are written by a python script from a template 30 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 31 | *.manifest 32 | *.spec 33 | 34 | # Installer logs 35 | pip-log.txt 36 | pip-delete-this-directory.txt 37 | 38 | # Unit test / coverage reports 39 | htmlcov/ 40 | .tox/ 41 | .coverage 42 | .coverage.* 43 | .cache 44 | nosetests.xml 45 | coverage.xml 46 | *.cover 47 | .hypothesis/ 48 | .pytest_cache/ 49 | 50 | # Translations 51 | *.mo 52 | *.pot 53 | 54 | # Django stuff: 55 | *.log 56 | local_settings.py 57 | db.sqlite3 58 | 59 | # Flask stuff: 60 | instance/ 61 | .webassets-cache 62 | 63 | # Scrapy stuff: 64 | .scrapy 65 | 66 | # Sphinx documentation 67 | docs/_build/ 68 | 69 | # PyBuilder 70 | target/ 71 | 72 | # Jupyter Notebook 73 | .ipynb_checkpoints 74 | 75 | # pyenv 76 | .python-version 77 | 78 | # celery beat schedule file 79 | celerybeat-schedule 80 | 81 | # SageMath parsed files 82 | *.sage.py 83 | 84 | # Environments 85 | .env 86 | .venv 87 | env/ 88 | venv/ 89 | ENV/ 90 | env.bak/ 91 | venv.bak/ 92 | 93 | # Spyder project settings 94 | .spyderproject 95 | .spyproject 96 | 97 | # Rope project settings 98 | .ropeproject 99 | 100 | # mkdocs documentation 101 | /site 102 | 103 | # mypy 104 | .mypy_cache/ 105 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # How to Contribute 2 | 3 | We'd love to accept your patches and contributions to this project. There are 4 | just a few small guidelines you need to follow. 5 | 6 | ## Contributor License Agreement 7 | 8 | Contributions to this project must be accompanied by a Contributor License 9 | Agreement. You (or your employer) retain the copyright to your contribution; 10 | this simply gives us permission to use and redistribute your contributions as 11 | part of the project. Head over to to see 12 | your current agreements on file or to sign a new one. 13 | 14 | You generally only need to submit a CLA once, so if you've already submitted one 15 | (even if it was for a different project), you probably don't need to do it 16 | again. 17 | 18 | ## Code reviews 19 | 20 | All submissions, including submissions by project members, require review. We 21 | use GitHub pull requests for this purpose. Consult 22 | [GitHub Help](https://help.github.com/articles/about-pull-requests/) for more 23 | information on using pull requests. 24 | 25 | ## Community Guidelines 26 | 27 | This project follows [Google's Open Source Community 28 | Guidelines](https://opensource.google.com/conduct/). 29 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | 2 | Apache License 3 | Version 2.0, January 2004 4 | http://www.apache.org/licenses/ 5 | 6 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 7 | 8 | 1. Definitions. 9 | 10 | "License" shall mean the terms and conditions for use, reproduction, 11 | and distribution as defined by Sections 1 through 9 of this document. 12 | 13 | "Licensor" shall mean the copyright owner or entity authorized by 14 | the copyright owner that is granting the License. 15 | 16 | "Legal Entity" shall mean the union of the acting entity and all 17 | other entities that control, are controlled by, or are under common 18 | control with that entity. For the purposes of this definition, 19 | "control" means (i) the power, direct or indirect, to cause the 20 | direction or management of such entity, whether by contract or 21 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 22 | outstanding shares, or (iii) beneficial ownership of such entity. 23 | 24 | "You" (or "Your") shall mean an individual or Legal Entity 25 | exercising permissions granted by this License. 26 | 27 | "Source" form shall mean the preferred form for making modifications, 28 | including but not limited to software source code, documentation 29 | source, and configuration files. 30 | 31 | "Object" form shall mean any form resulting from mechanical 32 | transformation or translation of a Source form, including but 33 | not limited to compiled object code, generated documentation, 34 | and conversions to other media types. 35 | 36 | "Work" shall mean the work of authorship, whether in Source or 37 | Object form, made available under the License, as indicated by a 38 | copyright notice that is included in or attached to the work 39 | (an example is provided in the Appendix below). 40 | 41 | "Derivative Works" shall mean any work, whether in Source or Object 42 | form, that is based on (or derived from) the Work and for which the 43 | editorial revisions, annotations, elaborations, or other modifications 44 | represent, as a whole, an original work of authorship. For the purposes 45 | of this License, Derivative Works shall not include works that remain 46 | separable from, or merely link (or bind by name) to the interfaces of, 47 | the Work and Derivative Works thereof. 48 | 49 | "Contribution" shall mean any work of authorship, including 50 | the original version of the Work and any modifications or additions 51 | to that Work or Derivative Works thereof, that is intentionally 52 | submitted to Licensor for inclusion in the Work by the copyright owner 53 | or by an individual or Legal Entity authorized to submit on behalf of 54 | the copyright owner. For the purposes of this definition, "submitted" 55 | means any form of electronic, verbal, or written communication sent 56 | to the Licensor or its representatives, including but not limited to 57 | communication on electronic mailing lists, source code control systems, 58 | and issue tracking systems that are managed by, or on behalf of, the 59 | Licensor for the purpose of discussing and improving the Work, but 60 | excluding communication that is conspicuously marked or otherwise 61 | designated in writing by the copyright owner as "Not a Contribution." 62 | 63 | "Contributor" shall mean Licensor and any individual or Legal Entity 64 | on behalf of whom a Contribution has been received by Licensor and 65 | subsequently incorporated within the Work. 66 | 67 | 2. Grant of Copyright License. Subject to the terms and conditions of 68 | this License, each Contributor hereby grants to You a perpetual, 69 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 70 | copyright license to reproduce, prepare Derivative Works of, 71 | publicly display, publicly perform, sublicense, and distribute the 72 | Work and such Derivative Works in Source or Object form. 73 | 74 | 3. Grant of Patent License. Subject to the terms and conditions of 75 | this License, each Contributor hereby grants to You a perpetual, 76 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 77 | (except as stated in this section) patent license to make, have made, 78 | use, offer to sell, sell, import, and otherwise transfer the Work, 79 | where such license applies only to those patent claims licensable 80 | by such Contributor that are necessarily infringed by their 81 | Contribution(s) alone or by combination of their Contribution(s) 82 | with the Work to which such Contribution(s) was submitted. If You 83 | institute patent litigation against any entity (including a 84 | cross-claim or counterclaim in a lawsuit) alleging that the Work 85 | or a Contribution incorporated within the Work constitutes direct 86 | or contributory patent infringement, then any patent licenses 87 | granted to You under this License for that Work shall terminate 88 | as of the date such litigation is filed. 89 | 90 | 4. Redistribution. You may reproduce and distribute copies of the 91 | Work or Derivative Works thereof in any medium, with or without 92 | modifications, and in Source or Object form, provided that You 93 | meet the following conditions: 94 | 95 | (a) You must give any other recipients of the Work or 96 | Derivative Works a copy of this License; and 97 | 98 | (b) You must cause any modified files to carry prominent notices 99 | stating that You changed the files; and 100 | 101 | (c) You must retain, in the Source form of any Derivative Works 102 | that You distribute, all copyright, patent, trademark, and 103 | attribution notices from the Source form of the Work, 104 | excluding those notices that do not pertain to any part of 105 | the Derivative Works; and 106 | 107 | (d) If the Work includes a "NOTICE" text file as part of its 108 | distribution, then any Derivative Works that You distribute must 109 | include a readable copy of the attribution notices contained 110 | within such NOTICE file, excluding those notices that do not 111 | pertain to any part of the Derivative Works, in at least one 112 | of the following places: within a NOTICE text file distributed 113 | as part of the Derivative Works; within the Source form or 114 | documentation, if provided along with the Derivative Works; or, 115 | within a display generated by the Derivative Works, if and 116 | wherever such third-party notices normally appear. The contents 117 | of the NOTICE file are for informational purposes only and 118 | do not modify the License. You may add Your own attribution 119 | notices within Derivative Works that You distribute, alongside 120 | or as an addendum to the NOTICE text from the Work, provided 121 | that such additional attribution notices cannot be construed 122 | as modifying the License. 123 | 124 | You may add Your own copyright statement to Your modifications and 125 | may provide additional or different license terms and conditions 126 | for use, reproduction, or distribution of Your modifications, or 127 | for any such Derivative Works as a whole, provided Your use, 128 | reproduction, and distribution of the Work otherwise complies with 129 | the conditions stated in this License. 130 | 131 | 5. Submission of Contributions. Unless You explicitly state otherwise, 132 | any Contribution intentionally submitted for inclusion in the Work 133 | by You to the Licensor shall be under the terms and conditions of 134 | this License, without any additional terms or conditions. 135 | Notwithstanding the above, nothing herein shall supersede or modify 136 | the terms of any separate license agreement you may have executed 137 | with Licensor regarding such Contributions. 138 | 139 | 6. Trademarks. This License does not grant permission to use the trade 140 | names, trademarks, service marks, or product names of the Licensor, 141 | except as required for reasonable and customary use in describing the 142 | origin of the Work and reproducing the content of the NOTICE file. 143 | 144 | 7. Disclaimer of Warranty. Unless required by applicable law or 145 | agreed to in writing, Licensor provides the Work (and each 146 | Contributor provides its Contributions) on an "AS IS" BASIS, 147 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 148 | implied, including, without limitation, any warranties or conditions 149 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 150 | PARTICULAR PURPOSE. You are solely responsible for determining the 151 | appropriateness of using or redistributing the Work and assume any 152 | risks associated with Your exercise of permissions under this License. 153 | 154 | 8. Limitation of Liability. In no event and under no legal theory, 155 | whether in tort (including negligence), contract, or otherwise, 156 | unless required by applicable law (such as deliberate and grossly 157 | negligent acts) or agreed to in writing, shall any Contributor be 158 | liable to You for damages, including any direct, indirect, special, 159 | incidental, or consequential damages of any character arising as a 160 | result of this License or out of the use or inability to use the 161 | Work (including but not limited to damages for loss of goodwill, 162 | work stoppage, computer failure or malfunction, or any and all 163 | other commercial damages or losses), even if such Contributor 164 | has been advised of the possibility of such damages. 165 | 166 | 9. Accepting Warranty or Additional Liability. While redistributing 167 | the Work or Derivative Works thereof, You may choose to offer, 168 | and charge a fee for, acceptance of support, warranty, indemnity, 169 | or other liability obligations and/or rights consistent with this 170 | License. However, in accepting such obligations, You may act only 171 | on Your own behalf and on Your sole responsibility, not on behalf 172 | of any other Contributor, and only if You agree to indemnify, 173 | defend, and hold each Contributor harmless for any liability 174 | incurred by, or claims asserted against, such Contributor by reason 175 | of your accepting any such warranty or additional liability. 176 | 177 | END OF TERMS AND CONDITIONS 178 | 179 | APPENDIX: How to apply the Apache License to your work. 180 | 181 | To apply the Apache License to your work, attach the following 182 | boilerplate notice, with the fields enclosed by brackets "[]" 183 | replaced with your own identifying information. (Don't include 184 | the brackets!) The text should be enclosed in the appropriate 185 | comment syntax for the file format. We also recommend that a 186 | file or class name and description of purpose be included on the 187 | same "printed page" as the copyright notice for easier 188 | identification within third-party archives. 189 | 190 | Copyright [yyyy] [name of copyright owner] 191 | 192 | Licensed under the Apache License, Version 2.0 (the "License"); 193 | you may not use this file except in compliance with the License. 194 | You may obtain a copy of the License at 195 | 196 | http://www.apache.org/licenses/LICENSE-2.0 197 | 198 | Unless required by applicable law or agreed to in writing, software 199 | distributed under the License is distributed on an "AS IS" BASIS, 200 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 201 | See the License for the specific language governing permissions and 202 | limitations under the License. 203 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # e-Paper Display driver code for CircuitPython with a fractal demo 2 | 3 | An e-Paper display library for 4 | [CircuitPython](https://github.com/adafruit/circuitpython), 5 | a [MicroPython](https://micropython.org/) variant, 6 | with a demo that draws a Julia or Mandlebrot fractal on the display. 7 | 8 | ![Photo of an m4 displaying a Julia fractal](photos/m4-epaper27-julia.jpg?raw=true "Julia on a 2.7-inch hat") 9 | [as tweeted](https://twitter.com/gpshead/status/1005603935413915648) 10 | 11 | # Hardware and Software Requirements 12 | 13 | This code was written and tested on an Adafruit Metro M4 running CircuitPython 14 | 3.0. The e-Paper display code should work on anything capable of running 15 | CircuitPython. The fractal demo requires floating point; it'll be extremely 16 | slow if you get it running on an M0. 17 | 18 | The e-Paper displays are the monochrome SPI varities from Waveshare. 19 | Six digital I/O pins are required. 20 | 21 | ## Supported e-Paper Displays 22 | 23 | * The monochrome 2.7" Waveshare e-Paper display module 24 | (no hardware partial update support, ~6 seconds to update). 25 | * The monochrome 2.13" and 2.9" Waveshare e-Paper display modules (these have 26 | partial update support and take ~1-2 seconds to update). 27 | * The duochrome 2.13" color Waveshare e-Paper display modules (these take ~20 28 | seconds to update; sorting microscopic pigmented balls is not a fast). 29 | 30 | Based on my reading I *suspect* the interfaces for the above cover all of the 31 | SPI connected Waveshare display types but others have not yet been tested. 32 | 33 | ## Connections 34 | 35 | This library assumes the jumper on the displays remains in 8-bit mode with a 36 | dedicated data/command (DC) pin which is how the ones I purchased were shipped. 37 | 38 | 1. Digital input for Busy. 39 | 1. Digital output for Reset. 40 | 1. Digital output for Chip Select. 41 | 1. Digital output for SPI MOSI. 42 | 1. Digital output for SPI CLK 43 | 1. Digital output for Data / Command. 44 | 45 | By default the `epdif` code uses the hardware SPI bus for SPI MOSI and SPI CLK. 46 | 47 | # Licenses 48 | 49 | Apache 2.0 for top level code. The `third_party/` tree contains code from 50 | other sources which carries its own licenses. In particular the Waveshare code 51 | has a MIT style license on it so the ported code retains that license. 52 | 53 | # Disclaimer 54 | 55 | This is not an official Google product. 56 | -------------------------------------------------------------------------------- /asm_thumb/README.md: -------------------------------------------------------------------------------- 1 | # ARM Assembly CircuitPython Fractal Optimizations 2 | 3 | This package is where the fun continues... when originally writing 4 | the code, I was happy in the "Yay, it works!" sense, but wasn't 5 | satisfied with the speed _and_ wanted to get my hands dirty with 6 | a fancy MicroPython feature to learn some ARM assembly. 7 | 8 | A microcontroller running at 25x the clock speed of my old PCjr 9 | should have no trouble spitting out fractals in the blink of an eye, 10 | made even easier as the M4 has floating point hardware! 11 | Challenge accepted, lets give it a whirl. 12 | 13 | ## Prerequisite: Build a custom CircuitPython (!) 14 | 15 | To use any code in this subdirectory you will need to make a custom 16 | build of [CircuitPython](https://github.com/adafruit/circuitpython) 3 17 | or later and flash it to your device. Edit the 18 | `ports/atmel-samd/mpconfigport.h` file and ensure the following 19 | lines are present. By default, if they exist, they are probably `(0)` 20 | to disable the feature to save space instead of our required `(1)`. 21 | 22 | ```c 23 | #define MICROPY_EMIT_INLINE_THUMB (1) 24 | #define MICROPY_EMIT_INLINE_THUMB_FLOAT (1) 25 | ``` 26 | 27 | With that change saved, build your atmel-samd interpreter following the normal 28 | CircuitPython build instructions and it will be `@micropython.asm_thumb` 29 | enabled. 30 | 31 | # Performance Results 32 | 33 | Using the `third_party.waveshare.epd2in7` display size (176x264) on my Adafruit 34 | Metro M4 Express (120Mhz SAMD51J19) under CircuitPython 3.0 I observed this 35 | Julia fractal computation performance. 36 | 37 | ## Original optimized pure CircuitPython 3.0 code. 38 | 39 | 23.9 seconds 40 | 41 | ## + `_fractal_iterate()` implemented using asm. 42 | 43 | 12.4 seconds 44 | 45 | ## + `MonoBitmap.set_pixel` implemented using asm. 46 | 47 | 7.74 seconds 48 | 49 | ### + Removing the `use_julia` conditional. 50 | 51 | 7.5 seconds 52 | 53 | ## Reimplement the entire `for x in range(width)` loop using asm. 54 | 55 | 0.211 seconds 56 | 57 | ## + Fix the bug in the above code. 58 | 59 | 0.199 seconds - now that we are computing the correct thing. 60 | 61 | ## + Optimize the x-loop assembly code. 62 | 63 | **0.156 seconds**! 64 | 65 | Instructions and memory loads were moved outside of loops and the 14 cycle 66 | `vsqrt` instruction was removed by rethinking our math needs. 67 | 68 | ## + Stop printing a `*` to the serial console every row 69 | 70 | **0.140 seconds** 71 | 72 | A full *170x faster* than the original optimized Python code. 73 | 74 | ## Couldn't we optimize more? 75 | 76 | Yes. The next obvious step would be to move the `for y in range(height)` 77 | loop into assembly. If we weren't already an order of magnitude faster 78 | than the slow e-Paper displays this code was originally a demo for, that 79 | might make sense. For that purpose, we've already outdone ourselves. :) 80 | 81 | ## What about CircuitPython 4.1? 82 | 83 | It is noticably faster due to 4.1 enabling some overlooked micropython compile 84 | time optimizations. The pure CircuitPython 4.1 Julia fractal computation drops 85 | from our previous 23.9 down to 16.3 seconds. The asm version is 0.137 seconds. 86 | We're only *119x faster* now. :P 87 | 88 | # You Made This Look Too Easy a.k.a. How Hard Was This? 89 | 90 | It wasn't easy! I spent numerous multi-hour stretches on random evenings 91 | or weekends in early 2018 weeks hammering this out. I wasn't using git 92 | to track my work at the time, though I saved various snapshots of my progress 93 | which I used to recreate parts of my journey in this repository. 94 | 95 | ## Challenge: Debugging `@micropython.asm_thumb` Code 96 | 97 | It is painful. This code is ultimately native asm, you cannot simply do 98 | the equivalent of `print()` debugging or even try things out in a REPL 99 | as you can with normal Python code. Despite its appearance to fit within 100 | Python syntax, this really is just a weird looking subset of ARM Thumb 101 | assembly. 102 | 103 | In the earlier `_fractal_iterate` version of the code you may have noticed that 104 | I made the `fast_in` array extra large and left some debugging code commented 105 | out. That was from a debugging effort where I had it export some internal 106 | state so I could attempt to reason about what was or wasn't going right 107 | internally. 108 | 109 | ## An Example bug fixed along the way 110 | 111 | Fixed now, but I made commit 0d3efe94c84b84237c1e3193fe5746fa5c2eec7c with a 112 | known issue in my x-loop assembly code. Can you spot it? If so, you must have 113 | the ARM Thumb architecture reference manual memorized. :P It took me a very 114 | long time to debug that one. I only noticed it when I started playing with a 115 | newly acquired epd2in9 display which made me actually look closely at what was 116 | on the screen... One row of pixels at the edge was simply wrong. Look at 117 | the diff to see why. I spent many hours over many days puzzling over that. 118 | 119 | # Future Work 120 | 121 | MicroPython is _supposed_ to support non-assembly optimization features via 122 | `@micropython.native` (similar to Cython) and a Numba like `@micropython.viper` 123 | dynamic compiler. 124 | 125 | I'd love to test out their performance, but I have been unable to get them to 126 | build properly in my circuitpython tree. Viper might do well for the 127 | `MonoBitmap.set_pixel` function, but without support for floats it would not be 128 | of much help for the inner loop complex math iteration. 129 | 130 | It is also possible to write fractal algorithms using only integers. Great for 131 | running on previous generation smaller MCUs without floating point. The 132 | [famous fractint](https://fractint.org) program has been a repository of that 133 | art for decades. If you are really looking to get fractals on a smaller MCU 134 | without floating point, you may find inspiration there. There are also a 135 | people who have made real time [FPGA implementations of 136 | Mandlebrot](https://www.google.com/search?q=fpga+mandlebrot). Who needs a 137 | microcontroller at all? 138 | 139 | # Caveats 140 | 141 | Writing native assembly is instructive (pun intended!) but extremely difficult 142 | to maintain. Do it to learn, but it should always be a last resort. 143 | -------------------------------------------------------------------------------- /asm_thumb/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gpshead/epaper-circuitpython/29555dccd8dde5c592d6c9974423158358b53e34/asm_thumb/__init__.py -------------------------------------------------------------------------------- /asm_thumb/fractal.py: -------------------------------------------------------------------------------- 1 | # python3: CircuitPython 3.0 2 | 3 | # Author: Gregory P. Smith (@gpshead) 4 | # 5 | # Copyright 2018 Google LLC 6 | # 7 | # Licensed under the Apache License, Version 2.0 (the "License"); 8 | # you may not use this file except in compliance with the License. 9 | # You may obtain a copy of the License at 10 | # 11 | # https://www.apache.org/licenses/LICENSE-2.0 12 | # 13 | # Unless required by applicable law or agreed to in writing, software 14 | # distributed under the License is distributed on an "AS IS" BASIS, 15 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 16 | # See the License for the specific language governing permissions and 17 | # limitations under the License. 18 | 19 | """Compute a Mandlebrot or Julia fractal into a bitmap.""" 20 | 21 | import array 22 | import struct 23 | import time 24 | 25 | from . import monobitmap 26 | 27 | 28 | MAX_ITERATIONS = 40 29 | 30 | 31 | # Data must be passed in via an array as micropython.asm_thumb doesn't 32 | # support float arguments or more than three integer register inputs. 33 | # 34 | # Inputs: 35 | # Constant inputs: 36 | # 0 [0x00]. &MonoBitmap.bit_buf (addr/int) 37 | # 1 [0x04]. width (int) 38 | # 2 [0x08]. julia or mandlebrot iteration? (bool true=julia) 39 | # 3 [0x0c]. scale (float) 40 | # 4 [0x10]. center_x (float) 41 | # 5 [0x14]. center_y (float) 42 | # 6 [0x18]. julia_c real (float) 43 | # 7 [0x1c]. julia_c imag (float) 44 | # 8 [0x20]. max_iterations (int) 45 | # Variants: 46 | # - y 47 | # 48 | # r0: &buffer containing the above 32-bit values. 49 | # r1: y 50 | @micropython.asm_thumb 51 | def _xloop_iterate_and_set_pixels(r0, r1): 52 | # scaled_y_j: float = (y*scale - center_y)*1j 53 | push({r8,r9,r10,r11,r12}) 54 | mov(r8, r1) # REG: r8 <- y, free REG r1 55 | vmov(s11, r8) 56 | vcvt_f32_s32(s11, s11) # REG: s11 <- y (float) 57 | ldr(r1, [r0, 0x00]) # address of bit_buf 58 | ldr(r7, [r0, 0x04]) # width 59 | mov(r9, r1) # REG: r9 <- address of bit_buf 60 | ldr(r1, [r0, 0x20]) # MAX_ITERATIONS 61 | mov(r10, r7) # REG: r10 <- width 62 | mov(r11, r1) # REG: r11 <- MAX_ITERATIONS 63 | ldr(r7, [r0, 0x08]) # use_julia 64 | mov(r12, r7) # REG: r12 <- use_julia 65 | vldr(s10, [r0, 0x0c]) # REG: s10 <- scale 66 | vldr(s15, [r0, 0x10]) # REG: s15 <- center_x 67 | vldr(s12, [r0, 0x14]) # REG: s12 <- center_y 68 | vmul(s13, s11, s10) 69 | vsub(s12, s13, s12) # REG: s12 <- scaled_y_j (c_imag) 70 | vldr(s0, [r0, 0x18]) # REG: s0 <- julia_c real 71 | vldr(s1, [r0, 0x1c]) # REG: s1 <- julia_c imag 72 | 73 | # for x in range(width): 74 | mov(r1, 0) # REG: r1 <- x 75 | label(X_RANGE_START) 76 | # c: complex = x*scale - center_x + scaled_y_j 77 | vmov(s13, r1) 78 | vcvt_f32_s32(s13, s13) # REG: s13 <- x 79 | vmul(s14, s13, s10) 80 | vsub(s14, s14, s15) # REG: s14 <- (c_real) 81 | 82 | # if use_julia: 83 | mov(r7, r12) # REG: r7 <- use_julia 84 | cmp(r7, 0) 85 | beq(MANDLEBROT) 86 | label(JULIA) 87 | # v: int = iterate(julia_c, c) # Julia 88 | # iterate c= parameter. 89 | # (done before the loop - s0 and s1 are not clobbered by our code) 90 | #vldr(s0, [r0, 0x18]) # REG: s0 <- julia_c real 91 | #vldr(s1, [r0, 0x1c]) # REG: s1 <- julia_c imag 92 | # iterate z= parameter. 93 | ##vmov(s5, s14) 94 | vmov(r7, s14) 95 | vmov(s5, r7) # REG: s5 <- c_real 96 | ##vmov(s3, s12) 97 | vmov(r7, s12) 98 | vmov(s3, r7) # REG: s3 <- c_imag 99 | b(CALL_ITERATE) 100 | 101 | # else: 102 | label(MANDLEBROT) 103 | # v: int = iterate(c, 0) # Mandlebrot 104 | # iterate c= parameter. 105 | ##vmov(s0, s14) 106 | vmov(r7, s14) 107 | vmov(s0, r7) # REG: s0 <- c_real 108 | ##vmov(s1, s12) 109 | vmov(r7, s12) 110 | vmov(s1, r7) # REG: s1 <- c_imag 111 | # iterate z= parameter. 112 | mov(r7, 0) 113 | vmov(s5, r7) # REG: s5 <- 0 114 | vmov(s3, r7) # REG: s3 <- 0 115 | 116 | label(CALL_ITERATE) 117 | # Input: s0, s1, s5, s3 118 | # Output: r0 119 | #push({r0}) 120 | bl(FRACTAL_ITERATE) 121 | # set_pixel(x, y, v) 122 | # Inputs: r9 (&bit_buf), r10 (width), r1 (x), r2 (value), r8 (y) 123 | # set_pixel v= parameter is the result of iterate from r0 124 | mov(r2, r0) 125 | #pop({r0}) 126 | # set_pixel x= parameters is already in r1 due to the loop. 127 | bl(SET_PIXEL) 128 | 129 | mov(r7, r10) # REG: r7 <- width 130 | add(r1, 1) # x += 1 131 | cmp(r1, r7) # if x < width, repeat the loop 132 | blt(X_RANGE_START) 133 | b(RETURN) 134 | 135 | #### 136 | label(FRACTAL_ITERATE) 137 | mov(r6, r11) # r6 <- MAX_ITERATIONS 138 | # @asm_thumb doesn't do arm float immediates on vmov, 139 | # otherwise we could just write vmov(r4, 4.0). 140 | movt(r4, 0x4080) # r4 <- 32bit floating point 4.0 (2.0^2) 0x40800000 141 | ##mov(r4, 4) # r4 <- 2^2 142 | vmov(s4, r4) # s4 <- 2.0^2 143 | ##vcvt_f32_s32(s4, s4) # s4 = 2.0^2; 144 | # for n in range(MAX_ITERATIONS+1): 145 | mov(r0, 0) # r0 = n; current iteration number 146 | label(FI_LOOP) 147 | vmov(r2, s5) # start with zr real in s2. The loop 148 | vmov(s2, r2) # always begins/ends/cycles with it in s5. 149 | ##vmov(s2, s5) not supported by @arm_thumb 150 | # complex z = z * z + c 151 | vmul(s5, s3, s3) # zj^2 152 | vmul(s6, s2, s2) # zr^2 153 | vsub(s5, s6, s5) # z = z*z real part (zr^2 - zj^2) [into a NEW reg] 154 | ##vmls(s5, s6, s5) not supported by @arm_thumb 155 | vmul(s6, s2, s3) 156 | vadd(s3, s6, s6) # z = z*z imaginary part (zr*zj + zr*zj) 157 | # REG: s5 is the new zr real 158 | # REG: s3 is (still) zj imaginary 159 | vadd(s5, s5, s0) # zr += cr real part 160 | vadd(s3, s3, s1) # zj += cj imaginary part 161 | # complex abs(z) 162 | vmul(s6, s5, s5) # zr^2 163 | vmul(s7, s3, s3) # zj^2 164 | vadd(s6, s6, s7) # s6 <- zr^2 + zj^2 165 | # sqrt(A) < B is the same as A < B^2 for non-negative A and B. 166 | # This avoids a 14-cycle vsqrt instruction in our innermost loop! :) 167 | ##vsqrt(s6, s6) # sqrt(zr^2 + zj^2) 168 | # if abs(z) > 2 169 | vcmp(s6, s4) 170 | vmrs(APSR_nzcv, FPSCR) 171 | bgt(FI_END) # return n 172 | add(r0, 1) 173 | cmp(r0, r6) 174 | ble(FI_LOOP) 175 | mov(r0, 1) # return -1 176 | neg(r0, r0) 177 | label(FI_END) # return value is already in r0 178 | bx(lr) 179 | #### end FRACTAL_ITERATE() 180 | 181 | #### 182 | label(SET_PIXEL) 183 | mov(r3, r9) # r3 <- address of bit_buf 184 | mov(r5, r10) # r5 <- width 185 | mov(r6, r8) # r6 <- y 186 | mul(r5, r6) # r5 <- width * y 187 | add(r4, r1, r5) # r4 <- binary_idx (width * y + x) 188 | #mla(r4, r5, r6, r1) @asm_thumb doesn't support mla? 189 | mov(r5, r4) # r5 <- binary_idx 190 | mov(r7, 3) 191 | lsr(r4, r7) # r4 <- byte_idx 192 | mov(r7, 7) 193 | and_(r5, r7) 194 | sub(r5, r7, r5) # r5 <- bit_no 195 | mov(r6, 1) 196 | lsl(r6, r5) # r6 <- (1 << bit_no) 197 | add(r3, r3, r4) # r3 <- &bit_buf[byte_idx] 198 | ldrb(r4, [r3, 0]) # r4 <- bit_buf[byte_idx] 199 | mov(r7, 1) 200 | and_(r2, r7) # r2 <- value &= 1 (sets branch flags) 201 | beq(SPF_CLEAR_BIT) 202 | orr(r4, r6) # r4 |= (1 << bit_no) 203 | b(SPF_STORE) 204 | label(SPF_CLEAR_BIT) 205 | bic(r4, r6) # r4 &= ~(1 << bit_no) # Nice instruction! 206 | label(SPF_STORE) 207 | strb(r4, [r3, 0]) 208 | bx(lr) 209 | #### end SET_PIXEL 210 | 211 | label(RETURN) 212 | pop({r8,r9,r10,r11,r12}) 213 | 214 | 215 | # Created based on looking up how others have written Mandlebrot and Julia 216 | # fractal computations in Python. Well known algorithms. Python's 217 | # built-in complex number support makes them easy to express in code. 218 | # https://en.wikipedia.org/wiki/Mandelbrot_set#Computer_drawings 219 | # https://en.wikipedia.org/wiki/Julia_set#Pseudocode 220 | # https://github.com/ActiveState/code/blob/master/recipes/Python/579143_Mandelbrot_Set_made_simple/recipe-579143.py 221 | # https://github.com/ActiveState/code/blob/master/recipes/Python/577120_Julia_fractals/recipe-577120.py 222 | # http://0pointer.de/blog/projects/mandelbrot.html 223 | def get_fractal(width: int, height: int, use_julia: bool = True, 224 | max_iterations: int = MAX_ITERATIONS) -> monobitmap.MonoBitmap: 225 | scale = 1/(width/1.5) 226 | if use_julia: 227 | center_x, center_y = 1.15, 1.6 # Julia 228 | else: 229 | center_x, center_y = 2.2, 1.5 # Mandlebrot 230 | 231 | fractal = monobitmap.MonoBitmap(width, height) 232 | julia_c = 0.3+0.6j # Only load the complex constant once. 233 | 234 | xloop_params = array.array('i', (0,)*9) 235 | monobitmap.store_addr(xloop_params, fractal.bit_buf) 236 | xloop_params[1] = width 237 | xloop_params[2] = use_julia # True: Julia, False: Mandlebrot 238 | struct.pack_into('fffff', xloop_params, 0x0c, 239 | scale, center_x, center_y, 240 | julia_c.real, julia_c.imag) 241 | xloop_params[8] = max_iterations 242 | 243 | # Make these local 244 | compute_row_and_set_pixels = _xloop_iterate_and_set_pixels 245 | monotonic = time.monotonic 246 | 247 | start_time = monotonic() 248 | for y in range(height): 249 | compute_row_and_set_pixels(xloop_params, y) 250 | end_time = monotonic() 251 | print('Computation took', end_time - start_time, 'seconds.') 252 | return fractal 253 | 254 | -------------------------------------------------------------------------------- /asm_thumb/monobitmap.py: -------------------------------------------------------------------------------- 1 | # python3: CircuitPython 3.0 2 | 3 | # Author: Gregory P. Smith (@gpshead) 4 | # 5 | # Copyright 2018 Google LLC 6 | # 7 | # Licensed under the Apache License, Version 2.0 (the "License"); 8 | # you may not use this file except in compliance with the License. 9 | # You may obtain a copy of the License at 10 | # 11 | # https://www.apache.org/licenses/LICENSE-2.0 12 | # 13 | # Unless required by applicable law or agreed to in writing, software 14 | # distributed under the License is distributed on an "AS IS" BASIS, 15 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 16 | # See the License for the specific language governing permissions and 17 | # limitations under the License. 18 | 19 | """A monochrome bitmap to represent display frame buffers.""" 20 | 21 | import array 22 | 23 | 24 | class MonoBitmap: 25 | """A monochrome bitmap stored compactly. 26 | 27 | Attributes: 28 | bit_buf: The raw bitmap buffer bytearray. Do not resize! 29 | fast_in: Input array to pass to set_pixel_fast. Do not modify. 30 | width: The width. Do not modify. 31 | height: The height. Do not modify. 32 | 33 | Usage: 34 | hidden_cat = MonoBitmap(204, 112) 35 | hidden_cat.set_pixel(42, 23, 0) 36 | epd.display_frame_buf(hidden_cat.bit_buf) 37 | """ 38 | def __init__(self, width, height): 39 | self.width = width 40 | self.height = height 41 | self._bit_buf = bytearray(width * height // 8) 42 | 43 | # (&bit_buf, width, y) 44 | self.fast_in = array.array('i', [-1, width, 0]) 45 | store_addr(self.fast_in, self._bit_buf) 46 | 47 | @property 48 | def bit_buf(self): 49 | return self._bit_buf 50 | 51 | def set_pixel(self, x: int, y: int, value: bool) -> None: 52 | #assert self.width > x >= 0 53 | #assert self.height > y >= 0 54 | binary_idx = self.width * y + x 55 | #byte_idx, bit_no = divmod(binary_idx, 8) 56 | #bit_no = 7 - bit_no # The EPD's bit order. 57 | byte_idx = binary_idx >> 3 58 | bit_no = 7 - (binary_idx & 7) 59 | if value: 60 | self.bit_buf[byte_idx] |= (1 << bit_no) 61 | else: 62 | self.bit_buf[byte_idx] &= ~(1 << bit_no) 63 | 64 | # r0 must be the self.fast_in array (&bit_buf, width, y) 65 | # r1 is the x coordinate 66 | # r2 is the value to set for the pixel 67 | # No bounds checking is performed! 68 | @staticmethod 69 | @micropython.asm_thumb 70 | def set_pixel_fast(r0, r1, r2): 71 | # r0 <- inputs array 72 | # r1 <- x 73 | # r2 <- value 74 | ldr(r3, [r0, 0]) # r3 <- address of bit_buf 75 | ldr(r5, [r0, 4]) # r5 <- width 76 | ldr(r6, [r0, 8]) # r6 <- y 77 | mul(r5, r6) # r5 <- width * y 78 | add(r4, r1, r5) # r4 <- binary_idx (width * y + x) 79 | mov(r5, r4) # r5 <- binary_idx 80 | mov(r7, 3) 81 | lsr(r4, r7) # r4 <- byte_idx 82 | mov(r7, 7) 83 | and_(r5, r7) 84 | sub(r5, r7, r5) # r5 <- bit_no 85 | mov(r6, 1) 86 | lsl(r6, r5) # r6 <- (1 << bit_no) 87 | add(r3, r3, r4) # r1 <- &bit_buf[byte_idx] 88 | ldrb(r4, [r3, 0]) # r4 <- bit_buf[byte_idx] 89 | mov(r7, 1) 90 | and_(r2, r7) # r2 <- value &= 1 (sets the eq zero flag) 91 | beq(CLEAR_BIT) 92 | orr(r4, r6) # r4 |= (1 << bit_no) 93 | b(STORE) 94 | label(CLEAR_BIT) 95 | bic(r4, r6) # r4 &= ~(1 << bit_no) # Nice instruction! 96 | label(STORE) 97 | strb(r4, [r3, 0]) 98 | 99 | 100 | # This utility function really belongs somewhere else. 101 | @micropython.asm_thumb 102 | def store_addr(r0, r1): 103 | """Store r1 into r0[0]. Good for getting &thing into a buffer.""" 104 | str(r1, [r0, 0]) 105 | -------------------------------------------------------------------------------- /fractal.py: -------------------------------------------------------------------------------- 1 | # python3: CircuitPython 3.0 2 | 3 | # Author: Gregory P. Smith (@gpshead) 4 | # 5 | # Copyright 2018 Google LLC 6 | # 7 | # Licensed under the Apache License, Version 2.0 (the "License"); 8 | # you may not use this file except in compliance with the License. 9 | # You may obtain a copy of the License at 10 | # 11 | # https://www.apache.org/licenses/LICENSE-2.0 12 | # 13 | # Unless required by applicable law or agreed to in writing, software 14 | # distributed under the License is distributed on an "AS IS" BASIS, 15 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 16 | # See the License for the specific language governing permissions and 17 | # limitations under the License. 18 | 19 | """Compute a Mandlebrot or Julia fractal into a bitmap.""" 20 | 21 | import time 22 | 23 | import monobitmap 24 | 25 | 26 | MAX_ITERATIONS = 40 27 | 28 | 29 | # Avoiding the builtin abs lookup in the loop is ~10% faster. 30 | # Avoiding the builtin range lookup gains another ~3%. 31 | def _fractal_iterate(c, z=0, max_iter1=MAX_ITERATIONS+1, 32 | _abs=abs, _range=range) -> int: 33 | for n in _range(max_iter1): 34 | z = z * z + c 35 | if _abs(z) > 2: 36 | return n 37 | return -1 38 | 39 | 40 | # Created based on looking up how others have written Mandlebrot and Julia 41 | # fractal computations in Python. Well known algorithms. Python's 42 | # built-in complex number support makes them easy to express in code. 43 | # https://en.wikipedia.org/wiki/Mandelbrot_set#Computer_drawings 44 | # https://en.wikipedia.org/wiki/Julia_set#Pseudocode 45 | # https://github.com/ActiveState/code/blob/master/recipes/Python/579143_Mandelbrot_Set_made_simple/recipe-579143.py 46 | # https://github.com/ActiveState/code/blob/master/recipes/Python/577120_Julia_fractals/recipe-577120.py 47 | # http://0pointer.de/blog/projects/mandelbrot.html 48 | def get_fractal(width, height, use_julia=True, max_iterations=MAX_ITERATIONS): 49 | scale = 1/(width/1.5) 50 | if use_julia: 51 | center_x, center_y = 1.15, 1.6 # Julia 52 | else: 53 | center_x, center_y = 2.2, 1.5 # Mandlebrot 54 | 55 | fractal = monobitmap.MonoBitmap(width, height) 56 | set_pixel = fractal.set_pixel # faster name lookup 57 | iterate = _fractal_iterate # faster name lookup 58 | julia_c = 0.3+0.6j # Only load the complex constant once. 59 | max_iterations += 1 60 | 61 | start_time = time.monotonic() 62 | for y in range(height): 63 | scaled_y_j_m_cx = (y*scale - center_y)*1j - center_x 64 | for x in range(width): 65 | c = x*scale + scaled_y_j_m_cx 66 | if use_julia: 67 | n = iterate(julia_c, c, max_iter1=max_iterations) # Julia 68 | else: 69 | n = iterate(c, max_iter1=max_iterations) # Mandlebrot 70 | 71 | set_pixel(x, y, n & 1) 72 | 73 | print('*', end='') 74 | print() 75 | print('Computation took', time.monotonic() - start_time, 'seconds.') 76 | return fractal 77 | 78 | -------------------------------------------------------------------------------- /main.py: -------------------------------------------------------------------------------- 1 | # python3: CircuitPython 3.0 2 | 3 | # Author: Gregory P. Smith (@gpshead) 4 | # 5 | # Copyright 2018 Google LLC 6 | # 7 | # Licensed under the Apache License, Version 2.0 (the "License"); 8 | # you may not use this file except in compliance with the License. 9 | # You may obtain a copy of the License at 10 | # 11 | # https://www.apache.org/licenses/LICENSE-2.0 12 | # 13 | # Unless required by applicable law or agreed to in writing, software 14 | # distributed under the License is distributed on an "AS IS" BASIS, 15 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 16 | # See the License for the specific language governing permissions and 17 | # limitations under the License. 18 | 19 | import board 20 | import digitalio 21 | import random 22 | import time 23 | 24 | #from third_party.waveshare import color_epd2in13 as connected_epd 25 | from third_party.waveshare import epd2in7 as connected_epd 26 | #from third_party.waveshare import epd2in9 as connected_epd 27 | #from third_party.waveshare import epd2in13 as connected_epd 28 | 29 | try: 30 | # Optimized version - requires a custom CircuitPython build. 31 | from asm_thumb import fractal 32 | from asm_thumb import monobitmap 33 | HAVE_ASM = True 34 | except (ImportError, SyntaxError): 35 | import fractal 36 | import monobitmap 37 | HAVE_ASM = False 38 | 39 | 40 | def sample_keys(): 41 | """Yields four bools representing the button press state on the 2.7" EPD. 42 | 43 | The 2.7 inch EPD rpi hat has four push buttons connected to RPI hat pins. 44 | 45 | Follow the traces to see which pins those are and wire them up to digital 46 | inputs D2 through D5 on your CircuitPython device. A button press grounds 47 | the pin. 48 | 49 | This function samples them all and yields four bools for each button. 50 | """ 51 | DigitalInOut = digitalio.DigitalInOut 52 | Pull = digitalio.Pull 53 | with DigitalInOut(board.D2) as key1, DigitalInOut(board.D3) as key2, \ 54 | DigitalInOut(board.D4) as key3, DigitalInOut(board.D5) as key4: 55 | key1.switch_to_input(Pull.UP) 56 | key2.switch_to_input(Pull.UP) 57 | key3.switch_to_input(Pull.UP) 58 | key4.switch_to_input(Pull.UP) 59 | for k in (key1, key2, key3, key4): 60 | yield not k.value # False is pressed 61 | 62 | 63 | class StatusLED: 64 | """A simple interface to the onboard pixel.""" 65 | 66 | def __init__(self): 67 | if hasattr(board, 'NEOPIXEL'): 68 | import neopixel 69 | self._led = neopixel.NeoPixel(board.NEOPIXEL, 1) 70 | self._led.brightness = 1/16 71 | elif hasattr(board, 'APA102_MOSI'): 72 | import adafruit_dotstar 73 | self._led = adafruit_dotstar.DotStar( 74 | board.APA102_SCK, board.APA102_MOSI, 1) 75 | self._led.brightness = 0.7 76 | else: 77 | self._led = [None] 78 | 79 | def off(self): 80 | self._led[0] = b'\0\0\0' 81 | 82 | def busy(self): 83 | self._led[0] = b'\x50\x10\0' 84 | 85 | def ready(self): 86 | self._led[0] = b'\x10\x50\0' if HAVE_ASM else b'\x10\0\x70' 87 | 88 | 89 | def main(): 90 | led = StatusLED() 91 | led.busy() 92 | 93 | epd = connected_epd.EPD() 94 | print("Initializing display...") 95 | epd.init() 96 | if HAVE_ASM: 97 | print("@micropython.asm_thumb implementation loaded.") 98 | else: 99 | print("Pure Python implementation loaded.") 100 | 101 | keys = [1,] # Print message on start. 102 | while True: 103 | if any(keys): 104 | led.ready() 105 | print("Awaiting key1-key4 button press.") 106 | keys = list(sample_keys()) 107 | if any(keys): 108 | led.busy() 109 | 110 | if keys[0]: 111 | print("Computing Mandlebrot fractal.") 112 | fractal_image = fractal.get_fractal(epd.width, epd.height, 113 | use_julia=False) 114 | print("Displaying.") 115 | if getattr(epd, 'colors', 2) > 2: 116 | epd.display_frames(None, fractal_image.bit_buf) 117 | else: 118 | epd.display_bitmap(fractal_image, fast_ghosting=True) 119 | del fractal_image 120 | elif keys[1]: 121 | print("Setting display to white.") 122 | epd.clear_frame_memory(0xff) 123 | epd.display_frame() 124 | elif keys[2]: 125 | raw_framebuf = bytearray(epd.fb_bytes) 126 | for pos in range(len(raw_framebuf)): 127 | raw_framebuf[pos] = random.randint(0, 255) 128 | print("Displaying random framebuf.") 129 | if getattr(epd, 'colors', 2) <= 2: 130 | epd.display_frame_buf(raw_framebuf, fast_ghosting=True) 131 | else: 132 | raw_framebuf2 = bytearray(epd.fb_bytes) 133 | for pos in range(len(raw_framebuf2)): 134 | raw_framebuf2[pos] = random.randint(0, 255) 135 | epd.display_frames(raw_framebuf, raw_framebuf2) 136 | del raw_framebuf2 137 | del raw_framebuf 138 | elif keys[3]: 139 | print("Computing Julia fractal.") 140 | fractal_image = fractal.get_fractal(epd.width, epd.height, 141 | use_julia=True) 142 | print("Displaying.") 143 | if getattr(epd, 'colors', 2) > 2: 144 | epd.display_frames(fractal_image.bit_buf, None) 145 | else: 146 | epd.display_bitmap(fractal_image, fast_ghosting=True) 147 | del fractal_image 148 | else: 149 | # This effectively debounces the keys, we merely sample them 150 | # rather than treat a state change as an interrupt. If a key 151 | # is not pressed for this duration it may not be noticed. 152 | time.sleep(0.05) # short time between polling. 153 | 154 | print("Done.") 155 | 156 | 157 | if __name__ == '__main__': 158 | main() 159 | -------------------------------------------------------------------------------- /monobitmap.py: -------------------------------------------------------------------------------- 1 | # python3: CircuitPython 3.0 2 | 3 | # Author: Gregory P. Smith (@gpshead) 4 | # 5 | # Copyright 2018 Google LLC 6 | # 7 | # Licensed under the Apache License, Version 2.0 (the "License"); 8 | # you may not use this file except in compliance with the License. 9 | # You may obtain a copy of the License at 10 | # 11 | # https://www.apache.org/licenses/LICENSE-2.0 12 | # 13 | # Unless required by applicable law or agreed to in writing, software 14 | # distributed under the License is distributed on an "AS IS" BASIS, 15 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 16 | # See the License for the specific language governing permissions and 17 | # limitations under the License. 18 | 19 | """A monochrome bitmap to represent display frame buffers.""" 20 | 21 | 22 | class MonoBitmap: 23 | """A monochrome bitmap stored compactly. 24 | 25 | Attributes: 26 | bit_buf: The raw bitmap buffer bytearray. Do not resize! 27 | width: The width. Do not modify. 28 | height: The height. Do not modify. 29 | 30 | Usage: 31 | hidden_cat = MonoBitmap(204, 112) 32 | hidden_cat.set_pixel(42, 23, 0) 33 | epd.display_frame_buf(hidden_cat.bit_buf) 34 | """ 35 | def __init__(self, width, height): 36 | self.width = width 37 | self.height = height 38 | self.bit_buf = bytearray(width * height // 8) 39 | 40 | def set_pixel(self, x: int, y: int, value: bool) -> None: 41 | #assert self.width > x >= 0 42 | #assert self.height > y >= 0 43 | binary_idx = self.width * y + x 44 | #byte_idx, bit_no = divmod(binary_idx, 8) 45 | #bit_no = 7 - bit_no # The EPD's bit order. 46 | byte_idx = binary_idx >> 3 47 | bit_no = 7 - (binary_idx & 7) 48 | if value: 49 | self.bit_buf[byte_idx] |= (1 << bit_no) 50 | else: 51 | self.bit_buf[byte_idx] &= ~(1 << bit_no) 52 | -------------------------------------------------------------------------------- /photos/m4-epaper27-julia.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gpshead/epaper-circuitpython/29555dccd8dde5c592d6c9974423158358b53e34/photos/m4-epaper27-julia.jpg -------------------------------------------------------------------------------- /third_party/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gpshead/epaper-circuitpython/29555dccd8dde5c592d6c9974423158358b53e34/third_party/__init__.py -------------------------------------------------------------------------------- /third_party/waveshare/LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (C) Waveshare July 4 2017 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documnetation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in 11 | all copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS OR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | THE SOFTWARE. 20 | 21 | ****************************************************************************** 22 | * 23 | * Copyright (c) 2017 Waveshare 24 | * All rights reserved. 25 | * 26 | * THIS SOFTWARE IS PROVIDED BY COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 27 | * AND ANY EXPRESS, IMPLIED OR STATUTORY WARRANTIES, INCLUDING, BUT NOT 28 | * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY, FITNESS FOR A 29 | * PARTICULAR PURPOSE AND NON-INFRINGEMENT OF THIRD PARTY INTELLECTUAL PROPERTY 30 | * RIGHTS ARE DISCLAIMED TO THE FULLEST EXTENT PERMITTED BY LAW. IN NO EVENT 31 | * SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, 32 | * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT 33 | * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, 34 | * OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF 35 | * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING 36 | * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, 37 | * EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 38 | * 39 | ****************************************************************************** 40 | -------------------------------------------------------------------------------- /third_party/waveshare/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gpshead/epaper-circuitpython/29555dccd8dde5c592d6c9974423158358b53e34/third_party/waveshare/__init__.py -------------------------------------------------------------------------------- /third_party/waveshare/color_epd1in54.py: -------------------------------------------------------------------------------- 1 | # python3: CircuitPython 2 | 3 | # Author: Gregory P. Smith (@gpshead) 4 | # 5 | # Copyright 2019 Google LLC 6 | # 7 | # Licensed under the Apache License, Version 2.0 (the "License"); 8 | # you may not use this file except in compliance with the License. 9 | # You may obtain a copy of the License at 10 | # 11 | # https://www.apache.org/licenses/LICENSE-2.0 12 | # 13 | # Unless required by applicable law or agreed to in writing, software 14 | # distributed under the License is distributed on an "AS IS" BASIS, 15 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 16 | # See the License for the specific language governing permissions and 17 | # limitations under the License. 18 | 19 | from third_party.waveshare import color_epd2in13 20 | 21 | class EPD(color_epd2in13.EPD): 22 | width = 152 23 | height = 252 24 | -------------------------------------------------------------------------------- /third_party/waveshare/color_epd2in13.py: -------------------------------------------------------------------------------- 1 | # Ported to CircuitPython by Gregory P. Smith 2 | 3 | ## 4 | # @filename : epd2in13b.py 5 | # @brief : Implements for Dual-color e-paper library 6 | # @author : Yehui from Waveshare 7 | # 8 | # Copyright (C) Waveshare August 15 2017 9 | # 10 | # Permission is hereby granted, free of charge, to any person obtaining a copy 11 | # of this software and associated documnetation files (the "Software"), to deal 12 | # in the Software without restriction, including without limitation the rights 13 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 14 | # copies of the Software, and to permit persons to whom the Software is 15 | # furished to do so, subject to the following conditions: 16 | # 17 | # The above copyright notice and this permission notice shall be included in 18 | # all copies or substantial portions of the Software. 19 | # 20 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 21 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 22 | # FITNESS OR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 23 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 24 | # LIABILITY WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 25 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 26 | # THE SOFTWARE. 27 | # 28 | 29 | import time 30 | 31 | from . import epdif 32 | 33 | # EPD2IN13B commands 34 | PANEL_SETTING = 0x00 35 | POWER_SETTING = 0x01 36 | POWER_OFF = 0x02 37 | POWER_OFF_SEQUENCE_SETTING = 0x03 38 | POWER_ON = 0x04 39 | POWER_ON_MEASURE = 0x05 40 | BOOSTER_SOFT_START = 0x06 41 | DEEP_SLEEP = 0x07 42 | DATA_START_TRANSMISSION_1 = 0x10 # B&W data 43 | DATA_STOP = 0x11 44 | DISPLAY_REFRESH = 0x12 45 | DATA_START_TRANSMISSION_2 = 0x13 # tinted data 46 | VCOM_LUT = 0x20 47 | W2W_LUT = 0x21 48 | B2W_LUT = 0x22 49 | W2B_LUT = 0x23 50 | B2B_LUT = 0x24 51 | PLL_CONTROL = 0x30 52 | TEMPERATURE_SENSOR_CALIBRATION = 0x40 53 | TEMPERATURE_SENSOR_SELECTION = 0x41 54 | TEMPERATURE_SENSOR_WRITE = 0x42 55 | TEMPERATURE_SENSOR_READ = 0x43 56 | VCOM_AND_DATA_INTERVAL_SETTING = 0x50 57 | LOW_POWER_DETECTION = 0x51 58 | TCON_SETTING = 0x60 59 | RESOLUTION_SETTING = 0x61 60 | GET_STATUS = 0x71 61 | AUTO_MEASURE_VCOM = 0x80 62 | VCOM_VALUE = 0x81 63 | VCM_DC_SETTING = 0x82 64 | PARTIAL_WINDOW = 0x90 65 | PARTIAL_IN = 0x91 66 | PARTIAL_OUT = 0x92 67 | PROGRAM_MODE = 0xA0 68 | ACTIVE_PROGRAM = 0xA1 69 | READ_OTP_DATA = 0xA2 70 | POWER_SAVING = 0xE3 71 | 72 | class EPD: 73 | width = 104 74 | height = 212 75 | colors = 3 76 | 77 | def __init__(self): 78 | self.reset_pin = epdif.RST_PIN 79 | self.dc_pin = epdif.DC_PIN 80 | self.busy_pin = epdif.BUSY_PIN 81 | 82 | def _delay_ms(self, ms): 83 | time.sleep(ms / 1000.) 84 | 85 | def _send_command(self, command): 86 | self.dc_pin.value = 0 87 | epdif.spi_transfer(command.to_bytes(1, 'big')) 88 | 89 | def _send_data(self, data): 90 | self.dc_pin.value = 1 91 | if isinstance(data, int): 92 | epdif.spi_transfer(data.to_bytes(1, 'big')) 93 | else: 94 | # The EPD needs CS to cycle hi between every byte so we loop doing 95 | # one byte transfers to cause that. Not efficient, but it makes 96 | # it work. Data sheets say needs to be at least a 60ns CS pulse. 97 | for i in range(len(data)): 98 | epdif.spi_transfer(data[i:i+1]) 99 | 100 | @property 101 | def fb_bytes(self): 102 | return self.width * self.height // 8 103 | 104 | def init(self): 105 | try: 106 | epdif.epd_io_bus_init() 107 | except RuntimeError: 108 | pass # It avoids global io bus reinitialization. Good. 109 | self.reset_pin = epdif.RST_PIN 110 | self.dc_pin = epdif.DC_PIN 111 | self.busy_pin = epdif.BUSY_PIN 112 | self.reset() 113 | self._send_command(POWER_SETTING) # Adafruit EPD 114 | self._send_data(b'\x03\x00\x2b\x2b\x09') # Adafruit EPD 115 | self._send_command(BOOSTER_SOFT_START) 116 | self._send_data(b'\x17\x17\x17') 117 | self._send_command(POWER_ON) 118 | self.wait_until_idle() 119 | # Q: what does this setting do? 120 | self._send_command(PANEL_SETTING) 121 | self._send_data(0xCF) # Adafruit EPD 122 | #self._send_data(0x8F) 123 | self._send_command(VCOM_AND_DATA_INTERVAL_SETTING) 124 | self._send_data(0x37) 125 | self._send_command(PLL_CONTROL) # Adafruit EPD 126 | self._send_data(0x29) # Adafruit EPD 127 | self._send_command(RESOLUTION_SETTING) 128 | self._send_data(self.height.to_bytes(2, 'little')) 129 | self._send_data(self.width.to_bytes(2, 'little')) 130 | self._send_command(VCM_DC_SETTING) # Adafruit EPD 131 | self._send_data(0x0A) # Adafruit EPD 132 | 133 | def wait_until_idle(self): 134 | while self.busy_pin.value == 1: # 0: idle, 1: busy 135 | self._delay_ms(10) 136 | 137 | def reset(self): 138 | self.reset_pin.value = 0 # module reset 139 | self._delay_ms(200) 140 | self.reset_pin.value = 1 141 | self._delay_ms(200) 142 | 143 | def clear_frame_memory(self, pattern: int, tint_pattern: int = -1): 144 | self._send_command(DATA_START_TRANSMISSION_1) 145 | self._delay_ms(2) 146 | row = pattern.to_bytes(1, 'big') * ((self.width + 7) // 8) 147 | for j in range(self.height): 148 | self._send_data(row) 149 | if tint_pattern < 0: 150 | tint_pattern = pattern 151 | self._delay_ms(2) 152 | self._send_command(DATA_START_TRANSMISSION_2) 153 | self._delay_ms(2) 154 | row = tint_pattern.to_bytes(1, 'big') * ((self.width + 7) // 8) 155 | for j in range(self.height): 156 | self._send_data(row) 157 | self._delay_ms(2) 158 | 159 | # TODO An API that takes a two bits per pixel image input would be good. 160 | 161 | def display_frame(self): 162 | self.display_frames(None, None) 163 | 164 | def display_frames(self, frame_buffer_black, frame_buffer_red): 165 | # TODO: assert buffer lengths and _send_data all of it w/o loop? 166 | if frame_buffer_black: 167 | self._send_command(DATA_START_TRANSMISSION_1) 168 | self._delay_ms(2) 169 | for i in range(0, self.width * self.height / 8): 170 | self._send_data(frame_buffer_black[i]) 171 | self._delay_ms(2) 172 | if frame_buffer_red: 173 | self._send_command(DATA_START_TRANSMISSION_2) 174 | self._delay_ms(2) 175 | for i in range(0, self.width * self.height / 8): 176 | self._send_data(frame_buffer_red[i]) 177 | self._delay_ms(2) 178 | 179 | # Observation: On some EPDs a display refresh won't do anything 180 | # unless two buffers have been written. 181 | self._send_command(DISPLAY_REFRESH) 182 | self.wait_until_idle() 183 | 184 | # after this, call epd.init() to awaken the module 185 | def sleep(self): 186 | self._send_command(VCOM_AND_DATA_INTERVAL_SETTING) 187 | self._send_data(0x37) 188 | self._send_command(VCM_DC_SETTING_REGISTER) #to solve Vcom drop 189 | self._send_data(0x00) 190 | self._send_command(POWER_SETTING) #power setting 191 | self._send_data(0x02) #gate switch to external 192 | self._send_data(b'\x00\x00\x00') 193 | self._wait_until_idle() 194 | self._send_command(POWER_OFF) #power off 195 | 196 | # TODO: pixel and drawing functions should be put in a shared class. 197 | # such as MonoBitmap... the only actual relevance to us is set_absolute_pixel 198 | # which describes how the bits are set per byte and that a color pixel is 199 | # a 0 bit (1 transparent) and a black pixel is a 1 bit (0 white) in their 200 | # respective frame buffers. the color buffer is added after the b&w is drawn. 201 | 202 | def _set_absolute_pixel(self, frame_buffer, x: int, y: int, tinted: bool): 203 | """Set a pixel black or tinted in a bytearray frame buffer.""" 204 | if (x < 0 or x >= self.width or y < 0 or y >= self.height): 205 | raise ValueError('x %d, y %d out of range' % (x, y)) 206 | if tinted: 207 | frame_buffer[(x + y * self.width) / 8] &= ~(0x80 >> (x % 8)) 208 | else: 209 | frame_buffer[(x + y * self.width) / 8] |= 0x80 >> (x % 8) 210 | 211 | set_pixel = _set_absolute_pixel # I got rid of the rotation code. 212 | 213 | # TODO(gps): left off porting here. 214 | 215 | def draw_line(self, frame_buffer, x0, y0, x1, y1, tinted): 216 | # Bresenham algorithm 217 | dx = abs(x1 - x0) 218 | sx = 1 if x0 < x1 else -1 219 | dy = -abs(y1 - y0) 220 | sy = 1 if y0 < y1 else -1 221 | err = dx + dy 222 | while((x0 != x1) and (y0 != y1)): 223 | self.set_pixel(frame_buffer, x0, y0 , tinted) 224 | if (2 * err >= dy): 225 | err += dy 226 | x0 += sx 227 | if (2 * err <= dx): 228 | err += dx 229 | y0 += sy 230 | 231 | def draw_horizontal_line(self, frame_buffer, x, y, width, tinted): 232 | for i in range(x, x + width): 233 | self.set_pixel(frame_buffer, i, y, tinted) 234 | 235 | def draw_vertical_line(self, frame_buffer, x, y, height, tinted): 236 | for i in range(y, y + height): 237 | self.set_pixel(frame_buffer, x, i, tinted) 238 | 239 | def draw_rectangle(self, frame_buffer, x0, y0, x1, y1, tinted): 240 | min_x = x0 if x1 > x0 else x1 241 | max_x = x1 if x1 > x0 else x0 242 | min_y = y0 if y1 > y0 else y1 243 | max_y = y1 if y1 > y0 else y0 244 | self.draw_horizontal_line(frame_buffer, min_x, min_y, max_x - min_x + 1, tinted) 245 | self.draw_horizontal_line(frame_buffer, min_x, max_y, max_x - min_x + 1, tinted) 246 | self.draw_vertical_line(frame_buffer, min_x, min_y, max_y - min_y + 1, tinted) 247 | self.draw_vertical_line(frame_buffer, max_x, min_y, max_y - min_y + 1, tinted) 248 | 249 | def draw_filled_rectangle(self, frame_buffer, x0, y0, x1, y1, tinted): 250 | min_x = x0 if x1 > x0 else x1 251 | max_x = x1 if x1 > x0 else x0 252 | min_y = y0 if y1 > y0 else y1 253 | max_y = y1 if y1 > y0 else y0 254 | for i in range(min_x, max_x + 1): 255 | self.draw_vertical_line(frame_buffer, i, min_y, max_y - min_y + 1, tinted) 256 | 257 | def draw_circle(self, frame_buffer, x, y, radius, tinted): 258 | # Bresenham algorithm 259 | x_pos = -radius 260 | y_pos = 0 261 | err = 2 - 2 * radius 262 | if (x >= self.width or y >= self.height): 263 | return 264 | while True: 265 | self.set_pixel(frame_buffer, x - x_pos, y + y_pos, tinted) 266 | self.set_pixel(frame_buffer, x + x_pos, y + y_pos, tinted) 267 | self.set_pixel(frame_buffer, x + x_pos, y - y_pos, tinted) 268 | self.set_pixel(frame_buffer, x - x_pos, y - y_pos, tinted) 269 | e2 = err 270 | if (e2 <= y_pos): 271 | y_pos += 1 272 | err += y_pos * 2 + 1 273 | if(-x_pos == y_pos and e2 <= x_pos): 274 | e2 = 0 275 | if (e2 > x_pos): 276 | x_pos += 1 277 | err += x_pos * 2 + 1 278 | if x_pos > 0: 279 | break 280 | 281 | def draw_filled_circle(self, frame_buffer, x, y, radius, tinted): 282 | # Bresenham algorithm 283 | x_pos = -radius 284 | y_pos = 0 285 | err = 2 - 2 * radius 286 | if (x >= self.width or y >= self.height): 287 | return 288 | while True: 289 | self.set_pixel(frame_buffer, x - x_pos, y + y_pos, tinted) 290 | self.set_pixel(frame_buffer, x + x_pos, y + y_pos, tinted) 291 | self.set_pixel(frame_buffer, x + x_pos, y - y_pos, tinted) 292 | self.set_pixel(frame_buffer, x - x_pos, y - y_pos, tinted) 293 | self.draw_horizontal_line(frame_buffer, x + x_pos, y + y_pos, 2 * (-x_pos) + 1, tinted) 294 | self.draw_horizontal_line(frame_buffer, x + x_pos, y - y_pos, 2 * (-x_pos) + 1, tinted) 295 | e2 = err 296 | if (e2 <= y_pos): 297 | y_pos += 1 298 | err += y_pos * 2 + 1 299 | if(-x_pos == y_pos and e2 <= x_pos): 300 | e2 = 0 301 | if (e2 > x_pos): 302 | x_pos += 1 303 | err += x_pos * 2 + 1 304 | if x_pos > 0: 305 | break 306 | -------------------------------------------------------------------------------- /third_party/waveshare/epd2in13.py: -------------------------------------------------------------------------------- 1 | # Ported to CircuitPython by Gregory P. Smith 2 | # The Waveshare EPD 2.13" display uses a protocol compatible with the 2.9". 3 | # So this file now just wraps that. 4 | 5 | ## 6 | # @filename : epd2in13.py 7 | # @brief : Implements for e-paper library 8 | # @author : Yehui from Waveshare 9 | # 10 | # Copyright (C) Waveshare September 9 2017 11 | # 12 | # Permission is hereby granted, free of charge, to any person obtaining a copy 13 | # of this software and associated documnetation files (the "Software"), to deal 14 | # in the Software without restriction, including without limitation the rights 15 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 16 | # copies of the Software, and to permit persons to whom the Software is 17 | # furished to do so, subject to the following conditions: 18 | # 19 | # The above copyright notice and this permission notice shall be included in 20 | # all copies or substantial portions of the Software. 21 | # 22 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 23 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 24 | # FITNESS OR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 25 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 26 | # LIABILITY WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 27 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 28 | # THE SOFTWARE. 29 | # 30 | from . import epd2in9 31 | 32 | class EPD(epd2in9.EPD): 33 | width = 128 # physically 122, logically 128 34 | height = 250 35 | 36 | # TODO convert to raw bytes literals to save space / mem / import time 37 | lut_full_update = bytes(( 38 | 0x22, 0x55, 0xAA, 0x55, 0xAA, 0x55, 0xAA, 0x11, 39 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 40 | 0x1E, 0x1E, 0x1E, 0x1E, 0x1E, 0x1E, 0x1E, 0x1E, 41 | 0x01, 0x00, 0x00, 0x00, 0x00, 0x00 42 | )) 43 | 44 | lut_partial_update = bytes(( 45 | 0x18, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 46 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 47 | 0x0F, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 48 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 49 | )) 50 | -------------------------------------------------------------------------------- /third_party/waveshare/epd2in7.py: -------------------------------------------------------------------------------- 1 | # ported to CircuitPython 3.0.0-beta0 by Gregory P. Smith 2 | 3 | ## 4 | # @filename : epd2in7.py 5 | # @brief : Implements for e-paper library 6 | # @author : Yehui from Waveshare 7 | # 8 | # Copyright (C) Waveshare July 31 2017 9 | # 10 | # Permission is hereby granted, free of charge, to any person obtaining a copy 11 | # of this software and associated documnetation files (the "Software"), to deal 12 | # in the Software without restriction, including without limitation the rights 13 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 14 | # copies of the Software, and to permit persons to whom the Software is 15 | # furished to do so, subject to the following conditions: 16 | # 17 | # The above copyright notice and this permission notice shall be included in 18 | # all copies or substantial portions of the Software. 19 | # 20 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 21 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 22 | # FITNESS OR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 23 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 24 | # LIABILITY WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 25 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 26 | # THE SOFTWARE. 27 | # 28 | 29 | import time 30 | 31 | from . import epdif 32 | 33 | # EPD2IN7 commands 34 | PANEL_SETTING = 0x00 35 | POWER_SETTING = 0x01 36 | POWER_OFF = 0x02 37 | POWER_OFF_SEQUENCE_SETTING = 0x03 38 | POWER_ON = 0x04 39 | POWER_ON_MEASURE = 0x05 40 | BOOSTER_SOFT_START = 0x06 41 | DEEP_SLEEP = 0x07 42 | DATA_START_TRANSMISSION_1 = 0x10 # NOTE: B&W data according to datasheet 43 | DATA_STOP = 0x11 44 | DISPLAY_REFRESH = 0x12 45 | DATA_START_TRANSMISSION_2 = 0x13 # NOTE: Red Data according to datasheet 46 | PARTIAL_DATA_START_TRANSMISSION_1 = 0x14 47 | PARTIAL_DATA_START_TRANSMISSION_2 = 0x15 48 | PARTIAL_DISPLAY_REFRESH = 0x16 49 | LUT_FOR_VCOM = 0x20 50 | LUT_WHITE_TO_WHITE = 0x21 51 | LUT_BLACK_TO_WHITE = 0x22 52 | LUT_WHITE_TO_BLACK = 0x23 53 | LUT_BLACK_TO_BLACK = 0x24 54 | PLL_CONTROL = 0x30 55 | TEMPERATURE_SENSOR_COMMAND = 0x40 56 | TEMPERATURE_SENSOR_CALIBRATION = 0x41 57 | TEMPERATURE_SENSOR_WRITE = 0x42 58 | TEMPERATURE_SENSOR_READ = 0x43 59 | VCOM_AND_DATA_INTERVAL_SETTING = 0x50 60 | LOW_POWER_DETECTION = 0x51 61 | TCON_SETTING = 0x60 62 | TCON_RESOLUTION = 0x61 63 | SOURCE_AND_GATE_START_SETTING = 0x62 64 | GET_STATUS = 0x71 65 | AUTO_MEASURE_VCOM = 0x80 66 | VCOM_VALUE = 0x81 67 | VCM_DC_SETTING_REGISTER = 0x82 68 | PROGRAM_MODE = 0xA0 69 | ACTIVE_PROGRAM = 0xA1 70 | READ_OTP_DATA = 0xA2 71 | 72 | class EPD: 73 | width = 176 74 | height = 264 75 | 76 | def __init__(self): 77 | self.reset_pin = None 78 | self.dc_pin = None 79 | self.busy_pin = None 80 | 81 | # TODO convert to raw bytes literals to save space / mem / import time 82 | lut_vcom_dc = bytes(( 83 | 0x00, 0x00, 84 | 0x00, 0x0F, 0x0F, 0x00, 0x00, 0x05, 85 | 0x00, 0x32, 0x32, 0x00, 0x00, 0x02, 86 | 0x00, 0x0F, 0x0F, 0x00, 0x00, 0x05, 87 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 88 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 89 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 90 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 91 | )) 92 | 93 | # R21H 94 | lut_ww = bytes(( 95 | 0x50, 0x0F, 0x0F, 0x00, 0x00, 0x05, 96 | 0x60, 0x32, 0x32, 0x00, 0x00, 0x02, 97 | 0xA0, 0x0F, 0x0F, 0x00, 0x00, 0x05, 98 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 99 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 100 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 101 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 102 | )) 103 | 104 | # R22H r 105 | lut_bw = bytes(( 106 | 0x50, 0x0F, 0x0F, 0x00, 0x00, 0x05, 107 | 0x60, 0x32, 0x32, 0x00, 0x00, 0x02, 108 | 0xA0, 0x0F, 0x0F, 0x00, 0x00, 0x05, 109 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 110 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 111 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 112 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 113 | )) 114 | 115 | # R24H b 116 | lut_bb = bytes(( 117 | 0xA0, 0x0F, 0x0F, 0x00, 0x00, 0x05, 118 | 0x60, 0x32, 0x32, 0x00, 0x00, 0x02, 119 | 0x50, 0x0F, 0x0F, 0x00, 0x00, 0x05, 120 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 121 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 122 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 123 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 124 | )) 125 | 126 | # R23H w 127 | lut_wb = bytes(( 128 | 0xA0, 0x0F, 0x0F, 0x00, 0x00, 0x05, 129 | 0x60, 0x32, 0x32, 0x00, 0x00, 0x02, 130 | 0x50, 0x0F, 0x0F, 0x00, 0x00, 0x05, 131 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 132 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 133 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 134 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 135 | )) 136 | 137 | 138 | def delay_ms(self, ms): 139 | time.sleep(ms / 1000.) 140 | 141 | def send_command(self, command): 142 | self.dc_pin.value = 0 143 | epdif.spi_transfer(command.to_bytes(1, 'big')) 144 | 145 | def send_data(self, data): 146 | self.dc_pin.value = 1 147 | if isinstance(data, int): 148 | epdif.spi_transfer(data.to_bytes(1, 'big')) 149 | else: 150 | # The EPD appears to want CS to cycle between every byte 151 | # so we loop doing one byte transfers. Not efficient, but 152 | # it makes it work. 153 | for i in range(len(data)): 154 | epdif.spi_transfer(data[i:i+1]) 155 | 156 | @property 157 | def fb_bytes(self): 158 | return self.width * self.height // 8 159 | 160 | def init(self): 161 | try: 162 | epdif.epd_io_bus_init() 163 | except RuntimeError: 164 | pass # it avoids global io bus reinitialization. good. 165 | self.reset_pin = epdif.RST_PIN 166 | self.dc_pin = epdif.DC_PIN 167 | self.busy_pin = epdif.BUSY_PIN 168 | # EPD hardware init start 169 | self.reset() 170 | self.send_command(POWER_SETTING) 171 | self.send_data(0x03) # VDS_EN, VDG_EN 172 | self.send_data(0x00) # VCOM_HV, VGHL_LV[1], VGHL_LV[0] 173 | self.send_data(0x2b) # VDH 174 | self.send_data(0x2b) # VDL 175 | self.send_data(0x09) # VDHR 176 | self.send_command(BOOSTER_SOFT_START) 177 | self.send_data(0x07) 178 | self.send_data(0x07) 179 | self.send_data(0x17) 180 | # Power optimization 181 | self.send_command(0xF8) 182 | self.send_data(0x60) 183 | self.send_data(0xA5) 184 | # Power optimization 185 | self.send_command(0xF8) 186 | self.send_data(0x89) 187 | self.send_data(0xA5) 188 | # Power optimization 189 | self.send_command(0xF8) 190 | self.send_data(0x90) 191 | self.send_data(0x00) 192 | # Power optimization 193 | self.send_command(0xF8) 194 | self.send_data(0x93) 195 | self.send_data(0x2A) 196 | # Power optimization 197 | self.send_command(0xF8) 198 | self.send_data(0xA0) 199 | self.send_data(0xA5) 200 | # Power optimization 201 | self.send_command(0xF8) 202 | self.send_data(0xA1) 203 | self.send_data(0x00) 204 | # Power optimization 205 | self.send_command(0xF8) 206 | self.send_data(0x73) 207 | self.send_data(0x41) 208 | self.send_command(PARTIAL_DISPLAY_REFRESH) 209 | self.send_data(0x00) 210 | self.send_command(POWER_ON) 211 | self.wait_until_idle() 212 | 213 | self.send_command(PANEL_SETTING) 214 | self.send_data(0xAF) # KW-BF KWR-AF BWROTP 0f 215 | self.send_command(PLL_CONTROL) 216 | self.send_data(0x3A) # 3A 100HZ 29 150Hz 39 200HZ 31 171HZ 217 | self.send_command(VCM_DC_SETTING_REGISTER) 218 | self.send_data(0x12) 219 | self.delay_ms(2) 220 | self.set_lut() 221 | # EPD hardware init end 222 | 223 | def wait_until_idle(self): 224 | while self.busy_pin.value == 0: # 0: busy, 1: idle 225 | self.delay_ms(10) 226 | 227 | def reset(self): 228 | self.reset_pin.value = 0 # module reset 229 | self.delay_ms(200) 230 | self.reset_pin.value = 1 231 | self.delay_ms(200) 232 | 233 | def set_lut(self): 234 | assert len(self.lut_vcom_dc) == 44 235 | self.send_command(LUT_FOR_VCOM) # vcom 236 | self.send_data(self.lut_vcom_dc) 237 | assert len(self.lut_ww) == 42 238 | self.send_command(LUT_WHITE_TO_WHITE) # ww -- 239 | self.send_data(self.lut_ww) 240 | assert len(self.lut_bw) == 42 241 | self.send_command(LUT_BLACK_TO_WHITE) # bw r 242 | self.send_data(self.lut_bw) 243 | assert len(self.lut_bb) == 42 244 | self.send_command(LUT_WHITE_TO_BLACK) # wb w 245 | self.send_data(self.lut_bb) 246 | assert len(self.lut_wb) == 42 247 | self.send_command(LUT_BLACK_TO_BLACK) # bb b 248 | self.send_data(self.lut_wb) 249 | 250 | def clear_frame_memory(self, pattern=0xff): 251 | """Fill the frame memory with a pattern byte. Does not call update.""" 252 | self.send_command(DATA_START_TRANSMISSION_2) 253 | self.delay_ms(2) 254 | row = pattern.to_bytes(1, 'big') * ((self.width + 7) // 8) 255 | for j in range(self.height): 256 | self.send_data(row) 257 | 258 | def display_frame(self): 259 | # TODO Determine if the 2.7" display can do double buffering. 260 | self.send_command(DISPLAY_REFRESH) 261 | self.wait_until_idle() 262 | 263 | def display_frame_buf(self, frame_buffer, fast_ghosting=None): 264 | """fast_ghosting ignored on the 2.7" display; it is always slow.""" 265 | fb_bytes = self.width * self.height // 8 266 | assert len(frame_buffer) == fb_bytes 267 | 268 | # This seems to do nothing on my EPD. 269 | #self.send_command(DATA_START_TRANSMISSION_1) 270 | #self.delay_ms(2) 271 | #for i in range(fb_bytes): 272 | # self.send_data(b'\xff') 273 | #self.delay_ms(2) 274 | 275 | # This is all that is needed on my display. 276 | # DATA_START_TRANSMISSION_1 seems to have no effect 277 | # despite a data sheet documenting it and mentioning 2 278 | # as being for the "Red" channel (bad datasheet...). 279 | self.send_command(DATA_START_TRANSMISSION_2) 280 | self.delay_ms(2) 281 | self.send_data(frame_buffer) 282 | self.delay_ms(2) 283 | 284 | self.display_frame() 285 | 286 | def display_bitmap(self, bitmap, fast_ghosting=False): 287 | """Render a MonoBitmap onto the display. 288 | 289 | fast_ghosting ignored on the 2.7" display; it is always slow. 290 | """ 291 | self.display_frame_buf(bitmap.bit_buf, fast_ghosting=fast_ghosting) 292 | 293 | # After this command is transmitted, the chip would enter the deep-sleep 294 | # mode to save power. The deep sleep mode would return to standby by 295 | # hardware reset. The only one parameter is a check code, the command would 296 | # be executed if check code = 0xA5. 297 | # Use self.reset() to awaken and use self.init() to initialize. 298 | def sleep(self): 299 | self.send_command(DEEP_SLEEP) 300 | self.delay_ms(2) 301 | self.send_data(0xa5) 302 | 303 | ### END OF FILE ### 304 | -------------------------------------------------------------------------------- /third_party/waveshare/epd2in9.py: -------------------------------------------------------------------------------- 1 | # Ported to CircuitPython 3.0 by Gregory P. Smith 2 | 3 | ## 4 | # @filename : epd2in9.py 5 | # @brief : Implements for e-paper library 6 | # @author : Yehui from Waveshare 7 | # 8 | # Copyright (C) Waveshare September 9 2017 9 | # 10 | # Permission is hereby granted, free of charge, to any person obtaining a copy 11 | # of this software and associated documnetation files (the "Software"), to deal 12 | # in the Software without restriction, including without limitation the rights 13 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 14 | # copies of the Software, and to permit persons to whom the Software is 15 | # furished to do so, subject to the following conditions: 16 | # 17 | # The above copyright notice and this permission notice shall be included in 18 | # all copies or substantial portions of the Software. 19 | # 20 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 21 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 22 | # FITNESS OR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 23 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 24 | # LIABILITY WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 25 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 26 | # THE SOFTWARE. 27 | # 28 | 29 | import time 30 | 31 | from . import epdif 32 | 33 | # EPD2IN9 commands 34 | DRIVER_OUTPUT_CONTROL = 0x01 35 | BOOSTER_SOFT_START_CONTROL = 0x0C 36 | GATE_SCAN_START_POSITION = 0x0F 37 | DEEP_SLEEP_MODE = 0x10 38 | DATA_ENTRY_MODE_SETTING = 0x11 39 | SW_RESET = 0x12 40 | TEMPERATURE_SENSOR_CONTROL = 0x1A 41 | MASTER_ACTIVATION = 0x20 42 | DISPLAY_UPDATE_CONTROL_1 = 0x21 43 | DISPLAY_UPDATE_CONTROL_2 = 0x22 44 | WRITE_RAM = 0x24 45 | WRITE_VCOM_REGISTER = 0x2C 46 | WRITE_LUT_REGISTER = 0x32 47 | SET_DUMMY_LINE_PERIOD = 0x3A 48 | SET_GATE_TIME = 0x3B 49 | BORDER_WAVEFORM_CONTROL = 0x3C 50 | SET_RAM_X_ADDRESS_START_END_POSITION = 0x44 51 | SET_RAM_Y_ADDRESS_START_END_POSITION = 0x45 52 | SET_RAM_X_ADDRESS_COUNTER = 0x4E 53 | SET_RAM_Y_ADDRESS_COUNTER = 0x4F 54 | TERMINATE_FRAME_READ_WRITE = 0xFF 55 | 56 | 57 | class EPD: 58 | width = 128 59 | height = 296 60 | 61 | def __init__(self): 62 | assert not (self.width & 3), "width must be a multiple of 8" 63 | self.reset_pin = None 64 | self.dc_pin = None 65 | self.busy_pin = None 66 | self.lut = self.lut_full_update 67 | 68 | # TODO convert to raw bytes literals to save space / mem / import time 69 | lut_full_update = bytes(( 70 | 0x02, 0x02, 0x01, 0x11, 0x12, 0x12, 0x22, 0x22, 71 | 0x66, 0x69, 0x69, 0x59, 0x58, 0x99, 0x99, 0x88, 72 | 0x00, 0x00, 0x00, 0x00, 0xF8, 0xB4, 0x13, 0x51, 73 | 0x35, 0x51, 0x51, 0x19, 0x01, 0x00 74 | )) 75 | 76 | lut_partial_update = bytes(( 77 | 0x10, 0x18, 0x18, 0x08, 0x18, 0x18, 0x08, 0x00, 78 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 79 | 0x00, 0x00, 0x00, 0x00, 0x13, 0x14, 0x44, 0x12, 80 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 81 | )) 82 | 83 | def _delay_ms(self, ms): 84 | time.sleep(ms / 1000.) 85 | 86 | def _send_command(self, command): 87 | self.dc_pin.value = 0 88 | epdif.spi_transfer(command.to_bytes(1, 'big')) 89 | 90 | def _send_data(self, data): 91 | self.dc_pin.value = 1 92 | if isinstance(data, int): 93 | epdif.spi_transfer(data.to_bytes(1, 'big')) 94 | else: 95 | # The EPD needs CS to cycle hi between every byte so we loop doing 96 | # one byte transfers to cause that. Not efficient, but it makes 97 | # it work. Data sheets say needs to be at least a 60ns CS pulse. 98 | for i in range(len(data)): 99 | epdif.spi_transfer(data[i:i+1]) 100 | 101 | @property 102 | def fb_bytes(self): 103 | return self.width * self.height // 8 104 | 105 | def init(self, lut=None): 106 | try: 107 | epdif.epd_io_bus_init() 108 | except RuntimeError: 109 | pass # It avoids global io bus reinitialization. Good. 110 | self.reset_pin = epdif.RST_PIN 111 | self.dc_pin = epdif.DC_PIN 112 | self.busy_pin = epdif.BUSY_PIN 113 | # EPD hardware init start 114 | self.lut = lut or self.lut_full_update 115 | self.reset() 116 | self._send_command(DRIVER_OUTPUT_CONTROL) 117 | self._send_data((self.height - 1) & 0xFF) 118 | self._send_data(((self.height - 1) >> 8) & 0xFF) 119 | self._send_data(0x00) # GD = 0 SM = 0 TB = 0 120 | self._send_command(BOOSTER_SOFT_START_CONTROL) 121 | self._send_data(0xD7) 122 | self._send_data(0xD6) 123 | self._send_data(0x9D) 124 | self._send_command(WRITE_VCOM_REGISTER) 125 | self._send_data(0xA8) # VCOM 7C 126 | self._send_command(SET_DUMMY_LINE_PERIOD) 127 | self._send_data(0x1A) # 4 dummy lines per gate 128 | self._send_command(SET_GATE_TIME) 129 | self._send_data(0x08) # 2us per line 130 | self._send_command(DATA_ENTRY_MODE_SETTING) 131 | self._send_data(0x03) # X increment Y increment 132 | self.set_lut(self.lut) 133 | # EPD hardware init end 134 | 135 | def wait_until_idle(self): 136 | while self.busy_pin.value == 1: # 0: idle, 1: busy 137 | self._delay_ms(10) 138 | 139 | ## 140 | # @brief: module reset. 141 | # often used to awaken the module in deep sleep, 142 | ## 143 | def reset(self): 144 | self.reset_pin.value = 0 # module reset 145 | self._delay_ms(200) 146 | self.reset_pin.value = 1 147 | self._delay_ms(200) 148 | 149 | ## 150 | # @brief: set the look-up table register 151 | ## 152 | def set_lut(self, lut=None): 153 | self.lut = lut or self.lut_full_update 154 | assert len(self.lut) == 30 # the length of look-up table is 30 bytes 155 | self._send_command(WRITE_LUT_REGISTER) 156 | self._send_data(self.lut) 157 | 158 | ## 159 | # @brief: put an image to the frame memory. 160 | # this won't update the display. 161 | ## 162 | def set_frame_memory(self, bitmap, x, y): 163 | """Place bitmap at x (multiple of 8), y in the EPD frame buffer. 164 | 165 | bitmap: A MonoBitmap instance; must be a multiple of 8 wide. 166 | """ 167 | if x & 0x7 or bitmap.width & 0x7 or x < 0 or y < 0: 168 | raise ValueError('bad x, y, or width: %d, %d, %d' 169 | % (x, y, bitmap.width)) 170 | image_width = bitmap.width 171 | image_height = bitmap.height 172 | if (x + image_width >= self.width): 173 | x_end = self.width - 1 174 | else: 175 | x_end = x + image_width - 1 176 | if (y + image_height >= self.height): 177 | y_end = self.height - 1 178 | else: 179 | y_end = y + image_height - 1 180 | self._set_memory_area(x, y, x_end, y_end) 181 | for j in range(y, y_end + 1): 182 | # The 2.13" display only likes receiving one row of data per WRITE_RAM. 183 | # At a guess: Internally it may be bit based and does this to avoid 184 | # implementing skipping partial end of row bytes given the non 185 | # multiple of 8 width resolution? 186 | self._set_memory_pointer(x, j) 187 | offset = j * self.width // 8 188 | self._send_command(WRITE_RAM) 189 | self._send_data(bitmap.bit_buf[offset+x:offset+(x_end//8)+1]) 190 | 191 | def clear_frame_memory(self, pattern=0xff): 192 | """Fill the frame memory with a pattern byte. Does not call update.""" 193 | self._set_memory_area(0, 0, self.width - 1, self.height - 1) 194 | self._set_memory_pointer(0, 0) 195 | row = pattern.to_bytes(1, 'big') * ((self.width + 7) // 8) 196 | for j in range(self.height): 197 | # Some displays only accept one row of data per WRITE_RAM. 198 | self._set_memory_pointer(0, j) 199 | self._send_command(WRITE_RAM) 200 | self._send_data(row) 201 | 202 | ## 203 | # @brief: update the display 204 | # there are 2 memory areas embedded in the e-paper display 205 | # but once this function is called, 206 | # the the next action of SetFrameMemory or ClearFrame will 207 | # set the other memory area. 208 | ## 209 | def display_frame(self): 210 | """Calling this will swap the display for the other buffer.""" 211 | self._send_command(DISPLAY_UPDATE_CONTROL_2) 212 | self._send_data(0xC4) 213 | self._send_command(MASTER_ACTIVATION) 214 | self._send_command(TERMINATE_FRAME_READ_WRITE) 215 | self.wait_until_idle() 216 | 217 | def display_frame_buf(self, frame_buffer, fast_ghosting=False): 218 | assert len(frame_buffer) == self.fb_bytes 219 | for _ in range(2): 220 | self._set_memory_area(0, 0, self.width-1, self.height-1) 221 | for j in range(0, self.height): 222 | # Some displays only accept one row of data per WRITE_RAM. 223 | self._set_memory_pointer(0, j) 224 | offset = j * self.width // 8 225 | self._send_command(WRITE_RAM) 226 | self._send_data(frame_buffer[offset:offset + (self.width//8) + 1]) 227 | self.display_frame() 228 | if fast_ghosting: 229 | break 230 | 231 | def display_bitmap(self, bitmap, fast_ghosting=False): 232 | """Render a MonoBitmap onto the display. 233 | 234 | Args: 235 | bitmap: A MonoBitmap instance 236 | fast_ghosting: If true the display update is twice as fast by only 237 | refreshing once; this can leave a ghost of the previous contents. 238 | """ 239 | # TODO: add partial update support. 240 | # if bitmap size is full frame size and x/y offsets are 0: 241 | # epd.init(epd.lut_full_update) 242 | # else: 243 | # epd.init(epd.lut_partial_update) 244 | 245 | self.set_frame_memory(bitmap, 0, 0) 246 | self.display_frame() 247 | if not fast_ghosting: 248 | self.set_frame_memory(bitmap, 0, 0) 249 | self.display_frame() 250 | 251 | ## 252 | # @brief: specify the memory area for data R/W 253 | ## 254 | def _set_memory_area(self, x_start, y_start, x_end, y_end): 255 | if x_start & 0x7: 256 | raise ValueError('x must be a multiple of 8 (%d)' % (x_start,)) 257 | self._send_command(SET_RAM_X_ADDRESS_START_END_POSITION) 258 | self._send_data((x_start >> 3) & 0xFF) 259 | self._send_data((x_end >> 3) & 0xFF) 260 | self._send_command(SET_RAM_Y_ADDRESS_START_END_POSITION) 261 | self._send_data(y_start & 0xFF) 262 | self._send_data((y_start >> 8) & 0xFF) 263 | self._send_data(y_end & 0xFF) 264 | self._send_data((y_end >> 8) & 0xFF) 265 | 266 | ## 267 | # @brief: specify the start point for data R/W 268 | ## 269 | def _set_memory_pointer(self, x, y): 270 | if x & 0x7: 271 | raise ValueError('x must be a multiple of 8') 272 | self._send_command(SET_RAM_X_ADDRESS_COUNTER) 273 | self._send_data((x >> 3) & 0xFF) 274 | self._send_command(SET_RAM_Y_ADDRESS_COUNTER) 275 | self._send_data(y & 0xFF) 276 | self._send_data((y >> 8) & 0xFF) 277 | self.wait_until_idle() 278 | 279 | ## 280 | # @brief: After this command is transmitted, the chip would enter the 281 | # deep-sleep mode to save power. 282 | # The deep sleep mode would return to standby by hardware reset. 283 | # You can use reset() to awaken or init() to initialize 284 | ## 285 | def sleep(self): 286 | self._send_command(DEEP_SLEEP_MODE) 287 | self.wait_until_idle() 288 | 289 | -------------------------------------------------------------------------------- /third_party/waveshare/epdif.py: -------------------------------------------------------------------------------- 1 | # ported to CircuitPython 3.0.0-beta0 by Gregory P. Smith 2 | 3 | ## 4 | # @filename : epdif.py 5 | # @brief : EPD hardware interface implements (GPIO, SPI) 6 | # @author : Yehui from Waveshare 7 | # 8 | # Copyright (C) Waveshare July 4 2017 9 | # 10 | # Permission is hereby granted, free of charge, to any person obtaining a copy 11 | # of this software and associated documnetation files (the "Software"), to deal 12 | # in the Software without restriction, including without limitation the rights 13 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 14 | # copies of the Software, and to permit persons to whom the Software is 15 | # furished to do so, subject to the following conditions: 16 | # 17 | # The above copyright notice and this permission notice shall be included in 18 | # all copies or substantial portions of the Software. 19 | # 20 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 21 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 22 | # FITNESS OR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 23 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 24 | # LIABILITY WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 25 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 26 | # THE SOFTWARE. 27 | # 28 | 29 | import adafruit_bus_device.spi_device 30 | import board 31 | import digitalio 32 | 33 | # Pin definition as hooked up on my Metro M4 (reassigned to instances in init) 34 | # These pins are also present on the ItsyBitsy M4 35 | RST_PIN = board.D11 36 | DC_PIN = board.D9 37 | CS_PIN = board.D10 38 | BUSY_PIN = board.D7 39 | 40 | #_SPI_MOSI = board.MOSI 41 | #_SPI_CLK = board.SCK 42 | _SPI_MOSI = board.A2 43 | _SPI_CLK = board.A3 44 | _SPI_BUS = None 45 | _init = False 46 | 47 | 48 | def spi_transfer(data): 49 | with _SPI_BUS as device: 50 | device.write(data) 51 | 52 | def epd_io_bus_init(): 53 | global _init 54 | if _init: 55 | raise RuntimeError("epd_io_bus_init() called twice") 56 | _init = True 57 | global RST_PIN, DC_PIN, CS_PIN, BUSY_PIN 58 | DInOut = digitalio.DigitalInOut 59 | OUTPUT = digitalio.Direction.OUTPUT 60 | INPUT = digitalio.Direction.INPUT 61 | RST_PIN = DInOut(RST_PIN) 62 | RST_PIN.direction = OUTPUT 63 | DC_PIN = DInOut(DC_PIN) 64 | DC_PIN.direction = OUTPUT 65 | CS_PIN = DInOut(CS_PIN) 66 | CS_PIN.direction = OUTPUT 67 | BUSY_PIN = DInOut(BUSY_PIN) 68 | BUSY_PIN.direction = INPUT 69 | global _SPI_BUS 70 | # bus vs bitbang isn't really important for slow displays, detecting 71 | # when to use one vs the other is overkill... 72 | if (_SPI_CLK == getattr(board, 'SCK', None) and 73 | _SPI_MOSI == getattr(board, 'MOSI', None)): 74 | import busio as io_module 75 | else: 76 | import bitbangio as io_module 77 | _SPI_BUS = adafruit_bus_device.spi_device.SPIDevice( 78 | io_module.SPI(_SPI_CLK, _SPI_MOSI), CS_PIN, 79 | baudrate=2000000) 80 | 81 | ### END OF FILE ### 82 | -------------------------------------------------------------------------------- /update-device.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | #------------------------------------------------------------------------- 3 | # Author: Gregory P. Smith (@gpshead) 4 | # 5 | # Copyright 2018 Google LLC 6 | # 7 | # Licensed under the Apache License, Version 2.0 (the "License"); 8 | # you may not use this file except in compliance with the License. 9 | # You may obtain a copy of the License at 10 | # 11 | # https://www.apache.org/licenses/LICENSE-2.0 12 | # 13 | # Unless required by applicable law or agreed to in writing, software 14 | # distributed under the License is distributed on an "AS IS" BASIS, 15 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 16 | # See the License for the specific language governing permissions and 17 | # limitations under the License. 18 | #------------------------------------------------------------------------- 19 | 20 | # A convenient way to push code changes to a Circuitpython device. 21 | # You may not need the depending on your Linux distro setup. I'm 22 | # using a minimal Debian install on a compute stick rather than a 23 | # desktop distro configured to auto-mount USB devices. 24 | # 25 | # This assumes you have something like this in your /etc/fstab: 26 | # /dev/sda1 /mnt vfat user,noauto 0 0 27 | # 28 | mount /dev/sda1 29 | rsync --checksum --inplace --exclude '*README*' --exclude '*LICENSE' \ 30 | -r *.py asm_thumb third_party /mnt 31 | sync 32 | umount /mnt 33 | --------------------------------------------------------------------------------