
1. 项目概述与核心思路手头有个Raspberry Pi Pico和一块Waveshare的1.8寸LCD屏想用它来显示清晰易读的大号数字比如做个简易的计时器、计数器或者传感器读数显示器。直接用库里的字体要么太小看不清要么太占宝贵的Pico内存。这时候我自然想到了经典的7段数码管——那种计算器、老式电子钟上用的显示方式。它结构简单识别度高而且本质上就是7个发光段的组合非常适合用程序来模拟。这个项目的核心思路就是在SPI液晶屏上用画线或画矩形的方式“画”出7段数码管的每一个段然后通过一个编码表来决定显示哪个数字时点亮哪几个段。听起来简单但真做起来从驱动屏幕、设计绘图算法到优化性能每一步都有不少细节要考虑。我选择用MicroPython来开发主要是图它上手快、交互方便对于这种需要频繁调试的图形项目特别友好。下面我就把从硬件连接到最终实现动态显示的全过程包括中间踩过的坑和总结的技巧详细拆解一遍。2. 硬件准备与连接要点2.1 核心组件选型解析这次用到的两个核心硬件是Raspberry Pi Pico和Waveshare 1.8英寸 LCD显示屏。选择Pico是因为它性价比极高双核ARM Cortex-M0264KB RAM对于驱动一块小屏并运行逻辑代码绰绰有余而且GPIO引脚丰富。这块Waveshare屏型号是Pico-LCD-1.8分辨率160x12865K色通过SPI接口通信。选它有几个关键理由一是专为Pico设计引脚排布对应几乎可以直插使用排针排母省去了飞线的麻烦二是SPI接口占用引脚少仅需3根数据线MOSI, SCLK, DC加2根控制线CS, RST还能通过背光控制引脚BL调节亮度把更多的GPIO留给其他传感器或输入设备三是官方提供了基础的MicroPython驱动给了我们一个不错的起点。2.2 硬件连接与供电细节连接非常简单但顺序有讲究。首先确保Pico未通电。将显示屏直接插在Pico的GPIO排针上注意方向屏幕的“Pico-LCD-1.8”字样通常朝向Pico的USB接口方向。这种设计使得GPIO 8到GPIO 13正好被屏幕占用。如果你需要用到这些引脚的其他功能就得小心了。连接好后通过Micro-USB线给Pico供电。这里有个重要细节屏幕的背光BL引脚默认可能悬空或接高电平如果屏幕不亮检查一下代码里是否正确初始化并拉高了背光控制引脚。我一开始就忘了还以为屏幕坏了。注意虽然屏幕可以直接插上但如果你打算将项目放入外壳或经常移动强烈建议使用排母和排针焊接而不是简单地插在面包板上接触不良会导致SPI通信失败屏幕出现花屏或完全无显示。3. 软件开发环境与驱动基础3.1 MicroPython环境搭建开发环境我选用Thonny IDE。首先需要给Pico刷入最新的MicroPython固件。去MicroPython官网下载针对RP2040Pico的芯片的.uf2文件按住Pico上的BOOTSEL按钮的同时插入USB线将其识别为U盘然后把固件文件拖进去Pico会自动重启。完成后在Thonny中配置解释器选择“MicroPython (Raspberry Pi Pico)”并连接正确的串口。环境就绪后第一件事通常是点个灯测试但我们现在要直接驱动屏幕。3.2 SPI显示屏驱动原理剖析Waveshare提供的驱动代码本质上是封装了与屏幕控制器通常是ST7735S通信的底层命令。SPI通信协议负责高速传输像素数据。驱动代码主要干几件事初始化发送一系列配置命令设置屏幕参数、定义基本的像素点绘制函数pixel、区域填充函数fill以及字符显示函数。但自带的字符显示功能固定且单一无法满足我们自定义大尺寸数字的需求。驱动代码中最关键的是pixel(x, y, color)函数。它接收坐标和颜色值通常是一个16位的RGB565颜色整数然后通过SPI总线将“在这个位置画这个颜色”的指令和数据发送给屏幕。我们所有高级的图形功能包括画线、画矩形、乃至模拟7段数码管都建立在反复调用这个pixel函数的基础上。理解这一点很重要因为我们的优化方向之一就是减少pixel调用的次数。4. 7段数码管模拟的核心算法设计4.1 字符编码映射表设计7段数码管顾名思义由a, b, c, d, e, f, g七个段组成有时还有一个右下角的小数点dp。我们可以用一个7位的二进制数或一个字节8位多一位给小数点来表示一个字符。每一位对应一个段的亮灭1亮0灭。我设计了一个字典来作为编码映射表这样查找起来非常直观SEGMENT_CODES { ‘0’: 0b0111111, # a,b,c,d,e,f段亮 ‘1’: 0b0000110, # b,c段亮 ‘2’: 0b1011011, ‘3’: 0b1001111, ‘4’: 0b1100110, ‘5’: 0b1101101, ‘6’: 0b1111101, ‘7’: 0b0000111, ‘8’: 0b1111111, ‘9’: 0b1101111, ‘A’: 0b1110111, # 十六进制A-F ‘b’: 0b1111100, ‘C’: 0b0111001, ‘d’: 0b1011110, ‘E’: 0b1111001, ‘F’: 0b1110001, ‘-’: 0b1000000, # 负号仅g段亮 ‘ ‘: 0b0000000 # 空格全灭 }这里有个细节十六进制显示时“A”和“b”、“d”用了小写是为了与数字“8”区分开这是7段显示约定俗成的做法。这个映射表是整个项目的“大脑”决定了显示什么内容。4.2 基于坐标计算的动态绘制函数有了编码下一步就是根据编码把对应的段“画”出来。每个段其实是一个细长的矩形。我们需要定义一个函数draw_segment(start_x, start_y, segment_id, size, color)。这里的start_x, start_y是单个数字的左上角原点坐标size是缩放因子例如基本段宽为1像素size3则段宽为3像素segment_id指明要画a-g中的哪一段。关键在于计算每个段的起始和结束坐标。我采用的方法是预先定义好一个“标准”7段数码管size1时各段的坐标模板。例如a段是顶部水平段它的坐标可以定义为从(start_x h_gap, start_y)到(start_x h_gap seg_length, start_y)的一条线但为了显示效果更粗我们实际是画一个矩形。h_gap是水平间隔seg_length是段的长度。当size增大时不仅段的长度和宽度要乘以size段与段之间的间隔也要等比例放大否则数字会显得很拥挤。函数内部根据segment_id选择对应的坐标计算逻辑然后调用驱动的fill_rect函数如果没有就用循环调用pixel实现来绘制一个实心矩形。绘制一个完整的数字就是遍历0-6这七个段ID检查编码中对应位是否为1如果是则调用draw_segment绘制该段。实操心得直接画矩形比画线再填充效率高。因为驱动里通常提供了fill_rect的优化实现它可能是一次性传输整个矩形区域的数据远比用循环逐个像素设置快得多。务必利用好这个函数。5. 代码实现与分层架构5.1 驱动层集成与封装我不建议将Waveshare提供的庞大驱动代码全部粘贴在主程序里这会让主逻辑难以阅读和维护。更好的做法是将其保存为一个单独的st7735.py模块文件然后在主程序中import。在驱动模块中我重点封装了一个Display类初始化方法里完成了SPI总线设置、引脚定义、屏幕硬件复位和初始化序列。将背光控制BL也封装成类的方法方便开关。这样主程序里只需要几行from st7735 import Display disp Display(spi_bus0, sck2, mosi3, dc1, cs0, rst5, bl4) disp.backlight(True) # 打开背光清晰且模块化。5.2 业务逻辑层数码管模拟器类这是项目的核心类我称之为SevenSegmentSim。它的初始化方法接收一个Display实例作为参数实现了硬件无关的设计。类内部主要包含属性存储颜色前景色、背景色、默认大小、字符间隔等配置。方法draw_digit(char, x, y, sizeNone, fgNone, bgNone)在指定位置绘制单个字符。这是对底层draw_segment的封装和调度。draw_number(num_str, x, y, ...)绘制一个数字字符串如“-12.5”需要处理多个字符的排列、小数点单独绘制一个小矩形和负号。clear_area(x, y, width, height)清空指定区域为背景色用于局部刷新比全屏刷新高效。在draw_digit方法里逻辑非常清晰先根据size和字符宽度计算出这个数字所占的矩形区域如果需要背景色非透明则调用clear_area填充背景然后解析char从SEGMENT_CODES字典中取得编码最后循环7次根据编码位决定是否绘制对应段。5.3 应用层与主循环逻辑主程序main.py现在变得非常简洁和聚焦于业务逻辑from machine import SPI, Pin from st7735 import Display from seven_seg_sim import SevenSegmentSim import time # 1. 初始化硬件 spi SPI(0, baudrate20_000_000, polarity0, phase0, sckPin(2), mosiPin(3)) disp Display(spispi, dcPin(1), csPin(0), rstPin(5), blPin(4)) seg_display SevenSegmentSim(disp, fg0xFFFF, bg0x0000) # 白字黑底 # 2. 清屏 disp.fill(0) # 3. 演示逻辑 counter 0 while True: # 在屏幕中央绘制一个放大的计数器 num_str str(counter % 10000).zfill(4) # 计算居中坐标需要知道总宽度 total_width seg_display.calc_width(num_str, size3) start_x (160 - total_width) // 2 seg_display.draw_number(num_str, start_x, 40, size3) counter 1 time.sleep(0.5) # 局部刷新只清空数字区域而不是全屏 seg_display.clear_area(start_x, 40, total_width, seg_display.calc_height(3))这个架构的好处是驱动换了一块屏只要接口类似只需修改st7735.py想改变显示样式比如改成14段管只需修改SevenSegmentSim类主程序只管“要显示什么”不管“怎么显示”。6. 性能优化与高级特性实现6.1 双缓冲与局部刷新策略直接绘制到屏幕单缓冲在动态更新时如果数字变化你会看到各个段被逐个擦除和重绘的过程可能有闪烁感。对于Pico这种内存有限的设备全双缓冲在内存中开辟一个完整的帧缓冲区不现实因为160x128x2字节 ≈ 40KB几乎占用了可用RAM的一半。我采用的优化策略是局部刷新和脏矩形。在draw_digit函数中先计算这个数字新状态和旧状态需要额外存储上一帧的编码的差异。只重绘那些状态发生变化的段。例如数字从“7”变成“1”只有a、d、e、f、g段需要从亮变灭b、c段保持亮那么我们就只重绘a、d、e、f、g这五个段所在的矩形区域用背景色填充再绘制新状态。这大大减少了SPI通信的数据量。6.2 多尺寸与颜色动态切换的实现尺寸因子size的实现前面提到需要等比缩放段宽、段长和段间距。在代码里我定义了几个基本常量SEG_WIDTH段宽、SEG_LENGTH段长、H_GAP水平段间隔、V_GAP垂直段间隔。在绘制时所有这些尺寸都乘以size。但要注意size为1时段宽至少是1像素否则看不见。当size较大时比如5绘制的矩形块可能会因为像素化而边缘出现锯齿但对于7段数码管的风格来说这反而增添了复古感。颜色动态切换更简单。draw_digit函数接受前景色(fg)和背景色(bg)参数。在绘制每个段时前景色就是段的颜色。背景色用于清除区域。你可以很容易地实现根据数值范围改变颜色比如温度低于0度显示蓝色高于30度显示红色。def draw_digit_with_color_logic(value, x, y): if value 0: fg_color 0x001F # 蓝色 elif value 100: fg_color 0xF800 # 红色 else: fg_color 0x07E0 # 绿色 seg_display.draw_digit(str(value), x, y, size2, fgfg_color)6.3 支持小数点和特殊符号小数点DP的处理需要单独考虑。它在数字的右下角。在draw_number函数中当解析到‘.’时不调用draw_digit而是在当前数字的绘制区域右下角画一个小的实心圆或矩形。需要精细计算其位置使其与数字主体协调。对于“-”负号和空格“ ”我们已经包含在编码表中。还可以扩展支持更多符号比如“°”度通常用上标的小‘o’模拟这可能需要自定义一个非标准的段组合编码。7. 常见问题排查与调试技巧7.1 屏幕无显示或花屏这是最常见的问题。请按以下顺序排查现象可能原因排查步骤屏幕完全无显示背光也不亮电源问题或背光未开启1. 检查USB线是否供电充足。2. 用万用表测量Pico的3.3V和GND引脚是否有输出。3. 检查代码中背光控制引脚BL是否被设置为输出并拉高。背光亮但屏幕全白/全黑/杂乱色块SPI初始化或通信失败1.核对引脚连接确保SCK、MOSI、DC、CS、RST与代码定义一致。2. 检查SPI总线号和速率设置。尝试降低baudrate如先降到1_000_000。3. 检查RST引脚复位时序确保在初始化前有一个有效的低电平脉冲。4. 确认使用的驱动代码与屏幕型号完全匹配。显示内容错位、撕裂或部分区域异常显存操作越界或刷新逻辑错误1. 检查所有绘图函数的坐标计算确保没有超出屏幕范围0x160, 0y128。2. 检查局部刷新时清除的矩形区域是否完全覆盖需要更新的旧内容避免残留。调试技巧在代码开始时先尝试用驱动里最简单的fill(color)函数填充整个屏幕为红色、绿色、蓝色确认屏幕基本驱动和颜色输出正常。然后再逐步测试画点、画线功能。7.2 显示刷新缓慢或闪烁严重这关系到性能优化。SPI速率在确保稳定的前提下尽量提高SPI时钟频率。Pico的SPI可以跑到几十MHz但对于这块小屏20-30MHz通常是安全且高效的选择。减少函数调用开销在MicroPython中函数调用和循环有一定开销。在draw_segment内部如果采用循环画像素会极慢。务必使用驱动的fill_rect或hline、vline等批量绘图函数。避免全屏刷新这是最关键的一点。除非必要不要每次更新都调用disp.fill(0)。使用前面提到的局部刷新和脏矩形算法。内存碎片在长时间运行的循环中避免频繁创建新的对象如列表、字典。将常量如编码字典、坐标模板定义在循环外部。7.3 数字显示不美观或比例失调这是算法细节问题。段宽段长比例标准7段数码管的段长宽比大约在4:1到5:1之间比较美观。如果SEG_LENGTH太短数字显得矮胖太长则稀疏。多调整几次找到视觉上舒服的比例。间隔Gap设置段与段之间的水平间隔H_GAP和垂直间隔V_GAP不能为0否则段会连在一起。通常设置为SEG_WIDTH的1-2倍。“1”字太窄数字“1”只有两竖在多个数字并列时可能显得比其他数字窄。一种常见的处理方法是让“1”的两竖段稍微向中间靠拢或者人为地增加“1”的占位宽度但这会破坏统一坐标计算。通常保持统一规则接受这种细微差异这更符合真实的数码管外观。8. 项目扩展与应用场景8.1 扩展至多位数与滚动显示现在的draw_number函数已经支持绘制字符串。要显示多位数只需计算好每个数字的起始x坐标依次绘制即可。x坐标的增量是数字宽度 数字间隔。滚动显示如跑马灯则是在每次循环中改变整个数字字符串的起始x坐标并在移出屏幕时从另一侧进入。需要注意在滚动前后清除受影响的所有区域避免拖影。8.2 打造实用小工具基于这个核心你可以快速构建一些实用的小设备简易秒表/倒计时器结合Pico的内部RTC或定时器实现时间计算并用大号数字显示。配合一个按钮控制开始/暂停/重置。传感器数据显示器连接一个DHT11温湿度传感器或DS18B20温度传感器读取数据后用不同颜色如蓝、绿、红代表低温、舒适、高温显示温度值。网络时钟如果使用Pico W可以连接Wi-Fi通过NTP协议获取网络时间制作一个永远准时的数码管时钟。系统状态监视器显示Pico的CPU温度、内存使用率等信息。8.3 优化为可移植的库你可以将SevenSegmentSim类进一步打磨增加更多的自定义选项比如段与段之间的圆角效果绘制圆角矩形替代普通矩形。添加阴影或发光效果在段的主体颜色周围绘制一圈稍暗或稍亮的像素。支持不同的数码管类型如14段管显示更全的字母。提供更丰富的字体将7段编码扩展到部分字母。然后将其打包成一个单独的.mpy文件MicroPython的预编译字节码或者发布到PyPI通过mip安装方便其他开发者直接引用。这个项目从硬件连接到软件架构从基础绘图到性能优化完整地展示了一个嵌入式图形应用的开发流程。它没有用到复杂的图形库而是从最基础的像素操作构建起所需的功能这种“造轮子”的过程对于深入理解嵌入式显示原理非常有帮助。当你看到自己编写的代码让屏幕上亮起清晰、自定义的数码管数字时那种成就感是直接用现成字体库无法比拟的。最重要的是这套框架非常轻量留给Pico充足的资源去处理其他任务这才是嵌入式开发的精髓所在。