AVRマイコン開発3|TinyやATmegaを使って|I2Cと割込みの詳細

当サイトにはアフィリエイト広告が表示されます。

※Arduinoやmbedの記事ではありません。
Atmel AVRの開発のパート2。
AVRをCとアセンブラで開発、JTAGICEでデバッグ。
CPUはTiny2313・ATmega328・ATmega1284など。
Arduinoに使われてますが、ここではJTAGICEと共にC言語やアセンブラで直接コーディングしています。

スポンサーリンク
興味があるところを読む

I2C

I2Cの理屈や詳細は、ここが詳しい。
使わせて頂いたライブラリのリンクは現在は不明。
読むだけだと頭に入らないんで自分なりにまとめた。

・I2Cはマルチドロップ可。RS485なんかと同じ。
・常にマスターが通信を起動する。クロックもマスターが出す。SPIと同じ。
・ACK/NACKで返事をする。

マスター → スレーブと送信する場合

黄色の吹き出しがマスター、赤の吹き出しはスレーブが出したものだ。

  1. 開始マーク(1bit)
    通信開始の合図なのだ。
  2. 相手アドレス(7bit)
    相手(スレーブ)のアドレスだけど、ソフトでは左に1bitシフトして出す。例えば0x20の相手にデータを渡したいなら0x40を出力するってことだ。
  3. R/W(1bit)
    データを渡すなら0、受け取るなら1をセットする。
  4. A-ACK(1bit) スレーブが出力
    2:で指定した相手が準備完了って意味でACKを出した。
  5. データ(8bit)
    相手に渡すデータ列。
  6. D-ACK(1bit) スレーブが出力
    相手がデータの受取完了って意味でACKを出した。
  7. 終了マーク(1bit)
    通信終了の合図なのだ。 

マスター ← スレーブと受信する場合

黄色の吹き出しがマスター、赤の吹き出しはスレーブが出したものだ。

  • ③R/W(1bit)
    データを受け取るんで1をセットした。
  • ⑤データ(8bit) スレーブが出力
    相手が出力したデータ列。
  • ⑥D-NACK(1bit) マスターが出力
    データの受取完了&終わりって意味で、マスターがNACKを出した(複数バイトの受信のときはここでACKを出し、最後のデータを受信したらNACKを出す).
    ※ACKを出力するのは、マスタ、スレーブに限らず、データを受信した側です。通常は正常応答という意味で、データを受信した側がACKを送信しますが、最終データという意味でNOACKを送信する場合もあります。

プログラムだけど、実験したのはTWIでのI2Cだ。
CPUはマスター/スレーブ共Atmega328P。
スレーブは、エレキジャックの「AVRのI2C通信プログラミング(5) TWI使用のI2Cスレーブ」をそのまま使わせて頂いた。有り難うございます。

マスターはチョイ加工してる。
メイン部分はエレキジャックのテスト用のマスターのソースを元にしてるけど、ライブラリはfleuryさんのTWIマスターを使わせて頂いた・・・徹底的に削り込んだ素晴らしいプログラムなり。
このライブラリには、TWIマスター(C言語版)と、ソフトI2Cマスター(アセンブリ言語版)の両方のソースが入ってる。
どっちも完全にコンパチとのことだから、TWIの無いAVRでもOKなはずだ(ソフトI2Cマスターは未だ試してない)。

//マスター側メイン部分(ライブラリはfleuryさんのTWIマスターライブラリ)
 	include <avr/io.h>
#include "i2cmaster.h"

#define true 1
#define false 0

#define bitLED1 4     // PD4
#define bitLED2 5     // PD5
#define LED1 (1<<bitLED1)
#define LED2 (1<<bitLED2)

#define LED1_ON PORTD|=LED1       // LED1 ON
#define LED1_OFF PORTD&=~LED1   // LED1 OFF
#define LED2_ON PORTD|=LED2      // LED2 ON
#define LED2_OFF PORTD&=~LED2   // LED2 OFF

unsigned char LedData = 0;

int main(void)
{
    unsigned char dat;
    DDRD = LED1 | LED2;     //LED用ポートのみ出力に設定

    i2c_init();
    while (1) {
        _my_Delay3();
        i2c_start(0x40);        // スレーブアドレス=0x20(左に1bitシフトした値)+ write
        i2c_write(LedData);
        i2c_stop();

        dat = LedData & 3;
        if (dat == 0) { LED1_OFF; LED2_OFF; }
        else if (dat == 1) { LED1_ON; LED2_OFF; }
        else if (dat == 2) { LED1_OFF; LED2_ON; }
        else { LED1_ON; LED2_ON; }

        _my_Delay3();

        i2c_start(0x41);          // スレーブアドレス=0x20(左に1bitシフトした値)+ read
        dat = i2c_readNak();   // このデータは最終データとなるので、スレーブへの応答は"NACK"
        i2c_stop();

        LedData = dat;
        LedData++;
    }
}

// ディレー(最適化をゼロにしないと弾かれてコールされないかも)
void _my_Delay3(void)
{
        long i;
        for (i=0L;i<10000L;++i);
}
//fleuryさんのTWIマスターライブラリの修正部分
/*************************************************************************
* Title: I2C master library using hardware TWI interface
* Author: Peter Fleury <pfleury@gmx.ch> http://jump.to/fleury
* File: $Id: twimaster.c,v 1.3 2005/07/02 11:14:21 Peter Exp $
* Software: AVR-GCC 3.4.3 / avr-libc 1.2.3
* Target: any AVR device with hardware TWI
* Usage: API compatible with I2C Software Library i2cmaster.h
**************************************************************************/
#include <inttypes.h>
#include <compat/twi.h>

#include "i2cmaster.h"


/* define CPU frequency in Mhz here if not defined in Makefile */
#ifndef F_CPU
#define F_CPU 8000000UL   // 内部クロック使用(CLKDIV8を非チェック)
#endif

/* I2C clock in Hz */
#define SCL_CLOCK 100000L // 分周の関係でF_CPU=8MHzだと20k以上の速度が無難だろう
//	elekijackのスレーブ(コメントを一部修正)
/*
* mega328p_slave_drv.c
*
* Created: 平成 25/1/6 6:13:12
* Author: oj3
*/

//
// elekijackのスレーブ(「TWI使用のI2Cスレーブ その1~4」)
// http://www.eleki-jack.com/mycom2/2007/11/avri2c5_twii2c_1.html#more
// http://www.eleki-jack.com/mycom2/avr/avri2c/
//
// CPU:mega328p 内蔵OSCの8MHz I2C通信速度=マスターが決めるんでスレーブではやることナシ
// コンパイルオプション:変更ナシ
//

#include <avr/io.h>

typedef unsigned char BYTE;
typedef unsigned short WORD;

// PD4にLED1, PD5にLED2が接続されているものとする
#define bitLED1 4     // PD4
#define bitLED2 5     // PD5
#define LED1 (1<<bitLED1)
#define LED2 (1<<bitLED2)

#define LED1_ON PORTD|=LED1        // LED1 ON
#define LED1_OFF PORTD&=~LED1     // LED1 OFF
#define LED2_ON PORTD|=LED2         // LED2 ON
#define LED2_OFF PORTD&=~LED2     // LED2 OFF

#define rgTWISlEna (1<<TWEA)|(1<<TWEN)     // スレーブ ACK応答
#define rgClrTWInt rgTWISlEna|(1<<TWINT)     // TWINT割り込み要因フラグのクリア

BYTE MyAdrs;
BYTE I2CData;

// プロトタイプ
void I2CSlInit(BYTE adrs);
void I2CSlCom();

void SlaveInit(BYTE dat);
void SlaveReceive(BYTE dat);
BYTE SlaveSend(void);

//-----------------------------------------------------------
// プログラム本体
//-----------------------------------------------------------
int main(void)
{
    DDRD = LED1 | LED2; // LEDポートのみ出力ポートに設定
    MyAdrs = 0x20; // 自I2Cスレーブ・アドレス
    I2CSlInit(MyAdrs); // I2Cスレーブ初期化
    while(1) I2CSlCom();
}
// -----------------------------------------------------------
// I2Cスレーブ初期化
// IN adrs:My I2Cスレーブ・アドレス
// -----------------------------------------------------------
void I2CSlInit(BYTE adrs)
{
    adrs <<= 1;
    // adrs |= 1; // ジェネラル・コール・アドレス許可
    TWAR = adrs;
    TWCR = rgTWISlEna;
}
// -----------------------------------------------------------
// I2Cスレーブ処理(メイン・ループに入れる)
// -----------------------------------------------------------
void I2CSlCom()
{
    if(!(TWCR & (1<<TWINT))) return; // TWINTが未セットのとき終了

    // TWINTがセットされているとき
    switch(TWSR) { // TWSR=状態コード
    //----------------------------
    // データ受取
    //----------------------------
    // CB(Write)受信
    case 0x60:
        SlaveInit(TWDR); // データ受取のシーケンス開始処理(TWDR=CB)
        TWCR = rgClrTWInt; // INT要求フラグ・クリア
        break;
    // data受信
    case 0x80:
        SlaveReceive(TWDR); // データ受取処理(TWDR=data)
        TWCR = rgClrTWInt; // INT要求フラグ・クリア
        break;
    //----------------------------
    // データ送出
    //----------------------------
    // CB(Read)受信
    case 0xA8:
        SlaveInit(TWDR); // データ送出のシーケンス開始処理(TWDR=CB)
        TWDR = SlaveSend(); // 第1バイト送出
        TWCR = rgClrTWInt; // INT要求フラグ・クリア
        break;
    // ACK受信
    case 0xB8:
        TWDR = SlaveSend(); // 第2バイト以降送出(送出継続)
        TWCR = rgClrTWInt; // INT要求フラグ・クリア
        break;
    //----------------------------
    // その他
    //----------------------------
    case 0xC0: // NOACK受信(最終データなのでスレーブ送信終了)
    case 0xA0: // スレーブ受信中にストップ・コンディションが来た
        TWCR = rgClrTWInt; // INT要求フラグ・クリア
        break;
    }
}
//---------------------------------------------------------------------------------
// CB受信イベント・ハンドラ
// IN dat: 受信したデータ(コントロール・バイト)
// このハンドラでは、コントロール・バイトを受信したタイミング、つまり、通信パケットの送受信の始まりの
// タイミングを知ることができます。用途としては、たとえば、送受信バッファのポインタを0に初期化するとか
// 送受信のバイト数のカウンタを0にリセットするなどのパケット単位の処理の初期化に使います。
//---------------------------------------------------------------------------------
void SlaveInit(BYTE dat)
{}
//---------------------------------------------------------------------------------
// スレーブ受信イベント・ハンドラ
// IN dat: 受信したデータ
// このハンドラでは、データを受信したタイミングと、受信データを得ることができます。受信データをバッファ
// に格納するなどの処理はこのハンドラで行います。
//---------------------------------------------------------------------------------
void SlaveReceive(BYTE dat)
{
    I2CData = dat;
    switch(dat & 3) {
    case 0:
        LED1_OFF;
        LED2_OFF;
        break;
    case 1:
        LED1_ON;
        LED2_OFF;
        break;
    case 2:
        LED1_OFF;
        LED2_ON;
        break;
    default:
        LED1_ON;
        LED2_ON;
        break;
    }
}
//---------------------------------------------------------------------------------
// スレーブ送信イベント・ハンドラ
// OUT Acc: 送信するデータ
// このハンドラは、スレーブ送信直前に呼び出されるため、送信バッファからデータを取り出すなどの処理は
// ここで行います。
//---------------------------------------------------------------------------------
BYTE SlaveSend(void)
{
    return I2CData; // 過去にマスタから送られてきたデータを送り返す
}
スポンサーリンク

ポート設定 

一年ほど離れてたら、チョー基本的な事を忘れてるんでメモ。

  • ポートBの方向設定 
    DDRB = 0b11110000;  // BIT7~4=OUT方向、BIT3~0=IN方向
     
  • ポートBへOUTする
    PORTB |= BIT00;        // BIT0だけを1にする
    PORTB |= _BV(PB0);
    sbi(PORTB, PB0);

    PORTB &= ~BIT00;      // BIT0だけを0にする
    PORTB &= ~_BV(PB0);
    cbi(PORTB, PB0);
     
  • 内部プルアップの設定(BIT7だけをプルアップする例)
    DDRB = 0b01111111;    // IN方向に設定しておいて・・・
    PORTB = 0b10000000;  // 1をOUTするとプルアップされる
     
  • ビットを反転させる(BIT0を反転させる)
    DDRB = 0b00000001;    // OUT方向に設定しておいて・・・
    PINB = 0b000000001;    // PINBで1を書く

割込みの初期設定

割込みがらみのレジスタ設定 詳細 マニュアル(P39)
mega328では、INT0~1とPCINT0~20の割込みがあるが、INTはPIN別+優先順位高、PCINTは8PIN分まとめて発生し優先順位が低い。
検索文字列としては INTでは EICRA/EIMSK 、PCINTでは PCICR/PCMSK 、その他 sei() cli() ISR などだ。

レジスタ設定ではビットが定義してあるので sbi(EIMSK, INT0) などとする。
割込み関数は ISR(INT0_vect) で記述する。Symbol名の一覧表は概要のページ下部を参照のこと。
なおPCINTはレベル変化で割り込みが発生するので、ISR関数内でポートを読んで処理を決めてやること。

<例>
    cli();
    DDRC = 0b11111111;    // OUT方向に設定しても割込は発生する(この機能によりソフトウエア割込みを実現)
    sbi(PCICR, PCIE1);        // PCINT8~14の割込を使用する
    sbi(PCMSK1, PCINT8);   // PCINT8のマスクを解除
    sei();

ISR(PCINT1_vect)
{
    bool LimitOn = (PINC & _BV(PCINT8))? TRUE: FALSE;    // HレベルならリミットONと判断してる
}

INT0~1 INT0_vect / INT1_vect

PCINT0~20 PCINT0_vect / PCINT1_vect / PCINT2_vect

スポンサーリンク

定義済みの変数など

as6(Atmel studio 6)を起動して、ソリューションエクスプローラの”Dipendencies\io.h”にCPU名の一覧がある。
  __AVR_ATmega1284P__ とか __AVR_ATmega328P__ __AVR_ATtiny4313__ ・・・などなど。
これらの定義済み#defineは、コンパイル時に参照可能なので以下のような使い方が可能。

// 1284はUART0を使用
#if defined (__AVR_ATmega1284P__)
#define _ISR_RX_NAME    USART0_RX_vect    // 受信割込みベクタ
#define _ISR_UDRE_NAME  USART0_UDRE_vect  // 送信    "
#warning "_uart.cは1284p用に作成された"
// 328はUARTは一つだけだ
#elif defined (__AVR_ATmega328P__)
#define _ISR_RX_NAME    USART_RX_vect    // 受信割込みベクタ
#define _ISR_UDRE_NAME  USART_UDRE_vect  // 送信    "
#warning "_uart.cは328p用に作成された"
#endif

また各CPUに特有のレジスタ名や割り込みベクタ名などは、iomxxxx.h(xxxxにはCPUの型番が入る)に記述されてる。

SPI

SPIを使ったんでメモ。 SPIの詳細な説明 データシート翻訳
SPI通信の相手は特殊な専用品なんだけど、ま、理屈は同じなんで。

この部品は送信準備ができるとDRラインをHにする。
つまりDR=1になったらSPIで受信してやる。
DRはPCINT1割込みで取ってる.。
なお下記プログラムでは送信と受信を分けてるけど、別に一つでも同じ事だよね。
 BYTE rxd = RspiPutch(0) ってやれば受信もするもん。

// 初期化
 	
    //-------------------------------------------------------
    // PBポートのI/O方向の設定
    //-------------------------------------------------------
    DDRB = 0b11101101;  // PB7=OUT 未使用
                        // PB6=OUT 未使用
                        // PB5=OUT SCK
                        // PB4=IN  MISO
                        // PB3=OUT MOSI
                        // PB2=OUT 未使用
                        // PB1=IN  DR(PCINT1)
                        // PB0=OUT 未使用
                            
    // ピン変化割込みの設定
    sbi(PCICR, PCIE0);  // PCINT0~7の何れかの変化が割込を起こす
    sbi(PCMSK0,PCINT1); // PCINT1(DR)のみ割込を許可
    
    //-------------------------------------------------------
    // SPI設定(xxxxManualより)
    //  ・MSB が最初に転送されます
    //  ・SCK=Lレベルはアイドル状態(CPOL=0)
    //  ・入力データのサンプリングはSCKの立下りで行われます(CPHA=1)
    //  ・SPI周波数は200~400KHz
    //-------------------------------------------------------
    SPCR = 0b01010110;  // b7=0:SPIF(転送終了時)の割込みは使わない
                        // b6=1:SPIを使う
                        // b5=0:MSB first
                        // b4=1:master mode
                        // b3=0:CPOL(クロックの向き)0=常時L, 1=常時H
                        // b2=1:CPHA(サンプルエッジ)0=前, 1=後ろ
                        // b1/0=10:分周比は32または64(SPI2Xでどちらかを指定する)
                        //  SPI2X=0 & 00=4, 01=16, 10=64, 11=128
    sbi(SPSR, SPI2X);   //  SPI2X=1 & 00=2, 01=8 , 10=32, 11=64 → '110'=32分周=CPUφ8MHz÷32=250KHz
//割込みと送受信
 	
// PCINT0割込み分岐処理
ISR(PCINT0_vect)
{
    if (PINB & _BV(PCINT1)) {   // DR割込みなら・・・
        BYTE d = RspiGetch();   // 受信する
    }
}

// 送信
BYTE RspiPutch(BYTE wdata)
{
    SPDR = wdata;               // 1バイト送信
    while (!(SPSR & BIT07));    // シリアル転送完了待ち
    BYTE rdata = SPDR;          // 念のため読み出し
    return rdata;
}

// 受信
BYTE RspiGetch(void)
{
    SPDR = 0;                   // CLK送出
    while (!(SPSR & BIT07));    // シリアル転送完了待ち
    BYTE rdata = SPDR;          // 1バイト受信
    return rdata;
}
スポンサーリンク

購入リンク

¥34,241 (2023/01/30 04:50時点 | Amazon調べ)
スポンサーリンク
  • URLをコピーしました!

コメント

コメントする

CAPTCHA


興味があるところを読む