Перейти к содержанию

Ретаргетинг (retargeting)

Функция printf форматирует строку, но сама не знает, куда её отправить: у микроконтроллера нет консоли. Библиотека C оставляет для этого крючок — одну-две низкоуровневые функции вывода. Ретаргетинг — это их реализация под конкретную плату.

Обновлено Редакция GAW

Ретаргетинг (retargeting) — реализация низкоуровневых функций стандартной библиотеки C под конкретное устройство, чтобы printf, puts и scanf работали с реальным каналом: UART, ITM, SEGGER RTT или дисплеем. Форматирование библиотека выполняет сама; от прошивки нужно только «как вывести байт» и «как получить байт».

Newlib (Cortex-M)

Документация newlib описывает набор системных подпрограмм, на которые опирается библиотека. Для вывода главная — write: через неё идёт вывод во все файлы, включая stdout, и документация прямо советует сделать минимальную реализацию способной писать, например, в последовательный порт. Для кучи нужна sbrk — без неё не работает malloc, а значит и буферизованный printf.

В проектах STM32Cube файл syscalls.c уже содержит слабые (weak) версии _read и _write. _write в цикле вызывает __io_putchar() для каждого символа, и приложению остаётся определить одну функцию:

int __io_putchar(int ch)
{
    while (!(USART1->SR & USART_SR_TXE)) { }
    USART1->DR = (uint8_t)ch;
    return ch;
}

avr-libc (AVR)

Здесь поток собирают из функций одного символа макросом FDEV_SETUP_STREAM(put, get, flags) и назначают его stdout. Пошаговый пример с UART ATmega328P — в уроке курса AVR с нуля про UART и printf.

Подводные камни

  • Блокировка. Вывод через UART на 115 200 бод занимает время на каждый символ, а printf в обработчике прерывания к тому же нереентерабелен.
  • Буферизация. Поток stdout может быть буферизован, и строка уходит не сразу; буфер отключают через setvbuf.
  • Память. Полная реализация printf велика — avr-libc поэтому предлагает три варианта vfprintf, от минимального до версии с плавающей точкой, — а буферы newlib берутся из динамической памяти.

Вместо своего канала библиотеку можно направить через отладчик — это semihosting, но без отладчика такая прошивка падает. Сравнение каналов и их цены — в статье о логировании и трассировке.

Частые вопросы

Как сделать, чтобы printf на STM32 писал в UART?

В проектах на newlib переопределить функцию _write(int file, char *ptr, int len), отправляя len байтов в UART. В шаблонах STM32Cube _write уже есть и вызывает для каждого символа __io_putchar(), так что достаточно определить __io_putchar с передачей одного байта через HAL или регистр данных. Аналогично можно направить вывод в ITM_SendChar() или SEGGER RTT.

Как подключить printf к UART на AVR?

В avr-libc поток создают из функции вывода одного символа: static FILE uart_out = FDEV_SETUP_STREAM(uart_putchar, NULL, _FDEV_SETUP_WRITE); и в начале main присваивают stdout = &uart_out. Функция uart_putchar ждёт освобождения регистра данных UART и записывает в него символ. Вариант vfprintf по умолчанию в avr-libc не печатает числа с плавающей точкой; версию с их поддержкой выбирают ключами компоновщика.

Почему printf ничего не выводит, хотя _write определена?

Частая причина — буферизация stdout: стандарт C допускает построчную и полную буферизацию потока (разд. 7.21.3), и символы ждут в буфере перевода строки или его заполнения. Помогает fflush(stdout) или отключение буфера вызовом setvbuf(stdout, NULL, _IONBF, 0) (разд. 7.21.5.6). Вторая причина — не определена sbrk, без которой, по документации newlib, не работает malloc.

Источники

  1. Red Hat newlib C Library documentation: System Calls (write, sbrk) — разд. 2.1: определения интерфейса с ОС; write — запись в файл; подпрограммы libc используют её для вывода во все файлы, включая stdout, поэтому для отладочного вывода, например в последовательный порт, минимальную реализацию write нужно сделать способной на это; пример опирается на подпрограмму outbyte; malloc требует подпрограммы sbrk
  2. STMicroelectronics. STM32CubeF1, коммит c7656f1: Projects/…/SW4STM32/syscalls.c — слабые (weak) функции _read и _write; _write в цикле вызывает __io_putchar для каждого символа, объявленную как extern int __io_putchar(int ch) __attribute__((weak)), — приложение определяет её и выбирает канал вывода
  3. AVR-LibC 2.3.2: include/stdio.h — потоки создаются из функций вывода и ввода одного символа: макрос FDEV_SETUP_STREAM(put, get, rwflag) или fdevopen(); указатели stdin, stdout, stderr назначает приложение; три варианта vfprintf выбираются ключами компоновщика: по умолчанию — без плавающей точки, минимальный и с плавающей точкой
  4. ISO/IEC 9899:201x (C11), Committee Draft N1570, April 2011 — разд. 7.21.3: поток может быть без буферизации, с полной или с построчной буферизацией; разд. 7.21.5.6: setvbuf с режимом _IONBF отключает буферизацию

Нашли ошибку?