你好,我是吴老师。
这份是给你自己读的——不用等我讲,你一段一段往下看,看到让你停下来的地方,就照着做一遍。看不懂的整段复制给 AI,加一句"请一次只解释一个概念,讲完问我一个小问题"。
读完之后,你应该能:说出屏幕上任意一个点的坐标;用画线、画方框、画圆拼出一幅简单的图;解释为什么"只改一小块"比"整屏重画"快;说出 SPI 那几根线各自干什么;说出 LVGL 是什么、它和手绘各适合做什么。
用时:60–90 分钟。准备:板子、数据线、已经接入板卡 Skill 的 WorkBuddy。
一、今天只做一件很小的事
第 3 课,我们让整块屏幕变成一种颜色。你按一下 BOOT,七万六千八百个小点全都换了色,挺痛快的。
今天反过来,我们只做一件很小的事:点亮其中的一个点。
你可能会说,这有什么难的?整屏都变了,变一个点还不是小菜一碟?
那我就要问你了:屏幕上有七万六千八百个点,你要点亮哪一个?
"最中间那个。"——不行,板子听不懂"中间"。
板子只认数字。你得给它一个号码,这个号码叫坐标。
所以今天真正的问题不是"怎么画",而是——怎么说清楚我要哪一个。
二、坐标:就像电影院找座位
怎么给七万多个点编号?总不能一个一个起名字吧。
你可以把它想成一张电影票:上面写着"第几排、第几座"。两个数一确定,座位就唯一了。
屏幕也一样,只是这两个数有自己的名字:
- X:从左边开始数。最左边是
0,往右挪一格加 1,一直数到239 - Y:从上边开始数。最上边是
0,往下挪一格加 1,一直数到319
原点 (0, 0) 就在左上角。
| 位置 | X | Y | 像电影票上的 |
|---|---|---|---|
| 左上角 | 0 | 0 | 第 1 排第 1 座 |
| 右上角 | 239 | 0 | 第 1 排最后一座 |
| 左下角 | 0 | 319 | 最后一排第 1 座 |
| 右下角 | 239 | 319 | 最后一排最后一座 |
| 正中间 | 120 | 160 | 中间靠前一点 |
⚠️ 这里有个坑,几乎每个人都会踩
屏幕的 Y 是往下增长的,Y 越大越靠屏幕下方。这和你数学课上的坐标系正好相反。
数学课上,Y 往上是正方向;屏幕上,Y 往下才是正方向。
是不是谁写错了?不是。这只是约定不同。
屏幕为什么要这么定?你想想画面是怎么送过去的:从左上角那个点开始,一个一个数过去,第一行数完数第二行——数数的顺序,就决定了 Y 的方向。
你可以这样记:屏幕不是一张坐标纸,是一篇从左上角开始读的文章。
(顺带一提:正中间为什么是 (120, 160) 而不是 (120, 159.5)?因为点的编号是从 0 开始的整数,没有"半个点"这种东西。)
任务 1:点亮五个点
请新建一个屏幕坐标实验:
开机后整屏填黑,然后在下面五个位置各画一个方块:
左上角 (0,0)、右上角 (239,0)、左下角 (0,319)、右下角 (239,319)、正中间 (120,160)。
方块边长 ___ 个像素(你自己定,参考 8);
颜色用 ___(你自己定,参考亮黄),亮度不超过 80。
请说明你用的画方块函数叫什么、坐标是怎么传进去的。
编译烧录前先核对目标端口,保留旧程序。
想好了再点开:我们的参考数字
这不是标准答案。 参考值:边长 8 像素,颜色 亮黄。
边长为什么值得挑一挑? 它决定了你等一下能不能看出问题:
- 填 8:右下角那块的缺角看得清清楚楚;
- 填 4:太小了,缺的那一角不明显,这个实验就白做了;
- 填 40:太大,五块会互相挨上,反而看不清楚。
这一节的重点不是"画出来",是看出右下角缺了一角。边长选小了,你就看不到。
颜色随便选,只有一个要求:和黑底对比要强。深蓝、深红在黑底上几乎看不见。
先猜再烧。 五个都出现以后,重点看右下角那个——它是不是缺了一角?
三、板子不会提醒你画到外面去了
我们做个小实验。假如我让板子在 (239, 319) 画一个边长 8 的方块。
这个方块要占横向 239 到 246、纵向 319 到 326。可是——屏幕最右边只到 239,最下边只到 319。
结果是什么?
方块缺了一大角。
而且板子不报错,不提醒,不闪灯。它只是把屏幕外面那部分安安静静地丢掉了。
这像什么?像你在作业本的最后一格写了个很大的字,超出纸边的那一半,本子上不会出现,老师也不会提醒你——它就那么没了。
所以在我们这门课上,检查的标准不只要看"有没有画出来",还要看"是不是和你预想的一模一样"。
| 你可能会遇到的情况 | 先怀疑什么 |
|---|---|
| 方块缺了一角 | 坐标加上宽高以后,超出 240×320 了吗? |
| 东西出现在上下颠倒的位置 | Y 的方向是不是记反了? |
| 左右反了 | 屏幕方向和 Skill 里的配置一致吗? |
| 什么都没出来 | 是不是先用整屏黑色把它盖住了?顺序对吗? |
四、不只是点:还能画线、方框和圆
会画点了,就能画更多东西。有意思的是:画的东西不一样,你要告诉板子的数字个数也不一样。
一条线:给板子两个点
画一个点要给 2 个数,画一条线要给 4 个数——起点一对,终点一对。
中间那些点呢?不用你操心。你给了两头,板子自己算出中间该点亮哪些点。就像你在一张纸上点了两个点,然后用尺子一连。
一个圆:只用圆规就够了
这里有个地方值得你想一想:为什么画圆只要 3 个数?
你想想圆规是怎么用的:把针尖扎在一个点上(圆心),把铅笔腿张开到某个宽度(半径),转一圈——圆就出来了。
圆规不需要你告诉它"第 10 行第 20 列要画、第 10 行第 21 列要画……"。它只需要圆心 + 半径。
板子也一样。圆的定义就是"到一个点的距离等于某个长度的所有点"。规则一定,圆就唯一了。
这个比喻有个地方要修正:圆规是"画"出来的,板子是"判断"出来的。它会拿每一个点去算:这个点到圆心的距离等于半径吗?等于就点亮,不等于就放过。板子手里只有加减乘除,它并不"懂"什么是圆。
把规则交给板子,它替你把点算出来——这是编程里特别省力的一个思路。
一张表收好
| 要画什么 | 要给板子几个数 | 分别是 | 函数 |
|---|---|---|---|
| 一个点 | 2 | X、Y | drawPixel |
| 一条线 | 4 | 起点 X/Y、终点 X/Y | drawLine |
| 空心方框 | 4 | 起点 X/Y、宽、高 | drawRect |
| 实心方块 | 4 | 起点 X/Y、宽、高 | fillRect |
| 圆 | 3 | 圆心 X/Y、半径 | drawCircle / fillCircle |
(别忘了,这几个数之外,每次都还要再给一个颜色。)
任务 2:把四种图形都画一遍
请新建一个图形实验,黑底上画出这些图形方便对比:
1. 从 (0,0) 到 (239,319) 画一条 ___ 颜色的对角线;
2. 在 (60,60) 画一个 ___×___ 的红色方框,只画边框;
3. 在 (60,140) 画一个**同样大小**的红色方块,填满;
4. 在 (170,80) 画一个半径 ___ 的黄色圆,只画边框;
5. 在 (170,220) 画一个**同样半径**的黄色圆,填满。
注意 2 和 3 必须一样大、4 和 5 必须一样大 —— 这样比出来的差别才是"描边还是填充"造成的。
亮度都不超过 80。请用中文注释,保留旧程序。
想好了再点开:我们的参考数字
这不是标准答案。 参考值:方框 40×40,圆半径 30,对角线颜色 青色。
为什么 2 和 3 必须一样大? 这是"对照实验"的老规矩(第 3 课讲过):只能改一个条件。
- 一个 40×40 的空心方框,和一个 50×50 的实心方块 —— 就算看出不一样, 你也说不清是"描边和填充"的差别,还是"大小不同"的差别。
- 一样大,差别就只剩一个来源:描边还是填充。
看到空心圆的边有点毛糙?那是对的 —— 屏幕上的点是一格一格的, 画不出真正平滑的曲线,只能给一条锯齿状的"差不多是圆的线"。
凑近看一眼那个空心圆的边:是不是有点毛糙?
为什么?因为屏幕上的点是一格一格的,它画不出行云流水的曲线——只能给你一条锯齿状的"差不多是圆的线"。
五、描边和填充:看起来一样大,干的活差 10 倍
drawRect 只画四条边(描边),fillRect 把里面全填上(填充)。
它们要的数一样多,画出来的点却差很多。边长 40 的方框:
| 画法 | 像什么 | 要画多少个点 | 要送多少字节 |
|---|---|---|---|
描边 drawRect | 用彩笔勾个边框 | 约 156 个 | 约 312 字节 |
填充 fillRect | 把里面全涂满 | 1,600 个 | 3,200 字节 |
就像画画:勾个轮廓几秒钟,把里面涂满要花好几分钟。屏幕上看起来差不多大,板子干的活差了约 10 倍。
六、所以,改多少就送多少
| 要做的事 | 要送的数据 | 打个比方 |
|---|---|---|
| 给一颗 RGB 灯换色 | 3 字节 | 递一张三个字的纸条 |
| 改屏幕上 1 个点 | 2 字节 | 比给灯换色还少 |
| 改 10×10 的小块 | 200 字节 | 抄一小段话 |
| 整块屏幕换色 | 153,600 字节 | 抄一本厚厚的书 |
第 3 课我们算过 15 万字节,你可能会以为"屏幕就是这么重"。不是的。
15 万是因为我们当时要求改整块屏幕。今天只改一个点,板子就只送 2 个字节。
改多少,就送多少。 这句话你先记住,第 6 课它会变成一条硬规矩。
任务 3:测一测,谁更快
请加上计时,分别测出这两件事各用了多少毫秒:
1. 整屏填充红蓝交替,一共 ___ 次;
2. 只填充中间 ___×___ 的小方块,红蓝交替**同样次数**。
结果同时显示在屏幕上、并通过串口打印出来。按 BOOT 可以重测一轮。
两次测试里,除了"填充范围",其它条件必须完全一样。
想好了再点开:我们的参考数字
这不是标准答案。 参考值:次数 100,小方块 40×40。
先自己算一下这两个数字意味着什么:
- 整屏是 240×320 = 76,800 个点;
- 40×40 的小方块是 1,600 个点;
- 76,800 ÷ 1,600 = 48 倍。
所以你该预测:整屏用的时间大约是小方块的 48 倍。
次数也有讲究:
- 填 100:能测出稳定的毫秒数;
- 填 2:太快了,计时器可能显示 0 或 1 毫秒,什么都比不出来。
小方块改成 80×80 呢?点数变成 6,400,倍数就降到 12 倍。 方块越大,倍数越小——你可以改一个数字再测一次,验证这个规律。
先预测再烧录。 整屏 76,800 个点,小方块 1,600 个点——点数是 48 倍,那时间也会是 48 倍吗?
如果差得没那么多,想一想:这 100 次里,除了"送数据",还有什么在花时间?(提示:下命令、算颜色,也都要时间。)
七、数据走哪条路:SPI
好,现在知道要送什么了。接下来的问题是:这些东西是怎么从主控跑到屏幕上来的?
这条通道叫 SPI。你可以把它想成工厂里的一条传送带:
| 线的名字 | GPIO | 在传送带上扮演什么 |
|---|---|---|
| SCLK 时钟 | 12 | 节拍器。它每响一下,带子往前推一格 |
| MOSI 数据 | 11 | 传送带本身,一个字节一个字节往屏幕那头运 |
| CS 片选 | 10 | 点名员。叫到谁,谁才接货 |
| DC 命令/数据 | 46 | 贴在包裹上的标签:这是"指令"还是"货" |
DC 这根线最巧
请你想一想:同一条传送带上运过去的字节,屏幕怎么知道它们是什么意思?
"准备接收"、"从第 120 列开始"、"这个点是红色"——这些都是不同的意思,可它们走的是同一条线。
所以每个包裹都要贴张标签。DC 就是这张标签:
- 标签写命令 → 屏幕把它当指令,比如"从第几行开始"
- 标签写数据 → 屏幕把它当颜色本身
这就是工程师常说的取舍:用一根线省下一根 GPIO,代价是每次发货都要先说一句"接下来是什么"。
还有一个坑:这条路只能往外走
SPI 本来还有一条反向的线叫 MISO(屏幕往主控送数据)。我们这块板上接是接了,但没打通。
这意味着:我们能给屏幕送东西,却不能把屏幕上的画面读回来。
就像一个只能往外寄信、收不到回信的邮筒。
所以以后你想把屏幕画面截个图存下来,不能指望"把屏幕上的点读出来",只能自己记住画了什么。这个坑以后做"你画我猜"的时候还会再遇到一次。
八、换一种画法:LVGL
前面七节,你一直在用"手绘"的方式:自己算坐标、自己选函数、自己一个一个点地画。
现在认识另一条路。
先看一个对比
假设你要在屏幕正中间放一个按钮,上面写"开始"。
手绘的做法:
- 想好按钮多大——比如 120×40;
- 算出左上角坐标——横着居中就是
(240−120)÷2 = 60,竖着放中间就是160−20 = 140; fillRect(60, 140, 120, 40, 蓝色)把底填上;drawRect(60, 140, 120, 40, 白边)描一圈边;- 再算出文字该画在哪,把"开始"两个字画上去;
- 按钮被按下时要变个样子——你得自己重画一遍;
- 按钮要挪个位置——上面每一步全部重算。
LVGL 的做法:
lv_obj_t *btn = lv_btn_create(lv_scr_act()); // 造一个按钮
lv_obj_t *lbl = lv_label_create(btn); // 按钮上放一行字
lv_label_set_text(lbl, "开始"); // 字是"开始"
lv_obj_center(btn); // 居中
就这四行。注意最后一行——"居中"是你说的一句话,不是一个算出来的数字。
LVGL 到底是什么
LVGL 是一个界面工具箱(也叫"图形库""UI 框架")。
你可以把它想成一位很会做界面的助手:
- 你说"我要一个按钮",它就给你一个按钮——你不需要知道按钮是多少个点拼出来的;
- 你说"居中",它自己算坐标;
- 你说"这里可以上下滑",它自己处理滑动、惯性、滚动条;
- 屏幕什么时候刷新、重画哪一块,它自己决定。
前面手绘时你干的那些活——算坐标、描边、擦旧图、只重画变化的部分——LVGL 全替你做了。
前面算的那些账没有白算。第 3 课的 15 万字节、第五节"描边和填充差 10 倍"、第六节"改多少送多少"——LVGL 内部照样要做这些事,只是它替你做了。 你懂这些,画面卡的时候才知道该去哪儿找原因。
常用的 LVGL 指令
下面这些是 LVGL 最常用的一批。今天只要认得住名字、知道它是干什么的就够了,不用背。
① 开局(两条,缺一不可)
| 指令 | 干什么 |
|---|---|
lv_init() | 启动 LVGL。开机时调用一次 |
lv_timer_handler() | 让 LVGL 干活。放在 loop() 里反复调用 |
lv_timer_handler()是最容易被忘掉的一条。忘了它,屏幕会一动不动——因为 LVGL 根本没机会干活。
② 造东西(都要告诉它"放在谁里面")
| 指令 | 造什么 |
|---|---|
lv_label_create(父) | 一行文字 |
lv_btn_create(父) | 一个按钮 |
lv_obj_create(父) | 一个空白容器(用来装别的) |
lv_bar_create(父) | 一条进度条 |
lv_img_create(父) | 一张图片 |
③ 安排位置
| 指令 | 干什么 |
|---|---|
lv_obj_set_pos(对象, x, y) | 放到指定坐标 |
lv_obj_center(对象) | 居中 |
lv_obj_align(对象, LV_ALIGN_TOP_MID, 0, 8) | 对齐到某条边,再加一点偏移 |
lv_obj_set_size(对象, 宽, 高) | 设置大小 |
④ 设置内容
| 指令 | 干什么 |
|---|---|
lv_label_set_text(标签, "文字") | 设置文字 |
lv_label_set_text_fmt(标签, "第 %d 项", i) | 带数字的文字 |
lv_bar_set_value(进度条, 60, LV_ANIM_ON) | 设置进度值 |
⑤ 改外观
| 指令 | 干什么 |
|---|---|
lv_obj_set_style_bg_color(对象, 颜色, 0) | 改背景色 |
lv_obj_set_style_text_font(对象, 字体, 0) | 改字体 |
它自己管的三件事
这三件事最能说明"手绘和 LVGL 到底差在哪":
| 这件事 | 手绘 | LVGL |
|---|---|---|
| 按钮按下时变个样子 | 你自己重画一遍 | 自动(自带按下效果) |
| 内容超出屏幕要能滑动 | 自己算位移、自己模拟惯性 | 说一句就行 |
| 屏幕什么时候刷新、重画哪块 | 自己算、自己擦 | 它自己决定 |
以后是不是都用 LVGL?
不是。两条路各有各的用处。
| 什么时候用手绘 | 什么时候用 LVGL |
|---|---|
| 画游戏画面(飞机、贪吃蛇) | 做界面(按钮、列表、菜单) |
| 要精确控制每一个点 | 页面元素多、要能滚动、要有标准外观 |
| 画面每帧都在变 | 变化不快,但要好看、要好用 |
打个比方:
- 手绘像自己拿笔画画——每一笔都在你手上,画什么像什么,但什么都要自己来;
- LVGL 像用现成的积木搭——按钮、滑块、列表都是现成的,搭得快、长得正,但形状是它给的。
我们后面的游戏课(五子棋、贪吃蛇、飞机大战)都用手绘;做菜单、设置页、列表这类界面时,LVGL 能省很多力气。
什么时候才用得上它?
LVGL 要有触摸才真正好用——因为你搭出来的按钮,是要用手指去点的。
触摸我们还没学(第 8 课)。 所以今天先认识它、记住几条常用指令就够;第 8 课学完触摸,我们再回来用它做真正的界面。
不过——不点也能看。你可以先让 AI 搭一个界面出来,纯显示、不用点,先看看 LVGL 搭的东西长什么样。
请用 LVGL 8.3 给这块板搭一个纯显示的界面(ESP32-S3 + 2.8 寸 ILI9341,竖屏 240x320):
1. 顶部一行标题文字"我的第一个 LVGL 界面";
2. 中间一个按钮,按钮上写"点击我";
3. 底部一条进度条,值设成 ___(你自己定,参考 60)。
触摸还没学,所以这一版**按钮不用能点**,只要能看见就行。
请参考项目里已经跑通的 LVGL 示例(Example_34_lvgl_scroll_list)的初始化和配置,
特别是它怎么把 LVGL 和屏幕接起来的。别忘了 loop 里要有 lv_timer_handler()。
请用中文注释;烧录前先运行 arduino-cli board list 核对端口;另存一个新版本。
想好了再点开:我们的参考值
这不是标准答案。 参考值:进度条 60。
进度条的取值是 0 到 100,代表"完成了百分之多少"。所以:
- 填 60:一看就是"过半了、还没满";
- 填 100:是满的——好看,但看不出"进度"这个意思;
- 填 0:是空的——同样看不出这条是个进度条。
想让它更像真进度条?让 AI 改成"每秒自动加一点,涨到 100 再回到 0"。 那样你就顺便看到 LVGL 的动画是怎么跑的——一行
lv_bar_set_value(..., LV_ANIM_ON)的事。
如果界面没出来
| 现象 | 先检查 |
|---|---|
| 屏幕一片黑/不动 | loop() 里有 lv_timer_handler() 吗? |
| 编译报找不到 lvgl.h | LVGL 库装了没?版本是 8.x 吗(别用 9.x)? |
| 花屏、颜色不对 | lv_conf.h 的配置和示例一致吗? |
| 出来一下就没了 | lv_scr_act() 拿到的屏幕对象对吗? |
九、自检小测
先自己想,再点开对答案。
1. 屏幕的坐标原点在哪里?X 和 Y 各朝哪个方向增长?
原点在左上角。X 向右增长到 239,Y 向下增长到 319。
2. 在 (239, 319) 画一个边长 8 的方块,会完整显示吗?
不会。方块要占 239–246,超出屏幕的部分被丢掉,你会看到一个缺角的方块,而且板子不会报错。
3. 画一条线、一个方框、一个圆,分别要给板子几个数?
线 4 个(起点 X/Y + 终点 X/Y),方框 4 个(起点 X/Y + 宽高),圆 3 个(圆心 X/Y + 半径)。都还要再加一个颜色。
4. 画圆为什么只要圆心和半径?
因为圆的定义就是"到一个点距离等于半径的所有点"。规则一定,圆就唯一了。这就像用圆规:扎住一点、张开固定宽度、转一圈。
5. 40×40 的方框,描边和填充送的数据差多少?
描边约 156 个点 / 312 字节,填充 1,600 个点 / 3,200 字节,相差约 10 倍。
6. DC 这根线是干什么的?
告诉屏幕接下来送过去的字节是命令(比如"从第几行开始画")还是数据(颜色本身)。像包裹上的标签。
7. 为什么不能把屏幕上的画面读回来?
SPI 负责反向传输的 MISO(GPIO13)在这块板上没有真正打通,只能给屏幕送,不能从屏幕读。
8. LVGL 是什么?它和前面的"手绘"最大的区别在哪?
LVGL 是一个界面工具箱(图形库 / UI 框架)。
最大的区别:手绘要你自己算坐标、自己描边、自己擦旧图;LVGL 只要你说"我要什么"——"居中""可以上下滑""按钮带按下效果",这些它自己管。
所以手绘是"每一笔都在你手上",LVGL 是"用现成积木搭"。
9. 用 LVGL,`loop()` 里必须写哪一句?忘了会怎样?
lv_timer_handler()。它是让 LVGL 干活的那一句。
忘了它,LVGL 就没有机会处理刷新、动画和事件——屏幕会一动不动,而且不报错。这是新手最容易踩的坑。
10. 以后是不是所有画面都用 LVGL?
不是。两条路各有各的用处:
- 手绘适合游戏画面(飞机、贪吃蛇)、要精确控制每个像素、每帧都在变的场合;
- LVGL 适合做界面(按钮、列表、菜单)、元素多、要滚动、要标准外观的场合。
后面的游戏课都用手绘;做菜单和设置页时,LVGL 省力气。
十、还想玩点什么
| 方向 | 可以描述给 AI 的效果 | 值得继续想 |
|---|---|---|
| 像素画 | 用方块拼出一个笑脸或房子 | 你要怎样把"图案"翻译成一串坐标? |
| 星空 | 随机位置点亮几十个白点 | 每次重画整屏,和只在旧点上盖黑色,哪种更好? |
| 进度条 | 底部长条按进度从左往右填充 | 从 30% 涨到 31%,这一帧要改几个点? |
| 心电图 | 一条线从左到右移动,到头回到最左 | 每一帧要擦掉什么、画上什么? |
| 用 LVGL 搭个菜单 | 一列按钮,选一个就变个颜色 | 和手绘一个按钮比,省了哪几步? |
挑战:坐标寻宝图
请做一个坐标寻宝游戏:
开机后随机藏一个 8×8 的方块(不显示)。我在串口输入 "x y"(如 "50 200"),
板子在我猜的位置画一个灰色标记,并按距离回答:
小于 ___ 回答"烫!",小于 ___ 回答"热",小于 ___ 回答"温",否则回答"冷"。
猜中(距离小于 8)时把宝藏显示成亮黄色,串口回答"找到了!你一共猜了__次"。
按 BOOT 开始新一轮。
想好了再点开:我们的参考数字
这不是标准答案。 参考值:20 / 60 / 120(三个数字必须从小到大)。
这三个数字决定的是游戏有多好猜。先想清楚:
- 三个数都填得很大(比如 100/200/300):一猜就说"烫",几次就中了 —— 太简单,没意思;
- 三个数都填得很小(比如 5/10/15):全屏几乎都是"冷",等于没有提示 —— 太难,只能瞎猜;
- 参考值 20/60/120:把屏幕横着分成几个圈,"烫"的区域很小,得靠一点点逼近。
还有一个玩法可以想:提示能不能不只是"冷热",而是给个方向? 比如"往右下一点"。这样猜起来更快 —— 但那已经是另一套规则了,你来定。
玩完想一想这个:板子理解你在找东西吗?
不。它只是做了几次减法和比较。所有的"聪明"都来自你写下的规则——规则是你给的,不是它自己想出来的。
最后那道题留给下一课:让画面动起来,真正的难点不是"画",而是擦和只改该改的地方。