基于状态机的Arduino蓝牙控制:从HC-06模块到稳定嵌入式系统设计 📅 2026/8/19 7:15:31 1. 项目缘起一个被低估的蓝牙控制方案最近在整理工作室的旧项目时翻出了一个尘封已久的HC-06蓝牙模块和一块Arduino Uno。看着它们我突然想起几年前一个非常经典但常被新手忽视的应用场景用手机蓝牙通过一个清晰、健壮的状态机FSM逻辑去远程控制一个简单的硬件系统。这个想法听起来简单但实际做起来很多人会直接写一堆if-else判断串口数据代码很快就变得难以维护和扩展。今天我就想结合这个“Simple FSM example with HC-06”的标题从头到尾拆解一遍如何用状态机的思想构建一个稳定、可读、易扩展的蓝牙控制程序。这不仅仅是让一个LED灯闪烁更是教你一种应对复杂逻辑的编程思维方式特别适合那些希望自己的Arduino项目能从“玩具级”迈向“产品级”雏形的朋友。HC-06作为最经典、最廉价的蓝牙串口透传模块其核心价值在于将复杂的蓝牙协议栈简化为一个普通的串口。对Arduino来说你只是在和Serial1或通过软串口通信这大大降低了入门门槛。而状态机则是管理项目逻辑的利器尤其当你的设备需要响应多种不同的命令并在不同命令间切换模式时——比如一个智能灯需要区分“开关”、“调色”、“亮度渐变”等不同状态。我们将用最基础的Arduino Uno平台不依赖任何复杂的库从接线开始到状态机理论再到一行行代码实现和调试技巧完整呈现这个过程。你会发现理解了这套方法后你可以轻松管理更复杂的项目比如蓝牙小车、智能家居中控等。2. 硬件准备与核心原理解析2.1 硬件清单与连接图这个项目的硬件部分极其精简目的是让注意力集中在逻辑和编程上。你需要准备以下材料Arduino Uno 开发板 x1项目的主控核心。HC-06 蓝牙串口模块 x1确保是主从一体的HC-06而非需要配对的HC-05。LED 发光二极管 x1作为被控对象任何颜色均可。220Ω 电阻 x1用于限流保护LED和Arduino引脚。杜邦线 若干用于连接。USB 数据线 x1为Arduino供电和下载程序。智能手机 x1安装任意串口蓝牙调试APP如“Serial Bluetooth Terminal”。连接方式遵循“电源共地、信号直连”的原则。特别注意HC-06的工作电压是3.3V但其信号引脚TXD RXD可以耐受5V电平因此可以直接与Arduino Uno的5V引脚连接。具体接线如下HC-06 VCC- Arduino3.3V或5V实测5V更稳定但模块可能轻微发热长期使用建议3.3VHC-06 GND- ArduinoGNDHC-06 TXD- ArduinoPin 10我们将这里定义为软串口的RXHC-06 RXD- ArduinoPin 11我们将这里定义为软串口的TXLED 阳极长脚- ArduinoPin 13通过220Ω电阻LED 阴极短脚- ArduinoGND这里为什么用Pin 10和11而不是硬件串口Pin 0 1因为Arduino Uno的硬件串口Serial通常用于和电脑通信上传程序和调试。如果我们把它用于连接HC-06在上传新程序时需要断开蓝牙模块否则会因串口冲突导致上传失败非常麻烦。使用软串口SoftwareSerial库虚拟一个串口可以完美地将调试通道Serial和控制通道SoftwareSerial分离互不干扰。2.2 状态机FSM核心思想浅析状态机不是什么高深的概念它就是一种编程模型用来描述一个事物在其生命周期内所经历的各种状态以及触发这些状态之间转换的事件和条件。你可以把它想象成一个流程图或者更生活化一点像一个电灯开关。一个最简单的电灯状态机只有两个状态“灯亮”和“灯灭”。触发状态转换的事件是“按下开关”。无论当前灯是亮是暗只要“按下开关”这个事件发生状态就会切换到另一个。状态机的核心要素有三个状态State系统在某一时刻所处的模式。比如“待机”、“运行”、“报警”、“模式A”、“模式B”。事件Event来自外部或内部、能触发状态改变的信号。比如“收到蓝牙命令‘A’”、“按键被按下”、“定时器超时”。转换Transition定义在某个状态下当某个事件发生时系统应切换到哪个新状态并执行什么动作。在代码中我们通常用一个变量如currentState来记录当前状态用switch-case或if-else-if语句来根据当前状态和收到的事件决定下一步做什么。这样做的好处是逻辑清晰。假设我们有一个风扇命令‘L’低速 ‘M’中速 ‘H’高速 ‘O’关机。如果没有状态机代码可能是一堆散乱的判断。有了状态机我们可以明确在“关机”状态下收到‘L’事件则转换到“低速”状态并启动风扇低速转动在“低速”状态下收到‘O’事件则转换到“关机”状态并停止风扇。每一段代码只负责一个状态下的逻辑大大降低了复杂度。2.3 HC-06蓝牙模块通信机制HC-06模块在此项目中扮演“事件传递者”的角色。它工作在从机模式等待手机主机连接。一旦配对成功手机和Arduino之间就建立起一个透明的串口桥梁。你在手机APP里发送的每一个字符或字符串都会原封不动地从HC-06的TXD引脚发送给Arduino的RX引脚。这里有一个关键细节数据格式和缓存。蓝牙串口传输的是字节流。当我们发送字符串“LED_ON”时Arduino是一个字节一个字节‘L’ ‘E’ ‘D’ ‘_’ ‘O’ ‘N’地接收。如果我们的程序处理不当比如在字节还没收全时就进行判断就会导致命令识别错误。因此一个常见的做法是设定一个终止符比如换行符‘\n’或回车符‘\r’。在串口监听中我们将收到的字符存入缓冲区直到遇到终止符才认为一条完整的命令已接收完毕然后对其进行解析。这就是我们项目代码中readSerialString()函数要做的事情。3. 软件实现从零搭建状态机框架3.1 开发环境与库配置我们使用Arduino IDE进行开发。首先需要引入一个核心库SoftwareSerial.h。这个库是Arduino标准库的一部分无需额外安装。它允许我们将任意两个数字引脚模拟成串口RX/TX来使用。在代码开头我们通过#include SoftwareSerial.h来包含它。接下来我们定义项目的常量和全局变量。这一步是搭建程序骨架好的定义能让后续逻辑一目了然。#include SoftwareSerial.h // 1. 定义引脚 #define BLUETOOTH_RX_PIN 10 // HC-06 TXD 接 Arduino 10 #define BLUETOOTH_TX_PIN 11 // HC-06 RXD 接 Arduino 11 #define LED_PIN 13 // LED连接引脚 // 2. 定义状态State enum DeviceState { STATE_IDLE, // 空闲状态 STATE_LED_ON, // LED开启状态 STATE_LED_OFF, // LED关闭状态 STATE_BLINKING // LED闪烁状态 }; DeviceState currentState STATE_IDLE; // 当前状态变量 // 3. 定义事件Event对应的命令字符串 const char* CMD_IDLE IDLE; const char* CMD_LED_ON ON; const char* CMD_LED_OFF OFF; const char* CMD_BLINK BLINK; // 4. 初始化软串口对象指定RX TX引脚 SoftwareSerial bluetoothSerial(BLUETOOTH_RX_PIN, BLUETOOTH_TX_PIN); // 5. 用于存储从串口读取的命令缓冲区 String inputString ; bool stringComplete false; // 标志位表示是否收到完整命令定义解析enum枚举类型用于定义状态比用数字0123更可读。将命令定义为字符串常量方便统一修改和管理。SoftwareSerial对象的初始化参数顺序是(RX, TX)这里RX接HC-06的TXD所以是BLUETOOTH_RX_PINPin 10。inputString和stringComplete是串口数据接收的“经典搭配”用于异步处理数据。3.2 核心状态机引擎与命令解析函数状态机的核心是一个处理函数它根据currentState和接收到的事件命令来决定下一步行动。我们单独编写这个函数保持主循环的整洁。/** * 状态机处理函数 * param event 接收到的事件命令字符串 */ void processFSM(String event) { switch (currentState) { case STATE_IDLE: if (event CMD_LED_ON) { digitalWrite(LED_PIN, HIGH); currentState STATE_LED_ON; Serial.println(Event: ON - State: LED_ON); } else if (event CMD_BLINK) { currentState STATE_BLINKING; Serial.println(Event: BLINK - State: BLINKING); // 注意闪烁逻辑需要在loop()中根据状态持续执行 } // 在IDLE状态下收到OFF命令无效果 break; case STATE_LED_ON: if (event CMD_LED_OFF) { digitalWrite(LED_PIN, LOW); currentState STATE_LED_OFF; Serial.println(Event: OFF - State: LED_OFF); } else if (event CMD_IDLE) { digitalWrite(LED_PIN, LOW); // 退出前关灯 currentState STATE_IDLE; Serial.println(Event: IDLE - State: IDLE); } // 在LED_ON状态下再次收到ON命令无效果幂等性 break; case STATE_LED_OFF: if (event CMD_LED_ON) { digitalWrite(LED_PIN, HIGH); currentState STATE_LED_ON; Serial.println(Event: ON - State: LED_ON); } else if (event CMD_IDLE) { currentState STATE_IDLE; Serial.println(Event: IDLE - State: IDLE); } break; case STATE_BLINKING: if (event CMD_IDLE) { digitalWrite(LED_PIN, LOW); // 停止闪烁确保灯灭 currentState STATE_IDLE; Serial.println(Event: IDLE - State: IDLE (Stop Blink)); } else if (event CMD_LED_ON || event CMD_LED_OFF) { // 在闪烁状态下直接响应开关命令并退出闪烁状态 digitalWrite(LED_PIN, (event CMD_LED_ON) ? HIGH : LOW); currentState (event CMD_LED_ON) ? STATE_LED_ON : STATE_LED_OFF; Serial.print(Event: ); Serial.print(event); Serial.println( - Exit BLINKING); } break; } }这个函数体现了状态机的精髓。注意看STATE_BLINKING的处理在闪烁状态下如果收到IDLE命令就停止闪烁并回到空闲如果收到ON或OFF则立即执行开关动作并切换到对应状态。这比简单地用if-else判断命令要清晰得多因为它考虑了“当前状态”这个重要上下文。接下来是串口命令解析函数。它的任务是从字节流中提取出完整的命令字符串。/** * 从软件串口读取字符串以换行符‘\n’为结束标志 */ void readBluetoothCommand() { while (bluetoothSerial.available()) { char inChar (char)bluetoothSerial.read(); if (inChar \n) { // 遇到换行符认为命令结束 stringComplete true; } else { inputString inChar; // 将字符添加到字符串 } // 可选防止缓冲区溢出设置最大长度 if (inputString.length() 20) { inputString ; stringComplete false; } } }3.3 主程序架构与状态持续行为处理setup()函数负责初始化loop()函数则是永不停止的主循环负责协调所有功能。void setup() { // 初始化硬件串口用于调试输出波特率115200便于快速打印 Serial.begin(115200); // 初始化软件串口与HC-06通信HC-06默认波特率通常是9600或115200 bluetoothSerial.begin(9600); // 如果HC-06已改为115200这里需匹配 // 设置LED引脚为输出模式 pinMode(LED_PIN, OUTPUT); // 初始状态LED灭处于空闲 digitalWrite(LED_PIN, LOW); currentState STATE_IDLE; Serial.println(System Started. FSM Bluetooth Control Ready.); Serial.println(Commands: ON, OFF, BLINK, IDLE); } void loop() { // 1. 持续检查并读取蓝牙命令 readBluetoothCommand(); // 2. 如果收到完整命令则处理 if (stringComplete) { // 去除可能的回车符和首尾空格 inputString.trim(); Serial.print(Received: ); Serial.println(inputString); // 将接收到的命令作为事件送入状态机处理 processFSM(inputString); // 处理完毕清空缓冲区准备接收下一条命令 inputString ; stringComplete false; } // 3. 处理与状态相关的持续行为例如闪烁 handleStateActions(); }主循环清晰分为三步检查事件、处理事件、执行状态动作。其中handleStateActions()函数专门负责那些需要持续运行的行为比如闪烁。这是状态机处理中一个重要的模式事件触发的是状态的瞬时转换和动作而某些状态本身可能需要持续的行为。/** * 处理需要持续执行的状态行为 */ void handleStateActions() { static unsigned long previousBlinkMillis 0; // 记录上次闪烁时间 const unsigned long blinkInterval 500; // 闪烁间隔500ms switch (currentState) { case STATE_BLINKING: // 非阻塞式闪烁避免使用delay() unsigned long currentMillis millis(); if (currentMillis - previousBlinkMillis blinkInterval) { previousBlinkMillis currentMillis; // 翻转LED状态 digitalWrite(LED_PIN !digitalRead(LED_PIN)); } break; // 其他状态无需持续行为可以留空或添加未来可能的功能 default: // 空闲、开、关状态不需要持续动作 break; } }这里使用了Arduino经典的millis()非阻塞定时方法来实现闪烁。在STATE_BLINKING状态下每500毫秒翻转一次LED引脚的电平而不阻塞loop()循环中的其他操作如继续接收蓝牙命令。这是编写响应式系统的关键技巧。4. 系统调试、烧录与实战演示4.1 代码烧录与硬件串口调试将完整的代码编译并上传到Arduino Uno。上传前请务必断开HC-06模块与Pin 10、11的连接或者至少断开RX/TX线防止串口冲突导致上传失败。上传成功后重新连接好HC-06的线路。打开Arduino IDE的串口监视器工具 - 串口监视器将波特率设置为115200。你应该能看到“System Started...”的提示信息。此时串口监视器是我们与Arduino对话的“调试终端”所有通过Serial.println()打印的信息都会在这里显示这对于观察状态转换和命令接收至关重要。4.2 手机APP连接与命令发送在手机上打开“Serial Bluetooth Terminal”这类APP应用商店搜索有很多。首次使用时需要配对HC-06。打开手机蓝牙设置搜索名为“HC-06”的设备默认密码通常是1234或0000完成配对。然后回到APP内在连接设备列表中选择“HC-06”。连接成功后APP界面通常会有一个输入框和一个发送按钮。现在你可以尝试发送预定义的命令了发送ON 观察LED点亮同时串口监视器会打印“Event: ON - State: LED_ON”。发送OFF LED熄灭打印“Event: OFF - State: LED_OFF”。发送BLINK LED开始以1Hz频率闪烁打印“Event: BLINK - State: BLINKING”。在闪烁状态下发送ON或OFF LED会立即停止闪烁并切换到常亮或常灭状态。在任何状态下除了IDLE发送IDLE 系统都会回到初始空闲状态LED灭。通过这个交互过程你可以清晰地看到状态机是如何根据当前状态和不同事件进行跳转的。串口监视器的输出就像一份运行日志帮你验证逻辑是否正确。4.3 常见问题排查与优化技巧即使按照步骤操作你也可能会遇到一些问题。这里列出几个常见的坑和解决办法问题1手机APP连接不上HC-06或者连接后马上断开。检查电源HC-06供电不足是首要原因。确保VCC接在了稳定的3.3V或5V上并且GND共地良好。可以尝试用外部电源如手机充电宝为Arduino供电而非电脑USB口因为某些电脑USB口电流输出能力较弱。检查波特率代码中bluetoothSerial.begin(9600)的波特率必须与HC-06模块当前设置的波特率一致。HC-06默认通常是9600但也可能是115200。如果不确定可以尝试修改代码为begin(115200)或begin(38400)等常见值。更彻底的办法是通过AT命令查询并修改HC-06的波特率但这需要额外的接线和操作。问题2发送命令后LED无反应但串口监视器有打印接收到的命令。检查LED接线确认LED长脚阳极通过电阻接到了Pin 13短脚阴极接到了GND。电阻必不可少直接连接可能烧毁LED或损坏Arduino引脚。检查命令字符串确保手机APP发送的命令完全匹配代码中定义的字符串包括大小写。发送“on”和“ON”在字符串比较中是不同的。可以在processFSM函数里加一句Serial.println(event);来确认收到的字符串到底是什么。问题3系统反应迟钝或者闪烁不稳定。优化串口读取我们的readBluetoothCommand()函数在loop()中每次只读取一个可用字节。如果手机快速发送大量数据可能处理不过来。但这在简单的命令控制中影响不大。如果追求极致可以考虑使用更大的缓冲区或者更高效的分析方法。注意非阻塞延时闪烁逻辑使用了millis()这是正确的。确保不要在loop()中的任何地方使用delay()函数否则会阻塞整个系统导致蓝牙命令无法及时响应。进阶优化技巧增加命令容错可以在解析命令时使用inputString.toUpperCase()将所有命令转为大写实现大小写不敏感。丰富状态反馈除了在串口打印还可以让Arduino通过蓝牙模块向手机APP发送状态回执。例如在processFSM函数里每个状态转换后添加bluetoothSerial.println(State: OK)让手机端也知道命令已执行。扩展更多状态这个框架易于扩展。如果你想增加一个“呼吸灯”状态只需在enum里加一个STATE_BREATHING在processFSM里增加该状态下的转换逻辑并在handleStateActions里用analogWrite和millis()实现PWM呼吸效果即可。通过这个从硬件连接到软件逻辑再到调试排错的完整流程你得到的不仅仅是一个让LED闪烁的蓝牙遥控器。你掌握的是一种用状态机来结构化编程思想的工具以及一整套在Arduino上构建可靠、可维护嵌入式系统的基础方法。下次当你的项目需要处理多个模式、多种输入时不妨先画出一个状态转换图然后套用这个框架你会发现复杂的逻辑变得井然有序。