串口通信是 Arduino 与外部世界(如电脑、传感器、显示器、其他单片机等)进行信息交换最基本、最常用的方式之一。这对于程序调试、数据显示以及设备间通信都至关重要。
串口通信基础概念
- 定义: 串行通信(Serial Communication)意味着数据一位一位地(逐位)在单条传输线上依次传输。这是相对于并行通信(同时传输多位数据)而言的。
- 异步 vs 同步: Arduino 主要使用 异步串行通信 (Asynchronous Serial)。这意味着:
- 发送方和接收方没有共享的时钟信号来同步数据传输。
- 通信双方必须事先约定好通信速度(波特率)。
- 每个传输的数据包(通常是 1 个字节)会被起始位(Start Bit)和停止位(Stop Bit)包裹,它们帮助接收方识别数据的开始和结束。
- 数据格式:常见的格式是8N1:8个数据位(Data Bits),无奇偶校验(No Parity Check),1个停止位(1 Stop Bit)。
- 硬件部件 (UART/USART): Arduino 板上的微控制器芯片(如 ATmega328P)内部集成了称为 UART (Universal Asynchronous Receiver/Transmitter) 或 USART (Universal Synchronous/Asynchronous Receiver/Transmitter) 的硬件模块。这个模块专门负责处理串行通信的底层细节,如生成起始/停止位、进行数据位的移位、计算波特率、检测错误(如果使用奇偶校验)等。开发者无需手动操作这些位,通过库函数即可使用。
- 物理接口 (逻辑电平 & 物理连接):
- 逻辑电平: Arduino Uno/Nano/等经典5V Arduino 使用 TTL电平:
- 逻辑0 (LOW): 接近 0V (通常 < 0.8V)
- 逻辑1 (HIGH): 接近 Vcc (通常 > 2.5V-3V,对于5V系统就是 ~5V)
- 物理连接: Arduino 板上标有RX (Receive 接收) 和 TX (Transmit 发送) 的引脚是 连接内部 UART/USART 模块 的。RX 是输入,TX 是输出。与其他设备连接时,基本原则是交叉连接:即本机的 TX 连接对方的 RX,本机的 RX 连接对方的 TX。
- USB 转换: Arduino 板上通常有一个 USB 转串口芯片(如 ATmega16U2, CH340, CP2102, FT232 等)。这个芯片将 USB 协议转换为 TTL 串口协议。
- 你通过 USB 线在 Arduino IDE 的串口监视器看到的 “串口”,实际上是 USB 信号被板载转换芯片转换后在 Arduino 的RX/TX 引脚上呈现的 TTL 串口信号(这个逻辑对开发者是透明的)。所以,当你通过 USB 编程或使用串口监视器时,你已经在使用主控芯片上的 UART 了。
- 波特率 (Baud Rate): 衡量串口通信速度的单位,表示每秒传输的符号数 (symbols per second)。对于常见的8N1 格式,每个字节传输需要 10 个符号(1起始位 + 8数据位 + 1停止位)。因此:
- 波特率9600: 每秒最多传输 9600 / 10 = 960 个字节。实际最大持续速率略低于此值。
- 常用波特率:300, 600, 1200, 2400, 4800, 9600, 19200, 38400, 57600, 115200, 等。
- 关键:通信双方(Arduino 代码和电脑串口工具)必须设置成完全相同的波特率,否则接收到的将是乱码!
- 逻辑电平: Arduino Uno/Nano/等经典5V Arduino 使用 TTL电平:
更多信息请查看:通信协议之异步串行通信UART
Arduino 串口库 (Serial)
Arduino 提供了 Serial 对象(类)来简化对板载主 UART 的使用。对于拥有多个硬件串口的板子(如 Mega2560),还会有 Serial1, Serial2, Serial3。
核心函数(常用方法)
- 初始化和状态检查:
- begin(speed): 初始化串口通信。在setup() 中必须调用一次! speed 就是波特率(如 9600, 115200)。
- end(): 关闭串口通信(通常不需要主动调用)。
- available(): 检查串口接收缓冲区中是否有可读取的数据。返回一个整数,表示可读的字节数。最常用的函数之一,用于判断是否有数据到达。
- availableForWrite(): 检查发送缓冲区是否有空闲空间(可写的字节数)。
- 发送数据 (Write – TX):
- print(val): 将数据作为文本发送(可读的 ASCII 字符形式)。
- val 可以是多种类型:整数(byte, int, long)、浮点数(float, double)、字符串(char*, String)。
- 例如:print(42) 发送字符’4′ 和 ‘2’(对应的 ASCII 码为 52 和 50)。
- print(42, format): 可选参数指定格式。常用格式:
- BIN 二进制 (发送”101010″)
- OCT 八进制 (发送”52″)
- DEC 十进制 (默认,发送”42″)
- HEX 十六进制 (发送”2A”)
- print(3.1415, decimalPlaces): 指定浮点数的小数位数(如2 发送 “3.14”)。
- println(val): 和print() 类似,但在发送完数据后自动追加一个回车换行符(\r\n)。极其常用,使串口监视器中的数据按行显示。
- write(val): 发送原始字节数据 (二进制)。
- val 可以是单个字节(byte/char,发送其二进制值)。
- 可以是一个字节数组 (byte buf[])。
- 可以是指定长度的字节数组 (Serial.write(buf, len))。发送效率高于print()。
- 与print() 关键区别:write(65) 发送的是数值 65 对应的单字节(0x41, 在 ASCII 中是大写字母 ‘A’)。而 Serial.print(65) 发送的是字符 ‘6’ (0x36) 和 ‘5’ (0x35) 两个字节。
- 接收数据 (Read – RX):
- read(): 从接收缓冲区读取第一个字节(最早收到的)。
- 返回类型是int。如果没有数据可读,则返回 -1。
- 通常结合available() 使用: 先检查是否有数据 (if (Serial.available() > 0)),再读取 (Serial.read())。
- 读取操作会将字节从接收缓冲区移除。
- peek(): 查看接收缓冲区中的下一个字节(第一个可读字节),但不将它从缓冲区中移除。 下次read() 还是读这个字节。返回类型是 int,没有数据则返回 -1。
- readBytes(buffer, length): 从缓冲区读取length 个字节并存放到指定的字节数组 buffer 中。 会阻塞程序直到读取到指定数量的字节或超时。适用于接收特定长度的数据帧。
- readString(): 从缓冲区读取所有可用数据并将其作为String 对象返回。会阻塞直到收到结束符(通常是超时判定结束)。
- readStringUntil(terminator): 读取字符直到遇到指定的结束字符terminator (如 ‘\n’ 换行符),并将其作为 String 对象返回。非常常用于接收以换行符结束的命令行。会阻塞直到收到结束符或超时。
- parseInt() / Serial.parseFloat(): 从流中查找并解析下一个有效的整数或浮点数。会忽略前面的非数字字符。非常适合接收数字命令。会阻塞直到解析到数字或超时。
- read(): 从接收缓冲区读取第一个字节(最早收到的)。
- 控制信号 (部分功能需硬件支持,且较少直接使用):
- setTimeout(time): 设置超时时间(毫秒),影响readBytes(), readString(), readStringUntil(), parseInt(), parseFloat() 等阻塞函数的等待时间。默认超时是 1000ms。
- flush(): (行为在较新版本Arduino中已改变!) 在旧版本中,它会阻塞直到所有发送数据都完成传输(待发送缓冲区为空)。在较新版本Arduino IDE (大约 1.0之后) 中,flush() 只清空接收缓冲区(丢弃所有未读数据),不再影响发送缓冲区。为避免混淆,发送后等待发送完成最好使用循环检查Serial.availableForWrite() == SERIAL_TX_BUFFER_SIZE 或 while (Serial && Serial.availableForWrite() < someSize) { }。要清空接收缓冲区,直接用 flush() (新版本)或读取所有数据直到 available() = 0。
- print(val): 将数据作为文本发送(可读的 ASCII 字符形式)。
工作流程 (典型)
发送数据:Serial.print() 和 Serial.println()
Arduino 通过 Serial 对象来控制串口通信。最常用的发送函数是 Serial.print() 和 Serial.println()。
Serial.begin(baudRate) 初始化串口
在发送或接收数据之前,你必须在 setup() 函数中初始化串口,并设置波特率。
void setup() { Serial.begin(9600); // 初始化串口,波特率为 9600 } void loop() { // ... }
Serial.print() 和 Serial.println()
- print(data):将数据发送到串口,但不换行。发送后光标会停留在同一行。
- println(data):将数据发送到串口,并自动换行。发送后光标会移动到下一行的开头。
示例代码:发送不同类型的数据
void setup() { Serial.begin(9600); // 初始化串口 } void loop() { Serial.print("Hello, Arduino!"); // 发送字符串 Serial.print(" "); // 发送空格 Serial.print(123); // 发送整数 Serial.print(" "); Serial.print(3.14, 2); // 发送浮点数,保留两位小数 Serial.println(); // 换行 Serial.println("---"); // 发送分隔符并换行 int sensorValue = analogRead(A0); // 读取模拟引脚A0的值 Serial.print("Sensor Value: "); Serial.println(sensorValue); // 发送传感器值并换行 delay(1000); // 延时1秒 }
将代码上传到 Arduino 板后,打开 Arduino IDE 中的串口监视器(通常在右上角),确保波特率设置为 9600,你将看到 Arduino 不断发送的数据。
接收数据:从串口读取数据
从串口接收数据是实现 Arduino 与电脑或其他设备交互的关键。
Serial.available()
在读取数据之前,你需要检查串口缓冲区是否有可用的数据。Serial.available() 函数返回串口缓冲区中当前可读取的字节数。
Serial.read()
Serial.read() 函数从串口缓冲区读取一个字节(字符)。如果没有可读取的数据,它会返回 -1。
Serial.readString() 和 Serial.readStringUntil(terminator)
- readString():读取串口缓冲区中的所有可用数据,直到超时(默认1秒)或缓冲区为空,返回一个 String 对象。
- readStringUntil(terminator):读取串口缓冲区中的数据,直到遇到指定的终止符(例如换行符 \n 或回车符 \r),或者超时。
示例代码:接收并处理数据
String receivedData; void setup() { Serial.begin(9600); // 初始化串口 Serial.println("Arduino Ready! Send 'LED_ON' or 'LED_OFF' to control LED."); pinMode(LED_BUILTIN, OUTPUT); // 设置内置LED为输出模式 } void loop() { if (Serial.available() > 0) { // 如果串口有数据可读 // 使用 readStringUntil 读取一行数据,直到遇到换行符 '\n' receivedData = Serial.readStringUntil('\n'); // 去除字符串前后的空格和回车符 receivedData.trim(); Serial.print("Received: "); Serial.println(receivedData); if (receivedData == "LED_ON") { digitalWrite(LED_BUILTIN, HIGH); // 打开内置LED Serial.println("LED is ON"); } else if (receivedData == "LED_OFF") { digitalWrite(LED_BUILTIN, LOW); // 关闭内置LED Serial.println("LED is OFF"); } else { Serial.println("Unknown command."); } } }
上传代码后,打开串口监视器。在顶部的输入框中输入 “LED_ON” 或 “LED_OFF”,然后点击“发送”按钮或按下回车键。你将看到 Arduino 根据接收到的命令控制内置 LED 的亮灭。
注意: 当使用 Serial.readStringUntil(‘\n’) 时,确保你发送的数据以换行符结束。在串口监视器中输入后按回车键通常会自动添加换行符。
Arduino Uno/Nano (ATmega328P) 的硬件限制
- 只有一个硬件串口:Serial 使用的是 Pin 0 (RX) 和 Pin 1 (TX)。
- 关键限制: 当你使用串口监视器进行烧录或通信时,避免在 Pin 0 和 Pin 1 上连接其他重要或信号复杂的设备,因为 USB 通讯和烧录程序就是通过它们进行的。强行连接可能导致冲突或烧录失败。烧录时最好断开外部连接。
- 共享引脚: Pin 0 和 Pin 1 同时是 GPIO。如果你不使用串口通信,可以当作普通数字IO用。一旦使用了begin(),它们就被UART占用。
- 缓冲区大小: 接收和发送缓冲区都比较小(通常各为 64 或 128 或 256 字节,取决于具体板型和核心定义)。如果发送或接收速度太快而没有及时处理,会导致缓冲区溢出和数据丢失。
SoftwareSerial 库 – 软件模拟串口
- 为什么需要:
- Uno/Nano/等只有一个硬件串口已被Serial 占用(连接USB)。
- 你的项目需要同时连接多个串口设备(如蓝牙模块、GPS模块、另一个Arduino等)。
- 是什么:SoftwareSerial 库允许你将 任意两个数字引脚 (D2-D13 通常比较安全,避免0,1, 和高频引脚) 模拟成额外的 RX 和 TX 引脚,创建一个软件实现的串口。本质是通过定时器和精确的时序控制在这些引脚上逐位发送/接收数据。
- 特点:
- 灵活: 几乎可以将任何数字引脚用作 RX/TX。
- 成本: 节省硬件开销(不需要额外的硬件 UART)。
- 缺点:
- 速度慢: 软件模拟受 CPU 速度限制,通常最大波特率低于硬件串口(115200 有时能达到,但 9600, 38400 更稳定可靠)。高波特率下稳定性不如硬件串口。
- 中断冲突: 软件模拟使用硬件定时器中断和引脚状态变化中断。它可能会与某些需要精确时序或频繁使用中断的库(如某些电机驱动库如h、某些显示器库、某些红外库)产生冲突。
- 占用 CPU: 发送和接收时需要 CPU 全神贯注地进行位操作,可能影响主程序的其他时间敏感任务。
- 只能单实例接收: 一个SoftwareSerial 实例一次只能在一个 RX 引脚上监听(接收数据)。
- 时序要求高: 需要确保时序精准才能实现稳定的通信。
- 用法:
- #include <SoftwareSerial.h>
- 创建对象:SoftwareSerial mySerial(rxPin, txPin); // 指定RX, TX引脚
- 在setup() 中初始化:begin(baudRate); // 如9600
- 在loop() 中,使用available(), mySerial.read(), mySerial.print(), mySerial.write() 等方法进行通信,用法类似 Serial 对象。
- 如果系统中有多个SoftwareSerial 实例,通常需要循环监听(轮询)每个实例的 available()。SoftwareSerial 库提供了 listen() 方法来主动选择哪个实例进行接收。
调试技巧和常见问题
- 波特率不一致: 这是新手上路最常见的错误!务必检查两边设置的波特率必须一字不差。
- 引线混乱: 检查连线是否正确:Arduino 的 TX 连 对方的 RX;Arduino 的 RX 连 对方的 TX。
- 电平不兼容: 虽然 Arduino (5V TTL) 能“碰运气”连某些3V 设备(如 ESP8266/ESP32 某些型号可能有5V容忍),但长期使用风险很大!最安全的方式:
- 连接3V TTL 设备时,使用电平转换器模块。
- 连接 RS-232 设备(如老式电脑串口DB9,使用 +/- 12V电平)时,必须使用 USB转RS232适配器 或 RS232转TTL转换模块 (如MAX232芯片模块)。
- RX/TX 引脚冲突: Uno/Nano 在烧录或使用Serial 时,Pin 0 和 Pin 1 很忙。烧录前断开外部连接或使用自动复位电路。
- 缓冲区溢出: 当发送方速度远快于接收方处理速度时发生。表现为数据丢失、乱码。
- 发送方: 避免在循环中无节制的println() 或write()。适当加 delay() 或根据 Serial.availableForWrite() 来调整发送节奏。
- 接收方: 使用readBytes() 读取固定长度数据包或 readStringUntil() 及时读取行结束的数据。在 loop() 中尽快处理接收到的数据,避免阻塞。
- 乱码: 原因可能:波特率错、硬件问题、连线故障、供电不稳、中断冲突(尤其 SoftwareSerial)、程序逻辑错误导致数据错乱。
- 使用串口监视器: Arduino IDE 自带的串口监视器是调试的金钥匙。它能:
- 设置波特率。
- 显示 Arduino 发送过来的数据(文本或16进制显示)。
- 发送数据给 Arduino(手动输入或回车发送)。
- 自动滚动、清屏、显示时间戳。
- 选择正确的行结束符(换行/ 回车 / 两者都 / 无):确保与 Arduino 接收端代码(如 readStringUntil(‘\n’))匹配。通常选 “Both NL & CR” 或 “Newline”。
- 高级调试工具: 如PuTTY (功能更强的终端软件)、逻辑分析仪(抓取波形)、专业的串口助手软件。
典型应用场景
- 调试输出: 使用print() / println() 输出变量值、程序状态、调试信息到电脑串口监视器。这是开发调试时无价的手段。
- 从PC接收命令: 电脑发送指令控制Arduino,如控制LED亮灭、舵机角度、设置参数等。
- 数据传输: Arduino 读取传感器数据后发送给电脑记录或显示。
- 连接无线模块: 通过串口连接蓝牙模块(如 HC-05/HC-06)或 WiFi 模块(如 ESP8266/ESP01S),实现无线通信。
- 连接显示屏: 连接 OLED/LCD 模块(如 I2C/SPI 控制太复杂时,某些模块支持串口指令)。
- 连接 GPS 模块: 接收 NMEA 0183 协议数据获取位置信息。
- Arduino间通信: 多个 Arduino 通过串口(硬件或软件模拟)交换数据。
- 连接其他微控制器: 与树莓派(Pi)、ESP32、STM32 等其他开发板进行通信。
- 程序更新/配置: 部分库或模块通过串口接受固件更新或配置指令(如 ESP8266的AT指令)。
- 构建串行协议: 在串口基础上构建更高级的应用层协议(如基于字符命令、定长数据包、Modbus RTU 等)。
- 连接 ROS: 在机器人系统中,Arduino 常作为底层电机/传感器控制器,通过串口与运行ROS的主控计算机(如 Raspberry Pi)通信。