磐船峡

ちょっと前の話なりますが、1/9の成人の日に磐船峡で開催されたオリエンテーリングの大会に参加しました。
磐船峡というとピンとこない人もいると思いますが、大阪府民の森のほしだ園地と言えばわかる人もいるのではないかと思います。
o-wonderforest.com

大阪と奈良の県境近くにあり、最近では星のブランコが有名です。
大会の方は、「昭和レトロ」と題された大会で、昭和らしくマスターマップやクレヨンチェックなどアナログ感満載の大会でした(オリエンテーリングを昭和にやったことのある人しかわからないと思いますが)。
自分的に満足のいく結果は出せませんでしたが、楽しく走れました。
朝、自宅を出て、バスで枚方市駅に行き、そこから京阪で私市に行って、歩いて行ったのですが、そのコースは生駒トレイルで使っているコースで、今年の生駒トレイルは昨日開催だったと思いますが、私はコロナ前の2020年に参加したのでなんか懐かしく感じてしまいました。
トレイルランで走る人も多いみたいで、私市駅でも数人のグループがこれから走るという感じでしたし、ほしだ園地でも数多くのランナーを見かけました。生駒トレイルの試走をされていたのかもしれませんね。
そんなことを考えているとまた走りたくなってしまいました。結構楽しく走れるコースなので、大会でなくても走りに行きたいなあと思っています。
さて、次の土曜日はいよいよ石舞台100です。なんか、大寒波がやってくるみたいで、願わくば寒波が過ぎ去って今日くらいの穏やかな天気になっていてほしいと思っています。レースのレポートはまた次回にでも書いてみたいと思います。

設計ミスで基板を再発注

PCBWayに注文していた基板が届いた。注文していたケースも届いて、さあ組み立てたら完成と思ったら、いろいろミスをしていたことが発覚。

ケースの型番が違う

まず、基板がケースに入らない。どうやら、ケースを注文したときに型番を間違えたらしい。本当は、TakachiのLC-145を注文しないといけなかったのに、実際に注文していたのはLC-135という一つ小さい型番のケースだった。そりゃ入らないはずだ。

プログラムもできてなかった

そして、プログラムも実は完成していないことに気づいた。できたと思いこんでいたが、この間作ったのはI2C液晶を動かすプログラムで、セグメント液晶を動かすプログラムはまだ完成していなかった。そこで、慌てて、I2C液晶用のプログラムに、以前のセグメント液晶を動かすプログラムを合体させて作った。そして動かしてみると、時刻の表示がおかしい。ここで最初のミスに気付く。

配線のミスが発覚

液晶ディスプレイの15番ピンにPICの4番ピン(RA2/SEG2)を繋がないといけなかったのに、よーく回路図を見直してみると5番ピン(RA3/SEG3)につながっている。まあ、仕方がない、このくらいならプログラムの方を修正すれば何とかなるということでプログラム側を修正した。ところがまだ表示がおかしい。コロンを表示させるつもりが、2桁目(時刻の1の位)のセグメントAを表示させるようになっていた。ここで、また回路図を見直したら、時と分の間のコロンのつもりで、分と秒の間のコロンに接続していた。コロンは2つあるのだが、PICのピン数の関係でどちらか一方しか使えないために、どちらかを選ばないといけなかった。見栄え的に時と分の間のコロンがないと間抜けな感じになるので、そっちに繋ぐつもりが間違えて分と秒の間のコロンにつながっていた。これはプログラムでは修正がきかない大きなミス。まあ、何とかする方法はあるのだが、あまりやりたくないがパターンカットをして、線でつないでやることでリカバリは可能だ。

液晶の向きが違うし、ねじ穴も小さい

しかし、ケースと基板をよくよく見ていると、液晶ディスプレイの向きがケースを縦に置いたときにちょうど読めるようにな向きになっている。ふつうはどう考えても安定性から言って横置きにするでしょ。もう、何というか情けなくなってきた。
そして、更に、基板のねじ穴も思ったよりも小さく、もう一回り大きくした方が良いということも分かった。まあ、ねじは小さなねじを買ってきて付ければ何とかならないわけでもないが。

結局基板を再設計して、再発注

ということで、最初はケースだけ買いなおせばよいかと思っていたが、こんなに基板でミスをしていたのでは、基板を発注しなおした方が良いという結論になった。幸いPCBwayなら発注してから到着まで1週間ほどなので、基板の方を発注しなおすことにした。問題は、春節がもうすぐやってくるので春節の休みにかかってしまうと到着までさらに時間がかかるということ。PCBwayのHPを見ると、まだ休みまではしばらくありそうなので制作まではできそうだった。
そこで、大急ぎで配線とプリントパターンをやり直した。

配線をやり直した回路図

配線は難なく変更できたが、プリントパターンはケースをLC-135にすることにすると、もう少しコンパクトにしなけらばいけない。しかし、基板の外形をLC-135に入るように小さくしてしまうと全部の部品が入りきらない。
だが、よくよく考えると液晶は裏面に実装するのでその部分の表面は空いている。そこで、その部分にPICを持ってくることで何とか全部の部品を配置することができた。そして、今度は液晶の向きをケースを横置きにして使用するようにした。また、ねじ穴も一回り大きくした。
さあ、これで大丈夫なはずだが、でも何となく不安。とにかくやるしかない。

やり直した配線パターン。液晶ディスプレイの向きを90°回転し、液晶の反対面にPICが来るように配置。

PCBWAYで基板発注

