手把手实现第一个串口连接:从零开始掌握 SerialPort 通信
你有没有遇到过这样的场景?手头有一块开发板,连上电脑后却不知道如何读取它发出来的数据;或者想用 JavaScript 写一个简单的传感器监控程序,却发现“串口”这个词听起来既熟悉又遥远。
别担心,这正是我们今天要解决的问题。
本文不讲空泛理论,也不堆砌术语,而是带你一步步亲手建立第一条串行通信链路——使用 Node.js 和serialport库,让代码真正“听见”硬件的声音。
整个过程就像调试一台老式收音机:选对频道(端口)、调好频率(波特率),然后就能清晰地接收到信号。准备好了吗?我们从最基础的环境搭建开始。
为什么是 SerialPort?JavaScript 怎么也能操作串口?
在很多人印象里,串口通信属于 C/C++、Python 或嵌入式工程师的领域。但随着 Electron、Node.js 在工业控制和 IoT 领域的渗透,JavaScript 正在打破软硬边界。
serialport就是这个趋势中的明星库。它是一个基于 Node.js 的开源项目,封装了操作系统底层的串口访问机制,让你可以用熟悉的 JavaScript 语法打开 COM 端口、设置通信参数、收发数据。
更重要的是:
✅ 跨平台支持 Windows / macOS / Linux
✅ 支持 USB 转串口芯片(如 CH340、CP2102)
✅ 可集成到 Web 服务或桌面应用中(比如用 Electron 做可视化串口助手)
换句话说,你现在可以用写前端的方式,去跟单片机对话。
第一步:安装与环境准备
打开终端,创建项目并安装核心依赖:
mkdir my-first-serial && cd my-first-serial npm init -y npm install serialport @serialport/parser-readline💡 提示:如果你使用的是 Linux 或 macOS,在后续运行脚本时可能会遇到权限问题。建议先执行:
bash sudo usermod -aG dialout $USER然后重启系统,确保当前用户有访问
/dev/tty*的权限。
第二步:找出你的设备接在哪个端口
每当你把 Arduino、ESP32 或任何带串口的设备插入电脑,系统都会为它分配一个“路径”——Windows 上叫COM3、COM4,Linux/macOS 上则是/dev/ttyUSB0或/dev/cu.usbserial-*。
但我们怎么知道是哪一个?
用SerialPort.list()来扫描所有可用串口:
const { SerialPort } = require('serialport'); SerialPort.list().then(ports => { if (ports.length === 0) { console.log('❌ 没有发现任何串口设备'); return; } console.log('✅ 发现以下串口设备:'); ports.forEach(port => { console.log(` 路径: ${port.path}`); console.log(` 制造商: ${port.manufacturer || '未知'}`); console.log(` PID/VID: ${port.productId}/${port.vendorId}\n`); }); }).catch(err => { console.error('枚举失败:', err.message); });保存为list-ports.js并运行:
node list-ports.js你会看到类似输出:
✅ 发现以下串口设备: 路径: COM3 制造商: Arduino LLC PID/VID: 0043/2341记下这个path,接下来我们就用它建立连接。
第三步:连接设备 —— 波特率必须匹配!
这是最关键的一步:通信双方必须使用相同的波特率(baudRate),否则就像两个人用不同语速说话,结果只能听到“咕噜咕噜”。
常见波特率包括 9600、115200、57600 等。Arduino 默认常用 9600 或 115200;一些高速传感器可能用 460800 甚至更高。
假设我们的设备使用115200 波特率,其他参数为标准配置(8 数据位、1 停止位、无校验),就可以创建连接了:
const { SerialPort } = require('serialport'); const { ReadlineParser } = require('@serialport/parser-readline'); // 👇 修改为你自己的端口路径 const PORT_PATH = process.platform === 'win32' ? 'COM3' : '/dev/ttyUSB0'; const port = new SerialPort({ path: PORT_PATH, baudRate: 115200, dataBits: 8, stopBits: 1, parity: 'none', autoOpen: true // 自动打开,无需手动调用 open() });这时候串口已经尝试连接了。但怎么知道是否成功呢?
第四步:监听事件,接收第一行数据
serialport是事件驱动的,就像网页里的 click 事件一样,你可以监听open、data、error、close等关键状态。
我们来绑定几个重要事件:
// 当串口成功打开 port.on('open', () => { console.log('🎉 串口已打开,波特率:', port.settings.baudRate); // 可选:发送初始化指令(例如 AT 命令) const welcomeMsg = Buffer.from('Hello Device!\r\n'); port.write(welcomeMsg, (err) => { if (err) return console.error('发送失败:', err); console.log('👋 已向设备发送问候'); }); }); // 使用解析器按换行符拆分数据 const parser = new ReadlineParser({ delimiter: '\r\n' }); port.pipe(parser); parser.on('data', (line) => { console.log('📩 收到一行数据:', line.toString()); }); // 错误处理 port.on('error', (err) => { console.error('🚨 串口错误:', err.message); }); // 连接断开(比如拔掉了 USB 线) port.on('close', () => { console.log('🔌 串口已关闭'); });将这段代码保存为serial-client.js并运行:
node serial-client.js如果一切正常,你应该会看到:
🎉 串口已打开,波特率: 115200 👋 已向设备发送问候 📩 收到一行数据: TEMP:25.3,HUM:60 📩 收到一行数据: OK恭喜!你刚刚完成了人生中第一次真正的串口通信。
关键参数详解:五个不能错的设置
为了帮助你避免“乱码”、“收不到数据”这类经典坑,这里列出必须与设备一致的五大参数:
| 参数 | 典型值 | 说明 |
|---|---|---|
baudRate | 9600, 115200 | 速率要完全一致,差一点都会出错 |
dataBits | 8 | 通常为 8,表示每个字符 8 位数据 |
stopBits | 1 | 标志一帧结束,一般设为 1 |
parity | ‘none’ | 奇偶校验,现代设备大多关闭 |
flowControl | false | 流控用于防丢包,高吞吐场景开启 |
⚠️ 特别提醒:
很多初学者以为“波特率差不多就行”,但实际上必须精确匹配。比如设备是 115200,你设成 115000 就会持续出错。
数据粘包怎么办?教你用解析器正确分包
原始串口数据是以字节流形式到达的,没有天然的消息边界。这就导致一个问题:一条完整的消息可能被分成两次接收。
例如:
- 第一次收到"HELLO WOR"
- 第二次收到"LD\r\n"
合起来才是完整的一条"HELLO WORLD\r\n"—— 这就是所谓的“拆包”。
解决方案就是使用解析器(Parser)。
serialport提供多种内置解析器:
1. 按行解析(推荐多数场景)
const parser = port.pipe(new ReadlineParser({ delimiter: '\n' })); parser.on('data', line => console.log('完整一行:', line));适用于以\n或\r\n结尾的日志、AT 指令等。
2. 固定长度解析
如果你知道每次发送都是 10 字节:
const { ByteLengthParser } = require('@serialport/parser-byte-length'); const parser = new ByteLengthParser({ length: 10 }); port.pipe(parser).on('data', chunk => { console.log('收到固定长度数据:', chunk); });3. 自定义分隔符(高级)
比如某些协议使用0xFF 0xFE作为帧头:
const { DelimiterParser } = require('@serialport/parser-delimiter'); const parser = new DelimiterParser({ delimiter: Buffer.from([0xFF, 0xFE]) }); port.pipe(parser).on('data', frame => { console.log('收到完整帧:', frame); });常见问题与应对策略
❌ 打不开串口:Permission Denied
原因:Linux/macOS 用户没有权限访问/dev/tty*
解决方法:
sudo usermod -aG dialout $USER然后注销重新登录。
🛠️ 临时方案:
sudo node serial-client.js(不推荐长期使用)
❌ 收到一堆乱码或空字符串
原因:波特率不匹配!
排查步骤:
1. 查看设备文档确认正确波特率
2. 用串口调试工具(如 XCOM、SSCOM)验证
3. 尝试常见组合:9600、19200、38400、115200
❌ 只能收到前几个字符
原因:未使用解析器,直接监听port.on('data')导致截断
解决方法:务必使用ReadlineParser或其它合适解析器进行流式处理
❌ 断线后无法自动恢复
默认情况下,一旦 USB 断开,serialport不会自动重连。
加入简单的重连逻辑即可:
let reconnectInterval; port.on('close', () => { console.log('⚠️ 串口已关闭,准备重连...'); if (!reconnectInterval) { reconnectInterval = setInterval(() => { console.log('🔁 正在尝试重新连接...'); connect(); // 重试连接函数 }, 3000); } }); function connect() { try { port.open((err) => { if (err) { console.log('重连失败:', err.message); return; } console.log('✅ 重连成功!'); clearInterval(reconnectInterval); reconnectInterval = null; }); } catch (e) { console.log('连接异常:', e.message); } }实际应用场景举例
掌握了基本技能后,你可以做很多有趣的事:
✅ 温湿度监控系统
STM32 + DHT22 → UART → PC 上 Node.js 接收 → WebSocket → 浏览器实时图表
✅ 工业 PLC 数据采集
通过串口轮询 Modbus RTU 协议,定时读取电流、电压数据并存入数据库
✅ 自制串口调试助手
用 Electron + React 构建图形化界面,支持日志保存、命令发送、自动补全
✅ 自动化测试平台
MCU 出厂前自动烧录 + 串口验证功能是否正常,大幅提升产线效率
最佳实践建议
- 优先使用
parser处理数据流,不要直接监听原始data事件 - 根据
manufacturer或pid/vid自动识别目标设备,提升用户体验 - 增加超时机制:长时间无响应时提示“设备未就绪”
- 记录通信日志:便于后期排查问题
- 避免暴露串口接口至公网:防止恶意操作物理设备
写在最后
你不需要成为嵌入式专家才能玩转串口。
通过serialport,JavaScript 开发者现在可以轻松打通软件与硬件之间的最后一公里。
回顾一下今天我们走过的路:
- 安装依赖、列出端口
- 配置正确的通信参数
- 成功打开串口并收发数据
- 使用解析器处理粘包问题
- 解决常见连接故障
- 展望实际工程应用
这一切都不再神秘。只要你有一根 USB 线、一块开发板、一段代码,就能让数字世界与物理世界真正对话。
如果你正在做一个物联网项目,或是需要和某个老旧设备通信,不妨试试serialport。也许下一秒,你就会听到那句期待已久的:“OK”。
对了,你在用什么设备做串口开发?Arduino?树莓派?还是工业PLC?欢迎在评论区分享你的实战经验!