SoCVista5展开.docx

上传人:b****3 文档编号:4866127 上传时间:2022-12-11 格式:DOCX 页数:28 大小:2.47MB
下载 相关 举报
SoCVista5展开.docx_第1页
第1页 / 共28页
SoCVista5展开.docx_第2页
第2页 / 共28页
SoCVista5展开.docx_第3页
第3页 / 共28页
SoCVista5展开.docx_第4页
第4页 / 共28页
SoCVista5展开.docx_第5页
第5页 / 共28页
点击查看更多>>
下载资源
资源描述

SoCVista5展开.docx

《SoCVista5展开.docx》由会员分享,可在线阅读,更多相关《SoCVista5展开.docx(28页珍藏版)》请在冰豆网上搜索。

SoCVista5展开.docx

SoCVista5展开

双刃剑之一展开

在我们讨论的四种上古神器(包括:

重定时、展开、折叠和脉动)中,展开和折叠是相生相克的。

这两件神器一伸一缩,代表两种相反的做法,但各有其用武之地。

这一帖推出的上古神器是:

双刃剑之一展开。

那么展开是什么意思呢?

看看课本上的定义:

展开用于产生一个新的程序来描述原有程序的多次迭代。

更具体的说,以展开因子J(unfoldingfactorJ)展开一个DSP程序,就会产生一个以原程序连续迭代J次的新程序。

展开也被称为“环路展开”。

试图直接去理解这个定义很困难。

我猜测这是因为看问题的角度不正确而致,往往直到看完了整章才恍然大悟。

我们不急着进入正题,而是先来谈谈DSP程序到底是个什么,可以怎么来看待DSP程序。

个人见解:

DSP程序本质上是对一系列的输入数据进行某种形式的处理(如滤波),然后按一定的次序输出结果数据。

可由软件或者硬件来进行这种处理,但不论何种实现形式,都会涉及到数据的存储和计算。

如果是软件实现,数据是存储在用户定义的变量中;如果是硬件实现,数据存储在延时单元中。

DSP程序具有特定的功能,对于设计者而言,只要能保证DSP的功能不变,可以对DSP的具体实现进行各种调整,以满足实际应用的指标。

综上所述,DSP程序的实现可以看成是对输入数据进行存储和计算的过程,期间还会产生中间数据,也需要临时的存储。

打个比方,制衣厂将面料做成漂亮的衣服,面料就是输入数据,衣服就是输出数据,要将面料做成衣服需要经过多道工序,中间的半成品需要临时仓库来存储;同样是制衣厂,完成相同功能,但是工厂的部门配置、员工组织和仓库等却可以因地制宜。

我们讨论的其他三件神器,都是可以看做一个“工厂”,在保证功能不变的提前下,可以对内部的数据存储和计算流程进行重新的安排,以便达到各个性能指标之间的折中。

下面的讨论分三节:

1.展开的概念和做法,读完这一节,就可以对任何一个没有开关(多路选择器)的DFG进行J(J为任意正整数)阶展开。

2.展开的性质。

使用展开技术,必然要熟知其性质才能得心应手,对于每一个展开的性质会给出详细的证明,并分析其物理意义。

3.展开的应用。

从展开的对象可分:

直路展开、环路展开和开关展开;从展开的用途可分:

缩短采样周期和并行处理。

讲解:

第一节、展开的概念和做法

首先,来看一个软件程序中循环展开的例子。

如图1示,一个数组对应元素相加的例子。

如果采用串行执行,那么需要循环进行N=2000000000000000次加法,可想而知这么多次的计算是需要“一定时间”的。

要是现有一个双核平台可以使用,那么如何充分利用双核资源来提高计算速度呢?

注意到循环中每次迭代都是互不相关的,可以并行计算,有两种计算形式:

1)分块计算,将数据分为连续的两块,第一部分下标从1到N/2,第二部分下标从N/2+1到N,并分别在两个CPU上同时进行计算;2)交叉计算,将奇序号的元素分配到CPU1计算,而偶序号的元素分配到CPU2计算,看起来就是数据交叉的分配到不同CPU。

从这个简单的例子中,可以大致了解到展开的基本思路:

采用多份“相同的”资源来同时进行计算。

我们所要讨论的展开就是基于“交叉数据分配”原则的并行处理方法。

下面来看看一个简单的单环路DFG的例子,如图2。

