ESP-NOW 无线通信详解

不依赖Wi-Fi路由器,无需TCP/IP协议栈,ESP32设备间直接"对话"------ESP-NOW是乐鑫基于Wi-Fi MAC层打造的无连接私有协议。它继承了Wi-Fi物理层约2Mbps的实际吞吐,却将协议栈开销降到最低:配对即用、一发即忘,功耗仅为标准Wi-Fi的十分之一。从传感器数据采集、遥控器到智能家居本地控制网络,ESP-NOW填补了"低功耗"和"高速率"之间的黄金地带。本文将从协议原理出发,带你完成点对点通信、一对多广播、加密传输,并和第11章的Deep Sleep结合,打造一个唤醒后秒发数据的超低功耗传感器节点。


一、引言

在前面的章节中,我们分别学习了 Wi-Fi 联网和 BLE 通信。它们都能满足设备之间的数据交换需求,但都有一个共同的前提:需要一个"中心节点"。Wi-Fi 需要路由器做中继,BLE 需要手机或网关做主机。

那如果两个 ESP32 之间想直接通信呢?------用串口有线连接当然可以,但不方便远距离部署;用 Wi-Fi 走 TCP/IP 又太重,还得事先配好路由器。ESP-NOW 就是为了解决这个"无中心直连"需求而生的。

ESP-NOW 的核心设计哲学是:简单到极致。它没有连接建立的过程,没有握手,没有 ACK 重传的复杂状态机(虽然底层的 Wi-Fi ACK 仍在工作),上层 API 只有三个核心动作------初始化、添加对端、发送数据。接收方注册一个回调,数据到了就被调用。仅此而已。

二、ESP-NOW 协议概览

2.1 什么是 ESP-NOW

ESP-NOW 是乐鑫在 Wi-Fi 的 MAC 层(数据链路层)之上实现的一种无连接通信协议。它直接利用 Wi-Fi 的物理层帧格式,在 MAC 帧的 Vendor Specific 字段中封装 ESP-NOW 数据,省去了 TCP/IP 协议栈的层层封装和解析开销。

关键特征:

特性 说明
通信模式 点对点、一对多广播、组播
最大数据负载 250 字节
加密支持 AES-CCMP(对标 WPA2 加密强度)
实际吞吐 约 1~2 Mbps(取决于环境)
通信距离 同标准 Wi-Fi(开阔环境约 200~300m)
对端数量 加密对端 ≤ 6 个,非加密对端 ≤ 20 个
是否需要 Wi-Fi 连接 否(但底层需要初始化 Wi-Fi 驱动)

2.2 与 Wi-Fi / BLE 的对比

维度 ESP-NOW Wi-Fi (Station) BLE
是否需要路由器/主机
单次传输时间 1~3ms 10~100ms(含TCP握手) 3~15ms
峰值电流 ~80mA(仅发送) ~180mA ~30mA
数据速率 ~2Mbps 最高 150Mbps 1Mbps
协议复杂度 极低

ESP-NOW 的优势在于极低的传输等待时间 ------从调用 esp_now_send() 到对端收到数据,通常只需要 1~3ms。而标准 Wi-Fi 从建立 TCP 连接到真正发送数据,至少需要几十毫秒。

2.3 工作模式与限制

ESP-NOW 需要初始化 Wi-Fi 驱动,但不要求连接路由器。它支持两种组合模式:

  • Station 模式(最常用):使用 Station MAC 地址作为身份标识
  • SoftAP + Station 共存模式:可以同时作为 AP 供其他设备连接,又通过 ESP-NOW 与其他 ESP32 通信

⚠️ 注意:ESP-NOW 和标准 Wi-Fi 共用同一个 Wi-Fi 射频硬件。如果设备既用 ESP-NOW 又要连接路由器,两者会分时复用射频通道。ESP-NOW 的通道必须与路由器所在通道一致,否则通信会中断。


三、ESP-NOW 核心 API

ESP-IDF 为 ESP-NOW 提供了简洁的 C 语言 API。我们先从最核心的初始化、发送和接收入手。

3.1 初始化流程

c 复制代码
#include "esp_now.h"
#include "esp_wifi.h"

#define ESPNOW_CHANNEL   1      // 默认通道

