
みなさんこんにちは! 美しいIDEで記述されたコードが、プロセッサまたはマイクロコントローラーで消化可能な一連のバイトにどのように変わると思いますか? だから私は個人的にはあまりしません。 それ自体でうまく機能します。 [ファイルの追加]ボタンをクリックしました-IDE自体がプロジェクトにソースを追加し、コンパイラとリンカー自体を呼び出します。 自分でコードを書き、些細なことを考えないでください。
しかし、これらすべての暗黙の事柄が表面に現れて、悪夢の中で夢を見始めることがあります。 最も単純なHello Worldでさえ、ボンネットの下には、スムーズで快適な動きを提供するさまざまな非常に興味深いギアがあります。 マイクロコントローラーの世界も例外ではありません。
今日は
私のプロジェクトのビルドシステムの交換に焦点を合わせます。 さまざまな理由から、私はArduinoと密接に関係しており、向きを変えることができるものを探す必要がありました。 猫の下で、ArduinoシステムビルドからSTM32マイクロコントローラーとCMakeを使用したstm32duinoフレームワークのファームウェアアセンブリへの移行の私の経験が説明されています。
ビルドシステムArduino
Arduinoが好きなのは、マイクロコントローラの世界にスムーズに入ることができるからです。 しかし、多くの人が言うように、これは初心者向けであり、多かれ少なかれ真剣なプロジェクトには、arduinoは適していません。 私はカテゴリーになりたくないし、arduinoは吸うとジャズだと言いたい。 物事を整理しましょう。
だから、私はArduinoで次のことを強調します:
- Arduino IDE-エディタ、コンパイラ、および1セットのライブラリセット。 小さなプロジェクトからすばやく開始したり、インターネットからダウンロードしたプロジェクトを組み立てたりすることができます。 追加の資金をインストールする必要はありません-すべてが箱から出してすぐです。
- Arduinoボード -ATMegaプロセッサ(またはAtmelのその他)に基づくボードのオープンソース設計。 ボードは多かれ少なかれ標準化されており、多くのシールドと周辺機器があります。
- Arduinoフレームワーク -マイクロコントローラーの低レベルのロジックとレジスターを隠すC ++クラス、インターフェース、およびライブラリーのセット。 ユーザーには、便利でかなり高レベルの作業用インターフェイスが提供されます。
これらの各部分は、他の部分から独立しています。 そのため、ArduinoIDEでは、他のマイクロコントローラー(たとえば、ESP8266)で記述したり、逆にArduinoIDEを放棄して、Atmel Studio、Eclipse、またはvimのどこかでArduinoの魅力をすべて学ぶことができます。 一部のボードは、Arduinoボード(クアッドコプターフライトコントローラーなど)とはあまり似ていませんが、Arduinoスケッチを簡単に追加できます。 また、逆に、ArduinoボードはベアCまたはアセンブラーでプログラムできます。 Arduinoフレームワークに関しては、多くのマイクロコントローラー(
STM32 、
TI )に移植されているため、この世界に入るしきい値を許容レベルまで下げていることに注意してください。
中央にはstm32f103c6(実際にはstm32f103cb)上のボードがあり、右側にはarduino nanoがあります。 ここからの写真( ここでは、 stm32duino の質問が少し深く影響を受けています)私のプロジェクトでは、私はすぐにArduino IDEを放棄して、より使い慣れたAtmel Studioを支持しました。
また 、周辺機器とメモリが少し多い、より強力なプラットフォームとしてSTM32を支持して、Arduinoボードを
拒否しました。 私は本当にArduinoフレームワークを拒否したくありません-それは非常に美しく、便利で、あなたが必要とするすべてを提供します。 STM32マイクロコントローラー用のArduinoポートであるstm32duinoを使用します。 しかし、
SPL (レジスターに対する正統派の抽象化とST自体からのコントローラーのスタッフィング)は何とか入りませんでした-コードが大きすぎていです。
しかし、arduino-
buildシステムには別の非常に暗黙的な部分があり
ます 。 この暗黙性は、すべてがどのように機能するかを考えて理解したくない場合に、小規模プロジェクトにとって大きなプラスになります。 ファイルとライブラリを追加するだけで、すべてが収集されます。 初心者にとっては、これが一番です。 しかし、プロジェクトが少しでも大きくなり、いくつかのライブラリが追加され、コンパイラの微調整が必要になると、arduinoのビルドシステムが干渉し始めます。 ここに私のプロジェクトに干渉したもののリストがあります
- ソースをディレクトリにソートできない
ソースコードがほんの数個である場合、簡単にナビゲートできます。 プロジェクト内のファイル数が数十を超える場合、それらをすべてヒープにダンプするのは得策ではありません。 プロジェクトのさまざまなコンポーネントまたは独立した部分をさまざまなディレクトリに分解し、必要に応じてインクルードパスを登録すると便利です。
残念ながら、Arduinoでは、ソースを異なるディレクトリに分解することはできません。または、少なくとも何らかの形でIDEのファイルをグループ化することはできません。 Arduinoビルドシステムは、すべてのファイルが同じディレクトリにあることを意味します。 さらに、プロジェクトのソースコードは、メインスケッチファイルと同じディレクトリでファイルを見つけるという事実によって正確に接続されます。 ところで、スケッチが置かれているディレクトリ自体も、とにかく呼び出されるべきではありません-スケッチとまったく同じように呼び出されるべきです。
- ライブラリをインストールする必要があります
また、これは1回限りの操作ですが、プロジェクトをアセンブルするための個別の指示を記述する必要があります。ここからライブラリをダウンロードし、そこに置いて、このように構成します。
ライブラリに何らかの変更を加える必要がある場合があります(間違いを修正するか、自分で作業するため)。そして、少なくともgithubで分岐する必要があります。 また、プロジェクトの大文字のアセンブリ手順では、元のライブラリが機能しないことを記述します。
しかし、別の方法で。 ライブラリをプロジェクトのソースコードの横にあるバージョン管理システムに配置することはできません。ライブラリをプロジェクトに接続することはできません。
- ライブラリ設定はライブラリ自体で構成されます
大型コンピュータ用のライブラリでは、すべてが定義で構成されます。 ライブラリ全体が提供され、クライアントは自分の裁量でそれを使用します。
Arduinoライブラリでは、これは当てはまりません。 通常、ライブラリには、自分でファイルを作成し、特定の設定を有効または無効にし、周辺機器へのリンクを修正する必要があるヘッダーファイルが含まれています。 しかし、このライブラリを使用する複数のプロジェクトがあり、パラメーターが異なる場合はどうなりますか? 結局、ライブラリは共有ディレクトリにインストールされ、あるプロジェクトの変更は別のプロジェクトに影響を及ぼします。
また、構成ファイルのバージョン管理の問題は未解決のままです
- プロジェクトは常に全体として再構築されます
1つのファイルで小さな変更があったとしても、すべてのファイルが再コンパイルされるまで待つ必要があります。 私の場合、約1分です。
これはすべて、目に見える進歩がないことによってさらに悪化します。 動作するかどうかは明確ではありません。 ビルドが成功した場合、ビルドシステムはファームウェアのサイズを書き込みます。 ただし、エラーが発生した場合(たとえば、フラッシュメモリのサイズを超えた場合)、ビルドシステムはこれをまったく報告しません。
- 最後に、コンパイルキーを柔軟に変更することはできません
最適化設定も変更できません。 そして、あなたは微調整を忘れることができます。 私は何を言うことができます-あなたはインクルードパスを登録したり、定義することはできません!
患者の検査

