10、UART与串口驱动开发:QNX串口驱动配置、Android串口HAL实现、调试串口与日志输出、波特率与流控制设置
串口,说白了就是嵌入式系统里最老牌、最可靠的通信方式之一。在车载智能座舱里,它可能连接着GPS模块、调试板、甚至某些老旧的传感器。我这些年调过的串口,少说也有几十种了。嗯,今天咱们就聊聊QNX和Android Automotive下,串口驱动到底该怎么搞。
10.1 串口通信基础回顾
先简单过一下基础。UART是异步串行通信,靠起始位、数据位、校验位和停止位来传输数据。你想想看,没有时钟线,全靠约定好的波特率来同步,这就是它的特点。
串口通信的几个关键参数:
- 波特率:每秒传输的符号数。常见的有9600、115200、921600等。
- 数据位:通常是8位,也有7位或9位的情况。
- 校验位:奇校验、偶校验、或无校验。
- 停止位:1位、1.5位或2位。
- 流控制:硬件流控制(RTS/CTS)或软件流控制(XON/XOFF)。
重要提醒:在车载项目中,波特率不匹配是最常见的坑。我曾经遇到过一台设备,明明配置的是115200,结果对方用的是115200的变体——实际是115200的0.5%误差,导致数据全是乱码。所以,建议用示波器量一下实际波形,确认波特率。
10.2 QNX串口驱动配置
QNX的串口驱动,核心是devc-ser*系列驱动。我习惯用devc-ser8250或devc-seromap,具体取决于硬件平台。
启动串口驱动的典型命令:
# 启动串口1,波特率115200,8N1,无流控制
devc-ser8250 -e -F -b 115200 -c 0x3f8 -i 4 &
# 参数说明:
# -e 启用硬件流控制(可选)
# -F 前台运行(调试用)
# -b 设置波特率
# -c 基地址(I/O端口或内存映射地址)
# -i 中断号
配置完成后,通过open()打开设备节点:
int fd = open("/dev/ser1", O_RDWR | O_NONBLOCK);
if (fd < 0) {
perror("open serial port failed");
return -1;
}
然后设置串口参数。QNX里用devctl()来操作:
struct termios tio;
devctl(fd, DEV_TCGETATTR, &tio, sizeof(tio), NULL);
// 设置波特率
cfsetispeed(&tio, B115200);
cfsetospeed(&tio, B115200);
// 8N1模式
tio.c_cflag &= ~PARENB; // 无校验
tio.c_cflag &= ~CSTOPB; // 1位停止位
tio.c_cflag |= CS8; // 8位数据位
// 启用接收
tio.c_cflag |= CREAD;
devctl(fd, DEV_TCSETATTR, &tio, sizeof(tio), NULL);
个人经验:QNX下串口非阻塞模式特别重要。我做过一个项目,GPS模块每秒输出一次NMEA数据,如果用阻塞读,一旦数据延迟,整个线程就卡死了。所以,O_NONBLOCK几乎是标配。
10.3 Android串口HAL实现
Android Automotive里,串口访问通常通过HAL层封装。我参与过一个项目,需要把QNX的串口服务通过IPC暴露给Android上层。嗯,这里的关键是HAL的接口设计。
一个典型的串口HAL头文件:
// hardware/serial.h
typedef struct serial_device {
struct hw_device_t common;
int (*open)(const char *port, int baudrate);
int (*close)(void);
int (*read)(uint8_t *buf, size_t len);
int (*write)(const uint8_t *buf, size_t len);
int (*set_baudrate)(int baudrate);
int (*set_flowcontrol)(int enable);
} serial_device_t;
实现时,底层通过JNI调用QNX的串口API:
// serial_hal_qnx.cpp
static int serial_open(const char *port, int baudrate) {
int fd = open(port, O_RDWR | O_NONBLOCK);
if (fd < 0) return -1;
struct termios tio;
tcgetattr(fd, &tio);
cfsetispeed(&tio, baudrate);
cfsetospeed(&tio, baudrate);
tio.c_cflag |= (CLOCAL | CREAD);
tio.c_cflag &= ~CSIZE;
tio.c_cflag |= CS8;
tio.c_cflag &= ~PARENB;
tio.c_cflag &= ~CSTOPB;
tcsetattr(fd, TCSANOW, &tio);
return fd;
}
避坑指南:我曾经在Android HAL层直接调用QNX的open(),结果发现权限不够。因为Android应用运行在沙箱里,直接访问/dev/ser1会被SELinux拦截。解决方案是:通过QNX的资源管理器(Resource Manager)暴露一个安全接口,Android HAL通过QNX IPC来访问。
10.4 调试串口与日志输出
调试串口,说白了就是开发者的“眼睛”。在QNX下,我通常把调试串口配置为系统控制台:
# 在构建镜像时,指定调试串口
-serial 0x3f8,115200
# 或者运行时重定向
sloginfo -w &
slog2info -w &
日志输出的几个层次:
- Kernel log:通过
sloginfo查看,适合驱动级别的调试。 - Application log:通过
slog2info查看,适合应用层。 - 串口直出:直接把
printf输出到调试串口,适合快速验证。
我习惯在驱动里加一个调试开关:
#define SERIAL_DEBUG 1
#if SERIAL_DEBUG
#define serial_dbg(fmt, ...) \
printf("[SERIAL] " fmt "\n", ##__VA_ARGS__)
#else
#define serial_dbg(fmt, ...)
#endif
这样,调试时打开宏,发布时关掉,不影响性能。
10.5 波特率与流控制设置
波特率设置,看似简单,其实有坑。QNX支持的波特率是固定的:
| 宏定义 | 实际波特率 | 常见用途 |
|---|---|---|
| B9600 | 9600 | 老旧设备、GPS模块 |
| B115200 | 115200 | 调试串口、蓝牙模块 |
| B921600 | 921600 | 高速数据传输 |
| B3000000 | 3Mbps | 某些高速传感器 |
流控制方面,硬件流控制(RTS/CTS)是最可靠的。我建议在长距离传输或高速率时一定要开启:
// 启用硬件流控制
tio.c_cflag |= CRTSCTS;
// 禁用硬件流控制
tio.c_cflag &= ~CRTSCTS;
软件流控制(XON/XOFF)我基本不用。为什么?因为一旦数据里出现0x11或0x13,就会误触发流控制,导致数据丢失。嗯,这个坑我踩过。
核心要点:波特率越高,对线缆质量要求越高。3Mbps时,线长超过1米就可能出问题。我建议在车载项目中,如果距离超过30cm,就用RS422或RS485,而不是直接TTL电平。
10.6 实战:一个完整的串口收发示例
最后,给一个完整的QNX串口收发示例。这个代码我在多个项目中复用,基本没出过问题:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <fcntl.h>
#include <unistd.h>
#include <devctl.h>
#include <termios.h>
int main() {
int fd = open("/dev/ser1", O_RDWR | O_NONBLOCK);
if (fd < 0) {
perror("open");
return 1;
}
struct termios tio;
devctl(fd, DEV_TCGETATTR, &tio, sizeof(tio), NULL);
cfsetispeed(&tio, B115200);
cfsetospeed(&tio, B115200);
tio.c_cflag |= (CLOCAL | CREAD | CRTSCTS);
tio.c_cflag &= ~CSIZE;
tio.c_cflag |= CS8;
tio.c_cflag &= ~PARENB;
tio.c_cflag &= ~CSTOPB;
devctl(fd, DEV_TCSETATTR, &tio, sizeof(tio), NULL);
// 发送数据
const char *msg = "AT+GPS=1\r\n";
write(fd, msg, strlen(msg));
// 接收数据(非阻塞)
char buf[256];
int n = read(fd, buf, sizeof(buf) - 1);
if (n > 0) {
buf[n] = '\0';
printf("Received: %s\n", buf);
}
close(fd);
return 0;
}
个人习惯:我每次写完串口代码,都会用stty命令验证一下配置。比如stty -F /dev/ser1 -a,看看波特率、流控制是否生效。这个小习惯帮我省了不少调试时间。
串口驱动开发,说难不难,说简单也不简单。关键是把基础参数搞对,把流控制选好,把调试手段备足。嗯,做到这三点,大部分串口问题都能快速定位。