1. 项目概述

IOFusion 是一个面向 Arduino 平台(特别是 ATmega328P 架构)的轻量级、确定性硬件抽象库,其核心设计目标是 在资源受限的 8 位 MCU 上实现高精度、低抖动的定时采样与信号生成 。它并非通用型外设驱动封装,而是针对嵌入式实时控制场景中反复出现的共性问题——如周期性 ADC 采集、数字信号频率/占空比测量、正交编码器模拟输出、以及精确 PWM 波形生成——所构建的一套协同工作的底层硬件助手模块。

该库严格遵循“ ISR 快进快出,计算下沉至主循环 ”的实时系统设计哲学。所有时间敏感操作(如定时器中断触发、GPIO 状态捕获、计数器累加)均在 Timer2 中断服务程序(ISR)内完成,且仅执行原子性标志置位与寄存器递增等极低成本操作;而所有涉及浮点运算、数组遍历、字符串解析、状态机判断等耗时任务,则全部移交至 loop() 函数中异步处理。这种明确的职责分离,确保了中断响应延迟稳定可控(典型值 < 2.5μs),为构建确定性系统提供了坚实基础。

IOFusion 的工程价值在于其对“ 确定性 ”(Determinism)的极致追求:

  • 时间确定性 :Timer2 驱动的固定周期 tick(默认 1ms)构成整个系统的调度节拍,所有采样与生成动作均严格对齐此节拍;
  • 行为确定性 AnalogSampler 不在 ISR 中启动 ADC 转换,而是通过标志位通知 loop() 主动读取,彻底规避 ADC 启动延时与转换时间波动带来的采样相位漂移;
  • 资源确定性 :所有模块均采用静态内存分配(无 malloc / new ),避免堆碎片与动态分配失败风险;
  • 接口确定性 :统一采用 JSON-like ASCII 命令行协议,响应格式可预测,便于上位机自动化解析。

该库特别适用于以下典型嵌入式应用场景:

  • 工业现场的多通道传感器数据同步采集系统(温度、压力、电流);
  • 电机闭环控制中的编码器位置反馈模拟与 PWM 驱动信号生成;
  • 数字电源中对开关管驱动信号的频率/占空比实时监测;
  • 教学实验平台中对信号时序关系的可视化验证(如 PWM 与 ADC 采样点的相位关系)。

2. 核心模块架构与原理分析

2.1 Timer2Driver:系统节拍发生器

Timer2Driver 是 IOFusion 的心脏模块,负责为整个系统提供稳定、低抖动的周期性中断源。它直接操作 ATmega328P 的 8 位 Timer2 模块,配置为 CTC(Clear Timer on Compare Match)模式,利用 OCR2A 寄存器设定比较匹配值,从而产生精确的定时中断。

其关键配置参数如下表所示(以 Arduino Uno 默认 16MHz 主频为例):

参数 取值 计算公式 实际周期 说明
F_CPU 16,000,000 Hz MCU 主频
PRESCALER 64 CS22=1, CS21=0, CS20=0 分频系数,兼顾精度与最大周期
OCR2A 249 (F_CPU / PRESCALER / TARGET_FREQ) - 1 1.000 ms 目标节拍周期(1kHz)
ISR Overhead ≤ 2.5 μs 实测(含 RETI 保证 loop() 有充足时间处理
// Timer2Driver.cpp 关键初始化代码片段
void Timer2Driver::begin(uint16_t period_ms) {
  // 1. 停止 Timer2
  TCCR2B = 0x00;
  // 2. 清零计数器与中断标志
  TCNT2 = 0;
  TIFR2 = _BV(OCF2A);
  // 3. 设置 CTC 模式 & 64 分频
  TCCR2A = _BV(WGM21); // CTC mode, OC2A disconnected
  TCCR2B = _BV(CS22);  // 64 prescaler (CS22=1, CS21=0, CS20=0)
  // 4. 计算并写入 OCR2A
  uint8_t ocr2a_val = static_cast<uint8_t>((F_CPU / 64UL / period_ms) - 1);
  OCR2A = ocr2a_val;
  // 5. 使能比较匹配 A 中断
  TIMSK2 = _BV(OCIE2A);
}

Timer2 ISR 的唯一职责是:

  1. 置位全局 tick_flag volatile bool 类型,防止编译器优化);
  2. DigiIn 模块的输入计数器进行原子递增(使用 ATOMIC_BLOCK cli()/sei() 保护);
  3. 更新 EncoderGenerator 的内部状态机(仅修改方向/位置变量);
  4. 执行 reti 指令返回。

