Программирование, радиоэлектроника,
саморазвитие и частичка из моей жизни здесь...

AVR Си - выносим функции и переменные в модули, компиляция нескольких файлов в avr-gcc, Makefile

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

Содержание

  1. Зачем программу для AVR C разбивать на модули
  2. Выносим функции и константы в отдельный модуль, простой пример
  3. Компиляция и прошивка программы состоящей из нескольких Си-файлов в МК
  4. Настройка Geany для компиляции нескольких C-файлов
  5. Makefile для проектов на AVR C
  6. Примеры работы с Makefile
  7. Настройка Geany для работы с Makefile
  8. В звершение

Зачем программу для AVR C разбивать на модули

Если вы только начали знакомиться с программированием AVR микроконтроллеров (МК) на языке Си, то ваши простейшие программы, скорее всего, состоят из одного файла с исходным кодом, например: main.c.

В этом файле у вас содержатся директивы для подключения внешних заголовочных файлов *.h, определения значений для констант, переменных, реализации функций и код главной функции main().

Код такой программы размещен в одном файле, он не большой по размеру, легко читается, в него легко вносить изменения и дополнения. Как правило, такая простая программа выполняет простейшие функции, служит для обучения и экспериментов.

При разработке реального проекта, в котором задействовано множество портов МК, различные электронные датчики, устройства обмена и отображения информации, программный код получится достаточно объемный, его целесообразно разбить на логические части, распределить по отдельным модулям и файлам.

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

Выносим функции и константы в отдельный модуль, простой пример

В статье Простая программа для AVR микроконтроллера на языке Си был приведен пример очень простой программы, ее код содержится всего в одном файле с расширением ".c".

Немного оптимизированный и более наглядный код программы приведен в комментарии #13 к этой же статье. Сейчас мы переделаем этот код таким образом, что константы и функции будут вынесены в отдельный модуль.

Теперь программа будет состоять из трех файлов:

  • main.c - основной файл программы;
  • library.c - библиотека, содержащая реализацию функций, которые будут использоваться в основной программе;
  • library.h - заголовочный файл библиотеки, содержит список функций, служебные константы.

Файлы "library.c" и "library.h" - это и есть наша библиотека, которая будет подключена к основной программе с помощью директивы "#include".

Содержание файла "main.c":

/* Светодиодная мигалка на микроконтроллере ATmega8.
 * https://ph0en1x.net 
 */
#include <avr/io.h>

// Включаем заголовочный файл библиотеки.
#ifndef LIBRARY
    #define LIBRARY
    #include "library.h"
#endif

// Основная функция программы.
void main(void) {
    port_setup(PD0, PD1);
    while (1) { 
        led_blink(PD0);
        led_blink(PD0);
        delay_2();
        led_blink(PD1);
        led_blink(PD1);
    }
}

Строкой "#include <avr/io.h>" подключается заголовочный файл "io.h" из установленной в системе библиотеки avr-libc, в нем содержится набор IO (Input Output) макроопределений для различных типов микроконтроллеров, определяемых опцией "-mmcu=X" при компиляции программы.

Строка "#ifndef LIBRARY" содержит директиву условной компиляции, которая проверяет объявлена ли константа с именем "LIBRARY" и в случае если это так - выполняет код внутри блока до директивы "#endif". Внутри этого блока кода мы объявляем константу "LIBRARY" и подключаем заголовочный файл библиотеки.

Данная директива в этом файле приведена для примера, здесь можно было бы обойтись и без нее (у нас библиотека включается в проект только один раз). Подобное подключение библиотеки целесообразно в случае если необходимо использовать ее в нескольких разных модулях проекта.

Эта конструкция кода позволит исключить повторное включение файла "library.h" в проект и тем самым избежать ошибки в процессе компиляции, вызванной повторным включением одного и того же кода библиотеки.

Дальше идет реализация основной функции "main()", которая будет запущена при старте программы. В ней мы используем функции из созданной нами библиотеки, а также константы "PD0" и "PD1" полученные из включения файла "io.h".

Думаю вы заметили здесь две немножко отличающиеся формы использования директивы "#include":

#include <avr/io.h>
#include "library.h"

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

Если путь к файлу взят в скобки <library.h>, то поиск файла для включения в код будет выполнен сперва в глобальных папках, которые прописаны в среде разработке или указаны компилятору.

А если путь указан в двойных кавычках "library.h", то поиск файла будет осуществляться сперва в локальных, по отношению к проекту, папках.

В приведенном выше примере сперва выполняется поиск и подключение файла "io.h", он должен находиться в папке "avr" относительно глобальной директории в которой будет выполняться поиск.

Давайте найдем реально размещение файла "io.h", это можно сделать командой:

locate io.h | grep avr

Получим примерно следующий список:

/usr/lib/avr/include/avr/io.h
/usr/lib/avr/include/stdio.h
/usr/share/doc/avr-libc/avr-libc-user-manual/group__avr__io.html
/usr/share/doc/avr-libc/avr-libc-user-manual/group__avr__stdio.html
/usr/share/man/man3/io.h.3avr.gz
/usr/share/man/man3/stdio.h.3avr.gz

"/usr/lib/avr/include/" - это и есть глобальная директория, в которой компилятор avr-gcc ищет заголовочные файлы для включения. Можете изучить ее и убедиться в том, что там собрано достаточно много различных библиотек.

Вернемся к нашей программе. Дальше в коде ми подключаем файл "library.h", который нами же был написан и размещен в директории проекта вместе с основным файлом программы "main.c".

Если бы в директории проекта била создана дочерная папка 'modules' и файл "library.h" был размещен в ней, то включение этого файла в код нужно было бы выполнять вот так:

#include "modules/library.h"

Позже, можете попробовать изменить "library.h" на <library.h> - и при попытке скомпилировать ее увидите что из этого получится.

При использовании компилятора AVR-GCC нужно запомнить следующее:

  • #include <путь> - компилятор ищет файл в установленных для него путях, они могут быть вшиты по умолчания или же заданы с помощью опции "-I" в командной строке;
  • #include "путь" - компилятор ищет файл сначала в папке относительно проекта, а потом в глобальных путях (там же где и #include <путь>).

Содержание файла "library.h":

#include <avr/io.h>

// Константа, задающая частоту МК, используется в функциях _delay_ms .
#define F_CPU 1000000UL

#include <util/delay.h>

// Значения временных задержек для мигания светодиодов.
#define DELAY_MS_1 100
#define DELAY_MS_2 700

// Прототипы функций (возвращаемое значение, название, параметры).
static void delay_1(void);
void delay_2(void);
void led_blink(unsigned int pin_num);
void port_setup(int port_1, int port_2);

Это заголовочный файл, который содержит описание библиотеки, ее функций, констант.

Подключаем "io.h", поскольку в функциях библиотеки будет идти работа с портом и связанными с ним константами для текущего МК.

Директива "#include <util/delay.h>" подключает модуль из которого мы будем использовать всего лишь одну функцию "_delay_ms".

Но почему же она размещена после объявления константы "F_CPU"? - вы можете попробовать разместить директиву включения "delay.h" сразу после директивы с включением "io.h" и посмотреть на результат, компиляция завершится с ошибкой:

warning: #warning "F_CPU not defined for <util/delay.h>"

Дело в том что функция, объявленная в "delay.h" требует установленного значения константы "F_CPU", которое говорит сколько тактов делаем микропроцессор за одну секунду (частота), например 1000000 - означает частоту 1МГц. На основе значения этой константы функция "_delay_ms" делает расчет количества тактов для реализации необходимой временной задержки.

Константы "DELAY_MS_1" и "DELAY_MS_2" содержат числовые значения задержки по времени в миллисекундах, которые используются во внутренних функциях.

В конце файла перечислены все прототипы функций библиотеки с указанием возвращаемого значения, имени и аргументов. Эти прототипы можно сравнить с фасадами магазинов, в которых внутри размещено много различных вещей.

Зачем здесь нужны эти прототипы функций? - в языке Си код выполняется в том порядке, в котором мы его задаем. Если ми написали реализацию двух функций А и Б, причем реализация Б идет после А, то при попытке вызвать функцию А в которой внутри вызывается функция Б компилятором будет выведена ошибка с сообщением о том, что функция Б не найдена.