したがって、Arduinoビルドシステムの問題は明らかです。 ここでビジネスを明確にするには、より良いものを考え出す必要があります。 しかし、別のビルドシステムに移る前に、まずArduino自体がプロジェクトをビルドする方法を理解する必要があります。 そして、それは非常に興味深い働きをします。
まず、ビルドシステムは-o nulキーを使用してスケッチファイル(.ino)をビルドしようとします(何も書き込まず、コンパイルエラーのみを収集します)。 コンパイラがヘッダーを見つけられない場合、ビルドシステムはインストールされているライブラリでこのヘッダーを探します。 -がある場合、-Iスイッチ(オプションのインクルードパス)を追加し、もう一度ビルドを試みます。 ビルドシステムが別のヘッダーを見つけられない場合、操作が繰り返されます。 -Iスイッチのパスを含む行が蓄積され、プロジェクト内の次のファイルに適用されます。
-Iスイッチのトリックに加えて、ビルドシステムはライブラリ自体もアセンブルします。 これを行うために、彼女はライブラリディレクトリ内のすべての* .cおよび* .cppファイルをコンパイルしようとします。 ライブラリのヘッダー依存関係は、すでに説明した方法で解決されます。 ライブラリは、すべてのファイルが使用されていなくても、全体としてコンパイルされます。 そして、リンカーが超過分をスローするのは良いことです。 そうでない場合は?
したがって、グローバルオブジェクトがライブラリ(たとえば、SerialまたはSPIクラス)で宣言されている場合、このコードが実際に使用されていなくても、このオブジェクトのコードは常にファームウェアに入ります。 つまり 誤って追加された#includeディレクティブにより、ファームウェアに数キロ余分に追加される可能性があります。
しかし、それだけではありません。 すべての依存関係がコンパイルされると、コンパイル、コード生成、最適化、およびすべてのジャズに適切なキーを使用して、コンパイルプロセスがもう一度開始されます。 一般に、各ファイルは少なくとも2回コンパイルされ、特に失敗したファイル(.inoなど)は、プロジェクトに含まれるライブラリの数だけコンパイルできます。
あらゆる種類のシステム(割り込みベクトル、ボードの基本的な初期化)もプロジェクトでコンパイルされます。 幸いなことに、コンパイルされたファイルの一部は、.a静的ライブラリにコンパイルされます。 しかし、これは、実際、stm32duinoの作成者によるシャーマニズムです。他のライブラリでは、すべてがファイルごとに行われます。
ライブラリが1〜2個ある小さなArduinoプロジェクトの場合、アセンブリに対するこのアプローチはあまりオーバーヘッドを生み出しません。 しかし、25個のcppファイルと5個のライブラリからなる私のプロジェクトは、ほぼ1分間コンパイルされ始めました。 ツールチェーンのコンパイラ、リンカー、およびその他のプログラムが何回起動されるか知っていますか? 240回!!!
それにもかかわらず、アセンブリ自体には軍事的なものは何もありません。 私は、arduinoなしでは繰り返せないいくつかの隠されたアセンブリメカニズムを恐れていました。 実際、すべては、特定のキーのセットを使用してコンパイラーとリンカーを順次呼び出して収集されます。 そのため、別のビルドシステムでも同じことが繰り返されます。
STM32およびstm32duinoフレームワークでビルドする際に、ビルドシステムの作業を調査したことは注目に値します。 しかし、ATMegaコントローラーの従来のarduinoでビルドする場合、すべてがほぼ同じです(少しだけ単純です)
CMakeを試す

