Беспричинное создание `Serial`, почему?

Возьмем очень простой набросок, любой простой набросок в формате .ino. Скажем, просто светодиодная мигалка, вот такая

void setup() 
{
  pinMode(LED_BUILTIN, OUTPUT);
  digitalWrite(LED_BUILTIN, LOW);
}

void loop() 
{
  static bool on;
  digitalWrite(LED_BUILTIN, (on = !on) ? HIGH : LOW);
  delay(1000);
}

После компиляции в Arduino IDE компилятор сообщает об использовании 10 байтов для глобальных переменных. Это идеально подходит для наших целей.

Теперь добавим к нему дополнительный файл .cpp. Назовем его foo.cpp. Файл выглядит следующим образом

#include <Arduino.h>

int foo()
{
  return Serial.available();
}

Скомпилируйте все вместе. Теперь компилятор сообщает об использовании 185 байтов для глобальных переменных. Но почему?!

Очевидно, что это дополнительное использование глобальной памяти связано с Serial. Это связано с тем, что на глобальный объект Serial ссылается foo. В исходном скетче Serial не создается (или не отбрасывается) компоновщиком как неиспользуемый символ. В этом случае на него ссылается foo, поэтому компоновщик сохраняет его.

Но... Но компоновщик GCC обычно умнее этого. Я ожидаю, что он увидит, что символ foo нигде не упоминается, поэтому ссылка на Serial внутри foo также "не считается" и не должен требовать экземпляра Serial в конечном коде.

На самом деле, если бы я попытался использовать свой собственный «тяжелый» класс, свою собственную глобальную переменную этого класса и сослаться на нее в foo (вместо Serial), GCC будет работать так, как ожидалось: независимо от того, насколько «тяжелой» является моя глобальная переменная, компоновщик GCC отбросит мою глобальную переменную из окончательного кода и сообщит те же 10 байтов для глобальных переменных, как и ожидалось. Использование глобальной памяти останется равным 10, пока я не сошлюсь на foo откуда-нибудь.

Но Serial какой-то особенный. Даже «неиспользуемое» упоминание Serial приводит к его появлению в глобальной памяти. Что в нем такого особенного? Что заставляет его вести себя таким образом?

, 👍1

Обсуждение

Я уверен, что в реализации Serial есть некоторая «изменчивость»., @Juraj

@Juraj: Какое конкретное появление volatile вы имеете в виду? Компилятор должен сохранять каждое появление *volatile access*. Но если функция никогда не вызывается, нет доступа для сохранения. Если Serial делает изменчивый доступ в своем конструкторе, то он всегда должен сохраняться, независимо от того, использует ли клиентский код Serial или нет. Это не то поведение, которое мы наблюдаем., @AnT

Вы проверяли командную строку для компилятора и компоновщика? Может быть какая-то опция, которая останавливает компоновщик от сброса. И вы смотрели в объектные файлы, чтобы увидеть, что использует пространство? objdump здесь очень помогает., @the busybee

при ссылке HardwareSerial.o находится в архиве .a. если бы это было с помощью одного .o в командной строке компоновщика, оно всегда было бы связано. если он находится в .a, он связан только в том случае, если это требуется. это не ответ, а только наводка. Я не знаю, как определяется «требуется», @Juraj


2 ответа


Лучший ответ:

1

Я предполагаю (хотя и не проверял), что это побочный эффект что тот же файл содержит экземпляр Serial и ассоциированные ISR. определение макроса ISR выглядит примерно вот так (немного упрощенно):

#define ISR(vector) extern "C" \
    void __attribute__((signal, used, externally_visible)) vector(void)

Ключевым здесь является атрибут «используемый». Он сообщает компилятору, что следует считать используемым, даже если ни один код не вызывает ISR (по уважительной причине: она вызывается аппаратно).

Когда ваша программа скомпонована, объектный файл foo.o загружается HardwareSerial0.o. Затем компоновщик видит последовательные ISR, которые с пометкой «б/у». Они, в свою очередь, используют объект Serial, который затем, похоже, также используется.

,

В моей исходной программе foo.cpp фактически является частью моей библиотеки Arduino. Насколько мне известно, в процессе сборки Arduino библиотеки компилируются и архивируются в настоящие статические библиотеки. И тогда объектные файлы извлекаются из них, только если они нужны (по стандартному поведению ln). Таким образом, в моем примере foo.o не должен быть извлечен из библиотеки и, следовательно, не должен тянуть с собой HardwareSerial0.o. Однако странное поведение все еще сохраняется: как только библиотека используется скетчем, Serial немедленно захватывает кусок памяти., @AnT

@AnT, есть ли ваша библиотека dot_a_linkage в library.properties? без него файлы .o помещаются в командную строку компоновщика, @Juraj

@Juraj: Нет, этого не было. И это на самом деле решает проблему сразу., @AnT


0

Я скомпилировал примеры с microCore/attiny13, который имеет другую реализацию Serial, компилятор ведет себя так, как ожидалось.

Поэтому уменьшим в HardwareSerial.h

 #define SERIAL_RX_BUFFER_SIZE 64

 to

#define SERIAL_RX_BUFFER_SIZE 0

использует ровно 121 байт (то есть 185-64) то же самое для

 #define SERIAL_TX_BUFFER_SIZE 64

 to

#define SERIAL_TX_BUFFER_SIZE 0

дает 57 байт (121-64). Используется Защищенный поток:

unsigned char _rx_buffer[SERIAL_RX_BUFFER_SIZE];
unsigned char _tx_buffer[SERIAL_TX_BUFFER_SIZE];

Я не просматривал все используемые библиотеки, чтобы найти остальные, но я предполагаю, что это глобальные определения (буферы) для глобального объекта Serial. В библиотеке microCore все буферы установлены на 0, вот как я ее нашел.

,