news 2026/9/13 12:28:13

esp-nimble-cpp 的 Bluetooth 5.x 扩展广播(Extended Advertising)完全指南:从 PHY 到 API 迁移

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
esp-nimble-cpp 的 Bluetooth 5.x 扩展广播(Extended Advertising)完全指南:从 PHY 到 API 迁移

esp-nimble-cpp 的 Bluetooth 5.x 扩展广播(Extended Advertising)完全指南:从 PHY 到 API 迁移

【免费下载链接】TasmotaAlternative firmware for ESP8266 and ESP32 based devices with easy configuration using webUI, OTA updates, automation using timers or rules, expandability and entirely local control over MQTT, HTTP, Serial or KNX. Full documentation at项目地址: https://gitcode.com/GitHub_Trending/ta/Tasmota

本文以 Bluetooth 5 features.md 为骨架,结合 esp-nimble-cpp(NimBLE-Arduino 的 ESP32 移植版,作为 Tasmota 固件的 BLE 依赖库存放在lib/libesp32_div/esp-nimble-cpp)的源码与示例,系统讲解 Bluetooth 5.x 扩展广播的核心概念、三种 PHY 的区别、启用方式,以及启用后扫描、连接、广播三大 API 的变化与迁移路径。读完本文,你将能在自己的 ESP32 工程中正确开启并配置扩展广播,写出支持 251 字节甚至 1650 字节链式广播、多实例广播与 CODED 远距离通信的完整代码。

一、扩展广播(Extended Advertising)带来了什么

经典 BLE(Bluetooth 4.x)的广播被严格限制为31 字节的广播数据(ADV 报文),这迫使开发者把设备名、服务 UUID、厂商数据等塞进一个很小的空间,不得不频繁依赖扫描响应(Scan Response)或牺牲信息量。

Bluetooth 5.x 的扩展广播(Extended Advertising)从根本上打破了这一限制,esp-nimble-cpp 在CONFIG_BT_NIMBLE_EXT_ADV开启后完整暴露了这些能力。按原文档的描述,其核心收益有三点:

能力经典广播(4.x)扩展广播(5.x)
单次广播数据31 字节251 字节
链式(chained)广播数据不支持最高 1650 字节(取决于配置)
PHY(物理层)仅 1M1M / 2M / CODED 可选
广播实例1 个多个(实例 ID 递增,最多实例数取决于配置)
周期广播(Periodic Advertising)不支持支持同步(文档标注为待实现)

需要特别说明的是,1650 字节并非默认值。原文档明确写了 “up to 1650 bytes when chained (configuration dependant)”——链式广播允许的最终长度取决于 NimBLE 栈的具体配置(如BLE_EXT_ADV_MAX_SIZE等相关选项)。251 字节是单次扩展广播报文的理论上限,去掉头部等开销后实际可用载荷约为 230 字节左右,这一点在仓库示例的注释中也有印证(见下文示例代码注释 “251 bytes (minus header bytes ~20)”)。

1.1 三种 PHY(物理层)的取舍

原文档指出扩展广播引入了新的物理层:

  • 1M PHY:经典 BLE 默认的 1 Mbps 物理层,兼容性最好,所有 BLE 5.x 设备都支持;
  • 2M PHY:将数据率提升到 2 Mbps,适合吞吐优先、链路距离较短的应用;
  • CODED PHY:通过前向纠错编码(Coded PHY,即 Long Range 模式)换取更远的通信距离,但数据率会显著下降(约 125 kbps 或 500 kbps),适合远距离广播/扫描场景。

此外还有周期广播(Periodic Advertising):扫描设备可以先与某个信标的广播序列建立同步,从而在下一个预期广播到来之前休眠或执行其他任务,以节省 CPU 周期与功耗。原文档明确标注该项为 “To be implemented”(待实现),因此本文不做展开,读者在使用时应注意该能力在 esp-nimble-cpp 中尚不可用。

二、启用扩展广播:三种配置入口

扩展广播是编译期特性,由配置项CONFIG_BT_NIMBLE_EXT_ADV控制,取值为 1 时启用。根据你的构建环境,有三种设置方式:

2.1 ESP-IDF:menuconfig 图形化配置

在 ESP-IDF 工程中执行idf.py menuconfig,依次进入:

