当前位置: 首页 > news >正文

Linux下C语言串口编程实战:从原理到健壮代码实现

1. 项目概述:为什么串口通信在Linux下依然重要?

在嵌入式开发、工业控制、物联网设备调试这些领域,如果你和硬件打交道,那么串口通信绝对是你绕不开的一项基础技能。很多人觉得,现在都是网络、USB满天飞的时代了,串口是不是过时了?恰恰相反,串口因其简单、可靠、对硬件要求极低,至今仍是设备间、设备与上位机之间最直接、最底层的通信方式之一。无论是给单片机烧录程序、读取传感器数据,还是与PLC、工控机、路由器交换信息,串口往往是第一选择,甚至是唯一选择。

Linux系统,作为服务器和嵌入式领域的绝对主力,对串口的支持是内核级别的,非常完善。但正因为其“底层”和“完善”,对于刚接触的开发者来说,如何用C语言在Linux下正确地打开、配置、读写一个串口,反而成了一堵无形的墙。网上资料零散,有的只讲理论,有的代码片段无法直接运行,更别提那些隐藏在细节里的“坑”了。这篇文章,我就结合自己多年在嵌入式Linux开发中踩过的坑、积累的经验,带你从原理到实战,彻底搞懂Linux下的串口编程。我们不只讲“怎么做”,更要讲清楚“为什么这么做”,让你不仅能写出能跑的代码,更能写出稳定、健壮的代码。

2. 串口通信核心原理与Linux下的抽象

2.1 串口硬件与通信协议基础

串口,全称串行通信接口,其核心思想是数据一位一位地按顺序传输。与我们熟悉的USB或网线(并行传输多个数据位)不同,它只需要一对数据线(TX发送,RX接收)就能完成双向通信,硬件成本极低。

最经典的串口标准是RS-232,它定义了电气特性(如±3V至±15V代表1和0)、物理接口(DB9或DB25接头)和基本的控制信号。虽然我们现在更多接触的是其逻辑电平的变种,如TTL电平(0V和3.3V/5V),但通信协议本身是相通的。

一个标准的异步串行数据帧包含以下几个部分:

  1. 起始位:一个逻辑低电平,标志一帧数据的开始。
  2. 数据位:通常是5、6、7或8位,代表实际传输的数据。
  3. 校验位:可选,用于简单的错误检测,如奇校验或偶校验。
  4. 停止位:1、1.5或2个逻辑高电平,标志一帧数据的结束。

通信双方必须预先约定好相同的参数,也就是我们常说的“波特率、数据位、停止位、校验位”,缺一不可。波特率决定了数据传输的速度,单位是bps(位每秒)。常见的波特率有9600, 115200等。115200bps意味着每秒传输115200位数据,对于8位数据、无校验、1停止位的配置,每秒大约能传输115200 / 10 = 11520字节的有效数据。

注意:很多人混淆波特率和比特率。在串口通信中,由于一个数据帧包含起始位、停止位等额外开销,有效数据速率(比特率)是低于波特率的。例如9600波特率下,传输8位数据,实际有效数据速率可能只有9600 / 10 * 8 = 7680 bps。

2.2 Linux如何管理串口设备:一切皆文件

Linux哲学中“一切皆文件”在串口设备上体现得淋漓尽致。当你在Linux系统上插入一个USB转串口适配器,或者使用开发板上的原生串口,系统会在/dev目录下创建一个对应的设备文件。常见的命名规则是ttyS0,ttyS1(通常代表原生串口,即COM1, COM2),或者ttyUSB0,ttyUSB1(代表USB转串口设备)。

这个设备文件,就是你的C语言程序与物理串口硬件交互的桥梁。你可以像操作普通文件一样,使用open()read()write()close()等系统调用来操作它。但是,串口不是普通文件,它有一大堆属性需要配置,比如波特率、数据位等。这就需要用到termios这个关键的数据结构和一系列函数。

termios结构体定义在<termios.h>头文件中,它包含了控制终端(包括串口)行为的所有参数。通过tcgetattr()tcsetattr()函数,我们可以获取和设置这些参数。理解并正确配置termios的各个字段,是成功进行串口编程的第一步,也是最容易出错的一步。

3. C语言实战:从零构建一个健壮的串口通信程序

