文章目录
- 前言
- 一、需要准备的硬件
- 二、接线与软件准备
- 三、程序
-
四、说明
前言
本项目是我本人在大学暑期社会实践中的一个产出成果,个人感觉这个装置的制作和使用还是蛮初级的,而且在编程上也是使用了许多取巧的方法,所以对初学者还是比较友好的,有兴趣的friends可以通过这个项目对esp8266闪存系统有一个比较初步的认识,并且可以了解到与u8g2字库有关的知识和OLED显示的基本语句与格式,而且在这个文章里我会也将一些我遇到的问题和总结的经验分享给大家供大家参考,如有不足和出错请多指正。
OK,我们废话不多说先来看看这个项目吧!
emm...大家可能已经发现了这个图像右上角的署名,在这里先为我们实验室的b站账号拉一波关注,我们实验室的创作内容中有关于本项目的详细讲解视频一共有6期,大家如果有疑问可以先关注一波然后观看视频哦(PS:视频也是我本人讲解的)
视频观看方式:NUAA航天创新实验室的个人空间_哔哩哔哩_Bilibili
视频有关源代码地址:https://gitee.com/zzy_mars/dsdtj/tree/master
一、需要准备的硬件
1.esp8266
2.六个按钮
3.一块四引脚的OLED显示屏
4.一块面包板
5.其他:杜邦线,数据线
二、接线与软件准备
1.接线图:
注意事项:
1、这里尤其要注意esp8266的引脚对应关系,事实上你一般所买到的esp8266控制板上的引脚标注并不直观对应于IO口的序号所以这里要注意接线和编程定义的引脚要严格用IO口的序号下面是这种对应关系:
2、这里的D0(IO16)引脚请不要使用,然后接线对应引脚最好按照上面给出的来,主要是因为这种接线是我尝试过的多种方式中最稳定的一种。
2.关于软件烧录:
这里需要强调的是我们需要烧录两次,可能没接触过闪存的小伙伴就有疑问了:这烧录两次不是就把程序覆盖掉了吗?其实这里我们上传题库所用的闪存是esp8266上一块独立存储空间,需要用到相关库和其中的函数才能对其进行操作,所以我们这里相当于在第一次烧录时把esp8266当U盘去使用,给上面传了一个文本文档而已,而辅助这个文本文档上传的代码并不是我们最后功能所关注的。因此在第二次烧录的时候我们直接用功能代码将之前的覆盖,这里用相关代码创建了一个copy缓存(为了不对原来文本的完整性造成破坏而且能对文本相同内容任意的地重复地操作)并用读取文本的代码和控制OLED显示的代码实现主要功能,而并没有去直接操作之前写入的文本文档,这就是烧录两次的原因。所以当你做出成品后,如果想对功能代码有所改动只需再烧录一次功能代码即可,而如果要想改变题库则需要先烧录新题库,再烧录功能代码。
关于烧录,如果是喜欢使用vscode的friends只需在创建项目的时候选一下开发板类型就行了;而使用Arduino IDE的friends就要麻烦一点点,因为一般直接下载的Arduino IDE是不自带为esp8266编译的选项的所以用Arduino IDE的小伙伴看到这里如果发现自己之前还没使用过esp8266可以先在CSDN上搜索有关怎么添加esp8266编译项的博客,都讲的很详细而且实际操作也没毛病,所以这里我就不详述了。
我主要说一下烧录的时候可能遇到的一些问题和解决方案供大家参考:
1.如果烧录不上去可以尝试将D0引脚接地并在程序烧录过程中长按reset键
2.注意要使用数据线烧录,不要用普通的充电线
3.注意烧录顺序:先烧文本写入的程序,再烧文本读取与控制显示屏的程序
4.看看烧录串口选项对不对,板子类型对不对,串口监视器波特率设置对不对,库文件导入有没有问题。。。。
三、程序
看程序之前先谈功能需求:这里所制作的迷你答题机是为了实现弱人机交互,通过使用者按下的按键来与题库中的题目答案进行对比从而实现在规定时间内显示题目并由使用者作答而且在作答时间结束后能够显示使用者作答情况的功能。
所以我们可以看出需求所对应的代码部分:
需求 | 有关代码内容 |
接收用户按下按钮信息 | 按钮引脚的电频设置与如何区分长按和短按 |
题目的写入 | 有关闪存的使用设置与写的语句格式 |
题目的显示 | u8g2库的使用与有关字库的选取和设置 |
计时功能 | 定时器函数 |
答题结果显示 | 题库答案检索与答题计分的函数 |
注:因为个人所需,在制作中我采用的是有我们社会实践的题库,如果大家需要更换题库只需按照我所给出的题库录入格式重新录入你所需的题库即可
1、写入题库的代码
//南京航空航天大学_航天学院_本科生创新实验室出品
//by_zzy
#include <FS.h> //闪存库文件
String file_bank = "/答题机"; //对即将建立的文件命个名
void setup() {
Serial.begin(9600);
SPIFFS.format(); //格式化闪存
delay(30);
if(SPIFFS.begin()==1)//启动闪存并检测是否成功启动
{
Serial.println("启动成功");
}
else
{
Serial.println("启动失败_请reset");
}
File wfile = SPIFFS.open(file_bank,"w"); // //建立File对象读取题库文件,wfile相当于一个克隆的文件,注意一定只能用“w”
//写入动作
/*注意下面题库录入的格式*/
wfile.print("[011.今年是哪一年?|A.1920 B.1922|C.2020 D.2022|*D ") ;
wfile.print("[022.今年生肖是什么?|A.鼠 B.兔|C.猴 D.虎|*D ") ;
wfile.close(); // 完成关闭wfile
Serial.println("完成写入");
}
void loop() {
}
2.功能代码
(这里为方便查看分两部分给出,使用的时候按顺序拼在一起就行了)
1.设置与一些函数的定义:
//南京航空航天大学_航天学院_本科生创新实验室出品
//by_zzy
#include <FS.h> //闪存库文件
#include <U8g2lib.h> //u8g2库文件
#include <Ticker.h> //esp8266内置定时器库
//以下三个库用以将字符数组转换为整形数字
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
Ticker timer1; //定时器对象建立
U8G2_SSD1306_128X64_NONAME_F_HW_I2C u8g2(U8G2_R0, U8X8_PIN_NONE,14,2); //显示屏I2C通信设置
String file_bank = "/答题机"; //闪存中我们之前所存储的题库文件检索
/*关于引脚连线的定义*/
#define A 13 //选项A对应GPIO引脚
#define B 12 //选项B对应GPIO引脚
#define C 4 //选项C对应GPIO引脚
#define D 5 //选项D对应GPIO引脚
#define OK 3 //开始答题的GPIO控制引脚
#define BACK 1 //返回初始界面的GPIO引脚
/*关于一些界面显示的设定*/
int now = -1; //界面转换因子
int dot; //定时器计时因子
const int set_period = 60000; //设置答题时间(ms)
int x = 3; //当前显示的开始光标位置x
int y = 3; //前显示的开始光标位置y
/*关于答题流程的定义*/
int list = 1; //当前题号,根据此号码在rfile文件中读取所需题目
byte answer ; //储存用户输入的答案之后判断对错
int right_num=0; //储存用户输入的正确答案数
int sum = 0; //储存用户答题数
File rfile ; //建立一个文件之后用于克隆原题库(原题库的名称就是“/答题机”)
void setup() {
Serial.begin(9600);
if(SPIFFS.begin()==1) //启动闪存并检测是否成功启动
{
Serial.println("启动成功");
}
else
{
Serial.println("启动失败_请reset");
}
if (SPIFFS.exists(file_bank)==1) //检测是否存在题库文件
{
Serial.print(file_bank);
Serial.println("存在!");
}
else
{
Serial.print(file_bank);
Serial.print("不存在!_请重新上传或写入文件");
}
u8g2.begin(); //u8g2启动
u8g2.enableUTF8Print(); //设置utf8打印的开启(简而言之:开启中文打印)
/*这里我们拉高按键所接引脚的电平,另一端接地,对应引脚电平变低即代表按下按钮*/
pinMode(A,INPUT_PULLUP);
pinMode(B,INPUT_PULLUP);
pinMode(C,INPUT_PULLUP);
pinMode(D,INPUT_PULLUP);
pinMode(OK,INPUT_PULLUP);
pinMode(BACK,INPUT_PULLUP);
u8g2.setFont(u8g2_font_wqy12_t_gb2312); //加载中文全字库
}
void count(int pin ) //计时器的回调函数每100ms调用一次,计时因子dot加100
{
dot+=100;
}
/**************最重要的部分:题库克隆,题目搜索,题目显示的实现**************/
void bank_in()
{
y=10; //让题目显示的初始行数
char data[3]; //定义一个字符数组储存所读到的两字节的题号
int data_num=0; //定义一个整型变量储存由字符型转化后的整型题号
rfile = SPIFFS.open(file_bank, "r"); //克隆题库到当前的rfile中
do{
/*if(char(rfile.read())=='[')有三个功能*/
//1.当读到'['时开始读取后面紧接的两位题号
//2.删除'['不让其显示
//3.每次判断的时候都会执行read操作这样就会不断删除当前不符合要求的字符,一直删到与list对应的那个题号前
if(char(rfile.read())=='[')
/*********************************************************************************/
{
data[0]=rfile.read(); //读取储存字符型题号第一位
data[1]=rfile.read(); //读取储存字符型题号第二位
data[2]='\0'; //data[2]设置为'\0'将其可做字符串调用
data_num=atoi(data); //将字符串data转换为整型赋值给data_num
}
if(data_num == list) //在当前所读取的data_num整型题号与用户所应该进行回答的题号list一致时显示该题目
{
do{ //将题目一行行显示
u8g2.setCursor(x,y); //设置当前显示内容的行
do{u8g2.print(char(rfile.read()));}while(char(rfile.peek())!='|'); //开始显示此行
if(char(rfile.peek())=='|'){y=y+12;char(rfile.read());} //当读到'|'准备换行操作,让光标的y加一行,并删除'|'让其不显示
}while(char(rfile.peek())!='*'); //一直执行带有换行的显示操作,直到读到'*'标志题目内容结束
char(rfile.read()); //删除字符'*'让其不显示
answer=char(rfile.read()); //'*'后紧接的一个字节为该题目的答案,读取并储存该题目答案到answer中
}
}while( data_num != list); //题目查找循环,若当前读取存储的data_num与list不符则继续查找
rfile.close();
}
2.主循环:
void loop()
{
if(dot>=set_period) {now = 7;} //定时超过设定的时间进入now=7界面
switch (now)
{
case -1: //显示默认界面,按钮操作开始答题并开启计时器
list = 1;
dot = 0;
right_num = 0;
sum = 0;
timer1.detach();
u8g2.firstPage();
do {
u8g2.setCursor(10, 10);
u8g2.print("欢迎使用迷你答题机");
u8g2.setCursor(10, 40);
u8g2.print("按OK键开始计时答题");
}
while ( u8g2.nextPage() );
if(digitalRead(OK)==LOW)
{dot = 0;timer1.attach(0.1,count, 2);now = 0;}
break;
case 0: //题目显示界面
u8g2.firstPage();
do {
u8g2.setCursor(10, 10);
bank_in();
}
while ( u8g2.nextPage() );
if(digitalRead(BACK)==LOW)
{now =5;}
if(digitalRead(A)==LOW)
{now =1;}
if(digitalRead(B)==LOW)
{now =2;}
if(digitalRead(C)==LOW)
{now =3;}
if(digitalRead(D)==LOW)
{now =4;}
break;
case 1: //选择A
if(answer=='A')
{
u8g2.firstPage();
do {
u8g2.setCursor(56, 40);
u8g2.print("正确");
}while ( u8g2.nextPage() );
right_num++;
sum++;
delay(1000);
now = 6;
}
else
{
u8g2.firstPage();
do {
u8g2.setCursor(56, 40);
u8g2.print("错误");
}
while ( u8g2.nextPage() );
sum++;
delay(1000);
now = 6;
}
if(digitalRead(BACK)==LOW)
{now =5;}
if(digitalRead(A)==LOW)
{now =1;}
if(digitalRead(B)==LOW)
{now =2;}
if(digitalRead(C)==LOW)
{now =3;}
if(digitalRead(D)==LOW)
{now =4;}
break;
case 2: //选择B
if(answer=='B')
{
u8g2.firstPage();
do {
u8g2.setCursor(56, 40);
u8g2.print("正确");
}while ( u8g2.nextPage() );
sum++;
right_num++;
delay(1000);
now = 6;
}
else
{
u8g2.firstPage();
do {
u8g2.setCursor(56, 40);
u8g2.print("错误");
}
while ( u8g2.nextPage() );
sum++;
delay(1000);
now = 6;
}
if(digitalRead(BACK)==LOW)
{now =5;}
if(digitalRead(A)==LOW)
{now =1;}
if(digitalRead(B)==LOW)
{now =2;}
if(digitalRead(C)==LOW)
{now =3;}
if(digitalRead(D)==LOW)
{now =4;}
break;
case 3: //选择C
if(answer=='C')
{
u8g2.firstPage();
do {
u8g2.setCursor(56, 40);
u8g2.print("正确");
}while ( u8g2.nextPage() );
sum++;
right_num++;
delay(1000);
now = 6;
}
else
{
u8g2.firstPage();
do {
u8g2.setCursor(56, 40);
u8g2.print("错误");
}
while ( u8g2.nextPage() );
sum++;
delay(1000);
now = 6;
}
if(digitalRead(BACK)==LOW)
{now =5;}
if(digitalRead(A)==LOW)
{now =1;}
if(digitalRead(B)==LOW)
{now =2;}
if(digitalRead(C)==LOW)
{now =3;}
if(digitalRead(D)==LOW)
{now =4;}
break;
case 4: //选择D
if(answer=='D')
{
u8g2.firstPage();
do {
u8g2.setCursor(56, 40);
u8g2.print("正确");
}while ( u8g2.nextPage() );
sum++;
right_num++;
delay(1000);
now = 6;
}
else
{
u8g2.firstPage();
do {
u8g2.setCursor(56, 40);
u8g2.print("错误");
}
while ( u8g2.nextPage() );
sum++;
delay(1000);
now = 6;
}
if(digitalRead(BACK)==LOW)
{now =5;}
if(digitalRead(A)==LOW)
{now =1;}
if(digitalRead(B)==LOW)
{now =2;}
if(digitalRead(C)==LOW)
{now =3;}
if(digitalRead(D)==LOW)
{now =4;}
break;
case 5: //若用户在答题时按下BACK键后的界面
timer1.detach();
u8g2.firstPage();
do {
u8g2.setCursor(30, 17);
u8g2.print("<计时已暂停>");
u8g2.setCursor(13, 30);
u8g2.print("您确定要放弃答题?");
u8g2.setCursor(18, 43);
u8g2.print("长按BACK放弃答题");
u8g2.setCursor(18, 56);
u8g2.print("长按OK继续答题");
}while ( u8g2.nextPage() );
delay(2000);
if(digitalRead(BACK)==LOW){now =-1;}
if(digitalRead(OK)==LOW){timer1.attach(0.1,count, 2);now = 0;}
break;
case 6: //当前题目回答完成后对list++
list++;
now = 0;
break;
case 7: //计时结束的界面
timer1.detach();
dot = 0;
u8g2.firstPage();
do {
u8g2.setCursor(10, 10);
u8g2.print("一分钟已到答题结束");
u8g2.setCursor(20, 30);
u8g2.print("共答题数:");
u8g2.setCursor(80, 30);
u8g2.print(sum);
u8g2.setCursor(20, 43);
u8g2.print("答对题数:");
u8g2.setCursor(80, 43);
u8g2.print(right_num);
u8g2.setCursor(10, 56);
u8g2.print("按BACK返回初始界面");
} while ( u8g2.nextPage() );
if(digitalRead(BACK)==LOW)
{now = -1;}
break;
default:{break;}
}
}
四、说明
程序这里涉及到多个库文件,同样,使用vscode的小伙伴可以直接查找使用,而使用Arduino IDE的小伙伴则需要在网上找一下相关的库文件然后导入。给大家梳理一下要用到的库:
#include <FS.h> //闪存库文件
#include <U8g2lib.h> //u8g2库文件
#include <Ticker.h> //esp8266内置定时器库
//以下三个库用以将字符数组转换为整形数字
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
大家可以注意到代码挺长的,可能看到有些弄不到头脑,如果是高级玩家,可以看看注释得到一些灵感,如果是新手可以去我们的视频教学区观看详细讲解,讲解视频会带大家详细过一遍代码。