数码之家

 找回密码
 立即注册

QQ登录

只需一步,快速开始

微信登录

微信扫一扫,快速登录

搜索
查看: 1983|回复: 41

换药不换汤,基于STM32平台软件的DDS演奏程序,音质轻松秒杀传统蜂鸣器

[复制链接]
发表于 2020-3-23 21:32:47 | 显示全部楼层 |阅读模式

马上注册,认识更多玩家好友,查阅更多资源,享有更多功能

您需要 登录 才可以下载或查看,没有帐号?立即注册 微信登录

x
之前我发过了《基于软件模拟DDS的蜂鸣器驱动程序》,不过这几天又做了一些测试优化,把能工作的稳定版本释放吧!并详细介绍原理。Here we go:

•使用STM32演奏音乐——
•基于DDS原理的 蜂鸣器(×) 扬声器(√) 驱动程序


STM32 Cortex

STM32 Cortex


效果简单展示:https://tieba.baidu.com/p/6577638558
受制于录制设备,听起来的饱和失真是不存在的。

打赏

参与人数 6M币 +122 收起 理由
869294732 + 12 謝謝分享
fm007 + 20 優秀文章
2545889167 + 20
5bin + 30 好贴666
trg13 + 20 不知道51单片机可以实现这个功能不?简单的.
玛德陛下 + 20 謝謝分享

查看全部打赏

 楼主| 发表于 2020-3-23 21:33:18 | 显示全部楼层
一、实验目的

•初步掌握STM32的GPIO,IIS,SysTick,DAC等外设使用;
•熟练掌握Cortex-M3的指令集的应用;
•熟练Keil开发环境;
•熟练使用ASM对STM32进行简单开发;
•就是好玩啊,能放音乐音质还不错。


点评

我以为很简单的pwm输出就可以了 原来这么复杂的  详情 回复 发表于 7 天前

打赏

参与人数 1M币 +20 收起 理由
trg13 + 20 好吧 需要的这么高级的频率啊,我以为很简.

查看全部打赏

回复 支持 反对

使用道具 举报

 楼主| 发表于 2020-3-23 21:34:50 | 显示全部楼层
二、实验原理

1、DDS简介:
DDS,即Directdigital synthesis,直接数字合成,是一种非常灵活的模拟信号产生方式,能够产生多种波形如正弦波,三角波,方波等,还可以根据用户的需要产生任意的波形。不仅如此,由于主要模块是数字系统,因而具有分辨率高,波形频率变化范围非常广泛而且连续;频率转化时间短,可以快速在多个频率之间切换等特点。并且更改频率时相位依然保持连续。DDS在仪器仪表及通信系统中具有广泛应用。

基本的DDS架构如下:相位累加器(NCO),预先固化周期波形的N个采样值的存储器(ROM),模数转换器(DAC),低通滤波器(LPF)组成。
image.png

整个系统由一个参考时钟fc驱动,在每个时钟周期,相位累加器会按一定步进M自增,并产生相应的地址输出到波表存储器中,存储器输出相应点的采样值送到DAC,并转换成相应的模拟信号,经过低通滤波器平滑后输出。其中步进M叫做相位控制字,fc不变的情况下,k越大输出频率fo越大,二者呈线性关系。对于一个固定的频率ω=d θ/dt,相位增长速率恒定,所以相位累加器随时间均匀增加就实现了这个相位变化关系。


回复 支持 反对

使用道具 举报

 楼主| 发表于 2020-3-23 21:36:45 | 显示全部楼层
相位累加器行为可由数字轮图示,设相位累加器的长度为N,则其计数范围是0到2^N-1,相当于角度0-360度的变化,对应的ROM表就应该有2^N个采样值。当相位累加器从0不断增加直到溢出截断,对应数字轮旋转了一圈就遍历了一遍波表,输出一个完整的周期。设M=1,每个时钟相位累加器自增1,则累加器经历了2^N个状态,输出频率即为fo=fc/2^N,M为任意值时fo=M*fc/2^N。理论输出频率的受到Nyquist限制,为fc/2,实际情况下要比这个结果低些。为了改善输出波形的质量和使得输出滤波器易于设计。

image.png
回复 支持 反对

使用道具 举报

 楼主| 发表于 2020-3-23 21:39:16 | 显示全部楼层
2、多音色合成

