This post is older than 2 years and might not be relevant anymore
More Info: Consider searching for newer posts

I can't write data to flash.

Hi,

I have an application which has 2 services and 3 characteristics. I want to keep the value of characteristics when the characteristic value changed. I tried FDS and i can not make it so i tried Flash Storage and it didn't work. I looked all examples and also GitHub but i couln't make it work. Where am I making a mistake? Here is my code of Flash Storage;

My aim is keep the data save when mcu power gone. I am using nrf52840 DK, SoftDevice s140

#define NUM_PAGES 4
#define PAGE_SIZE_WORDS 1024
static void fstorage_evt_handler(nrf_fstorage_evt_t * p_evt);
//static void fstorage_init(void)
//{
    NRF_FSTORAGE_DEF(nrf_fstorage_t fstorage) =
    {
        /* Set a handler for fstorage events. */
        .evt_handler = fstorage_evt_handler,

        /* These below are the boundaries of the flash space assigned to this instance of fstorage.
         * You must set these manually, even at runtime, before nrf_fstorage_init() is called.
         * The function nrf5_flash_end_addr_get() can be used to retrieve the last address on the
         * last page of flash available to write data. */
        .start_addr = 0x3e000,
        .end_addr   = 0x3ffff,
        
    };

//}

void fstorage_write(void)
{
    ret_code_t rc = nrf_fstorage_write(
    &fstorage,   /* The instance to use. */
    0x3F000,     /* The address in flash where to store the data. */
    &number,        /* A pointer to the data. */
    sizeof(number), /* Lenght of the data, in bytes. */
    NULL            /* Optional parameter, backend-dependent. */
    );
        if (rc == NRF_SUCCESS)
        {
            /* The operation was accepted.
               Upon completion, the NRF_FSTORAGE_WRITE_RESULT event
               is sent to the callback function registered by the instance. */
        }
        else
        {
            /* Handle error.*/
        }
}

void fstorage_erase(void)
{
    static uint32_t pages_to_erase = 4;
    ret_code_t rc = nrf_fstorage_erase(
    &fstorage,   /* The instance to use. */
    0x3F000,     /* The address of the flash pages to erase. */
    pages_to_erase, /* The number of pages to erase. */
    NULL            /* Optional parameter, backend-dependent. */
    );
    if (rc == NRF_SUCCESS)
    {
        /* The operation was accepted.
           Upon completion, the NRF_FSTORAGE_ERASE_RESULT event
           is sent to the callback function registered by the instance. */
    }
    else
    {
        /* Handle error.*/
    }
}

void fstorage_read(void)
{
  
    ret_code_t rc = nrf_fstorage_read(
    &fstorage,   /* The instance to use. */
    0x3F000,     /* The address in flash where to read data from. */
    &number,        /* A buffer to copy the data into. */
    sizeof(number)  /* Lenght of the data, in bytes. */
    );
      if (rc == NRF_SUCCESS)
      {
          /* The operation was accepted.
             Upon completion, the NRF_FSTORAGE_READ_RESULT event
             is sent to the callback function registered by the instance.
             Once the event is received, it is possible to read the contents of 'number'. */
      }
      else
      {
          /* Handle error.*/
      }
}

here is my timer;

// This is a timer event handler
static void timer_timeout_handler(void * p_context)
{
    // Update temperature characteristic and voltage characteristic value.
    //int32_t temperature = 0;   
    //sd_temp_get(&temperature);
    voltage = 0;
    voltage = saadc_measure();
      if(voltage < 0)   //don't let adc value negative
      voltage = 0;
    our_voltage_characteristic_update(&m_our_service, &voltage);
    our_temperature_characteristic_update_2(&m_our_service, &number); //update characteristic value every 100ms

    if(button_value == 1) //this condition changes characteristics value
    {
      nrf_gpio_pin_toggle(LED_4); //code check
      number=0x35;
      fstorage_erase();  //erase before writing
      fstorage_write();   //write the new value 
      
    }

}

here is fs event handler;

