网站首页 全球最实用的IT互联网站!

人工智能P2P分享Wind搜索发布信息网站地图标签大全

当前位置:诺佳网 > 电子/半导体 > 控制/MCU >

【MCU学习】GPIO详解

时间:2026-01-24 11:45

人气:

作者:admin

标签:

导读:本实验通过一个“小实验框架 GPIO Mode Lab”,在同一个 GPIO 引脚上依次配置不同模式,并用 ADC 探头测量电压、同时读取数字电平,系统化地观察。...

@[toc]

概要

在这里插入图片描述
在这里插入图片描述
摘自:STM32F10x 参考手册

在这里插入图片描述

本实验通过一个“小实验框架 GPIO Mode Lab”,在同一个 GPIO 引脚上依次配置不同模式,并用 ADC 探头测量电压、同时读取数字电平,系统化地观察:

  • 内部上拉 / 下拉电阻的大致阻值和效果
  • 外部电阻(不同阻值、接 VCC / 接 GND / 悬空)与内部电阻的“谁主导”关系
  • 推挽输出(OUTPUT)和开漏输出(OPEN-DRAIN)的真实行为
  • 浮空输入在模拟量和数字量上的不稳定性

并由此得出一系列可量化的直观结论,可写成一篇“GPIO 行为直观实验报告”。


整体架构流程

整体分为三层:

  1. 实验框架层:GPIO Mode Lab 类
    • 固定一组 GPIO 模式序列:
      • INPUTINPUT_PULLUPINPUT_PULLDOWNINPUT_ANALOG
      • OUTPUT LOW / OUTPUT HIGH
      • OUTPUT_OPEN_DRAIN LOW / OUTPUT_OPEN_DRAIN HIGH
    • 每轮实验:依次切换模式 → 延时稳定 → ADC 采样多次求平均 → 读取数字电平 → 串口打印结果和说明。
  2. 串口交互层:MiniShell + 菜单
    • 用户通过串口输入:
      • 外部电阻值 R_ext(单位 Ω,支持 0 表示无)
      • 电阻连接方式:node → R → GND / VCC / none
    • 支持多轮实验连续运行,任意输入阶段按 q/Q 退出。
  3. 测量与分析层:ADC + 简单电路模型
    • ADC:10 位,0–1023,对应约 0–3.3 V。
    • 对特定场景(如 INPUT_PULLUP + R_ext→GND)使用分压公式:
      • Vnode / Vref = R_down / (R_up + R_down)
      • 估算内部上拉电阻 R_up = R_down * (1 / ratio - 1),其中 ratio = ADC / 1023
    • 把不同 R_ext / 接法 / 模式的结果整理成表格,对比得出工程结论。

技术名词解释

  • INPUT(浮空输入)
    仅打开数字输入缓冲,不启用内部上拉/下拉。引脚呈高阻态,电平完全由外部电路和泄漏、噪声等决定。
  • INPUT_PULLUP / INPUT_PULLDOWN
    在 INPUT 的基础上,内部通过一只几十 kΩ 量级的“弱上拉 / 弱下拉”电阻,把引脚轻微拉向 VCC 或 GND,常用于按键等简单输入,避免悬空。
  • INPUT_ANALOG
    关闭数字输入缓冲和施密特触发器,仅保留到 ADC 的模拟路径,减小噪声和漏电,专用于电压采样。
  • OUTPUT(推挽输出)
    上下两个 MOS 管组成推挽结构,可主动拉高到 VCC 或拉低到 GND,等效输出电阻较小,能驱动一定电流
  • OUTPUT_OPEN_DRAIN(开漏输出)
    仅有下拉管能导通到 GND,上拉管常关;输出 LOW 时主动拉低,输出 HIGH 时为高阻态,需要外部上拉决定高电平,适合 I²C、线与等总线。
  • 浮空(Floating)
    引脚未通过明显的上拉/下拉或驱动源确定电平,表现为 ADC 读数在中间随机漂移,digitalRead 可能随机判 0/1。
  • 内部上拉/下拉电阻
    MCU 内部集成的可选电阻网络,通常在几十 kΩ 量级,用于给输入引脚提供弱上拉/下拉,避免悬空。

技术细节

1. 测量配置与流程

  • ADC 分辨率:10 位,0..1023Vref ≈ 3.3 V
  • 每个模式下:
    1. pinMode(target, mode) 配置模式
    2. 若为输出模式,则 digitalWrite 设为 HIGH/LOW
    3. 延时若干 ms 等待电平稳定
    4. analogRead(probe) 多次采样取平均 → 得到 ADC avg 和电压
    5. 把探针脚临时设为 INPUTdigitalRead 一次 → 观察数字门限行为
    6. 串口打印模式名、ADC、电压、数字结果和文字说明

2. 外部电阻与接法的实验组合

通过串口交互设置:

  • R_ext(示例:100 kΩ、15 kΩ;输入整数:100000、15000)
  • 接法:node -> R -> GND / VCC / none

本次记录的数据覆盖六种典型组合:

  1. R_ext = 100 kΩ,接 VCC
  2. R_ext = 100 kΩ,接 GND
  3. R_ext = 100 kΩ,逻辑上当作 none(菜单选 3)
  4. R_ext = 15 kΩ,接 VCC
  5. R_ext = 15 kΩ,接 GND
  6. R_ext = 15 kΩ,逻辑上当作 none(菜单选 3)

3. 实验数据表格

3.1 R_ext = 100 kΩ,接 VCC

序号模式ADC电压 (V)digitalRead备注
1INPUT (floating)7152.3061外部 100k 上拉占主导,输入脚高阻跟着被拉高
2INPUT_PULLUP9172.9581内部上拉与外部 100k 上拉并联,更接近 VCC
3INPUT_PULLDOWN2680.8650内部下拉 vs 外部上拉分压,电平在中间偏低
4INPUT_ANALOG7132.3001数字缓冲关掉,模拟只看到外部 100k 上拉
5OUTPUT LOW00.0000推挽强拉低,压制 100k 上拉
6OUTPUT HIGH10223.2971推挽强拉高,与上拉同向
7OUTPUT_OPEN_DRAIN LOW00.0000开漏下管导通,压制外部上拉
8OUTPUT_OPEN_DRAIN HIGH7152.3061开漏高阻,完全由 100k 上拉决定

