资讯详情

资讯详情

HAL库实战:DHT11温湿度传感器+OLED显示完整工程

简介基于STM32 HAL库的DHT11温湿度采集与OLED显示工程以STM32F0系列为硬件平台适合物联网方向学生与嵌入式入门开发者用于掌握环境监测节点的完整实现思路。工程围绕DHT11单总线通信和OLED显示两大核心覆盖HAL库的GPIO、I2C/SPI接口配置、传感器40位数据帧解析、显示驱动初始化等关键环节可直接从工程文件了解引脚分配与时钟配置。压缩包含193个文件以59个.h、29个.c源码为主另有Keil工程配置.uvprojx、CubeMX图形化配置.ioc、编译产物.hex/.axf及.o、.d、.map和.lst等中间文件整体约6.9MB便于对照源码检查构建过程。目前已有772人学习下载。参考该工程可以快速搭建温湿度实时显示环境掌握DHT11时序读取与校验思路理解OLED驱动的文本坐标设置与刷新逻辑并复用到后续物联网设备的驱动层开发中尤其适合入门阶段对照实物调试。1. 肉眼可见的数据才有意义HAL库DHT11温湿度OLED显示小工程做物联网节点会被DHT11“教育”一次才算真正碰过单总线时序。手里这套基于HAL库的温湿度传感器OLED显示的小项目是一个能直接抄的本地传感器面板MCU兼任“读数据的人”和“画屏幕的人”按微秒级时序把DHT11的40 bit数据咬出来再解析校验后刷到0.96寸I2C OLED上。它适合刚把图形化配置工具跑通、准备往物联网方向走的开发者也适合给产品原型做本地调试屏。项目不大却把HAL库的极限延时、单总线读时序、I2C驱动、OLED页面刷新这四件事串在了一起看完不仅能跑通还能把这套“读-解析-显示”框架换个传感器继续用。2. 硬件连接与工程框架单总线读数据、I2C出画面的基础2.1 硬件清单与接线四根线把数据链搭起来这个项目的物料非常少。一块带HAL库支持的Cortex-M内核开发板、一个DHT11模块、一块0.96寸I2C接口的OLED屏幕加上面包板和杜邦线就能开工。整个系统就两个传感器设备加一颗主控芯片没有多余的外围电路非常适合当第一个亲手调通的物联网本地节点。| 物料 | 数量 | 说明 | | 开发板 | 1 | 任意Cortex-M0/M3内核MCU开发板HAL库支持工作电压3.3V | | DHT11模块 | 1 | 三引脚VCC/GND/Data部分模块四引脚带一个空脚NC | | OLED 0.96寸 | 1 | SSD1306驱动I2C接口默认地址0x3C | | 上拉电阻 | 1 | 4.7k~10k接在Data与VCC之间若模块板载则免 | | 面包板/杜邦线 | 若干 | 调试期快速改线用 |接线关系也很固定DHT11的VCC接3.3VGND接GNDData接MCU的一个GPIO比如PA0OLED的SCL接I2C时钟线比如PB8SDA接I2C数据线比如PB9VCC和GND同样并到3.3V电源轨上。两块器件共用同一条3.3V电源轨注意别接到5VDHT11虽然标称3.3到5V都能跑但OLED模块多数只支持3.3V电压不一致很容易把屏幕烧掉这种翻车经历我已经听过不止一次了。关于Data线上的上拉电阻这是单总线协议的物理基础。总线空闲时靠上拉电阻保持高电平DHT11靠拉低电平来写0和应答如果不加上拉读回来的bit边沿会变得很钝轻则偶尔校验错重则整包数据全是0xFF。我一般建议用10k功耗低时序也够稳。模块如果板载了上拉电阻就不必再外接但你要确认板子上那颗电阻真的焊上了有些便宜的模块会省掉这颗料。2.2 技术栈与工程结构为什么HAL库要配手工微秒延时软件层面用的就是HAL库加一套自己写的驱动。HAL库最常用的延时是阻塞式的毫秒延时但DHT11的时序要求是微秒级启动信号拉低要维持18ms以上应答等待是20到40微秒每一位数据的高电平持续时间也只有几十微秒。毫秒级延时没法精确表达这些时间窗口所以驱动里必须自带一个微秒延时函数这是这个项目里最容易被忽视的技术选型。常见做法是用一个通用定时器做阻塞延时开启定时器后读计数器值达到目标微秒数就退出。也可以利用内核的DWT单元做周期计数不额外占用定时器。我一般用定时器方案因为图形化配置工具里勾选时钟源后就能生成初始化代码逻辑直观换芯片时改动也小。生成工程时记得勾选“每个外设独立生成.c/.h文件”的选项后面加传感器驱动时不用翻大文件找初始化代码。工程结构上我把HAL库工程分成两层。底层驱动文件是自建的放在用户代码目录里刻意不塞进配置工具管理的目录这样重新生成工程时不会被覆盖。project/ ├── Core/ │ ├── Inc/ │ │ ├── main.h │ │ ├── dht11.h │ │ └── oled.h │ └── Src/ │ ├── main.c │ ├── dht11.c │ └── oled.c ├── Drivers/ │ ├── CMSIS/ │ └── Vendor_HAL_Driver/ └── user/ └── delay.cCore里是配置工具生成的入口和中断文件Drivers里是HAL库源码user目录放自己写的微秒延时和通用小工具。dht11.c和oled.c放在Core/Src下的原因是编译链默认会搜这个目录少改一处工程配置。HAL库版本之间有些宏定义差异比如GPIO方向切换的接口在不同版本里长得不一样但整体写法一致照着现有API改就行。2.3 这套组合为什么适合做物联网节点的本地面板物联网节点最常见的问题就是云端收到一个异常数据但分不清是传感器坏了、协议解析错了还是网络传输丢了包。给节点配一块OLED本地屏等于给数据链加了一个旁路观测点传感器读出来什么屏幕立刻显示什么无线模块只负责转发屏幕上的同一份数据。这样排查问题时一眼就能看出故障出在哪一段。这个项目就是典型的“传感器显示”最小闭环。DHT11负责采集HAL库跑在MCU里负责解析OLED负责呈现。后续如果接Wi-Fi模组或4G模组只需把解析好的温湿度结构体序列化成JSON发出去本地显示逻辑完全不用动。这也是我推荐先把显示环节做扎实的原因——它让传感器数据从“黑匣子”变成了看得见的东西在物联网项目里可视化永远是第一块拼图。3. 核心代码拆解从DHT11的微秒时序到OLED的页面刷新3.1 单总线启动时序先把DHT11从睡眠里叫醒DHT11的读操作以一次明确的握手开始。主机先把数据线拉低至少18msDHT11检测到这个持续低电平后才会准备好数据随后主机拉高20到40微秒释放总线DHT11会主动把总线拉低约80微秒表示应答再拉高80微秒后开始输出40位数据。整个过程里主机在拉高后必须把GPIO从输出模式切换到输入模式否则读不到DHT11发来的电平。HAL库里切换GPIO方向并不像标准库那样直接改寄存器而是重新初始化GPIO。下面这段启动函数放在每一次读数据之前void DHT11_Start(void) { // 配置为开漏输出低电平有效 GPIO_InitTypeDef gpio_out {0}; gpio_out.Pin DHT11_Pin; gpio_out.Mode GPIO_MODE_OUTPUT_OD; // 开漏输出 gpio_out.Pull GPIO_PULLUP; // 外部上拉为主内部上拉作兜底 gpio_out.Speed GPIO_SPEED_FREQ_HIGH; HAL_GPIO_Init(DHT11_GPIO_Port, gpio_out); HAL_GPIO_WritePin(DHT11_GPIO_Port, DHT11_Pin, GPIO_PIN_RESET); Delay_us(20000); // 拉低20ms超过18ms的最低要求 HAL_GPIO_WritePin(DHT11_GPIO_Port, DHT11_Pin, GPIO_PIN_SET); Delay_us(30); // 拉高30us后释放总线 // 切回输入模式准备读DHT11的应答信号 GPIO_InitTypeDef gpio_in {0}; gpio_in.Pin DHT11_Pin; gpio_in.Mode GPIO_MODE_INPUT; gpio_in.Pull GPIO_PULLUP; HAL_GPIO_Init(DHT11_GPIO_Port, gpio_in); }这里有几个参数值得说清楚。拉低延时20ms而不是刚好18ms是给DHT11的启动检测留余量拉高延时30us落在20到40us窗口内。开漏输出模式配合外部上拉电阻实现“线与”是多设备共享总线的标准接法在单总线上既当发送方又当接收方时必须用这个模式。切换输入模式后立刻开始读应答不要浪费时间在日志输出上否则会错过DHT11拉低的那80us窗口。3.2 逐位读取用高电平持续时间区分0和1DHT11输出的每一位都以50us低电平开始然后是高电平。逻辑0的高电平持续26到28us逻辑1的高电平持续约70us。所以读取策略很简单跳过低电平测量高电平的时间长度超过阈值就判定为1否则为0。这套判定方式本质上是在数电平的宽度而不是捕捉边沿对时序抖动有比较好的容忍度。uint8_t DHT11_ReadBit(void) { uint32_t timeout 0; // 等待低电平结束每一位数据都以50us低电平打头 while (HAL_GPIO_ReadPin(DHT11_GPIO_Port, DHT11_Pin) GPIO_PIN_RESET) { if (timeout 300) return 0xFF; // 超时保护避免死等 Delay_us(1); } // 测量高电平持续时间 uint32_t high_time 0; while (HAL_GPIO_ReadPin(DHT11_GPIO_Port, DHT11_Pin) GPIO_PIN_SET) { high_time; Delay_us(1); if (high_time 100) break; // 超过100us说明时序异常 } return (high_time 50) ? 1 : 0; }阈值的选取是这段代码的关键。0的高电平约28us1的高电平约70us取中间值50us能把两类信号干净地分开。延时1us的函数本身有误差但引脚电平的边沿抖动和延时误差叠加后仍远小于50us的判定窗口所以实际跑起来很稳。两个while循环都加了超时保护一旦DHT11没接好或总线被卡死函数返回0xFF而不是让整个系统死循环这对嵌入式代码来说是最低限度的自我保护。3.3 数据解析与校验40位数据拼出温度和湿度一次完整读取得到40位数据按顺序排列湿度整数8位、湿度小数8位、温度整数8位、温度小数8位、校验字节8位。校验字节等于前四个字节相加后取低8位。这个校验能挡住大部分时序错乱产生的坏数据是DHT11驱动里最后一道防线。typedef struct { uint8_t humi_int; uint8_t humi_dec; uint8_t temp_int; uint8_t temp_dec; } DHT11_Data; uint8_t DHT11_ReadData(DHT11_Data *data) { uint8_t buf[5] {0}; DHT11_Start(); // 应答信号DHT11拉低80us后拉高80us跳过这段 uint32_t wait 0; while (HAL_GPIO_ReadPin(DHT11_GPIO_Port, DHT11_Pin) GPIO_PIN_RESET) { if (wait 300) return 1; // 无应答检查接线 Delay_us(1); } while (HAL_GPIO_ReadPin(DHT11_GPIO_Port, DHT11_Pin) GPIO_PIN_SET) { if (wait 300) return 1; Delay_us(1); } for (int i 0; i 40; i) { uint8_t bit DHT11_ReadBit(); if (bit 0xFF) return 3; // 读位超时放弃本次数据 buf[i / 8] 1; buf[i / 8] | (bit 0x01); } if ((uint8_t)(buf[0] buf[1] buf[2] buf[3]) ! buf[4]) return 2; // 校验失败数据不可信 >void OLED_WritePage(uint8_t page, uint8_t *buf, uint8_t len) { uint8_t cmd[4]; cmd[0] 0x00; // 控制字节接下来是命令 cmd[1] 0xB0 page; // 页地址0~7 cmd[2] 0x00; // 列地址低4位 cmd[3] 0x10; // 列地址高4位 HAL_I2C_Master_Transmit(hi2c1, OLED_ADDR, cmd, 4, 100); // 写数据控制字节0x40后面是显示内容 uint8_t data[129]; data[0] 0x40; memcpy(data[1], buf, len); HAL_I2C_Master_Transmit(hi2c1, OLED_ADDR, data, len 1, 100); }为什么要先发4字节命令再发数据因为在页地址模式下SSD1306的列地址不会自动回到0必须每次先指定页地址和列起始地址否则连续刷新时内容会依次错位。参数page范围是0到7对应屏幕从上到下的8页len最大128字节正好是每页的宽度。函数内部临时开了129字节的栈缓冲在内存紧张的MCU上也能接受但如果要刷大面积画面建议把缓冲提到函数外复用。3.5 刷新策略与主循环读-解析-显示的调度节奏显示驱动的另一个坑是刷新闪烁。0.96寸OLED的GRAM只有128x64字节但I2C在400kHz下传一屏数据也要约20ms。如果每秒刷新一次数字每秒跳变时屏幕会隐约闪动这在室内仪表上很刺眼。所以我在刷新前加了变化检测温湿度没变化就不重新写GRAM。void OLED_ShowDHT11(DHT11_Data *d) { static uint8_t last_temp 0xFF, last_humi 0xFF; // 数据没变化就不刷新避免整屏闪烁 if (d-temp_int last_temp d-humi_int last_humi) return; last_temp d-temp_int; last_humi d-humi_int; char line[20]; snprintf(line, sizeof(line), Temp:%d.%dC, d-temp_int, d-temp_dec); OLED_ShowString(0, 0, line); snprintf(line, sizeof(line), Humi:%d.%d%%, d-humi_int, d-humi_dec); OLED_ShowString(2, 0, line); }static局部变量在函数退出后仍保留数值适合做这种轻量级变化检测。这里只比较整数部分因为DHT11的温度分辨率是1度、湿度分辨率是1%小数位基本恒为0只比较整数位可以减少无意义的屏幕刷新。OLED_ShowString的第一个参数是页码0和2分别对应屏幕的上两行这样只重写两页而不动整个GRAM。主循环的调度节奏是整条数据链的节拍器while (1) { DHT11_Data d; if (DHT11_ReadData(d) 0) { OLED_ShowDHT11(d); printf(temp%d.%d humi%d.%d\r\n, d.temp_int, d.temp_dec, d.humi_int, d.humi_dec); } HAL_Delay(1000); // DHT11采样周期最短约1秒读太频繁反而失败 }读失败时不显示也不打印直接等下一个周期。这样做的好处是OLED上永远保留最后一次成功的数据不会因为一次偶发校验失败而清屏或显示乱码。HAL_Delay(1000)保证DHT11有充足的采样间隔同时让串口日志保持每秒一行的可读节奏。4. 编译烧录与运行验证从配置工具引脚到串口日志4.1 引脚规划与外设初始化先分配好硬件资源在配置工具里把工程初始化搭好比手写寄存器快得多。需要配置的外设有四个一个GPIO用于DHT11的数据线一组I2C用于OLED一个通用定时器用于微秒延时一个USART用于串口日志。下表是我常用的分配方案引脚编号只是示例换别的引脚要同步改代码里的宏定义。| 外设 | 引脚 | 关键配置 | | GPIOA Pin0 | PA0 | 开漏输出、上拉、高速DHT11 Data | | I2C1 | PB8/PB9 | 标准模式或快速模式目标400kHz | | TIM1 | 内部时钟 | 预分频72-1计数器向上计数 | | USART1 | PA9/PA10 | 115200, 8N1 |GPIO初始化为开漏输出的原因在上一章说过单总线需要线与能力。I2C的速率建议直接上400kHzOLED的SSD1306完全支持快速模式用的还是阻塞式传输速度越快留给主循环的时间越多。定时器预分频按主频计算如果MCU主频是72MHz预分频72-1后计数器每加1正好是1微秒延时函数里读到的计数器值就是已经过去的微秒数。4.2 微秒延时与printf重定向让日志开口说话配置好定时器后微秒延时的实现就是把计数器清零然后阻塞等待计数到达目标值。这个函数简单直接但要注意它会让CPU空转所以只用于传感器时序这类短延时场景。void Delay_us(uint32_t us) { __HAL_TIM_SET_COUNTER(htim1, 0); HAL_TIM_Base_Start(htim1); while (__HAL_TIM_GET_COUNTER(htim1) us); HAL_TIM_Base_Stop(htim1); }HAL_TIM_Base_Start和Stop放在延时的首尾是因为这个定时器专用于延时不承载其他功能每次调用都启动和停止不会有计数溢出干扰。如果us超过65535计数器会回绕所以调用方要保证传参不超过这个范围DHT11时序里最大的延时是20ms远低于上限。printf重定向在HAL库里有现成套路重写fputc函数把字符通过串口发出去。这样整个工程里的printf都能往串口打调试传感器时序时特别方便。int fputc(int ch, FILE *f) { HAL_UART_Transmit(huart1, (uint8_t *)ch, 1, 100); return ch; }重定向之后串口助手里看到的每一行都对应一次完整的读-解析-显示调度。如果printf输出乱码先查波特率是不是115200再查串口助手的编码格式这两个问题出现的频率远高于配置错误。4.3 烧录与运行验证从串口数据到屏幕显示的完整流程焊好线、编译、下载、复位之后按下面几步走每一步都有明确的预期结果打开串口助手设115200波特率观察输出。正常情况下每秒钟应该有一行temp和humi数据数值稳定不跳动。用手捏住DHT11外壳体温会让温度在几秒内上升1到2度湿度也会因手汗明显变化。用嘴对DHT11哈气湿度应该快速上升数据里能够直接看到变化。观察OLED是否跟随串口同步变化OLED显示与串口一致说明读-解析-显示链路完整。如果串口有输出但OLED不亮先检查I2C地址是0x3C还是0x3D如果OLED亮但数据不动大概率是DHT11的GPIO方向切换或上拉电阻没接对。这两类问题占了新手调试时间的八成。4.4 串口日志如何帮我们定位故障运行稳定的日志长这样temp25.0 humi45.0 temp25.0 humi45.0 temp25.3 humi46.0每行间隔1秒数值在小范围内波动。如果出现下面的情况对应关系很明确日志完全没有输出的查串口重定向和USART引脚是否被占输出全是“temp0.0 humi0.0”的查DHT11应答和GPIO方向切换间歇性缺行的说明上次读取校验失败查上拉电阻和读取间隔。这些规律可以直接写进自己的调试清单里下次遇到类似问题不用重新猜。5. 避坑指南单总线时序、上拉电阻与OLED刷新的那些坑5.1 读出来全是0或255先量上拉别急着改代码现象串口稳定输出temp0.0 humi0.0OLED跟随显示0.0接线检查了好几遍还是没变化。原因DHT11的数据线没有上拉电阻。开发板内部上拉默认可能是关闭或弱上拉DHT11驱动时靠外部上拉维持空闲高电平。没有上拉读回来的电平被引脚寄生电容拉着走时序全乱。解决在Data和VCC之间接一个4.7k到10k的电阻。如果是洞洞板模块先确认模块背面有没有贴片上拉有的模块省了这颗电阻必须自己补。接上之后复位再读数值立刻正常。从那以后我拿到任何单总线传感器第一步永远是看原理图确认上拉而不是先写代码。5.2 第二次读取就开始失败20ms启动时序被毫秒延时吞掉了现象运行时第一个数据100%正确之后校验错误率飙升甚至完全读不到应答重启又正常几秒钟。原因把HAL_Delay当成万能延时用在启动信号里。HAL_Delay的最小粒度是1ms而DHT11要求拉高20到40us后立刻读应答毫秒级延时实际delay出来的时间可能超过1msDHT11早已输出完应答信号。解决所有微秒级时序一律走定时器延时函数毫秒级的调度才用HAL_Delay。在主循环的1000ms等待里用HAL_Delay没问题但在DHT11_Start和逐位读取里必须用Delay_us。给代码加注释提醒后来者不要“优化”成HAL_Delay这个坑我已经踩过两回每次都是同样的表现。5.3 OLED花屏或显示一半I2C地址和写命令时序背锅现象屏幕能亮但数字和单位花掉或者只显示上半屏下半屏是噪点动态刷新时错位更明显。原因第一SSD1306模块的I2C地址有两种0x3C和0x3D由模块背面的电阻选择地址不对会连设备都扫描不到。第二写命令时没有区分命令和控制字节SSD1306把数据误当成命令执行GRAM内容全乱。解决先用I2C扫描程序确认设备地址再核对OLED_WritePage里的控制字节。命令控制字节是0x00数据是0x40。另外每次写GRAM前要重新设置页地址和列地址不能靠上一次的指针位置否则连续刷新会出现内容错位。5.4 屏幕像水波纹一样闪整屏刷新的代价现象OLED每秒刷新一次数字变化时整个屏有肉眼可见的闪烁和拖影看久了眼睛难受。原因每次刷新都顺序写完8页、128列I2C传一整屏数据需要约20毫秒。在这20毫秒里屏幕不同区域被先后改写人眼看到的就是从上到下的滚动刷新传感器数据本身没问题纯粹是显示节奏没控制好。解决引入变化检测数据没变就不刷需要更新时只刷变化的页面。OLED的显示内容固定在屏幕上方两行时只写第0页和第2页。如果一定要全屏刷新先关显示、更新GRAM、再开显示能明显减轻闪烁。这个局部刷新策略尤其适合仪表类界面数据变化越快收益越大。5.5 数值突然跳变又恢复读取间隔低于DHT11的采样周期现象日志里偶尔出现一次温度从25跳到80再跳回来间隔几十秒一次没有规律校验和也没有报错。原因DHT11内部采样周期约1到2秒如果主循环读得太快会读到传感器上一次数据的残留电平或不完整数据。校验和虽然挡掉了大部分坏包但个别情况下坏包正好通过了校验和坏数据就漏到了显示层。解决严格控制读间隔每秒最多读一次。校验失败不要立即重试直接放弃本次结果等1秒后的下一个周期。数值跳变时在显示层加一阶滤波新值与前值偏差超过5度就连续读两次确认。这套处置之后日志里再没出现过跳变关键是让调度节奏尊重传感器本身的物理特性。6. 二次开发思路把温湿度计改装成物联网上报节点6.1 先给MCU加一个“会说话的嘴”改造的第一步是把解析好的数据搬出MCU。常见做法是在串口上接一个Wi-Fi模块或4G模组把DHT11读到的温湿度组成JSON字符串发到云端。OLED继续留在本地做调试期的观测窗。数据流变成“DHT11→MCU→OLED和通信模组”显示和上报共用同一份解析结果。void SensorPublishTask(void) { DHT11_Data d; if (DHT11_ReadData(d) 0) { char json[64]; snprintf(json, sizeof(json), {\temp\:%d.%d,\humi\:%d.%d}, d.temp_int, d.temp_dec, d.humi_int, d.humi_dec); MQTT_Publish(sensor/device01, json); // 本地显示与上报使用同一份数据云端错了本地能对账 OLED_ShowDHT11(d); } }这个函数把读、显、传三件事放在一起数据只在一个地方解析杜绝了两套代码各自读一遍导致数值不一致的问题。MQTT_Publish是一个很薄的封装底层对接哪个模组取决于你手上的硬件但上层接口不变后续换通信方式不用动业务代码。6.2 低功耗改造读一次睡一次如果节点用电池供电HAL_Delay阻塞等待的1000ms就成了浪费。改法是把调度权交还给RTC定时器唤醒后读一次数据、刷一次屏、发一次包然后进Stop模式。DHT11上电初始化约1秒后才能读所以唤醒后先等DHT11稳定再启动读取时序。void RTC_WakeupCallback(void) { HAL_GPIO_TogglePin(LED_Pin); DHT11_Data d; if (DHT11_ReadData(d) 0) { OLED_ShowDHT11(d); MQTT_Publish(sensor/device01, data_to_json(d)); } // 回到Stop模式等下一个RTC周期 }低功耗改造时有个容易被忽略的点OLED的SSD1306即使不显示也在耗电进去Stop模式前要把I2C总线释放、给OLED发一条关显示命令能省下几毫安电流。我在一次改造中实测关显示后整机待机电流降了一半这个收益比优化DHT11时序大得多。6.3 换DHT22的适配思路这套框架换DHT22时单片机部分几乎不用改要改的是时序阈值和数据结构。DHT22同样是单总线但0和1的高电平持续时间更短启动拉低至少1ms即可读取间隔可以缩短到2秒。把DHT11_ReadBit里的50us阈值调整为DHT22对应的阈值再把数据解析从整数小数拆成16位带符号温度即可。这也是当初我把驱动单独拆成.c/.h文件的原因——传感器换了HAL库的I2C和GPIO那部分完全不动只动数据处理这一层。做到这里一个能显示、能上报、能低功耗运行的温湿度节点就成型了。我后来做每个传感器节点都强制先搭本地屏加串口日志再上无线不然云端数据错了根本分不清是传感器坏了还是协议写错了。希望帮到你。本文还有配套的精品资源点击获取
觉得有用,分享给同行:

为您的企业打造数字门面

稳重轻奢商务风格,端正雅致视觉,长效耐看不易过时。

立即咨询 →