引言
UART(Universal Asynchronous Receiver/Transmitter)是一种用于异步串行通信的硬件协议,常用于计算机和外设之间的数据交换。在嵌入式系统中,UART 通信非常常见,用于连接传感器、微控制器、调制解调器等设备。为了实现高效的 UART 通信,通常需要使用非阻塞式 I/O 操作,这时候 select
函数就派上用场了。
UART 通信中的 select
函数
select
函数允许我们同时监控多个文件描述符,并在这些文件描述符变为可读、可写或发生错误时通知程序。这样可以避免程序在等待 I/O 操作时被阻塞,提高系统的响应速度和效率。
select
函数的工作原理
- 监控文件描述符:
select
接受一组文件描述符,并监控这些文件描述符的状态。 - 阻塞等待:可以设置阻塞等待或非阻塞等待,直到文件描述符变为可读、可写或发生错误,或者达到超时时间。
- 返回事件:
select
返回后,程序可以检查哪些文件描述符变为可读、可写或发生错误,并进行相应的处理。
使用 select
进行 UART 通信的注意事项
在使用 select
进行 UART 通信时,有几个关键点需要注意:
- 正确初始化文件描述符集
每次调用 select
之前,需要重新初始化文件描述符集(fd_set
),因为 select
调用会修改这个集合:
FD_ZERO(&rfds);
FD_SET(fd, &rfds);
- 设置超时值
在每次调用 select
之前,需要设置超时值(struct timeval
)。同样,select
会修改这个结构体,因此每次调用前都需要重新设置:
struct timeval tv;
tv.tv_sec = timeout_us / 1000000;
tv.tv_usec = timeout_us % 1000000;
- 处理
select
的返回值
检查 select
的返回值以确定是否有文件描述符变为可读、可写或出错:
int ret = select(fd + 1, &rfds, NULL, NULL, &tv);
if (ret == -1) {
// Handle error
} else if (ret == 0) {
// Handle timeout
} else {
if (FD_ISSET(fd, &rfds)) {
// Handle readable data
}
}
- 避免文件描述符泄漏
确保在程序结束或不再需要文件描述符时关闭它们,防止资源泄漏:
close(fd);
- 多线程环境下的文件描述符使用
如果在多线程环境下使用文件描述符,需要确保对文件描述符的操作是线程安全的。使用互斥锁(mutex)保护对文件描述符的操作,避免竞态条件。
- 检查文件描述符的有效性
在调用 select
之前检查文件描述符是否有效,确保文件描述符在整个生命周期中是有效的:
int check_fd_valid(int fd) {
return fcntl(fd, F_GETFD) != -1 || errno != EBADF;
}
- 处理边界情况
处理 read
返回 0
的情况,这通常表示文件结束或没有数据可用:
ssize_t bytesRead = read(fd, buff, len);
if (bytesRead == -1) {
// Handle read error
} else if (bytesRead == 0) {
// Handle EOF or no data
}
示例代码
以下是综合了上述注意事项的改进代码示例:
#include <stdio.h>
#include <string.h>
#include <sys/select.h>
#include <unistd.h>
#include <errno.h>
#include <stdint.h>
#include <stdarg.h>
#include <fcntl.h>
#include <pthread.h>
void ota_info(const char *fmt, ...) {
va_list args;
va_start(args, fmt);
vprintf(fmt, args);
printf("\n");
va_end(args);
}
int check_fd_valid(int fd) {
return fcntl(fd, F_GETFD) != -1 || errno != EBADF;
}
int uart_read(int fd, uint8_t *buff, int len, int timeout_us)
{
int ret;
struct timeval tv;
fd_set rfds;
memset(buff, 0, len);
// Debug log to print fd value before select
ota_info("uart_read: fd=%d, timeout_us=%d, thread_id=%ld", fd, timeout_us, pthread_self());
// Check if fd is valid
if (!check_fd_valid(fd)) {
ota_info("Invalid file descriptor: %d", fd);
return -1;
}
FD_ZERO(&rfds);
FD_SET(fd, &rfds);
memset(&tv, 0, sizeof(tv));
tv.tv_sec = timeout_us / 1000000;
tv.tv_usec = timeout_us % 1000000;
ret = select(fd + 1, &rfds, NULL, NULL, &tv);
if (ret == -1) {
// Select itself failed
ota_info("select() failed. ret %d, errno %d, %m", ret, errno);
return -1;
} else if (ret == 0) {
// Timeout occurred without any file descriptor becoming ready
ota_info("select() timed out.");
return 0; // Indicate timeout
}
if (FD_ISSET(fd, &rfds)) {
ssize_t bytesRead = read(fd, buff, len);
if (bytesRead == -1) {
// Error during read
ota_info("read() failed. errno %d, %m", errno);
return -1;
} else if (bytesRead == 0) {
// EOF reached or no data available
ota_info("read() returned 0, no data available or EOF reached.");
return 0;
}
// Return actual bytes read
ota_info("read() succeeded. bytesRead=%ld, thread_id=%ld", bytesRead, pthread_self());
return bytesRead;
} else {
// This branch should not be reachable given the checks above
ota_info("select() returned with no file descriptor ready.");
return 0;
}
}
int main() {
// Example usage
int fd = 0; // Example file descriptor, should be initialized properly
uint8_t buffer[256];
int len = 256;
int timeout_us = 5000000; // 5 seconds
int result = uart_read(fd, buffer, len, timeout_us);
if (result > 0) {
printf("Read %d bytes\n", result);
} else if (result == 0) {
printf("Timeout or no data available\n");
} else {
printf("Error occurred\n");
}
return 0;
}
通过遵循这些注意事项,可以确保在使用 select
函数进行 UART 通信时,代码更加健壮和可靠,减少出现难以复现的错误的可能性。希望这篇博客能帮助你在项目中更好地使用 select
函数进行 UART 通信。