理论讲得再多,不如一行代码。接下来,我们一步步用C语言实现一个完整的串口操作模块。这个模块将包含初始化、发送、接收等基本功能,并充分考虑错误处理和资源管理。

3.1 环境准备与串口设备确认

在开始写代码之前,首先要确定你的串口设备在哪里。将你的USB转串口线插入电脑,或者在虚拟机中配置好串口映射。

打开终端,输入以下命令:

dmesg | grep tty

或者

ls /dev/ttyUSB* /dev/ttyS* /dev/ttyACM*

你会看到类似/dev/ttyUSB0的输出,这就是你的串口设备文件。记下这个路径,我们将在代码中使用它。

为了有权限操作这个设备文件,你可能需要将当前用户加入到dialout组(某些系统是uucp组),或者直接使用sudo来运行你的程序。更推荐前者,因为更安全。

sudo usermod -a -G dialout $USER

执行后需要注销并重新登录才能生效。

3.2 核心代码实现与逐行解析

下面是一个封装好的串口操作函数集,我将逐段解释其关键点。

#include <stdio.h> #include <stdlib.h> #include <string.h> #include <unistd.h> #include <fcntl.h> #include <termios.h> #include <errno.h> // 打开并配置串口 int serial_open(const char *port, int baudrate) { int fd; struct termios options; // 1. 以读写、非阻塞方式打开设备文件 fd = open(port, O_RDWR | O_NOCTTY | O_NONBLOCK); if (fd < 0) { perror("无法打开串口设备"); return -1; } // 2. 清空输入输出缓冲区,避免残留数据干扰 tcflush(fd, TCIOFLUSH); // 3. 获取当前的终端属性 if (tcgetattr(fd, &options) < 0) { perror("获取串口属性失败"); close(fd); return -1; } // 4. 设置输入输出波特率 cfsetispeed(&options, baudrate); cfsetospeed(&options, baudrate); // 5. 配置基本参数:8位数据,无校验,1位停止位 options.c_cflag &= ~PARENB; // 禁用奇偶校验 options.c_cflag &= ~CSTOPB; // 1位停止位 (如果设为1,则代表2位停止位) options.c_cflag &= ~CSIZE; // 清除数据位掩码 options.c_cflag |= CS8; // 设置为8位数据位 options.c_cflag &= ~CRTSCTS; // 禁用硬件流控 (RTS/CTS) options.c_cflag |= CREAD | CLOCAL; // 启用接收器,忽略调制解调器控制线 // 6. 配置输入模式:关闭软件流控,启用规范模式(按行读取) options.c_iflag &= ~(IXON | IXOFF | IXANY); // 关闭软件流控 options.c_iflag &= ~(INLCR | ICRNL); // 禁止特殊字符转换 // 7. 配置输出模式:关闭输出处理 options.c_oflag &= ~OPOST; // 原始输出模式,不进行任何处理 // 8. 配置本地模式:关闭回显,关闭规范模式(改为非规范模式,实现原始数据读取) options.c_lflag &= ~(ICANON | ECHO | ECHOE | ISIG); // 这里我们暂时使用规范模式,便于按行读取。如需原始数据,需设置非规范模式并配置VTIME和VMIN。 // 9. 设置超时控制(非规范模式下的关键!) // 这里我们先设置为规范模式,所以VTIME和VMIN在规范模式下意义不同。 // 为了演示非规范模式,我们注释掉下面的c_lflag设置,并启用非规范模式配置。 // options.c_lflag &= ~ICANON; // 启用非规范模式 // options.c_cc[VTIME] = 5; // 等待时间,单位是0.1秒,这里设0.5秒 // options.c_cc[VMIN] = 0; // 最小读取字符数,0表示即使没有数据也立即返回 // 10. 将配置立即应用到串口 if (tcsetattr(fd, TCSANOW, &options) < 0) { perror("设置串口属性失败"); close(fd); return -1; } // 11. 再次清空缓冲区 tcflush(fd, TCIOFLUSH); printf("串口 %s 已打开并配置成功,波特率:%d\n", port, baudrate); return fd; }

