使用LVGL模拟示波器创建正弦函数波形
代码
oscilloscope_sine_wave.c
c
/**
* @file oscilloscope_sine_wave.c
* @brief LVGL 正弦函数示波器动画 - 实现文件
* @details 本文件实现了完整的示波器波形显示面板,包括:
* 1. 正弦波数据生成(带相位偏移的连续动画)
* 2. Canvas 网格背景绘制(深蓝色主题)
* 3. 波形线条绘制(绿色荧光效果)
* 4. 定时器驱动的动画循环
* 5. 时基/触发状态 UI 显示
*
* @note 需要 LVGL v8.x 或 v9.x 库支持
* @note 建议使用 320x480 分辨率的屏幕以获得最佳显示效果
* @note 正弦波公式:y = sin((i + phase) / count * 2 * PI)
* 其中 i 为采样点索引,phase 为动画相位偏移量
*/
#include "oscilloscope_sine_wave.h"
#include <stdio.h>
#include <math.h>
#include <stdlib.h>
/* ==================== 常量定义 ==================== */
/** 波形显示区域尺寸 */
#define WAVEFORM_AREA_WIDTH 800 /**< 波形区域宽度(像素),与屏幕宽度一致 */
#define WAVEFORM_AREA_HEIGHT 380 /**< 波形区域高度(像素),留出上下空间给标签 */
/** 网格线数量 */
#define GRID_LINES_HORIZONTAL 8 /**< 水平网格线数量(将高度等分为 8 段) */
#define GRID_LINES_VERTICAL 10 /**< 垂直网格线数量(将宽度等分为 10 段) */
/** 波形采样点数量 */
#define WAVEFORM_SAMPLE_COUNT 800 /**< 正弦波采样点数,决定波形分辨率 */
/** 定时器刷新间隔(毫秒) */
#define WAVEFORM_TIMER_INTERVAL 50 /**< 50ms = 20Hz 刷新率,保证动画流畅 */
/** 相位步进值(每次定时器回调增加的相位值) */
#define WAVEFORM_PHASE_STEP 5 /**< 相位步进,值越大波形移动越快 */
/* ==================== 静态变量定义 ==================== */
/** 示波器页面容器对象 */
static lv_obj_t *oscilloscope_page = NULL;
/** 波形绘制画布对象(使用 lv_obj 或 lv_canvas 作为绘制区域) */
static lv_obj_t *waveform_canvas = NULL;
/** 时基显示标签 */
static lv_obj_t *timebase_label = NULL;
/** 触发状态显示标签 */
static lv_obj_t *trigger_label = NULL;
/** 波形更新定时器 */
static lv_timer_t *waveform_timer = NULL;
/** 波形采样数据缓冲区(浮点型,范围 -1.0 ~ 1.0) */
static float waveform_data[WAVEFORM_SAMPLE_COUNT];
/** 当前动画相位偏移量(0~359 度循环) */
static uint32_t waveform_phase = 0;
/** 正弦波 PI 常量 */
static const float PI = 3.14159265358979f;
/* ==================== 正弦波数据生成 ==================== */
/**
* @brief 生成正弦波采样数据
*
* @param phase 当前相位偏移量(0~359),用于产生动画效果
* @param data 输出数据数组指针(调用者提供的缓冲区)
* @param count 采样点数量
*
* @details 正弦波生成公式:
* angle = (i + phase) / count * 2 * PI
* data[i] = sin(angle)
*
* 其中:
* - i: 采样点索引(0 ~ count-1)
* - phase: 动画相位偏移(每帧递增,产生波形移动效果)
* - count: 总采样点数
* - PI: 圆周率常量
*
* 波形范围:-1.0(波谷) ~ +1.0(波峰)
*
* @note 该函数为静态函数,仅在本文件内部使用
*/
static void generate_sine_wave(uint32_t phase, float *data, uint16_t count)
{
uint16_t i;
float angle;
for (i = 0; i < count; i++) {
/* 计算当前采样点的角度(弧度制)
* (i + phase) / count 将采样点归一化到 [0, 2*PI) 范围
* 乘以 2*PI 得到完整的正弦周期角度
* phase 的加入使波形产生"流动"动画效果
*/
angle = (float)(i + phase) * 3.0f / (float)count * 2.0f * PI;
/* 计算正弦值,结果范围 [-1.0, 1.0] */
data[i] = sinf(angle);
}
}
/* ==================== Canvas 绘制回调 ==================== */
/**
* @brief Canvas 主绘制事件回调函数
*
* @param e 事件对象指针
*
* @details 该函数在 LVGL 绘制循环中被调用,负责绘制:
* 1. 深蓝色背景矩形
* 2. 水平/垂直网格线(模拟示波器刻度)
* 3. 中心十字线(加粗高亮)
* 4. 绿色正弦波形曲线
*
* @note 仅在 LV_EVENT_DRAW_MAIN 事件时执行,避免重复绘制
*/
static void canvas_draw_event_cb(lv_event_t *e)
{
lv_event_code_t event_code = lv_event_get_code(e);
lv_obj_t *obj = lv_event_get_target(e);
lv_obj_t *parent;
int32_t width, height;
lv_draw_ctx_t *draw_ctx;
lv_area_t grid_area;
lv_draw_rect_dsc_t rect_dsc;
lv_draw_line_dsc_t line_dsc;
int32_t h_step, v_step;
int32_t x_step, center_y, amplitude;
lv_point_t p1, p2;
lv_point_t h_center_p1, h_center_p2;
lv_point_t v_center_p1, v_center_p2;
uint16_t i;
/* 仅在主绘制事件时执行,避免在刷新/覆盖等事件时重复绘制 */
if (event_code != LV_EVENT_DRAW_MAIN) {
return;
}
parent = lv_obj_get_parent(obj);
if (parent == NULL) {
return;
}
if (!(const lv_area_t*)lv_event_get_param(e)) {
return;
}
width = lv_obj_get_width(obj);
height = lv_obj_get_height(obj);
draw_ctx = lv_event_get_draw_ctx(e);
/* ========== 1. 绘制网格背景 ========== */
grid_area.x1 = 0;
grid_area.y1 = 0;
grid_area.x2 = width - 1;
grid_area.y2 = height - 1;
lv_draw_rect_dsc_init(&rect_dsc);
/* 深蓝色背景(#1a1a2e),模拟示波器暗色屏幕 */
rect_dsc.bg_color = lv_color_hex(0x1a1a2e);
rect_dsc.bg_opa = LV_OPA_COVER;
lv_draw_rect(draw_ctx, &rect_dsc, &grid_area);
/* ========== 2. 绘制网格线 ========== */
lv_draw_line_dsc_init(&line_dsc);
/* 网格线颜色(#3d3d5c),半透明显示 */
line_dsc.color = lv_color_hex(0x3d3d5c);
line_dsc.opa = LV_OPA_50;
line_dsc.width = 1;
/* 水平网格线:将高度等分为 GRID_LINES_HORIZONTAL 段 */
h_step = height / GRID_LINES_HORIZONTAL;
for (i = 0; i <= GRID_LINES_HORIZONTAL; i++) {
p1.x = 0;
p1.y = i * h_step;
p2.x = width - 1;
p2.y = i * h_step;
lv_draw_line(draw_ctx, &line_dsc, &p1, &p2);
}
/* 垂直网格线:将宽度等分为 GRID_LINES_VERTICAL 段 */
v_step = width / GRID_LINES_VERTICAL;
for (i = 0; i <= GRID_LINES_VERTICAL; i++) {
p1.x = i * v_step;
p1.y = 0;
p2.x = i * v_step;
p2.y = height - 1;
lv_draw_line(draw_ctx, &line_dsc, &p1, &p2);
}
/* ========== 3. 绘制中心十字线(加粗高亮) ========== */
line_dsc.color = lv_color_hex(0x5d5d7c);
line_dsc.width = 2;
/* 水平中心线 */
h_center_p1.x = 0;
h_center_p1.y = height / 2;
h_center_p2.x = width - 1;
h_center_p2.y = height / 2;
lv_draw_line(draw_ctx, &line_dsc, &h_center_p1, &h_center_p2);
/* 垂直中心线 */
v_center_p1.x = width / 2;
v_center_p1.y = 0;
v_center_p2.x = width / 2;
v_center_p2.y = height - 1;
lv_draw_line(draw_ctx, &line_dsc, &v_center_p1, &v_center_p2);
/* ========== 4. 绘制正弦波形 ========== */
if (WAVEFORM_SAMPLE_COUNT > 1) {
lv_draw_line_dsc_init(&line_dsc);
/* 波形线条颜色(#00ff88),模拟示波器绿色荧光 */
line_dsc.color = lv_color_hex(0x00ff88);
line_dsc.opa = LV_OPA_COVER;
line_dsc.width = 3; /**< 线条加粗,模拟示波器荧光余辉效果 */
line_dsc.round_end = 1;
line_dsc.round_start = 1;
/* 计算每个采样点的 X 轴间距 */
x_step = width / (WAVEFORM_SAMPLE_COUNT - 1);
/* 波形中心 Y 坐标(屏幕垂直中点) */
center_y = height / 2;
/* 波形振幅(使用屏幕高度的 40%,留出上下边距) */
amplitude = height * 2 / 5;
/* 将采样数据转换为屏幕坐标点并绘制连线 */
lv_point_t points[WAVEFORM_SAMPLE_COUNT];
for (i = 0; i < WAVEFORM_SAMPLE_COUNT; i++) {
/* X 坐标:从左到右均匀分布 */
points[i].x = i * x_step;
/* Y 坐标:以中心线为基准,振幅缩放
* 注意 LVGL 坐标系 Y 轴向下为正
* 因此用 center_y - data[i] * amplitude(减去是因为波形向上为负 Y)
*/
points[i].y = center_y - (int32_t)(waveform_data[i] * amplitude);
}
/* 使用 lv_draw_line 绘制连续波形线 */
for (i = 0; i < WAVEFORM_SAMPLE_COUNT - 1; i++) {
lv_draw_line(draw_ctx, &line_dsc, &points[i], &points[i + 1]);
}
}
}
/* ==================== 定时器动画回调 ==================== */
/**
* @brief 波形更新定时器回调函数
*
* @param timer 定时器对象指针(本实现中未使用)
*
* @details 该函数由 LVGL 定时器自动调用,负责:
* 1. 递增动画相位偏移量(0~359 度循环)
* 2. 根据新相位重新生成正弦波采样数据
* 3. 标记 Canvas 需要重绘
*
* @note 定时器刷新率为 20Hz(50ms/帧),保证动画流畅
*/
static void waveform_timer_cb(lv_timer_t *timer)
{
(void)timer;
/* 递增相位偏移量(0~359 度循环)
* 每次增加 5 度,约 72 帧完成一个完整周期(72 * 50ms = 3.6s)
*/
waveform_phase = (waveform_phase + WAVEFORM_PHASE_STEP) % 360;
/* 根据新相位重新生成正弦波采样数据 */
generate_sine_wave(waveform_phase, waveform_data, WAVEFORM_SAMPLE_COUNT);
/* 标记 Canvas 对象需要重绘,触发 canvas_draw_event_cb */
if (waveform_canvas != NULL) {
lv_obj_invalidate(waveform_canvas);
}
}
/* ==================== 公共 API 实现 ==================== */
/**
* @brief 创建示波器页面
*
* @param parent 父对象指针(通常为 NULL)
* @return 示波器页面容器对象指针
*
* @details 创建流程:
* 1. 创建全屏页面容器
* 2. 创建 Canvas 绘制区域
* 3. 创建时基和触发状态标签
* 4. 创建 20Hz 动画定时器
* 5. 生成初始正弦波数据并触发首次绘制
*/
lv_obj_t* oscilloscope_sine_wave_create(lv_obj_t *parent)
{
/* 创建页面容器对象 */
oscilloscope_page = lv_obj_create(parent);
/* 设置页面为全屏大小 */
lv_obj_set_size(oscilloscope_page, LV_HOR_RES, LV_VER_RES);
/* 创建波形绘制 Canvas 区域 */
waveform_canvas = lv_obj_create(oscilloscope_page);
lv_obj_set_size(waveform_canvas, WAVEFORM_AREA_WIDTH, WAVEFORM_AREA_HEIGHT);
lv_obj_align(waveform_canvas, LV_ALIGN_TOP_MID, 0, 10); /**< 顶部居中,留 10px 上边距 */
/* 注册 Canvas 绘制事件回调 */
lv_obj_add_event_cb(waveform_canvas, canvas_draw_event_cb, LV_EVENT_DRAW_MAIN, NULL);
/* 创建时基显示标签 */
timebase_label = lv_label_create(oscilloscope_page);
lv_label_set_text(timebase_label, "Timebase: 5.0ms/div");
lv_obj_align(timebase_label, LV_ALIGN_BOTTOM_LEFT, 5, -5);
/* 创建触发状态显示标签 */
trigger_label = lv_label_create(oscilloscope_page);
lv_label_set_text(trigger_label, "Trigger: READY | Level: 0.00V");
lv_obj_align(trigger_label, LV_ALIGN_BOTTOM_RIGHT, -5, -5);
/* 生成初始正弦波数据(phase=0) */
generate_sine_wave(0, waveform_data, WAVEFORM_SAMPLE_COUNT);
/* 创建 20Hz 动画定时器,自动驱动波形刷新 */
waveform_timer = lv_timer_create(waveform_timer_cb, WAVEFORM_TIMER_INTERVAL, NULL);
lv_timer_ready(waveform_timer); /**< 立即触发一次回调,显示初始波形 */
return oscilloscope_page;
}
/**
* @brief 用外部数据更新波形显示
*
* @param data 采样数据数组指针(浮点型,范围 -1.0 ~ 1.0)
* @param count 采样数据个数
*
* @note 当 count 超过 WAVEFORM_SAMPLE_COUNT 时自动截断
*/
void oscilloscope_sine_wave_update(const float *data, uint16_t count)
{
/* 防止数组越界 */
if (count > WAVEFORM_SAMPLE_COUNT) {
count = WAVEFORM_SAMPLE_COUNT;
}
/* 将外部数据复制到内部缓冲区 */
for (uint16_t i = 0; i < count; i++) {
waveform_data[i] = data[i];
}
/* 触发 Canvas 重绘 */
if (waveform_canvas != NULL) {
lv_obj_invalidate(waveform_canvas);
}
}
/**
* @brief 设置时基(Timebase)显示值
*
* @param value_ms 时基值,单位为毫秒/格(ms/div)
*
* @note 自动根据数值大小选择显示单位:
* >= 1000ms -> 秒(s/div)
* >= 1ms -> 毫秒(ms/div)
* < 1ms -> 微秒(us/div)
*/
void oscilloscope_sine_wave_set_timebase(float value_ms)
{
char buffer[32];
if (timebase_label == NULL) {
return;
}
if (value_ms >= 1000.0f) {
/* 大于等于 1 秒,显示为秒 */
snprintf(buffer, sizeof(buffer), "Timebase: %.1fms/div", (float)value_ms / 1000.0f);
} else if (value_ms >= 1.0f) {
/* 1ms ~ 999ms,显示为毫秒 */
snprintf(buffer, sizeof(buffer), "Timebase: %.1fms/div", (float)value_ms);
} else {
/* 小于 1ms,显示为微秒 */
snprintf(buffer, sizeof(buffer), "Timebase: %.1fms/div", (float)value_ms * 1000.0);
}
lv_label_set_text(timebase_label, buffer);
}
/**
* @brief 设置触发状态
*
* @param triggered 触发状态:true=ACTIVE(触发激活),false=READY(待触发)
* @param level 触发电平电压值(单位:伏特 V)
*/
void oscilloscope_sine_wave_set_trigger(bool triggered, float level)
{
char buffer[64];
if (trigger_label == NULL) {
return;
}
snprintf(buffer, sizeof(buffer),
"Trigger: %s | Level: %.2fV",
triggered ? "ACTIVE" : "READY", level);
lv_label_set_text(trigger_label, buffer);
}
/**
* @brief 获取示波器页面容器对象指针
*
* @return 示波器页面容器对象指针
*/
lv_obj_t* oscilloscope_sine_wave_get_obj(void)
{
return oscilloscope_page;
}
/**
* @brief 启动波形动画定时器
*
* @note 如果定时器已在运行则不会重复创建
*/
void oscilloscope_sine_wave_start_animation(void)
{
if (waveform_timer == NULL) {
waveform_timer = lv_timer_create(waveform_timer_cb, WAVEFORM_TIMER_INTERVAL, NULL);
lv_timer_ready(waveform_timer); /**< 立即触发一次 */
}
}
/**
* @brief 停止波形动画定时器
*
* @note 停止后波形将不再自动刷新
*/
void oscilloscope_sine_wave_stop_animation(void)
{
if (waveform_timer != NULL) {
lv_timer_del(waveform_timer);
waveform_timer = NULL;
}
}
oscilloscope_sine_wave.h
c
/**
* @file oscilloscope_sine_wave.h
* @brief LVGL 正弦函数示波器动画 - 头文件
* @details 本文件定义了示波器波形显示面板的公共接口,
* 包括页面创建、波形数据更新、时基设置、触发状态设置、动画启停等功能。
* @note 需要 LVGL v8.x 或 v9.x 库支持,建议使用 320x480 分辨率屏幕。
*/
#ifndef OSCILLOSCOPE_SINE_WAVE_H
#define OSCILLOSCOPE_SINE_WAVE_H
#include "lvgl/lvgl.h"
#include <stdint.h>
#include <stdbool.h>
#ifdef __cplusplus
extern "C" {
#endif
/* ==================== 公共函数声明 ==================== */
/**
* @brief 创建示波器页面
*
* @param parent 父对象指针(通常为 NULL,表示全屏页面)
* @return 示波器页面容器对象指针
*
* @note 该函数会初始化所有内部资源,包括 Canvas 对象、
* 定时器、网格参数等。调用后页面自动可见。
*/
lv_obj_t* oscilloscope_sine_wave_create(lv_obj_t *parent);
/**
* @brief 用外部数据更新波形显示
*
* @param data 采样数据数组指针(浮点型,范围 -1.0 ~ 1.0)
* @param count 采样数据个数
*
* @note 当 count 超过内部缓冲区大小时会自动截断。
* 调用后会自动触发 Canvas 重绘。
*/
void oscilloscope_sine_wave_update(const float *data, uint16_t count);
/**
* @brief 设置时基(Timebase)显示值
*
* @param value_ms 时基值,单位为毫秒/格(ms/div)
*
* @note 函数会自动根据数值大小选择显示单位:
* >= 1000ms 显示为秒(s/div),>= 1ms 显示为毫秒(ms/div),
* < 1ms 显示为微秒(us/div)。
*/
void oscilloscope_sine_wave_set_timebase(float value_ms);
/**
* @brief 设置触发状态
*
* @param triggered 触发状态:true=触发激活(ACTIVE),false=待触发(READY)
* @param level 触发电平电压值(单位:伏特 V)
*
* @note 该函数仅更新 UI 标签显示,不影响波形生成逻辑。
*/
void oscilloscope_sine_wave_set_trigger(bool triggered, float level);
/**
* @brief 获取示波器页面容器对象指针
*
* @return 示波器页面容器对象指针
*/
lv_obj_t* oscilloscope_sine_wave_get_obj(void);
/**
* @brief 启动波形动画定时器
*
* @note 启动后定时器将以 20Hz 频率回调,自动更新相位并刷新波形。
* 如果定时器已在运行,则不会重复创建。
*/
void oscilloscope_sine_wave_start_animation(void);
/**
* @brief 停止波形动画定时器
*
* @note 停止后波形将不再自动刷新,需手动调用 update 函数才能更新显示。
*/
void oscilloscope_sine_wave_stop_animation(void);
#ifdef __cplusplus
}
#endif
#endif /* OSCILLOSCOPE_SINE_WAVE_H */
实验结果
示波器动画