Linux应用开发-7-串口通讯与终端设备
基础知识:
- tty(teletype([通信] 电传打字机) 的缩写用于串口设备)
- ls /dev/tty* 查看设备所有的串口打印设备
- ls /dev/ttySTM* 查看用于串口通信的几个口
- /dev/ttySTM0 开发板的串口4(对于板子接线来说),它被默认被用在命令行的终端,对应USB转串口
- /dev/ttySTM1 对应开发板串口1
- /dev/ttySTM3 对应开发板串口3
stty(set teletype)命令
#它会输出当前终端的参数即STM0
stty
/*
speed 115200 baud; line = 0;
-brkint ixoff -imaxbel iutf8
-iexten
*/
#指定设备输出信息
#-F`(--file) 告诉 stty命令不要操作我当前正在使用的这个终端,而是去操作你后面指定的那个设备文件
stty -F /dev/ttySTM3# 还可以设置通讯速率,其中 ispeed 为输入速率,ospeed 为输出速率
stty -F /dev/ttySTM0 ispeed 9600 ospeed 9600
如果没有设备ttySTM3,则进行启用
#编辑启动文件
nano /boot/uEnv.txt
#找到对应串口取消注释
#重启板子
reboot
文件中内容修改见以下位置
#Docs: http://elinux.org/Beagleboard:U-boot_partitioning_layout_2.0uname_r=4.19.94-stm-r1
#uuid=
dtb=stm32mp157a-basic.dtb###U-Boot Overlays###
###Documentation: https://embed-linux-tutorial.readthedocs.io/zh_CN/latest/linu$
###Master Enable
enable_uboot_overlays=1
#overlay_start# BUS class/function
dtoverlay=/usr/lib/linux-image-4.19.94-stm-r1/overlays/stm-fire-i2c1.dtbo
dtoverlay=/usr/lib/linux-image-4.19.94-stm-r1/overlays/stm-fire-i2c2.dtbo
dtoverlay=/usr/lib/linux-image-4.19.94-stm-r1/overlays/stm-fire-ltdc.dtbo
# DEV class/function
#dtoverlay=/usr/lib/linux-image-4.19.94-stm-r1/overlays/stm-fire-485r1.dtbo
#dtoverlay=/usr/lib/linux-image-4.19.94-stm-r1/overlays/stm-fire-485r2.dtbo
dtoverlay=/usr/lib/linux-image-4.19.94-stm-r1/overlays/stm-fire-adc.dtbo
#dtoverlay=/usr/lib/linux-image-4.19.94-stm-r1/overlays/stm-fire-bluetooth.dtbo
#dtoverlay=/usr/lib/linux-image-4.19.94-stm-r1/overlays/stm-fire-btwifi.dtbo
dtoverlay=/usr/lib/linux-image-4.19.94-stm-r1/overlays/stm-fire-can.dtbo
#dtoverlay=/usr/lib/linux-image-4.19.94-stm-r1/overlays/stm-fire-cam.dtbo
dtoverlay=/usr/lib/linux-image-4.19.94-stm-r1/overlays/stm-fire-hdmi.dtbo
dtoverlay=/usr/lib/linux-image-4.19.94-stm-r1/overlays/stm-fire-key.dtbodtoverlay=/usr/lib/linux-image-4.19.94-stm-r1/overlays/stm-fire-lcd.dtbo
dtoverlay=/usr/lib/linux-image-4.19.94-stm-r1/overlays/stm-fire-led.dtbo
#dtoverlay=/usr/lib/linux-image-4.19.94-stm-r1/overlays/stm-fire-mipi.dtbo
#dtoverlay=/usr/lib/linux-image-4.19.94-stm-r1/overlays/stm-fire-mpu6050.dtbo
dtoverlay=/usr/lib/linux-image-4.19.94-stm-r1/overlays/stm-fire-sound.dtbo
dtoverlay=/usr/lib/linux-image-4.19.94-stm-r1/overlays/stm-fire-touch-capacitiv$
#dtoverlay=/usr/lib/linux-image-4.19.94-stm-r1/overlays/stm-fire-touch-capaciti$
#dtoverlay=/usr/lib/linux-image-4.19.94-stm-r1/overlays/stm-fire-usart1.dtbo
#===================================文件中下面位置取消注释=================================
dtoverlay=/usr/lib/linux-image-4.19.94-stm-r1/overlays/stm-fire-usart3.dtbo
#overlay_endcmdline=coherent_pool=1M net.ifnames=0 vt.global_cursor_default=0#In the event of edid real failures, uncomment this next line:
#cmdline=coherent_pool=1M net.ifnames=0 vt.global_cursor_default=0 video=HDMI-A$#Use an overlayfs on top of a read-only root filesystem:
#cmdline=coherent_pool=1M net.ifnames=0 vt.global_cursor_default=0 overlayroot=$
#flash_firmware=continued
#flash_firmware=once# specify kernel eth0 mac address
ethaddr=6e:c8:57:e6:34:f5# specify storage_media
storage_media=init=/opt/scripts/tools/eMMC/init-eMMC-flasher-v3.sh
串口通讯配置接线
-
查看串口的详细信息,最主要的是波特率,windows和串口波特率保持一致,
stty -F /dev/ttySTM3 -
关闭回显
stty -F /dev/ttySTM3 -echo-表示减,-echo代表禁用或关闭echo这个设置- 关闭回显(输入的内容返回回来)原因:进行串口通信时,发送给设备的命令,设备自己处理后可能会返回结果,你不需要设备把你发的命令原样再发回来。如果开启
echo,可能会导致数据混乱或重复。
- 关闭回显(输入的内容返回回来)原因:进行串口通信时,发送给设备的命令,设备自己处理后可能会返回结果,你不需要设备把你发的命令原样再发回来。如果开启
-
连接串口线及跳线帽,跳帽(UART3_TXD<—->T2IN、UART3_RXD<—->R2OUT),串口3与主机进行连接(串口usb转DB9)