首先来挖掘一下这个迭代环路所蕴含的并行性,将迭代的过程画出来,如图2左图,A和B的奇数列和偶数列是互不相关的,可以同时进行计算。

对左图的DFG进行二阶展开,这里暂不管是怎么展开的(稍后会详细讨论),先来看看2阶展开前后DFG有什么变化:

1.计算节点的个数变为原来的2倍。

左图的每一个节点对应右图的两个节点,如A{A0,A1},B{B0,B1}。

2.边的数目也是原来的2倍,2条变4条。

3.延时的数目不变,都是2个延时。

4.迭代边界,左图是1u.t.,右图是2u.t.。

5.附加题:

采样边界如何变化?

左图一次迭代计算一个样本,右图可以计算两个,所以左右图的采样边界相等,即1u.t./1=2u.t./2。

----------------------------------------------------------------------------------------------------------------------

细心的同志也许会有疑问:

展开后的DFG,不论是迭代边界还是采样边界,都不比原来的小,而且要命的是展开还消耗了更多的计算节点和边(边对应连线资源),怎么说展开就是并行执行,展开速度就快了呢?

这里要纠正一种错误的“认识”,所谓的迭代边界或者是采样边界,也就是“边界”表征的是迭代DSP程序所能达到的最快的运行速度和最大的吞吐率。

迭代边界决定系统最高运行频率;采样边界决定了系统最大吞吐率(两者是有区别的,这在前面的章节讨论过)。

对于任意的一个迭代DFG,能不能达到这个极限速度呢?

一般是不行的,虽然理论上该DFG可以达到这个极限,但是由于实现结构的各种限制,导致实际的迭代周期和采样周期都“远远”大于其相应的边界值。

摆在眼前的问题是,如何变换系统架构,使其达到极限速度?

展开,展开就可以消除一些制约系统运行速度的限制,使得新的DFG运行速度逼近或者完全达到这个极限——这点真的很诱人,有了展开就能将系统的速度和吞吐率提到理论上的极限。

----------------------------------------------------------------------------------------------------------------------

再来看看课本上的IIR例子(英文版P119/中文版P89),如图3,采用数据交叉分配法对其进行二阶展开,将输入的奇数列和偶数列分配到2份计算硬件上。

回忆第三章并行处理的做法,可以直接用到这个问题中,分别令n=2k和2k+1(k为非负整数),代入原始迭代公式得到奇数列和偶数列两个新的迭代公式,如图4,

虽然两个新的迭代公式并不是完全独立的,比如偶数列y(2k)的计算会用到奇数列y(2k-9)这个输出,反过来奇数列y(2k+1)也会用到偶数列的y(2k-8)这个输出。

虽然如此,并不影响对其进行2阶展开,根据新的迭代公式画出DFG如图5。

值得注意:

在画出新迭代公式的DFG之前,先对其进行一些修改,将变量序号表示为

也就是修改为如下形式

其中J=2。

采用与第三章相同的做法,从迭代公式入手可以导出一个DFG的J阶展开结构,但是这个方法还不够强大,也不够直观。

某些情况下,我们只能对DFG直接进行操作,而没有明显的迭代公式可用或者迭代公式非常复杂。

下面将要介绍的展开做法是直接基于DFG的,只要能将一个DSP抽象成DFG,就可以对其进行任意阶的展开。

首先定义几个数学符号,向下取整\floor(x),向上取整\ceil(x),取余数a%b。

重点来了,两步展开算法。

DFG的J阶展开,也就是采用J倍的复制硬件同时进行计算做法如下

1.节点的J倍复制。

对于原始DFG中任意一个节点U,在新的DFG中对应J个新的节点,分别记为U_0,U_1,….,U_{J-1}。

2.边的连接和边延时的确定。

假设原始DFG存在一条边e如下,边延时(也就是边的权值)为w

经过第一个步骤后,节点U和V均被J倍复制为对应的U_0,U_1,….,U_{J-1}和V_0,V_1,….,V_{J-1},这些新节点之间将建立起对应原始DFG中e边的J条新边,关系如下

也就是说,新的节点U_i和V_{(i+w)%J}之间将形成一条权值为\floor((i+w)/J)的新边,其中i=0,1,…,J-1。

展开的步骤就这两个,非常简单,相信只要细心去操作,总能正确的对一个DFG进行J阶展开。

