Июл 172019
 

Форматный вывод на Си для микроконтроллеров.

Алгоритмы и программные решения
Форматированный ввод-вывод применяется очень широко, в первую очередь это, конечно, взаимодействие с пользователем, а так-же отладочный вывод, логи, работа с различными текстовыми протоколами и многое другое. В этой статье рассматриваются особенности применения форматированного вывода (ввод оставим на потом) для микроконтроллеров.
Первая программа написанная для ПК традиционно называется «Hello, world» и естественно пишет в стандартный вывод эту знаменитую фразу:

#include <stdio.h> int main(){ printf("%s", "Hello, world"); return 0; }

Первая программа для микроконтроллера обычно зовётся «Blinky» и она просто мигает светодиодом. Дело в том, что заставить работать традиционный «Hello, world» на микроконтроллере не так уж и просто для начинающего. Во первых, нет стандартного устройства вывода и его функциональность ещё нужно реализовать. Во вторых, не всегда бывает очевидно как подружить стандартные функции вывода с целевым устройством. Ситуация усугубляется тем, что в каждом компиляторе (тулчейне) это делается каким-то своим способом.

Форматный вывод.

Что-же, в общем, за зверь такой форматный вывод? Упрощенно говоря, это — вывод значений различных типов в виде текстовых полей.

Текстовое поле состоит из собственно значения, преобразованного в строку символов, и заполняющих символов для получения нужной ширины поля. Заполняющие символы могут находится слева или справа от значения, с помощью этого получается выравнивание по правому или левому краю соответственно. Заполнение также может находится внутри значения между определёнными его элементами, например между знаком и числом (как на рисунке), или между базой шестнадцатеричного числа (0x) и числом.
В качестве заполняющего символа обычно используется пробел, однако иногда могут использоваться нули, например в качестве ведущих нулей, или ещё что-нибудь.
Таким образом, форматированный вывод это — преобразование значений различных типов в текстовую форму и вывод их с определённым выравниванием и определённым заполнением.

Требования и особенности ввода-вывода для МК.

1. Гибкость. В отличии от старших братьев, для МК нет и не может быть стандартного устройства ввода-вывода. В каждой системе будет что-то своё, с уникальными требованиями и особенностями. В одной системе будет вывод в USART, во второй — на ЖК дисплей, в третей — в файл на SD карточке, в четвёртой — всё сразу. Значит система форматированного ввода-вывода должна быть достаточно гибкой, настраиваемой и независимой от аппаратных особенностей целевой платформы.

2. Требования к ресурсам. В МК ресурсов бывает мало и очень мало. В идеале, в прошивку МК должна попасть только та функциональность, которая реально используется. Скорость и размер кода имеют значение, под час решающее. Если мы не используем вывод типов с плавающей точкой, то и в полученной прошивке не должно быть кода, который его осуществляет.

3. Стойкость к ошибкам кодирования. В идеале, хорошо было-бы если ошибка кодирования приводила бы сразу к ошибке компиляции, ну или по крайней мере к предупреждению. Чтоб не надо было заливать программу в железо и ходить отладчиком вылавливать место, где там, в функцию предался неправильный аргумент.

4. Доступность. Библиотека ввода-вывода должна быть в наличии для целевой платформы.

5. Функциональность часто ставится на последнее место в угоду скорости и компактности.

Стандартная библиотека Си.

Стандартным и единственно доступным средством форматированного вывода в Си является семейство функций: printf, sprintf, snprintf и fprintf.
Рассматривать будем функцию printf, как наиболее типичного и часто используемого представителя функций форматного вывода, при необходимости переходя к другим. Итак фунция printf имеет следующий синтаксис:

int ptintf(const char *fmt, ...);

