简介:这是一份基于HC蓝牙模块与51单片机的电子琴项目压缩包,面向单片机入门学习者、电子设计爱好者,以及想通过蓝牙无线控制方式完成综合实训的开发者。项目以51单片机为控制核心,配合HC-05蓝牙模块接收手机或平板指令,实现音符播放、音调调节等电子琴功能,适合作为课程设计或毕业设计参考。压缩包共42个文件,约2.49MB,涵盖C语言源码、Keil工程文件、硬件原理图与PCB、Hex固件、编译中间文件及说明文档;项目主体代码与蓝牙通信逻辑清晰,还附带多首歌曲的曲谱文本,便于直接下载测试与二次扩展。已有146人学习,内容结构清晰,资料完整,既能学习单片机外围电路与C编程,也能了解HC蓝牙模块的通信配置方法。无论用于毕设、课程设计还是个人兴趣开发,都可快速上手并作为综合练手项目借鉴。
1. 基于HC蓝牙模块和51单片机的电子琴到底能做什么
我第一次拿到这个标题时,第一反应不是“51能不能弹琴”,而是“这个东西的难点到底在哪里”。一块STC89C52、一个HC-05蓝牙模块、一块4×4矩阵键盘和一个无源蜂鸣器,确实能拼出一台电子琴,但它不是把手机音乐通过蓝牙放出来那么简单。手机端的音符指令要经过蓝牙串口进到51,51要把串口接收、矩阵键盘扫描和定时器发声同时在同一个程序里调度好,还要保证按键按下时声音不缺拍、蓝牙发命令时不阻塞发声。这个项目适合做课程设计的人,也适合把手头闲置蓝牙模块用起来的工程师。它真正的价值在于:用最少的硬件,把实时中断、串口通信、键扫状态机这三件事揉在一起,让51单片机完成一次从“驱动外设”到“处理事件”的跨越。
2. 基于51单片机的电子琴怎么发声:半周期翻转与查表
2.1 无源蜂鸣器的方波发声原理
电子琴的核心是可控频率的方波。51单片机没有DAC,也不像STM32那样有成熟的PWM输出,最直接的办法是:用定时器产生中断,在中断服务程序里翻转一次蜂鸣器引脚。只要翻转间隔稳定,引脚上就是频率准确的方波,无源蜂鸣器就会发出对应频率的声音。
这里的关键是“半周期”这个概念。比如C4的物理频率是261.63Hz,一个完整周期约3823微秒,半个周期约1911微秒。51单片机的定时器T0每计数一次占用一个机器周期,在11.0592MHz晶振下,一个机器周期约1.085微秒。也就是说,我把定时器初值设成能让它约1911微秒溢出一次,每次溢出后翻转P3.4引脚,最后就能得到频率在261.63Hz附近的方波。
如果用delay翻转也行,但会产生一个致命问题:按键扫描、蓝牙串口接收都会阻塞发声,按一个键时声音断断续续。所以正规做法是定时器中断翻转,主循环只负责“检测哪个键被按下”。
2.2 用11.0592MHz晶振算出一张T0重装值表
T0在方式1下是16位定时器,初值范围是0到65535。要让它在N个机器周期后溢出,重装值就是65536减去N。前面说过,N等于半个周期除以机器周期,也可以直接写成:
N = fosc / (24 × f)
其中fosc是晶振频率,f是目标音名频率,24来自“1秒内完整周期数×2个半周期×12分频”。对11.0592MHz晶振来说,分子固定是460800。
以C4为例:
N = 460800 / 261.63 ≈ 1761
重装值就是65536 - 1761 = 63775,写成十六进制是0xF91F。用同样的方法可以算出一张16键音阶表,从C4到D6,覆盖两个八度。
| 索引 | 音名 | 频率(Hz) | TH0 | TL0 |
|---|---|---|---|---|
| 0 | C4 | 261.63 | 0xF9 | 0x1F |
| 1 | D4 | 293.66 | 0xF9 | 0xDF |
| 2 | E4 | 329.63 | 0xFA | 0x8A |
| 3 | F4 | 349.23 | 0xFA | 0xD9 |
| 4 | G4 | 392.00 | 0xFB | 0x68 |
| 5 | A4 | 440.00 | 0xFB | 0xE9 |
| 6 | B4 | 493.88 | 0xFC | 0x5B |
| 7 | C5 | 523.25 | 0xFC | 0x8F |
| 8 | D5 | 587.33 | 0xFC | 0xEF |
| 9 | E5 | 659.25 | 0xFD | 0x44 |
| 10 | F5 | 698.46 | 0xFD | 0x6C |
| 11 | G5 | 783.99 | 0xFD | 0xB4 |
| 12 | A5 | 880.00 | 0xFD | 0xF4 |
| 13 | B5 | 987.77 | 0xFE | 0x2E |
| 14 | C6 | 1046.50 | 0xFE | 0x48 |
| 15 | D6 | 1174.66 | 0xFE | 0x78 |
如果手里不是11.0592MHz晶振,或者想换成别的音阶,用下面这段Python重新算表即可:
fosc = 11_059_200 notes = [261.63, 293.66, 329.63, 349.23, 392.00, 440.00, 493.88, 523.25, 587.33, 659.25, 698.46, 783.99, 880.00, 987.77, 1046.50, 1174.66] for f in notes: n = int(round(fosc / (24 * f))) value = 65536 - n print(hex(value >> 8), hex(value & 0xFF))把输出结果按顺序填进两个unsigned char数组,就能适配不同晶振。注意四舍五入造成的误差约0.06%,也就是一个音分左右,人耳基本听不出来。
2.3 矩阵键盘扫描:列线拉低、行线读回
16键电子琴用4×4矩阵键盘,比独立按键省4个IO口。P1口的低4位P1.0到P1.3接矩阵的4根列线,高4位P1.4到P1.7接4根行线。行线需要接10kΩ上拉电阻到VCC,默认读到高电平;列线由单片机输出,扫描时每次只拉低一列。
具体扫描流程是:把第0列拉低,其余列拉高,然后读P1口高4位。如果某一行也变成低电平,说明第0列与那一行的交叉点有按键按下。接着把第1列拉低,重复同样的动作,直到四列都扫完。整套扫描在几毫秒内完成,人按按键的速度远慢于扫描速度,不会感觉到延迟。
矩阵键盘的坑在“串键”。如果多个按键同时按下,扫描结果里会出现多个低电平位。简单电子琴不追求和弦,代码里只返回先扫到的那个键,也就是说它天然是单音电子琴。后续想支持双音甚至和弦,得从电路和算法两个层面重新设计,这个放到最后一章说。
3. HC蓝牙模块上电验证与矩阵键盘接线
3.1 HC蓝牙模块与51单片机的接线分配
HC-05和HC-06的引脚差别不大,核心就是VCC、GND、TXD、RXD四个脚。和51连接时,蓝牙模块的TXD接单片机的P3.0(RXD),蓝牙的RXD接单片机的P3.1(TXD),也就是交叉连接,和串口通信的基本规则一致。
这里有一个电平问题:HC系列模块的IO电平是3.3V,51单片机的P3.1在输出高电平时是5V。5V直接进RXD,模块长期工作会发热,严重时直接烧掉。常规做法是加一级分压:P3.1串一个1kΩ电阻后接模块RXD,再从模块RXD对地接一个2kΩ电阻。5V经过两个电阻分压后大约3.3V,这样电平就安全了。
完整的引脚分配表:
| 模块/外设 | 引脚 | 接51单片机 |
|---|---|---|
| HC-05/06 | VCC | 5V |
| HC-05/06 | GND | GND |
| HC-05/06 | TXD | P3.0(RXD) |
| HC-05/06 | RXD | 经1kΩ+2kΩ分压后接P3.1(TXD) |
| 矩阵键盘 | 列线C0~C3 | P1.0~P1.3 |
| 矩阵键盘 | 行线R0~R3 | P1.4~P1.7 |
| 无源蜂鸣器 | 负极经三极管 | P3.4 |
3.2 先用USB-TTL验证HC-05:AT指令与波特率陷阱
蓝牙模块如果直接接到51上出了问题,很难判断是模块坏了、波特率不对还是接线错误。所以我一般会把HC-05单独拆下来,用一个USB转TTL小板先验证。接线和接51类似:USB-TTL的TXD接模块RXD,USB-TTL的RXD接模块TXD,两者共地,VCC接5V。
HC-05和HC-06有个明显区别:HC-05支持AT指令,可以配置名称、波特率和主从模式。让HC-05进入AT模式的方法是:按住模块上的按键再上电,此时模块指示灯慢闪,串口波特率固定为38400。如果上电后指示灯快闪,说明在正常通信模式,此时发AT指令无效。
在串口助手中选择38400波特率,发送下面这些指令:
AT AT+NAME=51Keyboard AT+UART=9600,0,0 AT+ROLE=0 AT+RESET这里的AT+UART=9600,0,0含义是:设置波特率9600,停止位1位,无校验。这是最常见的配置,也是后面51固件里要用到的波特率。AT+ROLE=0表示模块作为从机,等待手机连接。
改完参数并复位后,模块回到工作模式,此时波特率才变成9600。很多人在这里卡住:AT模式下用38400发了指令,复位后忘了把串口助手切回9600,结果收不到任何数据。
3.3 矩阵键盘和蜂鸣器驱动的最小连接
矩阵键盘的引脚顺序不固定,常见模块是4个脚为行、4个脚为列,但不同板子的丝印标识可能不一样。接线前最好用万用表蜂鸣档测一下:任意按一个键,测出哪两个脚导通,再倒推行列关系。接反了也能用,只是代码里的键位映射会乱,不影响功能。
蜂鸣器这里必须用无源蜂鸣器。有源蜂鸣器内部带了振荡源,通电就响,频率基本固定,无法弹奏。无源蜂鸣器本身没有振荡源,需要外部方波驱动,正好和51的定时器翻转方案匹配。
51的IO口驱动能力很弱,直接驱动无源蜂鸣器声音很小。常见的驱动方式是用一个NPN三极管:P3.4经过1kΩ电阻接基极,发射极接地,集电极接蜂鸣器负极,蜂鸣器正极接5V。P3.4输出低电平时三极管导通,蜂鸣器有电流;输出高电平时截止。定时器中断里不断翻转P3.4,蜂鸣器两端就得到了放大的方波。代码里静音时把P3.4置高,正好对应三极管截止。
4. C51固件实现:串口中断、定时器0发声与按下事件处理
4.1 Timer0发声、Timer1做波特率:51单片机上的分工
这颗51芯片只有两个定时器,但项目里有三个需求要同时满足:定时器0做主音发声,定时器1做串口波特率发生器,主循环做键盘扫描。好在发出一个音不需要占用整个定时器,只要在中断里翻转引脚,主循环照常扫描,串口照常收发。
定时器1工作在方式2,也就是8位自动重装。这种方式在定时器溢出时,TH1会自动把初值装回TL1,不需要中断干预,适合做波特率发生器。11.0592MHz晶振下,TH1设为0xFD,SMOD为0,波特率正好9600。选择11.0592MHz晶振的目的就在这里:12MHz晶振算不出标准9600波特率,误差会超过串口容忍范围。
定时器0工作在方式1,也就是16位定时器,用于发声。它只在键按下时才启动,松开后停止,不会空转浪费CPU时间。
代码里有两个关键状态:一个是ongoing,表示当前正在发声的音符索引加1,0表示静音;另一个是last_key,保存上一次扫描到的键值,用来识别“按下事件”。每次按键从无到有,只触发一次note_on,按键按住不放不会重复触发。
4.2 可直接编译的C51代码
下面这份代码用Keil C51编译,芯片选STC89C52RC或AT89S52均可。蓝牙协议在手机上用任意“蓝牙串口”工具发送,字符“1”到“7”对应C4到B4,“a”到“i”对应C5到D6,字符“0”表示静音。
#include <reg52.h> #include <intrins.h> #define NOTE_OFF 0xFF sbit BEEP = P3^4; // 11.0592MHz晶振,T0方式1,C4~D6 unsigned char code th_tab[16] = { 0xF9,0xF9,0xFA,0xFA,0xFB,0xFB,0xFC,0xFC, 0xFC,0xFD,0xFD,0xFD,0xFD,0xFE,0xFE,0xFE }; unsigned char code tl_tab[16] = { 0x1F,0xDF,0x8A,0xD9,0x68,0xE9,0x5B,0x8F, 0xEF,0x44,0x6C,0xB4,0xF4,0x2E,0x48,0x78 }; unsigned char ongoing = 0; // 0=静音,1~16=当前响的音符 unsigned char last_key = NOTE_OFF; unsigned char uart_buf; unsigned char uart_flag; void delay_ms(unsigned int ms) { unsigned int i, j; for (i = 0; i < ms; i++) { for (j = 0; j < 120; j++); } } void note_on(unsigned char n) { TR0 = 0; // 先停定时器,避免切换瞬间杂音 BEEP = 1; // 静音电平 if (n >= 16) return; TH0 = th_tab[n]; TL0 = tl_tab[n]; TR0 = 1; // 启动发声 ongoing = n + 1; } void note_off(void) { TR0 = 0; BEEP = 1; ongoing = 0; } void timer0_isr(void) interrupt 1 { if (ongoing == 0) { // 理论不会进来,做保护 TR0 = 0; BEEP = 1; return; } TH0 = th_tab[ongoing - 1]; TL0 = tl_tab[ongoing - 1]; BEEP = !BEEP; // 半周期翻转一次 } unsigned char scan_matrix(void) { unsigned char col, row, i, key = NOTE_OFF; for (col = 0; col < 4; col++) { P1 = (P1 & 0xF0) | (~(0x01 << col) & 0x0F); _nop_(); _nop_(); row = (P1 >> 4) & 0x0F; if (row != 0x0F) { for (i = 0; i < 4; i++) { if ((row & (0x08 >> i)) == 0x00) { key = (i << 2) + col; break; } } break; } } P1 |= 0x0F; // 恢复所有列线为高 return key; } void uart_isr(void) interrupt 4 { unsigned char ch; if (RI) { ch = SBUF; RI = 0; if (ch >= '1' && ch <= '7') { uart_buf = ch - '1'; // '1'~'7'对应音0~音6 uart_flag = 1; } else if (ch >= 'a' && ch <= 'i') { uart_buf = ch - 'a' + 7; // 'a'~'i'对应音7~音15 uart_flag = 1; } else if (ch == '0') { uart_buf = NOTE_OFF; uart_flag = 1; } } } void hardware_init(void) { TMOD = 0x21; // T0方式1,T1方式2 TH1 = 0xFD; TL1 = 0xFD; TR1 = 1; // 启动波特率发生器 SCON = 0x50; // 串口方式1,允许接收 PCON = 0x00; // SMOD=0 ES = 1; ET0 = 1; P1 = 0x0F; BEEP = 1; EA = 1; } void main(void) { unsigned char key; hardware_init(); while (1) { key = scan_matrix(); // 无键按下时,串口命令生效;“0”静音命令始终生效 if (uart_flag) { uart_flag = 0; if (key == NOTE_OFF || uart_buf == NOTE_OFF) { key = uart_buf; } } if (key != last_key) { // 边沿触发,不是电平触发 if (key == NOTE_OFF) note_off(); else note_on(key); last_key = key; } delay_ms(5); // 键盘扫描周期约5ms } }4.3 volatile标志位与按键事件处理逻辑
代码里最值得琢磨的是主循环事件合并那部分。scan_matrix每5毫秒扫一次键盘,串口中断则随时可能收到手机命令,两个事件源会竞争同一个发声通道。
处理规则是:键盘一直优先。只要键盘上有键按住,蓝牙发来的其他音符命令就忽略;只有蓝牙发来的“0”静音命令可以打断键盘。如果手机端想完整弹奏,就不要同时碰矩阵键盘。这个取舍在课程设计和工程演示里都够用,也让代码逻辑保持简单。
还有个细节:uart_flag在串口中断里被置1,主循环里被清0。中断里只做最轻量的事——把字节存入uart_buf、置标志位,不做重装定时器,也不调用任何复杂函数。51的中断嵌套不深,中断服务程序里一旦调用delay这类阻塞函数,整个系统就卡住了。这是51编程的基本纪律。
定时器0的中断服务程序里,ongoing用“当前音索引+1”表示,而不是直接用索引,是为了让0能表达“静音”这一状态。这样note_off只需要把ongoing清0,中断即使晚一步进来,也能通过判断主动关闭并静音,不会出现蜂鸣器引脚悬在某个电平上一直响的情况。
5. 电子琴调音、串键、断连排查与进阶改法
5.1 音准排查:换晶振就要换表
给蜂鸣器调音最直接的办法是手机下载一个调音器App,让蜂鸣器长按某个键,看App显示的频率是偏高还是偏低。如果所有音都整体偏高,常见原因是晶振频率标称11.0592MHz,实际振荡在11.2MHz以上,或者电容负载导致偏差。这时候不需要改代码,把2.2节Python脚本里的fosc换成实际频率,重新生成表格。
如果只有个别音不准,多半是表格里的四舍五入出了问题。注意表格里相邻两个音相差约一个全音或半音,比如E4到F4只差约0.06%,人耳对此不敏感,真正要警惕的是把11.0592MHz的表套用在12MHz晶振上,那会直接导致整体跑偏将近半个音。
5.2 矩阵键盘串键、HC蓝牙模块断连的检查顺序
矩阵键盘串键先查硬件:行线如果忘了接上拉电阻,P1口高四位在悬空时会被干扰,扫出来的键值随机跳变。上拉电阻选10kΩ稳定,不要用1kΩ,否则功耗大且驱动能力不匹配。
还有一个隐蔽的坑:矩阵扫描代码里P1 = (P1 & 0xF0) | ...是读-修改-写操作,读的是P1锁存器,不是引脚。如果前面某次输出0后没有恢复为1,下一次扫描会把列线状态搞乱。代码里每轮结束时有P1 |= 0x0F,这个恢复动作不能删。
蓝牙断连时按顺序查四件事:模块供电是不是5V,TXD和RXD有没有交叉,电脑串口助手的波特率是38400还是9600,以及HC-05是否处于AT模式。大多数“收不到数据”的问题都出在波特率,而不是模块损坏。
5.3 再往前走:DDS移调、双音与MIDI改法
想让电子琴支持黑键和任意移调,可以把这套“频率查表”改成“DDS增量累加”方案。在固定中断频率下,每次中断累加一个和音符频率成正比的增量,累加器溢出时翻转一次引脚。这样音符频率可以做到很高精度,移调只需要改增量值,不需要维护多张表。
如果想支持双音,51的两个定时器就捉襟见肘了。常见做法是让Timer0继续发主音,Timer1在空闲时发副音,两个音不能真正同时响,只能快速轮流驱动蜂鸣器,利用余响让人耳感觉像双音。效果有限,但课程设计够用。
更工业化的方向是让蓝牙模块走MIDI协议,手机端用MIDI键盘App发送标准MIDI消息,单片机解析0x90开音和0x80关音事件,再把note编号映射到频率表。这个改法把矩阵键盘省掉,也把“输入源”从自定义字符协议换成通用协议,以后接电脑DAW都不需要改硬件。整体难度比自定义串口协议高一个级别,但做完之后,这个基于HC蓝牙模块和51单片机的电子琴才算真正从“能响”变成“能演奏”。
本文还有配套的精品资源,点击获取