本帖最后由 仔爸 于 2025-10-20 11:04 编辑
最近一直想学习使用esp32等MCU微控制器来驱动显示屏(包括液晶显示屏和墨水屏),趁着DFROBOT开展的FireBeetle 2 ESP32 C5评测项目,来尝试一下。
不过由于FireBeetle 2 ESP32 C5没有官方推出的micropython固件,因此刚拿到手的时候,这块C5真的像块砖头,在我手里活不起来。得益于PY学习笔记(苦行僧)自行编译的集成了不同显示屏驱动与LVGL轻量级通用图形库,让我可以直接拿来烧录使用。这里有一篇他的文章,我看了收获很多,大家也可以参考:[尝鲜DFRobot ESP32-C5开发板,玩转LVGL Micropython](https://mc.dfrobot.com.cn/thread-396470-1-1.html)
一、硬件对比
首先我们可以看一下esp32, esp32-c6和esp32-c5的比较,通过询问豆包,给出如下结果:ESP32 与 ESP32 C5、C6 对比:https://www.doubao.com/thread/wed26643be24cbd4a),给出的结论是:初代ESP32 主打性能与兼容性,C6 强化无线能力(双频段 + 音频),C5 则是低成本简化版,按需选择即可。
二、烧录固件
在esp32 c5上烧录micropython固件也是走了一点弯路,不能用普通的方法进行烧录,比如直接用thonny软件烧录本地固件,以及使用flash download tool,对于FireBeetle 2 ESP32 C5来说都是有问题的,据DFROBOT的刘工(DF刘忠渝)说是因为如下原因(在测试的时候,确实也出现了内存错误,估计是内存小的原因):
- ESP32-C5是revision v0.1版本的 烧录固件一定要用压缩包里的flash download软件
- C5 RAM有限。一些micropython示例会提示内存分配失败
- 还有3块屏幕的驱动文件没有添加进去 。硬件发出前会添加进去
这是他提供的烧录文件及适配各种屏幕的固件包(烧录工具: flash_download_tool.zip包含st7789显示驱动的固件: ESP32-C5(N4)(revision v0.1)-st7789固件.rar)
使用压缩包里的工具烧录还是挺简单的,注意烧录地址是0x2000,不要搞错了。
三、项目实施
这是项目运行后的效果:

1. 硬件连接
固件烧录成功之后,进行了一些基础的点LED之类的,都没有问题,接下来就要尝试连接屏幕了。手头还是有多块屏,而且提供的固件也比较全,我就选了一块1.54英寸,分辨率是240x240的8针屏幕(不带触摸),通过SPI方式通信。
将屏幕的通信引脚与FireBeetle 2 ESP32 C5扩展板的IO进行链接,如下表所示,注意VCC接3.3V而不要接5V(查资料别人都这么说,我也不敢接5V,万一屏幕废了呢):

2. 初步探索
打开Thonny软件,用USB线连接C5,进入Micropython编程模式。在Thonny底部的终端窗口输入如下命令查看一下固件集成了哪些模块:
复制代码
显示如下图所示内容,其中发现st7789屏幕驱动和LVGL库(版本9.3.0)都在。

赶紧查看一下st7789里面有些啥,是不是有ST7789类,

点底部一行最右边的三个小点,看一下ST7789类里面具体有哪些方法,如下所示,好像没有高级一点的绘图方法可用,这就坚定了我尝试学习和使用LVGL的决心。
- ['__class__', '__init__', '__module__', '__name__', '__qualname__', '__bases__', '__del__', '__dict__', 'add_event_cb', 'delete', 'delete_event', 'delete_refr_timer', 'enable_invalidation', 'get_antialiasing', 'get_color_format', 'get_dpi', 'get_event_count', 'get_event_dsc', 'get_horizontal_resolution', 'get_inactive_time', 'get_layer_bottom', 'get_layer_sys', 'get_layer_top', 'get_next', 'get_offset_x', 'get_offset_y', 'get_physical_horizontal_resolution', 'get_physical_vertical_resolution', 'get_refr_timer', 'get_rotation', 'get_screen_active', 'get_screen_prev', 'get_theme', 'get_vertical_resolution', 'init', 'is_double_buffered', 'is_invalidation_enabled', 'reset', 'send_event', 'set_antialiasing', 'set_color_format', 'set_default', 'set_dpi', 'set_offset', 'set_physical_resolution', 'set_rotation', 'set_theme', 'trigger_activity', '_INVOFF', '_INVON', '_ORIENTATION_TABLE', '_displays', '_dummy_set_memory_location', '_flush_cb', '_flush_ready_cb', '_init_bus', '_madctl', '_on_size_change', '_set_memory_location', 'get_backlight', 'get_default', 'get_displays', 'get_params', 'get_power', 'set_backlight', 'set_color_inversion', 'set_params', 'set_physical_horizontal_resolution', 'set_physical_vertical_resolution', 'set_power']
复制代码
3. 对LVGL的学习尝试
之前一直有听说过LVGL这个东东,但一直没有尝试,毕竟在代码编程中进行界面布局是一件令人抓狂的事情,这也是N年以前我使用C等语言编写界面遭遇滑铁卢而接触到Delphi软件就觉得图形化布局非常方便的感受,后来无论是Word中的控件,还是网页前端,都希望这些界面控件能拖拖拉拉放放,再修改一下属性参数就搞定。
当然我希望LVGL也是这样的,结果……居然要纯代码,郁闷中。
而且自从我烧录了带有LVGL的micropython固件,网上找了一圈,资料最多的当然是C代码,毕竟micropython中的LVGL是从C移植过来的。
这里有一个LVGL中文手册的网站(https://lvgl.100ask.net/master/index.html),大家可以一起学习,不过最新版本的LVGL很少提供micropython代码,只提供C代码,应该是8.X版本的手册内容同时提供C和mpy代码。不过我测试发现,LVGL 8.X和9.X很多代码规则和格式都发生了变化(社区太强,更新太快,学习成本大大增加啊)
一通尝试并且遇到诸多困难之后,倒是没有学到多少内容,但收集资料倒是不少, 同时在github上follow了不少大牛,留着以后学习时参考。
4. 牛刀小试
接下来,我尝试使用esp32 c5在st7789驱动的屏幕上显示当地的天气内容,参考了github上大牛的一些代码。
- 上传到esp32 c5上的文件结构(所有文件在文末压缩包提供)


- import lcd_bus
- import fs_driver
- from micropython import const
- import machine
- import urequests
- import lcd_utils
- from display_driver import init_display
- import time
- import json
- import network
- import lvgl as lv
- # import math
- import task_handler
-
- # 初始化WiFi
- sta_if = network.WLAN(network.STA_IF)
- sta_if.active(False)
- SSID = "wifi名称" # 更换为自己的wifi名
- PASSWORD = "wifi密码" # 更换为wifi密码
-
- city = "ningbo" # 设置要显示天气的城市
- '''
- weather_dict = {'0@1x.png':'晴','1@1x.png':'晴','2@1x.png':'晴',
- '3@1x.png':'晴','4@1x.png':'多云','5@1x.png':'晴间多云',
- '6@1x.png':'晴间多云','7@1x.png':'大部多云','8@1x.png':'大部多云',
- '9@1x.png':'阴','10@1x.png':'阵雨','11@1x.png':'雷阵雨',
- '12@1x.png':'雷阵雨伴有冰雹','13@1x.png':'小雨','14@1x.png':'中雨',
- '15@1x.png':'大雨','16@1x.png':'暴雨','17@1x.png':'大暴雨',
- '18@1x.png':'特大暴雨','19@1x.png':'冻雨','20@1x.png':'雨夹雪',
- '21@1x.png':'阵雪','22@1x.png':'小雪','23@1x.png':'中雪',
- '24@1x.png':'大雪','25@1x.png':'暴雪','26@1x.png':'浮尘',
- '27@1x.png':'扬沙','28@1x.png':'沙尘暴','29@1x.png':'强沙尘暴',
- '30@1x.png':'雾','31@1x.png':'霾','32@1x.png':'风',
- '33@1x.png':'大风','34@1x.png':'飓风','35@1x.png':'热带风暴',
- '36@1x.png':'龙卷风','37@1x.png':'冷','38@1x.png':'热',
- '99@1x.png':'未知'}
- '''
- weather_url = f"https://api.seniverse.com/v3/weather/now.json?key=S9hoa4Wza9Hcs2uX_&location={city}&language=zh-Hans&unit=c"
-
- def fetchWeather():
- try:
- result = urequests.get(weather_url)
- print(result.text)
- return result.text
- except Exception as e:
- print("Weather fetch error:", e)
- return None
-
- def connect_wifi():
- if not sta_if.isconnected():
- print("Connecting to WiFi...")
- sta_if.active(True)
- sta_if.connect(SSID, PASSWORD)
- max_wait = 10
- while max_wait > 0:
- if sta_if.isconnected():
- print("WiFi Connected. IP:", sta_if.ifconfig()[0])
- return True
- max_wait -= 1
- time.sleep(1)
- print("WiFi Connection Failed")
- return False
- return True
-
- # 更新天气信息并分别存入对应的变量中
- def update_weather(e):
- if connect_wifi():
- weather_data = fetchWeather()
- if weather_data:
- try:
- weather_json = json.loads(weather_data)
- weather = weather_json["results"][0]["now"]["text"]
- temp = weather_json["results"][0]["now"]["temperature"]
- weather_image_keys = f"{weather_json["results"][0]["now"]["code"]}@1x.png"
- incity=weather_json["results"][0]["location"]["name"]
- last_update=weather_json["results"][0]["last_update"]
-
- #ui_label1.set_text(incity)
- ui_label2.set_text(f"{temp}°C")
- ui_label3.set_text(weather)
- ui_label4.set_text(last_update)
-
- try:
- with open(f'weather_incons/{weather_image_keys}', 'rb') as f:
- png_data = f.read()
- img_cogwheel = lv.image_dsc_t({'data_size': len(png_data), 'data': png_data})
- img1.set_src(img_cogwheel)
- except Exception as e:
- print(f"Could not load weather icon {weather_image_keys}: {e}")
- ui_label3.set_text("Icon Load Error")
- except Exception as e:
- print(f"Weather JSON parse error: {e}")
- ui_label3.set_text("Weather Parse Error")
- else:
- ui_label3.set_text("Weather Fetch Failed")
- else:
- ui_label.set_text("WiFi Not Connected")
-
- def SetFlag( obj, flag, value):
- if (value):
- obj.add_flag(flag)
- else:
- obj.remove_flag(flag)
- return
-
- # 连接wifi
- connect_wifi()
- # 初始化LVGL
- lv.init()
-
- # 初始化显示屏
- display = init_display()
- display.set_power(False)
- display.init()
- display.set_color_inversion(True)
- display.set_rotation(lv.DISPLAY_ROTATION._0)
- display.set_backlight(1)
-
- #th = task_handler.TaskHandler()
-
- # 初始化LVGL
- scr = lv.screen_active()
- scr.set_style_bg_color(lv.color_hex(0x000000), 0)
-
-
-
- # 读取字体(提前上传到esp32中)
- fs_drv = lv.fs_drv_t()
- fs_driver.fs_register(fs_drv, 'S')
- myfont = lv.binfont_create("S:myfont_18.bin")
-
-
- # 获取天气数据
- weather_data = fetchWeather()
- temp, weather, weather_image_keys,incity,last_update= "N/A", "Unknown", '99@1x.png',"Unknown","Unknown"
- if weather_data:
- try:
- weather_json = json.loads(weather_data)
- weather = weather_json["results"][0]["now"]["text"]
- temp = weather_json["results"][0]["now"]["temperature"]
- weather_image_keys = f"{weather_json["results"][0]["now"]["code"]}@1x.png"
- print(weather)
- except Exception as e:
- print(f"Initial weather JSON parse error: {e}")
-
- # 读入天气图标并显示
- try:
- with open(f'weather_incons/{weather_image_keys}', 'rb') as f:
- png_data = f.read()
- img_cogwheel_argb = lv.image_dsc_t({
- 'data_size': len(png_data),
- 'data': png_data
- })
- except:
- print(f"Could not find weather_incons/{weather_image_keys}")
- # Create a fallback image or handle the error appropriately
- img_cogwheel_argb = None
-
-
- img1 = lv.image(scr)
- if img_cogwheel_argb:
- img1.set_src(img_cogwheel_argb)
- img1.align(lv.ALIGN.CENTER, 0, -25)
- img1.set_size(58, 58)
-
-
- # 显示城市(拼音)
- ui_label1 = lv.label(scr)
- ui_label1.set_text(city.upper())
- ui_label1.set_style_text_color(lv.color_hex(0x00FF00), 0)
- ui_label1.align(lv.ALIGN.CENTER, 0, -80)
- ui_label1.set_style_text_font(myfont, 0)
-
- # 显示温度
- ui_label2 = lv.label(scr)
- ui_label2.set_text(f"{temp}°C")
- ui_label2.set_style_text_color(lv.color_hex(0x00FF00), 0)
- ui_label2.align(lv.ALIGN.CENTER, 0, 30)
- ui_label2.set_style_text_font(myfont, 0)
-
- # 显示天气
- ui_label3 = lv.label(scr)
- ui_label3.set_text(weather)
- ui_label3.set_style_text_color(lv.color_hex(0xFFFF00), 0)
- ui_label3.align(lv.ALIGN.CENTER, 0, 10)
- ui_label3.set_style_text_font(myfont, 0)
-
- # 显示最后更新时间
- ui_label4 = lv.label(scr)
- ui_label4.set_text(last_update)
- ui_label4.set_style_text_color(lv.color_hex(0xFFFF00), 0)
- ui_label4.align(lv.ALIGN.CENTER, 0, 100)
- ui_label4.set_style_text_font(myfont, 0)
-
- # 天气3小时更新一次
- weather_timer = lv.timer_create(update_weather, 10800000, None)
-
- # 更新一次信息
- update_weather(None)
-
-
- # --- Main Loop ---
- while True:
- lv.tick_inc(5)
- #lv.task_handler()
- time.sleep_ms(5)
复制代码
四、反思
对于LVGL进行编程还不是很熟悉,发现之前我是通过framebuf等方式实现绘图,速度较慢,而LVGL中又有多种不同的显示方式,如对象类型,画布类型等。我尝试了画布类型,发现问题较多,主要是没有有效的参考文档,全靠瞎找。接下来我会继续学习,期待与大家有更多的交流。
esp32-c5程序资料: esp32-c5网络天气资料.zip
|