Это обычная функция с переменным числом параметров. Здесь первый аргумент fmt является форматной строкой, которая содержит, как обычный текст, так и специальные управляющие последовательности. Троеточие (…) обозначает список дополнительных параметров, их может быть от нуля и до сколько поместится в стеке. Да, да все параметры в функцию printf передаются преимущественно в стеке (за исключением архитектуры ARM, где первые 4 параметра передаются в регистрах). Запихивает их в стек вызывающая функция, она-же инициализирует кадр стека перед вызовом и очищает после. Сама printf узнать сколько и каких параметров ей было передано может узнать только из форматной строки. При передачи параметров размером меньше (signed char, unsigned char, char и на некоторых платформах signed/unsigned short), чем int, они будут расширенны соответствующим расширением(знаковым или без-знаковым) до int-a. Это сделано для того, чтобы стек был всегда выровнен по границе машинного слова, а так-же уменьшает количество возможных ошибок при нестыковке размера фактического параметра и ожидаемого из форматной строки. Так-же параметры типа float при передаче в функции с переменным числом аргументов, приводятся к типу double.
Форматная строка содержит как символы непосредственно выводимые в поток, так и специальные управляющие последовательности, которые начинаются со знака «%». Управляющие последовательности имеют следующий формат:

%[флаги][ширина][.точность|длинна]спецификатор[дополнительный модификатор]

Единственным обязательным элементом здесь является спецификатор, который определяет интерпретацию типа соответствующего параметра и может принимать следующие значения:
Таблица 1. Специфокаторы.

Флаги определяют дополнительные параметры форматирования (их может быть несколько):
Таблица 2. Флаги.

Ширина — десятичное число, задаёт минимальное количество символов выводимых для соответствующего параметра. Если выводимое значение содержит меньше символов, чем указано, то оно будет дополнено пробелами (или нулями если есть флаг «0») до нужной ширины слева или справа, если указан флаг «-«. Например:

printf("a =%10d", 123); Будет выведено ("_" - подчерк считать пробелом): a_=_ _ _ _ _ _ _123 printf("a =%-10d", 123); a_=123_ _ _ _ _ _ _ printf("a =%010d", 123); a_=0000000123 

Если вместо десятичного числа указать «*», то значение ширины будет считанно из дополнительного целочисленного параметра. Это позволяет задавать значение ширины поля вывода из переменной:

for(int i=1; i < 10; i++) printf("i =%0*d", i, i);

Здесь первый раз i передаётся в качестве ширины поля, второй — значения.
Точность или длинна — десятичное число после знака точки «.». В случае вывода целых чисел этот элемент означает минимальное количество записанных знаков, если выводимое число короче указанной длинны, то оно дополняется ведущими нулями, если число длиннее, то оно не урезается.

printf("a = %.6d", 123); a = 000123

Таким образом есть уже два способа вывести целое с нужным количеством ведущих нулей.
Для чисел с плавающей точкой в форматах «e», «E» и «f» этот элемент означает число знаков после десятичной точки. Результат округляется.

printf("f = %.2f", 12.3456); f = 12.35

Для форматов «g» и «G» это — общее количество выведенных значимых цифр.

printf("f = %.2g", 12.3456); f = 12

Для строк «s» этот элемент называется длинна и означает максимальное количество выведенных символов, обычно строки выводятся пока не встретится нулевой завершающий символ.

printf("%.5s\n", "Hello, world"); Hello

Также как и в элемента «ширина», в место точности можно поставить звёздочку и передать её значение в виде дополнительного целочисленного параметра:

char str[] = "Hello, world"; for(unsigned i = 1; i < sizeof(str); i++) printf("%.*s\n", i, str); H He Hel Hell Hello Hello, Hello, Hello, w Hello, wo Hello, wor Hello, worl Hello, world

Дополнительный модификатор служит для указания размерности типа:
h — применяется со спецификаторами i, d, o, u, x и X для знаковых и без-знаковых коротких целых short и unsigned short).
l — совместно со спецификаторами i, d, o, u, x и X означают длинные целые long и unsigned long).
l — совместно со спецификаторами s и c «широкие» многобайтные строку и символ соответственно.
L — обозначает тип long double, применяется со спецификаторами e, E, f, g и G.
В компиляторах поддерживающих тип long long, таких как GCC и IAR, часто есть для него нестандартный модификатор ll.
В стандарте С99 добавлены модификаторы «t» и «Z» для типов ptrdiff_t и size_t соответственно.

Работа над ошибками

