疑似電波時計の制作もようやく胸突き八丁を越えたという感じのところまでやってきました。不慣れなTWELITEのプログラムに取り組んだのでかなり長い道のりでした。
前回は、TWELITEからのシリアル通信で、スリープ中のPICを起こしてデータを送信するということをやりました。
alasixosaka.hatenablog.com
今回は、逆にPICからの送信でTWELITEをスリープさせてみる実験です。
これは、時刻サーバーがなんらかのトラブルで停止してしまっているとき、あるいはTWELITE同士の通信がエラーでうまく受信できなかったとき(TWELITEは2.4GHzを通信に使うので、電子レンジのノイズに弱く、実際に家で使っているTWELITEを使った電波式の温度計も電子レンジを使っているとうまく受信できないことがあります)に、TWELITEの今のプログラムでは、サーバーからの送信を永久に待ち続けるということになってしまっているため、例えばTWELITEがスリープから目覚めて時刻サーバーからのデータを待っている時間が一定時間を越えたらもう一度スリープに入るようにするためです。以前テストしたように、TWELITEをループで回して、一定時刻を待っていると勝手に再起動がかかってしまうという問題をどうしても解決できなかったのでいわば苦肉の策です。
alasixosaka.hatenablog.com
時刻サーバーはおおむね2秒間隔で時刻データを送信しています。しかし、予定では1日1回ESP8266が起動してWifi経由でNTPから時刻データを取ってきます。この間がだいたい10秒くらいです、この間は時刻データの送信を止めています。ですので最大でも12-3秒待てば時刻データが送られてくるはずなので、受信側(つまり時計側)のTWELITEが目覚めたら、今起きたよということをPICに教えて、PIC側で秒数カウントし、15-20秒以上たっても時刻データが送られてこなかったら、スリープしなさいという命令をPICから出すことにします。PICのRTCCは各レジスタがBCDコードで管理されているので、10秒とか20秒を設定するのは簡単ですが、15秒とかは逆に中途半端な秒数でいちいちBCDを通常の16進数に変換してやる必要があります。ですので、カウント時間は20秒とします。
PIC側のプログラムです、コンフィクレーションの設定は省略しています。今回は、時刻に加えて、TWELITE側からの時刻データ以外のシリアル通信の内容を表示するために、I2Cの液晶ディスプレイを使いました。そのせいで、プログラムがずいぶん長くなって見づらくなってしまいました。最後の方にある関数群が液晶ディスプレイを動かすための関数です。こちらのサイトを参考にさせていただきました。
machoto2.g2.xrea.com
#include <xc.h> #include <PIC16f19155.h> #include <stdio.h> #include <string.h> #define _XTAL_FREQ 1000000 #define LCD_ADD 0x7C #define CONTRAST 0x28 // for 3.3V char str1[7]; char str[20]; char RxData; char Rflg; int x; char h; char m; char s; char y; char d; char mon; char wd; int cnt = 0; int cntr = 0; int almf = 0; char alm; char state = 1; //1:setup 2:wake up 3:recieve 4:error sleep //******************* プロトタイプ ******************************* void i2cByteWrite(char, char, char); void i2cTxData(char); void LCD_dat(char); void LCD_cmd(char); void LCD_clr(void); void LCD_posyx(char,char); void LCD_int(void); void LCD_str(char *); void LCD_ROMstr(const char *); char moji[] = "Hello, PIC World!"; char moji2[] = "Wak-tech"; char msgStart[] ="Test"; void putch(char data) { LCD_dat(data); // LCD への一文字表示関数 } void readRTCC(){ if (RTCCONbits.RTCSYNC==1){ while(RTCCONbits.RTCSYNC); } h = HOURS; m = MINUTES; s = SECONDS; str1[0] = (h >> 4) + 48; str1[1] = (h & 0xF) + 48; str1[2] = (m >> 4) + 48; str1[3] = (m & 0xF) + 48; str1[4] = (s >> 4) + 48; str1[5] = (s & 0xF) + 48; } void setRTCC(){ RTCCONbits.RTCWREN = 1; // Disable RTCC RTCCONbits.RTCEN = 0; RTCCONbits.RTCCLKSEL = 0; // set RTCC time 2022-09-11 21-44-10 YEAR = y; // year MONTH = mon; // month WEEKDAY = wd; // weekday DAY = d; // day HOURS = h; // hours MINUTES = m; // minutes SECONDS = s; // seconds // set Alarm time 2022-09-11 21-44-10 ALRMCONbits.ALRMEN = 0; // ARPT 0; ALRMRPT = 0x00; ALRMMTH = mon; // month ALRMWD = wd; // weekday ALRMDAY = d; // day ALRMHR = h; // hours ALRMMIN = m; // minutes ALRMSEC = s; // seconds // Re-enable the alarm ALRMCONbits.ALRMEN = 1; // AMASK Every Second; CHIME enabled; ALRMEN enabled; ALRMCON = 0xC4; // CAL 0; RTCCAL = 0x00; // Enable RTCC RTCCONbits.RTCEN = 1; while(!RTCCONbits.RTCEN); // Disable write operations on RTCC timer registers RTCCONbits.RTCWREN = 0; } void sendt(){ while (!PIR3bits.TX1IF); TX1REG = 0x74; } char read() { while(!PIR3bits.RC1IF){ if(RC1STAbits.OERR){ // Overrunエラーなら RC1STAbits.CREN=0; // エラーをクリア RC1STAbits.CREN=1; // 再起動 RxData = RC1REG; // 空読み continue; // 再受信 } PIR3bits.RC1IF = 0; } return (char)RC1REG; } void __interrupt() isr(void) { INTCONbits.GIE = 0 ; int n = 0; if (PIR3bits.RC1IF){ RxData = read(); str[n] = RxData; n = n + 1; while(1){ RxData = read(); str[n] = RxData; n = n + 1; if (RxData == 10){ break; } } state = str[n-3] - 0x6F; if ((state == 2)||(state == 1)){ //"p" or "q" wake up/setup なら20秒間待機 LCD_posyx(1,1); printf("%s", str); alm = s + 32; if (alm>=0x60){ alm = alm -0x60; } if (state==1){ cnt++; LCD_posyx(1,8); printf(" "); LCD_posyx(1,8); printf("st%d", cnt); } }else if(state == 3){ //"r" なら正常受信 wd = str[n-4]; y = str[n-5]; mon = str[n-6]; d = str[n-7]; h = str[n-8]; m = str[n-9]; s = str[n-10]; cntr++; setRTCC(); LCD_posyx(1,12); printf(" "); LCD_posyx(1,12); printf("r%d", cntr); } }else{ if (PIR8bits.RTCCIF){ PIR8bits.RTCCIF = 0; readRTCC(); LCD_posyx(0,0); printf("%s", str1); switch(state){ case 1: if (s == alm){ LCD_posyx(1,1); printf("TIME"); sendt(); } break; case 2: if (s == alm){ LCD_posyx(1,1); printf("time"); sendt(); sendt(); } break; case 4: LCD_posyx(1,1); printf("sleep"); sendt(); break; default: break; } } } INTCONbits.GIE = 1 ; return; } void main(void) { char num = 10; ANSELA = 0b00000000; ANSELB = 0b00000000; ANSELC = 0b00000000; TRISA = 0b00001000; TRISB = 0x00; TRISC = 0xFF; // Port すべて入力 WPUC = 0xFF; // 弱プルアップ ON PPSLOCK = 0x55; PPSLOCK = 0xAA; PPSLOCKbits.PPSLOCKED = 0; RC6PPS = 0x0D; //RC6をTXにする RX1PPS = 0x17; //RC7をRXにする RB0PPS = 0x0F; //RB0をTX2にする RX2PPS = 0x09; //RB1をRX2にする ODCONC = 0b00011000; // RC3,RC4をオープンドレイン SSP1CLKPPS = 0x13; // RC3をCLK入力に指定 RC3PPS = 0x13; // RC3をCLK出力に指定 SSP1DATPPS = 0x14; // RC4をDATに入力指定 RC4PPS = 0x14; // RC4をDAT出力に指定 PPSLOCK = 0x55; PPSLOCK = 0xAA; PPSLOCKbits.PPSLOCKED = 1; // SSP1設定 ----------------------------------------------- SSP1STAT = 0b10000000; // スルーレート制御はOff SSP1ADD = 1; // クロック設定 125k@1MHz SSP1CON1 = 0b00101000; // I2C Master modeにする SSP1CON2 = 0; SSP1CON3 = 8; // NOSC HFINTOSC; NDIV 1; OSCCON1 = 0x60; // CSWHOLD may proceed; SOSCPWR Low power; OSCCON3 = 0x00; // MFOEN disabled; LFOEN disabled; ADOEN disabled; SOSCEN enabled; EXTOEN disabled; HFOEN disabled; OSCEN = 0x08; // HFFRQ 1_MHz; OSCFRQ = 0x00; // MFOR not ready; OSCSTAT = 0x00; // TUN 0; OSCTUNE = 0x00; // ACTUD enabled; ACTEN disabled; ACTCON = 0x00; //__builtin_write_RTCWREN(); RC1STA = 0b10010000; // 非同期送信 9600baud TX1STA = 0b00100100; BAUD1CON = 0b00001000; SP1BRGH = 0; SP1BRG = 25; y = 0x22; mon = 0x9; wd = 0x0; d = 0x11; h = 0x21; m = 0x44; s = 0x10; setRTCC(); // Clear the RTCC interrupt flag PIR8bits.RTCCIF = 0; //Enable RTCC interrupt PIE8bits.RTCCIE = 1; PIR3bits.RC1IF = 0; PIE3bits.RC1IE = 1; INTCONbits.PEIE = 1 ; // 周辺装置割り込み有効 INTCONbits.GIE = 1 ; // 全割込み処理を許可する RA0 = 0; LCD_int(); // LCDを初期化 LCD_str(msgStart); // LCD上段に"LCD Disp Test" LCD_posyx(1,1); // 下段にカーソル移動 while(1){ } return; } //******************************************************************** // I2C 関連 //******************************************************************** //-------- ByteI2C送信 void i2cByteWrite(char addr, char cont, char data){ SSP1CON2bits.SEN = 1; // Start condition 開始 while(SSP1CON2bits.SEN); // Start condition 確認 i2cTxData(addr); // アドレス送信 i2cTxData(cont); // 制御コード送信 i2cTxData(data); // データ送信 SSP1CON2bits.PEN = 1; // Stop condition 開始 while(SSP1CON2bits.PEN); // Stop condition 確認 } //-------- Data送信 void i2cTxData(char data){ PIR3bits.SSP1IF = 0; // 終了フラグクリア SSP1BUF = data; // データセット while(!PIR3bits.SSP1IF); // 送信終了待ち } //******************************************************************** // LCD 関連 //******************************************************************** //-------- 1文字表示 void LCD_dat(char chr){ i2cByteWrite(0x7C, 0x40, chr); __delay_us(50); // 50μsec } //-------- コマンド出力 void LCD_cmd(char cmd){ i2cByteWrite(0x7C, 0x00, cmd); if(cmd & 0xFC) // 上位6ビットに1がある命令 __delay_us(50); // 50usec else __delay_ms(2); // 2msec ClearおよびHomeコマンド } //-------- 全消去 void LCD_clr(void){ LCD_cmd(0x01); //Clearコマンド出力 } //-------- カーソル位置指定 void LCD_posyx(char ypos, char xpos){ unsigned char pcode; switch(ypos & 0x03){ case 0: pcode=0x80;break; case 1: pcode=0xC0;break; } LCD_cmd(pcode += xpos); } //-------- 初期化 void LCD_int(void){ __delay_ms(100); LCD_cmd(0x38); // 8bit 2行 表示命令モード LCD_cmd(0x39); // 8bit 2行 拡張命令モード LCD_cmd(0x14); // OSC BIAS 設定1/5 // コントラスト設定 LCD_cmd(0x70 + (CONTRAST & 0x0F)); LCD_cmd(0x5C + (CONTRAST >> 4)); LCD_cmd(0x6B); // Ffollwer __delay_ms(100); __delay_ms(100); LCD_cmd(0x38); // 表示命令モード LCD_cmd(0x0C); // Display On LCD_cmd(0x01); // Clear Display } //-------- 文字列出力 void LCD_str(char *str){ while(*str) //文字列の終わり(00)まで継続 LCD_dat(*str++); //文字出力しポインタ+1 } //-------- Rom 文字列出力 void LCD_ROMstr(const char *str){ while(*str) //文字列の終わり(00)まで継続 LCD_dat(*str++); //文字出力しポインタ+1 }
I2C関連とLCD関連の関数群は参考サイトをそのまま使っています。コメントも親切に書かれているので特に説明の必要はないと思います。液晶関連の関数は、最後の方に並んでいますが、LCD_dat()が一文字出力。LCD_cmd()がコマンド出力。LCD_clr()が画面消去。LCD_posyx()がカーソル位置指定。LCD_init()が初期化。LCD_str()が文字列出力です。最後のLCD_ROMstr()のROM文字列出力は使っていません。それ以外では、putch()関数をLCD用に下記のように変更しています。
void putch(char data) { LCD_dat(data); // LCD への一文字表示関数 }
それ以外の処理で今回付け加えた部分は、割り込み関数内で、TWELITEからのシリアル通信を受ける部分で、
state = str[n-3] - 0x6F;
として、TWELITEの状態を判別するようにしています。TWELITEからは、起動したときに”setup"、タイムアップで強制スリープするときに"slees"、スリープから目覚めたときに"wakeuq"、時刻データを受信したときに時刻データの末尾に"r"を送信するようにしています。つまり送信データの最後が"p"なら起動、"q"ならスリープからの復帰、"r"なら時刻データを受信、"s"なら強制スリープ。というようになっています。それをASCIIコードで取得したデータがstr[n-3]に格納されているので、0x6Fを引いて、それぞれ1,2,3,4に変換し変数stateに格納しています。そして、その下の部分で、stateの値を判別してそれぞれの処理を行っています。
stateが1か2のとき、すなわち起動直後ないし、スリープから復帰したとき、まず、送られてきたシリアルメッセージをそのまま液晶ディスプレイの2行目左端から表示します。
LCD_posyx(1,1); printf("%s", str);
の部分がそうです。そして、
alm = s + 32; if (alm>=0x60){ alm = alm -0x60; }
として、現在時刻から20秒後の秒数を変数almに代入しています。現在の秒数が41秒より後の場合は、単純に20秒足すと、60秒を越えてしまいますので、0x60を引いて調整しています。なお、BCDコードで記述するため、20秒間は32となります。
stateが1の場合は、さらにsetupの回数を数えて、2行目の8列目から回数を表示します。これは、後で書きますが、やはり時々再起動がかかってしまうので、どのくらい再起動がかかったかを確認するために付け加えた部分です。
stateが3の場合は、正常に時刻データを受信していますので、時刻データをそれぞれの変数に格納して、PICのRTCCを更新しています。
また、正常受信の回数もカウントして、2行目の12列名から表示するようにしています。
else以下の部分はRTCC割り込み処理に関する部分です。
まず、RTCCの値を読み込んで、液晶ディスプレイの1行目1列目から時刻を表示します。
readRTCC(); LCD_posyx(0,0); printf("%s", str1);
次に、stateの値によって処理を分岐し、1の場合、つまり起動後の状態で、秒数が先ほど設定したalmの値になったら、つまり20秒間正常に時刻データを受信できなかったら、液晶ディスプレイの2行目1列目にタイムアップを意味する"TIME"を表示し、sendt()として、シリアルポートからTWELITEにASCIIコードの"t"を送信しています。これが、TWELITEへのスリープの指示です。
if (s == alm){ LCD_posyx(1,1); printf("TIME"); sendt(); }
stateが2の場合、つまり、スリープからの復帰の状態も同様に、20秒たったら、液晶ディスプレイの2行目1列目から"time"を表示し、ASCIIコードの"t"をシリアル送信しています。スリープからの復帰の場合は、"time"として、(再)起動からの時と区別するようにしています。また、sendt()を2回行っていますが、なぜだかわかりませんが、スリープからの復帰の場合、"t"を1回送っただけではTWELITE側でうまく受信できないことがあるため、2度送信しています。
if (s == alm){ LCD_posyx(1,1); printf("time"); sendt(); sendt(); }
また、割り込み処理ルーチン内の処理が長くなったことと、割り込み事由がRTCC割り込みとシリアル割り込みの2つになったことで、割り込み処理中にさらに割り込みが起こることを防ぐために、処理ルーチンの最初で全体割り込みを禁止し、処理ルーチンを抜ける前に全体割り込みを許可するようにしています。こうすることで、仮にシリアル割り込み処理中にRTCC割り込みが発生しても割り込みを受け付けず正常に処理が終了するようになっています。その分、割り込みがかち合った場合は時刻が更新されず1秒飛ぶことになりますが。
次に、TWELITE側のプログラムです。
// use twelite mwx c++ template library #include <TWELITE> #include <NWK_SIMPLE> #include <STG_STD> /*** Config part */ // application ID const uint32_t DEFAULT_APP_ID = 0x1234abcd; // channel const uint8_t DEFAULT_CHANNEL = 13; const uint8_t u32Baud = 115200; uint32_t OPT_BITS = 0; uint32_t sleeptime = 60 * 60 * 1000; /*** function prototype */ bool analyze_payload(packet_rx& rx); /*** application defs */ /*** setup procedure (run once at cold boot) */ void setup() { delay(1000); /*** SETUP section */ auto&& set = the_twelite.settings.use<STG_STD>(); auto&& nwk = the_twelite.network.use<NWK_SIMPLE>(); // settings: configure items set << SETTINGS::appname("PARENT"); set << SETTINGS::appid_default(DEFAULT_APP_ID); // set default appID set << SETTINGS::ch_default(DEFAULT_CHANNEL); // set default channel set << SETTINGS::lid_default(0x00); // set default LID set.reload(); // load from EEPROM. OPT_BITS = set.u32opt1(); // this value is not used in this example. // the twelite main class the_twelite << set // apply settings (appid, ch, power) << TWENET::rx_when_idle() // open receive circuit (if not set, it can't listen packts from others) ; // Register Network nwk << set; // apply settings (LID and retry) nwk << NWK_SIMPLE::logical_id(0x00) // set Logical ID. (0x00 means parent device) ; // configure hardware Serial.setup(64, 192); // start the peripheral with 115200bps. Serial.begin(9600); /*** BEGIN section */ the_twelite.begin(); // start twelite! /*** INIT message */ Serial << "\0" << "setup" << mwx::crlf; //Serial1 << "--- MONOSTICK_Parent act ---" << mwx::crlf; } void wakeup() { Serial << "\0" << "wakeq" << mwx::crlf; } /*** loop procedure (called every event) */ void loop() { while(Serial.available()) { int c = Serial.read(); switch(c) { case 't': Serial << "slees" << crlf; the_twelite.sleep(sleeptime); break; default: break; } } } void on_rx_packet(packet_rx& rx, bool_t &handled) { if(0) { // this part is disabed. serparser_attach pout; pout.begin(PARSER::ASCII, rx.get_psRxDataApp()->auData, rx.get_psRxDataApp()->u8Len, rx.get_psRxDataApp()->u8Len); Serial << "RAW PACKET -> "; pout >> Serial; Serial.flush(); } // output type2 (generate ASCII FORMAT) // :0DCC3881025A17000000008D000F424154310F0D2F01D200940100006B39 // *1*2*3*4------*5------*6*7--*8 if (1) { smplbuf_u8<256> buf; pack_bytes(buf , uint8_t(rx.get_addr_src_lid()) // *1:src addr (LID) , uint8_t(0xCC) // *2:cmd id (0xCC, fixed) , uint8_t(rx.get_psRxDataApp()->u8Seq) // *3:seqence number , uint32_t(rx.get_addr_src_long()) // *4:src addr (long) , uint32_t(rx.get_addr_dst()) // *5:dst addr , uint8_t(rx.get_lqi()) // *6:LQI , uint16_t(rx.get_length()) // *7:payload length , rx.get_payload() // *8:payload // , make_pair(rx.get_payload().begin() + 4, rx.get_payload().size() - 4) // note: if you want the part of payload, use make_pair(). ); serparser_attach pout; pout.begin(PARSER::ASCII, buf.begin(), buf.size(), buf.size()); //Serial << "ASCII FMT -> "; //pout >> Serial; //Serial.flush(); } // packet analyze analyze_payload(rx); } bool analyze_payload(packet_rx& rx) { bool b_handled = false; #if 1 // expand packet payload (shall match with sent packet data structure, see pack_bytes()) uint8_t fourchars[4]{}; // init all elements as default (0). auto&& np = expand_bytes(rx.get_payload().begin(), rx.get_payload().end() , fourchars ); #else // an example to pass std::pair<char*,int>. char fourchars[5]{}; auto&& np = expand_bytes( rx.get_payload().begin(), rx.get_payload().end() , make_pair((char *)fourchars, 4) ); #endif // if heading 4 bytes are not present, unexpected packet data. if (np == nullptr) return false; // display fourchars at first /* Serial << fourchars << format("(ID=%d/LQ=%d)", rx.get_addr_src_lid(), rx.get_lqi()) << "-> " << crlf; */ // Slp_Wk_and_Tx if (!b_handled && !strncmp((char*)fourchars, "TXSP", 4)) { b_handled = true; uint32_t tick_ms; uint16_t u16work_ct; np = expand_bytes(np, rx.get_payload().end() , tick_ms , u16work_ct ); if (np != nullptr) { Serial << format("Tick=%d WkCt=%d", tick_ms, u16work_ct); } else { Serial << ".. error .."; } } // AMB2 if (!b_handled && ( !strncmp((char*)fourchars, "RTCC", 4) // BMx280 ) ) { b_handled = true; uint8_t u8sec; uint8_t u8min; uint8_t u8hour; uint8_t u8day; uint8_t u8month; uint8_t u8year; uint8_t u8wday; uint16_t dummy = 0; np = expand_bytes(np, rx.get_payload().end() , u8sec , u8min , u8hour , u8day , u8month , u8year , u8wday ); if (np != nullptr) { Serial << "\0" << u8sec << u8min << u8hour << u8day << u8month << u8year << u8wday << "r" ; } else { Serial << ".. error .."; } } // Unknown if (!b_handled) Serial << "..not analyzed.."; // finally put line break. Serial << mwx::crlf; the_twelite.sleep(sleeptime); // returns status return b_handled; }
こちらの処理は、PICのプログラムの所で書いたように、TWELITE側から送るメッセージを、起動したときに"setup"、スリープから復帰したときに"wakeuq"とシリアル通信で送るようにしています。setup()の最後の所の、
/*** INIT message */ Serial << "\0" << "setup" << mwx::crlf;
と、その下にある、
void wakeup() { Serial << "\0" << "wakeq" << mwx::crlf; }
の部分がそうです。void wakeup()関数は、TWELITEがスリープから復帰したときに自動的に実行されます。実行後は、loop関数に移ります。
loop関数内では、シリアルからの受信をチェックし、シリアルから"t"が送られてきたら、シリアルに"slees"とこれからスリープするというメッセージを送信し、スリープを実行します。
while(Serial.available()) { int c = Serial.read(); //Serial << mwx::crlf << char(c) << ':'; switch(c) { case 't': Serial << "slees" << crlf; the_twelite.sleep(sleeptime); break; default: break; } }
時刻サーバーから時刻を受信したときの処理は、void on_rx_packet()に書かれています。ここの部分は特に変更していません。データを解析し、それをシリアルで送信しています。
ただ、PICのプログラムの所で説明したように、
Serial << "\0" << u8sec << u8min << u8hour << u8day << u8month << u8year << u8wday << "r" ;
として、末尾に"r"を加えています。
配線です。FritzingにはデフォルトでI2CLCDのパーツがなく、ネットで探してダウンロードしたものを使いました。RC3にCLK、RC4にDATを繋いでI2Cで接続します。
TWELITEとPICはシリアルで接続しています。
それでもやっぱりTWELITEが再起動する
TWELITEをloop関数内で待機させておくと再起動するということで、PIC側からコントロールしてTWELITEを強制的にスリープさせるようにしてみましたが、それでもやっぱり再起動がかかってしまいます。loop関数でシリアル受信を待つところがあるので結局は同じことになるのかもしれません。ただ、再起動がかかってもPIC側で再起動から20秒後にスリープ命令を出すのでスリープに入ることは確認できました。なので、これ以上は追及せずこの形で最終的に組み上げようと思っています。