static void espnow_init(void)
{
    // 1. 初始化 Wi-Fi(必须,即使不连路由器)
    ESP_ERROR_CHECK(esp_netif_init());
    ESP_ERROR_CHECK(esp_event_loop_create_default());
    wifi_init_config_t cfg = WIFI_INIT_CONFIG_DEFAULT();
    ESP_ERROR_CHECK(esp_wifi_init(&cfg));
    ESP_ERROR_CHECK(esp_wifi_set_mode(WIFI_MODE_STA));
    ESP_ERROR_CHECK(esp_wifi_start());

    // 2. 初始化 ESP-NOW
    ESP_ERROR_CHECK(esp_now_init());

    // 3. 注册发送和接收回调
    ESP_ERROR_CHECK(esp_now_register_send_cb(espnow_send_cb));
    ESP_ERROR_CHECK(esp_now_register_recv_cb(espnow_recv_cb));
}

3.2 发送回调与接收回调

c 复制代码
// 发送完成回调
static void espnow_send_cb(const uint8_t *mac_addr, esp_now_send_status_t status)
{
    if (status == ESP_NOW_SEND_SUCCESS) {
        ESP_LOGI(TAG, "发送成功 -> " MACSTR, MAC2STR(mac_addr));
    } else {
        ESP_LOGE(TAG, "发送失败 -> " MACSTR, MAC2STR(mac_addr));
    }
}

// 接收回调
static void espnow_recv_cb(const esp_now_recv_info_t *recv_info,
                           const uint8_t *data, int len)
{
    ESP_LOGI(TAG, "收到来自 " MACSTR " 的 %d 字节数据",
             MAC2STR(recv_info->src_addr), len);
    ESP_LOG_BUFFER_HEX("data", data, len > 32 ? 32 : len);
}

3.3 添加对端与发送数据

c 复制代码
#include "esp_now.h"
#include "esp_mac.h"   // MACSTR 宏

// 示例:添加对端并发送数据
void espnow_send_to_peer(const uint8_t *peer_mac)
{
    // 1. 注册对端
    esp_now_peer_info_t peer = {
        .channel = ESPNOW_CHANNEL,
        .ifidx = WIFI_IF_STA,
        .encrypt = false,
    };
    memcpy(peer.peer_addr, peer_mac, 6);

    // 如果对端已存在,先移除再添加(确保参数更新)
    esp_now_del_peer(peer_mac);
    ESP_ERROR_CHECK(esp_now_add_peer(&peer));

    // 2. 发送数据
    uint8_t payload[] = "Hello ESP-NOW!";
    ESP_ERROR_CHECK(esp_now_send(peer_mac, payload, sizeof(payload)));
}

// 广播:将 peer_addr 设为广播地址
void espnow_broadcast(void)
{
    uint8_t broadcast_mac[6] = {0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF};

    esp_now_peer_info_t peer = {
        .channel = ESPNOW_CHANNEL,
        .ifidx = WIFI_IF_STA,
        .encrypt = false,
    };
    memcpy(peer.peer_addr, broadcast_mac, 6);
    esp_now_del_peer(broadcast_mac);
    ESP_ERROR_CHECK(esp_now_add_peer(&peer));

    uint8_t data[] = "Broadcast!";
    esp_now_send(broadcast_mac, data, sizeof(data));
}

四、实战一:点对点通信

现在我们实现一个完整的点对点示例:设备 A(发送端) 每 5 秒向设备 B(接收端) 发送一次温湿度数据。

4.1 发送端代码

c 复制代码
#include <stdio.h>
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "esp_log.h"
#include "esp_now.h"
#include "esp_wifi.h"
#include "esp_mac.h"
#include "nvs_flash.h"

#define TAG "SENDER"
#define ESPNOW_CHANNEL 1

// 接收端 MAC 地址(替换为你的实际 MAC)
#define PEER_MAC   {0xC8, 0xF0, 0x9E, 0xXX, 0xXX, 0xXX}

static void espnow_send_cb(const uint8_t *mac_addr, esp_now_send_status_t status)
{
    ESP_LOGI(TAG, "发送%s", status == ESP_NOW_SEND_SUCCESS ? "成功" : "失败");
}

