hantas's blog

ブログ移転しました→ http://blog.taniho.net/

STM32F401 nucleo boardあれこれ

秋月でnucleo boardを買ったはいいものの,何もわからず押入れの奥で眠らせている人向けの記事です。
必要な資料の見つけ方,開発の始め方,初期設定の仕方,基板分離の仕方などについて触れていきます。

あらかじめ用意しておきたいドキュメント等

nucleo board

NUCLEO-F401RE STM32 Nucleo-64 development board with STM32F401RET6 MCU, supports Arduino and ST morpho connectivity - STMicroelectronics

  • User Manual / UM1724: STM32 Nucleo-64 boards
    nucleo boardのユーザーマニュアルです

STM32F401RE

STM32F401RE STM32 Dynamic Efficiency MCU, ARM Cortex-M4 core with DSP and FPU, up to 512 Kbytes Flash, 84 MHz CPU, Art Accelerator - STMicroelectronics

  • Product Specifications / DS10086: ARM® Cortex®-M4 32b MCU+FPU, 105 DMIPS, 512KB Flash/96KB RAM, 11 TIMs, 1 ADC, 11 comm. interfaces
    各機能の概要を確認します ピン配置が掲載されています
  • Reference Manual / RM0368: STM32F401xB/C and STM32F401xD/E advanced ARM®-based 32-bit MCUs
    各機能の詳細を確認できます
  • Application Note / AN2606: STM32 microcontroller system memory boot mode
    ブートモードの詳細が書かれています

他にも有用なドキュメントがあるので必要に応じてこのページを参照しましょう。

開発環境の導入

Linuxの場合はこちらの記事を参考にしてください(近々書き直します)

STM32 Nucleo Boardの開発環境構築 - hantas's blog

Windowsは,まだβですがEm::Blocksがいいと思います。 (CoIDEはSTM32F401に対応していません)
Home - Em::Blocks
書き込み環境ですが,Windowsの場合2つの選択肢があります。

  1. マイクロマウス界隈で有名な方が作成された書き込みソフトがあります。
    STM32 FLASH Writer Program Using Python
    ただし,僕の環境では適切なmotファイルが生成されなかったため,binファイルを書き込めるように改造して使っています。
  2. 公式のツールを使いましょう。
    STSW-LINK004 STM32 ST-LINK utility - STMicroelectronics

初期設定

クロック設定

WindowsExcelが導入されている場合は非常に簡単です。
STSW-STM32091 Clock configuration tool for STM32F40x/41x microcontrollers (AN3988) - STMicroelectronics
このエクセルシートを実行し,適切な入力クロックなどを記入して更新,生成されたファイルをプロジェクトに入れるだけです。

ここでHSIは内部クリスタル(マイコンに内蔵されています),HSEは外付されたクリスタルです。

このクロック設定を有効化するため,電源投入時にSystemInit()関数を呼び出してあげましょう。

基板分離

nucleo boardはST-Linkとマイコンボード本体を分離できるように設計されています(すごいっ)。
そのために確認しておくべきことをまとめておきます。
"UM1724: STM32 Nucleo-64 boards"を見ながら作業してください。

  1. 5.1 Cuttable PCBに概要が書かれています。
  2. 書き込み手段
    主に次の2つの手法があります。
    • SWD(JTAGみたいなやつ)を使って書き込む場合,5.2.4 Using ST-LINK/V2-1 to program/debug an external STM32 applicationを見ながら,ST-Linkとマイコンボードを接続します。
      この時,ジャンパ抵抗をいくつか付け替える必要があります。
    • USARTを使って書き込む場合,書き込み用のピンを確認して書き込み器と接続します。(Miceで主流な方法は,FT234ボードを接続します。GNDも接続するように)
  3. 電源
    電源は外部から入れてやる必要があります。電圧はいろいろと選択肢があります。 その時,ジャンパピンを動かして電源を外部入力できるようにしてあげましょう。
  4. 外部クリスタル
    必要な場合,外部クリスタルを接続できます。 同時にコンデンサもつける必要があります。
    またその場合,ジャンパ抵抗をいくつか付け替える必要があります。

とりあえずここまで。何かあれば追記します。

整数型の変数について

この記事は新入生向けに噛み砕いて説明をします。 内容的には基本情報技術者試験の「基礎理論-離散数学」レベルの知識にC言語の知識をプラスした程度となります。
2進数・10進数・16進数の理解,演算,正負の付け方,またC言語における変数の型の話をまとめています。

