插上设备,实际测试看看效果,再打开设备管理器,发现人体学输入设备里只多出了一个人体学输入设备(图中另一个是我本来的USB鼠标),在键盘和鼠标里都多了一个HID类型的键盘和鼠标,再分别查看它们的PID和VID,发现都是一样的。
上两节我们实现了USB鼠标和USB键盘的实例,也许有人会问能不能用一套硬件同时实现USB鼠标和USB键盘的功能,答案是肯定的,而且这种应用场合是有的,比如带鼠标功能的多媒体键盘。
实现这种USB HID复合设备有两种方法,在《usbhid协议入门》一节已经讲到其中一种方法,说一个USB HID设备可以包含多种功能的报告描述符合集,这样可以实现复合设备,如带鼠标功能的USB键盘,这种复合键盘可以通过在报告描述里包含键盘和鼠标两种报告来实现,两个报告用报告ID来区分。这节我们就用这种方法来实现同时带鼠标和键盘功能的USB HID复合设备,有关另外一种方法的详细教程和实例可以参考本工作室推出的USB学习板。
既然可以用“在报告描述里包含键盘和鼠标两种报告来实现”,那么我们就把上两节的键盘和鼠标实例的报告描述符放在一起,再加上报告ID就是了,修改后的报告描述符如下:code char MouseReportDescriptor[119] = {
//-------------键盘部分报告描述符----------------
//表示用途页为通用桌面设备
0x05, 0x01, // USAGE_PAGE (Generic Desktop)
//表示用途为键盘
0x09, 0x06, // USAGE (Keyboard)
//表示应用集合,必须要以END_COLLECTION来结束它,见最后的END_COLLECTION
0xa1, 0x01, // COLLECTION (Application)
//报告ID(报告ID 0是保留的)
0x85, 0x01, //Report ID (1)
//表示用途页为按键
0x05, 0x07, // USAGE_PAGE (Keyboard)
//用途最小值,这里为左ctrl键
0x19, 0xe0, // USAGE_MINIMUM (Keyboard LeftControl)
//用途最大值,这里为右GUI键,即window键
0x29, 0xe7, // USAGE_MAXIMUM (Keyboard Right GUI)
//逻辑最小值为0
0x15, 0x00, // LOGICAL_MINIMUM (0)
//逻辑最大值为1
0x25, 0x01, // LOGICAL_MAXIMUM (1)
//报告大小(即这个字段的宽度)为1bit,所以前面的逻辑最小值为0,逻辑最大值为1
0x75, 0x01, // REPORT_SIZE (1)
//报告的个数为8,即总共有8个bits
0x95, 0x08, // REPORT_COUNT (8)
//输入用,变量,值,绝对值。像键盘这类一般报告绝对值,
//而鼠标移动这样的则报告相对值,表示鼠标移动多少
0x81, 0x02, // INPUT (Data,Var,Abs)
//上面这这几项描述了一个输入用的字段,总共为8个bits,每个bit表示一个按键
//分别从左ctrl键到右GUI键。这8个bits刚好构成一个字节,它位于报告的第一个字节。
//它的最低位,即bit-0对应着左ctrl键,如果返回的数据该位为1,则表示左ctrl键被按下,
//否则,左ctrl键没有按下。最高位,即bit-7表示右GUI键的按下情况。中间的几个位,
//需要根据HID协议中规定的用途页表(HID Usage Tables)来确定。这里通常用来表示
//特殊键,例如ctrl,shift,del键等
//这样的数据段个数为1
0x95, 0x01, // REPORT_COUNT (1)
//每个段长度为8bits
0x75, 0x08, // REPORT_SIZE (8)
//输入用,常量,值,绝对值
0x81, 0x03, // INPUT (Cnst,Var,Abs)
//上面这8个bit是常量,设备必须返回0
//这样的数据段个数为5
0x95, 0x05, // REPORT_COUNT (5)
//每个段大小为1bit
0x75, 0x01, // REPORT_SIZE (1)
//用途是LED,即用来控制键盘上的LED用的,因此下面会说明它是输出用
0x05, 0x08, // USAGE_PAGE (LEDs)
//用途最小值是Num Lock,即数字键锁定灯
0x19, 0x01, // USAGE_MINIMUM (Num Lock)
//用途最大值是Kana,这个是什么灯我也不清楚^_^
0x29, 0x05, // USAGE_MAXIMUM (Kana)
//如前面所说,这个字段是输出用的,用来控制LED。变量,值,绝对值。
//1表示灯亮,0表示灯灭
0x91, 0x02, // OUTPUT (Data,Var,Abs)
//这样的数据段个数为1
0x95, 0x01, // REPORT_COUNT (1)
//每个段大小为3bits
0x75, 0x03, // REPORT_SIZE (3)
//输出用,常量,值,绝对
0x91, 0x03, // OUTPUT (Cnst,Var,Abs)
//由于要按字节对齐,而前面控制LED的只用了5个bit,
//所以后面需要附加3个不用bit,设置为常量。
//报告个数为6
0x95, 0x06, // REPORT_COUNT (6)
//每个段大小为8bits
0x75, 0x08, // REPORT_SIZE (8)
//逻辑最小值0
0x15, 0x00, // LOGICAL_MINIMUM (0)
//逻辑最大值255
0x25, 0xFF, // LOGICAL_MAXIMUM (255)
//用途页为按键
0x05, 0x07, // USAGE_PAGE (Keyboard)
//使用最小值为0
0x19, 0x00, // USAGE_MINIMUM (Reserved (no event indicated))
//使用最大值为0x65
0x29, 0x65, // USAGE_MAXIMUM (Keyboard Application)
//输入用,变量,数组,绝对值
0x81, 0x00, // INPUT (Data,Ary,Abs)
//以上定义了6个8bit宽的数组,每个8bit(即一个字节)用来表示一个按键,所以可以同时
//有6个按键按下。没有按键按下时,全部返回0。如果按下的键太多,导致键盘扫描系统
//无法区分按键时,则全部返回0x01,即6个0x01。如果有一个键按下,则这6个字节中的第一
//个字节为相应的键值(具体的值参看HID Usage Tables),如果两个键按下,则第1、2两个
//字节分别为相应的键值,以次类推。
//关集合,跟上面的对应
0xc0 , // END_COLLECTION
//-----------------------鼠标部分报告描述符----------------------------
0x05, 0x01, // USAGE_PAGE (Generic Desktop)
0x09, 0x02, // USAGE (Mouse)
0xa1, 0x01, // COLLECTION (Application)
0x85, 0x02, // 报告ID (2)
0x09, 0x01, // USAGE (Pointer)
0xa1, 0x00, // COLLECTION (Physical)
0x05, 0x09, // USAGE_PAGE (Button)
0x19, 0x01, // USAGE_MINIMUM (Button 1)
0x29, 0x03, // USAGE_MAXIMUM (Button 3)
0x15, 0x00, // LOGICAL_MINIMUM (0)
0x25, 0x01, // LOGICAL_MAXIMUM (1)
0x95, 0x03, // REPORT_COUNT (3)
0x75, 0x01, // REPORT_SIZE (1)
0x81, 0x02, // INPUT (Data,Var,Abs)
0x95, 0x01, // REPORT_COUNT (1)
0x75, 0x05, // REPORT_SIZE (5)
0x81, 0x03, // INPUT (Cnst,Var,Abs)
0x05, 0x01, // USAGE_PAGE (Generic Desktop)
0x09, 0x30, // USAGE (X)
0x09, 0x31, // USAGE (Y)
0x09, 0x38, // USAGE (Wheel)
0x15, 0x81, // LOGICAL_MINIMUM (-127)
0x25, 0x7f, // LOGICAL_MAXIMUM (127)
0x75, 0x08, // REPORT_SIZE (8)
0x95, 0x03, // REPORT_COUNT (3)
0x81, 0x06, // INPUT (Data,Var,Rel)
0xc0, // END_COLLECTION
0xc0 // END_COLLECTION
};
这个报告描述符定义了两个报告输入报告(即数据包)和一个输出报告,两个输入报告中,一个是键盘,一个是鼠标,输出报告是用于指示LED状态的,两个输出报告所定义的数据包格式可以参考上两节内容。
由于电路板上只有两个按键,所有用K1模拟鼠标左移,K2 模拟键盘上的Windows图标键(按下K2后会弹出开始菜单),其代码如下:void main()
{
unsigned char i = 0;
signed char cKeyIn[9];
static bit bKey1Pressed = 0; //键按下标志,防止重入
static bit bKey2Pressed = 0; //键按下标志,防止重入
if (Init_D12()!=0) //初始化D12
return; //如果初始化不成功,返回
IT0 = 0; //外部中断0为电平触发方式
EX0 = 1; //开外部中断0
PX0 = 0; //设置外部中断0中断优先级
EA = 1; //开80C51总中断
P0 = 0;
while(1)
{
usbserve(); //处理USB事件
if(bEPPflags.bits.configuration)
{
//在这里添加端点操作代码
if(bEPPflags.bits.ep2_rxdone ) //主端点接收到数据(从主机发往设备的数据)
{
bEPPflags.bits.ep2_rxdone = 0;
//判断NumLock状态
if(EpBuf[1] & 0x01) //EpBuf为接收缓冲
{
P0 = 0x01;
}
else
{
P0 = 0x00;
}
}
K1 = 1; //P3.5
K2 = 1; //P3.6
for(i=0;i<100;i++); //延时
if(~K1 ) //K1按下(模拟鼠标左移)
{
cKeyIn[0]=2; //报告ID,第一个字节为报告ID(报告描述符里定义了鼠标ID为2)
cKeyIn[1]=0;
cKeyIn[2]=-1; //鼠标左移
cKeyIn[3]=0;
cKeyIn[4]=0;
D12_WriteEndpoint(5,5,cKeyIn); //发5个字节到PC机,第一个字节为报告ID(报告描述符里定义了鼠标ID为2)
}
if(~K2 & !bKey2Pressed) //K2按下(模拟左Windows键)
{
bKey2Pressed = 1; //防止重入
cKeyIn[0]=1; //报告ID,第一个字节为报告ID(报告描述符里定义了键盘ID为1)
cKeyIn[1]=0x08; //特殊键
cKeyIn[2]=0; //保留
cKeyIn[3]=0;
cKeyIn[4]=0;
cKeyIn[5]=0;
cKeyIn[6]=0;
cKeyIn[6]=0;
cKeyIn[8]=0;
D12_WriteEndpoint(5,9,cKeyIn); //发9个字节到PC机,第一个字节为报告ID(报告描述符里定义了键盘ID为1)
}
else if(K2 & bKey2Pressed) //K2弹起
{
bKey2Pressed = 0; //防止重入
cKeyIn[0]=1; //报告ID,第一个字节为报告ID(报告描述符里定义了键盘ID为1)
cKeyIn[1]=0;
cKeyIn[2]=0; //保留
cKeyIn[3]=0;
cKeyIn[4]=0;
cKeyIn[5]=0;
cKeyIn[6]=0;
cKeyIn[7]=0;
cKeyIn[8]=0;
D12_WriteEndpoint(5,9,cKeyIn); //发9个字节到PC机,第一个字节为报告ID(报告描述符里定义了键盘ID为1)
}
}
}
}
实例中为了演示方便,没有加入按键消抖功能,实际应用中应加上。
从实例中可以看出,所有输入输出数据包都在最低字节位置插入了一个报告ID,有用数据都从第二个字节开始。
论坛中还有另外一篇组合设备的使用
USB复合设备和USB组合设备的区别和实现代码分析
鸣谢: