JasonYANG170 的 LogoYANG Docs
首页
硬件
软件
AI
3D
主站
首页
硬件
软件
AI
3D
主站
OSHWLab

EDA简易单词卡

本项目使用ESP8266模组实现的简易单词卡项目,支持web端单词管理功能,可用于英文单词学习等。

开源硬件2026-01-30T07:41:34.254Z★ 127
查看原始项目 ↗
EDA简易单词卡

项目文档

项目介绍

本项目使用 ESP8266 模组实现的简易单词卡项目,支持web端单词管理功能,可用于英文单词学习等。项目器件较少,贴片均为0805大封装,成本偏低,结构简单,适用于教学

固件功能

中文显示

✅ 支持常见中文显示,内置中文字库,无需字库芯片。

扩展存储

✅ 支持FAT16格式的最大2GB存储卡扩展

ADC按键

✅ 多个按键仅需一个IO即可识别,提高IO利用率

单词管理

✅ 可进入WEB控制后台添加、删除、修改单词及数据

项目参数

ESP8266

✅ ESP8266系列主控,自带WIFI通讯功能

TF卡

✅ 支持TF卡扩展存储,FAT16格式,最大2GB

OLED屏幕

✅ 支持SSD1306/SSD1315驱动的OLED 0.96寸屏幕

硬件设计

主控电路

主控选用 ESP8266 模组,集成 WiFi 功能,提供丰富的 GPIO 接口。

ESP8266 Board
ESP32S3 主控模组原理图部分
ESP8266 Circuit
主控模组启动电路原理图部分
查看数据手册 立创商城购买
TF卡扩展电路

TF卡这里移除CS引脚的上拉电阻,因为CS引脚为GPIO15引脚,是ESP8266模式切换的重要IO,已经在启动电路部分接下拉电阻。

Audio Circuit
INMP441音频采集电路原理图部分
电压转换电路

电压转换部分采用 ME6217系列LDO ,低压差,800mA,SOT23封装的小巧LDO供电,符合ESP8266建议的500mA供电要求。

ME6217C33M5G LDO电路原理图部分
显示电路

显示部分采用 SSD1306/SSD1315 OLED 屏幕,分辨率128×64,尺寸0.96。由于本项目采用的是OLED模块,模块通常板载上拉电阻,因此原理图中未添加上拉电阻。

SSD1306 OLED显示屏电路原理图部分
查看数据手册 立创商城购买
供电接口

供电接口采用立插设计的TypeC接口,CC脚接5.1k下拉识别电阻,支持双TypeC口的线材供电。

舵机电路图
舵机控制电路原理图部分
按键电路

这里采用ADC的方式来识别按键,灵感来自之前做EDA-Piano电子琴的SC12B触摸传感器,通过BCD端口接电阻产生的模拟电压确认按键。这里为每个按键配置不同的上拉电阻,这样当ADC检测到不同电压时来识别是哪个按键按下。ESP8266的ADC电压范围是0-1V,理论上来说只要你买的电阻误差足够低,就可以通过一个ADC去识别很多个按键。不过通过ADC检测时要注意干扰,最好不要裸板操作,而是配合塑料外壳使用。

按键电路
按键电路原理图部分
按键电路
SC12B数据手册

软件开发

完整源码请在附件查看

开发环境
  • 软件环境:VSCode + PlatformIO
  • 开发语言:C / C++
  • 框架:Arduino Framework
ESP8266 Arduino Core’s Documentation
BSP 模块

本项目采用 BSP(Board Support Package)板级适配架构.

  • display.cpp/.h: OLED屏幕显示部分实现
  • input.cpp/.h: 按键控制部分实现
  • learning.cpp/.h: 学习部分实现
  • storage.cpp/.h: TF存储部分实现
  • webserver.cpp/.h: WEB控制端页面及路由实现
  • main.cpp/.h: 主程序
  • config.h: 板级配置及用户配置文件

config.h

config 配置文件

在这里可以自定义WIFI配置,ADC按键配置及单词数量信息。建议购买高精度电阻,如果电阻精度不高会存在误差,这时就需要在这里修改ADC阈值到实际测量的电压。另外最大单词数量也需要在这里定义,存储的单词越多,单片机处理速度可能会较慢。


#ifndef CONFIG_H
#define CONFIG_H
// WiFi配置 - 使用inline避免重复定义
inline const char* ap_ssid = "EDA-WordCard";
inline const char* ap_password = "12345678";
// OLED 配置
#define OLED_W 128
#define OLED_H 64
#define OLED_ADDR 0x3C
// SD卡SPI引脚配置
#define PIN_CS   15  // D8
// 按键ADC阈值
#define BTN_LEFT_MIN   200   // 不会 (约0.25V)
#define BTN_LEFT_MAX   300
#define BTN_MIDDLE_MIN 500   // 模糊 (约0.57V)
#define BTN_MIDDLE_MAX 650
#define BTN_RIGHT_MIN  750   // 会 (约0.83V)
#define BTN_RIGHT_MAX  1000
// 学习状态
enum StudyResult {
  UNKNOWN = 0,
  DONT_KNOW = 1,
  VAGUE = 2,
  KNOW = 3
};
// 单词结构
struct Word {
  String name;
  String chinese;
  int study;
  String example;
  int frequency; // 出现频率 = 100 - study
};
// 全局变量声明 - 需要在一个.cpp文件中定义
extern Word currentWord;
extern bool showAnswer;
extern int totalWords;
extern int studiedWords;
extern int currentWordIndex;
extern String wordFiles[500]; // 最多支持500个单词文件
#endif
  

display.h

display 显示功能

在display部分定义了显示功能的函数,屏幕初始化,显示居中,展示信息的UI格式都在这里定义,这里的中文显示采用的u8g2_font_wqy12_t_gb2312字体,无需外挂字库芯片。


#ifndef DISPLAY_H
#define DISPLAY_H
#include 
#include 
#include "config.h"
// OLED显示对象声明
extern U8G2_SSD1306_128X64_NONAME_F_HW_I2C u8g2;
// 显示功能函数
void initDisplay();
void drawCenteredText(const char* text, int y, const uint8_t* font);
void showMessage(String line1, String line2 = "", String line3 = "", String line4 = "");
void displayCurrentWord();
void showStats();
void processStudyResult(StudyResult result);
#endif
  

input.h

input 功能按键输入

在这里定义的是功能按键的功能函数,处理按键按下的结果以及ADC电压对应的按键识别部分。。


class Oscillator {
private:
    int pos_;
    int amplitude_;
    int offset_;
    double phase_;
    double phase0_;
    int trim_;
public:
    void SetA(unsigned int amplitude);
    void SetO(int offset);
    void SetPh(double Ph);
    void SetTrim(int trim);
    void Refresh();
    int GetPosition();
};
  

learning.h

learning 单词学习逻辑

这里主要定义的是单词的学习逻辑功能部分。为了确保单片机的处理效率,这里单词选取还是依照的随机数生成。因为如果单词数量多,要读取熟练度的话就意味着单片机要从所有单词数据提取熟练度数据判断,所有这里性能考虑还是随机数生成。


#ifndef LEARNING_H
#define LEARNING_H
#include 
#include "config.h"
// 学习功能函数
int selectWordByFrequency();
void nextWord();
#endif
  

storage.h

storage TF存储

TF存储功能则是对TF卡单词数据的增删改查的实现。


#ifndef STORAGE_H
#define STORAGE_H
#include 
#include 
#include 
#include "config.h"
// 存储功能函数
bool initStorage();
void scanWordFiles();
void calculateStudiedWords();
void loadConfig();
void saveConfig();
void loadWord(String fileName);
void saveWordProgress();
#endif
  

webserver.h

webserver WEB管理后台

借助ESP8266的WIFI功能,像往期项目一样,这里添加了一个单词管理后台,你可以通过连接“EDA-WordCard”的WIFI热点,密码是“12345678”。进入后台可以对单词数据进行增删改查,不再需要读卡器插入电脑修改数据。


#ifndef STORAGE_H
#define STORAGE_H
#include 
#include 
#include 
#include "config.h"
// 存储功能函数
bool initStorage();
void scanWordFiles();
void calculateStudiedWords();
void loadConfig();
void saveConfig();
void loadWord(String fileName);
void saveWordProgress();
#endif
  

实现原理

display.cpp

display 显示部分实现

显示部分采用U8G2显示库,利用该库集成的中文国标字体u8g2_font_wqy12_t_gb2312即可显示中文,无需外挂或自定义字库。


#include "display.h"
#include 
// OLED显示对象定义
U8G2_SSD1306_128X64_NONAME_F_HW_I2C u8g2(U8G2_R0, /* reset=*/ U8X8_PIN_NONE);
// 初始化显示
void initDisplay() {
  Wire.begin(D2, D1);
  u8g2.begin();
}
// 居中显示文本
void drawCenteredText(const char* text, int y, const uint8_t* font) {
  u8g2.setFont(font);
  int textWidth = u8g2.getUTF8Width(text);
  int x = (128 - textWidth) / 2;  // 屏幕宽度128,居中计算
  u8g2.drawUTF8(x, y, text);
}
// 显示消息函数 - 优化UI
void showMessage(String line1, String line2, String line3, String line4) {
  u8g2.clearBuffer();
  if (line1.length() > 0) {
    drawCenteredText(line1.c_str(), 16, u8g2_font_wqy12_t_gb2312);
  }
  if (line2.length() > 0) {
    drawCenteredText(line2.c_str(), 32, u8g2_font_wqy12_t_gb2312);
  }
  if (line3.length() > 0) {
    drawCenteredText(line3.c_str(), 48, u8g2_font_wqy12_t_gb2312);
  }
  if (line4.length() > 0) {
    drawCenteredText(line4.c_str(), 64, u8g2_font_wqy12_t_gb2312);
  }
  u8g2.sendBuffer();
}
// 显示当前单词
void displayCurrentWord() {
  u8g2.clearBuffer();
  if (!showAnswer) {
    // 英文单词使用统一字体
    drawCenteredText(currentWord.name.c_str(), 30, u8g2_font_wqy12_t_gb2312);
    drawCenteredText("显示答案", 60, u8g2_font_wqy12_t_gb2312);
  } else {
    // 显示完整信息
    // 英文单词
    drawCenteredText(currentWord.name.c_str(), 13, u8g2_font_wqy12_t_gb2312);
    // 中文翻译
    drawCenteredText(currentWord.chinese.c_str(), 30, u8g2_font_wqy12_t_gb2312);
    // 学习进度
    String studyInfo = "进度: " + String(currentWord.study);
    drawCenteredText(studyInfo.c_str(), 45, u8g2_font_wqy12_t_gb2312);
    // 操作提示
    drawCenteredText("知道 | 模糊 | 忘记", 60, u8g2_font_wqy12_t_gb2312);
  }
  u8g2.sendBuffer();
}
// 显示统计信息
void showStats() {
  u8g2.clearBuffer();
  drawCenteredText("学习统计", 16, u8g2_font_wqy12_t_gb2312);
  String line1 = "单词总计: " + String(totalWords);
  String line2 = "已学习: " + String(studiedWords);
  drawCenteredText(line1.c_str(), 35, u8g2_font_wqy12_t_gb2312);
  drawCenteredText(line2.c_str(), 50, u8g2_font_wqy12_t_gb2312);
  u8g2.sendBuffer();
}
// 处理学习结果
void processStudyResult(StudyResult result) {
  u8g2.clearBuffer();
  String resultText = "";
  String studyText = "进度: " + String(currentWord.study);
  switch (result) {
    case DONT_KNOW:
      // 不会:熟练度不变
      resultText = "不会";
      drawCenteredText(resultText.c_str(), 30, u8g2_font_wqy12_t_gb2312);
      drawCenteredText(studyText.c_str(), 50, u8g2_font_wqy12_t_gb2312);
      break;
    case VAGUE:
      // 模糊:熟练度+25
      currentWord.study += 25;
      if (currentWord.study > 100) currentWord.study = 100;
      resultText = "模糊";
      studyText = "进度: " + String(currentWord.study);
      drawCenteredText(resultText.c_str(), 30, u8g2_font_wqy12_t_gb2312);
      drawCenteredText(studyText.c_str(), 50, u8g2_font_wqy12_t_gb2312);
      break;
    case KNOW:
      // 会:熟练度+50
      currentWord.study += 50;
      if (currentWord.study > 100) currentWord.study = 100;
      resultText = "知道";
      studyText = "进度: " + String(currentWord.study);
      drawCenteredText(resultText.c_str(), 30, u8g2_font_wqy12_t_gb2312);
      drawCenteredText(studyText.c_str(), 50, u8g2_font_wqy12_t_gb2312);
      break;
    case UNKNOWN:
    default:
      return; // 无效输入,不处理
  }
  u8g2.sendBuffer();
  delay(1500);
}
  

input.cpp

input ADC按键识别实现

基于ESP8266的ADC功能实现,这样只需要一个ADC接口即可识别多个按键功能,此功能的实现对电阻精度要求较高,不同电阻存在误差,可能需要在config文件中去配置实际按下时测得的ADC值。


#include "input.h"
#include "display.h"
#include "storage.h"
#include "learning.h"
// 获取按键结果
StudyResult getButtonResult(int btnValue) {
  if (btnValue >= BTN_LEFT_MIN && btnValue <= BTN_LEFT_MAX) {
    return DONT_KNOW;
  } else if (btnValue >= BTN_MIDDLE_MIN && btnValue <= BTN_MIDDLE_MAX) {
    return VAGUE;
  } else if (btnValue >= BTN_RIGHT_MIN && btnValue <= BTN_RIGHT_MAX) {
    return KNOW;
  }
  return UNKNOWN;
}
// 处理输入
void handleInput() {
  int btnValue = analogRead(A0);
  if (btnValue > 100) { // 有按键按下
    if (!showAnswer) {
      // 第一次按键显示答案
      showAnswer = true;
      displayCurrentWord();
    } else {
      // 第二次按键处理学习结果
      StudyResult result = getButtonResult(btnValue);
      if (result != UNKNOWN) {
        processStudyResult(result);
        saveWordProgress();
        calculateStudiedWords(); // 重新计算已学习单词数量
        saveConfig();
        nextWord();
      }
    }
    // 等待按键释放
    while (analogRead(A0) > 100) {
      delay(50);
    }
    delay(200); // 防抖
  }
}

storage.cpp

storage 存储功能实现

存储部分采用ArduinoJson库完成对json数据文件的格式识别及增删改查,单词数据以json形式储存,可以查看example_word文件夹下的示例单词


#include "storage.h"
#include 
// 初始化存储
bool initStorage() {
  pinMode(PIN_CS, OUTPUT);
  digitalWrite(PIN_CS, HIGH);
  SPI.begin();
  if (!SD.begin(PIN_CS)) {
    return false;
  }
  return true;
}
// 计算已学习的单词数量(学习进度>0的单词)
void calculateStudiedWords() {
  studiedWords = 0;
  for (int i = 0; i < totalWords; i++) {
    File file = SD.open("/" + wordFiles[i], FILE_READ);
    if (file) {
      JsonDocument doc;
      deserializeJson(doc, file);
      int study = doc["study"] | 0;
      if (study > 0) {
        studiedWords++;
      }
      file.close();
    }
  }
  Serial.println("Calculated studied words: " + String(studiedWords));
}
// 扫描单词文件
void scanWordFiles() {
  totalWords = 0;
  File root = SD.open("/");
  while (true) {
    File entry = root.openNextFile();
    if (!entry) break;
    String fileName = entry.name();
    if (fileName.endsWith(".json") && !fileName.equals("config.json")) {
      wordFiles[totalWords] = fileName;
      totalWords++;
      if (totalWords >= 100) break;
    }
    entry.close();
    }
  root.close();
  Serial.println("Found " + String(totalWords) + " word files");
}
// 加载配置
void loadConfig() {
  if (SD.exists("/config.json")) {
    File file = SD.open("/config.json", FILE_READ);
    if (file) {
      JsonDocument doc;
      deserializeJson(doc, file);
      // studiedWords现在是动态计算的,不从配置文件加载
      String version = doc["version"] | "1.0";
      Serial.println("Config loaded - version: " + version);
      file.close();
    }
  }
}
// 保存配置
void saveConfig() {
  // 先删除原配置文件
  if (SD.exists("/config.json")) {
    SD.remove("/config.json");
  }
  // 重新创建配置文件
  File file = SD.open("/config.json", FILE_WRITE);
  if (file) {
    JsonDocument doc;
    // 不再保存studiedWords,因为它是动态计算的
    doc["totalWords"] = totalWords;
    doc["version"] = "1.1B";
    serializeJson(doc, file);
    file.close();
    Serial.println("Config saved - total: " + String(totalWords));
  } else {
    Serial.println("Failed to save config");
  }
}
// 加载单词
void loadWord(String fileName) {
  File file = SD.open("/" + fileName, FILE_READ);
  if (file) {
    JsonDocument doc;
    deserializeJson(doc, file);
    currentWord.name = doc["name"].as();
    currentWord.chinese = doc["chinese"].as();
    currentWord.study = doc["study"] | 0;
    currentWord.example = doc["example"].as();
    currentWord.frequency = 100 - currentWord.study;
    file.close();
  }
}
// 保存单词进度
void saveWordProgress() {
  String fileName = wordFiles[currentWordIndex];
  // 先删除原文件,确保完全覆盖
  if (SD.exists("/" + fileName)) {
    SD.remove("/" + fileName);
  }
  // 重新创建文件并写入
  File file = SD.open("/" + fileName, FILE_WRITE);
  if (file) {
    JsonDocument doc;
    doc["name"] = currentWord.name;
    doc["chinese"] = currentWord.chinese;
    doc["study"] = currentWord.study;
    doc["example"] = currentWord.example;
    serializeJson(doc, file);
    file.close();
    Serial.println("Saved word: " + currentWord.name + ", study: " + String(currentWord.study));
  } else {
    Serial.println("Failed to save word progress");
  }
}
  