はじめに

2進数,16進数などの言葉を聞いたことがある人は多いかと思います。 なぜこれらの表記法があるのでしょうか?

羊がたくさんいるとき,私達は「1匹,2匹,3匹,4匹,5匹。」と数えます。
同じ数の羊をコンピュータは「1,10,11,100,101」と数えます。
同じものを数えていますが,その表記法が違うだけです。

コンピュータは基本的にデジタル回路です。値の読み書きは,0と1しかありません。 私達が使っている10進数の数字を理解することができないのです。 そのため,コンピュータが扱える0と1だけで数字を表す2進数という表現方法が用いられるようになりました。
プログラムの理解には2進数の理解が必須です。 また,2進数をわかりやすくするためによく用いられる16進数も覚えておくと良いでしょう。

これからの記事を読むにあたって,ひとつだけ注意点です。
例えば”1010”と書かれたときに,これは10進数の1010なのか,2進数(10進数に直すと10)なのか,16進数(10進数に直すと4112)なのか区別がつきません。 そこで,一般的に用いられている表記法を用います。

  • 10進数の場合はそのまま書く:1010
  • 2進数の場合は頭に”0b”とつける:0b1010
  • 16進数の場合は頭に"0x"とつける:0x1010

n進数

まずはn進数<->n進数の相互変換ができるようになりましょう。 これは基礎中の基礎なので確実にできるようにしておいてください。

基数

「基数」というと何なのかよくわかりませんが,実は毎日使っています。

「さんまん せん よんひゃく じゅう ご (31415)」

この赤文字が「基数」です。 あえて数式を分解するなら次のような感じです。

104 + 1×103 + 4×102 + 1×101 + 5×100 = 31415

このように,10進数の場合は基数が100,101,102,103,104,……と順に続いていきます。 基数が10のn乗なので,10進数というわけです。

同じように考えて,2進数の場合は基数が20,21,22,23,24,……と続いていくはずです。

したがって,10進数の31415を2進数で表現すると,次の式から0111101010110111となります。

1×214 + 1×213 + 1×212 + 1×211 + 1×29 + 1×27 + 1×25 + 1×24 + 1×22 + 1×21 + 1×20 = 0b0111101010110111

16進数の場合は,160,161,162,163,164,……と続きます。

同様に,10進数の31415を2進数で表現すると,次式から7AB7となります(ただし,Aは10進数で10,Bは10進数で11を意味します)。

7×163 + A×162 + B×161 + 7×160 = 0x7AB7

変換

※この表は後で使います

2進数 16進数 10進数(参考)
0000 0 0
0001 1 1
0010 2 2
0011 3 3
0100 4 4
0101 5 5
0110 6 6
0111 7 7
1000 8 8
1001 9 9
1010 A 10
1011 B 11
1100 C 12
1101 D 13
1110 E 14
1111 F 15
10000 10 16
  1. 2進 -> 10進
    先ほど説明した基数の考え方を使います。
    LSB(一番右に示される桁)から順番に2のn乗をかけながら,足しあわせていきます。
    例:0b10011010
    1×27 + 1×24 + 1×23 + 1×21 = 154
  2. 2進 -> 16進
    一番簡単です。 これは基数もクソもありません。覚えゲーです。 実は2進数の4桁と16進数の1桁はちょうど一対一で対応しています。 2進数と16進数は桁の繰り上げが起こるタイミングが一致していることを確認してください。
    あわせて,上に示した表は丸暗記してください。 例:0b10011010
    1. 2進数を4桁ずつ分離します
      1001 1010
    2. 表にしたがって16進数に直します
      9 A
    3. くっつけます
      0x9A
  3. 10進 -> 2進
    2の0〜16乗まですべて覚えましょう。 それの足し合わせで2進数表記できるので,頑張ってください。
    (真面目に変換したい人は各自調べてみてください)
  4. 16進 -> 2進
    さっきの逆です。
    例:0xE5
    1. 1桁ずつに分けます
      E 5
    2. 対応表に基づいて変換します
      1110 0101
    3. くっつけます
      0b11100101

演算

次は二進数の演算について軽く触れていきましょう。

加算

加算はとても簡単です。 小学校で習った筆算と同じことをすればできます。 ただし1+1の計算で繰り上がりが発生することだけ注意してください。
例:0b00001010+0b00111100

f:id:hantas:20160310000938p:plain

減算

