驱动开发有大半情况是需要和硬件芯片交互的,而国内,最多的情况就是拿到国外的芯片,然后进行仿制,故能根据芯片设计出解决方案这种技术是许多高级工程师的基本操作。
PL2303 是一个被广泛使用的 USB 转 RS232 串口芯片。其中一些型号早已停产,但还在市场上流通,被使用在一些产品上。在较新的 Windows 10 系统中,首次通过 USB 连接 PL2303 时,系统默认安装的驱动是不能使用的。
因为工作原因,笔者曾在淘宝购买了一款使用USB PL2303来输出GPS信号的GPS产品,厂家明确告知我这款产品只支持Android和Linux,这个消息是错误,后来我还是到官网找到了合适的官方驱动,但正好可以使用这款产品来写一个基于windows系统的驱动案例。
产品和开发方式
GPS产品使用起来非常简单,打开串口之后设置波特率为115200即可主动上报GPS数据。
在Ubuntu 22.04下可以用minicom配置串口后打开:
此时串口的名称是ttyUSB0, 当设置了115200的波特率之后,就会主动上报数据:
这里的数据就是GPS的定位数据,具体怎么解析就不展开说了。
有两个方式来开发这种仅支持Linux的芯片的window驱动:
第一个方案是通过在Linux平台下查看源代码,然后总结出源代码中是如何使用的,再把这些方案移植到windows平台下,这样速度很快。
第二个方案就是在Linux中记录和USB设备的交互,然后根据总结的交互策略来编写windows驱动,这样会慢一点。
有朋友私信提出还可以逆向官方的驱动,这条路确实可行,但麻烦之处是官方的驱动很可能会初始化一大堆没什么用的功能和配置,这个工作量就太大了,事实上这个驱动笔者只用了数小时就完成了,笔者并不打算用更多时间。
在这次开发中,由于Linux平台已经开发了源代码,所以使用第一种方法即可解决问题,但由于该案例并不具备商业化的基础,故仅维持最低程度的核心功能(上报GPS信息)即可;同时笔者对linux系统不熟悉,只有能看懂内核代码的程度,所以下面的分析不会详细讲解Linux驱动,只是为了提取需要的信息。
这个驱动比较好的展示了一种驱动开发的方案:尽可能简单的提供功能,仅提供一种系统服务,笔者接触的GNSS或5G芯片都是用一种简单粗暴的方式来处理,打开端口后,数据一直上报,没有过滤也没有其它复杂的操作,最后使用了数百行代码就完成了驱动上报GPS功能,即使在IOS、Android中也是如此。
Android源码简介
源码的位置在drivers/usb/serial/pl2303.c中,在大部分android开源版本中都能找到,由于这里对我们来说仅仅就是参考,所以不用在意具体版本,可以看到关键的函数如下:
static void pl2303_set_termios (struct usb_serial_port *port, struct termios *old_termios)
{
struct usb_serial *serial = port->serial;
struct pl2303_private *priv = usb_get_serial_port_data(port);
unsigned long flags;
unsigned int cflag;
unsigned char *buf;
int baud;
int i;
u8 control;
dbg("%s - port %d", __FUNCTION__, port->number);
if ((!port->tty) || (!port->tty->termios)) {
dbg("%s - no tty structures", __FUNCTION__);
return;
}
spin_lock_irqsave(&priv->lock, flags);
if (!priv->termios_initialized) {
*(port->tty->termios) = tty_std_termios;
port->tty->termios->c_cflag = B9600 | CS8 | CREAD | HUPCL | CLOCAL;
priv->termios_initialized = 1;
}
spin_unlock_irqrestore(&priv->lock, flags);
cflag = port->tty->termios->c_cflag;
/* check that they really want us to change something */
if (old_termios) {
if ((cflag == old_termios->c_cflag) &&
(RELEVANT_IFLAG(port->tty->termios->c_iflag) == RELEVANT_IFLAG(old_termios->c_iflag))) {
dbg("%s - nothing to change...", __FUNCTION__);
return;
}
}
buf = kzalloc (7, GFP_KERNEL);
if (!buf) {
dev_err(&port->dev, "%s - out of memory.\n", __FUNCTION__);
return;
}
i = usb_control_msg (serial->dev, usb_rcvctrlpipe (serial->dev, 0),
GET_LINE_REQUEST, GET_LINE_REQUEST_TYPE,
0, 0, buf, 7, 100);
dbg ("0xa1:0x21:0:0 %d - %x %x %x %x %x %x %x", i,
buf[0], buf[1], buf[2], buf[3], buf[4], buf[5], buf[6]);
if (cflag & CSIZE) {
switch (cflag & CSIZE) {
case CS5: buf[6] = 5; break;
case CS6: buf[6] = 6; break;
case CS7: buf[6] = 7; break;
default:
case CS8: buf[6] = 8; break;
}
dbg("%s - data bits = %d", __FUNCTION__, buf[6]);
}
baud = 0;
switch (cflag & CBAUD) {
case B0: baud = 0; break;
case B75: baud = 75; break;
case B150: baud = 150; break;
case B300: baud = 300; break;
case B600: baud = 600; break;
case B1200: baud = 1200; break;
case B1800: baud = 1800; break;
case B2400: baud = 2400; break;
case B4800: baud = 4800; break;
case B9600: baud = 9600; break;
case B19200: baud = 19200; break;
case B38400: baud = 38400; break;
case B57600: baud = 57600; break;
case B115200: baud = 115200; break;
case B230400: baud = 230400; break;
case B460800: baud = 460800; break;
default:
dev_err(&port->dev, "pl2303 driver does not support the baudrate requested (fix it)\n");
break;
}
dbg("%s - baud = %d", __FUNCTION__, baud);
if (baud) {
buf[0] = baud & 0xff;
buf[1] = (baud >> 8) & 0xff;
buf[2] = (baud >> 16) & 0xff;
buf[3] = (baud >> 24) & 0xff;
}
/* For reference buf[4]=0 is 1 stop bits */
/* For reference buf[4]=1 is 1.5 stop bits */
/* For reference buf[4]=2 is 2 stop bits */
if (cflag & CSTOPB) {
buf[4] = 2;
dbg("%s - stop bits = 2", __FUNCTION__);
} else {
buf[4] = 0;
dbg("%s - stop bits = 1", __FUNCTION__);
}
if (cflag & PARENB) {
/* For reference buf[5]=0 is none parity */
/* For reference buf[5]=1 is odd parity */
/* For reference buf[5]=2 is even parity */
/* For reference buf[5]=3 is mark parity */
/* For reference buf[5]=4 is space parity */
if (cflag & PARODD) {
buf[5] = 1;
dbg("%s - parity = odd", __FUNCTION__);
} else {
buf[5] = 2;
dbg("%s - parity = even", __FUNCTION__);
}
} else {
buf[5] = 0;
dbg("%s - parity = none", __FUNCTION__);
}
i = usb_control_msg (serial->dev, usb_sndctrlpipe (serial->dev, 0),
SET_LINE_REQUEST, SET_LINE_REQUEST_TYPE,
0, 0, buf, 7, 100);
dbg ("0x21:0x20:0:0 %d", i);
/* change control lines if we are switching to or from B0 */
spin_lock_irqsave(&priv->lock, flags);
control = priv->line_control;
if ((cflag & CBAUD) == B0)
priv->line_control &= ~(CONTROL_DTR | CONTROL_RTS);
else
priv->line_control |= (CONTROL_DTR | CONTROL_RTS);
if (control != priv->line_control) {
control = priv->line_control;
spin_unlock_irqrestore(&priv->lock, flags);
set_control_lines(serial->dev, control);
} else {
spin_unlock_irqrestore(&priv->lock, flags);
}
buf[0] = buf[1] = buf[2] = buf[3] = buf[4] = buf[5] = buf[6] = 0;
i = usb_control_msg (serial->dev, usb_rcvctrlpipe (serial->dev, 0),
GET_LINE_REQUEST, GET_LINE_REQUEST_TYPE,
0, 0, buf, 7, 100);
dbg ("0xa1:0x21:0:0 %d - %x %x %x %x %x %x %x", i,
buf[0], buf[1], buf[2], buf[3], buf[4], buf[5], buf[6]);
if (cflag & CRTSCTS) {
__u16 index;
if (priv->type == HX)
index = 0x61;
else
index = 0x41;
i = usb_control_msg(serial->dev,
usb_sndctrlpipe(serial->dev, 0),
VENDOR_WRITE_REQUEST,
VENDOR_WRITE_REQUEST_TYPE,
0x0, index, NULL, 0, 100);
dbg ("0x40:0x1:0x0:0x%x %d", index, i);
}
kfree (buf);
}
static int pl2303_open (struct usb_serial_port *port, struct file *filp)
{
struct termios tmp_termios;
struct usb_serial *serial = port->serial;
struct pl2303_private *priv = usb_get_serial_port_data(port);
unsigned char *buf;
int result;
dbg("%s - port %d", __FUNCTION__, port->number);
if (priv->type != HX) {
usb_clear_halt(serial->dev, port->write_urb->pipe);
usb_clear_halt(serial->dev, port->read_urb->pipe);
}
buf = kmalloc(10, GFP_KERNEL);
if (buf==NULL)
return -ENOMEM;
#define FISH(a,b,c,d) \
result=usb_control_msg(serial->dev, usb_rcvctrlpipe(serial->dev,0), \
b, a, c, d, buf, 1, 100); \
dbg("0x%x:0x%x:0x%x:0x%x %d - %x",a,b,c,d,result,buf[0]);
#define SOUP(a,b,c,d) \
result=usb_control_msg(serial->dev, usb_sndctrlpipe(serial->dev,0), \
b, a, c, d, NULL, 0, 100); \
dbg("0x%x:0x%x:0x%x:0x%x %d",a,b,c,d,result);
FISH (VENDOR_READ_REQUEST_TYPE, VENDOR_READ_REQUEST, 0x8484, 0);
SOUP (VENDOR_WRITE_REQUEST_TYPE, VENDOR_WRITE_REQUEST, 0x0404, 0);
FISH (VENDOR_READ_REQUEST_TYPE, VENDOR_READ_REQUEST, 0x8484, 0);
FISH (VENDOR_READ_REQUEST_TYPE, VENDOR_READ_REQUEST, 0x8383, 0);
FISH (VENDOR_READ_REQUEST_TYPE, VENDOR_READ_REQUEST, 0x8484, 0);
SOUP (VENDOR_WRITE_REQUEST_TYPE, VENDOR_WRITE_REQUEST, 0x0404, 1);
FISH (VENDOR_READ_REQUEST_TYPE, VENDOR_READ_REQUEST, 0x8484, 0);
FISH (VENDOR_READ_REQUEST_TYPE, VENDOR_READ_REQUEST, 0x8383, 0);
SOUP (VENDOR_WRITE_REQUEST_TYPE, VENDOR_WRITE_REQUEST, 0, 1);
SOUP (VENDOR_WRITE_REQUEST_TYPE, VENDOR_WRITE_REQUEST, 1, 0);
if (priv->type == HX) {
/* HX chip */
SOUP (VENDOR_WRITE_REQUEST_TYPE, VENDOR_WRITE_REQUEST, 2, 0x44);
/* reset upstream data pipes */
SOUP (VENDOR_WRITE_REQUEST_TYPE, VENDOR_WRITE_REQUEST, 8, 0);
SOUP (VENDOR_WRITE_REQUEST_TYPE, VENDOR_WRITE_REQUEST, 9, 0);
} else {
SOUP (VENDOR_WRITE_REQUEST_TYPE, VENDOR_WRITE_REQUEST, 2, 0x24);
}
kfree(buf);
/* Setup termios */
if (port->tty) {
pl2303_set_termios (port, &tmp_termios);
}
//FIXME: need to assert RTS and DTR if CRTSCTS off
dbg("%s - submitting read urb", __FUNCTION__);
port->read_urb->dev = serial->dev;
result = usb_submit_urb (port->read_urb, GFP_KERNEL);
if (result) {
dev_err(&port->dev, "%s - failed submitting read urb, error %d\n", __FUNCTION__, result);
pl2303_close (port, NULL);
return -EPROTO;
}
dbg("%s - submitting interrupt urb", __FUNCTION__);
port->interrupt_in_urb->dev = serial->dev;
result = usb_submit_urb (port->interrupt_in_urb, GFP_KERNEL);
if (result) {
dev_err(&port->dev, "%s - failed submitting interrupt urb, error %d\n", __FUNCTION__, result);
pl2303_close (port, NULL);
return -EPROTO;
}
return 0;
}
pl2303_open
下面的宏定义比较关键:
#define FISH(a,b,c,d) \
result=usb_control_msg(serial->dev, usb_rcvctrlpipe(serial->dev,0), \
b, a, c, d, buf, 1, 100); \
dbg("0x%x:0x%x:0x%x:0x%x %d - %x",a,b,c,d,result,buf[0]);
#define SOUP(a,b,c,d) \
result=usb_control_msg(serial->dev, usb_sndctrlpipe(serial->dev,0), \
b, a, c, d, NULL, 0, 100);
usb_control_msg函数是用来向控制端口发送信息的,FISH可以理解为从USB设备读取数据、SOUP理解为向设备写数据。
对于USB设备来说,最关键的就是写配置数据,可以理解应用层要设置一些启动参数;由于笔者对许多功能都不感兴趣,忽略读信息即可。
整理后的数据如下:
SOUP (VENDOR_WRITE_REQUEST_TYPE, VENDOR_WRITE_REQUEST, 0x0404, 0);
SOUP (VENDOR_WRITE_REQUEST_TYPE, VENDOR_WRITE_REQUEST, 0x0404, 1);
SOUP (VENDOR_WRITE_REQUEST_TYPE, VENDOR_WRITE_REQUEST, 0, 1);
SOUP (VENDOR_WRITE_REQUEST_TYPE, VENDOR_WRITE_REQUEST, 1, 0);
SOUP (VENDOR_WRITE_REQUEST_TYPE, VENDOR_WRITE_REQUEST, 2, 0x44);
/* reset upstream data pipes */
SOUP (VENDOR_WRITE_REQUEST_TYPE, VENDOR_WRITE_REQUEST, 8, 0);
SOUP (VENDOR_WRITE_REQUEST_TYPE, VENDOR_WRITE_REQUEST, 9, 0);
这些数据将会被我们用来在windows下设置启动设备,如果读者对这些值感兴趣,可以尝试获取PL2303等我官方文档进行查询。
pl2303_set_termios
这个函数代码有点多,不过笔者仔细看了之后,总结了4处usb_control_msg函数调用:
// 读取波特率等信息
i = usb_control_msg (serial->dev, usb_rcvctrlpipe (serial->dev, 0),
GET_LINE_REQUEST, GET_LINE_REQUEST_TYPE,
0, 0, buf, 7, 100);
// 设置波特率等信息
i = usb_control_msg (serial->dev, usb_sndctrlpipe (serial->dev, 0),
SET_LINE_REQUEST, SET_LINE_REQUEST_TYPE,
0, 0, buf, 7, 100);
// 读取波特率等信息
i = usb_control_msg (serial->dev, usb_rcvctrlpipe (serial->dev, 0),
GET_LINE_REQUEST, GET_LINE_REQUEST_TYPE,
0, 0, buf, 7, 100);
// 启动传输?
i = usb_control_msg(serial->dev,
usb_sndctrlpipe(serial->dev, 0),
VENDOR_WRITE_REQUEST,
VENDOR_WRITE_REQUEST_TYPE,
0x0, index, NULL, 0, 100);
笔者注意到每次都是写入7个字节,故需要搞清楚这七个字节是怎么定义的,在函数中我们找到下面的代码和注释:
if (baud) {
buf[0] = baud & 0xff;
buf[1] = (baud >> 8) & 0xff;
buf[2] = (baud >> 16) & 0xff;
buf[3] = (baud >> 24) & 0xff;
}
/* For reference buf[4]=0 is 1 stop bits */
/* For reference buf[4]=1 is 1.5 stop bits */
/* For reference buf[4]=2 is 2 stop bits */
此处可以看出buf数组中的0~3就是一个32位的无符号整型,表示波特率;buf[4]是停止位;
接下来在下面的代码中发现剩余的位的作用:
switch (cflag & CSIZE) {
case CS5: buf[6] = 5; break;
case CS6: buf[6] = 6; break;
case CS7: buf[6] = 7; break;
default:
case CS8: buf[6] = 8; break;
}
/* For reference buf[5]=0 is none parity */
/* For reference buf[5]=1 is odd parity */
/* For reference buf[5]=2 is even parity */
/* For reference buf[5]=3 is mark parity */
/* For reference buf[5]=4 is space parity */
最终我们整理出下面的定义:
buff[0] = 0x00;
buff[1] = 0xC2;
buff[2] = 0x01;
buff[3] = 0x00;
buff[6] = 0x08;
0~3个字节其实就是115200的,这里需要大数端和小数端的知识。
数据上报
当整理出来数据之后,在Windows平台下的思路就非常简单了:
- 1. 提供和usb_control_msg的替代功能函数;
- 2. 根据总结的流程来设计初始化流程;
- 3. 找到如何获取数据的方式;
笔者并未在pl2303.c代码找到详细的上报流程,但笔者用了另外一种简单的方案来解决这个问题。
在window平台下使用USBView,我们可以将所有的USB描述符枚举出来:
这个软件即使没有装驱动,也能将USB描述符枚举出来,这里看设备描述符就发现,它只有一个配置和三个端点,三个端点如下所示:
中断端点先排除掉,一般中断断点传承几个字节就不错了,不会有厂家真的用它传输吧?只剩两个端点,所以使用哪个端点上报GPS数据就非常明显了,只有0x83可以用。