51单片机 《只因你太美》实现
成品展示(实现方法请往下翻)
“5.12 qRX:/ 08/07 W@M.jp 鸡你太美 单片机版 v.douyin.com/iYss4rj3/ 复制此链接,打开Dou音搜索,直接观看视频!” (复制的时候记得删除引号)
感想
最开始学习到点阵实现的时候我就在想,能不能用单片机整一个活,学完蜂鸣器之后就觉得应该是可以实现的,因为蜂鸣器是用单片机的中断函数来实现的,那么就存在一边放歌一遍播放动画的可能性,但是在我拼接代码之后我发现,每毫秒中断一次会让整个发亮效果很差,然后动画也像是卡成了PPT,虽然他本来就是连续的PPT。。。
只能说很可惜,我尝试失败了,所以成品的展示是使用剪辑的方式让两部分合在一起。还有就是我对乐理的极其不了解,导致这首《只因你太美》明明是降A大调,但是我实在是找不到他对应的频率,只能使用A大调来实现音乐的部分。
最后,我的解决办法只能通过剪辑来融合在一起了。
具体实现方法
本文章参考资料来源如下,供大家学习,作者本身使用的代码并没有直接照搬,而是了解了基本实现之后有自己的修改和优化,所以于原文的代码有出入是正常的。
最主要参考,江协科技的51开发视频:
音乐代码参考:[11-1] 蜂鸣器_哔哩哔哩_bilibili
动画代码参考:[9-2] LED点阵屏显示图形&动画_哔哩哔哩_bilibili
动画的每一帧参考:51单片机 点阵矩阵 坤坤代码_单片机蔡徐坤点阵代码-CSDN博客
其他参考:
51单片机的机器周期和时钟周期计算及11.0592Mhz晶振的机器周期是多少_11.0592mhz晶振的时钟周期-CSDN博客
ROM, FLASH和RAM的区别 - 知乎 (zhihu.com)
代码部分
分成两个部分,动画部分和音乐部分。
首先展示的是动画部分:
这两个头文件我就不展示了
#include <REGX52.H>
#include <intrins.h>
动画部分
main.c
//main.c
//头文件引入
#include <REGX52.H>
#include "Delay.h"
#include "MatrixLed.h"
//主函数入口
void main()
{
MatrixLed_Show_donghua();
}
Delay.h
#ifndef __DELAY_H__
#define __DELAY_H__
#include <intrins.h>
void Delay(unsigned long ms); //ms=1,延时1ms
void Delay10us(unsigned char us); //us=1,延时10us
#endif
Delay.c
#include "DELAY.H"
/**
* @brief 延时函数
* @param ms 你打算延时的ms数量
* @param 无
* @retval 无
*/
void Delay(unsigned long ms) //@11.0592MHz
{
unsigned char i, j;
while(ms--)
{
_nop_();
i = 2;
j = 199;
do
{
while (--j);
} while (--i);
}
}
/**
* @brief 延时函数 us=1 延时10us
* @param us 你打算延时的us数量
* @param 无
* @retval 无
*/
void Delay10us(unsigned char us) //@11.0592MHz
{
unsigned char i;
for(; us>=0; us--)
{
i = 2;
while (--i);
}
}
MatrixLed.h
#ifndef __MATRIXLED_H__
#define __MATRIXLED_H__
#include <REGX52.H>
#include "Delay.h"
//定义 74HC595 控制管脚
sbit RCK=P3^5; //RCLK
sbit SCK=P3^6; //SRCLK
sbit SER=P3^4; //SER
#define MatrixLed_Port P0
void MatrixLed_74HC595_Write(unsigned char dat);
void MatrixLed_ShowColumn(unsigned char column,unsigned char dat);
void MatrixLed_Show_donghua();
#endif
MatrixLed.c
#include "MatrixLed.h"
//kun舞具体点阵数据
unsigned char code MatrixLed_Buff[] =
{
0x00,0x0E,0x1B,0x7F,0x7F,0x1B,0x0E,0x00,
0x00,0x04,0x0C,0x18,0x7F,0x7F,0x1B,0x0E,
0x00,0x08,0x0C,0x05,0x7F,0x7F,0x1E,0x0C,
0xC0,0x00,0x41,0x22,0x1A,0x7C,0x7D,0x1A,
0x00,0x60,0x61,0x12,0x0A,0x7E,0x7F,0x08,
0x00,0x06,0x0E,0x08,0x0B,0x3E,0x3E,0x0B,
0x00,0x03,0x0B,0x08,0x0B,0x3E,0x3E,0x0B,
0x00,0x06,0x0E,0x08,0x09,0x3F,0x3E,0x0B,
0x00,0x0E,0x13,0x7E,0x7E,0x1F,0x0E,0x00,
0x08,0x13,0x7E,0x7E,0x12,0x1D,0x0C,0x00,
0x09,0x12,0x7E,0x7F,0x10,0x13,0x03,0x00,
0x09,0x12,0x7E,0x7F,0x10,0x1C,0x0C,0x00,
0x00,0x0C,0x15,0x7E,0x7E,0x17,0x0E,0x00,
0x00,0x30,0x38,0x0D,0x7E,0x7F,0x1D,0x08,
0xC0,0xC0,0x30,0x1B,0x7C,0x7F,0x08,0x00,
0x00,0x00,0x37,0xF8,0xFA,0x34,0x00,0x00,
0x00,0x00,0x1B,0x7C,0x7C,0x1A,0x01,0x00,
0x00,0x00,0x09,0x1E,0x7C,0x7F,0x18,0x00,
0x00,0x00,0x1B,0x7C,0x7C,0x1B,0x00,0x00,
0x00,0x00,0x09,0x1E,0x7C,0x7F,0x18,0x00,
0x00,0x00,0x1B,0x7C,0x7C,0x1B,0x00,0x00,
0x08,0x10,0x16,0x7C,0x7F,0x10,0x08,0x00,
0x00,0x10,0x13,0x7C,0x7E,0x18,0x00,0x00,
0x00,0x08,0x13,0x7C,0x7F,0x10,0x10,0x00,
0x00,0x08,0x12,0xFC,0xFD,0x3A,0x00,0x00,
0x00,0x08,0x12,0xFC,0xFC,0x22,0x10,0x00,
0x00,0x00,0x61,0x12,0x7C,0x7F,0x10,0x60,
0x00,0x00,0x20,0x11,0x12,0x7C,0x7E,0x19,
0x00,0x00,0x00,0x31,0x0A,0x7C,0x7E,0x0D,
0x00,0x00,0x20,0x11,0x12,0x7C,0x7E,0x19,
0x00,0x00,0x00,0x31,0x0A,0x7C,0x7E,0x0D,
0x00,0x00,0x20,0x11,0x12,0x7C,0x7E,0x19,
0x00,0x00,0x00,0x31,0x0A,0x7C,0x7E,0x0D,
0x00,0x00,0x20,0x11,0x12,0x7C,0x7E,0x19,
0x00,0x00,0x00,0x31,0x0A,0x7C,0x7E,0x0D,
};
/**
* @brief 展示kun舞
* @param 无
* @retval 无
*/
void MatrixLed_Show_donghua()
{
unsigned char i , index = 0, count = 0;
while(1)
{
for(i = 0; i<8 ; i++ )
{
MatrixLed_ShowColumn(i, MatrixLed_Buff[i + index]);
}
count ++; //这里使用计次时间延时的原因在于,直接使用Delay只会扫描一遍,灯关很暗,用计次的方式就可以重复扫描,灯关就亮很多
if(count > 15)
{
count = 0;
index += 8 ;
if(index > 34*8)
{
index = 0;
}
}
}
}
/**
* @brief 74HC595写入函数
* @param 控制某列中亮的灯
* @retval 无
*/
void MatrixLed_74HC595_Write(unsigned char dat)
{
unsigned char i;
SCK=0;
RCK=0;
for(i=0;i<8;i++)
{
SER=dat&(0x80>>i);
SCK=1;
SCK=0;
}
RCK=1;
RCK=0;
}
/**
* @brief 显示某一列的LED灯
* @param 某一列
* @param LED灯要怎么亮
* @retval 无
*/
void MatrixLed_ShowColumn(unsigned char column,unsigned char dat)
{
MatrixLed_74HC595_Write(dat);
MatrixLed_Port=~(0x80>>column);
Delay(1);
MatrixLed_Port = 0xff;
}
音乐部分
main.c
//头文件引入
#include <REGX52.H>
#include "Delay.h"
#include "Buzzer.h"
#include "Timer0.h"
#include "Chicken.h"
//主函数入口
void main()
{
Timer0Init();
while(1)
{
//音乐
if(MusicTable[MusicSelect][0] != 0xff) //如果不是停止符
{
FreqSelect = MusicTable[MusicSelect][0]; //选择音符对应的频率
Delay( SPEED / 4 * MusicTable[MusicSelect][1] ); //选择音符对应的时值
MusicSelect++ ;
TR0 = 0;
Delay(5);
TR0 = 1;
}
else //如果是终止符,重置,循环播放
{
FreqSelect = 0 , MusicSelect = 0;
}
}
}
//中断函数,每一毫秒中断一次
void Timer0_Routine() interrupt 1
{
if(FreqTable[FreqSelect]) //如果不是休止符
{
/*取对应频率值的重装载值到定时器*/
TL0 = FreqTable[FreqSelect] % 256; //设置定时初值
TH0 = FreqTable[FreqSelect] / 256; //设置定时初值
Buzzer = !Buzzer; //翻转蜂鸣器IO口
}
}
Delay.h
#ifndef __DELAY_H__
#define __DELAY_H__
#include <intrins.h>
void Delay(unsigned long ms); //ms=1,延时1ms
void Delay10us(unsigned char us);
#endif
Delay.c
#include "Delay.h"
/**
* @brief 延时函数
* @param ms 你打算延时的ms数量
* @param 无
* @retval 无
*/
void Delay(unsigned long ms) //@11.0592MHz
{
unsigned char i, j;
while(ms--)
{
_nop_();
i = 2;
j = 199;
do
{
while (--j);
} while (--i);
}
}
/**
* @brief 延时函数 us=1 延时10us
* @param us 你打算延时的us数量
* @param 无
* @retval 无
*/
void Delay10us(unsigned char us) //@11.0592MHz
{
unsigned char i;
for(; us>=0; us--)
{
i = 2;
while (--i);
}
}
Timer0.h
#ifndef __TIMER0_H__
#define __TIMER0_H__
#include <REGX52.H>
void Timer0Init(void);
#endif
Timer0.c
#include "Timer0.h"
/**
* @brief 定时器0初始化函数
* @param 无
* @param
* @retval 无
*/
void Timer0Init() //1毫秒@11.0592MHz
{
TMOD &= 0xF0; //设置定时器模式
TMOD |= 0x01; //设置定时器模式
TL0 = 0x66; //设置定时初值
TH0 = 0xFC; //设置定时初值
TF0 = 0; //清除TF0标志
TR0 = 1; //定时器0开始计时
ET0 = 1; //允许定时器0的使用
EA = 1; //定时器总开关开启
PT0 = 0; //中断优先级设置为低优先级 默认为0
}
/*定时器0函数模板
//每一毫秒中断一次
void Timer0_Routine() interrupt 1
{
static unsigned int T0Count = 0;
TL0 = 0x66; //设置定时初值
TH0 = 0xFC; //设置定时初值
T0Count++;
if(T0Count >= 1000) //1s
{
}
}
*/
Buzzer.h
#ifndef __BUZZER_H__
#define __BUZZER_H__
#include <REGX52.H>
#include <INTRINS.h>
//特殊寄存器、管脚设定
//蜂鸣器端口
sbit Buzzer = P2^5;
void Buzzer_Delay500us(unsigned char cnt) ;
void Buzzer_Time(unsigned int time);
#endif
Buzzer.c
#include "Buzzer.h"
/**
* @brief 蜂鸣器特有延时500us函数
* @param cnt 次数*500us=延时时间
* @retval 无
*/
void Buzzer_Delay500us(unsigned char cnt) //@11.0592MHz
{
unsigned char i;
for(;cnt>0;cnt--)
{
_nop_();
i = 227;
while (--i);
}
}
/**
* @brief 蜂鸣器发声时间
* @param 发声时间
* @retval 无
*/
void Buzzer_Time(unsigned int time)
{
unsigned char i = 0;
for(i = 0; i < time*2; i ++)
{
Buzzer = !Buzzer;
Buzzer_Delay500us(1);
}
}
Chicken.h
#ifndef __CHICKEN_H__
#define __CHICKEN_H__
//音符和音频选择位
unsigned char FreqSelect = 0 , MusicSelect = 0;
//音符与索引对应表,P:休止符,L:低音,M:中音,H:高音
#define L1 9
#define L2 11
#define L3 13
#define L4 14
#define L5 16
#define L6 18
#define L7 20
#define M1 21
#define M2 23
#define M3 25
#define M4 26
#define M5 28
#define M6 30
#define M7 32
#define H1 33
#define P 36 //休止符
////索引与频率对照表
unsigned int code FreqTable[] = {
63453,63570,63680,63784,63883,63975,64063,64146,64224,64297,64367,64433, //低音
64494,64553,64608,64660,64709,64756,64800,64841,64880,64917,64951,64984, //中音
65015,65044,65072,65098,65123,65146,65168,65188,65208,65226,65244,65260, //高音
0, //休止符
};
//只因你太米音乐乐谱,[][0]存放乐谱,[][1]存放对应的时长倍数,按照十六分音符作为基准
unsigned char code MusicTable[][2] = {
//音符,时值,
//1
P ,2,
L6,1,
L6,1,
M3,2,
M3,2,
//2
L6,4,
P ,4,
P ,4,
P ,2,
L6,1,
L6,3,
//3
P ,2,
P ,4,
P ,2,
L6,1,
L6,1,
M3,2,
M3,2,
//4
L6,4,
P ,4,
P ,4,
P ,2,
L6,1,
L6,3,
//5
P ,2,
P ,2,
L6,1,
L6,1,
L6,2,
L6,1,
L6,1,
M3,2,
M3,2,
//6
L6,4,
P,4,
P,4,
P,2,
L6,1,
L6,3,
//7
P,2,
P,4,
P,2,
L6,1,
L6,1,
M3,2,
M3,2,
//8
L6,4,
P,4,
P,4,
P,2,
L6,1,
L6,3,
//后面不会了,这个连续的X又怎么搞啊,淦,不愧是kun歌
0xFF //终止标志
};
//播放速度,值为四分音符的时长(ms)
#define SPEED 560
#endif
下面这一部分是建立在读者对江协科技的视频有了解的基础上补充的内容,如果您完全不了解那也可以不看的
器材准备
普中51实验板(STC89C52RC芯片),已经集成了无缘蜂鸣器和8*8的点阵屏,如下图
理论知识
江协科技使用的是12MHZ的晶振,而作者的开发板是11.0592MHZ的晶振,所以在周期方面需要调整。
机器周期和时钟周期的区别
单位转换关系
12MHZ:
时钟周期:1/12Mhz,1单位是秒所以12Mhz要转为秒为12000000hz 1/12000000≈0.00000008s 机器周期:12×时钟周期=0.00000008s×12=0.000001s 转为us就是1us
11.0592Mhz:
时钟周期:1/11.0592Mhz,1单位是秒所以11.0592Mhz要转为秒为11059200hz 1/11059200≈0.00000009s 机器周期:12×时钟周期=0.00000009s×12=0.00000109s 转为us就是1.09us
乐理知识
键盘与音符对照
音符时值
音符名称 | -写法- | -时值- |
---|---|---|
全音符 | 6 - - - | 四拍 |
二分音符 | 6 - | 两拍 |
四分音符 | 6 | 一拍 |
八分音符 | 6 | 半拍 |
十六分音符 | (数字下面加两条横线) | 1/4拍 |
简谱
以最快的音符时长作为基准,也就是十六分音符作为单位
其中这一首谱子的四分音符=107
也就是每分钟演奏107个四分音符 107(个) / 60(s) = 1.78(个/s) 1(个) / 1.78(个/s) = 0.56(s)
运算之后就是每560ms一个四分音符
(我代码只实现到了第一大段,也就是“迎面走来的你让我如此蠢蠢欲动“之前)
EXCEL表格
查阅资料之后(百度,GPT等,说实话我也没太理解),简谱上 1=bA 表示的是降A大调,最后只能使用一份从百度查来的对照表来实现频率转换。(降A大调我确实没能理解什么意思,找不到音名对应的频率,欢迎各位给予指导,这里我只能用A大调来代替降A大调)
从这个表中可以看到C调的低音6(440HZ)对应A调的低音1,将对应频率列成表格:
关于 单片机内存问题
在编写播放音乐的数组这个过程中难免需要大量的存储空间,从单片机的手册中知道,RAM的存储空间只有512字节,所以在定义数组的时候需要使用
code
关键字,通过这个方式将数组存储在ROM中,如下:
unsigned char code MusicTable[][2]
关于RAM和ROM的知识可以看下面这篇博主的文章,这里我截取部分。
ROM, FLASH和RAM的区别 - 知乎 (zhihu.com)
ROM (Read Only Memory)程序存储器
ROM全称Read Only Memory,顾名思义,它是一种只能读出事先所存的数据的固态半导体存储器。ROM中所存数据稳定,一旦存储数据就再也无法将之改变或者删除,断电后所存数据也不会消失。其结构简单,因而常用于存储各种固化程序和数据。
在单片机中用来存储程序数据及常量数据或变量数据,凡是c文件及h文件中所有代码、全局变量、局部变量、‘const’限定符定义的常量数据、startup.asm文件中的代码(类似ARM中的bootloader或者X86中的BIOS,一些低端的单片机是没有这个的)通通都存储在ROM中。
RAM (Random Access Memory)随机访问存储器
RAM又称随机存取存储器,存储单元的内容可按照需要随机取出或存入,且存取的速度与存储单元的位置无关。这种存储器在断电时,将丢失其存储内容,所以主要用于存储短时间使用的程序。
它主要用来存储程序中用到的变量。凡是整个程序中,所用到的需要被改写的量(包括全局变量、局部变量、堆栈段等),都存储在RAM中。
ROM, FLASH和RAM的区别
对于RAM, ROM以及FLASH的区别,简单地说,在计算机中,RAM 、ROM都是数据存储器。RAM 是随机存取存储器,它的特点是易挥发性,即掉电失忆。ROM 通常指固化存储器(一次写入,反复读取),它的特点与RAM 相反。ROM又分一次性固化、光擦除和电擦除重写两种类型。
在应用中,常规上ROM是用来存储固化程序的,RAM是用来存放数据的。由于FLASH ROM比普通的ROM读写速度快,擦写方便,一般用来存储用户程序和需要永久保存的数据。譬如说,现在家用的电子式电度表,它的内核是一款单片机,该单片机的程序就是存放在ROM里的。电度表在工作过程中,是要运算数据的,要采集电压和电流,并根据电压和电流计算出电度来。电压和电流时一个适时的数据,用户不关心,它只是用来计算电度用,计算完后该次采集的数据就用完了,然后再采集下一次,因此这些值就没必要永久存储,就把它放在RAM里边。然而计算完的电度,是需要永久保存的,单片机会定时或者在停电的瞬间将电度数存入到FLASH里。