Forum: Mikrocontroller und Digitale Elektronik Ich suche eine schnelle ADC / DAC Berechnungs Routine für eine 32 Bit Plattform


von Sebastian B. (mircobolle)


Lesenswert?

Hallo,

ich habe hier eine DAC-Routine, die mir eine "bit-codierte" Spannung am 
Ausgang ausgeben soll. Die Auflösung ist 1 mV / Bit.

Die Plattform auf der diese Routine läuft ist ein 32 Bit Controller.
Der DAC sowie der ADC arbeiten mit 12 Bit.

Ich möchte nun erreichen, dass beide Routinen möglichst schnell diese 
Umrechnung vornehmen, damit ich eine höhere Abtastrate / Ausgabe Rate 
erreichen kann.

Als Beispiel poste ich jetzt den Code für meinen externen DAC (via SPI). 
Kernpunkt sind hier die Umrechnungen der Spannung. Falls jemand 
Verbesserungsmöglichkeiten sieht, wie die Routine schneller laufen kann 
wäre ich sehr dankbar!!
1
//-----------------------------------------------------------------------------
2
// Funktionsname: DAC_Update_A
3
// Beschreibung:  Setze den Ausgang A des DAC 
4
// Uebergabe:     u16 u16_output_a 
5
// Rueckgabe:     -
6
// Zeit (Zyklen): -
7
//-----------------------------------------------------------------------------
8
void       DAC_Update_A        (u16 u16_output_a){
9
  u16 u16_dac       = 0;
10
  u8  u8_dac_msb;
11
  u8  u8_dac_lsb;
12
  u32 u32_dac_code;
13
  u16 u16_dac_code;
14
  
15
  /*
16
      CODE = Soll_Spannung * 0xFFF /(2 * REF)
17
  */
18
  
19
  if (u16_output_a <= u16_Reference){
20
    u32_dac_code = (u32) (u16_output_a * (u32) 0x0FFF);
21
    u16_dac_code = (u16) (u32_dac_code / u16_Reference);
22
    
23
    u16_DAC_Output_A = u16_output_a; 
24
    
25
    u16_dac = u16_dac | (e_Speed      << SPD_SHIFT);   /* Setze Speed Bit     */
26
  
27
    u16_dac = u16_dac | R1_MASK;                       /* Setze R1 Bit */
28
    
29
    u8_dac_msb = (u8) ((u16_dac & 0xF000)>>8);
30
    u8_dac_msb = (u8) (u8_dac_msb | ((u16_dac_code & 0x0F00)>>8));
31
    
32
    u8_dac_lsb = (u8) (u16_dac_code & 0x0FF);
33
34
    SPI_WriteData(2,u8_dac_msb,false,false,0);    /* Write to Control Register   */
35
    SPI_WriteData(2,u8_dac_lsb,true,false,0);     /* Set Output value      */    
36
        
37
  } else {
38
    /* OUT OF RANGE */
39
  }
40
  
41
}

DANKE

PS:
Aktuell benötigt die Routine für die Ausführung zwischen 58 -  98 
Mikrosekunden.

von Sebastian B. (mircobolle)


Lesenswert?

Die Typ-Definitionen habe ich vergessen:
1
/* Globale Typedefinitionen--------------------------------------------------*/
2
typedef unsigned char u8;                  // unsigned 8 bit type definition 
3
typedef signed char s8;                    // signed 8 bit type definition   
4
typedef unsigned short u16;                  // unsigned 16 bit type definition
5
typedef signed short s16;                    // signed 16 bit type definition  
6
typedef unsigned int u32;                 // unsigned 32 bit type definition
7
typedef signed int s32;                   // signed 32 bit type definition  
8
typedef float fl;                          // floating point, length 4 byte  
9
typedef double db;                         // floating point, length 8 byte

ausserdem
1
u16_Reference = k_DAC_Reference_External * 2;

und
1
const u16 k_DAC_Reference_External  = 5477; /* 5.477 Volt */

von Matthias L. (Gast)


Lesenswert?

> Die Auflösung ist 1 mV / Bit.
> 5477; /* 5.477 Volt */
> 12bit

Hm.. Passt irgendwie nicht..


Achja, Zur Berechnung immer 2^N Werte nehmen, nicht 2^N-1.

von Matthias L. (Gast)


Lesenswert?

>      CODE = Soll_Spannung * 0xFFF /(2 * REF)
..
>    u32_dac_code = (u32) (u16_output_a * (u32) 0x0FFF);
>    u16_dac_code = (u16) (u32_dac_code / u16_Reference);