では減算はどうすればいいでしょうか。
例えば0b0001-0b0010は幾つになるでしょうか……? -0001? 1111?

2の補数

2進数の世界にはマイナスの記号は存在しません。 そのかわり,負の数を2進数で表現したい場合は「2の補数」という技を使います。

ここでは8ビットの2進数があるとします。
”00000000”
では2の補数を使って,”−11”を表現してみましょう。

  1. まずは絶対値を二進数で表記
    11を二進数で表記します。
    0b00001011
  2. ビットをすべて反転
    0b11110100
  3. 1加算する
    0b11110101

できました。-11は0b11110101として表記することができます。

2の補数で表された2進数を10進数に戻すときは,逆順に,1減算した後にビットを反転すれば,10進数の絶対値を得ることができます。

減算

2の補数とは,単なる負数の表現法というわけではありません。
2の補数で表現された2進数を「加算」することによって,減算の計算をすることができるのです。
例:20-31を2進数で計算
20は,2進数で0b00010100
31は,2進数で0b00011111
-31は,2進数で0b11100001

f:id:hantas:20160310010047p:plain

というわけで-11という解が出てきました。

C言語における整数型

さて,2進数で整数を扱う基礎は以上となります。 これらの基礎を学んだ上で,C言語にはどのように使われているか,少しだけ考えてみましょう。

符号長

変数の型はいくつかありました。

char, short, long, long long, そして忌々しきint

符号長を表にまとめておきます。 要は使うことのできる2進数の桁数です。

符号長
char 8bit
short 16bit
long 32bit
long long 64bit
int 処理系依存(近年のコンピュータは32bit)

最大値・最小値

ここまで勉強してきた内容から,変数の最大値と最小値を算出することができるはずです。
signedとunsigned,それぞれ考えてみましょう。

自力で考えてみたほうが理解が深まります。

unsigned

すべての桁を”1”にすれば最大値が出ます。 最小値は0です。

signed

正数で一番大きな数は,MSBが0,それ以外が1の場合です。

負数で一番小さな数は,MSBが1,それ以外が0の場合です。

ここでまとめる必要もないので

既にまとめているサイトが多数あるので引用します。 自分の考えがあっていたか確認してみてください。 色々なデータ型の最大値・最小値 - miyapongの日記(仮)

型名をわかりやすくする

正直,charとかlonglongだとか言われても何ビットかパット見でわかりません。
また,”char”はsignedなのかunsignedなのか,コンパイルオプションを見ないとわからないのです(デフォルトでunsignedとしている開発環境が多い)

そこで,C標準ライブラリに"stdint.h"というライブラリがあります。 こいつを使うことで変数のサイズがひと目でわかるようになります。

標準の型 新たに定義された型
signed char int8_t
unsigned char uint8_t
signed short int16_t
unsigned short uint16_t
signed long int32_t
unsigned long uint32_t
signed long long int64_t
unsigned long long uint64_t

他にも幾つかの型が宣言されています。 便利なのでこちらを使うことを推奨します。

おわりに

Winプログラムではなく,マイコン向けのプログラムを書くのにビット列の扱い方を教えないのはあまりに無茶だと思います。
せめて2進数の整数についてはこれくらいの知識を持って今後の製作に励んでもらいたいと思います。
また,興味のある人は是非「基本情報技術者試験」や「応用情報技術者試験」などの資格にチャレンジしてもらいたいと思っています。
テクノロジやマネジメントについて,幅広い分野をかじることができるのできっと楽しいと思います。

(多分)浮動小数点型の基礎に続く

参考: 2016年、C言語はどう書くべきか (前編) | プログラミング | POSTD

myprintf -> myiostream

サークル内での話なのですが,マイコン開発をする際のバグ取りに絶大な威力を発揮する方法として "myprintf" が代々伝わっています。 (いつからなのでしょうか?)

"myprintf" - マイプリントエフ [名]
1. SCI1を利用してTeraTermに文字を送るため,SH向けに作成されたヘッダファイルのこと
2. 1.のヘッダファイルを用いて文字を送ること
3. (広義) 1.のヘッダファイルをRXなど他のマイコンに対応させたファイルのこと

……正直なところ,こんなイメージが部内に漂っているような気がします。 代々伝わってきた "myprintf" がデバッグに広く使われているのは嬉しい限りですが,少し悲しくも感じます。

問題点

まあそんなことは良いのですが,本題はこのヘッダファイルについてです。

