PlatformIO IDE(VSCode) - 实现 VS1838B 红外接收驱动

最近做一个项目需要用到红外接收,项目工程针对的是 STM32F103C8T6 平台,使用的是 Arduino 框架,想着不重复造轮子,结果第三方红外库都没有同时满足 ST STM32 平台 和 Arduino 框架两个条件的,索性就自己造个轮子呗,也不难!

硬件连接

本文的实现驱动的硬件条件如下:

  • STM32F103C8T6 最小系统板
  • stlink v2 仿真调试器
  • VS1838B 红外接收头
  • 常规的红外遥控器

VS1838B 有三个管脚,VCC 、 GND 和 OUT,搭建的硬件电路很简单,不需要其他的配套电路,直接连接单片机管脚和电源就能正常使用,这里我将管脚连接到 PA0 管脚。

红外相关知识

要想编写红外驱动需要了解红外接收器输出得信号特点,根据 VS1838B 输出信号有的放矢的设计信号解析驱动。

笔者手头的红外遥控器采用 NEC 编码协议^[ NEC红外线编码协议],其中红外发射芯片会自动扫描矩阵键盘电路,根据不同的键码,生成一串二进制数据,再按每位的二进制数据用相应的红外光信号发出。遥控器上的一个按键按下后的 bit 数据:

引导码用于标识一个键码数据的传输开始,将传输 32 位数据,共四个字节,按照先低位后高位传输。第一个字节为用户编码,用于标识遥控器的厂家,第二个字节为用户编吗的反码,用于校验用户编码。第三个字节是按键编码用于区别按下的键,第四个字节为按键编码的反码,用于校验按键编码, 从而确保红外线传输的数据的有效性。

引导码、位数据 0 和位数据 1 信号特征明显,可以参考如下:

上面就是红外要发射数据的格式了,真正发射光信号的时候,其实是将待发射的数据调制到一个 38KHz 的光信号中的,红外接收头比如 VS1838B,接收到这个信号后会对信号进行信号放大、带通滤波、解调、波形整形最终还原出原始的 NEC 协议按键编码,但是要注意的是从 VS1838B 输出的信号是与原始编码反相的^[35 红外接收头在linux内核里的驱动],通俗讲就是高低电平是反着的。

驱动设计思路

为了找到解析 VS1838B 输出信号的方法,我们还是得先分析一下引导码、位数据 0 和 位数据 1 的特征,其实我们只要找到能区分这三个信号的方法即可。

观察上一小节信号特征图,可以发现三种信号最明显的区别就是低电平时间:

信号 低电平时间(ms)
引导码 4.5ms
b0 0.56ms
b1 1.68ms

但是我们知道 VS1838B 输出信号是与图中高低信号相反的,所以针对 VS1838B 的输出信号,我们只需要判断每段高电平的时间就可以区分出引导码、位数据 0 和 位数据 1。

一组红外数据的信号是以引导码开始的,所以正常状态下一旦判断到是引导码,下面就应该通过区分位数据信号来保存位数据生成字节数据了,需要连续分辨 32 位数据是 0 或 1即可。

为了保证及时的判断管脚电平的变化,这里采用 IO 外部中断的方式,在中断子程序中实现解析逻辑,解析数据之前关键的一点还是得到高电平时间,将外部中断触发方式设置为电平变化触发,也就是无论电平下降沿还是上升沿都会触发中断。

中断处理逻辑中,加入状态机的处理形式,可分两种状态:

  • 正常状态:等待引导码
  • 接收数据状态:解析 0、1 数据

最终结果存储在一个 32 位整型数据中,那么整个中断的处理逻辑步骤可参考如下:

  1. 判断进入中断时管脚电平
    • 如果是高电平,记录此刻时间,退出中断
    • 如果是低电平,用此刻时间减去记录的时间就得到了高电平脉冲宽度(单位 us),然后进行第 2 步
  2. 判断此时状态:
    • 如果是正常状态,脉宽如果大于 4000us 说明是引导码,此时将状态切换为接收数据状态,结果置为 0,然后退出中断
    • 如果是接收数据状态,判断脉宽如果在 1120us 和 2240us 之间说明是 1 ,将结果的对应位置 1 ,然后判断是否接受完所有位:
      • 如果是,对结果进行校验(利用反码),如果正确进行数据处理
      • 如果没有,退出中断

驱动程序实现

根据上述思路设计了红外接收驱动程序,以库的形式对程序封装,声明对象只需指定管脚和处理回调函数即可,在 PlatformIO IDE 项目工程目录 lib 中新建 IrRemote 目录,目录下放置源代码 irRemote.h 和 irRemote.cpp。理论上只要是使用 Arduino 框架,并且管脚支持 IO 外部中断,均可以正常使用。

