Arduino+蓝牙模块+LCD屏:手机无线文本显示实战与避坑指南
前阵子朋友问我能不能用蓝牙把手机里的内容发到一块小屏幕上做个像模像样的“电子留言板”我想了想这不就是一块 Arduino、一个蓝牙模块、一块 LCD 屏的组合嘛。接线图网上随便搜都有一大堆但真拿到手就会发现配对不上、乱码、显示错位、供电不足重启……一堆问题等着你。索性把这次从零搭到跑通的完整过程写出来顺便把那些没人告诉你的坑也一并填了。这篇主要解决三个问题怎么把蓝牙收发的数据正确接到 Arduino 上、怎么解析出一行干净可读的内容、怎么让 16 列 I2C 屏优雅地把长内容展示出来。无论你是第一次摸 Arduino 的纯新手还是想拿这套组合做个快速原型的老手都可以直接对着下面的步骤抄作业。1. 项目到底在解决什么问题手机到屏幕的“最后一跳”先把这个项目的定位说清楚。它本质上是一个低成本无线数据可视化终端手机或其他蓝牙设备作为数据源通过无线链路把文本信息发送出去Arduino 作为接收端把数据解析出来再驱动 LCD 屏完成显示。听起来简单但拆开来看这里面其实有四个独立的环节每个环节都可能成为整个链路的“断点”发射端手机上的蓝牙调试助手、自写的 APP 或者其他蓝牙透传设备负责把文本字符串按字节发送出去。传输链路蓝牙模块HC-05/HC-06/JDY-31 等在中间做的是“无线转串口”的工作也就是所谓的透传。手机发来的数据模块从蓝牙协议栈里解出来然后从 TX 引脚按串口协议一位一位地送出去。MCU 接收端Arduino 通过串口硬件串口或 SoftwareSerial 模拟串口读取这些字节按预定的协议帧把它们拼装成完整的消息。显示端LCD 屏接收 MCU 的控制指令把消息渲染在人眼可见的 16×2 或 20×4 字符点阵上。我之所以要专门把这“最后一跳”拎出来说是因为很多人在这个项目上栽跟头并不是某个环节的技术有多难而是没搞清楚数据在这条路径上是什么形态。手机发出去的是字符串但蓝牙模块解出来的是一串不保证边界的数据流——它不知道这一帧消息到哪里结束下一帧从哪里开始。如果你只是简单地在 Arduino 的 loop 里读串口、读到啥就显示啥那至少会遇到三类问题半包问题一帧消息超过几十字节时蓝牙的底层传输会把数据拆成多个包Arduino 一次 read 只能拿到一部分直接显示就会看到半行字。粘包问题两条消息连续发送时由于没有帧间隔Arduino 可能把它们当成一条长消息处理。脏数据问题模块上电瞬间、AT 命令回显、调试信息等都可能混入数据流。所以这个项目的核心难点不在于“把线接对”而在于给串口数据定义一套可靠的帧格式让接收端能准确切分出每条完整消息。下面的内容就围绕这个主线展开。2. 硬件选型的一个关键分岔路HC-05 还是 HC-06LCD 选 I2C 版还是并口版2.1 蓝牙模块不同型号的“性格”淘宝上最常见的蓝牙透传模块有两种HC-05 和 HC-06。另外近年 JDY-31 和 HM-10BLE 4.0也逐渐流行。放在这个项目里我认为选择的核心依据是你需不需要配置主从角色和 AT 指令深度定制。HC-05主从一体模块可做主、可做从支持比较完整的 AT 命令集。这意味着如果你希望“开发板主动去找手机连接”而不是等手机来连它就必须选 HC-05。它的缺点也很明显模块体积稍大默认波特率是 9600初次上电时状态引脚的电平处理容易让人迷惑。HC-06纯从机模块出厂固件只允许手机主动连接它。优点是稳定、便宜、配置简单缺点是你无法让 Arduino 主动发起连接也只能通过 AT 指令调整波特率和配对码改不了角色。HM-10BLE 4.0 低功耗模块用 Nordic 的芯片。如果你的项目将来要跑在纽扣电池上或者需要和微信小程序、iOS 的 CoreBluetooth 打交道HM-10 是更现代的选择。但它传输的不是标准串口透传那么简单需要按 BLE 的 GATT 特征值写数据对新手并不友好。JDY-31这年头非常流行的低成本替代品同样走 AT 指令工作在经典蓝牙 SPP 协议上透传体验和 HC-05 几乎一致。它的优势之一是用 USB 转 TTL 模块直接连电脑就能配置不需要像 HC-05 那样额外接 enter 按键进入 AT 模式。就“Arduino 蓝牙 LCD 显示手机信息”这个需求而言我建议新手直接选HC-06 或 JDY-31原因很简单你只需要“等手机过来连”不需要“板子去找手机”。这种情况下主从一体纯粹是浪费。我更倾向于 JDY-31因为它的 AT 配置体验比 HC-06 顺畅而且透传模式下丢包率在实测中更低。2.2 如果 LCD 带 I2C 适配器你的烦恼能少一半LCD 屏采取两个主流方案原生并口的LCD160216 字符×2 行以及带有PCF8574/I2C 转接板的版本。我的建议非常明确能选 I2C 就绝不选并口。原因如下并口版本至少需要 6 个 GPIORS、E、D4-D7如果再加上蓝牙模块的 RX/TXArduino Uno 的 IO 会被占掉一大半后续想加 LED、蜂鸣器、按键都得抠引脚。并口连线多接触不良的概率呈指数上升。杜邦线松一根显示就出现乱码排查起来极其痛苦。I2C 版本只占用 SDAA4、SCLA5两根线加电源LCD 本身自带 PCF8574 芯片MCU 端只需要调用LiquidCrystal_I2C库就能完成所有操作。唯一需要留神的是I2C 地址。绝大多数 PCF8574 模块的地址是0x27但也有部分是0x3F。第一次接上之后如果屏幕什么都没显示用 I2C 扫描程序跑一遍就能确认不要直接怀疑屏坏了。如果你手头只有并口屏也不是不行后面我会给一套并口版本的接线和代码。只是从“少踩坑”的角度I2C 版实在是太省心了。3. 实际接线与电平匹配99% 的新手都会忽略 RX/TX 交叉和 3.3V 逻辑电平接线是这个项目最容易出错、但又是很多人最不爱细看的部分。先说最核心的两个原则原则一收发交叉。蓝牙模块的 TX 要接 Arduino 的 RX接收端蓝牙模块的 RX 要接 Arduino 的 TX发送端。平时模块上标着 TX/RX很容易让人想当然地“TX 对 TX、RX 对 RX”直连——那叫环回自己跟自己对上了数据根本进不到 MCU。原则二留意电平区间。HC-05、HC-06 的串口逻辑电平是3.3V不是 5V。Arduino Uno 的引脚是 5V 逻辑。如果你把 Arduino 的 TX5V 高电平直接怼到蓝牙模块的 RX3.3V 逻辑短期看不出问题但长期会给模块内部的电平转换电路带来额外压力。稳妥方案是用一个 1kΩ 2kΩ 的分压电阻把 Arduino TX 输出的 5V 降到 3.3V 左右再接模块 RX。模块的 TX 到 Arduino RX 通常可以直接连因为 3.3V 的高电平已经能稳定被 Arduino 识别。如果你用的是 JDY-31它的串口逻辑电平往往是 3.3V 兼容多种输入但同样不建议长期 5V 直灌。下面是 I2C 液晶屏方案的完整接线表蓝牙模块引脚Arduino Uno 引脚说明VCC5V多数蓝牙模块支持 5V 供电内部带稳压GNDGND必须共地否则串口电平无参考TXRXPin 0或 SoftwareSerial RX模块发数据给 ArduinoRXTXPin 1或 SoftwareSerial TX经分压 3.3V 接入Arduino 可发 AT 指令、也可透传数据I2C LCD 引脚Arduino Uno 引脚说明VCC5V背光和逻辑均用 5VGNDGND与蓝牙模块共地SDAA4I2C 数据线SCLA5I2C 时钟线有一种特殊情况需要注意如果你用蓝牙模块的 TX 接到了 Arduino 的 Pin 0硬件串口 RX那么烧录程序时要先拔掉蓝牙的 TX 线。因为 Arduino IDE 烧录时也需要占用硬件串口蓝牙模块如果还连在 RX 上会把 AT 指令回显或者噪声混入引导加载过程导致烧录失败或者烧录成功了但程序莫名复位。这一点经常让人反复折腾很久却不知道是接线惹的祸。所以更推荐的方式是给蓝牙模块单独分配一对软串口引脚。比如Arduino Pin 2 → 蓝牙模块 RX经分压到 3.3VArduino Pin 3 → 蓝牙模块 TX软件里用SoftwareSerial 或 AltSoftSerial把它们做成串口 2。这样做的好处是烧录程序时完全不用动接线硬件串口留给 USB 调试两者互不干扰排错时还能同时在串口监视器里看日志。缺点只有一个SoftwareSerial 在高速率下不够稳定但我们的项目只跑 9600 波特率完全够用。下面是一份可以直接抄的接线清单软串口方案蓝牙 VCC 接到 Arduino 5V。蓝牙 GND 接到 Arduino GND。蓝牙 RX 串联一个 1kΩ 电阻后再接 Arduino Pin 2在蓝牙 RX 端与 GND 之间并一个 2kΩ 电阻形成分压。蓝牙 TX 直接接 Arduino Pin 3。I2C LCD 的 VCC/GND 分别接 Arduino 5V/GNDSDA 接 A4SCL 接 A5。接完之后建议先用万用表量一下蓝牙 RX 引脚的电压在 Arduino 上电状态下通电后它应该约等于 0V空闲态发送数据时才会有 3.3V 左右的脉冲。如果量到 5V说明分压有问题立刻检查电阻连接。4. 软件部分的核心帧协议设计、串口读取与 LCD 渲染4.1 帧格式让数据有始有终串口是一个字节一个字节流的接收端需要知道到什么地方才算是“一条完整消息”。我在这类项目里习惯用非常朴素的帧头 帧尾方案帧头0xAA也就是ª的字节码帧尾0x55U有效载荷UTF-8 编码的文本字节手机端发送时把字符串封装成AA 文本内容 55的形式。Arduino 端不断在串口缓冲区里查找AA帧头一旦找到就开始把后续字节写入临时数组直到遇到55或长度上限为止。这样设计简单实用几乎不会出现切分错乱而且每一帧对应一条消息天然规避了粘包问题。为什么不直接用换行符\n当分隔符因为换行符在很多蓝牙调试助手里会触发“发送新行”的行为容易把数据帧切碎而且 LCD 显示的多行文本里也可能会包含换行用来做帧分隔不够干净。4.2 核心代码读取、拼包、渲染三步走下面是一份基于 I2C 屏和软串口的完整例程注释都放在关键位置。波特率使用9600这是 HC-06 和 JDY-31 的默认值也兼顾了软串口的稳定性#include Wire.h #include LiquidCrystal_I2C.h #include SoftwareSerial.h // 初始化 1602 屏地址按实际扫描结果改 LiquidCrystal_I2C lcd(0x27, 16, 2); // 软串口RX 用 Pin 3蓝牙 TX 接这里TX 用 Pin 2蓝牙 RX 接这里 SoftwareSerial bluetooth(3, 2); #define FRAME_HEAD 0xAA #define FRAME_TAIL 0x55 #define MAX_MSG_LEN 80 // 接收状态机变量 boolean inFrame false; char msgBuffer[MAX_MSG_LEN]; int msgLen 0; void setup() { Serial.begin(9600); // 硬件串口调试用 bluetooth.begin(9600); // 蓝牙串口 lcd.init(); // 初始化液晶 lcd.backlight(); // 打开背光 lcd.clear(); lcd.setCursor(0, 0); lcd.print(Bluetooth Ready); } void loop() { while (bluetooth.available() 0) { byte b bluetooth.read(); // 状态机寻找帧头 if (!inFrame) { if (b FRAME_HEAD) { inFrame true; msgLen 0; } continue; } // 已收到帧头开始收正文 if (b FRAME_TAIL inFrame) { // 收到帧尾一帧结束立即显示 msgBuffer[msgLen] \0; displayMessage(msgBuffer); inFrame false; continue; } // 保护防止超长数据造成数组溢出 if (msgLen MAX_MSG_LEN - 1) { msgBuffer[msgLen] (char)b; } else { // 帧太长丢弃这一帧重置状态 inFrame false; } } } void displayMessage(char *msg) { lcd.clear(); int len strlen(msg); int line1Len len 16 ? 16 : len; char line1[17]; memcpy(line1, msg, line1Len); line1[line1Len] \0; lcd.setCursor(0, 0); lcd.print(line1); if (len 16) { // 超出一行显示第二段 int line2Len len - 16 16 ? 16 : len - 16; char line2[17]; memcpy(line2, msg 16, line2Len); line2[line2Len] \0; lcd.setCursor(0, 1); lcd.print(line2); } else { // 不足一行的情况第二行留空 lcd.setCursor(0, 1); lcd.print(); } }这里面的状态机思想值得多说一句。它的作用是把“持续到来的字节流”切分成“一条条完整帧”。每来一个字节都问自己两个问题我现在在不在帧里这个字节是不是结束符这种设计比“攒够一个字节就立即显示”要稳得多强烈建议你在所有串口类项目中都套用。4.3 超过 16 个字符怎么办滚动显示或分屏1602 屏一行只有 16 列两行最多显示 32 个字符。如果手机端发来一条 50 字的短信上面这段代码会丢弃超过 32 的部分。更友好的做法是加一个轮播滚动逻辑第一次收到消息先显示第 1 行前 16 个字符、第 2 行后 16 个字符。按下按键或每隔 3 秒整体上下/左右移位一次显示下一段。要实现滚动基线思路是在displayMessage里把完整消息存到一个全局字符串messageStore[128]再由一个UpdateDisplay()函数负责管理当前偏移量每次从messageStore offset取两个 16 字节的片段到 LCD。配合millis()做无阻塞延时效果就像电子广告牌一样。这段代码我放在进阶部分先用上面的静态版本把通路跑通再说。4.4 手机端到底发什么串口调试助手的正确使用姿势用手机发送数据时多数人默认会用“经典蓝牙串口调试工具”这类 App。连接配对后新建连接在输入框里写纯文本然后点击发送。要注意不要勾选“自动添加换行”或者只把\n当普通字符发送否则它会在你的帧尾 0x55 之外再塞一个 0x0A程序会把这个 0x0A 当成正文字节存下来显示时可能会在 32 字符处制造一个换行残影。发送内容按我们约定的格式例如AAHELLO WORLD55但注意在 App 里输入的是 ASCII 字符 A A不是十六进制字节 0xAA。如果你直接在输入框里打字AAHELLO WORLD55Arduino 收到的第一个字节是0x41字符 A而不是0xAA。程序会一直等帧头等不到什么都不显示。正确做法有两种方法一在 App 的十六进制发送模式里输入AA 48 45 4C 4C 4F 55对应AAHELLO的十六进制码。方法二改程序把帧头定义为两个普通可见字符比如##这样手机端直接发##HELLO55就能被识别。这种字符型帧头对新手更友好也更容易调试。我实际项目里推荐用字符型帧头因为不少蓝牙调试助手的十六进制模式界面很别扭而且容易误操作。帧头和帧尾也可以改成和只要收发两端一致就行。下面把代码里的帧头帧尾定义改一下即可#define FRAME_HEAD [ #define FRAME_TAIL ]手机端就直接发[HELLO WORLD]干净直观收到后程序会把方括号之间的文字显示在屏上。这种思路也同样适用于更复杂的类 JSON 协议只是在 Arduino 上解析 JSON 开销较大、不推荐。5. 实测中最容易翻车的四个问题与完整排查链路这一节是我最想让你认真看的部分。我把自己和周围朋友做这类项目时踩过的坑整理成清单每个问题都给出从现象到根因再到方案的完整链路。5.1 手机搜不到蓝牙模块现象手机蓝牙扫描列表里一直刷不出 HC-06 或 JDY-31。排查链路先确认模块有没有进入可被发现模式。HC-06 上电即进入可配对状态指示灯一般快闪HC-05 则需要先按模块上的小按键或者通过 AT 指令把模块设为“允许配对”模式。如果指示灯是慢闪约 2 秒一次它很可能没在广播。常见根因VCC 接错了引脚。有些模块标的是 5V 输入、板上带 LDO但有些廉价板子只有 3.3V 稳压管5V 直接灌进去会发热甚至烧毁。先摸一下模块温度很烫基本就是供电问题。解决方案换一个 3.3V 或 5V 供电试试注意区分型号。实测中 JDY-31 的默认广播名是JDY-31HC-06 通常是HC-06HC-05 是HC-05这些基本都是直接可见的。5.2 连上了但 LCD 显示乱码 / 间隔性乱码现象手机发送简单字符串后屏上出现“╪╫╪”“烫烫烫”或半个汉字乱码。排查链路先不经过 Arduino直接把蓝牙模块 TX 接到 USB 转 TTL 模块的 RX用电脑串口调试助手看原始字节。如果电脑端也乱码问题在蓝牙模块的波特率与当前串口调试助手波特率不一致。如果电脑端正常问题就在 Arduino 这端。常见根因一波特率不匹配。模块出厂默认 9600但有人买到了被改成 38400 或 115200 的模块。ATM 指令把它改回来即可连接模块先把 RX 悬空或接低电平进入 AT 模式发ATBAUD4HC-05 表示 9600或ATBAUD9600JDY-31。常见根因二软串口时序丢 bit。SoftwareSerial 在 9600 波特率下通常没事但如果你的代码里在displayMessage()里用了delay()这期间蓝牙 RX 引脚上的数据就会积压再被读取时容易错位。改法是把显示逻辑改为非阻塞或者把蓝牙挪到硬件串口Pin 0/1把调试信息挪到软串口。解决方案规范帧协议 把显示改成定时任务是根治乱码的最好组合。波特率统一 9600。5.3 LCD 无显示只有背光亮现象背光亮了但屏幕上没有任何字符。排查链路先用 I2C 扫描代码确认地址。常见问题是默认0x27不等于实际地址0x3F代码里初始化的地址不对屏幕就不会响应。常见根因二对比度电位器没拧对。虽然 I2C 版的对比度由背后电位器控制出厂设置一般能看到字但如果你买到的模块电位器被拧到了极端位置也会出现白屏或全黑方块。解决方案用万能 I2C 扫描程序打印设备地址再把地址填进LiquidCrystal_I2C lcd(addr, 16, 2)。如果扫描到的是 0x3F 而你写的是 0x27那当然什么都显示不了。5.4 Arduino 烧录程序时反复失败现象点上传之后 IDE 一直报“avrdude: stk500_recv(): programmer is not responding”。排查链路如果蓝牙模块的 TX 连在 Pin 0硬件 RX上先拔掉这根线再烧录。如果还是失败断掉所有外部供电只保留 USB 线看看能否上传成功。常见根因外部 5V 电源和 USB 同时给 Arduino 供电形成了压差竞争导致复位不稳定。解决方案养成习惯——烧录之前把所有接在 RX/TX 上的外设断开烧完再插回去。或者干脆用软串口方案硬件串口留给烧录与调试。6. 进阶玩法从“显示一条消息”到“带时间戳的滚动留言板”基础通路跑通后这个项目的潜力其实比想象中大不少。下面展开三个我认为最实用、也最容易扩展的方向。6.1 加入 DS3231 实时时钟显示消息接收时间如果你只是想显示一条手机发来的文本不需要时钟。但如果要做留言板或通知屏接收时间是很重要的信息。用 DS3231 模块可以做到收到每条消息后把YYYY-MM-DD HH:MM写到第一行把消息正文滚动显示在第二行。DS3231 走 I2C 总线和 LCD 一起用完全不冲突。接线只需再并一组VCC/GND/SDA/SCL代码里用RTClib库读时间即可。实现时要注意DS3231 的 I2C 地址是0x68和 PCF8574 的0x27不冲突所以可以直接挂同一条总线。收到帧后先不急着清屏而是读取当前时间、拼成一行再把消息正文放入滚动区。6.2 自制手机 App用 BLE 或经典蓝牙发送自定义协议如果你不满足于调试助手想自己写个小 App 来控制显示路径上有两个分岔经典蓝牙 SPPAndroid 的BluetoothSocket写起来不算复杂用UUID 00001101-0000-1000-8000-00805F9B34FB就能建立串口通道。这种方式和 HC-05、HC-06、JDY-31 完全兼容。iOS 对经典蓝牙 SPP 支持很差基本只能走 MFi 认证。BLE 4.0用 HM-10 模块 iOS 原生CoreBluetooth或 Android 的BluetoothGatt。只需写自定义 Service 和 Characteristic手机端把字符串写入 Write Characteristic模块解包后从串口吐出来。优点是低功耗、iOS 原生支持缺点是调试链路长一些。做 App 的时候最容易被忽略的是数据分帧手机端发送时也最好按[消息内容]的格式封装一帧再写入 socket避免半包、粘包。不要把“分帧”的责任全抛给 Arduino。6.3 多屏联动与远程消息中转更进一步可以把这套显示终端接到一个本地网关树莓派或 PC上网关通过 Wi-Fi 接收云端消息再通过蓝牙下发到 Arduino。这样你就拥有了一个真正意义上的“IoT 留言板”手机在任何地方发一条消息到云端屋里的 LCD 就自动显示。网关端只需要几行 Pythonimport bluetooth import time sock bluetooth.BluetoothSocket(bluetooth.RFCOMM) sock.connect((XX:XX:XX:XX:XX:XX, 1)) # 换成 HC-06 的 MAC while True: msg input( ) framed [ msg ] sock.send(framed.encode()) time.sleep(0.5) sock.close()这是一个很有代表性的“链路打通”思路无线感知层蓝牙 计算层Arduino 展示层LCD各司其职。以后你想做一个“门铃联动屏”或“空气质量通知屏”都是在这个骨架上替换传感器和显示内容而已。7. 我个人踩过几次坑之后的体会这套组合做下来我最深的感受是串口项目的难点从来不在“接线”和“API 调用”而在“数据边界”。你把帧协议定义清楚、把状态机写好、把共地接牢大概率一两小时内就能跑通。反过来如果一上来就去查“为什么乱码”很容易陷入盲人摸象——波特率、电平、供电、干扰都可能产生类似的现象。这里再留一个小技巧调试期间把 Arduino 的硬件串口接上 USB 线在 IDE 的串口监视器里开 9600然后在displayMessage()里同时把消息打印到硬件串口。这样你在电脑上能看到 Arduino 到底收到了什么、解析出了什么与 LCD 上的内容一对照问题出在传输层还是显示层立刻一目了然。这比你在 LCD 上瞎猜要高效得多。如果你已经按上面的方法跑通了下一步建议去改一版滚动显示把四条消息存进数组里用按键换页。等到你把这些都玩熟再回头看这个“Arduino 蓝牙 LCD”组合你会发现它其实就是一个微缩版的物联网显示终端任何数据源都可以往这个接收端里塞。到那时候你需要的可能就不是一块 1602 屏而是一块 OLED、一个触摸屏甚至一个网页仪表盘了。