傅里叶同学告诉我们,任何周期函数,都可以看作是不同频率,不同振幅,不同相位正弦波的叠加。各种乐器发出的每个音调,无论时域下波形有多复杂,也依然可以进行分解成为若干个简单正弦的叠加。反过来,把若干个特定的正弦波合理的组合起来就可以拟合不同的乐器效果。
设有周期信号f(t),其傅里叶级数为:
image.png

image.png

由于三角函数系的正交性:

image.png

可知上式中的sin nωt和cos nωt相当于“筛选器”,只筛选出f(t)中于自己相同的成分,与自己不同的成分则为零被滤除。2/T为幅度修正,因为∫[-π->π]sin nx*sin nx dx =∫[-π->π]cos nx*cos nx dx =T/2。为了得到f(t)中sin nωt和cos nωt成分的幅度,所以要在积分后乘以2/T。
总而言之,就是使用适当的方法,产生一系列的Asin (nωt+θ)并叠加就可以得到不同的输出波形因而有不同的声效。
回复 支持 反对

使用道具 举报

 楼主| 发表于 2020-3-23 21:42:05 | 显示全部楼层
3、实现DDS架构及其多谐波输出

首先要明确STM32F103RC系列的硬件资源(实际核心板上的型号是STM32F103ZG,但是后续可以直接烧录到RC系列不用任何修改)。模拟DDS需要用到的硬件有:SysTick,用作节拍发生,在其中断服务程序中可以不断地累加地址计数器并查找波表数据并输出;DAC将数字的波表数据转化为对应模拟信号。此外,还需要占用若干RAM单元用作变量存储,以及程序FLASH定义波表数据。   

波表数据定义如下图所示,为了节约空间,根据sinx的性质,只定义了0-90度的数据,共计25k的采样点,数据类型为int16型,共占用50Kbytes空间。通过简单的计算便可通过1/4波表得到0-360度的sinx值。
image.png


波表数据将通过DAC输出并转化为模拟量,本程序定义了两种输出方式分别用片上DAC或者通过IIS接口驱动外部DAC进行输出。对于片上输出方式,先初始化DAC,随后将调制后16-bit波表数据通过两个12-bitDAC通道输出,在外部用16:1的权电阻合并后理论的到16bit输出精度,其中PA4输出低4位,PA5输出高12位,在使用DAC之前,应该做到:
i.在RCC_APB1ENR中开启DAC时钟
ii.将PA4,5设置为模拟输入;
iii.将DAC的两个通道设置为软件触发,并使能。
image.png

相关函数:RCCInitialize (位于Initialize.s),DACInitialize (位于DAC.s), DACOutput (位于DAC.s)。

SysTick作为模拟DDS架构的“心脏”,周期性地调用频率产生服务。中断设置为2000个主时钟发生一次,在11.2896MHz石英晶体倍频10倍下,中断频率是56.448KHz。在该中断中完成正弦表的相位累加器的自增以及查表,调制输出等一些列任务。所以模拟的DDS参考时钟fc等于中断频率,查找表等效长度为25k*4,则步进频率fstep=0.56448Hz,对于音调发生精度已经足够。系统启动后,先调用SysTickInitialize初始化SysTick,用于设置重装值。向STK_LOAD中写入0xF9,每计数250次产生一个中断,当SysTick时钟是系统时钟的1/8时正好每隔2000个系统时钟中断一次。 SysTick可由函数SysTickCtrl控制,当传入参数非0时开启SysTick及其中断;当传入参数为0是关闭SysTick及其中断,并清除当前计数值寄存器STK_VAL.
image.png

回复 支持 反对

使用道具 举报

 楼主| 发表于 2020-3-23 21:46:43 | 显示全部楼层
运行在SysTick ISR中的软件DDS结构图示:
绘图1.png

打赏

参与人数 1M币 +20 收起 理由
沙漠臭屁虫 + 20 優秀文章

查看全部打赏

回复 支持 反对

使用道具 举报

 楼主| 发表于 2020-3-23 21:50:00 | 显示全部楼层
多谐波相位发生部分:
该部分用于实时的产生基波以及最高到8次谐波的相位。本部分的关键变量有两个,在程序中定义如下图所示:

image.png

