Audio-Player von SD-Karte mit ATtiny13          

von Arne Rossius                
Elektronik-Labor  Projekte   AVR   T13-Contest


Diese Schaltung kann bis zu 64 Audiodateien in einem speziellen Format von einer SD-Karte abspielen. Die Audio-Ausgabe erfolgt mit einer 8-Bit-PWM, ein passives analoges Filter stellt das Audiosignal wieder her. Per Ein-Knopf-Bedienung kann zum nächsten Titel gesprungen bzw. der zuletzt gespielte Titel wiederholt werden, außerdem kann per Impulsfolge jeder Titel direkt angewählt werden. Des Weiteren kann die "AutoAdvance"-Funktion eingeschaltet werden, so dass am Ende eines Titels automatisch der nächste abgespielt wird.


Schaltung

Die Schaltung besteht neben dem ATtiny13 aus folgenden Teilen:

Schaltplan,
Aufbau auf dem Steckbrett (Foto 1),
Aufbau auf dem Steckbrett (Foto 2).

SD-Karte

Da SD-Karten nur mit einer Spannungsversorgung zwischen 2,7 V und 3,6 V arbeiten können, werden die Signale vom ATtiny13 durch Spannungsteiler auf etwa 3,3 V herabgesetzt. Das MISO-Signal von der SD-Karte zum AVR ist direkt verbunden, da der mit 5 V betriebene AVR eine Spannung von 3,3 V zuverlässig als High erkennt. Die Spannungsversorgung der SD-Karte erfolgt mit zwei in Reihe geschalteten Dioden, um die Spannung von 5 V auf etwa 3,6 V herabzusetzen. Der 1 kΩ-Widerstand dient als Grundlast.

Das #CS-Signal ist im Ruhezustand high, es fließt also kontinuierlich Strom durch den Spannungsteiler. Wenn die Stromaufnahme der Schaltung besonders niedrig sein muss, sollte stattdessen ein Pegelwandler-IC wie z.B. 74HC4050 eingesetzt werden. Alternativ kann möglicherweise auch der AVR an 3,3 V betrieben werden, das Audiosignal ist dann natürlich entsprechend leiser.

Filter

Das Audio-Ausgangssignal des AVR wird von einer 8-Bit-PWM mit einer Frequenz von 37,5 kHz erzeugt. Um ein analoges Audiosignal zu erhalten wird ein Tiefpassfilter benötigt, dessen Grenzfrequenz etwa die Hälfte der Abtastrate beträgt. Die Abtastrate entspricht in diesem Fall der PWM-Frequenz. Ich habe ein Chebyshev-Filter fünfter Ordnung ausgewählt. Zur Berechnung habe ich den Chebyshev Pi LC Low Pass Filter Calculator verwendet und anschließend die Schaltung in LTspice simuliert, um die Werte für den unsymmetrischen Fall (Eingangswiderstand ungleich Ausgangswiderstand) und reale Bauteilwerte so anzupassen, dass sich trotzdem eine brauchbare Übertragungsfunktion ergibt.

Audio-Ausgabe

Wird ein Kopfhörer verwendet, so kann dieser ohne weitere Verstärkung direkt hinter dem Filter angeschlossen werden. Der Wert des Vorwiderstands und des Potis können je nach Empfindlichkeit des Kopfhörers angepasst werden, mit den angegebenen Werten ergibt sich ein guter Einstellbereich mit meinem Sennheiser PX-200.

Für den Betrieb mit Lautsprechern habe ich einen Verstärker auf LM386-Basis verwendet. Für eine leise Wiedergabe reicht auch eine Betriebsspannung von 5 V aus, dann ist es allerdings leicht möglich, den Verstärker zu übersteuern. Mit der angegebenen Spannung von 12 V kam es auch bei voll aufgedrehtem Poti und in lauten Passagen nie zu Clipping. Für das Poti sollte möglichst ein logarithmischer Typ verwendet werden, um die Lautstärke im unteren Bereich feinfühliger einstellen zu können.

Anstelle des Verstärkers können auch Aktivboxen angeschlossen werden. Um die Lautstärke zu begrenzen sollte trotzdem ein Spannungsteiler oder bei bekanntem Eingangswiderstand der Boxen ein Vorwiderstand verwendet werden. Auch ein Anschluss von Lautsprechern ohne Verstärker (und sogar ohne Filter, nur mit Entkoppelkondensator) ist möglich.


Software

AVR-Assemblerprogramm und Hexfile.

Fuses

Die Software läuft mit der maximalen Taktfrequenz des internen Oszillators von 9,6 MHz. Außerdem muss die Start-Up-Time auf 4 ms eingestellt werden (siehe unten). Zusammen mit einer Brown-Out-Detection bei 2,7 V ergeben sich die Fuse-Bytes:

Fuse-ByteWert
low0x76 (0111.0110)
high0xFB (1111.1011)

Taster am Reset-Pin

Die Software verwendet einen Trick, um eine Bedienung zu ermöglichen, obwohl bereits alle fünf Portpins anderweitig belegt sind. Der ATtiny13 bietet die Möglichkeit, nach einem Reset die Ursache aus einem Register auszulesen. Auf diese Weise kann zwischen einem Power-On-Reset nach dem Anlegen der Spannung und einem externen Reset per Taster am Reset-Pin unterschieden werden. Darüberhinaus bleiben bei einem Reset die Register unverändert, so dass z.B. die Nummer des aktuellen Titels auch nach einem Reset noch zur Verfügung steht. Die Start-Up-Time des Controllers wird mit den Fuse-Bits auf 4 ms eingestellt und dient zur Entprellung des Tasters.

