こんにちは。Yukiです。
今回は、CH32V203C8T6でRTCを使ってみたいと思います。
ちなみにですが、CH32V203K8T6でもRTCは使用できますが、色々成約があるので注意です。(下の方で書きます)
RTCの簡単な説明
RTCモジュールはReal Time Clockの略で、日付や時間を刻み続けてくれる機能です。
外付けだと、EPSONのRTCモジュール(RX8901やRA8000)などが比較的有名です。
今回は、マイコンの内部に機能として入っているということで、使ってみたいと思います。
CH32V203のRTC
CH32V203シリーズのマイコンにはRTC機能が搭載されています。
機能としては
- 220 のプリスケーラ(分周器)がついていて、自由に選択可能
- 32bitのカウンタ
- いろんなクロックを入力可能
- 割り込み可能
- RTCのみでのリセット可能
- VBATの電源供給によりRTC動作
まあ普通ですね。使いにくいなと思ったのは、カウンタしか用意されていないことでしょうか。
(たしかSTM32シリーズとかだと日付のレジスタと、時間のレジスタが別れていたので、うるう年とかそういうめんどくさいことを考えなくて良かった気がします)
ちなみにですが、CH32V203K8T6などのCH32V203F6, F8, G6, G8, K6, K8はVBAT端子とOSC32端子がないため、バックアップ動作(メイン電源を消しても、VBATに接続されている電源でRTCのみ省電力動作)はできません。
回路例

RTCを使って時計を作ろう的なやつのマイコン部分のみ切り取って持ってきました。
OSC32INとOSC32OUTに水晶振動子とC0G特性があるセラコンをつければOKです。
(MEMS発振器とかでもいいと思います)

回路図上のシンボルが異なっていますが、BT1はスーパーキャパシタです。
本当は3.3Vから抵抗の間にダイオードを入れたほうが、漏れ電流が少なくて良いのかもしれないです。
基本的に回路はこれだけです。これだけでRTCのバックアップ動作をします。
ちなみにですが、CH32V203K8T6等のバックアップ動作ができないマイコンは内部発振器LSIを使っても大丈夫です。(ただし精度は若干落ちます)
ちなみにですが、LSI(内部低クロック発振器)を使うと、バックアップ動作はできなくなります。
初期化プログラム例
#include <ch32v20x.h>
//RTCをLSI(OSC32)かつバックアップ動作で初期化
void RTC_LSI_init(void){
RCC->APB1PCENR |= RCC_PWREN; //Powerインターフェイス有効
RCC->APB1PCENR |= RCC_BKPEN; //Backupインターフェイス有効
PWR->CTLR |= PWR_CTLR_DBP; //Backup RTC有効
RTC->PSCRL = 0x7FFF; //32.768kHzを1sに分周
RCC->BDCTLR |= RCC_LSEON; //LSE有効
RCC->BDCTLR |= (1 << 8); //RTCクロックをLSEに設定
RCC->BDCTLR &= ~(1 << 9);
RCC->BDCTLR |= RCC_RTCEN; //RTCクロック有効
}
int main(void){
RTC_LSI_init(void);
}上が初期化例です。
ちなみに、分周器は220 用意されているので、1sカウントさせたいのであれば、1.048576MHzまではいけるはずです。(OSC32IN/OUT端子で使えるかは知らないですけど。まあ安価なので32.768kHzで良いと思いますが)
LSI(内部低クロック)や外部からクロック源を入れるとかってことであれば、1MHz以下にしたほうがいいと思いますね。(そうじゃないと1sごとにカウントできない)
日付/時間読み取りのプログラム例
RTCを使うということは、日付や時間にアクセスする必要があるということだと思います。
(時計やアラームを作る場合であれば、日付や時間はアクセス必須です)
ここでは、日付/時間読み取りのプログラム例を提示します。
#include <ch32v20x.h>
#include <time.h>
//RTC 年を取得
int RTC_Year_Read(void){
unsigned long RTC_val = (RTC->CNTH << 16) | RTC->CNTL; //すべてくっつける
time_t RTC_time = RTC_val; //Time型に変換(いらない気もする)
struct tm *utc_time = gmtime(&RTC_time); //UNIX時間から変換
int Year = utc_time->tm_year + 1900; //年 1900年ズレてるらしい
return (Year);
}
//RTC 月と日付を取得 (3月28日なら戻り値0328)
int RTC_Month_Day_Read(void){
unsigned long RTC_val = (RTC->CNTH << 16) | RTC->CNTL; //すべてくっつける
time_t RTC_time = RTC_val; //Time型に変換(いらない気もする)
struct tm *utc_time = gmtime(&RTC_time); //UNIX時間から変換
int Month = utc_time->tm_mon + 1; //月
int Day = utc_time->tm_mday; //日付
return (Month *100 + Day);
}
//RTC 時間+分数で取得 (12時34分なら戻り値1234)
int RTC_HourMin_Read(void){
unsigned long RTC_val = (RTC->CNTH << 16) | RTC->CNTL; //すべてくっつける
time_t RTC_time = RTC_val; //Time型に変換(いらない気もする)
struct tm *utc_time = gmtime(&RTC_time); //UNIX時間から変換
int Hour = utc_time->tm_hour; //時
int Min = utc_time->tm_min; //分
return (Hour*100 + Min);
}
//RTC 秒数を取得
int RTC_Sec_Read(void){
unsigned long RTC_val = (RTC->CNTH << 16) | RTC->CNTL; //すべてくっつける
time_t RTC_time = RTC_val; //Time型に変換(いらない気もする)
struct tm *utc_time = gmtime(&RTC_time); //UNIX時間から変換
int Sec = utc_time->tm_sec; //秒
return (Sec);
}もともと7セグに表示する目的で作っていたため、ややこしくなっていますが、ほぼどれも要領は同じです。
このマイコンのRTCモジュールは、日付や時間が別々のレジスタに入っている構造ではないので、(ただカウンタにカウントされていくだけ)UNIX時間で保存したほうが楽です。ということで、これからやることはUNIX時間→時間に変換しているだけです。
まず、RTC->CNTHとRTC->CNTLでレジスタが16bitずつ別れているため、unsigned long RTC_val = (RTC->CNTH << 16) | RTC->CNTL;
でRTCのカウンタに入っているデータを合体させます。
次に、time_t RTC_time = RTC_val;
でtime_t変数にします。(実はtime_t ってlong long int型なので、わざわざする必要ない気もしますが、一応やっておきます)
次に、struct tm *utc_time = gmtime(&RTC_time);
で、tm構造体に変換します。gmtime関数で年や日付、時間などに分解されます。
このtm構造体に日付だの時間だの入っています。
| tm_sec | 秒 0~61が入る (60,61はうるう秒考慮用) |
| tm_min | 分 0~59が入る |
| tm_hour | 時 0~23が入る |
| tm_mday | 日 1~31が入る |
| tm_mon | 月 0~11 (0が1月なので注意) |
| tm_year | 年 (1900年から始まっている) |
| tm_wday | 曜日 (0:日曜日 1:月曜日 … 6:土曜日) |
なんでか知らないですが、tm構造体では1900年から始まるのですが、UNIX時間では1970年から始まっていることに注意が必要です。(ややこしいですが、とにかくtm構造体から取り出したり、代入するときは1900を足したり引いたりすることを忘れないでください)
int Sec = utc_time->tm_sec;
あとは、上のような要領で取り出せます。
(これでやればうるう年とかも考慮しなくていいので楽ですね)
RTCのカウンタに書き込む場合
RTCのカウンタに書き込む場合、RTCをカウンタ書き込みモードにする必要があります。
void RTC_Set_UNIX(time_t tm){
RTC->CTLRL |= RTC_CTLRL_CNF; //Settingモード(レジスタに書き込みできるモード)
RTC->CNTH = (tm >> 16);
RTC->CNTL = (tm & 0xFFFF);
RTC->CTLRL &= ~RTC_CTLRL_CNF; //クロック動作開始
}
int main(void){
static unsigned int timer_cnt_hour = 0;
static unsigned int timer_cnt_min = 0;
static unsigned int timer_cnt_sec = 0;
static unsigned int timer_cnt_time = 0;
struct tm time_info = {0};
time_info.tm_year = time_set_year - 1900; //年 1990年から始まるので-1900
time_info.tm_mon = time_set_month - 1; //月 0から始まるので-1
time_info.tm_mday = time_set_day; // 日
time_info.tm_hour = time_set_hour; // 時
time_info.tm_min = time_set_min; // 分
time_info.tm_sec = time_set_sec; // 秒
time_t unix_time = mktime(&time_info); //UNIX時間に変更
RTC_Set_UNIX(unix_time); //UNIX時間を書き込み
}RTC->CTLRL |= RTC_CTLRL_CNF;
RTC->CTLRLレジスタのCNFビットを1にすることで、カウンタに書き込みできるモードになります。
RTC->CNTH = (tm >> 16);
RTC->CNTL = (tm & 0xFFFF);
カウンタは16bitずつ別れているので、ハイ側(32~17bit側)は16bitシフトダウンして代入しています。
ロー側(16~1bit側)は下位16bit分マスクして代入しています。
(PIC(XCコンパイラ)とかならRTC_CNTとかでH側とL側まとめて操作できるのに、この辺不便ですよね)
最後にCNFビットを0に戻して終了です。(0に戻した時点でカウント動作が開始されます)
ちなみに
上のプログラムでは忘れていましたが、mktimeとかで生成に失敗した場合は、NULLが返されるらしいです。なので本当は、
time_t unix_time = mktime(&time_info);
if(unix_time == NULL){
//エラー処理
return -1;
}みたいにしたほうが安全だと思います。
時計のサンプル
時計についてはアメブロの方で公開しようと思っています。
記事を書き次第、だしますので、少々お待ち下さい。
余談
UNIX時間は、32bitだと2038年1月19日3時14分7秒に32bit目に繰り上がるので、負の値になり誤動作すると言われています。詳しくは2038年問題で調べてみてください。
ちなみに、このマイコンだと32bit分あるので、符号をないものと考えると、2106年2月7日6時28分ごろにカウントできなくなります。この日を超えると、カウンタがオーバーフローするので1970年に戻りますね。
まあこのマイコンが後80年後動いているとは思えないですし、その頃には新しい時間の数え方の方法が出ているでしょう。(そもそも80年後はRTCも64bit化されているかもしれないですし)

コメント