其中,SinWavLutCt相当于基波的相位累加器,而SinWavStepSize决定了每个中断后基波相位累加器的步进量,相当于频率控制字。改变步进幅度即可改变基波频率,进而各次谐波频率相应改变。根据前面的推导可知基波频率步进为0.56448Hz。
各次谐波的相位累加器的实时结果将与基波相位累加器的实时结果呈现倍数关系,因此可直接将基波的相位累加器与自身进行简单加法运算即可得到各次谐波相位累加器的实时结果,即节省了RAM,也加速了处理。
image.png

回复 支持 反对

使用道具 举报

发表于 2020-3-23 21:51:33 | 显示全部楼层
虽然看不懂,但是好NB的样子
楼主NB!
回复 支持 反对

使用道具 举报

 楼主| 发表于 2020-3-23 21:52:20 | 显示全部楼层
各次谐波的实时相位偏移一旦求出,就可以与预设的初始相位偏移值累加,作为最终的相位偏移结果对正弦表进行寻址。其中基波的相位偏移总是0,各次谐波与基波的相位偏移由音色表定义。下图展示了该程序中所有音色表的定义:

image.png

关于音色表的详细定义将在后面阐述。


ii.谐波分量寻址及加权合并

上述的各谐波实时相位偏移产生后就进行采样值寻址,得到固化在MCU内部FLASH的正弦波表的基地址后,用相位偏移作为偏移地址,从波表中加载int16的采样值数据。
各谐波的实时采样值得到后乘以音色表中各自对应的权重,之后再累加。音色表中的权重值总和为256,所以最后数值再除以256得到标准化的16-bit包含各次谐波的采样数据。

image.png

上述模块的对应函数是:SineWaveGenerate,定义在ToneGeneration.s中,该函数的关键段分析如下:

image.png

上图是抗混叠限制代码段,通过计算对于特定基波的最高解析谐波数来控制后续正弦表寻址阶段,当某一谐波频率高于极限就放弃它以及比它更高次谐波的寻址,防止输出发生混叠。
对于以56.448KHz中断频率不断输出采样值的过程来说,其采样率就是56.448KHz。根据采样定理,输出频率低于采样频率的1/2以下才不会出现混叠,在本例中极限输出频率被限定在21KHz。

根据前面所述,产生21KHz的极限频率所需的频率控制字值为?42000?,该值被传入的频率控制字除得的结果就代表能进行最高几次的谐波解析不越界。之后的两条指令作为饱和运算,最高只解析8次谐波所以除法结果限制到8以内。
回复 支持 反对

使用道具 举报

 楼主| 发表于 2020-3-23 21:55:36 | 显示全部楼层
各谐波采样值的获取代码段如图所示:

image.png

其中第一条指令用于加载该次谐波的初始相移,并将指针移动到下一次谐波的初始相移数据位置。第二到第四条指令实时的完成该次谐波的相位偏移值产生,并且将相位偏移值限制在合理范围。由前面可知,定义在FLASH中的1/4波表长度是25Ksps,等效长度为100Ksps。等效于在FLASH中固化了100K个采样值数据,所以最高的输入相位值偏移不应该超过99999.当计算后的相位偏移值大于99999时,减去100000,相当于减掉2pi的弧度,由周数函数性质sin(x)=sin(x±2pi)可知重映射后能得到正确结果并使得相位偏移值在可接受范围内。
随后两条指令将初始的相移加到上一步产生的相位偏移值中,并且将相位偏移值限制在合理范围。由于相位有可能是负的,所以要映射到正的区间里面去。第七第八条指令再次进行限幅处理,若有正向的相位偏移仍有可能在某些时候导致最终的相位偏移值超过极限范围。最后将合法的相位偏移值保留在R0中并调用SineValueExtend获得对应的正弦采样值。

正弦采样值获取的代码段如图所示:

image.png

由于为了空间节省,只定义了1/4个周期的正弦表数据,所以对于剩下的3/4个周期必须作出适当的处理方可获得,这些处理非常简单取得了时间和空间的最优化。

根据正弦函数性质,有:
sin(x+pi)=sin(x)*cos(pi)+cos(x)*sin(pi)=-sin(x);  完成0-180度到180-360度的映射。
sin(x+pi/2)=sin(x)*cos(pi/2)+cos(x)*sin(pi/2)=cos(x)=sin(pi/2)*cos(x)-cos(pi/2)*sin(x)=sin(pi/2-x);完成0-90度到90-180度的映射。

