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

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

只看楼主收藏回复

大家好,我来啦!我即将分享的就是我的一个小制作,名字已经在标题上面报(shuo)出(lou)来(zui)了。STC-ISP镇楼。


IP属地:上海1楼2016-04-16 21:12回复
    话说它是怎么来的呢?一天我在读某本8051的书,在里面看到有一个小制作,可以用无源蜂鸣器播放一些简单的音乐,不过只能播放14个音,并且由于是用STC89C52RC做的,限于各方面资源只能同时播放一条音轨。这怎么够我拿出去展(zhuang)示(bi)呢?我希望它能播放钢琴上88个音中的任意一个,并能同时播放至少四条音轨。于是用STC15F2K60S2做芯片,经过几个星期的开发,终于实现了这样的目的!好吧今天有点晚了,明天再接着更。


    IP属地:上海2楼2016-04-16 21:22
    收起回复
      关于这方面的知识,书上一笔带过:“用蜂鸣器输出音乐,仅仅是好玩而已,应用很少,里边包含了音阶、乐谱的相关内容,程序也有一点复杂,所以就不仔细给大家去讲解了,仅提供一个可以播放《两只老虎》的程序,大家可以下载到板子上玩玩,满足一下好奇心。”这也忒敷衍了吧!幸运的是,就在早些时候我曾经看到过一篇文章,详细地介绍了关于声波频率与音高之间的关系,只可惜原文现在我找不到了,不过内容我还大概记得:国际标准音高是a1=440Hz(a1表示小字一组的A),每相邻两个半音的频率比是 低音:高音=1:¹²√2。这已经足够我做我的播放器了。由这个公式可以算出,当a1=440Hz时,a2=880Hz,a=220Hz(小字组的A),以此类推。


      IP属地:上海4楼2016-04-17 09:31
      回复
        所以呢,我开始实验了。那本书上面的例程是要在定时器中断里面反转蜂鸣器电平,我想着既然都用STC15F2K60S2了,那就好好利用一下丰富的资源吧:STC15系列不仅有十六位自动重载计时器,还能选择当定时器溢出时不是中断而是输出脉冲,这可以大大精简程序。于是我很快地编出了一个测试程序:
        //注:定时器频率是22MHz
        #include "STC15.h"
        void main(void)
        {
        AUXR |= 0x80; //定时器时钟1T模式
        TMOD &= 0xF0; //设置定时器模式为十六位自动重载模式
        TL0 = 0xA8;
        TH0 = 0x61;
        TR0 = 1; //定时器0开始计时
        INT_CLKO = 0x01; //允许T0CLKO输出时钟脉冲
        while (1);
        }


        IP属地:上海5楼2016-04-17 09:48
        收起回复
          关于这个程序我还有几个注释:
          1.要想听到声音,需要使用无源蜂鸣器,并且其正极接+5V,负极接单片机的P3.5/T0CLKO脚。
          2.采用22MHz是因为22000000是440的整数倍,可以尽可能避免误差。
          3.我是希望能让蜂鸣器输出440Hz的声音的,TL0和TH0的初值是怎么来的呢?首先,先计算22000000÷440=50000,得到应该在50000时钟周期之内完成一个脉冲周期。再考虑其实T0溢出两次才是一个完整的脉冲周期,所以再除以2,得到应该让T0每25000时钟周期溢出一次。将25000转换成十六进制数,Bingo!0x61A8就出来了。


          IP属地:上海6楼2016-04-17 09:57
          回复
            我把程序下载了以后,果然成功发出了声音。然而后来跟钢琴声音一对比,发现音高不对。当时这个问题耽搁了我数小时之久。后来再仔细想了想重载值计算方法,猛然大悟:要让T0每25000时钟溢出一次,不应该直接赋一个25000给TH0和TL0,而应该用65535减去25000,得到0x9E57。于是程序的数据改为:
            TL0 = 0x57;
            TH0 = 0x9E;
            这回下载进去,终于正常了。嘛,这种粗心大意的毛病我犯过N多次了,希望大家引以为戒。


            IP属地:上海7楼2016-04-17 15:45
            回复
              于是我就开始继续前行了。嗯,只能让它播放这么一个音是不是太无趣了?多加几个音测试一下怎么样?
              #include "STC15.h"
              uint16 code NotesReload[5] = {0xFFFF,0x9E57,0xCF2B,0xE795,0xF3CA};
              //NotesReload存放着经事先计算得到的a1、a2、a3、a4的重载值。
              //NotesReload[1]是a1,[2]是a2,以此类推。
              //当赋一个NotesReload[0]给TL0和TH0时,T0溢出得很快,发出的声波频率非常高,
              //已经达到了超声波,人耳听不见,可以当作休止符使用,这样就不用为了空拍
              //专门加一个判断并特殊处理了。if语句执行起来还是挺慢的。
              void main(void)
              {
              AUXR |= 0x80;
              TMOD &= ~0x04;
              TL0 = NotesReload[1]; //取重载值低8位
              TH0 = NotesReload[1]>>8; //取重载值高8位
              TR0 = 1;
              INT_CLKO = 0x01;
              while (1);
              }


              IP属地:上海9楼2016-04-17 16:13
              回复
                经过测试,把下标换成其他数也能正常工作,证明程序没有问题!那么接下来要干什么呢?不如让它能初步支持播放节拍吧!经过开发,我编出了如下程序:
                //注:此处晶振由22MHz改为了24MHz,理由一会再讲
                #include "STC15.h"
                #include "typedefine.h"
                uint16 volatile index = 0; //指示现在播放到第几个音的索引
                uint8 code MusicNote[5] = {1,2,0,3,4}; //要播放的音符
                uint16 code MusicBeat[5] = {2000,2000,2000,2000,2000}; //每个音符的持续毫秒数
                uint16 code NotesReload[5] = {0xFFFF,0x9E57,0xCF2B,0xE795,0xF3CA}; //跟刚才是一样的
                uint16 volatile BeatTemp = 0;
                //用来更新T0计数值和节拍计数值的宏
                #define ChangeNote() TR0 = 0;\ //这一句是停止T0以免给TH0和TL0赋值时出岔子.
                TL0 = NotesReload[(MusicNote[index])];\ //装入音符重载值的低8位
                TH0 = NotesReload[(MusicNote[index])]>>8;\ //装入音符重载值的高8位
                BeatTemp = MusicBeat[index];\ //更新当前音符的播放时间
                TR0 = 1; //重启T0
                void main(void)
                {
                ChangeNote(); //装入第一个音符
                AUXR |= 0x80;
                TMOD &= ~0x04;
                AUXR |= 0x04;
                T2L = 0x10;
                T2H = 0xEE; //24MHz下定时1ms
                IE2 |= 0x04;
                EA = 1;
                AUXR |= 0x10;
                TR0 = 1;
                INT_CLKO = 0x01;
                while (1);
                }
                void BeatInterrupt(void) interrupt 12 //这是T2中断函数
                { //处理节拍
                BeatTemp --; //更新时间计数
                if(BeatTemp == 0) //如果当前音符已经播放完毕
                {
                index++; //索引指向下一个音符
                ChangeNote();
                }
                return;
                }


                IP属地:上海10楼2016-04-17 16:53
                回复
                  为什么要采用24MHz晶振呢?请大家想想,我用22MHz 的初衷是什么?对,是为了尽可能消除误差。假设可以采用绝对精确的22MHz,那么输出的440Hz声波就是绝对精确的。如果采用绝对精确的24MHz,那么输出不可避免地会有约六万分之一的误差。但是后来我考虑了实际情况,22MHz的晶振市面上几乎找不到,而用芯片内部的R/C振荡器去做误差可能能达到1%,倒不如改用24MHz的外部晶振来避免这足足1%的误差,那六万分之一的小瑕疵难道还不能忍一忍吗?!再者,以后我很可能还要把“播放器”这个模块整合进其他小制作里面。如果用22MHz会难以移植,改为24MHz就能和大多数其他制作兼容了。嗯,真是益处多多,于是我自此改用了24MHz。


                  IP属地:上海来自手机贴吧11楼2016-04-20 21:11
                  收起回复
                    哦对了,我习惯把unsigned char写成uint8,把long写成int32等等来省事,每次都要写一堆typedef很烦,就做了一个typedefine.h头文件,里面都是些typedef ,然后每次新编程序就copy-paste这个头文件就可以啦这个头文件的代码我等一会就会发在这层楼的回复里面。


                    IP属地:上海来自手机贴吧12楼2016-04-20 21:22
                    回复
                      啊,终于迎来了期中考试后的第一个周末,我考得还不错,应该这周末可以多多更新了吧
                      楼上提到的头文件在回复里放不下,我发在这里吧。
                      //以下是文件内容
                      #ifndef _TYPEDEFINE_H_
                      #define _TYPEDEFINE_H_
                      typedef signed long int int32;
                      typedef unsigned long int uint32;
                      typedef signed int int16;
                      typedef unsigned int uint16;
                      typedef signed char int8 ;
                      typedef unsigned char uint8 ;
                      typedef bit int1 ;
                      #endif


                      IP属地:上海13楼2016-04-29 20:34
                      回复
                        好,十楼的程序已经给大家详细讲了一下了。当然,这程序我已经测试过了的,肯定不会出什么问题的。各位现在已经可以用蜂鸣器播放极简单的音乐了,拿出去给别人炫耀一下应该不成大问题了不过如果就只能到此为止的话那这东西还是废柴一个,所以接着开发吧!这一回编出的程序可以依靠T0+T2真正地播放你想要的音乐了,激动吗?
                        #include "STC15.h"
                        #include "typedefine.h"
                        uint16 volatile index = 0;
                        uint8 code MusicNote[5] = {37,38,0,39,40}; //记载具体要播放的音,你可以自由更改
                        //同样下一个数组也可以更改
                        int16 code MusicBeat[5] = {2000,2000,2000,2000,2000}; //这五个音每个都播放2000毫秒即2秒,不难理解吧
                        volatile BeatTemp = 0;
                        uint16 code NoteCycle[89] = //钢琴88个键每个音的重载值都被我算好放在这里了
                        { //注:这里的重载值跟之前程序的稍有不同:上次的可以直接放到TH0和TL0里面用,
                        //而这里的是记录每两次电平翻转之间的间隔纳秒数,最终载入TH0和TL0的值需要计算一下,这一块我过一会再讲
                        1 ,18182,17161,16198,15289,
                        14431,13621,12856,12135,11454,10811,10204, 9631, 9091, 8581, 8099, 7645,
                        7215, 6810, 6428, 6067, 5727, 5405, 5102, 4816, 4545, 4290, 4050, 3822,
                        3608, 3405, 3214, 3034, 2863, 2703, 2551, 2408, 2273, 2145, 2025, 1911,
                        1804, 1703, 1607, 1517, 1432, 1351, 1276, 1204, 1136, 1073, 1012, 956,
                        902, 851, 804, 758, 716, 676, 638, 602, 568, 536, 506, 478,
                        451, 426, 402, 379, 358, 338, 319, 301, 284, 268, 253, 239,
                        225, 213, 201, 190, 179, 169, 159, 150, 142, 134, 127, 119
                        };
                        #define ChangeNote() TR0 = 0;\
                        TL0 = (uint16)0xFFFF-(uint16)NoteCycle[(MusicNote[index])];\
                        TH0 = ((uint16)0xFFFF-(uint16)NoteCycle[(MusicNote[index])])>>8;\
                        BeatTemp = MusicBeat[index];\
                        TR0 = 1;
                        void main(void)
                        {
                        //P3M0 = 0xFF;
                        //P3M1 = 0x00;
                        ChangeNote();
                        AUXR &= 0x7F;
                        TMOD &= 0xF0;
                        AUXR |= 0x04;
                        T2L = 0x40;
                        T2H = 0xA2;
                        IE2 |= 0x04;
                        EA = 1;
                        AUXR |= 0x10;
                        TR0 = 1;
                        INT_CLKO = 0x01;
                        while (1);
                        }
                        void BeatInterrupt(void) interrupt 12
                        {
                        BeatTemp --;
                        if(BeatTemp == 0)
                        {
                        TR0 = 0;
                        index++;
                        TL0 = (uint16)0xFFFF-(uint16)NoteCycle[(MusicNote[index])];
                        TH0 = ((uint16)0xFFFF-(uint16)NoteCycle[(MusicNote[index])])>>8;
                        TR0 = 1;
                        BeatTemp = MusicBeat[index];
                        }
                        return;
                        }


                        IP属地:上海14楼2016-04-29 21:00
                        回复
                          注释:
                          1.为什么我要用T0+T2呢?其实是为了兼容考虑。现在是做实验,可以随便拿STC15F2K60S2,但是以后移植到其它小制作上面的时候说不定芯片就是STC15W408AS,甚至是STC15F104W,这时候用T0+T2可以与任何芯片完美兼容。而如果用了T1,就只能用STC15F2K60S2,STC15W1K60S,STC15W4K58S4,STC15W404S,STC15W1K08PWM系列了。(以后还要用到三路PCA,那就只能用STC15F2K60S2了,其它几个不是没有PCA就是只有两路PCA)
                          而我现在用到T0+T2,你会发现即使STC15F104W都可以工作,岂不是很好?再考虑以后还要用三路PCA,也有STC15F2K60S2,STC15W404AS,STC15F408AD系列可用,这就宽松多了。


                          IP属地:上海15楼2016-04-30 08:44
                          回复
                            2.为什么用T0播放音乐,T2控制节拍呢?这也没有什么特别的理由,基本是随便定的,但还是有过这样的考虑:以后T1也有可能利用起来,到时候肯定用T1播放音乐。那么现在用T0播放音乐的话,由于T0和T1设置方法基本一致,以后设置T1也能顺手拈来。现在用T2播放音乐的话,以后T1还要在捣腾一下,比较麻烦。所以就这样了。


                            IP属地:上海17楼2016-04-30 08:58
                            收起回复
                              呵呵


                              IP属地:江苏来自Android客户端18楼2016-05-02 15:24
                              回复