Component config > Bluetooth > NimBLE options > Enable extended advertising

勾选该项后保存退出,重新构建即可。仓库示例的 sdkconfig.defaults 展示了在 IDF 工程中以文本方式启用该特性的完整配置块:

CONFIG_BT_ENABLED=y CONFIG_BTDM_CTRL_MODE_BLE_ONLY=y CONFIG_BTDM_CTRL_MODE_BR_EDR_ONLY=n CONFIG_BTDM_CTRL_MODE_BTDM=n CONFIG_BT_BLUEDROID_ENABLED=n CONFIG_BT_NIMBLE_ENABLED=y CONFIG_BT_NIMBLE_EXT_ADV=y

其中CONFIG_BT_NIMBLE_ENABLED=y表示使用 NimBLE 作为唯一 BLE 协议栈(Bluedroid 被关闭),CONFIG_BTDM_CTRL_MODE_BLE_ONLY=y表示控制器仅启用 BLE 模式——扩展广播要求 NimBLE 栈且控制器支持 BLE 5 特性。

2.2 Arduino:nimconfig.h

在 Arduino(Arduino-ESP32 核心 + NimBLE-Arduino)环境中,直接在nimconfig.h中设置该宏为 1 即可。

2.3 PlatformIO:build_flags

在 PlatformIO 工程(包括本仓库 Tasmota 这类基于 PlatformIO 的固件)中,通过build_flags传入编译宏,例如在platformio.ini中加入:

build_flags = -DCONFIG_BT_NIMBLE_EXT_ADV=1

注意:无论哪种方式,该选项都需要与 NimBLE 主机栈的BLE_EXT_ADV(MYNEWT_VAL)联动生效。从 NimBLEExtAdvertising.h 的编译守卫可以看出,扩展广播相关类仅在CONFIG_BT_NIMBLE_ENABLED && MYNEWT_VAL(BLE_ROLE_BROADCASTER) && MYNEWT_VAL(BLE_EXT_ADV)同时成立时才会被编译进固件。

三、启用后:API 的整体变化

原文档给出了启用扩展广播后五条关键行为变化,这是理解整套新 API 的纲领:

  1. 扫描端NimBLEScan::start会自动同时在 1M PHY 和 CODED PHY 上扫描;
  2. 连接端NimBLEClient::connect默认使用对端设备正在监听的 primary PHY(除非显式指定);
  3. 连接端NimBLEClient::setConnectPhy变为可用,用于指定连接使用的 PHY(默认全部);
  4. 广播端NimBLEAdvertising不再可用,被NimBLEExtAdvertising取代,NimBLEDevice::getAdvertising()返回的不再是NimBLEAdvertising*而是NimBLEExtAdvertising*
  5. 广播数据端NimBLEAdvertisementData不再可用,被NimBLEExtAdvertisement取代,所有广播配置(含广播间隔、广播结束回调)都集中在这个新类上。

下面的三个小节分别对应扫描、连接、广播三条链路,逐一展开源码级细节。

四、扫描端:双 PHY 自动扫描与 setPhy

启用扩展广播后,NimBLEScan::start会自动在 1M 与 CODED 两个 PHY 上发起扫描。这一点在源码中得到印证:NimBLEScan.cpp 内部调用ble_gap_ext_disc_params构造扩展扫描参数,并分别向 1M 与 CODED 两个 PHY 传入扫描参数:

# if MYNEWT_VAL(BLE_EXT_ADV) ble_gap_ext_disc_params scan_params; scan_params.passive = m_scanParams.passive; // ... ble_gap_ext_disc(m_ownAddrType, m_scanParams.limited, m_phy & SCAN_1M ? &scan_params : NULL, m_phy & SCAN_CODED ? &scan_params : NULL, NimBLEScan::handleGapEvent, ...); #endif

而扫描的 PHY 集合可以通过NimBLEScan::setPhy(Phy phyMask)显式指定(见 NimBLEScan.cpp),可选的掩码值在 NimBLEScan.cpp 的注释中列出:

  • NIMBLE_CPP_SCAN_1M:仅扫 1M PHY;
  • NIMBLE_CPP_SCAN_CODED:仅扫 CODED PHY;
  • NIMBLE_CPP_SCAN_ALL(默认):1M + CODED 全扫。

