Signed-off-by: Feng Tian <feng.tian@intel.com> Reviewed-by: Elvin Li <elvin.li@intel.com> git-svn-id: https://svn.code.sf.net/p/edk2/code/trunk/edk2@14459 6f19259b-4bc3-4df7-8a09-765794883524
		
			
				
	
	
		
			1053 lines
		
	
	
		
			29 KiB
		
	
	
	
		
			C
		
	
	
	
	
	
			
		
		
	
	
			1053 lines
		
	
	
		
			29 KiB
		
	
	
	
		
			C
		
	
	
	
	
	
| /** @file
 | |
| 
 | |
|   EHCI transfer scheduling routines.
 | |
| 
 | |
| Copyright (c) 2007 - 2013, Intel Corporation. All rights reserved.<BR>
 | |
| This program and the accompanying materials
 | |
| are licensed and made available under the terms and conditions of the BSD License
 | |
| which accompanies this distribution.  The full text of the license may be found at
 | |
| http://opensource.org/licenses/bsd-license.php
 | |
| 
 | |
| THE PROGRAM IS DISTRIBUTED UNDER THE BSD LICENSE ON AN "AS IS" BASIS,
 | |
| WITHOUT WARRANTIES OR REPRESENTATIONS OF ANY KIND, EITHER EXPRESS OR IMPLIED.
 | |
| 
 | |
| **/
 | |
| 
 | |
| #include "Ehci.h"
 | |
| 
 | |
| 
 | |
| /**
 | |
|   Create helper QTD/QH for the EHCI device.
 | |
| 
 | |
|   @param  Ehc                   The EHCI device.
 | |
| 
 | |
|   @retval EFI_OUT_OF_RESOURCES  Failed to allocate resource for helper QTD/QH.
 | |
|   @retval EFI_SUCCESS           Helper QH/QTD are created.
 | |
| 
 | |
| **/
 | |
| EFI_STATUS
 | |
| EhcCreateHelpQ (
 | |
|   IN USB2_HC_DEV          *Ehc
 | |
|   )
 | |
| {
 | |
|   USB_ENDPOINT            Ep;
 | |
|   EHC_QH                  *Qh;
 | |
|   QH_HW                   *QhHw;
 | |
|   EHC_QTD                 *Qtd;
 | |
|   EFI_PHYSICAL_ADDRESS    PciAddr;
 | |
| 
 | |
|   //
 | |
|   // Create an inactive Qtd to terminate the short packet read.
 | |
|   //
 | |
|   Qtd = EhcCreateQtd (Ehc, NULL, NULL, 0, QTD_PID_INPUT, 0, 64);
 | |
| 
 | |
|   if (Qtd == NULL) {
 | |
|     return EFI_OUT_OF_RESOURCES;
 | |
|   }
 | |
| 
 | |
|   Qtd->QtdHw.Status   = QTD_STAT_HALTED;
 | |
|   Ehc->ShortReadStop  = Qtd;
 | |
| 
 | |
|   //
 | |
|   // Create a QH to act as the EHC reclamation header.
 | |
|   // Set the header to loopback to itself.
 | |
|   //
 | |
|   Ep.DevAddr    = 0;
 | |
|   Ep.EpAddr     = 1;
 | |
|   Ep.Direction  = EfiUsbDataIn;
 | |
|   Ep.DevSpeed   = EFI_USB_SPEED_HIGH;
 | |
|   Ep.MaxPacket  = 64;
 | |
|   Ep.HubAddr    = 0;
 | |
|   Ep.HubPort    = 0;
 | |
|   Ep.Toggle     = 0;
 | |
|   Ep.Type       = EHC_BULK_TRANSFER;
 | |
|   Ep.PollRate   = 1;
 | |
| 
 | |
|   Qh            = EhcCreateQh (Ehc, &Ep);
 | |
| 
 | |
|   if (Qh == NULL) {
 | |
|     return EFI_OUT_OF_RESOURCES;
 | |
|   }
 | |
| 
 | |
|   PciAddr           = UsbHcGetPciAddressForHostMem (Ehc->MemPool, Qh, sizeof (EHC_QH));
 | |
|   QhHw              = &Qh->QhHw;
 | |
|   QhHw->HorizonLink = QH_LINK (PciAddr + OFFSET_OF(EHC_QH, QhHw), EHC_TYPE_QH, FALSE);
 | |
|   QhHw->Status      = QTD_STAT_HALTED;
 | |
|   QhHw->ReclaimHead = 1;
 | |
|   Qh->NextQh        = Qh;
 | |
|   Ehc->ReclaimHead  = Qh;
 | |
| 
 | |
|   //
 | |
|   // Create a dummy QH to act as the terminator for periodical schedule
 | |
|   //
 | |
|   Ep.EpAddr   = 2;
 | |
|   Ep.Type     = EHC_INT_TRANSFER_SYNC;
 | |
| 
 | |
|   Qh          = EhcCreateQh (Ehc, &Ep);
 | |
| 
 | |
|   if (Qh == NULL) {
 | |
|     return EFI_OUT_OF_RESOURCES;
 | |
|   }
 | |
| 
 | |
|   Qh->QhHw.Status = QTD_STAT_HALTED;
 | |
|   Ehc->PeriodOne  = Qh;
 | |
| 
 | |
|   return EFI_SUCCESS;
 | |
| }
 | |
| 
 | |
| 
 | |
| /**
 | |
|   Initialize the schedule data structure such as frame list.
 | |
| 
 | |
|   @param  Ehc                   The EHCI device to init schedule data.
 | |
| 
 | |
|   @retval EFI_OUT_OF_RESOURCES  Failed to allocate resource to init schedule data.
 | |
|   @retval EFI_SUCCESS           The schedule data is initialized.
 | |
| 
 | |
| **/
 | |
| EFI_STATUS
 | |
| EhcInitSched (
 | |
|   IN USB2_HC_DEV          *Ehc
 | |
|   )
 | |
