STM32で外付けADC ADS1015をうごかしてみた
久しぶりの更新です.本当は鳥人間が終わったあと,夏くらいから更新再開しようかな(というかそれくらいに更新できる進捗出せるかな)と思っていたのですが,コロナのおかげで鳥コンが消し飛び,しかも自宅待機で時間ができてしまったので,進捗を出してみました.まあ,鳥コンなくなったのは残念なのですが,逆にこれを利用して一つチームの電装の技術レベルを上げられたらなあと思います.今回はその一環としての記事です.ついでに自分がマイクロマウスを作るときの練習としてもやっています.
今回のテーマは「STM32でI2C通信をしてみる」です.マイクロマウスはともかく,鳥人間ではセンサーにI2C通信が採用されているものが多く,またそこそこの距離の有線通信が可能という特徴から,鳥電装界隈では基本の通信方式だと思います.今回はそれをADS1015というADCで使用して見ました.ADCも鳥人間ではしょっちゅう使う道具ですね.長いケーブルにアナログ信号流すのはさすがにまずいですから.ということでやっていきましょう.
0.IDEについて
更新をしていなかった間にIDEが変わるという大事件がおきました.これまではSystemWorkbenchForSTM32(SW4STM32)を使っていましたが,これは現在新規導入非推奨になっています.変わって,現在公式が推しているIDEはSTM32CubeIDEというIDEです.これは初期化コード自動生成ツールのCubeMXと,もう一つの公式IDEであるTrueStudioとを統合したものになっています.使用する感覚はCubeMXとSW4STM32を使っていたころとほとんど変わらず,ソフトの重さや不具合も少なくなったように感じます(最もSW4STM32でトラブルが頻発したのは私の固有の問題な気はしますが…).特に絶対にこれは残そう!というコードもなかったので,今後はこちらで開発を行っていきます.導入や具体的な使用方法はググればいくらでも出てきますし,「STM32を始める本~CubeIDE対応版~」(ゆーくりっど著,技術書展7にて配布)にわかりやすくまとまっています.Boothにて販売されているようです.細かい使用方法についても多少は説明を挟みます.
1.準備するもの
今回使用したものは以下のようになっています.
開発環境
- IDE:STM32CubeIDE 1.3.0
電子部品はすべて秋月電子通商にて調達しました.ADCはADS1015を使用します.表面実装部品ですが,秋月電子にはDIP化キットのみが販売されています.Texas Instruments製で,シリーズのADCがたくさんあります.分解能や変換できるチャンネル数,PGA機能の有無や通信方式などの違いがあります.レジスタマップがよく似ているので,それらを使用する際にも参考になると思います.ADS1015は分解能が12bit,サンプリングレートが3300SPSのもので,4chの変換が可能です.PGA(平たく言うと読み取れる電圧範囲を変える機能)も搭載しています.I2C通信で操作できます.もっとも,使用するのは1chだけなのですが….一番手軽に手に入ったのでしょうがないですね.
最後に適当な可変抵抗を用意します.私はジョイスティックを使いましたが,これは何でもいいです.動作確認ができれば問題なし!です.
2.配線
配線の対応は表2.1のようになります.特に何も考えることはありません.今回使用したのは秋月で売っているADS1015のDIPモジュールなので,1kΩのプルアップ抵抗が最初からついています.販売時は接続されていませんが,はんだ付けをしてプルアップすることができます.今回はこれを利用しているので,I2C通信に必須なプルアップ抵抗を省略しています.
なお,図2.1ではADS1015のADR(4)に+3V3を入力しています.これによってADS1015のI2Cアドレスを0x49に変えています.そのほかのアドレスにしたい場合は,表2.1にあるように,GNDやSDA,SCLを入力してください.
表2.1 配線の対応
Nucleo-F303K8 | ADS1015 |
---|---|
PB7(D4) | 4(SDA) |
PB6(D5) | 3(SCL) |
+3V3 | 1(VIN) |
GND | 2(GND) |
GND(0x48)/+3V3(0x49)/PB7(0x4a)/PB6(0x4b) | ADR |
3.プログラム作成
本題中の本題です.英語のブログも探しましたが,ADS1015を使っている記事は確認できませんでした.ただ,似た製品のADS1115を使用している記事はありました.ADS1115はADS1015とは違い,分解能を16bitにした代わりにサンプリングレートを860SPSまで落としている製品で,レジスタマップはそっくりです.また,ADS1015を使用するArduinoライブラリも存在します.今回はこれらを参考にしてコードを作成しました.
参考文献はこちら
ADS1115 with STM32 CubeMx - Thecodeprogram https://thecodeprogram.com/ads1115-with-stm32-cubemx
adafruit/Adafruit_ADS1X15-GitHub https://github.com/adafruit/Adafruit_ADS1X15
3.1.初期設定
まずはcubeMXの設定(GUI)から.新しいプロジェクトを用意します.このときTargeted Project TypeをSTM32Cubeで設定することを忘れないように.意図的に忘れた人はブラウザバックをお勧めします.画面がDevice configuration Toolになったら,左側のペリフェラルが一覧になっているリストの中から,Connectivity->I2C1を選択します.すると,I2C1 Mode and Configurationという画面が,リストの横に出てきます.上の段のModeの中から,I2Cを選択します.下側のI2CのConfigurationですが,ひとまずデフォルトの設定のままで大丈夫です.一応,設定一覧(表3.1)を書いておきます.
このうち,人によって変えることがあるのはI2C Speed Modeくらいでしょうか.基本的にはStandard(100kbps)を使うと思いますが,ほかにFast Mode(400kbps),Fast Mode Plus(1Mbps)がF303K8では選択できます.コアを変えれば,さらにHigh Speed Mode(3.4Mbps)も選択できます.ADS1015はStandardとFast Modeはそのまま使え,High Speed Modeは最初にactivateすれば利用できます.今回使用するF303K8ではFast Modeまで使え,特に仕様変更は必要ないことがわかります.High Speed Modeが使いたい人はそこはデータシートから頑張って作ってください.
表3.1 I2C1設定
Timing configuration | Slave Features | ||
---|---|---|---|
I2C Speed Mode | Standard Mode | Clock No Strech Mode | Disabled |
I2C Speed Frezuency(kHz) | 100 | General Call Address Detection | Disabled |
Rise Time(ns9) | 0 | Primary Address length selection | 7-bit |
Fall Time(ns) | 0 | Dual Address Acknowledged | Disnabled |
Coefficient of Digital Filter | 0 | Primary slave address | 0 |
Analog Filter | Enabled | ||
Timing | 0x2000090E |
設定ができれば,図3.1のようにPB7とPB6が緑色になります.PB7がI2C1-SDA,PB6がI2C1-SCLとなります.今回はADCしか使わないので他のペリフェラルはすべて起動しませんが,ほかの素子と併用する際は,この2つのピン以外のピンを使ってください. …実はここに落とし穴があります.実は,PB7,PB6以外にもI2C1を使う際に使ってはいけないピンが存在します.私と同じ,Nucleo-F303K8ボードを買ったままの状態で使う際,PA11とPB5に設定を入れてはいけません.「STM32 Nucleo-32 boards User manual」によると,工場出荷の状態ではPA5とPB7が,PA6とPB6とがはんだブリッジ
で接続されているためです.搭載されているSTM32F303K8T6のI2CはPB6,7に設定されているのですが,Arduino Nanoと互換性を持たせるため,こんな配線をしたのだと思われますが….とんでもなく使いづらくしてくれました.これによりPA5,6を使用するSPI通信はcubeIDEでは不可能になります(Arduinoとして使う分には,使うピンが違うらしく問題なさそうです).これは面倒な制約だ….一応,解決策もあります.ここのはんだジャンパーを切断してしまえばいいのです.ユーザーマニュアル添付の回路図を見ると,対応するのはSB16,SB18という二つのジャンパー.これは裏面についている,2つの0Ω抵抗のことです(図3.2内の赤枠).こいつを外せば,SPI通信も同時にできるようになるはずです(実証はしていません).当初,私はここにLEDを付けるためGPIOを設定していて,これが原因でI2C通信が正常に動かず,1週間近くとかしました.マイコンを触るときには,必ずデータシートを読みましょう….あと,これを疑ったのはSTの英語版FAQに記載があったためです.同じようなトラブルは必ず発生しているので,海外のFAQ等も確認したほうがよいでしょう. 設定できたらCode Generateをしましょう.今回は分割コンパイルもやってみたかったので,Project Manager->Code Generator->Generated files->General peripheral initialization as a pair of '.c/.h' files per peripheralにチェックを入れました.これで分割コンパイルもできるようになります.
3.2.ADCを動かす関数
Code Generateすると,プロジェクトの中のCodeの中にある,IncとSrcにたくさんの.c/.hファイルが入っています.これらのファイルには設定したペリフェラルに関する初期設定がすべて書き込まれており,すでにincludeまで書いてあります.今回はmain.cをのぞいてコードを書き足すことはありません.その代わりに,ADCの機能を設定するファイルを追加したいと思います.Srcの中に"myadc.c",Incの中に"myadc.h"を作成してください.Src/Incを右クリックして,New->sourse file/header fileを選択すると作成できます.細かい文法は,C言語の書き方そのままなので説明は省略.ほかのヘッダファイルを参考にしつつ書きました.
というわけで,myadc.hとmyadc.cの中身はこちら.
myadc.h
#ifndef INC_MYADC_H_ #define INC_MYADC_H_ #include "main.h" #include "i2c.h" typedef struct{ uint8_t m_i2caddress; uint8_t m_conversionDelay; uint8_t m_bitShift; uint16_t m_gain; }MYADC; #define ADC_ZER 0x48 // ADR should be GND #define ADC_ONE 0x49 // ADR should be VDD #define ADC_TWO 0x4a // ADC should be SDA #define ADC_THR 0x4b // ADC should be SCL //conversion delay #define ADC_CONVERSIONDELAY 20 //pointer register #define ADC_REG_P_MASK 0x03 //point mask #define ADC_REG_P_CONVERT 0x00 //conversion #define ADC_REG_P_CONFIG 0x01 //configuration #define ADC_REG_P_LOWTHRESH 0x02 //low threshold #define ADC_REG_P_HITHRESH 0x03 // high threshold //config register #define ADC_REG_CONFIG_OS_MASK (0x8000) ///< OS Mask #define ADC_REG_CONFIG_OS_SINGLE (0x8000) ///< Write: Set to start a single-conversion #define ADC_REG_CONFIG_OS_BUSY (0x0000) ///< Read: Bit = 0 when conversion is in progress #define ADC_REG_CONFIG_OS_NOTBUSY (0x8000) ///< Read: Bit = 1 when device is not performing a conversion #define ADC_REG_CONFIG_MUX_MASK (0x7000) ///< Mux Mask #define ADC_REG_CONFIG_MUX_DIFF_0_1 (0x0000) ///< Differential P = AIN0, N = AIN1 (default) #define ADC_REG_CONFIG_MUX_DIFF_0_3 (0x1000) ///< Differential P = AIN0, N = AIN3 #define ADC_REG_CONFIG_MUX_DIFF_1_3 (0x2000) ///< Differential P = AIN1, N = AIN3 #define ADC_REG_CONFIG_MUX_DIFF_2_3 (0x3000) ///< Differential P = AIN2, N = AIN3 #define ADC_REG_CONFIG_MUX_SINGLE_0 (0x4000) ///< Single-ended AIN0 #define ADC_REG_CONFIG_MUX_SINGLE_1 (0x5000) ///< Single-ended AIN1 #define ADC_REG_CONFIG_MUX_SINGLE_2 (0x6000) ///< Single-ended AIN2 #define ADC_REG_CONFIG_MUX_SINGLE_3 (0x7000) ///< Single-ended AIN3 #define ADC_REG_CONFIG_PGA_MASK (0x0E00) ///< PGA Mask #define ADC_REG_CONFIG_PGA_6_144V (0x0000) ///< +/-6.144V range = Gain 2/3 #define ADC_REG_CONFIG_PGA_4_096V (0x0200) ///< +/-4.096V range = Gain 1 #define ADC_REG_CONFIG_PGA_2_048V (0x0400) ///< +/-2.048V range = Gain 2 (default) #define ADC_REG_CONFIG_PGA_1_024V (0x0600) ///< +/-1.024V range = Gain 4 #define ADC_REG_CONFIG_PGA_0_512V (0x0800) ///< +/-0.512V range = Gain 8 #define ADC_REG_CONFIG_PGA_0_256V (0x0A00) ///< +/-0.256V range = Gain 16 #define ADC_REG_CONFIG_MODE_MASK (0x0100) ///< Mode Mask #define ADC_REG_CONFIG_MODE_CONTIN (0x0000) ///< Continuous conversion mode #define ADC_REG_CONFIG_MODE_SINGLE (0x0100) ///< Power-down single-shot mode (default) #define ADC_REG_CONFIG_DR_MASK (0x00E0) ///< Data Rate Mask #define ADC_REG_CONFIG_DR_128SPS (0x0000) ///< 128 samples per second #define ADC_REG_CONFIG_DR_250SPS (0x0020) ///< 250 samples per second #define ADC_REG_CONFIG_DR_490SPS (0x0040) ///< 490 samples per second #define ADC_REG_CONFIG_DR_920SPS (0x0060) ///< 920 samples per second #define ADC_REG_CONFIG_DR_1600SPS (0x0080) ///< 1600 samples per second (default) #define ADC_REG_CONFIG_DR_2400SPS (0x00A0) ///< 2400 samples per second #define ADC_REG_CONFIG_DR_3300SPS (0x00C0) ///< 3300 samples per second #define ADC_REG_CONFIG_CMODE_MASK (0x0010) ///< CMode Mask #define ADC_REG_CONFIG_CMODE_TRAD (0x0000) ///< Traditional comparator with hysteresis (default) #define ADC_REG_CONFIG_CMODE_WINDOW (0x0010) ///< Window comparator #define ADC_REG_CONFIG_CPOL_MASK (0x0008) ///< CPol Mask #define ADC_REG_CONFIG_CPOL_ACTVLOW (0x0000) ///< ALERT/RDY pin is low when active (default) #define ADC_REG_CONFIG_CPOL_ACTVHI (0x0008) ///< ALERT/RDY pin is high when active #define ADC_REG_CONFIG_CLAT_MASK (0x0004) ///< Determines if ALERT/RDY pin latches once asserted #define ADC_REG_CONFIG_CLAT_NONLAT (0x0000) ///< Non-latching comparator (default) #define ADC_REG_CONFIG_CLAT_LATCH (0x0004) ///< Latching comparator #define ADC_REG_CONFIG_CQUE_MASK (0x0003) ///< CQue Mask #define ADC_REG_CONFIG_CQUE_1CONV (0x0000) ///< Assert ALERT/RDY after one conversions #define ADC_REG_CONFIG_CQUE_2CONV (0x0001) ///< Assert ALERT/RDY after two conversions #define ADC_REG_CONFIG_CQUE_4CONV (0x0002) ///< Assert ALERT/RDY after four conversions #define ADC_REG_CONFIG_CQUE_NONE (0x0003) ///< Disable the comparator and put ALERT/RDY in high state (default) #define ADC_REG_BITSHIFT 4 MYADC AdcInit(uint8_t, uint8_t, uint8_t, uint16_t); void AdcSetContinuous(MYADC, uint8_t); void AdcSetSingleShot(MYADC, uint8_t); uint16_t AdcReadSingle(MYADC adcSet); #endif /* INC_MYADC_H_ */
myadc.c
#include "myadc.h" static uint8_t ADSwrite[6]; static uint16_t sendReg; /* * Set ADC setting */ MYADC AdcInit(uint8_t address, uint8_t conversionDelay, uint8_t bitShift, uint16_t gain){ MYADC adcSet; adcSet.m_i2caddress = address; adcSet.m_conversionDelay = conversionDelay; adcSet.m_bitShift = bitShift; adcSet.m_gain = gain; return adcSet; } /* * Config ADC by singlemode, and continuous reading */ void AdcSetContinuous(MYADC adcSet, uint8_t ch){ sendReg = ADC_REG_CONFIG_CQUE_NONE | // Disable the comparator (default val) ADC_REG_CONFIG_CLAT_NONLAT | // Non-latching (default val) ADC_REG_CONFIG_CPOL_ACTVLOW | // Alert/Rdy active low (default val) ADC_REG_CONFIG_CMODE_TRAD | // Traditional comparator (default val) ADC_REG_CONFIG_DR_1600SPS | // 1600 samples per second (default) ADC_REG_CONFIG_MODE_CONTIN; // Continuous mode sendReg |= adcSet.m_gain; switch(ch){ case(0): sendReg |= ADC_REG_CONFIG_MUX_SINGLE_0; break; case(1): sendReg |= ADC_REG_CONFIG_MUX_SINGLE_1; break; case(2): sendReg |= ADC_REG_CONFIG_MUX_SINGLE_2; break; case(3): sendReg |= ADC_REG_CONFIG_MUX_SINGLE_3; break; } ADSwrite[0] = ADC_REG_P_CONFIG; ADSwrite[1] = (sendReg & 0xFF00) >> 8; ADSwrite[2] = (sendReg & 0x00FF); HAL_I2C_Master_Transmit(&hi2c1, adcSet.m_i2caddress << 1, ADSwrite, 3, 100); ADSwrite[0] = 0x00; HAL_I2C_Master_Transmit(&hi2c1, adcSet.m_i2caddress << 1 , ADSwrite, 1 ,100); HAL_Delay(adcSet.m_conversionDelay); } /* * Config ADC by singlemode, and single shot reading */ void AdcSetSingleShot(MYADC adcSet, uint8_t ch){ sendReg = ADC_REG_CONFIG_CQUE_NONE | // Disable the comparator (default val) ADC_REG_CONFIG_CLAT_NONLAT | // Non-latching (default val) ADC_REG_CONFIG_CPOL_ACTVLOW | // Alert/Rdy active low (default val) ADC_REG_CONFIG_CMODE_TRAD | // Traditional comparator (default val) ADC_REG_CONFIG_DR_1600SPS | // 1600 samples per second (default) ADC_REG_CONFIG_MODE_SINGLE; // Single-shot mode(default) sendReg |= adcSet.m_gain; switch(ch){ case(0): sendReg |= ADC_REG_CONFIG_MUX_SINGLE_0; break; case(1): sendReg |= ADC_REG_CONFIG_MUX_SINGLE_1; break; case(2): sendReg |= ADC_REG_CONFIG_MUX_SINGLE_2; break; case(3): sendReg |= ADC_REG_CONFIG_MUX_SINGLE_3; break; } ADSwrite[0] = ADC_REG_P_CONFIG; ADSwrite[1] = (sendReg & 0xFF00) >> 8; ADSwrite[2] = (sendReg & 0x00FF); HAL_I2C_Master_Transmit(&hi2c1, adcSet.m_i2caddress << 1, ADSwrite, 3, 100); ADSwrite[0] = 0x00; HAL_I2C_Master_Transmit(&hi2c1, adcSet.m_i2caddress << 1 , ADSwrite, 1 ,100); HAL_Delay(adcSet.m_conversionDelay); } /* * AD convert to 12bit(int) */ uint16_t AdcReadSingle(MYADC adcSet){ int16_t reading = 0; ADSwrite[0] = ADC_REG_P_CONVERT; HAL_I2C_Master_Transmit(&hi2c1, adcSet.m_i2caddress << 1 , ADSwrite, 1 ,100); HAL_Delay(adcSet.m_conversionDelay); HAL_I2C_Master_Receive(&hi2c1, adcSet.m_i2caddress <<1, ADSwrite, 2, 100); reading = (ADSwrite[0] << 8 | ADSwrite[1] ) >> adcSet.m_bitShift; return reading; }
myadc.hにたくさん宣言されているマクロは,主要な設定項目です.今回は単に1chについてAD変換できれば充分としてプログラムは書いていませんが,ほかの用途で使用する場合に備え,マクロはすべて定義してあります.実はこの部分はArduinoのコードからの流用で,C++で書かれているArduinoライブラリの名残がMYADC構造体です.ここにADCのアドレスや通信待ち時間,AD変換されたデータの空きビットの大きさ,PGAのゲインなど,おおよそそのADCを使う間は変更しなさそうなことを設定しています.myadc.cで定義されている関数は,そのMYADC構造体に設定を書き込む関数,連続変換を設定する関数,いちいち変換したらリセットされる関数,そしてAD変換を行い値を読み取る関数からなっています.そのため,AdcInit関数とAdcSetContinuous関数は一回,AdcSetSingleShot関数とAdcReadSingle関数は読み取りのたびに使用すればよいようになっています.AdcSetContinuous関数は連続で一つのチャンネルを変換するときに使うことを想定していて,このADCに一つしか測定対象をつなげない場合に使います.一方,AdcSetSingleShot関数は毎回リセットするので動作は遅くなりますが,複数チャンネルの変換が可能になります.複数チャンネル使用の時にはAdcSetSingleShot関数,一つしか使わないならAdcSetContinuous関数を使ってください.
3.3 main.cに書く内容
main.cには,myadc.c/.hを利用してコードを作成することができます.#include "myadc.h"を忘れないこと.今回は連続変換の場合を書いていますが,いちいちリセットする場合もコメントアウトした状態で書いてあります.なお,
今回のサンプルプログラムはmain関数までのみ掲載しており,そのあとの変更を加えない自動生成部分は省略しています.
main.c
/* Includes ------------------------------------------------------------------*/ #include "main.h" #include "i2c.h" #include "gpio.h" /* Private includes ----------------------------------------------------------*/ /* USER CODE BEGIN Includes */ #include "myadc.h" /* USER CODE END Includes */ /* Private typedef -----------------------------------------------------------*/ /* USER CODE BEGIN PTD */ /* USER CODE END PTD */ /* Private define ------------------------------------------------------------*/ /* USER CODE BEGIN PD */ /* USER CODE END PD */ /* Private macro -------------------------------------------------------------*/ /* USER CODE BEGIN PM */ /* USER CODE END PM */ /* Private variables ---------------------------------------------------------*/ /* USER CODE BEGIN PV */ /* Private variables ---------------------------------------------------------*/ MYADC ad1; float voltage; const float voltageConv = 4.096 / 2047.0; /* USER CODE END PV */ /* Private function prototypes -----------------------------------------------*/ void SystemClock_Config(void); /* USER CODE BEGIN PFP */ /* USER CODE END PFP */ /* Private user code ---------------------------------------------------------*/ /* USER CODE BEGIN 0 */ /* USER CODE END 0 */ /** * @brief The application entry point. * @retval int */ int main(void) { /* USER CODE BEGIN 1 */ /* USER CODE END 1 */ /* MCU Configuration--------------------------------------------------------*/ /* Reset of all peripherals, Initializes the Flash interface and the Systick. */ HAL_Init(); /* USER CODE BEGIN Init */ /* USER CODE END Init */ /* Configure the system clock */ SystemClock_Config(); /* USER CODE BEGIN SysInit */ /* USER CODE END SysInit */ /* Initialize all configured peripherals */ MX_GPIO_Init(); MX_I2C1_Init(); /* USER CODE BEGIN 2 */ ad1 = AdcInit(0x49, ADC_CONVERSIONDELAY, ADC_REG_BITSHIFT, ADC_REG_CONFIG_PGA_4_096V); AdcSetContinuous(ad1, 0); /* USER CODE END 2 */ /* Infinite loop */ /* USER CODE BEGIN WHILE */ while (1) { //AdcSetSingleShot(ad1, 0); voltage = AdcReadSingle(ad1) * voltageConv; /* USER CODE END WHILE */ /* USER CODE BEGIN 3 */ } /* USER CODE END 3 */ }
ちなみに,ADS1115(16bitADC)を使用するときには,bitshiftの設定を0にすればいけます.また,今回は0V~3.3Vまでの範囲のAD変換を行いたいので,ゲインのモードは4.096Vに設定しています.ADS1015はADRピンに入力する信号を変えることでI2Cアドレスの変更ができます.今回はVCCを入力し,0x49としました.
動作の様子は…省略させてください….というかどうやって映したらいいんですかね().何しろ,ADCに接続したジョイスティックを動かして,読み取った値をcubeIDE内蔵のデバッガ機能で監視しただけなので….デバッガ機能についてもググっていただければたくさん記事が出てくるのでそちらを参考にしてください.
ちなみにですが,このADCが送ってくるデータは「intの範囲で12bit」です.符号付きです.というのも,負の範囲まで電圧の測定ができる子だからです.だから,今回の使用方法だと,設定範囲の半分も測定に生かせていないという….本当だったら,レギュレータ使って1.7Vくらいの電圧を出して,測定対象との差分を,ゲイン2.048V設定で測定するのがよい気がします.そのためには比較測定をする関数を用意しないといけませんが….
4.終わりに
ザックリと説明してきました.いろいろ用意していない機能もたくさんありますが,これをベースにいろいろ作れそうです.とりあえず,ほかにもいろいろ行き当たりばったりやっていきたいと思います.