3.2 R_ext = 100 kΩ,接 GND

序号模式ADC电压 (V)digitalRead备注
1INPUT (floating)00.0000外部 100k 下拉占主导
2INPUT_PULLUP6532.1061内部上拉 vs 100k 下拉分压,估算 Rup≈56.7 kΩ
3INPUT_PULLDOWN00.0000内部+外部下拉叠加,牢牢在 0V
4INPUT_ANALOG00.0000模拟输入也只看到外部下拉
5OUTPUT LOW00.0000推挽强拉低
6OUTPUT HIGH10223.2971推挽强拉高,压制 100k 下拉
7OUTPUT_OPEN_DRAIN LOW00.0000开漏拉低
8OUTPUT_OPEN_DRAIN HIGH00.0000开漏高阻 + 仅有 100k 下拉 → 节点仍为低

3.3 R_ext = 100 kΩ,逻辑当作 none(菜单选 3)

注:代码中 extType=EXT_NONE,因此提示为“no external resistor (node floating)”,物理上是否仍接 100k 视实验连线而定。这里按“逻辑视为悬空”来理解。

序号模式ADC电压 (V)digitalRead备注
1INPUT (floating)1280.4131浮空,受杂散电容/泄漏影响,偏低但数字偶判 1
2INPUT_PULLUP8612.7771仅内部上拉,电平接近 VCC
3INPUT_PULLDOWN00.0000仅内部下拉
4INPUT_ANALOG2370.7650模拟浮空,电压在低中区间漂移
5OUTPUT LOW00.0000推挽拉低
6OUTPUT HIGH10223.2971推挽拉高
7OUTPUT_OPEN_DRAIN LOW00.0000开漏拉低
8OUTPUT_OPEN_DRAIN HIGH2380.7680开漏高阻 + 无上拉,下垂到中间偏低

3.4 R_ext = 15 kΩ,接 GND

序号模式ADC电压 (V)digitalRead备注
1INPUT (floating)00.0000外部 15k 下拉很强,直接拉到 0V
2INPUT_PULLUP2760.890015k 下拉明显比内部上拉强,节点偏低;估算 Rup≈40.6 kΩ
3INPUT_PULLDOWN00.0000内部+外部下拉,更低
4INPUT_ANALOG00.0000模拟通道也看到 0V
5OUTPUT LOW00.0000推挽拉低
6OUTPUT HIGH10213.2941推挽拉高,压制 15k 下拉
7OUTPUT_OPEN_DRAIN LOW00.0000开漏拉低
8OUTPUT_OPEN_DRAIN HIGH00.0000开漏高阻 + 15k 下拉 → 节点为低

3.5 R_ext = 15 kΩ,接 VCC

序号模式ADC电压 (V)digitalRead备注
1INPUT (floating)9953.2101外部 15k 上拉很强,几乎 3.3V
2INPUT_PULLUP10133.2681内部+外部上拉并联,更接近满刻度
3INPUT_PULLDOWN7382.3811内部下拉 vs 15k 上拉分压,仍被视为 HIGH
4INPUT_ANALOG9963.2131模拟通道看到 3.2V 左右
5OUTPUT LOW10.0030推挽强拉低,对 15k 上拉也是轻松压制
6OUTPUT HIGH10223.2971推挽拉高,与上拉同向
7OUTPUT_OPEN_DRAIN LOW10.0030开漏拉低
8OUTPUT_OPEN_DRAIN HIGH9953.2101开漏高阻 + 15k 上拉 → 节点接近 3.3V

3.6 R_ext = 15 kΩ,逻辑当作 none(菜单选 3)

同样地,代码逻辑将其视为“无外部电阻”,以下理解为“悬空”场景。

序号模式ADC电压 (V)digitalRead备注
1INPUT (floating)1740.5611浮空偏低,但数字采样到 1(说明门限在中间)
2INPUT_PULLUP8602.7741仅内部上拉,接近 VCC
3INPUT_PULLDOWN00.0000仅内部下拉
4INPUT_ANALOG2320.7480浮空模拟,低中间漂移
5OUTPUT LOW00.0000推挽拉低
6OUTPUT HIGH10223.2971推挽拉高
7OUTPUT_OPEN_DRAIN LOW00.0000开漏拉低
8OUTPUT_OPEN_DRAIN HIGH1800.5810开漏高阻 + 无上拉,电平漂在中间偏低

同样地,代码逻辑将其视为“无外部电阻”,以下理解为“悬空”场景。

序号模式ADC电压 (V)digitalRead备注
1INPUT (floating)1740.5611浮空偏低,但数字采样到 1(说明门限在中间)
2INPUT_PULLUP8602.7741仅内部上拉,接近 VCC
3INPUT_PULLDOWN00.0000仅内部下拉
4INPUT_ANALOG2320.7480浮空模拟,低中间漂移
5OUTPUT LOW00.0000推挽拉低
6OUTPUT HIGH10223.2971推挽拉高
7OUTPUT_OPEN_DRAIN LOW00.0000开漏拉低
8OUTPUT_OPEN_DRAIN HIGH1800.5810开漏高阻 + 无上拉,电平漂在中间偏低

4. GPIO 模式与外部场景对比总表

下面用一张总表,横向对比“同一个 GPIO 模式在不同外部电阻场景下”的典型行为,便于在文章中一眼看出规律。

说明:

  • “低 / 高”指数字电平稳定为 0 / 1;
  • “中间电压”指 ADC 在 0.8–2.5 V 区间,属于分压或浮空状态;
  • “浮空/不稳定”指 ADC 明显在中间且 digitalRead 有抖动可能。
模式 / 场景100 kΩ → GND100 kΩ → VCC浮空(ext=none)15 kΩ → GND15 kΩ → VCC
INPUT (floating)0 V,稳定低,完全由外部下拉决定≈2.3 V,中间偏高,数字判高0.4–0.6 V 浮动,数字判值不稳定0 V,稳定低,下拉很强≈3.2 V,稳定高,上拉很强
INPUT_PULLUP≈2.1 V,中间偏高,数字判高,弱上拉与 100k 下拉分压≈3.0 V,接近 VCC,内部+外部上拉并联更硬≈2.8 V,稳定高,仅内部上拉≈0.9 V,中间偏低,数字已判低,15k 压制内部上拉≈3.27 V,稳定高,内部+15k 上拉都朝上
INPUT_PULLDOWN0 V,稳定低,内部+外部都往下拉≈0.87 V,中间偏低,数字判低0 V,稳定低,仅内部下拉0 V,稳定低,内部+15k 下拉都往下≈2.38 V,中间偏高,数字已判高,15k 压制内部下拉
INPUT_ANALOG0 V,只看到外部下拉≈2.3 V,只看到外部上拉0.7–0.8 V 左右,中间电压,浮空漂移0 V,只看到外部 15k 下拉≈3.21 V,只看到外部 15k 上拉
OUTPUT LOW0 V,强拉低,压制 100k 下拉0 V,强拉低,压制 100k 上拉0 V,强拉低0 V,强拉低,压制 15k 下拉≈0 V,强拉低,压制 15k 上拉
OUTPUT HIGH≈3.30 V,强拉高,压制 100k 下拉≈3.30 V,强拉高,与 100k 上拉同向≈3.30 V,强拉高≈3.29 V,强拉高,压制 15k 下拉≈3.30 V,强拉高,与 15k 上拉同向
OPEN_DRAIN LOW0 V,下管导通,压制外部0 V,下管导通,压制外部0 V,下管导通0 V,下管导通≈0 V,下管导通
OPEN_DRAIN HIGH(高阻)0 V,高阻 + 100k 下拉 → 低≈2.3 V,高阻 + 100k 上拉 → 中间/偏高,高电平0.6–0.8 V,中间电压,浮空漂移,数字多为低0 V,高阻 + 15k 强下拉 → 低≈3.2 V,高阻 + 15k 强上拉 → 稳定高

小结

结合上面的完整表格,可以得出几条在结论(每条都能用上述数据佐证):

  1. 内部上拉电阻量级在几十千欧,可以用“R_ext→GND + ADC 分压”反推出大致范围
    • R_ext=100 kΩ → GND 时推算约 56.7 kΩ,R_ext=15 kΩ → GND 时推算约 40.6 kΩ,印证了“弱上拉”的工程经验。
  2. 外部电阻是“比拼阻值”的游戏:谁阻值小,谁主导节点电平
    • 100 kΩ 下拉 vs 内部上拉:得到中间电压(约 2.1 V);
    • 15 kΩ 下拉 vs 内部上拉:节点被明显拉向 0 V,digitalRead 直接读 0。
  3. 当外部电阻和内部上/下拉连在同一侧电源时,只能增强高/低电平,不能再用来估 R_up
    • 比如 15 kΩ → VCC + INPUT_PULLUP,电压接近满刻度,只能说明“上拉更硬”,不适合再反算内部阻值。
  4. 浮空输入真的会乱飘,模拟值在 0.x V 区间抖动,数字判 0/1 都有可能
    • R_ext=none 的多组数据表明,INPUT/INPUT_ANALOG 下 ADC 在 0.4–0.8 V 对应的计数间漂移,digitalRead 既有 0 也有 1,说明浮空脚高度不可靠。
  5. 推挽输出表现为几乎理想的电压源,轻松压住 15 kΩ、100 kΩ 这类负载
    • 不论 100 kΩ 还是 15 kΩ 接到 VCC 或 GND,OUTPUT HIGH/LOW 电压几乎仍为理想的 0 / 3.3 V,佐证了推挽输出的强驱动能力。
  6. 开漏输出 HIGH 态的“高阻”本质:节点完全等于外部网络的结果
    • OD HIGH + R→GND 得低电平,OD HIGH + R→VCC 得高电平,OD HIGH + none 得中间漂移电平,清楚展示了开漏 + 上拉构成“线与总线”的物理基础。

代码部分

/**
 * @brief 示例主程序
 *
 * 通过编译开关选择运行不同的硬件实验:
 * - LED 自动检测实验(使用 AutoDetect 框架)
 * - GPIO 模式学习实验(使用 GpioModeLab)
 */

#include < PinNames.h >
#include "AutoDetect.h"
#include "GpioModeLab.h"

/**
 * @brief 实验选择开关
 *
 * 将对应宏改为 1/0 以选择要编译运行的实验。
 */
#define DEMO_LED_AUTODETECT   0   ///< LED 自动检测演示
#define DEMO_GPIO_MODE_LAB    1   ///< GPIO 模式学习实验

#if DEMO_LED_AUTODETECT

/***************************************
 *  LED 自动检测演示 (AutoDetect)
 ***************************************/

/**
 * @brief 候选引脚列表
 * 包含所有可能连接LED的引脚,排除已知用途的引脚
 */
const PinName allPins[] = {
  // Port A(去掉 PA_0: probe、PA_2/PA_3: USART2、PA_13/PA_14: SWD)
           PA_1,            PA_4,  PA_5,  PA_6,  PA_7,
  PA_8,  PA_9,  PA_10,    PA_11, PA_12,        PA_15,
  // Port B
  PB_0,  PB_1,  PB_2,  PB_3,  PB_4,  PB_5,  PB_6,  PB_7,
  PB_8,  PB_9,  PB_10, PB_11, PB_12, PB_13, PB_14, PB_15,
  // Port C
  PC_0,  PC_1,  PC_2,  PC_3,  PC_4,  PC_5,  PC_6,  PC_7,
  PC_8,  PC_9,  PC_10, PC_11, PC_12, PC_13, PC_14, PC_15,
  // Port F
  PF_0,  PF_1,  PF_2,  PF_3,  PF_4,  PF_5,  PF_6,  PF_7,
  PF_8,  PF_9,  PF_10, PF_11, PF_12, PF_13, PF_14, PF_15
};

