¿Por qué Serial.write () es más lento que memcpy ()?

Yo uso Serial.write() para transmitir 53 bytes a la PC. Para la medición del tiempo utilizo micros() (antes y después de la función de escritura). Hay un retraso de 1 s después de cada transmisión.

El tiempo de la función Serial.write() es 532 us con 1000000 baudios y 360 us con 9600 baudios.

La función Serial.write() es claramente asíncrona porque la transmisión de 53 bytes con 9600 baudios es 53 * 8/9600 * 1e6 = 44167 us. (Por cierto, en el caso de una velocidad de 1000000 baudios, no es tan obvio que la función sea asíncrona).

Yo uso Serial.availableForWrite() antes de Serial.write() para confirmar que hay suficiente espacio en el búfer (esto devuelve 63 cada vez, el tamaño del búfer es 64 predeterminado).

No entiendo estos números. Usando memcpy() para copiar los 53 bytes solo se necesitan 32 us. ¿Copiar al búfer en serie no es lo mismo que la función memcpy()? ¿Y por qué ¿Hay una diferencia en los tiempos de copia cuando la velocidad en baudios es diferente? Serial.write () es aún más lento con velocidades en baudios más altas según los resultados. ¿Por qué Serial.availableForWrite() devuelve 63 mientras que el búfer el tamaño es 64 (según SERIAL_TX_BUFFER_SIZE)?

Actualización: Gracias por todas sus respuestas.

He probado otra biblioteca para comunicación en serie: https://github.com/greiman/SerialPort

Parece ser más rápido que el original. Yo uso 2 tim e mediciones y un retraso en el código, estos 3 tiempos delta representan el paso completo de la siguiente manera:

writeTime + memcpyTime + delaymicros = computedTime! = realTime

Se miden los 2 primeros tiempos, el delaymicros es el retraso teórico (900 us), puedo calcular el tiempo de paso de estos. Este tiempo de paso calculado es diferente del tiempo real (el tiempo de paso medido, yo también mido todo el paso). Parece que el tiempo adicional se puede encontrar en el retraso.

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

writeReal = 100 + 1350 – 1030 = 430

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

writeReal = 570 + 1520-1500 = 590

Luego medí el tiempo de delaymicros (que es 900 us en teoría), el tiempo que falta se puede encontrar allí. Programé un retraso de 900 us, pero el retraso real fue de alrededor de 1200 en la primera y de 920 en la segunda.

Estas mediciones pueden probar la existencia de las interrupciones, porque medir solo las funciones de escritura no dar todo el tiempo de escritura (especialmente con la biblioteca serial descargada). La biblioteca descargada puede funcionar más rápido, pero requiere un búfer tx más grande debido a errores (256 funciona correctamente en lugar de 64).

Aquí está el código : Utilizo un checkSum en la función de envío (que es 570-532 = 38 us). Utilizo Simulink para recibir y procesar los datos.

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

Comentarios

  • ¿Puede proporcionar el código que utilizó para obtener estos resultados?
  • Se pierde 1 byte de búfer debido a la implementación del búfer de anillo. La cola y la cabeza del búfer completo pueden ‘ t apuntar al mismo índice, porque la cola y la cabeza están en el mismo índice en caso de búfer vacío

Un swer

Si echa un vistazo a la implementación:

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

Básicamente responde todas las preguntas.

Si el búfer está vacío y no envía nada, enviará el carácter directamente para que sea sincrónico (pero enviar el carácter será mucho más rápido que la sobrecarga de las devoluciones de llamada de funciones, etc.)

No, no puede usar memcpy, ya que solo reemplaza el búfer, pero no hace nada más, como iniciar el registro de datos listo interrupción ni configuración adecuada de los contadores de cabeza / cola (es un búfer redondo, por lo que puede haber un escenario en el que sobrescriba algo fuera del búfer)

Y la función de escritura se llama para cada carácter por separado (todas las demás especializaciones de escritura están usando este)

Además, si el búfer está lleno, «esperará hasta que haya un espacio para otro carácter.

Respuesta

Copiar al búfer en serie no es lo mismo que

, no.

El búfer en serie es un búfer circular . Debido a eso, hay muchos cálculos involucrados en calcular exactamente en qué lugar del búfer colocar el siguiente carácter. Eso lleva tiempo. memcpy() simplemente copia un bloque de memoria directamente sobre otro. No realiza comprobaciones y no se puede ajustar como un búfer circular.

La razón por la que las velocidades en baudios más altas parecen más lentas es porque cada carácter se toma del búfer mediante una interrupción.Cuanto mayor sea la velocidad en baudios, más a menudo se activa la interrupción, por lo que la CPU dedica más tiempo a procesar esa interrupción para enviar el siguiente carácter. E igualmente, menos tiempo está disponible para la CPU para procesar la colocación de datos en el búfer en serie.

Respuesta

La función que estaba probando es presumiblemente Serial.write(const uint8_t *buffer, size_t size). Si lo busca en HardwareSerial.h, verá

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

y la implementación está en 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; }  

Esto es escribir los bytes uno por uno, y por cada byte, se está ejecutando el código completo de HardwareSerial::write(uint8_t c). Esto incluye las pruebas para el búfer lleno o vacío, y la aritmética para actualizar _tx_buffer_head.

Como se indica en el comentario sobre el código, habría sido posible anule esto con una implementación especializada. En principio, puede copiar el búfer lineal en el búfer de anillo utilizando como máximo dos llamadas a memcpy() y actualizando _tx_buffer_head solo una vez. Eso probablemente sería más eficiente que la implementación actual, al menos para tamaños de búfer en el rango que está usando (cerca, pero menos que la capacidad del búfer de anillo).

¿Valdría la pena? Quizás para su caso de uso lo haría. Pero también podría hacer que el código sea más complejo y requerir más flash. Y es probable que el beneficio sea realmente pequeño para las personas que escriben búferes pequeños. No estoy seguro de que se pueda aceptar una solicitud de extracción que implemente este tipo de optimización. Puede intentarlo si lo desea.

Deja una respuesta

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *