Varför är Serial.write () långsammare än memcpy ()?

Jag använder Serial.write() för att överföra 53 byte till PC. För tidsmätningen använder jag micros() (före och efter skrivfunktionen). Det finns en fördröjning på 1s efter varje överföring.

Tiden för Serial.write() -funktionen är 532 oss med 1000000 baudhastighet och 360 us med 9600 baudhastighet.

Funktionen Serial.write() är tydligt asynkron eftersom överföringen av 53 byte med 9600 baudhastighet är 53 * 8/9600 * 1e6 = 44167 oss. (Förresten, när det gäller 1000000 baudhastighet, är det inte så uppenbart att funktionen är asynkron.)

Jag använder Serial.availableForWrite() före Serial.write() för att bekräfta att det finns tillräckligt med utrymme i bufferten (detta returnerar 63 varje gång, buffertstorleken är standard 64).

Jag förstår inte dessa siffror. memcpy() för att kopiera 53 byte tar bara 32 oss. Är kopiering till den seriella bufferten inte samma som memcpy() -funktionen? Och varför är det finns en skillnad i kopieringstiderna när baudhastigheten är annorlunda? Serial.write () är ännu långsammare med högre baudhastigheter enligt resultaten. Varför returnerar Serial.availableForWrite() 63 medan bufferten storlek är 64 (enligt SERIAL_TX_BUFFER_SIZE)?

Uppdatering: Tack för alla dina svar.

Jag har provat ett annat bibliotek för seriekommunikation: https://github.com/greiman/SerialPort

Det verkar vara snabbare än originalet. Jag använder 2 tim mätningar och en fördröjning i koden representerar dessa 3 deltaTimes hela steget enligt följande:

writeTime + memcpyTime + delaymicros = computedTime! = realTime

De första två gånger mäts, delaymicros är den teoretiska fördröjningen (900 us), jag kan beräkna stegtiden från dessa. Denna beräknade stegtid skiljer sig från realTime (den uppmätta stegtiden, jag mäter också hela steget). Det verkar som att den extra tiden kan hittas i förseningen.

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

writeReal = 100 + 1350 – 1030 = 430

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

writeReal = 570 + 1520 – 1500 = 590

Sedan mätte jag fördröjningsmikrotiden (vilken är 900 oss i teorin), kan den saknade tiden hittas där. Jag programmerade 900 us-fördröjning, men den verkliga förseningen var omkring 1200 i det första och 920 i det andra testet.

Dessa mätningar kan bevisa förekomsten av avbrott, eftersom det bara är att mäta skrivfunktionerna ge hela skrivtiden (särskilt med det nedladdade seriella biblioteket). Det nedladdade biblioteket kan fungera snabbare men kräver större tx-buffert på grund av fel (256 fungerar ordentligt istället för 64).

Här är koden : Jag använder en checkSum i sendig-funktionen (vilket är 570-532 = 38 us). Jag använder Simulink för att ta emot och bearbeta data.

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

Kommentarer

  • Kan du ange koden som du använde för att få dessa resultat?
  • 1 byte buffert går förlorad på grund av implementering av ringbuffert. svans ett huvud med full buffert kan ’ t peka på samma index, eftersom svans ett huvud är på samma index i fall av tom buffert

An svara

Om du tittar på implementeringen:

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

Det svarar i princip på alla frågor.

Om bufferten är tom och den inte skickar någonting, skickar den karaktären direkt så att den är synkron (men att skicka tecknet kommer att gå mycket snabbare än överbelastning av funktionsanrop och så)

Nej, du kan inte använda memcpy, eftersom det bara ersätter buffert, men det gör ingenting annat, som att faktiskt starta dataregistret klart avbryta eller korrekt inställning av huvud- / svansräknare (det är en rund buffert, så det kan finnas ett scenario du kommer att skriva över något utanför bufferten)

Och skrivfunktion kallas för varje tecken separat (all annan specialisering för att skriva använder den här)

Även om bufferten är full, väntar den tills det finns utrymme för ett annat tecken.

Svar

Kopiering till seriell buffert är inte detsamma som

, nr.

Den seriella bufferten är en cirkulär buffert . På grund av det finns det många beräkningar involverade i att räkna ut exakt var i bufferten för att placera nästa tecken. Det tar tid. memcpy() kopierar bara ett minnesblock direkt över ett annat. Det gör inga kontroller och det kan inte lindas runt som en cirkulär buffert.

Anledningen till att högre baudhastigheter verkar långsammare är att varje tecken tas från bufferten genom ett avbrott.Ju högre överföringshastighet desto oftare utlöses avbrottet, och ju mer tid spenderas CPU: n på att bearbeta avbrottet för att skicka nästa tecken. Och lika mindre, mindre tid är tillgänglig för CPU: n för att bearbeta placeringen av data i den seriella bufferten.

Svar

Funktionen du testade är förmodligen Serial.write(const uint8_t *buffer, size_t size). Om du söker efter det i HardwareSerial.h ser du

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

och implementeringen finns i 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; }  

Detta skriver byte-en en, och för varje byte körs hela koden för HardwareSerial::write(uint8_t c). Detta inkluderar testerna för full eller tom buffert och aritmetiken för uppdatering av _tx_buffer_head.

Som anges i kommentaren ovanför koden skulle det ha varit möjligt att åsidosätta detta med en specialiserad implementering. I princip kan du kopiera den linjära bufferten till ringbufferten med högst två samtal till memcpy() och uppdatera _tx_buffer_head bara en gång. Det skulle sannolikt vara effektivare än den nuvarande implementeringen, åtminstone för buffertstorlekar i det intervall du använder (nära, men mindre än ringbuffertkapaciteten).

Skulle det vara värt det? Kanske för ditt användningsfall skulle det. Men det kan också göra koden mer komplex och kräva mer blixt. Och fördelen är sannolikt väldigt liten för människor som skriver små buffertar. Jag är inte säker på att en pull-begäran som implementerar denna typ av optimering kan accepteras. Du kan försöka om du känner för det.

Lämna ett svar

Din e-postadress kommer inte publiceras. Obligatoriska fält är märkta *