三、基于LVGL和STM32上外设可做一些交互功能展示
·
一、工程基于:
https://blog.csdn.net/weixin_44386927/article/details/164758373?spm=1001.2014.3001.5502
之前的篇文章。
二、介绍
1.通过在屏幕上实现计算机功能
核心部分:
ui界面:
/*********************
* DEFINES
*********************/
#define UI_BG_HEX 0x1E1E2E /* 屏幕底色 */
#define UI_CARD_HEX 0x2A2A3C /* 显示卡片底色 */
#define UI_DIGIT_HEX 0x2E3046 /* 数字键 */
#define UI_FUNC_HEX 0x3C3F58 /* 功能键 C / +/- / % */
#define UI_OP_HEX 0xF5A623 /* 运算符键 */
#define UI_EQ_HEX 0x4C6FFF /* 等号键 */
#define UI_TEXT_HEX 0xFFFFFF
#define UI_MUTED_HEX 0x8A90A6
#define UI_PAD 6 /* 屏幕四周留白 */
#define UI_GAP 5 /* 卡片与按键之间的缝 */
#define KEY_CLEAR 'c'
#define KEY_SIGN 's'
#define KEY_PERCENT 'p'
/**********************
* TYPEDEFS
**********************/
typedef struct
{
double acc; /* 左操作数(累加结果) */
char op; /* 待执行的运算符,0 表示无 */
char entry[20]; /* 当前输入 / 当前显示的数字文本 */
bool fresh; /* true:下一个数字按键要覆盖 entry */
bool error; /* true:显示 Error,需按 C 复位 */
} calc_ctx_t;
/**********************
* STATIC VARIABLES
**********************/
static calc_ctx_t calc;
static lv_obj_t * expr_label = NULL;
static lv_obj_t * main_label = NULL;
/**********************
* STATIC FUNCTIONS
**********************/
/* 把 double 转成便于阅读的字符串:最多 10 位有效数字 */
static void calc_format(double v, char * buf, size_t n)
{
if (isnan(v) || isinf(v))
{
snprintf(buf, n, "Error");
return;
}
snprintf(buf, n, "%.10g", v);
}
/* 重置计算器状态 */
static void calc_reset(void)
{
calc.acc = 0.0;
calc.op = 0;
calc.fresh = true;
calc.error = false;
strcpy(calc.entry, "0");
}
/* 将数值写入 entry 文本 */
static void calc_store(double v)
{
calc_format(v, calc.entry, sizeof(calc.entry));
if (strcmp(calc.entry, "Error") == 0)
{
calc.error = true;
}
}
/* 把状态刷新到界面 */
static void calc_refresh(void)
{
char expr[40];
/* 校准界面会先清空屏幕,此时标签已失效 */
if ((expr_label == NULL) || (main_label == NULL))
{
return;
}
lv_label_set_text(main_label, calc.entry);
if (calc.error || calc.op == 0)
{
expr[0] = '\0';
}
else
{
char acc_buf[24];
calc_format(calc.acc, acc_buf, sizeof(acc_buf));
snprintf(expr, sizeof(expr), "%s %c", acc_buf, calc.op);
}
lv_label_set_text(expr_label, expr);
}
/* 二元运算 */
static double calc_apply(char op, double a, double b)
{
switch (op)
{
case '+':
return a + b;
case '-':
return a - b;
case '*':
return a * b;
case '/':
return (b == 0.0) ? NAN : a / b;
default:
return b;
}
}
/* 输入数字 0-9 */
static void calc_input_digit(char d)
{
size_t len;
if (calc.error)
{
calc_reset();
}
if (calc.fresh)
{
calc.entry[0] = d;
calc.entry[1] = '\0';
calc.fresh = false;
}
else
{
len = strlen(calc.entry);
if (len >= sizeof(calc.entry) - 1U)
{
return;
}
if (strchr(calc.entry, 'e') != NULL)
{
return; /* 科学计数法的结果不再追加 */
}
if ((len == 1U) && (calc.entry[0] == '0'))
{
calc.entry[0] = d; /* 去掉多余前导 0 */
}
else
{
calc.entry[len] = d;
calc.entry[len + 1] = '\0';
}
}
calc_refresh();
}
/* 输入小数点 */
static void calc_input_dot(void)
{
size_t len;
if (calc.error)
{
calc_reset();
}
if (calc.fresh)
{
strcpy(calc.entry, "0.");
calc.fresh = false;
calc_refresh();
return;
}
if ((strchr(calc.entry, '.') == NULL) && (strchr(calc.entry, 'e') == NULL))
{
len = strlen(calc.entry);
if (len < sizeof(calc.entry) - 1U)
{
calc.entry[len] = '.';
calc.entry[len + 1] = '\0';
calc_refresh();
}
}
}
/* 输入运算符 + - * / */
static void calc_input_op(char op)
{
double cur;
if (calc.error)
{
return;
}
cur = atof(calc.entry);
if ((calc.op != 0) && (!calc.fresh))
{
double result = calc_apply(calc.op, calc.acc, cur);
calc.acc = result;
calc_store(result);
if (calc.error)
{
calc_refresh();
return;
}
}
else
{
calc.acc = cur;
}
calc.op = op;
calc.fresh = true;
calc_refresh();
}
/* 按下等号 */
static void calc_equal(void)
{
double cur;
if (calc.error)
{
return;
}
cur = atof(calc.entry);
if (calc.op != 0)
{
double result = calc_apply(calc.op, calc.acc, cur);
calc.op = 0;
calc.acc = 0.0;
calc_store(result);
}
else
{
calc.acc = cur;
}
calc.fresh = true;
calc_refresh();
}
/* 正负号切换 */
static void calc_sign(void)
{
size_t len;
if (calc.error || (strcmp(calc.entry, "0") == 0))
{
return;
}
len = strlen(calc.entry);
if (calc.entry[0] == '-')
{
memmove(calc.entry, calc.entry + 1, len);
}
else if (len < sizeof(calc.entry) - 1U)
{
memmove(calc.entry + 1, calc.entry, len + 1U);
calc.entry[0] = '-';
}
else
{
return;
}
calc_refresh();
}
/* 百分比:当前值 / 100 */
static void calc_percent(void)
{
if (calc.error)
{
return;
}
calc_store(atof(calc.entry) / 100.0);
calc_refresh();
}
/* 所有按键共用的点击回调,user_data 即键值字符 */
static void key_event_cb(lv_event_t * e)
{
char key = (char)(intptr_t)lv_event_get_user_data(e);
if ((key >= '0') && (key <= '9'))
{
calc_input_digit(key);
return;
}
switch (key)
{
case '.':
calc_input_dot();
break;
case '+':
case '-':
case '*':
case '/':
calc_input_op(key);
break;
case '=':
calc_equal();
break;
case KEY_CLEAR:
calc_reset();
calc_refresh();
break;
case KEY_SIGN:
calc_sign();
break;
case KEY_PERCENT:
calc_percent();
break;
default:
break;
}
}
/**********************
* 触摸四点校准
**********************/
#define CAL_POINT_COUNT 4U /* 靶标个数 */
#define CAL_MARGIN 30 /* 靶标距屏幕边缘距离 */
#define CAL_SAMPLES 6U /* 单点采样次数 */
#define CAL_DEBOUNCE 3U /* 去抖次数(20ms/次) */
static lv_obj_t * cal_ring = NULL;
static lv_obj_t * cal_bar_h = NULL;
static lv_obj_t * cal_bar_v = NULL;
static lv_obj_t * cal_tip = NULL;
static lv_timer_t * cal_timer = NULL;
static uint16_t cal_raw[CAL_POINT_COUNT][2];
static int32_t cal_px[CAL_POINT_COUNT];
static int32_t cal_py[CAL_POINT_COUNT];
static uint8_t cal_index; /* 当前靶标序号 */
static uint8_t cal_phase; /* 0 等按下 / 1 采样 / 2 等抬起 */
static uint8_t cal_debounce;
static uint8_t cal_samples;
static uint32_t cal_sum_x;
static uint32_t cal_sum_y;
/* 把标定结果限制在 12 位 ADC 的有效范围内 */
static uint16_t cal_clamp(double value)
{
if (value < 0.0)
{
return 0U;
}
if (value > 4095.0)
{
return 4095U;
}
return (uint16_t)(value + 0.5);
}
/* 把靶标移到第 cal_index 个位置并刷新提示文字 */
static void cal_move_target(void)
{
char text[40];
int32_t x = cal_px[cal_index];
int32_t y = cal_py[cal_index];
lv_obj_set_pos(cal_ring, x - 28, y - 28);
lv_obj_set_pos(cal_bar_h, x - 20, y - 2);
lv_obj_set_pos(cal_bar_v, x - 2, y - 20);
snprintf(text, sizeof(text), "TAP THE CROSS %u/%u",
(unsigned)(cal_index + 1U), (unsigned)CAL_POINT_COUNT);
lv_label_set_text(cal_tip, text);
}
/* 四点采集完成:解算标定参数、应用并返回计算器界面 */
static void cal_finish(void)
{
XPT2046_Calibration cal;
lv_display_t * disp = lv_display_get_default();
int32_t scr_w = lv_display_get_horizontal_resolution(disp);
int32_t scr_h = lv_display_get_vertical_resolution(disp);
int32_t xl = CAL_MARGIN;
int32_t xr = scr_w - 1 - CAL_MARGIN;
int32_t yt = CAL_MARGIN;
int32_t yb = scr_h - 1 - CAL_MARGIN;
double r[CAL_POINT_COUNT][2];
double h_left;
double h_right;
double v_top;
double v_bottom;
double slope;
double raw_lo;
double raw_hi;
uint8_t swap;
uint8_t i;
for (i = 0U; i < CAL_POINT_COUNT; i++)
{
r[i][0] = (double)cal_raw[i][0];
r[i][1] = (double)cal_raw[i][1];
}
/* 水平方向(左上->右上)哪个原始通道变化更大,它就是横轴 */
{
double dx = fabs(r[0][0] - r[1][0]) + fabs(r[2][0] - r[3][0]);
double dy = fabs(r[0][1] - r[1][1]) + fabs(r[2][1] - r[3][1]);
swap = (dx >= dy) ? 0U : 1U;
}
if (swap == 0U)
{
h_left = (r[0][0] + r[2][0]) / 2.0;
h_right = (r[1][0] + r[3][0]) / 2.0;
v_top = (r[0][1] + r[1][1]) / 2.0;
v_bottom = (r[2][1] + r[3][1]) / 2.0;
}
else
{
h_left = (r[0][1] + r[2][1]) / 2.0;
h_right = (r[1][1] + r[3][1]) / 2.0;
v_top = (r[0][0] + r[1][0]) / 2.0;
v_bottom = (r[2][0] + r[3][0]) / 2.0;
}
/* 两点几乎重合说明采样无效,重新采一轮 */
if ((fabs(h_right - h_left) < 80.0) || (fabs(v_bottom - v_top) < 80.0))
{
cal_index = 0U;
cal_phase = 0U;
cal_debounce = 0U;
cal_move_target();
lv_label_set_text(cal_tip, "BAD DATA, TAP AGAIN");
return;
}
/* 水平轴:由两个靶标点线性外推到屏幕左右边缘 */
slope = (h_right - h_left) / (double)(xr - xl);
raw_lo = h_left - (double)xl * slope;
raw_hi = h_left + (double)(scr_w - 1 - xl) * slope;
cal.x_min = cal_clamp(raw_lo);
cal.x_max = cal_clamp(raw_hi);
/* 竖直轴 */
slope = (v_bottom - v_top) / (double)(yb - yt);
raw_lo = v_top - (double)yt * slope;
raw_hi = v_top + (double)(scr_h - 1 - yt) * slope;
cal.y_min = cal_clamp(raw_lo);
cal.y_max = cal_clamp(raw_hi);
cal.swap_xy = swap;
cal.invert_x = 0U;
cal.invert_y = 0U;
XPT2046_SetCalibration(&cal);
printf("\r\n==== XPT2046 calibration ====\r\n");
printf("#define XPT2046_RAW_X_MIN %uU\r\n", (unsigned)cal.x_min);
printf("#define XPT2046_RAW_X_MAX %uU\r\n", (unsigned)cal.x_max);
printf("#define XPT2046_RAW_Y_MIN %uU\r\n", (unsigned)cal.y_min);
printf("#define XPT2046_RAW_Y_MAX %uU\r\n", (unsigned)cal.y_max);
printf("#define XPT2046_SWAP_XY %uU\r\n", (unsigned)cal.swap_xy);
printf("#define XPT2046_INVERT_X %uU\r\n", (unsigned)cal.invert_x);
printf("#define XPT2046_INVERT_Y %uU\r\n", (unsigned)cal.invert_y);
printf("=============================\r\n");
if (cal_timer != NULL)
{
lv_timer_delete(cal_timer);
cal_timer = NULL;
}
cal_ring = NULL;
cal_bar_h = NULL;
cal_bar_v = NULL;
cal_tip = NULL;
ui_init();
}
/* 校准采样状态机,20ms 一次 */
static void cal_timer_cb(lv_timer_t * timer)
{
uint16_t rx = 0U;
uint16_t ry = 0U;
uint8_t pressed;
(void)timer;
pressed = XPT2046_ReadRawPoint(&rx, &ry);
switch (cal_phase)
{
case 3U: /* 刚进入:等长按的手指先离开 */
if (pressed == 0U)
{
cal_phase = 0U;
cal_debounce = 0U;
}
break;
case 0U: /* 等待一次新的按下 */
if (pressed != 0U)
{
if (cal_debounce < CAL_DEBOUNCE)
{
cal_debounce++;
}
if (cal_debounce >= CAL_DEBOUNCE)
{
cal_phase = 1U;
cal_sum_x = rx;
cal_sum_y = ry;
cal_samples = 1U;
}
}
else
{
cal_debounce = 0U;
}
break;
case 1U: /* 累积采样 */
if ((pressed != 0U) && (cal_samples < CAL_SAMPLES))
{
cal_sum_x += rx;
cal_sum_y += ry;
cal_samples++;
if (cal_samples < CAL_SAMPLES)
{
break;
}
}
/* 采样够了或者手指提前抬起,都记下这一点的平均值 */
cal_raw[cal_index][0] = (uint16_t)(cal_sum_x / cal_samples);
cal_raw[cal_index][1] = (uint16_t)(cal_sum_y / cal_samples);
cal_index++;
cal_phase = 2U;
break;
case 2U: /* 等手指抬起,避免一次点击采多点 */
if (pressed == 0U)
{
cal_phase = 0U;
cal_debounce = 0U;
if (cal_index >= CAL_POINT_COUNT)
{
cal_finish();
}
else
{
cal_move_target();
}
}
break;
default:
break;
}
}
/* 进入四点校准界面 */
static void touch_calibration_start(void)
{
lv_display_t * disp = lv_display_get_default();
lv_obj_t * scr = lv_screen_active();
int32_t scr_w = lv_display_get_horizontal_resolution(disp);
int32_t scr_h = lv_display_get_vertical_resolution(disp);
uint8_t i;
lv_obj_clean(scr);
lv_obj_set_style_bg_color(scr, lv_color_hex(0x101828), 0);
lv_obj_set_style_bg_opa(scr, LV_OPA_COVER, 0);
lv_obj_remove_flag(scr, LV_OBJ_FLAG_SCROLLABLE);
/* 计算器的标签随 clean 一起被删掉了,置空避免后续误用 */
expr_label = NULL;
main_label = NULL;
cal_px[0] = CAL_MARGIN;
cal_py[0] = CAL_MARGIN;
cal_px[1] = scr_w - 1 - CAL_MARGIN;
cal_py[1] = CAL_MARGIN;
cal_px[2] = CAL_MARGIN;
cal_py[2] = scr_h - 1 - CAL_MARGIN;
cal_px[3] = scr_w - 1 - CAL_MARGIN;
cal_py[3] = scr_h - 1 - CAL_MARGIN;
for (i = 0U; i < CAL_POINT_COUNT; i++)
{
cal_raw[i][0] = 0U;
cal_raw[i][1] = 0U;
}
/* 靶标:一个圆环加十字线 */
cal_ring = lv_obj_create(scr);
lv_obj_remove_style_all(cal_ring);
lv_obj_set_size(cal_ring, 56, 56);
lv_obj_set_style_radius(cal_ring, LV_RADIUS_CIRCLE, 0);
lv_obj_set_style_border_width(cal_ring, 2, 0);
lv_obj_set_style_border_color(cal_ring, lv_color_hex(0x4C6FFF), 0);
lv_obj_remove_flag(cal_ring, LV_OBJ_FLAG_SCROLLABLE);
lv_obj_remove_flag(cal_ring, LV_OBJ_FLAG_CLICKABLE);
cal_bar_h = lv_obj_create(scr);
lv_obj_remove_style_all(cal_bar_h);
lv_obj_set_size(cal_bar_h, 40, 4);
lv_obj_set_style_bg_color(cal_bar_h, lv_color_white(), 0);
lv_obj_set_style_bg_opa(cal_bar_h, LV_OPA_COVER, 0);
lv_obj_remove_flag(cal_bar_h, LV_OBJ_FLAG_SCROLLABLE);
lv_obj_remove_flag(cal_bar_h, LV_OBJ_FLAG_CLICKABLE);
cal_bar_v = lv_obj_create(scr);
lv_obj_remove_style_all(cal_bar_v);
lv_obj_set_size(cal_bar_v, 4, 40);
lv_obj_set_style_bg_color(cal_bar_v, lv_color_white(), 0);
lv_obj_set_style_bg_opa(cal_bar_v, LV_OPA_COVER, 0);
lv_obj_remove_flag(cal_bar_v, LV_OBJ_FLAG_SCROLLABLE);
lv_obj_remove_flag(cal_bar_v, LV_OBJ_FLAG_CLICKABLE);
cal_tip = lv_label_create(scr);
lv_obj_set_style_text_font(cal_tip, &lv_font_montserrat_14, 0);
lv_obj_set_style_text_color(cal_tip, lv_color_white(), 0);
lv_obj_align(cal_tip, LV_ALIGN_CENTER, 0, 0);
lv_obj_set_style_text_align(cal_tip, LV_TEXT_ALIGN_CENTER, 0);
cal_index = 0U;
cal_phase = 3U; /* 先等长按的手指抬起 */
cal_debounce = 0U;
cal_samples = 0U;
cal_move_target();
if (cal_timer != NULL)
{
lv_timer_delete(cal_timer);
}
cal_timer = lv_timer_create(cal_timer_cb, 20U, NULL);
}
/* 切换界面会删掉正在派发事件的按钮,交给 async 在本轮事件结束后执行 */
static void cal_start_async(void * unused)
{
(void)unused;
touch_calibration_start();
}
/* 长按 C 键进入校准 */
static void key_long_cb(lv_event_t * e)
{
char key = (char)(intptr_t)lv_event_get_user_data(e);
if (key == KEY_CLEAR)
{
(void)lv_async_call(cal_start_async, NULL);
}
}
/* 长按顶部显示区也能进入校准(万一按键位置偏得厉害按不中 C) */
static void card_long_cb(lv_event_t * e)
{
(void)e;
(void)lv_async_call(cal_start_async, NULL);
}
/* 创建一个圆角触控按键,key 为键值字符 */
static lv_obj_t * key_create(lv_obj_t * parent, const char * txt,
uint32_t bg_hex, const lv_font_t * font,
char key)
{
lv_obj_t * btn;
lv_obj_t * lbl;
lv_color_t bg = lv_color_hex(bg_hex);
btn = lv_button_create(parent);
lv_obj_remove_style_all(btn);
lv_obj_set_style_bg_color(btn, bg, 0);
lv_obj_set_style_bg_opa(btn, LV_OPA_COVER, 0);
lv_obj_set_style_radius(btn, 8, 0);
lv_obj_set_style_bg_color(btn, lv_color_lighten(bg, 45), LV_STATE_PRESSED);
lv_obj_remove_flag(btn, LV_OBJ_FLAG_SCROLLABLE);
lv_obj_add_event_cb(btn, key_event_cb, LV_EVENT_CLICKED, (void *)(intptr_t)key);
lv_obj_add_event_cb(btn, key_long_cb, LV_EVENT_LONG_PRESSED, (void *)(intptr_t)key);
lbl = lv_label_create(btn);
lv_label_set_text(lbl, txt);
lv_obj_set_style_text_font(lbl, font, 0);
lv_obj_set_style_text_color(lbl, lv_color_white(), 0);
lv_obj_center(lbl);
return btn;
}
/* 按键放入网格并自动拉伸填满格子 */
static void key_place(lv_obj_t * grid, int32_t row, int32_t col, int32_t span,
const char * txt, uint32_t bg_hex,
const lv_font_t * font, char key)
{
lv_obj_t * btn = key_create(grid, txt, bg_hex, font, key);
lv_obj_set_grid_cell(btn,
LV_GRID_ALIGN_STRETCH, col, span,
LV_GRID_ALIGN_STRETCH, row, 1);
}
/**********************
* GLOBAL FUNCTIONS
**********************/
void ui_init(void)
{
lv_display_t * disp;
lv_obj_t * scr;
lv_obj_t * card;
lv_obj_t * keypad;
int32_t scr_w;
int32_t scr_h;
int32_t card_h;
int32_t keypad_y;
int32_t keypad_h;
/* 键盘 4 列 x 5 行 */
static const int32_t col_dsc[] = {LV_GRID_FR(1), LV_GRID_FR(1),
LV_GRID_FR(1), LV_GRID_FR(1),
LV_GRID_TEMPLATE_LAST};
static const int32_t row_dsc[] = {LV_GRID_FR(1), LV_GRID_FR(1),
LV_GRID_FR(1), LV_GRID_FR(1),
LV_GRID_FR(1), LV_GRID_TEMPLATE_LAST};
if (cal_timer != NULL)
{
lv_timer_delete(cal_timer);
cal_timer = NULL;
}
calc_reset();
scr = lv_screen_active();
lv_obj_clean(scr); /* 从校准界面返回时清掉旧对象 */
lv_obj_set_style_bg_color(scr, lv_color_hex(UI_BG_HEX), 0);
lv_obj_set_style_bg_opa(scr, LV_OPA_COVER, 0);
lv_obj_remove_flag(scr, LV_OBJ_FLAG_SCROLLABLE);
disp = lv_display_get_default();
scr_w = lv_display_get_horizontal_resolution(disp);
scr_h = lv_display_get_vertical_resolution(disp);
card_h = (scr_h * 26) / 100; /* 240 -> 62 */
keypad_y = UI_PAD + card_h + UI_GAP;
keypad_h = scr_h - keypad_y - UI_PAD;
/* ---------- 显示卡片 ---------- */
card = lv_obj_create(scr);
lv_obj_remove_style_all(card);
lv_obj_set_pos(card, UI_PAD, UI_PAD);
lv_obj_set_size(card, scr_w - (2 * UI_PAD), card_h);
lv_obj_set_style_bg_color(card, lv_color_hex(UI_CARD_HEX), 0);
lv_obj_set_style_bg_opa(card, LV_OPA_COVER, 0);
lv_obj_set_style_radius(card, 12, 0);
lv_obj_set_style_pad_hor(card, 10, 0);
lv_obj_set_style_pad_ver(card, 4, 0);
lv_obj_set_style_pad_row(card, 0, 0);
lv_obj_set_flex_flow(card, LV_FLEX_FLOW_COLUMN);
lv_obj_set_flex_align(card, LV_FLEX_ALIGN_END,
LV_FLEX_ALIGN_END, LV_FLEX_ALIGN_END);
lv_obj_remove_flag(card, LV_OBJ_FLAG_SCROLLABLE);
/* 长按显示区进入触摸校准 */
lv_obj_add_flag(card, LV_OBJ_FLAG_CLICKABLE);
lv_obj_add_event_cb(card, card_long_cb, LV_EVENT_LONG_PRESSED, NULL);
/* 上行:算式,如 "12 +" */
expr_label = lv_label_create(card);
lv_label_set_text(expr_label, "");
lv_obj_set_width(expr_label, LV_PCT(100));
lv_obj_set_style_text_align(expr_label, LV_TEXT_ALIGN_RIGHT, 0);
lv_obj_set_style_text_font(expr_label, &lv_font_montserrat_14, 0);
lv_obj_set_style_text_color(expr_label, lv_color_hex(UI_MUTED_HEX), 0);
/* 下行:当前输入或计算结果 */
main_label = lv_label_create(card);
lv_label_set_text(main_label, "0");
lv_obj_set_width(main_label, LV_PCT(100));
lv_label_set_long_mode(main_label, LV_LABEL_LONG_MODE_CLIP);
lv_obj_set_style_text_align(main_label, LV_TEXT_ALIGN_RIGHT, 0);
lv_obj_set_style_text_font(main_label, &lv_font_montserrat_28, 0);
lv_obj_set_style_text_color(main_label, lv_color_hex(UI_TEXT_HEX), 0);
/* ---------- 键盘区 ---------- */
keypad = lv_obj_create(scr);
lv_obj_remove_style_all(keypad);
lv_obj_set_pos(keypad, UI_PAD, keypad_y);
lv_obj_set_size(keypad, scr_w - (2 * UI_PAD), keypad_h);
lv_obj_set_style_pad_column(keypad, UI_GAP, 0);
lv_obj_set_style_pad_row(keypad, UI_GAP, 0);
lv_obj_set_grid_dsc_array(keypad, col_dsc, row_dsc);
lv_obj_remove_flag(keypad, LV_OBJ_FLAG_SCROLLABLE);
/* 第 1 行:功能键 */
key_place(keypad, 0, 0, 1, "C", UI_FUNC_HEX, &lv_font_montserrat_20, KEY_CLEAR);
key_place(keypad, 0, 1, 1, "+/-", UI_FUNC_HEX, &lv_font_montserrat_14, KEY_SIGN);
key_place(keypad, 0, 2, 1, "%", UI_FUNC_HEX, &lv_font_montserrat_20, KEY_PERCENT);
key_place(keypad, 0, 3, 1, "/", UI_OP_HEX, &lv_font_montserrat_20, '/');
/* 第 2 行 */
key_place(keypad, 1, 0, 1, "7", UI_DIGIT_HEX, &lv_font_montserrat_20, '7');
key_place(keypad, 1, 1, 1, "8", UI_DIGIT_HEX, &lv_font_montserrat_20, '8');
key_place(keypad, 1, 2, 1, "9", UI_DIGIT_HEX, &lv_font_montserrat_20, '9');
key_place(keypad, 1, 3, 1, "*", UI_OP_HEX, &lv_font_montserrat_20, '*');
/* 第 3 行 */
key_place(keypad, 2, 0, 1, "4", UI_DIGIT_HEX, &lv_font_montserrat_20, '4');
key_place(keypad, 2, 1, 1, "5", UI_DIGIT_HEX, &lv_font_montserrat_20, '5');
key_place(keypad, 2, 2, 1, "6", UI_DIGIT_HEX, &lv_font_montserrat_20, '6');
key_place(keypad, 2, 3, 1, "-", UI_OP_HEX, &lv_font_montserrat_20, '-');
/* 第 4 行 */
key_place(keypad, 3, 0, 1, "1", UI_DIGIT_HEX, &lv_font_montserrat_20, '1');
key_place(keypad, 3, 1, 1, "2", UI_DIGIT_HEX, &lv_font_montserrat_20, '2');
key_place(keypad, 3, 2, 1, "3", UI_DIGIT_HEX, &lv_font_montserrat_20, '3');
key_place(keypad, 3, 3, 1, "+", UI_OP_HEX, &lv_font_montserrat_20, '+');
/* 第 5 行:0 占两格 */
key_place(keypad, 4, 0, 2, "0", UI_DIGIT_HEX, &lv_font_montserrat_20, '0');
key_place(keypad, 4, 2, 1, ".", UI_DIGIT_HEX, &lv_font_montserrat_20, '.');
key_place(keypad, 4, 3, 1, "=", UI_EQ_HEX, &lv_font_montserrat_20, '=');
calc_refresh();
}
效果展示:

2.通过屏幕进行调整led灯随频率闪烁变化
核心部分:
/*********************
* DEFINES
*********************/
#define UI_BG_HEX 0x101828 /* 屏幕底色 */
#define UI_PANEL_HEX 0x1A2130 /* 面板 / 图表底色 */
#define UI_HEAD_HEX 0x1E2434 /* 顶栏底色 */
#define UI_BTN_HEX 0x2A3550 /* 普通按键 */
#define UI_TXT_HEX 0xFFFFFF
#define UI_MUTED_HEX 0x8A90A6
#define UI_ACCENT_HEX 0x33C6FF /* 高亮 / 频谱柱 */
#define UI_OK_HEX 0x36D399 /* 运行中 */
#define UI_STOP_HEX 0xF87272 /* 已停止 */
#define UI_PAD 4
#define UI_GAP 4
#define UI_HEAD_H 24
#define UI_MAIN_H 92
#define UI_CARD_W 150
#define UI_SLIDER_H 28
#define UI_ROW_H 36
/* 滑杆刻度:滑杆值 = 频率(Hz) × SLIDER_SCALE,步进 0.1Hz */
#define SLIDER_SCALE 10
#define SLIDER_MIN ((int32_t)(SW_FREQ_MIN_HZ * (float)SLIDER_SCALE + 0.5f))
#define SLIDER_MAX ((int32_t)(SW_FREQ_MAX_HZ * (float)SLIDER_SCALE + 0.5f))
/* 频谱图:柱数 与 纵轴满量程 */
#define SPEC_BARS 24
#define SPEC_MAX_VAL 100
/* 频谱图内部布局:面板 = 上边距 + 图表 + 2px 间隔 + 频率刻度条 + 下边距 */
#define SPEC_PAD 4
#define SPEC_AXIS_H 20
#define SPEC_CHART_H (UI_MAIN_H - (2 * SPEC_PAD) - SPEC_AXIS_H - 2)
#define SPEC_TICK_NUM 4 /* 频率刻度个数:0.1 / 1 / 10 / 100 Hz */
/**********************
* STATIC VARIABLES
**********************/
static lv_obj_t * lbl_value = NULL; /* 目标频率数值 */
static lv_obj_t * lbl_act = NULL; /* 顶栏实测频率 */
static lv_obj_t * led_out = NULL; /* 输出电平指示灯 */
static lv_obj_t * chart = NULL; /* 频谱图 */
static lv_chart_series_t * chart_ser = NULL;
static lv_obj_t * slider = NULL; /* 频率滑杆 */
static lv_obj_t * btn_run = NULL; /* 启动 / 停止 */
static lv_obj_t * lbl_run = NULL;
static lv_obj_t * spec_marker = NULL; /* 当前频率在频谱上的指示线 */
static int32_t spec_plot_w = 0; /* 频谱绘图区宽度(像素) */
static lv_timer_t * ui_timer = NULL; /* 周期刷新定时器 */
static uint8_t freq_lock = 0u; /* 防止滑杆回调递归 */
/**********************
* FORWARD DECLS
**********************/
static void ui_set_freq(float hz);
/**********************
* STATIC FUNCTIONS
**********************/
/* 刷新"目标频率"大号数字 */
static void ui_refresh_value(void)
{
char buf[16];
if (lbl_value == NULL)
{
return;
}
snprintf(buf, sizeof(buf), "%.1f", (double)switch_gen_get_freq());
lv_label_set_text(lbl_value, buf);
}
/* 刷新启动/停止按键的外观 */
static void ui_refresh_run(void)
{
if ((btn_run == NULL) || (lbl_run == NULL))
{
return;
}
if (switch_gen_get_enable() != 0u)
{
lv_label_set_text(lbl_run, "STOP");
lv_obj_set_style_bg_color(btn_run, lv_color_hex(UI_STOP_HEX), 0);
}
else
{
lv_label_set_text(lbl_run, "START");
lv_obj_set_style_bg_color(btn_run, lv_color_hex(UI_OK_HEX), 0);
}
}
/* 设置频率(限幅 + 同步滑杆 + 刷新显示) */
static void ui_set_freq(float hz)
{
int32_t sv;
if (freq_lock != 0u)
{
return;
}
freq_lock = 1u;
switch_gen_set_freq(hz);
sv = (int32_t)(switch_gen_get_freq() * (float)SLIDER_SCALE + 0.5f);
if (sv < SLIDER_MIN)
{
sv = SLIDER_MIN;
}
if (sv > SLIDER_MAX)
{
sv = SLIDER_MAX;
}
if (slider != NULL)
{
lv_slider_set_value(slider, sv, LV_ANIM_OFF);
}
ui_refresh_value();
freq_lock = 0u;
}
/* 频率 -> 频谱绘图区横坐标(对数轴;第 i 根柱的有效中心 = (i + 0.5) / SPEC_BARS) */
static int32_t spec_x_of(float f, int32_t plot_w)
{
float lmin = log10f(SW_FREQ_MIN_HZ);
float bin = (log10f(SW_FREQ_MAX_HZ) - lmin) / (float)(SPEC_BARS - 1);
float i;
int32_t x;
if (plot_w <= 0)
{
return 0;
}
if (f < SW_FREQ_MIN_HZ)
{
f = SW_FREQ_MIN_HZ;
}
if (f > SW_FREQ_MAX_HZ)
{
f = SW_FREQ_MAX_HZ;
}
i = (log10f(f) - lmin) / bin;
x = (int32_t)(((i + 0.5f) / (float)SPEC_BARS) * (float)plot_w + 0.5f);
if (x < 0)
{
x = 0;
}
if (x > (plot_w - 1))
{
x = plot_w - 1;
}
return x;
}
/* 根据当前频率生成方波频谱:基波 + 奇次谐波各形成一个谱峰 */
static void spectrum_update(float f0)
{
static int32_t vals[SPEC_BARS];
float lmin = log10f(SW_FREQ_MIN_HZ);
float lmax = log10f(SW_FREQ_MAX_HZ);
float bin = (lmax - lmin) / (float)(SPEC_BARS - 1);
int32_t i;
if ((chart == NULL) || (chart_ser == NULL))
{
return;
}
/* 当前频率指示线跟着频率一起移动 */
if (spec_marker != NULL)
{
lv_obj_set_pos(spec_marker, spec_x_of(f0, spec_plot_w) - 1, 0);
}
for (i = 0; i < SPEC_BARS; i++)
{
float li = lmin + bin * (float)i;
float amp = 0.0f;
int32_t n;
for (n = 1; n <= 51; n += 2)
{
float fn = f0 * (float)n;
float d;
if (fn > (SW_FREQ_MAX_HZ * 1.5f))
{
break;
}
/* 相邻柱在对数频率轴上的距离,< 1 表示落在该柱的窗口内 */
d = fabsf(li - log10f(fn)) / bin;
if (d < 1.0f)
{
amp += (1.0f - d) / (float)n;
}
}
vals[i] = (int32_t)(amp * (float)SPEC_MAX_VAL + 0.5f);
if (vals[i] > SPEC_MAX_VAL)
{
vals[i] = SPEC_MAX_VAL;
}
if (vals[i] < 0)
{
vals[i] = 0;
}
}
lv_chart_set_series_values(chart, chart_ser, vals, SPEC_BARS);
lv_chart_refresh(chart);
}
/* 周期刷新:实测频率、指示灯、频谱图 */
static void ui_timer_cb(lv_timer_t * timer)
{
char buf[24];
uint8_t en;
uint8_t out;
float meas;
float f0;
(void)timer;
if ((lbl_act == NULL) || (led_out == NULL))
{
return;
}
en = switch_gen_get_enable();
out = switch_gen_get_output();
meas = switch_gen_get_measured();
if ((en != 0u) && (meas >= 0.05f))
{
snprintf(buf, sizeof(buf), "ACT %.1f Hz", (double)meas);
f0 = meas;
}
else
{
snprintf(buf, sizeof(buf), "ACT --");
f0 = switch_gen_get_freq();
}
lv_label_set_text(lbl_act, buf);
/* 输出电平指示灯 */
if (out != 0u)
{
lv_led_on(led_out);
}
else
{
lv_led_off(led_out);
}
spectrum_update(f0);
}
/* 步进按键:user_data = ±1(Hz) */
static void step_cb(lv_event_t * e)
{
float step = (float)(intptr_t)lv_event_get_user_data(e);
ui_set_freq(switch_gen_get_freq() + step);
}
/* 预设按键:user_data = 频率 × 10 */
static void preset_cb(lv_event_t * e)
{
int32_t tenths = (int32_t)(intptr_t)lv_event_get_user_data(e);
ui_set_freq((float)tenths / (float)SLIDER_SCALE);
}
/* 滑杆拖动 */
static void slider_cb(lv_event_t * e)
{
(void)e;
if (freq_lock != 0u)
{
return;
}
ui_set_freq((float)lv_slider_get_value(slider) / (float)SLIDER_SCALE);
}
/* 启动 / 停止 */
static void run_cb(lv_event_t * e)
{
(void)e;
switch_gen_set_enable((switch_gen_get_enable() != 0u) ? 0u : 1u);
ui_refresh_run();
ui_timer_cb(NULL);
}
/* 创建一个圆角按键,返回按键对象,文本作为其唯一子对象 */
static lv_obj_t * make_button(lv_obj_t * parent, const char * txt,
uint32_t bg_hex, const lv_font_t * font,
lv_event_cb_t cb, void * user_data)
{
lv_obj_t * btn;
lv_obj_t * lbl;
lv_color_t bg = lv_color_hex(bg_hex);
btn = lv_button_create(parent);
lv_obj_remove_style_all(btn);
lv_obj_set_style_bg_color(btn, bg, 0);
lv_obj_set_style_bg_opa(btn, LV_OPA_COVER, 0);
lv_obj_set_style_radius(btn, 7, 0);
lv_obj_set_style_bg_color(btn, lv_color_lighten(bg, 40), LV_STATE_PRESSED);
lv_obj_remove_flag(btn, LV_OBJ_FLAG_SCROLLABLE);
if (cb != NULL)
{
lv_obj_add_event_cb(btn, cb, LV_EVENT_CLICKED, user_data);
}
lbl = lv_label_create(btn);
lv_label_set_text(lbl, txt);
lv_obj_set_style_text_font(lbl, font, 0);
lv_obj_set_style_text_color(lbl, lv_color_hex(UI_TXT_HEX), 0);
lv_obj_center(lbl);
return btn;
}
/**********************
* 触摸四点校准
**********************/
#define CAL_POINT_COUNT 4U /* 靶标个数 */
#define CAL_MARGIN 30 /* 靶标距屏幕边缘距离 */
#define CAL_SAMPLES 6U /* 单点采样次数 */
#define CAL_DEBOUNCE 3U /* 去抖次数(20ms/次) */
static lv_obj_t * cal_ring = NULL;
static lv_obj_t * cal_bar_h = NULL;
static lv_obj_t * cal_bar_v = NULL;
static lv_obj_t * cal_tip = NULL;
static lv_timer_t * cal_timer = NULL;
static uint16_t cal_raw[CAL_POINT_COUNT][2];
static int32_t cal_px[CAL_POINT_COUNT];
static int32_t cal_py[CAL_POINT_COUNT];
static uint8_t cal_index; /* 当前靶标序号 */
static uint8_t cal_phase; /* 0 等按下 / 1 采样 / 2 等抬起 */
static uint8_t cal_debounce;
static uint8_t cal_samples;
static uint32_t cal_sum_x;
static uint32_t cal_sum_y;
/* 把标定结果限制在 12 位 ADC 的有效范围内 */
static uint16_t cal_clamp(double value)
{
if (value < 0.0)
{
return 0U;
}
if (value > 4095.0)
{
return 4095U;
}
return (uint16_t)(value + 0.5);
}
/* 把靶标移到第 cal_index 个位置并刷新提示文字 */
static void cal_move_target(void)
{
char text[40];
int32_t x = cal_px[cal_index];
int32_t y = cal_py[cal_index];
lv_obj_set_pos(cal_ring, x - 28, y - 28);
lv_obj_set_pos(cal_bar_h, x - 20, y - 2);
lv_obj_set_pos(cal_bar_v, x - 2, y - 20);
snprintf(text, sizeof(text), "TAP THE CROSS %u/%u",
(unsigned)(cal_index + 1U), (unsigned)CAL_POINT_COUNT);
lv_label_set_text(cal_tip, text);
}
/* 四点采集完成:解算标定参数、应用并返回主界面 */
static void cal_finish(void)
{
XPT2046_Calibration cal;
lv_display_t * disp = lv_display_get_default();
int32_t scr_w = lv_display_get_horizontal_resolution(disp);
int32_t scr_h = lv_display_get_vertical_resolution(disp);
int32_t xl = CAL_MARGIN;
int32_t xr = scr_w - 1 - CAL_MARGIN;
int32_t yt = CAL_MARGIN;
int32_t yb = scr_h - 1 - CAL_MARGIN;
double r[CAL_POINT_COUNT][2];
double h_left;
double h_right;
double v_top;
double v_bottom;
double slope;
double raw_lo;
double raw_hi;
uint8_t swap;
uint8_t i;
for (i = 0U; i < CAL_POINT_COUNT; i++)
{
r[i][0] = (double)cal_raw[i][0];
r[i][1] = (double)cal_raw[i][1];
}
/* 水平方向(左上->右上)哪个原始通道变化更大,它就是横轴 */
{
double dx = fabs(r[0][0] - r[1][0]) + fabs(r[2][0] - r[3][0]);
double dy = fabs(r[0][1] - r[1][1]) + fabs(r[2][1] - r[3][1]);
swap = (dx >= dy) ? 0U : 1U;
}
if (swap == 0U)
{
h_left = (r[0][0] + r[2][0]) / 2.0;
h_right = (r[1][0] + r[3][0]) / 2.0;
v_top = (r[0][1] + r[1][1]) / 2.0;
v_bottom = (r[2][1] + r[3][1]) / 2.0;
}
else
{
h_left = (r[0][1] + r[2][1]) / 2.0;
h_right = (r[1][1] + r[3][1]) / 2.0;
v_top = (r[0][0] + r[1][0]) / 2.0;
v_bottom = (r[2][0] + r[3][0]) / 2.0;
}
/* 两点几乎重合说明采样无效,重新采一轮 */
if ((fabs(h_right - h_left) < 80.0) || (fabs(v_bottom - v_top) < 80.0))
{
cal_index = 0U;
cal_phase = 0U;
cal_debounce = 0U;
cal_move_target();
lv_label_set_text(cal_tip, "BAD DATA, TAP AGAIN");
return;
}
/* 水平轴:由两个靶标点线性外推到屏幕左右边缘 */
slope = (h_right - h_left) / (double)(xr - xl);
raw_lo = h_left - (double)xl * slope;
raw_hi = h_left + (double)(scr_w - 1 - xl) * slope;
cal.x_min = cal_clamp(raw_lo);
cal.x_max = cal_clamp(raw_hi);
/* 竖直轴 */
slope = (v_bottom - v_top) / (double)(yb - yt);
raw_lo = v_top - (double)yt * slope;
raw_hi = v_top + (double)(scr_h - 1 - yt) * slope;
cal.y_min = cal_clamp(raw_lo);
cal.y_max = cal_clamp(raw_hi);
cal.swap_xy = swap;
cal.invert_x = 0U;
cal.invert_y = 0U;
XPT2046_SetCalibration(&cal);
printf("\r\n==== XPT2046 calibration ====\r\n");
printf("#define XPT2046_RAW_X_MIN %uU\r\n", (unsigned)cal.x_min);
printf("#define XPT2046_RAW_X_MAX %uU\r\n", (unsigned)cal.x_max);
printf("#define XPT2046_RAW_Y_MIN %uU\r\n", (unsigned)cal.y_min);
printf("#define XPT2046_RAW_Y_MAX %uU\r\n", (unsigned)cal.y_max);
printf("#define XPT2046_SWAP_XY %uU\r\n", (unsigned)cal.swap_xy);
printf("#define XPT2046_INVERT_X %uU\r\n", (unsigned)cal.invert_x);
printf("#define XPT2046_INVERT_Y %uU\r\n", (unsigned)cal.invert_y);
printf("=============================\r\n");
if (cal_timer != NULL)
{
lv_timer_delete(cal_timer);
cal_timer = NULL;
}
cal_ring = NULL;
cal_bar_h = NULL;
cal_bar_v = NULL;
cal_tip = NULL;
ui_init();
}
/* 校准采样状态机,20ms 一次 */
static void cal_timer_cb(lv_timer_t * timer)
{
uint16_t rx = 0U;
uint16_t ry = 0U;
uint8_t pressed;
(void)timer;
pressed = XPT2046_ReadRawPoint(&rx, &ry);
switch (cal_phase)
{
case 3U: /* 刚进入:等长按的手指先离开 */
if (pressed == 0U)
{
cal_phase = 0U;
cal_debounce = 0U;
}
break;
case 0U: /* 等待一次新的按下 */
if (pressed != 0U)
{
if (cal_debounce < CAL_DEBOUNCE)
{
cal_debounce++;
}
if (cal_debounce >= CAL_DEBOUNCE)
{
cal_phase = 1U;
cal_sum_x = rx;
cal_sum_y = ry;
cal_samples = 1U;
}
}
else
{
cal_debounce = 0U;
}
break;
case 1U: /* 累积采样 */
if ((pressed != 0U) && (cal_samples < CAL_SAMPLES))
{
cal_sum_x += rx;
cal_sum_y += ry;
cal_samples++;
if (cal_samples < CAL_SAMPLES)
{
break;
}
}
/* 采样够了或者手指提前抬起,都记下这一点的平均值 */
cal_raw[cal_index][0] = (uint16_t)(cal_sum_x / cal_samples);
cal_raw[cal_index][1] = (uint16_t)(cal_sum_y / cal_samples);
cal_index++;
cal_phase = 2U;
break;
case 2U: /* 等手指抬起,避免一次点击采多点 */
if (pressed == 0U)
{
cal_phase = 0U;
cal_debounce = 0U;
if (cal_index >= CAL_POINT_COUNT)
{
cal_finish();
}
else
{
cal_move_target();
}
}
break;
default:
break;
}
}
/* 进入四点校准界面 */
static void touch_calibration_start(void)
{
lv_display_t * disp = lv_display_get_default();
lv_obj_t * scr = lv_screen_active();
int32_t scr_w = lv_display_get_horizontal_resolution(disp);
int32_t scr_h = lv_display_get_vertical_resolution(disp);
uint8_t i;
lv_obj_clean(scr);
lv_obj_set_style_bg_color(scr, lv_color_hex(0x101828), 0);
lv_obj_set_style_bg_opa(scr, LV_OPA_COVER, 0);
lv_obj_remove_flag(scr, LV_OBJ_FLAG_SCROLLABLE);
/* 主界面的对象随 clean 一起被删掉了,置空并停掉刷新定时器 */
lbl_value = NULL;
lbl_act = NULL;
led_out = NULL;
chart = NULL;
chart_ser = NULL;
spec_marker = NULL;
slider = NULL;
btn_run = NULL;
lbl_run = NULL;
if (ui_timer != NULL)
{
lv_timer_delete(ui_timer);
ui_timer = NULL;
}
cal_px[0] = CAL_MARGIN;
cal_py[0] = CAL_MARGIN;
cal_px[1] = scr_w - 1 - CAL_MARGIN;
cal_py[1] = CAL_MARGIN;
cal_px[2] = CAL_MARGIN;
cal_py[2] = scr_h - 1 - CAL_MARGIN;
cal_px[3] = scr_w - 1 - CAL_MARGIN;
cal_py[3] = scr_h - 1 - CAL_MARGIN;
for (i = 0U; i < CAL_POINT_COUNT; i++)
{
cal_raw[i][0] = 0U;
cal_raw[i][1] = 0U;
}
/* 靶标:一个圆环加十字线 */
cal_ring = lv_obj_create(scr);
lv_obj_remove_style_all(cal_ring);
lv_obj_set_size(cal_ring, 56, 56);
lv_obj_set_style_radius(cal_ring, LV_RADIUS_CIRCLE, 0);
lv_obj_set_style_border_width(cal_ring, 2, 0);
lv_obj_set_style_border_color(cal_ring, lv_color_hex(0x4C6FFF), 0);
lv_obj_remove_flag(cal_ring, LV_OBJ_FLAG_SCROLLABLE);
lv_obj_remove_flag(cal_ring, LV_OBJ_FLAG_CLICKABLE);
cal_bar_h = lv_obj_create(scr);
lv_obj_remove_style_all(cal_bar_h);
lv_obj_set_size(cal_bar_h, 40, 4);
lv_obj_set_style_bg_color(cal_bar_h, lv_color_white(), 0);
lv_obj_set_style_bg_opa(cal_bar_h, LV_OPA_COVER, 0);
lv_obj_remove_flag(cal_bar_h, LV_OBJ_FLAG_SCROLLABLE);
lv_obj_remove_flag(cal_bar_h, LV_OBJ_FLAG_CLICKABLE);
cal_bar_v = lv_obj_create(scr);
lv_obj_remove_style_all(cal_bar_v);
lv_obj_set_size(cal_bar_v, 4, 40);
lv_obj_set_style_bg_color(cal_bar_v, lv_color_white(), 0);
lv_obj_set_style_bg_opa(cal_bar_v, LV_OPA_COVER, 0);
lv_obj_remove_flag(cal_bar_v, LV_OBJ_FLAG_SCROLLABLE);
lv_obj_remove_flag(cal_bar_v, LV_OBJ_FLAG_CLICKABLE);
cal_tip = lv_label_create(scr);
lv_obj_set_style_text_font(cal_tip, &lv_font_montserrat_14, 0);
lv_obj_set_style_text_color(cal_tip, lv_color_white(), 0);
lv_obj_align(cal_tip, LV_ALIGN_CENTER, 0, 0);
lv_obj_set_style_text_align(cal_tip, LV_TEXT_ALIGN_CENTER, 0);
cal_index = 0U;
cal_phase = 3U; /* 先等长按的手指抬起 */
cal_debounce = 0U;
cal_samples = 0U;
cal_move_target();
if (cal_timer != NULL)
{
lv_timer_delete(cal_timer);
}
cal_timer = lv_timer_create(cal_timer_cb, 20U, NULL);
}
/* 切换界面会删掉正在派发事件的按钮,交给 async 在本轮事件结束后执行 */
static void cal_start_async(void * unused)
{
(void)unused;
touch_calibration_start();
}
/* 长按左侧目标频率卡片进入校准 */
static void card_long_cb(lv_event_t * e)
{
(void)e;
(void)lv_async_call(cal_start_async, NULL);
}
/**********************
* GLOBAL FUNCTIONS
**********************/
void ui_init(void)
{
lv_display_t * disp;
lv_obj_t * scr;
lv_obj_t * head;
lv_obj_t * card;
lv_obj_t * unit;
lv_obj_t * row;
lv_obj_t * spec_panel;
lv_obj_t * spec_axis;
int32_t scr_w;
int32_t scr_h;
int32_t spec_x;
int32_t spec_w;
int32_t plot_w;
int32_t main_y;
int32_t slider_y;
int32_t row_y;
int32_t run_y;
/* 重新构建界面前,先清掉旧的定时器与对象指针 */
if (cal_timer != NULL)
{
lv_timer_delete(cal_timer);
cal_timer = NULL;
}
if (ui_timer != NULL)
{
lv_timer_delete(ui_timer);
ui_timer = NULL;
}
cal_ring = NULL;
cal_bar_h = NULL;
cal_bar_v = NULL;
cal_tip = NULL;
lbl_value = NULL;
lbl_act = NULL;
led_out = NULL;
chart = NULL;
chart_ser = NULL;
spec_marker = NULL;
slider = NULL;
btn_run = NULL;
lbl_run = NULL;
disp = lv_display_get_default();
scr = lv_screen_active();
scr_w = lv_display_get_horizontal_resolution(disp);
scr_h = lv_display_get_vertical_resolution(disp);
lv_obj_clean(scr);
lv_obj_set_style_bg_color(scr, lv_color_hex(UI_BG_HEX), 0);
lv_obj_set_style_bg_opa(scr, LV_OPA_COVER, 0);
lv_obj_remove_flag(scr, LV_OBJ_FLAG_SCROLLABLE);
main_y = UI_PAD + UI_HEAD_H + UI_GAP;
slider_y = main_y + UI_MAIN_H + UI_GAP;
row_y = slider_y + UI_SLIDER_H + UI_GAP;
run_y = row_y + UI_ROW_H + UI_GAP;
/* ---------- 顶栏 ---------- */
head = lv_obj_create(scr);
lv_obj_remove_style_all(head);
lv_obj_set_pos(head, UI_PAD, UI_PAD);
lv_obj_set_size(head, scr_w - (2 * UI_PAD), UI_HEAD_H);
lv_obj_set_style_bg_color(head, lv_color_hex(UI_HEAD_HEX), 0);
lv_obj_set_style_bg_opa(head, LV_OPA_COVER, 0);
lv_obj_set_style_radius(head, 8, 0);
lv_obj_remove_flag(head, LV_OBJ_FLAG_SCROLLABLE);
{
lv_obj_t * title = lv_label_create(head);
lv_label_set_text(title, "FREQ SWITCH");
lv_obj_set_style_text_font(title, &lv_font_montserrat_14, 0);
lv_obj_set_style_text_color(title, lv_color_hex(UI_TXT_HEX), 0);
lv_obj_align(title, LV_ALIGN_LEFT_MID, 7, 0);
}
lbl_act = lv_label_create(head);
lv_label_set_text(lbl_act, "ACT --");
lv_obj_set_style_text_font(lbl_act, &lv_font_montserrat_14, 0);
lv_obj_set_style_text_color(lbl_act, lv_color_hex(UI_ACCENT_HEX), 0);
lv_obj_align(lbl_act, LV_ALIGN_RIGHT_MID, -7, 0);
/* ---------- 左:目标频率卡片(长按进入触摸校准) ---------- */
card = lv_obj_create(scr);
lv_obj_remove_style_all(card);
lv_obj_set_pos(card, UI_PAD, main_y);
lv_obj_set_size(card, UI_CARD_W, UI_MAIN_H);
lv_obj_set_style_bg_color(card, lv_color_hex(UI_PANEL_HEX), 0);
lv_obj_set_style_bg_opa(card, LV_OPA_COVER, 0);
lv_obj_set_style_radius(card, 10, 0);
lv_obj_remove_flag(card, LV_OBJ_FLAG_SCROLLABLE);
lv_obj_add_flag(card, LV_OBJ_FLAG_CLICKABLE);
lv_obj_add_event_cb(card, card_long_cb, LV_EVENT_LONG_PRESSED, NULL);
led_out = lv_led_create(card);
lv_obj_set_size(led_out, 14, 14);
lv_obj_align(led_out, LV_ALIGN_TOP_RIGHT, -8, 8);
lv_led_set_color(led_out, lv_color_hex(UI_OK_HEX));
lv_led_off(led_out);
/* 不要让指示灯挡住卡片的长按(进入触摸校准) */
lv_obj_remove_flag(led_out, LV_OBJ_FLAG_CLICKABLE);
{
lv_obj_t * lb = lv_label_create(card);
lv_label_set_text(lb, "TARGET");
lv_obj_set_style_text_font(lb, &lv_font_montserrat_14, 0);
lv_obj_set_style_text_color(lb, lv_color_hex(UI_MUTED_HEX), 0);
lv_obj_align(lb, LV_ALIGN_TOP_MID, 0, 5);
}
lbl_value = lv_label_create(card);
lv_label_set_text(lbl_value, "10.0");
lv_obj_set_style_text_font(lbl_value, &lv_font_montserrat_28, 0);
lv_obj_set_style_text_color(lbl_value, lv_color_hex(UI_TXT_HEX), 0);
lv_obj_align(lbl_value, LV_ALIGN_CENTER, 0, 4);
unit = lv_label_create(card);
lv_label_set_text(unit, "Hz");
lv_obj_set_style_text_font(unit, &lv_font_montserrat_14, 0);
lv_obj_set_style_text_color(unit, lv_color_hex(UI_MUTED_HEX), 0);
lv_obj_align(unit, LV_ALIGN_BOTTOM_MID, 0, -4);
/* ---------- 右:频谱图(对数频率轴 0.1 ~ 100 Hz) ---------- */
spec_x = UI_PAD + UI_CARD_W + UI_GAP;
spec_w = scr_w - UI_PAD - spec_x;
plot_w = spec_w - (2 * SPEC_PAD);
spec_plot_w = plot_w;
spec_panel = lv_obj_create(scr);
lv_obj_remove_style_all(spec_panel);
lv_obj_set_pos(spec_panel, spec_x, main_y);
lv_obj_set_size(spec_panel, spec_w, UI_MAIN_H);
lv_obj_set_style_bg_color(spec_panel, lv_color_hex(UI_PANEL_HEX), 0);
lv_obj_set_style_bg_opa(spec_panel, LV_OPA_COVER, 0);
lv_obj_set_style_radius(spec_panel, 10, 0);
lv_obj_remove_flag(spec_panel, LV_OBJ_FLAG_SCROLLABLE);
chart = lv_chart_create(spec_panel);
lv_obj_set_pos(chart, SPEC_PAD, SPEC_PAD);
lv_obj_set_size(chart, plot_w, SPEC_CHART_H);
lv_obj_set_style_bg_color(chart, lv_color_hex(0x141B28), 0);
lv_obj_set_style_bg_opa(chart, LV_OPA_COVER, 0);
lv_obj_set_style_radius(chart, 4, 0);
lv_obj_set_style_border_width(chart, 0, 0);
lv_obj_set_style_pad_all(chart, 0, 0);
lv_obj_set_style_line_color(chart, lv_color_hex(0x27324A), LV_PART_MAIN);
lv_obj_remove_flag(chart, LV_OBJ_FLAG_SCROLLABLE);
lv_chart_set_type(chart, LV_CHART_TYPE_BAR);
lv_chart_set_point_count(chart, SPEC_BARS);
lv_chart_set_axis_range(chart, LV_CHART_AXIS_PRIMARY_Y, 0, SPEC_MAX_VAL);
lv_chart_set_div_line_count(chart, 3, 4);
chart_ser = lv_chart_add_series(chart, lv_color_hex(UI_ACCENT_HEX),
LV_CHART_AXIS_PRIMARY_Y);
lv_chart_set_all_values(chart, chart_ser, 0);
/* 当前频率指示线:竖直白线,随频率在频谱上左右移动 */
spec_marker = lv_obj_create(chart);
lv_obj_remove_style_all(spec_marker);
lv_obj_set_size(spec_marker, 2, SPEC_CHART_H);
lv_obj_set_pos(spec_marker, 0, 0);
lv_obj_set_style_bg_color(spec_marker, lv_color_white(), 0);
lv_obj_set_style_bg_opa(spec_marker, LV_OPA_70, 0);
lv_obj_remove_flag(spec_marker, LV_OBJ_FLAG_SCROLLABLE);
lv_obj_remove_flag(spec_marker, LV_OBJ_FLAG_CLICKABLE);
/* 频率刻度条:在对数轴上标出 0.1 / 1 / 10 / 100 Hz 的位置 */
spec_axis = lv_obj_create(spec_panel);
lv_obj_remove_style_all(spec_axis);
lv_obj_set_pos(spec_axis, SPEC_PAD, SPEC_PAD + SPEC_CHART_H + 2);
lv_obj_set_size(spec_axis, plot_w, SPEC_AXIS_H);
lv_obj_remove_flag(spec_axis, LV_OBJ_FLAG_SCROLLABLE);
{
/* 对数轴上的十进制倍数刻度 */
static const float tick_f[SPEC_TICK_NUM] = {0.1f, 1.0f, 10.0f, 100.0f};
static const char * tick_s[SPEC_TICK_NUM] = {"0.1", "1", "10", "100"};
lv_obj_t * tick[SPEC_TICK_NUM];
int32_t k;
for (k = 0; k < SPEC_TICK_NUM; k++)
{
tick[k] = lv_label_create(spec_axis);
lv_label_set_text(tick[k], tick_s[k]);
lv_obj_set_style_text_font(tick[k], &lv_font_montserrat_14, 0);
lv_obj_set_style_text_color(tick[k], lv_color_hex(UI_MUTED_HEX), 0);
}
/* 文字宽度要布局之后才知道,用它把标签中点对准刻度所在位置 */
lv_obj_update_layout(spec_axis);
for (k = 0; k < SPEC_TICK_NUM; k++)
{
int32_t w = lv_obj_get_width(tick[k]);
int32_t x = spec_x_of(tick_f[k], plot_w) - (w / 2);
if (x < 0)
{
x = 0;
}
if (x > (plot_w - w))
{
x = plot_w - w;
}
lv_obj_set_pos(tick[k], x,
(SPEC_AXIS_H - lv_obj_get_height(tick[k])) / 2);
}
}
/* ---------- 频率滑杆 ---------- */
slider = lv_slider_create(scr);
lv_obj_set_pos(slider, UI_PAD, slider_y);
lv_obj_set_size(slider, scr_w - (2 * UI_PAD), UI_SLIDER_H);
lv_slider_set_range(slider, SLIDER_MIN, SLIDER_MAX);
lv_obj_set_style_bg_color(slider, lv_color_hex(UI_BTN_HEX), LV_PART_MAIN);
lv_obj_set_style_bg_opa(slider, LV_OPA_COVER, LV_PART_MAIN);
lv_obj_set_style_radius(slider, LV_RADIUS_CIRCLE, LV_PART_MAIN);
lv_obj_set_style_bg_color(slider, lv_color_hex(UI_ACCENT_HEX), LV_PART_INDICATOR);
lv_obj_set_style_bg_opa(slider, LV_OPA_COVER, LV_PART_INDICATOR);
lv_obj_set_style_radius(slider, LV_RADIUS_CIRCLE, LV_PART_INDICATOR);
lv_obj_set_style_bg_color(slider, lv_color_hex(UI_TXT_HEX), LV_PART_KNOB);
lv_obj_set_style_bg_opa(slider, LV_OPA_COVER, LV_PART_KNOB);
lv_obj_set_style_radius(slider, LV_RADIUS_CIRCLE, LV_PART_KNOB);
lv_obj_set_style_pad_all(slider, 4, LV_PART_KNOB);
lv_obj_add_event_cb(slider, slider_cb, LV_EVENT_VALUE_CHANGED, NULL);
/* ---------- 步进 / 预设按键 ---------- */
row = lv_obj_create(scr);
lv_obj_remove_style_all(row);
lv_obj_set_pos(row, UI_PAD, row_y);
lv_obj_set_size(row, scr_w - (2 * UI_PAD), UI_ROW_H);
lv_obj_set_flex_flow(row, LV_FLEX_FLOW_ROW);
lv_obj_set_style_pad_column(row, UI_GAP, 0);
lv_obj_remove_flag(row, LV_OBJ_FLAG_SCROLLABLE);
{
lv_obj_t * b;
b = make_button(row, "-", UI_BTN_HEX, &lv_font_montserrat_20,
step_cb, (void *)(intptr_t)(-1));
lv_obj_set_flex_grow(b, 1);
lv_obj_set_height(b, LV_PCT(100));
b = make_button(row, "+", UI_BTN_HEX, &lv_font_montserrat_20,
step_cb, (void *)(intptr_t)(1));
lv_obj_set_flex_grow(b, 1);
lv_obj_set_height(b, LV_PCT(100));
b = make_button(row, "1", UI_BTN_HEX, &lv_font_montserrat_14,
preset_cb, (void *)(intptr_t)(1 * SLIDER_SCALE));
lv_obj_set_flex_grow(b, 1);
lv_obj_set_height(b, LV_PCT(100));
b = make_button(row, "5", UI_BTN_HEX, &lv_font_montserrat_14,
preset_cb, (void *)(intptr_t)(5 * SLIDER_SCALE));
lv_obj_set_flex_grow(b, 1);
lv_obj_set_height(b, LV_PCT(100));
b = make_button(row, "10", UI_BTN_HEX, &lv_font_montserrat_14,
preset_cb, (void *)(intptr_t)(10 * SLIDER_SCALE));
lv_obj_set_flex_grow(b, 1);
lv_obj_set_height(b, LV_PCT(100));
b = make_button(row, "50", UI_BTN_HEX, &lv_font_montserrat_14,
preset_cb, (void *)(intptr_t)(50 * SLIDER_SCALE));
lv_obj_set_flex_grow(b, 1);
lv_obj_set_height(b, LV_PCT(100));
b = make_button(row, "100", UI_BTN_HEX, &lv_font_montserrat_14,
preset_cb, (void *)(intptr_t)(100 * SLIDER_SCALE));
lv_obj_set_flex_grow(b, 1);
lv_obj_set_height(b, LV_PCT(100));
}
/* ---------- 启动 / 停止 ---------- */
btn_run = make_button(scr, "START", UI_OK_HEX, &lv_font_montserrat_20,
run_cb, NULL);
lv_obj_set_pos(btn_run, UI_PAD, run_y);
lv_obj_set_size(btn_run, scr_w - (2 * UI_PAD), scr_h - UI_PAD - run_y);
lbl_run = lv_obj_get_child(btn_run, 0);
/* ---------- 初始状态 ---------- */
ui_set_freq(switch_gen_get_freq());
ui_refresh_run();
ui_timer = lv_timer_create(ui_timer_cb, 100U, NULL);
ui_timer_cb(NULL);
}
效果展示:

3.通过屏幕上显示串口接收和串口输入输出通信交互
核心部分:
/*********************
* DEFINES
*********************/
#define UI_BG_HEX 0x101828 /* 屏幕底色 */
#define UI_PANEL_HEX 0x1A2130 /* 面板底色 */
#define UI_HEAD_HEX 0x1E2434 /* 顶栏底色 */
#define UI_BTN_HEX 0x2A3550 /* 普通按键 */
#define UI_FIELD_HEX 0x141B28 /* 输入 / 显示区底色 */
#define UI_TXT_HEX 0xFFFFFF
#define UI_MUTED_HEX 0x8A90A6
#define UI_ACCENT_HEX 0x33C6FF /* 高亮 / 计数 */
#define UI_OK_HEX 0x36D399 /* 发送按键 */
#define UI_STOP_HEX 0xF87272 /* 清空按键 */
#define UI_PAD 4
#define UI_GAP 4
#define UI_HEAD_H 20
#define UI_HEAD_Y UI_PAD
#define UI_LOG_Y (UI_HEAD_Y + UI_HEAD_H + UI_GAP) /* 28 */
#define UI_LOG_H 56
#define UI_ROW_Y (UI_LOG_Y + UI_LOG_H + UI_GAP) /* 88 */
#define UI_ROW_H 26
#define UI_KB_Y (UI_ROW_Y + UI_ROW_H + UI_GAP) /* 118 */
#define UI_INPUT_W 174 /* 输入框宽度 */
#define UI_SEND_W 62 /* 发送按键宽度 */
#define UI_CLR_W 68 /* 清空按键宽度 */
#define RX_POLL_MS 50U /* 接收轮询周期(ms) */
#define LOG_MAX_CHARS 420U /* 显示区缓存上限,超过先自动清空 */
#define INPUT_MAX_CHARS 32U /* 单条发送内容上限 */
/**********************
* STATIC VARIABLES
**********************/
static lv_obj_t * log_ta = NULL; /* 接收显示区(只读) */
static lv_obj_t * in_ta = NULL; /* 输入框 */
static lv_obj_t * kb = NULL; /* 软键盘 */
static lv_obj_t * lbl_rx = NULL; /* 顶栏接收计数 */
static lv_timer_t * ui_timer = NULL; /* 接收轮询定时器 */
static uint32_t log_len = 0U; /* 显示区当前字符数 */
static uint32_t rx_total = 0U; /* 累计接收字节数 */
/**********************
* FORWARD DECLS
**********************/
static void ui_log_add(const char *txt);
/**********************
* STATIC FUNCTIONS
**********************/
/* 清空接收显示区并复位字符计数 */
static void ui_log_clear(void)
{
if (log_ta == NULL)
{
return;
}
lv_textarea_set_text(log_ta, "");
log_len = 0U;
}
/* 追加一行文本到接收显示区;缓存接近上限时先自动清空 */
static void ui_log_add(const char *txt)
{
uint32_t len;
if ((log_ta == NULL) || (txt == NULL))
{
return;
}
len = (uint32_t)strlen(txt);
if (len == 0U)
{
return;
}
/* 接收满:先自动清理缓存界面,再从空开始显示 */
if ((log_len + len) > LOG_MAX_CHARS)
{
ui_log_clear();
}
/* 单条就超过上限:只保留尾部 */
if (len > LOG_MAX_CHARS)
{
txt += (len - LOG_MAX_CHARS);
len = LOG_MAX_CHARS;
}
lv_textarea_add_text(log_ta, txt);
log_len += len;
/* 光标移到末尾并滚到底部,保证最新数据可见 */
lv_textarea_set_cursor_pos(log_ta, LV_TEXTAREA_CURSOR_LAST);
lv_obj_scroll_to_y(log_ta, LV_COORD_MAX, LV_ANIM_OFF);
}
/* 把原始接收字节过滤成可显示文本(\r 丢弃,\n 保留,其它控制符变 '.') */
static uint16_t rx_filter(const uint8_t *src, uint16_t n, char *dst)
{
uint16_t i;
uint16_t m = 0U;
for (i = 0U; i < n; i++)
{
uint8_t c = src[i];
if (c == (uint8_t)'\r')
{
continue;
}
if (c == (uint8_t)'\n')
{
dst[m++] = '\n';
continue;
}
if (c == (uint8_t)'\t')
{
dst[m++] = ' ';
continue;
}
if ((c < 0x20U) || (c == 0x7FU))
{
dst[m++] = '.';
continue;
}
dst[m++] = (char)c;
}
dst[m] = '\0';
return m;
}
/* 周期把串口1 收到的字节搬进显示区 */
static void ui_rx_timer_cb(lv_timer_t * timer)
{
char buf[40];
char cnt[16];
uint8_t raw[32];
uint16_t n;
(void)timer;
n = uart1_rx_read(raw, (uint16_t)sizeof(raw));
while (n > 0U)
{
(void)rx_filter(raw, n, buf);
ui_log_add(buf);
rx_total += n;
n = uart1_rx_read(raw, (uint16_t)sizeof(raw));
}
if (lbl_rx != NULL)
{
snprintf(cnt, sizeof(cnt), "RX %lu", (unsigned long)rx_total);
lv_label_set_text(lbl_rx, cnt);
}
}
/* 读取输入框内容,经串口1 发出并本地回显 */
static void ui_send_text(void)
{
const char *txt;
char echo[INPUT_MAX_CHARS + 8U];
if (in_ta == NULL)
{
return;
}
txt = lv_textarea_get_text(in_ta);
if ((txt == NULL) || (txt[0] == '\0'))
{
return;
}
uart1_send_string(txt); /* 发送输入内容 */
uart1_send((const uint8_t *)"\r\n", 2U); /* 补一个换行,便于对端按行解析 */
snprintf(echo, sizeof(echo), "> %s\n", txt);/* 本地回显 */
ui_log_add(echo);
lv_textarea_set_text(in_ta, ""); /* 清空输入框 */
lv_textarea_set_cursor_pos(in_ta, LV_TEXTAREA_CURSOR_LAST);
}
/* SEND 按键 */
static void send_cb(lv_event_t * e)
{
(void)e;
ui_send_text();
}
/* CLR 按键 */
static void clear_cb(lv_event_t * e)
{
(void)e;
ui_log_clear();
}
/* 软键盘:按 OK 键直接发送 */
static void kb_cb(lv_event_t * e)
{
if (lv_event_get_code(e) == LV_EVENT_READY)
{
ui_send_text();
}
}
/* 创建一个圆角按键,返回按键对象,文本作为其唯一子对象 */
static lv_obj_t * make_button(lv_obj_t * parent, const char * txt,
uint32_t bg_hex, const lv_font_t * font,
lv_event_cb_t cb, void * user_data)
{
lv_obj_t * btn;
lv_obj_t * lbl;
lv_color_t bg = lv_color_hex(bg_hex);
btn = lv_button_create(parent);
lv_obj_remove_style_all(btn);
lv_obj_set_style_bg_color(btn, bg, 0);
lv_obj_set_style_bg_opa(btn, LV_OPA_COVER, 0);
lv_obj_set_style_radius(btn, 6, 0);
lv_obj_set_style_bg_color(btn, lv_color_lighten(bg, 40), LV_STATE_PRESSED);
lv_obj_remove_flag(btn, LV_OBJ_FLAG_SCROLLABLE);
if (cb != NULL)
{
lv_obj_add_event_cb(btn, cb, LV_EVENT_CLICKED, user_data);
}
lbl = lv_label_create(btn);
lv_label_set_text(lbl, txt);
lv_obj_set_style_text_font(lbl, font, 0);
lv_obj_set_style_text_color(lbl, lv_color_hex(UI_TXT_HEX), 0);
lv_obj_center(lbl);
return btn;
}
/**********************
* 触摸四点校准
**********************/
#define CAL_POINT_COUNT 4U /* 靶标个数 */
#define CAL_MARGIN 30 /* 靶标距屏幕边缘距离 */
#define CAL_SAMPLES 6U /* 单点采样次数 */
#define CAL_DEBOUNCE 3U /* 去抖次数(20ms/次) */
static lv_obj_t * cal_ring = NULL;
static lv_obj_t * cal_bar_h = NULL;
static lv_obj_t * cal_bar_v = NULL;
static lv_obj_t * cal_tip = NULL;
static lv_timer_t * cal_timer = NULL;
static lv_timer_t * cal_exit_timer = NULL; /* 校准完成后的返回延时 */
static lv_timer_t * boot_cal_timer = NULL; /* 开机按住屏幕进校准的检测 */
static uint16_t cal_raw[CAL_POINT_COUNT][2];
static int32_t cal_px[CAL_POINT_COUNT];
static int32_t cal_py[CAL_POINT_COUNT];
static uint8_t cal_index; /* 当前靶标序号 */
static uint8_t cal_phase; /* 0 等按下 / 1 采样 / 2 等抬起 */
static uint8_t cal_debounce;
static uint8_t cal_samples;
static uint32_t cal_sum_x;
static uint32_t cal_sum_y;
/* 把标定结果限制在 12 位 ADC 的有效范围内 */
static uint16_t cal_clamp(double value)
{
if (value < 0.0)
{
return 0U;
}
if (value > 4095.0)
{
return 4095U;
}
return (uint16_t)(value + 0.5);
}
/* 校准结果提示看够 0.7 秒后回主界面 */
static void cal_exit_cb(lv_timer_t * timer)
{
lv_timer_delete(timer);
cal_exit_timer = NULL;
ui_init();
}
/* 把靶标移到第 cal_index 个位置并刷新提示文字 */
static void cal_move_target(void)
{
char text[40];
int32_t x = cal_px[cal_index];
int32_t y = cal_py[cal_index];
lv_obj_set_pos(cal_ring, x - 28, y - 28);
lv_obj_set_pos(cal_bar_h, x - 20, y - 2);
lv_obj_set_pos(cal_bar_v, x - 2, y - 20);
snprintf(text, sizeof(text), "TAP THE CROSS %u/%u",
(unsigned)(cal_index + 1U), (unsigned)CAL_POINT_COUNT);
lv_label_set_text(cal_tip, text);
}
/* 四点采集完成:解算标定参数、应用并返回主界面 */
static void cal_finish(void)
{
XPT2046_Calibration cal;
lv_display_t * disp = lv_display_get_default();
int32_t scr_w = lv_display_get_horizontal_resolution(disp);
int32_t scr_h = lv_display_get_vertical_resolution(disp);
int32_t xl = CAL_MARGIN;
int32_t xr = scr_w - 1 - CAL_MARGIN;
int32_t yt = CAL_MARGIN;
int32_t yb = scr_h - 1 - CAL_MARGIN;
double r[CAL_POINT_COUNT][2];
double h_left;
double h_right;
double v_top;
double v_bottom;
double slope;
double raw_lo;
double raw_hi;
uint8_t swap;
uint8_t saved;
uint8_t i;
for (i = 0U; i < CAL_POINT_COUNT; i++)
{
r[i][0] = (double)cal_raw[i][0];
r[i][1] = (double)cal_raw[i][1];
}
/* 水平方向(左上->右上)哪个原始通道变化更大,它就是横轴 */
{
double dx = fabs(r[0][0] - r[1][0]) + fabs(r[2][0] - r[3][0]);
double dy = fabs(r[0][1] - r[1][1]) + fabs(r[2][1] - r[3][1]);
swap = (dx >= dy) ? 0U : 1U;
}
if (swap == 0U)
{
h_left = (r[0][0] + r[2][0]) / 2.0;
h_right = (r[1][0] + r[3][0]) / 2.0;
v_top = (r[0][1] + r[1][1]) / 2.0;
v_bottom = (r[2][1] + r[3][1]) / 2.0;
}
else
{
h_left = (r[0][1] + r[2][1]) / 2.0;
h_right = (r[1][1] + r[3][1]) / 2.0;
v_top = (r[0][0] + r[1][0]) / 2.0;
v_bottom = (r[2][0] + r[3][0]) / 2.0;
}
/* 两点几乎重合说明采样无效,重新采一轮 */
if ((fabs(h_right - h_left) < 80.0) || (fabs(v_bottom - v_top) < 80.0))
{
cal_index = 0U;
cal_phase = 0U;
cal_debounce = 0U;
cal_move_target();
lv_label_set_text(cal_tip, "BAD DATA, TAP AGAIN");
return;
}
/* 水平轴:由两个靶标点线性外推到屏幕左右边缘 */
slope = (h_right - h_left) / (double)(xr - xl);
raw_lo = h_left - (double)xl * slope;
raw_hi = h_left + (double)(scr_w - 1 - xl) * slope;
cal.x_min = cal_clamp(raw_lo);
cal.x_max = cal_clamp(raw_hi);
/* 竖直轴 */
slope = (v_bottom - v_top) / (double)(yb - yt);
raw_lo = v_top - (double)yt * slope;
raw_hi = v_top + (double)(scr_h - 1 - yt) * slope;
cal.y_min = cal_clamp(raw_lo);
cal.y_max = cal_clamp(raw_hi);
cal.swap_xy = swap;
cal.invert_x = 0U;
cal.invert_y = 0U;
XPT2046_SetCalibration(&cal);
/* 新参数立刻生效;同时写进内部 Flash,下次上电 XPT2046_Init() 会自动读回。
这样"触控偏移 -> 重新校准"一次就够了,不需要再靠串口打印数值回填宏。
写失败不影响本次使用,只是掉电后会退回上一次保存的值。 */
saved = XPT2046_CalibrationSave();
if (cal_timer != NULL)
{
lv_timer_delete(cal_timer);
cal_timer = NULL;
}
if (cal_tip != NULL)
{
lv_label_set_text(cal_tip, (saved != 0U) ? "CALIBRATED & SAVED"
: "CALIBRATED (SAVE FAILED)");
}
/* 停一下让用户看清结果,再回主界面 */
cal_exit_timer = lv_timer_create(cal_exit_cb, 700U, NULL);
}
/* 校准采样状态机,20ms 一次 */
static void cal_timer_cb(lv_timer_t * timer)
{
uint16_t rx = 0U;
uint16_t ry = 0U;
uint8_t pressed;
(void)timer;
pressed = XPT2046_ReadRawPoint(&rx, &ry);
switch (cal_phase)
{
case 3U: /* 刚进入:等长按的手指先离开 */
if (pressed == 0U)
{
cal_phase = 0U;
cal_debounce = 0U;
}
break;
case 0U: /* 等待一次新的按下 */
if (pressed != 0U)
{
if (cal_debounce < CAL_DEBOUNCE)
{
cal_debounce++;
}
if (cal_debounce >= CAL_DEBOUNCE)
{
cal_phase = 1U;
cal_sum_x = rx;
cal_sum_y = ry;
cal_samples = 1U;
}
}
else
{
cal_debounce = 0U;
}
break;
case 1U: /* 累积采样 */
if ((pressed != 0U) && (cal_samples < CAL_SAMPLES))
{
cal_sum_x += rx;
cal_sum_y += ry;
cal_samples++;
if (cal_samples < CAL_SAMPLES)
{
break;
}
}
/* 采样够了或者手指提前抬起,都记下这一点的平均值 */
cal_raw[cal_index][0] = (uint16_t)(cal_sum_x / cal_samples);
cal_raw[cal_index][1] = (uint16_t)(cal_sum_y / cal_samples);
cal_index++;
cal_phase = 2U;
break;
case 2U: /* 等手指抬起,避免一次点击采多点 */
if (pressed == 0U)
{
cal_phase = 0U;
cal_debounce = 0U;
if (cal_index >= CAL_POINT_COUNT)
{
cal_finish();
}
else
{
cal_move_target();
}
}
break;
default:
break;
}
}
/* 进入四点校准界面 */
static void touch_calibration_start(void)
{
lv_display_t * disp = lv_display_get_default();
lv_obj_t * scr = lv_screen_active();
int32_t scr_w = lv_display_get_horizontal_resolution(disp);
int32_t scr_h = lv_display_get_vertical_resolution(disp);
uint8_t i;
lv_obj_clean(scr);
lv_obj_set_style_bg_color(scr, lv_color_hex(0x101828), 0);
lv_obj_set_style_bg_opa(scr, LV_OPA_COVER, 0);
lv_obj_remove_flag(scr, LV_OBJ_FLAG_SCROLLABLE);
/* 主界面的对象随 clean 一起被删掉了,置空并停掉刷新定时器 */
log_ta = NULL;
in_ta = NULL;
kb = NULL;
lbl_rx = NULL;
if (ui_timer != NULL)
{
lv_timer_delete(ui_timer);
ui_timer = NULL;
}
cal_px[0] = CAL_MARGIN;
cal_py[0] = CAL_MARGIN;
cal_px[1] = scr_w - 1 - CAL_MARGIN;
cal_py[1] = CAL_MARGIN;
cal_px[2] = CAL_MARGIN;
cal_py[2] = scr_h - 1 - CAL_MARGIN;
cal_px[3] = scr_w - 1 - CAL_MARGIN;
cal_py[3] = scr_h - 1 - CAL_MARGIN;
for (i = 0U; i < CAL_POINT_COUNT; i++)
{
cal_raw[i][0] = 0U;
cal_raw[i][1] = 0U;
}
/* 靶标:一个圆环加十字线 */
cal_ring = lv_obj_create(scr);
lv_obj_remove_style_all(cal_ring);
lv_obj_set_size(cal_ring, 56, 56);
lv_obj_set_style_radius(cal_ring, LV_RADIUS_CIRCLE, 0);
lv_obj_set_style_border_width(cal_ring, 2, 0);
lv_obj_set_style_border_color(cal_ring, lv_color_hex(0x4C6FFF), 0);
lv_obj_remove_flag(cal_ring, LV_OBJ_FLAG_SCROLLABLE);
lv_obj_remove_flag(cal_ring, LV_OBJ_FLAG_CLICKABLE);
cal_bar_h = lv_obj_create(scr);
lv_obj_remove_style_all(cal_bar_h);
lv_obj_set_size(cal_bar_h, 40, 4);
lv_obj_set_style_bg_color(cal_bar_h, lv_color_white(), 0);
lv_obj_set_style_bg_opa(cal_bar_h, LV_OPA_COVER, 0);
lv_obj_remove_flag(cal_bar_h, LV_OBJ_FLAG_SCROLLABLE);
lv_obj_remove_flag(cal_bar_h, LV_OBJ_FLAG_CLICKABLE);
cal_bar_v = lv_obj_create(scr);
lv_obj_remove_style_all(cal_bar_v);
lv_obj_set_size(cal_bar_v, 4, 40);
lv_obj_set_style_bg_color(cal_bar_v, lv_color_white(), 0);
lv_obj_set_style_bg_opa(cal_bar_v, LV_OPA_COVER, 0);
lv_obj_remove_flag(cal_bar_v, LV_OBJ_FLAG_SCROLLABLE);
lv_obj_remove_flag(cal_bar_v, LV_OBJ_FLAG_CLICKABLE);
cal_tip = lv_label_create(scr);
lv_obj_set_style_text_font(cal_tip, &lv_font_montserrat_14, 0);
lv_obj_set_style_text_color(cal_tip, lv_color_white(), 0);
lv_obj_align(cal_tip, LV_ALIGN_CENTER, 0, 0);
lv_obj_set_style_text_align(cal_tip, LV_TEXT_ALIGN_CENTER, 0);
cal_index = 0U;
cal_phase = 3U; /* 先等长按的手指抬起 */
cal_debounce = 0U;
cal_samples = 0U;
cal_move_target();
if (boot_cal_timer != NULL)
{
lv_timer_delete(boot_cal_timer);
boot_cal_timer = NULL;
}
if (cal_timer != NULL)
{
lv_timer_delete(cal_timer);
}
cal_timer = lv_timer_create(cal_timer_cb, 20U, NULL);
}
/* 切换界面会删掉正在派发事件的按钮,交给 async 在本轮事件结束后执行 */
static void cal_start_async(void * unused)
{
(void)unused;
touch_calibration_start();
}
/* ---------------- 开机"救命"校准入口 ----------------
* 标定偏移时可能连顶栏标题都点不中(点哪儿都跑偏),所以再给一条完全不
* 依赖坐标的通路:上电后 8 秒内按住屏幕任意位置 0.8 秒,直接进四点校准。
* 只在开机后检测一次,过了这个窗口就不再理会,免得正常使用时误触发。
*/
#define BOOT_CAL_WINDOW_MS 8000U
#define BOOT_CAL_HOLD_MS 800U
static uint8_t boot_cal_armed = 0U;
static uint32_t boot_cal_deadline = 0U;
static uint32_t boot_cal_hold = 0U;
static void boot_cal_cb(lv_timer_t * timer)
{
uint16_t raw_x = 0U;
uint16_t raw_y = 0U;
uint32_t now = lv_tick_get();
if (now > boot_cal_deadline)
{
lv_timer_delete(timer);
boot_cal_timer = NULL;
return;
}
if (XPT2046_ReadRawPoint(&raw_x, &raw_y) != 0U)
{
if (boot_cal_hold == 0U)
{
boot_cal_hold = now;
}
else if ((uint32_t)(now - boot_cal_hold) >= BOOT_CAL_HOLD_MS)
{
boot_cal_armed = 2U; /* 标记已用过,避免 ui_init 里再建定时器 */
boot_cal_timer = NULL;
lv_timer_delete(timer);
(void)lv_async_call(cal_start_async, NULL);
}
}
else
{
boot_cal_hold = 0U;
}
}
/* 长按顶栏标题进入校准 */
static void title_long_cb(lv_event_t * e)
{
(void)e;
(void)lv_async_call(cal_start_async, NULL);
}
/**********************
* GLOBAL FUNCTIONS
**********************/
void ui_init(void)
{
lv_display_t * disp;
lv_obj_t * scr;
lv_obj_t * head;
lv_obj_t * title;
lv_obj_t * btn;
int32_t scr_w;
int32_t scr_h;
int32_t field_w;
/* 重新构建界面前,先清掉旧的定时器与对象指针 */
if (cal_timer != NULL)
{
lv_timer_delete(cal_timer);
cal_timer = NULL;
}
if (cal_exit_timer != NULL)
{
lv_timer_delete(cal_exit_timer);
cal_exit_timer = NULL;
}
if (ui_timer != NULL)
{
lv_timer_delete(ui_timer);
ui_timer = NULL;
}
cal_ring = NULL;
cal_bar_h = NULL;
cal_bar_v = NULL;
cal_tip = NULL;
log_ta = NULL;
in_ta = NULL;
kb = NULL;
lbl_rx = NULL;
log_len = 0U;
disp = lv_display_get_default();
scr = lv_screen_active();
scr_w = lv_display_get_horizontal_resolution(disp);
scr_h = lv_display_get_vertical_resolution(disp);
field_w = scr_w - (2 * UI_PAD);
lv_obj_clean(scr);
lv_obj_set_style_bg_color(scr, lv_color_hex(UI_BG_HEX), 0);
lv_obj_set_style_bg_opa(scr, LV_OPA_COVER, 0);
lv_obj_remove_flag(scr, LV_OBJ_FLAG_SCROLLABLE);
/* ---------- 顶栏 ---------- */
head = lv_obj_create(scr);
lv_obj_remove_style_all(head);
lv_obj_set_pos(head, UI_PAD, UI_HEAD_Y);
lv_obj_set_size(head, field_w, UI_HEAD_H);
lv_obj_set_style_bg_color(head, lv_color_hex(UI_HEAD_HEX), 0);
lv_obj_set_style_bg_opa(head, LV_OPA_COVER, 0);
lv_obj_set_style_radius(head, 6, 0);
lv_obj_remove_flag(head, LV_OBJ_FLAG_SCROLLABLE);
/* 标题:长按进入触摸校准 */
title = lv_label_create(head);
lv_label_set_text(title, "UART1 LINK");
lv_obj_set_style_text_font(title, &lv_font_montserrat_14, 0);
lv_obj_set_style_text_color(title, lv_color_hex(UI_TXT_HEX), 0);
lv_obj_align(title, LV_ALIGN_LEFT_MID, 6, 0);
lv_obj_add_flag(title, LV_OBJ_FLAG_CLICKABLE);
lv_obj_add_event_cb(title, title_long_cb, LV_EVENT_LONG_PRESSED, NULL);
/* 顶栏整条也可以长按进校准:万一点不准标题,还有更大的目标可点 */
lv_obj_add_flag(head, LV_OBJ_FLAG_CLICKABLE);
lv_obj_add_event_cb(head, title_long_cb, LV_EVENT_LONG_PRESSED, NULL);
lbl_rx = lv_label_create(head);
lv_label_set_text(lbl_rx, "RX 0");
lv_obj_set_style_text_font(lbl_rx, &lv_font_montserrat_14, 0);
lv_obj_set_style_text_color(lbl_rx, lv_color_hex(UI_ACCENT_HEX), 0);
lv_obj_align(lbl_rx, LV_ALIGN_RIGHT_MID, -6, 0);
/* ---------- 接收显示区(只读、可滚屏) ---------- */
log_ta = lv_textarea_create(scr);
lv_obj_set_pos(log_ta, UI_PAD, UI_LOG_Y);
lv_obj_set_size(log_ta, field_w, UI_LOG_H);
lv_textarea_set_one_line(log_ta, false);
lv_textarea_set_max_length(log_ta, LOG_MAX_CHARS + 64U);
lv_textarea_set_placeholder_text(log_ta, "RX DATA ...");
lv_obj_set_style_text_font(log_ta, &lv_font_montserrat_14, 0);
lv_obj_set_style_text_color(log_ta, lv_color_hex(UI_TXT_HEX), 0);
lv_obj_set_style_text_align(log_ta, LV_TEXT_ALIGN_LEFT, 0);
lv_obj_set_style_bg_color(log_ta, lv_color_hex(UI_FIELD_HEX), 0);
lv_obj_set_style_bg_opa(log_ta, LV_OPA_COVER, 0);
lv_obj_set_style_border_width(log_ta, 1, 0);
lv_obj_set_style_border_color(log_ta, lv_color_hex(0x27324A), 0);
lv_obj_set_style_radius(log_ta, 6, 0);
lv_obj_set_style_pad_all(log_ta, 4, 0);
lv_obj_set_style_bg_color(log_ta, lv_color_hex(UI_FIELD_HEX), LV_PART_MAIN);
lv_obj_set_style_bg_opa(log_ta, LV_OPA_COVER, LV_PART_MAIN);
lv_obj_remove_flag(log_ta, LV_OBJ_FLAG_CLICKABLE); /* 只读:不接受点击编辑 */
/* ---------- 输入框(软键盘输入) ---------- */
in_ta = lv_textarea_create(scr);
lv_obj_set_pos(in_ta, UI_PAD, UI_ROW_Y);
lv_obj_set_size(in_ta, UI_INPUT_W, UI_ROW_H);
lv_textarea_set_one_line(in_ta, true);
lv_textarea_set_max_length(in_ta, INPUT_MAX_CHARS);
lv_textarea_set_placeholder_text(in_ta, "input...");
lv_obj_set_style_text_font(in_ta, &lv_font_montserrat_14, 0);
lv_obj_set_style_text_color(in_ta, lv_color_hex(UI_TXT_HEX), 0);
lv_obj_set_style_bg_color(in_ta, lv_color_hex(UI_FIELD_HEX), 0);
lv_obj_set_style_bg_opa(in_ta, LV_OPA_COVER, 0);
lv_obj_set_style_border_width(in_ta, 1, 0);
lv_obj_set_style_border_color(in_ta, lv_color_hex(UI_ACCENT_HEX), 0);
lv_obj_set_style_radius(in_ta, 6, 0);
lv_obj_set_style_pad_all(in_ta, 4, 0);
lv_obj_set_style_bg_color(in_ta, lv_color_hex(UI_FIELD_HEX), LV_PART_MAIN);
lv_obj_set_style_bg_opa(in_ta, LV_OPA_COVER, LV_PART_MAIN);
/* ---------- SEND / CLR 按键 ---------- */
btn = make_button(scr, "SEND", UI_OK_HEX, &lv_font_montserrat_14,
send_cb, NULL);
lv_obj_set_pos(btn, UI_PAD + UI_INPUT_W + UI_GAP, UI_ROW_Y);
lv_obj_set_size(btn, UI_SEND_W, UI_ROW_H);
btn = make_button(scr, "CLR", UI_STOP_HEX, &lv_font_montserrat_14,
clear_cb, NULL);
lv_obj_set_pos(btn, UI_PAD + UI_INPUT_W + UI_GAP + UI_SEND_W + UI_GAP, UI_ROW_Y);
lv_obj_set_size(btn, UI_CLR_W, UI_ROW_H);
/* ---------- 软键盘 ---------- */
kb = lv_keyboard_create(scr);
/* 这里必须用 lv_obj_align 而不是 lv_obj_set_pos:
lv_keyboard 的构造函数内部执行了 lv_obj_align(BOTTOM_MID, 0, 0),
会给键盘留下 LV_STYLE_ALIGN = LV_ALIGN_BOTTOM_MID。
而 lv_obj_set_pos() 只写 x/y、清不掉对齐方式,最终位置会按
"底部居中 + (x,y)" 计算:y = 240 - 118 + 118 = 240,
键盘整个被挪到屏幕下边缘之外 —— 现象就是"键盘创建了却看不见"。
lv_obj_align(TOP_LEFT, UI_PAD, UI_KB_Y) 会同时设定对齐方式,
结果就是左上角相对屏幕偏移 (4,118),和布局宏一致。 */
lv_obj_align(kb, LV_ALIGN_TOP_LEFT, UI_PAD, UI_KB_Y);
lv_obj_set_size(kb, field_w, scr_h - UI_PAD - UI_KB_Y);
lv_keyboard_set_mode(kb, LV_KEYBOARD_MODE_TEXT_LOWER);
lv_obj_set_style_pad_all(kb, 2, 0);
lv_obj_set_style_pad_row(kb, 2, 0);
lv_obj_set_style_pad_column(kb, 2, 0);
lv_obj_set_style_bg_color(kb, lv_color_hex(UI_PANEL_HEX), 0);
lv_obj_set_style_bg_opa(kb, LV_OPA_COVER, 0);
lv_obj_set_style_radius(kb, 6, 0);
lv_obj_set_style_bg_color(kb, lv_color_hex(UI_BTN_HEX), LV_PART_ITEMS);
lv_obj_set_style_bg_opa(kb, LV_OPA_COVER, LV_PART_ITEMS);
lv_obj_set_style_text_color(kb, lv_color_hex(UI_TXT_HEX), LV_PART_ITEMS);
lv_obj_set_style_radius(kb, 4, LV_PART_ITEMS);
lv_obj_set_style_border_width(kb, 0, LV_PART_ITEMS);
lv_obj_set_style_shadow_width(kb, 0, LV_PART_ITEMS);
lv_obj_set_style_bg_color(kb, lv_color_hex(UI_ACCENT_HEX), LV_STATE_PRESSED | LV_PART_ITEMS);
lv_obj_remove_flag(kb, LV_OBJ_FLAG_SCROLLABLE);
lv_obj_add_event_cb(kb, kb_cb, LV_EVENT_READY, NULL);
lv_keyboard_set_textarea(kb, in_ta);
/* ---------- 初始状态 ---------- */
ui_timer = lv_timer_create(ui_rx_timer_cb, RX_POLL_MS, NULL);
rx_total = 0U;
lv_label_set_text(lbl_rx, "RX 0");
/* ---------- 开机"救命"校准检测(只做一次) ---------- */
if (boot_cal_armed == 0U)
{
boot_cal_armed = 1U;
boot_cal_deadline = lv_tick_get() + BOOT_CAL_WINDOW_MS;
boot_cal_hold = 0U;
boot_cal_timer = lv_timer_create(boot_cal_cb, 20U, NULL);
}
}
效果展示:


