香橙派OrangePi PC+串口开发实战:从库函数到Linux原生API
1. 香橙派串口开发基础准备第一次拿到香橙派开发板时最让我头疼的就是串口配置问题。OrangePi PC提供了三个可用的串口设备分别是/dev/ttyS1、/dev/ttyS2和/dev/ttyS3但默认都是关闭状态。这里有个坑要注意官方文档说默认开启但实测下来需要手动配置。开启串口有两种常用方法我推荐第一种因为更直接可控。首先通过SSH或终端登录系统然后编辑/boot/orangepiEnv.txt文件。找到对应串口的配置项比如要开启ttyS1就添加一行uart1_used1。保存后重启使用ls /dev/ttyS*命令就能看到设备节点了。如果你用的是Armbian系统配置文件路径会变成/boot/armbianEnv.txt这点特别容易搞错。我就曾经在这个问题上浪费了半天时间一直纳闷为什么修改不生效。另外硬件连接也要注意香橙派的串口引脚排列与树莓派不同建议先用gpio readall命令确认TX、RX引脚位置。2. wiringPi库的快速上手对于刚接触嵌入式开发的新手wiringPi库真是个福音。它封装了底层硬件操作让串口通信变得异常简单。先来看看最基本的串口初始化代码#include wiringSerial.h int main() { int fd serialOpen(/dev/ttyS1, 115200); if(fd 0) { printf(串口打开失败\n); return -1; } serialPuts(fd, Hello OrangePi!\n); serialClose(fd); return 0; }编译时需要链接wiringPi库gcc demo.c -lwiringPi -o demo。这个简单的例子已经实现了串口发送功能但实际项目中我们更需要全双工通信。下面是我在智能家居项目中用到的多线程通信框架void* send_thread(void* arg) { int fd *(int*)arg; while(1) { char buf[128]; fgets(buf, sizeof(buf), stdin); serialPuts(fd, buf); } } void* recv_thread(void* arg) { int fd *(int*)arg; while(1) { if(serialDataAvail(fd)) { char c serialGetchar(fd); putchar(c); } } }这种架构虽然简单但在实际使用中我发现有个常见问题当串口数据量较大时会出现数据丢失。解决方法是在接收线程中加入适当的延时比如usleep(1000)。3. 深入Linux原生串口API当你对串口通信有了基本了解后就该深入Linux原生API了。与wiringPi不同原生API需要我们自己处理所有参数配置这包括波特率设置数据位、停止位配置流控设置终端模式选择核心数据结构是termios它包含了所有的串口配置参数。下面这个函数展示了如何完整配置一个串口int setup_serial(int fd, int speed) { struct termios options; // 获取当前配置 if(tcgetattr(fd, options) 0) { perror(tcgetattr); return -1; } // 设置输入输出波特率 cfsetispeed(options, speed); cfsetospeed(options, speed); // 8N1配置 options.c_cflag ~PARENB; // 无奇偶校验 options.c_cflag ~CSTOPB; // 1位停止位 options.c_cflag ~CSIZE; // 清除数据位掩码 options.c_cflag | CS8; // 8位数据位 // 启用接收和忽略控制线 options.c_cflag | (CLOCAL | CREAD); // 原始模式输入 options.c_lflag ~(ICANON | ECHO | ECHOE | ISIG); // 禁用软件流控 options.c_iflag ~(IXON | IXOFF | IXANY); // 原始模式输出 options.c_oflag ~OPOST; // 设置超时100ms options.c_cc[VTIME] 1; options.c_cc[VMIN] 0; // 立即应用配置 if(tcsetattr(fd, TCSANOW, options) 0) { perror(tcsetattr); return -1; } return 0; }在实际项目中我发现termios配置最容易出错的是流控设置。有一次调试GPS模块就是因为IXON标志位没清除导致数据接收不全。建议在开发初期就打印出所有标志位进行验证。4. 完整项目实战串口通信框架结合前面所学我们可以构建一个更健壮的串口通信框架。这个框架包含以下功能多线程处理错误检测与恢复数据缓冲机制心跳包检测首先是串口初始化部分int uart_init(const char *device, int baud) { int fd open(device, O_RDWR | O_NOCTTY | O_NDELAY); if(fd 0) { perror(open); return -1; } // 恢复阻塞模式 fcntl(fd, F_SETFL, 0); // 配置串口参数 if(setup_serial(fd, baud) 0) { close(fd); return -1; } // 清空缓冲区 tcflush(fd, TCIOFLUSH); return fd; }数据收发部分采用环形缓冲区设计避免数据丢失#define BUF_SIZE 1024 typedef struct { char buffer[BUF_SIZE]; int head; int tail; pthread_mutex_t lock; } ring_buffer; void buf_init(ring_buffer *rb) { memset(rb-buffer, 0, BUF_SIZE); rb-head rb-tail 0; pthread_mutex_init(rb-lock, NULL); } int buf_put(ring_buffer *rb, char c) { pthread_mutex_lock(rb-lock); int next (rb-head 1) % BUF_SIZE; if(next ! rb-tail) { rb-buffer[rb-head] c; rb-head next; pthread_mutex_unlock(rb-lock); return 0; } pthread_mutex_unlock(rb-lock); return -1; // 缓冲区满 } int buf_get(ring_buffer *rb, char *c) { pthread_mutex_lock(rb-lock); if(rb-tail ! rb-head) { *c rb-buffer[rb-tail]; rb-tail (rb-tail 1) % BUF_SIZE; pthread_mutex_unlock(rb-lock); return 0; } pthread_mutex_unlock(rb-lock); return -1; // 缓冲区空 }在实际部署时我发现直接读写串口设备可能会导致线程阻塞。解决方法是为每个文件描述符设置超时时间并使用select多路复用机制int uart_read(int fd, char *buf, int len, int timeout_ms) { fd_set rfds; struct timeval tv; FD_ZERO(rfds); FD_SET(fd, rfds); tv.tv_sec timeout_ms / 1000; tv.tv_usec (timeout_ms % 1000) * 1000; int ret select(fd1, rfds, NULL, NULL, tv); if(ret 0) { return read(fd, buf, len); } return ret; }这个框架在我参与的工业传感器项目中表现稳定连续运行30天没有出现通信故障。关键点在于正确处理了各种边界条件和异常情况比如串口断开重连、数据校验和超时重试等。