普冉(PUYA)单片机开发实战:I2C主从通信中的DMA配置与优化
1. 为什么I2C通信需要DMA从“跑腿小弟”到“自动驾驶”搞过单片机I2C通信的朋友尤其是用过像普冉PY32F003这类资源紧凑型MCU的肯定都经历过这种场景主程序正忙着处理传感器数据或者刷新屏幕突然一个I2C传输请求过来CPU就得立刻放下手头的活亲自去搬运每一个字节的数据。这个过程就像你正在写代码快递员每送一个包裹都要你亲自下楼签收一样效率低得让人抓狂。更糟的是在高速或大数据量传输时这种频繁的中断和等待会严重拖慢整个系统的响应速度。这时候DMA直接存储器访问就该登场了。你可以把它理解成你雇的一个“跑腿小弟”。当你需要收发I2C数据时你只需要告诉DMA小弟三件事数据在哪源地址、要送到哪目标地址、有多少数据量。然后你就可以转身去干别的重要工作了搬数据的脏活累活全交给DMA。它不占用CPU资源在后台默默地把数据从内存搬到I2C外设发送或者从I2C外设搬到内存接收。整个过程CPU几乎可以“置身事外”只有当整批数据搬完DMA小弟才会过来拍拍你肩膀说“老板活干完了”触发一个中断通知你。在PY32F003上玩I2C主从通信尤其是主从双方都要高速收发时DMA几乎不是“优化选项”而是“必选项”。这颗Cortex-M0内核的芯片主频不算高如果让CPU去伺候I2C的每一个时钟和数据位其他任务就别想流畅运行了。我实测过用查询或基础中断方式做连续15字节的I2C收发CPU占用率能飙到80%以上而换成DMA后同样的操作CPU占用几乎可以忽略不计主循环里的LED闪烁都变得丝滑无比。所以用好DMA是释放PY32F003性能、实现稳定高效I2C通信的关键一步。2. PY32F003的DMA资源盘点与通道选择策略在动手配置之前我们得先摸清家底PY32F003的DMA到底有多少“家当”它内置了一个DMA控制器DMA1共有5个独立的通道Channel 1~5。这5个通道可以分配给不同的外设但一个通道在同一时间只能服务一个外设请求。这就引出了第一个核心问题I2C通信需要占用几个DMA通道答案是至少两个。因为I2C通信是全双工的虽然物理上是半双工但逻辑上收发独立发送TX和接收RX是完全独立的数据流需要独立的DMA通道来服务。在PY32F003的参考手册里I2C1的发送请求映射到DMA1的通道1接收请求映射到通道2。这个映射关系是硬件固定的你不能随意更改。所以为I2C配置DMA我们通常就锁定通道1TX和通道2RX。这里有个新手容易踩的坑以为DMA通道很多可以随便用。实际上DMA是系统级的稀缺资源。除了I2C像ADC、UART、SPI这些常用外设都要抢DMA通道。如果你的项目里同时用了I2C和ADC采样并且ADC也开了DMA那就要非常小心地规划通道分配避免冲突。比如ADC1通常可能占用通道1这就和I2C TX冲突了。所以我的经验是在项目初期画系统架构图时就要把各个外设的DMA需求标清楚做好资源分配表。对于I2C主从通信实验两块板子都需要独立配置DMA。主机和从机的代码在DMA配置部分几乎一模一样因为它们的硬件结构是对称的。但要注意如果你的应用场景是“一主多从”主机作为唯一的主动发起方它的DMA负担会更重可能需要更关注其DMA传输的稳定性和中断优先级设置。3. 手把手配置从零搭建I2C DMA驱动光说不练假把式咱们直接上代码看看在PY32F003的HAL库环境下怎么一步步把DMA配通。我会把关键点拆开揉碎了讲保证你跟着做就能成功。3.1 硬件与时钟初始化打好地基任何外设使用前开时钟是第一步。DMA、I2C、GPIO的时钟都得打开。这里有个细节__HAL_RCC_SYSCFG_CLK_ENABLE()这一句不能省。因为DMA通道与外设的映射关系是通过SYSCFG系统配置控制器来配置的不开它的时钟后续的HAL_SYSCFG_DMA_Req函数会失效。void HAL_I2C_MspInit(I2C_HandleTypeDef *hi2c) { GPIO_InitTypeDef GPIO_InitStruct {0}; // 1. 开启相关时钟 __HAL_RCC_SYSCFG_CLK_ENABLE(); // 系统配置时钟必须开 __HAL_RCC_DMA_CLK_ENABLE(); // DMA时钟 __HAL_RCC_I2C_CLK_ENABLE(); // I2C时钟 __HAL_RCC_GPIOA_CLK_ENABLE(); // GPIOA时钟我用的是PA2/PA3 // 2. 配置I2C引脚PA2: SDA, PA3: SCL GPIO_InitStruct.Pin GPIO_PIN_2 | GPIO_PIN_3; GPIO_InitStruct.Mode GPIO_MODE_AF_OD; // 开漏输出这是I2C标准要求 GPIO_InitStruct.Pull GPIO_PULLUP; // 内部上拉确保总线空闲时为高电平 GPIO_InitStruct.Speed GPIO_SPEED_FREQ_VERY_HIGH; // 速度拉满 GPIO_InitStruct.Alternate GPIO_AF12_I2C; // 复用功能选择AF12 HAL_GPIO_Init(GPIOA, GPIO_InitStruct); // 3. 复位一下I2C确保从干净状态开始 __HAL_RCC_I2C_FORCE_RESET(); __HAL_RCC_I2C_RELEASE_RESET(); // 4. 配置I2C中断事件和错误中断 HAL_NVIC_SetPriority(I2C1_IRQn, 0, 0); HAL_NVIC_EnableIRQ(I2C1_IRQn); // ... DMA配置见下文 }关键点解析GPIO模式一定要选GPIO_MODE_AF_OD复用开漏输出。I2C总线是“线与”逻辑开漏输出配合上拉电阻是实现多设备共享总线的硬件基础。选成推挽输出Push-Pull是常见错误会导致总线冲突。上拉电阻代码里开启了内部上拉GPIO_PULLUP。对于低速100kHz、短距离通信PY32F003的内部上拉电阻大约40kΩ勉强够用。但如果通信距离稍长、速度上到400kHz或者总线上设备多强烈建议在SDA和SCL线上各加一个4.7kΩ的外部上拉电阻到3.3V这是保证波形干净、通信稳定的关键。复用功能GPIO_AF12_I2C这个值需要查数据手册。不同型号、不同引脚对应的AF编号可能不同不能想当然。3.2 DMA通道详细配置方向、对齐与模式接下来是重头戏DMA初始化。我们需要初始化两个DMA_HandleTypeDef结构体分别对应发送和接收。static DMA_HandleTypeDef HdmaCh1; // 发送通道句柄 static DMA_HandleTypeDef HdmaCh2; // 接收通道句柄 // 在HAL_I2C_MspInit函数内继续... // 5. 配置DMA请求映射告诉DMA哪个通道给I2C用 HAL_SYSCFG_DMA_Req(9); // 映射DMA1通道1给I2C1_TX HAL_SYSCFG_DMA_Req(0xA00); // 映射DMA1通道2给I2C1_RX // 6. 配置发送通道内存 - I2C数据寄存器 HdmaCh1.Instance DMA1_Channel1; HdmaCh1.Init.Direction DMA_MEMORY_TO_PERIPH; // 方向内存到外设 HdmaCh1.Init.PeriphInc DMA_PINC_DISABLE; // 外设地址不递增 HdmaCh1.Init.MemInc DMA_MINC_ENABLE; // 内存地址递增 HdmaCh1.Init.PeriphDataAlignment DMA_PDATAALIGN_BYTE; // 外设数据对齐字节 HdmaCh1.Init.MemDataAlignment DMA_MDATAALIGN_BYTE; // 内存数据对齐字节 HdmaCh1.Init.Mode DMA_NORMAL; // 模式普通模式传输一次 HdmaCh1.Init.Priority DMA_PRIORITY_VERY_HIGH; // 优先级很高 HAL_DMA_Init(HdmaCh1); __HAL_LINKDMA(hi2c, hdmatx, HdmaCh1); // 将句柄关联到I2C句柄 // 7. 配置接收通道I2C数据寄存器 - 内存 HdmaCh2.Instance DMA1_Channel2; HdmaCh2.Init.Direction DMA_PERIPH_TO_MEMORY; // 方向外设到内存 HdmaCh2.Init.PeriphInc DMA_PINC_DISABLE; HdmaCh2.Init.MemInc DMA_MINC_ENABLE; HdmaCh2.Init.PeriphDataAlignment DMA_PDATAALIGN_BYTE; HdmaCh2.Init.MemDataAlignment DMA_MDATAALIGN_BYTE; HdmaCh2.Init.Mode DMA_NORMAL; HdmaCh2.Init.Priority DMA_PRIORITY_HIGH; // 优先级可以比发送通道略低 HAL_DMA_Init(HdmaCh2); __HAL_LINKDMA(hi2c, hdmarx, HdmaCh2); // 关联接收DMA // 8. 配置DMA中断可选用于传输完成通知 HAL_NVIC_SetPriority(DMA1_Channel1_IRQn, 1, 1); HAL_NVIC_EnableIRQ(DMA1_Channel1_IRQn); HAL_NVIC_SetPriority(DMA1_Channel2_3_IRQn, 0, 1); // 通道2和3共享中断 HAL_NVIC_EnableIRQ(DMA1_Channel2_3_IRQn);逐项拆解避坑指南方向Direction这是最容易配反的地方。记住一个口诀“TX是M2P内存到外设RX是P2M外设到内存”。I2C发送时数据是从你定义的数组内存搬到I2C的数据寄存器外设。接收则相反。地址递增PeriphInc/MemIncPeriphInc必须设为DISABLE因为I2C的数据寄存器就一个固定地址每个字节都往这个寄存器读写。MemInc必须设为ENABLE这样DMA每搬运一个字节就会自动指向数组的下一个元素否则所有数据都会堆在数组的第一个位置。数据对齐DataAlignment这是PY32F003 I2C DMA的一个大坑必须设为DMA_PDATAALIGN_BYTE和DMA_MDATAALIGN_BYTE。因为I2C是8位一个字节一个字节通信的。我实测过如果设成HALF_WORD半字16位或WORD字32位DMA会试图一次读写16/32位数据但I2C数据寄存器只有8位有效这会导致总线挂死程序卡住。很多朋友遇到的DMA配置后I2C卡死问题八成是这个原因。模式ModeDMA_NORMAL是普通模式传输完指定长度就停止。还有DMA_CIRCULAR循环模式适合需要持续收发如音频流的场景。对于普通的命令/数据交互用普通模式就够了。优先级Priority当多个DMA通道同时有请求时优先级高的先被响应。我把发送通道设成VERY_HIGH接收设成HIGH是因为在实际通信中主机发送完地址和命令后需要尽快把数据发出去发送的及时性可能比接收稍重要一点。这个可以根据实际场景调整。中断配置DMA传输完成中断不是必须的因为I2C本身也有传输完成中断。但开启DMA完成中断有个好处你能明确知道DMA搬运工作何时彻底结束便于进行更精细的状态管理和错误处理。注意PY32F003的DMA通道2和3是共享一个中断向量的DMA1_Channel2_3_IRQn。3.3 主从机收发函数与DMA启动配置好底层上层应用就清爽多了。主从机的收发函数核心就是调用HAL库的DMA传输函数。// 主机发送函数示例 HAL_StatusTypeDef app_i2c_master_transmit(void) { // 等待I2C就绪防止上次传输未完成 while (HAL_I2C_GetState(I2cHandle) ! HAL_I2C_STATE_READY) {} // 启动DMA传输指定从机地址、发送缓冲区、长度 HAL_StatusTypeDef status HAL_I2C_Master_Transmit_DMA(I2cHandle, I2C_SLAVE_ADDRESS, mI2cTxBuf, EXDATA_LEN); if (status ! HAL_OK) { // 错误处理比如重试或报错 Error_Handler(); } return status; } // 从机接收函数示例 HAL_StatusTypeDef app_i2c_slave_receive(void) { // 从机处于监听模式等待主机发送数据过来 HAL_StatusTypeDef status HAL_I2C_Slave_Receive_DMA(I2cHandle, mI2cRxBuf, EXDATA_LEN); if (status ! HAL_OK) { Error_Handler(); } return status; }关键区别主机函数Master_Transmit/Receive_DMA需要指定从机地址I2C_SLAVE_ADDRESS。这个地址通常是7位地址左移一位即0xA0这种形式最低位是读写位由库函数内部处理。从机函数Slave_Transmit/Receive_DMA不需要指定对方地址因为它自己的地址已经在I2C初始化时通过OwnAddress1配置好了。从机一直监听总线当主机呼叫它的地址时它才会响应。启动DMA传输后CPU就自由了。你可以去执行其他任务比如扫描按键、更新显示。当传输完成会触发I2C的事件中断并在对应的回调函数里通知你。// 主机发送完成回调函数 void HAL_I2C_MasterTxCpltCallback(I2C_HandleTypeDef *hi2c) { if(hi2c-Instance ! I2C1) return; // 判断是哪个I2C触发的 printf(主机DMA发送完成\r\n); // 这里可以置位一个标志位通知主循环进行下一步操作 } // 从机接收完成回调函数 void HAL_I2C_SlaveRxCpltCallback(I2C_HandleTypeDef *hi2c) { if(hi2c-Instance ! I2C1) return; printf(从机DMA接收完成收到%d字节数据\r\n, EXDATA_LEN); // 处理接收到的数据 mI2cRxBuf }4. 实战优化技巧让你的I2C DMA又快又稳配置通了只是第一步要想在实际项目里用得顺手还得掌握一些优化和调试技巧。这些都是我踩过坑后总结出来的经验。4.1 提升传输效率循环DMA与双缓冲对于需要持续不断传输数据的场景比如从传感器连续读取数据流DMA_NORMAL模式就不够用了。因为每次传输完都要重新配置和启动DMA会产生额外开销。这时可以用DMA_CIRCULAR循环模式。在循环模式下DMA传输完缓冲区末尾的数据后会自动跳回到缓冲区开头重新开始传输形成一个闭环。这对于实现一个“数据流”管道非常有用。配置很简单只需把初始化结构体里的Mode从DMA_NORMAL改为DMA_CIRCULAR即可。但循环模式有个问题当DMA在后台循环写入数据时你的程序也在读取数据如果读写指针处理不好就会读到“新旧数据混合”的混乱内容。为了解决这个问题双缓冲技术就派上用场了。双缓冲的思路是准备两个一样大小的缓冲区比如BufferA和BufferB。DMA配置为循环模式但长度设为单个缓冲区的两倍让它依次在BufferA和BufferB上循环。同时你使用一个标志位和DMA的传输计数器CNDTR来判断DMA当前正在操作哪个缓冲区。#define BUF_SIZE 256 uint8_t dma_buffer[2][BUF_SIZE]; // 双缓冲 volatile uint8_t active_buf 0; // 当前CPU可安全读取的缓冲区索引 // 在DMA传输完成中断或半传输中断中切换缓冲区 void DMA1_Channel2_3_IRQHandler(void) { if(__HAL_DMA_GET_FLAG(HdmaCh2, DMA_FLAG_HTIF2)) { // 半传输完成第一个缓冲区满 active_buf 1; // 此时DMA正在写BufferBCPU可以安全读取BufferA __HAL_DMA_CLEAR_FLAG(HdmaCh2, DMA_FLAG_HTIF2); } if(__HAL_DMA_GET_FLAG(HdmaCh2, DMA_FLAG_TCIF2)) { // 全传输完成第二个缓冲区也满 active_buf 0; // 此时DMA回到BufferACPU可以安全读取BufferB __HAL_DMA_CLEAR_FLAG(HdmaCh2, DMA_FLAG_TCIF2); } HAL_DMA_IRQHandler(HdmaCh2); }这样CPU总是读取active_buf指向的那个“安静”的缓冲区而DMA则在另一个缓冲区上工作实现了读写分离数据安全又高效。4.2 稳定性保障错误处理与超时机制DMA虽然省心但也不是万无一失。I2C总线可能受到干扰从机可能无响应导致传输挂死。一个健壮的系统必须有错误处理机制。首先要开启I2C的错误中断。在HAL_I2C_MspInit中我们只使能了事件中断错误中断同样重要。// 在I2C MSP初始化中补充错误中断 HAL_NVIC_SetPriority(I2C1_ER_IRQn, 0, 1); // I2C错误中断 HAL_NVIC_EnableIRQ(I2C1_ER_IRQn);然后在中断服务函数中处理错误void I2C1_ER_IRQHandler(void) { HAL_I2C_ER_IRQHandler(I2cHandle); } // 错误回调函数 void HAL_I2C_ErrorCallback(I2C_HandleTypeDef *hi2c) { uint32_t error_code HAL_I2C_GetError(hi2c); if(error_code HAL_I2C_ERROR_AF) { // 应答失败ACK Failure printf(I2C错误从机无应答\r\n); } else if(error_code HAL_I2C_ERROR_BERR) { // 总线错误 printf(I2C错误总线错误\r\n); } else if(error_code HAL_I2C_ERROR_ARLO) { // 仲裁丢失 printf(I2C错误仲裁丢失\r\n); } // 错误发生后最好重新初始化I2C和DMA HAL_I2C_DeInit(I2cHandle); HAL_DMA_DeInit(HdmaCh1); HAL_DMA_DeInit(HdmaCh2); // ... 重新初始化 }其次实现软件超时机制。HAL库的DMA函数本身不提供超时参数。我们可以用一个硬件定时器比如SysTick或基本定时器来实现。volatile uint32_t dma_timeout_counter 0; #define DMA_TIMEOUT_MS 100 // 超时时间100ms // 在启动DMA传输前启动一个超时计时器 void start_dma_timeout(void) { dma_timeout_counter HAL_GetTick(); } // 在主循环或定时器中断中检查 void check_dma_timeout(void) { if(传输未完成标志 (HAL_GetTick() - dma_timeout_counter DMA_TIMEOUT_MS)) { printf(DMA传输超时\r\n); // 强制停止DMA和I2C HAL_I2C_DMAStop(I2cHandle); HAL_DMA_Abort(HdmaCh1); // ... 执行错误恢复流程 } }4.3 调试与排坑常见问题速查通信完全没反应SCL/SDA线一直是高电平检查硬件连接这是第一要务确保SDA、SCL、GND三根线都正确连接且接触良好。我遇到过无数次因为杜邦线接触不良导致的诡异问题。检查上拉电阻用万用表量一下SCL和SDA线对地的电压空闲时应该是接近VCC3.3V。如果电压偏低说明上拉电阻不够加上外部4.7kΩ上拉。检查GPIO配置确认引脚模式是GPIO_MODE_AF_OD复用开漏而不是推挽输出。能发送起始信号但发送地址后无应答NACK检查从机地址确保主机代码里写的从机地址和从机自身配置的地址一致。注意7位地址和8位读写地址的区别通常库函数要求传入7位地址左移一位后的值。检查从机是否初始化成功从机程序是否正常运行它的I2C外设初始化了吗中断和DMA使能了吗用逻辑分析仪抓波形这是最直接的调试手段。看主机发出的地址字节是否正确看从机在第9个时钟周期是否把SDA拉低表示ACK。DMA启动后程序卡死或跑飞检查DMA数据对齐百分之九十的卡死问题都是PeriphDataAlignment和MemDataAlignment没设成BYTE。务必检查检查缓冲区地址和长度确保你传给DMA的缓冲区地址是有效的比如全局数组或静态数组的地址并且长度没有溢出。检查中断优先级如果I2C中断和DMA中断的优先级设置不合理可能会发生嵌套中断冲突。可以尝试调整优先级或者简化中断服务函数尽快处理完退出。数据传输出现错位或乱码检查内存地址递增确认MemInc设为ENABLEPeriphInc设为DISABLE。检查缓冲区越界确保你定义的数组大小EXDATA_LEN大于等于实际传输的长度。检查主从机时钟同步确保主从机使用相同或兼容的I2C时钟速度如100kHz。如果主机太快从机可能跟不上。5. 进阶应用一主多从与大数据块传输当你掌握了基础的单主单从通信后可以尝试更复杂的场景这对DMA的配置提出了更高要求。一主多从架构主机需要轮询或寻址多个从机。这时主机的DMA通道是复用的。每次与不同从机通信前你不需要重新初始化DMA但需要重新配置DMA的目标外设地址吗不需要。因为I2C通信中从机地址是通过I2C总线协议起始信号后的第一个字节传递的而不是通过DMA。DMA只负责把内存里的数据这个数据里就包含了从机地址字节搬到I2C数据寄存器。所以你只需要在内存缓冲区里准备好包含目标从机地址的数据包然后启动DMA传输即可。关键在于总线仲裁和错误处理。当多个主机或者一个主机快速切换寻址时有可能发生仲裁丢失。必须在HAL_I2C_ErrorCallback中妥善处理HAL_I2C_ERROR_ARLO错误通常的做法是稍作延迟后重试。大数据块传输PY32F003的DMA一次传输的最大数据量受限于通道的CNDTR寄存器这是一个16位寄存器所以单次传输最大长度是65535字节。对于超过这个长度的数据你需要进行分包。更优雅的做法是结合DMA传输完成中断和循环模式。例如你需要传输一个200KB的固件包。你可以定义一个4KB的DMA循环缓冲区。主程序负责将200KB的数据分次填充到这个4KB的缓冲区里注意填充速度不能慢于DMA发送速度而DMA则在循环模式下持续不断地将缓冲区里的数据发出去。这需要精细的流量控制比如使用信号量或标志位来同步“填充数据”和“DMA取数据”这两个过程。最后别忘了功耗考量。在低功耗应用中当I2C和DMA都不工作时记得关闭它们的时钟以省电。在PY32F003中可以通过__HAL_RCC_DMA_CLK_DISABLE()和__HAL_RCC_I2C_CLK_DISABLE()来实现。在需要通信前再重新开启并初始化。虽然HAL库的初始化函数会开时钟但主动管理时钟开关是低功耗设计的良好习惯。
本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2409787.html
如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!