/* * @Author: mbw * @Date: 2024-08-22 16:15:08 * @LastEditors: mbw && 1600520629@qq.com * @LastEditTime: 2024-08-31 10:24:34 * @FilePath: \USART1_Interrupt - RT-Thread\bsp\src\bsp_flash.c * @Description: * * Copyright (c) 2024 by ${git_name_email}, All Rights Reserved. */ #include "bsp_flash.h" #include "bsp_rtc.h" #include "rthw.h" #include "stdlib.h" #include "user_rtt.h" #include "perf_counter.h" #define LOG_TAG "bsp_flash" // 该模块对应的标签。不定义时,默认:NO_TAG #define LOG_LVL LOG_LVL_DBG // 该模块对应的日志输出级别。不定义时,默认:调试级别 #include // 必须在 LOG_TAG 与 LOG_LVL 下面 // #define TEST 512 #define MAX(a, b) ((a) > (b)) ? (a) : (b) ErrorStatus Flash_Write_Record(TeRecord record); const uint32_t hr_start_addr[7] = {FLASH_HR_ALARM_START_ADDR, FLASH_HR_ALARM_RCY_START_ADDR, FLASH_HR_FAULT_START_ADDR, FLASH_HR_FAULT_RCY_START_ADDR, FLASH_HR_POWER_FAILURE_START_ADDR, FLASH_HR_POWER_ON_START_ADDR, FLASH_HR_SENSOR_FAILURE_START_ADDR}; /*各历史数据的条数*/ const uint16_t hr_record_max_num[7] = {HR_ALARM_MAX_NUM, HR_ALARM_RCY_MAX_NUM, HR_FAULT_MAX_NUM, HR_FAULT_RCY_MAX_NUM, HR_POWER_FAILURE_MAX_NUM, HR_POWER_ON_MAX_NUM, HR_SENSOR_FAILURE_MAX_NUM}; const uint8_t hr_record_pages[7] = {HR_ALARM_PAGES, HR_ALARM_RCY_PAGES, HR_FAULT_PAGES, HR_FAULT_RCY_PAGES, HR_POWER_FAILURE_PAGES, HR_POWER_ON_PAGES, HR_SENSOR_FAILURE_PAGES}; const uint8_t hr_record_start_page_num[7] = {FLASH_HR_ALARM_PAGE, FLASH_HR_ALARM_RCY_PAGE, FLASH_HR_FAULT_PAGE, FLASH_HR_FAULT_RCY_PAGE, FLASH_HR_POWER_FAILURE_PAGE, FLASH_HR_POWER_ON_PAGE, FLASH_HR_SENSOR_FAILIRE_START_PAGE}; // 定义常量 #define CHECKSUM_MASK 0xFF // 数据帧结构体 typedef struct { uint32_t headerIndexChecksum; // 帧头、索引和校验和 uint32_t timestamp; // 时间戳 } DataFrame; static rt_base_t interrupt_value; static void BSP_Flash_UnLock(void) { interrupt_value = rt_hw_interrupt_disable(); /* Flash控制寄存器解锁 */ std_flash_unlock(); } static void BSP_Flash_Lock(void) { /* Flash控制寄存器加锁 */ std_flash_lock(); rt_hw_interrupt_enable(interrupt_value); } // 计算和校验值 uint8_t Sum_Check(TuFlashHrRecordFrame *frame) { uint8_t sum = 0; sum += (frame->time.year << 4) | frame->time.month; sum += (frame->time.day << 3) | (frame->time.hour >> 2); sum += ((frame->time.hour & 0x03) << 6) | frame->time.minute; return ~sum; // 取反作为校验和 } #if 1 /// @brief /// @param addr /// @param buf /// @param len /// @param mode 选择读的大小 字节 半字 字 /// @return static size_t Flash_Read(uint32_t addr, uint8_t *buf, size_t len) { size_t read_len = 0; for (size_t i = 0; i < len; i++, buf++, addr++, read_len++) { *buf = *(uint8_t *)addr; } return read_len; } /** * @description: 向Flash中写入数据 * @param {uint32_t} addr * @param {uint8_t} *buf * @param {size_t} len * @return {*} */ static std_status_t Flash_WriteWord(uint32_t addr, uint32_t write_data) { if (addr & 0x03 != 0) // 地址4字节对齐 { LOG_E("addr %#X is not aligned\r\n", addr); return ERROR; } /* 清除错误标志 */ std_flash_clear_flag(FLASH_FLAG_ALL_ERR); BSP_Flash_UnLock(); if (std_flash_get_lock_status() == false) { if (std_flash_word_program(addr, write_data) != SUCCESS) // 写入操作 字节写入 { LOG_D("addr = %#X, read %d", addr, *(uint32_t *)addr); BSP_Flash_Lock(); return ERROR; } if (*(uint32_t *)addr != write_data) { LOG_D("Flash_Write Data Mismatch at addr %#X, expected %d got %d", addr, write_data, *(uint32_t *)addr); BSP_Flash_Lock(); return ERROR; } } BSP_Flash_Lock(); return SUCCESS; } static std_status_t Flash_WritePage(uint32_t addr, uint32_t *buf) { std_status_t status = SUCCESS; if (addr & 0x03 != 0) // 地址4字节对齐 { LOG_E("addr %#X is not aligned\r\n", addr); return ERROR; } for (int i = 0; i < FLASH_HR_PAGE_NUM; i++) // 页写入的WORD数 { status = Flash_WriteWord((addr + i * sizeof(uint32_t)), *(buf + i)); // 注意:地址增加应按uint32_t大小增加 if (status != SUCCESS) { return status; // 返回错误状态 } } return status; // 如果所有写入成功,返回SUCCESS } /** * @description: 擦除产品信息页 * @return {*} */ void Flash_ErasePage_ConfigInfo(void) { if (std_flash_page_erase(127) != SUCCESS) { LOG_D("error_page = %#x\r\n"); } } /** * @description: 写产品信息页(最后一页) * @param {uint8_t*} frame * @return {*} */ void Flash_Write_ConfigInfo(uint32_t *frame) { Flash_WritePage(FLASH_CONFIG_INFO_START_ADDR, frame); } /*获取当前页号*/ uint32_t Flash_Get_Current_Page(uint32_t addr) { return (addr - FLASH_START_ADDR) / FLASH_PAGE_BYTE_SIZE; } std_status_t Flash_ErasePage(uint32_t index) { BSP_Flash_UnLock(); RTT_LOG_D("ErasePage %d", index); std_status_t status = std_flash_erase(FLASH_ERASE_PAGE, index); BSP_Flash_Lock(); return status; } /** * @description: 擦除历史记录区的页 * @param {TeRecord} addr 需要擦除的页的起始地址 * @return {*} */ static ErrorStatus Flash_ErasePage_Records(TeRecord record, uint8_t page_offset) { ErrorStatus flag = SUCCESS; uint8_t erase_page = 0; if (page_offset <= hr_record_pages[record] - 1) { erase_page = page_offset; } else { erase_page = hr_record_pages[record] - 1; } if (Flash_ErasePage(Flash_Get_Current_Page(hr_start_addr[record] + erase_page * FLASH_PAGE_BYTE_SIZE)) != SUCCESS) { return ERROR; } for (uint16_t i = 0; i < FLASH_PAGE_BYTE_SIZE; i++) // 检查擦除是否完整 { if (*(uint32_t *)((hr_start_addr[record] + FLASH_PAGE_BYTE_SIZE * erase_page) + i) != FLASH_DEFAULT_VALUE) { flag = ERROR; } } return flag; } /** * @description: 擦除数据记录区 * @return {*} */ void BSP_Flash_EraseRecodrs(void) { /*擦除所有的历史记录区*/ for (TeRecord record = kRecordAlarm; record < kRecordSensorFailure; record++) { for (uint8_t i = 0; i < hr_record_pages[record]; i++) { Flash_ErasePage_Records(record, i); } } /*擦除初始信息区*/ Flash_ErasePage_ConfigInfo(); } /** * @description: 先读再擦,以确保原有数据不会被擦除 * @param {uint8_t} *page_buf * @return {*} */ void Flash_ErasePage_ReadConfigInfo(uint8_t *page_buf) { Flash_Read(FLASH_CONFIG_INFO_START_ADDR, page_buf, FLASH_PAGE_BYTE_SIZE); Flash_ErasePage_ConfigInfo(); } #if 0 /** * @description: 获取产品的出厂或到期时间 * @return {*} */ ErrorStatus Flash_GetProductTimeLimit(TuFlashProductTimeLimitFrame *pLimitTime, TeFlashProductTimeLimitId id) { ErrorStatus flag = ERROR; uint32_t addr = 0; if (id == kFactoryTimeId) { addr = FLASH_FACTORY_TIME_START_ADDR; } else if (id == kExpirationTimeId) { addr = FLASH_EXPIRATION_TIME_START_ADDR; } if (addr != 0) { Flash_Read(addr, pLimitTime->buf, sizeof(TuFlashProductTimeLimitFrame)); // LOG_D("pLimitTime->Struct.header = %#x\r\n", pLimitTime->Struct.header); if (pLimitTime->Struct.header == FLASH_PRODUCT_TIME_FRAME_HEADER) { flag = SUCCESS; LOG_D("Flash_GetProductTimeLimit(%d):%04d-%02d-%02d,%02d:%02d:%02d\r\n", id, pLimitTime->Struct.year, pLimitTime->Struct.month, pLimitTime->Struct.day, pLimitTime->Struct.hour, pLimitTime->Struct.minute, pLimitTime->Struct.second); } else { LOG_D("Flash_GetProductTimeLimit Error!"); } } return flag; } /** * @description: 设置产品的出厂或到期时间 * @param {uint16_t} year * @param {uint8_t} mon * @param {uint8_t} day * @param {uint8_t} hour * @param {uint8_t} min * @param {uint8_t} second * @param {TeFlashProductTimeLimitId} id * @return {*} */ void Flash_SetProductTimeLimit(uint16_t year, uint8_t mon, uint8_t day, uint8_t hour, uint8_t min, uint8_t second, TeFlashProductTimeLimitId id) { TuFlashProductTimeLimitFrame LimitTime; TuFlashProductTimeLimitFrame ReadLimitTime; uint8_t in_page_offset = 0; uint8_t page_buf[FLASH_PAGE_SIZE] = {0}; // 暂存最后一页的值 Flash_ErasePage_ReadConfigInfo(page_buf); LimitTime.Struct.header = FLASH_PRODUCT_TIME_FRAME_HEADER; LimitTime.Struct.year = year; LimitTime.Struct.month = mon; LimitTime.Struct.day = day; LimitTime.Struct.hour = hour; LimitTime.Struct.minute = min; LimitTime.Struct.second = second; if (id == kFactoryTimeId) { in_page_offset = (FLASH_FACTORY_TIME_START_ADDR - FLASH_CONFIG_INFO_START_ADDR); } else if (id == kExpirationTimeId) { in_page_offset = (FLASH_EXPIRATION_TIME_START_ADDR - FLASH_CONFIG_INFO_START_ADDR); } for (uint8_t i = 0; i < sizeof(TuFlashProductTimeLimitFrame); i++) { page_buf[in_page_offset + i] = LimitTime.buf[i]; } Flash_Write_ConfigInfo(&page_buf[0]); Flash_GetProductTimeLimit(&ReadLimitTime, id); } #endif /** * @description: 产品信息初始化, * @return {*} */ void BSP_Flash_Init(void) { if (*(uint32_t *)FLASH_CONFIG_INFO_START_ADDR != FLASH_FIRST_INIT_VALUE) { uint32_t info_page_buf[128] = {0}; BSP_Flash_EraseRecodrs(); // 擦除历史数据区 // TODO: 此处需要完善,比如写入初始化标志位,写入产品信息,写入出厂时间,写入到期时间, Flash_WritePage(FLASH_CONFIG_INFO_START_ADDR, info_page_buf); } } /** * @description: 获取索引起始地址 * @param {TeRecord} record * @return {*} */ TsRecordIndex Flash_GetIndex_Pos(TeRecord record) { uint32_t imdex_addr = RT_NULL; TsRecordIndex Index_Frame = {0}; for (uint8_t page = 1; page <= hr_record_pages[record]; page++) // 遍历对应项的历史数据扇区 { for (uint8_t i = 0; i < FLASH_HR_PAGE_RECORD_NUM; i++) // 获取扇区偏移量 { imdex_addr = (hr_start_addr[record] + FLASH_PAGE_BYTE_SIZE * (page - 1) + HR_RECORD_FRAME_LEN * i); if (*(uint32_t *)imdex_addr == FLASH_DEFAULT_VALUE) { /*获取最新的数据所在位置,如果值为0xFFFFFFFF,则表示该页有空位,是最新的页*/ if (page < hr_record_pages[record]) // 当前页不是最后一页 { if (*(uint32_t *)(imdex_addr + FLASH_PAGE_BYTE_SIZE) == FLASH_DEFAULT_VALUE) { /* 没进行循环写入*/ Index_Frame.hr_start_addr = hr_start_addr[record]; Index_Frame.hr_index_addr = imdex_addr; Index_Frame.hr_num = (imdex_addr - hr_start_addr[record]) / HR_RECORD_FRAME_LEN; Index_Frame.hr_page = page; return Index_Frame; } else // 已经开始循环了 { Index_Frame.hr_start_addr = (imdex_addr + FLASH_PAGE_BYTE_SIZE); Index_Frame.hr_index_addr = imdex_addr; Index_Frame.hr_num = hr_record_max_num[record]; Index_Frame.hr_page = page; return Index_Frame; } } else // 当前页是最后一页,那么首地址应该在第一页,其偏移量是最后一页的偏移量 { Index_Frame.hr_start_addr = (hr_start_addr[record] + HR_RECORD_FRAME_LEN * i); Index_Frame.hr_index_addr = imdex_addr; Index_Frame.hr_num = hr_record_max_num[record]; Index_Frame.hr_page = page; if (1 == hr_record_max_num[record])// 只有一条数据,则索引地址应该在第一页 { Index_Frame.hr_start_addr = hr_start_addr[record]; } return Index_Frame; } } } } RTT_LOG_E("Flash_GetIndex_Pos Error!"); return Index_Frame; } /** * @description: * @param {TuFlashHrRecordFrame} *pHrRecord * @param {TeRecord} record * @param {uint8_t} index * @return {*} */ ErrorStatus Flash_Read_Record(TuFlashHrRecordFrame *pHrRecord, TeRecord record, size_t index) { // LOG_D("/*********************Flash_Read_Record**********************/"); TsRecordIndex Index_Frame = Flash_GetIndex_Pos(record); // 已存储的数量 uint32_t index_addr = 0; if (index <= hr_record_max_num[record]) { if (index <= Index_Frame.hr_num) { { if (Index_Frame.hr_start_addr == hr_start_addr[record]) // 是默认的起始地址,则读取数据就是对应的偏移量 { index_addr = hr_start_addr[record] + (index - 1) * HR_RECORD_FRAME_LEN; } else { if ((Index_Frame.hr_start_addr + index * HR_RECORD_FRAME_LEN) <= hr_record_pages[record] * FLASH_PAGE_BYTE_SIZE + hr_start_addr[record]) // 没超过所在项目的页最大地址,正常读取,此时应该在缓存页 { RTT_LOG_D("read :hr_start_addr1 = %#x", Index_Frame.hr_start_addr); index_addr = Index_Frame.hr_start_addr + (index - 1) * HR_RECORD_FRAME_LEN; } else // 超过了所在项目的最大地址 { RTT_LOG_D("read :hr_start_addr2 = %#x", Index_Frame.hr_start_addr); index_addr = (Index_Frame.hr_start_addr + (index - 1) * HR_RECORD_FRAME_LEN - hr_record_pages[record] * FLASH_PAGE_BYTE_SIZE); } } } { pHrRecord->hr_data = (*(uint32_t *)index_addr); if (pHrRecord->time.check_sum == Sum_Check(pHrRecord)) { // RTT_LOG_D("Flash_Read_(%d)Record(%d):%04d-%02d-%02d,%02d:%02d", record, index, // BASE_YEAR + pHrRecord->time.year, pHrRecord->time.month, pHrRecord->time.day, // pHrRecord->time.hour, pHrRecord->time.minute); RTT_LOG_D("read :hr_start_addr = %#x, hr_index_addr: %#x, hr_num: %d, hr_page:%d, read_data : %#X\r\n", Index_Frame.hr_start_addr, index_addr, Index_Frame.hr_num, Index_Frame.hr_page, pHrRecord->hr_data); return SUCCESS; } else { RTT_LOG_E("Error Time: %d-%d-%d,%d:%d", pHrRecord->time.year, pHrRecord->time.month, pHrRecord->time.day, pHrRecord->time.hour, pHrRecord->time.minute); RTT_LOG_E("read :hr_start_addr = %#x, hr_index_addr: %#x, hr_num: %d, hr_page:%d, read_data : %#X", Index_Frame.hr_start_addr, index_addr, Index_Frame.hr_num, Index_Frame.hr_page, pHrRecord->hr_data); RTT_LOG_E("Flash_GetMaxNum((%d)_Records(%d) Sum_Check(%#X) != check_sum(%#X),index_addr :%#X\r\n", Index_Frame.hr_num, record, Sum_Check(pHrRecord), pHrRecord->time.check_sum, index_addr); pHrRecord->hr_data = FLASH_DEFAULT_VALUE; // 默认值 return ERROR; } } } else { RTT_LOG_E("index num (%d) > Actual storage num %d", index, Index_Frame.hr_num); return ERROR; } } else { RTT_LOG_E("index num (%d) > hr record max num %d", index, hr_record_max_num[record]); return ERROR; } } /** * @description: 向对应的历史数据存储区写入一次数据 * @param {TeRecord} record * @return {*} */ ErrorStatus Flash_Write_Record(TeRecord record) { uint8_t rtctime[3], rtcdate[3]; TuFlashHrRecordFrame RecoderFrame = {0}; TsRecordIndex Index_Frame; Index_Frame = Flash_GetIndex_Pos(record); __cycleof__("my algorithm") { BSP_Rtc_Get_Calendar(rtcdate, rtctime); } if (record == kRecordSensorFailure && Index_Frame.hr_num >= HR_FAULT_MAX_NUM) { RTT_LOG_E("Flash_Write_(%d)Record(%d) num >= recorded Max num(%d)", record, Index_Frame.hr_num, hr_record_max_num[record]); return ERROR; } RecoderFrame.time.year = (std_rtc_convert_bcd2bin(rtcdate[0]) - 24); RecoderFrame.time.month = std_rtc_convert_bcd2bin(rtcdate[1]); RecoderFrame.time.day = std_rtc_convert_bcd2bin(rtcdate[2]); RecoderFrame.time.hour = std_rtc_convert_bcd2bin(rtctime[0]); RecoderFrame.time.minute = std_rtc_convert_bcd2bin(rtctime[1]); RecoderFrame.time.check_sum = Sum_Check(&RecoderFrame); RTT_LOG_D("write:hr_start_addr = %#x, hr_index_addr: %#x, hr_num: %d, hr_page:%d, write_data : %#X", Index_Frame.hr_start_addr, Index_Frame.hr_index_addr, Index_Frame.hr_num, Index_Frame.hr_page, RecoderFrame.hr_data); RTT_LOG_D("Flash_Write_(%d)Record:%04d-%02d-%02d %02d:%02d, check_sum : %#X", record, RecoderFrame.time.year + BASE_YEAR, RecoderFrame.time.month, RecoderFrame.time.day, RecoderFrame.time.hour, RecoderFrame.time.minute, RecoderFrame.time.check_sum); { /*写入数据*/ if (Flash_WriteWord(Index_Frame.hr_index_addr, RecoderFrame.hr_data) != SUCCESS) { RTT_LOG_E("hr_write_data_error"); RTT_LOG_E("hr_start_addr = %#x, hr_index_addr: %#x, hr_num: %d, hr_page:%d, write_data : %#X", Index_Frame.hr_start_addr, Index_Frame.hr_index_addr, Index_Frame.hr_num, Index_Frame.hr_page, RecoderFrame.hr_data); return ERROR; } } { /*如果把当前页写完了,要把下一页擦了,这样索引才不会出错,否则会导致索引出错*/ if (Index_Frame.hr_index_addr == hr_start_addr[record] + (Index_Frame.hr_page) * FLASH_PAGE_BYTE_SIZE - HR_RECORD_FRAME_LEN) { RTT_LOG_I("Index_Frame.hr_index_addr = hr_start_addr[record] + i * FLASH_PAGE_BYTE_SIZE - HR_RECORD_FRAME_LEN = %#X", Index_Frame.hr_index_addr); if (hr_record_pages[record] - Index_Frame.hr_page > 0) // 当前页不是最后一页,还不需要返回擦 { if (Flash_ErasePage(Index_Frame.hr_page + hr_record_start_page_num[record]) != SUCCESS) { RTT_LOG_I("Erase Page %d", Index_Frame.hr_page + hr_record_start_page_num[record]); return ERROR; } } else // 当前页是最后一页 此时需要将第一页擦除掉 { if (Flash_ErasePage(hr_record_start_page_num[record]) != SUCCESS) // 擦除第一页 { RTT_LOG_I("Erase Page %d", hr_record_start_page_num[record]); return ERROR; } } } } { if (Index_Frame.hr_num < hr_record_max_num[record]) { Flash_Read_Record(&RecoderFrame, record, Index_Frame.hr_num + 1); // 读取刚才写入数据 } else // 超了最大数后,每次读出来的数据都是最大数 { Flash_Read_Record(&RecoderFrame, record, Index_Frame.hr_num); // 读取刚才写入数据 } } return SUCCESS; } static void TEST_Flash_Write_Record(int argc, char **argv) { if (argc == 3) { TeRecord record = (TeRecord)atoi(argv[1]); int num = atoi(argv[2]); for (uint8_t i = 0; i < num; i++) { Flash_Write_Record(record); rt_thread_mdelay(100); } } else { LOG_E("TEST_Flash_Write_Record --use _cmd_ [record(0~6)] [num]"); } } MSH_CMD_EXPORT(TEST_Flash_Write_Record, "TEST_Flash_Write_Record"); // static void TEST_Flash_Read_Record(int argc, char **argv) // { // TuFlashHrRecordFrame RecoderFrame = {0}; // if (argc == 3) // { // TeRecord record = (TeRecord)atoi(argv[1]); // int num = atoi(argv[2]); // Flash_Read_Record(&RecoderFrame, record, num); // } // else // { // LOG_E("TEST_Flash_Read_Record --use _cmd_ [record(0~6)] [num]"); // } // } // MSH_CMD_EXPORT(TEST_Flash_Read_Record, "TEST_Flash_Read_Record"); /*获取当前项目记录数*/ uint8_t Flash_GetNum_Records(TeRecord record) { TsRecordIndex Index_Frame = Flash_GetIndex_Pos(record); return Index_Frame.hr_num; } /** * @description: 获取总记录数 * @param {TsTotalRecords} *pTotalRecords * @return {*} * @note: */ ErrorStatus Flash_GetTotalRecord(TsTotalRecords *pTotalRecords) { ErrorStatus flag = ERROR; pTotalRecords->alarm = Flash_GetNum_Records(kRecordAlarm); pTotalRecords->alarm_rcy = Flash_GetNum_Records(kRecordAlarmRcy); pTotalRecords->fault = Flash_GetNum_Records(kRecordFault); pTotalRecords->fault_rcy = Flash_GetNum_Records(kRecordFaultRcy); pTotalRecords->power_failure = Flash_GetNum_Records(kRecordPowerFailure); pTotalRecords->power_on = Flash_GetNum_Records(kRecordPowerOn); pTotalRecords->sensor_failure = Flash_GetNum_Records(kRecordSensorFailure); flag = SUCCESS; return flag; } /** * @brief 获取记录时间 * @param record_type * @param index * @param pRecordsTime * @return ErrorStatus */ ErrorStatus Flash_GetRecord(TeFrameC2 record_type, uint8_t index, TsRecordsTime *pRecordsTime) { ErrorStatus flag = ERROR; uint16_t years = 0; if (kNumOfRecords < record_type && record_type < kGetCurrentTime) { TuFlashHrRecordFrame pHrReadRecord; Flash_Read_Record(&pHrReadRecord, (TeRecord)(record_type - 1), index); years = BASE_YEAR + pHrReadRecord.time.year; LOG_D("(%d)Flash_GetRecord[%d]:%04d-%02d-%02d,%02d:%02d", record_type - 1, index, years, pHrReadRecord.time.month, pHrReadRecord.time.day, pHrReadRecord.time.hour, pHrReadRecord.time.minute); pRecordsTime->year_h = years / 256; pRecordsTime->year_l = years % 256; pRecordsTime->month = pHrReadRecord.time.month; pRecordsTime->day = pHrReadRecord.time.day; pRecordsTime->hour = pHrReadRecord.time.hour; pRecordsTime->minute = pHrReadRecord.time.minute; flag = SUCCESS; } return flag; } #endif