建设网站公司 销售额 排行怎样在百度做网站
建设网站公司 销售额 排行,怎样在百度做网站,电子商务网站建设考卷,静态网站模板中英文1. 为什么你需要SPI DMA#xff1f;从“苦力活”到“甩手掌柜”的蜕变
如果你用过沁恒CH32V307的SPI#xff0c;不管是驱动屏幕、读写Flash还是连接传感器#xff0c;最开始大概率都是像我一样#xff0c;用最基础的查询方式。代码里写满了while(SPI_I2S_GetFlagStatus(..…1. 为什么你需要SPI DMA从“苦力活”到“甩手掌柜”的蜕变如果你用过沁恒CH32V307的SPI不管是驱动屏幕、读写Flash还是连接传感器最开始大概率都是像我一样用最基础的查询方式。代码里写满了while(SPI_I2S_GetFlagStatus(...) RESET)这样的等待语句。发送几个字节数据感觉还行但当我第一次尝试用SPI刷新一整块320x240的TFT屏幕时问题就来了。主循环被死死地卡在等待SPI发送完成的状态里CPU啥也干不了就眼巴巴地等着SPI外设一个bit一个bit地把数据“吐”出去。这时候如果系统里还有按键扫描、网络通信或者其他实时任务整个系统就会变得非常卡顿响应迟钝。这就是基础SPI查询模式的瓶颈CPU被“栓”在了数据传输这件“苦力活”上。CPU需要不断地查询状态、搬运数据成了整个系统效率的短板。而DMA直接存储器访问就是为了解放CPU而生的。你可以把它想象成一个非常专业的“快递小哥”。你只需要告诉DMA两件事货在哪里源地址送到哪里去目标地址以及有多少货数据量。然后你就可以转身去处理其他更重要的任务了比如处理算法、响应事件。DMA这个“快递小哥”会自己找到SPI外设默默地把数据从内存搬到SPI的发送数据寄存器或者从SPI的接收寄存器搬到内存全程不需要CPU插手。在CH32V307上启用SPI DMA模式带来的性能提升是立竿见影的。最直观的感受就是CPU占用率大幅下降。以前刷新一帧屏幕数据CPU占用率可能飙升到80%以上现在可能连10%都不到。其次数据传输的吞吐量更稳定。因为DMA的传输是硬件自动化的不受CPU中断响应延迟、任务调度的影响对于需要连续、高速、稳定数据流的应用比如音频流、图像传输、高速数据采集至关重要。我实测过一个项目用查询模式SPI读取SPI Flash速度大概在1.5MB/s左右就遇到瓶颈了而切换到DMA模式后轻松跑满了SPI时钟的理论带宽达到了接近4MB/s效率提升了一倍多。所以如果你的项目满足以下任何一个条件那么SPI DMA就是你必须要掌握的技术需要传输大量数据超过几十个字节系统有实时性要求CPU不能被数据传输长时间阻塞追求极致的传输效率和带宽利用率。接下来我们就亲手把CH32V307的SPI DMA模式配置起来。2. 庖丁解牛CH32V307 SPI DMA的配置全景图配置SPI DMA不像调用一个库函数那么简单它需要我们对CH32V307的DMA控制器和SPI外设有清晰的了解。别担心我们一步步来拆解。首先你得知道CH32V307的DMA控制器功能挺强大的支持多个通道每个通道可以独立配置为外设到存储器、存储器到外设等传输方向。第一步依然是基础的SPI初始化。这部分和查询模式很像但有个关键点需要注意SPI的硬件流控制。在SPI的初始化结构体SPI_InitTypeDef里我们需要明确告诉SPI外设我们要使用DMA来发送和/或接收数据。相关的配置项是SPI_I2S_DMACmd函数。通常我们会在SPI基本参数模式、速率、数据大小、CPOL/CPHA配置完成并启用SPI_Cmd之后再单独开启DMA请求。例如如果你只需要用DMA发送数据那么就开启发送DMA请求SPI_I2S_DMACmd(SPI1, SPI_I2S_DMAReq_Tx, ENABLE);。如果需要同时用DMA接收那就也把接收请求打开。这一点很多新手会忽略导致DMA配置了半天SPI根本不向DMA发出传输请求数据自然不动。第二步重头戏DMA通道的配置。这是核心中的核心。你需要根据数据手册找到SPI1的发送和接收分别对应DMA的哪个通道。以CH32V307常见的SPI1为例其发送Tx通常对应DMA1的某个通道比如通道3接收Rx对应另一个通道比如通道2。这个映射关系一定要查数据手册确认不能想当然。配置DMA通道时我们需要填充一个DMA_InitTypeDef结构体里面有几个参数至关重要DMA_PeripheralBaseAddr: 外设地址。对于SPI发送这里填SPI1的数据发送寄存器地址(uint32_t)SPI1-DATAR对于接收则填数据接收寄存器地址。DMA_MemoryBaseAddr: 内存地址。就是你准备好的数据数组的地址发送或者用来存放接收数据的数组地址接收。DMA_DIR: 传输方向。发送是DMA_DIR_PeripheralDST内存是源外设是目标接收是DMA_DIR_PeripheralSRC。DMA_BufferSize: 要传输的数据量单位是“数据项”的数量。如果你设置数据宽度是字节那这里就是字节数。DMA_PeripheralInc和DMA_MemoryInc: 地址递增模式。外设地址SPI数据寄存器通常不递增固定读写同一个寄存器所以设为DMA_PeripheralInc_Disable。内存地址肯定是递增的设为DMA_MemoryInc_Enable。DMA_PeripheralDataSize和DMA_MemoryDataSize: 数据宽度。必须和SPI配置的数据宽度一致如果SPI是8位数据这里就选DMA_PeripheralDataSize_Byte和DMA_MemoryDataSize_Byte。如果SPI是16位这里也必须选16位否则数据会错乱。DMA_Mode: 模式。一般我们选择DMA_Mode_Normal正常模式传输完指定数量就停止。如果需要循环传输比如用于连续刷新屏幕则选择DMA_Mode_Circular循环模式。DMA_Priority: 通道优先级。如果只有一个DMA传输设为默认的DMA_Priority_Low即可。如果多个DMA同时工作需要根据实时性要求安排优先级。配置完DMA初始化结构体后调用DMA_Init(DMA1_Channel3, DMA_InitStructure)进行初始化然后使能该DMA通道DMA_Cmd(DMA1_Channel3, ENABLE)。第三步联动与启动。配置好SPI和DMA后它们还是两个独立的模块。真正的传输启动通常是由你手动拉低SPI片选CS信号然后启动DMA传输开始的。但这里有个顺序讲究应该先使能DMA通道再使能SPI的DMA请求最后再操作片选和触发SPI通讯。有时候你还需要在DMA传输完成中断里拉高片选信号以结束本次SPI帧传输。为了更直观我把发送和接收的DMA配置关键参数做成了一个对比表格你可以配置时对照着看配置项SPI发送DMA (内存 - SPI_DR)SPI接收DMA (SPI_DR - 内存)说明与常见坑点外设地址(uint32_t)(SPIx-DATAR)(uint32_t)(SPIx-DATAR)同一个寄存器读写方向由硬件根据场景区分内存地址发送数组首地址接收缓冲区首地址确保数组在传输期间有效如非局部变量传输方向DMA_DIR_PeripheralDSTDMA_DIR_PeripheralSRC方向配反是经典错误会导致数据传不到外设或读不回内存外设地址递增DisableDisableSPI数据寄存器固定绝不递增内存地址递增EnableEnable通常需要递增以存取连续数据数据宽度必须与SPI_DataSize一致必须与SPI_DataSize一致8位/16位必须严格匹配否则数据错位工作模式Normal(单次) /Circular(循环)Normal(单次) /Circular(循环)屏幕刷新常用循环模式Flash读写常用正常模式传输完成中断可选使能强烈建议使能接收DMA最好用中断通知CPU取数据发送可选3. 实战案例一用DMA加速SPI Flash的连续读写理论讲得再多不如一行代码。我们拿最经典的SPI Flash比如W25Q128读写开刀。在查询模式下我们读一页数据256字节需要循环256次每次发送命令、地址然后等待并读取一个字节。CPU忙得团团转。用上DMA我们可以让CPU只负责发命令和地址然后把长达256字节甚至更长的数据读取任务完全丢给DMA。首先我们来看DMA读取SPI Flash数据的流程。这里有个关键技巧SPI Flash在主机发送完读命令和地址后会持续在MISO线上输出数据只要主机继续提供时钟。因此我们可以配置一个DMA接收通道让它自动从SPI数据寄存器中“捞取”连续不断涌来的数据存到我们指定的内存缓冲区里。具体步骤如下初始化SPI和对应的DMA接收通道例如SPI1_Rx用DMA1_Channel2。配置为从外设到内存数据宽度8位正常模式并使能传输完成中断。在DMA接收通道的中断服务函数里做三件事清除中断标志、关闭DMA通道如果是正常模式、拉高片选信号结束本次读取。主函数中拉低片选用查询方式发送读命令0x03和24位地址。注意发送命令和地址时SPI的DMA接收请求可以先不开启或者确保DMA还没开始工作避免把命令字节也当作数据收回来。命令地址发送完毕后立即使能之前配置好的DMA接收通道并确保SPI的DMA接收请求是开启的。此时DMA会开始自动搬运SPI接收到的数据到缓冲区。之后CPU就可以去做别的事情了。当指定数量的数据比如256字节全部接收完毕DMA控制器会触发传输完成中断我们在中断里完成收尾工作。下面是一段核心代码示例展示了如何配置DMA接收以及中断处理// 定义接收缓冲区和传输大小 uint8_t flash_rx_buffer[256]; #define DATA_SIZE 256 // DMA接收通道配置以SPI1_RX用DMA1_Channel2为例 void SPI1_RX_DMA_Config(void) { DMA_InitTypeDef DMA_InitStructure; RCC_AHBPeriphClockCmd(RCC_AHBPeriph_DMA1, ENABLE); // 使能DMA1时钟 DMA_DeInit(DMA1_Channel2); DMA_InitStructure.DMA_PeripheralBaseAddr (uint32_t)(SPI1-DATAR); DMA_InitStructure.DMA_MemoryBaseAddr (uint32_t)flash_rx_buffer; DMA_InitStructure.DMA_DIR DMA_DIR_PeripheralSRC; // 外设为源 DMA_InitStructure.DMA_BufferSize DATA_SIZE; 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_High; DMA_InitStructure.DMA_M2M DMA_M2M_Disable; DMA_Init(DMA1_Channel2, DMA_InitStructure); // 使能DMA通道的传输完成中断 DMA_ITConfig(DMA1_Channel2, DMA_IT_TC, ENABLE); // 配置DMA中断优先级并开启NVIC配置代码略 } // DMA1_Channel2中断服务函数 void DMA1_Channel2_IRQHandler(void) { if(DMA_GetITStatus(DMA1_IT_TC2)) { // 检查是否是传输完成中断 DMA_ClearITPendingBit(DMA1_IT_TC2); // 清除中断标志 DMA_Cmd(DMA1_Channel2, DISABLE); // 关闭DMA通道 SPI_I2S_DMACmd(SPI1, SPI_I2S_DMAReq_Rx, DISABLE); // 可选关闭SPI的DMA请求 GPIO_SetBits(FLASH_CS_PORT, FLASH_CS_PIN); // 拉高片选结束读取 // 此时flash_rx_buffer中已存有DATA_SIZE个字节的数据可以处理了 // 例如设置一个标志位通知主循环 flash_read_done_flag 1; } } // 主函数中发起一次DMA读取 void read_flash_sector_dma(uint32_t addr) { flash_read_done_flag 0; // 1. 确保DMA通道是关闭的并设置好内存地址和传输量可在配置函数或此处设置 DMA_SetCurrDataCounter(DMA1_Channel2, DATA_SIZE); // 2. 拉低片选开始SPI通讯 GPIO_ResetBits(FLASH_CS_PORT, FLASH_CS_PIN); // 3. 用查询方式发送读命令和地址此时DMA接收未开启 SPI_I2S_SendData(SPI1, 0x03); // Read command // ... 发送24位地址代码略 // 4. 使能SPI的DMA接收请求然后立即使能DMA通道 SPI_I2S_DMACmd(SPI1, SPI_I2S_DMAReq_Rx, ENABLE); DMA_Cmd(DMA1_Channel2, ENABLE); // 5. CPU可在此处执行其他任务等待中断标志flash_read_done_flag被置位 }对于SPI Flash的写入Page Program使用DMA发送同样能大幅提升效率。配置一个DMA发送通道将内存中要写入的数据数组自动搬运到SPI发送寄存器。流程是发送写使能命令、发送页编程命令和地址、然后启动DMA发送数据、最后等待Flash内部编程完成。这里要注意Flash的页大小限制通常256字节一次DMA传输不要超过一页。通过DMA你可以快速填充Flash的多个连续页CPU只需在每页编程完成后检查一下状态即可。4. 实战案例二驾驭SPI TFT屏幕告别刷新卡顿驱动SPI接口的TFT屏幕是DMA大显身手的另一个绝佳场景。尤其是那些分辨率较高的屏幕比如320x240甚至480x320一帧图像的数据量能达到数十KB。用查询方式刷新屏幕会有明显的逐行扫描的“刷屏”感并且系统几乎无法响应其他操作。使用DMA驱动屏幕的核心在于利用其“循环模式”Circular Mode。在这种模式下DMA在传输完指定数量的数据后会自动重置传输计数器并从头开始下一轮传输周而复始无需CPU干预。这对于需要持续刷新屏幕的场景简直是神器。我们通常会将屏幕的显存一个二维数组作为DMA的内存源配置DMA在循环模式下不断地将显存中的数据发送到SPI从而刷新屏幕。然而这里有一个非常重要的细节屏幕刷新通常需要配合“数据/命令”DC引脚和片选CS引脚的控制。发送命令如设置坐标、开启写RAM时DC引脚拉低发送像素数据时DC引脚拉高。而DMA本身只能控制数据搬运无法自动控制GPIO。这就引出了两种解决方案方案一使用DMA的“存储器到存储器”模式配合GPIO控制较为复杂。我们可以构建一个包含命令和数据的复合缓冲区但需要精心设计数据结构并且命令和数据之间的DC引脚切换时序很难用纯DMA完美控制通常还是需要CPU介入。方案二更常用将DMA用于纯数据传输命令由CPU控制。这是最稳定、最常用的方法。具体操作如下CPU负责发送屏幕初始化命令序列、设置绘图窗口x, y坐标等。这些操作数据量小用查询方式即可。当需要填充颜色或刷新图像时CPU发送“写RAM”命令例如0x2C后拉高DC引脚。紧接着CPU启动配置为循环模式的DMA发送通道。DMA会源源不断地将显存中的像素数据通常是16位的RGB565颜色值发送到SPI。此时屏幕就会开始持续刷新。如果你想更新画面只需要修改显存数组中的内容DMA会自动将新内容发送出去实现动态显示。当你需要切换屏幕操作比如从刷新A区域切换到刷新B区域则需要先停止DMA然后由CPU发送新的设置命令再重新启动DMA。这种方式的优点是逻辑清晰控制灵活。我实测过一个400x240的屏幕使用查询方式刷新率不到20帧CPU占用率极高。改用上述DMA循环模式后刷新率稳定在45帧以上并且CPU解放出来可以流畅地运行UI逻辑和触摸检测。在屏幕驱动中你可能会遇到一个典型问题花屏或错位。根据社区反馈有人在使用DMA给屏幕发送图像时图像最下方一行出现了花屏。这很可能是因为DMA传输速度与屏幕消隐时序不匹配造成的。屏幕在每行或每帧数据接收完毕后内部需要时间处理行消隐、帧消隐。如果DMA不顾一切地以最高速度发送数据可能会在屏幕还没准备好接收下一行数据时就把数据送过去了导致错位。解决方案是适当降低SPI的时钟分频比如从SPI_BaudRatePrescaler_2降到SPI_BaudRatePrescaler_4或者在每帧/每行数据发送完成后由CPU插入一个短暂的延时可以通过DMA传输完成中断来插入等待屏幕就绪。另一个检查点是数据对齐确保DMA的数据宽度16位与SPI的数据宽度16位以及你显存中像素数据的格式完全一致。5. 避坑指南SPI DMA实战中的常见“雷区”与调试技巧配置SPI DMA的过程就像探险一路上会遇到不少“坑”。我结合自己的经验和社区里大家常问的问题总结了几条最常见的“雷区”和解决办法。第一个大坑DMA传输根本没启动或者数据传丢了。请按以下清单逐一核对时钟使能了吗除了SPI和GPIO的时钟千万别忘了开启DMA控制器的时钟RCC_AHBPeriphClockCmd(RCC_AHBPeriph_DMA1, ENABLE)。SPI的DMA请求开关打开了吗这是最容易被遗忘的一步初始化SPI后务必调用SPI_I2S_DMACmd(SPIx, SPI_I2S_DMAReq_Tx/Rx, ENABLE)。DMA通道映射对吗确认你初始化的DMA通道如DMA1_Channel3确实是SPI1_Tx所对应的通道。查数据手册的DMA请求映射表。传输方向设置对了吗发送是内存到外设PeripheralDST接收是外设到内存PeripheralSRC。方向反了数据就“倒流”了。数据宽度匹配吗SPI配置为SPI_DataSize_8bDMA也必须配置为DMA_PeripheralDataSize_Byte和DMA_MemoryDataSize_Byte。如果SPI是16位DMA也必须是16位。不匹配会导致数据被拆散或合并产生乱码。内存缓冲区有效吗确保你用于DMA传输的数组不是局部变量函数退出后栈空间可能被覆盖。最好定义为全局变量或静态变量。第二个常见问题DMA中断不触发。如果你使能了传输完成中断但在中断服务函数里打了断点却进不去NVIC配置了吗使能DMA通道中断后必须在NVIC中配置对应的中断通道优先级并使其能。例如DMA1_Channel3的中断函数是DMA1_Channel3_IRQHandler需要在NVIC中使能DMA1_Channel3_IRQn。中断标志清除了吗在中断服务函数里一定要先读取并清除对应的中断标志位例如DMA_GetITStatus(DMA1_IT_TC3)和DMA_ClearITPendingBit(DMA1_IT_TC3)。不清除标志位会导致中断持续触发或者再也进不了中断。传输真的完成了吗检查DMA_BufferSize是否设置正确以及DMA通道是否成功启动。可以用调试器查看DMA通道的CNDTR寄存器当前剩余数据计数看它是否在递减。第三个棘手问题数据错乱或时序问题。比如前面提到的屏幕花屏或者Flash读写数据不对片选CS信号时序。DMA传输启动和结束时片选信号的控制至关重要。通常需要在启动DMA前拉低CS并在DMA传输完成中断里拉高CS。确保CS的拉低和拉高时机包裹住了整个数据帧包括命令、地址、数据。过早拉高CS会截断数据传输。SPI时钟极性相位CPOL/CPHA。DMA不改变SPI的通讯时序。务必确保SPI的CPOL和CPHA设置与你的从设备Flash、屏幕要求严格一致。用逻辑分析仪抓取波形比对是最可靠的方法。竞争条件。当同时使用DMA发送和接收全双工或者快速连续启动多次DMA传输时要注意资源竞争。例如上一次DMA传输还没结束中断未触发就修改了内存缓冲区地址或重新启动了DMA会导致不可预知的行为。良好的做法是设置一个状态标志位等待一次DMA传输完全结束中断置位标志后再准备下一次传输。最后分享两个我常用的调试利器逻辑分析仪这是调试SPI问题的“眼睛”。用它同时抓取SCK、MOSI、MISO、CS甚至DC引脚波形可以一目了然地看到数据是否正确、时序是否符合要求、DMA是否在持续发送数据。很多软件如Saleae Logic能直接解析SPI协议非常方便。调试器查看外设寄存器在IDE的调试模式下直接查看SPI的SR状态寄存器、DMA的CNDTR、CCR等寄存器。观察TXE、RXNE、BSY等标志位的变化以及DMA计数器的递减能帮你精准定位程序卡在了哪个环节。配置SPI DMA的过程确实比查询模式繁琐但一旦调通那种系统运行流畅、资源得到充分利用的成就感会让你觉得一切努力都是值得的。它让你从繁琐的低级数据搬运中解脱出来更专注于产品本身的应用逻辑。