Hm..
Was ist mit folgendem Ansatz:

         0x1000       2^16
CODE = -----------  ------   SollSpannung
         2*UREF       2^16

...

         1      4096 * 2^16
CODE = -----  -------------  SollSpannung
        2^16    2 * UREF
         1
     = -----  KONSTANTE      Sollspannung
        2^16
1
CODE = (u16) ( (KONSTANTE * (u32)Sollspannung) >> 16 );

mit UREF=5477 folgt: KONSTANTE = 24506

Setzt man die Formel 5477(mV) ein, so folgt: CODE=2048
Ein u32-Überlauf findet erst bei Sollspannung>=175261(mV) statt.

Damit umgehst du dir die komplizierte Division.

von Sebastian B. (mircobolle)


Lesenswert?

Matthias Lipinsky wrote:

>
1
> CODE = (u16) ( (KONSTANTE * (u32)Sollspannung) >> 16 );
2
>
>
> mit UREF=5477 folgt: KONSTANTE = 24506
>
> Setzt man die Formel 5477(mV) ein, so folgt: CODE=2048
> Ein u32-Überlauf findet erst bei Sollspannung>=175261(mV) statt.
>
> Damit umgehst du dir die komplizierte Division.

Vielen Dank für die schnelle Antwort.
Hätte nicht gedacht, dass man da noch so viel rausholen kann. Du hast 
eine komplette Division eingesparrt. Das macht Performanz-technisch 
einiges aus !!!
Werde das ganze nochmal durchrechnen mit Bitbreiten, Überlaufen etc.

Die Herleitung hast du wirklich sehr schön verdeutlicht.
Nochmals vielen Dank.

von Matthias L. (Gast)


Lesenswert?

>Das macht Performanz-technisch einiges aus !!!

Ob und wieviel das ausmacht, musst du mal prüfen. Auch die Überläufe. 
Kann ja sein, dass ich einen Schusselfehler gemacht habe. Das Prinzip 
ist aber offenslichtlich rübergekommen.

von Sebastian B. (mircobolle)


Lesenswert?

Matthias Lipinsky wrote:
>>Das macht Performanz-technisch einiges aus !!!
>
> Ob und wieviel das ausmacht, musst du mal prüfen. Auch die Überläufe.
> Kann ja sein, dass ich einen Schusselfehler gemacht habe. Das Prinzip
> ist aber offenslichtlich rübergekommen.

Ergebnisse sind da:

die routine rechnet nun nach [Matthias Lipinsky] so:
1
    u32_dac_code = (u32) (u32_Reference * u16_output_a);
2
    u16_dac_code = (u16) (u32_dac_code >> 16);

Die Routine braucht jetzt konstant 54 Mikrosekunden !
Im Vergleich zu vorher 98 Mikrosekunden (Spitze) ist das eine 
Verbesserung um fast Faktor 2 !!!

Besten Dank für die hervorragende Unterstützung.

von Matthias L. (Gast)


Lesenswert?

>Die Routine braucht jetzt konstant 54 Mikrosekunden !

Wie hast du das ermittelt?

Das klassische Debugging-Verfahren?
..Portpin togglen..?

von Sebastian B. (mircobolle)


Lesenswert?

Matthias Lipinsky wrote:
>>Die Routine braucht jetzt konstant 54 Mikrosekunden !
>
> Wie hast du das ermittelt?
>
> Das klassische Debugging-Verfahren?
> ..Portpin togglen..?

Genau.
Hier setzte ich die "Messpunkte" mit dem Macro:
mBASICDEF_Set_MeasurePoint()
1
           case PROTOCOL_AOUTPUT_1:
2
                       if (e_access == REMOTE_CONTROL_ACCESS_WRITE){
3
                          mBASICDEF_Set_MeasurePoint();
4
                          DAC_Update_A(u16_val);
5
                          mBASICDEF_ReSet_MeasurePoint();
6
                          RS232_PROTOCOL_SendResponse(PROTOCOL_STAT_SET_OK,&s_u8_current_pmt,1);                          
7
                       }else{
8
                          RS232_PROTOCOL_SendResponse(PROTOCOL_STAT_GET_OK,&u16_DAC_Output_A,2); 
9
                       }
10
                       break;

das Makro selbst sieht so aus:
1
#define mBASICDEF_Set_MeasurePoint() (BASICDEF_BREAKPOINT=0)
2
#define mBASICDEF_ReSet_MeasurePoint() (BASICDEF_BREAKPOINT=1)

und BASICDEF_BREAKPOINT ist wie richtig vermutet ein Port-Pin ;-)


