Proteus编码器正交信号仿真测试ESP32转速测量
用Proteus仿真编码器信号,驱动ESP32实现高精度转速测量 🛠️
你有没有遇到过这样的情况:电机还没到货,编码器样品还在路上,但老板已经催着要看到“转速反馈功能”的演示?或者,好不容易接上硬件,却发现计数乱跳、方向反了、高速丢脉冲……一顿排查下来,到底是信号问题?接线问题?还是代码逻辑写错了?
别急。今天我们就来搞一票“无中生有”的操作—— 不用一片真实编码器,只靠仿真,就能让ESP32跑出精准的RPM读数 。😎
整个过程就像在拍科幻片:你在电脑里造一个旋转的电机,在虚拟世界里发出A/B相正交信号,再让真实的ESP32板子“以为”它正在控制一台飞速运转的机器。而这一切,只需要 Proteus + Arduino IDE + 一块ESP32开发板 就能搞定。
这不仅是个炫技玩法,更是嵌入式工程师提升效率的硬核技能。尤其当你面对复杂的运动控制系统时,提前验证算法逻辑,可以少走太多弯路。
从零开始理解:为什么是“正交”编码器?🌀
我们常说的“编码器”,其实有很多类型。但说到最常用、成本最低、响应最快的,还得是 增量式正交编码器(Quadrature Encoder) 。
它不像绝对值编码器那样告诉你“你现在在第几度”,而是通过两个方波信号的变化,告诉你“动了没?往哪动?动了多少?”。
这两个信号,就是传说中的 Channel A 和 Channel B 。
它们长得几乎一模一样——都是方波,频率相同。唯一的区别是: 相位差90° 。
这个90°不是随便定的,它是整个系统判断方向的关键。
想象一下:
- 当轴正转时,A相先变高,B相随后跟上 → A超前B;
- 当轴反转时,B相先变高,A相才动 → B超前A。
于是,只要观察这两个信号的跳变顺序,就能知道旋转方向。🧠
更妙的是,如果我们对每个上升沿和下降沿都做检测,就可以在一个完整周期内捕捉到4个不同的状态:
| 状态 | A | B |
|---|---|---|
| S0 | 0 | 0 |
| S1 | 1 | 0 |
| S2 | 1 | 1 |
| S3 | 0 | 1 |
然后回到S0……
如果状态变化是
S0→S1→S2→S3→S0
,那就是正转;
如果是反过来
S0→S3→S2→S1→S0
,那就是反转。
这种技术叫做 四倍频解码(4x Decoding) ,相当于把编码器的分辨率直接翻了4倍!
比如一个标称100 PPR(Pulses Per Revolution)的编码器,在软件处理后实际可以获得400个有效边沿/圈,大大提升了低速下的测量精度。
当然,这一切的前提是: A和B必须严格保持90°相位差,且同步性极好 。否则,状态机一乱,方向就错,计数也就废了。
在Proteus里“伪造”一个真实的编码器 🎭
现实中没有编码器没关系,我们可以自己造一个。
Proteus本身并没有现成的“旋转编码器”元件模型,但它有强大的信号发生器功能,完全可以模拟出符合要求的A/B相信号。
怎么做?
打开Proteus ISIS,找到
ANALOG SIGNAL GENERATOR
(模拟信号发生器),拖两个出来。
分别命名为:
-
Signal_A
-
Signal_B
设置参数如下:
| 参数 | 值 |
|---|---|
| Waveform | Square |
| Frequency | 1kHz(举例) |
| Amplitude | 5V |
| Offset | 0V |
| Rise Time / Fall Time | 1ns(理想边沿) |
关键来了: Phase(相位)
- Signal_A 设置为 0°
- Signal_B 设置为 90° → 表示正转
- 如果想测试反转?改成 270° 或 -90° 即可!
然后将这两个信号连接到ESP32的GPIO引脚,比如IO34和IO35。
⚠️ 注意:ESP32工作电压是3.3V,而Proteus默认可能输出5V TTL电平。虽然大多数ESP32 GPIO支持5V容忍(input-tolerant),但为了安全起见,建议加入分压电阻或使用电平转换电路,尤其是在长期运行或批量测试中。
不过在仿真阶段,我们可以暂时忽略这点,毕竟重点是验证逻辑是否正确。
为什么选这两个引脚?
因为IO34和IO35属于ESP32的 输入专用引脚 (不能作为输出使用),非常适合用来接收外部信号。而且它们支持中断触发,完全满足编码器输入需求。
实战!让ESP32读懂“虚拟编码器”的语言 🔌
现在,真正的主角登场了——ESP32。
这块集Wi-Fi、蓝牙、双核处理器于一身的小芯片,不只是拿来连MQTT发数据的。它的GPIO响应速度快、中断机制灵活,再加上丰富的定时器资源,简直是做实时信号采集的绝佳平台。
我们的目标很明确:
1. 捕捉A/B相信号的边沿变化;
2. 判断当前是正转还是反转;
3. 统计单位时间内的脉冲数量;
4. 计算并输出RPM(转/分钟);
5. 所有结果通过串口打印,供调试查看。
听起来不难?但细节决定成败。
方案选择:软件中断 vs 硬件PCNT模块
ESP32有个隐藏神器—— PCNT(Pulse Counter)外设 ,专门用于处理正交编码器信号,能自动计数、判向、甚至滤波去抖。
那我们为什么不直接用它?
答案是: 太复杂了,不适合初学者快速验证 。
PCNT需要配置多个寄存器、启用专用通道、处理溢出中断……等你调通的时候,项目deadline早就过了。
所以,今天我们采用一种更直观、更易懂的方式: 外部中断 + 定时采样法 。
简单来说:
- 只监听A相的电平变化(CHANGE模式);
- 一旦发生变化,立即进入中断函数;
- 在中断里读取A和B当前的状态;
- 根据两者关系判断增减计数;
- 主程序每隔一段时间统计增量,计算转速。
这种方法虽然不如PCNT高效,但在中低速场景下完全够用,更重要的是—— 你能看懂每一行代码在干什么 。
核心代码解析 💻
#include <Arduino.h>
#include <TimerOne.h> // 第三方库,需提前安装
// 编码器引脚定义
#define ENCODER_A 34
#define ENCODER_B 35
// 编码器参数
#define PULSES_PER_REV 100 // 每圈100个脉冲(PPR)
#define SAMPLE_INTERVAL 100 // 采样周期:100ms
// 全局变量
volatile long pulseCount = 0; // 脉冲总数(可正可负)
long lastPulseCount = 0; // 上次记录的值
float rpm = 0.0; // 当前转速
// 中断服务程序(ISR)
void IRAM_ATTR onEncoderInterrupt() {
int a = digitalRead(ENCODER_A);
int b = digitalRead(ENCODER_B);
// 判断方向的核心逻辑
if (a == b) {
pulseCount--; // 反转
} else {
pulseCount++; // 正转
}
}
// 定时器回调函数:每100ms执行一次
void updateRPM() {
noInterrupts(); // 关闭中断,防止竞争
long current = pulseCount;
long delta = current - lastPulseCount;
lastPulseCount = current;
interrupts(); // 恢复中断
// 计算转速(RPM)
float revPerSec = (float)delta / PULSES_PER_REV / (SAMPLE_INTERVAL / 1000.0);
rpm = revPerSec * 60.0;
// 输出结果
Serial.print("RPM: ");
Serial.println(rpm, 2); // 保留两位小数
}
void setup() {
Serial.begin(115200);
pinMode(ENCODER_A, INPUT_PULLUP);
pinMode(ENCODER_B, INPUT_PULLUP);
// 绑定中断:A相任意边沿触发
attachInterrupt(digitalPinToInterrupt(ENCODER_A), onEncoderInterrupt, CHANGE);
// 初始化TimerOne(基于硬件定时器1)
Timer1.initialize(SAMPLE_INTERVAL * 1000); // 单位:微秒
Timer1.attachInterrupt(updateRPM); // 注册回调
}
void loop() {
delay(100); // 主循环空闲,所有工作由中断完成
}
关键点拆解 🔍
✅ 为什么只监听A相?
因为我们不需要同时监控两个通道的所有边沿。只要A相一变,我们就立刻去读B相的状态,就能判断出是往前走一步还是往后退一步。
这样做的好处是:
- 减少中断次数(原本四个边沿都会触发,现在只有A相触发);
- 降低CPU负载;
- 避免双重中断嵌套带来的不确定性。
当然,代价是牺牲了一点响应速度,但对于≤5kHz的信号,完全没问题。
✅ 方向判断逻辑是怎么来的?
还记得前面那个状态表吗?
当A领先B时,典型序列为:
- A=1, B=0 → A=1, B=1 → A=0, B=1 → A=0, B=0
注意看: A ≠ B 的时候是在前进;A == B 的时候是在后退?
不对。
仔细分析你会发现,在标准四倍频序列中,真正决定方向的是 边沿发生时另一相信号的电平 。
但我们这里用了个简化技巧:
在A相跳变瞬间读取AB同异。
实验表明,在理想方波下, 当A变化时,若B与A相同,则说明处于反向移动路径 。
举个例子:
- 正转过程中,A从0→1时,B仍为0 → A≠B → ++ ✅
- 反转过程中,A从0→1时,B已为1 → A==B → – ✅
这个逻辑虽然不是数学上最严谨的查表法,但在大多数情况下足够稳定,且代码极其简洁。
💡 提示:如果你发现方向总是反的,要么交换A/B线,要么把
++和--对调即可。
✅ 为什么要用
noInterrupts()
?
因为
pulseCount
是一个被中断频繁修改的全局变量,而主程序也在读它。
如果不加保护,可能出现“读到一半被中断打断”的情况,导致数据错误(即 竞态条件 )。
虽然在这里只是读一次,风险较低,但养成良好习惯很重要。特别是当你后续扩展功能(如PID控制)时,这类问题会突然爆发。
所以加上
noInterrupts()
和
interrupts()
这对组合拳,确保临界区安全。
✅ TimerOne 库怎么用?
Arduino原生的
millis()
和
delay()
依赖于软件调度,在高精度场合不够可靠。而
TimerOne
库直接操作ESP32的硬件定时器(Timer Group 1),提供微秒级精度的周期性中断。
安装方式:
pio lib install "TimerOne"
或在Arduino IDE库管理中搜索安装。
记得包含头文件,并正确初始化。
仿真与现实之间的鸿沟 ⚖️
你说,仿真跑通了,串口显示RPM=1200,是不是万事大吉?
别高兴太早。仿真是理想的,现实是残酷的。
以下这些坑,我帮你踩过了👇
🕳️ 1. 信号抖动与毛刺
现实中,机械编码器由于触点弹跳或电磁干扰,会产生大量虚假边沿。
而在Proteus里,默认信号是完美的阶跃波形,根本看不到这些问题。
解决方案:
-
硬件层
:加RC滤波电路(如10kΩ + 100nF);
-
软件层
:在ISR中加入延时去抖(不推荐!会影响响应);
- 更好的做法:改用
输入捕获+定时器滤波
,或者启用ESP32的PCNT内置消抖功能。
🕳️ 2. 高速丢脉冲
当转速很高时(比如编码器输出频率超过10kHz),单靠
digitalRead()
+ 软件中断的方法很容易丢脉冲。
原因很简单:每次中断都有上下文切换开销,如果边沿来得太密,来不及处理。
测试数据显示,基于
attachInterrupt
的方案在ESP32上极限大约在
30~50kHz总事件率
左右,超过就会漏记。
应对策略:
- 改用
PCNT外设
,它能在硬件层面完成计数和判向,完全不占用CPU;
- 或者使用
DMA+GPIO矩阵+定时器捕获
组合拳,实现更高性能采集。
🕳️ 3. 相位偏移不准
在Proteus中你可以精确设置90°相位差,但现实中由于布线长度不同、信号延迟差异,可能导致相位偏离。
尤其是长距离传输时,B相比A相慢了几纳秒,就可能造成误判。
建议:
- 使用差分信号传输(如RS422接口编码器);
- PCB布局尽量保持A/B走线等长;
- 在固件中加入相位校准补偿项(高级玩法)。
🕳️ 4. 启动初始状态未知
系统上电瞬间,A/B相可能处于任意电平组合,此时第一次中断无法确定是从哪个状态跳过来的。
这会导致首几个脉冲的方向判断出错。
解决办法:
- 上电后先读一次A/B初始状态,作为参考基准;
- 或者等到第一个完整的周期后再开始计数(适用于低速启动场景)。
如何把这套系统玩得更溜?🚀
你以为这就完了?不,这才刚开始。
既然我们已经有了可靠的转速感知能力,接下来就可以做一些真正有意思的事:
🌐 远程监控:把RPM上传到云端
利用ESP32自带的Wi-Fi功能,轻松实现数据远传。
你可以:
- 发送到MQTT服务器(如Mosquitto);
- 推送至InfluxDB + Grafana做可视化仪表盘;
- 用微信公众号推送报警信息(如超速停机);
示例片段:
#include <WiFi.h>
#include <PubSubClient.h>
const char* ssid = "your_ssid";
const char* password = "your_pass";
const char* mqtt_server = "broker.hivemq.com";
WiFiClient espClient;
PubSubClient client(espClient);
void connectToWifi() {
WiFi.begin(ssid, password);
while (WiFi.status() != WL_CONNECTED) delay(500);
}
void publishRPM() {
char payload[16];
sprintf(payload, "%.2f", rpm);
client.publish("motor/rpm", payload);
}
从此,你可以在办公室喝着咖啡,看着手机上的实时转速曲线,优雅地掌控千里之外的设备。
🧠 加入闭环控制:PID调速
有了转速反馈,下一步自然是做闭环控制。
比如你想让电机恒定运行在1000 RPM,就可以设计一个简单的PID控制器:
float setpoint = 1000.0;
float kp = 2.0, ki = 0.5, kd = 0.1;
float error, prevError = 0, integral = 0;
void updatePID() {
error = setpoint - rpm;
integral += error;
float derivative = error - prevError;
float output = kp * error + ki * integral + kd * derivative;
prevError = error;
// 控制PWM占空比
ledcWrite(0, constrain(output, 0, 255));
}
结合前面的转速测量,你就拥有了一个完整的 直流电机恒速控制系统 原型。
教学与工程价值:不止是“能跑就行”🎓
这套方法的价值,远远超出“临时救急”。
对于教学场景 👩🏫
学生常常难以理解“正交信号”到底是什么样子。现在你可以:
- 在Proteus里动态调整频率和相位;
- 实时观察串口输出的RPM变化;
- 甚至故意断开B相,看看系统会不会失控;
这种“看得见摸得着”的交互式学习,比纯理论讲解强十倍。
对于产品开发 🏭
在正式投板前,你可以:
- 用仿真验证固件逻辑是否健壮;
- 测试各种边界条件(如极速启停、反向突变);
- 注入故障信号(如丢脉冲、噪声干扰)来检验异常处理机制;
相当于提前进行了一轮“虚拟压力测试”。
对于远程协作 👥
疫情期间,很多工程师居家办公,拿不到硬件怎么办?
现在你可以把
.pdsprj
文件打包发给同事,对方只需打开Proteus,加载HEX文件,就能复现你的全部测试过程。
沟通效率直接拉满。
写在最后:工具的意义在于解放思维 🧩
我们花这么多时间搭建仿真环境,不是为了逃避硬件,而是为了让思想走得更快。
当你不再被“线没焊好”、“芯片烧了”、“信号测不出”这些琐事拖累时,你才能真正专注于
系统设计的本质
:
如何更准确地感知世界?
如何更智能地做出决策?
如何构建更可靠的自动化系统?
而这,才是工程师真正的战场。
所以,下次当你面对一个新的传感器、一个新的协议、一个新的控制算法时,不妨先问问自己:
“我能先在仿真里把它跑通吗?”
也许答案就是—— 能 。
更多推荐


所有评论(0)