DMA在SPI和I2C通信中的应用详解

发布于:2025-04-03 ⋅ 阅读:(26) ⋅ 点赞:(0)

DMA在SPI和I2C通信中的应用详解

目录

1. DMA基本原理

1.1 什么是DMA

DMA(Direct Memory Access,直接内存访问)是一种允许外围设备(如SPI、I2C控制器)直接访问系统内存而无需CPU干预的数据传输技术。在传统的数据传输中,CPU需要执行一系列指令来移动数据,这不仅占用宝贵的处理时间,还限制了数据传输速率。

DMA控制器是一个专门的硬件模块,能够独立于CPU执行内存操作,允许数据传输在后台进行,同时CPU可以执行其他任务。当传输完成时,DMA控制器会通知CPU(通常通过中断)。

1.2 DMA工作模式

在嵌入式微控制器中,DMA通常支持以下几种工作模式:

1. 存储器到存储器模式

  • 在内存区域之间复制数据
  • 不涉及外设,纯粹的内存操作
  • 典型应用:大块数据的快速复制

2. 存储器到外设模式

  • 数据从内存传输到外设
  • 典型应用:SPI/I2C发送操作

3. 外设到存储器模式

  • 数据从外设传输到内存
  • 典型应用:SPI/I2C接收操作

4. 外设到外设模式

  • 在两个外设之间直接传输数据
  • 较少使用,但在特定场景效率很高

5. 循环模式

  • DMA在完成传输后自动重新加载初始配置
  • 适用于持续性数据流,如ADC采样

1.3 DMA在嵌入式系统中的优势

  1. 降低CPU负载:CPU不必参与数据传输过程,可专注于其他任务。

  2. 提高传输效率:DMA控制器优化设计用于数据移动,比CPU执行循环复制更高效。

  3. 确定性时序:DMA传输通常具有可预测的时序,有助于实时系统的设计。

  4. 减少中断频率:无需每字节/字产生中断,只在传输完成时通知CPU。

  5. 降低功耗:CPU可在DMA传输过程中进入低功耗模式,节省能源。

2. DMA+SPI通信实现

2.1 SPI+DMA工作原理

SPI(Serial Peripheral Interface)是一种全双工同步串行通信接口,常用于与传感器、存储器和其他外设通信。传统的SPI实现中,CPU需要不断写入和读取SPI数据寄存器(DR),这在大数据量传输时效率低下。

当将DMA与SPI结合使用时:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

  1. 发送流程

    • CPU配置SPI寄存器和DMA参数
    • DMA控制器在SPI发送缓冲区准备好时,自动将下一个数据从内存传输到SPI数据寄存器
    • 此过程循环直至所有数据发送完成
    • DMA发出传输完成中断通知CPU
  2. 接收流程

    • CPU配置SPI寄存器和DMA参数
    • 当SPI接收缓冲区有数据时,DMA自动将数据从SPI数据寄存器传输到内存
    • 此过程循环直至接收完所有数据
    • DMA发出传输完成中断通知CPU
  3. 全双工操作

    • 大多数SPI控制器支持同时使用两个DMA通道
    • 一个通道负责TX(发送),另一个负责RX(接收)
    • 两个DMA通道协同工作实现全双工数据传输

2.2 配置步骤

以STM32微控制器为例,配置SPI+DMA的基本步骤如下:

  1. 配置GPIO引脚

    • 设置SPI相关引脚(SCK、MISO、MOSI、CS)
    • 配置引脚的复用功能、速度和上拉/下拉状态
  2. 配置SPI外设

    • 设置SPI模式、时钟分频、数据格式等
    • 启用SPI的DMA请求功能
  3. 配置DMA

    • 设置DMA通道和流
    • 配置源地址和目标地址
    • 设置传输方向、大小和模式
    • 配置优先级和中断
  4. 启动传输

    • 启用DMA
    • 启用SPI
    • 开始传输过程
  5. 处理完成中断

    • 接收DMA传输完成中断
    • 执行必要的后续处理

2.3 代码实现示例

以下是STM32平台上实现SPI+DMA传输的简化示例:

// SPI+DMA初始化 (基于STM32 HAL库)
void SPI_DMA_Init(void) {
    // GPIO配置
    GPIO_InitTypeDef GPIO_InitStruct = {0};
    
    // 使能时钟
    __HAL_RCC_SPI1_CLK_ENABLE();
    __HAL_RCC_DMA2_CLK_ENABLE();
    __HAL_RCC_GPIOA_CLK_ENABLE();
    
    // 配置SPI引脚
    GPIO_InitStruct.Pin = GPIO_PIN_5 | GPIO_PIN_6 | GPIO_PIN_7;
    GPIO_InitStruct.Mode = GPIO_MODE_AF_PP;
    GPIO_InitStruct.Pull = GPIO_NOPULL;
    GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_VERY_HIGH;
    GPIO_InitStruct.Alternate = GPIO_AF5_SPI1;
    HAL_GPIO_Init(GPIOA, &GPIO_InitStruct);
    
    // 配置SPI
    hspi1.Instance = SPI1;
    hspi1.Init.Mode = SPI_MODE_MASTER;
    hspi1.Init.Direction = SPI_DIRECTION_2LINES;
    hspi1.Init.DataSize = SPI_DATASIZE_8BIT;
    hspi1.Init.CLKPolarity = SPI_POLARITY_LOW;
    hspi1.Init.CLKPhase = SPI_PHASE_1EDGE;
    hspi1.Init.NSS = SPI_NSS_SOFT;
    hspi1.Init.BaudRatePrescaler = SPI_BAUDRATEPRESCALER_16;
    hspi1.Init.FirstBit = SPI_FIRSTBIT_MSB;
    hspi1.Init.TIMode = SPI_TIMODE_DISABLE;
    hspi1.Init.CRCCalculation = SPI_CRCCALCULATION_DISABLE;
    HAL_SPI_Init(&hspi1);
    
    // 配置DMA - TX
    hdma_spi1_tx.Instance = DMA2_Stream3;
    hdma_spi1_tx.Init.Channel = DMA_CHANNEL_3;
    hdma_spi1_tx.Init.Direction = DMA_MEMORY_TO_PERIPH;
    hdma_spi1_tx.Init.PeriphInc = DMA_PINC_DISABLE;
    hdma_spi1_tx.Init.MemInc = DMA_MINC_ENABLE;
    hdma_spi1_tx.Init.PeriphDataAlignment = DMA_PDATAALIGN_BYTE;
    hdma_spi1_tx.Init.MemDataAlignment = DMA_MDATAALIGN_BYTE;
    hdma_spi1_tx.Init.Mode = DMA_NORMAL;
    hdma_spi1_tx.Init.Priority = DMA_PRIORITY_MEDIUM;
    hdma_spi1_tx.Init.FIFOMode = DMA_FIFOMODE_DISABLE;
    HAL_DMA_Init(&hdma_spi1_tx);
    
    // 关联DMA和SPI
    __HAL_LINKDMA(&hspi1, hdmatx, hdma_spi1_tx);
    
    // 配置DMA - RX (类似TX配置)
    hdma_spi1_rx.Instance = DMA2_Stream0;
    // ... 其他RX配置类似 ...
    hdma_spi1_rx.Init.Direction = DMA_PERIPH_TO_MEMORY;
    // ... 其他设置相同 ...
    HAL_DMA_Init(&hdma_spi1_rx);
    
    __HAL_LINKDMA(&hspi1, hdmarx, hdma_spi1_rx);
    
    // 配置DMA中断
    HAL_NVIC_SetPriority(DMA2_Stream3_IRQn, 0, 0);
    HAL_NVIC_EnableIRQ(DMA2_Stream3_IRQn);
    HAL_NVIC_SetPriority(DMA2_Stream0_IRQn, 0, 0);
    HAL_NVIC_EnableIRQ(DMA2_Stream0_IRQn);
}

// 使用DMA发送SPI数据
void SPI_DMA_Transmit(uint8_t *data, uint16_t size) {
    // 选中从设备 (拉低CS引脚)
    HAL_GPIO_WritePin(SPI_CS_GPIO_Port, SPI_CS_Pin, GPIO_PIN_RESET);
    
    // 启动DMA传输
    HAL_SPI_Transmit_DMA(&hspi1, data, size);
    
    // 注意:CS引脚会在DMA完成中断处理函数中拉高
}

// DMA完成中断处理函数
void HAL_SPI_TxCpltCallback(SPI_HandleTypeDef *hspi) {
    // 传输完成,释放从设备 (拉高CS引脚)
    HAL_GPIO_WritePin(SPI_CS_GPIO_Port, SPI_CS_Pin, GPIO_PIN_SET);
    
    // 通知应用层传输完成
    SPI_TransferComplete_Callback();
}

// 使用DMA接收SPI数据
void SPI_DMA_Receive(uint8_t *buffer, uint16_t size) {
    // 选中从设备
    HAL_GPIO_WritePin(SPI_CS_GPIO_Port, SPI_CS_Pin, GPIO_PIN_RESET);
    
    // 启动DMA接收
    HAL_SPI_Receive_DMA(&hspi1, buffer, size);
    
    // CS引脚会在接收完成回调中拉高
}

// 接收完成回调
void HAL_SPI_RxCpltCallback(SPI_HandleTypeDef *hspi) {
    HAL_GPIO_WritePin(SPI_CS_GPIO_Port, SPI_CS_Pin, GPIO_PIN_SET);
    
    // 通知应用层接收完成
    SPI_ReceiveComplete_Callback();
}

// 全双工传输
void SPI_DMA_TransmitReceive(uint8_t *txData, uint8_t *rxData, uint16_t size) {
    HAL_GPIO_WritePin(SPI_CS_GPIO_Port, SPI_CS_Pin, GPIO_PIN_RESET);
    
    // 启动全双工DMA传输
    HAL_SPI_TransmitReceive_DMA(&hspi1, txData, rxData, size);
}

// 全双工传输完成回调
void HAL_SPI_TxRxCpltCallback(SPI_HandleTypeDef *hspi) {
    HAL_GPIO_WritePin(SPI_CS_GPIO_Port, SPI_CS_Pin, GPIO_PIN_SET);
    
    // 通知应用层传输完成
    SPI_TransferComplete_Callback();
}

2.4 高级应用场景

1. 大容量存储设备访问

与Flash存储器(如SPI NOR Flash或SD卡)交互时,DMA+SPI组合能显著提高性能:

// 从Flash中读取大量数据的示例
void ReadFlashBlock(uint32_t address, uint8_t *buffer, uint32_t size) {
    uint8_t cmd[5] = {0};
    
    // 准备读取命令
    cmd[0] = FLASH_READ_CMD;       // 读取命令 (通常为0x03)
    cmd[1] = (address >> 24) & 0xFF; // 地址字节3
    cmd[2] = (address >> 16) & 0xFF; // 地址字节2
    cmd[3] = (address >> 8) & 0xFF;  // 地址字节1
    cmd[4] = address & 0xFF;         // 地址字节0
    
    // 选中Flash芯片
    FLASH_CS_LOW();
    
    // 发送命令和地址
    HAL_SPI_Transmit(&hspi1, cmd, 5, HAL_MAX_DELAY);
    
    // 使用DMA读取数据
    HAL_SPI_Receive_DMA(&hspi1, buffer, size);
    
    // CS信号会在DMA完成回调中释放
}

// 写入大量数据到Flash
void WriteFlashPage(uint32_t address, uint8_t *data, uint16_t size) {
    // 略去了擦除和写使能操作,实际应用中需要这些步骤
    
    uint8_t cmd[5] = {0};
    cmd[0] = FLASH_PAGE_PROGRAM;   // 页编程命令 (通常为0x02)
    cmd[1] = (address >> 24) & 0xFF;
    cmd[2] = (address >> 16) & 0xFF;
    cmd[3] = (address >> 8) & 0xFF;
    cmd[4] = address & 0xFF;
    
    FLASH_CS_LOW();
    
    // 发送命令和地址
    HAL_SPI_Transmit(&hspi1, cmd, 5, HAL_MAX_DELAY);
    
    // 使用DMA发送数据
    HAL_SPI_Transmit_DMA(&hspi1, data, size);
}
2. 无缝数据流处理

DMA循环模式允许创建连续数据流,对于需要连续数据流的应用如音频处理非常有用:

#define BUFFER_SIZE 1024
uint8_t dmaBuffer[BUFFER_SIZE];
volatile uint16_t dmaHead = 0;
volatile uint16_t dmaTail = 0;

// 初始化循环DMA
void InitCircularDMA(void) {
    hdma_spi1_rx.Init.Mode = DMA_CIRCULAR;
    HAL_DMA_Init(&hdma_spi1_rx);
    
    // 启动连续接收
    HAL_SPI_Receive_DMA(&hspi1, dmaBuffer, BUFFER_SIZE);
}

// DMA半完成中断处理函数
void HAL_SPI_RxHalfCpltCallback(SPI_HandleTypeDef *hspi) {
    // 处理缓冲区的前半部分
    dmaHead = 0;
    ProcessData();
}

// DMA完成中断处理函数
void HAL_SPI_RxCpltCallback(SPI_HandleTypeDef *hspi) {
    // 处理缓冲区的后半部分
    dmaHead = BUFFER_SIZE/2;
    ProcessData();
}

void ProcessData(void) {
    // 处理从dmaHead到BUFFER_SIZE/2的数据
    while(dmaTail != dmaHead + BUFFER_SIZE/2) {
        // 处理dmaBuffer[dmaTail]
        dmaTail = (dmaTail + 1) % BUFFER_SIZE;
    }
}
3. 双缓冲技术

通过使用双缓冲区,可以实现一个缓冲区处理数据的同时另一个缓冲区接收新数据:

#define BUFFER_SIZE 512
uint8_t buffer0[BUFFER_SIZE];
uint8_t buffer1[BUFFER_SIZE];
volatile uint8_t activeBuffer = 0;

// 初始化双缓冲接收
void InitDoubleBufferDMA(void) {
    // 开始接收到第一个缓冲区
    activeBuffer = 0;
    HAL_SPI_Receive_DMA(&hspi1, buffer0, BUFFER_SIZE);
}

// DMA完成回调
void HAL_SPI_RxCpltCallback(SPI_HandleTypeDef *hspi) {
    // 切换缓冲区
    activeBuffer = !activeBuffer;
    
    // 启动下一次接收
    if(activeBuffer == 0) {
        HAL_SPI_Receive_DMA(&hspi1, buffer0, BUFFER_SIZE);
        // 在另一个任务中处理buffer1
        ProcessBuffer(buffer1);
    } else {
        HAL_SPI_Receive_DMA(&hspi1, buffer1, BUFFER_SIZE);
        // 在另一个任务中处理buffer0
        ProcessBuffer(buffer0);
    }
}

3. DMA+I2C通信实现

3.1 I2C+DMA工作原理

I2C(Inter-Integrated Circuit)是一种双线制串行总线,使用SDA(数据线)和SCL(时钟线)进行通信。I2C实现了主从架构,可支持多主机和多从机连接在同一总线上。

传统I2C通信中,CPU需要逐字节操作数据寄存器(DR),对于大数据量传输效率较低。通过DMA,可以显著提高I2C传输效率:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

  1. 主设备发送流程

    • CPU配置I2C寄存器设置从机地址、传输方向等
    • CPU配置DMA传输参数
    • DMA控制器将数据从内存直接传输到I2C数据寄存器
    • I2C控制器处理起始条件、地址发送、数据传输和停止条件等时序
    • 传输完成后DMA触发中断通知CPU
  2. 主设备接收流程

    • CPU配置I2C和DMA参数
    • I2C控制器自动处理起始条件、从机寻址等
    • 接收的数据通过DMA直接从I2C数据寄存器传输到内存
    • 传输完成后通知CPU
  3. 主要差异(与SPI+DMA相比)

    • I2C通信需要完整的寻址过程
    • I2C通信包含应答机制(ACK/NACK)
    • I2C通信中存在时钟拉伸的可能性
    • I2C通常是半双工通信

3.2 配置步骤

以STM32微控制器为例,配置I2C+DMA的基本步骤如下:

  1. 配置GPIO引脚

    • 设置I2C相关引脚(SCL、SDA)为开漏输出模式
    • 配置上拉电阻和复用功能
  2. 配置I2C外设

    • 设置时钟速度(标准模式100kHz或快速模式400kHz)
    • 配置地址模式(7位或10位)
    • 启用I2C的DMA请求功能
  3. 配置DMA

    • 配置DMA通道和流
    • 设置源地址和目标地址
    • 配置传输方向(内存到外设或外设到内存)
    • 设置数据宽度、增量模式等参数
  4. 启动传输

    • 启用DMA
    • 启动I2C传输
    • 注册中断处理函数

3.3 代码实现示例

以下是STM32平台上实现I2C+DMA传输的简化示例:

