Ronny10
13.02.2007, 21:00
In der letzten Zeit wird hier viel über den I2C-Bus und seine Einsatzmöglichkeiten gesprochen. Ich möchte euch als Beispiel eine Erweiterung mit einem als I2C-Slave programmierten ATmega8 vorstellen. Damit erweitert man den ASURO um 4 ADC-Kanäle und 16 digitale I/O's. Der Slave arbeitet mit dem Hardware-I2C-Interface (SDA an Pin27, SCL an Pin28 ).
Wie funktioniert das?
Als erstes muss man sich eine Art Befehlssatz ausdenken die der Master (ASURO) dem Slave schickt und die dieser dann ausführt.
Die header-Datei twi_register.h mit dem Befehlssatz:
#define sbi(port,bit) port|=(1<<(bit))
#define cbi(port,bit) port&=(~(1<<(bit)))
#define BIT(n) (1<<(n))
#define MAX_REG 255
#define FALSE 0
#define TRUE 1
/* ic2-slave-adresse */
#define TWI_ADR 0x80
/* set-befehle */
#define TWI_SET_DDRB 1 // definiere über das ddr eingänge und ausgänge von portb
#define TWI_SET_PORTB 2 // setze pull up widerstände an portb
#define TWI_SET_PINB 3 // setze ein einzelnes bit an portb
#define TWI_RES_PINB 4 // rücksetze ein einzelnes bit an portb
#define TWI_SET_DDRC 5 // wie portb
#define TWI_SET_PORTC 6
#define TWI_SET_PINC 7
#define TWI_RES_PINC 8
#define TWI_SET_DDRD 9 // wie portb
#define TWI_SET_PORTD 10
#define TWI_SET_PIND 11
#define TWI_RES_PIND 12
/* get-befehle */
#define TWI_GET_PINB 13 // lese das pinregister von portb
#define TWI_GET_PINC 14
#define TWI_GET_PIND 15
#define TWI_GET_ADC0 16 // lese adc0
#define TWI_GET_ADC1 17
#define TWI_GET_ADC2 18
#define TWI_GET_ADC3 19
Da gibt es Set- Res- und Get-Befehle. Set-Befehle zum setzen von Bits in den Datenrichtungs-Registern, von Ports oder einzelnen Port-Bits und natürlich auch zum zurücksetzen einzelner Bits (Res-Befehl). Mit den Get-Befehlen kann man den Zustand der Pinregister (digitale Eingänge) abfragen oder einen ADC-Kanal einlesen (2x8Bit) den man vorher mit einem Set-Befehl selektiert hat.
Ein Befehl besteht immer aus zwei Byte, dem Befehl und einem Parameter. Wenn ein Befehl keinen Parameter benötigt, z.B. TWI_GET_ADC0, müssen aber trotzdem zwei Byte gesendet werden. Dabei ist es egal welchen Wert der Parameter hat, da er ja nicht ausgewertet wird.
Das ist die I2C-Interrupt-Routine I2C_Slave.c für den ATmega8-Slave:
#include <avr/io.h>
#include <avr/interrupt.h>
#include <compat/twi.h>
#include "twi_register.h"
volatile unsigned char reg = MAX_REG;
volatile unsigned char twi_comm = FALSE;
void twi_init( unsigned char adr )
{
TWAR = adr;
TWCR = BIT(TWINT) | BIT(TWEN) | BIT(TWEA) | BIT(TWIE);
sei();
}
SIGNAL(SIG_2WIRE_SERIAL)
{
switch (TWSR) {
/* SLAVE RECEIVER */
case TW_SR_DATA_ACK:
case TW_SR_DATA_NACK:
if( reg < MAX_REG )
{
switch( reg )
{
case TWI_SET_DDRB: DDRB = TWDR;
break;
case TWI_SET_PORTB: PORTB = TWDR;
break;
case TWI_SET_PINB: sbi(PORTB, TWDR);
break;
case TWI_RES_PINB: cbi(PORTB, TWDR);
break;
case TWI_SET_DDRC: DDRC = TWDR;
break;
case TWI_SET_PORTC: PORTC = TWDR;
break;
case TWI_SET_PINC: sbi(PORTC, TWDR);
break;
case TWI_RES_PINC: cbi(PORTC, TWDR);
break;
case TWI_SET_DDRD: DDRD = TWDR;
break;
case TWI_SET_PORTD: PORTD = TWDR;
break;
case TWI_SET_PIND: sbi(PORTD, TWDR);
break;
case TWI_RES_PIND: cbi(PORTD, TWDR);
break;
case TWI_GET_ADC0: ADMUX = BIT(REFS0) + 0x00;
ADCSRA |= BIT(ADSC);
break;
case TWI_GET_ADC1: ADMUX = BIT(REFS0) + 0x01;
ADCSRA |= BIT(ADSC);
break;
case TWI_GET_ADC2: ADMUX = BIT(REFS0) + 0x02;
ADCSRA |= BIT(ADSC);
break;
case TWI_GET_ADC3: ADMUX = BIT(REFS0) + 0x03;
ADCSRA |= BIT(ADSC);
break;
}
reg = MAX_REG;
}
else
{
reg = TWDR;
twi_comm = reg;
}
TWCR = BIT(TWINT) | BIT(TWEN) | BIT(TWEA) | BIT(TWIE);
break;
/* SLAVE TRANSMITTER */
case TW_ST_SLA_ACK: // twi-adresse und read empfangen
switch( twi_comm )
{
case TWI_GET_PINB: TWDR = PINB;
break;
case TWI_GET_PINC: TWDR = PINC;
break;
case TWI_GET_PIND: TWDR = PIND;
break;
case TWI_GET_ADC0:
case TWI_GET_ADC1:
case TWI_GET_ADC2:
case TWI_GET_ADC3:
while (!(ADCSRA & BIT(ADIF)))
;
TWDR = ADCL;
break;
}
TWCR = BIT(TWINT) | BIT(TWEN) | BIT(TWEA) | BIT(TWIE); // transmit first byte
break;
case TW_ST_DATA_ACK: // erstes daten-byte des adc übertragen und ack empfangen
// sende jetzt das zweite daten-byte des adc
switch( twi_comm )
{
case TWI_GET_ADC0:
case TWI_GET_ADC1:
case TWI_GET_ADC2:
case TWI_GET_ADC3:
TWDR = ADCH;
}
TWCR = BIT(TWINT) | BIT(TWEN) | BIT(TWEA) | BIT(TWIE); // sende zweites byte
break;
default: TWCR = BIT(TWINT) | BIT(TWEN) | BIT(TWEA) | BIT(TWIE);
break;
}
}
Leider kommt das mit den Tabs beim kopieren nicht so 100%tig hin.
Das ist die Initialisierungsfunktion M8_I2C_Slave.c (main):
/*
autor: Peter Wilbert
version: 1.0
datum: 27.01.07
atmega8 als i2c-slave erweitert den asuro
um 4 adc's und 16 digitale i/o's
*/
#include <avr/io.h>
#include <avr/interrupt.h>
#include "twi_register.h"
extern void twi_init( unsigned char );
/* *** hauptprogramm ***
initialisiert den i2c-bus und verweilt dann in einer
endlosschleife. das eigentliche programm ist die twi-
interruptroutine in der die eingehenden slave-befehle
direkt ausgeführt werden
*/
int main(void)
{
/*
adc init
*/
ADCSRA = BIT(ADEN) + BIT(ADPS2) + BIT(ADPS1); // init adc prescale 64
ADMUX = BIT(REFS0) + 0x00; // adc einmal anstossen
ADCSRA |= BIT(ADSC); // start adc
while (!(ADCSRA & BIT(ADIF))) // warte bis ok
;
/*
twi initialisieren
*/
twi_init(TWI_ADR);
/*
for ever
*/
for(;;)
;
return(0);
}
Mit dieser Erweiterung sollte man erst einmal genügend I/O-Pins und ADC-Kanäle für Erweiterungen zur Verfügung haben! Den Befehlssatz kann man ja für andere Funktionen erweitern.
Peter (Ronny10)
Wie funktioniert das?
Als erstes muss man sich eine Art Befehlssatz ausdenken die der Master (ASURO) dem Slave schickt und die dieser dann ausführt.
Die header-Datei twi_register.h mit dem Befehlssatz:
#define sbi(port,bit) port|=(1<<(bit))
#define cbi(port,bit) port&=(~(1<<(bit)))
#define BIT(n) (1<<(n))
#define MAX_REG 255
#define FALSE 0
#define TRUE 1
/* ic2-slave-adresse */
#define TWI_ADR 0x80
/* set-befehle */
#define TWI_SET_DDRB 1 // definiere über das ddr eingänge und ausgänge von portb
#define TWI_SET_PORTB 2 // setze pull up widerstände an portb
#define TWI_SET_PINB 3 // setze ein einzelnes bit an portb
#define TWI_RES_PINB 4 // rücksetze ein einzelnes bit an portb
#define TWI_SET_DDRC 5 // wie portb
#define TWI_SET_PORTC 6
#define TWI_SET_PINC 7
#define TWI_RES_PINC 8
#define TWI_SET_DDRD 9 // wie portb
#define TWI_SET_PORTD 10
#define TWI_SET_PIND 11
#define TWI_RES_PIND 12
/* get-befehle */
#define TWI_GET_PINB 13 // lese das pinregister von portb
#define TWI_GET_PINC 14
#define TWI_GET_PIND 15
#define TWI_GET_ADC0 16 // lese adc0
#define TWI_GET_ADC1 17
#define TWI_GET_ADC2 18
#define TWI_GET_ADC3 19
Da gibt es Set- Res- und Get-Befehle. Set-Befehle zum setzen von Bits in den Datenrichtungs-Registern, von Ports oder einzelnen Port-Bits und natürlich auch zum zurücksetzen einzelner Bits (Res-Befehl). Mit den Get-Befehlen kann man den Zustand der Pinregister (digitale Eingänge) abfragen oder einen ADC-Kanal einlesen (2x8Bit) den man vorher mit einem Set-Befehl selektiert hat.
Ein Befehl besteht immer aus zwei Byte, dem Befehl und einem Parameter. Wenn ein Befehl keinen Parameter benötigt, z.B. TWI_GET_ADC0, müssen aber trotzdem zwei Byte gesendet werden. Dabei ist es egal welchen Wert der Parameter hat, da er ja nicht ausgewertet wird.
Das ist die I2C-Interrupt-Routine I2C_Slave.c für den ATmega8-Slave:
#include <avr/io.h>
#include <avr/interrupt.h>
#include <compat/twi.h>
#include "twi_register.h"
volatile unsigned char reg = MAX_REG;
volatile unsigned char twi_comm = FALSE;
void twi_init( unsigned char adr )
{
TWAR = adr;
TWCR = BIT(TWINT) | BIT(TWEN) | BIT(TWEA) | BIT(TWIE);
sei();
}
SIGNAL(SIG_2WIRE_SERIAL)
{
switch (TWSR) {
/* SLAVE RECEIVER */
case TW_SR_DATA_ACK:
case TW_SR_DATA_NACK:
if( reg < MAX_REG )
{
switch( reg )
{
case TWI_SET_DDRB: DDRB = TWDR;
break;
case TWI_SET_PORTB: PORTB = TWDR;
break;
case TWI_SET_PINB: sbi(PORTB, TWDR);
break;
case TWI_RES_PINB: cbi(PORTB, TWDR);
break;
case TWI_SET_DDRC: DDRC = TWDR;
break;
case TWI_SET_PORTC: PORTC = TWDR;
break;
case TWI_SET_PINC: sbi(PORTC, TWDR);
break;
case TWI_RES_PINC: cbi(PORTC, TWDR);
break;
case TWI_SET_DDRD: DDRD = TWDR;
break;
case TWI_SET_PORTD: PORTD = TWDR;
break;
case TWI_SET_PIND: sbi(PORTD, TWDR);
break;
case TWI_RES_PIND: cbi(PORTD, TWDR);
break;
case TWI_GET_ADC0: ADMUX = BIT(REFS0) + 0x00;
ADCSRA |= BIT(ADSC);
break;
case TWI_GET_ADC1: ADMUX = BIT(REFS0) + 0x01;
ADCSRA |= BIT(ADSC);
break;
case TWI_GET_ADC2: ADMUX = BIT(REFS0) + 0x02;
ADCSRA |= BIT(ADSC);
break;
case TWI_GET_ADC3: ADMUX = BIT(REFS0) + 0x03;
ADCSRA |= BIT(ADSC);
break;
}
reg = MAX_REG;
}
else
{
reg = TWDR;
twi_comm = reg;
}
TWCR = BIT(TWINT) | BIT(TWEN) | BIT(TWEA) | BIT(TWIE);
break;
/* SLAVE TRANSMITTER */
case TW_ST_SLA_ACK: // twi-adresse und read empfangen
switch( twi_comm )
{
case TWI_GET_PINB: TWDR = PINB;
break;
case TWI_GET_PINC: TWDR = PINC;
break;
case TWI_GET_PIND: TWDR = PIND;
break;
case TWI_GET_ADC0:
case TWI_GET_ADC1:
case TWI_GET_ADC2:
case TWI_GET_ADC3:
while (!(ADCSRA & BIT(ADIF)))
;
TWDR = ADCL;
break;
}
TWCR = BIT(TWINT) | BIT(TWEN) | BIT(TWEA) | BIT(TWIE); // transmit first byte
break;
case TW_ST_DATA_ACK: // erstes daten-byte des adc übertragen und ack empfangen
// sende jetzt das zweite daten-byte des adc
switch( twi_comm )
{
case TWI_GET_ADC0:
case TWI_GET_ADC1:
case TWI_GET_ADC2:
case TWI_GET_ADC3:
TWDR = ADCH;
}
TWCR = BIT(TWINT) | BIT(TWEN) | BIT(TWEA) | BIT(TWIE); // sende zweites byte
break;
default: TWCR = BIT(TWINT) | BIT(TWEN) | BIT(TWEA) | BIT(TWIE);
break;
}
}
Leider kommt das mit den Tabs beim kopieren nicht so 100%tig hin.
Das ist die Initialisierungsfunktion M8_I2C_Slave.c (main):
/*
autor: Peter Wilbert
version: 1.0
datum: 27.01.07
atmega8 als i2c-slave erweitert den asuro
um 4 adc's und 16 digitale i/o's
*/
#include <avr/io.h>
#include <avr/interrupt.h>
#include "twi_register.h"
extern void twi_init( unsigned char );
/* *** hauptprogramm ***
initialisiert den i2c-bus und verweilt dann in einer
endlosschleife. das eigentliche programm ist die twi-
interruptroutine in der die eingehenden slave-befehle
direkt ausgeführt werden
*/
int main(void)
{
/*
adc init
*/
ADCSRA = BIT(ADEN) + BIT(ADPS2) + BIT(ADPS1); // init adc prescale 64
ADMUX = BIT(REFS0) + 0x00; // adc einmal anstossen
ADCSRA |= BIT(ADSC); // start adc
while (!(ADCSRA & BIT(ADIF))) // warte bis ok
;
/*
twi initialisieren
*/
twi_init(TWI_ADR);
/*
for ever
*/
for(;;)
;
return(0);
}
Mit dieser Erweiterung sollte man erst einmal genügend I/O-Pins und ADC-Kanäle für Erweiterungen zur Verfügung haben! Den Befehlssatz kann man ja für andere Funktionen erweitern.
Peter (Ronny10)