/* * 模块名称:菜单系统 * 模块功能:通过LCD显示参数,按键导航/调整,AT24C02存储 * 适用平台:STM32F103C8T6 * 作者:王建锋 * 创建日期:2026-07-14 * 修改记录: * 2026-07-14 王建锋 创建初始版本 */ #include "menu.h" #include "button_driver.h" #include "at24c02.h" #include "lcd.h" #include "tim.h" #include #define COLOR_GRAY 0x8410 /* RGB565 灰 */ /* ======================== EEPROM 存储布局 ======================== */ #define EEPROM_DATA_ADDR 0x10 /* 参数存储起始地址 */ #define EEPROM_MAGIC 0xA55A /* 有效数据标识 */ #define EEPROM_MAGIC_ADDR 0x0E /* Magic 存储地址(2字节) */ /* ======================== 参数定义 ======================== */ typedef struct { const char *name; /* 显示名称 */ const char *unit; /* 单位 */ int32_t default_val; /* 默认值 */ int32_t min; /* 最小值 */ int32_t max; /* 最大值 */ int32_t step; /* 步进 */ } ParamDef_t; static const ParamDef_t s_param_def[PARAM_COUNT] = { {"PWM1", "", 500, 0, 999, 50 }, {"PWM2", "", 500, 0, 999, 50 }, {"PWM3", "", 500, 0, 999, 50 }, {"PWM4", "", 500, 0, 999, 50 }, }; /* ======================== 运行时状态 ======================== */ static int32_t s_param_val[PARAM_COUNT]; /* 当前参数值 */ static uint8_t s_sel = 0; /* 选中的参数索引 */ static uint8_t s_editing = 0; /* 0=导航, 1=编辑 */ static uint8_t s_redraw = 1; /* 需要重绘标志 */ /* ======================== 静态函数声明 ======================== */ static void draw_menu(void); static void load_params(void); static int save_params(void); static void apply_param(uint8_t idx); /* ======================== 公开函数 ======================== */ /* * 函数功能:菜单系统初始化 * 入口参数:无 * 返回值:无 * 函数说明:从EEPROM加载参数,清屏,绘制初始界面 */ void menu_init(void) { lcd_draw_string(0, 0, "Menu Loading...", COLOR_WHITE, COLOR_BLACK); load_params(); s_sel = 0; s_editing = 0; s_redraw = 1; apply_param(PARAM_PWM1); apply_param(PARAM_PWM2); apply_param(PARAM_PWM3); apply_param(PARAM_PWM4); st7735s_fill_screen(COLOR_BLACK); draw_menu(); } /* * 函数功能:菜单系统循环处理 * 入口参数:无 * 返回值:无 * 函数说明:轮询按键事件,更新参数/界面。需在主循环中频繁调用 */ void menu_process(void) { ButtonEvent evt; uint8_t i; int32_t *p_val; /* 依次检查5个按键的事件 */ for (i = 0; i < 5; i++) { evt = button_driver_get_event(i); if (evt == BTN_NONE_PRESS) { continue; } if (evt == BTN_SINGLE_CLICK) { switch (i) { case 0: /* KEY1: 上移 */ if (!s_editing) { if (s_sel > 0) { s_sel--; } else { s_sel = PARAM_COUNT - 1; } s_redraw = 1; } break; case 1: /* KEY2: 减小 */ p_val = &s_param_val[s_sel]; *p_val -= s_param_def[s_sel].step; if (*p_val < s_param_def[s_sel].min) { *p_val = s_param_def[s_sel].min; } s_editing = 1; s_redraw = 1; apply_param(s_sel); break; case 2: /* KEY3: 下移 */ if (!s_editing) { s_sel++; if (s_sel >= PARAM_COUNT) { s_sel = 0; } s_redraw = 1; } break; case 3: /* KEY4: 增大 */ p_val = &s_param_val[s_sel]; *p_val += s_param_def[s_sel].step; if (*p_val > s_param_def[s_sel].max) { *p_val = s_param_def[s_sel].max; } s_editing = 1; s_redraw = 1; apply_param(s_sel); break; case 4: /* KEY5: 确认保存 */ if (save_params() == AT24C02_OK) { lcd_draw_string(0, 80, "SAVED! ", COLOR_GREEN, COLOR_BLACK); printf("Menu: params saved\r\n"); } else { lcd_draw_string(0, 80, "SAVE FAIL! ", COLOR_RED, COLOR_BLACK); printf("Menu: params save FAIL\r\n"); } s_editing = 0; s_redraw = 1; break; default: break; } } /* 只处理一个按键事件每帧 */ break; } if (s_redraw) { draw_menu(); s_redraw = 0; } } /* * 函数功能:获取指定参数的当前值 * 入口参数:id - 参数ID * 返回值:参数值 */ int32_t menu_get_param(ParamId_t id) { if (id >= PARAM_COUNT) { return 0; } return s_param_val[id]; } /* * 函数功能:强制保存参数到EEPROM * 入口参数:无 * 返回值:0=成功,负值=失败 */ int menu_save_params(void) { return save_params(); } /* ======================== 内部函数 ======================== */ /* * 函数功能:从EEPROM加载参数 * 入口参数:无 * 返回值:无 * 函数说明:读取EEPROM中存储的参数值,若Magic不对则使用默认值 */ static void load_params(void) { uint8_t magic_buf[2]; uint8_t data_buf[PARAM_COUNT * 4]; int ret; uint8_t i; /* 先读取Magic */ ret = at24c02_read_buf(EEPROM_MAGIC_ADDR, magic_buf, 2); if ((ret != AT24C02_OK) || (magic_buf[0] != ((EEPROM_MAGIC >> 8) & 0xFF)) || (magic_buf[1] != (EEPROM_MAGIC & 0xFF))) { /* Magic不匹配:使用默认值 */ for (i = 0; i < PARAM_COUNT; i++) { s_param_val[i] = s_param_def[i].default_val; } printf("Menu: using defaults (no saved data)\r\n"); return; } /* 读取参数数据 */ ret = at24c02_read_buf(EEPROM_DATA_ADDR, data_buf, PARAM_COUNT * 4); if (ret != AT24C02_OK) { for (i = 0; i < PARAM_COUNT; i++) { s_param_val[i] = s_param_def[i].default_val; } printf("Menu: EEPROM read fail, using defaults\r\n"); return; } /* 解析数据(小端:低字节在前) */ for (i = 0; i < PARAM_COUNT; i++) { s_param_val[i] = (int32_t)( (uint32_t)data_buf[i * 4 + 0] | ((uint32_t)data_buf[i * 4 + 1] << 8) | ((uint32_t)data_buf[i * 4 + 2] << 16) | ((uint32_t)data_buf[i * 4 + 3] << 24)); /* 钳位到有效范围 */ if (s_param_val[i] < s_param_def[i].min) { s_param_val[i] = s_param_def[i].min; } if (s_param_val[i] > s_param_def[i].max) { s_param_val[i] = s_param_def[i].max; } } printf("Menu: params loaded from EEPROM\r\n"); } /* * 函数功能:保存参数到EEPROM * 入口参数:无 * 返回值:0=成功,负值=失败 * 函数说明:先写Magic标识,再写参数数据 */ static int save_params(void) { uint8_t magic_buf[2]; uint8_t data_buf[PARAM_COUNT * 4]; uint8_t i; int ret; /* 打包Magic */ magic_buf[0] = (EEPROM_MAGIC >> 8) & 0xFF; magic_buf[1] = EEPROM_MAGIC & 0xFF; /* 打包参数(小端) */ for (i = 0; i < PARAM_COUNT; i++) { data_buf[i * 4 + 0] = (uint8_t)(s_param_val[i] & 0xFF); data_buf[i * 4 + 1] = (uint8_t)((s_param_val[i] >> 8) & 0xFF); data_buf[i * 4 + 2] = (uint8_t)((s_param_val[i] >> 16) & 0xFF); data_buf[i * 4 + 3] = (uint8_t)((s_param_val[i] >> 24) & 0xFF); } /* 写入EEPROM */ ret = at24c02_write_buf(EEPROM_MAGIC_ADDR, magic_buf, 2); if (ret != AT24C02_OK) { return ret; } ret = at24c02_write_buf(EEPROM_DATA_ADDR, data_buf, PARAM_COUNT * 4); if (ret != AT24C02_OK) { return ret; } return AT24C02_OK; } /* * 函数功能:将指定参数立即应用到对应PWM通道 * 入口参数:idx - 参数索引 (0=PWM1, 1=PWM2, 2=PWM3, 3=PWM4) * 返回值:无 */ static void apply_param(uint8_t idx) { uint32_t duty = (uint32_t)s_param_val[idx]; static const uint16_t ch_map[4] = { TIM_CHANNEL_1, TIM_CHANNEL_2, TIM_CHANNEL_3, TIM_CHANNEL_4 }; if (idx < PARAM_COUNT) { __HAL_TIM_SET_COMPARE(&htim3, ch_map[idx], duty); printf("PWM%d duty=%lu\r\n", idx + 1, (unsigned long)duty); } } /* * 函数功能:绘制菜单界面 * 入口参数:无 * 返回值:无 * 函数说明:清空参数区域,重新绘制所有参数行 */ static void draw_menu(void) { char buf[24]; uint8_t i; uint16_t color; /* 绘制标题行 */ lcd_draw_string(0, 0, "-- PARAM MENU --", COLOR_CYAN, COLOR_BLACK); /* 绘制各参数(每行16像素,从y=16开始) */ for (i = 0; i < PARAM_COUNT; i++) { if (i == s_sel) { if (s_editing) { /* 编辑态:高亮 + 左右尖括号 */ snprintf(buf, sizeof(buf), ">%s:%ld %s< ", s_param_def[i].name, (long)s_param_val[i], s_param_def[i].unit); color = COLOR_YELLOW; } else { /* 选中态:箭头 + 高亮 */ snprintf(buf, sizeof(buf), ">%s:%ld %s ", s_param_def[i].name, (long)s_param_val[i], s_param_def[i].unit); color = COLOR_WHITE; } } else { /* 未选中:普通显示 */ snprintf(buf, sizeof(buf), " %s:%ld %s ", s_param_def[i].name, (long)s_param_val[i], s_param_def[i].unit); color = COLOR_GRAY; } /* 填充整行背景以覆盖之前内容 */ lcd_draw_string(0, 16 + i * 16, buf, color, COLOR_BLACK); } /* 底部提示 */ if (s_editing) { lcd_draw_string_6x8(0, 140, "KEY5:SAVE ", COLOR_GREEN, COLOR_BLACK); } else { lcd_draw_string_6x8(0, 140, "K1/K3:Sel K2/K4:Adj", COLOR_GRAY, COLOR_BLACK); } }