aboutsummaryrefslogtreecommitdiffstats
path: root/contrib/sysmobts-calib/sysmobts-layer1.c
diff options
context:
space:
mode:
authorHolger Hans Peter Freyther <zecke@selfish.org>2012-06-12 18:12:46 +0200
committerHolger Hans Peter Freyther <zecke@selfish.org>2012-06-12 18:17:11 +0200
commita540332df3de735f75a2959814190b6cbf3c6e4c (patch)
tree46451e05a7d9680a611c9f49a458a971944484c3 /contrib/sysmobts-calib/sysmobts-layer1.c
parentad3e31dc4b9ee1063d9e633ca884315b5a5c9710 (diff)
sysmobts-calib: Add a utility to calibrate the sysmobts v2 hardware
It has been tested with the OCXO and the network listen mode of the firmware. For other sources we are not required to synchronize to the network and the tool needs to be adjusted.
Diffstat (limited to 'contrib/sysmobts-calib/sysmobts-layer1.c')
-rw-r--r--contrib/sysmobts-calib/sysmobts-layer1.c686
1 files changed, 686 insertions, 0 deletions
diff --git a/contrib/sysmobts-calib/sysmobts-layer1.c b/contrib/sysmobts-calib/sysmobts-layer1.c
new file mode 100644
index 00000000..d886997f
--- /dev/null
+++ b/contrib/sysmobts-calib/sysmobts-layer1.c
@@ -0,0 +1,686 @@
+/* Layer1 handling for the DSP/FPGA */
+/*
+ * (C) 2012 Holger Hans Peter Freyther
+ *
+ * All Rights Reserved
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation; either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+#include <stdio.h>
+#include <stdlib.h>
+#include <inttypes.h>
+
+#include <sys/types.h>
+#include <sys/stat.h>
+#include <fcntl.h>
+#include <string.h>
+#include <unistd.h>
+#include <time.h>
+
+#include <sysmocom/femtobts/superfemto.h>
+#include <sysmocom/femtobts/gsml1prim.h>
+
+#define ARRAY_SIZE(ar) (sizeof(ar)/sizeof((ar)[0]))
+
+#define BTS_DSP2ARM "/dev/msgq/superfemto_dsp2arm"
+#define BTS_ARM2DSP "/dev/msgq/superfemto_arm2dsp"
+#define L1_SIG_ARM2DSP "/dev/msgq/gsml1_sig_arm2dsp"
+#define L1_SIG_DSP2ARM "/dev/msgq/gsml1_sig_dsp2arm"
+
+int set_clock_cor(int clock_cor, int calib, int source);
+static int wait_read_ignore(int seconds);
+
+static int sys_dsp2arm = -1,
+ sys_arm2dsp = -1,
+ sig_dsp2arm = -1,
+ sig_arm2dsp = -1;
+
+static int sync_indicated = 0;
+static int time_indicated = 0;
+
+static int open_devices()
+{
+ sys_dsp2arm = open(BTS_DSP2ARM, O_RDONLY);
+ if (sys_dsp2arm == -1) {
+ perror("Failed to open dsp2arm system queue");
+ return -1;
+ }
+
+ sys_arm2dsp = open(BTS_ARM2DSP, O_WRONLY);
+ if (sys_arm2dsp == -1) {
+ perror("Failed to open arm2dsp system queue");
+ return -2;
+ }
+
+ sig_dsp2arm = open(L1_SIG_DSP2ARM, O_RDONLY);
+ if (sig_dsp2arm == -1) {
+ perror("Failed to open dsp2arm sig queue");
+ return -3;
+ }
+
+ sig_arm2dsp = open(L1_SIG_ARM2DSP, O_WRONLY);
+ if (sig_arm2dsp == -1) {
+ perror("Failed to open arm2dsp sig queue");
+ return -4;
+ }
+
+ return 0;
+}
+
+/**
+ * Send a primitive to the system queue
+ */
+static int send_primitive(int primitive, SuperFemto_Prim_t *prim)
+{
+ prim->id = primitive;
+ return write(sys_arm2dsp, prim, sizeof(*prim)) != sizeof(*prim);
+}
+
+/**
+ * Wait for a confirmation
+ */
+static int wait_primitive(int wait_for, SuperFemto_Prim_t *prim)
+{
+ memset(prim, 0, sizeof(*prim));
+ int rc = read(sys_dsp2arm, prim, sizeof(*prim));
+ if (rc != sizeof(*prim)) {
+ printf("Short read in %s: %d\n", __func__, rc);
+ return -1;
+ }
+
+ if (prim->id != wait_for) {
+ printf("Got primitive %d but waited for %d\n",
+ prim->id, wait_for);
+ return -2;
+ }
+
+ return 0;
+}
+
+/* The Cnf for the Req, assume it is a +1 */
+static int answer_for(int primitive)
+{
+ return primitive + 1;
+}
+
+static int send_recv_primitive(int p, SuperFemto_Prim_t *prim)
+{
+ int rc;
+ rc = send_primitive(p, prim);
+ if (rc != 0)
+ return -1;
+
+ rc = wait_primitive(answer_for(p), prim);
+ if (rc != 0)
+ return -2;
+ return 0;
+}
+
+static int answer_for_sig(int prim)
+{
+ static const GsmL1_PrimId_t cnf[] = {
+ [GsmL1_PrimId_MphInitReq] = GsmL1_PrimId_MphInitCnf,
+ [GsmL1_PrimId_MphCloseReq] = GsmL1_PrimId_MphCloseCnf,
+ [GsmL1_PrimId_MphConnectReq] = GsmL1_PrimId_MphConnectCnf,
+ [GsmL1_PrimId_MphActivateReq] = GsmL1_PrimId_MphActivateCnf,
+ [GsmL1_PrimId_MphConfigReq] = GsmL1_PrimId_MphConfigCnf,
+ [GsmL1_PrimId_MphMeasureReq] = GsmL1_PrimId_MphMeasureCnf,
+ };
+
+ if (prim < 0 || prim >= ARRAY_SIZE(cnf)) {
+ printf("Unknown primitive: %d\n", prim);
+ exit(-3);
+ }
+
+ return cnf[prim];
+}
+
+static int is_indication(int prim)
+{
+ return
+ prim == GsmL1_PrimId_MphTimeInd ||
+ prim == GsmL1_PrimId_MphSyncInd ||
+ prim == GsmL1_PrimId_PhConnectInd ||
+ prim == GsmL1_PrimId_PhReadyToSendInd ||
+ prim == GsmL1_PrimId_PhDataInd ||
+ prim == GsmL1_PrimId_PhRaInd;
+}
+
+
+static int send_recv_sig_prim(int p, GsmL1_Prim_t *prim)
+{
+ int rc;
+ prim->id = p;
+ rc = write(sig_arm2dsp, prim, sizeof(*prim));
+ if (rc != sizeof(*prim)) {
+ printf("Failed to write: %d\n", rc);
+ return -1;
+ }
+
+ do {
+ rc = read(sig_dsp2arm, prim, sizeof(*prim));
+ if (rc != sizeof(*prim)) {
+ printf("Failed to read: %d\n", rc);
+ return -2;
+ }
+ } while (is_indication(prim->id));
+
+ if (prim->id != answer_for_sig(p)) {
+ printf("Wrong L1 result got %d wanted %d for prim: %d\n",
+ prim->id, answer_for_sig(p), p);
+ return -3;
+ }
+
+ return 0;
+}
+
+static int wait_for_indication(int p, GsmL1_Prim_t *prim)
+{
+ int rc;
+ memset(prim, 0, sizeof(*prim));
+
+ struct timespec start_time, now_time;
+ clock_gettime(CLOCK_MONOTONIC, &start_time);
+
+ /*
+ * TODO: select.... with timeout. The below will work 99% as we will
+ * get time indications very soonish after the connect
+ */
+ for (;;) {
+ clock_gettime(CLOCK_MONOTONIC, &now_time);
+ if (now_time.tv_sec - start_time.tv_sec > 10) {
+ printf("Timeout waiting for indication.\n");
+ return -4;
+ }
+
+ rc = read(sig_dsp2arm, prim, sizeof(*prim));
+ if (rc != sizeof(*prim)) {
+ printf("Failed to read.\n");
+ return -1;
+ }
+
+ if (!is_indication(prim->id)) {
+ printf("No indication: %d\n", prim->id);
+ return -2;
+ }
+
+ if (p != prim->id && prim->id == GsmL1_PrimId_MphSyncInd) {
+ printf("Got sync.\n");
+ sync_indicated = 1;
+ continue;
+ }
+ if (p != prim->id && prim->id == GsmL1_PrimId_MphTimeInd) {
+ time_indicated = 1;
+ continue;
+ }
+
+ if (p != prim->id) {
+ printf("Wrong indication got %d wanted %d\n",
+ prim->id, p);
+ return -3;
+ }
+
+ break;
+ }
+
+ return 0;
+}
+
+static int set_trace_flags(uint32_t dsp)
+{
+ SuperFemto_Prim_t prim;
+ memset(&prim, 0, sizeof(prim));
+
+ prim.u.setTraceFlagsReq.u32Tf = dsp;
+ return send_primitive(SuperFemto_PrimId_SetTraceFlagsReq, &prim);
+}
+
+static int reset_and_wait()
+{
+ int rc;
+ SuperFemto_Prim_t prim;
+ memset(&prim, 0, sizeof(prim));
+
+ rc = send_recv_primitive(SuperFemto_PrimId_Layer1ResetReq, &prim);
+ if (rc != 0)
+ return -1;
+ if (prim.u.layer1ResetCnf.status != GsmL1_Status_Success)
+ return -2;
+ return 0;
+}
+
+/**
+ * Open the message queues and (re-)initialize the DSP and FPGA
+ */
+int initialize_layer1(uint32_t dsp_flags)
+{
+ if (open_devices() != 0) {
+ printf("Failed to open devices.\n");
+ return -1;
+ }
+
+ if (set_trace_flags(dsp_flags) != 0) {
+ printf("Failed to set dsp flags.\n");
+ return -2;
+ }
+ if (reset_and_wait() != 0) {
+ printf("Failed to reset the firmware.\n");
+ return -3;
+ }
+ return 0;
+}
+
+/**
+ * Print systems infos
+ */
+int print_system_info()
+{
+ int rc;
+ SuperFemto_Prim_t prim;
+ memset(&prim, 0, sizeof(prim));
+
+ rc = send_recv_primitive(SuperFemto_PrimId_SystemInfoReq, &prim);
+ if (rc != 0) {
+ printf("Failed to send SystemInfoRequest.\n");
+ return -1;
+ }
+
+ if (prim.u.systemInfoCnf.status != GsmL1_Status_Success) {
+ printf("Failed to request SystemInfoRequest.\n");
+ return -2;
+ }
+
+#define INFO_DSP(x) x.u.systemInfoCnf.dspVersion
+#define INFO_FPGA(x) x.u.systemInfoCnf.fpgaVersion
+ printf("DSP v%d.%d.%d FPGA v%d.%d.%d Rev: %d Option: %d\n",
+ INFO_DSP(prim).major, INFO_DSP(prim).minor, INFO_DSP(prim).build,
+ INFO_FPGA(prim).major, INFO_FPGA(prim).minor, INFO_FPGA(prim).build,
+ prim.u.systemInfoCnf.boardVersion.rev,
+ prim.u.systemInfoCnf.boardVersion.option);
+#undef INFO_DSP
+#undef INFO_FPGA
+ return 0;
+}
+
+int activate_rf_frontend(int clock_source, int initial_cor)
+{
+ int rc;
+ SuperFemto_Prim_t prim;
+ memset(&prim, 0, sizeof(prim));
+
+ prim.u.activateRfReq.timing.u8TimSrc = 1;
+ prim.u.activateRfReq.msgq.u8UseTchMsgq = 0;
+ prim.u.activateRfReq.msgq.u8UsePdtchMsgq = 0;
+
+ prim.u.activateRfReq.rfTrx.iClkCor = initial_cor;
+ prim.u.activateRfReq.rfTrx.clkSrc = clock_source;
+
+ prim.u.activateRfReq.rfRx.iClkCor = initial_cor;
+ prim.u.activateRfReq.rfRx.clkSrc = clock_source;
+
+ rc = send_recv_primitive(SuperFemto_PrimId_ActivateRfReq, &prim);
+ return rc;
+}
+
+static int mph_init(int band, int arfcn, HANDLE *layer1)
+{
+ int rc;
+ GsmL1_Prim_t prim;
+ memset(&prim, 0, sizeof(prim));
+
+ prim.u.mphInitReq.deviceParam.devType = GsmL1_DevType_Rxd;
+ prim.u.mphInitReq.deviceParam.freqBand = band;
+ prim.u.mphInitReq.deviceParam.u16Arfcn = arfcn;
+ prim.u.mphInitReq.deviceParam.u16BcchArfcn = arfcn;
+ prim.u.mphInitReq.deviceParam.fRxPowerLevel = -75.f;
+ prim.u.mphInitReq.deviceParam.u8AutoTA = 1;
+
+ rc = send_recv_sig_prim(GsmL1_PrimId_MphInitReq, &prim);
+ if (rc != 0) {
+ printf("Failed to initialize the physical channel.\n");
+ return -1;
+ }
+
+ if (prim.u.mphInitCnf.status != GsmL1_Status_Success) {
+ printf("MPH Init failed.\n");
+ return -2;
+ }
+
+#if 0
+ if (prim.u.mphInitCnf.freqBand != band) {
+ printf("Layer1 ignored the band: %d\n",
+ prim.u.mphInitCnf.freqBand);
+ return -3;
+ }
+#endif
+
+ *layer1 = prim.u.mphInitCnf.hLayer1;
+ return 0;
+}
+
+int mph_close(HANDLE layer1)
+{
+ int rc;
+ GsmL1_Prim_t prim;
+ memset(&prim, 0, sizeof(prim));
+
+ prim.u.mphCloseReq.hLayer1 = layer1;
+ rc = send_recv_sig_prim(GsmL1_PrimId_MphCloseReq, &prim);
+ if (rc != 0) {
+ printf("Failed to close the MPH\n");
+ return -6;
+ }
+ if (prim.u.mphCloseCnf.status != GsmL1_Status_Success) {
+ printf("MPH Close failed.\n");
+ return -7;
+ }
+
+ return 0;
+}
+
+int follow_sch(int band, int arfcn, int clock, int ref, HANDLE *layer1)
+{
+ int rc;
+ GsmL1_Prim_t prim;
+
+ time_indicated = 0;
+ sync_indicated = 0;
+
+ rc = mph_init(band, arfcn, layer1);
+ if (rc != 0)
+ return rc;
+
+ /* 1.) Connect */
+ memset(&prim, 0, sizeof(prim));
+ prim.u.mphConnectReq.hLayer1 = *layer1;
+ prim.u.mphConnectReq.u8Tn = 0;
+ prim.u.mphConnectReq.logChComb = GsmL1_LogChComb_V;
+ rc = send_recv_sig_prim(GsmL1_PrimId_MphConnectReq, &prim);
+ if (rc != 0) {
+ printf("Failed to connect.\n");
+ return -1;
+ }
+ if (prim.u.mphConnectCnf.status != GsmL1_Status_Success) {
+ printf("Connect failed.\n");
+ return -2;
+ }
+ if (prim.u.mphConnectCnf.u8Tn != 0) {
+ printf("Wrong timeslot.\n");
+ return -3;
+ }
+
+ /* 2.) Activate */
+ memset(&prim, 0, sizeof(prim));
+ prim.u.mphActivateReq.hLayer1 = *layer1;
+ prim.u.mphActivateReq.u8Tn = 0;
+ prim.u.mphActivateReq.sapi = GsmL1_Sapi_Sch;
+ prim.u.mphActivateReq.dir = GsmL1_Dir_RxDownlink;
+ rc = send_recv_sig_prim(GsmL1_PrimId_MphActivateReq, &prim);
+ if (rc != 0) {
+ printf("Activation failed.\n");
+ return -4;
+ }
+ if (prim.u.mphActivateCnf.status != GsmL1_Status_Success) {
+ printf("Activation not successful.\n");
+ return -5;
+ }
+
+ /* 3.) Wait for indication... TODO: check... */
+ printf("Waiting for connect indication.\n");
+ rc = wait_for_indication(GsmL1_PrimId_PhConnectInd, &prim);
+ if (rc != 0) {
+ printf("Didn't get a connect indication.\n");
+ return rc;
+ }
+
+ /* 4.) Indication Syndication TODO: check... */
+ if (!sync_indicated) {
+ printf("Waiting for sync indication.\n");
+ rc = wait_for_indication(GsmL1_PrimId_MphSyncInd, &prim);
+ if (rc < 0) {
+ printf("Didn't get a sync indication.\n");
+ return -23;
+ } else if (rc == 0) {
+ if (!prim.u.mphSyncInd.u8Synced) {
+ printf("Failed to get sync.\n");
+ return -23;
+ } else {
+ printf("Synced.\n");
+ }
+ }
+ } else {
+ printf("Already synced.\n");
+ }
+
+ return 0;
+}
+
+int follow_bcch(HANDLE layer1)
+{
+ int rc;
+ GsmL1_Prim_t prim;
+
+ /* 1.) Activate BCCH... */
+ memset(&prim, 0, sizeof(prim));
+ prim.u.mphActivateReq.hLayer1 = layer1;
+ prim.u.mphActivateReq.u8Tn = 0;
+ prim.u.mphActivateReq.sapi = GsmL1_Sapi_Bcch;
+ prim.u.mphActivateReq.dir = GsmL1_Dir_RxDownlink;
+
+ rc = send_recv_sig_prim(GsmL1_PrimId_MphActivateReq, &prim);
+ if (rc != 0) {
+ printf("Activation failed.\n");
+ return -4;
+ }
+ if (prim.u.mphActivateCnf.status != GsmL1_Status_Success) {
+ printf("Activation not successful.\n");
+ return -5;
+ }
+
+ /* 2.) Wait for indication... */
+ printf("Waiting for connect indication.\n");
+ rc = wait_for_indication(GsmL1_PrimId_PhConnectInd, &prim);
+ if (rc != 0) {
+ printf("Didn't get a connect indication.\n");
+ return rc;
+ }
+
+ /* 3.) Wait for PhDataInd... */
+ printf("Waiting for BCCH data.\n");
+ rc = wait_for_indication(GsmL1_PrimId_PhDataInd, &prim);
+ if (rc != 0) {
+ printf("Didn't get BCCH data.\n");
+ return rc;
+ }
+
+ return 0;
+}
+
+int set_clock_cor(int clock_cor, int calib, int source)
+{
+ int rc;
+ SuperFemto_Prim_t prim;
+ memset(&prim, 0, sizeof(prim));
+
+ prim.u.rfClockSetupReq.rfTrx.iClkCor = clock_cor;
+ prim.u.rfClockSetupReq.rfTrx.clkSrc = calib;
+ prim.u.rfClockSetupReq.rfRx.iClkCor = clock_cor;
+ prim.u.rfClockSetupReq.rfRx.clkSrc = calib;
+ prim.u.rfClockSetupReq.rfTrxClkCal.clkSrc = source;
+
+ rc = send_recv_primitive(SuperFemto_PrimId_RfClockSetupReq, &prim);
+ if (rc != 0) {
+ printf("Failed to set the clock setup.\n");
+ return -1;
+ }
+ if (prim.u.rfClockSetupCnf.status != GsmL1_Status_Success) {
+ printf("Clock setup was not successfull.\n");
+ return -2;
+ }
+
+ return 0;
+}
+
+int rf_clock_info(HANDLE *layer1, int *clkErr, int *clkErrRes)
+{
+ SuperFemto_Prim_t prim;
+ memset(&prim, 0, sizeof(prim));
+
+ int rc;
+
+ /* reset the counter */
+ prim.u.rfClockInfoReq.u8RstClkCal = 1;
+ rc = send_recv_primitive(SuperFemto_PrimId_RfClockInfoReq, &prim);
+ if (rc != 0) {
+ printf("Failed to reset the clock info.\n");
+ return -1;
+ }
+
+ /* wait for a value */
+ wait_read_ignore(15);
+
+ /* ask for the current counter/error */
+ memset(&prim, 0, sizeof(prim));
+ prim.u.rfClockInfoReq.u8RstClkCal = 0;
+ rc = send_recv_primitive(SuperFemto_PrimId_RfClockInfoReq, &prim);
+ if (rc != 0) {
+ printf("Failed to get the clock info.\n");
+ return -2;
+ }
+
+ printf("Error: %d Res: %d\n",
+ prim.u.rfClockInfoCnf.rfTrxClkCal.iClkErr,
+ prim.u.rfClockInfoCnf.rfTrxClkCal.iClkErrRes);
+ *clkErr = prim.u.rfClockInfoCnf.rfTrxClkCal.iClkErr;
+ *clkErrRes = prim.u.rfClockInfoCnf.rfTrxClkCal.iClkErrRes;
+ return 0;
+}
+
+int power_scan(int band, int arfcn, int duration, float *mean_rssi)
+{
+ int rc;
+ HANDLE layer1;
+ GsmL1_Prim_t prim;
+
+ /* init */
+ rc = mph_init(band, arfcn, &layer1);
+ if (rc != 0)
+ return rc;
+
+ /* mph measure request */
+ memset(&prim, 0, sizeof(prim));
+ prim.u.mphMeasureReq.hLayer1 = layer1;
+ prim.u.mphMeasureReq.u32Duration = duration;
+ rc = send_recv_sig_prim(GsmL1_PrimId_MphMeasureReq, &prim);
+ if (rc != 0) {
+ printf("Failed to send measurement request.\n");
+ return -4;
+ }
+
+ if (prim.u.mphMeasureCnf.status != GsmL1_Status_Success) {
+ printf("MphMeasureReq was not confirmed.\n");
+ return -5;
+ }
+
+ *mean_rssi = prim.u.mphMeasureCnf.fMeanRssi;
+
+ /* close */
+ rc = mph_close(layer1);
+ return rc;
+}
+
+/**
+ * Wait for indication...
+ */
+int wait_for_sync(HANDLE layer1, int cor, int calib, int source)
+{
+ GsmL1_Prim_t prim;
+ int rc;
+
+ rc = set_clock_cor(cor, calib, source);
+ if (rc != 0) {
+ printf("Failed to set the clock correction.\n");
+ return -1;
+ }
+
+ sync_indicated = 0;
+ rc = wait_for_indication(GsmL1_PrimId_MphSyncInd, &prim);
+ if (rc < 0 && rc != -4) {
+ return rc;
+ } else if (rc == 0) {
+ if (!prim.u.mphSyncInd.u8Synced) {
+ printf("Failed to get sync.\n");
+ return 0;
+ }
+ printf("Synced.\n");
+ return 1;
+ }
+
+ return 0;
+}
+
+/**
+ * Make sure the pipe is not running full.
+ *
+ */
+static int wait_read_ignore(int seconds)
+{
+ int max, rc;
+ fd_set fds;
+ struct timeval timeout;
+
+ max = sys_dsp2arm > sig_dsp2arm ? sys_dsp2arm : sig_dsp2arm;
+
+ timeout.tv_sec = seconds;
+ timeout.tv_usec = 0;
+
+ while (1) {
+ FD_ZERO(&fds);
+ FD_SET(sys_dsp2arm, &fds);
+ FD_SET(sig_dsp2arm, &fds);
+
+
+ rc = select(max + 1, &fds, NULL, NULL, &timeout);
+ if (rc == -1) {
+ printf("Failed to select.\n");
+ return -1;
+ } else if (rc) {
+ if (FD_ISSET(sys_dsp2arm, &fds)) {
+ SuperFemto_Prim_t prim;
+ rc = read(sys_dsp2arm, &prim, sizeof(prim));
+ if (rc != sizeof(prim)) {
+ perror("Failed to read system primitive");
+ return -2;
+ }
+ }
+ if (FD_ISSET(sig_dsp2arm, &fds)) {
+ GsmL1_Prim_t prim;
+ rc = read(sig_dsp2arm, &prim, sizeof(prim));
+ if (rc != sizeof(prim)) {
+ perror("Failed to read signal primitiven");
+ return -3;
+ }
+ }
+ } else if (timeout.tv_sec <= 0 && timeout.tv_usec <= 0) {
+ break;
+ }
+
+#ifndef __linux__
+#error "Non portable code"
+#endif
+ }
+ return 0;
+}