news 2026/10/4 12:50:58

ESP32 LED光通信实战:从零搭建PacketLED协议

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
ESP32 LED光通信实战:从零搭建PacketLED协议

1. 项目缘起与整体设计思路

1.1 为什么想到让两颗 ESP32 用 LED 互相“说话”

先说清楚这个项目到底在干什么。PacketLED 的核心思路非常朴素:两颗 ESP32 开发板,各自接一颗 LED,不接任何额外的通信模块,不连 WiFi,不走蓝牙,也不拉 UART 线,就让这两颗芯片通过“一颗 LED 发光、另一颗 LED 接收光”的方式完成数据传递。发送端控制 LED 的亮灭节奏,接收端用 ADC 采样这颗 LED 两端的电压变化,把光信号还原成电信号,再解码成字节,最终实现两块板子之间的单向或双向通信。

这个想法听起来像是“玩具”,但它背后涉及的知识点非常密集:GPIO 输出控制、ADC 采样与滤波、时序协议设计、帧同步、误码处理、中断与任务调度。对于已经玩过点灯、串口打印、简单传感器读取的嵌入式爱好者来说,这是一个非常好的进阶练手项目。它逼着你从“调库”走向“自己定义协议、自己处理噪声、自己解决同步问题”。

我最初动这个念头,是因为手头有两块 ESP32 开发板,一块是经典的 ESP32-WROOM-32,另一块是 ESP32-C3。平时做项目总是习惯性地上 WiFi 或蓝牙,但有些场景下无线协议栈太重,功耗也高,而红外、超声波又需要额外器件。LED 本身就是最便宜的光电器件,正向导通时发光,反向或弱光条件下它其实也能当光敏元件用。很多老玩家都知道 LED 可以当光电二极管使,只是响应速度和灵敏度不如专用器件,但对于低速数据通信来说完全够用。

这个项目的目标读者很明确:已经会 Arduino 基础操作、能点亮 LED、能读 ADC、想进一步理解“通信协议到底怎么从零搭起来”的人。如果你连pinMode和analogRead都没用过,建议先把这两件事跑通再来看。整个项目不需要额外采购任何模块,只需要两块 ESP32、两颗 LED、若干电阻和杜邦线,成本极低,但学到的东西比买一个现成通信模块多得多。

1.2 方案选型:为什么不用红外接收头、不用光敏电阻

有人会问,既然要传光信号,为什么不直接用红外发射管加一体化接收头?原因有三点。第一,红外接收头内部自带解调电路,通常只认 38kHz 载波,你没法自由定义任意低速波形,协议被限死了。第二,手头不一定有红外对管,但 LED 人人都有。第三,用 LED 当接收器,你能完整地看到“光→电流→电压→ADC 数值”这条链路,对理解模拟前端非常有帮助。

那为什么不用光敏电阻?光敏电阻响应慢,暗电阻和亮电阻变化虽然大,但恢复时间长,做几百赫兹以上的调制基本没戏。LED 结电容小,反向偏置或零偏置下受光照产生的光电流虽然微弱,但响应速度可以到微秒级,适合做调制通信。当然,直接用 LED 做接收,信号幅度很小,通常只有几十毫伏到几百毫伏,必须借助 ESP32 内部 ADC 或者外部运放来放大。ESP32 的 ADC 虽然线性度一般,但做这种低速、短距离通信,配合软件滤波和阈值判决,完全能跑起来。

另一个关键选型是通信距离。这个项目不适合做几米以上的传输,最佳距离是两颗 LED 面对面、间距 1 到 5 厘米。距离越近,接收端 ADC 读数变化越明显,误码率越低。如果你想拉远,必须加透镜或者用高亮 LED 加大驱动电流,但那样接收端就需要跨阻放大器,复杂度上升。我的建议是先把近距离跑通,再考虑优化。

1.3 整体架构:发送端、接收端与协议分层

整个系统分成三层来理解。最底层是物理层:发送端 GPIO 驱动 LED,接收端 ADC 采样 LED 两端电压。中间是链路层:定义“0”和“1”的波形表示、帧起始标志、帧结束标志、校验方式。最上层是应用层:把要传的字节组装成帧,接收端解析后打印到串口或者控制板载 LED 作为指示。

发送端我用的是 ESP32 的 GPIO 口直接驱动 LED,串联一个 220 欧姆限流电阻。为什么是 220 欧姆?ESP32 GPIO 高电平输出约 3.3V,LED 正向压降约 2.0V(红色),那么电阻上的压降是 1.3V,电流约 5.9mA。这个电流足够让 LED 明显发光,又不会超过 ESP32 单个 GPIO 的推荐拉电流(约 12mA 持续)。如果你用蓝色或白色 LED,正向压降约 3.0V,220 欧姆下电流只有 1.4mA,亮度会偏暗,建议换成 100 欧姆。但电阻太小又可能超过 GPIO 电流上限,所以红色或黄色 LED 是最稳妥的选择。

接收端我用的是另一颗 LED,反向并联或者直接并联在 ADC 输入脚和地之间。这里有个细节:LED 正向接在 ADC 脚上时,发送端的光会让接收 LED 产生光生电压,但这个电压方向与 LED 正向导通方向一致,ADC 能读到正值。实际测试中,接收 LED 两端不加偏置时,光生电压只有几十毫伏,ESP32 ADC 在 0 到 100mV 区间噪声很大。所以我后来改成给接收 LED 加一个反向偏置,也就是阴极接 3.3V,阳极接 ADC 脚并通过高阻值电阻接地。这样光照产生的光电流会在电阻上形成电压,ADC 读数变化更明显。这个偏置方式下文会详细讲。

协议层我设计得非常简单:用脉宽区分“0”和“1”。短脉冲代表 0,长脉冲代表 1,帧与帧之间用更长的间隔作为起始标志。接收端用 ADC 连续采样,记录超过阈值的持续时间,再根据持续时间判决比特值。这个方案的好处是不需要精确的时钟同步,对采样率要求也不高,ESP32 的 ADC 在 Arduino 环境下跑 10kHz 采样率很轻松。

2. 核心细节解析与实操要点

2.1 LED 当光接收器:原理与偏置电路设计

LED 本质上是一个 PN 结二极管。当它正向导通时会发光,这是电生光。反过来,当光子打到 PN 结上时,会在耗尽区激发出电子空穴对,如果此时 PN 结处于反向偏置或者零偏置,这些载流子会被内建电场分离,形成光电流。这就是 LED 可以作为光接收器的物理基础。只不过 LED 的结面积大、结电容大,响应速度不如专用光电二极管,但做几十 kHz 以下的通信绰绰有余。

关键问题是:光电流非常小。一颗普通 5mm 红色 LED,在 1 厘米距离、发送端 6mA 驱动下,接收端短路光电流大约在几十微安到几百微安量级。如果你直接把这个电流灌进一个电阻,比如 1 兆欧,那么电压就是几十毫伏到几百毫伏。ESP32 的 ADC 量程是 0 到 3.3V,12 位分辨率下 1LSB 约 0.8mV,理论上能分辨,但实际噪声可能有几个 LSB,所以信号必须足够大。

我的做法是给接收 LED 加反向偏置。具体接法:接收 LED 的阴极接 ESP32 的 3.3V,阳极接 ADC 输入脚(比如 GPIO34),同时阳极通过一个 1 兆欧电阻接地。这样 LED 处于反向偏置状态,光照产生的光电流从 3.3V 经过 LED 流向电阻,在电阻上形成电压。无光时,ADC 读数接近 0;有光时,ADC 读数上升。实测在 1 厘米距离下,有光和无光的 ADC 差值可以达到 200 到 500 个计数(12 位下),信噪比足够判决。

注意:ESP32 的 ADC1 通道(GPIO32 到 GPIO39)在 WiFi 开启时也能用,但 ADC2 通道与 WiFi 冲突。这个项目虽然不用 WiFi,但养成用 ADC1 的习惯没坏处。另外,ESP32-C3 的 ADC 引脚分布不同,GPIO0 到 GPIO4 是 ADC1,接线前务必查一下自己板子的引脚图。

电阻值的选择需要权衡。电阻越大,光电流产生的电压越大,但 RC 时间常数也越大,响应变慢。1 兆欧配合 LED 结电容(约 100pF)和布线电容,时间常数大约 100 微秒,对应带宽约 1.6kHz。如果你把通信速率定在 100 到 500 比特每秒,这个带宽完全够用。如果想提速到几千比特每秒,电阻要降到 100 千欧左右,但信号幅度也会同比下降,需要加运放放大。

2.2 发送端驱动电路:限流、亮度与调制深度

发送端看起来简单,就是 GPIO 推 LED,但有几个坑我踩过。第一,ESP32 的 GPIO 在高电平时的驱动能力不是对称的,拉电流和灌电流能力不同。数据手册里单个 GPIO 持续拉电流推荐不超过 12mA,瞬间可以到 40mA。如果你用 100 欧姆电阻接红色 LED,电流约 13mA,短时间没问题,但长时间跑可能发热。我最终用 220 欧姆,电流约 6mA,亮度足够,板子也不烫。

第二,LED 的调制深度直接影响接收端判决。所谓调制深度,就是“亮”和“灭”两种状态下接收端 ADC 读数的差值。如果你用 PWM 调光让 LED 一直半亮,然后在此基础上叠加调制,接收端看到的交流分量就小,判决困难。所以这个项目里发送端 LED 必须是完全亮和完全灭两种状态,不能有基础亮度。代码里就是digitalWrite(pin, HIGH)和digitalWrite(pin, LOW),不要用analogWrite。

第三,发送端的 LED 和接收端的 LED 要尽量对准。LED 的发光角度通常有 20 到 60 度,接收 LED 的感光面也有方向性。我试过把两颗 LED 平放在桌面上,间距 2 厘米,接收信号很弱;后来把两颗 LED 的头部正对,间距 1 厘米,信号立刻强了很多。如果你要做成固定装置,可以用热缩管把两颗 LED 套在一起,中间留 5 毫米间隙,这样抗环境光干扰能力也更强。

发送端的代码逻辑很简单:先把要发送的字节拆成比特,每个比特根据 0 或 1 输出不同宽度的脉冲,比特之间插入固定间隔。比如我用 200 微秒脉冲代表 0,600 微秒脉冲代表 1,比特间隔 400 微秒。这样接收端只需要测量高电平持续时间,大于 400 微秒判为 1,小于 400 微秒判为 0。帧起始用一个 1200 微秒的长脉冲,帧结束用 800 微秒脉冲。这些时间参数不是固定的,你可以根据实际采样率和 LED 响应速度调整。

2.3 接收端 ADC 采样与软件滤波

接收端是整个项目最难的部分。ESP32 的 ADC 噪声大是出了名的,尤其是低电压区间。我一开始直接读analogRead,发现无光时读数在 0 到 80 之间跳,有光时在 200 到 350 之间跳,虽然能区分,但偶尔会有毛刺导致误判。后来我加了两级滤波:第一级是硬件 RC 低通,在 ADC 脚和地之间并一个 100nF 电容,截止频率约 1.6kHz,把高频噪声滤掉。第二级是软件滑动平均,连续采 8 个点取平均,等效采样率降到 1.25kHz,但信号干净很多。

采样率的选择需要计算。假设最短脉冲是 200 微秒,那么采样周期必须远小于 200 微秒,否则可能采不到这个脉冲。我设定采样周期 50 微秒,也就是 20kHz 采样率。但 ESP32 的analogRead在 Arduino 环境下单次转换约 100 微秒,达不到 20kHz。所以我改用adc1_get_raw或者analogReadMillis这类更底层的接口,或者直接用定时器触发采样。实际测试中,我用micros()做时间戳,循环里连续读 ADC,记录每次读数超过阈值的时间点,这样虽然采样间隔不均匀,但通过时间戳可以准确计算脉冲宽度。

阈值判决是另一个关键。固定阈值在环境光变化时会失效。比如白天室内光和晚上台灯下,无光时的 ADC 基线完全不同。我的做法是动态阈值:每次通信开始前,先让发送端 LED 熄灭 100 毫秒,接收端在这段时间内采集 100 个样本,取最大值作为“暗电平”,然后加上一个固定偏移量(比如 100)作为判决阈值。这样无论环境光怎么变,阈值都能自适应。如果环境光很强,暗电平本身就很高,信号可能被淹没,这时候就需要给接收 LED 加遮光罩,或者提高发送端驱动电流。

实操心得:ESP32 的 ADC 在 0 到 0.1V 区间非线性非常严重,读数基本不可用。所以接收 LED 的偏置电阻不要太大,要让有光时的电压落在 0.3V 到 2.5V 之间。如果发现读数总是很小,先检查 LED 是不是接反了,再检查电阻是不是太大。

3. 实操过程与核心环节实现

3.1 硬件连接清单与接线步骤

先列一下我用的物料。两块 ESP32 开发板,我用的是一块 ESP32-WROOM-32 和一块 ESP32-C3,其实两块一样的也行。两颗 5mm 红色 LED,正向压降约 2.0V。电阻若干:发送端用 220 欧姆,接收端偏置用 1 兆欧,滤波电容用 100nF。杜邦线若干,面包板一块。

发送端接线:ESP32 的 GPIO25 接 220 欧姆电阻一端,电阻另一端接发送 LED 的阳极,发送 LED 的阴极接 GND。GPIO25 是 DAC 引脚,但这里当普通 GPIO 用没问题。你也可以用 GPIO2,板载 LED 就在 GPIO2 上,但板载 LED 通常有电阻和驱动电路,亮度可能不够,建议外接。

接收端接线:接收 LED 的阴极接 3.3V,阳极接 GPIO34(ADC1 通道 6)。GPIO34 同时接 1 兆欧电阻一端,电阻另一端接 GND。GPIO34 再并一个 100nF 电容到 GND。注意 GPIO34 是输入专用引脚,没有内部上拉,正好适合做 ADC 输入。如果你用 ESP32-C3,ADC1 通道在 GPIO0 到 GPIO4,选 GPIO1 或 GPIO2 都行,但注意 GPIO2 可能影响启动模式,建议用 GPIO1。

两颗 LED 的摆放:把发送 LED 和接收 LED 的头部正对,间距 1 到 2 厘米。我用热缩管剪了一小段,把两颗 LED 套在一起,中间留 5 毫米空隙,这样环境光干扰小很多。如果你没有热缩管,用黑色电工胶带卷一个筒也行。

3.2 发送端代码实现与时间参数计算

发送端代码的核心是一个sendByte函数和一个sendFrame函数。先定义时间参数:

#define BIT_0_US 200 #define BIT_1_US 600 #define BIT_GAP_US 400 #define FRAME_START_US 1200 #define FRAME_END_US 800

为什么这么定?假设接收端采样周期 50 微秒,那么 200 微秒的脉冲会被采到 4 个点,600 微秒采到 12 个点,区分度足够。比特间隔 400 微秒是为了让接收端有时间复位判决状态,避免上一个脉冲的拖尾影响下一个。帧起始 1200 微秒比任何数据脉冲都长,接收端一旦检测到超过 1000 微秒的高电平,就知道新帧开始了。

发送一个字节的流程:先发帧起始脉冲,然后从最高位到最低位依次发送 8 个比特,每个比特根据值是 0 还是 1 输出对应宽度的脉冲,脉冲之间插入 400 微秒低电平。8 个比特发完后,发帧结束脉冲。如果要发多个字节,可以在帧结束前连续发,或者每帧只发一个字节。我为了简单,每帧只发一个字节,帧之间间隔 10 毫秒。

代码里用delayMicroseconds做精确延时。注意 ESP32 的delayMicroseconds在 Arduino 环境下精度还可以,但如果你开了 WiFi 或蓝牙,可能会有抖动。这个项目不需要无线功能,建议在代码里关掉 WiFi,WiFi.mode(WIFI_OFF),减少干扰。

void sendBit(int bit) { digitalWrite(LED_PIN, HIGH); if (bit == 0) { delayMicroseconds(BIT_0_US); } else { delayMicroseconds(BIT_1_US); } digitalWrite(LED_PIN, LOW); delayMicroseconds(BIT_GAP_US); } void sendByte(uint8_t data) { digitalWrite(LED_PIN, HIGH); delayMicroseconds(FRAME_START_US); digitalWrite(LED_PIN, LOW); delayMicroseconds(BIT_GAP_US); for (int i = 7; i >= 0; i--) { sendBit((data >> i) & 1); } digitalWrite(LED_PIN, HIGH); delayMicroseconds(FRAME_END_US); digitalWrite(LED_PIN, LOW); delayMicroseconds(10000); }

