资讯动态

STM32F1实战:手写DHT11温湿度驱动与单总线时序解析

发布时间:2026/10/28 1:04:51 来源:尧图企业网站定制
如果你问一个做了三五年嵌入式的朋友“你第一颗正经入门的32位单片机是什么”十有八九会得到同一个答案STM32F1更具体点很可能是那块蓝色的小板子——F103C8T6。今天这篇不聊虚的就拿STM32F1系列配合最常见的DHT11温湿度传感器从选型、环境、时序原理、手写驱动到踩坑实录完整走一遍。整个过程都是以我实际做项目、带新人的经验为底子写的适合刚学完51、想跳32位MCU的同学也适合已经买了开发板但卡在“能点灯却读不到传感器”阶段的弟兄们。网上关于DHT11的例程其实多如牛毛但大多数只有代码没有为什么出了问题只能干瞪眼。这篇文章我把涉及的关键细节全部展开单总线时序到底长什么样、为什么读数据之前要把IO切回输入、微秒延时为什么不能用空循环凑合以及实测中让人崩溃的首读失败和长线干扰。换句话说你把这篇文章从头到尾跟一遍能跑通不说以后碰到类似时序型传感器DS18B20、DHT22也能自己排查问题。1. 为什么过了这么多年STM32F1依然是入门32位MCU的首选1.1 F1系列的江湖地位和它解决的核心问题先给完全没接触过的朋友补个背景STM32F1系列是意法半导体在Arm Cortex-M3内核上推出的主力产品线最经典的就是F103主频72MHz虽然放到今天看指标平平但它的意义在于把“32位MCU的门槛”拉到了白菜价。一个F103C8T6芯片几块钱到十几块钱一块完整的蓝色“蓝丸”最小系统板也就十几块再加上十几块钱的ST-Link下载器总共不到一杯奶茶钱就能拥有一套完整的32位开发环境。这在嵌入式圈子里有多重要回想一下51单片机时代8位机学完之后很多人想接触RTOS、复杂外设、DMA这些概念就会发现在8位平台上这些事做起来非常别扭。STM32F1的出现相当于给学习者准备了一把标准钥匙Cortex-M3内核统一了中断控制器NVIC、SysTick定时器、内存映射这些底层概念你只要在F1上把这些玩明白后面换F4、G0、G4甚至H7核心思路完全打通无非是外设寄存器和库函数的差异。1.2 围绕DHT11这个经典练习F103的资源到底够不够说到DHT11温湿度传感器它几乎和STM32F1是同一个年代的“黄金搭档”。DHT11用的是单总线协议一条数据线既要发命令又要传数据靠的是精确到微秒级的时序。这种传感器对于单片机有三个要求GPIO能快速切换输入输出模式、延时能达到微秒级精度、引脚能正确处理外部上拉电平。这三个要求STM32F1全部轻松满足72MHz的主频意味着每条指令大约13.9纳秒微秒级操作对于它来说游刃有余。而且用STM32F1来调DHT11还有一个隐形的好处F1的外设模型比较简单GPIO就是纯粹的GPIO笔直的寄存器操作路径让你一眼能看穿数据流动。相比之下H7这种带D-Cache的高端内核做位级协议反而会因为缓存一致性问题多出很多麻烦。所以F1加DHT11这个组合并不是因为“这两样都老”而是因为它们在复杂度上刚刚好匹配是练时序协议理解能力的完美起步项目。同时还能配合OLED屏、串口、定时器组成一个“传感器采集显示上传”的完整体验闭环这也是我特别推荐新手拿它当第一个实战项目的原因。2. 搭好开发环境三件套板子、IDE和下载器的选型逻辑2.1 开发板怎么选蓝丸、Nucleo还是国内教学板经常有新手私信我问“买哪块板子好”我的答案通常是看你想怎么学。如果打算跟着视频课一套流程走下来那直接买课程配套的教学板最省心比如正点原子的mini板或精英板、野火的指南者这些板子资料极其详尽每个外设都有配套例程DHT11这种小传感器更是随手一搜就有对应工程。缺点嘛就是板子体积大、价格稍贵外围电路多有些新手反而会被板载资源搞得眼花缭乱。如果喜欢自己折腾、追求极简那蓝丸F103C8T6绝对是最佳选择。整个板子就一颗芯片、一个USB口、两块晶振和若干排针没有多余的板载外设。DHT11实验只需要把传感器模块插到面包板上再用三根杜邦线接到PB1、3.3V和GND干干净净。官方还有个Nucleo-F103RB自带ST-Link调试器推免驱动适合想规范化学习的人不过价格是国产蓝丸的几倍。三个选择我统一做了个对比开发板价格调试器配套资料适合人群蓝丸最小系统板十几元需另购ST-Link通用例程多需自己接线喜欢动手、预算有限正点原子/野火教学板百元级板载或另购从入门到进阶非常系统跟视频课走的新手Nucleo-F103RB百元级板载ST-LinkST官方例程偏爱官方生态的人2.2 Keil MDK还是STM32CubeIDE标准库还是HAL库IDE方面国内老教程的主力是Keil MDK你随手在网上搜“STM32F1 DHT11例程”十个里有九个是Keil工程。Keil的界面确实老气但胜在稳定、占用资源小调试起来很顺手。STM32CubeIDE是ST官方的免费IDE基于Eclipse配合CubeMX图形化配置工具能够自动生成初始化代码新用户上手很友好。但这里有个实际问题老例程的Keil工程文件在CubeIDE里没法直接打开你得自己重新建工程、移植代码对于只想快速把DHT11跑通的新手来说迁移成本偏高。库的选择比IDE更关键。F1有两套主要固件库标准外设库StdPeriph和HAL库。标准库已经在官方停更但它在F1项目中的使用量依然庞大API粒度细比如读一个引脚电平就是GPIO_ReadInputDataBit一次调用到位。HAL库是ST当前的亲儿子配合CubeMX用起来确实省事但它封装层次多切换GPIO模式要调一堆结构体和函数微秒级时序操作时代码噪音非常大还容易因为函数调用开销影响时序节奏。我的建议非常直接如果你是在F1上做DHT11这种位级时序协议跟着老教程用标准库最合理如果已经习惯了HAL那读取DHT11的关键代码段也请直接用寄存器操作别在中断里反复调HAL库的大函数。2.3 下载器选择与最容易忽略的连线细节下载器现在的主流是ST-Link V2淘宝十几块包邮SWD模式只需要接四根线3.3V、SWDIO、SWCLK、GND。CMSIS-DAP协议的下载器很多自制调试器用的就是它也能在Keil里用和ST-Link操作差不多。串口ISP下载是更老的方式要把BOOT0跳帽拉高再复位下载慢还得手动切换只适合作应急方案。连线细节是最容易翻车的地方。我见过很多新手折腾半小时下载不了程序最后发现是下载器没跟板子共地SWDIO和SWCLK两根信号线是参考GND电平的不共地时序信号根本没法定。实际操作时最好养成习惯先把下载器GND和板子GND用杜邦线连好再接SWDIO和SWCLK。另外如果供电靠ST-Link输出3.3V点亮LED和运行DHT11这种微功耗负载完全没问题但别指望它给大功率外设供电老老实实用USB给板子供电更稳。3. DHT11单总线时序拆解想写出稳定驱动必须先看懂这张图3.1 单总线协议的特点省了引脚费了耐心DHT11和MCU之间只有一根数据线没有SCL时钟线也没有MOSI/MISO之分所有命令和数据都靠这根线上的电平变化和时间长度来传递。这就是单总线协议名字很直白。它和I2C这类有时钟线的同步协议最大的区别在于I2C的时序由主机控制的时钟决定从机只要跟着时钟走就行单总线则全靠时间窗口来区分逻辑0和逻辑1任何一段时序的偏差都可能让整包数据解错。DHT11的电气结构有个关键点它的数据引脚是开漏输出自己不能主动输出高电平必须靠外部上拉电阻把数据线拉高。这就是为什么裸传感器比模块难调——模块上已经焊好了上拉电阻和滤波电容而裸传感器只有三根脚你得上拉处理。数据线在空闲状态下被上拉电阻维持在高电平发送起始信号、传输数据时再通过拉低来产生脉冲。理解了这一点后面看时序图就顺了。3.2 起始信号、响应信号和40位数据位的完整过程一次完整的DHT11通信流程可以拆成五步主机拉低数据线持续至少18毫秒这是DHT11规定的启动信号时间太短传感器不响应主机拉高数据线维持20到40微秒然后释放总线同时把引脚从输出模式切成输入模式DHT11检测到启动信号后先拉低总线80微秒作为响应再拉高80微秒告诉主机“我准备好了”DHT11开始逐位发送40位数据每一位都由一段约50微秒的低电平开始随后根据数据是高电平26到28微秒逻辑0还是高电平70微秒逻辑1来区分数据发送完毕DHT11释放总线数据线回到高电平空闲状态。这40位数据的排布是固定的湿度整数部分8位、湿度小数部分8位、温度整数部分8位、温度小数部分8位、校验和8位。前四个字节的数值相加取低8位应该正好等于校验和字节。DHT11的湿度小数位和温度小数位在实际器件中通常恒为0但读回来的流程是完整的别自己省掉这两字节的解析。3.3 采样窗口的选择为什么大家都在40微秒处读引脚理解了位时序之后最关键的问题就是怎么区分一个位是0还是1我们发现每一位都是从50微秒低电平开始但高电平持续时长不同。如果我从低电平结束那一刻起延迟大约40微秒再去读数据线对于逻辑0它的高电平只维持26到28微秒40微秒时早就变回低电平了读回0对于逻辑1它的高电平依然维持在高状态读回1。这个40微秒的采样点就落在了0和1的判定区间正中间留足了余量。很多初版代码用“检测到高电平后计时看高电平持续了多久再判断0/1”的思路理论上也对但实现起来要开一个高精度计时器代码复杂不少。40微秒采样法简单直接只要你的微秒延时精度够几十位数据读下来基本不会出错。这也是我推荐新手先按采样窗口思路写驱动的原因——代码量小逻辑清晰出问题时也方便用逻辑分析仪定位。4. 手写DHT11驱动GPIO切换、微秒延时和校验和4.1 引脚选择与GPIO初始化先定引脚。我这里用PB1做示范原因很简单它没有被默认占用和开发板上的其他外设冲突概率小引出也方便。在选择引脚时记得避开PB2它和BOOT1复用、PA13/PA14SWD调试口也不要用已经被板载LED、按键占用的引脚否则调试时容易互相干扰。GPIO初始化分两个函数来写一个配成推挽输出一个配成浮空输入。因为DHT11通信前主机要发起始信号这时引脚必须是输出模式等起始信号发完要读DHT11的响应和数据又必须切成输入模式。F1的GPIO模式切换速度是纳秒级的对微秒级时序几乎没有影响放心切#define DHT11_GPIO_PORT GPIOB #define DHT11_GPIO_PIN GPIO_Pin_1 #define DHT11_RCC_GPIO RCC_APB2Periph_GPIOB void DHT11_GPIO_Init_Output(void) { GPIO_InitTypeDef GPIO_InitStructure; RCC_APB2PeriphClockCmd(DHT11_RCC_GPIO, ENABLE); GPIO_InitStructure.GPIO_Pin DHT11_GPIO_PIN; GPIO_InitStructure.GPIO_Mode GPIO_Mode_Out_PP; GPIO_InitStructure.GPIO_Speed GPIO_Speed_50MHz; GPIO_Init(DHT11_GPIO_PORT, GPIO_InitStructure); GPIO_SetBits(DHT11_GPIO_PORT, DHT11_GPIO_PIN); } void DHT11_GPIO_Init_Input(void) { GPIO_InitTypeDef GPIO_InitStructure; GPIO_InitStructure.GPIO_Pin DHT11_GPIO_PIN; GPIO_InitStructure.GPIO_Mode GPIO_Mode_IN_FLOATING; GPIO_Init(DHT11_GPIO_PORT, GPIO_InitStructure); }这里要说明一个细节输入模式用的是浮空输入因为DHT11模块上已经有上拉电阻外部已经把电平拉高了如果你用的是无上拉的裸传感器这里改成带上拉输入模式或者自己在外部加一个4.7到10千欧的上拉电阻到3.3V否则引脚电平悬空读回来的数据必然出错。4.2 微秒延时SysTick定时器的正统做法DHT11时序全是微秒级代码里的delay_us必须足够精确。我见过太多人在这一步图省事写一个for循环空转当作延时函数结果编译优化等级一变整个时序彻底乱掉。正确做法是利用Cortex-M3内核自带的SysTick定时器。SysTick就是一个24位递减计数器主频72MHz时每计数72次正好1微秒。我们配置它每秒触发一次中断在中断里递减一个全局计数器delay_us就变成查询这个计数器static volatile uint32_t sysTickCounter 0; void SysTick_Handler(void) { if (sysTickCounter 0) { sysTickCounter--; } } void delay_init(void) { if (SysTick_Config(72)) { while (1); } } void delay_us(uint32_t us) { sysTickCounter us; while (sysTickCounter 0); } void delay_ms(uint32_t ms) { while (ms--) { delay_us(1000); } }SysTick_Config(72)的意思是把重装载值设为7272MHz时钟除以72得到1MHz也就是1微秒中断一次。这个方案的好处是不依赖任何外设定时器每个Cortex-M3内核都有SysTick代码移植到其他F1型号甚至其他厂家的M3芯片上都能直接用。4.3 读一个字节的完整逻辑与超时保护有了GPIO切换和延时函数就可以写核心读取代码了。读一个字节的循环逻辑如下等待每位的低电平结束延时40微秒到采样点读引脚电平判断0或1最后等待该位的高电平结束再进入下一位避免把上一位的尾巴误判成下一位的低电平。uint8_t DHT11_ReadByte(void) { uint8_t i, data 0; for (i 0; i 8; i) { while (GPIO_ReadInputDataBit(DHT11_GPIO_PORT, DHT11_GPIO_PIN) Bit_RESET); delay_us(40); if (GPIO_ReadInputDataBit(DHT11_GPIO_PORT, DHT11_GPIO_PIN) Bit_SET) { data (data 1) | 0x01; } else { data (data 1); } while (GPIO_ReadInputDataBit(DHT11_GPIO_PORT, DHT11_GPIO_PIN) Bit_SET); } return data; }注意这里的两个while循环理论上是等DHT11把电平拉高或拉低但万一传感器没接好、断线了程序会卡在这些循环里出不来。我的建议是给等待加上超时保护比如用一个变量递减减到0就返回一个异常值避免整个系统假死。特别是在现场调试阶段这个保护能大大节省你排查问题的时间。4.4 完整读取函数发起通信、读取五字节、校验读取一次完整数据的函数要完成四件事切输出模式、拉低20毫秒发起起始信号、拉高30微秒后切输入模式、等待DHT11响应、连续读5个字节、最后把引脚恢复成输出高电平。恢复成输出高这一步很多人会漏掉其实很有必要一方面为下一次通信做准备另一方面避免引脚长时间处于浮空状态引入干扰。uint8_t DHT11_ReadData(uint8_t *humi_int, uint8_t *humi_dec, uint8_t *temp_int, uint8_t *temp_dec, uint8_t *checksum) { uint8_t buf[5]; uint8_t i; uint16_t timeout; DHT11_GPIO_Init_Output(); GPIO_ResetBits(DHT11_GPIO_PORT, DHT11_GPIO_PIN); delay_us(20000); GPIO_SetBits(DHT11_GPIO_PORT, DHT11_GPIO_PIN); delay_us(30); DHT11_GPIO_Init_Input(); timeout 1000; while (GPIO_ReadInputDataBit(DHT11_GPIO_PORT, DHT11_GPIO_PIN) Bit_SET) { if (--timeout 0) return 1; } timeout 1000; while (GPIO_ReadInputDataBit(DHT11_GPIO_PORT, DHT11_GPIO_PIN) Bit_RESET) { if (--timeout 0) return 1; } timeout 1000; while (GPIO_ReadInputDataBit(DHT11_GPIO_PORT, DHT11_GPIO_PIN) Bit_SET) { if (--timeout 0) return 1; } for (i 0; i 5; i) { buf[i] DHT11_ReadByte(); } DHT11_GPIO_Init_Output(); GPIO_SetBits(DHT11_GPIO_PORT, DHT11_GPIO_PIN); if ((uint8_t)(buf[0] buf[1] buf[2] buf[3]) buf[4]) { *humi_int buf[0]; *humi_dec buf[1]; *temp_int buf[2]; *temp_dec buf[3]; *checksum buf[4]; return 0; } return 2; }响应等待那段我写了三段带超时的循环分别对应等待总线被DHT11拉低、等待低电平结束、等待高电平结束。这样的写法即使哪一步时序没对上程序也会在几毫秒内退出而不是永远卡在while里。这也是“能跑的例程”和“好用的例程”之间最大的区别。4.5 主函数正确接入串口打印把上面这些组合起来主程序就非常简单了。这里串口打印用printf重定向到USART1波特率9600或者115200都行。注意DHT11的官方手册明确规定两次读取间隔不能小于1秒因为传感器本身的采样周期就是1秒左右读太快只会浪费CPU而且数据不更新。我用2秒间隔int main(void) { uint8_t humi_int, humi_dec, temp_int, temp_dec, checksum; delay_init(); USART1_Init(115200); delay_ms(1000); while (1) { if (DHT11_ReadData(humi_int, humi_dec, temp_int, temp_dec, checksum) 0) { printf(Humi: %d.%d%% Temp: %d.%d C\r\n, humi_int, humi_dec, temp_int, temp_dec); } else { printf(Read DHT11 failed\r\n); } delay_ms(2000); } }printf重定向到串口的实现方式很简单重写fputc函数就行int fputc(int ch, FILE *f) { while (USART_GetFlagStatus(USART1, USART_FLAG_TXE) RESET); USART_SendData(USART1, (uint8_t)ch); return ch; }5. 实测中真正折磨人的几个坑首读失败、长线和优化等级5.1 首次上电读取失败不是驱动写错了我调试DHT11时遇到最多的现象是程序刚烧录完第一次读取就失败串口打印Read DHT11 failed但过一会儿自动恢复或者复位一次就好了。很多人以为这是驱动代码有bug拼了命调时序参数结果越调越乱。真实原因是DHT11上电后需要至少1秒的稳定时间在这个窗口期内它自己都还没准备好自然没法响应主机请求。对策很简单main函数里初始化外设之后先延时1秒再进主循环。另一个更通用的思路是设计重试机制第一次读取失败没关系循环里隔1秒再试一次连续读几次成功后就正常了。我实际测试过加了启动延时之后首次读取成功率几乎百分之百。这里忍不住插一句遇到“一次失败但复位就好”的问题先别急着怀疑代码逻辑去查硬件上电时序和稳定时间往往比抠代码快得多。5.2 编译优化等级篡改空循环时序的隐形杀手这个坑极其隐蔽症状也很迷惑人同一个工程在开发板上能跑换了台电脑、改了优化等级之后数据突然全是校验错误。问题出在那个被当成延时用的for循环。Keil默认的-O0优化不会动空循环但一旦开到-O2甚至-O3编译器会把这个空循环优化成一条跳转指令或者直接删掉延时时间变成原来的几十分之一甚至归零。DHT11对微秒级时序敏感延时一错读回来的40位数据基本全是0或者全乱。解决办法有两个从根上杜绝一是用SysTick或定时器的延时函数替代空循环这个我前面代码里已经做了不受编译优化影响二是如果你确实要用循环延时必须把循环变量声明为volatile并且用汇编级别的空指令卡时间。但说实话嵌入式调试里这种问题没必要自己硬扛上SysTick才是正路。如果你已经写了空循环延时又碰到“优化一开就挂”的情况基本可以直接锁定是这个原因。5.3 长引线和上拉电阻单总线经不起折腾DHT11特别怕线长。单总线协议本身是为短距离通信设计的在我的实际测试中用杜邦线连接DHT11模块和STM32F1线长超过20到30厘米之后校验错误就开始零星出现超过半米基本能稳定复现读取失败。原因也不难理解数据线上的分布电容变大脉冲沿变缓MCU在40微秒采样点读到的电平可能已经偏离了真实状态。如果实在迫不得已要用长线有几个实操技巧可以缓解数据线尽量短宁可绕远也不要贴着电源线走在数据线对地并一个10纳法的小电容可以滤掉部分高频干扰但注意电容太大反而会让脉冲沿更缓适得其反上拉电阻也可以适当减小比如用2.2千欧增强驱动能力。如果这些做完了还是不稳定那就老老实实换方案要么换成更耐长线的I2C接口温湿度传感器比如SHT30要么加一个缓冲芯片。5.4 裸传感器与模块的区别上拉电阻不能没有很多新手第一次用DHT11买的是那种五毛钱一颗的裸传感器往面包板上一插三根线一接就指望它能跟模块一样正常工作。结果当然是失败。区别就在于模块上焊好了上拉电阻和滤波电容裸传感器全靠你自己补。DHT11数据引脚是开漏结构必须靠外部上拉电阻才能把空闲状态维持在高电平这一点在原理上是硬要求不是可选项。如果你用裸传感器正确的接法是VCC接3.3VGND接地数据脚接MCU的GPIO同时在数据脚和VCC之间焊一个4.7到10千欧的上拉电阻最好再在VCC和GND之间并一个0.1微法的陶瓷电容。还有一点要注意DHT11的供电范围是3.3到5.5V但STM32F1的GPIO引脚虽然大部分是5V容忍的为了简单稳妥还是统一用3.3V供电比较好这样传感器输出的高电平就是3.3V和MCU电平完美匹配。6. 从采集到展示OLED显示和串口上传的扩展思路6.1 I2C OLED显示温湿度DHT11读到的数据如果只在串口助手里看总感觉少了点什么。接一块0.96寸的I2C OLED屏SSD1306驱动芯片整个项目瞬间就变成一个独立的桌面温湿度计。OLED模块只有四根线VCC、GND、SCL、SDA用杜邦线接到F103的PB6SCL和PB7SDA就能开始干活。我在F1上用OLED一般建议直接软件模拟I2C两个GPIO改改翻转逻辑就行不用调硬件I2C那堆烦人的超时和中断配置。关于F1硬件I2C的传言很多实际用下来并不是不能用但软件I2C对新手来说调试更友好出问题也好排查。char str[20]; sprintf(str, Humi:%d.%d%%, humi_int, humi_dec); SSD1306_ShowString(0, 0, str); sprintf(str, Temp:%d.%dC, temp_int, temp_dec); SSD1306_ShowString(0, 2, str);需要注意OLED屏内部上电初始化需要一小段时间主程序里给SSD1306_Init()之后最好也加个100毫秒左右的延时再开始刷屏否则第一屏可能显示乱码或者空白。6.2 用定时器中断替代阻塞式延时前面的主循环代码用了delay_ms(2000)简单是简单但有个问题LED闪烁、其他传感器采集这类并发任务全部被阻塞了属于“点灯思维”阶段的写法。稍微进阶一点就会想到用定时器中断来生成时间基准。比如用TIM2配置1秒中断在中断里置一个标志位主循环检测到标志位才去读DHT11其余时间可以处理别的事情。volatile uint8_t one_second_flag 0; void TIM2_IRQHandler(void) { if (TIM_GetITStatus(TIM2, TIM_IT_Update)) { TIM_ClearITPendingBit(TIM2, TIM_IT_Update); one_second_flag 1; } } while (1) { if (one_second_flag) { one_second_flag 0; DHT11_ReadData(humi_int, humi_dec, temp_int, temp_dec, checksum); } // 其他任务... }这个转变虽然只是一个小技巧但思路价值很大从“死等”到“事件驱动”是从单片机小白走向工程化开发的必经一步。6.3 串口上传到上位机和小型IoT串口打印已经能解决调试问题但如果想把DHT11数据变成一个长期的记录系统可以接个ESP8266模块用AT指令通过串口把温湿度数据发到局域网里的MQTT服务器再接Home Assistant之类的平台。STM32F1的串口外设足够可靠唯一的建议是串口通信务必加简单的协议头校验别裸发裸收否则数据错位排查起来比代码本身还费劲。我更推荐的进阶方向是用Python写一个简单的串口上位机pyserial读取串口数据、matplotlib实时画温湿度曲线。这样你不仅练了MCU端的数据采集还顺带接触了上位机数据处理整个系统的完整性一下就上来了。DHT11的精度作为环境监测够用但你要是做科研级别的测量趁早换SHT30或SHT40这类数字传感器。说了这么多最后分享一个我调试时序外设时最受益的小习惯花几十块钱买一个逻辑分析仪不用多高级8通道、24MHz采样率就够用。DHT11通信无法用示波器直观观察但逻辑分析仪可以完整抓下起始信号、响应信号和40位数据的波形一眼就能看出哪一位的时序偏了哪些地方需要调整延时。我当年调DS18B20时被时序折磨了整整两天最后用逻辑分析仪一抓波形问题瞬间现出原形。现在但凡碰到类似单总线、一线协议的传感器第一反应永远是先抓波形再改代码。这套思路相信对你也一样好用。

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

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

免费获取报价 →
↑