MPU9250の地磁気センサをSPIで読む

はじめまして、覚醒ゆうたマンです。
Qiita初投稿になります。マナー違反などあればそっと教えてくださると幸いです。

さて、題名の通りMPU9250の地磁気センサをSPI通信で読み取りたいわけですが、MPU9250の仕様上めんどくさく、実際に行ってる事例がネットで見つからなかったのでデータシートと格闘して頑張ってきたのでその成果をここに書いておこうと思います。
信頼性の薄い適当な記事よりデータシートをお望みの方はこちらへ(英語読めるならこの記事読んだ後こっちで確認することをおすすめします)
https://strawberry-linux.com/catalog/items?code=12250

この記事ではFIFOとFSYNC(外部割込み用のピン？)は使わない想定で行きます。
以下の流れで話を進めていこうと思います。とりあえず動けばいいやという人はコードだけ見とけば良いと思います。

- MPU9250の内部構成
- MPU9250のI2Cマスターの動作
- MPU9250のI2Cマスターの設定レジスタ
- 地磁気センサ初期化手順
- 実際のコード

#MPU9250の内部構成
そもそもMPU9250が地磁気まで自分で読んでSPI出力でデータを返していれば僕がデータシートと格闘してこんな記事を書くこともなかったわけですが、残念ながらもう少し複雑な構造になっています
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/853544/063909c4-90b1-dc43-a2b4-61af1b96f015.png)
MPU9250のデータシートからとってきた内部構成図です。MPU9250は6軸センサ＋地磁気センサ(旭化成のAK8963)という構成になっています。I2Cで読む人の多くはSerialInterfaceBypassMuxを外部のI2Cに切り替え、I2CバスからMPU9250とは別のスレーブとして地磁気センサと通信を行います。しかしSPI通信で通信を行うときはこれはできません。
SPI通信でMPU9250を使うならSerialInterfaceBypassMuxをMasterI2CSerialInterfaceのほうに切り替え、内部のI2Cマスターモジュールを動かすことになります。これがI2Cで地磁気センサと通信し、地磁気の測定結果を専用のレジスタに保存してくれます

#MPU9250のI2Cマスターの動作
MPU9250のI2Cマスターは、ジャイロや加速度の値の更新レートに合わせて設定された通信を繰り返します。この通信はあらかじめ設定したスレーブデバイスの設定したアドレスから設定したバイト数だけ読み取る、または設定したデータを書き込むという形で行われます。読み取り、または書き込み先のレジスタを対象のスレーブに送信し、それからデータの書き込みか読み取りを行います。このスレーブデバイスは最大5個まで登録できるようですが、5個目のスレーブ4は1バイトしかデータの読み書きができません。今回は全ての動作をスレーブ0で行いたいと思います。
初期設定などのときは何度も同じレジスタに同じデータを書き込んでも面白くないので内部の割り込み機能を使うなどすればスムーズに通信ができると思います(この記事でもその機能を使って初期化を行います)。

#MPU9250のI2Cマスターの設定レジスタ
I2Cマスターの設定レジスタと書きましたが全体機能も使用するのでそこら辺についても説明します。
今回設定するレジスタは

・Sample Rate Divider
・Configuration
・Gyroscope Configuration
・I2C Master Control
・I2C_SLV0_ADDR
・I2C_SLV0_REG
・I2C_SLV0_CTRL
・INT Pin/Bypass Enable Configuration
・Interrupt Enable
・I2C_SLV0_DO
・User Control
の11個のレジスタを設定します。この辺設定するだけでSPIで地磁気が読めます。やるしかないですね。通信速度で~~お気楽~~I2C勢と差をつけましょう。

__Sample Rate Divider__(0x19)
サンプルレートからデータ更新レートを決定します。データ更新レートでI2Cマスターが動きます。
(出力レート)=(サンプルレート)/(SMPLRT_DIV + 1)
__Configuration__(0x1A)
次のように各ビットに機能が振られてます

<table>
<tr>
<th>BIT</th><th>Name</th><th>機能</th>
</tr>
<tr>
<td>[7]</td><td>-</td><td>-</td>
</tr>
<tr>
<td>[6]</td><td>FIFO mode</td><td>今回は気にしない。
FIFOの上書きどうするか</td>
</tr>
<tr>
<td>[5:3]</td><td>EXT_SYNC_SET</td><td>外部割込み使わないので0</td>
</tr>
<tr>
<td>[2:0]</td><td>DLPF_CFG</td><td>後述するFchoice_bと合わせて
サンプルレートの決定に使用</td>
</tr>
</table>

内部サンプルレートFsとジャイロのバンド帯は下の表のように決まります

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/853544/c1746f7f-b472-fe0c-0254-73a1a27330ee.png)
Fchoiceは次のGyro Configurationレジスタの下位2ビットで決まりますが、レジスタに書き込む値は上の表のFchoiceの値を反転させたものであることに気を付けてください(僕は引っかかった)。

__Gyro Configuration__(0x1B)

<table>
<tr>
<th>BIT</th><th>Name</th><th>機能</th>
</tr>
<tr>
<td>[7]</td><td>XGYRO_Cten</td><td>X Gyro self-test</td>
</tr>
<tr>
<td>[6]</td><td>YGYRO_Cten</td><td>Y Gyro self-test</td>
</tr>
<tr>
<td>[5]</td><td>ZGYRO_Cten</td><td>Z Gyro self-test</td>
</tr>
<tr>
<td>[4:3]</td><td>GYRO_FS_SEL</td><td>ジャイロレンジ＝250*2^(GYRO_FS_SEL+1)[dps]</td>
</tr>
<tr>
<td>[2]</td><td>-</td><td>-</td>
</tr>
<tr>
<td>[1:0]</td><td>FChoice_b</td><td>Fchoiceを反転した値</td>
</tr>
</table>

__I2C MasterControl__(0x24)

<table>
<tr>
<th>BIT</th><th>Name</th><th>機能</th>
</tr>
<tr>
<td>[7]</td><td>MULT_MST_EN</td><td>正直よくわからないが0で動いた。</td>
</tr>
<tr>
<td>[6]</td><td>WAIT_FOR_ES</td><td>I2Cマスタがデータ取り終わるまでデータレディ割り込みを遅らせる。
有効化するべき</td>
</tr>
<tr>
<td>[5]</td><td>SLV_3_FIFO_EN</td><td>FIFOつかわない→０</td>
</tr>
<tr>
<td>[4]</td><td>I2C_MST_P_NSR</td><td>１つのスレーブとの通信が終わったときにストップコンディションを入れるか
リスタートコンディションを入れるか。
０→リスタート
</td>
</tr>
<tr>
<td>[3:0]</td><td>I2C_MST_CLK</td><td>下の表のようにマスタークロックの周波数を決定
</td>
</tr>
</table>
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/853544/1d9d7fd0-2faf-7787-c38f-3b4ee8f34730.png)

__I2C_SLV0_ADDR__(0x25)
スレーブ0のスレーブアドレス
読み取りの際だけ最上位ビットを1にします
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/853544/4ae3a689-92ce-c9a2-e684-32b3c2f9ec00.png)

__I2C_SLV0_REG__(0x26)
スレーブ0の読み取り開始または書き込み先のレジスタ
__I2C_SLV0_CTRL__(0x27)

<table>
<tr>
<th>BIT</th><th>Name</th><th>機能</th>
</tr>
<tr>
<td>[7]</td><td>I2C_SLV0_EN</td><td>スレーブ0有効化</td>
</tr>
<tr>
<td>[6]</td><td>I2C_SLV0_BYTE_SW</td><td>データを2ビット1組で入れ替えるかどうか。
1→入れ替える</td>
</tr>
<tr>
<td>[5]</td><td>I2C_SLV0_REG_DIS</td><td>I2C_SLV0_REGつかわない→１</td>
</tr>
<tr>
<td>[4]</td><td>I2C_SLV0_GRP</td><td>データスワップでデータ入れ替えるときのペアの決め方。０ならデータ配列のインデックスが0,1/2,3/4,5/...の組で入れ替えが行われ、1なら0/1,2/3,4/...で行われる。地磁気のデータをステータスレジスタ含めて読み取り、データスワップまでしたいなら使うと思う(僕は使いませんでした)。
</td>
</tr>
<tr>
<td>[3:0]</td><td>I2C_SLV0_LENG</td><td>スレーブ0から読み取るバイト数
</td>
</tr>
</table>

