目录
这是本人在实际工作中遇到的设计任务,借此研究状态机设计的优化策略
1. 功能描述
作为模块B,当允许进行传输后,读出FIFO中的数据包,在模块B的相关项填上自身状态信息
协议如下
数据协议 | ||
---|---|---|
1 | [15:0] | 头帧,应为ABCD |
2 | [15:0] | 数据体长度,表示第3项至校验和前一项的项目个数,此处应为 5 |
3 | [15:0] | 模块A当前工作状态 |
4 | [15:0] | 模块B当前工作状态 |
5 | [15:0] | 模块A已完成调用补偿数据个数 |
6 | [15:0] | 模块B已完成调用补偿数据个数 |
7 | [15:0] | 备用 |
8 | [15:0] | 第2~7项按字累加校验和 |
2. 参数设计
Signal | Direction | Width(bits) | Description |
---|---|---|---|
rstn | input | 1 | 异步复位 |
clk | input | 1 | 时钟 |
rx_fifo_rdata | input | 15 | 接受来自模块A的数据FIFO |
rx_fifo_rdata_val | input | 1 | 接受FIFO读出数据有效 |
rx_fifo_rd_en | output | 1 | 接受FIFO读使能 |
rx_fifo_empty | input | 1 | 接受FIFO空标志 |
tx_fifo_wdata | output | 15 | 发送FIFO写数据 |
tx_fifo_wr_en | output | 1 | 发送FIFO写使能 |
tx_fifo_full | input | 1 | 发送FIFO满标志 |
en_trans | input | 1 | 模块B允许传输数据包标志,是一个脉冲 |
work_mode | input | 15 | 模块B当前工作状态 |
compensation_num | input | 15 | 模块B已完成调用补偿数据个数 |
3. 逻辑设计
从状态机的角度起始,先等待en_trans的脉冲,有了脉冲就可以读RX_FIFO,注意读的时候可能读出的数据不属于协议包的数据,所以要判断帧头。并且在读的过程中也可能写的快、读的慢导致这一包数据断断续续。
3.1. IDLE
这个状态比较简单,什么都不做,只等待en_trans的脉冲
3.2. RD_RX_FIFO
该状态下一直拉高rx_fifo_rd_en,读出的数据有效且rx_fifo_rdata为16‘hABCD说明接下来读出的才是协议包。
3.3. PKG_GEN
在该状态下要在读出的数据包中的第4项和第6项填入work_mode和compensation_num,然后将该包发送出去。
实际上,我们也可以先将该数据包的所有内容解析并寄存下来,然后再打一个新的包发送出去。但是这样会额外消耗解析一个数据包的时间,而且这样的状态机设计会比较复杂。
本文的思路是直接将旧包发送出去,只不过在某些项目替换成新的内容,其实就是一种流水线
这个过程可能有点复杂,于是我们分成如下几个小任务:
● 使用data_num记录读出的是旧包的第几项数据,不可多读数据
即RX_FIFO如果存有多个旧包,不能将下一个旧包的数据读出
● 根据data_num,将旧协议包中第4项和第6项重新拼凑一个新的16bit量填入并发送
● 校验和重新计算
● 发送完毕后转回IDLE
同时还要注意RX_FIFO读出的有效数据不一定是时序连续的
设计思路如下
● data_num时序
data_num用来标记当前读出的数据是不是旧包、是旧包的第几项。注意data_num的初始值并不是我们写死的,而是从协议包的第2项获取的。
在RD_RX_FIFO最后一拍帧头已经读出来了,所以PKG_GEN状态下的第一个有效的读出数据就是协议包的第2项,应赋值给data_num,但是在这之后的每读出的一个有效数据都要令data_num减1。
即data_num要与rx_fifo_rdata时序对齐
那么PKG_GEN状态下,如何区分rx_fifo_rdata是要赋值给data_num,还是要data_num减1?最简单的方法——状态机增加一个新的状态,就叫GET_DATA_NUM吧。之前的RD_RX_FIFO也改叫GET_HEAD了。
读出帧头就转入GET_DATA_NUM,在GET_DATA_NUM状态下读出的数据就赋值给data_num,并转入GET_DATA,如下图所示
每次都会为data_num赋值,所以data_num无需担心减溢出问题
● rx_fifo_rd_en控制
从进入GET_HEAD之后rx_fifo_rd_en就要一直拉高,读出旧包。但什么时候拉低呢?在能够读出旧包最后一项就拉低,即不多读,万一多读读出来个16’hABCD第二个旧包的帧头,那不就麻烦了?
那到底是什么时候拉低呢?很简单——cur_state == GET_DATA && rx_fifo_rd_en && !rx_fifo_empty && data_num == 'd1
● 拼包与转发
从帧头读出来开始到data_num为零结束,每读出一个有效数据,都需要判断是否是属于协议包的第4、6项。如果是则不发送,而是拼一个新的16bit发送,如果不是则直接转发。
如下图
发现没,tx_fifo_wdata不就是rx_fifo_rdata打了一拍的结果?tx_fifo_wr_en也是。
然后你就会遇到一个问题:数读出来了,结果TX_FIFO满了写不进去怎么办?
写满了那就等到不满了再写,可是RX_FIFO一直在读呀,这样就会丢失数据。所以rx_fifo_rd_en不能无脑拉高,必须与tx_fifo_wr_en配合。之前关于rx_fifo_rd_en设计有误。
● rx_fifo_rd_en与tx_fifo_wr_en 的协同
怎么配合?必须写成功之后再读下一个数
如下图所示,当rx_fifo_rd_en && rx_fifo_empty
时读出新的数据,同时拉低rx_fifo_rd_en
等待写成功反馈。
tx_fifo_wdata
寄存,并且tx_fifo_wr_en
一直拉高,直到tr_fifo_wr_en && !tx_fifo_full
成立表示写入成功,此时若data_num
大于0就可以再次拉高rx_fifo_rd_en
了。
同理在GET_HEAD和GET_DATA_NUM两个状态下也要保证成功写入之后再进入下一个状态
莫忘记tx_fifo_wdata还要判断data_num来判断要不要用rx_fifo_rdata的值
这里有一个伏笔。此处读出一个数,写入一个数,然后再读。那么是否可以在写数的时候读出新的数呢?因为读出的旧数已经被寄存到tx_fifo_wdata中了,可以读新的数了。
● 校验和与转回IDLE
校验和重新计算就可以,根据data_num
。每从RX_FIFO中读出一个数据,若data_num
不是0、4、6那么就加的是rx_fifo_rdata[7:0]
,如果data_num
是0那么check_sum
可以不加并给到tx_fifo_wdata
。
转回IDLE的条件也很简单,最后一个校验和写入TX_FIFO成功即可,即data_num == 'd0 && tx_fifo_wr_en && !tx_fifo_full
最终得到状态机为
4. 逻辑优化
5. 测试
这里的测试用于证明设计优化后的逻辑没有问题,但是综合的代码可能还比较复杂。
在TB中使用mailbox充当RX_FIFO和TX_FIFO,并且预先向RX_FIFO填入以下数据,除了协议帧还包括乱数
rx_fifo.put(16'h1651);
rx_fifo.put(16'hFEAC);
rx_fifo.put(16'hABCD);
rx_fifo.put(16'h5);
rx_fifo.put(16'hEEEE);
rx_fifo.put(16'h0);
rx_fifo.put(16'h137);
rx_fifo.put(16'h0);
rx_fifo.put(16'h0);
rx_fifo.put(16'h1111);
rx_fifo.put(16'h5455);
rx_fifo.put(16'h5C9A);
输入的work_mode
为16’hFFFF,输入的compensation_num为16’h250
之后的测试transcript如下,可以看出正确获取协议帧并填入了正确的数据
上波形
在IDLE状态下,en_trans拉高一拍进入GET_HEAD状态,并拉高rx_fifo_rd_en
。
之后rx_fifo_rdata
读出两个没用的数据之后出现了帧头16’hABCD,并进入了GET_DATA_NUM状态,注意此时rx_fifo_rd_en
采样为高,但是由于rx_fifo_rd_en && !rx_fifo_empty
说明下一个有效数据能够读出因此拉低rx_fifo_rd_en
,同时拉高rx_fifo_rdata_val_dly
GET_DATA_NUM的第一拍就直接是数据长度16’h5,并且data_num
记录下16’h6,check_sum
加上16’h5。写那边将16’hABCD写入了TX_FIFO,写入完成后,即满足rx_fifo_rdata_val_dly && !tx_fifo_wr_en
,进入GET_DATA。
之后开始正常的并行流水过程,读新数据、写旧数据。直到最后data_num == 16'h1
时不再读了,data_num == 16'h0
时不再写入
如下图所示粉色表示读过程、蓝色表示写过程,可见因为rx_fifo_empty
和tx_fifo_full
均未出现拉高的情况,所以不会出现不读也不写的时间,一直在读一直在写,速度很快。