Im Zusammenhang für das Projekt "RN-Netzwerk", wo es ja darum geht, PC und Avr zu verbinden, ist ja natürlich auch eine Integration des I2C-Bus unbedingt erforderlich. Da ich mit einer RNBFRA 1.2 Karte gesegnet bin, sollte natürlich gerade der AT90S2313 (der Co-Prozessor) auch zum Mitspieler werden. Das bisherige "durchrouten" über die RS232 ist ja doch nicht sehr befriedigend.
Über die Therorie und Praxis des I2C bzw. TWI -Bus gibt es hervorragende Artikel in RN-Wissen. Bitte dort nachzulesen, wenn irgendetwas unklar ist.
Und nochwas: Ich betrachte das als "Open-Source" Projekt. Wenn also jemand Hand anlegen möchte oder sonstwelche Ideen dazu hat, bitte um Rückmeldungen. Aber immer auch so, dass alle was davon haben. Also eventuell mit "Change-History" und Kommentaren.
Inhaltsverzeichnis
Bascom Software I2C Routinen
Das Ziel ist:
- Kommunikation auf der PC-Plattform via IP (Stream-Sockets),
- vom PC zum Atmega32 über COMx und AVR-UART,
- und auf der Mikrocontroller-Ebene dann über I2C, (später ev. auch andere Busse)
- ausserdem soll das Ganze natürlich bei einer ansprechenden Bus-Geschwindigkeit funktionieren. Getestet hab ich die Soft-Routinen mit 400 kHz, was bei Multimaster-Betrieb auch recht günstig ist, da ja dann der Bus für einen Message weniger lange belegt ist (ergo weniger Konflikte).
I2C auf AT90S2313
In diesem Prozessor (und auch auf dem Tiny2313) gibt's keine Hardwarunterstützung. Und in einer Multimaster-Umgebung war auch mit der I2C.LIB von Bascom nichts zu machen. Denn gerade bei I2CSTART kümmert sich Bascom in keiner Weise darum, ob auf dem Bus gerade irgendwas abläuft. Und dann war ja immer noch das Problem, daß es keine SLAVE-Funktionen gibt (Nur um Geld).
Step 1: Multimasterfähige I2c-Funktionen
Es genügte fürs Erste,
- die Start-Routine durch eine multimasterfähige Funktion zu ersetzen,
$external I2cwaitbus 'wait for free bus & reserve it
- und das Adressen- oder Bytesenden durch eine Funktion, die auch die Arbitrierung kontrolliert.
$external I2cwmaster 'send byte & watch arbitration
- I2crbyte u. I2cstop kann man eigentlich im Moment lassen, da sie nichts böses tun.
Step 2: I2c-Slave-Funktionen
Da gibt es eigentlich nur drei Funktionen:
- warten auf eine Startbedingung und lesen der I2C-Adresse
$external I2c_get_addr 'wait for start-cond & read in I2C address
Und, je nachdem, ob Read oder Write
- Bytes vom Master empfangen
$external I2c_slave_rx_data 'ACK & receive master data
- Bytes zum Master senden
$external I2c_slave_tx_byte 'ACK & send data to master
I2c_get_addr
Ich habe aus mehreren Gründen darauf verzichtet, die Adress-Prüfung zu automatisieren, wie das ja sonst üblich ist:
- Ich möchte in weiterer Folge die Funktionen auch als eine Art I2C-Monitor/Logger verwenden
- Ich seh' nicht ein, warum ein Slave immer sowohl Read als auch Write zulassen soll
- Und ich wollt mir auch die Parametriesierung ersparen, ob auch General-Calls oder nicht akzeptiert werden sollen. Irgendeine Art von "IF" muss der User ja sowieso machen, also soll er auch gleich selbst entscheiden, was er "acked" oder nicht.
d.h. also, wenn die Funktion zurückkommt, ist ein I2C-Start erfolgt und eine Adresse ist gesendet worden. Die steht dann vollständig in Feld "I2C_ADDR"
- NULL ist eine General Call Adresse
- SLA+W ist eine "Slave-Write" Adresse
- SLA+R ist eine "Slave-Read" Adresse
Welche SLA-Adresse seine eigene ist, kann der User nun selbst entscheiden.
I2c_slave_rx_data
wird das aufgerufen, wird ein ACK ausgesendet und es werden Daten vom Master empfangen und in den Buffer geschrieben, der im Feld "I2c_write" angegeben ist. Wieviel das sind, entscheidet der Master durch "STOP" oder "Repeated Start". Einen Begrenzung seitens der Funktion ist nicht vorgesehen, da sich ja die Applikationen im Master und im Slave sowieso irgendwie vereinbart haben müssen, wieviele Bytes wann zu senden sind.
Ist die Routine fertig, steht die Anzahl der empfangenen Bytes im Feld "I2c_cntr"
I2c_slave_tx_byte
wird das aufgerufen, wird ein ACK ausgesendet und es werden die Daten zum Master gesendet, die im Feld "I2c_read" angegeben sind. Beendet wird das vom Master duch ein "NACK". Einen Begrenzung der Anzahl durch die Funktion ist auch hier nicht vorgesehen.
Ist die Routine fertig, steht die Anzahl der gesendeten Bytes im Feld "I2c_cntr", falls das den User interessieren sollte.
DEMO MYI2C.ZIP
Das Demo-Set Bascom_Soft-I2c_Library#Web_Links kann heruntergeladen werden, besteht aus drei Teilen
- 2313.bas Demo Programm
- myi2c.bas Include File
- myi2c.lib eine Bascom library, die gehört zu den anderen Bascom Libraries, irgendwo im Installations-directory.
Beispiel 2313.bas
Das Demo-Programm macht eigentlich nix ausser Bytes zu empfangen oder zu senden. Wurde was empfangen und es ist auch gerade eine Sekunde 'rum, sendet er selbst an das RNBFRA Out-Port (PCF) das erste Byte, das im Empfangsbuffer steht. Damit sind die I2C Routinen prinzipiell gut zu testen.
Das vollständige Programm ist in der Zip-File. Das Wesentliche ist folgendes:
$crystal = 4000000 ' Quarzfrequenz $hwstack = 48 I2cinit ' normale Bascom Funktion I2c_write = Varptr(m32_byte(1)); ' Data receive-Buffer I2c_read = Varptr(m32_byte(1)); ' Data send-Buffer Do Loadadr I2c_flag , Z Gosub I2c_get_addr ' I2C Address read Select Case I2c_addr Case Co1_adr: Loadadr I2c_flag , Z Gosub I2c_slave_rx_data ' daten empfangen bis Stop/Rep Sent_flag = 1 '------------------------------------ ' WORKOUT Message '------------------------------------ Case Co1_adrr: Loadadr I2c_flag , Z Gosub I2c_slave_tx_byte ' daten senden bis NAK Case Else I2cinit 'Bus freigeben End Select If Sent_flag = 1 And Timeout = 1 Then Sent_flag = 0 Timeout = 0 Gosub Out_transmit 'senden an PCF End If Loop End '----------------------------------------------------- ' '----------------------------------------------------- Out_transmit: Do Gosub I2cwaitbus ' wait for Bus $asm ldi r17, out_adr !Call I2cwmaster ' send SLA+W & arbitr.Check $end Asm If Err = 0 Then Loadadr M32_byte(1) , X $asm ld r17, x+ !Call I2cwmaster ' send data & arbitr.Check $end Asm End If If Err = 0 Then I2cstop End If Loop Until Err = 0 I2cinit Return
Anmerkung: Das Senden muß eventuell mehrmals wiederholt werden. Auf einem Multimaster-Bus ist es ja nicht gesagt, daß nicht schon mal der Bus verloren gehen kann, weil ein anderer dazwischenfunkt.
Include-File MyI2c.bas
'---------------------------------------------- ' MyI2C.BAS ' I2c Soft-functions '---------------------------------------------- ' PicNick was here www.roboternetz.de '---------------------------------------------- '--------------------------- Library und definitionen ------------------ $lib "MyI2c.LIB" '--------------------------- Slave receive & Send --------------------- Const I2c_m_start = 1 $external I2c_get_addr 'wait for start-cond & read in I2C address Declare Sub I2c_get_addr $external I2c_slave_rx_data 'ACK & receive master data Declare Sub I2c_slave_rx_data $external I2c_slave_tx_byte 'ACK & send data to master Declare Sub I2c_slave_tx_byte '--------------------------- Multimaster sending --------------------- $external I2cwaitbus 'wait for free bus & reserve it Declare Sub I2cwaitbus $external I2cwmaster 'send byte & watch arbitration Declare Sub I2cwmaster '------------------------------------- ' I2C-STRUCTURE '------------------------------------- Dim I2c_flag As Byte '0 Kontroll-flags Dim I2c_addr As Byte '1 slave adresse Dim I2c_cntr As Byte '2 bei empfang anzahl bytes Dim I2c_write As Word '3/4 adresse empfangsbuffer Dim I2c_read As Word '5/6 adresse sendebuffer '--------------------------------------------- ' '--------------------------------------------- I2cinit
Bascom Library Myi2c.lib
Slave Receiver
Bei einer Busgeschwindigkeit von 400 kHz dauert ein Takt (1 Bit) gerade mal 2.5 µS, d.h. um SCL High oder Low zu erkennen, stehen 1.25 µS zur Verfügung. Da macht der AT90S2313 bei 4 MHZ gerade mal 5 Zyklen. Das geht sich nur aus, wenn man den Master durch "Clock-Stretching" synchronisiert. Da bei 400 kHz das Übertragen von 8 Bit nur 20 µS dauert, werden auch in dieser Zeit alle Interrupts disabled.
Zum Byte-Lesen gibt es zwei Varianten:
- I2C Adresse lesen (8-Bit) und in das Feld I2C_addr schreiben. Vor dem Return wird der Bus angehalten.
Das Hauptprogramm kann nun die Adresse prüfen auf
- Eigene Adresse und WRITE
- Eigene Adresse und READ
- GCA (Adresse=0)
- Weder noch.
Wenn das Hauptprogramm den Aufruf nicht annehmen kann oder will, wird einfach durch "I2CINIT" der Bus wieder freigegeben.
- Datenbytes lesen. Dabei wird aber auch auf STOP oder REPEATED-START geprüft. Das ist gleichzeitig das Zeichen, dass keine Bytes mehr zu empfangen sind. Sonst wird jedes Byte "acked" und in den Buffer geschrieben.
[I2c_slave_tx_ack] I2c_slave_tx_ack: * sbi _sdaDDR,_sda ;Hold SDA LO (ACK) Wt_ack_scl: * in r1, SREG ; save SREG cli ; disable interrupts * cbi _sclDDR,_scl ;SCL Release Wt_scl_hi2: * sbis _sclPIN,_scl ;SCL rjmp wt_scl_hi2 ;wait SCL hi Wt_scl_lo3: * sbic _sclPIN,_scl ;SCL rjmp wt_scl_lo3 ;wait SCL lo * sbi _sclDDR,_scl ;SCL Stretch * cbi _sdaDDR,_sda ;release SDA * out SREG, R1 ret [end] ;----------------------------------------------------- [I2c_slave_tx_nak] I2c_slave_tx_nak: $external I2c_slave_tx_ack * cbi _sdaDDR,_sda ;Release SDA rjmp Wt_ack_scl [end] ;----------------------------------------------------- [I2c_release] I2c_release: * cbi _sdaDDR,_sda ;SDA Release * cbi _sclDDR,_scl ;SCL Release clr r22 std Z + 0, r22 ;clear flag ret [end] ;----------------------------------------------------- [I2c_wait_start] I2c_wait_start: ldi r22, 0 std Z + 0, r22 ; clear I2c_flag Wt_hi_hi: * sbis _sclPIN,_scl ; SCL ? rjmp wt_hi_hi ; wait SCL hi * sbis _sdaPIN,_sda ; SDA ? rjmp wt_hi_hi ; wait SDA hi Wt_sda_lo0: * sbic _sdaPIN,_sda ; SDA rjmp wt_sda_lo0 ; wait SDA lo * sbis _sclPIN,_scl ; SCL start condition ? rjmp wt_hi_hi ; no, repeat the whole sequence Wt_scl_lo0: * sbic _sclPIN,_scl ; SCL rjmp wt_scl_lo0 ; wait SCL lo * sbi _sclDDR,_scl ; SCL Stretch ret [end] ;----------------------------------------------------- [I2c_get_addr] I2c_get_addr: $external I2c_wait_start ldd r22, Z + 0 ; i2c_flag * cpi r22, I2c_m_Start breq rept_s ; start-cond. already set rcall I2c_wait_start ; wait for Bus-start rept_s: * clr r22 std Z + 0, r22 ;clear I2c_flag ldi r24 ,1 ;set 'end'-Bit * in r1, SREG ;save SREG cli ;disable interrupts clc ;clear carry Wta_scl_hi: * cbi _sclDDR,_scl ;SCL Release Wta_scl_hi1: * sbis _sclPIN,_scl ;SCL rjmp wta_scl_hi1 ;wait SCL hi * sbic _sdaPIN,_sda ;SDA = 0 ? sec ;SDA = 1 -> set carry Wta_scl_lo: * sbic _sclPIN,_scl ;SCL rjmp wta_scl_lo ;wait SCL lo * sbi _sclDDR,_scl ;SCL Stretch rol r24 ;roll carry in/out brcc Wta_scl_hi ;Bit loop std Z + 1, r24 ;Store i2c address * out SREG, r1 ret [end] ;----------------------------------------------------- [I2c_slave_rx_data] I2c_slave_rx_data: ldd xl, Z + 3 ; Load Buffer Address ldd xh, Z + 4 ; clr r23 std Z + 2, r23 ; clear counter I2c_read_loop: $external I2c_slave_tx_ack rcall I2c_slave_tx_ack ; Send ACK * in r1, SREG ; save SREG cli ; disable interrupts clt ; clear T-Bit rcall I2c_read_byte ; read 8 Bit brtc Rd_sto_ack ; T-Bit clear ? o.k.--> * out SREG, r1 std Z + 2, r23 ; store counter std z + 0, r22 ; set start/stop/none flag ret Rd_sto_ack: * out SREG, r1 st x+, r24 ; store character inc r23 ; data counter++ rjmp I2c_read_loop ; no, cont'd next byte '------------------------------------------- I2c_read_byte: ldi r24 ,1 ;set end-bit clc ;clear carry Rd_loop: * cbi _sclDDR,_scl ;SCL Release Wt_scl_hi: * sbis _sclPIN,_scl ; SCL rjmp wt_scl_hi ; wait SCL hi * sbis _sdaPIN,_sda ; SDA ? rjmp Wt_chk_0 ; SDA Low-Check sec ; SDA Hi-Check Wt_chk_1: * sbis _sclPIN,_scl ; SCL rjmp wt_ok_x ; scl low---> ok, next bit * sbic _sdaPIN,_sda ; SCL Hi & SDA lo --> repeated Start rjmp wt_chk_1 ; SCL & SDA Hi --> cont'd wait Rep_start_cond: * sbic _sclPIN,_scl ; SCL rjmp Rep_start_cond ; wait SCL lo * sbi _sclDDR,_scl ; SCL STRETCH SET ; set t-Bit * ldi r22, I2c_m_Start ; signal REP-Start ret Wt_chk_0: ; SDA is Low * sbis _sclPIN,_scl ; SCL ? rjmp wt_ok_x ; scl low---> ok, next bit * sbis _sdaPIN,_sda ; SDA ? rjmp wt_chk_0 ; SCL Hi & SDA lo --> cont'd wait Stop_cond: : stop-condition SET ; set t-bit ret ; xit Wt_ok_x: * sbi _sclDDR,_scl ; STRETCH rol r24 ; roll carry in/out brcc rd_loop ; next bit ret ; byte done [end]
Slave Transmitter
Hier erwartet der Slave, daß der Master beim letzten Byte ein "NACK" zurücksendet, denn eine STOP/REPEATED START-Bedingung kann da nicht vom Master kommen, da ja der Slave auf der SDA-Line "sitzt". Wie auch immer, die Funktion prüft trotzdem, ob jedes von ihr gesetzte HIGH auf der SDA Line auch wirklich kommt. Wenn nicht, wird abgebrochen.
;----------------------------------------------------- [I2c_slave_tx_byte] I2c_slave_tx_byte: $external I2c_slave_tx_ack rcall I2c_slave_tx_ack ; Send ACK ldd xl, Z + 5 ;Load Buffer Address ldd xh, Z + 6 ; * in r1, SREG ; save SREG cli ; disable interrupts rcall Tx_loop * out SREG, r1 ret ;-------------------------------------------------- Tx_loop: ld r24, x+ ; next character ldi r25, 8 ; 8 Bit Tx_c_loop: Sbrc R24 , 7 ;Data Bit ? rjmp Tx_Bit_1 ;set SDA High ;-------------------------------------------------- ;---------------- send NULL------------------------ * sbi _sdaDDR,_sda ;set SDA Low * cbi _sclDDR,_scl ;SCL Release-------------------- Tx0_scl_hi_0: * sbis _sclPIN,_scl ;SCL rjmp Tx0_scl_hi_0 ;wait scl hi nop nop Tx0_scl_lo_0: * sbic _sclPIN,_scl ;SCL rjmp Tx0_scl_lo_0 ;wait scl lo * sbi _sclDDR,_scl ;SCL Stretch rjmp Tx_step ;-------------------------------------------------- ;---------------- send HIGH------------------------ Tx_bit_1: * cbi _sdaDDR,_sda ;SDA Release High * cbi _sclDDR,_scl ;SCL Release Tx1_scl_hi_0: * sbis _sclPIN,_scl ;SCL rjmp Tx1_scl_hi_0 ;wait scl hi * sbis _sdaPIN,_sda ;SDA HI ? ret ;Bus Lost , anyhow Tx1_scl_lo_0: * sbic _sclPIN,_scl ;SCL rjmp Tx1_scl_lo_0 ;wait scl lo * sbi _sclDDR,_scl ;SCL Stretch Tx_step: rol r24 ;roll carry in/out dec r25 ;count bits brne Tx_c_loop ;next bit ;-------------------------------------------------- ;---------------- get ACK/NAK --------------------- * cbi _sdaDDR,_sda ;SDA Release * cbi _sclDDR,_scl ;SCL Release Tx_scl_hi_1: ;get ack bit * sbis _sclPIN,_scl ;SCL rjmp Tx_scl_hi_1 ;wait scl hi * sbic _sdaPIN,_sda ;SDA (ACK) ? ret ;no-ack --> return Tx_scl_lo_1: * sbic _sclPIN,_scl ;SCL rjmp Tx_scl_lo_1 ;wait scl lo * sbi _sclDDR,_scl ;SCL Stretch rjmp tx_loop ; next [end]
Master
Wait Bus & Start
Die Funktion prüft, ob SCL und SDA High sind. Wenn das wenigstens 15 mal hintereinander gelingt, ist ziemlich sicher der Bus gerade frei. Dann wird die Start-Bedingung gesetzt. Nun sollte keiner am Bus mehr dazwischenfunken. Trotzdem kann das passieren, deswegen dann beim Senden die Arbitrierungs-Prüfung.
Die Pause-Funktion "_i2c_hp_delay" ist übrigens die, die Bascom aus "Config I2cdelay=" generiert.
;--------------------------------------------------------- ; MASTER Functions ;--------------------------------------------------------- ; Wait for free Bus ;--------------------------------------------------------- [I2cwaitbus] I2cwaitbus: $EXTERNAL _I2C * in r1, SREG ; save SREG cli ; disable interrupts Ck_1_x: LDI r23,0x0f ; 15 times Ck_1_y: * sbis _sclPIN,_scl ; SCL RJMP Ck_1_x ; wait SCL Hi * sbis _sdaPIN,_sda ; SDA HI ? RJMP Ck_1_x ; wait SDA Hi DEC r23 BRNE Ck_1_y ; (8) hang on * sbi _sdaDDR,_sda ; SDA down for Start * out SREG, r1 Rjmp _i2c_hp_delay ; half period delay [end]
Send Byte & Arbitration Check
Auch hier werden die Interrupts disabled, damit nicht ev. auf dem Bus ein Stillstand eintritt, den ein anderer als "Bus-ist-frei" interpretieren kann. Bei jedem gesetzen HIGH auf der SDA-Line wird geprüft, ob nicht jemand anders irgendwie doch ein LOW erzwingt. Ist das so, gilt der Bus als "LOST" und ein Fehlerzeichen "ERR=1" wird gesetzt. Das Hauptprogramm kann (muß) seinen Sendeversuch dann wiederholen. Das gilt natürlich auch, wenn kein "ACK" kommt.
;--------------------------------------------------------- ; Send one Byte R17 & check Arbitration ;--------------------------------------------------------- [I2cwmaster] I2cwmaster: $EXTERNAL _I2C * in r1, SREG ; save SREG cli ; disable interrupts Sec ; set carry flag Rol r17 ; shift in carry and out bit one rcall I2cwmaster_loop * out SREG, r1 ret I2cwmaster_loop: * sbi _sclDDR,_scl ;SCL DOWN BRCC I2cwmaster_low ;carry clear --> send Low ; send High ------------------------------------------ NOP ; filler * cbi _sdaDDR,_sda ; release SDA RCALL _i2c_hp_delay ; wait * cbi _sclDDR,_scl ; SCL Release Tx_1_ck: * sbis _sclPIN,_scl ; SCL RJMP Tx_1_ck ; wait SCL Hi * sbis _sdaPIN,_sda ; SDA still HI ? RJMP Tx_lost ; No, ---> arbitration lost Tx_1_ok: RCALL _i2c_hp_delay ;wait LSL r17 ;2^^7 -->Cy brne I2cwmaster_loop ;more bits ? ;--------------------------------------------------- I2cwmaster_loop_x: ; Get ACK / NACK * sbi _sclDDR,_scl ; SCL DOWN * cbi _sdaDDR,_sda ; release SDA RCALL _i2c_hp_delay ; wait * cbi _sclDDR,_scl ; SCL Release Tx_x_ck: * sbis _sclPIN,_scl ; SCL RJMP Tx_x_ck ; wait Hi CLT ; clear t-bit * sbic _sdaPIN,_sda ; SDA = ACK/NAK ? Tx_lost: SET ; set t-bit BLD r6,2 ; t-Bit -> to Bascom "ERR" RJMP _i2c_hp_delay ; wait & return ' send Low ------------------------------------------ I2cwmaster_low: * sbi _sdaDDR,_sda ;down SDA RCALL _i2c_hp_delay ;wait * cbi _sclDDR,_scl ;SCL Release Tx_0_ck: * sbis _sclPIN,_scl ;SCL RJMP Tx_0_ck ;wait SCL Hi RJMP Tx_1_ok ;continue [end]
Bemerkungen
Bei Soft-Slave-Funktionen ist das Problem das Erkennen einer Startbedingung und der nachfolgenden Adresse. Die Dauer einer Startbedingung richtet sich leider nach der Bus-Geschwindigkeit, und daher ist der Prozessor in dieser Situation für andere Dinge blockiert und kann auch irgendwelche anderen Interrupts nicht gut brauchen.
Natürlich funktioniert die Sache am Besten in einer Multimaster-Umgebung, denn da kann sich ein Master sowieso nicht drauf verlassen, daß der Bus jederzeit zur Verfügung steht. Also wird er, wenn er Pech hat, ohnehin mehrere Sendeversuche machen müssen, bis ein Slave reagiert.
Man muß also nur darauf achten, daß ein Slave mit den obigen Funktionen möglichst oft bereit ist, am Bus zu lesen. Dann klappt das ganz gut.
Gut ist es auch, wenn an sich auf dem I2C-Bus ein reges Leben herrscht, denn immerhin, bei jeder Message, die unseren Slave nicht betrifft, kann er die "WAIT START" Routine verlassen und hat etwas Zeit, auch was anderes zu tun.
Autor
Web Links
http://www.oldformation.at/electronic/download/down.htm