void app_main(void)
{
    // 初始化 NVS(Wi-Fi 需要)
    nvs_flash_init();
    esp_netif_init();
    esp_event_loop_create_default();

    // Wi-Fi Station 模式
    wifi_init_config_t cfg = WIFI_INIT_CONFIG_DEFAULT();
    esp_wifi_init(&cfg);
    esp_wifi_set_mode(WIFI_MODE_STA);
    esp_wifi_start();
    // 设置 Wi-Fi 通道(必须与接收端一致)
    esp_wifi_set_channel(ESPNOW_CHANNEL, WIFI_SECOND_CHAN_NONE);

    // ESP-NOW 初始化
    esp_now_init();
    esp_now_register_send_cb(espnow_send_cb);

    // 添加对端
    uint8_t peer_mac[6] = PEER_MAC;
    esp_now_peer_info_t peer = {
        .channel = ESPNOW_CHANNEL,
        .ifidx = WIFI_IF_STA,
        .encrypt = false,
    };
    memcpy(peer.peer_addr, peer_mac, 6);
    esp_now_add_peer(&peer);

    // 模拟温湿度数据
    float temp = 25.5, humid = 60.0;
    while (1) {
        esp_now_send(peer_mac, (uint8_t *)&temp, sizeof(temp));
        vTaskDelay(pdMS_TO_TICKS(5000));

        esp_now_send(peer_mac, (uint8_t *)&humid, sizeof(humid));
        vTaskDelay(pdMS_TO_TICKS(5000));
    }
}

4.2 接收端代码

c 复制代码
#include <stdio.h>
#include <string.h>
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "esp_log.h"
#include "esp_now.h"
#include "esp_wifi.h"
#include "esp_mac.h"
#include "nvs_flash.h"

#define TAG "RECEIVER"
#define ESPNOW_CHANNEL 1

static void espnow_recv_cb(const esp_now_recv_info_t *info,
                           const uint8_t *data, int len)
{
    if (len == sizeof(float)) {
        float value;
        memcpy(&value, data, sizeof(float));
        ESP_LOGI(TAG, "收到浮点数据: %.2f (来自 " MACSTR ")",
                 value, MAC2STR(info->src_addr));
    } else {
        ESP_LOGI(TAG, "收到 %d 字节 (来自 " MACSTR ")", len, info->src_addr);
    }
}

void app_main(void)
{
    nvs_flash_init();
    esp_netif_init();
    esp_event_loop_create_default();

    wifi_init_config_t cfg = WIFI_INIT_CONFIG_DEFAULT();
    esp_wifi_init(&cfg);
    esp_wifi_set_mode(WIFI_MODE_STA);
    esp_wifi_start();
    esp_wifi_set_channel(ESPNOW_CHANNEL, WIFI_SECOND_CHAN_NONE);

    esp_now_init();
    esp_now_register_recv_cb(espnow_recv_cb);

    ESP_LOGI(TAG, "接收端已启动,MAC: ");
    uint8_t mac[6];
    esp_read_mac(mac, ESP_MAC_WIFI_STA);
    ESP_LOGI(TAG, MACSTR, MAC2STR(mac));

    // 接收端不需要添加对端,只需注册回调
    while (1) {
        vTaskDelay(pdMS_TO_TICKS(60000));
    }
}

关键点:接收端无需调用 esp_now_add_peer() ------ESP-NOW 的接收回调对任何来源的数据都触发,不论是否注册过对端。只有发送时才需要将对端添加到对端列表。


五、实战二:一对多广播

广播模式是 ESP-NOW 的一大亮点。将目标 MAC 地址设为 0xFF:FF:FF:FF:FF:FF,同频道内所有 ESP-NOW 设备都会收到数据。

c 复制代码
#define ESPNOW_CHANNEL 1

// 发送端------广播
void app_main(void)
{
    nvs_flash_init();
    esp_netif_init();
    esp_event_loop_create_default();

    wifi_init_config_t cfg = WIFI_INIT_CONFIG_DEFAULT();
    esp_wifi_init(&cfg);
    esp_wifi_set_mode(WIFI_MODE_STA);
    esp_wifi_start();
    esp_wifi_set_channel(ESPNOW_CHANNEL, WIFI_SECOND_CHAN_NONE);

    esp_now_init();
    esp_now_register_send_cb(espnow_send_cb);

    // 添加广播对端
    esp_now_peer_info_t peer = {
        .channel = ESPNOW_CHANNEL,
        .ifidx = WIFI_IF_STA,
        .encrypt = false,
    };
    memset(peer.peer_addr, 0xFF, 6);
    esp_now_add_peer(&peer);

    uint32_t count = 0;
    while (1) {
        char msg[32];
        snprintf(msg, sizeof(msg), "broadcast seq=%lu", (unsigned long)count++);

        esp_err_t ret = esp_now_send(peer.peer_addr, (uint8_t *)msg, strlen(msg) + 1);
        ESP_LOGI(TAG, "广播: %s (%s)", msg, ret == ESP_OK ? "OK" : "FAIL");

        vTaskDelay(pdMS_TO_TICKS(2000));
    }
}

