PIC18 Direct Memory Access w/XC8 Compiler

Status
Not open for further replies.

Jon Wilder

Active Member
Hi all.

I recently purchased the all new PIC18F27Q84. This PIC has 8 DMA controllers on it. Does the compiler automatically use the DMA hardware if it sees a purpose for it (like for memory to memory transfers)? Or must I manually code it in?
 
Never used that device, but as far as I'm aware you'll have to program the DMA yourself?.
 
Never used that device, but as far as I'm aware you'll have to program the DMA yourself?.

OK...not an issue if I do have to. I just wasn't sure if it was something the compiler automatically used, like it does the FSR/INDF register pair when you define multi-byte arrays and such.
 
The library for the EEPROM/flash read/write doesn't do the actual write so I suspect the DMA will be the same.

Mike.
 
Example applications using DMA:


The DMA block operation is the same as the Q43
 
If I'm not wandering too far off-topic... may I ask if anyone has seen an example of generating NTSC monochrome video from a block of RAM using DMA on a Q43 device, please?

TIA... Stay safe. Happy Holidays. Cheerful regards, Mike
 
If I'm not wandering too far off-topic... may I ask if anyone has seen an example of generating NTSC monochrome video from a block of RAM using DMA on a Q43 device, please?

TIA... Stay safe. Happy Holidays. Cheerful regards, Mike

I haven't seen one but setting up the needed DMA transfer trigger events using timers for the H/V timing and video pixel clock should be straight forward if you've ever built a discrete NTSC video generator. There is also a 8-Bit DAC with a 10us settling time for grey scale and maybe color if you want to get fancy.
 
Here's a quick and dirty proof of concept of using DMA to the 8-bit DAC for NTSC sync. The Q43 DAC is too slow for real video generation so this is a little less than half speed generation example just to show a possible way using the MCC generator. I need to play with the code speed time to get it to actually sync on a real display.


V sync with stair-step video signal.

H and V sync
C:
void DMA5_Initialize(void)
{
    //DMA Instance Selection : 0x04
    DMASELECT = 0x04;
    //Source Address : vsync
    DMAnSSA = &vsync;
    //Destination Address : &DAC1DATL
    DMAnDSA = &DAC1DATL;
    //DMODE unchanged; DSTP not cleared; SMR GPR; SMODE incremented; SSTP not cleared;
    DMAnCON1 = 0x02;
    //Source Message Size : 31
    DMAnSSZ = 31;
    //Destination Message Size : 1
    DMAnDSZ = 1;
    //Start Trigger : SIRQ TMR4;
    DMAnSIRQ = 0x5B;
    //Abort Trigger : AIRQ None;
    DMAnAIRQ = 0x00;
  
    // Clear Destination Count Interrupt Flag bit
    PIR12bits.DMA5DCNTIF = 0;
    // Clear Source Count Interrupt Flag bit
    PIR12bits.DMA5SCNTIF = 0;
    // Clear Abort Interrupt Flag bit
    PIR12bits.DMA5AIF = 0;
    // Clear Overrun Interrupt Flag bit
    PIR12bits.DMA5ORIF =0;
  
    PIE12bits.DMA5DCNTIE = 1;
    DMA5_SetDCNTIInterruptHandler(DMA5_DefaultInterruptHandler);
    PIE12bits.DMA5SCNTIE = 1;
    DMA5_SetSCNTIInterruptHandler(DMA5_DefaultInterruptHandler);
    PIE12bits.DMA5AIE = 0;
    PIE12bits.DMA5ORIE = 0;
  
    //EN enabled; SIRQEN enabled; DGO not in progress; AIRQEN disabled;
    DMAnCON0 = 0xC0;
  
}

C:
/*
* File:   ntsc.h
* Author: root
*
* Created on December 20, 2020, 10:05 AM
*/

#ifndef NTSC_H
#define    NTSC_H

