ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

Linux串口编程从入门到工程实践:open_serial函数设计全解析

Linux串口编程从入门到工程实践:open_serial函数设计全解析 干嵌入式这些年打交道最多的就是串口。调试工装、采集PLC数据、升级固件、连扫码枪哪一样都绕不开UART。今天想聊的这份“工业开启串口”自用无bug版本是我在Linux环境下沉淀下来的一个串口封装函数——别看只是把串口打开这里面的门道真不少全是工业现场逼出来的细节。如果你也需要在Linux板卡上用串口对接工业设备或者正打算自己封装一套串口工具库这篇内容应该能帮你省掉不少踩坑时间。我以前也喜欢到处抄串口初始化的代码今天拷一段、明天改一行结果每次换设备、换接线方式就出问题。后来花了两个晚上把这些零散经验整理成一个open_serial()函数不断补边界条件才敢叫它“自用无bug版本”。这篇文章我不会只丢一段代码给你而是把为什么要这么写、为什么不那样写全部拆开讲清楚。1. 项目背景为什么“开启串口”这么点事也能水出一篇长文1.1 工业场景下的串口到底有什么不一样很多人觉得串口就是“发数据、收数据”写个调试助手一分钟就搞定。但在工业现场串口要承担的东西比实验室里重得多。现场有变频器、PLC、触摸屏、仪表、扫码枪线缆动不动就拉出去几十米周围还有电机、电源带来的电磁干扰。这种环境下串口的开启和参数配置如果不规范会出现各种乱七八糟的软故障偶尔乱码、丢一个字节、长时间跑下来卡死甚至设备一上电就连接不上。工业串口还有一个特点设备类型五花八门。有的用RS232有的用RS485有的只要两线半双工有的必须带硬件流控。你面对的这些设备往往不会主动告诉你它内部是怎么配置的只能靠现场对接。更麻烦的是工业设备经常需要长时间稳定性测试可能连续运行几天几夜一旦串口底层打开的时候留下一个隐患后面就是断断续续的折磨。所以“开启串口”这件事在工业项目里并不是简单调用一个open()就行。它背后至少包括打开设备节点时阻塞和非阻塞方式怎么选波特率、数据位、校验位、停止位如何正确设置内核默认的终端处理模式要不要关掉软件流控、硬件流控该不该开串口被占用、设备拔插、权限不足该怎么处理RS485半双工时方向引脚怎么切换才不丢最后一个字节。这些东西全堆在“开启串口”四个字下面单独拎出来写一篇长文一点不夸张。1.2 自用版本的衡量标准不挑设备、不乱码、不丢帧我给自己订的规矩很简单这个版本换到任何一台Linux板卡上遇到任何常见的工业串口设备只要参数填对第一次打开就能稳定通信。不能因为设备节点是/dev/ttyUSB0和/dev/ttyS0有差异就改代码也不能因为某根线没接RTS/CTS就彻底收不到数据。所谓“无bug”我的标准是三个维度第一不挑设备。USB转串口也好板载UART也好工控机原生COM口也好打开方式一致配置思路一致。第二不乱码。波特率、数据位、校验位这些必须精确映射到系统termios结构体一个标志位错了协议就全乱了。第三不丢帧。不只要打开设备还要把内核的缓冲、流控、原始模式全部理干净确保数据进来之后不会被内核二次加工也不会因为软件流控被莫名暂停。后面这几点就是这篇文章的主要内容。2. 串口开启函数的核心设计从open到termios2.1 打开设备节点只写O_RDWR还不够在Linux下串口设备就是文件一般出现在/dev/ttyS0、/dev/ttyUSB0、/dev/ttyAMA0这些节点上。第一次写串口程序的人最常见的是直接fd open(/dev/ttyUSB0, O_RDWR | O_NONBLOCK)然后就拿着这个fd去配置了。这里有个很多教程不会讲的坑open的时候必须带O_NOCTTY意思是不要把当前进程变成这个终端的控制终端。如果不加一旦你的进程在后台运行可能收到终端的SIGTTOU、SIGTTIN之类的信号程序会莫名其妙被挂起甚至被杀掉。工业现场用systemd服务跑程序的情况非常多这个标志尤其重要。打开方式上我建议第一版就做两个路径阻塞模式open(dev, O_RDWR | O_NOCTTY)和非阻塞模式open(dev, O_RDWR | O_NOCTTY | O_NONBLOCK)。注意不要用O_NDELAY那是历史遗留语义在各种平台上有差异不如O_NONBLOCK清晰。我自用版本里block_mode参数控制的就是这个标志位。日常调试一般用阻塞模式配合后面要讲的VMIN和VTIME实现超时控制如果要写复杂的数据收发逻辑再用非阻塞模式配合select或poll。还有一点打开之前最好确认权限。很多USB转串口设备挂载后属于dialout组当前用户不在这个组里open会返回Permission denied。现场的工控机如果图省事很多人直接chmod 777 /dev/ttyUSB0我不推荐正规做法是把用户加进dialout组或者写udev规则。这个问题看起来很低级但每次换新机器都会遇到。2.2 参数配置波特率、数据位、校验、停止位如何映射成termios打开设备之后核心工作就是配置结构体struct termios。Linux的串口参数都放在这里面不能凭感觉瞎填每个位都有明确含义。首先要做的不是memset清零而是先调用tcgetattr(fd, opt)把内核当前对这个设备的配置读回来。这样做的原因很简单内核默认有些标志是合理的比如CLOCAL和CREAD如果你全部清零再从头设很容易漏掉东西。正确做法是保留现有配置然后修改你关心的一小部分。波特率这一块最容易被新手误解。cfsetispeed和cfsetospeed接受的不是数字115200而要传入B115200这种宏。常见的宏有B9600、B19200、B38400、B57600、B115200、B230400、B460800、B921600。所以我在函数里加了一个switch映射把用户传入的普通整数转换成对应的宏。数据位、校验位、停止位的组合常见就下面几种配置数据位校验位停止位需要设置的标志8N1最常见8None1CS88E18Even1CS8 PARENB8O18Odd1CS8 PARENB PARODD7E17Even1CS7 PARENB8N28None2CS8 CSTOPB数据位这里必须先清掉CSIZE位再设置否则可能残留上一个设备的数据位。校验位要同时操作PARENB和PARODD奇校验英文是Odd所以PARENB | PARODD偶校验只设PARENB无校验两个都不设。停止位只有1和2两种2停止位需要设置CSTOPB。这里多提一句非标准波特率比如某些惯导设备用的250000、某些扫码枪用的921600标准termios接口不一定直接支持。Linux下可以通过termios2或ioctl设置自定义波特率但工业设备绝大多数还是落在常用波特率档位上所以我自用版本先覆盖标准档够了。真遇到非标的再单独写一个open_serial_custom_baud()思路是一样的。2.3 原始模式还是规范模式别让内核帮你“加工”数据这一节可能是整个串口编程里最重要的。Linux的终端驱动默认工作工作在“规范模式”canonical mode内核会把你从串口收到的数据先缓存起来直到收到换行符\n之后才把整行数据交给调用read()的程序。这是给键盘终端设计的对串口二进制协议来说就是灾难。你明明发过来一串帧程序read()却永远等不到那一行结束。所以开启串口之后必须把ICANON关掉。同时还要关掉一组相关的标志ECHO关闭回显否则程序会把收到的数据原封不动发回串口ISIG关闭按键产生的SIGINT、SIGQUIT信号IEXTEN关闭终端扩展功能OPOST关闭输出处理否则内核会把\n转为\r\n之类的直接破坏协议内容。我习惯把这些标志位一次性清掉让串口进入所谓的“原始模式”raw mode。代码里经常看到一段opt.c_iflag ~(IGNBRK | BRKINT | PARMRK | ISTRIP | INLCR | IGNCR | ICRNL | IXON | IXOFF | IXANY); opt.c_oflag ~OPOST; opt.c_lflag ~(ECHO | ECHONL | ICANON | ISIG | IEXTEN);其中INLCR、IGNCR、ICRNL是对\r和\n做映射的对串口数据来说全部不需要。IGNBRK、BRKINT、PARMRK涉及到中断和校验错误有些场景可以留着但我统一关掉避免内核因为奇偶校验错误往数据流里插入0xFF 0x00之类的字节。另外ISTRIP要关闭意思是不要把所有字节的高位剥掉。这个特别隐蔽一旦被内核剥掉最高位8位数据帧直接变成7位语义数据就错了。2.4 流控工业设备最容易被忽略的坑流控分两种软件流控和硬件流控。工业串口对接我建议默认全部关闭除非你非常确定设备需要。软件流控就是IXON、IXOFF、IXANY用XON/XOFF字符对应十六进制0x11和0x13来暂停和恢复数据。听起来很智能但在二进制协议里就是个隐患。如果设备通过串口发送的数据中包含0x13内核会认为对方发来XOFF然后自动暂停发送你的程序就表现为“写到一半卡死”。这就是很多串口程序跑着跑着突然停止发送的经典原因。因此工业场景必须关掉软件流控。硬件流控对应的是CRTSCTS也就是RTS/CTS握手。很多USB转串口或者工控机原生串口不接RTS和CTS这两根线。如果不小心开了CRTSCTS程序write()的时候会一直等CTS信号而对方根本没拉这个信号结果就是数据发不出去程序看着像死锁。所以默认也是关闭。只有一种情况我才会开硬件流控你明确知道设备的说明书里要求使用RTS/CTS并且接线确实把RTS/CTS连上了。开发阶段先用默认关闭跑通协议再决定要不要开流控这是最稳妥的顺序。2.5 完整代码自用无bug版open_serial()把上面这些原则整合起来就是我一直在用的版本。函数不长但是每个分支、每个错误码都经过反复打磨#include stdio.h #include string.h #include unistd.h #include fcntl.h #include errno.h #include termios.h #include sys/ioctl.h int open_serial(const char *dev, int baud, int data_bits, char parity, int stop_bits, int block_mode) { int fd; struct termios opt; speed_t speed_baud; fd open(dev, O_RDWR | O_NOCTTY | (block_mode ? 0 : O_NONBLOCK)); if (fd 0) { perror(open serial); return -1; } if (tcgetattr(fd, opt) ! 0) { perror(tcgetattr); close(fd); return -2; } switch (baud) { case 9600: speed_baud B9600; break; case 19200: speed_baud B19200; break; case 38400: speed_baud B38400; break; case 57600: speed_baud B57600; break; case 115200: speed_baud B115200; break; case 230400: speed_baud B230400; break; case 460800: speed_baud B460800; break; case 921600: speed_baud B921600; break; default: fprintf(stderr, unsupported baud: %d\n, baud); close(fd); return -3; } cfsetispeed(opt, speed_baud); cfsetospeed(opt, speed_baud); opt.c_cflag ~CSIZE; switch (data_bits) { case 8: opt.c_cflag | CS8; break; case 7: opt.c_cflag | CS7; break; case 6: opt.c_cflag | CS6; break; case 5: opt.c_cflag | CS5; break; default: fprintf(stderr, invalid data bits: %d\n, data_bits); close(fd); return -4; } opt.c_cflag ~(PARENB | PARODD); if (parity E) { opt.c_cflag | PARENB; } else if (parity O) { opt.c_cflag | PARENB | PARODD; } else if (parity ! N) { fprintf(stderr, invalid parity: %c\n, parity); close(fd); return -5; } opt.c_cflag ~CSTOPB; if (stop_bits 2) { opt.c_cflag | CSTOPB; } else if (stop_bits ! 1) { fprintf(stderr, invalid stop bits: %d\n, stop_bits); close(fd); return -6; } opt.c_cflag | CLOCAL | CREAD; opt.c_cflag ~CRTSCTS; opt.c_iflag ~(IGNBRK | BRKINT | PARMRK | ISTRIP | INLCR | IGNCR | ICRNL | IXON | IXOFF | IXANY); opt.c_oflag ~OPOST; opt.c_lflag ~(ECHO | ECHONL | ICANON | ISIG | IEXTEN); opt.c_cc[VMIN] block_mode ? 1 : 0; opt.c_cc[VTIME] 0; tcflush(fd, TCIOFLUSH); if (tcsetattr(fd, TCSANOW, opt) ! 0) { perror(tcsetattr); close(fd); return -7; } return fd; }这个函数我最满意的一点是任何参数非法都会立刻返回负数并且关闭已经打开的fd。不会出现“参数写错但函数看起来成功然后后面莫名其妙收不到数据”的悬案。错误码从-1到-7每一项都有明确含义调用方拿到负数就知道是哪里出了问题。block_mode参数控制O_NONBLOCK同时我配置了VMIN1, VTIME0意思是阻塞模式下read()至少要等到一个字节才返回。如果你希望有超时返回可以把VTIME设成比如5那么一个字节没到也会最多等500毫秒返回。这个组合在实际项目中很常用。3. 把“开启串口”做成工业级超时、锁、RS485切换3.1 阻塞与非阻塞不同场景下怎么选open_serial()只是打开串口真正麻烦的是后面怎么收发数据。阻塞模式最简单一个线程专门读串口来了数据就处理没数据就睡着。但缺点是一旦read()阻塞很难中途退出。工业程序里我更喜欢非阻塞加select()的方式代码模样大概是这样fd_set rfds; struct timeval tv; FD_ZERO(rfds); FD_SET(fd, rfds); tv.tv_sec 1; tv.tv_usec 0; int ret select(fd 1, rfds, NULL, NULL, tv); if (ret 0) { ssize_t n read(fd, buf, sizeof(buf)); if (n 0) { // 处理收到的数据 } else if (n 0 errno ! EAGAIN) { // 真正的错误需要考虑重新开启串口 } } else if (ret 0) { // 超时可以定期检查其他状态 }这里有两个容易踩的坑。第一select()返回可读并不代表能读满你想要的字节数read()实际上可能只返回一部分数据所以上层一定要自己处理“粘包、半包”。第二read()返回-1不一定是错误如果errno是EAGAIN表示现在没数据继续等就行如果返回EINTR说明被信号打断了应该重新继续读这些都是真正的工业级代码里必须处理的分支。所以我的封装思路是open_serial()只负责打开和配置而数据收发单独写一个serial_read_frame()之类的状态机按帧长度、帧头帧尾去解析。千万不要把开启和收发混在一个函数里做不然以后换协议会很痛苦。3.2 串口被复用加锁和排他打开工业上位机最烦的一个问题串口被别的进程占用了。西门子组态软件、Modbus调试工具、自己的采集程序如果同时打开同一个COM口轻则数据串味重则直接崩溃。Linux下做排他比较直接打开设备后可以调一个ioctlint yes 1; ioctl(fd, TIOCEXCL, yes);这个调用之后其他进程再打开这个串口会失败。当前进程关闭fd后排他自动释放。如果你用的内核或者驱动不支持TIOCEXCL退一步可以用flock(fd, LOCK_EX | LOCK_NB)简单有效但是注意flock跟open的引用计数有时会让人困惑不适合所有驱动。在Windows下也有类似的共享模式问题C#的SerialPort默认是“独占”的如果之前没有正确释放再打开就会报“Access denied”。开发上位机的时候一定要把SerialPort对象放在using里确保异常路径也会被释放。这一点和Linux下close(fd)同样重要。3.3 RS485半双工的方向切换RS485是工业现场最常见的总线之一硬件上是半双工只有两根线同一时刻只能收或者发。很多USB转RS485模块方向上电后由芯片自动处理但一些板卡上会用GPIO手动控制收发器的DE/RE引脚。这个方向切换如果做得不好最常见的问题就是发完一帧数据后立刻拉低方向引脚导致最后一个字节还没有完全发送出去就被截断了。我自用模块里处理RS485方向的标准步骤如下open_serial()打开串口后单独初始化控制GPIO为输出并拉低默认接收状态发送数据前把GPIO拉高进入发送模式write(fd, buf, len)写入数据后一定要调用tcdrain(fd)等待发送缓冲区全部发送完确认清空后再把GPIO拉低切回接收模式。代码就是rs485_dir_set(1); // 拉高进入发送模式 write(fd, buf, len); tcdrain(fd); // 等待发送FIFO真正排空 rs485_dir_set(0); // 拉低恢复接收模式有些硬件方案是直接用RTS信号去控制方向这种情况下可以配置struct serial_rs485并调用TIOCSRS485。但前提是底层驱动支持USB转串口很多不支持这个ioctl不如GPIO手动控制通用。还有个小细节半双工切换需要“安静窗口”发送结束后不要立刻读串口最好等几个位的时间再切回接收否则对方回包的前几个字节可能丢失。具体延时可以用一个字节时长估算如果波特率是9600一个字节大约1ms切到接收后延时2~3个字节时间就很稳。3.4 设备拔插和重新打开USB转串口在工业现场是非常普遍的外设但它也带来一个麻烦设备和串口线热插拔之后Linux内核会重新枚举USB设备原来的/dev/ttyUSB0可能变成/dev/ttyUSB1如果你的程序一直死等旧节点那必然连不上。我处理这个问题分两步。第一步用udev固定设备名。在/etc/udev/rules.d/写一条规则根据USB设备的idVendor和idProduct创建一个稳定符号链接比如/dev/ttyMeter。这样不管你插到哪个USB口程序都固定打开同一个路径。第二步程序里做“串口状态机”。不能在open失败后直接退出而是定期重试。检测到串口断开read返回严重错误立刻关闭旧fd清理状态间隔几百毫秒重新open_serial()。工业设备重启也是同理经常是串口服务器比程序启动慢所以要支持自动化重连。重连的时候有一个必须注意的点不能只重新open()一定要重新cfsetispeed、cfsetospeed、tcsetattr。因为内核在设备重新枚举后终端参数可能已经恢复默认值。光把open_serial()跑一遍也就是为了这个。3.5 MCU端串口开启DMA和中断别漏配置前面主要讲Linux上位机其实MCU端开启串口的思路完全一样只是换了一套寄存器。STM32、GD32这类芯片初始化串口并不是像很多人想的那样“设置波特率就完事”你需要同时配置GPIO复用和时钟USART外设时钟中断优先级和NVIC如果使用DMA还要配置DMA方向、数据宽度、环形缓冲区。很多项目跑起来乱码或丢数据排查半天最后发现MCU端少了GPIO复用配置或者DMA只开了半天中断导致接收缓冲区溢出。所以在做整机联调时我通常让两端都先跑最简轮询模式确认协议通了再往MCU端加DMA、加环形缓冲。这样如果出问题排查面会小很多。4. 常见问题排查实录与调试工具4.1 排查速查表打不开、乱码、丢帧、卡死下面这个表是我这些年整理出来的按现象分类遇到问题直接对着查大部分情况都很管用。现象常见原因排查手段解决办法打开串口返回失败设备节点不存在ls /dev/ttyUSB*拔插线缆重新插拔确认驱动加载打开串口提示权限不够用户不在dialout组执行usermod -aG dialout $USER重新登录或用sudo打开成功但收不到数据硬件流控被错误开启检查CRTSCTS是否清除按文中代码关闭流控打开成功但收不到数据没设置 CLOCALCREAD用stty -F /dev/ttyUSB0 -a查看数据乱码波特率不匹配两端确认波特率修改参数重新打开数据乱码校验位不正确查看设备手册统一8N1或8E1丢字节、丢帧线缆太长或干扰缩短线缆检查屏蔽降波特率或换RS485写入后程序卡死开了软件流控收到XOFF确认IXON/IXOFF已关闭用原始模式程序偶发退出read返回EINTR/EAGAIN未处理打印errno循环重读多个程序抢串口其他进程占用见4.2用排他锁排查的时候有一个顺序很关键先硬件、再系统、最后才是应用层。很多新手一上来就改代码结果发现是USB线接触不良或者对方设备波特率不对。我现在的习惯是先在串口助手里把数据收通再怀疑自己的open_serial()。4.2 Windows下查看串口被哪个程序占用虽然我主力是Linux但偶尔也要在Windows上用C#或者WinForm调试。Windows下串口被占用的表现很直接打开COM3的时候直接抛异常或者提示“端口已被打开”。但具体哪个程序占用系统不会直接告诉你需要借助工具。我常用的有两个第一款是Sysinternals的Process Explorer。打开后按快捷键CtrlF弹出搜索框输入COM3它会列出所有打开过这个串口句柄的进程。看到结果后右键进程选择关闭句柄或者直接在任务管理器里结束进程。第二款是Device Monitoring Studio它不仅能看占用还能监控这个串口上所有数据收发记录相当于一个更高级的“串口监听器”。在Windows现场排查一些“底层驱动偷偷收发数据”的问题时这个工具非常管用。命令行党也有办法下载Sysinternals的handle64.exe执行handle64.exe -a COM3它会显示进程名和PID。注意Windows下打开失败的错误码如果是5那多半就是Access Denied十有八九是端口被占用要不就是权限不足。4.3 USB转串口的坑CH340、CP2102、FT232 怎么选USB转串口的芯片是串口调试的第一个拦路虎。市面主流有三类CH340、CP2102、FT232。CH340最便宜国产芯片很多十几块的USB转TTL小板都用它。Win7经常要手动装驱动Linux内核自带ch341驱动一般插上就能用。缺点是抗干扰能力相对弱有些山寨版会虚焊或者上拉电阻缺失。CP2102是Silicon Labs的产品稳定性比CH340好一截驱动也齐全。很多工规USB转串口用这个性价比不错。FT232是FTDI的芯片算是工业现场的老大哥。贵但驱动、兼容性、抗干扰都是最靠谱的。如果现场设备协议复杂、线缆长、环境电磁干扰大我现在会直接选择FT232方案省下来的排查时间远超那点差价。另外有三个布线上的坑比芯片选型更容易出事第一必须共地。USB转串口的GND和板卡GND一定要接好否则信号参考地不一致接收数据就是乱的。这是“乱码”最常见的物理原因。第二电平要匹配。如果USB转串口输出是5V TTL而你接的是3.3V单片机就可能会烧IO口。选板卡的时候看清有没有3.3V/5V跳线或者加电平转换芯片。第三线材质量真的影响大。USB线镀铜好一点的信号就是稳。别在这上面省钱。4.4 调试工具体验工具不在多顺手就行。Linux下我常用picocom或minicom做简单验证偶尔用cutecom图形界面。但更重要的一招是“自发自收”把串口的TX和RX直接短接然后在调试工具里发一串数据。如果工具能收回来和自己发出去的完全一样说明从USB转串口到驱动程序、到系统配置整条链路都是通的。再往下接设备问题就只可能在设备和接线上了。这个步骤我每次都会做特别节省时间。Windows下用SSCOM或XCOM都很方便支持定时发送、进制显示、保存日志。工业调试的时候日志保存功能尤其重要程序跑了一晚上出问题如果没存日志第二天只能从头开始查。所以我自己的工具箱里每个上位机程序都会加上串口原始数据落盘功能。5. 版本演进这个“无bug”版本到底改了什么5.1 早期版本踩过的三个大坑最早的版本远没有现在这么干净光是“开启串口”这一个动作我就在实际项目里填过不少坑。第一次写串口程序什么都没关就配置完波特率直接read()。结果数据明明发过来了却永远读不到。后来才明白是规范模式的内核按行缓存导致的。那一次让我知道串口程序必须主动让自己进入原始模式默认配置是给终端用的不是给工业数据用的。第二次是在写一个和仪表通信的程序运行一段时间后写数据就卡死了。排查了很久最后发现仪表发的数据里有一个字节恰好是0x13也就是XOFF字符。因为我没有关闭软件流控内核收到0x13以后自动“暂停发送”设备一直等我继续发而我在等设备回包两边就这么死等下去。从那以后我配置串口的第一件事就是清掉IXON/IXOFF。第三次是串口打开失败时我只是简单返回了-1但忘了关闭已经打开的fd或者设备节点正在被别的东西占用。最后程序重试多次跑到文件描述符耗尽系统再也打不开任何串口。后来我把所有错误分支都统一成“关fd、返回负数”再也不留半开的句柄。还有一次是USB串口拔掉后没有及时关闭fd重新插上程序还在读写旧节点。内核那边早就对这个fd做了“设备消失”处理读写会直接出错但我当时没判断错误导致程序崩了。这些经验最后都变成了现在代码里的边界条件。5.2 边界条件压倒功能我心中的“无bug”定义总有人看到我的这段open_serial()觉得代码量挺小没什么技术含量。但真正的门槛不在“能打开”而在“打开失败怎么办”“参数写错怎么办”“设备突然拔掉怎么办”“有没有给后续留一个干净的可复现状态”。我对“自用无bug版本”的理解是不是我写的代码永远不会出错而是它出错的时候会清楚地告诉你哪里错了并且不会留下一个半打开的fd、不会静默吞掉错误、不会让后面的人完全摸不着头脑。一点个人心得收尾每次接一个新的工业设备我的调试顺序都是固定的——先用串口助手自发自收取一遍再跑open_serial()打开端口然后发送一条固定的握手帧。如果握手都通过我才会去看应用层协议。这个习惯帮我少走了很多弯路。串口底层脏活累活一大堆但只要把开启这步做扎实整个项目后面都会轻松很多。
返回列表