Interrupts sind nicht deaktiviert muss ich dazu sagen, aber wie gesagt 
ist das Muster auf dem Oszi konstant.

von Benedikt K. (benedikt)


Lesenswert?

Was ist das eigentlich für ein lahmer Controller?
Ich kenne jetzt nicht wirklich viele 32bit Controller, aber die die ich 
kenne, lasen sich alle mit etlichen 10MIPs takten, und die haben alle 
einen Hardaremultiplizierer, der nur wenige Takte braucht.
Daher kommen mir ein paar 10µs für die Berechnung sehr lange vor.
Ich würde daher darauf tippen, dass die SPI Schnittstelle die 
eigentliche Bremse ist.

von Matthias L. (Gast)


Lesenswert?

Dazu müsste man sich mal die Funktion
1
SPI_WriteData
ansehen...

von Sebastian B. (mircobolle)


Lesenswert?

Matthias Lipinsky wrote:
> Dazu müsste man sich mal die Funktion
>
1
> SPI_WriteData
2
>
> ansehen...

kein Problem.

Aber an SPI_WriteData dürfte es meiner Meinung nach nicht liegen, da die 
Funktion den Schreibauftrag nur entgegenimmt und in einen Ring-Puffer 
schreibt. Das eigentliche Versenden wird von der SPI-Interrupt Service 
Routine erledigt.

>Was ist das eigentlich für ein lahmer Controller?
MCF51QE128 von Freescale. Low-Power Controller.
Ich habe das identische System auf dem MC9S08QE128 und auf dem 
MCF51QE128 laufen lassen und erstaunlicherweise zieht der 32 Bit 
Controller im Vergleich zum 8 Bit Controller 20 mA WENIGER Strom.

Die interne Clock ist auf 36 kHz getrimmt.
Das ergibt nach den Clock Einstellungen einen Bus-Takt von 4,608 MHz, 
die CPU läuft mit einem Takt von BUSCLK * 2, also 9,216 MHz.
1
//-----------------------------------------------------------------------------
2
// Funktionsname: SPI_WriteData
3
// Beschreibung:  Sollen Daten ueber SPI uebertragen werden, wird diese
4
//                Funktion aufgerufen.
5
// 
6
//                b_RiseCsAfterTransmit gibt an, ob die CS Leistung NACH der
7
//                Uebertragung des Bytes HIGH gesetzt werden soll.
8
//                
9
//                Das Flag b_RS gibt an ob der RS Pin VOR einer Uebertragung auf
10
//                HIGH oder LOW gesetzt werden soll.
11
//             
12
//                Die Wartezeit u8_wait_time gibt an wie lange NACH der Versendung
13
//                eines Bytes gewartet werden soll. 
14
//
15
// Uebergabe:     SPI_Channel_e e_Channel, u8 u8_Data, bool b_RiseCsAfterTransmit,bool b_RS, u8 u8_wait_time
16
// Rueckgabe:     -
17
// Zeit (Zyklen): -
18
//-----------------------------------------------------------------------------
19
void SPI_WriteData (SPI_Channel_e e_Channel, u8 u8_Data, bool b_RiseCsAfterTransmit,bool b_RS, u8 u8_wait_time){
20
  
21
  if (e_Channel == 1){ 
22
    v_s_TxBuf_1[v_u8_TxWriteIndex_1].u8_data      = u8_Data;
23
    v_s_TxBuf_1[v_u8_TxWriteIndex_1].b_rise_cs    = b_RiseCsAfterTransmit;
24
    v_s_TxBuf_1[v_u8_TxWriteIndex_1].b_rs         = b_RS;
25
    v_s_TxBuf_1[v_u8_TxWriteIndex_1].u8_wait_time = u8_wait_time;
26
    
27
    
28
    v_b_TxBufEmpty_1                              = false;
29
    
30
    v_u8_TxWriteIndex_1 += 1;
31
    v_u8_TxWriteIndex_1 = v_u8_TxWriteIndex_1 % MAX_BUFFER_COUNT_1;
32
    
33
    /* Wenn Transmit Buffer Empty Interrupt deaktiviert UND wenn Transmitfunktion gerade NICHT aktiv */
34
    if ((SPI1C1_SPTIE == 0)&&(v_u8_busy_1 == false)){       
35
         SPI1C1_SPTIE                      = 1;    /* Interrupts wieder aktivieren */
36
    }
37
  }
38
  if (e_Channel == 2){
39
    v_s_TxBuf_2[v_u8_TxWriteIndex_2].u8_data      = u8_Data;
40
    v_s_TxBuf_2[v_u8_TxWriteIndex_2].b_rise_cs    = b_RiseCsAfterTransmit;
41
    v_s_TxBuf_2[v_u8_TxWriteIndex_2].b_rs         = b_RS;
42
    v_s_TxBuf_2[v_u8_TxWriteIndex_2].u8_wait_time = u8_wait_time;
43
    
44
    
45
    v_b_TxBufEmpty_2                              = false;
46
    
47
    v_u8_TxWriteIndex_2 += 1;
48
    v_u8_TxWriteIndex_2 = v_u8_TxWriteIndex_2 % MAX_BUFFER_COUNT_2;
49
    
50
    /* Wenn Transmit Buffer Empty Interrupt deaktiviert UND wenn Transmitfunktion gerade NICHT aktiv */
51
    if ((SPI2C1_SPTIE == 0)&&(v_u8_busy_2 == false)){       
52
         SPI2C1_SPTIE                      = 1;    /* Interrupts wieder aktivieren */
53
    }
54
  }
55
}