#ifdef    __cplusplus
extern "C" {
#endif
#include <xc.h>
#include "mcc_generated_files/dma5.h"
#include "mcc_generated_files/tmr4.h"
#include "mcc_generated_files/dac1.h"


#define SYNC_LEVEL    0
#define BLANK_LEVEL    15
#define BLACK_LEVEL    25
#define VIDEO_LEVEL    50

    extern uint8_t vsync[256];
    extern uint8_t hsync[256];
    extern volatile uint32_t vcounts, vfcounts;

    void ntsc_init(void);

#ifdef    __cplusplus
}
#endif

#endif    /* NTSC_H */
C:
#include "ntsc.h"

volatile uint32_t vcounts = 0, vfcounts = 0;
volatile bool h_mode = true, mode_init = false; // horizonal scan

void vcntd(void);
void vcnts(void);
void vint(void);

void ntsc_init(void)
{
    uint8_t count = 0, vramp = BLACK_LEVEL;

    TMR4_Start();
    TMR4_StopTimer(); // init timer 4 as the data pacer for DAC updates
    DMA5_SetDCNTIInterruptHandler(vcntd);
    DMA5_SetSCNTIInterruptHandler(vcnts);

    /*
     * setup the static V, H and video patterns for DMA and TM4 clocking
     */
    for (count = 0; count < 4; count++) {
        vsync[count] = SYNC_LEVEL;
        hsync[count] = BLANK_LEVEL;
    }

    for (count = 4; count < 8; count++) {
        vsync[count] = BLANK_LEVEL;
        hsync[count] = SYNC_LEVEL;
    }

    for (count = 8; count < 25; count++) {
        vsync[count] = vramp;
        hsync[count] = SYNC_LEVEL;
        vramp = vramp + 2;
        if (vramp > VIDEO_LEVEL) {
            vramp = VIDEO_LEVEL;
        }
    }
    for (count = 25; count < 40; count++) {
        vsync[count] = BLANK_LEVEL;
        hsync[count] = SYNC_LEVEL;
    }

    DMA5_StopTransfer();
    DAC1_SetOutput(128);
    TMR4_SetInterruptHandler(vint); // set dma start trigger ISR
    TMR4_Start(); // get the ball rolling
}

void vcntd(void)  // each DMA transfer code, 31 total bytes
{
    vcounts++;
}

void vcnts(void) // each scan line interrupt code, 262 total for scan lines and V sync
{
    vfcounts++;
    if (h_mode) { // Horizontal sync (hsync) pulse: Start each scanline with 0.3V, then 0V for 4.7us (microseconds), and then back to 0.3V.
        if (vfcounts >= 243) {
            vcounts = 0;
            vfcounts = 0;
            h_mode = false;
            mode_init = true;
        }
        if (mode_init) {
            mode_init = false;
            DMASELECT = 0x04;
            DMAnCON0bits.EN = 0;
            DMAnSSA = &hsync;
            DMAnCON0bits.EN = 1;
        }
    } else { // Vertical sync (vsync) pulse: Lines 243-262 of each frame (off the bottom of the TV) start with 0.3V for 4.7us, and the rest is 0V.
        if (vfcounts >= 20) {
            vcounts = 0;
            vfcounts = 0;
            h_mode = true;
            mode_init = true;
        }
        if (mode_init) {
            mode_init = false;
            DMASELECT = 0x04;
            DMAnCON0bits.EN = 0;
            DMAnSSA = &vsync;
            DMAnCON0bits.EN = 1;
        }
    }
}

void vint(void)  // timer 4 interrupt, dma trigger
{
    DMA5_StartTransfer();
}
 
Last edited:
The prime problem with DMA based data transfers that have time constraints like NTSC signals is the pacing method as pure software triggered DMA runs at the memory bus speed between modules, flash, eeprom or ram. In my example above Timer 4 sets the time slice for each DMA transfer to the DAC. With a Q43 the shortest practical (the CPU and memory bus is ISR bound) time slice for DAC updates is about 5us if main is to have any CPU time between DMA and onboard DAC memory system usage with the arbitrator of the system bus sharing the bus using assigned priorities from 0 (highest) to 7 (lowest).

// System Arbiter Configuration
ISRPR = 0x01; // Interrupt Service Routine Priority;
MAINPR = 0x02; // Main Routine Priority;
DMA1PR = 0x00; // DMA1 Priority;
DMA2PR = 0x03; // DMA2 Priority;
SCANPR = 0x04; // Scanner Priority;

So what we need is a timing pacer that doesn't use controller memory or cpu resources.

If you use an external device (DAC with SPI) then the serial data transfer time plus memory transfer overhead will be the pacing speed. So if you need 500ns for each complete DMA to SPI transfer then the time splice to calculate the data array size need for the NTSC signal will be in 500ns byte patterns. During most of this 500ns the memory bus will be free for MAIN execution.
 
I'm so not a fan of MCC. Writing your own code is most of the fun of playing with micros. That and quite honestly I'm so sick and tired of all of the efforts being made to remove all barriers to entry into the embedded world.

Yes was I a beginner at one time? I sure was, as were all of you. But I learned on a 16F628A and a socketed programmer, writing my code in Notepad and using the MPASM standalone GUI. That was my starting point. I didn't copy example or open source code off the 'net. I didn't use any "code configurators" or the like. I learned it bare metal on a bread board.

Anyway...I digress.

Thanks for all the input. I was just curious to know if the compiler itself made use of the DMA hardware automagically like it does with the FSR/INDF combo when dealing with arrays and pointers. I see now that it doesn't, which is a good thing as I can use the hardware without worrying about running into compiler resource conflicts.

One thing I'll say that I learned the hard way is that in the case of the Q84, the DMA hardware wants the system arbiter priority lock bit set before it will work. Tore my hair out for 2-3 days before I figured that one out.
 
Just for info, if you use the CCS compiler (which I much prefer to the Microchip ones), that can handle DMA _if_ you want it to, but it's absolutely controllable.
There are extra built-in functions for such as SPI where you provide a start pointer and byte count then just just check for a "done" flag or interrupt to see when that has completed.
 
To use the CCS compiler, what editor etc. do you use as I hate MPLAB.X. - utter garbage.

Mike.
Edit, a quick google answered my questions but any tips still welcome.
 
It comes with its own IDE, but I prefer MPLAB X !!
I've never had a problem with that, or the original mplab - I reckon I first used that over 20 years ago.

MPLAB is just an editor and debugger in this use, you don't need any of the convoluted tools or libraries.
 

Now if you had used the MCC generated DMA arbiter priority setting function you wouldn't have lost 2-3 days.
C:
void DMA5_SetDMAPriority(uint8_t priority)
{
    // This function is dependant on the PR1WAY CONFIG bit
    PRLOCK = 0x55;
    PRLOCK = 0xAA;
    PRLOCKbits.PRLOCKED = 0;
    DMA5PR = priority;
    PRLOCK = 0x55;
    PRLOCK = 0xAA;
    PRLOCKbits.PRLOCKED = 1;
}

Most of us are long past the point of discovering how to do X in a general abstract way on a generic abstract controller. MCC is really not for beginners, it's for experienced coders that need to get real work done. I use the MCC to generate a initial device independent framework to start the actual programming work. When a specialized controller routine is needed I write it, when a generic routine can be used then it is.
 

I didn't really "lose 2-3 days" as I'm just writing generic code as a means of learning the chip. It was more so a learning exercise for me. Using MCC I would have learned nothing. Doing it by hand forced me to have to read the datasheet and go through the motions required to learn. So in the end I learned something.

Now if I were on a deadline that would be a different story.
 

My problem is that people think that using MCC effectively is a short-cut. It's not, it's a HLL abstraction like using the C language. You need to read the datasheet and understand the chip before you start using it or it's just plugging random values and selections instead of using it as a chip baseline design tool.
 

I restructured the code a bit to DMA the video waveform into the PORTB LAT register address. The B0 pin is sync, B1 is video. With this I can get pretty good static NTSC B/W video with just DMA and a little state machine code in the DMA compete ISR using just two 1K pots for a video mixer.

Maybe useful for a static test or display pattern from ROM with a little more processing in a proper scan converter..



Single moving line, alternating dot pattern.
C:
/*
* File:   ntsc.h
* Author: root
*
* Created on December 20, 2020, 10:05 AM
*/

#ifndef NTSC_H
#define    NTSC_H

#ifdef    __cplusplus
extern "C" {
#endif
#include <xc.h>
#include <stdint.h>
#include <stdlib.h>
#include <stdbool.h>
#include "mcc_generated_files/dma5.h"
#include "mcc_generated_files/pin_manager.h"

#define DMA_M        0x04    // DMA modules number

#define SYNC_LEVEL    0 // clear all PORTB bits
#define BLANK_LEVEL    1 // PORTB bit 0 set
#define BLACK_LEVEL    1 // "
#define VIDEO_LEVEL    2 // PORTB bit 1

#define DMA_B        473    // timing adjustment of H sync pulses for 63.2us
#define V_BUF_SIZ    512    // data buffer array size
#define S_COUNT        247    // scanlines
#define H_SYNC        3    // number of H sync lines
#define H_COUNT        12    // post H sync scanlines

#define S_END        37    // H scan pulse  
#define B_START        48    // H front-porch
#define V_START        48    // Video start
#define V_DOTS        160    // scanline video dot position
#define V_END        400    // Video end
#define V_H        DMA_B/2

    enum s_mode_t {
        sync0, sync1, sync2, sync3, sync_error
    };

    extern uint8_t vsync[V_BUF_SIZ];
    extern uint8_t hsync[V_BUF_SIZ];
    extern volatile uint8_t vbuffer[V_BUF_SIZ], *vbuf_ptr;
    extern volatile uint32_t vcounts;
    extern volatile uint8_t vfcounts, scan_line;
    extern volatile bool ntsc_vid, ntsc_flip;
    extern volatile enum s_mode_t s_mode;

    void ntsc_init(void);

    /*
     * NTSC state machine options
     *
     * scan_line: Set to zero to display data on all scan lines or [1..S_COUNT] to display only on that scan line
     * nstc_vid: Blank video when scan_line is set to zero for all lines
     * ntsc_flip: use alternative scan_line buffer
     */

#ifdef    __cplusplus
}
#endif

#endif    /* NTSC_H */
C:
#include "ntsc.h"

volatile uint32_t vcounts = 0;
volatile uint8_t vfcounts = 0, scan_line = 0;
volatile bool ntsc_vid = true, ntsc_flip = false;

volatile enum s_mode_t s_mode;
volatile uint8_t vbuffer[V_BUF_SIZ], *vbuf_ptr;

void vcntd(void);
void vcnts(void);

/*
* setup the data formats and hardware for the DMA engine
*/
void ntsc_init(void)
{
    uint16_t count = 0;

    DMA5_StopTransfer();
    vbuf_ptr = vsync;
    SLRCONB = 0xff; // reduce PORTB slewrate
    DMA5_SetDMAPriority(0);
    DMA5_SetDCNTIInterruptHandler(vcnts);
    DMASELECT = DMA_M;
    DMAnCON0bits.EN = 0;
    DMAnSSA = (volatile uint24_t) vbuf_ptr;
    DMAnSSZ = DMA_B;
    DMAnDSZ = DMAnSSZ;
    DMAnCON0bits.EN = 1;

    /*
     * setup the static V, H and video patterns for DMA transfer engine to PORTB
     */
    for (count = 0; count < B_START; count++) {
        vsync[count] = SYNC_LEVEL;
        vbuffer[count] = SYNC_LEVEL;
        hsync[count] = SYNC_LEVEL;
    }

    for (count = S_END; count < B_START; count++) {
        vsync[count] = BLANK_LEVEL;
        vbuffer[count] = BLANK_LEVEL;
        hsync[count] = SYNC_LEVEL;
    }

    for (count = V_START; count < V_END; count++) {
        vsync[count] = BLANK_LEVEL;
        vbuffer[count] = BLANK_LEVEL;
        hsync[count] = SYNC_LEVEL;
        if ((count % 8)) { // add a bit of default texture
            if (count > V_DOTS)
                vsync[count] += VIDEO_LEVEL; // set bit 1 of PORTB
        } else {
            if (!(count % 8)) { // add a bit of default texture
                if (count > V_DOTS)
                    vbuffer[count] += VIDEO_LEVEL; // set bit 1 of PORTB
            }
        }
    }
    for (count = V_END; count < (V_BUF_SIZ - 1); count++) {
        vsync[count] = BLANK_LEVEL;
        vbuffer[count] = BLANK_LEVEL;
        hsync[count] = SYNC_LEVEL;
    }

    for (count = (DMA_B - 5); count < (V_BUF_SIZ - 1); count++) {
        hsync[count] = BLANK_LEVEL;
    }

    for (count = V_H; count < (V_H + 10); count++) {
        hsync[count] = BLANK_LEVEL; // double speed H pulses
    }

    // default scan mode to all lines
    s_mode = sync1;

    /*
     * kickstart the DMA engine
     */
    DMA5_StartTransfer();
}

/*
* routine not used
*/
void vcntd(void) // each DMA transfer interrupt
{
    vcounts++;
}

/*
* NTSC DMA state machine
* ISR triggered by the completed DMA transfer of the data buffer to PORTB
* Generates the required HV sync for fake-progressive NTSC scanning on most modern TV sets
* http://people.ece.cornell.edu/land/courses/ece5760/video/gvworks/GV%27s%20works%20%20NTSC%20demystified%20-%20B&W%20Video%20and%20Sync%20-%20Part%201.htm
*/
void vcnts(void) // each scan line interrupt, 262 total for scan lines and V sync
{
    vfcounts++;
    IO_RB4_Toggle();

    switch (s_mode) {
    case sync0: // H sync and video, one line
        if (vfcounts >= S_COUNT) { // 243
            vfcounts = 0;
            s_mode = sync2;
            DMASELECT = DMA_M;
            DMAnCON0bits.EN = 0;
            DMAnSSA = (volatile uint24_t) & hsync;
            DMAnSSZ = DMA_B;
            DMAnDSZ = DMAnSSZ;
            DMAnCON0bits.EN = 1;
        } else {
            if (vfcounts == scan_line) {
                IO_RB1_SetDigitalOutput();
            } else {
                IO_RB1_SetDigitalInput();
            }
        }
        break;
    case sync1: // H sync and video, all lines
        if (vfcounts >= S_COUNT) {
            vfcounts = 0;
            s_mode = sync2;
            DMASELECT = DMA_M;
            DMAnCON0bits.EN = 0;
            DMAnSSA = (volatile uint24_t) & hsync;
            DMAnSSZ = DMA_B;
            DMAnDSZ = DMAnSSZ;
            DMAnCON0bits.EN = 1;
        } else {
            if (ntsc_vid) {
                IO_RB1_SetDigitalOutput();
            } else {
                IO_RB1_SetDigitalInput();
            }
        }
        break;
    case sync2: // V sync and no video
        if (vfcounts >= H_SYNC) {
            vfcounts = 0;
            s_mode = sync3;
            DMASELECT = DMA_M;
            DMAnCON0bits.EN = 0;
            DMAnSSA = (volatile uint24_t) vbuf_ptr;
            DMAnSSZ = DMA_B;
            DMAnDSZ = DMAnSSZ;
            DMAnCON0bits.EN = 1;
            IO_RB1_SetDigitalInput(); // turn-off video bits
        }
        break;
    case sync3: // H sync and no video
        if (vfcounts >= H_COUNT) {
            vfcounts = 0;
            if ((bool) scan_line) {
                s_mode = sync0;
            } else {
                s_mode = sync1;
            }
            DMASELECT = DMA_M;
            DMAnCON0bits.EN = 0;
            if (ntsc_flip) {
                vbuf_ptr = vbuffer;
            } else {
                vbuf_ptr = vsync;
            }
            DMAnSSA = (volatile uint24_t) vbuf_ptr;
            DMAnSSZ = DMA_B;
            DMAnDSZ = DMAnSSZ;
            DMAnCON0bits.EN = 1;
        }
        break;
    default:
        vfcounts = 0;
        s_mode = sync1;
        DMASELECT = DMA_M;
        DMAnCON0bits.EN = 0;
        ntsc_flip = false;
        vbuf_ptr = vsync;
        DMAnSSA = (volatile uint24_t) vbuf_ptr;
        DMAnSSZ = DMA_B;
        DMAnDSZ = DMAnSSZ;
        DMAnCON0bits.EN = 1;
        IO_RB1_SetDigitalOutput(); // video bits, on
        break;
    }

    /*
     * re-trigger the state machine for a new scanline
     */
    DMA5_StartTransfer();
}
C:
#pragma warning disable 520
#pragma warning disable 1498

#include <stdlib.h>
#include <stdbool.h>
#include "mcc_generated_files/mcc.h"
#include "mcc_generated_files/tmr5.h"
#include "qconfig.h"
#include "vtouch.h"
#include "vtouch_build.h"
#include "timers.h"
#include "eadog.h"
#include "ntsc.h"

volatile uint16_t tickCount[TMR_COUNT];
char buffer[256];

void rxtx_handler(void);
void led_flash(void);

/*
* main program loop processing
*/
void rxtx_handler(void) // timer & serial data transform functions are handled here
{
    if (TimerDone(TMR_DIS)) {
        BLED_Toggle();
        sprintf(buffer, "%lu,%u    ", vcounts++, vfcounts);
        eaDogM_WriteStringAtPos(3, 0, buffer);
        sprintf(buffer, "%NTSC      ");
        eaDogM_WriteStringAtPos(0, 0, buffer);
        StartTimer(TMR_DIS, 500);
    }
}

/*
             Main application
*/
void main(void)
{

    // Initialize the device
    SYSTEM_Initialize();
    TMR5_SetInterruptHandler(led_flash);
    ntsc_init();

    // Enable high priority global interrupts
    INTERRUPT_GlobalInterruptHighEnable();

    // Enable low priority global interrupts.
    INTERRUPT_GlobalInterruptLowEnable();

    SPI1CON0bits.EN = 1;
    init_display();
    sprintf(buffer, "%s ", build_version);
    eaDogM_WriteStringAtPos(0, 0, buffer);
    sprintf(buffer, "%s ", build_date);
    eaDogM_WriteStringAtPos(1, 0, buffer);
    sprintf(buffer, "%s ", build_time);
    eaDogM_WriteStringAtPos(2, 0, buffer);
    BLED_SetLow();

    StartTimer(TMR_DIS, 500);

    TMR6_Stop(); // disable software timers to stop scan-line jitter
    while (true) {
        // Add your application code
        //                rxtx_handler();
    }
}

/*
* This runs in the timer5 ISR
*/
void led_flash(void)
{
    LED2_Toggle();
    ntsc_flip = !ntsc_flip;
    scan_line++;
}



Yellow: NTSC color video from a small camera as a O-scope sync source to tweak the Q43 video
Red: Q43 video
Green: scan line interrupts.


Static Bar pattern with a few scan-line glitches from ISR background processing.
 
Last edited:
Status
Not open for further replies.
Cookies are required to use this site. You must accept them to continue using the site. Learn more…