【Arduino工作〜発展編】AVR-RustでAtmega328pからPWM波形を出力したい!
※ 当ページには【広告/PR】を含む場合があります。
2021/10/29

今回からLチカレベルから卒業して、次なるステップとしてPWM波形の出力方法を取り上げてみます。
AVRマイコンでのPWM概論
前回の記事でのLチカは、一定の間隔でポートからの出力をON/OFFさせているようなプログラムでした。
PWMはLチカとは異なり、AVRのタイマーを利用して波形を生成することになります。
まずは実際のPWM波形出力プログラムに進む前に、Atmega328pのタイマーの話をまとめましょう。
なお、LTspiceからPWM波形のシミュレーションを行うためのガイダンス記事を以下に用意しましたので、マイコンを模してPWMの回路シミュレーションを行いたい場合には何かしら参考になると思います。
AVRマイコンのタイマーモード
まずPWM波形を出力生成するためには、AVRマイコンでのタイマーモードについて理解しておく必要があります。
「標準モード」:
いわゆる普通のタイマー。
8bitと16bitの2種類があり、カウンタとして動作させたい時に使用
「CTCモード」:
コンペアマッチ(指定値とタイマーカウントが一致)を使うモード。
この指定値はOCR●AかOCR●Bで設定する。
タイマーカウントがその指定値を超えたら矩形波形が出力電圧を反転し、
同時にタイマは0に戻る
「高速PWMモード」:
PWM波形を出力する際に使用。
ただしタイマー波形はノコギリ波ベースとなる
「位相基準PWMモード」:
こちらもPWM波形を出力する際に使用。
ただしタイマー波形は三角波ベースとなる
があります。
PWMを出力したい場合、
CTC
高速PWM
位相基準PWM
ここでは典型的なPWM波形として良く利用される
高速PWMモード
高速PWMモード?
PWMとは連続周期を持つ矩形波で、デューティー比でその波形を制御しています。
AVRマイコンのタイマーにおける高速PWMモードとは、以下の模式図のようにノコギリ波を内部で発生させている状態になります。

今回利用するノコギリ波は、
ICR*レジスタ
また
OCR*Aレジスタ
よって、ICR*の設定値とOCR*Aの設定値の比率が、デューティー比そのものになります。
タイマーとPWM出力ができるピン
AVRマイコンの製品の種類によって事情が異なりますが、Atmega328pはタイマー0・タイマー1・タイマー2の三種類があります。
各タイマーは、それぞれ2つのPWM出力ピンを持っています。
これにより、Atmega328pでは最大で6つのPWM出力ピンを設定することができます。
ただしタイマー0とタイマー2が8bit、タイマー1が16bitのカウンターという仕様ですので、8bitタイマーの場合
0~255
0~65535
ということで、精度の求められる制御ではタイマー1からPWM波形を得るほうが適切で、精度を問わないならタイマー0/2を使うようにします。
ということで、Atmega328pのPWM出力をざっくりまとめると以下のようになります。
タイマー0:
カウンター:
8bit
ピン出力値レジスタ(ピン名):
OCR0A(PD6)
OCR0B(PD5)
タイマ/PWMの設定レジスタ:
TCCR0A
TCCR0B
タイマー1:
カウンター:
16bit
ピン出力値レジスタ(ピン名):
OCR1A(PB1)
OCR1B(PB2)
タイマ/PWMの設定レジスタ:
TCCR1A
TCCR1B
タイマー2:
カウンター:
8bit
ピン出力値レジスタ(ピン名):
OCR2A(PB3)
OCR2B(PD3)
タイマ/PWMの設定レジスタ:
TCCR2A
TCCR2B
以降の実装例では、タイマー1のPWMピンのA側(PB1ピン)を使って波形出力を試します。
タイマー1のレジスタ操作
先程も述べたように、Atmega328pには計6つのPWMピンがあります。
AVRマイコン開発初学者のうちは、どのピンを使えばいいのか悩ましく感じるかも知れませんが、多くの方が大体最初に触れるのが、16ビットタイマー1からのPWM波形出力かと思います。
タイマー1には、OC1Aの出力先であるPB1ピン(ArduinoではD9)と、OC1Bの出力先であるPB2ピン(ArduinoではD10)の2つが存在します。
PWMピンの使い方を一つ覚えると、他のPWMピンもほぼ同じ操作で使うことができるので、ここではまず1つのピンを使ってPWMを発生させることにフォーカスしましょう。
TCCR1AとTCCR1Bレジスタ
TCCR1AとTCCR1Bレジスタでは、タイマー1のモード選択や、周波数分周、PWMの詳細を設定することになります。
最初にTCCR1Aレジスタ(8bit)の構造は以下の通りです。
ここで重要なビット設定は4-7ビット目の組み合わせです。
コンペアマッチした際に、波形がどのように変化するかを設定します。
6-7ビット目(COM1A1-COM1A0)はOC1Aピンからの出力モード、4-5ビット目(COM1B1-COM1B0)はOC1Bピンからの出力モードをそれぞれ設定します。
COM1A1-COM1A0: OC1Aピンの出力モード
00 ... 出力なし
01 ... コンペアマッチでトグル
10 ... コンペアマッチでLOW(ノン・インバートモード)
11 ... コンペアマッチでHIGH(インバートモード)
COM1B1-COM1B0: OC1Bピンの出力モード
00 ... 出力なし
01 ... コンペアマッチでトグル
10 ... コンペアマッチでLOW(ノン・インバートモード)
11 ... コンペアマッチでHIGH(インバートモード)
ノン・インバートモードの時はOCR1A(OCR1B)とカウンターの値を比較して、カウンターより小さい時にOC1A(OC1B)の出力をHIGH、大きい時にはLOWにすることを指します。
インバートモードはその反対です。
トグルモードはその名の通り比較マッチ時に、HIGHとLOWを反転させます。
もう一方のTCCR1Bレジスタ(8bit)の構造は以下のようになります。
1CNC1と1CES1は上級者向けですので、ここではあまり気にせずデフォルトのままで結構です。
TCCR1Bレジスタで重要なのは0-2ビット目の組み合わせです。
CS12-CS11-CS10:
000...タイマー停止
001...分周無し(=1)
010...1/8
011...1/64
100...1/256
101...1/1024
110...外部クロックT1立ち下がりエッジ
111...外部クロックT1立ち上がりエッジ
また、TCCR1AとTCCR1Bの2つのレジスタ渡って設定するWGM1*も重要な設定になります。
WGM13-WGM12-WGM11-WGM10:
0000...標準・TOP(0xFFFF)
0001...8bit位相標準PWM・TOP(0x00FF)
0010...9bit位相標準PWM・TOP(0x01FF)
0011...10bit位相標準PWM・TOP(0x03FF)
1000...位相/周波数標準PWM・TOP(ICR1)
1001...位相/周波数標準PWM・TOP(OCR1A)
1010...位相標準PWM・TOP(ICR1)
1011...位相標準PWM・TOP(OCR1A)
0100...CTC・TOP(ICR1)
1100...CTC・TOP(OCR1A)
1101...無し
0101...8bit高速PWM・TOP(0x00FF)
0110...9bit高速PWM・TOP(0x01FF)
0111...10bit高速PWM・TOP(0x03FF)
1110...高速PWM・TOP(ICR1)
1111...高速PWM・TOP(OCR1A)
タイマーの設定とは言っても組み合わせ次第で、かなりのバリエーションが存在していることが分かります。
OCR1AとICR1レジスタ
先程の高速PWMモードの説明で利用した図を再掲します。

タイマーモードごとに微妙に意味が変わってきますが、高速PWMモードでは、ICR1レジスタがノコギリ波の最大値を表しています。
タイマー1は16bitですので、ICR1レジスタには16bitの整数(0~65535)を入力するように利用します。
例えば、クロック周波数1MHz(=1μs)の場合、
1[μs] * 50000 = 50[ms]
ICR1 = 49999
ICR1 = 50000
他方で、OCR1Aレジスタではコンペアマッチした時に、HIGH(かLOW)にする時間(クロック数単位)を設定して利用します。
このOCR1Aの値を変えることで、自由にデューティー比を変えることが可能になります。
OCR1Aレジスタにも16bitまでの整数を指定します。
例えば、0からカウントして25000回の位置を指定すると、
1[μs] * 25000 = 25[ms]
OCR1A = 24499
以上、ここまでで色々と小難しいタイマーとレジスタの話を説明しましたが、実際のプログラムの実装はかなりアッサリとします。
プログラミングをやりながら、レジスタの設定値の意味を考えても良いかも知れません。
Rustでのプログラム実装
ではここからRustでの実装部分を説明します。
実行ファイルのビルドから書き込みまでの手順は前回の記事と同じです。
前回の記事との主な違いは
src/main.ts
マイコンから発信できるPWM波が本気を出すと数千Hzの周波数でも可能なので、LEDで目視して確認するのは不可能です。
もちろんオシロスコープなどがあれば精細な波形を確認することもできるのですが、今回は目視できるくらい低速な周波数のPWMを分周機能を利用して作ってみます。
以下が今回のメインプログラムになります。
#![feature(llvm_asm, lang_items, unwind_attributes)]
#![no_std]
#![no_main]
extern crate avr_delay;
extern crate avr_std_stub;
extern crate avrd;
use avrd::atmega328p::{DDRB};
use avrd::atmega328p::{TCCR1A, TCCR1B, OCR1A, ICR1};
use core::ptr::write_volatile;
#[no_mangle]
pub extern "C" fn main() {
unsafe {
//👇①TCCR1Aレジスタ
// COM1A1-COM1A0-COM1B1-COM1B0-無し-無し-WGM11-WGM10
// 1000(コンペアマッチAでLOW)-00-10(高速PWM・TOP(ICR1)モードの下位ビット)
write_volatile(TCCR1A, 0b10000010);
//👇②TCCR1Bレジスタ
// 1CNC1-1CES1-無し-WGM13-WGM12-CS12-CS11-CS10
// [プリスケーラx64]: 00-0-11(高速PWM・TOP(ICR1)モードの上位ビット)-011(1/64)
write_volatile(TCCR1B, 0b00011011);
//👇③周期間隔: 1μs * 65536 ~ 65.5ms
write_volatile(ICR1, 65535);
//👇④HIGH時間: 1μ * 32768 ~ 32.8ms (DUTY比0.5相当)
write_volatile(OCR1A, 32767);
//PB1ピンを出力として設定
write_volatile(DDRB, 0b00000010);
}
}
まずポイント毎に解説していきます。 AVR-Rustライブラリの概要に関しては、
①の箇所では、TCCR1Aレジスタを設定しています。 ここでは
COM1A1-COM1A0-COM1B1-COM1B0
②の箇所でTCCR1Bレジスタを設定しています。
CS12-CS11-CS10
また、TCCR1AとTCCR1Bレジスタに渡って設定した、
WGM13-WGM12-WGM11-WGM10
高速PWM・TOP(ICR1)モード
③の箇所で、タイマーのノコギリ波の最大値を65536カウントの最大値でICR1へ指定しています。
④の箇所で、デューティー比0.5狙いの32768カウントでOCR1Aレジスタに指定して、後はPB1ピンからPWM波形が出力されるのを確認するだけです。
(低速)PWMを目視で確認
周波数に関して、Atmega328pだと実質の内部クロック1MHz(出荷値)ですので、1クロックは
1 / 1[MHz] = 1E-6[s] = 1[μs]
ということで、タイマー1(16bit,0~65535)の高速PWMモードだと、
1[μs] * 65536 = 65.536[ms]
65.5msは周波数換算で約15Hzになり、コレでもかなり動体視力の良い人間の目でないと視覚できないので、プレスケーラ値を64にて、1/64に分周します。
この周波数が64分の1になると、
約0.24Hz程度(4秒に一回)

4秒に一回の周期でPWM波形が確かに出力されいるのが分かります。
実際にはもっと高速なスイッチングで利用するのですが、今回はPWM波形をLEDの点滅具合で目視出来るような速度でやってみました。
まとめ
以上、今回はもっともシンプルなAVRマイコンからのPWM波形出力をRustで実装してみました。
次回はもう少しPWMの発展的に使った例で、AVR-Rustライブラリの使いこなしを模索していこうかと思っています。