/ 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)