广播场景下,接收端可以用 recv_info->src_addr 区分消息来源。同一个频道内最多可以有几十个设备同时监听,无需任何配网流程,上电即通。


六、实战三:加密通信

ESP-NOW 支持 AES-CCMP 加密(128 位密钥),对标 WPA2 的加密强度。加密通信需要先通过不加密通道交换 Primary Master Key(PMK),然后设置每个对端的 Local Master Key(LMK)。

6.1 发送端(加密配对)

c 复制代码
#include "esp_now.h"

static uint8_t s_pmk[16] = {0x01, 0x02, 0x03, 0x04, 0x05, 0x06,
                             0x07, 0x08, 0x09, 0x0A, 0x0B, 0x0C,
                             0x0D, 0x0E, 0x0F, 0x10};  // PMK(保密)
static uint8_t s_lmk[16] = {0x11, 0x12, 0x13, 0x14, 0x15, 0x16,
                             0x17, 0x18, 0x19, 0x1A, 0x1B, 0x1C,
                             0x1D, 0x1E, 0x1F, 0x20};  // LMK(保密)

void espnow_encrypted_send(const uint8_t *peer_mac)
{
    // 1. 设置全局 PMK
    esp_now_set_pmk(s_pmk);

    // 2. 添加加密对端
    esp_now_peer_info_t peer = {
        .channel = ESPNOW_CHANNEL,
        .ifidx = WIFI_IF_STA,
        .encrypt = true,       // 启用加密
    };
    memcpy(peer.peer_addr, peer_mac, 6);
    memcpy(peer.lmk, s_lmk, 16);  // 设置该对端的 LMK
    esp_now_add_peer(&peer);

    // 3. 发送数据(自动加密)
    uint8_t secret[] = "This is secret data";
    esp_now_send(peer_mac, secret, sizeof(secret));
}

6.2 注意事项

  • PMK(Primary Master Key):全局密钥,所有加密对端共享。用于派生加密参数。
  • LMK(Local Master Key):每个对端独立设置。两端设备的 LMK 必须相同,否则无法解密。
  • 加密对端最大数量:6 个(非加密对端最多 20 个)。
  • 加密数据长度不变,但底层 Wi-Fi 帧会增加约 16 字节的 MIC(消息完整性校验码)。

七、实战四:结合 Deep Sleep 的低功耗节点

我们将第11章的"BLE 传感器节点"改造为ESP-NOW 版本,实现"Deep Sleep 唤醒 → 采集 → ESP-NOW 发送 → 继续睡"的超低功耗循环。

c 复制代码
#include "esp_now.h"
#include "esp_sleep.h"
#include "esp_wifi.h"
#include "esp_mac.h"

RTC_DATA_ATTR static int s_send_count = 0;

// 接收端 MAC(替换为你的接收网关 MAC)
#define GATEWAY_MAC {0xC8, 0xF0, 0x9E, 0xAA, 0xBB, 0xCC}

void espnow_send_cb(const uint8_t *mac_addr, esp_now_send_status_t status)
{
    // 回调运行在 ISR 或高优先级任务中,仅设置标志
}