| {
 | |
|   EFI_PCI_IO_PROTOCOL   *PciIo;
 | |
|   VOID                  *Buf;
 | |
|   EFI_PHYSICAL_ADDRESS  PhyAddr;
 | |
|   VOID                  *Map;
 | |
|   UINTN                 Pages;
 | |
|   UINTN                 Bytes;
 | |
|   UINTN                 Index;
 | |
|   EFI_STATUS            Status;
 | |
|   EFI_PHYSICAL_ADDRESS  PciAddr;
 | |
| 
 | |
|   //
 | |
|   // First initialize the periodical schedule data:
 | |
|   // 1. Allocate and map the memory for the frame list
 | |
|   // 2. Create the help QTD/QH
 | |
|   // 3. Initialize the frame entries
 | |
|   // 4. Set the frame list register
 | |
|   //
 | |
|   PciIo = Ehc->PciIo;
 | |
| 
 | |
|   Bytes = 4096;
 | |
|   Pages = EFI_SIZE_TO_PAGES (Bytes);
 | |
| 
 | |
|   Status = PciIo->AllocateBuffer (
 | |
|                     PciIo,
 | |
|                     AllocateAnyPages,
 | |
|                     EfiBootServicesData,
 | |
|                     Pages,
 | |
|                     &Buf,
 | |
|                     0
 | |
|                     );
 | |
| 
 | |
|   if (EFI_ERROR (Status)) {
 | |
|     return EFI_OUT_OF_RESOURCES;
 | |
|   }
 | |
| 
 | |
|   Status = PciIo->Map (
 | |
|                     PciIo,
 | |
|                     EfiPciIoOperationBusMasterCommonBuffer,
 | |
|                     Buf,
 | |
|                     &Bytes,
 | |
|                     &PhyAddr,
 | |
|                     &Map
 | |
|                     );
 | |
| 
 | |
|   if (EFI_ERROR (Status) || (Bytes != 4096)) {
 | |
|     PciIo->FreeBuffer (PciIo, Pages, Buf);
 | |
|     return EFI_OUT_OF_RESOURCES;
 | |
|   }
 | |
| 
 | |
|   Ehc->PeriodFrame      = Buf;
 | |
|   Ehc->PeriodFrameMap   = Map;
 | |
| 
 | |
|   //
 | |
|   // Program the FRAMELISTBASE register with the low 32 bit addr
 | |
|   //
 | |
|   EhcWriteOpReg (Ehc, EHC_FRAME_BASE_OFFSET, EHC_LOW_32BIT (PhyAddr));
 | |
|   //
 | |
|   // Program the CTRLDSSEGMENT register with the high 32 bit addr
 | |
|   //
 | |
|   EhcWriteOpReg (Ehc, EHC_CTRLDSSEG_OFFSET, EHC_HIGH_32BIT (PhyAddr));
 | |
| 
 | |
|   //
 | |
|   // Init memory pool management then create the helper
 | |
|   // QTD/QH. If failed, previously allocated resources
 | |
|   // will be freed by EhcFreeSched
 | |
|   //
 | |
|   Ehc->MemPool = UsbHcInitMemPool (
 | |
|                    PciIo,
 | |
|                    EHC_BIT_IS_SET (Ehc->HcCapParams, HCCP_64BIT),
 | |
|                    EHC_HIGH_32BIT (PhyAddr)
 | |
|                    );
 | |
| 
 | |
|   if (Ehc->MemPool == NULL) {
 | |
|     Status = EFI_OUT_OF_RESOURCES;
 | |
|     goto ErrorExit1;
 | |
|   }
 | |
| 
 | |
|   Status = EhcCreateHelpQ (Ehc);
 | |
| 
 | |
|   if (EFI_ERROR (Status)) {
 | |
|     goto ErrorExit;
 | |
|   }
 | |
| 
 | |
|   //
 | |
|   // Initialize the frame list entries then set the registers
 | |
|   //
 | |
|   Ehc->PeriodFrameHost      = AllocateZeroPool (EHC_FRAME_LEN * sizeof (UINTN));
 | |
|   if (Ehc->PeriodFrameHost == NULL) {
 | |
|     Status = EFI_OUT_OF_RESOURCES;
 | |
|     goto ErrorExit;
 | |
|   }
 | |
| 
 | |
|   PciAddr  = UsbHcGetPciAddressForHostMem (Ehc->MemPool, Ehc->PeriodOne, sizeof (EHC_QH));
 | |
| 
 | |
|   for (Index = 0; Index < EHC_FRAME_LEN; Index++) {
 | |
|     //
 | |
|     // Store the pci bus address of the QH in period frame list which will be accessed by pci bus master.
 | |
|     //
 | |
|     ((UINT32 *)(Ehc->PeriodFrame))[Index] = QH_LINK (PciAddr, EHC_TYPE_QH, FALSE);
 | |
|     //
 | |
|     // Store the host address of the QH in period frame list which will be accessed by host.
 | |
|     //
 | |
|     ((UINTN *)(Ehc->PeriodFrameHost))[Index] = (UINTN)Ehc->PeriodOne;
 | |
|   }
 | |
| 
 | |
|   //
 | |
|   // Second initialize the asynchronous schedule:
 | |
|   // Only need to set the AsynListAddr register to
 | |
|   // the reclamation header
 | |
|   //
 | |
|   PciAddr = UsbHcGetPciAddressForHostMem (Ehc->MemPool, Ehc->ReclaimHead, sizeof (EHC_QH));
 | |
|   EhcWriteOpReg (Ehc, EHC_ASYNC_HEAD_OFFSET, EHC_LOW_32BIT (PciAddr));
 | |
|   return EFI_SUCCESS;
 | |
| 
 | |
| ErrorExit:
 | |
|   if (Ehc->PeriodOne != NULL) {
 | |
|     UsbHcFreeMem (Ehc->MemPool, Ehc->PeriodOne, sizeof (EHC_QH));
 | |
|     Ehc->PeriodOne = NULL;
 | |
|   }
 | |
| 
 | |
|   if (Ehc->ReclaimHead != NULL) {
 | |
|     UsbHcFreeMem (Ehc->MemPool, Ehc->ReclaimHead, sizeof (EHC_QH));
 | |
|     Ehc->ReclaimHead = NULL;
 | |
|   }
 | |
| 
 | |
|   if (Ehc->ShortReadStop != NULL) {
 | |
|     UsbHcFreeMem (Ehc->MemPool, Ehc->ShortReadStop, sizeof (EHC_QTD));
 | |
|     Ehc->ShortReadStop = NULL;
 | |
|   }
 | |
| 
 | |
| ErrorExit1:
 | |
|   PciIo->FreeBuffer (PciIo, Pages, Buf);
 | |
|   PciIo->Unmap (PciIo, Map);
 | |
| 
 | |
|   return Status;
 | |
| }
 | |
| 
 | |
