news 2026/2/23 2:15:21

STM32 I2C DMA传输实现方法:从零实现

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
STM32 I2C DMA传输实现方法:从零实现

STM32 I2C + DMA 实战指南:如何让CPU“躺平”也能高效通信

你有没有遇到过这样的场景?
系统里接了五六个I2C传感器,定时轮询采集数据。结果发现主循环卡顿、中断满天飞,CPU占用率飙到80%以上——而真正干的活,不过是读几个字节。

问题出在哪?
传统轮询或中断方式处理I2C通信,在面对批量数据时,成了系统的性能瓶颈。

那有没有办法让CPU少操点心,把数据搬运这种“体力活”交给别人干?

答案是:用DMA(Direct Memory Access)配合STM32的硬件I2C外设

今天我们就来手把手实现一个稳定、高效、低负载的I2C-DMA传输方案,让你的MCU在后台默默完成通信任务,主线程该干嘛干嘛。


为什么需要 I2C + DMA?

先说个现实:很多开发者还在用HAL_I2C_Master_Transmit()这类阻塞函数做I2C通信。这在调试阶段没问题,但一旦上项目,尤其是多设备、高频次采集的场景下,就会暴露三大痛点:

  1. CPU占用太高:每发/收一字节都要进中断或轮询标志位;
  2. 实时性差:长数据包传输期间,其他任务被严重延迟;
  3. 容易丢数据:若中断响应不及时,RXDR寄存器可能溢出。

而DMA的出现,就是为了解决这些“脏活累活”。它像一个专职搬运工,当I2C准备好收发数据时,自动从内存搬数据到外设(或反过来),全程无需CPU插手。

目标效果:一次配置,启动传输 → CPU去跑GUI、处理算法 → 数据传完后打个招呼:“老板,活干完了。”


硬件机制拆解:I2C 和 DMA 是怎么搭上线的?

I2C 外设的关键角色

STM32的I2C不是软件模拟的GPIO翻转,而是独立硬件模块,支持:

  • 自动产生起始/停止信号
  • 地址匹配与ACK控制
  • 错误检测(NACK、总线冲突等)
  • 最关键的是:能发出DMA请求

具体来说:
- 当TXDR为空(发送寄存器空),I2C会触发一个DMA请求,告诉DMA:“我可以发数据了,快给我送!”
- 当RXDR有数据(接收寄存器非空),也会触发DMA请求:“我收到字节了,快帮我拿走!”

这两个事件分别对应I2C_TXEI2C_RXNE中断源,只不过我们不用它们来进中断,而是用来“唤醒”DMA。


DMA 控制器做了什么?

DMA的本质是外设和内存之间的直通隧道。只要配置好路线图,它就能自己搬数据。

以STM32F4为例,DMA1有6个通道,每个通道可以绑定不同的外设请求源。比如:

外设请求信号对应DMA通道
I2C1_TXI2C1_TX_DMA_STREAMDMA1_Stream6, Channel 1
I2C1_RXI2C1_RX_DMA_STREAMDMA1_Stream5, Channel 1

只要你在代码中开启TXDMAENRXDMAEN位,I2C一有需求就会拉高这个请求线,DMA立刻响应,开始搬运。

整个过程如下图所示(文字描述版):

[内存缓冲区] ←→ [DMA控制器] ←→ [I2C外设] ←→ [SDA/SCL总线] ←→ [从机]
  • 写操作:DMA 把 tx_data[] 搬到 I2C_TDR
  • 读操作:DMA 把 I2C_RDR 搬到 rx_data[]

CPU只负责三件事:
1. 配置参数
2. 启动传输
3. 收到完成通知后处理结果

其余时间,它可以休眠、调度任务、跑RTOS……完全解放!


核心实现步骤详解

下面我们将基于STM32 HAL库 + STM32F4系列平台,一步步搭建I2C-DMA通信框架。

第一步:初始化 I2C 外设