// I2C+DMA初始化 (基于STM32 HAL库)
void I2C_DMA_Init(void) {
    // GPIO配置
    GPIO_InitTypeDef GPIO_InitStruct = {0};
    
    // 使能时钟
    __HAL_RCC_I2C1_CLK_ENABLE();
    __HAL_RCC_DMA1_CLK_ENABLE();
    __HAL_RCC_GPIOB_CLK_ENABLE();
    
    // 配置I2C引脚
    GPIO_InitStruct.Pin = GPIO_PIN_6 | GPIO_PIN_7;
    GPIO_InitStruct.Mode = GPIO_MODE_AF_OD;        // 开漏输出模式
    GPIO_InitStruct.Pull = GPIO_PULLUP;            // 使用内部上拉
    GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_HIGH;
    GPIO_InitStruct.Alternate = GPIO_AF4_I2C1;
    HAL_GPIO_Init(GPIOB, &GPIO_InitStruct);
    
    // 配置I2C
    hi2c1.Instance = I2C1;
    hi2c1.Init.ClockSpeed = 400000;                // 400kHz快速模式
    hi2c1.Init.DutyCycle = I2C_DUTYCYCLE_2;
    hi2c1.Init.OwnAddress1 = 0;                    // 主设备不需要地址
    hi2c1.Init.AddressingMode = I2C_ADDRESSINGMODE_7BIT;
    hi2c1.Init.DualAddressMode = I2C_DUALADDRESS_DISABLE;
    hi2c1.Init.OwnAddress2 = 0;
    hi2c1.Init.GeneralCallMode = I2C_GENERALCALL_DISABLE;
    hi2c1.Init.NoStretchMode = I2C_NOSTRETCH_DISABLE;
    HAL_I2C_Init(&hi2c1);
    
    // 配置DMA - TX
    hdma_i2c1_tx.Instance = DMA1_Stream6;
    hdma_i2c1_tx.Init.Channel = DMA_CHANNEL_1;
    hdma_i2c1_tx.Init.Direction = DMA_MEMORY_TO_PERIPH;
    hdma_i2c1_tx.Init.PeriphInc = DMA_PINC_DISABLE;
    hdma_i2c1_tx.Init.MemInc = DMA_MINC_ENABLE;
    hdma_i2c1_tx.Init.PeriphDataAlignment = DMA_PDATAALIGN_BYTE;
    hdma_i2c1_tx.Init.MemDataAlignment = DMA_MDATAALIGN_BYTE;
    hdma_i2c1_tx.Init.Mode = DMA_NORMAL;
    hdma_i2c1_tx.Init.Priority = DMA_PRIORITY_MEDIUM;
    hdma_i2c1_tx.Init.FIFOMode = DMA_FIFOMODE_DISABLE;
    HAL_DMA_Init(&hdma_i2c1_tx);
    
    // 关联DMA和I2C
    __HAL_LINKDMA(&hi2c1, hdmatx, hdma_i2c1_tx);
    
    // 配置DMA - RX
    hdma_i2c1_rx.Instance = DMA1_Stream0;
    hdma_i2c1_rx.Init.Channel = DMA_CHANNEL_1;
    hdma_i2c1_rx.Init.Direction = DMA_PERIPH_TO_MEMORY;
    // ... 其他设置与TX类似 ...
    HAL_DMA_Init(&hdma_i2c1_rx);
    
    __HAL_LINKDMA(&hi2c1, hdmarx, hdma_i2c1_rx);
    
    // 配置DMA中断
    HAL_NVIC_SetPriority(DMA1_Stream6_IRQn, 1, 0);
    HAL_NVIC_EnableIRQ(DMA1_Stream6_IRQn);
    HAL_NVIC_SetPriority(DMA1_Stream0_IRQn, 1, 0);
    HAL_NVIC_EnableIRQ(DMA1_Stream0_IRQn);
}

// 使用DMA向I2C从设备写入数据
void I2C_DMA_Write(uint8_t slaveAddress, uint8_t *data, uint16_t size) {
    // 使用DMA向指定地址的从设备发送数据
    HAL_I2C_Master_Transmit_DMA(&hi2c1, slaveAddress << 1, data, size);
    
    // 传输完成将通过回调函数通知
}

// DMA发送完成回调
void HAL_I2C_MasterTxCpltCallback(I2C_HandleTypeDef *hi2c) {
    // 通知应用层传输完成
    I2C_TxComplete_Callback();
}

// 使用DMA从I2C从设备读取数据
void I2C_DMA_Read(uint8_t slaveAddress, uint8_t *buffer, uint16_t size) {
    // 使用DMA从指定地址的从设备接收数据
    HAL_I2C_Master_Receive_DMA(&hi2c1, slaveAddress << 1, buffer, size);
    
    // 接收完成将通过回调函数通知
}

// DMA接收完成回调
void HAL_I2C_MasterRxCpltCallback(I2C_HandleTypeDef *hi2c) {
    // 通知应用层接收完成
    I2C_RxComplete_Callback();
}

// 向特定寄存器写入数据
void I2C_DMA_WriteRegister(uint8_t slaveAddress, uint8_t regAddress, uint8_t *data, uint16_t size) {
    uint8_t *buffer = malloc(size + 1);
    if(buffer == NULL) return;
    
    // 第一个字节是寄存器地址
    buffer[0] = regAddress;
    memcpy(buffer + 1, data, size);
    
    // 使用DMA发送
    HAL_I2C_Master_Transmit_DMA(&hi2c1, slaveAddress << 1, buffer, size + 1);
    
    // 注意:buffer将在传输完成回调中释放
    // 这要求修改回调函数以接收缓冲区指针,或使用全局变量
}

// 从特定寄存器读取数据
void I2C_DMA_ReadRegister(uint8_t slaveAddress, uint8_t regAddress, uint8_t *buffer, uint16_t size) {
    // 首先写入寄存器地址
    HAL_I2C_Master_Transmit(&hi2c1, slaveAddress << 1, &regAddress, 1, HAL_MAX_DELAY);
    
    // 然后使用DMA读取数据
    HAL_I2C_Master_Receive_DMA(&hi2c1, slaveAddress << 1, buffer, size);
}

// 使用存储器映射方式读取寄存器
// 某些I2C外设支持直接读取指定寄存器的特殊传输
void I2C_DMA_MemRead(uint8_t slaveAddress, uint8_t memAddress, uint8_t *buffer, uint16_t size) {
    HAL_I2C_Mem_Read_DMA(&hi2c1, slaveAddress << 1, memAddress, I2C_MEMADD_SIZE_8BIT, buffer, size);
}

// 使用存储器映射方式写入寄存器
void I2C_DMA_MemWrite(uint8_t slaveAddress, uint8_t memAddress, uint8_t *data, uint16_t size) {
    HAL_I2C_Mem_Write_DMA(&hi2c1, slaveAddress << 1, memAddress, I2C_MEMADD_SIZE_8BIT, data, size);
}

// DMA错误处理回调
void HAL_I2C_ErrorCallback(I2C_HandleTypeDef *hi2c) {
    // 处理错误情况
    I2C_Error_Handler(hi2c->ErrorCode);
}

3.4 高级应用场景

1. 传感器数据批量读取

许多传感器支持数据寄存器的连续读取,DMA能显著提高这类操作的效率:

#define ACC_ADDR        0x19    // 加速度计I2C地址
#define ACC_DATA_REG    0x28    // 数据寄存器起始地址
#define MAG_ADDR        0x1E    // 磁力计I2C地址
#define MAG_DATA_REG    0x68    // 磁力计数据寄存器

// 加速度数据结构
typedef struct {
    int16_t x;
    int16_t y;
    int16_t z;
} AccelData_t;

// 批量读取加速度计数据
void ReadAccelerometerData(AccelData_t *data) {
    uint8_t buffer[6];  // XYZ三轴,每轴2字节
    
    // 从连续寄存器中读取6字节
    I2C_DMA_ReadRegister(ACC_ADDR, ACC_DATA_REG, buffer, 6);
    
    // 数据处理将在DMA完成中断中进行
}

// 自定义DMA接收完成回调
void I2C_RxComplete_Callback(void) {
    // 数据已通过DMA传输到buffer
    // 现在处理数据
    AccelData_t data;
    
    // 假设buffer是全局变量
    data.x = (int16_t)((buffer[1] << 8) | buffer[0]);
    data.y = (int16_t)((buffer[3] << 8) | buffer[2]);
    data.z = (int16_t)((buffer[5] << 8) | buffer[4]);
    
    // 更新传感器数据
    UpdateSensorData(&data);
}
2. EEPROM批量读写操作

对于I2C EEPROM的大块数据读写,DMA提供了显著性能优势:

#define EEPROM_ADDR     0x50    // EEPROM I2C地址
#define PAGE_SIZE       64      // EEPROM页大小

// 读取EEPROM数据
void EEPROM_ReadData(uint16_t address, uint8_t *buffer, uint16_t size) {
    uint8_t addrBytes[2];
    
    // 准备地址字节(大端序)
    addrBytes[0] = (address >> 8) & 0xFF;  // 高字节
    addrBytes[1] = address & 0xFF;         // 低字节
    
    // 发送地址
    HAL_I2C_Master_Transmit(&hi2c1, EEPROM_ADDR << 1, addrBytes, 2, HAL_MAX_DELAY);
    
    // 使用DMA读取数据
    HAL_I2C_Master_Receive_DMA(&hi2c1, EEPROM_ADDR << 1, buffer, size);
}

// 写入EEPROM数据
void EEPROM_WriteData(uint16_t address, uint8_t *data, uint16_t size) {
    uint16_t bytesWritten = 0;
    uint16_t currentPage, bytesToWrite;
    
    while(bytesWritten < size) {
        // 计算当前页
        currentPage = (address + bytesWritten) / PAGE_SIZE;
        
        // 计算当前页剩余空间
        bytesToWrite = PAGE_SIZE - ((address + bytesWritten) % PAGE_SIZE);
        
        // 确保不超出要写入的总数据量
        if(bytesToWrite > (size - bytesWritten))
            bytesToWrite = size - bytesWritten;
        
        // 写入单页数据
        EEPROM_WritePage(address + bytesWritten, data + bytesWritten, bytesToWrite);
        
        // 等待写入完成
        while(HAL_I2C_IsDeviceReady(&hi2c1, EEPROM_ADDR << 1, 10, HAL_MAX_DELAY) != HAL_OK);
        
        bytesWritten += bytesToWrite;
    }
}

// 写入单页EEPROM数据
void EEPROM_WritePage(uint16_t address, uint8_t *data, uint16_t size) {
    uint8_t *buffer = malloc(size + 2);
    if(buffer == NULL) return;
    
    // 准备地址字节
    buffer[0] = (address >> 8) & 0xFF;  // 高字节
    buffer[1] = address & 0xFF;         // 低字节
    
    // 准备数据
    memcpy(buffer + 2, data, size);
    
    // 使用DMA发送地址+数据
    HAL_I2C_Master_Transmit_DMA(&hi2c1, EEPROM_ADDR << 1, buffer, size + 2);
    
    // 注意:必须在传输完成后释放buffer
}
3. 多传感器数据融合系统

在复杂系统中,可能需要从多个I2C设备收集数据并进行处理:

#define NUM_SENSORS 5
typedef struct {
    uint8_t address;
    uint8_t dataReg;
    uint16_t dataSize;
    uint8_t *buffer;
    void (*processFunc)(uint8_t*);
} Sensor_t;

Sensor_t sensors[NUM_SENSORS];
volatile uint8_t currentSensor = 0;
volatile bool transferInProgress = false;

// 初始化传感器配置
void InitSensorSystem(void) {
    // 配置加速度计
    sensors[0].address = ACC_ADDR;
    sensors[0].dataReg = ACC_DATA_REG;
    sensors[0].dataSize = 6;
    sensors[0].buffer = malloc(6);
    sensors[0].processFunc = ProcessAccData;
    
    // 配置磁力计
    sensors[1].address = MAG_ADDR;
    sensors[1].dataReg = MAG_DATA_REG;
    sensors[1].dataSize = 6;
    sensors[1].buffer = malloc(6);
    sensors[1].processFunc = ProcessMagData;
    
    // 配置其他传感器...
}

// 启动连续传感器读取
void StartSensorReading(void) {
    if(!transferInProgress) {
        currentSensor = 0;
        ReadNextSensor();
    }
}

// 读取下一个传感器
void ReadNextSensor(void) {
    transferInProgress = true;
    I2C_DMA_ReadRegister(sensors[currentSensor].address, 
                        sensors[currentSensor].dataReg,
                        sensors[currentSensor].buffer,
                        sensors[currentSensor].dataSize);
}

// 自定义DMA接收完成回调
void I2C_RxComplete_Callback(void) {
    // 处理当前传感器数据
    sensors[currentSensor].processFunc(sensors[currentSensor].buffer);
    
    // 准备读取下一个传感器
    currentSensor = (currentSensor + 1) % NUM_SENSORS;
    
    if(currentSensor == 0) {
        // 完成一轮读取,触发数据融合
        FuseSensorData();
    }
    
    // 开始下一个传感器读取
    ReadNextSensor();
}

4. 性能对比分析

4.1 CPU使用率

在传统轮询或中断驱动的数据传输中,CPU负责处理每个数据字节,导致较高的CPU占用。而使用DMA可以显著降低CPU负载:

传输方式 CPU使用率(典型值) 说明
轮询传输 70-100% CPU持续等待并处理每个数据字节
中断传输 20-40% 每个字节产生中断,由CPU处理
DMA传输 1-5% 只在传输开始和结束时需要CPU干预

DMA传输期间,CPU可以执行其他任务,如算法处理、用户界面更新等,大幅提高系统整体效率。

4.2 传输效率

针对不同传输量场景的性能对比(基于400kHz I2C和10MHz SPI的典型值):

数据量 SPI轮询 SPI+DMA 提升比例 I2C轮询 I2C+DMA 提升比例
16字节 0.1ms 0.08ms 20% 0.4ms 0.35ms 12%
256字节 0.26ms 0.2ms 23% 6.5ms 5.6ms 14%
4KB 4.1ms 3.3ms 19% 102ms 91ms 11%

注意:

  • 小数据量传输中,DMA的优势较小,因为DMA配置本身也有一定开销
  • 大数据量传输中,DMA的优势更为显著
  • SPI通常比I2C传输速率高一个数量级
  • DMA对I2C的性能提升通常低于对SPI的提升,因为I2C协议本身有更多开销(地址设置、应答等)

4.3 响应时间

采用DMA的另一个关键优势是改善系统的响应时间:

方式 最大中断延迟 实时性影响
轮询传输 不适用(阻塞) 严重影响系统响应时间
中断传输 中等(每字节中断) 频繁中断可能影响其他任务
DMA传输 最低(仅传输完成时) 最小影响,适合实时系统

在基于RTOS的系统中,DMA传输可以让高优先级任务更及时地响应事件,提高系统实时性能。

5. 常见问题与解决方案

5.1 DMA传输中断问题

问题1:DMA传输完成但外设状态未更新

部分MCU中,DMA传输完成中断可能早于外设实际完成传输的时间点触发。

解决方案

void HAL_SPI_TxCpltCallback(SPI_HandleTypeDef *hspi) {
    // 检查SPI是否仍在忙碌状态
    while(__HAL_SPI_GET_FLAG(hspi, SPI_FLAG_BSY)) {
        // 等待SPI传输真正完成
    }
    
    // 现在可以安全地处理后续操作
    HAL_GPIO_WritePin(SPI_CS_GPIO_Port, SPI_CS_Pin, GPIO_PIN_SET);
    SPI_TransferComplete_Callback();
}

问题2:DMA与中断优先级配置不当导致的竞态条件

解决方案

  • 确保DMA中断优先级高于使用传输结果的任务优先级
  • 在关键部分使用临界区保护共享资源
void ProcessDMACompletedData(void) {
    // 进入临界区
    portENTER_CRITICAL();
    
    // 处理DMA已完成的数据
    // ...
    
    // 离开临界区
    portEXIT_CRITICAL();
}

问题3:DMA传输未完成时启动新传输

解决方案

// 使用标志跟踪DMA状态
volatile bool dmaTransferInProgress = false;

bool StartNewDMATransfer(uint8_t *data, uint16_t size) {
    // 检查DMA是否忙
    if(dmaTransferInProgress) {
        return false;  // DMA忙,无法启动新传输
    }
    
    dmaTransferInProgress = true;
    HAL_SPI_Transmit_DMA(&hspi1, data, size);
    return true;
}

void HAL_SPI_TxCpltCallback(SPI_HandleTypeDef *hspi) {
    // 标记DMA传输完成
    dmaTransferInProgress = false;
    
    // 其他处理...
}

5.2 数据一致性问题

问题1:DMA写入缓冲区的同时CPU尝试读取数据

解决方案

  • 使用双缓冲区技术
  • 使用信号量或标志指示缓冲区状态
// 简单的双缓冲实现
uint8_t buffer0[BUFFER_SIZE];
uint8_t buffer1[BUFFER_SIZE];
volatile uint8_t activeBuffer = 0;
volatile uint8_t processingBuffer = 1;

// DMA接收完成回调
void HAL_I2C_RxCpltCallback(I2C_HandleTypeDef *hi2c) {
    // 切换缓冲区
    uint8_t temp = activeBuffer;
    activeBuffer = processingBuffer;
    processingBuffer = temp;
    
    // 启动新的接收
    HAL_I2C_Receive_DMA(&hi2c1, 
                         activeBuffer == 0 ? buffer0 : buffer1, 
                         BUFFER_SIZE);
    
    // 通知数据处理任务
    osSignalSet(dataProcessTaskHandle, DATA_READY_SIGNAL);
}

// 数据处理任务
void DataProcessTask(void const *argument) {
    while(1) {
        // 等待数据就绪信号
        osSignalWait(DATA_READY_SIGNAL, osWaitForever);
        
        // 处理非活动缓冲区的数据
        ProcessBuffer(processingBuffer == 0 ? buffer0 : buffer1);
    }
}

问题2:CPU修改DMA正在发送的数据

解决方案

  • 在发送前复制数据到专用的DMA缓冲区
  • 使用信号量防止多次写入
uint8_t dmaTxBuffer[TX_BUFFER_SIZE];
SemaphoreHandle_t dmaTxSemaphore;

void InitDMATx(void) {
    dmaTxSemaphore = xSemaphoreCreateBinary();
    xSemaphoreGive(dmaTxSemaphore);  // 初始为可用状态
}

bool SendDataWithDMA(uint8_t *data, uint16_t size) {
    if(size > TX_BUFFER_SIZE) return false;
    
    // 尝试获取信号量
    if(xSemaphoreTake(dmaTxSemaphore, pdMS_TO_TICKS(10)) == pdTRUE) {
        // 复制数据到DMA缓冲区
        memcpy(dmaTxBuffer, data, size);
        
        // 启动DMA传输
        HAL_SPI_Transmit_DMA(&hspi1, dmaTxBuffer, size);
        return true;
    }
    
    return false;  // 无法获取信号量,传输忙
}

void HAL_SPI_TxCpltCallback(SPI_HandleTypeDef *hspi) {
    // 释放信号量,允许新传输
    xSemaphoreGive(dmaTxSemaphore);
    
    // 其他处理...
}

问题3:缓存一致性问题(在具有缓存的系统上)

解决方案

  • 使用非缓存区域进行DMA传输
  • 在传输前后执行缓存操作
void FlushDMABufferForCPU(void *buffer, uint32_t size) {
    // 对于基于Cortex-M7等带缓存的MCU
    SCB_InvalidateDCache_by_Addr((uint32_t*)buffer, size);
}

void FlushDMABufferForDMA(void *buffer, uint32_t size) {
    // 确保CPU所做的更改对DMA可见
    SCB_CleanDCache_by_Addr((uint32_t*)buffer, size);
}

5.3 调试技巧

DMA传输故障诊断流程

  1. 确认DMA配置

    • 检查DMA流和通道是否正确映射到外设
    • 验证地址增量设置
    • 检查数据宽度(字节、半字、字)配置
  2. 检查DMA传输状态标志

