Por que Serial.write () é mais lento que memcpy ()?

Eu uso Serial.write() para transmitir 53 bytes para o PC. Para a medição do tempo, uso micros() (antes e depois da função de gravação). Há um atraso de 1s após cada transmissão.

O tempo da função Serial.write() é 532 us com taxa de transmissão de 1.000.000 e 360 us com taxa de transmissão de 9600.

A função Serial.write() é claramente assíncrona porque a transmissão de 53 bytes com taxa de transmissão de 9600 é 53 * 8/9600 * 1e6 = 44167 us. (A propósito, no caso da taxa de transmissão de 1000000, não é tão óbvio que a função seja assíncrona.)

Eu uso Serial.availableForWrite() antes de Serial.write() para confirmar se há espaço suficiente no buffer (isso retorna 63 a cada vez, o tamanho do buffer é 64).

Não entendo esses números. Usando memcpy() para copiar os 53 bytes leva apenas 32 us. Copiar para o buffer serial não é o mesmo que a função memcpy()? E por que é há uma diferença nos tempos de cópia quando a taxa de baud é diferente? Serial.write () é ainda mais lento com taxas de baud mais altas de acordo com os resultados. Por que Serial.availableForWrite() retorna 63 enquanto o buffer o tamanho é 64 (de acordo com SERIAL_TX_BUFFER_SIZE)?

Atualização: Obrigado por todas as suas respostas.

Eu tentei outra biblioteca para comunicação serial: https://github.com/greiman/SerialPort

Parece ser mais rápido que o original. Eu uso 2 tim e medições e um atraso no código, esses 3 deltaTimes representam toda a etapa da seguinte forma:

writeTime + memcpyTime + delaymicros = computedTime! = realTime

Os primeiros 2 tempos são medidos, o delaymicros é o atraso teórico (900 us), posso calcular o tempo do passo a partir deles. Este tempo de etapa computado é diferente de realTime (o tempo de etapa medido, eu também medi toda a etapa). Parece que o tempo adicional pode ser encontrado no atraso.

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

writeReal = 100 + 1350 – 1030 = 430

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

writeReal = 570 + 1520 – 1500 = 590

Então eu medi o tempo de atraso de micros (que é 900 nós em teoria), o tempo que falta pode ser encontrado lá. Programei um atraso de 900 us, mas o atraso real foi em torno de 1200 no primeiro e 920 no segundo teste.

Essas medições podem provar a existência de interrupções, porque medir apenas as funções de gravação não dar todo o tempo de gravação (especialmente com a biblioteca serial baixada). A biblioteca baixada pode funcionar mais rápido, mas requer um buffer tx maior por causa dos erros (256 funciona corretamente em vez de 64).

Aqui está o código : Eu uso uma soma de verificação na função sendig (que é 570-532 = 38 us). Eu uso Simulink para receber e processar os dados.

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

Comentários

  • Você pode fornecer o código usado para obter esses resultados?
  • 1 byte de buffer é perdido devido à implementação do buffer em anel. tail e head do buffer cheio podem ‘ t apontar para o mesmo índice, porque tail e head estão no mesmo índice em caso de buffer vazio

Um swer

Se você der uma olhada na implementação:

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

Ele basicamente responde a todas as perguntas.

Se o buffer estiver vazio e não estiver enviando nada, ele enviará o caractere diretamente para que seja síncrono (mas enviar o caractere será muito mais rápido do que a sobrecarga de retornos de chamada de função e assim)

Não, você não pode usar memcpy, pois ele apenas substitui o buffer, mas não faz nada mais, como realmente iniciar o registro de dados pronto interrupção nem configuração adequada dos contadores iniciais / finais (é buffer redondo, então pode haver um cenário em que você sobrescreverá algo fora do buffer)

E a função de gravação é chamada para cada caractere separadamente (todas as outras especializações de gravação estão usando este)

Além disso, se o buffer estiver cheio, ele “esperará até que haja um espaço para outro caractere.

Resposta

Copiar para o buffer serial não é o mesmo que

, não.

O buffer serial é um buffer circular . Por causa disso, há muitos cálculos envolvidos para descobrir exatamente onde no buffer colocar o próximo caractere. Isso leva tempo. memcpy() apenas copia um bloco de memória diretamente sobre outro. Ele não faz verificações e não pode “contornar como um buffer circular.

A razão pela qual as taxas de transmissão mais altas parecem mais lentas é porque cada caractere é retirado do buffer por uma interrupção.Quanto mais alta a taxa de transmissão, mais frequentemente a interrupção é disparada e, portanto, mais tempo é gasto pela CPU no processamento dessa interrupção para enviar o próximo caractere. E igualmente, menos tempo fica disponível para a CPU processar a colocação de dados no buffer serial.

Resposta

A função que você estava testando é presumivelmente Serial.write(const uint8_t *buffer, size_t size). Se você pesquisar em HardwareSerial.h, verá

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

e a implementação está em 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; }  

Isso é escrever o byte um por um, e para cada byte, todo o código de HardwareSerial::write(uint8_t c) está sendo executado. Isso inclui os testes de buffer cheio ou vazio e a aritmética para atualizar _tx_buffer_head.

Conforme declarado no comentário acima do código, seria possível substituir isso por uma implementação especializada. Em princípio, você poderia copiar o buffer linear para o buffer de anel usando no máximo duas chamadas para memcpy() e atualizando _tx_buffer_head apenas uma vez. Isso provavelmente seria mais eficiente do que a implementação atual, pelo menos para tamanhos de buffer no intervalo que você está usando (perto, mas menos do que a capacidade do buffer em anel).

Valeria a pena? Talvez para o seu caso de uso sim. Mas também pode tornar o código mais complexo e exigir mais flash. E o benefício provavelmente será muito pequeno para pessoas que escrevem buffers pequenos. Não tenho certeza se uma solicitação pull implementando esse tipo de otimização pode ser aceita. Você pode tentar se desejar.

Deixe uma resposta

O seu endereço de email não será publicado. Campos obrigatórios marcados com *