CommonPIC32

Annotated lib/vga_display.c

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