最近刷FPGA校招笔试题,发现UART发送模块几乎是必考。题目要求波特率可配置、支持8位数据位、1位停止位,我写的时候总是在起始位拉低之后、数据位还没发完就提前跳到停止位,仿真波形看着像少发了一位。想问下这种题的标准写法应该用状态机还是移位寄存器?起始位和停止位的时序边界到底怎么卡才不会错?
2026年FPGA校招笔试里「用Verilog写一个可配置波特率的UART发送模块」这类题,为什么很多人卡在起始位和停止位的状态切换上?
12 个回答

先说个我当年也踩过的坑:很多人把 UART 发送写成「移位寄存器 + 一个计数器」,计数器一到就 shift 一下,结果起始位和停止位没被当成真正的一位来对待,而是被当成「顺便的动作」,于是数据还没走完就跳停止位了。
建议老老实实上三段式状态机:IDLE、START、DATA、STOP 四个状态。核心思路是每个状态都恰好占用一个完整波特周期,状态机只负责「什么时候换状态」,计数器只负责「这一位发够了没有」。起始位拉低后,不要立刻切到 DATA,而是让计数器数满一个波特周期再切;停止位拉高后同理,也要保持一个周期再回 IDLE。这样每一位的时间边界就天然对齐了。
为什么容易错?因为大家往往在状态切换的瞬间顺手把计数器清零,而切换又发生在计数器满的那一拍,导致某一位实际只持续了半个周期甚至更短。一个稳妥做法是:计数器只在「它自己数满」时清零,状态切换用另一个信号去触发,别在 case 里到处 rst 计数器。
另外提醒一个风险点:笔试题里波特率可配置通常意味着分频系数是个输入或参数,仿真时如果分频值设得很小(比如 4、8),边界问题会被放大,波形一眼就能看出少一位。平时练的时候故意把分频值调小去跑仿真,比对着 115200 慢慢看高效得多。
你现在是更纠结状态机怎么写,还是计数器使能条件老对不上?

先说结论:这题卡人往往不是不会写状态机,而是没想清楚「一位数据到底占多长时间」这件事。你可以先不写 Verilog,拿纸画一条时间轴:假设波特率 115200,那每一位就是约 8.68 微秒。起始位从高变低那一刻,只是这一位的开始,它必须整整占满 8.68 微秒,之后才轮到 bit0。很多人潜意识里把「拉低起始位」当成一个瞬时动作,拉完马上就想发数据,于是起始位被压成半个周期甚至更短,接收端采到的东西自然全乱。
一个更抗错的结构是让计数器当「节拍器」,状态机只管「现在该输出什么电平」。具体说,用一个 baud_cnt 从 0 数到 DIV-1,数满产生一个 tick,只有 tick 到来时才允许状态迁移,而且迁移时不要顺手清零计数器——清零动作放在 tick 有效的那一拍统一做,否则边界会漂。状态迁移条件写成「当前状态 + tick 有效」两个条件同时满足,比在状态里散着判断清楚得多。
风险提示:笔试里常见的坑还有两个。一是停止位回 IDLE 时提前一拍,导致停止位只有半个周期,波形上表现为最后一帧还没拉高完整就又开始下一帧起始位;二是波特率分频系数算错,有人把 DIV 设成「时钟频率除以波特率」却没考虑 tick 本身占一拍。建议仿真时把每一位的边界用时间标尺量一下,确认相邻跳变间隔都等于一个位周期,这比盯着整体波形看靠谱。
替代做法:如果你想省状态,也可以只用移位寄存器加一个位计数,把起始位和停止位当成固定的两端塞进移位链里。但可配置波特率加状态清晰这两点,三段式状态机更好讲、更好调、面试官也更好追问。你现在用的是哪种分频写法,DIV 是常数还是参数?

