基于ESP32与蓝牙SPP打造PC系统监控仪表盘:从硬件选型到UI优化

基于ESP32与蓝牙SPP打造PC系统监控仪表盘:从硬件选型到UI优化

1. 项目概述:从想法到现实

最近在折腾一个挺有意思的小项目,起因是工作室里那台主力渲染和编译的PC机。机器性能是够的,但每次想看看它在跑什么任务、CPU温度多少、内存占用率如何,都得切到任务管理器或者打开一堆监控软件,界面一多,屏幕就乱,打断工作流。我就琢磨着,能不能搞一个独立的、物理形态的监控屏,像汽车仪表盘一样,把关键信息实时、直观地展示出来,而且最好能无线连接,不占USB口,摆放也自由。

这个想法让我立刻想到了ESP32这块“万能”的芯片。它自带Wi-Fi和蓝牙,性能对于处理传感器数据和驱动屏幕绰绰有余,关键是开发社区生态极其丰富。于是,“基于ESP32制造系统监控屏,支持与PC蓝牙连接”这个项目就成型了。本质上,它就是一个运行在ESP32上的客户端,通过蓝牙从PC端的一个服务程序获取系统状态数据(如CPU/GPU占用率、温度、内存、网络流量等),然后在一块小屏幕上以仪表、图表等形式美观地展示出来。

这玩意儿适合谁呢?我觉得三类朋友可能会感兴趣:一是像我一样的DIY爱好者和极客,享受自己动手打造专属工具的乐趣;二是对PC硬件状态有实时监控需求的用户,比如超频玩家、视频渲染工作者或者游戏主播;三是想学习ESP32蓝牙通信、嵌入式UI开发以及PC端服务编写的开发者。它不只是一个成品,更是一个融合了硬件、嵌入式软件、桌面应用和通信协议的综合学习项目。

2. 核心方案设计与硬件选型

2.1 整体架构拆解

整个系统的核心是数据流:PC端采集数据 -> 通过蓝牙发送 -> ESP32接收并解析 -> 屏幕渲染显示。因此,方案设计需要围绕这条数据链路展开。

为什么选择蓝牙而非Wi-Fi?这是一个关键决策点。Wi-Fi和蓝牙(这里特指经典蓝牙SPP协议)都能实现数据传输。Wi-Fi的优势是带宽高、传输距离远,但连接过程相对复杂(需要配网),功耗也更高。对于我们这个监控场景,数据量很小(每秒几KB到几十KB足矣),传输距离要求不高(通常就在电脑桌附近),更重要的是追求连接的稳定性和简便性。蓝牙配对一次后,基本可以做到开机自连,ESP32作为从机,PC作为主机,连接逻辑清晰可靠。因此,蓝牙SPP(串口协议)是更轻量、更直接的选择。

