Ретаргетинг (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.
Источники
- Red Hat newlib C Library documentation: System Calls (write, sbrk) — разд. 2.1: определения интерфейса с ОС; write — запись в файл; подпрограммы libc используют её для вывода во все файлы, включая stdout, поэтому для отладочного вывода, например в последовательный порт, минимальную реализацию write нужно сделать способной на это; пример опирается на подпрограмму outbyte; malloc требует подпрограммы sbrk
- STMicroelectronics. STM32CubeF1, коммит c7656f1: Projects/…/SW4STM32/syscalls.c — слабые (weak) функции _read и _write; _write в цикле вызывает __io_putchar для каждого символа, объявленную как extern int __io_putchar(int ch) __attribute__((weak)), — приложение определяет её и выбирает канал вывода
- AVR-LibC 2.3.2: include/stdio.h — потоки создаются из функций вывода и ввода одного символа: макрос FDEV_SETUP_STREAM(put, get, rwflag) или fdevopen(); указатели stdin, stdout, stderr назначает приложение; три варианта vfprintf выбираются ключами компоновщика: по умолчанию — без плавающей точки, минимальный и с плавающей точкой
- ISO/IEC 9899:201x (C11), Committee Draft N1570, April 2011 — разд. 7.21.3: поток может быть без буферизации, с полной или с построчной буферизацией; разд. 7.21.5.6: setvbuf с режимом _IONBF отключает буферизацию
Нашли ошибку?
Смотрите также
- Логирование и трассировкаprintf через UART, ITM и SWO, SEGGER RTT, уровни журнала, метки времени
- UART и printfUBRR0, U2X0, UDRE0, RXC0 и printf через поток avr-libc
- ITMБлок Cortex-M, который превращает записи программы в пакеты трассировки
- SEGGER RTTОбмен с отладчиком через кольцевые буферы в ОЗУ, без отдельных выводов
- SemihostingМеханизм Arm для ввода-вывода прошивки через подключённый отладчик
- Куча (heap)ОЗУ для malloc между .bss и стеком