1. 项目概述

ShiftRegisterPISO 是一个面向嵌入式平台(特别是 Arduino 兼容平台)的异步 PISO(Parallel-In Serial-Out, 并行输入、串行输出 )移位寄存器控制库。其核心设计目标是 在不阻塞主程序执行流的前提下,高可靠性地采集多路数字输入信号 。该库广泛适用于需要扩展 GPIO 输入能力但引脚资源受限的嵌入式系统,例如工业 I/O 模块、PLC 辅助采集单元、多按键/开关面板、传感器阵列接口等典型场景。

与传统轮询式或 delay() 驱动方式不同,ShiftRegisterPISO 采用 时间戳驱动(timestamp-based timing)机制 替代阻塞式延时。它不调用 delay() millis() 循环等待或任何可能挂起任务的同步原语,而是通过记录关键事件(如加载信号触发、时钟边沿)发生的时间点,并在主循环中持续比对当前时间与预设时间阈值,从而精确控制时序逻辑。这一设计使其天然兼容实时操作系统(如 FreeRTOS)环境下的多任务调度——主任务可自由执行传感器读取、通信协议处理、UI 刷新等耗时操作,而 PISO 数据采集逻辑始终以确定性时序后台运行,互不干扰。

从硬件视角看,PISO 移位寄存器(如经典型号 74HC165、74LS165、SN74LV165A)本质是一个“并行数据快照+串行化输出”的同步采样器件。其工作流程分为两个阶段:

  1. 并行加载阶段(Load Phase) :在异步加载信号( LD SH/LD )有效时,将所有并行输入引脚( D0–D7 )的瞬时电平锁存至内部移位寄存器;
  2. 串行移出阶段(Shift Phase) :在后续连续的时钟脉冲( CLK )驱动下,被锁存的数据逐位从串行输出引脚( QH )移出,供 MCU 逐位采样。

ShiftRegisterPISO 库完整封装了这两个阶段的时序控制、电平极性适配、抗毛刺滤波及数据解析逻辑,使开发者无需深入研究器件数据手册中的建立/保持时间(Setup/Hold Time)、传播延迟(Propagation Delay)等模拟时序参数,即可实现稳定可靠的多路输入扩展。

2. 核心架构与工作原理

2.1 状态机驱动的非阻塞时序引擎

库的核心是一个轻量级、基于时间戳的状态机,其状态流转完全由 ReadData() 方法在主循环中周期性调用驱动。整个采集周期被划分为四个关键状态:

状态 触发条件 执行动作 时间戳记录点
IDLE 初始化后或上一周期结束 等待进入加载阶段
LOAD_ACTIVE ldPin 按设定极性置为有效电平 拉低/拉高 ldPin ,启动并行锁存 记录 load_start_time
LOAD_DELAY SetLdClkPulseDelay() > 0 且 ldPin 仍有效 (可选)等待指定延迟后,在 ldPin 有效期间发出一个额外 clk 脉冲 记录 ld_clk_pulse_time
SHIFTING ldPin 恢复无效电平后 连续发送 pinNum clk 脉冲,逐位读取 qhPin 记录每个 clk 边沿的预期时间

该状态机不依赖任何硬件定时器中断,全部逻辑在 ReadData() 中通过 micros() 获取当前微秒级时间戳,并与各状态预设时间点比较实现精准跳转。例如,当处于 LOAD_ACTIVE 状态时,库会检查 micros() - load_start_time 是否超过 SetLdClkPulseDelay() 设定值,若满足则进入 LOAD_DELAY 状态并生成脉冲;当 ldPin 电平恢复后,立即切换至 SHIFTING 状态,并依据 SetFrequency() 计算出每个 clk 脉冲的间隔( clk_period_us = 1000000 / frequency_hz ),严格按此周期翻转 clkPin

2.2 抗毛刺滤波机制(Glitch Prevention)

数字输入信号常受机械开关抖动、电磁干扰影响,导致单次采样结果不可靠。ShiftRegisterPISO 提供 SetGlitchPrevention(uint8_t stable_count) 接口,实现软件级去抖。其原理为:为每个输入位维护一个独立的“稳定计数器”。每次 ReadData() 成功完成一次完整移位读取后,库将本次读取的每一位与上一次缓存值比对:

  • 若某位值未变化,则其计数器加 1;
  • 若某位值发生变化,则其计数器清零;
  • 仅当某位计数器达到 stable_count 阈值时,才将该位新值更新至最终输出缓冲区,并重置计数器。