Основным недостатком функций семейства printf считается вовсе не громоздкость и неторопливость — размер кода и накладные расходы на запихивание параметров в стек и разбор форматной строки обычно считаются приемлемыми, а подверженность ошибкам кодирования. Самое неприятное в этих ошибках то, что они могут быть неочевидными и проявляться не всегда, или не на всех платформах.
Большинство ошибок связано с несоответствием спецификаторов указанных в форматной строке с количеством и типами фактических аргументов. При этом можно выделить следующие ситуации:
— занимаемый в стеке размер параметров ожидаемых из форматной строки меньше размера фактически переданных. Типы фактические параметров совпадают с ожидаемыми. В этом случае просто выведутся параметры указанные в форматной строке, а лишние будут проигнорированы.
— занимаемый в стеке размер параметров ожидаемых из форматной строки меньше размера фактически переданных. Типы фактические параметров не совпадают с ожидаемыми. В этом случае параметры просто будут интерпретированы в соответствии с форматной строкой и в общем случае будет выведен мусор.
— размер фактических параметров меньше ожидаемых. Здесь поведение не определено и зависит от кучи разных факторов — от платформы, от компилятора, от содержимого стека на момент вызова printf. Поведение printf при этом может быть от внешне корректной работы и до чтения и даже записи произвольных участков памяти со всеми вытекающими последствиями.
Многие ошибки возникают при переносе кода с одной платформы на другую у которых отличаются размеры типов. Например:

printf("A = %x", 0x12345678); 

На платформах, где int имеет 32 бита этот код работает правильно, а где int — 16 бит — будут выведены только 2 младших или старших (в зависимости от порядка следования байт) байта.
К счастью некоторые компиляторы, например GCC, знают printf «в лицо» и выдают предупреждения в случае несовпадения фактических параметров с форматной строкой. Однако это работает только если форматная строка задана литералом. А если она хранится в переменной (например extern указатель инициализируемый в другом модуле), то компилятор бессилен и проследить соответствеи параметров может быто очень не просто.

Особенности реализаций

AVR

AVR достаточно неудобная платформа для работы со строками из-за того, что они могут располагаться в различных адресных пространствах и когда работаешь со строкой всегда нужно помнить где именно она расположена. В первую очередь нас естественно интересуют строки в RAM и во Flash.
Примеры для AVR рассчитаны для запуска на симуляторе sImulavr и используют его механизм отладочного вывода как стандартный вывод. Это позволит не отвлекаться на инициализацию устройств выводи и т. д. К тому-же не надо загружать прошивку в устройство для запуска.

AVR-GCC он-же WinAVR

В AVR-GCC, а точнее в avr-libc самая удачная, на мой взгляд, реализация стандартной библиотеки ввода-вывода Си. В ней имеется возможность выбирать необходимый функционал функций семейства printf. Они прекрасно работают со строками как RAM так и во Flash. Все функции семейства, включая snprintf и fprintf разделяют общий код и очень хорошо оптимизированы как по скорости, таки по объёму кода.
Для поддержки находящихся во Flash памяти строк введен новый спецификатор форматной строки %S — S — заглавная, строчная s по-прежнему означает строку в RAM. Но во Flash памяти может быть и сама форматная строка, для этого есть специальные модификации функций с суффиксом «_P» printf_P, fprintf_P, sprintf_P и т. д., которые ожидают форматную строку во Flash.
Для того чтобы printf заработала, нужно написать функцию вывода символа и определить файловый дескриптор стандартного вывода.

#include <avr/io.h> // для printf #include <stdio.h> // для PROGMEM #include <avr/pgmspace.h> // порт для отладочного вывода - определен в параметрах запуска симулятора #define special_output_port (*((volatile char *)0x24)) // прототип функции вывода символа static int my_putchar(char c, FILE *stream); // определяем дескриптор для стандартного вывода static FILE mystdout = FDEV_SETUP_STREAM( my_putchar, // функция вывода символа NULL, // функция ввода символа, нам сейчас не нужна _FDEV_SETUP_WRITE // флаги потока - только вывод ); // функция вывода символа static int my_putchar(char c, FILE *stream) { special_output_port = c; return 0; } // форматная строка во flash PROGMEM char str1[] ="Format str from flash\nlong = %15ld\nFlash string = %10S\n"; // просто строка во flash PROGMEM char str2[] = "string form flash"; __attribute((OS_main)) int main(void) { // инициализируем стандартный дескриптор stdout = &mystdout; // форматная строка в RAM и выводим строку из RAM printf("Format str from ram\nlong = %15ld\nRam string = %10s\n", 1234567890l , "string from RAM"); // форматная строка в flash и выводим строку из flash printf_P(str1, 1234567890l , str2); return 0; }

