akira.keiのアイコン画像
akira.kei 2025年12月07日作成 (2025年12月07日更新) © MIT
セットアップや使用方法 セットアップや使用方法 閲覧数 153
akira.kei 2025年12月07日作成 (2025年12月07日更新) © MIT セットアップや使用方法 セットアップや使用方法 閲覧数 153

8ピンPICのPIC16F18313を使う(その19)I2C温湿度センサー

<前の記事 : 次の記事>

温湿度センサー

仕事で湿度を測る必要があり、湿度センサーを探していたところなかなかいいのがあったので、使い勝手を試してみた。I2C接続だからPCやMacに接続しにくいが、間にPICやArduinoを挟めばいい。とにかく動くのか確認してしたかったので、まずArduinoに繋いでみた。
SHT40i
Copilotに相談しながら書いたらあっさり動いたので、センサー自体に問題がないことがまずは確認できた。

PC/Macと接続

Mac側はPythonで温度と湿度を表示する。MacからUSBシリアル変換でバイナリ0x05(ENQ)を送信すると、PICがI2C通信してSHT40iから6バイトの温湿度データを受け取り、そのままMacにバイナリで返す、というシンプル構成にした。
USB↔︎PIC↔︎SHT40i
このPythonプログラムもほとんどCopilotに書いてもらった。AIってすごい。

PIC側はMCCで...

電源に2ピン、シリアル通信に2ピン、I2Cに2ピン、LEDに1ピン、リセットに1ピンで8ピン全て使用した。まずはお手本を、ということでMCC(Microchip Code Configurator)を使ったら、比較的あっさり動いてしまった。mcc_generated_filesを直接編集するという禁じ手も使っている。

#include "mcc_generated_files/mcc.h" #include "mcc_generated_files/examples/i2c1_master_example.h" extern uint8_t rflg; uint8_t cmdbuf[1]; uint8_t datbuf[6]; #define ADDR 0x44 void main(void) { uint8_t i; SYSTEM_Initialize(); LED_SetLow(); cmdbuf[0]=0xfd; INTERRUPT_PeripheralInterruptEnable(); INTERRUPT_GlobalInterruptEnable(); while (1) { if(rflg) { rflg=0; I2C1_WriteNBytes(ADDR,cmdbuf,1); __delay_ms(75); I2C1_ReadNBytes(ADDR,datbuf,6); for(i=0;i<6;i++) putchar(datbuf[i]); } } }

禁じ手の具体的な箇所は「void EUSART_Receive_ISR(void)」内の最後に

if(getch()==0x05) { rflg=1; LED_Toggle();}

を入れただけだ。ちなみに少し上の方にある「char getch(void)」ではエラーになるので、「int getch(void)」に変更した。まぁここまではまだマシだったんだ。

Copilotの無自覚な嘘

Copilotに「PIC16F18313でMCCを使わずにプログラムを書きたい」と相談しながらプログラムを書いてみたが、PythonやArduinoでは頼りになるのに、PICでは全然ダメだったw

これこれこんな仕様でこういうふうに、と最終形を指示してCopilotに作ってもらったプログラムは全然動かない。あれはどうこれはこうなどとあれこれ相談しながら書き直しても、アドバイスが無限ループになってきてこっちの頭が混乱する。

最初からやり直してみよう、pragma configはこんな感じでOKまではよかったが、オシレータ設定はこうすると8MHzになりますってところが間違ってた。それもLチカをやってると1秒周期で点滅すると言ったら、それはおかしいよディレイは500msなんだからと言い出す始末。いやトグルさせてんだから点いて500ms消えて500msで1秒周期だろって言ったらその通りとの回答が悪びれることなく返ってくる。

シリアル通信も接続するPPSの設定誤り、ボーレート計算の誤りなど、多分どこかのweb記事内容を理解ぜす切り貼りしたことが原因だろうなと思われる間違いが頻出。I2C接続も同じようにレート計算を誤り、PPS設定を間違った。

こうやって順に確認しながらプログラミングしていくと、ふと気がついたんだ。「これ、いつも自分でやってる作業と同じじゃん」「Copilot、いらないジャン。。。」

そして出来上がった

pragma_config.hはいつもと同じなので抜粋すると

#pragma config RSTOSC = HFINT1 //#pragma config CLKOUTEN = ON #pragma config CLKOUTEN = OFF

CLKOUTENがONがコメントアウトされているのは、動作クロックの確認をしなきゃいけない羽目になっていたからだ。main関数はシンプルで初期化したら無限ループでシリアルの受信待ちになる。本当ならポーリング処理(割り込み中はフラグセットのみ、メインループで測定処理)した方が安全だが、割り込みはシリアルしかないので大丈夫。

void main(void) { OSC_Init(); ANSELA = 0x00; LED_Init(); UART_Init(); I2C_Init(); INTR _Init(); while (1) {} }

オシレータはpragma configで完結せず、OSCFRQbits.HFFRQで設定する。Copilotはなぜかこれに3を設定したがったのだが、正しくは4だ。ここで躓いてわざわざCLOCKOUTENをONにしてオシロで確認までしたのだが「データシートのXXページを見ろ、3じゃなくて0b0100って書いてあるだろ」と指摘したら、わざわざその部分の要約まで作って「0b0100に設定してください」と上から目線。。。

