你的浏览器版本过低,可能导致网站不能正常访问!
为了你能正常使用网站功能,请使用这些浏览器。

【经验分享】STM32 USB-HID通信移植步骤

[复制链接]
STMCU小助手 发布时间:2022-2-9 21:48
最近发现很多人对STM32的USB通信很感兴趣。要将USB的通信协议搞懂确实是一个比较漫长的过程。但是USB的HID通信无论是上位机的设计还是STM32程序的编程都非常的简单。只是我想很多人都不知道而已。这篇文章的目的是让大家以最短的时间将USB加到你的设备中。如果想学得更深就靠大家。
) V6 L. q% T* {) ^8 N. E  p
. P, ]7 h* a! J- e- v! x4 ?HID只是适合低速传输,其理论上可以达到64KB/S,但多由于windows系统和硬件的关系一般达不到这个传输数度。但这个速度对于一般系统的控制和数据传输都已经足够了,而且是免驱,省去了很多麻烦。2 Q+ T1 R+ ]) k' i% C' k
6 \# j  s- o5 R$ C1 J
8 |* _3 |* f0 u# p$ J% ]9 ~8 k: S- s
一、安装完MDK后请打开C:/Keil/ARM/Examples/ST/STM32F10xUSBLib/Demos路径,将Custom_HID在同一个目录下复制一份,如果你要放到其他路径你需要在MDK Options for target的C/C++中添加USB的头文件路径(MDK下的/INC/ST/STM32F10x/USB)。
3 [8 |/ V* Z' L* X# C. ]5 L
! l+ @" \2 w% B; \; x+ }: A9 j二、打开usb_desc.c文件,该文件主要包含的端点描述符、设备描述符、配置描述符和字符描述符等。具体请大家参考其他资料了,这里主要说几个常用1 n" Y0 K% i, I4 {) y) k

