(Фрагмент [[http://panchul.livejournal.com/184647.html | курса аппаратного программирования]] от Юрия Панчула) ====== Часть 5. Программирование на голом железе и зачатие операционной системы ====== (сырой материал) - Лаба - знакомство с платой chipKit32 / PIC32 / MIPS и программирование её на С с помощью Arduino-подобного GUI. Кнопочки, лампочки, дисплейчик, IO Shield. - Введение в ассемблер на примере MIPS - Лаба - загрузка в память программок на ассемблере - Концепция простейшей многозадачной операционной системы - Лаба - своя игрушечная многозадачная ОС, которая бутится, ставит обработку прерывания по таймеру, в котором переключает задачи по схеме round-robin. ===== 5.1. Знакомство с платой Uno32 ===== Лабораторная работа: знакомство с платой Uno32. Процессор PIC32 с архитектурой MIPS32. Простая среда разработки chipKit MPIDE и программирование на С. Подключение кнопочек и семисегментного индикатора с помощью макетной платы. Концепция прерывания. Необходимое оборудование: * Плата Uno32 * Адаптер PICkit 3 (начиная с раздела 5.3) * Компьютер с Linux или Windows * Кабель mini-USB * Универсальная макетная плата * Две кнопки с проводами * Семисегментный светодиодный индикатор * Девять резисторов 220 Ом с проводами Пример 1: управление двумя светодиодами, имеющимися на плате Uno32, с помощью двух внешних кнопок. Пример 2: управление семисегментным индикатором. Игра "нажми вместе". При нажатии двух кнопок на индикаторе отображается разность времени нажатия в миллисекундах. Используется аппаратное прерывание от таймера. Студентам предлагается доработать примеры в некотором направлении. Возможный список задач: * Сделать счетчик нажатий кнопок. Одна кнопка увеличивает счётчик, другая уменьшает. * Секундомер. Первая кнопка запускает отсчет. Вторая увеличивает темп в 10 или 100 раз. * "Электронный кубик" - генератор случайных чисел. === Пример 5.1.1. === Среда MPIDE. Управление двумя светодиодами, имеющимися на плате Uno32, с помощью двух внешних кнопок. TODO: нарисовать схему подключения кнопок. Требуются два резистора 10кОм для подтяжки к +3.3В. // Кнопки на контактах 11 и 12. const int button1 = 11; const int button2 = 12; // Светодиоды на контактах 13 и 43. const int led1 = 13; const int led2 = 43; void setup() { // Сигналы от кнопок используем как входы. pinMode (button1, INPUT); pinMode (button2, INPUT); // Сигналы управления светодиодами - выходы. pinMode (led1, OUTPUT); pinMode (led2, OUTPUT); } void loop() { int need_wait = 0; // Опрашиваем первую кнопку. if (digitalRead (button1) == HIGH) { // Не нажата - гасим первый светодиод. digitalWrite (led1, LOW); need_wait = 1; } // Опрашиваем вторую кнопку. if (digitalRead (button2) == HIGH) { // Не нажата - гасим второй светодиод. digitalWrite (led2, LOW); need_wait = 1; } // Если надо, подождём. if (need_wait) delay (150); // Зажигаем оба светодиода. digitalWrite (led1, HIGH); digitalWrite (led2, HIGH); delay (50); } === Пример 5.1.2 === Управление семисегментным индикатором. Игра "нажми вместе". При нажатии двух кнопок на индикаторе отображается разность времени нажатия в миллисекундах. Используется аппаратное прерывание от таймера. TODO: нарисовать примерную схему подключения 7-сегментного индикатора. Требуются восемь резисторов 220Ом для ограничения тока через светодиоды. Годится любой светодиодный индикатор с общим катодом. Можно с общим анодом, если поменять местами LATxSET и LATxCLR в функции display(). {{led-diagram-3.jpg}} ||**Контакт Uno32 Digital**||**Сегмент LED**|| || 2 || A || || 3 || B || || 4 || C || || 5 || D || || 6 || E || || 7 || F || || 8 || G || || 9 || H || #include // Кнопки на контактах 11 и 12. const int button1 = 11; const int button2 = 12; // 7-сегментный индикатор на контактах 2-9. const int segm_a = 2; const int segm_b = 3; const int segm_c = 4; const int segm_d = 5; const int segm_e = 6; const int segm_f = 7; const int segm_g = 8; const int segm_h = 9; // Время в миллисекундах. unsigned msec; // Состояние кнопок. int button1_was_pressed, button2_was_pressed; // Моменты нажатия кнопок. unsigned time1, time2; void setup() { // Сигналы от кнопок используем как входы. pinMode (button1, INPUT); pinMode (button2, INPUT); // Сигналы управления светодиодами - выходы. pinMode (segm_a, OUTPUT); pinMode (segm_b, OUTPUT); pinMode (segm_c, OUTPUT); pinMode (segm_d, OUTPUT); pinMode (segm_e, OUTPUT); pinMode (segm_f, OUTPUT); pinMode (segm_g, OUTPUT); pinMode (segm_h, OUTPUT); // Устанавливаем прерывание от таймера с частотой 1000 Гц. OpenTimer2 (T2_ON | T2_PS_1_256, F_CPU / 256 / 1000); ConfigIntTimer2 (T2_INT_ON | T2_INT_PRIOR_3); } // Отображение одной цифры на дисплее void display (unsigned digit) { static const unsigned pattern [10] = { 1+2+4+8+16+32, // Цифра 0 2+4, // Цифра 1 1+2 +8+16 +64, // Цифра 2 1+2+4+8 +64, // Цифра 3 2+4 +32+64, // Цифра 4 1 +4+8 +32+64, // Цифра 5 1 +4+8+16+32+64, // Цифра 6 1+2+4, // Цифра 7 1+2+4+8+16+32+64, // Цифра 8 1+2+4+8 +32+64, // Цифра 9 }; if (digit > 9) digit = 9; unsigned mask = pattern[digit]; if (mask & 1) LATDSET = 1 << 8; // Контакт 2 - сигнал RD8 else LATDCLR = 1 << 8; if (mask & 2) LATDSET = 1 << 0; // Контакт 3 - сигнал RD0 else LATDCLR = 1 << 0; if (mask & 4) LATFSET = 1 << 1; // Контакт 4 - сигнал RF1 else LATFCLR = 1 << 1; if (mask & 8) LATDSET = 1 << 1; // Контакт 5 - сигнал RD1 else LATDCLR = 1 << 1; if (mask & 16) LATDSET = 1 << 2; // Контакт 6 - сигнал RD2 else LATDCLR = 1 << 2; if (mask & 32) LATDSET = 1 << 9; // Контакт 7 - сигнал RD9 else LATDCLR = 1 << 9; if (mask & 64) LATDSET = 1 << 10; // Контакт 8 - сигнал RD10 else LATDCLR = 1 << 10; if (mask & 128) LATDSET = 1 << 3; // Контакт 9 - сигнал RD3 else LATDCLR = 1 << 3; } void loop() { // Опрашиваем кнопки. int button1_pressed = (digitalRead (button1) == LOW); int button2_pressed = (digitalRead (button2) == LOW); // Если кнопки были нажаты - запоминаем время. if (button1_pressed && ! button1_was_pressed) time1 = msec; if (button2_pressed && ! button2_was_pressed) time2 = msec; button1_was_pressed = button1_pressed; button2_was_pressed = button2_pressed; if (button1_pressed && button2_pressed) { // Обе кнопки нажаты: показываем разность. if (time1 > time2) { display (time1 - time2); } else { display (time2 - time1); } } else { // Отображаем текущее время, десятые доли секунды. display (msec / 100 % 10); } delay(1); } // Обработчик прерывания от таймера. extern "C" { void __ISR (_TIMER_2_VECTOR, IPL3AUTO) timer2_handler(void) { // Сбрасываем флаг прерывания. mT2ClearIntFlag(); // Наращиваем счётчик миллисекунд. msec++; } } ===== 5.2. Введение в ассемблер MIPS ===== Организация оперативной памяти, разбиение на слова. Понятие машинной инструкции и счётчика команд. Регистры общего назначения, номера и имена регистров. Передача параметров и возврат значения в регистрах. Регистр адреса возврата. Стек, регистр стека, место в стеке для каждой вызываемой функции (фрейм). Основные инструкции системы команд MIPS. Разбиение по функциональным группам. Псевдоинструкции LI, LA. Управление периферийными модулями: таймер, сигналы ввода-вывода. Концепция управляющих регистров, отображаемых на память. ===== 5.3. Практическая работа на ассемблере ===== Среда разработки MPIDE. Использование JTAG-адаптера PICkit 3 для загрузки программ на плату. Пример 1: (TODO) управление двумя светодиодами с помощью двух внешних кнопок. Тот же, что в разделе 5.1, но на ассемблере. Задание для продвинутых студентов: переписать на ассемблере пример 2 из раздела 5.1. === Пример 5.3.1 === Среда MPLABX. Управление двумя светодиодами с помощью двух внешних кнопок. Тот же, что в разделе 5.1, но на ассемблере. TODO: нарисовать схему подключения кнопок. Требуются два резистора 10кОм для подтяжки к +3.3В. // Пример для PIC32 на языке ассемблера. // Файл имеет расширение .S - ассебмлер с препроцессором Си. // Включаем набор стандартных определений для микроконтроллера PIC32. #include .text // Начинаем секцию выполняемого кода. .set noreorder // Отключаем переупорядочивание инструкций // // Функция ожидания, параметр в миллисекундах. // delay: .globl delay // Помечаем метку delay как глобальную li t0, 40000 // В зависимости от частоты процессора mul t1, a0, t0 // Вычисляем нужное количество тактов 1: bne t1, zero, 1b // Крутим цикл ожидания addiu t1, -1 j ra // Возврат nop // // Выполнение начинается с метки main. // main: .global main // Помечаем метку main как глобальную // // Включаем кэш, ускоряем обращение к памяти. // li v0, 2 // Отключаем кэш, два такта ожидания la t0, CHECON sw v0, (t0) // CHECON := 2 li v0, 0x40 // Ноль тактов ожидания для RAM la t0, BMXCONCLR sw v0, (t0) // BMXCONCLR := 0x40 li v0, 0x30 // Включаем кэш la t0, CHECONSET sw v0, (t0) // CHECONSET := 0x30 mfc0 v0, _CP0_CONFIG ori v0, 3 mtc0 v0, _CP0_CONFIG // Разрешаем кэширование сегмента kseg0 ehb // // Сигналы от кнопок используем как входы. // li v0, 1 << 8 la t0, TRISGSET sw v0, (t0) // Сигнал 11 (RG8) как вход от первой кнопки li v0, 1 << 7 la t0, TRISGSET sw v0, (t0) // Сигнал 12 (RG7) как вход от второй кнопки // // Сигналы управления светодиодами - выходы. // li v0, 1 << 6 la t0, TRISGCLR sw v0, (t0) // Сигнал 13 (RG6) - выход для светодиода 1 li v0, 1 << 0 la t0, TRISFCLR sw v0, (t0) // Сигнал RF0 - выход для светодиода 2 la s0, PORTG // Адрес PORTG храним в регистре s0 la s1, LATGSET // Адрес LATGSET храним в регистре s1 la s2, LATGCLR // Адрес LATGCLR храним в регистре s2 la s3, LATFSET // Адрес LATGSET храним в регистре s3 la s4, LATFCLR // Адрес LATGCLR храним в регистре s4 loop: // Начало бесконечного цикла li t0, 0 // Переменная need_wait в регистре s0 // // Опрашиваем первую кнопку. // lw v0, (s0) // Опрашиваем порт G ext v0, v0, 8, 1 // Сигнал RG8 beq v0, zero, 1f // Переход, если кнопка нажата nop // Не нажата - гасим первый светодиод. li v0, 1 << 6 sw v0, (s2) // Запись в LATGCLR, сигнал RG6 li t0, 1 // Установка need_wait в 1 1: // // Опрашиваем вторую кнопку. // lw v0, (s0) // Опрашиваем порт G ext v0, v0, 7, 1 // Сигнал RG7 beq v0, zero, 2f // Переход, если кнопка нажата nop // Не нажата - гасим второй светодиод. li v0, 1 << 0 sw v0, (s4) // Запись в LATFCLR, сигнал RF0 li t0, 1 // Установка need_wait в 1 2: // Если надо, подождём. beq t0, zero, 3f // Переход, если need_wait равно 0 nop jal delay // Вызов delay(150) li a0, 150 3: // // Зажигаем оба светодиода. // li v0, 1 << 6 sw v0, (s1) // Запись в LATGSET, сигнал RG6 li v0, 1 << 0 sw v0, (s3) // Запись в LATFSET, сигнал RF0 jal delay // Вызов delay(50) li a0, 50 j loop // Бесконечный цикл nop // // Параметры конфигурации микроконтроллера PIC32 // для частоты 80MHz на плате Uno32. // // Входной делитель PLL - 1:2 // Умножитель PLL - 20x // Выходной делитель PLL - 1:1 .section .config_BFC02FF4, code .word 0xfff8ffd9 // Системная частота - первичный генератор с PLL // Первичный генератор - режим HS // Выход CLKO - включен // Частота периферии - 1:1 .section .config_BFC02FF8, code .word 0xff60ce5b // Сигналы отладчика - PGC2/PGD2 .section .config_BFC02FFC, code .word 0x7ffffffa ===== 5.4. Концепция многозадачного выполнения ===== В основе многозадачности лежит стремление к простоте. В примере 5.1.1 нам приходилось обрабатывать всего два входных сигнала (кнопки). Тем не менее, основной цикл программы выглядит неочевидным. В реальных системах микроконтроллеру приходится обрабатывать десятки или даже сотни входных событий. Хороший способ справиться с этим - разбить программу на независимые части меньшего размера, выполняющиеся параллельно, не мешая друг другу. Такие части называют //задачами// (tasks). В больших операционных системах задачи, выполняющиеся в раздельных адресных пространствах, называются //процессами//. Вот как могли бы выглядеть задачи в примере 5.1.1. Намного проще, не правда ли? void task1() { for (;;) { if (нажата первая кнопка) { гасим первый светодиод; delay (150); } зажигаем первый светодиод; delay (150); } } void task2() { for (;;) { if (нажата вторая кнопка) { гасим второй светодиод; delay (150); } зажигаем второй светодиод; delay (150); } } Хотелось бы иметь возможность запускать на одном процессоре несколько задач, но чтобы они оставались независимыми и не мешали друг другу. Для этого нам понадобятся два новых понятия: - Контекст выполнения - Прерывания //Контекстом выполнения// называется содержимое регистров процессора и стека выполняемой задачи. Ход программы полностью определяется контекстом выполнения. Если бы мы смогли в какой-то момент остановить процессор, запомнить куда-нибудь значения всех регистров (а указатель стека тоже находится в регистре), установить новые значения для регистров и пустить выполнение дальше, мы получили бы совсем другую задачу. Такое действие называется //переключением контекста//. Если переключать контекст достаточно быстро, скажем 100 раз в секунду, будет создаваться впечатление параллельного и одновременного выполнения всех задач. Скажем, 10 миллисекунд работаем первая задача, потом 10 миллисекунд вторая, и дальше по кругу. Такой алгоритм распределения процессорного времени обычно называют циклическим (round robin). В больших операционных системах применяются и другие алгоритмы, более сложные. //Прерывания// (TODO) ===== 5.5. Пример реализации многозадачности ===== (TODO) MPIDE: пример с переключением двух задач по таймеру. #include // Кнопки на контактах 11 и 12. const int button1 = 11; const int button2 = 12; // 7-сегментный индикатор на контактах 2-9. const int segm_a = 2; const int segm_b = 3; const int segm_c = 4; const int segm_d = 5; const int segm_e = 6; const int segm_f = 7; const int segm_g = 8; const int segm_h = 9; // Размер стека для задач: пятьсот слов, или примерно два килобайта. #define STACK_NWORDS 500 // Память для стеков задач. int task1_stack [STACK_NWORDS]; int task2_stack [STACK_NWORDS]; // Указатели стека для задач. int *task1_stack_pointer; int *task2_stack_pointer; // Номер текущей задачи. int current_task = 0; // // Отображение одного сегмента на дисплее // void display (int segment, int on) { switch (segment) { case 'a': if (on) LATDSET = 1 << 8; // Контакт 2 - сигнал RD8 else LATDCLR = 1 << 8; break; case 'b': if (on) LATDSET = 1 << 0; // Контакт 3 - сигнал RD0 else LATDCLR = 1 << 0; break; case 'c': if (on) LATFSET = 1 << 1; // Контакт 4 - сигнал RF1 else LATFCLR = 1 << 1; break; case 'd': if (on) LATDSET = 1 << 1; // Контакт 5 - сигнал RD1 else LATDCLR = 1 << 1; break; case 'e': if (on) LATDSET = 1 << 2; // Контакт 6 - сигнал RD2 else LATDCLR = 1 << 2; break; case 'f': if (on) LATDSET = 1 << 9; // Контакт 7 - сигнал RD9 else LATDCLR = 1 << 9; break; case 'g': if (on) LATDSET = 1 << 10; // Контакт 8 - сигнал RD10 else LATDCLR = 1 << 10; break; case 'h': if (on) LATDSET = 1 << 3; // Контакт 9 - сигнал RD3 else LATDCLR = 1 << 3; break; } } // // Функция ожидания, с остановом при нажатой кнопке. // void wait (int msec, int button) { while (msec >= 5) { // Если нажата указанная кнопка - останавливаемся, // пока она не освободится. while (digitalRead (button) == LOW) ; delay (5); msec -= 5; } } // // Первая задача: вращаем нижнее кольцо восьмёрки, сегменты D-E-G-C. // Функция не должна возвращать управление. // void task1() { for (;;) { display ('d', 1); wait (100, button1); display ('d', 0); display ('e', 1); wait (100, button1); display ('e', 0); display ('g', 1); wait (100, button1); display ('g', 0); display ('c', 1); wait (100, button1); display ('c', 0); } } // // Вторая задача: вращаем верхнее кольцо восьмёрки, сегменты A-B-G-F. // Функция не должна возвращать управление. // void task2() { for (;;) { display ('a', 1); wait (150, button2); display ('a', 0); display ('b', 1); wait (150, button2); display ('b', 0); display ('g', 1); wait (150, button2); display ('g', 0); display ('f', 1); wait (150, button2); display ('f', 0); } } // // Установка начального значения стека для запуска новой задачи. // int *create_task (int start, int *stack) { stack += STACK_NWORDS - 36 - 4; stack [3] = 0; // at stack [4] = 0; // v0 stack [5] = 0; // v1 stack [6] = 0; // a0 stack [7] = 0; // a1 stack [8] = 0; // a2 stack [9] = 0; // a3 stack [10] = 0; // t0 stack [11] = 0; // t1 stack [12] = 0; // t2 stack [13] = 0; // t3 stack [14] = 0; // t4 stack [15] = 0; // t5 stack [16] = 0; // t6 stack [17] = 0; // t7 stack [18] = 0; // s0 stack [19] = 0; // s1 stack [20] = 0; // s2 stack [21] = 0; // s3 stack [22] = 0; // s4 stack [23] = 0; // s5 stack [24] = 0; // s6 stack [25] = 0; // s7 stack [26] = 0; // t8 stack [27] = 0; // t9 stack [28] = 0; // s8 stack [29] = 0; // ra stack [30] = 0; // hi stack [31] = 0; // lo stack [33] = 0x10000003; // Status: CU0, EXL, IE stack [34] = 0; // SRSCtl stack [35] = start; // EPC: адрес начала return stack; } // // Начальная инициализация программы. // void setup() { // Сигналы от кнопок используем как входы. pinMode (button1, INPUT); pinMode (button2, INPUT); // Сигналы управления светодиодами - выходы. pinMode (segm_a, OUTPUT); pinMode (segm_b, OUTPUT); pinMode (segm_c, OUTPUT); pinMode (segm_d, OUTPUT); pinMode (segm_e, OUTPUT); pinMode (segm_f, OUTPUT); pinMode (segm_g, OUTPUT); pinMode (segm_h, OUTPUT); // Устанавливаем прерывание от таймера с частотой 100 Гц. OpenTimer2 (T2_ON | T2_PS_1_256, F_CPU / 256 / 100); ConfigIntTimer2 (T2_INT_ON | T2_INT_PRIOR_3); // Создаём две задачи. task1_stack_pointer = create_task ((int) task1, task1_stack); task2_stack_pointer = create_task ((int) task2, task2_stack); } // // Основной цикл программы. // void loop() { // Ничего не делаем, ждём прерывания от таймера. // После первого же прерывания начинают работать задачи. } // // Обработчик прерывания от таймера. // extern "C" { __ISR (_TIMER_2_VECTOR, IPL3AUTO) void timer2_handler() { // Сбрасываем флаг прерывания. mT2ClearIntFlag(); // Извлекаем значение указателя стека для текущей задачи. int *sp; asm volatile ("move %0, $sp" : "=r" (sp)); // Переключаемся на другую задачу: меняем указатель стека. // Заметьте: в первый раз переменная current_task равна 0 // и выполняется переключение со стека инициализации на // первую задачу. if (current_task == 1) { task1_stack_pointer = sp; sp = task2_stack_pointer; current_task = 2; } else { if (current_task == 2) task2_stack_pointer = sp; sp = task1_stack_pointer; current_task = 1; } // Устанавливаем новое значение указателя стека. При выходе из // функции из стека будут извлечены значения для остальных регистров. // Перечисляем здесь все регистры, которые необходимо сохранять и // и восстанавливать из стека при переключении контекста. // Компилятор сгенерирует нужные команды. asm volatile ("move $sp, %0" : : "r" (sp) : "$1","$2","$3","$4","$5","$6","$7","$8","$9", "$10","$11","$12","$13","$14","$15","$16","$17", "$18","$19","$20","$21","$22","$23","$24","$25", "$30","$31","hi","lo","sp"); } } === MPLABX: пример с прерыванием от таймера === (TODO: адаптировать предыдущий пример для MPLABX) #include // Include to use PIC32 peripheral libraries // Define system operating frequency #define F_CPU 80000000 void delay (int msec) { int count = F_CPU/2000 * msec; while (count > 0) count--; } int main() { SYSTEMConfig (F_CPU, SYS_CFG_ALL); INTConfigureSystem (INT_SYSTEM_CONFIG_MULT_VECTOR); // Enable timer interrupt at 10 Hz. OpenTimer2 (T2_ON | T2_PS_1_256, F_CPU / 256 / 5); ConfigIntTimer2 (T2_INT_ON | T2_INT_PRIOR_3); // Initialize the digital pins as an output. TRISGCLR = 1 << 6; // RG6 - upper LED TRISFCLR = 1 << 0; // RF0 - lower LED for (;;) { LATGCLR = 1 << 6; // Set the upper LED off delay (200); // Wait LATGSET = 1 << 6; // Set the upper LED on delay (200); // Wait } } void __ISR (_TIMER_2_VECTOR, IPL3AUTO) timer2_handler(void) { //mT2ClearIntFlag(); // Clear interrupt flag IFS0CLR = 1 << _TIMER_2_IRQ; LATFINV = 1 << 0; // Invert RF0 - lower LED } /* * Device configuration bits. */ #pragma config FNOSC = PRIPLL // Primary oscillator with PLL #pragma config POSCMOD = XT // XT oscillator #pragma config FPLLMUL = MUL_20 // PLL multiplier = 20x #pragma config FPLLIDIV = DIV_2 // PLL divider = 1/2 #pragma config FPLLODIV = DIV_1 // PLL postscaler = 1/1 #pragma config FPBDIV = DIV_8 // Peripheral bus clock = SYSCLK/8