スレーブ3まで全く同じようなレジスタが繰り返されます
スレーブ４だけ機能が違うのでちょっとレジスタの中身が変わります。スレーブ４は初期設定時に使うことを想定しているのか、通信完了割り込みが別で用意されているみたいです。ただ、これを有効化するためにI2Cマスター割り込みを有効化しないといけないのですがこれの設定用のビットが見つからなかったです。データシートしっかりして…
まあデータレディ割り込みをI2Cデータ準備後にかかるようにすればスレーブ0でも初期設定用に十分使えるので(書き込みにしとけば書き込んだ後に割り込みが入る)、読み書き全部それでやればいいと思います

__INT Pin/Bypass Enable Configuration__(0x37)
たぶんI2Cマスターを有効化した時点で内部の回路の切り替えがMPU9250のI2CバスとI2CマスターのI2Cバスを分離すると思うのですが(一応そう書いてるし)、念のためBypassモードを切っておきます(逆にI2Cで使うならここを設定しないといけないはず)。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/853544/7b5825f4-92f9-466d-3542-328dc202fbda.png)
ピン出力使わないなら[1]ビットだけ気にすればいいと思います。
一応割り込みが発生するたびにINTピンの出力がハイになるのでマイコンをそれで駆動すればデータ取り立てほやほやの状態でデータを読めていいと思います。

__Interrupt Enable__(0x38)
第0ビットだけ有効化しましょう(データレディ割り込み)。それ以外は使っても面白くなさそう(エラー検知とかするなら便利かも)

__I2C_SLV0_DO__(0x63)
I2Cマスターからデータを書き込むときはここから1バイト書き込みます

__User Control__
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/853544/68de5d6b-8170-dcc4-2be9-44e37324fcd3.png)
[5]ビットがI2Cマスター有効化ビットです。ここは確実に1にしましょう
[4]のI2Cスレーブモジュール無効化のビットで、どこかにSPIで通信するなら設定しとけって書いてあった気がするのでここは1のほうがいいと思います。
あとは0でいいと思います

#地磁気センサ初期化手順
基本的な流れとしては
MPU9250の初期化→MPU9250のI2C設定→I2C書き込み完了をポーリングで監視→次のデータを設定→設定完了までループ→I2Cを読み取り用に設定
という感じで僕は地磁気センサを初期化しました。1つだけ分からないのが、書き込みが完了したあとスレーブ0のアドレスを読み取り用に変更してデータを読み取るよう変更しても、毎度データを書き込んでからデータを読み取るという動作をしていました(ロジアナでECL,EDAピンを測定して判明)。適当な読み取り専用レジスタにデータを書き込ませてからデータを読み始めると直ったのですが、訳が分かりません。~~まあ動いたのでﾖｼ!~~　誰か知ってたら教えて欲しいです。
地磁気センサAK8963の設定項目は測定モードと分解能くらいしかないです。普通は100Hz繰り返し測定モード(8Hzか100Hzの2択)で分解能16bit(14bitか16bitしかない)を選択すると思うのでコントロールレジスタ(0x0A)に0x16を書き込むことになると思います。
ここで事細かに設定手順を説明しようかと思いましたが残りはコード見た方が早いと思うのでそちらをご参照ください。esp32のコードですがノリと雰囲気は分かると思います。


#実際のコード
一応は動作確認をとっていますが責任はなにも負いません。

