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 "init.h" 21 #include "vga_display.h" 22 23 24 25 /* Display state. */ 26 27 VGA_Display(vga_display); 28 29 /* Pixel data. */ 30 31 static const int ZERO_LENGTH = 1; 32 static const uint8_t zerodata[1] = {0}; 33 34 35 36 /* Initialise the state machine. */ 37 38 void init_vga(display_config_t *display_config, int line_channels, 39 int line_timer, int transfer_int_num) 40 { 41 /* Display parameters. */ 42 43 vga_display.display_config = display_config; 44 vga_display.line_channels = line_channels; 45 46 /* Initial state. */ 47 48 vga_display.state_handler = vbp_active; 49 vga_display.line = 0; 50 51 /* Configure a general display timer to start line data transfer and for the 52 horizontal sync. */ 53 54 vga_display.line_timer = line_timer; 55 56 /* Configure a separate transfer interrupt condition, if indicated. 57 Otherwise, transfers are initiated by the line timer. */ 58 59 vga_display.transfer_int_num = transfer_int_num; 60 61 /* Update the addresses used for the display. */ 62 63 vga_set_frame(display_config); 64 } 65 66 /* Initialise a separate transfer timer if different from the general display 67 timer. */ 68 69 void init_vga_with_timers(display_config_t *display_config, int line_channels, 70 int line_timer, int transfer_timer) 71 { 72 /* Initialise the basic properties of the display. */ 73 74 init_vga(display_config, line_channels, line_timer, 75 transfer_timer ? timer_interrupt_number(transfer_timer) : -1); 76 77 /* Configure a line timer for horizontal sync and line data transfers. */ 78 79 /* The timers have no prescaling (0). */ 80 81 timer_init(line_timer, 0, vga_display.hfreq_limit); 82 83 /* Enable interrupt requests when the CPU needs to perform the transfer, as 84 opposed to the DMA channels doing so. */ 85 86 if (!line_channels) 87 timer_init_interrupt(line_timer, 7, 3); 88 89 timer_on(line_timer); 90 91 /* Configure a separate transfer timer, if indicated. */ 92 93 if (!line_channels || !transfer_timer || (transfer_timer == line_timer)) 94 return; 95 96 /* The timer wraps around immediately. */ 97 98 timer_init(transfer_timer, 0, 1); 99 timer_on(transfer_timer); 100 } 101 102 103 104 /* Configure the transfer of pixel data. */ 105 106 void vga_configure_transfer(uint32_t output) 107 { 108 vga_display.output = output; 109 110 if (vga_display.line_channels) 111 vga_configure_dma_transfer(output); 112 } 113 114 /* Configure DMA channels for the transfer of pixel data. */ 115 116 void vga_configure_dma_transfer(uint32_t output) 117 { 118 int dual_channel = vga_display.line_channels == 2; 119 int channel = 0; 120 121 /* Determine whether an initiating channel is used. */ 122 123 int initiating_channel = vga_display.transfer_int_num >= 0; 124 125 /* Determine the different interrupt conditions, with pixel data employing 126 any specified transfer condition or the line condition otherwise. */ 127 128 int line_int_num = timer_interrupt_number(vga_display.line_timer); 129 int transfer_int_num = initiating_channel ? 130 vga_display.transfer_int_num : line_int_num; 131 132 /* Where dual line channels are involved, put the first before any 133 initiating channel, chaining it to such a channel. */ 134 135 if (dual_channel) 136 { 137 vga_configure_line_channel(channel++, transfer_int_num, 138 initiating_channel ? dma_chain_next : dma_chain_none, 139 output); 140 } 141 142 /* Introduce a special initiating channel if a separate transfer interrupt 143 has been indicated. */ 144 145 if (initiating_channel) 146 { 147 vga_configure_zero_channel(channel++, line_int_num, 1, output); 148 } 149 150 /* A line channel is always configured, chaining it to any initiating 151 channel. */ 152 153 vga_configure_line_channel(channel++, transfer_int_num, 154 initiating_channel ? dma_chain_previous : dma_chain_none, 155 output); 156 157 /* A zero channel is always configured, chaining it to the preceding line 158 channel, with the same transfer interrupt initiating the transfer. */ 159 160 vga_configure_zero_channel(channel, transfer_int_num, 0, output); 161 } 162 163 void vga_configure_line_channel(int channel, int int_num, enum dma_chain chain, 164 uint32_t output) 165 { 166 dma_init(channel, 2); 167 168 /* If initiating, configure to always be enabled. */ 169 170 if (chain == dma_chain_none) 171 dma_set_auto_enable(channel, 1); 172 173 /* Chain to either the previous or next channel where a special initiating 174 channel is used. */ 175 176 else 177 dma_set_chaining(channel, chain); 178 179 /* Initiate DMA on the transfer interrupt, transferring line data to the 180 first byte of the output word. */ 181 182 dma_set_interrupt(channel, int_num, 1); 183 184 dma_set_destination(channel, HW_PHYSICAL(output), 1); 185 dma_set_cell(channel, vga_display.transfer_cell_size); 186 } 187 188 void vga_configure_zero_channel(int channel, int int_num, int initiating, 189 uint32_t output) 190 { 191 dma_init(channel, 3); 192 193 /* If initiating, configure to always be enabled. */ 194 195 if (initiating) 196 dma_set_auto_enable(channel, 1); 197 198 /* Otherwise, chain to the preceding channel and register prior transfer 199 events before the channel is activated. */ 200 201 else 202 { 203 dma_set_chaining(channel, dma_chain_previous); 204 dma_set_receive_events(channel, 1); 205 } 206 207 /* Transfer upon the indicated interrupt condition. */ 208 209 dma_set_interrupt(channel, int_num, 1); 210 dma_set_transfer(channel, PHYSICAL((uint32_t) zerodata), 211 ZERO_LENGTH, 212 HW_PHYSICAL(output), 1, 213 ZERO_LENGTH); 214 } 215 216 /* Configure output compare units for horizontal and vertical sync. */ 217 218 void vga_configure_sync(int hsync_unit, int vsync_unit) 219 { 220 /* Record the peripherals in use. */ 221 222 vga_display.hsync_unit = hsync_unit; 223 vga_display.vsync_unit = vsync_unit; 224 225 /* Horizontal sync. */ 226 227 /* Configure output compare in dual compare (continuous output) mode using 228 the timer as time base. The interrupt condition drives the first DMA 229 channel and is handled to drive the display state machine. */ 230 231 oc_init(hsync_unit, 0b101, vga_display.line_timer); 232 oc_set_pulse(hsync_unit, vga_display.hsync_end); 233 oc_set_pulse_end(hsync_unit, vga_display.hsync_start); 234 oc_init_interrupt(hsync_unit, 7, 3); 235 oc_on(hsync_unit); 236 237 /* Vertical sync. */ 238 239 /* Configure output compare in single compare (output driven low) mode using 240 the timer as time base. The unit is enabled later. It is only really used 241 to achieve precisely-timed level transitions in hardware. */ 242 243 oc_init(vsync_unit, 0b010, vga_display.line_timer); 244 oc_set_pulse(vsync_unit, 0); 245 } 246 247 248 249 /* Update the display addresses from the general display configuration. */ 250 251 void vga_set_frame(display_config_t *display_config) 252 { 253 vga_wait_visible(); 254 255 vga_display.screen_start = display_config->screen_start; 256 vga_display.screen_limit = display_config->screen_limit; 257 vga_display.screen_size = display_config->screen_size; 258 } 259 260 /* Wait for the visible region to be completed. */ 261 262 void vga_wait_visible(void) 263 { 264 while (((volatile void (*)()) vga_display.state_handler) == visible_active) 265 __asm__ __volatile__("wait"); 266 } 267 268 269 270 /* Display state machine interrupt handler. */ 271 272 void vga_interrupt_handler(void) 273 { 274 uint32_t ifs; 275 276 if (!vga_display.line_channels) 277 { 278 /* Check for a timer interrupt condition. */ 279 280 ifs = REG(TIMERIFS) & TIMER_INT_FLAGS(vga_display.line_timer, TxIF); 281 282 if (ifs) 283 { 284 vga_transfer_interrupt_handler(); 285 CLR_REG(TIMERIFS, ifs); 286 } 287 } 288 289 /* Check for a OC1 interrupt condition. */ 290 291 ifs = REG(OCIFS) & OC_INT_FLAGS(1, OCxIF); 292 293 if (ifs) 294 { 295 vga_hsync_interrupt_handler(); 296 CLR_REG(OCIFS, ifs); 297 } 298 299 } 300 301 /* Display state machine interrupt handler. */ 302 303 void vga_hsync_interrupt_handler(void) 304 { 305 vga_display.line += 1; 306 vga_display.state_handler(); 307 } 308 309 /* Visible region pixel output handler, used when the CPU is responsible for 310 producing pixel output rather than the DMA channels. */ 311 312 void vga_transfer_interrupt_handler(void) 313 { 314 display_config_t *cfg = vga_display.display_config; 315 uint8_t *current, *end, *output; 316 317 if (vga_display.state_handler != visible_active) 318 return; 319 320 /* Generate the pixel signal. */ 321 322 output = (uint8_t *) vga_display.output; 323 end = vga_display.linedata + cfg->line_length; 324 325 /* This is potentially not as efficient as loading words and shifting bytes 326 but it appears difficult to implement that approach without experiencing 327 data load exceptions. */ 328 329 for (current = vga_display.linedata; current < end; current++) 330 REG(output) = *current; 331 332 /* Reset the signal level. */ 333 334 REG(output) = 0; 335 } 336 337 338 339 /* Vertical back porch region. */ 340 341 void vbp_active(void) 342 { 343 if (vga_display.line < vga_display.visible_start) 344 return; 345 346 /* Enter the visible region. */ 347 348 vga_display.state_handler = visible_active; 349 350 /* Set the line address. */ 351 352 vga_display.linedata = vga_display.screen_start; 353 354 if (vga_display.line_channels) 355 start_visible(); 356 } 357 358 /* Visible region. */ 359 360 void visible_active(void) 361 { 362 display_config_t *cfg = vga_display.display_config; 363 364 if (vga_display.line < vga_display.vfp_start) 365 { 366 /* Update the line address and handle wraparound. */ 367 368 if (!(vga_display.line % cfg->line_multiplier)) 369 { 370 vga_display.linedata += cfg->line_length; 371 372 if (vga_display.linedata >= vga_display.screen_limit) 373 vga_display.linedata -= vga_display.screen_size; 374 } 375 376 if (vga_display.line_channels) 377 update_visible(); 378 return; 379 } 380 381 /* End the visible region. */ 382 383 vga_display.state_handler = vfp_active; 384 385 /* Disable the channel for the next line. */ 386 387 if (vga_display.line_channels) 388 stop_visible(); 389 } 390 391 /* Vertical front porch region. */ 392 393 void vfp_active(void) 394 { 395 if (vga_display.line < vga_display.vsync_start) 396 return; 397 398 /* Enter the vertical sync region. */ 399 400 vga_display.state_handler = vsync_active; 401 402 /* Bring vsync low when the next line starts. */ 403 404 vsync_low(); 405 } 406 407 /* Vertical sync region. */ 408 409 void vsync_active(void) 410 { 411 if (vga_display.line < vga_display.vsync_end) 412 return; 413 414 /* Start again at the top of the display. */ 415 416 vga_display.line = 0; 417 vga_display.state_handler = vbp_active; 418 419 /* Bring vsync high when the next line starts. */ 420 421 vsync_high(); 422 } 423 424 425 426 /* Enable the channels for the next line. */ 427 428 void start_visible(void) 429 { 430 update_visible(); 431 update_transfers(1); 432 } 433 434 /* Update the channels for the next line. */ 435 436 void update_visible(void) 437 { 438 uint32_t transfer_start = (uint32_t) vga_display.linedata; 439 uint32_t transfer_length = vga_display.display_config->line_length / 440 vga_display.line_channels; 441 442 /* Determine whether an initiating channel is used. */ 443 444 int initiating_channel = vga_display.transfer_int_num >= 0; 445 int channel = 0; 446 447 /* Update the source of a secondary line channel. */ 448 449 if (vga_display.line_channels == 2) 450 { 451 dma_set_source(channel, PHYSICAL(transfer_start), transfer_length); 452 transfer_start += transfer_length; 453 channel++; 454 } 455 456 /* Skip any initiating channel. */ 457 458 if (initiating_channel) 459 channel++; 460 461 /* Update the source of a line channel, with potentially updated start 462 address. */ 463 464 dma_set_source(channel, PHYSICAL(transfer_start), transfer_length); 465 } 466 467 /* Disable the channels for the next line. */ 468 469 void stop_visible(void) 470 { 471 update_transfers(0); 472 } 473 474 /* Enable or disable transfers. */ 475 476 void update_transfers(int enable) 477 { 478 void (*fn)() = enable ? dma_on : dma_off; 479 480 /* Determine whether an initiating channel is used. */ 481 482 int initiating_channel = vga_display.transfer_int_num >= 0; 483 int channel = 0; 484 485 /* Update line channels if no initiating channel is used. */ 486 487 if (vga_display.line_channels == 2) 488 { 489 if (!initiating_channel) 490 fn(channel); 491 channel++; 492 } 493 494 /* Update any initiating channel. */ 495 496 if (initiating_channel) 497 { 498 fn(channel); 499 channel++; 500 } 501 502 /* Update line channels if no initiating channel is used. */ 503 504 if (!initiating_channel) 505 fn(channel); 506 } 507 508 509 510 /* Bring vsync low (single compare, output driven low) when the next line 511 starts. */ 512 513 void vsync_low(void) 514 { 515 oc_init(vga_display.vsync_unit, 0b010, vga_display.line_timer); 516 oc_on(vga_display.vsync_unit); 517 } 518 519 /* Bring vsync high (single compare, output driven high) when the next line 520 starts. */ 521 522 void vsync_high(void) 523 { 524 oc_init(vga_display.vsync_unit, 0b001, vga_display.line_timer); 525 oc_on(vga_display.vsync_unit); 526 }