简介:本资源是一份面向嵌入式开发工程师与DSP系统学习者的TI C6678平台SRIO通信实战代码包,聚焦Serial RapidIO高速互连技术在多核DSP场景下的初始化、设备枚举与基础通信实现。压缩包仅含1个核心C源文件(test_srio.c),大小为8KB,完整覆盖6678系统启动配置、SRIO控制器寄存器级初始化、端口枚举流程、中断服务例程及通信验证逻辑,代码经实机测试可稳定运行,适用于通信基站、雷达信号处理等对低延迟互连有严苛要求的工业与军工嵌入式项目。资源已获635人学习下载,读者可直接复用该轻量级SRIO驱动框架,快速掌握C6678平台下PHY/MAC层协同配置方法、设备ID解析与资源分配机制,并基于其结构扩展数据传输、错误恢复等高级功能。 最近在整理老工程时,翻出来一个 6678 SRIO 的源代码项目,这个包当时是从一块评估板项目里抽出来的,被同事压缩成 6678_srio_program.rar 后一直躺在网盘里。里面包含了完整的 CCS 工程、LSU/Doorbell 的初始化代码、收发测试函数,以及一部分硬件配置说明。对正在搞 TI 6678 DSP 和 FPGA 高速互连的朋友来说,这份代码有很高的参考价值,因为它把 Serial RapidIO 最常用的 DirectIO 和 Doorbell 两条路径都写清楚了。这篇文章就借着这份源代码,聊聊 6678 SRIO 的架构理解、代码关键点、板级调试流程,以及我在实际调板过程中踩过的那些坑。
这个主题偏实战,所以我默认你已经有 CCS 和 6678 的基础,但 SRIO 还不太熟。如果你刚接触 KeyStone 架构,也不用担心,我会把寄存器和传输流程尽量掰开讲,让你拿到代码后能直接照着跑起来。
1. 项目背景与 SRIO 基础认知
1.1 6678 的 SRIO 外设能干什么
TMS320C6678 是 TI KeyStone 系列里的八核 DSP,内部集成了一个 Serial RapidIO 2.1 控制器,最高支持 4 lane、每 lane 5 Gbaud。这个外设最大的价值,是让 DSP 跟 FPGA、其他 DSP、或者交换芯片之间建立一条低延迟、高带宽的串行互连通道。相比并行 EMIF 接口,SRIO 的引脚更少、速率更高;相比 PCIe,SRIO 在嵌入式领域里对包交换和点对点传输的支持更直接,很多雷达、图像处理和通信设备都会用到它。
源码包里的工程名称就叫 6678_srio_program,目标非常明确:演示一个最基本的 SRIO 通信链路。它做的事情概括起来就两件:一是通过 LSU(Load/Store Unit)发起 DirectIO 读写,把 DSP 侧的数据写到远端设备的地址空间;二是通过 Doorbell 中断接收远端设备发来的通知。这两件事覆盖了 SRIO 日常使用的绝大部分场景,所以把这份代码吃透,后面再接触 RapidIO 交换机、多核并发传输,就有了基础。
1.2 为什么选择 SRIO 而不是 PCIe 或 EMIF
有朋友问过我,6678 上不是也有 PCIe 吗,为什么非要用 SRIO?我个人的理解是,这主要取决于互连的对端是什么。如果对端是 x86 主机或者 GPU,PCIe 自然是首选;但如果对端是 FPGA 或另外一个 DSP,SRIO 更合适。RapidIO 协议在设计时就很强调嵌入式板间通信,支持直接内存访问语义、消息传递和全局寻址,硬件上又能跑在背板、短距离 PCB 走线甚至光纤上,这些特性让它在信号处理板卡中相当有存在感。
另一个原因是软件生态。TI 对 KeyStone 系列的 SRIO 提供了很完整的驱动库和例程,源码包里的初始化逻辑其实就是基于 TI 的 CSL(Chip Support Library)和 Platform Library 改出来的。相比自己去抠 PHY 层时序,直接基于官方寄存器映射写代码会更稳,也更容易在 CCS 里单步调试。换句话说,如果你只是想在 6678 和 FPGA 之间跑高速数据,先跑通 SRIO 再考虑其他接口,是一个很务实的路径。
1.3 拿到源码包后先看懂整体结构
解压 6678_srio_program.rar 之后,第一眼看到的目录结构通常是这样的:
src/:C 源码目录,包含 main.c、srio_init.c、srio_transfer.c 等文件include/:头文件目录,包含 srio_macros.h、cslr_srio.h 等cfg/:平台的 .cmd 链接文件和启动配置project/:CCS 工程文件目录,带 .project 和 .cprojectdoc/:简单的说明文档和寄存器速查表
建议阅读顺序是:先把 main.c 里 SRIO 相关调用做一个索引,再去看 srio_init.c 里的初始化函数,然后是 LSU 和 Doorbell 的收发实现。代码里很多寄存器操作看起来很枯燥,但最好对照 TRM(Technical Reference Manual)里 SRIO 章节的寄存器来描述看一遍,比如 LSU 有 0 到 15 一共 16 组寄存器,每组都包含 LSU_Reg0 到 LSU_Reg6,这样后续修改地址的时候才不容易晕。
2. 核心模块代码拆解
2.1 main 函数里的初始化顺序
这份代码的 main 函数保持了一个很清晰的初始化链:关中断、初始化 DDR3、初始化 SRIO、配置 Doorbell、发起一个自回环测试或外部回环测试。实际项目中,SRIO 初始化必须在使能中断之前完成,因为一旦远端的 FPGA 在链路训练完成后马上发 Doorbell,中断服务函数可能还没挂好,就丢中断了。
我重点看的是SrioInit()这个函数,它内部会做这样几件事:
- 配置 PLL,产生 SRIO 模块所需的参考时钟
- 设置端口协议和 lane 数,比如 4x 或 1x
- 配置 LSU 寄存器组,默认指向远端的某个设备地址
- 设置 Doorbell 路由表和中断映射
- 把 SRIO 端口使能并等待 link 状态变为 OK
之前调试时为了图快,我一度跳过 PLL 配置,直接把 SRIO 模块打开,结果链路怎么都训练不上。后来补上 PLL 配后发现,SRIO 的 core clock 和 serdes clock 必须按要求来,频率算错的话 PHY 层会直接掉链子。所以拿到别人代码时,第一步不要乱砍,先把初始化链路顺序保留住,再根据板卡实际时钟去改倍频系数。
2.2 LSU 寄存器控制的 DirectIO 传输
SRIO 的 DirectIO 操作其实很像 DMA:DSP 告诉 SRIO 控制器,把本端某块内存写到远端某个地址,或者从远端某地址读到本端内存。这份代码里用到了 LSU 寄存器组,核心是填充下面几个字段:
LSU_Reg0:远端设备 ID 和传输类型LSU_Reg2:远端目标地址的低 32 位LSU_Reg3:远端目标地址的高 32 位(如果需要 64 位地址)LSU_Reg4:本端源地址的低 32 位LSU_Reg5:本端源地址的高 32 位、数据字节数、优先级LSU_Reg6:控制字段,比如发起传输、请求完成、中断使能等
代码里一般会写一个类似SrioDirectIORead()或SrioDirectIOWrite()的函数,把参数组包后写入 LSU 寄存器,然后读 LSU_Reg6 的 busy 位来判断传输是否完成。注意,寄存器写入顺序是很讲究的,通常是先把地址、长度、控制字段都配好,最后再触发。如果你一边写一边触发,很容易造成 LSU 控制逻辑状态异常。
一个很容易忽略的点是:LSU 的地址都是物理地址,不是虚拟地址。6678 在 KeyStone 中通常把共享内存映射到 0x0C000000,所以如果你想从 DDR3 拿数据发出去,务必确认 DDR3 映射在哪个地址段,然后在 LSU 源地址字段写对应的物理地址。否则不发神,发一堆 0xDEADBEEF 出来也是常有的事。
2.3 Doorbell 中断机制与代码实现
Doorbell 是 RapidIO 里非常轻量的消息通知机制,本质上就是一个 32 位的信息字段。远端设备可以发一个 Doorbell 包给 6678,而这个包不携带大量数据,主要用于“告诉 DSP 我这边数据准备好了”或者“链路状态变了”。源码包里对应的中断流程大致是:
- 在 SRIO 模块初始化时,把 Doorbell 到达事件映射到某个中断组合,再通过 KeyStone 的 Event Combiner 映射到 CPU 中断
- 中断服务函数里读取 Doorbell 寄存器,解析信息字段,清中断标志
- 主循环里通过信号量或全局标志来响应这次 Doorbell 事件
这一套流程里最容易出问题的不是配置,而是中断清标志。SRIO 的 Doorbell 状态寄存器里,会有多个 event 位,有些位是写 1 清零,有些位是读后清零。如果你没有仔细看寄存器说明,直接写一个 0 或者写错位,中断标志清不掉,ISR 就会反复触发,几乎卡死整个应用。代码里一般会用SRIO_DOORBELL_ICRR这类宏来辅助清标志,但宏定义在不同 CSL 版本里叫法可能不同,移植时要特别留意。
2.4 工程文件与编译环境注意事项
这个工程是用 TI CCS 建的,版本大概在 5.x 到 6.x 之间。CCS 里导入工程时要注意 Compiler、SYS/BIOS 和 CSL 的版本。尤其是 CSL 头文件,不同版本的寄存器位域定义会有些差别,比如有的版本把LSU_Reg2里的地址字段拆成了多个位域,有的版本直接给了 32 位变量。如果你的项目自带了一套 CSL,而源码包里的 include 路径跟你本机安装的版本不一致,编译时会有大量红色报错。遇到这种问题,最好的办法不是全覆盖,而是对照 TI 官方 CSL 文档,把差异部分一处处改掉。
还要注意 .cmd 文件里的内存映射。SRIO 收发测试一般会用到 MSMC(0x0C000000)和 DDR3(0x80000000 起)。如果 .cmd 里没有给 SRIO 收发缓冲区安排足够的内存段,运行起来很容易踩到未初始化区域,导致 LSU 读到非法 memory,表现就是传输完成中断永远不来。我在调试时最常做的就是先在 main 函数里给缓冲区做一次固定赋值,确保内存可读写,再开启 SRIO 传输。
3. 从 CCS 到板卡的完整实操流程
3.1 硬件连接与准备
在跑代码之前,先确认硬件拓扑。最常见的场景是 6678 DSP 板和一块 FPGA 板通过 SMA 或板载走线连接 SRIO。我手里的设备是 6678 评估板 + Xilinx FPGA 开发板,中间通过一个 4x 的 SRIO 接口连接。两端都要配置成相同的速率和 lane 数,比如都是 3.125 Gbaud 的 4x,或者 5 Gbaud 的 1x。
硬件准备阶段需要测量参考时钟。SRIO 的 SerDes 参考时钟通常由板上的时钟芯片提供,6678 要求这个时钟的精度尽量高,一般是 125 MHz 或 156.25 MHz。如果时钟漂移大,链路训练很难稳定。另外,SRIO 的 lane 布线一般有等长要求,但做系统集成时不一定能完全保证。如果链路偶发训练失败,优先查时钟源和电源纹波,不要一上来就改代码。
接下来就是给板卡上电,连接 CCS 仿真器(XDS200V2 或 XDS560)。我会先用 CCS 的 Target Configuration 连接 6678 内核,然后检查 GEL 文件里的 PLL 初始化是否正常,确保 CPU 主频稳定。这一步没做好,后面所有外设都会连带出问题。
3.2 导入工程并编译烧写
打开 CCS 后,直接选择 Import CCS Eclipse Project,然后选到解压后的 project 目录。如果工程类型是老的 CCS 3.3 格式,CCS 6 以上版本可能不能直接识别,需要手动创建空工程,然后把 src 下的 .c 文件全部拖进去,再把 include 路径和 .cmd 文件路径配好。这个过程比较机械,但能锻炼对工程结构的理解。
编译前要检查的配置项有:
- 编译选项里的目标 CPU 型号,确保是 6678
- 优化级别,建议先用 -O0 调试,跑通以后再开 -O2 或 -O3
- Include path 中是否包含 CSL、XDC 目录,特别是 SRIO 相关头文件
- 是否使能了 SYS/BIOS,如果项目里用了 SYS/BIOS,要注意 Heap 大小和中断配置
编译成功后,把生成的 .out 下载到 6678 的 L2 SRAM 上跑,还是一般直接加载到 DDR3 跑?我个人的习惯是先加载到 L2 SRAM 里跑一个最小测试,因为不依赖 DDR3 初始化,能快速验证 SRIO 时钟和初始化函数。等最小测试把链路跑起来,再把代码放到 DDR3 里做性能测试,这样效率更高。
3.3 跑通一个简单的 SRIO 读写测试
源码包里一般会提供两个函数:一个是写测试,一个是读测试。写测试的逻辑是,DSP 侧初始化一段源缓冲区,往里面填充逐字节递增的数据,然后通过 LSU 指令把这段数据写到远端 FPGA 的 BAR 地址。读测试逻辑反过来,从远端地址读一段数据到 DSP 的接收缓冲区,然后打印出来看是否与期望值一致。
在实际跑测试时,FPGA 侧也要有对应的逻辑响应。如果 FPGA 里只做了 RapidIO 端点,那么 DirectIO 写请求到达后,数据会落入 FPGA 内部寄存器或者 Block RAM;读请求则需要 FPGA 返回 Completer 或者 NWrite 响应。如果 FPGA 逻辑没有正确处理,DSP 侧 LSU 会一直处于 busy 或返回 error。所以两边最好先协商好地址映射表。例如:
- FPGA 端接收窗口:0x00000000 ~ 0x0000FFFF
- FPGA 端发送缓冲区(DSP 读取):0x00010000 ~ 0x0001FFFF
DSP 侧的 LSU 配置时,远端地址就写 FPGA 映射后的地址。我在调试时经常对着 TRM 看 FPGA 侧逻辑,两边交叉验证,比单看 DSP 代码容易定位问题。
3.4 实测速率与性能优化空间
当读写测试稳定后,就可以测性能了。最简单的测法是用 6678 的计时器,统计一次大块 DirectIO 写入的时间,然后除以字节数得到带宽。但直接这样测并不准,因为 LSU 触发后 CPU 会等待门铃中断或 busy 位清除。更好的方式是配合 SRIO 的 EDMA 或在多个 LSU 寄存器组之间轮流发请求,把 LSU 的流水线利用起来。
源码包里的示例可能只用了 LSU 0,最大带宽肯定上不去。要知道 SRIO 在 4x 5 Gbaud 时理论带宽约 5 GBaud * 4 * 8/10 = 16 Gbps,也就是 2 GB/s,但因为协议开销,实际有效带宽通常在 7 到 12 Gbps 之间,具体取决于包大小、读/写方向和 FPGA 端处理能力。如果想提升吞吐,建议用更大的 burst,比如每次写 4 KB 以上,并开启 multiple LSU request,尽量避免小包传输。
还有一个优化点是对齐。SRIO 对 address 和 length 的对齐要求比较严格,比如 32-bit 传输时地址最好 4 字节对齐,64-bit 时最好 8 字节对齐。不对齐会导致 LSU 报错,或者自动拆包影响效率。代码里可以使用#pragma DATA_ALIGN来确保缓冲区对齐。
4. 常见问题与排查记录
4.1 链路无法训练成功的排查套路
链路训练失败是 SRIO 调试里最常见也最头痛的问题。我在这个项目中遇到的典型现象是:SrioInit 初始化完成后,读端口状态寄存器PORT_STAT一直显示UNINITIALIZED或者ERROR。排查时,我会按下面这个顺序走:
- 先看 PHY 参考时钟是否都有,并且频率正确。用示波器测量 SRIO 参考时钟引脚,频率切到 125 MHz,看波形幅度是否足够。
- 再看两端 lane 数、速率是否一致。如果 FPGA 配的是 1x 5 Gbaud,DSP 这边却配的是 4x 3.125 Gbaud,肯定训练失败。
- 检查 SerDes 配置,特别是 TX 的 swing、pre-emphasis,以及 RX 的 equalization。6678 内部的 SerDes 参数在
SrioInit里有一组默认值,不一定适配所有板卡。如果链路抖动大,适当调 TX 驱动强度,或者打开 RX adaptive equalizer。 - 用 SRIO 自回环模式测试。代码包里如果有 loopback 测试宏,先把 SRIO 配置成 internal loopback,确认 DSP 内部 SerDes 和 MAC 层的通路没有问题,再改成外部传输。
我在调板时发现了一个很有意思的问题:初始代码里的 SerDes 配置是基于 156.25 MHz 参考时钟的,但我的板卡实际用的是 125 MHz 时钟。链路训练一直失败,后来我把 CDR 分频系数和 PLL 倍频系数改成对应 125 MHz 的参数,问题立刻消失。这个案例说明,初始化代码里的 SerDes 表格必须跟板级实际时钟匹配,不能照搬。
4.2 Doorbell 中断收不到的常见原因
Doorbell 中断收不到,多半是中断映射或清标志环节出了问题。先用 SRIO 寄存器里的 Debug 寄存器看 Doorbell 事件是否已经到达。如果到达了,但 CPU 没有进中断,那就查中断路由:
- 确认 SRIO Doorbell event 是否组合到了正确的 CorePac 中断输入
- 确认中断控制器里对应 event 是否 enable
- 确认 ISR 里是否做了
ache无效化操作,或者是不是中断栈空间不足
还有一个小坑是 Doorbell 的 ID 过滤。SRIO 可以按源设备 ID 白名单过滤 Doorbell,如果初始化时没有把你远端设备的 ID 加入白名单,Doorbell 包就会被硬件丢掉。我在调试中就用到了这个过滤功能,但一开始没配好,结果 FPGA 发的 Doorbell 永远到不了 DSP,排查了好久才发现是 ID 匹配规则里写反了。
4.3 地址映射和数据对齐问题
DirectIO 场景下,最容易出现数据错乱。我遇到过一个很经典的现象:DSP 向 FPGA 写 1024 字节,FPGA 收到的数据前一半是正确的,后一半变成了上一次发送的旧数据。原因就是 DSP 侧源地址没有 8 字节对齐,导致 LSU 把一块非对齐的内存按照多个 segment 传输,而 FPGA 侧的缓冲处理没有按 segment 边界切分。后来我把收发缓冲区都用#pragma DATA_ALIGN强制 8 字节对齐,并保证传输长度也是 8 的整数倍,问题就消失了。
另外,如果你的系统是多核共享内存,核之间很可能因为 cache 一致性问题导致 SRIO 发出去的数据是旧值。6678 的 L2 cache 是写回策略,如果 CPU 先写了源缓冲,然后立刻触发 LSU 传输,LSU 可能读的是 L2 cache 里的脏数据,但外部看到的已经是过期的数据。解决办法是在发起 SRIO 传输之前,对源缓冲区做CACHE_wbL2操作;在接收数据之后,对目的缓冲区做CACHE_invL2操作。
4.4 版本兼容性与后续扩展建议
这份源代码主要是参考性质,直接拿到新板子上编译时,要留意 CSL 版本和 BSP 版本差异。由于 TI 更新了 CSL 库,某些寄存器结构体名称和字段会改变。比如旧版本使用CSL_SRIO_LSU_READ_WRITE宏,新版本可能推荐直接操作CSL_SrioRegs指针。建议不要把代码粘进新工程就算完,而是根据实际使用到的字段去对照寄存器 map,逐项确认。
如果后续希望做更复杂的应用,可以在现有基础上扩展:
- 利用多个 LSU 寄存器组并行,提高带宽
- 增加 Type 9/Type 5 消息传输,配合 Doorbell 做核间消息
- 接入 SYS/BIOS,使用中断信号量来同步 SRIO 完成事件
- 加上错误恢复逻辑,比如超时重发或链路重建
我在实际项目里,最终把 SRIO 收到数据后的处理放在 DSP 核 0,主循环里轮询 Doorbell 标志,而核 1 到核 7 各自处理信号处理任务,通过共享队列和 SRIO 互联起来,整体能跑得比较稳。这个架构就是从这份基础代码一步步改出来的。
最后分享一个小技巧:在调试 SRIO 时,尽量在 LSU 寄存器操作前后加一些 printf 或日志打印,把提交的源地址、目标地址、长度都打出来。因为 SRIO 寄存器操作的错误很多时候是配置参数的问题,直接把参数打到 Debug 窗口,能省下不少端到端排查的时间。这个习惯我一直保留到现在,尤其是在调 FPGA 和 DSP 联合调试时,非常实用。
本文还有配套的精品资源,点击获取