7 Z  a; e0 T% n" ju8 DeviceDescriptor[SIZ_DEVICE_DESC]为USB设备描述符。当中的
1 l8 j+ g2 b: w, A% ]# l' N
1 r7 v1 h. X$ c# X0x83, /*idVendor (0x0483)*/  ]! ~2 ^. I4 w1 U( Z, T6 q! a! K
+ i. k3 r" @) P% E. T$ x1 n, t
0x04,
- p' @, J9 g+ h, ?9 R' X2 }( A/ w: W9 d% w/ w& p& [
0x50, /*idProduct = 0x5750*/
5 s9 i- J/ |/ n# J
. f: r0 G9 d. `# S5 F/ @  I0x57,
9 d9 m- Y# C) ~  `& Q; ^& Q" A4 |9 N" R% S
//idVender字段。厂商ID号,我们这里取0x0483,仅供实验用。3 M0 ~4 K$ x# [) U3 t9 S% g

( ?) B& |" `' b//实际产品不能随便使用厂商ID号,必须跟USB协会申请厂商ID号。% s- V& {; Z$ |3 q: D% d; }8 J* z
# M' w  Q8 l( z6 x* {/ L* n
//注意小端模式,低字节在先。
6 E7 |$ [' f. J( Z0 m) r8 H
3 _8 A2 {' W  R( M: B  G//idProduct字段。产品ID号,我们这里取0x5750。
5 n; z# M7 R' ?( d$ C1 g+ ^7 x/ c+ Z; q/ c6 Y9 u
//注意小端模式,低字节应该在前。
- F2 u' j4 ~$ C9 I, k/ {
: a  }( C) J& k! l* Jconst u8 ConfigDescriptor[SIZ_CONFIG_DESC]是配置描述符,注意如下  Q/ [. Y% e* q, o( H
/ k* f8 b. `  b3 l9 X
USB_ENDPOINT_DESCRIPTOR_TYPE, /* bDescriptorType: */$ @% a  |8 b8 ?. k/ y0 w
0 x0 m" ?: j1 L3 n4 g+ l

  j! F; K* g- g0x81, /* bEndpointAddress: Endpoint Address (IN) */
. K" F# c# J% W" B4 _: w( @7 w  f- j  l) |1 u6 H5 L
0x03, /* bmAttributes: Interrupt endpoint */
5 h5 C$ _' V6 M! G# H# ~, u# E. o6 X. \! ^+ u8 b
0x02, /* wMaxPacketSize: 2 Bytes max */
/ l2 k# L  y: |6 J2 x: P$ C% p
5 F) o: O( \! u0x00,
8 ]6 K9 }$ A/ |$ _- n" p& j/ _: I" K# A3 I/ b$ a$ [
0x20, /* bInterval: Polling Interval (32 ms) */
( u& f6 ~$ X/ X$ P$ n. e2 W8 o% j/ @2 [, @
/* 34 *// v4 e7 \& y1 S, G. x9 w

, ]) F9 w3 X' F8 L" C4 I8 B  s- Z" f$ M
0x07, /* bLength: Endpoint Descriptor size */% h6 k6 |1 m" X% O, ]

, h' f7 z) w& t% L1 zUSB_ENDPOINT_DESCRIPTOR_TYPE, /* bDescriptorType: */. y+ d& N- \+ F, A$ R2 Q6 N

  k7 B0 F  x1 D' \- z" E, M4 ]4 E/* Endpoint descriptor type */
( n9 g$ q, q# E
" C$ b2 ?& M; d" |; ~1 _2 g: }0x01, /* bEndpointAddress: */
' A. M# \, `- \7 H# U2 ~2 U- K
. @1 |' f: N& h1 {" O/* Endpoint Address (OUT) *// ^; D" y: g% l( l: A
& ]* t; b5 g# p+ R7 e* d
0x03, /* bmAttributes: Interrupt endpoint */
( l) L! [2 K+ ?- v& X: a' N4 N8 X! R  l6 Y* {0 E6 i7 p- |3 |
0x02, /* wMaxPacketSize: 2 Bytes max */
, ?! B! G/ X7 q; d3 M
9 E1 a* I  q! i  ?0x00,3 g+ d' H3 H) B' V8 p7 }5 R
8 l7 o; l5 @* y) J0 U
0x20, /* bInterval: Polling Interval (20 ms) */
6 I4 s, ~* |0 e$ G  b( E! @; \2 G% c+ _
4 V! ?% F3 F; ?/ r% L: E1 K# \

# A) v. ~% f, i0 v上面包含了“输入端点描述符”和“输出端点描述符”。2 m& f& V0 a# y5 T: B
9 k; M5 E6 X$ U4 H% U
//wMaxPacketSize字段。该端点的最大包长。' Y1 @. P( Y: n8 [' j9 g  ^! L

- D5 P/ }4 c# K# g2 n//bInterval字段。端点查询的时间,
; d% r' N. P0 ]2 w! F0 R8 b6 c+ G7 Z- ?2 q+ C7 M0 {* i
1 s/ J3 r; h8 X9 J
为了实现更高速的通信我们修改如下:( N! q- Q- s$ C% Y6 A
8 I% f0 h% M( K$ o
/******************** Descriptor of endpoint ********************/* Y9 o& R6 {8 N+ t: L& K9 n

. O3 o; X+ s! I, ]! u, l: x/* 27 */# I! Z( k3 I# U
0 w8 D" o3 h/ k8 f. x2 W
0x07, /*bLength: Endpoint Descriptor size*/. a  [' o# g, T. ]+ p4 ~

( n  b. y/ ]1 {& [) MUSB_ENDPOINT_DESCRIPTOR_TYPE, /*bDescriptorType:*/7 b. f1 u- U2 K/ [
3 Z8 [5 i; ^. D

) o: E3 W* p7 Y( a; B) t6 x
8 c4 t2 E: y- I+ v: Y+ Y2 `. o$ o0x81, /*bEndpointAddress: Endpoint Address (IN)*/
1 w' l9 ~& g8 c1 l9 t$ v) i* e2 K  i. v( [* o
0x03, /*bmAttributes: Interrupt endpoint*/7 ]5 l1 X2 M' `2 l( }9 M0 r5 V
4 v4 m2 `! g, p$ y& ~
0x40, /*wMaxPacketSize: 64 Byte max */
+ ?9 c6 D) a* s% w. L' j
; S, L  v0 F7 q& a2 ?$ m0x00,
6 q7 N' k4 @+ @" U$ Y" E5 h: e
/ g5 Y3 R. o8 X9 ~- {- v0x0A, /*bInterval: Polling Interval (10 ms)*/
6 e0 s6 `' y& f" U) P. t+ |0 }3 y0 X+ P& b
1 d, `- v' Q+ _8 w6 ?4 x/* 34 */& Z3 g2 N4 @) k# K

8 H4 k+ u! @5 V1 k/******************** Descriptor of endpoint ********************/) j2 D( o0 v$ L5 o- u
; f; U! ]- [- N" h
/* 27 */; W( d6 ~$ ~  I2 A

% q' L" x, Y- ~1 {: ?0x07, /*bLength: Endpoint Descriptor size*/& ]# h9 P4 Z( i

! a- z. L9 Q9 k$ wUSB_ENDPOINT_DESCRIPTOR_TYPE, /*bDescriptorType:*/
7 H" g6 ~: y, E& w# U( C2 y2 T" `# j' Y
3 a' ~% |; \% ?7 x' m& N$ G9 T

3 K% y* M; m. H1 L5 h0x01, /*bEndpointAddress: Endpoint Address (OUT)*/# q3 J; M9 U# ~

, a( o: D$ J/ Z% F% L$ M0x03, /*bmAttributes: Interrupt endpoint*/
) R6 S, \1 f5 F7 z/ [
0 K6 ?. E6 [5 K0x40, /*wMaxPacketSize: 64 Byte max */6 S6 p8 P. Q/ z
& G8 f7 O& m( R1 \5 j
0x00,
- ~" T3 e6 J# S! O* Q& f0 |( r. |2 B8 I- U4 M' c
0x0A, /*bInterval: Polling Interval (10 ms)*/3 K7 B. h0 P# m7 R
- M9 {- G, b; ?  y2 |: A1 ^4 O4 b) _, y
const u8 ReportDescriptor[SIZ_REPORT_DESC]为HID专用的报告描述符,具体的大家就参考资料了,这里可以直接复制了。
7 o4 C) P$ f7 t! ~
. Y. L* h5 B& yconst u8 CustomHID_ReportDescriptor[CUSTOMHID_SIZ_REPORT_DESC] =
* t6 y" x. m( S, x  }1 ?, l3 t/ u
+ o- X) p0 q. G0 D{
4 H4 ^8 i" t9 @; [  f8 o  M" ~1 \; v4 x. t- n5 }, z* @
0x05, 0xFF, // USAGE_PAGE(User define)
2 o" t  l+ ]+ o  [2 K" F8 D7 M
# ?: p9 |3 F* t( c  |0x09, 0xFF, // USAGE(User define)& D- |- G, _& I! M- m
1 \, X6 E5 j2 f7 k
0xa1, 0x01, // COLLECTION (Application)
+ Y. b8 C" f* |- t: r! h3 z( G) f: \+ i8 N' L
0x05, 0x01, // USAGE_PAGE(1)7 i$ D- j- R, G! E
5 k2 C: o6 N1 m4 E
0x19, 0x00, // USAGE_MINIMUM(0)
1 X+ t5 ?* y& G5 h$ l, D  N+ I5 c
; a9 I4 q# X7 l4 E6 @( D1 u0x29, 0xFF, // USAGE_MAXIMUM(255). F% I, g" I; R$ K/ ~  }/ |  ~$ C6 G

* x3 u, p& I& V+ R* h5 F0x15, 0x00, // LOGICAL_MINIMUM (0)/ q& U; i5 X8 ]- b, J5 B7 b2 g8 G

+ x( U% Y, k0 X2 \/ p& ?) M3 `' _0x25, 0xFF, // LOGICAL_MAXIMUM (255)
5 @  X2 d  ^1 }/ m
4 P" H7 q/ {$ w( i0x75, 0x08, // REPORT_SIZE (8)
: f6 y1 o8 v) H3 J& p$ ~% L
  E6 l' `( F. W, ?0x95, 0x40, // REPORT_COUNT (64)
4 \" k, u4 g' H0 q( j! K4 E$ C$ C- C# t7 a8 X9 P
0x81, 0x02, // INPUT (Data,Var,Abs)
4 x% P( l5 _+ F- W3 \
7 V! m1 j  @# Z6 `2 q. I0x05, 0x02, // USAGE_PAGE(2)
1 w4 V$ {7 Q0 h6 Y+ m' l% x2 d3 T, Y7 _
0x19, 0x00, // USAGE_MINIMUM (0)
6 H( }/ K+ N3 u4 e, [; G8 v
7 ~; }* X6 ?. J1 e8 G9 o0x29, 0xFF, // USAGE_MAXIMUM (255)
* s( \4 L# u: m) l5 }
$ ^% {9 P$ m+ r( d5 s* u9 q6 T- g0x15, 0x00, // LOGICAL_MINIMUM (0)* a2 J, t  ^* A

: A) D6 o( @) A" I0x25, 0xFF, // LOGICAL_MAXIMUM (255)
$ X* x. |: a- Y: R0 r/ {
/ |  A( H2 ~, N# M/ T0x95, 0x08, // REPORT_COUNT (8)
/ b1 B8 r+ M) z2 R6 C) d% J$ x% X
0x75, 0x40, // REPORT_SIZE (64)8 W6 S  G: ~6 i" `3 Z9 A, j, o7 u
! m$ t  y+ Z4 v0 A
0x91, 0x02, // OUTPUT (Data,Var,Abs)
" ]( @2 [- Q( z4 `8 T6 D/ u& p- x  \
0xc0 // END_COLLECTION
# O/ H" g, d  J* H. Y
5 u0 H6 K$ O* U}; /* ReportDescriptor */
. ~) o6 @7 b8 E. e1 [. [% z/ r
. Y+ `8 z* P1 t- B# o3 |9 w( ?
' R, O6 t' Z9 P9 j: a% t6 p( _9 C; t9 \# {9 I
const u8 CustomHID_StringVendor[CUSTOMHID_SIZ_STRING_VENDOR]
9 g# H2 ]& E9 h" s% ~
2 B' e3 [% {  b/ R" p' Oconst u8 StringProduct[SIZ_STRING_PRODUCT]
3 y! P8 f. R4 o; Z( U! n! s& z9 C+ b$ O* J- r1 Z
const u8 StringSerial[SIZ_STRING_SERIAL]
) A7 {  d- z, q& v$ y. y2 \
, \9 p( k# q+ N+ V4 F分别是“厂商字符”、“产品字符”、“产品序列号”,这些将在USB HID设备加载的时候显示。但是这需要这些字符要求为Unicode编码,你需要将你要显示的字符先转为Unicode编码。3 \% ~8 h6 n: w4 y- ]; Y

# Y! o' v3 `2 @. {" ?8 T. W5 c#define CUSTOMHID_SIZ_REPORT_DESC 398 K& A. g, K6 a) c+ N/ R9 g
& w* [' e( Y5 C+ W. d% ^4 R5 x
#define CUSTOMHID_SIZ_STRING_VENDOR 64
# |5 K7 C. J; P
( Q1 K0 H9 s& Z, Y" a% M9 q#define CUSTOMHID_SIZ_STRING_PRODUCT 28
3 I% T( b( v: Y3 Q8 p
, Q  m# i* l. Y: R#define CUSTOMHID_SIZ_STRING_SERIAL 26) \4 K4 h4 a% a7 p! m

4 `+ f$ u1 l: G: g! ~. N8 ^) U4 s- F* {: T; X
三、打开hw_config.c文件,将那些没有的函数删除,只保留如下函数+ W9 W! @: M% `' L7 T5 v

5 W" N' e  T8 p# k3 ga) Set_System(void)% k( m& S, T3 |  S1 c- Q+ s
1 P" u' R( Z+ Z, W; t, o
b) void Set_USBClock(void)
' y& M  O3 Y8 m2 F% y, W4 W! E
7 K) D1 q  v6 Pc) void USB_Interrupts_Config(void)
7 S; v; Q  P$ S2 Y; G0 G
/ a: c$ h. T& R; K! R1 \3 M! hd) void USB_Cable_Config (FunctionalState NewState)
- s% W1 x( R: b. c; t* Z, `' F; R$ ?. |
特别要注意最后一个函数,其主要作用是控制USB的上拉电阻,让电脑检测USB设备是否连接的。2 @1 j1 F, r9 x% z8 q1 o
0 i( E, j7 c: r) [9 _+ P3 [

; H. b+ c5 ~0 @4 v' A
+ L5 `, O4 p! e% [8 k( H四、打开stm32f10x_it.c文件,把EXTI15_10_IRQHandler等中断内的代码删除。+ V- S" g/ {: _2 h4 ~
2 j" s( ]& O& r7 B2 G& [1 A+ ~! h# V' Z: f
打开usb_prop.c文件,修改如下:
$ o9 V. A: {8 i, i! a4 x* q. f3 C4 W
# }0 V; O% K3 T! R+ n6 V+ \void CustomHID_Reset(void)2 P; v9 G( E9 c' d; J. ~

/ l# c' N% K% v{$ V9 K1 X  y  S6 P

" V8 w3 `6 s( r, N7 N/* Set Joystick_DEVICE as not configured */
# s- {3 ?9 A5 ]1 a1 f+ ]
' y) ~4 M$ c7 ~( XpInformation->Current_Configuration = 0;7 ^( Q8 {+ u& R9 o1 w7 l7 {

. L( P- g' z7 J6 j0 dpInformation->Current_Interface = 0;/*the default Interface*/
  ~8 I1 K  Z5 _$ O
# g* L0 B6 h7 Y; c; c2 C% iSetBTABLE(BTABLE_ADDRESS);
6 n. J# \5 I4 `$ o( a7 [* _
2 Q. Y: E1 E, C+ T/ Q. z+ u, n
) t# D  `+ U' E2 |& g; q. c$ b/ U4 K: G+ Z9 v
/* Initialize Endpoint 0 */
/ Z: ]  X9 p8 l# W4 K6 G* ~; j7 S; N0 t% ~: ^; A3 q% B! W
SetEPType(ENDP0, EP_CONTROL);% y* z/ y8 b' y/ y

# r6 F/ U; @1 y4 [7 A) i# QSetEPTxStatus(ENDP0, EP_TX_STALL);
) q; _: ~8 [6 q$ N9 l! h0 `3 z; T$ g  _
SetEPRxAddr(ENDP0, ENDP0_RXADDR);6 R' h9 F' h' t8 v) ^$ m

" s, W* m- D3 B5 d% hSetEPTxAddr(ENDP0, ENDP0_TXADDR);! k0 v3 K1 }1 m' E
/ ?$ d  t, I1 T/ N) r
Clear_Status_Out(ENDP0);
+ q1 ^: A2 J% e% ~+ @2 f9 F6 R) u# V( W( C: U
SetEPRxCount(ENDP0, Device_Property.MaxPacketSize);
" ]: d: V$ A( C1 T1 |6 L+ V0 D' L0 ?$ Z
SetEPRxValid(ENDP0);
+ w7 \8 ]2 {+ y, H9 ~2 P) z8 x. G+ Y) \1 T: t
: ]0 V) V1 f1 t3 _$ d1 x/ ]8 m

; k8 r8 e" K; @. j# ~/* Initialize Endpoint 1 */9 n: g* r6 c1 R8 R9 D3 y

7 _- ?% f* D- Y& P0 J. _SetEPType(ENDP1, EP_INTERRUPT);
: A. G1 i0 C. h+ L1 _3 N. c7 `9 L
SetEPTxAddr(ENDP1, ENDP1_TXADDR);3 ^; Q& z4 x# U4 v7 _. P  F' Z
: m+ f. U9 X6 {8 f
SetEPTxCount(ENDP1, 64);, ~1 ^% X, l( s/ a" ?
( O& Q, F7 v' S2 w  ]" l
SetEPRxStatus(ENDP1, EP_RX_DIS);$ g7 K) T1 i# p9 X0 k5 D
$ ?% m- ~' w0 e! g
SetEPTxStatus(ENDP1, EP_TX_NAK);. j  H' Z4 B8 Q8 U7 |

: m- x2 s* U& }7 d- p
0 y* |. S, T5 Y" a1 k( ]" y# X6 e/ _* @& w) q* t
/* Initialize Endpoint 1 */
- F" Y, e, }) Z$ n8 }6 l# m' a! j9 L; \  T6 |0 e) \
// SetEPType(ENDP1, EP_INTERRUPT);
" b6 ~0 |8 x9 V' z* C
" ?5 _- K9 t0 PSetEPRxAddr(ENDP1, ENDP1_RXADDR);
. \2 l. s: z+ [1 r) {
9 M: T" E( V. c; oSetEPRxCount(ENDP1, 64);7 g+ L3 X- `3 m9 t

  y% L$ c/ i" o4 Z" V) ]// SetEPTxStatus(ENDP1, EP_TX_DIS);
' W6 P. W7 m4 V8 C! g5 ]6 V1 `# b$ T3 |' G
SetEPRxStatus(ENDP1, EP_RX_VALID);
5 [1 J: s7 B; u* i( n4 ~' t7 F) s" M
/* Set this device to response on default address */0 o' P/ }* f( C& Y1 C
) E7 ~. @+ h; c% K' {- j) x- a: F2 C" u
SetDeviceAddress(0);+ q7 T7 ]3 ?" C) y& ?3 u
$ i" m  ]& |  u" C2 B
}
, c) n2 Y& U0 y9 ?; l, _4 |8 P6 l

! O* P; H. k- p$ p五、usb_endp.c文件0 M2 T4 {5 P. m, E3 ?/ N& w* M$ `

+ L6 d- p5 Z5 W; r) v
void EP1_OUT_Callback(void)
4 f$ K4 T* w, y* U1 ~% a5 r; U4 O4 w: o+ b' c* Z
{: ?8 s  D" D1 _
. _" U  P2 J2 E1 c) e
这些写接收代码
" u$ g* x3 S+ L4 \* P0 s2 t" o8 N. E4 E, ?* ?- O3 N
}* m* u5 M& ^5 ^3 H3 B
9 {0 q1 J2 E; g8 W8 y

# q; V6 v1 {% p8 A+ b: C& Q# @5 K0 J0 a# m, m! F& D  B
六、数据发送和接收,举例说明
- o+ U6 |) }, k4 ~$ ~% L% L, K( B  B& |4 c! w/ T
1、数据接收
5 L( x1 s* d( }9 Y* A: o- |: O1 E$ R% Z: v" O% W5 S- f. I
u8 DataLen;: V: k" z* C! n' c8 Q
5 \0 z& Q2 p% v  X# u
DataLen = GetEPRxCount(ENDP1);0 }, M( c: `$ a# M

  v% d7 P* A2 e: N) i/ p+ B# JPMAToUserBufferCopy(TX1_buffer, ENDP1_RXADDR, DataLen);
3 Q, c0 f9 A; I' W$ t
( m* i1 b7 h" D$ V0 ISetEPRxValid(ENDP1);: Q% G: i/ R7 x* H
+ Z6 E) G" p5 L. [7 X% ?
USART1_Send(DataLen);
; z+ F, b+ U% q( E
2 |$ P: Q3 U, E) X+ Q. {# T- Zcount_out = 1;
2 J& B' m6 `  p4 _$ r2 t) G) m3 Y- T" w/ i, _/ f

3 {+ ^3 r3 l5 r+ Z- W# H2 I" |- o" h2、数据发送4 m  |  d) P+ K, c- O) `7 _

4 t+ C  H- w4 m2 g. m$ ?- wUserToPMABufferCopy(InBuffer, GetEPTxAddr(ENDP1), 64);
% T( S* k3 r9 G) d  a0 ?
0 U8 V/ A, H4 ]8 T3 wSetEPTxCount(ENDP1, 64);
% I/ {& A$ C7 {; G8 U4 ?
+ R% y$ z, E) Y$ eSetEPTxValid(ENDP1);
3 X. g8 ^7 x0 P0 ^8 C7 a  ^' H
( U& ?8 z- K0 C* e! }如果你发送数据较为频繁,每次发送前应使用GetEPTxStatus(ENDP1)检测上次发送是否完成。如果端点状态处于EP_TX_VALID,说明发送未结束,如果端点状态处于EP_TX_NAK,说明发送结束。
) c  g, S! m/ Q' o
$ ^2 V$ N- y. L: m% l: c
赞 收藏 评论0 发布时间:2022-2-9 21:48

举报

0个回答

所属标签

相似技术帖

官网相关资源

关于
我们是谁
投资者关系
意法半导体可持续发展举措
创新与技术
意法半导体官网
联系我们
联系ST分支机构
寻找销售人员和分销渠道
社区
媒体中心
活动与培训
隐私策略
隐私策略
Cookies管理
行使您的权利
官方最新发布
人形机器人运动控制、感知与智能配电
半导体创新技术与应用方向
EE架构与软件定义汽车
12V/48V 汽车智能配电(SPD)
区域控制单元(ZCU)与分区架构
关注我们
st-img 微信公众号
st-img 手机版