こんにちは、ENGかぴです。
Raspberry Pi PicoをVSCodeの拡張機能であるPico SDKを使用するとRTCを実装することができます。マイコンに内蔵されているRTCに時刻を書き込んでシリアルモニターに表示して動作確認します。
本記事はPico SDKを使ってC/C++のプロジェクトを作成してRTCを実装します。
Raspberry Pi Pico(以下Picoとする)と拡張基板のGrove Shield for Pi Picoを使用しています。PCとPicoの通信はUSBを介して行うため、AE-CH340E-TYPEC(秋月電子)を使用しています。
VSCodeのダウンロードとインストールの方法やVSCodeにPicoの開発環境を追加する方法は下記記事を参考にしてください。
Raspberry Pi Picoの開発をVSCodeで行う方法
Picoを使用してArduino IDEやVSCodeで動作確認したことをまとめています。
RTCのAPIを使用する
PicoのマイコンであるRP2040のデータシートを確認するとRTCに関する記述が4.8.4. Reference clock(P549)にあります。
Picoは外部クロックとして12MHzの水晶発振子が実装されておりこれをPLLで逓倍して125MHz(デフォルト設定)のシステムクロックを生成しています。RTCのクロックはこのシステムクロックを分周して1Hzのクロックを生成して動作しています。
125MHzを分周して1Hzを生成するためPLLの誤差や元のクロックの影響が大きくなります。このため長時間使用すると誤差が大きくなります。
PicoのRTCは12MHzのクロックからRTCクロックを生成しているため長期間の時刻の管理に適しませんが、ログのタイムスタンプや周期的な処理のトリガーなど短期間の時間管理であれば多少の誤差が出ても問題ないケースがあります。
本記事ではUARTを使って自作の電文によってRTCの時刻を書き込むことで任意の時刻でRTCを動作させます。
PR:わからないを放置せず、あなたにあった最低限のスキルを身に着けるコツを教える テックジム 「書けるが先で、理解が後」を体験しよう!
プロジェクトの生成
プロジェクトの生成方法は下記記事にまとめています。
Raspberry Pi Picoの開発をVSCodeで行う方法
プロジェクト生成のFeaturesにRTCの項目はありません。プロジェクトを作成した後にCMakeListsファイルにインクルード対象のファイルを追加します。
UARTはデフォルトでUART0が使用できるようになっているため特にチェックを入れる必要はありません。
プロジェクトを生成した後CMakeLists.txtファイルを開いてRTCのハードウェアをインクルードできるようにします。
# Add the standard library to the build
target_link_libraries(rtc
pico_stdlib
hardware_rtc
)
#以下を追加してもよい
target_link_libraries(rtc
hardware_rtc)
target_link_libraies()にhardware_rtcを追加します。2通りの例に従ってpico_stdlibの下に追加するか、target_link_libraries()を新たに生成してhardware_rtcを追加します。
PR:スキマ時間で自己啓発!スマホで学べる人気のオンライン資格講座【スタディング】まずは気になる講座を無料で体験しよう!
初期化処理を実装する
#include "hardware/rtc.h" //CMakeLists.txtのtarget_link_librariesにhardware_rtcを追加
#include <stdlib.h>
datetime_t setdate = {
.year = 2025,
.month = 8,
.day = 10,
.dotw = 0, // 曜日(0=日曜)
.hour = 12,
.min = 0,
.sec = 0
};
//RTCの初期化
rtc_init();
rtc_set_datetime(&setdate);
最初にhardware/rtc.hをインクルードします。CMakeLists.txtに追加していると候補として表示されます。<stdlib.h>は文字列から整数値に変換するstrtol()関数を使用するためにインクルードしています。
メインの無限ループに入る前に初期化処理を行います。rtc_init()関数を使用するとRTCに関する初期化処理が行われます。rtc_set_datetime()関数は時刻の書き込みに使用します。