前回までで疑似電波時計のハード、ソフトのあらましは完成したので、いよいよ本番の制作に入ります。
alasixosaka.hatenablog.com
今回はセグメントLCDをディスプレイに使用するので配線数が多く、ユニバーサル基板では半田付け作業が大変になるので基板を起こして業者に発注することにしました。最近は格安で基板を製作してくれるところがいくつかあるので結構お手軽に頼めるようになりました。昔は、銅を一面に張り付けた基板にマジックなどでマスキングをして、塩化第二鉄でエッチングしてととても作業が大変だったのでユニバーサル基板の方がはるかにお手軽だったのですが、今は逆に複雑な回路は業者に発注してしまった方がはるかに楽です。しかも値段もそれほど高くない。
今回は、自分で回路図を書いて業者に発注するまでのお話です。ただ、細かい手順などは参考サイトを見ていただくとして、ここでは大まかな流れだけを書いておきます。

回路図とプリントパターンの作成

これをやらないと業者にも発注ができません。今回はKiCADというフリーソフトを使いました。プロ用にはEAGLEというCADが有名ですが、個人で趣味程度の基板制作であればKiCADで十分問題ありません。
KiCADの最新版はVer6です。公式サイトからダウンロードしてインストールします。
www.kicad.org
KiCADの使い方はこちらのサイトを参考にしました。
www.jh4vaj.com
ただ、このサイトはある程度KiCADの操作に慣れている人向けに書かれているので、もっと詳しくは、古いバージョンになりますが、こちらのサイトが参考になります。
spiceman.jp
こちらのサイトでは、KiCADの基本的な使い方から基板発注のやり方まで詳しく書かれています。私も非常に参考になりました。
今回作成した回路図はこちらになります。

今までFrizingで書いていたブレッドボード上の回路と大きく変わっていません。ポイントは、TWELITEとのシリアル通信用のポートをRB6、RB7とし、PICのプログラミング用端子と兼用にしたことです。これは、セグメント液晶の駆動用にほとんどのポートを使ってしまい空きポートがないためにこうしました。基本的にはあらかじめプログラムしたPICを基板に差し込む予定なのでプログラム用の端子は必要ないと思われるのですが、プログラムにバグがあったりしたときに簡単に修正できるようにオンサーキットでプログラムできるように配線をしておきました。プログラミング時と通常使用時の変更はジャンパーピンで行うことにしています。また、TWELITEのリセット端子にスイッチを付けてリセットできるようにしました。こちらも、念のためという感じですが、TWELITEは基本電源ONでリセットがかかるのですが、ブレッドボードで試したところタイミングの問題かTWELITEを強制的にリセットしてやらないとスタートしないことが何度かあったのでリセットスイッチを付けています。また、PIC側にも同様にリセットスイッチを付けました。PICのリセットは、MCLR端子をGNDに落とすことで行います。

基板サイズを決めてプリントパターンを作成する
回路図ができたら、回路図からプリントパターンを作成します。ただし、その前にプリント基板のサイズを決める必要があります。
今回は、Takachiのケースを使うことにして、ケースに合うサイズにプリント基板を作成することにしました。別に普通のプラケースに自分で穴をあけてやってもいいのですが、今回選んだTakachiのケースは電池ボックスが内蔵されているので電池駆動には何かと便利です。また、基板取付用のボスがあらかじめついていますのでこれを利用するために基板サイズを決定しました。使用するケースは、LC145H-M2というケースで、シリコンカバー付きのプラスチックケースです。基板サイズは86×77.5mmとなります。
https://www.takachi-el.co.jp/products/LCS

ちょっと手狭ですが、なんとか全部の部品を配置することができました。
部品を配置したら、配線をする必要があります。KiCADには自動配線機能がありませんので、自分で配線するか、他のソフトで自動配線をするかのどちらかになります。今回は配線が多いので、Freeroutingというフリーソフトを使いました。使い方は、先の参考サイトに出ています。
spiceman.jp
ここでやらかしてしまったことに気づきました。実は、液晶ディスプレイは他の部品と反対の面に実装する予定だったのですが、回路図を書いたときはそのことをすっかり忘れてしまって、同じ面に実装したときに配線しやすいように書いてしまっていました。裏面に実装するので考えていたのとピン配置が裏返ってしまうのでせっかく綺麗に配線できるように回路図を書いたにもかかわらず汚い配線になってしまいました。回路図を引き直すことも考えたのですが、液晶に数字を表示させる配列を全部変更してしまう必要があるので面倒なのでそのまま作ってしまうことにしました。なので、液晶の周りの配線はちょっとみっともないことになっています。プリントパターンはこんな感じになりました。見やすいようにベタ塗りの部分は塗りつぶさずに表示しています。

出来上がったプリントパターン

ガーバーファイルを作成して業者に発注する

パターンができたらあとはガーバーファイルとドリルファイルを作成して発注します。PCBWAYへの発注はこちらのサイトを参考にしました。
tmegane.hatenablog.com
参考サイトにも書いてありますが、PCBWAYに発注するときは、「拡張X2属性を含む」というところのチェックを外しておく必要があります。それさえ注意しておけばあまり問題はないかと思います。
基板発注に関してはこちらのサイトも参考にしました。
xn--p8jqu4215bemxd.com
こちらのサイトでPCBWAYが一番のおすすめということでしたので今回はPCBWAYに頼みました。初回ということで$5オフのクーポンが使えたので基板制作料は実質的にタダになりました。あとは送料ですが、配送業者を色々選択できるようになっています。デフォルトはDHLとなっていますが$19とお高いので、もう少し安いところに頼みました。$10くらいです。もっと安いところもあったのですが、コメント欄にトラブルが生じることもあるとか書いてあってちょっと心配になったので時間はかかるけど比較的信頼できそうなところにしました。DHLとかだとすぐに着くみたいです。急ぐときにはいいかもしれません。
状況を確認すると1/4に発注したのですが、1/8の時点でもう出荷されていました。とりあえず春節前に基板の製造は終わったようです。あとは到着を待つのみです。

PICからのシリアル送信でTWELITEをスリープさせる

疑似電波時計の制作もようやく胸突き八丁を越えたという感じのところまでやってきました。不慣れな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秒後にスリープ命令を出すのでスリープに入ることは確認できました。なので、これ以上は追及せずこの形で最終的に組み上げようと思っています。

TWELITEとPICでシリアル通信する

電子工作の疑似電波時計制作の続きです。
前回は、TWELITEの消費電流を測ってみました。
alasixosaka.hatenablog.com

そして、DOZEモードに入らず、スリープしない限り結構な電力を消費していることがわかってしまいました。
そこで、TWELITEが時刻サーバーからのデータの受信に失敗したとき(時刻サーバーが何らかの理由で止まってしまった場合など)にPIC側から命令を出してTWELITEにスリープするように指示することを考えました。まあ、いずれにしても時刻を受信したらそれをPIC側に送ってやる必要があって、それもシリアル通信になるので、TWELITEとPICはシリアルで通信する必要があります。ただ、以前の構想では、TWELITEからPIC側に時刻データを送るだけでよかったのですが、PIC側から指示を出す必要があるので双方向に通信する必要があります。そして、PICは通常はスリープしている(タイミングによっては起きている場合もある)ので、シリアル通信でスリープから起こしてやる必要があります。なので、今回は、TWELITEからシリアルデータを送信してスリープ中のPICを起こしてデータをうまく受信できるのかというのをやってみました。

TWELITEのSerial1が使えない

TWELITEにはシリアルポートが2つ搭載されいて、TWELITEのMWXライブラリではSerialという名称で使われるUART0とSerial1という名称で使われるUART1があり、MWXライブラリでは通常はSerialの方はセットアップが自動で行われ、Srrial1の方は、プログラムで明示的にセットアップをする必要があるようです。
Serial1を使ったサンプルアクトは、Unit_UART1というフォルダに入っています。これを参考に、もともとのTWELITE親機のプログラムにSerial1を使う処理を追加して子機からの送信をモニタリングするのを通常のSerialで行い、PICとの通信をSerial1で行おうとしました。Serial1を使うには、Setup関数内で、

// initialize the object. (allocate Tx/Rx buffer, and etc..)
Serial1.setup(64, 192);

// start the peripheral with 115200bps.
Serial1.begin(115200);

のように宣言すればよい。
以前書いたように、PICをクロック1MHzで動かすとシリアル通信で115200bpsで通信することができないので、速度の部分を9600bpsに書き換えてPICとの通信を試みました。
結論から言うと、115200bpsでも9600bpsでも通信することができませんでした。これはPIC側の問題ではなく、TWELITE側の問題です。私が使っているチップが古いためなのかもしれませんが、UART1の動作は確認できませんでした。サンプルアクトのUnit_UART1を動かして、USBシリアル変換器と繋いでみましたがそれでも全く通信ができず、Serial1を使うことは諦めることにしました。
そうすると、Serialの通信速度をどうするかという問題にぶち当たります。Serialの設定を変えて9600bpsで動かすのか、PICのクロックを4MHzくらいに上げて115200bpsで通信するのかという選択になります。できればPIC側は低いクロックで動かしたいのでTWELITE側で設定を変えて9600bpsを試してみました。
上に書いたSetup関数内の設定のSerial1を下のようにSerialに変更することで、問題なく動作することがわかりました。

// initialize the object. (allocate Tx/Rx buffer, and etc..)
Serial.setup(64, 192);

// start the peripheral with 9600bps.
Serial.begin(9600);

TWELITEをプログラムするときに、TWELITERとの通信は115200bpsで行われるので、ここの設定でSerialを9600bpsに変えてしまうと、次からプログラムができないのではないかと心配したのですが、TWELITERを繋いでプログラムを書き込むときには問題なく115200bpsで通信して、プログラムが書き込み終えた段階でTWELITEのシリアルポートは9600bpsで動くようになるようです。

スリープ中のPICをTWELITEで起こす

配線は次のようにします。TWELITEとPICをシリアルでつないで、PICはさらにFT232RLとも繋いでいます。PICはシリアルポートが2つあるので、その一つをTWELITEとの通信用に、もう一つはPCに繋いで通信の様子をモニタします。

PICをスリープ状態でシリアル通信で割り込みにより起動するという例があまりなかったのですが、こちらのサイトを参考にしました。
ameblo.jp

PIC側のプログラムです。コンフィクレーションの設定は省略しています。PPSを使ってシリアルポートのTX1,RX1をそれぞれ、RC6,RC7に割り当て、TX2,RX2をそれぞれRB0,RB1に割り当てています。TWELITEからは時刻データが送られてきますので、データは順番に配列のstr1[]に格納されます。データに含まれる”CR"(ASCIIコードの13)を検出して、その一つ前が曜日、その前が年、というように順番にそれぞれの変数に値を代入します。それをprintf関数を使ってシリアルポート2に出力しています。出力が終わったらスリープします。シリアルデータが送られてきたらスリープからウェイクアップして割り込み処理により受信を行います。
スリープからの起動でシリアル通信するとはじめの何ビットかはデータの取りこぼしが発生します。データシートによれば13bitの0をまず送信することを推奨しています。ですので、データの始めから秒、分、時というようにせず、データのお尻から変数に取り込んでいます。また、PICは常にスリープ状態にあるわけではないので、時計の時刻を更新するタイミングでは起きています。その場合にシリアル受信をすると、ダミーデータをきっちり読み込んでしまいますので、データ長が一定せず、頭から読んだときにうまく時刻データがずれてしまうという問題もあるので、お尻から読むようにしています。
また、スリープする前に、BAUD1CONbits.WUE = 1; という1行を入れています。これは、スリープからシリアル通信で起動するときのお作法のようなもので、参考にしたブログにも書いてありました。でも、プログラムを書く段になってすっかり忘れてしまって初めはこの文を入れずにうまくいかないと思って悩んでしまいました。

#include <xc.h>
#include <PIC16f19155.h>
#include <stdio.h>
#include <string.h>

#define _XTAL_FREQ 1000000
char RxData;
char Rflg;
int x;

void __interrupt() isr(void)
{
    PIR3bits.RC1IF = 0;
    if((RC1STAbits.OERR)||(RC1STAbits.FERR)){
        RC1STA = 0;
        RC1STA = 0x90;
    }
    else{
        RxData = RC1REG;
        Rflg = 1;
    }
}

void putch(unsigned char ch) {
    while (!PIR3bits.TX2IF); 
                      //送信終了待ち
                             
    TX2REG = ch;
}


void main(void) {
    char num = 10;
    
    ANSELA = 0b00000000;  
    ANSELB = 0b00000000;
    ANSELC = 0b00000000;
    TRISA  = 0b00001000;  
    TRISB = 0x00;
    TRISC  = 0b10000000;
    
    PPSLOCK = 0x55;
    PPSLOCK = 0xAA;
    PPSLOCKbits.PPSLOCKED = 0;
    RC6PPS = 0x0D;     //RC6をTXにする
    RX1PPS = 0x17;     //RC7をRXにする
    RB0PPS = 0x0F;     //RB0をTX2にする
    RX2PPS = 0x09;     //RB1をRX2にする
    PPSLOCK = 0x55;
    PPSLOCK = 0xAA;
    PPSLOCKbits.PPSLOCKED = 1;
    
     // 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;
    
    RC2STA   = 0b10010000;      // 非同期送信 9600baud
    TX2STA   = 0b00100100;
    BAUD2CON = 0b00001000;
    SP2BRGH  = 0;
    SP2BRG   = 25;
    
    PIR3bits.RC1IF = 0;
    PIE3bits.RC1IE = 1;
    INTCONbits.PEIE   = 1 ;             // 周辺装置割り込み有効
    INTCONbits.GIE    = 1 ;             // 全割込み処理を許可する
    
    while(1){
        
        Rflg = 0;
        char str1[30]="";
        x = 0;
        while(1){
            if (Rflg==1){
                Rflg=0;
                RA0 = 0;
                str1[x]=RxData;
                x++;
                if (RxData ==13){                 
                    break;
                }
            }
        }
        int i = 0;
        while(1){
            if (str1[i] == 13){
                break;
            }else{
                i = i + 1;
            }
        }
        char wday = str1[i-1];
        char year = str1[i-2];
        char month = str1[i-3];
        char day = str1[i-4];
        printf("%x/%x/%x/%x",year,month,day,wday);
        printf("\n\r");
        char hour = str1[i-5];
        char min = str1[i-6];
        char sec = str1[i-7];
        printf("%x:%x:%x",hour,min,sec);
        printf("\n\r");
        __delay_ms(10);
        //printf("%s\r\n",str1);    //受信したデータをそのまま返す
        //printf("go to sleep");
        BAUD1CONbits.WUE = 1;
        SLEEP();
        NOP();
    }
    return;
}

TWELITE側のプログラムです。基本は前回と変わっていません。変更点だけを下記します。
時刻を無線で受信した後、シリアルで送信するところをまず、Serial << "\0"としてダミーデータを送っています。これは参考サイトのやり方そのままですがこれでうまくいきました。また、最後のcrlfをコメントアウトしていますが、これはループから抜ける時にcrlfを送信しているので、重複を避けるためにコメントアウトしました。

Serial  << "\0"
<< u8sec
<< u8min
<< u8hour
<< u8day
<< u8month
<< u8year
<< u8wday
//<< crlf
;

これで、TWELITEからのシリアル出力で、スリープ状態のPICに時刻データを送信できることを確認しました。
今度は、逆にPICからのシリアル送信でTWELITEをスリープさせることをやってみます。

今年の走り初めは音羽山

あけましておめでとうございます。
今年は2023年、兎年。ということはついに還暦を迎えるということになります。
このブログを始めたときは還暦までにトレランで100km完走を目標にしていましたが、全く届いていません。今日は1月3日ですが、一年の計は元旦にありと申します。今年こそ100kmと言いたいところですが、無理なのは見え見えなので、まずは50km以上を目指したいと思います。ウォークでは昨年60kmを達成したので、今年は100kmにチャレンジできればと思っています。
さて、今年の走り初めは音羽山に登ってきました。
年末年始は帰省していました。自分の実家の奈良(といってもほぼ三重みたいなところですが)から、家内の実家の滋賀を回って、大阪に帰ってきました。滋賀から帰る途中に音羽山に登ってきました。
ちなみに、今回は初めて電車で帰省したのですが、奈良から滋賀に向かうのに、近鉄伊賀鉄道、JRを乗り継いでみました。伊賀鉄道はかつては近鉄の路線だったのですが、近鉄が手放して現在は伊賀鉄道として運営されています。ここの路線は近鉄時代を含めて初めて乗りました。こんな感じのラッピング電車が走っていました。伊賀鉄道は忍者押しで結構頑張っています。

伊賀鉄道。忍者の顔
伊賀鉄道の側面。ねこのキャラクター

さて、音羽山の話に戻ります。
石山から京阪に乗り、石山寺で下車。昔、東海自然歩道を走った時に大津京から、逢坂山、音羽山を越えて、石山寺に出たことがありますが、今回はその逆コースです。
alasixosaka.hatenablog.com
石山寺駅から音羽山、逢坂山を越えて、大津駅を目指します。
石山寺は結構有名なお寺なので、もっと人が多いのかと思いきや、京阪はガラガラで、参拝する人もほとんどいなさそうな感じ。まあ、お寺なので神社と違って正月は人が少ないのかな。
とりあえず、駅を降りて、お寺の方に向かわず、東海自然歩道音羽山に向かう。

