最近刷牛客发现不少公司FPGA笔试题从单纯写计数器升级成手撕SPI主机,要求支持CPOL/CPHA四种模式切换还要带片选控制。我平时只写过UART收发,对SPI的移位采样时机一直理不清,尤其全双工时MISO和MOSI怎么在同一状态机里同步处理。想请教笔试时这类题该按什么步骤拆:先画时序图还是先定状态?有没有通用的状态机模板可以套?
2026年FPGA校招Verilog笔试新增「用有限状态机实现SPI主机四线全双工收发」,没写过协议时序的应届生该怎么拆解这道题?
10 个回答

先说个可能不太中听的判断:这道题卡你的不是状态机,是「边沿」这件事没想透。你写过UART,UART是按固定波特率在中间采样,天然回避了「时钟沿到底干嘛」的问题;SPI不一样,它的时钟是主机自己发出去的,所以每个沿都可以由你定义语义。四种模式本质就两句话:CPOL决定空闲时SCK是高还是低,CPHA决定第一个有效沿是采样还是移出。CPOL=0时空闲低,第一个沿是上升沿;CPHA=0时这个上升沿就采样,CPHA=1时它改成移出、第二个沿才采样。你把这两句在纸上各画一遍波形,四条时序图摆一起,会发现结构完全一样,只是把「采样沿」和「移出沿」两个变量换了个位置。
所以我的建议是:先画图,但别画得太细。画四张图,每张只标两样东西——第几个SCK沿采样、第几个沿移出,MOSI/MISO的采样时刻对齐。这一步做完,你脑子里应该只剩下一个抽象模型:SCK有「有效沿」和「无效沿」两类,采样发生在某一类上,移出发生在另一类上。状态机就按这个模型写,跟具体是哪种模式解耦。
状态划分其实很朴素:IDLE等start,CS拉低,然后进一个收发循环,循环次数等于位宽(一般8位),循环里每次等一个SCK沿做一件事,最后CS拉高回IDLE。关键取舍在于:是把SCK当成状态机驱动的输出,还是当成外部给的时钟去边沿检测?笔试题里通常要你自己产生SCK,那就用系统时钟分频出一个tick,用tick驱动状态机翻转SCK,同时在同一状态里采样或移出。这样时序可控,也好写。
全双工其实不难,难在你把它想成两件事。就一个移位寄存器:mosi_shift左移出最高位到MOSI,同时把MISO上的值从最低位塞进来。采样沿采MISO,移出沿更新MOSI,两者在同一个状态里处理,互不干扰,因为一个是读、一个是写。你要是分成两个并行模块反而容易乱。
面试和笔试都爱在这几个地方埋坑:CPHA=1时第一个沿是移出,很多人写成采样;CS拉低和第一个SCK沿之间要不要留间隔;循环结束时最后一个沿之后要等半个周期再拉CS。这些你在时序图上都会自然看出来。
最后,模板可以套,但别背模板。你真正要背的是那四张图和「采样沿/移出沿」这个抽象,剩下的状态机是它的自然产物。你打算用系统时钟分频自己产生SCK,还是让题目给SCK、你只做边沿检测?这两种写法骨架差别挺大,先确认下题目倾向哪种。

别急着写状态机,先把CPOL/CPHA四张图画出来。画完你会发现:CPOL只管SCK空闲电平,CPHA只管「第一个沿是采样还是移出」,剩下的收发循环四种模式一模一样。状态就IDLE、CS拉低、8次移位循环、CS拉高,收发共用一个移位寄存器:采样沿读MISO进低位,移出沿把最高位打到MOSI。笔试时间紧,你就按这个骨架写,模式用参数或case分支出采样沿类型,别为四种模式写四套。

先说个容易被忽略的点:笔试里这类题真正拉开差距的不是状态机写得漂不漂亮,而是你有没有把「沿的语义」和「状态机」解耦。很多人一上来就想用状态机去区分四种模式,结果写出四套分支,越写越乱,最后自己都绕晕。正确的顺序其实是先画时序图,而且是带着目的画——每张图只标两件事:第几个SCK沿采样、第几个沿移出。画完你会发现CPOL只是把空闲电平和第一个沿的类型翻了个面,CPHA只是把「采样沿」和「移出沿」这两个角色对调了一下。换句话说,四种模式的收发循环结构完全一样,区别只在两个变量上。
把这件事想透之后,状态机就变得非常朴素:IDLE等启动,CS拉低,然后进一个8拍的循环,每拍里做两件事——在采样沿把MISO读进来,在移出沿把寄存器的最高位打到MOSI,循环结束CS拉高回到IDLE。收发共用一个移位寄存器,这是全双工的关键:发出去一位的同时正好收进来一位,8拍之后两边都齐了。你可以把采样沿和移出沿抽象成两个信号,用CPOL/CPHA组合出来的参数去选,这样状态机主体只有一套。
对没写过协议时序的应届生,我建议的练习路径是:先照着现成波形手画四种模式的时序,再写一个只支持模式0的版本跑通,然后加参数化支持四种。别一上来就追求通用,先让一个模式能对。笔试时间紧,能写出结构清晰、注释到位、模式0正确的主干,往往比写一堆半对的通用代码得分高。你平时用哪套仿真工具验证时序,波形看起来顺手吗?
最后提醒一个常见误区:别把CS的控制塞进移位循环里,CS拉低和拉高应该是独立的边界状态,循环只管8拍数据。这样状态转移图干净,面试官一眼就能看懂你的思路。

先别急着动状态机,拿张纸把CPOL=0/1和CPHA=0/1四张时序图各画一遍。画的时候你会发现规律就两条:CPOL决定SCK空闲是高还是低,CPHA决定第一个有效沿是采样还是移出。剩下的收发循环四种模式一模一样。状态就按IDLE、CS拉低、8次移位、CS拉高来分,收发共用一个移位寄存器,采样沿读MISO、移出沿写MOSI,模式差异用一个参数控制采样沿类型就行。笔试时先保证模式0写对跑通,再谈通用。你打算用哪种方式参数化采样沿,case还是两个独立信号?

先给你一个能救命的判断:笔试里这题大概率不会真让你把四种模式都跑仿真,它看的是你有没有把「模式差异」和「收发循环」分开。所以别一上来就搭状态机,先做一件更划算的事——在纸上竖着画四行波形,每行只标两件事:第几个SCK沿采样、第几个沿移出。画完你会发现CPOL只翻转空闲电平和首个沿类型,CPHA只把采样沿和移出沿这两个角色对调,收发循环结构四行完全一样。
这时候再定状态:IDLE、CS拉低、8拍移位循环、CS拉高,收发共用一个移位寄存器,采样沿读MISO进低位、移出沿把最高位打到MOSI,模式差异收敛成一个参数或一个小case。
风险在哪?很多人把CPHA理解成「第二个沿采样」,一旦CPOL=1就懵了,因为「第一个沿」是下降沿还是上升沿会跟着变。稳妥做法是别记「第几个沿」,记「采样沿 = 空闲电平之后的第一个有效沿(CPHA=0)或第二个(CPHA=1)」,用信号表达而不是靠脑记。
替代做法也有:如果时间实在紧,先把模式0写死跑通,其余模式用参数化采样沿类型扩展,阅卷老师通常认这个思路。你打算用case分模式,还是用两个独立信号表示采样沿/移出沿?

先讲一个笔试现场很常见的翻车点:很多人把CPOL和CPHA当成两件独立的事去背,结果CPOL=1、CPHA=1的时候,脑子里「第一个沿采样」这句话直接失效,因为第一个沿已经从上升沿变成下降沿了。所以拆题顺序上,我建议先画图,但画法有讲究——四张图不要平铺,而是竖着叠成四行,每行只标两个信息:第几个SCK沿采样、第几个沿移出,MOSI和MISO的采样点上下对齐。画完你会看到一件事:四种模式的收发循环结构完全一样,CPOL只是把空闲电平和首个沿类型翻了个面,CPHA只是把采样沿和移出沿这两个角色对调。这个认知一旦建立,状态机就没什么可纠结的了,IDLE、CS拉低、8拍移位循环、CS拉高,收发共用一个移位寄存器,采样沿读MISO进低位、移出沿把最高位打到MOSI,模式差异收敛成一个参数或者一个很小的case分支。
替代做法也有:如果时间紧,先保证模式0写对跑通,再谈通用化,别一上来就追求四种模式全覆盖。风险在于,有些题面会要求「模式可切换」,这时候如果你把模式0硬编码进去,后面补参数会很别扭,所以骨架阶段就留一个采样沿类型的信号比较稳妥。
你打算用哪个方式参数化采样沿,是case分支还是两个独立信号?这个选择会影响后面代码可读性。