3.3 接收端代码实现与脉冲宽度测量

接收端的核心是持续采样 ADC,检测高电平脉冲的起始和结束,计算持续时间,然后判决比特。我用了一个状态机:状态 0 是等待帧起始,状态 1 是接收数据比特,状态 2 是等待帧结束。

采样循环里,我先读 ADC 值,和动态阈值比较。如果超过阈值且上一个状态是低电平,记录当前micros()作为脉冲起始时间。如果低于阈值且上一个状态是高电平,记录当前micros()作为脉冲结束时间,计算宽度。然后根据宽度判决:如果宽度在 1000 到 1400 微秒之间,认为是帧起始,清零比特计数器,进入接收状态。如果宽度在 500 到 700 微秒之间,判为 1。如果宽度在 150 到 250 微秒之间,判为 0。如果宽度在 700 到 900 微秒之间,认为是帧结束,把收到的字节打印出来。

动态阈值的计算:在 setup 里先让发送端不发数据,接收端采集 200 个样本,取最大值作为暗电平,然后阈值设为暗电平加 80。这个 80 是经验值,你可以根据实际信号幅度调整。如果信号很强,可以设 150;如果信号弱,设 50。

int readADC() { return analogRead(ADC_PIN); } void loop() { int val = readADC(); unsigned long now = micros(); if (val > threshold && lastState == LOW) { pulseStart = now; lastState = HIGH; } else if (val <= threshold && lastState == HIGH) { unsigned long width = now - pulseStart; lastState = LOW; if (width > 1000 && width < 1400) { bitCount = 0; currentByte = 0; receiving = true; } else if (receiving) { if (width > 500 && width < 700) { currentByte = (currentByte << 1) | 1; bitCount++; } else if (width > 150 && width < 250) { currentByte = (currentByte << 1) | 0; bitCount++; } else if (width > 700 && width < 900) { if (bitCount == 8) { Serial.print("Received: "); Serial.println(currentByte); } receiving = false; } } } }

这段代码在 ESP32-WROOM-32 上跑,接收端串口能稳定打印出发送端发来的字节。我测试连续发送 0x00 到 0xFF,接收端偶尔会丢一两个字节,但大部分都能正确收到。丢字节的原因主要是环境光突变或者采样时机不巧,后面可以通过加校验位和重传机制改善。

3.4 双向通信与帧校验的扩展思路

单向跑通后,你可以把两块板子都配上发送 LED 和接收 LED,实现双向通信。但双向有个问题:如果两边同时发送,会互相干扰。解决办法是定义主从角色,主机先发,从机收到后回复,主机收到回复后再发下一帧。这就是最简单的停等协议。

帧校验我建议加一个简单的校验和。在 8 个数据位后面再加 8 个校验位,校验位是数据位的按位取反,或者累加和。接收端收到数据后计算校验,如果不匹配就丢弃。这样虽然速率降低一半,但可靠性大幅提升。如果你追求速率,可以用 4 位数据加 4 位校验,或者用 CRC-8。

注意:双向通信时,两颗 LED 面对面,发送时自己的接收 LED 也会收到自己的光,造成自干扰。解决办法是发送时暂时关闭自己的接收采样,或者用方向性遮挡让发送 LED 的光主要照向对方而不是自己的接收 LED。我用热缩管套住两颗 LED 时,把发送和接收分开套,中间加一层黑色隔板,效果不错。

4. 常见问题与排查技巧实录

4.1 接收端读数始终为零或变化极小

这是最常见的问题。先检查接收 LED 的极性。反向偏置接法是阴极接 3.3V,阳极接 ADC 脚。如果你接反了,LED 正向导通,ADC 会读到接近 3.3V 的固定值,光照变化完全看不出来。用万用表二极管档测一下 LED 极性,长脚是阳极,短脚是阴极,或者看 LED 内部电极,小的是阳极。