但是,为什么这样展开就能保证DFG的功能不变呢?

课本(英文版P123/中文版P92)上有一个证明,思路如下:

J阶展开,就是将原始DFG中任一节点U的J次连续迭代“交叉分配”到J个对应的新节点上同时进行一次迭代,如下图示

原始DFG中节点U的J次迭代,只需由新DFG的J个相应节点U_i(i=0,1,…,J-1)各进行一次迭代即可完成,原先串行执行J次的计算现在由J个节点同时执行一次。

对于原始DFG中的任一条边

,存在迭代间关系为,U节点的第i次迭代的输出作为V节点第i+w次迭代的输入,在新的DFG中,必需维持这种数据依赖关系不变方可保证功能的正确。

从上面的讲解可知,U节点的第i次迭代被分配到U_{i%J}节点执行,而V节点的第i+w次迭代被分配到V_{(i+w)%J}节点执行。

假设U_{i%J}第k次迭代输出经过p个延时,作为V_{(i+w)%J}的第k+p次迭代输入就能保证新DFG功能与原始DFG一致,那么肯定有

化简公式,有

当i=0,1,…,J-1时,可化简为

也就是新边的延时p由如上公式确定。

至此,证明完毕。

小结:

展开是构造并行结构的方法之一,其基本思想就是将原始DFG中任一个节点的J次迭代交叉分配到新的相应J个节点上同时执行。

展开算法只包括两个步骤:

1)节点的J倍复制;2)边的连接和边延时的确定。

边的连接和边的延时确定,是基于新旧DFG的节点间数据依赖关系的一致性。

题外话:

这一节的收获,我觉得不是记住展开算法的两个步骤,因为记住这两个步骤是极其容易的;而把这两个步骤背后的意义弄明白才是关键。

在边的连接和边的延时确定的证明中,我们是以一种调度的观点来看待DSP系统的,这种数据调度的观点在以后的章节中还会频繁使用,故而请大家仔细去体会。

前一节介绍了展开的具体操作方法,只有两个步骤,非常的简洁,如下图示。

使用展开算法很容易将一个DFG进行任意阶展开。

通过前一节的例子,我们也大致了解到展开前后DFG的区别,对于J阶展开:

1.展开后DFG节点数量为展开前DFG节点数量的J倍。

2.展开后DFG边数量为展开前DFG边数量的J倍。

3.展开前后DFG的总延时数目不变。

4.展开后DFG迭代边界为展开前DFG迭代边界的J倍,但采样边界不变(保持相等)。

以上这些认识都是通过观察得出,没有严格证明。

这一节,将严格的把展开的最常用的性质列出并证明,同时也尽可能的去思索这些性质的物理意义,看看在硬件上到底对应怎么一回事。

讲解:

第二节、展开的性质

性质一展开算法保持DFG中的延时数目不变。

DFG的延时数目,对应系统所使用的寄存器数量。

展开算法的目的是构造多路并行的DFG,以便提高系统的运行吞吐率。

直观的想,J路并行的系统,所消耗的资源(实现面积)应该是原系统的J倍,但其实不尽然,虽然节点和边的数量变为原来J倍,但延时数目,也就是寄存器数目却保持不变。

回顾展开算法的第二个步骤,原始DFG中U到V延时为w的一条边展开后得到J条对应边,且新边的延时为\floor((i+w)/J),其中i=0,1,…,J-1。

如此说来,只要新形成的J条边总延时等于原始边延时w,就说明展开不改变DFG的延时数目。

对应延时为w的边的J条新边延时总和为

根据

的大小,化简去掉\floor()符号即可得证。

证明:

其中

显然

将w公式带入

以下需证

1.当m=0时,显然成立。

2.当1<=m<=J-1时,

因为\floor(m/J)=0,又因为J<=m+J-1<=2J,所以\floor((m+J-1)/J)=1。

也就是说式子

前一部分项为0,后一部分项为1。

那么那些项为1呢?

如下示

假设从第k+1项开始为1,则有m+k+1=J,也就是k+1=J-m。

因为从k+1到J-1项均为1,所以上式共有(J-1)-(k+1)+1=(J-1)-(J-m)+1=J-1-J+m+1=m,即

综上所述,有

成立。