И это верно, потому что мы пытаемся вызвать функцию, реализация которой следует в коде позже - после вызываемой нами функции А.

Ну а теперь представьте себе что у вас 10 функций и причем в каждой из них перекрестно вызывается другая соседняя, в каком порядке в коде размещать реализации функций чтобы все заработало? - в некоторых случаях такой комбинации может и не существовать, придется сливать некоторые функции в одну, дублировать код...

Здесь нам и приходят на помощь объявления прототипов функций, они говорят компилятору что данные функции готовы к использованию в любом из  последующих участков кода.

Содержание файла "library.c":

#include "library.h"

void port_setup(int port_1, int port_2) {
	DDRD |= (1 << port_1);
    DDRD |= (1 << port_2);
}

void led_blink(unsigned int pin_num) {
	PORTD |= (1 << pin_num);
	delay_1();
	PORTD &= ~(1 << pin_num);
	delay_1();
}

static void delay_1(void) {
	_delay_ms(DELAY_MS_1);
}

void delay_2(void) {
	extern int delay_ms_leds;
	_delay_ms(DELAY_MS_2);
}

Этот файл содержит реализацию функций нашей библиотеки. В начале подключается файл "library.h", в нем мы раньше определили необходимые константы и описали реализуемые в библиотеке функции.

Дальше идет определение и реализация четырех функций:

  • port_setup - настройка каналов порта;
  • led_blink - мигание светодиодом на пине, связанном с каналом порта (включение и выключение с задержкой);
  • delay_1 - задержка по времени, используемая в функции led_blink;
  • delay_2 - задержка по времени, используемая в основной программе.

Здесь важно обратить внимание на объявление функции "static void delay_1(void)", а именно на ключевое слово static - оно дает компилятору понять что функция должна быть доступна к вызову только внутри библиотеки. Если попробовать ее вызвать из файла "main.c" то получим ошибку:

warning: ‘delay_1’ used but never defined

Остальные функции, которые описаны и содержатся в этом модуле будут доступны к использованию основной программе и другим модулям внутри ее.

Компиляция и прошивка программы состоящей из нескольких Си-файлов в МК

Итак, вместо одного файла с исходным кодом на Си у нас теперь три файла. Сейчас я покажу как их скомпилировать и прошить в AVR микроконтроллер, здесь все не намного сложнее чем в ситуации с одним файлом.

Для компиляции исходного кода в объектный файл в одной из прошлых статей была использована команда вида:

avr-gcc -mmcu=atmega8 -Os leds_blinking.c -o leds_blinking.o

Это пример для программы, исходный код которой содержится всего лишь в одном файле "leds_blinking.c". Для компиляции здесь указан к использованию микроконтроллер ATMega8.

В случае с тремя файлами "main.c", "library.c", "library.h" команда компиляции будет выглядеть следующим образом:

avr-gcc -mmcu=atmega8 -Os main.c library.c -o main.o

Все что изменилось в логической структуре команды - мы добавили имя файла "library.c" к списку аргументов сразу же после указания "main.c". Таким образом, код этих файлов будет скомпилирован в один общий объектный файл, функции из библиотеки "library.c" станут доступными к использованию в программе из файла "main.c".

Генерируем файл прошивки для МК в формате формате Intel Hex:

avr-objcopy -j .text -j .data -O ihex main.o main.hex

Записываем прошивку в микроконтроллер ATMega8 используя программатор USBAsp:

avrdude -c usbasp -p m8 -P usb -U flash:w:main.hex

Настройка Geany для компиляции нескольких C-файлов

Для успешной компиляции написанной нами программы из трех файлов, в среде программирования Geany нужно сделать некоторые исправления.

Выбираем пункт меню "Build" - "Set Build Commands" и прописываем следующие команды сборки и прошивки, как указано ниже.

C commands:

  • 1. Compile - "avr-gcc -mmcu=atmega8 -Os main.c library.c -o main.o"
  • 2. Build - "avr-objcopy -j .text -j .data -O ihex  main.o  main.hex"

