Skip to content

Instantly share code, notes, and snippets.

@lerouxb
Created January 2, 2024 18:29
Show Gist options
  • Save lerouxb/0ba2b5d5318ad0c1199148bf4e72e4fd to your computer and use it in GitHub Desktop.
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
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