如果极性没错,检查偏置电阻。1 兆欧电阻如果虚焊或者阻值不对,光电流没有路径,电压上不去。用万用表测电阻两端,应该是 1 兆欧左右。再检查 ADC 引脚是否选对,ESP32-WROOM-32 的 GPIO34 是 ADC1 通道 6,analogRead(34)应该能读到值。如果你用的是 ESP32-C3,ADC 引脚编号不同,analogRead(1)对应 GPIO1。

还有一个可能是发送端 LED 太暗。用手机摄像头对着发送 LED,发送数据时应该能看到闪烁。如果看不到,检查发送端电阻是不是太大,或者 GPIO 是不是没配置成输出模式。

4.2 误码率高,接收数据随机跳变

误码率高通常有三个原因:阈值不合适、采样率不够、环境光干扰。先调阈值。在接收端串口打印原始 ADC 值,观察无光时的基线范围和有光时的峰值范围。如果两个范围有重叠,说明信号太弱,需要缩短 LED 间距或者增大发送电流。如果范围分得很开但仍有误码,可能是采样率不够,漏掉了短脉冲。把采样循环里的analogRead换成更快的adc1_get_raw,或者降低通信速率,把 BIT_0_US 从 200 改成 400。

环境光干扰也很常见。日光灯有 100Hz 闪烁,会在 ADC 读数上叠加周期性波动。解决办法是给接收 LED 加遮光罩,或者在软件里做差分:连续采两个点,一个在发送端 LED 灭时采,一个在亮时采,用差值判决。但这样需要发送端配合,实现起来复杂。最简单的还是物理遮光。

4.3 通信距离短,稍微拉远就收不到

LED 光强随距离平方衰减。1 厘米到 2 厘米,信号强度降到四分之一。如果你需要 5 厘米以上,要么加大发送电流,要么用高亮 LED,要么加透镜。加大电流受限于 GPIO 驱动能力,可以用三极管或者 MOSFET 驱动 LED,电流可以到 100mA 以上。但接收端光电流也会同比增大,偏置电阻要相应减小,否则 ADC 会饱和。

另一个技巧是用脉冲驱动而不是直流驱动。发送端 LED 在极短时间内通过较大电流,平均电流不大,但瞬间光强很高。比如用 100 欧姆电阻,5V 供电(如果板子有 5V 引脚),瞬间电流 30mA,脉宽 200 微秒,占空比很低,平均电流只有几毫安。这样接收端信号强,发送端也不会过热。但注意 ESP32 GPIO 不能直接接 5V,需要用三极管开关。

4.4 常见问题速查表

现象可能原因排查方法解决措施
ADC 读数始终为 0接收 LED 极性接反万用表测 LED 极性阴极接 3.3V,阳极接 ADC
ADC 读数始终为 3.3V接收 LED 正向导通检查是否阴极接 GND改为反向偏置接法
无光和有光读数无差异偏置电阻虚焊或阻值错误万用表测电阻重新焊接 1 兆欧电阻
误码率高阈值不合适串口打印原始 ADC 值动态阈值,暗电平加 80
短脉冲丢失采样率不够测量循环周期降低通信速率或换快速 ADC 接口
日光灯下误码环境光 100Hz 干扰关灯测试加遮光罩或软件差分
距离稍远就收不到光强衰减缩短间距测试加大驱动电流或加透镜
双向通信自干扰自己的发送 LED 照到自己的接收 LED遮挡测试加隔板或分时收发

4.5 独家避坑技巧与经验总结

第一个技巧:用板载 LED 做快速验证。很多 ESP32 开发板 GPIO2 上有一颗板载 LED,你可以先用它做发送端,不用外接任何元件,快速验证代码逻辑。接收端还是用外接 LED,因为板载 LED 通常没有反向偏置电路,当接收器不好用。

第二个技巧:串口绘图器看波形。Arduino IDE 的串口绘图器可以把 ADC 值实时画出来,你一眼就能看出脉冲形状、噪声幅度、阈值位置。调阈值的时候,把阈值也打印出来,画在同一张图上,非常直观。