Nach dem Einschalten werden zunächst nur die Register initialisiert, anschließend geht der Controller in den Power-Down-Schlafmodus, um Strom zu sparen. Nach dem ersten externen Reset wird der erste Titel (Titel 0) von der SD-Karte abgespielt. Drückt man während des Abspielens erneut auf den Reset-Taster, so springt die Software zum nächsten Titel. Befindet sie sich bereits beim letzten Titel, so wird wieder mit dem ersten Titel begonnen.

Wird das Titelende ohne vorzeitigen Reset erreicht, geht der Controller wieder in den Schlafmodus. Erfolgt nun ein Reset, wird der selbe Titel nochmals abgespielt, um zum nächsten Titel vorzurücken muss also zweimal auf den Taster gedrückt werden.

AutoAdvance-Modus

Wird der AutoAdvance-Modus aktiviert, spielt die Software automatisch den nächsten Titel ab, wenn das Ende eines Titels erreicht wird. Erst nach dem Ende des letzten Titels geht der Controller in den Schlafmodus. Die Auswahl dieses Modus erfolgt durch das Einsetzen eines 1 kΩ-Widerstands zwischen dem MISO-Pin der SD-Karte und Masse. Nach dem Ende eines Titels erfolgt kein Datentransfer zwischen SD-Karte und AVR mehr, der MISO-Pin wird also von der SD-Karte nicht mehr getrieben. Ohne den Widerstand wird er vom internen Pull-up des AVR auf High gezogen, mit dem Widerstand bleibt er auf Low. Nach dem Ende jedes Titels wird der Pegel an diesem Pin eingelesen und ggf. zum nächsten Titel weitergeschaltet.

Direkte Titelanwahl per Impulsfolge

Um auch von einem anderen Controller aus Titel anwählen zu können gibt es die Möglichkeit der direkten Titelanwahl per Impulsfolge am Reset-Pin. Dazu werden 2+n negative Impulse auf den Resetpin gegeben, um den Titel n (zwischen 0 und 63) auszuwählen. Wird ein nicht existenter Titel ausgewählt oder werden mehr als 65 Impulse empfangen, so wird nichts abgespielt. Diese Funktion kann verwendet werden, um einen laufenden Titel abzubrechen. Ein einzelner Reset-Impuls bzw. ein Druck auf den Reset-Taster beginnt den abgebrochenen Titel von vorne.

Die Impulse müssen lang genug sein, um einen Reset auszulösen und dürfen beliebig lang werden. Laut Datenblatt des ATtiny13 ist bei 5 V Betriebsspannung eine Impulslänge von 500 ns ausreichend. Zwischen zwei Impulsen sollten mindestens 5 ms liegen, damit die Software nach der Start-Up-Time von 4 ms genug Zeit hat, den Reset-Impuls-Zähler zu erhöhen. Anschließend enthält die Software eine Warteschleife von 10 ms, bevor der Impulszähler ausgewertet und der entsprechende Titel gestartet wird. Die maximale Länge zwischen zwei Impulsen darf also nicht größer als 14 ms (4 ms Start-Up-Time und 10 ms Warteschleife) werden, sicherheitshalber sollte sie nicht mehr als 10 ms betragen.

SD-Karte

Die Software unterstüzt sowohl MMC- als auch SD- und SDHC-Karten. Manche Karten sind beim Lesen jedoch langsamer als andere, eine 8 GB-SDHC-Karte (Class 4) von Kingston war sogar so langsam, dass die Audiodatei nicht mehr flüssig abgespielt werden konnte. Mit einer 16 MB-MMC von Canon und einer noname 256 MB SD-Karte funktionierte es hingegen problemlos. Wichtig ist hier nicht die durchschnittliche Lesegeschwindigkeit, sondern die maximale Zeit, die nach der Übermittlung der Adresse an die SD-Karte benötigt wird, bevor die Daten bereitstehen. Aufgrund des kleinen SRAMs des ATtiny13 ist der Puffer für die gelesenen Daten nur 48 Byte groß, bei einer Abtastfrequenz von 37,5 kHz reicht ein vollständig gefüllter Puffer deshalb gerade einmal 1,28 ms. Braucht die SD-Karte zum Lesen des Sektors länger, so verringert sich die Abspielgeschwindigkeit und es ist ein deutliches "Krächzen" zu hören. Bei den beiden kleineren Karten ist die Zeit für alle Sektoren gleich und liegt unter diesem Wert, bei der SDHC-Karte fluktuiert die Zeit jedoch stark und erreicht an einigen Stellen zu hohe Werte. In den folgenden Oszillogrammen wird das Taktsignal zur SD-Karte für die drei verschiedenen Karten gezeigt, im letzten Bild sind auch die drei Phasen eingezeichnet. Jede der "Impulsnadeln" besteht in Wirklichkeit aus einer schnellen Folge von acht Taktimpulsen, es wird also jeweils ein Byte gelesen.

Weitere Informationen zur Ansteuerung von SD-Karten sind in How to Use MMC/SDC zu finden.


Daten auf der SD-Karte

Datenformat

Die Audiodaten liegen ohne Partitionierung oder Dateisystem direkt auf der SD-Karte. Der erste Sektor auf der Karte (Sektor 0, normalerweise als Bootsektor/MBR verwendet) enthält eine Tabelle, an welcher Stelle die einzelnen Titel beginnen und wie lang diese sind. Diese Tabelle besteht aus 64 Einträgen für die Titel 0 bis 63, welche aus jeweils 8 Bytes bestehen:

BytesInhalt
0~3Startsektor des ersten Titels (Titel 0)
4~7Länge des ersten Titels (in Sektoren)
8~11Startsektor des zweiten Titels (Titel 1)
12~15Länge des zweiten Titels (in Sektoren)
16~19Startsektor des dritten Titels (Titel 2)
20~23Länge des dritten Titels (in Sektoren)
. . .. . .
504~507Startsektor des 64. Titels (Titel 63)
508~511Länge des 64. Titels (in Sektoren)

Alle Werte sind als unsigned 32-bit little endian gespeichert (niederwertigstes Byte zuerst). Ein Wert von 1 wird also als Byte-Folge 0x01, 0x00, 0x00, 0x00 gespeichert und der größtmögliche Wert ist 232 − 1 = 4294967295 (Byte-Folge 0xFF, 0xFF, 0xFF, 0xFF).

Die Länge jedes Titels muss ein Vielfaches von 512 sein, damit die Länge als Anzahl Sektoren angegeben werden kann. Da dies bei den meisten Titeln nicht der Fall sein wird müssen die Audiodaten durch Anfügen von Bytes mit dem Wert 0x80 verlängert werden, bis das nächste Vielfache von 512 erreicht ist. Die gesamten Daten eines Titels müssen in aufeinanderfolgenden Sektoren gespeichert sein. Die Reihenfolge der Titeldaten auf der SD-Karte muss nicht unbedingt der Reihenfolge der Titel in der Tabelle im ersten Sektor entsprechen.

Erzeugen der Audiodaten

Zuerst müssen aus vorliegenden Audiodateien, z.B. im MP3-Format, die Audiodaten im speziellen Format für die SD-Karte generiert werden. Am einfachsten geht das mit dem Programm Audacity, welches für Windows, Linux und MacOS verfügbar ist. Um eine Datei zu konvertieren sollten diese Schritte befolgt werden:

  1. Bestehende Audiodatei importieren: File → Import → Audio. Bei Stereo-Dateien werden nun beide Tonspuren angezeigt (Screenshot).
  2. Audio zu Mono konvertieren: Tracks → Stereo Track to Mono. Jetzt wird nur noch eine Tonspur angezeigt (Screenshot).
  3. Audiodaten auf eine Abtastrate von 37,5 kHz konvertieren: Tracks → Resample und 37500 eingeben (Screenshot).
  4. Bei Bedarf, insbesondere bei leisen Aufnahmen, kann die Tonspur noch normalisiert werden: Effects → Normalize. Es sollten sowohl "Remove any DC offset" als auch "Normalize maximum amplitude to" ausgewählt sein, bei letzterer Einstellung sollte zudem eine maximale Amplitude von 0 dB angegeben werden (Screenshot).
  5. Abtastrate für das Projekt auf 37,5 kHz einstellen: im Feld "Project Rate" unten links den Wert 37500 eingeben (Screenshot).
  6. Daten exportieren: File → Export. Als Typ "Other uncompressed files" auswählen (Screenshot). Bei "Options" als Header "RAW (header-less)" und als Encoding "Unsigned 8 bit PCM" einstellen (Screenshot). Der Dateiname sollte die Endung ".raw" bekommen.
  7. Wird anschließend nach Metadaten gefragt (Screenshot), kann das Fenster ohne Änderungen mit "OK" bestätigt werden. Da nur Audiodaten exportiert werden, spielen die Metadaten keine Rolle.

Wer lieber mit Konsolenprogrammen arbeitet kann die Audiodateien auch mit mplayer konvertieren. Die Befehlszeile dazu sieht so aus (wobei outfile.raw die Ausgabedatei und infile.mp3 die existierende Musikdatei ist, natürlich sind auch andere Formate als MP3 möglich):
mplayer -af pan=1:0.5:0.5,resample=37500:0:2 -format u8 -ao pcm:nowaveheader:file=outfile.raw infile.mp3

Schreiben der Audiodaten auf die SD-Karte

Um die Audiodaten möglichst einfach auf die SD-Karte schreiben zu können habe ich ein Programm in Visual Basic geschrieben, welches anhand der einzelnen, mit Audacity erzeugten Audiodateien die Tabelle für den ersten Sektor erzeugt und alles in eine Image-Datei für die SD-Karte schreibt. Das Programm lässt sich mit wine auch unter Linux benutzen.

Anschließend muss das erzeugte Image noch auf die SD-Karte geschrieben werden. Dabei gehen alle bereits auf der SD-Karte gespeicherten Daten verloren! Unter Linux geht das ganz einfach mit dd, wenn "imagefile" das mit dem Visual-Basic-Programm erzeugte Image und /dev/sdx der Gerätename des SD-Kartenlesers ist:
dd if=imagefile of=/dev/sdx

