ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

ESP32+MicroPython实现LCD显示图片文字与B站粉丝数全攻略

ESP32+MicroPython实现LCD显示图片文字与B站粉丝数全攻略

1. 项目整体设计与思路拆解

1.1 从标题拆解核心需求

看到“ESP32_Py_Board LCD显示图片、文字、B站粉丝数”这个标题时,我第一反应是“这又是一个桌面小摆件的典型需求”。它本质上是三合一:本地数据展示(图片和文字)加上网络数据拉取(粉丝数)。ESP32是主控,Py_Board意味着你大概率拿到的是一块专门为MicroPython优化的开发板,外挂一块LCD屏幕,然后写一个脚本,让屏幕既能显示静态资源,又能在联网后定时刷新粉丝数。

这类项目的核心痛点在于:大部分新手把例程拼起来时,会发现“图片能显示但颜色不对”“文字全是方块”“粉丝数刷不出来”,最后卡在细节上。所以我决定把整个过程拆开,从硬件选型到编码实现再到踩坑排查,一步步讲清楚,是我自己实测过的方案,直接用就行。

1.2 为什么选ESP32与MicroPython方案

先回答最基础的问题:为什么选ESP32,而不是Arduino Uno或者树莓派Pico?因为ESP32自带Wi-Fi和蓝牙,频率足够跑图片解码,而且MicroPython可以直接用urequests发HTTP请求,不需要像C语言那样一点点封装。Py_Board这个命名通常说明固件已经预装了MicroPython,你只需要用USB连上电脑,打开Thonny就能写Python。

MicroPython的劣势是性能不如C,但在这个场景里完全够用。显示一张320x240的图片,用ILI9341这种SPI接口的LCD,刷新一帧大概几十毫秒到一百毫秒,完全不影响用户体验。如果换成树莓派Pico,你得自己搞定网络,反而更麻烦。

我推荐的具体组合是:ESP32 DevKit V1(或类似带USB转串口的板子)加上一块带IL9341控制器的2.2寸或者2.4寸SPI LCD屏。这种屏在淘宝上十几二十块钱就能买到,带触摸不带触摸都无所谓,我们只用来显示。

1.3 整体思路:本地资源加网络数据

整个项目可以分成四层:硬件层、驱动层、应用逻辑层、数据层。硬件层就是ESP32加LCD,驱动层负责把像素怼到屏幕上,应用逻辑层决定什么时候显示什么内容,数据层负责从B站拉粉丝数。

我用一张思维图来帮助理解(这里用文字描述):屏幕底部是一张背景图,中间区域显示文字“Hello ESP32”,右上角是粉丝数,每隔30秒自动刷新一次。图片文件放在文件系统的/images目录下,字体文件放在/fonts目录下,代码本身放在主目录的main.py里。

这样设计的好处是:图片和字体不占代码空间,可以随时用文件管理器替换;网络请求单独封装成一个函数,即使断网也不影响屏幕已经显示的静态内容。

2. 核心细节解析与实操要点

2.1 LCD型号与接线方式

市面上常见的LCD模块,驱动芯片不外乎ILI9341、ST7735、ST7789。我强烈建议选ILI9341,因为MicroPython生态里对它的支持最成熟,而且色深是16位,显示效果比ST7735系列好不少。ST7735一般是1.8寸小屏,像素点少,放中文文字会很挤。

接线表我直接给你(以我用的板子为例):

LCD引脚ESP32引脚
VCC3V3
GNDGND
CSGPIO5
RESETGPIO18
DCGPIO19
SDI(MOSI)GPIO23
SCKGPIO18 (注意和RESET复用?一定要查你的板子,通常用GPIO18)
LED3V3(或GPIO控制背光)

这里有个非常坑的点:很多ESP32开发板的引脚标注用的是GPIO编号,而部分LCD模块的丝印用的是SPI的默认名称(比如MOSI、SCK),你很容易搞混。我用的是某国产开发板,明确对照了原理图才没烧掉。

2.2初始化LCD的超容易踩的坑

在MicroPython中初始化ILI9341常用st7789或ili9341库,如果固件自带只能用machine.SPI和machine.Pin手动初始化。

先看初始化代码:

from machine import Pin, SPI import ili9341 spi = SPI(2, baudrate=40000000, sck=Pin(18), mosi=Pin(23)) cs = Pin(5, Pin.OUT) dc = Pin(19, Pin.OUT) rst = Pin(21, Pin.OUT) # 用哪个GPIO都行,只要对应 display = ili9341.ILI9341(spi, cs=cs, dc=dc, rst=rst, w=320, h=240, r=0) display.fill(display.color(255, 255, 255))

注意r=0是旋转角度,如果你的屏幕显示方向不对,改成r=1或r=2试试。我一开始忘了设置baudrate,默认32MHz也能跑,但提高到40MHz后刷新速度明显提升,字体滚动也没那么卡。

2.3 图片显示:格式转换与内存管理

MicroPython读取图片的常规操作是先把图片转成RGB565格式的字节流,再一次性写入屏幕。很多人直接加载JPEG,结果报MemoryError,原因很简单:JPEG解码需要的内存对ESP32来说太紧张了。

我实测好用的路径是:在电脑上把图片统一转成320x240的BMP格式,再用imagemagick或在线工具转成565的RGB数组存成.dat文件。这个方法有点笨,但是很稳定,尤其是你想让屏幕显示固定背景图的时候。

如果你用的是st7789驱动,很多库内置了jpg解码插件,可以用st7789.jpg方法显示JPEG,但同样需要处理大图。我的建议是无论如何都把图片缩放到屏幕分辨率,再用工具压成565格式,运行时的内存占用能低到5KB以内。

2.4 文字显示:字体制作与中文支持

LCD显示文字有两种方式:一种是直接用MicroPython内置的glcd字体,缺点是只有英文数字;另一种是加载自定义字体文件。我使用的是freesans或者simhei字体,通过一个工具把字体转成.py或.fnt文件,然后在代码里用glcd或ucfont库渲染。

中文文字的难点在于字库文件较大,一般10x10的GB2312字体也要几十KB,放不下。我的做法是:把需要的几十个常用字做成一张位图,然后通过坐标偏移索引到对应汉字,这样可以显著缩小体积。如果你只需要固定显示“粉丝数”三个字和数字,这个方案最节省内存。

具体操作:用font2bitmap工具生成font_lcd.py,里面包含了每个字符的像素数组。代码示例:

from font_lcd import font display.text(font, "Hello", 10, 10, display.color(0,0,0))

如果你的库不支持这个API,就退回像素绘制:

def draw_char(font, char, x, y, color): for row in font[char]: for i, bit in enumerate(row): if bit: display.pixel(x+i, y+idx, color)

这个方法虽然慢,但可控性强,而且对于固定文本完全够用。

2.5 B站粉丝数获取:API请求与解析

B站有一个公开接口可以直接获取用户的主页数据,无需登录,就是https://api.bilibili.com/x/relation/stat?vmid=你的UID。返回的JSON里包含data.follower字段,就是粉丝数。

注意这里有一个坑:B站的接口有风控,频繁请求会被封IP,所以一定不能每秒刷一次,建议至少间隔30秒或者60秒。而且请求时最好带上User-Agent,否则很容易被拦截。

在MicroPython里用urequests发请求:

import urequests import ujson def get_fans(uid): url = "https://api.bilibili.com/x/relation/stat?vmid={}".format(uid) headers = {"User-Agent": "Mozilla/5.0"} try: resp = urequests.get(url, headers=headers, timeout=10) data = ujson.loads(resp.text) resp.close() return data["data"]["follower"] except Exception as e: print("Error:", e) return None

注意一定要resp.close(),否则MicroPython的socket资源会泄漏,跑几次内存就爆了。

3. 实操过程与核心环节实现

3.1 环境准备与固件烧录

第一步是用USB线连接板子,安装CP210x或CH340驱动,然后打开Thonny(如果你没有用Thonny,任何MicroPython IDE都行)。在Thonny的菜单栏选择“解释器”并选择“MicroPython (ESP32)”然后选择对应的COM口,之后你就能看到MicroPython的REPL提示符了。

如果你的板子预装的是原生固件,建议重刷一次带st7789或者ili9341的固件,网上有很多预编译固件,省得自己折腾编译。刷固件方式用esptool:

esptool.py --port COM3 --baud 460800 erase_flash esptool.py --port COM3 --baud 460800 write_flash 0x1000 esp32-20240601-v1.23.0.bin

刷好以后,可以通过Thonny直接上传代码和资源文件。

3.2 初始化LCD并显示一张静态图片