void app_main(void)
{
    esp_sleep_wakeup_cause_t cause = esp_sleep_get_wakeup_cause();

    if (cause == ESP_SLEEP_WAKEUP_UNDEFINED) {
        // 首次上电:初始化
        nvs_flash_init();
        s_send_count = 0;
    }

    // 每次唤醒都执行的采集和发送流程
    s_send_count++;
    ESP_LOGI(TAG, "#%d 唤醒", s_send_count);

    // 1. 初始化 Wi-Fi + ESP-NOW
    esp_netif_init();
    esp_event_loop_create_default();

    wifi_init_config_t cfg = WIFI_INIT_CONFIG_DEFAULT();
    esp_wifi_init(&cfg);
    esp_wifi_set_mode(WIFI_MODE_STA);
    esp_wifi_start();
    esp_wifi_set_channel(1, WIFI_SECOND_CHAN_NONE);

    esp_now_init();
    esp_now_register_send_cb(espnow_send_cb);

    // 2. 添加网关对端
    uint8_t gw_mac[6] = GATEWAY_MAC;
    esp_now_peer_info_t peer = {
        .channel = 1, .ifidx = WIFI_IF_STA, .encrypt = false,
    };
    memcpy(peer.peer_addr, gw_mac, 6);
    esp_now_add_peer(&peer);

    // 3. 采集传感器数据(I2C 初始化)
    init_i2c();
    float temp, humid;
    sht30_read_data(&temp, &humid);

    // 4. 通过 ESP-NOW 发送到网关
    // 自定义数据包格式:count + temp + humid
    uint8_t packet[12];
    memcpy(packet, &s_send_count, 4);
    memcpy(packet + 4, &temp, 4);
    memcpy(packet + 8, &humid, 4);

    esp_now_send(gw_mac, packet, sizeof(packet));
    vTaskDelay(pdMS_TO_TICKS(10));  // 等待发送完成

    // 5. 清理并进入 Deep Sleep
    esp_wifi_stop();
    esp_now_deinit();

    esp_sleep_enable_timer_wakeup(30 * 60 * 1000000);  // 30 分钟
    esp_deep_sleep_start();
}

这个方案的功耗特性和第11章的 BLE 版本几乎一致(都用了相同的 Deep Sleep 策略),但发送延迟从 BLE 的 200~500ms 连接建立时间降低到 ESP-NOW 的 1~3ms。网关端可以是一个常开电源的 ESP32,接收数据后通过 Wi-Fi 上传到 MQTT 服务器,实现低功耗传感器网络与云端的桥梁。


🧠 深度思考

ESP-NOW 是我非常欣赏的一个协议------不是因为它技术有多先进,而是因为它在"够用"和"简单"之间拿捏得恰到好处。

Wi-Fi 协议栈太重,BLE 的连接模型太繁琐,Zigbee 需要网关和特定硬件...... 嵌入式开发者其实经常面临一个困境:我就想传一个温度值,为什么需要搭一套完整的网络协议栈? ESP-NOW 的回答是:不需要。它直接把数据塞进 Wi-Fi MAC 帧的厂商自定义字段里,一发一收,干净利落。

当然,这种"简单"是有代价的。它没有 TCP 那样的拥塞控制,没有应用层的 ACK 重传(虽然你可以自己在数据包里加序列号实现),也没有路由能力------两个设备必须在一个 Wi-Fi 频道内,中间隔一堵厚墙就不可靠了。所以 ESP-NOW 并不适合做大面积组网,但在"单个房间内的传感器采集"、"遥控器控制"、"玩具之间互动"这些场景里,它几乎是完美的选择。

我个人的经验是:ESP-NOW 适合做系统的"最后三米"通信层,而不要试图用它做整个网络。网关收集 ESP-NOW 数据后再通过 Wi-Fi/Ethernet/4G 上云,这可能是 ESP-NOW 最高频、最实用的部署模式。


⚠️ 避坑指南 / 注意事项

  1. Wi-Fi 初始化是必须的 :即使不连接路由器,也必须调用 esp_wifi_init()esp_wifi_start()。Wi-Fi 驱动负责 MAC 层的收发操作,没有它 ESP-NOW 无法工作。

  2. 频道必须一致 :ESP-NOW 通信双方必须工作在同一个 Wi-Fi 频道上。如果一方调用了 esp_wifi_set_channel() 改变了频道,双方需保持一致。特别是同时使用 Wi-Fi Station 连接路由器 + ESP-NOW 时,ESP-NOW 频道必须与路由器频道一致(连路由器时频道由 AP 决定,不可手动设置)。

  3. 250 字节数据限制 :ESP-NOW 单次发送的最大数据负载为 250 字节。超过此长度会返回 ESP_ERR_ESPNOW_ARG。如果数据量较大,需要分片发送并在应用层重组。

  4. 发送回调的上下文esp_now_register_send_cb() 注册的回调运行在 Wi-Fi 任务的上下文中 (不是你的应用任务),里面不能做阻塞操作(如 vTaskDelay()malloc()、互斥锁等待)。建议在回调中只设置标志位或信号量,在主循环中处理后续逻辑。

  5. 发送失败不等于对方没收到 :ESP-NOW 的发送回调返回 ESP_NOW_SEND_SUCCESS 只表示底层 Wi-Fi ACK 已收到 ,即数据已被对端的 Wi-Fi 硬件确认接收,但并不保证对端的应用层一定处理了该数据。反之,ESP_NOW_SEND_FAIL 通常是底层连续重试失败(超过最大重试次数),但如果只是偶尔丢包,发送回调仍然可能是 SUCCESS

  6. Deep Sleep 后需重新初始化 :因为 Deep Sleep 会完全断电 Wi-Fi 外设,唤醒后必须重新调用 esp_wifi_init()esp_now_init() → 重新添加对端。esp_now_deinit()esp_wifi_stop() 在入睡前的清理也非常重要,否则底层可能会漏电。

  7. ESP-NOW 与 BLE 不能同时使用:ESP32-S3 的 Wi-Fi 和 BLE 共享 2.4GHz 射频前端,且 ESP-NOW 依赖 Wi-Fi MAC 层。虽然理论上 Wi-Fi 和 BLE 可以分时复用,但 ESP-NOW 运行期间 BLE 功能不可用。如果项目同时需要 ESP-NOW 和 BLE,需要在软件中切换模式。

  8. MAC 地址白名单:生产环境中建议在接收端维护一个 MAC 地址白名单,只处理已知设备的数据。否则只要同一频道上的任意 ESP32 发送数据,接收端的回调都会被触发,容易被恶意或无意干扰。


总结

本章我们全面覆盖了 ESP-NOW 无线通信的核心知识点:

  • 无连接协议:ESP-NOW 在 Wi-Fi MAC 层上实现,无需 TCP/IP 协议栈,无需路由器
  • 低延迟:单次传输仅 1~3ms,适合对实时性有要求的场景
  • 三种通信模式:点对点、一对多广播、加密通信
  • 四类实战:从基础的一对一通信到加密传输,再到结合 Deep Sleep 的低功耗传感器节点
  • 250 字节数据限制:适合传感器数据、遥控指令等小数据量场景

至此,你已经掌握了 ESP32-S3 上所有的无线通信方式:Wi-Fi、BLE、ESP-NOW。下一章我们将整合这些通信能力,搭建一个完整的物联网云平台接入方案------让 ESP32-S3 通过 MQTT 协议连接到阿里云 IoT / AWS IoT / 自建 Broker,实现远程监控和数据上云。


下篇预告

下一章我们将打通"设备到云端"的最后一段路------MQTT 物联网通信。你将学习如何在 ESP32-S3 上实现 MQTT 客户端,连接到主流的物联网平台,实现设备属性上报、远程控制指令下发和 OTA 固件升级。这是每个物联网项目都绕不开的实战环节。

相关推荐
李迟7 小时前
一种轻量级C++ CSV文件读写库的实现方案
开发语言·c++
小园子的小菜7 小时前
Java 并发编程:线程安全队列全解 —— 阻塞与非阻塞实现原理及源码深度剖析
java·开发语言
Y3815326627 小时前
2026 年 7 月,SERP API 调用的稳定性实战:超时、重试、降级
开发语言·数据库·人工智能·php
Wang's Blog7 小时前
Go-Zero项目开发38:深入限流器实现与应用
开发语言·golang·go-zero
风吹心凉8 小时前
python3基础2026.7.28
开发语言·python
Rabitebla8 小时前
C++ 内存管理全面复习:从内存分布到 operator new/delete
java·c语言·开发语言·c++·算法·leetcode
xcLeigh8 小时前
Go入门:main包与main函数的特殊地位
开发语言·后端·golang
嘶哈哈哈8 小时前
雷迅 CAN Babel 新品测评:一个小巧的 CAN/UART 双功能 USB 调试适配器
单片机·嵌入式硬件
心念枕惊8 小时前
PHP 在领域驱动(DDD)设计中的核心实践
android·开发语言·php
圣光SG8 小时前
Java操作题练习(二)
java·开发语言·python