Waarom is Serial.write () langzamer dan memcpy ()?

Ik gebruik Serial.write() om 53 bytes naar de pc te verzenden. Voor de tijdmeting gebruik ik micros() (voor en na de schrijffunctie). Er is een vertraging van 1s na elke verzending.

De tijd van de Serial.write() -functie is 532 us met een baudsnelheid van 1000000 en 360 us met een baudsnelheid van 9600.

De Serial.write() functie is duidelijk asynchroon omdat de overdracht van 53 bytes met 9600 baudrate 53 * 8/9600 * 1e6 = 44167 us is. (Trouwens, in het geval van een baudrate van 1000000 is het niet zo duidelijk dat de functie asynchroon is.)

Ik gebruik Serial.availableForWrite() voor Serial.write() om te bevestigen dat er voldoende ruimte in de buffer is (dit geeft elke keer 63 terug, de buffergrootte is standaard 64).

Ik begrijp deze getallen niet. memcpy() om de 53 bytes te kopiëren kost slechts 32 us. Is het kopiëren naar de seriële buffer niet hetzelfde als de memcpy() -functie? En waarom is is er een verschil in de kopieertijden wanneer de baudrate anders is? Serial.write () is zelfs langzamer met hogere baudrates volgens de resultaten. Waarom retourneert Serial.availableForWrite() 63 terwijl de buffer grootte is 64 (volgens SERIAL_TX_BUFFER_SIZE)?

Update: bedankt voor al je antwoorden.

Ik heb een andere bibliotheek geprobeerd voor seriële communicatie: https://github.com/greiman/SerialPort

Het lijkt sneller te zijn dan de originele. Ik gebruik 2 tim e metingen en een vertraging in de code, deze 3 deltaTimes vertegenwoordigen de hele stap als volgt:

writeTime + memcpyTime + delaymicros = computedTime! = realTime

De eerste 2 keer worden gemeten, de delaymicros is de theoretische vertraging (900 us), hieruit kan ik de staptijd berekenen. Deze berekende staptijd is anders dan realTime (de gemeten staptijd, ik meet ook de hele stap). Het lijkt erop dat de extra tijd te vinden is in de vertraging.

SerialPort-bibliotheek: 100 + 30 + 900 = 1030! = 1350

writeReal = 100 + 1350 – 1030 = 430

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

writeReal = 570 + 1520 – 1500 = 590

Vervolgens heb ik de delaymicros-tijd gemeten (die is 900 ons in theorie), de ontbrekende tijd is daar te vinden. Ik heb een vertraging van 900 us geprogrammeerd, maar de echte vertraging was rond de 1200 in de eerste en 920 in de tweede test.

Deze metingen kunnen het bestaan van de interrupts bewijzen, omdat het meten van alleen de schrijffuncties niet geef alle schrijftijd (vooral met de gedownloade seriële bibliotheek). De gedownloade bibliotheek kan sneller werken, maar vereist een grotere tx-buffer vanwege fouten (256 werkt correct in plaats van 64).

Hier is de code : Ik gebruik een checkSum in de sendig-functie (dat is 570-532 = 38 us). Ik gebruik Simulink om de gegevens te ontvangen en te verwerken.

 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; }  

Reacties

  • Kunt u de code opgeven die u heeft gebruikt om deze resultaten te krijgen?
  • 1 byte buffer gaat verloren vanwege implementatie van de ringbuffer. tail een head of full buffer kan ‘ t naar dezelfde index wijzen, omdat tail een head op dezelfde index staat in geval van lege buffer

An swer

Als je de implementatie bekijkt:

 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; }  

Het beantwoordt in feite alle vragen.

Als de buffer leeg is en het “niets verzendt”, zal het het karakter direct verzenden zodat het synchroon is (maar het verzenden van het personage zal veel sneller zijn dan overhead van functie-callbacks en zo)

Nee, je kunt memcpy niet gebruiken, omdat het alleen de buffer vervangt, maar het doet niets anders, zoals het feitelijk starten van het gegevensregister onderbreking noch juiste instelling van head / tail-tellers (het is ronde buffer, dus er kan een scenario zijn dat je iets buiten de buffer overschrijft)

En de schrijffunctie wordt voor elk karakter afzonderlijk aangeroepen (alle andere specialisaties van schrijven gebruiken deze)

Ook als de buffer vol is, zal het “wachten tot er een spatie is voor een ander karakter.

Antwoord

Kopiëren naar de seriële buffer is niet hetzelfde als

, nee.

De seriële buffer is een circulaire buffer . Daarom zijn er veel berekeningen bij het uitzoeken waar in de buffer het volgende teken precies moet worden geplaatst. Dat kost tijd. memcpy() kopieert gewoon het ene blok geheugen rechtstreeks over het andere. Het controleert niet en het kan “niet ronddraaien als een circulaire buffer.

De reden waarom hogere baudrates langzamer lijken, is omdat elk karakter door een interrupt uit de buffer wordt gehaald.Hoe hoger de baudrate, hoe vaker de interrupt wordt getriggerd, en dus hoe meer tijd de CPU besteedt aan het verwerken van die interrupt om het volgende teken te verzenden. En evenzo, hoe minder tijd er beschikbaar is voor de CPU om het plaatsen van gegevens in de seriële buffer te verwerken.

Antwoord

De functie die u aan het testen was, is vermoedelijk Serial.write(const uint8_t *buffer, size_t size). Als u ernaar zoekt in HardwareSerial.h, ziet u

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

en de implementatie is in 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; }  

Dit is het schrijven van de bytes met één, en voor elke byte, wordt de hele code van HardwareSerial::write(uint8_t c) uitgevoerd. Dit omvat de tests voor een volledige of lege buffer en de rekenkunde voor het bijwerken van _tx_buffer_head.

Zoals vermeld in de opmerking boven de code, zou het mogelijk zijn geweest om overschrijf dit met een gespecialiseerde implementatie. In principe zou je de lineaire buffer naar de ringbuffer kunnen kopiëren door maximaal twee aanroepen van memcpy() te gebruiken en _tx_buffer_head slechts één keer bij te werken. Dat zou waarschijnlijk efficiënter zijn dan de huidige implementatie, tenminste voor buffergroottes in het bereik dat u gebruikt (dichtbij, maar minder dan de ringbuffercapaciteit).

Zou het het waard zijn? Misschien voor uw gebruik. Maar het kan de code ook complexer maken en meer flash vereisen. En het voordeel is waarschijnlijk erg klein voor mensen die kleine buffers schrijven. Ik weet niet zeker of een pull-verzoek dat dit soort optimalisatie implementeert, kan worden geaccepteerd. Je mag het proberen als je daar zin in hebt.

Geef een reactie

Het e-mailadres wordt niet gepubliceerd. Vereiste velden zijn gemarkeerd met *