Index: chromeos/drivers/ath6kl/htc2/htc.c |
diff --git a/chromeos/drivers/ath6kl/htc2/htc.c b/chromeos/drivers/ath6kl/htc2/htc.c |
new file mode 100644 |
index 0000000000000000000000000000000000000000..0068685972a98714e541b574b86960489015ac65 |
--- /dev/null |
+++ b/chromeos/drivers/ath6kl/htc2/htc.c |
@@ -0,0 +1,558 @@ |
+//------------------------------------------------------------------------------ |
+// <copyright file="htc.c" company="Atheros"> |
+// Copyright (c) 2007-2008 Atheros Corporation. All rights reserved. |
+// |
+// This program is free software; you can redistribute it and/or modify |
+// it under the terms of the GNU General Public License version 2 as |
+// published by the Free Software Foundation; |
+// |
+// Software distributed under the License is distributed on an "AS |
+// IS" basis, WITHOUT WARRANTY OF ANY KIND, either express or |
+// implied. See the License for the specific language governing |
+// rights and limitations under the License. |
+// |
+// |
+//------------------------------------------------------------------------------ |
+//============================================================================== |
+// Author(s): ="Atheros" |
+//============================================================================== |
+#include "htc_internal.h" |
+ |
+#ifdef DEBUG |
+static ATH_DEBUG_MASK_DESCRIPTION g_HTCDebugDescription[] = { |
+ { ATH_DEBUG_SEND , "Send"}, |
+ { ATH_DEBUG_RECV , "Recv"}, |
+ { ATH_DEBUG_SYNC , "Sync"}, |
+ { ATH_DEBUG_DUMP , "Dump Data (RX or TX)"}, |
+ { ATH_DEBUG_IRQ , "Interrupt Processing"} |
+}; |
+ |
+ATH_DEBUG_INSTANTIATE_MODULE_VAR(htc, |
+ "htc", |
+ "Host Target Communications", |
+ ATH_DEBUG_MASK_DEFAULTS, |
+ ATH_DEBUG_DESCRIPTION_COUNT(g_HTCDebugDescription), |
+ g_HTCDebugDescription); |
+ |
+#endif |
+ |
+static void HTCReportFailure(void *Context); |
+static void ResetEndpointStates(HTC_TARGET *target); |
+ |
+void HTCFreeControlBuffer(HTC_TARGET *target, HTC_PACKET *pPacket, HTC_PACKET_QUEUE *pList) |
+{ |
+ LOCK_HTC(target); |
+ HTC_PACKET_ENQUEUE(pList,pPacket); |
+ UNLOCK_HTC(target); |
+} |
+ |
+HTC_PACKET *HTCAllocControlBuffer(HTC_TARGET *target, HTC_PACKET_QUEUE *pList) |
+{ |
+ HTC_PACKET *pPacket; |
+ |
+ LOCK_HTC(target); |
+ pPacket = HTC_PACKET_DEQUEUE(pList); |
+ UNLOCK_HTC(target); |
+ |
+ return pPacket; |
+} |
+ |
+/* cleanup the HTC instance */ |
+static void HTCCleanup(HTC_TARGET *target) |
+{ |
+ A_INT32 i; |
+ |
+ DevCleanup(&target->Device); |
+ |
+ for (i = 0;i < NUM_CONTROL_BUFFERS;i++) { |
+ if (target->HTCControlBuffers[i].Buffer) { |
+ A_FREE(target->HTCControlBuffers[i].Buffer); |
+ } |
+ } |
+ |
+ if (A_IS_MUTEX_VALID(&target->HTCLock)) { |
+ A_MUTEX_DELETE(&target->HTCLock); |
+ } |
+ |
+ if (A_IS_MUTEX_VALID(&target->HTCRxLock)) { |
+ A_MUTEX_DELETE(&target->HTCRxLock); |
+ } |
+ |
+ if (A_IS_MUTEX_VALID(&target->HTCTxLock)) { |
+ A_MUTEX_DELETE(&target->HTCTxLock); |
+ } |
+ /* free our instance */ |
+ A_FREE(target); |
+} |
+ |
+/* registered target arrival callback from the HIF layer */ |
+HTC_HANDLE HTCCreate(void *hif_handle, HTC_INIT_INFO *pInfo) |
+{ |
+ HTC_TARGET *target = NULL; |
+ A_STATUS status = A_OK; |
+ int i; |
+ A_UINT32 ctrl_bufsz; |
+ A_UINT32 blocksizes[HTC_MAILBOX_NUM_MAX]; |
+ |
+ AR_DEBUG_PRINTF(ATH_DEBUG_TRC, ("HTCCreate - Enter\n")); |
+ |
+ A_REGISTER_MODULE_DEBUG_INFO(htc); |
+ |
+ do { |
+ |
+ /* allocate target memory */ |
+ if ((target = (HTC_TARGET *)A_MALLOC(sizeof(HTC_TARGET))) == NULL) { |
+ AR_DEBUG_PRINTF(ATH_DEBUG_ERR, ("Unable to allocate memory\n")); |
+ status = A_ERROR; |
+ break; |
+ } |
+ |
+ A_MEMZERO(target, sizeof(HTC_TARGET)); |
+ A_MUTEX_INIT(&target->HTCLock); |
+ A_MUTEX_INIT(&target->HTCRxLock); |
+ A_MUTEX_INIT(&target->HTCTxLock); |
+ INIT_HTC_PACKET_QUEUE(&target->ControlBufferTXFreeList); |
+ INIT_HTC_PACKET_QUEUE(&target->ControlBufferRXFreeList); |
+ |
+ /* give device layer the hif device handle */ |
+ target->Device.HIFDevice = hif_handle; |
+ /* give the device layer our context (for event processing) |
+ * the device layer will register it's own context with HIF |
+ * so we need to set this so we can fetch it in the target remove handler */ |
+ target->Device.HTCContext = target; |
+ /* set device layer target failure callback */ |
+ target->Device.TargetFailureCallback = HTCReportFailure; |
+ /* set device layer recv message pending callback */ |
+ target->Device.MessagePendingCallback = HTCRecvMessagePendingHandler; |
+ target->EpWaitingForBuffers = ENDPOINT_MAX; |
+ |
+ A_MEMCPY(&target->HTCInitInfo,pInfo,sizeof(HTC_INIT_INFO)); |
+ |
+ ResetEndpointStates(target); |
+ |
+ /* setup device layer */ |
+ status = DevSetup(&target->Device); |
+ |
+ if (A_FAILED(status)) { |
+ break; |
+ } |
+ |
+ |
+ /* get the block sizes */ |
+ status = HIFConfigureDevice(hif_handle, HIF_DEVICE_GET_MBOX_BLOCK_SIZE, |
+ blocksizes, sizeof(blocksizes)); |
+ if (A_FAILED(status)) { |
+ AR_DEBUG_PRINTF(ATH_DEBUG_ERR,("Failed to get block size info from HIF layer...\n")); |
+ break; |
+ } |
+ |
+ /* Set the control buffer size based on the block size */ |
+ if (blocksizes[1] > HTC_MAX_CONTROL_MESSAGE_LENGTH) { |
+ ctrl_bufsz = blocksizes[1] + HTC_HDR_LENGTH; |
+ } else { |
+ ctrl_bufsz = HTC_MAX_CONTROL_MESSAGE_LENGTH + HTC_HDR_LENGTH; |
+ } |
+ for (i = 0;i < NUM_CONTROL_BUFFERS;i++) { |
+ target->HTCControlBuffers[i].Buffer = A_MALLOC(ctrl_bufsz); |
+ if (target->HTCControlBuffers[i].Buffer == NULL) { |
+ AR_DEBUG_PRINTF(ATH_DEBUG_ERR, ("Unable to allocate memory\n")); |
+ status = A_ERROR; |
+ break; |
+ } |
+ } |
+ |
+ if (A_FAILED(status)) { |
+ break; |
+ } |
+ |
+ /* carve up buffers/packets for control messages */ |
+ for (i = 0; i < NUM_CONTROL_RX_BUFFERS; i++) { |
+ HTC_PACKET *pControlPacket; |
+ pControlPacket = &target->HTCControlBuffers[i].HtcPacket; |
+ SET_HTC_PACKET_INFO_RX_REFILL(pControlPacket, |
+ target, |
+ target->HTCControlBuffers[i].Buffer, |
+ ctrl_bufsz, |
+ ENDPOINT_0); |
+ HTC_FREE_CONTROL_RX(target,pControlPacket); |
+ } |
+ |
+ for (;i < NUM_CONTROL_BUFFERS;i++) { |
+ HTC_PACKET *pControlPacket; |
+ pControlPacket = &target->HTCControlBuffers[i].HtcPacket; |
+ INIT_HTC_PACKET_INFO(pControlPacket, |
+ target->HTCControlBuffers[i].Buffer, |
+ ctrl_bufsz); |
+ HTC_FREE_CONTROL_TX(target,pControlPacket); |
+ } |
+ |
+ } while (FALSE); |
+ |
+ if (A_FAILED(status)) { |
+ if (target != NULL) { |
+ HTCCleanup(target); |
+ target = NULL; |
+ } |
+ } |
+ |
+ AR_DEBUG_PRINTF(ATH_DEBUG_TRC, ("HTCCreate - Exit\n")); |
+ |
+ return target; |
+} |
+ |
+void HTCDestroy(HTC_HANDLE HTCHandle) |
+{ |
+ HTC_TARGET *target = GET_HTC_TARGET_FROM_HANDLE(HTCHandle); |
+ AR_DEBUG_PRINTF(ATH_DEBUG_TRC, ("+HTCDestroy .. Destroying :0x%X \n",(A_UINT32)target)); |
+ HTCCleanup(target); |
+ AR_DEBUG_PRINTF(ATH_DEBUG_TRC, ("-HTCDestroy \n")); |
+} |
+ |
+/* get the low level HIF device for the caller , the caller may wish to do low level |
+ * HIF requests */ |
+void *HTCGetHifDevice(HTC_HANDLE HTCHandle) |
+{ |
+ HTC_TARGET *target = GET_HTC_TARGET_FROM_HANDLE(HTCHandle); |
+ return target->Device.HIFDevice; |
+} |
+ |
+/* wait for the target to arrive (sends HTC Ready message) |
+ * this operation is fully synchronous and the message is polled for */ |
+A_STATUS HTCWaitTarget(HTC_HANDLE HTCHandle) |
+{ |
+ HTC_TARGET *target = GET_HTC_TARGET_FROM_HANDLE(HTCHandle); |
+ A_STATUS status; |
+ HTC_PACKET *pPacket = NULL; |
+ HTC_READY_EX_MSG *pRdyMsg; |
+ HTC_SERVICE_CONNECT_REQ connect; |
+ HTC_SERVICE_CONNECT_RESP resp; |
+ |
+ AR_DEBUG_PRINTF(ATH_DEBUG_TRC, ("HTCWaitTarget - Enter (target:0x%X) \n", (A_UINT32)target)); |
+ |
+ do { |
+ |
+#ifdef MBOXHW_UNIT_TEST |
+ |
+ status = DoMboxHWTest(&target->Device); |
+ |
+ if (status != A_OK) { |
+ break; |
+ } |
+ |
+#endif |
+ |
+ /* we should be getting 1 control message that the target is ready */ |
+ status = HTCWaitforControlMessage(target, &pPacket); |
+ |
+ if (A_FAILED(status)) { |
+ AR_DEBUG_PRINTF(ATH_DEBUG_ERR, (" Target Not Available!!\n")); |
+ break; |
+ } |
+ |
+ /* we controlled the buffer creation so it has to be properly aligned */ |
+ pRdyMsg = (HTC_READY_EX_MSG *)pPacket->pBuffer; |
+ |
+ if ((pRdyMsg->Version2_0_Info.MessageID != HTC_MSG_READY_ID) || |
+ (pPacket->ActualLength < sizeof(HTC_READY_MSG))) { |
+ /* this message is not valid */ |
+ AR_DEBUG_ASSERT(FALSE); |
+ status = A_EPROTO; |
+ break; |
+ } |
+ |
+ |
+ if (pRdyMsg->Version2_0_Info.CreditCount == 0 || pRdyMsg->Version2_0_Info.CreditSize == 0) { |
+ /* this message is not valid */ |
+ AR_DEBUG_ASSERT(FALSE); |
+ status = A_EPROTO; |
+ break; |
+ } |
+ |
+ target->TargetCredits = pRdyMsg->Version2_0_Info.CreditCount; |
+ target->TargetCreditSize = pRdyMsg->Version2_0_Info.CreditSize; |
+ |
+ AR_DEBUG_PRINTF(ATH_DEBUG_WARN, (" Target Ready: credits: %d credit size: %d\n", |
+ target->TargetCredits, target->TargetCreditSize)); |
+ |
+ /* check if this is an extended ready message */ |
+ if (pPacket->ActualLength >= sizeof(HTC_READY_EX_MSG)) { |
+ /* this is an extended message */ |
+ target->HTCTargetVersion = pRdyMsg->HTCVersion; |
+ target->MaxMsgPerBundle = pRdyMsg->MaxMsgsPerHTCBundle; |
+ } else { |
+ /* legacy */ |
+ target->HTCTargetVersion = HTC_VERSION_2P0; |
+ target->MaxMsgPerBundle = 0; |
+ } |
+ |
+#ifdef HTC_FORCE_LEGACY_2P0 |
+ /* for testing and comparison...*/ |
+ target->HTCTargetVersion = HTC_VERSION_2P0; |
+ target->MaxMsgPerBundle = 0; |
+#endif |
+ |
+ AR_DEBUG_PRINTF(ATH_DEBUG_TRC, |
+ ("Using HTC Protocol Version : %s (%d)\n ", |
+ (target->HTCTargetVersion == HTC_VERSION_2P0) ? "2.0" : ">= 2.1", |
+ target->HTCTargetVersion)); |
+ |
+ if (target->MaxMsgPerBundle > 0) { |
+ /* limit what HTC can handle */ |
+ target->MaxMsgPerBundle = min(HTC_HOST_MAX_MSG_PER_BUNDLE, target->MaxMsgPerBundle); |
+ /* target supports message bundling, setup device layer */ |
+ if (A_FAILED(DevSetupMsgBundling(&target->Device,target->MaxMsgPerBundle))) { |
+ /* device layer can't handle bundling */ |
+ target->MaxMsgPerBundle = 0; |
+ } else { |
+ /* limit bundle what the device layer can handle */ |
+ target->MaxMsgPerBundle = min(DEV_GET_MAX_MSG_PER_BUNDLE(&target->Device), |
+ target->MaxMsgPerBundle); |
+ } |
+ } |
+ |
+ if (target->MaxMsgPerBundle > 0) { |
+ AR_DEBUG_PRINTF(ATH_DEBUG_TRC, |
+ (" HTC bundling allowed. Max Msg Per HTC Bundle: %d\n", target->MaxMsgPerBundle)); |
+ target->SendBundlingEnabled = TRUE; |
+ target->RecvBundlingEnabled = TRUE; |
+ if (!DEV_IS_LEN_BLOCK_ALIGNED(&target->Device,target->TargetCreditSize)) { |
+ AR_DEBUG_PRINTF(ATH_DEBUG_WARN, ("*** Credit size: %d is not block aligned! Disabling send bundling \n", |
+ target->TargetCreditSize)); |
+ /* disallow send bundling since the credit size is not aligned to a block size |
+ * the I/O block padding will spill into the next credit buffer which is fatal */ |
+ target->SendBundlingEnabled = FALSE; |
+ } |
+ } |
+ |
+ /* setup our pseudo HTC control endpoint connection */ |
+ A_MEMZERO(&connect,sizeof(connect)); |
+ A_MEMZERO(&resp,sizeof(resp)); |
+ connect.EpCallbacks.pContext = target; |
+ connect.EpCallbacks.EpTxComplete = HTCControlTxComplete; |
+ connect.EpCallbacks.EpRecv = HTCControlRecv; |
+ connect.EpCallbacks.EpRecvRefill = NULL; /* not needed */ |
+ connect.EpCallbacks.EpSendFull = NULL; /* not nedded */ |
+ connect.MaxSendQueueDepth = NUM_CONTROL_BUFFERS; |
+ connect.ServiceID = HTC_CTRL_RSVD_SVC; |
+ |
+ /* connect fake service */ |
+ status = HTCConnectService((HTC_HANDLE)target, |
+ &connect, |
+ &resp); |
+ |
+ if (!A_FAILED(status)) { |
+ break; |
+ } |
+ |
+ } while (FALSE); |
+ |
+ if (pPacket != NULL) { |
+ HTC_FREE_CONTROL_RX(target,pPacket); |
+ } |
+ |
+ AR_DEBUG_PRINTF(ATH_DEBUG_TRC, ("HTCWaitTarget - Exit\n")); |
+ |
+ return status; |
+} |
+ |
+ |
+ |
+/* Start HTC, enable interrupts and let the target know host has finished setup */ |
+A_STATUS HTCStart(HTC_HANDLE HTCHandle) |
+{ |
+ HTC_TARGET *target = GET_HTC_TARGET_FROM_HANDLE(HTCHandle); |
+ HTC_PACKET *pPacket; |
+ A_STATUS status; |
+ |
+ AR_DEBUG_PRINTF(ATH_DEBUG_TRC, ("HTCStart Enter\n")); |
+ |
+ /* make sure interrupts are disabled at the chip level, |
+ * this function can be called again from a reboot of the target without shutting down HTC */ |
+ DevDisableInterrupts(&target->Device); |
+ /* make sure state is cleared again */ |
+ target->OpStateFlags = 0; |
+ target->RecvStateFlags = 0; |
+ |
+ /* now that we are starting, push control receive buffers into the |
+ * HTC control endpoint */ |
+ |
+ while (1) { |
+ pPacket = HTC_ALLOC_CONTROL_RX(target); |
+ if (NULL == pPacket) { |
+ break; |
+ } |
+ HTCAddReceivePkt((HTC_HANDLE)target,pPacket); |
+ } |
+ |
+ do { |
+ |
+ AR_DEBUG_ASSERT(target->InitCredits != NULL); |
+ AR_DEBUG_ASSERT(target->EpCreditDistributionListHead != NULL); |
+ AR_DEBUG_ASSERT(target->EpCreditDistributionListHead->pNext != NULL); |
+ |
+ /* call init credits callback to do the distribution , |
+ * NOTE: the first entry in the distribution list is ENDPOINT_0, so |
+ * we pass the start of the list after this one. */ |
+ target->InitCredits(target->pCredDistContext, |
+ target->EpCreditDistributionListHead->pNext, |
+ target->TargetCredits); |
+ |
+ if (AR_DEBUG_LVL_CHECK(ATH_DEBUG_TRC)) { |
+ DumpCreditDistStates(target); |
+ } |
+ |
+ /* the caller is done connecting to services, so we can indicate to the |
+ * target that the setup phase is complete */ |
+ status = HTCSendSetupComplete(target); |
+ |
+ if (A_FAILED(status)) { |
+ break; |
+ } |
+ |
+ /* unmask interrupts */ |
+ status = DevUnmaskInterrupts(&target->Device); |
+ |
+ if (A_FAILED(status)) { |
+ HTCStop(target); |
+ } |
+ |
+ } while (FALSE); |
+ |
+ AR_DEBUG_PRINTF(ATH_DEBUG_TRC, ("HTCStart Exit\n")); |
+ return status; |
+} |
+ |
+static void ResetEndpointStates(HTC_TARGET *target) |
+{ |
+ HTC_ENDPOINT *pEndpoint; |
+ int i; |
+ |
+ for (i = ENDPOINT_0; i < ENDPOINT_MAX; i++) { |
+ pEndpoint = &target->EndPoint[i]; |
+ |
+ A_MEMZERO(&pEndpoint->CreditDist, sizeof(pEndpoint->CreditDist)); |
+ pEndpoint->ServiceID = 0; |
+ pEndpoint->MaxMsgLength = 0; |
+ pEndpoint->MaxTxQueueDepth = 0; |
+#ifdef HTC_EP_STAT_PROFILING |
+ A_MEMZERO(&pEndpoint->EndPointStats,sizeof(pEndpoint->EndPointStats)); |
+#endif |
+ INIT_HTC_PACKET_QUEUE(&pEndpoint->RxBuffers); |
+ INIT_HTC_PACKET_QUEUE(&pEndpoint->TxQueue); |
+ INIT_HTC_PACKET_QUEUE(&pEndpoint->RecvIndicationQueue); |
+ pEndpoint->target = target; |
+ } |
+ /* reset distribution list */ |
+ target->EpCreditDistributionListHead = NULL; |
+} |
+ |
+/* stop HTC communications, i.e. stop interrupt reception, and flush all queued buffers */ |
+void HTCStop(HTC_HANDLE HTCHandle) |
+{ |
+ HTC_TARGET *target = GET_HTC_TARGET_FROM_HANDLE(HTCHandle); |
+ AR_DEBUG_PRINTF(ATH_DEBUG_TRC, ("+HTCStop \n")); |
+ |
+ LOCK_HTC(target); |
+ /* mark that we are shutting down .. */ |
+ target->OpStateFlags |= HTC_OP_STATE_STOPPING; |
+ UNLOCK_HTC(target); |
+ |
+ /* Masking interrupts is a synchronous operation, when this function returns |
+ * all pending HIF I/O has completed, we can safely flush the queues */ |
+ DevMaskInterrupts(&target->Device); |
+ |
+ /* flush all send packets */ |
+ HTCFlushSendPkts(target); |
+ /* flush all recv buffers */ |
+ HTCFlushRecvBuffers(target); |
+ |
+ ResetEndpointStates(target); |
+ |
+ AR_DEBUG_PRINTF(ATH_DEBUG_TRC, ("-HTCStop \n")); |
+} |
+ |
+void HTCDumpCreditStates(HTC_HANDLE HTCHandle) |
+{ |
+ HTC_TARGET *target = GET_HTC_TARGET_FROM_HANDLE(HTCHandle); |
+ |
+ LOCK_HTC_TX(target); |
+ |
+ DumpCreditDistStates(target); |
+ |
+ UNLOCK_HTC_TX(target); |
+ |
+ DumpAR6KDevState(&target->Device); |
+} |
+ |
+/* report a target failure from the device, this is a callback from the device layer |
+ * which uses a mechanism to report errors from the target (i.e. special interrupts) */ |
+static void HTCReportFailure(void *Context) |
+{ |
+ HTC_TARGET *target = (HTC_TARGET *)Context; |
+ |
+ target->TargetFailure = TRUE; |
+ |
+ if (target->HTCInitInfo.TargetFailure != NULL) { |
+ /* let upper layer know, it needs to call HTCStop() */ |
+ target->HTCInitInfo.TargetFailure(target->HTCInitInfo.pContext, A_ERROR); |
+ } |
+} |
+ |
+A_BOOL HTCGetEndpointStatistics(HTC_HANDLE HTCHandle, |
+ HTC_ENDPOINT_ID Endpoint, |
+ HTC_ENDPOINT_STAT_ACTION Action, |
+ HTC_ENDPOINT_STATS *pStats) |
+{ |
+ |
+#ifdef HTC_EP_STAT_PROFILING |
+ HTC_TARGET *target = GET_HTC_TARGET_FROM_HANDLE(HTCHandle); |
+ A_BOOL clearStats = FALSE; |
+ A_BOOL sample = FALSE; |
+ |
+ switch (Action) { |
+ case HTC_EP_STAT_SAMPLE : |
+ sample = TRUE; |
+ break; |
+ case HTC_EP_STAT_SAMPLE_AND_CLEAR : |
+ sample = TRUE; |
+ clearStats = TRUE; |
+ break; |
+ case HTC_EP_STAT_CLEAR : |
+ clearStats = TRUE; |
+ break; |
+ default: |
+ break; |
+ } |
+ |
+ A_ASSERT(Endpoint < ENDPOINT_MAX); |
+ |
+ /* lock out TX and RX while we sample and/or clear */ |
+ LOCK_HTC_TX(target); |
+ LOCK_HTC_RX(target); |
+ |
+ if (sample) { |
+ A_ASSERT(pStats != NULL); |
+ /* return the stats to the caller */ |
+ A_MEMCPY(pStats, &target->EndPoint[Endpoint].EndPointStats, sizeof(HTC_ENDPOINT_STATS)); |
+ } |
+ |
+ if (clearStats) { |
+ /* reset stats */ |
+ A_MEMZERO(&target->EndPoint[Endpoint].EndPointStats, sizeof(HTC_ENDPOINT_STATS)); |
+ } |
+ |
+ UNLOCK_HTC_RX(target); |
+ UNLOCK_HTC_TX(target); |
+ |
+ return TRUE; |
+#else |
+ return FALSE; |
+#endif |
+} |
+ |
+AR6K_DEVICE *HTCGetAR6KDevice(void *HTCHandle) |
+{ |
+ HTC_TARGET *target = GET_HTC_TARGET_FROM_HANDLE(HTCHandle); |
+ return &target->Device; |
+} |
+ |