I2C mit USI? - Ein Versuch

Mikro23

Aktives Mitglied
02. Jan. 2017
378
33
28
Großraum Hannover
Sprachen
  1. ANSI C
  2. Assembler
Ursprünglich wollte ich dieses USI ja ganz links liegenlassen und garnicht weiter beachten, aber nachdem die bit-banging-I2C-Funktionen hier ganz gut funktioniert haben, wollte ich mal rausfinden, ob das unter Einbeziehung vorhandener Hardware nicht noch einfacher und kleiner geht.

tl;dr: nö

Um nun nicht wieder bei Adam und Eva anzufangen, habe ich das mal nach dem zur AppNote 310* gehörigen C-Beispiel in Assembler nachgebaut.
Sieht vielleicht noch ein wenig unaufgeräumt aus und man kann sicher noch vieles verbessern. Ich habe bisher noch kein größeres Assemblerprogramm für AVRs geschrieben und kenne mich auch mit dem Zusammenspiel mit C nicht so gut aus. Die IO-Registerbezeichnungen aus der C-Headerdatei funktionieren in Assembler nicht problemlos, zumindest nicht für cbi, sbi, sbis, sbic, in und out. Außerdem habe ich wohl nicht ganz verstanden, warum es zwei um 0x20 verschiedene Adressen für ein und das selbe Register gibt und wann und warum und wofür der Compiler und der Assembler welche benutzt. Um nicht lange nach den Ursachen zu forschen, habe ich die problematischen Register einfach selbst (unter anderem Namen) definiert.

Es gibt zwei grundsätzlich unterschiedliche Herangehensweisen. Die eine aus der bit-banging-Version benutzt viele Funktionen. Neben init, stop und start gibt es noch repeatedStart, startWait, write, readAck und readNack. Die meisten davon müssen dann vom aufrufenden Programm benutzt werden.
Die andere aus der AN310 geht, ähnlich wie die xmega-Version von Atmel, über einen message-buffer, sodaß es neben init nur noch eine master-transceive-Funktion gibt. Das aufrufende Programm muß hier nur den buffer füllen und die transceive-Funktion aufrufen.
Möglicherweise liegt es an diesem etwas umständlicheren Weg über den buffer, daß diese Version mit ca. 200 Bytes (wenn man die Fehlerbehandlung ganz wegläßt) trotz benutzter Hardware noch größer als die bit-banging-Version mit 160 Bytes ist.

Da für 100 kHz ein delay von 4 – 5 µs gebraucht wird und bei F_CPU = 1 MHz ein Unterprogrammaufruf incl. return schon allein 7 µs braucht, ist das delay inline.
Bei einer höheren Taktfrequenz (min. 2 MHz, und für fast-I2C min. 8 MHz) könnte man durch Verwendung eines Unterprogramms für das delay nochmal gut 15 Bytes einsparen.

Im Beispiel kann während der Übertragung kein Interrupt auftreten. Ich habe nicht getestet, ob es mit Interrupts funktioniert. Sicherheitshalber würde ich sie – auch bei der bit-banging-Version – während der Übertragung lieber sperren.

Vielleicht hat ja mal jemand eine Idee eine kürzere USI-Version zu schreiben. Für mich vorerst das Fazit: Wenn der Controller echte I2C-Hardware hat, benutze ich sie. Wenn nicht, benutze ich die bit-banging-Version. Da kann man außerdem gegenüber der USI-Version die Pins völlig frei wählen.

*(jetzt: Atmel-2561-Using-the-USI-Module-as-a-I2C-Master_AP-Note_AVR310)
 
In der Form läuft es jetzt auf einem tiny84.
Die Kommentare habe ich übrigens weitgehend aus dem C-Beispiel der AN310 übernommen.


CodeBox Assembler
; extern uint8_t usi_error;

#include <avr/io.h>

#define USI_I2C_NO_ACK_ON_DATA        0x05 // The slave did not acknowledge  all data
#define USI_I2C_NO_ACK_ON_ADDRESS    0x06 // The slave did not acknowledge  the address

;***** Adapt these SCA and SCL port and pin definition to your target !!
;
#define PIN_USI_SDA        6        // SDA Port A, Pin 6  
#define PIN_USI_SCL        4        // SCL Port A, Pin 4
#define I2C_PORT        PORTA   // SDA Port A
#define I2C_PORT        PORTA   // SCL Port A        

;-- map the IO register back into the IO address space
#define DDR_USI        (_SFR_IO_ADDR(I2C_PORT) - 1)
#define PORT_USI    _SFR_IO_ADDR(I2C_PORT)
#define PIN_USI        (_SFR_IO_ADDR(I2C_PORT) - 2)

#define GPIO2        0x15
#define USIDR_        0x0F
#define USICR_        0x0D
#define USISR_        0x0E

; extern void usi_i2c_master_init(void)
.global usi_i2c_master_init
.func usi_i2c_master_init
usi_i2c_master_init:
    sbi PORT_USI, PIN_USI_SDA    ; Enable pullup on SDA to set high as released state
    sbi PORT_USI, PIN_USI_SCL    ; Enable pullup on SCL to set high as released state
    sbi DDR_USI, PIN_USI_SCL    ; Enable SCL as output
    sbi DDR_USI, PIN_USI_SDA    ; Enable SDA as output
    ser r24                        ; release level on SDA
    out USIDR_,r24
    ldi r24,0x2A                 ; Disable Interrupts, Set Two-wire mode, Software clock
    out USICR_,r24       
    ldi r24,0xF0                ; Clear USISIF, USIOIF, USIPF, USIDC & Counter
    out USISR_,r24
    ret
.endfunc

; inline delay for F_CPU = 1 MHz & F_I2C = 100 kHz

; extern uint8_t usi_i2c_master_transfer( uint8_t temp);
; temp = r24, return = r25(=0):r24   
.global usi_i2c_master_transfer
.func usi_i2c_master_transfer
usi_i2c_master_transfer:
    out USISR_,r24                ; Set USISR according to temp
1:    rjmp 2f                        ; delay 5 ṁs
2:    rjmp 3f
3:    nop
    ldi r24,0x2B
    out USICR_, r24                ; Generate positve SCL edge       
4:    sbis PIN_USI,PIN_USI_SCL
    rjmp 4b                        ; wait until SCL is high
    rjmp 5f                        ; delay 4 ṁs
5:    rjmp 6f
6:    out USICR_, r24                ; Generate negative SCL edge
    sbis USISR_,USIOIF            ; Check for transfer complete
    rjmp 1b
    rjmp 7f                        ; delay 5 ṁs
7:    rjmp 8f
8:    nop
    in r24,USIDR_                ; Read out data
    ser r25                        ; Release SDA
    out USIDR_,r25
    sbi DDR_USI,PIN_USI_SDA        ; Enable SDA as output
    ret
.endfunc

; extern uint8_t usi_i2c_master_stop(void)
; return = r24
.global usi_i2c_master_stop
.func usi_i2c_master_stop
usi_i2c_master_stop:
    cbi PORT_USI,PIN_USI_SDA    ; Pull SDA low
    sbi PORT_USI,PIN_USI_SCL    ; Release SCL
1:    sbis PIN_USI,PIN_USI_SCL   
    rjmp 1b                        ; wait until SCL is high
    rjmp 2f                        ; delay 4 ṁs
2:    rjmp 3f   
3:    sbi PORT_USI,PIN_USI_SDA    ; Release SDA
    rjmp 4f                        ; delay 5 ṁs
4:    rjmp 5f
5:    nop
    ldi r24,0x01                ; return true
    ret
.endfunc

