Created
August 2, 2025 17:15
-
-
Save samneggs/e04c32a77fe526d85a4773a30b27fb68 to your computer and use it in GitHub Desktop.
Display number in 'LED 7 segment' format, fast, variable size
This file contains hidden or 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
| # 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