✔零知开源(零知IDE)是一个专为电子初学者/电子兴趣爱好者设计的开源软硬件平台,在硬件上提供超高性价比STM32系列开发板、物联网控制板。取消了Bootloader程序烧录,让开发重心从 "配置环境" 转移到 "创意实现",极大降低了技术门槛。零知IDE编程软件,内置上千个覆盖多场景的示例代码,支持项目源码一键下载,项目文章在线浏览。零知开源(零知IDE)平台通过软硬件协同创新,让你的创意快速转化为实物,来动手试试吧!
✔访问零知实验室,获取更多实战项目和教程资源吧!
目录
[1.1 元件清单](#1.1 元件清单)
[1.2 接线方案表](#1.2 接线方案表)
[1.3 接线示意图](#1.3 接线示意图)
[1.4 实物连接图](#1.4 实物连接图)
[3.1 核心中断服务函数](#3.1 核心中断服务函数)
[3.2 波形显示优化算法](#3.2 波形显示优化算法)
[3.3 布局与状态显示系统](#3.3 布局与状态显示系统)
[3.4 按键消抖与系统复位](#3.4 按键消抖与系统复位)
[3.5 完整代码](#3.5 完整代码)
[4.1 操作流程](#4.1 操作流程)
[4.2 视频演示](#4.2 视频演示)
[5.1 光电编码原理](#5.1 光电编码原理)
[5.2 A/B相信号产生](#5.2 A/B相信号产生)
[5.3 格雷码编码](#5.3 格雷码编码)
项目概述
基于零知增强板(主控 STM32F407VET6) 打造了一套旋转编码器计数系统,不仅实现了编码器的精准计数与方向判断,还通过 ST7789 显示屏实时可视化展示 A/B 相脉冲波形、格雷码状态变化和 90° 相位差特征,同时配套串口调试、按键重置功能
项目难点及解决方案
问题描述:单引脚边沿检测导致每转多格 / 少格计数,硬件抖动引发误触发
**解决方案:**同时监听 CLK/DT 两相的所有边沿CHANGE变化,实现4 步格雷码完整循环计数一次,采用 volatile 关键字保护中断中修改的全局变量
一、系统硬件部分
1.1 元件清单
| 元件名称 | 规格型号 | 数量 | 备注 |
|---|---|---|---|
| 零知增强板 | STM32F407VET6 | 1 | 主控制器,支持硬件SPI |
| 旋转编码器 | 增量式 AB 相旋转编码器 | 1 | 带按键,5V/3.3V兼容 |
| TFT显示屏 | ST7789 240×320 | 1 | SPI接口,3.5寸IPS屏 |
| 杜邦线 | 20cm 母对母 | 10 | 连接导线 |
1.2 接线方案表
严格按照代码定义的引脚分配:
| 零知增强板引脚 | 连接元件 | 元件引脚 | 功能说明 |
|---|---|---|---|
| 53 | ST7789 TFT | CS | 片选信号 |
| 9 | ST7789 TFT | DC | 数据/命令选择 |
| 8 | ST7789 TFT | RST | 复位信号 |
| 51 (SDA) | ST7789 TFT | SDA | SPI数据线 |
| 52 (SCL) | ST7789 TFT | SCL | SPI时钟线 |
| 3.3V | ST7789 TFT | VCC | 电源正极 |
| GND | ST7789 TFT | GND | 电源地 |
| 2 | 旋转编码器 | CLK | A相信号(外部中断) |
| 3 | 旋转编码器 | DT | B相信号(外部中断) |
| 4 | 旋转编码器 | SW | 按键信号(内部上拉) |
| 5V | 旋转编码器 | + | 电源正极 |
| GND | 旋转编码器 | GND | 电源地 |
ST7789 显示屏的 SDA(MOSI)、SCL(SCK)使用零知增强板的默认 SPI 引脚,无需额外定义,代码中采用 Adafruit 库的硬件 SPI 实现
1.3 接线示意图

TFT显示屏的背光引脚需要单独连接到3.3V,确保所有GND引脚共地
1.4 实物连接图

二、安装与使用部分
2.1 开源平台-输入"旋转编码器计数系统 "并搜索-代码下载自动打开

2.2 连接-验证-上传

2.3 调试-串口监视器

三、核心代码讲解
代码采用中断驱动 + 主循环刷新的架构,中断中完成编码器状态解码、计数、波形缓冲区更新,主循环中完成显示屏刷新、按键检测,中断与主循环通过标志位通信,避免中断中直接操作显示屏
3.1 核心中断服务函数
cpp
void updateEncoder() {
uint8_t MSB = digitalRead(CLK); // A相状态
uint8_t LSB = digitalRead(DT); // B相状态
// 组合当前格雷码状态(2位二进制)
uint8_t encoded = (MSB << 1) | LSB;
// 计算查找表索引:前2位为上次状态,后2位为当前状态
uint8_t tableIndex = (lastEncoded << 2) | encoded;
// 从16状态查找表获取方向(-1, 0, 1)
int8_t direction = encoderTable[tableIndex];
if (direction != 0) {
static int8_t accumulatedSteps = 0;
accumulatedSteps += direction;
// 实时更新波形显示,不依赖计数确认
updateWaveform(MSB, LSB);
// 完整4步循环确认机制
if (accumulatedSteps >= 4) {
counter++; // 顺时针计数
directionCW = true; // 方向标志
accumulatedSteps = 0; // 重置累计
encoderUpdated = true; // 触发显示更新
} else if (accumulatedSteps <= -4) {
counter--; // 逆时针计数
directionCW = false; // 方向标志
accumulatedSteps = 0; // 重置累计
encoderUpdated = true; // 触发显示更新
}
// 累计步数在-3到3之间:未完成完整循环,不计数
}
lastEncoded = encoded; // 保存当前状态供下次使用
}
中断函数中的全局变量进行volatile关键字修饰,告诉编译器不要对该变量进行优化,因为该变量可能在任何时刻被中断修改
3.2 波形显示优化算法
cpp
void updateWaveform(uint8_t a, uint8_t b) {
waveBufferA[waveIndex] = a;
waveBufferB[waveIndex] = b;
waveIndex = (waveIndex + 1) % WAVE_BUFFER_SIZE; // 循环索引,避免越界
}
void updateWaveformDisplay() {
int pixelPerSample = 3; // 时基拉伸:每个采样点3像素
int displaySamples = min(WAVE_BUFFER_SIZE, (gridW - 4) / pixelPerSample);
for (int i = 0; i < displaySamples; i++) {
int idx = (waveIndex + WAVE_BUFFER_SIZE - displaySamples + i) % WAVE_BUFFER_SIZE;
int x = gridX + 2 + i * pixelPerSample;
// A相波形绘制(绿色)
int yA = gridY_A + (waveBufferA[idx] ? 5 : gridH - 5);
// 绘制水平线段(稳定电平)
for (int px = 0; px < pixelPerSample; px++) {
tft.drawPixel(x + px, yA, COLOR_SUCCESS);
}
// 绘制垂直线段(状态跳变)
if (i > 0) {
int prevIdx = (waveIndex + WAVE_BUFFER_SIZE - displaySamples + i - 1) % WAVE_BUFFER_SIZE;
if (waveBufferA[idx] != waveBufferA[prevIdx]) {
int yA_prev = gridY_A + (waveBufferA[prevIdx] ? 5 : gridH - 5);
tft.drawLine(x, yA_prev, x, yA, COLOR_SUCCESS);
}
}
// B相波形同理...
}
}
时基拉伸与可视化,pixelPerSample=3使波形更易观察;垂直连接线清晰展示状态变化时刻,上下区域避免波形重叠混淆
3.3 布局与状态显示系统
cpp
// 左侧面板 - 计数器和状态信息
void drawLeftPanel() {
// 计数值显示(动态颜色)
tft.setTextSize(4);
if (counter > 0) {
tft.setTextColor(COLOR_SUCCESS); // 正数:绿色
} else if (counter < 0) {
tft.setTextColor(COLOR_WARNING); // 负数:黄色
} else {
tft.setTextColor(COLOR_TEXT); // 零:白色
}
// 居中显示计数值
String countStr = String(counter);
int16_t x1, y1;
uint16_t w, h;
tft.getTextBounds(countStr, 0, 0, &x1, &y1, &w, &h);
int centerX = (LEFT_PANEL_W - w) / 2;
tft.setCursor(centerX, 75);
tft.println(countStr);
// 方向指示(动态文本)
tft.setTextSize(2);
if (directionCW) {
tft.setTextColor(COLOR_SUCCESS);
tft.setCursor(25, 148);
tft.println("CW >>"); // 顺时针
} else {
tft.setTextColor(COLOR_WARNING);
tft.setCursor(25, 148);
tft.println("<< CCW"); // 逆时针
}
}
绿色=正数/顺时针,黄色=负数/逆时针,白色=中性
3.4 按键消抖与系统复位
cpp
void checkButton() {
int btnState = digitalRead(SW);
if (btnState == LOW) {
// 50ms消抖,平衡响应速度和稳定性
if (millis() - lastButtonPress > 50) {
counter = 0; // 计数器清零
// 清空波形缓冲区
for (int i = 0; i < WAVE_BUFFER_SIZE; i++) {
waveBufferA[i] = 0;
waveBufferB[i] = 0;
}
waveIndex = 0;
// 重绘界面
drawMainUI();
Serial.println("\n 按钮按下 - 计数器已重置 \n");
}
lastButtonPress = millis();
}
}
状态保存记录上次按下时间,避免重复触发;计数器、波形、显示三部分同步清零
3.5 完整代码
cpp
/**************************************************************************************
* 文件: RotaryEncoder_Display_Fixed.ino
* 作者:零知实验室(深圳市在芯间科技有限公司)
* -^^- 零知实验室,让电子制作变得更简单! -^^-
* 时间: 2026-1-30
*
* 项目功能:
* 旋转编码器工作原理交互系统,实时可视化展示A/B相脉冲波形、
* 格雷码状态变化和方向判断逻辑,帮助理解增量式旋转编码器工作机制。
***************************************************************************************/
#include <Adafruit_GFX.h>
#include <Adafruit_ST7789.h>
// ST7789显示屏引脚定义
#define TFT_CS 53
#define TFT_DC 9
#define TFT_RST 8
Adafruit_ST7789 tft = Adafruit_ST7789(TFT_CS, TFT_DC, TFT_RST);
// 屏幕尺寸(横屏)
#define SCREEN_WIDTH 320
#define SCREEN_HEIGHT 240
// 旋转编码器引脚定义
#define CLK 2
#define DT 3
#define SW 4
// 颜色定义 - 现代简约配色
#define COLOR_BG 0x0841 // 深蓝背景
#define COLOR_PRIMARY 0x07FF // 青色主题色
#define COLOR_ACCENT 0xFD20 // 橙色强调色
#define COLOR_SUCCESS 0x07E0 // 绿色
#define COLOR_WARNING 0xFFE0 // 黄色
#define COLOR_TEXT 0xFFFF // 白色文字
#define COLOR_SECONDARY 0x8410 // 灰色次要文字
#define COLOR_PANEL 0x1082 // 面板背景
#define COLOR_GRID 0x2945 // 网格线
// 布局定义
#define LEFT_PANEL_X 0
#define LEFT_PANEL_W 160
#define RIGHT_PANEL_X 160
#define RIGHT_PANEL_W 160
#define DIVIDER_X 159
// 全局变量
volatile long counter = 0;
volatile bool encoderUpdated = false;
volatile bool directionCW = true;
unsigned long lastButtonPress = 0;
// 编码器状态变量
volatile uint8_t lastEncoded = 0;
// 波形显示缓冲区
#define WAVE_BUFFER_SIZE 100
uint8_t waveBufferA[WAVE_BUFFER_SIZE];
uint8_t waveBufferB[WAVE_BUFFER_SIZE];
uint8_t waveIndex = 0;
// 格雷码查找表(修正方向)
const int8_t encoderTable[] = {
0, -1, 1, 0,
1, 0, 0, -1,
-1, 0, 0, 1,
0, 1, -1, 0
};
void setup() {
// 设置编码器引脚为输入模式
pinMode(CLK, INPUT);
pinMode(DT, INPUT);
pinMode(SW, INPUT_PULLUP);
// 初始化串口
Serial.begin(115200);
Serial.println("========================================");
Serial.println("旋转编码器演示系统(零知实验室)");
Serial.println("========================================");
// 初始化显示屏
tft.init(240, 320);
tft.setRotation(3); // 横屏显示
tft.fillScreen(COLOR_BG);
// 显示启动画面
drawStartupScreen();
delay(2000);
// 绘制主界面
drawMainUI();
// 初始化波形缓冲区
for (int i = 0; i < WAVE_BUFFER_SIZE; i++) {
waveBufferA[i] = 0;
waveBufferB[i] = 0;
}
// 读取编码器初始状态
lastEncoded = (digitalRead(CLK) << 1) | digitalRead(DT);
// 附加中断,监听所有边沿变化
attachInterrupt(digitalPinToInterrupt(CLK), updateEncoder, CHANGE);
attachInterrupt(digitalPinToInterrupt(DT), updateEncoder, CHANGE);
Serial.println("系统初始化完成");
Serial.println("开始监听编码器...\n");
}
void loop() {
// 检查编码器是否有更新
if (encoderUpdated) {
encoderUpdated = false;
updateDisplay();
}
// 检查按钮
checkButton();
delay(10);
}
// 中断服务函数,使用查找表快速解码,解决抖动和误判
void updateEncoder() {
// 读取当前编码器状态
uint8_t MSB = digitalRead(CLK);
uint8_t LSB = digitalRead(DT);
uint8_t encoded = (MSB << 1) | LSB;
uint8_t tableIndex = (lastEncoded << 2) | encoded;
// 从查找表获取方向
int8_t direction = encoderTable[tableIndex];
if (direction != 0) {
// 使用静态变量累计步进,完整4步循环才计数
// 累计不足4步,转一格不计数的问题;需要完整循环,正转反转误判的问题
static int8_t accumulatedSteps = 0;
accumulatedSteps += direction;
// 波形实时更新(不管是否计数)
updateWaveform(MSB, LSB);
// 每累计4步(完整的格雷码循环)才计数一次
if (accumulatedSteps >= 4) {
counter++;
directionCW = true;
accumulatedSteps = 0; // 重置累计
encoderUpdated = true;
// 串口调试输出
Serial.print("方向: 顺时针 | 计数: ");
Serial.print(counter);
Serial.print(" | 格雷码: ");
Serial.print(MSB);
Serial.println(LSB);
} else if (accumulatedSteps <= -4) {
counter--;
directionCW = false;
accumulatedSteps = 0; // 重置累计
encoderUpdated = true;
// 串口调试输出
Serial.print("方向: 逆时针 | 计数: ");
Serial.print(counter);
Serial.print(" | 格雷码: ");
Serial.print(MSB);
Serial.println(LSB);
}
// 如果累计步数在-3到3之间,说明还没完成一个完整循环,不计数
}
lastEncoded = encoded;
}
// 更新波形缓冲区
void updateWaveform(uint8_t a, uint8_t b) {
waveBufferA[waveIndex] = a;
waveBufferB[waveIndex] = b;
waveIndex = (waveIndex + 1) % WAVE_BUFFER_SIZE;
}
// 检查按钮(改进消抖)
void checkButton() {
int btnState = digitalRead(SW);
if (btnState == LOW) {
// 消抖时间从200ms改为50ms,响应更快
if (millis() - lastButtonPress > 50) {
counter = 0;
// 清空波形缓冲区
for (int i = 0; i < WAVE_BUFFER_SIZE; i++) {
waveBufferA[i] = 0;
waveBufferB[i] = 0;
}
waveIndex = 0;
drawMainUI();
Serial.println("\n 按钮按下 - 计数器已重置 \n");
}
lastButtonPress = millis();
}
}
// ==================== UI绘制函数 ====================
// 启动画面
void drawStartupScreen() {
tft.fillScreen(COLOR_BG);
// 标题
tft.setTextSize(3);
tft.setTextColor(COLOR_PRIMARY);
tft.setCursor(20, 60);
tft.println("Rotary Encoder");
tft.setTextSize(2);
tft.setTextColor(COLOR_ACCENT);
tft.setCursor(30, 100);
tft.println("Counting System");
// 副标题 旋转编码器计数系统
tft.setTextSize(1);
tft.setTextColor(COLOR_SECONDARY);
tft.setCursor(60, 140);
tft.println("Interactive Tool");
// 零知实验室标识
tft.setTextColor(COLOR_TEXT);
tft.setCursor(80, 200);
tft.println("Lingzhi Lab");
}
// 绘制主界面
void drawMainUI() {
tft.fillScreen(COLOR_BG);
// 绘制中间分隔线
tft.drawFastVLine(DIVIDER_X, 0, SCREEN_HEIGHT, COLOR_SECONDARY);
tft.drawFastVLine(DIVIDER_X + 1, 0, SCREEN_HEIGHT, COLOR_SECONDARY);
// 左侧面板 - 计数和状态显示
drawLeftPanel();
// 右侧面板 - 波形显示
drawRightPanel();
}
// 绘制左侧面板(计数器和状态)
void drawLeftPanel() {
// 顶部标题
tft.fillRoundRect(5, 5, LEFT_PANEL_W - 10, 30, 6, COLOR_PANEL);
tft.setTextSize(2);
tft.setTextColor(COLOR_PRIMARY);
tft.setCursor(15, 13);
tft.println("ENCODER");
// 计数值显示区
tft.fillRoundRect(5, 45, LEFT_PANEL_W - 10, 70, 6, COLOR_PANEL);
tft.setTextSize(1);
tft.setTextColor(COLOR_SECONDARY);
tft.setCursor(15, 53);
tft.println("Counter Value:");
tft.setTextSize(4);
tft.setTextColor(COLOR_TEXT);
tft.setCursor(40, 75);
tft.println("0");
// 方向显示区
tft.fillRoundRect(5, 125, LEFT_PANEL_W - 10, 40, 6, COLOR_PANEL);
tft.setTextSize(1);
tft.setTextColor(COLOR_SECONDARY);
tft.setCursor(15, 133);
tft.println("Direction:");
tft.setTextSize(2);
tft.setTextColor(COLOR_TEXT);
tft.setCursor(25, 148);
tft.println("---");
// 状态信息区
tft.fillRoundRect(5, 175, LEFT_PANEL_W - 10, 60, 6, COLOR_PANEL);
tft.setTextSize(1);
tft.setTextColor(COLOR_ACCENT);
tft.setCursor(15, 183);
tft.println("Gray Code: 00");
tft.setTextColor(COLOR_SECONDARY);
tft.setCursor(15, 198);
tft.println("Phase: 90deg");
tft.setCursor(15, 213);
tft.println("Press SW to Reset");
}
// 绘制右侧面板(波形显示)
void drawRightPanel() {
// 顶部标题
tft.fillRoundRect(RIGHT_PANEL_X + 5, 5, RIGHT_PANEL_W - 10, 30, 6, COLOR_PANEL);
tft.setTextSize(2);
tft.setTextColor(COLOR_PRIMARY);
tft.setCursor(RIGHT_PANEL_X + 15, 13);
tft.println("WAVEFORM");
// 波形显示区域
tft.fillRoundRect(RIGHT_PANEL_X + 5, 45, RIGHT_PANEL_W - 10, 190, 6, COLOR_PANEL);
// A相标签
tft.setTextSize(2);
tft.setTextColor(COLOR_SUCCESS);
tft.setCursor(RIGHT_PANEL_X + 15, 55);
tft.println("A");
// B相标签
tft.setTextColor(COLOR_WARNING);
tft.setCursor(RIGHT_PANEL_X + 15, 145);
tft.println("B");
// 绘制波形网格
drawWaveformGrid();
// 图例说明
tft.setTextSize(1);
tft.setTextColor(COLOR_SECONDARY);
tft.setCursor(RIGHT_PANEL_X + 45, 60);
tft.println("CLK (Phase A)");
tft.setCursor(RIGHT_PANEL_X + 45, 150);
tft.println("DT (Phase B)");
}
// 绘制波形网格
void drawWaveformGrid() {
int gridX = RIGHT_PANEL_X + 40;
int gridY_A = 75; // A相波形位置
int gridY_B = 165; // B相波形位置
int gridW = 105;
int gridH = 40;
// A相波形区域
tft.fillRect(gridX, gridY_A, gridW, gridH, COLOR_BG);
tft.drawRect(gridX, gridY_A, gridW, gridH, COLOR_GRID);
tft.drawLine(gridX, gridY_A + gridH/2, gridX + gridW, gridY_A + gridH/2, COLOR_GRID);
// B相波形区域
tft.fillRect(gridX, gridY_B, gridW, gridH, COLOR_BG);
tft.drawRect(gridX, gridY_B, gridW, gridH, COLOR_GRID);
tft.drawLine(gridX, gridY_B + gridH/2, gridX + gridW, gridY_B + gridH/2, COLOR_GRID);
}
// 更新显示内容
void updateDisplay() {
// 更新计数值
updateCounterDisplay();
// 更新波形
updateWaveformDisplay();
// 更新方向
updateDirectionDisplay();
// 更新格雷码状态
updateGrayCodeDisplay();
}
// 更新计数器显示
void updateCounterDisplay() {
// 清除旧数值
tft.fillRect(10, 70, LEFT_PANEL_W - 20, 40, COLOR_PANEL);
// 显示新数值
tft.setTextSize(4);
// 根据数值正负改变颜色
if (counter > 0) {
tft.setTextColor(COLOR_SUCCESS);
} else if (counter < 0) {
tft.setTextColor(COLOR_WARNING);
} else {
tft.setTextColor(COLOR_TEXT);
}
// 居中显示
String countStr = String(counter);
int16_t x1, y1;
uint16_t w, h;
tft.getTextBounds(countStr, 0, 0, &x1, &y1, &w, &h);
int centerX = (LEFT_PANEL_W - w) / 2;
tft.setCursor(centerX, 75);
tft.println(countStr);
}
// 优化波形显示,拉伸时基,更清晰展示相位差
void updateWaveformDisplay() {
int gridX = RIGHT_PANEL_X + 40;
int gridY_A = 75;
int gridY_B = 165;
int gridW = 105;
int gridH = 40;
// 清除波形区域
tft.fillRect(gridX + 1, gridY_A + 1, gridW - 2, gridH - 2, COLOR_BG);
tft.fillRect(gridX + 1, gridY_B + 1, gridW - 2, gridH - 2, COLOR_BG);
// 绘制中线
tft.drawLine(gridX, gridY_A + gridH/2, gridX + gridW, gridY_A + gridH/2, COLOR_GRID);
tft.drawLine(gridX, gridY_B + gridH/2, gridX + gridW, gridY_B + gridH/2, COLOR_GRID);
// 绘制波形 - 每个样本占3个像素,拉伸显示
int pixelPerSample = 3;
int displaySamples = min(WAVE_BUFFER_SIZE, (gridW - 4) / pixelPerSample);
for (int i = 0; i < displaySamples; i++) {
int idx = (waveIndex + WAVE_BUFFER_SIZE - displaySamples + i) % WAVE_BUFFER_SIZE;
int x = gridX + 2 + i * pixelPerSample;
// A相波形(绿色)- 绘制方块状波形
int yA = gridY_A + (waveBufferA[idx] ? 5 : gridH - 5);
// 绘制水平线段(拉伸显示)
for (int px = 0; px < pixelPerSample; px++) {
tft.drawPixel(x + px, yA, COLOR_SUCCESS);
}
// 绘制垂直连接线(状态变化时)
if (i > 0) {
int prevIdx = (waveIndex + WAVE_BUFFER_SIZE - displaySamples + i - 1) % WAVE_BUFFER_SIZE;
if (waveBufferA[idx] != waveBufferA[prevIdx]) {
int yA_prev = gridY_A + (waveBufferA[prevIdx] ? 5 : gridH - 5);
tft.drawLine(x, yA_prev, x, yA, COLOR_SUCCESS);
}
}
// B相波形(黄色)- 同样处理
int yB = gridY_B + (waveBufferB[idx] ? 5 : gridH - 5);
for (int px = 0; px < pixelPerSample; px++) {
tft.drawPixel(x + px, yB, COLOR_WARNING);
}
if (i > 0) {
int prevIdx = (waveIndex + WAVE_BUFFER_SIZE - displaySamples + i - 1) % WAVE_BUFFER_SIZE;
if (waveBufferB[idx] != waveBufferB[prevIdx]) {
int yB_prev = gridY_B + (waveBufferB[prevIdx] ? 5 : gridH - 5);
tft.drawLine(x, yB_prev, x, yB, COLOR_WARNING);
}
}
}
}
// 更新方向显示
void updateDirectionDisplay() {
// 清除旧内容
tft.fillRect(10, 143, LEFT_PANEL_W - 20, 20, COLOR_PANEL);
tft.setTextSize(2);
if (directionCW) {
tft.setTextColor(COLOR_SUCCESS);
tft.setCursor(25, 148);
tft.println("CW >>");
} else {
tft.setTextColor(COLOR_WARNING);
tft.setCursor(25, 148);
tft.println("<< CCW");
}
}
// 更新格雷码显示
void updateGrayCodeDisplay() {
// 清除旧内容
tft.fillRect(10, 180, LEFT_PANEL_W - 20, 50, COLOR_PANEL);
tft.setTextSize(1);
// 显示格雷码
tft.setTextColor(COLOR_ACCENT);
tft.setCursor(15, 183);
tft.print("Gray Code: ");
uint8_t clkState = digitalRead(CLK);
uint8_t dtState = digitalRead(DT);
tft.print(clkState);
tft.print(dtState);
// 显示相位关系
tft.setTextColor(COLOR_SECONDARY);
tft.setCursor(15, 198);
tft.print("Phase Shift: 90");
tft.print((char)247); // 度数符号
// 提示信息
tft.setCursor(15, 213);
tft.println("Press SW to Reset");
}
/******************************************************************************
* 深圳市在芯间科技有限公司
* 淘宝店铺:在芯间科技零知板
* 店铺网址:https://shop533070398.taobao.com
* 版权说明:
* 1.本代码的版权归【深圳市在芯间科技有限公司】所有,仅限个人非商业性学习使用。
* 2.严禁将本代码或其衍生版本用于任何商业用途。
* 3.任何商业用途均需事先获得【深圳市在芯间科技有限公司】的书面授权。
******************************************************************************/
系统整体流程图
展示中断、主循环、UI 刷新、按键检测的逻辑关系

差分测量模式原理与实现
cpp
// 差分测量通过比较相邻状态实现
uint8_t encoded = (digitalRead(CLK) << 1) | digitalRead(DT);
uint8_t tableIndex = (lastEncoded << 2) | encoded;
int8_t direction = encoderTable[tableIndex];
// 查找表定义状态转移方向
const int8_t encoderTable[] = {
0, -1, 1, 0, // 上次=00,当前=00,01,10,11
1, 0, 0, -1, // 上次=01
-1, 0, 0, 1, // 上次=10
0, 1, -1, 0 // 上次=11
};
将A/B相电平组合为2位格雷码,当前状态与上次状态组成4位索引,查表得到方向值,累计4个有效方向变化确认一次计数
差分测量原理图

每个棘爪格对应完整的4状态循环
四、项目结果演示
4.1 操作流程
根据上述接线方案表完成硬件接线、烧录代码到零知增强板,驱动系统实现完整的功能
①系统启动
零知增强板通过 USB 供电,系统上电后首先显示启动画面,自动进入分屏主 UI,左侧显示计数、右侧显示 A/B 相波形网格
②正常操作
顺时针旋转编码器,计数器增加,方向显示"CW >>",A相波形超前B相90°;逆时针旋转编码器,计数器减少,方向显示"<< CCW",B相波形超前A相90°;观察波形区域,清晰看到两相脉冲的相位差和状态变化

③系统复位
按下编码器中间按键,计数器归零,波形清空,界面刷新

4.2 视频演示
零知增强板旋转编码器计数系统
首先展示系统启动过程,然后演示顺时针和逆时针旋转时计数器的变化、方向指示的切换、以及波形显示的实时更新。特别展示了A/B相脉冲波形的90°相位差特征,以及按下复位键后系统清空重置的过程。最后通过串口监视器展示实时的状态输出信息
五、旋转编码器技术讲解
旋转编码器是一种将旋转运动转换为电信号的传感器,增量式编码器通过输出两路相位差90°的方波信号来检测旋转方向和角度变化
5.1 光电编码原理

编码器内部有一个带有栅格的光码盘,红外发射管和接收管分别位于码盘两侧、旋转时,栅格交替遮挡光线,产生脉冲信号
5.2 A/B相信号产生
正交编码: 两个光电传感器安装位置相差1/4个栅格间距,产生相位差90°的A相和B相信号

正转:A 相脉冲的上升沿 / 下降沿超前B 相 90°, A 相先变化,B 相后变化
反转:B 相脉冲的上升沿 / 下降沿超前A 相 90°, B 相先变化,A 相后变化
5.3 格雷码编码
A/B相组合形成4种状态:00、01、11、10,相邻状态仅有 1 位发生变化
顺时针旋转时,A 引脚先于 B 引脚接地,格雷码按00→01→11→10→00的顺序循环

逆时针旋转时,B 引脚先于 A 引脚接地,格雷码按00→10→11→01→00的顺序循环

当A改变状态时:如果 B != A,则顺时针转动旋钮、如果 B = A,则逆时针转动旋钮
六、常见问题解答(FAQ)
Q1:编码器按键按下无法重置计数?
*A:进行排查:*代码已启用INPUT_PULLUP,外接上拉电阻会导致按键电平无法拉低;按键无反应,调整增大消抖时间;通过万用表测量按键按下 / 松开的通断
Q2:波形显示不清晰、相位差看不到、波形断裂?
*A:进行以下排查:*调整pixelPerSample,进一步拉伸时基;匀速旋转编码器实现连续波形、检查波形缓冲区WAVE_BUFFER_SIZE
项目资源整合
图形显示基础库: adafruit/Adafruit-GFX-Library
显示屏驱动库: adafruit/Adafruit-ST7735-Library