Linux那些事儿 之 戏说USB(33)字符串描述符

关于字符串描述符,前面的前面已经简单描述过了,地位仅次于设备/配置/接口/端点四大描述符,那四大设备必须得支持,而字符串描述符对设备来说则是可选的。

这并不是就说字符串描述符不重要,对咱们来说,字符串要比数字亲切的多,提供字符串描述符的设备也要比没有提供的设备亲切的多,不会有人会专门去记前面使用lsusb列出的04b4表示的是Cypress Semiconductor Corp.。

一提到字符串,不可避免就得提到字符串使用的语言。字符串亲切是亲切,但不像数字那样全球通用,使用中文了,老外看不懂,使用法文阿拉伯文什么的咱又看不懂,你知道目前世界上有多少种语言吗?有得说七千多种,有得说五千多种,无一定论,不过使用人口超过100万的语言也足足有140多种。字符串描述符也需要应对多种语言的情况,当然这并不是说设备里就要存储这么多不同语言的字符串描述符,这未免要求过高了些,代价也忒昂贵了些,要知道这些描述符不会凭空生出来,是要存储在设备的EEPROM里的,此物是需要MONEY的,要节约MONEY,尽量少占用EEPROM。所以说只提供几种语言的字符串描述符就可以了,甚至说只提供一种语言,比如英语就可以了。

不过不管哪种语言,在PC里或者设备里存放都只能用二进制数字,这就需要在语言与数字之间进行编码,这个所谓的编码和这个世界上其它事物一样,都是有多种的,起码每种语言都会存在独立的编码方式,咱们的简体中文可以使用GB2312、GBK、GB18030等,台湾那边儿是繁体,用的就是big5,这么一来每种语言自己内部交流是不成问题了,可相互之间就像鸡同鸭讲了。于是世界上的某些角落就出现了那么一些有志青年,立志要将各种语言的编码体系给统一起来,于是就出现了UNICODE和ISO-10646。

Spec里就说了,字符串描述符使用的就是UNICODE编码,usb设备里的字符串可以通过它来支持多种语言,不过你需要在向设备请求字符串描述符的时候指定一个你期望看到的一种语言,俗称语言ID,即Language ID。这个语言ID使用两个字节表示,所有可以使用的语言ID在http://www.usb.org/developers/docs/USB_LANGIDs.pdf 文档里都有列出来,从这个文档里你也可以明白为啥要使用两个字节,而不是一个字节表示。这么说吧,比如中文是0X04,但是中文还有好几种,所以就需要另外一个字节表示是哪种中文,简体就是0X02,注意合起来表示简体中文并不是0X0402或者0X0204,因为这两个字节并不是分的那么清,bit0~9一共10位去表示Primary语言ID,其余6位去表示Sub语言ID,毕竟一种语言的Sub语言不可能会特别的多,没必要分去整整一半8bits,所以简体中文的语言ID就是0X0804。

不多罗唆,还是结合代码,从上节飘过的usb_cache_string说起,看看如何去获得一个字符串描述符,它在message.c里定义

char *usb_cache_string(struct usb_device *udev, int index)
{
	char *buf;
	char *smallbuf = NULL;
	int len;

	if (index <= 0)
		return NULL;

	buf = kmalloc(MAX_USB_STRING_SIZE, GFP_NOIO);
	if (buf) {
		len = usb_string(udev, index, buf, MAX_USB_STRING_SIZE);
		if (len > 0) {
			smallbuf = kmalloc(++len, GFP_NOIO);
			if (!smallbuf)
				return buf;
			memcpy(smallbuf, buf, len);
		}
		kfree(buf);
	}
	return smallbuf;
}
每个成年人都有那么一个身份证号码,每个字符串描述符都有一个序号,身份证号可能会重复,字符串描述符这个序号是不能重复的,不过这点不用你我操心,都是设备已经固化好了的东西,重复不重复也不是咱们要操心的事。咱们要操心的事太多了,要操心吃还要操心睡。


也好理解,什么东西一多了,最好最节约最省事的区分方式就是编号,字符串描述符当然可以有很多个,参数的index就是表示了你希望获得其中的第几个。但是不可疏忽大意的是,你不能指定index为0,0编号是有特殊用途的,你指定0了就什么也得不到。你去华为找工号000的,不会有人应你,根本就没这人,你找001,这次有人应你,不过是保安,赶你走的,没事儿找任老总干吗,没看一个接一个的自杀正是多事之秋么。

有关这个函数,还需要明白两点,第一是它采用的方针策略,就是苦活儿累活儿找个usb_string()去做,自己一边儿看着,这个usb_string()怎么工作的之后再看,现在只要注意下它的参数,比usb_cache_string()的参数多了两个,就是buf和size,也就是需要传递一个存放返回的字符串描述符的缓冲区。但是你调用usb_cache_string()的时候并没有指定一个明确的size,usb_cache_string()也就不知道你想要的那个字符串描述符有多大,于是它就采用了这么一个技巧,先申请一个足够大的缓冲区,这里是256字节,拿这个缓冲区去调用usb_string(),通过usb_string()的返回值会得到字符串描述符的真实大小,然后再拿这个真实的大小去申请一个缓冲区,并将大缓冲区里放的字符串描述符数据拷贝过来,这时那个大缓冲区当然就没什么利用价值了,于是再把它给释放掉。

第二就是申请那个小缓冲区的时候,使用的并不是usb_string()的返回值,而是多了1个字节,也就是说要从大缓冲区里多拷一个字节到小缓冲区里,为什么?这牵涉到C里字符串方面那个人见人愁鬼见鬼哭的代码杀手——字符串结束符。如果你说俺是危言耸听夸大其实,那只能说明你不是天才就是C代码写的少,咱不说C++,因为C++里更多的是用string。

字符串都需要那么一个结束符,这点是个正常人都知道的,但并不是每个正常人都能每时每刻的记得给字符串加上这么一个结束符。就像是个人都知道钞票不是万能的,但并不是每个人都知道:钞票不是万能的,有时还需要信用卡。可能你小心了1000次,但在第1001次的时候你给忘记了,你的代码就可能就可能挂了。

你从设备那里得到字符串之后得给它追加一个结束符。本来usb_string()里已经为buf追加好了,但是它返回的长度里还是没有包括进这个结束符的1个字节,所以usb_cache_string()为smallbuf申请内存的时候就得多准备那么一个字节,以便将buf里的那个结束符也给拷过来。现在就看看usb_string()的细节,定义在message.c里
int usb_string(struct usb_device *dev, int index, char *buf, size_t size)
{
	unsigned char *tbuf;
	int err;

	if (dev->state == USB_STATE_SUSPENDED)
		return -EHOSTUNREACH;
	if (size <= 0 || !buf || !index)
		return -EINVAL;
	buf[0] = 0;
	tbuf = kmalloc(256, GFP_NOIO);
	if (!tbuf)
		return -ENOMEM;

	err = usb_get_langid(dev, tbuf);
	if (err < 0)
		goto errout;

	err = usb_string_sub(dev, dev->string_langid, index, tbuf);
	if (err < 0)
		goto errout;

	size--;		/* leave room for trailing NULL char in output buffer */
	err = utf16s_to_utf8s((wchar_t *) &tbuf[2], (err - 2) / 2,
			UTF16_LITTLE_ENDIAN, buf, size);
	buf[err] = 0;

	if (tbuf[1] != USB_DT_STRING)
		dev_dbg(&dev->dev,
			"wrong descriptor type %02x for string %d (\"%s\")\n",
			tbuf[1], index, buf);

 errout:
	kfree(tbuf);
	return err;
}
6行,这几行做些例行检查,设备不能是挂起的,index也不能是0的,只要传递了指针就是需要检查的。

10行,初始化buf,usb_cache_string()并没有对这个buf初始化,所以这里必须要加上这么一步。当然usb_string()并不仅仅只有在usb_cache_string()里调用,可能会在很多地方调用到它,不过不管在哪里,这里谨慎起见,还是需要这么一步。

11行,申请一个256字节大小的缓冲区。前面一直强调说要初始化要初始化,怎么到这里俺就自己打自己一耳光,没有去初始化tbuf?这是因为没必要,为什么没必要,你看看usb_string()最后面的那一堆就明白了。

