基本示波
This commit is contained in:
parent
9529ecd0d5
commit
042af6036e
47
include/ad7606.h
Normal file
47
include/ad7606.h
Normal file
@ -0,0 +1,47 @@
|
||||
#ifndef AD7606_H
|
||||
#define AD7606_H
|
||||
|
||||
#include <Arduino.h>
|
||||
#include <SPI.h>
|
||||
|
||||
// 可按需修改为你的实际接线。若 OS 引脚已硬连到 GND/3V3,可将其保持为 -1(不配置)。
|
||||
namespace ad7606 {
|
||||
|
||||
// 数据/控制引脚
|
||||
static const int PIN_AD_MISO = 8; // DOUTA -> ESP32 MISO
|
||||
static const int PIN_AD_CS = 12; // CS_N
|
||||
static const int PIN_AD_CONVST = 13; // CONVST(CO-A/CO-B 共用)
|
||||
static const int PIN_AD_RESET = 5; // RESET
|
||||
static const int PIN_AD_BUSY = 14; // BUSY
|
||||
|
||||
// 可选:过采样和模式引脚(-1 表示不配置/已硬件固定)
|
||||
static const int PIN_AD_OS0 = -1;
|
||||
static const int PIN_AD_OS1 = -1;
|
||||
static const int PIN_AD_OS2 = -1;
|
||||
static const int PIN_AD_SER = -1; // SER=1 串行模式(若已焊 3.3V,可 -1)
|
||||
static const int PIN_AD_STBY = -1; // STBY=1 正常工作(若已焊 3.3V,可 -1)
|
||||
|
||||
// 量程:用于将 16-bit 原码转换为电压(缺省 ±10V)
|
||||
static constexpr float AD_FS_VOLTS = 5.0f; // 根据硬件 RANGE 引脚实际设置改成 5.0/2.5 等
|
||||
|
||||
// 初始化(与 TFT 共用 SPI:外部需已调用 SPI.begin 并指定 MISO=SENSOR_DOUT)
|
||||
void init();
|
||||
|
||||
// 触发并读取 8 路原始数据(阻塞等待 BUSY 变低)
|
||||
void readAll(int16_t* data8);
|
||||
|
||||
// 读取 CH0 原始码
|
||||
inline int16_t readCH0() {
|
||||
int16_t tmp[8];
|
||||
readAll(tmp);
|
||||
return tmp[0];
|
||||
}
|
||||
|
||||
// 原码转电压,范围 ±AD_FS_VOLTS
|
||||
inline float codeToVolts(int16_t code) {
|
||||
return (static_cast<float>(code) / 32768.0f) * AD_FS_VOLTS;
|
||||
}
|
||||
|
||||
} // namespace ad7606
|
||||
|
||||
#endif
|
||||
@ -1,55 +0,0 @@
|
||||
#ifndef GAME_H
|
||||
#define GAME_H
|
||||
|
||||
#include <Arduino.h>
|
||||
#include "ui.h" // for DisplayList and colors
|
||||
|
||||
namespace esp32 {
|
||||
namespace game {
|
||||
|
||||
using namespace esp32::ui;
|
||||
|
||||
// 棋盘尺寸(经典 10x20)
|
||||
static const int COLS = 10;
|
||||
static const int ROWS = 20;
|
||||
|
||||
enum class State : uint8_t { Playing = 0, GameOver = 1 };
|
||||
|
||||
struct Piece {
|
||||
uint8_t type; // 0..6
|
||||
int8_t x; // 左上角/参考点
|
||||
int8_t y;
|
||||
// 仅使用一个朝向(简化:不提供旋转)
|
||||
int8_t blocks[4][2]; // 4 个方块偏移
|
||||
uint16_t color;
|
||||
};
|
||||
|
||||
struct Tetris {
|
||||
uint8_t board[ROWS][COLS]; // 0=空,其它=颜色索引或标记
|
||||
uint16_t colors[8]; // 颜色表(index->RGB565)
|
||||
Piece current;
|
||||
uint32_t lastDropMs = 0;
|
||||
uint32_t dropInterval = 700; // ms
|
||||
State state = State::Playing;
|
||||
uint32_t score = 0;
|
||||
uint32_t rng = 1;
|
||||
};
|
||||
|
||||
void init(Tetris& g);
|
||||
void reset(Tetris& g);
|
||||
void update(Tetris& g, uint32_t nowMs);
|
||||
|
||||
// 输入
|
||||
void moveLeft(Tetris& g);
|
||||
void moveRight(Tetris& g);
|
||||
void hardDrop(Tetris& g);
|
||||
|
||||
// UI 构建
|
||||
void buildScene(const Tetris& g, DisplayList& out,
|
||||
int16_t screenW, int16_t screenH,
|
||||
int16_t cell, int16_t originX, int16_t originY);
|
||||
|
||||
} // namespace game
|
||||
} // namespace esp32
|
||||
|
||||
#endif // GAME_H
|
||||
@ -1,30 +0,0 @@
|
||||
#ifndef TIMER_H
|
||||
#define TIMER_H
|
||||
|
||||
#include <Arduino.h>
|
||||
#include "ui.h"
|
||||
|
||||
namespace esp32 {
|
||||
namespace timer {
|
||||
|
||||
struct ClockState {
|
||||
uint32_t startMs = 0; // 起始时间
|
||||
uint32_t elapsedMs = 0; // 当前经过时间
|
||||
static const int MAX_LAPS = 12;
|
||||
uint16_t lapCount = 0;
|
||||
float lapAngles[MAX_LAPS]; // 存储圈记录的角度(秒针位置)
|
||||
uint32_t lapMs[MAX_LAPS]; // 每次记录的时间戳 (ms)
|
||||
bool running = true;
|
||||
};
|
||||
|
||||
void init(ClockState& s);
|
||||
void reset(ClockState& s);
|
||||
void update(ClockState& s, uint32_t nowMs);
|
||||
void addLap(ClockState& s);
|
||||
void buildScene(const ClockState& s, esp32::ui::DisplayList& out,
|
||||
int16_t screenW, int16_t screenH);
|
||||
|
||||
} // namespace timer
|
||||
} // namespace esp32
|
||||
|
||||
#endif // TIMER_H
|
||||
79
src/ad7606.cpp
Normal file
79
src/ad7606.cpp
Normal file
@ -0,0 +1,79 @@
|
||||
#include "ad7606.h"
|
||||
|
||||
namespace ad7606 {
|
||||
|
||||
static inline void pinWriteOpt(int pin, int level) {
|
||||
if (pin >= 0) digitalWrite(pin, level);
|
||||
}
|
||||
static inline void pinModeOpt(int pin, uint8_t mode) {
|
||||
if (pin >= 0) pinMode(pin, mode);
|
||||
}
|
||||
|
||||
static void resetPulse() {
|
||||
pinWriteOpt(PIN_AD_RESET, LOW);
|
||||
delayMicroseconds(2);
|
||||
pinWriteOpt(PIN_AD_RESET, HIGH);
|
||||
delayMicroseconds(2);
|
||||
pinWriteOpt(PIN_AD_RESET, LOW);
|
||||
}
|
||||
|
||||
static void startConvst() {
|
||||
pinWriteOpt(PIN_AD_CONVST, LOW);
|
||||
delayMicroseconds(1);
|
||||
pinWriteOpt(PIN_AD_CONVST, HIGH);
|
||||
delayMicroseconds(1);
|
||||
}
|
||||
|
||||
void init() {
|
||||
// 仅配置 GPIO;SPI 总线由外部统一 SPI.begin(...) 完成(与 TFT 共用)
|
||||
pinMode(PIN_AD_CS, OUTPUT);
|
||||
pinMode(PIN_AD_BUSY, INPUT);
|
||||
pinMode(PIN_AD_RESET, OUTPUT);
|
||||
pinMode(PIN_AD_CONVST, OUTPUT);
|
||||
|
||||
pinModeOpt(PIN_AD_OS0, OUTPUT);
|
||||
pinModeOpt(PIN_AD_OS1, OUTPUT);
|
||||
pinModeOpt(PIN_AD_OS2, OUTPUT);
|
||||
pinModeOpt(PIN_AD_SER, OUTPUT);
|
||||
pinModeOpt(PIN_AD_STBY, OUTPUT);
|
||||
|
||||
digitalWrite(PIN_AD_CS, HIGH);
|
||||
digitalWrite(PIN_AD_CONVST, HIGH);
|
||||
|
||||
// 若引脚未硬件固定,软件拉到默认工作状态
|
||||
pinWriteOpt(PIN_AD_SER, HIGH); // 串行模式
|
||||
pinWriteOpt(PIN_AD_STBY, HIGH); // 正常工作
|
||||
|
||||
// 不过采样 OS[2:0] = 000(若 OS 引脚未连,可忽略)
|
||||
pinWriteOpt(PIN_AD_OS0, LOW);
|
||||
pinWriteOpt(PIN_AD_OS1, LOW);
|
||||
pinWriteOpt(PIN_AD_OS2, LOW);
|
||||
|
||||
resetPulse();
|
||||
delay(1);
|
||||
|
||||
// 做一次假触发,确保内部状态正常
|
||||
startConvst();
|
||||
}
|
||||
|
||||
void readAll(int16_t* data8) {
|
||||
// 触发转换
|
||||
startConvst();
|
||||
|
||||
// 等 BUSY 低(高=转换进行中)
|
||||
while (digitalRead(PIN_AD_BUSY) == HIGH) {
|
||||
// 可根据需要加入超时
|
||||
}
|
||||
|
||||
// 读取8个16位(RD/SC 由 SPI SCLK 提供)
|
||||
SPI.beginTransaction(SPISettings(8000000, MSBFIRST, SPI_MODE0));
|
||||
digitalWrite(PIN_AD_CS, LOW);
|
||||
for (int i = 0; i < 8; ++i) {
|
||||
uint16_t raw = SPI.transfer16(0x0000);
|
||||
data8[i] = static_cast<int16_t>(raw);
|
||||
}
|
||||
digitalWrite(PIN_AD_CS, HIGH);
|
||||
SPI.endTransaction();
|
||||
}
|
||||
|
||||
} // namespace ad7606
|
||||
215
src/game.cpp
215
src/game.cpp
@ -1,215 +0,0 @@
|
||||
#include "game.h"
|
||||
|
||||
namespace esp32 {
|
||||
namespace game {
|
||||
|
||||
using namespace esp32::ui;
|
||||
using namespace esp32::utils;
|
||||
|
||||
static uint32_t xorshift32(uint32_t& s){
|
||||
uint32_t x = s;
|
||||
x ^= x << 13; x ^= x >> 17; x ^= x << 5; s = x; return x;
|
||||
}
|
||||
|
||||
static uint8_t rand7(uint32_t& s){ return (uint8_t)(xorshift32(s) % 7); }
|
||||
|
||||
static void setPieceShape(Piece& p){
|
||||
// 定义 7 种方块在默认朝向下的 4 个格子偏移
|
||||
// 坐标以 (0,0) 为参考,落点左上角附近
|
||||
switch (p.type){
|
||||
case 0: // I 形: 水平
|
||||
p.blocks[0][0] = -1; p.blocks[0][1] = 0;
|
||||
p.blocks[1][0] = 0; p.blocks[1][1] = 0;
|
||||
p.blocks[2][0] = 1; p.blocks[2][1] = 0;
|
||||
p.blocks[3][0] = 2; p.blocks[3][1] = 0; break;
|
||||
case 1: // O 方块
|
||||
p.blocks[0][0] = 0; p.blocks[0][1] = 0;
|
||||
p.blocks[1][0] = 1; p.blocks[1][1] = 0;
|
||||
p.blocks[2][0] = 0; p.blocks[2][1] = 1;
|
||||
p.blocks[3][0] = 1; p.blocks[3][1] = 1; break;
|
||||
case 2: // T
|
||||
p.blocks[0][0] = -1; p.blocks[0][1] = 0;
|
||||
p.blocks[1][0] = 0; p.blocks[1][1] = 0;
|
||||
p.blocks[2][0] = 1; p.blocks[2][1] = 0;
|
||||
p.blocks[3][0] = 0; p.blocks[3][1] = 1; break;
|
||||
case 3: // L
|
||||
p.blocks[0][0] = -1; p.blocks[0][1] = 0;
|
||||
p.blocks[1][0] = 0; p.blocks[1][1] = 0;
|
||||
p.blocks[2][0] = 1; p.blocks[2][1] = 0;
|
||||
p.blocks[3][0] = 1; p.blocks[3][1] = 1; break;
|
||||
case 4: // J
|
||||
p.blocks[0][0] = -1; p.blocks[0][1] = 0;
|
||||
p.blocks[1][0] = 0; p.blocks[1][1] = 0;
|
||||
p.blocks[2][0] = 1; p.blocks[2][1] = 0;
|
||||
p.blocks[3][0] = -1; p.blocks[3][1] = 1; break;
|
||||
case 5: // S
|
||||
p.blocks[0][0] = 0; p.blocks[0][1] = 0;
|
||||
p.blocks[1][0] = 1; p.blocks[1][1] = 0;
|
||||
p.blocks[2][0] = -1; p.blocks[2][1] = 1;
|
||||
p.blocks[3][0] = 0; p.blocks[3][1] = 1; break;
|
||||
case 6: // Z
|
||||
p.blocks[0][0] = -1; p.blocks[0][1] = 0;
|
||||
p.blocks[1][0] = 0; p.blocks[1][1] = 0;
|
||||
p.blocks[2][0] = 0; p.blocks[2][1] = 1;
|
||||
p.blocks[3][0] = 1; p.blocks[3][1] = 1; break;
|
||||
}
|
||||
}
|
||||
|
||||
static bool collide(const Tetris& g, const Piece& p, int nx, int ny){
|
||||
for (int i=0;i<4;++i){
|
||||
int cx = nx + p.blocks[i][0];
|
||||
int cy = ny + p.blocks[i][1];
|
||||
if (cx < 0 || cx >= COLS || cy >= ROWS) return true;
|
||||
if (cy >= 0 && g.board[cy][cx] != 0) return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
static void spawnPiece(Tetris& g){
|
||||
g.current.type = rand7(g.rng);
|
||||
g.current.x = COLS/2 - 1;
|
||||
g.current.y = -1; // 允许出屏生成
|
||||
setPieceShape(g.current);
|
||||
// 颜色:从表里取 1..7
|
||||
g.current.color = g.colors[1 + (g.current.type % 7)];
|
||||
if (collide(g, g.current, g.current.x, g.current.y+1)){
|
||||
g.state = State::GameOver;
|
||||
}
|
||||
}
|
||||
|
||||
static void lockPiece(Tetris& g){
|
||||
for (int i=0;i<4;++i){
|
||||
int cx = g.current.x + g.current.blocks[i][0];
|
||||
int cy = g.current.y + g.current.blocks[i][1];
|
||||
if (cy>=0 && cy<ROWS && cx>=0 && cx<COLS){
|
||||
g.board[cy][cx] = 1 + (g.current.type % 7);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static int clearLines(Tetris& g){
|
||||
int cleared = 0;
|
||||
for (int y=ROWS-1; y>=0; --y){
|
||||
bool full = true;
|
||||
for (int x=0; x<COLS; ++x){ if (g.board[y][x]==0){ full=false; break; } }
|
||||
if (full){
|
||||
++cleared;
|
||||
for (int yy=y; yy>0; --yy){
|
||||
for (int x=0;x<COLS;++x){ g.board[yy][x] = g.board[yy-1][x]; }
|
||||
}
|
||||
for (int x=0;x<COLS;++x) g.board[0][x]=0;
|
||||
++y; // 重新检查此行
|
||||
}
|
||||
}
|
||||
if (cleared>0){ g.score += cleared * 100; }
|
||||
return cleared;
|
||||
}
|
||||
|
||||
void init(Tetris& g){
|
||||
// 初始化颜色表
|
||||
g.colors[0] = 0; // 未用
|
||||
g.colors[1] = WHITE;
|
||||
g.colors[2] = rgbTo565(0x00FFFF); // CYAN
|
||||
g.colors[3] = YELLOW;
|
||||
g.colors[4] = GREEN;
|
||||
g.colors[5] = BLUE;
|
||||
g.colors[6] = RED;
|
||||
g.colors[7] = rgbTo565(0xFF00FF); // MAGENTA
|
||||
reset(g);
|
||||
}
|
||||
|
||||
void reset(Tetris& g){
|
||||
for (int y=0;y<ROWS;++y) for (int x=0;x<COLS;++x) g.board[y][x]=0;
|
||||
g.state = State::Playing;
|
||||
g.score = 0;
|
||||
g.dropInterval = 700;
|
||||
g.lastDropMs = millis();
|
||||
g.rng ^= millis(); if (g.rng==0) g.rng=1;
|
||||
spawnPiece(g);
|
||||
}
|
||||
|
||||
void update(Tetris& g, uint32_t nowMs){
|
||||
if (g.state != State::Playing) return;
|
||||
if (nowMs - g.lastDropMs >= g.dropInterval){
|
||||
g.lastDropMs = nowMs;
|
||||
if (!collide(g, g.current, g.current.x, g.current.y+1)){
|
||||
g.current.y += 1;
|
||||
} else {
|
||||
lockPiece(g);
|
||||
clearLines(g);
|
||||
spawnPiece(g);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void moveLeft(Tetris& g){
|
||||
if (g.state != State::Playing) return;
|
||||
if (!collide(g, g.current, g.current.x-1, g.current.y)){
|
||||
g.current.x -= 1;
|
||||
}
|
||||
}
|
||||
|
||||
void moveRight(Tetris& g){
|
||||
if (g.state != State::Playing) return;
|
||||
if (!collide(g, g.current, g.current.x+1, g.current.y)){
|
||||
g.current.x += 1;
|
||||
}
|
||||
}
|
||||
|
||||
void hardDrop(Tetris& g){
|
||||
if (g.state != State::Playing) return;
|
||||
while (!collide(g, g.current, g.current.x, g.current.y+1)){
|
||||
g.current.y += 1;
|
||||
}
|
||||
lockPiece(g);
|
||||
clearLines(g);
|
||||
spawnPiece(g);
|
||||
}
|
||||
|
||||
void buildScene(const Tetris& g, DisplayList& out,
|
||||
int16_t screenW, int16_t screenH,
|
||||
int16_t cell, int16_t originX, int16_t originY){
|
||||
// 边框
|
||||
out.addFillRect(900, originX-2, originY-2, COLS*cell+4, ROWS*cell+4, WHITE);
|
||||
out.addFillRect(901, originX-1, originY-1, COLS*cell+2, ROWS*cell+2, BLACK);
|
||||
|
||||
// 棋盘格子
|
||||
for (int y=0; y<ROWS; ++y){
|
||||
for (int x=0; x<COLS; ++x){
|
||||
uint8_t v = g.board[y][x];
|
||||
if (v){
|
||||
uint16_t c = g.colors[v % 8];
|
||||
out.addFillRect(1000 + y*COLS + x, originX + x*cell, originY + y*cell, cell-1, cell-1, c);
|
||||
} else {
|
||||
// 画浅网格可选:注释掉减少绘制量
|
||||
// out.addFillRect(2000 + y*COLS + x, originX + x*cell, originY + y*cell, cell-1, 1, 0x8410);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 当前方块
|
||||
if (g.state == State::Playing){
|
||||
for (int i=0;i<4;++i){
|
||||
int cx = g.current.x + g.current.blocks[i][0];
|
||||
int cy = g.current.y + g.current.blocks[i][1];
|
||||
if (cy>=0){
|
||||
out.addFillRect(3000 + i, originX + cx*cell, originY + cy*cell, cell-1, cell-1, g.current.color);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 顶部文字:Score
|
||||
char buf[24];
|
||||
snprintf(buf, sizeof(buf), "Score: %lu", (unsigned long)g.score);
|
||||
out.addText(8000, 4, 4, WHITE, buf);
|
||||
|
||||
if (g.state == State::GameOver){
|
||||
out.addText(8001, 8, screenH - 24, YELLOW, "Game Over");
|
||||
out.addText(8002, 8, screenH - 14, WHITE, "Left: Home Right: Retry");
|
||||
} else {
|
||||
out.addText(8003, 8, screenH - 14, WHITE, "Up: Drop Left/Right: Move");
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace game
|
||||
} // namespace esp32
|
||||
232
src/main.cpp
232
src/main.cpp
@ -2,11 +2,10 @@
|
||||
#include <Adafruit_ST7735.h>
|
||||
#include <SPI.h>
|
||||
#include <U8g2_for_Adafruit_GFX.h>
|
||||
#include "game.h"
|
||||
#include "init.h"
|
||||
#include "timer.h"
|
||||
#include "ui.h"
|
||||
#include "utils.h"
|
||||
#include "ad7606.h"
|
||||
|
||||
U8G2_FOR_ADAFRUIT_GFX u8g2; // 给 Adafruit_GFX 套一层 U8g2 的“文字引擎”
|
||||
using namespace esp32;
|
||||
@ -29,27 +28,22 @@ const int BTN_UP = 10; // 上按钮
|
||||
|
||||
// 颜色宏兼容已由 utils.h 提供
|
||||
|
||||
// 应用状态(主屏轮播)
|
||||
// 应用状态
|
||||
struct AppState {
|
||||
// 轮播:当前索引与动画偏移(像素)
|
||||
int currentIndex = 0; // 0: Timer, 1: Web, 2: Game
|
||||
float carouselOffset = 0.0f; // 动画帧间偏移,完成后归零
|
||||
int targetIndex = -1; // 动画结束时要切换到的索引(无动画=-1)
|
||||
|
||||
// 图标资源
|
||||
Icon iconTimer;
|
||||
Icon iconWeb;
|
||||
Icon iconGame;
|
||||
|
||||
// 场景管理
|
||||
enum class Scene : uint8_t { Home = 0, Timer = 1, Game = 2 };
|
||||
Scene scene = Scene::Home;
|
||||
game::Tetris tetris;
|
||||
timer::ClockState clock;
|
||||
// 环形缓冲:宽度=屏幕宽;时间窗口=3秒
|
||||
static const int GraphW = 128; // 与屏幕宽度一致
|
||||
int16_t samples[GraphW];
|
||||
int writeIdx = 0; // 下一个写入位置
|
||||
int sampleCount = 0; // 已写入样本(<=GraphW)
|
||||
int16_t lastRaw = 0; // 最新原始码
|
||||
uint32_t lastSampleMs = 0; // 上一次采样时间
|
||||
static const uint32_t WindowMs = 3000; // 3 秒窗口
|
||||
static const uint32_t SampleIntervalMs = WindowMs / GraphW; // 约 23ms
|
||||
};
|
||||
|
||||
static UI appUI;
|
||||
static AppState app;
|
||||
|
||||
static bool prevLeft = false;
|
||||
static bool prevRight = false;
|
||||
static bool prevUp = false;
|
||||
@ -67,53 +61,68 @@ static const int LABEL_MARGIN = 12; // 图标与文字竖向间距
|
||||
static const int CHAR_W = 14;
|
||||
static const int CHAR_H = 14;
|
||||
|
||||
// 工具:推入一条新样本(原码)
|
||||
static inline void pushSample(AppState& s, int16_t code, uint32_t nowMs) {
|
||||
// 控制写入速率,保持一屏=3秒
|
||||
if (nowMs - s.lastSampleMs < AppState::SampleIntervalMs) return;
|
||||
s.lastSampleMs = nowMs;
|
||||
s.samples[s.writeIdx] = code;
|
||||
s.writeIdx = (s.writeIdx + 1) % AppState::GraphW;
|
||||
if (s.sampleCount < AppState::GraphW) s.sampleCount++;
|
||||
s.lastRaw = code;
|
||||
}
|
||||
|
||||
// 根据状态输出显示列表
|
||||
static void buildScene(void* ctx, DisplayList& out) {
|
||||
AppState* s = (AppState*)ctx;
|
||||
if (s->scene == AppState::Scene::Home) {
|
||||
// 顶部标题居中 - 支持中文
|
||||
const char* title = "主菜单"; // 中文标题
|
||||
out.addText(100, SCREEN_W / 2, 8, WHITE, title, true); // 使用居中
|
||||
auto* st = static_cast<AppState*>(ctx);
|
||||
|
||||
// 图标与文字
|
||||
for (int i = 0; i < 3; ++i) {
|
||||
int rel = i - s->currentIndex; // 相对索引
|
||||
int x = CENTER_X + rel * SPACING + (int)(s->carouselOffset);
|
||||
if (x + ICON_W < 0 || x > SCREEN_W)
|
||||
continue;
|
||||
const Icon* ic = nullptr;
|
||||
const char* label = "";
|
||||
switch (i) {
|
||||
case 0:
|
||||
ic = &s->iconTimer;
|
||||
label = "计时器"; // 中文标签
|
||||
break;
|
||||
case 1:
|
||||
ic = &s->iconWeb;
|
||||
label = "网页"; // 中文标签
|
||||
break;
|
||||
case 2:
|
||||
ic = &s->iconGame;
|
||||
label = "游戏"; // 中文标签
|
||||
break;
|
||||
}
|
||||
out.addIcon(10 + i, x, BASE_Y, ic);
|
||||
// 标签居中显示
|
||||
int labelX = x + ICON_W / 2; // 图标中心点
|
||||
int labelY = BASE_Y + ICON_H + LABEL_MARGIN;
|
||||
out.addText(200 + i, labelX, labelY, WHITE, label, true); // 使用居中
|
||||
}
|
||||
} else if (s->scene == AppState::Scene::Game) {
|
||||
// 游戏画面:棋盘居中
|
||||
const int cell = 6;
|
||||
int boardW = game::COLS * cell;
|
||||
int boardH = game::ROWS * cell;
|
||||
int originX = (SCREEN_W - boardW) / 2;
|
||||
int originY = 16;
|
||||
game::buildScene(s->tetris, out, SCREEN_W, SCREEN_H, cell, originX,
|
||||
originY);
|
||||
} else if (s->scene == AppState::Scene::Timer) {
|
||||
timer::buildScene(s->clock, out, SCREEN_W, SCREEN_H);
|
||||
// 背景整屏清除
|
||||
out.addFillRect(1, 0, 0, SCREEN_W, SCREEN_H, BLACK);
|
||||
|
||||
// 显示当前原始码(居中顶部,去掉电压)
|
||||
char line[48];
|
||||
snprintf(line, sizeof(line), "CH0: %d", st->lastRaw);
|
||||
out.addText(2, SCREEN_W / 2, 10, WHITE, line, true);
|
||||
|
||||
// 图形区域:最大化剩余高度,无边框
|
||||
const int topY = 24; // 标题高度
|
||||
const int graphH = SCREEN_H - topY; // 剩余高度
|
||||
const int gw = AppState::GraphW; // 128
|
||||
const int gx = 0;
|
||||
|
||||
if (st->sampleCount == 0) return; // 尚无数据
|
||||
|
||||
// 动态范围(min-200, max+200)
|
||||
int16_t minV = st->samples[0];
|
||||
int16_t maxV = st->samples[0];
|
||||
for (int i = 1; i < st->sampleCount; ++i) {
|
||||
int16_t v = st->samples[i];
|
||||
if (v < minV) minV = v;
|
||||
if (v > maxV) maxV = v;
|
||||
}
|
||||
int32_t rangeMin = (int32_t)minV - 200;
|
||||
int32_t rangeMax = (int32_t)maxV + 200;
|
||||
if (rangeMin < -32768) rangeMin = -32768;
|
||||
if (rangeMax > 32767) rangeMax = 32767;
|
||||
int32_t span = rangeMax - rangeMin;
|
||||
if (span <= 0) span = 1;
|
||||
|
||||
auto mapY = [&](int16_t code) -> int {
|
||||
float rel = (float)((int32_t)code - rangeMin) / (float)span; // 0..1
|
||||
if (rel < 0.0f) rel = 0.0f; if (rel > 1.0f) rel = 1.0f;
|
||||
float y = (1.0f - rel) * (graphH - 1); // 倒置
|
||||
int iy = topY + (int)(y + 0.5f);
|
||||
if (iy < topY) iy = topY;
|
||||
if (iy > topY + graphH - 1) iy = topY + graphH - 1;
|
||||
return iy;
|
||||
};
|
||||
|
||||
int prevY = mapY(st->samples[(st->writeIdx) % gw]);
|
||||
for (int x = 1; x < gw; ++x) {
|
||||
int idx = (st->writeIdx + x) % gw;
|
||||
int y = mapY(st->samples[idx]);
|
||||
out.addLine(100 + x, gx + x - 1, prevY, gx + x, y, GREEN);
|
||||
prevY = y;
|
||||
}
|
||||
}
|
||||
|
||||
@ -123,8 +132,8 @@ void setup() {
|
||||
pinMode(BTN_RIGHT, INPUT_PULLDOWN);
|
||||
pinMode(BTN_UP, INPUT_PULLDOWN);
|
||||
|
||||
// 1. 把硬件 SPI 绑定到你现有的引脚
|
||||
SPI.begin(TFT_SCLK, -1 /*MISO 不用*/, TFT_MOSI, TFT_CS);
|
||||
// 1. 把硬件 SPI 绑定到现有的引脚(与 AD7606 共用:MISO=AD7606 DOUTA)
|
||||
SPI.begin(TFT_SCLK, ad7606::PIN_AD_MISO, TFT_MOSI, TFT_CS);
|
||||
SPI.setFrequency(27000000); // ST7735 通常 27MHz 左右比较稳
|
||||
|
||||
// 2. 初始化屏幕
|
||||
@ -156,14 +165,16 @@ void setup() {
|
||||
// 启用内存帧缓冲,避免闪烁(若内存不足会返回 false)
|
||||
appUI.enableBackBuffer(SCREEN_W, SCREEN_H);
|
||||
|
||||
// 加载图标(LittleFS: /icons/*.r565,格式: [w][h][pixels])
|
||||
// 若不存在,将在渲染时以蓝色占位矩形显示
|
||||
UI::loadIconFromFS("/icons/timer.r565", app.iconTimer);
|
||||
UI::loadIconFromFS("/icons/web.r565", app.iconWeb);
|
||||
UI::loadIconFromFS("/icons/game.r565", app.iconGame);
|
||||
|
||||
// 绑定场景构建器
|
||||
appUI.setScene(buildScene, &app);
|
||||
|
||||
// 初始化 AD7606(与 TFT 共用 SPI)
|
||||
ad7606::init();
|
||||
|
||||
// 清空波形缓存并初始化计时
|
||||
for (int i = 0; i < AppState::GraphW; ++i) app.samples[i] = 0;
|
||||
app.sampleCount = 0;
|
||||
app.lastSampleMs = millis();
|
||||
}
|
||||
|
||||
void loop() {
|
||||
@ -171,87 +182,14 @@ void loop() {
|
||||
bool rightPressed = digitalRead(BTN_RIGHT) == HIGH;
|
||||
bool upPressed = digitalRead(BTN_UP) == HIGH;
|
||||
|
||||
if (app.scene == AppState::Scene::Home) {
|
||||
// 主页:左右滑动选择,Up 进入游戏
|
||||
if (leftPressed && !prevLeft && !appUI.hasActiveTweens()) {
|
||||
if (app.currentIndex > 0 && app.targetIndex == -1) {
|
||||
app.targetIndex = app.currentIndex - 1;
|
||||
app.carouselOffset = 0.0f;
|
||||
appUI.addTweenFloat(&app.carouselOffset, (float)SPACING, 300);
|
||||
}
|
||||
}
|
||||
if (rightPressed && !prevRight && !appUI.hasActiveTweens()) {
|
||||
if (app.currentIndex < 2 && app.targetIndex == -1) {
|
||||
app.targetIndex = app.currentIndex + 1;
|
||||
app.carouselOffset = 0.0f;
|
||||
appUI.addTweenFloat(&app.carouselOffset, -(float)SPACING, 300);
|
||||
}
|
||||
}
|
||||
// 进入计时器:index==0
|
||||
if (upPressed && !prevUp && app.currentIndex == 0) {
|
||||
timer::init(app.clock);
|
||||
app.scene = AppState::Scene::Timer;
|
||||
}
|
||||
// 进入游戏:index==2
|
||||
if (upPressed && !prevUp && app.currentIndex == 2) {
|
||||
game::init(app.tetris);
|
||||
app.scene = AppState::Scene::Game;
|
||||
}
|
||||
} else {
|
||||
if (app.scene == AppState::Scene::Game) {
|
||||
// 游戏内:左右移动,Up 硬降;GameOver 时 左=返回主页,右=重玩
|
||||
if (leftPressed && !prevLeft) {
|
||||
if (app.tetris.state == game::State::GameOver) {
|
||||
app.scene = AppState::Scene::Home;
|
||||
app.targetIndex = -1;
|
||||
} else {
|
||||
game::moveLeft(app.tetris);
|
||||
}
|
||||
}
|
||||
if (rightPressed && !prevRight) {
|
||||
if (app.tetris.state == game::State::GameOver) {
|
||||
game::reset(app.tetris);
|
||||
} else {
|
||||
game::moveRight(app.tetris);
|
||||
}
|
||||
}
|
||||
if (upPressed && !prevUp) {
|
||||
if (app.tetris.state == game::State::Playing) {
|
||||
game::hardDrop(app.tetris);
|
||||
}
|
||||
}
|
||||
game::update(app.tetris, millis());
|
||||
} else if (app.scene == AppState::Scene::Timer) {
|
||||
// 计时器:Left 返回主页,Right 重置,Up 增加 lap 标记
|
||||
if (leftPressed && !prevLeft) {
|
||||
app.scene = AppState::Scene::Home;
|
||||
app.targetIndex = -1;
|
||||
}
|
||||
if (rightPressed && !prevRight) {
|
||||
timer::reset(app.clock);
|
||||
}
|
||||
if (upPressed && !prevUp) {
|
||||
timer::addLap(app.clock);
|
||||
}
|
||||
timer::update(app.clock, millis());
|
||||
}
|
||||
}
|
||||
|
||||
prevLeft = leftPressed;
|
||||
prevRight = rightPressed;
|
||||
prevUp = upPressed;
|
||||
// 读取 AD7606 CH0 原始码并按速率写入缓冲
|
||||
uint32_t now = millis();
|
||||
int16_t ch0 = ad7606::readCH0();
|
||||
pushSample(app, ch0, now);
|
||||
|
||||
// 驱动 UI 帧刷新
|
||||
appUI.tick(millis());
|
||||
|
||||
// 动画结束:如果没有活动 tween 且存在 targetIndex,则完成索引切换并重置偏移
|
||||
if (app.scene == AppState::Scene::Home) {
|
||||
if (!appUI.hasActiveTweens() && app.targetIndex != -1) {
|
||||
app.currentIndex = app.targetIndex;
|
||||
app.targetIndex = -1;
|
||||
app.carouselOffset = 0.0f;
|
||||
}
|
||||
}
|
||||
|
||||
delay(16); // 简单节流 ~60FPS
|
||||
}
|
||||
|
||||
139
src/timer.cpp
139
src/timer.cpp
@ -1,139 +0,0 @@
|
||||
#include "timer.h"
|
||||
#include "utils.h"
|
||||
#include <math.h>
|
||||
|
||||
namespace esp32 {
|
||||
namespace timer {
|
||||
|
||||
using namespace esp32::ui;
|
||||
using namespace esp32::utils;
|
||||
|
||||
static float deg2rad(float d){ return d * 3.1415926f / 180.0f; }
|
||||
static const int CHAR_W = 6;
|
||||
static const int CHAR_H = 8;
|
||||
static const uint16_t CYAN_COLOR = rgbTo565(0x00FFFF);
|
||||
|
||||
void init(ClockState& s){
|
||||
s.startMs = millis();
|
||||
s.elapsedMs = 0;
|
||||
s.lapCount = 0;
|
||||
s.running = true;
|
||||
}
|
||||
|
||||
void reset(ClockState& s){
|
||||
s.startMs = millis();
|
||||
s.elapsedMs = 0;
|
||||
s.lapCount = 0;
|
||||
s.running = true;
|
||||
}
|
||||
|
||||
void update(ClockState& s, uint32_t nowMs){
|
||||
if (s.running){
|
||||
s.elapsedMs = nowMs - s.startMs;
|
||||
}
|
||||
}
|
||||
|
||||
void addLap(ClockState& s){
|
||||
if (s.lapCount >= ClockState::MAX_LAPS) return;
|
||||
// 记录当前秒针角度(0-60秒 -> 0-360度)
|
||||
uint32_t sec = (s.elapsedMs / 1000) % 60;
|
||||
float angleDeg = (sec / 60.0f) * 360.0f;
|
||||
s.lapAngles[s.lapCount] = angleDeg;
|
||||
s.lapMs[s.lapCount] = s.elapsedMs;
|
||||
s.lapCount++;
|
||||
}
|
||||
|
||||
void buildScene(const ClockState& s, DisplayList& out,
|
||||
int16_t screenW, int16_t screenH){
|
||||
// 标题
|
||||
out.addText(6000, 4, 4, WHITE, "Timer");
|
||||
// 时间文本 mm:ss.cc (cc=厘秒)
|
||||
uint32_t totalCs = s.elapsedMs / 10;
|
||||
uint32_t totalSec = totalCs / 100;
|
||||
uint32_t mm = totalSec / 60;
|
||||
uint32_t ss = totalSec % 60;
|
||||
uint32_t cs = totalCs % 100;
|
||||
char buf[16];
|
||||
snprintf(buf, sizeof(buf), "%02lu:%02lu.%02lu", (unsigned long)mm, (unsigned long)ss, (unsigned long)cs);
|
||||
// 背景擦除该区域避免残影
|
||||
// out.addFillRect(5999, 0, 14, 72, 9, BLACK);
|
||||
out.addText(6001, 4, 14, YELLOW, buf);
|
||||
|
||||
// 钟表圆心与半径
|
||||
int16_t radius = 54; // 缩小一点,右侧留空间展示 laps
|
||||
int16_t cx = screenW / 2 ; // 向左偏移为右侧列表腾位置
|
||||
int16_t cy = screenH / 2 + 5; // 稍微偏下
|
||||
|
||||
// 外圈
|
||||
out.addCircle(6002, cx, cy, radius, WHITE);
|
||||
|
||||
// 刻度 (12 个, 每 30°),使用短线
|
||||
for (int i=0;i<12;++i){
|
||||
float ang = deg2rad(i * 30.0f - 90.0f); // 使 0 点在顶端
|
||||
int16_t x1 = cx + (int16_t)((radius - 8) * cosf(ang));
|
||||
int16_t y1 = cy + (int16_t)((radius - 8) * sinf(ang));
|
||||
int16_t x2 = cx + (int16_t)((radius - 2) * cosf(ang));
|
||||
int16_t y2 = cy + (int16_t)((radius - 2) * sinf(ang));
|
||||
out.addLine(6100 + i, x1, y1, x2, y2, WHITE);
|
||||
}
|
||||
|
||||
// 时针、分针、秒针角度
|
||||
float totalMinutes = s.elapsedMs / 60000.0f;
|
||||
float hours = fmodf(totalMinutes / 60.0f, 12.0f);
|
||||
float minutes = fmodf(totalMinutes, 60.0f);
|
||||
float seconds = fmodf(s.elapsedMs / 1000.0f, 60.0f);
|
||||
|
||||
float hourDeg = (hours / 12.0f) * 360.0f - 90.0f;
|
||||
float minuteDeg = (minutes / 60.0f) * 360.0f - 90.0f;
|
||||
float secondDeg = (seconds / 60.0f) * 360.0f - 90.0f;
|
||||
|
||||
// 指针长度
|
||||
int16_t hourLen = (int16_t)(radius * 0.5f);
|
||||
int16_t minuteLen = (int16_t)(radius * 0.7f);
|
||||
int16_t secondLen = (int16_t)(radius * 0.85f);
|
||||
|
||||
// 计算并添加指针
|
||||
auto addHand = [&](uint16_t key, float deg, int16_t len, uint16_t color){
|
||||
float rad = deg2rad(deg);
|
||||
int16_t x2 = cx + (int16_t)(len * cosf(rad));
|
||||
int16_t y2 = cy + (int16_t)(len * sinf(rad));
|
||||
out.addLine(key, cx, cy, x2, y2, color);
|
||||
};
|
||||
addHand(6200, hourDeg, hourLen, WHITE);
|
||||
addHand(6201, minuteDeg, minuteLen, GREEN);
|
||||
addHand(6202, secondDeg, secondLen, RED);
|
||||
|
||||
// Laps:在圆周上标记记录点(用小线段或小圆点)
|
||||
for (uint16_t i=0;i<s.lapCount;++i){
|
||||
float lapDeg = s.lapAngles[i] - 90.0f;
|
||||
float rad = deg2rad(lapDeg);
|
||||
int16_t lx = cx + (int16_t)((radius - 4) * cosf(rad));
|
||||
int16_t ly = cy + (int16_t)((radius - 4) * sinf(rad));
|
||||
out.addCircle(6300 + i, lx, ly, 2, BLUE);
|
||||
}
|
||||
|
||||
// Laps 列表(右侧显示最近记录)
|
||||
int16_t lapListX = screenW - 52;
|
||||
int16_t lapListY = 4;
|
||||
out.addText(6400, lapListX, lapListY, WHITE, "Laps");
|
||||
int visible = s.lapCount;
|
||||
if (visible > 6) visible = 6; // 最多显示 6 条
|
||||
// 显示最近的 visible 条(倒序)
|
||||
for (int i = 0; i < visible; ++i) {
|
||||
int idx = s.lapCount - 1 - i;
|
||||
uint32_t lapCs = s.lapMs[idx] / 10;
|
||||
uint32_t lapSec = lapCs / 100;
|
||||
uint32_t lmm = lapSec / 60;
|
||||
uint32_t lss = lapSec % 60;
|
||||
uint32_t lcs = lapCs % 100;
|
||||
char lapBuf[16];
|
||||
snprintf(lapBuf, sizeof(lapBuf), "%02lu:%02lu.%02lu", (unsigned long)lmm, (unsigned long)lss, (unsigned long)lcs);
|
||||
out.addText(6401 + i, lapListX, lapListY + (i + 1) * CHAR_H + 2, CYAN_COLOR, lapBuf);
|
||||
}
|
||||
|
||||
// 底部操作提示
|
||||
out.addText(6450, 2, screenH - 12, WHITE, "Up: Lap Right: Reset Left: Home");
|
||||
}
|
||||
|
||||
} // namespace timer
|
||||
} // namespace esp32
|
||||
Loading…
x
Reference in New Issue
Block a user