第16周 项目1-基数排序

本文深入探讨了基数排序算法的原理与应用,通过代码实现展示了如何使用C语言对链表进行排序。从低到高逐位排序,最终输出排序后的结果。
/* 
*文件名称:1.pp 
*作者:崔从敏 
*完成日期:2015年12月14日 
*问题描述:验证算法
*/
#include <stdio.h>
#include <malloc.h>
#include <string.h>
#define MAXE 20         //线性表中最多元素个数
#define MAXR 10         //基数的最大取值
#define MAXD 8          //关键字位数的最大取值
typedef struct node
{
    char data[MAXD];    //记录的关键字定义的字符串
    struct node *next;
} RecType;
void CreaLink(RecType *&p,char *a[],int n);
void DispLink(RecType *p);
void RadixSort(RecType *&p,int r,int d) //实现基数排序:*p为待排序序列链表指针,r为基数,d为关键字位数
{
    RecType *head[MAXR],*tail[MAXR],*t; //定义各链队的首尾指针
    int i,j,k;
    for (i=0; i<=d-1; i++)                  //从低位到高位循环
    {
        for (j=0; j<r; j++)                 //初始化各链队首、尾指针
            head[j]=tail[j]=NULL;
        while (p!=NULL)                 //对于原链表中每个结点循环
        {
            k=p->data[i]-'0';           //找第k个链队
            if (head[k]==NULL)          //进行分配
            {
                head[k]=p;
                tail[k]=p;
            }
            else
            {
                tail[k]->next=p;
                tail[k]=p;
            }
            p=p->next;                  //取下一个待排序的元素
        }
        p=NULL;                         //重新用p来收集所有结点
        for (j=0; j<r; j++)             //对于每一个链队循环
            if (head[j]!=NULL)          //进行收集
            {
                if (p==NULL)
                {
                    p=head[j];
                    t=tail[j];
                }
                else
                {
                    t->next=head[j];
                    t=tail[j];
                }
            }
        t->next=NULL;                   //最后一个结点的next域置NULL
        //以下的显示并非必要
        printf("  按%d位排序\t",i);
        DispLink(p);
    }
}
void CreateLink(RecType *&p,char a[MAXE][MAXD],int n)   //采用后插法产生链表
{
    int i;
    RecType *s,*t;
    for (i=0; i<n; i++)
    {
        s=(RecType *)malloc(sizeof(RecType));
        strcpy(s->data,a[i]);
        if (i==0)
        {
            p=s;
            t=s;
        }
        else
        {
            t->next=s;
            t=s;
        }
    }
    t->next=NULL;
}
void DispLink(RecType *p)   //输出链表
{
    while (p!=NULL)
    {
        printf("%c%c ",p->data[1],p->data[0]);
        p=p->next;
    }
    printf("\n");
}
int main()
{
    int n=10,r=10,d=2;
    int i,j,k;
    RecType *p;
    char a[MAXE][MAXD];
    int b[]= {75,23,98,44,57,12,29,64,38,82};
    for (i=0; i<n; i++)     //将b[i]转换成字符串
    {
        k=b[i];
        for (j=0; j<d; j++) //例如b[0]=75,转换后a[0][0]='7',a[0][1]='5'
        {
            a[i][j]=k%10+'0';
            k=k/10;
        }
        a[i][j]='\0';
    }
    CreateLink(p,a,n);
    printf("\n");
    printf("  初始关键字\t");        //输出初始关键字序列
    DispLink(p);
    RadixSort(p,10,2);
    printf("  最终结果\t");         //输出最终结果
    DispLink(p);
    printf("\n");
    return 0;
}


运行结果:

<think>我们目前讨论的是使用Verilog实现RTP包排序的问题。用户之前提到了查找对乱序RTP包进行排序的方法,而我的任务是用Verilog实现排序算法。由于RTP包通常带有序列号(sequencenumber),我们可以利用这个序列号对包进行排序。考虑到Verilog是硬件描述语言,我们需要设计一个适合硬件实现的排序算法。常见的硬件排序算法有:1.插入排序(InsertionSort)-适合小规模数据,硬件实现较简单。2.归并排序(MergeSort)-可以并行化,适合硬件,但实现复杂。3.基数排序(RadixSort)-适合固定位宽的数据,硬件友好。4.冒泡排序(BubbleSort)-实现简单,但效率较低。在硬件中,我们通常希望算法能够并行化以提高速度,同时考虑资源消耗。由于RTP包的序列号是16位的(根据RTP标准),我们可以利用序列号的值进行排序。考虑到实时性,我们需要一个低延迟的排序方法。这里我们可以考虑使用双调排序(BitonicSort)或奇偶排序(Odd-EvenSort)等并行排序算法,它们适合在FPGA上实现。然而,对于RTP包排序,我们通常需要的是一个缓冲区(buffer)来存储乱序到达的包,然后按照序列号排序后输出。由于包是乱序到达,我们可能需要一个重排序缓冲区(Re-orderBuffer,ROB)。ROB的实现可以采用以下思路:1.使用一个固定大小的缓冲区(例如一个RAM或寄存器组)来存储RTP包。2.每个RTP包有一个序列号(seq),我们按照序列号对包进行排序。3.由于包可能不是连续到达,我们需要记录哪些位置的包已经到达(使用有效位valid)。4.当收到一个包时,将其存入缓冲区,并标记该位置有效。5.排序输出策略:我们可以按序列号顺序输出缓冲区中连续的包(即从最小的序列号开始,如果连续的序列号都存在则输出,直到遇到空缺则停止,等待空缺的包到达)。但是,这种重排序缓冲区通常需要管理逻辑,例如使用一个队列,按序列号大小入队,然后按顺序出队。在硬件中,我们可以用以下方法:方法一:使用内容可寻址存储器(CAM)或类似结构,根据序列号查找并存储包。但CAM资源消耗大。方法二:使用寄存器组(registerfile)和排序网络(sortingnetwork)。例如,我们可以将到达的包存储在寄存器组中,同时记录有效位。然后使用一个排序网络(如插入排序网络或冒泡排序网络)对有效包按序列号进行排序。然后从最小的序列号开始输出连续的包。方法三:使用FIFO队列的变种,例如优先级队列,但我们要求按序列号顺序输出,而序列号是递增的(但可能有回绕)。RTP序列号是16位,会从65535回绕到0。所以我们需要处理序列号回绕。针对序列号回绕问题:比较两个序列号a和b时,如果|a-b|>32768,则说明发生了回绕,此时应该将较小的序列号视为较大(或相反)?实际处理中,通常使用一个16位的减法结果来判断先后顺序。我们可以这样判断:如果b-a(有符号)在[-32768,32767]范围内,且b-a>=0,那么b在a之后;如果b-a为负且绝对值较大(如小于-32768),则说明b是回绕后的值,应认为b在a之后(因为序列号达到65535后下一个是0)。更简单的做法是:将序列号扩展为32位,然后用32位进行比较,这样回绕问题就不存在了(因为回绕后的序列号相当于加上65536),但这样会消耗更多资源。然而,为了简化,我们可以采用16位无符号数的减法:如果seq1-seq2<0x8000(即32768),那么seq1在seq2之后(假设序列号递增)。但这种方法在比较时需要小心。另一种常见做法是:在RTP中,序列号可以视为一个16位的循环计数器。我们可以定义两个序列号的差值小于32768并且为正数时,认为前者小于后者。但硬件实现比较器较复杂。考虑到实现的复杂度,我们可能希望尽量避免处理回绕,或者将回绕处理单独考虑。实际上,在缓冲区内,包与包之间的序列号差通常不会超过32768(因为缓冲区大小有限),所以我们可以直接比较两个序列号的差值来判断顺序。当两个序列号相差超过32768时,说明发生了回绕,此时我们可以反直觉地认为数值小的序列号实际上是更大的(因为它属于新的一轮)。但这样实现比较器会很复杂。因此,我们可以将序列号扩展为17位(或32位),然后在比较时,根据实际到达的包的序列号动态调整一个基序列号(basesequence)。例如,当我们收到一个包时,根据当前基序列号将其扩展为32位(基序列号以上的位由基序列号决定,可以设定当基序列号大于32768时,序列号小于32768的包都被视为基序列号+65536)。这种方法虽然可行,但管理基序列号也增加了复杂度。因此,我们可以考虑以下简化:缓冲区大小设为有限(例如64个包),这样序列号在缓冲区内最多跨越64个序号,远远小于32768,所以我们可以直接按无符号比较两个序列号(因为回绕不会在64个序号内发生两次)。这样我们就避免了回绕问题。设计步骤:1.设置一个固定大小的缓冲区(例如N=64),每个槽位包含:-有效位(1位)-序列号(16位)-数据(根据RTP包的数据位宽)2.输入:当收到一个RTP包时,我们将其写入缓冲区。写入的位置可以选择:-按序列号对缓冲区槽位进行排序,然后插入到合适位置(插入时需要移动元素,硬件复杂)-或者,我们采用一个循环缓冲区,按到达顺序存储,但排序时进行排序。这样每次收到包只需写下一个位置,然后启动排序。3.排序:在每个时钟期,我们使用一个排序网络(例如冒泡排序的并行实现)对缓冲区中的有效包进行排序。排序结果是一个有序的列表(按序列号递增)。4.输出:从有序列表的头部开始,如果序列号是连续的(即当前最小序列号是上一个输出包的下一个序列号),则输出该包,并将其有效位置0,释放该槽位。注意:我们只能输出连续序列号开始的连续包。但是,上述方法在缓冲区较大时,排序网络会很大(N=64时,比较器的数量为O(N^2))。因此,我们可以考虑使用其他排序算法,如基数排序,或者使用分组排序(如先分组排序再合并)。替代方案:使用一个大的移位寄存器,每个位置可以存储一个包。每次新包到来时,与当前寄存器的包并行比较,找到插入位置,然后通过一个多路选择器网络插入到正确位置(类似插入排序,但并行插入)。这种结构称为“插入排序器”(insertionsorter),适合小的N。对于N=64,我们可以使用二叉树结构来实现插入排序,这样需要O(logN)级,但每级有多个比较器。实际上,我们可以使用一个多级比较网络。考虑到FPGA的资源,如果N不大(如64),我们可以用寄存器堆和排序网络来实现。例如,使用冒泡排序的并行实现(奇偶排序)需要N*(N-1)/2个比较器,当N=64时,需要2016个比较器,这是非常多的。因此我们可以采用更高效的排序网络,如Batcher奇偶归并排序,其比较器数量为O(Nlog^2N),当N=64时,比较器数量约为约300个(需要精确计算)。或者采用双调排序(Bitonicsort),比较器数量为O(Nlog^2N)。双调排序网络在硬件中较为常用。双调排序需要比较器的级数为log2(N)*(log2(N)+1)/2。当N=64时,log2(64)=6,因此级数为6*7/2=21级。每级有N/2个比较器,所以总比较器数量为21*32=672个比较器(实际上每级的比较器数量并不完全相同,但可以按最大估算)。双调排序网络是可以完全并行的,但需要多级流水线。因此,我们可以在FPGA上实现一个双调排序网络,对缓冲区中的64个包按序列号排序。但我们需要注意的是,缓冲区中有些槽位可能无效(即没有包)。所以在排序时,我们只对有效包排序。一个简单的方法是将所有无效包赋予一个最大的序列号(比如全1),这样排序后有效包就会排在前面。然后我们只处理有效包中序列号连续的部分。但这样会浪费资源,因为无效包也参与了排序。改进方法是:在排序前,先将有效包聚集到一起(例如用优先编码器),然后只对有效包排序(但有效包的数量是动态的,所以排序网络需要动态调整大小,这很复杂)。因此,为了简化,我们直接使用固定大小的排序网络,无效包用最大值填充。步骤:1.将新包写入缓冲区中一个空闲槽位(即当前无效的槽位),将其有效位置1。如果没有空闲槽位,则丢弃新包(或丢弃最老的包,但通常我们会设置足够大的缓冲区防止丢包)。2.在排序前,将每个槽位的数据和有效位一起处理:如果有效位为0,则将用于排序的序列号设置为一个比任何合法序列号都大的值(比如17'h10000,但比较时我们使用17位,因此合法序列号是16位,最大为65535,所以可以设置最大值为17位1,即131071)。这样,无效包会排到最后。3.使用双调排序网络对所有槽位进行排序(基于扩展后的17位序列号,这样处理回绕?不,这里我们还没有处理回绕)。注意:由于我们扩展到了17位,那么合法序列号的最大值也是65535,我们设置无效包为131071,这样在排序时无效包确实排在最后。4.排序后,有效包按序列号升序排列在前面连续的位置(从0到k-1,k为有效包个数),无效包在后面。5.然后,我们从排序后的第一个槽位开始检查连续序列号:从索引0开始,如果当前包有效(实际上排序后前k个都是有效的),并且当前包的序列号等于预期的下一个序列号(预期序号用一个寄存器记录),则输出该包,并将该槽位置为无效(或标记为空闲),然后更新预期序号为当前包的序列号+1(注意回绕:加1后超过65535则变为0)。6.注意:在输出时,可能一次输出多个连续的包(因为排序后是连续的,但可能会有空缺)。我们需要在输出时从最小的序号开始,连续输出直到遇到空缺。例如,预期下一个序列号为100,而排序后第一个包的序列号是100,输出后预期变成101;然后检查第二个包,如果是101则输出,以此类推,直到遇到序列号不是预期的包则停止输出。由于排序后有效包按序列号递增排列,但是序列号不一定是连续的(可能有空缺)。因此,我们不能一次性输出所有排序后的包,而是只能输出从预期序号开始的连续包。预期序号(expected_seq)的维护:初始时,我们可以设置为接收到的第一个包的序列号(但第一个包可能是乱序的,因此我们可能需要等待序列号为0的包?)。实际上,预期序号应该从第一个接收到的包开始,然后按顺序输出。我们可以这样初始化:当系统启动时,预期序号为0。当收到第一个包时,如果它的序列号不等于0,则我们需要等待0号包?这可能会导致问题。在RTP中,接收端一开始并不知道发送端起始的序列号,因此我们可以将第一个接收到的包作为起始,设置expected_seq为这个包的序列号,然后输出这个包,并将expected_seq加1。之后,就等待下一个expected_seq的包。因此,在排序完成后,我们从排序后的结果中检查从第一个位置开始,看是否有包等于expected_seq,如果有则输出,并将expected_seq加1(回绕处理);然后继续检查排序后的下一个位置(但注意:排序后的下一个位置包序列号可能大于expected_seq,因为排序后是按序排列,但不连续)。因此,我们需要在排序后从前向后扫描,但只输出连续的从expected_seq开始的包。具体操作:从排序后的第一个包开始(索引0),直到某个包不连续为止:-如果当前包序列号等于expected_seq,则输出该包,将expected_seq加1(注意回绕),并将该槽位标记为空闲(有效位置0)。然后继续检查下一个槽位(索引1)。-如果当前包序列号不等于expected_seq(由于排序后是递增的,所以一定大于expected_seq),则停止输出(因为出现空缺)。-注意:排序后的结果中,前k个是有效包,所以扫描只进行到k-1,并且只输出连续序列。这样,我们每次排序后只输出一个连续的序列段(从expected_seq开始直到遇到空缺)。然后等待新的包到来,再次排序,再尝试输出。为了能够在一个时钟期完成排序,我们需要使用流水线。双调排序网络通常需要多个时钟期(例如21级)。因此,我们可以将排序过程流水化。整体设计框图:+-------------------------------------------------------------+|Buffer:64entries,eachhasvalid,seq,data||+-------+||+----->|Write||||+-------+||newpkt--+||||+--v--+||||MUX|//写入空闲槽位|||+--+--+|||||||+--v--+||||Buffer|||+--+--+||+------------+||||||+----v---++----v-----+|||Fill||Re-order||||Max||Control|||+----+---++----+-----+||||||+----v--------------v---+|||SortingNetwork|//双调排序网络,流水线|||(BitonicSorter)|||+-----------+-----------+|||||+-----------v-----------+|||ContinuousOutput|//输出连续包|||andUpdateExpectedSeq|||+-----------+-----------+|||||v||Outputdata|+--------------------------------------------------------------+注意:在排序网络之前,我们需要将无效槽位填充最大值(FillMax模块),排序网络输出后的结果需要检测连续包并输出,同时更新缓冲区的有效位和expected_seq。由于排序网络是流水线的,我们需要将整个缓冲区更新和输出过程也流水化。新包的写入和输出的读取需要协调,避免同时读写同一个位置。我们可以使用双缓冲?或者使用异步FIFO?但在这个设计中,排序网络是期性的工作,新包的写入是实时发生的。所以我们需要在排序期间暂停写入吗?这取决于排序网络的流水线深度。如果排序网络需要21期,那么每21期才能进行一次排序和输出。但新包是随时到达的,因此我们需要一个输入FIFO来缓存新包,等排序结束后再写入缓冲区,然后开始下一次排序。这样,两次排序之间会间隔至少21期。因此,整体流程:1.新包到达时,先进入一个输入FIFO。2.当输入FIFO不为空,并且当前排序网络空闲(即不在排序中)时,我们可以将FIFO中的包写入缓冲区(写入一个空闲槽位)。然后启动排序网络(将当前缓冲区的状态送入排序网络流水线)。3.排序网络经过21期后,输出排序结果。4.在排序网络输出端,我们进行连续包的输出(可能需要几个期,因为连续包可能多个,但通常连续包输出可以在一个期内完成,因为排序后连续包在排序结果的头部连续出现,我们只需要遍历直到遇到空缺)。5.输出完成后,我们将输出包发送到下游,并释放缓冲区中对应的槽位(有效位置0)。6.然后,如果输入FIFO中还有包,则再次写入缓冲区并启动排序;否则等待新包。这个设计可能需要一个状态机控制。此外,双调排序网络的具体实现比较复杂,我们可以参考已有的Verilog双调排序代码。由于排序需要21级流水线,每级需要64个比较器吗?不,双调排序网络每一级都会变化,不是每级都是N/2个比较器。但是,总比较器数量较大。我们可以考虑用模块化设计。由于资源消耗较大,如果N=64过大,我们可以减小N,比如32,那么log2(32)=5,级数=5*6/2=15级,比较器数量约为300个(具体计算:双调排序的比较器总数C(n)=(nlog2(n)(log2(n)+1))/4,其中n为元素个数。当n=64时,C(64)=(64*6*7)/4=64*10.5=672;当n=32时,C(32)=(32*5*6)/4=32*7.5=240)。对于FPGA来说,240个比较器(每个比较器是17位无符号比较)是可以接受的。因此,我们可以根据实际需求设置缓冲区大小。如果网络乱序不是很严重,32个包可能就足够了。总结:用Verilog实现RTP包的排序,我们可以使用一个重排序缓冲区,结合双调排序网络进行排序,然后输出连续的包。同时,处理序列号回绕问题的方法是通过设置缓冲区大小来避免回绕(因为缓冲区小于32768),并使用17位的扩展(用于无效包的最大值)来进行排序和无效包的处理。下面提供部分Verilog代码框架(不包括完整的双调排序网络,因为排序网络代码较长,我们可以用模块实例化):模块接口:modulertp_sorter(inputwireclk,inputwirerst_n,inputwire[15:0]in_seq,//输入包的序列号inputwire[WIDTH-1:0]in_data,//输入包的数据inputwirein_valid,//输入包有效outputregin_ready,//可以接收新包outputreg[15:0]out_seq,//输出包的序列号outputreg[WIDTH-1:0]out_data,//输出包的数据outputregout_valid,//输出包有效inputwireout_ready//下游准备好接收);参数:缓冲区大小N(例如32),数据位宽WIDTH。内部结构:-输入FIFO:深度设为4,使用简单的移位寄存器FIFO。-缓冲区:一个寄存器数组,每个元素为{valid,seq,data},共N个槽位。-双调排序网络:使用流水线实现,输入为N个槽位(经过处理:无效槽位用{1'b1,17'd131071}填充?注意,我们比较时只比较17位序列号,所以数据部分不参与排序,因此排序网络只操作{valid,seq}?不,我们填充最大值时,实际排序时只使用扩展的序列号(17位)进行比较,而数据部分保持不变。所以排序后数据部分也跟着交换。双调排序网络模块:modulebitonic_sorter#(parameterN=32,parameterDW=17//序列号位宽)(inputwireclk,inputwire[N*DW-1:0]in,//输入向量,包含N个DW位的元素outputwire[N*DW-1:0]out);//内部实现:包括多级流水线//...但是,我们缓冲区需要保存的数据不止序列号,还有数据包(可能很大)。排序网络需要交换整个槽位的数据。因此,DW应设置为1(valid)+16(seq)+WIDTH(data)。但这样DW会很大,导致排序网络的资源消耗很大(因为比较器会很大,而且交换的数据量也大)。改进:我们可以只对序列号进行排序,并生成索引(即输出排序后的索引),然后再根据索引将数据重新排列。这样排序网络只操作索引和序列号,而不操作数据部分。最后根据排序后的索引读取缓冲区的数据。这种方法称为“间接排序”。具体方法:在排序之前,我们创建一个索引数组index[0:N-1](0到N-1),以及对应的键值数组(key[0:N-1]),键值就是扩展的序列号(17位)。我们将整个缓冲区槽位的有效位也编码到键值中:如果无效,则键值设为最大值(17'd131071);如果有效,则键值设为{1'b0,seq}(这样有效包都会在无效包前面,因为我们比较时是从小到大,有效包的键值最小为0,最大为65535,无效包是131071)。注意:这样设置后,有效包的键值在0~65535之间,无效包的键值都是131071。排序网络对键值数组和索引数组同时排序(即每次交换时同时交换键值和索引)。这样,排序后,前k个键值(有效包)是顺序排列的,同时索引数组的前k个值就是这些包在缓冲区中的位置索引。然后我们只需要按索引去缓冲区读取数据。这样,排序网络操作的数据宽度只有17位(键值)和log2(N)位(索引)。例如N=32时,索引5位。那么整个排序网络的数据宽度为(17+5)=22位。而整个数据包(1+16+WIDTH)可能很大(WIDTH可能几十个字节,即几百位),所以间接排序可以大大减少排序网络的数据宽度,从而减少资源消耗。输出连续包时,我们只需要处理排序后的前k个索引以及对应的序列号(注意:排序后的序列号可以从键值数组中取出,但实际序列号在排序网络中是和索引一起被排序的?其实我们排序时键值就包含了有效位和序列号,但无效包我们不关心序列号,而有效包我们只需要取出序列号的16位)。我们可以这样设计:在排序网络之前,我们生成键值数组和索引数组:for(i=0;i<N;i=i+1)beginkeys[i]=buffer[i].valid?{1'b0,buffer[i].seq}:17'd131071;indexes[i]=i;end排序网络输入:keys[0]...keys[N-1]和indexes[0]...indexes[N-1],输出:sorted_keys[0]...sorted_keys[N-1]和sorted_indexes[0]...sorted_indexes[N-1]。排序后,我们关注sorted_keys[0]到sorted_keys[k-1](其中k为有效包个数,但排序后只要sorted_keys[i]小于131071,就表示有效包,因为有效包的键值最高位为0,而无效包的最高位是1(键值131071的二进制是17'b1_000000000000000,但注意我们给有效包的键值是{1'b0,seq},所以最高位是0,无效包是{1'b1,0}?不,我们给无效包直接赋值131071,即17'b1_11111111111111111,这样所有无效包的键值都是这个值,有效包的键值最高位都是0,所以排序后有效包在前面,无效包在后面,而且有效包内部按序列号升序排列)。然后,我们按顺序检查sorted_keys[i](i从0开始):将sorted_keys[i]分解为:valid_flag和seq_val,其中valid_flag=sorted_keys[i][16],seq_val=sorted_keys[i][15:0]。但实际上,我们只需要检查valid_flag:若为1,则这个包是无效包(因为键值131071的最高位为1),所以遇到无效包就停止输出检查。所以,我们只关心那些valid_flag为0的包。我们得到seq_val,然后与expected_seq比较。但是,expected_seq是16位的序列号,而这里seq_val也是16位的。比较两个16位序列号是否连续:因为我们已经避免了回绕(缓冲区大小N较小),所以直接比较:seq_val==expected_seq。如果相等,则输出包:从缓冲区中的第sorted_indexes[i]个槽位取出数据输出,然后expected_seq加1(回绕:expected_seq+1,如果达到65536就回绕为0)。同时,我们释放该槽位:将缓冲区中第sorted_indexes[i]个槽位的有效位置0。然后继续下一个i(i++),直到遇到seq_val!=expected_seq(或者遇到无效包,或者sorted_keys[i]的valid_flag==1)则停止。注意:由于排序后的有效包是按序列号升序排列,所以如果我们连续输出,它们也必定是连续的。因此,当输出一个包后,下一个包(i+1)的序列号应该等于expected_seq+1,这样我们才能连续输出。因此,我们可以在一个期内同时输出多个包(但需要下游的out_ready支持多个包同时输出,通常我们下游是流水线,一次只能接收一个包,所以我们需要将连续多个包依次输出,可能需要多期)。因此,在输出控制部分,我们需要一个状态机来逐个输出连续包:状态机状态:IDLE:等待排序网络完成OUTPUT:从i=0开始,逐个输出连续包,直到遇到不连续或无效包但排序网络输出的结果是一次性得到的,所以我们在得到排序结果后,会得到一个排序后的索引数组。然后我们可以用计数器从0开始扫描。具体输出步骤:在排序网络完成后的第一个期(即在状态OUTPUT),我们设置一个指针j=0。然后检查sorted_keys[j]的最高位:如果为1,表示已经扫描到无效包(说明有效包已经结束)且没有包可输出,则回到IDLE。如果为0,则检查序列号seq_val[j]是否等于expected_seq。如果等于,则触发输出(将缓冲区中对应槽位的数据通过输出接口发送),同时更新expected_seq,释放该槽位(有效位清0),然后j++,继续下一个(下一个期)。如果不等于,则停止输出,回到IDLE。但这样在OUTPUT状态内,每次只能输出一个包。连续输出多个包需要多个期(每个包一期)。这样设计是可行的,因为我们下游可能一次只能接收一个包。另外,在输出时,要考虑输出接口的ready:如果下游ready为0,则当前包无法输出,我们需要暂停,直到下游ready。因此,输出控制部分的状态机:状态1:IDLE-等待排序完成信号(由排序网络流水线完成后发出一个valid信号),然后进入状态2。状态2:OUTPUT-逐个输出连续包。每个期,检查指针j(从0开始),然后:if(j>=N)进入IDLE(因为已经扫描完所有的排序结果)。elseif(sorted_keys[j][16]==1)//无效包,表示有效包已经输出完,进入IDLE。else//有效包if(sorted_keys[j][15:0]==expected_seq)and(out_ready==1)thenout_valid=1;out_seq=sorted_keys[j][15:0];out_data=从缓冲区中第sorted_indexes[j]个槽位取数据;//更新expected_seqexpected_seq=expected_seq+1;//考虑回绕,用模65536加法//释放该槽位buffer[sorted_indexes[j]].valid=0;j=j+1;//下一个//停留在OUTPUT状态,下一个期继续检查jelse//当前包不是连续的,或者下游没有准备好//注意:即使下游没有准备好,我们也不能在此时输出,所以如果下游没准备好,我们停留在此状态等待//注意:如果是因为包不连续,那么即使下游准备好我们也不输出,因此我们结束输出,回到IDLE。//因此,需要分开处理://如果包连续但下游没准备好,则等待(状态保持,不更新j,也不释放槽位)//如果包不连续(即seq_val不等于expected_seq),则结束输出过程,回到IDLE,并保留j(因为j以后还有有效包?但我们是按顺序扫描,所以不连续的包后面的包即使连续也不输出(因为要求从expected_seq开始连续))。if(out_ready==0)then//等待下游准备好,状态保持,j不变else//下游准备好,但包不连续(即缺失包),则结束输出state<=IDLE;注意:如果包连续但下游没准备好,我们等待。这样可能会导致排序网络不能及时启动(因为我们还停留在OUTPUT状态),但输入FIFO可能满了,我们仍然需要先处理输出。此外,我们需要注意,在输出过程中,新包的写入是可以同时进行的吗?由于新包的写入是在IDLE状态下进行的(并且排序网络空闲时),所以在输出状态,我们不接收新包(即in_ready=0)。因此,在输出状态,输入FIFO会累积。但输出状态可能持续多个期(等待下游ready)。如果下游卡住,输入FIFO可能会满。所以我们需要设计输入FIFO的backpressure。总体状态机:输入FIFO的控制:当状态为IDLE时,如果输入FIFO非空,且缓冲区有空闲槽位(即有无效的槽),则从FIFO中取一个包写入缓冲区(写入一个空闲槽位,同时将该槽位有效位置1),然后启动排序网络(将当前缓冲区的键值数组和索引数组送入排序网络流水线)。然后进入状态SORTING,等待排序结果。状态转换:复位后:状态=IDLE,expected_seq=0(或者初始化时可以为0,也可以等待第一个包时设置)IDLE状态:如果有新包在输入FIFO中(非空)并且有空闲槽位,则:1.从FIFO中取出包。2.在缓冲区中找一个空闲槽位(可以用优先编码器找到第一个无效槽位),写入包(置有效位,序列号,数据)。3.生成键值数组和索引数组(基于缓冲区当前所有槽位)。4.启动排序网络(设置valid信号),进入SORTING状态。另外,在IDLE状态,如果排序网络完成信号到达(实际上,排序网络是在流水线工作,启动后需要多个期,所以我们这里需要先进入SORTING状态等待完成信号),但我们不应该这样处理。所以:我们设计:启动排序后进入SORTING状态,SORTING状态持续一定的期数(即排序网络的延迟),然后当排序网络输出有效时,进入OUTPUT状态。在SORTING状态:只是等待,计数器(或信号)指示流水线完成。我们可以用一个计数器,计数21期(排序网络延迟固定),然后进入OUTPUT状态。或者,排序网络模块有一个输出valid信号,我们在SORTING状态等待该信号为1,然后进入OUTPUT状态。在OUTPUT状态:如上所述,逐个输出连续包。当一次连续输出过程结束后,回到IDLE状态。需要注意的是,在输出过程中,可能会有新的包到达并进入输入FIFO。当回到IDLE后,会再次处理输入FIFO。然而,在IDLE状态启动排序网络时,排序网络开始执行,在排序网络流水线工作期间(SORTING状态),我们不能再写入新的包,因为缓冲区内容需要固定不变(因为排序网络正在处理当前快照)。因此,在SORTING和OUTPUT状态,输入FIFO可以接收新包,但不能写入缓冲区。直到回到IDLE状态,我们才可以再次写入。另外,在输出过程中,我们正在从缓冲区中移除包(释放槽位),所以空闲槽位会增加。因此在输出过程结束后,缓冲区有了更多的空闲槽位,这时可以接收更多的包。双调排序网络模块的设计可以查阅资料。Verilog实现双调排序网络可以参考开源项目。由于篇幅和复杂度,我们无法在这里给出完整的双调排序网络代码。但双调排序网络的核心是递归构建,我们可以用generate生成。最后,由于序列号回绕问题我们通过缓冲区大小避免了,但在比较时,我们期望序列号是递增的,而缓冲区大小限制了跳变在64(或32)以内,因此不会跨越32768边界,所以直接比较16位无符号数即可。综上所述,我们给出了一个用Verilog实现RTP包排序的方案。这只是一个框架,实际实现需要考虑很多细节,包括双调排序网络的正确实现、关键路径的优化以及资源使用等。相关问题:1.双调排序网络在Verilog中如何实现?2.RTP包排序对时序要求很高,如何优化流水线深度?3.处理序列号回绕除了缓冲区大小限制还有其他方法吗?4.如何验证该排序设计的正确性?参考文献:[1]Batcher,K.E.(1968).Sortingnetworksandtheirapplications.InProceedingsoftheApril30–May2,1968,springjointcomputerconference(pp.307-314).</think> ### 使用Verilog实现RTP包排序的算法方案 在RTP(实时传输协议)通信中,数据包可能乱序到达,需要根据序列号(16位)进行排序。以下是基于Verilog的硬件排序实现方案,结合缓冲区管理和排序网络技术实现高效低延迟的乱序包排序。 --- ### **核心设计架构** ```verilog module rtp_sorter ( input wire clk, input wire rst_n, // 输入接口 input wire [15:0] in_seq, input wire [DATA_WIDTH-1:0] in_data, input wire in_valid, output reg in_ready, // 输出接口 output reg [15:0] out_seq, output reg [DATA_WIDTH-1:0] out_data, output reg out_valid, input wire out_ready ); parameter N = 32; // 缓冲区大小(推荐32-64) parameter DATA_WIDTH = 64; // RTP数据位宽 // 缓冲区、状态机、排序网络等内部逻辑 endmodule ``` --- ### **1. 缓冲区设计** - **数据结构**: 每个缓冲区槽位包含: ```verilog reg valid; // 有效位(1位) reg [15:0] seq; // 序列号(16位) reg [DATA_WIDTH-1:0] data; // RTP数据 ``` - **写入策略**: - 通过优先编码器选择首个空闲槽位写入新包。 - 写操作在`in_valid & in_ready`时触发。 - **序列号处理**: - 缓冲区大小 \(N\) 设置为小于32768(如32),避免序列号回绕问题。 - 无需特殊回绕处理(因缓冲区内最大序号差 \(\ll 32768\))。 --- ### **2. 双调排序网络(Bitonic Sorter)** - **间接排序**: 键值数组(17位)存储扩展序列号,索引数组(\(\log_2 N\)位)存储槽位索引: ```verilog wire [16:0] keys[N]; // 键值:{1'b0, seq}(有效包)或17'd131071(无效包) reg [$clog2(N)-1:0] indexes[N]; // 索引 ``` - 无效包赋予最大值(131071),确保排序后排在尾部。 - **排序优势**: - 资源消耗:O(\(N \log^2 N\)) 比较器(N=32时约240个)。 - 流水线级数:\(\log N \times (\log N +1)/2\)(N=32需15级流水)。 - **模块接口**: ```verilog bitonic_sorter #(N, 17+$clog2(N)) sorter ( .clk(clk), .keys_in(keys_concatenated), .indexes_in(indexes_concatenated), .sorted_keys(keys_sorted), .sorted_indexes(indexes_sorted) ); ``` --- ### **3. 输出控制逻辑** - **连续包输出**: 1. 从排序结果头部开始扫描有效包(`valid_flag=0`)。 2. 若当前包序列号 = `expected_seq`: - 输出数据,更新: `expected_seq <= (expected_seq == 65535) ? 0 : expected_seq + 1` - 清除槽位有效位。 3. 若序列号不连续或下游未就绪(`out_ready=0`)则停止。 - **状态机设计**: | 状态 | 描述 | |-------------|--------------------------------| | `IDLE` | 等待新包,启动排序 | | `SORTING` | 排序网络流水线执行(15期) | | `OUTPUT` | 按序输出连续包 | --- ### **4. 整体工作流程** 1. **输入阶段**: - 新包存入输入FIFO,若缓冲区有空闲槽位则写入。 - 为键值数组赋值:有效包 = `{1'b0, seq}`,无效包 = `131071`。 2. **排序阶段**: - 启动双调排序网络,15期后输出排序索引。 3. **输出阶段**: - 按索引顺序扫描缓冲区,输出连续包并释放槽位。 - 非连续包留待后续排序期处理。 --- ### **关键优化点** 1. **资源优化**: - 间接排序:仅操作键值(17位)和索引(\(\log_2 N\)位),避免大位宽数据交换。 - 并行比较:双调排序网络每级流水线同步比较。 2. **时序优化**: - 流水线设计:排序与输出阶段重叠,输入FIFO缓存新包。 - 动态调整:通过`expected_seq`避免全局复位。 3. **鲁棒性**: - 输入FIFO防溢出(推荐深度4)。 - 缓冲区满时丢弃最旧包(或流控通知上游)。 --- ### **示例场景** 假设缓冲区(N=4)状态: | 槽位 | valid | seq | data | |------|-------|------|------| | 0 | 1 | 100 | D1 | | 1 | 1 | 102 | D2 | | 2 | 0 | - | - | | 3 | 1 | 101 | D3 | 1. **排序后**:键值 = `[100, 101, 102, 131071]` 2. **输出**: - 100 → 输出D1(`expected_seq=101`) - 101 → 输出D3(`expected_seq=102`) - 102 → 输出D2(`expected_seq=103`) - 槽位2无效跳过。 --- ### **实现注意事项** 1. **验证方法**: - 测试序列:连续序列、回绕(65535→0)、大间隔丢失包。 - 覆盖率:槽位满/空、连续输出边界、排序延迟。 2. **参数调整**: - 增大\(N\)可容忍更大乱序,但资源消耗显著增加。 - 小规模设计(N=8)适合低功耗场景。 --- ### **完整模块框架** ```verilog module rtp_sorter (...); // 参数与接口声明 reg [1:0] state; reg [15:0] expected_seq; reg [N-1:0] buffer_valid; reg [15:0] buffer_seq [N]; reg [DATA_WIDTH-1:0] buffer_data [N]; // 双调排序网络实例化 // 状态机逻辑(IDLE->SORTING->OUTPUT) // 键值数组生成逻辑 // 输出控制逻辑(扫描 sorted_keys) endmodule ``` --- ### **相关问题** 1. 双调排序网络在Verilog中如何实现? 2. 如何优化排序网络的关键路径延迟? 3. 处理RTP序列号回绕的其他方案? 4. 如何验证乱序包排序硬件的正确性? [^1]: BSGS算法细节参见 [大小步算法 - clover_hxy的博客](https://blog.youkuaiyun.com/clover_hxy/article/details/...)
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值