news 2026/10/19 1:03:58

STM32+华为云IoT+Qt:智能家居远程控制系统实战

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
STM32+华为云IoT+Qt:智能家居远程控制系统实战

刚开始做智能家居控制系统的时候,我和很多人一样,以为最难的是单片机那边:无非是把传感器数据采上来,再控制几个继电器开关。可真把一个设备跑起来、把数据送上云、再在手机和电脑上远程控制之后才发现,真正决定项目能不能“用起来”的,是云端接入、协议设计、终端联调这一整套链路。这套“STM32 + 华为云IoT + Qt”的组合,是我在反复试错之后固定下来的方案:STM32负责硬件采集和控制,华为云IoT负责设备接入与数据中转,Qt负责跨平台的控制端界面。三端各干各的活,又通过MQTT协议串成一条完整的链路。

这套方案适合什么人?如果你正在做毕业设计、想给自己的嵌入式作品加远程控制能力,或者刚接触物联网开发、想从“跑马灯Demo”跨到“可远程使用的完整设备”,这篇文章都能帮你少走不少弯路。我会把三端的开发思路、关键配置、核心代码、联调顺序都拆开讲,也会把我在实际调试中踩过的坑一并列出来。

1. 项目拆解:这套组合解决什么问题

1.1 三个关键字的定位

先看清楚这个项目标题里的三件事分别承担什么职责。

STM32是设备端的“大脑”。它负责读取温湿度、光照、开关状态等数据,也负责根据收到的指令去操作继电器、电机这类执行机构。之所以选STM32而不是直接用ESP8266或者树莓派,是因为STM32的资源占用小、外设丰富、实时性可控,而且它在嵌入式教学和实际产品里都很常见,性价比高。你可以把它理解成“家里的开关面板和传感器节点”。

华为云IoT是中间的“通信枢纽”。它管设备认证、数据存储、指令转发。没有它,你的设备只能在局域网里玩,出了家门就没法远程控制。有了云平台之后,设备无论在哪,只要能上网,控制端就能通过平台找到它。华为云IoT(IoTDA)对个人项目和中小规模设备接入是够用的,免费额度对于学习验证来说基本够用。

Qt是“遥控器”。它帮我做了一个跨平台的控制面板,同一个界面代码,编译出来就能跑在Windows、Linux、Android甚至树莓派上。Qt的信号槽机制和清爽的界面框架,用来做这种实时数据展示加按钮控制的工具非常顺手。整体来看,这个项目就是一个典型的“端—云—端”物联网应用:感知端、平台端、控制端。

1.2 端到端数据链路

把三端连起来之后,完整的数据流是这样的:

  • 上行:STM32通过传感器采集数据,把温湿度、光照、继电器状态打包成JSON格式,通过串口交给ESP8266,ESP8266以MQTT协议发布到华为云IoT平台的属性上报Topic。平台收到后保存到设备影子。Qt客户端定时查询设备影子,或者通过平台推送,把数据显示在界面上。
  • 下行:用户在Qt界面上点击“开灯”按钮,Qt调用华为云IoT的应用侧接口下发命令。平台把命令投递给设备对应的命令Topic。ESP8266收到后转发给STM32,STM32解析命令内容,执行继电器动作,再把执行结果作为命令响应发回平台。

整条链路里最关键的设计约束是:设备永远只和云平台通信,控制端也永远只和云平台通信。设备与控制端之间不直接建立连接,这就解决了远程控制和网络穿透的问题。哪怕你家路由器重启了、IP变了,控制端照样能找到设备。

1.3 为什么不是其他方案

很多人会问:我自己搭一个Mosquitto服务器不是更自由吗?项目刚开始时我也这么干过,后来放弃了四个理由:

方案优点我遇到的坑
自建Mosquitto完全可控、想怎么玩怎么玩没有设备认证体系,要自己写接入管理;数据没有默认存储;服务器需要公网IP或内网穿透,维护成本高
公有云通用MQTT服务部署简单、便宜同样缺少设备模型概念,多设备、多属性管理要自己做
华为云IoTDA自带产品模型、设备影子、规则引擎、设备模拟器平台配置有固定的Topic结构,需要花点时间理解它的产品模型设计

对我来说,华为云IoT最值钱的部分是“产品模型”和“设备影子”。产品模型相当于给设备建了一份“数据字典”,一个项目里所有设备有哪些属性、哪些命令,在平台上定义清楚之后,设备上报的数据格式、控制端下发的指令格式都统一了。设备影子则类似MQTT里retain消息的加强版,平台会缓存设备最新一次上报的属性,新上线的控制端随时能读到设备当前状态。这些功能自己搭服务器要实现还挺费劲,用云平台等于直接买了一辆装好的车。

2. STM32端:数据采集与云接入

2.1 硬件准备与接线

我做这个项目时用的是一套非常常见的低成本硬件组合:

  • 主控板:STM32F103C8T6,就是俗称“蓝板”的那片,72MHz主频跑传感器采集和指令解析完全够用。
  • 温湿度传感器:DHT22(也叫AM2302),精度比DHT11高不少,DHT11的湿度误差动不动±5%,做实际展示比较尴尬。
  • 光照传感器:BH1750,I2C接口,直接读出lux数值,代码简单。
  • 显示模块:0.96寸OLED,I2C接口,用来在本地显示设备状态,调试时不依赖电脑也能看到数据。
  • 网络模块:ESP8266-01S,通过串口AT指令方式工作,负责WiFi接入和MQTT收发。
  • 执行机构:一路继电器模块,控制一个灯泡或者小风扇。

接线时有一个容易忽略的点:ESP8266-01S是3.3V供电的,串口逻辑电平也是3.3V,而STM32F103的GPIO虽然号称可以容忍5V,但保险起见还是把STM32串口TXD接ESP8266的RXD之间加一个电阻分压,或者直接用3.3V供电的USB转串口模块做中转。我在第一阶段调试时直接在3.3V的ESP8266上接了5V的供电,模块发热严重还经常掉线,排查了很久才意识到是供电问题。

2.2 传感器采集程序设计

STM32端的主循环建议用“状态机 + 定时采集”的思路,不要每个传感器都去阻塞等待,否则后面加网络处理时程序会变得很难梳理。我在项目里用一个1秒周期的定时器做采集调度,到点后依次读取DHT22和BH1750,把结果存入一个全局结构体,需要上报时直接从结构体里取。

DHT22的读取时序比较讲究,核心流程是:主机拉低总线至少18ms发起起始信号,然后释放总线,传感器应答后连续输出40bit数据,其中湿度16bit、温度16bit、校验8bit。关键是要精确控制GPIO拉高拉低的延时,所以读取函数必须关中断或者用定时器捕获来做。写一个能跑的通用版本不难,但要在不同主频的STM32之间移植,延时函数一定要用定时器倒计时实现,不要用空循环粗略估算。

BH1750就友好得多,标准的I2C读两个字节,高位在前,最后除以1.2换算成lux。这里分享一个小细节:BH1750连续采集模式下,如果两次读取间隔太短,第二次读到的可能是缓存值。我实测加一个200ms的延时或者每次读取前重新发一次上电指令,数据才稳定。

2.3 网络通道:ESP8266 AT指令桥接

STM32F103没有原生网络协议栈,所以我的做法是让ESP8266当网络协处理器。STM32通过串口发AT指令,ESP8266负责WiFi连接和MQTT通信。这种方式虽然不如ESP32片上跑协议栈“高大上”,但胜在稳定可控,而且AT指令的逻辑很清晰,出现问题容易定位。

关键AT指令序列如下:

AT+CWMODE=1 AT+CWJAP="你的WiFi名","你的WiFi密码" AT+MQTTUSERCFG=0,1,"clientId","username","password",0,0,"" AT+MQTTCONN=0,"broker地址",1883,1 AT+MQTTSUB=0,"$oc/devices/{设备ID}/sys/commands/#",1 AT+MQTTPUB=0,"$oc/devices/{设备ID}/sys/properties/report","{\"services\":[{\"service_id\":\"sensor\",\"properties\":{\"temperature\":26.5,\"humidity\":58}}]}",1,0

我用的是官方AT固件2.x版本,MQTT相关指令比老版本完善很多,支持用户名密码认证,也能自定义Client ID。STM32端的串口解析我建议不要用逐字节阻塞等待,而是用DMA + 空闲中断把一帧完整的AT响应收进来,再用状态机判断是否收到“OK”或“ERROR”。这个细节很关键,因为ESP8266返回的响应帧可能乱序,例如收到MQTT订阅消息时没有先回复OK,解析逻辑需要区分“AT命令应答”和“主动推送的MQTT消息”两种帧。

2.4 上报数据与下行指令处理

上报数据时最重要的是JSON字段必须和云平台产品模型里定义的属性完全一致。平台是按产品模型来校验数据的,你上报一个模型里没有的属性,平台会直接丢弃或者报错。我一开始就是没注意大小写,上报了Temperature而不是temperature,数据一直不上云,排查了半天才发现。

属性上报的payload格式:

{ "services": [ { "service_id": "sensor", "properties": { "temperature": 26.5, "humidity": 58.0, "light": 320.0 } } ] }

下行指令方面,STM32需要提前订阅命令下发Topic。ESP8266收到平台下发的MQTT消息后,会把内容通过串口推给STM32,内容一般是:

{ "command_name": "set_relay", "service_id": "relay", "paras": { "status": 1 } }

STM32解析出command_name和paras.status之后,操作对应的GPIO翻转继电器,然后组织命令响应发回平台。这里有个坑:命令下发Topic里带着request_id参数,响应的时候必须把这个request_id原样拼回响应Topic里,否则平台匹配不上命令和响应,界面上会一直显示“命令下发超时”。

3. 华为云IoT平台配置与鉴权

3.1 创建产品与定义属性模型

在平台侧的第一件事是创建产品。进入IoTDA控制台,选择“产品”,新建一个产品,行业选“智能家居”,协议选MQTT,数据格式选JSON。产品创建完成后系统会生成一个产品ID,后面设备接入时要用。

接着就是定义产品模型。我定义了三个服务:

服务ID属性数据类型说明
sensortemperaturedecimal温度,单位℃
sensorhumiditydecimal湿度,单位%
sensorlightint光照度,单位lux
relaystatusint继电器状态,0关1开

另外还要在relay服务里定义一个命令set_relay,下发参数status类型为int。这个命令在平台侧会对应生成一个命令下发Topic,也是Qt控制端要调用的目标。

平台的产品模型相当于你和设备之间的“共同语言”。写死在程序里的字段名、类型、范围都必须以这里为准。如果后面要改属性,记得两边同步改,只改一边就会出现“设备上报正常但平台显示离线”的诡异现象。

3.2 设备注册与MQTT鉴权参数

产品建好后,在产品下注册设备,拿到两个核心参数:设备ID和设备密钥。设备ID一般是产品ID加一串固定后缀,设备密钥是在注册时由平台生成的一串字符。

MQTT接入时,连接参数是这样组织的:

  • broker地址:控制台“总览”页面的接入地址,我所在区域是类似xxxx.iot-mqtts.cn-north-4.myhuaweicloud.com这样的格式。
  • 端口:1883(明文)或8883(TLS加密)。
  • Client ID:直接填设备ID。
  • Username:直接填设备ID。
  • Password:不是设备密钥本身,而是用密钥对“时间戳”做HMAC-SHA256计算出来的哈希值。

第一次做的时候我直接把密钥当密码填了,结果MQTT连一次断一次。后来才搞明白,平台要求密码是用密钥对当前时间戳(Unix秒)做HMAC-SHA256签名。生成密码可以用下面的Python脚本:

import hmac import hashlib import time device_secret = "你的设备密钥" timestamp = str(int(time.time())) password = hmac.new( device_secret.encode('utf-8'), timestamp.encode('utf-8'), hashlib.sha256 ).hexdigest() print(password)

把生成的密码填进MQTT参数里,时间戳是接入那一刻的秒数,密码有有效期,过期后需要重新生成。这让手动配置显得麻烦,但换来了安全性——就算WiFi被蹭网,别人也拿不到你的设备控制权。我把这个密码生成逻辑直接写进了STM32初始化代码,每次上电时通过串口传给ESP8266,避免拿电脑手动算好再烧录进去。

3.3 先用平台模拟器打通链路

STM32代码还没完全调好之前,有一件事特别提效:用平台自带的设备模拟器先把云端链路跑通。在IoTDA控制台的设备详情页里,有一个“调试”功能,可以模拟设备上报属性、模拟应用下发命令。我把模拟器当作假的ESP8266,直接填写属性JSON上报,然后在另一侧下发命令,看平台能不能正常投递和响应。

这一步的价值在于把问题分层:如果模拟器上报成功,说明产品模型定义正确;如果模拟器能收到命令,说明命令下发链路正常。接下来再接真实设备,如果出问题,问题基本锁定在STM32或者ESP8266那一侧,不需要同时怀疑云端。我项目联调时大部分时间都花在这一步,能提前发现字段不匹配、Topic拼写错误这些低级但致命的问题。

3.4 规则引擎与数据流转

设备数据上云之后,如果只想在控制端看看当前值,设备影子就够用了。但如果你想记录历史数据、做超限告警,就需要用平台规则引擎。

IoTDA的规则引擎支持把设备属性变化转发到多种数据目的地,我在项目中把温湿度数据转发到了对象存储服务,定期生成CSV,方便做历史曲线。配置方式也很直接:在IoTDA控制台“规则引擎”里新建一条规则,数据来源选择“设备属性变化”,筛选条件可以是“温度大于30”,动作选择转发到对象存储或消息队列。这样在后端就能做数据分析或者告警通知,这是本地Broker方案很难白嫖到的能力。

这部分不是必须的,但如果你的项目是毕业设计或者想拿给更多人展示,把历史数据和告警加上,整体完整度会提升一大截。

4. Qt跨平台控制端开发

4.1 为什么用Qt,怎么选版本

控制端我选择了Qt,而不是给每个平台单独写App。原因是这个项目的控制端功能并不复杂:展示几个实时数据、画一个简单曲线、放几个开关按钮。Qt Widgets一套代码,Windows、Linux、Android都能编译跑起来,我不用维护三套代码。

版本选择上,我用的是Qt 5.15 LTS配合Qt Widgets。如果你是新项目,也可以直接用Qt 6系列,但要注意Qt 6移除了部分旧模块,第三方MQTT库和串口库的兼容性要先确认。界面布局用最简单的QWidget加布局管理器就足够,不需要上QML。图表展示如果平台没有Charts模块,可以先用一个自定义的QPainter折线图,数据量不大时反而更可控。

4.2 通信方案:应用侧API还是设备MQTT

这一步是初学者最容易纠结的地方。Qt控制端如果要给STM32设备下发命令,最“直觉”的做法是用一个MQTT客户端连接同一台broker,订阅设备的Topic。但我在华为云IoT上试过之后发现一个限制:平台规定设备只能收发自己设备ID命名的Topic,不能让一个设备直接去订阅另一个设备的Topic。也就是说,Qt端不能简单登录成“控制设备”就去控制“STM32设备”。

跨设备控制的正规路子有两条。

第一条是应用侧API。Qt端以应用身份调用华为云IoT的HTTPS接口,获取访问Token之后,调用“下发设备命令”接口。这种方式权限清晰,官方支持,推荐正式项目使用。

第二条是用MQTT加平台规则引擎做联动。把Qt端也注册成一个普通设备,Qt上报一个“控制请求”属性,平台规则引擎检测到这个属性变化后,触发“下发命令”动作,把指令投递给STM32设备。这种方式界面响应更实时,但多了一层平台规则配置,调试时链路更长。

我在项目里选了第一条路做主要控制通道,第二条路只在验证规则引擎时玩了一下。下面的代码以第一条路为主。

4.3 登录鉴权、命令下发与状态刷新代码

Qt端对接华为云IoT的API,核心是三步:获取Token、下发命令、查询设备影子。

获取Token的接口调用示例:

void CloudClient::login(const QString &user, const QString &pass) { QNetworkRequest req(QUrl("https://iam.cn-north-4.myhuaweicloud.com/v3/auth/tokens")); req.setHeader(QNetworkRequest::ContentTypeHeader, "application/json"); QJsonObject pwd, userObj, domain, identity, scope, project, auth, root; userObj["name"] = user; userObj["password"] = pass; domain["name"] = "你的账号名"; userObj["domain"] = domain; pwd["user"] = userObj; identity["methods"] = QJsonArray{"password"}; identity["password"] = pwd; auth["identity"] = identity; project["name"] = "cn-north-4"; scope["project"] = project; auth["scope"] = scope; root["auth"] = auth; QNetworkReply *reply = mgr_.post(req, QJsonDocument(root).toJson()); connect(reply, &QNetworkReply::finished, this, [=]{ token_ = QString::fromUtf8(reply->rawHeader("X-Subject-Token")); qDebug() << "token ok:" << token_.left(20) << "..."; }); }

注意Token在HTTP响应头里,不在返回体里。这是最容易踩的坑,我把返回体翻了个底朝天,最后才注意到响应头里有X-Subject-Token字段。

命令下发接口:

void CloudClient::sendRelayCommand(int onoff) { QNetworkRequest req(QUrl(apiBase + "/v5/iot/" + projectId + "/devices/" + deviceId + "/commands")); req.setHeader(QNetworkRequest::ContentTypeHeader, "application/json"); req.setRawHeader("X-Auth-Token", token_.toUtf8()); QJsonObject paras; paras["status"] = onoff; QJsonObject root; root["service_id"] = "relay"; root["command_name"] = "set_relay"; root["paras"] = paras; QNetworkReply *reply = mgr_.post(req, QJsonDocument(root).toJson()); connect(reply, &QNetworkReply::finished, this, [=]{ if (reply->error() == QNetworkReply::NoError) qDebug() << "command sent"; else qDebug() << "command failed:" << reply->errorString(); }); }

设备状态刷新我用的是定时器轮询设备影子,每3秒一次:

void CloudClient::refreshShadow() { QNetworkRequest req(QUrl(apiBase + "/v5/iot/" + projectId + "/devices/" + deviceId + "/shadow")); req.setRawHeader("X-Auth-Token", token_.toUtf8()); QNetworkReply *reply = mgr_.get(req); connect(reply, &QNetworkReply::finished, this, [=]{ QJsonDocument doc = QJsonDocument::fromJson(reply->readAll()); QJsonObject reported = doc.object()["shadow"].toArray()[0].toObject()["reported"].toObject(); double temp = reported["temperature"].toDouble(); double hum = reported["humidity"].toDouble(); emit shadowUpdated(temp, hum, reported["status"].toInt()); }); }

我给界面暴露了一个shadowUpdated信号,界面上直接连接这个信号去更新QLabel和按钮状态,数据驱动的思路很清晰。

4.4 界面布局与跨平台打包

界面我按三块来排:顶部是实时数据卡片,显示温度、湿度、光照;中间是继电器开关,用一个大按钮点击切换;底部是一张简易温度曲线,用QPainter每30秒画一个点。布局用QVBoxLayout和QHBoxLayout嵌套,一套代码在任何分辨率下都能自适应。

打包方面,关键是带着Qt运行库一起发布。Windows下用windeployqt工具,Linux下用linuxdeployqt,Android下编译成APK时要注意网络权限和TLS库,我实际使用中Android端的OpenSSL库版本和Qt内置的不一致会导致HTTPS请求失败,建议手动把对应架构的libssl.so和libcrypto.so一起打包进去。这也是跨平台开发里最容易被忽略的细节,界面都写好了,结果到了真机上网络请求全失败,排查起来特别消耗耐心。

5. 端到端联调与高频排错

5.1 联调顺序

我推荐所有做这一步项目的人都按下面的顺序联调,不要跳步:

  1. 平台侧先用设备模拟器上报属性,确认产品模型字段正确。
  2. 用平台官方调试工具下发命令,确认命令能投递并返回响应。
  3. STM32加ESP8266,先用手机热点或路由器网络,串口打印AT日志确认MQTT连接成功。
  4. STM32上报真实传感器数据,在平台设备详情页看属性有没有刷新。
  5. 平台下发命令,看STM32串口能否打印出命令JSON,继电器是否动作。
  6. Qt客户端先跑在Windows上,用测试环境验证登录、查询影子、命令下发。
  7. 最后把Qt端换成Android手机,验证移动网络和WiFi两种环境下都能远程控制。