/**
 * @brief 排除引脚列表
 * 包含串口、SWD调试接口等不能用于LED控制的引脚
 */
const PinName excludedPins[] = {
  PA_2, PA_3,    // USART2 (Serial2)
  PA_13, PA_14,  // SWD
  PC_0           // 已知 LED=PC_0,避免再次测试
};

/// @brief 候选引脚总数
const int ALL_PIN_COUNT      = sizeof(allPins) / sizeof(allPins[0]);

/// @brief 排除引脚总数
const int EXCLUDED_PIN_COUNT = sizeof(excludedPins) / sizeof(excludedPins[0]);

/// @brief 探头引脚,用于检测LED连接状态
const PinName PROBE_PIN = PA_0;

/**
 * @brief LED专用自动检测类
 * 继承自AutoDetect框架,专门用于检测LED引脚并演示闪烁效果
 */
class LedDetect : public AutoDetect {
public:
  LedDetect(const PinName* pins,
            int pinCount,
            const PinName* excluded,
            int excludedCount,
            PinName probePin,
            Stream& log)
    : AutoDetect(pins, pinCount, excluded, excludedCount, probePin, log)
  {}

protected:
  /// @brief 打印 LED 自动检测的头信息。
  void logHeader() override {
    log_.println("LED pin auto-detect (AutoDetect framework)");
    log_.println("Please connect PA0 to LED low-side pad.");
  }

  /// @brief 找到 LED 引脚后,在已找到状态下循环闪灯(低电平亮)。
  void loopOnFound(PinName pin) override {
    digitalWrite((int)pin, LOW);   // 亮
    delay(300);
    digitalWrite((int)pin, HIGH);  // 灭
    delay(300);
  }

  /// @brief 找到 LED 引脚瞬间的动作:释放其它引脚,只保留 LED 为输出。
  void onFound(PinName pin) override {
    // 1) 把所有候选脚恢复为输入,避免继续强推导致发热
    for (int i = 0; i < pinCount_; ++i) {
      if (isExcluded(pins_[i])) continue;
      pinMode((int)pins_[i], INPUT);
    }
    // 2) 只保留 LED 引脚为输出,高电平默认灭(低电平亮)
    pinMode((int)pin, OUTPUT);
    digitalWrite((int)pin, HIGH);

    foundPin_ = pin;
  }
};

/// @brief 全局检测器指针,指向具体的检测器实例
AutoDetect* g_detector = nullptr;

/**
 * @brief setup:初始化串口通信和LED检测器
 */
void setup() {
  Serial2.begin(115200);
  delay(100);

  static LedDetect ledDetector(allPins,
                               ALL_PIN_COUNT,
                               excludedPins,
                               EXCLUDED_PIN_COUNT,
                               PROBE_PIN,
                               Serial2);
  g_detector = &ledDetector;

  g_detector- >begin();
}

/**
 * @brief loop:执行LED引脚检测和闪烁演示
 */
void loop() {
  if (g_detector) {
    g_detector- >update();
  }
}

#elif DEMO_GPIO_MODE_LAB

/***************************************
 *  GPIO 模式学习实验 (GpioModeLab)
 ***************************************/

#include "MiniShell.h"

const PinName TARGET_PIN    = PB_1;
const PinName PROBE_ADC_PIN = PA_0;

GpioModeLab gpioLab(TARGET_PIN, PROBE_ADC_PIN, Serial2);
MiniShell   shell(Serial2);

static bool g_running = false;  // 当前是否在跑一轮实验
static bool g_quit    = false;  // 全局退出标志

/**
 * @brief 做一次“配置 + gpioLab.begin()”。
 * @return true  正常开始一轮实验
 *         false 用户在任意输入阶段按 q/Q,要求退出所有实验
 */
static bool configureExperimentOnce() {
  Serial2.println("========================================");
  Serial2.println(" GPIO Mode Lab - external resistor configuration");
  Serial2.println("  (press 'q' at any time to quit)");
  Serial2.println("----------------------------------------");
  Serial2.println("Connect TARGET_PIN < - > PROBE_ADC_PIN with a wire.");
  Serial2.println();
  Serial2.println("Step 1: input external resistor value (Ohm).");
  Serial2.println("        Enter 0 if no external resistor.");
  Serial2.print  ("R_ext (Ohm) = ");

  long r = 0;
  if (shell.readUInt(r) == MiniShell::QUIT) {
    Serial2.println("Quit requested.");
    return false;
  }
  Serial2.println();  // 换行

  GpioModeLab::ExternalNodeType type = GpioModeLab::EXT_NONE;

  if (r <= 0) {
    Serial2.println("No external resistor will be used (floating node).");
  } else {
    Serial2.println();
    Serial2.println("Step 2: choose how this resistor is connected:");
    Serial2.println("  [1] node - > R - > GND");
    Serial2.println("  [2] node - > R - > VCC");
    Serial2.println("  [3] ignore resistor (treat as none)");
    Serial2.print  ("Select 1/2/3 (or 'q' to quit): ");

    char sel = 0;
    if (shell.readMenuKey("123", sel) == MiniShell::QUIT) {
      Serial2.println("Quit requested.");
      return false;
    }

    if      (sel == '1') type = GpioModeLab::EXT_TO_GND;
    else if (sel == '2') type = GpioModeLab::EXT_TO_VCC;
    else                 type = GpioModeLab::EXT_NONE;
  }

  gpioLab.setExternal((float)r, type);
  gpioLab.begin();
  return true;
}

/**
 * @brief setup:打印总说明。
 */
void setup() {
  Serial2.begin(115200);
  delay(100);

  Serial2.println("GPIO Mode Lab - multi-run demo");
  Serial2.println("Use this firmware to learn GPIO modes.");
  Serial2.println("At ANY time, press 'q' or 'Q' to quit all experiments.");
  Serial2.println();
}

