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


※ 当ページには【広告/PR】を含む場合があります。
2021/10/29
【Arduino工作〜発展編】AVR-RustでAtmega328pをLチカ!レベル2
【Arduino工作〜発展編】AVR-RustでAtmega328pのUSARTを使ってみる
蛸壺の中の工作室|AVR-RustでAtmega328pからPWM波形を出力したい!
AVR-Rustライブラリ からAVRマイコンの組込開発を考えてみる記事の第3段です。

合同会社タコスキングダム|蛸壺の技術ブログ
【Arduino工作〜発展編】AVR-RustでAtmega328pをLチカ!レベル2

ruduinoの内部の実装を参考にしながら、もう少しだけ応用範囲を拡げられるようにAVR-Rustライブラリを使ったLチカを紹介します。



今回からLチカレベルから卒業して、次なるステップとしてPWM波形の出力方法を取り上げてみます。


AVRマイコンでのPWM概論



前回の記事でのLチカは、一定の間隔でポートからの出力をON/OFFさせているようなプログラムでした。
PWMはLチカとは異なり、AVRのタイマーを利用して波形を生成することになります。
まずは実際のPWM波形出力プログラムに進む前に、Atmega328pのタイマーの話をまとめましょう。
なお、LTspiceからPWM波形のシミュレーションを行うためのガイダンス記事を以下に用意しましたので、マイコンを模してPWMの回路シミュレーションを行いたい場合には何かしら参考になると思います。

合同会社タコスキングダム|蛸壺の技術ブログ
【LTspice入門】LTspiceでマイコンのPWM波形発生モデルを作成・シミュレートする方法

LTspice上でマイコンに見立てた自作コンポーネントから擬似的な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マイコン開発初学者のうちは、どのピンを使えばいいのか悩ましく感じるかも知れませんが、多くの方が大体最初に触れるのが、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)の構造は以下の通りです。

ビット番号 7 6 5 4 3 2 1 0
名称 COM1A1 COM1A0 COM1B1 COM1B0 無し 無し WGM11 WGM10

ここで重要なビット設定は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)の構造は以下のようになります。

ビット番号 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での実装部分を説明します。
実行ファイルのビルドから書き込みまでの手順は前回の記事と同じです。


合同会社タコスキングダム|蛸壺の技術ブログ
【Arduino工作〜発展編】AVR-RustでAtmega328pをLチカ!レベル2

ruduinoの内部の実装を参考にしながら、もう少しだけ応用範囲を拡げられるようにAVR-Rustライブラリを使ったLチカを紹介します。




前回の記事との主な違いは
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