cr100/vgamode.py

408 lines
12 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#!/usr/bin/env python
import sys
from dataclasses import dataclass, replace
class Polarity:
Negative = 0
Positive = 1
@dataclass(frozen=True)
class VideoMode:
pixel_clock_khz: int
visible_width: int
hfront_porch: int
hsync_pulse: int
hback_porch: int
hsync_polarity: Polarity
@property
def total_width(self):
return (
self.visible_width + self.hfront_porch + self.hsync_pulse + self.hback_porch
)
@property
def line_rate_khz(self):
return self.pixel_clock_khz / self.total_width
@property
def line_time_us(self):
return 1000 / self.line_rate_khz
@property
def visible_line_time_us(self):
return 1000 * self.visible_width / self.pixel_clock_khz
visible_height: int
vfront_porch: int
vsync_pulse: int
vback_porch: int
vsync_polarity: Polarity
@property
def total_lines(self):
return (
self.visible_height
+ self.vfront_porch
+ self.vsync_pulse
+ self.vback_porch
)
@property
def frame_rate_hz(self):
return self.line_rate_khz / self.total_lines * 1000
@property
def frame_time_ms(self):
return 1000 / self.frame_rate_hz
def __repr__(self):
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>"
def change_visible_width(mode_in, new_w, new_clock=None):
print(mode_in)
if new_clock is None:
new_clock = mode_in.pixel_clock_khz * new_w / mode_in.visible_width
new_mode = replace(
mode_in,
pixel_clock_khz=new_clock,
visible_width=new_w,
)
print(new_mode)
ratio = new_clock / mode_in.pixel_clock_khz
new_line_counts = round(mode_in.total_width * ratio)
print(new_line_counts, mode_in.total_width * ratio)
new_pulse = round(mode_in.hsync_pulse * ratio)
new_porch_counts = new_line_counts - new_pulse - new_w
porch_ratio = mode_in.hfront_porch / (mode_in.hfront_porch + mode_in.hback_porch)
new_front = round(new_porch_counts * porch_ratio)
new_back = new_porch_counts - new_front
print(
(mode_in.hfront_porch, mode_in.hsync_pulse, mode_in.hback_porch),
"->",
(new_front, new_pulse, new_back),
)
new_mode = replace(
new_mode, hfront_porch=new_front, hsync_pulse=new_pulse, hback_porch=new_back
)
print(new_mode)
print()
return new_mode
def change_visible_height(mode_in, new_h):
delta_rows = mode_in.visible_height - new_h
delta_front_porch = delta_rows // 2
delta_back_porch = delta_rows - delta_front_porch
new_mode = replace(
mode_in,
visible_height=new_h,
vfront_porch=mode_in.vfront_porch + delta_front_porch,
vback_porch=mode_in.vback_porch + delta_back_porch,
)
print(new_mode)
print()
assert new_mode.total_lines == mode_in.total_lines
return new_mode
def pio_hard_delay(instr, n, file):
assert n > 0
assert n < 128
while n > 0:
cycles = min(n, 32)
print(f" {instr} [{cycles-1}]", file=file)
n -= cycles
print(file=file)
def print_pio_hsync_program(
program_name_base, mode, h_divisor, cycles_per_pixel, file=sys.stdout
):
net_khz = mode.pixel_clock_khz / h_divisor
err = (mode.visible_width + mode.hfront_porch) % h_divisor
print(
f"""
; Horizontal sync program for {mode}
; PIO clock frequency = {mode.pixel_clock_khz:.1f}/{h_divisor}khz = {net_khz:.1f}
;
.program {program_name_base}_hsync
.wrap_target ; Program wraps to here
; ACTIVE + FRONTPORCH {mode.visible_width} + {mode.hfront_porch} error {err}
mov x, osr ; Copy value from OSR to x scratch register
activeporch:
jmp x-- activeporch ; Remain high in active mode and front porch
""",
file=file,
)
cycles, err = divmod(mode.hsync_pulse + err, h_divisor)
print(
f"syncpulse: ; {mode.hsync_pulse}/{h_divisor} clocks [actual {cycles} error {err}]",
file=file,
)
pio_hard_delay(f"set pins, {mode.hsync_polarity:d}", cycles, file=file)
cycles, err = divmod(mode.hback_porch + err, h_divisor)
print(
f"backporch: ; {mode.hback_porch}/{h_divisor} clocks [actual {cycles} error {err}]",
file=file,
)
pio_hard_delay(f"set pins, {not mode.hsync_polarity:d}", cycles - 1, file=file)
print(" irq 0 [1]", file=file)
print(".wrap", file=file)
print(
f"""
% c-sdk {{
static inline void {program_name_base}_hsync_program_init(PIO pio, uint sm, uint offset, uint pin) {{
pio_sm_config c = {program_name_base}_hsync_program_get_default_config(offset);
// Map the state machine's SET pin group to one pin, namely the `pin`
// parameter to this function.
sm_config_set_set_pins(&c, pin, 1);
sm_config_set_clkdiv_int_frac(&c, {cycles_per_pixel * h_divisor}, 0);
// Set this pin's GPIO function (connect PIO to the pad)
pio_gpio_init(pio, pin);
// Set the pin direction to output at the PIO
pio_sm_set_consecutive_pindirs(pio, sm, pin, 1, true);
// Load our configuration, and jump to the start of the program
pio_sm_init(pio, sm, offset, &c);
// Set up the value in the OSR register
pio_sm_put(pio, sm, {mode.visible_width} + {mode.hfront_porch} - 1);
pio_sm_exec_wait_blocking(pio, sm, pio_encode_pull(false, true));
// Set the state machine running
pio_sm_set_enabled(pio, sm, true);
}}
%}}
""",
file=file,
)
def pio_yloop(instr, n, label, comment, file):
assert n <= 65
if n < 3:
print(f"{label}: ; {comment}", file=file)
for i in range(n):
print(f" {instr}", file=file)
elif n <= 33:
print(f" set y, {n-1 if n <= 32 else 31}", file=file)
print(f"{label}: ; {comment}", file=file)
print(f" {instr}", file=file)
print(f" jmp y--, {label}", file=file)
if n == 33:
print(f" {instr}", file=file)
elif n <= 65:
print(f" set y, {(n-2)//2}", file=file)
print(f"{label}: ; {comment}", file=file)
print(f" {instr}", file=file)
print(f" {instr}", file=file)
print(f" jmp y--, {label}", file=file)
if n & 1:
print(f" {instr}", file=file)
print(file=file)
def print_pio_vsync_program(program_name_base, mode, cycles_per_pixel, file=sys.stdout):
print(
f"""
.program {program_name_base}_vsync
.side_set 1 opt
; Vertical sync program for {mode}
;
.wrap_target ; Program wraps to here
""",
file=file,
)
pio_yloop(
"wait 1 irq 0",
mode.vfront_porch,
"frontporch",
f"{mode.vfront_porch} lines",
file=file,
)
pio_yloop(
f"wait 1 irq 0 side {mode.vsync_polarity:d}",
mode.vsync_pulse,
"syncpulse",
f"{mode.vsync_pulse} lines",
file=file,
)
pio_yloop(
f"wait 1 irq 0 side {not mode.vsync_polarity:d}",
mode.vback_porch,
"backporch",
f"{mode.vback_porch} lines",
file=file,
)
print(
"""
; ACTIVE
mov x, osr ; Copy value from OSR to x scratch register
active:
wait 1 irq 0 ; Wait for hsync to go high
irq 1 ; Signal that we're in active mode
jmp x-- active ; Remain in active mode, decrementing counter
""",
file=file,
)
print(".wrap", file=file)
print(
f"""
% c-sdk {{
static inline void {program_name_base}_vsync_program_init(PIO pio, uint sm, uint offset, uint pin) {{
pio_sm_config c = {program_name_base}_vsync_program_get_default_config(offset);
// Map the state machine's SIDESET to one pin, namely the `pin`
// parameter to this function.
// sm_config_set_sideset(&c, 1, true, false);
sm_config_set_sideset_pins(&c, pin);
sm_config_set_sideset(&c, 2, true, false);
sm_config_set_clkdiv_int_frac(&c, {cycles_per_pixel}, 0);
// Set this pin's GPIO function (connect PIO to the pad)
pio_gpio_init(pio, pin);
// Set the pin direction to output at the PIO
pio_sm_set_consecutive_pindirs(pio, sm, pin, 1, true);
// Load our configuration, and jump to the start of the program
pio_sm_init(pio, sm, offset, &c);
// Set up the value in the OSR register
pio_sm_put(pio, sm, {mode.visible_height} - 1);
pio_sm_exec_wait_blocking(pio, sm, pio_encode_pull(false, true));
// Set the state machine running
pio_sm_set_enabled(pio, sm, true);
}}
%}}
""",
file=file,
)
def print_pio_pixel_program(
program_name_base, mode, out_instr, cycles_per_pixel, file=sys.stdout
):
net_khz = cycles_per_pixel * mode.pixel_clock_khz
assert cycles_per_pixel >= 2
print(
f"""
.program {program_name_base}_pixel
; Pixel generator program for {mode}
; PIO clock frequency = {cycles_per_pixel}×{mode.pixel_clock_khz}khz = {net_khz}
.wrap_target
set pins, 0 ; Zero RGB pins in blanking
mov x, y ; Initialize counter variable
wait 1 irq 1 [{cycles_per_pixel-1}] ; wait for vsync active mode
colorout:
{out_instr} [{cycles_per_pixel-2}]
jmp x-- colorout ; Stay here thru horizontal active mode
.wrap""",
file=file,
)
print(
f"""
% c-sdk {{
enum {{ {program_name_base}_pixel_clock_khz = {mode.pixel_clock_khz}, {program_name_base}_sys_clock_khz = {cycles_per_pixel * mode.pixel_clock_khz} }};
static inline void {program_name_base}_pixel_program_init(PIO pio, uint sm, uint offset, uint pin, uint n_pin) {{
pio_sm_config c = {program_name_base}_pixel_program_get_default_config(offset);
// Map the state machine's OUT & SET pins
sm_config_set_out_pins(&c, pin, n_pin);
sm_config_set_set_pins(&c, pin, n_pin);
sm_config_set_fifo_join(&c, PIO_FIFO_JOIN_TX);
// 15 pixels (30 bits) per word, left is MSB
sm_config_set_out_shift(&c, false, true, 30);
// Set this pin's GPIO function (connect PIO to the pad)
for(uint i=0; i<n_pin; i++) {{
// gpio_set_slew_rate(pin + i, GPIO_SLEW_RATE_FAST);
gpio_set_drive_strength(pin + i, GPIO_DRIVE_STRENGTH_8MA);
pio_gpio_init(pio, pin + i);
}}
// Set the pin direction to output at the PIO
pio_sm_set_consecutive_pindirs(pio, sm, pin, n_pin, true);
// Load our configuration, and jump to the start of the program
pio_sm_init(pio, sm, offset, &c);
// Set up the value in the Y register
pio_sm_put(pio, sm, {mode.visible_width} - 1);
pio_sm_exec_wait_blocking(pio, sm, pio_encode_pull(false, true));
pio_sm_exec_wait_blocking(pio, sm, pio_encode_mov(pio_y, pio_osr));
pio_sm_exec_wait_blocking(pio, sm, pio_encode_out(pio_null, 30));
// Set the state machine running
pio_sm_set_enabled(pio, sm, true);
}}
%}}
""",
file=file,
)
mode_vga_640x480 = VideoMode(
25_175, 640, 16, 96, 48, Polarity.Negative, 480, 10, 2, 33, Polarity.Negative
)
mode_vga_660x480 = change_visible_width(mode_vga_640x480, 660, 26_000)
mode_vga_660x477 = change_visible_height(mode_vga_660x480, 477)
mode_vga_720x400 = VideoMode(
28_321, 720, 18, 108, 54, Polarity.Negative, 400, 10, 2, 36, Polarity.Positive
)
mode_vga_660x400 = change_visible_width(mode_vga_720x400, 660, 26_000)
if 0:
print(mode_vga_640x480, 6 * mode_vga_640x480.pixel_clock_khz)
print(mode_vga_660x480, 6 * mode_vga_660x480.pixel_clock_khz)
print(mode_vga_720x400, 6 * mode_vga_720x400.pixel_clock_khz)
print(mode_vga_660x400, 6 * mode_vga_660x400.pixel_clock_khz)
def print_all(
mode, h_divisor=1, out_instr="out pins, 2", cycles_per_pixel=6, file=sys.stdout
):
program_name = (
f"vga_{mode.visible_width}x{mode.visible_height}_{mode.frame_rate_hz:.0f}"
)
print_pio_hsync_program(program_name, mode, h_divisor, cycles_per_pixel, file=file)
print("\n\n\n", file=file)
print_pio_vsync_program(program_name, mode, cycles_per_pixel, file=file)
print("\n\n\n", file=file)
print_pio_pixel_program(program_name, mode, out_instr, cycles_per_pixel, file=file)
with open("vga_660x477_60.pio", "wt", encoding="utf-8") as f:
print_all(mode_vga_660x477, file=f)