这里给出完整的最小示例,让屏幕先亮起来,显示一张图画。

# main.py from machine import Pin, SPI import ili9341 import time import struct def load_image(filename): with open(filename, "rb") as f: # 跳过BMP文件头(54字节) f.seek(54) buf = f.read() return buf # SPI初始化 spi = SPI(2, baudrate=40000000, polarity=0, phase=0) cs = Pin(5, Pin.OUT) dc = Pin(19, Pin.OUT) rst = Pin(21, Pin.OUT) display = ili9341.ILI9341(spi, cs=cs, dc=dc, rst=rst, w=320, h=240, r=0) # 清屏为白色 display.fill(display.color(255, 255, 255)) # 显示图片 image_data = load_image("/images/bg.bmp") display.show_image(image_data, 0, 0, 320, 240)

这里的show_image是我自己基于push_image封装的,实际上很多库自带blit_image。如果你用的是st7789的库,可以这么写:

import st7789 display = st7789.ST7789(spi, 240, 320, cs=cs, dc=dc, rst=rst, backlight=Pin(4, Pin.OUT)) display.init() display.jpg("/images/bg.jpg", 0, 0)

实测下来,用BMP方式加载最快。如果你发现颜色反了,检查一下图片的红色和蓝色是不是交换了,因为RGB565和RGB888的通道顺序不同,在转换工具里勾选“RGB565(大端)”即可。

3.3 显示文字并实现滚动效果

文字部分我建议先实现一个最简单的函数,显示一行字符串:

def draw_text(text, x, y, size=1, color=0x0000): display.text(text, x, y, color, size)

官方例程支持的字体很小,为了显示大号数字粉丝数,我可以手工用整块区域擦除重绘,比如粉丝数每五秒刷新一次:

fans_str = str(fans_count) display.fill_rect(200, 180, 110, 40, display.color(255, 255, 255)) display.text(fans_str, 210, 185, display.color(0, 0, 0), 2)

如果需要滚动显示公告,可以用一个变量控制偏移量,使用scroll函数,或者每次重绘整个区域。我一直用的是重绘,虽然慢一点,但逻辑简单,不容易出bug。

3.4 整合网络请求与定时刷新

现在把三件事合起来:开机显示背景图,显示文字,然后循环拉取粉丝数更新显示。

import urequests import ujson import time FANS_UID = 1234567 REFRESH_SEC = 60 def get_fans(): try: resp = urequests.get("https://api.bilibili.com/x/relation/stat?vmid={}".format(FANS_UID), timeout=5) data = ujson.loads(resp.text) resp.close() return data["data"]["follower"] except: return None def main(): # 初始化屏幕和图片 draw_image() draw_text("Hello ESP32", 20, 20) while True: fans = get_fans() if fans is not None: draw_fans(fans) time.sleep(REFRESH_SEC)

这段代码足够跑通整个逻辑循环。但要注意,如果断网了,urequests会抛出异常,屏幕那边如果不处理就会卡住。所以我把get_fans封装成捕获所有异常并返回None,主循环即使失败也不会终止。

4. 常见问题与排查技巧实录

4.1 屏幕不亮或白屏

先检查三点:供电电压够不够?ESP32的3V3引脚输出电流有限,LCD本身的背光电流很容易把3V3的电压拖垮,我遇到过因为背光接在3V3导致屏幕无法初始化的情况,后来改成外接5V供电(通过板载稳压)或者把背光单独接到GPIO控制才解决。

然后检查复位引脚。有些LCD模块的RESET脚接ESP32的EN引脚不太好,需要单独接一个GPIO,并在初始化前后拉高拉低。

最后确认CS脚是不是正确对应到了模块的CS,很多模块上印的是“CS”或“SS”,对应关系混乱很常见。

4.2 图片色彩怪异或上下颠倒

如果图像能显示但色调不对,基本都是图片转换时通道顺序错了。使用在线转换工具时,一定要选择“RGB565大端”选项,不要选“小端”。

上下颠倒或左右反了,可以修改初始化参数r=0改成r=2,或者调整mirror参数。如果屏幕中间有一条缝,检查SPI时钟极性和相位是否对,大多数ILI9341用polarity=0 phase=0没问题,但部分模块可能要polarity=1 phase=1。

4.3 B站粉丝数请求超时

