备赛电赛,选了国产紫光FPGA做RISC-V软核,现在UART和GPIO已经能跑了,但想加定时器中断做多任务调度,不知道中断控制器(PLIC/CLINT)的具体寄存器怎么配,向量表怎么跳转,有没有用国产FPGA做过的大佬给个详细步骤?
2026年了,FPGA大赛电赛题用国产FPGA做RISC-V软核,中断控制器和定时器怎么挂到总线?求具体寄存器配置步骤
6 个回答

先别急着翻寄存器手册,我第一反应是你把CLINT和PLIC当成两个独立IP了。紫光同创的软核方案里,定时器通常挂在CLINT下,中断控制器才是PLIC,两者基地址在《SoC寄存器映射》表里,一般差着好几页。你要做多任务调度,核心是先把CLINT的mtimecmp配好,让定时器到点产生MSIP中断,然后PLIC那边只处理外部中断。具体步骤:查手册确认CLINT基地址,写mtimecmp为当前mtime加一个周期值,再使能全局中断位。PLIC那边更简单,先读claim寄存器拿到中断号,处理完写complete。向量表的话,看你的软核是直接跳转还是查表,紫光那个默认是查表,注意入口地址要4字节对齐。你UART和GPIO都能跑了,说明总线时序没问题,就差这两块地址别搞混。你手头是哪个具体型号?不同系列的基地址可能不一样。

先说结论:你卡在中断上,多半不是寄存器不会写,而是没把中断链路画完整。国产FPGA做RISC-V软核,常见坑有三个:一是CLINT和PLIC的地址空间重叠或偏移,二是中断号映射表和你实际挂载的外设编号对不上,三是向量表跳转时没考虑mstatus.MIE位没打开。给你个排查顺序,按这个走基本能通。第一步,确认软核是单核还是多核,单核的话CLINT只有一个,多核就要注意hart id,紫光这套一般默认单核,先别考虑多核扩展。第二步,查你的软核生成工具自动生成的linker script,里面会标出CLINT和PLIC的基地址,别用网上的通用值硬套,不同型号差很多。第三步,定时器挂CLINT,配mtimecmp和mtime,这两个寄存器是64位的,写的时候先写高32位再写低32位,否则会有瞬间的错乱值触发假中断。第四步,PLIC那边,先全局使能,再设置优先级阈值(一般设0),然后对具体中断号写enable位,最后在中断服务程序里走claim-complete流程。注意claim读回来的中断号可能跟你预期不一样,务必用打印或LED验证。向量表这块,紫光软核默认是查表方式,在启动文件里有个__vector_table数组,每个入口放一个跳转指令,你要在C代码里用attribute((aligned(128)))把数组对齐,否则跳转会错乱。最后提醒一句:调试中断时别开优化,不然mtimecmp和claim的读写顺序会被编译器乱序。你现在的阶段,先把定时器中断跑通,再加调度器,别一上来就搞多任务。另外,你确认过PLIC的基地址是0x0C000000还是0x0D000000吗?不同版本差很多,这个错了后面全白搭。

换个思路,别死磕PLIC和CLINT的寄存器细节,先看紫光官方的裸机例程里有没有timer和interrupt的demo。一般来说,官方SDK里会带一个hello_timer或者blinky_with_interrupt的工程,你把那个工程里的中断初始化函数抄过来,改一下定时器周期就行。我当年做类似的事,就是先把例程跑通,再反过来读寄存器定义,比对着手册硬写快得多。具体到你的问题:定时器挂总线,其实就两件事,一是地址映射对,二是使能链路通。地址映射很好查,紫光PDS里生成软核后,有个generated文件夹,里面会有memory_map.h,直接搜CLINT和PLIC的宏定义。使能链路就三步:CLINT的mtimecmp配好,PLIC的enable位写好,最后开全局中断。这里有个常见误区:很多人忘了PLIC的priority寄存器必须写非零值,否则中断会被mask掉。另外,向量表跳转有个小技巧,你可以在startup.S里先设好mtvec为向量表基地址,然后在C代码里用一个函数指针数组,每个元素指向对应的ISR,这样比在汇编里逐个写跳转指令好维护。风险点在于:紫光这个软核的PLIC可能只支持有限的中断源,你如果GPIO和定时器都用,得确认中断号没冲突。建议你先只挂定时器,跑通一个中断,再加第二个。你现在的板子具体是哪块?如果方便的话,把PDS版本和软核配置截图发出来,社区里有人遇到过同样问题,能直接给你答案。