性质二原始DFG中延时为w_l的环路l的J阶展开,得到展开后的DFG中的gcd(w_l,J)个环路,gcd(w_l,J)个环路中的每个环路包含了w_l/gcd(w_l,J)个延时以及环路l中出现的每个节点的J/gcd(w_l,J)个拷贝。

这个性质的证明比较饶人,有兴趣的同志可以阅读书本上的证明,这里不去讲解。

直观的理解,性质二告诉我们三个内容:

1.一个环路展开会得到若干个环路,也就是说环路展开后得到的仍然是环路。

2.原始环路的延时会平分到新的环路中。

3.对于任一个原始节点U的新复制得到的J个节点也会平分到新的环路中。

对一个环路进行展开,可能得到一个新的环路,也可能得到多个(>1)新的环路,新环路的数量跟实际的待展开环路的总延时以及展开阶数有关。

规定gcd(w_l,J)表示w_l和J的最大公约数。

性质二定量地指出,延时为w_l的环路,J阶展开会得到gcd(w_l,J)个新的环路,而且每个新环路分配所得的延时数目为w_l/gcd(w_l,J),对于原始任一个节点U的J份复制也会平分到gcd(w_l,J)个环路中,也就是每个环路获得U的J/gcd(w_l,J)个拷贝。

性质三展开一个迭代边界为T_inf的DFG,会得到一个迭代边界为J*T_inf的J阶展开DFG。

证明:

假设原始DFG的迭代边界为

根据性质二,J阶展开所得新环路的计算时间为

,延时数目为

,则新环路迭代边界为

注意,虽然展开后系统的迭代边界扩大J倍,但是由于新系统可以同时处理J个样本点,所以采样边界为J*T_inf/J=T_inf,也就是说展开不改变系统的极限吞吐率。

展开可能会导致更长的关键路径。

容易犯的一个错误就是认为J阶展开就是原始系统的J倍加速,其实不尽然,而且也不见得展开阶数越大系统就越快。

如果用系统实际吞吐率来衡量系统速度,那么展开阶数J与吞吐率一般不成正比。

使用展开时,必须综合考虑展开所消耗的资源数量和所带来的系统加速,选择恰当的折中方案,此外不论采用多少阶的展开,系统的速度都不可能突破采样边界的限制。

为了弄清楚展开对关键路径长度的影响,看如下例子,

假设节点A、B和C的计算时间均为1u.t.,展开前DFG(左图)关键路径为C-A,长度为2u.t.,3阶展开后DFG(右图)关键路径为C0-A0-B1-C2-A2,长度为5u.t.。

很多场合关心的问题是:

如何确定展开阶数才不会导致关键路径长度的增加呢?

这里先给出答案,当w>=J,也就是展开阶数J不大于路径延时w时,将不会导致更长的关键路径出现。

留个思考题:

为什么w>=J就能保证不会出现更长的关键路径,当w

[提示,仔细体会展开步骤二中,旧路径延时w是如何在其对应的J条新边中进行分配的]

在重定时一章讲过,通过重定时可以改变系统的关键路径长度。

由于展开可能导致更长的关键路径,结合重定时就有可能改善这一点。

以下的性质和推论将有助于进一步理解展开对关键路径的作用以及展开和重定时之间的联系。

性质四考虑原始DFG中延时为w的路径,当w

证明:

1.当w=0时,显然所有复制得到的J个路径延时均为0,性质四成立。

2.当J>w>0时,\floor((w+J-1)/J)=1。

J个路径的延时如下示,其中前一部分延时为0,后一部分延时为1,假设从\floor((w+k+1)/J)开始及之后的项为1,那么共有(J-1)-(k+1)+1项为1,因为w+k+1=J,即k+1=J-w,带入上式可知共有(J-1)-(J-w)+1=w项为1,同时也说明有J-w项为0。

综上所述,性质四成立。

推论一原始DFG中包含J个或更多延时的任何路径都能生成J个路径,其每个路径具有1个或更多延时。

因此,原始DFG中具有J个或更多延时的路径不能生成一条在J阶展开DFG中的关键路径。

根据性质四,容易理解推论一。

值得注意,关键路径定义为非零延时的最长路径,如果一条边具有1个以上延时就不能是关键路径中的边,性质四给出了原始DFG中延时为w的边展开后得到新边中,延时为0的新边数目为J-w条,如果不想这些边成为关键路径中的边,就必须限制J-w<=0,也就是J<=w。

