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 /* Display state machine interrupt handler. */ 247 248 void vga_interrupt_handler(void) 249 { 250 uint32_t ifs; 251 252 if (!vga_display.line_channels) 253 { 254 /* Check for a timer interrupt condition. */ 255 256 ifs = REG(TIMERIFS) & TIMER_INT_FLAGS(vga_display.line_timer, TxIF); 257 258 if (ifs) 259 { 260 vga_transfer_interrupt_handler(); 261 CLR_REG(TIMERIFS, ifs); 262 } 263 } 264 265 /* Check for a OC1 interrupt condition. */ 266 267 ifs = REG(OCIFS) & OC_INT_FLAGS(1, OCxIF); 268 269 if (ifs) 270 { 271 vga_hsync_interrupt_handler(); 272 CLR_REG(OCIFS, ifs); 273 } 274 275 } 276 277 /* Display state machine interrupt handler. */ 278 279 void vga_hsync_interrupt_handler(void) 280 { 281 vga_display.line += 1; 282 vga_display.state_handler(); 283 } 284 285 /* Visible region pixel output handler, used when the CPU is responsible for 286 producing pixel output rather than the DMA channels. */ 287 288 void vga_transfer_interrupt_handler(void) 289 { 290 display_config_t *cfg = vga_display.display_config; 291 uint8_t *current, *end, *output; 292 293 if (vga_display.state_handler != visible_active) 294 return; 295 296 /* Generate the pixel signal. */ 297 298 output = (uint8_t *) vga_display.output; 299 end = vga_display.linedata + cfg->line_length; 300 301 /* This is potentially not as efficient as loading words and shifting bytes 302 but it appears difficult to implement that approach without experiencing 303 data load exceptions. */ 304 305 for (current = vga_display.linedata; current < end; current++) 306 REG(output) = *current; 307 308 /* Reset the signal level. */ 309 310 REG(output) = 0; 311 } 312 313 314 315 /* Vertical back porch region. */ 316 317 void vbp_active(void) 318 { 319 if (vga_display.line < vga_display.display_config->visible_start) 320 return; 321 322 /* Enter the visible region. */ 323 324 vga_display.state_handler = visible_active; 325 326 /* Set the line address. */ 327 328 vga_display.linedata = vga_display.display_config->screen_start; 329 330 if (vga_display.line_channels) 331 start_visible(); 332 } 333 334 /* Visible region. */ 335 336 void visible_active(void) 337 { 338 display_config_t *cfg = vga_display.display_config; 339 340 if (vga_display.line < cfg->vfp_start) 341 { 342 /* Update the line address and handle wraparound. */ 343 344 if (!(vga_display.line % cfg->line_multiplier)) 345 { 346 vga_display.linedata += cfg->line_length; 347 348 if (vga_display.linedata >= cfg->screen_limit) 349 vga_display.linedata -= cfg->screen_size; 350 } 351 352 if (vga_display.line_channels) 353 update_visible(); 354 return; 355 } 356 357 /* End the visible region. */ 358 359 vga_display.state_handler = vfp_active; 360 361 /* Disable the channel for the next line. */ 362 363 if (vga_display.line_channels) 364 stop_visible(); 365 } 366 367 /* Vertical front porch region. */ 368 369 void vfp_active(void) 370 { 371 if (vga_display.line < vga_display.display_config->vsync_start) 372 return; 373 374 /* Enter the vertical sync region. */ 375 376 vga_display.state_handler = vsync_active; 377 378 /* Bring vsync low when the next line starts. */ 379 380 vsync_low(); 381 } 382 383 /* Vertical sync region. */ 384 385 void vsync_active(void) 386 { 387 if (vga_display.line < vga_display.display_config->vsync_end) 388 return; 389 390 /* Start again at the top of the display. */ 391 392 vga_display.line = 0; 393 vga_display.state_handler = vbp_active; 394 395 /* Bring vsync high when the next line starts. */ 396 397 vsync_high(); 398 } 399 400 401 402 /* Enable the channels for the next line. */ 403 404 void start_visible(void) 405 { 406 update_visible(); 407 update_transfers(1); 408 } 409 410 /* Update the channels for the next line. */ 411 412 void update_visible(void) 413 { 414 uint32_t transfer_start = (uint32_t) vga_display.linedata; 415 uint32_t transfer_length = vga_display.display_config->line_length / 416 vga_display.line_channels; 417 418 /* Determine whether an initiating channel is used. */ 419 420 int initiating_channel = vga_display.transfer_int_num >= 0; 421 int channel = 0; 422 423 /* Update the source of a secondary line channel. */ 424 425 if (vga_display.line_channels == 2) 426 { 427 dma_set_source(channel, PHYSICAL(transfer_start), transfer_length); 428 transfer_start += transfer_length; 429 channel++; 430 } 431 432 /* Skip any initiating channel. */ 433 434 if (initiating_channel) 435 channel++; 436 437 /* Update the source of a line channel, with potentially updated start 438 address. */ 439 440 dma_set_source(channel, PHYSICAL(transfer_start), transfer_length); 441 } 442 443 /* Disable the channels for the next line. */ 444 445 void stop_visible(void) 446 { 447 update_transfers(0); 448 } 449 450 /* Enable or disable transfers. */ 451 452 void update_transfers(int enable) 453 { 454 void (*fn)() = enable ? dma_on : dma_off; 455 456 /* Determine whether an initiating channel is used. */ 457 458 int initiating_channel = vga_display.transfer_int_num >= 0; 459 int channel = 0; 460 461 /* Update line channels if no initiating channel is used. */ 462 463 if (vga_display.line_channels == 2) 464 { 465 if (!initiating_channel) 466 fn(channel); 467 channel++; 468 } 469 470 /* Update any initiating channel. */ 471 472 if (initiating_channel) 473 { 474 fn(channel); 475 channel++; 476 } 477 478 /* Update line channels if no initiating channel is used. */ 479 480 if (!initiating_channel) 481 fn(channel); 482 } 483 484 485 486 /* Bring vsync low (single compare, output driven low) when the next line 487 starts. */ 488 489 void vsync_low(void) 490 { 491 oc_init(vga_display.vsync_unit, 0b010, vga_display.line_timer); 492 oc_on(vga_display.vsync_unit); 493 } 494 495 /* Bring vsync high (single compare, output driven high) when the next line 496 starts. */ 497 498 void vsync_high(void) 499 { 500 oc_init(vga_display.vsync_unit, 0b001, vga_display.line_timer); 501 oc_on(vga_display.vsync_unit); 502 }