```main.c
/* MPU9250 example

   This example code read 9-axis via spi
*/
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "esp_system.h"
#include "driver/spi_master.h"


#define GPIO_SPI_MISO 12
#define GPIO_SPI_MOSI 13
#define GPIO_SPI_CLK  14
#define GPIO_SPI_CS   15

#define MAX_DATA_SIZE 30

uint8_t rx_buf[MAX_DATA_SIZE];
uint8_t tx_buf[MAX_DATA_SIZE];
spi_transaction_t tr;
spi_device_handle_t spi;

float accl[3],gyro[3],mag[3];

void MPU9250_read_bytes(uint8_t reg,uint8_t len);
void MPU9250_write_bytes(uint8_t reg,uint8_t len);
uint8_t MPU9250_read_byte(uint8_t reg){
	MPU9250_read_bytes(reg,1);
	return rx_buf[0];
}
void MPU9250_write_byte(uint8_t reg,uint8_t data){
	tx_buf[0]=data;
	MPU9250_write_bytes(reg,1);
}
void MPU9250_init(){
	printf("init spi bus\n");
	//init spi bus

    esp_err_t ret;
    spi_bus_config_t busconf;
    memset(&busconf,0,sizeof(busconf));
    busconf.miso_io_num=12;
    busconf.mosi_io_num=13;
    busconf.sclk_io_num=14;
    busconf.quadhd_io_num=-1;
    busconf.quadwp_io_num=-1;
    busconf.max_transfer_sz=30;
    //Initialize the SPI bus
    ret=spi_bus_initialize(HSPI_HOST, &busconf, 1);
    ESP_ERROR_CHECK(ret);
    printf("sccess\n");

    spi_device_interface_config_t devconf;
	memset(&devconf, 0, sizeof(devconf));
    devconf.clock_speed_hz=1000000;//1MHz
    devconf.mode=3;
    devconf.spics_io_num=GPIO_SPI_CS;
    devconf.address_bits=8;
    devconf.queue_size=7;//	てきとー
    ret=spi_bus_add_device(HSPI_HOST, &devconf, &spi);
    ESP_ERROR_CHECK(ret);
    printf("finish add read device\n");

    if(MPU9250_read_byte(0x75)!=0x71){
    	printf("No mpu9250 on the spi bus\n");
    }
    printf("mpu9250 connected\n");

    //割り込み設定
    //INT_PIN:ActiveHigh&PP&output 50us pulse, 全ての読み取りで割り込みクリア, FSYNCピン割り込み無効,バイパス無効
    MPU9250_write_byte(0x37,0x00);
    //データレディ割り込みだけ
    MPU9250_write_byte(0x38,0x01);
    //sample_rate=internal_Fs(1kHz)/10=100Hz
    MPU9250_write_byte(0x19,9);
    //内部サンプルレート1kHz,ジャイロバンド帯92Hz//Fchoice_bはあとでちゃんと有効かすること
    MPU9250_write_byte(0x1A,0x02);
    //加速度サンプルレート1kHz,バンド帯92Hz
    MPU9250_write_byte(0x1D,0x08|0x02);
    //FIFO無効
    MPU9250_write_byte(0x23,0x00);
    //FIFO無効,I2Cマスタ有効,I2C_SLAVE機能無効,I2Cインターフェース無効,各種リセットなし
    MPU9250_write_byte(0x6A,0x30);
    //全センサ全軸有効化
    MPU9250_write_byte(0x6C,0x00);
    //加速度レンジ-2g～+2g
    MPU9250_write_byte(0x1C,0x00);
    //ジャイロレンジ±2000dps
    MPU9250_write_byte(0x1B,(0x03<<3));
    //I2C設定
    MPU9250_write_byte(0x24,0x4D);//I2Cマスタ設定、外部センサーロード待ち有効,SCL=400kHz
    //I2C_SLV0設定
    MPU9250_write_byte(0x25,0x0c);//I2Cスレーブアドレス
    MPU9250_write_byte(0x26,0x0A);//スレーブ書き込み先レジスタ
    MPU9250_write_byte(0x63,0x16);//書き込みデータ、AK8963モード＝16bit連続測定100Hz
    MPU9250_write_byte(0x27,0x98);//スレーブ0コントロール：SLV0有効
    MPU9250_read_byte(0x3A);//受信割り込みフラグクリア
    //データレディー受信割り込みフラグをポーリングで監視
    while(!(MPU9250_read_byte(0x3A)&0x01)){
    	vTaskDelay(1);
    }
    MPU9250_write_byte(0x26,0x00);//データ書き込み先を適当な読み取り専用レジスタへ
    MPU9250_write_byte(0x63,0xff);//適当なデータを書き込みこれないとなんか書き込みがストップしない
    while(!(MPU9250_read_byte(0x3A)&0x01)){
    	vTaskDelay(1);
    }
    //I2CMasterReset
    MPU9250_write_byte(0x6A,0x36);//要らない気がする
    //I2C_SLV0設定
    MPU9250_write_byte(0x25,0x0c|0x80);//スレーブアドレスを読み取り用へ
    MPU9250_write_byte(0x26,0x02);//ST1レジスタから読み出し
    MPU9250_write_byte(0x27,0x98);//SLV0有効
    while(!(MPU9250_read_byte(0x3A)&0x01)){
    	vTaskDelay(1);
    }

}
void MPU9250_read_data(){
	MPU9250_read_bytes(0x3b,22);//MPU9250から加速度6バイト,温度2バイト,ジャイロ2バイト,地磁気8バイト
	int accl_raw_data[3];
	for(uint8_t i=0;i<3;++i){
		accl_raw_data[i]=(int)((((uint16_t)rx_buf[2*i])<<8)+rx_buf[i*2+1]);
		if(accl_raw_data[i]>32767)accl_raw_data[i]-=65536;
		accl[i]=(float)accl_raw_data[i]/32767.0f*2;//レンジ±2g
	}
	int gyro_raw_data[3];
	for(uint8_t i=0;i<3;++i){
		gyro_raw_data[i]=(int)((((uint16_t)rx_buf[2*i+8])<<8)+rx_buf[i*2+9]);
		if(gyro_raw_data[i]>32767)gyro_raw_data[i]-=65536;
		gyro[i]=(float)gyro_raw_data[i]/32767.0f*2000*3.1416/180;//レンジ±2000dps,rad/sに変換
	}
	int mag_raw_data[3];
	for(uint8_t i=0;i<3;++i){
		mag_raw_data[i]=(int)((((uint16_t)rx_buf[2*i+16])<<8)+rx_buf[i*2+15]);//加速度、地磁気と上位バイトと下位バイトの順序が逆
		if(mag_raw_data[i]>32767)mag_raw_data[i]-=65536;
		mag[i]=(float)mag_raw_data[i]*0.6f;//16ビット測定モードで分解能0.6uT
	}
}

void app_main()
{
	MPU9250_init();
	while(1){
		MPU9250_read_data();
		printf("accl=%f,%f,%f,gyro=%f,%f,%f,mag=%f,%f,%f\n",accl[0],accl[1],accl[2],gyro[0],gyro[1],gyro[2],mag[0],mag[1],mag[2]);
		vTaskDelay(100);
	}
}

void MPU9250_read_bytes(uint8_t addr, uint8_t len){
	memset(&tr,0,sizeof(tr));
	tr.rx_buffer=rx_buf;
	tr.tx_buffer=NULL;
	tr.addr=addr|0x80;
	tr.length=8*len;
	tr.rxlength=8*len;
    assert(spi_device_polling_transmit(spi, &tr)==ESP_OK);            // Should have had no issues.
}
void MPU9250_write_bytes(uint8_t addr, uint8_t len){
	memset(&tr,0,sizeof(tr));
	tr.rx_buffer=NULL;
	tr.tx_buffer=tx_buf;
	tr.addr=addr;
	tr.length=8*len;
    assert(spi_device_polling_transmit(spi, &tr)==ESP_OK);            // Should have had no issues.
}




```