| 
 | |
| /**
 | |
|   Free the schedule data. It may be partially initialized.
 | |
| 
 | |
|   @param  Ehc                   The EHCI device.
 | |
| 
 | |
| **/
 | |
| VOID
 | |
| EhcFreeSched (
 | |
|   IN USB2_HC_DEV          *Ehc
 | |
|   )
 | |
| {
 | |
|   EFI_PCI_IO_PROTOCOL     *PciIo;
 | |
| 
 | |
|   EhcWriteOpReg (Ehc, EHC_FRAME_BASE_OFFSET, 0);
 | |
|   EhcWriteOpReg (Ehc, EHC_ASYNC_HEAD_OFFSET, 0);
 | |
| 
 | |
|   if (Ehc->PeriodOne != NULL) {
 | |
|     UsbHcFreeMem (Ehc->MemPool, Ehc->PeriodOne, sizeof (EHC_QH));
 | |
|     Ehc->PeriodOne = NULL;
 | |
|   }
 | |
| 
 | |
|   if (Ehc->ReclaimHead != NULL) {
 | |
|     UsbHcFreeMem (Ehc->MemPool, Ehc->ReclaimHead, sizeof (EHC_QH));
 | |
|     Ehc->ReclaimHead = NULL;
 | |
|   }
 | |
| 
 | |
|   if (Ehc->ShortReadStop != NULL) {
 | |
|     UsbHcFreeMem (Ehc->MemPool, Ehc->ShortReadStop, sizeof (EHC_QTD));
 | |
|     Ehc->ShortReadStop = NULL;
 | |
|   }
 | |
| 
 | |
|   if (Ehc->MemPool != NULL) {
 | |
|     UsbHcFreeMemPool (Ehc->MemPool);
 | |
|     Ehc->MemPool = NULL;
 | |
|   }
 | |
| 
 | |
|   if (Ehc->PeriodFrame != NULL) {
 | |
|     PciIo = Ehc->PciIo;
 | |
|     ASSERT (PciIo != NULL);
 | |
| 
 | |
|     PciIo->Unmap (PciIo, Ehc->PeriodFrameMap);
 | |
| 
 | |
|     PciIo->FreeBuffer (
 | |
|              PciIo,
 | |
|              EFI_SIZE_TO_PAGES (EFI_PAGE_SIZE),
 | |
|              Ehc->PeriodFrame
 | |
|              );
 | |
| 
 | |
|     Ehc->PeriodFrame = NULL;
 | |
|   }
 | |
| 
 | |
|   if (Ehc->PeriodFrameHost != NULL) {
 | |
|     FreePool (Ehc->PeriodFrameHost);
 | |
|     Ehc->PeriodFrameHost = NULL;
 | |
|   }
 | |
| }
 | |
| 
 | |
| 
 | |
| /**
 | |
|   Link the queue head to the asynchronous schedule list.
 | |
|   UEFI only supports one CTRL/BULK transfer at a time
 | |
|   due to its interfaces. This simplifies the AsynList
 | |
|   management: A reclamation header is always linked to
 | |
|   the AsyncListAddr, the only active QH is appended to it.
 | |
| 
 | |
|   @param  Ehc                   The EHCI device.
 | |
|   @param  Qh                    The queue head to link.
 | |
| 
 | |
| **/
 | |
| VOID
 | |
| EhcLinkQhToAsync (
 | |
|   IN USB2_HC_DEV          *Ehc,
 | |
|   IN EHC_QH               *Qh
 | |
|   )
 | |
| {
 | |
|   EHC_QH                  *Head;
 | |
|   EFI_PHYSICAL_ADDRESS    PciAddr;
 | |
| 
 | |
|   //
 | |
|   // Append the queue head after the reclaim header, then
 | |
|   // fix the hardware visiable parts (EHCI R1.0 page 72).
 | |
|   // ReclaimHead is always linked to the EHCI's AsynListAddr.
 | |
|   //
 | |
|   Head                    = Ehc->ReclaimHead;
 | |
| 
 | |
|   Qh->NextQh              = Head->NextQh;
 | |
|   Head->NextQh            = Qh;
 | |
| 
 | |
|   PciAddr = UsbHcGetPciAddressForHostMem (Ehc->MemPool, Qh->NextQh, sizeof (EHC_QH));
 | |
|   Qh->QhHw.HorizonLink    = QH_LINK (PciAddr, EHC_TYPE_QH, FALSE);
 | |
|   PciAddr = UsbHcGetPciAddressForHostMem (Ehc->MemPool, Head->NextQh, sizeof (EHC_QH));
 | |
|   Head->QhHw.HorizonLink  = QH_LINK (PciAddr, EHC_TYPE_QH, FALSE);
 | |
| }
 | |
| 
 | |
| 
 | |
| /**
 | |
|   Unlink a queue head from the asynchronous schedule list.
 | |
|   Need to synchronize with hardware.
 | |
| 
 | |
|   @param  Ehc                   The EHCI device.
 | |
|   @param  Qh                    The queue head to unlink.
 | |
| 
 | |
| **/
 | |
| VOID
 | |
| EhcUnlinkQhFromAsync (
 | |
|   IN USB2_HC_DEV          *Ehc,
 | |
|   IN EHC_QH               *Qh
 | |
|   )
 | |
| {
 | |
|   EHC_QH                  *Head;
 | |
|   EFI_STATUS              Status;
 | |
|   EFI_PHYSICAL_ADDRESS    PciAddr;
 | |
| 
 | |
|   ASSERT (Ehc->ReclaimHead->NextQh == Qh);
 | |
| 
 | |
|   //
 | |
|   // Remove the QH from reclamation head, then update the hardware
 | |
|   // visiable part: Only need to loopback the ReclaimHead. The Qh
 | |
|   // is pointing to ReclaimHead (which is staill in the list).
 | |
|   //
 | |
|   Head                    = Ehc->ReclaimHead;
 | |
| 
 | |
|   Head->NextQh            = Qh->NextQh;
 | |
|   Qh->NextQh              = NULL;
 | |
| 
 | |
|   PciAddr = UsbHcGetPciAddressForHostMem (Ehc->MemPool, Head->NextQh, sizeof (EHC_QH));
 | |
|   Head->QhHw.HorizonLink  = QH_LINK (PciAddr, EHC_TYPE_QH, FALSE);
 | |
| 
 | |
|   //
 | |
|   // Set and wait the door bell to synchronize with the hardware
 | |
|   //
 | |
|   Status = EhcSetAndWaitDoorBell (Ehc, EHC_GENERIC_TIMEOUT);
 | |
| 
 | |
|   if (EFI_ERROR (Status)) {
 | |
|     DEBUG ((EFI_D_ERROR, "EhcUnlinkQhFromAsync: Failed to synchronize with doorbell\n"));
 | |
|   }
 | |
| }
 | |
| 
 | |
| 
 | |
| /**
 | |
|   Link a queue head for interrupt transfer to the periodic
 | |
|   schedule frame list. This code is very much the same as
 | |
|   that in UHCI.
 | |
| 
 | |
|   @param  Ehc                   The EHCI device.
 | |
|   @param  Qh                    The queue head to link.
 | |
| 
 | |
| **/
 | |
| VOID
 | |
| EhcLinkQhToPeriod (
 | |
|   IN USB2_HC_DEV          *Ehc,
 | |
|   IN EHC_QH               *Qh
 | |
|   )
 | |
| {
 | |
|   UINTN                   Index;
 | |
|   EHC_QH                  *Prev;
 | |
|   EHC_QH                  *Next;
 | |
|   EFI_PHYSICAL_ADDRESS    PciAddr;
 | |
| 
 | |
|   for (Index = 0; Index < EHC_FRAME_LEN; Index += Qh->Interval) {
 | |
|     //
 | |
|     // First QH can't be NULL because we always keep PeriodOne
 | |
|     // heads on the frame list
 | |
|     //
 | |
|     ASSERT (!EHC_LINK_TERMINATED (((UINT32*)Ehc->PeriodFrame)[Index]));
 | |
|     Next  = (EHC_QH*)((UINTN*)Ehc->PeriodFrameHost)[Index];
 | |
|     Prev  = NULL;
 | |
| 
 | |
|     //
 | |
|     // Now, insert the queue head (Qh) into this frame:
 | |
|     // 1. Find a queue head with the same poll interval, just insert
 | |
|     //    Qh after this queue head, then we are done.
 | |
|     //
 | |
|     // 2. Find the position to insert the queue head into:
 | |
|     //      Previous head's interval is bigger than Qh's
 | |
|     //      Next head's interval is less than Qh's
 | |
|     //    Then, insert the Qh between then
 | |
|     //
 | |
|     while (Next->Interval > Qh->Interval) {
 | |
|       Prev  = Next;
 | |
|       Next  = Next->NextQh;
 | |
|     }
 | |
| 
 | |
|     ASSERT (Next != NULL);
 | |
| 
 | |
|     //
 | |
|     // The entry may have been linked into the frame by early insertation.
 | |
|     // For example: if insert a Qh with Qh.Interval == 4, and there is a Qh
 | |
|     // with Qh.Interval == 8 on the frame. If so, we are done with this frame.
 | |
|     // It isn't necessary to compare all the QH with the same interval to
 | |
|     // Qh. This is because if there is other QH with the same interval, Qh
 | |
|     // should has been inserted after that at Frames[0] and at Frames[0] it is
 | |
|     // impossible for (Next == Qh)
 | |
|     //
 | |
|     if (Next == Qh) {
 | |
|       continue;
 | |
|     }
 | |
| 
 | |
|     if (Next->Interval == Qh->Interval) {
 | |
|       //
 | |
|       // If there is a QH with the same interval, it locates at
 | |
|       // Frames[0], and we can simply insert it after this QH. We
 | |
|       // are all done.
 | |
|       //
 | |
|       ASSERT ((Index == 0) && (Qh->NextQh == NULL));
 | |
| 
 | |
|       Prev                    = Next;
 | |
|       Next                    = Next->NextQh;
 | |
| 
 | |
|       Qh->NextQh              = Next;
 | |
|       Prev->NextQh            = Qh;
 | |
| 
 | |
|       Qh->QhHw.HorizonLink    = Prev->QhHw.HorizonLink;
 | |
|       PciAddr = UsbHcGetPciAddressForHostMem (Ehc->MemPool, Qh, sizeof (EHC_QH));
 | |
|       Prev->QhHw.HorizonLink  = QH_LINK (PciAddr, EHC_TYPE_QH, FALSE);
 | |
|       break;
 | |
|     }
 | |
| 
 | |
|     //
 | |
|     // OK, find the right position, insert it in. If Qh's next
 | |
|     // link has already been set, it is in position. This is
 | |
|     // guarranted by 2^n polling interval.
 | |
|     //
 | |
|     if (Qh->NextQh == NULL) {
 | |
|       Qh->NextQh              = Next;
 | |
|       PciAddr = UsbHcGetPciAddressForHostMem (Ehc->MemPool, Next, sizeof (EHC_QH));
 | |
|       Qh->QhHw.HorizonLink    = QH_LINK (PciAddr, EHC_TYPE_QH, FALSE);
 | |
|     }
 | |
| 
 | |
|     PciAddr = UsbHcGetPciAddressForHostMem (Ehc->MemPool, Qh, sizeof (EHC_QH));
 | |
| 
 | |
|     if (Prev == NULL) {
 | |
|       ((UINT32*)Ehc->PeriodFrame)[Index]     = QH_LINK (PciAddr, EHC_TYPE_QH, FALSE);
 | |
|       ((UINTN*)Ehc->PeriodFrameHost)[Index]  = (UINTN)Qh;
 | |
|     } else {
 | |
|       Prev->NextQh            = Qh;
 | |
|       Prev->QhHw.HorizonLink  = QH_LINK (PciAddr, EHC_TYPE_QH, FALSE);
 | |
|     }
 | |
|   }
 | |
| }
 | |
| 
 | |
| 
 | |
| /**
 | |
|   Unlink an interrupt queue head from the periodic
 | |
|   schedule frame list.
 | |
| 
 | |
|   @param  Ehc                   The EHCI device.
 | |
|   @param  Qh                    The queue head to unlink.
 | |
| 
 | |
| **/
 | |
| VOID
 | |
| EhcUnlinkQhFromPeriod (
 | |
|   IN USB2_HC_DEV          *Ehc,
 | |
|   IN EHC_QH               *Qh
 | |
|   )
 | |
| {
 | |
|   UINTN                   Index;
 | |
|   EHC_QH                  *Prev;
 | |
|   EHC_QH                  *This;
 | |
| 
 | |
|   for (Index = 0; Index < EHC_FRAME_LEN; Index += Qh->Interval) {
 | |
|     //
 | |
|     // Frame link can't be NULL because we always keep PeroidOne
 | |
|     // on the frame list
 | |
|     //
 | |
|     ASSERT (!EHC_LINK_TERMINATED (((UINT32*)Ehc->PeriodFrame)[Index]));
 | |
|     This  = (EHC_QH*)((UINTN*)Ehc->PeriodFrameHost)[Index];
 | |
|     Prev  = NULL;
 | |
| 
 | |
|     //
 | |
|     // Walk through the frame's QH list to find the
 | |
|     // queue head to remove
 | |
|     //
 | |
|     while ((This != NULL) && (This != Qh)) {
 | |
|       Prev  = This;
 | |
|       This  = This->NextQh;
 | |
|     }
 | |
| 
 | |
|     //
 | |
|     // Qh may have already been unlinked from this frame
 | |
|     // by early action. See the comments in EhcLinkQhToPeriod.
 | |
|     //
 | |
|     if (This == NULL) {
 | |
|       continue;
 | |
|     }
 | |
| 
 | |
|     if (Prev == NULL) {
 | |
|       //
 | |
|       // Qh is the first entry in the frame
 | |
|       //
 | |
|       ((UINT32*)Ehc->PeriodFrame)[Index] = Qh->QhHw.HorizonLink;
 | |
|       ((UINTN*)Ehc->PeriodFrameHost)[Index] = (UINTN)Qh->NextQh;
 | |
|     } else {
 | |
|       Prev->NextQh            = Qh->NextQh;
 | |
|       Prev->QhHw.HorizonLink  = Qh->QhHw.HorizonLink;
 | |
|     }
 | |
|   }
 | |
| }
 | |
| 
 | |
| 
 | |
| /**
 | |
|   Check the URB's execution result and update the URB's
 | |
|   result accordingly.
 | |
| 
 | |
|   @param  Ehc                   The EHCI device.
 | |
|   @param  Urb                   The URB to check result.
 | |
| 
 | |
|   @return Whether the result of URB transfer is finialized.
 | |
| 
 | |
| **/
 | |
| BOOLEAN
 | |
| EhcCheckUrbResult (
 | |
|   IN  USB2_HC_DEV         *Ehc,
 | |
|   IN  URB                 *Urb
 | |
|   )
 | |
| {
 | |
|   LIST_ENTRY              *Entry;
 | |
|   EHC_QTD                 *Qtd;
 | |
|   QTD_HW                  *QtdHw;
 | |
|   UINT8                   State;
 | |
|   BOOLEAN                 Finished;
 | |
|   EFI_PHYSICAL_ADDRESS    PciAddr;
 | |
| 
 | |
|   ASSERT ((Ehc != NULL) && (Urb != NULL) && (Urb->Qh != NULL));
 | |
| 
 | |
|   Finished        = TRUE;
 | |
|   Urb->Completed  = 0;
 | |
| 
 | |
|   Urb->Result     = EFI_USB_NOERROR;
 | |
| 
 | |
|   if (EhcIsHalt (Ehc) || EhcIsSysError (Ehc)) {
 | |
|     Urb->Result |= EFI_USB_ERR_SYSTEM;
 | |
|     goto ON_EXIT;
 | |
|   }
 | |
| 
 | |
|   EFI_LIST_FOR_EACH (Entry, &Urb->Qh->Qtds) {
 | |
|     Qtd   = EFI_LIST_CONTAINER (Entry, EHC_QTD, QtdList);
 | |
|     QtdHw = &Qtd->QtdHw;
 | |
|     State = (UINT8) QtdHw->Status;
 | |
| 
 | |
|     if (EHC_BIT_IS_SET (State, QTD_STAT_HALTED)) {
 | |
|       //
 | |
|       // EHCI will halt the queue head when met some error.
 | |
|       // If it is halted, the result of URB is finialized.
 | |
|       //
 | |
|       if ((State & QTD_STAT_ERR_MASK) == 0) {
 | |
|         Urb->Result |= EFI_USB_ERR_STALL;
 | |
|       }
 | |
| 
 | |
|       if (EHC_BIT_IS_SET (State, QTD_STAT_BABBLE_ERR)) {
 | |
|         Urb->Result |= EFI_USB_ERR_BABBLE;
 | |
|       }
 | |
| 
 | |
|       if (EHC_BIT_IS_SET (State, QTD_STAT_BUFF_ERR)) {
 | |
|         Urb->Result |= EFI_USB_ERR_BUFFER;
 | |
|       }
 | |
| 
 | |
|       if (EHC_BIT_IS_SET (State, QTD_STAT_TRANS_ERR) && (QtdHw->ErrCnt == 0)) {
 | |
|         Urb->Result |= EFI_USB_ERR_TIMEOUT;
 | |
|       }
 | |
| 
 | |
|       Finished = TRUE;
 | |
|       goto ON_EXIT;
 | |
| 
 | |
|     } else if (EHC_BIT_IS_SET (State, QTD_STAT_ACTIVE)) {
 | |
|       //
 | |
|       // The QTD is still active, no need to check furthur.
 | |
|       //
 | |
|       Urb->Result |= EFI_USB_ERR_NOTEXECUTE;
 | |
| 
 | |
|       Finished = FALSE;
 | |
|       goto ON_EXIT;
 | |
| 
 | |
|     } else {
 | |
|       //
 | |
|       // This QTD is finished OK or met short packet read. Update the
 | |
|       // transfer length if it isn't a setup.
 | |
|       //
 | |
|       if (QtdHw->Pid != QTD_PID_SETUP) {
 | |
|         Urb->Completed += Qtd->DataLen - QtdHw->TotalBytes;
 | |
|       }
 | |
| 
 | |
|       if ((QtdHw->TotalBytes != 0) && (QtdHw->Pid == QTD_PID_INPUT)) {
 | |
|         EhcDumpQh (Urb->Qh, "Short packet read", FALSE);
 | |
| 
 | |
|         //
 | |
|         // Short packet read condition. If it isn't a setup transfer,
 | |
|         // no need to check furthur: the queue head will halt at the
 | |
|         // ShortReadStop. If it is a setup transfer, need to check the
 | |
|         // Status Stage of the setup transfer to get the finial result
 | |
|         //
 | |
|         PciAddr = UsbHcGetPciAddressForHostMem (Ehc->MemPool, Ehc->ShortReadStop, sizeof (EHC_QTD));
 | |
|         if (QtdHw->AltNext == QTD_LINK (PciAddr, FALSE)) {
 | |
|           DEBUG ((EFI_D_VERBOSE, "EhcCheckUrbResult: Short packet read, break\n"));
 | |
| 
 | |
|           Finished = TRUE;
 | |
|           goto ON_EXIT;
 | |
|         }
 | |
| 
 | |
|         DEBUG ((EFI_D_VERBOSE, "EhcCheckUrbResult: Short packet read, continue\n"));
 | |
|       }
 | |
|     }
 | |
|   }
 | |
| 
 | |
| ON_EXIT:
 | |
|   //
 | |
|   // Return the data toggle set by EHCI hardware, bulk and interrupt
 | |
|   // transfer will use this to initialize the next transaction. For
 | |
|   // Control transfer, it always start a new data toggle sequence for
 | |
|   // new transfer.
 | |
|   //
 | |
|   // NOTICE: don't move DT update before the loop, otherwise there is
 | |
|   // a race condition that DT is wrong.
 | |
|   //
 | |
|   Urb->DataToggle = (UINT8) Urb->Qh->QhHw.DataToggle;
 | |
| 
 | |
|   return Finished;
 | |
| }
 | |
| 
 | |
| 
 | |
| /**
 | |
|   Execute the transfer by polling the URB. This is a synchronous operation.
 | |
| 
 | |
|   @param  Ehc               The EHCI device.
 | |
|   @param  Urb               The URB to execute.
 | |
|   @param  TimeOut           The time to wait before abort, in millisecond.
 | |
| 
 | |
|   @return EFI_DEVICE_ERROR  The transfer failed due to transfer error.
 | |
|   @return EFI_TIMEOUT       The transfer failed due to time out.
 | |
|   @return EFI_SUCCESS       The transfer finished OK.
 | |
| 
 | |
| **/
 | |
| EFI_STATUS
 | |
| EhcExecTransfer (
 | |
|   IN  USB2_HC_DEV         *Ehc,
 | |
|   IN  URB                 *Urb,
 | |
|   IN  UINTN               TimeOut
 | |
|   )
 | |
| {
 | |
|   EFI_STATUS              Status;
 | |
|   UINTN                   Index;
 | |
|   UINTN                   Loop;
 | |
|   BOOLEAN                 Finished;
 | |
|   BOOLEAN                 InfiniteLoop;
 | |
| 
 | |
|   Status       = EFI_SUCCESS;
 | |
|   Loop         = TimeOut * EHC_1_MILLISECOND;
 | |
|   Finished     = FALSE;
 | |
|   InfiniteLoop = FALSE;
 | |
| 
 | |
|   //
 | |
|   // According to UEFI spec section 16.2.4, If Timeout is 0, then the caller
 | |
|   // must wait for the function to be completed until EFI_SUCCESS or EFI_DEVICE_ERROR
 | |
|   // is returned.
 | |
|   //
 | |
|   if (TimeOut == 0) {
 | |
|     InfiniteLoop = TRUE;
 | |
|   }
 | |
| 
 | |
|   for (Index = 0; InfiniteLoop || (Index < Loop); Index++) {
 | |
|     Finished = EhcCheckUrbResult (Ehc, Urb);
 | |
| 
 | |
|     if (Finished) {
 | |
|       break;
 | |
|     }
 | |
| 
 | |
|     gBS->Stall (EHC_1_MICROSECOND);
 | |
|   }
 | |
| 
 | |
|   if (!Finished) {
 | |
|     DEBUG ((EFI_D_ERROR, "EhcExecTransfer: transfer not finished in %dms\n", (UINT32)TimeOut));
 | |
|     EhcDumpQh (Urb->Qh, NULL, FALSE);
 | |
| 
 | |
|     Status = EFI_TIMEOUT;
 | |
| 
 | |
|   } else if (Urb->Result != EFI_USB_NOERROR) {
 | |
|     DEBUG ((EFI_D_ERROR, "EhcExecTransfer: transfer failed with %x\n", Urb->Result));
 | |
|     EhcDumpQh (Urb->Qh, NULL, FALSE);
 | |
| 
 | |
|     Status = EFI_DEVICE_ERROR;
 | |
|   }
 | |
| 
 | |
|   return Status;
 | |
| }
 | |
| 
 | |
| 
 | |
| /**
 | |
|   Delete a single asynchronous interrupt transfer for
 | |
|   the device and endpoint.
 | |
| 
 | |
|   @param  Ehc                   The EHCI device.
 | |
|   @param  DevAddr               The address of the target device.
 | |
|   @param  EpNum                 The endpoint of the target.
 | |
|   @param  DataToggle            Return the next data toggle to use.
 | |
| 
 | |
|   @retval EFI_SUCCESS           An asynchronous transfer is removed.
 | |
|   @retval EFI_NOT_FOUND         No transfer for the device is found.
 | |
| 
 | |
| **/
 | |
| EFI_STATUS
 | |
| EhciDelAsyncIntTransfer (
 | |
|   IN  USB2_HC_DEV         *Ehc,
 | |
|   IN  UINT8               DevAddr,
 | |
|   IN  UINT8               EpNum,
 | |
|   OUT UINT8               *DataToggle
 | |
|   )
 | |
| {
 | |
|   LIST_ENTRY              *Entry;
 | |
|   LIST_ENTRY              *Next;
 | |
|   URB                     *Urb;
 | |
|   EFI_USB_DATA_DIRECTION  Direction;
 | |
| 
 | |
|   Direction = (((EpNum & 0x80) != 0) ? EfiUsbDataIn : EfiUsbDataOut);
 | |
|   EpNum    &= 0x0F;
 | |
| 
 | |
|   EFI_LIST_FOR_EACH_SAFE (Entry, Next, &Ehc->AsyncIntTransfers) {
 | |
|     Urb = EFI_LIST_CONTAINER (Entry, URB, UrbList);
 | |
| 
 | |
|     if ((Urb->Ep.DevAddr == DevAddr) && (Urb->Ep.EpAddr == EpNum) &&
 | |
|         (Urb->Ep.Direction == Direction)) {
 | |
|       //
 | |
|       // Check the URB status to retrieve the next data toggle
 | |
|       // from the associated queue head.
 | |
|       //
 | |
|       EhcCheckUrbResult (Ehc, Urb);
 | |
|       *DataToggle = Urb->DataToggle;
 | |
| 
 | |
|       EhcUnlinkQhFromPeriod (Ehc, Urb->Qh);
 | |
|       RemoveEntryList (&Urb->UrbList);
 | |
| 
 | |
|       gBS->FreePool (Urb->Data);
 | |
|       EhcFreeUrb (Ehc, Urb);
 | |
|       return EFI_SUCCESS;
 | |
|     }
 | |
|   }
 | |
| 
 | |
|   return EFI_NOT_FOUND;
 | |
| }
 | |
