迫真の氷結晶

初投稿です。

  • HOME
  • 自己紹介
  • 作品集
  • 生活系

防犯ブザーを改造してサウンドロップを自作する Part2 - Raspberry PiのGPIOでAVRマイコンにプログラムを書き込む

2021-06-20(更新: 2021-06-24)
  • #技術系
  • #電子工作

AVRマイコン ATtiny85 と EEPROM 24FC512 を使ってオリジナルサウンドロップを作る連載のPart2です。

はじめに

Part1 では、Raspberry Pi で EEPROM 24FC512 に音声データを書き込みました。

今回は、AVRマイコン ATtiny85 でLチカ(LEDチカチカ)をするまでの方法をじっくり学びます。

「いいから早く音声を鳴らしたいぜ」という方はごめんなさい。今回はまだそこまで扱いません。

電子工作は入出力のかたまりのようなものなので、AVRの入出力に関する前提知識のない状態でコードだけ見てもさっぱりだと思います。なので今回はまずそこから説明させてくださいませ。

(とは言ったものの、出力について書くだけで精一杯だったので入力については次回に回します…)

また、LチカをやるのでLEDと適当な抵抗器(100Ωから3kΩくらいまでならだいたいなんでもいい)を1つずつ用意しておいてください。

今回の目標

  • Raspberry Pi上でATtiny85用のプログラムバイナリを作り、それをATtiny85に書き込めるようになること。
  • ATtiny85を使った最低限の出力をするプログラムを作成できるようになること。

目次

  • AVRマイコンにプログラムを書き込むための方法
    • ふつうのC言語の実行方法の話
    • AVR向けのC言語のコンパイルについて
    • クロスコンパイルから書き込みまでの流れ
  • クロスコンパイル環境の導入
    • arduino-core パッケージについて
    • arduino-core パッケージをインストールする
  • ATtiny85の入出力を知る
    • ATtiny85のピン
    • ポートについて
    • I/Oレジスタについて
    • I/Oレジスタを操作する方法
    • I/Oレジスタ仕様
    • AVRマイコンのメモリとI/Oアドレス空間
  • STEP1 - C言語でLチカのプログラムを書く
    • avr/io.h
    • util/delay.h
    • I/Oピンの初期化
    • Lチカ部分
  • STEP2 - ソースファイルのコンパイルとHEXファイルへの変換
    • コンパイル
    • ELFファイルをHEXファイルに変換
  • STEP3 - HEXファイルをATtiny85に書き込む
    • ATtiny85のプログラミング(書き込み)に使う信号線とピン
    • avrdudeのコンフィグファイルの下準備
    • 配線作業
    • 接続テスト
    • PB4に抵抗とLEDを配線
    • プログラムを書き込んで実行する
  • まとめ

AVRマイコンにプログラムを書き込むための方法

ふつうのC言語の実行方法の話

まず、ふつうのC言語プログラムの実行方法の話をします。

C言語のプログラムを実行するには、C言語のソースコードが書かれたファイルを用意し、それをコンパイラに与えて最終的な実行ファイルを生成する必要があります。これをコンパイルとかビルドと呼びます。

正確には、オブジェクトファイルを作るところまでをコンパイルと呼んで、最終的な実行可能形式にすることをビルドと呼びますが、今回はビルドのことをコンパイルと呼ばせてください。

コンパイル(ビルド)は、一般的なC言語環境だと gcc というコンパイラ環境を使って行います。

ただしこれは、そのマシン上で動かすソースコードをそのマシン上でコンパイルする時の話です。

AVR向けのC言語のコンパイルについて

今回はAVRマイコンで動くプログラムをRaspberry Pi上でコンパイルしたいです。

しかし、普通のgcc環境ではAVR用の実行ファイルを生成することができません。なぜなら、Raspberry Piに入っているCPUとAVRのCPUが異なっているからです。

このため、AVRに合わせた専用環境を構築する必要があります。このような専用の開発環境をクロス開発環境と呼びます。

今回はRaspberry Pi上にAVR向けのクロス開発環境を導入する必要があります。

この環境の構築方法については後で説明します。

クロスコンパイルから書き込みまでの流れ

クロス開発環境を整えたあとの話をします。

C言語をクロスコンパイルして、実行ファイルをATtiny85に書き込むまでの流れを次の図に示します。

C言語がコンパイルされてATtinyX5に書き込まれるまでの手順
C言語ソースコードをavr-gccに渡し実行ファイルを生成し、その実行ファイルをavr-objcopyに渡しhexファイルを生成し、そのhexファイルをavrdudeに渡してGPIO経由でATtinyX5にプログラムを書き込む。

