当前位置: 首页 > wzjs >正文

gif表情包在线制作网站wordpress壁纸主题下载

gif表情包在线制作网站,wordpress壁纸主题下载,c2c网站的类型,cms系统排行榜DMA在SPI和I2C通信中的应用详解 目录 1. DMA基本原理 1.1 什么是DMA1.2 DMA工作模式1.3 DMA在嵌入式系统中的优势 2. DMASPI通信实现 2.1 SPIDMA工作原理2.2 配置步骤2.3 代码实现示例2.4 高级应用场景 3. DMAI2C通信实现 3.1 I2CDMA工作原理3.2 配置步骤3.3 代码实现示例3.4…

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

目录

  • 1. DMA基本原理
    • 1.1 什么是DMA
    • 1.2 DMA工作模式
    • 1.3 DMA在嵌入式系统中的优势
  • 2. DMA+SPI通信实现
    • 2.1 SPI+DMA工作原理
    • 2.2 配置步骤
    • 2.3 代码实现示例
    • 2.4 高级应用场景
  • 3. DMA+I2C通信实现
    • 3.1 I2C+DMA工作原理
    • 3.2 配置步骤
    • 3.3 代码实现示例
    • 3.4 高级应用场景
  • 4. 性能对比分析
    • 4.1 CPU使用率
    • 4.2 传输效率
    • 4.3 响应时间
  • 5. 常见问题与解决方案
    • 5.1 DMA传输中断问题
    • 5.2 数据一致性问题
    • 5.3 调试技巧
  • 6. 最佳实践与优化技巧
    • 6.1 缓冲区管理
    • 6.2 中断处理优化
    • 6.3 电源管理考量

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);// 配置SPIhspi1.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 - TXhdma_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; // 地址字节3cmd[2] = (address >> 16) & 0xFF; // 地址字节2cmd[3] = (address >> 8) & 0xFF;  // 地址字节1cmd[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);// 在另一个任务中处理buffer1ProcessBuffer(buffer1);} else {HAL_SPI_Receive_DMA(&hspi1, buffer1, BUFFER_SIZE);// 在另一个任务中处理buffer0ProcessBuffer(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);// 配置I2Chi2c1.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 - TXhdma_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 - RXhdma_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.1ms0.08ms20%0.4ms0.35ms12%
256字节0.26ms0.2ms23%6.5ms5.6ms14%
4KB4.1ms3.3ms19%102ms91ms11%

注意:

  • 小数据量传输中,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等带缓存的MCUSCB_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传输和缓存一致性”

文章转载自:

http://C2iB2EAk.jqkrt.cn
http://xnuAf7kg.jqkrt.cn
http://fFPTprEW.jqkrt.cn
http://NlUp8BEE.jqkrt.cn
http://cczj2anF.jqkrt.cn
http://AmThHMae.jqkrt.cn
http://SZIdAAVw.jqkrt.cn
http://bDRlL3jP.jqkrt.cn
http://Pydd0GjA.jqkrt.cn
http://7rLCehpY.jqkrt.cn
http://OTVhztcV.jqkrt.cn
http://M0tsy7Ss.jqkrt.cn
http://Jnowek2O.jqkrt.cn
http://tNbk4oIQ.jqkrt.cn
http://srfqCXsm.jqkrt.cn
http://AeObc8rf.jqkrt.cn
http://L8ffgYGV.jqkrt.cn
http://GStmPNHu.jqkrt.cn
http://Xuxbj9kl.jqkrt.cn
http://fzv2Zfe7.jqkrt.cn
http://YwBaWaWG.jqkrt.cn
http://pyJxEia2.jqkrt.cn
http://JSNiR49F.jqkrt.cn
http://Vz0ZR6j0.jqkrt.cn
http://uSN8QzZu.jqkrt.cn
http://JOdoN8oM.jqkrt.cn
http://Chg84rFg.jqkrt.cn
http://hBXlD8Zw.jqkrt.cn
http://xYXzmnOb.jqkrt.cn
http://58ltEo2D.jqkrt.cn
http://www.dtcms.com/wzjs/626387.html

相关文章:

  • 中文企业网站模板下载网站导航功能
  • 哪个网站是专门做装修的建设银行考试报名网站
  • 用jsp做学校网站建设网站聊天室
  • 怎么给网站加友情链接网站建设定义是什么意思
  • 网站开发包括几个部分自动点击器安卓
  • 定制网站和模板建站哪个好用工程造价价格信息网
  • 做淘宝代码的网站wordpress tag搜索
  • 网站开发需要的知识和技术百度竞价教程
  • 深圳 企业 网站建设高端网站设计元素图片
  • 青岛哪家做网站的公司好wordpress怎样设置留言
  • 网站建设推广实训总结网站代码字体变大
  • 临夏市建设局网站闽侯福州网站建设
  • 凡科建设网站如何如何做网站首页收录
  • 番禺网站建设效果网站运营专员岗位要求
  • 产品网站建设框架泰安微信网站制作
  • 做购物网站流程站长工具的使用seo综合查询排名
  • 三角镇建网站公司企业信息系统查询系统官网江苏
  • 深圳网站制作易捷网络微信公众号开发步骤
  • pc端宣传网站开发办网站需要备案吗
  • 黄浦网站建设公司动画形式的h5在哪个网站做
  • 国家外管局网站怎么做收汇代卖平台哪个好
  • 网站建设费记在什么科目下专业做微视频的网站
  • 网站改版怎么弄青岛专业网站开发公司
  • 为某网站做网站推广策划方案推广教程
  • 五华区网站住房城乡建设部官网
  • 开办 网站建设费 科目通过法人姓名查企业
  • 调颜色网站襄阳最新消息
  • 给网站设置长尾关键词网站写文案
  • 容易收录的网站一般做网站是在什么网站找素材
  • php网站的后台地址如何让自己做的网站让别人看到