int myprintf(const char*, ...); は,通常のprintfと同様にシリアルで文字列を送信できるようにした関数ですが, これにはいくつかの問題があって,

  • バッファサイズが固定(100byte)なので100文字以上になる文字列を渡すと範囲外アクセスをする危険性がある
  • 内部的にvsprintfを用いているのでバッファオーバーフローの危険性がある
  • 条件付き書式の修飾子を書き間違えやすい(一部のコンパイラではエラーを出さないので,人為的ミスが多発しやすい)

などなど,あまりよろしいものではありません。

書き換える

というわけで,この"myprintf"を捨て去るため,C++的に作り変えることにしました。

ところで,入門者向けのコードでこのようなものを目にすることが多いと思います。

std::cout << "Hello World!" << std::endl;

この構文が不思議で不思議でたまりませんでした。 シェルのリダイレクトっぽい雰囲気があるので言わんとすることはわかるのですが,Cしか触ったことのなかった自分にとってはとても謎。

実はこれ,ビットシフト演算子をオーバーライドしているのですね。 そして返り値にstd::coutの参照を与えることで,1度に複数の文字列を出力させているようです。

そこで,myprintfを一新し,このcout風に書けるように自前のクラスを作りたいと思います。

いくつかのサイトを参考にしながら"ComPc"クラスにオレオレcoutを実装することにしました。

  • ComPcクラスはシングルトン
  • 左ビットシフト演算子をオーバーライドして,std::string,const char*, 整数型を右辺に取ったときに文字(もしくは10進数表記)を出力
  • CpmPc::hex(整数型) を右辺に取ったときは16進の文字を出力

書いてて意味が分からない気がしますが,次のように使います。

uint8_t hoge = 0x3C;
ComPc *compc = ComPc::getInstance();
*compc << "Hello World!\n";
*compc << "value: " << compc->hex(hoge) << "\n";

// output
Hello World!
value: 3C

実装コードの一部分を掲載しておきますが,作りかけのため参考程度としてください。

(send1byte, sendnbyteは別に定義しているメンバ関数で,1または整数バイトをUSARTを通して送信する関数です)

unsigned char ComPc::bit2hex(const uint8_t val){
    if(val < 0x0A) return '0' + val;
    else if(val > 0x0F) return 'X';
    else return 'A' + (val - 0x0A);
}

std::string ComPc::hex(const uint8_t val){
    std::string tmp;
    tmp += " 0x";
    for (int i=1; i>=0; i--) {
        tmp += bit2hex((val>>(4*i)) & 0x0F);
    }
    tmp += " ";
    return tmp;
}

ComPc& ComPc::operator << (const char chr) {
    send1byte(chr);
    return *this;
}

ComPc& ComPc::operator << (const std::string& str) {
    sendnbyte(str.c_str(), str.length());
    return *this;
}

実際にはそれぞれの関数をオーバーロードして,様々な引数を取れるようにしています。

ポイントは,ComPcの参照を返しているところです。

まとめ

  • コードがC++っぽくなってきた
  • 本家とは使い方が異なるので,そのうち変更したい

参考

C 言語よりお得な C++ その8 | RVF/RC45 blog

C++ - シングルトンのベターな実装方法 - Qiita

STM32でのSPI覚え書き

Qt記事少しの間だけおやすみします。

先日ついにハーフマウスの基板が届いたので,かかりっきりでいました。 こいつ,めちゃくちゃ可愛いです。(合わせて書き込み充電基板も発注しておきました)

f:id:hantas:20151212140622j:plain

さて,困難な問題は分割して取り組もう,ということで割り込みもDMAも使わないSPI通信に成功したのでログを残しておきます。 今回使用したモジュールはSPIモード3での通信を行っています。 送信8bit,受信8bitでWHO_AM_Iが帰ってくることまで確認しています。

主要部分のコードは次の通り。

