器→工具, 电子电路

Arduino 串口通信 (Serial Communication)

钱魏Way · · 3 次浏览
!文章内容如有错误或排版问题,请提交反馈,非常感谢!

串口通信是 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 代码和电脑串口工具)必须设置成完全相同的波特率,否则接收到的将是乱码!

更多信息请查看:通信协议之异步串行通信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(): 从流中查找并解析下一个有效的整数或浮点数。会忽略前面的非数字字符。非常适合接收数字命令。会阻塞直到解析到数字或超时。
    • 控制信号 (部分功能需硬件支持,且较少直接使用):
      • 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。

工作流程 (典型)

发送数据: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)通信。

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注