方法一、 Windows 主机通讯
下载调试信息软件-野火多功能调试助手上位机 — 野火产品资料下载中心 文档
数据发送
#在开发板上的终端执行执行数据发送
echo board > /dev/ttySTM3
echo embedfire > /dev/ttySTM3
#windows上接收数据#在开发板上的终端执行以下指令
#使用 cat命令读取终端设备文件
cat /dev/ttySTM3
#cat命令会等待接受window发来的信息
注意:windows发送时,输入的字符串之后一定要加回车enter

方法二、 Ubuntu 主机通讯
分配 USB 转串口设备
串口转usb插入主机,虚拟机会弹出选项,选择连接到虚拟机

执行ls /dev/tty*

注:如果未显示ttyUSB的选项执行下面步骤
ls /dev/tty*

1.排查USB设备是否插入虚拟机lsusb

2.查看内核日志进行排查

显示名为 brltty 的程序(BRLTTY 6.4 Linux Screen Driver)强行介入,它也想“认领”这个USB设备,与 ch341 驱动发生了冲突。(brltty 是一个为盲人用户提供的辅助功能服务,用于将终端内容输出到盲文显示器(可以放心卸载))
执行卸载:sudo apt remove brltty 再次执行就会出现ttyUSB这个设备文件了。
安装和配置 minicom
虚拟机中执行:
#安装minicom插件
sudo apt install minicom
#运行配置
sudo minicom -s
#执行这个插件
sudo minicom

以下三个地方需要修改:

配置完要保存配置:

9600这里是因为板子串口的配置,要对应才能进行数据传输。

板子端输入echo board > /dev/ttySTM3 虚拟机就能接收到

