资讯动态

STM32F103+DHT11+SGP30室内空气质量监测终端设计全解析

发布时间:2026/10/13 20:24:57 来源:尧图企业网站定制
简介这是一份基于STM32F103的嵌入式课程设计资源面向物联网、嵌入式方向的学生与开发者解决室内空气质量的实时监测与超标提醒问题。项目以DHT11采集温湿度、SGP30传感模块检测CO2和甲烷等有害气体浓度配合蜂鸣器实现阈值报警并通过OLED同步显示数据可作为环境监测类课设、毕设或DIY项目的完整参考方案。压缩包共205个文件整体大小7.32MB以C源代码、H头文件、Keil工程编译生成的中间文件为主同时包含HEX固件、uvprojx工程配置、调试信息文件以及清理脚本等覆盖从编写、编译到烧录验证的完整开发链路。目前已有396人学习下载。通过分析stm32f10x系列标准外设库的调用方式与各模块接线逻辑可快速掌握I2C、单总线通信和多传感器协同工作的工程实现代码结构清晰注释完整便于直接移植或二次开发也适合作为课程设计答辩的参考材料。1. 一块STM32F103把温湿度、TVOC、报警和显示串起来值得做吗把STM32F103作为主控接上DHT11温湿度传感器、SGP30气体传感器、蜂鸣器模块和一块OLED显示屏这几乎是小规模室内空气质量监测终端的最低成本组合。它解决的不是一个单点难题而是让你在一个工程里同时面对单总线协议、I2C时序、GPIO控制和显示刷新四类常规任务DHT11负责温湿度SGP30负责eCO2和TVOCOLED负责展示蜂鸣器负责超限报警。对准备课程设计、电子竞赛或物联网入门项目的人来说这是性价比很高的“动手练级”路线对已经会点灯的熟手它的价值在于把传感器驱动、阈值策略和异常处理串成一套完整闭环。我见过不少人单独调每个模块都能通一旦四个模块合在一起就跑不动问题往往不在某个传感器而在共总线、共电源和时序混杂。下文从硬件选型到驱动代码再到最值得记住的踩坑点按一条能走通的路径展开。2. 硬件选型和接线STM32F103的资源和引脚分配怎么定2.1 主控与模块选型为什么是STM32F103而不是更强的芯片STM32F103是Cortex-M3内核主频72MHz常见型号有64KB到512KB Flash、20KB到64KB SRAM。这个项目实际消耗很小DHT11用普通GPIO位操作SGP30和OLED共用一条I2C总线蜂鸣器要么用GPIO要么用一路定时器PWM再加上串口输出日志整体资源占用不到F103中低配型号的三分之一。选它的理由不是性能而是生态成熟、参考代码多、引脚兼容性强出了问题最容易找到原因。模块选型上要注意几个细节。DHT11建议买带PCB小板的那种板上通常已经集成4.7kΩ上拉电阻和滤波电容裸传感器还要自己补上拉反而麻烦。SGP30本体是贴片封装市面上能买到的大多是转接板板上带电平转换和稳压接口直接引出VCC、GND、SDA、SCL四根线可以3.3V直连。OLED选0.96寸、128x64分辨率、SSD1306控制器的I2C版本不要选SPI版本因为SGP30也是I2C两个设备共用一条总线能省两根IO接线也简单。蜂鸣器模块要先确认有源还是无源这个直接决定驱动方式后面第4章细说。这套组合的“等”字一般还意味着预留扩展位。常见做法是把PA9、PA10留下来接串口做日志输出再用剩下的GPIO挂一个继电器模块或者烟雾传感器模块但主线仍然是DHT11、SGP30、蜂鸣器、OLED四件套。2.2 引脚分配和接线表单总线、两路I2C与报警IO的划分引脚分配的原则是需要外设复用的功能优先放到默认复用引脚上不需要复用的功能放普通IO。项目里只有I2C需要复用SGP30和OLED都挂在I2C1上所以SCL和SDA固定用PB6、PB7。DHT11是单总线协议完全靠GPIO模拟放在PA0这个引脚既是普通IO也是ADC输入不冲突。蜂鸣器如果是无源蜂鸣器需要PWM输出放在PA1正好能复用TIM3_CH2如果是有源蜂鸣器PA1当普通IO用也完全没问题。模块模块引脚STM32F103引脚说明DHT11VCC3.3V模块板载上拉电阻时可直接3.3VDHT11DATAPA0单总线外部4.7kΩ上拉到3.3VDHT11GNDGND共地SGP30VCC3.3V必须3.3V严禁5VSGP30SDAPB7I2C1数据线SGP30SCLPB6I2C1时钟线OLEDVCC3.3VSSD1306供电接5V会烧OLEDSDAPB7与SGP30共I2C1OLEDSCLPB6与SGP30共I2C1蜂鸣器I/OPA1有源直接GPIO无源接PWM蜂鸣器VCC3.3V或5V取决于模块驱动电路看丝印I2C总线上两个设备并联外部上拉电阻一般接两个4.7kΩ到3.3V即可STM32F103内部虽然有弱上拉但驱动能力不足以保证400kHz下的信号边沿外部上拉不能省。2.3 供电与电平匹配3.3V逻辑下模块能不能直连STM32F103的IO逻辑电平是3.3V所有模块必须在这个前提下对接。DHT11模块在3.3V供电时能工作但相比5V供电时序边沿会稍微变缓这在软件延时上留一点余量就能覆盖。SGP30本身就是3.3V器件不存在电平问题。OLED模块如果板上有稳压和电平转换3.3V供电最稳如果买的是那种纯裸屏加转接板的组合务必确认转接板支持3.3V。有一个供电坑比电平匹配更容易翻车SGP30内部加热元件工作电流不小OLED背光亮起时电流也不低两者叠加可能到30-60mA。如果最小系统板上的3.3V稳压器输出能力不足比如某些小板用的是250mA级别的LDOSGP30测量值会漂OLED亮度会闪。常见做法是直接用USB的5V输入让板载稳压器承担全部供电如果要求更低纹波就外接单独的3.3V稳压模块给传感器和OLED供电STM32F103的供电独立。蜂鸣器模块要单独说。带三极管驱动的有源蜂鸣器模块有的设计成低电平触发有的是高电平触发接之前看丝印或说明书触发极性搞反的后果就是不上电也响、上电反而不响。若模块标注支持5V供电且板载驱动电路带电平转换可以直接用5V供电但控制引脚信号地和电源地必须共地否则IO控制不了。3. 传感器驱动代码DHT11单总线时序与SGP30的I2C读取参数3.1 DHT11驱动起始信号、40位数据与CRC校验DHT11是单总线协议总线空闲时是高电平。主机发起一次读取要先把总线拉低至少18ms作为起始信号然后释放总线DHT11检测到这个下降沿后会拉低约80us应答再拉高约80us之后开始逐位发送40位数据。每一位数据的开始都是50us左右的低电平区别在高电平持续时间高电平持续26-28us表示逻辑0持续约70us表示逻辑1。写驱动前先把引脚方向搞清楚。开始阶段PA0是推挽输出拉低唤醒后要立刻切回输入靠外部上拉电阻把总线恢复成高电平否则总线一直被你自己的IO拉低DHT11根本没有应答机会。这个切换顺序是被读不到数据问题追着打的头号原因。// PA0 推挽输出用于发送起始信号 static void dht11_io_output(void) { GPIOA-CRL ~(0xF 0); // 清空PA0模式配置 GPIOA-CRL | (0x3 0); // 推挽输出50MHz } // PA0 浮空输入靠外部4.7k上拉拉高 static void dht11_io_input(void) { GPIOA-CRL ~(0xF 0); GPIOA-CRL | (0x4 0); // 浮空输入模式 }这段配置操作的是GPIOA的CRL寄存器PA0恰好是CRL的低4位0x3对应推挽输出、0x5对应开漏输出、0x4对应浮空输入。浮空输入模式下总线的电平完全由外部上拉电阻决定这是DHT11单总线正确工作的前提。// 读取DHT11成功返回0超时返回1CRC错误返回2 uint8_t dht11_read(uint8_t *hum, uint8_t *temp) { uint8_t data[5] {0}; uint32_t timeout; GPIOA-BSRR 0x0001; // PA0置高 dht11_io_output(); GPIOA-BRR 0x0001; // PA0拉低发起起始信号 delay_ms(20); // 拉低至少18ms GPIOA-BSRR 0x0001; // 释放总线 dht11_io_input(); // 切到输入等DHT11应答 timeout 5000; while (GPIOA-IDR 0x0001) // 等总线被DHT11拉低 if (--timeout 0) return 1; timeout 5000; while (!(GPIOA-IDR 0x0001)) // 等DHT11拉高 if (--timeout 0) return 1; timeout 5000; while (GPIOA-IDR 0x0001) // 等80us应答低电平结束 if (--timeout 0) return 1; for (int i 0; i 40; i) { timeout 5000; while (!(GPIOA-IDR 0x0001)) // 每一位开始是50us低电平 if (--timeout 0) return 1; delay_us(40); // 高电平开始后40us采样 data[i / 8] 1; if (GPIOA-IDR 0x0001) // 此时为高说明是1为低说明是0 data[i / 8] | 1; timeout 5000; while (GPIOA-IDR 0x0001) // 等当前位结束 if (--timeout 0) return 1; } if (data[4] ! (uint8_t)(data[0] data[1] data[2] data[3])) return 2; // 校验失败数据不可信 *hum data[0]; // data[0]湿度整数, data[1]湿度小数 *temp data[2]; // data[2]温度整数, data[3]温度小数 return 0; }这段代码的关键是采样点选在40us逻辑0的高电平只有26-28us40us时总线已经变低逻辑1的高电平约70us40us时总线还是高。这个时间窗口是读对每一位的根基。延时函数建议用SysTick做微秒级延时不要用空循环72MHz的MCU上开优化后空循环可能被优化掉读出来的数据必然跳变。DHT11是8位分辨率的廉价传感器小数部分恒为0不要指望它有很高的精度它的价值在于“够用、便宜、协议简单”。3.2 SGP30驱动初始化、eCO2/TVOC读取与等待时间SGP30是I2C接口7位地址是0x58。上电后需要发一条初始化命令来完成加热器配置然后每1秒读一次测量结果。核心命令有三条0x2003是初始化0x2008是测量空气质量0x2015和0x201E分别是读取和写入基线。测量命令发出后要等12ms左右才能读数据读回来的9个字节里包含三组“16位数据CRC8校验”依次是eCO2、TVOC和保留字段。#define SGP30_ADDR 0x58 #define SGP30_CMD_INIT 0x2003 #define SGP30_CMD_MEASURE 0x2008 // 读取一次空气质量成功返回0 uint8_t sgp30_read_air_quality(uint16_t *eco2, uint16_t *tvoc) { uint8_t cmd[2] {0x20, 0x08}; uint8_t buf[6]; if (i2c_write(SGP30_ADDR, cmd, 2) ! 0) return 1; // 发送测量命令失败 delay_ms(12); // 等待测量完成 if (i2c_read(SGP30_ADDR, buf, 6) ! 0) return 2; // 读取数据失败 if (!sgp30_crc8(buf, 2, buf[2])) return 3; // eCO2的CRC校验失败 if (!sgp30_crc8(buf 3, 2, buf[5])) return 4; // TVOC的CRC校验失败 *eco2 (buf[0] 8) | buf[1]; *tvoc (buf[3] 8) | buf[4]; return 0; }注意这里读的是6个字节只取eCO2和TVOC两组。每条I2C读操作都要配超时这个习惯后面第5章会解释原因。这个函数里12ms的等待时间不能省读早了要么拿不到数据要么拿到的是上一次的残留值。// SGP30使用的CRC-8多项式0x31初值0xFF uint8_t sgp30_crc8(const uint8_t *data, uint8_t len, uint8_t expected) { uint8_t crc 0xFF; for (uint8_t i 0; i len; i) { crc ^ data[i]; for (uint8_t b 0; b 8; b) { if (crc 0x80) crc (crc 1) ^ 0x31; else crc 1; } } return crc expected; }CRC实现是SGP30数据手册里定义的算法多项式0x31、初值0xFF。校验失败时说明I2C线上有干扰或者供电不稳这种情况下直接丢弃本次数据比强算出一个错误数值有意义得多。SGP30的eCO2输出范围是400-60000ppmTVOC是0-60000ppb刚上电时eCO2默认停在400ppm附近“400ppm”是空气的基准CO2浓度如果看到这个值不要惊讶。SGP30和DHT11最大的区别在响应特性。SGP30是一个相对值传感器依赖内部基线来判断当前气体浓度它的读数反映的是“相对洁净空气的偏差”不是绝对精确的CO2浓度。测量周期固定1秒连续调用间隔小于1秒没有意义电源反复通断会让基线丢失重新建立基线需要连续运行很久。3.3 采样周期与数字滤波让读数不再上下乱跳两个传感器的刷新节奏不同。DHT11从数据手册到实际使用都要求两次读取间隔至少1秒我一般放到2秒一次SGP30固定1秒一次。OLED显示1秒刷新一轮就够了人眼不需要更快。把这几个节拍放进主循环的任务调度比用阻塞式延时一个个串行调用要稳得多。数字滤波我一般分层做第一层是突变抑制SGP30单次读数与上次相比突变超过30%时先不采信因为它的物理响应本来就慢突变大多来自干扰或传感器靠近了污染源第二层是滑动平均对温度和湿度这种噪声明显的量用中值滤波对eCO2用5点滑动平均。#define FILTER_LEN 5 static uint16_t eco2_buf[FILTER_LEN]; static uint8_t eco2_idx 0; static uint32_t eco2_sum 0; void filter_init(uint16_t value) { for (uint8_t i 0; i FILTER_LEN; i) { eco2_buf[i] value; // 用首个有效值填满缓冲区 eco2_sum value; } } void filter_add(uint16_t value) { eco2_sum - eco2_buf[eco2_idx]; eco2_buf[eco2_idx] value; eco2_sum value; eco2_idx (eco2_idx 1) % FILTER_LEN; } uint16_t filter_avg(void) { return (uint16_t)(eco2_sum / FILTER_LEN); }注意filter_init这一步很多人忽略。如果用全零缓冲区直接开始累加前5次平均值会被拉得很低刚上电时你会看到eCO2从80ppm往上升然后以为是设备坏了。用第一次读数填满缓冲区输出从一开始就是平滑可信的。温度突变对SGP30读数有影响但规格书上并没有给出官方的温度补偿公式所以我一般不在软件里强行做补偿而是加一条经验规则温度在短时间内变化超过5°C时暂停eCO2报警判断半小时等系统热平衡后再恢复。这比任何一个拟合公式都可靠也避免空调刚开时蜂鸣器乱响。4. OLED显示与蜂鸣器报警把数据变成可读、可听的反馈4.1 OLED关键初始化命令与页面布局OLED用的SSD1306控制器128x64分辨率内置1KB显存。I2C模式下常见地址是0x3C也有少量模块是0x3D。初始化命令序列很长但真正影响成败的只有几条下面这张表列出最关键的其余照抄数据手册即可。命令参数作用0xAE / 0xAF无关显示 / 开显示0x8D0x14打开内部电荷泵不开屏幕不亮0x200x00水平寻址模式适合整帧刷新0xD50x80显示时钟分频影响刷新率0xA80x3F复用比1/640xD30x00显示偏移00x810xCF对比度值越大越亮0xDA0x12COM引脚配置驱动方式电荷泵命令最容易漏。有些精简过的初始化代码为了省几条命令把0x8D 0x14去掉结果屏幕亮度极低或者完全不亮很多人以为是OLED坏了其实是电荷泵没开。页面布局按“行”来规划。SSD1306的显存按页组织64像素高度被分成8页每页8像素高。用6x8点阵字库时一行字正好占一页128x64的屏幕可以放4行大字加一条状态栏。我的排版习惯是第一行显示温度和湿度第二行eCO2第三行TVOC第四行显示当前状态是NORMAL还是ALARM。typedef struct { int16_t temperature; // 单位0.1°C250表示25.0°C uint8_t humidity; // 单位% uint16_t eco2; // ppm uint16_t tvoc; // ppb uint8_t alarm; // 0正常 1报警 } air_quality_t; void oled_render_screen(const air_quality_t *aq) { char line[24]; oled_clear(); snprintf(line, sizeof(line), T:%d.%dC H:%d%%, aq-temperature / 10, aq-temperature % 10, aq-humidity); oled_draw_text(0, 0, line); snprintf(line, sizeof(line), eCO2:%dppm, aq-eco2); oled_draw_text(0, 2, line); snprintf(line, sizeof(line), TVOC:%dppb, aq-tvoc); oled_draw_text(0, 4, line); oled_draw_text(0, 6, aq-alarm ? !! ALARM !! : NORMAL ); oled_refresh(); }这里oled_draw_text的第二个参数是页号0、2、4、6四行正好隔开。如果MCU内存紧张可以把snprintf换成手写的整数转字符串函数避免引入格式化的栈开销。刷新策略上我一般只在数据变化时刷新对应行状态栏变化时才整屏重推I2C总线上少传数据留给传感器读取更多带宽。4.2 蜂鸣器报警阈值与回差避免边界反复响蜂鸣器先分清楚有源和无源。有源蜂鸣器内部自带振荡源IO口给高电平就响频率固定不可调驱动最简单无源蜂鸣器需要外部给方波才发声常见频率2.7kHz附近必须用定时器的PWM输出用GPIO直接输出电平只会听到很小的“哒哒”声因为直流信号驱动不了压电片或电磁线圈。我一般建议新手先用有源蜂鸣器把整套流程跑通再换无源蜂鸣器实现多级报警音。报警逻辑不能只盯单次读数否则会变成“报警-解除-报警”的循环闪烁需要加两个机制回差和持续时间判定。#define ECO2_ALARM_HIGH 1000 // eCO2超过1000ppm开始报警 #define ECO2_ALARM_LOW 850 // eCO2回落到850ppm以下才解除 #define HUM_ALARM_HIGH 70 // 湿度超过70%报警 #define TEMP_ALARM_HIGH 35 // 温度超过35°C报警 static uint8_t alarm_state 0; void alarm_update(const air_quality_t *aq) { uint8_t now_alarm 0; if (aq-eco2 ECO2_ALARM_HIGH) now_alarm 1; if (aq-humidity HUM_ALARM_HIGH) now_alarm 1; if (aq-temperature TEMP_ALARM_HIGH) now_alarm 1; if (alarm_state 0 now_alarm) { alarm_state 1; // 满足条件进入报警 } if (alarm_state 1 aq-eco2 ECO2_ALARM_LOW aq-humidity HUM_ALARM_HIGH aq-temperature TEMP_ALARM_HIGH) { alarm_state 0; // 必须跌破回差值才解除 } buzzer_ctrl(alarm_state); }1000ppm的eCO2阈值对应人多的办公室或密闭卧室的典型水平850ppm的回差值保证读数在995和1005之间波动时不会触发蜂鸣器反复通断。如果还想再稳一点可以把“连续3次检测超过阈值才报警”这个条件加进去代价是报警响应慢2秒但换来的是几乎不会误报。报警声音本身也可以做得不那么刺耳响500ms、停200ms循环比持续长鸣更容易让人警觉又不像长鸣那么烦。5. 避坑记录总线死锁、数值漂移、读不到数据的排查顺序这一章写5个我见过或者自己踩过的坑每条按现象、原因、解决的顺序讲。调试顺序建议先查DHT11再查SGP30最后查I2C和电源因为单总线问题最直观I2C问题最容易伪装成传感器故障。5.1 DHT11读不到数据先查引脚配置和上拉电阻现象读函数一直返回超时或者读出来的湿度是255、温度是255偶尔能读到一次但隔几次又失败。原因有三个层次。第一层是PA0没有外部上拉或者上拉电阻太大10kΩ在长线下拉不回来第二层是释放总线前没有把引脚切到输入模式总线一直被自己的IO拉低第三层是延时函数被编译器优化掉了空循环被优化成零指令时序全部错位。解决外部加4.7kΩ上拉到3.3V读之前先调dht11_io_input()切输入微秒延时改用SysTick实现不要用空循环。如果模块是5V供电版本确认模块上的上拉电阻是接到5V还是3.3V3.3V的MCU引脚接收5V上拉的电平会超规格。5.2 SGP30数值偏高漂移基线没建立别急着换传感器现象刚上电eCO2显示400-500ppm过一会儿涨到800甚至1000多或者用酒精棉擦过手读数几个小时内降不下来怀疑传感器坏了。原因SGP30的基线需要连续供电才能建立数据手册给出的稳定时间是12小时左右。频繁断电会让基线反复丢失每次上电都是从“未知状态”开始重新评估。酒精、油漆、清洁剂产生的VOC会让读数大幅抬升这是它的工作原理决定的不是故障。解决不要频繁断电首次上电至少连续运行12小时后再看趋势不要盯绝对值。如果产品需要快速开机可用把基线存到片内Flash或外部EEPROM关机前发0x2015读取基线上电初始化后用0x201E写回可以跳过漫长的预热期。这个操作我在第6章展开。5.3 I2C总线死锁OLED和SGP30共总线时的复位手段现象OLED突然黑屏SGP30读取连续超时用逻辑分析仪看SDA一直被拉低主控复位数次后还是卡死。原因I2C从设备在主机复位的瞬间把SDA拉低总线进入死锁状态。常见诱因是OLED初始化过程中断电、SGP30供电波动导致内部状态机异常。一旦SDA被从设备占用主机发的起始条件无法产生整条总线瘫痪。解决所有I2C读写操作带超时这是底线。超时后先把SCL和SDA两个引脚配成开漏输出手动产生至少9个时钟脉冲让从设备释放总线再重新初始化I2C外设。这个释放逻辑我建议写成一个公共函数任何一次读写超时都调用它。吃过这个亏后我现在做项目的第一原则是显示可以重启传感器数据不能丢。5.4 蜂鸣器乱响阈值抖动与供电不足现象报警阈值附近的读数来回波动蜂鸣器跟着疯狂通断或者无源蜂鸣器接上去声音很小像蚊子叫。原因阈值没有回差单次读数一旦越限立即报警回落立即解除边界处必然抖动。无源蜂鸣器用GPIO高低电平驱动直流信号无法让压电片振荡声音自然出不来。还有一类原因是蜂鸣器模块的触发极性搞反低电平触发模块当高电平触发用。解决报警逻辑加回差和持续时间判定参考第4.2节代码。无源蜂鸣器改用定时器PWM输出2.7kHz方波。接模块前看丝印搞清楚高电平触发还是低电平触发单片机侧取反即可。5.5 OLED花屏和数据跳变初始化序列与刷新占用的坑现象屏幕有横向条纹或者刷新时上半屏正常下半屏花掉DHT11的湿度在40%到80%之间来回跳。原因花屏多半是初始化命令精简过头电荷泵没开、复用比设置不对、扫描方向和段重映射不匹配。DHT11数据跳变则要么是采样点落在高低电平的边界上要么是读取间隔小于1秒读到了新旧数据切换的中间态。解决OLED初始化序列照抄数据手册的推荐值不要自作聪明删命令刷新显示用页寻址模式数据变哪一页刷哪一页不要在中断里做I2C传输。DHT11采样点从40us微调到50us读取间隔放到2秒连续3次读失败就报传感器异常不要用旧值继续糊弄。6. 进阶让这套系统从能演示变成能长期跑演示阶段跑通只是第一步长期稳定运行才是这个项目真正考验人的地方。我一般会从四个方向做加固。第一个方向是基线管理。SGP30的基线写入命令是0x201E读取基线是0x2015。常见做法是每隔10分钟读一次基线并暂存到内存在检测到系统即将关机时写入STM32内部Flash。上电时先执行0x2003初始化延时100ms后把Flash里的基线写回这样设备开机的首次读数就接近真实的当前环境浓度不用再等12小时。写Flash要注意擦写寿命把基线数据放在独立的Flash页不要频繁写一天最多写几次。第二个方向是数据落盘和长稳验证。用USART1把每秒一行的CSV数据发出去格式固定为时间戳、温度、湿度、eCO2、TVOC、报警状态。串口另一端用PC的串口工具记录文件连续跑72小时重点看eCO2基线是否随时间漂移、OLED是否出现花屏、I2C是否有偶发超时。拿到的日志是判断这套系统值不值得继续投入的最直接证据。第三个方向是低功耗。如果目标是电池供电STM32F103进入Stop模式后功耗可以降到微安级别但SGP30的加热元件需要持续供电才能维持基线这和你省电的目标冲突。可行做法是加一个MOS管控制SGP30的电源每10分钟上电2分钟完成测量其余时间断电DHT11和OLED跟随断电STM32进Stop模式。代价是SGP30的基线会在断电期间丢失重新上电后需要几分钟重新建立趋势监测仍然有效但绝对值参考意义下降。第四个方向是输出更直观的结论。温湿度结合可以做露点近似计算eCO2和TVOC加一个0-100的空气质量指数显示页从“显示原始数值”升级为“显示健康建议”。这些算法不需要高深公式线性映射加分段阈值就能让终端用户一眼看懂。我习惯把“显示可以牺牲、数据不能丢”当作这类系统的设计原则所有传感器读取必须有超时报警判断必须独立于显示刷新OLED花屏时蜂鸣器依然能报警。这套思路让我少走很多弯路也希望帮到你。本文还有配套的精品资源点击获取

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