【Arduino工作〜発展編】AVR-RustでAtmega328pからPWM波形を出力したい!


2021/10/29
蛸壺の中の工作室|AVR-RustでAtmega328pからPWM波形を出力したい!

AVR-RustライブラリからAVRマイコンの組込開発を考えてみる記事の第3段です。

今回から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の3つの内どれかを使うことになります。

ここでは典型的なPWM波形として良く利用される
高速PWMモードを利用します。

高速PWMモード?

PWMとは連続周期を持つ矩形波で、デューティー比でその波形を制御しています。

AVRマイコンのタイマーにおける高速PWMモードとは、以下の模式図のようにノコギリ波を内部で発生させている状態になります。

合同会社タコスキングダム|蛸壺の中の工作室

今回利用するノコギリ波は、
ICR*レジスタ(ただし「*」はタイマーの番号)で波形最大値を設定しています。

また
OCR*AレジスタでPWM波形の立ち下がりの値を設定し、この設定を基準に、ノコギリ波との比較からPWM波形が出力される仕組みになっています。

よって、ICR*の設定値とOCR*Aの設定値の比率が、デューティー比そのものになります。

タイマーとPWM出力ができるピン

AVRマイコンの製品の種類によって事情が異なりますが、Atmega328pはタイマー0・タイマー1・タイマー2の三種類があります。

各タイマーは、それぞれ2つのPWM出力ピンを持っています。

これにより、Atmega328pでは最大で6つのPWM出力ピンを設定することができます。

ただしタイマー0とタイマー2が8bit、タイマー1が16bitのカウンターという仕様ですので、8bitタイマーの場合
0~255まで、16bitタイマーでは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マイコン開発初学者のうちは、どのピンを使えばいいのか悩ましく感じるかも知れませんが、多くの方が大体最初に触れるのが、タイマー1のPB1ピンによるPWM波形出力かと思います。

PWMピンの使い方を一つ覚えると、他のPWMピンもほぼ同じ操作で使うことができるので、ここではまず1つのピンを使ってPWMを発生させることにフォーカスしましょう。

TCCR1AとTCCR1Bレジスタ

TCCR1AとTCCR1Bレジスタでは、タイマー1のモード選択や、周波数分周、PWMの詳細を設定することになります。

最初にTCCR1Aレジスタ(8bit)の構造は以下の通りです。

ビット番号

7

6

5

4

3

2

1

0

名称

COM1A1

COM1A0

COM1B1

COM1B0

無し

無し

WGM11

WGM10

ここで重要なビット設定は4-7ビット目の組み合わせです。コンペアマッチした際に、波形がどのように変化するかを設定します。

            COM1A1-COM1A0-COM1B1-COM1B0:
    0000 ... 出力波形なし
    0101 ... コンペアマッチでトグル
    1010 ... コンペアマッチでLOW
    1111 ... コンペアマッチでHIGH
        
もう一方のTCCR1Bレジスタ(8bit)の構造は以下のようになります。

ビット番号

7

6

5

4

3

2

1

0

名称

1CNC1

1CES1

無し

WGM13

WGM12

CS12

CS11

CS10

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]を1周期としたいなら、0から始めて50000回カウントアップさせるので、ICR1 = 49999と指定します。微々たる違いですが、ICR1 = 50000ではないことに注意してください。

他方で、OCR1Aレジスタではコンペアマッチした時に、HIGH(かLOW)にする時間(クロック数単位)を設定して利用します。

このOCR1Aの値を変えることで、自由にデューティー比を変えることが可能になります。

OCR1Aレジスタにも16bitまでの整数を指定します。

例えば、0からカウントして25000回の位置を指定すると、
1[μs] * 25000 = 25[ms]が1周期中でHIGH(もしくはLOW)時間が25msのPWM波形が生成できます。この場合、OCR1A = 24499と指定します。

以上、ここまでで色々と小難しいタイマーとレジスタの話を説明しましたが、実際のプログラムの実装はかなりアッサリとします。

プログラミングをやりながら、レジスタの設定値の意味を考えても良いかも知れません。


Rustでのプログラム実装

ではここからRustでの実装部分を説明します。

実行ファイルのビルドから書き込みまでの手順は前回の記事と同じです。

前回の記事との主な違いは
src/main.tsの内容だけですので、ビルドの方法やAtmega328pへの書き込み手順の詳細は割愛させていただきます。

マイコンから発信できる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を1000にして、コンペアマッチでLOWへ立ち下げるような波形としています。

②の箇所でTCCR1Bレジスタを設定しています。
CS12-CS11-CS10を011とすることで、基礎周波数の1/64で分周しています。

また、TCCR1AとTCCR1Bレジスタに渡って設定した、
WGM13-WGM12-WGM11-WGM10は1110です。これで、タイマー1は高速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ライブラリの使いこなしを模索していこうかと思っています。

参考サイト

The AVR-Rust project | Guthub

記事を書いた人

記事の担当:taconocat

ナンデモ系エンジニア

電子工作を身近に知っていただけるように、材料調達からDIYのハウツーまで気になったところをできるだけ細かく記事にしてブログ配信してます。