static void fstorage_evt_handler(nrf_fstorage_evt_t * p_evt)
{
    if (p_evt->result != NRF_SUCCESS)
    {
        NRF_LOG_INFO("--> Event received: ERROR while executing an fstorage operation.");
        return;
    }

    switch (p_evt->id)
    {
        case NRF_FSTORAGE_EVT_WRITE_RESULT:
        {
            NRF_LOG_INFO("--> Event received: wrote %d bytes at address 0x%x.",
                         p_evt->len, p_evt->addr);
        } break;

        case NRF_FSTORAGE_EVT_ERASE_RESULT:
        {
            NRF_LOG_INFO("--> Event received: erased %d page from address 0x%x.",
                         p_evt->len, p_evt->addr);
        } break;

        default:
            break;
    }
}

and main;

int main(void)
{
    bool erase_bonds;
    // Initialize.
    log_init();
    
    ret_code_t ret;
    
    serial_uart_init();
    timers_init();
    buttons_leds_init(&erase_bonds);
    power_management_init();
    ble_stack_init();
    gap_params_init();
    gatt_init();
    saadc_init();
    services_init();
    advertising_init();

    conn_params_init();
    peer_manager_init();

    // Start execution.
    NRF_LOG_INFO("OurCharacteristics tutorial started.");
    application_timers_start();
    advertising_start(erase_bonds);
    
    nrf_fstorage_init(&fstorage,&nrf_fstorage_sd,NULL);
    
    fstorage_read();  //read the flash value when mcu is restarting after mcu power off

    // Enter main loop.

Parents
  • Hi,

    I do not immediately see the problem, but I have a few questions:

    • In what way does it not work? Please explain in detail.
      • Do you get some error returned from a function call?
      • Do you not get the expected events?
      • Do you not read back valid data?
    • Can you upload the log from running your app? (I see you have logging in the fstorage event handler). Before that, it would be useful to add logging in your fstorage_write(), fstorage_read() and fstorage_erase() functions.

    By the way, I recommend you go back to using FDS. Since you need to update the data, FDS is probably what you want, as that gives you all the functionality you need for free.

Reply
  • Hi,

    I do not immediately see the problem, but I have a few questions:

    • In what way does it not work? Please explain in detail.
      • Do you get some error returned from a function call?
      • Do you not get the expected events?
      • Do you not read back valid data?
    • Can you upload the log from running your app? (I see you have logging in the fstorage event handler). Before that, it would be useful to add logging in your fstorage_write(), fstorage_read() and fstorage_erase() functions.

    By the way, I recommend you go back to using FDS. Since you need to update the data, FDS is probably what you want, as that gives you all the functionality you need for free.

Children
  • I'm trying FDS and i think i can succesfully write flash. Here is my code and output(I cut the irrelevant parts);

    My aim is still same. I want to keep characteristic value in flash and keep it safe when power lost. But i can't assign the data in flash to my variable (uint16_t number) when DK restart. When i cut power and restart DK, i checked the FDS stats and it said "2 valid 1 dirty record found". I think i can write to the flash but i can not read the value from it. 

    main.c

    
    #include "our_service.h"
    #include "fds_config.h"
    
    
    #define SCHED_MAX_EVENT_DATA_SIZE       sizeof(app_button_cfg_t)
    #define SCHED_QUEUE_SIZE                20        
    
    
    ////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
    
    ///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
    fds_record_desc_t   record_desc;
    fds_record_t        record;
    
    
    ///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
                                                /**< Advertising module instance. */
    
    
    #define DEAD_BEEF                       0xDEADBEEF                              /**< Value used as error code on stack dump, can be used to identify stack location on stack unwind. */
    
    
    int button_value;
    uint16_t number;
    
    static bool button_1;
    static bool button_2;
    static bool button_3;
    static bool button_4;
    
    static int TIME_COUNTER=0;
    
    
    
    
    
    // Declare an app_timer id variable and define our timer interval and define a timer interval
    APP_TIMER_DEF(m_our_char_timer_id);
    APP_TIMER_DEF(m_our_char_timer_id_2);
    APP_TIMER_DEF(m_our_char_timer_id_3);
    
    
    
    int gpio_set(void)
    {
      nrf_gpio_cfg_output(LED_1);
      nrf_gpio_cfg_output(LED_2);
      nrf_gpio_cfg_output(LED_3);
      nrf_gpio_cfg_output(LED_4);
      nrf_gpio_pin_set(LED_1);
      nrf_gpio_pin_set(LED_2);
      nrf_gpio_pin_set(LED_3);
      nrf_gpio_pin_set(LED_4);
    }
    
    
    void button_handler(uint8_t pin_no, uint8_t button_action)
    {
        NRF_LOG_INFO("Button press detected \r\n");
        if(button_action == APP_BUTTON_PUSH)
        {
            switch(pin_no)
            {
                case BUTTON_1:
                    nrf_gpio_pin_toggle(LED_1);
                    NRF_LOG_INFO("Button 1 pressed \r\n");
                    button_1 = true;
                    break;
                case BUTTON_2:
                    nrf_gpio_pin_toggle(LED_2);
                    NRF_LOG_INFO("Button 2 pressed \r\n");
                    button_2 = true;
                    break;
                case BUTTON_3:
                    nrf_gpio_pin_toggle(LED_3);
                    NRF_LOG_INFO("Button 3 pressed \r\n");
                    button_3 = true;
                    break;
                case BUTTON_4:
                    nrf_gpio_pin_toggle(LED_4);
                    NRF_LOG_INFO("Button 4 pressed \r\n");
                    button_4 = true;  
                    break;
                default:
                    break;
            }
        }
    }
    
    int buttons_init(void)
    {
     static  app_button_cfg_t p_buttons[4] = {{BUTTON_1, APP_BUTTON_ACTIVE_LOW, NRF_GPIO_PIN_PULLUP, button_handler},
                                          {BUTTON_2, APP_BUTTON_ACTIVE_LOW, NRF_GPIO_PIN_PULLUP, button_handler},
                                          {BUTTON_3, APP_BUTTON_ACTIVE_LOW, NRF_GPIO_PIN_PULLUP, button_handler},
                                          {BUTTON_4, APP_BUTTON_ACTIVE_LOW, NRF_GPIO_PIN_PULLUP, button_handler}};
     app_button_init(p_buttons, 4, 5);
    
     app_button_enable();
    }
    
    
    // This is a timer event handler
    static void timer_timeout_handler(void * p_context)
    {
        // Update temperature characteristic and voltage characteristic value.
    //    int32_t temperature = 0;   
    //    sd_temp_get(&temperature);
        voltage = 0;
        voltage = saadc_measure();
    
          if(voltage < 0)   //don't let adc value negative
             voltage = 0;
    
        our_voltage_characteristic_update(&m_our_service, &voltage);
        our_temperature_characteristic_update_2(&m_our_service, &number);
    
        if(button_value == 1)
        {
          nrf_gpio_pin_toggle(LED_4);
          number = 0x3132;
    //      enter_Scheduler_flag = true;
    //      app_sched_event_put(&saklanacak_deger, sizeof(saklanacak_deger), timer_scheduler_event_handler);
        }
    
    }
    
    
    static void timer_timeout_handler_2(void * p_context)
    {
      ++TIME_COUNTER;
    
                    
    }
    static void timer_timeout_handler_3(void * p_context)
    {
    
    }
    
    
    
    
    /**@brief Function for the Timer initialization.
     *
     * @details Initializes the timer module. This creates and starts application timers.
     */
    static void timers_init(void)
    {
        // Initialize timer module.
        ret_code_t err_code = app_timer_init();
        APP_ERROR_CHECK(err_code);
    
        app_timer_create(&m_our_char_timer_id, APP_TIMER_MODE_REPEATED, timer_timeout_handler);
                        
        app_timer_create(&m_our_char_timer_id_2, APP_TIMER_MODE_REPEATED, timer_timeout_handler_2);
    //
        app_timer_create(&m_our_char_timer_id_3, APP_TIMER_MODE_REPEATED, timer_timeout_handler_3);
        
    }
    
    
    
    
    /**@brief Function for starting timers.
     */
    static void application_timers_start(void)
    {
    
        // Start our timer
        app_timer_start(m_our_char_timer_id, OUR_CHAR_TIMER_INTERVAL, NULL);     
                       
        app_timer_start(m_our_char_timer_id_2, APP_TIMER_TICKS(500), NULL);   
                           
        app_timer_start(m_our_char_timer_id_3, APP_TIMER_TICKS(5000), NULL);          
    
    }
    
    
    
    
    
    
    
    
    /**@brief Function for the Event Scheduler initialization.
     */
    static void scheduler_init(void)
    {
        APP_SCHED_INIT(SCHED_MAX_EVENT_DATA_SIZE, SCHED_QUEUE_SIZE);
    }
    
    
    static void fds_evt_handler(fds_evt_t const * p_evt)
    {
        NRF_LOG_INFO("Event: %s received (%s)",
                      fds_evt_str[p_evt->id],
                      fds_err_str[p_evt->result]);
    
        switch (p_evt->id)
        {
            case FDS_EVT_INIT:
                if (p_evt->result == FDS_SUCCESS)
                {
                    m_fds_initialized = true;
                }
                break;
    
            case FDS_EVT_WRITE:
            {
                if (p_evt->result == FDS_SUCCESS)
                {
                    NRF_LOG_INFO("Record ID:\t0x%04x",  p_evt->write.record_id);
                    NRF_LOG_INFO("File ID:\t0x%04x",    p_evt->write.file_id);
                    NRF_LOG_INFO("Record key:\t0x%04x", p_evt->write.record_key);
                }
            } break;
    
            case FDS_EVT_DEL_RECORD:
            {
                if (p_evt->result == FDS_SUCCESS)
                {
                    NRF_LOG_INFO("Record ID:\t0x%04x",  p_evt->del.record_id);
                    NRF_LOG_INFO("File ID:\t0x%04x",    p_evt->del.file_id);
                    NRF_LOG_INFO("Record key:\t0x%04x", p_evt->del.record_key);
                }
                m_delete_all.pending = false;
            } break;
    
            default:
                break;
        }
    }
    
    /**@brief   Sleep until an event is received. */
    static void power_manage(void)
    {
    #ifdef SOFTDEVICE_PRESENT
        (void) sd_app_evt_wait();
    #else
        __WFE();
    #endif
    }
    
    /**@brief   Wait for fds to initialize. */
    static void wait_for_fds_ready(void)
    {
        while (!m_fds_initialized)
        {
            power_manage();
        }
    }
    
    //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
    
    
    //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
    
    /**@brief Function for application main entry.
     */
    int main(void)
    {   
        
    
        bool erase_bonds;
        // Initialize.
        log_init();
        
        ret_code_t ret;
        ret_code_t err_code;
        ret_code_t rc;
        gpio_set();
        timers_init();
        buttons_leds_init(&erase_bonds);
        buttons_init();
        power_management_init();
        ble_stack_init();
        gap_params_init();
        gatt_init();
        saadc_init();
        services_init();
        advertising_init();
        conn_params_init();
        peer_manager_init();
         
        // Start execution.
        NRF_LOG_INFO("OurCharacteristics tutorial started.");
    
        application_timers_start();
        advertising_start(erase_bonds);
        
        /* Register first to receive an event when initialization is complete. */
        (void) fds_register(fds_evt_handler);
        rc = fds_init();
        APP_ERROR_CHECK(rc);
        
      /* Wait for fds to initialize. */
         wait_for_fds_ready();
         
       // Set up record.
          record.file_id           = 0x0002;
          record.key               = 0x1002;
          record.data.p_data       = &number;
          record.data.length_words = (sizeof(number)+3)/sizeof(uint32_t);   /* one word is four bytes. */
     
    
         NRF_LOG_INFO("FDS_REGISTER complete.");
    
    
        // Enter main loop.
        for (;;)
        {
            idle_state_handle(); 
            //app_sched_execute();
            
            if(TIME_COUNTER>2)
            {
              TIME_COUNTER = 9;
    
              if(button_1 == true)
              {
                nrf_gpio_pin_toggle(LED_2);
                delete_all_begin();
                delete_all_process();
    
                button_1 = false;
              }
              else if(button_2 == true)
              {
                    ret_code_t rc; 
    
               /* System config not found; write a new one. */
                  NRF_LOG_INFO("Writing config file...");
    
                  rc = fds_record_write(&record_desc, &record);
                  APP_ERROR_CHECK(rc);
    
                  button_2 = false;
              }
              else if (button_3 == true)
              {
                    ret_code_t ret = fds_gc();
                    APP_ERROR_CHECK(ret);
                    NRF_LOG_INFO("garbage collection returned %s\r\n", fds_err_str[rc]);
                     button_3 = false;
              }
              else if(button_4 == true)
              {
                    fds_stat_t stat = {0};
    
                    rc = fds_stat(&stat);
                    APP_ERROR_CHECK(rc);
    
                    NRF_LOG_INFO("Found %d valid records.", stat.valid_records);
                    NRF_LOG_INFO("Found %d dirty records (ready to be garbage collected).", stat.dirty_records);
        
    
                      fds_record_desc_t desc = {0};
                      fds_find_token_t  tok  = {0};
    
                      rc = fds_record_find(CONFIG_FILE, CONFIG_REC_KEY, &desc, &tok);
                      if (rc == FDS_SUCCESS)
                        {
                            /* A config file is in flash. Let's update it. */
                            fds_flash_record_t config = {0};
    
                            /* Open the record and read its contents. */
                            rc = fds_record_open(&desc, &config);
                            APP_ERROR_CHECK(rc);
    
                            /* Copy the configuration from flash into m_dummy_cfg. */
                            memcpy(&record, config.p_data, sizeof(number));
    
                            NRF_LOG_INFO("Config file found, updating boot count to %d.", m_dummy_cfg.boot_count);
    
               
    
                            /* Close the record when done reading. */
                            rc = fds_record_close(&desc);
                            APP_ERROR_CHECK(rc);
    
    //                        /* Write the updated record to flash. */
    //                        rc = fds_record_update(&desc, &m_dummy_record);
    //                        APP_ERROR_CHECK(rc);
                        }
    //                    else
    //                    {
    //                        /* System config not found; write a new one. */
    //                        NRF_LOG_INFO("Writing config file...");
    //
    //                        rc = fds_record_write(&desc, &m_dummy_record);
    //                        APP_ERROR_CHECK(rc);
    //                    }
                button_4 = false;
              }
            
            }
            
        }
    }
    
    
    /**
     * @}
     */
    

    fds_config.h

    #include "fds.h"
    #include "nrf_fstorage.h"
    #include "nrf_fstorage_sd.h"
    
    #define CONFIG_FILE     0x0001
    #define CONFIG_REC_KEY  0x1000
    
    
    /* Keep track of the progress of a delete_all operation. */
    static struct
    {
        bool delete_next;   //!< Delete next record.
        bool pending;       //!< Waiting for an fds FDS_EVT_DEL_RECORD event, to delete the next record.
    } m_delete_all;
    
    
    
    /* A dummy structure to save in flash. */
    typedef struct
    {
        uint32_t boot_count;
        char     device_name[16];
        bool     config1_on;
        bool     config2_on;
    } configuration_t;
    
    
    /* Array to map FDS return values to strings. */
    char const * fds_err_str[] =
    {
        "FDS_SUCCESS",
        "FDS_ERR_OPERATION_TIMEOUT",
        "FDS_ERR_NOT_INITIALIZED",
        "FDS_ERR_UNALIGNED_ADDR",
        "FDS_ERR_INVALID_ARG",
        "FDS_ERR_NULL_ARG",
        "FDS_ERR_NO_OPEN_RECORDS",
        "FDS_ERR_NO_SPACE_IN_FLASH",
        "FDS_ERR_NO_SPACE_IN_QUEUES",
        "FDS_ERR_RECORD_TOO_LARGE",
        "FDS_ERR_NOT_FOUND",
        "FDS_ERR_NO_PAGES",
        "FDS_ERR_USER_LIMIT_REACHED",
        "FDS_ERR_CRC_CHECK_FAILED",
        "FDS_ERR_BUSY",
        "FDS_ERR_INTERNAL",
    };
    
    /* Array to map FDS events to strings. */
    static char const * fds_evt_str[] =
    {
        "FDS_EVT_INIT",
        "FDS_EVT_WRITE",
        "FDS_EVT_UPDATE",
        "FDS_EVT_DEL_RECORD",
        "FDS_EVT_DEL_FILE",
        "FDS_EVT_GC",
    };
    
    /* Dummy configuration data. */
    static configuration_t m_dummy_cfg =
    {
        .config1_on  = false,
        .config2_on  = true,
        .boot_count  = 0x0,
        .device_name = "dummy",
    };
    
    /* A record containing dummy configuration data. */
    static fds_record_t const m_dummy_record =
    {
        .file_id           = CONFIG_FILE,
        .key               = CONFIG_REC_KEY,
        .data.p_data       = &m_dummy_cfg,
        /* The length of a record is always expressed in 4-byte units (words). */
        .data.length_words = (sizeof(m_dummy_cfg) + 3) / sizeof(uint32_t),
    };
    
    /* Flag to check fds initialization. */
    static bool volatile m_fds_initialized;
    
    
    bool record_delete_next(void)
    {
        fds_find_token_t  tok   = {0};
        fds_record_desc_t desc  = {0};
    
        if (fds_record_iterate(&desc, &tok) == FDS_SUCCESS)
        {
            ret_code_t rc = fds_record_delete(&desc);
            if (rc != FDS_SUCCESS)
            {
                return false;
            }
    
            return true;
        }
        else
        {
            /* No records left to delete. */
            return false;
        }
    }
    
    
    /**@brief   Begin deleting all records, one by one. */
    void delete_all_begin(void)
    {
        m_delete_all.delete_next = true;
    }
    
    
    /**@brief   Process a delete all command.
     *
     * Delete records, one by one, until no records are left.
     */
    void delete_all_process(void)
    {
        if (   m_delete_all.delete_next
            & !m_delete_all.pending)
        {
            NRF_LOG_INFO("Deleting next record.");
    
            m_delete_all.delete_next = record_delete_next();
            if (!m_delete_all.delete_next)
            {
                NRF_LOG_INFO("No records left to delete.");
            }
        }
    }
    

    Output:

    Button 1 : Delete record one by one.(i think it deletes last one am i right?)

    Button 2 : Write variable number to flash.

    Button 3 : Run GC.

    Button 4 : Show stats.

    <info> app: OurCharacteristics tutorial started.
    <info> app: Fast advertising.
    <info> app: Event: FDS_EVT_INIT received (FDS_SUCCESS)
    <info> app: FDS_REGISTER complete.
    
    <info> app: Button press detected
    <info> app: Button 2 pressed
    <info> app: Writing config file...
    <info> app: Event: FDS_EVT_WRITE received (FDS_SUCCESS)
    <info> app: Record ID:  0x0001
    <info> app: File ID:    0x0002
    <info> app: Record key: 0x1002
    <info> app: Button press detected
    
    <info> app: Button press detected
    <info> app: Button 4 pressed
    <info> app: Found 1 valid records.
    <info> app: Found 1 dirty records (ready to be garbage collected).
    <info> app: Button press detected
    
    
    <info> app: Button press detected
    <info> app: Button 3 pressed
    <info> app: garbage collection returned FDS_ERR_NOT_FOUND
    <info> app: Event: FDS_EVT_GC received (FDS_SUCCESS)
    <info> app: Button press detected
    
    <info> app: Button press detected
    <info> app: Button 4 pressed
    <info> app: Found 1 valid records.
    <info> app: Found 0 dirty records (ready to be garbage collected).
    <info> app: Button press detected
    
    <info> app: Button press detected
    <info> app: Button 1 pressed
    <info> app: Deleting next record.
    <info> app: Event: FDS_EVT_DEL_RECORD received (FDS_SUCCESS)
    <info> app: Record ID:  0x0001
    <info> app: File ID:    0x0002
    <info> app: Record key: 0x1002
    <info> app: Button press detected
    

    How fds retrieve value ?Does it copy the data inside of flash to record.data.p_data ?

    I am very confused...

  • Hi,

    FDS usage is not that complicated, you just need to understand the basics. FDS organizes records by file ID and record ID, and you can search for those. So when you store a record you provide the file and record ID, and when you want to read it you need to search for the same record ID. You can see a good description of this under Usage in the FDS documentation.

    So you need to fix this:

    1. Write your records with whatever you select as record and file id(s)
    2. To read, search for the record(s) using fds_record_find() as shown in the page I linked to.
    3. Open the record you have found with fds_record_open() to read the actual data.
    4. Close the record with fds_record_close() when you are don reading it.
Related