城会玩小组吧 关注:38贴子:2,335

回复:第一个分享帖——基于蜂鸣器的多音轨“播放器”

取消只看楼主收藏回复

还是回归正题吧。
3.关于重载值
NoteCycle[89]里面存储的是什么呢?首先说明,NoteCycle[0]是充当一个空拍,这和前面的原理差不多。只不过前面是让它溢出得很快,发出超声波;现在是溢出得很慢,发出次声波,总之就是听不见。不要担心,这个次声波杀不了人的啦~你看我就没死呢,我对着自己放了足足3分钟这样的次声波,最终安然无恙。之所以从超声波切换成次声波是为了节省CPU资源。现在用T0,播放声音都是全自动的,不会有中断。而以后用了PCA播放音乐,每”溢出“(其实是”匹配“,这里如果你不熟悉就先当成”溢出“来理解吧)一次就需要一次中断来帮助它运行(这个东西我也会以后细讲,现在要是不明白就当作是在使用”手动重装“模式的定时器好了),到时候就会体现区别了:要输出次声波,只需要几毫秒中断一次;如果输出超声波,1个机器周期就要中断一次,你觉得CPU忙得过来吗?


IP属地:上海21楼2016-05-04 19:42
回复
    再往后,NoteCycle[1]这个重载值对应着钢琴最左边的键,NoteCycle[2]对应着从左往右第二个键(是黑键),以此类推。不过现在的重载值表示的是声波的每个脉冲周期持续纳秒数。举例:钢琴最左边的键,发出的声音为27.5Hz,那么每个周期持续1/27.5≈0.036364秒,换算成纳秒就是36364纳秒。NoteCycle[1]放的就应该是这个值。
    不过,为什么NoteCycle[1]是18182呢?经过我测试,真的用这样的方波在无源蜂鸣器上面输出27.5Hz的声音的话音效会非常奇怪,所以我把每个音都人为地提高了一个八度,声音会好一点,而一般人可能都不会察觉有这个提高。而且测试下来播放高音也没有因为有这个提高而变得很尖。虽然这样确实不严谨了啦,大家就忍一忍吧。


    IP属地:上海22楼2016-05-04 20:49
    收起回复
      现在我们已经得到了18182这个重载值,应该相应地给T0什么值呢?首先,还是跟之前一样,由于T0溢出2次才是一个周期,所以首先把18182除以2得9091,说明T0每9091纳秒应该溢出1次。然后想想看,24MHz的工作频率下想让1T计数速度下的T0每9091纳秒溢出1次,就应该用9091除以24(这个24是指的T0每纳秒计数24次,因为工作频率是24MHz嘛),再用0xFFFF减去之前算出的商。
      但是,这样要让CPU做很麻烦的除法,非常耗时间;何况做了这个除法以后还会带来误差。所以我就想出了一个很聪明的办法:我们本来的目的是让T0每9091纳秒溢出1次,工作在1T模式时需要做很多除法,但是我们也可以让T0工作在12T模式。这个12T分频不就是一个天然的除法吗?
      供给T0的时钟12分频了以后,T0的计数频率是2次/纳秒。这时候让T0每9091纳秒溢出1次,你再算算,是不是正好应该给T0装入0xFFFF-18182的差呀!各个除法因为一个12分频就全部正好抵消了,我们是多么幸运啊!事实也证明我当初决定采用24MHz而不是12MHz什么的工作频率是恰当的。


      IP属地:上海24楼2016-05-07 14:08
      收起回复
        你问我为什么不干脆就在电脑里事先把这个减法也算好直接装进NoteCycle节省单片机运作的时间呢?第一个,做了这个减法以后,在T0上面会节省一点时间,但是以后在PCA上面反而会浪费更多时间,这一点我也会放到以后再讲。第二,这个减法会增加我本人的工作量。我要么得拿笔拿纸做88个多位数笔算,要么把88个值一一敲进计算器来算,要么耗费大量脑细胞编一个电脑上的C++程序让它自动算重载值并输出到txt文件里,反正都不是轻松的活儿。综上,做这个减法完全是一个费力却然并卵的事,根本没有去做的必要。


        IP属地:上海25楼2016-05-07 14:20
        回复
          所以,你会看到我给TH0和TL0赋的值就是直接用0xFFFF减去NoteCycle某个元素的差。至于一堆(uint16)这种强制类型转换是我的编程习惯,无视就好。还有,我没有特意用哪个语句设置T0工作在12T模式,是因为T0复位以后默认就是以12T模式工作,不必要专门去弄了。


          IP属地:上海26楼2016-05-07 14:29
          回复
            Sorry,电脑出了点问题,很久没有更新了呢。今天就给大家带来一些更新吧。
            14楼的程序写得比较乱,所以后来我把它整理了一下并加了一些注释,变成了这个样子:(Keil源代码直接复制然后粘贴到这里时中文会变成乱码,我只好把源文件用记事本打开才得以完整拷过来)
            #include "STC15.h"
            #include "typedefine.h"
            #include "music.h"
            uint8 code MusicNote[5] = {37,38,0,39,40}; //音乐的音符
            uint16 code MusicBeat[5] = {500,500,500,500,500}; //音乐的节拍,每个值是该音符的持续毫秒数
            uint16 volatile BeatTemp = 0; //节拍缓存
            uint16 volatile index = 0; //音符索引,标记播放到第几个音符
            /*音符重载值计算方法:
            重载值=0xFFFF-该音符的时间周期(时间周期在music.h的NoteCycle有定义,单位:微秒)×
            (系统频率÷1000000÷系统分频系数÷定时器分频系数÷2)
            注:本程序里,系统频率=24MHz,分频系数为1(不分频),定时器12分频,此处(系统频率÷1000000÷系统分频系数÷定时器分频系数÷2)=1,所以干脆不用乘了
            */
            #define ChangeNote() TR0 = 0;/*暂时关闭T0*/\
            TL0 = (uint16)0xFFFF-(uint16)NoteCycle[(MusicNote[index])];/*按照上面的计算方法计算重载值,直接取低8位*/\
            TH0 = ((uint16)0xFFFF-(uint16)NoteCycle[(MusicNote[index])])>>8;/*取高8位*/\
            BeatTemp = MusicBeat[index];/*更新节拍计数*/\
            TR0 = 1;/*重新打开T0*/
            void main(void)
            {
            EA = 1; //预先开好总中断
            //P3M0 = 0xFF;
            //P3M1 = 0x00;
            //CLK_DIV &= 0xFB;
            //CLK_DIV |= 0x02;
            TMOD &= 0xF0; //设置T0:Gate=0;做定时器用;工作在16位自动重装模式
            AUXR &= 0x7F; //T0工作在12T模式
            ChangeNote(); //载入第一个音符并播放
            INT_CLKO = 0x01; //需要进行输出(不需要中断)
            AUXR |= 0x04; //设置T2:做定时器用
            T2L = 0x40; //在24MHz下定时1ms
            T2H = 0xA2;
            IE2 |= 0x04; //允许中断
            AUXR |= 0x10; //启动T2
            while (1); //进入死循环等待中断
            }
            void BeatInterrupt(void) interrupt 12 //管理节拍的中断
            {
            BeatTemp --; //设置节拍
            if(BeatTemp == 0) //如果当前音符已经播放完毕
            {
            index ++; //索引自增
            ChangeNote(); //切换音符
            }
            return;
            }


            IP属地:上海28楼2016-05-20 18:18
            收起回复
              楼上的代码跟10楼的代码作用是一样的,仅有的几处代码改动意图也很明显,我在此就不再多做什么说明了。
              现在我们所达到的效果已经令人满意了,然而这离我最开始的目标还相去甚远,所以让我们继续开发吧。这一次我要向标题上的“多音轨”迈进了,也将要开始使用PCA了。顺便说一下,从开始使用(必须要三路)PCA来实现同时播放4条音轨起我的程序不再能兼容STC15系列的所有单片机了,仅有STC15F2K60S2系列、STC15W404AS系列和STC15F408AD系列可以使用,哪怕STC15W4K56S4系列都因为只有2路PCA可以使用而不能胜任我们的制作。(STC8单片机倒是有4路PCA,但恐怕会有一些不兼容吧,大家要是拿到了样片又有牺牲精神可以把程序灌进去试试)以后我可能会考虑再做一个只用T0+T1+T2播放2音轨版本的程序来兼容STC15W404S系列、STC15W1K29S系列、STC15W4K56S4系列等。


              IP属地:上海29楼2016-05-20 18:49
              回复
                PCA是Programmable Counter Array的缩写,意思是可编程计数器阵。(请允许我用刚得到的红字特权装个逼)——不过光看着这个名字真是让人一头雾水,根本无从看出它的作用。至少我第一次听说它的时候就是这么觉得的。
                要想正确地了解它,最好的途径就是看数据手册。请把STC15系列的数据手册(喂喂,不要拿旧的STC15F104E数据手册!)翻到第912页。我们可以看到,STC15系列单片机的PCA可以实现 软件定时器、 外部脉冲的捕捉、 高速脉冲输出以及脉宽调制(PWM)输出 四个功能。我们想要让PCA输出一个频率可调节的脉冲,所以应该选用高速脉冲输出模式。但是,我们有必要先了解一下它的软件定时器模式。


                IP属地:上海本楼含有高级字体31楼2016-05-25 19:42
                收起回复
                  请翻到第921页。我们可以看到这样一张图:

                  莫慌,我们一块一块来看这个逻辑图(不知道“逻辑图”这个叫法标不标准:-))
                  这张图介绍的是PCA定时器。PCA定时器是什么呢?这是三路PCA模块共用的一个定时器。现在且不要管那三路PCA模块,我们来了解这个定时器本身。我们把它和T2类比以便于理解。它的CH和CL两个寄存器相当于T2的T2H和T2L:复位后是0,开始计数以后计数源每一个脉冲周期CL加1,CL每溢出一次CH就会加1,CH溢出后可以引发中断(当然也可以禁止中断)。但它不像T2一样会自动重装,我拿一个例子来说明。
                  复位以后T2H和T2L各是0。我给T2H赋一个0x80,T2L赋0xFF。然后我启动T2。当T2溢出了以后,会自动再给T2H装入0x80、给T2L装入0xFF,然后从这个值开始重新计数。如果我给CH赋一个0x80,CL赋0xFF再启动PCA定时器,不一会儿PCA定时器溢出了,这时候CH不会被装入0x80而会归0,CL也会归0,如此从0x0000起计数,上一次给CH和CL赋的值可以说就丢失了。明白了吧?
                  T2的中断请求标志位是隐藏的,而PCA定时器的是用户可见的,它就是CF。T2的运行允许位是T2R,PCA定时器的是CR——把CR置1,PCA定时器就启动了。


                  IP属地:上海本楼含有高级字体32楼2016-05-25 20:51
                  收起回复
                    英语看得懂吧好,我们继续。
                    我们现在讲PCA模块的16位软件定时器的功能,因为16位软件定时器模式的图和高速脉冲输出模式的图几乎一样,所以我们就直接来看高速脉冲输出的图好了,但是虽然图看的是P<sub>925</sub>的,说明应该先看P<sub>924</sub>的。
                    这张图的意思是:只要ECOMn(即ECOM0、1或2,这是CCAPM0、1或2的一个位,这是三路PCA各自的寄存器,很快我就会讲到了。ECOMn就是一个使能位,聪明的你不会不能理解的。)为1,就使能CH、CL构成的一个unsigned int数(我也不知道怎么叫它叫做16位数会让人理解成十进制的16位数而不是二进制的16位数)与CCAPnH、CCAPnL构成的unsigned int数比较,如果两数相等,就引发中断。


                    IP属地:上海34楼2016-05-29 07:55
                    收起回复
                      终于橙名了2333
                      让我们迎来久违的更新吧!
                      ——楼上这一段乍一看也不是很清楚,所以我再用更通俗的语言讲一讲。
                      想象一下正在递增的PCA定时器。假设这一刻CH、CL的值(以下简称定时器的值吧,不然打字麻烦)为0x0000,我们想让它在值为0x4000时引发一个中断,应该怎么做?
                      按照32楼那样等它溢出,需要等上整整0x10000个周期。难道要直接给CH、CL赋值吗?No!实际使用中,我们就要启用CCAPnH、CCAPnL了。以PCA模块0为例。
                      PCA模块0的相应寄存器是CCAP0H、CCAP0L。有了它,我们就可以控制PCA在任何想要的时长后中断(当然,超过65536个周期就要另想办法)。比方说要让它在值为0x4000时引发一个中断,那么只要给CCAP0H、CCAP0L赋一个0x4000并允许PCA中断就可以了。


                      IP属地:上海35楼2016-06-08 18:54
                      收起回复
                        百度nmb,又占楼放广告
                        PCA定时器开始从0计数了。每个周期除了定时器的值自动+1以外,单片机还会自动将PCA定时器的值与所有三路PCA模块的值进行比较。如果与某个模块比较相等并且这个模块的中断允许位为1,就会发生中断。(中断允许位是一个专门控制中断的位,名字叫ECCFn,这个n也是0、1或2,稍后我讲寄存器的时候一起讲)
                        大概能看懂了吗?不管你能不能看懂,我还是建议你再去看一看STC15系列数据手册的PCA部分。我可不是干教师这一行的,有没有漏讲什么东西我可不敢保证呀(苦笑)。


                        IP属地:上海37楼2016-06-19 15:12
                        回复
                          mmm...我想我大概不能再拖更了,所以我们开始吧。
                          看了上面的介绍,你八成就会发问了:过了0x4000个周期以后PCA引发了中断,但是再往后又是每0x10000个周期等到PCA计时器溢出后的下一轮计数到此才会引发中断了,那么有效中断就只有一次,不就和T0/T1的手动重装模式一样了吗?那要连续多次定时0x4000怎么办呢?
                          很好,你已经明白了PCA的任何模块都不能自我重装。那么要想达到T0等自动重装的效果,我们大可以参考一下普通定时器手动重装模式下达到连续定时效果的方法。(好长的句子


                          IP属地:上海38楼2016-07-01 18:54
                          回复
                            所谓“连续定时”,也是我一时找不到词才造的说法其实就是比方说在16位手动重装模式下需要每0x4000周期时用的办法。我们可以先让定时器(这里定时器指T0这样的普通定时器而非PCA定时器)一个初值0xC000让它跑起来,溢出引发中断时马上再给它0xC000继续跑,这样就可以实现约每0x4000周期中断一次。
                            现在我们的PCA可以采取类似的思路实现"连续定时"。啥,你要把CH、CL像手动重装模式下的TH0、TL0一样处理?我都说了,用那种办法只会白白把附带的PCA模块给浪费掉。所以,我们应该去动CCAPnH、CCAPnL。


                            IP属地:上海39楼2016-07-01 19:28
                            回复
                              请再一次想象正从0x0000开始计数的PCA定时器。现在,我们要让它每0x4000周期中断一次了。
                              读到这里,你应该可以独立想出来该怎么办了。没错,我们先给PCA模块0装入0x4000,这样引起第一次中断;每进入中断服务程序时,把PCA模块0的值加上0x4000,保证它再过0x4000周期还会再次中断。如果PCA模块0的值已经超过了0x10000,那么只管把加起来的和减去0x10000就好了。
                              现在是从0x0000开始计数,那么每次应装入PCA模块0的值依次为:
                              (打开定时器前)0x4000--->(第一次中断)0x8000--->(第二次中断)0xC000--->(第三次中断)0x0000(超过0x10000,直接减去0x10000)--->(第四次中断)0x4000……】
                              就这么简单?
                              就这么简单!


                              IP属地:上海40楼2016-07-10 12:53
                              收起回复