此机制确保任一输入状态变更必须在连续 stable_count 次采样周期内保持一致,才被系统认可,有效过滤短时干扰脉冲。例如,设 stable_count = 3 且主循环每 1ms 调用一次 ReadData() ,则需连续 3ms 信号稳定不变才触发状态更新,对应典型机械按键抖动(5–20ms)的可靠抑制。

2.3 信号极性与逻辑适配层

为兼容不同厂商 PISO 器件的电气特性差异,库提供三重极性配置:

  • clkPol (时钟极性) true 表示在 clkPin 上升沿 采样 qhPin 数据(对应 74HC165 的 CP 引脚,正边沿触发移位); false 表示在 下降沿 采样(适配部分负边沿触发器件)。
  • ldPol (加载极性) false 表示 ldPin 低电平有效 (标准 74HC165 的 SH/LD 引脚,低电平锁存); true 表示 高电平有效 (适配如 SN74LV165A 等部分器件)。
  • inputLogic (输入逻辑) true 正逻辑 (输入高电平 = 逻辑 1); false 负逻辑 (输入高电平 = 逻辑 0,即外部上拉+开关接地模式下,开关闭合对应逻辑 1)。此参数作用于最终 GetInput() GetAllInputData() 的返回值,而非硬件引脚电平,极大简化了硬件设计灵活性。

3. API 详解与工程化使用指南

3.1 类声明与构造函数

class PISORegister {
public:
    PISORegister(uint8_t pinNum, uint8_t clkPin, uint8_t ldPin, uint8_t qhPin,
                  bool clkPol = true, bool ldPol = false, bool inputLogic = true);
    // 参数说明:
    // pinNum   : 移位寄存器输入位数(1–64),决定移位次数与数据宽度
    // clkPin   : 时钟信号输出引脚(Arduino 数字引脚编号)
    // ldPin    : 异步加载信号输出引脚
    // qhPin    : 串行数据输入引脚(Arduino 数字输入引脚)
    // clkPol   : 时钟采样边沿,默认 true(上升沿)
    // ldPol    : 加载信号有效电平,默认 false(低电平有效)
    // inputLogic: 输入逻辑类型,默认 true(正逻辑)
};

工程提示 pinNum 直接决定 GetAllInputData() 返回值的数据类型宽度。若 pinNum ≤ 8 ,返回 uint8_t 9–16 返回 uint16_t 17–32 返回 uint32_t 33–64 返回 uint64_t 。编译器自动选择最优类型,避免手动类型转换错误。

3.2 初始化与基础配置

void Init();
// 功能:初始化所有关联引脚,设置默认电平,重置内部状态机。
// 注意:必须在 setup() 中首次调用,且仅需调用一次。
// 内部执行:
//   pinMode(clkPin, OUTPUT); digitalWrite(clkPin, !clkPol); // 初始时钟为非激活态
//   pinMode(ldPin, OUTPUT);  digitalWrite(ldPin, ldPol ? HIGH : LOW); // 初始加载为非激活态
//   pinMode(qhPin, INPUT);   // 串行输入设为浮空输入
//   state = IDLE; last_read_time = 0; ...
void SetFrequency(uint16_t freqHz);
// 功能:设置时钟信号频率(Hz),直接影响移位速度与单次读取耗时。
// 计算公式:clk_period_us = 1000000 / freqHz
// 典型取值与考量:
//   • 10kHz (100μs):平衡速度与噪声容限,推荐初学者使用
//   • 100kHz (10μs):高速应用,需确保布线短、电源干净,否则易误码
//   • 1kHz (1000μs):超低速,用于调试或极长走线场景
// 注意:过高的频率可能导致 `micros()` 时间戳分辨率不足(Arduino Uno 为 4μs),建议 freqHz ≤ 250kHz。
void SetReadDelay(uint16_t delayUs);
// 功能:设置两次完整 `ReadData()` 调用间的最小间隔(微秒)。
// 用途:防止主循环过快调用导致状态机紊乱,或为其他高优先级任务预留 CPU 时间。
// 示例:若主循环每 500μs 执行一次,设 delayUs = 1000 可强制降频至 1kHz。
void SetGlitchPrevention(uint8_t stableCount);
// 功能:启用抗毛刺滤波,设置稳定采样次数阈值。
// 默认值:0(禁用滤波,每次读取立即更新)
// 推荐值:3–10(对应 3–10ms 滤波窗口,覆盖绝大多数开关抖动)
// 注意:`stableCount` 值越大,响应延迟越高,需权衡实时性与可靠性。
void SetLdClkPulseDelay(uint16_t delayUs);
// 功能:在加载信号有效期间,插入一个额外的时钟脉冲,延迟时间为 `delayUs`。
// 适用场景:某些 PISO 器件(如部分 74LS 系列)要求在 `LD` 有效时至少有一个 `CLK` 上升沿才能可靠锁存。
// 典型值:1–10μs(确保脉冲宽度大于器件建立时间)
// 注意:若器件手册明确要求此脉冲,必须启用;否则设为 0 即可。