/**
 * @brief loop:
 *  - 没有在跑实验时:弹出菜单配置一轮;可多轮;
 *  - 正在跑实验时:调用 gpioLab.update() 推进;
 *  - 任意阶段输入 q/Q:在 MiniShell 里统一处理,结束循环。
 */
void loop() {
  if (g_quit) {
    return;
  }

  if (!g_running) {
    bool ok = configureExperimentOnce();
    if (!ok) {
      g_quit = true;
      Serial2.println("nExperiment loop stopped by user.");
      return;
    }
    g_running = true;
    return;
  }

  gpioLab.update();

  if (gpioLab.isFinished()) {
    g_running = false;
    Serial2.println("n=== One experiment round finished. ===");
    Serial2.println("You can start a new configuration, or press 'q' at any prompt to quit.");
    Serial2.println();
    delay(500);
  }
}

#else

#warning "No demo enabled. Set DEMO_LED_AUTODETECT or DEMO_GPIO_MODE_LAB to 1."

void setup() {}
void loop()  {}

#endif
#include "GpioModeLab.h"

/// ADC 满量程值(10 位 ADC:0..1023)
static const int   ADC_MAX_COUNTS = 1023;
/// 参考电压(按 3.3V 算)
static const float ADC_VREF       = 3.3f;

/**
 * @brief 本实验中要依次测试的 GPIO 模式列表。
 *
 * - name       : 模式名称(日志打印用)
 * - mode       : 传给 pinMode 的模式值
 * - isOutput   : 是否为输出模式
 * - driveLevel : 输出模式下的电平(0=LOW,1=HIGH,-1=不驱动)
 */
static const GpioModeLab::ModeTest kModeTests[] = {
  { "INPUT (floating)",        INPUT,             false, -1 },
  { "INPUT_PULLUP",            INPUT_PULLUP,      false, -1 },
  { "INPUT_PULLDOWN",          INPUT_PULLDOWN,    false, -1 },
  { "INPUT_ANALOG",            INPUT_ANALOG,      false, -1 },
  { "OUTPUT LOW",              OUTPUT,            true,   0 },
  { "OUTPUT HIGH",             OUTPUT,            true,   1 },
  { "OUTPUT_OPEN_DRAIN LOW",   OUTPUT_OPEN_DRAIN, true,   0 },
  { "OUTPUT_OPEN_DRAIN HIGH",  OUTPUT_OPEN_DRAIN, true,   1 },
};

GpioModeLab::GpioModeLab(PinName targetPin,
                         PinName probeAdcPin,
                         Stream& log)
  : targetPin_(targetPin),
    probeAdcPin_(probeAdcPin),
    log_(log),
    tests_(kModeTests),
    testCount_(sizeof(kModeTests) / sizeof(kModeTests[0])),
    currentIndex_(0),
    finished_(false),
    extResOhms_(0.0f),
    extType_(EXT_NONE)
{}

void GpioModeLab::setExternal(float resistorOhms, ExternalNodeType type) {
  if (resistorOhms <= 0.0f || type == EXT_NONE) {
    extResOhms_ = 0.0f;
    extType_    = EXT_NONE;
  } else {
    extResOhms_ = resistorOhms;
    extType_    = type;
  }
}

void GpioModeLab::begin() {
  finished_     = false;
  currentIndex_ = 0;

  log_.println("========================================");
  log_.println(" GPIO Mode Lab - learn GPIO modes");
  log_.println("  - targetPin  : PB1 (default in sketch)");
  log_.println("  - probeAdcPin: PA0 (ADC)");
  log_.println("Please connect TARGET_PIN < - > PROBE_ADC_PIN with a wire.");

  log_.print("External wiring: ");
  if (extType_ == EXT_NONE || extResOhms_ <= 0.0f) {
    log_.println("no external resistor (node floating).");
  } else if (extType_ == EXT_TO_GND) {
    log_.print("node - > ");
    log_.print(extResOhms_, 0);
    log_.println(" Ohm - > GND.");
  } else if (extType_ == EXT_TO_VCC) {
    log_.print("node - > ");
    log_.print(extResOhms_, 0);
    log_.println(" Ohm - > VCC.");
  }
  log_.println("========================================");
}

/**
 * @brief 对探头 ADC 引脚进行多次采样并取平均值。
 */
int GpioModeLab::readAdcAverage(uint8_t samples) {
  long sum = 0;
  pinMode((int)probeAdcPin_, INPUT_ANALOG);
  delay(2);

  for (uint8_t i = 0; i < samples; ++i) {
    sum += analogRead((int)probeAdcPin_);
    delay(2);
  }
  return (int)(sum / samples);
}

/**
 * @brief 根据 ADC 原始值粗略判断电平状态。
 */
int GpioModeLab::classifyLevel(int rawAdc) const {
  const int lowTh  = ADC_MAX_COUNTS / 4;       // ≈ 256
  const int highTh = (ADC_MAX_COUNTS * 3) / 4; // ≈ 768

  if (rawAdc < lowTh)  return 0; // LOW
  if (rawAdc > highTh) return 1; // HIGH
  return 2;                      // MID / unknown
}

/**
 * @brief 打印单次模式测试的结果,并给出英文说明。
 */