スタート地点の京阪石山寺駅。降りた人は数人。

石山高校の脇を抜けて、しばらく住宅地の中を進む。2kmほど進んだところからようやくトレイルスタート。冬のせいかトイレが近い。駅でも済ませておいたが、この先はトイレがないと思われるので、トレイルを少し進んだところにあるトイレで済ませておく。トイレを過ぎたあたりから本格的なのぼりが始まる。
全体の距離は10kmほどだが、標高差は500m近くあって結構登りごたえがある。正月早々ということからか人を全然見かけない。途中で降りてくる女性とすれ違っただけだった。
5km過ぎたところでようやくパノラマ台に到着。

パノラマ台に到着
パノラマ台から琵琶湖を眺める

ここでいったん下りになるが、音羽山へは再び上る。このあたりでようやく何人かとすれ違う。
しばらくゆるい尾根道が続いたが、山頂手前はまた急なのぼり。上り詰めたところで音羽山山頂に到着。

音羽山山頂。人影もなく静か
音羽山山頂からの眺め。京都市方面

ここで、コンビニで買ったおにぎりを2つ食べる。距離が短いこともあって結局この日はおにぎりを2つ食べただけだった。まあ、おにぎりにせずとも、ゴールするだけならようかんやジェル程度の行動食で済ませることもできたが、自分の場合は、補給を取るのがあまり上手じゃないので、補給の訓練を兼ねてある程度走ったらちゃんと補給というのをやっている。
ここから、逢坂山方面に向かう。また、トイレに行きたくなった。東海自然歩道の標識に、トイレが少し先にあると書いてあったのでそこで済ませようと思っていたが、いつの間にか通り過ぎていた。トイレはどこにあったのだろうか?
この先はゴールするまでトイレはなさそうなのでゴールするまで我慢することにする。
国道1号線を渡って、逢坂山に登る。

国道1号線を渡るところ

逢坂山をおりるところで、東海自然歩道と別れて、JR大津駅方面に向かう。12時頃大津駅に到着。だいたい2時間くらい。もう少しかかるかと思ったがまずまずのペースで走れた。年末に比叡山に登って重いトレーニングをしたので、少し軽めのトレーニングということで10kmにしたが、結構登りごたえがありまずまずのトレーニングができたと思っている。

京都一周トレイル 東山コースを走ってきました

昨日(12/29)、京都一周トレイルの東山コース(伏見稲荷比叡山)を走ってきました。
本当は大原まで走りたかったのですが、前日にYamapで活動記録を見ると山頂付近は結構雪が積もっているとのこと。
そこで、そもそも行こうかどうしようか悩んだのですが、一応バックアッププランとして、生駒のトレランのコースを一部変更して、生駒山上を通るルートで走ろうかとも思ったんですが、実は来年1月の石舞台100にエントリーしていて、勿論一番ぬるい33㎞のコースを走るんですが、それでも登りが2400mもあるということで、トレーニングをしておかねばならないという事情があり、生駒のコースでは登りが1000mほどにしかならない。まあ、石舞台のレース当日も雪がある可能性もあるしと思い、コースを短縮して比叡山まで登って帰りはケーブルカーで帰るということにしようかと考えた。比叡山から先は尾根上のトレイルを走って、大原の里に下るだけなので、登りの練習としては比叡山まででやめてもまあ良いかなという感じ。それで登りがだいたい2000mくらいになる。
今回は、冬の最中で、しかも比叡山の山頂まで行くということで、結構厚着をして出かけた。
アンダーは、普段はメリノウールのアンダーを使っているが、今回は自転車用のエステル系の発熱素材を使ったものを使ってみた。これは、この間の柳谷観音に登ったときに使って結構暖かかったので、一度トレランでも使ってみようかと思って使ってみた。エステルなので吸湿性がなく、汗冷えをあまりしないというのも良い点。使ってみてちょっと気になったのは自転車用で裾が少し短いので、走っているうちにお腹や背中が出てくるということ。それ以外はメリノのシャツよりも良いと思った。
ミッドレイヤーはいつものようにファイントラックのフロウラップ。アウターはちょっと厚めのモンベルのプリマプラスをチョイス。ズボンもモンベルの厚手のノマドパンツ。タイツはちょっと苦手なので厚手のズボンだけで行くことにした。靴はアルトラのモンブラン。靴下はファイントラックのドライレイヤー5本指に、メリノの靴下を重ね履き。これは最近は冬によくやるやり方で、5本指のドライレイヤーを履いておくとあまり蒸れないし、メリノ厚手をその上から履くと寒くない。それに、防寒具としてマイクロフリースのシャツとウィンドブレーカーのズボンをザックに入れておいた(結局使わなかったけれど)。
それと、念のため軽アイゼンを持参。ライトとエマージェンシーシートも念のため持参。行動食として、いつものようにおにぎり2個。羊羹、ナッツ、柿の種、エナジージェルなど。そして冬場で寒いということで、カップラーメンリフィルを手作りのジップロックジーにいれて、山専サーモスにお湯を入れて持参した。いろいろ装備を用意するとかなりの分量になってしまった。給水は今回もハイドレーションにした。電車で行くときはハイドレーションの方が目立たなくてよいし、前ポケットも使えるのでハイドレーションにすることが多い。ハイドレーションの中は水にクエン酸を溶かしたもの。スポーツドリンクなど甘い系の飲み物が苦手なので、以前は塩梅水を使っていたが、それでも後半になってくると甘味が気になってくるので、クエン酸にしてみたらいい感じだったので最近はロングの時はもっぱらクエン酸を愛用している。スポーツ用のドリンクでクエン酸だけを使っている人はあまりいないと思うけど、このクエン酸はもともとはアレルギーの治療のために医者から毎晩飲みなさいということでもらったもので、それをスポーツ用にも使ってみたらいい感じだったという副作用のような感じで生まれた。シンプルだし、洗うのも簡単なので重宝している。
ザックは、レイドライトのResponsive24。これは持っているトレラン用ザックで最も大容量で、上記の装備もちゃんと入った。ベルトのサイズがちょっと小さめ(もうワンサイズ大きいのにすればよかったと思っている)なのが難点だが。ところで、このザック、ハイドレーションを吊り下げるマジックテープがついているが、チューブを取り出す口がないとばっかり思っていた。そのため、いつもメイン気室のチャックを少し開けてそこからチューブを取り出していたのだが、前回、東海自然歩道を走った時に、チャックが開いて中のものが落ちるというトラブルがあり、帰ってからよく調べると、ちゃんとチューブを取り出す口が開いていることに気づいた。一見すると口が無いように思えてしまう構造になっているのだが、肩ひもの内側をちょっとめくるとチューブを通す口が現れる構造になっていて、ちょっと見ただけでは気づかない構造になっている。もう3年くらい使っているが初めて気づいた。

朝、京都駅で乗り換えて奈良線稲荷駅で下車。奈良線の列車は空いていて、稲荷で降りる人もあまりいなかったが、伏見稲荷の前の道路は渋滞していて、年末だというのに結構な人がやってきている。

稲荷の駅を降りると、いきなり大鳥居がお出迎え

京都一周トレイルは前から一度走ってみたいと思っていたのですが、スタートの伏見稲荷はコロナの前はいつも大にぎわいで、行くなら今のうちかと思って出かけてみましたが、それでも予想以上に人がいてここを走っていくのはちょっと迷惑になりそうと思って、最初は歩いて進むことにした、神域というのもあるし。しかし、伏見稲荷さんは太っ腹ですね。普通はトレイルコースに指定されてしまうと、ランナーが走って通るので神社は神域を穢されるということで嫌がるところが多いのだが。
まずは、伏見稲荷大社の本殿で本日の安全祈願。そこから、有名な千本鳥居に入っていく。

有名な千本鳥居。よくインスタなんかでも使われる

伏見稲荷は子供の頃に来た記憶があるのと、たしか、学生時代に留学生を連れて来たことがあるが、それ以来になるので、もう30年以上来ていないことになる。景色は昔と変わっていない。
京都一周トレイルのコースは、もろに千本鳥居の中を通って行って、途中で稲荷山の山頂へのルートと別れる。そこまで来るとようやく人が少なくなって走ることができるようになった。
そしてしばらく行くとようやく京都一周トレイルのポストを発見。このポストは写真ではちょっとわかりづらいが、小さいけどポストの上部にコースの概略が書いてあって工夫されていてわかりやすい。

この日初めて見る京都一周トレイルのポスト

ここまでは、あらかじめ自作の地図アプリに入れておいたコースを見ながら進んできた。東海自然歩道でもあるあるなのですが、有名な施設の中や周辺ではこういう道案内が無かったりする。なので、事前に調べておくということは重要。京都一周トレイルは地図も販売されていて、高槻の本屋でも手に入る。今回は念のため紙の地図も持参した。トイレの場所や、ややこしいポイントのところは拡大図なんかがあっていざというときには役立つと思って持って行った。結局使わなかったが。
稲荷山を下りると、宮内庁管理の看板があるところに出た。皇族の方のお墓らしい。あとで、地図を見ると後堀川天皇孝明天皇など幕末期の天皇のお墓もこの辺りにある。

皇族のお墓? 宮内庁管理。奈良にもこの手のお墓はいっぱいある

そして、その先に泉涌寺泉涌寺といえば、歴代天皇の菩提を弔う寺として有名。本当に観光コースになっている。トレランで走るだけでなくハイキングでも十分楽しめる。

泉涌寺の山門

泉涌寺を過ぎると一旦今熊野の街中に降りてくる。この辺りは京都女子大が近く、たしか学生の頃に一緒に活動していた京女の学生でこの辺りに下宿していた人が結構いた。本当に意外だったのが、結構な割合で下宿生がいるということだった。京大とか全国から学生が集まってくる大学は下宿生が多いというのはわかるのだが、都会の女子大って偏見かも知れないけど地元のお嬢様が通う学校だと思っていた。
今熊野の街中を抜けると再び山に入る。

トレイルにあった看板。京女鳥部の森。京都女子大と関係あるのかな?

ようやくトレイルらしい道が始まった。

ここまではほぼ舗装道路。ようやくトレイルという感じ。

六条山からまたすぐに舗装路に出て、一号線の方に向かう。ここで前を走っている女性のトレイルランナーを発見。今日見かけた始めてのトレイルランナー。よく道を知っているのか、このややこしいところを立ち止まることもなくスイスイと進んでいく。国道一号線を渡るのに、一旦一号線沿いに出て、しばらく下って、トンネルをくぐる。トンネルをくぐってまた一号線沿いに進んでそこから山に入る。今度は清水山に登る。このあたりはいわゆる東山三十六峰。清水山の登りで後ろから来たトレイルランナーに抜かれる。半ズボンで気合の入った格好。しかもかなりのペース。この人は相当早そう打と思った。
ここの登りは結構きつかった。とは言え、まだまだ大文字山もあるし、その先には今日一番の比叡山も控えている。こんなところで疲れているわけにはいかない。

清水山山頂付近

一旦下って、すぐに上り返して、東山公園の展望台に出る。そろそろトイレに行きたいと思っていたところで、丁度トイレがあったので済ませておく。

東山公園の展望台

