1+ # SPDX-FileCopyrightText: 2025 Liz Clark for Adafruit Industries
2+ #
3+ # SPDX-License-Identifier: MIT
4+
5+ """
6+ `adafruit_epd.uc8179` - Adafruit UC8179 - ePaper display driver
7+ ====================================================================================
8+ CircuitPython driver for Adafruit UC8179 display breakouts
9+ * Author(s): Liz Clark
10+ """
11+
12+ import time
13+
14+ import adafruit_framebuf
15+ from micropython import const
16+
17+ from adafruit_epd .epd import Adafruit_EPD
18+
19+ try :
20+ """Needed for type annotations"""
21+ import typing
22+
23+ from busio import SPI
24+ from digitalio import DigitalInOut
25+ from typing_extensions import Literal
26+
27+ except ImportError :
28+ pass
29+
30+ __version__ = "0.0.0+auto.0"
31+ __repo__ = "https://github.com/adafruit/Adafruit_CircuitPython_EPD.git"
32+
33+ # UC8179 commands
34+ _UC8179_PANELSETTING = const (0x00 )
35+ _UC8179_POWERSETTING = const (0x01 )
36+ _UC8179_POWEROFF = const (0x02 )
37+ _UC8179_POWERON = const (0x04 )
38+ _UC8179_DEEPSLEEP = const (0x07 )
39+ _UC8179_WRITE_RAM1 = const (0x10 )
40+ _UC8179_DATASTOP = const (0x11 )
41+ _UC8179_DISPLAYREFRESH = const (0x12 )
42+ _UC8179_WRITE_RAM2 = const (0x13 )
43+ _UC8179_DUALSPI = const (0x15 )
44+ _UC8179_WRITE_VCOM = const (0x50 )
45+ _UC8179_TCON = const (0x60 )
46+ _UC8179_TRES = const (0x61 )
47+ _UC8179_GET_STATUS = const (0x71 )
48+
49+ BUSY_WAIT = const (500 ) # milliseconds
50+
51+
52+ class Adafruit_UC8179 (Adafruit_EPD ):
53+ """driver class for Adafruit UC8179 ePaper display breakouts"""
54+
55+ # Default initialization sequence
56+ _default_init_code = bytes ([
57+ _UC8179_POWERSETTING , 4 ,
58+ 0x07 , # VGH=20V
59+ 0x07 , # VGL=-20V
60+ 0x3F , # VDH=15V
61+ 0x3F , # VDL=-15V
62+
63+ _UC8179_POWERON , 0 ,
64+ 0xFF , 100 , # busy wait
65+
66+ _UC8179_PANELSETTING , 1 ,
67+ 0b010111 , # BW OTP LUT
68+
69+ _UC8179_TRES , 4 ,
70+ 0x02 , 0x88 , 0x01 , 0xE0 ,
71+
72+ _UC8179_DUALSPI , 1 , 0x00 ,
73+
74+ _UC8179_WRITE_VCOM , 2 , 0x10 , 0x07 ,
75+ _UC8179_TCON , 1 , 0x22 ,
76+
77+ 0xFE # End marker
78+ ])
79+
80+ def __init__ (
81+ self ,
82+ width : int ,
83+ height : int ,
84+ spi : SPI ,
85+ * ,
86+ cs_pin : DigitalInOut ,
87+ dc_pin : DigitalInOut ,
88+ sramcs_pin : DigitalInOut ,
89+ rst_pin : DigitalInOut ,
90+ busy_pin : DigitalInOut ,
91+ ) -> None :
92+ # Adjust height to be divisible by 8 (direct from Arduino)
93+ if (height % 8 ) != 0 :
94+ height += 8 - (height % 8 )
95+
96+ super ().__init__ (width , height , spi , cs_pin , dc_pin , sramcs_pin , rst_pin , busy_pin )
97+
98+ # Calculate buffer sizes exactly as Arduino does: width * height / 8
99+ self ._buffer1_size = width * height // 8
100+ self ._buffer2_size = self ._buffer1_size
101+
102+ if sramcs_pin :
103+ # Using external SRAM
104+ self ._buffer1 = self .sram .get_view (0 )
105+ self ._buffer2 = self .sram .get_view (self ._buffer1_size )
106+ else :
107+ # Using internal RAM
108+ self ._buffer1 = bytearray (self ._buffer1_size )
109+ self ._buffer2 = bytearray (self ._buffer2_size )
110+
111+ # Create frame buffers
112+ self ._framebuf1 = adafruit_framebuf .FrameBuffer (
113+ self ._buffer1 ,
114+ width ,
115+ height ,
116+ buf_format = adafruit_framebuf .MHMSB ,
117+ )
118+ self ._framebuf2 = adafruit_framebuf .FrameBuffer (
119+ self ._buffer2 ,
120+ width ,
121+ height ,
122+ buf_format = adafruit_framebuf .MHMSB ,
123+ )
124+
125+ # Set up which frame buffer is which color
126+ self .set_black_buffer (0 , True )
127+ self .set_color_buffer (1 , False )
128+
129+ # UC8179 uses single byte transactions
130+ self ._single_byte_tx = True
131+
132+ # Custom init code if needed
133+ self ._epd_init_code = None
134+
135+ # Default refresh delay (from Adafruit_EPD base class in Arduino)
136+ self .default_refresh_delay = 15 # seconds
137+ # pylint: enable=too-many-arguments
138+
139+ def begin (self , reset : bool = True ) -> None :
140+ """Begin communication with the display and set basic settings"""
141+ # Direct port of Arduino begin() method
142+
143+ # Note: Arduino sets _data_entry_mode = THINKINK_UC8179
144+ # This appears to be specific to SRAM organization for this chip
145+
146+ if reset :
147+ self .hardware_reset ()
148+
149+ # Black buffer defaults to inverted (0 means black)
150+ self .set_black_buffer (0 , True )
151+ # Red/color buffer defaults to not inverted (1 means red)
152+ self .set_color_buffer (1 , False )
153+
154+ self .power_down ()
155+
156+ def busy_wait (self ) -> None :
157+ """Wait for display to be done with current task, either by polling the
158+ busy pin, or pausing"""
159+ if self ._busy :
160+ # Wait for busy pin to go HIGH
161+ while not self ._busy .value :
162+ self .command (_UC8179_GET_STATUS )
163+ time .sleep (0.1 )
164+ else :
165+ # No busy pin, just wait
166+ time .sleep (BUSY_WAIT / 1000.0 )
167+ # Additional delay after busy signal
168+ time .sleep (0.2 )
169+
170+ def power_up (self ) -> None :
171+ """Power up the display in preparation for writing RAM and updating"""
172+ self .hardware_reset ()
173+
174+ # Use custom init code if provided, otherwise use default
175+ init_code = self ._epd_init_code if self ._epd_init_code else self ._default_init_code
176+
177+ # Process initialization sequence
178+ self ._command_list (init_code )
179+
180+ # Set display resolution (using WIDTH and HEIGHT macros in Arduino)
181+ self .command (
182+ _UC8179_TRES ,
183+ bytearray ([self ._width >> 8 , self ._width & 0xFF , self ._height >> 8 , self ._height & 0xFF ]),
184+ )
185+
186+ def power_down (self ) -> None :
187+ """Power down the display - required when not actively displaying!"""
188+ self .command (_UC8179_POWEROFF )
189+ self .busy_wait ()
190+
191+ # Only deep sleep if we have a reset pin to wake it up
192+ if self ._rst :
193+ self .command (_UC8179_DEEPSLEEP , bytearray ([0x05 ]))
194+ time .sleep (0.1 )
195+
196+ def update (self ) -> None :
197+ """Update the display from internal memory"""
198+ self .command (_UC8179_DISPLAYREFRESH )
199+ time .sleep (0.1 )
200+ self .busy_wait ()
201+
202+ if not self ._busy :
203+ # If no busy pin, use default refresh delay
204+ time .sleep (self .default_refresh_delay / 1000.0 )
205+
206+ def write_ram (self , index : Literal [0 , 1 ]) -> int :
207+ """Send the one byte command for starting the RAM write process. Returns
208+ the byte read at the same time over SPI. index is the RAM buffer, can be
209+ 0 or 1 for tri-color displays."""
210+ if index == 0 :
211+ return self .command (_UC8179_WRITE_RAM1 , end = False )
212+ if index == 1 :
213+ return self .command (_UC8179_WRITE_RAM2 , end = False )
214+ raise RuntimeError ("RAM index must be 0 or 1" )
215+
216+ def set_ram_address (self , x : int , y : int ) -> None : # noqa: PLR6301, F841
217+ """Set the RAM address location, not used on this chipset but required by
218+ the superclass"""
219+ # Not used in UC8179 chip
220+ pass
221+
222+ def set_ram_window (self , x1 : int , y1 : int , x2 : int , y2 : int ) -> None : # noqa: PLR6301, F841
223+ """Set the RAM window, not used on this chipset but required by
224+ the superclass"""
225+ # Not used in UC8179 chip
226+ pass
227+
228+ def _command_list (self , init_sequence : bytes ) -> None :
229+ """Process a command list for initialization"""
230+ i = 0
231+ while i < len (init_sequence ):
232+ cmd = init_sequence [i ]
233+ i += 1
234+
235+ # Check for end marker
236+ if cmd == 0xFE :
237+ break
238+
239+ # Get number of data bytes
240+ num_data = init_sequence [i ]
241+ i += 1
242+
243+ # Check for delay command (0xFF)
244+ if cmd == 0xFF :
245+ time .sleep (num_data / 1000.0 )
246+ else :
247+ # Send command with data if any
248+ if num_data > 0 :
249+ self .command (cmd , bytearray (init_sequence [i :i + num_data ]))
250+ i += num_data
251+ else :
252+ self .command (cmd )
0 commit comments