Skip to content

Instantly share code, notes, and snippets.

@samneggs
Created August 2, 2025 17:15
Show Gist options
  • Save samneggs/e04c32a77fe526d85a4773a30b27fb68 to your computer and use it in GitHub Desktop.
Save samneggs/e04c32a77fe526d85a4773a30b27fb68 to your computer and use it in GitHub Desktop.
Display number in 'LED 7 segment' format, fast, variable size
# template 240x160 on core 1
from st7796 import LCD_3inch5
from random import randint
from machine import freq, I2C, Pin, mem32
import time, _thread, gc, framebuf , array
from time import sleep_ms, sleep_us
MAXSCREEN_X = const(240)
MAXSCREEN_Y = const(160)
SHOWING = const(0)
EXIT = const(1)
#colors
BROWN = const(0x6092)
BLUE = const(0b_11111_00000_00000)
GREEN = const(0b111_00000_00000_111)
YELLOW = const(0xff)
PINK = const(0x1ff8)
PURPLE = const(0x1188)
RED = const(0b_00000_11111_00000)
GREY = const(0b000_11000_11000_110)
WHITE = const(0xffff)
LT_BLUE= const(0b_11111_00101_00101)
FPS_CORE0 = const(0)
FPS_CORE1 = const(1)
SCORE = const(2) # Index for score in values array
LIVES = const(3) # Index for lives in values array
HEALTH = const(4) # Index for health in values array
MEM_FREE = const(5)
NUM_VALUES = const(10) # Total number of odometers we'll track
class Draw_number:
def __init__(self, max_width):
self.max_width = max_width # Store the maximum screen width
self.values = [0] * NUM_VALUES # Current displayed values array
self.targets = [0] * NUM_VALUES # Target values array
self.speed = 1 # How many units to change per update
self.max_diff = 10_000 # Maximum value to add/subtract at once
self.FPS0 = bytearray(35) # FPS array for core 0
self.FPS1 = bytearray(35) # FPS array for core 1
self.char_map = bytearray(( # Character bitmap data for digits 0-9
0x3E, 0x63, 0x73, 0x7B, 0x6F, 0x67, 0x3E, 0x00, # U+0030 (0)
0x0C, 0x0E, 0x0C, 0x0C, 0x0C, 0x0C, 0x3F, 0x00, # U+0031 (1)
0x1E, 0x33, 0x30, 0x1C, 0x06, 0x33, 0x3F, 0x00, # U+0032 (2)
0x1E, 0x33, 0x30, 0x1C, 0x30, 0x33, 0x1E, 0x00, # U+0033 (3)
0x38, 0x3C, 0x36, 0x33, 0x7F, 0x30, 0x78, 0x00, # U+0034 (4)
0x3F, 0x03, 0x1F, 0x30, 0x30, 0x33, 0x1E, 0x00, # U+0035 (5)
0x1C, 0x06, 0x03, 0x1F, 0x33, 0x33, 0x1E, 0x00, # U+0036 (6)
0x3F, 0x33, 0x30, 0x18, 0x0C, 0x0C, 0x0C, 0x00, # U+0037 (7)
0x1E, 0x33, 0x33, 0x1E, 0x33, 0x33, 0x1E, 0x00, # U+0038 (8)
0x1E, 0x33, 0x33, 0x3E, 0x30, 0x18, 0x0E, 0x00)) # U+0039 (9)
@staticmethod
@micropython.asm_thumb
def avg_fps_asm(r0, r1): # r0 = fps[] , r1 = current_fps
mov(r2,0xff)
cmp(r1,r2)
blt(GOOD_FPS)
mov(r1,r2)
label(GOOD_FPS)
ldrb(r2,[r0,0]) # r2 = fps[0]
add(r2,r2,1) # fps[0] += 1
cmp(r2,33)
blt(LT_32) # if fps[0] > 32:
mov(r2,1)
label(LT_32)
strb(r2,[r0,0]) # fps[0] = new index
add(r2,r2,r0)
strb(r1,[r2,0]) # fps[fps[0]] = current_fps
mov(r2,1) # r2 = i
mov(r3,0) # r3 = tot
label(LOOP)
add(r0,r0,1)
ldrb(r4,[r0,0]) # r4 = fps[i]
add(r3,r3,r4) # tot += fps[i]
add(r2,r2,1)
cmp(r2,33) #33
blt(LOOP)
asr(r0,r3,5)
def set_target(self, idx, new_target): # Set target for specific index
if new_target < 0: # Ensure non-negative
new_target = 0
diff = new_target - self.targets[idx] # Calculate difference
if diff > self.max_diff: # Limit how much we can add at once
self.targets[idx] += self.max_diff
elif diff < -self.max_diff: # Limit how much we can subtract at once
self.targets[idx] -= self.max_diff
else: # Within limits, set the exact target
self.targets[idx] = new_target
@micropython.viper
def update(self, idx: int) -> bool: # Update specific index value
current:int = int(self.values[int(idx)])
target:int = int(self.targets[int(idx)])
speed:int = int(self.speed)
if current == target: # Already at target
return False
changed:bool = False
if current < target: # Need to increment
if (target - current) <= speed:
current = target # Avoid overshooting
else:
current += speed # Normal increment
changed = True
elif current > target: # Need to decrement
if (current - target) <= speed:
current = target # Avoid undershooting
else:
current -= speed # Normal decrement
changed = True
if changed:
self.values[int(idx)] = current # Store updated value
return changed
def update_all(self): # Update all odometer values
changed = False
for i in range(NUM_VALUES):
if self.update(i):
changed = True
return changed
def add(self, idx, amount, immed = False): # Add to target value at index
if immed:
self.values[idx] += amount
self.targets[idx] = self.values[idx]
else:
self.set_target(idx, self.targets[idx] + amount)
def subtract(self, idx, amount): # Subtract from target value at index
self.set_target(idx, self.targets[idx] - amount)
def set_speed(self, new_speed): # Set animation speed (units per update)
if new_speed < 1:
new_speed = 1
self.speed = new_speed
def set(self, idx, value):
if idx == FPS_CORE0: # Special case for FPS_CORE0
self.values[idx] = int(self.avg_fps_asm(self.FPS0, 1_000 // (1 + int(time.ticks_diff(int(time.ticks_ms()), value)))))
self.targets[idx] = self.values[idx]
elif idx == FPS_CORE1: # Special case for FPS_CORE1
self.values[idx] = int(self.avg_fps_asm(self.FPS1, 1_000 // int(time.ticks_diff(int(time.ticks_ms()), value))))
self.targets[idx] = self.values[idx]
else: # Normal case for other values
self.values[idx] = value
self.targets[idx] = value
def done(self, idx): # Check if specific odometer is done
return self.values[idx] == self.targets[idx]
def all_done(self): # Check if all odometers are done
for i in range(NUM_VALUES):
if not self.done(i):
return False
return True
def draw(self, idx, x_offset, y_offset, color=0xffff, size=1):
self.draw_viper(self.values[idx], x_offset, y_offset, color, size)
@micropython.viper
def draw_viper(self, num: int, x_offset: int, y_offset: int, color: int, size: int):
char_ptr = ptr8(self.char_map) # Get pointer to character map
screen_ptr = ptr16(LCD.fbdraw) # Get pointer to framebuffer
char = 0 # Character position counter
width = int(self.max_width)
offset = width * y_offset + x_offset # Calculate starting offset
first = 1 # Flag for first digit
while num > 0 or first: # Process each digit
first = 0 # Clear first digit flag
total = num // 10 # Calculate quotient
digit = num - (total * 10) # Extract current digit
num = total # Update number for next iteration
for y in range(8): # Process each row of the digit
row_data = char_ptr[digit * 8 + y] # Get row bitmap data
for x in range(8): # Process each pixel in the row
if row_data & (1 << x) > 0: # Check if pixel is set
addr = size * y * width + x - (char * 8) + offset # Calculate pixel address
screen_ptr[addr] = color # Set pixel color
if size > 1: # Handle size scaling
screen_ptr[width + addr] = color # Draw pixel in second row
if size > 2: # Handle larger scaling
screen_ptr[2 * width + addr] = color # Draw pixel in third row
char += 1
class Led_number:
def __init__(self):
self.control_array = array.array('i', [0] * 7) # Pre-allocated control array
self.segment_patterns = bytearray(( # Seven segment patterns for digits 0-9
0b01111110, # 0: segments a,b,c,d,e,f
0b00110000, # 1: segments b,c
0b01101101, # 2: segments a,b,d,e,g
0b01111001, # 3: segments a,b,c,d,g
0b00110011, # 4: segments b,c,f,g
0b01011011, # 5: segments a,c,d,f,g
0b01011111, # 6: segments a,c,d,e,f,g
0b01110000, # 7: segments a,b,c
0b01111111, # 8: segments a,b,c,d,e,f,g
0b01111011)) # 9: segments a,b,c,d,f,g
@micropython.viper
def draw_digit(self, digit:int, x:int, y:int, size:int):
patterns = ptr8(self.segment_patterns) # Get pointer to segment patterns
pattern = patterns[digit] # Get pattern for this digit
thick:int = size >> 3 # Segment thickness = size/4
if thick < 1: # Minimum thickness of 1
thick = 1
seg_len:int = size >> 1 # Segment length
half_size:int = size >> 1 # Half size for positioning
if pattern & 0b01000000: # Segment a (top horizontal)
self.draw_horizontal_fast(x + thick, y, seg_len - thick, thick)
if pattern & 0b00100000: # Segment b (top right vertical)
self.draw_vertical_fast(x + seg_len, y + thick, half_size - thick, thick)
if pattern & 0b00010000: # Segment c (bottom right vertical)
self.draw_vertical_fast(x + seg_len, y + half_size + thick, half_size - thick, thick)
if pattern & 0b00001000: # Segment d (bottom horizontal)
self.draw_horizontal_fast(x + thick, y + size, seg_len - thick, thick)
if pattern & 0b00000100: # Segment e (bottom left vertical)
self.draw_vertical_fast(x, y + half_size + thick, half_size - thick, thick)
if pattern & 0b00000010: # Segment f (top left vertical)
self.draw_vertical_fast(x, y + thick, half_size - thick, thick)
if pattern & 0b00000001: # Segment g (middle horizontal)
self.draw_horizontal_fast(x + thick, y + half_size, seg_len - thick, thick)
@staticmethod
@micropython.asm_thumb
def draw_horizontal_asm(r0) -> int: # r0: control_array
mov(r1,r0)
ldr(r0, [r1, 0])
ldr(r2, [r1, 4]) # r2 = x
ldr(r3, [r1, 8]) # r3 = y
ldr(r4, [r1, 12]) # r4 = length
ldr(r5, [r1, 16]) # r5 = thick
ldr(r6, [r1, 20]) # r6 = color
mov(r7, MAXSCREEN_X) # r7 = width constant
add(r1, r3, r5) # r1 = y + thick
cmp(r1, MAXSCREEN_Y) # Compare with screen height
bgt(exit_error) # Exit if off screen
add(r1, r2, r4) # r1 = x + length
cmp(r1, MAXSCREEN_X) # Compare with screen width constant
ble(length_ok) # If within bounds, continue
sub(r4, r7, r2) # Clip: length = MAXSCREEN_X - x
cmp(r4, 0) # Check if clipped length is valid
ble(exit_error) # Exit if no pixels to draw
label(length_ok)
cmp(r2, 0) # Check x >= 0
blt(exit_error) # Exit if x < 0
cmp(r3, 0) # Check y >= 0
blt(exit_error) # Exit if y < 0
mov(r1, r3) # r1 = y
mul(r1, r7) # r1 = y * width
add(r1, r1, r2) # r1 = y * width + x (start_offset)
lsl(r1, r1, 1) # r1 *= 2 (16-bit pixels)
add(r0, r0, r1) # r0 = fb_ptr + start_offset
mov(r1, 0) # r1 = row counter (i)
label(row_loop)
cmp(r1, r5) # Compare i with thick
bge(exit_success) # Exit when all rows done
mov(r2, 0) # r2 = column counter (j)
label(col_loop)
cmp(r2, r4) # Compare j with length
bge(next_row) # Go to next row when length done
strh(r6, [r0, 0]) # Store color at current position
add(r0, r0, 2) # Move to next pixel (16-bit)
add(r2, r2, 1) # j++
b(col_loop) # Continue column loop
label(next_row)
mov(r3, r2) # r3 = j (pixels drawn this row)
lsl(r3, r3, 1) # r3 = j * 2 (16-bit adjustment)
sub(r0, r0, r3) # Move back to start of current row
lsl(r3, r7, 1) # r3 = width * 2 (16-bit pixels)
add(r0, r0, r3) # Move to start of next row
add(r1, r1, 1) # i++
b(row_loop) # Continue row loop
label(exit_success)
mov(r0, 1) # Return success
b(exit_end) # Jump to end
label(exit_error)
mov(r0, 0) # Return error
label(exit_end)
@micropython.viper
def draw_horizontal_fast(self, x:int, y:int, length:int, thick:int):
ctrl = ptr32(self.control_array) # Get pointer to control array
ctrl[1] = x # Store x
ctrl[2] = y # Store y
ctrl[3] = length # Store length
ctrl[4] = thick # Store thick
return self.draw_horizontal_asm(self.control_array) # Call assembly function
# @micropython.viper
# def draw_horizontal(self, fb_ptr:ptr16, x:int, y:int, length:int, thick:int, color:int, width:int):
# start_offset:int = y * width + x # Calculate starting framebuffer offset
# i:int = 0 # Row counter
# while i < thick: # Draw thickness rows
# offset:int = start_offset + i * width # Calculate row offset
# j:int = 0 # Column counter
# while j < length: # Draw segment length
# fb_ptr[offset + j] = color # Set pixel color
# j += 1 # Next column
# i += 1 # Next row
@staticmethod
@micropython.asm_thumb
def draw_vertical_asm(r0) -> int: # r0: control_array
mov(r1,r0) # r0: fb_ptr
ldr(r0, [r1, 0])
ldr(r2, [r1, 4]) # r2 = x
ldr(r3, [r1, 8]) # r3 = y
ldr(r4, [r1, 12]) # r4 = length
ldr(r5, [r1, 16]) # r5 = thick
ldr(r6, [r1, 20]) # r6 = color
mov(r7, MAXSCREEN_X) # r7 = width constant
add(r1, r3, r4) # r1 = y + length
cmp(r1, MAXSCREEN_Y) # Compare with screen height
bgt(exit_error_v) # Exit if off screen
add(r1, r2, r5) # r1 = x + thick
cmp(r1, MAXSCREEN_X) # Compare with screen width constant
ble(thick_ok) # If within bounds, continue
sub(r5, r7, r2) # Clip: thick = MAXSCREEN_X - x
cmp(r5, 0) # Check if clipped thick is valid
ble(exit_error_v) # Exit if no pixels to draw
label(thick_ok)
cmp(r2, 0) # Check x >= 0
blt(exit_error_v) # Exit if x < 0
cmp(r3, 0) # Check y >= 0
blt(exit_error_v) # Exit if y < 0
mov(r1, r3) # r1 = y
mul(r1, r7) # r1 = y * width
add(r1, r1, r2) # r1 = y * width + x (start_offset)
lsl(r1, r1, 1) # r1 *= 2 (16-bit pixels)
add(r0, r0, r1) # r0 = fb_ptr + start_offset
mov(r1, 0) # r1 = row counter (i)
label(row_loop_v)
cmp(r1, r4) # Compare i with length
bge(exit_success_v) # Exit when all rows done
mov(r2, 0) # r2 = column counter (j)
label(col_loop_v)
cmp(r2, r5) # Compare j with thick
bge(next_row_v) # Go to next row when thick done
strh(r6, [r0, 0]) # Store color at current position
add(r0, r0, 2) # Move to next pixel (16-bit)
add(r2, r2, 1) # j++
b(col_loop_v) # Continue column loop
label(next_row_v)
mov(r3, r2) # r3 = j (pixels drawn this row)
lsl(r3, r3, 1) # r3 = j * 2 (16-bit adjustment)
sub(r0, r0, r3) # Move back to start of current row
lsl(r3, r7, 1) # r3 = width * 2 (16-bit pixels)
add(r0, r0, r3) # Move to start of next row
add(r1, r1, 1) # i++
b(row_loop_v) # Continue row loop
label(exit_success_v)
mov(r0, 1) # Return success
b(exit_end_v) # Jump to end
label(exit_error_v)
mov(r0, 0) # Return error
label(exit_end_v)
@micropython.viper
def draw_vertical_fast(self, x:int, y:int, length:int, thick:int):
ctrl = ptr32(self.control_array) # Get pointer to control array
ctrl[1] = x # Store x
ctrl[2] = y # Store y
ctrl[3] = length # Store length
ctrl[4] = thick # Store thick
return self.draw_vertical_asm(self.control_array) # Call assembly function
# @micropython.viper
# def draw_vertical(self, fb_ptr:ptr16, x:int, y:int, length:int, thick:int, color:int, width:int):
# start_offset:int = y * width + x # Calculate starting framebuffer offset
# i:int = 0 # Row counter
# while i < length: # Draw segment length
# offset:int = start_offset + i * width # Calculate row offset
# j:int = 0 # Column counter
# while j < thick: # Draw thickness columns
# fb_ptr[offset + j] = color # Set pixel color
# j += 1 # Next column
# i += 1 # Next row
@micropython.viper
def draw_number(self, framebuffer, num:int, x:int, y:int, size:int):
ctrl = ptr32(self.control_array) # Get pointer to control array
ctrl[0] = int(ptr16(framebuffer)) # Store fb_ptr address (not used in asm)
digit_width:int = size - (size >> 2) # Width per digit (size + 25% spacing)
temp_num:int = num # Working copy of number
digit_count:int = 0 # Count digits for positioning
if temp_num == 0: # Handle zero case
digit_count = 1
else:
while temp_num > 0: # Count digits
temp_num //= 10 # Remove rightmost digit
digit_count += 1 # Increment count
current_x:int = x + (digit_count - 1) * digit_width # Start from rightmost position
temp_num = num # Reset working number
first:bool = True # Flag for first digit
while temp_num > 0 or first: # Process each digit
first = False # Clear first digit flag
digit:int = temp_num % 10 # Extract rightmost digit
self.draw_digit(digit, current_x, y, size) # Draw digit
temp_num //= 10 # Remove processed digit
current_x -= digit_width # Move to next digit position
def draw(self, framebuffer, number, x, y, size, color=0xFFFF): # Public interface method
self.control_array[5] = color
self.draw_number(framebuffer, number, x, y, size) # Call viper drawing method
@micropython.viper
def draw():
status = ptr8(LCD.aux)
LCD.fill2(LCD.fbdraw,0x0)
size = (int(time.ticks_ms()) & 0xfff ) // 40
led_num.draw(LCD.fbdraw, int(time.ticks_ms()), 0, 0, 45, 0xff) #framebuffer, number, x, y, size, color=0xFFFF)
led_num.draw(LCD.fbdraw, int(time.ticks_ms()), 80-size, 110 - size, size, 0xff) #framebuffer, number, x, y, size, color=0xFFFF)
led_num.draw(LCD.fbdraw, int(time.ticks_ms()), 0, 100, 45, 0xff) #framebuffer, number, x, y, size, color=0xFFFF)
draw_num.draw(FPS_CORE0,220,0)
draw_num.draw(FPS_CORE1,220,10)
#draw_num.draw(MEM_FREE,100,40,LT_BLUE)
status[SHOWING] = 0
@micropython.viper
def main():
status = ptr8(LCD.aux)
gc.collect()
print(gc.mem_free())
score_ticks = 0
while not status[EXIT]:
while not status[SHOWING]: sleep_ms(1)
ticks = int(time.ticks_ms())
if not FIRE_BUTTON.value() and ticks - score_ticks >1000:
score_ticks = ticks
draw_num.add(SCORE, 100)
if not MAGIC_BUTTON.value() and ticks - score_ticks >1000:
score_ticks = ticks
draw_num.add(SCORE, 100,1)
draw_num.update_all()
draw()
#draw_num.set(MEM_FREE, gc.mem_free())
draw_num.set(FPS_CORE0, ticks)
shutdown()
def shutdown():
LCD.aux[EXIT] = 1
sleep_ms(200)
LCD.off() #LCD off
sleep_ms(200)
freq(150_000_000,48_000_000)
print('core0 done')
@micropython.viper
def core1():
status = ptr8(LCD.aux)
sleep_ms(200)
while not status[EXIT]:
ticks=int(time.ticks_ms())
status[SHOWING] = 1
LCD.show_all()
LCD.flip()
draw_num.set(FPS_CORE1, ticks)
print('core1 done')
if __name__=='__main__':
FIRE_BUTTON = Pin(14, Pin.IN, Pin.PULL_UP)
MAGIC_BUTTON = Pin(15, Pin.IN, Pin.PULL_UP)
freq(220_000_000) #220
machine.mem32[0x40010048] = 1<<11 # enable peri_ctrl clock
LCD = LCD_3inch5(MAXSCREEN_X,MAXSCREEN_Y)
draw_num = Draw_number(MAXSCREEN_X)
led_num = Led_number()
_thread.start_new_thread(core1, ())
sleep_ms(200)
try:
main()
shutdown()
except KeyboardInterrupt :
shutdown()
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment