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在嵌入式系统中的优势
降低CPU负载:CPU不必参与数据传输过程,可专注于其他任务。
提高传输效率:DMA控制器优化设计用于数据移动,比CPU执行循环复制更高效。
确定性时序:DMA传输通常具有可预测的时序,有助于实时系统的设计。
减少中断频率:无需每字节/字产生中断,只在传输完成时通知CPU。
降低功耗:CPU可在DMA传输过程中进入低功耗模式,节省能源。
2. DMA+SPI通信实现
2.1 SPI+DMA工作原理
SPI(Serial Peripheral Interface)是一种全双工同步串行通信接口,常用于与传感器、存储器和其他外设通信。传统的SPI实现中,CPU需要不断写入和读取SPI数据寄存器(DR),这在大数据量传输时效率低下。
当将DMA与SPI结合使用时:
发送流程:
- CPU配置SPI寄存器和DMA参数
- DMA控制器在SPI发送缓冲区准备好时,自动将下一个数据从内存传输到SPI数据寄存器
- 此过程循环直至所有数据发送完成
- DMA发出传输完成中断通知CPU
接收流程:
- CPU配置SPI寄存器和DMA参数
- 当SPI接收缓冲区有数据时,DMA自动将数据从SPI数据寄存器传输到内存
- 此过程循环直至接收完所有数据
- DMA发出传输完成中断通知CPU
全双工操作:
- 大多数SPI控制器支持同时使用两个DMA通道
- 一个通道负责TX(发送),另一个负责RX(接收)
- 两个DMA通道协同工作实现全双工数据传输
2.2 配置步骤
以STM32微控制器为例,配置SPI+DMA的基本步骤如下:
配置GPIO引脚:
- 设置SPI相关引脚(SCK、MISO、MOSI、CS)
- 配置引脚的复用功能、速度和上拉/下拉状态
配置SPI外设:
- 设置SPI模式、时钟分频、数据格式等
- 启用SPI的DMA请求功能
配置DMA:
- 设置DMA通道和流
- 配置源地址和目标地址
- 设置传输方向、大小和模式
- 配置优先级和中断
启动传输:
- 启用DMA
- 启用SPI
- 开始传输过程
处理完成中断:
- 接收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传输效率:
主设备发送流程:
- CPU配置I2C寄存器设置从机地址、传输方向等
- CPU配置DMA传输参数
- DMA控制器将数据从内存直接传输到I2C数据寄存器
- I2C控制器处理起始条件、地址发送、数据传输和停止条件等时序
- 传输完成后DMA触发中断通知CPU
主设备接收流程:
- CPU配置I2C和DMA参数
- I2C控制器自动处理起始条件、从机寻址等
- 接收的数据通过DMA直接从I2C数据寄存器传输到内存
- 传输完成后通知CPU
主要差异(与SPI+DMA相比):
- I2C通信需要完整的寻址过程
- I2C通信包含应答机制(ACK/NACK)
- I2C通信中存在时钟拉伸的可能性
- I2C通常是半双工通信
3.2 配置步骤
以STM32微控制器为例,配置I2C+DMA的基本步骤如下:
配置GPIO引脚:
- 设置I2C相关引脚(SCL、SDA)为开漏输出模式
- 配置上拉电阻和复用功能
配置I2C外设:
- 设置时钟速度(标准模式100kHz或快速模式400kHz)
- 配置地址模式(7位或10位)
- 启用I2C的DMA请求功能
配置DMA:
- 配置DMA通道和流
- 设置源地址和目标地址
- 配置传输方向(内存到外设或外设到内存)
- 设置数据宽度、增量模式等参数
启动传输:
- 启用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, ®Address, 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传输故障诊断流程:
确认DMA配置:
- 检查DMA流和通道是否正确映射到外设
- 验证地址增量设置
- 检查数据宽度(字节、半字、字)配置
检查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");
}
}
- 使用调试计数器跟踪传输进度:
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++;
}
// 在调试窗口监视这些计数器
使用逻辑分析仪监控外设信号:
- 监控SPI/I2C总线活动
- 验证时序与协议是否符合预期
- 检查传输完成与DMA中断之间的关系
检查内存对齐:
// 确保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 缓冲区管理
高效缓冲区策略:
- 双缓冲与三缓冲:
// 三缓冲实现
#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;
}
}
}
- 环形缓冲区:
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);
}
- 零拷贝技术:
// 使用指针交换代替内存复制
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 中断处理优化
高效中断处理:
- 最小化中断处理函数:
// 在中断处理函数中仅做必要工作
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;
}
}
}
- 优先级管理:
// 配置中断优先级
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);
// 应用任务低优先级
// ...
}
- 串联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可以显著提高能效:
- 在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();
}
- 选择最佳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;
}
}
- 优化唤醒时间:
// 配置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负载、增加数据吞吐量、提高系统实时响应性以及降低功耗。
实施时应考虑以下最佳实践:
- 优化缓冲区管理,如使用双缓冲或环形缓冲区
- 最小化中断处理函数,将数据处理移至主任务
- 妥善处理数据一致性问题,特别是在RTOS环境中
- 为DMA传输设计适当的错误检测和恢复机制
- 在电池供电系统中利用DMA+低功耗模式最大化能效
通过仔细实施这些策略,可以构建更高效、更可靠的嵌入式通信系统,同时减轻微控制器的负担,使其能够执行更多有价值的计算任务。
参考资料
- STMicroelectronics, “STM32 DMA控制器应用手册”
- NXP Semiconductors, “使用DMA提高I2C和SPI性能”
- Texas Instruments, “MSP432微控制器DMA应用指南”
- FreeRTOS, “使用DMA进行外设通信的RTOS考量”
- ARM, “Cortex-M DMA传输和缓存一致性”