传感器 · 红外避障 · 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。

红外避障模块(LM393 + 红外对管)实物图
红外避障模块实物:LM393 比较器 + 一对红外发射/接收管,OUT 输出数字电平,板上电位器调触发距离
低=有OUT 低电平判障碍
30 ms消抖稳定窗口
PC13板载状态灯
0x3COLED I2C 地址

有无障碍实测

测试动作预期实测结果
上电,无障碍OLED 显示 CLEAR,串口 OUT:1,LED 灭CLEAR / OUT:1 / LED 灭
手或白纸挡在模块前方OLED 显示 OBSTACLE,串口 OUT:0,LED 亮,CNT +1OBSTACLE / 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
未验证的环境不当成支持。本项目没有在 Arduino IDE、Keil、STM32CubeIDE 下编译或验证过,所以不提供这些环境的支持说明。

What it does

这个模块干什么

红外避障模块就是给单片机装一个「有没有东西挡住」的开关。模块上一边发红外光、一边收红外光,前方有障碍物把光反射回来,输出就拉低;没障碍就拉高。它不告诉你障碍有多远,只回答「有 / 没有」。巡线小车、防撞检测、计数、到位检测都用这一类模块。

它靠障碍物把红外光反射回来才触发,所以反光差的物体(黑色、吸光材料)会难触发,镜面、浅色物体更容易触发。板子上的电位器调的是触发距离/灵敏度。

两个要点

要点意思
不是测距它只输出「过没过阈值」这一个二值结果,不知道障碍到底有多远
反射式靠障碍物反射红外光触发,反光率直接影响能不能触发、触发多远

和测距传感器怎么选

红外避障模块超声波 HC-SR04VL53L0X(ToF)
输出高低电平(有 / 无)距离(时间差)距离(激光 ToF)
能不能测距不能,只判有无能,厘米级能,毫米级
成本低低高
适合场景巡线、防撞、计数、到位检测粗略测距、倒车精确测距

对比依据为三类器件的工作原理,由我们整理成表。资料参考

别和能测距的传感器搞混。红外避障是只判有无的开关量;HC-SR04 超声波和VL53L0X 激光 ToF都能给出具体距离。要做防撞、到位检测用红外避障,要量具体距离用测距传感器。

Wiring

接线

本项目实测 红外避障模块和 OLED 分开接,模块 3 根线 + OLED 4 根线。下面接法实机跑通。

红外避障模块

信号模块丝印STM32F103C8T6
电源VCC3.3V(或 5V,见模块丝印)
地GNDGND
输出OUTPA0

OLED

信号OLEDSTM32F103C8T6
电源VCC / VDD3.3V
地GNDGND
时钟SCL / SCKPB8
数据SDAPB9

烧录与串口(不接也能跑,接上方便看数据)

用途连接说明
烧录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 标记)刷新串口和 OLEDg_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。

PlatformIO 工程 ZIP 含 platformio.ini、src/main.cpp、README.md。已去掉本机构建路径,解压即可用。约 3.5 KB
下载 ZIP

第一次使用

  1. 按上面接线表接好:模块 OUT→PA0、VCC→3.3V、GND→GND;OLED SDA→PB9、SCL→PB8、VCC→3.3V、GND→GND
  2. 下载 ZIP,解压,用 VS Code + PlatformIO 打开这个文件夹
  3. 等 Adafruit SSD1306 和 GFX 库自动装完,点 Build,看到 SUCCESS
  4. 接上 ST-Link,点 Upload,看到 ** Verified OK ** 和 [SUCCESS]
  5. 打开串口监视器(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 保持不变
这块板子不能用网页烧录器。网页烧录(WebSerial)主要面向 ESP32 / ESP8266 这类自带 USB 串口下载的芯片。STM32F103C8T6 本项目用的是 ST-Link + SWD,浏览器接触不到 SWD 接口。
不要照抄 COM 号。截图里出现的串口号是我们这台电脑的,你的电脑多半不一样。查当前串口:pio device list,找 CP2102 / CP210x / CH340 对应的那个。

Troubleshooting

常见问题排查

下面每一条都是这个模块真实会遇到的。标注「实测」的是我们自己踩过的。

障碍很近但 OUT 一直是高电平(没检测到)

按这个顺序查三处:

  1. 电位器是否调到临界。顺 / 逆时针微调试试,触发距离会跟着变。
  2. 障碍物反光率。黑色 / 吸光材料很难反射红外光,换一张白纸测试,能触发就是模块正常。
  3. OUT 是否真接在 PA0。接错引脚或共地不良都会导致读不到。
阈值附近 LED 乱跳 / 计数乱涨

电位器调到了临界点,LM393 输出在抖。障碍物刚好卡在触发距离上时,光强就在阈值上下抖,OUT 快速来回跳。

怎么办:代码已做 30ms 消抖能挡掉大部分,但机械上还是建议微调电位器离开临界,或者把灵敏度调低一点。

OLED 不亮 / 串口打印 OLED 0x3C: NOT found

十有八九是 I2C 引脚或地址。按这个顺序查:

  1. 确认 SDA 接 PB9、SCL 接 PB8,不是默认的 PB7/PB6。
  2. 确认代码里 setSDA/setSCL 在 Wire.begin() 之前,顺序反了配置不生效。
  3. 确认 OLED 地址。常见 0.96 寸 SSD1306 大多是 0x3C,少数是 0x3D。
  4. 确认共地。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 的对比资料参考 三类器件的工作原理,由我们整理成表

本页所有「资料参考」内容均由我们重新整理表述,未直接复制原文。不同厂家 / 批次模块可能存在引脚顺序或检测距离差异,参数以你手上模块的实际丝印和卖家说明为准。