VBLEARN 项目1: 万年历制作,已基本完成开发,上传了源代码.
本帖最后由 vblearn 于 2015-2-21 08:29 编辑刚接触ARDUINO. 想做个万年历.
硬件,使用NANO328DS3231, 1602显示, 蜂鸣器 按钮S
支持日历,时钟,计时,倒计时 等.
支持背光的开关
可以通过串口对时钟进行校准.
在此开贴,记录开发过程,也希望共同学习进步.
欢迎楼主将开发过程发进帖子,共同学习共同进步。 按开发步骤,先写点设计.
>需要的功能
1钟表功能: 显示当前的日期,时间,星期及温度。显示公历和农历。
2计时功能 : 可以打开一个计时器
3倒计时器 :定时间,倒计时, 到时间提醒.
>需要解决的技术问题
1 使用I2C驱动1602模块. 控制背光, 显示两行字符. (已解决) 包括三个功能的不同界面.
2 使用I2C驱动DS3231. 包括时间的设置和时间温度的读取 (已解决) (两个I2C设备使用同一套 SAD,SCL线,靠设备号区分)
3 使用蜂鸣器发声提示. (已解决),但如果是播放一段音符,还需要进一步开发.
4 使用最少的键,完成模式的切换, 倒计时的调整, 开始/停止等. 这里需要处理的问题有: 键盘去抖, 长按与短按的区分, 背光的控制(长亮,有键盘时亮一段时间) 等.
5 农历的计算,可能需要参考一些其它程序,将代码移植过来.
6 设置的保存,考虑断电后某些设置, 可以保存在EEPROM中.
7 使用串口对时间进行校正. (已解决)
8 系统的总体结构, 由于存在多个"进程",需要一个总体的调度方式, 计划参考操作系统中的进程处理方式.
>可能的扩展.
如果作为连接到计算机的电路,可能由计算机开始倒计时等功能,或定时提示一些内容.
先完成了技术中的1,2,7 上个图.
本想用NANO. 但可能是接错,烧了一块. 还是先用UNO了.
现在能够使用串口设置时间, 使用1602显示DS3231中的时间.
显示部分代码,但农历的程序还没有写.
void ReadDS3231()
{
int second,minute,hour,date,month,year,week,temperature,NLMonth,NLDate;
second=Clock.getSecond();
minute=Clock.getMinute();
hour=Clock.getHour(h12, PM);
date=Clock.getDate();
month=Clock.getMonth(Century);
year=Clock.getYear();
week=Clock.getDoW();
NLMonth = 12; //Chinese date
NLDate = 13;
temperature=Clock.getTemperature();
char sState;
sState='C';
sprintf(m_s0, "20%02d-%02d-%02d W%1d %2d", year,month,date,week,temperature);
sprintf(m_s1, "%02d:%02d:%02d %.1s %2d#%2d", hour,minute, second,sState,NLMonth,NLDate);
//lcd.clear();
lcd.setCursor(0,0);
lcd.print(m_s0);
lcd.setCursor(0,1);
lcd.print(m_s1);
}
另外串口部分也值得说说. 我使用的编码方式是一个"C"后面跟12个数字,表示年月日时分秒. 在分解串时保留了扩展其它命令的可能.
void S_ReadSerial()
{
byte bTreated;
while (Serial.available() > 0) {
byte inChar = Serial.read();
bTreated = 0;
if ((inChar>='a' && inChar<='z') || (inChar>='A' && inChar<='Z')) //command
{
m_cCommand=inChar; //record the command
m_p = 0; //
m_inString=""; //
bTreated = 1;//mark treated
}
if (isDigit(inChar)) {
// convert the incoming byte to a char
// and add it to the string:
m_inString += (char)inChar;
m_p=m_p+1;
bTreated = 2; //mark treated as digit
if (m_p>=12)
{
//send command and number
FCommand(m_cCommand);
m_p=0;
m_cCommand='#'; //no command
}
}
if (bTreated==0)
{
m_cCommand='#'; //clear Command
}
}
}
这部分处理命令,现在只有一个命令.
int FCommand(char c)
{
switch (c)
{
case 'C': //Change time calibrate
//split string and go
S_SetTime();
break;
default:
break;
}
}
分解字符串和设置时间部分.
void S_SetTime()
{
//split them_inString
byte y, m, d, h, minute, s;
y = F_GetValueFromString(0,2);
m = F_GetValueFromString(2,2);
d = F_GetValueFromString(4,2);
h = F_GetValueFromString(6,2);
minute = F_GetValueFromString(8,2);
s = F_GetValueFromString(10,2);
Clock.setSecond(s);//Set the second
Clock.setMinute(minute);//Set the minute
Clock.setHour(h);//Set the hour
Clock.setDate(d);//Set the date of the month
Clock.setMonth(m);//Set the month of the year
Clock.setYear(y);//Set the year (Last two digits of the year)
}
byte F_GetValueFromString(int p,int len)
{
byte v = 0;
byte i,j;
for (i=p;i<p+len;i++)
{
v=v*10+(m_inString-'0');
}
return v;
}
<多线程的处理>
由于本程序要涉及几个模块的依次执行,而且每个模块的重复时间又不一样.所以必须使用一个调度的程序,对各模块的运行进行管理.
在开始部分定义了如下的常量和变量.记录了线程数, 各线程的下标等. 每个线程记录是否运行及下一次调用的时间. 基于millis.
#define NumberOfProcess 5
#define procReadTime 0 //Read time from DS3231
#define procDisplay 1 //Display
#define procKeyInput 2 //Key
#define procMusic 3 //Play Music
#define procSerial 4 //treat Serial
byte m_aProcEnable; //0 to disable, 1 to enable;
unsigned long m_aProcMillis; //wait to run after this time
unsigned long m_currentmillis;//current time
初始化部分
//init process
byte i;
m_aProcEnable = 1;
m_aProcEnable = 1;
m_aProcEnable = 1;
m_aProcEnable = 0; //default disable
m_aProcEnable = 1;
for (i=0;i<NumberOfProcess;i++)
{
m_aProcMillis=m_currentmillis; //define to current time
}
实际的LOOP函数void loop()
{
//call each process
byte i;
m_currentmillis=millis();//current time
for (i=0;i<NumberOfProcess;i++)
{
if (m_aProcEnable==1)
{
if ( m_aProcMillis<m_currentmillis) //not consider the overroll of millis
{
//call the process
switch (i)
{
case procSerial:
S_ProcSerial();
break;
case procDisplay:
S_ProcDisplay();
break;
case procKeyInput:
S_ProcKeyInput();
break;
case procMusic:
S_ProcMusic();
break;
case procReadTime:
S_ProcReadTime();
break;
default:
Serial.print ("Unknow process"); //Report error
Serial.println(i);
}
}
}
}
//no delay here
}
对一个线程, 在结束自己的任务后,预约下次运行的时间.
例如 这样就可以预约100ms后运行.
void S_ProcMusic()
{
//play music here
//Set Next Process time
m_aProcMillis=m_currentmillis+100;
}
还要说明的是,这里没有处理MILLIS的回滚问题.如果回滚(50天之后).则由于当前的时间大于下一次预约的时间, 所以会每次循环都运行所有的可用线程. 但不会有太大的问题. 只不过是多运行几次. 当然可以修改程序处理这种问题. (比如判断两个时间的最高位). 这个问题之后再进一步处理.
本帖最后由 vblearn 于 2015-2-17 14:54 编辑
<键盘问题的处理>
按计划,键盘中需要处理防抖, 短按长按的区分等. 如果只有简单的程序,可以直接用DELAY来处理,但现在需要多线程处理. (否则的话,长按的过程中,LCD就不会更新).
使用了如下的方式;
全局变量
其中 ShakeTime 表示防抖的时间, LongTime表示长按的时间,在这两者之间会被判断为短按.
//////////////////Keyboard Public Variables
#define NumberOfKey 4
byte m_aKeyPort; //keep IO port for each key
unsigned long m_aKeyMillis; //last state change time of key
byte m_aKeyState; //Key state
#define KeyShakeTime 20//Anti-Shake time
#define KeyLongTime 2000//For long push
初始化部分
void S_InitKeyInput()
{
//init the port for keys
m_aKeyPort=8;
m_aKeyPort=9;
m_aKeyPort=10;
m_aKeyPort=11;
for (byte i=0; i<NumberOfKey;i++)
{
pinMode(m_aKeyPort, INPUT);
m_aKeyMillis=m_currentmillis;
m_aKeyState=digitalRead(m_aKeyPort); //record current state
}
}
每线程调用时,如下处理.
void S_ProcKeyInput()
{
//Check each port
byte i,b;
for (i=0;i<NumberOfKey;i++)
{
b=digitalRead(m_aKeyPort);
//calculate the time between change
if (b!=m_aKeyState)
{ //some thing change
Serial.println(m_currentmillis);
if (b==HIGH) {
//未处理低变高
}
else //b==LOW 处理高变低
{ //calculate the time
unsigned long dt=m_currentmillis-m_aKeyMillis; //time between this change and last change.
if (dt>KeyShakeTime) //not a shake
{
if (dt>KeyLongTime) // long press
{
S_RunKey(i,1);
}
else //short click
{
S_RunKey(i,0);
}
}
}
//record the time
m_aKeyMillis=m_currentmillis;
m_aKeyState=b;
}
}
//Set Next Process time
m_aProcMillis=m_currentmillis+5; //check each time
}
最后,用一个函数来处理键盘的命令
现在只是使用串口调试. :)
//Run function for the key, i is index, bLong is Long or Short click
void S_RunKey(byte i, byte bLong)
{
if (bLong==0)
{
Serial.print ("short key");
Serial.println (i);
}
else
{
Serial.print ("long key");
Serial.println (i);
}
} 农历程序,拿好不谢!我叫雷锋! 哦,还有一个日出日落的程序。不过要经纬度,可以设置自己当地的经纬来使用。 本帖最后由 vblearn 于 2015-2-18 14:38 编辑
下面还处理了 蜂鸣器的控制, 背光的控制, 农历的实现, 键盘功能的定义等.
不一一细说了.
在此附上程序,希望与大家多交流.
做的还真好。 最后上个简化壳的图,不要见笑.
以后再用其它的材料吧. {:soso_e104:} 支持好作品~ 进行了一些修改和升级。原来使用两个按钮来调节增减,正好手头有旋转编码器,这样在调节倒计时的时间设置时比较方便。
正在实际使用中。有需求变化再进一步修改。
:):):):)谢谢 好人 板子型号?自带温度检测?
页:
[1]
2