irRemote.h

#ifndef IRREMOTE_H_
#define IRREMOTE_H_

#include "Arduino.h"

typedef void (*IrRemoteCBPtr)(uint32_t);

enum IrRemoteState { WAITING, READING };

/**
 * @brief 红外接收结果联合体
 *
 * 定义红外接收联合体是为了可以从原生的 32 位数据,轻松获取字节数据
 */
union IrRemoteResult {
  uint32_t value;
  uint8_t bytes[4];
};

class IrRemote {
 public:
  /**
   * @brief 红外类的构造函数
   *
   * 指定红外的管脚和回调函数
   *
   * @param pin 连接红外数据输出的管脚
   * @param callback
   * 收到数据后要执行的回调函数,回调函数具有一个参数用于传递数据结果
   */
  IrRemote(uint8_t pin, IrRemoteCBPtr callback);

  /**
   * @brief 红外数据接收处理函数
   *
   * 分析红外输出电平及持续时间,解析得到数据
   */
  void handler(void);

  /**
   * @brief 启动红外接收
   *
   * 开启红外数据解析逻辑
   */
  void begin(void);

  /**
   * @brief 关闭红外接收
   *
   * 关闭红外数据解析逻辑
   */
  void end(void);

 private:
  uint8_t pin;             // 连接红外输出的管脚
  uint32_t time;           // 上一次 FALLING 的时刻,单位 us
  uint32_t width;          // 脉冲宽度,单位 us
  uint32_t count;          // 数据接收位数计数
  IrRemoteState state;     // 处理状态
  IrRemoteResult result;   // 收到的数据结果
  IrRemoteCBPtr callback;  // 收到数据的回调函数

  /**
   * @brief 红外接收结果校验
   *
   * 校验红外接收结果,最后两个字节互为反码即为正确结果
   *
   * @param result 解析得到的红外接收结果
   */
  bool check(IrRemoteResult result);
};

#endif

irRemote.cpp

#include "irRemote.h"

void nullCB(uint32_t _) {}

void irRxHandler(IrRemote *ir) { ir->handler(); }

IrRemote::IrRemote(uint8_t pin, IrRemoteCBPtr callback) {
  this->pin = pin;
  this->callback = callback;

  pinMode(this->pin, INPUT);
}

void IrRemote::handler() {
  if (digitalRead(this->pin) == HIGH) {
    this->time = micros();
  } else {
    this->width = micros() - this->time;
    switch (this->state) {
      case WAITING:
        if (this->width > 4000 && this->width < 10000) {
          this->state = READING;
          this->count = 0;
          this->result.value = 0;
        }
        break;
      case READING:
        if (this->width > 1120 && this->width < 2240) {
          this->result.value |= (0x00000001 << this->count);
        }
        this->count++;
        if (this->count >= 32) {
          this->count = 0;
          this->state = WAITING;
          if (check(this->result)) {
            detachInterrupt(this->pin);
            this->callback(this->result.value);
            attachInterrupt(this->pin, (voidArgumentFuncPtr)irRxHandler, this,
                            CHANGE);
          }
        }
        break;
      default:
        break;
    }
  }
}

void IrRemote::begin() {
  this->state = WAITING;
  attachInterrupt(this->pin, (voidArgumentFuncPtr)irRxHandler, this, CHANGE);
}

void IrRemote::end() { detachInterrupt(this->pin); }

bool IrRemote::check(IrRemoteResult result) {
  if ((result.bytes[3] + result.bytes[2] == 0xFF) &&
      (result.bytes[1] + result.bytes[0] == 0xFF)) {
    return true;
  }
  return false;
}

测试驱动

编写一个例程测试一下驱动,当收到数据的时候通过串口把数据打印出来,main.cpp 内容如下:

#include <Arduino.h>
#include "irRemote.h"

void test(uint32_t result) { Serial.println(result, HEX); }

IrRemote ir(PB0, test);

void setup() {
  // 启动红外接收
  ir.begin();
  Serial.begin(9600);
}

void loop() {}

将连接了 VS1838B 的 stm32f103c8t6 最小系统板通过 stlink 连接电脑同时将其 usb 直连到电脑,编译上传程序,待运行以后,打开串口调试助手,对着 VS1838B 按下红外遥控按钮,便可以看到对应的 32 位原数据(从高位到低位四个字节分别是按键编码反码、按键编码、用户编码反码、用户编码):

总结

本文讲的红外驱动实现思路对于 NEC 协议的红外数据解析应该是通用的,笔者因为使用了 Arduino 框架,所以是以 Arduino 为基础设计的驱动程序,当然不使用 Arduino 框架也可以按照实现思路进行设计对应的驱动,思路可行就可以!希望本文介绍的思路能够帮到您!