传感器 · 红外避障 · LM393
红外避障模块 STM32F103 接线与代码
红外避障模块就是一个「有没有东西挡住」的开关——前方有障碍物把红外光反射回来,输出就拉低;没障碍就拉高。本页是 STM32F103C8T6 + 红外避障模块(LM393) + 0.96 OLED 的完整实机验证记录,含接线表、完整代码和 PlatformIO 工程下载,全部实测通过。最值得看的是「电位器调到临界会抖、代码怎么用 30ms 消抖把它稳住」和「上电那一刻为什么不能误触发计数」。
Quick Facts
- 模块:红外避障模块,LM393 + 红外对管
- 接线:OUT→PA0,VCC/GND,共 3 根线
- 输出:低电平=有障碍,高电平=无障碍
- 显示:0.96 寸 OLED,SSD1306,地址 0x3C
- 消抖:30ms 稳定判定
- 实测:挡手/白纸稳定触发,移开恢复
Verified
实机验证结果
本项目实测 下面的结果全部来自我们自己的实机测试,更新于 2026-08-17。
有无障碍实测
| 测试动作 | 预期 | 实测结果 |
|---|---|---|
| 上电,无障碍 | OLED 显示 CLEAR,串口 OUT:1,LED 灭 | CLEAR / OUT:1 / LED 灭 |
| 手或白纸挡在模块前方 | OLED 显示 OBSTACLE,串口 OUT:0,LED 亮,CNT +1 | OBSTACLE / OUT:0 / LED 亮 / CNT 每次 +1 |
| 移开障碍物 | 恢复 CLEAR,LED 灭 | 恢复 CLEAR / LED 灭 |
| 微调模块电位器 | 能改变触发距离 | 触发距离随电位器变化 |
| 阈值附近来回晃 | 不误触发(代码已消抖) | 30ms 消抖后稳定 |
串口输出(115200)
IR obstacle demo v1.0 starting...
OLED 0x3C: found
STATE: CLEAR | OUT: 1 | CNT: 0
STATE: OBSTACLE | OUT: 0 | CNT: 1
STATE: CLEAR | OUT: 1 | CNT: 1
STATE: OBSTACLE | OUT: 0 | CNT: 2
编译烧录记录
PlatformIO 上传输出 ** Programming Finished ** → ** Verified OK ** → [SUCCESS],烧录成功。Flash 占用 49.8%,RAM 占用 8.5%。
| 项目 | 本项目实际使用 |
|---|---|
| 主控 | STM32F103C8T6(BluePill) |
| 模块 | 红外避障模块(LM393 + 红外对管,OUT 数字输出) |
| 显示 | 0.96 寸 OLED,SSD1306,128×64,地址 0x3C |
| 状态灯 | BluePill 板载 LED,PC13(低电平点亮) |
| 开发工具 | VS Code + PlatformIO |
| 框架 | Arduino / STM32duino |
| 烧录 | ST-Link |
| 串口 | 115200 |
| 依赖库 | Adafruit SSD1306、Adafruit GFX Library |
What it does
这个模块干什么
红外避障模块就是给单片机装一个「有没有东西挡住」的开关。模块上一边发红外光、一边收红外光,前方有障碍物把光反射回来,输出就拉低;没障碍就拉高。它不告诉你障碍有多远,只回答「有 / 没有」。巡线小车、防撞检测、计数、到位检测都用这一类模块。
它靠障碍物把红外光反射回来才触发,所以反光差的物体(黑色、吸光材料)会难触发,镜面、浅色物体更容易触发。板子上的电位器调的是触发距离/灵敏度。
两个要点
| 要点 | 意思 |
|---|---|
| 不是测距 | 它只输出「过没过阈值」这一个二值结果,不知道障碍到底有多远 |
| 反射式 | 靠障碍物反射红外光触发,反光率直接影响能不能触发、触发多远 |
和测距传感器怎么选
| 红外避障模块 | 超声波 HC-SR04 | VL53L0X(ToF) | |
|---|---|---|---|
| 输出 | 高低电平(有 / 无) | 距离(时间差) | 距离(激光 ToF) |
| 能不能测距 | 不能,只判有无 | 能,厘米级 | 能,毫米级 |
| 成本 | 低 | 低 | 高 |
| 适合场景 | 巡线、防撞、计数、到位检测 | 粗略测距、倒车 | 精确测距 |
对比依据为三类器件的工作原理,由我们整理成表。资料参考
Wiring
接线
本项目实测 红外避障模块和 OLED 分开接,模块 3 根线 + OLED 4 根线。下面接法实机跑通。
红外避障模块
| 信号 | 模块丝印 | STM32F103C8T6 |
|---|---|---|
| 电源 | VCC | 3.3V(或 5V,见模块丝印) |
| 地 | GND | GND |
| 输出 | OUT | PA0 |
OLED
| 信号 | OLED | STM32F103C8T6 |
|---|---|---|
| 电源 | VCC / VDD | 3.3V |
| 地 | GND | GND |
| 时钟 | SCL / SCK | PB8 |
| 数据 | SDA | PB9 |
烧录与串口(不接也能跑,接上方便看数据)
| 用途 | 连接 | 说明 |
|---|---|---|
| 烧录 | ST-Link → SWD 四线(3.3V / GND / SWDIO / SWCLK) | 烧录用 |
| 串口 | PA9(TX) / PA10(RX) → CP2102 等 | 看串口数据(115200) |
1. OLED 的 I2C 用的是重映射引脚 PB8/PB9,不是默认的 PB7/PB6。代码里必须先
Wire.setSDA(PB9)、Wire.setSCL(PB8),再 Wire.begin(),顺序反了也读不到屏幕。2. 模块 OUT 要接 PA0,并且代码里用「低电平 = 有障碍」判断。不同厂家模块的输出极性可能不同,接上后先用手挡一下看串口 / OLED 对不对,反了再检查接线或改判断极性。
3. 必须共地。模块、OLED、STM32 的 GND 要接在一起,否则信号对不上。
| 要点 | 说明 |
|---|---|
| OUT 走内部上拉 | 代码用 INPUT_PULLUP 读 PA0,模块 LM393 输出开漏,板上通常也带上拉,不冲突 |
| 供电 3.3V 或 5V | 模块常见 3.3~5V,以手上模块丝印为准;本项目用 3.3V 与主控同电平 |
| PA13 / PA14 | 留给 ST-Link 调试,不要占用 |
| 丝印为准 | 不同厂家红外避障模块排针顺序可能不同(常见 VCC/GND/OUT),按丝印接,不要照着照片数孔位 |
Principle
工作原理
资料参考 这一节来自公开资料和常规工程实践,不是我们实测的结论,用来帮助理解。实测结果只在上面「实机验证结果」一节。
红外对管怎么判断有没有障碍
模块上一边是红外发射管,持续发出人眼看不见的红外光;另一边是红外接收管。无障碍时,发射出去的红外光直接散掉、收不回来,接收管信号弱;有障碍时,红外光被障碍物反射回来,接收管信号变强。
LM393 把「光强」变成「高低电平」
接收管出来的是一路随反射光强弱变化的模拟信号。模块上的 LM393 电压比较器把这一路信号和电位器设定的参考电压做比较——超过阈值就输出低电平,没超过就输出高电平。所以电位器调的就是这个参考电压,也就是触发距离 / 灵敏度。
| 项目 | 说明 | 来源 |
|---|---|---|
| 核心器件 | LM393 电压比较器 + 一对红外发射/接收管 | 公开资料 |
| 输出 | 数字电平:无障碍=高,有障碍=低 | 本项目实测 |
| 触发距离/灵敏度 | 板上电位器可调,调的是比较器参考电压 | 公开资料 |
| 供电 | 常见 3.3~5V,以模块丝印为准 | 公开资料 |
| 输出驱动能力 | 弱,直接驱动负载建议加三极管/MOS | 常规工程实践 |
为什么不是测距
这个模块只输出「过没过阈值」这一个二值结果,没有把信号强度换算成距离。要测具体距离,得用超声波(测回波时间差)或 ToF 激光测距(测光飞行时间)。
Program logic
程序逻辑
本项目实测 下面是对实际代码 src/main.cpp(v1.0)逻辑的说明,不展开语法,只讲「程序是怎么想的」。
整体是一套「读电平 → 消抖 → 更新状态 → 输出」的状态机
主循环 loop() 里只做三件事:读模块电平并消抖、有变化才刷新串口、有变化才刷新 OLED。没有变化时什么都不做,CPU 几乎全空转。
| 步骤 | 做什么 | 对应代码 |
|---|---|---|
| 1. 读电平 | 读 PA0,低电平 = 有障碍 | readIrObstacle() → digitalRead(PA0) == LOW |
| 2. 消抖 | 电平变化只记时刻,稳定 30ms 才更新状态 | updateIrState() |
| 3. 计触发 | 无障碍 → 有障碍,计一次 triggerCount | 状态翻转时 g_triggerCount++ |
| 4. 状态灯 | 有障碍点灯(PC13 低电平),无障碍熄灭 | digitalWrite(LED_PIN, ...) |
| 5. 输出 | 只在状态变化时(dirty 标记)刷新串口和 OLED | g_serialDirty / g_oledDirty |
为什么 30ms 消抖必须做
LM393 在检测阈值附近会来回跳(障碍物刚好卡在触发距离上时,光强就在阈值上下抖),OUT 会快速抖动。如果每次跳变都当一次状态变化,串口会刷屏、计数会乱涨。代码的做法是:电平一变化先记下时刻、不更新状态,等它稳定 30ms 才认这次变化。
上电那一刻为什么不能误触发计数
如果上电时把初始状态硬编码成「无障碍」,而实际模块前方正好有障碍,那么第一次读到的电平会和初始值不同,就会被当成一次「无障碍 → 有障碍」的触发,计数凭空 +1。代码的处理是:上电时直接以真实电平作为初始状态,不把它当成一次触发。
输出为什么「有变化才发」
串口和 OLED 刷新都靠两个 dirty 标记:状态变了才置位,主循环里看到标记才输出、输出完清掉。这样障碍物一直在、或一直不在的时候,串口不会一直刷、OLED 不会一直整屏重写,留出时间给别的任务。
Code
完整代码
本项目实测 下面是实际烧录进板子、实机测试通过的那一份(v1.0),一字未改。环境:VS Code + PlatformIO + Arduino(STM32duino)。
关键处说明
1. 把 I2C 指到 PB8 / PB9。STM32duino 默认 I2C 引脚是 PB7/PB6,必须手动指到重映射的 PB8/PB9。setSDA/setSCL 必须在 begin() 之前调用。本项目 I2C 用 100kHz。
2. 模块 OUT 用「低电平 = 有障碍」判断。PA0 开内部上拉(INPUT_PULLUP),读到的电平就是模块开漏输出的结果。
3. 30ms 消抖状态机。LM393 在阈值附近会抖,电平变化只记时刻、不更新状态,稳定 30ms 才认。这是本页「常见问题」里「阈值附近乱跳」的解法。
4. 上电以真实电平作初始状态。避免上电那一瞬间因为初始值不对而误触发一次计数。
5. 板载 LED 低电平点亮。PC13 在 BluePill 上是低电平点亮,有障碍时写 LOW、无障碍写 HIGH。
#include <Arduino.h>
#include <Wire.h>
#include <Adafruit_GFX.h>
#include <Adafruit_SSD1306.h>
#define FIRMWARE_VERSION "v1.0"
// =========================
// GPIO 配置
// =========================
// 红外避障模块 OUT -> PA0(数字输入,内部上拉)
// 模块的 LM393 输出是开漏,板上通常已有上拉;这里再开内部上拉更稳。
// 板载 LED -> PC13(bluepill 板载 LED,低电平点亮)
constexpr uint8_t IR_OUT_PIN = PA0;
constexpr uint8_t LED_PIN = PC13;
// OLED 使用 I2C1 重映射引脚:SDA=PB9 / SCL=PB8(默认 PB7/PB6 弃用)
constexpr uint8_t I2C_SDA_PIN = PB9;
constexpr uint8_t I2C_SCL_PIN = PB8;
constexpr uint8_t OLED_ADDRESS = 0x3C;
constexpr uint8_t SCREEN_WIDTH = 128;
constexpr uint8_t SCREEN_HEIGHT = 64;
constexpr int OLED_RESET = -1;
// 消抖:LM393 在检测阈值附近会抖动,要求电平稳定 30ms 才更新状态
constexpr uint32_t DEBOUNCE_MS = 30UL;
Adafruit_SSD1306 display(SCREEN_WIDTH, SCREEN_HEIGHT, &Wire, OLED_RESET);
// 当前状态
bool g_obstacle = false; // true = 检测到障碍(OUT 低电平)
bool g_hasOled = false; // OLED 是否初始化成功
uint32_t g_triggerCount = 0; // 触发次数(无障碍 -> 有障碍)
bool g_oledDirty = false; // OLED 需要重绘
bool g_serialDirty = false; // 串口需要输出(有变化)
// 去抖用:上次采到的原始电平与变化时刻
static bool g_rawState = false;
static uint32_t g_lastChangeMs = 0;
// =========================
// 读取
// =========================
// 模块 OUT 引脚:低电平 = 有障碍,高电平 = 无障碍
static bool readIrObstacle() {
return digitalRead(IR_OUT_PIN) == LOW;
}
// =========================
// 状态机(带消抖)
// =========================
static void updateIrState() {
const bool raw = readIrObstacle();
// 电平抖动阶段:只记变化时刻,不更新状态
if (raw != g_rawState) {
g_rawState = raw;
g_lastChangeMs = millis();
return;
}
// 稳定达到 30ms 才真正更新状态
if (millis() - g_lastChangeMs >= DEBOUNCE_MS) {
if (g_obstacle != g_rawState) {
g_obstacle = g_rawState;
if (g_obstacle) {
g_triggerCount++; // 无障碍 -> 有障碍 计一次触发
}
digitalWrite(LED_PIN, g_obstacle ? LOW : HIGH); // 有障碍点灯
g_oledDirty = true;
g_serialDirty = true;
}
}
}
// =========================
// OLED UI
// =========================
static const char* getStateText() {
return g_obstacle ? "OBSTACLE" : "CLEAR";
}
static void drawOledUi() {
display.clearDisplay();
display.setTextWrap(false);
display.setTextColor(SSD1306_WHITE);
display.setTextSize(1);
display.setCursor(0, 0);
display.print(F("06 IR OBSTACLE " FIRMWARE_VERSION));
display.setTextSize(2);
display.setCursor(0, 20);
display.print(getStateText());
display.setTextSize(1);
display.setCursor(0, 52);
display.print(F("OUT:"));
display.print(g_obstacle ? 0 : 1);
display.print(F(" CNT:"));
display.print(g_triggerCount);
display.display();
}
// =========================
// 串口输出(有变化才输出)
// =========================
static void printSerialState() {
Serial.printf("STATE: %s | OUT: %d | CNT: %lu\n",
getStateText(),
g_obstacle ? 0 : 1,
(unsigned long)g_triggerCount);
}
void setup() {
Serial.begin(115200);
delay(100);
Serial.printf("IR obstacle demo %s starting...\n", FIRMWARE_VERSION);
pinMode(IR_OUT_PIN, INPUT_PULLUP);
pinMode(LED_PIN, OUTPUT);
// I2C1 重映射:PB9=SDA / PB8=SCL,必须先 setSDA/setSCL 再 begin()
Wire.setSDA(I2C_SDA_PIN);
Wire.setSCL(I2C_SCL_PIN);
Wire.begin();
Wire.setClock(100000); // I2C 100kHz,与 01~05 系列公共标准一致
if (display.begin(SSD1306_SWITCHCAPVCC, OLED_ADDRESS)) {
g_hasOled = true;
display.clearDisplay();
display.setTextColor(SSD1306_WHITE);
display.display();
Serial.println(F("OLED 0x3C: found"));
} else {
Serial.println(F("OLED 0x3C: NOT found (continue with LED + serial only)"));
}
// 以真实上电电平作为初始状态,避免上电时误触发计数
g_rawState = readIrObstacle();
g_obstacle = g_rawState;
g_lastChangeMs = millis();
digitalWrite(LED_PIN, g_obstacle ? LOW : HIGH);
// 首帧:串口 + OLED 都显示一次当前状态
printSerialState();
if (g_hasOled) {
drawOledUi();
}
}
void loop() {
updateIrState();
if (g_serialDirty) {
g_serialDirty = false;
printSerialState();
}
if (g_oledDirty && g_hasOled) {
g_oledDirty = false;
drawOledUi();
}
}
依赖库:Adafruit SSD1306 和 Adafruit GFX Library,用 PlatformIO 打开工程会按 platformio.ini 自动下载,不用手动安装。
Download
下载
解压后用 VS Code + PlatformIO 打开这个文件夹就能 Build,一般不需要改代码,也不需要改 platformio.ini。
第一次使用
- 按上面接线表接好:模块 OUT→PA0、VCC→3.3V、GND→GND;OLED SDA→PB9、SCL→PB8、VCC→3.3V、GND→GND
- 下载 ZIP,解压,用 VS Code + PlatformIO 打开这个文件夹
- 等 Adafruit SSD1306 和 GFX 库自动装完,点 Build,看到 SUCCESS
- 接上 ST-Link,点 Upload,看到
** Verified OK **和[SUCCESS] - 打开串口监视器(115200),手挡在模块前方,OLED 应从 CLEAR 变 OBSTACLE,串口 OUT 从 1 变 0,CNT +1
正常应该看到什么
| 动作 | OLED / 串口 / LED 上的表现 |
|---|---|
| 上电 | OLED 显示 06 IR OBSTACLE v1.0 与 CLEAR;串口 STATE: CLEAR | OUT: 1;LED 灭 |
| 障碍物挡在模块前方 | OLED 变 OBSTACLE;串口 OUT: 0;LED 亮;CNT +1 |
| 移开障碍物 | OLED 恢复 CLEAR;LED 灭;CNT 保持不变 |
pio device list,找 CP2102 / CP210x / CH340 对应的那个。
Troubleshooting
常见问题排查
下面每一条都是这个模块真实会遇到的。标注「实测」的是我们自己踩过的。
障碍很近但 OUT 一直是高电平(没检测到)
按这个顺序查三处:
- 电位器是否调到临界。顺 / 逆时针微调试试,触发距离会跟着变。
- 障碍物反光率。黑色 / 吸光材料很难反射红外光,换一张白纸测试,能触发就是模块正常。
- OUT 是否真接在 PA0。接错引脚或共地不良都会导致读不到。
阈值附近 LED 乱跳 / 计数乱涨
电位器调到了临界点,LM393 输出在抖。障碍物刚好卡在触发距离上时,光强就在阈值上下抖,OUT 快速来回跳。
怎么办:代码已做 30ms 消抖能挡掉大部分,但机械上还是建议微调电位器离开临界,或者把灵敏度调低一点。
OLED 不亮 / 串口打印 OLED 0x3C: NOT found
十有八九是 I2C 引脚或地址。按这个顺序查:
- 确认 SDA 接 PB9、SCL 接 PB8,不是默认的 PB7/PB6。
- 确认代码里
setSDA/setSCL在Wire.begin()之前,顺序反了配置不生效。 - 确认 OLED 地址。常见 0.96 寸 SSD1306 大多是
0x3C,少数是0x3D。 - 确认共地。OLED 的 GND 必须和 STM32 接在一起。
有障碍时板载 LED 反而灭、无障碍反而亮
PC13 是低电平点亮。BluePill 的板载 LED 接在 PC13 上,低电平亮、高电平灭,所以代码里「有障碍写 LOW、无障碍写 HIGH」。
如果表现正好反了,检查是否抄错极性,或者换用别的高电平点亮的状态灯引脚。
串口看不到数据
先查当前串口:
pio device list
找到 CP2102 / CP210x / CH340 对应的那个 COM,然后在自己电脑的 platformio.ini 里加:
monitor_port = COMx
monitor_speed = 115200
COMx 要换成你自己电脑实际识别到的端口号,不要直接抄。只有需要看串口时才加这两行,正常用 ST-Link 烧录不需要。
状态反了:有障碍显示 CLEAR
不同厂家模块的输出极性可能不同。确认模块 OUT 接的是 PA0,且代码用「低电平=有障碍」判断。如果反了,改代码里 readIrObstacle() 的判断(把 == LOW 改成 == HIGH),或换一根逻辑相反的引脚接线。
References
资料来源
本页信息来源声明
| 内容 | 来源 |
|---|---|
| 接线、代码、实测结果、编译烧录记录、串口输出 | 本项目实测 2026-08-17 实机测试 |
| 红外对管反射式工作原理、LM393 比较器、电位器调阈值 | 资料参考 公开资料与常规工程实践 |
| 与超声波 / ToF 的对比 | 资料参考 三类器件的工作原理,由我们整理成表 |
本页所有「资料参考」内容均由我们重新整理表述,未直接复制原文。不同厂家 / 批次模块可能存在引脚顺序或检测距离差异,参数以你手上模块的实际丝印和卖家说明为准。