Für Windows gibt es verschiedene Programme, mit denen das Image auf die SD-Karte geschrieben werden kann. Ich habe mit der Stand-Alone-Version von Roadkil's Disk Image gute Erfahrungen gemacht. Zum Schreiben des Images auf die SD-Karte in der Registerkarte "Write Image" das der SD-Karte entsprechende Laufwerk ausgewählen, die Image-Datei laden und auf "Start" klicken.

Soll die SD-Karte später wieder für einen anderen Zweck eingesetzt werden, muss sie zunächst neu partitioniert und formatiert werden. Viele Geräte wie z.B. Kameras bieten dazu eine entsprechende Funktion an.


;SD card sound player for ATtiny13 (audio: 37.5 kHz, 8 bit unsigned, mono)
;ATtiny13 @ 9.6 MHz (internal R/C osc.), lfuse=0x76, hfuse=0xFB (BOD @ 2.7 V)
;(C) Arne Rossius, http://www.ebps.de.vu/
;
;
;
;PORTS
;
;  PB0 = SD-card MOSI
;  PB1 = sound output (PWM)
;  PB2 = SD-Card clock
;  PB3 = SD-card MISO, a 1k resistor to ground enables auto-advance mode
;  PB4 = SD-card #CS
;  PB5 = play/next/control input (#RESET pin): push-button to ground
;
;  Connect MOSI, SCK, #CS with resistive voltage dividers 2.2k / 1k to ground.
;  Connect MISO directly to SD card.
;  SD card power through 2 diodes 1N4148 and resistor ~1k to ground = ~3.6 V.
;
;
;
;USAGE
;
;  Press play/next button after power-on => play first title.
;  Press play/next button while playing => advance to next title (wrap-around).
;  Press play/next button after playback has stopped => play same title again.
;
;  Direct title selection: n+2 negative pulses to reset pin to select title n,
;                          where n = 0~63. Pulse width min. 2 us, distance
;                          between pulses from 5 ms to 10 ms, delay after pulse
;                          train > 15 ms. If a non-existent title is selected,
;                          nothing will be played. Button can be used normally
;                          after playback has started.
;
;  Auto-advance mode: at end of title, automatically start playing the next
;                     title until a non-existent title is reached. No wrap-
;                     around (playback stops after last title). Button and
;                     direct title selection can be used normally. To restart
;                     at first title after all titles have been played, press
;                     button twice (first press restarts last title, second
;                     press advances with wrap-around) or using direct title
;                     selection (2 pulses).
;
;  SD card data format:
;    * Header sector (sector 0): 8 bytes per title for up to 64 titles:
;      - 4 bytes offset (in sectors), little endian, e.g. 1 for first title,
;        zero if title does not exist. Gaps (non-existent title(s) followed by
;        one or more existent titles) are allowed, but titles after first gap
;        can only be reached with direct title selection.
;      - 4 bytes length (in sectors) of title, little endian. Ignored for non-
;        existent titles.
;    * Following sectors: audio data, 1 byte per sample, unsigned, idle = 0x80,
;      37500 bytes per second. Gaps between header and first title data or
;      between data areas of two titles are allowed. Title data does not have
;      to be in the same order as titles in the header sector. Each title must
;      end on a sector boundary, the unused area in the last sector should be
;      padded with 0x80.
;
;
;
;VERSIONS
;
;  2013-04-28  Arne Rossius
;    * first version



.include "tn13def.inc"



.equ  MAX_TITLES = 64
.equ  BUFFER_SIZE = 48



.equ  SD_SCK = 2
.equ  SD_MOSI = 0
.equ  SD_MISO = 3
.equ  SD_CS = 4



.def  zero = R0
.def  zeroL = R0 ;same as 'zero'
.def  zeroH = R1
.def  sreg_backup = R2
.def  reset_count = R3
.def  try_title = R4
.def  title = R5
.def  state_old = R6
.def  state = R7
      .equ  sStop = 0
      .equ  sPlay = 1
      .equ  sReset = 2
.def  size1 = R8
.def  size2 = R9
.def  size3 = R10
.def  size4 = R11
.def  sector1 = R12
.def  sector2 = R13
.def  sector3 = R14
.def  sector4 = R15

.def  temp = R16
.def  temp2 = R17
.def  count = R18
.def  inttmp = R19
.def  mask = R20
.def  buflen = R21
.def  flags = R22
     
.equ  fFastSPI = 0 ;use fast SPI transfers to SD card
      .equ  fSDHC = 1 ;card is an SDHC card
      .equ  fIgnoreNonexistent = 2 ;play nothing for non-existent titles

.def  Z3 = R24 ;32-bit Z register: upper 2 bytes
.def  Z4 = R25
;R27:R26 = X pointer (RAM pointer to start of ring buffer)
;R29:R28 = Y pointer (RAM pointer to end of ring buffer)
.def  Z1 = R30 ;32-bit Z register: lower 2 bytes
.def  Z2 = R31

;===============================================================================

.dseg
      RAM_Buffer: .byte BUFFER_SIZE
      RAM_Buffer_end:

;===============================================================================

.cseg
.org 0x000
      rjmp  reset
     
.org OVF0addr
      ;timer 0 overflow interrupt
      in    sreg_backup, SREG
     
      ;check if ring buffer contains data
      tst   buflen
      breq  ovf0_end ;buffer is empty
     
      ;read byte from buffer and output to PWM register
      ld    inttmp, X+
      out   OCR0B, inttmp ;set new PWM value
      dec   buflen
      cpi   XL, RAM_Buffer_end
      brlo  PC+2
      ldi   XL, RAM_Buffer
     
ovf0_end:
      ;return
      out   SREG, sreg_backup
      reti

;===============================================================================

.macro _ldi_d
  .if (@1 == 0)
     
movw  @02:@01, zeroH:zeroL
      movw  @04:@03, zeroH:zeroL
 
.else
      ;load immediate double-word
      ldi   @01, BYTE1(@1)
      ldi   @02, BYTE2(@1)
      ldi   @03, BYTE3(@1)
      ldi   @04, BYTE4(@1)
  .endif
.endmacro

.macro _brne
      ;branch if not equal, long distance
      breq  PC+2
      rjmp  @0
.endmacro

;===============================================================================

reset:
      ;init state register
      mov   temp, state
      cpi   temp, sReset
      breq  PC+2
      mov   state_old, temp
      ldi   temp, sReset
      mov   state, temp
      inc   reset_count
     
      ;init stack pointer
      ldi   temp, RAMEND
      out   SPL, temp
     
      ;init ports (keep PWM output high-Z for now (prevent popping noise)
      ldi   temp, 1<<SD_CS | 1<<SD_SCK | 1<<SD_MOSI
      out   DDRB, temp
      ldi   temp, 0x00 | 1<<SD_CS | 1<<SD_MISO ;enable pull-up on MISO pin
      out   PORTB, temp

      ;disable analog comparator
      ldi   temp, 0x80
      out   ACSR, temp
     
     
;init registers
      clr   zero
      clr   zeroH
     
clr   flags
      clr   buflen
      ldi   XL, RAM_Buffer
      ldi   YL, RAM_Buffer
     
      ;delay
      rcall delay_10ms
     
      ;check reset reason
      in    temp, MCUSR
      sbrc  temp, EXTRF
      rjmp  reset_ext ;external reset: start playing / next title
      ;other reset source: assume power-on reset
      ldi   temp, sStop
      mov   state, temp
      clr   title
      clr   reset_count
      ;wait for external reset
      rjmp  wait_for_reset
     
reset_ext:
      ;external reset occured
      mov   temp, reset_count
      cpi   temp, 2
      brsh  reset_pulse_train
      mov   temp, state_old
      andi  flags, ~(1<<fIgnoreNonexistent)
      cpi   temp, sPlay
      breq  next_title
      cpi   temp, sStop
      breq  play_title
      ;program should never get here
      clr   try_title
      rjmp  playback_start
     
reset_pulse_train:
      ;pulse train (multiple resets in quick succession) detected
      ori   flags, 1<<fIgnoreNonexistent
      subi  temp, 2 ;2 pulses = title 0, 3 pulses = title 1, ...
      mov   try_title, temp
      rjmp  playback_start
     
next_title:
      ;advance to next title
      mov   try_title, title
      inc   try_title
      rjmp  playback_start
     
play_title:
      ;play last title again
      mov   try_title, title
     
playback_start:
      ;start playback
      clr   reset_count
      ldi   temp, sPlay
      mov   state, temp
      mov   temp, try_title
      cpi   temp, MAX_TITLES
      brlo  playback_start_end
      sbrc  flags, fIgnoreNonexistent
      rjmp  wait_for_reset
      clr   try_title
playback_start_end:
     
init:
      ;init SD card
      rcall sd_init
      cpi   temp, 0
      breq  init_end
      ;init failed: ~100 ms delay, then try again
      ldi   count, 5
      ldi   temp, 0
      ldi   temp2, 0
init_delay:
      dec   temp2
      brne  init_delay
      dec   temp
      brne  init_delay
      dec   count
      brne  init_delay
      rjmp  init
init_end:

header:
      ;read header sector (sector 0) to find title
      movw  sector2:sector1, zeroH:zeroL
      movw  sector4:sector3, zeroH:zeroL
      rcall prepare_sector
      ldi   count, 0
header_read:
      rcall read_header_entry
      cp    count, try_title
      breq  header_finish
      inc   count
      rjmp  header_read
header_finish:
      ;flush remaining bytes in header sector
      cpi   count, 64
      breq  header_end
      rcall dump_header_entry
      inc   count
      rjmp  header_finish
header_end:
      rcall finish_sector
     
      ;check if header entry for selected title exists
      cp    sector1, zero
      cpc   sector2, zero
      cpc   sector3, zero
      cpc   sector4, zero
      breq  title_not_found
     
      ;title found in header sector
      mov   title, try_title
     
      ;init timer 0: PWM output on OC0B, fast PWM mode, non-inverted, 8 bit
      ldi   temp, 0x80 ;idle: 50% duty cycle
      out   OCR0B, temp
      ldi   temp, 1<<COM0B1 | 1<<WGM01 | 1<<WGM00
      out   TCCR0A, temp
      ldi   temp, 0x01 ;Clk/1
      out   TCCR0B, temp
      ldi   temp, 1<<TOV0 ;clear overflow interrupt flag
      out   TIFR0, temp
      ldi   temp, 1<<TOIE0 ;enable overflow interrupt
      out   TIMSK0, temp
      sbi   DDRB, 1 ;enable output pin
     
      ;enable interrupts
      sei
     
      ;all set, start playback
      rjmp  main_loop
     
title_not_found:
      ;selected title does not exist
      sbrc  flags, fIgnoreNonexistent
      rjmp  stop_playback
      tst   try_title
      breq  stop_playback ;first title doesn't exist => play nothing
      clr   try_title ;try playing first title (title 0)
      rjmp  header
     
;-------------------------------------------------------------------------------

main_loop:
     
      ;start sector
      rcall prepare_sector
     
      ;read 512 data bytes and write them to the ring buffer
      ldi   ZL, LOW(512)
      ldi   ZH, HIGH(512)
read_sector:
      rcall sd_read
      rcall buffer_write
      ldi   temp, 1
      sbiw  ZH:ZL, 1
      brne  read_sector
     
      ;end sector
      rcall finish_sector

      ;decrement file size (in sectors) counter
      sub   size1, temp
      sbc   size2, zero
      sbc   size3, zero
      sbc   size4, zero
      breq  playback_finished
     
      ;increment sector counter
      ldi   temp, 1
      add   sector1, temp
      adc   sector2, zero
      adc   sector3, zero
      adc   sector4, zero
     
      rjmp  main_loop
     
     
     
playback_finished:
      ;title ended
      ;check if auto-advance is enabled
      rcall sd_clock ;clock burst to make SD card release MISO
      rjmp  PC+1
      rjmp  PC+1
      sbic  PINB, SD_MISO
      rjmp  auto_advance_end
      mov   temp, title
      cpi   temp, MAX_TITLES - 1
      brsh  auto_advance_end ;last title reached => stop playback
      mov   try_title, title
      inc   try_title
      ori   flags, 1<<fIgnoreNonexistent ;stop if next title doesn't exist
      rjmp  header
auto_advance_end:

stop_playback:
      ;set state to "stopped"
      ldi   temp, sStop
      mov   state, temp
     
      ;put SD card to sleep mode
      cbi   PORTB, SD_CS ;assert chip select
      ldi   temp, 0 ;CMD0 (software reset = go to idle state)
      _ldi_d      Z, 0
      rcall sd_command
      rcall sd_clock
      sbi   PORTB, SD_CS ;de-assert chip select
      rcall sd_clock ;clock burst to make SD card release MISO
      cbi   PORTB, SD_MOSI ;set MOSI = low (avoid current through divider)
     
      ;wait until buffer is empty
buffer_empty:
      tst   buflen
      brne  buffer_empty
     
      ;disable PWM (set output to high-Z to prevent popping noise)
      ;(input buffers are disabled in sleep mode, so floating pins won't
      ; cause any excessive current draw)
      cli ;disable interrupts
      cbi   DDRB, 1
      ldi   temp, 0
      out   TCCR0A, temp
      out   TCCR0B, temp
     
wait_for_reset:
      ;go to sleep mode (wait for reset)
      ldi   temp, sStop
      mov   state, temp
      ldi   temp, 1<<SE | 1<<SM1 ;select power down mode
      out   MCUCR, temp
      sleep
      ;program should never get here
      rjmp  PC
     
;===============================================================================

delay_10ms:
      ;delay ~10ms (~125*256*3 cycles)
      ldi   temp, 125
      ldi   temp2, 0
delay_10ms_loop:
      dec   temp2
      brne  delay_10ms_loop
      dec   temp
      brne  delay_10ms_loop
      ret
     
;--------------------

buffer_write:
      ;write byte to ring buffer
     
      ;wait until buffer has space
      cpi   buflen, BUFFER_SIZE
      breq  buffer_write
     
      ;write byte to buffer
      st    Y+, temp
      inc   buflen
      cpi   YL, RAM_Buffer_end
      brlo  PC+2
      ldi   YL, RAM_Buffer
     
      ret

;--------------------

read_header_entry:
      ;read one header entry from header sector
     
      ;read sector address of title
      rcall sd_read
      mov   sector1, temp
      rcall sd_read
      mov   sector2, temp
      rcall sd_read
      mov   sector3, temp
      rcall sd_read
      mov   sector4, temp
     
      ;read length of title (in sectors)
      rcall sd_read
      mov   size1, temp
      rcall sd_read
      mov   size2, temp
      rcall sd_read
      mov   size3, temp
      rcall sd_read
      mov   size4, temp
     
      ret

;--------------------

dump_header_entry:
      ;dump one header entry (8 bytes) from header sector
      ldi   temp, 8
dump_header_entry_loop:
      rcall sd_clock
      dec   temp
      brne  dump_header_entry_loop
      ret

;--------------------

prepare_sector:
      ;prepare to read a sector (sector address given by sector[4:1])
     
      ;assert chip select
      cbi   PORTB, SD_CS
     
      ;send CMD17 (read block)
      ldi   temp, 17
      sbrc  flags, fSDHC
      rjmp  prepare_sector_sdhc
      ldi   Z1, 0 ;standard SD: address = sector * 512
      mov   Z2, sector1
      mov   Z3, sector2
      mov   Z4, sector3
      lsl   Z2
      rol   Z3
      rol   Z4
      rjmp  prepare_sector_cmd
prepare_sector_sdhc:
      movw  Z2:Z1, sector2:sector1 ;SDHC: address = sector
      movw  Z4:Z3, sector4:sector3
prepare_sector_cmd:
      rcall sd_command
     
      ;wait until data is ready
prepare_sector_wait:
      rcall sd_read
      ;TODO: check for errors
      cpi   temp, 0xFE ;check for "data ready" answer
      brne  prepare_sector_wait
     
      ret

;--------------------

finish_sector:
      ;finish reading a sector (assumes all 512 bytes have already been read)
     
      ;dump checksum (2 bytes)
      rcall sd_clock
      rcall sd_clock
     
      ;dummy read
      rcall sd_clock
     
      ;de-assert chip select
      sbi   PORTB, SD_CS
     
      ret

;===============================================================================

sd_init:
      ;initialize SD card
     
      ;use slow SPI transfers at first; clear SDHC flag
      andi  flags, ~(1<<fFastSPI | 1<<fSDHC)
     
      ;send at least 74 clock pulses (10 bytes = 80 pulses)
      sbi   PORTB, SD_CS ;de-assert chip select (card not selected)
      ldi   count, 10
sd_init_clocks:
      rcall sd_clock
      dec   count
      brne  sd_init_clocks
     
      ;send CMD0 (software reset = go to idle state)
      cbi   PORTB, SD_CS ;assert chip select
     
ldi   temp, 0
      _ldi_d      Z, 0
     
rcall sd_command
      rcall sd_clock
      cpi   temp, 0x01
      _brne sd_init_error
     
      ;send CMD8 (check voltage range)
      ldi   temp, 8
      _ldi_d      Z, 0x122 ;2.7~3.6V, check pattern 0x22 (same CRC as for CMD0(0))
      rcall sd_command
      cpi   temp, 0x05 ;unknown command => MMC (or old SD card?)
      breq  sd_init_mmc
      cpi   temp, 0x01
      _brne sd_init_error
      rcall sd_clock ;ignore byte 1
      rcall sd_clock ;ignore byte 2
      rcall sd_read ;read byte 3
      cpi   temp, 0x01 ;voltage range OK?
      brne  sd_init_error
      rcall sd_read ;read byte 4
      cpi   temp, 0x22 ;matches check pattern?
      brne  sd_init_error
      rcall sd_clock ;dummy read
     
sd_init_41:
      ;send ACMD41 (initiate init process) until card leaves idle state
      ;TODO: timeout
      ldi   temp, 55 ;CMD55: prepare for ACMD
      _ldi_d      Z, 0
      rcall sd_command
      rcall sd_clock
      ldi   temp, 41 ;ACMD41
      _ldi_d      Z, 1<<30 ;bit 30 (HCS) set
      rcall sd_command
      rcall sd_clock ;dummy read
      cpi   temp, 0x01 ;card still in idle state
      breq  sd_init_41
      cpi   temp, 0x05 ;unknown command => possibly a MMC (?)
      breq  sd_init_mmc
      cpi   temp, 0x00
      brne  sd_init_error
     
      ;use fast SPI for all following transfers
      ori   flags, 1<<fFastSPI
     
      ;send CMD58 (read OCR) to determine if card is SDHC
     
ldi   temp, 58
      _ldi_d      Z, 0
     
rcall sd_command
      cpi   temp, 0x00
      brne  sd_init_error
      rcall sd_read ;read byte 1
      rcall sd_clock ;ignore byte 2
      rcall sd_clock ;ignore byte 3
      rcall sd_clock ;ignore byte 4
      rcall sd_clock ;dummy read
      sbrc  temp, 6 ;HCS bit set?
      ori   flags, 1<<fSDHC
     
sd_init_blocksize:
      ;send CMD16 (set block size) to set block size to 512 bytes
     
ldi   temp, 16
      _ldi_d      Z, 512
     
rcall sd_command
      rcall sd_clock
     
      ;init finished successfully
      sbi   PORTB, SD_CS ;de-assert chip select
      clr   temp
      ret
     
sd_init_mmc:
      ;initialize MMC (or old SD card) which doesn't accept ACMD41
sd_init_mmc_loop:
      ;send CMD1 until card leaves idle state
      ;TODO: timeout
      ldi   temp, 1
      _ldi_d      Z, 0
      rcall sd_command
      rcall sd_clock ;dummy read
      cpi   temp, 0x01 ;card still in idle state
      breq  sd_init_mmc_loop
      cpi   temp, 0x00
      brne  sd_init_error
     
      ;use fast SPI for all following transfers
      ori   flags, 1<<fFastSPI
     
      rjmp  sd_init_blocksize

sd_init_error:
      ;error during SD card init
      sbi   PORTB, SD_CS ;de-assert chip select
      ldi   temp, 0xFF
      ret
     
;--------------------

sd_command:
      ;send command to SD card, return read value (bit 7 set => error)
      ori   temp, 0x40
      rcall sd_write
      mov   temp, Z4
      rcall sd_write
      mov   temp, Z3
      rcall sd_write
      mov   temp, Z2
      rcall sd_write
      mov   temp, Z1
      rcall sd_write
      ldi   temp, 0x95 ;real CRC needed for CMD0(0) and CMD8(0x122)
      rcall sd_write
      ;read response (max. 9 reads, then abort if response bit 7 still set)
      push  count
      ldi   count, 9
sd_command_read:
      rcall sd_read
      dec   count
      sbrc  temp, 7
      brne  sd_command_read
      pop   count
      ret

;--------------------

.macro sd_write_fast_bit
      ldi   temp2, 1<<SD_MISO ;keep pull-up on MISO enabled
      lsl   temp ;MSB of temp is shifted into carry flag
      adc   temp2, zero ;set bit 0 (MOSI) when carry flag is set
      out   PORTB, temp2 ;output data, set clock = low
      out   PINB, mask ;toggle SCK (set clock = high)
.endmacro

sd_write:
      ;send one byte to SD card
      sbrs  flags, fFastSPI
      rjmp  sd_write_slow
     
      ;fast SPI write
      ldi   mask, 1<<SD_SCK ;mask for clock output toggle (write to PINB)
      sd_write_fast_bit ;bit 7
      sd_write_fast_bit ;bit 6
      sd_write_fast_bit ;bit 5
      sd_write_fast_bit ;bit 4
      sd_write_fast_bit ;bit 3
      sd_write_fast_bit ;bit 2
      sd_write_fast_bit ;bit 1
      sd_write_fast_bit ;bit 0
      out   PINB, mask ;toggle SCK (set clock = low)
      ret
     
sd_write_slow:
      ;slow SPI write (< 400 kHz)
      ldi   temp2, 8
sd_write_loop:
      lsl   temp
      brcs  PC+2
      cbi   PORTB, SD_MOSI
      brcc  PC+2
      sbi   PORTB, SD_MOSI
      rcall spi_delay
      sbi   PORTB, SD_SCK
      rcall spi_delay
      cbi   PORTB, SD_SCK
      dec   temp2
      brne  sd_write_loop
      ret

;--------------------

.macro sd_read_fast_bit
      ;MSB of temp is shifted into carry flag
      lsl   temp
      sbic  PINB, SD_MISO
      ori   temp, 0x01
      out   PINB, mask ;toggle SCK (set clock = high)
      out   PINB, mask ;toggle SCK (set clock = low)
.endmacro

sd_read:
      ;receive one byte from SD card
      sbi   PORTB, SD_MOSI ;set MOSI = high
      sbrs  flags, fFastSPI
      rjmp  sd_read_slow
     
      ;fast SPI read
      ldi   mask, 1<<SD_SCK ;mask for clock output toggle (write to PINB)
      sd_read_fast_bit ;bit 7
      sd_read_fast_bit ;bit 6
      sd_read_fast_bit ;bit 5
      sd_read_fast_bit ;bit 4
      sd_read_fast_bit ;bit 3
      sd_read_fast_bit ;bit 2
      sd_read_fast_bit ;bit 1
      sd_read_fast_bit ;bit 0
      ret
     
sd_read_slow:
      ;slow SPI read (< 400 kHz)
      ldi   temp2, 8
sd_read_loop:
      rcall spi_delay
      lsl   temp
      sbic  PINB, SD_MISO
      ori   temp, 0x01
      sbi   PORTB, SD_SCK
      rcall spi_delay
     
cbi   PORTB, SD_SCK
      dec   temp2
     
brne  sd_read_loop
      ret

;--------------------
     
sd_clock:
      ;send 8 SPI clock pulses (dummy read)
      sbi   PORTB, SD_MOSI ;set MOSI = high
      sbrs  flags, fFastSPI
      rjmp  sd_clock_slow
     
      ;fast clock pulses
      ldi   mask, 1<<SD_SCK ;mask for clock output toggle (write to PINB)
      out   PINB, mask ;bit 7
      out   PINB, mask
      out   PINB, mask ;bit 6
      out   PINB, mask
      out   PINB, mask ;bit 5
      out   PINB, mask
      out   PINB, mask ;bit 4
      out   PINB, mask
      out   PINB, mask ;bit 3
      out   PINB, mask
      out   PINB, mask ;bit 2
      out   PINB, mask
      out   PINB, mask ;bit 1
      out   PINB, mask
      out   PINB, mask ;bit 0
      out   PINB, mask
      ret
     
sd_clock_slow:
      ;slow clock pulses (< 400 kHz)
      ldi   temp2, 8
sd_clock_loop:
      rcall spi_delay
      sbi   PORTB, SD_SCK
      rcall spi_delay
      cbi   PORTB, SD_SCK
      dec   temp2
      brne  sd_clock_loop
      ret

;--------------------

spi_delay:
      ;10 cycles delay
      ;(rcall = 3 cycles)
      rjmp  PC+1 ;2 cycles
      nop ;1 cycle
      ret ;4 cycles

;===============================================================================

;debug_byte:
;     push  temp
;     push  temp2
;     ldi   temp2, 8
;debug_byte_loop:
;     sbi   PORTB, 1
;     sbrc  temp, 7
;     rcall debug_byte_delay
;     cbi   PORTB, 1
;     rcall debug_byte_delay
;     lsl   temp
;     dec   temp2
;     brne  debug_byte_loop
;     pop   temp2
;     pop   temp
;     ret
;    
;debug_byte_delay:
;     rjmp  PC+1
;     rjmp  PC+1
;     rjmp  PC+1
;     rjmp  PC+1
;     rjmp  PC+1
;     rjmp  PC+1
;     rjmp  PC+1
;     rjmp  PC+1
;     rjmp  PC+1
;     rjmp  PC+1
;     ret
     


Elektronik-Labor  Projekte   AVR   T13-Contest