void GpioModeLab::printResult(const ModeTest& t,
                              int rawAdc,
                              int digitalLevel,
                              int idx)
{
  int cls = classifyLevel(rawAdc);

  log_.print("n[");
  log_.print(idx + 1);
  log_.print("/");
  log_.print(testCount_);
  log_.print("] Mode = ");
  log_.println(t.name);

  float volts = (float)rawAdc * ADC_VREF / (float)ADC_MAX_COUNTS;

  log_.print("  ADC avg   = ");
  log_.print(rawAdc);
  log_.print("  (");
  log_.print(volts, 3);
  log_.print(" V)  - > ");

  if (cls == 0)      log_.print("LOW");
  else if (cls == 1) log_.print("HIGH");
  else               log_.print("MID/unknown");

  log_.print("n  digitalRead = ");
  log_.print(digitalLevel);
  log_.println();

  // 简单英文说明(详细中文可以看源码注释)
  log_.print("  info: ");
  if (!t.isOutput && t.mode == INPUT && t.driveLevel < 0) {
    log_.println("Digital input, floating (no pull). High impedance, level decided by external circuit.");
  } else if (!t.isOutput && t.mode == INPUT_PULLUP) {
    log_.println("Digital input with internal pull-up (~tens of kOhm) to VCC. Good for buttons, avoids floating.");
  } else if (!t.isOutput && t.mode == INPUT_PULLDOWN) {
    log_.println("Digital input with internal pull-down to GND. Default level is low.");
  } else if (!t.isOutput && t.mode == INPUT_ANALOG) {
    log_.println("Analog input: digital buffer off, only ADC path enabled. Used for ADC measurement.");
  } else if (t.isOutput && t.mode == OUTPUT && t.driveLevel == 0) {
    log_.println("Push-pull output, driving LOW. Strongly sinks current to GND.");
  } else if (t.isOutput && t.mode == OUTPUT && t.driveLevel == 1) {
    log_.println("Push-pull output, driving HIGH. Strongly sources current to VCC.");
  } else if (t.isOutput && t.mode == OUTPUT_OPEN_DRAIN && t.driveLevel == 0) {
    log_.println("Open-drain output, pulling LOW (transistor to GND on).");
  } else if (t.isOutput && t.mode == OUTPUT_OPEN_DRAIN && t.driveLevel == 1) {
    log_.println("Open-drain output, HIGH = high-Z. Level decided by external pull-up/down.");
  } else {
    log_.println("Uncategorized mode.");
  }

  // 只有在“节点通过已知电阻接 GND + INPUT_PULLUP”时,用分压估算内部上拉电阻
  if (!t.isOutput &&
      t.mode == INPUT_PULLUP &&
      extType_ == EXT_TO_GND &&
      extResOhms_ > 0.0f &&
      rawAdc > 0 &&
      rawAdc < ADC_MAX_COUNTS) {

    float ratio = (float)rawAdc / (float)ADC_MAX_COUNTS; // Vnode / Vref
    // Vnode / Vref = Rdown / (Rup + Rdown)  = >  Rup = Rdown * (1/ratio - 1)
    float rup   = extResOhms_ * (1.0f / ratio - 1.0f);

    log_.print("  Est. internal pull-up ~= ");
    if (rup > 1000.0f) {
      log_.print(rup / 1000.0f, 1);
      log_.println(" kOhm");
    } else {
      log_.print(rup, 1);
      log_.println(" Ohm");
    }
  }
}

/**
 * @brief 执行下一步模式测试。
 */
void GpioModeLab::update() {
  if (finished_) {
    delay(200);
    return;
  }

  if (currentIndex_ >= testCount_) {
    finished_ = true;
    log_.println("nAll GPIO mode tests finished.");
    return;
  }

  const ModeTest& t = tests_[currentIndex_];

  // 1. 配置目标引脚模式
  pinMode((int)targetPin_, t.mode);
  delay(5);

  // 2. 若为输出模式,设置输出电平
  if (t.isOutput && t.driveLevel >= 0) {
    digitalWrite((int)targetPin_, t.driveLevel ? HIGH : LOW);
  }

  delay(10); // 等待电平稳定

  // 3. 采样 ADC 平均值
  int rawAdc = readAdcAverage(16);

  // 4. 再以数字输入方式读取一次
  pinMode((int)probeAdcPin_, INPUT);
  delay(2);
  int dig = digitalRead((int)probeAdcPin_);

  // 5. 打印结果
  printResult(t, rawAdc, dig, currentIndex_);

  currentIndex_++;
  delay(300); // 模式之间稍作停顿
}
#pragma once

#include < Arduino.h >
#include < PinNames.h >

/**
 * @brief GPIO 模式实验:在一个目标引脚上依次配置不同模式,
 *        并通过一个 ADC 探头引脚采样电压,用于学习各模式的差异。
 */
class GpioModeLab {
public:
  /**
   * @brief 单个模式测试描述。
   */
  struct ModeTest {
    const char* name;      ///< 模式名称
    uint8_t     mode;      ///< pinMode 使用的模式值
    bool        isOutput;  ///< 是否为输出模式
    int         driveLevel;///< 输出电平:0=LOW,1=HIGH,-1=不驱动
  };

  /**
   * @brief 外部接线类型
   *
   * - EXT_NONE   : 节点无额外电阻
   * - EXT_TO_GND : 节点 - > R - > GND
   * - EXT_TO_VCC : 节点 - > R - > VCC
   */
  enum ExternalNodeType {
    EXT_NONE = 0,
    EXT_TO_GND,
    EXT_TO_VCC
  };

  /**
   * @brief 构造函数
   * @param targetPin   被测试的 GPIO 引脚(会被配置为各种模式)
   * @param probeAdcPin 用于 ADC 采样的探头引脚(只读电压)
   * @param log         日志输出流(例如 Serial2)
   */
  GpioModeLab(PinName targetPin, PinName probeAdcPin, Stream& log);

  /**
   * @brief 配置外部电阻及其连接方式。
   *
   * @param resistorOhms 电阻值(单位:欧姆)。<=0 表示无电阻。
   * @param type         连接方式:EXT_NONE / EXT_TO_GND / EXT_TO_VCC
   */
  void setExternal(float resistorOhms, ExternalNodeType type);

  /**
   * @brief 初始化实验(在 setup() 中调用)。
   */
  void begin();

  /**
   * @brief 运行实验的下一步(在 loop() 中反复调用)。
   */
  void update();

  /**
   * @brief 实验是否已经完成所有模式测试。
   */
  bool isFinished() const { return finished_; }

private:
  PinName targetPin_;
  PinName probeAdcPin_;
  Stream& log_;

  const ModeTest* tests_;
  int             testCount_;
  int             currentIndex_;
  bool            finished_;

  // 外部电阻配置
  float            extResOhms_; ///< 外接电阻值(欧姆),0 表示无
  ExternalNodeType extType_;    ///< 外接电阻接到哪:GND / VCC / none

  int  readAdcAverage(uint8_t samples);
  int  classifyLevel(int rawAdc) const;
  void printResult(const ModeTest& t, int rawAdc, int digitalLevel, int idx);
};
#include "MiniShell.h"

MiniShell::Result MiniShell::readLine(char* buf, size_t len) {
  size_t idx = 0;

  while (true) {
    if (!io_.available()) continue;

    char c = io_.read();

    // 全局退出命令
    if (c == 'q' || c == 'Q') {
      io_.println();
      return QUIT;
    }

    // 回车/换行:一行结束
    if (c == 'r' || c == 'n') {
      if (idx > 0) {
        buf[idx] = '�';
        return OK;
      }
      // 空行则继续读
      continue;
    }

    // 退格
    if (c == 'b' || c == 127) {
      if (idx > 0) {
        idx--;
        io_.print("b b");
      }
      continue;
    }

    // 普通字符,回显并存入缓冲
    if (idx < len - 1) {
      buf[idx++] = c;
      io_.print(c);
    }
  }
}

MiniShell::Result MiniShell::readUInt(long& value) {
  char line[16];
  Result r = readLine(line, sizeof(line));
  if (r != OK) return r;
  value = atol(line);
  return OK;
}

MiniShell::Result MiniShell::readMenuKey(const char* valid, char& outKey) {
  while (true) {
    if (!io_.available()) continue;
    char c = io_.read();

    if (c == 'q' || c == 'Q') {
      io_.println();
      return QUIT;
    }

    // 检查是否在候选集合中
    for (const char* p = valid; *p; ++p) {
      if (c == *p) {
        io_.println(c);
        outKey = c;
        return OK;
      }
    }
  }
}
#pragma once

#include < Arduino.h >

/**
 * @brief 串口迷你命令行:统一处理行输入、菜单输入和 q/Q 退出。
 */
class MiniShell {
public:
  /// 输入结果
  enum Result {
    OK = 0,   ///< 正常返回
    QUIT      ///< 用户输入 q/Q 请求退出
  };

  explicit MiniShell(Stream& io) : io_(io) {}

  /**
   * @brief 读取一整行文本(回车结束),支持退格、回显。
   *        若过程中收到 q/Q,则返回 QUIT。
   *
   * @param buf 缓冲区
   * @param len 缓冲区长度
   */
  Result readLine(char* buf, size_t len);

  /**
   * @brief 读取一个正整数(十进制),回车结束。
   *        若过程中收到 q/Q,则返回 QUIT。
   *
   * @param value 输出的整数
   */
  Result readUInt(long& value);

  /**
   * @brief 从给定候选集合中读取一个按键(例如 "123"),
   *        若收到 q/Q,则返回 QUIT。
   *
   * @param valid    C 字符串,如 "123"
   * @param outKey   输出选中的字符
   */
  Result readMenuKey(const char* valid, char& outKey);

private:
  Stream& io_;
};
#pragma once

#include < Arduino.h >
#include < PinNames.h >

/**
 * @brief 通用自动检测框架
 *
 * 该类提供了一个通用的引脚自动检测框架,通过扫描候选引脚列表,
 * 使用探头引脚检测特定条件,自动识别匹配的引脚。
 * 支持排除特定引脚、自定义匹配规则和日志输出。
 */
class AutoDetect {
public:
  /**
   * @brief 构造函数
   *
   * @param pins 候选引脚列表
   * @param pinCount 候选引脚数量
   * @param excluded 排除引脚列表
   * @param excludedCount 排除引脚数量
   * @param probePin 探头引脚
   * @param log 日志输出流
   */
  AutoDetect(const PinName* pins,
             int pinCount,
             const PinName* excluded,
             int excludedCount,
             PinName probePin,
             Stream& log);

  /// @brief 析构函数
  virtual ~AutoDetect() {}

  /// @brief 初始化检测过程,在setup()中调用
  void begin();

  /// @brief 执行检测循环,在loop()中反复调用
  void update();

  /// @brief 检查检测是否完成
  /// @return true如果扫描完成,false如果仍在进行
  bool isFinished() const { return finished_; }

  /// @brief 检查是否找到匹配引脚
  /// @return true如果找到匹配引脚,false否则
  bool isFound()    const { return found_; }

  /// @brief 获取找到的匹配引脚
  /// @return 匹配的引脚名,如果未找到返回NC
  PinName getFoundPin() const { return foundPin_; }

protected:
  // —— 可在子类中按需覆写的钩子 —— //

  /// @brief 开始检测时打印头信息
  virtual void logHeader();

  /// @brief 测试引脚前打印信息
  virtual void logTestingPin(PinName pin);

  /// @brief 打印探头读取的电平值
  virtual void logProbeValues(int vLow, int vHigh);

  /// @brief 打印检测进度条
  virtual void logProgress(int index, int count);

  /// @brief 找到匹配引脚时打印信息
  virtual void logFound(PinName pin);

  /// @brief 扫描完成但未找到匹配时打印信息
  virtual void logFinishedNoFound();

  /// @brief 判断是否为匹配引脚,默认规则:探头电平变化
  /// @return true如果匹配,false否则
  virtual bool isMatch(int vLow, int vHigh, PinName pin);

  /**
   * @brief 找到匹配引脚时调用的钩子函数,由子类实现具体行为。
   *
   * @param pin 被判定为匹配的引脚。
   */
  virtual void onFound(PinName pin) = 0;

  /// @brief 已找到匹配引脚后的循环行为,默认空实现
  virtual void loopOnFound(PinName pin);

  // 工具函数
  /// @brief 检查引脚是否在排除列表中
  /// @return true如果被排除,false否则
  bool isExcluded(PinName p) const;

  /// @brief 获取引脚的端口字符(A, B, C等)
  /// @return 端口字符
  char portChar(PinName p) const;

  /// @brief 获取引脚的编号(0-15)
  /// @return 引脚编号
  int  pinNumber(PinName p) const;

protected:
  const PinName* pins_;
  int            pinCount_;
  const PinName* excluded_;
  int            excludedCount_;
  PinName        probePin_;
  Stream&        log_;

