Perché Serial.write () è più lento di memcpy ()?

Uso Serial.write() per trasmettere 53 byte al PC. Per la misurazione del tempo utilizzo micros() (prima e dopo la funzione di scrittura). Cè un ritardo di 1 secondo dopo ogni trasmissione.

Il tempo della funzione Serial.write() è di 532 us con 1000000 baud rate e 360 us con 9600 baud rate.

La funzione Serial.write() è chiaramente asincrona perché la trasmissione di 53 byte con velocità di 9600 baud è 53 * 8/9600 * 1e6 = 44167 us. (A proposito, nel caso di 1000000 baud rate, non è così ovvio che la funzione sia asincrona.)

Uso Serial.availableForWrite() prima di Serial.write() per confermare che cè abbastanza spazio nel buffer (questo restituisce 63 ogni volta, la dimensione del buffer è 64 predefinita).

Non capisco questi numeri. Utilizzo memcpy() per copiare i 53 byte richiede solo 32 noi. La copia nel buffer seriale non è la stessa della funzione memcpy()? E perché cè una differenza nei tempi di copia quando la velocità di trasmissione è diversa? Serial.write () è ancora più lenta con velocità di trasmissione più elevate in base ai risultati. Perché Serial.availableForWrite() restituisce 63 mentre il buffer la dimensione è 64 (secondo SERIAL_TX_BUFFER_SIZE)?

Aggiornamento: grazie per tutte le tue risposte.

Ho provato unaltra libreria per la comunicazione seriale: https://github.com/greiman/SerialPort

Sembra essere più veloce delloriginale. Uso 2 tim Le misurazioni e un ritardo nel codice, questi 3 deltaTimes rappresentano lintero passaggio come segue:

writeTime + memcpyTime + delaymicros = computedTime! = realTime

Vengono misurati i primi 2 tempi, il delaymicros è il ritardo teorico (900 us), posso calcolare il tempo del passo da questi. Questo tempo di passo calcolato è diverso dal tempo reale (il tempo di passo misurato, misuro anche lintero passo). Sembra che il tempo aggiuntivo possa essere trovato nel ritardo.

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

writeReal = 100 + 1350 – 1030 = 430

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

writeReal = 570 + 1520-1500 = 590

Poi ho misurato il tempo di delaymicros (che è 900 us in teoria), il tempo mancante si trova lì. Ho programmato un ritardo di 900 us, ma il ritardo reale era di circa 1200 nel primo e di 920 nel secondo test.

Queste misurazioni possono provare lesistenza degli interrupt, perché misurare solo le funzioni di scrittura non funziona. dare tutto il tempo di scrittura (specialmente con la libreria seriale scaricata). La libreria scaricata può funzionare più velocemente, ma richiede un buffer tx più grande a causa di errori (256 funziona correttamente invece di 64).

Ecco il codice : Utilizzo un checksum nella funzione sendig (che è 570-532 = 38 us). Utilizzo Simulink per ricevere ed elaborare i dati.

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

Commenti

  • Puoi fornire il codice che hai usato per ottenere questi risultati?
  • 1 byte di buffer viene perso a causa dellimplementazione del buffer circolare. tail un head of full buffer può ‘ t puntare allo stesso indice, perché tail e head sono sullo stesso indice in caso di buffer vuoto

Un swer

Se dai unocchiata allimplementazione:

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

Fondamentalmente risponde a tutte le domande.

Se il buffer è vuoto e non invia nulla, invierà il carattere direttamente in modo che sia sincrono (ma linvio del carattere sarà molto più veloce delloverhead dei callback delle funzioni e così)

No, non puoi usare memcpy, poiché sostituisce semplicemente il buffer, ma non fa nientaltro, come avviare effettivamente il registro dei dati pronto interrompe né la corretta impostazione dei contatori testa / coda (è un buffer rotondo, quindi può essere possibile sovrascrivere qualcosa al di fuori del buffer)

E la funzione di scrittura viene chiamata per ogni carattere separatamente (tutte le altre specializzazioni di scrittura stanno usando questo)

Anche se il buffer è pieno, aspetterà finché non ci sarà uno spazio per un altro carattere.

Risposta

La copia nel buffer seriale non è la stessa di

, no.

Il buffer seriale è un buffer circolare . Per questo motivo sono necessari molti calcoli per capire esattamente dove posizionare il carattere successivo nel buffer. Ci vuole tempo. memcpy() copia solo un blocco di memoria direttamente su un altro. Non esegue controlli e non può “t avvolgere” intorno come un buffer circolare.

Il motivo per cui velocità di trasmissione più elevate sembrano più lente è perché ogni carattere viene preso dal buffer da un interrupt.Più alto è il baud rate, più spesso viene attivato linterrupt, e quindi più tempo viene impiegato dalla CPU nellelaborare linterrupt per inviare il carattere successivo. Allo stesso modo, minore è il tempo a disposizione della CPU per elaborare limmissione dei dati nel buffer seriale.

Risposta

La funzione che stavi testando è presumibilmente Serial.write(const uint8_t *buffer, size_t size). Se lo cerchi in HardwareSerial.h, vedrai

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

e limplementazione è 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; }  

Questo sta scrivendo quello byte di uno e per ogni byte viene eseguito lintero codice di HardwareSerial::write(uint8_t c). Ciò include i test per il buffer pieno o vuoto e laritmetica per laggiornamento di _tx_buffer_head.

Come affermato nel commento sopra il codice, sarebbe stato possibile sovrascrivi questo con unimplementazione specializzata. In linea di principio, potresti copiare il buffer lineare nel buffer circolare utilizzando al massimo due chiamate a memcpy() e aggiornando _tx_buffer_head solo una volta. Sarebbe probabilmente più efficiente dellattuale implementazione, almeno per le dimensioni del buffer nellintervallo che stai utilizzando (vicino, ma inferiore alla capacità del buffer circolare).

Ne varrebbe la pena? Forse per il tuo caso duso lo sarebbe. Ma potrebbe anche rendere il codice più complesso e richiedere più flash. E il vantaggio è probabilmente molto piccolo per le persone che scrivono buffer piccoli. Non sono sicuro che una richiesta pull che implementa questo tipo di ottimizzazione possa essere accettata. Puoi provare se ti senti così.

Lascia un commento

Il tuo indirizzo email non sarà pubblicato. I campi obbligatori sono contrassegnati *