こんにちは、ENGかぴです。
Raspberry Pi PicoをVSCodeの拡張機能であるPico SDKを使用するとI2C通信を実装することができます。マイコン内蔵のRTCの時刻を読み出して時刻データに加工してLCD(I2C通信)に表示して動作確認します。
本記事はPico SDKを使ってC/C++のプロジェクトを作成してI2Cを実装します。下記記事で取得した時刻をI2C通信でLCDに表示します。。
Raspberry Pi Pico(以下Picoとする)と拡張基板のGrove Shield for Pi Picoを使用しています。PCとPicoの通信はUSBを介して行うため、AE-CH9102F-TYPEC(秋月電子)を使用しています。LCDはAQM1602XA-RN-GBW(秋月電子)を使用しています。
VSCodeのダウンロードとインストールの方法やVSCodeにPicoの開発環境を追加する方法は下記記事を参考にしてください。
Raspberry Pi Picoの開発をVSCodeで行う方法
Picoを使用してArduino IDEやVSCodeで動作確認したことをまとめています。
I2CのAPIを使用する
I2C通信はSCLとSDAの2本の信号線で行う通信です。プルアップ抵抗と信号線の長さによって通信速度が決まります。プルアップ抵抗を大きくすると信号線のL分と抵抗により信号の波形がなまってしまい通信不良の原因となるため注意が必要です。
デフォルトはSCLクロックが400KHzですがプルアップ抵抗によって通信ができないことがあるため通信速度の調整が必要です。
プルアップ抵抗は1kΩから10kΩで使用され、Arduino環境のデフォルトである100kHzで通信できることが多いですが、マイコン内蔵のプルアップの場合はマイコンの種類によりますが約40kΩ程度になるため、さらに低速度の通信になります。
PR:わからないを放置せず、あなたにあった最低限のスキルを身に着けるコツを教える テックジム 「書けるが先で、理解が後」を体験しよう!
プロジェクトの生成
プロジェクトの生成方法は下記記事にまとめています。
Raspberry Pi Picoの開発をVSCodeで行う方法
FeaturesのI2C interfaceにチェックを入れてサンプルコードを含めてプロジェクトを生成します。

RTCの時刻の書き込みにUART0を使用しますが、UARTはデフォルトでUART0が使用できるようになっているため特にチェックを入れる必要はありません。
プロジェクトを生成するとデフォルトでI2C関連の初期化と定義が自動で生成されます。
PR:スキマ時間で自己啓発!スマホで学べる人気のオンライン資格講座【スタディング】まずは気になる講座を無料で体験しよう!
初期化処理を実装する
#include "hardware/i2c.h"
#define I2C_PORT i2c0
#define I2C_SDA 8
#define I2C_SCL 9
i2c_init(I2C_PORT, 50*1000); //デフォルトは400kHzだが内臓プルアップを使うので低速にする
gpio_set_function(I2C_SDA, GPIO_FUNC_I2C);
gpio_set_function(I2C_SCL, GPIO_FUNC_I2C);
gpio_pull_up(I2C_SDA);
gpio_pull_up(I2C_SCL);
最初にhardware/i2c.hをインクルードします。FeaturesでI2CにチェックしているとCMakeLists.txtに追加されているので候補として表示されます。
I2CのチャンネルはI2C0とI2C1があります。Pico SDKではI2C0のデフォルトピンはGP8がSDA、GP9がSCLで割り当てられています。
メインの無限ループに入る前に初期化処理を行います。i2c_init()関数を使用するとI2Cに関する初期化処理が行われます。第1引数にI2Cポートの種別を指定し、第2引数にSCLの通信速度を指定します。デフォルトは400kHzですが、スレーブが応答できる通信速度に調整する必要があります。
例ではSDAとSCLのピンにマイコン内蔵のプルアップを実装しているため、デフォルトよりも低速の50kHzを指定しています。
gpio_set_function()関数はGPIOピンの周辺機器の設定を行います。第1引数に対象のGPIOピンを指定し、第2引数に周辺機器の機能を指定します。I2Cとして使用するためデフォルトから変更せずにGPIO_FUNC_I2Cを指定しています。
gpio_pull_up()関数はGPIOピンのマイコン内蔵のプルアップを有効にする設定です。引数に対象のGPIOピンを指定します。
LCDの初期化

PicoのI2CのAPIでコマンドを送信し、LCDの初期化を行います。LCDの初期化の例は下記記事にまとめています。
Seeeduino XIAOを使ってBME280のデータをLCDに表示する
LCDで文字を表示するために初期化を行います。秋月電子のHPに公開されているAQM1602XA-RN-GBWのLCD資料(参考資料)の例をPico SDKのAPIに置き換えて説明します。コマンド書き込みの例としてLcdComand()関数を実装します。
/* LCDへのコマンド処理 */
void LcdComand(uint8_t cmd){
uint8_t src[2] = { 0x00, cmd };
i2c_write_blocking(I2C_PORT, SLAVE_ADRS, src, 2, false);
sleep_ms(1);
}
LCDへのコマンドはスレーブアドレス(0x3E)を指定し0x00を書き込んだ後に指定コマンドを書き込みます。src[]配列を準備して0x00とコマンドを格納します(3行目)。
コマンドの送信はi2c_write_blocking()関数を使用します。第1引数にI2Cポートの種別を指定します。第2引数にスレーブアドレスを指定します。第3引数に送信するデータのアドレスを指定します。第4引数に送信するデータ数を指定します。
第5引数にI2C通信のストップ条件を送信するかを指定します。trueを指定するとストップ条件を送信せず、スレーブを選択した状態を維持するので連続して次のデータを送信することができます。
LcdComand()の引数にコマンドを指定してLCDを初期化する例は以下の通りです。
/* LCD初期化 */
void LcdInit(void){
LcdComand(FUNC1_SET); //8ビットバス・2LINE表示
LcdComand(FUNC2_SET); //拡張コマンド
LcdComand(INT_OSC); //内部周波数調整
LcdComand(CONST_SET); //コントラスト1
LcdComand(PWR_ICON_SET); //コントラスト2
LcdComand(FOLLOWER_SET); //フォロワー制御
LcdComand(FUNC1_SET); //拡張コマンドをオフ
DspClear();
LcdComand(DISP_ONOFF_SET);
//初期の文字を表示
sleep_ms(2000);
}
初期化の例に従ってコマンドを送信して初期化を行います。基本的に初期化の例の通りの値で問題ありません。
コントラスト設定を薄くしようとC5を0にしてみましたが文字が見えなくなるほど薄くなってしまったため、Contrast Setに0x70、Power/ICON/Contrast controlに0x54をセットしました。
ST7032のデータシートを確認するとI2C使用時のレイアウトではOPF1、OPF2は0とするためFonに1をセットすると内部フォロアが有効になります。コントラストは内部フォロアも関係するためFollower controlのRab0~Rab3の調整が有効なので、Follower controlに0x6Bをセットしました。
LCDに文字を表示
/* LCDに文字を表示 */
void LcdWriteData(uint8_t *data, uint8_t sz){
uint8_t src[sz + 1];
uint8_t i;
src[0] = 0x40;
for( i=0; i < sz; i++){
src[i + 1] = data[i];
}
i2c_write_blocking(I2C_PORT, SLAVE_ADRS, src, sz + 1, false);
}
LCDに文字を書き込むためLcdWriteData()関数を実装します。第1引数に表示する文字を示すアドレス、第2引数にサイズを指定します。
LcdWriteData()関数でデータの配列を生成します。スレーブアドレスに続けてcontrol byteを指定しますが、LCDを2段で表示するため、2-line interface protocolに従って制御コード(control byte)を指定します。

