最近刷FPGA校招笔试真题,发现SPI主机这道题几乎年年出现,但每次都在CPOL和CPHA的组合上翻车。我按Mode0写了状态机,仿真波形看着对,可面试官说换成Mode3就全乱。到底SPI四种模式在状态机里怎么统一处理?采样边沿和移位边沿到底该用哪个时钟域?有没有一个能覆盖四种模式的通用写法?
2026年FPGA校招笔试Verilog代码题,「用状态机实现SPI主机」为什么总在时钟极性和相位配置上设坑?
8 个回答

先说结论:面试官不是想看你会背Mode0,而是想看你能不能把CPOL和CPHA从「时序图上的两根线」抽象成「两个控制信号」。很多人翻车的原因是脑子里只有一张Mode0波形图,写代码时把采样边沿和移位边沿直接绑死在时钟的上升沿/下降沿上,一旦要求换模式,等于整个状态机的骨架要重写。
正确的思考顺序应该反过来:先问自己,SPI本质上是不是一个移位寄存器?主机在每个SCK周期里做两件事——移出一位、采进一位。这两件事在时间上必须错开,否则会在同一个边沿上既改数据又读数据,读到的是亚稳态或者刚被自己改掉的值。CPHA决定的正是这个「错开」的方式:CPHA=0时第一个边沿采样、第二个边沿移位;CPHA=1时第一个边沿移位、第二个边沿采样。而CPOL只决定空闲电平,也就是「第一个边沿」到底是上升还是下降。
把这两句话吃透,通用写法自然就出来了。我一般会先由一个分频器产生一个SCK的「基准节拍」,然后在内部生成两个使能信号:sample_en和shift_en。CPOL只影响输出到引脚上的SCK电平(空闲时是0还是1),而sample_en和shift_en则根据CPHA决定是落在节拍的第一个半周期还是第二个半周期。这样状态机本身只干三件事:字节计数、片选拉低拉高、以及在shift_en时把移位寄存器推进一步。位操作全部交给移位寄存器,状态机不碰具体某一位。
面试时我建议的讲法是:先画四种模式的时序图差异,重点讲清「采样沿」和「移位沿」这两个概念,再说你打算用parameter把CPOL/CPHA参数化。这一步很关键——直接写死Mode0,面试官会觉得你只会抄例程;能主动提出参数化,说明你有工程抽象意识。至于时钟域,一般做法是整个SPI逻辑都跑在系统时钟下,用分频计数产生SCK节拍,而不是真的去用SCK当时钟,否则跨时钟域和综合约束都会变复杂。
你现在是自己写仿真验证四种模式,还是只对着波形图看?如果能自己搭一个带CPOL/CPHA参数的testbench跑四组,基本就稳了。

别急着写状态机。先做一件事:把CPOL和CPHA定义成两个parameter,然后写一个「节拍发生器」。CPOL只管空闲时SCK是高还是低,CPHA只管采样发生在第一个边沿还是第二个边沿。内部你只需要两个使能信号,一个管采样、一个管移位,状态机只负责数够8个bit然后收工。这样Mode0到Mode3只是参数不同,代码主体一行不用改。面试时先说时序差异,再说参数化,比直接写死Mode0强很多。

