The "Holy Bible" for embedded engineers
Efficient Data Transfer Without CPU Intervention
Understanding DMA controllers, programming, and optimization for embedded systems
Direct Memory Access (DMA) is a hardware mechanism that allows peripheral devices to transfer data directly to and from memory without CPU intervention. This enables high-performance data transfer while freeing the CPU for other tasks.
DMA Characteristics:
Programmed I/O:
DMA:
┌─────────────────────────────────────┐
│ DMA Transfer Flow │
├─────────────────────────────────────┤
│ 1. CPU Configures DMA Controller │
│ (Source, destination, count) │
├─────────────────────────────────────┤
│ 2. DMA Controller Requests Bus │
│ (Becomes bus master) │
├─────────────────────────────────────┤
│ 3. DMA Controller Performs Transfer│
│ (Memory ↔ Peripheral) │
├─────────────────────────────────────┤
│ 4. DMA Controller Releases Bus │
│ (CPU regains bus control) │
├─────────────────────────────────────┤
│ 5. DMA Controller Signals Completion│
│ (Interrupt or status flag) │
└─────────────────────────────────────┘
DMA follows the efficiency and autonomy principle—enable high-performance data transfer with minimal CPU overhead while maintaining system reliability and performance.
DMA Design Goals:
DMA controllers are specialized hardware components that manage data transfers between memory and peripheral devices. Understanding their architecture is essential for effective DMA programming.
DMA controllers follow the specialization and efficiency principle—provide dedicated hardware for data transfer operations to maximize performance and minimize system overhead.
Controller Design Goals:
Core Components:
┌─────────────────────────────────────┐
│ DMA Controller │
├─────────────────────────────────────┤
│ Channel Registers │
│ (Source, destination, count) │
├─────────────────────────────────────┤
│ Control Logic │
│ (Transfer control, arbitration)│
├─────────────────────────────────────┤
│ Bus Interface │
│ (Address, data, control) │
├─────────────────────────────────────┤
│ Interrupt Logic │
│ (Completion, error handling) │
└─────────────────────────────────────┘
DMA Channel Structure:
// DMA channel configuration structure
typedef struct {
uint32_t source_address; // Source memory address
uint32_t destination_address; // Destination memory address
uint32_t transfer_count; // Number of transfers
uint32_t control; // Control register
uint32_t status; // Status register
} dma_channel_t;
// DMA control register bits
#define DMA_CTRL_ENABLE (1 << 0) // Enable channel
#define DMA_CTRL_TCIE (1 << 1) // Transfer complete interrupt enable
#define DMA_CTRL_HTIE (1 << 2) // Half transfer interrupt enable
#define DMA_CTRL_TEIE (1 << 3) // Transfer error interrupt enable
#define DMA_CTRL_DIR (1 << 4) // Transfer direction
#define DMA_CTRL_CIRC (1 << 5) // Circular mode
#define DMA_CTRL_PINC (1 << 6) // Peripheral increment mode
#define DMA_CTRL_MINC (1 << 7) // Memory increment mode
#define DMA_CTRL_PSIZE_8BIT (0 << 8) // Peripheral data size
#define DMA_CTRL_PSIZE_16BIT (1 << 8)
#define DMA_CTRL_PSIZE_32BIT (2 << 8)
#define DMA_CTRL_MSIZE_8BIT (0 << 10) // Memory data size
#define DMA_CTRL_MSIZE_16BIT (1 << 10)
#define DMA_CTRL_MSIZE_32BIT (2 << 10)
#define DMA_CTRL_PL_LOW (0 << 12) // Priority level
#define DMA_CTRL_PL_MEDIUM (1 << 12)
#define DMA_CTRL_PL_HIGH (2 << 12)
#define DMA_CTRL_PL_VERY_HIGH (3 << 12)
DMA programming involves configuring the DMA controller, setting up transfer parameters, and handling transfer completion. Understanding DMA programming is essential for implementing efficient data transfer.
DMA programming follows the configuration and management principle—properly configure DMA parameters for optimal performance while managing transfer lifecycle and error conditions.
Programming Goals:
DMA Channel Configuration:
// Configure DMA channel for memory-to-memory transfer
void configure_dma_channel(dma_channel_t *channel,
uint32_t source, uint32_t destination,
uint32_t count) {
// Disable channel before configuration
channel->control &= ~DMA_CTRL_ENABLE;
// Wait for channel to be disabled
while (channel->control & DMA_CTRL_ENABLE);
// Configure transfer parameters
channel->source_address = source;
channel->destination_address = destination;
channel->transfer_count = count;
// Configure control register
channel->control = DMA_CTRL_ENABLE | // Enable channel
DMA_CTRL_TCIE | // Transfer complete interrupt
DMA_CTRL_TEIE | // Transfer error interrupt
DMA_CTRL_MINC | // Memory increment mode
DMA_CTRL_MSIZE_32BIT | // 32-bit memory transfers
DMA_CTRL_PL_HIGH; // High priority
}
// Start DMA transfer
void start_dma_transfer(dma_channel_t *channel) {
// Clear any pending flags
channel->status = 0;
// Enable the channel
channel->control |= DMA_CTRL_ENABLE;
}
// Check transfer status
bool is_dma_transfer_complete(dma_channel_t *channel) {
return (channel->status & (1 << 1)) != 0; // Transfer complete flag
}
// Check for transfer errors
bool has_dma_transfer_error(dma_channel_t *channel) {
return (channel->status & (1 << 3)) != 0; // Transfer error flag
}
DMA Interrupt Handling:
// DMA interrupt handler
void dma_interrupt_handler(dma_channel_t *channel) {
// Check transfer completion
if (is_dma_transfer_complete(channel)) {
// Transfer completed successfully
printf("DMA transfer completed\n");
// Clear transfer complete flag
channel->status &= ~(1 << 1);
// Disable channel
channel->control &= ~DMA_CTRL_ENABLE;
// Signal completion to application
dma_transfer_complete_callback();
}
// Check for transfer errors
if (has_dma_transfer_error(channel)) {
// Transfer error occurred
printf("DMA transfer error\n");
// Clear error flag
channel->status &= ~(1 << 3);
// Disable channel
channel->control &= ~DMA_CTRL_ENABLE;
// Handle error
dma_transfer_error_callback();
}
}
DMA controllers support various transfer modes optimized for different use cases. Understanding these modes is essential for selecting the most appropriate transfer configuration.
DMA transfer modes follow the optimization and flexibility principle—provide various transfer configurations to optimize performance for different data transfer patterns and requirements.
Transfer Mode Goals:
Memory-to-Memory Transfer:
// Configure DMA for memory-to-memory transfer
void setup_memory_to_memory_dma(dma_channel_t *channel,
uint32_t *source, uint32_t *destination,
uint32_t count) {
configure_dma_channel(channel,
(uint32_t)source,
(uint32_t)destination,
count);
// Set control bits for memory-to-memory transfer
channel->control |= DMA_CTRL_MINC | // Memory increment
DMA_CTRL_PINC | // Peripheral increment
DMA_CTRL_MSIZE_32BIT | // 32-bit memory
DMA_CTRL_PSIZE_32BIT; // 32-bit peripheral
}
// Example memory-to-memory transfer
void copy_memory_with_dma(uint32_t *source, uint32_t *destination, uint32_t count) {
dma_channel_t *channel = get_dma_channel(0); // Use channel 0
// Setup DMA transfer
setup_memory_to_memory_dma(channel, source, destination, count);
// Start transfer
start_dma_transfer(channel);
// Wait for completion
while (!is_dma_transfer_complete(channel)) {
// Wait or perform other tasks
}
printf("Memory copy completed via DMA\n");
}
Memory-to-Peripheral Transfer:
// Configure DMA for memory-to-peripheral transfer
void setup_memory_to_peripheral_dma(dma_channel_t *channel,
uint32_t *source, uint32_t peripheral_addr,
uint32_t count) {
configure_dma_channel(channel,
(uint32_t)source,
peripheral_addr,
count);
// Set control bits for memory-to-peripheral transfer
channel->control |= DMA_CTRL_MINC | // Memory increment
DMA_CTRL_MSIZE_32BIT | // 32-bit memory
DMA_CTRL_PSIZE_32BIT; // 32-bit peripheral
}
// Example: Send data to UART via DMA
void send_data_via_dma(uint8_t *data, uint32_t length) {
dma_channel_t *channel = get_dma_channel(1); // Use channel 1
// Setup DMA transfer to UART data register
setup_memory_to_peripheral_dma(channel,
(uint32_t*)data,
UART_DR_ADDRESS,
length);
// Start transfer
start_dma_transfer(channel);
// DMA will automatically send data to UART
printf("DMA transfer to UART started\n");
}
Peripheral-to-Memory Transfer:
// Configure DMA for peripheral-to-memory transfer
void setup_peripheral_to_memory_dma(dma_channel_t *channel,
uint32_t peripheral_addr, uint32_t *destination,
uint32_t count) {
configure_dma_channel(channel,
peripheral_addr,
(uint32_t)destination,
count);
// Set control bits for peripheral-to-memory transfer
channel->control |= DMA_CTRL_MINC | // Memory increment
DMA_CTRL_MSIZE_32BIT | // 32-bit memory
DMA_CTRL_PSIZE_32BIT; // 32-bit peripheral
}
// Example: Receive data from ADC via DMA
void receive_adc_data_via_dma(uint16_t *buffer, uint32_t count) {
dma_channel_t *channel = get_dma_channel(2); // Use channel 2
// Setup DMA transfer from ADC data register
setup_peripheral_to_memory_dma(channel,
ADC_DR_ADDRESS,
(uint32_t)buffer,
count);
// Start transfer
start_dma_transfer(channel);
// DMA will automatically receive data from ADC
printf("DMA transfer from ADC started\n");
}
DMA optimization involves improving transfer efficiency, reducing overhead, and maximizing throughput. Understanding optimization techniques is crucial for high-performance embedded systems.
DMA optimization follows the efficiency and throughput principle—maximize data transfer performance while minimizing resource usage and system overhead.
Optimization Goals:
Burst Transfer Optimization:
// Configure DMA for burst transfers
void setup_burst_dma(dma_channel_t *channel, uint32_t burst_size) {
// Set burst size for optimal performance
uint32_t burst_config = 0;
switch (burst_size) {
case 4:
burst_config = (0 << 16); // 4-beat burst
break;
case 8:
burst_config = (1 << 16); // 8-beat burst
break;
case 16:
burst_config = (2 << 16); // 16-beat burst
break;
default:
burst_config = (0 << 16); // Default to 4-beat
break;
}
channel->control |= burst_config;
}
// Optimize DMA transfer with burst mode
void optimized_dma_transfer(uint32_t *source, uint32_t *destination,
uint32_t count) {
dma_channel_t *channel = get_dma_channel(0);
// Configure for optimal burst size
setup_burst_dma(channel, 8); // 8-beat burst
// Setup transfer
configure_dma_channel(channel,
(uint32_t)source,
(uint32_t)destination,
count);
// Start transfer
start_dma_transfer(channel);
}
Memory Alignment Optimization:
// Ensure memory alignment for optimal DMA performance
void* allocate_aligned_buffer(size_t size, size_t alignment) {
void *ptr = NULL;
if (posix_memalign(&ptr, alignment, size) != 0) {
return NULL;
}
return ptr;
}
// DMA-optimized buffer allocation
uint32_t* allocate_dma_buffer(uint32_t count) {
// Allocate buffer aligned to cache line size for optimal DMA performance
return (uint32_t*)allocate_aligned_buffer(count * sizeof(uint32_t), 64);
}
// Optimized DMA transfer with aligned buffers
void aligned_dma_transfer(uint32_t count) {
// Allocate aligned buffers
uint32_t *source = allocate_dma_buffer(count);
uint32_t *destination = allocate_dma_buffer(count);
if (source && destination) {
// Initialize source data
for (uint32_t i = 0; i < count; i++) {
source[i] = i;
}
// Perform DMA transfer
dma_channel_t *channel = get_dma_channel(0);
configure_dma_channel(channel,
(uint32_t)source,
(uint32_t)destination,
count);
start_dma_transfer(channel);
// Wait for completion
while (!is_dma_transfer_complete(channel));
// Clean up
free(source);
free(destination);
}
}
Circular DMA Mode:
// Configure DMA for circular mode (continuous transfer)
void setup_circular_dma(dma_channel_t *channel,
uint32_t *buffer, uint32_t buffer_size) {
configure_dma_channel(channel,
(uint32_t)buffer,
(uint32_t)buffer,
buffer_size);
// Enable circular mode
channel->control |= DMA_CTRL_CIRC | // Circular mode
DMA_CTRL_HTIE | // Half transfer interrupt
DMA_CTRL_TCIE; // Transfer complete interrupt
}
// Circular DMA for continuous data streaming
void start_circular_dma_stream(uint32_t *buffer, uint32_t buffer_size) {
dma_channel_t *channel = get_dma_channel(3); // Use channel 3
// Setup circular DMA
setup_circular_dma(channel, buffer, buffer_size);
// Start transfer
start_dma_transfer(channel);
printf("Circular DMA stream started\n");
}
// Handle circular DMA interrupts
void circular_dma_interrupt_handler(dma_channel_t *channel) {
if (channel->status & (1 << 2)) { // Half transfer
// Process first half of buffer
process_buffer_half(0);
channel->status &= ~(1 << 2);
}
if (channel->status & (1 << 1)) { // Transfer complete
// Process second half of buffer
process_buffer_half(1);
channel->status &= ~(1 << 1);
}
}
DMA is widely used in embedded systems for high-performance data transfer. Understanding DMA applications is essential for designing efficient embedded systems.
Embedded DMA follows the performance and resource principle—provide high-performance data transfer while working within embedded system constraints and requirements.
Embedded DMA Goals:
Audio Processing:
// DMA for audio data transfer
typedef struct {
uint16_t *audio_buffer;
uint32_t buffer_size;
dma_channel_t *dma_channel;
bool is_playing;
} audio_dma_t;
// Initialize audio DMA
audio_dma_t* init_audio_dma(uint32_t sample_rate, uint32_t buffer_ms) {
audio_dma_t *audio = malloc(sizeof(audio_dma_t));
// Calculate buffer size
uint32_t buffer_size = (sample_rate * buffer_ms) / 1000;
audio->buffer_size = buffer_size;
// Allocate audio buffer
audio->audio_buffer = malloc(buffer_size * sizeof(uint16_t));
// Get DMA channel
audio->dma_channel = get_dma_channel(4);
// Configure DMA for audio output
configure_dma_channel(audio->dma_channel,
(uint32_t)audio->audio_buffer,
DAC_DATA_REGISTER,
buffer_size);
// Enable circular mode for continuous audio
audio->dma_channel->control |= DMA_CTRL_CIRC;
return audio;
}
// Start audio playback
void start_audio_playback(audio_dma_t *audio) {
// Fill buffer with audio data
generate_audio_data(audio->audio_buffer, audio->buffer_size);
// Start DMA transfer
start_dma_transfer(audio->dma_channel);
audio->is_playing = true;
printf("Audio playback started via DMA\n");
}
Image Processing:
// DMA for camera image transfer
typedef struct {
uint8_t *image_buffer;
uint32_t width;
uint32_t height;
dma_channel_t *dma_channel;
} camera_dma_t;
// Initialize camera DMA
camera_dma_t* init_camera_dma(uint32_t width, uint32_t height) {
camera_dma_t *camera = malloc(sizeof(camera_dma_t));
camera->width = width;
camera->height = height;
// Allocate image buffer
uint32_t buffer_size = width * height * 2; // 16-bit pixels
camera->image_buffer = malloc(buffer_size);
// Get DMA channel
camera->dma_channel = get_dma_channel(5);
// Configure DMA for camera data input
configure_dma_channel(camera->dma_channel,
CAMERA_DATA_REGISTER,
(uint32_t)camera->image_buffer,
buffer_size / 2); // 32-bit transfers
return camera;
}
// Capture image via DMA
void capture_image_dma(camera_dma_t *camera) {
// Start DMA transfer
start_dma_transfer(camera->dma_channel);
// Wait for completion
while (!is_dma_transfer_complete(camera->dma_channel));
printf("Image captured via DMA\n");
// Process captured image
process_image(camera->image_buffer, camera->width, camera->height);
}
Direct Memory Access (DMA) is a powerful mechanism for high-performance data transfer in embedded systems. Understanding DMA controllers, programming, and optimization is essential for creating efficient embedded applications.
Key Takeaways:
The Path Forward:
As embedded systems become more data-intensive, DMA will become increasingly important for maintaining system performance. Modern DMA controllers continue to evolve, providing new features and optimization opportunities.
Remember: DMA is not just about data transfer—it’s about understanding how to efficiently move data while freeing the CPU for other tasks. The skills you develop here will enable you to create high-performance, efficient embedded systems.