Wednesday, October 24, 2012

Using DMA to automatically transfer display data (or, it's RTFM, not 'Haphazardly Cast Your Eyes Left to Right Over The Manual While Thinking of Something Else')

As an aside from the process of perfecting the REM detector, I decided to start to clean up the display-painting parts of the firmware.

It takes half a kilobyte of data to completely fill the display (128-by-32 off-or-on pixels) and the data is sent through one of the chip's serial peripherals.  Since there is so much data to transmit per display update, it takes a LOT of processor cycles to do so; it takes even more cycles to do so by waiting and polling the serial peripheral instead of setting up an interrupt (as I did to debug the display).

Since the process of getting that data out to the display could be fairly straightforward, it seemed like the perfect excuse to take advantage of the AVR XMEGA microcontroller's onboard DMA feature.  And since I made a couple of errors along the way (and couldn't find a straightforward explanation of my problem online), it seemed appropriate to describe the situation here.

What is DMA?

Briefly, DMA (direct memory access) is a way of taking the load off of the processor by 'automating' data transfers.  Specifically, the DMA controller transfers a block of data from one location in memory to another while the processor executes other tasks.  Without a DMA controller, any large data transfer would take up processor time as the processor accessed and copied each byte of data individually.

There are many situations where you would want to transfer a lot of data from one location to another, or a small amount of data from (or to) one location a great many times.  In my scenario, I have a buffer in local memory that represents what the display will look like; the buffer is local so that manipulating it (compositing the image, blanking regions, adding text) is easier.  However, that buffer data needs to be periodically sent (serially) to the display hardware.  Ideally, the DMA will automate the process of taking each byte of the buffer in turn and sending it to the serial peripheral when it's ready to accept the bytes.

Implementation on the XMEGA

The XMEGA A microcontroller that I'm using has four independent DMA channels.  Each of them has a lot of configuration, including the ability to specify the source and destination data addresses and the ability to set what triggers the data transfers.

My problem was that I didn't read the manual closely enough; I thought that setting the channel trigger to the serial peripheral meant that every time the send register was empty (the trigger source), that a single byte would be sent.  In the default mode, however, the trigger causes the DMA to transfer an entire block of data as quickly as possible; since the serial peripheral sends out bytes a LOT slower than the rate that the DMA controller pushes bytes, this meant that each transfer only resulted in a few randomly-selected bytes of the buffer actually being sent to the display.  This caused me some consternation until I re-read the manual and realized my error; this fast operation is useful when copying data into SRAM or other fast destinations, but completely inappropriate for slow, single-byte destinations like the USART peripheral.

For slower destinations which will need to signal the transmission of each byte (or each burst of 2, 4, or 8 bytes) one at a time, the 'Single-Shot Data transfer' mode is used.  This mode completes a single burst, instead of a whole block, with each DMA channel trigger activation.

Since I want to transfer the complete contents of the display buffer to a single address on the serial peripheral, I need the destination address to be fixed and the source address to increment during the transmission, and reset at the end for the next update.

The actual C code I used to initialize the DMA controller and channel on the ATXMEGA128A4U is shown below; USARTC1 is the serial peripheral I'm using as my transmitter (it's been set up in master SPI mode for my display module, and then used to initialize the display module) and debugBuffer is the 512-byte-long stretch of internal memory that I'm using as my display buffer.

//set up a DMA channel
//enable the DMA controller
DMA_CTRL = DMA_ENABLE_bm;
//set the burst length to 1 byte
DMA_CH0_CTRLA = ( DMA_CH_SINGLE_bm | DMA_CH_BURSTLEN_1BYTE_gc );
//set the following: source address incremented, reload after each block; destination address fixed (reload after each block)
DMA_CH0_ADDRCTRL = ( DMA_CH_SRCRELOAD_TRANSACTION_gc | DMA_CH_SRCDIR_INC_gc | DMA_CH_DESTRELOAD_TRANSACTION_gc | DMA_CH_DESTDIR_FIXED_gc );
//now set the DMA trigger source to the USART data register being empty
DMA_CH0_TRIGSRC = DMA_CH_TRIGSRC_USARTC1_DRE_gc;
//load the block transfer count register with the number of bytes in our blocks (that is, 128*4 = 512)
DMA_CH0_TRFCNT = 512;
//now put in the initial source address; should be the memory address of the first byte of the display buffer
DMA_CH0_SRCADDR0 = ( (uint16_t) debugBuffer >> 0 ) & 0xFF;
DMA_CH0_SRCADDR1 = ( (uint16_t) debugBuffer >> 8 ) & 0xFF;
DMA_CH0_SRCADDR2 = 0x00;
//now specify the destination address; the transmit register of the USARTC1
DMA_CH0_DESTADDR0 = (( (uint16_t) &USARTC1_DATA ) >> 0) & 0xFF;
DMA_CH0_DESTADDR1 = (( (uint16_t) &USARTC1_DATA ) >> 8) & 0xFF;
DMA_CH0_DESTADDR2 = 0x00;

Once the DMA channel is set up (and assuming the USART and display have been initialized), I can update the display with the current contents of the buffer by simply enabling the DMA channel:

DMA_CH0_CTRLA |= 0b10000000;

1 comment:

  1. Fantastic example, worked first time for me - and first time messing around with DMA! Cheers :)

    ReplyDelete