# ESP32+ST7735S屏幕实战:用MicroPython实现天气时钟(附完整代码)
最近在捣鼓ESP32和那块小巧的ST7735S屏幕,想做个放在桌面的天气时钟。网上资料不少,但要么代码不全,要么步骤跳得太快,对新手不太友好。折腾了几天,总算把网络请求、屏幕驱动、数据解析这些环节都打通了,整个过程踩了不少坑,也积累了一些实用的经验。这篇文章就是把这些实战经验整理出来,手把手带你从零开始,用MicroPython打造一个功能完整的天气时钟。无论你是刚接触嵌入式开发的爱好者,还是想找个具体项目练手的学生,跟着步骤走,应该都能顺利跑起来。
这个项目的核心思路其实不复杂:ESP32通过Wi-Fi连接到网络,定期从天气API获取数据,然后解析出温度、湿度、天气状况等信息,最后在ST7735S屏幕上用图形和文字展示出来。但真正做起来,你会发现每个环节都有细节需要注意——比如SPI通信的引脚配置、屏幕驱动的初始化参数、网络请求的稳定性处理,还有如何在小屏幕上优雅地布局信息。我会把完整的代码都贴出来,并解释关键部分的设计逻辑,你可以直接复制使用,也可以根据自己的需求进行修改。
## 1. 硬件准备与连接方案
手头的硬件清单很简单:一块ESP32开发板(我用的是ESP32-WROOM-32),一块1.8寸的ST7735S TFT屏幕(分辨率128x160),再加上几根杜邦线。ESP32的引脚资源比较丰富,但SPI接口的分配需要留意,不同的引脚组合会影响通信速度和稳定性。
先说说ST7735S屏幕的引脚定义。这种屏幕通常有8个引脚:VCC(3.3V)、GND、SCL(时钟)、SDA(数据)、RES(复位)、DC(数据/命令选择)、CS(片选),有些版本还有BLK(背光控制)。我用的这块屏幕引脚顺序如下表所示:
| 屏幕引脚 | 功能说明 | 推荐连接ESP32引脚 |
| :--- | :--- | :--- |
| VCC | 电源正极(3.3V) | 3.3V |
| GND | 电源地 | GND |
| SCL | SPI时钟线 | GPIO 18 (VSPI CLK) |
| SDA | SPI数据线(MOSI) | GPIO 23 (VSPI MOSI) |
| RES | 复位信号 | GPIO 15 |
| DC | 数据/命令选择 | GPIO 2 |
| CS | 片选信号 | GPIO 5 |
| BLK | 背光控制(可选) | GPIO 4(通过PWM调光) |
> 提示:如果你用的ESP32板子型号不同,或者屏幕引脚顺序有差异,请务必以实际屏幕的说明书为准。RES、DC、CS这三个控制引脚可以灵活分配,不一定要完全按照我的配置,只要在代码里对应修改即可。
连接时有个小技巧:尽量使用短一点的杜邦线,并且把电源线(VCC和GND)扎在一起,可以减少信号干扰。如果屏幕背光可以独立控制,建议接到一个支持PWM输出的GPIO上,这样能实现亮度调节,在不同环境光下观看更舒服。我把它接到了GPIO 4,后面代码里会用PWM来驱动。
硬件连好后,先别急着写代码。最好用万用表测一下VCC和GND之间有没有短路,确保电源连接正确。ESP32的3.3V输出能力有限,如果屏幕功耗较大,可以考虑外接一个3.3V稳压模块,避免开发板供电不足导致屏幕显示异常。
## 2. ST7735S屏幕驱动深度解析
网上能找到的ST7735S驱动代码很多,但质量参差不齐。有的只实现了基本显示,缺少关键功能;有的初始化序列不完整,导致颜色显示异常。我参考了几个开源项目,最终整合了一个比较稳定的驱动类,不仅支持基本的图形绘制,还加入了中文字库显示和局部刷新优化。
驱动代码的核心是继承MicroPython内置的`framebuf.FrameBuffer`类。`FrameBuffer`提供了一个内存中的帧缓冲区,我们只需要实现将缓冲区数据发送到屏幕的`show()`方法即可。但ST7735S的初始化比较繁琐,需要按照特定顺序发送一系列命令和参数。
```python
from machine import SPI, Pin
import framebuf
import time
class ST7735S(framebuf.FrameBuffer):
def __init__(self, spi, dc, rst, cs, bl=None, width=128, height=160):
self.spi = spi
self.dc = Pin(dc, Pin.OUT)
self.rst = Pin(rst, Pin.OUT)
self.cs = Pin(cs, Pin.OUT)
self.bl = Pin(bl, Pin.OUT) if bl else None
self.width = width
self.height = height
self.buffer = bytearray(width * height * 2) # RGB565格式,每个像素2字节
super().__init__(self.buffer, width, height, framebuf.RGB565)
self._init_display()
def _init_display(self):
# 硬件复位
self.rst(1)
time.sleep_ms(5)
self.rst(0)
time.sleep_ms(20)
self.rst(1)
time.sleep_ms(150)
# 初始化命令序列
self._write_cmd(0x11) # 退出睡眠模式
time.sleep_ms(120)
# 颜色模式设置:16位RGB565
self._write_cmd(0x3A)
self._write_data(bytearray([0x05]))
# 内存访问控制:设置扫描方向
self._write_cmd(0x36)
self._write_data(bytearray([0xC0]))
# 关闭显示反转
self._write_cmd(0x20)
# 开启显示
self._write_cmd(0x29)
# 伽马校正参数(优化显示效果)
gamma_cmd = [
0xE0, 0x0F, 0x1A, 0x0F, 0x18, 0x2F, 0x28, 0x20,
0x22, 0x1F, 0x1B, 0x23, 0x37, 0x00, 0x07, 0x02, 0x10
]
self._write_cmd(0x26)
self._write_data(bytearray(gamma_cmd[1:]))
time.sleep_ms(100)
def _write_cmd(self, cmd):
self.dc(0) # 命令模式
self.cs(0)
self.spi.write(bytearray([cmd]))
self.cs(1)
def _write_data(self, data):
self.dc(1) # 数据模式
self.cs(0)
self.spi.write(data)
self.cs(1)
```
这个驱动类有几个关键设计点值得说明:
1. **双缓冲机制**:我们在内存中维护一个`buffer`,所有绘图操作都先在这个缓冲区进行,最后调用`show()`一次性发送到屏幕。这样避免了频繁的SPI通信,提高了刷新效率。
2. **RGB565颜色格式**:ST7735S支持多种颜色格式,RGB565(16位色)在色彩表现和内存占用之间取得了很好的平衡。每个像素用2字节表示:红色5位、绿色6位、蓝色5位。
3. **硬件SPI优化**:ESP32的硬件SPI比软件模拟快得多。初始化时我把波特率设到了40MHz,实际测试中稳定工作在30MHz左右,完全满足128x160分辨率的需求。
> 注意:有些屏幕初始化后显示方向可能是反的,或者颜色不对。这时候需要调整`0x36`命令的参数。这个命令控制内存访问方向,常见的取值有`0xC0`、`0xA0`、`0x00`等,分别对应不同的旋转角度和镜像设置。多试几次就能找到适合你屏幕的配置。
驱动类写好之后,可以写个简单的测试程序验证基本功能:
```python
def test_display():
# 使用硬件SPI1,引脚根据你的连接调整
spi = SPI(1, baudrate=30000000, polarity=0, phase=0,
sck=Pin(18), mosi=Pin(23), miso=Pin(19))
lcd = ST7735S(spi, dc=2, rst=15, cs=5, bl=4)
# 测试基本图形
lcd.fill(0x0000) # 黑色背景
lcd.rect(10, 10, 50, 30, 0xF800) # 红色矩形框
lcd.fill_rect(70, 10, 50, 30, 0x07E0) # 绿色填充矩形
lcd.text("Hello", 20, 60, 0x001F) # 蓝色文字
lcd.show()
print("显示测试完成")
```
如果屏幕上能看到红色边框、绿色方块和蓝色文字,说明驱动工作正常。这时候你可能发现英文字符显示没问题,但中文全是乱码。这是因为MicroPython默认只包含英文字库,要显示中文需要额外处理。
## 3. 中文字库集成与文本渲染方案
要在ST7735S上显示中文,最直接的方法是使用点阵字库。我选择的是16x16像素的GB2312字库,这个尺寸在128x160的屏幕上显示一行中文刚好合适。字库文件可以从开源项目获取,通常是一个包含所有汉字点阵数据的二进制文件。
字库的集成方式有两种:一是将字库文件存放在ESP32的文件系统中,运行时读取;二是将常用汉字编码成Python字典直接嵌入代码。考虑到天气时钟用到的汉字不多(主要是城市名、天气描述等),我选择了第二种方案,这样可以减少文件IO,提高显示速度。
```python
class GBKFont16x16:
def __init__(self):
# 常用汉字点阵数据(示例,实际需要完整的字库)
self.font_dict = {
"天": bytes([
0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00
]), # 这里应该是实际的点阵数据
"气": bytes([...]),
# ... 更多汉字
}
def get_char(self, char):
# 返回指定字符的点阵数据
return self.font_dict.get(char, None)
def text_width(self, text):
# 计算文本宽度(像素)
return len(text) * 16
def text_height(self):
return 16
# 在ST7735S驱动类中添加中文显示方法
def draw_chinese(self, text, x, y, color, bg_color=None):
font = GBKFont16x16()
for i, char in enumerate(text):
data = font.get_char(char)
if data:
# 将点阵数据绘制到屏幕上
for row in range(16):
byte1 = data[row * 2]
byte2 = data[row * 2 + 1]
for col in range(16):
bit_pos = 15 - col
if bit_pos >= 8:
pixel = (byte2 >> (bit_pos - 8)) & 0x01
else:
pixel = (byte1 >> bit_pos) & 0x01
if pixel:
self.pixel(x + i*16 + col, y + row, color)
elif bg_color is not None:
self.pixel(x + i*16 + col, y + row, bg_color)
```
实际项目中,我建议使用外部字库文件,因为嵌入所有汉字点阵会让代码变得非常庞大。可以从网上下载标准的GB2312 16x16点阵字库文件(通常叫`HZK16`),然后实现一个文件读取器:
```python
class HZK16Font:
def __init__(self, filename='hzk16'):
self.font_file = open(filename, 'rb')
def get_char(self, char):
# GB2312编码计算偏移量
code = char.encode('gb2312')
if len(code) != 2:
return None
zone = code[0] - 0xA1
pos = code[1] - 0xA1
offset = (zone * 94 + pos) * 32 # 每个字符32字节
self.font_file.seek(offset)
return self.font_file.read(32)
def deinit(self):
self.font_file.close()
```
使用外部字库时,需要先将字库文件上传到ESP32的文件系统。在Thonny中,可以通过"视图→文件"打开文件管理器,然后将`hzk16`文件拖到设备目录下。这样虽然增加了一点存储空间占用,但换来了完整的汉字支持。
对于天气时钟来说,我们还需要显示温度数字、时间数字等。MicroPython自带的`framebuf`模块提供了8x8像素的英文字库,但对于大号数字显示不够美观。我实现了一个简单的数字放大算法,可以将8x8字库渲染成16x16或更大的尺寸:
```python
def draw_big_number(self, num_str, x, y, color, scale=2):
"""绘制放大数字,scale为放大倍数"""
for i, char in enumerate(num_str):
# 获取原始8x8点阵
char_data = self._get_8x8_char(char)
if not char_data:
continue
# 放大渲染
for py in range(8):
for px in range(8):
if char_data[py] & (1 << (7 - px)):
# 绘制放大后的像素块
self.fill_rect(
x + i * 8 * scale + px * scale,
y + py * scale,
scale, scale, color
)
```
这样处理之后,温度数字可以显示得更大更清晰。在实际的天气时钟界面中,当前温度我会用32x32的大字体显示,时间用24x24的字体,其他信息用16x16的常规字体,形成清晰的视觉层次。
## 4. 网络连接与天气数据获取实战
ESP32的Wi-Fi功能很强大,但实际使用中会遇到各种网络问题。我总结了一个相对健壮的连接方案,包含自动重连、超时处理和错误恢复机制。首先需要配置Wi-Fi连接:
```python
import network
import time
import ntptime
class WiFiManager:
def __init__(self, ssid, password):
self.ssid = ssid
self.password = password
self.wlan = network.WLAN(network.STA_IF)
self.connected = False
def connect(self, max_retries=5):
if not self.wlan.isconnected():
self.wlan.active(True)
self.wlan.connect(self.ssid, self.password)
retry_count = 0
while not self.wlan.isconnected() and retry_count < max_retries:
print(f'连接中... ({retry_count + 1}/{max_retries})')
time.sleep(2)
retry_count += 1
if self.wlan.isconnected():
self.connected = True
print('Wi-Fi连接成功')
print('IP地址:', self.wlan.ifconfig()[0])
# 同步网络时间
self.sync_time()
return True
else:
print('Wi-Fi连接失败')
return False
return True
def sync_time(self, retries=3):
for i in range(retries):
try:
ntptime.settime()
print('时间同步成功')
return True
except Exception as e:
print(f'时间同步失败 ({i+1}/{retries}): {e}')
time.sleep(1)
return False
def is_connected(self):
return self.wlan.isconnected()
```
连接Wi-Fi后,下一步是获取天气数据。国内有很多免费的天气API,比如和风天气、心知天气等。我选择了一个简单的开放API作为示例,实际使用时需要替换成你自己的API密钥。
```python
import urequests
import json
class WeatherClient:
def __init__(self, api_key, city_code):
self.api_key = api_key
self.city_code = city_code
self.base_url = "http://api.seniverse.com/v3/weather/now.json"
def fetch_weather(self):
try:
# 构建请求URL
url = f"{self.base_url}?key={self.api_key}&location={self.city_code}&language=zh-Hans&unit=c"
# 发送HTTP GET请求
response = urequests.get(url, timeout=10)
if response.status_code == 200:
data = json.loads(response.text)
response.close()
# 解析天气数据
weather_info = {
'temp': data['results'][0]['now']['temperature'],
'humidity': data['results'][0]['now']['humidity'],
'text': data['results'][0]['now']['text'],
'wind_dir': data['results'][0]['now']['wind_direction'],
'wind_scale': data['results'][0]['now']['wind_scale']
}
return weather_info
else:
print(f'API请求失败: {response.status_code}')
response.close()
return None
except Exception as e:
print(f'获取天气数据出错: {e}')
return None
def fetch_forecast(self):
"""获取天气预报(未来几天)"""
try:
url = f"http://api.seniverse.com/v3/weather/daily.json?key={self.api_key}&location={self.city_code}&language=zh-Hans&unit=c&start=0&days=3"
response = urequests.get(url, timeout=10)
if response.status_code == 200:
data = json.loads(response.text)
response.close()
forecast = []
for day in data['results'][0]['daily'][:3]: # 取最近3天
forecast.append({
'date': day['date'],
'high': day['high'],
'low': day['low'],
'text': day['text_day']
})
return forecast
else:
response.close()
return None
except Exception as e:
print(f'获取预报出错: {e}')
return None
```
> 注意:在实际部署时,建议将API密钥等敏感信息存储在单独的配置文件中,不要硬编码在代码里。另外,免费API通常有调用频率限制,天气时钟这种应用每10-30分钟更新一次数据就足够了,不要频繁请求。
网络请求可能会因为各种原因失败(信号弱、服务器问题等),所以需要完善的错误处理。我设计了一个带重试和缓存机制的获取流程:
```python
class WeatherManager:
def __init__(self, api_key, city_code):
self.client = WeatherClient(api_key, city_code)
self.last_update = 0
self.cache = None
self.cache_time = 1800 # 缓存30分钟(秒)
def get_weather(self):
current_time = time.time()
# 如果缓存有效且未过期,直接返回缓存
if self.cache and (current_time - self.last_update) < self.cache_time:
return self.cache
# 否则重新获取
for attempt in range(3):
weather = self.client.fetch_weather()
if weather:
self.cache = weather
self.last_update = current_time
return weather
time.sleep(2 ** attempt) # 指数退避重试
# 所有重试都失败,返回缓存(如果有)或默认值
return self.cache or {
'temp': '--',
'humidity': '--',
'text': '获取失败',
'wind_dir': '--',
'wind_scale': '--'
}
```
这个设计确保了即使网络暂时不可用,时钟也能显示最近一次成功获取的数据,而不是空白或错误信息。对于时间显示,我们还需要处理时区问题。中国使用东八区时间(UTC+8),但NTP服务器返回的是UTC时间,需要手动调整:
```python
def get_local_time():
# 获取当前UTC时间
utc_time = time.time()
# 转换为东八区时间(+8小时)
local_time = utc_time + 8 * 3600
# 格式化时间
t = time.localtime(local_time)
return {
'year': t[0],
'month': t[1],
'day': t[2],
'hour': t[3],
'minute': t[4],
'second': t[5],
'weekday': t[6]
}
```
有了时间、天气数据,接下来就是最重要的部分:如何在小小的128x160屏幕上优雅地展示所有信息。
## 5. 界面设计与显示优化技巧
128x160的分辨率确实有限,但通过合理的布局和视觉设计,完全可以呈现清晰美观的天气时钟界面。我的设计方案将屏幕分为四个区域:
1. **顶部状态栏**(高度20像素):显示Wi-Fi连接状态、电池电量(如果有)、更新时间
2. **主时间显示区**(高度60像素):大字体显示当前时间(小时:分钟)
3. **天气信息区**(高度50像素):显示温度、湿度、天气图标和描述
4. **底部信息区**(高度30像素):显示日期、星期、城市名称
具体实现时,我创建了一个`WeatherClockUI`类来管理所有绘制逻辑:
```python
class WeatherClockUI:
def __init__(self, display):
self.display = display
self.width = display.width
self.height = display.height
# 颜色定义(RGB565格式)
self.COLORS = {
'bg': 0x0000, # 黑色背景
'time': 0xFFFF, # 白色时间
'temp': 0xF800, # 红色温度
'humidity': 0x07E0, # 绿色湿度
'date': 0x7BEF, # 灰色日期
'status': 0x7BEF, # 灰色状态
'city': 0x7BEF, # 灰色城市名
}
# 天气图标定义(16x16像素)
self.ICONS = {
'晴': self._create_sun_icon(),
'多云': self._create_cloud_icon(),
'阴': self._create_cloudy_icon(),
'雨': self._create_rain_icon(),
'雪': self._create_snow_icon(),
# ... 更多天气图标
}
def draw_background(self):
"""绘制背景和分割线"""
self.display.fill(self.COLORS['bg'])
# 绘制区域分割线
self.display.hline(0, 20, self.width, 0x4208) # 状态栏下划线
self.display.hline(0, 80, self.width, 0x4208) # 时间区下划线
self.display.hline(0, 130, self.width, 0x4208) # 天气区下划线
def draw_time(self, hour, minute):
"""绘制时间(大字体)"""
time_str = f"{hour:02d}:{minute:02d}"
# 计算居中位置
time_width = len(time_str) * 16 # 假设每个数字16像素宽
x = (self.width - time_width) // 2
y = 25 # 时间区垂直居中
# 绘制小时和分钟
for i, char in enumerate(time_str):
if char == ':':
# 绘制冒号(两个点)
self.display.fill_rect(x + i*16 + 4, y + 6, 4, 4, self.COLORS['time'])
self.display.fill_rect(x + i*16 + 4, y + 14, 4, 4, self.COLORS['time'])
else:
self.draw_big_number(char, x + i*16, y, self.COLORS['time'], scale=2)
def draw_weather(self, temp, humidity, weather_text):
"""绘制天气信息"""
# 温度(左侧)
temp_str = f"{temp}°C"
self.display.text(temp_str, 10, 85, self.COLORS['temp'])
# 湿度(右侧)
humidity_str = f"{humidity}%"
humidity_x = self.width - len(humidity_str)*8 - 10
self.display.text(humidity_str, humidity_x, 85, self.COLORS['humidity'])
# 天气图标(居中)
if weather_text in self.ICONS:
icon_x = (self.width - 16) // 2
self._draw_icon(icon_x, 100, self.ICONS[weather_text])
# 天气描述文字
text_x = (self.width - len(weather_text)*8) // 2
self.display.text(weather_text, text_x, 120, self.COLORS['temp'])
def draw_date_city(self, year, month, day, weekday, city):
"""绘制日期和城市信息"""
# 日期格式:YYYY-MM-DD 星期X
date_str = f"{year}-{month:02d}-{day:02d}"
weekday_str = ["一", "二", "三", "四", "五", "六", "日"][weekday]
date_full = f"{date_str} 星期{weekday_str}"
date_x = (self.width - len(date_full)*8) // 2
self.display.text(date_full, date_x, 140, self.COLORS['date'])
# 城市名称
city_x = (self.width - len(city)*8) // 2
self.display.text(city, city_x, 155, self.COLORS['city'])
def draw_status(self, wifi_connected, last_update):
"""绘制状态栏信息"""
# Wi-Fi状态图标
if wifi_connected:
self._draw_wifi_icon(5, 5)
else:
self._draw_no_wifi_icon(5, 5)
# 最后更新时间
update_str = f"更新:{last_update:02d}:{time.localtime()[4]:02d}"
update_x = self.width - len(update_str)*8 - 5
self.display.text(update_str, update_x, 5, self.COLORS['status'])
def update_display(self, time_data, weather_data, city, wifi_status):
"""完整更新显示"""
self.draw_background()
self.draw_time(time_data['hour'], time_data['minute'])
self.draw_weather(weather_data['temp'], weather_data['humidity'], weather_data['text'])
self.draw_date_city(time_data['year'], time_data['month'],
time_data['day'], time_data['weekday'], city)
self.draw_status(wifi_status, time_data['minute'])
self.display.show()
```
界面设计有几个优化点值得注意:
1. **避免频繁全屏刷新**:每次更新只修改变化的部分,比如时间每分钟变一次,就只重绘时间区域;天气数据每30分钟更新一次,就只重绘天气区域。这样可以减少闪烁,提高响应速度。
2. **使用双缓冲减少闪烁**:虽然我们的驱动已经使用了帧缓冲区,但在更新复杂界面时,如果直接修改缓冲区然后刷新,仍然可能看到绘制过程。更好的做法是在内存中准备两个缓冲区,一个用于绘制,完成后交换到显示缓冲区。
3. **动画过渡效果**:在时间变化时,可以添加简单的数字滚动动画;在天气图标切换时,可以添加淡入淡出效果。这些细节能显著提升用户体验。
4. **夜间模式**:根据时间自动切换深色/浅色主题。晚上使用深色背景、低亮度,减少对眼睛的刺激。
## 6. 完整项目集成与性能调优
把前面所有模块组合起来,就得到了完整的天气时钟程序。主程序的结构如下:
```python
import machine
import time
import network
import ntptime
import urequests
import json
from machine import SPI, Pin, PWM
def main():
# 硬件初始化
spi = SPI(1, baudrate=30000000, polarity=0, phase=0,
sck=Pin(18), mosi=Pin(23), miso=Pin(19))
# 初始化显示屏
display = ST7735S(spi, dc=2, rst=15, cs=5, bl=4)
# 初始化背光PWM(可选)
pwm = PWM(Pin(4))
pwm.freq(1000)
pwm.duty(800) # 80%亮度
# 初始化Wi-Fi
wifi = WiFiManager("你的Wi-Fi名称", "你的Wi-Fi密码")
# 初始化天气客户端
weather_mgr = WeatherManager("你的API密钥", "城市代码")
# 初始化UI
ui = WeatherClockUI(display)
# 连接Wi-Fi
if not wifi.connect():
print("Wi-Fi连接失败,进入离线模式")
# 显示错误信息
display.fill(0)
display.text("Wi-Fi连接失败", 20, 60, 0xF800)
display.text("检查配置后重启", 20, 80, 0xF800)
display.show()
return
# 主循环
last_weather_update = 0
last_minute = -1
while True:
current_time = time.time()
local_time = get_local_time()
# 每分钟更新一次时间显示
if local_time['minute'] != last_minute:
last_minute = local_time['minute']
# 每30分钟更新一次天气
if current_time - last_weather_update > 1800:
weather_data = weather_mgr.get_weather()
last_weather_update = current_time
else:
# 使用缓存的天气数据
weather_data = weather_mgr.cache
# 更新显示
ui.update_display(local_time, weather_data, "北京", wifi.is_connected())
# 整点报时(可选)
if local_time['minute'] == 0 and local_time['second'] == 0:
# 可以在这里添加蜂鸣器提示音
pass
# 根据时间调整背光亮度
hour = local_time['hour']
if 22 <= hour or hour < 6: # 夜间
pwm.duty(200) # 20%亮度
else: # 白天
pwm.duty(800) # 80%亮度
# 休眠一段时间,降低功耗
time.sleep(0.5)
if __name__ == "__main__":
main()
```
这个主循环有几个关键设计:
1. **事件驱动更新**:不是固定时间间隔刷新,而是根据数据变化决定何时更新。时间每分钟变一次,天气每30分钟更新一次,这样既保证了信息及时性,又减少了不必要的网络请求和屏幕刷新。
2. **功耗优化**:ESP32在运行状态功耗不低,但我们的天气时钟不需要实时处理复杂任务。在循环末尾添加`time.sleep(0.5)`,让CPU大部分时间处于空闲状态,可以显著降低功耗。如果使用电池供电,还可以考虑深度睡眠模式,每分钟唤醒一次更新显示。
3. **错误恢复机制**:Wi-Fi连接可能意外断开,API服务可能暂时不可用。程序需要能够检测这些异常并尝试恢复,而不是直接崩溃。
4. **配置管理**:把Wi-Fi密码、API密钥、城市代码等配置信息放在单独的`config.py`文件中,方便修改而不影响主程序逻辑。
```python
# config.py
WIFI_SSID = "你的Wi-Fi名称"
WIFI_PASSWORD = "你的Wi-Fi密码"
WEATHER_API_KEY = "你的天气API密钥"
CITY_CODE = "beijing" # 城市代码
TIMEZONE_OFFSET = 8 # 时区偏移(东八区)
```
对于想要进一步优化的开发者,这里还有一些进阶技巧:
**内存优化**:MicroPython环境内存有限(通常只有几百KB),需要特别注意内存使用。避免创建大量临时对象,尽量复用缓冲区。比如天气数据解析时,直接操作接收到的bytes对象,而不是转换成字符串再解析。
**显示性能**:SPI通信速度是显示性能的瓶颈。可以通过以下方式优化:
- 使用DMA传输(如果硬件支持)
- 压缩传输数据(比如只传输变化区域)
- 使用硬件加速的图形操作
**电源管理**:如果使用电池供电,可以添加电池电量检测,在电量低时降低屏幕亮度、减少刷新频率。还可以实现自动关机功能,在特定时间段(比如深夜)完全关闭显示。
**扩展功能**:基本的天气时钟完成后,可以考虑添加更多实用功能:
- 显示室内温湿度(需要连接DHT11/DHT22传感器)
- 显示空气质量指数(需要连接PM2.5传感器)
- 添加按钮切换显示模式(时间/天气/传感器数据)
- 支持OTA无线更新固件
最后,部署到实际设备时,建议将主程序保存为`main.py`,这样ESP32上电后会自动运行。开发调试阶段,可以先保存为其他名称(如`weather_clock.py`),通过REPL手动启动,方便修改和测试。
整个项目从硬件连接到软件实现,涉及了嵌入式开发的多个方面:外设驱动、网络通信、数据解析、用户界面、电源管理等。虽然只是一个简单的天气时钟,但涵盖了物联网设备开发的典型流程。希望这个详细的实现方案能帮助你顺利完成自己的项目,也欢迎在此基础上继续扩展功能。