ワークショップの同僚は、CMakeを見ることを勧めました-彼らはすでにArduinoプロジェクトを組み立てるための既製のツールを持っています。 だから、私はCMakeをインストールし、例から最小限のプロジェクトを取り、CMakeList.txtを少し提出しました。 しかし、CMakeを開始したときに、構成段階(コンパイラー、リンカーなどがチェックされるとき)でld.exeでクラッシュしました。 何が起こっているのかを正確に理解する試みは失敗に終わりました-別に起動された同じオンラインコマンドは問題なく実行されました。 私はこれを回避する方法を理解していませんでした。
解決策を探して、ツールチェーンファイルを注意深く調べたところ、そこをまったく掘り下げていなかったことがわかりました。 実際のarduinoには、コンパイラ(必ずしもAVRとは限りません)でボードを追加できるプラグインシステムがありますが
、私が研究しているツールチェーンの1つは AVR専用です。 さらに、2〜3種類の標準ボードしか使用できません。
2番目のツールチェーンはより良く見えました。 彼は、boards.txtなどのArduinoファイルを解析し、optionsディレクトリを調べ、利用可能なすべてのアセンブリオプション、ボード、およびプロセッサを収集しました。 それにもかかわらず、何かが私を混乱させました。 出口で、私は再びarduinoスタイルのモノリスを手に入れると思われました。 ARMコンパイラがAVRの代わりに機能するかどうかはあまり明確ではありませんでした。 さらに、ライブラリを制御する方法はまだ明確ではありません。
一般に、このツールチェーンについて悪いことを言うことはできません。 単純に、メイクアップファイルを生成する際にクラッシュを無効にすることなく、他の場所で解決策を探すことにしました。
クーコックス
良いIDEでコードを書くのが大好きです。 インターネットは、STM32で最も人気のあるIDE(Keil、IAR、CooCox)と呼ばれることがよくあります。 最初の2つは有償で、使いやすさのトピックに関する質素なレビューがあります。 しかし、CooCoxは高く評価されています。 また、Eclipseに基づいて構築されているという事実によって賄われており、私はずっと彼に会いたかった。 これにより、ビルドプロセスを微調整できるように思えました。 悲しいことに、私は間違っていたと言います。
CooCoxをインストールしたので、コンパイラがなくても驚いた。 一般に、これは理解できます-IDEは別個であり、コンパイラは別個です。 Visual Studioで何年も使用した後も、これは珍しいことです。 まあ、大丈夫、これは
コンパイラーを個別のインストーラーとしてインストールすることで簡単に解決できますが、最初はコンパイラーをAtmel Studioから取りました。

そして、問題が始まりました。 1時間、LEDの点滅を開始しようとしました。 インターネットでは、さまざまな程度のコンパイルのオプションが約12個見つかりました。 どこで間違えられるのでしょうか? いえいえ コンパイル、塗りつぶし-動作しません。 プロジェクトを数回再作成しましたが、役に立ちませんでした。 その結果、彼はヌビアの指示そのものに従って、すべての指示を明確かつ逐語的に実行しました。 ファームウェアはUARTを介してダウンロードされ、システムブートローダーと電球が楽しく点滅しました。
問題の原因であるstm32duino / libmaple USBブートローダーを非難したいと思いますが、STM32コントローラーの初期化プロセスの誤解を認める方が合理的です。 質問を理解したので
、別の記事で 「発見」について説明し
ました 。 一言で言えば、問題はファームウェアの誤った開始アドレスにありました。 ブートローダーを使用する場合、ファームウェアは異なる開始アドレスで組み立てる必要があります。 実際、このパラメーターは従来のarduinoにはありませんが、STM32マイクロコントローラーにとっては非常に重要です。
STM32ブートローダーに関する考察もちろん、UARTを介してフラッシュすることは可能ですが、これはまだhemoです。 STフラッシュデモンストレーターでは、常にブートジャンパーを切り替えて、マウスで多くクリックする必要があります。 私は中国のST-Linkを持っています。それを接続し、それを通して縫い、そしてデバズすることができました。 本当にインサーキットデバッグが必要なときにこれに到達するでしょう。
しかし、現時点では、いくつかの理由からUSB経由のファームウェアが最も便利だと考えています。 とにかく、電源とUSBシリアル用のUSBケーブルを接続しました。 また、すぐにUSB大容量ストレージを積極的に選択する予定です。 最終的に、USBは完成したデバイスにも出力されます。 では、なぜUSB経由でフラッシュするのではなく、追加のコンポーネントを使用するのでしょうか?
構成管理のトピックに戻ります。具体的には、ライブラリを保存する場所と方法について説明します。 「私はすべてを私と一緒に運ぶ」というオプションに引き寄せられます。 リポジトリ内のすべてのライブラリの変更(構成を含む)をバージョン管理したいと思います。 もちろん、古典的なUnixパッチもありますが、前世紀のようです。 しかし、見返りに何を使うべきでしょうか? 「1つのチームでソースコードを引き出し、構築し、MKにアップロードする」というスタイルのソリューションが必要です。
長い間、gitでサブモジュールとサブツリーを処理しようとしました。 私にとって、Gitをきちんと使用したことがない人としては、何も明確ではありません。 私はGit Bookの最初の数章も読んでいましたが、さらに混乱しました。 私はこれらの問題についてより経験豊富な同僚に助けを求めましたが、彼らはさらに指を曲げ、それはさらに明確になりませんでした。
サブツリーを介して行うことにしました 。 何が私を脅かすか想像できません。
リポジトリに必要なファイルを直接取得したため、サブツリーが気に入った。 そして、それらを編集する機能を備えています。 ただし、サブモジュールでは、変更を別のリポジトリのどこかに保存する必要があります。 バグ修正のためにこれが妥当である場合(簡単に分岐して修正をメインリポジトリにプッシュできます)、ライブラリ設定を保存するためにこれは少なくとも奇妙です。 異なるリポジトリにある1つのプロジェクトの変更をバージョン管理することは望ましくありません。
そこで、STM32F1xxシリーズコントローラーのstm32duinoをレポジトリに追加しました。 サンプルとライブラリを使用して、完全に強化されました。 正直に言うと、私はその構造が好きではなく、そこからすべてを必要としているわけではありません。 しかし、私はそれを別の方法で、そしてそれを後で保持する方法を知りません。 CooCoxプロジェクトで好きなようにすべてのファイルをレイアウトしたという事実によって補われました。
その結果、必要なすべてのファイルをプロジェクトに取り込み、Arduinoと同じ定義を設定して、リンクプロセスを開始しました。 間違って行った。 シンボルがない場合、どのソースが宣言されているかを調べ、このソースのみを追加しました。 非常に興味深い経験でした 途中で、それが何でどこにあるのか(stm32duino / libmapleを意味する)とそこでの動作を理解しました。

