Linux 串口应用编程完全学习笔记(从 API 到 GPS 实战)

@[TOC] Linux 串口应用编程完全学习笔记(从 API 到 GPS 实战)

🎉 写给读者 :串口(UART)是嵌入式开发中最常用的通信接口之一。本文从 Linux 串口编程的 统一接口 (open/ioctl/read/write)讲起,深入剖析 termios 结构体,带你实现 自发自收测试GPS 模块数据解析 。每个知识点都配有 白话解释生活化类比完整代码常见错误解决,让你彻底掌握串口应用编程。


1. Linux 串口通信概述 ------ 一切皆文件

1.1 一句话白话解释

在 Linux 系统中,串口设备也被当作 文件 来操作。你只需要 open 打开设备文件(如 /dev/ttymxc5),用 read 读数据,用 write 写数据,用 ioctl 或专用函数设置 行规程(波特率、数据位、停止位等)。

1.2 生活化类比 📞

把串口通信想象成 打电话

  • 打开设备 = 拿起电话听筒(open
  • 设置行规程 = 调好通话频率、音量(设置波特率、数据位等)
  • 写数据 = 对着话筒说话(write
  • 读数据 = 听对方说话(read
  • 关闭设备 = 挂断电话(close

1.3 Linux 串口通信框图

复制代码
┌─────────────────────────────────────────────┐
│                   APP                        │
│   (调用 open/read/write/ioctl/tcsetattr等)   │
├─────────────────────────────────────────────┤
│              系统调用层 (VFS)                 │
├─────────────────────────────────────────────┤
│            tty 核心层 / 行规程层              │
│         (处理 termios、线路规则等)            │
├─────────────────────────────────────────────┤
│              UART 设备驱动                    │
├─────────────────────────────────────────────┤
│              硬件 (UART 控制器)               │
└─────────────────────────────────────────────┘

1.4 串口编程的套路(三步走)

步骤 函数 说明
1 open() 打开串口设备文件,如 /dev/ttymxc5
2 设置行规程 使用 tcgetattr/tcsetattr 配置 termios 结构体
3 read()/write() 读写数据
4 close() 关闭设备

2. termios 结构体 ------ 串口配置的核心

2.1 一句话白话解释

struct termios 是一个 参数包,里面存放了串口的所有配置:波特率、数据位、停止位、校验位、是否回显、是否原始模式等。

2.2 termios 结构体定义

c 复制代码
typedef unsigned char cc_t;
typedef unsigned int speed_t;
typedef unsigned int tcflag_t;

#define NCCS 19
struct termios {
    tcflag_t c_iflag;   /* 输入模式标志 */
    tcflag_t c_oflag;   /* 输出模式标志 */
    tcflag_t c_cflag;   /* 控制模式标志 */
    tcflag_t c_lflag;   /* 本地模式标志 */
    cc_t    c_line;     /* 线路规程 */
    cc_t    c_cc[NCCS]; /* 控制字符数组 */
};

2.3 各字段详解(表格)

字段 作用 常用标志位
c_iflag 输入处理选项 IGNBRK(忽略 break)、INPCK(启用输入奇偶校验)
c_oflag 输出处理选项 OPOST(启用输出处理)、ONLCR(换行转回车换行)
c_cflag 硬件控制选项 CS8(8 位数据)、CLOCAL(忽略 modem 控制)、CREAD(启用接收)
c_lflag 本地模式选项 ECHO(回显)、ICANON(规范模式,即行缓冲)
c_cc 特殊控制字符 VMINVTIME(用于非规范模式)

2.4 生活化类比(termios 就像空调遥控器)

空调遥控器上有 模式、温度、风速、定时 等多个按键 ------ 这就像 termios 的多个标志位。你要按 tcsetattr 这个"确认键"才能让设置生效。

2.5 行规程函数表(常用)

函数名 作用 白话解释
tcgetattr(fd, &termios) 获取当前串口属性 读取当前的"遥控器设置"
tcsetattr(fd, opt, &termios) 设置串口属性 把新设置写入"遥控器",opt 决定何时生效
tcflush(fd, queue) 清空输入/输出缓冲区 扔掉还没发/还没收的数据
cfsetispeed(&termios, speed) 设置输入波特率 设置"说话"的速度
cfsetospeed(&termios, speed) 设置输出波特率 设置"听话"的速度
cfsetspeed(&termios, speed) 同时设置输入输出波特率 一键设置双向速度

2.6 常见波特率常量

波特率
B9600 9600
B115200 115200
B4800 4800
B19200 19200
B57600 57600

3. 串口收发实验 ------ 自发自收(短接 TX 和 RX)

3.1 一句话白话解释

把串口的 发送引脚(TX)接收引脚(RX) 用导线短接,这样你 write 出去的数据马上就会被自己 read 回来。这是最简单的串口测试方法。

3.2 硬件连接示意图

⚠️ 注意 :做实验时可以用 金属镊子或杜邦线 短接扩展板上 UART_A 的 TX 和 RX 插针。如果没有扩展板,请查看原理图,短接底板对应引脚。

3.3 完整可编译运行的串口自发自收代码

c 复制代码
#include <stdio.h>
#include <string.h>
#include <sys/types.h>
#include <errno.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <termios.h>
#include <stdlib.h>

/**
 * 配置串口参数
 * @param fd     已打开的串口文件描述符
 * @param nSpeed 波特率 (支持2400/4800/9600/115200,默认9600)
 * @param nBits  数据位 (7 或 8)
 * @param nEvent 校验位 ('O'奇校验, 'E'偶校验, 'N'无校验)
 * @param nStop  停止位 (1 或 2)
 * @return 0成功,-1失败
 */
int set_opt(int fd, int nSpeed, int nBits, char nEvent, int nStop)
{
	struct termios newtio, oldtio;
	
	// 获取当前串口属性,保存到 oldtio 以便恢复
	if (tcgetattr(fd, &oldtio) != 0) { 
		perror("SetupSerial 1");
		return -1;
	}
	
	// 清零 newtio 结构体
	bzero(&newtio, sizeof(newtio));
	
	// 启用接收器,设置为本地连接模式(忽略调制解调器控制线)
	newtio.c_cflag |= CLOCAL | CREAD; 
	// 清除数据位掩码,以便重新设置
	newtio.c_cflag &= ~CSIZE; 

	// 设置为原始输入模式:关闭规范输入、回显、信号字符处理
	newtio.c_lflag &= ~(ICANON | ECHO | ECHOE | ISIG);  /* 输入标志 */
	// 设置为原始输出模式:不进行输出处理
	newtio.c_oflag &= ~OPOST;   /* 输出标志 */

	// 设置数据位
	switch (nBits) {
	case 7:
		newtio.c_cflag |= CS7;
		break;
	case 8:
		newtio.c_cflag |= CS8;
		break;
	}

	// 设置校验位
	switch (nEvent) {
	case 'O':                     // 奇校验 (Odd)
		newtio.c_cflag |= PARENB;   // 启用校验
		newtio.c_cflag |= PARODD;   // 设置为奇校验
		newtio.c_iflag |= (INPCK | ISTRIP); // 启用输入校验检查,并剥离校验位
		break;
	case 'E':                     // 偶校验 (Even)
		newtio.c_iflag |= (INPCK | ISTRIP);
		newtio.c_cflag |= PARENB;   // 启用校验
		newtio.c_cflag &= ~PARODD;  // 清除奇校验位,即为偶校验
		break;
	case 'N':                     // 无校验 (None)
		newtio.c_cflag &= ~PARENB;  // 关闭校验
		break;
	}

	// 设置波特率 (输入/输出波特率分别设置)
	switch (nSpeed) {
	case 2400:
		cfsetispeed(&newtio, B2400);
		cfsetospeed(&newtio, B2400);
		break;
	case 4800:
		cfsetispeed(&newtio, B4800);
		cfsetospeed(&newtio, B4800);
		break;
	case 9600:
		cfsetispeed(&newtio, B9600);
		cfsetospeed(&newtio, B9600);
		break;
	case 115200:
		cfsetispeed(&newtio, B115200);
		cfsetospeed(&newtio, B115200);
		break;
	default:
		// 不支持的波特率默认使用 9600
		cfsetispeed(&newtio, B9600);
		cfsetospeed(&newtio, B9600);
		break;
	}
	
	// 设置停止位
	if (nStop == 1)
		newtio.c_cflag &= ~CSTOPB;  // 1位停止位
	else if (nStop == 2)
		newtio.c_cflag |= CSTOPB;   // 2位停止位
	
	// 设置读取策略:
	// VMIN = 1 表示至少读到1个字节才返回
	// VTIME = 0 表示不等待,如果没有数据立即返回(read 返回0)
	newtio.c_cc[VMIN]  = 1;  
	newtio.c_cc[VTIME] = 0; 
	/* 注释中 VTIME 单位有误:实际上是 0.1 秒,此处 VTIME=0 即无限等待直到 VMIN 满足 */
	
	// 清空输入输出缓冲区
	tcflush(fd, TCIFLUSH);
	
	// 立即应用新的串口属性
	if (tcsetattr(fd, TCSANOW, &newtio) != 0) {
		perror("com set error");
		return -1;
	}
	return 0;
}

/**
 * 打开串口设备
 * @param com 设备节点路径,如 "/dev/ttyS0"
 * @return 文件描述符,失败返回 -1
 */
int open_port(char *com)
{
	int fd;
	// 以读写方式打开,不设置为控制终端 (O_NOCTTY)
	fd = open(com, O_RDWR | O_NOCTTY);
	if (-1 == fd) {
		return -1;
	}
	
	// 设置文件状态标志为0,即设置为阻塞模式(清除 O_NONBLOCK)
	if (fcntl(fd, F_SETFL, 0) < 0) {
		printf("fcntl failed!\n");
		return -1;
	}
	return fd;
}

/**
 * 主函数:串口发送并回显测试
 * 用法: ./serial_send_recv </dev/ttySAC1 or other>
 */
int main(int argc, char **argv)
{
	int fd;
	int iRet;
	char c;

	// 检查命令行参数
	if (argc != 2) {
		printf("Usage: \n");
		printf("%s </dev/ttySAC1 or other>\n", argv[0]);
		return -1;
	}

	// 1. 打开串口设备
	fd = open_port(argv[1]);
	if (fd < 0) {
		printf("open %s err!\n", argv[1]);
		return -1;
	}

	// 2. 配置串口 (115200, 8数据位, 无校验, 1停止位)
	iRet = set_opt(fd, 115200, 8, 'N', 1);
	if (iRet) {
		printf("set port err!\n");
		return -1;
	}

	printf("Enter a char: ");
	
	// 3. 主循环:读取键盘输入 -> 发送到串口 -> 从串口读取一个字符 -> 显示
	while (1) {
		scanf("%c", &c);          // 从标准输入读取一个字符
		iRet = write(fd, &c, 1);  // 发送该字符到串口
		iRet = read(fd, &c, 1);   // 尝试从串口读取一个字节
		if (iRet == 1)
			printf("get: %02x %c\n", c, c); // 以十六进制和字符形式打印
		else
			printf("can not get data\n");
	}

	return 0;
}

3.4 上机实验步骤

bash 复制代码
# Ubuntu 上编译
arm-buildroot-linux-gnueabihf-gcc -o serial_send_recv serial_send_recv.c

# 开发板上运行(假设串口设备为 /dev/ttymxc5)
./serial_send_recv /dev/ttymxc5

3.5 常见错误及解决方法

错误现象 可能原因 解决方法
open: Permission denied 没有权限访问设备 chmod 666 /dev/ttymxc5 或使用 sudo
tcgetattr: Input/output error 设备不是串口或驱动问题 ls -l /dev/ttymxc* 确认设备存在
发送了但 read 永远阻塞 TX 和 RX 没有短接 用杜邦线短接 TX 和 RX 引脚
读到的数据乱码 波特率不匹配 检查设置,与硬件实际波特率一致
发送后读回的数据多/少 VMIN/VTIME 设置不当 调试 c_cc[VMIN]c_cc[VTIME]

4. GPS 模块实验 ------ 解析 NMEA0183 数据

4.1 GPS 系统简介(白话版)

GPS 是一个由 24 颗以上卫星 组成的导航系统。你的 GPS 模块(接收机)同时接收多颗卫星的信号,计算出你当前的位置(经度、纬度、海拔),然后通过串口输出这些数据。

4.2 NMEA0183 格式 ------ GPS 数据的"通用语言"

一句话白话 :NMEA0183 是一种 ASCII 文本格式 ,每句话以 $ 开头,以换行结束,逗号分隔字段。不同语句类型(如 $GPGGA$GPRMC)包含不同的定位信息。

格式示意图

复制代码
$GPGG,<1>,<2>,<3>,<4>,<5>,<6>,<7>,<8>,<9>,...*hh<CR><LF>

典型例子

复制代码
$GPGGA,074529.82,2429.6717,N,11804.6973,E,1,8,1.098,42.110,M,,M,,*76

4.3 $XXGGA 字段详解(表格)

字段 含义 示例值 白话解释
<1> UTC 时间(hhmmss.ss) 074529.82 07:45:29.82 世界协调时
<2> 纬度(ddmm.mmmm) 2429.6717 24 度 29.6717 分
<3> 南北半球 N N=北纬,S=南纬
<4> 经度(dddmm.mmmm) 11804.6973 118 度 04.6973 分
<5> 东西半球 E E=东经,W=西经
<6> GPS 状态 1 0=未定位,1=单点定位,2=差分定位,4=RTK固定解
<7> 使用的卫星数 8 8 颗卫星
<8> HDOP(水平精度因子) 1.098 越小越精确,<2 很好
<9> 海拔高度(米) 42.110 海拔 42.11 米
<10> 高程异常(米) (空) 大地水准面与椭球面之差
<11> 差分时间(秒) (空) 最近一次差分更新距今秒数
<12> 参考站号 (空) DGPS 参考站 ID

💡 注意 :语句开头的 GP 表示 GPS 系统,也可能是 BD(北斗)、GL(格洛纳斯)、GN(多系统联合)。

4.4 GPS 模块硬件连接

复制代码
┌─────────────┐      ┌─────────────┐
│  IMX6ULL    │      │  GPS 模块   │
│  UART5_RX   ├──────┼→ TX         │
│  (GPIO5)    │      │             │
│  UART5_TX   ├──────┼→ RX(可选) │
│  3.3V       ├──────┼→ VCC        │
│  GND        ├──────┼→ GND        │
└─────────────┘      └─────────────┘

⚠️ 注意 :大多数 GPS 模块只需要 接收数据,所以只需将 GPS 模块的 TX 接到开发板的 RX。如果不需要发送命令给 GPS,可以不接开发板的 TX。

4.5 完整可编译运行的 GPS 数据读取与解析代码

c 复制代码
#include <stdio.h>
#include <string.h>
#include <sys/types.h>
#include <errno.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <termios.h>
#include <stdlib.h>

/* set_opt(fd,115200,8,'N',1) */
int set_opt(int fd,int nSpeed, int nBits, char nEvent, int nStop)
{
	struct termios newtio,oldtio;
	
	if ( tcgetattr( fd,&oldtio) != 0) { 
		perror("SetupSerial 1");
		return -1;
	}
	
	bzero( &newtio, sizeof( newtio ) );
	newtio.c_cflag |= CLOCAL | CREAD; 
	newtio.c_cflag &= ~CSIZE; 

	newtio.c_lflag  &= ~(ICANON | ECHO | ECHOE | ISIG);  /*Input*/
	newtio.c_oflag  &= ~OPOST;   /*Output*/

	switch( nBits )
	{
	case 7:
		newtio.c_cflag |= CS7;
	break;
	case 8:
		newtio.c_cflag |= CS8;
	break;
	}

	switch( nEvent )
	{
	case 'O':
		newtio.c_cflag |= PARENB;
		newtio.c_cflag |= PARODD;
		newtio.c_iflag |= (INPCK | ISTRIP);
	break;
	case 'E': 
		newtio.c_iflag |= (INPCK | ISTRIP);
		newtio.c_cflag |= PARENB;
		newtio.c_cflag &= ~PARODD;
	break;
	case 'N': 
		newtio.c_cflag &= ~PARENB;
	break;
	}

	switch( nSpeed )
	{
	case 2400:
		cfsetispeed(&newtio, B2400);
		cfsetospeed(&newtio, B2400);
	break;
	case 4800:
		cfsetispeed(&newtio, B4800);
		cfsetospeed(&newtio, B4800);
	break;
	case 9600:
		cfsetispeed(&newtio, B9600);
		cfsetospeed(&newtio, B9600);
	break;
	case 115200:
		cfsetispeed(&newtio, B115200);
		cfsetospeed(&newtio, B115200);
	break;
	default:
		cfsetispeed(&newtio, B9600);
		cfsetospeed(&newtio, B9600);
	break;
	}
	
	if( nStop == 1 )
		newtio.c_cflag &= ~CSTOPB;
	else if ( nStop == 2 )
		newtio.c_cflag |= CSTOPB;
	
	newtio.c_cc[VMIN]  = 1;  /* 读数据时的最小字节数: 没读到这些数据我就不返回! */
	newtio.c_cc[VTIME] = 0; /* 等待第1个数据的时间: 
	                         * 比如VMIN设为10表示至少读到10个数据才返回,
	                         * 但是没有数据总不能一直等吧? 可以设置VTIME(单位是10秒)
	                         * 假设VTIME=1,表示: 
	                         *    10秒内一个数据都没有的话就返回
	                         *    如果10秒内至少读到了1个字节,那就继续等待,完全读到VMIN个数据再返回
	                         */

	tcflush(fd,TCIFLUSH);
	
	if((tcsetattr(fd,TCSANOW,&newtio))!=0)
	{
		perror("com set error");
		return -1;
	}
	//printf("set done!\n");
	return 0;
}

int open_port(char *com)
{
	int fd;
	//fd = open(com, O_RDWR|O_NOCTTY|O_NDELAY);
	fd = open(com, O_RDWR|O_NOCTTY);
    if (-1 == fd){
		return(-1);
    }
	
	  if(fcntl(fd, F_SETFL, 0)<0) /* 设置串口为阻塞状态*/
	  {
			printf("fcntl failed!\n");
			return -1;
	  }
  
	  return fd;
}


/*
 * ./serial_send_recv <dev>
 */
int main(int argc, char **argv)
{
	int fd;
	int iRet;
	char c;

	/* 1. open */

	/* 2. setup 
	 * 115200,8N1
	 * RAW mode
	 * return data immediately
	 */

	/* 3. write and read */
	
	if (argc != 2)
	{
		printf("Usage: \n");
		printf("%s </dev/ttySAC1 or other>\n", argv[0]);
		return -1;
	}

	fd = open_port(argv[1]);
	if (fd < 0)
	{
		printf("open %s err!\n", argv[1]);
		return -1;
	}

	iRet = set_opt(fd, 115200, 8, 'N', 1);
	if (iRet)
	{
		printf("set port err!\n");
		return -1;
	}

	printf("Enter a char: ");
	while (1)
	{
		scanf("%c", &c);
		iRet = write(fd, &c, 1);
		iRet = read(fd, &c, 1);
		if (iRet == 1)
			printf("get: %02x %c\n", c, c);
		else
			printf("can not get data\n");
	}

	return 0;
}

4.6 接线

4.7 上机实验步骤

bash 复制代码
# Ubuntu 上编译
arm-buildroot-linux-gnueabihf-gcc -o gps_read gps_read.c

# 开发板上运行(假设 GPS 模块连接在 /dev/ttymxc5)
./gps_read /dev/ttymxc5

# 将 GPS 模块放在室外或窗边,等待定位(可能需要几分钟)

预期输出(定位成功后):

复制代码
Listening to GPS on /dev/ttymxc5 at 9600 baud...

📍 GPS Fix Info:
   Time (UTC): 074529.82
   Lat: 2429.6717 N
   Lon: 11804.6973 E
   Quality: 1 (GPS single fix)
   Satellites: 8
   HDOP: 1.098
   Altitude: 42.110 m

4.7 常见错误及解决方法

错误现象 可能原因 解决方法
没有任何输出 GPS 模块未上电或串口连接错误 检查 VCC/GND,确认 TX 接开发板 RX
输出全是 $GPGGA,,,,,,0 GPS 未定位(室内或无卫星信号) 移到室外,等待几分钟
输出乱码 波特率不匹配 确认 GPS 模块波特率(常见 9600)
open: No such file or directory 设备节点不存在 ls /dev/ttymxc* 查看实际串口名
收到数据但解析不到经纬度 语句类型不是 GGA 打印原始数据,看输出语句类型(可能是 $GPRMC

移到室外,等待几分钟

5. 完整速查表 📋

5.1 串口配置速查

配置项 常用值 termios 设置
波特率 9600, 115200 cfsetospeed(&term, B9600)
数据位 8 `term.c_cflag
停止位 1 term.c_cflag &= ~CSTOPB
校验位 term.c_cflag &= ~PARENB
原始模式 cfmakeraw(&term)
阻塞读取 至少 1 字节 term.c_cc[VMIN]=1; term.c_cc[VTIME]=0

5.2 常用 NMEA 语句

语句 内容 主要字段
$GPGGA 定位数据(时间、经纬度、海拔、卫星数、精度) 最常用
$GPRMC 推荐最小定位信息(速度、方向、日期) 用于导航
$GPGSA 当前卫星信息(DOP、使用中的卫星) 精度评估
$GPGSV 可见卫星状态(仰角、方位、信噪比) 调试用
$GPVTG 地面速度信息 速度方向

5.3 GPS 质量值含义

含义 精度
0 未定位 无效
1 GPS 单点定位 2-5 米
2 差分定位(DGPS) 0.5-1 米
4 RTK 固定解 厘米级
5 RTK 浮点解 分米级

6 常见面试题

问题 答案要点
termiosc_cflagCREADCLOCAL 有什么作用? CREAD 启用接收,CLOCAL 忽略 modem 控制线(如 DCD)
什么是原始模式?与规范模式有什么区别? 原始模式不处理特殊字符,不缓冲行,适合传输二进制数据
VMINVTIME 的含义? VMIN 最小读取字节数,VTIME 等待时间(0.1 秒单位)
GPS 模块为什么需要较长时间才能定位? 冷启动时需要下载星历数据,约 30 秒到几分钟
NMEA 语句中的校验和 *hh 如何计算? $ 后到 * 前的所有字符异或

7.2 仍有哪个知识点你觉得自己解释得不够清楚?为什么?

tcsetattropt 参数(TCSANOWTCSADRAINTCSAFLUSH 解释得不够深入。虽然代码中使用了 TCSANOW,但没有详细说明三者区别以及什么时候用哪个。原因是在简单的串口配置场景中,TCSANOW(立即生效)足够用,而 TCSADRAIN(等待输出完成后更改)和 TCSAFLUSH(清空输入输出缓冲区后更改)在更复杂的流控或 modem 控制场景才体现价值。后续可以补充一个对比表格,并给出使用 TCSADRAIN 修改波特率的例子,避免数据损坏。

💡 补充说明

  • TCSANOW:立即更改配置,可能丢弃未发送的数据。
  • TCSADRAIN:等待所有输出发送完毕再更改,适合更改影响输出行为的参数(如波特率)。
  • TCSAFLUSH:等待输出完成后,同时清空输入缓冲区。

🎉 恭喜!你已经完整学完了 Linux 串口应用编程的所有核心知识。现在你可以:

  • termios 结构体配置任意串口参数
  • 编写程序实现串口收发和自发自收测试
  • 解析 GPS 模块的 NMEA0183 数据,提取经纬度、海拔等信息
  • 根据实际需求,扩展为 GSM 模块控制、传感器数据采集等应用

如果本文对你有帮助,欢迎分享给更多嵌入式开发者!

相关推荐
BackCatK Chen3 天前
STM32保姆级入门教程|第7章:串口通信(USART)收发数据 + printf重定向打印调试(功能超详细+CubeIDE手把手)
stm32·串口通信·usart·stm32cubeide·printf重定向·嵌入式调试·中断接收
沈跃泉3 天前
C++串口类实现
c++·windows·串口通信·串口类
冷凝雨3 天前
复旦微FM33 MCU 底层开发指南——UART
stm32·单片机·串口·uart·fm33lc0·复旦微电子
liuluyang53013 天前
DW_apb_uart 16650 寄存器详解
单片机·嵌入式硬件·uart·基础外设
胡摩西14 天前
当大模型遇上毫米级定位:机器人将拥有“空间思维”?
人工智能·机器人·slam·gps·室内定位·roomaps
π同学20 天前
ESP-IDF+vscode开发ESP32第三讲——UART
vscode·esp32·uart·esp-idf
小贺儿开发21 天前
【Arduino与Unity交互探究】03 超声波测距模块
unity·arduino·串口通信·传感器·videoplayer·硬件交互
我在人间贩卖青春23 天前
U(S)ART 串口应用
单片机·串口·uart·usart
我在人间贩卖青春24 天前
U(S)ART 串口理论
串口·uart·usart