回复 支持 反对

使用道具 举报

 楼主| 发表于 2020-3-23 21:57:08 | 显示全部楼层
iii.幅度调制

得到了混有各次谐波的实时采样值后,将进行幅度调制,从而产生响度不同且具有不同声效的音调。该部分的架构如图所示:

image.png

一个随着时间流逝不断自增的地址计数器对幅度表进行寻址,并且不断输出幅度数据进行幅度调制,与之前的正弦波发生部分类似。但是幅度表的步进是固定的,步进量为1/12,或者说每隔12次中断后才自增一次,调到下一个数据进行寻址。一个幅度表包含8192个uint16数据,但只有13位是有效的。不同的幅度表可以对恒定的正弦波谐波组合进行不同的时变调制,从而输出不同的声效。
本历程中定义了4个幅度表,其中0号和1号表主要负责长音调,其幅度衰减较慢,2号和3号负责短音调,其幅度衰减较快。1号和3号表不带颤音效果,0号和2号表有颤音效果。
幅度表输出结果还受一个固定的幅度乘子AmpValmultipler的调制,使得单独的每个音调响度都可以自由改变。该乘子是3位的变量,接受的范围是0-7。0用于曲谱的休止符。一般正常音调的数值是4,根据情感变化这个数值可高可低,带来响度的变化。

经过幅度乘子调制的实时幅度值是16-bit数据。历程中一个幅度表的定义如图所示:

image.png

回复 支持 反对

使用道具 举报

 楼主| 发表于 2020-3-23 21:58:53 | 显示全部楼层
上述模块的对应函数是:AmpValGenerate,定义在ToneGeneration.s中,该函数的关键段分析如下:

image.png

前3条指令加载了3个关键数据:幅度表基地址,幅度表地址偏移计数器以及地址计数器自增延时计数器值。第4条指令对地址计数器自增延时计数器值进行更新并检查,如果该值变为0就在接下来的语句中重设该计数器值到12,并且对幅度表地址计数器偏移加2,指向下一个幅度数据。
第5条指令从幅度表中获取幅度数据。第6条指令对更新幅度表地址计数器或者不更新做判断并且跳转到相应分支。剩下的指令主要是加载幅度乘子并且使之与原始幅度数据相乘并输出最终结果。幅度表最大偏移不超过8191字节,所以当地址偏移达到8190字节后地址偏移计数器不再自增。

iv.音调的平滑过渡及连音处理

得到了实时的幅度值与实时谐波采样值后,先将实时幅度值经行平滑调制后再用调制的结果调制实时谐波采样值,该部分的架构如图所示:

image.png

平滑部分的查表行为与之前相似,不再赘述。值得注意的是,用于平滑处理的衰减表只有3000个16-bit采样数据,但是在当前音调未结束时,该部分只会输出恒定的0x0800采样结果,平滑效果不启动。只有在当前音调即将结束时,只剩下3000个中断时间,该部分才正式开始进行查表工作,把衰减表里逐渐衰减的采样值逐个输出。
音调平滑过度的意义如下所述:对于一个标准音调,其包络曲线如图所示,在两个相邻包络之间若无平滑过渡就会带来幅度突变带来切换噪声,如图中绿圈部分所示。若在音调即将结束时强制衰减其幅度至零则幅度突变不复存在,切换噪声被消除,如红色虚线所示。

连音效果需要禁用平滑效果并且要求相邻音调的软切换,在后面将进一步阐述。

image.png




回复 支持 反对

使用道具 举报

 楼主| 发表于 2020-3-23 22:00:35 | 显示全部楼层
上述模块的对应函数是:FadeOut,定义在ToneGeneration.s中,该函数的关键段分析如下:

其中的第2第3条指令用于判断当前音调是否即将结束,并产生衰减表的偏移地址。音调发生服务程序中使用了一个持续时间计数器ToneDurCt来标记当前音调持续的采样时间数,每过一个采样时间该数值减1。若当前音调只剩下3000个采样时间的长度后,第五条指令不再起作用,若平滑过渡没有被禁止,第6条指令检测淡出效果禁止跳转也不起作用,返回值由原来恒定的0x0800替换成衰减表中不断递减的数值。

image.png


v.采样数据的标准化及输出