我换个角度说,这道题真正在考的不是状态机写法,而是你有没有能力把「协议时序」和「控制流程」这两层拆开。你写过UART,UART的采样点是固定的,波特率一算就定了,天然回避了「时钟沿到底干嘛」的问题;SPI不一样,SCK是主机自己发出去的,每个沿的语义由你定义,所以四种模式本质就是两句话:CPOL决定空闲电平,CPHA决定第一个有效沿是采样还是移出。
想透这一层之后,状态机的结构其实非常朴素:IDLE等启动,CS拉低,然后进一个8拍的循环,每拍里在采样沿把MISO读进来、在移出沿把寄存器最高位打到MOSI,循环结束CS拉高回IDLE。收发共用一个移位寄存器,这是全双工的关键——你不需要两个状态机,也不需要为MISO和MOSI分别设计流程,它们本来就该在同一拍里完成。
真正容易出错的地方在边界:CS拉低到第一个SCK沿之间要不要留空闲拍?最后一个沿之后CS什么时候拉高?这些在笔试里往往比状态划分更能看出你有没有真的写过。我的建议是先把模式0的完整波形跑通,再抽象出采样沿和移出沿两个信号,最后把四种模式套进去验证一遍。别急着写通用模板,模板是理解之后的产物,不是理解之前的拐杖。

先说结论:笔试时先画图,但别画完整的四张全时序,画「采样沿/移出沿」这张对照表就够了。拿一张纸竖着列四行,每行写三个东西:CPOL=0/1对应的空闲电平、CPHA=0/1时第一个有效沿是采样还是移出、MISO在哪个沿被读进来。四行写完你会看到,收发循环的结构四行完全一样,变的只是「哪个沿干采样这件事」。状态划分就按IDLE、CS拉低、8拍移位、CS拉高走,收发共用一个移位寄存器,采样沿把MISO挪进低位,移出沿把最高位甩到MOSI。别为四种模式写四套状态机,那是给自己挖坑。你打算用case分支出采样沿,还是干脆算出采样沿的极性当参数传进去?

先说拆解顺序:一定是先画图,而且是先画「采样沿/移出沿」这两条线,不是先定状态。你拿一张纸横着画四个小波形,每个波形只标三样东西——SCK空闲电平、第一个有效沿是采样还是移出、MISO在哪拍被读进来。画完你会发现CPOL只决定空闲高还是低,CPHA只决定第一个沿干采样还是干移出,剩下的8拍收发循环四种模式长得一模一样。 状态划分就按IDLE、CS拉低、8拍移位循环、CS拉高走,收发共用一个移位寄存器:采样沿把MISO挪进低位,移出沿把最高位甩到MOSI,全双工本来就是同一拍里两件事一起干,不需要两个状态机。模式差异用一个参数或小case分支控制采样沿类型就行,别为四种模式写四套。你平时UART是按固定波特率在中间采样,天然回避了「沿的语义」这层,SPI的时钟是你自己发的,所以先想透沿的语义,状态机反而是最朴素的部分。你现在是卡在CPHA=1那两种模式的采样点判断上,还是卡在状态怎么划分?

先纠正一个很多应届生容易走偏的直觉:你会写UART,其实已经具备拆这道题的核心能力了,只是UART帮你把「沿」这件事藏起来了。UART是异步的,采样点在位中间,你只需要算波特率分频,根本不用思考「时钟沿的语义」;SPI是同步的,SCK是你自己发出去的,每个沿到底干嘛由你定义,所以四种模式的差异本质上就是两句话——CPOL决定空闲电平,CPHA决定第一个有效沿是采样还是移出。把这层想透,题目就从「四种模式」塌缩成「一套循环加两个参数」。
拆解顺序我建议是这样:先画图,但别一上来画四张完整全时序,太费时间。拿一张纸竖着列四行,每行只写三个信息——CPOL对应的SCK空闲电平、CPHA=0/1时第一个有效沿是采样还是移出、MISO在第几个沿被读进来。四行写完你会看到一个关键事实:收发循环的骨架四行完全一样,变的只是「哪个沿干采样」。这一步大概五分钟,但能帮你省掉后面半小时的纠结。
然后是状态划分。别被「四线全双工」吓到,状态就那么几个:IDLE等启动、CS拉低、8拍移位循环、CS拉高回IDLE。收发共用一个移位寄存器,这是全双工的核心——采样沿把MISO挪进寄存器低位,移出沿把寄存器最高位甩到MOSI,同一拍里两件事一起干,不需要为收发各写一套状态机。模式差异用一个参数或很小的case分支控制采样沿类型就够了,千万别为四种模式写四套分支,那是给自己挖坑。
真正的坑在边界,不在循环里。CS拉低到第一个SCK沿之间的建立时间、最后一个沿到CS拉高的保持时间、以及CPHA=0时第一个沿就要采样(此时数据必须提前准备好)这几个点,笔试里不一定会让你写精确延时,但你脑子里要有这根弦。很多人循环写得漂亮,边界一塌糊涂,仿真波形一看CS和数据对不齐。
如果笔试时间紧,我的取舍是:先保证模式0写对跑通,再用参数把采样沿极性抽出来,别追求一次写全四种。你平时仿真用的是哪种工具,ModelSim还是Vivado自带仿真?这会影响你验证时怎么看波形对齐。
发表回答
登录后可在本页底部提交回答