Помимо stdout есть стандартные дескрипторы stdin и stderr для стандартного ввода и стандартного вывода ошибок соответственно. Используя функцию fprintf можно явно указывать нужный файловый дескриптор и при необходимости можно определить сколько угодно устройств вывода:

static FILE lcd = FDEV_SETUP_STREAM( my_lcd_putchar, NULL, _FDEV_SETUP_WRITE); static FILE usart= FDEV_SETUP_STREAM( my_usart_putchar, NULL, _FDEV_SETUP_WRITE); … stdout = &lcd; stderr = &usart; … fprintf(stderr, "Hello, world!"); fprintf(stdout, "Hello, world!");

В avr-libc имеется три уровня функциональности библиотеки ввода-вывода:
1. нормальный — поддерживается вся упомянутая выше функциональность, кроме вывода чисел с плавающей точкой. Этот режим включен по умолчанию и каких либо опций для него указывать не надо. Поддержка чисел с плавающей точкой занимает много места, а нужна сравнительно редко. Приведенный выше пример, скомпилированный с этим уровнем функциональности, занимает порядка 1946 байт Flash памяти.
2. минимальный — поддерживаются только самые базовые вещи: целые, строки, символы. Флаги, ширина поля и точность, если они есть в форматной строке, разбираются корректно, но игнорируются, поддерживается только флаг «#». Пример, скомпилированный с этим уровнем функциональности, занимает порядка 1568 байт Flash памяти. Его вполне можно было-бы применить на контроллере с 2 Кб flash памяти. Включается указанием в командной строке компоновщика («Linker options» в AVRStudio, а не компилятора, как расплывчато указано в документации avr-libc) следующих опций:

-Wl,-u,vfprintf -lprintf_min

3. максимальный — полная функциональность, включая поддержку чисел с плавающей точкой. Включается опциями

-Wl,-u,vfprintf -lprintf_flt -lm

Скомпилированный пример занимает при этом 3488 байт.
Функции семейства printf из avr-libc не используют буферизацию, не считая буферов для конвертации чисел, и выводят символы в устройство по мере их обработки. Поэтому, если буферизация нужна, то ее можно реализовать самостоятельно в функции вывода символа, там можно реализовать и кольцевой буфер и все, что угодно. Также в этих функциях не используется динамическая память, что в нашем случае очень хорошо, зато активно используется стек. Попробуем определить максимальное использование стека в них. Для этого в приложенном архиве заходим в каталог AvrGccPrintf, компилируем проект посредством AVRStudio и запускаем симуляцию с помощью runSimul.cmd.

После открывает образовавшийся файл trace.log, находим значение указателя стека (SP) после входа в main (после пролога), находим минимальное значение SP (стек растёт вниз) и вычитаем из первого второе. У меня получилось 0x455 — 0x429 = 0x2c = 44 байта использует сама функция fprintf, плюс еще 8 байт в стеке занимают ее параметры, итого 52 байта. Еще 14 байт занимает один файловый дескриптор и ещё 6 байт три стандартных указателя на файловые дескрипторы (stdout, stdin, stderr). Итого 72 байта RAM только на вызов fprintf, без учета всего остального.
Также из файла trace.log можно узнать общее время выполнения функций и где процессор проводит его больше всего.

Подробнее о стандартной библиотеке ввода-вывода avr-libc можно здесь:
www.nongnu.org/avr-libc/user-manual/group__avr__stdio.html

IAR for AVR

