总览
OverviewATECC608A 是 Microchip CryptoAuthentication 系列的硬件加密协处理器 + 安全密钥存储芯片。跟本工作区里其它传感器类项目(BMM150/DPS310)不同,它本身不产生物理量数据,产出的是"证明身份"和"保护密钥"——私钥在芯片内部生成、加密运算在芯片内部完成,正常使用流程里私钥不会以明文形式出现在总线上或主控MCU的内存里。这次把它接到 ESP32-2432S028R 上,做了一个中文触摸UI,把检测、Lock流程、密钥生成、签名验证、随机数、哈希这几个核心功能逐个点亮演示。
ATECC608A
- 硬件密钥安全存储,最多16个slot
- ECDSA签名/验签(NIST P256椭圆曲线)
- SHA-256、HMAC、AES-128
- 内置随机数发生器(NIST SP 800-90A/B/C)
- 出厂唯一72-bit序列号,防伪造/防克隆
ESP32-2432S028R(CYD)
- ESP32-D0WD-V3 双核
- 板载2.8" ST7789 TFT,240×320,带触摸(XPT2046)
- CN1扩展口:GND/IO22/IO27/3V3
- 本项目把CN1当I2C用(SCL/SDA)
- 触摸只判断"按下没有",不做坐标映射
中文触摸UI
- 检测/信息/锁定确认/锁定结果 4个基础页
- 密钥生成/签名验证/随机数/SHA256哈希 4个demo页
- U8g2_for_TFT_eSPI + 文泉驿点阵字体渲染中文
- 长按5秒+进度条确认永久锁定,防误触
- ⚠ 当前固定SIMULATE_LOCK=1,全部demo数据都是模拟值
ATECC608A 详解
CryptoAuthentication 安全存储芯片板载型号 ATECC608A-MAHDA-S(UDFN8封装,"MAH"是UDFN封装的官方标记代号)。板子丝印/淘宝标注为"WCMCU-608"。
| 项目 | 参数 |
|---|---|
| 核心功能 | 硬件密钥安全存储(16 slot) + ECDSA签名/验签(P256) + ECDH密钥协商 + SHA-256/HMAC/AES-128 + 硬件随机数发生器 + 2个高耐久单调计数器 |
| 接口 | 标准I2C(最高1MHz) 或 单线SWI(省一个GPIO),本资料用I2C |
| I2C地址 | 固定 0x60(7-bit,不可改) |
| 供电 | 2.0~5.5V,IO电平1.8~5.5V |
| 功耗 | 等待I/O时约2mA(典型),执行ECC命令时3~14mA,待机约800µA,睡眠典型30nA/最大150nA |
| 工作温度 | -40℃~+85℃ |
| EEPROM寿命 | 40万次/字节写入;数据保持35℃下30~50年 |
| 封装 | 8-pad UDFN 或 8-lead SOIC |
| 唯一序列号 | 出厂焊死的72-bit唯一序列号(带厂商签名,防伪造/防克隆),本资料实测S/N: 0123E19A6949E835EE |
| Pin | 功能 |
|---|---|
| 1 / 2 / 3 | NC(不接) |
| 4 | GND |
| 5 | SDA |
| 6 | SCL |
| 7 | NC(不接) |
| 8 | VCC |
ATECC608A(A版本)有一个已知I2C电路小bug:芯片处于Idle模式、总线频率≤300kHz、且总线上还挂了其他I2C设备时,总线上别的设备产生的数据模式有极小概率被误判成唤醒脉冲,可能干扰正在发给其他设备的数据;B版本(ATECC608B)修了这个问题。本项目用的是A版本。如果以后要跟别的I2C传感器(如SHT41/BH1750等)共用一条总线,建议把总线速率设到400kHz或更高来规避。
芯片出厂默认是未Lock状态(未配置)。像随机数、生成密钥、签名这些完整加密功能,需要先写入Configuration Zone并执行Lock命令才能用;但读取芯片是否存在、读序列号(serialNumber())在未Lock状态下就能正常工作,适合做"通不通"的先期测试。详见第5节安全机制。
ESP32 CYD 详解
ESP32-2432S028R · 2.8" 屏圈内俗称"CYD"(Cheap Yellow Display),主控是ESP32-D0WD-V3双核芯片,板载一块2.8寸ST7789驱动的TFT彩屏(240×320,SPI接口,带XPT2046电阻触摸),还带一个CN1四针扩展口,跟本工作区里的HC-SR04、BMM150、DPS310几个项目共用同一套板子和引脚约定。
| 项目 | 参数 |
|---|---|
| 主控 | ESP32-D0WD-V3,双核,自带Wi-Fi/蓝牙(本项目未使用) |
| 屏幕 | 2.8" ST7789,240×320,SPI接口,用TFT_eSPI库驱动 |
| 触摸 | XPT2046电阻触摸,本项目只用IRQ引脚判断"有没有按下",不做坐标映射(CS常拉低) |
| CN1扩展口 | GND / IO22 / IO27 / 3V3 四针,本资料把 IO22 当SCL、IO27 当SDA 用 |
| 烧录方式 | PlatformIO命令行 pio run --target upload |
触摸走的是SPI接口,不占用I2C的IO22/IO27:TOUCH_CS=33、TOUCH_IRQ=36。固件里把CS常拉低,只轮询IRQ电平——按下即为低电平,省掉坐标标定这一步,跟本工作区03号项目(BMI160单独测试UI)"点哪都算"的思路一致。
接线
仅4根线,I2C模式只用到4根线,ATECC608A模块接到CYD的CN1扩展口上。
WCMCU-608 (ATECC608A)ESP32 CYD · CN1
安全机制:Configuration 与 Lock
⚠ 一次性、不可逆操作芯片出厂时是"空白"状态,密钥怎么用、能不能读、能不能改,这些规则都还没定下来。要真正用上它的加密功能,得先写一份配置,再执行一个叫Lock的操作,把这份配置永久锁死——锁死之后配置就再也改不了了,这是芯片故意设计成不可逆的,防的是运行中途配置被人篡改。
固件main.ino第27行 #define SIMULATE_LOCK 1:界面、交互、长按5秒的确认流程全部真实,但没有真的调用写配置和锁定这两个函数,也不会真的执行签名/生成密钥/随机数——全部用标了"模拟"字样的假数据展示。屏幕右下角常驻一行黄字"模拟模式 - 未触碰真实芯片"提醒这一点。要真的对物理芯片执行Lock,需要把这个宏改成0并重新烧录,且Lock之后配置永久生效、不能反悔。
官方安全做法:不要自己手写Configuration Zone字节去Lock —— ArduinoECCX08库自带一份Arduino官方验证过的默认TLS配置,是目前最安全可靠的参考做法:
#include <ArduinoECCX08.h> #include <utility/ECCX08SelfSignedCert.h> #include <utility/ECCX08DefaultTLSConfig.h> // 库自带的默认TLS配置数组 // 确认 ECCX08.locked() 为 false 之后: ECCX08.writeConfiguration(ECCX08_DEFAULT_TLS_CONFIG); // 写入官方默认配置 ECCX08.lock(); // 永久锁定,不可逆! // Lock 之后才能: ECCX08.generatePrivateKey(slot, publicKey); // 在 slot(0~4)里生成新私钥,返回对应公钥 ECCX08.ecSign(slot, message32bytes, signature); // 用该私钥签名(输入必须是32字节) ECCX08.ecdsaVerify(message32bytes, signature, publicKey); // 验签 ECCX08.random(65535); // 生成随机数(Lock后才可用)
触摸UI与9个界面状态
中文UI · U8g2_for_TFT_eSPI + 文泉驿字体屏幕在任意时刻只处于一个"状态",单击/长按5秒驱动状态跳转。以下是每个页面显示什么、代表什么:
| 页面(状态) | 屏幕显示 | 代表什么 | 怎么进入/离开 |
|---|---|---|---|
| 检测中... | 纯标题,无内容 | 开机瞬间,正在调用ECCX08.begin()探测I2C总线0x60地址 | 开机自动进入,几百毫秒后自动跳下一页 |
| 未找到芯片 | 红字+接线提示(SDA=IO27 SCL=IO22) | I2C探测失败,多半是接线错 | 长按5秒重启重试 |
| 已找到芯片 | 绿字+型号+72位序列号+锁定状态 | 主页/信息页,序列号是出厂唯一防伪编号 | 未锁定时单击进锁定确认页;已锁定单击进密钥生成;长按5秒重新扫描 |
| 永久锁定确认 | "不可撤销"提示+红色进度条 | 二次确认页,长按满5秒才生效,防误触 | 长按满5秒触发Lock(本项目当前为模拟) |
| 锁定成功/失败 | 结果+模拟标注 | 明确告知这次是否真的改了芯片配置 | 单击返回主页 |
| 生成密钥 | 槽位号+公钥十六进制 | 芯片内部生成密钥对,私钥永远不出芯片 | 单击下一个;长按5秒回主页 |
| 签名与验证 | 签名值+"验证:通过" | 用密钥对数据签名再验证,身份认证核心用法 | 同上 |
| 生成随机数 | 一个数字 | 芯片硬件随机数发生器(TRNG),每次换新数 | 单击换一个新数;长按5秒回主页 |
| SHA-256哈希 | 32字节数据的摘要值 | 硬件SHA-256加速,验证数据完整性 | 同上 |
完整固件代码
PlatformIO · Arduino (ESP32) ✅ 编译/烧录/触摸交互均已验证用 PlatformIO + Arduino核心,ArduinoECCX08库源码直接拷进工程lib/目录(不依赖联网下载);TFT_eSPI/U8g2/U8g2_for_TFT_eSPI三个库走registry/git URL拉取。
下面这份代码就是工程里实际跑的那份,文件在 06_测试代码\ESP32_ATECC608A_UI演示\src\main.ino,同目录 固件源码解说与屏幕说明.md 有更详细的分段代码解说(含真实行号),配套B站视频"关键代码解读"章节用的就是这份材料。
[env:esp32dev]
platform = espressif32@6.13.0
board = esp32dev
framework = arduino
board_build.partitions = huge_app.csv
; Arduino-ESP32 核心3.x(ESP-IDF5)在这台机器上链接报错:
; undefined reference to `esp_bt_controller_mem_release'
; 是工具链BT库版本不匹配的已知问题,跟代码无关。固定用2.0.17这个稳定核心版本绕开。
; (跟 02_中文触摸效率时钟、U1205 BMM150、02601 DPS310 项目踩过的坑是同一个,写法照抄。)
platform_packages =
framework-arduinoespressif32 @ https://github.com/espressif/arduino-esp32/releases/download/2.0.17/esp32-2.0.17.zip
upload_port = COM20
monitor_port = COM20
monitor_speed = 115200
; TFT_eSPI版本/引脚定义照抄02_中文触摸效率时钟项目里已验证过的配置。
; U8g2 + U8g2_for_TFT_eSPI:给TFT_eSPI加中文字体支持(文泉驿点阵字库wqy16/wqy12),
; 字库数据直接编译进固件,不用额外生成.vlw字库文件。
lib_deps =
bodmer/TFT_eSPI @ ^2.5.43
olikraus/U8g2 @ ^2.35.19
https://github.com/Bodmer/U8g2_for_TFT_eSPI.git
build_flags =
-D USER_SETUP_LOADED=1
-D ST7789_DRIVER=1
-D TFT_WIDTH=240
-D TFT_HEIGHT=320
-D TFT_MOSI=13
-D TFT_SCLK=14
-D TFT_CS=15
-D TFT_DC=2
-D TFT_RST=-1
-D TFT_MISO=-1
-D TFT_BL=21
-D TFT_BACKLIGHT_ON=HIGH
-D TFT_INVERSION_OFF=1
-D SPI_FREQUENCY=40000000
-D LOAD_GLCD=1
-D LOAD_FONT2=1
-D LOAD_FONT4=1
-D LOAD_FONT6=1
-D LOAD_FONT7=1
-D LOAD_FONT8=1
-D LOAD_GFXFF=1
-D SMOOTH_FONT=1
/*
ESP32-2432S028R(2.8寸屏CYD) + ATECC608A 触摸UI演示(中文版)
接线:模块 VCC->3.3V,GND->GND,SDA->IO27,SCL->IO22(跟BMM150/DPS310项目同一路I2C排针)
屏幕/触摸引脚沿用 02_中文触摸效率时钟、03_BMI160单独测试UI 两个项目已验证过的配置。
中文字体用 U8g2_for_TFT_eSPI + U8g2 自带的文泉驿点阵字体(wqy16/wqy12),
不用额外生成字库文件,字库数据直接编译进固件。
交互:
- 单击:翻页/切换演示内容
- 长按(HOLD_MS,当前5秒,按住期间屏幕下方有红色进度条):确认"永久Lock"这类
不可逆操作;主页长按=重新扫描芯片;演示页长按=返回主页
SIMULATE_LOCK = 1 (当前默认):
只走UI/状态机流程,不会真的调用 ECCX08.writeConfiguration()/lock(),也不会真的
执行签名/生成密钥/随机数——全部用标了"模拟"字样的假数据展示,用来先把触摸交互
和翻页调试顺,不碰物理芯片的真实配置。
确认UI没问题、且用户明确同意要真的Lock这颗芯片之后,把这个宏改成0再重新烧录,
才会真的执行 Configuration+Lock 和调用真实的签名/密钥/随机数功能。
*/
#include <SPI.h>
#include <TFT_eSPI.h>
#include <U8g2_for_TFT_eSPI.h>
#include <Wire.h>
#include <ArduinoECCX08.h>
#define SIMULATE_LOCK 1
#define I2C_SDA 27
#define I2C_SCL 22
#define TOUCH_CS 33
#define TOUCH_IRQ 36
#define HOLD_MS 5000UL
#define KEY_SLOT 0
#define SCREEN_W 320
#define SCREEN_H 240
#define COL_BG TFT_BLACK
#define COL_TEXT TFT_WHITE
#define COL_DIM TFT_DARKGREY
#define COL_OK TFT_GREEN
#define COL_WARN TFT_RED
#define COL_ACCENT TFT_YELLOW
#define COL_BAR_BG 0x2104 // 深灰
#define COL_BAR_FG TFT_RED
TFT_eSPI tft = TFT_eSPI();
U8g2_for_TFT_eSPI u8f;
enum AppState {
STATE_DETECT,
STATE_ERROR,
STATE_INFO,
STATE_LOCK_CONFIRM,
STATE_MENU_KEYGEN,
STATE_MENU_SIGN,
STATE_MENU_RANDOM,
STATE_MENU_SHA256,
};
static AppState state = STATE_DETECT;
static bool chipLockedReal = false; // ECCX08.locked() 的真实结果
static bool simLocked = false; // 仅SIMULATE_LOCK=1时,软件层面假装"已锁定"
static String serialNumberStr;
static bool isLockedEffective() {
#if SIMULATE_LOCK
return simLocked;
#else
return chipLockedReal;
#endif
}
// ---------------- 触摸:只用IRQ判断"有没有摁下",不做坐标映射 ----------------
// XPT2046在CS拉低期间,只要屏幕被摁着,PENIRQ会一直是低电平——足够用来做
// "单击/长按"这种不需要具体坐标的交互,省掉坐标标定这一步(跟03号项目思路一致,
// 那边也是靠"点哪都算",没做坐标映射)。
static void touchInit() {
pinMode(TOUCH_CS, OUTPUT);
digitalWrite(TOUCH_CS, LOW); // 常拉低,让IRQ一直有效
pinMode(TOUCH_IRQ, INPUT);
}
static bool isTouchedNow() {
return digitalRead(TOUCH_IRQ) == LOW;
}
// ---------------- 画面绘制小工具 ----------------
// 中文用U8g2的文泉驿点阵字体:wqy16(16px,标题/正文用) + wqy12(12px,提示小字用)。
// 这两个字体是等宽位图字体,一个汉字/一个ASCII字符基本都是"字号"px宽,
// 320屏两边各留12px边距,可用宽度296px,正文最多约18个汉字,够用不会超屏。
static int cursorY;
static void resetCursor(int y) { cursorY = y; }
static void setPrimaryFont() { u8f.setFont(u8g2_font_wqy16_t_gb2312); } // 16px
static void setSecondaryFont() { u8f.setFont(u8g2_font_wqy12_t_gb2312); } // 12px
static void drawHeader(const char *title, uint16_t color) {
tft.fillScreen(COL_BG);
u8f.setForegroundColor(color);
setPrimaryFont();
u8f.drawUTF8(12, 30, title);
tft.drawFastHLine(0, 40, SCREEN_W, COL_DIM);
#if SIMULATE_LOCK
u8f.setForegroundColor(COL_ACCENT);
setSecondaryFont();
u8f.drawUTF8(12, SCREEN_H - 8, "模拟模式 - 未触碰真实芯片");
#endif
resetCursor(62);
}
// tier: 2=正文(默认,16px文泉驿),1=小字提示(12px文泉驿)
static void printLine(const String &text, uint16_t color, uint8_t tier = 2) {
u8f.setForegroundColor(color);
if (tier >= 2) setPrimaryFont(); else setSecondaryFont();
u8f.drawUTF8(12, cursorY, text.c_str());
cursorY += (tier >= 2) ? 24 : 18;
}
static void printGap(int px) { cursorY += px; }
static void drawProgressBar(uint32_t elapsedMs) {
const int x = 12, y = SCREEN_H - 46, w = SCREEN_W - 24, h = 16;
uint32_t clamped = elapsedMs > HOLD_MS ? HOLD_MS : elapsedMs;
int fillW = (int)((uint32_t)w * clamped / HOLD_MS);
tft.fillRect(x, y, w, h, COL_BAR_BG);
if (fillW > 0) tft.fillRect(x, y, fillW, h, COL_BAR_FG);
tft.drawRect(x, y, w, h, COL_TEXT);
}
static void clearProgressBar() {
const int x = 12, y = SCREEN_H - 46, w = SCREEN_W - 24, h = 16;
tft.fillRect(x, y, w, h, COL_BAR_BG);
tft.drawRect(x, y, w, h, COL_TEXT);
}
// 只转前 len 字节,每 8 字节一行(16个hex字符),用正文字号显示也不会超屏。
static String toHex(const uint8_t *data, int len) {
String s;
char buf[3];
for (int i = 0; i < len; i++) {
sprintf(buf, "%02X", data[i]);
s += buf;
}
return s;
}
// ---------------- 各页面绘制 ----------------
static void renderDetectPage() {
drawHeader("检测中...", COL_TEXT);
}
static void renderErrorPage() {
drawHeader("未找到芯片", COL_WARN);
printLine("请检查接线:", COL_TEXT);
printLine("SDA=IO27 SCL=IO22", COL_TEXT);
printGap(10);
printLine("长按5秒重试", COL_DIM, 1);
}
static void renderInfoPage() {
drawHeader("已找到芯片", COL_OK);
printLine("型号:ATECC608A", COL_TEXT);
printLine("序列号:", COL_DIM, 1);
printLine(serialNumberStr, COL_TEXT);
printGap(6);
if (isLockedEffective()) {
printLine("已锁定", COL_OK);
printGap(6);
printLine("轻触:查看演示", COL_DIM, 1);
} else {
printLine("未锁定(出厂状态)", COL_WARN);
printGap(6);
printLine("轻触:进入锁定步骤", COL_DIM, 1);
}
printLine("长按5秒:重新扫描", COL_DIM, 1);
}
static void renderLockConfirmPage(uint32_t heldMs) {
drawHeader("永久锁定确认", COL_WARN);
printLine("该操作不可撤销!", COL_TEXT);
printLine("确认后配置永久生效", COL_TEXT);
printGap(10);
printLine("长按5秒确认锁定", COL_ACCENT);
drawProgressBar(heldMs);
}
static void renderLockResultPage(bool ok) {
drawHeader(ok ? "锁定成功" : "锁定失败", ok ? COL_OK : COL_WARN);
#if SIMULATE_LOCK
printLine("(模拟,未写入真实配置)", COL_ACCENT);
#else
printLine(ok ? "配置已写入并锁定" : "写入或锁定失败", COL_TEXT);
#endif
printGap(10);
printLine("轻触继续", COL_DIM, 1);
}
static void renderKeygenPage() {
drawHeader("生成密钥", COL_TEXT);
printLine("槽位:" + String(KEY_SLOT), COL_TEXT);
printGap(6);
uint8_t publicKey[64];
#if SIMULATE_LOCK
for (int i = 0; i < 64; i++) publicKey[i] = (uint8_t)(0xA5 ^ (i * 7 + 3));
printLine("公钥(模拟):", COL_ACCENT, 1);
#else
ECCX08.generatePrivateKey(KEY_SLOT, publicKey);
printLine("公钥(真实):", COL_ACCENT, 1);
#endif
printLine(toHex(publicKey, 8), COL_TEXT);
printLine(toHex(publicKey + 8, 8), COL_TEXT);
printGap(10);
printLine("轻触:下一个 长按5秒:主页", COL_DIM, 1);
}
static void renderSignPage() {
drawHeader("签名与验证", COL_TEXT);
const byte input[32] = {
0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 0x0f,
0x10, 0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17, 0x18, 0x19, 0x1a, 0x1b, 0x1c, 0x1d, 0x1e, 0x1f
};
byte signature[64];
byte publicKey[64];
bool verified;
#if SIMULATE_LOCK
for (int i = 0; i < 64; i++) signature[i] = (uint8_t)(0x5A ^ (i * 11 + 1));
verified = true;
printLine("签名(模拟):", COL_ACCENT, 1);
#else
ECCX08.generatePublicKey(KEY_SLOT, publicKey);
ECCX08.ecSign(KEY_SLOT, input, signature);
verified = ECCX08.ecdsaVerify(input, signature, publicKey);
printLine("签名(真实):", COL_ACCENT, 1);
#endif
printLine(toHex(signature, 8), COL_TEXT);
printLine(toHex(signature + 8, 8), COL_TEXT);
printGap(6);
printLine(verified ? "验证:通过" : "验证:失败", verified ? COL_OK : COL_WARN);
printGap(10);
printLine("轻触:下一个 长按5秒:主页", COL_DIM, 1);
}
static void renderRandomPage() {
drawHeader("生成随机数", COL_TEXT);
long value;
#if SIMULATE_LOCK
value = random(0, 65535);
printLine("随机数(模拟):", COL_ACCENT, 1);
#else
value = ECCX08.random(65535);
printLine("随机数(真实):", COL_ACCENT, 1);
#endif
printGap(6);
printLine(String(value), COL_TEXT);
printGap(10);
printLine("轻触:换一个新数", COL_DIM, 1);
printLine("长按5秒:返回主页", COL_DIM, 1);
}
static void renderSha256Page() {
drawHeader("SHA-256哈希", COL_TEXT);
const byte input[32] = {
0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 0x0f,
0x10, 0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17, 0x18, 0x19, 0x1a, 0x1b, 0x1c, 0x1d, 0x1e, 0x1f
};
byte digest[32];
printLine("对32字节数据求哈希", COL_TEXT, 1);
printGap(4);
#if SIMULATE_LOCK
for (int i = 0; i < 32; i++) digest[i] = (uint8_t)(0x3C ^ (i * 13 + 5));
printLine("摘要(模拟):", COL_ACCENT, 1);
#else
ECCX08.SHA256(input, sizeof(input), digest);
printLine("摘要(真实):", COL_ACCENT, 1);
#endif
printLine(toHex(digest, 8), COL_TEXT);
printLine(toHex(digest + 8, 8), COL_TEXT);
printGap(10);
printLine("轻触:下一个 长按5秒:主页", COL_DIM, 1);
}
static void renderCurrentState() {
switch (state) {
case STATE_DETECT: renderDetectPage(); break;
case STATE_ERROR: renderErrorPage(); break;
case STATE_INFO: renderInfoPage(); break;
case STATE_LOCK_CONFIRM: renderLockConfirmPage(0); break;
case STATE_MENU_KEYGEN: renderKeygenPage(); break;
case STATE_MENU_SIGN: renderSignPage(); break;
case STATE_MENU_RANDOM: renderRandomPage(); break;
case STATE_MENU_SHA256: renderSha256Page(); break;
}
}
// ---------------- Lock 执行(模拟或真实,由 SIMULATE_LOCK 决定) ----------------
static bool performLock() {
#if SIMULATE_LOCK
delay(1200); // 假装在写配置,凑一个真实感的等待时间
simLocked = true;
return true;
#else
if (!ECCX08.writeConfiguration(ECCX08_DEFAULT_TLS_CONFIG)) return false;
if (!ECCX08.lock()) return false;
chipLockedReal = true;
return true;
#endif
}
// ---------------- 触摸事件 -> 状态机 ----------------
static void onShortTap() {
switch (state) {
case STATE_ERROR:
break; // 只能长按重试,单击不响应
case STATE_INFO:
state = isLockedEffective() ? STATE_MENU_KEYGEN : STATE_LOCK_CONFIRM;
renderCurrentState();
break;
case STATE_LOCK_CONFIRM:
// 没按满5秒的单击视为无效操作,停留在本页(不取消也不确认)
clearProgressBar();
break;
case STATE_MENU_KEYGEN:
state = STATE_MENU_SIGN;
renderCurrentState();
break;
case STATE_MENU_SIGN:
state = STATE_MENU_RANDOM;
renderCurrentState();
break;
case STATE_MENU_RANDOM:
state = STATE_MENU_SHA256;
renderCurrentState();
break;
case STATE_MENU_SHA256:
state = STATE_MENU_KEYGEN;
renderCurrentState();
break;
default:
break;
}
}
static void onLongPressComplete() {
switch (state) {
case STATE_ERROR:
ESP.restart();
break;
case STATE_INFO:
// 在主页长按 = 真的重新扫描一次芯片(软复位),跟"演示页长按=回主页"分开,
// 两个长按动作不会互相冲突。
ESP.restart();
break;
case STATE_LOCK_CONFIRM: {
drawHeader("锁定中...", COL_ACCENT);
printLine("请稍候", COL_TEXT);
bool ok = performLock();
renderLockResultPage(ok);
// 下一次单击会走到 renderCurrentState(),state先切到INFO,
// 让"轻触继续"点一下之后按当前锁定状态正确分流到菜单。
state = STATE_INFO;
break;
}
case STATE_MENU_KEYGEN:
case STATE_MENU_SIGN:
case STATE_MENU_RANDOM:
case STATE_MENU_SHA256:
// 长按回主页,不用整个重启——软件层面的模拟锁定状态也会保留。
state = STATE_INFO;
renderCurrentState();
break;
default:
break;
}
}
void setup() {
Serial.begin(115200);
delay(300);
tft.init();
tft.setRotation(1);
tft.fillScreen(COL_BG);
u8f.begin(tft);
u8f.setFontMode(1); // 透明背景,不用每次单独填底色
u8f.setFontDirection(0);
touchInit();
Wire.begin(I2C_SDA, I2C_SCL);
renderDetectPage();
if (!ECCX08.begin()) {
Serial.println("ATECC608A not found");
state = STATE_ERROR;
renderCurrentState();
return;
}
serialNumberStr = ECCX08.serialNumber();
chipLockedReal = ECCX08.locked();
Serial.print("S/N: ");
Serial.println(serialNumberStr);
Serial.print("Locked: ");
Serial.println(chipLockedReal ? "yes" : "no");
state = STATE_INFO;
renderCurrentState();
}
void loop() {
static uint32_t lastPoll = 0;
static bool wasDown = false;
static uint32_t pressStartMs = 0;
static bool longFired = false;
static uint32_t lastActionMs = 0;
uint32_t now = millis();
if (now - lastPoll < 20) return;
lastPoll = now;
bool down = isTouchedNow();
if (down && !wasDown) {
pressStartMs = now;
longFired = false;
}
if (down) {
uint32_t held = now - pressStartMs;
if ((state == STATE_LOCK_CONFIRM) ||
(state == STATE_ERROR) ||
(state == STATE_INFO) ||
(state == STATE_MENU_KEYGEN) || (state == STATE_MENU_SIGN) ||
(state == STATE_MENU_RANDOM) || (state == STATE_MENU_SHA256)) {
if (state == STATE_LOCK_CONFIRM) drawProgressBar(held);
if (!longFired && held >= HOLD_MS) {
longFired = true;
lastActionMs = now;
onLongPressComplete();
}
}
}
if (!down && wasDown && !longFired && (now - lastActionMs > 250)) {
lastActionMs = now;
onShortTap();
}
wasDown = down;
}
代码下载
打包好的3份代码,选跟你硬件匹配的下载下载前先看清楚自己手上是什么板子——Arduino UNO R3和ESP32用的软件工具、代码格式都不一样,选错了打不开。
| 压缩包 | 适用硬件 | 代码格式 | 用什么软件打开 |
|---|---|---|---|
| UNO_R3_ATECC608A_通信测试.zip | Arduino UNO R3 | 单个.ino文件 | Arduino IDE,双击.ino直接打开 |
| ESP32_ATECC608A_通信测试.zip | 普通ESP32(不需要屏幕) | PlatformIO工程(文件夹) | VS Code + PlatformIO插件,打开整个文件夹 |
| ESP32_ATECC608A_UI演示.zip | ESP32-2432S028R(2.8"屏/CYD) | PlatformIO工程(文件夹) | VS Code + PlatformIO插件,打开整个文件夹 |
Arduino IDE格式(.ino单文件):用官方Arduino IDE软件直接打开,上传前先在"库管理器"里搜索安装ArduinoECCX08库。PlatformIO工程格式(文件夹):需要VS Code + PlatformIO插件,"打开文件夹"选中整个解压出来的文件夹(不是打开某个单独文件)。两个ESP32压缩包里ArduinoECCX08库源码已经打包进lib/目录,不用再下载;但UI演示工程还用到TFT_eSPI/U8g2/U8g2_for_TFT_eSPI三个屏幕字体库,编译时会自动联网下载,编译这个工程电脑需要能上网。
两个ESP32工程的platformio.ini里写死了upload_port = COM20,改成你电脑实际显示的COM口号(Windows设备管理器能看到)。UI演示代码默认模拟模式(SIMULATE_LOCK=1),屏幕数据都是假的;要真正执行Configuration+Lock,需要自己把宏改成0重新烧录——这一步永久锁死芯片配置,改了不能反悔。
实拍演示
B站视频制作中配套B站教程视频(开箱接线→上电检测→Lock流程→密钥生成/签名验证→随机数/SHA256→关键代码解读→总结,共8章)正在制作,成片草稿见 03_制作脚本与中间文件\输出成片.mp4,标题/简介/封面定稿后会更新本节链接。
开箱接线、上电检测、Lock长按进度条、密钥生成/签名验证/随机数/SHA256四个demo页的实拍素材在 01_原始素材_手机拍摄\IMG_9931.MOV,口播稿逐段对照见 02_口播稿与脚本\口播稿_ATECC608A教程.md。
参考资料来源
References| 资料 | 说明 |
|---|---|
| ATECC608A Summary Datasheet | DS40001977B(Microchip官方,本地存档见04_规格书与Datasheet) |
| ATECC608A→608B迁移说明 | AN2237(Microchip官方) |
| ArduinoECCX08 官方库 | github.com/arduino-libraries/ArduinoECCX08 |
| Adafruit ATECC608 教程 | learn.adafruit.com/adafruit-atecc608-breakout |
| ESP32-WROOM-32SE(内置版) | Espressif官方文档 — 官方直接把ATECC608A焊在模组里的方案,跟本项目"外挂独立模块"是两种做法 |