制御コードはデータシートを確認するとcontrol byte以降に制御コードを書き込まないのでCoに0をセットし、RSがH(1)になるので0x40(6行目)になります。
制御コード以降に文字データを配列にセット(7~9行目)してi2c_write_blocking()関数でデータを送信します。
datestr[]={"2025/08/10 SUN 12:00:00"}; //生成した時間の文字列
/* LCDの表示生成 */
void despset(void){
DspClear(); //LCDをクリア
LcdWriteData( (uint8_t*)&datestr[0], 14); //1段目の表示
DspLine2Top(); //2段目にカーソル移動
LcdWriteData( (uint8_t*)&datestr[15], 8); //2段目の表示
}
RTCから読み出した時刻から文字列を生成してdatestr[]配列に格納しています。
LcdWriteData()関数の第1引数にdatestr[]のアドレスを指定し、第2引数に表示する文字数を指定します。LCDの1段目は、datestr[0]から14バイトの文字を指定して表示します。LCDの2段目はdatestr[15]から8バイトの文字を指定して表示します。
PR:RUNTEQ(ランテック )- マイベスト4年連続1位を獲得した実績を持つWebエンジニア養成プログラミングスクール
動作確認

PicoのUART0をシリアルモニターで使用するためUSBモジュール(AE-CH9102F-TYPEC)を使用しています。PicoはDC3.3VのマイコンなのでUSBモジュールの出力がDC3.3Vになるようにしています。
I2CはSCLとSDAをR1とR2のようにプルアップ(1kΩ~10kΩ)する必要がありますが、Picoの内蔵のプルアップを使用しています。
通信速度はスレーブ(本記事ではLCD)に合わせて調整する必要がありますが、Pico内蔵のプルアップを使用しているため、デフォルトの400kHzに対して50kHzと低速な速度を指定して通信を行っています。
Picoは電源を投入して4秒後にLCDの1段目に「Pico SDK RTC」、2段目に「Ver0.01」を表示します。その後、初期のRTC時間をLCDとシリアルモニターに1秒ごとに表示しながら電文を待機します。

電文をシリアルモニターで送信します。電文の構成はヘッダーに「RTC」の文字列に年から秒を「-」(ハイフン)で繋げたものです。
シリアルモニターで電文を送信して初期の時刻を変更します。シリアルモニターでポートを選択します。USBモジュールを挿入して生成されたCOMポートを選択します。ボーレートはデフォルトの115200bpsを選択します。

シリアルモニターで「RTC2025-08-12-02-19-19-30」を文字列で送信するとPicoで電文を判断して時刻を書き込みます。電文を送信すると2025年8月12日(火)19:19:30から時刻の更新がスタートします。

シリアルモニターで送信した電文の時刻からスタートし、LCDの表示が更新されていることが確認できました。
ソースコード全体
ソースコードは記事作成時点において動作確認できていますが、使用しているライブラリの更新により動作が保証できなくなる可能性があります。また、ソースコードを使用したことによって生じた不利益などの一切の責任を負いかねます。参考資料としてお使いください。
#include <stdio.h>
#include "pico/stdlib.h"
#include "hardware/i2c.h"
#include "hardware/rtc.h" //CMakeList.txtのtarget_link_librariesにhardware_rtcを追加
#include <stdlib.h>
#define I2C_PORT i2c0
#define I2C_SDA 8
#define I2C_SCL 9
#define SLAVE_ADRS 0x3E
#define LINE1_ADRS 0x40
#define LINE2_TOP (0x40 +0x80)
#define FUNC1_SET 0x38
#define FUNC2_SET 0x39
#define INT_OSC 0x14
#define CONST_SET 0x70
#define PWR_ICON_SET 0x56
#define FOLLOWER_SET 0x6B
#define CLR_DISP 0x01
#define DISP_ONOFF_SET 0x0C
#define PIN_DO1 25
#define TIME_OFF -1 //タイマーを使用しない場合
#define TIME_UP 0 //タイムアップ
#define BASE_CNT 10 //ベースタイマカウント値
#define LED_ONOFF 50
#define RTC_READ 90
#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];
};
uint8_t initmoji[2][16] ={"Pico SDK RTC "," Ver0.01"};
RING_MNG u0_rcvdata;
uint32_t beforetimCnt;
datetime_t 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 datestr[32]; //LCDに表示する時刻の文字列
char week[] = {"sun"};
uint8_t Rcvdata[sizeof(str) + OFFSET];
void LcdInit(void);
void LcdComand(uint8_t cmd);
void LcdWriteData(uint8_t *data, uint8_t sz);
void DspLine2Top(void);
void DspClear(void);
void mainApp(void);
void mainTimer(void);
void weekset(void);
void rcvdatechk(void);
void ReadPointerAdd(void);
bool chkdata(void);
void despset(void);
int main()
{
stdio_init_all();
gpio_init(PIN_DO1); // ピン初期化
gpio_set_dir(PIN_DO1, GPIO_OUT); //DOピン
rtc_init();
rtc_set_datetime(&setdate);
i2c_init(I2C_PORT, 50*1000); //デフォルトは400kHzだが内臓プルアップを使うので低速にする
gpio_set_function(I2C_SDA, GPIO_FUNC_I2C);
gpio_set_function(I2C_SCL, GPIO_FUNC_I2C);
gpio_pull_up(I2C_SDA);
gpio_pull_up(I2C_SCL);
LcdInit();
sleep_ms(2000);
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);
weekset();
sprintf(datestr,"%d/%02d/%02d %s %02d:%02d:%02d",
now.year,now.month,now.day,week,now.hour, now.min, now.sec );
uart_puts(uart0,datestr);
uart_puts(uart0,"\r\n");
despset();
}
}
/* LCDの表示生成 */
void despset(void){
DspClear(); //LCDをクリア
LcdWriteData( (uint8_t*)&datestr[0], 14); //1段目の表示
DspLine2Top(); //2段目にカーソル移動
LcdWriteData( (uint8_t*)&datestr[15], 8); //2段目の表示
}
/* タイマ管理 */
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;
}
/* LCD初期化処理 */
void LcdInit(void){
LcdComand(FUNC1_SET); //8ビットバス・2LINE表示
LcdComand(FUNC2_SET); //拡張コマンド
LcdComand(INT_OSC); //内部周波数調整
LcdComand(CONST_SET); //コントラスト1
LcdComand(PWR_ICON_SET); //コントラスト2
LcdComand(FOLLOWER_SET); //フォロワー制御
LcdComand(FUNC1_SET); //拡張コマンドをオフ
DspClear();
LcdComand(DISP_ONOFF_SET);
LcdWriteData( &initmoji[0][0], sizeof(initmoji[0])); //1段目の表示
DspLine2Top(); //カーソル移動
LcdWriteData( &initmoji[1][0], sizeof(initmoji[1])); //2段目の表示
sleep_ms(2000);
}
/* LCDへのコマンド処理 */
void LcdComand(uint8_t cmd){
uint8_t src[2] = { 0x00, cmd };
i2c_write_blocking(I2C_PORT, SLAVE_ADRS, src, 2, false);
sleep_ms(1);
}
/* LCDに文字を表示 */
void LcdWriteData(uint8_t *data, uint8_t sz){
uint8_t src[sz + 1];
uint8_t i;
src[0] = 0x40;
for( i=0; i < sz; i++){
src[i + 1] = data[i];
}
i2c_write_blocking(I2C_PORT, SLAVE_ADRS, src, sz + 1, false);
}
/* 2段目にカーソル移動 */
void DspLine2Top(void){
LcdComand(LINE2_TOP);
sleep_us(40);
}
/* ディスプレイクリア */
void DspClear(void){
LcdComand(CLR_DISP);
sleep_us(40);
}
メインのファイルの内容をコピーして置き換えることで使用できます。
プロジェクトを生成時のCMakeファイルの構成によって動作しない場合があります。CMakeLists.txtの構成で条件が不足している可能性があるため必要に応じて修正してください。
関連リンク
Arduinoのライブラリを使って動作確認を行ったことを下記リンクにまとめています。
Arduinoで学べるマイコンのソフト開発と標準ライブラリの使い方
Seeeduino XIAOで学べるソフト開発と標準ライブラリの使い方
ESP32-WROOM-32Eで学べるソフト開発と標準ライブラリの使い方
PR:企業で求められる即戦力技術を身に付ける テックキャンプエンジニア転職
最後まで、読んでいただきありがとうございました。