mirror of
https://github.com/FreeRTOS/FreeRTOS-Kernel.git
synced 2025-08-19 17:48:33 -04:00
Update FreeRTOS+Trace recorder library to v3.0.2
Add streaming version of the FreeRTOS+Trace recorder, also V3.0.2
This commit is contained in:
parent
f218cf5680
commit
96ff3925d2
32 changed files with 6067 additions and 123 deletions
566
FreeRTOS-Plus/Source/FreeRTOS-Plus-Trace(streaming)/Demo.c
Normal file
566
FreeRTOS-Plus/Source/FreeRTOS-Plus-Trace(streaming)/Demo.c
Normal file
|
@ -0,0 +1,566 @@
|
|||
/*******************************************************************************
|
||||
* Tracealyzer v3.0.0 Demo Application
|
||||
* Percepio AB, www.percepio.com
|
||||
*
|
||||
* traceDemoApp.c
|
||||
*
|
||||
* Demo application for Tracealyzer demo project.
|
||||
*
|
||||
* Terms of Use
|
||||
* This software is copyright Percepio AB. The recorder library is free for
|
||||
* use together with Percepio products. You may distribute the recorder library
|
||||
* in its original form, including modifications in trcHardwarePort.c/.h
|
||||
* given that these modification are clearly marked as your own modifications
|
||||
* and documented in the initial comment section of these source files.
|
||||
* This software is the intellectual property of Percepio AB and may not be
|
||||
* sold or in other ways commercially redistributed without explicit written
|
||||
* permission by Percepio AB.
|
||||
*
|
||||
* Disclaimer
|
||||
* The trace tool and recorder library is being delivered to you AS IS and
|
||||
* Percepio AB makes no warranty as to its use or performance. Percepio AB does
|
||||
* not and cannot warrant the performance or results you may obtain by using the
|
||||
* software or documentation. Percepio AB make no warranties, express or
|
||||
* implied, as to noninfringement of third party rights, merchantability, or
|
||||
* fitness for any particular purpose. In no event will Percepio AB, its
|
||||
* technology partners, or distributors be liable to you for any consequential,
|
||||
* incidental or special damages, including any lost profits or lost savings,
|
||||
* even if a representative of Percepio AB has been advised of the possibility
|
||||
* of such damages, or for any claim by any third party. Some jurisdictions do
|
||||
* not allow the exclusion or limitation of incidental, consequential or special
|
||||
* damages, or the exclusion of implied warranties or limitations on how long an
|
||||
* implied warranty may last, so the above limitations may not apply to you.
|
||||
*
|
||||
* Tabs are used for indent in this file (1 tab = 4 spaces)
|
||||
*
|
||||
* Copyright Percepio AB, 2014.
|
||||
* www.percepio.com
|
||||
******************************************************************************/
|
||||
|
||||
|
||||
/* Standard includes. */
|
||||
#include <stdio.h>
|
||||
#include <stdint.h>
|
||||
|
||||
/* Kernel includes. */
|
||||
#include <FreeRTOS.h>
|
||||
#include "task.h"
|
||||
#include "queue.h"
|
||||
#include "semphr.h"
|
||||
|
||||
#if (configUSE_TIMERS == 1)
|
||||
#include "timers.h"
|
||||
#endif
|
||||
|
||||
#include "trcRecorder.h"
|
||||
|
||||
#define ISR3_PERIOD 18
|
||||
#define ISR2_PERIOD 24
|
||||
#define ISR1_PERIOD 30
|
||||
|
||||
#define EXECTIME_CONTROLLER 1
|
||||
#define EXECTIME_SUPERV 2
|
||||
#define EXECTIME_SENSOR 1
|
||||
|
||||
typedef struct
|
||||
{
|
||||
int32_t code;
|
||||
int32_t value;
|
||||
char data[512]; // Makes the Memory Heap Utilization graph more interresting!
|
||||
} QueueMessage;
|
||||
|
||||
#define MSGCODE_SENSOR_VALUE 1
|
||||
#define MSGCODE_CONTROL_VALUE 10
|
||||
#define MSGCODE_CONTROL_DEFAULT_VALUE 99
|
||||
|
||||
static portTASK_FUNCTION_PROTO( trcDemoTaskSensor, pvParameters );
|
||||
static portTASK_FUNCTION_PROTO( trcDemoTaskActuator, pvParameters );
|
||||
static portTASK_FUNCTION_PROTO( trcDemoTaskController, pvParameters );
|
||||
static portTASK_FUNCTION( trcDemoTaskSuperv, pvParameters );
|
||||
static portTASK_FUNCTION( trcDemoISR, pvParameters );
|
||||
|
||||
#if (configUSE_TIMERS == 1)
|
||||
xTimerHandle timer;
|
||||
void myTimerHandler(xTimerHandle tmr);
|
||||
#endif
|
||||
|
||||
xQueueHandle sensorQ, actuatorQ;
|
||||
xSemaphoreHandle Sem1, Sem2, Sem3;
|
||||
|
||||
void doSomeWork(uint32_t n);
|
||||
|
||||
int readSensor(int i);
|
||||
|
||||
/******************************************************************************
|
||||
* doSomeWork
|
||||
*
|
||||
* Performs a busy wait for X milliseconds. Used to generate suitable execution
|
||||
* times in the demo application, in a portable way.
|
||||
******************************************************************************/
|
||||
void doSomeWork(uint32_t ms)
|
||||
{
|
||||
portTickType old = xTaskGetTickCount();
|
||||
portTickType new = 0;
|
||||
while (ms > 0)
|
||||
{
|
||||
new = xTaskGetTickCount();
|
||||
if (old < new)
|
||||
{
|
||||
/* If uiTraceTickCount has changed, we only reduce 1ms of the wait
|
||||
time. This way we do not count time that this task has waited
|
||||
for other tasks. This will result in somewhat correct execution
|
||||
times. */
|
||||
old = new;
|
||||
ms--;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/******************************************************************************
|
||||
* readSensor
|
||||
*
|
||||
* Simulates a set of sensors/inputs, using a simple physics simulation.
|
||||
* The values are intentionally ints (rather than floats) to avoid very slow
|
||||
* execution times on chips without hardware floating point support.
|
||||
******************************************************************************/
|
||||
int readSensor(int i)
|
||||
{
|
||||
static int pos[3] = {40, 80, 120};
|
||||
static int speed[3] = {0, 0, -3};
|
||||
static int acc[3] = {0, 0, 0};
|
||||
|
||||
i--;
|
||||
|
||||
acc[i] = (-pos[i] >> 3);
|
||||
speed[i] += acc[i];
|
||||
pos[i] += speed[i];
|
||||
|
||||
return (int)pos[i];
|
||||
}
|
||||
|
||||
/******************************************************************************
|
||||
* trcDemoTaskSensor
|
||||
*
|
||||
* The "Sensor" tasks (three instances of this code). Reads data using
|
||||
* "readSensor" when trigged by the timer interrupt through a semaphore.
|
||||
******************************************************************************/
|
||||
static portTASK_FUNCTION( trcDemoTaskSensor, pvParameters )
|
||||
{
|
||||
char name[30] = "SensorX\0";
|
||||
QueueMessage sensorReading;
|
||||
unsigned char idx = (unsigned char)(*(int*)pvParameters);
|
||||
xSemaphoreHandle mySem = NULL;
|
||||
|
||||
/* Change 'X' to the index */
|
||||
name[6] = '0' + idx;
|
||||
|
||||
switch (idx)
|
||||
{
|
||||
case 1: mySem = Sem1; break;
|
||||
case 2: mySem = Sem2; break;
|
||||
case 3: mySem = Sem3; break;
|
||||
}
|
||||
|
||||
vTraceStoreUserEventChannelName(name);
|
||||
|
||||
/* Initialize xNextWakeTime - this only needs to be done once. */
|
||||
|
||||
sensorReading.value = 0;
|
||||
sensorReading.code = 0;
|
||||
|
||||
for( ;; )
|
||||
{
|
||||
|
||||
xSemaphoreTake(mySem, 0xFFFFFFFF);
|
||||
|
||||
sensorReading.code = idx;
|
||||
sensorReading.value = readSensor(idx);
|
||||
|
||||
doSomeWork(EXECTIME_SENSOR);
|
||||
|
||||
vTracePrintF(name , "Sending msg %d", sensorReading.value);
|
||||
|
||||
if (xQueueSend(sensorQ, &sensorReading, 1) != pdTRUE)
|
||||
{
|
||||
vTracePrint(name, "Sensor queue full. Sample dropped!");
|
||||
}
|
||||
vTraceInstanceFinishedNow();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
/******************************************************************************
|
||||
* trcDemoTaskSuperv
|
||||
*
|
||||
* The "Supervisor" task. Performs no action in the simulation, apart from
|
||||
* preempting other tasks in the scheduling.
|
||||
******************************************************************************/
|
||||
static portTASK_FUNCTION( trcDemoTaskSuperv, pvParameters )
|
||||
{
|
||||
int counter = 0;
|
||||
|
||||
#if (configUSE_TIMERS == 1)
|
||||
static int timerState = 0;
|
||||
#endif
|
||||
|
||||
portTickType xNextWakeTime;
|
||||
(void)pvParameters;
|
||||
|
||||
xNextWakeTime = xTaskGetTickCount();
|
||||
|
||||
#if (configUSE_TIMERS == 1)
|
||||
timer = xTimerCreate("Tmr5", 5, 1, (void*)42, myTimerHandler);
|
||||
xTimerChangePeriod(timer, 10, 0);
|
||||
#endif
|
||||
|
||||
for( ;; )
|
||||
{
|
||||
vTaskDelayUntil( &xNextWakeTime, 40);
|
||||
|
||||
counter = (counter + 1) % 16;
|
||||
|
||||
if (counter == 8) vTaskPrioritySet(NULL, 2);
|
||||
|
||||
if (counter == 0) vTaskPrioritySet(NULL, 3);
|
||||
|
||||
doSomeWork(EXECTIME_SUPERV);
|
||||
|
||||
#if (configUSE_TIMERS == 1)
|
||||
if (timerState == 0)
|
||||
{
|
||||
xTimerStart(timer, 0);
|
||||
timerState = 1;
|
||||
}
|
||||
else if (timerState == 1)
|
||||
{
|
||||
xTimerStop(timer, 0);
|
||||
timerState = 0;
|
||||
}
|
||||
#endif
|
||||
}
|
||||
}
|
||||
|
||||
/******************************************************************************
|
||||
* trcDemoTaskController
|
||||
*
|
||||
* The "Controller" task. Periodically reads all accumulated messages in SensorQ
|
||||
* (i.e., those produced by the Sensor tasks since the last execution of
|
||||
* the Controller task). Calculates their mean value and send this to Actuator.
|
||||
******************************************************************************/
|
||||
static portTASK_FUNCTION( trcDemoTaskController, pvParameters )
|
||||
{
|
||||
portTickType xNextWakeTime;
|
||||
QueueMessage* sensorReading;
|
||||
QueueMessage actuatorMessage;
|
||||
int32_t sum[4];
|
||||
int32_t count[4];
|
||||
|
||||
(void)pvParameters;
|
||||
|
||||
vTraceStoreUserEventChannelName("UE TEST");
|
||||
|
||||
/* Initialize xNextWakeTime - this only needs to be done once. */
|
||||
xNextWakeTime = xTaskGetTickCount();
|
||||
for(;;)
|
||||
{
|
||||
sum[1] = 0;
|
||||
sum[2] = 0;
|
||||
sum[3] = 0;
|
||||
count[1] = 0;
|
||||
count[2] = 0;
|
||||
count[3] = 0;
|
||||
|
||||
vTaskDelayUntil(&xNextWakeTime, 60);
|
||||
|
||||
for(;;)
|
||||
{
|
||||
// To demostrate malloc/free tracing
|
||||
sensorReading = ( QueueMessage * ) pvPortMalloc( sizeof( QueueMessage ) );
|
||||
|
||||
if (xQueueReceive(sensorQ, sensorReading, 0) != pdTRUE)
|
||||
{
|
||||
vPortFree(sensorReading);
|
||||
break;
|
||||
}
|
||||
else
|
||||
{
|
||||
if (sensorReading->code >= 1 && sensorReading->code <= 3)
|
||||
{
|
||||
sum[sensorReading->code] += sensorReading->value;
|
||||
count[sensorReading->code]++;
|
||||
}
|
||||
}
|
||||
doSomeWork(EXECTIME_CONTROLLER);
|
||||
|
||||
//vTracePrintF(0, "0x%4x", 111);
|
||||
//vTracePrintF(0, "0x%04x", 111);
|
||||
//vTracePrintF(0, "0x%4X", 111);
|
||||
//vTracePrintF(0, "0x%04X", 111);
|
||||
//
|
||||
//vTracePrint(0, "123456789012345678901234567890123456789012345678901234567890");
|
||||
//vTracePrint("UE TEST", "1234567890123456789012345678901234567890123456789012345");
|
||||
//vTracePrint("UE TEST", "12345678901234567890123456789012345678901234567890123456");
|
||||
//vTracePrint("UE TEST", "123456789012345678901234567890123456789012345678901234567");
|
||||
//vTracePrint("UE TEST", "12345678901234567890123456789012345678901234567890");
|
||||
//
|
||||
//vTracePrintF("UE TEST", "%5d", 42);
|
||||
//vTracePrintF("UE TEST", "%05d", 42);
|
||||
//vTracePrintF("UE TEST", "%5d", -42);
|
||||
//vTracePrintF("UE TEST", "%05d", -42);
|
||||
//vTracePrintF("UE TEST", "%05d", -1042);
|
||||
//vTracePrintF("UE TEST", "%05d", -11042);
|
||||
//
|
||||
//vTracePrintF("UE TEST", "%5u", -1);
|
||||
//vTracePrintF("UE TEST", "%05u", -1);
|
||||
//vTracePrintF("UE TEST", "%5u", 10);
|
||||
//vTracePrintF("UE TEST", "%05u", 10);
|
||||
//
|
||||
//vTracePrintF("UE TEST", "%5u", 11042);
|
||||
//vTracePrintF("UE TEST", "%05u", 11042);
|
||||
//
|
||||
//vTracePrintF("UE TEST", "%5u", 111042);
|
||||
//vTracePrintF("UE TEST", "%05u", 111042);
|
||||
//
|
||||
//vTracePrintF("UE TEST", "%15d", 111042);
|
||||
//vTracePrintF("UE TEST", "%015d", 111042);
|
||||
|
||||
vPortFree(sensorReading);
|
||||
vTraceInstanceFinishedNow();
|
||||
|
||||
}
|
||||
|
||||
actuatorMessage.code = MSGCODE_CONTROL_VALUE;
|
||||
actuatorMessage.value = 0;
|
||||
|
||||
if (count[1] > 0)
|
||||
actuatorMessage.value += (int32_t)(0.5 * sum[1]/count[1]);
|
||||
if (count[2] > 0)
|
||||
actuatorMessage.value += (int32_t)(0.25 * sum[2]/count[2]);
|
||||
if (count[3] > 0)
|
||||
actuatorMessage.value += (int32_t)(0.25 * sum[3]/count[3]);
|
||||
|
||||
xQueueSend(actuatorQ, &actuatorMessage, 10000);
|
||||
}
|
||||
}
|
||||
|
||||
/******************************************************************************
|
||||
* trcDemoISR
|
||||
*
|
||||
* The "FakeISR" task, that simulates the ISRTimer interrupts, which triggers
|
||||
* the Sensor tasks.
|
||||
******************************************************************************/
|
||||
static portTASK_FUNCTION( trcDemoISR, pvParameters )
|
||||
{
|
||||
portTickType xNextWakeTime;
|
||||
portBASE_TYPE dummy;
|
||||
int i = 0;
|
||||
|
||||
(void)pvParameters;
|
||||
|
||||
/* Initialize xNextWakeTime - this only needs to be done once. */
|
||||
xNextWakeTime = xTaskGetTickCount();
|
||||
vTraceSetISRProperties("ISRTimer1", 3);
|
||||
vTraceSetISRProperties("ISRTimer2", 2);
|
||||
vTraceSetISRProperties("ISRTimer3", 1);
|
||||
vTaskDelayUntil( &xNextWakeTime, 1);
|
||||
|
||||
for( ;; )
|
||||
{
|
||||
i++;
|
||||
|
||||
if (i % ISR3_PERIOD == 0)
|
||||
{
|
||||
portENTER_CRITICAL();
|
||||
vTraceStoreISRBegin((void*)"ISRTimer3");
|
||||
xSemaphoreGiveFromISR(Sem3, &dummy);
|
||||
vTraceStoreISREndManual(dummy);
|
||||
portEXIT_CRITICAL();
|
||||
}
|
||||
|
||||
if (i % ISR2_PERIOD == 0)
|
||||
{
|
||||
portENTER_CRITICAL();
|
||||
vTraceStoreISRBegin((void*)"ISRTimer2");
|
||||
xSemaphoreGiveFromISR(Sem2, &dummy);
|
||||
vTraceStoreISREndManual(dummy);
|
||||
portEXIT_CRITICAL();
|
||||
}
|
||||
|
||||
if (i % ISR1_PERIOD == 0)
|
||||
{
|
||||
portENTER_CRITICAL();
|
||||
vTraceStoreISRBegin((void*)"ISRTimer1");
|
||||
xSemaphoreGiveFromISR(Sem1, &dummy);
|
||||
vTraceStoreISREndManual(dummy);
|
||||
portEXIT_CRITICAL();
|
||||
|
||||
|
||||
}
|
||||
|
||||
vTaskDelayUntil( &xNextWakeTime, 1);
|
||||
}
|
||||
}
|
||||
|
||||
/******************************************************************************
|
||||
* trcDemoTaskActuator
|
||||
*
|
||||
* The "Actuator" task, just reads the data from Controller and logs it in a
|
||||
* User Event (i.e., the "vTracePrintF" calls).
|
||||
******************************************************************************/
|
||||
static portTASK_FUNCTION( trcDemoTaskActuator, pvParameters )
|
||||
{
|
||||
QueueMessage inMessage;
|
||||
(void)pvParameters;
|
||||
vTraceStoreUserEventChannelName("Actuator Out Signal");
|
||||
|
||||
for( ;; )
|
||||
{
|
||||
vTraceInstanceFinishedNow();
|
||||
/* Place this task in blocked state until it is time to run again. */
|
||||
if (xQueueReceive(actuatorQ, &inMessage, 35) != pdTRUE)
|
||||
{
|
||||
vTracePrintF("Actuator Out Signal", "No data...");
|
||||
}
|
||||
else
|
||||
{
|
||||
if (inMessage.code == MSGCODE_CONTROL_VALUE)
|
||||
{
|
||||
vTracePrintF("Actuator Out Signal", "Out: %d",
|
||||
inMessage.value);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#if (configUSE_TIMERS == 1)
|
||||
void myTimerHandler(xTimerHandle tmr)
|
||||
{
|
||||
(void) tmr;
|
||||
vTracePrintF("Timer", "Timer handler!");
|
||||
}
|
||||
#endif
|
||||
|
||||
int sensorTaskID1 = 1;
|
||||
int sensorTaskID2 = 2;
|
||||
int sensorTaskID3 = 3;
|
||||
|
||||
#define vTaskCreateNotTraced( pvTaskCode, pcName, usStackDepth, pvParameters, uxPriority ) \
|
||||
{ void* this_pxCreatedTask; \
|
||||
vTraceSetReadyEventsEnabled(0); \
|
||||
xTaskGenericCreate( ( pvTaskCode ), ( pcName ), ( usStackDepth ), ( pvParameters ), ( uxPriority ), ( &this_pxCreatedTask ), ( NULL ), ( NULL ) ); \
|
||||
vTraceSetReadyEventsEnabled(1); \
|
||||
if (this_pxCreatedTask != NULL){ vTraceExcludeTaskFromTrace(this_pxCreatedTask); }else{ return -42;} }
|
||||
|
||||
/******************************************************************************
|
||||
* trcDemoTaskActuator
|
||||
*
|
||||
* The "Actuator" task, just reads the data from Controller and logs it in a
|
||||
* User Event (i.e., the "vTracePrintF" calls).
|
||||
******************************************************************************/
|
||||
int vStartDemoApplication(void)
|
||||
{
|
||||
void* objectHandle = NULL;
|
||||
|
||||
vTraceStoreUserEventChannelName("Messages");
|
||||
|
||||
vTracePrint("Messages", "Demo starting...");
|
||||
vTracePrint("Messages", "vTraceUserEvent creates basic User Events.");
|
||||
vTracePrint("Messages", "vTracePrintF creates advanced user events, like printf");
|
||||
//vTracePrintF("Messages", "A float: %f (should be 1)", (float)1.0);
|
||||
//vTracePrintF("Messages", "A double: %lf (should be 1)", (double)1.0);
|
||||
//vTracePrintF("Messages", "A signed 8-bit value: %bd (should be -1)", -1);
|
||||
|
||||
vTraceStoreUserEventChannelName("Timer");
|
||||
|
||||
sensorQ = xQueueCreate(15, sizeof(QueueMessage) );
|
||||
if( sensorQ == 0 )
|
||||
{
|
||||
vTracePrint("Messages", "Could not create SensorQ!\n");
|
||||
return -1;
|
||||
}
|
||||
vTraceSetQueueName(sensorQ, "SensorQueue");
|
||||
|
||||
actuatorQ = xQueueCreate(3, sizeof(QueueMessage) );
|
||||
if( actuatorQ == 0 )
|
||||
{
|
||||
vTracePrint("Messages", "Could not create ActuatorQ!\n");
|
||||
return -2;
|
||||
}
|
||||
vTraceSetQueueName(actuatorQ, "ActuatorQueue");
|
||||
|
||||
vSemaphoreCreateBinary(Sem1);
|
||||
if( Sem1 == NULL )
|
||||
{
|
||||
vTracePrint("Messages", "Could not create Sem1!\n");
|
||||
return -3;
|
||||
}
|
||||
vTraceSetSemaphoreName(Sem1, "SemaphSenX");
|
||||
|
||||
vSemaphoreCreateBinary(Sem2);
|
||||
if( Sem2 == NULL )
|
||||
{
|
||||
vTracePrint("Messages", "Could not create Sem2!\n");
|
||||
return -4;
|
||||
}
|
||||
vTraceSetSemaphoreName(Sem2, "SemaphSenY");
|
||||
|
||||
vSemaphoreCreateBinary(Sem3);
|
||||
if( Sem3 == NULL )
|
||||
{
|
||||
vTracePrint("Messages", "Could not create Sem3!\n");
|
||||
return -5;
|
||||
}
|
||||
vTraceSetSemaphoreName(Sem3, "SemaphSenZ");
|
||||
|
||||
xTaskCreate( trcDemoTaskSensor, "SensorX",
|
||||
configMINIMAL_STACK_SIZE*2, &sensorTaskID1, 8, &objectHandle );
|
||||
if (objectHandle == NULL)
|
||||
{
|
||||
return -6;
|
||||
}
|
||||
|
||||
xTaskCreate( trcDemoTaskSensor, "SensorY",
|
||||
configMINIMAL_STACK_SIZE*2, &sensorTaskID2, 7, &objectHandle );
|
||||
if (objectHandle == NULL)
|
||||
{
|
||||
return -7;
|
||||
}
|
||||
|
||||
xTaskCreate( trcDemoTaskSensor, "SensorZ",
|
||||
configMINIMAL_STACK_SIZE*2, &sensorTaskID3, 6, &objectHandle );
|
||||
if (objectHandle == NULL)
|
||||
{
|
||||
return -8;
|
||||
}
|
||||
|
||||
xTaskCreate( trcDemoTaskController, "Control",
|
||||
configMINIMAL_STACK_SIZE*2, NULL, 4, &objectHandle );
|
||||
if (objectHandle == NULL)
|
||||
{
|
||||
return -9;
|
||||
}
|
||||
|
||||
xTaskCreate( trcDemoTaskActuator, "Actuator",
|
||||
configMINIMAL_STACK_SIZE*2, NULL, 5, &objectHandle );
|
||||
if (objectHandle == NULL)
|
||||
{
|
||||
return -10;
|
||||
}
|
||||
|
||||
xTaskCreate( trcDemoTaskSuperv, "Superv",
|
||||
configMINIMAL_STACK_SIZE*2, NULL, 3, &objectHandle );
|
||||
if (objectHandle == NULL)
|
||||
{
|
||||
return -11;
|
||||
}
|
||||
|
||||
xTaskCreate( trcDemoISR, "(Hide)FakeISR",
|
||||
configMINIMAL_STACK_SIZE*2, NULL, configMAX_PRIORITIES-1, &objectHandle);
|
||||
if (objectHandle == NULL)
|
||||
{
|
||||
return -12;
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue