編集履歴一覧に戻る
KazHatのアイコン画像

KazHat が 2021年03月01日18時05分09秒 に編集

コメント無し

本文の変更

概要 ==== ロータリーエンコーダを使用して使いやすいデジタル時計を作ることを目指しました。表示は見やすくするためできるだけ大きなサイズのディスプレイと考え、2.8インチのLCDを採用しています。 主な使用部品 ==== - PIC 18F26K22: 28ピンDIPパッケージというちょうどよいサイズ、64KBというPICとしては十分なメモリ(本当はもっとメモリは欲しいです)、電源は2.3V~5.5V対応と、どれをとっても使いやすいPIC。秋月電子で価格290円で購入。 - 2.8インチ SPI 240x320ドットLCD: eBayにて900円ほどで購入。電源電圧は5V、3.3Vどちらもいけます。ジャンパで切り替える仕様ですが、5V仕様のまま3.3Vでも動作させられました。インタフェースはSPI。 タッチにもなっていて、別インタフェースですが、SPIで接続できます。ただし、昨今のスマホと異なり抵抗膜タイプです。 完全独立してSDカードのスロットとインタフェースも搭載されいます。(今回は未使用) - RTC (リアルタイムクロック): RTC8025モジュール 秋月で450円で購入しました。I2Cインタフェースで接続。 - 温湿度センサ: AM2320 確かeBayで購入。価格は160円ほどだったと思います。秋月でも取り扱っていますが、少しお高い。 -40~80℃の範囲で0.1℃単位ですが0.5℃精度の温度計、0~99.9%の0.1%step、3%の精度の湿度計になっています。インタフェースは、I2Cまたは、SPIと両対応。 - ロータリーエンコーダ: 秋月にて購入した24クリックタイプ。ALPINEのEC12シリーズのもの。80円 DIP化基板(同秋月)60円を利用。 時間変更する時、アラーム時刻変更などの時に活用。 - 照度センサ フォトトランジスタNJL7502L。秋月で2個入り100円。 周囲光に応じてLCDのバックライトを調整するために使用。 - 圧電スピーカ 秋月にて13mmサイズのものを使用。30円。アラーム用に使用。 - スライドSW アラームのオンオフをコントロールするために使用。 - 電源 現状、PICKIT2から3.3V供給しています。最終形では、USB電源を3.3Vレギュレータで降圧して供給することを想定しています。 LCDのバックライト以外は、5Vでも動作するものばかりなので、5V電源、バックライトだけ何かしらの対応という解もあるかも。 回路図 ==== ![キャプションを入力できます](https://camo.elchika.com/de589cf1b03b0bae2b667e7aeb97c992a58d5ae1/687474703a2f2f73746f726167652e676f6f676c65617069732e636f6d2f656c6368696b612f76312f757365722f31366135336537332d623631642d346264652d393364642d3464366364646238653162312f66386639626430632d336132392d343138312d626138392d623934356634363966613866/) 作成した回路 ==== ![キャプションを入力できます](https://camo.elchika.com/1ddcb6be6d112da1c35c49a02ff2ac4ffb6d4be9/687474703a2f2f73746f726167652e676f6f676c65617069732e636f6d2f656c6368696b612f76312f757365722f31366135336537332d623631642d346264652d393364642d3464366364646238653162312f32373764333636372d346339632d343962322d613065612d623933303131303962653864/) 1枚のブレッドボードに乗りきらなかったので、2つに分かれています。配線が多すぎて銅配線されているかわかりにくいかもしれませんので、少し説明します。 左側のボードには、PIC、フォトトランジスタ、動作確認用緑色LED、バックライト制御用トランジスタが搭載されています。 左下に青と黒の配線が束になって画面外に出て行っていますが、これがLCDにつながっています。回路図で示したように主にPICと接続されています。 画面右側へ伸びている信号は、PICkit2との接続信号で、プログラムの書き込みに使用されますが、現状、電源供給もこちらから行っています。 右側のボードには、上から、圧電スピーカ、温湿度センサ、ロータリーSW、RTCが搭載されています。いくつか抵抗が見えますが、プルアップ抵抗です。詳しくは回路図をご覧ください。 なお、回路図にある、3.3Vレギュレータ、外部電源消失時のバックアップ電源のスーパーキャパシタは搭載されていません。ご了承ください。 動作 ==== 2つの画面モードをもっています。 ![キャプションを入力できます](https://camo.elchika.com/77ab938558c42ebef3f612417c08ff3b07babe5d/687474703a2f2f73746f726167652e676f6f676c65617069732e636f6d2f656c6368696b612f76312f757365722f31366135336537332d623631642d346264652d393364642d3464366364646238653162312f61633334306436652d306162312d343231352d383537332d323033386630633431336265/) ![キャプションを入力できます](https://camo.elchika.com/f6def50921de1f63df43b4005ce2bcdd7dea6c5e/687474703a2f2f73746f726167652e676f6f676c65617069732e636f6d2f656c6368696b612f76312f757365722f31366135336537332d623631642d346264652d393364642d3464366364646238653162312f33376461366437342d373739362d343836372d393131302d343262373431346565643234/) 切替はカレンダー部をタッチすることで行えます。 ケースはアクリル板とかで製作したいと思っていますが、まだチャレンジできていません。その辺にあった段ボールで作ったものなので、少し見栄えがよろしくありませんが、意外としっかりしています。LCDは、きちんとネジで止めてあります。 時刻を変更したい時は、時間の所を長押しすると、色が変わって変更できるようになります。ロータリーSWを回すと、普通の時計のように時刻を進めたり、遅らせたりできます。時刻の場合、1クリックで1分変更になりますが、早回しすると、2倍速~8倍速で進めることができるので、初期設定とかでたくさん変更しようとする場合も苦にならないようプログラムしています。 同様に、日付の所を長押しすると、押した場所によって、年、月、日の色が変わり、ロータリーSWで修正ができるようになります。 長押しすると、OK、Cancelボタンが出て、OK押すと設定した状態が反映されます。時間の設定の時は、OKで0秒スタートとなります。 通常の状態では、いつでもロータリーSWを回してアラーム時刻を変更できるようになっています。スライドSWでアラームのオン、オフができます。アラームなった時は、画面タップでスヌーズするようになっているので、目覚まし時計としての最低限の機能は有していると思っています。 通常は、時間の修正はほとんど必要ないので、使う機能として、アラーム時刻をロータリー、アラームのオンオフをスライドSWと2つのインタフェースだけで使えるのが人にやさしいと思っています。 過去に作った時計ではプッシュSWしかインタフェースとして持たせられず、ボタン数を最小限にしたため、長押しも2種類設けたり、かなりトリッキーなインタフェースになってしまっていました。また、時刻の調整もボタンだけで行うというのが、かなり使い勝手に影響していたなと思えたので、ロータリーSWは時計というものに対してよいインタフェースと思いました。 プログラムは、MPLAB X、XC8 (V2.20)を使って組み上げました。 プログラム自体はかなり長いので、割愛します。すでにプログラムは、64Kバイトの容量に対して87%のサイズになっているので、何か入れようとするとメモリの制限を気にしなくてはいけない状況になります。 日付、温度表示のフォントがギザギザなのは、8x8ドットレベルのフォントを見やすいように拡大表示したためです。大きなフォントを作ればよいわけですが、その分メモリを消費するので悩ましいです。8x8ドット文字なら、1文字8バイト、それを日付と同じ2倍サイズ、16x16ドットになると4倍の32バイトとメモリを食うのですよね。使う文字だけなら良いのですが、アルファベット+数字とか考えると100文字レベルのデータが必要になり、すぐKB(キロバイト)レベルの消費になってしまうのですよね。コンパイラもフリー版なので、最適化が十分されず大きめのコードを出すので、より制約を受けるのですよね。それが使用部品のPICの所に書いたメモリ問題なのですよね。 プログラムも構成をまず追記します。 ソースは、 main.c / main.h AlarmSound.c / AlarmSound.h Draw.c / Draw.h font.h LCD320x240color.c / LCD320x240color.h RTC8025.c / RTC8025.h TempHumidityAM2320.c / TempHumidityAM2320.h TouchXT2046.c / TouchXT2046.h その他MCCで生成されたソースファイル群がありadc, ccp5, device_config, epwm1, epwm3, ext_int, i2c2_master, interrupt_manager, mcc, memory, pin_manager, spi1, tmr0~6がcプログラムと、headerという形で構成されています。

+

メインプログラムは、以下のようになっています。 ```pic:main() void main(void) { char str[100]; uint8_t jj; uint16_t x, y;

+

// Initialize the device SYSTEM_Initialize();

+

//以下のハンドラの設定は、ここInterruptEnableの前にしないとハングする。それがわからず相当悩んだ IOCB5_SetInterruptHandler(AlarmWHandler); //アラーム Alarm Wの割り込み // If using interrupts in PIC18 High/Low Priority Mode you need to enable the Global High and Low Interrupts // If using interrupts in PIC Mid-Range Compatibility Mode you need to enable the Global and Peripheral Interrupts // Use the following macros to: // Enable the Global Interrupts INTERRUPT_GlobalInterruptEnable(); // Disable the Global Interrupts //INTERRUPT_GlobalInterruptDisable(); // Enable the Peripheral Interrupts INTERRUPT_PeripheralInterruptEnable(); // Disable the Peripheral Interrupts //INTERRUPT_PeripheralInterruptDisable(); SPI1_Open(SPI1_DEFAULT); //これでSPIが使えるようになる //LCDの初期化 glcd_init(); // lcd_fill(BLUE); //画面をクリア lcd_fill(BLACK); //画面をクリア(真っ黒) // ロータリー用TMR1の割り込み TMR1_SetInterruptHandler(RotaryHandler); //Touch/SlideSWは、TMR5の10ms割り込みで状態チェック TMR5_SetInterruptHandler(Timer5Handler); TMR5_StartTimer(); TouchCount = 0; TouchStatus = 0; //初めて起動したときは、タッチの調整を実施し、そのデータをEEPROMに保持 if (DATAEE_ReadByte(AddressInit) == 0xff) { TouchAdjust(); lcd_fill(BLUE); //画面をクリア(真っ黒) /* //タッチが正常にできているか動作確認用 while (1) { uint16_t x, y, dx, dy; if (TouchStatus != 0) { //描画してみる if (GetTouchLocation(&x, &y) != -1) { TransCoordination(x, y, &dx, &dy); lcd_draw_pixel_at(dx, dy, RED); sprintf(str, "P1=(%4d, %4d) P2=(%3d, %3d)", x, y, dx, dy); display_drawChars(0, 130, str, WHITE, BLACK, 1); } } }; */ DATAEE_WriteByte(AddressInit, 0x55); //調整したタッチ座標を書き込む DATAEE_WriteByte(AddressTouch, lo(T_x1)); DATAEE_WriteByte(AddressTouch+1, hi(T_x1)); DATAEE_WriteByte(AddressTouch+2, lo(T_y1)); DATAEE_WriteByte(AddressTouch+3, hi(T_y1)); DATAEE_WriteByte(AddressTouch+4, lo(T_x2)); DATAEE_WriteByte(AddressTouch+5, hi(T_x2)); DATAEE_WriteByte(AddressTouch+6, lo(T_y2)); DATAEE_WriteByte(AddressTouch+7, hi(T_y2)); //日付、アラーム設定のデフォルト値を書き込む for (jj=0; jj<4; jj++) { DATAEE_WriteByte(AddressYMD+jj, DateTime[jj+3]); } for (jj=0; jj<3; jj++) { DATAEE_WriteByte(AddressAlarm+jj, AlarmTime[jj]); } } else { //データを読み出す T_x1 = DATAEE_ReadByte(AddressTouch) + (DATAEE_ReadByte(AddressTouch+1)<<8); T_y1 = DATAEE_ReadByte(AddressTouch+2) + (DATAEE_ReadByte(AddressTouch+3)<<8); T_x2 = DATAEE_ReadByte(AddressTouch+4) + (DATAEE_ReadByte(AddressTouch+5)<<8); T_y2 = DATAEE_ReadByte(AddressTouch+6) + (DATAEE_ReadByte(AddressTouch+7)<<8); // sprintf(str, "P1=(%d, %d) P2=(%d, %d)", T_x1, T_y1, T_x2, T_y2); // display_drawChars(0, 140, str, WHITE, BLACK, 1); //日付、アラーム設定を読み出す for (jj=0; jj<4; jj++) { DateTime[jj+3] = DATAEE_ReadByte(AddressYMD+jj); } for (jj=0; jj<3; jj++) { AlarmTime[jj] = DATAEE_ReadByte(AddressAlarm+jj); } } // RTCの初期化 init_RTC(); INT0_SetInterruptHandler(RTC_handler); //時刻表示は変化があった所だけ表示更新するので、BCDではありえない数値を設定しておく // for (jj = 0; jj < 3; jj++) preDateTime[jj] = 0xff; //アラームの初期化 AlarmInitialize(); //アラーム音のTMR0の初期設定 AlarmSoundOff(); //念のため LED_SetHigh(); //最初の確認用 LEDを光らせて、ハード、プログラム書き込み等の初期チェック Mode = NormalInit; //初期状態はノーマル while (1) { // Add your application code #ifdef DEBUG sprintf(str, "Tch=%d", TouchCount); display_drawChars(250, 80, str, BLACK, WHITE, 1); #endif if (Mode <= Normal) NormalProc(); else if (Mode & Setting) SettingProc(); //スライドSW SlideSWProc(); } } ``` そして、主要機構であるローターリーSWの処置は、以下のようにしています。 ```PIC:ロータリーSWの処理 /* * TMR1の0.1ms毎の割り込みで、ロータリーエンコーダの状態をチェック */ void RotaryHandler() { static uint8_t StartRot = 0; static int8_t RotCW = 0; // 時計回り=1、反時計回り=0 static uint8_t RotA_status = 0xff; uint8_t newData; //A端子側のレベルをチェック。停止時=1、動かすと0となり、クリックポイントで1に戻る RotA_status = (RotA_status << 1) | ROT_A_GetValue(); if ((StartRot == 0) && (RotA_status & 0x07) == 0) { //A端子が0になったら、回転開始 StartRot = 1; //B端子のレベルで回転方向を判定 RotCW = ROT_B_GetValue(); } else if ((StartRot == 1) && ((RotA_status & 0x07) == 0x07)) { newData = ROT_B_GetValue(); if (RotCW != newData) { // B端子側が、1→0の時CW(インクリ)、0→1の時RCW(デクリ) if (newData) { //A端子がHになった時、B端子がHなら反時計方向に回転 RotCount--; } else { //A端子がHになった時、B端子がLなら時計方向に回転 RotCount++; } } //ちゃんと回転せず元に戻った場合、カウントさせず、終了するケースあり。 StartRot = 0; } } ``` RotCountの数値を見て、時計方向の回転の場合はプラス、反時計回しした場合はマイナスとして、設定しようとしている状態、時刻だったり、アラーム時刻を増減させます。 なお、このRotaryHandler()は、0.1ms毎のタイマ割り込みで常に監視しています。 全ソースを掲載するべきかもしれませんが、非常に長いので、掲載方法は、考えないといけないと思ってまだ、割愛させていただきたいと思います。