; extern uint8_t usi_i2c_master_transceive( uint8_t *msg, uint8_t msgSize)
; GPIO2 bit0 = addressmode, bit1 = writemode
; &msg = r25:r24, msgsize = r22
.global usi_i2c_master_transceive
.func usi_i2c_master_transceive
usi_i2c_master_transceive:
        mov r18,r22                    ; r18 = msgsize
        sts usi_error,r1            ; clr error
        ldi r23,0x01                ; set addressmode and
        out GPIO2,r23                ;   clr writemode   
        movw r30,r24                ; &msg -> Z
        ldd r23,Z+0                    ; *msg -> r23
        sbrc r23,0                    ; skip if I2C_WRITE
        rjmp 1f
        sbi GPIO2,1                    ; set writemode
1:        sbi PORT_USI,PIN_USI_SCL    ; Release SCL
2:        sbis PIN_USI,PIN_USI_SCL
        rjmp 2b                        ; wait until SCL is high
        rjmp 3f                        ; delay 5 ṁs
3:        rjmp 4f
4:        nop
                                    ; generate start condition
        cbi PORT_USI,PIN_USI_SDA    ; Force SDA LOW
        rjmp 5f                        ; delay 4 ṁs
5:        rjmp 6f
6:        cbi PORT_USI,PIN_USI_SCL    ; Pull SCL LOW
        sbi PORT_USI,PIN_USI_SDA    ; Release SDA
        movw r20,r24                ; &msg -> r21:r20
        ser r19                        ; Load NACK to confirm End Of Transmission
send:    in r25,GPIO2
        andi r25,0x03                ; addressMode || WriteMode
        breq read
        cbi PORT_USI,PIN_USI_SCL    ; Pull SCL LOW
        movw r30,r20                ; &msg -> Z
        ldd r24,Z+0                    ; *msg -> r24
        out USIDR_,r24       
        ldi r24,0xF0                ; USISR_8bit Send 8 bits on bus
        rcall usi_i2c_master_transfer
        cbi DDR_USI,PIN_USI_SDA     ; Enable SDA as input
        ldi r24,0xFE                ; USISR_1bit receive 1 bit on bus
        rcall usi_i2c_master_transfer
        sbrs r24,0                    ; Skip if I2C_NACK_BIT
        rjmp clradr
        ldi r24,0x00                ; return FALSE
        sbis GPIO2,0                ; skip if addressmode
        rjmp noack
        ldi r25,USI_I2C_NO_ACK_ON_ADDRESS
        sts usi_error,r25   
        ret
noack:    ldi r25,USI_I2C_NO_ACK_ON_DATA   
        sts usi_error,r25
        ret
clradr:    cbi GPIO2,0                    ; clr addressmode, address transmission only once
        rjmp next
read:    cbi DDR_USI,PIN_USI_SDA        ; Enable SDA as input
        ldi r24,0xF0                ; USISR_8bit receive 8 bits on bus
        rcall usi_i2c_master_transfer
        movw r30,r20                ; &msg -> Z
        std Z+0,R24                    ; r24 -> *msg
        cpi r18,0x01                ; If transmission of last byte was performed
        brne ack
        out USIDR_,r19                ; Load NACK to confirm End Of Transmission
        rjmp nack
ack:    out USIDR_,r1                ; Load ACK, Set datareg bit 7 (output for SDA) low
nack:    ldi r24,0xFE                ; USISR_1bit  Generate ACK/NACK
        rcall usi_i2c_master_transfer
next:    subi r18,0x01                ; --msgSize
        subi r20,0xFF                ; &msg + 1
        sbci r21,0xFF
        cpse r18,r1                    ; skip if msgSize == 0
        rjmp send
        rcall usi_i2c_master_stop
        ldi r24,0x01                ; return TRUE
        ret
.endfunc



Edit: Tabs versteht das Forum immer noch nicht richtig. Im Original sind alle mnemonics und alle Kommentare untereinander.
 
Hier noch die Header-Datei, um die Funktion in C einzubinden.


CodeBox C
#ifndef _USI_I2C_MASTER_H
#define _USI_I2C_MASTER_H

#ifndef F_CPU
#define F_CPU 1000000
#endif
#include <avr/io.h>
/****************************************************************************
  Bit and byte definitions
****************************************************************************/
#define I2C_READ        1
#define I2C_WRITE        0

#define USI_I2C_NO_ACK_ON_DATA        0x05 // The slave did not acknowledge  all data
#define USI_I2C_NO_ACK_ON_ADDRESS    0x06 // The slave did not acknowledge  the address

//********** Prototypes **********//

void usi_i2c_master_init(void);
uint8_t usi_i2c_master_transceive(uint8_t *, uint8_t);

#endif
 
Zum Testen der USI-Funktion startet das Hauptprogramm eine realtimeclock und den watchdog interrupt, der alle halbe Sekunde die Sekunden und Minuten der RTC ausliest und auf einem LCD anzeigt.


CodeBox C
#include "main.h"

uint8_t usi_error = 0;
uint8_t messageBuf[MESSAGEBUF_SIZE];

ISR(WATCHDOG_vect)
{
    // dummy-ISR zum Aufwecken des Controllers
}
void start_rtc(void)
{
    messageBuf[0] = DS1307 | I2C_WRITE;
    messageBuf[1] = 0;            // address
    messageBuf[2] = 0x00;        // start RTC
    usi_i2c_master_transceive(messageBuf, 3);
}
uint8_t read_rtc(uint8_t adr)
{
    messageBuf[0] = DS1307 | I2C_WRITE;
    messageBuf[1] = adr;
    usi_i2c_master_transceive(messageBuf, 2);
    messageBuf[0] = DS1307 | I2C_READ;
    do
        usi_i2c_master_transceive(messageBuf, 2);
    while (usi_error == USI_I2C_NO_ACK_ON_ADDRESS);
    return messageBuf[1];
}

int main(void)
{
    uint8_t sec = 0, min = 0;
    
    DDRA = LEDRT | LEDGN | CS;    // 0b10000011
    PORTA = CS;                    // 0b10000000
    DDRB = CLK | SI | RS;        // 0b00000111
    PRR = (1<<PRTIM1) | (1<<PRTIM0) | (1<<PRADC);
    
    lcd_init();
    xfunc_out = lcd_putc;        // Ausgabefunktion initialisieren

    usi_i2c_master_init();
    start_rtc();

    set_sleep_mode(1<<SM1);        // sleep mode power down
    
    wdt_enable(WDTO_500MS);        // watchdog auf 500 ms
    WDTCSR |= (1<<WDIE);        // wdt interrupt enable
    
    sei();
    
    FOREVER {
        sleep_mode();
        wdt_reset();
        WDTCSR |= (1<<WDIE);    // re-enable wdt-interrupt
        PINA |= LEDGN;
        sec = read_rtc(0);
        min = read_rtc(1);
        lcd_cursor(0, 6);
        xprintf(PSTR("%02X"),min);
        xputs(PSTR(":"));
        xprintf(PSTR("%02X"),sec);
        if (MCUSR & (1<<WDRF)) {
            lcd_cursor(1, 0);
            xputs(PSTR("WDR"));    // anzeigen ob ein watchdog-reset aufgetreten ist
        }
    }
}
 
main.h

CodeBox C
#ifndef _MAIN_H
#define _MAIN_H

#define F_CPU        1000000UL

#include <avr/io.h>
#include <avr/interrupt.h>
#include <avr/sleep.h>
#include <util/delay.h>
#include <avr/wdt.h>
#include "DisplayDOGM163-5VSPI.h"
#include "xitoa.h"
#include "USI-I2C.h"

#define FOREVER        for(;;)

#define MESSAGEBUF_SIZE    4
#define DS1307        0xD0

#define LEDRT        0x01    // 0b00000001 PA0
#define LEDGN        0x02    // 0b00000010 PA1

#define CLK            0x01    // 0b00000001 PB0
#define SI            0x02    // 0b00000010 PB1
#define RS            0x04    // 0b00000100 PB2
#define CS            0x80    // 0b10000000 PA7

#endif
 
:hmmmm:Hab grad nicht genug Zeit, das alles durchzugehen, aber kurz zu Deinen "Fragen":
Außerdem habe ich wohl nicht ganz verstanden[*], warum es zwei um 0x20 verschiedene Adressen für ein und das selbe Register gibt und wann und warum und wofür der Compiler und der Assembler welche benutzt. Um nicht lange nach den Ursachen zu forschen, habe ich die problematischen Register einfach selbst (unter anderem Namen) definiert.
Für den Zugriff auf die "normalen" 64 I/O-Register gibt es die Instruktionen In und Out, die eben genau diese 64 Register Adressieren können.
Der Opcode für In lautet:
10110AAdddddAAAA
Die fünf d-Bits verschlüsseln das verwendete der 32 Rechenregister, die sechs A-Bits das adressierte der 64 I/O-Register.
Out ist analog dazu 10111AAdddddAAAA.
Beides sind Ein-Takt Instruktionen.

Grundsätzlich kann auf den gesamten I/O-Space aber auch wie auf SRAM zugegriffen werden, also mit LD und ST (in allen Variationen inklusive LDS und STS). Dort werden allerdings zuerst die 32 Rechenregister adressiert (Adressen 0x00..0x1F), dann die 64-Standart-I/O-Register (0x20..0x5F), danach eventuelle Extended-I/Os, und dann der eigentliche SRAM.
Deswegen der Offset um die 0x20 Adressen (Rechenregister).

Die ersten 32 I/O-Register sind sogar direct Bit accessible...
cbi, sbi, sbis, sbic
Du kannst also ohne Umweg über ein Rechenregister und ohne Manipulation des Statusregisters einzelne Bits direkt setzen oder löschen beziehungsweise durch diese bedingt verzweigen (Stichwort: ISR ohne sichern und wiederherstellen von Rechen- und Statusregister).

[*] aber eigentlich ist das doch für Dich nichts neues...
:hmmmm:
Mir war irgendwo im Zusammenhang mit C und dort eingebundenen ASM-Dateien aufgefallen, daß der Offset im … ähm... "C-Assembler" genau andersrum ist.

Also im "normalen" AVR-Assembler sind für die konventionellen I/Os die Adressen für die Verwendung mit IN/OUT bzw SBI/CBI/SBIS/SBIC angelegt, alle nach den 64 haben dann logischerweise den Rechenregister-Offset für LD(S)/ST(S) drin. Will man hier aus irgendwelchen Gründen auf die untersten I/Os wie auf SRAM zugreifen, muß man selbst den Offset dazuaddieren.

Beim "C-Assembler" hingegen ist immer der Offset drauf, hier muß also bei Verwendung von IN, OUT usw der Offset abgezogen werden.

Edit: hier...
 
Zuletzt bearbeitet:
Ja, das ist mir im Prinzip schon klar.

Was ich nicht nachvollziehen konnte war folgendes:
In der zugehörigen Headerdatei io.h (in diesem Fall iotn84a.h (sucht der Compiler selbst raus, nach dem Controllertyp, den man ja beim Anlegen des Projekts auswählen muß)) steht:


CodeBox C
#define GPIOR2    _SFR_IO8(0x15)
Wenn ich im Assemblerquellcode sbi GPIOR2,0 schreibe, steht im Disassemblerlisting (Programm mit Simulator gestartet) sbi 0x15,0 aber das Bit landet in einem anderen Register und dann hatte ich keine Lust mehr das weiter zu untersuchen und habe die betreffenden Registernamen kurzerhand ohne die _SFR… neu definiert.

Komischerweise haben die paar Zeilen, die ich aus der bit-banging-Version von Peter Fleury leicht verändert übernommen habe, funktioniert:

CodeBox C
#define I2C_PORT    PORTA
#define PORT_USI    _SFR_IO_ADDR(I2C_PORT)
#define DDR_USI    (_SFR_IO_ADDR(I2C_PORT) - 1)
etc.
und als ich das ganze mit GPIO2 bzw. GPIOR2 versuchte, ging es nicht, warum auch immer.

Der C-Compiler verbirgt das so, daß man sich darum nicht kümmern muß und der Assembler alleine macht das offenbar auch richtig. Nur wenn man beides mischt, muß man anscheinend noch irgendwas beachten oder einstellen, was ich übersehen habe. Bei den Beispielen die ich damals gebracht habe, hat es wohl nur funktioniert, weil die besagten Befehle dabei nicht vorkamen.
 
Schau mal in sfr_defs.h.

Dort sind einige Erläuterungen, vielleicht hilft dir das etwas weiter.



CodeBox C
#define _MMIO_BYTE(mem_addr) (*(volatile uint8_t *)(mem_addr))
#define _SFR_IO8(io_addr) _MMIO_BYTE((io_addr) + __SFR_OFFSET)

#define _SFR_MEM_ADDR(sfr) ((uint16_t) &(sfr))
#define _SFR_IO_ADDR(sfr) (_SFR_MEM_ADDR(sfr) - __SFR_OFFSET)
 
  • Like
Reaktionen: Mikro23
Danke, das hatte ich noch nicht gesehen. Gleich mal ausprobieren…
 
Die Datei sfr_defs.h ist garnicht so leicht zu finden. Im Atmel Studio rechte Maustaste: Goto Implementation funktioniert nicht. Nur über die Windows-Suche im Atmel-Programmordner.
 
So jetzt geht‘s. Alle GPIOR2 durch _SFR_IO_ADDR(GPIOR2) ersetzt, dito bei den USI-Registern.

Sieht nicht schön aus, und warum es so funktioniert habe ich immer noch nicht ganz verstanden, aber ich werde mir die sfr_defs und Erklärungen noch mal in Ruhe durchlesen.
 
Nachdem du kompiliert hast, müsste die Datei sfr_defs.h auch im Solution Explorer unter Dependencies gelistet sein.
 
  • Like
Reaktionen: Mikro23
Stimmt. Mit der Betonung auf nach dem Kompilieren. ;)
Dann kann man sie sogar mit der rechten Maustaste mit Goto Implementation aufrufen.
 

Über uns

  • Makerconnect ist ein Forum, welches wir ausschließlich für einen Gedankenaustausch und als Diskussionsplattform für Interessierte bereitstellen, welche sich privat, durch das Studium oder beruflich mit Mikrocontroller- und Kleinstrechnersystemen beschäftigen wollen oder müssen ;-)
  • Dirk
  • Du bist noch kein Mitglied in unserer freundlichen Community? Werde Teil von uns und registriere dich in unserem Forum.
  •  Registriere dich

User Menu

 Kaffeezeit

  • Wir arbeiten hart daran sicherzustellen, dass unser Forum permanent online und schnell erreichbar ist, unsere Forensoftware auf dem aktuellsten Stand ist und der Server regelmäßig gewartet wird. Auch die Themen Datensicherheit und Datenschutz sind uns wichtig und hier sind wir auch ständig aktiv. Alles in allem, sorgen wir uns darum, dass alles Drumherum stimmt :-)

    Dir gefällt das Forum und unsere Arbeit und du möchtest uns unterstützen? Unterstütze uns durch deine Premium-Mitgliedschaft!
    Wir freuen uns auch über eine Spende für unsere Kaffeekasse :-)
    Vielen Dank! :ciao:


     Spende uns! (Paypal)