// 設定

    RCC_AHB1PeriphClockCmd(RCC_AHB1Periph_GPIOA, ENABLE);
    RCC_AHB1PeriphClockCmd(RCC_AHB1Periph_GPIOB, ENABLE);
    RCC_AHB1PeriphClockCmd(RCC_AHB1Periph_GPIOC, ENABLE);
    RCC_AHB1PeriphClockCmd(RCC_AHB1Periph_GPIOD, ENABLE);
    RCC_APB1PeriphClockCmd(RCC_APB1Periph_SPI3, ENABLE);

    GPIO_InitTypeDef GPIO_InitStructure;
    GPIO_StructInit(&GPIO_InitStructure);
    GPIO_InitStructure.GPIO_Pin = GPIO_Pin_15;
    GPIO_InitStructure.GPIO_Mode = GPIO_Mode_OUT;
    GPIO_InitStructure.GPIO_Speed = GPIO_Speed_100MHz;
    GPIO_InitStructure.GPIO_OType = GPIO_OType_PP;
    GPIO_InitStructure.GPIO_PuPd = GPIO_PuPd_NOPULL;
    GPIO_Init(GPIOA, &GPIO_InitStructure);
    GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF;
    GPIO_InitStructure.GPIO_Pin = GPIO_Pin_10 | GPIO_Pin_11 | GPIO_Pin_12;
    GPIO_Init(GPIOC, &GPIO_InitStructure);
    GPIO_SetBits(GPIOA, GPIO_Pin_15);

    GPIO_PinAFConfig(GPIOC, GPIO_PinSource10, GPIO_AF_SPI3);
    GPIO_PinAFConfig(GPIOC, GPIO_PinSource11, GPIO_AF_SPI3);
    GPIO_PinAFConfig(GPIOC, GPIO_PinSource12, GPIO_AF_SPI3);

    SPI_InitTypeDef SPI_InitStructure;
    SPI_InitStructure.SPI_Direction = SPI_Direction_2Lines_FullDuplex;
    SPI_InitStructure.SPI_Mode = SPI_Mode_Master;
    SPI_InitStructure.SPI_DataSize = SPI_DataSize_8b;
    SPI_InitStructure.SPI_CPOL = SPI_CPOL_High;
    SPI_InitStructure.SPI_CPHA = SPI_CPHA_2Edge;
    SPI_InitStructure.SPI_NSS = SPI_NSSInternalSoft_Set | SPI_NSS_Soft;
    SPI_InitStructure.SPI_BaudRatePrescaler = SPI_BaudRatePrescaler_8;
    SPI_InitStructure.SPI_FirstBit = SPI_FirstBit_MSB;
    SPI_InitStructure.SPI_CRCPolynomial = 7;
    SPI_Init(SPI3, &SPI_InitStructure);
    SPI_Cmd(SPI3, ENABLE);

// 送信,受信

    uint16_t ret;
    GPIO_ResetBits(GPIOA, GPIO_Pin_15); // CSをセット
    while (SPI_I2S_GetFlagStatus(SPI3, SPI_I2S_FLAG_TXE) == RESET); // 送信可能になるまで待つ
    SPI_I2S_SendData(SPI3, 0x8F); // 送信(今回はWHO_AM_Iの8bitコマンド)
    while (SPI_I2S_GetFlagStatus(SPI3, SPI_I2S_FLAG_RXNE) == RESET); // 受信可能になるまで待つ
    ret = SPI_I2S_ReceiveData(SPI3); // 空データを受信する

    while (SPI_I2S_GetFlagStatus(SPI3, SPI_I2S_FLAG_TXE) == RESET);
    SPI_I2S_SendData(SPI3, 0x00); // 空データを送信する
    while (SPI_I2S_GetFlagStatus(SPI3, SPI_I2S_FLAG_RXNE) == RESET);
    ret = SPI_I2S_ReceiveData(SPI3); // ほしいデータを受信する
    while (SPI_I2S_GetFlagStatus(SPI3, SPI_I2S_FLAG_TXE) == RESET);
    GPIO_SetBits(GPIOA, GPIO_Pin_15); //CSをリセット

という感じです。 今回詰まったのは何点かあって,

  • CPOLとCPHAがわかりにくい →せいぜい4通りなので全部試せばいいよね(オイ)
  • NSS(CS)の使い方がわからない →手動で切り替える
  • 0が帰ってくる →全二重通信なので,送信時に送られてきた空データも受信してあげる必要がある

以上がポイントでした。

CPOLとCPHAの件ですが,こんなに乱暴では申し訳ないので表にまとめておきます。

SPI_CPOL SPI_CPHA CKP CKE Mode
SPI_CPOL_Low SPI_CPHA_1Edge 0 1 0
SPI_CPOL_Low SPI_CPHA_2Edge 0 0 1
SPI_CPOL_High SPI_CPHA_1Edge 1 1 2
SPI_CPOL_High SPI_CPHA_2Edge 1 0 3

次は送受信完了割り込みと,送信準備完了割り込みを使いながら通信できるように頑張ってみます。