Add capacitive touch-button test optimized from btntest.
TTP223-style debounce (majority, min touch, rearm), SoftSPI+LVGL shell, flashable firmware/cyd. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
13
.gitignore
vendored
Normal file
13
.gitignore
vendored
Normal file
@@ -0,0 +1,13 @@
|
||||
.pio/
|
||||
.vscode/.browse.c_cpp.db*
|
||||
.vscode/c_cpp_properties.json
|
||||
.vscode/launch.json
|
||||
.vscode/ipch
|
||||
*.pyc
|
||||
__pycache__/
|
||||
.DS_Store
|
||||
Thumbs.db
|
||||
assets/
|
||||
*.elf
|
||||
# 构建目录里的 bin 不入库;firmware/ 预编译包要入库
|
||||
!firmware/**/*.bin
|
||||
62
README.md
Normal file
62
README.md
Normal file
@@ -0,0 +1,62 @@
|
||||
# 触控感应按钮计数(CYD)
|
||||
|
||||
复用 [btntest](../btntest) 的 CYD + SoftSPI 触控 + LVGL 壳,针对 **电容触控感应模块**(如 TTP223)单独优化:抖沿确认、最短触摸、松手再武装。
|
||||
|
||||
GPIO22 读模块 OUT:每次有效触摸,屏幕计数 +1。
|
||||
|
||||
## 硬件
|
||||
|
||||
| 项目 | 说明 |
|
||||
|------|------|
|
||||
| 板子 | CYD ESP32-2432S028R |
|
||||
| 模块 | 三脚触控感应(VCC / GND / OUT),典型 TTP223 |
|
||||
| 输出 | **GPIO22** |
|
||||
|
||||
### 接线
|
||||
|
||||
| 模块 | CYD |
|
||||
|------|-----|
|
||||
| VCC / 3.3V | **3.3V** |
|
||||
| GND | **GND** |
|
||||
| OUT / S / DO | **GPIO22** |
|
||||
|
||||
开机自动采空闲电平,反相视为触摸(兼容「触摸变高」或「触摸变低」)。引脚用浮空 `INPUT`(推挽输出,不上/下拉)。
|
||||
|
||||
屏上 `OUT HIGH/LOW` 与 `act=`:触摸时 OUT 应变到 `act` 那一侧并 +1;`hold` / 进度条显示持续触摸。
|
||||
|
||||
## 相对机械轻触的优化
|
||||
|
||||
| 点 | 说明 |
|
||||
|----|------|
|
||||
| 多数表决 | 5 次采样,压接近阈值时的抖沿 |
|
||||
| 积分迟滞 | 更宽进入/离开门限 |
|
||||
| 最短确认 | 稳定触摸 ≥ 35ms 才计 1 次 |
|
||||
| 松手再武装 | 松开后再冷却 120ms,防悬停连计 |
|
||||
| UI | 显示 TOUCHING / hold ms / 触摸条 |
|
||||
|
||||
上电时请勿按住感应区。
|
||||
|
||||
## 操作
|
||||
|
||||
- 轻触感应面一次:计数 +1
|
||||
- 屏上触控 **Reset**:清零
|
||||
|
||||
## 构建 / 烧录
|
||||
|
||||
```bash
|
||||
pio run -e cyd -t upload
|
||||
```
|
||||
|
||||
预编译包:
|
||||
|
||||
```powershell
|
||||
cd firmware\cyd
|
||||
.\flash.ps1
|
||||
# 或 .\flash.ps1 -Port COMx
|
||||
```
|
||||
|
||||
导出固件:
|
||||
|
||||
```powershell
|
||||
.\firmware\export_from_build.ps1
|
||||
```
|
||||
13
copy_lv_conf.py
Normal file
13
copy_lv_conf.py
Normal file
@@ -0,0 +1,13 @@
|
||||
Import("env") # noqa: F821
|
||||
from pathlib import Path
|
||||
|
||||
src = Path(env["PROJECT_DIR"]) / "include" / "lv_conf.h" # noqa: F821
|
||||
dst_dir = Path(env["PROJECT_LIBDEPS_DIR"]) / env["PIOENV"] # noqa: F821
|
||||
dst_dir.mkdir(parents=True, exist_ok=True)
|
||||
dst = dst_dir / "lv_conf.h"
|
||||
if src.exists():
|
||||
dst.write_text(src.read_text(encoding="utf-8"), encoding="utf-8")
|
||||
lvgl = dst_dir / "lvgl" / "lv_conf.h"
|
||||
if (dst_dir / "lvgl").exists():
|
||||
lvgl.write_text(src.read_text(encoding="utf-8"), encoding="utf-8")
|
||||
print(f"copy_lv_conf: synced -> {dst}")
|
||||
21
firmware/README.md
Normal file
21
firmware/README.md
Normal file
@@ -0,0 +1,21 @@
|
||||
# 预编译固件
|
||||
|
||||
目录:[`cyd/`](cyd/)
|
||||
|
||||
板子:**CYD ESP32-2432S028R**(ILI9341)
|
||||
功能:触控感应按钮(GPIO22)计数 + SoftSPI 触控 + LVGL(壳同 btntest,电容触摸专用滤波)
|
||||
|
||||
## 刷写
|
||||
|
||||
```powershell
|
||||
cd firmware\cyd
|
||||
.\flash.ps1
|
||||
# 或 .\flash.ps1 -Port COM3
|
||||
```
|
||||
|
||||
## 重新打包
|
||||
|
||||
```powershell
|
||||
.\firmware\export_from_build.ps1
|
||||
.\firmware\export_from_build.ps1 -SkipBuild
|
||||
```
|
||||
16
firmware/cyd/VERSION.txt
Normal file
16
firmware/cyd/VERSION.txt
Normal file
@@ -0,0 +1,16 @@
|
||||
name: CYD Touch Button (cyd)
|
||||
git: unknown
|
||||
date: 2026-10-23
|
||||
board: ESP32-2432S028R (ILI9341)
|
||||
features: SoftSPI touch, LVGL, capacitive touch btn on GPIO22
|
||||
flash_size: 4MB
|
||||
offsets:
|
||||
0x1000 bootloader.bin
|
||||
0x8000 partitions.bin
|
||||
0xe000 boot_app0.bin
|
||||
0x10000 firmware.bin
|
||||
sha256:
|
||||
bootloader.bin 3D234A7471F67B013686DABD4DEE7C1FA915C9928463616A94BC9297ACF1ABF8
|
||||
boot_app0.bin F94C5D786A7A8FAB06AC5D10E33BF37711A6697636DC037559EA19CC410A17F0
|
||||
firmware.bin 0423AA960BA1E4ADF3EFA6D40426A9047B2B9401BA27FF5E36C03F175012794F
|
||||
partitions.bin 148B959CBFF1C38AA8E1D5C0BA9D612C54997B945E56A63F41223EEF650653A1
|
||||
BIN
firmware/cyd/boot_app0.bin
Normal file
BIN
firmware/cyd/boot_app0.bin
Normal file
Binary file not shown.
BIN
firmware/cyd/bootloader.bin
Normal file
BIN
firmware/cyd/bootloader.bin
Normal file
Binary file not shown.
BIN
firmware/cyd/firmware.bin
Normal file
BIN
firmware/cyd/firmware.bin
Normal file
Binary file not shown.
5
firmware/cyd/flash.bat
Normal file
5
firmware/cyd/flash.bat
Normal file
@@ -0,0 +1,5 @@
|
||||
@echo off
|
||||
setlocal
|
||||
cd /d "%~dp0"
|
||||
powershell -NoProfile -ExecutionPolicy Bypass -File "%~dp0flash.ps1" %*
|
||||
exit /b %ERRORLEVEL%
|
||||
98
firmware/cyd/flash.ps1
Normal file
98
firmware/cyd/flash.ps1
Normal file
@@ -0,0 +1,98 @@
|
||||
# Flash archived CYD TF+Speaker firmware (no rebuild needed)
|
||||
# Usage:
|
||||
# .\flash.ps1
|
||||
# .\flash.ps1 -Port COM5
|
||||
# .\flash.ps1 -Port COM5 -Baud 460800
|
||||
|
||||
param(
|
||||
[string]$Port = "",
|
||||
[int]$Baud = 921600
|
||||
)
|
||||
|
||||
$ErrorActionPreference = "Stop"
|
||||
$here = Split-Path -Parent $MyInvocation.MyCommand.Path
|
||||
Set-Location $here
|
||||
|
||||
function Find-Esptool {
|
||||
$pio = Get-Command pio -ErrorAction SilentlyContinue
|
||||
if (-not $pio) {
|
||||
$pioPath = Join-Path $env:USERPROFILE "AppData\Local\Programs\Python\Python312\Scripts\pio.exe"
|
||||
if (Test-Path $pioPath) {
|
||||
$env:Path = "$(Split-Path $pioPath);$env:Path"
|
||||
$pio = Get-Command pio -ErrorAction SilentlyContinue
|
||||
}
|
||||
}
|
||||
if ($pio) {
|
||||
$pkg = Join-Path $env:USERPROFILE ".platformio\packages\tool-esptoolpy\esptool.py"
|
||||
if (Test-Path $pkg) { return @{ Kind = "py"; Path = $pkg } }
|
||||
}
|
||||
$py = Get-Command python -ErrorAction SilentlyContinue
|
||||
if ($py) {
|
||||
try {
|
||||
& python -c "import esptool" 2>$null
|
||||
if ($LASTEXITCODE -eq 0) { return @{ Kind = "mod"; Path = "esptool" } }
|
||||
} catch {}
|
||||
}
|
||||
$esp = Get-Command esptool.py -ErrorAction SilentlyContinue
|
||||
if ($esp) { return @{ Kind = "py"; Path = $esp.Source } }
|
||||
return $null
|
||||
}
|
||||
|
||||
if (-not $Port) {
|
||||
$ports = @()
|
||||
try {
|
||||
Add-Type -AssemblyName System.IO.Ports -ErrorAction SilentlyContinue | Out-Null
|
||||
$ports = [System.IO.Ports.SerialPort]::GetPortNames() | Sort-Object
|
||||
} catch {}
|
||||
if (-not $ports -or $ports.Count -eq 0) {
|
||||
$ports = Get-CimInstance Win32_SerialPort -ErrorAction SilentlyContinue |
|
||||
Select-Object -ExpandProperty DeviceID | Sort-Object
|
||||
}
|
||||
if (-not $ports -or $ports.Count -eq 0) {
|
||||
Write-Error "No serial ports found. Plug in the CYD USB cable, then retry with -Port COMx"
|
||||
}
|
||||
if ($ports.Count -eq 1) {
|
||||
$Port = $ports[0]
|
||||
} else {
|
||||
Write-Host "Available ports: $($ports -join ', ')"
|
||||
$Port = $ports[-1]
|
||||
Write-Host "Using last port: $Port (override with -Port)"
|
||||
}
|
||||
}
|
||||
|
||||
$tool = Find-Esptool
|
||||
if (-not $tool) {
|
||||
Write-Error "esptool not found. Install PlatformIO (`pio`) or: pip install esptool"
|
||||
}
|
||||
|
||||
$needed = @("bootloader.bin", "partitions.bin", "boot_app0.bin", "firmware.bin")
|
||||
foreach ($f in $needed) {
|
||||
if (-not (Test-Path (Join-Path $here $f))) {
|
||||
Write-Error "Missing $f — run ..\export_from_build.ps1 from repo root first"
|
||||
}
|
||||
}
|
||||
|
||||
$argsCommon = @(
|
||||
"--chip", "esp32",
|
||||
"--port", $Port,
|
||||
"--baud", "$Baud",
|
||||
"--before", "default_reset",
|
||||
"--after", "hard_reset",
|
||||
"write_flash", "-z",
|
||||
"--flash_mode", "dio",
|
||||
"--flash_freq", "40m",
|
||||
"--flash_size", "4MB",
|
||||
"0x1000", "bootloader.bin",
|
||||
"0x8000", "partitions.bin",
|
||||
"0xe000", "boot_app0.bin",
|
||||
"0x10000", "firmware.bin"
|
||||
)
|
||||
|
||||
Write-Host "Flashing $here -> $Port @ $Baud"
|
||||
if ($tool.Kind -eq "mod") {
|
||||
& python -m esptool @argsCommon
|
||||
} else {
|
||||
& python $tool.Path @argsCommon
|
||||
}
|
||||
if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE }
|
||||
Write-Host "Done. Open serial monitor at 115200 if needed."
|
||||
7
firmware/cyd/meta.json
Normal file
7
firmware/cyd/meta.json
Normal file
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"boardId": "esp32-2432s028",
|
||||
"name": "CYD Touch Button",
|
||||
"summary": "Capacitive touch module (TTP223-like) on GPIO22: each touch increments count",
|
||||
"description": "Based on btntest shell. SoftSPI touch + LVGL. Capacitive debounce: majority vote, min touch 35ms, rearm after release. VCC/GND/OUT->GPIO22. Auto polarity. Touch Reset clears.",
|
||||
"tags": ["touch-button", "capacitive", "ttp223", "gpio22", "lvgl", "counter"]
|
||||
}
|
||||
BIN
firmware/cyd/partitions.bin
Normal file
BIN
firmware/cyd/partitions.bin
Normal file
Binary file not shown.
57
firmware/export_from_build.ps1
Normal file
57
firmware/export_from_build.ps1
Normal file
@@ -0,0 +1,57 @@
|
||||
# Rebuild (optional) and refresh firmware/cyd from PlatformIO build output.
|
||||
# Usage:
|
||||
# .\firmware\export_from_build.ps1
|
||||
# .\firmware\export_from_build.ps1 -SkipBuild
|
||||
|
||||
param([switch]$SkipBuild)
|
||||
|
||||
$ErrorActionPreference = "Stop"
|
||||
$root = Split-Path -Parent (Split-Path -Parent $MyInvocation.MyCommand.Path)
|
||||
Set-Location $root
|
||||
|
||||
$env:Path = "C:\Users\dsx2016\AppData\Local\Programs\Python\Python312\Scripts;" + $env:Path
|
||||
|
||||
if (-not $SkipBuild) {
|
||||
pio run -e cyd
|
||||
if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE }
|
||||
}
|
||||
|
||||
$src = Join-Path $root ".pio\build\cyd"
|
||||
$dst = Join-Path $root "firmware\cyd"
|
||||
New-Item -ItemType Directory -Force -Path $dst | Out-Null
|
||||
|
||||
foreach ($f in @("bootloader.bin", "partitions.bin", "firmware.bin")) {
|
||||
$p = Join-Path $src $f
|
||||
if (-not (Test-Path $p)) { Write-Error "Missing build artifact: $p" }
|
||||
Copy-Item $p $dst -Force
|
||||
}
|
||||
|
||||
$bootApp = Join-Path $env:USERPROFILE ".platformio\packages\framework-arduinoespressif32\tools\partitions\boot_app0.bin"
|
||||
if (-not (Test-Path $bootApp)) {
|
||||
Write-Error "boot_app0.bin not found under PlatformIO packages"
|
||||
}
|
||||
Copy-Item $bootApp (Join-Path $dst "boot_app0.bin") -Force
|
||||
|
||||
$git = ""
|
||||
try { $git = (git -C $root rev-parse --short HEAD).Trim() } catch { $git = "unknown" }
|
||||
$date = Get-Date -Format "yyyy-MM-dd"
|
||||
$hashes = Get-ChildItem (Join-Path $dst "*.bin") | Get-FileHash -Algorithm SHA256
|
||||
$lines = @(
|
||||
"name: CYD Touch Button (cyd)",
|
||||
"git: $git",
|
||||
"date: $date",
|
||||
"board: ESP32-2432S028R (ILI9341)",
|
||||
"features: SoftSPI touch, LVGL, capacitive touch btn on GPIO22",
|
||||
"flash_size: 4MB",
|
||||
"offsets:",
|
||||
" 0x1000 bootloader.bin",
|
||||
" 0x8000 partitions.bin",
|
||||
" 0xe000 boot_app0.bin",
|
||||
" 0x10000 firmware.bin",
|
||||
"sha256:"
|
||||
)
|
||||
foreach ($h in $hashes) {
|
||||
$lines += (" {0,-15} {1}" -f (Split-Path $h.Path -Leaf), $h.Hash)
|
||||
}
|
||||
$lines | Set-Content -Encoding utf8 (Join-Path $dst "VERSION.txt")
|
||||
Write-Host "Exported -> $dst (git $git)"
|
||||
16
include/button.h
Normal file
16
include/button.h
Normal file
@@ -0,0 +1,16 @@
|
||||
#pragma once
|
||||
|
||||
#include <stdint.h>
|
||||
|
||||
void button_init();
|
||||
/** 轮询:检测有效触摸边沿,返回本周期是否有一次有效触摸 */
|
||||
bool button_poll_press();
|
||||
uint32_t button_count();
|
||||
void button_reset_count();
|
||||
bool button_is_down();
|
||||
/** 当前持续触摸时长(ms);未触摸为 0 */
|
||||
uint32_t button_hold_ms();
|
||||
/** 原始电平:1=HIGH,0=LOW */
|
||||
int button_raw_level();
|
||||
/** 触摸有效电平:1=HIGH 有效,0=LOW 有效(开机按空闲电平自动判定) */
|
||||
int button_active_level();
|
||||
1127
include/lv_conf.h
Normal file
1127
include/lv_conf.h
Normal file
File diff suppressed because it is too large
Load Diff
20
include/pins.h
Normal file
20
include/pins.h
Normal file
@@ -0,0 +1,20 @@
|
||||
#pragma once
|
||||
|
||||
// CYD ESP32-2432S028R — 触控/屏脚与 tftest / btntest 一致
|
||||
|
||||
#define TFT_BL_PIN 21
|
||||
|
||||
#define CYD_TOUCH_IRQ 36
|
||||
#define CYD_TOUCH_MOSI 32
|
||||
#define CYD_TOUCH_MISO 39
|
||||
#define CYD_TOUCH_CLK 25
|
||||
#define CYD_TOUCH_CS 33
|
||||
|
||||
// 三脚触控感应模块 OUT/S/DO → GPIO22(VCC→3.3V,GND→GND)
|
||||
// 典型:TTP223 电容触摸(非机械轻触)
|
||||
#define BTN_PIN 22
|
||||
|
||||
#define SCREEN_WIDTH 240
|
||||
#define SCREEN_HEIGHT 320
|
||||
#define UI_HOR_RES 320
|
||||
#define UI_VER_RES 240
|
||||
12
include/touch.h
Normal file
12
include/touch.h
Normal file
@@ -0,0 +1,12 @@
|
||||
#pragma once
|
||||
|
||||
#include <lvgl.h>
|
||||
#include <stdint.h>
|
||||
|
||||
void touch_init();
|
||||
void touch_read_cb(lv_indev_t *indev, lv_indev_data_t *data);
|
||||
|
||||
// 调试:最近一次触点(物理 / LVGL 旋转后逻辑)
|
||||
extern int16_t g_dbg_phys_x, g_dbg_phys_y;
|
||||
extern int16_t g_dbg_logic_x, g_dbg_logic_y;
|
||||
extern bool g_dbg_touch_down;
|
||||
4
include/ui.h
Normal file
4
include/ui.h
Normal file
@@ -0,0 +1,4 @@
|
||||
#pragma once
|
||||
|
||||
void ui_create();
|
||||
void ui_refresh_count();
|
||||
56
platformio.ini
Normal file
56
platformio.ini
Normal file
@@ -0,0 +1,56 @@
|
||||
; CYD ESP32-2432S028 — Capacitive touch btn(GPIO22) + SoftSPI 触控 + LVGL
|
||||
; 壳复用 btntest;触控/屏配置参考 tftest / hw269test
|
||||
[env:cyd]
|
||||
platform = espressif32
|
||||
board = esp32dev
|
||||
framework = arduino
|
||||
monitor_speed = 460800
|
||||
upload_speed = 460800
|
||||
board_build.partitions = default.csv
|
||||
|
||||
lib_deps =
|
||||
bodmer/TFT_eSPI@^2.5.43
|
||||
lvgl/lvgl@9.2.2
|
||||
|
||||
build_flags =
|
||||
-DCORE_DEBUG_LEVEL=0
|
||||
-DUSER_SETUP_LOADED=1
|
||||
-DILI9341_2_DRIVER=1
|
||||
-DTFT_WIDTH=240
|
||||
-DTFT_HEIGHT=320
|
||||
-DTFT_MISO=12
|
||||
-DTFT_MOSI=13
|
||||
-DTFT_SCLK=14
|
||||
-DTFT_CS=15
|
||||
-DTFT_DC=2
|
||||
-DTFT_RST=-1
|
||||
-DTFT_BL=21
|
||||
-DTFT_BACKLIGHT_ON=HIGH
|
||||
-DTOUCH_CS=-1
|
||||
-DLOAD_GLCD=1
|
||||
-DLOAD_FONT2=1
|
||||
-DLOAD_FONT4=1
|
||||
-DLOAD_FONT6=1
|
||||
-DLOAD_FONT7=1
|
||||
-DLOAD_FONT8=1
|
||||
-DLOAD_GFXFF=1
|
||||
-DSMOOTH_FONT=1
|
||||
-DSPI_FREQUENCY=40000000
|
||||
-DSPI_READ_FREQUENCY=20000000
|
||||
-DLV_CONF_SKIP
|
||||
-DLV_COLOR_DEPTH=16
|
||||
-DLV_MEM_SIZE=49152
|
||||
-DLV_FONT_MONTSERRAT_14=1
|
||||
-DLV_FONT_MONTSERRAT_16=1
|
||||
-DLV_FONT_MONTSERRAT_18=1
|
||||
-DLV_FONT_MONTSERRAT_20=1
|
||||
-DLV_FONT_MONTSERRAT_28=1
|
||||
-DLV_USE_TFT_ESPI=1
|
||||
-DLV_USE_FLEX=1
|
||||
-DLV_USE_BUTTON=1
|
||||
-DLV_USE_LABEL=1
|
||||
-DLV_USE_BAR=1
|
||||
-DLV_USE_SLIDER=1
|
||||
-I${PROJECT_DIR}/include
|
||||
|
||||
extra_scripts = pre:copy_lv_conf.py
|
||||
131
src/button.cpp
Normal file
131
src/button.cpp
Normal file
@@ -0,0 +1,131 @@
|
||||
#include "button.h"
|
||||
#include "pins.h"
|
||||
|
||||
#include <Arduino.h>
|
||||
|
||||
/**
|
||||
* 触控感应(电容触摸,如 TTP223)相对机械轻触:
|
||||
* - 无机械弹跳,但接近阈值时会抖沿
|
||||
* - 手指悬停/缓慢离开易连计
|
||||
* 策略:多数表决 + 积分迟滞 + 最短确认时间 + 松手再武装
|
||||
*/
|
||||
|
||||
static uint32_t g_count = 0;
|
||||
static bool g_stable_down = false;
|
||||
static bool g_counted_this_touch = false;
|
||||
static int g_active_level = HIGH;
|
||||
static int g_idle_level = LOW;
|
||||
static int8_t g_score = 0;
|
||||
static uint32_t g_down_since_ms = 0;
|
||||
static uint32_t g_rearm_until_ms = 0;
|
||||
static uint32_t g_hold_start_ms = 0;
|
||||
|
||||
// 比机械键更稳:更宽迟滞、更长确认、松手后再武装
|
||||
static constexpr int8_t SCORE_MAX = 8;
|
||||
static constexpr int8_t DOWN_TH = 5; // 约需连续多次“触摸”采样
|
||||
static constexpr int8_t UP_TH = -4; // 离开要更干净才算松开
|
||||
static constexpr uint32_t MIN_TOUCH_MS = 35; // 稳定触摸至少这么久才 +1
|
||||
static constexpr uint32_t REARM_MS = 120; // 松手后冷却,防悬停连计
|
||||
static constexpr int MAJORITY_N = 5;
|
||||
|
||||
static bool raw_is_down(int raw) { return raw == g_active_level; }
|
||||
|
||||
static int read_majority() {
|
||||
int high_n = 0;
|
||||
for (int i = 0; i < MAJORITY_N; i++) {
|
||||
if (digitalRead(BTN_PIN) == HIGH) {
|
||||
high_n++;
|
||||
}
|
||||
delayMicroseconds(30);
|
||||
}
|
||||
return (high_n >= ((MAJORITY_N / 2) + 1)) ? HIGH : LOW;
|
||||
}
|
||||
|
||||
void button_init() {
|
||||
// 电容模块多为推挽输出:用浮空 INPUT,避免上/下拉与模块打架
|
||||
pinMode(BTN_PIN, INPUT);
|
||||
delay(30);
|
||||
|
||||
int high_n = 0;
|
||||
for (int i = 0; i < 40; i++) {
|
||||
if (digitalRead(BTN_PIN) == HIGH) {
|
||||
high_n++;
|
||||
}
|
||||
delay(2);
|
||||
}
|
||||
g_idle_level = (high_n >= 20) ? HIGH : LOW;
|
||||
g_active_level = (g_idle_level == HIGH) ? LOW : HIGH;
|
||||
|
||||
g_stable_down = false;
|
||||
g_counted_this_touch = false;
|
||||
g_score = -SCORE_MAX;
|
||||
g_down_since_ms = 0;
|
||||
g_rearm_until_ms = 0;
|
||||
g_hold_start_ms = 0;
|
||||
g_count = 0;
|
||||
|
||||
Serial.printf("TouchBtn GPIO%d idle=%s active=%s (capacitive, float INPUT)\n",
|
||||
BTN_PIN, g_idle_level == HIGH ? "HIGH" : "LOW",
|
||||
g_active_level == HIGH ? "HIGH" : "LOW");
|
||||
}
|
||||
|
||||
bool button_poll_press() {
|
||||
const int raw = read_majority();
|
||||
if (raw_is_down(raw)) {
|
||||
if (g_score < SCORE_MAX) {
|
||||
g_score++;
|
||||
}
|
||||
} else if (g_score > -SCORE_MAX) {
|
||||
g_score--;
|
||||
}
|
||||
|
||||
const uint32_t now = millis();
|
||||
bool fired = false;
|
||||
|
||||
if (!g_stable_down && g_score >= DOWN_TH) {
|
||||
g_stable_down = true;
|
||||
g_counted_this_touch = false;
|
||||
g_down_since_ms = now;
|
||||
g_hold_start_ms = now;
|
||||
} else if (g_stable_down && g_score <= UP_TH) {
|
||||
g_stable_down = false;
|
||||
g_hold_start_ms = 0;
|
||||
g_down_since_ms = 0;
|
||||
// 松手后冷却:悬停/回弹不会立刻再计
|
||||
g_rearm_until_ms = now + REARM_MS;
|
||||
g_counted_this_touch = false;
|
||||
}
|
||||
|
||||
if (g_stable_down && !g_counted_this_touch && now >= g_rearm_until_ms) {
|
||||
if ((now - g_down_since_ms) >= MIN_TOUCH_MS) {
|
||||
g_count++;
|
||||
g_counted_this_touch = true;
|
||||
fired = true;
|
||||
Serial.printf("Touch press count=%lu hold>=%lums score=%d\n",
|
||||
(unsigned long)g_count, (unsigned long)MIN_TOUCH_MS,
|
||||
(int)g_score);
|
||||
}
|
||||
}
|
||||
|
||||
return fired;
|
||||
}
|
||||
|
||||
uint32_t button_count() { return g_count; }
|
||||
|
||||
void button_reset_count() {
|
||||
g_count = 0;
|
||||
Serial.println("TouchBtn count reset");
|
||||
}
|
||||
|
||||
bool button_is_down() { return g_stable_down; }
|
||||
|
||||
uint32_t button_hold_ms() {
|
||||
if (!g_stable_down || g_hold_start_ms == 0) {
|
||||
return 0;
|
||||
}
|
||||
return millis() - g_hold_start_ms;
|
||||
}
|
||||
|
||||
int button_raw_level() { return digitalRead(BTN_PIN); }
|
||||
|
||||
int button_active_level() { return g_active_level; }
|
||||
70
src/main.cpp
Normal file
70
src/main.cpp
Normal file
@@ -0,0 +1,70 @@
|
||||
#include <Arduino.h>
|
||||
#include <lvgl.h>
|
||||
|
||||
#include "button.h"
|
||||
#include "pins.h"
|
||||
#include "touch.h"
|
||||
#include "ui.h"
|
||||
|
||||
static constexpr uint32_t DRAW_BUF_SIZE =
|
||||
(SCREEN_WIDTH * SCREEN_HEIGHT / 10 * (LV_COLOR_DEPTH / 8));
|
||||
static uint32_t draw_buf[DRAW_BUF_SIZE / 4];
|
||||
|
||||
void setup() {
|
||||
Serial.begin(460800);
|
||||
delay(200);
|
||||
Serial.println();
|
||||
Serial.printf("LVGL %d.%d.%d | Capacitive touch btn GPIO%d\n",
|
||||
lv_version_major(), lv_version_minor(), lv_version_patch(),
|
||||
BTN_PIN);
|
||||
Serial.println("BOOT_OK");
|
||||
|
||||
pinMode(TFT_BL_PIN, OUTPUT);
|
||||
digitalWrite(TFT_BL_PIN, HIGH);
|
||||
|
||||
touch_init();
|
||||
button_init();
|
||||
|
||||
lv_init();
|
||||
|
||||
lv_display_t *disp =
|
||||
lv_tft_espi_create(SCREEN_WIDTH, SCREEN_HEIGHT, draw_buf, sizeof(draw_buf));
|
||||
lv_display_set_rotation(disp, LV_DISPLAY_ROTATION_270);
|
||||
|
||||
lv_indev_t *indev = lv_indev_create();
|
||||
lv_indev_set_type(indev, LV_INDEV_TYPE_POINTER);
|
||||
lv_indev_set_read_cb(indev, touch_read_cb);
|
||||
lv_indev_set_display(indev, disp);
|
||||
|
||||
ui_create();
|
||||
}
|
||||
|
||||
void loop() {
|
||||
// 电容模块沿抖更慢:每圈多采几次,配合积分+最短确认
|
||||
bool pressed = false;
|
||||
for (int i = 0; i < 4; i++) {
|
||||
if (button_poll_press()) {
|
||||
pressed = true;
|
||||
}
|
||||
delayMicroseconds(250);
|
||||
}
|
||||
|
||||
static bool last_down = false;
|
||||
static int last_raw = -1;
|
||||
static uint32_t last_hold_bucket = 0;
|
||||
const bool down = button_is_down();
|
||||
const int raw = button_raw_level();
|
||||
// hold 每 ~50ms 刷新一次,触摸条能平滑走
|
||||
const uint32_t hold_bucket = button_hold_ms() / 50;
|
||||
if (pressed || down != last_down || raw != last_raw ||
|
||||
(down && hold_bucket != last_hold_bucket)) {
|
||||
last_down = down;
|
||||
last_raw = raw;
|
||||
last_hold_bucket = hold_bucket;
|
||||
ui_refresh_count();
|
||||
}
|
||||
|
||||
lv_timer_handler();
|
||||
lv_tick_inc(5);
|
||||
delay(5);
|
||||
}
|
||||
125
src/touch.cpp
Normal file
125
src/touch.cpp
Normal file
@@ -0,0 +1,125 @@
|
||||
#include "touch.h"
|
||||
#include "pins.h"
|
||||
|
||||
#include <Arduino.h>
|
||||
|
||||
/**
|
||||
* SoftSPI XPT2046
|
||||
* BL↔TR 是矩形主对角线镜像(XY 对调),不是 180°(180° 会搞成 TL↔BR)。
|
||||
* 先按 rot2 得到旧 UI,再做对角修正,反变换喂给 LVGL 270。
|
||||
*/
|
||||
|
||||
int16_t g_dbg_phys_x = -1;
|
||||
int16_t g_dbg_phys_y = -1;
|
||||
int16_t g_dbg_logic_x = -1;
|
||||
int16_t g_dbg_logic_y = -1;
|
||||
bool g_dbg_touch_down = false;
|
||||
|
||||
static void soft_write(uint8_t data) {
|
||||
for (int i = 7; i >= 0; --i) {
|
||||
digitalWrite(CYD_TOUCH_MOSI, (data >> i) & 1);
|
||||
digitalWrite(CYD_TOUCH_CLK, HIGH);
|
||||
delayMicroseconds(2);
|
||||
digitalWrite(CYD_TOUCH_CLK, LOW);
|
||||
delayMicroseconds(2);
|
||||
}
|
||||
}
|
||||
|
||||
static uint16_t soft_read12() {
|
||||
uint16_t v = 0;
|
||||
digitalWrite(CYD_TOUCH_CLK, HIGH);
|
||||
delayMicroseconds(2);
|
||||
digitalWrite(CYD_TOUCH_CLK, LOW);
|
||||
delayMicroseconds(2);
|
||||
for (int i = 0; i < 12; ++i) {
|
||||
digitalWrite(CYD_TOUCH_CLK, HIGH);
|
||||
delayMicroseconds(2);
|
||||
v = (uint16_t)((v << 1) | (digitalRead(CYD_TOUCH_MISO) ? 1u : 0u));
|
||||
digitalWrite(CYD_TOUCH_CLK, LOW);
|
||||
delayMicroseconds(2);
|
||||
}
|
||||
return v;
|
||||
}
|
||||
|
||||
static uint16_t xpt_read(uint8_t cmd) {
|
||||
digitalWrite(CYD_TOUCH_CS, LOW);
|
||||
soft_write(cmd);
|
||||
uint16_t v = soft_read12();
|
||||
digitalWrite(CYD_TOUCH_CS, HIGH);
|
||||
return v;
|
||||
}
|
||||
|
||||
static int16_t clampi(int32_t v, int32_t lo, int32_t hi) {
|
||||
if (v < lo) {
|
||||
return (int16_t)lo;
|
||||
}
|
||||
if (v > hi) {
|
||||
return (int16_t)hi;
|
||||
}
|
||||
return (int16_t)v;
|
||||
}
|
||||
|
||||
void touch_init() {
|
||||
pinMode(CYD_TOUCH_CS, OUTPUT);
|
||||
pinMode(CYD_TOUCH_CLK, OUTPUT);
|
||||
pinMode(CYD_TOUCH_MOSI, OUTPUT);
|
||||
pinMode(CYD_TOUCH_MISO, INPUT);
|
||||
pinMode(CYD_TOUCH_IRQ, INPUT);
|
||||
digitalWrite(CYD_TOUCH_CS, HIGH);
|
||||
digitalWrite(CYD_TOUCH_CLK, LOW);
|
||||
Serial.println("Touch: SoftSPI + diagonal fix (BL/TR)");
|
||||
}
|
||||
|
||||
void touch_read_cb(lv_indev_t * /*indev*/, lv_indev_data_t *data) {
|
||||
uint16_t adc_x = xpt_read(0x90);
|
||||
uint16_t adc_y = xpt_read(0xD0);
|
||||
adc_x = (uint16_t)((adc_x + xpt_read(0x90)) / 2);
|
||||
adc_y = (uint16_t)((adc_y + xpt_read(0xD0)) / 2);
|
||||
|
||||
const bool irq = (digitalRead(CYD_TOUCH_IRQ) == LOW);
|
||||
const bool in_range =
|
||||
(adc_x > 80 && adc_x < 4000 && adc_y > 80 && adc_y < 4000);
|
||||
|
||||
if (!in_range || (!irq && (adc_x < 200 || adc_y < 200))) {
|
||||
data->state = LV_INDEV_STATE_RELEASED;
|
||||
g_dbg_touch_down = false;
|
||||
return;
|
||||
}
|
||||
|
||||
const int16_t p_x = (int16_t)adc_x;
|
||||
const int16_t p_y = (int16_t)(4095 - adc_y);
|
||||
|
||||
int16_t x = map(p_x, 200, 3700, 1, SCREEN_WIDTH);
|
||||
int16_t y = map(p_y, 240, 3800, 1, SCREEN_HEIGHT);
|
||||
x = clampi(x, 0, SCREEN_WIDTH - 1);
|
||||
y = clampi(y, 0, SCREEN_HEIGHT - 1);
|
||||
|
||||
// 旧 UI(LVGL270):(y, 239-x) — 该空间里 BL↔TR,做主对角线镜像
|
||||
// (ux,uy) -> (uy*(319/239), ux*(239/319))
|
||||
const int32_t old_ux = y;
|
||||
const int32_t old_uy = (SCREEN_WIDTH - 1) - x;
|
||||
int16_t ui_x = clampi((old_uy * (UI_HOR_RES - 1)) / (UI_VER_RES - 1), 0,
|
||||
UI_HOR_RES - 1);
|
||||
int16_t ui_y = clampi((old_ux * (UI_VER_RES - 1)) / (UI_HOR_RES - 1), 0,
|
||||
UI_VER_RES - 1);
|
||||
|
||||
// 反变换喂 LVGL,使最终落在 (ui_x, ui_y)
|
||||
const int16_t feed_x = (int16_t)((SCREEN_WIDTH - 1) - ui_y);
|
||||
const int16_t feed_y = ui_x;
|
||||
|
||||
g_dbg_phys_x = x;
|
||||
g_dbg_phys_y = y;
|
||||
g_dbg_logic_x = ui_x;
|
||||
g_dbg_logic_y = ui_y;
|
||||
g_dbg_touch_down = true;
|
||||
|
||||
static uint32_t last_log = 0;
|
||||
if (millis() - last_log > 80) {
|
||||
last_log = millis();
|
||||
Serial.printf("Touch UI(%d,%d)\n", ui_x, ui_y);
|
||||
}
|
||||
|
||||
data->point.x = feed_x;
|
||||
data->point.y = feed_y;
|
||||
data->state = LV_INDEV_STATE_PRESSED;
|
||||
}
|
||||
161
src/ui.cpp
Normal file
161
src/ui.cpp
Normal file
@@ -0,0 +1,161 @@
|
||||
#include "ui.h"
|
||||
#include "button.h"
|
||||
#include "pins.h"
|
||||
|
||||
#include <Arduino.h>
|
||||
#include <lvgl.h>
|
||||
#include <stdio.h>
|
||||
|
||||
static constexpr uint32_t COL_BG = 0x0A1016;
|
||||
static constexpr uint32_t COL_INK = 0xEAF0F6;
|
||||
static constexpr uint32_t COL_MUTED = 0x7A8B9C;
|
||||
static constexpr uint32_t COL_BTN = 0x243140;
|
||||
static constexpr uint32_t COL_ACCENT = 0x4FC3F7;
|
||||
static constexpr uint32_t COL_TOUCH = 0x3DDC97;
|
||||
|
||||
static lv_obj_t *lbl_title;
|
||||
static lv_obj_t *lbl_pin;
|
||||
static lv_obj_t *lbl_count;
|
||||
static lv_obj_t *lbl_hint;
|
||||
static lv_obj_t *lbl_state;
|
||||
static lv_obj_t *lbl_hold;
|
||||
static lv_obj_t *lbl_raw;
|
||||
static lv_obj_t *bar_touch;
|
||||
static lv_obj_t *btn_reset;
|
||||
static lv_obj_t *lbl_reset;
|
||||
|
||||
static void style_btn(lv_obj_t *btn, uint32_t bg) {
|
||||
lv_obj_set_style_radius(btn, 10, 0);
|
||||
lv_obj_set_style_bg_color(btn, lv_color_hex(bg), 0);
|
||||
lv_obj_set_style_bg_opa(btn, LV_OPA_COVER, 0);
|
||||
lv_obj_set_style_shadow_width(btn, 0, 0);
|
||||
lv_obj_set_style_border_width(btn, 0, 0);
|
||||
lv_obj_set_style_pad_all(btn, 0, 0);
|
||||
}
|
||||
|
||||
void ui_refresh_count() {
|
||||
if (lbl_count) {
|
||||
lv_label_set_text_fmt(lbl_count, "%lu", (unsigned long)button_count());
|
||||
}
|
||||
|
||||
const bool down = button_is_down();
|
||||
const uint32_t hold = button_hold_ms();
|
||||
|
||||
if (lbl_state) {
|
||||
lv_label_set_text(lbl_state, down ? "TOUCHING" : "IDLE");
|
||||
lv_obj_set_style_text_color(lbl_state,
|
||||
lv_color_hex(down ? COL_TOUCH : COL_MUTED), 0);
|
||||
}
|
||||
if (lbl_hold) {
|
||||
if (down) {
|
||||
lv_label_set_text_fmt(lbl_hold, "hold %lu ms", (unsigned long)hold);
|
||||
} else {
|
||||
lv_label_set_text(lbl_hold, "hold --");
|
||||
}
|
||||
lv_obj_set_style_text_color(lbl_hold,
|
||||
lv_color_hex(down ? COL_ACCENT : COL_MUTED), 0);
|
||||
}
|
||||
if (bar_touch) {
|
||||
// 触摸条:IDLE 空;触摸后按 hold 缓升(约 800ms 满)
|
||||
int32_t v = 0;
|
||||
if (down) {
|
||||
v = (int32_t)((hold > 800) ? 100 : (hold * 100) / 800);
|
||||
}
|
||||
lv_bar_set_value(bar_touch, v, LV_ANIM_OFF);
|
||||
lv_obj_set_style_bg_color(bar_touch,
|
||||
lv_color_hex(down ? COL_TOUCH : 0x1A2430),
|
||||
LV_PART_INDICATOR);
|
||||
}
|
||||
if (lbl_raw) {
|
||||
const int lv = button_raw_level();
|
||||
lv_label_set_text_fmt(lbl_raw, "OUT %s act=%s", lv ? "HIGH" : "LOW",
|
||||
button_active_level() ? "HIGH" : "LOW");
|
||||
lv_obj_set_style_text_color(lbl_raw,
|
||||
lv_color_hex(lv == button_active_level()
|
||||
? COL_TOUCH
|
||||
: COL_MUTED),
|
||||
0);
|
||||
}
|
||||
}
|
||||
|
||||
static void on_reset(lv_event_t *e) {
|
||||
if (lv_event_get_code(e) != LV_EVENT_CLICKED) {
|
||||
return;
|
||||
}
|
||||
button_reset_count();
|
||||
ui_refresh_count();
|
||||
}
|
||||
|
||||
void ui_create() {
|
||||
lv_obj_t *scr = lv_screen_active();
|
||||
lv_obj_set_style_bg_color(scr, lv_color_hex(COL_BG), 0);
|
||||
lv_obj_set_style_bg_opa(scr, LV_OPA_COVER, 0);
|
||||
|
||||
lbl_title = lv_label_create(scr);
|
||||
lv_label_set_text(lbl_title, "Touch");
|
||||
lv_obj_set_style_text_color(lbl_title, lv_color_hex(COL_INK), 0);
|
||||
lv_obj_set_style_text_font(lbl_title, &lv_font_montserrat_28, 0);
|
||||
lv_obj_align(lbl_title, LV_ALIGN_TOP_MID, 0, 2);
|
||||
|
||||
lbl_pin = lv_label_create(scr);
|
||||
lv_label_set_text_fmt(lbl_pin, "Cap OUT->GPIO%d 3.3V", BTN_PIN);
|
||||
lv_obj_set_style_text_color(lbl_pin, lv_color_hex(COL_MUTED), 0);
|
||||
lv_obj_set_style_text_font(lbl_pin, &lv_font_montserrat_14, 0);
|
||||
lv_obj_align(lbl_pin, LV_ALIGN_TOP_MID, 0, 32);
|
||||
|
||||
lbl_count = lv_label_create(scr);
|
||||
lv_label_set_text(lbl_count, "0");
|
||||
lv_obj_set_style_text_color(lbl_count, lv_color_hex(COL_ACCENT), 0);
|
||||
lv_obj_set_style_text_font(lbl_count, &lv_font_montserrat_28, 0);
|
||||
lv_obj_align(lbl_count, LV_ALIGN_TOP_MID, 0, 56);
|
||||
|
||||
lbl_hint = lv_label_create(scr);
|
||||
lv_label_set_text(lbl_hint, "count");
|
||||
lv_obj_set_style_text_color(lbl_hint, lv_color_hex(COL_MUTED), 0);
|
||||
lv_obj_set_style_text_font(lbl_hint, &lv_font_montserrat_14, 0);
|
||||
lv_obj_align(lbl_hint, LV_ALIGN_TOP_MID, 0, 90);
|
||||
|
||||
lbl_state = lv_label_create(scr);
|
||||
lv_label_set_text(lbl_state, "IDLE");
|
||||
lv_obj_set_style_text_color(lbl_state, lv_color_hex(COL_MUTED), 0);
|
||||
lv_obj_set_style_text_font(lbl_state, &lv_font_montserrat_20, 0);
|
||||
lv_obj_align(lbl_state, LV_ALIGN_TOP_MID, 0, 110);
|
||||
|
||||
lbl_hold = lv_label_create(scr);
|
||||
lv_label_set_text(lbl_hold, "hold --");
|
||||
lv_obj_set_style_text_color(lbl_hold, lv_color_hex(COL_MUTED), 0);
|
||||
lv_obj_set_style_text_font(lbl_hold, &lv_font_montserrat_14, 0);
|
||||
lv_obj_align(lbl_hold, LV_ALIGN_TOP_MID, 0, 134);
|
||||
|
||||
bar_touch = lv_bar_create(scr);
|
||||
lv_obj_set_size(bar_touch, UI_HOR_RES - 64, 10);
|
||||
lv_obj_align(bar_touch, LV_ALIGN_TOP_MID, 0, 154);
|
||||
lv_bar_set_range(bar_touch, 0, 100);
|
||||
lv_bar_set_value(bar_touch, 0, LV_ANIM_OFF);
|
||||
lv_obj_set_style_radius(bar_touch, 5, 0);
|
||||
lv_obj_set_style_bg_color(bar_touch, lv_color_hex(0x1A2430), 0);
|
||||
lv_obj_set_style_bg_opa(bar_touch, LV_OPA_COVER, 0);
|
||||
lv_obj_set_style_bg_color(bar_touch, lv_color_hex(COL_TOUCH),
|
||||
LV_PART_INDICATOR);
|
||||
lv_obj_set_style_radius(bar_touch, 5, LV_PART_INDICATOR);
|
||||
|
||||
lbl_raw = lv_label_create(scr);
|
||||
lv_label_set_text(lbl_raw, "OUT --");
|
||||
lv_obj_set_style_text_color(lbl_raw, lv_color_hex(COL_MUTED), 0);
|
||||
lv_obj_set_style_text_font(lbl_raw, &lv_font_montserrat_14, 0);
|
||||
lv_obj_align(lbl_raw, LV_ALIGN_TOP_MID, 0, 170);
|
||||
|
||||
btn_reset = lv_button_create(scr);
|
||||
lv_obj_set_size(btn_reset, UI_HOR_RES - 48, 40);
|
||||
lv_obj_align(btn_reset, LV_ALIGN_TOP_MID, 0, 192);
|
||||
style_btn(btn_reset, COL_BTN);
|
||||
lv_obj_set_ext_click_area(btn_reset, 12);
|
||||
lv_obj_add_event_cb(btn_reset, on_reset, LV_EVENT_CLICKED, nullptr);
|
||||
lbl_reset = lv_label_create(btn_reset);
|
||||
lv_label_set_text(lbl_reset, "Reset");
|
||||
lv_obj_set_style_text_color(lbl_reset, lv_color_hex(COL_INK), 0);
|
||||
lv_obj_set_style_text_font(lbl_reset, &lv_font_montserrat_20, 0);
|
||||
lv_obj_center(lbl_reset);
|
||||
|
||||
ui_refresh_count();
|
||||
}
|
||||
Reference in New Issue
Block a user