1 /*******************************************************************************
2 * Copyright 2019-2020 Microchip FPGA Embedded Systems Solutions.
3 *
4 * SPDX-License-Identifier: MIT
5 *
6 * PolarFire SoC MSS USB Driver Stack
7 * USB Logical Layer (USB-LL)
8 * USBH-HID class driver.
9 *
10 * This file implements Host side HID class specific initialization
11 * and request handling.
12 *
13 */
14
15 #include "mpfs_hal/mss_hal.h"
16 #include "mss_usb_host_hid.h"
17 #include "mss_usb_host.h"
18 #include "mss_usb_std_def.h"
19
20 #ifdef __cplusplus
21 extern "C" {
22 #endif
23
24 #ifdef MSS_USB_HOST_ENABLED
25
26 /***************************************************************************//**
27 Constant values internally used by USBH-HID driver.
28 */
29 #define MSS_USBH_HID_CLASS_ID 0x03u
30 #define MSS_USBH_HID_SUBCLASS_ID 0x01u
31 #define MSS_USBH_HID_PROTOCOL_ID 0x02u
32
33 #define MSS_USBH_HID_DRIVER_ID (uint32_t)((MSS_USBH_HID_CLASS_ID << 16) |\
34 (MSS_USBH_HID_SUBCLASS_ID << 8)|\
35 (MSS_USBH_HID_PROTOCOL_ID) )
36
37
38 #define USBH_HID_INTR_RX_PIPE MSS_USB_RX_EP_1
39 #define USBH_HID_INTR_TX_PIPE_FIFOADDR 0x100u
40 #define USBH_HID_INTR_RX_PIPE_FIFOADDR 0x300u
41 #define USBH_HID_INTR_TX_PIPE_FIFOSZ 0x40u
42 #define USBH_HID_INTR_RX_PIPE_FIFOSZ 0x40u
43 #define USBH_HID_DESC_SIZE 9
44 #define USBH_HID_DESC 0x21
45 #define USBH_HID_REPORT_DESC 0x22
46 #define USBH_HID_SET_IDLE 0x0A
47
48 #define HID_MIN_POLL 10
49 #define HC_PID_DATA0 0
50 #define HC_PID_DATA2 1
51 #define HC_PID_DATA1 2
52 #define HC_PID_SETUP 3
53
54 /* States for HID State Machine */
55 typedef enum
56 {
57 HID_IDLE= 0,
58 HID_SEND_DATA,
59 HID_BUSY,
60 HID_GET_DATA,
61 HID_READ_DATA,
62 HID_WAIT_READ_DATA,
63 HID_SYNC,
64 HID_POLL,
65 HID_ERROR,
66 }
67 HID_State;
68
69
70 typedef struct USBH_HIDDescriptor
71 {
72 uint8_t bLength;
73 uint8_t bDescriptorType;
74 uint16_t bcdHID; /* indicates what endpoint this descriptor is
75 * describing */
76 uint8_t bCountryCode; /* specifies the transfer type. */
77 uint8_t bNumDescriptors; /* specifies the transfer type. */
78 uint8_t bReportDescriptorType;/* Maximum Packet Size this endpoint is
79 * capable of sending or receiving */
80 uint16_t wItemLength; /* is used to specify the polling interval
81 * of certain transfers. */
82 }
83 USBH_HIDDesc_TypeDef_t;
84
85 USBH_HIDDesc_TypeDef_t USBH_HID_Desc;
86
87 #pragma data_alignment = 4
88 uint8_t g_hid_report[50] = {0};
89 volatile uint8_t toggle_in = 0;
90 HID_State HID_Machine_state= HID_IDLE;
91 volatile uint8_t next = 0;
92 volatile uint8_t USBH_HID_RX_buffer[10] = {0x00};
93
94 /***************************************************************************//**
95 Types internally used by USBH-HID driver.
96 */
97 typedef struct {
98 uint8_t num;
99 uint16_t maxpktsz;
100 uint16_t desclen;
101 } tdev_ep_t;
102
103 /***************************************************************************//**
104 Private functions declarations for USBH-HID driver.
105 */
106 static volatile uint8_t g_usbh_hid_alloc_event = 0u;
107 static volatile uint8_t g_usbh_hid_cep_event = 0u;
108 static volatile uint8_t g_usbh_hid_tx_event = 0u;
109 static volatile uint8_t g_usbh_hid_rx_event = 0u;
110
111 static uint8_t g_hid_tdev_addr = 0u;
112 static mss_usb_state_t hid_tdev_state = MSS_USB_NOT_ATTACHED_STATE;
113 static uint8_t g_hid_conf_desc[34] = {0};
114 static tdev_ep_t g_tdev_in_ep = {0};
115 static volatile mss_usbh_hid_state_t g_hid_state = USBH_HID_IDLE;
116 static mss_usbh_hid_err_code_t g_hidh_error_code = USBH_HID_NO_ERROR;
117 static mss_usbh_hid_user_cb_t* g_hidh_user_cb;
118
119 static uint8_t usbh_hid_allocate_cb(uint8_t tdev_addr);
120 static uint8_t usbh_hid_release_cb(uint8_t tdev_addr);
121 static uint8_t usbh_hid_cep_done_cb(uint8_t tdev_addr,
122 uint8_t status, uint32_t count);
123 static uint8_t usbh_hid_tx_complete_cb(uint8_t tdev_addr,
124 uint8_t status,
125 uint32_t count);
126 static uint8_t usbh_hid_rx_cb(uint8_t tdev_addr, uint8_t status, uint32_t count);
127 static mss_usbh_hid_err_code_t MSS_USBH_HID_validate_class_desc(uint8_t* p_cd);
128 static mss_usbh_hid_err_code_t MSS_USBH_HID_extract_tdev_ep_desc(void);
129 static void USBH_HID_Handle(void);
130
131 /***************************************************************************//**
132 Definition of Class call-back functions used by USBH driver.
133 */
134 mss_usbh_class_cb_t hid_class =
135 {
136 MSS_USBH_HID_DRIVER_ID,
137 usbh_hid_allocate_cb,
138 usbh_hid_release_cb,
139 usbh_hid_cep_done_cb,
140 usbh_hid_tx_complete_cb,
141 usbh_hid_rx_cb,
142 0
143 };
144
145 /*******************************************************************************
146 * EXPORTED API Functions
147 ******************************************************************************/
148
149 /******************************************************************************
150 * See mss_usb_host_hid.h for details of how to use this function.
151 */
152 void
MSS_USBH_HID_init(mss_usbh_hid_user_cb_t * user_sb)153 MSS_USBH_HID_init
154 (
155 mss_usbh_hid_user_cb_t* user_sb
156 )
157 {
158 g_hid_state = USBH_HID_IDLE;
159 memset(g_hid_conf_desc, 0u, sizeof(g_hid_conf_desc));
160 g_tdev_in_ep.maxpktsz = 0u;
161 g_tdev_in_ep.num = 0u;
162 g_hid_tdev_addr = 0u;
163 g_hidh_user_cb = user_sb;
164 HID_Machine_state= HID_IDLE;
165 }
166
167 /******************************************************************************
168 * See mss_usb_host_hid.h for details of how to use this function.
169 */
MSS_USBH_HID_get_handle(void)170 void* MSS_USBH_HID_get_handle
171 (
172 void
173 )
174 {
175 return((void*)&hid_class);
176 }
177
178 /******************************************************************************
179 * See mss_usb_host_hid.h for details of how to use this function.
180 */
181 void
MSS_USBH_HID_task(void)182 MSS_USBH_HID_task
183 (
184 void
185 )
186 {
187 uint8_t std_req_buf[USB_SETUP_PKT_LEN] = {0};
188 static volatile uint32_t wait_mili = 0u;
189
190 switch (g_hid_state)
191 {
192 case USBH_HID_IDLE:
193 if (g_usbh_hid_alloc_event)
194 {
195 g_usbh_hid_alloc_event = 0;
196 g_hid_state = USBH_HID_GET_CLASS_DESCR;
197 }
198 break;
199
200 case USBH_HID_GET_CLASS_DESCR:
201 /* Read all the User Configuration descripter, HID descripter,
202 * Interface and Endpoint descriptor in one go instaed of reading
203 * each descripter individually.
204 * May seperated it after completing implementation
205 * #define USB_CONFIGURATION_DESC_SIZE 9
206 * #define USB_HID_DESC_SIZE 9
207 * #define USB_INTERFACE_DESC_SIZE 9
208 * #define USB_ENDPOINT_DESC_SIZE 7
209 */
210 hid_tdev_state = MSS_USBH_get_tdev_state(g_hid_tdev_addr);
211 if (MSS_USB_ADDRESS_STATE == hid_tdev_state)
212 {
213 mss_usb_ep_state_t cep_st;
214 cep_st = MSS_USBH_get_cep_state();
215 if (MSS_USB_CEP_IDLE == cep_st)
216 {
217 MSS_USBH_configure_control_pipe(g_hid_tdev_addr);
218 memset(std_req_buf, 0u, 8*(sizeof(uint8_t)));
219 MSS_USBH_construct_get_descr_command(std_req_buf,
220 USB_STD_REQ_DATA_DIR_IN,
221 USB_STANDARD_REQUEST,
222 USB_STD_REQ_RECIPIENT_DEVICE,
223 USB_STD_REQ_GET_DESCRIPTOR,
224 USB_CONFIGURATION_DESCRIPTOR_TYPE,
225 0, /*stringID*/
226 34);
227
228 g_hid_state = USBH_HID_WAIT_GET_CLASS_DESCR;
229 MSS_USBH_start_control_xfr(std_req_buf,
230 (uint8_t*)&g_hid_conf_desc,
231 USB_STD_REQ_DATA_DIR_IN,
232 34);
233 }
234 }
235 break;
236
237 case USBH_HID_WAIT_GET_CLASS_DESCR:
238 if (g_usbh_hid_cep_event)
239 {
240 mss_usbh_hid_err_code_t res = USBH_HID_NO_ERROR;
241 g_usbh_hid_cep_event = 0u;
242 res = MSS_USBH_HID_validate_class_desc(0);
243
244 if (res == 0u)
245 {
246 g_hid_state = USBH_HID_SET_CONFIG;
247 }
248 else
249 {
250 g_hid_state = USBH_HID_ERROR;
251 g_hidh_error_code = res;
252 }
253
254 res = MSS_USBH_HID_extract_tdev_ep_desc();
255
256 if (res == 0u)
257 {
258 g_hid_state = USBH_HID_SET_CONFIG;
259 }
260 else
261 {
262 g_hid_state = USBH_HID_ERROR;
263 g_hidh_error_code = res;
264 }
265 }
266 break;
267
268 case USBH_HID_SET_CONFIG:
269 if (0 != g_hidh_user_cb->hidh_valid_config)
270 {
271 g_hidh_user_cb->hidh_valid_config();
272 }
273
274 memset(std_req_buf, 0u, 8*(sizeof(uint8_t)));
275 std_req_buf[1] = USB_STD_REQ_SET_CONFIG;
276 std_req_buf[2] = g_hid_conf_desc[5];
277 g_hid_state = USBH_HID_WAIT_SET_CONFIG;
278
279 MSS_USBH_start_control_xfr(std_req_buf,
280 (uint8_t*)&g_hid_conf_desc,
281 USB_STD_REQ_DATA_DIR_IN,
282 0u);
283 break;
284
285 case USBH_HID_WAIT_SET_CONFIG:
286 if (g_usbh_hid_cep_event)
287 {
288 g_usbh_hid_cep_event = 0;
289 wait_mili = MSS_USBH_get_milis();
290 g_hid_state = USBH_HID_WAIT_DEV_SETTLE;
291 }
292 break;
293
294 case USBH_HID_WAIT_DEV_SETTLE:
295 /* After SET_CONFIG command, we must give time for device to settle
296 * down as per spec*/
297 if ((MSS_USBH_get_milis() - wait_mili) > 60)
298 {
299 g_hid_state = USBH_HID_REQ_GET_HID_DESC;
300 }
301 break;
302
303 case USBH_HID_REQ_GET_HID_DESC:
304 {
305 for (uint32_t i = 0; i < sizeof(g_hid_conf_desc); i++)
306 {
307 g_hid_conf_desc[i] = 0;
308 }
309
310 memset(std_req_buf, 0u, 8*(sizeof(uint8_t)));
311 MSS_USBH_construct_get_descr_command(std_req_buf,
312 USB_STD_REQ_DATA_DIR_IN,
313 USB_STANDARD_REQUEST,
314 USB_STD_REQ_RECIPIENT_INTERFACE,
315 USB_STD_REQ_GET_DESCRIPTOR,
316 USBH_HID_DESC,
317 0, /*stringID*/
318 USBH_HID_DESC_SIZE);
319
320 g_hid_state = USBH_HID_WAIT_GET_HID_DESC;
321 MSS_USBH_start_control_xfr(std_req_buf,(uint8_t*)&USBH_HID_Desc,
322 USB_STD_REQ_DATA_DIR_IN,
323 9);
324
325 }
326 break;
327
328 case USBH_HID_WAIT_GET_HID_DESC:
329 if (g_usbh_hid_cep_event)
330 {
331 mss_usbh_hid_err_code_t res = USBH_HID_NO_ERROR;
332 g_usbh_hid_cep_event = 0u;
333 g_hid_state = USBH_HID_REQ_GET_REPORT_DESC;
334 }
335 break;
336
337 case USBH_HID_REQ_GET_REPORT_DESC:
338 MSS_USBH_configure_control_pipe(g_hid_tdev_addr);
339 memset(std_req_buf, 0u, 8*(sizeof(uint8_t)));
340 MSS_USBH_construct_get_descr_command(std_req_buf,
341 USB_STD_REQ_DATA_DIR_IN,
342 USB_STANDARD_REQUEST,
343 USB_STD_REQ_RECIPIENT_INTERFACE,
344 USB_STD_REQ_GET_DESCRIPTOR,
345 USBH_HID_REPORT_DESC,
346 0, /*stringID*/
347 USBH_HID_Desc.wItemLength);
348
349 g_hid_state = USBH_HID_WAIT_REQ_GET_REPORT_DESC;
350 MSS_USBH_start_control_xfr(std_req_buf,
351 (uint8_t*)&g_hid_report,
352 USB_STD_REQ_DATA_DIR_IN,
353 USBH_HID_Desc.wItemLength);
354 break;
355
356 case USBH_HID_WAIT_REQ_GET_REPORT_DESC:
357 if (g_usbh_hid_cep_event)
358 {
359 mss_usbh_hid_err_code_t res = USBH_HID_NO_ERROR;
360 g_usbh_hid_cep_event = 0u;
361 g_hid_state = USBH_HID_REQ_SET_IDLE;
362 }
363 break;
364
365 case USBH_HID_REQ_SET_IDLE:
366 memset(std_req_buf, 0u, 8*(sizeof(uint8_t)));
367 MSS_USBH_construct_get_descr_command(std_req_buf,
368 USB_STD_REQ_DATA_DIR_OUT,
369 USB_CLASS_REQUEST,
370 USB_STD_REQ_RECIPIENT_INTERFACE,
371 USBH_HID_SET_IDLE,
372 0,
373 0, /*stringID*/
374 0);
375
376 g_hid_state = USBH_HID_WAIT_REQ_SET_IDLE;
377 MSS_USBH_start_control_xfr(std_req_buf,
378 (uint8_t*)&g_hid_report,
379 USB_STD_REQ_DATA_DIR_IN,
380 0);
381 break;
382
383 case USBH_HID_WAIT_REQ_SET_IDLE:
384 if (g_usbh_hid_cep_event)
385 {
386 mss_usbh_hid_err_code_t res = USBH_HID_NO_ERROR;
387 g_usbh_hid_cep_event = 0u;
388 g_hid_state = USBH_HID_DEVICE_READY;
389 if (0 != g_hidh_user_cb->hidh_tdev_ready)
390 {
391 g_hidh_user_cb->hidh_tdev_ready();
392 }
393 }
394
395 break;
396
397 case USBH_HID_REQ_SET_PROTOCOL:
398
399 memset(std_req_buf, 0u, 8*(sizeof(uint8_t)));
400 std_req_buf[0] = 0x21;
401 std_req_buf[1] = 0x0B;
402 std_req_buf[2] = 0x00;
403 std_req_buf[3] = 0x01;
404 std_req_buf[4] = 0x00;
405 std_req_buf[5] = 0x00;
406 std_req_buf[6] = 0x00;
407 std_req_buf[7] = 0x00;
408
409 g_hid_state = USBH_HID_WAIT_REQ_SET_PROTOCOL;
410 MSS_USBH_start_control_xfr(std_req_buf,
411 (uint8_t*)&g_hid_report,
412 USB_STD_REQ_DATA_DIR_IN,
413 0);
414 break;
415
416 case USBH_HID_WAIT_REQ_SET_PROTOCOL:
417 if (g_usbh_hid_cep_event)
418 {
419 mss_usbh_hid_err_code_t res = USBH_HID_NO_ERROR;
420 g_usbh_hid_cep_event = 0u;
421 g_hid_state = USBH_HID_REQ_SET_PROTOCOL;
422 }
423 break;
424
425 case USBH_HID_DEVICE_READY:
426 USBH_HID_Handle();
427 break;
428
429 default:
430 {
431 ASSERT(0); /*Reset recovery should be tried.*/
432 }
433 break;
434 }
435 }
436
437
438 /*******************************************************************************
439 * See mss_usb_host_hid.h for details of how to use this function.
440 */
441 mss_usbh_hid_state_t
MSS_USBH_HID_get_state(void)442 MSS_USBH_HID_get_state
443 (
444 void
445 )
446 {
447 return (g_hid_state);
448 }
449
450 /*******************************************************************************
451 * Internal Functions
452 ******************************************************************************/
453 /*
454 This Call-back function is executed when the USBH-HID driver is allocated
455 to the attached device by USBH driver.
456 */
457 uint8_t
usbh_hid_allocate_cb(uint8_t tdev_addr)458 usbh_hid_allocate_cb
459 (
460 uint8_t tdev_addr
461 )
462 {
463 g_hid_tdev_addr = tdev_addr;
464 g_usbh_hid_alloc_event = 1u;
465
466 return (USB_SUCCESS);
467 }
468
469 /*
470 This Call-back function is executed when the USBH-HID driver is released
471 from the attached device by USBH driver.
472 */
473 uint8_t
usbh_hid_release_cb(uint8_t tdev_addr)474 usbh_hid_release_cb
475 (
476 uint8_t tdev_addr
477 )
478 {
479 g_hid_state = USBH_HID_IDLE;
480 memset(g_hid_conf_desc, 0u, sizeof(g_hid_conf_desc));
481 g_tdev_in_ep.maxpktsz = 0u;
482 g_tdev_in_ep.num = 0u;
483 g_hid_tdev_addr = 0u;
484
485 MSS_USB_CIF_dma_clr_ctrlreg(MSS_USB_DMA_CHANNEL2);
486
487 MSS_USB_CIF_dma_clr_ctrlreg(MSS_USB_DMA_CHANNEL1);
488
489 if (0 != g_hidh_user_cb->hidh_driver_released)
490 {
491 g_hidh_user_cb->hidh_driver_released();
492 }
493
494 return (USB_SUCCESS);
495 }
496
497 /*
498 This Call-back function is executed when the control transfer initiated by this
499 driver is complete.
500 */
501 uint8_t
usbh_hid_cep_done_cb(uint8_t tdev_addr,uint8_t status,uint32_t count)502 usbh_hid_cep_done_cb
503 (
504 uint8_t tdev_addr,
505 uint8_t status,
506 uint32_t count
507 )
508 {
509 g_usbh_hid_cep_event = status;
510
511 return (USB_SUCCESS);
512 }
513
514 /*
515 This Call-back function is executed when the data OUT transfer initiated by
516 this driver is complete.
517 */
518 uint8_t
usbh_hid_tx_complete_cb(uint8_t tdev_addr,uint8_t status,uint32_t count)519 usbh_hid_tx_complete_cb
520 (
521 uint8_t tdev_addr,
522 uint8_t status,
523 uint32_t count
524 )
525 {
526 g_usbh_hid_tx_event = 1;
527
528 return (USB_SUCCESS);
529 }
530
531 /*
532 This Call-back function is executed when the data IN transfer initiated by
533 this driver is complete.
534 */
535 uint8_t
usbh_hid_rx_cb(uint8_t tdev_addr,uint8_t status,uint32_t count)536 usbh_hid_rx_cb
537 (
538 uint8_t tdev_addr,
539 uint8_t status,
540 uint32_t count
541 )
542 {
543 if (0 == status)
544 {
545 g_usbh_hid_rx_event = 1u;
546 }
547 else
548 {
549 if (MSS_USB_EP_NAK_TOUT & status)
550 {
551 /* Device responding with NAKs. Retry*/
552 g_hid_state = USBH_HID_DEVICE_RETRY;
553 }
554 else
555 {
556 ASSERT(0);/* Handling any other error. Not yet supported */
557 }
558 }
559
560 return (USB_SUCCESS);
561 }
562
563 /*
564 This function validates the HID class descriptors.
565 */
566 mss_usbh_hid_err_code_t
MSS_USBH_HID_validate_class_desc(uint8_t * p_cd)567 MSS_USBH_HID_validate_class_desc
568 (
569 uint8_t* p_cd
570 )
571 {
572 return (USBH_HID_NO_ERROR);
573 }
574
575 /*
576 This function extract the endpoint information from the Config Descriptor.
577 */
578 mss_usbh_hid_err_code_t
MSS_USBH_HID_extract_tdev_ep_desc(void)579 MSS_USBH_HID_extract_tdev_ep_desc
580 (
581 void
582 )
583 {
584 /* FirstEP Attributes Not INTR. bInterfaceclass For HID value should be 0x03 */
585 if (!(g_hid_conf_desc[30u] & USB_EP_DESCR_ATTR_INTR))
586 {
587 return (USBH_HID_WRONG_DESCR);
588 }
589
590 /* TdevEP is IN type for HID class */
591 if (g_hid_conf_desc[29u] & USB_STD_REQ_DATA_DIR_MASK)
592 {
593 g_tdev_in_ep.num = (g_hid_conf_desc[29u] & 0x7fu);
594 g_tdev_in_ep.maxpktsz = (uint16_t)((g_hid_conf_desc[32u] << 8u) |
595 (g_hid_conf_desc[31u]));
596
597 g_tdev_in_ep.desclen = (uint16_t)((g_hid_conf_desc[26u] << 8u) |
598 (g_hid_conf_desc[25u]));
599
600 }
601 else
602 {
603 return (USBH_HID_WRONG_DESCR);
604 }
605
606 return (USBH_HID_NO_ERROR);
607 }
608
609 /*
610 This function read the report from the hid device.
611 */
USBH_HID_Handle(void)612 static void USBH_HID_Handle(void)
613 {
614 static uint32_t wait_mili = 0;
615
616 switch (HID_Machine_state)
617 {
618 case HID_IDLE:
619 HID_Machine_state = HID_GET_DATA;
620
621 MSS_USBH_configure_in_pipe(g_hid_tdev_addr,
622 USBH_HID_INTR_RX_PIPE,
623 g_tdev_in_ep.num,
624 USBH_HID_INTR_RX_PIPE_FIFOADDR,
625 USBH_HID_INTR_RX_PIPE_FIFOSZ,
626 g_tdev_in_ep.maxpktsz,
627 1,
628 DMA_DISABLE,
629 MSS_USB_DMA_CHANNEL2,
630 MSS_USB_XFR_INTERRUPT,
631 ADD_ZLP_TO_XFR,
632 32768);
633
634 HID_Machine_state = HID_READ_DATA;
635 break;
636
637 case HID_READ_DATA:
638 MSS_USBH_read_in_pipe(g_hid_tdev_addr,
639 USBH_HID_INTR_RX_PIPE,
640 g_tdev_in_ep.num,
641 g_tdev_in_ep.maxpktsz,
642 (uint8_t*)&USBH_HID_RX_buffer,
643 g_tdev_in_ep.maxpktsz);
644
645 HID_Machine_state = HID_POLL;
646
647 wait_mili = MSS_USBH_get_milis();
648
649 break;
650
651 case HID_POLL:
652 if ((MSS_USBH_get_milis() - wait_mili) > HID_MIN_POLL)
653 {
654 HID_Machine_state = HID_WAIT_READ_DATA;
655 }
656 break;
657
658 case HID_WAIT_READ_DATA:
659 if (g_usbh_hid_rx_event)
660 {
661 g_usbh_hid_rx_event = 0;
662 HID_Machine_state = HID_READ_DATA;
663 if (0 != g_hidh_user_cb->hidh_decode)
664 {
665 g_hidh_user_cb->hidh_decode((uint8_t*)&USBH_HID_RX_buffer);
666 }
667 }
668 break;
669
670 default:
671 break;
672 }
673
674 }
675
676 #endif /* MSS_USB_HOST_ENABLED */
677
678 #ifdef __cplusplus
679 }
680 #endif
681