MicroPython的urequests超时参数一定要设,不然默认会一直等,看起来就像卡死。设置timeout=5后,如果5秒内没响应就走异常流程。不过timeout在MicroPython某些版本上只对连接阶段有效,如果服务端一直不返回数据还是会卡住,所以最好在调用前判断一下Wi-Fi是否在线:

import network wlan = network.WLAN(network.STA_IF) if not wlan.isconnected(): print("Wi-Fi lost, reconnecting...") wlan.connect('ssid', 'password')

还有一个坑是TLS。B站接口是可以走HTTP的,尽量避免用HTTPS,因为MicroPython的TLS握手需要大量内存,容易导致MemoryError。我用的是http://api.bilibili.com,实测仍然能获取数据,不会重定向。

4.4 中文字体显示为方块

前面说过,MicroPython标准库的text()不支持中文,如果你用了中文字符,会直接变成方框。我的解决方案是自绘字模。具体实现时,我把要显示的字符(比如“粉、丝、数、关、注”)做成一个字典,然后通过一个函数渲染。

这个字典可以放在单独的文件里,结构如下:

char_map = { "粉": [[0,1,0,1,0], [1,0,1,0,1], ...], "丝": [...], }

然后用上文提到的draw_char函数把每个像素点画出来。虽然麻烦,但效果稳定,而且这个字模文件可以复用。

我建议你用现成的font_to_py工具,它可以把TTF字体转换成MicroPython可用的.py模块,使用方式:

from font_to_py import font display.show_font(font, "粉丝数", x=10, y=10, color=0xFFFF)

如果你懒得转换,还有一个特别推荐的懒人方案:把“粉丝数”三个字做成一张透明背景的小图,然后当作图片直接贴上去,这样完全绕开字体问题,而且视觉上更美观。

4.5 内存不足与文件系统满

ESP32的RAM有限,如果程序里缓存了大图片,很容易出现MemoryError。我踩过的坑就是直接加载一张320x240的RGB888图片,需要320x240x3=230KB,直接就爆了。解决方法是先用Buffer或者把图片分成上下两半分别显示。

文件系统满的话,用os.listdir()查看剩余空间,如果确实满了,可以删掉不必要的字体文件,或者使用压缩过的图片格式。我通常把图片全部转成JPG而不是BMP,因为JPG在文件系统里占空间小,内存解码时用st7789.jpg方法直接解码,这样内存占用也不会太大。

5. 扩展方向与我的实操心得

5.1 再加一个DHT11温湿度显示

既然你有了屏幕和网络,为什么不把周围的环境数据也显示出来?在我的项目里,我额外接了一个DHT11温湿度传感器,在屏幕底部显示实时温度湿度。这个扩展只需要在原有代码基础上增加一个传感器读取函数,非常容易。

5.2 增加一个按键切换页面

你可以把一个GPIO口配置为输入,接一个轻触按键,按一下切换显示粉丝数、时间、天气等不同界面。这样会让项目显得更完整,也更适合放在桌面上当个信息面板。

5.3 我最想提醒你的事

折腾完这个项目之后,我最大的感受是:硬件问题比软件问题更容易让人崩溃。有一次屏幕怎么都不亮,我以为是接线问题,排查半天才发现是模块的背光引脚没接好。所以建议你第一次调试时,先不要写网络逻辑,先确认屏幕能显示白色、红色、绿色、蓝色,再逐步叠加功能。

另外,MicroPython代码不需要追求效率,因为它不是一个性能平台,能用简单代码解决问题就尽量别去整复杂优化。比如滚动数字时,我尝试过用DMA操作像素数组,结果内存分配反而更容易出错,后来干脆用fill_rect擦除重绘,开发效率高了很多。

5.4 下一步还能做什么

如果你想认真做产品,可以考虑把粉丝数显示改成圆角卡片样式,再用外接的实时时钟或者网络对时模块显示日期。如果你只是做个练手项目,那么这篇文章里讲到的所有细节已经足够你搭建一个完整能运行的小东西了。

按照我个人的经验,从零到能用一周的碎片时间就能搞定,最大的成本不是写代码,而是等待图片转换、烧录、试错的过程。只要沉住气,会很有成就感。最后再分享一个我私藏的小技巧:每次修改代码前,把能跑的旧版本复制一份备份,因为MicroPython不像电脑开发有完善的版本管理,一个粗心就可能把屏幕刷成砖,恢复起来很费劲。备份能让你随时回退,减少心碎时刻。

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进