東屋の先はヘリポートになっていて立ち入り禁止になっているのだが、入って写真を撮っている人がいる。まあ、気持ちはわからなくもないが、やっぱり立ち入り禁止と書いてあったら入らないの常識というものだと思う。最近は自分の都合ばかり考えてこういう行為をする人が目立つように思う。困ったものだ。
尾根を北上して、粟田口の方へ降りていく。降りていく途中で、展望が開けるところがあって、見るとちっちゃな観覧車があった。後で調べると動物園の観覧車とのこと。昔はなかったな。

粟田口の先にちっちゃな観覧車を発見

粟田口への下り口は本来は粟田神社の方へ降りていくことになっていたが、崩落かなんかあったみたいでう回路を通るようになっていた。粟田神社の参道を通って三条通に出る。しばらく三条通を東に進む。旧都ホテル、現ウェスティンホテルの前を通ると、カラスがいっぱいいる。カラスもおいしい食べ物があるところを知っているのだろう。
三条通を渡って、蹴上の疎水のところに出る。有名なレンガ造りのトンネルをくぐる。ここの煉瓦はらせん状にねじれている。強度を持たせるためにこういう構造になっているそうな。高槻にもJR線下のトンネルに同じようなものがいくつかある。

琵琶湖疎水の有名なレンガ造りのトンネル。

トンネルをくぐって坂を登ると疎水沿いに出る。

琵琶湖疎水。向こうが京都市
琵琶湖疎水。向こうが琵琶湖側

琵琶湖疎水は、滋賀と京都の間の水運にかつては用いられていた。琵琶湖の湖面と京都市の市街地の高さがほとんど差がないことを利用して、トンネルを掘り、琵琶湖の水を京都市内に引いて、船で物資を運んでいた。そして、蹴上のところと、京都市内の高さの差を利用して、同時に発電を行っていた。発電でできた電気は、京都の市電を動かすのにつかわれていた。京都は全国で初めて市電が走った町である。この蹴上には発電のための段差があったので、船はインクラインというケーブルカーのようなものを利用して上げ下げしていた。いまもインクラインの線路や船が残されている。

かつて水運に使われた船とインクライン

この辺りはかつては京阪の路面電車が走っていたが、地下化されて、蹴上の駅は今は地下にある。そして、この区間京都市地下鉄東西線と共用で使われている。蹴上の駅の上を通って、日向神社の参道を通って大文字山に登る。大文字山は学生の頃さんざん上ったが、ここから登るのは初めてだ。始めのうちは傾斜がそれほどでもなく、走れることは走れる。これくらいの走ろうか歩こうか迷うくらいの傾斜が走ってしまうと実は結構きつい。今日は日没までに山頂にたどり着ければよいが、レースでは制限時間があるのでこんなところでゆっくり登っていられないのでとりあえず走って登る。いくつもの小さなピークを越えていくので意外にアップダウンがある。アップダウンを繰り返しながら登っていくという感じ。どうせ上るのだから本当は降りたくないのだけれど。山頂が近づくと傾斜が急になって歩くしかなくなった。今日は荷物が多いのと、比叡山は雪ということで、ポールを持ってこなかったが、持ってくればよかったと後悔した。
山頂のすぐ下で鹿ケ谷へ降りる道との分岐に出る。本来のトレイルコースはここから下りていくルートだが、せっかくなので、大文字山の山頂へ寄り道する。

大文字山頂直下の鹿ケ谷への分岐。

ちょっと登ると大文字山頂の裏にでるので、そこから尾根をたどって山頂に出た。いやー何年ぶりだろう。学生の時以来なので、30数年ぶりか。相変わらずここからの眺めは絶景だ。もうお昼近くになっているので大文字の山頂で一つ目のおにぎりを食べる。

大文字の山頂。本当に久しぶり
大文字山から京都市内の眺め

ここから大文字のいわゆる大の字のところへ降りていくとさらに絶景が楽しめるが、今日は京都一周トレイルを走っているので、またさっきの分岐に戻って、山を下りる。
降りていく途中でトレランのファンだというおじさんに会った。なんでも今日大会があったそうで、応援に来てたのだとか。大会に参加しているランナーと間違えられて、ずいぶん遅いなと思われたみたい。このおじさん、この辺で大会があるといつも応援に行っているらしい。女子の第一人者の丹羽薫さんのファンとのことで、大文字の山頂で一緒に撮った写真を見せてくださった。丹羽さんはこのあたりでいつもトレーニングしているらしい。いい人なのであったら声を掛けたらいいと教えてくれた。私も、丹羽さんは好きなランナーの一人です。でも、京都の人とは知らなかった。テレビで見る時は全然関西弁が出ないので別の所の人かと思っていた。もっとも、結婚して京都に住むようになったのかもしれないし。とにかく、面白い出会いであった。
大文字山を下りて鹿ケ谷に出る。この辺りは左京区で学生時代に過ごした懐かしい当たり。

鹿ケ谷に降りてきたところにある不動尊