先别急着纠结状态机还是移位寄存器,这俩其实不是二选一——移位寄存器负责『这一位发什么』,状态机负责『现在轮到哪位』。你波形看着少发一位,多半是状态切换和计数器清零撞在同一拍上了。
打个比方:波特率计数器是个节拍器,状态机是个指挥。指挥只能在节拍器敲响的那一下换动作,而且换动作时别顺手把节拍器归零,否则下一拍的时间就被压缩了。你可以在代码里让 baud_cnt 一直数到 DIV-1,数满产生一个 tick 脉冲,所有状态迁移都写成『tick 有效时才判断下一步』,清零也只在 tick 那一拍统一做。
风险上提醒一点:很多人为了省事,把起始位写成『拉低后马上进 DATA』,相当于起始位只占了很短一段,接收端采样点全偏了。正确做法是起始位拉低后老老实实等满一个完整波特周期再进 DATA,停止位拉高后同样保持一个周期再回 IDLE。
如果你时间紧,替代做法是先不管可配置,把波特率写死跑通仿真,确认波形上每一位宽度都一样,再加分频参数。你现在用的是自己搭的 testbench 还是 Vivado 自带仿真?这个会影响我建议你怎么查边界。

这类题卡人的地方,表面看是状态机写法,深层其实是『时间观念』没建立起来。我按一条主线说:先建立位时间的概念,再谈结构,最后才是代码风格。
第一步,把波特率翻译成时间。115200 下一位大约 8.68 微秒,9600 下大约 104 微秒。你脑子里要有一条时间轴:起始位低电平占一格,bit0 占一格,一直到 bit7,然后停止位高电平再占一格。很多人潜意识觉得『拉低起始位』是个瞬间动作,拉完立刻发数据,结果起始位被压成半格甚至更短,接收端采样点全错位。
第二步,理解为什么状态机比纯移位寄存器稳。纯移位寄存器也能做,但它把『发什么』和『发多久』揉在一起,边界一乱就很难定位。三段式状态机(IDLE/START/DATA/STOP)把职责拆开:状态机只决定『当前该输出什么电平』,计数器只回答『这一位发够了没有』。两者通过一个 tick 信号握手,逻辑清晰,仿真波形一眼就能看出哪一位宽度不对。
第三步,卡边界的具体做法。baud_cnt 从 0 数到 DIV-1,数满产生 tick;只有 tick 有效时才允许状态迁移,并且清零动作放在 tick 那一拍统一做,不要写在状态切换的分支里。起始位拉低后等一个完整周期再进 DATA,停止位拉高后也保持一个周期再回 IDLE。这样每一位的时间边界天然对齐,不会出现『少发一位』的错觉。
往深一层说,这道题考的不只是 UART,而是你对『同步时序 + 使能条件』的直觉。以后写 SPI、I2C、甚至自定义协议,都是同一套思路:先定义节拍,再让状态跟着节拍走。建议你仿真时把每一位的边界用光标量一下,宽度一致就说明结构对了。你现在的分频参数是按系统时钟算的,还是直接抄的例程?这个值不对,波形也会骗你。

这种题在笔试里反复出现,但真正卡人的点其实不在状态机本身,而在你有没有建立「一位 = 一个完整波特周期」的时间观。很多人写的时候脑子里想的是「拉低起始位,然后发数据」,把起始位当成了瞬间动作,结果起始位只占了半个周期甚至更短,接收端采样点全部错位,波形上看起来就像少发了一位。
所以我的建议是:先别急着敲代码,拿张纸画时间轴。假设波特率 115200,一位约 8.68 微秒。起始位从高变低那一刻只是这一位的开始,它必须整整占满一个周期,之后才轮到 bit0。把这条时间轴画出来,你会立刻明白为什么不能用「拉低就切状态」这种写法。
结构上,三段式状态机确实比纯移位寄存器稳,原因是它把「发什么」和「发多久」拆开了。状态机只回答「当前该输出什么电平」,波特率计数器只回答「这一位发够了没有」。两者职责分离之后,边界就天然对齐了。常见写法是 IDLE、START、DATA、STOP 四个状态,每个状态恰好占一个完整波特周期。
最容易翻车的地方是计数器清零的时机。如果你在状态切换的那一拍顺手把计数器清零,而切换又发生在计数器满的那一拍,就会导致某一位实际只持续了半个周期。稳妥做法是让计数器一直数到 DIV-1,数满产生一个 tick 脉冲,所有状态迁移都写成「tick 有效时才判断下一步」,清零动作也只在 tick 那一拍统一做,别在状态迁移里随手清。
从求职角度说,这道题面试官真正想看的不是你能不能默写状态机,而是你有没有时序边界的概念。你可以在简历或笔试复盘里准备一句话解释「为什么起始位要等满一个周期」,这比单纯说「我用了三段式」更能体现理解深度。你现在是自己写仿真验证,还是在纸笔笔试里手推波形?这两种练法重点不太一样。

