关于字符串描述符的地位仅次于设备/配置/接口/端点四大描述符,那四大设备必须得支持,而字符串描述符对设备来说则是可选的,这并不是就说字符串描述符不重要,对咱们来说,提供字符串描述符的设备要比没有提供的设备亲切的多,不会有人会专门去记前面使用lsusb列出的04b4表示的是Cypress Semiconductor Corp。
一提到字符串,不可避免就得提到字符串使用的语言。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里定义
每个字符串描述符都有一个序号(index),字符串描述符这个序号是不能重复的,不过这点不用你我操心,都是设备已经固化好了的东西,重复不重复也不是咱们要操心的事。
参数的index就是表示了你希望获得其中的第几个。但是不可疏忽大意的是,你不能指定index为0,0编号是有特殊用途的,你指定0了就什么也得不到。
有关这个函数,还需要明白两点,第一是它采用的方针策略,就是苦活儿累活儿找个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里字符串方面那个人见人愁鬼见鬼哭的代码杀手——字符串结束符。字符串都需要那么一个结束符,这点是个正常人都知道的,但并不是每个正常人都能每时每刻的记得给字符串加上这么一个结束符。原因是spec说字符串描述符不是一个NULL-terminated字符串,意思也就是字符串描述符没有一个结束符,你从设备那里得到字符串之后得给它追加一个结束符。本来usb_string()里已经为buf追加好了,但是它返回的长度里还是没有包括进这个结束符的1个字节,所以usb_cache_string()为smallbuf申请内存的时候就得多准备那么一个字节,以便将buf里的那个结束符也给拷过来。现在就看看usb_string()的细节,定义在message.c里。
首先开始的几行做些例行检查,设备不能是挂起的,index也不能是0的,只要传递了指针就是需要检查的。接着初始化buf,因为usb_cache_string()并没有对这个buf初始化,所以这里必须要加上这么一步。当然usb_string()并不仅仅只有在usb_cache_string()里调用,可能会在很多地方调用到它,不过不管在哪里,这里谨慎起见,还是需要这么一步。
申请一个256字节大小的缓冲区。前面一直强调说要初始化要初始化,怎么到这里俺就自己打自己一耳光,没有去初始化tbuf?这是因为没必要,为什么没必要,你看看usb_string()最后面的那一堆就明白了。
struct usb_device里有have_langid和string_langid这么两个字段是和字符串描述符有关的,string_langid用来指定使用哪种语言,have_langid用来指定string_langid是否有效。如果have_langid为空,就说明没有指定使用哪种语言,那么获得的字符串描述符使用的是哪种语言就完全看设备的脸色和心情了。你可能会疑惑为什么当have_langid为空的时候,要调用两次usb_string_sub()?那问题就变成为什么have_langid为空的时候,要获取两遍的字符串描述符?你可以比较一下两次调用usb_string_sub()的参数有什么区别,第一次调用的时候,语言ID和index都为0,第二次调用的时候就明确的指定了语言ID和index。这里的玄机就在index为0的时候,也就是0编号的字符串描述符是什么东东,前面只说了它有特殊的用途这里必须得解释一下。有问题就要找协议,spec 9.6.7里说了,编号0的字符串描述符包括了设备所支持的所有语言ID,对应的就是Table 9-15
第一次调用usb_string_sub()就是为了获得这张表,获得这张表做嘛用?你接着往下看代码。如果usb_string_sub()返回个负数,就表示没有获得这张表,没有取到0号字符串描述符。如果返回值比4要小,就表示获得的表里没有包含任何一个语言ID。要知道一个语言ID占用2个字节,还有前两个字节表示表的长度还有类型,所以得到的数据至少要为4,才能够得到一个有效的语言ID。如果返回值比4要大,就使用获得的数据的第3,4两个字节设置string_langid,同时设置have_langid为1。这一堆if判断代码就是在你没有指定使用哪种语言的时候,去获取设备里默认使用的语言,也就是0号字符串描述符里的第一个语言ID所指定的语言。如果没有找到这个默认的语言ID,即usb_string_sub()返回值小于4的情况,就没有办法再去获得其它的字符串描述符了,因为没有指定语言啊,设备不知道你是要英语还是中文还是别的
第二次调用usb_string_sub()是使用指定的语言ID,或者前面获得的默认语言ID去获得想要的那个字符串描述符。usb_string_sub()函数处理大概过程就是首先检查一下你的设备是不是属于那种有怪僻的,如果是一个没有毛病遵纪守法的合格设备,就调用usb_get_string()去帮着自己获得字符串描述符。USB_QUIRK_STRING_FETCH_255就是在include/linux/usb/quirks.h里定义的那些形形色色的毛病之一。usb_string_sub()核心就是message.c里定义的usb_get_string函数。
我已经不记得这是第多少次遇到usb_control_msg()了,老习惯,还是简单说一下它的一堆参数,wValue的高位字节表示描述符的类型,低位字节表示描述符的序号,所以 传递参数(USB_DT_STRING << 8) + index,wIndex对于字符串描述符应该设置为使用语言的ID,所以传递参数langid,至于wLength,就是描述符的长度,对于字符串描述符很难有一个统一的确定的长度,所以一般来说上头儿传递过来的通常是一个比较大的255字节。和获得设备描述符时一样,因为一些厂商搞出的设备古灵精怪的,可能需要多试几次才能成功。
还是回过头去看usb_string_sub函数,如果usb_get_string()成功的得到了期待的字符串描述符,则返回获得的字节数,如果这个数目小于2,就再读两个字节试试,要想明白这两个字节是什么内容,需要看看spec Table 9-16
Table 9-15是0号字符串描述符的格式,这个Table 9-16是其它字符串描述符的格式,很明显可以看到,它的前两个字节分别表示了长度和类型,如果读2个字节成功的话,就可以准确的获得这个字符串描述符的长度,然后可以再拿这个准确的长度去请求一次。
然后分析一下前面调用usb_get_string()的结果,如果它的返回值还是小于2,那就返回一个错误码。相反判断rc大于等于2,说明终于获得了一个有效的字符串描述符。buf的前两个字节有一个为空时,也就是Table 9-16的前两个字节有一个为空时,调用了message.c里定义的usb_try_string_workarounds函数:
这个函数的目的是从usb_get_string()返回的数据里计算出前面有效的那一部分的长度。它的核心就是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的,如果不是,就没必要再往下循环了,后边儿的字符也不再看了。否则就到了if判断,将newlength赋给buf[0],即bLength。length指向的是usb_get_string()返回的原始数据的长度,使用for循环计算出的有效长度将它给修改了。这个for循环终止的条件有两个,另外一个就是buf[newlength + 1],也就是这个Unicode字符的高字节不为0,这时它不存在对应的ISO-8859-1形式,为什么加上这个判断?
回到usb_string_sub()函数,紧接在usb_try_string_workarounds函数调用之后判断buf[0]是否小于rc,buf[0]表示的就是bLength的值,如果它小于usb_get_string()获得的数据长度,说明这些数据里存在一些垃圾。要知道这个rc是要做为真实有效的描述符长度返回的,所以这个时候需要将buf[0]赋给rc。另外,每个Unicode字符需要使用两个字节来表示,所以rc必须为偶数,2的整数倍,如果为奇数,就得将最后那一个字节给抹掉,也就是将rc减1。咱们可以学习一下这里将一个数字转换为偶数时采用的技巧,(rc & 1)在rc为偶数时等于0,为奇数时等于1,再使用rc减去它,得到的就是一个偶数。
总结usb_string_sub()函数的下面几行:
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 */
}
咱们应该看得出,在成功获得一个字符串描述符时,usb_string_sub()返回的是一个NULL-terminated字符串的长度,并没有涉及到结束符。牢记这一点,咱们回到usb_string函数。
size--先将size,也就是buf的大小减1,目的就是为结束符保留1个字节的位置。
tbuf里保存的是GET_DESCRIPTOR请求返回的原始数据,err是usb_string_sub()的返回值,一切顺利的话,它表示的就是一个有效的描述符的大小。这里idx的初始值为0,而u的初始值为2,目的是从tbuf的第三个字节开始拷贝给buf,毕竟咱们的目的是获得字符串描述符里的那个bString,而不是整个描述符的内容。u每次都是增加2,这是因为采用的UTF-16是用两个字节表示一个字符的,所以循环一次要处理两个字节。循环里的if-else组合你可能比较糊涂,要搞清楚,还要蓦然回首看一下前面刚普及过的一些理论。tbuf里每个Unicode字符两个字节,又是little-endpoint的,所以判断这个Unicode字符的高位字节是不是为0,如果不为0,则ISO-8859-1里没有这个字符,就设置buf里的对应字符为‘?’。如果它的高位字节为0,就说明这个Unicode字符ISO-8859-1里也有,就直接将它的低位字节赋给buf。这么一个for循环下来,就将tbuf里的Unicode字符串转化成了buf里的ISO-8859-1字符串。
最后为buf追加一个结束符。咱们这节也就结束了。