最终得到的采样数据为最大拥有16+16+11=43bit有效数位的补码数据,而期望的数据是16bit的无符号数据通过2路12-bit DAC输出,所以要进行简单处理再传输。该部分的架构如图所示:采样数据经过算术右移27位得到16-bit的补码,加上0x8000后转换成16-bit无符号整数。随后一路右移丢弃低4位后输出到DAC通道2中,另一路将高12位掩盖只留下低4位有效数据输出到DAC通道1中。

image.png

回复 支持 反对

使用道具 举报

 楼主| 发表于 2020-3-23 22:01:15 | 显示全部楼层
上述模块的对应函数是:TonesGenerate,定义在ToneGeneration.s中,该函数的关键段分析如下:

程序段中第一条指令将当前音调持续的采样时间数传递给R0,并在第2条指令调用FadeOut得到衰减值并在下一条指令中保存到R5防止被破坏。第4条指令调用SineWaveGenerate获得谐波采样值,并在第5条指令中保持到R4.第6条指令调用AmpValGenerate得到幅度值。随后两条指令将三个数据相乘调制,并通过合适的方法输出。根据宏定义选择由片内DAC输出或者外部DAC输出。

image.png


回复 支持 反对

使用道具 举报

 楼主| 发表于 2020-3-23 22:03:05 | 显示全部楼层
4、查找表的定义及其生成方式

i.正弦表和幅度表
两个幅度表都是相应函数的等间隔采样后经过标准化直接生成的。其中正弦表是对sin(x)在区间[0,pi/2]做等间隔采样并标准化,而幅度表是对A*exp(-B*t)*[(1-C)+C*exp(-D*t)*sin(E*t)]进行等间隔采样并标准化,表达式中的A,B,C,D,E是可设置的参数,灵活地设置参数可生成不同的幅度表使得音调出现不同的声效,此外,在幅度表的起始阶段还受到ln(1+F*t)等间隔采样值的调制。上述功能对应生成工具的1,2项,如下图所示。

image.png

ii.歌曲音调查找表
该功能对应生成工具的第3项,将易于理解的音符记录形式转换为在程序中直接使用的十六进制定义。一条完整的记录如下:[PitchName] + [Op] +[Off] + [AmpIndex] + [AmpMul] + [ToneDur] + [SwTyp],其中[PitchName] 是基本唱名,接受1-7的输入范围; [Op] 是偏移方向,+号代表向更高的八度进行偏移,而-号代表向更低的八度偏移;[Off]定义了偏移量,单位是1个八度; [AmpIndex] 代表振幅表编号,决定了每个音调的声效。历程中定义了4个幅度表,其中0号和1号表主要负责长音调,其幅度衰减较慢,2号和3号负责短音调,其幅度衰减较快。1号和3号表不带颤音效果,0号和2号表有颤音效果; [AmpMul] 是响度设置,接受0-7的输入范围,其中0用作休止符使得该音调完全静音; [ToneDur] 表示音调长度倍数,可接受的输入是1-9,根据乐谱的特性,若最短音符是1/4音符,可以在音调查找表参数设置中将音调长度步进设置为1/4音符的长度,当遇到一个1/4音符时,这项参数是1,1/2音符是2,全音符是4,依此类推; [SwTyp]决定了不同音调之间的切换方式,接受的输入范围是0-3,对应2个bit的数据,其中最低的bit决定音调是软切换还是硬切换,置位表示硬切换,最高bit决定淡出效果是否启用,用于消除切换噪声,置位表示禁止。


打赏

参与人数 1M币 +20 收起 理由
trg13 + 20 为什么不用C语言写呢 汇编好复杂的说.

查看全部打赏

回复 支持 反对

使用道具 举报

 楼主| 发表于 2020-3-23 22:05:12 | 显示全部楼层
生成的歌曲音调查找表是可直接复制到程序中的16进制数定义,如图所示:

image.png

为了方便观察,只录入了2个音符,原始乐谱中的两个音符的记录如图:

P19.png

转换后的第一个音符的16进制数据是:0x0403030B,0x0F0032B6,录入的记录字符是6-03413。转换后的一个音符对应2个32-bit的字数据,其排列方式小端模式。字的每个位域定义如下所阐述:

image.png