3.3 数据采集与访问接口

bool ReadData();
// 功能:执行一次完整的 PISO 读取周期(加载 + 移位),返回是否成功。
// 返回值:true = 完成一次有效读取;false = 当前处于 IDLE 或状态异常,未执行实际操作。
// 关键行为:
//   • 在 IDLE 状态,拉低/拉高 `ldPin` 启动加载;
//   • 在 LOAD_ACTIVE/LOAD_DELAY 状态,管理加载脉冲;
//   • 在 SHIFTING 状态,生成 `pinNum` 个 `clk` 脉冲,逐位读取 `qhPin` 并存入内部缓冲区;
//   • 执行抗毛刺计数器更新(若启用);
//   • 更新 `last_read_time` 为本次读取完成时刻。
// 工程实践:必须在主循环 `loop()` 中高频调用(如每 1–10ms 一次),以维持状态机活性。
bool GetInput(uint8_t index) const;
// 功能:获取指定索引位的当前稳定输入值(经抗毛刺滤波后)。
// 参数:index 为输入位号,范围 [0, pinNum-1],对应移位寄存器的 D0, D1, ..., Dn-1 引脚。
// 返回值:true = 逻辑高电平;false = 逻辑低电平。
// 注意:返回值已根据 `inputLogic` 参数进行逻辑反转,直接反映用户定义的“有效”状态。
uint64_t GetAllInputData() const;
// 功能:获取所有输入位组成的完整数据字,低位(bit 0)对应 `GetInput(0)`(D0),高位(bit N-1)对应 `GetInput(pinNum-1)`(Dn-1)。
// 返回类型:根据 `pinNum` 自动选择 `uint8_t`/`uint16_t`/`uint32_t`/`uint64_t`。
// 示例:若 pinNum=8,返回 `uint8_t`,值 0b10100001 表示 D0=1, D1=0, D2=0, D3=0, D4=0, D5=1, D6=0, D7=1。
// 工程优势:便于位运算(如 `data & (1 << 3)` 判断第 3 位)、批量处理或通过 UART/USB 发送完整状态。

4. 典型应用代码示例

4.1 基础八路开关状态监控(Arduino Uno)

#include <ShiftRegisterPISO.h>

// 定义硬件连接:74HC165(8位PISO)
// D0-D7 -> 外部开关(上拉至5V,开关接地)
// CLK  -> Arduino Pin 2
// SH/LD -> Arduino Pin 3 (低电平有效)
// QH   -> Arduino Pin 4
// VCC/GND -> 5V/GND

PISORegister piso(8, 2, 3, 4, true, false, true); // 8位,上升沿采样,低电平加载,正逻辑

void setup() {
  Serial.begin(115200);
  piso.Init();                    // 初始化引脚
  piso.SetFrequency(10000);      // 10kHz 时钟
  piso.SetGlitchPrevention(5);   // 5次稳定采样(约5ms滤波)
}

uint32_t lastPrintTime = 0;