von Matthias L. (Gast)


Lesenswert?

Hier gibts doch Kanidaten für viel Zeit:
1
v_u8_TxWriteIndex_1 = v_u8_TxWriteIndex_1 % MAX_BUFFER_COUNT_1;
2
...
3
v_u8_TxWriteIndex_2 = v_u8_TxWriteIndex_2 % MAX_BUFFER_COUNT_2;

Wie groß sind denn di beiden Konstanten?
Das ist doch die Pufferlänge-1, stimmts?

Nimm da 2^N-1 Werte, dann kansnt du aus dem Modulo ein AND machen

von Benedikt K. (benedikt)


Lesenswert?

Sebastian B. wrote:
> MCF51QE128 von Freescale. Low-Power Controller.

OK, also eine CISC CPU. Das erklärt dann, wieso es etwas länger dauert.

Außer wenn SPI sehr langsam getaktet ist, kommt mir die Sende Routine 
ineffizient vor:
Ich gehe mal von 2 Takten pro C-Befehl aus (was eher wenig ist), selbst 
dann benötigt SPI_WriteData rund 30 Takte. Wenn die SPI Schnittstelle 
also mit mehr als 2,6MHz läuft, dann ist es schneller die Daten direkt 
zu Senden und auf das Ende zu warten (bzw. währenddessen andere 
Berechnungen zu machen, was man in C nicht immer wirklich gut festlegen 
kann, da der Compiler die Reihenfolge der Befehle öfters umsortiert).

von Sebastian B. (mircobolle)


Lesenswert?

Matthias Lipinsky wrote:
> Hier gibts doch Kanidaten für viel Zeit:
>
>
1
> v_u8_TxWriteIndex_1 = v_u8_TxWriteIndex_1 % MAX_BUFFER_COUNT_1;
2
> ...
3
> v_u8_TxWriteIndex_2 = v_u8_TxWriteIndex_2 % MAX_BUFFER_COUNT_2;
4
>
>
> Wie groß sind denn di beiden Konstanten?
> Das ist doch die Pufferlänge-1, stimmts?
>
> Nimm da 2^N-1 Werte, dann kansnt du aus dem Modulo ein AND machen
1
/* lokale Definitionen-------------------------------------------------------*/
2
#define MAX_BUFFER_COUNT_1 128

stimmt, bei "modulo" wird ja auch nochmal richtig gerechnet ...

das lasse ich mir nochmal durch den Kopf gehen.

>Außer wenn SPI sehr langsam getaktet ist, kommt mir die Sende Routine
>ineffizient vor:
>Ich gehe mal von 2 Takten pro C-Befehl aus (was eher wenig ist), selbst
>dann benötigt SPI_WriteData rund 30 Takte. Wenn die SPI Schnittstelle
>also mit mehr als 2,6MHz läuft, dann ist es schneller die Daten direkt
>zu Senden und auf das Ende zu warten (bzw. währenddessen andere
>Berechnungen zu machen, was man in C nicht immer wirklich gut festlegen
>kann, da der Compiler die Reihenfolge der Befehle öfters umsortiert).

Auch das werde ich mir mal überlegen und ob es in meinem System "direkt" 
zu realisieren wäre. Im Moment schreibe ich ja alle "Aufträge" in einen 
Puffer, der abgearbeitet wird. Jeder Auftrag setzt u.a. auch Port-Pins, 
welche spezielle Signale für die Hardware darstellen.

Man muss dazu sagen, dass das System ja eigentlich für einen 8 Bit 
Controller konzipiert war. Deshalb fand ich diese Entkopplung von 
Sende-Auftrag und eigentlicher Abarbeitung in der ISR ganz vernünftigt. 
Nun läuft das System allerdings auf einem 32 Bit Controller und ich 
versuche noch so viel Performanz wie möglich rauszukitzeln.

Vielen Dank für die Beiträge!!

von Matthias L. (Gast)


Lesenswert?

1
/* lokale Definitionen-------------------------------------------------------*/
2
#define MAX_BUFFER_COUNT_1 128
wird diese Konstante dann so verwendet:
1
StrukturFIFO   v_s_TxBuf_2[MAX_BUFFER_COUNT_1];

Wenn ja, dann hast du ja 0..128, also 129 Array-Elemente.

Mach da mal ne 127 draus, und aus dem % ein AND.

Das sollte nochmal einiges an Zeit sparen.

von Benedikt K. (benedikt)


Lesenswert?

Matthias Lipinsky wrote:
>
1
> /* lokale
2
> Definitionen-------------------------------------------------------*/
3
> #define MAX_BUFFER_COUNT_1 128
4
>
> wird diese Konstante dann so verwendet:
>
1
> StrukturFIFO   v_s_TxBuf_2[MAX_BUFFER_COUNT_1];
2
>
>
> Wenn ja, dann hast du ja 0..128, also 129 Array-Elemente.

Nein!

Ein Array array[4] hat die Elemente array[0], array[1], array[2] und 
array[3] also 4 insgesamt.
Das passt schon. Wenn der Compiler intelligent ist, macht er dann auch 
automatisch ein &127 aus dem %128. Nur leider sind nicht alle Compiler 
so intelligent.

von Matthias L. (Gast)


Lesenswert?

>Nein!

>Ein Array array[4] hat die Elemente array[0], array[1], array[2] und
>array[3] also 4 insgesamt.

Ja. Schusselfehler.
(Ich war noch bei ST von Arbeit)


>Wenn der Compiler intelligent ist, macht er dann auch
>automatisch ein &127 aus dem %128.

Darauf verlasse ich mich nicht.

von Sebastian B. (mircobolle)


Lesenswert?

Matthias Lipinsky wrote:
>
1
> /* lokale
2
> Definitionen-------------------------------------------------------*/
3
> #define MAX_BUFFER_COUNT_1 128
4
>
> wird diese Konstante dann so verwendet:
>
1
> StrukturFIFO   v_s_TxBuf_2[MAX_BUFFER_COUNT_1];
2
>

Guten Abend zusammen,

das "Verunden" mit 127 ist eine elegante Methoden den Überlauf von 127 
auf 0 zu bewerkstelligen.
1
v_u8_TxWriteIndex_1 += 1;
2
v_u8_TxWriteIndex_1 = v_u8_TxWriteIndex_1 & (MAX_BUFFER_COUNT_1-1);

Den einzigen Nachteil den ich hier sehe ist die "Eingeschränktheit" der 
Puffergroesse. Da diese immer das Format 2^N haben muss. Bei Modulo 
hingegen kann theoretisch jeder mögliche Wert des Datentyp angegeben 
werden.

Ich werde morgen mal messen was für eine Zeitersparnis der oben genannte 
Code bringt. ;-) Bin mal gespannt.

Viele Gruesse
Sebastian

von Läubi .. (laeubi) Benutzerseite


Lesenswert?

Sebastian B. wrote:
> Den einzigen Nachteil den ich hier sehe ist die "Eingeschränktheit" der
> Puffergroesse. Da diese immer das Format 2^N haben muss. Bei Modulo
> hingegen kann theoretisch jeder mögliche Wert des Datentyp angegeben
> werden.
Schnell und universell sind meist zwei dinge die sich nicht immer unter 
einen Hut bringen lassen... ;)

von Uwissender (Gast)


Lesenswert?

> Matthias Lipinsky (lippy):
> Achja, Zur Berechnung immer 2^N Werte nehmen, nicht 2^N-1.

wieso?

von Uwissender (Gast)


Lesenswert?

sry, plz ignore:

N=12 -> 2^12 -> 4096 -> 0x1000

Hex ist wohl noch nicht meine Stärke ...

Bitte melde dich an um einen Beitrag zu schreiben. Anmeldung ist kostenlos und dauert nur eine Minute.
Bestehender Account
Schon ein Account bei Google/GoogleMail? Keine Anmeldung erforderlich!
Mit Google-Account einloggen
Noch kein Account? Hier anmelden.