4.通过屏幕上调整定时器的频率和控制占空比的变化
对应到屏幕上显示对应占空比变化和频率实时变化
核心部分:
/**********************
* GLOBAL FUNCTIONS
**********************/
/*********************
* PWM 页布局(TIM2_CH2 = PA1)
*********************/
#define UI_PAGE_PWM 0U /* 页面编号:PWM 设置页 */
#define UI_PAGE_UART 1U /* 页面编号:串口终端页 */
#define UI_PAGE_TOUCH 2U /* 页面编号:触摸自检页 */
#define UI_PAGE_ADC 3U /* 页面编号:ADC 采集页 */
/**********************
* STATIC VARIABLES
**********************/
static lv_obj_t * log_ta = NULL; /* 接收显示区(只读) */
static lv_obj_t * in_ta = NULL; /* 输入框 */
static lv_obj_t * kb = NULL; /* 软键盘 */
static lv_obj_t * lbl_rx = NULL; /* 顶栏接收计数 */
static lv_timer_t * ui_timer = NULL; /* 接收轮询定时器 */
static uint32_t log_len = 0U; /* 显示区当前字符数 */
static uint32_t rx_total = 0U; /* 累计接收字节数 */
static uint8_t ui_page = UI_PAGE_PWM; /* 当前页面编号 */
/* PWM 页的对象与状态 */
static lv_obj_t * pwm_lbl_f = NULL; /* F 频率 */
static lv_obj_t * pwm_lbl_d = NULL; /* D 占空比 */
static lv_obj_t * pwm_lbl_t = NULL; /* T 周期 */
static lv_obj_t * pwm_lbl_on = NULL; /* Ton 高电平时间 */
static lv_obj_t * pwm_btn_run = NULL; /* RUN / STOP 按钮 */
static lv_obj_t * pwm_wave = NULL; /* 方波折线 */
static lv_timer_t * pwm_timer = NULL; /* 数值/波形刷新定时器 */
static int32_t pwm_wave_w = 304; /* 折线可用宽度(像素) */
static lv_point_precise_t pwm_pts[1 + (PWM_WAVE_CYCLES * 4)];
/* PWM 页显示缓存:只有真正变化时才重绘。
本机是单缓冲 + 阻塞刷屏,界面如果一直重绘,会把 LVGL 的触摸采样
定时器挤到后面(表现为点不动、长按也进不了校准),所以必须做到
"参数不变就一个像素都不刷"。 */
static uint8_t pwm_shown_valid = 0U; /* 缓存是否有效(页面刚重建时无效) */
static float pwm_shown_freq = 0.0f; /* 上次显示的频率 Hz */
static float pwm_shown_duty = 0.0f; /* 上次显示的占空比 % */
static uint8_t pwm_shown_run = 0U; /* 上次的启停状态 */
/* ---------------- 页面公共部分 ---------------- */
/* 释放当前页面的定时器与对象指针(切页 / 进校准前都要调) */
static void ui_page_release(void)
{
uint8_t st_i;
if (ui_timer != NULL)
{
lv_timer_delete(ui_timer);
ui_timer = NULL;
}
if (pwm_timer != NULL)
{
lv_timer_delete(pwm_timer);
pwm_timer = NULL;
}
if (st_timer != NULL)
{
lv_timer_delete(st_timer);
st_timer = NULL;
}
/* 校准还挂着的两个定时器也一并收掉(正常情况下已经是 NULL) */
if (cal_timer != NULL)
{
lv_timer_delete(cal_timer);
cal_timer = NULL;
}
if (cal_exit_timer != NULL)
{
lv_timer_delete(cal_exit_timer);
cal_exit_timer = NULL;
}
log_ta = NULL;
in_ta = NULL;
kb = NULL;
lbl_rx = NULL;
pwm_lbl_f = NULL;
pwm_lbl_d = NULL;
pwm_lbl_t = NULL;
pwm_lbl_on = NULL;
pwm_btn_run = NULL;
pwm_wave = NULL;
pwm_shown_valid = 0U; /* 页面重建后必须强制刷一次 */
/* 自检页的标签随 lv_obj_clean 一起没了;文本缓存也清掉,
这样下次重建时每一行都会重新写一遍(而不是被"内容没变"跳过)。 */
for (st_i = 0U; st_i < ST_ROWS; st_i++)
{
st_line[st_i] = NULL;
st_cache[st_i][0] = '\0';
}
st_evt_lbl = NULL;
st_left_lbl = NULL;
}
/* 切换页面必须走 async:直接在按钮自己的事件里重建,会把正在派发事件的按钮删掉 */
static void page_switch_async(void * arg)
{
ui_build_page((uint8_t)(intptr_t)arg);
}
static void ui_page_switch_cb(lv_event_t * e)
{
(void)lv_async_call(page_switch_async, lv_event_get_user_data(e));
}
/* 顶栏:标题(长按进触摸校准)+ 右上角页面切换按钮 */
static lv_obj_t * ui_make_head(lv_obj_t * scr, const char * title_text,
const char * btn_text, uint8_t btn_page)
{
lv_obj_t * head;
lv_obj_t * title;
lv_obj_t * btn;
int32_t field_w = lv_display_get_horizontal_resolution(lv_display_get_default())
- (2 * UI_PAD);
head = lv_obj_create(scr);
lv_obj_remove_style_all(head);
lv_obj_set_pos(head, UI_PAD, UI_HEAD_Y);
lv_obj_set_size(head, field_w, UI_HEAD_H);
lv_obj_set_style_bg_color(head, lv_color_hex(UI_HEAD_HEX), 0);
lv_obj_set_style_bg_opa(head, LV_OPA_COVER, 0);
lv_obj_set_style_radius(head, 6, 0);
lv_obj_remove_flag(head, LV_OBJ_FLAG_SCROLLABLE);
title = lv_label_create(head);
lv_label_set_text(title, title_text);
lv_obj_set_style_text_font(title, &lv_font_montserrat_14, 0);
lv_obj_set_style_text_color(title, lv_color_hex(UI_TXT_HEX), 0);
lv_obj_align(title, LV_ALIGN_LEFT_MID, 6, 0);
lv_obj_add_flag(title, LV_OBJ_FLAG_CLICKABLE);
lv_obj_add_event_cb(title, title_long_cb, LV_EVENT_LONG_PRESSED, NULL);
/* 顶栏整条也可以长按进校准:万一点不准标题,还有更大的目标可点 */
lv_obj_add_flag(head, LV_OBJ_FLAG_CLICKABLE);
lv_obj_add_event_cb(head, title_long_cb, LV_EVENT_LONG_PRESSED, NULL);
if (btn_text != NULL)
{
btn = make_button(head, btn_text, UI_BTN_HEX, &lv_font_montserrat_14,
ui_page_switch_cb, (void *)(intptr_t)btn_page);
lv_obj_set_size(btn, 46, 16);
lv_obj_align(btn, LV_ALIGN_RIGHT_MID, -3, 0);
}
return head;
}
/* ---------------- PWM 设置页(TIM2_CH2 = PA1) ---------------- */
/* 纳秒 -> ns / us / ms / s 文本(全部用整数运算,不依赖 %f) */
static void pwm_fmt_ns(char *buf, size_t n, uint32_t ns)
{
if (ns < 1000U)
{
snprintf(buf, n, "%lu ns", (unsigned long)ns);
}
else if (ns < 1000000U)
{
snprintf(buf, n, "%lu.%03lu us",
(unsigned long)(ns / 1000U), (unsigned long)(ns % 1000U));
}
else if (ns < 1000000000U)
{
snprintf(buf, n, "%lu.%03lu ms",
(unsigned long)(ns / 1000000U),
(unsigned long)((ns / 1000U) % 1000U));
}
else
{
snprintf(buf, n, "%lu.%03lu s",
(unsigned long)(ns / 1000000000U),
(unsigned long)((ns / 1000000U) % 1000U));
}
}
/* 频率 -> Hz / kHz 文本 */
static void pwm_fmt_freq(char *buf, size_t n, double hz)
{
uint32_t v;
if (hz < 1000.0)
{
v = (uint32_t)(hz * 10.0 + 0.5); /* 0.1 Hz */
snprintf(buf, n, "%lu.%lu Hz",
(unsigned long)(v / 10U), (unsigned long)(v % 10U));
}
else
{
v = (uint32_t)(hz / 10.0 + 0.5); /* 0.01 kHz */
snprintf(buf, n, "%lu.%02lu kHz",
(unsigned long)(v / 100U), (unsigned long)(v % 100U));
}
}
/* 方波示意图:按占空比铺 PWM_WAVE_CYCLES 个周期的高/低电平折线 */
static void pwm_wave_update(double duty)
{
const int32_t y_hi = 8;
const int32_t y_lo = 54;
int32_t pw;
int32_t on;
int32_t k;
int32_t x0;
uint8_t idx = 0U;
pw = (pwm_wave_w > 0) ? (pwm_wave_w / PWM_WAVE_CYCLES) : 0;
if (duty < 0.0) { duty = 0.0; }
if (duty > 100.0) { duty = 100.0; }
on = (int32_t)(((double)pw * duty) / 100.0 + 0.5);
if ((pw <= 0) || (on <= 0)) /* 0%:一条低电平横线 */
{
pwm_pts[0].x = 0; pwm_pts[0].y = y_lo;
pwm_pts[1].x = (int32_t)pwm_wave_w; pwm_pts[1].y = y_lo;
lv_line_set_points(pwm_wave, pwm_pts, 2U);
return;
}
if (on >= pw) /* 100%:一条高电平横线 */
{
pwm_pts[0].x = 0; pwm_pts[0].y = y_hi;
pwm_pts[1].x = (int32_t)pwm_wave_w; pwm_pts[1].y = y_hi;
lv_line_set_points(pwm_wave, pwm_pts, 2U);
return;
}
pwm_pts[idx].x = 0; pwm_pts[idx].y = y_lo; idx++; /* 起点:低 */
for (k = 0; k < PWM_WAVE_CYCLES; k++)
{
x0 = k * pw;
pwm_pts[idx].x = x0; pwm_pts[idx].y = y_hi; idx++; /* 上升沿 */
pwm_pts[idx].x = x0 + on; pwm_pts[idx].y = y_hi; idx++; /* 高电平 */
pwm_pts[idx].x = x0 + on; pwm_pts[idx].y = y_lo; idx++; /* 下降沿 */
pwm_pts[idx].x = x0 + pw; pwm_pts[idx].y = y_lo; idx++; /* 低电平 */
}
lv_line_set_points(pwm_wave, pwm_pts, idx);
}
/* 把当前 PWM 参数刷到界面:数值 + 方波示意图。
* 参数没变就直接返回 —— 本机单缓冲阻塞刷屏,界面一直在重绘会把
* LVGL 的触摸采样挤掉,所以静止时必须一次重绘都不产生。 */
static void pwm_page_update(void)
{
char num[24];
char buf[40];
char tbuf[24];
float freq;
float duty;
double period_ns;
uint32_t d10;
uint8_t running;
uint8_t dirty_freq;
uint8_t dirty_duty;
uint8_t dirty_run;
lv_obj_t * lbl;
if ((pwm_lbl_f == NULL) || (pwm_wave == NULL))
{
return;
}
freq = pwm_ch_get_real_freq();
duty = pwm_ch_get_real_duty();
running = pwm_ch_is_running();
if (pwm_shown_valid != 0U)
{
dirty_freq = (freq != pwm_shown_freq) ? 1U : 0U;
dirty_duty = (duty != pwm_shown_duty) ? 1U : 0U;
dirty_run = (running != pwm_shown_run) ? 1U : 0U;
if ((dirty_freq == 0U) && (dirty_duty == 0U) && (dirty_run == 0U))
{
return; /* 没变化:一个像素都不刷 */
}
}
else
{
dirty_freq = 1U;
dirty_duty = 1U;
dirty_run = 1U;
pwm_shown_valid = 1U;
}
pwm_shown_freq = freq;
pwm_shown_duty = duty;
pwm_shown_run = running;
period_ns = 1000000000.0 / (double)((freq > 0.001f) ? freq : 0.001f);
if (period_ns > 4294967000.0)
{
period_ns = 4294967000.0;
}
/* ---- 频率 / 周期 ---- */
if (dirty_freq != 0U)
{
pwm_fmt_freq(num, sizeof(num), (double)freq);
snprintf(buf, sizeof(buf), "F %s", num);
lv_label_set_text(pwm_lbl_f, buf);
pwm_fmt_ns(tbuf, sizeof(tbuf), (uint32_t)(period_ns + 0.5));
snprintf(buf, sizeof(buf), "T %s", tbuf);
lv_label_set_text(pwm_lbl_t, buf);
}
/* ---- 占空比 + 方波示意图 ---- */
if (dirty_duty != 0U)
{
d10 = (uint32_t)((double)duty * 10.0 + 0.5);
snprintf(buf, sizeof(buf), "D %lu.%lu %%",
(unsigned long)(d10 / 10U), (unsigned long)(d10 % 10U));
lv_label_set_text(pwm_lbl_d, buf);
pwm_wave_update((double)duty);
}
/* ---- 高电平时间:频率或占空比任一变化都要重算 ---- */
if ((dirty_freq != 0U) || (dirty_duty != 0U))
{
pwm_fmt_ns(tbuf, sizeof(tbuf),
(uint32_t)((period_ns * (double)duty / 100.0) + 0.5));
snprintf(buf, sizeof(buf), "Ton %s", tbuf);
lv_label_set_text(pwm_lbl_on, buf);
}
/* ---- 波形灰/亮 + RUN/STOP 文字:只有启停变化才动 ---- */
if (dirty_run != 0U)
{
lv_obj_set_style_line_color(pwm_wave,
lv_color_hex((running != 0U) ? UI_ACCENT_HEX
: UI_MUTED_HEX),
0);
if (pwm_btn_run != NULL)
{
lbl = lv_obj_get_child(pwm_btn_run, 0);
if (lbl != NULL)
{
lv_label_set_text(lbl, (running != 0U) ? "STOP" : "RUN");
}
}
}
}
static void pwm_timer_cb(lv_timer_t * timer)
{
(void)timer;
pwm_page_update();
}
/* 频率 -1k / -100 / +100 / +1k */
static void pwm_freq_cb(lv_event_t * e)
{
int32_t step = (int32_t)(intptr_t)lv_event_get_user_data(e);
pwm_ch_set_freq(pwm_ch_get_freq() + (float)step);
pwm_page_update();
}
/* 占空比:user_data 以 0.1% 为单位(±10 = 1%,±100 = 10%) */
static void pwm_duty_cb(lv_event_t * e)
{
int32_t step10 = (int32_t)(intptr_t)lv_event_get_user_data(e);
pwm_ch_set_duty(pwm_ch_get_duty() + ((float)step10 / 10.0f));
pwm_page_update();
}
/* 运行 / 停止 */
static void pwm_run_cb(lv_event_t * e)
{
(void)e;
pwm_ch_set_enable((pwm_ch_is_running() == 0U) ? 1U : 0U);
pwm_page_update();
}
/* 回到默认:1kHz / 50% */
static void pwm_default_cb(lv_event_t * e)
{
(void)e;
pwm_ch_set_freq(PWM_CH_DEFAULT_FREQ_HZ);
pwm_ch_set_duty(PWM_CH_DEFAULT_DUTY_PCT);
pwm_ch_set_enable(1U);
pwm_page_update();
}
static void ui_build_pwm(lv_obj_t * scr)
{
static const char *const freq_txt[4] = {"-1k", "-100", "+100", "+1k"};
static const int32_t freq_step[4] = {-1000, -100, 100, 1000};
static const char *const duty_txt[4] = {"-10%", "-1%", "+1%", "+10%"};
static const int32_t duty_step[4] = {-100, -10, 10, 100};
lv_obj_t * info;
lv_obj_t * panel;
lv_obj_t * btn;
int32_t field_w = lv_display_get_horizontal_resolution(lv_display_get_default())
- (2 * UI_PAD);
int32_t btn_w = (field_w - (3 * UI_GAP)) / 4;
int32_t big_w = (field_w - UI_GAP) / 2;
int32_t i;
(void)ui_make_head(scr, "PWM TIM2_CH2 / PA1", "UART", UI_PAGE_UART);
/* ---------- 数值:左边 F/T,右边 D/Ton ---------- */
info = lv_obj_create(scr);
lv_obj_remove_style_all(info);
lv_obj_set_pos(info, UI_PAD, PWM_INFO_Y);
lv_obj_set_size(info, field_w, PWM_INFO_H);
lv_obj_remove_flag(info, LV_OBJ_FLAG_SCROLLABLE);
pwm_lbl_f = lv_label_create(info);
lv_obj_set_style_text_font(pwm_lbl_f, &lv_font_montserrat_14, 0);
lv_obj_set_style_text_color(pwm_lbl_f, lv_color_hex(UI_ACCENT_HEX), 0);
lv_obj_align(pwm_lbl_f, LV_ALIGN_TOP_LEFT, 0, 0);
pwm_lbl_d = lv_label_create(info);
lv_obj_set_style_text_font(pwm_lbl_d, &lv_font_montserrat_14, 0);
lv_obj_set_style_text_color(pwm_lbl_d, lv_color_hex(UI_OK_HEX), 0);
lv_obj_align(pwm_lbl_d, LV_ALIGN_TOP_RIGHT, 0, 0);
pwm_lbl_t = lv_label_create(info);
lv_obj_set_style_text_font(pwm_lbl_t, &lv_font_montserrat_14, 0);
lv_obj_set_style_text_color(pwm_lbl_t, lv_color_hex(UI_MUTED_HEX), 0);
lv_obj_align(pwm_lbl_t, LV_ALIGN_BOTTOM_LEFT, 0, 0);
pwm_lbl_on = lv_label_create(info);
lv_obj_set_style_text_font(pwm_lbl_on, &lv_font_montserrat_14, 0);
lv_obj_set_style_text_color(pwm_lbl_on, lv_color_hex(UI_MUTED_HEX), 0);
lv_obj_align(pwm_lbl_on, LV_ALIGN_BOTTOM_RIGHT, 0, 0);
/* ---------- 方波示意图 ---------- */
panel = lv_obj_create(scr);
lv_obj_remove_style_all(panel);
lv_obj_set_pos(panel, UI_PAD, PWM_WAVE_Y);
lv_obj_set_size(panel, field_w, PWM_WAVE_H);
lv_obj_set_style_bg_color(panel, lv_color_hex(UI_FIELD_HEX), 0);
lv_obj_set_style_bg_opa(panel, LV_OPA_COVER, 0);
lv_obj_set_style_border_width(panel, 1, 0);
lv_obj_set_style_border_color(panel, lv_color_hex(0x27324A), 0);
lv_obj_set_style_radius(panel, 6, 0);
lv_obj_remove_flag(panel, LV_OBJ_FLAG_SCROLLABLE);
pwm_wave_w = field_w - (2 * PWM_WAVE_PAD);
pwm_wave = lv_line_create(panel);
lv_obj_remove_style_all(pwm_wave);
lv_obj_set_pos(pwm_wave, PWM_WAVE_PAD, PWM_WAVE_PAD);
lv_obj_set_style_line_width(pwm_wave, 2, 0);
lv_obj_set_style_line_rounded(pwm_wave, true, 0);
lv_obj_set_style_line_opa(pwm_wave, LV_OPA_COVER, 0);
lv_obj_set_style_line_color(pwm_wave, lv_color_hex(UI_ACCENT_HEX), 0);
/* ---------- 频率 / 占空比 加减按钮 ---------- */
for (i = 0; i < 4; i++)
{
btn = make_button(scr, freq_txt[i], UI_BTN_HEX, &lv_font_montserrat_14,
pwm_freq_cb, (void *)(intptr_t)freq_step[i]);
lv_obj_set_pos(btn, UI_PAD + (i * (btn_w + UI_GAP)), PWM_ROW1_Y);
lv_obj_set_size(btn, btn_w, PWM_ROW_H);
}
for (i = 0; i < 4; i++)
{
btn = make_button(scr, duty_txt[i], UI_BTN_HEX, &lv_font_montserrat_14,
pwm_duty_cb, (void *)(intptr_t)duty_step[i]);
lv_obj_set_pos(btn, UI_PAD + (i * (btn_w + UI_GAP)), PWM_ROW2_Y);
lv_obj_set_size(btn, btn_w, PWM_ROW_H);
}
/* ---------- 运行 / 默认值 ---------- */
pwm_btn_run = make_button(scr, "STOP", UI_BTN_HEX, &lv_font_montserrat_14,
pwm_run_cb, NULL);
lv_obj_set_pos(pwm_btn_run, UI_PAD, PWM_ROW3_Y);
lv_obj_set_size(pwm_btn_run, big_w, PWM_ROW3_H);
btn = make_button(scr, "1kHz 50%", UI_BTN_HEX, &lv_font_montserrat_14,
pwm_default_cb, NULL);
lv_obj_set_pos(btn, UI_PAD + big_w + UI_GAP, PWM_ROW3_Y);
lv_obj_set_size(btn, big_w, PWM_ROW3_H);
/* ---------- 先刷一次,再挂 100ms 周期刷新 ---------- */
pwm_page_update();
pwm_timer = lv_timer_create(pwm_timer_cb, PWM_REFRESH_MS, NULL);
}
效果展示:

5.通过对adc电位输入变化电平
在屏幕上显示adc 电压变化图
核心部分:
/*********************
* ADC 采集页布局(ADC1_INP10 = PC0)
*********************/
#define ADC_INFO_Y 26 /* 采样信息 3 行(左 3 右 3) */
#define ADC_INFO_H 48
#define ADC_INFO_ROW_H 16 /* 单行高度 */
#define ADC_WAVE_Y 78 /* 波形图外框 */
#define ADC_WAVE_H 80
#define ADC_WAVE_PAD 4 /* 折线相对外框的内缩 */
#define ADC_ROW_Y 164 /* HOLD / CAL / CLR 按钮行 */
#define ADC_ROW_H 36
#define ADC_HINT_Y 206 /* 底部提示行 */
#define ADC_LBL_COUNT 6U /* 信息标签个数 */
/* 波形刷新周期。304x72 的折线重绘一次约 41000 个像素,本机 SPI5 只有
8.6MHz,也就是约 41ms 的刷屏时间;250ms 刷一次大约占掉 16% 的刷屏带宽,
再快就会明显挤到触摸响应。想看"更新更快"可以把它调小,代价是界面变卡。 */
#define ADC_REFRESH_MS 250U
/* ADC 采集页的对象与状态。信息 6 个标签:下标偶数靠左、奇数靠右,
每两个一行,共 3 行(通道 / 点数 / 当前值 / 采样率 / MIN,MAX / AVG,PP)。 */
static lv_obj_t * adc_lbl[ADC_LBL_COUNT] = {NULL, NULL, NULL, NULL, NULL, NULL};
static lv_obj_t * adc_btn_hold = NULL; /* HOLD / RUN 按钮 */
static lv_obj_t * adc_wave = NULL; /* 采样波形折线 */
static lv_timer_t * adc_timer = NULL; /* 取帧 / 画波形定时器 */
static int32_t adc_wave_w = 304; /* 折线可用宽度(像素) */
static int32_t adc_wave_h = 72; /* 折线可用高度(像素) */
static lv_point_precise_t adc_pts[ADC_APP_FRAME_LEN];
static uint16_t adc_frame[ADC_APP_FRAME_LEN]; /* 取出来的最近一整帧 */
static char adc_cache[ADC_LBL_COUNT][32]; /* 上次显示过的文本 */
static uint8_t adc_hold = 0U; /* 1 = 冻结显示(采集照常跑) */
static uint8_t adc_shown_valid = 0U; /* 缓存是否有效(页面刚重建时无效) */
static uint32_t adc_last_seq = 0U; /* 上次显示的那一帧的序号 */
static uint64_t adc_sum = 0ULL; /* 累计码值和(算平均值用) */
static uint32_t adc_count = 0U; /* 累计点数 */
static uint16_t adc_min = 0xFFFFU;/* 累计最小值(原始码) */
static uint16_t adc_max = 0U; /* 累计最大值(原始码) */
/* ---------------- ADC 采集页(ADC1_INP10 / PC0) ----------------
*
* 数据源是 adc.c 里一直在跑的"连续转换 + 循环 DMA"采集流,这里只负责定时
* 取最近一整帧、算统计、画波形。取帧的判据是"帧序号变了没有":序号没变说明
* 还没采满新的一帧,这一轮一个像素都不刷(本机刷屏很贵,见 ADC_REFRESH_MS)。
*/
/* 累计统计复位:进页面、点 CLR、重新标定之后都调它 */
static void adc_stats_reset(void)
{
adc_sum = 0ULL;
adc_count = 0U;
adc_min = 0xFFFFU;
adc_max = 0U;
adc_shown_valid = 0U; /* 下一轮强制重刷,MIN/MAX/AVG 立刻归零 */
}
/* 文本没变就不 set_text:刷屏是单缓冲 + 异步 DMA,能省一次重绘是一次 */
static void adc_set_text(uint8_t idx, const char * txt)
{
if ((idx >= ADC_LBL_COUNT) || (adc_lbl[idx] == NULL))
{
return;
}
if (strcmp(adc_cache[idx], txt) == 0)
{
return;
}
strncpy(adc_cache[idx], txt, sizeof(adc_cache[idx]) - 1U);
adc_cache[idx][sizeof(adc_cache[idx]) - 1U] = '\0';
lv_label_set_text(adc_lbl[idx], adc_cache[idx]);
}
/* 把一帧原始码画成折线:满量程画到顶,0 画到底,横向铺满整个波形区 */
static void adc_wave_update(const uint16_t * frame)
{
int32_t i;
if (adc_wave == NULL)
{
return;
}
for (i = 0; i < (int32_t)ADC_APP_FRAME_LEN; i++)
{
int32_t y = adc_wave_h - 1
- (int32_t)(((uint32_t)frame[i] * (uint32_t)adc_wave_h) / 65536U);
if (y < 0)
{
y = 0;
}
else if (y > (adc_wave_h - 1))
{
y = adc_wave_h - 1;
}
adc_pts[i].x = ((int32_t)i * (adc_wave_w - 1)) / (int32_t)(ADC_APP_FRAME_LEN - 1U);
adc_pts[i].y = y;
}
lv_line_set_points(adc_wave, adc_pts, ADC_APP_FRAME_LEN);
}
/* 周期刷新:取最新一整帧 -> 更新统计 -> 画波形 -> 刷文字 */
static void adc_page_update(lv_timer_t * timer)
{
char buf[40];
uint32_t seq;
uint32_t i;
uint32_t sum = 0U;
uint32_t fmin = 0xFFFFFFFFU;
uint32_t fmax = 0U;
uint32_t sr;
uint32_t win10;
uint32_t mv10;
uint32_t mv_min;
uint32_t mv_max;
uint32_t mv_avg;
(void)timer;
if (adc_lbl[0] == NULL)
{
return;
}
/* ---- 一路都不变的静态信息:文本相同就不会真的重绘 ---- */
sr = (uint32_t)(adc_app_get_sample_rate() + 0.5f);
if (sr == 0U)
{
sr = 1U;
}
win10 = (1000U * ADC_APP_FRAME_LEN * 10U) / sr; /* 一帧时间,单位 0.1ms */
snprintf(buf, sizeof(buf), "CH%u PC0/INP10", (unsigned)ADC_APP_CHANNEL_NUM);
adc_set_text(0U, buf);
snprintf(buf, sizeof(buf), "%upt %lu.%lums",
(unsigned)ADC_APP_FRAME_LEN,
(unsigned long)(win10 / 10U), (unsigned long)(win10 % 10U));
adc_set_text(1U, buf);
snprintf(buf, sizeof(buf), "SR %lu Hz", (unsigned long)sr);
adc_set_text(3U, buf);
if (adc_hold != 0U)
{
return; /* 冻结:数字和波形都停在上一次的样子 */
}
/* ---- 取最近一整帧;序号没变说明还没有新的一帧,这轮直接跳过不重绘 ---- */
seq = adc_app_read_frame(adc_frame, ADC_APP_FRAME_LEN);
if ((adc_shown_valid != 0U) && (seq == adc_last_seq))
{
return;
}
adc_shown_valid = 1U;
adc_last_seq = seq;
/* ---- 这一帧的极值和 ---- */
for (i = 0U; i < ADC_APP_FRAME_LEN; i++)
{
uint32_t v = (uint32_t)adc_frame[i];
if (v < fmin)
{
fmin = v;
}
if (v > fmax)
{
fmax = v;
}
sum += v;
}
/* ---- 并进累计统计(点 CLR 归零):和用 64 位存,连续跑一天也不溢出 ---- */
adc_sum += (uint64_t)sum;
adc_count += ADC_APP_FRAME_LEN;
if ((uint16_t)fmin < adc_min)
{
adc_min = (uint16_t)fmin;
}
if ((uint16_t)fmax > adc_max)
{
adc_max = (uint16_t)fmax;
}
/* ---- 波形 ---- */
adc_wave_update(adc_frame);
/* ---- 文字 ---- */
mv10 = adc_app_code_to_mv10(adc_frame[ADC_APP_FRAME_LEN - 1U]);
mv_min = adc_app_code_to_mv(adc_min);
mv_max = adc_app_code_to_mv(adc_max);
mv_avg = adc_app_code_to_mv((uint16_t)(adc_sum / (uint64_t)adc_count));
snprintf(buf, sizeof(buf), "V %lu.%lu mV",
(unsigned long)(mv10 / 10U), (unsigned long)(mv10 % 10U));
adc_set_text(2U, buf);
snprintf(buf, sizeof(buf), "MIN %lu MAX %lu",
(unsigned long)mv_min, (unsigned long)mv_max);
adc_set_text(4U, buf);
snprintf(buf, sizeof(buf), "AVG %lu PP %lu",
(unsigned long)mv_avg, (unsigned long)(mv_max - mv_min));
adc_set_text(5U, buf);
}
/* HOLD / RUN:只冻结显示,不停采集 —— 这样点一下 RUN 立刻就能接上最新数据,
不用重新标定/重启 DMA。 */
static void adc_hold_cb(lv_event_t * e)
{
lv_obj_t * btn = (lv_obj_t *)lv_event_get_target(e);
lv_obj_t * lbl = (btn != NULL) ? lv_obj_get_child(btn, 0) : NULL;
adc_hold = (uint8_t)((adc_hold == 0U) ? 1U : 0U);
if (lbl != NULL)
{
lv_label_set_text(lbl, (adc_hold != 0U) ? "HOLD" : "RUN");
}
}
/* CAL:重新做一次偏移标定,10 万周期级,实际不到 1ms,不会卡到界面 */
static void adc_cal_cb(lv_event_t * e)
{
(void)e;
adc_app_recalibrate();
adc_stats_reset();
}
/* CLR:累计统计归零 */
static void adc_clr_cb(lv_event_t * e)
{
(void)e;
adc_stats_reset();
}
static void ui_build_adc(lv_obj_t * scr)
{
/* 6 个标签的配色:通道 / 点数窗口 / 当前值 / 采样率 / MIN,MAX / AVG,PP */
static const uint32_t lbl_hex[ADC_LBL_COUNT] =
{
UI_ACCENT_HEX, UI_MUTED_HEX, UI_TXT_HEX,
UI_OK_HEX, UI_MUTED_HEX, UI_MUTED_HEX
};
lv_obj_t * info;
lv_obj_t * panel;
lv_obj_t * btn;
lv_obj_t * hint;
int32_t field_w = lv_display_get_horizontal_resolution(lv_display_get_default()) - (2 * UI_PAD);
int32_t btn_w = (field_w - (2 * UI_GAP)) / 3;
int32_t i;
(void)ui_make_head(scr, "ADC ADC1_INP10 / PC0", "UART", UI_PAGE_UART);
/* ---------- 采样信息:3 行,每行左一个右一个 ---------- */
info = lv_obj_create(scr);
lv_obj_remove_style_all(info);
lv_obj_set_pos(info, UI_PAD, ADC_INFO_Y);
lv_obj_set_size(info, field_w, ADC_INFO_H);
lv_obj_remove_flag(info, LV_OBJ_FLAG_SCROLLABLE);
for (i = 0; i < (int32_t)ADC_LBL_COUNT; i++)
{
adc_lbl[i] = lv_label_create(info);
lv_obj_set_style_text_font(adc_lbl[i], &lv_font_montserrat_14, 0);
lv_obj_set_style_text_color(adc_lbl[i], lv_color_hex(lbl_hex[i]), 0);
lv_obj_align(adc_lbl[i],
((i & 1) != 0) ? LV_ALIGN_TOP_RIGHT : LV_ALIGN_TOP_LEFT,
0, (i / 2) * ADC_INFO_ROW_H);
adc_cache[i][0] = '\0'; /* 清掉缓存,保证本轮一定写一次 */
}
/* ---------- 波形图 ---------- */
panel = lv_obj_create(scr);
lv_obj_remove_style_all(panel);
lv_obj_set_pos(panel, UI_PAD, ADC_WAVE_Y);
lv_obj_set_size(panel, field_w, ADC_WAVE_H);
lv_obj_set_style_bg_color(panel, lv_color_hex(UI_FIELD_HEX), 0);
lv_obj_set_style_bg_opa(panel, LV_OPA_COVER, 0);
lv_obj_set_style_border_width(panel, 1, 0);
lv_obj_set_style_border_color(panel, lv_color_hex(0x27324A), 0);
lv_obj_set_style_radius(panel, 6, 0);
lv_obj_remove_flag(panel, LV_OBJ_FLAG_SCROLLABLE);
/* 三条静态参考线(顶/中/底):只在建页面时画一次,不参与刷新 */
for (i = 0; i < 3; i++)
{
lv_obj_t * ref = lv_obj_create(panel);
lv_obj_remove_style_all(ref);
lv_obj_set_size(ref, field_w - 10, 1);
lv_obj_set_style_bg_color(ref, lv_color_hex(0x27324A), 0);
lv_obj_set_style_bg_opa(ref, LV_OPA_COVER, 0);
lv_obj_set_pos(ref, 5, (i * (ADC_WAVE_H - 4)) / 2);
lv_obj_remove_flag(ref, LV_OBJ_FLAG_SCROLLABLE);
lv_obj_remove_flag(ref, LV_OBJ_FLAG_CLICKABLE);
}
adc_wave_w = field_w - (2 * ADC_WAVE_PAD);
adc_wave_h = ADC_WAVE_H - (2 * ADC_WAVE_PAD);
adc_wave = lv_line_create(panel);
lv_obj_remove_style_all(adc_wave);
lv_obj_set_pos(adc_wave, ADC_WAVE_PAD, ADC_WAVE_PAD);
lv_obj_set_style_line_width(adc_wave, 2, 0);
lv_obj_set_style_line_color(adc_wave, lv_color_hex(UI_ACCENT_HEX), 0);
lv_obj_set_style_line_opa(adc_wave, LV_OPA_COVER, 0);
/* 先摆一条居中的平线占位:有数据之后马上会被整帧折线顶掉 */
adc_pts[0].x = 0;
adc_pts[0].y = adc_wave_h / 2;
adc_pts[1].x = adc_wave_w - 1;
adc_pts[1].y = adc_wave_h / 2;
lv_line_set_points(adc_wave, adc_pts, 2U);
/* ---------- HOLD / CAL / CLR ---------- */
adc_btn_hold = make_button(scr, "RUN", UI_BTN_HEX, &lv_font_montserrat_14, adc_hold_cb, NULL);
lv_obj_set_pos(adc_btn_hold, UI_PAD, ADC_ROW_Y);
lv_obj_set_size(adc_btn_hold, btn_w, ADC_ROW_H);
btn = make_button(scr, "CAL", UI_BTN_HEX, &lv_font_montserrat_14, adc_cal_cb, NULL);
lv_obj_set_pos(btn, UI_PAD + btn_w + UI_GAP, ADC_ROW_Y);
lv_obj_set_size(btn, btn_w, ADC_ROW_H);
btn = make_button(scr, "CLR", UI_STOP_HEX, &lv_font_montserrat_14, adc_clr_cb, NULL);
lv_obj_set_pos(btn, UI_PAD + (2 * (btn_w + UI_GAP)), ADC_ROW_Y);
lv_obj_set_size(btn, btn_w, ADC_ROW_H);
/* ---------- 底部提示(只能是 ASCII,字库里没有中文字形) ---------- */
hint = lv_label_create(scr);
lv_label_set_text(hint, "PC0 input 0 ~ VREF+ (3.3V max)");
lv_obj_set_style_text_font(hint, &lv_font_montserrat_14, 0);
lv_obj_set_style_text_color(hint, lv_color_hex(UI_MUTED_HEX), 0);
lv_obj_set_pos(hint, UI_PAD + 2, ADC_HINT_Y);
/* ---------- 先立刻刷一次,再挂周期刷新 ---------- */
adc_page_update(NULL);
adc_timer = lv_timer_create(adc_page_update, ADC_REFRESH_MS, NULL);
}
效果展示:

6.屏幕上展示正方体运动
可在屏幕上通过控制键,控制正方体转动
核心部分:
/*********************
* 3D 立方体页布局与参数
*********************/
#define CUBE_VIEW_X 4 /* 视口(立方体就画在这里面) */
#define CUBE_VIEW_Y 26
#define CUBE_VIEW_W 124
#define CUBE_VIEW_H 124
#define CUBE_PAD_X 134 /* 方向键区(3x3,中间那格放 SPD) */
#define CUBE_PAD_Y 26
#define CUBE_PAD_BTN_W 58
#define CUBE_PAD_BTN_H 40
#define CUBE_PAD_GAP 4
#define CUBE_PAD_COL_X(n) (CUBE_PAD_X + ((n) * (CUBE_PAD_BTN_W + CUBE_PAD_GAP)))
#define CUBE_PAD_ROW_Y(n) (CUBE_PAD_Y + ((n) * (CUBE_PAD_BTN_H + CUBE_PAD_GAP)))
#define CUBE_STATUS_Y 158 /* 姿态 / 档位 / 运行状态行 */
#define CUBE_BTN_Y 178 /* SPIN / HOME 一行 */
#define CUBE_BTN_H 34
#define CUBE_BTN_W 154
#define CUBE_HINT_Y 216 /* 底部提示行 */
#define CUBE_TICK_MS 50U /* 按住方向键时的角度推进周期 */
#define CUBE_MAX_DT_MS 300U /* 单次最多推进多少毫秒,掉帧时别一下跳太多 */
#define CUBE_TAP_MS 100.0f /* 轻点一下按多少毫秒的量转,保证点得动 */
#define CUBE_SPD_COUNT 3U /* 转速档位个数 */
#define CUBE_HOME_YAW (-30.0f) /* HOME 复位到的角度 */
#define CUBE_HOME_PITCH 22.0f
#define CUBE_D2R 0.017453292f /* 度 -> 弧度 */
/* 世界单位 -> 像素。立方体顶点到中心的距离是 sqrt(3)=1.732,取 33 时最外角
落在 33*1.732=57 像素上,视口半径有 62,任何角度都不会被裁掉。 */
#define CUBE_SCALE 33.0f
#define CUBE_LX (-0.352f) /* 光照方向(已归一化) */
#define CUBE_LY 0.553f
#define CUBE_LZ 0.754f
#define CUBE_AMBIENT 0.34f /* 环境光占比,剩下的按法线夹角给 */
#define CUBE_DIFFUSE 0.66f
#define CUBE_FACE_HEX 0x2FB6F0 /* 面的基色 */
#define CUBE_EDGE_HEX 0xE8F8FF /* 棱的颜色 */
#define CUBE_BIT_UP 0x01U /* cube_keys 的位 */
#define CUBE_BIT_DOWN 0x02U
#define CUBE_BIT_LEFT 0x04U
#define CUBE_BIT_RIGHT 0x08U
/* ---------------- 3D 立方体页(屏幕方向键控制旋转) ----------------
*
* 本机没有 GPU,所以是纯软件 3D,但只做最少的事:
* 1) 8 个顶点按 YAW(绕 Y) -> PITCH(绕 X) 旋转,正交投影到屏幕;
* 2) 6 个面各拿"旋转后的外法线"判断朝向,nz <= 0 就是背面,直接不画
* —— 立方体是凸的,看得见的一定是正对着观察者的那 3 个面,
* 既省掉了一半三角形,也省掉了排序;
* 3) 每个可见面按法线和光线的夹角算明暗(面画成两个三角形填色),
* 再把 4 条棱描一遍,棱角清楚。
*
* 填色用的是 lv_draw_triangle(),它跑在 LVGL 自己的绘制管线里、跟着局部
* 刷新走,所以不需要自己开帧缓冲,也不用管刷屏的字节序。
*
* 瓶颈还是刷屏:124x124 一次要 ~28ms(SPI5 8.6MHz)。所以——
* - 角度按真实时间推进(掉帧不会忽快忽慢),但只在按键按着 / 自动旋转时
* 才干活,松手后一帧都不刷;
* - 每帧只让"这次和上次的投影包围盒"失效,正对时只有 66x66,
* 平均能省一半刷屏量。
*/
/* 立方体的 8 个顶点,边长 2,中心在原点 */
static const float cube_v[8][3] =
{
{-1.0f, -1.0f, -1.0f}, { 1.0f, -1.0f, -1.0f}, { 1.0f, 1.0f, -1.0f}, {-1.0f, 1.0f, -1.0f},
{-1.0f, -1.0f, 1.0f}, { 1.0f, -1.0f, 1.0f}, { 1.0f, 1.0f, 1.0f}, {-1.0f, 1.0f, 1.0f}
};
/* 6 个面:4 个顶点按"绕这个面走一圈"的顺序排,再配一条外法线 */
static const uint8_t cube_face_v[6][4] =
{
{4U, 5U, 6U, 7U}, /* +Z 前 */
{1U, 0U, 3U, 2U}, /* -Z 后 */
{5U, 1U, 2U, 6U}, /* +X 右 */
{0U, 4U, 7U, 3U}, /* -X 左 */
{3U, 2U, 6U, 7U}, /* +Y 上 */
{0U, 1U, 5U, 4U} /* -Y 下 */
};
static const float cube_face_n[6][3] =
{
{ 0.0f, 0.0f, 1.0f},
{ 0.0f, 0.0f, -1.0f},
{ 1.0f, 0.0f, 0.0f},
{-1.0f, 0.0f, 0.0f},
{ 0.0f, 1.0f, 0.0f},
{ 0.0f, -1.0f, 0.0f}
};
/* 旋转一个点:先绕 Y 转 yaw,再绕 X 转 pitch。
顶点和法线共用这一份,保证两者永远是一致的。 */
static void cube_rotate(const float in[3], float cos_yaw, float sin_yaw,
float cos_pit, float sin_pit, float out[3])
{
float xr = (in[0] * cos_yaw) + (in[2] * sin_yaw);
float zr = (in[2] * cos_yaw) - (in[0] * sin_yaw);
out[0] = xr;
out[1] = (in[1] * cos_pit) - (zr * sin_pit);
out[2] = (in[1] * sin_pit) + (zr * cos_pit);
}
/* 明暗系数 -> 0xRRGGBB:面的基色整体乘一遍,越亮越接近基色 */
static uint32_t cube_face_color(float k)
{
uint32_t r = (uint32_t)((float)((CUBE_FACE_HEX >> 16) & 0xFFU) * k);
uint32_t g = (uint32_t)((float)((CUBE_FACE_HEX >> 8) & 0xFFU) * k);
uint32_t b = (uint32_t)((float)(CUBE_FACE_HEX & 0xFFU) * k);
if (r > 255U) { r = 255U; }
if (g > 255U) { g = 255U; }
if (b > 255U) { b = 255U; }
return (r << 16) | (g << 8) | b;
}
/* 把 8 个顶点按当前视角投影成屏幕坐标(画图和算包围盒共用这一份) */
static void cube_project(lv_point_precise_t p[8])
{
lv_area_t coords;
float cos_yaw, sin_yaw, cos_pit, sin_pit;
float v[3];
int32_t cx, cy;
uint8_t i;
lv_obj_get_coords(cube_view, &coords);
cx = (coords.x1 + coords.x2) / 2;
cy = (coords.y1 + coords.y2) / 2;
cos_yaw = cosf(cube_yaw * CUBE_D2R);
sin_yaw = sinf(cube_yaw * CUBE_D2R);
cos_pit = cosf(cube_pitch * CUBE_D2R);
sin_pit = sinf(cube_pitch * CUBE_D2R);
for (i = 0U; i < 8U; i++)
{
cube_rotate(cube_v[i], cos_yaw, sin_yaw, cos_pit, sin_pit, v);
/* 屏幕 y 向下,所以取负 */
p[i].x = cx + (int32_t)(v[0] * CUBE_SCALE);
p[i].y = cy - (int32_t)(v[1] * CUBE_SCALE);
}
}
/* 投影后的包围盒,外扩 2 像素给 1 像素宽棱线和取整留余量 */
static void cube_bounds(lv_area_t * area)
{
lv_point_precise_t p[8];
int32_t x1 = 100000, y1 = 100000, x2 = -100000, y2 = -100000;
uint8_t i;
cube_project(p);
for (i = 0U; i < 8U; i++)
{
if (p[i].x < x1) { x1 = p[i].x; }
if (p[i].y < y1) { y1 = p[i].y; }
if (p[i].x > x2) { x2 = p[i].x; }
if (p[i].y > y2) { y2 = p[i].y; }
}
area->x1 = x1 - 2;
area->y1 = y1 - 2;
area->x2 = x2 + 2;
area->y2 = y2 + 2;
}
/* 让"这次 + 上次"的包围盒失效。两帧取并集是必须的:
不加上次的范围,上一帧露在新范围外面的那部分会留下残影。 */
static void cube_invalidate(void)
{
lv_area_t now;
lv_area_t both;
if (cube_view == NULL)
{
return;
}
cube_bounds(&now);
if ((cube_prev_area.x2 < cube_prev_area.x1) || (cube_prev_area.y2 < cube_prev_area.y1))
{
lv_obj_invalidate(cube_view); /* 还没有"上次",整块重画一次 */
}
else
{
both.x1 = LV_MIN(cube_prev_area.x1, now.x1);
both.y1 = LV_MIN(cube_prev_area.y1, now.y1);
both.x2 = LV_MAX(cube_prev_area.x2, now.x2);
both.y2 = LV_MAX(cube_prev_area.y2, now.y2);
lv_obj_invalidate_area(cube_view, &both);
}
cube_prev_area = now;
}
/* 视口的 DRAW_MAIN:LVGL 先把底色/边框画完才会调到这里,
所以三角形和棱直接往上画,就是压在背景之上的。 */
static void cube_draw_cb(lv_event_t * e)
{
lv_layer_t * layer = lv_event_get_layer(e);
lv_point_precise_t p[8];
lv_draw_triangle_dsc_t tri;
lv_draw_line_dsc_t line;
float cos_yaw, sin_yaw, cos_pit, sin_pit;
float v[3];
float lit;
uint8_t i, f;
if ((cube_view == NULL) || (layer == NULL))
{
return;
}
cos_yaw = cosf(cube_yaw * CUBE_D2R);
sin_yaw = sinf(cube_yaw * CUBE_D2R);
cos_pit = cosf(cube_pitch * CUBE_D2R);
sin_pit = sinf(cube_pitch * CUBE_D2R);
cube_project(p);
lv_draw_triangle_dsc_init(&tri);
tri.opa = LV_OPA_COVER;
lv_draw_line_dsc_init(&line);
line.color = lv_color_hex(CUBE_EDGE_HEX);
line.width = 1;
line.opa = LV_OPA_COVER;
for (f = 0U; f < 6U; f++)
{
const uint8_t * fv = cube_face_v[f];
/* 拿法线判断这个面朝不朝着观察者 */
cube_rotate(cube_face_n[f], cos_yaw, sin_yaw, cos_pit, sin_pit, v);
if (v[2] <= 0.02f)
{
continue; /* 背面:看不见,不画 */
}
/* 法线和光线夹角越小越亮,面与面之间就有了立体感 */
lit = (v[0] * CUBE_LX) + (v[1] * CUBE_LY) + (v[2] * CUBE_LZ);
if (lit < 0.0f)
{
lit = 0.0f;
}
tri.color = lv_color_hex(cube_face_color(CUBE_AMBIENT + (CUBE_DIFFUSE * lit)));
/* 投影后的面一定是平行四边形(凸的),拆两个三角形填掉 */
tri.p[0] = p[fv[0]];
tri.p[1] = p[fv[1]];
tri.p[2] = p[fv[2]];
lv_draw_triangle(layer, &tri);
tri.p[0] = p[fv[0]];
tri.p[1] = p[fv[2]];
tri.p[2] = p[fv[3]];
lv_draw_triangle(layer, &tri);
/* 四条棱描一遍。这里刻意用 p1/p2 而不是 points 数组:
只有 p1/p2 的线段不会去 lv_malloc,逐帧画也没有堆开销。 */
for (i = 0U; i < 4U; i++)
{
line.p1 = p[fv[i]];
line.p2 = p[fv[(i + 1U) & 3U]];
lv_draw_line(layer, &line);
}
}
}
/* 角度变了:重画立方体 + 刷状态行 */
static void cube_update(void)
{
char buf[48];
if (cube_status != NULL)
{
snprintf(buf, sizeof(buf), "YAW %d PITCH %d SPD %d %s",
(int)cube_yaw, (int)cube_pitch, (int)cube_spd_tab[cube_spd],
(cube_spin != 0U) ? "SPIN" : ((cube_keys != 0U) ? "TURN" : "IDLE"));
lv_label_set_text(cube_status, buf);
}
cube_invalidate();
}
/* 按当前档位把角度推进 dt 毫秒对应的量(按住连转和自动旋转共用) */
static void cube_advance(float dt_ms)
{
float step = (float)cube_spd_tab[cube_spd] * dt_ms / 1000.0f;
/* 上/下让顶面、底面转过来;左/右绕着竖轴转 */
if ((cube_keys & CUBE_BIT_UP) != 0U) { cube_pitch += step; }
if ((cube_keys & CUBE_BIT_DOWN) != 0U) { cube_pitch -= step; }
if ((cube_keys & CUBE_BIT_LEFT) != 0U) { cube_yaw += step; }
if ((cube_keys & CUBE_BIT_RIGHT) != 0U) { cube_yaw -= step; }
/* 自动旋转:两个轴都给一点,看起来更像"翻滚"而不是原地打转 */
if (cube_spin != 0U)
{
cube_yaw += (step * 0.55f);
cube_pitch += (step * 0.30f);
}
/* 角度收在 -180..180:连转很久也不会因为数值变大而丢精度 */
while (cube_yaw > 180.0f) { cube_yaw -= 360.0f; }
while (cube_yaw < -180.0f) { cube_yaw += 360.0f; }
while (cube_pitch > 180.0f) { cube_pitch -= 360.0f; }
while (cube_pitch < -180.0f) { cube_pitch += 360.0f; }
cube_update();
}
/* 方向键:按下开始转(同时立刻走一小步,轻点一下也有反应),松开就停 */
static void cube_key_cb(lv_event_t * e)
{
uint8_t bit = (uint8_t)(uintptr_t)lv_event_get_user_data(e);
if (lv_event_get_code(e) == LV_EVENT_PRESSED)
{
cube_keys |= bit;
cube_tick = lv_tick_get();
cube_advance(CUBE_TAP_MS);
}
else
{
/* RELEASED / PRESS_LOST(手指滑出按键也会走这里) */
cube_keys &= (uint8_t)(~bit);
cube_update();
}
}
/* 方向键挂三个事件:按下、松开、按下后滑出去 */
static void cube_bind_key(lv_obj_t * btn, uint8_t bit)
{
lv_obj_add_event_cb(btn, cube_key_cb, LV_EVENT_PRESSED, (void *)(uintptr_t)bit);
lv_obj_add_event_cb(btn, cube_key_cb, LV_EVENT_RELEASED, (void *)(uintptr_t)bit);
lv_obj_add_event_cb(btn, cube_key_cb, LV_EVENT_PRESS_LOST, (void *)(uintptr_t)bit);
}
/* 周期推进:没在转就直接返回,一帧都不刷 */
static void cube_tick_cb(lv_timer_t * timer)
{
uint32_t dt;
(void)timer;
if ((cube_view == NULL) || ((cube_keys == 0U) && (cube_spin == 0U)))
{
return;
}
dt = lv_tick_elaps(cube_tick); /* 按真实间隔推进,掉帧也不忽快忽慢 */
cube_tick = lv_tick_get();
if (dt > CUBE_MAX_DT_MS)
{
dt = CUBE_MAX_DT_MS; /* 卡了一下也别一次跳一大截 */
}
cube_advance((float)dt);
}
/* SPD:循环 30 / 60 / 120 度每秒 */
static void cube_spd_cb(lv_event_t * e)
{
char buf[16];
(void)e;
cube_spd = (uint8_t)((cube_spd + 1U) % CUBE_SPD_COUNT);
if (cube_btn_spd != NULL)
{
lv_obj_t * lbl = lv_obj_get_child(cube_btn_spd, 0);
if (lbl != NULL)
{
snprintf(buf, sizeof(buf), "SPD %d", cube_spd_tab[cube_spd]);
lv_label_set_text(lbl, buf);
}
}
cube_tick = lv_tick_get();
cube_update();
}
/* SPIN:开着的时候两个轴一起慢慢转,当展示用 */
static void cube_spin_cb(lv_event_t * e)
{
lv_obj_t * btn = (lv_obj_t *)lv_event_get_target(e);
lv_obj_t * lbl = (btn != NULL) ? lv_obj_get_child(btn, 0) : NULL;
cube_spin = (uint8_t)((cube_spin == 0U) ? 1U : 0U);
if (lbl != NULL)
{
lv_label_set_text(lbl, (cube_spin != 0U) ? "STOP" : "SPIN");
}
cube_tick = lv_tick_get();
cube_update();
}
/* HOME:回到一开始那个"能同时看到三个面"的角度 */
static void cube_home_cb(lv_event_t * e)
{
(void)e;
cube_yaw = CUBE_HOME_YAW;
cube_pitch = CUBE_HOME_PITCH
cube_update();
}
效果展示:

7.添加sd+fatfs系统,并在屏幕显示sd文件目录情况,移植fc模拟器,屏幕上添加虚拟按键。
(注意:游戏运行时注意nes文件大小,超过256kb的游戏无法运行。(原因硬件资源受限导致的))。
核心部分:内容较多仅介绍ui和主要文件结构,详细内容查阅源码。
#define SD_BR_ROOT "0:/" /* 起始目录:卡根 */
#define SD_BR_ROWS 8U /* 一屏显示几行:240 高塞不下更多了 */
#define SD_BR_ROW_H 17
#define SD_BR_LIST_Y 42 /* 首行 y */
#define SD_BR_PATH_Y 26 /* 当前路径行 y */
#define SD_BR_STAT_Y 181 /* 统计/结果行 y */
#define SD_BR_BTN_Y 198
#define SD_BR_BTN_H 24
#define SD_BR_BTN_W 75 /* 4 个按钮刚好铺满 312 宽 */
#define SD_BR_HINT_Y 226
#define SD_BR_TICK_MS 200U
#define SD_BR_TXT_MAX 80U
#define SD_BR_STAT_MAX 56U
#define SD_BR_REQ_NONE 0U
#define SD_BR_REQ_RELIST 1U /* 需要(挂载后)重新列当前目录 */
#define SD_BR_REQ_LAUNCH 2U /* 加载并启动选中的 .nes */
static sd_dir_entry_t sd_br_ent[SD_DIR_MAX_ENTRIES];
static lv_obj_t * sd_br_row[SD_BR_ROWS];
static int32_t sd_br_row_map[SD_BR_ROWS]; /* 行 -> sd_br_ent 下标;-1 = 空行 */
static lv_obj_t * sd_br_path = NULL;
static lv_obj_t * sd_br_stat = NULL;
static lv_obj_t * sd_br_hint = NULL;
static lv_timer_t * sd_br_timer = NULL;
static char sd_br_path_str[SD_DIR_PATH_MAX];
static char sd_br_launch[SD_DIR_PATH_MAX];
static char sd_br_stat_str[SD_BR_STAT_MAX];
static uint32_t sd_br_count; /* 这次列目录实际拿到几条 */
static uint32_t sd_br_total; /* 目录里一共有几条(可能多于 count) */
static uint32_t sd_br_page;
static uint8_t sd_br_req = SD_BR_REQ_NONE;
static uint8_t sd_br_mounted;
static void sd_br_refresh(void);
/* 状态行:走一句提示,顺带定颜色(绿=好、红=坏、蓝=进行中、灰=中性) */
static void sd_br_set_stat(const char * txt, uint32_t hex)
{
if (sd_br_stat == NULL)
{
return;
}
(void)snprintf(sd_br_stat_str, sizeof(sd_br_stat_str), "%s", txt);
lv_label_set_text(sd_br_stat, sd_br_stat_str);
lv_obj_set_style_text_color(sd_br_stat, lv_color_hex(hex), 0);
}
#if 0 /* ---- 旧的自检页代码,保留备查(不参与编译)----
* 上电自检已经取消,这一页换成了下面的目录浏览器。这段代码当初是用来
* 定位"能读不能写"的(见本文件 890 行附近的说明),留着是为了哪天要
* 回头对参数时能直接照着看,不影响编译产物。 */
/* 把上一次自检结果铺到 6 行上 */
static void sd_show_result(void)
{
const sd_test_result_t * r = sd_last_result();
char buf[48];
/* 底部两行临时让给诊断信息(定位"能读不能写"用,问题解决后还原)。
原来这里放的是 "write 128KB to 0:/SDTEST.BIN" / "then read back and compare"。
diag1 = 绕开 FatFs 的原始块读写探针结果;
diag2 = 探针里抓到的 HAL 错误码/状态,或者 FatFs 写失败瞬间的同款信息。 */
if (sd_hint1 != NULL)
{
lv_label_set_text(sd_hint1, (r->diag1[0] != '\0') ? r->diag1
: "write 128KB to 0:/SDTEST.BIN");
}
if (sd_hint2 != NULL)
{
lv_label_set_text(sd_hint2, (r->diag2[0] != '\0') ? r->diag2
: "then read back and compare");
}
if (r->mount_ok == 0U)
{
(void)snprintf(buf, sizeof(buf), "MOUNT FAIL: %s", sd_fresult_str(r->fresult));
sd_row_set(0U, buf);
sd_row_set(1U, "no card / not FAT?");
sd_row_set(2U, "-");
sd_row_set(3U, "-");
sd_row_set(4U, "-");
sd_row_set(5U, "RESULT: FAIL");
sd_row_set_color(5U, UI_STOP_HEX);
return;
}
sd_row_set(0U, "MOUNT OK");
(void)snprintf(buf, sizeof(buf), "%s CAP %luMB",
sd_card_type_str(r->card_type), (unsigned long)r->card_mb);
sd_row_set(1U, buf);
if (r->write_ok != 0U)
{
(void)snprintf(buf, sizeof(buf), "WR %luKB OK %lukB/s",
(unsigned long)(r->test_bytes / 1024U), (unsigned long)r->wr_kbps);
}
else
{
(void)snprintf(buf, sizeof(buf), "WR FAIL: %s", sd_fresult_str(r->fresult));
}
sd_row_set(2U, buf);
if (r->verify_ok != 0U)
{
(void)snprintf(buf, sizeof(buf), "RD/VERIFY OK %lukB/s", (unsigned long)r->rd_kbps);
}
else if (r->mismatch_at != 0xFFFFFFFFU)
{
(void)snprintf(buf, sizeof(buf), "MISMATCH @ %lu", (unsigned long)r->mismatch_at);
}
else
{
(void)snprintf(buf, sizeof(buf), "RD FAIL: %s", sd_fresult_str(r->fresult));
}
sd_row_set(3U, buf);
(void)snprintf(buf, sizeof(buf), "USED %lu%% FREE %luMB",
(unsigned long)r->used_pct, (unsigned long)r->free_mb);
sd_row_set(4U, buf);
sd_row_set(5U, (r->verify_ok != 0U) ? "RESULT: PASS" : "RESULT: FAIL");
sd_row_set_color(5U, (r->verify_ok != 0U) ? UI_OK_HEX : UI_STOP_HEX);
}
static void sd_btn_retest_cb(lv_event_t * e)
{
(void)e;
/* 只置标志,真正的读写放到定时器里跑,让 RUNNING 先画出来 */
sd_row_set(0U, "RUNNING ...");
sd_row_set(1U, "-");
sd_row_set(2U, "-");
sd_row_set(3U, "-");
sd_row_set(4U, "-");
sd_row_set(5U, "RESULT: ---");
sd_row_set_color(5U, UI_ACCENT_HEX);
sd_req = SD_REQ_TEST;
}
static void sd_btn_eject_cb(lv_event_t * e)
{
(void)e;
sd_req = SD_REQ_EJECT;
}
static void sd_tick_cb(lv_timer_t * t)
{
uint8_t req = sd_req;
(void)t;
if (req == SD_REQ_NONE)
{
return;
}
sd_req = SD_REQ_NONE;
if (req == SD_REQ_EJECT)
{
sd_unmount();
sd_row_set(0U, "UNMOUNTED");
sd_row_set(1U, "-");
sd_row_set(2U, "-");
sd_row_set(3U, "-");
sd_row_set(4U, "-");
sd_row_set(5U, "press RE-TEST");
sd_row_set_color(5U, UI_MUTED_HEX);
return;
}
(void)sd_selftest();
sd_show_result();
}
#endif /* ==================== 旧的自检页代码到此为止 ==================== */
/* ==================== SD 页:目录浏览器 ==================== */
/* 按 sd_br_count / sd_br_page 把 8 行和状态行刷一遍。
* 只碰内存里的数组,不读卡,所以翻页和返回上级之后的刷新都是瞬时的。 */
static void sd_br_refresh(void)
{
uint32_t i;
uint32_t pages;
uint32_t page;
uint32_t first;
char buf[SD_BR_TXT_MAX];
if (sd_br_path != NULL)
{
lv_label_set_text(sd_br_path, sd_br_path_str);
}
pages = (sd_br_count == 0U) ? 1U : ((sd_br_count + SD_BR_ROWS - 1U) / SD_BR_ROWS);
page = (sd_br_page < pages) ? sd_br_page : (pages - 1U);
sd_br_page = page;
first = page * SD_BR_ROWS;
for (i = 0U; i < SD_BR_ROWS; i++)
{
uint32_t idx = first + i;
lv_obj_t * lbl;
uint32_t hex = UI_MUTED_HEX;
int32_t map = -1;
if (sd_br_row[i] == NULL)
{
continue;
}
if (idx < sd_br_count)
{
map = (int32_t)idx;
if (sd_br_ent[idx].is_dir != 0U)
{
/* 目录名后面补个 '/',一眼能看出是可以点进去的 */
(void)snprintf(buf, sizeof(buf), "%s/", sd_br_ent[idx].name);
hex = UI_ACCENT_HEX;
}
else
{
(void)snprintf(buf, sizeof(buf), "%s", sd_br_ent[idx].name);
hex = (sd_br_ent[idx].is_nes != 0U) ? UI_OK_HEX : UI_MUTED_HEX;
}
}
else
{
buf[0] = '\0';
}
sd_br_row_map[i] = map;
lbl = lv_obj_get_child(sd_br_row[i], 0);
if (lbl != NULL)
{
lv_label_set_text(lbl, buf);
lv_obj_set_style_text_color(lbl, lv_color_hex(hex), 0);
}
if (map >= 0)
{
lv_obj_remove_flag(sd_br_row[i], LV_OBJ_FLAG_HIDDEN);
}
else
{
lv_obj_add_flag(sd_br_row[i], LV_OBJ_FLAG_HIDDEN); /* 空行藏掉,也点不动 */
}
}
/* 状态行:异常优先,正常时显示"第几条到第几条 / 共几条 + 页码" */
if (sd_br_mounted == 0U)
{
sd_br_set_stat("NO CARD - press MOUNT", UI_STOP_HEX);
}
else if (sd_br_count == 0U)
{
sd_br_set_stat("empty dir", UI_MUTED_HEX);
}
else
{
uint32_t lo = first + 1U;
uint32_t hi = first + SD_BR_ROWS;
if (hi > sd_br_count)
{
hi = sd_br_count;
}
if (sd_br_total > sd_br_count)
{
/* 条目太多,只装下了前 SD_DIR_MAX_ENTRIES 条 —— 得说清楚,
否则用户会以为后面的文件不见了 */
(void)snprintf(sd_br_stat_str, sizeof(sd_br_stat_str),
"%lu-%lu of %lu (first %lu only)",
(unsigned long)lo, (unsigned long)hi,
(unsigned long)sd_br_total, (unsigned long)sd_br_count);
}
else
{
(void)snprintf(sd_br_stat_str, sizeof(sd_br_stat_str),
"%lu-%lu of %lu page %lu/%lu",
(unsigned long)lo, (unsigned long)hi,
(unsigned long)sd_br_total,
(unsigned long)(page + 1U), (unsigned long)pages);
}
lv_label_set_text(sd_br_stat, sd_br_stat_str);
lv_obj_set_style_text_color(sd_br_stat, lv_color_hex(UI_MUTED_HEX), 0);
}
}
/* 真正去读卡:没挂载就先挂载,然后列当前目录。阻塞几百毫秒,只在定时器里调。 */
static void sd_br_do_list(void)
{
sd_dir_list_result_t res;
char buf[SD_BR_STAT_MAX];
if (sd_br_mounted == 0U)
{
sd_br_mounted = (sd_ensure_mounted() == (uint8_t)FR_OK) ? 1U : 0U;
}
if (sd_br_mounted == 0U)
{
sd_br_count = 0U;
sd_br_total = 0U;
sd_br_refresh(); /* 这里会把状态行改成 NO CARD */
return;
}
sd_dir_list(sd_br_path_str, sd_br_ent, SD_DIR_MAX_ENTRIES, &res);
if (res.fresult != (uint8_t)FR_OK)
{
sd_br_count = 0U;
sd_br_total = 0U;
(void)snprintf(buf, sizeof(buf), "LIST FAIL: %s", sd_fresult_str(res.fresult));
sd_br_refresh();
sd_br_set_stat(buf, UI_STOP_HEX);
return;
}
sd_br_count = res.count;
sd_br_total = res.total;
/* 目录可能变小了(比如刚从深层退回来),把页码夹回有效范围 */
if ((sd_br_count == 0U) || ((sd_br_page * SD_BR_ROWS) >= sd_br_count))
{
sd_br_page = 0U;
}
sd_br_refresh();
}
/* 加载并启动选中的 .nes。nes_game_run() 会一直阻塞到游戏退出为止。 */
static void sd_br_do_launch(void)
{
nes_game_status_t st;
const nes_rom_info_t * info;
char buf[SD_BR_STAT_MAX];
sd_br_set_stat("LOADING ...", UI_ACCENT_HEX);
st = nes_game_run(sd_br_launch);
if (st == NES_GAME_CORE_MISSING)
{
/* ROM 已经读进内存了,只是还没接模拟器核心 —— 把卡带信息摆出来,
这本身就说明"读卡 + 解头 + 载入内存"这条路是通的。 */
info = nes_game_info();
if (info != NULL)
{
(void)snprintf(buf, sizeof(buf), "ROM ok M%u %uK/%uK %lums core absent",
(unsigned)info->mapper,
(unsigned)info->prg_kb,
(unsigned)info->chr_kb,
(unsigned long)nes_game_last_ms());
}
else
{
(void)snprintf(buf, sizeof(buf), "ROM ok, core absent");
}
sd_br_set_stat(buf, UI_ACCENT_HEX);
}
else if (st == NES_GAME_OK)
{
sd_br_set_stat("game exited", UI_OK_HEX);
}
else
{
(void)snprintf(buf, sizeof(buf), "FAIL: %s", nes_game_status_text(st));
sd_br_set_stat(buf, UI_STOP_HEX);
}
/* 游戏期间屏幕被 NES 输出接管过,回来让 LVGL 整屏重画一遍;
页面对象都还在(没被 lv_obj_clean),不用重建页面。
这里必须用 lv_refr_now() 立刻刷完,不能只标记 dirty 等定时器:
游戏跑了多久、lv_tick 就往前跳了多久,刷新定时器的周期判断这时
已经不可靠,干等下去屏幕会一直停在游戏最后一帧。 */
lv_obj_invalidate(lv_screen_active());
lv_refr_now(lv_display_get_default());
}
/* 点某一行:目录就进去,.nes 就启动,其它文件只提示一句 */
static void sd_br_row_cb(lv_event_t * e)
{
uint8_t row = (uint8_t)(intptr_t)lv_event_get_user_data(e);
int32_t idx;
char tmp[SD_DIR_PATH_MAX];
if (row >= SD_BR_ROWS)
{
return;
}
idx = sd_br_row_map[row];
if (idx < 0)
{
return; /* 空行 */
}
if (sd_path_join(sd_br_path_str, sd_br_ent[idx].name, tmp, sizeof(tmp)) == 0U)
{
sd_br_set_stat("path too long", UI_STOP_HEX);
return;
}
if (sd_br_ent[idx].is_dir != 0U)
{
(void)snprintf(sd_br_path_str, sizeof(sd_br_path_str), "%s", tmp);
sd_br_page = 0U;
sd_br_req = SD_BR_REQ_RELIST;
sd_br_set_stat("READING ...", UI_ACCENT_HEX);
return;
}
if (sd_br_ent[idx].is_nes != 0U)
{
(void)snprintf(sd_br_launch, sizeof(sd_br_launch), "%s", tmp);
sd_br_req = SD_BR_REQ_LAUNCH;
return;
}
sd_br_set_stat("not a .nes file", UI_MUTED_HEX);
}
/* 返回上级:根目录(0:/)的上级还是它自己,点了没反应也不会出错 */
static void sd_br_btn_up_cb(lv_event_t * e)
{
char tmp[SD_DIR_PATH_MAX];
(void)e;
if (sd_path_parent(sd_br_path_str, tmp, sizeof(tmp)) == 0U)
{
return;
}
if (strcmp(tmp, sd_br_path_str) == 0)
{
return; /* 已经在根了 */
}
(void)snprintf(sd_br_path_str, sizeof(sd_br_path_str), "%s", tmp);
sd_br_page = 0U;
sd_br_req = SD_BR_REQ_RELIST;
sd_br_set_stat("READING ...", UI_ACCENT_HEX);
}
static void sd_br_btn_prev_cb(lv_event_t * e)
{
(void)e;
if (sd_br_page > 0U)
{
sd_br_page--;
sd_br_refresh(); /* 数据都在内存里,翻页不用重新读卡 */
}
}
static void sd_br_btn_next_cb(lv_event_t * e)
{
(void)e;
if (((sd_br_page + 1U) * SD_BR_ROWS) < sd_br_count)
{
sd_br_page++;
sd_br_refresh();
}
}
static void sd_br_btn_mount_cb(lv_event_t * e)
{
(void)e;
/* 先真的卸载一次再挂:换过卡之后只有这样才会重新走一遍识别, */
sd_unmount();
sd_br_mounted = 0U;
sd_br_req = SD_BR_REQ_RELIST;
sd_br_set_stat("MOUNTING ...", UI_ACCENT_HEX);
}
/* 定时器:所有会阻塞界面的活儿都在这里干(挂载、读目录、跑游戏) */
static void sd_br_tick_cb(lv_timer_t * t)
{
uint8_t req = sd_br_req;
(void)t;
if (req == SD_BR_REQ_NONE)
{
return;
}
sd_br_req = SD_BR_REQ_NONE;
if (req == SD_BR_REQ_LAUNCH)
{
sd_br_do_launch();
}
else
{
sd_br_do_list();
}
}
/* 建一行:左对齐文字、按下变亮;行号塞进 user_data,点击时再换算成条目下标 */
static lv_obj_t * sd_br_make_row(lv_obj_t * scr, uint8_t row_idx, int32_t y, int32_t w)
{
lv_obj_t * row = lv_button_create(scr);
lv_obj_t * lbl;
lv_color_t bg = lv_color_hex(UI_FIELD_HEX);
lv_obj_remove_style_all(row);
lv_obj_set_pos(row, UI_PAD, y);
lv_obj_set_size(row, w, SD_BR_ROW_H);
lv_obj_set_style_bg_color(row, bg, 0);
lv_obj_set_style_bg_opa(row, LV_OPA_COVER, 0);
lv_obj_set_style_radius(row, 4, 0);
lv_obj_set_style_bg_color(row, lv_color_lighten(bg, 40), LV_STATE_PRESSED);
lv_obj_remove_flag(row, LV_OBJ_FLAG_SCROLLABLE);
lv_obj_add_event_cb(row, sd_br_row_cb, LV_EVENT_CLICKED, (void *)(intptr_t)row_idx);
lbl = lv_label_create(row);
lv_label_set_long_mode(lbl, LV_LABEL_LONG_CLIP); /* 名字太长就截断,不换行 */
lv_obj_set_width(lbl, w - 12);
lv_obj_set_style_text_font(lbl, &lv_font_montserrat_14, 0);
lv_obj_set_style_text_color(lbl, lv_color_hex(UI_MUTED_HEX), 0);
lv_obj_align(lbl, LV_ALIGN_LEFT_MID, 6, 0);
lv_label_set_text(lbl, "");
return row;
}
static void ui_build_sd(lv_obj_t * scr)
{
lv_display_t * disp = lv_display_get_default();
int32_t scr_w = lv_display_get_horizontal_resolution(disp);
int32_t field_w = scr_w - (2 * UI_PAD);
lv_obj_t * btn;
uint8_t i;
(void)ui_make_head(scr, "SD FILES", "PWM", UI_PAGE_PWM);
/* ---------- 当前路径 ---------- */
if (sd_br_path_str[0] == '\0')
{
(void)snprintf(sd_br_path_str, sizeof(sd_br_path_str), "%s", SD_BR_ROOT);
}
sd_br_path = lv_label_create(scr);
lv_label_set_long_mode(sd_br_path, LV_LABEL_LONG_CLIP);
lv_obj_set_width(sd_br_path, field_w);
lv_obj_set_style_text_font(sd_br_path, &lv_font_montserrat_14, 0);
lv_obj_set_style_text_color(sd_br_path, lv_color_hex(UI_ACCENT_HEX), 0);
lv_obj_set_pos(sd_br_path, UI_PAD + 2, SD_BR_PATH_Y);
lv_label_set_text(sd_br_path, sd_br_path_str);
/* ---------- 列表(8 行)---------- */
for (i = 0U; i < SD_BR_ROWS; i++)
{
sd_br_row[i] = sd_br_make_row(scr, i,
SD_BR_LIST_Y + ((int32_t)i * SD_BR_ROW_H),
field_w);
sd_br_row_map[i] = -1;
}
/* ---------- 统计 / 结果行 ---------- */
sd_br_stat = lv_label_create(scr);
lv_label_set_long_mode(sd_br_stat, LV_LABEL_LONG_CLIP);
lv_obj_set_width(sd_br_stat, field_w);
lv_obj_set_style_text_font(sd_br_stat, &lv_font_montserrat_14, 0);
lv_obj_set_style_text_color(sd_br_stat, lv_color_hex(UI_MUTED_HEX), 0);
lv_obj_set_pos(sd_br_stat, UI_PAD + 2, SD_BR_STAT_Y);
lv_label_set_text(sd_br_stat, "");
/* ---------- 按钮:返回上级 / 上一页 / 下一页 / 重新挂载 ---------- */
btn = make_button(scr, "UP", UI_BTN_HEX, &lv_font_montserrat_14,
sd_br_btn_up_cb, NULL);
lv_obj_set_pos(btn, UI_PAD, SD_BR_BTN_Y);
lv_obj_set_size(btn, SD_BR_BTN_W, SD_BR_BTN_H);
btn = make_button(scr, "PREV", UI_BTN_HEX, &lv_font_montserrat_14,
sd_br_btn_prev_cb, NULL);
lv_obj_set_pos(btn, UI_PAD + (SD_BR_BTN_W + UI_GAP), SD_BR_BTN_Y);
lv_obj_set_size(btn, SD_BR_BTN_W, SD_BR_BTN_H);
btn = make_button(scr, "NEXT", UI_BTN_HEX, &lv_font_montserrat_14,
sd_br_btn_next_cb, NULL);
lv_obj_set_pos(btn, UI_PAD + (2 * (SD_BR_BTN_W + UI_GAP)), SD_BR_BTN_Y);
lv_obj_set_size(btn, SD_BR_BTN_W, SD_BR_BTN_H);
/* 换过卡要重新识别,得先真的卸载再挂载,所以这个键先调 sd_unmount() */
btn = make_button(scr, "MOUNT", UI_OK_HEX, &lv_font_montserrat_14,
sd_br_btn_mount_cb, NULL);
lv_obj_set_pos(btn, UI_PAD + (3 * (SD_BR_BTN_W + UI_GAP)), SD_BR_BTN_Y);
lv_obj_set_size(btn, SD_BR_BTN_W, SD_BR_BTN_H);
/* ---------- 操作提示 ---------- */
sd_br_hint = lv_label_create(scr);
lv_label_set_text(sd_br_hint, "tap dir to enter, .nes to play");
lv_obj_set_style_text_font(sd_br_hint, &lv_font_montserrat_14, 0);
lv_obj_set_style_text_color(sd_br_hint, lv_color_hex(UI_MUTED_HEX), 0);
lv_obj_set_pos(sd_br_hint, UI_PAD + 2, SD_BR_HINT_Y);
/* 先把空列表摆出来,真正的挂载和列目录交给定时器 ——
定时器第一次触发在 200ms 后,这段时间足够 MOUNTING 画出来,
用户不会看到一屏没反应的界面。 */
sd_br_mounted = 0U;
sd_br_count = 0U;
sd_br_total = 0U;
sd_br_page = 0U;
sd_br_req = SD_BR_REQ_RELIST;
sd_br_refresh(); /* 行先清空、路径先显示 */
sd_br_set_stat("MOUNTING ...", UI_ACCENT_HEX);
sd_br_timer = lv_timer_create(sd_br_tick_cb, SD_BR_TICK_MS, NULL);
}
核心文件结构说明:

效果展示:



8.屏幕上添加视频播放器,播放sd卡上的视频。
由于文件较长,这里就不展示了。
介绍一下文件结构,以及如何使用



效果展示:


9.屏幕上添加dac功能,显示dac的输出波形,以及调整波形频率类似信号发生器。
核心部分:
static const char *const dac_wave_name[DAC_APP_WAVE_COUNT] =
{
"SINE", "SQUARE", "TRI", "SAW"
};
/* 频率格式化:<1kHz 用 Hz(一位小数),>=1kHz 用 kHz(两位小数)。
整数拆分自己拼,不走 float 版 printf —— 那会额外拉进来几 KB 库代码。 */
static void dac_fmt_freq(char * buf, size_t n, float hz)
{
uint32_t v10 = (uint32_t)((hz * 10.0f) + 0.5f); /* 0.1 Hz 为单位 */
if (v10 < 10000U) /* < 1000.0 Hz */
{
(void)snprintf(buf, n, "%lu.%lu Hz",
(unsigned long)(v10 / 10U), (unsigned long)(v10 % 10U));
}
else
{
uint32_t v100 = (uint32_t)((hz * 100.0f) + 0.5f); /* 0.01 Hz 为单位 */
(void)snprintf(buf, n, "%lu.%02lu kHz",
(unsigned long)(v100 / 100000U),
(unsigned long)((v100 / 1000U) % 100U));
}
}
/* 采样率(触发率)格式化:Hz / kHz / MHz 三档,从 256Hz 到 1.28MHz 都不会挤爆标签 */
static void dac_fmt_samp(char * buf, size_t n, float hz)
{
if (hz < 1000.0f)
{
(void)snprintf(buf, n, "%lu Hz", (unsigned long)(hz + 0.5f));
}
else if (hz < 1000000.0f)
{
uint32_t v = (uint32_t)((hz / 100.0f) + 0.5f); /* 0.1 kHz 为单位 */
(void)snprintf(buf, n, "%lu.%lu kHz",
(unsigned long)(v / 10U), (unsigned long)(v % 10U));
}
else
{
uint32_t v = (uint32_t)((hz / 10000.0f) + 0.5f); /* 0.01 MHz 为单位 */
(void)snprintf(buf, n, "%lu.%02lu MHz",
(unsigned long)(v / 100U), (unsigned long)(v % 100U));
}
}
/* 折线这一屏铺多少个周期:横轴是固定时间窗,所以周期数与频率成正比 ——
频率高就密、频率低就疏,和示波器固定时基看波形的感觉一致。
基准取 1kHz = DAC_WAVE_CYCLES 个周期;低频封底 1 个周期,不然整条线会拉成
一条直线什么都看不出来;高频封顶,再密就糊成一团。 */
static float dac_wave_cycles(void)
{
float cyc = (dac_app_get_freq() * (float)DAC_WAVE_CYCLES)
/ DAC_APP_DEFAULT_FREQ_HZ;
if (cyc < DAC_WAVE_CYC_MIN)
{
cyc = DAC_WAVE_CYC_MIN;
}
else if (cyc > DAC_WAVE_CYC_MAX)
{
cyc = DAC_WAVE_CYC_MAX;
}
return cyc;
}
/* 波形展示框:把 DMA 正在搬的那张表画出来,横向铺 dac_wave_cycles() 个周期。
取的就是硬件真正在用的同一份数据,所以框里看到的就是 PA5 上的形状。
横轴按分段数均分,纵轴把 0..4095 的码值映射成"上高下低"的像素。 */
static void dac_wave_draw(void)
{
int32_t i;
uint32_t span;
if (dac_wave == NULL)
{
return;
}
/* 本屏要覆盖的码值个数 = 周期数 × 每周期点数;至少一个整周期,好让线闭上 */
span = (uint32_t)((dac_wave_cycles() * (float)DAC_APP_WAVE_LEN) + 0.5f);
if (span < DAC_APP_WAVE_LEN)
{
span = DAC_APP_WAVE_LEN;
}
for (i = 0; i < (int32_t)DAC_WAVE_POINTS; i++)
{
uint32_t idx = ((uint32_t)i * span) / DAC_WAVE_SEG;
uint32_t code = dac_app_wave_point(idx);
dac_pts[i].x = (i * (dac_wave_w - 1)) / (int32_t)(DAC_WAVE_POINTS - 1U);
dac_pts[i].y = (dac_wave_h - 1)
- (int32_t)((code * (uint32_t)(dac_wave_h - 1)) / DAC_APP_FULL_SCALE);
}
lv_line_set_points(dac_wave, dac_pts, DAC_WAVE_POINTS);
}
/* 把参数刷到界面。四个显示量一个都没变就直接返回 —— 本机是单缓冲 + 阻塞刷屏,
静止的时候界面一次重绘都不该有,否则会把 LVGL 的触摸采样挤掉。 */
static void dac_page_update(void)
{
float freq = dac_app_get_freq();
uint8_t wave = dac_app_get_wave();
uint8_t running = dac_app_is_running();
char buf[40];
uint32_t i;
if ((dac_shown_valid != 0U) && (freq == dac_shown_freq)
&& (wave == dac_shown_wave) && (running == dac_shown_run))
{
return;
}
if (dac_lbl_f != NULL)
{
dac_fmt_freq(buf, sizeof(buf), freq);
lv_label_set_text(dac_lbl_f, buf);
}
if (dac_lbl_w != NULL)
{
lv_label_set_text(dac_lbl_w, dac_wave_name[wave]);
}
if (dac_lbl_s != NULL)
{
dac_fmt_samp(buf, sizeof(buf), dac_app_get_sample_rate());
lv_label_set_text(dac_lbl_s, buf);
}
/* 形状看波形类型、疏密看频率:两个里动一个就得重画折线 */
if ((wave != dac_shown_wave) || (freq != dac_shown_freq))
{
dac_wave_draw();
}
/* 当前波形对应的那个按键点亮,一眼看出选中的是哪个 */
for (i = 0U; i < DAC_APP_WAVE_COUNT; i++)
{
if (dac_btn_wave[i] != NULL)
{
lv_obj_set_style_bg_color(dac_btn_wave[i],
lv_color_hex((i == wave) ? UI_ACCENT_HEX : UI_BTN_HEX),
0);
}
}
/* 按钮上写的是"按下去会发生什么":正在输出就显示 STOP。
make_button 里标签是按钮的第一个子对象,直接拿它改文字。 */
if (dac_btn_run != NULL)
{
lv_obj_t * lbl = lv_obj_get_child(dac_btn_run, 0);
if (lbl != NULL)
{
lv_label_set_text(lbl, (running != 0U) ? "STOP" : "RUN");
}
}
dac_shown_freq = freq;
dac_shown_wave = wave;
dac_shown_run = running;
dac_shown_valid = 1U;
}
/* 频率加减。低频段把 ±10 的步进自动收成 ±1:10Hz 往下,10 的步进一下就跨过去
了,想停在 2Hz、5Hz 这种整数上根本停不住。 */
static void dac_freq_cb(lv_event_t * e)
{
int32_t step = (int32_t)(intptr_t)lv_event_get_user_data(e);
float f = dac_app_get_freq();
if ((f <= 10.0f) && ((step == 10) || (step == -10)))
{
step = (step > 0) ? 1 : -1;
}
dac_app_set_freq(f + (float)step);
dac_page_update();
}
static void dac_wave_cb(lv_event_t * e)
{
dac_app_set_wave((uint8_t)(intptr_t)lv_event_get_user_data(e));
dac_page_update();
}
static void dac_run_cb(lv_event_t * e)
{
(void)e;
dac_app_set_enable((dac_app_is_running() != 0U) ? 0U : 1U);
dac_page_update();
}
/* 回到上电默认(1kHz 正弦):参数调乱了想快速回到一个已知状态时很有用 */
static void dac_default_cb(lv_event_t * e)
{
(void)e;
dac_app_set_wave(DAC_APP_DEFAULT_WAVE);
dac_app_set_freq(DAC_APP_DEFAULT_FREQ_HZ);
dac_app_set_enable(1U);
dac_page_update();
}
static void dac_timer_cb(lv_timer_t * timer)
{
(void)timer;
dac_page_update();
}
static void ui_build_dac(lv_obj_t * scr)
{
static const char *const freq_txt[4] = {"-100", "-10", "+10", "+100"};
static const int32_t freq_step[4] = {-100, -10, 10, 100};
lv_obj_t * info;
lv_obj_t * panel;
lv_obj_t * btn;
int32_t field_w = lv_display_get_horizontal_resolution(lv_display_get_default())
- (2 * UI_PAD);
int32_t btn_w = (field_w - (3 * UI_GAP)) / 4;
int32_t big_w = (field_w - UI_GAP) / 2;
int32_t i;
char buf[24];
(void)ui_make_head(scr, "DAC DAC1_OUT2 / PA5", "3D", UI_PAGE_CUBE);
/* ---------- 参数:左上频率、右上波形名,下行采样率 / 点数 ---------- */
info = lv_obj_create(scr);
lv_obj_remove_style_all(info);
lv_obj_set_pos(info, UI_PAD, DAC_INFO_Y);
lv_obj_set_size(info, field_w, DAC_INFO_H);
lv_obj_remove_flag(info, LV_OBJ_FLAG_SCROLLABLE);
dac_lbl_f = lv_label_create(info);
lv_obj_set_style_text_font(dac_lbl_f, &lv_font_montserrat_14, 0);
lv_obj_set_style_text_color(dac_lbl_f, lv_color_hex(UI_ACCENT_HEX), 0);
lv_obj_align(dac_lbl_f, LV_ALIGN_TOP_LEFT, 0, 0);
dac_lbl_w = lv_label_create(info);
lv_obj_set_style_text_font(dac_lbl_w, &lv_font_montserrat_14, 0);
lv_obj_set_style_text_color(dac_lbl_w, lv_color_hex(UI_OK_HEX), 0);
lv_obj_align(dac_lbl_w, LV_ALIGN_TOP_RIGHT, 0, 0);
dac_lbl_s = lv_label_create(info);
lv_obj_set_style_text_font(dac_lbl_s, &lv_font_montserrat_14, 0);
lv_obj_set_style_text_color(dac_lbl_s, lv_color_hex(UI_MUTED_HEX), 0);
lv_obj_align(dac_lbl_s, LV_ALIGN_BOTTOM_LEFT, 0, 0);
dac_lbl_p = lv_label_create(info);
lv_obj_set_style_text_font(dac_lbl_p, &lv_font_montserrat_14, 0);
lv_obj_set_style_text_color(dac_lbl_p, lv_color_hex(UI_MUTED_HEX), 0);
lv_obj_align(dac_lbl_p, LV_ALIGN_BOTTOM_RIGHT, 0, 0);
(void)snprintf(buf, sizeof(buf), "%luPT 12bit", (unsigned long)DAC_APP_WAVE_LEN);
lv_label_set_text(dac_lbl_p, buf);
/* ---------- 波形展示框:底色 + 三条参考线 + 折线 ---------- */
panel = lv_obj_create(scr);
lv_obj_remove_style_all(panel);
lv_obj_set_pos(panel, UI_PAD, DAC_WAVE_Y);
lv_obj_set_size(panel, field_w, DAC_WAVE_H);
lv_obj_set_style_bg_color(panel, lv_color_hex(UI_FIELD_HEX), 0);
lv_obj_set_style_bg_opa(panel, LV_OPA_COVER, 0);
lv_obj_set_style_border_width(panel, 1, 0);
lv_obj_set_style_border_color(panel, lv_color_hex(0x27324A), 0);
lv_obj_set_style_radius(panel, 6, 0);
lv_obj_remove_flag(panel, LV_OBJ_FLAG_SCROLLABLE);
for (i = 0; i < 3; i++) /* 满量程 / 中点 / 零,给幅度一个参照 */
{
lv_obj_t * ref = lv_obj_create(panel);
lv_obj_remove_style_all(ref);
lv_obj_set_size(ref, field_w - 20, 1);
lv_obj_set_style_bg_color(ref, lv_color_hex(0x27324A), 0);
lv_obj_set_style_bg_opa(ref, LV_OPA_COVER, 0);
lv_obj_set_pos(ref, 10, ((DAC_WAVE_H - 4) * i) / 2);
lv_obj_remove_flag(ref, LV_OBJ_FLAG_SCROLLABLE);
lv_obj_remove_flag(ref, LV_OBJ_FLAG_CLICKABLE);
}
dac_wave_w = field_w - (2 * DAC_WAVE_PAD);
dac_wave_h = DAC_WAVE_H - (2 * DAC_WAVE_PAD);
dac_wave = lv_line_create(panel);
lv_obj_remove_style_all(dac_wave);
lv_obj_set_pos(dac_wave, DAC_WAVE_PAD, DAC_WAVE_PAD);
lv_obj_set_style_line_width(dac_wave, 2, 0);
lv_obj_set_style_line_rounded(dac_wave, true, 0);
lv_obj_set_style_line_opa(dac_wave, LV_OPA_COVER, 0);
lv_obj_set_style_line_color(dac_wave, lv_color_hex(UI_ACCENT_HEX), 0);
/* ---------- 频率加减 ---------- */
for (i = 0; i < 4; i++)
{
btn = make_button(scr, freq_txt[i], UI_BTN_HEX, &lv_font_montserrat_14,
dac_freq_cb, (void *)(intptr_t)freq_step[i]);
lv_obj_set_pos(btn, UI_PAD + (i * (btn_w + UI_GAP)), DAC_ROW1_Y);
lv_obj_set_size(btn, btn_w, DAC_ROW_H);
}
/* ---------- 波形类型:选中的那个底色点亮,见 dac_page_update ---------- */
for (i = 0; i < 4; i++)
{
dac_btn_wave[i] = make_button(scr, dac_wave_name[i], UI_BTN_HEX,
&lv_font_montserrat_14, dac_wave_cb,
(void *)(intptr_t)i);
lv_obj_set_pos(dac_btn_wave[i], UI_PAD + (i * (btn_w + UI_GAP)), DAC_ROW2_Y);
lv_obj_set_size(dac_btn_wave[i], btn_w, DAC_ROW_H);
}
/* ---------- RUN/STOP + 回到默认 ---------- */
dac_btn_run = make_button(scr, "RUN", UI_OK_HEX, &lv_font_montserrat_14,
dac_run_cb, NULL);
lv_obj_set_pos(dac_btn_run, UI_PAD, DAC_ROW3_Y);
lv_obj_set_size(dac_btn_run, big_w, DAC_ROW3_H);
btn = make_button(scr, "1kHz SINE", UI_BTN_HEX, &lv_font_montserrat_14,
dac_default_cb, NULL);
lv_obj_set_pos(btn, UI_PAD + big_w + UI_GAP, DAC_ROW3_Y);
lv_obj_set_size(btn, big_w, DAC_ROW3_H);
/* ---------- 先按当前状态刷一次,再挂兜底刷新 ---------- */
dac_shown_valid = 0U; /* 页面重建过,显示缓存必须作废 */
dac_page_update();
dac_timer = lv_timer_create(dac_timer_cb, DAC_REFRESH_MS, NULL);
}
效果展示:



10.源码分享:
https://gitee.com/c_y_l_N_H/lvglstm32h72.4tft-project
更多推荐


所有评论(0)