性质五任何通过重定时J阶展开DFG得到的G_J所能得到的可行时钟周期,都可以通过直接对原始DFG的G重定时和以J为展开因子的展开来得到。

性质五指明了,先展开再重定时和先重定时再展开存在一一对应关系,如果使用先展开后重定时得到一个新的DFG,那么这个DFG肯定可以通过某种重定时再展开来得到。

具体证明就省略了,有兴趣的同志可以看书本。

通过前两节的学习,可以掌握如何对一个没有开关的DFG进行J阶展开。

因为前面讲到展开的阶数和系统吞吐率往往不是正比关系,那么什么时候需要对DFG进行展开,而且展开阶数应该是多少才合理呢?

这些都是实际应用前必须考虑的问题。

这一节的内容不像前两节那么“死板”,蕴含很多乐趣。

下面将通过一些例子来讲解如何使用展开,希望大家能够融会贯通,举一而反三。

从例子中也能进一步理解迭代边界和采样边界等概念。

讲解:

第三节、展开的应用

这节主要练习展开的两个典型应用:

采样周期的缩短和并行处理。

值得注意的是,展开不等同于并行处理,它只是构造并行电路的其中一种方法。

从迭代边界的理论中可知,一个DFG的实际运行速度往往没有达到其理论上可以到达的极限速度,系统的极限速度用迭代边界(理论最小时钟周期)或者采样边界(理论最大吞吐率)来衡量。

一个给定的DFG,不论怎么改造,速度都不可能突破边界速度,所以我们不是无止境的追求提高速度,而是考虑“怎么改造DFG,使其实际运行速度接近或等于边界速度”。

还记得本章一开始说过,通过展开,可以将一个DFG的实际运行速度提高到理论的极限速度,也就是以迭代边界作为实际运行周期,从而达到最大吞吐率。

那为什么说展开可以实现这一点呢?

原来,原始DFG不能以边界速度运行,往往是因为存在以下2个方面的限制因素(当然了,也许还存在其他限制因素,这里只讨论展开可以解决的限制因素)。

如图12所示,制约系统不能以边界速度运行的2个因素是:

1.DFG中存在某些节点(一个以上)计算时间大于迭代边界T_inf。

2.迭代边界T_inf不是整数时间单位(本书中,假设任意节点的计算时间都是整数个单位时间)。

第一个限制因素容易理解,如果DFG中,存在一个节点的计算时间大于T_inf,那么该DFG不可能以迭代周期来运行,否则这个节点可能不能在一个周期内完成计算任务。

由展开性质三[展开一个迭代边界为T_inf的DFG,会得到一个迭代边界为J*T_inf的J阶展开DFG],通过展开增大新DFG的迭代边界,就可以去除这个限制。

如果新DFG能以迭代边界为周期运行,虽然新DFG迭代边界比原始DFG迭代边界大,但是新DFG可以同时处理多个(J个)样本,采样边界不变,也就是说新DFG吞吐率达到理论最大,这就是我们想要的。

补充一句,新旧DFG的采样边界是相等的,采样边界决定系统极限吞吐率,所以不论是新DFG还是旧DFG,只要以迭代边界为实际运行周期,不管迭代边界大小如何,都能达到极限吞吐率。

特定功能的DFG,不论是否展开,其采样边界都是一个定值,也就是说理论最大吞吐率是一个定值。

怎么知道一个DFG是否已经达到极限吞吐率呢?

只需看系统实际运行周期是否等于迭代周期。

虽然J阶展开后DFG比原始DFG迭代边界增大,但只要新DFG是以其迭代边界(J*T_inf)为周期运行,就可以保证系统是以理论最大吞吐率在工作。

思考题:

展开会消耗额外的资源,针对第一个限制因素,展开前后,以什么为代价换取了什么好处?

第二个限制因素中,首先假设DFG中任意节点的计算时间都是整数个单位时间。

如果迭代边界不是整数,也就是说以带小数的单位时间为周期驱动系统,这个小数部分的单位时间,对任何一个节点来说都是不足以完成任何任务。

因此,实际的运行时钟为整数个单位时间才“可能”是最合理的。

先看看第一种情况的例子,如图13所示,一个IIR滤波器,抽象出DFG如图13右图示,节点旁边小挂号内数字为节点计算时间。