注:上述操作流程 上下左右选中,然后enter选中,以及shift+字母
说明minicom命令键:先按下 Ctrl+A 组合键,然后松开,再按下 o键弹出配置,同理先按下 Ctrl+A 组合键,然后松开,再按下 x键是退出。
串口通讯(系统调用)termios
说明:termios-串口(或任何终端设备)的“设置总控制面板”(所有配置项的集合体,类似C的struct);用Linux打开一个串口设备文件(本文:/dev/ttySTM3)内核会为这个文件描述符关联一套默认的 termios 配置。编程可以通过系统调用(system calls)来读取这个“控制面板”,修改上面的“旋钮”和“开关”,然后再把新的设置应用回去。
1)termios 的核心作用:定义串口的行为
主要控制四大方面的行为
-
1.物理层参数 (Hardware Parameters) :定义最基本的电气信号规则。
-
波特率 (Baud Rate) :通信双方的速度,必须完全一致。
-
数据位 (Data Bits) :一帧数据包含多少个bit(通常是8)。
-
停止位 (Stop Bits) :一帧数据的结束标记(通常是1位)。
-
校验位 (Parity Bit) :用于简单的数据错误检测(通常是无校验)。
-
-
2.输入数据处理 (Input Processing) :定义内核在将接收到的数据交给应用程序之前,要对数据做的预处理。
- 是否要检查校验位错误?是否要将回车符
\r自动转换成换行符\n?是否要处理软件流控字符(XON/XOFF)?
- 是否要检查校验位错误?是否要将回车符
-
3.输出数据处理 (Output Processing) :定义内核在将应用程序数据发送到串口硬件之前,要对数据做什么后处理。
- 是否要将换行符
\n自动转换成回车换行符\r\n(很多Windows设备需要)?
- 是否要将换行符
-
4.本地模式 (Local Modes) :定义终端驱动程序的行为模式,这对于串口通信至关重要。
-
是否回显 (Echo) :把收到的字符再发送回去(用
stty -F "操作的串口文件路径" -echo关掉)。 -
是否是“规范模式”(Canonical Mode) :这是默认模式,数据以“行”为单位进行处理,直到你输入回车键,数据才会被提交给程序。
-
是否处理信号字符:比如按下
Ctrl+C时,是否要给程序发送一个SIGINT信号来中断它。
-
2)termios 结构体详解(控制面板上的分区)
termios 结构体总览,5个功能区:
struct termios {tcflag_t c_iflag; /* 输入模式标志 (Input mode flags) */tcflag_t c_oflag; /* 输出模式标志 (Output mode flags) */tcflag_t c_cflag; /* 控制模式标志 (Control mode flags) */tcflag_t c_lflag; /* 本地模式标志 (Local mode flags) */cc_t c_cc[NCCS]; /* 控制字符 (Control characters) */
};
-
1.
c_cflag(Control Flags) - 硬件控制区-
设置波特率:
B115200,B9600,通过cfsetispeed()和cfsetospeed()两个函数。 -
设置数据位 (Character Size)
CS8,CS7,CS6,CS5(CS8代表8位数据位,最常用) -
设置停止位
CSTOPB,设置则是2位停止位;不设置则是1位停止位。 -
开启校验位 (Parity Enable)
PARENB。 -
PARODD开启校验位,设置此标志则为奇校验,否则为偶校验。 -
开启硬件流控 (RTS/CTS)
CRTSCTS
-
-
2.
c_lflag(Local Flags) - 模式控制区 (决定串口是作为“原始数据通道”还是“交互式终端”)-
ICANON-icanon: 启用规范模式。默认此模式。在此模式下,输入基于“行”,内核会处理退格、删除等编辑操作,只有当用户按下回车时,整行数据才对read()可见。-串口数据通信时,必须禁用。 -
ECHO-echo: 回显输入字符。 -必须禁用。 -
ISIG-isig: 当接收到INTR,QUIT,SUSP等字符时(例:Ctrl+C,Ctrl+),产生相应的信号。-必须禁用。
-
-
3.
c_iflag(Input Flags) - 输入处理区 (控制对接收到的字节的处理)-
IXON-ixon,IXOFF-ixoff,IXANY-ixany: 启用软件流控 (XON/XOFF)。通常禁用。 -
INPCK-inpck: 启用输入校验检查。 -
ISTRIP-istrip: 剥离第8位(即只保留7-bit ASCII)。通常禁用。 -
ICRNL-icrnl: 将接收到的回车符\r转换成换行符\n。是否需要看对方设备的要求。
-
-
4.
c_oflag(Output Flags) - 输出处理区(控制对要发送的字节的处理)OPOST-opost: 启用输出处理。如果禁用,所有其他c_oflag选项都会被忽略,数据将“按原样”发送,是实现原始数据传输的快捷方式。
-
5.
c_cc[NCCS](Control Characters) - 特殊字符定义区(定义了各种特殊控制字符的功能)-
两个主要功能:
VMIN-vmin:read()函数在返回前需要读取的最少字节数。VTIME-vtime:read()函数的超时时间,单位是 1/10 秒。
-
VMIN和VTIME的组合可以实现对read()行为的精确控制:vmin > 0, vtime = 0: 阻塞式读取(返回字节数>0,持续读取无时间超时代表立即返回)。read()会一直等待,直到读满VMIN个字节才返回。vmin = 0, vtime > 0: 定时读取(无限制返回字节数,超时时间>0代表等待)。read()会等待VTIME* 0.1 秒,然后返回它在这段时间内读到的所有字节(可能是0)。vmin > 0, vtime > 0: 带超时的阻塞读取。read()等待VMIN个字节,但如果在两个字节之间等待超过VTIME* 0.1 秒,就会超时并返回当前已读到的字节。vmin = 0, vtime = 0: 纯非阻塞。read()立即返回,不管有没有数据。
-
3)termios 的标准用法 (编程步骤)
**思想:**先获取当前的设置,然后在此基础上修改。
**具体操作:**程序首先打开/dev/ttySTM3串口设备,接着通过termios接口将其精心配置为适合机器间通信的“原始模式”,设定了9600波特率、8N1数据格式以及一个短暂的读取超时以避免程序阻塞。在将这些配置应用到硬件并清空缓冲区后,程序会发送一条“Hello from Lubancat!”问候语,随即短暂等待并尝试读取任何可能返回的响应数据,最后规范地关闭串口以释放系统资源,从而完成一个健壮的初始化、配置、数据收发和清理的完整通信周期。
**具体目的:**如何编写一个C语言程序,来“代替” minicom 或 stty,用代码去编程控制和配置串口。
**思想核心证明:**1.Linux系统中所有的串口配置都保存在一个名为 termios 的结构体中;2.您可以通过C函数 tcgetattr() 来读取这个结构体;3.可以通过修改这个结构体的成员(如 c_cflag、c_iflag)或使用特定函数(如 cfsetispeed())来修改波特率、数据位、停止位和校验位;4.可以通过 tcsetattr() 函数将修改后的配置写回给串口,使其立即生效。
#include <stdio.h>
#include <fcntl.h> //
#include <unistd.h> //
#include <termios.h> // 包含termios相关的函数和结构体
#include <string.h> // 包含 strerrorint main() {// 1. 打开串口设备文件// O_RDWR: 可读可写// O_NOCTTY: 不将此设备设置为主控终端// O_NDELAY: 非阻塞模式 (在某些系统上可能需要)int fd = open("/dev/ttySTM3", O_RDWR | O_NOCTTY);if (fd < 0) {perror("Failed to open serial port");return -1;}// 2. 获取当前的termios配置struct termios options;//创建一个临时的、本地的容器(变量),//这个容器的结构正好可以用来完整地存放串口的所有配置信息。//接下来的所有修改操作,都是先在这个名为 `options` 的“工作台”上完成。//tcgetattr(fd, &options)=>读取串口当前的实际配置,并填写到您的“工作台”上。//在这个函数中:fd 告诉 tcgetattr 函数:请去获取由这个号码所代表的那个串口的当前配置。”tcgetattr(fd, &options);// 3. 修改配置 (这是核心步骤)// 3.1 设置为原始模式 (Raw Mode)// 这是与设备进行纯数据通信的关键cfmakeraw(&options); // cfmakeraw会帮你禁用ICANON, ECHO, ISIG等,并禁用输入输出处理// 3.2 设置波特率 (例如 9600)cfsetispeed(&options, B9600); // 设置输入波特率cfsetospeed(&options, B9600); // 设置输出波特率// 3.3 设置数据位、停止位、校验位 (通过c_cflag)// `|` (按位或 `OR`) || `&` (按位与 `AND`) || `~` (按位非 `NOT`)//options.c_cflag相当于一个控制面板,一位(bit)都连接着一个**物理开关**,控制着一项功能//以下每一个变量设置对应位上的值options.c_cflag &= ~CSIZE; // 清除数据位掩码options.c_cflag |= CS8; // 设置为8位数据位options.c_cflag &= ~PARENB; // 禁用校验位options.c_cflag &= ~CSTOPB; // 设置为1位停止位options.c_cflag |= CREAD; // 启用接收options.c_cflag |= CLOCAL; // 忽略调制解调器状态线// 3.4 设置read()的超时行为 (通过c_cc)options.c_cc[VMIN] = 0; // read()立即返回options.c_cc[VTIME] = 5; // 超时时间 0.5 秒// 4. 应用新的配置// TCSANOW: 立即生效if (tcsetattr(fd, TCSANOW, &options) != 0) {perror("Failed to set attributes");close(fd);return -1;}// 清空串口的输入输出缓冲区 (可选,但推荐)tcflush(fd, TCIOFLUSH);// 5. 进行读写操作char *msg = "Hello from Lubancat!\n";int n_written = write(fd, msg, strlen(msg));if (n_written < 0) {perror("Failed to write to port");} else {printf("Sent %d bytes: %s", n_written, msg);}sleep(1); // 等待一会,看看有没有数据返回char read_buf[256];int n_read = read(fd, read_buf, sizeof(read_buf));if (n_read > 0) {read_buf[n_read] = '\0'; // 添加字符串结束符printf("Received %d bytes: %s\n", n_read, read_buf);}// 6. 关闭串口close(fd);return 0;
}
对照函数表
#include <termios.h>
#include <unistd.h>//读取当前串口配置参数
int tcgetattr(int fd, struct termios *termios_p);//设置串口配置参数
int tcsetattr(int fd, int optional_actions,const struct termios *termios_p);//获取输入波特率
speed_t cfgetispeed(const struct termios *termios_p);//获取输出波特率
speed_t cfgetospeed(const struct termios *termios_p);//设置输入波特率
int cfsetispeed(struct termios *termios_p, speed_t speed);//设置输出波特率
int cfsetospeed(struct termios *termios_p, speed_t speed);//同时设置输入输出波特率
int cfsetspeed(struct termios *termios_p, speed_t speed);//清空buffer数据
int tcflush(int fd, int queue_selector);//等待所有输出都被发送
int tcdrain(int fd);//在一个指定的时间区内发送连续的0位流
int tcsendbreak(int fd, int duration);//用于对输入和输出流控制进行控制
int tcflow(int fd, int action);//将终端设置为原始模式
void cfmakeraw(struct termios *termios_p);
具体应用时代码
#include <stdio.h>
#include <fcntl.h>
#include <unistd.h>
#include <termios.h>
#include <string.h>int main() {/***************************************************************** 配置阶段 (Initialization) - 这部分代码在程序生命周期中只执行一次****************************************************************/// 1. 打开串口设备文件int fd = open("/dev/ttySTM3", O_RDWR | O_NOCTTY);if (fd < 0) {perror("Failed to open serial port");return -1;}// 2. 获取并修改termios配置 (所有stty/termios操作都在这里完成)struct termios options;tcgetattr(fd, &options);// 设置为原始模式, 9600, 8N1等...cfmakeraw(&options);cfsetispeed(&options, B9600);cfsetospeed(&options, B9600);options.c_cflag &= ~CSIZE;options.c_cflag |= CS8;options.c_cflag &= ~PARENB;options.c_cflag &= ~CSTOPB;options.c_cflag |= CREAD | CLOCAL;options.c_cc[VMIN] = 0;options.c_cc[VTIME] = 5; // 0.5秒超时// 3. 应用新的配置if (tcsetattr(fd, TCSANOW, &options) != 0) {perror("Failed to set attributes");close(fd);return -1;}// 4. 清空缓冲区tcflush(fd, TCIOFLUSH);printf("Serial port configured. Starting communication loop...\n");/***************************************************************** 通信循环 (Communication Loop) - 这部分代码会无限次地重复执行****************************************************************/while (1) {// --- 循环读取 ---char read_buf[256];int n_read = read(fd, read_buf, sizeof(read_buf));if (n_read > 0) {read_buf[n_read] = '\0';printf("Received %d bytes: %s", n_read, read_buf);// 在这里添加对接收到数据的处理逻辑...} else if (n_read < 0) {perror("Error reading from serial port");// 可以选择在这里跳出循环或进行错误处理break; }// 如果 n_read == 0, 说明超时了但没有读到数据,这是正常情况,继续下一次循环即可。// --- 循环写入 (示例) ---// 比如可以每隔5秒发送一次心跳包// write(fd, "HEARTBEAT\n", 10);// sleep(5);}/***************************************************************** 清理阶段 (Cleanup) - 正常情况下, 程序不会执行到这里,除非循环被break****************************************************************/printf("Closing serial port.\n");close(fd);return 0;
}
(tty_demo)在开发板上运行时,它会打开开发板自己的串口(/dev/ttySTM3),配置它(9600, 8N1等),然后向这个串口发送数据(write(fd, msg, …)),并接收数据(read(fd, read_buf, …))。
**顺带一提:**ioctl 是“底层实现”: 在Linux内核的“幕后”,tcgetattr() 和 tcsetattr() 这些函数内部调用的,正是 ioctl 这个更底层的、更“原始”的系统调用。