关键点解析:

  1. 打开标志O_NOCTTY防止该设备成为控制终端,O_NONBLOCK以非阻塞方式打开,防止open调用在等待某些事件(如载波检测)时卡住。但在后续读写时,我们可能会将其改回阻塞模式。
  2. tcflush:这一步至关重要。刚打开的串口缓冲区里可能有垃圾数据,不清空的话第一次read可能会读到莫名其妙的东西。
  3. CLOCAL标志:这个标志告诉系统“忽略调制解调器状态线”。对于直接连接的串口线(比如连接单片机),必须设置此标志,否则程序会一直等待“数据载波检测”信号,导致openread阻塞。
  4. 规范模式 vs 非规范模式
    • 规范模式:输入以行为单位进行处理。read会一直等待,直到读到行结束符(如\n)或缓冲区满。适合与需要命令行交互的设备通信。
    • 非规范模式:输入是原始的字节流。read的行为由VMINVTIME控制。这是与大多数嵌入式设备(如传感器、单片机)通信的推荐模式,可以精确控制读取的字节数和超时。
  5. VMINVTIME:这是非规范模式的灵魂。
    • VMIN = 0, VTIME > 0read立即返回。如果有数据,就读取;如果没有数据,等待VTIME*0.1秒后返回0。这是定时器模式,适合轮询。
    • VMIN > 0, VTIME = 0read会一直阻塞,直到收到至少VMIN个字节。这是阻塞模式
    • VMIN > 0, VTIME > 0read会等待VMIN个字节,但如果在收到VMIN个字节前,时间超过了VTIME*0.1秒,它也会返回已收到的字节数。这是混合模式,最常用。

3.3 数据收发函数与主循环示例

有了初始化函数,我们再实现发送和接收函数。

// 发送数据 int serial_write(int fd, const unsigned char *data, int length) { int ret; ret = write(fd, data, length); if (ret < 0) { perror("串口写入失败"); } else if (ret != length) { fprintf(stderr, "警告:试图写入 %d 字节,实际写入 %d 字节\n", length, ret); } // 注意:write不一定能一次性写完所有数据,在非阻塞模式下尤其如此。 // 生产代码中应考虑循环写入,直到所有数据发送完毕或出错。 return ret; } // 接收数据(简单示例,使用规范模式按行读) int serial_read_line(int fd, unsigned char *buffer, int buffer_size) { int ret; memset(buffer, 0, buffer_size); ret = read(fd, buffer, buffer_size - 1); // 留一个位置给字符串结束符 if (ret < 0) { if (errno != EAGAIN && errno != EWOULDBLOCK) { perror("串口读取失败"); } return ret; // 返回错误码 } else if (ret == 0) { // 在非阻塞模式下,没有数据可读时会返回0 return 0; } buffer[ret] = '\0'; // 确保字符串结束 // 简单处理:如果读到换行符,就认为是一行 if (buffer[ret-1] == '\n' || buffer[ret-1] == '\r') { buffer[ret-1] = '\0'; // 去掉换行符 } return ret; }

一个简单的主函数示例,演示如何调用:

int main() { int fd; unsigned char buffer[256]; int n; // 打开串口,波特率115200 fd = serial_open("/dev/ttyUSB0", B115200); if (fd < 0) { return 1; } // 发送数据 serial_write(fd, "AT\r\n", 4); usleep(100000); // 等待100ms,让设备有时间响应。这是一个粗略的做法。 // 尝试读取一行响应 n = serial_read_line(fd, buffer, sizeof(buffer)); if (n > 0) { printf("收到响应: %s\n", buffer); } // 简单的主循环:读取用户输入并发送,同时监听串口返回 printf("进入交互模式,输入‘quit’退出\n"); while (1) { fd_set readfds; struct timeval tv; int retval; // 监听标准输入和串口 FD_ZERO(&readfds); FD_SET(STDIN_FILENO, &readfds); FD_SET(fd, &readfds); int max_fd = (fd > STDIN_FILENO) ? fd : STDIN_FILENO; // 设置超时 tv.tv_sec = 1; tv.tv_usec = 0; retval = select(max_fd + 1, &readfds, NULL, NULL, &tv); if (retval == -1) { perror("select()"); break; } else if (retval) { // 标准输入有数据(用户输入) if (FD_ISSET(STDIN_FILENO, &readfds)) { if (fgets(buffer, sizeof(buffer), stdin)) { // 去除换行符 buffer[strcspn(buffer, "\n")] = 0; if (strcmp(buffer, "quit") == 0) { break; } strcat(buffer, "\r\n"); // 添加回车换行 serial_write(fd, buffer, strlen(buffer)); } } // 串口有数据 if (FD_ISSET(fd, &readfds)) { n = serial_read_line(fd, buffer, sizeof(buffer)); if (n > 0) { printf("<< %s\n", buffer); } } } // 超时,继续循环 } close(fd); printf("程序退出\n"); return 0; }

这个主循环使用了select系统调用来同时监听标准输入(键盘)和串口文件描述符,实现了简单的双向交互。这是编写串口调试工具或与交互式设备通信的常用模式。

4. 高级配置与性能调优要点

4.1 硬件流控与软件流控

在某些高速或长距离通信场景,为了防止数据丢失,需要启用流控。

  • 硬件流控(RTS/CTS):使用额外的两根线(RTS和CTS)来协调发送和接收。当接收方缓冲区快满时,通过CTS线通知发送方暂停。需要在c_cflag中设置CRTSCTS。这要求你的串口线和设备都支持。
    options.c_cflag |= CRTSCTS; // 启用硬件流控
  • 软件流控(XON/XOFF):通过发送特殊的控制字符(XON, ASCII 17;XOFF, ASCII 19)来控制数据流。在c_iflag中设置IXONIXOFF。这种方式会占用数据通道,且在某些二进制数据传输中可能引发问题(如果数据中恰好包含XON/XOFF字符),所以使用较少。

4.2 非规范模式下的精确控制

对于需要读取固定长度数据包或要求低延迟的应用,必须使用非规范模式。下面是一个配置示例:

// 在serial_open函数中,替换规范模式的设置 options.c_lflag &= ~(ICANON | ECHO | ECHOE | ISIG); // 确保ICANON被清除 // 设置非规范模式下的读取行为 options.c_cc[VTIME] = 1; // 超时时间 = 0.1秒 options.c_cc[VMIN] = 64; // 最小读取字符数 = 64 // 同时,为了确保能读取到任意字节(包括0x00),需要关闭输入处理 options.c_iflag &= ~(IGNBRK | BRKINT | PARMRK | ISTRIP | INLCR | IGNCR | ICRNL | IXON); options.c_oflag &= ~OPOST; options.c_cflag |= CREAD | CLOCAL; options.c_cflag &= ~CSIZE; options.c_cflag |= CS8; options.c_cflag &= ~PARENB; options.c_cflag &= ~CSTOPB; options.c_cflag &= ~CRTSCTS;

这个配置意味着:read调用会尝试读取最多64个字节,但如果在0.1秒内没有新数据到达,它就会返回当前已读取到的所有字节。这非常适合接收不定长但需要快速响应的数据包。

4.3 串口读写中的缓冲区管理

Linux内核为每个打开的串口维护着输入和输出缓冲区。tcflush(fd, queue_selector)用来管理这些缓冲区:

  • TCIFLUSH:清空输入缓冲区(已收到但未读取的数据)。
  • TCOFLUSH:清空输出缓冲区(已写入但未发送的数据)。
  • TCIOFLUSH:清空输入和输出缓冲区。

在以下情况清空缓冲区是必要的:

  1. 打开串口后,清除可能存在的垃圾数据。
  2. 改变串口参数(波特率等)前后。
  3. 在发送重要命令前,清空输入缓冲区,避免读到旧的响应。
  4. 程序异常退出或重启后重新连接时。

5. 实战中常见的“坑”与排查技巧

5.1 权限问题与设备节点

问题open失败,提示“Permission denied”。排查

  1. 检查当前用户是否在dialoutuucp组内:groups $USER
  2. 检查设备文件权限:ls -l /dev/ttyUSB0。通常权限是crw-rw----,所属组是dialout
  3. 临时解决方案:使用sudo运行程序。永久解决方案:将用户加入正确的组并重新登录。

问题:设备文件/dev/ttyUSB0不存在。排查

  1. 确认硬件已连接。dmesg | tail查看内核日志,确认设备是否被识别。
  2. 确认驱动已加载。USB转串口芯片(如FTDI, PL2303, CH340)通常需要内核驱动。使用lsmod | grep usbserial查看。
  3. 在某些系统中,可能需要modprobe usbserial手动加载模块。

5.2 波特率不匹配与数据乱码

问题:发送和接收的数据全是乱码。排查

  1. 首要怀疑对象:波特率。确保程序设置的波特率与设备端(如单片机)的波特率完全一致。115200和9600差很多,但57600和38400有时在低速时也能偶然收到几个正确字符,极具迷惑性。
  2. 数据格式:检查数据位、停止位、校验位是否匹配。最常见的是8N1(8数据位,无校验,1停止位)。
  3. 电平问题:确认是RS-232电平(±电压)还是TTL电平(0V/3.3V/5V)。电脑的串口通常是RS-232,而单片机是TTL,直接连接会损坏设备!必须使用USB-TTL转换器或电平转换芯片。

5.3 阻塞、非阻塞与超时控制

问题read函数一直卡住不返回。排查

  1. 检查是否设置了CLOCAL标志。如果没有,read会等待DCD(数据载波检测)信号。
  2. 检查是否处于规范模式。规范模式下,read会等待行结束符。如果设备从不发送\nread就会一直等。
  3. 解决方案:使用非规范模式,并结合VMINVTIME设置合理的超时。或者,使用fcntl将文件描述符设置为非阻塞模式,然后配合selectpoll进行超时等待。
    // 设置为非阻塞模式 int flags = fcntl(fd, F_GETFL, 0); fcntl(fd, F_SETFL, flags | O_NONBLOCK); // 然后使用select/poll进行带超时的监听

5.4 数据丢失与粘包处理

问题:发送的数据很快,但接收端发现数据不完整,或者几条消息粘在一起了。排查与解决

  1. 启用流控:如果波特率很高(如115200以上)或线路较长,考虑启用硬件流控。
  2. 调整内核缓冲区:Linux内核的串口缓冲区大小是有限的。可以通过ioctl调整,但更常见的做法是在应用层解决。
  3. 应用层协议设计:这是解决粘包问题的根本。不要依赖read一次返回的就是一个完整的数据包。你应该:
    • 定义帧结构:例如,使用固定的帧头(如0xAA, 0x55)、长度字段、数据、校验和(如CRC16)和帧尾。
    • 实现状态机解析:在接收函数中,实现一个简单的状态机,逐个字节读取并判断,只有收到完整的、校验正确的帧才交给业务逻辑处理。
    • 使用select/poll+非阻塞读+循环:这是高性能串口程序的标配。在select指示有数据可读后,在一个循环中反复调用read,直到将内核缓冲区中的数据全部读完。

5.5 多线程环境下的串口访问

问题:在多线程程序中同时读写串口,导致数据错乱或程序崩溃。原则串口设备是一个典型的需要互斥访问的资源。绝对不能在没有同步机制的情况下,让多个线程同时调用readwrite解决方案

  1. 最简方案:将所有串口操作封装到一个模块中,对外提供线程安全的API。在模块内部使用互斥锁(pthread_mutex_t)保护所有对文件描述符fd的操作。
  2. 生产者-消费者模型:创建一个专用的串口读写线程。其他线程通过线程安全的队列(如使用互斥锁和条件变量实现的队列)向这个线程发送要写入的数据;串口线程将读到的数据放入另一个队列,供其他线程消费。这样串口资源只被一个线程访问,简化了同步。

6. 调试技巧与工具推荐

  1. 先验证硬件和线路

    • 使用cat /dev/ttyUSB0命令(需要先stty配置好参数,或使用screenminicom),直接查看串口原始输出。如果能收到数据,说明硬件和驱动没问题。
    • 使用echo "test" > /dev/ttyUSB0发送简单数据,在设备端(如用逻辑分析仪或另一个串口工具)查看是否收到。
  2. 使用成熟的终端工具进行交叉验证

    • minicom:Linux下最经典的串口工具。sudo minicom -s进入配置菜单,设置端口和参数。
    • screen:一个简单的替代品。screen /dev/ttyUSB0 115200
    • picocom:更轻量,适合脚本调用。picocom -b 115200 /dev/ttyUSB0
    • CuteComGtkTerm:图形化工具,对新手更友好。

    先用这些工具确认你的设备通信正常,参数正确,然后再用你自己的C程序去模拟工具的行为。

  3. 逻辑分析仪或示波器:当通信问题非常诡异,软件层面无法解决时,终极武器就是逻辑分析仪。直接抓取TX、RX线上的波形,可以直观地看到起始位、数据位、停止位,精确测量波特率,是排查硬件和底层协议问题的利器。

  4. 程序内日志:在你的串口读写函数中加入详细的日志,记录每次打开、配置、发送、接收的字节数和内容(以十六进制打印)。当问题出现时,这些日志是无价之宝。

7. 从示例到项目:构建一个简单的AT命令调试器

掌握了基础,我们可以做一个更有用的东西:一个简单的AT命令调试器。很多物联网模块(如4G、Wi-Fi、蓝牙模块)都使用AT命令通过串口进行控制。

设计思路

  1. 封装一个更健壮的serial_port结构体,包含文件描述符、互斥锁、接收缓冲区、解析状态机等。
  2. 实现非阻塞读取和数据帧解析。AT命令通常以\r\n结尾,我们可以以此作为帧分隔符。
  3. 实现命令发送和响应等待功能,并设置超时。
  4. 提供同步和异步两种API。同步API发送命令后阻塞等待响应;异步API注册一个回调函数,当收到特定响应(如+EVENT)时自动调用。

关键代码结构示意:

typedef struct { int fd; pthread_mutex_t lock; unsigned char rx_buffer[1024]; int rx_index; // ... 其他状态,如解析状态机状态、回调函数列表等 } serial_port_t; int at_send_command(serial_port_t *port, const char *cmd, char *resp_buf, int buf_len, int timeout_ms); void at_register_async_handler(serial_port_t *port, const char *prefix, void (*handler)(const char*));

这个项目会综合运用我们讲到的所有知识点:串口配置、非阻塞IO、多线程同步、协议解析、超时处理。把它实现出来,你对Linux串口编程的理解会上一个大台阶。

最后,串口编程就像一门“手艺”,原理不难,但细节决定成败。最宝贵的经验往往来自于一次次调试和踩坑。希望这篇长文能为你铺平道路,但真正的掌握,还需要你动手去写,去调试,去解决那些意料之外的问题。当你能够稳定可靠地与各种硬件设备对话时,你会发现这片看似古老的技术领域,依然充满着创造的乐趣和挑战。

http://www.cnnetsun.cn/news/3596263.html

相关文章:

  • Boost ASIO异步网络编程:从核心原理到高并发服务器实战
  • 人类学思维在商业决策与产品设计中的应用
  • AgentFAIR:基于多智能体协作的地理空间数据FAIR原则自动化评估框架
  • Exchange Server AD Sync原理与混合部署实战指南
  • HC-05蓝牙模块的使用
  • Unity Input System实战:基于Action Map构建多状态输入管理框架
  • Unity Mod Manager (UMM) 模组管理框架:原理、安装与故障排查指南
  • 企业大模型全链路学习框架与应用实践
  • C++构造函数初始化列表:原理、应用与性能优化详解
  • C++智能指针实战指南:从内存管理到RAII范式
  • CDN技术深度解析:原理、架构与实战应用
  • 企业级AI API限流策略与Key管理实践
  • 跨境电商商品采集skill来了,可部署龙虾、workbuddy
  • C++计算几何算法库:从基础原理到工程实践
  • Oracle游标管理机制与性能优化实践
  • 影刀RPA 网页登录处理:表单登录与状态判断
  • Kimi Hosted Agent平台:企业级AI代理API接入与实战指南
  • Claude Code使用限额提升:AI编程助手安装配置与优化指南
  • C++数组操作实战:商品库存管理模拟题精解与竞赛技巧
  • C++哈希表深度解析:从原理到性能优化实战
  • 建站免费SEO工具推荐:网站不收录诊断,3分钟查明原因的4款工具
  • Windows 11安装Open Babel 3.1.1指南与化学数据处理
  • 系统架构设计师认证:技术人职业跃迁的关键路径
  • Python Pygame实战:从零构建经典扫雷游戏,掌握二维数组与事件驱动编程
  • LlamaIndex节点解析实战:中文RAG优化与分块策略
  • 【Kimi联网搜索结果安全白皮书】:首次公开企业级审计日志中隐藏的11类敏感信息泄露风险
  • 职场AI写作进阶:公文、汇报、方案的润色与逻辑升级
  • Arm架构AIOS联盟技术解析:统一生态下的开发实践与优化
  • D:\UnityEditor\2019.4.40f1c1\Editor\Data\il2cpp\build/deploy/net471/UnityLinker.exe did not run prop
  • TI N2HET高精度定时器:引脚安全、信号滤波与中断机制详解