| 
 | |
| 
 | |
| /**
 | |
|   Remove all the asynchronous interrutp transfers.
 | |
| 
 | |
|   @param  Ehc                   The EHCI device.
 | |
| 
 | |
| **/
 | |
| VOID
 | |
| EhciDelAllAsyncIntTransfers (
 | |
|   IN USB2_HC_DEV          *Ehc
 | |
|   )
 | |
| {
 | |
|   LIST_ENTRY              *Entry;
 | |
|   LIST_ENTRY              *Next;
 | |
|   URB                     *Urb;
 | |
| 
 | |
|   EFI_LIST_FOR_EACH_SAFE (Entry, Next, &Ehc->AsyncIntTransfers) {
 | |
|     Urb = EFI_LIST_CONTAINER (Entry, URB, UrbList);
 | |
| 
 | |
|     EhcUnlinkQhFromPeriod (Ehc, Urb->Qh);
 | |
|     RemoveEntryList (&Urb->UrbList);
 | |
| 
 | |
|     gBS->FreePool (Urb->Data);
 | |
|     EhcFreeUrb (Ehc, Urb);
 | |
|   }
 | |
| }
 | |
| 
 | |
| 
 | |
| /**
 | |
|   Flush data from PCI controller specific address to mapped system
 | |
|   memory address.
 | |
| 
 | |
|   @param  Ehc                The EHCI device.
 | |
|   @param  Urb                The URB to unmap.
 | |
| 
 | |
|   @retval EFI_SUCCESS        Success to flush data to mapped system memory.
 | |
|   @retval EFI_DEVICE_ERROR   Fail to flush data to mapped system memory.
 | |
| 
 | |
| **/
 | |
| EFI_STATUS
 | |
| EhcFlushAsyncIntMap (
 | |
|   IN  USB2_HC_DEV         *Ehc,
 | |
|   IN  URB                 *Urb
 | |
|   )
 | |
| {
 | |
|   EFI_STATUS                    Status;
 | |
|   EFI_PHYSICAL_ADDRESS          PhyAddr;
 | |
|   EFI_PCI_IO_PROTOCOL_OPERATION MapOp;
 | |
|   EFI_PCI_IO_PROTOCOL           *PciIo;
 | |
|   UINTN                         Len;
 | |
|   VOID                          *Map;
 | |
| 
 | |
|   PciIo = Ehc->PciIo;
 | |
|   Len   = Urb->DataLen;
 | |
| 
 | |
|   if (Urb->Ep.Direction == EfiUsbDataIn) {
 | |
|     MapOp = EfiPciIoOperationBusMasterWrite;
 | |
|   } else {
 | |
|     MapOp = EfiPciIoOperationBusMasterRead;
 | |
|   }
 | |
| 
 | |
|   Status = PciIo->Unmap (PciIo, Urb->DataMap);
 | |
|   if (EFI_ERROR (Status)) {
 | |
|     goto ON_ERROR;
 | |
|   }
 | |
| 
 | |
|   Urb->DataMap = NULL;
 | |
| 
 | |
|   Status = PciIo->Map (PciIo, MapOp, Urb->Data, &Len, &PhyAddr, &Map);
 | |
|   if (EFI_ERROR (Status) || (Len != Urb->DataLen)) {
 | |
|     goto ON_ERROR;
 | |
|   }
 | |
| 
 | |
|   Urb->DataPhy  = (VOID *) ((UINTN) PhyAddr);
 | |
|   Urb->DataMap  = Map;
 | |
|   return EFI_SUCCESS;
 | |
| 
 | |
| ON_ERROR:
 | |
|   return EFI_DEVICE_ERROR;
 | |
| }
 | |
| 
 | |
| 
 | |
| /**
 | |
|   Update the queue head for next round of asynchronous transfer.
 | |
| 
 | |
|   @param  Ehc                   The EHCI device.
 | |
|   @param  Urb                   The URB to update.
 | |
| 
 | |
| **/
 | |
| VOID
 | |
| EhcUpdateAsyncRequest (
 | |
|   IN  USB2_HC_DEV         *Ehc,
 | |
|   IN URB                  *Urb
 | |
|   )
 | |