同时,扫描回调中还能拿到广播报文的 PHY 信息。仓库的 NimBLE_extended_scan 示例演示了如何在onResult中读取getPrimaryPhy()/getSecondaryPhy(),并在onScanEnd里轮换SCAN_ALL → SCAN_1M → SCAN_CODED观察不同 PHY 下的扫描差异:

class ScanCallbacks : public NimBLEScanCallbacks { void onResult(const NimBLEAdvertisedDevice* advertisedDevice) override { printf("Advertised Device found: %s\n PHY1: %d\n PHY2: %d\n", advertisedDevice->toString().c_str(), advertisedDevice->getPrimaryPhy(), advertisedDevice->getSecondaryPhy()); } // ... }; NimBLEDevice::init("NimBLE Extended Scanner"); NimBLEScan* pScan = NimBLEDevice::getScan(); pScan->setScanCallbacks(&scanCallbacks); pScan->setActiveScan(true); pScan->setPhy(NimBLEScan::Phy::SCAN_ALL); // 默认即 SCAN_ALL pScan->start(10 * 1000); // 0 表示无限扫描

从源码还可以看到,扩展扫描对链式(分片)广播有专门的合并处理:当getDataStatus() == BLE_GAP_EXT_ADV_DATA_STATUS_INCOMPLETE时(NimBLEScan.cpp),扫描器会等待后续分片到达后再上报完整结果;对于使用同一地址但不同 set ID 的广播,会按sid(set identifier)区分成独立的广播设备(NimBLEScan.cpp)。

五、连接端:setConnectPhy 与连接 PHY 的选择

启用扩展广播后,NimBLEClient::connect默认会使用对端广播时监听的 primary PHY 发起连接。如果希望显式控制连接使用的 PHY,可以使用NimBLEClient::setConnectPhy(uint8_t phyMask)

源码实现非常直接——它只是把掩码存入成员变量m_phyMask(NimBLEClient.cpp),真正生效是在连接时传给ble_gap_ext_connect(NimBLEClient.cpp):

rc = ble_gap_ext_connect(NimBLEDevice::m_ownAddrType, peerAddr, m_connectTimeout, m_phyMask, &m_connParams, ...);

m_phyMask默认值是三个 PHY 全开,见构造函数初始化列表(NimBLEClient.cpp):

# if MYNEWT_VAL(BLE_EXT_ADV) m_phyMask{BLE_GAP_LE_PHY_1M_MASK | BLE_GAP_LE_PHY_2M_MASK | BLE_GAP_LE_PHY_CODED_MASK}, # endif

setConnectPhy的掩码定义与 NimBLE 栈一致(见 NimBLEClient.cpp 的注释):

  • 0x01=BLE_GAP_LE_PHY_1M_MASK
  • 0x02=BLE_GAP_LE_PHY_2M_MASK
  • 0x04=BLE_GAP_LE_PHY_CODED_MASK

例如只允许 CODED 远距离连接:

NimBLEClient* pClient = NimBLEDevice::createClient(); pClient->setConnectPhy(BLE_GAP_LE_PHY_CODED_MASK); pClient->connect(targetAddress);

此外,类中还保留有updatePhy(uint8_t txPhysMask, uint8_t rxPhysMask, uint16_t phyOptions = 0)(NimBLEClient.h),用于在已建立的连接上动态切换收发 PHY——例如先以 1M 建连,再协商升级到 2M 提速。

六、广播端:NimBLEExtAdvertising 与 NimBLEExtAdvertisement

这是启用扩展广播后改动最大、也最需要理解的一部分。

  • 旧的NimBLEAdvertising类被NimBLEExtAdvertising取代;
  • 旧的NimBLEAdvertisementDataNimBLEExtAdvertisement取代;
  • NimBLEDevice::getAdvertising()返回类型自动切换为NimBLEExtAdvertising*

从 NimBLEExtAdvertising.h 可以看到,NimBLEExtAdvertisement是“一切广播配置的集合体”,其公开接口覆盖:

数据内容类

  • setName(name, isComplete)/setShortName(name):设备名(完整名或短名);
  • addServiceUUID/removeServiceUUID/removeServices:单个服务 UUID;
  • setCompleteServices/setCompleteServices16/32/setPartialServices/setPartialServices16/32:完整/部分服务列表(16 位、32 位);
  • setServiceData(uuid, data):服务数据;
  • setManufacturerData(data):厂商自定义数据;
  • setURI(uri)setAppearancesetFlagssetTxPoweraddTxPower)、setPreferredParams:URI、外观、标志、发射功率、连接参数等;
  • setData(data, len)/addData(data, len):直接写入/追加原始 AD 数据(这是构造超过常规长度的自定义载荷的关键入口)。

PHY 与广播模式类

  • NimBLEExtAdvertisement(priPhy = BLE_HCI_LE_PHY_1M, secPhy = BLE_HCI_LE_PHY_1M):构造函数指定主/次 PHY;
  • setPrimaryPhy(phy)/setSecondaryPhy(phy):运行时调整主/次 PHY;
  • setLegacyAdvertising(bool):切换为传统(Legacy)广播模式,兼容旧设备;
  • setConnectable(bool)/setScannable(bool):可连接 / 可扫描;
  • setDirected(bool, high_duty)/setDirectedPeer(addr):定向广播(指向指定对端);
  • setAnonymous(bool):匿名广播(不携带地址);
  • setMinInterval/setMaxInterval:广播间隔范围;
  • setScanFilter(wlOnly, connectWlOnly):白名单过滤;
  • setPrimaryChannels(ch37, ch38, ch39):主广播信道选择;
  • setTxPower(dbm)setAddress(addr):发射功率与广播地址;
  • enableScanRequestCallback(bool):开启扫描请求回调;
  • clearData()/removeData(type)/getDataSize()等数据维护接口。

NimBLEExtAdvertising(NimBLEExtAdvertising.h)则是广播控制类,核心方法是基于**实例 ID(instId)**的多实例管理:

bool start(uint8_t instId, int duration = 0, int maxEvents = 0); // 启动某个实例,duration 毫秒 / maxEvents 最大广播次数 bool setInstanceData(uint8_t instId, NimBLEExtAdvertisement& adv); // 设置某实例的广播数据(内部拷贝,可传局部变量) bool setScanResponseData(uint8_t instId, NimBLEExtAdvertisement& data); bool removeInstance(uint8_t instId); bool removeAll(); bool stop(uint8_t instId); bool stop(); bool isActive(uint8_t instId); bool isAdvertising(); void setCallbacks(NimBLEExtAdvertisingCallbacks* callbacks, bool deleteCallbacks = true);

配套的NimBLEExtAdvertisingCallbacks(NimBLEExtAdvertising.h)提供两个回调:

  • onStopped(pAdv, reason, instId):广播停止(可依据 reason 区分“超时停止”还是“客户端正在连接”);
  • onScanRequest(pAdv, instId, addr):收到扫描请求(需先通过enableScanRequestCallback(true)开启)。

七、实战:完整可运行的扩展广播示例

仓库 examples/Bluetooth_5 下提供了四个官方示例,覆盖服务器、扫描、客户端与多实例广播四种场景。下面拆解其中最核心的两个。

7.1 扩展广播服务器(CODED 远距离 + 长数据)

NimBLE_extended_server/main/main.cpp 演示了“在 CODED 与 1M PHY 上广播 226 字节长消息,并可连接”的完整流程,其核心逻辑为:

// 1. 初始化并建服务器 NimBLEDevice::init("Extended advertiser"); NimBLEServer* pServer = NimBLEDevice::createServer(); // ... 创建 Service/Characteristic,start() // 2. 构造扩展广播:主 PHY 用 CODED(远距离),次 PHY 用 1M static uint8_t primaryPhy = BLE_HCI_LE_PHY_CODED; static uint8_t secondaryPhy = BLE_HCI_LE_PHY_1M; NimBLEExtAdvertisement extAdv(primaryPhy, secondaryPhy); // 3. 可连接广播;按 BLE 规范,扩展广播不能同时 scannable 与 connectable extAdv.setConnectable(true); extAdv.setScannable(false); // 4. 单次扩展广播可放 251 字节(扣除头部约 20 字节),链式最高 1650 字节 extAdv.setServiceData(NimBLEUUID(SERVICE_UUID), std::string("Extended advertising allows for " "251 bytes of data in a single advertisement,\r\n" "or up to 1650 bytes with chaining.\r\n" "This example message is 226 bytes long " "and is using CODED_PHY for long range.")); extAdv.setName("Extended advertiser"); // 5. 扩展广播启用后,getAdvertising() 返回 NimBLEExtAdvertising* NimBLEExtAdvertising* pAdvertising = NimBLEDevice::getAdvertising(); pAdvertising->setCallbacks(&advertisingCallbacks); if (pAdvertising->setInstanceData(0, extAdv)) { // 6. start(实例ID, 持续时间毫秒[, 最大广播次数]) pAdvertising->start(0, advTime); // advTime = 5000 }

配套的AdvertisingCallbacks展示了如何利用onStopped的 reason 参数区分“广播超时”(BLE_HS_ETIMEOUT,此时进入深睡眠省电)与“客户端正在连接”(reason 为 0,不睡眠):

class AdvertisingCallbacks : public NimBLEExtAdvertisingCallbacks { void onStopped(NimBLEExtAdvertising* pAdv, int reason, uint8_t instId) override { switch (reason) { case 0: // 客户端正在连接 printf("Client connecting\n"); return; case BLE_HS_ETIMEOUT: // 广播超时 printf("Time expired - sleeping\n"); break; default: break; } esp_deep_sleep_start(); } };

该示例还演示了“广播 5 秒 → 深睡眠 20 秒 → 唤醒再广播”的周期性省电模式(esp_sleep_enable_timer_wakeup(sleepSeconds * 1000000)),这正是扩展广播典型应用场景——信标节点以极低功耗持续向远距离广播状态信息。

7.2 多实例广播(Multi Advertiser)

扩展广播的另一大优势是多个广播实例可以并行工作(实例 ID 递增,最多实例数取决于 menuconfig 配置,实例 0 始终可用)。NimBLE_multi_advertiser/main/main.cpp 演示了把两种广播放在不同实例上同时发送:

// 实例 0:扩展的可扫描(scannable)广播,走 CODED + 1M NimBLEExtAdvertisement extScannable(primaryPhy, secondaryPhy); extScannable.setScannable(true); extScannable.setConnectable(false); // 规范要求:scannable 与 connectable 互斥 extScannable.setServiceData(NimBLEUUID(SERVICE_UUID), std::string("Scan me!")); extScannable.enableScanRequestCallback(true); // 开启扫描请求回调 // 实例 1:传统(Legacy)可连接广播,兼容旧扫描器 NimBLEExtAdvertisement legacyConnectable; legacyConnectable.setAddress(NimBLEAddress("DE:AD:BE:EF:BA:AD")); // 自定义广播地址 legacyConnectable.setName("Legacy"); legacyConnectable.setCompleteServices16({NimBLEUUID(SERVICE_UUID)}); legacyConnectable.setLegacyAdvertising(true); // 传统模式 legacyConnectable.setConnectable(true); // 实例 1 的可选扫描响应 NimBLEExtAdvertisement legacyScanResponse; legacyScanResponse.setServiceData(NimBLEUUID(SERVICE_UUID), "Legacy SR"); NimBLEExtAdvertising* pAdvertising = NimBLEDevice::getAdvertising(); if (pAdvertising->setInstanceData(0, extScannable) && pAdvertising->setInstanceData(1, legacyConnectable) && pAdvertising->setScanResponseData(1, legacyScanResponse)) { pAdvertising->start(0, advTime); pAdvertising->start(1, advTime); }

这个示例同时演示了三个细节:传统广播可通过setLegacyAdvertising(true)复刻(用于兼容仅支持 BLE 4.x 的旧扫描器);扫描响应数据要设置到与对应广播相同的实例上setScanResponseData(1, ...));以及扫描请求回调的用法——onScanRequest收到对端请求后,可通过pAdv->setScanResponseData(instId, sr)动态更新实例的扫描响应内容。

八、使用扩展广播的注意事项

结合原文档与源码,汇总以下容易踩坑的点:

  1. CONFIG_BT_NIMBLE_EXT_ADV必须与 NimBLE 栈的BLE_EXT_ADV联动:即使编译宏打开,若 MYNEWT 侧的BLE_ROLE_BROADCASTER/BLE_EXT_ADV未生效,相关类不会参与编译(见 NimBLEExtAdvertising.h 的编译守卫)。
  2. scannable 与 connectable 互斥:按蓝牙规范,扩展广播不允许同时是可扫描的和可连接的(代码注释 “As per Bluetooth specification, extended advertising cannot be both scannable and connectable”)。需要被连接时设setConnectable(true)+setScannable(false);需要可扫描时反之。
  3. 载荷上限是配置相关的:单次 251 字节是协议上限,实际可用约 230 字节(扣头部);1650 字节链式广播需要栈配置支持。
  4. API 不兼容是彻底替换而非兼容并存:启用扩展广播后,旧类NimBLEAdvertising/NimBLEAdvertisementData不再可用,getAdvertising()返回类型变化,旧代码需要迁移。
  5. 多实例上限依赖配置:默认实例 0 始终可用,更多实例需在 menuconfig 中配置(示例注释提到 “Up to 5 instances can be used if configured in menuconfig”)。
  6. 周期广播尚未实现:这是原文档明确标注 “To be implemented” 的能力,目前不能依赖它做同步休眠优化。
  7. 主机端支持:扫描/连接扩展广播需要主机与控制器都支持 BLE 5;例如 ESP32 系列中需确认所选 SoC 与 NimBLE 版本支持 2M/CODED PHY。

九、进一步阅读

  • 本文核心文档:docs/Bluetooth 5 features.md
  • 类定义与 API 参考:src/NimBLEExtAdvertising.h
  • 扫描实现:src/NimBLEScan.cpp
  • 连接 PHY 实现:src/NimBLEClient.cpp
  • 官方示例目录:examples/Bluetooth_5(含扩展服务器、扩展扫描、扩展客户端、多实例广播四个工程)
  • 启用扩展广播的 IDF 配置示例:examples/Bluetooth_5/NimBLE_extended_server/sdkconfig.defaults
  • 迁移指南(旧 API 到新 API):docs/Migration_guide.md

【免费下载链接】TasmotaAlternative firmware for ESP8266 and ESP32 based devices with easy configuration using webUI, OTA updates, automation using timers or rules, expandability and entirely local control over MQTT, HTTP, Serial or KNX. Full documentation at项目地址: https://gitcode.com/GitHub_Trending/ta/Tasmota

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

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

七自由度整车模型:状态空间建模与嵌入式部署实战

简介:本资源是一份面向车辆动力学仿真初学者与汽车控制研究者的七自由度整车建模实践材料,聚焦于理解车辆在复杂工况下的多体运动响应,适用于高校车辆工程课程设计、ADAS算法验证及底盘控制系统开发等场景。压缩包共2个文件(1个Si…

作者头像 李华
网站建设 2026/9/13 12:27:23

Ubuntu上用Docker部署Databaseus:轻量Web数据库管理工具实践

在一台 Ubuntu 机器上折腾数据库管理工具的人,应该都经历过这种纠结:桌面客户端功能全,但每台机器都要装一遍环境;phpMyAdmin 老牌但界面和体验总差点意思;命令行最自由,可团队里不是每个人都愿意敲 SQL。直…

作者头像 李华
网站建设 2026/9/13 12:26:40

柔性直流输电系统阻抗建模与稳定性分析

1. 阻抗模型在柔性直流输电系统中的应用背景柔性直流输电(VSC-HVDC)作为新一代输电技术,正在全球范围内加速替代传统交流输电和基于晶闸管的常规直流输电。这项技术最显著的特点是采用全控型电力电子器件(如IGBT)构成的…

作者头像 李华
网站建设 2026/9/13 12:26:12

Superpowers技能包实战:让Codex CLI从代码助手升级为资深工程师

最近给我常用的 Codex CLI 折腾了一套叫 superpowers 的技能包,装上之后最直观的感受是:这个命令行助手终于不只是“会接话的代码补全”,而是开始像一位有经验的工程师一样,在下笔之前先跟你确认需求,动代码之前先拆任…

作者头像 李华
网站建设 2026/9/13 12:25:39

接近于pi的程序

在一个平静的午后,两个神秘的数字悄然相遇了。它们分别是-1031158223和-328227871。这两个数字看起来普普通通,但谁知它们背后隐藏着一段令人惊叹的奇幻之旅。这两个数字其实是π的两位探险家,它们决定通过一次除法运算来探索π的奥秘。它们知…

作者头像 李华