#define _XTAL_FREQ 8000000UL void OSC_Init(void) { OSCCON1bits.NOSC = 0b110; OSCCON1bits.NDIV = 0; OSCFRQbits.HFFRQ = 0b0100; // 8MHz __delay_ms(10); }

UART部分の初期化の前に、PPSのロック・アンロック方法をCopilotに教えてもらわなかったら迷宮入りだったかもしれない。これはCopilotに聞いてよかったw でもなんでdo while(0)なんだろ。

#define PPS_LOCK(x) do { \ PPSLOCK = 0x55; \ PPSLOCK = 0xAA; \ PPSLOCKbits.PPSLOCKED = (x); \ } while(0)

UART初期化は以下の通り。ボーレートの設定値もデータシートに直接書いてあるため計算させてない。

void UART_Init(void) { TRISAbits.TRISA4 = 0; // TX TRISAbits.TRISA5 = 1; // RX PPS_LOCK(0); RA4PPS = 0x14; // TX -> RA4 RXPPS = 0x05; // RX <- RA5 PPS_LOCK(1); SP1BRGH = 0x00; SP1BRGL = 207; TX1STAbits.SYNC = 0; TX1STAbits.BRGH = 1; BAUD1CONbits.BRG16 = 1; RC1STAbits.SPEN = 1; TX1STAbits.TXEN = 1; RC1STAbits.CREN = 1; } #define UART_Write(b) do { while (!TX1STAbits.TRMT); TX1REG = (b);} while(0)

UARTの受信は1バイトだけなので直接レジスタを読んでいる。送信はマクロ化して関数呼び出しを避けた。

I2Cの方も大体同じような感じなのだが、最初にCopilotに書かせたやつはとうとう動作しなかった。

#define I2C_SPEED 100000UL // 100kHz #define I2C_ADDR 0x44 void I2C_Init(void) { TRISAbits.TRISA1 = 1; // SCL TRISAbits.TRISA2 = 1; // SDA PPS_LOCK(0); RA1PPS = 0x18; // MSSP SCL out RA2PPS = 0x19; // MSSP SDA out SSP1CLKPPS = 0x01; // RA1 in SSP1DATPPS = 0x02; // RA2 in PPS_LOCK(1); SSP1CON1 = 0b00101000; // I2C Master mode SSP1CON2 = 0x00; SSP1STAT = 0x00; SSP1ADD = ((_XTAL_FREQ / (4 * I2C_SPEED)) - 1); // ≈19 SSP1CON1bits.SSPEN = 1; }

I2C送受信はちょっと複雑なんだが、Copilotが書いてくれた。Start, Stop, Write, Readのシーケンスは測定ルーチンの中で。

#define I2C_Start() do { SSP1CON2bits.SEN = 1; while (SSP1CON2bits.SEN); } while(0) #define I2C_Stop() do { SSP1CON2bits.PEN = 1; while (SSP1CON2bits.PEN); } while(0) void I2C_Write(uint8_t data) { SSP1BUF = data; while (SSP1STATbits.BF); while (SSP1CON2bits.ACKSTAT); // 0=ACK, 1=NACK } uint8_t I2C_Read(uint8_t ack) { SSP1CON2bits.RCEN = 1; while (!SSP1STATbits.BF); uint8_t data = SSP1BUF; SSP1CON2bits.ACKDT = ack; // 0=ACK, 1=NACK SSP1CON2bits.ACKEN = 1; while (SSP1CON2bits.ACKEN); return data; }

そして実際の測定ルーチンは脳筋だ。1バイト読み書きを順に並べてある。

void SHT40_Measure(void) { I2C_Start(); I2C_Write(I2C_ADDR << 1); // Writeアドレス I2C_Write(0xFD); // 高精度測定コマンド I2C_Stop(); __delay_ms(20); // 測定待ち I2C_Start(); I2C_Write((I2C_ADDR << 1) +1); // Readアドレス uint8_t t_msb = I2C_Read(0); uint8_t t_lsb = I2C_Read(0); uint8_t t_crc = I2C_Read(0); uint8_t h_msb = I2C_Read(0); uint8_t h_lsb = I2C_Read(0); uint8_t h_crc = I2C_Read(1); I2C_Stop(); // UARTで返す(6バイト) UART_Write(t_msb); UART_Write(t_lsb); UART_Write(t_crc); UART_Write(h_msb); UART_Write(h_lsb); UART_Write(h_crc); }

割り込みルーチンでやってることは簡単だ。UARTから0x05が来たらセンサーで測定するという橋渡しをしている。

void INTR_Init(void) { PIR1bits.RCIF = 0; PIE1bits.RCIE = 1; INTCONbits.PEIE = 1; INTCONbits.GIE = 1; } void __interrupt() isr(void) { LED_TOGGLE; if (PIR1bits.RCIF) { PIR1bits.RCIF=0; if (RC1REG == 0x05) SHT40_Measure(); } }

このほかLEDの部分も書いておけば完成だ。(ただし、書く順番には気をつける必要がある)

void LED_Init() { TRISAbits.TRISA0 = 0; // LED LATAbits.LATA0 = 0; } #define LED_TOGGLE do {LATAbits.LATA0 ^= 1;} while(0)
akira.keiのアイコン画像
機械系エンジニアだが電子工作を趣味としている。週末はひとりバーベキュー。
ログインしてコメントを投稿する