【 声明:版权所有,欢迎转载,请勿用于商业用途。 联系信箱:feixiaoxing @163.com】
前面为了开发lvgl,我们做了一些准备。这里面包括了屏幕的驱动,触摸屏的驱动,屏幕和lvgl的适配,触摸和lvgl的适配,以及触摸的简单修改和标定。做好了这几部分之后,基本上就可以做lvgl的开发了。

1、lvgl开发基本就是纯软开发
这里的lvgl,其实不光用在mcu上,还比较多的用在了linux上。比如很多的linux界面,早期的时候可能用qt还是比较多的,现在越来越多的开发者转到了lvgl上面。如果底层驱动都ok了,相关的适配也就没有问题了,那么l此时vgl之上的开发基本就是纯软开发了。
2、lvgl可以用windows仿真
对于lvgl开发的同学,如果对底层参与不多,完全可以先用windows vs开发后,再移植到嵌入式设备上面,这样效率反而是最高的。反之,如果每一步都是用硬件去开发,反而效率是最低的。
3、纯软+ai开发
软件开发中,ai适配最好的其实就是纯软开发。比如一般的网页前端开发、java后端开发,这部分用ai的其实已经很多了。那现在有了ai之后,用ai做lvgl开发也是非常方便的。对于简单的、不复杂的、页面不多的应用,ai开发的效率远远高于个人本身。所以大家在开发的过程当中,对于纯软这部分呢,也要尽可能用ai去做。这部分既然效率高,是趋势,我们不妨好好研究、好好去利用起来就好了。
4、以抽奖程序作为demo进行ai开发
前面我们用ai写了一个进度条程序。其实自己稍微改一下,或者让ai改一下界面和逻辑,就可以变成一个抽奖程序。当然,下面代码里面因为还涉及到了驱动和适配的内容,稍显复杂。而真正和业务相关的部分,代码不算多的。
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "driver/spi_master.h"
#include "driver/gpio.h"
#include "esp_log.h"
#include "esp_timer.h"
#include "lvgl.h"
static void lvgl_task(void *arg);
// ================= CONFIG =================
#define LCD_W 480
#define LCD_H 320
#define PIN_MOSI 13
#define PIN_CLK 14
#define PIN_CS 15
#define PIN_DC 2
#define PIN_RST 4
#define PIN_BL 12
#define TP_MOSI 23
#define TP_MISO 19
#define TP_CLK 18
#define TP_CS 5
#define TP_IRQ 27
static spi_device_handle_t spi_lcd;
static spi_device_handle_t spi_tp;
static const char *TAG = "ILI9488_LVGL";
// ======================================================
// GPIO control
// ======================================================
static inline void dc_cmd(void) { gpio_set_level(PIN_DC, 0); }
static inline void dc_data(void) { gpio_set_level(PIN_DC, 1); }
static void lcd_reset(void)
{
gpio_set_level(PIN_RST, 0);
vTaskDelay(pdMS_TO_TICKS(100));
gpio_set_level(PIN_RST, 1);
vTaskDelay(pdMS_TO_TICKS(150));
}
// ======================================================
// SPI CMD / DATA
// ======================================================
static void lcd_cmd(uint8_t cmd)
{
spi_transaction_t t = {
.length = 8,
.tx_buffer = &cmd,
};
dc_cmd();
spi_device_polling_transmit(spi_lcd, &t);
}
static void lcd_data(const void *data, int len)
{
spi_transaction_t t = {
.length = len * 8,
.tx_buffer = data,
};
dc_data();
spi_device_polling_transmit(spi_lcd, &t);
}
// ======================================================
// ILI9488 INIT (stable version)
// ======================================================
static void ili9488_init(void)
{
lcd_reset();
lcd_cmd(0x01); // Software reset
vTaskDelay(pdMS_TO_TICKS(120));
lcd_cmd(0x11); // Sleep out
vTaskDelay(pdMS_TO_TICKS(120));
// RGB565 mode (important for stability)
lcd_cmd(0x3A);
uint8_t pix = 0x66;
lcd_data(&pix, 1);
// MADCTL (display orientation)
lcd_cmd(0x36);
uint8_t mad = 0x28; // change to 0x28 if upside-down
lcd_data(&mad, 1);
lcd_cmd(0x29); // Display ON
vTaskDelay(pdMS_TO_TICKS(50));
ESP_LOGI(TAG, "LCD init OK");
}
// ======================================================
// Set drawing window
// ======================================================
static void set_window(int x1,int y1,int x2,int y2)
{
uint8_t d[4];
lcd_cmd(0x2A);
d[0]=x1>>8; d[1]=x1;
d[2]=x2>>8; d[3]=x2;
lcd_data(d,4);
lcd_cmd(0x2B);
d[0]=y1>>8; d[1]=y1;
d[2]=y2>>8; d[3]=y2;
lcd_data(d,4);
lcd_cmd(0x2C);
}
// ======================================================
// Touch read (XPT2046 style)
// ======================================================
static uint16_t tp_read(uint8_t cmd)
{
uint8_t tx[3] = {cmd,0,0};
uint8_t rx[3] = {0};
spi_transaction_t t = {
.length = 24,
.tx_buffer = tx,
.rx_buffer = rx,
};
spi_device_polling_transmit(spi_tp, &t);
return ((rx[1] << 8) | rx[2]) >> 3;
}
static void touch_read(lv_indev_drv_t *drv, lv_indev_data_t *data)
{
static bool touched = false;
if (gpio_get_level(TP_IRQ) == 0)
{
data->state = LV_INDEV_STATE_PRESSED;
// Read raw touch coordinates
uint16_t x_raw = tp_read(0xD0);
uint16_t y_raw = tp_read(0x90);
// Calibrate and convert to screen coordinates
// Swap X and Y, and invert Y axis for correct orientation
// Adjust these values based on your touch panel calibration
int x = (LCD_W * (3900 - y_raw)) / 3900; // very critical, by feixiaoxing
int y = LCD_H - 1 - (LCD_H * (x_raw - 100)) / 3900;
// Clamp values
if (x < 0) x = 0;
if (x >= LCD_W) x = LCD_W - 1;
if (y < 0) y = 0;
if (y >= LCD_H) y = LCD_H - 1;
data->point.x = x;
data->point.y = y;
if (!touched) {
ESP_LOGI(TAG, "Touch: x=%d, y=%d (raw: x=%d, y=%d)",
data->point.x, data->point.y, x_raw, y_raw);
touched = true;
}
}
else
{
data->state = LV_INDEV_STATE_RELEASED;
touched = false;
}
}
// ======================================================
// LVGL flush (stable version)
// ======================================================
static uint8_t line_buf[480 * 3];
static void lcd_flush(lv_disp_drv_t *disp, const lv_area_t *area, lv_color_t *color_p)
{
int w = area->x2 - area->x1 + 1;
int h = area->y2 - area->y1 + 1;
set_window(area->x1, area->y1, area->x2, area->y2);
dc_data();
spi_transaction_t t = {
.length = w * 3 * 8,
.tx_buffer = line_buf,
};
for (int y = 0; y < h; y++)
{
for (int x = 0; x < w; x++)
{
uint16_t c = color_p[y * w + x].full;
line_buf[x*3+0] = ((c >> 11) & 0x1F) << 3;
line_buf[x*3+1] = ((c >> 5) & 0x3F) << 2;
line_buf[x*3+2] = (c & 0x1F) << 3;
}
spi_device_polling_transmit(spi_lcd, &t);
}
lv_disp_flush_ready(disp);
}
// Lottery Program
// ======================================================
static lv_obj_t *label_number; // Display the random number
static lv_obj_t *btn_start; // Start button
static lv_obj_t *btn_stop; // Stop button
static bool is_running = false; // Lottery running flag
static int current_number = 0; // Current random number
// Lottery update task
static void lottery_task(void *arg)
{
char buf[16];
while (1) {
if (is_running) {
// Generate random number between 1-999
current_number = (rand() % 1000);
sprintf(buf, "%03d", current_number);
lv_label_set_text(label_number, buf);
}
vTaskDelay(pdMS_TO_TICKS(50)); // Update every 50ms
}
}
// Start button callback
static void btn_start_event_cb(lv_event_t *e)
{
lv_event_code_t code = lv_event_get_code(e);
if (code == LV_EVENT_CLICKED) {
if (!is_running) {
is_running = true;
ESP_LOGI(TAG, "Lottery started");
}
}
}
// Stop button callback
static void btn_stop_event_cb(lv_event_t *e)
{
lv_event_code_t code = lv_event_get_code(e);
if (code == LV_EVENT_CLICKED) {
if (is_running) {
is_running = false;
ESP_LOGI(TAG, "Lottery stopped: %03d", current_number);
}
}
}
static void ui_create(void)
{
// Initialize random seed
srand(esp_timer_get_time());
// Create title label
lv_obj_t *label_title = lv_label_create(lv_scr_act());
lv_label_set_text(label_title, "LOTTERY");
lv_obj_align(label_title, LV_ALIGN_CENTER, 0, -80);
lv_obj_set_style_text_font(label_title, &lv_font_montserrat_14, LV_PART_MAIN);
// Create number display label
label_number = lv_label_create(lv_scr_act());
lv_label_set_text(label_number, "0");
lv_obj_align(label_number, LV_ALIGN_CENTER, 0, 0);
lv_obj_set_style_text_font(label_number, &lv_font_montserrat_32, LV_PART_MAIN);
lv_obj_set_style_text_color(label_number, lv_color_hex(0xFF0000), LV_PART_MAIN);
// Create start button
btn_start = lv_btn_create(lv_scr_act());
lv_obj_set_size(btn_start, 120, 50);
lv_obj_align(btn_start, LV_ALIGN_CENTER, -80, 70);
lv_obj_add_event_cb(btn_start, btn_start_event_cb, LV_EVENT_ALL, NULL);
lv_obj_t *label_start = lv_label_create(btn_start);
lv_label_set_text(label_start, "Start");
lv_obj_center(label_start);
// Create stop button
btn_stop = lv_btn_create(lv_scr_act());
lv_obj_set_size(btn_stop, 120, 50);
lv_obj_align(btn_stop, LV_ALIGN_CENTER, 80, 70);
lv_obj_add_event_cb(btn_stop, btn_stop_event_cb, LV_EVENT_ALL, NULL);
lv_obj_t *label_stop = lv_label_create(btn_stop);
lv_label_set_text(label_stop, "Stop");
lv_obj_center(label_stop);
// Create lottery task
xTaskCreate(lottery_task, "lottery", 2048, NULL, 5, NULL);
}
// ================= LVGL Tick Timer =================
static void lv_tick_cb(void *arg)
{
lv_tick_inc(1); // LVGL 1ms tick
}
// ======================================================
// Backlight control
// ======================================================
static void backlight_init(void)
{
gpio_config_t io = {
.pin_bit_mask = 1ULL << PIN_BL,
.mode = GPIO_MODE_OUTPUT
};
gpio_config(&io);
gpio_set_level(PIN_BL, 1);
}
// ======================================================
// MAIN ENTRY
// ======================================================
void app_main(void)
{
gpio_set_direction(PIN_DC, GPIO_MODE_OUTPUT);
gpio_set_direction(PIN_RST, GPIO_MODE_OUTPUT);
// ================= LCD SPI =================
spi_bus_config_t bus_lcd = {
.mosi_io_num = PIN_MOSI,
.miso_io_num = -1,
.sclk_io_num = PIN_CLK,
.max_transfer_sz = 1024 * 10
};
spi_bus_initialize(SPI2_HOST, &bus_lcd, SPI_DMA_CH_AUTO);
spi_device_interface_config_t dev_lcd = {
.clock_speed_hz = 20 * 1000 * 1000, // 20MHz
.mode = 0,
.spics_io_num = PIN_CS,
.queue_size = 7,
};
spi_bus_add_device(SPI2_HOST, &dev_lcd, &spi_lcd);
// ================= TOUCH SPI =================
spi_bus_config_t bus_tp = {
.mosi_io_num = TP_MOSI,
.miso_io_num = TP_MISO,
.sclk_io_num = TP_CLK,
.max_transfer_sz = 32
};
spi_bus_initialize(SPI3_HOST, &bus_tp, SPI_DMA_CH_AUTO);
spi_device_interface_config_t dev_tp = {
.clock_speed_hz = 2 * 1000 * 1000,
.mode = 0,
.spics_io_num = TP_CS,
.queue_size = 3,
};
spi_bus_add_device(SPI3_HOST, &dev_tp, &spi_tp);
gpio_set_direction(TP_IRQ, GPIO_MODE_INPUT);
// ================= LCD INIT =================
ili9488_init();
// ================= LVGL INIT =================
lv_init();
static lv_color_t *buf1;
buf1 = heap_caps_malloc(LCD_W * 20 * sizeof(lv_color_t), MALLOC_CAP_DMA);
static lv_disp_draw_buf_t draw_buf;
lv_disp_draw_buf_init(&draw_buf, buf1, NULL, LCD_W * 20);
// LVGL tick timer (1ms)
esp_timer_handle_t timer;
const esp_timer_create_args_t tick_args = {
.callback = &lv_tick_cb,
.name = "lv_tick"
};
esp_timer_create(&tick_args, &timer);
esp_timer_start_periodic(timer, 1000); // 1ms
static lv_disp_drv_t disp_drv;
lv_disp_drv_init(&disp_drv);
disp_drv.hor_res = LCD_W;
disp_drv.ver_res = LCD_H;
disp_drv.flush_cb = lcd_flush;
disp_drv.draw_buf = &draw_buf;
disp_drv.full_refresh = 1;
lv_disp_drv_register(&disp_drv);
static lv_indev_drv_t indev_drv;
lv_indev_drv_init(&indev_drv);
indev_drv.type = LV_INDEV_TYPE_POINTER;
indev_drv.read_cb = touch_read;
lv_indev_drv_register(&indev_drv);
ui_create(); // Create LVGL UI
// Create LVGL task handler in separate task
xTaskCreate(lvgl_task, "lvgl_task", 4096, NULL, 5, NULL);
// no flush operation here again
backlight_init(); // Initialize backlight here, by feixiaoxing
// Keep main task alive
while (1)
{
vTaskDelay(pdMS_TO_TICKS(100));
}
}
// ================= LVGL Task =================
static void lvgl_task(void *arg)
{
while (1)
{
lv_timer_handler(); // LVGL main loop
vTaskDelay(pdMS_TO_TICKS(10));
}
}
5、后续验证和测试
如之前所说,如果是纯lvgl的开发,可以先用windows vs2019 + lvgl8.3仿真好,再port到设备上面进行测试。哪怕是串口、232、485、can这些,其实也是可以通过serial做一个接口,先在windows开发好之后,再移植到设备,这样效率才是最高的。
软硬分离的好处,就是各司其职。如果软件规模不大,软硬件都是一个人来做,这种情况还可以接受。一旦软件规模超过一个人能力的范围,就必须要软硬分家了。