ATmega328P (3)

Date: 2020/03/06 (initial publish), 2021/07/13 (last update)

Source: jp/note-00017.md

Previous Post Top Next Post

TOC

前回に続きArduino Uno/Nano に使われている基本のシリアルAVRの ATmega328P を中心としてAVRチップのプログラムの勉強・練習の続きとして、 「AVR Libc Reference Manual」を読み込んでAVR独特の世界をみました。

メモリー

PCでのプログラムとはメモリー関係は、少々勝手が違い、IO関係を直接触る上、 メモリー空間も狭いので要注意です。

さらにATmega328Pを含むAVRはプログラムとデータに対してメモリとバスを分離する ハーバード構造を使用し、メモリーアドレス空間もそれぞれ別です。 (PCはプログラムとデータがメモリーアドレス空間を共有するノイマン構造。)

ATmega328Pだと:

実際のデーターメモリー領域は以下です。

IN/OUT命令関連のことがよく分からない。どうもAVRの前の世代のマイコン8051の命令 のことのようだ。アセンブラコード移植を意識しているようだ。INTEL系はIOは0x00から 始まる独立アドレス空間なので、アセンブラコードは0x20オフセットした LDS/STS命令に置き換えると言っているようです。

プログラム領域(フラッシュ)へのアクセスには専用のアセンブラコードLPM/SPMがあります。

EEPROM領域へのアクセスは専用のI/Oレジスタ経由で行うようです。

Cコードからは、マクロが準備されているので、プログラム領域たやEEPROM領域へのアクセス には専用マクロ等を使うようだ。詳しくは「ATmega328Pマニュアル」の「データ用EEPROMメモリ」や、 「AVR Libc Reference Manual」の「Data in Program Space」や、 avr/eeprom.havr/pgmspace.hを参照しましょう。

また、AVRはハーバード構造なので、リンカーに渡すデーターメモリーの開始アドレス は実際のアドレスに0x800000 を手動で加えることも要注意です。

メモリー消費の確認

メモリー消費は、avr-sizeコマンドで調べます。フラッシュ消費は「Program」、 SRAM消費は「Data」で示されます。SRAMの初期化データーをフラッシュが 保持するため、「.data」が両方でカウントされます。

 $ avr-size --mcu=atmega328p --format=avr blink.elf
AVR Memory Usage
----------------
Device: atmega328p

Program:     162 bytes (0.5% Full)
(.text + .data + .bootloader)

Data:          0 bytes (0.0% Full)
(.data + .bss + .noinit)

メモリー消費の節約

通常変数はデーターメモリー領域に置かれますが、場所をとる文字定数や 配列となったデーターベースはプログラムメモリー領域のフラッシュに置き 貴重なSRAMの使用を節約したいころです。

PROGMEMマクロや PSTRマクロや PGM_Pマクロや pgm_read_byte() 関数はそのためにあります。これらは「Program Space Utilities」として avr/pgmspace.hにより提供されています。

puts_P()fprint_P()等というサフックス付き変種関数は、 プログラム領域(フラッシュ)に置いた文字列を出力文字列やフォーマット 文字列として利用しながらputs()fprint()という相当する標準 関数と同等の機能を提供します。これらの変種関数も含めて 標準の「Standard IO facilities」としてstdio.hにより提供されています。

この辺はAVR独特の世界の詳細なので、他と併せて実例で学びましょう。

標準ストリーム

もうひとつのAVR独特の世界は標準ストリームです。

C標準とは違い、avr-libcには適用できるデバイス情報がないため、 標準ストリームstdinstdoutstderrのストリームは アプリケーション起動時に事前に初期化されません。

また、stdio.hは、malloc()を使用するのはメモリー消費の節約の点で 好ましく無いのでこれを避ける標準入出力を作る仕組みを提供するために FDEV_SETUP_STREAM()マクロなどが提供されています。

これらもAVR独特の世界の詳細なので、他と併せて実例で学びましょう。

標準ライブラリの機能の制限・拡張

ATmega328Pはavr5なので、リンクされる標準ライブラリのバイナリは /usr/lib/avr/lib/avr5/にあります。ライブラリが標準ライブラリの libc.aと数学関数ライブラリのlibm.aと分かれているのは普通だし チップ対応のlibatmega328p.aがあるのは分かりますが、 libprintf_flt.alibprintf_min.alibscanf_flt.alibscanf_min.aが気になります。

これらは、printf()scanf()関連機能をコンパイラオプションで 制限・拡張することで、無駄にメモリー消費しないために存在します。 標準では使いそうにない浮動小数点関係のフォーマット機能は削られ ています。まあ普通のAVR使用環境ではデフォルトで充分です。

必要になったら「AVR Libc Reference Manual」の「Standard IO facilities」の vfprintf()vfscanf()の説明のそれぞれの最後の部分を精読しましょう。

AVR独特の世界の例

コンソールターミナルへの出力(stdio.h)

「AVR Libc Reference Manual」のデモプロジェクトは、 /usr/share/doc/avr-libc/examples/にあります。

これを参考に、「LEDタイマー点灯」に加筆して、端子入力に合わせて シリアルに端子の接地状況を出力し、接続したPCのコンソールターミナルへ AVRの状態を出力する簡単なプログラムのhello.cを作成しました。

(シリアル入力なしで、デモコードより簡単なところからスタートです。)

// Arduino nano board is 16 MHz
#define F_CPU 16000000UL
// Usable Baud Rate ... 9600 14400 19200 28800 38400 57600 2%
#define BAUD 19200
// Use with $ picocom -b 19200 /dev/ttyUSB0
#include <avr/io.h>
#include <avr/pgmspace.h>
#include <stdio.h>
#include <util/delay.h>
#include <util/setbaud.h>

static void uart_init(void) {
  UBRR0H = UBRRH_VALUE;
  UBRR0L = UBRRL_VALUE;
#if USE_2X
  UCSR0A |= (1 << U2X0);
#else
  UCSR0A &= ~(1 << U2X0);
#endif
  /* async, non-parity, 1-bit stop, 8-bit data */
  UCSR0C = _BV(UCSZ01) | _BV(UCSZ00);
  /* transmitter enable */
  UCSR0B = _BV(TXEN0);
}

int uart_putchar(char c, FILE *stream) {
  if (c == '\n') {
    uart_putchar('\r', stream);
  }
  loop_until_bit_is_set(UCSR0A, UDRE0);
  UDR0 = c;
  return 0;
}

#define SURE_LO 0
#define UNSURE 1
#define SURE_HI 2
#define BIT5HI 0b00100000

FILE uart_str = FDEV_SETUP_STREAM(uart_putchar, NULL, _FDEV_SETUP_WRITE);

int check(void) {
  // Check if any one of low 5 bits 01234 of PORTB is grounded
  if ((PINB & 0x1f) != 0x1f) {
    _delay_ms(50);  // 50 ms delay
    if ((PINB & 0x1f) != 0x1f) {
      return SURE_LO;
    }
  }
  // Check if all low 5 bits 01234 of PORTB are not grounded
  else if ((PINB & 0x1f) == 0x1f) {
    _delay_ms(50);  // 50 ms delay
    if ((PINB & 0x1f) == 0x1f) {
      return SURE_HI;
    }
  }
  return UNSURE;
}

int main(void) {
  int previous = SURE_HI;  // pull-uped
  uart_init();
  stdout = &uart_str;
  // For Data Direction Register of port B, set 5 pin as output for LED
  DDRB = BIT5HI;
  DDRC = 0;
  DDRD = 0;
  // pull-up for all port B non-5 pins (0b11011111)
  PORTB = ~BIT5HI;
  PORTC = 0xff;
  PORTD = 0xff;
  printf_P(PSTR("Hello world!\n"));
  while (1) {
    if ((previous == SURE_HI) & (check() == SURE_LO)) {
      // LED ON
      PORTB |= BIT5HI;
      printf_P(PSTR("PINB=%02X PINC=%02X PIND=%02X\n"), PINB, PINC, PIND);
      _delay_ms(3000);  // 3000 ms delay
      // LED OFF
      PORTB &= ~BIT5HI;
      previous = SURE_LO;
    } else if ((previous == SURE_LO) & (check() == SURE_HI)) {
      previous = SURE_HI;
    }
  }
}

実はこのような動くコードになるまでは手間がかなりかかりました。それと 言うのも現在のavr-libcは、最近の同一機能レジスターが複数ある他の AVRのサポートを考えてか、そのヘッダーで提供するハードウエアーに対応 するレジスター名やビットの名前に、データーシートに無い「0」という 数字を加えたデーターシートと違う名前を採用しているようです。

そのため、「AVR Libc Reference Manual」にあるデモプロジェクトのコードは、 どうもそのままではATmega328Pでは動かなくなっているようです。ヘッダー ファイルの定義とデーターシートを見比べて、レジスター名を合わせ込んで 対応しました。

