时间:2026-01-24 11:45
人气:
作者:admin
@[toc]


摘自:STM32F10x 参考手册

本实验通过一个“小实验框架 GPIO Mode Lab”,在同一个 GPIO 引脚上依次配置不同模式,并用 ADC 探头测量电压、同时读取数字电平,系统化地观察:
并由此得出一系列可量化的直观结论,可写成一篇“GPIO 行为直观实验报告”。
整体分为三层:
INPUT、INPUT_PULLUP、INPUT_PULLDOWN、INPUT_ANALOGOUTPUT LOW / OUTPUT HIGHOUTPUT_OPEN_DRAIN LOW / OUTPUT_OPEN_DRAIN HIGHR_ext(单位 Ω,支持 0 表示无)node → R → GND / VCC / noneq/Q 退出。INPUT_PULLUP + R_ext→GND)使用分压公式: Vnode / Vref = R_down / (R_up + R_down)R_up = R_down * (1 / ratio - 1),其中 ratio = ADC / 1023。0..1023,Vref ≈ 3.3 V。pinMode(target, mode) 配置模式digitalWrite 设为 HIGH/LOWanalogRead(probe) 多次采样取平均 → 得到 ADC avg 和电压INPUT,digitalRead 一次 → 观察数字门限行为通过串口交互设置:
R_ext(示例:100 kΩ、15 kΩ;输入整数:100000、15000)node -> R -> GND / VCC / none本次记录的数据覆盖六种典型组合:
R_ext = 100 kΩ,接 VCCR_ext = 100 kΩ,接 GNDR_ext = 100 kΩ,逻辑上当作 none(菜单选 3)R_ext = 15 kΩ,接 VCCR_ext = 15 kΩ,接 GNDR_ext = 15 kΩ,逻辑上当作 none(菜单选 3)| 序号 | 模式 | ADC | 电压 (V) | digitalRead | 备注 |
|---|---|---|---|---|---|
| 1 | INPUT (floating) | 715 | 2.306 | 1 | 外部 100k 上拉占主导,输入脚高阻跟着被拉高 |
| 2 | INPUT_PULLUP | 917 | 2.958 | 1 | 内部上拉与外部 100k 上拉并联,更接近 VCC |
| 3 | INPUT_PULLDOWN | 268 | 0.865 | 0 | 内部下拉 vs 外部上拉分压,电平在中间偏低 |
| 4 | INPUT_ANALOG | 713 | 2.300 | 1 | 数字缓冲关掉,模拟只看到外部 100k 上拉 |
| 5 | OUTPUT LOW | 0 | 0.000 | 0 | 推挽强拉低,压制 100k 上拉 |
| 6 | OUTPUT HIGH | 1022 | 3.297 | 1 | 推挽强拉高,与上拉同向 |
| 7 | OUTPUT_OPEN_DRAIN LOW | 0 | 0.000 | 0 | 开漏下管导通,压制外部上拉 |
| 8 | OUTPUT_OPEN_DRAIN HIGH | 715 | 2.306 | 1 | 开漏高阻,完全由 100k 上拉决定 |
| 序号 | 模式 | ADC | 电压 (V) | digitalRead | 备注 |
|---|---|---|---|---|---|
| 1 | INPUT (floating) | 0 | 0.000 | 0 | 外部 100k 下拉占主导 |
| 2 | INPUT_PULLUP | 653 | 2.106 | 1 | 内部上拉 vs 100k 下拉分压,估算 Rup≈56.7 kΩ |
| 3 | INPUT_PULLDOWN | 0 | 0.000 | 0 | 内部+外部下拉叠加,牢牢在 0V |
| 4 | INPUT_ANALOG | 0 | 0.000 | 0 | 模拟输入也只看到外部下拉 |
| 5 | OUTPUT LOW | 0 | 0.000 | 0 | 推挽强拉低 |
| 6 | OUTPUT HIGH | 1022 | 3.297 | 1 | 推挽强拉高,压制 100k 下拉 |
| 7 | OUTPUT_OPEN_DRAIN LOW | 0 | 0.000 | 0 | 开漏拉低 |
| 8 | OUTPUT_OPEN_DRAIN HIGH | 0 | 0.000 | 0 | 开漏高阻 + 仅有 100k 下拉 → 节点仍为低 |
注:代码中
extType=EXT_NONE,因此提示为“no external resistor (node floating)”,物理上是否仍接 100k 视实验连线而定。这里按“逻辑视为悬空”来理解。
| 序号 | 模式 | ADC | 电压 (V) | digitalRead | 备注 |
|---|---|---|---|---|---|
| 1 | INPUT (floating) | 128 | 0.413 | 1 | 浮空,受杂散电容/泄漏影响,偏低但数字偶判 1 |
| 2 | INPUT_PULLUP | 861 | 2.777 | 1 | 仅内部上拉,电平接近 VCC |
| 3 | INPUT_PULLDOWN | 0 | 0.000 | 0 | 仅内部下拉 |
| 4 | INPUT_ANALOG | 237 | 0.765 | 0 | 模拟浮空,电压在低中区间漂移 |
| 5 | OUTPUT LOW | 0 | 0.000 | 0 | 推挽拉低 |
| 6 | OUTPUT HIGH | 1022 | 3.297 | 1 | 推挽拉高 |
| 7 | OUTPUT_OPEN_DRAIN LOW | 0 | 0.000 | 0 | 开漏拉低 |
| 8 | OUTPUT_OPEN_DRAIN HIGH | 238 | 0.768 | 0 | 开漏高阻 + 无上拉,下垂到中间偏低 |
| 序号 | 模式 | ADC | 电压 (V) | digitalRead | 备注 |
|---|---|---|---|---|---|
| 1 | INPUT (floating) | 0 | 0.000 | 0 | 外部 15k 下拉很强,直接拉到 0V |
| 2 | INPUT_PULLUP | 276 | 0.890 | 0 | 15k 下拉明显比内部上拉强,节点偏低;估算 Rup≈40.6 kΩ |
| 3 | INPUT_PULLDOWN | 0 | 0.000 | 0 | 内部+外部下拉,更低 |
| 4 | INPUT_ANALOG | 0 | 0.000 | 0 | 模拟通道也看到 0V |
| 5 | OUTPUT LOW | 0 | 0.000 | 0 | 推挽拉低 |
| 6 | OUTPUT HIGH | 1021 | 3.294 | 1 | 推挽拉高,压制 15k 下拉 |
| 7 | OUTPUT_OPEN_DRAIN LOW | 0 | 0.000 | 0 | 开漏拉低 |
| 8 | OUTPUT_OPEN_DRAIN HIGH | 0 | 0.000 | 0 | 开漏高阻 + 15k 下拉 → 节点为低 |
| 序号 | 模式 | ADC | 电压 (V) | digitalRead | 备注 |
|---|---|---|---|---|---|
| 1 | INPUT (floating) | 995 | 3.210 | 1 | 外部 15k 上拉很强,几乎 3.3V |
| 2 | INPUT_PULLUP | 1013 | 3.268 | 1 | 内部+外部上拉并联,更接近满刻度 |
| 3 | INPUT_PULLDOWN | 738 | 2.381 | 1 | 内部下拉 vs 15k 上拉分压,仍被视为 HIGH |
| 4 | INPUT_ANALOG | 996 | 3.213 | 1 | 模拟通道看到 3.2V 左右 |
| 5 | OUTPUT LOW | 1 | 0.003 | 0 | 推挽强拉低,对 15k 上拉也是轻松压制 |
| 6 | OUTPUT HIGH | 1022 | 3.297 | 1 | 推挽拉高,与上拉同向 |
| 7 | OUTPUT_OPEN_DRAIN LOW | 1 | 0.003 | 0 | 开漏拉低 |
| 8 | OUTPUT_OPEN_DRAIN HIGH | 995 | 3.210 | 1 | 开漏高阻 + 15k 上拉 → 节点接近 3.3V |
同样地,代码逻辑将其视为“无外部电阻”,以下理解为“悬空”场景。
| 序号 | 模式 | ADC | 电压 (V) | digitalRead | 备注 |
|---|---|---|---|---|---|
| 1 | INPUT (floating) | 174 | 0.561 | 1 | 浮空偏低,但数字采样到 1(说明门限在中间) |
| 2 | INPUT_PULLUP | 860 | 2.774 | 1 | 仅内部上拉,接近 VCC |
| 3 | INPUT_PULLDOWN | 0 | 0.000 | 0 | 仅内部下拉 |
| 4 | INPUT_ANALOG | 232 | 0.748 | 0 | 浮空模拟,低中间漂移 |
| 5 | OUTPUT LOW | 0 | 0.000 | 0 | 推挽拉低 |
| 6 | OUTPUT HIGH | 1022 | 3.297 | 1 | 推挽拉高 |
| 7 | OUTPUT_OPEN_DRAIN LOW | 0 | 0.000 | 0 | 开漏拉低 |
| 8 | OUTPUT_OPEN_DRAIN HIGH | 180 | 0.581 | 0 | 开漏高阻 + 无上拉,电平漂在中间偏低 |
同样地,代码逻辑将其视为“无外部电阻”,以下理解为“悬空”场景。
| 序号 | 模式 | ADC | 电压 (V) | digitalRead | 备注 |
|---|---|---|---|---|---|
| 1 | INPUT (floating) | 174 | 0.561 | 1 | 浮空偏低,但数字采样到 1(说明门限在中间) |
| 2 | INPUT_PULLUP | 860 | 2.774 | 1 | 仅内部上拉,接近 VCC |
| 3 | INPUT_PULLDOWN | 0 | 0.000 | 0 | 仅内部下拉 |
| 4 | INPUT_ANALOG | 232 | 0.748 | 0 | 浮空模拟,低中间漂移 |
| 5 | OUTPUT LOW | 0 | 0.000 | 0 | 推挽拉低 |
| 6 | OUTPUT HIGH | 1022 | 3.297 | 1 | 推挽拉高 |
| 7 | OUTPUT_OPEN_DRAIN LOW | 0 | 0.000 | 0 | 开漏拉低 |
| 8 | OUTPUT_OPEN_DRAIN HIGH | 180 | 0.581 | 0 | 开漏高阻 + 无上拉,电平漂在中间偏低 |
下面用一张总表,横向对比“同一个 GPIO 模式在不同外部电阻场景下”的典型行为,便于在文章中一眼看出规律。
说明:
- “低 / 高”指数字电平稳定为 0 / 1;
- “中间电压”指 ADC 在 0.8–2.5 V 区间,属于分压或浮空状态;
- “浮空/不稳定”指 ADC 明显在中间且 digitalRead 有抖动可能。
| 模式 / 场景 | 100 kΩ → GND | 100 kΩ → VCC | 浮空(ext=none) | 15 kΩ → GND | 15 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_PULLDOWN | 0 V,稳定低,内部+外部都往下拉 | ≈0.87 V,中间偏低,数字判低 | 0 V,稳定低,仅内部下拉 | 0 V,稳定低,内部+15k 下拉都往下 | ≈2.38 V,中间偏高,数字已判高,15k 压制内部下拉 |
| INPUT_ANALOG | 0 V,只看到外部下拉 | ≈2.3 V,只看到外部上拉 | 0.7–0.8 V 左右,中间电压,浮空漂移 | 0 V,只看到外部 15k 下拉 | ≈3.21 V,只看到外部 15k 上拉 |
| OUTPUT LOW | 0 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 LOW | 0 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 强上拉 → 稳定高 |
结合上面的完整表格,可以得出几条在结论(每条都能用上述数据佐证):
R_ext=100 kΩ → GND 时推算约 56.7 kΩ,R_ext=15 kΩ → GND 时推算约 40.6 kΩ,印证了“弱上拉”的工程经验。R_ext=none 的多组数据表明,INPUT/INPUT_ANALOG 下 ADC 在 0.4–0.8 V 对应的计数间漂移,digitalRead 既有 0 也有 1,说明浮空脚高度不可靠。OUTPUT HIGH/LOW 电压几乎仍为理想的 0 / 3.3 V,佐证了推挽输出的强驱动能力。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_++;
}