15行,使用指定的语言ID,或者前面获得的默认语言ID去获得想要的那个字符串描述符。现在看看定义在message.c里的usb_string_sub函数。
static int usb_string_sub(struct usb_device *dev, unsigned int langid,
			  unsigned int index, unsigned char *buf)
{
	int rc;

	/* Try to read the string descriptor by asking for the maximum
	 * possible number of bytes */
	if (dev->quirks & USB_QUIRK_STRING_FETCH_255)
		rc = -EIO;
	else
		rc = usb_get_string(dev, langid, index, buf, 255);

	/* If that failed try to read the descriptor length, then
	 * ask for just that many bytes */
	if (rc < 2) {
		rc = usb_get_string(dev, langid, index, buf, 2);
		if (rc == 2)
			rc = usb_get_string(dev, langid, index, buf, buf[0]);
	}

	if (rc >= 2) {
		if (!buf[0] && !buf[1])
			usb_try_string_workarounds(buf, &rc);

		/* There might be extra junk at the end of the descriptor */
		if (buf[0] < rc)
			rc = buf[0];

		rc = rc - (rc & 1); /* force a multiple of two */
	}

	if (rc < 2)
		rc = (rc < 0 ? rc : -EINVAL);

	return rc;
}
这个函数首先检查一下你的设备是不是属于那种有怪僻的,如果是一个没有毛病遵纪守法的合格设备,就调用usb_get_string()去帮着自己获得字符串描述符。USB_QUIRK_STRING_FETCH_255就是在include/linux/usb/quirks.h里定义的那些形形色色的毛病之一,《我是Hub》里详细的讲了设备的quirk,说了USB_QUIRK_STRING_FETCH_255就表示设备在获取字符串描述符的时候会crash。

usb_string_sub()的核心就是message.c里定义usb_get_string函数
static int usb_get_string(struct usb_device *dev, unsigned short langid,
			  unsigned char index, void *buf, int size)
{
	int i;
	int result;

	for (i = 0; i < 3; ++i) {
		/* retry on length 0 or stall; some devices are flakey */
		result = usb_control_msg(dev, usb_rcvctrlpipe(dev, 0),
			USB_REQ_GET_DESCRIPTOR, USB_DIR_IN,
			(USB_DT_STRING << 8) + index, langid, buf, size,
			USB_CTRL_GET_TIMEOUT);
		if (result == 0 || result == -EPIPE)
			continue;
		if (result > 1 && ((u8 *) buf)[1] != USB_DT_STRING) {
			result = -ENODATA;
			continue;
		}
		break;
	}
	return result;
}
我已经不记得这是第多少次遇到usb_control_msg()了,还是简单说一下它的一堆参数,wValue的高位字节表示描述符的类型,低位字节表示描述符的序号,所以有11行的(USB_DT_STRING << 8) + index,wIndex对于字符串描述符应该设置为使用语言的ID,所以有11的langid,至于wLength,就是描述符的长度,对于字符串描述符很难有一个统一的确定的长度,所以一般来说上头儿传递过来的通常是一个比较大的255字节。

和获得设备描述符时一样,因为一些厂商搞出的设备古灵精怪的,可能需要多试几次才能成功。要容许设备犯错误,就像人总要犯错误一样。

还是回过头去看usb_string_sub函数,如果usb_get_string()成功的得到了期待的字符串描述符,则返回获得的字节数,如果这个数目小于2,就再读两个字节试试,要想明白这两个字节是什么内容,需要看看spec Table 9-16

Table 9-15是0号字符串描述符的格式,这个Table 9-16是其它字符串描述符的格式,很明显可以看到,它的前两个字节分别表示了长度和类型,如果读2个字节成功的话,就可以准确的获得这个字符串描述符的长度,然后可以再拿这个准确的长度去请求一次。

该尝试的都尝试了,现在看看21行,分析一下前面调用usb_get_string()的结果,如果几次尝试之后,它的返回值还是小于2,那就返回一个错误码。如果你的辛苦没有白费,rc大于等于2,说明终于获得了一个有效的字符串描述符。