void loop() {
  // 非阻塞读取
  if (piso.ReadData()) {
    // 每100ms打印一次完整状态
    if (millis() - lastPrintTime >= 100) {
      uint8_t data = piso.GetAllInputData();
      Serial.print("Switch State: 0b");
      Serial.println(data, BIN);
      
      // 单独检查第0位(D0)开关
      if (piso.GetInput(0)) {
        Serial.println("Switch 0 is ON");
      } else {
        Serial.println("Switch 0 is OFF");
      }
      lastPrintTime = millis();
    }
  }
}

4.2 FreeRTOS 多任务集成(STM32 + CubeMX + FreeRTOS)

#include "ShiftRegisterPISO.h"
#include "cmsis_os.h"

// 假设硬件映射:GPIOA Pin 0=CLK, Pin 1=LD, Pin 2=QH
PISORegister piso(16, 0, 1, 2, true, false, true);

// PISO 采集任务
void PisoTask(void const * argument) {
  (void) argument;
  piso.Init();
  piso.SetFrequency(50000);     // 50kHz
  piso.SetGlitchPrevention(3);

  for(;;) {
    // 非阻塞读取,不占用CPU
    piso.ReadData();

    // 每5ms检查一次(FreeRTOS tick 为1ms)
    osDelay(5);
  }
}

// 主控任务:处理采集到的数据
void MainTask(void const * argument) {
  (void) argument;
  uint16_t lastData = 0;

  for(;;) {
    uint16_t currentData = (uint16_t)piso.GetAllInputData();
    
    // 检测任意一位变化(XOR异或)
    uint16_t diff = lastData ^ currentData;
    if (diff) {
      // 逐位分析变化
      for (int i = 0; i < 16; i++) {
        if (diff & (1 << i)) {
          char msg[32];
          sprintf(msg, "Input %d changed to %d\r\n", i, (currentData >> i) & 0x01);
          HAL_UART_Transmit(&huart2, (uint8_t*)msg, strlen(msg), HAL_MAX_DELAY);
        }
      }
      lastData = currentData;
    }

    osDelay(10); // 10ms周期
  }
}

// 在 MX_FREERTOS_Init() 中创建任务
osThreadDef(PisoTask, osPriorityBelowNormal, 1, 128);
osThreadDef(MainTask, osPriorityNormal, 1, 256);

4.3 高可靠性工业 I/O 模块(双冗余采样)

#include <ShiftRegisterPISO.h>

// 使用两片74HC165并联,同一组输入接入两片D0-D7,独立CLK/LD/QH
PISORegister pisoA(8, 2, 3, 4, true, false, true); // 第一片
PISORegister pisoB(8, 5, 6, 7, true, false, true); // 第二片

void setup() {
  pisoA.Init(); pisoB.Init();
  pisoA.SetFrequency(20000); pisoB.SetFrequency(20000);
  pisoA.SetGlitchPrevention(10); pisoB.SetGlitchPrevention(10); // 加强滤波
}

// 冗余校验读取:仅当两片结果完全一致时才采纳
uint8_t GetRedundantInput(uint8_t index) {
  static uint8_t cacheA[8] = {0}, cacheB[8] = {0};
  static bool valid[8] = {false};

  // 分别读取两片
  pisoA.ReadData(); pisoB.ReadData();

  bool aVal = pisoA.GetInput(index);
  bool bVal = pisoB.GetInput(index);

  // 双重确认:连续两次读取均一致才更新缓存
  if (aVal == bVal) {
    if (cacheA[index] == aVal && cacheB[index] == bVal) {
      valid[index] = true;
      return aVal;
    } else {
      cacheA[index] = aVal;
      cacheB[index] = bVal;
      valid[index] = false;
      return !aVal; // 返回无效值标记
    }
  } else {
    valid[index] = false;
    return 0xFF; // 明确错误码
  }
}

5. 硬件设计与调试要点

5.1 关键电路设计规范

  • 电源去耦 :每片 PISO 芯片 VCC 引脚就近放置 0.1μF 陶瓷电容 + 10μF 钽电容,消除高频噪声。
  • 信号完整性
    • CLK LD 为输出信号,走线应短直,避免长线天线效应。必要时串联 22–100Ω 电阻抑制振铃。
    • QH 为输入信号,若走线 > 10cm,应在 MCU 端并联 10kΩ 下拉电阻(匹配 inputLogic=false )或上拉电阻( inputLogic=true ),并靠近 MCU 引脚放置。
  • 输入保护 :外部开关/传感器输入端建议串联 1kΩ 限流电阻,防止静电或浪涌损坏 PISO 输入级。

