※Arduinoやmbedの記事ではありません。
Atmel AVRの開発のパート2。
AVRをCとアセンブラで開発、JTAGICEでデバッグ。
CPUはTiny2313・ATmega328・ATmega1284など。
Arduinoに使われてますが、ここではJTAGICEと共にC言語やアセンブラで直接コーディングしています。
I2C
I2Cの理屈や詳細は、ここが詳しい。
使わせて頂いたライブラリのリンクは現在は不明。
読むだけだと頭に入らないんで自分なりにまとめた。
・I2Cはマルチドロップ可。RS485なんかと同じ。
・常にマスターが通信を起動する。クロックもマスターが出す。SPIと同じ。
・ACK/NACKで返事をする。
マスター → スレーブと送信する場合
黄色の吹き出しがマスター、赤の吹き出しはスレーブが出したものだ。
- 開始マーク(1bit)
通信開始の合図なのだ。 - 相手アドレス(7bit)
相手(スレーブ)のアドレスだけど、ソフトでは左に1bitシフトして出す。例えば0x20の相手にデータを渡したいなら0x40を出力するってことだ。 - R/W(1bit)
データを渡すなら0、受け取るなら1をセットする。 - A-ACK(1bit) スレーブが出力
2:で指定した相手が準備完了って意味でACKを出した。 - データ(8bit)
相手に渡すデータ列。 - D-ACK(1bit) スレーブが出力
相手がデータの受取完了って意味でACKを出した。 - 終了マーク(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;
}
コメント