22行,buf的前两个字节有一个为空时,也就是Table 9-16的前两个字节有一个为空时,调用了message.c里定义的usb_try_string_workarounds函数
static void usb_try_string_workarounds(unsigned char *buf, int *length)
{
	int newlength, oldlength = *length;

	for (newlength = 2; newlength + 1 < oldlength; newlength += 2)
		if (!isprint(buf[newlength]) || buf[newlength + 1])
			break;

	if (newlength > 2) {
		buf[0] = newlength;
		*length = newlength;
	}
}
这个函数的目的是从usb_get_string()返回的数据里计算出前面有效的那一部分的长度。它的核心就是5行的那个for循环,不过要搞清楚这个循环,还真不是一件容易的事儿,得有相当的理论功底。

不久之前刚说了字符串描述符使用的是UNICODE编码,其实UNICODE指的是包含了字符集、编码、字型等等很多规范的一整套系统,字符集仅仅描述符系统中存在哪些字符,并进行分类,并不涉及如何用数字编码表示的问题。UNICODE使用的编码形式主要就是两种UTF,即UTF-8和UTF-16。使用usb_get_string()获得的字符串使用的是UTF-16编码规则,而且是little-endpoint的,每个字符都需要使用两个字节来表示。你看这个for循环里newlength每次加2,就是表示每次处理一个字符的,但是要弄明白怎么处理的,还需要知道这两个字节分别是什么东东,这就不得不提及ASCII、ISO-8859-1等几个名词儿。

ASCII是用来表示英文的一种编码规范,表示的最大字符数为256,每个字符占1个字节,但是英文字符没那么多,一般来说128个也就够了(最高位为0),这就已经完全包括了控制字符、数字、大小写字母,还有其它一些符号。对于法语、西班牙语和德语之类的西欧语言都使用叫做ISO-8859-1的东东,它扩展了ASCII码的最高位,来表示像n上带有一个波浪线(241),和u上带有两个点(252)这样的字符。而Unicode的低字节,也就是在0到255上同ISO-8859-1完全一样,它接着使用剩余的数字,256到65535,扩展到表示其它语言的字符。所以可以说ISO-8859-1就是Unicode的子集,如果Unicode的高字节为0,则它表示的字符就和ISO-8859-1完全一样了。

有上面的理论垫底儿,咱们再看看这个for循环,newlength从2开始,是因为前两个字节应该是表示长度和类型的,这里只逐个儿对上面Table 9-16里的bString中的每个字符做处理。还要知道usb_get_string()得到的结果是little-endpoint的,所以buf[newlength]和buf[newlength + 1]分别表示一个字符的低字节和高字节,那么isprint(buf[newlength]就是用来判断一下这个Unicode字符的低字节是不是可以print的,如果不是,就没必要再往下循环了,后边儿的字符也不再看了,然后就到了690行的if,将newlength赋给buf[0],即bLength。length指向的是usb_get_string()返回的原始数据的长度,692行使用for循环计算出的有效长度将它给修改了。isprint在include/linux/ctype.h里定义,你可以去看看,这里就不多说了。

这个for循环终止的条件有两个,另外一个就是buf[newlength + 1],也就是这个Unicode字符的高字节不为0,这时它不存在对应的ISO-8859-1形式,为什么加上这个判断?你接着看。

usb_string_sub()的26行,buf[0]表示的就是bLength的值,如果它小于usb_get_string()获得的数据长度,说明这些数据里存在一些垃圾,要把他们给揪出来排除掉。要知道这个rc是要做为真实有效的描述符长度返回的,所以这个时候需要将buf[0]赋给rc。

29行,每个Unicode字符需要使用两个字节来表示,所以rc必须为偶数,2的整数倍,如果为奇数,就得将最后那一个字节给抹掉,也就是将rc减1。咱们可以学习一下这里将一个数字转换为偶数时采用的技巧,(rc & 1)在rc为偶数时等于0,为奇数时等于1,再使用rc减去它,得到的就是一个偶数。

从21~30这几行,咱们应该看得出,在成功获得一个字符串描述符时,usb_string_sub()返回的是一个NULL-terminated字符串的长度,并没有涉及到结束符。牢记这一点,咱们回到usb_string函数的23行,先将size,也就是buf的大小减1,目的就是为结束符保留1个字节的位置。

26行,为buf追加一个结束符。咱们这节也就结束了。
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值