[ESP8266/ESP32]夜间计算训练器 精华

1749浏览
查看: 1749|回复: 2

[ESP8266/ESP32] 夜间计算训练器

[复制链接]
本帖最后由 zoologist 于 2021-11-16 14:25 编辑

为了更好的响应国家双减政策,培养对于社会有用的人才,以及减轻自己哄睡的负担,我制作了一个“夜间计算训练器”。
训练的方法是随机生成运算式,然后通过语音播报的方式要求用户输入。当然为了节省空间,使用了三组船型开关,通过 8421 码的方式进行输入。8421码每一位二值代码的“1”都代表一个固定数值。将每位“1”所代表的二进制数加起来就可以得到它所代表的十进制数字。因为代码中从左至右看每一位“1”分别代表数字“8”“4”“2”“1”,故得名8421码。例如:  输入为1001表示 8+1=9。这样用12个开关能够表示0-999个数字。当然,如果以后上高年级,可以考虑使用10个开关表示 0-1023的数字。
硬件上我们使用 DFRobot出品的ESP32FireBeetle,配套的萤火虫OLED12864显示屏 Gravity中英文语音合成模块(基于XFS5152芯片),然后还有用于输入的船型开关。
接下来开始硬件设计,整体电路图如下:
夜间计算训练器图2

左上角是一个用于消耗电力,保证在充电宝供电的情况下仍能正常工作的部分(大部分充电宝在电流小于100ma的情况下过一段时间会自动切断供电,这里可以通过定时器每隔一段时候拉出一个电流避免这种情况)。上方中间是一个USBTypeA公头,用于外部电源输入。就是说这个设计可以使用FireBeetle上的 Micro USB,也可以使用USBTypeA来进行供电。特别注意需要避免两者同时工作,避免电流倒灌的问题。
左下角是一个键盘矩阵的设计在,其中使用二极管来保证不会有误判情况,这样能够实现判断多个按键同时按下的情况。具体实现是采用动态扫描的方式来进行的,比如:首先拉高KEY_ROW1同时保持KEY_ROW2KEY_ROW3为低。这时,如果 U1-U4出现接通的情况,对应的KEY_COLx就会出现高电平。接下来再类似操作 KEY_ROW2…...这样就能实现全部按键输入的扫描判断。
夜间计算训练器图1
语音模块支持串口和I2C这里我们选择 I2C,上面预留了拉高到3.3V的电阻。
夜间计算训练器图3
可以看到板子上还有三个按键,通过这些按键能够实现菜单的选择以及确认提交输入结果、返回主菜单和重新播报题目的功能。核心的ESP32并非全部引脚都能当成 GPIO使用,如果有不当会导致反复重启,这次设计IO非常紧张:
夜间计算训练器图4
因为船型开关的存在,整体PCB较大,布线轻松就能完成:
夜间计算训练器图5
3D渲染
夜间计算训练器图6
拿到手装配完成:
夜间计算训练器图7
接下来开始软件部分的设计,代码的状态转移图如下:
夜间计算训练器图8
关键代码部分如下
1.     在状态1中绘制主菜单,绘制函数如下,显示 Current ,Current+1……Current+3 这三个条目。当有按键时,会将 Current进行增减再重新绘制,这样就实现了菜单的选择功能:
  1. // 在 OLED 上绘制菜单
  2. void ShowMainMenu(int Current)
  3. {
  4.   char Buffer[40];
  5.   OLED.clear();
  6.   // 第一个条目前添加  "→" 符号
  7.   sprintf(Buffer, "→%s", MainMenu[Current]);
  8.   OLED.disStr(0, 0, Buffer);
  9.   // 其余条目前添加空格和第一个条目对齐
  10.   sprintf(Buffer, "  %s", MainMenu[(Current + 1) % MENUITEMCOUNTER]);
  11.   OLED.disStr(0, 16, Buffer );
  12.   sprintf(Buffer, "  %s", MainMenu[(Current + 2) % MENUITEMCOUNTER]);
  13.   OLED.disStr(0, 32, Buffer);
  14.   sprintf(Buffer, "  %s", MainMenu[(Current + 3) % MENUITEMCOUNTER]);
  15.   OLED.disStr(0, 48, Buffer);
  16.   OLED.display();
  17. }
复制代码

2.     状态4中实现了生成算式的功能。使用随机数生成参加运算的数字,同时使用枚举类型定义了四则运算,这样random生成了0-3就对应了4个运算符。
  1. // 运算符,分别是加减乘除
  2. enum MATHOPERATION
  3. {
  4.   OPADD = 0, OPSUB, OPMUL, OPDIV
  5. };
复制代码

      以生成两位数相减为例,首先生成2个数字,然后测试运算结果,只有结果不是负数的情况才是一个合格的算式。之后生成的被减数再 Num[0]中,减数在Num[1]中,运算符存放在Op[0]中,正确的结果在Result
  1.       case 4://"两位数相减"
  2.         CalcNum = 2; // 2个数字运算
  3.         do
  4.         {
  5.           Num[0] = random(100);
  6.           Num[1] = random(91) + 10;
  7.         }
  8.         while (Num[0] < Num[1]);   //避免结果是负数
  9.         Op[0] = OPSUB;
  10.         Result = Num[0] - Num[1];
  11.         break;
