De ce Serial.write () este mai lent decât memcpy ()?

Folosesc Serial.write() pentru a transmite 53 de octeți către PC. Pentru măsurarea timpului folosesc micros() (înainte și după funcția de scriere). Există o întârziere de 1s după fiecare transmisie.

Timpul funcției Serial.write() este de 532 noi cu 1000000 baud rate și 360 us cu 9600 baud rate.

Funcția Serial.write() este clar asincronă, deoarece transmiterea a 53 de octeți cu o rată de 9600 baud este de 53 * 8/9600 * 1e6 = 44167 noi. (Apropo, în cazul ratei de 1000000 baud, nu este atât de evident că funcția este asincronă.)

Folosesc Serial.availableForWrite() înainte de Serial.write() pentru a confirma că există suficient spațiu în buffer (acesta returnează 63 de fiecare dată, dimensiunea bufferului este implicit 64).

Nu înțeleg aceste numere. memcpy() pentru a copia cei 53 de octeți ne necesită doar 32. Copierea în bufferul serial nu este la fel ca funcția memcpy()? Și de ce este există o diferență în timpii de copiere când rata de transmisie este diferită? Serial.write () este chiar mai lentă, cu rate de transmisie mai mari în funcție de rezultate. De ce Serial.availableForWrite() returnează 63 în timp ce memoria tampon dimensiunea este de 64 (conform SERIAL_TX_BUFFER_SIZE)?

Actualizare: Vă mulțumim pentru toate răspunsurile dvs.

Am încercat o altă bibliotecă pentru comunicarea în serie: https://github.com/greiman/SerialPort

Se pare că este mai rapid decât cel original. Folosesc 2 tim Măsurători și o întârziere în cod, aceste 3 deltaTimes reprezintă întregul pas după cum urmează:

writeTime + memcpyTime + delaymicros = computedTime! = realTime

Se măsoară primele 2 ori, delaymicros este întârzierea teoretică (900 us), pot calcula timpul de pas din acestea. Acest timp de pas calculat este diferit de realTime (timpul de pas măsurat, măsur și eu pasul întreg). Se pare că timpul suplimentar poate fi găsit în întârziere.

Biblioteca SerialPort: 100 + 30 + 900 = 1030! = 1350

writeReal = 100 + 1350 – 1030 = 430

Arduino Serial: 570 + 30 + 900 = 1500! = 1520

writeReal = 570 + 1520 – 1500 = 590

Apoi am măsurat timpul delaymicros (care este 900 de noi în teorie), timpul lipsă poate fi găsit acolo. Am programat 900 us întârziere, dar întârzierea reală a fost de aproximativ 1200 în primul și 920 în al doilea test.

Aceste măsurători pot dovedi existența întreruperilor, deoarece măsurarea numai a funcțiilor de scriere nu este acordați tot timpul de scriere (în special cu biblioteca serială descărcată). Biblioteca descărcată poate funcționa mai repede, dar necesită un buffer tx mai mare din cauza erorilor (256 funcționează corect în loc de 64).

Iată codul : Folosesc un checkSum în funcția sendig (care este 570-532 = 38 noi). Folosesc Simulink pentru a primi și procesa datele.

 struct sendMsg1 { byte errorCheck;//1 unsigned long dT1;//4 unsigned long dT2;//4 unsigned long t;//4 unsigned long plus[10];//40 };//53 sendMsg1 msg1; byte copyMsg1[53]; unsigned long time1; unsigned long time2; unsigned long time3; unsigned long time4; void setup() { Serial.begin(1000000); } void loop() { sensorRead(); sendMsg1_f(); //time3 = micros(); delayMicroseconds(900); //time4 = micros(); } void sensorRead() { time3 = micros(); msg1.t = micros(); for (unsigned long i = 0; i < 1; i++) { memcpy((void*)&(copyMsg1[i*sizeof(sendMsg1)]), (void*)&msg1, sizeof(sendMsg1)); } time4 = micros(); msg1.dT2 = time4 - time3; } void sendMsg1_f() { time1 = micros(); msg1.errorCheck = 0; for (int i = 0; i < sizeof(sendMsg1) - 1; i++) { msg1.errorCheck += ((byte*)&msg1)[i]; } Serial.write((byte*)&msg1,sizeof(sendMsg1)); time2 = micros(); msg1.dT1 = time2 - time1; }  

Comentarii

  • Puteți furniza codul pe care l-ați folosit pentru a obține aceste rezultate?
  • 1 octet de tampon se pierde din cauza implementării bufferului inelar. coada unui cap de buffer complet poate ‘ t punctul la același index, deoarece coada un cap sunt pe același index în caz de tampon gol

An swer

Dacă aruncați o privire asupra implementării:

 size_t HardwareSerial::write(uint8_t c) { _written = true; // If the buffer and the data register is empty, just write the byte // to the data register and be done. This shortcut helps // significantly improve the effective datarate at high (> // 500kbit/s) bitrates, where interrupt overhead becomes a slowdown. if (_tx_buffer_head == _tx_buffer_tail && bit_is_set(*_ucsra, UDRE0)) { // If TXC is cleared before writing UDR and the previous byte // completes before writing to UDR, TXC will be set but a byte // is still being transmitted causing flush() to return too soon. // So writing UDR must happen first. // Writing UDR and clearing TC must be done atomically, otherwise // interrupts might delay the TXC clear so the byte written to UDR // is transmitted (setting TXC) before clearing TXC. Then TXC will // be cleared when no bytes are left, causing flush() to hang ATOMIC_BLOCK(ATOMIC_RESTORESTATE) { *_udr = c; #ifdef MPCM0 *_ucsra = ((*_ucsra) & ((1 << U2X0) | (1 << MPCM0))) | (1 << TXC0); #else *_ucsra = ((*_ucsra) & ((1 << U2X0) | (1 << TXC0))); #endif } return 1; } tx_buffer_index_t i = (_tx_buffer_head + 1) % SERIAL_TX_BUFFER_SIZE; // If the output buffer is full, there"s nothing for it other than to // wait for the interrupt handler to empty it a bit while (i == _tx_buffer_tail) { if (bit_is_clear(SREG, SREG_I)) { // Interrupts are disabled, so we"ll have to poll the data // register empty flag ourselves. If it is set, pretend an // interrupt has happened and call the handler to free up // space for us. if(bit_is_set(*_ucsra, UDRE0)) _tx_udr_empty_irq(); } else { // nop, the interrupt handler will free up space for us } } _tx_buffer[_tx_buffer_head] = c; // make atomic to prevent execution of ISR between setting the // head pointer and setting the interrupt flag resulting in buffer // retransmission ATOMIC_BLOCK(ATOMIC_RESTORESTATE) { _tx_buffer_head = i; sbi(*_ucsrb, UDRIE0); } return 1; }  

Practic răspunde la toate întrebările.

Dacă bufferul este gol și „nu trimite nimic”, acesta va trimite caracterul direct astfel încât să fie sincron (dar trimiterea caracterului va fi mult mai rapidă decât cheltuielile de apel invers ale funcției și așa)

Nu, nu puteți utiliza memcpy, deoarece înlocuiește doar bufferul, dar nu face nimic altceva, ca și cum ar fi să începeți înregistrarea de date gata întrerupere și nici setarea corectă a contoarelor cap / coadă (este bufferul rotund, deci poate exista un scenariu pe care îl veți suprascrie în afara bufferului)

Și funcția de scriere este apelată pentru fiecare caracter separat (toate celelalte specializări de scriere îl folosesc pe acesta)

De asemenea, dacă bufferul este plin, va aștepta până când există spațiu pentru un alt caracter.

Răspuns

Copierea în bufferul serial nu este la fel ca

, nr.

Bufferul serial este un buffer circular . Din acest motiv, există o mulțime de calcule implicate în stabilirea exactă a locului în tampon pentru a plasa următorul caracter. Asta necesită timp. memcpy() doar copiază un bloc de memorie direct peste altul. Nu face verificări și nu se poate înfășura ca un tampon circular.

Motivul pentru care ratele de transmisie mai mari par mai lente se datorează faptului că fiecare caracter este preluat din tampon printr-o întrerupere.Cu cât rata de transmisie este mai mare, cu atât mai frecvent este declanșată această întrerupere și, cu atât, cu atât mai mult timp este petrecut de CPU în procesarea acestei întreruperi pentru a trimite următorul caracter. Și în mod egal, cu cât este mai puțin timp disponibil pentru CPU pentru a procesa plasarea datelor în bufferul serial.

Răspuns

Funcția pe care o testați este probabil Serial.write(const uint8_t *buffer, size_t size). Dacă îl căutați în HardwareSerial.h, veți vedea

  using Print::write; // pull in write(str) and write(buf, size) from Print  

și implementarea este în Print.cpp:

 /* default implementation: may be overridden */ size_t Print::write(const uint8_t *buffer, size_t size) { size_t n = 0; while (size--) { if (write(*buffer++)) n++; else break; } return n; }  

Aceasta scrie un octet la unul și pentru fiecare octet, se execută întregul cod al HardwareSerial::write(uint8_t c). Aceasta include testele pentru bufferul complet sau gol și aritmetica pentru actualizarea _tx_buffer_head.

După cum sa menționat în comentariul de mai sus, ar fi fost posibil să suprascrieți acest lucru cu o implementare specializată. În principiu, puteți copia bufferul liniar în bufferul de apel utilizând cel mult două apeluri către memcpy() și actualizând _tx_buffer_head o singură dată. Acest lucru ar fi probabil mai eficient decât implementarea actuală, cel puțin pentru dimensiunile bufferului din gama pe care o utilizați (aproape, dar mai mică decât capacitatea bufferului de inel).

Ar merita? Poate pentru cazul dvs. de utilizare ar fi. Dar, de asemenea, ar putea face codul mai complex și să necesite mai mult flash. Și beneficiul este probabil foarte mic pentru persoanele care scriu tampoane mici. Nu sunt sigur că o cerere de extragere care implementează acest tip de optimizare poate fi acceptată. Puteți încerca dacă doriți.

Lasă un răspuns

Adresa ta de email nu va fi publicată. Câmpurile obligatorii sunt marcate cu *