系统工作流程如下:

  1. PC端:运行一个后台服务(如用Python或C#编写),周期性地(例如每秒)调用系统API(如psutil库)获取CPU、内存、磁盘、网络、传感器温度等数据。
  2. 数据格式化:将采集到的数据打包成一个结构化的字符串或JSON格式,例如{“cpu”: 45.2, “mem”: 76.5, “gpu_temp”: 68}
  3. 蓝牙发送:PC通过已配对的蓝牙虚拟串口(COM口),将格式化后的数据字符串发送出去。
  4. ESP32端:通过蓝牙串口接收数据,解析JSON或自定义协议,提取出各项数值。
  5. 屏幕渲染:ESP32根据解析出的数据,调用图形库(如LVGL、TFT_eSPI)的函数,在屏幕上绘制进度条、数字、仪表盘或简易折线图。

2.2 硬件清单与选型理由

硬件是项目的骨架,选型直接决定了成本、显示效果和开发难度。

1. 主控芯片:ESP32开发板

  • 型号选择:ESP32-S3或ESP32-C3是较新的选择,性能更好且性价比高。但对于本项目,最经典且资料最多的ESP32-WROOM-32D/E完全够用。我手头正好有NodeMCU-32S(基于ESP32),就用了它。
  • 选型理由:集成双核处理器、蓝牙4.2 BR/EDR和BLE、Wi-Fi,内存(520KB SRAM)足够驱动UI和缓冲数据。引脚丰富,可轻松连接屏幕。庞大的Arduino和ESP-IDF社区支持,遇到问题几乎都能找到答案。

2. 显示屏幕:TFT液晶屏

  • 接口选择:SPI接口的屏幕是首选,因为它占用的GPIO口少(通常只需4-6个),驱动简单,虽然刷新率比并行接口(如ILI9341的8位并行)低,但对于刷新率要求不高的监控数据(1-2秒更新一次)完全足够,且成本更低。
  • 尺寸与分辨率:1.3寸、1.54寸或2寸的IPS屏是热门选择。我选用了一块1.54寸的IPS屏,分辨率240x240。这个尺寸放在桌面不占地方,分辨率足以清晰显示多个数据 widget。IPS屏的可视角度广,从侧面看也不会偏色。
  • 具体型号:我用的屏幕驱动芯片是ST7789,这是一款非常流行的SPI TFT驱动芯片,有成熟的Arduino库(TFT_eSPI)支持。

3. 其他必要材料

  • 杜邦线(母对母):用于连接ESP32和屏幕。
  • USB数据线:用于给ESP32供电和烧录程序。
  • 可选-外壳:3D打印一个外壳能让成品看起来更精致,也能保护电路。可以在Thingiverse等网站搜索“ESP32 TFT case”找到很多现成模型。

注意:屏幕选购陷阱。网上很多“ESP32 屏幕套装”卖的是并口屏幕,虽然可能附带转接板,但会占用大量GPIO(可能超过10个),对于GPIO资源紧张或想接其他传感器的项目不友好。购买时一定要确认是“SPI接口”的TFT屏幕。

3. 软件环境搭建与核心库配置

3.1 PC端数据采集服务搭建

PC端我们需要一个常驻后台的程序,负责采集并发送数据。Python因其简洁和强大的库生态成为首选。

1. 安装Python与环境依赖确保你的PC安装了Python 3.6以上版本。然后通过pip安装核心库:

pip install psutil pybluez
  • psutil:跨平台的系统监控库,能轻松获取CPU、内存、磁盘、网络、传感器信息。
  • pybluez:Python的蓝牙库,用于创建蓝牙串口服务。在Windows上安装pybluez可能需要一点技巧,有时使用pip install pybluez2或寻找预编译的whl文件更顺利。也可以考虑使用bleak库(专注于BLE),但经典蓝牙SPP更通用。

2. 编写数据采集与发送脚本核心是创建一个循环,每秒采集一次数据,并通过蓝牙串口发送。这里假设你已经将ESP32与PC配对,并在Windows的“设备与打印机”中创建了传入的蓝牙虚拟串口(例如COM5)。

import psutil import time import json import serial # 使用pyserial库,需安装:pip install pyserial # 配置蓝牙虚拟串口号和波特率 bluetooth_port = 'COM5' # 请根据你的实际端口修改 baud_rate = 115200 def get_system_stats(): """采集系统状态数据""" stats = { "cpu_percent": psutil.cpu_percent(interval=None), # 非阻塞式获取 "mem_percent": psutil.virtual_memory().percent, "mem_used_gb": round(psutil.virtual_memory().used / (1024**3), 2), "disk_percent": psutil.disk_usage('C:/').percent, # 监控C盘 } # 尝试获取CPU温度(Windows可能需要额外支持,如OpenHardwareMonitor) try: temps = psutil.sensors_temperatures() if 'coretemp' in temps: stats['cpu_temp'] = temps['coretemp'][0].current else: stats['cpu_temp'] = 0 except: stats['cpu_temp'] = 0 # 网络流量(每秒字节数) net_before = psutil.net_io_counters() time.sleep(1) net_after = psutil.net_io_counters() stats['net_sent_kbps'] = (net_after.bytes_sent - net_before.bytes_sent) / 1024 stats['net_recv_kbps'] = (net_after.bytes_recv - net_before.bytes_recv) / 1024 return stats def main(): print(f"尝试连接到 {bluetooth_port}...") try: # 初始化串口连接 ser = serial.Serial(bluetooth_port, baudrate=baud_rate, timeout=1) print("蓝牙串口连接成功!") except serial.SerialException as e: print(f"连接失败: {e}") print("请检查:1. ESP32是否已上电并进入蓝牙等待模式;2. 电脑蓝牙是否已配对并创建了COM口;3. COM口号是否正确。") return try: while True: data = get_system_stats() # 将数据转换为JSON字符串并发送 data_str = json.dumps(data) + '\n' # 加换行符作为数据包分隔符 ser.write(data_str.encode('utf-8')) print(f"已发送: {data_str.strip()}") time.sleep(1) # 每秒发送一次 except KeyboardInterrupt: print("\n程序被用户中断。") finally: ser.close() print("串口已关闭。") if __name__ == "__main__": main()

实操心得:Windows蓝牙串口稳定性。有时Windows的蓝牙虚拟串口会不稳定或连接失败。一个更可靠的方法是让Python脚本直接通过Socket与ESP32的蓝牙通信,但这需要更底层的蓝牙操作。对于初学者,先确保能用系统自带的“蓝牙串口”功能连接成功是关键。如果遇到问题,可以尝试重新配对,或使用第三方蓝牙串口工具(如Tera Term)先测试连通性。

3.2 ESP32端开发环境与编程

ESP32端我们使用Arduino IDE进行开发,因为它对新手友好,库管理方便。

1. Arduino IDE环境配置

  • 安装Arduino IDE(1.8.x或2.0版本均可)。
  • 在“文件”->“首选项”的“附加开发板管理器网址”中,添加ESP32的板支持网址:https://espressif.github.io/arduino-esp32/package_esp32_index.json
  • 打开“工具”->“开发板”->“开发板管理器”,搜索“esp32”,安装“Espressif Systems”提供的ESP32开发板包。
  • 安装完成后,在“工具”->“开发板”中选择你的ESP32型号(如“NodeMCU-32S”)。

2. 安装必要的库打开“工具”->“管理库...”,搜索并安装以下库:

  • TFT_eSPI:这是驱动SPI TFT屏幕的绝佳库,支持众多芯片(ST7789, ILI9341等),且经过高度优化。
  • ArduinoJson:用于解析从PC端发送过来的JSON格式数据,必不可少。

3. 配置TFT_eSPI库这是关键一步,很多新手在这里卡住。安装完TFT_eSPI库后,你需要到Arduino的库文件夹下找到它,并编辑其用户配置文件。

  • 路径通常类似:C:\Users\[你的用户名]\Documents\Arduino\libraries\TFT_eSPI
  • 找到User_Setup.hUser_Setup_Select.h文件。更简单的方法是:在TFT_eSPI库文件夹内,找到一个叫User_Setups的文件夹,里面有很多现成的针对不同屏幕的配置文件。找到与你屏幕最匹配的(例如Setup24_ST7789.h对应240x240的ST7789)。
  • 然后,回到库根目录,打开User_Setup_Select.h文件,注释掉其他所有的#include,取消注释(或添加)指向你屏幕配置文件的那一行。例如:
    //#include <User_Setup.h> // Default setup is root library folder #include <User_Setups/Setup24_ST7789.h> // Setup file for ESP32 and ST7789 240x240
  • 接着,打开你选择的那个具体配置文件(如Setup24_ST7789.h),根据屏幕引脚定义和你的接线,修改以下关键参数:
    #define TFT_CS 15 // 屏幕片选引脚 #define TFT_DC 2 // 数据/命令选择引脚 #define TFT_RST 4 // 复位引脚(如果屏幕有,否则接ESP32的EN或NC) #define TFT_MOSI 23 // SPI数据输入(主出从入) #define TFT_SCLK 18 // SPI时钟 // 背光控制引脚(如果有) #define TFT_BL 12 // 背光控制 #define TFT_BACKLIGHT_ON HIGH // 背光点亮电平
    务必根据你的实际接线修改这些引脚号!错误的引脚定义会导致白屏或花屏。

4. ESP32端程序编写与解析

4.1 蓝牙通信与数据解析

ESP32端的核心任务是建立蓝牙串口服务,接收数据,解析并驱动显示。

#include <BluetoothSerial.h> #include <ArduinoJson.h> #include <TFT_eSPI.h> TFT_eSPI tft = TFT_eSPI(); BluetoothSerial SerialBT; // 定义屏幕布局和颜色 #define SCREEN_WIDTH 240 #define SCREEN_HEIGHT 240 #define CHAR_WIDTH 12 // 大约字符宽度 #define CHAR_HEIGHT 16 // 大约字符高度 // 全局变量存储接收到的数据 float cpuPercent = 0; float memPercent = 0; float cpuTemp = 0; float netSent = 0; float netRecv = 0; void setup() { Serial.begin(115200); tft.init(); tft.setRotation(1); // 根据屏幕方向调整,0-3 tft.fillScreen(TFT_BLACK); tft.setTextColor(TFT_WHITE, TFT_BLACK); tft.setTextSize(2); // 初始化蓝牙串口,设备名可自定义 if (!SerialBT.begin("ESP32-System-Monitor")) { Serial.println("蓝牙初始化失败!"); while (1); // 停止执行 } Serial.println("蓝牙已启动,等待连接..."); tft.drawString("BT: Waiting...", 10, 10); // 绘制静态UI框架 drawStaticUI(); } void loop() { // 检查蓝牙是否有数据可读 if (SerialBT.available()) { String receivedData = SerialBT.readStringUntil('\n'); // 以换行符为分隔读取一行 receivedData.trim(); Serial.println("Received: " + receivedData); // 解析JSON数据 DynamicJsonDocument doc(512); // 根据数据大小调整缓冲区 DeserializationError error = deserializeJson(doc, receivedData); if (!error) { // 提取数据到全局变量 cpuPercent = doc["cpu_percent"]; memPercent = doc["mem_percent"]; cpuTemp = doc["cpu_temp"]; netSent = doc["net_sent_kbps"]; netRecv = doc["net_recv_kbps"]; // 更新屏幕显示 updateDisplay(); } else { Serial.print("JSON解析错误: "); Serial.println(error.c_str()); } } delay(100); // 短暂延迟,避免过于频繁的循环 } void drawStaticUI() { tft.fillScreen(TFT_BLACK); tft.setTextSize(2); tft.setTextColor(TFT_CYAN, TFT_BLACK); tft.drawString("System Monitor", 20, 5); tft.setTextSize(1); tft.setTextColor(TFT_WHITE, TFT_BLACK); // 绘制静态标签 tft.drawString("CPU:", 20, 40); tft.drawString("%", 180, 40); tft.drawString("MEM:", 20, 70); tft.drawString("%", 180, 70); tft.drawString("TEMP:", 20, 100); tft.drawString("C", 180, 100); tft.drawString("NET UP:", 20, 130); tft.drawString("KB/s", 180, 130); tft.drawString("NET DN:", 20, 160); tft.drawString("KB/s", 180, 160); // 绘制进度条背景框 tft.drawRect(60, 38, 110, 18, TFT_DARKGREY); // CPU进度条框 tft.drawRect(60, 68, 110, 18, TFT_DARKGREY); // MEM进度条框 } void updateDisplay() { tft.setTextSize(2); // 更新CPU百分比和进度条 drawProgressBar(60, 38, 110, 18, cpuPercent, TFT_GREEN, TFT_DARKGREY); tft.setTextColor(TFT_WHITE, TFT_BLACK); tft.setCursor(140, 40); tft.printf("%2.0f", cpuPercent); // 更新内存百分比和进度条 drawProgressBar(60, 68, 110, 18, memPercent, TFT_BLUE, TFT_DARKGREY); tft.setCursor(140, 70); tft.printf("%2.0f", memPercent); // 更新温度 tft.setCursor(140, 100); tft.setTextColor(getTempColor(cpuTemp), TFT_BLACK); tft.printf("%2.0f", cpuTemp); // 更新网络速度 tft.setTextColor(TFT_WHITE, TFT_BLACK); tft.setCursor(140, 130); tft.printf("%3.0f", netSent); tft.setCursor(140, 160); tft.printf("%3.0f", netRecv); } // 绘制进度条函数 void drawProgressBar(uint16_t x, uint16_t y, uint16_t w, uint16_t h, float percent, uint32_t color, uint32_t bgColor) { uint16_t barWidth = (percent / 100.0) * w; barWidth = constrain(barWidth, 0, w); // 限制宽度不超过总宽 tft.fillRect(x, y, w, h, bgColor); // 清空背景 tft.fillRect(x, y, barWidth, h, color); // 绘制填充部分 } // 根据温度返回颜色(低温绿,高温红) uint32_t getTempColor(float temp) { if (temp < 50) return TFT_GREEN; else if (temp < 70) return TFT_YELLOW; else return TFT_RED; }

4.2 显示优化与UI设计

上面的代码实现了基本功能,但UI比较简陋。我们可以利用TFT_eSPI库的强大功能进行优化。

1. 使用Sprite提升刷新效率直接在全屏上反复绘制文字和图形,即使区域很小,也可能导致闪烁。TFT_eSPISprite(精灵)功能可以创建一块内存中的画布,先在画布上完成所有绘制,然后一次性将整块画布推送到屏幕,实现无闪烁更新。

TFT_eSprite spr = TFT_eSprite(&tft); // 创建一个Sprite对象 void setup() { // ... 其他初始化代码 tft.init(); // 创建Sprite,大小与更新区域一致或略大 spr.createSprite(200, 120); // 假设我们只更新下方数据区域 spr.setTextColor(TFT_WHITE, TFT_BLACK); } void updateDisplay() { spr.fillSprite(TFT_BLACK); // 清空Sprite // 在Sprite上绘制所有动态内容 drawProgressBarOnSprite(0, 0, 110, 18, cpuPercent, TFT_GREEN); spr.setCursor(120, 0); spr.printf("CPU:%2.0f%%", cpuPercent); // ... 绘制其他内容 // 将Sprite推送到屏幕的指定位置 spr.pushSprite(20, 60); // (x, y)是Sprite在屏幕上的起始位置 }

2. 绘制更美观的图表对于网络速度这类随时间变化的数据,折线图比单纯的数字更直观。我们可以维护一个历史数据数组,在Sprite上绘制简单的折线图。

#define HISTORY_SIZE 50 float netSentHistory[HISTORY_SIZE] = {0}; int historyIndex = 0; void updateNetGraph() { // 将新数据存入历史数组 netSentHistory[historyIndex] = netSent; historyIndex = (historyIndex + 1) % HISTORY_SIZE; // 在Sprite上绘制折线图背景和坐标轴 spr.drawRect(10, 90, 180, 40, TFT_DARKGREY); // 绘制折线 int16_t prevX = 10, prevY = 130; for (int i = 0; i < HISTORY_SIZE; i++) { int idx = (historyIndex + i) % HISTORY_SIZE; int16_t x = 10 + (i * 180 / HISTORY_SIZE); // 将数值映射到Y轴坐标(假设最大值为200 KB/s) int16_t y = 130 - constrain(netSentHistory[idx], 0, 200) * 40 / 200; if (i > 0) { spr.drawLine(prevX, prevY, x, y, TFT_CYAN); } prevX = x; prevY = y; } }

5. 系统联调与问题排查实录

将代码分别烧录到ESP32和运行在PC端后,真正的挑战才开始。以下是几个我踩过的坑和解决方法。

5.1 连接建立失败

现象:PC端Python脚本报错SerialException: could not open port 'COM5': Permission denied[WinError 121]

  • 排查1:确认端口号。在Windows设备管理器的“端口(COM和LPT)”下查看蓝牙串口对应的COM号,确保脚本中的端口号一致。
  • 排查2:关闭占用程序。确保没有其他软件(如Arduino IDE的串口监视器、Putty、其他蓝牙终端)正在占用这个COM口。
  • 排查3:重新配对。有时蓝牙配对信息会出错。在电脑和ESP32上删除旧的配对记录,重新进行配对。ESP32端可以在setup()里使用SerialBT.disconnect()SerialBT.end()后再重新begin()
  • 排查4:检查ESP32蓝牙初始化。确保ESP32程序已烧录并运行,且蓝牙名称正确。可以在手机蓝牙搜索列表中查看是否能找到“ESP32-System-Monitor”。

5.2 数据接收乱码或不全

现象:ESP32串口监视器显示乱码,或JSON解析一直失败。

  • 排查1:波特率匹配。确保PC端serial.Serial和ESP32端Serial.beginSerialBT.begin使用的波特率一致。虽然蓝牙串口本身速率是自适应的,但虚拟串口层的波特率设置需要匹配,115200是通用选择。
  • 排查2:数据分隔符。我使用\n作为数据包结束标志。确保PC端发送的每个JSON字符串末尾都添加了\n。在ESP32端,使用readStringUntil('\n')来读取完整的一包数据。
  • 排查3:JSON缓冲区大小。如果数据包较大(例如包含更多传感器信息),需要增大DynamicJsonDocument doc(512)中的缓冲区大小。可以通过serializeJson(doc, Serial)打印doc的大小来估算。
  • 排查4:字符编码。双方都使用UTF-8编码。Python的encode('utf-8')和Arduino的默认字符串处理都是UTF-8,通常没问题。

5.3 屏幕显示异常

现象:白屏、花屏、颜色错乱、显示不全。

  • 排查1:引脚定义这是最常见的问题!反复检查User_Setup.h或你选择的配置文件中的引脚定义(TFT_CS,TFT_DC,TFT_RST,TFT_MOSI,TFT_SCLK)是否与你的实际接线一一对应。ESP32的引脚编号不是连续的,容易看错。
  • 排查2:屏幕初始化与旋转tft.init()必须在setup()中调用。tft.setRotation(0/1/2/3)可以调整屏幕方向,试试不同的值。
  • 排查3:电源问题。屏幕,尤其是背光,可能耗电较大。确保使用ESP32的VIN(5V)或稳定的3.3V引脚为屏幕供电,并且共地。如果屏幕有独立的背光控制引脚(TFT_BL),尝试在代码中将其设置为HIGH
  • 排查4:库冲突。确保只安装了TFT_eSPI库的一个版本,并且正确配置了用户设置文件。有时其他TFT库(如Adafruit的库)可能会引起冲突。

5.4 数据传输延迟或卡顿

现象:屏幕更新明显慢于1秒,或者数据跳跃。

  • 优化1:减少数据量。PC端只发送必要的数据,并适当降低发送频率(如改为2秒一次)。JSON键名可以缩短(如用"c”代替"cpu_percent"),但会降低可读性。
  • 优化2:优化ESP32端绘制。使用前面提到的Sprite进行局部刷新,避免全屏刷新。只更新变化的部分。
  • 优化3:检查蓝牙信号。确保ESP32和电脑蓝牙适配器之间没有严重的物理遮挡。USB 3.0接口有时会对2.4G信号(包括蓝牙)造成干扰,尝试将蓝牙适配器插在USB 2.0口或使用延长线。
  • 优化4:简化UI。如果使用了复杂的图形或历史图表,会消耗大量计算和内存。可以考虑简化显示内容,或者降低图表更新的频率。

6. 功能扩展与进阶玩法

基础功能实现后,这个项目还有很大的扩展空间。

1. 监控更多指标

  • GPU监控:PC端可以使用GPUtil(Python库)或调用NVIDIA SMI/AMD ADL来获取GPU占用率和温度,并加入到数据包中。
  • 进程监控:监控特定进程(如你的渲染软件、游戏)的CPU和内存占用。
  • 硬件传感器:在ESP32上连接DHT11温湿度传感器,同时显示桌面环境温湿度。

2. 显示模式与交互

  • 多页面切换:在ESP32上接一个按钮,单击切换不同的显示页面(如概览页、详细数据页、图表历史页)。
  • 屏幕旋转与亮度调节:通过加速度计实现自动旋转,或通过电位器调节屏幕亮度。

3. 无线升级(OTA)为ESP32配置Wi-Fi和OTA功能,这样以后更新程序就不需要再插线烧录了,直接通过网络上传新的固件。

4. 改用BLE(低功耗蓝牙)当前使用的是经典蓝牙(SPP),功耗相对较高。可以改用BLE(蓝牙低功耗),ESP32作为GATT服务器,PC作为客户端连接并订阅特征值通知。这样功耗会大大降低,甚至可以考虑用电池供电。不过BLE的通信模型比SPP复杂一些。

5. 集成到智能家居平台将ESP32监控屏的数据通过MQTT协议发布到Home Assistant或Node-RED等平台,这样你不仅能在桌面上看到,还能在手机App或家庭控制面板上查看PC状态,甚至设置自动化规则(如CPU温度过高时发送通知)。

我个人在完成基础版本后,增加了GPU监控和一个简单的折线图显示网络流量历史。最大的体会是,调试阶段一定要有耐心,特别是硬件接线和软件配置的对应关系,必须一丝不苟。当屏幕上第一次正确跳动出CPU百分比数字时,那种成就感是纯粹的。这个项目就像搭积木,基础框架搭好后,你可以根据自己的需求和创意,不断往上添加新的模块,乐趣无穷。