我当年第一次做这道题也栽过,后来想明白一个事:SPI主机这个题,考的根本不是你会不会写状态机,而是你有没有意识到「状态机不该管位操作」。你按Mode0写,状态机里大概率是这么干的:在SCK上升沿采样、下降沿移位,把这四个模式的行为直接焊死在状态转移的边沿判断里。面试官一让你换Mode3,等于把整个采样/移位的骨架抽掉,当然全乱。
正确的切法是分三层。最底层是移位寄存器,只负责移出和采进,它不关心SPI协议,只关心「给我一个移位使能,我就动一位」。中间层是采样和移位两个使能的生成逻辑,这两个信号由CPOL和CPHA共同决定:CPHA决定第一个边沿是采样还是移位,CPOL决定第一个边沿是上升还是下降。最上层才是状态机,它只干两件事——数够8个bit、控制片选拉低拉高。这么一分,换模式就只是改parameter,状态机一行不动。
为什么大家总在这里翻车?我观察下来主要是两个误区。第一个是把「时钟极性」理解成「我要不要翻转时钟」,其实CPOL只是描述空闲电平,SCK在空闲时是高还是低,它不改变采样的相对关系。第二个是把采样边沿和移位边沿绑死在同一类边沿上,比如都在上升沿做,结果同一个边沿又读又写,读到的要么是亚稳态要么是被自己刚改掉的值。这两个动作在时间上必须错开,这是理解四种模式的钥匙。
至于时钟域,别想复杂了。SPI主机里SCK是你自己产生的,它就是分频出来的,和系统时钟同源,所以采样和移位都在系统时钟域里用使能信号控制就行,不需要真的搞跨时钟域同步。面试时如果被追问,你可以说清楚「SCK是输出、是分频得到的,内部逻辑统一在系统时钟下用使能区分动作」,这句话很加分。
准备建议上,别只背波形图。拿张纸把Mode0到Mode3的时序图各画一遍,标出第一个边沿和第二个边沿分别干什么,画完你自己就通了。然后写一份参数化的代码,跑一遍仿真确认四种模式都能出正确波形。面试讲的时候顺序也有讲究:先说四种模式的时序差异,再说参数化怎么统一,最后才提状态机只负责计数和片选。这个顺序体现的是你从上往下的设计思维,比一上来就贴代码强得多。
你现在仿真的时候,是把SCK当成独立时钟在always块里用,还是用系统时钟加分频使能生成的?这个细节会直接影响你代码能不能参数化。

一句话:别让状态机碰数据。状态机只管「数到第几位了、片选什么时候拉」,采样和移位交给两个使能信号去驱动移位寄存器。CPHA决定第一个边沿干活还是第二个边沿干活,CPOL只决定空闲电平是高是低,把这俩当parameter传进去,内部生成sample_en和shift_en,四种模式就是同一套代码换参数。很多人翻车是因为把采样沿写死成上升沿,换模式就得重写骨架。你可以先拿Mode0和Mode3对比着画波形,把「第一个边沿、第二个边沿」分别标出来,画完就懂了。

我换个角度说这事吧,因为技术层面前面已经讲透了,我想聊聊为什么笔试/面试偏偏爱在这里设坑。SPI 的 CPOL/CPHA 本质上是「协议规定里少见的、需要你把抽象参数落到具体波形」的知识点。别的模块比如 UART,波特率、帧格式都是死的,你照着写就行;但 SPI 有四种合法组合,出题人只要把参数一改,就能瞬间区分「背了 Mode0 波形的人」和「真正理解移位寄存器时序的人」。这是出题成本极低、鉴别度极高的一道题,所以年年考。
那作为考生,风险在哪?最大的坑是你把「看起来对」当成「真的对」。你按 Mode0 写,仿真波形是对的,因为 Mode0 恰好采样沿和移位沿正好落在你代码里写死的那个边沿上。可一旦换 Mode3,采样沿反了,你读到的是一位错位的数据,波形上只是数据整体偏了一拍,肉眼不一定立刻看出来,但功能已经废了。所以准备的时候,别只跑一个模式,逼自己把四个模式都仿真一遍,尤其注意第一个 bit 的采样点。
替代做法上,如果你时间紧,来不及把参数化写得特别优雅,至少要做到:把采样沿和移位沿抽成两个信号,而不是直接在 always 块里写 posedge/negedge。哪怕你暂时只支持 Mode0,面试时也要能说清楚「如果让我改 Mode3,我改哪里」——通常就是改这两个信号的生成逻辑。这比闷头写一个漂亮的死代码强得多。
另外提醒一句,笔试现场时间有限,别一上来就追求通用写法把状态机写复杂。先把 8 bit 收发跑通,再回头参数化,是更稳的策略。顺便问一下,你现在是卡在「知道要参数化但不知道怎么写」还是「写出来了但仿真对不上」?这两种情况下的练法完全不一样。

