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发送数据</
2万+




被折叠的 条评论
为什么被折叠?



