
搞STM32H7的人应该都看过那个让人纠结的引脚复用表明明想上PSSI采集并行数据结果一看FMC的SDRAM占了PI0到PI7D0到D7直接被吞掉PSSI瞬间从16位变8位。你要是硬上32位SDRAMPSSI基本就残废了。但这里其实藏着一个非常实用的解法——把SDRAM配成16位数据宽度PI0到PI15整组端口全部释放给PSSI立马实现16位SDRAM和16位PSSI同时运行。这篇文章就把这个方案的原理、配置步骤、Cache一致性坑位和实测验证一条龙讲透适合正在做图像采集、高速数据记录或者并行ADC采集而且需要大缓存来兜底数据吞吐的人参考。1. 为什么SDRAM位宽会决定PSSI的生死引脚资源的数学账先把这个事从根上讲清楚。很多人第一次看到PSSI存在于STM32H7的引脚定义里会默认它是DCMI的替代品直接像以前搞F407那样接摄像头。但PSSI和DCMI有一个本质区别PSSI支持16位并行数据总线而且它的大多数数据引脚并不像DCMI那样分布在PE、PG口而是集中安排在PI端口上。这个设计上的偏好直接决定了它和FMC的SDRAM位宽选择有着极强的耦合关系。1.1 一张引脚复用表揭穿冲突真相在STM32H743/H753这类芯片上PSSI的16个数据引脚PI0到PI15分了两组PSSI_D0到PSSI_D7对应PI0到PI7PSSI_D8到PSSI_D15对应PI8到PI15。与此同时FMC的32位SDRAM数据线里FMC_D24到FMC_D31也恰恰落在PI0到PI7上。你再往下看PH8到PH15上扛着FMC_D16到FMC_D23而SDRAM的低16位FMC_D0到FMC_D15则分布在PD和PE端口。也就是说一旦你把FMC配置成32位SDRAM模式PI0到PI7就被FMC_D24到FMC_D31占死了PSSI的D0到D7直接不可用16位PSSI计划当场破产。我把这个映射关系整理成了一张对比表看起来更直接FMC SDRAM配置低16位数据线位置高16位数据线位置PI0到PI7占用情况PSSI可用数据位32位数据宽度PD0、PD1、PD14、PD15、PE7到PE15等PH8到PH15D16到D23、PI0到PI7D24到D31被FMC_D24到D31完全占用只剩PI8到PI158位16位数据宽度PD0、PD1、PD14、PD15、PE7到PE15等不启用完全释放给PSSI全部PI0到PI1516位所以结论非常明确想保留完整的16位PSSISDRAM就必须选16位模式。这不是性能妥协而是引脚复用规则下的最优解。1.2 16位SDRAM为什么是同时支持的前提标题里那个Simultaneous 16-bit SDRAM and 16-bit PSSI Support的核心就是“同时、完整”这两个词。市面上很多方案为了省事直接上32位SDRAMPSSI就只能退化成8位或者干脆放弃PSSI改用SPI。但如果你认真算一笔引脚账16位SDRAM并没有那么吃亏。这里要澄清一个常见误区16位SDRAM不等于性能砍半。SDRAM控制器的效率和总线位宽有关但实际吞吐瓶颈往往在DMA配置、AHB总线仲裁和SDRAM自身的刷新开销上。对于PSSI这类外设数据源速率通常也就是几十兆字节每秒的量级16位SDRAM的数据带宽完全够用。换句话说你为了PSSI把SDRAM从32位降到16位牺牲的那部分理论带宽在实际应用里大概率感知不到。另外一个容易被忽略的点是SDRAM的地址线和控制线不占用PI端口。SDRAM的地址线A0到A12、Bank地址线BA0/BA1、片选、时钟使能、行列选通等信号主要分布在PF、PG、PH上。所以16位SDRAM模式只占用PD、PE、PF、PG、PH不会和PI口的PSSI产生二次冲突。这也是这个方案能成立的空间基础。简单说硬件上的并行冗余虽然被砍了一些高16位但PSSI拿到了它最需要的那组引脚一换一非常值。2. 从CubeMX到代码双外设共存的完整初始化链路理解了引脚原理之后实际配置就顺畅很多。我推荐用STM32CubeMX先把引脚和时钟理顺因为它会在后台帮你检查复用冲突如果哪个引脚被两个外设同时占用会直接报错。这也是快速验证你选择的SDRAM位宽是否正确的一个手段先把FMC配成32位SDRAM再去启用PSSICubeMX大概率会给你标红一片然后把SDRAM位宽改成16位冲突警告消失整套链路立刻清爽。2.1 FMC SDRAM控制器配置时序不是抄的是算的CubeMX里的FMC配置界面比较直白在FMC页面里选择SDRAM Bank1然后设置数据宽度为16 bit。接下来填时序参数时很多人喜欢直接照搬网络上的值这其实有隐患不同SDRAM颗粒的时序差异很大最好打开数据手册对着算。以最常见的W9825G6KH为例这颗是256Mbit的SDRAM组织方式为2M x 16位 x 4个Bank行地址13位列地址10位。它的主要时序参数里tRCD大约是20nstRP也是20nstRC是63nsCAS延迟可以支持2或3。如果FMC的SDRAM时钟配置为100MHz也就是10ns一个周期那么tRCD和tRP至少要凑够2个时钟周期CAS Latency选3个周期tMRD模式寄存器到激活命令的延迟一般配置为2个周期即可。刷新参数是另一个容易错的地方。标准SDRAM要求刷新周期为64ms覆盖全部8192行算下来平均每行刷新间隔是7.8125us。在CubeMX里那个Refresh rate参数填的就是这个间隔用7.8us这个值即可。但要注意这个参数和SDRAM时钟频率有关联因为控制器是按时钟周期来数刷新计数的如果时钟变了这个数值也要重新核算不能无脑套。初始化过程中还有个隐藏关卡SDRAM上电后不是马上就能读写需要一整套上电初始化序列。CubeMX生成的HAL代码里通常只给出了最基础的时序配置具体的上电初始化命令流需要自己写。标准流程是先向SDRAM发送时钟配置使能命令SDCKE拉高等至少200us的稳定时间然后发送全Bank预充电PRECHARGE命令再执行至少两次自动刷新命令最后写入模式寄存器LMR设置CAS延迟和突发长度等参数。这里如果时序写错最常见的结果就是SDRAM初始化卡死或者读写数据全乱。我贴一段实际可用的FMC SDRAM初始化代码HAL库风格FMC_SDRAM_CommandTypeDef Command {0}; /* 1. 使能SDRAM时钟 */ Command.CommandMode FMC_SDRAM_CMD_CLK_ENABLE; Command.CommandTarget FMC_SDRAM_CMD_TARGET_BANK1; Command.AutoRefreshNumber 1; Command.ModeRegisterDefinition 0; HAL_SDRAM_SendCommand(hsdram1, Command, SDRAM_TIMEOUT); /* 2. 等待200us稳定时间 */ HAL_Delay(1); // 实际延时可以根据系统时钟精细调整 /* 3. 预充电全部Bank */ Command.CommandMode FMC_SDRAM_CMD_PALL; Command.CommandTarget FMC_SDRAM_CMD_TARGET_BANK1; Command.AutoRefreshNumber 1; Command.ModeRegisterDefinition 0; HAL_SDRAM_SendCommand(hsdram1, Command, SDRAM_TIMEOUT); /* 4. 连续两次自动刷新 */ Command.CommandMode FMC_SDRAM_CMD_AUTOREFRESH_MODE; Command.CommandTarget FMC_SDRAM_CMD_TARGET_BANK1; Command.AutoRefreshNumber 2; Command.ModeRegisterDefinition 0; HAL_SDRAM_SendCommand(hsdram1, Command, SDRAM_TIMEOUT); /* 5. 写模式寄存器CAS3突发长度1或8按需求调整 */ Command.CommandMode FMC_SDRAM_CMD_LOAD_MODE; Command.CommandTarget FMC_SDRAM_CMD_TARGET_BANK1; Command.AutoRefreshNumber 1; Command.ModeRegisterDefinition 0x23; // 标准SDRAM模式寄存器配置 HAL_SDRAM_SendCommand(hsdram1, Command, SDRAM_TIMEOUT); /* 6. 配置刷新定时器启动自动刷新 */ HAL_SDRAM_ProgramRefreshRate(hsdram1, 1386); // 具体数值依据SDRAM时钟计算有两点要特别注意模式寄存器那个0x23不是随便写的它拆开来看bit 0到bit 2是突发长度bit 3是突发类型bit 4到bit 6是CAS延迟bit 7是运行模式bit 8和bit 9是写突发模式。0x23这个值表示突发长度8、顺序突发、CAS等于3、正常运行模式。如果你的SDRAM颗粒对CAS有不同要求这个值必须重新计算。另外HAL_SDRAM_ProgramRefreshRate的那个计数数值不是刷新间隔的时间值而是根据SDRAM时钟算出来的计数器初值不同时钟频率下差异很大需要自己在数据手册里找到Refresh period的公式算一遍。2.2 PSSI初始化采样极性和DE信号的细节PSSI在CubeMX里的配置项不算复杂但每个选项都牵一发动全身。数据宽度选择16 bit这个没悬念。关键是采样极性PSSI支持在外部时钟的上升沿或下降沿采样数据这个必须和你的外部数据源对齐。比如数据源在时钟上升沿更新数据你就应该在下降沿采样这样保证数据稳定后才会被采到。如果选错了极性采集出来的数据会出现间歇性错位和毛刺而且是那种规律性很强的错位排查起来非常头痛。DE信号的极性同样重要。PSSI有一个DEData Enable输入引脚用来指示总线上哪些数据是有效的。如果你的外部数据源没有专门的DE引脚可以把它固定拉高或拉低然后在PSSI配置里把极性设成对应的有效电平。这里有一个很容易踩的坑如果外部设备没有DE信号而你把DE极性配置成了高有效那么DE引脚必须接高电平接反了的话DMA永远不会触发因为PSSI认为总线上一直没有有效数据。这个状态卡住的时候调试软件里看PSSI的中断标志位可能一切正常但数据就是不来很容易让人怀疑是DMA配置的问题。PSSI初始化的HAL代码相对简洁PSSI_HandleTypeDef hpssi; DMA_HandleTypeDef hdma_pssi; __HAL_RCC_PSSI_CLK_ENABLE(); hpssi.Instance PSSI; hpssi.Init.DataWidth PSSI_DATASIZE_16B; hpssi.Init.ClockPolarity PSSI_FALLING_EDGE; // 根据外部设备时序调整 hpssi.Init.DataEnablePolarity PSSI_DE_ACTIVE_HIGH; hpssi.Init.PackingMode PSSI_PACKING_DISABLE; // 16位模式下可以关掉 HAL_PSSI_Init(hpssi);PackingMode是PSSI的一个特殊功能它可以把两个8位数据打包成16位存储适合某些8位接口的输出设备。但在我们16位SDRAM加16位PSSI的配置下外部数据本身就是16位的打包模式没意义关掉就好。如果你遇到的是8位PSSI配32位SDRAM的场合打包模式就有它的价值了可以把两个8位样本合成一个32位字减少DMA传输次数。2.3 DMAMUX的灵活映射与双缓冲设计STM32H7系列引入了DMAMUX模块这个设计极大地解放了DMA的通道分配问题。以前在F4上一个外设的DMA请求只能绑到固定的DMA流上换流还不行H7上直接通过DMAMUX把外设请求映射到任意DMA通道上。所以PSSI的DMA请求可以自由地接到DMA1、DMA2或者DMA3的某个通道上完全看你怎么配。我个人推荐把PSSI的DMA挂到DMA3上因为DMA3通道数量更多也支持双缓冲模式配合PSSI这种持续采集的外设特别合适。双缓冲的核心理念是数据先DMA到SDRAM里的缓冲区A等缓冲区A满了之后自动切到缓冲区B同时触发中断让CPU去处理缓冲区A里的数据这样采集和处理可以并行不会因为CPU处理太慢而丢数据。在SDRAM里分配缓冲区时要注意对齐问题。DMA访问16位SDRAM缓冲区地址最好对齐到32位也就是4字节边界这样DMA在SDRAM上做突发传输时效率最高。如果你是16位外设传16位内存地址不要求像32位外设那样严格但养成对齐的习惯总没错避免某些DMA测试模式下出现诡异的数据错位。DMA配置核心代码大概长这样hdma_pssi.Instance DMA3_Channel0; // 任意通道通过DMAMUX映射 hdma_pssi.Init.Request DMA_REQUEST_PSSI; hdma_pssi.Init.Direction DMA_PERIPH_TO_MEMORY; hdma_pssi.Init.PeriphInc DMA_PINC_DISABLE; hdma_pssi.Init.MemInc DMA_MINC_ENABLE; hdma_pssi.Init.PeriphDataAlignment DMA_PDATAALIGN_HALFWORD; hdma_pssi.Init.MemDataAlignment DMA_MDATAALIGN_HALFWORD; hdma_pssi.Init.Mode DMA_NORMAL; // 按需改成循环模式 hdma_pssi.Init.Priority DMA_PRIORITY_HIGH; HAL_DMA_Init(hdma_pssi); HAL_DMA_Start(hdma_pssi, (uint32_t)PSSI-DR, (uint32_t)(SDRAM_BASE 0x1000), 4096); HAL_PSSI_DMAStart(hpssi, (uint32_t)PSSI-DR, (uint32_t)(SDRAM_BASE 0x1000), 4096);注意HAL_PSSI_DMAStart这个函数的第二个参数是外设地址第三个是内存地址第四个是传输长度这里单位是字还是半字要仔细看HAL库的注释不同版本可能有差异。如果这个长度和你的实际内存宽度不匹配会出现只传输了一半数据或者多传输了一倍数据的诡异现象而且仪表盘上看起来还一切正常。3. 数据通道上的第一个大坑Cache一致性很多人栽在这个问题上。配置完FMC SDRAM和PSSI之后代码逻辑看着都对DMA也启动了外部信号也给了但CPU去SDRAM里读数据的时候读出来的是全零或者完全没更新的旧数据。排查半天DMA传输计数也动了寄存器状态也对最后才怀疑到CPU的D-Cache头上。3.1 DMA写SDRAMCPU读到旧数据STM32H7的内核是Cortex-M7带独立的I-Cache和D-Cache。D-Cache会把CPU最近访问过的内存数据缓存到SRAM里下次CPU再读同一块地址会优先从Cache里取不再访问外部SDRAM。问题就出在这里DMAPSSI把新数据写到了SDRAM物理地址但CPU的Cache里还存着这块地址的旧数据而且被标记为有效CPU去读就直接拿到了Cache里的旧数据DMA写的新数据被完全忽略了。反过来也一样。如果CPU先往SDRAM写了数据写操作可能只更新了Cache还没来得及写回SDRAMDMA就开始搬运搬走的还是旧数据。这两个方向上的Cache一致性问题在DMA相关的H7开发中是排名前三的隐藏杀手。3.2 MPU把SDRAM区域设成非缓存解决这个问题的标准做法是通过MPU把SDRAM所在的地址区域标记为不可缓存或者至少标记为write-through模式。不可缓存意味着CPU每次读写都直接访问SDRAM物理地址不走Cache这样DMA和CPU看到的数据永远是同步的。代价是CPU读SDRAM的速度会下降一些但对于PSSI采集这种场景CPU本来就不需要高频访问SDRAM里的每个字节这个代价完全可接受。MPU配置代码示例MPU_Region_InitTypeDef MPU_InitStruct {0}; HAL_MPU_Disable(); MPU_InitStruct.Enable MPU_REGION_ENABLE; MPU_InitStruct.BaseAddress 0xC0000000; // SDRAM Bank1起始地址 MPU_InitStruct.Size MPU_REGION_SIZE_32MB; // 按实际SDRAM大小调整 MPU_InitStruct.AccessPermission MPU_REGION_FULL_ACCESS; MPU_InitStruct.IsBufferable MPU_REGION_NOT_BUFFERABLE; MPU_InitStruct.IsCacheable MPU_REGION_NOT_CACHEABLE; MPU_InitStruct.IsShareable MPU_REGION_NOT_SHAREABLE; MPU_InitStruct.Number MPU_REGION_NUMBER0; MPU_InitStruct.TypeExtField MPU_TEX_LEVEL0; MPU_InitStruct.SubRegionDisable 0x00; MPU_InitStruct.DisableExec MPU_INSTRUCTION_ACCESS_ENABLE; HAL_MPU_ConfigRegion(MPU_InitStruct); HAL_MPU_Enable(MPU_CONTROL_PRIVILEGED_DEFAULT);这里有个细节MPU Region Size要跟SDRAM实际容量匹配如果你的SDRAM是16MB配成32MB的Region也不是不可以但会把后面不存在的地址空间也标记为不可缓存如果后续还有别的外设映射在那块区域可能会有隐患。所以Region大小按实际SDRAM芯片容量来配最稳妥。另外这个MPU配置必须在SDRAM第一次被访问之前就生效最好放在系统初始化阶段而不是在PSSI启动前临时配一下。如果你不想用MPU还有另一个办法就是在每次DMA结束中断里手动做Cache失效操作SCB_InvalidateDCache_by_Addr((uint32_t *)buffer_addr, buffer_size);注意Invalidate的地址和大小要跟DMA实际写入的区域完全一致而且地址必须按32字节对齐大小也要是32字节的整数倍否则会留下边界残留问题导致前后两次采集的数据有一小段是脏的。相比之下MPU方案更省心一次配置终身受用。3.3 双DMA和总线的隐藏瓶颈当你同时跑多个DMA的时候比如PSSI的DMA往SDRAM写数据另一个DMA在搬数据或者供电机制控制某个外设总线上就会发生竞争。STM32H7的总线矩阵设计比F4复杂得多但它并不是所有端口都完全互联的。SDRAM挂在FMC上FMC的AHB接口是有限的多个主机同时访问SDRAM时总线矩阵会按优先级仲裁。这个优先级是可以调的AHB总线矩阵的主机优先级配置寄存器可以通过调整不同主机比如DMA3、CPU的AXI的优先级来保证PSSI的DMA不被其他主机长时间阻塞。我在实际调试时发现如果系统里同时启用了以太网DMA和PSSI DMA而且两者都要访问SDRAM默认优先级下PSSI可能会出现偶发丢数据把PSSI DMA通道的优先级提到最高之后丢数现象就消失了。但这里也要提醒一句优先级设得太极端会把CPU的实时性拖垮。因为CPU也经常通过AXI总线访问SDRAM比如运行在外部Flash里的程序指令预取如果SDRAM被DMA占死CPU取指都可能被卡住整个系统像死机一样。所以调整优先级是个权衡目标只是保证关键外设的最小带宽而不是把总线独占。4. 实测16位SDRAM加16位PSSI同时跑起来的验证方案理论配置完了代码也写了最终还是要靠实测来证明整个链路是通的。我习惯的验证思路是分层推进先单独测SDRAM再单独测PSSI最后联合起来测整条链路。每一步都有明确的判定标准这样出问题的时候能快速定位到具体模块。4.1 测试环境与信号源我常用的信号源是一个FPGA开发板它通过16位并行总线和一根时钟线连接到STM32H7的PSSI接口。FPGA内部生成一个计数器每次时钟上升沿把计数器值放到总线上同时输出一个低有效的DE信号告诉PSSI总线上数据有效。这样外部输入的数据就是已知的递增序列后面比对数据时非常直观。如果没有FPGA用逻辑分析仪的Pattern Generator功能也能实现类似效果或者用一个简单的16位并行ADC接上信号发生器让ADC采集正弦波并输出到PSSI总线上只是数据比对时要多做一步转换。测试环境的连接要点PSSI_D0到D15接到FPGA输出的16位数据线PSSI_CLK接FPGA提供的采样时钟PSSI_DE接FPGA的DE信号。如果FPGA输出的是3.3V电平STM32H7的IO可以直接对接但建议在数据线上串33欧姆电阻降低信号振铃。时钟线的完整性对PSSI采样质量影响最大时钟线上不要走太长也不要和高速信号线平行走否则采样误码率会直线上升。4.2 验证代码思路第一步先验证SDRAM基本读写功能。向SDRAM地址范围内写入一组已知数据比如先写0x0000到0xFFFF的递增序列再回读比对。这一步通过说明FMC SDRAM的初始化序列和时序配置基本正确。如果回读数据不对优先检查模式寄存器的CAS配置、刷新周期计数以及行/列地址宽度是否和SDRAM颗粒规格匹配。第二步单独验证PSSI。先不启动DMA只把PSSI的中断打开在中断里读PSSI的数据寄存器直接观察数据是否是预期的递增序列。这里建议用断点或者串口打印确认数据没问题后再关掉中断改走DMA方式。第三步把PSSI和DMA绑定起来DMA目标地址指向SDRAM。持续采集一段时间后停止DMA然后CPU遍历SDRAM缓冲区逐字核对数据是否等于FPGA发出的递增序列。如果中间断了一段说明DMA传输中丢了数据需要检查DMA的FIFO配置和总线优先级如果数据错位了大概率是采样极性的问题。这里我给出一个简单的校验思路uint16_t expected 0; uint32_t error_count 0; for (uint32_t i 0; i buffer_len; i) { if (buffer[i] ! expected) { error_count; if (error_count 10) { printf(Index %lu: expected %04X, got %04X\r\n, i, expected, buffer[i]); } } expected; } printf(Total errors: %lu\r\n, error_count);这个递增序列校验虽然简单但非常有效。它能直接告诉你数据是连续错位还是间歇性丢失这是定位采样极性和DMA配置两个不同方向问题的最快路径。4.3 踩坑记录三个最隐蔽的问题第一个坑SDRAM初始化序列中模式寄存器写入之后如果是突发长度配置为8读出的数据顺序可能不是线性递增而是交织的。这是SDRAM内部交错读取的特性通过把突发长度改成1可以规避。对PSSI采集场景来说单次DMA传输本来就需要连续数据突发长度1虽然效率低一些但兼容性最好。第二个坑PSSI的DE信号没接或者极性配反DMA永远不触发。前面提到过这里再强调一次PSSI的DE信号是门控数据采集的总开关它不拉高或拉低取决于极性数据寄存器永远不会更新。很多调试案例中PSSI配置成高有效DE外部FPGA的DE在空闲时是高电平结果数据一直不来。把DE改用低有效或者软件反转数据就正常了。这个问题特别隐蔽因为你去看PSSI寄存器的状态标志它不会主动报错就只是没有数据流产生。第三个坑16位SDRAM下DMA传输数据的宽度如果误配成32位会导致数据间隔跳变。这个问题的特征是采集到的数据每隔一个值就错位一次看起来是偶数索引的数据正确、奇数索引的数据不对或者干脆所有数据都整体偏移了一个字节。本质上是因为外设数据是16位的而DMA按32位搬运两个16位样本被拼成了一个32位字写入SDRAM后的地址分布跟CPU按16位读取时的访问模式错开了。解决方式就是统一用半字宽度配置DMA的外设和数据端或者你在CPU侧也按32位读取后自己拆分但没必要自找麻烦。5. 扩展玩法从PSSI加SDRAM到完整采集显示链路16位SDRAM加16位PSSI这个组合验证通过之后基本盘就稳了。这组合的价值不只是能采集数据而是给后续扩展留足了空间。5.1 加入LTDC显示PSSI采集的图像如果你做的是图像采集类应用接下来大概率会想接一个LCD显示器做实时预览。STM32H7的LTDC时序引脚主要在PJ和PK端口和PSSI的PI端口、FMC SDRAM的PD/PE/PF/PG端口都不冲突所以在LQFP176封装的芯片上你可以同时跑PSSI采集、SDRAM存储、LTDC显示三个外设构成一条完整的数据管线。这里要重新做一次带宽核算。假设PSSI的时钟是50MHz16位数据宽度那么它每秒钟往SDRAM里写入100MB的数据。LTDC这边如果是一块480x272的屏RGB56560Hz刷新率像素时钟大概10MHz每像素16位带宽约20MB/s