Previous Post | Top | Next Post |
TOC
前回に続きArduino Uno/Nano に使われている基本のシリアルAVRの ATmega328P を中心としてAVRチップのプログラムの勉強・練習の続きをしました。
AVRのコーディング上の注意点
以下をAVRのコーディング上の注意点の参考にしました。
- 2003年出版のIAR CベースのAVR035: Efficient C Coding for AVR
- 2011年出版のGCC4.5ベースのAtmel AVR4027: Tips and Tricks to Optimize Your C Code for 8-bit AVR Microcontrollers
わたしの現在の開発環境で使うのは、avr-gcc (GCC) 5.4.0です。“c99 plus GCC
extensions"となるように-std=gnu99
をWinAvrベースのMakefile
に指定してます。
ちなみに、avr-gcc (GCC) 5.4.0では-std=C99
は"ISO C99 standard"完全準拠の
はずです。avr-gcc helpの記述に-std=c11
、-std=c++11
がありISO 2011 C/C++
まで完全準拠が選べるようです。WinAvrのMakefile内に書かれた
「ISO C99準拠が不完全」とのコメントは古いバージョンのavr-gccに基づいている
ようですね。確かGCCは4以降では"ISO C99 standard"完全準拠だった気がします。
ちなみに、-std=gnu99
では、-std=c11
で導入された0b10111011
と言った
2進表記での整数定数表記ができます。これが便利です。
8 bitのATmega328Pぐらいにフォーカスして、現在の開発環境下で私なりに検討して、 ある程度の憶測も折り込み、よくまとまっていたAVR035文書の結論を以下に書きなお して見ました。
あまりコードサイズの最適化に頑張り過ぎることは無いと考えています。無理がある 時には、コードを凝るのではなく、所詮趣味でなので実現する機能を削るとか、値段 は知れているので高性能のチップに乗り換えるとかする方が良いのでしょう。実際 Arduino Nanoに載っているATmega328pですらかなり余裕があるので、読みやすくバグ 発生しにくいコーディングを心がけ、デバグしやすいように部分・部分をテストでき るモジュラーな構成を念頭にするもがいい気がします。
そういった意味では、裸のavr-gcc+avr-libcでの開発に拘らずに、Arduino系のインフ ラを利用したコード開発をすれば楽になる面もあるのも事実です。このメモ作成は、 あくまでAVRのコーディングの基礎学習行為の備忘録です。
Hints to Reduce Code Size
- Compile with full size optimization. (use avr-gcc
-s
option) - Use local variables whenever possible.
- Use the smallest explicit applicable data type such as
uint8_t
with<stdint.h>
. Use unsigned if applicable. - If a non-local variable is only referenced within one function, it should be declared
static
. - If a function is only referenced within one file, it should be declared
static
. - Specify
inline
for functions in library performing tasks that generates less than 2-3 lines assembly code. - If possible, avoid calling functions from inside the interrupt routine.
- Collect non-local data in structures whenever natural. This increases the possibility of indirect addressing without pointer reload.
- Use descending loop counters and branch on zero with pre-decrement if applicable.
- Use
do { } while(expression)
if applicable. - Access I/O memory directly (i.e., do not use pointers). Use
_BV(bit)
,_SFR_IO8(mem_addr)
, and other MACROs defined inavr/sfr_defs.h
. - Code reuse is intra-modular. Collect several functions in one module (i.e., in one file) to increase code reuse factor.
- Use pointers with offset or declare structures to access memory mapped I/O.
- Store Indirect to SRAM with Pre-decrement and Load Indirect from SRAM with Post-increment.
- In some cases, full speed optimization results in lower code size than full size optimization. Compile on a module by module basis to investigate what gives the best result.
- Declare main as
__attribute__((noreturn))
if not called from anywhere in the program. - Reduce the size of the Interrupt Vector segment (
INTVEC
) to what is actually needed by the application. Alternatively, concatenate all theCODE
segments into one declaration and it will be done automatically. - Optimize C_startup to not initialize unused segments (i.e.,
IDATA0
orIDATA1
if all variables are tiny or small).
ここで、16、17、18は、普通ここまでしない感があります。 ATtiny13A向けにコンパイル結果をアッセンブリーでハンド チューンして小さくしたい時等のテクニックの感があります。
Hints to Maximize Code Speed
- Use “if-else” and “switch-case” properly.
- For “if-else”, always put the most probable conditions in the first place.
- For “switch-case”, the compiler usually generates lookup tables with index and jump to the correct place directly. (要確認:
-03
のの場合?)
Five Hints to Reduce RAM Requirements
- All constants and literals should be placed in Flash by using
PROGMEM
,PSTR
, andPGM_P
MACROs. - Avoid using global variables if the variables are local in nature. This also saves code space. Local variables are allocated from the stack dynamically and are removed when the function goes out of scope.
- If using large functions with variables with a limited lifetime within the function, the use of subscopes can be beneficial.
- Get good estimates of the sizes of the software Stack and return Stack (Linker File).
- Do not waste space for the IDATA0 and UDATA0 segments unless you are using tiny variables (Linker File).
Checklist for Debugging Programs
- Ensure that the CSTACK segment is sufficiently large.
- Ensure that the RSTACK segment is sufficiently large.
- Ensure that the external memory interface is enabled if it should be enabled and disabled if it should be disabled.
- If a regular function and an interrupt routine are communicating through a global variable, make sure this variable is declared volatile to ensure that it is reread from RAM each time it is checked
検証のメモ
C_task
これはIARのCコンパイラー特定の書法です。
スタックを気にするなら、GCCのCコンパイラーでは最新「AVR Libc Reference Manual」の
Non-Returning main()
にあるように、__attribute__((noreturn))
を使うのですかね。
Macros vs. Functions
AVR035のP20に高らかに、「短い関数はマクロにした方がよい」とあります。
「C89(gcc3シリーズ)の昔ならいざ知らず、C99(gcc4シリーズ)以降なら
static inline
と書けばコードが埋め込まれるので問題ないのでは?」
と不思議に思っていました。
現在(2020, Debian buster)のavr-gcc (GCC) 5.4.0を用いて実状を確認しました。 (コードはエラーやワーニングが出ないようにちょっと手を入れてます)
Program 1: 152 Byte
#include <avr/io.h>
/* Read UART value and convert it to ASCII*/
char read_and_convert(void) { return (PINB + 0x48); }
/* Write to UART*/
int main(void) { UDR0 = read_and_convert(); }
Program 2: 146 Byte
#include <avr/io.h>
/* A macro to read UART value and convert it to ASCII*/
#define read_and_convert() (PINB + 0x48)
/* Write to UART*/
int main(void) { UDR0 = read_and_convert(); }
Program 3: 146 Byte
#include <avr/io.h>
/* Read UART value and convert it to ASCII*/
static inline char read_and_convert(void) { return (PINB + 0x48); }
/* Write to UART*/
int main(void) { UDR0 = read_and_convert(); }
これは、-s
の「optimize for size」でコンパイルした場合ですが、
Program 1とProgram 2だけを比べるとマクロがプログラムサイズで有利にも
見えますが、チャンとstatic inline
と書けば読みづらいマクロを使わなくとも
プログラムサイズを抑えられることが確認できました。
static inline
とまで書かなくとも、static
かinline
のいずれか1つでも
指定すれば146 Byteになります。
この6バイトの差ですが、winavrのMakefileでコンパイルした際に生成される.lss
拡張子の付いたファイルの内容を見れば分かります。
すべてのコードでmain()
への関数埋め込みでコードサイズが小さくなるように
なっています。GCCは賢いですね。
ただ、static
かinline
のいずれもかかれていないと、read_and_convert()
が外部から呼び出されれも良いように独立のread_and_convert()
のコードを削除せず
に残しています。ちなみにstatic
は、関数をそのファイル内部からだけ呼び出せるよ
うにする仕組みだそうです。この結果の差、納得ですね。
実際にいくつかプログラムを書いてgccで-s
オプション付きでコンパイルすると
よくオプティマイズされています。
for (;;) vs. while (1)
for (;;)
とwhile (1)
も最適化すれば同じと思っていたので、同様に
現在(2020, Debian buster)のavr-gcc (GCC) 5.4.0を用いて実状を確認しました。
GCCは賢いですね。全く同じコードが生成されていました。特にfor (;;)
等を
好んで使うことは無いようです。
また、do { } while(expression)
は条件分岐1つで実現できるのでメリット
for (;;)
やwhile (1)
にif
分岐を埋め込むよりかなりコンパクトですね。
decrement and increment
AVRのインストラクションセット を見ると、間接データーアクセスがある場合には以下のコマンドが使えるように 配慮するのは充分考えるべきことですね。
ST
: Store Indirect with Pre-decrementLD
: Load Indirect with Post-increment
また分岐(ブランチ)判断は、2値比較より、ゼロ値比較の方が比較値の設定を しなくて良い分がけ効率的なので、ループカウンターにdecrementを好むのは 納得できます。
当然条件分岐が発生するif
文やwhile
文などの中は、同時にゼロフラグも立つので
Pre-decrementが好ましいのも納得できます。
プログラムメモリー
フラッシュが32KBのATmega328PやATmega32u4や、フラッシュが128KBの ATmega1284P やAT90USB1287などにフォーカスしてプログラムメモリー関係を 調べました。(xmegaチップは本メモのスコープ外です。)
GCCのAVRオプションに関するところに;
Pointers in the implementation are 16 bits wide. The address of a function or
label is represented as word address so that indirect jumps and calls can
target any code address in the range of 64 Ki words.
とあるのですが、フラッシュが128KBに大きくなりバイト単位アドレスが 16ビット長に収まらなくなった際にどういう注意がいるのかが気になるので 調べてみました。
プログラムメモリーには、割り込み処理や初期化関係のコードがメモリー アドレスの頭にあります。その次に長い文字列等の定数データー群が置かれ、 さらに通常の主プログラムのコード群が収められます。そして、プログラム メモリーの最後にブートローダーのコードが収まります。
AVRのコマンド長は偶数(2または4)で、実行プログラムは偶数バイトに アラインされます。だからプログラムメモリー上の実行命令アドレッシング はワード(2 BYTE)単位でよく、4バイト命令である直接ジャンプ(JMP, CALL) 命令もアーギュメントにバイト単位のアドレスを2で割った数値を16ビット長 で収めることで、128KBまでのプログラムメモリー(フラッシュ)すべて を問題なく利用できます。
一方、文字列等の定数データー群へのアクセスは、バイト単位でしたい訳です
が、Cのプログラムメモリー上のポインタータイプのPGM_P
に関して
suzeof(PGM_P)
を確認すると2 BYTEとなっています。実際、
pgm_read_byte(s)
は以下のように2 BYTE命令2つにコンパイルされます。
fc 01 movw r30, r24
84 91 lpm r24, Z
これでは、LPMコマンドによる16ビットのZレジスター(R30, R31)によ り示されるアドレスからの1バイト読み込みなのでプログラムメモリー 上の最初の64KBにしか対応できません。まあ、実用上問題になることは ないですが…定数データーの大きさには注意がいります。
一方、プログラムメモリー(フラッシュ)へのプログラム書き込み・ 更新のSPMコマンドでは 64 BYTE(128 BYTE)等のページ単位の操作なので 128KBでも問題無いようです。これは必要な際にまたデーターシートの 「Boot Loader Support 」あたりを読むことにします。
本題から離れますが、感触として、より高機能な大きなプログラム用途 なら計算能力もある32Bit-ARM系チップを使った安い開発ボード($10以下) を使えば、クロックも72MHz以上で早く、メモリーアクセスも素直で SRAMも大きくフリーのGCCによるサポートもいいし楽な気がします。 Blue PillやBlack Pillという名前の高機能の 中華開発ボード が流通しています。 載っているチップのスペック はたとえば以下です。
- STMicro STM32F103C8T6 ARM Cortex-M3 MCU @ 72 MHz with 64KB flash, 20KB SRAM
- STMicro STM32F401CCU6 ARM Cortex-M4F MCU @ 84 MHz with 256KB flash, 64KB SRAM
- STMicro STM32F411CEU6 ARM Cortex-M4F MCU @ 100 MHz with 512KB flash, 128KB SRAM
2021年12月:とうとう、秋葉で550で売ってるRasberry Pi Picoが出ました。2つ買えば、デバガにもなります。
- Rasberry Pi RP2040 デュアルコア ARM Cortex M0+ MCU 133 MHz (48MHz default) with 2MB flash, 264KB SRAM
あんまり計算しないならこれで充分かも。メモリーは大きいので、MICRO PYTHONがつかえるとか。
参考情報ソース集
- GCC Wiki avr-gcc
- GCC Manual: AVR Options
- AVRのOnline文書一式表 (web)
- Memos on AVR chips
- How to make ISP with TTL-232R-5V
- クレア工房 AVRマイコン
- スタートアップ・割り込みベクター表関係のメモリー節約
- GCCにはメモリーモデルはない
- joshknows: AVR 8-bit Microcontrollers
- C Integer Promotion on 8-bit MCUs
- Embedded Systems/Atmel AVR
- いままでのATmega328Pの練習結果
Previous Post | Top | Next Post |