先说一个我自己带学弟时观察到的现象:大家其实不是不会写状态机,而是脑子里对『一位数据』没有物理长度的概念。你可以先做个不用写代码的练习,拿纸横着画一条时间轴,标上 115200 相应的约 8.68 微秒一格,然后把起始位、bit0 到 bit7、停止位一共十格填进去。填完你就会发现,起始位不是『拉低一下』,它是实打实占满一整格的。很多人的波形看着少发一位,本质是起始位被压成了半格甚至更短,后面所有位的采样点集体前移,接收端自然会错。
这个时间观建立起来之后,再谈结构就顺了。我建议就用 IDLE、START、DATA、STOP 四个状态,但关键不在状态数量,而在两个职责要分开:状态机只回答『现在该输出什么电平』,波特率计数器只回答『这一位发够了没有』。常见的错误写法是拉低起始位的同时顺手进 DATA,相当于把 START 状态当成了过场动画。正确做法是拉低后老老实实待在 START,等计数器数满一个完整周期再走。停止位同理,拉高之后也要保持一格再回 IDLE,不然最后一位的收尾会太仓促。
还有一个特别隐蔽的坑:别在状态切换的那一拍顺手把计数器清零。因为切换往往发生在计数器刚数满的那一下,你再清一次,下一位的实际时长就被压缩了,边界会慢慢漂移。稳妥的做法是让计数器一直数到分频值减一,数满产生一个 tick 脉冲,所有状态迁移都写成『tick 有效时才判断下一步』,清零也统一放在 tick 那一拍做。这样每一位的时间边界天然对齐,你仿真时用波形量一下,每格宽度应该是一致的。
如果你现在还在课设或备赛阶段,我的建议是别一上来就追求优雅。先写一版能跑通、波形对的,哪怕代码丑一点;跑通之后再回头把计数器和状态机拆干净。这个过程比直接抄一个『标准三段式』更能让你记住边界为什么这么卡。你现在用的是哪种仿真工具,Vivado 自带的还是 ModelSim 这类?不同工具看 tick 对齐的波形习惯不太一样。

大概率是把「拉低起始位」当成了瞬时动作。起始位也是一位,要占满一个波特周期,再进 DATA;停止位同理,拉高后得撑够一个周期才回 IDLE。写的时候让计数器数满再切状态,别在切换那一拍顺手清零计数器,八成就不丢位了。

起始位和停止位不是『动作』,是各占一整格的位。拉低后老实等一个波特周期再进 DATA,拉高后也等满再回 IDLE。八成是把切换和计数器清零放在同一拍了,让计数器在 tick 那拍统一清零试试。

大概率是把起始位当成瞬时动作了。它也是一整位,拉低后得占满一个波特周期再进 DATA,停止位同理。检查一下计数器是不是在状态切换那拍被顺手清零,改成 tick 那拍统一清,波形就对了。你现在仿真是用固定分频还是参数化的 DIV?

先说个很实用的判断法:如果你仿真波形看着像少发一位,几乎可以断定是起始位或停止位的『停留时间』不够,而不是状态机本身写错了。起始位拉低之后,它必须整整占满一个波特周期,接收端才认得出这是一位起始;同理停止位拉高之后也得撑满一个周期再回 IDLE,否则波形上那一位就被压缩甚至看不见了。所以别把『拉低起始位』当成一个瞬时动作,它是一位,有长度。
写法上我建议用状态机而不是纯移位寄存器,因为移位寄存器把『发什么』和『发多久』揉在一起,边界一乱你根本找不到是哪拍出的问题。状态机至少能让你一眼看出现在是 START 还是 DATA。但状态机也不是万能的,真正决定成败的是波特率计数器什么时候清零——如果状态切换和计数器清零撞在同一拍,那一位就被你偷偷缩短了。
你现在这个模块的波特率是写死分频还是参数化传入的?这会影响你后面调试时的验证方式。
发表回答
登录后可在本页底部提交回答