/ vgamode.py
vgamode.py
1 #!/usr/bin/env python 2 import sys 3 4 from enum import Enum, auto 5 from dataclasses import dataclass, replace 6 7 class Polarity: 8 Negative = 0 9 Positive = 1 10 11 @dataclass(frozen=True) 12 class VideoMode: 13 pixel_clock_khz: int 14 15 visible_width: int 16 hfront_porch: int 17 hsync_pulse: int 18 hback_porch: int 19 hsync_polarity: Polarity 20 21 @property 22 def total_width(self): 23 return self.visible_width + self.hfront_porch + self.hsync_pulse + self.hback_porch 24 25 @property 26 def line_rate_khz(self): 27 return self.pixel_clock_khz / self.total_width 28 29 @property 30 def line_time_us(self): 31 return 1000 / self.line_rate_khz 32 33 @property 34 def visible_line_time_us(self): 35 return 1000 * self.visible_width / self.pixel_clock_khz 36 37 visible_height: int 38 vfront_porch: int 39 vsync_pulse: int 40 vback_porch: int 41 vsync_polarity: Polarity 42 43 @property 44 def total_lines(self): 45 return self.visible_height + self.vfront_porch + self.vsync_pulse + self.vback_porch 46 47 @property 48 def frame_rate_hz(self): 49 return self.line_rate_khz / self.total_lines * 1000 50 51 @property 52 def frame_time_ms(self): 53 return 1000 / self.frame_rate_hz 54 55 def __repr__(self): 56 return f"<VideoMode {self.visible_width}x{self.visible_height} {self.line_rate_khz:.2f}kHz/{self.frame_rate_hz:.2f}Hz pclk={self.pixel_clock_khz:.0f}KHz hvis={self.visible_line_time_us:.2f}us>" 57 58 def change_visible_pixels(mode_in, new_w, new_clock=None): 59 print(mode_in) 60 if new_clock is None: 61 new_clock = mode_in.pixel_clock_khz * new_w / mode_in.visible_width 62 new_mode = replace(mode_in, 63 pixel_clock_khz = new_clock, 64 visible_width = new_w, 65 ) 66 print(new_mode) 67 ratio = new_clock / mode_in.pixel_clock_khz 68 new_visible_time = new_mode.visible_line_time_us 69 new_line_counts = round(mode_in.total_width * ratio) 70 print(new_line_counts, mode_in.total_width * ratio) 71 new_pulse = round(mode_in.hsync_pulse * ratio) 72 new_porch_counts = new_line_counts - new_pulse - new_w 73 porch_ratio = mode_in.hfront_porch / (mode_in.hfront_porch + mode_in.hback_porch) 74 new_front = round(new_porch_counts * porch_ratio) 75 new_back = new_porch_counts - new_front 76 print((mode_in.hfront_porch, mode_in.hsync_pulse, mode_in.hback_porch), "->", (new_front, new_pulse, new_back)) 77 new_mode = replace( 78 new_mode, 79 hfront_porch = new_front, 80 hsync_pulse = new_pulse, 81 hback_porch = new_back) 82 print(new_mode) 83 print() 84 return new_mode 85 86 def pio_hard_delay(instr, n, file): 87 assert n > 0 88 assert n < 128 89 while n > 0: 90 cycles = min(n, 32) 91 print(f" {instr} [{cycles-1}]", file=file) 92 n -= cycles 93 print(file=file) 94 95 def print_pio_hsync_program(program_name_base, mode, h_divisor, cycles_per_pixel, file=sys.stdout): 96 net_khz = mode.pixel_clock_khz / h_divisor 97 err = (mode.visible_width + mode.hfront_porch) % h_divisor 98 print(f""" 99 ; Horizontal sync program for {mode} 100 ; PIO clock frequency = {mode.pixel_clock_khz:.1f}/{h_divisor}khz = {net_khz:.1f} 101 ; 102 .program {program_name_base}_hsync 103 pull block ; Pull from FIFO to OSR (only happens once) 104 105 .wrap_target ; Program wraps to here 106 ; ACTIVE + FRONTPORCH {mode.visible_width} + {mode.hfront_porch} error {err} 107 mov x, osr ; Copy value from OSR to x scratch register 108 activeporch: 109 jmp x-- activeporch ; Remain high in active mode and front porch 110 111 """, file=file) 112 113 cycles, err = divmod(mode.hsync_pulse + err, h_divisor) 114 print(f"syncpulse: ; {mode.hsync_pulse}/{h_divisor} clocks [actual {cycles} error {err}]", file=file) 115 pio_hard_delay(f"set pins, {mode.hsync_polarity:d}", cycles, file=file) 116 117 cycles, err = divmod(mode.hback_porch + err, h_divisor) 118 print(f"backporch: ; {mode.hback_porch}/{h_divisor} clocks [actual {cycles} error {err}]", file=file) 119 pio_hard_delay(f"set pins, {not mode.hsync_polarity:d}", cycles - 1, file=file) 120 print(" irq 0 [1]", file=file) 121 print(".wrap", file=file) 122 123 print(f""" 124 % c-sdk {{ 125 static inline void {program_name_base}_hsync_program_init(PIO pio, uint sm, uint offset, uint pin) {{ 126 127 pio_sm_config c = {program_name_base}_hsync_program_get_default_config(offset); 128 129 // Map the state machine's SET pin group to one pin, namely the `pin` 130 // parameter to this function. 131 sm_config_set_set_pins(&c, pin, 1); 132 sm_config_set_clkdiv_int_frac(&c, {cycles_per_pixel * h_divisor}, 0); 133 // Set this pin's GPIO function (connect PIO to the pad) 134 pio_gpio_init(pio, pin); 135 // Set the pin direction to output at the PIO 136 pio_sm_set_consecutive_pindirs(pio, sm, pin, 1, true); 137 138 // Load our configuration, and jump to the start of the program 139 pio_sm_init(pio, sm, offset, &c); 140 // Set the state machine running 141 pio_sm_set_enabled(pio, sm, true); 142 }} 143 144 %}} 145 """, file=file) 146 147 def pio_yloop(instr, n, label, comment, file): 148 assert n <= 65 149 if n < 3: 150 print(f"{label}: ; {comment}", file=file) 151 for i in range(n): 152 print(f" {instr}", file=file) 153 elif n <= 33: 154 print(f" set y, {n-1 if n <= 32 else 31}", file=file) 155 print(f"{label}: ; {comment}", file=file) 156 print(f" {instr}", file=file) 157 print(f" jmp y--, {label}", file=file) 158 if n == 33: 159 print(f" {instr}", file=file) 160 elif n <= 65: 161 print(f" set y, {(n-2)//2}", file=file) 162 print(f"{label}: ; {comment}", file=file) 163 print(f" {instr}", file=file) 164 print(f" {instr}", file=file) 165 print(f" jmp y--, {label}", file=file) 166 if n & 1: 167 print(f" {instr}", file=file) 168 print(file=file) 169 170 def print_pio_vsync_program(program_name_base, mode, cycles_per_pixel, file=sys.stdout): 171 print(f""" 172 .program {program_name_base}_vsync 173 .side_set 1 opt 174 ; Vertical sync program for {mode} 175 ; 176 pull block ; Pull from FIFO to OSR (only once) 177 178 .wrap_target ; Program wraps to here 179 """, file=file) 180 181 pio_yloop("wait 1 irq 0", mode.vfront_porch, "frontporch", f"{mode.vfront_porch} lines", file=file) 182 183 pio_yloop(f"wait 1 irq 0 side {mode.vsync_polarity:d}", mode.vsync_pulse, "syncpulse", f"{mode.vsync_pulse} lines", file=file) 184 185 pio_yloop(f"wait 1 irq 0 side {not mode.vsync_polarity:d}", mode.vback_porch, "backporch", f"{mode.vback_porch} lines", file=file) 186 187 print(f""" 188 ; ACTIVE 189 mov x, osr ; Copy value from OSR to x scratch register 190 active: 191 wait 1 irq 0 ; Wait for hsync to go high 192 irq 1 ; Signal that we're in active mode 193 jmp x-- active ; Remain in active mode, decrementing counter 194 195 """, file=file) 196 197 198 print(".wrap", file=file) 199 200 print(f""" 201 % c-sdk {{ 202 static inline void {program_name_base}_vsync_program_init(PIO pio, uint sm, uint offset, uint pin) {{ 203 204 pio_sm_config c = {program_name_base}_vsync_program_get_default_config(offset); 205 206 // Map the state machine's SIDESET to one pin, namely the `pin` 207 // parameter to this function. 208 // sm_config_set_sideset(&c, 1, true, false); 209 sm_config_set_sideset_pins(&c, pin); 210 sm_config_set_sideset(&c, 2, true, false); 211 sm_config_set_clkdiv_int_frac(&c, {cycles_per_pixel}, 0); 212 213 // Set this pin's GPIO function (connect PIO to the pad) 214 pio_gpio_init(pio, pin); 215 // Set the pin direction to output at the PIO 216 pio_sm_set_consecutive_pindirs(pio, sm, pin, 1, true); 217 218 // Load our configuration, and jump to the start of the program 219 pio_sm_init(pio, sm, offset, &c); 220 // Set the state machine running 221 pio_sm_set_enabled(pio, sm, true); 222 223 }} 224 225 %}} 226 """, file=file) 227 228 229 230 def print_pio_pixel_program(program_name_base, mode, out_instr, cycles_per_pixel, file=sys.stdout): 231 net_khz = cycles_per_pixel * mode.pixel_clock_khz 232 assert cycles_per_pixel >= 2 233 print(f""" 234 .program {program_name_base}_pixel 235 ; Pixel generator program for {mode} 236 ; PIO clock frequency = {cycles_per_pixel}×{mode.pixel_clock_khz}khz = {net_khz} 237 pull block ; Pull from FIFO to OSR (only once) 238 mov y, osr ; Copy value from OSR to y scratch register 239 pull block ; Pull first pixel data 240 241 .wrap_target 242 set pins, 0 ; Zero RGB pins in blanking 243 mov x, y ; Initialize counter variable 244 245 wait 1 irq 1 [{cycles_per_pixel-1}] ; wait for vsync active mode 246 247 colorout: 248 {out_instr} [{cycles_per_pixel-2}] 249 jmp x-- colorout ; Stay here thru horizontal active mode 250 251 .wrap""", file=file) 252 253 print(f""" 254 % c-sdk {{ 255 enum {{ {program_name_base}_pixel_clock_khz = {mode.pixel_clock_khz}, {program_name_base}_sys_clock_khz = {cycles_per_pixel * mode.pixel_clock_khz} }}; 256 257 static inline void {program_name_base}_pixel_program_init(PIO pio, uint sm, uint offset, uint pin, uint n_pin) {{ 258 259 pio_sm_config c = {program_name_base}_pixel_program_get_default_config(offset); 260 261 // Map the state machine's OUT & SET pins 262 sm_config_set_out_pins(&c, pin, n_pin); 263 sm_config_set_set_pins(&c, pin, n_pin); 264 sm_config_set_fifo_join(&c, PIO_FIFO_JOIN_TX); 265 // 15 pixels (30 bits) per word, left is MSB 266 sm_config_set_out_shift(&c, false, true, 30); 267 268 // Set this pin's GPIO function (connect PIO to the pad) 269 for(uint i=0; i<n_pin; i++) {{ 270 pio_gpio_init(pio, pin + i); 271 }} 272 // Set the pin direction to output at the PIO 273 pio_sm_set_consecutive_pindirs(pio, sm, pin, n_pin, true); 274 275 // Load our configuration, and jump to the start of the program 276 pio_sm_init(pio, sm, offset, &c); 277 // Set the state machine running 278 pio_sm_set_enabled(pio, sm, true); 279 }} 280 281 %}} 282 """, file=file) 283 284 mode_vga_640x480 = VideoMode(25_175, 640, 16, 96, 48, Polarity.Negative, 480, 10, 2, 33, Polarity.Negative) 285 mode_vga_660x480 = change_visible_pixels(mode_vga_640x480, 660, 26_000) 286 287 mode_vga_720x400 = VideoMode(28_321, 720, 18, 108, 54, Polarity.Negative, 400, 10, 2, 36, Polarity.Positive) 288 mode_vga_660x400 = change_visible_pixels(mode_vga_720x400, 660, 26_000) 289 290 if 1: 291 print(mode_vga_640x480, 6*mode_vga_640x480.pixel_clock_khz) 292 print(mode_vga_660x480, 6*mode_vga_660x480.pixel_clock_khz) 293 294 print(mode_vga_720x400, 6*mode_vga_720x400.pixel_clock_khz) 295 print(mode_vga_660x400, 6*mode_vga_660x400.pixel_clock_khz) 296 297 def print_all(mode, h_divisor=1, out_instr="out pins, 2", cycles_per_pixel=6, file=sys.stdout): 298 program_name = f"vga_{mode.visible_width}x{mode.visible_height}_{mode.frame_rate_hz:.0f}" 299 print_pio_hsync_program(program_name, mode, h_divisor, cycles_per_pixel, file=file) 300 print("\n\n\n", file=file) 301 print_pio_vsync_program(program_name, mode, cycles_per_pixel, file=file) 302 print("\n\n\n", file=file) 303 print_pio_pixel_program(program_name, mode, out_instr, cycles_per_pixel, file=file) 304 305 with open("vga_660x480_60.pio", "wt", encoding="utf-8") as f: 306 print_all(mode_vga_660x480, file=f) 307 308 with open("vga_660x400_70.pio", "wt", encoding="utf-8") as f: 309 print_all(mode_vga_660x400, file=f)