电子实训课程实验项目
--电子琴
【前言】
为进一步激发学生对于硬件编程的兴趣而开展的课程“电子实训”课程到目前为止已经要告一段落了。将近四周的时间,从电路板印刷、贴片参观,到自己亲手将原件焊接到电路板上,再到一步一步熟悉STC编程当中的技巧,我们渐渐对硬件编程有了初步的认识,虽然并不一定能完成什么高级的设计,但是对目前所涉及到的数码管、LED、拨码开关、中断、定时器、传感器的应用已经有了初步认识,也可以写出一些简单的应用。为验证十几天的学习成果,每个人都会根据给出的53个例程当中选择自己比较熟悉或者擅长的方面进行一个创新设计或者扩展设计,也或者融合多个方面。
我完成的项目是一个“电子琴”,基于工程“电子音乐”改编而来,不过也做出了很多的改变,也增加了一些东西,使得整个项目看起来还算不错。
【实验目的与要求】
1、 熟练掌握定时器的应用,以及如何通过定时器驱动蜂鸣器发出一定频率的声音,掌握简谱与定时器重装值之间的关系,能够实现蜂鸣器谱乐;
2、 熟悉下位机编程,usb转串口原理应用,相关寄存器的使用,并且实现该功能,为上、下位机通信奠定基础;
3、 熟悉上位机编程方法,可以参考相关资料,设计并实现上位机;
【设计概要】
本案例的设计主要可以分为两个部分,上位机部分的设计和下位机部分的设计
上位机:
案例的上位机是用MFC以及MFC中的串口控件来完成的。上位机中共有8个按键,一个下拉框,三个复选框,七个按键完成对音符频率的选择,对应简谱中的DO,RE,MI,FA,SO,LA,XI,每当一个按键被按下的时候,上位机会通过串口将一个字节发送到单片机,在发送的时候,上位机会检测被选中的复选框,若选中的是“低八度”,则会发送低八度对应的音节;若选中的是“中八度”,则会发送中八度对应的音节,高八度也类似。下拉框控制的是串口号的选择,列出了电脑主机所配置的串口号,串口号下面有一个“打开窗口”的按钮,当点击的时候,上位机会检测下拉框中选择的串口号,并将其对应的串口号打开。在点击的同时,上位机会设置串口的相关属性值,m_ctrlComm.SetSettings("9600,n,8,1");设置串口波特率为9600,无校验位,8个数据位,1个停止位,并设置以二进制方式捡取数据,清空缓冲区。
下位机:
下位机设计的重要部分在于串口和蜂鸣器两个部分。串口部分使用的是串口1中断,串口1中断使用TH1和TL1作为串口中断的计时器,使用TL1=(65536-(Machine_Focs/4/BAUD1));TH1=(65536-(Machine_Focs/4/BAUD1))>>8作为重装值,当串口发生中断的时候,会触发中断处理程序。中断处理程序中会将已经转换成1的RI端口和TI端口转换成0,返回源程序,开始下一次中断的历程,并且会触发playmusic函数,将display中的音节在蜂鸣器中播放。
测试方法:
(1)打开“piano”工程文件,找到“hex”文件;
(2)打开ISP下载器,选中该“hex文件”,选中对应的端口,点击下载;
(3)下载完成,打开上位机“test.exe”,选中对应的端口,点击“打开串口”;
(4)串口打开,选中对应的音阶,低八度,中八度,或者高八度,然后点击按钮,蜂鸣器发出声音;
(5)测试完成;
【实验原理】
1、实验原理图
(1)无源蜂鸣器电路原理图
(2)芯片相关引脚图
(3)单片机下载电路
2、USB转串口原理:单片机集成了USB转串口模块,对应使用RXD线接收数据,用TXD发送数据。每个串口由2个数据缓冲器(相互独立1收1发)、一个移位寄存器(一字节数据一位一位发送出去)、一个串行控制器和一个波特率发生器(这个比较重要,结合相关的定时器)组成。对应发送、接收数据完成(RI、TI硬件置1)都会触发串口中断,但是无法确定是哪个触发的,所以在串口中断中我们要判断是接收数据产生的中断还是发送数据产生的中断,对于发送数据产生的中断,我们要软件将TI清0,并将数据就绪标志清0,允许下一字节数据发送,发送数据函数中通过while循环,等待发送数据准备就绪,完了将就绪的数据复制给SBUF;对于接收数据产生的中断,我们要软件将RI清0,并从SBUF中读取数据。
3、蜂鸣器原理:
本实验板采用的是无源蜂鸣器,无源内部不带震荡源,所以如果用直流信号无法令其鸣叫。必须用2K~5K的方波去驱动它。相比与有源蜂鸣器,无源蜂鸣器的优点在于价格便宜,可以通过控制其振动频率来改变发出的声音,做出“多来米发索拉西”的效果。因此,无源蜂鸣器可以用于音乐的播放。而有源蜂鸣器的优点在于使用简单,不需要编写“乐谱”。本实验板使用的无源蜂鸣器是电磁式蜂鸣器,电磁式蜂鸣器由振荡器、电磁线圈、磁铁、振动膜片及外壳等组成。接通电源后,接收到的音频信号电流通过电磁线圈,使电磁线圈产生磁场。振动膜片在电磁线圈和磁铁的相互作用下,周期性地振动发声。
蜂鸣器部分每个音符都有自己对应的频率,每个频率也多有自己对应的简谱码,每一个简谱码都对应了一个计时器重装值,比如说低1DO频率是262Hz,则振动周期T=1000ms/262Hz=3.817ms,要求定时器在3.817ms会产生一个周期振动,由于一个周期振动是两次电平变化,而每一次定时器中断只有一次电平变化,所以要将定时器设置成1.908ms震动一次,也就是1908us,因此可以得到定时器重装值为65536-1908=63628。
【源码展示与说明】
下位机部分:
/*文件名称piano.c
通过串口用上位机控制单片机上面的蜂鸣器发出不同频率的声音
*/
#include"STC15F2K60S2.H"
//宏定义
#defineuchar unsigned char
#defineuint unsigned int
#defineMachine_Focs 11059200L //晶振频率 11.0592MHz
#defineBAUD1 9600 //波特率,这里使用的是9600
sbitLED_SEL=P2^3;
sbitbeep=P3^4; //蜂鸣器引脚
uchartimeh,timel; //定时器的重装值
/*收发数据相关*/
uchardisplay; //单片机上SBUF缓冲的数据
ucharflag;
intcount=0; //计数器,用来分频
ucharduanxuan[]={0x3f,0x06,0x5b,0x4f,0x66,0x6d,
0x7d,0x07,0x7f,0x6f,0x77,0x7c,0x39,0x5e,0x79,0x71};
//段选信号,选择0-f
/*蜂鸣器振动频率相关*/
ucharcode quzi[] ={ //此数组作为各个音符在定时器中的重装值
//低八度
0xf8,0x8c,0xf9,0x5b,0xfa,0x15,0xfa,0x67,0xfb,0x04,0xfb,0x90,0xfc,0x0c,
//中八度
0xfc,0x44,0xfc,0xac,0xfd,0x09,0xfd,0x34,0xfd,0x82,0xfd,0xc8,0xfe,0x06,
//高八度
0xfe,0x22,0xfe,0x56,0xfe,0x6e,0xfe,0x9a,0xfe,0xc1,0xfe,0xe4,0xff,0x03
};
/***************************************************************
曲调
****************************************************************/
ucharquyin(uchar tem)
{
uchar qudiao,jp,weizhi; //定义曲调,音符,位置
qudiao=tem/16; //高四位为是曲调值
jp=tem%16; //低四位是音符
if(qudiao==1) //当曲调值为1的时候,低八度,低八度在quzi数组中的基址为0
qudiao=0;
else if(qudiao==2) //当曲调值为2的时候,中八度,中八度在quzi数组中的基址为14
qudiao=14;
else if(qudiao==3) //当曲调值为3的时候,高八度,高八度在quzi数组中的基址为28
qudiao=28;
weizhi=qudiao+(jp-1)*2; //基址加上偏移量得到音符对应数组中的位置
return weizhi; //返回位置值
}
/**********************
函数名称:void delay(unsigned int xms)
功能描述:延时
入口参数:xms:输入需要延时的毫秒值
出口参数:无
***********************/
voiddelay(uint xms)
{
uint i;
for(; xms>0; xms--)
for(i=114; i>0; i--)
{}
}
/**********************
函数名称:Timer0
功能描述:定时器0的中断响应函数,用来控制蜂鸣器
***********************/
voidTimer0() interrupt 1
{
count++;
if (count==4)
{
count=0;
}
TH0=timeh;
TL0=timel;
if(flag==1&&count==3) //四分频
{
beep=~beep;
}
}
/**************************************************************
函数名称:Uart1_Init
功能描述:初始化串口中断
***************************************************************/
voidUart1_Init(void)
{
AUXR=0X80; //辅助寄存器,使T0x12=1,此时不分频
SCON|=0X50; //SM0=0,SM1=1,串口以方式1工作,8位Uart,串行口1用定时器1作为其波特率发生器且定时器1工作于模式0;REN=1,开启串口接收
TL1=(65536-(Machine_Focs/4/BAUD1));
TH1=(65536-(Machine_Focs/4/BAUD1))>>8;
AUXR|=0X40; //使T0x12=1,此时不分频
RI=0; //接收终端标志位
TI=0; //发送中断标志位
TR1=1; //启动的定时器1
ES=1; //串口中断允许位
EA=1; //总中断允许位
}
/**************************************************************
函数名称:Init
功能描述:完成各部分功能模块的初始化
***************************************************************/
voidInit() //初始化操作
{
P3M0=0x00; //推挽模式
P3M1=0x00;
P2M0=0xff;
P2M1=0x00;
P0M0=0xff;
P0M1=0x00;
TMOD=0x01; //定时器0,方式1,要求每一次中断之后手动重装
ET0=1; //开启定时器0中断
EA=1; //开启总中断
TH0=0x00;
TL0=0x00;
TR0=1; //启动定时器0
beep=0; //蜂鸣器初始化0
flag=0;
P0=0;
Uart1_Init(); //串口中断
display = 0x00; //初始化数据缓冲器
LED_SEL=0; //设置数码管显示状态
}
/**********************
函数名称:void playmusic()
功能描述:播放音乐
***********************/
voidplaymusic(uchar p) //p为音节
{
uchar tem;
tem=quyin(p); //找到p音节在quzi数组中的位置
timeh=quzi[tem]; //音节重装值的高八位
timel=quzi[tem+1]; //音节重装值的低八位
TR0=1; //开启定时器0中断
delay(0x10*180); //延时一个节拍
TR0=0; //关闭定时器0中断
}
/*************************************************************
串口1中断相应程序
**************************************************************/
voidUart1_fun() interrupt 4
{
if(RI) //接受完数据以后,RI自动转1
{
flag=1;
RI=0;
display=SBUF;
//TR0=1;
playmusic(display);
}
//if(TI) //接受完数据以后,RI自动转1
//{
//TI=0;
//Uart1_Sendbusy=0;
//}
}
/**************************************************************
主函数
***************************************************************/
voidmain()
{
Init();
while(1){} //执行死循环
}
上位机部分:
int nIndex =m_chuankou_select.GetCurSel();
CString strCBText;
m_chuankou_select.GetLBText(nIndex,strCBText);
用来控制从下拉框m_chuankou_select中获得已经选择的串口号,并且将其存储在字符串strCBText中;
if(strCBText=="")
AfxMessageBox("请选择串口");
else if(strCBText=="com1")
m_ctrlComm.SetCommPort(1);
else if(strCBText=="com2")
m_ctrlComm.SetCommPort(2);
。。。。。。
用来打开strCBText对应的串口号
m_ctrlComm.SetSettings("9600,n,8,1");//波特率9600,无校验,8个数据位,1个停止位 m_ctrlComm.SetInputMode(1); //1:表示以二进制方式检取数据
m_ctrlComm.SetRThreshold(1);//参数1表示每当串口接收缓冲区中有多于或等于1个字符时将引发一个接收数据的OnComm事件
m_ctrlComm.SetInputLen(0);//设置当前接收区数据长度为0
m_ctrlComm.GetInput();//先预读缓冲区以清除残留数据
设置串口属性
UpdateData(TRUE); //读取编辑框内容
if(flag==3)
m_ctrlComm.SetOutput(COleVariant((CString)0x31));//发送数据
else if(flag==2)
m_ctrlComm.SetOutput(COleVariant((CString)0x21));//发送数据
else if(flag==1)
m_ctrlComm.SetOutput(COleVariant((CString)0x11));//发送数据
else
AfxMessageBox("请选择一个音阶");
控制发送高中低八度的DO,其他音节类似
CStringstr[]={"com1","com2","com3","com4","com5","com6","com7","com8","com9"};
((CComboBox*)GetDlgItem(IDC_chuankou_select))->ResetContent();
CString defaultstr=str[0];
SetDlgItemText(IDC_chuankou_select,defaultstr);
for(int i=1;i
{
((CComboBox*)GetDlgItem(IDC_chuankou_select))->AddString(str[i]);
}
填充下拉框
【实验总结】
1、 接收中断自动置1的问题:在串口传输的过程当中,使用RXD接收上位机传过来的数据,使用TXD传出数据,每当数据发送完成,TI会自动置位为1,请求接收中断处理;每当数据接收完成,RI会自动置位为1,请求发送中断处理,由于TI和RI以“或”逻辑关系向主机请求中断,所以主机响应中断时事先并不知道是TI还是RI请求的中断,必须在中断服务程序中查询TI和RI进行判别,然后分别处理,因此,两个中断请求标志位均不能由硬件自动置位,必须通过软件置零,否则将出现一次请求多次向英的错误。这里采用的是在中断处理程序中进行软件置位,也就是每一次中断结束时检测中断的类型,进而对对应的中断标志位进行置位。
2、 音节的节拍的完成:即控制某一频率的震动在某一指定时间内完成。这一功能是通过控制定时器0的开关实现的,即当单片机接收到串口数据时,会执行playmusic函数,在默认情况下,定时器0开关TR0=0是关闭状态,执行playmusic的同时,打开定时器开关TR0=1,中间延时0x10*180ms一个节拍,延时结束后将定时器控制TR0置0,等待下一次输入。由于在之前的“电子音乐”工程当中是用数组存储音节和节拍的,每两个字节决定一个音阶和一个节拍,在串口中一次只能传输一个字节,所以如果要实现节拍功能就会给用户带来很多不便,所以在这里舍去了节拍的选择,转而采用固定的一个节拍。
【实验心得】
1、 单片机使用c语言进行编程,这样给嵌入式开发者带来了很大的方便,毕竟我们最初接触到的语言就是c语言,不过和之前的c语言编程相对比,现在编程语句结构相对分散或者说是独立而不像之前的编程当中体现的非常强的逻辑,但是由于新加入的中断定时器等会再各个结构之间产生影响,仍然需要非常注意,特别是对于中断概念的理解,可以参考“深入理解计算机系统”这本书;
2、 要多编程,了解各个引脚,寄存器的用法、功能。虽说我们做硬件编程的时候可以随时查找数据手册寻找需要的知识,但是如果能记住这些寄存器、引脚的分布、用法,无疑会给硬件编程带来非常大的方便,这就要求我们多编程,才能比较好的掌握;
3、 stc硬件编程入门很容易,也就是说很容易便可以掌握stc板的一些基本用法,像是数码管、led、定时器等等,但是要编出比较强大好用的硬件程序,就必须多去了解各种中断用法、编程技巧等,并且这些知识经验只能靠自己在编程的过程中发现,不是任何老师可以教授的,所以,学习单片机,要亲力亲为。