datetime_t 型の変数に値を指定したものを準備して引数に指定すると任意の時間から時刻の更新をスタートします。値の範囲を外れた存在しない時刻を入力しないように注意が必要です。
RTCから時刻の取得
datetime_t now;
if( timrtc == TIME_UP ){
timrtc = RTC_READ;
rtc_get_datetime(&now);
}
if( now.sec ^ old_now.sec ){
weekset();
sprintf(str,"%d/%02d/%02d %s %02d:%02d:%02d",
now.year,now.month,now.day,week,now.hour, now.min, now.sec );
uart_puts(uart0,str);
uart_puts(uart0,"\r\n");//改行コード
old_now = now;
}
timrtcで1秒毎に時刻の更新を行います。rtc_get_datetime()関数を使用すると引数で指定したdatetime型の変数に取得した時刻が格納されます。
8行目は取得した時刻と1秒前のデータを比較して変化があれば、時刻を表示する文字列を生成します。sprintf()関数で取得した時刻のデータと曜日のデータから「2025/08/10 SUN 12:00:00」などの文字列を生成します。
uart_puts()関数は第1引数に指定したUARTに対して、第2引数で指定した文字列を送信します。10行目で生成した文字列をシリアルモニターに表示します。
自作の電文による時刻補正

シリアルモニターを使ってRTCの時刻を自作の電文を送信して書き込む仕組みを作ります。電文の構成はヘッダーに「RTC」の文字列に年から秒を「-」(ハイフン)で繋げたものを電文として生成します。
例のようにシリアルモニターで「RTC2025-08-10-00-12-00-00」を文字列で送信するとPicoで電文を判断して時刻を書き込みます。
/* 時刻のチェック */
bool chkdata(void){
datetime_t set;
bool ret = false;
//RTC2025-08-10-00-12-00-00
set.year = strtol((const char*)&Rcvdata[3], NULL, 10);
set.month = strtol((const char*)&Rcvdata[8], NULL, 10);
//日から秒は省略
if( set.year >= 2000 && set.year < 2100 ){
if( set.month >= 1 && set.month <= 12 ){
if( set.day >= 1 && set.day <= 31 ){
if( set.hour >= 0 && set.hour <= 23 ){
if( set.min >= 0 && set.min <=59 ){
if( set.sec >= 0 && set.sec <= 59 ){
if( set.dotw >= 0 && set.dotw <= 6 ){
setdate = set;
rtc_set_datetime(&setdate);
ret = true;
}
}
}
}
}
}
}
return ret;
}
シリアルモニターで取得した電文は文字列なので文字列から年~秒のデータを生成します。
7行目は年を生成していますが、strtol()関数を使用しています。第1引数に変換する文字列の先頭アドレスを指定します。例では年の文字列の配列を指定しています。
第2引数は文字変換が終了した位置を格納するポインタアドレスを指定します。使用しない場合はNULLとします。
第3引数は数値の基数を指定します。10進数なら10、16進数なら16を指定します。
電文は年~秒を-(ハイフン)で区切っているためハイフンまでの文字列を変換した結果を格納します。
格納した結果を年から秒の範囲チェックを行う最低限のチェック(11~17行目)をしてrtc_set_datetime()関数でRTCの時刻を書き込みます。
PR:RUNTEQ(ランテック )- マイベスト4年連続1位を獲得した実績を持つWebエンジニア養成プログラミングスクール
動作確認

PicoのUART0をシリアルモニターで使用するためUSBモジュール(AE-CH340E-TYPEC)を使用しています。PicoはDC3.3Vのマイコンなので電圧をDC5V~DC3.3Vに変換して接続しています。
Picoは電源を投入して5秒後に「Hello, RTC!」を表示します。その後、初期のRTC時間をシリアルモニターに1秒ごとに表示しながら電文を待機します。
シリアルモニターでポートを選択します。USBモジュールを挿入して生成されたCOMポートを選択します。ボーレートはデフォルトの115200bpsを選択します。

RTCの時刻が読み込まれて表示されていますが電文で時刻を書き込みます。メッセージ欄に「RTC2025-08-12-02-15-22-30」(2025年8月12日(火)15:22:30)をテキストとして送信します。送信すると電文によって書き込んだ時刻からRTC時刻が更新されていることが確認できました。