void CheckDMAStatus(DMA_HandleTypeDef *hdma) {
    uint32_t status = hdma->Instance->ISR;
    
    if(status & DMA_FLAG_TEIF) {
        printf("DMA Transfer Error\r\n");
    }
    if(status & DMA_FLAG_HTIF) {
        printf("DMA Half Transfer\r\n");
    }
    if(status & DMA_FLAG_TCIF) {
        printf("DMA Transfer Complete\r\n");
    }
    if(status & DMA_FLAG_FEIF) {
        printf("DMA FIFO Error\r\n");
    }
}
  1. 使用调试计数器跟踪传输进度
volatile uint32_t dmaTransferStartCount = 0;
volatile uint32_t dmaTransferCompleteCount = 0;
volatile uint32_t dmaErrorCount = 0;

void StartDMATransfer(uint8_t *data, uint16_t size) {
    dmaTransferStartCount++;
    HAL_SPI_Transmit_DMA(&hspi1, data, size);
}

void HAL_SPI_TxCpltCallback(SPI_HandleTypeDef *hspi) {
    dmaTransferCompleteCount++;
}

void HAL_SPI_ErrorCallback(SPI_HandleTypeDef *hspi) {
    dmaErrorCount++;
}

// 在调试窗口监视这些计数器
  1. 使用逻辑分析仪监控外设信号

    • 监控SPI/I2C总线活动
    • 验证时序与协议是否符合预期
    • 检查传输完成与DMA中断之间的关系
  2. 检查内存对齐

// 确保DMA缓冲区正确对齐(某些MCU要求)
// 使用属性指定对齐方式
uint8_t rxBuffer[RX_BUFFER_SIZE] __attribute__((aligned(4)));

// 或在运行时检查
if((uint32_t)buffer % 4 != 0) {
    printf("Warning: Buffer not aligned to 4 bytes\r\n");
}

6. 最佳实践与优化技巧

6.1 缓冲区管理

高效缓冲区策略

  1. 双缓冲与三缓冲
// 三缓冲实现
#define NUM_BUFFERS 3
uint8_t buffers[NUM_BUFFERS][BUFFER_SIZE];
volatile uint8_t activeBuffer = 0;       // DMA当前使用
volatile uint8_t processingBuffer = -1;  // 处理中
volatile uint8_t readyBuffer = -1;       // 待处理

// 启动初始传输
void StartInitialTransfer(void) {
    HAL_SPI_Receive_DMA(&hspi1, buffers[activeBuffer], BUFFER_SIZE);
}

// DMA完成回调
void HAL_SPI_RxCpltCallback(SPI_HandleTypeDef *hspi) {
    // 标记当前活动缓冲区为就绪
    if(readyBuffer == -1) {
        readyBuffer = activeBuffer;
    }
    else if(processingBuffer == -1) {
        // 特殊情况:处理速度快于接收
        processingBuffer = readyBuffer;
        readyBuffer = activeBuffer;
    }
    
    // 轮换到下一个可用缓冲区
    uint8_t nextBuffer = (activeBuffer + 1) % NUM_BUFFERS;
    while(nextBuffer == processingBuffer) {
        // 所有缓冲区都在使用中,等待
        // 实际应用中可使用信号量
    }
    
    activeBuffer = nextBuffer;
    
    // 启动新传输
    HAL_SPI_Receive_DMA(&hspi1, buffers[activeBuffer], BUFFER_SIZE);
    
    // 通知处理任务
    osSignalSet(processTaskHandle, 0x01);
}

// 处理任务
void ProcessTask(void const *argument) {
    while(1) {
        // 等待信号
        osSignalWait(0x01, osWaitForever);
        
        // 检查是否有就绪缓冲区
        if(readyBuffer != -1) {
            processingBuffer = readyBuffer;
            readyBuffer = -1;
            
            // 处理数据
            ProcessBuffer(buffers[processingBuffer]);
            
            // 标记处理完成
            processingBuffer = -1;
        }
    }
}
  1. 环形缓冲区
typedef struct {
    uint8_t *buffer;
    uint32_t size;
    volatile uint32_t head;  // DMA写入位置
    volatile uint32_t tail;  // 处理读取位置
    volatile uint32_t count; // 可用数据量
} RingBuffer_t;

RingBuffer_t rxRing;

// 初始化环形缓冲区
void RingBuffer_Init(RingBuffer_t *ring, uint8_t *buffer, uint32_t size) {
    ring->buffer = buffer;
    ring->size = size;
    ring->head = 0;
    ring->tail = 0;
}

// DMA半传输完成回调
void HAL_UART_RxHalfCpltCallback(UART_HandleTypeDef *huart) {
    // 更新环形缓冲区头指针
    rxRing.head = rxRing.size / 2;
    rxRing.count = rxRing.head - rxRing.tail;
    if(rxRing.count < 0) rxRing.count += rxRing.size;
    
    // 通知处理任务
    osSignalSet(processTaskHandle, 0x01);
}

// DMA全传输完成回调
void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart) {
    // 更新环形缓冲区头指针
    rxRing.head = rxRing.size;
    rxRing.count = rxRing.head - rxRing.tail;
    if(rxRing.count < 0) rxRing.count += rxRing.size;
    
    // 通知处理任务
    osSignalSet(processTaskHandle, 0x01);
    
    // 重置DMA传输
    HAL_UART_Receive_DMA(huart, rxRing.buffer, rxRing.size);
}
  1. 零拷贝技术
// 使用指针交换代替内存复制
void SwapBuffers(uint8_t **a, uint8_t **b) {
    uint8_t *temp = *a;
    *a = *b;
    *b = temp;
}

// 在双缓冲系统中使用
uint8_t *activeBuffer = buffer0;
uint8_t *processBuffer = buffer1;

void HAL_SPI_RxCpltCallback(SPI_HandleTypeDef *hspi) {
    // 交换缓冲区指针
    SwapBuffers(&activeBuffer, &processBuffer);
    
    // 启动新传输
    HAL_SPI_Receive_DMA(&hspi1, activeBuffer, BUFFER_SIZE);
    
    // 处理数据(或通知处理任务)
    ProcessBuffer(processBuffer);
}

6.2 中断处理优化

高效中断处理

  1. 最小化中断处理函数
// 在中断处理函数中仅做必要工作
void HAL_SPI_RxCpltCallback(SPI_HandleTypeDef *hspi) {
    // 标记接收完成
    rxDataReady = true;
    
    // 重新启动DMA(如需要)
    HAL_SPI_Receive_DMA(&hspi1, rxBuffer, BUFFER_SIZE);
    
    // 通知主循环或任务
    osSignalSet(processTaskHandle, SIGNAL_DATA_READY);
}

