Używam Serial.write()
do przesyłania 53 bajtów do komputera. Do pomiaru czasu używam micros()
(przed i po funkcji zapisu). Po każdej transmisji występuje 1s opóźnienie.
Czas działania funkcji Serial.write()
wynosi 532 us przy prędkości 1000000 bodów i 360 us przy szybkości 9600 bodów.
Funkcja Serial.write()
jest wyraźnie asynchroniczna, ponieważ transmisja 53 bajtów z szybkością 9600 bodów wynosi 53 * 8/9600 * 1e6 = 44167 us. (Nawiasem mówiąc, w przypadku szybkości transmisji 1000000 bodów nie jest tak oczywiste, że funkcja jest asynchroniczna.)
Używam Serial.availableForWrite()
przed Serial.write()
, aby potwierdzić, że jest wystarczająco dużo miejsca w buforze (za każdym razem zwraca 63, domyślny rozmiar bufora to 64).
Nie rozumiem tych liczb. Używanie memcpy()
skopiowanie 53 bajtów zajmuje tylko 32 bajty. Czy kopiowanie do bufora szeregowego to nie to samo, co funkcja memcpy()
? Dlaczego istnieje różnica w czasach kopiowania, gdy szybkość transmisji jest inna? Funkcja Serial.write () jest nawet wolniejsza przy wyższych szybkościach transmisji zgodnie z wynikami. Dlaczego Serial.availableForWrite()
zwraca 63, podczas gdy bufor rozmiar to 64 (według SERIAL_TX_BUFFER_SIZE)?
Aktualizacja: Dziękuję za wszystkie odpowiedzi.
Próbowałem innej biblioteki do komunikacji szeregowej: https://github.com/greiman/SerialPort
Wydaje się, że jest szybszy niż oryginał. Używam 2 razy Pomiary i opóźnienie w kodzie, te 3 czasy delta reprezentują cały krok w następujący sposób:
writeTime + memcpyTime + delaymicros = computedTime! = realTime
Pierwsze 2 czasy są mierzone, delaymicros to teoretyczne opóźnienie (900 us), z nich mogę obliczyć czas kroku. Ten obliczony czas kroku różni się od czasu rzeczywistego (zmierzony czas kroku, mierzę też cały krok). Wygląda na to, że dodatkowy czas można znaleźć w opóźnieniu.
Biblioteka SerialPort: 100 + 30 + 900 = 1030! = 1350
writeReal = 100 + 1350 – 1030 = 430
Arduino Serial: 570 + 30 + 900 = 1500! = 1520
writeReal = 570 + 1520 – 1500 = 590
Następnie zmierzyłem czas delaymicros (który to 900 nas w teorii) można tam znaleźć brakujący czas. Zaprogramowałem 900 nas opóźnienia, ale rzeczywiste opóźnienie wynosiło około 1200 w pierwszym i 920 w drugim teście.
Te pomiary mogą udowodnić istnienie przerwań, ponieważ mierzenie tylko funkcji zapisu nie daj cały czas na pisanie (szczególnie z pobraną biblioteką szeregową). Pobrana biblioteka może działać szybciej, ale wymaga większego bufora tx z powodu błędów (256 działa poprawnie zamiast 64).
Oto kod : Używam checkSum w funkcji sendig (czyli 570-532 = 38 us). Używam Simulink do odbierania i przetwarzania danych.
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; }
Komentarze
- Czy możesz podać kod użyty do uzyskania tych wyników?
- 1 bajt buforu jest tracony z powodu implementacji bufora pierścieniowego. Koniec nagłówka pełnego bufora może ' wskazywać ten sam indeks, ponieważ koniec nagłówka znajduje się na tym samym indeksie w przypadek pustego bufora
An swer
Jeśli przyjrzysz się implementacji:
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; }
W zasadzie odpowiada na wszystkie pytania.
Jeśli bufor jest pusty i nic nie wysyła, wyśle znak bezpośrednio, więc jest synchroniczny (ale wysłanie znaku będzie znacznie szybsze niż narzut wywołań zwrotnych funkcji i tak dalej)
Nie, nie możesz użyć memcpy, ponieważ zastępuje on tylko bufor, ale nie robi nic innego, jak faktycznie uruchamia rejestr danych gotowy przerwanie ani właściwe ustawienie liczników head / tail (jest to okrągły bufor, więc może być scenariusz, że nadpiszesz coś poza buforem)
I funkcja write jest wywoływana dla każdego znaku osobno (wszystkie inne specjalizacje zapisu używają tego)
Również jeśli bufor jest pełny, będzie czekać, aż będzie miejsce na kolejny znak.
Odpowiedź
Kopiowanie do bufora szeregowego to nie to samo, co
, nie.
Bufor szeregowy to bufor cykliczny . Z tego powodu istnieje wiele obliczeń potrzebnych do ustalenia, gdzie dokładnie w buforze należy umieścić następny znak. To wymaga czasu. memcpy()
po prostu kopiuje jeden blok pamięci bezpośrednio na inny. Nie sprawdza i nie może zawijać się jak bufor kołowy.
Przyczyną, dla której wyższe szybkości transmisji wydają się wolniejsze, jest to, że każdy znak jest pobierany z bufora przez przerwanie.Im wyższa szybkość transmisji, tym częściej to przerwanie jest wyzwalane, a więc tym więcej czasu procesor spędza na przetwarzaniu tego przerwania w celu wysłania następnego znaku. Co więcej, procesor ma mniej czasu na przetworzenie umieszczenia danych w buforze szeregowym.
Odpowiedź
Testowana funkcja to prawdopodobnie Serial.write(const uint8_t *buffer, size_t size)
. Jeśli szukasz go w HardwareSerial.h, zobaczysz
using Print::write; // pull in write(str) and write(buf, size) from Print
i implementacja jest w 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; }
To jest zapis bajtów o jeden, a dla każdego bajtu uruchamiany jest cały kod HardwareSerial::write(uint8_t c)
. Obejmuje to testy pełnego lub pustego bufora oraz arytmetykę aktualizowania _tx_buffer_head
.
Jak stwierdzono w komentarzu nad kodem, byłoby możliwe zastąpić to specjalną implementacją. Zasadniczo można skopiować bufor liniowy do bufora pierścieniowego, używając co najwyżej dwóch wywołań memcpy()
i aktualizując _tx_buffer_head
tylko raz. Byłoby to prawdopodobnie bardziej wydajne niż obecna implementacja, przynajmniej dla rozmiarów bufora z zakresu, którego używasz (blisko, ale mniej niż pojemność bufora pierścienia).
Czy byłoby warto? Może w twoim przypadku tak by się stało. Ale może również uczynić kod bardziej złożonym i wymagać więcej flashowania. A korzyść będzie prawdopodobnie bardzo mała dla osób piszących małe bufory. Nie jestem pewien, czy żądanie ściągnięcia implementujące ten rodzaj optymalizacji może zostać zaakceptowane. Możesz spróbować, jeśli masz na to ochotę.