如图14所示,原始DFG存在两个环路,环路边界分别为2u.t.和3u.t.,最糟糕的环路边界也就是迭代边界为3u.t.,而DFG中存在节点S和节点T,计算时间为4u.t.,所以不论如何重定时,该DFG都不能以小于4u.t.的时间为周期来运行。

对图14的DFG进行重定时,可以将关键路径长度缩小到4u.t.,即使如此,该DFG也没到达理论最高吞吐率(3u.t.处理一个样本)。

怎么办呢?

使用展开就能消除这个限制。

比如对原始DFG进行2阶展开,则所得新DFG迭代边界变为2*T_inf=2*3=6u.t.>4u.t.。

如图15所示,2阶展开后的DFG,最糟糕的环路用红色路线标识,迭代边界为6u.t.。

在新DFG,所有节点的计算时间都小于迭代边界,也就是第一个限制因素已经解除。

虽然新DFG迭代边界增大到6u.t.,但是由于一个周期内可以处理两个样本,所以等价采样边界为3u.t.,也就是每3u.t.处理一个样本。

对于原始DFG,由于最快只能以4u.t.为运行周期,所以等价于每4u.t.处理一个样本,而在新DFG中却是每3u.t.处理一个样本,新DFG吞吐率要大于原始DFG吞吐率。

正如一开始所说,展开可以解除制约系统达到最大吞吐率的因素。

这个例子中只存在第一种限制因素,使用展开解除这个因素,就能使新DFG以迭代边界为实际运行周期运行,所以达到理论上的最大吞吐率。

提醒一下爱浪费的同志,请以尽可能小的展开阶数实现解除限制因素的目标。

如果J阶展开就能解除制约,那么就没有必要使用大于J阶的展开,要知道展开阶数越高消耗的资源就越多,但是系统吞吐率是不可能再提高的,这就是“边界”意义。

因此,为了解除第一个限制因素,只需进行\ceil(t_u/T_inf)阶展开即可,其中t_u为最耗时的节点计算时间,T_inf为原始DFG的迭代边界,\ceil()为向上取整。

接着是第二种限制因素:

迭代边界不是整数个单位时间。

如图16所示环路的迭代边界为4/3=1.33333。

本着节约的美德,要以最小的展开阶数展开该环路,以解除第二个限制因素,显然,3阶展开即可。

[思考题:

为什么说3阶展开是解除第二个限制因素的最小展开阶数?

提示,展开性质三]

3阶展开后所得DFG如图17所示。

这个例子涉及到环路的展开,还记得展开性质二不?

[性质二原始DFG中延时为w_l的环路l的J阶展开,得到展开后的DFG中的gcd(w_l,J)个环路,gcd(w_l,J)个环路中的每个环路包含了w_l/gcd(w_l,J)个延时以及环路l中出现的每个节点的J/gcd(w_l,J)个拷贝。

]下面,先来验证性质二,原始环路延时总数为3,展开阶数为3,则应该得到gcd(w_l,J)=gcd(3,3)=3个新的环路,新环路的每个环路均分得延时数目为w_l/gcd(w_l,J)=3/3=1个,每个环路包含任一原始节点的J/gcd(w_l,J)=3/3=1分拷贝。

图17的结果完全符合性质二的结论。

新的三个环路迭代边界均为3*T_inf=3*4/3=4u.t.。

实际运行周期也可取4u.t.,所以新DFG的吞吐率可以到达理论最大值,等价于每4/3u.t.处理一个样本。

有时,DFG中会同时出现上面所提的两种限制因素。

同样的道理,本着节约的美德,应该以最小的展开阶数展开,以同时解除这两个限制因素。

思考题:

如何计算解除以上所说三种限制因素的最小展开阶数?

 

下面再来讨论展开与并行处理的关系。

前面简单说到,展开是构造并行处理电路的方法之一,其实不难理解,假设原始DFG一个周期只处理一个样本,则J阶展开的DFG一个周期可以并行处理J个样本。

这里值得注意的是,此周期非彼周期,展开后的关键路径往往比原关键路径长,以至于展开后DFG的运行周期要比原始DFG运行周期大,虽然如此,展开还是有可能进一

展开阅读全文
相关资源
猜你喜欢
相关搜索

当前位置:首页 > 小学教育 > 语文

copyright@ 2008-2022 冰豆网网站版权所有

经营许可证编号:鄂ICP备2022015515号-1