串口(UART)C语言调用方式
目录
- 串口(UART)C语言调用方式
引言
串口(UART - Universal Asynchronous Receiver Transmitter)是嵌入式系统和Linux应用中最常用的外设之一,广泛应用于:
- 与微控制器通信
- 连接GPS模块、蓝牙模块等外设
- 调试和日志输出
- 工业设备通信
- 传感器数据采集
本文将详细介绍在Linux系统中使用C语言调用串口的方法,包括核心API、代码示例。
核心API
Linux系统中,串口设备(实际上所有的字符设备和块设备都被表示为文件)被表示为文件(如 /dev/ttyS0、/dev/ttyUSB0 等),可以使用标准的文件操作函数进行访问。但为了配置串口参数,需要使用 termios 库。
1. 主要头文件
c
#include <stdio.h> // 标准输入输出
#include <stdlib.h> // 标准库
#include <unistd.h> // UNIX标准函数
#include <fcntl.h> // 文件控制
#include <termios.h> // 终端控制
#include <string.h> // 字符串处理
#include <errno.h> // 错误处理
2. 核心函数
打开串口
c
int open(const char *pathname, int flags);
参数:
pathname:串口设备路径,如 "/dev/ttyS0"flags:打开方式,如O_RDWR | O_NOCTTY | O_NDELAY
返回值:成功返回文件描述符,失败返回 -1
配置串口
c
int tcgetattr(int fd, struct termios *termios_p);
int tcsetattr(int fd, int optional_actions, const struct termios *termios_p);
参数:
fd:文件描述符optional_actions:生效时机,如TCSANOW(立即生效)termios_p:termios 结构体指针
返回值:成功返回 0,失败返回 -1
设置波特率
c
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);
参数:
termios_p:termios 结构体指针speed:波特率,如B9600、B115200等
返回值:成功返回 0,失败返回 -1
读写操作
c
ssize_t read(int fd, void *buf, size_t count);
ssize_t write(int fd, const void *buf, size_t count);
参数:
fd:文件描述符buf:数据缓冲区count:数据长度
返回值:成功返回读写的字节数,失败返回 -1
关闭串口
c
int close(int fd);
参数:
fd:文件描述符
返回值:成功返回 0,失败返回 -1
3. termios 结构体
c
struct termios {
tcflag_t c_iflag; // 输入模式标志
tcflag_t c_oflag; // 输出模式标志
tcflag_t c_cflag; // 控制模式标志
tcflag_t c_lflag; // 本地模式标志
cc_t c_cc[NCCS]; // 控制字符
speed_t c_ispeed; // 输入波特率
speed_t c_ospeed; // 输出波特率
};
4. termios核心API详细说明
控制模式标志 (c_cflag)
| 标志 | 描述 |
|---|---|
| CLOCAL | 本地连接,忽略调制解调器状态线 |
| CREAD | 启用接收器 |
| CSIZE | 数据位掩码 |
| CS5 | 5位数据位 |
| CS6 | 6位数据位 |
| CS7 | 7位数据位 |
| CS8 | 8位数据位 |
| CSTOPB | 2位停止位(默认1位) |
| PARENB | 启用奇偶校验 |
| PARODD | 使用奇校验(默认偶校验) |
| HUPCL | 关闭时挂断调制解调器连接 |
| CRTSCTS | 启用硬件流控制(RTS/CTS) |
输入模式标志 (c_iflag)
| 标志 | 描述 |
|---|---|
| IGNBRK | 忽略中断条件 |
| BRKINT | 中断条件产生SIGINT信号 |
| IGNPAR | 忽略奇偶校验错误 |
| PARMRK | 标记奇偶校验错误 |
| INPCK | 启用输入奇偶校验 |
| ISTRIP | 剥离8位数据的第8位 |
| INLCR | 将NL转换为CR |
| IGNCR | 忽略CR |
| ICRNL | 将CR转换为NL |
| IXON | 启用XON/XOFF流控制 |
| IXOFF | 启用XOFF输入流控制 |
| IXANY | 任何字符都可以重启输入 |
输出模式标志 (c_oflag)
| 标志 | 描述 |
|---|---|
| OPOST | 启用输出处理 |
| ONLCR | 将NL转换为CR-NL |
| OCRNL | 将CR转换为NL |
| ONOCR | 第一列不输出CR |
| ONLRET | NL执行CR功能 |
| OFILL | 使用填充字符而不是延迟 |
| OFDEL | 填充字符是DEL而不是NUL |
本地模式标志 (c_lflag)
| 标志 | 描述 |
|---|---|
| ISIG | 启用信号产生 |
| ICANON | 启用规范模式 |
| ECHO | 回显输入字符 |
| ECHOE | 擦除字符时回显 |
| ECHOK | 回显KILL字符 |
| ECHONL | 回显NL |
| NOFLSH | 不刷新输入队列 |
| TOSTOP | 后台进程尝试写入时发送SIGTTOU |
控制字符 (c_cc)
| 索引 | 描述 |
|---|---|
| VINTR | 中断字符 |
| VQUIT | 退出字符 |
| VERASE | 擦除字符 |
| VKILL | 杀死字符 |
| VEOF | 文件结束字符 |
| VTIME | 读取超时(0.1秒为单位) |
| VMIN | 最小读取字符数 |
| VSWTC | 交换字符 |
| VSTART | 开始字符 |
| VSTOP | 停止字符 |
| VSUSP | 挂起字符 |
波特率常量
| 常量 | 波特率 |
|---|---|
| B0 | 0(挂断) |
| B50 | 50 |
| B75 | 75 |
| B110 | 110 |
| B134 | 134.5 |
| B150 | 150 |
| B200 | 200 |
| B300 | 300 |
| B600 | 600 |
| B1200 | 1200 |
| B1800 | 1800 |
| B2400 | 2400 |
| B4800 | 4800 |
| B9600 | 9600 |
| B19200 | 19200 |
| B38400 | 38400 |
| B57600 | 57600 |
| B115200 | 115200 |
| B230400 | 230400 |
| B460800 | 460800 |
| B500000 | 500000 |
| B576000 | 576000 |
| B921600 | 921600 |
| B1000000 | 1000000 |
串口控制函数
| 函数 | 描述 | 参数 | 返回值 |
|---|---|---|---|
| tcgetattr | 获取终端属性 | int fd, struct termios *termios_p | 成功返回0,失败返回-1 |
| tcsetattr | 设置终端属性 | int fd, int optional_actions, const struct termios *termios_p | 成功返回0,失败返回-1 |
| tcflush | 刷新输入/输出队列 | int fd, int queue_selector | 成功返回0,失败返回-1 |
| tcflow | 控制终端流 | int fd, int action | 成功返回0,失败返回-1 |
| tcsendbreak | 发送中断 | int fd, int duration | 成功返回0,失败返回-1 |
| tcdrain | 等待输出完成 | int fd | 成功返回0,失败返回-1 |
tcsetattr的optional_actions参数
| 参数 | 描述 |
|---|---|
| TCSANOW | 立即生效 |
| TCSADRAIN | 等待输出完成后生效 |
| TCSAFLUSH | 等待输出完成,刷新输入缓冲区后生效 |
tcflush的queue_selector参数
| 参数 | 描述 |
|---|---|
| TCIFLUSH | 刷新输入队列 |
| TCOFLUSH | 刷新输出队列 |
| TCIOFLUSH | 刷新输入和输出队列 |
串口配置流程
- 打开串口设备文件
- 使用tcgetattr获取当前配置
- 修改termios结构体中的配置项
- 使用tcsetattr应用新配置
- 使用tcflush清除缓冲区
- 进行读写操作
- 关闭串口设备文件
代码示例
1. 串口初始化与配置
c
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <fcntl.h>
#include <termios.h>
#include <string.h>
#include <errno.h>
/**
* 打开并配置串口
* @param port 串口设备路径
* @param baud_rate 波特率
* @return 成功返回文件描述符,失败返回 -1
*/
int uart_open(const char *port, int baud_rate)
{
int fd;
struct termios options;
speed_t speed;
/**
* 打开串口设备文件
*
* @details 打开指定路径的串口设备文件,获取文件描述符用于后续操作
* @param pathname 串口设备路径,如 "/dev/ttyS0"
* @param flags 打开方式标志组合
* O_RDWR: 读写模式
* O_NOCTTY: 不使设备成为控制终端
* O_NDELAY: 非阻塞模式
* @return 成功返回文件描述符(非负整数),失败返回-1并设置errno
* @see man 2 open
*/
fd = open(port, O_RDWR | O_NOCTTY | O_NDELAY);
if (fd < 0) {
perror("open serial port failed");
return -1;
}
// 获取当前配置
if (tcgetattr(fd, &options) != 0) {
perror("tcgetattr failed");
close(fd);
return -1;
}
// 设置波特率
switch (baud_rate) {
case 9600:
speed = B9600;
break;
case 19200:
speed = B19200;
break;
case 38400:
speed = B38400;
break;
case 57600:
speed = B57600;
break;
case 115200:
speed = B115200;
break;
default:
speed = B9600;
printf("Unsupported baud rate, using 9600\n");
}
// 设置输入输出波特率
if (cfsetispeed(&options, speed) != 0 || cfsetospeed(&options, speed) != 0) {
perror("cfsetispeed/cfsetospeed failed");
close(fd);
return -1;
}
// 设置控制模式
options.c_cflag |= (CLOCAL | CREAD); // 启用接收器,设置本地模式
options.c_cflag &= ~CSIZE; // 清除数据位设置
options.c_cflag |= CS8; // 8位数据位
options.c_cflag &= ~PARENB; // 无校验位
options.c_cflag &= ~CSTOPB; // 1位停止位
options.c_cflag &= ~CRTSCTS; // 无硬件流控制
// 设置输入模式
options.c_iflag &= ~(IXON | IXOFF | IXANY); // 无软件流控制
options.c_iflag &= ~(INLCR | ICRNL | ISTRIP); // 禁用特殊字符处理
// 设置输出模式
options.c_oflag &= ~OPOST; // 原始输出
// 设置本地模式
options.c_lflag &= ~(ICANON | ECHO | ECHOE | ISIG); // 原始输入
// 设置超时
options.c_cc[VTIME] = 10; // 读取超时时间(0.1秒为单位)
options.c_cc[VMIN] = 0; // 最小读取字符数
// 应用配置
if (tcsetattr(fd, TCSANOW, &options) != 0) {
perror("tcsetattr failed");
close(fd);
return -1;
}
// 清除缓冲区
tcflush(fd, TCIOFLUSH);
return fd;
}
/**
* 关闭串口
* @param fd 文件描述符
*/
void uart_close(int fd)
{
if (fd >= 0) {
close(fd);
}
}
/**
* 读取串口数据
* @param fd 文件描述符
* @param buf 缓冲区
* @param len 缓冲区长度
* @return 读取的字节数,失败返回 -1
*/
int uart_read(int fd, char *buf, int len)
{
return read(fd, buf, len);
}
/**
* 写入串口数据
* @param fd 文件描述符
* @param buf 缓冲区
* @param len 数据长度
* @return 写入的字节数,失败返回 -1
*/
int uart_write(int fd, const char *buf, int len)
{
return write(fd, buf, len);
}
int main()
{
int fd;
char buf[256];
int len;
// 打开串口
fd = uart_open("/dev/ttyS0", 115200);
if (fd < 0) {
printf("Failed to open serial port\n");
return -1;
}
printf("Serial port opened successfully\n");
// 发送数据
const char *msg = "Hello, Serial Port!\n";
len = uart_write(fd, msg, strlen(msg));
if (len < 0) {
perror("uart_write failed");
} else {
printf("Sent %d bytes: %s", len, msg);
}
// 读取数据(非阻塞)
len = uart_read(fd, buf, sizeof(buf) - 1);
if (len > 0) {
buf[len] = '\0';
printf("Received %d bytes: %s", len, buf);
} else if (len == 0) {
printf("No data received\n");
} else {
perror("uart_read failed");
}
// 关闭串口
uart_close(fd);
printf("Serial port closed\n");
return 0;
}
2. 带超时的串口读取
c
/**
* 带超时的串口读取
* @param fd 文件描述符
* @param buf 缓冲区
* @param len 缓冲区长度
* @param timeout_ms 超时时间(毫秒)
* @return 读取的字节数,失败返回 -1
*/
int uart_read_timeout(int fd, char *buf, int len, int timeout_ms)
{
fd_set read_fds;
struct timeval timeout;
int ret;
// 设置文件描述符集
FD_ZERO(&read_fds);
FD_SET(fd, &read_fds);
// 设置超时时间
timeout.tv_sec = timeout_ms / 1000;
timeout.tv_usec = (timeout_ms % 1000) * 1000;
// 等待数据
ret = select(fd + 1, &read_fds, NULL, NULL, &timeout);
if (ret < 0) {
perror("select failed");
return -1;
} else if (ret == 0) {
// 超时
return 0;
}
// 有数据可读
if (FD_ISSET(fd, &read_fds)) {
return read(fd, buf, len);
}
return 0;
}
3. 串口配置示例
c
/**
* 配置串口参数
* @param fd 文件描述符
* @param baud_rate 波特率
* @param data_bits 数据位(5-8)
* @param parity 校验位(0: 无, 1: 奇, 2: 偶)
* @param stop_bits 停止位(1-2)
* @return 成功返回 0,失败返回 -1
*/
int uart_config(int fd, int baud_rate, int data_bits, int parity, int stop_bits)
{
struct termios options;
speed_t speed;
// 获取当前配置
if (tcgetattr(fd, &options) != 0) {
perror("tcgetattr failed");
return -1;
}
// 设置波特率
switch (baud_rate) {
case 9600:
speed = B9600;
break;
case 19200:
speed = B19200;
break;
case 38400:
speed = B38400;
break;
case 57600:
speed = B57600;
break;
case 115200:
speed = B115200;
break;
default:
speed = B9600;
printf("Unsupported baud rate, using 9600\n");
}
if (cfsetispeed(&options, speed) != 0 || cfsetospeed(&options, speed) != 0) {
perror("cfsetispeed/cfsetospeed failed");
return -1;
}
// 设置控制模式
options.c_cflag |= (CLOCAL | CREAD);
options.c_cflag &= ~CSIZE;
// 设置数据位
switch (data_bits) {
case 5:
options.c_cflag |= CS5;
break;
case 6:
options.c_cflag |= CS6;
break;
case 7:
options.c_cflag |= CS7;
break;
case 8:
options.c_cflag |= CS8;
break;
default:
options.c_cflag |= CS8;
printf("Unsupported data bits, using 8\n");
}
// 设置校验位
switch (parity) {
case 0: // 无校验
options.c_cflag &= ~PARENB;
break;
case 1: // 奇校验
options.c_cflag |= PARENB;
options.c_cflag |= PARODD;
break;
case 2: // 偶校验
options.c_cflag |= PARENB;
options.c_cflag &= ~PARODD;
break;
default:
options.c_cflag &= ~PARENB;
printf("Unsupported parity, using none\n");
}
// 设置停止位
if (stop_bits == 2) {
options.c_cflag |= CSTOPB;
} else {
options.c_cflag &= ~CSTOPB;
}
// 无硬件流控制
options.c_cflag &= ~CRTSCTS;
// 设置输入模式
options.c_iflag &= ~(IXON | IXOFF | IXANY);
options.c_iflag &= ~(INLCR | ICRNL | ISTRIP);
// 设置输出模式
options.c_oflag &= ~OPOST;
// 设置本地模式
options.c_lflag &= ~(ICANON | ECHO | ECHOE | ISIG);
// 设置超时
options.c_cc[VTIME] = 10;
options.c_cc[VMIN] = 0;
// 应用配置
if (tcsetattr(fd, TCSANOW, &options) != 0) {
perror("tcsetattr failed");
return -1;
}
// 清除缓冲区
tcflush(fd, TCIOFLUSH);
return 0;
}
常见问题与解决方案
1. 串口无法打开
问题 :open() 函数返回 -1,错误信息为 "Permission denied"
解决方案:
- 检查当前用户是否有串口设备的访问权限
- 将用户添加到 dialout 组:
sudo usermod -a -G dialout username - 重新登录后生效
问题 :open() 函数返回 -1,错误信息为 "No such file or directory"
解决方案:
- 检查串口设备路径是否正确
- 确认串口设备是否存在:
ls -la /dev/tty* - 对于 USB 转串口设备,检查设备是否正确识别
2. 数据传输问题
问题:发送数据后,接收端收到的数据不正确
解决方案:
- 检查波特率、数据位、校验位、停止位设置是否一致
- 检查流控制设置是否正确
- 确认线缆连接是否可靠
问题:读取数据时阻塞
解决方案:
- 检查
VMIN和VTIME设置 - 使用
select()实现超时机制 - 确保串口设置为非阻塞模式(如果需要)
3. 性能问题
问题:串口数据传输速度较慢
解决方案:
- 提高波特率(如果硬件支持)
- 优化缓冲区大小
- 减少系统调用次数,使用批量读写
- 考虑使用
termios的RAW模式减少处理开销