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