Created
January 2, 2024 18:29
-
-
Save lerouxb/0ba2b5d5318ad0c1199148bf4e72e4fd to your computer and use it in GitHub Desktop.
A display driver for Sharp's LS013B7DH03 display entirely in the ESP32's ULP coprocessor
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
from esp32 import ULP | |
from machine import mem32 | |
from esp32_ulp import src_to_binary | |
source = """\ | |
# constants from: | |
# https://github.com/espressif/esp-idf/blob/v5.0.2/components/soc/esp32/include/soc/reg_base.h | |
#define DR_REG_RTCIO_BASE 0x3ff48400 | |
# constants from: | |
# https://github.com/espressif/esp-idf/blob/v5.0.2/components/soc/esp32/include/soc/rtc_io_reg.h | |
#define RTC_IO_TOUCH_PAD6_REG (DR_REG_RTCIO_BASE + 0xac) | |
#define RTC_IO_TOUCH_PAD6_MUX_SEL_M (BIT(19)) | |
#define RTC_IO_TOUCH_PAD4_REG (DR_REG_RTCIO_BASE + 0xa4) | |
#define RTC_IO_TOUCH_PAD4_MUX_SEL_M (BIT(19)) | |
#define RTC_IO_TOUCH_PAD3_REG (DR_REG_RTCIO_BASE + 0xa0) | |
#define RTC_IO_TOUCH_PAD3_MUX_SEL_M (BIT(19)) | |
#define RTC_GPIO_OUT_REG (DR_REG_RTCIO_BASE + 0x0) | |
#define RTC_GPIO_ENABLE_REG (DR_REG_RTCIO_BASE + 0xc) | |
#define RTC_GPIO_ENABLE_S 14 | |
#define RTC_GPIO_OUT_DATA_S 14 | |
# constants from: | |
# https://github.com/espressif/esp-idf/blob/v5.0.2/components/soc/esp32/include/soc/rtc_io_channel.h | |
#define RTCIO_GPIO14_CHANNEL 16 | |
#define RTCIO_GPIO13_CHANNEL 14 | |
#define RTCIO_GPIO15_CHANNEL 13 | |
# When accessed from the RTC module (ULP) GPIOs need to be addressed by their channel number | |
.set clk, RTCIO_GPIO14_CHANNEL | |
.set mosi, RTCIO_GPIO13_CHANNEL | |
.set cs, RTCIO_GPIO15_CHANNEL | |
.text | |
cmd: .long 3 | |
word_address: .long 0 | |
line_counter: .long 0 | |
word_counter: .long 0 | |
.global entry | |
entry: | |
# These instructions take many cycles each so it _might_ be worth it to | |
# track whether we have initialised yet. But right now it probably isn't the | |
# lowest hanging fruit and certainly won't multiply the overall frame rate. | |
# connect GPIO to ULP (0: GPIO connected to digital GPIO module, 1: GPIO connected to analog RTC module) | |
WRITE_RTC_REG(RTC_IO_TOUCH_PAD6_REG, RTC_IO_TOUCH_PAD6_MUX_SEL_M, 1, 1); | |
WRITE_RTC_REG(RTC_IO_TOUCH_PAD4_REG, RTC_IO_TOUCH_PAD4_MUX_SEL_M, 1, 1); | |
WRITE_RTC_REG(RTC_IO_TOUCH_PAD3_REG, RTC_IO_TOUCH_PAD3_MUX_SEL_M, 1, 1); | |
# GPIO shall be output, not input (this also enables a pull-down by default) | |
WRITE_RTC_REG(RTC_GPIO_ENABLE_REG, RTC_GPIO_ENABLE_S + clk, 1, 1) | |
WRITE_RTC_REG(RTC_GPIO_ENABLE_REG, RTC_GPIO_ENABLE_S + mosi, 1, 1) | |
WRITE_RTC_REG(RTC_GPIO_ENABLE_REG, RTC_GPIO_ENABLE_S + cs, 1, 1) | |
# cs high | |
WRITE_RTC_REG(RTC_GPIO_OUT_REG, RTC_GPIO_OUT_DATA_S + cs, 1, 1) | |
# invert the COM signal. ie. toggle cmd between 1 and 3 | |
MOVE R1, cmd | |
LD R0, R1, 0 | |
JUMPR cmd3, 1, EQ | |
JUMP cmd1 | |
cmd3: | |
MOVE R0, 3 | |
MOVE R1, cmd | |
ST R0, R1, 0 | |
JUMP run | |
cmd1: | |
MOVE R0, 1 | |
MOVE R1, cmd | |
ST R0, R1, 0 | |
JUMP run | |
run: | |
# reset word_address to be the start of the display buffer in words | |
MOVE R0, 1024 | |
MOVE R1, word_address | |
ST R0, R1, 0 | |
# start line_counter at 1 | |
MOVE R0, 1 | |
MOVE R1, line_counter | |
ST R0, R1, 0 | |
per_line: | |
# NOTE: assume that line_counter is still stored in R0 because that's where | |
# it is stored in the initial case and it is also where it still is before | |
# we jump back here after the last word in the previous line | |
# we could have a write_byte and then we can just send command and display | |
# number as a word so we save on shift and or? but is that really faster | |
# because we'd end up with more jumps | |
# we could actually skip this shift for all lines after the first one and | |
# just leave a 0 in there because in those cases the second byte is a dummy, | |
# but MOVE (to get the 0 into R3) is just as many cycles. Only worth it if | |
# we also skip the OR. | |
LSH R3, R0, 8 # msb is the display line number | |
# load cmd into R2 | |
MOVE R1, cmd | |
LD R2, R1, 0 | |
# make R1 the command followed by the display line number | |
OR R1, R2, R3 # lsb is now the command including com bit | |
# R1 is the word we want to write, R3 is the return address, write_word can | |
# use all the registers it wants | |
MOVE R3, after_command_address | |
JUMP write_word | |
after_command_address: | |
# start the word counter at 0 | |
MOVE R0, 0 | |
MOVE R1, word_counter | |
ST R0, R1, 0 | |
per_word: | |
# restore the word_address to R2 | |
MOVE R1, word_address | |
LD R2, R1, 0 | |
# put the word we want to send in R1 | |
LD R1, R2, 0 | |
# and the return address in R3 | |
MOVE R3, after_word | |
JUMP write_word | |
after_word: | |
# restore the word address to R2 | |
MOVE R1, word_address | |
LD R2, R1, 0 | |
ADD R0, R2, 1 | |
# store the incremented word address at R1 | |
ST R0, R1, 0 | |
# restore the word counter to R2 | |
MOVE R1, word_counter | |
LD R2, R1, 0 | |
# increment it | |
ADD R0, R2, 1 | |
# store the incremented word counter at R1 which is still the address | |
ST R0, R1, 0 | |
# keep sending words until we've sent 8 | |
JUMPR per_word, 8, LT | |
# restore the line counter | |
MOVE R1, line_counter | |
LD R2, R1, 0 | |
# increment it | |
ADD R0, R2, 1 | |
# store the incremented line counter at R1 which is still the address | |
ST R0, R1, 0 | |
# NOTE: we are leaving the line counter at R0 because that is what per_line expects | |
# keep sending lines until we've sent 128 | |
JUMPR per_line, 129, LT # 1 to 128, not 0 to 127 | |
after_lines: | |
# then two dummy bytes | |
MOVE R1, 0 | |
MOVE R3, end | |
JUMP write_word | |
end: | |
# cs low | |
WRITE_RTC_REG(RTC_GPIO_OUT_REG, RTC_GPIO_OUT_DATA_S + cs, 1, 0) | |
HALT | |
# R0: used internally | |
# R1: the word to write (will be consumed) | |
# R2: used as write_bit (after_mosi)'s return address | |
# R3: return address | |
write_word: | |
MOVE R2, after_1 | |
%(write_bit)s | |
after_1: | |
MOVE R2, after_2 | |
%(write_bit)s | |
after_2: | |
MOVE R2, after_3 | |
%(write_bit)s | |
after_3: | |
MOVE R2, after_4 | |
%(write_bit)s | |
after_4: | |
MOVE R2, after_5 | |
%(write_bit)s | |
after_5: | |
MOVE R2, after_6 | |
%(write_bit)s | |
after_6: | |
MOVE R2, after_7 | |
%(write_bit)s | |
after_7: | |
MOVE R2, after_8 | |
%(write_bit)s | |
after_8: | |
MOVE R2, after_9 | |
%(write_bit)s | |
after_9: | |
MOVE R2, after_10 | |
%(write_bit)s | |
after_10: | |
MOVE R2, after_11 | |
%(write_bit)s | |
after_11: | |
MOVE R2, after_12 | |
%(write_bit)s | |
after_12: | |
MOVE R2, after_13 | |
%(write_bit)s | |
after_13: | |
MOVE R2, after_14 | |
%(write_bit)s | |
after_14: | |
MOVE R2, after_15 | |
%(write_bit)s | |
after_15: | |
MOVE R2, R3 | |
%(write_bit)s | |
mosi_high: | |
WRITE_RTC_REG(RTC_GPIO_OUT_REG, RTC_GPIO_OUT_DATA_S + mosi, 1, 1) | |
%(after_mosi)s | |
mosi_low: | |
WRITE_RTC_REG(RTC_GPIO_OUT_REG, RTC_GPIO_OUT_DATA_S + mosi, 1, 0) | |
%(after_mosi)s | |
""" | |
write_bit = """ | |
# mosi set to a single bit of data | |
AND R0, R1, 1 | |
# if only we could somehow write this R0 register to the pin without having to jump to the code and back... :( | |
JUMPR mosi_high, 1, EQ | |
JUMP mosi_low | |
""" | |
after_mosi = """ | |
# if only there was a way to say "pulse this thing briefly" without needing two of these slow macro calls.. | |
# clk high | |
WRITE_RTC_REG(RTC_GPIO_OUT_REG, RTC_GPIO_OUT_DATA_S + clk, 1, 1) | |
# clk low | |
WRITE_RTC_REG(RTC_GPIO_OUT_REG, RTC_GPIO_OUT_DATA_S + clk, 1, 0) | |
# shift R1 to the right via R0 | |
MOVE R0, R1 | |
RSH R1, R0, 1 | |
# the next bit or back out of write_word | |
JUMP R2 | |
""" | |
binary = src_to_binary(source % { | |
"after_mosi": after_mosi, # type: ignore | |
"write_bit": write_bit # type: ignore | |
}, cpu="esp32") | |
load_addr, entry_addr = 0, 16 | |
ULP_MEM_BASE = 0x50000000 | |
ULP_DATA_MASK = 0xffff # ULP data is only in lower 16 bits | |
# put the ULP's display buffer half-way through the 8K of RTC memory, should be | |
# well beyond the ULP code and leave exactly enough memory for the buffer | |
ULP_BUFFER_MEM_BASE = ULP_MEM_BASE + 4096 | |
ulp = ULP() | |
ulp.set_wakeup_period(0, 500000) # use timer0, wakeup after 500000usec (0.5s) | |
ulp.load_binary(load_addr, binary) | |
ulp.run(entry_addr) | |
import framebuf | |
class SHARP_ULP(framebuf.FrameBuffer): | |
@staticmethod | |
def rgb(r, g, b): | |
return int((r > 127) or (g > 127) or (b > 127)) | |
def __init__(self): | |
self.height = 128 | |
self.width = 128 | |
self._buffer = bytearray(self.height * self.width // 8) | |
self._mvb = memoryview(self._buffer) | |
super().__init__(self._buffer, self.width, self.height, framebuf.MONO_HMSB) | |
# copy the buffer over to RTC memory using lower 16 bits out of every 32 so | |
# that the ULP can access it | |
def show(self): | |
# 1024 32-bit words or 4096 bytes | |
for i in range(self.width*self.height // 16): | |
msb = self._buffer[i*2+1] | |
lsb = self._buffer[i*2] | |
mem32[ULP_BUFFER_MEM_BASE + i*4] = (msb << 8) | lsb | |
sharp = SHARP_ULP() | |
sharp.fill(1) | |
sharp.text("hello world", 8, 8, 0) | |
sharp.show() | |
def debug(): | |
# 1024 32-bit words or 4096 bytes | |
for i in range(128*128 // 16): | |
if i % 8 == 0: | |
print() | |
print(str.format('0x{:04X} ', mem32[ULP_BUFFER_MEM_BASE + i*4]), end='') | |
print() | |
#while True: | |
# print(hex(mem32[ULP_MEM_BASE + load_addr] & ULP_DATA_MASK), # cmd | |
# hex(mem32[ULP_MEM_BASE + load_addr + 4] & ULP_DATA_MASK), # loop_counter | |
# hex(mem32[ULP_MEM_BASE + load_addr + 8] & ULP_DATA_MASK) # word_counter | |
# ) |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment