ESP32与STM32F103实现SSDP局域网设备发现
发布时间:2026/9/12 5:02:08来源:尧图网络
简介这是一份面向Arduino与STM32F103开发者的ESP32 SSDP通信库资源包用于在局域网内实现设备自动发现与UPnP服务定位适合物联网、智能家居等场景中需要快速实现设备互联的工程师和电子爱好者。包内共计6个文件大小约8KB包含库必需的.h头文件、.cpp实现文件、library.properties配置、keywords.txt关键词、README说明文档以及一个示例程序.ino结构精简可直接导入Arduino IDE使用。目前已有282人学习借助这套源码可快速掌握SSDP广播、响应、通知等核心交互逻辑并结合STM32F103实践网络服务发现与设备控制减少底层网络协议的重复踩坑。整体是一份轻量、易上手的参考实现兼具学习与二次开发价值。1. SSDP 让 ESP32 和 STM32F103 在局域网里互相看见做多节点嵌入式项目时最头疼的不是通信协议选型而是设备上电后上位机怎么知道它存在。手动配 IP 在量产和现场维护阶段完全不可行SSDPSimple Service Discovery Protocol就是为这个场景设计的设备端往局域网多播组发一条通告客户端发一条 M-SEARCH 就能把网段内所有支持的设备枚举出来。ESP32SSDP-master 这个项目名对应的是一套典型分工——ESP32 作为搜索端master主动巡检STM32F103 作为设备端响应并提供描述地址双方不需要预先交换任何 IP 信息。这套方案的报文量极小两个平台都能轻松扛住适合做网关、产线设备盘点、批量固件管理这类基础设施。协议本身不难难点在两个平台的 socket 差异和响应时序控制上下面按一个可复现的落地路径展开。2. SSDP 协议要点UDP 多播、M-SEARCH 与 NOTIFY 的时序2.1 SSDP 报文格式与关键头字段SSDP 是 UPnP 协议栈里的设备发现层底层把 HTTP 报文直接装进 UDP 数据报因此叫 HTTPU。整个交互只有三种报文设备端上线主动发 NOTIFY 宣告搜索端发 M-SEARCH 查询设备端回 HTTP/1.1 200 OK。这三种报文全部发往同一个多播地址 239.255.255.250端口固定 1900。M-SEARCH 的每个头字段都有实际作用。MAN 固定为 ssdp:discover标记这是搜索意图MX 是响应延迟上限的秒数设备端必须在 0 到 MX 之间随机取一个延迟再回复否则几十台设备同时应答会把搜索端直接打崩ST 是搜索目标ssdp:all 表示全量搜索upnp:rootdevice 只找根设备也可以自定义厂家类型。响应报文里的 LOCATION 指向设备描述文档的完整 URLUSN 是设备唯一标识由 UUID 加设备类型拼接master 端靠它去重。区分请求和响应的关键是请求行请求以M-SEARCH * HTTP/1.1开头响应以HTTP/1.1 200 OK开头实现时不能只匹配 HTTP 字样。2.2 多播组与端口239.255.255.250:1900 的绑定细节239.255.255.250 是 IANA 为 SSDP 分配的本地链路多播组1900 是固定端口。两个平台的 socket 处理差异集中在这里搜索端要能发送多播报文同时需要加入多播组才能收到设备端的 NOTIFY 通告设备端要 bind 到本机任意地址的 1900 端口再用 IGMP 加入 239.255.255.250这样 M-SEARCH 和 NOTIFY 都能进入协议栈。TTL 参数容易被忽略。多播报文的 TTL 每经过一个三层设备减一局域网内取 4 足够取大了报文会跨 VLAN 扩散取 1 在某些交换机上又可能被静默丢弃。另外一个坑是 IP_MULTICAST_LOOP如果搜索端和设备端程序在同一台开发机上联调必须打开这个选项让多播报文回环否则本机发送的包本机收不到容易误判成设备端没回包。2.3 设备端与搜索端的角色划分设备端的完整时序是三条线并行上电后立即发一条 NOTIFY alive 让在线 master 秒级感知之后每隔一段时间常见 600 到 1800 秒重发宣告维持在线状态同时持续监听 1900 端口等待 M-SEARCH。master 端的 M-SEARCH 更像周期性点名巡检两者职责必须分开设备端不要去响应自己发出去的包master 也不要去监听除 1900 之外的端口。字段对应关系整理成下表写代码时直接对照取数字段M-SEARCH 请求200 OK 响应HOST239.255.255.250:1900无此头MANssdp:discover无此头MX2 ~ 5秒无此头STssdp:all 或具体类型回显请求中的 ST 值USN无uuid:xxx::upnp:rootdeviceLOCATION无http://IP:PORT/device-desc.xmlCACHE-CONTROL无max-age1800字段清楚后实现顺序就明朗了先在 ESP32 上把 M-SEARCH 发出去并成功解析响应再去 STM32F103 上做被动响应和上线宣告最后联调。3. ESP32 上实现 SSDP 搜索端masterM-SEARCH 的发送与响应解析3.1 用 socket 加入多播组并绑定 1900 端口ESP32 上实现 SSDP 搜索端我一般直接用 ESP-IDF 的 lwIP 组件开原生 socket不走上层 HTTP 框架因为 SSDP 报文就是裸文本socket 最直接。Wi-Fi 和网络栈初始化是 ESP32 项目的基础配置网上各类 esp32 教程都有直接略过。重点在 socket 创建后的三个设置绑定端口、加入多播组、设置接收超时。#include string.h #include freertos/FreeRTOS.h #include freertos/task.h #include esp_log.h #include lwip/sockets.h #include lwip/igmp.h #include lwip/inet.h #define SSDP_ADDR 239.255.255.250 #define SSDP_PORT 1900 static const char *TAG ssdp_master; static int ssdp_socket_create(void) { int sock socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP); struct sockaddr_in local; struct ip_mreq mreq; struct timeval tv; local.sin_family AF_INET; local.sin_port htons(SSDP_PORT); local.sin_addr.s_addr htonl(INADDR_ANY); bind(sock, (struct sockaddr *)local, sizeof(local)); /* 加入多播组否则收不到设备端的 NOTIFY alive 通告 */ mreq.imr_multiaddr.s_addr inet_addr(SSDP_ADDR); mreq.imr_interface.s_addr htonl(INADDR_ANY); setsockopt(sock, IPPROTO_IP, IP_ADD_MEMBERSHIP, (char *)mreq, sizeof(mreq)); /* MX3 时设备端响应会拖到 0~3 秒才到接收超时放宽到 4 秒 */ tv.tv_sec 4; tv.tv_usec 0; setsockopt(sock, SOL_SOCKET, SO_RCVTIMEO, (char *)tv, sizeof(tv)); return sock; }代码说明bind 到 1900 端口是可选但推荐的。不 bind 时源端口随机设备端的单播响应会发往这个随机端口同样能收到bind 之后搜索端和设备端的端口完全对齐抓包排错时只需要过滤udp.port 1900一个条件。IP_ADD_MEMBERSHIP 加入多播组后才能收到局域网内的 alive 宣告和 byebye 离线通知只做 M-SEARCH 响应的话不加入也能工作但会漏掉设备上下线事件。SO_RCVTIMEO 必须大于 MX否则收包循环会在设备端最长延迟的响应到达前提前退出。3.2 构造 M-SEARCH 报文并发送M-SEARCH 报文是纯 ASCII 文本按 HTTP 请求行加头字段拼接。最容易翻车的细节是行结尾必须是\r\n最后还要一个空行结束头区很多第一次写的同学只换行不回车设备端解析时整包丢弃。static void ssdp_send_search(int sock) { struct sockaddr_in dst; const char *msg M-SEARCH * HTTP/1.1\r\n HOST: 239.255.255.250:1900\r\n MAN: \ssdp:discover\\r\n MX: 3\r\n ST: upnp:rootdevice\r\n \r\n; dst.sin_family AF_INET; dst.sin_port htons(SSDP_PORT); dst.sin_addr.s_addr inet_addr(SSDP_ADDR); int ret sendto(sock, msg, strlen(msg), 0, (struct sockaddr *)dst, sizeof(dst)); if (ret 0) { ESP_LOGE(TAG, sendto failed: %d, errno); } }sendto 的目标地址就是多播组 239.255.255.250:1900内核会根据 socket 当前绑定的端口自动填充源地址。ST 头用 upnp:rootdevice 比 ssdp:all 更收敛返回的都是根设备如果只想发现自家产品可以自定义类型例如ST: urn:schemas-upnp-org:device:MySensor:1STM32F103 端要按完全相同的字符串回显。MX 取 3 是兼顾响应完整性和总耗时的经验值取 1 响应快但设备负载高时容易丢包取 5 会让一轮搜索最慢拖到 6 秒。3.3 解析响应先按 USN 去重再取 LOCATION收到 200 OK 后逐行扫描报文。设备端返回的头字段顺序不固定不能按固定行号取值正确做法是逐行匹配前缀。下面用 strtok 按回车换行切分每行判断 USN、LOCATION、ST 三个头typedef struct { char usn[128]; char location[192]; char st[64]; } ssdp_device_t; static void ssdp_parse_response(char *buf, ssdp_device_t *dev) { char *line strtok(buf, \r\n); while (line ! NULL) { if (strncmp(line, USN:, 4) 0) { strncpy(dev-usn, line 5, sizeof(dev-usn) - 1); } else if (strncmp(line, LOCATION:, 9) 0) { strncpy(dev-location, line 10, sizeof(dev-location) - 1); } else if (strncmp(line, ST:, 3) 0) { strncpy(dev-st, line 4, sizeof(dev-st) - 1); } line strtok(NULL, \r\n); } }strtok 会把\r\n替换成字符串结束符天然适合这种头字段解析代价是破坏原缓冲区所以传入的 buf 必须是可写副本。USN 是全网唯一的设备标识拿它做设备表的 key同一个 USN 只更新 LOCATION 和存活时间防止重复响应刷出多个条目。LOCATION 后面跟的是 http 开头的绝对 URLmaster 下一轮可以对该地址发起一次 HTTP GET 获取设备描述 XML。SSDP 报文本身很小单包不超过 1500 字节recvfrom 一次就能读完整条响应不需要考虑 UDP 分片重组。搜索任务完整跑起来是这样的节奏static void ssdp_task(void *arg) { int sock ssdp_socket_create(); char buf[512]; while (1) { ssdp_send_search(sock); struct sockaddr_in src; socklen_t slen sizeof(src); while (1) { int n recvfrom(sock, buf, sizeof(buf) - 1, 0, (struct sockaddr *)src, slen); if (n 0) break; /* 超时结束本轮收集 */ buf[n] 0; if (strstr(buf, 200 OK) NULL) continue; ssdp_device_t dev {0}; ssdp_parse_response(buf, dev); ssdp_device_update(dev); /* 按 USN 去重写入设备表 */ } vTaskDelay(pdMS_TO_TICKS(10000)); /* 10 秒一轮巡检 */ } }每个周期发一次 M-SEARCH收 4 秒之后再等 10 秒发下一轮。这个节奏对设备掉线的感知延迟约 14 秒网络噪声也很小。想缩短掉线感知时间可以降低 vTaskDelay但考虑到设备端的随机应答抖动搜索间隔低于 5 秒意义不大。注意 ssdp_device_update 要加互斥锁设备表可能同时被多个任务访问。4. STM32F103 上实现 SSDP 设备端NOTIFY 宣告与 M-SEARCH 响应4.1 STM32F103 接入以太网的方式与选型STM32F103 没有内置 MAC 和 PHY要跑 TCP/IP 必须外接以太网芯片。常见方案有三种ENC28J60 走 SPI只要 4 根信号线代价是带宽只有 10M 半双工但 SSDP 这类小报文完全够用DM9000A 走并口吞吐量上去了但占用 IO 太多小板子放不下更彻底的做法是换成带以太网 MAC 的型号。既然目标锁在 F103最常见也最稳的入门方案就是 ENC28J60 加 lwIPlwIP 源码包里自带 enc28j60 驱动配合 STM32F103 最小系统加一个 SPI 口就能跑起来。F103 的 SPI1 接 ENC28J60SCK 时钟建议 12MHz 以内F103 的 SPI 外设最高 18MHzENC28J60 在 12MHz 下更稳定。CS 用普通 GPIO 推挽输出INT 引脚接一个外部中断输入用于收包时唤醒处理。CubeMX 里除了 SPI1 还要开一个定时器给 lwIP 提供系统 tickHAL 库和标准外设库的差异只在底层初始化的写法lwIP 移植层结构相同。4.2 lwIP 绑定 1900 端口并加入多播组lwIP 侧核心是创建 UDP PCB 并绑定 1900 端口。SSDP 报文频率低、体积小raw API 比 netconn API 更省 RAM代价是回调函数跑在 tcpip_thread 上下文中不能在里面做阻塞操作。static struct udp_pcb *ssdp_pcb; #define SSDP_PORT 1900 static void ssdp_init(void) { ip4_addr_t group; ssdp_pcb udp_new(); if (ssdp_pcb NULL) { return; } /* 绑定本机任意地址的 1900 端口 */ udp_bind(ssdp_pcb, IP_ADDR_ANY, SSDP_PORT); /* 注册收包回调 */ udp_recv(ssdp_pcb, ssdp_recv_cb, NULL); /* 加入 SSDP 多播组端口绑定和加组缺一不可 */ IP4_ADDR(group, 239, 255, 255, 250); igmp_joingroup(IP4_ADDR_ANY, group); }参数说明udp_bind 的 IP_ADDR_ANY 表示绑本机所有接口对只有一个网口的 F103 来说绑定到具体网卡 IP 也行但 ANY 在 DHCP 重连换地址后不用重新绑。igmp_joingroup 的第一个参数传入 IP4_ADDR_ANYlwIP 会选默认网卡加入多播组必须在网卡拿到 IP 之后调用DHCP 未完成前加入会返回 ERR_VAL。不同 lwIP 版本宏名有差异1.4.1 用 IP_ADDR_ANY2.x 用 IP4_ADDR_ANY编译报错时按版本替换即可。加组失败时 lwIP 不会打印明显的错误日志调试时可以在调用后手动检查返回值。4.3 处理 M-SEARCH提取 ST 并随机延迟回复回调收到数据后第一步判断请求类型。SSDP 报文的请求行有M-SEARCH和NOTIFY两种都带HTTP/1.1必须用M-SEARCH前缀区分不能只找 HTTP 字样。第二步提取 ST 头判断是不是自己声明的设备类型不是就丢弃。第三步安排随机延迟回复。static void ssdp_recv_cb(void *arg, struct udp_pcb *pcb, struct pbuf *p, const ip_addr_t *addr, u16_t port) { char *msg; char *st_head; char st_val[64]; if (p NULL || p-len 16) { goto out; } msg (char *)p-payload; /* 只处理 M-SEARCHNOTIFY 由宣告任务主动发出 */ if (strncmp(msg, M-SEARCH, 8) ! 0) { goto out; } /* 提取 ST 头边界是 \r\n */ st_head strstr(msg, ST:); if (st_head NULL) { goto out; } memset(st_val, 0, sizeof(st_val)); strncpy(st_val, st_head 3, sizeof(st_val) - 1); st_val[strcspn(st_val, \r\n)] 0; if (strstr(st_val, upnp:rootdevice) NULL strstr(st_val, ssdp:all) NULL) { goto out; } /* 在 0~MX 秒内随机延迟回复避免多个设备同时响应 */ ssdp_delayed_response(addr, port); out: pbuf_free(p); }ssdp_delayed_response 内部用一个软件定时器在 0 到 3 秒之间随机触发真实发送动作。不能在回调里直接调用 vTaskDelay 做阻塞延时因为回调跑在 tcpip_thread 线程中阻塞它会导致整个 lwIP 协议栈停摆表现为所有 TCP 连接和 ARP 请求同时超时。这是 STM32 端最容易踩的坑看起来只卡了几百毫秒实际会造成大范围的网络异常。随机延时的目的是让多台设备在同一广播域内错峰回复搜索端 4 秒的接收窗口就是为这个设计的。回复报文的构造与搜索端对称注意响应要单播发给请求来源#define DEVICE_UUID uuid:350a5f82-1d8e-11ee-be6a-d48564c9a201 static void ssdp_send_response(const ip_addr_t *peer, u16_t port) { char msg[320]; struct pbuf *resp; int n; n snprintf(msg, sizeof(msg), HTTP/1.1 200 OK\r\n CACHE-CONTROL: max-age1800\r\n EXT:\r\n LOCATION: http://192.168.1.50:8080/device-desc.xml\r\n SERVER: FreeRTOS/10.2 UPnP/1.0 ESP32SSDP/1.0\r\n ST: upnp:rootdevice\r\n USN: DEVICE_UUID ::upnp:rootdevice\r\n \r\n); resp pbuf_alloc(PBUF_TRANSPORT, n, PBUF_RAM); if (resp ! NULL) { memcpy(resp-payload, msg, n); udp_sendto(ssdp_pcb, resp, peer, port); pbuf_free(resp); } }LOCATION 里的 IP 要填设备自己的实际地址正式代码应该从 netif 动态拼接不能写死否则换网络环境后 master 拿到的描述地址是错的。USN 的 UUID 建议在出厂烧录时生成并写入 Flash保证全网唯一搜索端靠它区分物理设备。SERVER 头里可以带上固件版本号master 解析后直接做版本比对这是个免费的顺带功能。CACHE-CONTROL max-age1800 告诉搜索端这条宣告的有效时长超过这个时间没刷新就应该把设备标记为离线。4.4 上线宣告 NOTIFY 的发送时机设备端初始化完成并拿到 IP 后立即向多播组发一条 NOTIFY alive让已在运行的 master 不用等下一轮搜索就能感知到新设备。报文格式与响应类似区别在请求行和头字段请求行是NOTIFY * HTTP/1.1用 NT 声明通告的设备类型用 NTS 声明状态为ssdp:alive没有 ST。发送目标地址同样是 239.255.255.250:1900。NOTIFY 要在网卡 up 之后发同时建议每隔 15 分钟重发一次配合 CACHE-CONTROL max-age1800保证宣告在过期前有刷新。OFFLINE 报文NTS 为 ssdp:byebye在设备正常关机时发一条让 master 立即清掉设备条目掉电场景发不出来也问题不大master 端靠周期巡检超时也能发现设备离线。5. 联调验证与参数调节search 间隔、TTL 与设备描述 URL5.1 用 Wireshark 抓包确认全链路两端都烧录后先把 PC 接到同一台交换机上抓包过滤器就一行udp.port 1900。正常现象是三类报文循环出现ESP32 发出的 M-SEARCH 多播、STM32F103 回的单播 200 OK、以及 STM32F103 上电初期的 NOTIFY alive。Linux 主机上也可以用 tcpdump 验证sudo tcpdump -i eth0 udp port 1900 -n -vv如果只看到 M-SEARCH 没有响应优先检查三处STM32 的 igmp_joingroup 返回值是否 ERR_OK、F103 的 IP 与 ESP32 是否在同一网段、ENC28J60 的 SPI 时钟是否超过 12MHz 导致收包乱码。如果响应有但 ESP32 端设备列表为空检查解析逻辑里 USN 去重是否把同一设备的重复响应误判为多条常见错误是 strncpy 之后没有补字符串结束符。5.2 search 间隔与 TTL 的参数权衡搜索间隔和 TTL 直接影响网络噪声和设备掉线感知速度参数推荐值参考下表参数推荐值说明MX3 秒设备端响应延迟上限取 1 易丢包取 5 拖慢整轮搜索TTL4多播报文跳数跨 VLAN 时按需调大搜索间隔10 ~ 30 秒小于 5 秒无实际收益设备端随机应答会造成抖动SO_RCVTIMEOMX 1等于 MX 可能刚好错过边界响应NOTIFY 重发周期900 秒约为 CACHE-CONTROL 的一半保证刷新先于过期调试阶段可以把搜索间隔压到 3 秒快速验证联调完成后改回生产值。TTL 在实际项目中按网络拓扑调整单交换机网络保持默认 4 即可不需要为了更快把它设成 1部分交换机对 TTL1 的多播转发策略并不一致。5.3 进阶LOCATION 指向描述 XML联动 OTA 与巡检SSDP 只负责发现真正的设备信息在 LOCATION 指向的 XML 里。master 端解析出 LOCATION 后对该 URL 发起一次 HTTP GET拿到设备描述文档文档里放固件版本、序列号、传感器类型等自定义字段。master 每轮巡检后比对版本号发现声明版本低于本地期望值就触发升级流程ESP32 的 OTA 升级链路可以复用同一套 HTTP 通道STM32F103 端则把新固件拉到内存后走 Bootloader 跳转。这样巡检、发现、版本管理三段逻辑全部收敛在 SSDP 这个入口上。一个实用的收包缓冲区技巧UDP 收包缓冲 512 字节对 SSDP 报文完全够用但描述 XML 的 HTTP 响应可能超过 512 字节这块数据单独走 TCP GET不要塞进 1900 端口的收包循环里。把设备描述 URL 固定放在http://ip:8080/device-desc.xml端口与 SSDP 的 1900 分离两个数据通道互不干扰排查问题时也能直接浏览器打开该地址验证设备端 HTTP 服务是否正常。本文还有配套的精品资源点击获取
网站建设高端定制企业官网