前述したように、ARM(STM32)でのアセンブリは、AVRでのアセンブリよりも複雑です。 問題は、コンパイラがより一般的であり、非常に細かく設定できることです。 一方で、これは適切であり、アセンブリプロセスを調整できます。他方では、構成が大幅に複雑になります。 そのため、ここではリンカスクリプトが追加で使用されます。結果のバイナリにどのセクションを配置するか、これらのセクションがどこにあるか、それらがどのくらいのスペースを取るかを記述する特別なファイルです。
低レベルのコードは、リンカスクリプトで宣言された文字を参照でき、逆もまた同様であることが判明しました。 リンカーが標準設定で呼び出された場合、これらの文字は見つかりません。 この問題を解決するために、リンカ設定ページで、[メモリウィンドウからメモリレイアウトを使用する]のチェックを外しました-スキャッタファイルフィールドが開きました。 そこで、Arduinoビルドシステム(
STM32F1 / variant / generic_gd32f103c / ld / bootloader_20.ld )で使用されるリンカースクリプトを登録しました。 その後、すべてがすぐにリンクされました。

しかし、ここで不愉快な驚きが私を待っていました-組み立てられたファームウェアはすぐに115kbを取りました(arduinoでは56kでした)。 さらに、すべてのキャラクターがそこで混同され、ファームウェアには多くのC ++ランタイムが含まれていました-rtti、manglers、abi、executions、そしてまだ不必要な多くのもの。 ArduinoとCooCoxが作成し、各キーのドキュメントを吸うリンカーコマンドラインを比較する必要がありました。
CooCoxでは、CコンパイラとC ++コンパイラを選択できないことが判明しました。 このため、各コンパイラを個別に構成することはできません。 そのため、C ++コードはgcc(g ++ではなく)を使用してコンパイルされ、デフォルトで大量のコード、rtti、ABIなどにリンクします。 そして、骨はそれほど簡単ではないようです。 コンパイラの追加フラグ用のフィールドがありますが、-fno-rttiなどを追加すると、gccはすぐに誓い始めます。

