[feat][pm] add pm util for pds31 that ble needs

This commit is contained in:
jzlv 2021-09-30 14:59:08 +08:00
parent 28de956075
commit e3449aabde
5 changed files with 435 additions and 51 deletions

View file

@ -24,7 +24,7 @@
#define __HAL_PM__H__
#ifdef __cplusplus
extern "C"{
extern "C" {
#endif
#include "hal_common.h"
@ -38,7 +38,7 @@ enum pm_pds_sleep_level {
PM_PDS_LEVEL_5, /*do not recommend to use*/
PM_PDS_LEVEL_6, /*do not recommend to use*/
PM_PDS_LEVEL_7, /*do not recommend to use*/
PM_PDS_LEVEL_31,
PM_PDS_LEVEL_31 = 31,
};
enum pm_hbn_sleep_level {
@ -57,16 +57,15 @@ enum pm_event_type {
PM_HBN_ACOMP1_WAKEUP_EVENT,
};
void pm_pds_mode_enter(enum pm_pds_sleep_level pds_level, uint8_t sleep_time);
void pm_pds_mode_enter(enum pm_pds_sleep_level pds_level, uint32_t sleep_time);
void pm_hbn_mode_enter(enum pm_hbn_sleep_level hbn_level, uint8_t sleep_time);
void pm_hbn_set_wakeup_callback(void (*wakeup_callback)(void));
void pm_set_wakeup_callback(void (*wakeup_callback)(void));
void pm_hbn_enter_again(bool reset);
void pm_hbn_out0_irq_register(void);
void pm_hbn_out1_irq_register(void);
void pm_irq_callback(enum pm_event_type event);
uint32_t hal_pds_enter_with_time_compensation(uint32_t pdsLevel, uint32_t pdsSleepCycles);
#ifdef __cplusplus
}
#endif
#endif
#endif

View file

@ -0,0 +1,35 @@
/**
* @file hal_pm_util.h
* @brief
*
* Copyright (c) 2021 Bouffalolab team
*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership. The
* ASF licenses this file to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance with the
* License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*
*/
#ifndef __HAL_PM_UTIL_H__
#define __HAL_PM_UTIL_H__
#include "hal_common.h"
#define ATTR_PDS_RAM_SECTION __attribute__((section(".pds_ram_code")))
#define ATTR_PDS_RAM_CONST_SECTION __attribute__((section(".pds_ram_data")))
uint32_t hal_pds_enter_with_time_compensation(uint32_t pdsLevel, uint32_t pdsSleepCycles);
void pm_set_tcm_recovery_callback(void (*tcm_recovery_cb)(void));
void pm_set_board_recovery_callback(void (*board_recovery_cb)(void));
#endif

View file

@ -26,8 +26,19 @@
#include "hal_clock.h"
#include "hal_rtc.h"
#include "hal_flash.h"
#include "hal_common.h"
/* Cache Way Disable, will get from l1c register */
uint8_t cacheWayDisable = 0;
/* PSRAM IO Configuration, will get from glb register */
uint32_t psramIoCfg = 0;
/* Flash offset value, will get from sf_ctrl register */
uint32_t flash_offset = 0;
SPI_Flash_Cfg_Type *flash_cfg;
#define PM_PDS_GPIO_IE_PUPD 0
#define PM_PDS_FLASH_POWER_OFF 1
#define PM_PDS_DLL_POWER_OFF 1
#define PM_PDS_PLL_POWER_OFF 1
@ -845,6 +856,19 @@ static ATTR_TCM_SECTION void PDS_Update_Flash_Ctrl_Setting(uint8_t fastClock)
SF_Ctrl_Set_Clock_Delay(fastClock);
}
ATTR_TCM_SECTION void pm_pds_enter_done(bool store_flag)
{
if (store_flag) {
__asm__ __volatile__(
"la a2, __ld_pds_bak_addr\n\t"
"sw ra, 0(a2)\n\t");
}
BL702_Delay_MS(1);
__WFI(); /* if(.wfiMask==0){CPU won't power down until PDS module had seen __wfi} */
}
/**
* @brief power management in pds(power down sleep) mode
*
@ -861,13 +885,29 @@ static ATTR_TCM_SECTION void PDS_Update_Flash_Ctrl_Setting(uint8_t fastClock)
* PDS7 ON ON ON OFF OFF OFF
* PDS31 ON OFF OFF OFF OFF OFF
*/
ATTR_TCM_SECTION void pm_pds_mode_enter(enum pm_pds_sleep_level pds_level, uint8_t sleep_time)
ATTR_TCM_SECTION void pm_pds_mode_enter(enum pm_pds_sleep_level pds_level, uint32_t sleep_time)
{
bool store_flag = 0;
PDS_DEFAULT_LV_CFG_Type *pPdsCfg = NULL;
uint32_t tmpVal;
SPI_Flash_Cfg_Type *flash_cfg;
uint32_t flash_cfg_len;
if (HBN_STATUS_ENTER_FLAG == BL_RD_REG(HBN_BASE, HBN_RSV0)) {
store_flag = 1;
// Get cache way disable setting
cacheWayDisable = (*(volatile uint32_t *)(L1C_BASE + 0x00) >> 8) & 0x0F;
// Get psram io configuration
psramIoCfg = *(volatile uint32_t *)(GLB_BASE + 0x88);
// Get flash offset
flash_offset = BL_RD_REG(SF_CTRL_BASE, SF_CTRL_SF_ID0_OFFSET);
} else {
store_flag = 0;
}
/* To make it simple and safe*/
cpu_global_irq_disable();
@ -954,7 +994,7 @@ ATTR_TCM_SECTION void pm_pds_mode_enter(enum pm_pds_sleep_level pds_level, uint8
/* pds31 : ldo11rt_iload_sel=1 */
if ((pds_level >= 0) && (pds_level <= 7)) {
HBN_Set_Ldo11rt_Drive_Strength(HBN_LDO11RT_DRIVE_STRENGTH_25_250UA);
} else if (pds_level == 31) {
} else if (pds_level == PM_PDS_LEVEL_31) {
HBN_Set_Ldo11rt_Drive_Strength(HBN_LDO11RT_DRIVE_STRENGTH_10_100UA);
} else {
/* pdsLevel error */
@ -963,14 +1003,52 @@ ATTR_TCM_SECTION void pm_pds_mode_enter(enum pm_pds_sleep_level pds_level, uint8
pPdsCfg->pdsCtl.pdsLdoVol = PM_PDS_LDO_LEVEL_DEFAULT;
pPdsCfg->pdsCtl.pdsLdoVselEn = 1;
#if PM_PDS_GPIO_IE_PUPD == 0
pPdsCfg->pdsCtl.gpioIePuPd = 0;
#endif
#if PM_PDS_RF_POWER_OFF == 0
pPdsCfg->pdsCtl.pdsCtlRfSel = 0;
#endif
/* config ldo11soc_sstart_delay_aon =2 , cr_pds_pd_ldo11=0 to speedup ldo11soc_rdy_aon */
AON_Set_LDO11_SOC_Sstart_Delay(0x2);
PDS_Default_Level_Config(pPdsCfg, sleep_time * 32768);
__WFI(); /* if(.wfiMask==0){CPU won't power down until PDS module had seen __wfi} */
PDS_Default_Level_Config(pPdsCfg, sleep_time);
if (store_flag) {
__asm__ __volatile__(
"csrr a0, mtvec\n\t"
"csrr a1, mstatus\n\t"
"la a2, __ld_pds_bak_addr\n\t"
"sw sp, 1*4(a2)\n\t"
"sw tp, 2*4(a2)\n\t"
"sw t0, 3*4(a2)\n\t"
"sw t1, 4*4(a2)\n\t"
"sw t2, 5*4(a2)\n\t"
"sw fp, 6*4(a2)\n\t"
"sw s1, 7*4(a2)\n\t"
"sw a0, 8*4(a2)\n\t"
"sw a1, 9*4(a2)\n\t"
"sw a3, 10*4(a2)\n\t"
"sw a4, 11*4(a2)\n\t"
"sw a5, 12*4(a2)\n\t"
"sw a6, 13*4(a2)\n\t"
"sw a7, 14*4(a2)\n\t"
"sw s2, 15*4(a2)\n\t"
"sw s3, 16*4(a2)\n\t"
"sw s4, 17*4(a2)\n\t"
"sw s5, 18*4(a2)\n\t"
"sw s6, 19*4(a2)\n\t"
"sw s7, 20*4(a2)\n\t"
"sw s8, 21*4(a2)\n\t"
"sw s9, 22*4(a2)\n\t"
"sw s10, 23*4(a2)\n\t"
"sw s11, 24*4(a2)\n\t"
"sw t3, 25*4(a2)\n\t"
"sw t4, 26*4(a2)\n\t"
"sw t5, 27*4(a2)\n\t"
"sw t6, 28*4(a2)\n\t");
}
pm_pds_enter_done(store_flag);
#if PM_PDS_PLL_POWER_OFF
GLB_Set_System_CLK(XTAL_TYPE, BSP_ROOT_CLOCK_SOURCE);
@ -978,15 +1056,26 @@ ATTR_TCM_SECTION void pm_pds_mode_enter(enum pm_pds_sleep_level pds_level, uint8
#endif
#if PM_PDS_FLASH_POWER_OFF
HBN_Set_Pad_23_28_Pullnone();
/* Init flash gpio */
SF_Cfg_Init_Flash_Gpio(0, 1);
if (pds_level < PM_PDS_LEVEL_4) {
HBN_Set_Pad_23_28_Pullnone();
/* Init flash gpio */
SF_Cfg_Init_Flash_Gpio(0, 1);
SF_Ctrl_Set_Owner(SF_CTRL_OWNER_SAHB);
SFlash_Restore_From_Powerdown(flash_cfg, 0);
SF_Ctrl_Set_Owner(SF_CTRL_OWNER_SAHB);
SFlash_Restore_From_Powerdown(flash_cfg, 0);
}
#endif
cpu_global_irq_enable();
if (store_flag) {
// Get cache way disable setting
*(volatile uint32_t *)(L1C_BASE + 0x00) &= ~(0x0F < 8);
*(volatile uint32_t *)(L1C_BASE + 0x00) |= cacheWayDisable;
// Get psram io configuration
*(volatile uint32_t *)(GLB_BASE + 0x88) = psramIoCfg;
}
}
/**
* @brief
@ -1080,9 +1169,11 @@ ATTR_TCM_SECTION void pm_hbn_mode_enter(enum pm_hbn_sleep_level hbn_level, uint8
}
}
void pm_hbn_set_wakeup_callback(void (*wakeup_callback)(void))
void pm_set_wakeup_callback(void (*wakeup_callback)(void))
{
BL_WR_REG(HBN_BASE, HBN_RSV1, (uint32_t)wakeup_callback);
/* Set HBN flag */
BL_WR_REG(HBN_BASE, HBN_RSV0, HBN_STATUS_ENTER_FLAG);
}
ATTR_HBN_RAM_SECTION void pm_hbn_enter_again(bool reset)
@ -1167,35 +1258,3 @@ void HBN_OUT1_IRQ(void)
__WEAK void pm_irq_callback(enum pm_event_type event)
{
}
/**
* @brief hal_pds_enter_with_time_compensation
*
* @param pdsLevel pds level support 0~3,31
* @param pdsSleepCycles if user set sleep time, pdsSleepCycles cannot be less than 32768, pdsSleepCycles is a multiple of 32768 preferably.
* @return uint32_t actual sleep time(ms)
*
* @note If necessaryplease application layer call vTaskStepTick
*/
uint32_t hal_pds_enter_with_time_compensation(uint32_t pdsLevel, uint32_t pdsSleepCycles)
{
uint32_t rtcLowBeforeSleep = 0, rtcHighBeforeSleep = 0;
uint32_t rtcLowAfterSleep = 0, rtcHighAfterSleep = 0;
uint32_t actualSleepDuration_32768cycles = 0;
uint32_t actualSleepDuration_ms = 0;
HBN_Get_RTC_Timer_Val(&rtcLowBeforeSleep, &rtcHighBeforeSleep);
pm_pds_mode_enter(pdsLevel, pdsSleepCycles / 32768);
HBN_Get_RTC_Timer_Val(&rtcLowAfterSleep, &rtcHighAfterSleep);
CHECK_PARAM((rtcHighAfterSleep - rtcHighBeforeSleep) <= 1); // make sure sleep less than 1 hour (2^32 us > 1 hour)
actualSleepDuration_32768cycles = (rtcLowAfterSleep - rtcLowBeforeSleep);
actualSleepDuration_ms = (actualSleepDuration_32768cycles >> 5) - (actualSleepDuration_32768cycles >> 11) - (actualSleepDuration_32768cycles >> 12);
// vTaskStepTick(actualSleepDuration_ms);
return actualSleepDuration_ms;
}

View file

@ -0,0 +1,291 @@
/**
* @file hal_pm_util.c
* @brief
*
* Copyright (c) 2021 Bouffalolab team
*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership. The
* ASF licenses this file to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance with the
* License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*
*/
#include "bl702_romdriver.h"
#include "bl702_sf_ctrl.h"
#include "hal_pm.h"
#include "hal_pm_util.h"
/* Cache Way Disable, will get from l1c register */
extern uint8_t cacheWayDisable;
/* PSRAM IO Configuration, will get from glb register */
extern uint32_t psramIoCfg;
/* Flash offset value, will get from sf_ctrl register */
extern uint32_t flash_offset;
extern SPI_Flash_Cfg_Type *flash_cfg;
void ATTR_PDS_RAM_SECTION pm_pds_fastboot_entry(void);
void (*tcm_recovery)(void) = NULL;
void (*board_recovery)(void) = NULL;
/**
* @brief hal_pds_enter_with_time_compensation
*
* @param pdsLevel pds level support 0~3,31
* @param pdsSleepCycles user set sleep time, clock of pds_time is 32768hz
* @return uint32_t actual sleep time(ms)
*
* @note If necessaryplease application layer call vTaskStepTick
*/
uint32_t hal_pds_enter_with_time_compensation(uint32_t pdsLevel, uint32_t pdsSleepCycles)
{
uint32_t rtcLowBeforeSleep = 0, rtcHighBeforeSleep = 0;
uint32_t rtcLowAfterSleep = 0, rtcHighAfterSleep = 0;
uint32_t actualSleepDuration_32768cycles = 0;
uint32_t actualSleepDuration_ms = 0;
if (pdsLevel >= 4 && HBN_Get_Status_Flag() != HBN_STATUS_ENTER_FLAG) {
pm_set_wakeup_callback(pm_pds_fastboot_entry);
}
HBN_Get_RTC_Timer_Val(&rtcLowBeforeSleep, &rtcHighBeforeSleep);
pm_pds_mode_enter(pdsLevel, pdsSleepCycles);
HBN_Get_RTC_Timer_Val(&rtcLowAfterSleep, &rtcHighAfterSleep);
CHECK_PARAM((rtcHighAfterSleep - rtcHighBeforeSleep) <= 1); // make sure sleep less than 1 hour (2^32 us > 1 hour)
actualSleepDuration_32768cycles = (rtcLowAfterSleep - rtcLowBeforeSleep);
actualSleepDuration_ms = (actualSleepDuration_32768cycles >> 5) - (actualSleepDuration_32768cycles >> 11) - (actualSleepDuration_32768cycles >> 12);
// vTaskStepTick(actualSleepDuration_ms);
return actualSleepDuration_ms;
}
/**
* @brief get delay value of spi flash init
*
* @param delay_index
* @return uint8_t
*/
static uint8_t ATTR_PDS_RAM_SECTION bflb_spi_flash_get_delay_val(uint8_t delay_index)
{
switch (delay_index) {
case 0:
return 0x00;
case 1:
return 0x80;
case 2:
return 0xc0;
case 3:
return 0xe0;
case 4:
return 0xf0;
case 5:
return 0xf8;
case 6:
return 0xfc;
case 7:
return 0xfe;
default:
return 0x00;
}
}
/**
* @brief config spi flash
*
* @param pFlashCfg flash parameter
*/
static void ATTR_PDS_RAM_SECTION bflb_spi_flash_set_sf_ctrl(SPI_Flash_Cfg_Type *pFlashCfg)
{
SF_Ctrl_Cfg_Type sfCtrlCfg;
uint8_t delay_index;
sfCtrlCfg.owner = SF_CTRL_OWNER_SAHB;
/* bit0-3 for clk delay */
sfCtrlCfg.clkDelay = (pFlashCfg->clkDelay & 0x0f);
/* bit0 for clk invert */
sfCtrlCfg.clkInvert = pFlashCfg->clkInvert & 0x01;
/* bit1 for rx clk invert */
sfCtrlCfg.rxClkInvert = (pFlashCfg->clkInvert >> 1) & 0x01;
/* bit4-6 for do delay */
delay_index = (pFlashCfg->clkDelay >> 4) & 0x07;
sfCtrlCfg.doDelay = bflb_spi_flash_get_delay_val(delay_index);
/* bit2-4 for di delay */
delay_index = (pFlashCfg->clkInvert >> 2) & 0x07;
sfCtrlCfg.diDelay = bflb_spi_flash_get_delay_val(delay_index);
/* bit5-7 for oe delay */
delay_index = (pFlashCfg->clkInvert >> 5) & 0x07;
sfCtrlCfg.oeDelay = bflb_spi_flash_get_delay_val(delay_index);
RomDriver_SFlash_Init(&sfCtrlCfg);
}
/**
* @brief
*
* @param media_boot
* @return int32_t
*/
int32_t ATTR_PDS_RAM_SECTION pm_spi_flash_init(uint8_t media_boot)
{
uint32_t stat;
uint32_t jdecId = 0;
uint32_t flash_read_try = 0;
/*use fclk as flash clok */
RomDriver_GLB_Set_SF_CLK(1, GLB_SFLASH_CLK_BCLK, 0); // 32M
bflb_spi_flash_set_sf_ctrl(flash_cfg);
/* Wake flash up from power down */
RomDriver_SFlash_Releae_Powerdown(flash_cfg);
//ARCH_Delay_US(15*((pFlashCfg->pdDelay&0x7)+1));
RomDriver_BL702_Delay_US(120);
do {
if (flash_read_try > 4) {
// bflb_bootrom_printd("Flash read id TO\r\n");
break;
} else if (flash_read_try > 0) {
RomDriver_BL702_Delay_US(500);
}
// bflb_bootrom_printd("reset flash\r\n");
/* Exit form continous read for accepting command */
RomDriver_SFlash_Reset_Continue_Read(flash_cfg);
/* Send software reset command(80bv has no this command)to deburst wrap for ISSI like */
RomDriver_SFlash_Software_Reset(flash_cfg);
/* Disable burst may be removed(except for 80BV) and only work with winbond,but just for make sure */
RomDriver_SFlash_Write_Enable(flash_cfg);
/* For disable command that is setting register instaed of send command, we need write enable */
RomDriver_SFlash_DisableBurstWrap(flash_cfg);
stat = RomDriver_SFlash_SetSPIMode(SF_CTRL_SPI_MODE);
if (SUCCESS != stat) {
// bflb_bootrom_printe("enter spi mode fail %d\r\n", stat);
// return BFLB_BOOTROM_FLASH_INIT_ERROR;
return -1;
}
RomDriver_SFlash_GetJedecId(flash_cfg, (uint8_t *)&jdecId);
/* Dummy disable burstwrap for make sure */
RomDriver_SFlash_Write_Enable(flash_cfg);
/* For disable command that is setting register instead of send command, we need write enable */
RomDriver_SFlash_DisableBurstWrap(flash_cfg);
jdecId = jdecId & 0xffffff;
// bflb_bootrom_printd("ID =%08x\r\n", jdecId);
flash_read_try++;
} while ((jdecId & 0x00ffff) == 0 || (jdecId & 0xffff00) == 0 || (jdecId & 0x00ffff) == 0xffff || (jdecId & 0xffff00) == 0xffff00);
/*clear offset setting*/
// reset image offset
BL_WR_REG(SF_CTRL_BASE, SF_CTRL_SF_ID0_OFFSET, flash_offset);
/* set read mode */
if ((flash_cfg->ioMode & 0x0f) == SF_CTRL_QO_MODE || (flash_cfg->ioMode & 0x0f) == SF_CTRL_QIO_MODE) {
stat = RomDriver_SFlash_Qspi_Enable(flash_cfg);
}
if (media_boot) {
RomDriver_L1C_Set_Wrap(DISABLE);
RomDriver_SFlash_Cache_Read_Enable(flash_cfg, flash_cfg->ioMode & 0xf, 0, 0x00);
}
return jdecId;
}
// can be placed in flash, here placed in pds section to reduce fast boot time
static void ATTR_PDS_RAM_SECTION pm_pds_restore_cpu_reg(void)
{
__asm__ __volatile__(
"la a2, __ld_pds_bak_addr\n\t"
"lw ra, 0(a2)\n\t"
"lw sp, 1*4(a2)\n\t"
"lw tp, 2*4(a2)\n\t"
"lw t0, 3*4(a2)\n\t"
"lw t1, 4*4(a2)\n\t"
"lw t2, 5*4(a2)\n\t"
"lw fp, 6*4(a2)\n\t"
"lw s1, 7*4(a2)\n\t"
"lw a0, 8*4(a2)\n\t"
"lw a1, 9*4(a2)\n\t"
"lw a3, 10*4(a2)\n\t"
"lw a4, 11*4(a2)\n\t"
"lw a5, 12*4(a2)\n\t"
"lw a6, 13*4(a2)\n\t"
"lw a7, 14*4(a2)\n\t"
"lw s2, 15*4(a2)\n\t"
"lw s3, 16*4(a2)\n\t"
"lw s4, 17*4(a2)\n\t"
"lw s5, 18*4(a2)\n\t"
"lw s6, 19*4(a2)\n\t"
"lw s7, 20*4(a2)\n\t"
"lw s8, 21*4(a2)\n\t"
"lw s9, 22*4(a2)\n\t"
"lw s10, 23*4(a2)\n\t"
"lw s11, 24*4(a2)\n\t"
"lw t3, 25*4(a2)\n\t"
"lw t4, 26*4(a2)\n\t"
"lw t5, 27*4(a2)\n\t"
"lw t6, 28*4(a2)\n\t"
"csrw mtvec, a0\n\t"
"csrw mstatus,a1\n\t"
"ret\n\t");
}
// must be placed in pds section
void ATTR_PDS_RAM_SECTION pm_pds_fastboot_entry(void)
{
// reload gp register
__asm__ __volatile__(
".option push\n\t"
".option norelax\n\t"
"la gp, __global_pointer$\n\t"
".option pop\n\t");
// recovery flash pad and param
RomDriver_SF_Cfg_Init_Flash_Gpio(0, 1);
pm_spi_flash_init(1);
// Restore tcm code
if (tcm_recovery)
tcm_recovery();
// Recovery gpio and clock
if (board_recovery)
board_recovery();
// Restore cpu registers
pm_pds_restore_cpu_reg();
}
void pm_set_tcm_recovery_callback(void (*tcm_recovery_cb)(void))
{
tcm_recovery = tcm_recovery_cb;
}
void pm_set_board_recovery_callback(void (*board_recovery_cb)(void))
{
board_recovery = board_recovery_cb;
}

View file

@ -123,7 +123,7 @@ int hbn_run_in_wakeup_addr(int argc, char *argv[])
/*cpu will wakeup when you set gpio9-gpio12 with GPIO_FUN_WAKEUP
* rtc can not wakeup level2
**/
pm_hbn_set_wakeup_callback(led_blink); /*cpu will run in wakeup callback not reset when it awakes*/
pm_set_wakeup_callback(led_blink); /*cpu will run in wakeup callback not reset when it awakes*/
pm_hbn_mode_enter(PM_HBN_LEVEL_0, 0);
return 0;