news 2026/5/1 2:02:17

GD32F103实战:SPI+DMA高效数据搬运配置详解

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
GD32F103实战:SPI+DMA高效数据搬运配置详解

1. 为什么需要SPI+DMA?

在嵌入式开发中,SPI(Serial Peripheral Interface)是最常用的高速通信接口之一,常用于连接Flash、显示屏、传感器等外设。但传统的SPI轮询或中断方式有个致命问题:每传输一个字节都需要CPU介入,当数据量达到KB级别时,CPU会被完全拖垮。

我曾在项目中遇到过这样的场景:需要从SPI Flash读取一张800x480的图片数据,采用轮询方式传输时,CPU占用率直接飙到90%以上,系统几乎无法响应其他任务。后来改用DMA(Direct Memory Access)方案后,CPU占用直接降到5%以下,整个过程就像给SPI接口装上了自动驾驶系统。

DMA的本质是硬件级的数据搬运工,它能直接在存储器和外设之间搬运数据,完全不需要CPU参与。具体到GD32F103的SPI+DMA方案,主要有三大优势:

  • 零CPU占用:数据传输全程由DMA控制器接管
  • 双工传输效率翻倍:可同时配置发送和接收DMA通道
  • 硬件级稳定性:避免因中断延迟导致的数据丢失

注意:GD32的DMA通道与STM32有所不同,配置时务必参考官方《DMA请求映射表》

2. GD32F103的SPI主模式配置

2.1 硬件连接检查

在开始写代码前,先确认硬件连接。以SPI0为例(对应GD32库中的SPI1),典型引脚配置如下:

引脚功能引脚号配置模式
CSPA4推挽输出
SCKPA5复用推挽输出
MOSIPA7复用推挽输出
MISOPA6浮空输入

这里有个坑点要注意:GD32的SPI外设编号在时钟使能时从0开始计数(如SPI0),但在初始化函数中又从1开始计数(如SPI1)。我第一次移植STM32代码时就栽在这个细节上,调试了半天才发现问题。

2.2 初始化代码详解

完整的SPI主模式初始化代码如下,关键参数已用注释标注:

void SPI1Init(void) { GPIO_InitPara GPIO_InitStructure; SPI_InitPara SPI_InitStructure; // 开启GPIO和SPI时钟 rcu_periph_clock_enable(RCU_GPIOA); rcu_periph_clock_enable(RCU_SPI0); // 注意这里是SPI0 // 配置SCK和MOSI为复用推挽输出 GPIO_InitStructure.GPIO_Pin = GPIO_PIN_5 | GPIO_PIN_7; GPIO_InitStructure.GPIO_Mode = GPIO_MODE_AF_PP; GPIO_InitStructure.GPIO_Speed = GPIO_SPEED_50MHZ; GPIO_Init(GPIOA, &GPIO_InitStructure); // 配置MISO为浮空输入 GPIO_InitStructure.GPIO_Pin = GPIO_PIN_6; GPIO_InitStructure.GPIO_Mode = GPIO_MODE_IN_FLOATING; GPIO_Init(GPIOA, &GPIO_InitStructure); // 配置CS引脚为推挽输出 GPIO_InitStructure.GPIO_Pin = GPIO_PIN_4; GPIO_InitStructure.GPIO_Mode = GPIO_MODE_OUT_PP; GPIO_Init(GPIOA, &GPIO_InitStructure); SPI1_CS_HIGH(); // 默认拉高CS // SPI参数配置 SPI_InitStructure.SPI_TransType = SPI_TRANSTYPE_FULLDUPLEX; // 全双工模式 SPI_InitStructure.SPI_Mode = SPI_MODE_MASTER; // 主模式 SPI_InitStructure.SPI_FrameFormat = SPI_FRAMEFORMAT_8BIT; // 8位数据帧 SPI_InitStructure.SPI_SCKPL = SPI_SCKPL_LOW; // 时钟极性 SPI_InitStructure.SPI_SCKPH = SPI_SCKPH_1EDGE; // 时钟相位 SPI_InitStructure.SPI_SWNSSEN = SPI_SWNSS_SOFT; // 软件控制CS SPI_InitStructure.SPI_PSC = SPI_PSC_4; // 时钟预分频 SPI_InitStructure.SPI_FirstBit = SPI_FIRSTBIT_MSB; // 高位先行 SPI_Init(SPI1, &SPI_InitStructure); // 注意这里变成SPI1 SPI_Enable(SPI1, ENABLE); // 使能SPI }

实测中发现,当SPI时钟超过18MHz时,需要将GPIO速度设置为50MHz以上,否则会出现波形畸变。如果使用杜邦线连接,建议将时钟降到10MHz以下。

3. DMA通道配置实战

3.1 DMA通道映射关系

GD32F103的DMA通道与STM32有所不同,以下是SPI0对应的DMA请求映射:

外设方向DMA0通道请求编号
SPI0_TX发送通道31
SPI0_RX接收通道22

这个映射关系非常重要,我曾经因为搞混通道号导致DMA无法触发,后来在手册第23章找到了这张表才解决问题。

3.2 双通道DMA初始化

发送和接收需要分别配置DMA通道,以下是完整配置代码:

void SPI1DMAInit(void) { DMA_InitPara DMA_InitStructure; rcu_periph_clock_enable(RCU_DMA0); // 发送DMA配置(内存->SPI) DMA_DeInit(DMA1_CHANNEL3); DMA_InitStructure.DMA_PeripheralBaseAddr = (uint32_t)&(SPI1->DTR); DMA_InitStructure.DMA_MemoryBaseAddr = 0; // 动态设置 DMA_InitStructure.DMA_DIR = DMA_DIR_PERIPHERALDST; DMA_InitStructure.DMA_BufferSize = 0; DMA_InitStructure.DMA_PeripheralInc = DMA_PERIPHERALINC_DISABLE; DMA_InitStructure.DMA_MemoryInc = DMA_MEMORYINC_ENABLE; DMA_InitStructure.DMA_PeripheralDataSize = DMA_PERIPHERALDATASIZE_BYTE; DMA_InitStructure.DMA_MemoryDataSize = DMA_MEMORYDATASIZE_BYTE; DMA_InitStructure.DMA_Mode = DMA_MODE_NORMAL; DMA_InitStructure.DMA_Priority = DMA_PRIORITY_MEDIUM; DMA_Init(DMA1_CHANNEL3, &DMA_InitStructure); // 接收DMA配置(SPI->内存) DMA_DeInit(DMA1_CHANNEL2); DMA_InitStructure.DMA_DIR = DMA_DIR_PERIPHERALSRC; DMA_InitStructure.DMA_BufferSize = 0; DMA_Init(DMA1_CHANNEL2, &DMA_InitStructure); DMA_ClearIntBitState(DMA1_INT_TC2 | DMA1_INT_TC3); }

关键点说明:

  1. 发送方向配置为DMA_DIR_PERIPHERALDST(内存到外设)
  2. 接收方向配置为DMA_DIR_PERIPHERALSRC(外设到内存)
  3. 内存地址递增必须开启,否则只能传输首字节
  4. 传输数据宽度建议保持字节单位,避免对齐问题

4. 双工传输的内存管理策略

4.1 传输启动函数实现

SPI是全双工接口,发送和接收是同步进行的。以下是经过实战验证的传输函数:

void SPI1DMATransfer(uint8_t *tx_buf, uint8_t *rx_buf, uint16_t len) { // 禁用DMA通道 DMA_Enable(DMA1_CHANNEL3, DISABLE); DMA_Enable(DMA1_CHANNEL2, DISABLE); // 等待通道就绪 while(DMA_GetCmdStatus(DMA1_CHANNEL3)); while(DMA_GetCmdStatus(DMA1_CHANNEL2)); // 配置发送通道 DMA_SetCurrDataCounter(DMA1_CHANNEL3, len); DMA_MemoryTargetConfig(DMA1_CHANNEL3, (uint32_t)tx_buf); // 配置接收通道 DMA_SetCurrDataCounter(DMA1_CHANNEL2, len); DMA_MemoryTargetConfig(DMA1_CHANNEL2, (uint32_t)rx_buf); // 使能SPI DMA请求 SPI_I2S_DMA_Enable(SPI1, SPI_I2S_DMA_TX, ENABLE); SPI_I2S_DMA_Enable(SPI1, SPI_I2S_DMA_RX, ENABLE); // 启动DMA传输 DMA_Enable(DMA1_CHANNEL3, ENABLE); DMA_Enable(DMA1_CHANNEL2, ENABLE); // 等待传输完成 while(!DMA_GetIntBitState(DMA1_INT_TC2)); DMA_ClearIntBitState(DMA1_INT_TC2); while(!DMA_GetIntBitState(DMA1_INT_TC3)); DMA_ClearIntBitState(DMA1_INT_TC3); }

4.2 内存地址的巧妙设计

在实际项目中,我发现可以优化内存使用:当只需要发送数据时,接收缓冲区可以用临时变量;当只需要接收数据时,发送缓冲区可以填充哑元数据。例如:

// 仅发送模式 uint8_t tx_data[256]; SPI1DMATransfer(tx_data, (uint8_t*)&dummy, 256); // 仅接收模式 uint8_t rx_data[256]; memset(tx_dummy, 0xFF, sizeof(tx_dummy)); SPI1DMATransfer(tx_dummy, rx_data, 256);

这种设计在读写SPI Flash时特别有用,既能节省内存,又能保证时序正确。

5. 常见问题与解决方案

5.1 DMA传输不触发

可能原因及解决方法:

  1. 时钟未开启:检查是否调用了rcu_periph_clock_enable(RCU_DMA0)
  2. 通道映射错误:对照手册确认SPI对应的DMA通道号
  3. SPI未使能DMA请求:检查是否调用了SPI_I2S_DMA_Enable
  4. 缓冲区地址未对齐:确保内存地址是4字节对齐的

5.2 数据错位问题

如果发现接收数据总是错位一位,可能是时钟相位配置问题。建议:

  1. 检查SPI_SCKPH参数是否符合外设要求
  2. 用逻辑分析仪捕获SPI波形,确认时钟边沿
  3. 尝试调整SPI_PSC降低时钟频率

5.3 大数据量传输稳定性

传输超过1KB数据时建议:

  1. 使用DMA_MODE_CIRCULAR循环模式
  2. 启用DMA半传输和完成中断
  3. 采用双缓冲机制(ping-pong buffer)

我在驱动SPI屏时,就采用了循环DMA+双缓冲的方案,即使传输1920x1080的图像数据也能稳定运行。关键是要合理设置DMA优先级,避免被其他高优先级DMA通道打断。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/4/16 3:48:48

Python使用正则替换字符串:从基础到进阶

在Python中,字符串替换是常见的操作,但简单的str.replace()方法只能处理固定字符串的替换。当需要模式匹配(如替换所有数字、邮箱、URL等)时,正则表达式(re模块)的re.sub()方法就派上了用场。本…

作者头像 李华
网站建设 2026/4/16 0:06:40

如何3分钟免费解锁QQ音乐加密文件:qmc-decoder完整使用指南

如何3分钟免费解锁QQ音乐加密文件:qmc-decoder完整使用指南 【免费下载链接】qmc-decoder Fastest & best convert qmc 2 mp3 | flac tools 项目地址: https://gitcode.com/gh_mirrors/qm/qmc-decoder 还在为QQ音乐下载的歌曲无法在其他播放器播放而烦恼…

作者头像 李华
网站建设 2026/4/14 17:29:42

动态规划 - 回文子串问题

回文子串 动态规划解决的回文串问题,核心是要求解“最值”(如最长回文子串)或“数目”(如回文子串总数),并且其判断过程天然存在“一个大的回文串去掉两头后依然是回文串”这种重叠子问题。 详细说明 这类…

作者头像 李华
网站建设 2026/4/16 5:15:00

twitterscraper高级查询技巧:掌握Twitter搜索运算符的完整指南

twitterscraper高级查询技巧:掌握Twitter搜索运算符的完整指南 【免费下载链接】twitterscraper Scrape Twitter for Tweets 项目地址: https://gitcode.com/gh_mirrors/tw/twitterscraper twitterscraper是一款强大的Twitter数据采集工具,能够帮…

作者头像 李华
网站建设 2026/4/15 18:36:45

别再只仿真原理图了!用ADS做微带功分器,版图仿真这一步才是关键

别再只仿真原理图了!用ADS做微带功分器,版图仿真这一步才是关键 在微波电路设计中,功分器作为基础无源器件,其性能直接影响整个系统的表现。许多工程师习惯性地将大量时间花在原理图仿真和优化上,认为只要原理图仿真结…

作者头像 李华
网站建设 2026/4/14 17:27:13

可视化操作:用Clawdbot在星图平台部署Qwen3-VL:30B,轻松接入飞书应用

可视化操作:用Clawdbot在星图平台部署Qwen3-VL:30B,轻松接入飞书应用 1. 前言:为什么选择这套方案? 在当今企业数字化转型浪潮中,将先进的多模态AI能力无缝融入日常办公流程已成为刚需。Qwen3-VL:30B作为当前最强大的…

作者头像 李华