ATmega328P (4)

Date: 2020/03/10 (initial publish), 2021/12/12 (last update)

Source: jp/note-00018.md

Previous Post Top Next Post

TOC

前回に続きArduino Uno/Nano に使われている基本のシリアルAVRの ATmega328P を中心としてAVRチップのプログラムの勉強・練習の続きをしました。

AVRのコーディング上の注意点

以下をAVRのコーディング上の注意点の参考にしました。

わたしの現在の開発環境で使うのは、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

  1. Compile with full size optimization. (use avr-gcc -s option)
  2. Use local variables whenever possible.
  3. Use the smallest explicit applicable data type such as uint8_t with <stdint.h>. Use unsigned if applicable.
  4. If a non-local variable is only referenced within one function, it should be declared static.
  5. If a function is only referenced within one file, it should be declared static.
  6. Specify inline for functions in library performing tasks that generates less than 2-3 lines assembly code.
  7. If possible, avoid calling functions from inside the interrupt routine.
  8. Collect non-local data in structures whenever natural. This increases the possibility of indirect addressing without pointer reload.
  9. Use descending loop counters and branch on zero with pre-decrement if applicable.
  10. Use do { } while(expression) if applicable.
  11. Access I/O memory directly (i.e., do not use pointers). Use _BV(bit), _SFR_IO8(mem_addr), and other MACROs defined in avr/sfr_defs.h.
  12. Code reuse is intra-modular. Collect several functions in one module (i.e., in one file) to increase code reuse factor.
  13. Use pointers with offset or declare structures to access memory mapped I/O.
  14. Store Indirect to SRAM with Pre-decrement and Load Indirect from SRAM with Post-increment.
  15. 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.
  16. Declare main as __attribute__((noreturn)) if not called from anywhere in the program.
  17. Reduce the size of the Interrupt Vector segment (INTVEC) to what is actually needed by the application. Alternatively, concatenate all the CODE segments into one declaration and it will be done automatically.
  18. Optimize C_startup to not initialize unused segments (i.e., IDATA0 or IDATA1 if all variables are tiny or small).

ここで、16、17、18は、普通ここまでしない感があります。 ATtiny13A向けにコンパイル結果をアッセンブリーでハンド チューンして小さくしたい時等のテクニックの感があります。

Hints to Maximize Code Speed

  1. 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

  1. All constants and literals should be placed in Flash by using PROGMEM, PSTR, and PGM_P MACROs.
  2. 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.
  3. If using large functions with variables with a limited lifetime within the function, the use of subscopes can be beneficial.
  4. Get good estimates of the sizes of the software Stack and return Stack (Linker File).
  5. Do not waste space for the IDATA0 and UDATA0 segments unless you are using tiny variables (Linker File).

Checklist for Debugging Programs

  1. Ensure that the CSTACK segment is sufficiently large.
  2. Ensure that the RSTACK segment is sufficiently large.
  3. Ensure that the external memory interface is enabled if it should be enabled and disabled if it should be disabled.
  4. 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とまで書かなくとも、staticinlineのいずれか1つでも 指定すれば146 Byteになります。

この6バイトの差ですが、winavrのMakefileでコンパイルした際に生成される.lss 拡張子の付いたファイルの内容を見れば分かります。

すべてのコードでmain()への関数埋め込みでコードサイズが小さくなるように なっています。GCCは賢いですね。

ただ、staticinlineのいずれもかかれていないと、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のインストラクションセット を見ると、間接データーアクセスがある場合には以下のコマンドが使えるように 配慮するのは充分考えるべきことですね。

また分岐(ブランチ)判断は、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という名前の高機能の 中華開発ボード が流通しています。 載っているチップのスペック はたとえば以下です。

2021年12月:とうとう、秋葉で550で売ってるRasberry Pi Picoが出ました。2つ買えば、デバガにもなります。

あんまり計算しないならこれで充分かも。メモリーは大きいので、MICRO PYTHONがつかえるとか。

参考情報ソース集

Previous Post Top Next Post