The Embedded New Testament

The "Holy Bible" for embedded engineers


Project maintained by theEmbeddedGeorge Hosted on GitHub Pages — Theme by mattgraham

Direct Memory Access (DMA)

Efficient Data Transfer Without CPU Intervention
Understanding DMA controllers, programming, and optimization for embedded systems


📋 Table of Contents


🏗️ DMA Fundamentals

What is Direct Memory Access?

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:

DMA vs. Programmed I/O

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 Philosophy

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 Controller Architecture

Understanding DMA Controllers

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 Controller Philosophy

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:

DMA Controller Components

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

Programming DMA Controllers

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 Philosophy

DMA programming follows the configuration and management principle—properly configure DMA parameters for optimal performance while managing transfer lifecycle and error conditions.

Programming Goals:

Basic DMA Setup

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 Transfer Modes

Understanding DMA Transfer Types

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 Mode Philosophy

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:

Transfer Mode Types

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

Optimizing DMA Performance

DMA optimization involves improving transfer efficiency, reducing overhead, and maximizing throughput. Understanding optimization techniques is crucial for high-performance embedded systems.

DMA Optimization Philosophy

DMA optimization follows the efficiency and throughput principle—maximize data transfer performance while minimizing resource usage and system overhead.

Optimization Goals:

Performance Optimization Techniques

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 in Embedded Systems

DMA Applications in Embedded Systems

DMA is widely used in embedded systems for high-performance data transfer. Understanding DMA applications is essential for designing efficient embedded systems.

Embedded DMA Philosophy

Embedded DMA follows the performance and resource principle—provide high-performance data transfer while working within embedded system constraints and requirements.

Embedded DMA Goals:

Common DMA Applications

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);
}

🎯 Conclusion

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.