コンパイルから書き込みまでに行う手順は次の3つです。

  1. avr-gcc というコマンドを使って、C言語ソースコード(xxx.c)をコンパイルして実行可能ファイル(xxx.elf)に変換する。
  2. avr-objcopy というコマンドを使って、実行可能ファイル(xxx.elf)をhexファイル(xxx.hex)に変換する。
  3. avrdude というコマンドを使って、hexファイル(xxx.hex)の機械語命令列をAVRマイコンに書き込む。
  • avr-gcc
    • AVR用のクロスコンパイラです。ソースコードをコンパイルし、さらにそれをAVR向けの実行可能ファイル(xxx.elf)に変換します。
  • avr-objcopy
    • avr-objcopy ファイル形式を変換するユーティリティです。これを使ってelfファイルををIntel HEX形式(xxx.hex)に変換します。
  • avrdude
    • AVRマイコンに対しデータの読み書きを行うコマンドです。hexファイルを渡すことでAVRマイコンにプログラムを書き込めます。ちなみに、Arduino IDEでスケッチを書き込むときにはこのコマンドが呼び出されています。

まとめ: AVRマイコンにプログラムを書き込むための方法

  • クロス開発環境を導入する必要がある。
  • 導入したコマンドを駆使して、ELF形式に変換→HEX形式に変換→書き込み の順番でプログラムをマイコンに書き込む。

クロスコンパイル環境の導入

前のセクションで紹介したツール一式をインストールしてクロスコンパイル環境を整えます。

といっても、やることは arduino-core パッケージをインストールすることだけです。

arduino-core パッケージについて

arduino-core パッケージをインストールすると、AVRマイコンにプログラムを書き込むための各種ツールをまとめてダウンロードすることができて便利です。

arduino-core パッケージは、AVR開発に必要な複数のパッケージをまとめたパッケージです。その中身は以下のようになっています。

  • arduino-core パッケージ:
    • avr-libc パッケージ
    • avrdude パッケージ
      • avrdude コマンド
    • binutils-avr パッケージ
      • avr-objcopy コマンド など
    • gcc-avr パッケージ
      • avr-gcc コマンドや、 avr-cpp, cc1, avr-as コマンドなどコンパイラ・ツールチェイン

雰囲気的には、コマンドやライブラリが /usr/bin//usr/lib/gcc/avr/5.4.0/ 内にインストールされる感じです(詳しいことは知らない)。

arduino-core パッケージをインストールする

次のコマンドを実行して arduino-core パッケージをインストールします。

pi@raspberrypi:~ $ sudo apt install arduino-core
The following additional packages will be installed:
  avr-libc avrdude binutils-avr gcc-avr
Suggested packages:
  arduino-mk dfu-programmer avrdude-doc gcc-doc
The following NEW packages will be installed:
  arduino-core avr-libc avrdude binutils-avr gcc-avr
0 upgraded, 5 newly installed, 0 to remove and 0 not upgraded.
Need to get 19.2 MB of archives.
After this operation, 126 MB of additional disk space will be used.
Do you want to continue? [Y/n] Y
...(省略)...

まとめ:クロスコンパイル環境の導入

apt install arduino-core というコマンドを実行してクロスコンパイル環境を構築した。

ATtiny85の入出力を知る

環境が整ったのでいよいよプログラミング、と言いたいところですが、残念ながらマイコンの入出力周りの勝手が分かっていないとマイコンプログラミングはできません。Hello, World! なら画面に出てきて「やったぜ。」となりますが、マイコンの場合は「まずどこに出力されるねん」という問題があるからです。

というわけで先にATtiny85の入出力とその制御方法について説明します。

先に結論

  • ATtiny85にはPB5からPB0までのピンがある。
  • ポートBを操作するとピンの電圧や入力受け付け状態をコントロールできる。
    • ポートを操作するにはI/Oレジスタを操作する。
      • I/Oレジスタを操作するにはI/Oレジスタがマッピングされたメモリに対し読み書きを行う。

ATtiny85のピン

ATtiny85に生えている8本の足、ピン(端子)について見ていきます。

https://avr.jp/user/ds.htm

このページから tiny45.pdf を開いて、ATtiny85のデータシート(日本語版)を見てください。( /user/DS/PDF/tiny45.pdf というパスに存在します。)

なぜ直リンクしないかというと、avr.jp に書いてある注意書きがなにやら香ばしくて、それを犯すと面倒な事になりそうだからです…。管理者は一人でAVRの技術情報を翻訳しまくっている熱心な方のようで、過去に翻訳物を巡って何やらやらトラブルがあったようですね。こわいな〜とづまりすとこ…

1. ピン配置 という章にATtiny85(25/45と共通)のピン(端子)の図と説明があります。

ATtiny25/45/85のピン配置
ATtiny25/45/85のピン配置(画像は秋月電子通商のサイトに置かれているデータシートから)
  • VCC: 電源ピン(5.5Vまで)
  • GND: 接地ピン(0V)
  • PB5, PB4, PB3, PB2, PB1, PB0: ポートBを構成するピン

ポートという言葉が出てきました。

ポートについて

ポートとは

まずポートとは何かというと、すみません、私の理解がゆるふわなので正確に説明することはできません。

私は、ポートとは「入出力機能」そのもののことであると理解しています。ポートBという入出力機能を、ある特定のピンが担っている、というふうに考えています。物というより概念だと思います。

入出力ポートをコントロールするという概念

ATtiny85は、PB5ピンからPB0ピンにポートBとしての機能が割り当てられています。

ポートBは6ビットの、内蔵プルアップ抵抗付きの双方向入出力ポートである、とマニュアルは言っています。

分かりづらいのでもう少し噛み砕いて説明します。

「ポートBは6ビット」なので、まずポートBというのは6ビットの値で表せます。

PORTB : 000000

そしてこの各ビットがそれぞれPB5, PB4, PB3, PB2, PB1, PB0に対応しています。

        PB5  PB4  PB3  PB2  PB1  PB0
PORTB :   0    0    0    0    0    0

また「双方向入出力ポート」なので、ポートBには入出力の方向というのが存在します。

ここではPB5, PB4, PB3の向きを「入力」、PB2, BP1 ,PB0の向きを「出力」に設定したとします。

        PB5  PB4  PB3  PB2  PB1  PB0
DDRB  :  IN   IN   IN  OUT  OUT  OUT
PORTB :   0    0    0    0    0    0

ここで、ポートBの値を 000101 に設定します。

        PB5  PB4  PB3  PB2  PB1  PB0
DDRB  :  IN   IN   IN  OUT  OUT  OUT
PORTB :   0    0    0    1    0    1

すると、PB2, PB0ピンにHIGHの電圧が、PB1ピンにLOWの電圧が印加されます。(正論理の場合)

このように入出力は、ピン1本1本を操作するのではなく、ポートというひとかたまりの単位で行います。

ここでは出力の例のみ示しました。とりあえずまだ入力の例は取り扱わないことにします。

ちなみに、ATtiny85にはポートBだけしかありませんが、Arduinoにも載っているATmega328PはポートB, ポートC, ポートDを持っています。 `/usr/lib/avr/include/avr/portpins.h` によると、ポートはAからLまで定義されているようです。ポートLを持つマイコンが実在するのかは分かりません。

I/Oレジスタについて

ポートをコントロールすると言いましたが、 ポートをコントロールするには「I/Oレジスタ」に対してアクセスを行います。I/Oレジスタを操作することで入出力ポートをコントロールできます。

ピン、ポートと来て今度はレジスタです。

レジスタとは

レジスタ(register)はコンピュータのプロセッサなどが内蔵する記憶回路で、制御装置や演算装置や実行ユニットに直結した、操作に要する速度が最速の、比較的少量のものを指す。 (Wikipedia レジスタ(コンピュータ)より引用)

レジスタとはマイコン内部の記憶回路です。プロセッサー自体の内部状態を保存しています。

ATtiny85のレジスタは1個あたり8ビットの幅を持っていて、CPUから見ると各レジスタは1バイトのメモリ領域に見えます。

レジスタの中でも、ペリフェラル(周辺モジュール)の制御用に用意されたレジスタのことをI/Oレジスタと言います。

このI/Oレジスタに対し読み書きを行うと入出力ポートの動作を制御できます

たとえば、ポートBの出力機能を担うレジスタの値を書き換えると、ポートBのピンの入出力方向を切り替えたり電圧を上げ下げしたりできます。また、入力用レジスタにはピンへの入力が逐次格納され、好きなときにその値を読むことができます。(AVRの場合は出力用のレジスタと入力用のレジスタが分かれている)

I/Oレジスタを操作する方法

I/Oレジスタを読み書きするには、I/Oレジスタがマッピングされた(対応付けられた)メモリアドレスに読み書きを行います。

先にC言語の例を見せると、たとえばこのようなコードでPORTB, PINB というI/Oレジスタを操作できます。

PORTB = 0x10; // PORTBレジスタに 0b00010000 を書き込み
unsigned char r = PINB; // PINBレジスタの値を読み込んで変数rに保存

ここには今言った「メモリアドレス」の姿はありませんが、実はこの PORTBPINB は、コンパイル中に、特定のメモリアドレスを指すポインタに展開されます。

PORTB は最終的に (*(volatile uint8_t *)((0x18) + 0x20)) に、PINB は最終的に (*(volatile uint8_t *)((0x16) + 0x20)) に展開されます。

ここに出てくる 0x18, 0x16 はそれぞれ PORTB, PINB に割り当てられたI/Oアドレスという値です。

I/Oレジスタ仕様

0x18 とか 0x16というアドレスが PORTB とか PINB というレジスタに対応しているといった情報は、マニュアルの 10.4. I/Oポート用レジスタ に載っています。

ポートBには PORTB, DDRB, PINB というレジスタがあります。これに0/1を書き込むことでポートBを制御できます。

ポートBに対応するレジスタとそのI/O空間上での場所
ポートBに対応するレジスタとそのI/O空間上での場所
  • PORTB は I/O空間上で 0x18 というアドレスに対応するレジスタです。
  • DDRB は I/O空間上で 0x17 というアドレスに対応するレジスタです。
  • PINB は I/O空間上で 0x16 というアドレスに対応するレジスタです。

また、縦方向に注目して図を見ると、レジスタの各ビットは各ピンに対応しています:

  • 0番目のビットの操作はピン PB0 に作用する。
  • 1番目のビットの操作はピン PB1 に作用する。
  • ...
  • 5番目のビットの操作はピン PB5 に作用する。

DDRB - Port B Data Direction Register

  • 各ピンについて、入力・出力を切り替えるレジスタ。
  • 1: 出力, 0: 入力
  • I/O空間上でのアドレス: 0x17

PORTB - Port B Data Register

  • 各ピンについて、HIGH/LOWの出力、またはプルアップ抵抗のオン・オフを設定するレジスタ。
  • ピンが出力モードの場合
    • 1: HIGH出力, 0: LOW出力 (PINB レジスタでこの出力を反転させることができるので注意)
  • ピンが入力モードの場合
    • 1: プルアップ抵抗あり, 0: プルアップ抵抗なし
  • I/O空間上でのアドレス: 0x18

PINB - Port B Input Pins Address

  • ピンが入力モードのときに入力値が格納されるレジスタ。
  • ふつうは読み込んで使うレジスタだが、ここに書き込むこともできる。
    • PINB1を書き込んだポートピンは出力が反転し、1のときLOW出力, 0のときHIGH出力になる(ローアクティブ)
  • I/O空間上でのアドレス: 0x16
  • (出力モードのときに読み込むとどういう値になるんだっけ…?)

AVRマイコンのメモリとI/Oアドレス空間

I/Oアドレス空間という話が出てきたので、AVRのメモリについてお話します。

AVRマイコンは3つの独立したメモリを持っています。

  • プログラム用フラッシュメモリ(ROM)
    • プログラム本体(機械語命令列)を保存する領域。ATtiny85では4K×16bit(4Kワード/4K語)の大きさを持つ。
  • データメモリ(データ用SRAM)
    • プログラム実行時にデータを操作する作業領域。
    • メモリ空間の先頭に32本の汎用レジスタと64本のI/Oレジスタがマッピングされている。
    • ATtiny85では512×8bit(512バイト)の大きさを持つ。
  • EEPROM
    • 不揮発性メモリ。電源を切っても内容が消えないので、設定情報を記録しておく用途などに使用可能。100,000回書き換え可能。
    • ATtiny85では512バイトの大きさを持つ。
avr-memory
AVRマイコンにはデータメモリとプログラム用フラッシュメモリとEEPROMがある。データメモリには汎用レジスタとI/Oレジスタがマッピングされている。

I/Oレジスタはデータメモリ空間上にマッピングされています。

データメモリ上での 0x0020 - 0x005F を、I/Oレジスタの世界から見て、 0x00 - 0x3F と見なしたのがI/Oアドレス空間というわけです。

まとめ: ATtiny85の入出力を知る

  • ATtiny85にはPB5からPB0までのピンがある。
  • ポートBを操作するとピンの電圧や入力受け付け状態をコントロールできる。
    • ポートを操作するにはI/Oレジスタを操作する。
      • I/Oレジスタを操作するにはI/Oレジスタがマッピングされたメモリに対し読み書きを行う。

おまけ:ポートピンはそれぞれ異なる様々な機能を持っている

ポートピンはそれぞれにいろいろな「交換機能」を持っています。

たとえばPB2を見てみると (SCK/USCK/SCL/ADC1/T0/INT0/PCINT2) と書かれています。これは、PB2はプログラム書き込み時のクロック入力端子を担ったり、アナログ・デジタル変換の入力ピンとして使ったりできることを表しています。PB2はハードウェアとしてそのような機能をサポートしているということです。もちろん、それらの機能を制御するためのレジスタがたくさんあります。詳しい入出力ポートの説明は10章 10. 入出力ポート に書かれています。

リファレンスマニュアルによると、PB2の機能は、SCK(SPIシリアルプログラミング用クロック入力)、USCK(3線動作USIのクロック入出力)、SCL(2線動作USI(TWI)のクロック入出力)、ADC1(A/D変換器チャネル1入力)、T0(タイマ/カウンタ0外部クロック入力)、INT0(外部割り込み0入力)、PCINT2(ピン変化割り込み元2入力)だそうです。まだ使ったことのない機能ばかりです。

STEP1 - C言語でLチカのプログラムを書く

ポートやレジスタについて分かったので、C言語のソースファイルを書き始めることができます。 記述量は僅かなので先に完成形を示します。

/* 
 * main.c
 * blink an LED
 * /
#include <avr/io.h>
#include <util/delay.h>

int main(void)
{
  DDRB  = 0b00010000; // PB4を出力, それ以外を入力に設定。
  PORTB = 0b00000000; // すべてのピンをリセット。PB4をLOW出力に、それ以外をプルアップ抵抗なしの入力に設定。

  /* Toggle ON/OFF every 500ms */
  while (1) {
    PORTB = 0b00010000; // PB4をHIGH出力に設定。
    _delay_ms(500); // 500ミリ秒待つ
    PORTB = 0b00000000; // PB4をLOW出力に、それ以外はプルアップ抵抗なしの入力に設定。
    _delay_ms(500); // 500ミリ秒待つ
  }
  return 0;
}

1行ずつ解説していきます。

まず先頭の おまじない インクルードヘッダから見ていきましょう。

avr/io.h

#include <avr/io.h>

これは /usr/lib/avr/include の中の avr/io.h をインクルードしています。

avr/io.h をインクルードすることでAVRデバイス特有のI/O定義をC言語から使えるようになります。

具体的には、C言語で PORTB = 0x10, DDRB = 0x10, r = PINB のような記述ができるようになります。なぜこのような書き方ができるようになるのかは後のおまけコーナーで解説します。

util/delay.h

#include <util/delay.h>

これは /usr/lib/avr/include の中の util/delay.h をインクルードしています。

指定ミリ秒だけループして待つ(インライン)関数 _delay_ms() と、そのマイクロ秒版 _delay_us() が定義されています。

注意:予めマクロ F_CPU を定義しておくこと

このヘッダファイルは内部で F_CPU というマクロを参照して待機時間の計算を行っているので、インクルードする箇所の前で予め #define F_CPU (1000000UL) と書くなどして F_CPU マクロを定義しておく必要があります。 F_CPU の値はCPUのクロック周波数です(1000000 = 1MHz)。 F_CPU の定義をどこにも書かなかった場合は警告が出て、F_CPU の値として 1000000UL が勝手に採用されます。

I/Oピンの初期化

int main(void)
{
  DDRB  = 0b00010000; // PB4を出力, それ以外を入力に設定。
  PORTB = 0b00000000; // すべてのピンをリセット。PB4をLOW出力に、それ以外をプルアップ抵抗なしの入力に設定。

今回は PB4 のピンに0/1を出力したいので、まず PB4 ピンを出力モードに設定します。

I/Oレジスタ仕様のセクションで述べたように、入出力の方向は DDRB レジスタで設定します。 PB4 のビットを立てた値を考えると、 0b00010000=(0x10) が得られます。

(bit)   --- --- PB5 PB4 PB3 PB2 PB1 PB0
DDRB  =   0   0   0   1   0   0   0   0  (=0x10)
PORTB =   0   0   0   0   0   0   0   0  (=0x00)

PORTB の初期値は、マニュアルを見ると 0x00 と決まっているのであんまり0で埋めておく理由はないです。

Lチカ部分

  /* Toggle ON/OFF every 500ms */
  while (1) {
    PORTB = 0b00010000; // PB4をHIGH出力に設定。
    _delay_ms(500); // 500ミリ秒待つ
    PORTB = 0b00000000; // PB4をLOW出力に、それ以外はプルアップ抵抗なしの入力に設定。
    _delay_ms(500); // 500ミリ秒待つ
  }
  return 0;
}

PB4をHIGH出力に設定し、500ミリ秒待ち、PB4をLOW出力にして、また500ミリ秒待つ、というのを永遠に繰り返します。

注意するべき点としては、 _delay_ms()_delay_us() の引数は、定数(コンパイル時に具体的な数値リテラルに変換され得る値)でなければならないということです。変数を引数に取ることはできません。

まとめ: STEP1 - C言語でLチカのプログラムを書く

  • #include <avr/io.h> のおかげで PORTB = 0x10, DDRB = 0x10, r = PINB のような記述ができる。
  • #include <util/delay.h> のおかげで _delay_ms() インライン関数が使える。
    • 引数は定数でなければならない。
  • ポートに対応する位置のビットを立てた値をレジスタに書き込んでポートを操作する。
    • PB4の出力をHIGHにする例: PORTB = 0b00010000;

おまけ1: I/Oインクルードの細かい話

デバイス特有のI/O定義が読み込まれる仕組み

avr/io.h の中では、マイコンの種類に応じて条件分岐が起こり、そのデバイス特有のI/O定義ヘッダが自動でインクルードされます。 たとえばマイコンの種類を指定するオプション -mmcu=attiny85 をコンパイラに与えた場合、その内部で __AVR_ATtiny85__ というマクロが定義され、avr/io.h 内の条件分岐

#elif defined (__AVR_ATtiny85__)
#  include<avr/iotn85.h>
#elif ...

にマッチし、その結果ATtiny85特有のI/O定義ヘッダ avr/iotn85.h が読み込まれます。(ちなみに avr/iotn85.h を直接インクルードするのはNGと avr/io.h 内に書いてあります。)

PORTB, DDRB, PINBの実体を探る

PORTB, DDRB, PINB は変数に見えますが、変数ではなく、レジスタのアドレスを指すポインタです。

その実体を探ってみましょう。

まずは次のような定義を avr/iotnx5.h の中に見つけることができます。

...
#define PINB    _SFR_IO8(0x16)
#define PINB5   5
#define PINB4   4
#define PINB3   3
#define PINB2   2
#define PINB1   1
#define PINB0   0

#define DDRB    _SFR_IO8(0x17)
#define DDB5    5
#define DDB4    4
#define DDB3    3
#define DDB2    2
#define DDB1    1
#define DDB0    0

#define PORTB   _SFR_IO8(0x18)
#define PB5     5
#define PB4     4
#define PB3     3
#define PB2     2
#define PB1     1
#define PB0     0
...

_SFR_IO8() とは何か?これを探ると今度は avr/sfr/defs.h

#define _SFR_IO8(io_addr) _MMIO_BYTE((io_addr) + __SFR_OFFSET)

と書かれています。レジスタの正体まであともうちょっとです。

同じファイルにこのような定義も書かれています:

#define __SFR_OFFSET 0x20
#define _MMIO_BYTE(mem_addr) (*(volatile uint8_t *)(mem_addr))

これらの定義より、 _SFR_IO8(io_addr)(*(volatile uint8_t *)((io_addr) + 0x20)) というポインタに展開されます。

つまり、

  • PORTB は最終的には (*(volatile uint8_t *)((0x18) + 0x20)) に展開されます。
  • DDRB は最終的には (*(volatile uint8_t *)((0x17) + 0x20)) に展開されます。
  • PINB は最終的には (*(volatile uint8_t *)((0x16) + 0x20)) に展開されます。

PORTB = 0x10 という変数への代入みたいな記述は、 (*(volatile uint8_t *)((0x18) + 0x20)) = 0x10 と書くのと等価であるというわけです。

この記述こそ、データメモリアドレス 0x38 (つまりレジスタPORTB) に直接アクセスする記述です。

おまけ2: レジスタ書き込みのスマートな記述方法

avr/iotnx5.h#define PB4 4 と定義されているおかげで、

DDRB  = 0b00010000;

は、ビットシフトを使ってこんな書き方をすることもできます:

DDRB = (1 << PB4);

複数のビットを立てた値を設定するときは、ビット演算を使って、こうです:

DDRB = (1 << PB4) | (1 << PB2) | (1 << PB0);

ちなみに avr/sfr_defs.h

#define _BV(bit) (1 << (bit))

という記述があるので、もっと短くしてこんな書き方もできます:

DDRB = _BV(PB4) | _BV(PB2) | _BV(PB0);

特定のビットだけ立ててその他のビットはそのままの値にしておきたい場合は、このように書きます:

DDRB = DDRB | _BV(PB4) | _BV(PB2) | _BV(PB0);
または
DDRB |= _BV(PB4) | _BV(PB2) | _BV(PB0);

といっても、本当に「そのままの値」にしておけるわけではなく、最初にレジスタの値を読み込んでから、変更の必要のないビットについては同じ値で書き戻しています。

特定のビットだけ落としてその他のビットはそのままの値にしておきたい場合は、このように書きます:

DDRB = DDRB & ~( _BV(PB4) | _BV(PB2) | _BV(PB0) );
または
DDRB &= ~( _BV(PB4) | _BV(PB2) | _BV(PB0) );

STEP2 - ソースファイルのコンパイルとHEXファイルへの変換

C言語のプログラムを、AVRマイコンが実行可能な形式のファイルに変換する作業をします。

コンパイル

avr-gcc コマンドを使って main.c をビルドして、ファイル main.elf を生成します:

pi@raspberrypi:~ $ avr-gcc main.c -o main.elf -DF_CPU=1000000 -mmcu=attiny85 -Os -Wall
  • -o オプション
    • 出力先のファイル名を指定します。
  • -D オプション
    • -DXXX=YYY と書くと、Cソースファイルで #define XXX YYY と書いたのと同じことになります。
    • (-D とマクロ名との間にスペースを入れないのが古くからの習わしです(最近はスペースを入れても構わないらしい))
  • -mmcu オプション
    • 実行ターゲットとなるMCU(マイコン)の種類を指定します。
    • 指定できる値は AVR Options に一覧になっています。
  • -Os オプション
    • サイズ最適化を有効にします。
    • -O0: 最適化なし, -O1/-O2/-O3: ちょっと最適化/けっこう最適化/めっちゃ最適化(適当)
    • 最適化をなしにすると util/delay.h に怒られます。
  • -Wall オプション
    • すべての警告(Warning)を表示します。

main.elf ファイルができました。

ELFファイルをHEXファイルに変換

avr-objcopy コマンドを使って main.elf を ファイル main.hex に変換します:

pi@raspberrypi:~ $ avr-objcopy -I elf32-avr -O ihex main.elf main.hex
  • -I オプション
    • 入力ファイルのフォーマットを指定します。
    • 今回のフォーマットはelf32-avrという形式らしいです。
  • -O オプション
    • 出力ファイルのフォーマットを指定します。
    • 今回はIntel HEX形式にしたいのでihexを指定します。

main.hex ファイルができました。

STEP3 - HEXファイルをATtiny85に書き込む

avrdude を使ってhexファイルをATtiny85に書き込むとついにプログラムが動き出します。

ATtiny85のプログラミング(書き込み)に使う信号線とピン

まずはATtiny85へのプログラムの書き込みに使う4つのピンとその役割を紹介します。

この4つのピンを適切にRaspberry PiのGPIOに接続することでATtiny85にプログラムを書き込むことができます。

ピン名入出力信号名役割
PB0 入力 MOSI(Master Out Slave In) シリアル(直列)データ入力
PB1 出力 MISO(Master In Slave Out) シリアルデータ出力
PB2 入力 SCK(Serial CLock) シリアルクロック入力
PB5 入力 RESET リセット入力(ローアクティブ)。RESETがGNDに引かれている間にプログラムを書き込める。
avr-memory
RESETピンがGNDに引かれている間ATtiny85にはSPIバスを使ってプログラムを書き込める。

avrdudeのコンフィグファイルの準備

今回Raspberry PiのGPIOピンを使って書き込みを行おうとしているので、どのGPIOピンを使って書き込みを行うのかavrdudeに伝える必要があります。

そのためにまず /etc/avrdude.conf を作業ディレクトリにコピーします。ファイル名は my-avrdude.conf にしてみました。

pi@raspberrypi:~ $ cp /etc/avrdude.conf my-avrdude.conf

そして、 my-avrdude.conf の末尾に以下の設定を追記します。

# Linux GPIO configuration for avrdude.
programmer
  id    = "pi_1";
  desc  = "Use the Linux sysfs interface to bitbang GPIO lines";
  type  = "linuxgpio";
  reset = 21;
  sck   = 25;
  mosi  = 19;
  miso  = 16;
;

これは、信号とGPIOピンとのペアを記述したものを pi_1 という名前で登録しています。

今回はGPIOピンを以下のように割り当てました。ピンの番号は自由ですが、どのGPIOピンが何番のGPIOなのかを https://pinout.xyz を使ってよく調べてください。

  • RESET: GPIO 21
  • SCK: GPIO 25
  • MOSI: GPIO 19
  • MISO: GPIO 16

配線作業

以上のようにコンフィグファイルを設定したので、ATtiny85のピンとRaspberry PiのGPIOピンを次の対応で接続します。

  • PB0 - GPIO 19 (MOSI)
  • PB1 - GPIO 16 (MISO)
  • PB2 - GPIO 25 (SCK)
  • PB5 - GPIO 21 (RESET)
ATtiny85とRaspberry Pi GPIOの配線図

GPIOピンの割り当てを変えた場合は適宜接続の対応を変えてください。

イラストとはちょっと違いますが、実際の配線の写真がこちらです。

ATtiny85とRaspberry Pi GPIOの配線の写真
実際の配線の写真

すみません、GPIO拡張アダプタとかいうちょっとズルいツールを使っています。

接続テスト

Raspberry PiからATtiny85が見えるかどうか接続テストをするために次のコマンドを実行します。

pi@raspberrypi:~ $ sudo avrdude -p t85 -C my-avrdude.conf -c pi_1 -v
  • -p t85: AVRデバイスを指定するオプションです。ATtiny85の場合はt85となります。何が指定できるかは $ avrdude -p ? で確認できます。
  • -C my-avrdude.conf: コンフィグファイルの場所を指定するオプションです。デフォルトでは /etc/avrdude.conf が読み込まれます。
  • -c pi_1: プログラマー(書き込み器)の種類を指定します。今回は pi_1 という名前でGPIO書き込みを設定しているのでこの名前を指定します。
  • -v: verbose。出力を詳細にするオプションです。
avrdude: Version 6.3-20171130
         Copyright (c) 2000-2005 Brian Dean, http://www.bdmicro.com/
         Copyright (c) 2007-2014 Joerg Wunsch

         System wide configuration file is "my-avrdude.conf"
         User configuration file is "/root/.avrduderc"
         User configuration file does not exist or is not a regular file, skipping

         Using Port                    : unknown
         Using Programmer              : pi_1
         AVR Part                      : ATtiny85
         Chip Erase delay              : 4500 us
         PAGEL                         : P00
         BS2                           : P00
         RESET disposition             : possible i/o
         RETRY pulse                   : SCK
         serial program mode           : yes
         parallel program mode         : yes
         Timeout                       : 200
         StabDelay                     : 100
         CmdexeDelay                   : 25
         SyncLoops                     : 32
         ByteDelay                     : 0
         PollIndex                     : 3
         PollValue                     : 0x53
         Memory Detail                 :

                                  Block Poll               Page                       Polled
           Memory Type Mode Delay Size  Indx Paged  Size   Size #Pages MinW  MaxW   ReadBack
           ----------- ---- ----- ----- ---- ------ ------ ---- ------ ----- ----- ---------
           eeprom        65     6     4    0 no        512    4      0  4000  4500 0xff 0xff
           flash         65     6    32    0 yes      8192   64    128  4500  4500 0xff 0xff
           signature      0     0     0    0 no          3    0      0     0     0 0x00 0x00
           lock           0     0     0    0 no          1    0      0  9000  9000 0x00 0x00
           lfuse          0     0     0    0 no          1    0      0  9000  9000 0x00 0x00
           hfuse          0     0     0    0 no          1    0      0  9000  9000 0x00 0x00
           efuse          0     0     0    0 no          1    0      0  9000  9000 0x00 0x00
           calibration    0     0     0    0 no          1    0      0     0     0 0x00 0x00

         Programmer Type : linuxgpio
         Description     : Use the Linux sysfs interface to bitbang GPIO lines
         Pin assignment  : /sys/class/gpio/gpio{n}
           RESET   =  21
           SCK     =  25
           MOSI    =  19
           MISO    =  16

avrdude: AVR device initialized and ready to accept instructions

Reading | ################################################## | 100% 0.00s

avrdude: Device signature = 0x1e9108 (probably t25)
avrdude: Expected signature for ATtiny85 is 1E 93 0B
         Double check chip, or use -F to override this check.

avrdude done.  Thank you.

うまく接続できているとこのように Device signature が表示されます。(今回実はATtiny85ではなくATtiny25を接続したので、t25のシグネチャが表示されている)

うまくいかなかった場合は avrdude: AVR device not responding と表示されます。配線を間違えていないか確認してください。それと、そもそもそのGPIOピンが壊れていないか確認してください(1敗)

PB4に抵抗とLEDを配線

接続確認ができたらPB4に抵抗とLEDをつないでください。

抵抗とLEDの配線イラスト
配線のイラスト
抵抗とLEDの配線
実際の配線の写真(イラストとはちょっと違う)

こんな感じで適当に220Ω抵抗と赤色LEDを繋ぎます。LEDのプラスマイナスの向きには注意してください(足が長いほうがプラス)。向きを間違えると光りません。

プログラムを書き込んで実行する

書き込みコマンドを実行します。

pi@raspberrypi:~ $ sudo avrdude -p t85 -C my-avrdude.conf -c pi_1 -v -U flash:w:main.hex:i
  • -U flash:w:main.hex:i: flash メモリに w(書き込み) を行う ← main.hex を ← intel hex 形式の
    • -U メモリの種類:操作の種類:ファイル名:ファイル形式 のフォーマットでメモリ操作を実行する
〜(省略)〜
avrdude: AVR device initialized and ready to accept instructions

Reading | ################################################## | 100% 0.00s

avrdude: Device signature = 0x1e9108 (probably t25)
avrdude: safemode: lfuse reads as 62
avrdude: safemode: hfuse reads as DF
avrdude: safemode: efuse reads as FF
avrdude: NOTE: "flash" memory has been specified, an erase cycle will be performed
         To disable this feature, specify the -D option.
avrdude: erasing chip
avrdude: reading input file "main.hex"
avrdude: writing flash (96 bytes):

Writing | ################################################## | 100% 0.07s

avrdude: 96 bytes of flash written
avrdude: verifying flash memory against main.hex:
avrdude: load data flash data from input file main.hex:
avrdude: input file main.hex contains 96 bytes
avrdude: reading on-chip flash data:

Reading | ################################################## | 100% 0.05s

avrdude: verifying ...
avrdude: 96 bytes of flash verified

avrdude: safemode: lfuse reads as 62
avrdude: safemode: hfuse reads as DF
avrdude: safemode: efuse reads as FF
avrdude: safemode: Fuses OK (E:FF, H:DF, L:62)

avrdude done.  Thank you.

書き込みがうまくいくと、下のような感じでLEDが点滅します。(動画はずっと前のもの)

うまくいかなかった場合は、うまくいくように頑張ってください😇

うまくいった方は、これでATtiny85プログラミングができるようになりました!おめでとうございます!

まとめ

今回は以下のことを行いました。

  • クロスコンパイル環境を導入した。
  • ATtiny85のピン・ポート・レジスタ・メモリについて知った。
  • Lチカのプログラムを書いた。
  • ソースファイルをコンパイルしてHEXファイルへ変換して、ATtiny85にプログラムを書き込んだ。
  • LEDを光らせた。

次回はマイコンとEEPROMを接続し、マイコンからEEPROMに読み出すのをやってみようと思います。

おまけ: コンパイルの仕組みを知る

avr-gcc を使うとソースファイルを実行可能形式にビルドしてくれますが、その内部では

  • 前処理(プリプロセス)
  • コンパイル処理
  • アセンブル処理
  • リンク処理

が行われています。

これについて知っておくとバグやエラーの発生原因の特定がしやすくなると思います。

が、さすがにこの記事が肥大化しすぎてしまったので、この説明はしない、もしくは次回以降に回したいと思います。

参考文献

迫真の氷結晶 © 2021