1 /* 2 * Generate a VGA signal using a PIC32 microcontroller. 3 * 4 * Copyright (C) 2017, 2018 Paul Boddie <paul@boddie.org.uk> 5 * 6 * This program is free software: you can redistribute it and/or modify 7 * it under the terms of the GNU General Public License as published by 8 * the Free Software Foundation, either version 3 of the License, or 9 * (at your option) any later version. 10 * 11 * This program is distributed in the hope that it will be useful, 12 * but WITHOUT ANY WARRANTY; without even the implied warranty of 13 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 14 * GNU General Public License for more details. 15 * 16 * You should have received a copy of the GNU General Public License 17 * along with this program. If not, see <http://www.gnu.org/licenses/>. 18 */ 19 20 #include "pic32_c.h" 21 #include "init.h" 22 #include "vga_display.h" 23 24 25 26 /* Display state. */ 27 28 vga_display_t vga_display; 29 30 /* Pixel data. */ 31 32 static const int ZERO_LENGTH = 1; 33 static const uint8_t zerodata[1] = {0}; 34 35 36 37 /* Initialise the state machine. */ 38 39 void init_vga(display_config_t *display_config, int line_channels, 40 int line_timer, int transfer_int_num) 41 { 42 /* Display parameters. */ 43 44 vga_display.display_config = display_config; 45 vga_display.line_channels = line_channels; 46 47 /* Initial state. */ 48 49 vga_display.state_handler = vbp_active; 50 vga_display.line = 0; 51 52 /* Configure a general display timer to start line data transfer and for the 53 horizontal sync. */ 54 55 vga_display.line_timer = line_timer; 56 57 /* Configure a separate transfer interrupt condition, if indicated. 58 Otherwise, transfers are initiated by the line timer. */ 59 60 vga_display.transfer_int_num = transfer_int_num; 61 } 62 63 /* Initialise a separate transfer timer if different from the general display 64 timer. */ 65 66 void init_vga_with_timers(display_config_t *display_config, int line_channels, 67 int line_timer, int transfer_timer) 68 { 69 /* Initialise the basic properties of the display. */ 70 71 init_vga(display_config, line_channels, line_timer, 72 transfer_timer ? timer_interrupt_number(transfer_timer) : -1); 73 74 /* Configure a line timer for horizontal sync and line data transfers. */ 75 76 /* The timers have no prescaling (0). */ 77 78 timer_init(line_timer, 0, display_config->hfreq_limit); 79 timer_on(line_timer); 80 81 /* Configure a separate transfer timer, if indicated. */ 82 83 if (!transfer_timer || (transfer_timer == line_timer)) 84 return; 85 86 /* The timer wraps around immediately. */ 87 88 timer_init(transfer_timer, 0, 1); 89 timer_on(transfer_timer); 90 } 91 92 93 94 /* Configure DMA channels for the transfer of pixel data. */ 95 96 void vga_configure_transfer(uint32_t output) 97 { 98 int dual_channel = vga_display.line_channels == 2; 99 int channel = 0; 100 101 /* Determine whether an initiating channel is used. */ 102 103 int initiating_channel = vga_display.transfer_int_num >= 0; 104 105 /* Determine the different interrupt conditions, with pixel data employing 106 any specified transfer condition or the line condition otherwise. */ 107 108 int line_int_num = timer_interrupt_number(vga_display.line_timer); 109 int transfer_int_num = initiating_channel ? 110 vga_display.transfer_int_num : line_int_num; 111 112 /* Where dual line channels are involved, put the first before any 113 initiating channel, chaining it to such a channel. */ 114 115 if (dual_channel) 116 { 117 vga_configure_line_channel(channel++, transfer_int_num, 118 initiating_channel ? dma_chain_next : dma_chain_none, 119 output); 120 } 121 122 /* Introduce a special initiating channel if a separate transfer interrupt 123 has been indicated. */ 124 125 if (initiating_channel) 126 { 127 vga_configure_zero_channel(channel++, line_int_num, 1, output); 128 } 129 130 /* A line channel is always configured, chaining it to any initiating 131 channel. */ 132 133 vga_configure_line_channel(channel++, transfer_int_num, 134 initiating_channel ? dma_chain_previous : dma_chain_none, 135 output); 136 137 /* A zero channel is always configured, chaining it to the preceding line 138 channel, with the same transfer interrupt initiating the transfer. */ 139 140 vga_configure_zero_channel(channel, transfer_int_num, 0, output); 141 } 142 143 void vga_configure_line_channel(int channel, int int_num, enum dma_chain chain, 144 uint32_t output) 145 { 146 dma_init(channel, 2); 147 148 /* If initiating, configure to always be enabled. */ 149 150 if (chain == dma_chain_none) 151 dma_set_auto_enable(channel, 1); 152 153 /* Chain to either the previous or next channel where a special initiating 154 channel is used. */ 155 156 else 157 dma_set_chaining(channel, chain); 158 159 /* Initiate DMA on the transfer interrupt, transferring line data to the 160 first byte of the output word. */ 161 162 dma_set_interrupt(channel, int_num, 1); 163 164 dma_set_destination(channel, HW_PHYSICAL(output), 1); 165 dma_set_cell(channel, vga_display.display_config->transfer_cell_size); 166 } 167 168 void vga_configure_zero_channel(int channel, int int_num, int initiating, 169 uint32_t output) 170 { 171 dma_init(channel, 3); 172 173 /* If initiating, configure to always be enabled. */ 174 175 if (initiating) 176 dma_set_auto_enable(channel, 1); 177 178 /* Otherwise, chain to the preceding channel and register prior transfer 179 events before the channel is activated. */ 180 181 else 182 { 183 dma_set_chaining(channel, dma_chain_previous); 184 dma_set_receive_events(channel, 1); 185 } 186 187 /* Transfer upon the indicated interrupt condition. */ 188 189 dma_set_interrupt(channel, int_num, 1); 190 dma_set_transfer(channel, PHYSICAL((uint32_t) zerodata), 191 ZERO_LENGTH, 192 HW_PHYSICAL(output), 1, 193 ZERO_LENGTH); 194 } 195 196 /* Configure output compare units for horizontal and vertical sync. */ 197 198 void vga_configure_sync(int hsync_unit, int vsync_unit) 199 { 200 /* Record the peripherals in use. */ 201 202 vga_display.hsync_unit = hsync_unit; 203 vga_display.vsync_unit = vsync_unit; 204 205 /* Horizontal sync. */ 206 207 /* Configure output compare in dual compare (continuous output) mode using 208 the timer as time base. The interrupt condition drives the first DMA 209 channel and is handled to drive the display state machine. */ 210 211 oc_init(hsync_unit, 0b101, vga_display.line_timer); 212 oc_set_pulse(hsync_unit, vga_display.display_config->hsync_end); 213 oc_set_pulse_end(hsync_unit, vga_display.display_config->hsync_start); 214 oc_init_interrupt(hsync_unit, 7, 3); 215 oc_on(hsync_unit); 216 217 /* Vertical sync. */ 218 219 /* Configure output compare in single compare (output driven low) mode using 220 the timer as time base. The unit is enabled later. It is only really used 221 to achieve precisely-timed level transitions in hardware. */ 222 223 oc_init(vsync_unit, 0b010, vga_display.line_timer); 224 oc_set_pulse(vsync_unit, 0); 225 } 226 227 228 229 /* Interrupt handlers. */ 230 231 void vga_interrupt_handler(void) 232 { 233 vga_display.line += 1; 234 vga_display.state_handler(); 235 } 236 237 238 239 /* Vertical back porch region. */ 240 241 void vbp_active(void) 242 { 243 if (vga_display.line < vga_display.display_config->visible_start) 244 return; 245 246 /* Enter the visible region. */ 247 248 vga_display.state_handler = visible_active; 249 250 /* Set the line address. */ 251 252 vga_display.linedata = vga_display.display_config->screen_start; 253 start_visible(); 254 } 255 256 /* Visible region. */ 257 258 void visible_active(void) 259 { 260 if (vga_display.line < vga_display.display_config->vfp_start) 261 { 262 /* Update the line address and handle wraparound. */ 263 264 if (!(vga_display.line % vga_display.display_config->line_multiplier)) 265 { 266 vga_display.linedata += vga_display.display_config->line_length; 267 268 if (vga_display.linedata >= vga_display.display_config->screen_limit) 269 vga_display.linedata -= vga_display.display_config->screen_size; 270 } 271 272 update_visible(); 273 return; 274 } 275 276 /* End the visible region. */ 277 278 vga_display.state_handler = vfp_active; 279 280 /* Disable the channel for the next line. */ 281 282 stop_visible(); 283 } 284 285 /* Vertical front porch region. */ 286 287 void vfp_active(void) 288 { 289 if (vga_display.line < vga_display.display_config->vsync_start) 290 return; 291 292 /* Enter the vertical sync region. */ 293 294 vga_display.state_handler = vsync_active; 295 296 /* Bring vsync low when the next line starts. */ 297 298 vsync_low(); 299 } 300 301 /* Vertical sync region. */ 302 303 void vsync_active(void) 304 { 305 if (vga_display.line < vga_display.display_config->vsync_end) 306 return; 307 308 /* Start again at the top of the display. */ 309 310 vga_display.line = 0; 311 vga_display.state_handler = vbp_active; 312 313 /* Bring vsync high when the next line starts. */ 314 315 vsync_high(); 316 } 317 318 319 320 /* Enable the channels for the next line. */ 321 322 void start_visible(void) 323 { 324 update_visible(); 325 update_transfers(1); 326 } 327 328 /* Update the channels for the next line. */ 329 330 void update_visible(void) 331 { 332 uint32_t transfer_start = (uint32_t) vga_display.linedata; 333 uint32_t transfer_length = vga_display.display_config->line_length / 334 vga_display.line_channels; 335 336 /* Determine whether an initiating channel is used. */ 337 338 int initiating_channel = vga_display.transfer_int_num >= 0; 339 int channel = 0; 340 341 /* Update the source of a secondary line channel. */ 342 343 if (vga_display.line_channels == 2) 344 { 345 dma_set_source(channel, PHYSICAL(transfer_start), transfer_length); 346 transfer_start += transfer_length; 347 channel++; 348 } 349 350 /* Skip any initiating channel. */ 351 352 if (initiating_channel) 353 channel++; 354 355 /* Update the source of a line channel, with potentially updated start 356 address. */ 357 358 dma_set_source(channel, PHYSICAL(transfer_start), transfer_length); 359 } 360 361 /* Disable the channels for the next line. */ 362 363 void stop_visible(void) 364 { 365 update_transfers(0); 366 } 367 368 /* Enable or disable transfers. */ 369 370 void update_transfers(int enable) 371 { 372 void (*fn)() = enable ? dma_on : dma_off; 373 374 /* Determine whether an initiating channel is used. */ 375 376 int initiating_channel = vga_display.transfer_int_num >= 0; 377 int channel = 0; 378 379 /* Update line channels if no initiating channel is used. */ 380 381 if (vga_display.line_channels == 2) 382 { 383 if (!initiating_channel) 384 fn(channel); 385 channel++; 386 } 387 388 /* Update any initiating channel. */ 389 390 if (initiating_channel) 391 { 392 fn(channel); 393 channel++; 394 } 395 396 /* Update line channels if no initiating channel is used. */ 397 398 if (!initiating_channel) 399 fn(channel); 400 } 401 402 403 404 /* Bring vsync low (single compare, output driven low) when the next line 405 starts. */ 406 407 void vsync_low(void) 408 { 409 oc_init(vga_display.vsync_unit, 0b010, vga_display.line_timer); 410 oc_on(vga_display.vsync_unit); 411 } 412 413 /* Bring vsync high (single compare, output driven high) when the next line 414 starts. */ 415 416 void vsync_high(void) 417 { 418 oc_init(vga_display.vsync_unit, 0b001, vga_display.line_timer); 419 oc_on(vga_display.vsync_unit); 420 }