Операционная система для микроконтроллеров с ядром AVR.
AvrOS v.1.0
Назначение.
Операционная система AvrOS v.1.0 для микроконтроллеров с ядром AVR предназначена для выполнения следующих функций:
Обслуживание задействованных встроенных и внешних устройств микроконтроллера.
Запуск задач на выполнение по некоторому событию. (Внешнее прерывание, прерывание от таймера и т.д.).
Поддержка протоколов связи различных уровней (MicroLAN, RS232 и т.п.).
Обслуживание задач реального времени.
Общая структура системы.
Операционная система условно подразделяется на несколько модулей (уровней):
Уровень аппаратного обеспечения (hardware level, HL). Файлы, относящиеся к этому уровню хранятся в каталоге ../hl. Данный уровень обеспечивает инициализацию и взаимодействие ПО со встроенными и внешними устройствами микроконтроллера. Все остальные уровни общаются с аппаратным обеспечением только через уровень HL.
Уровень задач реального времени (real-time level, RTL). Файлы, относящиеся к этому уровню хранятся в каталоге ../rtl. Данный уровень обеспечивает запуск задач реального времени (по событию от таймера, внешнего прерывания и т.п.). Задачи, запущенные в данном уровне наиболее приоритетные. Они не могут быть прерваны другими задачами. Исключение – когда задача уровня RTL запускает задачи более высоких уровней как процессы. Процессы (в отличии от задач RTL могут быть прерваны другими процессами и задачами, но имеют защиту от повторного вхождения). Программист так же может разрешить прерывания в задаче уровня RTL, но в этом случае он должен предусмотреть сам защиту от повторного вхождения в эту задачу.
Уровень операционной системы (operating-system level, OSL). Файлы, относящиеся к этому уровню хранятся в каталоге ../osl. Данный уровень обеспечивает запуск процессов не-реального времени. Но при этом запуск может быть осуществлен по событию с уровня RTL , например по таймеру. Например, уровень OSL обеспечивает поддержку высокоуровневых протоколов (например, уровень RTL обеспечивает прием-передачу пакетов по линии связи RS232, а уровень OSL обеспечивает разбор принятых пакетов и формирование ответа). Так же этот уровень содержит различные вызовы, не имеющие отношения к реального времени, например – получение кода нажатой клавиши, библиотеки вывода чисел и символов на индикатор, преобразования форматов.
Уровень программ пользователя (users program level, UPL). Файлы, относящиеся к этому уровню хранятся в каталоге ../upl. Этот уровень по приоритетам выполнения процессов абсолютно равноправен с уровнем OSL , но отделен от него, поскольку в каталоге ../upl находится программа пользователя. Пользователь так же может запускать свои задачи по тем же событиям и с теми же приоритетами, как и на уровне OSL. На уровне программ пользователя находится точка входа в программу void main().
Каталог ../system стоит особняком. В нем хранятся общесистемные настройки и макросы. (Например тактовая частота кварцевого генератора контроллера, параметры использования АЦП, таймеров и т.п.). Для каждого ресурса системы, за исключением памяти, должен быть определен разрешающий символ, иначе ресурс и все с ним связанное (макросы, процедуры и т.п.) просто не включаются в состав программы. Например, чтобы использовать таймер 0, достаточно в файле res_timers.h написать строчку: #define TIMER0_ENABLED. Этого достаточно, чтобы таймер 0 инициализировал ся и запустился. В этом же файле задаются остальные параметры таймера 0 (частота прерываний, делитель и т.п.). Для каждого ресурса (или нескольких сходных ресурсов) выделен свой файл с именем вида res_<имя модуля>.h, где <имя модуля> – название ресурса. Ресурсом может быть не только аппаратная, но и программная часть системы.
Приведем назначение файлов описания ресурсов:
Уровень HL.
res_pwm.h - описание настроек модуля поддержки ШИМ (на таймере1)
res_uart.h - описание настроек модуля поддержки UART.
res_adc.h - описание настроек модуля поддержки АЦП.
res_ints.h - описание настроек модуля поддержки внешних прерываний.
res_timers.h - описание настроек модуля поддержки встроенных таймеров (реализована пока для таймера 0, используемого в качестве системного таймера).
Уровень RTL.
На данный момент содержит изменяемых ресурсов.
Уровень OSL.
На данный момент содержит изменяемых ресурсов.
Уровень UPL.
Уровень пользователя не может содержать ресурсов системы.
Каталог system.
sysdef.h - описание некоторых параметров системы (тактовой частоты кварца, например).
файлы res_<имя модуля>.h – описания всех модулей системы.
Добавление нового модуля в систему.
Программист-пользователь AVROS может добавлять в нее новые модули. Рассмотрим, как это делается.
Предположим, нам надо ввести поддержку некоего модуля ШИМ. Это несложно.
Шаг1. Принимаем решение на каком уровне будет размещен модуль. Если модуль напрямую взаимодействует с аппаратурой – то он должен располагаться в каталоге hl (уровень аппаратного обеспечения). В нашем случае это так и есть.
Шаг2. Пишем драйвер, который сохраним в файле hl/userpwm.c. Общая структура файла hl/userpwm.c следующая:
#ifdef USERPWM_ENABLED
// код драйвера
void userpwm_init(){/*код инициализации*/}
....
#endif /* USERPWM_ENABLED */
Шаг2. Пишем заголовочный файл hl/userpwm.h. Этот файл – обязателен. В нем должен быть определен макрос userpwm_init_mac(). Это следует делать следующим образом.
#ifdef USERPWM_ENABLED
// Ссылка на процедуру инициализации модуля
#define userpwm_init_mac() userpwm_init()
// Все определения для драйвера, которые постоянны
...
#else
#define userpwm_init_mac()
#endif /* USERPWM_ENABLED */
Макрос userpwm_init_mac() испоьлзуется для автоматического вызова процедуры инициализации драйвера. Если даже драйвер не используется или процедура инициализации не нужна, то этот макрос все равно должен быть определен, хоть он и пустой. Общее правило определения имени макроса начальной инициализации
#define <имя_модуля>_init_mac() <Имя процедуры инициализации>(), где
<имя_модуля> - имя *.h файла, который описывает модуль. (В нашем случае userpwm, т.к. файл называется userpwm.h).
<Имя процедуры инициализации> - имя процедуры инициализации, описанной в файле с кодом драйвера (В нашем случае это файл userpwm.c, процедура void userpwm_init()).
Шаг 3. Создаем файл system/res_userpwm.h. В этом файле обязательно в начале должна быть строчка #define USERPWM_ENABLED. Если эту строчку закомментировать, то все, связанное с нашим драйвером не будет включено в код программы. Таким образом, чтобы добавить или удалить модуль из программы, достаточно раскомментировать или закомментировать всего одну строчку в файле res_<имя модуля>.h в каталоге system. Имя файла обязательно должно иметь формат: res_<имя_модуля>.h, поскольку файл добавляется в систему автоматически. Все файлы в каталоге system, имеющие формат имени res_<имя_модуля>.h при компиляции системы воспринимаются, как заголовочные файлы модулей и будут включены в систему.
Общая структура файла system/res_userpwm.h следующая:
#define USERPWM_ENABLED /* Закомментировать для отключения модуля */
#ifdef USERPWM_ENABLED
// Описание всех определений, которые часто изменяются (например разрядность ШИМ,
// частота ШИМ в нашем случае и т.п.)
#endif /* USERPWM_ENABLED */
Шаг 4. Открываем Makefile. Там есть строчки примерно такого вида
# Hardware level
HL-SRC = timer0.c int0.c adc0.c uart0.c pwm.c
HL-OBJ = hl.o
# Real-Time Level
RTL-SRC = rt_task.c
RTL-OBJ = rtl.o
# Operating-System Level
OSL-SRC = syscall.c
OSL-OBJ = osl.o
# User Program Level
UPL-SRC = main.c
UPL-OBJ = upl.o
Поскольку наш новый модуль находится в каталоге hl, то нетрудно догадаться что для
его добавления надо изменить строчку
HL-SRC = timer0.c int0.c adc0.c uart0.c pwm.c
на
HL-SRC = timer0.c int0.c adc0.c uart0.c pwm.c userpwm.c
То есть просто добавить имя файла с исходными текстами нашего нового модуля в список файлов с исходными текстами того уровня, где расположен наш модуль.
Вуаля ! Можете компилировать вашу новую систему и наслаждаться.
Вызов процедур пользователя по системному таймеру.
После запуска системы прерывания всегда разрешены и периодически приходит прерывания от системного таймера. Пользователь может добавлять процедуры своих модулей для вызова их по прерыванию. Для этого надо открыть файл rtl_mac.h (для модулей уровней upl и osl) или hl_mac.h (для модулей уровня rtl), раскомментировать в нем одну из строчек и добавить в нее имя своей процедуры, которая должна вызываться по таймеру.
Для модулей уровней upl и osl доступны следующие периоды вызова процедур:
- 0.1 сек (вид #define UPL_TASK_100MS_xx или #define OSL_TASK_100MS_xx)
- 1сек (вид #define UPL_TASK_1S_xx или #define OSL_TASK_1S_xx)
- 1минута (вид #define UPL_TASK_1M_xx или #define OSL_TASK_1M_xx)
- 1час. (вид #define UPL_TASK_1H_xx или #define OSL_TASK_1H_xx)
Для модулей уровня rtl доступны следующие периоды вызова процедур:
- FMAX (вид #define T_INT_MAX_xx )
- 0.1 сек (вид #define T_INT_100MS_xx)
- 1сек (вид #define T_INT_1S_xx)
- 1минута (вид #define T_INT_1M_xx)
- 1час. (вид #define T_INT_1H_xx)
Частота FMAX – максимально возможная частота прерываний таймера. (определяется как F_CLK (частота кварца) деленная на количество тактов, через которые таймер посылает прерывания. Или FMAX= F_CLK/TIMER0_DEVIDER. Параметр F_CLK должен быть корректно указан в файле system/sysdef.h, а параметр TIMER0_DEVIDER – в файле system/res_timers.h.
Процедуры защищены от повторного вхождения, но для корректной работы системы все же лучше соразмерять время выполнения процедуры, вызываемой по таймеру с периодом ее вызова.
Приведем пример как создать эффект “бегущий огонь” на плате NedoPC-90.8535.
Заходим в каталог upl.
Пишем файл main.c (или изменяем его):
//----------------------------------------------------------------------
#define LEDPORT PORTB /*светодиодный порт*/
#define LEDDDR DDRB /*регистр управления этим портом*/
//----------------------------------------------------------------------
char sled = 0x01; // 1 в том бите, где зажжен светодиод
// Процедура, которая будет вызываемой по таймеру с периодом 1сек
void ttask_leds_shift()
{sled = sled << 0x01; // Сдвигаем 1 влево на 1 разряд
if(sled > 0x0F){sled=0x01;} // Если 1 вышла за пределы младших 4х бит, возвращаем ее на место
output(LEDPORT,(input(LEDPORT) | 0x0F) & (~sled));_NOP();_NOP(); // Изменяем состояния порта
}
//----------------------------------------------------------------------
// Основная программа
int main() {AUTOINIT_ALL_MODULES();
// Инициализируем светодиодный порт
output(LEDPORT,input(LEDPORT) | 0x0F);_NOP();_NOP();
output(LEDDDR,input(LEDDDR) | 0x0F);_NOP();_NOP();
//
while(1){}
//
return(0);
}
//----------------------------------------------------------------------
Пишем файл main.h (или изменяем его):
//----------------------------------------------------------------------------
// File: main.h
// Contain headers for main.c
//----------------------------------------------------------------------------
void ttask_leds_shift();
//----------------------------------------------------------------------------
Заметьте, что имя процедуры, вызываемой по прерыванию, ОБЯЗАТЕЛЬНО должно быть указано в заголовочном файле. Иначе – ошибка компиляции.
Пишем файл rtl_mac.h (или изменяем его):
//----------------------------------------------------------------------
// Макросы вызова процедур по таймеру уровня UPL
//----------------------------------------------------------------------
// 100ms
//#define UPL_TASK_100MS_0()
//#define UPL_TASK_100MS_1()
//#define UPL_TASK_100MS_2()
//#define UPL_TASK_100MS_3()
//----------------------------------------------------------------------
// 1s
#define UPL_TASK_1S_0() ttask_leds_shift()
//#define UPL_TASK_1S_1()
//#define UPL_TASK_1S_2()
//#define UPL_TASK_1S_3()
//----------------------------------------------------------------------
// 1minute
//#define UPL_TASK_1M_0()
//#define UPL_TASK_1M_1()
//#define UPL_TASK_1M_2()
//#define UPL_TASK_1M_3()
//----------------------------------------------------------------------
// 1hour
//#define UPL_TASK_1H_0()
//#define UPL_TASK_1H_1()
//#define UPL_TASK_1H_2()
//#define UPL_TASK_1H_3()
//----------------------------------------------------------------------
Можете компилировать вашу новую систему и наслаждаться тем, как красиво бегает огонек.
Старт уровня пользователя.
Программы уровня пользователя должны стартовать после инициализации всех модулей более нижних уровней. Для того, чтобы это было так, самой первой строчкой в процедуре int main(); файла upl/main.c должна быть строчка AUTOINIT_ALL_MODULES(); то есть вызов макроса инициализации всех системных уровней.
Приведем пример процедуры int main();
int main()
{AUTOINIT_ALL_MODULES();
// Код программы пользователя
.....
// конец программы пользователя
return(0);
}
Для каждого файла *.c должен существовать заголовочный файл *.h, даже если он пустой. (Например для файла upl/main.c должен существовать файл upl/main.h.)