| {
 | |
|   LIST_ENTRY              *Entry;
 | |
|   EHC_QTD                 *FirstQtd;
 | |
|   QH_HW                   *QhHw;
 | |
|   EHC_QTD                 *Qtd;
 | |
|   QTD_HW                  *QtdHw;
 | |
|   UINTN                   Index;
 | |
|   EFI_PHYSICAL_ADDRESS    PciAddr;
 | |
| 
 | |
|   Qtd = NULL;
 | |
| 
 | |
|   if (Urb->Result == EFI_USB_NOERROR) {
 | |
|     FirstQtd = NULL;
 | |
| 
 | |
|     EFI_LIST_FOR_EACH (Entry, &Urb->Qh->Qtds) {
 | |
|       Qtd = EFI_LIST_CONTAINER (Entry, EHC_QTD, QtdList);
 | |
| 
 | |
|       if (FirstQtd == NULL) {
 | |
|         FirstQtd = Qtd;
 | |
|       }
 | |
| 
 | |
|       //
 | |
|       // Update the QTD for next round of transfer. Host control
 | |
|       // may change dt/Total Bytes to Transfer/C_Page/Cerr/Status/
 | |
|       // Current Offset. These fields need to be updated. DT isn't
 | |
|       // used by interrupt transfer. It uses DT in queue head.
 | |
|       // Current Offset is in Page[0], only need to reset Page[0]
 | |
|       // to initial data buffer.
 | |
|       //
 | |
|       QtdHw             = &Qtd->QtdHw;
 | |
|       QtdHw->Status     = QTD_STAT_ACTIVE;
 | |
|       QtdHw->ErrCnt     = QTD_MAX_ERR;
 | |
|       QtdHw->CurPage    = 0;
 | |
|       QtdHw->TotalBytes = (UINT32) Qtd->DataLen;
 | |
|       //
 | |
|       // calculate physical address by offset.
 | |
|       //
 | |
|       PciAddr = (UINTN)Urb->DataPhy + ((UINTN)Qtd->Data - (UINTN)Urb->Data); 
 | |
|       QtdHw->Page[0]    = EHC_LOW_32BIT (PciAddr);
 | |
|       QtdHw->PageHigh[0]= EHC_HIGH_32BIT (PciAddr);
 | |
|     }
 | |
| 
 | |
|     //
 | |
|     // Update QH for next round of transfer. Host control only
 | |
|     // touch the fields in transfer overlay area. Only need to
 | |
|     // zero out the overlay area and set NextQtd to the first
 | |
|     // QTD. DateToggle bit is left untouched.
 | |
|     //
 | |
|     QhHw              = &Urb->Qh->QhHw;
 | |
|     QhHw->CurQtd      = QTD_LINK (0, TRUE);
 | |
|     QhHw->AltQtd      = 0;
 | |
| 
 | |
|     QhHw->Status      = 0;
 | |
|     QhHw->Pid         = 0;
 | |
|     QhHw->ErrCnt      = 0;
 | |
|     QhHw->CurPage     = 0;
 | |
|     QhHw->Ioc         = 0;
 | |
|     QhHw->TotalBytes  = 0;
 | |
| 
 | |
|     for (Index = 0; Index < 5; Index++) {
 | |
|       QhHw->Page[Index]     = 0;
 | |
|       QhHw->PageHigh[Index] = 0;
 | |
|     }
 | |
| 
 | |
|     PciAddr = UsbHcGetPciAddressForHostMem (Ehc->MemPool, FirstQtd, sizeof (EHC_QTD));
 | |
|     QhHw->NextQtd = QTD_LINK (PciAddr, FALSE);
 | |
|   }
 | |
| 
 | |
|   return ;
 | |
| }
 | |
| 
 | |
| 
 | |
| /**
 | |
|   Interrupt transfer periodic check handler.
 | |
| 
 | |
|   @param  Event                 Interrupt event.
 | |
|   @param  Context               Pointer to USB2_HC_DEV.
 | |
| 
 | |
| **/
 | |
| VOID
 | |
| EFIAPI
 | |
| EhcMonitorAsyncRequests (
 | |
|   IN EFI_EVENT            Event,
 | |
|   IN VOID                 *Context
 | |
|   )
 | |
| {
 | |
|   USB2_HC_DEV             *Ehc;
 | |
|   EFI_TPL                 OldTpl;
 | |
|   LIST_ENTRY              *Entry;
 | |
|   LIST_ENTRY              *Next;
 | |
|   BOOLEAN                 Finished;
 | |
|   UINT8                   *ProcBuf;
 | |
|   URB                     *Urb;
 | |
|   EFI_STATUS              Status;
 | |
| 
 | |
|   OldTpl  = gBS->RaiseTPL (EHC_TPL);
 | |
|   Ehc     = (USB2_HC_DEV *) Context;
 | |
| 
 | |
|   EFI_LIST_FOR_EACH_SAFE (Entry, Next, &Ehc->AsyncIntTransfers) {
 | |
|     Urb = EFI_LIST_CONTAINER (Entry, URB, UrbList);
 | |
| 
 | |
|     //
 | |
|     // Check the result of URB execution. If it is still
 | |
|     // active, check the next one.
 | |
|     //
 | |
|     Finished = EhcCheckUrbResult (Ehc, Urb);
 | |
| 
 | |
|     if (!Finished) {
 | |
|       continue;
 | |
|     }
 | |
| 
 | |
|     //
 | |
|     // Flush any PCI posted write transactions from a PCI host
 | |
|     // bridge to system memory.
 | |
|     //
 | |
|     Status = EhcFlushAsyncIntMap (Ehc, Urb);
 | |
|     if (EFI_ERROR (Status)) {
 | |
|       DEBUG ((EFI_D_ERROR, "EhcMonitorAsyncRequests: Fail to Flush AsyncInt Mapped Memeory\n"));
 | |
|     }
 | |
| 
 | |
|     //
 | |
|     // Allocate a buffer then copy the transferred data for user.
 | |
|     // If failed to allocate the buffer, update the URB for next
 | |
|     // round of transfer. Ignore the data of this round.
 | |
|     //
 | |
|     ProcBuf = NULL;
 | |
| 
 | |
|     if (Urb->Result == EFI_USB_NOERROR) {
 | |
|       ASSERT (Urb->Completed <= Urb->DataLen);
 | |
| 
 | |
|       ProcBuf = AllocatePool (Urb->Completed);
 | |
| 
 | |
|       if (ProcBuf == NULL) {
 | |
|         EhcUpdateAsyncRequest (Ehc, Urb);
 | |
|         continue;
 | |
|       }
 | |
| 
 | |
|       CopyMem (ProcBuf, Urb->Data, Urb->Completed);
 | |
|     }
 | |
| 
 | |
|     EhcUpdateAsyncRequest (Ehc, Urb);
 | |
| 
 | |
|     //
 | |
|     // Leave error recovery to its related device driver. A
 | |
|     // common case of the error recovery is to re-submit the
 | |
|     // interrupt transfer which is linked to the head of the
 | |
|     // list. This function scans from head to tail. So the
 | |
|     // re-submitted interrupt transfer's callback function
 | |
|     // will not be called again in this round. Don't touch this
 | |
|     // URB after the callback, it may have been removed by the
 | |
|     // callback.
 | |
|     //
 | |
|     if (Urb->Callback != NULL) {
 | |
|       //
 | |
|       // Restore the old TPL, USB bus maybe connect device in
 | |
|       // his callback. Some drivers may has a lower TPL restriction.
 | |
|       //
 | |
|       gBS->RestoreTPL (OldTpl);
 | |
|       (Urb->Callback) (ProcBuf, Urb->Completed, Urb->Context, Urb->Result);
 | |
|       OldTpl = gBS->RaiseTPL (EHC_TPL);
 | |
|     }
 | |
| 
 | |
|     if (ProcBuf != NULL) {
 | |
|       FreePool (ProcBuf);
 | |
|     }
 | |
|   }
 | |
| 
 | |
|   gBS->RestoreTPL (OldTpl);
 | |
| }
 |