单词json数据示例

json 单词文件

所有单词数据以json形式保存,文件命名格式为“英文.json”


{
  "name": "computer",
  "chinese": "计算机",
  "study": 90,
  "example": "I use my computer for work and study."
}
  

安装结构

结构设计

项目采用双壳体,由前壳及后壳组成。

双外壳结构
简易单词卡组装图
前盖设计
前盖正面
前盖正面视图
前盖背面
前盖背面视图
  • 前盖设计有限位槽,限位支撑薄弱处添加倒角加强,避免断裂
  • OLED屏从外侧放入,卡入屏幕限位槽中,PCB从内侧放入,卡入PCB限位槽中/li>
  • 侧面为TF卡开孔,方便TF卡拔插
底盖设计
前盖正面
底盖正面视图
前盖背面
底盖背面视图
  • 底盖部分设有限位柱,确保PCB卡在顶底壳中间
  • 限位柱同样添加倒角避免断裂
  • TypeC部分开孔,并在外圈挖槽,确保不受线缆胶壳阻挡
实物展示
实物图
实物实物侧视图
拆解图
实物实物侧视图
实物图
底壳及主板
拆解图
分解图
拆解图
WEB管理后台

拓展方向

功能拓展建议

本项目提供了一个简易的单词卡方案,你可以基于此项目进行以下拓展:

  • 增加电池充放电ic,添加电池供电
  • 添加遗忘曲线计算算法
  • 集成到Homeassistant
  • 添加更多小功能

注意事项

使用注意事项
1
采购须知
采购SSD1315/SSD1306显示屏时请务必注意线序,本项目使用GND/VCC/SCL/SDA线序,EDA-Robot标准版复刻中很多同学因为买错线序导致短路烧板。
2
焊接建议
主控面器件较多,建议优先焊接主控面器件后再焊接TF卡面的器件。全程可使用烙铁焊接,无需加热台或风枪工具
3
测试流程
焊接完成后请优先检查MCU相邻IO引脚有无连锡短路,MCU到板间有无短路。上电前先用万用表蜂鸣挡测试3.3V和5V到GND有无短路。全部确认无误后再上电。
4
编译事宜
请务必确保源码路径中无中文路径,否则可能造成编译问题。编译前请安装好VSCODE和platformio工具,并清除build构建缓存。
5
烧录教程
烧录部分可以参考 EDA-Robot机器狗-烧录教程
物料替换对照表

立创商城采购物料清单

数量 器件 位号 商城编号
310uFC1,C4C15850
4100nFC2,C3,C5,C7C5137467
1TF PUSHCARD1C393941
1PZ254V-12-6PH1C492420
2ZX-QC66-9.5CJLEFT,RIGHTC7470135
1ZX-QC66-7.5TPMIDC7470157
1010kΩR1,R2,R3,R4,R5,R8,R9,R11,R12,R13C17414
25.1kΩR6,R7C27834
11.2MΩR14C17380
1470kΩR15C17709
1270kR16C17589
1100kR17C149504
1ME6217C33M5GU1C427602
1ESP8266U2C5248080
1HS96L03W2C03U3C492409
1MC-306LS-H65USB2C6332265

💡 提示:部分通用元件可选择等效型号,重点关注封装及电气参数一致性。

烧录分区表

下表列出了 ESP8266 固件烧录时的分区布局,地址与文件对应关系如下,可直接用于命令行烧录或 Flash 工具导入配置。

烧录地址 文件名称 说明
0x0 firmware.bin 统一固件
首页/硬件项目