インターネットでは、人々はCooCoxに2番目のバージョンを提供しました。すでに多くの設定があり、CコードとC ++コードを区別でき、ファイルタイプごとに正しいコンパイラを呼び出します。 ただし、同時に、コンパイラーごとに個別にキーを構成することはできません。 そのため、-fno-rttiキーはg ++だけに追加することはできません。gccにも送信されます。
一般に、2番目のCooCoxはそのままでは入場しませんでした-利便性を犠牲にして、魅力的なUIが多すぎます。 ちなみに、最初のUIはUIの点でも優れていません。100万の設定があり、ビルドコンソールのフォントは変更できません(完全なEclipseでは可能ですが)。
CMakeもう一度
CooCoxのようなよく知られたツールでは、コンパイラーの簡単な構成ができないため、nafigを使用します。 最下層に行き、すべてを手で書きます。 まあ、あなたの手で...もちろん、メイクファイルで。
前回、ほぼ10年前に裸のメイクファイルを作成しました。 私はすでにすべての微妙さを忘れましたが、これは非常に感謝のない仕事であることを確かに覚えています。 風変わりな構文と間違いを犯しやすい。 そして、それは非常にポータブルではありません。 CMakeをもう一度試してみることにしましたが、arduinoツールチェーンではなく、STM32で試してみました。 そして、すべてのルールを使用して、make、eclipse、コンパイラ、CMake、MinGW32を個別にインストールしました。
私はタルチェーンを見ませんでした。 これは、私が一般的なアイデアを得たところから目を引いたが、ツールチェーン自体は
ここから取っ
た 。 私はそれを普通の山に設置するのではなく、携帯することを決めました。 さらに、すべてが必要というわけではありませんが、共通の変数とプロシージャが宣言されている
gcc_stm32.cmakeと、コントローラーパラメーターが記述されている
gcc_stm32f1.cmakeの 2つのファイルだけが必要です。
私が持っているすべてのライブラリは、(理論的には)メインリポジトリと同期するディレクトリにあります(方法を理解したら)。 したがって、CMakeList.txtを各ライブラリに追加するのは、どういうわけか不快です。 ライブラリディレクトリに
一般的なCMakeList.txtを
1つ作成し、その中のすべてのライブラリのアセンブリを記述することにしました。 各ライブラリはアーカイブ(静的ライブラリ)に収集され、すべてがバイナリでリンクされます。
ライブラリを構築するためのCMakeスクリプト(すべてのライブラリに1つのスクリプト) 本体アセンブリには、以下で説明する重要なものがいくつかあります。 これまでのところ、
CMakeLists.txt全体を
提供します。
CMake自体のパラメーターについては。
stm32-cmakeの例
では 、使用するツールチェーンファイルを指定する
ことをお勧めします。 これは、メイクファイルを生成するCMake呼び出し中に別のキーで実行されます。 しかし、異なるプラットフォームやコンパイラー向けにプロジェクトをビルドする予定はありません。 したがって、必要なツールチェーンファイルへのリンクをメインCMakeLists.txtに登録しました。
ただし、コンパイラは自動的に推測しません。 より正確には、Unix(/ usrのデフォルトのパスで検索されます)で推測していましたが、Windowsでは明示的に指定する必要があります。 一般に、私のコマンドライン(WindowsのスラッシュをUnixに変更することを忘れないでください-CMakeはそれらを気に入らない)
cmake -G "MinGW Makefiles" "-DTOOLCHAIN_PREFIX=C:/Program Files (x86)/GNU Tools ARM Embedded/5.4 2016q3" .
メイクアップファイルが生成され、プロジェクトのビルドを開始できます。 コンパイルに特別な問題はありませんでした。 しかし、その後、強力なリンカーマジックとの闘いが始まりました。 以下は、私が途中で解決した個々の問題のリストです。
問題:ライブラリ間の相互依存関係
stm32duinoフレームワークはかなり大きいです。 ライブラリ用のCMakeList.txtを作成する段階でも、Arduino API(stm32duino自体)をエミュレートする部分と、ハードウェアアブストラクションレイヤー(HAL)でありマイクロコントローラの低レベル部分を隠すlibmapleをエミュレートする2つのライブラリに分割しようとしました。 いずれにせよ、それは私にとって理にかなっているように思えました。 しかし、リンクするには問題があることが判明しました。 stm32duinoはlibmapleの上に構築されていると思いましたが、2番目には最上層への呼び出しもありました。
冗談は、リンカーがライブラリを一度に1つずつ収集し、以前のライブラリに戻らないということです。 2番目のライブラリが最初のライブラリを呼び出す場合、リンカーは最初のライブラリを再度リンクする必要があることを理解しません。 したがって、未解決の文字が発生します。 プロジェクトでは2つのライブラリを1つに結合する必要がありました。
しかし、その後、リンカの動作を変更する特別なキー-Wl、-start-group / --end-groupがリンカにあることがわかりました。 彼らは彼に数回図書館を通過させるだけです。 まだ試していません。
stm32duinoとlibmapleをビルドするCMakeスクリプト 問題:システムコールがリンクしない
さらに、標準ライブラリと正常にリンクすることはできませんでした-_sbrk()、_ open()/ _ close()、_ read()/ _ write()、および何らかの理由で標準ライブラリから突き出ている他のいくつかが欠落していました。
実際、それらは
STM32duino \ variant \ generic_stm32f103c \ wirish \ syscalls.cに些細な実装を持っていますが、同じ理由でリンクしませんでした:stm32duinoをリンクするときにこれらの関数(弱いと宣言されています)は不要であり、捨てられます。 標準ライブラリは、リンクプロセスの最後で暗黙的に接続され、これらの文字を要求し始めますが、リンカーは戻りません。 もちろん、syscalls.cファイルを他のすべての後に個別にリンクできますが、純粋にスポーツの興味から、どこから来たのかを把握し始めました。
一部のシステムコールの簡単な実装 __weak int _open(const char *path, int flags, ...) { return 1; } __weak int _close(int fd) { return 0; } __weak int _fstat(int fd, struct stat *st) { st->st_mode = S_IFCHR; return 0; } __weak int _isatty(int fd) { return 1; } __weak int isatty(int fd) { return 1; } __weak int _lseek(int fd, off_t pos, int whence) { return -1; } __weak unsigned char getch(void) { return 0; } __weak int _read(int fd, char *buf, size_t cnt) { *buf = getch(); return 1; } __weak void putch(unsigned char c) { }
私が理解しているように、arm-gccコンパイラは非常に強力であり、既存のマイクロコントローラとプロセッサのかなりの半分でコンパイルできます。 しかし、小さなメモリを搭載した小型のマイクロコントローラー、またはLinuxをねじることができる厚い「石」になる可能性があるため、プラットフォームの機能を考慮して組み立てる必要があります。 したがって、リンカーには、特定のプラットフォームにリンクする必要があるものを正確に記述する* .specsファイルのセットがあります(これはリンカースクリプトに追加されます)。
そのため、デフォルトでは、標準ライブラリはプロジェクト(この場合は
newlib-nano)にリンクされています。 大型コンピューターのlibcがシステムコールとカーネルに依存している場合のみ、newlib-nanoの場合、ユーザーはこれらのシステムコールを提供する必要があります。 したがって、リンカでは、これらと同じ_sbrk()、_ open()/ _ close()、_ read()/ _ write()の宣言が必要です。
この問題は、リンカー設定に--specs = nosys.specsキーを追加することで解決しました。 このキーは、標準ライブラリの一部のリンクが無効になっているスペックファイルをリンカに示します。
問題:初期化コードがリンクしない
ある時点で、_init()関数の問題が明らかになりました。 newlib-nanoからこの関数の呼び出しがありますが、コードではこのシンボルはどこにも宣言されていません。 ここでのデザインの考え方は次のとおりだと思います。
- リセットまたは電源投入後、最低レベルの起動が実行されます。 libmapleの場合、これはSTM32duino \ variant \ generic_stm32f103c \ wirish \ start.Sです。 このコードのタスクは、制御を初期化関数に転送することです
start.S .type __start__, %function __start__: .fnstart ldr r1,=__msp_init mov sp,r1 ldr r1,=start_c bx r1 .pool .cantunwind .fnend
- 次のステップは、メモリを準備することです。 ここで、.bssセクション(ゼロで埋められた変数)を消去し、.dataセクションの変数に初期値を入力しますコードは、ファイルSTM32duino \ variant \ generic_stm32f103c \ wirish \ start_c.cにあります
start_c.c void __attribute__((noreturn)) start_c(void) { struct rom_img_cfg *img_cfg = (struct rom_img_cfg*)&_lm_rom_img_cfgp; int *src = img_cfg->img_start; int *dst = (int*)&__data_start__; int exit_code; if (src != dst) { int *end = (int*)&__data_end__; while (dst < end) { *dst++ = *src++; } } dst = (int*)&__bss_start__; while (dst < (int*)&__bss_end__) { *dst++ = 0; } __libc_init_array(); exit_code = main(0, 0, 0); if (exit) { exit(exit_code); } for (;;) continue; }
- その後、制御はnewlib-nanoから__libc_init_array()関数に転送されます。 この関数では、グローバルオブジェクトコンストラクターだけでなく、ボードを初期化するためのpremain()およびinit()関数を含む初期化子が呼び出されます。
__libc_init_array() void __libc_init_array (void) { size_t count; size_t i; count = __preinit_array_end - __preinit_array_start; for (i = 0; i < count; i++) __preinit_array_start[i] (); _init (); count = __init_array_end - __init_array_start; for (i = 0; i < count; i++) __init_array_start[i] (); }
- 同時に、__ libc_init_array()を使用すると、初期化の特定の段階の間にユーザー定義関数をフックして呼び出すことができます。 そして、この関数は_init()と呼ばれるべきです。 前のセクションの他のシステムコールと同様に、この関数は他の誰かが提供する必要があります。
- 次にmain()が呼び出されますが、現時点ではこれには興味がありません。
私が理解しているように、_init()関数は選択したプラットフォームに応じて置換する必要があります。たとえば、その実装は次のとおりです。<ARM Toolchain> \ lib \ gcc \ arm-none-eabi \ 5.4.1 \ armv7-m \ crti.o Arduinoはなんらかの方法で暗黙的にこのオブジェクトを接続しますが、このファイルは私のためにプルアップしませんでした。 これはおそらく、-specs = nosys.specsキーを使用して標準ライブラリの一部を無効にしたためです。
ここからの推奨で、空の実装をlibmapleコードに追加しました。
void __attribute__ ((weak)) _init(void) {}
良い意味では、この関数は空にしないでください。 premain()が行うことを行う必要があります-ボードを初期化します。 しかし、stm32duinoの作成者は異なる決定をしました。
一般に、すべてがリンクされていましたが、ファームウェアはまだ非常に大胆でした-120kb以上。 そこで余計なものを理解する必要がありました。 これを行うには、ツールチェーンのスクリプトを慎重に研究し、既にアセンブルされたものを分解する必要がありました。
問題:コードセクションが誤って定義されている
最初に目を引いたのは、開始アドレスです。 開始アドレスは0x00000000または0x00008000のいずれかでしたが、0x08002000であることは間違いありません。 STM32 CMakeツールチェーンを慎重に研究した結果、必要なパラメーターがリンカスクリプトで設定されていることに気付きました。
これはツールチェーンにも付属しています。 このスクリプトのみがデフォルトでは使用されませんが、別のコマンドSTM32_SET_TARGET_PROPERTIES()に含まれています。 開始アドレスが修正され、ファームウェアは100kにまで減りました。
これで、コードのファームウェアの最初に割り込みベクターのテーブルがありませんでした。 ファイル内のセクションの説明から判断すると、テーブルは別のセクション.isr_vectorにありますが、何らかの理由でそのサイズはゼロです。
1時間、私はリンカースクリプトを理解しようとしていました。 しかし、いくつかの非常に強力な低レベルの魔術がそこで行われます。いくつかのセクションが決定され、コードへの何らかの参照があります。 特に、私が理解しているように、割り込みベクトルのテーブルを含むセクションは、コードのどこかにある方法で説明する必要があります(おそらく
CMSISにあります。 つまり 私の間違いは、libmapleからの初期化コードで
汎用リンカースクリプトを使用しようとしていたことです。コードとデータセクションの名前と配置は異なります。
解決策は、libmapleからリンクスクリプトを明示的に指定することでした(
STM32duino / variant / generic_stm32f103c / ld / bootloader_20.ld )
問題:不適切な最適化設定
しかし、これは残念です。 リンカは、生成されたコードをフラッシュドライブに収めることができないと言った。 多数の不要なマックがファームウェアに侵入するという事実に加えて、CMakeはデフォルトでバーゲン構成で収集することが判明しました。 デバッグ構成では、最適化は-O2(速度の最適化)であり、リリース構成では-Os(サイズの最適化)です。 リリース構成に切り替えましたが、それでもうまくいきませんでした。ツールチェーンは-fltoフラグ(Link Time Optimization)を設定します。これはstm32duinoの半分ではビルドされません。
一般に、私は2つの構成からすべてのベストを取りました。 リリース構成から、-Osスイッチを使用しました。 デバッグから-gを取りました。これは、デバッグ情報をbinarに追加します。 とにかく、この情報はファームウェアには入りません。分解する方がはるかに便利です。
ファームウェアのサイズをさらに小さくするには、正しいコンパイルキーとコード生成キーの選択、および元のビルドシステムが示す必要なすべての定義の繰り返しが必要です。 もう一度、キーのリスト全体を比較して、プロジェクトにカップルを追加することにしました。
- -DDEBUG_LEVEL = DEBUG_NONEは、 libmaple内のロギングを無効にします。 定義すると、結果のファームウェアから約キロバイトが削除されます
- -fno-rtti -fno-exceptions-大量のコード(同じRTTI、例外、ABIなど)を削除します。 当然、これらのフラグはg ++
- -fno-unroll-loops -ffast-math -ftree-vectorizeの組み合わせは、少しコンパクトなコード(ファームウェア全体で100〜200バイト)を生成するだけです。
ここに私のキーがあり、最終的にコンパイラに渡します:
SET(CMAKE_C_FLAGS "-mthumb -fno-builtin -mcpu=cortex-m3 -Wall -std=gnu99 -ffunction-sections -fdata-sections -fomit-frame-pointer -mabi=aapcs -fno-unroll-loops -ffast-math -ftree-vectorize -nostdlib -march=armv7-m --param max-inline-insns-single=500" CACHE INTERNAL "c compiler flags") SET(CMAKE_CXX_FLAGS "-mthumb -fno-builtin -mcpu=cortex-m3 -Wall -std=c++11 -ffunction-sections -fdata-sections -fomit-frame-pointer -mabi=aapcs -fno-unroll-loops -ffast-math -ftree-vectorize -fno-rtti -fno-exceptions -nostdlib -fno-use-cxa-atexit -march=armv7-m --param max-inline-insns-single=500" CACHE INTERNAL "cxx compiler flags") SET(CMAKE_ASM_FLAGS "-mthumb -mcpu=cortex-m3 -x assembler-with-cpp" CACHE INTERNAL "asm compiler flags") SET(CMAKE_EXE_LINKER_FLAGS "-Wl,--gc-sections -mthumb -mcpu=cortex-m3 -march=armv7-m -mabi=aapcs -Wl,--warn-common -Wl,--warn-section-align" CACHE INTERNAL "executable linker flags")
ビルドシステムに対する勝利
ファームウェアの結果は48キロバイトです。 Arduinoバージョンは56kbでした。 違いは、malloc / freeおよび関連する関数が不足しているためです。これらはまだ使用していません。 完全な再構築には、arduinoで1分かかるのに17秒かかります。 わずか1〜2秒でインクリメンタルアセンブリ。
真実の瞬間とチップで何が起こったのかを記入してみてください。 正直に言うと、タンバリン、Google、CMakeの会社でこのような長いダンスをした後、ファームウェアがすぐに起動したことに非常に驚きました。 この鉄片が少なくとも電球で点滅したくない理由を理解するために、ブラックボックスモードでのハードデバッグのもう1週間を期待していました。
美しさのために、呼び出しSTM32_PRINT_SIZE_OF_TARGETS()を追加しました-アセンブリ後、メモリの統計がコンソールに書き込まれます。 メモリ消費が急増したかどうかをすぐに確認できます。

また、ファームウェアを分解するためのターゲットを追加しました(現時点では、メインブランチ
stm32-cmakeで既に提供されてい
ます )。 そこにコンパイラが行ったことをビルドしてすぐに確認するのは非常に便利です。ファームウェアが突然大きくなってしまったのです。
FUNCTION(STM32_ADD_DUMP_TARGET TARGET) IF(EXECUTABLE_OUTPUT_PATH) SET(FILENAME "${EXECUTABLE_OUTPUT_PATH}/${TARGET}") ELSE() SET(FILENAME "${TARGET}") ENDIF() ADD_CUSTOM_TARGET(${TARGET}.dump DEPENDS ${TARGET} COMMAND ${CMAKE_OBJDUMP} -x -D -S -s ${FILENAME} | ${CMAKE_CPPFILT} > ${FILENAME}.dump) ENDFUNCTION()
QtCreator
上記のアセンブリプロセス全体をコンソールで簡単に実行しました。 次に、IDEを接続します。 すでにEclipseについて知りたいと言っていましたが、インストールしたとき、それはひどくモンスターのように見えました。 しかし、最も重要なことは、ワークショップの概念を理解していなかったことです。 私の理解では、プロジェクトを開くと、このプロジェクトのすべてのファイルがすぐに表示されます。 Eclipseは多くの余分な身体の動きをしなければなりませんでした。 一言で言えば、彼はちょうど入っていませんでしたが、私は間違いなく彼にもう一度戻るつもりです。
私はかつてQt Creatorでプログラミングしたことを思い出しましたが、それは悪くないようです。 Qt Creator 4.2.2をインストールした後、CMakeプロジェクトの接続方法をグーグルで調べ始めました。 インターネット上の指示に従って、CMakeLists.txtファイルを単に開き、ウィザードの指示に従うことをお勧めしました。 まず第一に、彼はツール(キット)のインストールを提案しました。 ひどいカスタムアセンブリを考えると、かなり合理的です。

Qt Creatorはそのようなキットを誓います、彼らはコンパイラが公開されていないと言います。 しかし、あなたがそれを設定すると、それはまだ誓います-CMake自体を設定するコンパイラ(arm-gcc)は、(同じであっても)対応するフィールドで選択されたものと一致しません。 ただし、彼はすべてを正常に構築します。
プロジェクトのセットアップ時に、非常に重要な瞬間が1つ発生しました。 CMakeプロジェクトをインポートすると、CMakeLists.txtのみがインポートされました。 プロジェクトにソースコードは追加されていません。 私は数晩インターネットを勉強しました-役に立ちませんでした。 すべてはすべての人に有効ですが、私には有効ではありません。 問題はカスタムツールチェーンにあると思いましたが、MinGW32の最も単純なHello Worldは同じ方法でインポートされませんでした。
ソリューションはQtCreator自体によって提案されました。 私が作成したツールキットのツールは、間違ったタイプのCMakeジェネレーターを選択したと言っています。 コードブロックを選択する必要があります-これがないと、Qt Creatorはプロジェクトを解析できません。 正しいジェネレーターをインストールすると、すべてのファイルとサブディレクトリがプロジェクトウィンドウに表示されました。

これで、ファイル間を簡単に移動できますが、オートコンプリートはまだ機能していません。 実際、どんなに奇妙に聞こえても、Qt CreatorはCMakeLists.txtで指定された包含パスを単に無視しました。 つまり アセンブリは正常に動作し、エディターでは、#include'ovのほとんどがエラー(そのようなファイルはありません)として強調表示されます。 同じディレクトリからの作業のみが含まれます。 すべての設定を登り、数時間グーグルで検索しましたが、答えが見つかりませんでした。
更新 :コメントでは、オートコンプリートはコンパイラーを介して機能することを示唆しました。 したがって、キット設定で正しいコンパイラを設定することが重要です。 ただし、Qt Creatorは依然として誓いますが、オートコンプリートは機能します。
テキストエディタを本格的なIDEにする最も便利なことの1つは、ファームウェアの自動入力と起動です。 私はそのようにしました。 私にとっての主なビルドはGPSLogger.binターゲットであり、古典的なすべてではありません。 このターゲットは、.elfファイルから.binファイルを作成します。このファイルは、既にコントローラーに挿入できます。 展開手順により、ファームウェアの「フラッド」が起動します。

ping呼び出しのトリックに注意してください。
そのタスクは、充填と開始の間に1秒の休止を設けることです。それ以外の場合、マイクロコントローラはUSBを再起動して初期化する時間がありません。奇妙なQtCreator,
QtCreator. powershell, timeout .
「ランナー」(実行)として、ターミナルエミュレータを使用すると便利であることがわかりました。このことにより、デバッグ出力をシリアルでキャッチできます。つまり
実際、ロード後のファームウェアは自動的に起動します。シリーズに書かれているものを見たいかどうかだけを選択します。しかし、今日のデバッグについてはそうではありません。第一に、これについてはすでに何度も書かれていますが、新しいものを発明するとは思いません。次に、デバッガーを使用しません。このプロジェクトでは、インサーキットデバッグを必要とするヒープペリフェラルを使用しません。いくつかのUARTとI2Cのみを使用しています。そのため、ログの印刷をディベースすることはかなり可能です。そして第三に、私はまだ一ヶ月間コードを書いていません-私はすべてビルドシステムとIDEを扱っています。あと1週間か2週間かけてデバッガーに飛び込みたいとは思いません。誰が必要か-GoogleデバッガをさまざまなIDEにねじ込むことに関する多くの記事があります。おわりに
アセンブリチュートリアルを作成するという目標は設定しませんでした。むしろ、私のプロジェクトに適したビルドシステムを見つけるために、ここで私の痛みと苦しみについて説明します。この記事では、STM32コントローラーでプロジェクトをビルドするためにCMakeに切り替えた経験を述べました。libmapleに基づいたstm32duinoフレームワークを使用します。これは非常に標準的なフレームワークではないため、多くの困難と重要な瞬間がありました。作業の過程で、ボードの初期化方法、libc構造とそれが依存するもの、コードセクションでメモリを誰がどのように分散するかなど、リンカの複雑さを理解しました。通常、これらはCRT、コンパイラ、IDEの腸に隠されていますが、この場合、これらのニュアンスを明確に掘り下げる必要がありました。この記事で説明するシャーマニズムのほとんどは、STM32専用のアセンブリに関連していることに注意してください。ATMegaの従来のarduinoとアセンブリは簡単です。しかし、多くのアイデアはプラットフォームに関係なく同じままです。思慮深い読者は、私の記事にも多くの有用なものを見つけると思います。おそらく、私は特定のことを完全に開示したり、誤った判断をしたりしませんでした。まあ、私はエラーの可能性を排除しません。いずれにせよ、私は建設的な批判を受け入れています。