该 ISR 内 绝对禁止 执行以下操作:

  • 调用任何 Arduino API(如 digitalRead , analogRead , Serial.print );
  • 进行浮点运算( float , double );
  • 访问非 volatile 修饰的共享变量;
  • 调用任何可能阻塞或不可重入的函数。

2.2 AnalogSampler:确定性 ADC 采样器

AnalogSampler 解决了传统 analogRead() 在实时系统中的根本缺陷:ADC 转换时间(约 104μs)远大于 ISR 允许的执行窗口,且受 AVCC 稳定性、参考电压切换延时等影响,导致采样时刻无法精确对齐定时节拍。

其设计精髓在于 采样请求与结果获取的时空解耦

  • ISR 侧 :仅设置一个 volatile bool adc_pending 标志;
  • loop() :检测到标志后,立即调用 analogRead() 获取当前值,并将结果存入环形缓冲区(Ring Buffer),随后清除标志。

该机制确保:

  • 每次 ADC 读取都发生在 loop() 的确定性上下文中,不受中断抢占干扰;
  • 采样点严格对应于最近一次 Timer2 tick(误差 < loop() 执行延迟);
  • 多通道采样可通过轮询方式实现,各通道间相位差恒定。
// AnalogSampler.h 接口定义
class AnalogSampler {
public:
  void begin(uint8_t channel_count = 1); // 初始化通道数
  void setVref(float vref);                // 设置参考电压(默认 5.0V)
  void startConversion();                  // 在 loop() 中调用,触发单次读取
  int16_t getLatestSample(uint8_t ch);     // 获取指定通道最新有效值
  float getVoltage(uint8_t ch);            // 返回换算后的电压值(V)

private:
  volatile bool adc_pending_;              // ISR 置位,loop() 清除
  uint16_t samples_[ANALOG_MAX_CHANNELS]; // 存储最新采样值
  float vref_;                             // 用户配置的参考电压
};

getVoltage() 的实现体现了工程细节:

float AnalogSampler::getVoltage(uint8_t ch) {
  if (ch >= channel_count_) return 0.0f;
  // ADC 分辨率 10-bit → 0~1023 映射到 0~vref_
  return (static_cast<float>(samples_[ch]) * vref_) / 1023.0f;
}

工程提示 :若需更高精度,可启用 ADC 的 1.1V 内部参考源( analogReference(INTERNAL) ),此时必须调用 analogSampler.setVref(1.1f) ,否则电压换算结果将严重失准。

2.3 DigiIn:数字信号参数分析器

DigiIn 模块专为高频数字信号(如 PWM 输出、方波发生器)的实时参数测量而设计。它不依赖外部中断(INT0/INT1),而是利用 Timer2 的固定 tick,在每个节拍点对指定引脚执行 digitalRead() ,并通过软件算法推导出信号的频率与占空比。

其工作流程分为两层:

  • ISR 层 :每 tick 读取一次引脚电平,存入长度为 WINDOW_SIZE (默认 100)的布尔数组,并维护一个指向当前写入位置的索引 write_idx_
  • loop() :当缓冲区满后,扫描整个窗口,统计 HIGH 状态出现的次数,计算占空比;同时检测电平跳变沿,计算单位时间内的跳变次数,再除以 2 得到基频。
// DigiIn.h 关键结构
class DigiIn {
public:
  void begin(uint8_t pin, uint8_t window_size = 100);
  void update();                           // 在 loop() 中周期调用
  uint32_t getFrequencyHz();               // 返回最近窗口计算的频率(Hz)
  uint8_t getDutyCyclePct();               // 返回最近窗口计算的占空比(%)

private:
  const uint8_t pin_;
  const uint8_t window_size_;
  volatile uint8_t buffer_[MAX_WINDOW];    // 存储最近 window_size 个采样点
  volatile uint8_t write_idx_;              // 当前写入位置(原子操作)
  uint32_t freq_hz_;                        // 最新计算结果(非 volatile,由 loop() 更新)
  uint8_t duty_pct_;
};

覆盖限制说明 :由于 ATmega328P 的 digitalRead() 典型执行时间为 4~5μs, DigiIn 的理论最高可测频率约为 1000ms / (100 * 5μs) = 2kHz 。若需测量更高频率(如 20kHz 开关电源信号),需改用硬件输入捕获(ICP)功能,此为 COVERAGE_LIMITATIONS.md 中明确指出的 AVR 平台固有限制。