I2C_HandleTypeDef hi2c1; void MX_I2C1_Init(void) { hi2c1.Instance = I2C1; hi2c1.Init.ClockSpeed = 400000; // 400kHz 快速模式 hi2c1.Init.DutyCycle = I2C_DUTYCYCLE_2; // 标准占空比 hi2c1.Init.OwnAddress1 = 0x00; // 主机模式,无需自地址 hi2c1.Init.AddressingMode = I2C_ADDRESSINGMODE_7BIT; hi2c1.Init.DualAddressMode = I2C_DUALADDRESS_DISABLE; hi2c1.Init.GeneralCallMode = I2C_GENERALCALL_DISABLE; hi2c1.Init.NoStretchMode = I2C_NOSTRETCH_DISABLE; // 允许时钟延展 if (HAL_I2C_Init(&hi2c1) != HAL_OK) { Error_Handler(); } // ⚠️ 关键一步:使能DMA请求 __HAL_I2C_ENABLE_DMA_REQ(&hi2c1, I2C_DMA_REQ_TX); __HAL_I2C_ENABLE_DMA_REQ(&hi2c1, I2C_DMA_REQ_RX); }

🔍 注意事项:
-NoStretchMode设为DISABLE表示允许从机拉低SCL进行时钟延展,更兼容老旧器件。
- 若你的EEPROM写入慢,建议打开此功能避免超时。


第二步:配置并关联 DMA 通道

DMA_HandleTypeDef hdma_i2c1_tx; DMA_HandleTypeDef hdma_i2c1_rx; void MX_DMA_Init(void) { __HAL_RCC_DMA1_CLK_ENABLE(); // === TX DMA 配置:内存 → 外设 === 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_HIGH; hdma_i2c1_tx.Init.FIFOMode = DMA_FIFOMODE_DISABLE; if (HAL_DMA_Init(&hdma_i2c1_tx) != HAL_OK) { Error_Handler(); } // 将DMA句柄链接到I2C句柄 __HAL_LINKDMA(&hi2c1, hdmatx, hdma_i2c1_tx); // === RX DMA 配置:外设 → 内存 === hdma_i2c1_rx.Instance = DMA1_Stream5; hdma_i2c1_rx.Init.Channel = DMA_CHANNEL_1; hdma_i2c1_rx.Init.Direction = DMA_PERIPH_TO_MEMORY; hdma_i2c1_rx.Init.PeriphInc = DMA_PINC_DISABLE; hdma_i2c1_rx.Init.MemInc = DMA_MINC_ENABLE; hdma_i2c1_rx.Init.PeriphDataAlignment = DMA_PDATAALIGN_BYTE; hdma_i2c1_rx.Init.MemDataAlignment = DMA_MDATAALIGN_BYTE; hdma_i2c1_rx.Init.Mode = DMA_NORMAL; hdma_i2c1_rx.Init.Priority = DMA_PRIORITY_HIGH; hdma_i2c1_rx.Init.FIFOMode = DMA_FIFOMODE_DISABLE; if (HAL_DMA_Init(&hdma_i2c1_rx) != HAL_OK) { Error_Handler(); } __HAL_LINKDMA(&hi2c1, hdmarx, hdma_i2c1_rx); }

💡 提示:
- 使用__HAL_LINKDMA()宏非常关键,否则HAL库不知道哪个DMA属于哪个I2C。
- Stream选择要查参考手册(RM0090),确保没有与其他外设冲突。


第三步:发起非阻塞传输

现在一切就绪,只需调用两个函数即可启动DMA传输:

uint8_t tx_data[] = {0x01, 0x02, 0x03}; uint8_t rx_data[3]; uint8_t slave_addr = 0xA0; // 7位地址左移+写标志(例如AT24C02) // 发送命令(如写寄存器地址) if (HAL_I2C_Master_Transmit_DMA(&hi2c1, slave_addr, tx_data, 3) != HAL_OK) { Error_Handler(); } // 接收数据 if (HAL_I2C_Master_Receive_DMA(&hi2c1, slave_addr | 0x01, rx_data, 3) != HAL_OK) { Error_Handler(); }

注意:
- 第一个参数是I2C句柄
- 第二个是从机地址(7位格式)
- 第三个是缓冲区指针
- 第四个是数据长度(单位:字节)

传输一旦开始,函数立即返回,CPU继续执行后续代码。


第四步:通过回调处理完成事件

既然不阻塞,那怎么知道什么时候传完了?靠回调函数

void HAL_I2C_MasterTxCpltCallback(I2C_HandleTypeDef *hi2c) { if (hi2c->Instance == I2C1) { // 发送完成,可点亮LED或启动下一轮操作 HAL_GPIO_WritePin(LED_GPIO_Port, LED_Pin, GPIO_PIN_SET); } } void HAL_I2C_MasterRxCpltCallback(I2C_HandleTypeDef *hi2c) { if (hi2c->Instance == I2C1) { // 接收完成,处理数据 Process_Received_Data(rx_data, 3); } } void HAL_I2C_ErrorCallback(I2C_HandleTypeDef *hi2c) { if (hi2c->Instance == I2C1) { // 出错了!可能是NACK、BUSY、总线锁死 printf("I2C Error occurred!\n"); // 尝试恢复:关闭再重新初始化 HAL_I2C_DeInit(hi2c); MX_I2C1_Init(); } }

✅ 回调机制的优势:
- 不阻塞主线程
- 可扩展性强(可用于RTOS消息队列、事件组等)
- 易于集成进状态机或协议栈


实际应用场景:多传感器采集系统

设想这样一个工业监测节点:

  • 连接 BME280(温湿度压强)、SHT30、光照传感器、EEPROM
  • 每秒采集一次,每次读取约15字节数据
  • 主控需同时刷新OLED屏幕、上传数据到LoRa模块

如果使用普通中断方式,每秒将触发上百次中断,系统几乎无法响应。

换成 I2C-DMA 后呢?

指标中断方式I2C+DMA 方式
中断次数/秒~200+<10(仅完成中断)
CPU占用率>75%<15%
数据可靠性易丢包极高(DMA即时搬运)
功耗表现难进入低功耗模式可Sleep,DMA后台运行

🧩 组合玩法建议:
- 结合定时器触发采集
- 在DMA完成回调中启动下一设备读取,形成链式操作
- 加入超时守护任务(FreeRTOS中可用软件定时器)


常见坑点与避坑秘籍

❌ 坑点1:DMA传输没反应?

  • 检查是否调用了__HAL_LINKDMA()
  • 检查DMA Clock是否使能(__HAL_RCC_DMA1_CLK_ENABLE()
  • 检查Stream编号是否正确(不同型号映射不同)

❌ 坑点2:第一次正常,第二次失败?

  • 可能是DMA未重置。建议每次传输前检查状态:
    c if (hi2c1.State == HAL_I2C_STATE_READY) { HAL_I2C_Master_Transmit_DMA(...); }

❌ 坑点3:读不到数据,总是NACK?

  • 检查从机地址是否正确(7位还是8位?写0xA0还是0x50?)
  • 上拉电阻是否足够强(典型值4.7kΩ)
  • 总线是否有干扰或短路

✅ 秘籍1:提升稳定性的小技巧

  • 在错误回调中加入SCL打拍恢复逻辑(手动输出SCL脉冲释放BUSY状态)
  • 使用HAL_I2C_IsDeviceReady()检测从机是否就绪
  • 设置合理的超时时间(即使DMA不阻塞,高层协议也应防死锁)

✅ 秘籍2:低功耗设计中的妙用

// 启动DMA读取后,让CPU进入Sleep模式 HAL_I2C_Master_Receive_DMA(&hi2c1, addr, buf, len); HAL_PWR_EnterSLEEPMode(PWR_MAINREGULATOR_ON, PWR_SLEEPENTRY_WFI); // WFI唤醒后继续执行

DMA在后台工作,CPU休眠,直到传输完成产生中断才唤醒 —— 完美平衡性能与功耗。


总结:这才是现代嵌入式开发该有的样子

回到最初的问题:为什么要折腾I2C+DMA?

因为我们要构建的是健壮、高效、可扩展的系统,而不是靠“凑合能用”的代码堆出来的半成品。

通过本次实战,你应该已经掌握:

  • 如何让I2C摆脱CPU束缚,实现后台静默传输
  • 如何利用HAL库快速搭建DMA通信链路
  • 如何通过回调机制实现异步处理
  • 如何规避常见陷阱,提升系统鲁棒性

这项技术不仅适用于传感器读取,还可拓展至:
- EEPROM大批量读写
- OLED屏幕刷新(配合SPI-DMA)
- 音频I2S数据流传输
- 多节点I2C级联控制系统

当你学会把“数据搬运”这件事交给DMA,你就真正迈入了资源协同调度的大门

下次当你面对一个新的通信需求,别再问“怎么最快写出来”,而是想想:“能不能让它自己跑?

如果你正在做一个低功耗物联网终端、工业网关或者智能仪表,欢迎在评论区分享你的I2C应用场景,我们一起探讨最佳实践!

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

Free Exercise DB:全面解锁800+健身动作的开源数据库

Free Exercise DB&#xff1a;全面解锁800健身动作的开源数据库 【免费下载链接】free-exercise-db Open Public Domain Exercise Dataset in JSON format, over 800 exercises with a browsable public searchable frontend 项目地址: https://gitcode.com/gh_mirrors/fr/fr…

作者头像 李华
网站建设 2026/2/22 19:52:30

光影重塑革命:Qwen-Edit一键解决图片打光难题

光影重塑革命&#xff1a;Qwen-Edit一键解决图片打光难题 【免费下载链接】Relight 项目地址: https://ai.gitcode.com/hf_mirrors/dx8152/Relight 你是否曾经遇到过这样的情况&#xff1f;精心拍摄的照片因为光线问题变得平淡无奇&#xff0c;或是设计的角色因为光影效…

作者头像 李华
网站建设 2026/2/22 17:08:11

6款苹方字体完整指南:让Windows用户也能享受苹果原生字体体验

6款苹方字体完整指南&#xff1a;让Windows用户也能享受苹果原生字体体验 【免费下载链接】PingFangSC PingFangSC字体包文件、苹果平方字体文件&#xff0c;包含ttf和woff2格式 项目地址: https://gitcode.com/gh_mirrors/pi/PingFangSC 还在为网站在不同设备上字体显示…

作者头像 李华
网站建设 2026/2/22 4:48:52

STM32L4系列CubeMX时钟配置完整示例

STM32L4时钟配置实战&#xff1a;从CubeMX到稳定运行的每一步你有没有遇到过这样的情况&#xff1f;代码逻辑没问题&#xff0c;外设初始化也写了&#xff0c;结果IC通信就是没波形&#xff0c;ADC采样乱跳&#xff0c;甚至程序卡在HAL_Init()不动——最后发现&#xff0c;问题…

作者头像 李华
网站建设 2026/2/21 13:22:04

Goldleaf 终极使用指南:从入门到精通 Nintendo Switch 多用途工具

Goldleaf 终极使用指南&#xff1a;从入门到精通 Nintendo Switch 多用途工具 【免费下载链接】Goldleaf &#x1f342; Multipurpose homebrew tool for Nintendo Switch 项目地址: https://gitcode.com/gh_mirrors/go/Goldleaf Goldleaf 是一款专为 Nintendo Switch 设…

作者头像 李华
网站建设 2026/2/19 12:58:12

Cap开源录屏工具:3分钟上手专业级屏幕录制

Cap开源录屏工具&#xff1a;3分钟上手专业级屏幕录制 【免费下载链接】Cap Effortless, instant screen sharing. Open-source and cross-platform. 项目地址: https://gitcode.com/GitHub_Trending/cap1/Cap 还在为制作教学视频、产品演示或技术分享而烦恼吗&#xff…

作者头像 李华