目录
在学习嵌入式开发时,单纯的操作寄存器模拟串口通信的实验未免太过容易,于是我选择了一个经典的场景:让 51 单片机通过 USB 转串口,与 Ubuntu 虚拟机进行数据通信。这个过程看似简单,却串联起了虚拟机配置、SSH 远程开发、termios 串口编程、权限管理等多个知识点。本文就是我在实践中踩坑、排错、总结的完整记录,希望能为同样在入门阶段的同学提供一份清晰的实践指南,让串口通信不再是 "玄学",而是可以被精准控制的技术环节。
一、如何让虚拟机获取USB控制权?
首先进入根目录/下的dev目录。由于linux一切皆文件的思想,每一个外设都会被封装成文件,而dev表示的就是外设文件目录。

一般来说,"USB转串口"设备在Linux中的名字叫ttyUSB*或者ttyACM*,其中*代表编号。
可是这里面似乎并没有找到,这意味着目前的USB并没有被虚拟机识别到。这是因为虚拟机是运行在主机上的一个软件,此时的控制权依然还在主机(物理机),所以需要在VMWare中进行一些配置,将控制权交给虚拟机。不过值得注意的是:控制权在同一时刻只能由一台机器占用,虚拟机用了主机就无法使用了。

从此以后,你想从上位机Linux访问到底层单片机,就只需要直接读写这个/dev/ttyUSB0文件即可,而这个操作与以前学过的文件操作完全相同,所有的适配工作已经被Linux内核完成了。当然如果你以后成为一名驱动工程师,这些适配工作可能就得你自己写咯。
二、如何使用VSCode,而不是vim?
由于直接在Linux虚拟机中编程需要用到古老的vim和丑陋的ui界面,不方便函数查看、报错检测,于是我选择使用VSCode编写代码,直接避免了上述问题。
(1)查看虚拟机的ssh状态
检查ssh服务是否正确运行:

发现仅仅是安装了ssh却没有启动,于是使用systemctl start ssh命令运行,然后可以设置开机自启动等,这过程中每个人肯定会遇到不同的问题,用ai查一查基本都能解决。

(2)查看虚拟机IP地址
命令:ip addr

发现ip地址是192.168.202.132,将其记录下来并打开vscode。
(3)vscode连接到虚拟机Ubuntu


后续可能会要求你输入密码,按照要求即可。于是就发现你现在可以使用Windos下的vscode来编写linux代码了。只要你不关闭该虚拟机,就完全可以不用理会他,直接使用更加方便的vscode编辑器即可。
后续你可能遇到各种各样的问题,比如没有代码补全提醒,没有各种头文件等,这些自己上网络查询,然后在vscode中下载、配置一下即可,不再赘述。
三、测试Linux与串口通信
以下代码是AI生成的,因为本人还未曾学习Linux驱动开发,对于这一块不熟悉。主要是一些关于ttyUSB文件的波特率、形式等配置。
cpp
#include<fcntl.h> //open等文件相关的头文件
#include<unistd.h>
#include<stdio.h>
#include<errno.h>
#include<termios.h>
#include<fcntl.h>
#include<unistd.h>
#include<stdio.h>
#include<errno.h>
#include<termios.h>
// 移除未使用的<string>,避免冗余
void init_USB(int fd)
{
struct termios opt;
// 【修改1】增加tcgetattr容错:防止获取配置失败导致程序崩溃
if (tcgetattr(fd, &opt) != 0) {
perror("获取串口配置失败");
return;
}
cfsetispeed(&opt, B57600);
cfsetospeed(&opt, B57600);
opt.c_cflag |= CLOCAL | CREAD;
opt.c_cflag &= ~CSIZE;
opt.c_cflag |= CS8;
opt.c_cflag |= PARENB;
opt.c_cflag |= CMSPAR;
opt.c_cflag &= ~PARODD;
opt.c_cflag &= ~CSTOPB;
opt.c_cflag &= ~CRTSCTS;
opt.c_iflag &= ~(IXON | IXOFF | IXANY);
opt.c_iflag &= ~(INPCK | ISTRIP);
opt.c_lflag &= ~(ICANON | ECHO | ECHOE | ISIG);
opt.c_oflag &= ~OPOST;
// 【修改2】增加tcsetattr容错:配置失败时提示
if (tcsetattr(fd, TCSANOW, &opt) != 0) {
perror("配置串口参数失败");
}
}
int main()
{
int fd=open("/dev/ttyUSB0",O_RDWR | O_NOCTTY);
if(fd==-1)
{
perror("打开串口失败");
// 【修改3】打开失败后直接退出,避免后续操作无效fd
return -1;
}
init_USB(fd);
// 读取串口信息:原代码仅读1次,无阻塞/循环,大概率读不到数据
char buffer[100];
// 补充:打印提示,告知用户程序在等待数据
printf("等待51单片机发送数据...\n");
int len=read(fd,buffer,16); // 读16字节(匹配buffer大小,无问题)
if(len>0)
{
buffer[len]='\0';
// 优化:打印原始十六进制,便于核对51的SBUF数据(字符串可能有不可见字符)
printf("读取到 %d 字节数据:\n", len);
//printf("字符串形式:%s\n", buffer);
printf("十六进制形式:");
for(int i=0; i<len; i++) {
printf("0x%02X ", (unsigned char)buffer[i]);
}
printf("\n");
}
else if(len==0)
{
printf("未读取到数据(串口无数据发送)\n");
}
else
{
// 优化:打印具体错误原因,便于排查
perror("读取串口失败");
}
close(fd);
return 0;
}

最后结果发现与上一篇文章的结果完全一致,这说明我们已经成功的从51单片机中读取到消息了!
关于这篇文章,其实没有什么亮眼的操作,就干了一件事情:打通了从底层硬件到上位机Linux的链路,基于这个基础,后续我们可以在Linux上进行更为复杂的操作,比如增加数据链路层协议、唤醒缓冲区等等,就可以成为了一个较为工业化的项目了。
而这个过程,其实就是串口助手干的事情,只不过他更加完善、且富含方便操作的UI界面罢了。