Здесь есть несколько версий Си-шных библиотек, с отличающейся функциональностью:
— CLIB — относительно маленькая, но ограниченная библиотека. Нет поддержки файловых дескрипторов, локалей и многобайтных символов. Считается устаревшей.
— Normal DLIB — более новая. Так-же нет поддержки файловых дескрипторов, локалей и многобайтных символов, но есть некоторые плюшки из стандарта С99.
— Full DLIB — полная библиотека Си. Поддерживает всё согласно стандарту С99, но при этом очень объёмная. Рассматривать этот вариант не буду, так, как размер функции printf отсюда превышает доступные 4 Кб кода для бесплатной версии IAR KickStart for AVR.
В IAR имеется возможность выбирать возможности для printf. Для этого заходим с меню Project->Options далее в диалоге General Options->Library Configuration. В списке «Printf formatter» можно выбрать необходимый уровень функционала. Для CLIB это Large, Small, Tiny, для DLIB добавляется еще Full. По возможностям эти уровни примерно соответствуют аналогичным в avr-gcc, поэтому расписывать их не буду.

Для того чтобы printf заработала, надо определить функцию вывода символа:

int putchar(int outchar);

Разберём полный пример. Он также предназначен для запуска на симуляторе.

#define special_output_port (*((volatile char *)0x24)) #define special_exitcode_port (*((volatile char *)0x020)) #include <ioavr.h> #include <stdio.h> // для printf_P #include <pgmspace.h> __flash char str1[] ="Format str from flash\nlong = %15ld\nFlash string = %10S\n"; __flash char str2[] = "Hello"; int putchar(int outchar) { special_output_port = outchar; return outchar; } int main(void) { printf("Format str from ram\nlong = %15ld\nRam string = %10s\n", 1234567890l , "Hello"); printf_P(str1, 1234567890l , str2); special_exitcode_port = 0; return 0; }

Тут выясняются две неприятные особенности. Во-первых функции printf и printf_P видимо не разделяют общий код и printf_P всегда использует максимальный форматтер, независимо от того, что выбрали в настройках для printf, занимая порядка 3500 байт кода. Поэтому приведенный пример не помещается в четыре бесплатных килобайта. Для проверки одну из функций надо закомментировать.
Во-вторых, ни printf, ни printf_P не умеют читать строки из flash памяти — для них нет спецификатора.
Размеры printf для различных уровней функциональности примерно соответствуют аналогичным у avr-gcc, где чуть меньше, где чуть больше — непринципиально. А вот использование стека в разы выше, минимальный размер стека данных, при котором printf заработала, составил 200 байт для DLIB и около 150 для CLIB. Так, что на контроллерах с 2 кб flash, 128 RAM использовать эти функции не получится.
Демо-проект находится в каталоге AvrIarPrintf. Для запуска симуляции, а точнее преобразования генерируемого IAR-ом hex-а в пригодный для потребления simulavr-ом elf, на машине должен быть установлен WinAVR (и прописан в переменной окружения PATH, естественно).

Mspgcc

В стандартной библиотеке mspgcc для форматного вывода реализованы только функции printf и sprintf, плюс еще нестандартная функция uprintf, которая принимает указатель на функцию вывода символа в качестве первого аргумента. Файловых дескрипторов там нет и в помине, выбирать уровень функциональности форматтеров тоже нельзя. При этом printf «весит» порядка двух килобайт так, что запустить её, скажем, на MSP43 Launchpad-е не получится.
Для работы printf нужно, вполне ожидаемо, определить функцию int putchar(int c):

int putchar(int c) { // вывод символа ... return c; } 
IAR for MSP430, IAR for ARM и может еще какой IAR

Вообще в реализациях стандартной библиотеки от IAR Systems всё довольно однообразно для различных платформ, что не может не радовать. Как правило есть минимум две версии стандартной библиотеки — одна урезанная без поддержки файловых дескрипторов, вторая — полная, соответственно, с их поддержкой. Зовутся они как правило «Normal» и «Full» соответственно. Так-же в каждой из них можно выбирать необходимый функционал разбора форматной строки, поддерживаемый функциями семейства printf. Варианты уже уже описаны для IARfor AVR: Large, Small, Tiny и Full.
Если выбрать вариант библиотеки без файловых дескрипторов, то для работы функций вывода нужно определить лишь функцию int putchar(int outchar).
Если используем вариант с дескрипторами, то определить нужно функции __write, __lseek, __close и remove.
Минимальная их реализация, например, для STM32 может выглядеть так:

// Посылает один символ в USART1. static void Putch(uint8_t c) { if(c == '\n') Putch('\r'); while(( USART1->SR & USART_SR_TXE) == 0); USART1->DR = c; } // Вывод буфера в файл/поток, где // handle - номер открытого файла, // buf - буфер с данными, // bufSize - размер буфера, // возвращает количество фактически записанных символов. size_t __write(int handle, const unsigned char *buf, size_t bufSize) { for (int i = 0; i < bufSize; i++) { Putch(buf[i]); } return bufSize; } // Установка текущей позиции в потоке. // Нам сейчас не нужно. int __lseek(int handle, int ptr, int dir) {return 0; } // Закрыть поток. int __close(int handle) { return -1; } // Удалить файл. // Не знаю зачем printf требует определения этой функции. int remove(const char *fname) { return -1; } 
ARM + NewLib

Большинство сборок GCC под ARM используют NewLib в качестве стандартной библиотеки Си. Это достаточно взрослый проект и хорошо соответствует Си-шным стандартам, но при этом она относительно «тяжела» и требовательна к ресурсам. Для настройки библиотеки под свои нужды используется механизм системных вызовов. О нём уже немного писалось тут ispolzuem-libc-newlib-dlya-stm32, поэтому подробно на них останавливаться не буду, а упомяну об особенностях.
Первое это требования к памяти. Все stdio функции из NewLib используют хитрую буферизацию и динамически распределяют память для своих внутренних нужд. А значит им нужна куча и не маленькая, а в целых 3 Кбайт. Плюс примерно 1500 байт на статические структуры и плюс около 500 байт стека. Итого только чтоб напечатать «Hello, world!» нужно порядка 5 Кб оперативки. Что как-бы чуть больше, чем много для STM32-Discovery, на которой я запускал тестовый пример, с её 8 килобайтами. Также при использовании таких функций как printf по зависимостям тянется практически вся stdio плюс функции поддержки плавающей точки. В итоге тестовая прошивка занимает чуть меньше 30 Кб памяти программ. Если отказаться от использования чисел с плавающей точкой, то вместо printf можно использовать её облегченный аналог iprintf. В этом случае объём тестовой прошивки будет около 12 Кб.
Если какая либо из функций stdio не сможет выделить необходимую её память из кучи, то она тихонечко свалится в HardFault без объяснений причин.
Еще один момент это буферизация. Она может несколько запутать. Вызываем:

printf("Hello, World!");

И… Ничего не происходит. Хотя системные вызовы правильно определены, куча настроена, места в ней хватает.
Наш вывод был спокойно положен в буфер и ждет там либо пока буфер не заполнится, либо не будет принудительно сброшен. Сбросить этот буфер можно с помощью функции fflush, или послав на вывод символ новой строки.

printf("Hello, World!\n"); ... printf("Hello again!"); fflush(stdout); 

Режимом буферизации можно управлять с помощью функции setvbuf. Например, чтоб отключить буферизацию нужно сделать такой вызов до того, как в целевой поток был произведен какой либо вывод тыц:

setvbuf(stdout, NULL, _IONBF, 0); 

При этом потребление памяти кучи уменьшится более чем на 1.5 Кб.

Xprintf

Это реализация функций похожих на printf от товарища Chan-а. Качаем отсюда:
elm-chan.org/fsw/strf/xprintf.html
Библиотека содержит следующие функции для форматированного вывода — аналоги функций стандартной библиотеки Си:

void xputc (char c); void xputs (const char* str); void xfputs (void (*func)(unsigned char), const char* str); void xprintf (const char* fmt, ...); void xsprintf (char* buff, const char* fmt, ...); void xfprintf (void (*func)(unsigned char), const char* fmt, ...); 

Все функции написаны целиком на Си и их можно использовать практически на любом микроконтроллере. Однако они не учитывают особенностей некоторых МК, например, нет никакой поддержки строк во Flash памяти для семейства AVR, что не добавляет удобства использования. Для использования xprintf необходимо инийиализировать указатель xfunc_out, присвоив ему адрес функции вывода символа. Рассмотрим пример. Компилятор avr-gcc, проект AvrStudio, рассчитан на запуск в симуляторе simulavr.

#include <avr/io.h> // для PROGMEM и strncpy_P #include <avr/pgmspace.h> #include "xprintf.h" #define special_output_port (*((volatile char *)0x24)) #define special_input_port (*((volatile char *)0x25)) #define special_abort_port (*((volatile char *)0x22)) #define special_exitcode_port (*((volatile char *)0x020)) // функция вывода символа static void my_putchar(unsigned char c) { special_output_port = c; } // строки во Flash памяти PROGMEM char str1[] ="Format str from flash\nlong = %15ld\nFlash string = %10s\n"; PROGMEM char str2[] = "Hello form flash"; // буферы для временного копирования строк char buffer[60]; char buffer2[20]; __attribute((OS_main)) int main(void) { // инициализируем укзатель нашей функцией вывода символа xfunc_out = my_putchar; xprintf("Format str from ram\nlong = %15ld\nRam string = %10s\n", 1234567890l , "Hello"); xfprintf(my_putchar, "Hello, world!\n"); strncpy_P(buffer, str1, sizeof(buffer)); strncpy_P(buffer2, str2, sizeof(buffer2)); xprintf(buffer, 1234567890l , buffer2); special_exitcode_port = 0; return 0; }

Здесь для вывода строк из Flash памяти их приходится предварительно скопировать в оперативку.
Из достоинств этой библиотеки можно выделить компактность кода (~1600 байт для приведённого примера) и лёгкость использования на различных платформах и модифицировать. На этом достоинства заканчиваются. Из недостатков стоит отметить
относительно медленную работу, примерно в полтора-два медленнее чем стандартная printf из avr-libc, и несоответствие стандартам — не поддерживаются некоторые флаги (‘пробел’, ‘ #’, ‘+’ ), спецификаторы (i, p n), не считая флагов для чисел с плавающей точкой и т.д.
Потребление стека порядка 38 байт не считая аргументов.

Описание примеров.

В приложенном архиве находятся следующие примеры:

AvrGccPrintf

Проект для AvrStudio 4. Запускается на симуляторе Simulavr. Чтоб запустить нужно скомпилировать и выполнить скрипт «runSimul.cmd». В логе симуляции «trace.log» можно посмотреть времена выполнения функций и оценить расход стека.

AvrIarPrintf

Тоже, что и в предыдущем примере, только компилируется в IAR for AVR.

AvrXprintf

Проект для AvrStudio 4. Запускается на симуляторе Simulavr. Для вывода используется библиотека xprintf от ELM Chan.

Msp430GccPrintf

Коппилируется mspgcc. Проект для Code::Blocks. Запускать планировалось на MSP430 Launchpad с контроллером MSP430G2231. Пример должен считывать температуру со встроенного датчика и посылать ее через програмый usart. Но к сожалению функция printf из библиотеки mspgcc не помещается в имеющиеся 2 Кб памяти. По этому используется функция puts. которая выводит строчку «Hello, world!».

Msp430IarPrintf

IAR для MSP430. Запускается на MSP430 Launchpad. В отличии от mspgcc, минимальная версия printf умещается в 2Кб флеша MSP430G2231.

Stm32Format

GCC + NewLib. Работает на STM32 Discovery. собирается с помощью makefile. проверял с Yagarto и CodeSourcery.

IarArm

Работает на STM32 Discovery. Собирается с помощью IAR for ARM.

Итоги.

Преимущества использования стандартной библиотеки Си для форматного вывода:
— Стандартность. Этим всё сказано.
— Доступность. В каком-то виде есть практически в любом Си компиляторе.
— Разделение формата вывода и выводимых данных. Форматную строку легко вынести в отдельный файл/модуль, например для дальнейшей локализации.
— Хорошая функциональность.
Недостатки:
— Полностью стандартная реализация в большинстве случаев слишком «тяжела» для микроконтроллеров.
— Использование лишь одной функции, например printf, тянет за собой значительную часть библиотеки вывода, даже если реально используются только ограниченные возможности.
— Неаккуратное использование функций с форматной строкой может привести к трудно обнаруживаемым ошибкам кодирования.
— В каждом компиляторе используется какой-то свой способ для определения низкоуровневых функций вывода.

Понравилось? Поделитесь:

:

Sorry, the comment form is closed at this time.