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