お不動産の脇を通って、疎水沿いの哲学の道に出る。冬なので哲学の道はそれほど人がいなかった。
哲学の道を通って、銀閣寺の参道に出る。学生時代はここからよく大文字に登った。この先は比叡山の山道に入るので、ここでもう一度トイレに行っておく。
一瞬白川通りに入って、右に曲がって志賀越え道に出る。山中越えの道からバプティスト病院の前に出てその脇を通って登り口に出る。脇と言っても駐車場があって、ここは病院の敷地なのではと思ってしまう。そして、地図を確かめていると間違って私有地に入ってしまっていた。警告看板が手前にあったが地図を見ていて気付かずに2度目の看板でようやく気付いた。間違って入る人が多いのだろう。本当の登山道は細くてわかり難い。
ここから本格的な比叡山への登山道に入る。今日のハイライトだ。
自分の下調べしたルートでは、瓜生山に行かずに手前の沢を右に折れて登っていくルートになっていたが、分岐が見当たらずそのまま尾根を進む形になる。ちょっと不安になったが瓜生山まで行ってそこから右に折れれば最悪元の道に合流できそうなのでそのまま進むことにする。すると、しばらく行ったところに京都一周トレイルのポストがあった。ここからは自分の地図ではなくポストを頼りに進む。
ここまででずいぶんアップダウンを繰り返してきているので足が重い。だがここからが本番だし、これくらいで音を上げているようでは本番のレースを完走することはできない。瓜生山を越えると少し傾斜が緩くなって走れるようになった。比叡山へののぼりはだいたい3段で登るような感じになっている。ここでようやく1段目を越えた形。お昼も過ぎたのでそろそろ昼ご飯にしたいところ。ただ、もう少し上ってからにしたい。本来は走り終えて山頂で食べるのが良いのだろうが、絶対に山頂は寒いので、途中で食べたほうがよさそうだ。と思っていると京都市内を望める絶景ポイントに出た。

京都市内の眺望 妙法が見える

もう少し行くとまたのぼりが始まった。2段目ののぼりだ。ここで、上から降りてくるトレイルランナーとすれ違った。このあと何人かのトレイルランナーとすれ違った、やっぱりここでトレーニングしている人は多いみたい。
2段目を上り終えてしばらく行くと、腰かけるのによさそうな石があったのでここでお昼にする。もう一つのおにぎりとカップヌードルを食べる。やはり冬は暖かいものがいい。ちょっと重いしかさばるけど。比叡山なら山頂で食べるという手もあるのだけれど、予定よりも遅くなった場合はかなりお腹がすくことになるので念のため持ってきていた。食べると少し休憩になるし、ザックが軽くなる。少し身軽になったところで最後ののぼりに挑む。
川を三つほど渡って雲母坂に入る。入ってすぐに昔の結界の後があった。比叡山も昔は女人禁制だったのだろうか?

結界の後

しかし、雲母坂はきつかった、本当に最後の正念場といったところ。でも、ここまで雪は全くなし。このまま山頂まで行けるのではと思い始めたころに雪が見えてきた。

山頂に近づくと雪が見えてきた。

そして、急登を上っていると機械の音が聞こえる。すると八瀬からのケーブルの駅に着いた。

ケーブルの駅に到着

予定よりも1kmほど距離が少ないけどもう着いたと思ったが、実は早とちり。八瀬側からはケーブルがあってその先にロープウェイがあったのでまだもう少し上りがある。トレイル道はロープウェイとは違う方向に行くのだけれど。
そして、ケーブルの駅前に出ると一気に雪の量が増えて一面真っ白の銀世界。

ケーブルの駅前は雪で真っ白。

予定では、ここから坂本側に降りるケーブルまで行って坂本側に降りようと考えていた。もし、雪が深くてしんどそうならここから八瀬に降りてもよいと思っていたが、それほど雪も深くなさそうなので予定通り坂本側に降りることにして、ケーブルの駅をゴールにした。ただ、道は一面雪で真っ白なので、走るのは少し危険そうなので歩いていく。ここでも数人のハイカーとトレイルランナーとすれ違った。トレランの人たちは雪道を走っていた。途中で比良山系が見えるところに出たが、琵琶湖バレイのあたりも雪に覆われていた。

比良山系の様子

後ろ側にだだっ広い広場があった。あとで地図を見ると人工スキー場の跡らしい。ここを上っていくと比叡山の山頂に行けるが雪が深そうだし、トレイルのルートは山頂部を巻くように進むようになっているのでそちらを行くことにする。
ぐるっと回りこんで山頂の北側に出たところで比叡山ドライブウェイに当たる。京都一周トレイルのコースはここから北へ向かうが、今日はここでトレイルコースと別れて根本中堂からケーブルの駅に向かう。
ケーブルの駅からは琵琶湖が良く見えた。

ケーブルの駅から見た琵琶湖、湖北側
<>
湖南側

帰りのケーブルでもう一人一緒に乗ったトレイルランナーさんがいたが、短パン(ゲイターは履いていたが)で上も長袖のアンダーに半そでTシャツという姿で寒そうにしていた。そりゃ寒いだろうと。防寒着は何も持っていないらしい。いやあ、よくやるなあと思った。自分も結局持ってきた防寒着は一度も着なかったが、やや厚手の長ズボンに上は3枚着ていた。途中で暑いと思ったが脱ぐほどのこともない感じで結局最後まで同じ格好で過ごしてしまった。きっと石舞台100のレースでも同じような恰好をするに違いない。スタートは12時半でスタート時点では温いかもしれないが、ゴールするころには日が暮れているだろうから結構寒いに違いないと思っている。
とりあえず、今回は最後は雪でスローダウンしたが約5時間で約27km、登り約2000mのコースを何とか完走した。まあ、これなら石舞台100も何とかなるのではと思える内容だったし、生駒にせず比叡山に来てよかったと思った。
帰りは、ケーブルで坂本まで降りて、JRで帰った。ケーブルの中で時刻を調べると20分後くらいに京都行きがあるみたいなのでそれに乗ろうと思ったが、歩いていると途中で間に合いそうになかったので、結局ここでも走ることになった。まあ、クールダウンというところか。件のトレイルランナーさんも走っていて、あちらは京阪の駅から電車に乗ったみたい。

さて、今年のブログの更新はこれが最後です。最近はトレランのブログか電子工作のブログかわからん内容になってしまっていますが、まあ、出来事をつらつら書いていくのがブログだと思っていますので、この調子で続けていきたいと思っています。
いつも読んでくださる方、いつもありがとうございます。来年もまたよろしくお願いします。