|
本帖最后由 cruelfox 于 2016-7-30 12:48 编辑
软件上的工作比硬件多得多。因为想改造成USB键盘,不得不把USB HID的实现稍微看懂一下。PS/2模式硬件上也是保留的,暂时我还没去写软件。
总结一下,USB HID键盘需要使用两种HID报告:一是从设备到主机的,按键状态的报告,8字节;二是主机到设备的,指示灯状态的报告,1字节。第一个报告我使用EP1(Endpoint 1, 端点1)来发送,中断传输;第二个报告就使用默认的EP0,控制传输。USB的描述符,可以从现有的USB键盘上修改而来。下面是用USBTreeView这个工具查看到的的我的这个键盘的描述符:
其中USB报告描述符我没完全看懂,copy了现成的(这个也没必要自己重新写嘛)
USB的中断ISR,bare metal哦
- void USB_IRQHandler(void)
- {
- if(USB->ISTR & USB_ISTR_CTR)
- {
- if((USB->ISTR & 0x0f)==0) // EP_ID==0
- {
- switch(ep0_state)
- {
- case 0: ep0_state |= 0x80;
- break;
- case 1: if(USB->EP0R & USB_EP_CTR_TX)
- {
- USB->EP0R = USB_EP_TYPE_CONTROL|USB_EP_STAT_RX0;
- ep0_state=3;
- return;
- }
- else
- ep0_state=0;
- break;
- case 2: if((USB->EP0R & USB_EP_SETUP)==0) // OUT packet
- ep0_state |= 0x80;
- else
- ep0_state=0;
- break;
- case 3: ep0_state=0;
- break;
- case 4: ep0_state=0;
- break;
- default:ep0_state=0;
- break;
- }
- USB->EP0R = USB_EP_TYPE_CONTROL; // just clear flag
- return;
- }
- else // EP_ID can be 1
- {
- USB->EP1R = USB_EP_TYPE_INTERRUPT|1; // just clear flag
- ep1_wait=0;
- return;
- }
- }
- if(USB->ISTR & USB_ISTR_PMAOVR)
- {
- USB->ISTR = ~USB_ISTR_PMAOVR;
- }
- if(USB->ISTR & USB_ISTR_ERR)
- {
- USB->ISTR = ~USB_ISTR_ERR; // write 0 to clear
- }
- if(USB->ISTR & USB_ISTR_WKUP)
- {
- USB->CNTR &= ~USB_CNTR_FSUSP; // no suspend
- USB->ISTR = ~USB_ISTR_WKUP; // write 0 to clear
- }
- if(USB->ISTR & USB_ISTR_SUSP)
- {
- USB->CNTR |= USB_CNTR_FSUSP; // force suspend
- USB->CNTR |= USB_CNTR_LPMODE; // low power
- USB->ISTR = ~USB_ISTR_SUSP; // write 0 to clear
- }
- if(USB->ISTR & USB_ISTR_RESET)
- {
- USB->BTABLE = 0; // buffer table at bottom of PMA
- USB_PMA[0]=128; //ADDR0_TX
- USB_PMA[1]=0; //COUNT0_TX
- USB_PMA[2]=256; //ADDR0_RX
- USB_PMA[3]=0x8000|(3<<10); //RX buffer 128 bytes
- USB->EP0R = USB_EP_TYPE_CONTROL|USB_EP_STAT_RX1;
- ep0_state=0;
- USB_PMA[4]=384; //ADDR1_TX
- USB_PMA[5]=0; //COUNT1_TX
- USB->EP1R = USB_EP_TYPE_INTERRUPT|1; // EP1 type
- ep1_wait=0;
- USB->DADDR = USB_DADDR_EF; // enable function
- USB->ISTR = ~USB_ISTR_RESET; // write 0 to clear
- }
- if(USB->ISTR & USB_ISTR_SOF)
- {
- USB->ISTR = ~USB_ISTR_SOF; // write 0 to clear
- }
- }
复制代码 因为只有两个EP需要管,数据量也很小,STM32F0的PMA(Packet Memory Area)固定分配好就不动了,读写数据都直接在PMA上读写。在USB Reset中断的时候,把PMA和EP都重新初始化。
主程序中用一个无限循环,每次中断过后处理一下USB请求,以及来自键盘扫描的检测。
- while(1)
- {
- static char row=0;
- __WFI();
- if(ep0_state & 0x80) // request data processing
- {
- if(ep0_state==0x80) // SETUP phase
- {
- if(!setup_packet_service())
- {
- ep0_state=0;
- // not supported
- }
- // ep0_state should be set to 1 or 2, if processed
- }
- else // OUT phase
- {
- // process data
- show_LED(*(uint8_t *)(USB_PMA+128));
- ep0_state=4;
- USB_PMA[1]=0; // Zero length DATA0
- USB->EP0R = USB_EP_TYPE_CONTROL|USB_EP_STAT_TX0;
- }
- }
- else
- {
- if(usb_address && ep0_state==0)
- {
- USB->DADDR = USB_DADDR_EF|usb_address;
- usb_address=0;
- }
- }
- if(row!=scan_row) // new scan line
- {
- if(key_state[row]!=prev_key_state[row])
- {
- uint8_t test=0x80;
- uint8_t diff=key_state[row]^prev_key_state[row];
- for(i=0;i<8;i++)
- {
- if(diff & test)
- update_key_matrix(row,i,key_state[row]&test);
- test>>=1;
- }
- }
- row=scan_row;
- }
- }
复制代码
EP0的控制传输,把用到的请求处理一下
- char setup_packet_service(void)
- {
- if(ep0_std_req->bmRequestType & 0x20) // class-specific
- {
- switch(ep0_std_req->bRequest)
- {
- case REQ_GET_REPORT: break;
- case REQ_GET_IDLE:
- USB_PMA[64]=0xfa; // return 1 byte
- USB_PMA[1]=1;
- USB->EP0R = USB_EP_TYPE_CONTROL|USB_EP_STAT_TX0;
- ep0_state=1;
- return 1;
- break;
- case REQ_SET_REPORT:
- USB->EP0R = USB_EP_TYPE_CONTROL|USB_EP_STAT_RX0;
- ep0_state=2;
- return 1;
- break;
- case REQ_SET_IDLE:
- USB_PMA[1]=0; // Zero DATA
- USB->EP0R = USB_EP_TYPE_CONTROL|USB_EP_STAT_TX0;
- ep0_state=4;
- return 1;
- break;
- }
- return 0;
- }
- else // standard
- {
- switch(ep0_std_req->bRequest)
- {
- case REQ_GET_DESCRIPTOR:
- return descriptor_service();
- break;
- case REQ_SET_ADDRESS:
- if(ep0_std_req->bmRequestType!=0x00)
- return 0;
- usb_address=ep0_std_req->wValue;
- USB->EP0R = USB_EP_TYPE_CONTROL|USB_EP_STAT_TX0;
- USB_PMA[1]=0; // Zero length DATA0
- ep0_state=4; // No Data phase
- return 1;
- case REQ_SET_CONFIGURATION:
- if(ep0_std_req->bmRequestType!=0x00)
- return 0;
- USB->EP1R = USB_EP_TYPE_INTERRUPT|USB_EP_STAT_TX1|1;
- USB->EP0R = USB_EP_TYPE_CONTROL|USB_EP_STAT_TX0;
- USB_PMA[1]=0; // Zero DATA
- ep0_state=4; // No DATA phase
- return 1;
- default: return 0;
- }
- }
- }
复制代码
各种描述符,是USB开发首先要处理的
- char descriptor_service(void)
- {
- switch((ep0_std_req->wValue)>>8)
- {
- case DESC_TYPE_DEVICE:
- return ep0_preparedata(&DevDesc, sizeof(DevDesc));
- break;
- case DESC_TYPE_CONFIG:
- if(sizeof(ConfigDescData)>ep0_std_req->wLength)
- return ep0_preparedata(&ConfigDescData, ep0_std_req->wLength);
- else
- return ep0_preparedata(&ConfigDescData, sizeof(ConfigDescData));
- break;
- case DESC_TYPE_STRING:
- switch(ep0_std_req->wValue &0xff)
- {
- case 0 : return ep0_preparedata(&LangIDDesc, sizeof(LangIDDesc));
- case 1 : return ep0_preparedata(&UnicodeVendorDesc, sizeof(UnicodeVendorDesc));
- case 2 : return ep0_preparedata(&UnicodeProdDesc, sizeof(UnicodeProdDesc));
- default: return 0;
- }
- break;
- case REPORT_DESC_TYPE:
- return ep0_preparedata(&HidReportDesc, sizeof(HidReportDesc));
- default:
- return 0;
- }
- }
复制代码
下面说下我对键盘的处理。因为有14+8条线,其中8条一组我称为列,用PA0~7读取;另外14条我称为行,在一个时刻只有1条为低电平(输出),其它13条为高阻。在列扫描线上加上上拉,因此没有键按下的时候,PA0~7都是高电平的。若某键被按下,当在所在的行被扫描时,对应的列就会变成低。我用了Timer6中断,每0.5ms切换一次扫描线,这样扫描一遍矩阵键盘用7ms.
- void TIM6_DAC_IRQHandler(void)
- {
- __IO uint8_t *PA_IDR = (uint8_t *)&(GPIOA->IDR);
- TIM6->SR &= ~TIM_SR_UIF;
- prev_key_state[scan_row]=key_state[scan_row];
- key_state[scan_row]= *PA_IDR; // update key states
- switch(scan_row)
- {
- case 13: // next row PB14
- GPIOC->MODER = GPIOC_DEFAULT;
- GPIOB->MODER = GPIOB_DEFAULT|GPIO_MODER_MODER14_0;
- break;
- case 0: // next row PB15
- GPIOB->MODER = GPIOB_DEFAULT|GPIO_MODER_MODER15_0;
- break;
- case 1: // next row PB3
- GPIOB->MODER = GPIOB_DEFAULT|GPIO_MODER_MODER3_0;
- break;
- case 2: // next row PB4
- GPIOB->MODER = GPIOB_DEFAULT|GPIO_MODER_MODER4_0;
- break;
- case 3: // next row PB5
- GPIOB->MODER = GPIOB_DEFAULT|GPIO_MODER_MODER5_0;
- break;
- case 4: // next row PB6
- GPIOB->MODER = GPIOB_DEFAULT|GPIO_MODER_MODER6_0;
- break;
- case 5: // next row PB7
- GPIOB->MODER = GPIOB_DEFAULT|GPIO_MODER_MODER7_0;
- break;
- case 6: // next row PB8
- GPIOB->MODER = GPIOB_DEFAULT|GPIO_MODER_MODER8_0;
- break;
- case 7: // next row PB9
- GPIOB->MODER = GPIOB_DEFAULT|GPIO_MODER_MODER9_0;
- break;
- case 8: // next row PA8
- GPIOB->MODER = GPIOB_DEFAULT;
- GPIOA->MODER = GPIOA_DEFAULT|GPIO_MODER_MODER8_0;
- break;
- case 9: // next row PA9
- GPIOA->MODER = GPIOA_DEFAULT|GPIO_MODER_MODER9_0;
- break;
- case 10: // next row PA10
- GPIOA->MODER = GPIOA_DEFAULT|GPIO_MODER_MODER10_0;
- break;
- case 11: // next row PA15
- GPIOA->MODER = GPIOA_DEFAULT|GPIO_MODER_MODER15_0;
- break;
- case 12: // next row PC13
- GPIOA->MODER = GPIOA_DEFAULT;
- GPIOC->MODER = GPIOC_DEFAULT|GPIO_MODER_MODER13_0;
- break;
- }
- if(scan_row<13)
- scan_row++;
- else
- scan_row=0;
- }
复制代码
扫描的状态被Timer 6 ISR记录在 key_state[] 数组当中,上一次的状态保存为 prev_key_state[]. 这样在主程序中只要比较这两个数组,就知道是否键盘的状态有变化了。有变化(按下或者抬起)的时候,再调用 update_key_matrix() 函数去生成USB HID报告。
要从键盘的扫描行、列坐标得到按键的编码(此处不是ASCII,也不是PS/2的键盘扫描码,而是USB HID定义的键盘Usage ID),需要用到查找表。我需要切换Dvorak和QWERTY两个布局,因此需要准备两个查找表:
- const char hid_keymap_qwerty[14][8]={
- {HK_RShift, HK_NONE, HK_NONE, HK_A, HK_R, HK_7, HK_F9, HK_Esc},
- {HK_F12, HK_BS, HK_Up, HKR_Slash, HKR_Aster, HK_NONE, HKR_Enter, HK_NONE},
- {HK_Period, HK_V, HK_BSlash, HK_J, HK_P, HK_Q, HK_4, HK_F6},
- {HK_Comma, HK_C, HK_Quote, HK_H, HK_O, HK_Equal, HK_3, HK_F5},
- {HK_M, HK_X, HK_Colon, HK_G, HK_I, HK_Minus, HK_2, HK_F4},
- {HK_LShift, HK_NONE, HK_Caps, HK_F, HK_U, HK_0, HK_1, HK_F3},
- {HK_Scroll, HK_LAlt, HK_RCtrl, HK_D, HK_Y, HK_9, HK_BQuote, HK_F2},
- {HK_NONE, HK_RAlt, HK_LCtrl, HK_S, HK_T, HK_8, HK_Enter, HK_F1},
- {HK_PgUp, HK_Home, HK_Insert, HKR_5, HKR_6, HKR_4, HK_F10, HKR_Dot},
- {HK_Right, HK_Down, HK_Left, HKR_Num, HKR_Plus, HK_NONE, HK_NONE, HK_NONE},
- {HK_PgDn, HK_End, HK_F11, HKR_9, HKR_Minus, HKR_8, HKR_7, HK_NONE},
- {HK_Space, HK_N, HK_Z, HK_L, HK_RBr, HK_E, HK_6, HK_F8},
- {HK_Slash, HK_B, HK_Delete, HK_K, HK_LBr, HK_W, HK_5, HK_F7},
- {HK_Pause, HK_Tab, HK_PrtScr, HKR_2, HKR_3, HKR_1, HKR_0, HK_MODE}
- };
- const char hid_keymap_dvorak[14][8]={
- {HK_RShift, HK_NONE, HK_NONE, HK_A, HK_P, HK_7, HK_F9, HK_Esc},
- {HK_F12, HK_BS, HK_Up, HKR_Slash, HKR_Aster, HK_NONE, HKR_Enter, HK_NONE},
- {HK_V, HK_K, HK_BSlash, HK_H, HK_L, HK_Quote, HK_4, HK_F6},
- {HK_W, HK_J, HK_Minus, HK_D, HK_R, HK_RBr, HK_3, HK_F5},
- {HK_M, HK_Q, HK_S, HK_I, HK_C, HK_LBr, HK_2, HK_F4},
- {HK_LShift, HK_NONE, HK_Caps, HK_U, HK_G, HK_0, HK_1, HK_F3},
- {HK_Scroll, HK_LAlt, HK_RCtrl, HK_E, HK_F, HK_9, HK_BQuote, HK_F2},
- {HK_NONE, HK_RAlt, HK_LCtrl, HK_O, HK_Y, HK_8, HK_Enter, HK_F1},
- {HK_PgUp, HK_Home, HK_Insert, HKR_5, HKR_6, HKR_4, HK_F10, HKR_Dot},
- {HK_Right, HK_Down, HK_Left, HKR_Num, HKR_Plus, HK_NONE, HK_NONE, HK_NONE},
- {HK_PgDn, HK_End, HK_F11, HKR_9, HKR_Minus, HKR_8, HKR_7, HK_NONE},
- {HK_Space, HK_B, HK_Colon, HK_N, HK_Equal, HK_Period, HK_6, HK_F8},
- {HK_Z, HK_X, HK_Delete, HK_T, HK_Slash, HK_Comma, HK_5, HK_F7},
- {HK_Pause, HK_Tab, HK_PrtScr, HKR_2, HKR_3, HKR_1, HKR_0, HK_MODE}
- };
复制代码 上面的宏定义另写在 translate.h 头文件中。HK_NONE是没有实际按键的位置,HK_MODE是我另外加的一个键,用来切换两个键盘布局。这个“一键切换”的键加装,可以用原来键盘上的AT/XT开关,也可以用我预留的User I/O,做到后来发现用键盘矩阵的空闲位置更方便。
HID报告的8个字节,第一个是8个特殊键的状态(Shift, Alt, Ctrl, GUI),第二个保留,后6个每个非0值是一个按下的键的Usage ID. 产生HID报告的子程序:
- void update_key_matrix(char row, char col, char onoff)
- {
- static uint16_t hid_report[4]={0,0,0,0};
- static char (*hid_keymap)[8]=hid_keymap_dvorak;
- unsigned char key=hid_keymap[row][col];
- unsigned char *report =(unsigned char *)hid_report;
- char i;
- if(key==HK_MODE)
- {
- if(!onoff)
- {
- if(hid_keymap==hid_keymap_dvorak)
- {
- hid_keymap=hid_keymap_qwerty;
- GPIOB->BSRR = (1<<2);
- }
- else
- {
- hid_keymap=hid_keymap_dvorak;
- GPIOB->BRR = (1<<2);
- }
- }
- return;
- }
- if(key>=0x80) // Alt, Ctrl, Shift
- {
- uint8_t bitset = 1<<(key&7);
- if(onoff) // non-zero is key up
- report[0] &= (~bitset);
- else
- report[0] |= bitset;
- }
- else
- {
- if(onoff) // non-zero is key up
- {
- for(i=2;i<8;i++)
- {
- if(report[i]==key)
- {
- report[i]=0;
- break;
- }
- }
- }
- else
- {
- for(i=2;i<8;i++)
- {
- if(report[i]==key)
- break;
- if(report[i]==0)
- {
- report[i]=key;
- break;
- }
- }
- }
- }
- for(i=0;i<4;i++)
- USB_PMA[192+i]=hid_report[i];
- USB_PMA[5]=8; //COUNT1_TX
- if(ep1_wait==0)
- {
- USB->EP1R = USB_EP_TYPE_INTERRUPT|USB_EP_STAT_TX0|1;
- ep1_wait=1;
- }
- }
复制代码
完整的程序在附件中。我这个程序没有使用任何USB的库函数,完全是从底层操作,这样对USB的工作细节可以了解得比较清楚,当然,也费了很多工夫啦。
|
|