51单片机 《只因你太美》实现

510 阅读16分钟

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博客

只因你太美_简谱_搜谱网 (sooopu.com)

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的点阵屏,如下图

屏幕截图 2024-03-21 155552.png

理论知识

江协科技使用的是12MHZ的晶振,而作者的开发板是11.0592MHZ的晶振,所以在周期方面需要调整。

机器周期和时钟周期的区别

屏幕截图 2024-04-18 182747.jpg

单位转换关系

屏幕截图 2024-04-18 182849.jpg

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

乐理知识

键盘与音符对照

屏幕截图 2024-04-19 103848.jpg

音符时值

音符名称-写法--时值-
全音符6 - - -四拍
二分音符6 -两拍
四分音符6一拍
八分音符6半拍
十六分音符(数字下面加两条横线)1/4拍

简谱

以最快的音符时长作为基准,也就是十六分音符作为单位

其中这一首谱子的四分音符=107

也就是每分钟演奏107个四分音符 107(个) / 60(s) = 1.78(个/s) 1(个) / 1.78(个/s) = 0.56(s)

运算之后就是每560ms一个四分音符

(我代码只实现到了第一大段,也就是“迎面走来的你让我如此蠢蠢欲动“之前)

屏幕截图 2024-04-19 104659.jpg

EXCEL表格

屏幕截图 2024-04-19 140647.jpg

屏幕截图 2024-04-20 184946.jpg 查阅资料之后(百度,GPT等,说实话我也没太理解),简谱上 1=bA 表示的是降A大调,最后只能使用一份从百度查来的对照表来实现频率转换。(降A大调我确实没能理解什么意思,找不到音名对应的频率,欢迎各位给予指导,这里我只能用A大调来代替降A大调)

从这个表中可以看到C调的低音6(440HZ)对应A调的低音1,将对应频率列成表格:

屏幕截图 2024-04-20 190038.jpg

关于 单片机内存问题

屏幕截图 2024-04-19 164912.jpg 在编写播放音乐的数组这个过程中难免需要大量的存储空间,从单片机的手册中知道,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里。