Execute commands:

  • 1. Execute - "avrdude -c usbasp -p m8 -P usb -U flash:w:main.hex"

 Настройка команд компиляции и прошивки программы для AVR микроконтроллера в среде Geany

Рис. 1. Настройка команд компиляции и прошивки программы для AVR микроконтроллера в среде Geany.

Как видите все достаточно просто и удобно. Но это только если не учитывать того факта, что при работе над несколькими различными программами в среде программирования Geany придется постоянно прописывать индивидуальные настройки для каждой из них. 

Чтобы убрать это неудобство как раз самое время познакомиться с таким инструментом как Makefile.

Makefile для проектов на AVR C

GNU Makefile - это файл, содержащий набор инструкций для программы 'make', который в сочетании с ней позволяет выполнять преобразования файлов из одного формата в другой (компиляция, извлечение какой-то информации), запускать пользовательские программы с параметрами в процессе обработки файлов.

В нашем случае GNU Makefile и программа 'make' помогут нам автоматизировать цепочку "компиляция-сборка-прошивка". Также это даст нам в руки дополнительные удобные инструменты, для вызова которых нужно всего лишь передать в качестве аргумента для команды 'make' один из параметров.

Ниже приведен пример не сложного Makefile, в котором я собрал различные полезные инструменты для сборки и работы с проектом на основе AVR-GCC и AVRDUDE.

# Название: Makefile
# Автор: ph0en1x
# Копирайт: https://ph0en1x.net
# Лицензия: MIT

# Название проекта.
# Имя основного С-файла без расширения, пример для 'project1.c': 'project1'.
PROJECT = main

# Тип чипа для AVR GCC и частота ядра. 
# https://gcc.gnu.org/onlinedocs/gcc/AVR-Options.html
GCC_MCU = atmega8
CLOCK_HZ   = 1000000

# Опции для AVRDUDE.
# https://ph0en1x.net/77-avrdude-full-howto-samples-options-gui-linux.html
AVRDUDE_MCU             = m8
AVRDUDE_PROGRAMMER      = usbasp
AVRDUDE_PROGRAMMER_PORT = usb

# Fuses
FUSE_L = 0xe1
FUSE_H = 0xd9
FUSE_E = 0xff

# Список дополнительных C-файлов для компиляции (указывать через пробел).
C_FILES = library.c


# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
CFLAGS        = -g -Os -Wall -mcall-prologues -std=c99 -mmcu=$(GCC_MCU) -DF_CPU=$(CLOCK_HZ)UL
FUSES         = -U lfuse:w:$(FUSE_L):m -U hfuse:w:$(FUSE_H):m -U efuse:w:$(FUSE_E):m
FLASH         = -U flash:w:$(PROJECT).hex

AVR_GCC       = `which avr-gcc`
AVR_OBJCOPY   = `which avr-objcopy`
AVR_SIZE      = `which avr-size`
AVR_OBJDUMP   = `which avr-objdump`
AVRDUDE       = `which avrdude`
REMOVE        = `which rm`
NANO          = `which nano`
TAR           = `which tar`
DATETIME      = `date +"%d-%m-%Y"`

AVRDUDE_CMD   = $(AVRDUDE) -p $(AVRDUDE_MCU) -c $(AVRDUDE_PROGRAMMER) -P $(AVRDUDE_PROGRAMMER_PORT) -v

%.elf: %.c
	$(AVR_GCC) $(CFLAGS) $< $(C_FILES) -o $@

%.hex: %.elf
	$(AVR_OBJCOPY) -R .eeprom -O ihex $< $@

all: clean elf hex

program: $(PROJECT).hex
	$(AVRDUDE_CMD) $(FLASH)

fuses:
	$(AVRDUDE_CMD) $(FUSES)

elf: $(PROJECT).elf

hex: $(PROJECT).hex

size: $(PROJECT).elf
	$(AVR_SIZE) $(PROJECT).elf

disasm: $(PROJECT).elf
	$(AVR_OBJDUMP) -d $(PROJECT).elf

clean:
	$(REMOVE) -f *.hex *.elf *.o

edit:
	$(NANO) $(PROJECT).c

tar:
	$(TAR) -zcf $(PROJECT)_$(DATETIME).tgz ./* 

Описывать синтаксис и принципы построения Makefile я здесь не буду, поскольку это отдельная и достаточно обширная тема. Если у вас есть опыт программирования и работы в консоли GNU/Linux, то разобраться что и как здесь работает не составит особого труда.

Подробно о построении своего Makefile для сборки программы, а также о работе с программой 'make' вы можете почитать в официальном мануале от Free Software Foundation - GNU Make Manual.

В верхней части файла находятся различные опции, которые можно изменить по своему усмотрению под свой тип МК, программатор и проект.

Все настройки в файле сейчас адаптированы для сборки и прошивки программы, которая была рассмотрена выше. В списке дополнительных файлов для компиляции указан файл библиотеки "library.c". Прошиваться будет микроконтроллер ATMega8 с использованием программатора 'usbasp' через порт 'usb'.

Опция 'CLOCK_HZ' указывает компилятору частоту ядра МК в Герцах, это число может быть переопределено внутри программы с помощью директивы вида "#define F_CPU 1000000UL" (суффикс UL - означает Unsigned Long, тип значения).

Значения байтов Fuses приведены те, которые установлены по умолчанию в микроконтроллере ATMega8. Обязательно измените их под свой проект и будьте осторожны с установкой фьюзов!

Файл Makefile должен располагаться в той же папке, где находится основной файл исходного кода на языке Си со всеми библиотеками и ресурсами.

Примеры работы с Makefile

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

Перед использованием 'make' нужно перейти в директорию где располагается проект с файлом Makefile. Пусть это будет папка '/home/master/avr-test', выполним в консоли следующую команду:

cd /home/master/avr-test

Теперь приведу примеры команд и описание того что они делают и для чего служат.

Компилируем программу, получаем на выходе объектный файл (*.c -> *.elf):

make elf

Извлекаем нужные области данных из объектного файла и строим файл с прошивкой (*.elf -> *.hex):

make hex

Очистка промежуточных и результирующих файлов в проекте (*.hex *.elf *.o):

make clean

Цепочка операций по умолчанию (очистка + компиляция + генерация hex):

make

Данная команда по результату аналогична следующей:

make clean elf hex

Смотрим размеры секций с данными в объектном файле (*.elf):

make size

Примерный вывод команды:

   text    data     bss     dec     hex filename
    216       0       0     216      d8 main.elf

Дизассемблируем объектный файл (*.elf):

make disasm

Пример вывода:

main.elf:     file format elf32-avr
Disassembly of section .text:

00000000 <__vectors>:
   0:   12 c0           rjmp    .+36            ; 0x26 <__ctors_end>

.........<много кода>...........

000000ae <main>:
  ae:   61 e0           ldi     r22, 0x01       ; 1
  b0:   70 e0           ldi     r23, 0x00       ; 0
  b2:   80 e0           ldi     r24, 0x00       ; 0
  b4:   90 e0           ldi     r25, 0x00       ; 0
  b6:   c0 df           rcall   .-128           ; 0x38 <port_setup>
  b8:   80 e0           ldi     r24, 0x00       ; 0
  ba:   90 e0           ldi     r25, 0x00       ; 0
  bc:   d1 df           rcall   .-94            ; 0x60 <led_blink>
  be:   80 e0           ldi     r24, 0x00       ; 0
  c0:   90 e0           ldi     r25, 0x00       ; 0
  c2:   ce df           rcall   .-100           ; 0x60 <led_blink>
  c4:   ea df           rcall   .-44            ; 0x9a <delay_2>
  c6:   81 e0           ldi     r24, 0x01       ; 1
  c8:   90 e0           ldi     r25, 0x00       ; 0
  ca:   ca df           rcall   .-108           ; 0x60 <led_blink>
  cc:   81 e0           ldi     r24, 0x01       ; 1
  ce:   90 e0           ldi     r25, 0x00       ; 0
  d0:   c7 df           rcall   .-114           ; 0x60 <led_blink>
  d2:   f2 cf           rjmp    .-28            ; 0xb8 <main+0xa>

000000d4 <_exit>:
  d4:   f8 94           cli

000000d6 <__stop_program>:
  d6:   ff cf           rjmp    .-2             ; 0xd6 <__stop_program>

Прошиваем HEX-файл в память микроконтроллера:

make program

Если программа не была скомпилирована и не был HEX-файл не был создан, то это будет сделано автоматически перед запуском процесса прошивки.

Если же файлы *.elf и *.hex уже есть в папке проекта то будет запущена команда прошивки в МК уже существующего файла *.hex.

Открываем основной файл проекта с исходным кодом в консольном редакторе 'nano':

make edit

Для выхода из редактора nano нужно нажать комбинацию клавиш CTRL + X.

Чтобы выполнить компиляцию и прошивку программы, исходный код которой был изменен нужно выполнить очистку старых файлов (*.elf и *.hex), а потом уже наново выполнить сборку и прошивку. Эти операции можно выполнить всего лишь одной командой:

make clean program

Запакуем все файлы проекта в архив (со сжатием GZip) для резервирования или пересылки:

make tar

Имя файла будет содержать название проекта (из переменной PROJECT в Makefile), а также строчку с текущей датой, например: main_24-09-2018.tgz.

Прошивка фьюзов в МК (будьте предельно осторожны с этой командой):

make fuses

Значения Fuse-байтов для прошивки прописаны в шапке Makefile в переменных:

  • FUSE_L - младший fuse-байт (Lower);
  • FUSE_H - старший fuse-байт (Higher);
  • FUSE_E - дополнительный fuse-байт.

Как видите, легко запоминающийся набор команд творит магию - выполняет всю рутинную работу по компиляции, сборке и прошивке программы в МК, предоставляет дополнительные полезные инструменты.

Настройка Geany для работы с Makefile

Используя подобный Makefile для своих проектов можно очень просто перенастроить среду программирования Geany для работы с различными проектами без постоянной корректировки строчек с командами сборки и прошивки.

Вся магия будет выполняться в Makefile, а в Geany мы будем вызывать лишь уже стандартизированные команды, которые указаны в предыдущем разделе.

Переходим по пунктам меню "Build" - "Set Build Commands" и изменяем строчки с настройками на указанные ниже.

C commands:

  • 1. Compile - "make" (также можно использовать команду "make clean elf hex")
  • 2. Build - "make clean hex" (результат будет тот же, что и в предыдущей команде)

Independent commands:

  • 1. Get Size - "make size"
  • 2. Disasm - "make disasm"
  • 3. Pack project - "make tar"
  • 4. Write FUSES - "make fuses"

Execute commands:

  • 1. Execute - "make clean program"

Вот как это выглядит в окне настроек Build в Geany:

 Настройки Geany для программирования AVR микроконтроллеров с использованием Makefile

Рис. 2. Настройки Geany для программирования AVR микроконтроллеров с использованием Makefile.

Теперь для работы можно использовать следующие кнопки и пункты меню:

Кнопки и пункты меню в Geany для компиляции, сборки и обработки программы для AVR МК

Рис. 3. Кнопки и пункты меню в Geany для компиляции, сборки и обработки программы для AVR МК.

  • 1 - компиляция программы (получение файлов *.elf и *.hex);
  • 2 - аналогично пункту 1;
  • 3 - прошивка микроконтроллера;
  • 4 - меню с набором дополнительных команд: Get Size, Disasm, Pack project, Write FUSES.

Для пересборки и прошивки программы в МК достаточно нажать на кнопку 3. Просто и удобно!

В завершение

Архив с примером программы где используется разделение исходного кода и Makefile - Скачать (2,3КБ Zip).

После небольшой подготовки и настройки своего Makefile можно приступать к реализации уже более сложного проекта, который состоит из нескольких файлов с исходным кодом.

Удачи вам в ваших проектах!

Комментарии к публикации (2):
Professor #1Professor
01 Октябрь 2018 22:17

Спасибо вам за проделанную работу

0
ph0en1x #2ph0en1x
02 Октябрь 2018 12:18

Пожалуйста! И вам спасибо за отзыв Smile

0