STM32 USB HID键盘例程
0 _. G$ ~- J- O; k2 H, Z* O- G- B2 W; |- Z1 q
最全USB HID开发资料,悉心整理一个月,亲自测试- q* r0 S7 g9 A( Y' j A
5 b" D' [2 ~ x通过STM32CUbeMX建立USB HID的双向通讯实验成功. Z$ w1 u3 G# M
4 L- b# f# T% W1 r+ w
5 i" _7 j9 x. m3 T; p
发现很多人对STM32的USB通信很感兴趣。要将USB的通信协议搞懂确实是一个比较漫长的过程。但是USB的HID通信无论是上位机的设计还是STM32程序的编程都非常的简单。只是我想很多人都不知道而已。这篇文章的目的是让大家以最短的时间将USB加到你的设备中。如果想学得更深就靠大家。
5 H& F( A5 J9 W0 s' \3 s. N- ]: e& j0 k6 h) n
HID只是适合低速传输,其理论上可以达到64KB/S,但多由于windows系统和硬件的关系一般达不到这个传输数度。但这个速度对于一般系统的控制和数据传输都已经足够了,而且是免驱,省去了很多麻烦。如果您需要高速传输可参考我的另外一篇文章《STM32的USB例程修改步骤》文章在
8 ~+ D1 r5 [& N% |. M6 A- E& A8 k8 q3 k1 J6 g+ h7 V
一、安装完MDK后请打开C:/Keil/ARM/Examples/ST/STM32F10xUSBLib/Demos路径,将Custom_HID在同一个目录下复制一份,如果你要放到其他路径你需要在MDK Options for target的C/C++中添加USB的头文件路径(MDK下的/INC/ST/STM32F10x/USB)。( n) g$ \3 N- ]
: V- v) V' _" t; D二、打开usb_desc.c文件,该文件主要包含的端点描述符、设备描述符、配置描述符和字符描述符等。具体请大家参考其他资料了,这里主要说几个常用。" \& {8 m2 m* k J4 \5 j0 R
; c6 y4 k0 U, o iu8 DeviceDescriptor[SIZ_DEVICE_DESC]为USB设备描述符。当中的
/ H& t7 w2 G: }$ t0x83, /*idVendor (0x0483)*/
( m2 h/ M3 R* W2 n4 J7 s0x04,* d$ x, q" U! f2 G
0x50, /*idProduct = 0x5750*/- T$ p+ n2 o0 K9 J
0x57,
- F! [$ i! d! i2 a) }- f. B//idVender字段。厂商ID号,我们这里取0x0483,仅供实验用。/ _/ a6 s$ p; E7 q
//实际产品不能随便使用厂商ID号,必须跟USB协会申请厂商ID号。
+ r# ^3 K5 Q0 Y//注意小端模式,低字节在先。
- a7 T" W# b3 n' X2 y( o, z; y//idProduct字段。产品ID号,我们这里取0x5750。2 }2 \* U& E4 G2 o6 a4 ?
//注意小端模式,低字节应该在前。( y4 t: s! @* J
! |" T( {: [" L* g1 ]% u9 b& X
+ G: X9 i# f4 n$ S. m; yconst u8 ConfigDescriptor[SIZ_CONFIG_DESC]是配置描述符,注意如下3 w/ q, x( P) H! ^' ]8 i9 Q; a8 b
- USB_ENDPOINT_DESCRIPTOR_TYPE, /* bDescriptorType: */" Z1 w1 L9 R# B7 p' {. V( B
- 0x81, /* bEndpointAddress: Endpoint Address (IN) */
0 l/ Y' m, D( V2 A$ a/ j' d - 0x03, /* bmAttributes: Interrupt endpoint */ F% S+ r, s S0 c! g* |
- 0x02, /* wMaxPacketSize: 2 Bytes max */) W: L1 Y4 G; ]
- 0x00,- J& V2 Q$ l" f/ k9 {1 I" I
- 0x20, /* bInterval: Polling Interval (32 ms) */! P' r3 m$ v+ W) N0 |9 @3 S; g' s
- /* 34 */
1 d4 y1 p& |) u - 0x07, /* bLength: Endpoint Descriptor size */6 q8 [1 f0 D2 I& ~9 _
- USB_ENDPOINT_DESCRIPTOR_TYPE, /* bDescriptorType: */; Q& ~6 D. |# H# z# n" L5 j
- /* Endpoint descriptor type */
& g* X, L/ K3 M. I4 P. F - 0x01, /* bEndpointAddress: */
* ~. Q A3 h0 D: U( j. E - /* Endpoint Address (OUT) */
/ V+ K; G/ ?. G P$ H% ^# ] E$ n a - 0x03, /* bmAttributes: Interrupt endpoint */
. Y8 }+ M" c4 j6 `3 X7 F - 0x02, /* wMaxPacketSize: 2 Bytes max */
3 F' L) G4 x; B - 0x00,
+ Z& a2 q- T. j+ U - 0x20, /* bInterval: Polling Interval (20 ms) */
复制代码 3 F) j4 X# ?0 n) M# i
上面包含了“输入端点描述符”和“输出端点描述符”。
- d7 t( a9 u k% z# b9 C//wMaxPacketSize字段。该端点的最大包长。# e* F8 H% w+ m' G! T5 e
//bInterval字段。端点查询的时间,
]* Y/ x8 z) T2 _( F
8 V$ m' _2 _; f3 f1 s/ B为了实现更高速的通信我们修改如下:
8 N9 [) o3 j" K4 Z- \8 h7 j: b- /******************** Descriptor of endpoint ********************/& k- ^9 [4 {. j, n
- /* 27 */9 \- \0 H: q, |8 A) R* W
- 0x07, /*bLength: Endpoint Descriptor size*/
, c" O$ F$ r, K - USB_ENDPOINT_DESCRIPTOR_TYPE, /*bDescriptorType:*/$ ]9 p$ g2 B1 i+ k7 e/ p
- 0x81, /*bEndpointAddress: Endpoint Address (IN)*/% r: H! R c2 [
- 0x03, /*bmAttributes: Interrupt endpoint*/
& J, d9 t. H+ M y3 p' b - 0x40, /*wMaxPacketSize: 64 Byte max */0 o1 w4 P# I- s$ d
- 0x00,! S& |6 k/ {8 d# t1 u
- 0x0A, /*bInterval: Polling Interval (10 ms)*/
# l) X& D: E3 k2 C - /* 34 */
M% o! N; P& d4 c - /******************** Descriptor of endpoint ********************/
( b6 [1 E- Q' F6 A/ c! K& f5 `& | - /* 27 */
, q" J: Q a; `7 T9 M' x - 0x07, /*bLength: Endpoint Descriptor size*/. l& [7 Q# c* z$ W/ j# b. l5 [6 c
- USB_ENDPOINT_DESCRIPTOR_TYPE, /*bDescriptorType:*/( ~( m& K. P6 H2 I" s5 O6 e0 s8 J9 g
- 0x01, /*bEndpointAddress: Endpoint Address (OUT)*/; G/ w2 Z% K( B6 ~( S; I6 G% P
- 0x03, /*bmAttributes: Interrupt endpoint*/
2 ^6 M: ?. F! o9 s) J - 0x40, /*wMaxPacketSize: 64 Byte max */
- A, @1 a1 a1 P5 O7 r8 | - 0x00,* t! ^7 ]& o* g* d2 n% b2 }
- 0x0A, /*bInterval: Polling Interval (10 ms)*/
: Z# V% h% i/ k2 X' f
/ E% f' i: D1 v' a8 I5 z( a9 S- const u8 ReportDescriptor[SIZ_REPORT_DESC]为HID专用的报告描述符,具体的大家就参考资料了,这里可以直接复制了。
[' F8 Z$ L/ P( X( { - const u8 CustomHID_ReportDescriptor[CUSTOMHID_SIZ_REPORT_DESC] =1 v2 a! i0 e' z, X
- {
! B5 h ?' U2 g, _; f3 e - 0x05, 0xFF, // USAGE_PAGE(User define)+ J `; b a2 a, }1 u T) L7 m. y& u# X
- 0x09, 0xFF, // USAGE(User define)) B9 D1 `: N' o3 M1 W
- 0xa1, 0x01, // COLLECTION (Application)
\4 Q- v' ~- k% m0 |. V - 0x05, 0x01, // USAGE_PAGE(1)
5 z) t, @6 Z9 s j T) n - 0x19, 0x00, // USAGE_MINIMUM(0)
7 |, j1 d x5 Z - 0x29, 0xFF, // USAGE_MAXIMUM(255)
3 h0 Y2 u0 {3 n5 W: B5 i - 0x15, 0x00, // LOGICAL_MINIMUM (0)
- S. `$ b+ X0 i+ i. `! ~ - 0x25, 0xFF, // LOGICAL_MAXIMUM (255)
4 Y9 k. K% A7 U( Y3 A. G, @: g - 0x75, 0x08, // REPORT_SIZE (8); }/ F+ u! }2 C% j, K" C
- 0x95, 0x40, // REPORT_COUNT (64)& q _6 S: Q4 P. L/ V
- 0x81, 0x02, // INPUT (Data,Var,Abs)
' A# i( I( F+ K% q; e1 H - 0x05, 0x02, // USAGE_PAGE(2)& w; m5 c8 v8 X2 ] L: e
- 0x19, 0x00, // USAGE_MINIMUM (0)& K) q# |4 p e+ X- [
- 0x29, 0xFF, // USAGE_MAXIMUM (255)( E/ d. B- S" R6 X. h7 q
- 0x15, 0x00, // LOGICAL_MINIMUM (0)' v2 }# e/ d) K& V V: f. D
- 0x25, 0xFF, // LOGICAL_MAXIMUM (255)/ s; g8 ]; J& g% Y8 r B. y
- 0x95, 0x08, // REPORT_COUNT (8)
/ K7 i6 L7 g; {! ?0 } - 0x75, 0x40, // REPORT_SIZE (64)+ H6 |7 _2 I8 I
- 0x91, 0x02, // OUTPUT (Data,Var,Abs)4 r; }; t9 V# K. v* I( i* Q1 l" q
- 0xc0 // END_COLLECTION
& c/ ^3 k4 W2 ^1 e, t" _ - }; /* ReportDescriptor */3 h) [+ _) @; K8 t, c! x
- 1 \4 l7 D7 z( K8 M
- const u8 CustomHID_StringVendor[CUSTOMHID_SIZ_STRING_VENDOR]
7 L/ e5 q) d) I$ { - const u8 StringProduct[SIZ_STRING_PRODUCT]& g% {0 D% r" f: S- g0 J, b
- const u8 StringSerial[SIZ_STRING_SERIAL]
复制代码 * C. O4 ^) m7 I2 g: m+ @0 J
分别是“厂商字符”、“产品字符”、“产品序列号”,这些将在USB HID设备加载的时候显示。但是这需要这些字符要求为Unicode编码,你需要将你要显示的字符先转为Unicode编码。最好大家还要根据各个数组的长度修改如下定义。
* W6 Q+ R8 S" F; Y9 W# u
" t$ G1 t- b6 I: N" R8 t- #define CUSTOMHID_SIZ_REPORT_DESC 39
: a) e0 d$ \3 d" P7 E, l9 r" X - #define CUSTOMHID_SIZ_STRING_VENDOR 64# j C7 P+ k' O+ j6 T
- #define CUSTOMHID_SIZ_STRING_PRODUCT 28
! C; T- ?3 T0 C. z; o2 u - #define CUSTOMHID_SIZ_STRING_SERIAL 26
复制代码 2 f% p3 G! G! _1 f
三、打开hw_config.c文件,将那些没有的函数删除,只保留如下函数
$ }4 }; \4 l* b* `. ra) Set_System(void)7 Y, L3 s B' b, M- A
b) void Set_USBClock(void) + l4 B) {9 d1 ~+ o
c) void USB_Interrupts_Config(void)
% I; }. b0 f6 c% Nd) void USB_Cable_Config (FunctionalState NewState)
/ S. [6 L1 a/ } ^9 z" R- i7 R特别要注意最后一个函数,其主要作用是控制USB的上拉电阻,让电脑检测USB设备是否连接的。
, \# C( I% T1 `( [( X. E0 f5 O
/ P7 E! F& R8 t0 H9 ^四、打开stm32f10x_it.c文件,把EXTI15_10_IRQHandler等中断内的代码删除。
' k# I% |0 F6 q$ C打开usb_prop.c文件,修改如下:
5 A e! Q# v7 s( G9 n7 v- void CustomHID_Reset(void)& S' } I2 r6 A% \9 y, B
- { y6 [/ I3 ^' n! O3 U4 }! t
- /* Set Joystick_DEVICE as not configured */
2 ?9 T$ q% K R! x) Y+ b2 c; V - pInformation->Current_Configuration = 0;
. |" ~; h }& a' Z - pInformation->Current_Interface = 0;/*the default Interface*/( g1 P' V% I5 K8 Y5 `+ q( j% l
- SetBTABLE(BTABLE_ADDRESS);
( z4 S. P5 T6 ~/ S3 K
, ?- C5 c( U8 \: z! L- /* Initialize Endpoint 0 */% j& j2 x7 _- ^; f& F
- SetEPType(ENDP0, EP_CONTROL);
# y; ]2 E O( {$ j9 _ - SetEPTxStatus(ENDP0, EP_TX_STALL);9 }5 L" G U1 b" K* }" B
- SetEPRxAddr(ENDP0, ENDP0_RXADDR);
* T, W, b1 ~' H - SetEPTxAddr(ENDP0, ENDP0_TXADDR);
3 |( G. Z, d3 i7 s X, S8 @/ e - Clear_Status_Out(ENDP0);
% h+ x7 h7 Q+ l& A - SetEPRxCount(ENDP0, Device_Property.MaxPacketSize);
$ u g! f; ]' v# d0 R. d f: b/ C - SetEPRxValid(ENDP0);
: `' {* G+ R$ F. A
, x4 c% {6 ]; J T* b- /* Initialize Endpoint 1 */
! n; P- A, s( @. ^* S - SetEPType(ENDP1, EP_INTERRUPT);
5 q! f# V. H) G- u3 q - SetEPTxAddr(ENDP1, ENDP1_TXADDR);
( {$ b& E6 @6 M& o/ j4 r# i - SetEPTxCount(ENDP1, 64);+ |9 f; ?( w7 Y! J% E
- SetEPRxStatus(ENDP1, EP_RX_DIS);
' X. h- q1 e4 b! I T& r# k! X+ P - SetEPTxStatus(ENDP1, EP_TX_NAK);, M; C, T6 m9 H; r) O
! T' R% O8 z* @" y; b- /* Initialize Endpoint 1 */
' h1 w7 ?' [# T: B6 p - // SetEPType(ENDP1, EP_INTERRUPT);& i' T3 Q* z( B$ @& E& l9 k
- SetEPRxAddr(ENDP1, ENDP1_RXADDR);
& _& ]& W+ g2 x' O: Y - SetEPRxCount(ENDP1, 64);
2 }, n( d3 F* h- T& W! }) i - // SetEPTxStatus(ENDP1, EP_TX_DIS);' j( ~5 S; Y; Q+ Y) \" D
- SetEPRxStatus(ENDP1, EP_RX_VALID);
; `; e+ |4 m: m3 K6 v: f - /* Set this device to response on default address */- t/ r0 w- `1 t$ O
- SetDeviceAddress(0);
0 n2 v0 ^) Z8 F1 ?2 p( P1 } - }
复制代码 9 T0 A0 {9 U+ b1 i
五、usb_endp.c文件3 p) H; e8 \2 y; C8 R) J' M; x
- void EP1_OUT_Callback(void), a% |: W& b5 L B+ r" |
- {
5 X% K! q# [7 T; z# ~2 y - 这些写接收代码- w0 X3 @5 a: T: E
- }
复制代码
. I$ J/ T# i' U, X六、数据发送和接收,举例说明6 p9 }2 [. a6 c* K
1、数据接收
4 ?7 v/ n! \ P/ @3 M7 a- u8 DataLen;
- V5 q/ x/ T# P( s - DataLen = GetEPRxCount(ENDP1);
" P3 }& n P; c! o, K; A( C. T - PMAToUserBufferCopy(TX1_buffer, ENDP1_RXADDR, DataLen);
! M2 b! @! h1 E - SetEPRxValid(ENDP1);+ e5 ?+ \! v1 K8 Q5 e
- USART1_Send(DataLen);/ E6 ~2 j! o- _+ v6 {: W0 ^3 o! ~9 j* B
- count_out = 1;
复制代码 " d% u! R0 r( e, D
2、数据发送" m: u% U4 _* d [
- UserToPMABufferCopy(InBuffer, GetEPTxAddr(ENDP1), 64);& f5 z2 a5 Q4 Z, v5 x' j( r1 ?0 G
- SetEPTxCount(ENDP1, 64); : M+ x ]/ W) H1 b) w
- SetEPTxValid(ENDP1);
复制代码 / h! S% ~* ^" L# a) w0 |
如果你发送数据较为频繁,每次发送前应使用GetEPTxStatus(ENDP1)检测上次发送是否完成。如果端点状态处于EP_TX_VALID,说明发送未结束,如果端点状态处于EP_TX_NAK,说明发送结束。
$ J" g6 c8 `; k
6 ~3 E# l. X, E6 |5 H y/ W
! S! A/ k. s" G/ i(by xidongs)
, }' G. ~, z9 x8 l |