复制代码

3.     算式生成后就是对用户播报的过程,具体在状态5中,可以看到分别读出数值和运算符

  1.    // 语音播报表达式
  2.     readValue(Num[0]);
  3.     readOp(Op[0]);
  4.     readValue(Num[1]);
  5.     // 如果是三个数运算,那么多读出一个运算符和运算数
  6.     if (CalcNum == 3)
  7.     {
  8.       readOp(Op[1]);
  9.       readValue(Num[2]);
  10. }
复制代码

数字需要特别处理,为此编写一个函数readValue(),能够输出 0-999的读音:
  1. // 语音输出一个数字
  2. void readValue(int Value)
  3. {
  4.   if (NCDEBUG)
  5.   {
  6.     Serial.print("ReadValue:");
  7.     Serial.println(Value);
  8.   }
  9.   String DataBuffer[12] = {{"零"}, {"一"}, {"二"}, {"三"}, {"四"}, {"五"}, {"六"}, {"七"}, {"八"}, {"九"}, {"十"}, {"百"}};
  10.   // 百位不为零
  11.   if (Value / 100 != 0)
  12.   {
  13.     // 读出百位
  14.     ss.speak(DataBuffer[Value / 100]);
  15.     // 读出"百"
  16.     ss.speak(DataBuffer[11]);
  17.     // 如果十位为 0 ,那么直接读出"零"
  18.     if (Value / 10 % 10 == 0)
  19.     {
  20.       ss.speak(DataBuffer[0]);
  21.     }
  22.   }
  23.   else
  24.   {
  25.     // 十位不为零
  26.     if ((Value / 10 % 10) != 0)
  27.     {
  28.       // 读出十位(不含0的情况)
  29.       if (Value / 10 == 1) { // 13 这种直接读“十三”
  30.         ss.speak(DataBuffer[10]);
  31.       } else {  // 23 这种读出“二十三”
  32.         ss.speak(DataBuffer[Value / 10 % 10]);
  33.         ss.speak(DataBuffer[10]);
  34.       }
  35.     }
  36.   }
  37.   // 个位不为零
  38.   if ((Value % 10) != 0)
  39.   {
  40.     ss.speak(DataBuffer[Value % 10]);
  41.   }
  42.   // 零特别处理
  43.   if (Value == 0)
  44.   {
  45.     ss.speak(DataBuffer[0]);
  46.   }
  47. }
复制代码

4.     接下来在装填6中等待输入,其中GetMatrix()是矩阵扫描函数。前面提到过,基本操作是KEY_ROW1设置为HIGH,KEY_ROW2和KEY_ROW3设置为低,然后读取KEY_COL1-3的电平。需要特别注意的是这个动作完成后必须有足够的延时,否则再拉高KEY_ROW2,再读取KEY_COL1-3的值是不正确的。例如按键如下:
  
  
ROW1
ROW2
COL1
闭合(U1)
断开(U1)
COL2
断开(U1)
断开(U1)
开始扫描后,首先设置 COL1=HIGH, COL2=LOW,然后读取 ROW1=HIGHROW2=LOW;如果没有加入 Delay 那么接下来会设置COL1=LOW,COL2=HIGH,然后读取 ROW1会得到 HIGH的结果,这是因为ESP32速度很快,ROW1上的电荷还来不及释放掉。
  1. // 扫描按钮矩阵,返回当前的输入值
  2. int GetMatrix()
  3. {
  4.   int result = 0;
  5.   digitalWrite(KEY_ROW1, HIGH);
  6.   digitalWrite(KEY_ROW2, LOW);
  7.   digitalWrite(KEY_ROW3, LOW);
  8.   delay(10);
  9.   // 第一行
  10.   result = (digitalRead(KEY_COL1) << 3) +
  11.            (digitalRead(KEY_COL2) << 2) +
  12.            (digitalRead(KEY_COL3) << 1) +
  13.            digitalRead(KEY_COL4) ;
复制代码

5.     最后就是等待按下提交键进行判断。




zoologist  高级技匠
 楼主|

发表于 2021-11-16 14:25:51

本文提到的电路图
下载附件夜间计算训练器电路图.zip

本文提到的代码
下载附件NightCalc.zip
回复

使用道具 举报

zoologist  高级技匠
 楼主|

发表于 2021-11-30 13:47:29

工作的视频可以在B站看到

https://www.bilibili.com/video/BV1q3411b7P9/
回复

使用道具 举报

您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

为本项目制作心愿单
购买心愿单
心愿单 编辑
[[wsData.name]]

硬件清单

  • [[d.name]]
btnicon
我也要做!
点击进入购买页面
上海智位机器人股份有限公司 沪ICP备09038501号-4

© 2013-2024 Comsenz Inc. Powered by Discuz! X3.4 Licensed

mail