Почему Serial.write() медленнее, чем memcpy()?
Я использую Serial.write()
для передачи 53 байтов на ПК. Для измерения времени я использую micros()
(до и после функции записи). После каждой передачи существует задержка в 1 с.
Время функции Serial.write()
составляет 532 мкс при скорости 1000000 бод и 360 мкс при скорости 9600 бод.
Функция Serial.write()
явно асинхронна, поскольку передача 53 байтов со скоростью 9600 бод составляет 53*8/9600*1e6 = 44167 мкс. (Кстати, в случае со скоростью 1000000 бод не так очевидно, что функция асинхронная.)
Я использую Serial.availableForWrite()
перед Serial.write()
, чтобы подтвердить, что в буфере достаточно места (это возвращает 63 каждый раз, размер буфера по умолчанию 64).
Я не понимаю эти цифры. Использование memcpy()
для копирования 53 байтов занимает всего 32 мкс. Копирование в последовательный буфер не совпадает с функцией memcpy()
? И почему разница во времени копирования при разной скорости передачи? Согласно результатам, Serial.write() работает еще медленнее с более высокими скоростями передачи данных. Почему Serial.availableForWrite()
возвращает 63, в то время как размер буфера равен 64 (согласно SERIAL_TX_BUFFER_SIZE)?
Обновление: Спасибо за все ваши ответы.
Я попробовал другую библиотеку для последовательной связи: https://github.com/greiman/SerialPort
Кажется, он быстрее оригинала. Я использую 2 измерения времени и задержку в коде, эти 3 deltaTimes представляют весь шаг следующим образом:
writeTime + memcpyTime + delaymicros = вычисленное время != реальное время
Первые 2 раза измеряются, delaymicros — это теоретическая задержка (900 мкс), по ним я могу вычислить время шага. Это вычисленное время шага отличается от реального времени (измеренное время шага, я также измеряю весь шаг). Кажется, дополнительное время можно найти в задержке.
Библиотека последовательного порта: 100 + 30 + 900 = 1030 != 1350
writeReal = 100 + 1350 - 1030 = 430
Серийный номер Arduino: 570 + 30 + 900 = 1500 != 1520
writeReal = 570 + 1520 - 1500 = 590
Затем я измерил время delaymicros (теоретически оно составляет 900 мкс), недостающее время можно найти там. Я запрограммировал задержку 900 мкс, но реальная задержка была около 1200 в первом тесте и 920 во втором тесте.
Эти измерения могут подтвердить наличие прерываний, поскольку измерение только функций записи не дает всего времени записи (особенно с загруженной последовательной библиотекой). Загруженная библиотека может работать быстрее, но требует большего буфера tx из-за ошибок (256 работает правильно вместо 64).
Вот код: я использую контрольную сумму в функции отправки (что составляет 570-532 = 38 мкс). Я использую Simulink для получения и обработки данных.
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;
}
@Maci0503, 👍2
Обсуждение3 ответа
Лучший ответ:
Если вы посмотрите на реализацию:
size_t HardwareSerial::write(uint8_t c)
{
_written = true;
// Если буфер и регистр данных пусты, просто записываем байт
// в регистр данных и готово. Этот ярлык помогает
// значительно повысить эффективную скорость передачи данных при высоком уровне (>
// 500 кбит/с) битрейт, где накладные расходы на прерывание становятся замедлением.
if (_tx_buffer_head == _tx_buffer_tail && bit_is_set(*_ucsra, UDRE0)) {
// Если TXC очищается перед записью UDR и предыдущего байта
// завершается перед записью в UDR, TXC будет установлен, но байт
// все еще передается, что приводит к слишком быстрому возврату функции flush().
// Итак, запись UDR должна произойти первой.
// Запись UDR и очистка TC должны выполняться атомарно, иначе
// прерывания могут задержать очистку TXC, поэтому байт, записываемый в UDR
// передается (установка TXC) перед очисткой TXC. Тогда TXC будет
// очищаться, когда не осталось байтов, что приводит к зависанию flush()
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;
// Если выходной буфер полон, ничего не остается, кроме как
// ждем, пока обработчик прерывания немного его очистит
while (i == _tx_buffer_tail) {
if (bit_is_clear(SREG, SREG_I)) {
// Прерывания отключены, поэтому нам придется опросить данные
// сами регистрируем пустой флаг. Если он установлен, притворитесь
// произошло прерывание и вызовите обработчик для освобождения
// место для нас.
if(bit_is_set(*_ucsra, UDRE0))
_tx_udr_empty_irq();
} else {
// нет, обработчик прерывания освободит нам место
}
}
_tx_buffer[_tx_buffer_head] = c;
// сделать атомарным, чтобы предотвратить выполнение ISR между установкой
// указатель заголовка и установка флага прерывания, что приводит к буферу
// повторная передача
ATOMIC_BLOCK(ATOMIC_RESTORESTATE) {
_tx_buffer_head = i;
sbi(*_ucsrb, UDRIE0);
}
return 1;
}
В основном это ответы на все вопросы.
Если буфер пуст и он ничего не отправляет, он отправит символ напрямую, чтобы он был синхронным (но отправка символа будет намного быстрее, чем накладные расходы на обратные вызовы функций и т. д.)
Нет, вы не можете использовать memcpy, так как он просто заменяет буфер, но он не делает ничего другого, например, фактически запускает прерывание готовности регистра данных или правильную настройку счетчиков начала/конца (это круглый буфер, так что может быть сценарий, перезапишу что-то за пределами буфера)
И функция записи вызывается для каждого символа отдельно (все остальные специализации записи используют эту)
Кроме того, если буфер заполнен, он будет ждать, пока не появится место для другого символа.
Копирование в последовательный буфер отличается от memcpy()
, нет.
Последовательный буфер представляет собой циклический буфер. Из-за этого требуется много вычислений, чтобы определить, где именно в буфере разместить следующий символ. Это требует времени. memcpy()
просто копирует один блок памяти непосредственно поверх другого. Он не делает проверок и не может циклически повторяться, как циклический буфер.
Причина, по которой более высокие скорости передачи данных кажутся медленнее, заключается в том, что каждый символ берется из буфера прерыванием. Чем выше скорость передачи данных, тем чаще запускается это прерывание, и, следовательно, тем больше времени тратит ЦП на обработку этого прерывания для отправки следующего символа. И в равной степени тем меньше времени доступно процессору для обработки помещения данных в последовательный буфер.
Функция, которую вы тестировали, предположительно
Serial.write(const uint8_t *buffer, размер size_t)
.
Если вы выполните поиск в HardwareSerial.h, вы увидите
using Print::write; // извлекаем write(str) и write(buf, size) из Print
и реализация находится в Print.cpp:
/* реализация по умолчанию: может быть переопределена */
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;
}
Это запись байтов один за другим, и для каждого байта весь код
из HardwareSerial::write(uint8_t c)
выполняется. Это включает в себя
тесты на полный или пустой буфер и арифметика для обновления
_tx_buffer_head
.
Как указано в комментарии над кодом, было бы возможно
переопределить это с помощью специальной реализации. В принципе, вы могли бы
скопировать линейный буфер в кольцевой буфер, используя не более двух вызовов
memcpy()
и обновить _tx_buffer_head
только один раз. Это, вероятно,
быть более эффективным, чем текущая реализация, по крайней мере, для буфера
размеры в диапазоне, который вы используете (близко к размеру кольца, но меньше
буферная емкость).
Стоит ли? Возможно, для вашего варианта использования это было бы. Но это могло также сделать код более сложным и потребовать больше флэш-памяти. И польза вероятно, будет очень мало для людей, которые пишут небольшие буферы. Я не уверен, что запрос на вытягивание, реализующий такую оптимизацию, может быть принял. Вы можете попробовать, если хотите.
- Загрузка Arduino Nano дает ошибку: avrdude: stk500_recv(): programmer is not responding
- В чем разница между библиотеками Software Serial? Какая из них совместима с Arduino Nano?
- Как отправить команду AT на sim800l с помощью SoftwareSerial
- Проблемы с последовательной связью от Arduino к Bluetooth HC-05
- Как Arduino может проверить, подключен ли он к ПК и включен ли компьютер?
- Как отправлять и получать беззнаковые целые (unsigned int) от одного arduino к другому arduino
- Использование последовательных контактов TX/ RX для связи по USB
- NRF24L01+ (библиотека TMRH20): Получатель получает пустые данные
Можете ли вы предоставить код, который вы использовали для получения этих результатов?, @chrisl
1 байт буфера потерян из-за реализации кольцевого буфера. хвостовая часть заголовка полного буфера не может указывать на один и тот же индекс, потому что хвостовая часть головы находится в одном индексе в случае пустого буфера, @Juraj