1 /*
2 * COPYRIGHT: See COPYING in the top level directory
3 * PROJECT: ReactOS kernel
4 * FILE: hal/halx86/mp/ioapic.c
5 * PURPOSE:
6 * PROGRAMMER:
7 */
8
9 /* INCLUDES *****************************************************************/
10
11 #include <hal.h>
12 #define NDEBUG
13 #include <debug.h>
14
15 /* GLOBALS *****************************************************************/
16
17 MP_CONFIGURATION_INTSRC IRQMap[MAX_IRQ_SOURCE]; /* Map of all IRQs */
18 ULONG IRQCount = 0; /* Number of IRQs */
19 ULONG IrqApicMap[MAX_IRQ_SOURCE];
20
21 UCHAR BUSMap[MAX_BUS]; /* Map of all buses in the system */
22 UCHAR PCIBUSMap[MAX_BUS]; /* Map of all PCI buses in the system */
23
24 IOAPIC_INFO IOAPICMap[MAX_IOAPIC]; /* Map of all I/O APICs in the system */
25 ULONG IOAPICCount; /* Number of I/O APICs in the system */
26
27 ULONG IRQVectorMap[MAX_IRQ_SOURCE]; /* IRQ to vector map */
28
29 /* EISA interrupts are always polarity zero and can be edge or level
30 * trigger depending on the ELCR value. If an interrupt is listed as
31 * EISA conforming in the MP table, that means its trigger type must
32 * be read in from the ELCR */
33
34 #define default_EISA_trigger(idx) (EISA_ELCR_Read(IRQMap[idx].SrcBusIrq))
35 #define default_EISA_polarity(idx) (0)
36
37 /* ISA interrupts are always polarity zero edge triggered,
38 * when listed as conforming in the MP table. */
39
40 #define default_ISA_trigger(idx) (0)
41 #define default_ISA_polarity(idx) (0)
42
43 /* PCI interrupts are always polarity one level triggered,
44 * when listed as conforming in the MP table. */
45
46 #define default_PCI_trigger(idx) (1)
47 #define default_PCI_polarity(idx) (1)
48
49 /* MCA interrupts are always polarity zero level triggered,
50 * when listed as conforming in the MP table. */
51
52 #define default_MCA_trigger(idx) (1)
53 #define default_MCA_polarity(idx) (0)
54
55 /***************************************************************************/
56
57 extern VOID Disable8259AIrq(ULONG irq);
58 ULONG IOAPICRead(ULONG Apic, ULONG Offset);
59 VOID IOAPICWrite(ULONG Apic, ULONG Offset, ULONG Value);
60
61 /* FUNCTIONS ***************************************************************/
62
63 /*
64 * EISA Edge/Level control register, ELCR
65 */
EISA_ELCR_Read(ULONG irq)66 static ULONG EISA_ELCR_Read(ULONG irq)
67 {
68 if (irq < 16)
69 {
70 PUCHAR port = (PUCHAR)(0x4d0 + (irq >> 3));
71 return (READ_PORT_UCHAR(port) >> (irq & 7)) & 1;
72 }
73 DPRINT1("Broken MPtable reports ISA irq %lu\n", irq);
74 return 0;
75 }
76
77 static ULONG
IRQPolarity(ULONG idx)78 IRQPolarity(ULONG idx)
79 {
80 ULONG bus = IRQMap[idx].SrcBusId;
81 ULONG polarity;
82
83 /*
84 * Determine IRQ line polarity (high active or low active):
85 */
86 switch (IRQMap[idx].IrqFlag & 3)
87 {
88 case 0: /* conforms, ie. bus-type dependent polarity */
89 {
90 switch (BUSMap[bus])
91 {
92 case MP_BUS_ISA: /* ISA pin */
93 polarity = default_ISA_polarity(idx);
94 break;
95
96 case MP_BUS_EISA: /* EISA pin */
97 polarity = default_EISA_polarity(idx);
98 break;
99
100 case MP_BUS_PCI: /* PCI pin */
101 polarity = default_PCI_polarity(idx);
102 break;
103
104 case MP_BUS_MCA: /* MCA pin */
105 polarity = default_MCA_polarity(idx);
106 break;
107
108 default:
109 DPRINT1("Broken BIOS\n");
110 polarity = 1;
111 }
112 }
113 break;
114
115 case 1: /* high active */
116 polarity = 0;
117 break;
118
119 case 2: /* reserved */
120 DPRINT1("Broken BIOS\n");
121 polarity = 1;
122 break;
123
124 case 3: /* low active */
125 polarity = 1;
126 break;
127
128 default: /* invalid */
129 DPRINT1("Broken BIOS\n");
130 polarity = 1;
131 }
132 return polarity;
133 }
134
135 static ULONG
IRQTrigger(ULONG idx)136 IRQTrigger(ULONG idx)
137 {
138 ULONG bus = IRQMap[idx].SrcBusId;
139 ULONG trigger;
140
141 /*
142 * Determine IRQ trigger mode (edge or level sensitive):
143 */
144 switch ((IRQMap[idx].IrqFlag >> 2) & 3)
145 {
146 case 0: /* conforms, ie. bus-type dependent */
147 {
148 switch (BUSMap[bus])
149 {
150 case MP_BUS_ISA: /* ISA pin */
151 trigger = default_ISA_trigger(idx);
152 break;
153
154 case MP_BUS_EISA: /* EISA pin */
155 trigger = default_EISA_trigger(idx);
156 break;
157
158 case MP_BUS_PCI: /* PCI pin */
159 trigger = default_PCI_trigger(idx);
160 break;
161
162 case MP_BUS_MCA: /* MCA pin */
163 trigger = default_MCA_trigger(idx);
164 break;
165
166 default:
167 DPRINT1("Broken BIOS\n");
168 trigger = 1;
169 }
170 }
171 break;
172
173 case 1: /* edge */
174 trigger = 0;
175 break;
176
177 case 2: /* reserved */
178 DPRINT1("Broken BIOS\n");
179 trigger = 1;
180 break;
181
182 case 3: /* level */
183 trigger = 1;
184 break;
185
186 default: /* invalid */
187 DPRINT1("Broken BIOS\n");
188 trigger = 0;
189 }
190 return trigger;
191 }
192
193 static ULONG
Pin2Irq(ULONG idx,ULONG apic,ULONG pin)194 Pin2Irq(ULONG idx,
195 ULONG apic,
196 ULONG pin)
197 {
198 ULONG irq, i;
199 ULONG bus = IRQMap[idx].SrcBusId;
200
201 /*
202 * Debugging check, we are in big trouble if this message pops up!
203 */
204 if (IRQMap[idx].DstApicInt != pin)
205 {
206 DPRINT1("Broken BIOS or MPTABLE parser\n");
207 }
208
209 switch (BUSMap[bus])
210 {
211 case MP_BUS_ISA: /* ISA pin */
212 case MP_BUS_EISA:
213 case MP_BUS_MCA:
214 irq = IRQMap[idx].SrcBusIrq;
215 break;
216
217 case MP_BUS_PCI: /* PCI pin */
218 /*
219 * PCI IRQs are mapped in order
220 */
221 i = irq = 0;
222 while (i < apic)
223 {
224 irq += IOAPICMap[i++].EntryCount;
225 }
226 irq += pin;
227 break;
228
229 default:
230 DPRINT1("Unknown bus type %lu\n", bus);
231 irq = 0;
232 }
233 return irq;
234 }
235
236 static ULONG
AssignIrqVector(ULONG irq)237 AssignIrqVector(ULONG irq)
238 {
239 #if 0
240 static ULONG current_vector = FIRST_DEVICE_VECTOR, vector_offset = 0;
241 #endif
242 ULONG vector;
243 /* There may already have been assigned a vector for this IRQ */
244 vector = IRQVectorMap[irq];
245 if (vector > 0)
246 {
247 return vector;
248 }
249 #if 0
250 if (current_vector > FIRST_SYSTEM_VECTOR)
251 {
252 vector_offset++;
253 current_vector = FIRST_DEVICE_VECTOR + vector_offset;
254 }
255 else if (current_vector == FIRST_SYSTEM_VECTOR)
256 {
257 DPRINT1("Ran out of interrupt sources\n");
258 ASSERT(FALSE);
259 }
260
261 vector = current_vector;
262 IRQVectorMap[irq] = vector;
263 current_vector += 8;
264 return vector;
265 #else
266 vector = IRQ2VECTOR(irq);
267 IRQVectorMap[irq] = vector;
268 return vector;
269 #endif
270 }
271
272 /*
273 * Find the IRQ entry number of a certain pin.
274 */
275 static ULONG
IOAPICGetIrqEntry(ULONG apic,ULONG pin,ULONG type)276 IOAPICGetIrqEntry(ULONG apic,
277 ULONG pin,
278 ULONG type)
279 {
280 ULONG i;
281
282 for (i = 0; i < IRQCount; i++)
283 {
284 if (IRQMap[i].IrqType == type &&
285 (IRQMap[i].DstApicId == IOAPICMap[apic].ApicId || IRQMap[i].DstApicId == MP_APIC_ALL) &&
286 IRQMap[i].DstApicInt == pin)
287 {
288 return i;
289 }
290 }
291 return -1;
292 }
293
294
295 VOID
IOAPICSetupIrqs(VOID)296 IOAPICSetupIrqs(VOID)
297 {
298 IOAPIC_ROUTE_ENTRY entry;
299 ULONG apic, pin, idx, irq, first_notcon = 1, vector, trigger;
300
301 DPRINT("Init IO_APIC IRQs\n");
302
303 /* Setup IRQ to vector translation map */
304 memset(&IRQVectorMap, 0, sizeof(IRQVectorMap));
305
306 for (apic = 0; apic < IOAPICCount; apic++)
307 {
308 for (pin = 0; pin < IOAPICMap[apic].EntryCount; pin++)
309 {
310 /*
311 * add it to the IO-APIC irq-routing table
312 */
313 memset(&entry,0,sizeof(entry));
314
315 entry.delivery_mode = (APIC_DM_LOWEST >> 8);
316 entry.dest_mode = 1; /* logical delivery */
317 entry.mask = 1; /* disable IRQ */
318 entry.dest.logical.logical_dest = 0;
319
320 idx = IOAPICGetIrqEntry(apic,pin,INT_VECTORED);
321 if (idx == (ULONG)-1)
322 {
323 if (first_notcon)
324 {
325 DPRINT(" IO-APIC (apicid-pin) %u-%lu\n", IOAPICMap[apic].ApicId, pin);
326 first_notcon = 0;
327 }
328 else
329 {
330 DPRINT(", %u-%lu\n", IOAPICMap[apic].ApicId, pin);
331 }
332 continue;
333 }
334
335 trigger = IRQTrigger(idx);
336 entry.polarity = IRQPolarity(idx);
337
338 if (trigger)
339 {
340 entry.trigger = 1;
341 }
342
343 irq = Pin2Irq(idx, apic, pin);
344
345 vector = AssignIrqVector(irq);
346 entry.vector = vector;
347
348 DPRINT("Vector 0x%.08lx assigned to irq 0x%.02lx\n", vector, irq);
349
350 if (irq == 0)
351 {
352 /* Mask timer IRQ */
353 entry.mask = 1;
354 }
355
356 if ((apic == 0) && (irq < 16))
357 {
358 Disable8259AIrq(irq);
359 }
360 IOAPICWrite(apic, IOAPIC_REDTBL+2*pin+1, *(((PULONG)&entry)+1));
361 IOAPICWrite(apic, IOAPIC_REDTBL+2*pin, *(((PULONG)&entry)+0));
362
363 IrqApicMap[irq] = apic;
364
365 DPRINT("Vector %lx, Pin %lx, Irq %lx\n", vector, pin, irq);
366 }
367 }
368 }
369
370 static VOID
IOAPICClearPin(ULONG Apic,ULONG Pin)371 IOAPICClearPin(ULONG Apic, ULONG Pin)
372 {
373 IOAPIC_ROUTE_ENTRY Entry;
374
375 DPRINT("IOAPICClearPin(Apic %lu, Pin %lu\n", Apic, Pin);
376 /*
377 * Disable it in the IO-APIC irq-routing table
378 */
379 memset(&Entry, 0, sizeof(Entry));
380 Entry.mask = 1;
381
382 IOAPICWrite(Apic, IOAPIC_REDTBL + 2 * Pin, *(((PULONG)&Entry) + 0));
383 IOAPICWrite(Apic, IOAPIC_REDTBL + 1 + 2 * Pin, *(((PULONG)&Entry) + 1));
384 }
385
386 static VOID
IOAPICClear(ULONG Apic)387 IOAPICClear(ULONG Apic)
388 {
389 ULONG Pin;
390
391 for (Pin = 0; Pin < /*IOAPICMap[Apic].EntryCount*/24; Pin++)
392 {
393 IOAPICClearPin(Apic, Pin);
394 }
395 }
396
397 static VOID
IOAPICClearAll(VOID)398 IOAPICClearAll(VOID)
399 {
400 ULONG Apic;
401
402 for (Apic = 0; Apic < IOAPICCount; Apic++)
403 {
404 IOAPICClear(Apic);
405 }
406 }
407
408 VOID
IOAPICEnable(VOID)409 IOAPICEnable(VOID)
410 {
411 ULONG i, tmp;
412
413 /* Setup IRQ to vector translation map */
414 memset(&IRQVectorMap, 0, sizeof(IRQVectorMap));
415
416 /*
417 * The number of IO-APIC IRQ registers (== #pins):
418 */
419 for (i = 0; i < IOAPICCount; i++)
420 {
421 tmp = IOAPICRead(i, IOAPIC_VER);
422 IOAPICMap[i].EntryCount = GET_IOAPIC_MRE(tmp) + 1;
423 }
424
425 /*
426 * Do not trust the IO-APIC being empty at bootup
427 */
428 IOAPICClearAll();
429 }
430
431 VOID
IOAPICSetupIds(VOID)432 IOAPICSetupIds(VOID)
433 {
434 ULONG tmp, apic, i;
435 UCHAR old_id;
436
437 /*
438 * Set the IOAPIC ID to the value stored in the MPC table.
439 */
440 for (apic = 0; apic < IOAPICCount; apic++)
441 {
442
443 /* Read the register 0 value */
444 tmp = IOAPICRead(apic, IOAPIC_ID);
445
446 old_id = IOAPICMap[apic].ApicId;
447
448 if (IOAPICMap[apic].ApicId >= 0xf)
449 {
450 DPRINT1("BIOS bug, IO-APIC#%lu ID is %u in the MPC table\n", apic, IOAPICMap[apic].ApicId);
451 IOAPICMap[apic].ApicId = GET_IOAPIC_ID(tmp);
452 DPRINT1(" Fixed up to %u. (Tell your hardware vendor)\n", IOAPICMap[apic].ApicId);
453 }
454
455 /*
456 * We need to adjust the IRQ routing table
457 * if the ID changed.
458 */
459 if (old_id != IOAPICMap[apic].ApicId)
460 {
461 for (i = 0; i < IRQCount; i++)
462 {
463 if (IRQMap[i].DstApicId == old_id)
464 {
465 IRQMap[i].DstApicId = IOAPICMap[apic].ApicId;
466 }
467 }
468 }
469
470 /*
471 * Read the right value from the MPC table and
472 * write it into the ID register.
473 */
474 DPRINT("Changing IO-APIC physical APIC ID to %u\n", IOAPICMap[apic].ApicId);
475
476 tmp &= ~IOAPIC_ID_MASK;
477 tmp |= SET_IOAPIC_ID(IOAPICMap[apic].ApicId);
478
479 IOAPICWrite(apic, IOAPIC_ID, tmp);
480
481 /*
482 * Sanity check
483 */
484 tmp = IOAPICRead(apic, 0);
485 if (GET_IOAPIC_ID(tmp) != IOAPICMap[apic].ApicId)
486 {
487 DPRINT1("Could not set I/O APIC ID!\n");
488 ASSERT(FALSE);
489 }
490 }
491 }
492
493 /* This is performance critical and should probably be done in assembler */
IOAPICMaskIrq(ULONG Irq)494 VOID IOAPICMaskIrq(ULONG Irq)
495 {
496 IOAPIC_ROUTE_ENTRY Entry;
497 ULONG Apic = IrqApicMap[Irq];
498
499 *(((PULONG)&Entry)+0) = IOAPICRead(Apic, IOAPIC_REDTBL+2*Irq);
500 *(((PULONG)&Entry)+1) = IOAPICRead(Apic, IOAPIC_REDTBL+2*Irq+1);
501 Entry.dest.logical.logical_dest &= ~(1 << KeGetCurrentProcessorNumber());
502 if (Entry.dest.logical.logical_dest == 0)
503 {
504 Entry.mask = 1;
505 }
506 IOAPICWrite(Apic, IOAPIC_REDTBL+2*Irq+1, *(((PULONG)&Entry)+1));
507 IOAPICWrite(Apic, IOAPIC_REDTBL+2*Irq, *(((PULONG)&Entry)+0));
508 }
509
510 /* This is performance critical and should probably be done in assembler */
IOAPICUnmaskIrq(ULONG Irq)511 VOID IOAPICUnmaskIrq(ULONG Irq)
512 {
513 IOAPIC_ROUTE_ENTRY Entry;
514 ULONG Apic = IrqApicMap[Irq];
515
516 *(((PULONG)&Entry)+0) = IOAPICRead(Apic, IOAPIC_REDTBL+2*Irq);
517 *(((PULONG)&Entry)+1) = IOAPICRead(Apic, IOAPIC_REDTBL+2*Irq+1);
518 Entry.dest.logical.logical_dest |= 1 << KeGetCurrentProcessorNumber();
519 Entry.mask = 0;
520 IOAPICWrite(Apic, IOAPIC_REDTBL+2*Irq+1, *(((PULONG)&Entry)+1));
521 IOAPICWrite(Apic, IOAPIC_REDTBL+2*Irq, *(((PULONG)&Entry)+0));
522 }
523
IOAPICDump(VOID)524 VOID IOAPICDump(VOID)
525 {
526 ULONG apic, i;
527 ULONG reg0, reg1, reg2=0;
528
529 DbgPrint("Number of MP IRQ sources: %d.\n", IRQCount);
530 for (i = 0; i < IOAPICCount; i++)
531 {
532 DbgPrint("Number of IO-APIC #%d registers: %d.\n",
533 IOAPICMap[i].ApicId,
534 IOAPICMap[i].EntryCount);
535 }
536
537 /*
538 * We are a bit conservative about what we expect. We have to
539 * know about every hardware change ASAP.
540 */
541 DbgPrint("Testing the IO APIC.......................\n");
542
543 for (apic = 0; apic < IOAPICCount; apic++)
544 {
545 reg0 = IOAPICRead(apic, IOAPIC_ID);
546 reg1 = IOAPICRead(apic, IOAPIC_VER);
547 if (GET_IOAPIC_VERSION(reg1) >= 0x10)
548 {
549 reg2 = IOAPICRead(apic, IOAPIC_ARB);
550 }
551
552 DbgPrint("\n");
553 DbgPrint("IO APIC #%d......\n", IOAPICMap[apic].ApicId);
554 DbgPrint(".... register #00: %08X\n", reg0);
555 DbgPrint("....... : physical APIC id: %02X\n", GET_IOAPIC_ID(reg0));
556 if (reg0 & 0xF0FFFFFF)
557 {
558 DbgPrint(" WARNING: Unexpected IO-APIC\n");
559 }
560
561 DbgPrint(".... register #01: %08X\n", reg1);
562 i = GET_IOAPIC_MRE(reg1);
563
564 DbgPrint("....... : max redirection entries: %04X\n", i);
565 if ((i != 0x0f) && /* older (Neptune) boards */
566 (i != 0x17) && /* typical ISA+PCI boards */
567 (i != 0x1b) && /* Compaq Proliant boards */
568 (i != 0x1f) && /* dual Xeon boards */
569 (i != 0x22) && /* bigger Xeon boards */
570 (i != 0x2E) &&
571 (i != 0x3F))
572 {
573 DbgPrint(" WARNING: Unexpected IO-APIC\n");
574 }
575
576 i = GET_IOAPIC_VERSION(reg1);
577 DbgPrint("....... : IO APIC version: %04X\n", i);
578 if ((i != 0x01) && /* 82489DX IO-APICs */
579 (i != 0x10) && /* oldest IO-APICs */
580 (i != 0x11) && /* Pentium/Pro IO-APICs */
581 (i != 0x13)) /* Xeon IO-APICs */
582 {
583 DbgPrint(" WARNING: Unexpected IO-APIC\n");
584 }
585
586 if (reg1 & 0xFF00FF00)
587 {
588 DbgPrint(" WARNING: Unexpected IO-APIC\n");
589 }
590
591 if (GET_IOAPIC_VERSION(reg1) >= 0x10)
592 {
593 DbgPrint(".... register #02: %08X\n", reg2);
594 DbgPrint("....... : arbitration: %02X\n",
595 GET_IOAPIC_ARB(reg2));
596 if (reg2 & 0xF0FFFFFF)
597 {
598 DbgPrint(" WARNING: Unexpected IO-APIC\n");
599 }
600 }
601
602 DbgPrint(".... IRQ redirection table:\n");
603 DbgPrint(" NR Log Phy Mask Trig IRR Pol"
604 " Stat Dest Deli Vect: \n");
605
606 for (i = 0; i <= GET_IOAPIC_MRE(reg1); i++)
607 {
608 IOAPIC_ROUTE_ENTRY entry;
609
610 *(((PULONG)&entry)+0) = IOAPICRead(apic, 0x10+i*2);
611 *(((PULONG)&entry)+1) = IOAPICRead(apic, 0x11+i*2);
612
613 DbgPrint(" %02x %03X %02X ",
614 i,
615 entry.dest.logical.logical_dest,
616 entry.dest.physical.physical_dest);
617
618 DbgPrint("%C %C %1d %C %C %C %03X %02X\n",
619 (entry.mask == 0) ? 'U' : 'M', // Unmasked/masked
620 (entry.trigger == 0) ? 'E' : 'L', // Edge/level sensitive
621 entry.irr,
622 (entry.polarity == 0) ? 'H' : 'L', // Active high/active low
623 (entry.delivery_status == 0) ? 'I' : 'S', // Idle / send pending
624 (entry.dest_mode == 0) ? 'P' : 'L', // Physical logical
625 entry.delivery_mode,
626 entry.vector);
627 }
628 }
629
630 DbgPrint(".................................... done.\n");
631 }
632
633 VOID
HaliReconfigurePciInterrupts(VOID)634 HaliReconfigurePciInterrupts(VOID)
635 {
636 ULONG i;
637
638 for (i = 0; i < IRQCount; i++)
639 {
640 if (BUSMap[IRQMap[i].SrcBusId] == MP_BUS_PCI)
641 {
642 DPRINT("%02lx: IrqType %02x, IrqFlag %04x, SrcBusId %02x"
643 ", SrcBusIrq %02x, DstApicId %02x, DstApicInt %02x\n",
644 i, IRQMap[i].IrqType, IRQMap[i].IrqFlag, IRQMap[i].SrcBusId,
645 IRQMap[i].SrcBusIrq, IRQMap[i].DstApicId, IRQMap[i].DstApicInt);
646
647 HalSetBusDataByOffset(PCIConfiguration,
648 IRQMap[i].SrcBusId,
649 (IRQMap[i].SrcBusIrq >> 2) & 0x1f,
650 &IRQMap[i].DstApicInt,
651 0x3c /*PCI_INTERRUPT_LINE*/,
652 1);
653
654 }
655 }
656 }
657
Disable8259AIrq(ULONG irq)658 VOID Disable8259AIrq(ULONG irq)
659 {
660 UCHAR tmp;
661
662 if (irq >= 8)
663 {
664 tmp = READ_PORT_UCHAR((PUCHAR)0xA1);
665 tmp |= (1 << (irq - 8));
666 WRITE_PORT_UCHAR((PUCHAR)0xA1, tmp);
667 }
668 else
669 {
670 tmp = READ_PORT_UCHAR((PUCHAR)0x21);
671 tmp |= (1 << irq);
672 WRITE_PORT_UCHAR((PUCHAR)0x21, tmp);
673 }
674 }
675
IOAPICRead(ULONG Apic,ULONG Offset)676 ULONG IOAPICRead(ULONG Apic, ULONG Offset)
677 {
678 PULONG Base;
679
680 Base = (PULONG)IOAPICMap[Apic].ApicAddress;
681 *Base = Offset;
682 return *((PULONG)((ULONG)Base + IOAPIC_IOWIN));
683 }
684
IOAPICWrite(ULONG Apic,ULONG Offset,ULONG Value)685 VOID IOAPICWrite(ULONG Apic, ULONG Offset, ULONG Value)
686 {
687 PULONG Base;
688
689 Base = (PULONG)IOAPICMap[Apic].ApicAddress;
690 *Base = Offset;
691 *((PULONG)((ULONG)Base + IOAPIC_IOWIN)) = Value;
692 }
693
694 /* EOF */
695