// 将数据处理放在主任务中
void ProcessTask(void const *argument) {
    while(1) {
        // 等待数据就绪信号
        osSignalWait(SIGNAL_DATA_READY, osWaitForever);
        
        // 仅在接收到数据时处理
        if(rxDataReady) {
            ProcessReceivedData();
            rxDataReady = false;
        }
    }
}
  1. 优先级管理
// 配置中断优先级
void ConfigureInterruptPriorities(void) {
    // DMA中断较高优先级
    HAL_NVIC_SetPriority(DMA1_Stream0_IRQn, 5, 0);
    HAL_NVIC_SetPriority(DMA1_Stream6_IRQn, 5, 0);
    
    // 通信错误中断最高优先级
    HAL_NVIC_SetPriority(I2C1_ER_IRQn, 4, 0);
    
    // 通信事件中断中等优先级
    HAL_NVIC_SetPriority(I2C1_EV_IRQn, 6, 0);
    
    // 应用任务低优先级
    // ...
}
  1. 串联DMA传输
// 定义传输队列
typedef struct {
    uint8_t *buffer;
    uint16_t size;
} DMATxItem_t;

#define MAX_TX_QUEUE 10
DMATxItem_t txQueue[MAX_TX_QUEUE];
volatile uint8_t queueHead = 0;
volatile uint8_t queueTail = 0;
volatile uint8_t queueCount = 0;
volatile bool dmaActive = false;

// 添加传输请求到队列
bool EnqueueDMATransfer(uint8_t *data, uint16_t size) {
    if(queueCount >= MAX_TX_QUEUE) {
        return false;  // 队列已满
    }
    
    // 添加到队列
    txQueue[queueTail].buffer = data;
    txQueue[queueTail].size = size;
    queueTail = (queueTail + 1) % MAX_TX_QUEUE;
    queueCount++;
    
    // 如果DMA空闲,启动传输
    if(!dmaActive) {
        StartNextDMATransfer();
    }
    
    return true;
}

// 启动下一个DMA传输
void StartNextDMATransfer(void) {
    if(queueCount == 0) {
        dmaActive = false;
        return;  // 队列为空
    }
    
    dmaActive = true;
    HAL_SPI_Transmit_DMA(&hspi1, txQueue[queueHead].buffer, txQueue[queueHead].size);
}

// 传输完成回调
void HAL_SPI_TxCpltCallback(SPI_HandleTypeDef *hspi) {
    // 移除已完成的传输
    queueHead = (queueHead + 1) % MAX_TX_QUEUE;
    queueCount--;
    
    // 启动下一个传输
    StartNextDMATransfer();
}

6.3 电源管理考量

在电池供电的系统中,DMA可以显著提高能效:

  1. 在DMA传输期间进入低功耗模式
void StartEfficientDataTransfer(uint8_t *data, uint16_t size) {
    // 准备低功耗模式唤醒源
    PrepareWakeupSource(DMA_COMPLETE_IRQ);
    
    // 启动DMA传输
    HAL_SPI_Transmit_DMA(&hspi1, data, size);
    
    // 进入等待模式,DMA将在后台运行
    EnterLowPowerMode();
    
    // DMA完成中断将唤醒CPU
}

// DMA完成中断处理函数
void HAL_SPI_TxCpltCallback(SPI_HandleTypeDef *hspi) {
    // 唤醒后处理
    HandleTransferCompletion();
}
  1. 选择最佳DMA传输大小
// 权衡DMA设置开销和能效
void OptimizeTransferSize(uint8_t *data, uint32_t totalSize) {
    // 对于小数据量,使用传统方法
    if(totalSize < DMA_THRESHOLD_SIZE) {
        HAL_I2C_Master_Transmit(&hi2c1, slaveAddress, data, totalSize, HAL_MAX_DELAY);
        return;
    }
    
    // 对于大量数据,使用分段DMA传输
    uint32_t offset = 0;
    while(offset < totalSize) {
        uint16_t chunkSize = MIN(MAX_DMA_CHUNK_SIZE, totalSize - offset);
        
        HAL_I2C_Master_Transmit_DMA(&hi2c1, slaveAddress, data + offset, chunkSize);
        
        // 等待传输完成
        while(transferInProgress) {
            // 可以在此进入低功耗模式
            EnterLightSleep();
        }
        
        offset += chunkSize;
    }
}
  1. 优化唤醒时间
// 配置DMA完成中断的快速唤醒响应
void ConfigureForFastWakeup(void) {
    // 确保DMA中断优先级足够高
    HAL_NVIC_SetPriority(DMA1_Stream6_IRQn, 0, 0);
    
    // 预配置DMA,减少唤醒后的设置时间
    PreConfigureDMAForNextTransfer();
    
    // 使用快速启动外设模式(如果MCU支持)
    EnablePeripheralFastStartup(I2C1);
}

// 快速响应配置
void DMA_IRQHandler(void) {
    // 在处理任何事情之前快速清除挂起位
    CLEAR_PENDING_DMA_IRQ();
    
    // 最小化中断处理,仅记录完成并退出
    MarkTransferComplete();
    
    // 详细处理将在主循环中进行
}

总结

将DMA与SPI和I2C结合使用可以显著提高嵌入式系统的性能和能效。关键优势包括降低CPU负载、增加数据吞吐量、提高系统实时响应性以及降低功耗。

实施时应考虑以下最佳实践:

  1. 优化缓冲区管理,如使用双缓冲或环形缓冲区
  2. 最小化中断处理函数,将数据处理移至主任务
  3. 妥善处理数据一致性问题,特别是在RTOS环境中
  4. 为DMA传输设计适当的错误检测和恢复机制
  5. 在电池供电系统中利用DMA+低功耗模式最大化能效

通过仔细实施这些策略,可以构建更高效、更可靠的嵌入式通信系统,同时减轻微控制器的负担,使其能够执行更多有价值的计算任务。

参考资料

  1. STMicroelectronics, “STM32 DMA控制器应用手册”
  2. NXP Semiconductors, “使用DMA提高I2C和SPI性能”
  3. Texas Instruments, “MSP432微控制器DMA应用指南”
  4. FreeRTOS, “使用DMA进行外设通信的RTOS考量”
  5. ARM, “Cortex-M DMA传输和缓存一致性”