第3步是关键瓶颈。很多问题都出在ESP8266没能连上MQTT,所以我说一定要在串口监视器里看完整的日志,不要只看STM32主控的输出,ESP8266的AT应答才是判断连接状态的唯一依据。

5.2 高频问题速查表

现象可能原因解决办法
ESP8266连WiFi经常掉线供电电流不足给ESP8266单独接3.3V稳压供电,不要和传感器共用LDO
MQTT连接成功但属性不上报Topic拼写错误或payload字段与产品模型不符用平台模拟器对比Topic和数据格式
平台显示命令超时命令响应Topic里的request_id没原样带回解析下发消息时保存request_id,响应时拼进Topic
Qt登录返回401IAM请求body里domain或project填错检查账号名、项目名是否和控制台一致,Token有效期约24小时
Android端HTTPS请求失败OpenSSL库缺失或不匹配手动把libssl.so、libcrypto.so放进APK的lib目录
Qt界面数据不刷新轮询定时器没启动,或deviceId填错先用浏览器直接调用查询接口确认能返回JSON
继电器动作一下又恢复STM32端解析status类型错误确认下发JSON里status是整数还是字符串,统一转换

这些坑都是我实际踩过的。最搞笑的是有一次我一直以为STM32端解析代码有问题,去调试了两天,最后发现是ESP8266模块在频繁发包时进入了透传模式,串口数据全错乱了。解决办法是在上报完一帧数据后发送“+++”退出透传,或者干脆不用透传,一句句AT指令发包。

5.3 几个亲测实用的细节

最后分享几个不一定写在文档里、但实际项目里很有用的经验。

第一,上报周期不要设得太短。我一开始想追求“实时”,每秒上报一次,结果云平台把高频请求限流了,设备频繁掉线。后来改成5秒一次,界面用曲线展示,体验反而更好。

第二,STM32端一定要有本地“离线兜底”。当网络断了,继电器开关逻辑要在本地仍然可用。我的做法是ESP8266每30秒尝试重连一次,重连期间STM32用按键本地控制继电器,并把未上报的数据缓存到内存里,恢复连接后再补报。这套机制在家庭网络不稳定时救了我的项目很多次。

第三,Qt端轮询间隔要留有余量,在Android上尤其明显。3秒轮询在Windows上毫无压力,但安卓机的省电策略会让定时器休眠,长时间挂后台后就再也不刷新了。最终我给Android版本加了WAKE_LOCK权限,并把轮询间隔放宽到5秒,既省电又能稳定刷新。

第四,密码生成时间戳问题。STM32上电后如果时间没同步,生成的时间戳可能不对。我的做法是让ESP8266在MQTT连接前先通过AT+CIPSNTPTIME获取一次网络时间,再拿这个时间戳生成MQTT密码,保证密码一定是最新的。

结尾

这套系统做完到现在已经稳定跑了一段时间,我最大的体会是:智能家居项目真正的工作量不在某一个端上,而在端与端之间的“契约”。STM32、华为云IoT、Qt每一端单独看都不算复杂,但把它们通过Topic、JSON、产品模型、API串起来,才是完整的物联网工程思维。如果你正在做类似项目,我强烈建议先把平台模拟器用熟,再碰真实硬件,这也是我自己后来一直坚持的方法,联调效率比之前高出一大截。

最后再留一个小扩展方向:现在这套方案里Qt端还只是被动查询和控制,下一步可以给它加个语音识别入口,把语音指令转成语义JSON,再走同样的API下发命令。硬件端也可以把单个继电器换成多路加一个红外遥控模块,让老空调、电视也纳入这套控制系统里。技术主干已经跑通,扩展就只是加节点的事了。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/19 1:03:21

芯片应用赛道备赛指南:从赛题汇总到系统集成实战

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/10/19 1:03:11

不考虑STM32时,国产单片机选型指南:GD32、华大、ESP32等实战推荐

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/10/19 1:03:11

定时器不够用?4位扩展法将8位PWM精度提升至12位

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/10/19 1:02:57

协处理器系统集成实战:总线挂载、寄存器接口与缓存一致性解析

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/10/19 1:02:43

5美元MCU跑AI助手:开源MimiClaw嵌入式语音识别实战

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/10/19 1:02:23

生化仪液面探测原理:多模态传感与临床鲁棒性设计

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华