其实你可以更务实一点,先别管PLIC的完整配置流程,因为电赛题里定时器中断比外部中断更重要。CLINT是内核自带的,不需要挂总线,你只需要把定时器模块例化在总线上,然后让定时器的中断输出直接接到CLINT的MSIP引脚上,这样就不用碰PLIC了。紫光FPGA里,软核的CLINT一般会暴露两个外部引脚:msip和mtip,定时器中断接mtip就行。具体步骤:在PDS里把定时器IP的interrupt信号连到软核的mtip端口,然后软件里只需要写mtimecmp寄存器,使能全局中断,中断发生时读mtime确认是不是定时器到点,最后在mtvec指向的函数里处理。这样绕开了PLIC的claim/complete流程,代码量少一半以上。代价是你只能用定时器中断,不能同时处理其他外部中断,但电赛题目一般够用了。如果你非要PLIC,那就得按标准流程走,但说实话,对于学习软核和做调度器,CLINT直连更直观。你之前UART和GPIO能跑,说明总线接口没问题,现在只是多加一个定时器IP的事。建议你看一下紫光的example里有没有timer_demo,那个工程通常就是CLINT直连的。你打算用几个定时器?如果只需要一个做时间片轮转,这个方案最省事。

个人感觉你被「中断控制器」这个名词吓到了。其实在RISC-V软核里,CLINT和PLIC是两套独立的东西,很多初学者混在一起看。如果你只是想做多任务调度,核心是定时器中断,这个走CLINT就够了,PLIC是可选的。CLINT的寄存器很简单,就三个:mtime(64位,只读)、mtimecmp(64位,可写)、msip(32位,软件中断触发)。你只需要写mtimecmp为当前mtime值加一个时间间隔,然后打开全局中断。向量表那边,紫光软核的默认行为是:发生中断后,从mtvec指向的地址读取一个跳转指令,跳到对应的ISR。所以你要在链接脚本里确保mtvec指向一个4字节对齐的地址,然后在这个地址放一条jal指令,跳到你的C函数。这个比PLIC简单多了。PLIC那套claim/complete流程,是给多个外部中断源用的,比如你同时接按钮和网口,才需要。你现在的阶段,先把CLINT定时器中断跑通,调度器用简单的轮转就行。等你把多任务稳定了,再回头看PLIC也不迟。另外提醒一句:紫光PDS里生成的软核,默认中断使能寄存器可能在CSR层,别只在PLIC里找enable位,先查mstatus的MIE位和mien寄存器。你用的是PDS哪个版本?不同版本对CLINT的地址映射可能有差异。

直接给你一个能用的最小配置流程,按顺序写,别跳步。第一,在PDS里打开软核配置界面,确认CLINT基地址(一般是0x02000000),PLIC基地址(一般是0x0C000000),记下来。第二,写代码时先关全局中断(mstatus.MIE=0),然后配置CLINT:写mtimecmp低32位,再写高32位,顺序不能反。第三,配置PLIC:先写priority寄存器(每个中断源一个,写1),然后写enable寄存器(对应bit置1),再写threshold寄存器(写0表示接受所有优先级)。第四,开全局中断(mstatus.MIE=1)。第五,写中断服务程序,入口处读PLIC的claim地址,拿到中断号,根据中断号判断是定时器还是其他设备,处理完后写complete地址。第六,向量表放在startup.S里,用.word声明每个ISR的地址,然后在mtvec寄存器里填向量表的基地址。注意:紫光软核的向量表是查表方式,不是跳转方式,所以每个表项直接放函数地址就行。你如果只做定时器调度,可以完全跳过PLIC,直接让定时器中断走CLINT的mtip引脚,这样只需要配mtimecmp和全局中断位。最后,调试时用XVC或JTAG看寄存器值,别只靠打印,有时候打印本身会干扰中断时序。你目前的UART和GPIO是轮询还是中断?如果是轮询,建议先把它们改成中断模式,这样能顺便验证PLIC的链路。
发表回答
登录后可在本页底部提交回答