我觉得这个问题最值得深挖的,不是「怎么参数化」,而是「为什么你会在脑子里把 SPI 主机当成一个状态机」。这是很多校招同学的根本误区,它不只影响这一道题,还影响你后面做任何串行接口的模块划分能力。
你想,SPI 主机到底在干什么?它在一个字节周期里,每个 SCK 周期做两件事:移出一位到 MOSI,同时从 MISO 采进一位。注意这两件事在时间上必须错开——你不能在同一个边沿既改自己的输出又去读别人的输出,那读到的要么是亚稳态,要么是你自己刚改掉的值。CPHA 存在的意义,就是告诉你「采样和移位这两件事,分别落在第一个边沿还是第二个边沿」。CPOL 则更简单,它只决定 SCK 空闲时是高还是低,也就是「第一个边沿」到底是上升还是下降。
把这两句吃透,你会发现状态机在这里的角色非常轻:它只需要数够 8 个 bit、控制片选拉低拉高、以及在最后一个周期结束后收尾。真正跟模式相关的东西,全都在「采样使能」和「移位使能」这两个信号上。你只要把 CPOL、CPHA 做成 parameter,内部用组合逻辑生成这两个使能,四种模式就是同一套代码换参数。
那为什么大家会翻车?因为大多数人学 SPI 是从波形图入手的,脑子里只有一张 Mode0 的图,写代码时就把采样沿、移位沿直接焊死在 posedge/negedge 上。这样一来,模式一变,等于整个状态机的骨架要重写。
再往深一层,这其实是个模块划分的训练。一个数字设计能不能做得好,很大程度看你能不能把「协议相关的控制」和「数据通路的操作」分开。移位寄存器不该知道 SPI 是什么,它只认「给我一个使能,我移一位」。状态机不该管位操作,它只认「我数到第几位了」。中间那层使能生成逻辑,才是 CPOL/CPHA 真正落地的地方。
所以我的建议是:先别急着写代码,拿 Mode0 和 Mode3 两张时序图并排画出来,把「第一个边沿」和「第二个边沿」分别标上,看采样点分别落在哪。画完你自己就明白,所谓四种模式,不过是同一条逻辑换了两个参数。这时候你再去写 parameter 化的代码,就不会觉得是硬凑了。
面试时也一样,先讲清时序差异,再讲参数化思路,最后落到「状态机只管计数和片选」,这个顺序比你直接甩一段 Mode0 代码强太多。你目前是把四种模式的时序都画过一遍了,还是只对着 Mode0 的图在调?

先给你一个能跑通四模式的骨架思路:把 CPOL 和 CPHA 做成 parameter,别写死。CPOL 决定 SCK 空闲电平,也就是「第一个边沿」是上升还是下降;CPHA 决定第一个边沿是采样还是移位。内部不要直接拿 SCK 去做边沿判断,而是先产生两个使能:sample_en 和 shift_en,分别对应「该采了」和「该移了」。状态机只干两件事——数够 8 个 bit、控制 CS 拉低拉高。移位寄存器只管在 shift_en 时把 MOSI 更新、在 sample_en 时把 MISO 锁进来。这样 Mode0 到 Mode3 只是参数不同,代码主体一行不用改。面试时你先画两条波形把四种模式标清楚,再讲这个分层,比上来就写 Mode0 强太多。你目前是卡在采样沿的边沿检测,还是卡在状态机怎么数位?

你这个「Mode0 仿真对、Mode3 全乱」其实特别典型,我当年也踩过。问题基本不在状态机本身,而在于你把采样沿和移位沿直接写死进了 SCK 的上升/下降沿判断里,等于把 CPOL/CPHA 这两个参数硬编码了。
先说一个容易忽略的风险点:很多人以为「采样沿对了就行」,其实移位沿同样关键。SPI 一个 SCK 周期里必须做两件事——移出一位、采进一位,而这两件事不能在同一个边沿发生。你要是采样沿和移位沿都绑在同一个边沿上,Mode0 下可能碰巧对,换成 Mode3 就会读到自己刚改掉的值或者采到错位一位的数据,波形上只是整体偏一拍,肉眼很难发现。
替代做法是别让状态机直接碰 SCK 边沿。把 CPOL、CPHA 做成 parameter,内部先生成一个节拍:CPOL 决定空闲电平,也就是「第一个边沿」是升还是降;CPHA 决定第一个边沿是采样还是移位。这样你只需要两个使能信号 sample_en 和 shift_en,移位寄存器在 shift_en 时更新 MOSI、在 sample_en 时锁 MISO,状态机只数够 8 个 bit 加控制片选。
面试时你可以先画 Mode0 和 Mode3 的波形对比,把两个边沿标出来,再说参数化思路,比上来就写 Mode0 强很多。你现在是卡在边沿检测,还是状态机数位?
发表回答
登录后可在本页底部提交回答