如上图所示,64-bit的音调记录数据包含6个长度不一的位域,其中,0-15位是正弦表的步进值,决定了该音调的频率;16-23位是幅度表编号,决定了该音调不同的声效;24-31位是幅度乘子,决定了该音调的响度;32-55位是音调持续时间,用总采样数表示;第58-63位是音色表编号,决定该音调的音色。
56-57位重点说明如下:56位是切换方式,该位置位则使能硬切换,由上一个音调切换到当前音调时,所有的音调参数都会被更新到当前音调控制字的预设参数。若该位清零则使能软切换,切换到当前音调时,只有正弦波表地址偏移步进值和音符持续时间会被更新到当前音调控制字的预设参数。这个机制使得两个音调听起来连贯,是连音效果必须设置的关键位;57位是音符间淡出效果禁用使能位,若该位置位则当前音调的淡出效果被禁止,反之。该位也是连音处理的必备关键位。
对于普通的单个音符,输入的音符记录最后一个字符(切换方式)必须是1,转换成音符控制字后[57,56]=01,即当前音调硬切换且使能淡出效果;对于连音,第一个音符对应的音符记录最后一个字符(切换方式)必须是3,转换成音调控制字后[57,56]=11,即当前音调硬切换且除能淡出效果,使得当前音调与下一个音调能够连贯。处于中间的所有音符对应的音符记录最后一个字符(切换方式)必须是2,即当前音调软切换且除能淡出效果,使得当前音调能与前后音调连贯。最后一个音符对应的音符记录最后一个字符(切换方式)必须是0,转换成音调控制字后[57,56]=00,即当前音调软切换且使能淡出效果,使得当前音调能与上一个音调连贯且平滑结束当前的连音组合。除此之外的其它任何匹配方式将导致切换噪声!

回复 支持 反对

使用道具 举报

 楼主| 发表于 2020-3-23 22:06:26 | 显示全部楼层
iii.歌曲音色查找表

该功能对应生成工具的第4项,根据提示输入基波及其各次谐波的相对强度及其初始相位偏移即可生成可以直接复制到程序中的音色表定义,如图所示:

image.png

完成录入按Enter后立刻生成音色表定义,如右下图所示。其中每个字的定义详述如下:其中前两位是基波及其各次谐波的幅度定义,也代表基波及其各次谐波的权重。第一个字包含基波及2-4次谐波的强度数据,每个强度数据有8位,整个字被均分成4个位域,从低到高分别对应基波及其2-4次谐波的幅度数值。第二个字包含5-8次谐波的强度数据,整个字同样被均分成4个位域,从低到高分别对应5-8次谐波的幅度数值;其余的七个字是各次谐波的相位初始偏移值,从低到高依次是2-8次谐波。

image.png

回复 支持 反对

使用道具 举报

 楼主| 发表于 2020-3-23 22:07:34 | 显示全部楼层
三、实验过程

根据实际硬件决定的输出方式修改宏定义,以片上DAC输出为例,将宏定义OutputApproach修改为Onchip_DAC即可实现,如下图所示:

image.png

随后重新编译工程并烧录固件,即可在PA4,PA5得到音频输出。若需要用外部的IISDAC输出,请将IISDAC的DATA接到PB5,BCK接到PB3,LRCK接到PA15,MCK接到PB4,并修改宏定义到Exteral_IIS_DAC,重新编译工程并烧录固件即可在外部DAC得到音频输出。


回复 支持 反对

使用道具 举报

 楼主| 发表于 2020-3-23 22:10:21 | 显示全部楼层
The specific floor reserved for subsequent content complementation:

补充0:[20200323]说明文件和源码下载: 使用STM32演奏音乐——.zip (2.61 MB, 下载次数: 8)

点评

哈哈,不错的帖子,建议版主给加精!  详情 回复 发表于 2020-3-23 22:23

打赏

参与人数 2M币 +40 收起 理由
玫瑰余香 + 20 看得我一脸懵逼
cushion + 20 謝謝分享

查看全部打赏

回复 支持 1 反对 0

使用道具 举报

您需要登录后才可以回帖 登录 | 立即注册 微信登录

本版积分规则

Archiver|手机版|小黑屋|关于我们|联系我们|网站条款|数码之家 ( 闽ICP备05031405号 )

GMT+8, 2020-3-31 18:44 , Processed in 0.109201 second(s), 14 queries , MemCache On.

Powered by Discuz!

© 2001-2019 Comsenz Inc.

快速回复 返回顶部 返回列表