2.4 EncoderGenerator:正交编码器信号发生器

EncoderGenerator 并非用于解码物理旋转编码器,而是一个 可编程的正交信号(A/B 相)发生器 ,常用于电机控制系统的仿真测试或作为位置指令源。其输入为两个逻辑电平信号 up down ,输出为标准的两路互补正交波形(通常连接至 OC1A / OC1B 引脚)。

其状态机语义定义清晰:

  • up == HIGH && down == LOW → 正向步进(Position++,Direction = +1);
  • up == LOW && down == HIGH → 反向步进(Position--,Direction = -1);
  • up == LOW && down == LOW → 保持当前位置(Hold);
  • up == HIGH && down == HIGH 非法状态,被忽略 (避免误触发)。

该模块的输出波形严格遵循正交编码器标准:A 相与 B 相相位差 90°,且方向由 A 相相对于 B 相的超前/滞后关系决定。其内部通过查表法( const uint8_t QUAD_TABLE[4] )实现状态到输出电平的映射,确保零计算开销。

// EncoderGenerator.cpp 状态转移核心逻辑
void EncoderGenerator::update() {
  uint8_t up_state = digitalRead(up_pin_);
  uint8_t down_state = digitalRead(down_pin_);
  uint8_t key = (up_state << 1) | down_state; // 2-bit key: 00, 01, 10, 11

  switch (key) {
    case 0b01: // down HIGH, up LOW -> reverse
      position_--;
      direction_ = -1;
      break;
    case 0b10: // up HIGH, down LOW -> forward
      position_++;
      direction_ = +1;
      break;
    case 0b00: // both LOW -> hold
      direction_ = 0;
      break;
    default:   // 0b11 illegal -> ignore
      return;
  }

  // 查表更新 A/B 相输出(假设已配置为 PWM 模式)
  uint8_t quad_out = QUAD_TABLE[position_ & 0x03];
  digitalWrite(quad_a_pin_, quad_out & 0x01);
  digitalWrite(quad_b_pin_, (quad_out >> 1) & 0x01);
}

2.5 Timer1PWM:高精度 PWM 信号发生器

Timer1PWM 模块充分利用 ATmega328P 的 16 位 Timer1,提供远超 analogWrite() (基于 Timer0)的分辨率与频率灵活性。它直接配置 OCR1A / OCR1B 寄存器,支持相位正确 PWM(Phase Correct PWM)与快速 PWM(Fast PWM)两种模式,输出引脚固定为 OC1A (Pin 9)与 OC1B (Pin 10)。

其核心优势在于:

  • 频率独立可调 :通过修改 ICR1 (Top Value)寄存器,可在不改变占空比的前提下,动态调整 PWM 基频;
  • 占空比精细控制 :16 位分辨率(0~65535)支持 0.0015% 级别的占空比调节;
  • 双通道同步 :A/B 通道共享同一时钟源与 Top 值,确保严格同步。
// Timer1PWM.h 接口摘要
class Timer1PWM {
public:
  void begin(uint16_t freq_hz = 1000); // 初始化,设置基频
  void setFrequency(uint16_t freq_hz); // 动态修改频率
  void setDutyCycle(uint8_t channel, uint16_t duty_16bit); // channel: 0(A), 1(B)

private:
  uint16_t top_value_; // ICR1 value, derived from freq_hz
  uint16_t duty_a_;    // OCR1A value
  uint16_t duty_b_;    // OCR1B value
};

setFrequency() 的实现需精确计算 ICR1

void Timer1PWM::setFrequency(uint16_t freq_hz) {
  // Fast PWM mode: TOP = ICR1, Update OCRx at BOTTOM
  // f_PWM = f_CLK / (prescaler * (1 + ICR1))
  // => ICR1 = (f_CLK / (prescaler * f_PWM)) - 1
  const uint32_t prescaler = 1; // 使用 1:1 预分频(需确保 f_PWM <= 8MHz)
  top_value_ = static_cast<uint16_t>((F_CPU / (prescaler * freq_hz)) - 1);
  ICR1 = top_value_;
}

硬件约束 Timer1PWM 的最高输出频率受限于 F_CPU / 1 = 16MHz,但实际应用中,为保证足够的分辨率(如 10-bit),通常将频率设定在 1~50kHz 范围内。

3. 数据流与时序模型

IOFusion 的整体数据流严格遵循“ 中断驱动、主循环处理 ”的两级流水线模型,其核心交互关系可由下图清晰表达:

flowchart LR
  T2[Timer2Driver ISR<br/>每1ms触发] -->|置位 flag| AS[AnalogSampler<br/>loop()中读取ADC]
  T2 -->|递增计数器| DI[DigiIn<br/>loop()中分析窗口]
  T2 -->|更新状态机| EN[EncoderGenerator<br/>loop()中生成波形]
  LOOP[loop()] --> AS
  LOOP --> DI
  LOOP --> CMD[Command Parser<br/>串口命令解析]
  CMD --> PWM[Timer1PWM<br/>设置频率/占空比]
  CMD --> RESP[Serial Response<br/>JSON-like 输出]
  AS --> RESP
  DI --> RESP
  EN --> RESP

该模型的关键时序契约(Timing Contract)要求:

  • loop() 的最坏执行时间(Worst-case Latency)必须显著小于 DigiIn 的测量窗口持续时间 。例如,若 DigiIn 窗口为 100ms(100 个 1ms tick),则 loop() 的单次执行时间应控制在 10ms 以内,否则将导致窗口数据陈旧,频率/占空比计算失效;
  • AnalogSampler startConversion() 调用频率应等于或略高于 Timer2 tick 频率 。若 loop() 执行过慢,将导致部分 tick 的采样请求被丢弃,表现为采样率下降;
  • EncoderGenerator::update() Timer1PWM::setDutyCycle() 必须在 loop() 中被及时调用 ,否则输出波形将停滞在上一状态。

4. 命令行接口(CLI)协议详解

IOFusion 固件通过 UART( Serial )暴露一个简洁、健壮的 ASCII 命令行接口,所有命令与响应均采用类 JSON 格式,极大简化了上位机(PC、PLC、手机 App)的集成难度。该协议设计遵循“ 无状态、幂等、自描述 ”原则。

4.1 命令语法与响应格式

命令 功能 示例请求 示例成功响应 错误响应示例
analog? 查询所有已配置 ADC 通道电压 analog? {"analog":[2.45,3.12]} {"error":"adc not ready"}
digital? 查询所有 DigiIn 通道参数 digital? {"digital":[{"freq":1250,"duty":42}]} {"error":"dig_in overflow"}
encoder? 查询编码器位置与方向 encoder? {"encoder":{"pos":157,"dir":1}} {"error":"encoder init failed"}
pwm-freq <hz> 设置 Timer1 PWM 基频 pwm-freq 5000 {"pwm":{"freq":5000}} {"error":"pwm freq out of range"}
pwm-duty <ch> <pct> 设置指定通道占空比 pwm-duty 0 75 {"pwm":{"ch":0,"duty":75}} {"error":"pwm ch invalid"}
help 显示帮助信息 help {"help":"analog?, digital?, ... "}

协议细节

  • 所有命令以 \n \r\n 结尾;
  • 响应均为单行纯文本,不含额外空格或换行;
  • 数值字段均为十进制整数或浮点数,无科学计数法;
  • 错误响应始终包含 "error" 键,其值为简明英文描述,便于开发者快速定位问题。

4.2 CLI 实现要点

src/cmdline.cpp 中的解析器采用 状态机驱动的逐字符解析 ,避免 String 类(易造成内存碎片)与 sscanf (代码体积大)。其核心逻辑为:

  1. loop() 中持续调用 Serial.available() ,若有数据则读取单字节;
  2. 将字节流缓存至固定大小 cmd_buffer[32] ,遇 \n 则触发解析;
  3. 使用 strncmp() 匹配命令前缀,再用 strtol() 提取参数;
  4. 执行对应操作后,调用 Serial.print() 输出 JSON 响应。

该设计确保了 CLI 模块自身也符合 IOFusion 的确定性原则:无动态内存分配、无阻塞等待、解析时间可预测。

5. PlatformIO 集成与工程实践

IOFusion 作为 PlatformIO 库,其集成流程高度标准化,支持本地开发、团队共享与公共发布三种模式。

5.1 本地项目依赖配置

在已有 PlatformIO 项目的 platformio.ini 文件中,添加以下配置即可引入 IOFusion:

[env:uno]
platform = atmelavr
board = uno
framework = arduino
lib_deps =
  https://github.com/djwinter29-oss/arduino-iofusion.git

PlatformIO 将自动克隆仓库、解析 library.json ,并将 lib/IOFusion/include 添加至编译器头文件搜索路径。用户只需在 src/main.cpp #include <IOFusion.h> 即可使用全部功能。

5.2 库发布与版本管理