第三个技巧:先慢后快。一开始把 BIT_0_US 设成 1000,BIT_1_US 设成 3000,BIT_GAP_US 设成 2000,这样接收端有充足时间采样,几乎不会误码。跑通后再逐步缩短时间参数,找到稳定通信的极限速率。我实测在 1 厘米距离、1 兆欧偏置下,BIT_0_US 可以降到 150 微秒,对应约 1.6kbps 的速率,再快就误码了。

第四个技巧:加一个起始同步头。除了帧起始脉冲,我还在帧起始前加了 5 个 0 比特作为同步头,让接收端的动态阈值有时间收敛。同步头不参与数据,只用于训练。这个技巧在环境光缓慢变化时特别有用。

第五个技巧:用中断代替轮询。如果你觉得轮询采样占用 CPU 太多,可以用 ESP32 的定时器中断,每 50 微秒触发一次 ADC 采样,在中断里做状态机。但注意中断里不要做Serial.print,把数据存到缓冲区,在主循环里打印。这样采样更均匀,误码率更低。

这个项目我断断续续玩了两周,从最初的点亮 LED 到稳定传输字节,中间踩了不少坑。最深的体会是:通信协议不是凭空设计出来的,而是根据物理层的实际特性一点点磨出来的。LED 的响应速度、ADC 的噪声、环境光的干扰,这些因素共同决定了你能用的时间参数和判决策略。把 PacketLED 跑通之后,再看 UART、I2C、SPI 这些标准协议,你会对它们为什么那样设计有更具体的理解。后续如果你想继续折腾,可以试试用两颗 RGB LED 做三通道并行通信,或者用红外 LED 拉远距离,甚至用太阳能电池板当接收器,玩法很多,核心原理都是相通的。

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

自动化测试与运维实战:pytest、Ansible与UDS诊断技术解析

5月这个节点很有意思&#xff1a;工控展会扎堆、测试框架更新迭代、运维自动化方案集中落地。我在自动化圈子里泡了十来年&#xff0c;5月前后往往是项目验收和半年规划的过渡期&#xff0c;大家在群里聊得最多的也不是“哪家又发了新品”&#xff0c;而是一些实操层面的东西—…

作者头像 李华
网站建设 2026/10/4 12:46:28

Codex WebFetch 403 排查指南:四层链路定位与解决

1. 先别急着改配置&#xff1a;403 到底卡在哪一层Codex 的 WebFetch 报 403&#xff0c;是这两年被问得最多的一类问题。很多人一看到 403 就开始翻配置文件、换模型、重装 CLI&#xff0c;折腾一整天还是红的。问题出在哪&#xff1f;出在大家把 403 当成一个"错误"…

作者头像 李华
网站建设 2026/10/4 12:46:20

Word粘贴到WangEditor格式丢失?无格式丢失粘贴的HTML清洗实战

如果你经常需要把 Word 文档里的内容复制到网页编辑器中&#xff0c;一定经历过这种崩溃&#xff1a;标题层级变没了、加粗不生效、列表缩进成了纯文本&#xff0c;或者直接满屏style"mso-...的混乱 HTML。做前端这些年&#xff0c;我几乎每隔一阵就会碰上“Word 粘贴到 W…

作者头像 李华
网站建设 2026/10/4 12:43:09

小吃培训退费与调整怎么看:长沙曾食坊小吃培训走访

本篇要点&#xff1a;退费先看书面约定&#xff1b;课程调整怎么提&#xff1b;规则落到纸面更稳。报名时很少有人把"万一要退或要调"想在前&#xff0c;等真遇到才发现没写清。退费与调整不是用来规避什么&#xff0c;而是把可能的变动提前定好。本文从走访角度说清…

作者头像 李华
网站建设 2026/10/4 12:41:21

Cursor插件系统深度解析:harness沙盒与agent执行契约

1. “plugins”不是功能菜单&#xff0c;而是AI编程环境的神经突触你打开Cursor&#xff0c;点开Settings → Extensions&#xff0c;看到一堆“Install”按钮&#xff0c;下意识以为这是个和VS Code一样的插件市场——错了。这里的plugins根本不是传统意义上的扩展程序&#x…

作者头像 李华
网站建设 2026/10/4 12:40:46

Cosmius AI:小龙虾OpenClaw在电商领域的应用场景

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

作者头像 李华