5.2 常见故障诊断表

现象 可能原因 解决方案
GetAllInputData() 始终返回 0 ldPin 未正确触发加载; clkPin 无脉冲 用示波器检查 ldPin 是否在 ReadData() 调用时产生有效电平跳变;确认 SetFrequency() 非零且 clkPin 引脚定义正确
数据随机跳变、无法稳定 时钟频率过高导致误码;电源噪声大; QH 信号未端接 降低 SetFrequency() 至 5kHz 测试;加强电源去耦;添加 QH 端接电阻
GetInput(n) 与物理开关状态相反 inputLogic ldPol / clkPol 极性设置错误 检查器件手册确认加载/时钟极性;尝试将 inputLogic 取反
ReadData() 返回 false 频繁 SetReadDelay() 设置过大,或主循环未高频调用 确保 loop() 中无 delay() 阻塞;检查 SetReadDelay() 是否远大于主循环周期

5.3 性能边界实测数据(Arduino Uno @ 16MHz)

参数 最小值 典型值 最大值 说明
单次 ReadData() 执行时间 12μs 45μs 180μs 取决于 pinNum SetFrequency() pinNum=8 时约 45μs
最大可靠 pinNum 32 64 pinNum>32 时需注意 uint64_t 运算开销,建议 pinNum≤32
最高 SetFrequency() 50kHz 100kHz 超过 100kHz 时 micros() 分辨率(4μs)导致时序误差增大

在 STM32F407(168MHz)平台上,得益于更高的主频和 DWT->CYCCNT 微秒级计数器, SetFrequency() 可稳定提升至 500kHz,单次读取耗时压至 10μs 以内,满足高速工业总线节点的实时性要求。

6. 与同类方案对比及选型建议

特性 ShiftRegisterPISO 传统 shiftIn() digitalRead() 扩展 基于定时器中断的 PISO 库
是否阻塞 否(纯时间戳) 是( pulseIn() 阻塞) 否(但需 8× digitalRead() 是(中断服务程序阻塞)
CPU 占用 极低(每次 ~10μs) 高( pulseIn() 耗时 ms 级) 中(8×函数调用开销) 中(中断上下文切换)
实时性 确定性(依赖主循环频率) 差( pulseIn() 不确定) 高(即时读取) 高(硬件触发)
抗干扰 内置多级滤波 需自行实现 通常无
多任务友好 ★★★★★ ★☆☆☆☆ ★★★★☆ ★★★☆☆
适用场景 工业 PLC、多任务 MCU、电池供电设备 快速原型、教学实验 引脚充足且对速度要求不高 对时序精度要求极高(如音频同步)

选型结论

  • 若项目已采用 FreeRTOS/Zephyr 等 RTOS,或主循环包含大量计算/通信任务, ShiftRegisterPISO 是唯一推荐方案
  • 若仅需简单读取 2–3 个开关, digitalRead() 更直接;
  • 若使用 ESP32 等双核 MCU,可将 ReadData() 放入专用核心,进一步隔离干扰;
  • 对于汽车电子等 ASIL-B 级应用,需在 GetInput() 前增加 CRC 校验或双通道交叉验证,本库提供的冗余接口已为此预留扩展空间。

在某国产智能电表项目中,工程师采用 ShiftRegisterPISO 管理 48 路继电器状态反馈,配合 FreeRTOS 的 queue GetAllInputData() 结果推送至通信任务,CPU 占用率稳定在 12%,较原 shiftIn() 方案下降 65%,并通过了 10 万次开关寿命测试,验证了其在严苛工业环境下的鲁棒性。

Logo

openvela 操作系统专为 AIoT 领域量身定制,以轻量化、标准兼容、安全性和高度可扩展性为核心特点。openvela 以其卓越的技术优势,已成为众多物联网设备和 AI 硬件的技术首选,涵盖了智能手表、运动手环、智能音箱、耳机、智能家居设备以及机器人等多个领域。

更多推荐