【2024 DigiKey 创意大赛】 基于ESP32的儿童互动数学学习仪 作品总结与提交
<p>基于ESP32的儿童互动数学学习仪</p><p>作者:忙碌的死龙</p>
<p><span style="font-size:16px;"><strong>一、作品简介</strong></span></p>
<p><strong>前言</strong></p>
<p>幼小衔接阶段的数学计算通常包括基础的数数、认识数字、简单的加减法等。这个阶段的目标是让孩子对数学有一个初步的认识和兴趣,为小学阶段的学习打下基础。</p>
<p>例如:识别1到20的简单数字,能区分数字大小,例如4和7谁大。以及一些简单的加减法。</p>
<p> </p>
<p><strong>产品设计背景</strong></p>
<p>在幼儿教育中,数学启蒙是关键一环,但传统教学方法往往难以激发孩子的兴趣。为了解决这一问题,开发一个专为幼小衔接儿童打造的互动式数学学习应用。该应用通过图形化界面和触摸屏操作,让孩子们在玩乐中学习加减法,提高他们的计算能力和逻辑思维。</p>
<p> </p>
<p><strong>功能设计:</strong></p>
<p>设计一个适合小朋友的加减法学习应用,可以采用以下简化的步骤:</p>
<ol>
<li>
<p><strong>界面设计</strong>:使用明亮的颜色和卡通形象,以及大按钮和图标,确保界面友好且易于操作。</p>
</li>
<li>
<p><strong>互动性</strong>:通过触摸屏操作,让孩子通过拖动或点击图形化的图示(如水果、动物等)来完成加减法题目。</p>
</li>
<li>
<p><strong>倒计时</strong>:设置一个倒计时功能,增加游戏感,让孩子在有限时间内完成题目,提高专注力。</p>
</li>
<li>
<p><strong>错题复习</strong>:自动记录孩子做错的题目,并提供复习模式,帮助孩子巩固易错点。</p>
</li>
<li>
<p><strong>即时反馈</strong>:每当孩子答对题目,给予积极的反馈,如动画效果和表扬声音;答错时,提供正确答案和鼓励。</p>
</li>
</ol>
<p><strong>界面设计:</strong></p>
<p>我们完全可以参考实体书的一些做法,例如使用一些常见的水果,通过添加删除线或者外框,组织一些简单的加减法算式。</p>
<p><img src="https://bbs.eeworld.com.cn/data/attachment/forum/202410/28/172958d3w8zdmdkv3sd30d.png.thumb.jpg" /></p>
<p>在网上可以找到一些免费的扁平化水果图片和删除线条,组织成类似以下的一个界面设计。</p>
<p>从上到下分别是进度条提示、题目图形化提示,题目和答案选择按钮。</p>
<p></p>
<p><strong>出题函数逻辑设计:</strong></p>
<ol>
<li>
<p><strong>随机选择操作类型</strong>:决定是生成加法题目还是减法题目。</p>
</li>
<li>
<p><strong>随机生成数字A</strong>:随机选择一个数字A,确保它在1到9之间。</p>
</li>
<li>
<p><strong>根据数字A生成另一个随机数</strong>:</p>
<ul>
<li>对于加法,随机生成一个数字B,使得A和B的和小于或等于10。</li>
<li>对于减法,随机生成一个数字B,使得A和B的差大于0。</li>
</ul>
</li>
<li>
<p><strong>生成答案选项</strong>:为题目提供几个可能的答案选项,包括正确答案和几个错误的选项。</p>
</li>
</ol>
<p>测试做好的出题函数</p>
<p><img src="https://bbs.eeworld.com.cn/data/attachment/forum/202410/28/220555bkakrt9tw19zmnea.png.thumb.jpg" /></p>
<p> </p>
<p><span style="font-size:16px;"><strong>二、系统框图</strong></span></p>
<p> </p>
<p><span style="font-size: 16px;"><b>三、各部分功能说明</b></span></p>
<p>核心部分,esp32-s3-LCD开发板,负责显示数学题目和成绩,通过触屏实现答案选择和输入功能 ,若答题错误则将错误题目存储到 flash里。</p>
<p> </p>
<p>颜色传感器,通过该颜色传感器实现钞票面额识别,并通过图形化显示大约价值(例如100元可以买多少个芒果,多少个桃子),让小朋友可以直观了解到钞票的购买力。</p>
<p> </p>
<p><span style="font-size: 16px;"><b>四、作品源码</b></span></p>
<pre>
<code class="language-cpp">/*
* SPDX-FileCopyrightText: 2021-2023 Espressif Systems (Shanghai) CO LTD
*
* SPDX-License-Identifier: CC0-1.0
*/
#include "esp_log.h"
#include "freertos/FreeRTOS.h"
#include "freertos/semphr.h"
#include "freertos/task.h"
#include "esp_random.h"
#include "lvgl.h"
#include "bsp/esp-bsp.h"
#include <stdint.h>
#include <stdio.h>
#include <string.h>
static char *TAG = "app_main";
#define MAX_QUESTION_NUM 10
#define OPERATION_ADD 1
#define OPERATION_SUB 0
#define FRUIT_OFFSET 72
#define QUESTION_ERR_KEY "err_ques"
extern esp_err_t esp_storage_init();
extern esp_err_t esp_storage_set(const char *key, const void *value,
size_t length);
extern esp_err_t esp_storage_get(const char *key, void *value, size_t length);
/**
* @brief 定义一个数学题目结构体
* 该结构体用于存储一个数学题目的相关信息,包括两个数字、操作类型和答案。
*/
typedef struct {
uint8_t numbers; // 存储两个操作数,例如加法或减法中的两个数字
uint16_t operation : 2; // 存储操作类型,使用2位来表示不同的操作
uint16_t solution : 14; // 存储答案,使用14位来表示答案的值
} math_question_t;
typedef struct {
math_question_t question;
uint8_t error_count;
uint32_t question_num;
} err_question_t;
/**
* 全局数组,用于存储生成的数学题目的数组。
*/
math_question_t math_questions;
err_question_t err_questions = {0};
static char question_str;
// 创建二进制信号量
static SemaphoreHandle_t xquestionSemaphore = NULL;
static lv_style_t f_style; // 创建一个样式结构体
lv_obj_t *answer_btn = {0};
uint8_t score = 0;
uint8_t curren_que = 0;
void set_textlable(const char *str, lv_style_t *style, int16_t y_offset) {
/*Create a white label, set its text and align it to the center*/
lv_obj_t *label = lv_label_create(lv_scr_act());
lv_label_set_text(label, str);
if (style != NULL) {
lv_obj_add_style(label, style, LV_STATE_DEFAULT);
}
lv_obj_set_style_text_color(lv_scr_act(), lv_color_hex(0x333333),
LV_PART_MAIN);
lv_obj_align(label, LV_ALIGN_CENTER, 0, y_offset);
}
/**
* 按钮事件回调函数,处理按钮点击事件。
* @param e 事件对象指针。
*/
static void btn_event_cb(lv_event_t *e) {
// 获取事件代码,判断事件类型。
lv_event_code_t code = lv_event_get_code(e);
// 获取触发事件的对象的用户数据,这里存储的是按钮对应的答案值。
intptr_t value = (intptr_t)lv_obj_get_user_data(lv_event_get_target(e));
// 如果事件类型是点击事件(LV_EVENT_CLICKED)。
if (code == LV_EVENT_CLICKED) {
// 检查当前问题索引是否小于最大问题数,并且用户点击的答案是否正确。
if (curren_que < MAX_QUESTION_NUM &&
value == math_questions.solution) {
// 如果答案正确,更新得分并设置提示文本。
strcpy(question_str, "答对了!");
score += 10;
} else {
// 如果答案错误,设置提示文本。
strcpy(question_str, "答错了。");
uint8_t index =
err_questions.question_num; // 初始化index为当前错误题目的数量
for (int i = 0; i < err_questions.question_num; i++) {
if (memcmp(&math_questions, &err_questions.question,
sizeof(math_question_t)) == 0) { // 检查是否找到相同的题目
index = i;
err_questions.error_count++;
break;
}
}
if (index ==
err_questions.question_num) { // 如果没有找到相同的题目,添加新的错题
memcpy(&err_questions.question, &math_questions,
sizeof(math_question_t));
err_questions.error_count = 1; // 初始化错误次数为1
err_questions.question_num++; // 增加错误题目的数量
}
esp_storage_set(QUESTION_ERR_KEY, &err_questions, sizeof(err_questions));
}
// 清除当前活动屏幕上的所有对象。
lv_obj_clean(lv_scr_act());
set_textlable(question_str, &f_style, 40);
// 释放信号量,允许下一个问题的处理。
xSemaphoreGive(xquestionSemaphore);
}
}
// 生成指定范围内的随机数
// 参数:
// min: 随机数的最小值(包含)
// max: 随机数的最大值(包含)
// 返回值:
// 返回一个在范围内的随机数
uint32_t get_random(uint32_t min, uint32_t max) {
// 确保随机数生成器只被种子化一次
static bool seeded = false;
if (!seeded) {
// 使用硬件随机数作为种子,提高随机性
srand(esp_random());
seeded = true; // 标记为已种子化,避免重复种子化
}
uint32_t value = (rand()) % (max - min + 1) + min;
if (value == 0) {
value = (rand()) % (max - min + 1) + min;
}
return value; // 返回生成的随机数
}
/**
* 生成一系列数学题目的函数。
* 该函数生成MAX_QUESTION_NUM个数学题目,并存储在全局数组ques中。
* 每个题目随机选择加法或减法操作,并确保操作数不重复。
*/
void gen_question() {
int8_t last_value = 0; // 用于存储上一个生成的数字,以避免重复
for (int i = 0; i < MAX_QUESTION_NUM; i++) {
int8_t operation = get_random(0, 1); // 随机选择1(加法)或0(减法)
math_questions.numbers = get_random(0, 10); // 随机生成第一个操作数
// 确保第一个操作数不与上一个题目的第一个操作数相同
while (last_value == math_questions.numbers) {
math_questions.numbers = get_random(0, 10);
}
last_value = math_questions.numbers; // 更新上一个操作数的值
// 根据随机选择的操作类型生成题目
if (operation == OPERATION_ADD) {
math_questions.operation = OPERATION_ADD; // 设置操作类型为加法
// 生成第二个操作数,确保和不超过10
math_questions.numbers =
get_random(0, 10 - math_questions.numbers);
math_questions.solution = math_questions.numbers +
math_questions.numbers; // 计算答案
} else {
math_questions.operation = OPERATION_SUB; // 设置操作类型为减法
// 生成第二个操作数,确保差非负
math_questions.numbers =
get_random(0, math_questions.numbers);
math_questions.solution = math_questions.numbers -
math_questions.numbers; // 计算答案
}
}
}
LV_IMG_DECLARE(chengzi);
LV_IMG_DECLARE(taozi);
LV_IMG_DECLARE(mangguo);
LV_IMG_DECLARE(lizi);
LV_IMG_DECLARE(yangtao);
const lv_img_dsc_t *src_array = {&chengzi, &taozi, &mangguo, &lizi,
&yangtao};
LV_IMG_DECLARE(_fork);
LV_IMG_DECLARE(quan);
/**
* 创建图片对象并设置其位置。
* @param img_ptr 指向图片对象指针的指针。
* @param src 图片源。
* @param x_offset 水平偏移量。
* @param y_offset 垂直偏移量。
* @param index 图片索引。
*/
void create_and_position_image(lv_obj_t *img_ptr, const lv_img_dsc_t *src,
int16_t x_offset, int16_t y_offset, int index) {
img_ptr = lv_img_create(lv_scr_act()); // 创建图片对象
lv_img_set_src(img_ptr, src); // 设置图片源
lv_obj_align(img_ptr, LV_ALIGN_LEFT_MID, x_offset,
y_offset + (index % 2 == 0
? 0
: FRUIT_OFFSET)); // 设置图片对象的对齐方式和位置
}
/**
* 创建一个按钮,并为其设置样式和事件回调。
* @param btn 指向按钮对象的指针。
* @param value 按钮显示的值。
* @param index 按钮的索引,用于计算位置。
*/
void create_ans_button(lv_obj_t *btn, uint8_t value, uint8_t index) {
// 设置按钮的背景颜色为蓝色调色板中的主要颜色。
lv_obj_set_style_bg_color(btn, lv_palette_main(LV_PALETTE_BLUE), 0);
// 设置按钮的圆角半径为10,应用于默认状态和所有部分。
lv_obj_set_style_radius(btn, 10, LV_PART_MAIN | LV_STATE_DEFAULT);
// 根据按钮索引计算其在水平方向上的位置,并设置垂直位置为350。
lv_obj_set_pos(btn, 20 + 160 * index, 350);
// 设置按钮的大小为120x100像素。
lv_obj_set_size(btn, 120, 100);
// 为按钮添加事件回调函数,处理所有事件。
lv_obj_add_event_cb(btn, btn_event_cb, LV_EVENT_ALL, NULL);
// 将自定义样式应用到按钮的默认状态。
lv_obj_add_style(btn, &f_style, 0);
// 在按钮上创建一个标签对象,用于显示文本。
lv_obj_t *label = lv_label_create(btn);
// 准备一个字符缓冲区,用于存储按钮的值,并将其格式化为字符串。
char buff = "";
sprintf(buff, "%d", value);
// 设置标签的文本为按钮的值。
lv_label_set_text(label, buff);
// 将标签居中对齐。
lv_obj_center(label);
}
/**
* 创建一系列图片对象来显示数学题目的选项。
* @param que 指向数学题目结构体的指针,包含题目信息和选项数量。
*/
void create_images(math_question_t *que) {
const lv_img_dsc_t *src = src_array;
uint8_t total_images = que->solution; // 总图片数量
if (total_images > 10)
return; // 如果总图片数量超过10,则不创建图片
lv_obj_t *img_array; // 存储图片对象的数组
lv_obj_t *opt_array]; // 存储图片对象的数组
// 初始化x和y偏移量
int16_t y_offset = -140; // 垂直偏移量,用于将图片向下移动
int16_t x_offset = 60; // 水平偏移量,用于将图片向右移动
uint8_t count = que->numbers;
if (que->operation != OPERATION_ADD) {
count = que->numbers - que->numbers;
}
// 创建并设置每个图片对象,用于显示题目的数字
for (int i = 0; i < count; i++) {
create_and_position_image(img_array, src, x_offset, y_offset, i);
x_offset += (i % 2 == 1) ? FRUIT_OFFSET : 0; // 每两个图片水平间隔80
}
// 创建并设置每个图片对象,用于显示选项
for (int i = 0; i < que->numbers; i++) {
uint8_t img_index = que->numbers + i;
// 如果是加法,添加圈背景
if (que->operation == OPERATION_ADD) {
create_and_position_image(opt_array, &quan, x_offset, y_offset,
i + count);
}
create_and_position_image(img_array, src, x_offset, y_offset,
i + count);
// 如果是减法,添加打叉图案
if (que->operation != OPERATION_ADD) {
create_and_position_image(opt_array, &_fork, x_offset, y_offset,
i + count);
}
x_offset +=
((i + count) % 2 == 1) ? FRUIT_OFFSET : 0; // 每两个图片水平间隔80
}
// 格式化问题字符串,包含两个数字和一个操作符。
sprintf(question_str, "%d %s %d = ()", que->numbers,
que->operation ? "+" : "-", que->numbers);
set_textlable(question_str, &f_style, 40);
// 生成一个0到2之间的随机索引,用于确定正确答案按钮的位置。
uint8_t right_index = get_random(0, 2);
uint8_t v = -1; // 用于生成错误答案的变量。
// 循环创建三个答案按钮。
for (int i = 0; i < 3; i++) {
answer_btn =
lv_btn_create(lv_scr_act()); // 创建按钮并添加到当前活动屏幕上。
// 如果当前索引是正确答案的索引,设置按钮的用户数据为正确答案。
if (right_index == i) {
lv_obj_set_user_data(answer_btn, (void *)(intptr_t)que->solution);
create_ans_button(answer_btn, que->solution,
right_index); // 创建正确答案按钮。
} else {
// 如果当前索引不是正确答案的索引,生成错误答案。
if (que->solution == 0) {
int32_t wrong_solution = 0;
wrong_solution = que->solution + i + 1; // 计算错误答案。
// 设置按钮的用户数据为错误答案,并创建错误答案按钮。
lv_obj_set_user_data(answer_btn, (void *)(intptr_t)wrong_solution);
create_ans_button(answer_btn, wrong_solution, i);
} else {
int32_t wrong_solution = 0;
wrong_solution = que->solution + v; // 计算错误答案。
// 设置按钮的用户数据为错误答案,并创建错误答案按钮。
lv_obj_set_user_data(answer_btn, (void *)(intptr_t)wrong_solution);
create_ans_button(answer_btn, wrong_solution, i);
}
v = 1; // 更新v的值,以便下次循环生成不同的错误答案。
}
}
}
void app_main(void) {
/* Initialize display and LVGL */
bsp_display_start();
/* Set display brightness to 100% */
bsp_display_backlight_on();
// 初始化nvs
esp_storage_init();
esp_storage_get(QUESTION_ERR_KEY, &err_questions, sizeof(err_questions));
LV_FONT_DECLARE(OPPOSans);
lv_style_set_text_font(&f_style, &OPPOSans);
// 创建二进制信号量
xquestionSemaphore = xSemaphoreCreateBinary();
ESP_LOGI(TAG, "err questions num is %lu", err_questions.question_num);
gen_question();
for (int i = 0; i < MAX_QUESTION_NUM; i++) {
curren_que = i;
create_images(&math_questions);
xSemaphoreTake(xquestionSemaphore, portMAX_DELAY);
vTaskDelay(pdMS_TO_TICKS(300));
lv_obj_clean(lv_scr_act());
}
sprintf(question_str, "太棒了\n%d 分", score);
set_textlable(question_str, &f_style, -50);
vTaskDelay(pdMS_TO_TICKS(3000));
lv_obj_clean(lv_scr_act());
char buff = "";
memset(question_str, 0, sizeof(question_str));
for (int i = 0; i < err_questions.question_num; i++) {
sprintf(buff, "%d %s %d = ? %d\n", err_questions.question.numbers,
err_questions.question.operation ? "+" : "-",
err_questions.question.numbers, err_questions.error_count);
strncat(question_str, buff,
sizeof(question_str) - strlen(question_str) - 1);
}
set_textlable(question_str, NULL, 0);
}
</code></pre>
<p><span style="font-size:16px;"><strong>五、作品功能演示视频</strong></span></p>
<p>7ee91f0fd3b65e0c9fa0ab39dafebb79<br />
</p>
<p><span style="font-size:16px;"><strong>六、链接汇总</strong></span></p>
<p><a href="https://bbs.eeworld.com.cn/thread-1290801-1-1.html" target="_blank">「2024 DigiKey 创意大赛 」1、ESP32 S3 LCD DEV开箱</a></p>
<p><a href="https://bbs.eeworld.com.cn/thread-1289548-1-1.html" target="_blank">「2024 DigiKey 创意大赛 」2、运行LVGL demo</a></p>
<p><a href="https://bbs.eeworld.com.cn/thread-1292214-1-1.html" target="_blank">[经验分享] 「2024 DigiKey 创意大赛 」3、ESPNOW发送单包时间测试</a></p>
<p><a href="https://bbs.eeworld.com.cn/thread-1297440-1-1.html" target="_blank">「2024 DigiKey 创意大赛 」4、儿童互动数学学习设计 </a></p>
<p><a href="https://bbs.eeworld.com.cn/thread-1297683-1-1.html" target="_blank">「2024 DigiKey 创意大赛 」5、补充功能_错题表和钞票面额识别功能</a></p>
<p><span style="font-size:16px;"><strong>七、项目总结</strong></span></p>
<p>本项目开发了一款专为儿童设计的互动数学学习仪,通过集成触摸屏和简约界面,实现了直观的数学互动教学。通过图形化形象化处理10以内的加减法,让小朋友更直观的学习到数字和图形的关联,提高小朋友的学习兴趣,并通过反复做题增强记忆。通过本次活动的项目,我学习到了lvgl的图形组件和自定义字体的使用方式,感受到esp32的强大魅力。</p>
页:
[1]