来玩Arduino——做一个高考倒计时表


0x00 写在前面

为啥要写这篇文章呢?因为最近想用数码管做个高考倒计时牌,但是数码管还没买,于是先用自己有的OLED屏幕做了个桌面版的。班里一些同学看到这个逼格这么高的的玩具,也想做一个玩。于是我来写这篇文章教一下。

也是找个理由更新一下许久没更的博客……

本文写的非常简单适合小白,一些大佬请不要吐槽。

photo_2017-06-16_19-24-57.jpg

0x01 准备工作

  • 一个 Arduino nano (某宝,改进版的意思是非官方,但是除了串口转USB模块之外电路是一样的,官方比这个要贵)
  • 一个 I2C 的 OLED 屏幕 (某宝
  • 一个 DS1302 时钟模块 (某宝,别忘了自己准备CR2032电池,用于断电时走时)
  • 一个按键开关 (一包,某宝
  • 若干杜邦线 (自己找吧,我上面地址里的店铺是有的卖的,我一共用了10条公对公、5条公对母的杜邦线)
  • 一个面包板 (某宝
  • 一台电脑 (这就不给购买地址了)

0x02 必要准备

  1. 安装驱动和Arduino官方IDE
  2. 本文中所有程序都可以在GitHub上的sohaking/arduino-oled-countdown仓库找到。建议先Download ZIP下来。


展开这里看GitHub的Download ZIP在哪儿
githubdownloadzip.png

0x03 接线

  1. 将Arduino nano插在面包板上。从用杜邦线把上方右数第四个GND和左边的竖排负极连起来,再引一条线到右边的竖排负极。
  2. 下排右数第四个5V接到右边竖排正极,下排左数第二个3V3接到左边竖排正极。
  3. 插上OLED屏幕。OLED的VCC与GND接到右排5V的正负极。SCL、SDA引脚分别接到nano的D6、D7。
  4. 把DS1302的VCC和GND接到左排3.3V的正负极。CLK、DAT、RST引脚分别接到nano的D8、D9、D10。
  5. 把按键开关骑在中间断开的缝两边,这个有点难描述,看图。
    buttonconnection.png

0x04 开始使用Arduino IDE

  1. 把GitHub上的msparks/arduino-ds1302adafruit/Adafruit-GFX-Librarysohaking/arduino-oled-countdown通过Download ZIP的方式下载下来(前文有提到方法)并解压。并把arduino-ds1302和Adafruit-GFX-Library分别解压出来的内容和arduino-oled-countdown中的lib文件夹下的SSD1306syp文件夹复制到电脑上的文档库中的Arduino文件夹下的libraries中。
    extract.png
  2. 用USB线把nano连上电脑。打开Arduino IDE。设置好板子、端口、ISP等内容。
    arduinoide.png

0x05 引入部分汉字字模

为了节约成本,我这里没有使用中文字库,而是采用手动提取字模的方式。这个网上教程很多。我提取出来的字模在charBitmaps.h中。

charBitmaps.pnh.png

0x06 定义IO接口

为了方便,我把IO接口用定义在了ioDefinition.h中。这里的引脚和0x03部分中的接线相同。不过_IO_TO_BOARD_LED中的D13接口十分特殊,D13还连接到了板子上标着L的LED。因此我们可以通过控制D13引脚控制L的亮灭。

ioDefinition.png

0x07 程序部分

这里的程序部分通过讲解oled-countdown.ino中的代码实现。下面的代码为最小可执行代码,去除了一些用来装逼的代码。比如void zhuangbiLED(){}void showBoot(){}

#include "ioDefinition.h"
#include <stdio.h>
#include <string.h>
#include <time.h>
#include <math.h>
#include <DS1302.h>
#include <Adafruit_ssd1306syp.h>
#include "charBitmaps.h"
//上面是一些必要的库文件

Adafruit_ssd1306syp oled(_IO_OLED_SDA,_IO_OLED_SCL);   //创建一个SSD1306syp的实例,用来控制OLED
DS1302 rtc(_IO_CLOCK_RST,_IO_CLOCK_DAT,_IO_CLOCK_CLK); //创建一个DS1302的实例,用来获取时间等

time_t lastTime;

time_t makeTime(int year,int month,int day,int hour,int minute,int second){ //创建一个时间
    struct tm timeconv;
    timeconv.tm_hour=hour;timeconv.tm_min=minute;timeconv.tm_sec=second;timeconv.tm_year=year;timeconv.tm_mon=month;timeconv.tm_mday=day;timeconv.tm_isdst=0;
    return(mktime(&timeconv));
}

void shutDownScreen(){oled.clear();oled.update();}//清屏,保护屏幕

time_t getTime(){ //获取当前时间
    Time timeNow=rtc.time();struct tm timeconv;
    timeconv.tm_hour=timeNow.hr;timeconv.tm_min=timeNow.min;timeconv.tm_sec=timeNow.sec;timeconv.tm_year=timeNow.yr;timeconv.tm_mon=timeNow.mon;timeconv.tm_mday=timeNow.date;timeconv.tm_isdst=0;
    return(mktime(&timeconv));
}

void prepareCountdownInfo(){ //显示倒计时
    long cdtime=(difftime(makeTime(2018,6,7,9,0,0),getTime())); //makeTime()里面是目的时间
    int day=cdtime/86400;cdtime=cdtime%86400;
    int hour=cdtime/3600;cdtime=cdtime%3600;
    int minute=cdtime/60;cdtime=cdtime%60;
    int second=cdtime;
    //上面是计算倒计时
    char days[10],hr[10],mins[10],secs[10];
    //数字转字符串,为后面通过遍历字符进行数位分离做准备,同时添加前导零用于补位
    dtostrf((double)(day),4,0,days);dtostrf((double)(hour),2,0,hr);dtostrf((double)(minute),2,0,mins);dtostrf((double)(second),2,0,secs);
    //显示 0000天
    displayNumber(days,0,48);oled.drawBitmap(32,48,_CHARBMP_chinese_tian1,16,16,1);
    //显示 两位时
    displayNumber(hr,56,48);
    //显示 冒号
    oled.drawBitmap(72,48,_CHARBMP_symbol_colon_half,8,16,1);
    //显示 两位分
    displayNumber(mins,80,48);
    //显示 冒号
    oled.drawBitmap(96,48,_CHARBMP_symbol_colon_half,8,16,1);
    //显示 两位秒
    displayNumber(secs,104,48);
}

void prepareDateTime(){ //显示当前时间
    char yr[10],mth[10],days[10],hr[10],mins[10],secs[10],dow[1];
    Time timeNow=rtc.time();//获取当前时间
    //数字转字符串,为后面通过遍历字符进行数位分离做准备,同时添加前导零用于补位
    dtostrf((double)(timeNow.yr),4,0,yr);dtostrf((double)(timeNow.mon),2,0,mth);dtostrf((double)(timeNow.date),2,0,days);dtostrf((double)(timeNow.hr),2,0,hr);dtostrf((double)(timeNow.min),2,0,mins);dtostrf((double)(timeNow.sec),2,0,secs);dtostrf((double)((timeNow.day)-1),1,0,dow);
    //显示 0000年
    displayNumber(yr,0,0);oled.drawBitmap(32,0,_CHARBMP_chinese_nian1,16,16,1);
    //显示 00月
    displayNumber(mth,48,0);oled.drawBitmap(64,0,_CHARBMP_chinese_yue1,16,16,1);
    //显示 00日
    displayNumber(days,80,0);oled.drawBitmap(96,0,_CHARBMP_chinese_ri1,16,16,1);
    //显示 日期所对应的星期。如日一二三……
    displayChineseDOW(dow,0,16);
    //显示 两位时
    displayNumber(hr,32,16);
    //显示 冒号
    oled.drawBitmap(48,16,_CHARBMP_symbol_colon_half,8,16,1);
    //显示 两位分
    displayNumber(mins,56,16);
    //显示 冒号
    oled.drawBitmap(72,16,_CHARBMP_symbol_colon_half,8,16,1);
    //显示 两位秒
    displayNumber(secs,80,16);
}

int numberCharToInt(char o) { //手动一位字符串转数字
    if(o=='0')return(0);if(o=='1')return(1);if(o=='2')return(2);if(o=='3')return(3);if(o=='4')return(4);if(o=='5')return(5);if(o=='6')return(6);if(o=='7')return(7);if(o=='8')return(8);if(o=='9')return(9);return(0);
}

void displayNumber(char *e, int x, int y) { //显示阿拉伯数字
    int len=strlen(e);int lop;
    for(lop=0;lop<len;lop++)if((char *)e[lop]!=(char *)'.'){
        oled.drawBitmap(x+lop*8,y,_CHARBMP_numbers[numberCharToInt(e[lop])],8,16,1);
    }else{
        oled.drawBitmap(x+lop*8,y,_CHARBMP_symbol_dot,8,16,1);
    }
}

void displayChineseNumber(char *e, int x, int y) { //显示形如零一二三的中文数字
    int len=strlen(e);int lop;
    for(lop=0;lop<len;lop++)if((char *)e[lop]!=(char *)'.')oled.drawBitmap(x+lop*16,y,_CHARBMP_chinese_numbers[numberCharToInt(e[lop])],16,16,1);
}

void displayChineseDOW(char *e, int x, int y) { //显示星期几,传入"0"为日,"1"为一等等
    int len=strlen(e);int lop;
    for(lop=0;lop<len;lop++)if((char *)e[lop]!=(char *)'.'){
        int swap=numberCharToInt(e[lop]);
        if(swap==0){
            oled.drawBitmap(x+lop*16,y,_CHARBMP_chinese_ri1,16,16,1);
        }else{
            oled.drawBitmap(x+lop*16,y,_CHARBMP_chinese_numbers[swap],16,16,1);
        }
    }
}

void setup(){ //Arduino标准过程,在开机时运行
    pinMode(_IO_TO_BOARD_LED,OUTPUT);//板载LED初始化
    rtc.writeProtect(true);//时钟模块初始化
    rtc.halt(false);//时钟模块初始化
    oled.initialize();//OLED初始化
    oled.clear();//OLED初始化
    //下面是在(0,33)的位置写一句话
    oled.setCursor(0,33);oled.println(" - plz press the button to see.");oled.update();
    delay(2000); //停留2s
    shutDownScreen(); //清屏
}

void loop(){ //Arduino标准过程,在setup后不断循环执行
    Time ttttt=rtc.time(); //获取当前时间
    //下面是如果检测到按钮按下,那么把当前时间存入lastTime
    if(analogRead(_IO_BUTTON_AP)>700)lastTime=makeTime(ttttt.yr,ttttt.mon,ttttt.date,ttttt.hr,ttttt.min,ttttt.sec);
    if(difftime(getTime(),lastTime)<=10){ //如果距离lastTime不足10s,那么显示。也就是说这一段是显示10s后自动关屏
        oled.clear(); //清屏
        prepareDateTime(); //见上面
        prepareCountdownInfo(); //见上面
        displayEvent(); //见下面
        for(int i=0;i<128;i+=2)oled.drawPixel(i,31,1); //划出一条虚线
        oled.update(); //把缓存中的数据显示到屏幕
    }else{
        shutDownScreen(); //10秒后关屏
    }
    delay(40); //40ms后再运行第二次
}

void displayEvent(){ //用来显示“2018年高考:”
    displayNumber("2018",8,32);//显示数字2018
    //下面是显示“年高考:”
    oled.drawBitmap(40,32,_CHARBMP_chinese_nian1,16,16,1);oled.drawBitmap(56,32,_CHARBMP_chinese_gao1,16,16,1);oled.drawBitmap(72,32,_CHARBMP_chinese_kao1,16,16,1);oled.drawBitmap(88,32,_CHARBMP_symbol_colon,8,16,1);
}

0x08 烧写!

使用上面的Upload按钮可以烧写程序。请确保0x04中的第2步设置正确。

Upload.png

随后你的nano会自动重启,完成烧写后就能看到提示啦。这时候按下按钮就可以看到文章开头的效果了

0x09 校准时间

DS1302刚拿出来用的时候时间肯定不准确,需要调时间。你可以打开Arduino IDE中的例子中最下面的那个DS1302中的set_clock。

setclockexample.png

你需要先另存为它,然后将第17-19行的5、6、7分别改成10、9、8(即上面接线的引脚)。

changeIO.png

然后在setup()中修改成准确的时间。点击Upload,刷写完后启动,setup()里设置时间的指令会立刻执行。

根据这个道理我们可以先预先加个30s。也就是说,假设现在是 2017年6月16日星期五23:33:33,为了校准,我可以在程序里设置成Time t(2017, 6, 16, 23, 34, 00, Time::kFriday);。然后立刻刷写。等到23:33:58秒的时候按下nano上的reset按键,那么时间的误差就非常小了。

settime.png


协议: 本文根据 Creative Commons Attribution-NonCommercial-ShareAlike 4.0 License 进行授权。

标签: arduino


撰写新评论

account_circle
mail
insert_link
mode_comment