さらにデモコードより正確なボーレート計算法をすべく、丸め誤差に配慮する util/setbaud.hを使いました。

さて、これをコンパイルしてできるhello.hexを、USB経由でチップに以下で プログラムしました。

$ sudo avrdude -p m328p -c arduino -P /dev/ttyUSB0 -U flash:w:hello.hex

USBをつないだまま、以下を実行し、B端子や他の端子を接地したりすると、 ターミナルプログラムを起動したホストPCに端子の接地状況が表示されます。

$ picocom -b 19200 /dev/ttyUSB0
 ... (snip) ...
Terminal ready
Hello world!
PINB=3D PINC=3D PIND=FF
PINB=3D PINC=3D PIND=FF
PINB=3D PINC=3B PIND=FF
PINB=3B PINC=3B PIND=FF

メモリー消費を見てみましょう。

 $ avr-size --mcu=atmega328p --format=avr hello.elf
Device: atmega328p

Program:    2038 bytes (6.2% Full)
(.text + .data + .bootloader)

Data:         20 bytes (1.0% Full)
(.data + .bss + .noinit)

なんとstdio.hprintf()関連を使った途端、10倍以上の 2KBもメモリーを消費してしまいました。大きなATmega328Pだと 収まっていますが、printf()の重さは要注意です。

ちなみに、小さなATtiny13Aだと使えるメモリー容量はずっと小さく:

なので、printf()はコード中に絶対に使えません。

ちなみにシリアル入力をscanfで処理するようなコードを足したら 4KB以上メモリー消費してしまいました。

コンソールターミナルへの出力(コンパクト)

そもそも16進表示では見にくいのと、各端子の入出力やプルアップ設定も表示 させたいし、メモリー消費も抑えたいのでprintf()を使わずにプログラムを 書き直しました。

// Arduino nano board is 16 MHz
#define F_CPU 16000000UL
// Usable Baud Rate ... 9600 14400 19200 28800 38400 57600 2%
#define BAUD 19200
// Use with $ picocom -b 19200 /dev/ttyUSB0
#include <avr/io.h>
#include <avr/pgmspace.h>
#include <util/delay.h>
#include <util/setbaud.h>

static void uart_init(void) {
  UBRR0H = UBRRH_VALUE;
  UBRR0L = UBRRL_VALUE;
#if USE_2X
  UCSR0A |= (1 << U2X0);
#else
  UCSR0A &= ~(1 << U2X0);
#endif
  /* async, non-parity, 1-bit stop, 8-bit data */
  UCSR0C = _BV(UCSZ01) | _BV(UCSZ00);
  /* transmitter enable */
  UCSR0B = _BV(TXEN0);
}

int uart_putchar(char c) {
  if (c == '\n') {
    uart_putchar('\r');
  }
  loop_until_bit_is_set(UCSR0A, UDRE0);
  UDR0 = c;
  return 0;
}

int uart_puts_P(PGM_P s) {
  char c;
  while (1) {
    c = pgm_read_byte(s++);
    if (c == '\0') break;
    uart_putchar(c);
  }
  return 0;
}

int uart_put_byte(char c, char x0, char x1) {
  for (int i = 0; i < 8; i++) {
    uart_putchar((c & 0x80) ? x0 : x1);
    c = c << 1;
  }
  return 0;
}

int uart_put_byte_10(char c) { return uart_put_byte(c, '1', '0'); }

int uart_put_byte_OI(char c) { return uart_put_byte(c, 'O', 'I'); }

int uart_put_byte_MK(char c) { return uart_put_byte(c, '*', '_'); }

#define SURE_LO 0
#define UNSURE 1
#define SURE_HI 2
#define BIT5HI 0b00100000

int check(void) {
  // Check if any one of low 5 bits 01234 of PORTB is grounded
  if ((PINB & 0x1f) != 0x1f) {
    _delay_ms(50);  // 50 ms delay
    if ((PINB & 0x1f) != 0x1f) {
      return SURE_LO;
    }
  }
  // Check if all low 5 bits 01234 of PORTB are not grounded
  else if ((PINB & 0x1f) == 0x1f) {
    _delay_ms(50);  // 50 ms delay
    if ((PINB & 0x1f) == 0x1f) {
      return SURE_HI;
    }
  }
  return UNSURE;
}

int main(void) {
  int previous = SURE_HI;  // pull-uped
  uart_init();
  // For Data Direction Register of port B, set 5 pin as output for LED
  DDRB = BIT5HI;
  DDRC = 0;
  DDRD = 0;
  // pull-up for all port B non-5 pins (0b11011111)
  PORTB = ~BIT5HI;
  PORTC = 0xff;
  PORTD = 0xff;
  uart_puts_P(PSTR("Hello world!\n"));
  while (1) {
    if ((previous == SURE_HI) & (check() == SURE_LO)) {
      // LED ON
      PORTB |= BIT5HI;
      uart_puts_P(PSTR("DDRB ="));
      uart_put_byte_OI(DDRB);
      uart_puts_P(PSTR(" DDRC ="));
      uart_put_byte_OI(DDRC);
      uart_puts_P(PSTR(" DDRD ="));
      uart_put_byte_OI(DDRD);
      uart_puts_P(PSTR("\n"));
      uart_puts_P(PSTR("PORTB="));
      uart_put_byte_MK(PORTB);
      uart_puts_P(PSTR(" PORTC="));
      uart_put_byte_MK(PORTC);
      uart_puts_P(PSTR(" PORTD="));
      uart_put_byte_MK(PORTD);
      uart_puts_P(PSTR("\n"));
      uart_puts_P(PSTR("PINB ="));
      uart_put_byte_10(PINB);
      uart_puts_P(PSTR(" PINC ="));
      uart_put_byte_10(PINC);
      uart_puts_P(PSTR(" PIND ="));
      uart_put_byte_10(PIND);
      uart_puts_P(PSTR("\n.....................\n"));
      _delay_ms(3000);  // 3000 ms delay
      // LED OFF
      PORTB &= ~BIT5HI;
      previous = SURE_LO;
    } else if ((previous == SURE_LO) & (check() == SURE_HI)) {
      previous = SURE_HI;
    }
  }
}

USBをつないだまま、以下を実行し、B端子や他の端子を接地したりすると、 ターミナルプログラムを起動したホストPCに端子の接地状況が表示されます。

$ picocom -b 19200 /dev/ttyUSB0
... (snip) ...
Terminal ready
Hello world!
DDRB =IIOIIIII DDRC =IIIIIIII DDRD =IIIIIIII
PORTB=******** PORTC=_******* PORTD=********
PINB =00111011 PINC =00111111 PIND =11111101
.....................
DDRB =IIOIIIII DDRC =IIIIIIII DDRD =IIIIIIII
PORTB=******** PORTC=_******* PORTD=********
PINB =00111101 PINC =00111111 PIND =11111101
.....................
DDRB =IIOIIIII DDRC =IIIIIIII DDRD =IIIIIIII
PORTB=******** PORTC=_******* PORTD=********
PINB =00111110 PINC =00111111 PIND =11111101
.....................
DDRB =IIOIIIII DDRC =IIIIIIII DDRD =IIIIIIII
PORTB=******** PORTC=_******* PORTD=********
PINB =00110111 PINC =00110111 PIND =11111101
.....................

ソースコードが大幅に増え、表示する情報量もprintf()を使う場合の 約3倍となっています。

 $ avr-size --mcu=atmega328p --format=avr hello.elf
AVR Memory Usage
----------------
Device: atmega328p

Program:     754 bytes (2.4% Full)
(.text + .data + .bootloader)

Data:          0 bytes (0.0% Full)
(.data + .bss + .noinit)

でも一方メモリー消費はprintf()を使う場合の3分の1に収まっています。 この目的に特化したコードを書くこのアプローチなら小さなATtiny13Aでも 使えそうです。

後日追記: avrmon

チップ周辺の配線状態確認をホストPCのターミナルからするシェルのような avrmon をこの延長線上に書きま した。全デジタルピンの変更追跡、全デジタルピン間のマトリクススキャン 追跡、全アナログピンの変更追跡、全I/Oレジスターの読み書き機能等まで 入れメッセージ関連の文字列が増えたのもあり、10KB近くまでプログラムが 大きくなりましたが、まだフラッシュ全容量の1/3も使っていません。

winavr由来のMakefileもカスタマイズして、クロックをArduino Uno/Nano に 合わせ、ソースコードの自動整形やconfig.h.inのサポートやターミナル プログラム立ち上げなどの機能追加をしています。

アセンブラー

winavr由来のMakefileを使ってコンパイルしたら、hello.lstといった コンパイル結果のアセンブラーで書かれたダンプリストが作成されています。

細かなことはマニュアルを読むしか無いのですが、avr-gccは、GNU cc/asでは ありますが、アセンブラーはGNU標準のATT式ではなくINTEL式の代入される方が 前にオペランド記述される順であることに注意がいります。

Previous Post Top Next Post