次に、電文がNGの場合の動作を確認しました。8月51日とありえない時刻を送信すると、「RTCSET NG!」の文字列を表示します。電文が無効となることが確認できました。
ソースコード全体
ソースコードは記事作成時点において動作確認できていますが、使用しているライブラリの更新により動作が保証できなくなる可能性があります。また、ソースコードを使用したことによって生じた不利益などの一切の責任を負いかねます。参考資料としてお使いください。
#include <stdio.h>
#include "pico/stdlib.h"
#include "hardware/rtc.h" //CMakeLists.txtのtarget_link_librariesにhardware_rtcを追加
#include <stdlib.h>
#define PIN_DO1 25
#define TIME_OFF -1 //タイマーを使用しない場合
#define TIME_UP 0 //タイムアップ
#define BASE_CNT 10 //ベースタイマカウント値
#define LED_ONOFF 50
#define RTC_READ 100
#define RING_SZ 128
#define TIM_RX_WAIT 5 //50ms
#define OFFSET 3
datetime_t setdate = {
.year = 2025,
.month = 8,
.day = 10,
.dotw = 0, // 曜日(0=日曜)
.hour = 12,
.min = 0,
.sec = 0
};
struct RING_MNG{
uint8_t wp;
uint8_t rp;
uint8_t buf[RING_SZ];
};
RING_MNG u0_rcvdata;
uint32_t beforetimCnt;
datetime_t now;
datetime_t old_now;
int16_t timled = LED_ONOFF;
int16_t timrtc = RTC_READ;
int16_t timrcv = TIME_OFF;
char str[] = {"2025/08/10-00-12:00:00"};
char week[] = {"sun"};
uint8_t Rcvdata[sizeof(str) + OFFSET];
void mainApp(void);
void mainTimer(void);
void weekset(void);
void rcvdatechk(void);
void ReadPointerAdd(void);
bool chkdata(void);
int main()
{
stdio_init_all();
gpio_init(PIN_DO1); // ピン初期化
gpio_set_dir(PIN_DO1, GPIO_OUT); //DOピン
rtc_init();
rtc_set_datetime(&setdate);
now = old_now = setdate;
sleep_ms(5000);
uart_puts(uart0, "Hello, RTC!\n");
while (true) {
mainApp();
mainTimer();
rcvdatechk();
}
}
/* メイン処理 */
void mainApp(void){
while(uart_is_readable(uart0)){
u0_rcvdata.buf[u0_rcvdata.wp] = uart_getc(uart0);
//uart_putc(uart0,(char)u0_rcvdata.buf[u0_rcvdata.wp] );// PCから受信したデータを表示
if( ++u0_rcvdata.wp >= RING_SZ ){
u0_rcvdata.wp = 0;
}
}
if( timled == TIME_UP ){
timled = LED_ONOFF;
gpio_put(PIN_DO1, !gpio_get(PIN_DO1));
}
if( timrtc == TIME_UP ){
timrtc = RTC_READ;
rtc_get_datetime(&now);
}
if( now.sec ^ old_now.sec ){
weekset();
sprintf(str,"%d/%02d/%02d %s %02d:%02d:%02d",
now.year,now.month,now.day,week,now.hour, now.min, now.sec );
uart_puts(uart0,str);
uart_puts(uart0,"\r\n");
old_now = now;
}
}
/* タイマ管理 */
void mainTimer(void){
if( to_ms_since_boot(get_absolute_time()) - beforetimCnt > BASE_CNT ){
beforetimCnt = to_ms_since_boot(get_absolute_time());
//10msごとにここに遷移する
if( timled > TIME_UP ){
timled--;
}
if( timrtc > TIME_UP ){
timrtc--;
}
if( timrcv > TIME_UP ){
timrcv--;
}
}
}
/* 曜日の文字列セット */
void weekset(void){
switch (now.dotw)
{
case 0:
sprintf(week,"SUN");
break;
case 1:
sprintf(week,"MON");
break;
case 2:
sprintf(week,"TUE");
break;
case 3:
sprintf(week,"WED");
break;
case 4:
sprintf(week,"THU");
break;
case 5:
sprintf(week,"FRI");
break;
case 6:
sprintf(week,"SAT");
break;
default:
sprintf(week,"XXX");
break;
}
}
/* 受信データから時刻データを生成 */
void rcvdatechk(void){
int8_t rxsz;
uint8_t allsz;
uint8_t rp = u0_rcvdata.rp;
uint8_t i;
uint8_t dat[3];
if( timrcv == TIME_UP){
timrcv = TIME_OFF;
ReadPointerAdd();
}
rxsz = u0_rcvdata.wp - u0_rcvdata.rp; //受信データ数の算出
if( rxsz < 0 ){
rxsz = rxsz + sizeof(u0_rcvdata.buf);
}
if( rxsz == 0 ){
timrcv = TIME_OFF;
}
else{
if( timrcv == TIME_OFF ){
timrcv = TIM_RX_WAIT;
}
if( rxsz >= 3 ){
for( i = 0; i < 3; i++){//データサイズ算出のため仮おき
dat[i] = u0_rcvdata.buf[ rp ];
if(++rp >= sizeof(u0_rcvdata.buf) ) rp = 0;
}
if( dat[0] == 'R' && dat[1] == 'T' && dat[2] == 'C'){
allsz = sizeof(str) + OFFSET - 1;
if( rxsz >= allsz){
timrcv = TIME_OFF;
for(i=0; i < sizeof(Rcvdata); i++){
Rcvdata[i] = 0;
}
for( i=0; i < allsz; i++ ){
Rcvdata[i] = u0_rcvdata.buf[u0_rcvdata.rp];
ReadPointerAdd();
}
if( chkdata() == false ){
uart_puts(uart0, "RTCSET NG!\n");
}
}
}
}
}
}
/* 読み込み位置の更新 */
void ReadPointerAdd(void){
if(++u0_rcvdata.rp >= sizeof(u0_rcvdata.buf) ){
u0_rcvdata.rp = 0;
}
}
/* 時刻のチェック */
bool chkdata(void){
datetime_t set;
bool ret = false;
//RTC2025-08-10-00-12-00-00
set.year = strtol((const char*)&Rcvdata[3], NULL, 10);
set.month = strtol((const char*)&Rcvdata[8], NULL, 10);
set.day = strtol((const char*)&Rcvdata[11], NULL, 10);
set.dotw = strtol((const char*)&Rcvdata[14], NULL, 10);
set.hour = strtol((const char*)&Rcvdata[17], NULL, 10);
set.min = strtol((const char*)&Rcvdata[20], NULL, 10);
set.sec = strtol((const char*)&Rcvdata[23], NULL, 10);
if( set.year >= 2000 && set.year < 2100 ){
if( set.month >= 1 && set.month <= 12 ){
if( set.day >= 1 && set.day <= 31 ){
if( set.hour >= 0 && set.hour <= 23 ){
if( set.min >= 0 && set.min <=59 ){
if( set.sec >= 0 && set.sec <= 59 ){
if( set.dotw >= 0 && set.dotw <= 6 ){
setdate = set;
rtc_set_datetime(&setdate);
ret = true;
}
}
}
}
}
}
}
return ret;
}
メインのファイルの内容をコピーして置き換えることで使用できます。
プロジェクトを生成時のCMakeファイルの構成によって動作しない場合があります。CMakeLists.txtの構成で条件が不足している可能性があるため必要に応じて修正してください。
関連リンク
Arduinoのライブラリを使って動作確認を行ったことを下記リンクにまとめています。
Arduinoで学べるマイコンのソフト開発と標準ライブラリの使い方
Seeeduino XIAOで学べるソフト開発と標準ライブラリの使い方
ESP32-WROOM-32Eで学べるソフト開発と標準ライブラリの使い方
PR:企業で求められる即戦力技術を身に付ける テックキャンプエンジニア転職
最後まで、読んでいただきありがとうございました。