library.json 是 PlatformIO Registry 的元数据文件,其关键字段必须准确填写:

{
  "name": "IOFusion",
  "version": "0.3.1",
  "description": "Deterministic timer-driven analog/digital sampling and signal generation helpers for Arduino",
  "keywords": "arduino, timer, sampling, pwm, encoder",
  "repository": {
    "type": "git",
    "url": "https://github.com/djwinter29-oss/arduino-iofusion.git"
  },
  "authors": [
    {
      "name": "D. Winter",
      "email": "djwinter29@example.com",
      "url": "https://github.com/djwinter29-oss"
    }
  ],
  "frameworks": "arduino",
  "platforms": "atmelavr"
}

发布流程:

  1. 本地测试通过后, git commit -m "release: v0.3.1"
  2. git tag v0.3.1 && git push origin v0.3.1
  3. 执行 pio pkg publish --type library
  4. 若发布错误,可用 pio pkg unpublish --type library --owner djwinter29-oss --name IOFusion --version 0.3.1 撤回。

5.3 单元测试与覆盖率验证

IOFusion 提供了完整的主机端(Host-based)单元测试套件,运行于 native 平台(即开发者 PC),通过 ArduinoFake 库模拟 Arduino API 行为。测试脚本 tools/coverage.sh 自动执行:

  • 编译测试固件;
  • 运行测试二进制;
  • 生成 coverage/index.html (HTML 报告)与 coverage/coverage.xml (CI/CD 可解析格式)。

该测试框架覆盖了所有核心模块的边界条件,例如:

  • AnalogSampler::getVoltage() vref_=0.0f 时的除零防护;
  • DigiIn::update() window_size=1 时的极端情况;
  • Timer1PWM::setFrequency() 对超出范围频率(如 freq_hz=0 freq_hz > 8000000 )的错误处理。

6. 典型应用示例:电机闭环控制仿真平台

以下代码片段展示了如何将 IOFusion 各模块协同用于一个简化的电机控制仿真场景:使用 EncoderGenerator 模拟电机旋转, DigiIn 测量其输出频率, Timer1PWM 生成驱动信号, AnalogSampler 采集虚拟电流反馈。

// src/main.cpp
#include <IOFusion.h>

Timer2Driver timer2;
AnalogSampler analogSampler;
DigiIn digiIn(9); // 监测 OC1A (Pin 9) 输出
EncoderGenerator encoderGen(2, 3, 9, 10); // up=Pin2, down=Pin3, A=Pin9, B=Pin10
Timer1PWM pwm;

void setup() {
  Serial.begin(115200);
  delay(100);

  // 初始化所有模块
  timer2.begin(1); // 1ms tick
  analogSampler.begin(1);
  analogSampler.setVref(5.0f);
  digiIn.begin(9, 100); // Pin9, 100ms window
  pwm.begin(10000); // 10kHz PWM

  // 启动仿真:模拟电机正转
  pinMode(2, INPUT_PULLUP); // up
  pinMode(3, INPUT_PULLUP); // down
  digitalWrite(2, HIGH);    // up = HIGH
  digitalWrite(3, LOW);     // down = LOW
}

void loop() {
  // 1. 处理 IOFusion 数据流
  if (timer2.tick()) {
    // ISR 已完成标志设置与计数,此处执行耗时操作
    analogSampler.startConversion();
    digiIn.update();
    encoderGen.update();
  }

  // 2. 响应串口命令
  cmdline::process();

  // 3. 闭环控制逻辑(简化版)
  uint32_t measured_freq = digiIn.getFrequencyHz();
  if (measured_freq < 5000) {
    // 频率偏低,增大 PWM 占空比
    pwm.setDutyCycle(0, 40000); // ~61%
  } else if (measured_freq > 5500) {
    // 频率偏高,减小占空比
    pwm.setDutyCycle(0, 30000); // ~46%
  }

  // 4. 每秒打印一次状态(非实时关键)
  static unsigned long last_print = 0;
  if (millis() - last_print > 1000) {
    Serial.print("Freq: ");
    Serial.print(measured_freq);
    Serial.print("Hz, Pos: ");
    Serial.println(encoderGen.getPosition());
    last_print = millis();
  }
}

此示例印证了 IOFusion 的核心价值: 将复杂的硬件时序控制,转化为清晰、可维护、可测试的 C++ 对象接口 。工程师无需深究 AVR 汇编指令或寄存器位定义,即可快速构建出具备工业级确定性的嵌入式控制系统原型。

Logo

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

更多推荐