  int     index_;
  bool    found_;
  bool    finished_;
  PinName foundPin_;

  static const int PROGRESS_BAR_LEN = 20;
};
#include "AutoDetect.h"

AutoDetect::AutoDetect(const PinName* pins,
                       int pinCount,
                       const PinName* excluded,
                       int excludedCount,
                       PinName probePin,
                       Stream& log)
  : pins_(pins),
    pinCount_(pinCount),
    excluded_(excluded),
    excludedCount_(excludedCount),
    probePin_(probePin),
    log_(log),
    index_(0),
    found_(false),
    finished_(false),
    foundPin_(NC)
{}

/// @brief 检查引脚是否在排除列表中,包括探头引脚
bool AutoDetect::isExcluded(PinName p) const {
  if (p == probePin_) return true;
  for (int i = 0; i < excludedCount_; i++) {
    if (excluded_[i] == p) return true;
  }
  return false;
}

/// @brief 获取引脚的端口字符,从PinName的高4位提取
char AutoDetect::portChar(PinName p) const {
  return 'A' + (p > > 4);  // 高 4 位:port 号
}

/// @brief 获取引脚的编号,从PinName的低4位提取
int AutoDetect::pinNumber(PinName p) const {
  return p & 0xF;         // 低 4 位:pin 号
}

// —— 默认日志/行为实现,可在子类中覆写 —— //

/// @brief 默认实现:打印检测开始信息
void AutoDetect::logHeader() {
  log_.println("AutoDetect start");
}

/// @brief 默认实现:打印正在测试的引脚信息
void AutoDetect::logTestingPin(PinName pin) {
  log_.print("nTesting pin: ");
  log_.print(portChar(pin));
  log_.print(pinNumber(pin));
}

/// @brief 默认实现:打印探头读取的电平值
void AutoDetect::logProbeValues(int vLow, int vHigh) {
  log_.print("  probe LOW/HIGH = ");
  log_.print(vLow);
  log_.print(" / ");
  log_.println(vHigh);
}

/// @brief 默认实现:打印检测进度条
void AutoDetect::logProgress(int index, int count) {
  if (count <= 0) return;

  float ratio  = (float)index / (float)count;
  int   filled = (int)(ratio * PROGRESS_BAR_LEN);

  log_.print("r[");
  for (int i = 0; i < PROGRESS_BAR_LEN; i++) {
    if (i < filled) log_.print("#");
    else            log_.print(".");
  }
  log_.print("] ");
  log_.print(index);
  log_.print("/");
  log_.print(count);
  log_.print("   ");
}

/// @brief 默认实现:打印找到的匹配引脚信息
void AutoDetect::logFound(PinName pin) {
  log_.print("n >> > Pin FOUND: ");
  log_.print(portChar(pin));
  log_.println(pinNumber(pin));
}

/// @brief 默认实现:打印扫描完成但未找到匹配的信息
void AutoDetect::logFinishedNoFound() {
  log_.println("nOne full round finished, no match found.");
}

/// @brief 默认匹配规则:探头电平发生变化即认为匹配
bool AutoDetect::isMatch(int vLow, int vHigh, PinName /*pin*/) {
  // 默认规则:探头在此引脚高低切换时也发生变化
  return (vLow != vHigh);
}

/// @brief 默认实现:找到匹配后无特殊行为,由子类覆写
void AutoDetect::loopOnFound(PinName /*pin*/) {
  // 默认什么也不做,由子类决定
}

// —— 生命周期 —— //

/// @brief 初始化检测过程,设置引脚模式和初始状态
void AutoDetect::begin() {
  // 注意:日志串口应在外部先 begin(),这里只做逻辑打印
  index_    = 0;
  found_    = false;
  finished_ = false;
  foundPin_ = NC;

  logHeader();

  // 探头脚输入,下拉
  pinMode((int)probePin_, INPUT_PULLDOWN);

  // 所有候选引脚设置为输出高电平(不包括排除项)
  for (int i = 0; i < pinCount_; i++) {
    PinName p = pins_[i];
    if (isExcluded(p)) continue;
    pinMode((int)p, OUTPUT);
    digitalWrite((int)p, HIGH); // 假设"低电平有效",默认关闭
  }
}

/// @brief 执行检测循环的主要逻辑
void AutoDetect::update() {
  if (found_) {
    // 已找到:由子类决定在“已找到状态”下如何循环
    loopOnFound(foundPin_);
    return;
  }

  if (finished_) {
    // 已扫描完但没找到:空转即可
    delay(100);
    return;
  }

  // 跳过所有被排除的引脚
  while (index_ < pinCount_ && isExcluded(pins_[index_])) {
    index_++;
  }

  if (index_ >= pinCount_) {
    finished_ = true;
    logFinishedNoFound();
    return;
  }

  PinName testPin = pins_[index_];

  // 日志:正在测试哪个脚
  logTestingPin(testPin);

  // 拉低
  digitalWrite((int)testPin, LOW);
  delay(20);
  int vLow = digitalRead((int)probePin_);

  // 拉高
  digitalWrite((int)testPin, HIGH);
  delay(20);
  int vHigh = digitalRead((int)probePin_);

  // 日志:探头电平
  logProbeValues(vLow, vHigh);

  // 日志:进度条
  logProgress(index_ + 1, pinCount_);

  // 判断是否匹配
  if (isMatch(vLow, vHigh, testPin)) {
    found_    = true;
    foundPin_ = testPin;
    logFound(foundPin_);
    onFound(foundPin_);
    return;
  }

  index_++;
}
温馨提示:以上内容整理于网络,仅供参考,如果对您有帮助,留下您的阅读感言吧!
相关阅读
本类排行
相关标签
本类推荐

CPU | 内存 | 硬盘 | 显卡 | 显示器 | 主板 | 电源 | 键鼠 | 网站地图

Copyright © 2025-2035 诺佳网 版权所有 备案号:赣ICP备2025066733号
本站资料均来源互联网收集整理,作品版权归作者所有,如果侵犯了您的版权,请跟我们联系。

关注微信