ブログ移転しました
ブログ移転しました。 やはり,頑張って書いた記事は手元に持っておきたいので,自宅のサーバーで運用することにしました。
今後こちらのブログは更新せず,またこのブログの過去記事は新しいブログやホームページの方に移行させます。
STM32F401 nucleo boardあれこれ
秋月でnucleo boardを買ったはいいものの,何もわからず押入れの奥で眠らせている人向けの記事です。
必要な資料の見つけ方,開発の始め方,初期設定の仕方,基板分離の仕方などについて触れていきます。
あらかじめ用意しておきたいドキュメント等
nucleo board
- User Manual / UM1724: STM32 Nucleo-64 boards
nucleo boardのユーザーマニュアルです
STM32F401RE
- 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つの選択肢があります。
- マイクロマウス界隈で有名な方が作成された書き込みソフトがあります。
STM32 FLASH Writer Program Using Python
ただし,僕の環境では適切なmotファイルが生成されなかったため,binファイルを書き込めるように改造して使っています。 - 公式のツールを使いましょう。
STSW-LINK004 STM32 ST-LINK utility - STMicroelectronics
初期設定
クロック設定
WindowsでExcelが導入されている場合は非常に簡単です。
STSW-STM32091 Clock configuration tool for STM32F40x/41x microcontrollers (AN3988) - STMicroelectronics
このエクセルシートを実行し,適切な入力クロックなどを記入して更新,生成されたファイルをプロジェクトに入れるだけです。
ここでHSIは内部クリスタル(マイコンに内蔵されています),HSEは外付されたクリスタルです。
このクロック設定を有効化するため,電源投入時にSystemInit()関数を呼び出してあげましょう。
基板分離
nucleo boardはST-Linkとマイコンボード本体を分離できるように設計されています(すごいっ)。
そのために確認しておくべきことをまとめておきます。
"UM1724: STM32 Nucleo-64 boards"を見ながら作業してください。
- 5.1 Cuttable PCBに概要が書かれています。
- 書き込み手段
主に次の2つの手法があります。 - 電源
電源は外部から入れてやる必要があります。電圧はいろいろと選択肢があります。 その時,ジャンパピンを動かして電源を外部入力できるようにしてあげましょう。 - 外部クリスタル
必要な場合,外部クリスタルを接続できます。 同時にコンデンサもつける必要があります。
またその場合,ジャンパ抵抗をいくつか付け替える必要があります。
とりあえずここまで。何かあれば追記します。
整数型の変数について
この記事は新入生向けに噛み砕いて説明をします。
内容的には基本情報技術者試験の「基礎理論-離散数学」レベルの知識に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)」
この赤文字が「基数」です。 あえて数式を分解するなら次のような感じです。
3×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 |
- 2進 -> 10進
先ほど説明した基数の考え方を使います。
LSB(一番右に示される桁)から順番に2のn乗をかけながら,足しあわせていきます。
例:0b10011010
1×27 + 1×24 + 1×23 + 1×21 = 154 - 2進 -> 16進
一番簡単です。 これは基数もクソもありません。覚えゲーです。 実は2進数の4桁と16進数の1桁はちょうど一対一で対応しています。 2進数と16進数は桁の繰り上げが起こるタイミングが一致していることを確認してください。
あわせて,上に示した表は丸暗記してください。 例:0b10011010- 2進数を4桁ずつ分離します
1001 1010 - 表にしたがって16進数に直します
9 A - くっつけます
0x9A
- 2進数を4桁ずつ分離します
- 10進 -> 2進
2の0〜16乗まですべて覚えましょう。 それの足し合わせで2進数表記できるので,頑張ってください。
(真面目に変換したい人は各自調べてみてください) - 16進 -> 2進
さっきの逆です。
例:0xE5- 1桁ずつに分けます
E 5 - 対応表に基づいて変換します
1110 0101 - くっつけます
0b11100101
- 1桁ずつに分けます
演算
次は二進数の演算について軽く触れていきましょう。
加算
加算はとても簡単です。
小学校で習った筆算と同じことをすればできます。
ただし1+1の計算で繰り上がりが発生することだけ注意してください。
例:0b00001010+0b00111100
減算
では減算はどうすればいいでしょうか。
例えば0b0001-0b0010は幾つになるでしょうか……?
-0001? 1111?
2の補数
2進数の世界にはマイナスの記号は存在しません。 そのかわり,負の数を2進数で表現したい場合は「2の補数」という技を使います。
ここでは8ビットの2進数があるとします。
”00000000”
では2の補数を使って,”−11”を表現してみましょう。
- まずは絶対値を二進数で表記
11を二進数で表記します。
0b00001011 - ビットをすべて反転
0b11110100 - 1加算する
0b11110101
できました。-11は0b11110101として表記することができます。
2の補数で表された2進数を10進数に戻すときは,逆順に,1減算した後にビットを反転すれば,10進数の絶対値を得ることができます。
減算
2の補数とは,単なる負数の表現法というわけではありません。
2の補数で表現された2進数を「加算」することによって,減算の計算をすることができるのです。
例:20-31を2進数で計算
20は,2進数で0b00010100
31は,2進数で0b00011111
-31は,2進数で0b11100001
というわけで-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進数の整数についてはこれくらいの知識を持って今後の製作に励んでもらいたいと思います。
また,興味のある人は是非「基本情報技術者試験」や「応用情報技術者試験」などの資格にチャレンジしてもらいたいと思っています。
テクノロジやマネジメントについて,幅広い分野をかじることができるのできっと楽しいと思います。
(多分)浮動小数点型の基礎に続く
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++っぽくなってきた
- 本家とは使い方が異なるので,そのうち変更したい
参考
STM32でのSPI覚え書き
Qt記事少しの間だけおやすみします。
先日ついにハーフマウスの基板が届いたので,かかりっきりでいました。 こいつ,めちゃくちゃ可愛いです。(合わせて書き込み充電基板も発注しておきました)
さて,困難な問題は分割して取り組もう,ということで割り込みも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 |
次は送受信完了割り込みと,送信準備完了割り込みを使いながら通信できるように頑張ってみます。