一、用8080接口驱动OLED屏幕
正点原子的0.96 OLED 默认设置的是8080接口,但是其ESP32S3开发板手册里教学使用OLED的时候,却建议更改电阻焊接方式,将屏幕调整成IIC接口。这个很不人性,太坑了。
于是我尝试使用U8g2库函数,也能成功驱动了屏幕。有几个要点记录一下:
1. U8g2主要调用这条语句来初始化
U8G2_SSD1306_128X64_NONAME_F_8080 u8g2(U8G2_R0, 4, 5, 6, 7, 15, 16, 17, 18, /*enable=*/48, /*cs=*/47, /*dc=*/38, /*reset=*/255);
2. 因为开发板的RESET引脚是通过xl9555扩展出来的,所以上面的函数中先设为255,之后在setup函数中手动添加语句进行复位。
void setup() {
xl9555_io_config(OV_RESET, OUTPUT);
/* OLED 模块的复位引脚连接到 XL9555 器件的 P05 引脚 */
xl9555_pin_set(OV_RESET, HIGH); /* 默认拉高复位引脚 */
/* 进行复位 */
OLED_RST(0);
delay(100);
OLED_RST(1);
delay(100);
u8g2.begin();
}

二、使用Platformio
用Arduino IDE查看函数定义时经常会找不到信息,而用vscode+Platformio就要方便很多。关于Platformio.ini的正确配置查了好就资料,总算调试成功了,记录如下:
[env:esp32s3]
platform = espressif32
board = esp32-s3-devkitc-1
framework = arduino
monitor_speed = 115200
monitor_port = COM5
monitor_rts = 0
monitor_dtr = 0
board_build.arduino.partitions = default_16MB.csv
build_flags = -DBOARD_HAS_PSRAM
board_build.arduino.memory_type = qio_opi
board_upload.flash_size = 16MB
lib_extra_dirs = ~/Documents/Arduino/libraries
targets = upload, monitor
三、LVGL
因为开发板配套的2.4寸spi屏幕不带触摸,所以通过按键实现lvgl的屏幕控制。程序如下:
#include <lvgl.h>
#include "xl9555.h"
#if LV_USE_TFT_ESPI
#include <TFT_eSPI.h>
#endif
#include <examples/lv_examples.h>
/*Set to your screen resolution and rotation*/
#define TFT_HOR_RES 240
#define TFT_VER_RES 320
#define TFT_ROTATION LV_DISPLAY_ROTATION_90
static lv_group_t *group;
/*LVGL draw into this buffer, 1/10 screen size usually works well. The size is in bytes*/
#define DRAW_BUF_SIZE (TFT_HOR_RES * TFT_VER_RES / 10 * (LV_COLOR_DEPTH / 8))
uint32_t draw_buf[DRAW_BUF_SIZE / 4];
// 全局变量:存储按钮对象
static lv_obj_t *btn1, *btn2, *btn3;
void btn_event_cb(lv_event_t *e) {
lv_event_code_t code = lv_event_get_code(e);
lv_obj_t *btn = (lv_obj_t *)lv_event_get_target(e);
lv_obj_t *label = lv_obj_get_child(btn, 0);
if (code == LV_EVENT_PRESSED) {
// 根据不同的按钮执行不同的操作
if (btn == btn1) {
Serial.println("Button 1 clicked!");
lv_label_set_text(label, "Btn1 Clicked!");
} else if (btn == btn2) {
Serial.println("Button 2 clicked!");
lv_label_set_text(label, "Btn2 Clicked!");
} else if (btn == btn3) {
Serial.println("Button 3 clicked!");
lv_label_set_text(label, "Btn3 Clicked!");
}
}
if(code == LV_EVENT_RELEASED) {
// 恢复按钮颜色
if (btn == btn1) {
lv_label_set_text(label, "Button 1");
lv_obj_set_style_bg_color(btn, lv_palette_main(LV_PALETTE_BLUE), 0);
} else if (btn == btn2) {
lv_label_set_text(label, "Button 2");
lv_obj_set_style_bg_color(btn, lv_palette_main(LV_PALETTE_BLUE), 0);
} else if (btn == btn3) {
lv_label_set_text(label, "Button 3");
lv_obj_set_style_bg_color(btn, lv_palette_main(LV_PALETTE_BLUE), 0);
}
}
}
void create_ui() {
// 创建第一个按钮
btn1 = lv_button_create(lv_screen_active());
lv_obj_set_size(btn1, 100, 40);
lv_obj_set_pos(btn1, 20, 20);
lv_obj_set_style_bg_color(btn1, lv_palette_main(LV_PALETTE_BLUE), 0);
lv_obj_set_style_bg_color(btn1, lv_palette_main(LV_PALETTE_RED), LV_STATE_PRESSED);
lv_obj_set_style_radius(btn1, 10, 0);
lv_obj_t *label1 = lv_label_create(btn1);
lv_label_set_text(label1, "Button 1");
lv_obj_center(label1);
lv_obj_add_event_cb(btn1, btn_event_cb, LV_EVENT_ALL, NULL);
// 创建第二个按钮
btn2 = lv_button_create(lv_screen_active());
lv_obj_set_size(btn2, 100, 40);
lv_obj_set_pos(btn2, 20, 80);
lv_obj_set_style_bg_color(btn2, lv_palette_main(LV_PALETTE_BLUE), 0);
lv_obj_set_style_bg_color(btn2, lv_palette_main(LV_PALETTE_GREEN), LV_STATE_PRESSED);
lv_obj_set_style_radius(btn2, 10, 0);
lv_obj_t *label2 = lv_label_create(btn2);
lv_label_set_text(label2, "Button 2");
lv_obj_center(label2);
lv_obj_add_event_cb(btn2, btn_event_cb, LV_EVENT_ALL, NULL);
// 创建第三个按钮
btn3 = lv_button_create(lv_screen_active());
lv_obj_set_size(btn3, 100, 40);
lv_obj_set_pos(btn3, 20, 140);
lv_obj_set_style_bg_color(btn3, lv_palette_main(LV_PALETTE_BLUE), 0);
lv_obj_set_style_bg_color(btn3, lv_palette_main(LV_PALETTE_YELLOW), LV_STATE_PRESSED);
lv_obj_set_style_radius(btn3, 10, 0);
lv_obj_t *label3 = lv_label_create(btn3);
lv_label_set_text(label3, "Button 3");
lv_obj_center(label3);
lv_obj_add_event_cb(btn3, btn_event_cb, LV_EVENT_ALL, NULL);
// 启用按键导航
group = lv_group_create();
// 设置组属性
lv_group_set_wrap(group, true); // 启用循环导航
// 将按钮添加到组中(按顺序)
lv_group_add_obj(group, btn1);
lv_group_add_obj(group, btn2);
lv_group_add_obj(group, btn3);
// 设置第一个按钮为初始焦点
lv_group_focus_obj(btn1);
// 设置按钮的聚焦样式
lv_obj_set_style_border_color(btn1, lv_palette_main(LV_PALETTE_RED), LV_STATE_FOCUSED);
lv_obj_set_style_border_width(btn1, 3, LV_STATE_FOCUSED);
lv_obj_set_style_border_color(btn2, lv_palette_main(LV_PALETTE_RED), LV_STATE_FOCUSED);
lv_obj_set_style_border_width(btn2, 3, LV_STATE_FOCUSED);
lv_obj_set_style_border_color(btn3, lv_palette_main(LV_PALETTE_RED), LV_STATE_FOCUSED);
lv_obj_set_style_border_width(btn3, 3, LV_STATE_FOCUSED);
// 添加说明标签
lv_obj_t *info_label = lv_label_create(lv_screen_active());
lv_label_set_text(info_label, "Use UP/DOWN to navigate\nPress ENTER to select");
lv_obj_set_pos(info_label, 20, 200);
Serial.println("UI created with 3 buttons");
}
/* 改进的按键读取函数 - 在回调中直接处理焦点转移 */
void my_keypad_read(lv_indev_t *indev, lv_indev_data_t *data) {
static unsigned long last_press_time = 0;
static uint32_t last_key = 0;
static bool key_down = false;
// 读取按键状态
bool key0_pressed = (xl9555_get_pin(KEY0) == 0);
bool key1_pressed = (xl9555_get_pin(KEY1) == 0);
bool key2_pressed = (xl9555_get_pin(KEY2) == 0);
bool key3_pressed = (xl9555_get_pin(KEY3) == 0);
// 检查是否有按键按下
bool any_key_pressed = key0_pressed || key1_pressed || key2_pressed || key3_pressed;
// 去抖动处理 - 200ms 去抖动时间
unsigned long now = millis();
if (any_key_pressed != key_down) {
if (now - last_press_time > 200) { // 去抖动延迟
key_down = any_key_pressed;
last_press_time = now;
if (key_down) {
// 按键按下
if (key1_pressed) {
// ENTER 键 - 发送点击事件
data->state = LV_INDEV_STATE_PRESSED;
data->key = LV_KEY_ENTER;
last_key = LV_KEY_ENTER;
Serial.println("ENTER key pressed (will trigger click)");
}
else if (key0_pressed) {
// DOWN 键 - 移动焦点到下一个
if (group) {
lv_group_focus_next(group);
Serial.println("DOWN key pressed - moved focus to next");
}
// 我们不需要发送按键事件给LVGL,因为我们手动处理了焦点
data->state = LV_INDEV_STATE_PRESSED;
data->key = 0; // 设置为0,表示不处理其他操作
last_key = 0;
}
else if (key2_pressed) {
// UP 键 - 移动焦点到上一个
if (group) {
lv_group_focus_prev(group);
Serial.println("UP key pressed - moved focus to previous");
}
data->state = LV_INDEV_STATE_PRESSED;
data->key = 0; // 设置为0,表示不处理其他操作
last_key = 0;
}
else if (key3_pressed) {
// ESC 键
data->state = LV_INDEV_STATE_PRESSED;
data->key = LV_KEY_ESC;
last_key = LV_KEY_ESC;
Serial.println("ESC key pressed");
}
} else {
// 按键释放
data->state = LV_INDEV_STATE_RELEASED;
data->key = last_key;
Serial.println("Key released");
}
} else {
// 在去抖动期间,保持状态不变
if (key_down) {
data->state = LV_INDEV_STATE_PRESSED;
data->key = last_key;
} else {
data->state = LV_INDEV_STATE_RELEASED;
data->key = 0;
}
}
} else {
// 按键状态没有变化
if (key_down) {
data->state = LV_INDEV_STATE_PRESSED;
data->key = last_key;
} else {
data->state = LV_INDEV_STATE_RELEASED;
data->key = 0;
}
}
}
/*use Arduinos millis() as tick source*/
static uint32_t my_tick(void) {
return millis();
}
void setup() {
Serial.begin(115200);
Serial.println("Starting setup...");
xl9555_init();
xl9555_io_config(KEY0 | KEY1 | KEY2 | KEY3, INPUT);
/* 初始化 LCD 屏需要用到的引脚 */
xl9555_io_config(SLCD_PWR, OUTPUT);
xl9555_io_config(SLCD_RST, OUTPUT);
lv_init();
/*Set a tick source so that LVGL will know how much time elapsed. */
lv_tick_set_cb(my_tick);
lv_display_t *disp = lv_tft_espi_create(TFT_HOR_RES, TFT_VER_RES, draw_buf, sizeof(draw_buf));
lv_display_set_rotation(disp, TFT_ROTATION);
/*Initialize the (dummy) input device driver*/
lv_indev_t *indev = lv_indev_create();
lv_indev_set_type(indev, LV_INDEV_TYPE_KEYPAD);
lv_indev_set_read_cb(indev, my_keypad_read);
create_ui();
// 将输入设备与组关联
lv_indev_set_group(indev, group);
}
void loop() {
lv_timer_handler(); /* let the GUI do its work */
delay(5); /* let this time pass */
}
四、SD卡
这里遇到一个巨坑。我在sdcard.cpp文件中写了SD卡的初始化函数。一开始将SPIClass在函数内作为局部变量声明。
//重要,一定要将SPIClass设为全局变量,并在头文件中extern
SPIClass sdcard_spi;
void sdcard_init(void)
{
sdcard_spi.begin(SD_SCK, SD_MISO, SD_MOSI, SD_CS);
if (!SD.begin(SD_CS, sdcard_spi))
{
Serial.println("Card Mount Failed");
return;
}
}
然后在lv_conf.h中将Arduino对于SD卡的支持打开
/*API for Arduino LittleFs. */
#define LV_USE_FS_ARDUINO_ESP_LITTLEFS 1
#if LV_USE_FS_ARDUINO_ESP_LITTLEFS
#define LV_FS_ARDUINO_ESP_LITTLEFS_LETTER 'E' /*Set an upper cased letter on which the drive will accessible (e.g. 'A')*/
#endif
/*API for Arduino Sd. */
#define LV_USE_FS_ARDUINO_SD 1
#if LV_USE_FS_ARDUINO_SD
#define LV_FS_ARDUINO_SD_LETTER 'A' /*Set an upper cased letter on which the drive will accessible (e.g. 'A')*/
#endif
/*LODEPNG decoder library*/
#define LV_USE_LODEPNG 1
/*BMP decoder library*/
#define LV_USE_BMP 1
/* JPG + split JPG decoder library.
* Split JPG is a custom format optimized for embedded systems. */
#define LV_USE_TJPGD 1
在ino主文件调用sdcard_init()函数后,调用lv_image_set_src(bg_img, "A:/background.bin");来设置背景图片。结果程序一直重启。我怀疑过是不是内存不够,SD卡初始化语句的位置是不是不对等等原因,卡了三天。最后发现是因为没有把SPIClass声明为全局变量,原因是:
-
SD库在begin()时,内部保存了传入的SPIClass对象的引用。它记住了这个SPI通道,以便后续所有读写操作(open、read、seek等)都通过它来与硬件通信。 -
当sdcard_init()函数结束,栈内存被释放,这个指针就变成了“悬空指针”。下次调用
SD.open(),库通过这个无效指针去访问SPI硬件,导致内存访问违规,系统崩溃重启。
五、图片加载
图片可以通过多种方式加载。包括直接保留bmp、png、jpg原格式,从littlefs和sd卡加载;也可以通过lvgl的图形转换工具先转为bin文件后再加载;还可以转为c文件后加载。实测下来,直接通过原格式加载,由于涉及到解码需要占用大量内存,对图片大小的要求很严格,分辨率稍微大点的图片就会很慢或失败。而转为c文件不方便放到littlefs或sd卡中。还是先转为bin文件后加载效果最好。
bool load_background()
{
// 方法一:从文件系统加载图像文件
if (!LittleFS.exists("/background.bin"))
{
Serial.println("背景图片文件不存在");
return false;
}
Serial.println("找到背景图片");
bg_img = lv_image_create(lv_screen_active());
lv_image_set_src(bg_img, "E:/background.bin");
return true;
// 方法二:从SD卡加载图像文件
if (!SD.exists("/background.bin"))
{
Serial.println("背景图片文件不存在");
return false;
}
Serial.println("找到背景图片");
bg_img = lv_image_create(lv_screen_active());
lv_image_set_src(bg_img, "A:/background.bin");
return true;
// 方法三:使用C语言方式声明图像
LV_IMG_DECLARE(background);
bg_img = lv_image_create(lv_screen_active());
lv_image_set_src(bg_img, &background); // 使用声明的图像
return true;
}
六、驱动4.3寸RGB触摸屏
利用esp32_display_panel这个库文件的lcd_single_rgb和touch_i2c官方示例,稍微改一下引脚和屏幕参数,再结合lvgl库的官方示例就能比较容易地驱动正点原子的4.3寸800X480的RGB触摸屏。
#include "display.h"
#include "touch.h"
#include <lvgl.h>
#include <examples/lv_examples.h>
//#include <demos/lv_demos.h>
/*Set to your screen resolution and rotation*/
#define TFT_HOR_RES 800
#define TFT_VER_RES 480
#define TFT_ROTATION LV_DISPLAY_ROTATION_0
/*LVGL draw into this buffer, 1/10 screen size usually works well. The size is in bytes*/
#define DRAW_BUF_SIZE (TFT_HOR_RES * TFT_VER_RES / 20 * (LV_COLOR_DEPTH / 8))
uint32_t draw_buf[DRAW_BUF_SIZE / 4];
/* LVGL calls it when a rendered image needs to copied to the display*/
void my_disp_flush(lv_display_t *disp, const lv_area_t *area, uint8_t *px_map) {
uint32_t w = lv_area_get_width(area);
uint32_t h = lv_area_get_height(area);
bool success = display->drawBitmap(
area->x1, // 起始 X 坐标
area->y1, // 起始 Y 坐标
w, // 区块宽度
h, // 区块高度
px_map, // LVGL 渲染好的像素数据
0 // 非阻塞模式(根据库说明)
);
/*Call it to tell LVGL you are ready*/
lv_display_flush_ready(disp);
}
/*Read the touchpad*/
void my_touchpad_read(lv_indev_t *indev, lv_indev_data_t *data) {
touch->readRawData(1, 0, EXAMPLE_TOUCH_READ_PERIOD_MS);
std::vector<TouchPoint> points;
touch->getPoints(points);
if (points.empty()) {
data->state = LV_INDEV_STATE_RELEASED;
} else {
data->state = LV_INDEV_STATE_PRESSED;
data->point.x = points[0].x;
data->point.y = points[0].y;
Serial.printf("Touch point: x %d, y %d, strength %d\n", points[0].x, points[0].y, points[0].strength);
}
}
/*use Arduinos millis() as tick source*/
static uint32_t my_tick(void) {
return millis();
}
void setup() {
String LVGL_Arduino = "Hello Arduino! ";
LVGL_Arduino += String('V') + lv_version_major() + "." + lv_version_minor() + "." + lv_version_patch();
Serial.begin(115200);
Serial.println(LVGL_Arduino);
display_init();
touch_init();
lv_init();
/*Set a tick source so that LVGL will know how much time elapsed. */
lv_tick_set_cb(my_tick);
lv_display_t *disp;
/*create a display yourself*/
disp = lv_display_create(TFT_HOR_RES, TFT_VER_RES);
lv_display_set_flush_cb(disp, my_disp_flush);
lv_display_set_buffers(disp, draw_buf, NULL, sizeof(draw_buf), LV_DISPLAY_RENDER_MODE_PARTIAL);
/*Initialize the (dummy) input device driver*/
lv_indev_t *indev = lv_indev_create();
lv_indev_set_type(indev, LV_INDEV_TYPE_POINTER); /*Touchpad should have POINTER type*/
lv_indev_set_read_cb(indev, my_touchpad_read);
// lv_obj_t *label = lv_label_create(lv_screen_active());
// lv_label_set_text(label, "Hello Arduino, I'm LVGL!");
// lv_obj_align(label, LV_ALIGN_CENTER, 0, 0);
lv_example_anim_1();
Serial.println("Setup done");
}
void loop() {
lv_timer_handler(); /* let the GUI do its work */
delay(5); /* let this time pass */
}
1412

被折叠的 条评论
为什么被折叠?



