ARTICLE DETAIL

建站实战干货

来自一线的建站与推广经验沉淀,每一条都经过真实交付验证。

蓝牙电子琴课程设计:从51单片机驱动到HC-05串口通信

2026/9/16 17:38:44 拓冰建站 浏览量
蓝牙电子琴课程设计:从51单片机驱动到HC-05串口通信 简介基于HC蓝牙模块与51单片机的电子琴课程设计项目面向单片机初学及需完成课设、毕设的电子信息类专业学生。项目涵盖蓝牙无线通信、矩阵键盘扫描、LCD1602显示及旋律播放等核心模块并内置《反方向的钟》《天空之城》《你的名字》等多首歌曲数据可直接编译烧录演示。压缩包共44个文件、约2.49MB包含Keil工程源码.c/.h、硬件原理图与PCB设计.SchDoc/.PcbDoc、编译中间产物.obj/.lst/.hex及详细文档.md/.txt目录结构完整。已有95人学习参考项目经导师指导并通过95分评审代码运行稳定可在此基础上扩展音色、按键或增加蓝牙调参功能是入门单片机综合开发的实用范本。1. 蓝牙电子琴拿高分赢在“完整链路”而不是功能堆叠一个课程设计项目要在答辩环节拿到95分通常不是因为写了多少行代码而是评审老师能在你的项目里同时看到硬件设计、驱动开发、通信协议和应用逻辑四层东西并且每一层都能当场跑通。这个基于HC蓝牙模块和51单片机的电子琴项目正是这个套路核心是矩阵键盘读取、定时器驱动无源蜂鸣器发声、LCD1602显示状态再通过串口把HC-05蓝牙模块接进来让手机能远程点歌、切歌、播放《天空之城》《反方向的钟》这类预置曲目。对于电子信息、自动化、物联网方向的课程设计和毕业设计来说它把最基础的几个外设串成了一条完整的链路每个环节都能单独讲出原理也能连起来做现场演示。下面按“发声原理 — 硬件设计 — 人机交互 — 蓝牙通信 — 调试技巧”的顺序把整个项目拆开。2. 从发声原理到系统架构51单片机如何驱动无源蜂鸣器2.1 无源蜂鸣器发声给引脚一个对应频率的方波电子琴的声音输出器件是无源蜂鸣器它自身没有振荡源需要外部输入一定频率的方波才能发声。51单片机P1口或者P2口的引脚按固定周期翻转电平就能在蜂鸣器上产生方波。不同音符对应不同频率比如中音Do是523Hz也就是每秒输出523个完整方波周期每次引脚翻转的间隔大约是1/(2×523)≈956微秒。常见的做法是用定时器中断来精确控制翻转间隔而不是用delay()延时循环。原因很简单音乐播放期间还要同步处理按键扫描、LCD显示和串口数据主循环一旦被延时占住按键响应会变得迟滞蓝牙指令也会丢。定时器中断每次溢出时翻转一次引脚主循环只负责查频率表、往定时器里装初值两者互不干扰。2.2 定时器重装值计算从音符频率到TH0/TL051单片机定时器工作在方式1也就是16位计数模式。晶振一般是11.0592MHz一个机器周期是12个时钟周期所以定时器每计数一次需要12/11.0592MHz≈1.085微秒。要让定时器在半个方波周期后溢出需要装入的计数值是计数次数 (半周期时间) / (机器周期时间) (1000000 / (2 * freq)) / 1.085 装载值 65536 - 计数次数比如中音Do的523Hz计算过程如下// Keil C51 中计算定时器初值time 为半周期微秒数 // 晶振 11.0592MHz机器周期 ≈ 1.085us #define FOSC 11059200L #define MACHINE_CYCLE (FOSC / 12) // 每秒计数器递增次数 void SetTone(unsigned int freq) { unsigned long period_us; unsigned int reload; if (freq 0) { // freq0 表示休止符 TR0 0; // 停止定时器蜂鸣器静音 return; } period_us 1000000UL / freq; reload 65536 - (unsigned int)((period_us / 2) * (MACHINE_CYCLE / 1000000.0)); TH0 reload 8; TL0 reload 0xFF; TR0 1; }这段代码的逻辑是先根据频率算出一个完整周期的微秒数除以2得到半周期再换算成“半周期内定时器要数多少个机器周期”最后用65536减掉得到重装值。period_us / 2那一行用了浮点运算实际工程里可以把频率表预编译成TH0/TL0常量数组省掉运行时计算。改动MACHINE_CYCLE宏就可以适配12MHz等其他晶振。TIMER0中断服务函数只做一件事翻转蜂鸣器引脚电平。这样程序的其他部分即使忙碌音调也不会走调。void Timer0_ISR() interrupt 1 { TH0 tone_reload_high; // 重装初值保证频率稳定 TL0 tone_reload_low; beep_pin !beep_pin; // 翻转引脚产生方波 }2.3 系统架构HC蓝牙模块在链路里的定位整个项目从数据流角度看是三层结构输入层是4x4矩阵键盘和HC-05蓝牙模块串口控制层是STC89C52或其他51系列单片机输出层是LCD1602和无源蜂鸣器。蓝牙模块在这里的角色不是音频传输通道而是一条远程指令通道——手机通过蓝牙串口APP发送预定义好的帧格式单片机解析出音符编号或曲目编号后再走与按键完全相同的处理逻辑去驱动蜂鸣器。这也就解释了为什么HC-05在“电子琴”这个场景里比NRF24L01、ESP8266更适合HC-05不需要配对后组网手机蓝牙一搜就连串口透传的AT指令成熟稳定而且它在51单片机下的接线只有TXD、RXD、VCC、GND四根线课程设计答辩时现场演示不容易翻车。3. 原理图、PCB与源码结构硬件设计怎么搭才能过审3.1 原理图与PCB设计要点项目压缩包里的液晶屏电子琴.SchDoc和液晶屏电子琴.PcbDoc是Altium Designer格式核心器件不多STC89C52RC单片机、HC-05蓝牙模块、LCD1602液晶、4x4矩阵键盘、无源蜂鸣器、三极管驱动、自锁开关和5V稳压电路。原理图上需要注意几个细节HC-05的RXD要经过分压电阻再接单片机TXD因为蓝牙模块逻辑电平是3.3V51单片机TXD输出5V直接接会长期冲击蓝牙模块的IO口轻则通信不稳重则烧毁模块。常见做法是用两个电阻分压或者直接用1N4148二极管做电平转换。HC-05的TXD接51的RXD则不需要处理单片机的RXD引脚对3.3V高电平是兼容的。PCB布局上蜂鸣器驱动三极管基极串联1kΩ电阻蜂鸣器两端反向并联续流二极管这些都是常规要求。项目原版PCB把晶振和电容放在单片机最小系统旁边蓝牙模块通过4Pin排针引出方便烧录程序时拔掉——这个做法值得学习因为在Keil里烧录STC89C52时如果蓝牙模块占用P3.0/P3.1的串口并持续发送数据可能导致ISP下载失败。3.2 Keil工程文件结构解读源码目录里project.uvproj是Keil工程主文件main.c是程序入口LCD1602.c、Key.c、Timer0.c、Timer1.c、UART.c各自封装了对应模块的驱动。Music.h里存放的是音符定义和曲谱数据的结构体Delay.h提供基础的毫秒级延时。这种“一个外设一个.c文件”的组织方式在课程设计里非常加分评审问起来能清晰说出模块划分思路。以下是Music.h中的音符定义方式全部用宏和结构体组织阅读成本很低// 音符宏定义以频率值为基础便于直接填入曲谱 #define NOTE_C4 262 #define NOTE_D4 294 #define NOTE_E4 330 #define NOTE_F4 349 #define NOTE_G4 392 #define NOTE_A4 440 #define NOTE_B4 494 #define NOTE_C5 523 // 曲谱音符结构体0表示休止符duration单位是ms typedef struct { unsigned int freq; // 音符频率0表示休止 unsigned int duration; // 时值单位毫秒 } ScoreNote; // 示例《天空之城》片段 const ScoreNote score_sky[] { {NOTE_C4, 400}, {NOTE_E4, 400}, {NOTE_G4, 800}, {NOTE_E4, 400}, {NOTE_C4, 400}, {NOTE_D4, 800}, {0, 200}, // 休止符 };ScoreNote结构体让曲谱变得可读也方便后续用代码遍历播放。freq0专门用来表示休止符这在音乐播放里很关键否则音符之间连成一气完全听不出节奏。3.3 main.c 主循环任务轮询与模块初始化主程序采用典型的单片机前后台架构while(1)里不断轮询按键扫描和串口缓冲接收标志定时器0负责蜂鸣器方波输出定时器1负责音乐播放时每个音符的时值计时。void main() { // 初始化所有外设 LCD_Init(); // LCD1602 初始化为 8 位并行模式 UART_Init(9600); // 串口波特率 9600 Timer0_Init(); // 定时器0驱动蜂鸣器方波 Timer1_Init(); // 定时器1音乐节拍计时 Keyboard_Init(); // P1口低4位输出低电平P1口高4位输入 while (1) { Task_ScanKeyboard(); // 检测矩阵键盘并触发发声 Task_BluetoothCmd(); // 解析HC-05串口指令 Task_LCD_Display(); // 刷新LCD1602当前状态 } }这里的初始化顺序有一定讲究UART要先于Timer0和Timer1因为HC-05在模块上电后会先输出一串AT准备提示符如果串口还没初始化这些字符会丢失。Task_ScanKeyboard是轮询实现没有用外部中断是因为矩阵键盘4x4共16个键扫描周期不到2ms轮询足够实时。4. 矩阵键盘扫描与LCD1602显示人机交互的两块基石4.1 4x4矩阵键盘扫描原理与完整代码矩阵键盘节省IO的原理是用8根线排成4行4列检测时先让某一列为低电平再读行的状态逐列扫描就能确定按下的是哪个键。这个项目里矩阵键盘的16个键位8个映射为C大调一个八度的音符其余映射为暂停、切歌、音量增减和音符升降调。// 矩阵键盘扫描函数返回按键编号0表示无按键 unsigned char Key_Scan() { unsigned char row, col; // P1口低四位是列线高四位是行线 for (col 0; col 4; col) { P1 ~(0x01 col); // 把当前列拉低 for (row 0; row 4; row) { if ((P1 (0x10 row)) 0) { // 如果某行被拉低说明按键按下 while ((P1 (0x10 row)) 0); // 等待松手简单防抖 return row * 4 col; } } } return 0; }扫描逻辑的核心是“逐列拉低、逐行读取”先把P1.0置0读P1.4到P1.7看是否有位变成0如果没有就把P1.1置0再读一遍如此往复。return row * 4 col把二维坐标编码成一维编号后面查音符表时直接用这个编号做数组下标。需要说明的是while等待松手的写法适合课程设计的演示场景缺点是长按时不支持连发这一点在后面第六章会给出改进方案。4.2 音符频率对照表与按键映射十六个按键与音符的映射表如下按键编号C调音符频率(Hz)对应定时器重装值(TH0/TL0)0C4 (Do)2620xFC / 0x8C1D4 (Re)2940xFC / 0xB02E4 (Mi)3300xFC / 0xE83F4 (Fa)3490xFD / 0x064G4 (Sol)3920xFD / 0x425A4 (La)4400xFD / 0x706B4 (Si)4940xFD / 0x987C5 (高音Do)5230xFD / 0xB68升C/降D2770xFC / 0x9C9升D/降E3110xFC / 0xC410升F/降G3700xFD / 0x1C11升G/降A4150xFD / 0x5512升A/降B4660xFD / 0x8413暂停/恢复--14上一曲--15下一曲--表里的重装值是按11.0592MHz晶振、定时器0方式1计算的。用的时候直接把TH0/TL0写进寄存器即可不需要每次运行都重新算。这里把黑键也放进去是为了演示半音在实际乐曲里的作用——《反方向的钟》主歌部分就有明显的变化音没有黑键映射曲谱就无法完整复现。4.3 LCD1602显示当前音符、频率与播放状态LCD1602在这里承担的是“现场可见状态”的职责答辩时它可以让评委直观看到按键或蓝牙指令到底有没有触发成功。初始化时要按照时序先发功能设置命令0x388位数据、2行显示、5x7点阵再开显示0x0C最后清屏0x01。void LCD_ShowStatus(unsigned char note_name[], unsigned int freq) { char buffer[16]; LCD_SetCursor(0, 0); LCD_WriteString(Note: ); LCD_WriteString(note_name); LCD_SetCursor(0, 1); sprintf(buffer, Freq: %d Hz , freq); // 移除上一个音符的残留 LCD_WriteString(buffer); }LCD1602的D0-D7接在P0口需要外接10kΩ上拉排阻RS接P2.0、RW接P2.1、EN接P2.2这是最常见的接法。LCD_SetCursor(0, 0)的第一个参数是列第二个是行注意行号最大是1列号最大是15。很多人在这个接口上写反顺序结果第一行内容显示到了第二行问题又不好查。4.4 预置曲目的播放调度定时器1驱动的“按谱发声”预置曲目的播放逻辑是把Music.h里的ScoreNote数组作为播放队列由定时器1中断做时值计时。每过一个音符的duration毫秒就跳到下一个数组元素同时用新的频率重装定时器0。void Timer1_ISR() interrupt 3 { static unsigned int tick_ms 0; tick_ms 1; // 定时器1配置为1ms中断一次 if (tick_ms current_note_duration) { tick_ms 0; if (play_index total_notes) { SetTone(score[play_index].freq); // 切换频率 current_note_duration score[play_index].duration; play_index; } else { SetTone(0); // 曲目播完静音 TR1 0; // 停止计时 } } }这里把时值计数和频率切换放在中断里做而不是用延时函数循环播放好处是播放过程中矩阵键盘依然可以扫描蓝牙指令依然能被串口中断响应。比如说播放《天空之城》时按暂停键主循环里的Task_ScanKeyboard检测到暂停指令置一个play_flag标志中断服务函数里看到标志就只计时不切音实现真正的暂停。5. HC蓝牙模块串口协议设计与手机端联调5.1 HC-05与HC-06的选择差异及AT指令配置HC-05和HC-06是两种在课程设计里最常见的蓝牙串口模块两者都是CSR芯片方案但角色定位不同HC-05支持主从一体既可以被手机连接也可以主动去连接别的蓝牙设备HC-06是纯从机只能被搜索和配对。对电子琴这个项目来说手机连开发板是唯一的使用场景两者都能胜任。重点在于AT指令配置。HC-05上电前按住模块上的按键再上电才能进入AT指令模式此时模块波特率固定为38400而HC-06不需要按键直接上电就是AT模式。常见配置参数如下配置项指令说明查询版本ATVERSION?确认模块通信正常设置名称ATNAMEBluetoothPiano让手机能准确找到模块设置密码ATPSWD1234默认通常是1234部分版本是0000设置波特率ATUART9600,0,0参数1是波特率0是停止位1位无校验恢复默认ATRESET修改配置后必须执行才生效注意这里的ATUART9600,0,0第二个参数0表示停止位为1位第三个参数0表示无校验位。配置成与main.c中UART_Init(9600)完全一致否则会出现手机端发过来是01单片机收到是乱码的情况。5.2 自定义帧协议让串口数据能够被可靠解析HC-05把手机发来的数据原封不动透传到51单片机的RXD引脚但如果手机端直接发字符解析起来会出现一个很实际的问题多个控制命令之间没有边界比如连续按了“下一曲”和“暂停”单片机收到的是N P两个字符连在一起必须在主循环里做状态拆分。项目里建议用固定四字节帧格式字节含义取值Byte0帧头0xAAByte1命令字0x01播放/0x02暂停/0x03切歌/0x04音量Byte2数据音符编号或曲目编号Byte3校验前三个字节异或串口接收采用中断方式每收到一个字节存入缓冲区在主循环中检查是不是一帧完整的数据。C51中断接收的参考实现volatile unsigned char uart_buf[4]; volatile unsigned char uart_cnt 0; void UART_ISR() interrupt 4 { if (RI) { unsigned char ch SBUF; // 读取硬件接收缓冲区 RI 0; // 手动清中断标志 if (uart_cnt 0 ch ! 0xAA) return; // 非帧头字节直接丢弃 uart_buf[uart_cnt] ch; if (uart_cnt 4) { uart_cnt 0; // 校验前三个字节异或和等于第四个字节 if ((uart_buf[0] ^ uart_buf[1] ^ uart_buf[2]) uart_buf[3]) { uart_cmd_ready 1; // 置位完整帧标志主循环消费 } } } }中断服务函数只做收数据和验证帧格式两件事具体命令执行放到主循环的Task_BluetoothCmd()里避免中断服务函数执行时间过长导致定时器中断被延迟。校验方式用了最简单的三字节异或和对于课程设计已经完全够用如果要更鲁棒可以换成和校验即四字节相加后低8位为0。5.3 手机端联调步骤与踩坑点手机端推荐用“蓝牙串口”或“Serial Bluetooth Terminal”这类现成APP不需要自己写安卓应用。联调流程按下面顺序走每一步都有验证手段# 第一步确认模块进入AT模式串口调试助手发送 AT # 期望返回 OK # 第二步修改参数字符串注意空格和逗号是半角英文 ATNAMEHC05_Piano ATUART9600,0,0 ATRESET用串口调试助手接USB转TTL时要把TTL的TXD接模块的RXDTTL的RXD接模块的TXD交叉连接这是串口联调最常见的低级错误。连接成功后先不要接51单片机直接调试助手和蓝牙模块通信确认AT指令都正常再插到开发板上。实际的联调链路是手机蓝牙串口APP → HC-05蓝牙透传 → 51单片机P3.0/RXD → 串口中断收帧 → 主循环执行命令。链路每一段都可以单独验证手机和模块通信正常后用手机发送AA 01 01 00数据字节的异或和要自己算如果单片机上的LCD显示出“Play”状态而蜂鸣器响起说明整条链路已经打通。6. 从能响到好用矩阵键盘长按连发、Proteus仿真对齐与扩展技巧矩阵键盘按下发声的现有实现是“按下响应、松手结束”这对弹奏单音没问题但想弹出一个持续的长音时就尴尬了——手不能抖一抖音就断。改进方法是在定时器中断里做“长按标记累加”每次检测到按键按下时把press_ticks加1一旦超过某个阈值比如500ms就自动按等间隔触发一次按键事件这样长按时就能获得类似电子琴“延音”的效果。这个思路其实是把键盘扫描从“边沿触发”改成了“电平超时触发”改动量很小但弹奏体验提升非常明显。另一个容易被忽略的问题是Proteus仿真与实物的差异。Proteus里的HC-05模型行为与实物并不完全一致实物模块在上电瞬间会在TXD引脚输出一串乱码Proteus不会模拟这个行为。如果程序里对串口首个字节的判断不够严谨实物上电后可能出现一次误触发切歌这在仿真中根本发现不了。解决办法就是严格使用帧头过滤就像5.2节代码里uart_cnt 0 ch ! 0xAA就直接丢弃那样把上电噪声挡在协议层之外。如果想把项目再往前推一步建议把曲谱数据抽离成“窄带描述格式”。与其在Music.h里写死结构体数组不如在串口命令里定义一类0x05数据帧后面跟着“音符编号时值”交替字节让手机端可以随时下发新曲谱。这样51单片机就成了一个通用的“乐谱播放器”HC-05从单纯的远程控制通道变成了数据传输通道项目的技术层次又高了一档。{乐谱帧:AA 05 长度 音符 时值 ... 校验}这种格式设计还可以顺势引入简单的流式解析状态机把串口处理和音乐播放彻底解耦这个方向比继续堆功能更值得花时间。本文还有配套的精品资源点击获取