(Фрагмент [[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