Raspberry Pi Pico を自作アプリで操作できる I2C ツールにするファームウェアを作成したのですが、この記事ではそれを作るまでに苦労した話を紹介します。
SDK の I2C 関数が NACK を検出してくれなかったので関数の中身を調べたら NACK 検出を示すレジスタが値を変える前にNACKの有無を判定していたためと分かったので、この記事では SDK の関数を元に NACK を検出できる関数を作って、無事に動作確認ができるまでをまとめています。
改造した関数
作った関数と動作確認用のコードは以下になります。I2C 関数はヘッダにまとめています。
プロジェクトフォルダ: pico-test
pico-test
|-- pico_sdk.import.cmake
|-- CMakeLists.txt
-- try_07
|-- try_07.c
-- CMakeLists.txt
-- module_i2c
|-- module_i2c.h
|-- module_i2c.c
-- CMakeLists.txt
pico-test\CMakeLists.txt
cmake_minimum_required(VERSION 3.12)
# Pull in SDK (must be before project)
include(pico_sdk_import.cmake)
project(pico-test)
set(CMAKE_C_STANDARD 11)
set(CMAKE_CXX_STANDARD 17)
# Initialize the SDK
pico_sdk_init()
# Add subdirectory
add_subdirectory(try_07)
add_subdirectory(module_i2c)
pico-test\try_07\try_07.c
#include <stdio.h>
#include "pico/stdlib.h"
#include "class/cdc/cdc_device.h"
#include "../module_i2c/module_i2c.h"
int main() {
const uint LED_PIN = PICO_DEFAULT_LED_PIN;
uint8_t cntDAT;
uint8_t getDAT[1];
uint8_t inDAT[256];
uint8_t outDAT[16];
uint8_t byteADR;
uint8_t byteWRITE[16];
uint8_t byteDAT[16];
uint8_t i2c_result;
int i;
int intCNT;
stdio_init_all();
i2c_setup(i2c0, 100, 21, 20, true);
while (true) {
cntDAT = 0;
while(tud_cdc_available() > 0){
tud_cdc_read(getDAT, 1);
inDAT[cntDAT] = getDAT[0];
cntDAT++;
busy_wait_us_32(10000);
}
if(cntDAT > 0){
byteADR = 0x24;
byteWRITE[0] = 0x00;
byteWRITE[1] = inDAT[0] - '0';
if((byteWRITE[1] > 0) && (byteWRITE[1] < 10)){
i2c_result = i2c_write(byteADR, byteWRITE, 2, true);
if(i2c_result == RC_NACK){
outDAT[0] = 'F';
}
else if(i2c_result == RC_TIMEOUT){
outDAT[0] = 'T';
}
else{
outDAT[0] = 'P';
}
}
else{
i2c_result = i2c_read(byteADR, byteDAT, 1, true);
if(i2c_result == RC_NACK){
outDAT[0] = 'F';
}
else if(i2c_result == RC_TIMEOUT){
outDAT[0] = 'T';
}
else{
outDAT[0] = byteDAT[0] + '0';
}
}
outDAT[1] = 0x0a;
tud_cdc_write(outDAT, 2);
tud_cdc_write_flush();
}
}
}
pico-test\try_07\CMakeLists.txt
add_executable(try_07
try_07.c
)
# Add pico_stdlib library which aggregates commonly used features
target_link_libraries(try_07 pico_stdlib module_i2c)
# enable usb output, disable uart output
pico_enable_stdio_usb(try_07 1)
pico_enable_stdio_uart(try_07 0)
# create map/bin/hex file etc.
pico_add_extra_outputs(try_07)
pico-test\module_i2c\module_i2c.h
#ifndef MODULE_I2C_H
#define MODULE_I2C_H
#include <stdio.h>
#include "pico/stdlib.h"
#include "hardware/i2c.h"
#include "hardware/timer.h"
#include "pico/timeout_helper.h"
extern i2c_inst_t *reg_i2c;
extern uint16_t i2c_scl_freq_kHz;
extern uint8_t SDA_PIN;
extern uint8_t SCL_PIN;
extern const uint8_t RC_SUCCESS;
extern const uint8_t RC_TIMEOUT;
extern const uint8_t RC_NACK;
void i2c_scl_sda_pullup(bool enable_pullup);
void i2c_setup(i2c_inst_t *i2c_hw_block, uint16_t scl_freq_khz, uint8_t gp_for_scl, uint8_t gp_for_sda, bool scl_sda_pullup);
bool i2c_tx_timeout(uint64_t timeout_microseconds);
bool i2c_rx_timeout(uint64_t timeout_microseconds);
bool i2c_ck_nack();
void i2c_clr_intr();
uint8_t i2c_nack();
uint8_t i2c_abort();
uint8_t i2c_write(uint8_t slave_adr, uint8_t *write_data, uint8_t write_data_length, bool gen_stop);
uint8_t i2c_read(uint8_t slave_adr, uint8_t *read_data, uint8_t read_data_length, bool start);
uint8_t i2c_read_full(uint8_t slave_adr, uint8_t *reg_adr, uint8_t reg_adr_length, uint8_t *read_data, uint8_t read_data_length);
#endif
pico-test\module_i2c\module_i2c.c
#include "./module_i2c.h"
i2c_inst_t *reg_i2c = i2c0;
uint16_t i2c_scl_freq_kHz = 100;
uint8_t SDA_PIN = 20;
uint8_t SCL_PIN = 21;
uint8_t const RC_SUCCESS = 0xFF;
uint8_t const RC_TIMEOUT = 0x80;
uint8_t const RC_NACK = 0x00;
void i2c_scl_sda_pullup(bool enable_pullup){
gpio_set_pulls(SDA_PIN, enable_pullup, false);
gpio_set_pulls(SCL_PIN, enable_pullup, false);
}
void i2c_setup(i2c_inst_t *i2c_hw_block, uint16_t scl_freq_khz, uint8_t gp_for_scl, uint8_t gp_for_sda, bool scl_sda_internal_pullup){
reg_i2c = i2c_hw_block;
i2c_scl_freq_kHz = scl_freq_khz;
SCL_PIN = gp_for_scl;
SDA_PIN = gp_for_sda;
i2c_init(reg_i2c, i2c_scl_freq_kHz * 1000);
gpio_set_function(SDA_PIN, GPIO_FUNC_I2C);
gpio_set_function(SCL_PIN, GPIO_FUNC_I2C);
i2c_scl_sda_pullup(scl_sda_internal_pullup);
}
bool i2c_tx_timeout(uint64_t timeout_microseconds){
absolute_time_t timeTO = make_timeout_time_us(timeout_microseconds);
while(reg_i2c->hw->txflr > 0){
if(time_reached(timeTO)){
return true;
}
}
return false;
}
bool i2c_rx_timeout(uint64_t timeout_microseconds){
absolute_time_t timeTO = make_timeout_time_us(timeout_microseconds);
while(reg_i2c->hw->rxflr == 0){
if((reg_i2c->hw->status & 0x00000001) == 1){
timeTO = make_timeout_time_us(timeout_microseconds);
}
if(time_reached(timeTO)){
return true;
}
}
return false;
}
bool i2c_ck_nack(){
busy_wait_us_32((1000/i2c_scl_freq_kHz) * 10);
if((reg_i2c->hw->tx_abrt_source & 0x00000009) > 0){
return true;
}
return false;
}
void i2c_clr_intr(){
uint32_t uintTMP = reg_i2c->hw->clr_intr;
}
uint8_t i2c_nack(){
i2c_clr_intr();
return RC_NACK;
}
uint8_t i2c_abort(){
reg_i2c->hw->enable = 0x00000003;
while((reg_i2c->hw->enable & 0x00000002) > 0){
}
reg_i2c->hw->enable = 0;
reg_i2c->hw->enable = 1;
i2c_clr_intr();
return RC_TIMEOUT;
}
uint8_t i2c_write(uint8_t slave_adr, uint8_t *write_data, uint8_t write_data_length, bool gen_stop){
uint8_t cur;
uint32_t uintCMD;
reg_i2c->hw->enable = 0;
reg_i2c->hw->tar = 0x00000000 | slave_adr;
reg_i2c->hw->enable = 1;
for(cur = 0; cur < write_data_length; cur++){
uintCMD = write_data[cur];
if(cur == 0) uintCMD |= 0x00000400;
if((cur == (write_data_length -1)) && gen_stop) uintCMD |= 0x00000200;
reg_i2c->hw->data_cmd = uintCMD;
if(i2c_tx_timeout((1000/i2c_scl_freq_kHz)*100)){
return i2c_abort();
}
if(i2c_ck_nack()){
return i2c_nack();
}
}
return RC_SUCCESS;
}
uint8_t i2c_read(uint8_t slave_adr, uint8_t *read_data, uint8_t read_data_length, bool start){
int RC;
uint8_t cur = 0;
uint32_t uintCMD;
if(start){
reg_i2c->hw->enable = 0;
reg_i2c->hw->tar = 0x00000000 | slave_adr;
reg_i2c->hw->enable = 1;
}
for(cur = 0; cur < read_data_length; cur++){
uintCMD = 0x00000100;
if(cur == 0) uintCMD |= 0x00000400;
if(cur == (read_data_length-1)) uintCMD |= 0x00000200;
reg_i2c->hw->data_cmd = uintCMD;
if(i2c_rx_timeout((1000/i2c_scl_freq_kHz)*100)){
if(i2c_ck_nack()){
return i2c_nack();
}
else{
return i2c_abort();
}
}
read_data[cur] = reg_i2c->hw->data_cmd & 0xFF;
}
return RC_SUCCESS;
}
uint8_t i2c_read_full(uint8_t slave_adr, uint8_t *reg_adr, uint8_t reg_adr_length, uint8_t *read_data, uint8_t read_data_length){
uint8_t RC;
RC = i2c_write(slave_adr, reg_adr, reg_adr_length, false);
if(RC == RC_SUCCESS){
busy_wait_us_32((1000/i2c_scl_freq_kHz)*20);
return i2c_read(slave_adr, read_data, read_data_length, false);
}
else{
return RC;
}
}
pico-test\module_i2c\CMakeLists.txt
add_library(module_i2c
module_i2c.c
)
# Pull in our pico_stdlib which pulls in commonly used features
target_link_libraries(module_i2c pico_stdlib hardware_i2c)
改造した関数の解説
関数の中身を解説します。
void i2c_setup(i2c_inst_t *i2c_hw_block, uint16_t scl_freq_khz, uint8_t gp_for_scl, uint8_t gp_for_sda, bool scl_sda_internal_pullup){
reg_i2c = i2c_hw_block;
i2c_scl_freq_kHz = scl_freq_khz;
SCL_PIN = gp_for_scl;
SDA_PIN = gp_for_sda;
i2c_init(reg_i2c, i2c_scl_freq_kHz * 1000);
gpio_set_function(SDA_PIN, GPIO_FUNC_I2C);
gpio_set_function(SCL_PIN, GPIO_FUNC_I2C);
i2c_scl_sda_pullup(scl_sda_internal_pullup);
}
I2C を使うための初期設定を一つにまとめたものです。
最初の reg_i2c と i2c_scl_freq_kHz の二つの変数はこの関数の中だけではなく改造した Read/Write 関数でも参照するため、ここで適切な値を代入しておく必要があります。
i2c_init と gpio_set_function は SDK の関数をそのまま使用しているだけで、それぞれ I2C HW block の初期化と、指定した GPIO 端子の機能を設定する関数となっています。
i2c_scl_sda_pullup は GPIO 端子の内蔵 Pull-up の有効/無効を設定する関数で、中身は SDK の関数となっています。
uint8_t i2c_write(uint8_t slave_adr, uint8_t *write_data, uint8_t write_data_length, bool gen_stop){
uint8_t cur;
uint32_t uintCMD;
reg_i2c->hw->enable = 0;
reg_i2c->hw->tar = 0x00000000 | slave_adr;
reg_i2c->hw->enable = 1;
for(cur = 0; cur < write_data_length; cur++){
uintCMD = write_data[cur];
if(cur == 0) uintCMD |= 0x00000400;
if((cur == (write_data_length -1)) && gen_stop) uintCMD |= 0x00000200;
reg_i2c->hw->data_cmd = uintCMD;
if(i2c_tx_timeout((1000/i2c_scl_freq_kHz)*100)){
return i2c_abort();
}
if(i2c_ck_nack()){
return i2c_nack();
}
}
return RC_SUCCESS;
}
Write 関数です。中身を順番に追っていきます。
reg_i2c->hw->enable = 0;
reg_i2c->hw->tar = 0x00000000 | slave_adr;
reg_i2c->hw->enable = 1;
reg_i2c は変数で、RP2040 が持つ二つの I2C HW block (i2c0, i2c1) のどちらかを代入します。reg_i2c への値の代入は先の i2c_setup 関数で行っています。
reg_i2c->hw->xx は Raspberry pi pico の本体のマイコンである RP2040 のレジスタにアクセスするための記述です。(この記述でアクセスできるように pico-sdk が用意してくれています。) 例えば reg_i2c->hw->enable で I2C HW block の IC_ENABLE (I2C HW block が i2c0 の場合は 0x40044000 + offset 0x6C) のレジスタへのポインタとなり、この記述によって該当するレジスタの値を読み出したり、逆に値を書き込んだりできるようになっています。
最初に reg_i2c->hw->enable に 0 を代入して I2C HW block を Disable にします。RP2040 のデータシートによると、このレジスタの値 (厳密には 0 番目の bit の値) を 0 にすると I2C HW block が Disable になり、コマンドを蓄える TX Buffer や Slave から読み出したデータを蓄える RX Buffer、Status bit がクリアされます。
次に reg_i2c->hw->tar に Write を行う相手の Slave address を代入します。ここで代入する Slave address は R/W bit を除いた 7-bit です。
最後に reg_i2c->hw->enable に 1 を代入して Enable にします。
for(cur = 0; cur < write_data_length; cur++){
uintCMD = write_data[cur];
if(cur == 0) uintCMD |= 0x00000400;
if((cur == (write_data_length -1)) && gen_stop) uintCMD |= 0x00000200;
reg_i2c->hw->data_cmd = uintCMD;
if(i2c_tx_timeout((1000/i2c_scl_freq_kHz)*100)){
return i2c_abort();
}
if(i2c_ck_nack()){
return i2c_nack();
}
}
これら記述では Slave に書き込むデータを 1-byte ずつ処理しています。
I2C のコマンドの指示は reg_i2c->hw->data_cmd への値の書き込みによって行います。
8-bit 目が Master Write/Read の指示で、ここに 0 を書き込むと Master Write, 1 を書き込むと Master Read の指示となります。
Master Write を指示する場合、7-0 bit に Slave に書き込むデータを代入します。
10-bit 目は、このコマンド指示を実行する前に Start condition を生成するかどうかの指示で、1 を書き込むと SDA/SCL が Start condition を生成します。
9-bit 目は、このコマンド指示を実行した後に Stop condition を生成するかどうかの指示で、1 を書き込むと SDA/SCL が Stop condition を生成します。
記述では reg_i2c->hw->data_cmd に書き込む値として unitCMD という変数を用意して 7-0 bit に Slave に書き込むデータを代入し、必要に応じて 10-bit 目 と 9-bit 目の値に 1 を加えています。8-bit 目は触っていないので 0 のままです。
reg-i2c->hw->data_cmd に unitCMD を代入すると i2c_tx_timeout で data_cmd に書き込んだコマンド指示が実行開始されるのを待ち、一定時間 (ここでは SCL x 100 clock 分に設定)、実行開始されない場合は Timeout として i2c_abort 関数を呼び出します。実行開始された場合、今度は i2c_ck_nack で NACK の検出がなかったかを確認し、NACK が検出されていれば i2c_nack を呼び出します。i2c_aboart と i2c_nack はそれぞれ中断処理を行った後に Timeout または NACK のエラーコードを返します。Timeout にも NACK にもひっかからなかった場合は for loop の最初に戻って次のデータの処理を行います。
bool i2c_tx_timeout(uint64_t timeout_microseconds){
absolute_time_t timeTO = make_timeout_time_us(timeout_microseconds);
while(reg_i2c->hw->txflr > 0){
if(time_reached(timeTO)){
return true;
}
}
return false;
}
Timeout の判定です。
make_timeout_time_us は SDK の関数で、関数呼び出し時点の RP2040 の内部カウンタ (時計) の値に引数分の時間 (us) に相当する値を足した値を返します。記述ではその値を timeTO という変数に代入し、while loop の中で time_reached という関数がその値を参照しています。
time_reached も SDK の関数で、RP2040 の内部カウンタの値が引数の値を越えると true, それ以前は false を返します。
この記述では while loop で i2c_reg->hw->txflr の値を見張っています。このレジスタは I2C のコマンドバッファに蓄えられたコマンドの数を示しています。I2C のコマンドバッファには reg_i2c->hw->data_cmd に代入したコマンドが蓄えられるので、ここでは事前にコマンドバッファに送ったコマンドの処理が開始されて txflr が空になる (値が 0 になる) のを待っています。txflr が空になる前に time_reached が true を返すと Timeout と判定します。
ちなみに SDK の I2C 関数では i2c_reg->hw->status の 3-bit 目の値 (コマンドバッファが空になると 0, コマンドを一つ以上蓄えていると 1) で判定していましたが、見ているものは同じです。
bool i2c_ck_nack(){
busy_wait_us_32((1000/i2c_scl_freq_kHz) * 10);
if((reg_i2c->hw->tx_abrt_source & 0x00000009) > 0){
return true;
}
return false;
}
因縁の NACK の判定です。
SDK の I2C 関数が NACK を判定できなかったのは NACK 検出の bit の値が立つまでのライムラグが原因だったので、ここでは最初に待ち時間を設定しています。
busy_wait_us_32 は SDK の関数で、引数分の時間 (us) だけ処理待ちをします。ここでは待ち時間を SCL x 10 clock 分、大雑把に言うと 1-byte 分の通信時間としています。
待ち時間が経過したら reg_i2c->hw->tx_abrt_source の値を確認し、NACK に相当する bit (4-bit 目と 0-bit 目) が 1 になっていたら NACK と判定します。
Read 関数も改造していますが、同じような内容なので説明は省略します。
動作確認
それでは動作確認です。 SDK の関数の動作確認と同じように Slave device 用 Sketch を書き込んだ Arduino を Slave として接続して、改造した関数の動作を確認します。Raspberry pi pico は PC に接続して、Arduino IDE のシリアルモニタで通信します。Arduiono nano を PC に接続しているのは電源のためなので、電源供給してくれるものであれば PC でなくても構いません。
シリアルモニタから Raspberry pi pico に "1", "a", "2", "a", "5", "a" の順で文字を送った結果が以下になります。
この動作確認用の記述は、 0 から 9 までの数字を送ると Raspberry pi pico が I2C で Slave address の Register address = 0x00 に受け取った値を書き込み、それ以外の文字を送ると Raspberry pi pico は I2C で同 Register の値を読み出して、読み出した値をシリアルモニタに返します。
"1", "2", "5" に対しては "P" が返ってきていますが、これは Raspberry pi pico が I2C で送った数字を書き込もうとした結果を示していて、"P" は Pass、つまり成功を意味しています。失敗した場合は "F"ail でした。ちなみに、SDK の関数の場合は Raspberry pi pico が Slave device に送ったデータの数 (Byte) が返ってきていましたが、改造したこの関数ではその部分を変更しています。
"a" に対しては Raspberry pi pico が I2C で読み出した値が返ってきています。対象 Register の値は直前に書き込んだばかりなので、その書き込んだ値が正しく読み出せていることが分かります。
というわけで、読み書きに関して無事に動作していることが確認できました。
次は異常時ということで、Slave device の接続を外した状態で "1", "a" を 1文字ずつ送った結果が以下になります。
どちらの文字に対しても "F" つまり Fail が返ってきました。"1" に対しては i2c_write 関数, "a" に対しては i2c_read 関数が動作しますが、どちらも問題なく NACK を検出できたようです。SDK の関数の場合はここで write が NACK を検出できなくて、それがきっかけで関数の改造に手を付けることとなったのですが、これにて無事に目的達成です。
最後にもう一つ、SDA/SCL を GND に短絡した状態で "1", "a" を 1文字ずつ送った結果です。
SDA/SCL が GND に短絡していると Raspberry pi pico は他の誰かが I2C 通信のために SDA/SCL を使っていると考えて待機するのですが、そのまま一定時間が過ぎると Timeout と判断して諦めます。
"1", "a" のどちらに対しても "T" が返ってきています。つまり、i2c_write 関数も i2c_read 関数も Timeout の判定ができたということで、動作確認OK となります。
というわけで改造は完了です。
肝心の NACK の判定待ちが決め打ちの待機時間の挿入なので、待ち時間がまだ足りなくて NACK を検出できないという可能性はあるかもしれないのですが、今はこれでよしとします。そのうちもっと良いアイデアを思い付いたらまた改造します。