====== Сенсорное пианино ====== {{pk2_osa_piano.jpg}} ===== Введение ===== В данной статье рассматривается возможность обработки сенсорной клавиатуры с применением АЦП. В качестве примера разработаем программу "Пианино", обрабатывающую 36 сенсорных кнопок (3 октавы). Для интереса сделаем его многоголосым. В качестве аппаратной базы будем использовать демо-платы из набора pickit2 на базе контроллеров PIC16F690, PIC16F887 или PIC16F886. Программа будет работать под управлением ОСРВ OSA (прим.: ОСРВ - Операционная Система Реального Времени). Здесь 2-х минутное видео с демонстрацией того, что описывается в примере (пианист из меня, конечно, никакой). ==== Видео HQ (34 Mb) ====
struct
{
unsigned char Data[KBD_SIZE]; // Массив битов: 1 - кнопка нажата.
unsigned char cDataPos; // Две переменные - указатель
unsigned char cDataMask; // бита при приеме.
unsigned char Porogs[KBD_COLUMNS]; // Массив пороговых значений для
// определения, нажата ли кнопка
} KBD;
//**Примечание.** Мы предусмотрительно пользуемся константами KBD_SIZE (=36) и KBD_COLUMNS (=6), оставляя себе возможность минимальными силами увеличить или сократить количество клавиш.//
Теперь, мы должны помнить, программа "Пианино" проектируется для разных контроллеров, а у разных контроллеров будут использованы разные выводы для матрицы клавиатуры. Поэтому нам нужно предусмотреть какой-нибудь удобный способ адресации этих выводов. Предположим, что в матрице в столбцах указываются аналоговые входы, а в строках - управляющие выходы. Рассмотрим, какие данные нам нужны для описания строк и столбцов. Со строками (управляющими выходами) все просто: нужны только адрес порта и маска бита в порту, по которой соответствующий вывод будет устанавливаться либо в "1", либо в "0". Со столбцами (аналоговыми входами), учитывая нашу методику, немного сложнее: нам нужен, во-первых, номер АЦП-канала, во-вторых, указатели на PORT и TRIS регистры, поскольку нам придется управлять и тем и другим, и, наконец, в-третьих, - маска бита в порту. Таким образом, мы формируем две структуры для шаблонов:
typedef struct // Тип для задания аналогового входа
{ //
char cADCChannel; // Номер аналогового канала
char *pPort; // Указатель на регистр PORT
char *pTris; // Указатель на регистр направления
char cMask; // Маска бита в порту
} TColumn;
typedef struct // Тип для управляющего выхода
{ //
char *pPort; // Указатель на регистр PORT
char cMask; // Маска бита в порту
} TRow;
Теперь через эти типы можно объявлять массивы выводов контроллера, образующих строки и столбцы матрицы кнопок.
//**Примечание.** Учитывая, что обе переменные из типа TRow присутствуют в типе TColumn, можно было бы обойтись одним типом, просто для строк поля cADCChannel и pTris заполнять нулями, но для строгости мы будем использовать различные типы.//
=== Для синтезатора ===
Синтезатору для работы потребуется массив значений оцифрованных периодов синусоид для различных инструментов. Эти массивы будут храниться в программной памяти в виде констант (см. файл **sinus.c**). Т.к. скорость работы самого синтезатора хотелось бы максимально увеличить, то воспользуемся той особенностью, что обращение к массиву в RAM происходит быстрее, чем обращение к массиву в ROM. Поэтому для работы с массивом значений оцифрованного периода данные для текущего инструмента мы будем копировать в RAM. Т.е. нам нужно зарезервировать в RAM-памяти массив для этих целей:
char Sample[64];
Т.к. у нас предусмотрено синтезирование четырех инструментов, то в программе должна быть переменная, показывающая номер текущего инструмента. За выбор инструмента отвечает задача "Кнопка", в которой мы и будем производить копирование из ROM в RAM. Есть смысл сделать переменную, обозначающую номер текущего инструмента, статической внутри этой задачи:
static char s_cCurSample;
Теперь, синтезатор должен знать, какой канал "молчит", а по какому воспроизводится звук, причем ему нужно указать и частоту звука, и текущую фазу. Итак, у нас получается структура:
typedef struct // Для переменных управления звуком
{
unsigned int F; // Частота
unsigned int f; // Фаза
unsigned char key; // Клавиша, которая проигрывается в данный момент. (0 - молчит)
} TSound;
===== Реализация =====
==== Кнопка ====
Как мы уже писали, по нажатию кнопки должен изменяться инструмент. Не будем забывать, что пользователь может и не менять инструмент, т.е. кнопку вообще не будет трогать, следовательно, какой-то инструмент нужно загрузить в самом начале.
void Task_Button (void)
{
static char s_cCurSample;
s_cCurSample = 0;
CopySample(s_cCurSample);
for (;;)
{
//------------------------------------------------------------------------------
// Ожидаем нажатия на кнопку (с устранением дребезга)
//------------------------------------------------------------------------------
do
{
OS_Cond_Wait(!pin_BUTTON);
OS_Stimer_Delay(ST_BUTTON, 40 ms);
} while (pin_BUTTON);
//------------------------------------------------------------------------------
// Изменяем номер инструмента и копируем данные об инструменте в
// массив Sample
//------------------------------------------------------------------------------
s_cCurSample++;
s_cCurSample &= 3;
CopySample(s_cCurSample);
//------------------------------------------------------------------------------
// Ждем отпускания кнопки
//------------------------------------------------------------------------------
do
{
OS_Cond_Wait(pin_BUTTON);
OS_Stimer_Delay(ST_BUTTON, 40 ms);
} while (!pin_BUTTON);
}
}
Обратим внимание на то, что для формирования задержек используется не таймер задач (т.е. не сервис OS_Delay), а статический таймер. Это связано с тем, что код синтезатора, находящийся в прерывании вместе с системным обработчиком таймеров, может долго выполняться, когда активны все 8 каналов. И так как нам гарантированно нужно вместиться в 50 мкс (250 тактов), то любое ускорение кода приветствуется. В данном случае для ускорения применены статические таймеры. Дело тут не в том, что инкремент статического таймера производится быстрее, чем таймера задачи, а в том, что таймеры нужны только двум задачам из трех активных. И избавляясь от обработки одного неиспользуемого таймера, мы выигрываем драгоценные такты.
Переменная **s_cCurSample** описана как static, т.к. нам важно сохранение ее значения после переключения на другие задачи. Функция **CopySample** просто копирует массив из ROM-памяти в массив Sample:
void CopySample (char c)
{
char n;
c &= 3;
for (n = 0; n < 64; n++) Sample[n] = SAMPLES[c][n];
}
==== Клавиатура ====
Задача чтения состояния клавиатуры каждые 10 мс опрашивает все 6 строк с кнопками. Перед первым опросом обнуляются все значения порогов для всех столбцов (аналоговых входов). Подпрограмма чтения строки проверяет эти переменные и, если они нулевые, сохраняет туда считанные с аналоговых входов значения за вычетом 15% барьера.
//**Примечание.** При использовании прокладки между пальцем и пластиной емкостного датчика барьер должен стоять выше.//
void Task_Keyboard (void)
{
static char n;
static char s_cChanged;
//------------------------------------------------------------------------------
// Один раз выполняем чтение, чтобы точно быть уверенными в том, что все
// TRIS'ы аналоговых входов установлены в "0" (на выход) для разряда
// конденсаторов
//------------------------------------------------------------------------------
ReadRow(0);
//------------------------------------------------------------------------------
// При первом запуске все пороги устанавливаем в 0
//------------------------------------------------------------------------------
for (n = 0; n < KBD_COLUMNS; n++) KBD.Porogs[n] = 0;
for (;;)
{
//------------------------------------------------------------------------------
// Перед измерением состояния всех кнопок устанавливаем маску для первой
// кнопки
//------------------------------------------------------------------------------
KBD.cDataPos = 0; // Номер байта
KBD.cDataMask = 0x01; // Маска бита в байте
s_cChanged = 0; // Признак того, что состояние какой-то
// клавиши было изменено
for (n = 0; n < KBD_ROWS; n++) // Цикл по всем строкам
{
s_cChanged |= ReadRow(n); // Измерение всех датчиков в строке
// может длиться до 500 мкс, поэтому
// после прочтения каждой строки
// переключаем контекст
OS_Yield();
}
//------------------------------------------------------------------------------
// Если были изменения в кнопках, то отправляем сообщение задаче
// формирования звуковых переменных
//------------------------------------------------------------------------------
if (s_cChanged)
{
OS_Msg_Send_Now(msg_KBD, (OST_MSG) KBD.Data);
}
OS_Stimer_Delay(ST_KEYBOARD, 10 ms);
}
}
Здесь так же, как и в задаче **Task_Button** для формирования задержки применен статический таймер.
Теперь рассмотрим основную функцию клавиатуры, ту, которая занимается чтением состояний емкостных датчиков.
char ReadRow (char row)
{
char m, a, i, k; // Вспомогательные переменные
char col; // Текущий канал
TColumn Column; // Для сокращения кода обработки канала его
// параметры копируются из ROM в переменную
static char s_Changes[KBD_SIZE]; // Изменения в состояниях кнопок
// для подавления дребезга
static bit s_bChanged; // Переменная для возврата
//------------------------------------------------------------------------------
*ROWS[row].pPort |= ROWS[row].cMask;// Управляющий выход для линии в "1"
s_bChanged = 0; // Изначально считаем, что изменений
// не было
//******************************************************************************
// Цикл по всем каналам
//******************************************************************************
for (col = 0; col < KBD_COLUMNS; col++)
{
//------------------------------------------------------------------------------
// Копируем параметры канала в переменную
//------------------------------------------------------------------------------
Column.pPort = COLUMNS[col].pPort;
Column.pTris = COLUMNS[col].pTris;
Column.cMask = COLUMNS[col].cMask;
Column.cADCChannel = COLUMNS[col].cADCChannel;
//------------------------------------------------------------------------------
// Выбираем канал ADC
//------------------------------------------------------------------------------
CHS0 = 0;
CHS1 = 0;
CHS2 = 0;
if (Column.cADCChannel & 0x01) CHS0 = 1;
if (Column.cADCChannel & 0x02) CHS1 = 1;
if (Column.cADCChannel & 0x04) CHS2 = 1;
#if defined(_16F887) || defined(_16F886) || defined(_16F690)
CHS3 = 0;
if (Column.cADCChannel & 0x08) CHS3 = 1;
#endif
//------------------------------------------------------------------------------
// Начинаем измерение
//------------------------------------------------------------------------------
GIE = 0; // На время заряда конденсатора блокируем
// прерывания, чтобы получить фиксированную
// паузу
*Column.pTris |= Column.cMask; // Начать заряд переводом порта на вход
for (m = 0; m < 3; m++) NOP(); // ПАУЗА для заряда.
GODONE = 1; // Начинаем АЦ-преобразование
GIE = 1; // Теперь прерывания можно разрешить
while (GODONE) continue;
//------------------------------------------------------------------------------
// Измерение закончено, теперь разряжаем конденсатор, читаем результат
// и формируем массив нажатых кнопок
//------------------------------------------------------------------------------
*Column.pTris &= ~Column.cMask;
*Column.pPort &= ~Column.cMask; // Разряжаем конденсатор переводом входа на
// выход и установкой "0" на нем
a = ADRESH;
//------------------------------------------------------------------------------
// Устанавливаем порог срабатывания, если он еще не установлен
//------------------------------------------------------------------------------
m = KBD.Porogs[col]; // Для сокращения кода копируем элемент
// массива в переменную
i = 0;
if (a < m) i = KBD.cDataMask; // Сравниваем результат АЦП с порогом для
// данного канала. Если результат меньше
// порогового значения, значит,
// емкость на входе превышала допустим
// ое значение и кнопка считается нажатой
if (!m) // Если значение порога еще не установлено,
{ // то формируем его как 88% от ADRESH
m = a >> 3;
KBD.Porogs[col] = a - m;
}
//------------------------------------------------------------------------------
// Установка нового значения кнопки с подавлением дребезга
//------------------------------------------------------------------------------
m = KBD.Data[KBD.cDataPos]; // Для сокращения кода работаем не с элементом
k = s_Changes[KBD.cDataPos]; // массива, а с фиксированной переменной
//------------------------------------------------------------------------------
if ((m ^ i) & KBD.cDataMask)
{ // Состояние кнопки изменилось:
if (!(k & KBD.cDataMask)) // Только что
k |= KBD.cDataMask; // Устанавливаем признак изменения бита
else // Не только что
{
m ^= KBD.cDataMask; // устанавливаем новое значение кнопки
s_bChanged = 1; // Формируем переменную для возврата
k &= ~KBD.cDataMask; // Сбрасываем признак изменения бита
}
//------------------------------------------------------------------------------
} else { // Состояние кнопки не изменилось:
k &= ~KBD.cDataMask; // Сбрасываем признак изменения бита
}
//------------------------------------------------------------------------------
KBD.Data[KBD.cDataPos] = m; // Восстанавливаем значения массивов из
s_Changes[KBD.cDataPos] = k; // временных переменных
//------------------------------------------------------------------------------
// Устанавливаем маску для следующей кнопки
//------------------------------------------------------------------------------
KBD.cDataMask <<= 1;
if (!KBD.cDataMask)
{
KBD.cDataMask = 0x01;
KBD.cDataPos++;
}
};
*ROWS[row].pPort &= ~ROWS[row].cMask; // Сбросить управляющий выход линии
return s_bChanged;
}
Обратим внимание на строку формирования паузы:
for (m = 0; m < 3; m++) NOP();
В данном случае пауза рассчитана для сопротивления в RC-цепочке, равное 100К, что обеспечивает заряд емкости примерно до Vdd/2. При установке резисторов других номиналов константу в цикле желательно (но не обязательно) пересчитать пропорционально, т.е. для 200К нужно будет считать до 6.
==== Звук ====
Эта задача будет получать информацию о состоянии кнопок клавиатуры (через сообщение от **Task_Keyboard**) и формировать переменные управления звуком для "Синтезатора". При получении сообщения состояния кнопок копируются во внутренний массив для обработки, поскольку обработка предусматривает модификацию данных в этом массиве. После получения сообщения пробегаемся по всем переменным типа **TSound**, содержащим информацию о том, на каком канале какая нота воспроизводится, с целью:
* прекращения воспроизведения уже не нажатых клавиш;
* удаления из списка нажатых клавиш уже воспроизводимых на данный момент;
* вычисления количества свободных каналов.
После этого у нас есть переменная **cFreeSounds**, показывающая сколько звуковых каналов свободно, и список еще не обрабатываемых клавиш в массиве **Data**.
TSound S[MAX_CHANNELS]; // Переменные для формирования звука
void Task_Sound (void)
{
OST_MSG msg; // Переменная для приема сообщения
unsigned char Data[KBD_SIZE]; // Массив, куда будут скопированы состояния
// клавиш
unsigned char cMask; // Две вспомогательные переменны для побитового
unsigned char cPos; // индексирования кнопок в массиве Data
unsigned char cFreeSounds; // Переменная будет показывать, сколько свободных
// каналов (не воспроизводящих) есть на
// данный момент
unsigned char i, j; // Вспомогательные переменные
//------------------------------------------------------------------------------
for (;;)
{
//------------------------------------------------------------------------------
// Ждем изменения состояния кнопок.
// Копируем состояния кнопок в массив Data
//------------------------------------------------------------------------------
OS_Msg_Wait(msg_KBD, msg);
for (i = 0; i < KBD_SIZE; i++) Data[i] = ((char*)msg)[i];
//------------------------------------------------------------------------------
// Из списка нажатых кнопок удаляем те, которые в данный момент
// уже воспроизводятся. Одновременно считаем, сколько свободных каналов
// имеется на данный момент.
//------------------------------------------------------------------------------
cFreeSounds = 0;
for (i = 0; i < MAX_CHANNELS; i++) // Пробегаемся по всем каналам
{
if (S[i].key == 0) // Если данный канал "молчит", то
{ // увеличить счетчик свободных каналов
cFreeSounds++;
continue;
}
j = S[i].key - 1; // Формируем адрес бита в массиве Data,
cMask = 1 << (j & 7); // соответствующего текущему каналу
cPos = j >> 3;
if (Data[cPos] & cMask) // Если кнопка все еще нажата, то
Data[cPos] &= ~cMask; // Убираем ее из списка нажатых кнопок
else
{
cFreeSounds++; // Иначе прекращаем звук и увеличиваем
S[i].key = 0; // счетчик свободных каналов.
}
}
//------------------------------------------------------------------------------
// На данный момент cFreeSound содержит число незадействованных каналов,
// которые можно использовать для воспроизведения звуков для вновь
// нажатых клавиш
//------------------------------------------------------------------------------
cMask = 0x01; // Поиск клавиш начинаем с первой
cPos = 0;
j = 0; // Счетчик кнопок
i = 0; // Счетчик каналов
while ((j < KBD_KEYS) && cFreeSounds)
{
if (Data[cPos] & cMask) // Клавиша нажата?
{ // Да.
while (S[i].key) i++; // Ищем свободную ячейку
// Формируем звуковую переменную:
S[i].F = Freq[j]; // Устанавливаем частоту
S[i].f = 0; // Начальная фаза
S[i].key = j + 1; // Запоминаем номер воспроизводимой клавиши
cFreeSounds--; // Уменьшаем счетчик свободных каналов
}
j++; // Берем следующую кнопку для анализа
cMask <<= 1;
if (!cMask)
{
cMask = 0x01;
cPos++;
}
}
}
}
==== Синтезатор ====
Как мы уже решили раньше, подпрограмма синтезатора звука будет помещена внутрь прерывания по TMR2. Таймер 2 у нас настроен и для отсчета тактов модуля ШИМ, и для генерации прерываний. ШИМ у нас выбран максимально возможной частоты для 20 МГц и 8-разрядного разрешения, т.е. 78КГц. Теперь нам нужно выбрать постделитель для TMR2 такой, чтобы обеспечить частоту семплирования 20КГц. Очевидно, что ближайшим значением будет 4 (при этом мы получим частоту 19500). Итак, каждые 51.2 мкс вызывается прерывание, в котором генерируется звук, а именно - скважность импульсов ШИМ-сигнала.
void interrupt isr (void)
{
static unsigned char prs; // Предделитель для вызова OS_Timer
signed int temp_dac; // Сумма мгновенного значения
// сигнала для всех каналов
unsigned char m_cDAC; // Переменная для вывода через ШИМ
TMR2IF = 0;
temp_dac = 0;
//------------------------------------------------------------------------------
// Формируем мгновенное значение суммы всех каналов
//------------------------------------------------------------------------------
SOUND(0);
SOUND(1);
SOUND(2);
SOUND(3);
SOUND(4);
SOUND(5);
SOUND(6);
SOUND(7);
temp_dac >>= 3; // Т.к. 8 каналов, то сумму делим на 8.
//------------------------------------------------------------------------------
// Выводим полученное значение через ШИМ
//------------------------------------------------------------------------------
m_cDAC = *((char*)&temp_dac+0) + 0x80;
m_cDAC >>= 2;
CCP_bit1 = 0;
CCP_bit0 = 0;
if (temp_dac & 2) CCP_bit1 = 1;
if (temp_dac & 1) CCP_bit0 = 1 ;
CCPR1L = m_cDAC;
}
Обратим внимание на вызов макросов SOUND(x). Этот макрос написан просто для удобства добавления/удаления каналов в зависимости от требований к качеству звука и тактовой частоты контроллера (Например, понизив тактовую частоту в два раза, мы за 50 мкс будем успевать обработать только 4 канала; или, снизив частоту семплирования до 10 КГц, мы сможем обработать 16 каналов). Сам макрос выглядит так:
#define SOUND(x) \
if (S[x].key) { \
temp_dac += Sample[*((char*)&S[x].f+1) & 0x3F]; \
*((char*)&S[x].f+1) += *((char*)&S[x].F+1); \
*((char*)&S[x].f+0) += *((char*)&S[x].F+0); \
if (CARRY) *((char*)&S[x].f+1) += 1; \
}
После проверки активности канала по полю **cKey** мы из массива, где хранится оцифрованный период для конкретного музыкального инструмента, в соответствии с текущей фазой сигнала (поле **f**) выбираем нужное значение и прибавляем его к общей сумме **temp_dac**. После этого сдвигаем фазу на шаг, зависящий от частоты ноты, которая проигрывается на данном канале.
Теперь в прерывание осталось добавить обработку системных таймеров. Выбираем интервал для таймера равный 10 мс, или двумстам вызовам прерывания.
//------------------------------------------------------------------------------
// 1 раз в 200 вызовов (200 * 50мкс = 10мс) вызываем системный таймер
//------------------------------------------------------------------------------
if (!--prs)
{
OS_Timer(); // Обработка системных таймеров
prs = 200;
}
==== main() ====
Здесь будут проинициализированы периферия и операционная система, а также будут созданы задачи и запущен планировщик. Все задачи имеют одинаковый высший (нулевой) приоритет.
void main (void)
{
//------------------------------------------------------------------------------
// Инициализация периферии
//------------------------------------------------------------------------------
Init();
//------------------------------------------------------------------------------
// Инициализация системы
//------------------------------------------------------------------------------
OS_Init();
//------------------------------------------------------------------------------
// Создание задач (все задачи имеют одинаковый высший приоритет)
//------------------------------------------------------------------------------
OS_Task_Create(0, Task_Sound);
OS_Task_Create(0, Task_Button);
OS_Task_Create(0, Task_Keyboard);
//------------------------------------------------------------------------------
// Разрешаем прерывания и запускаем планировщик
//------------------------------------------------------------------------------
OS_EI();
OS_Run();
}
==== Init() ====
Весь текст я здесь приводить не буду (его можно посмотреть в исходных текстах, прилагаемых к статье), т.к. из-за того, что эта функция предусматривает работу на 4-х разных контроллерах (16F886, 16F887, 16F690 и 16F88), то код ее довольно громоздкий из-за наличия условных директив #ifdef…#endif.
Скажу только, что в этой функции производится инициализация:
* потов ввода/вывода;
* АЦП;
* таймеров;
* модуля ШИМ;
* прерываний.
==== Конфигурация OSA ====
Для конфигурирования работы операционной системы в нашем проекте воспользуемся утилитой OSAcfg_Tool.
=== 1. Выбираем папку, где располагается наш проект ===
Для этого в самом верху окна справа нажимаем кнопку **Browse**. Там выбираем путь к файлу OSAcfg.h - путь к нашему проекту ("C:\TEST\PICKIT2\PIANO"). Нажимаем OK. Если файл еще не создан, то программа спросит у Вас, действительно ли Вы хотите создать этот файл. Смело отвечаем "Yes" и идем дальше.
=== 2. Выбираем имя проекта ===
В поле **Name** можно ввести имя проекта. Этот пункт необязателен, а имя вводится исключительно для наглядности, чтобы не путаться потом, какой файл от какого проекта. Мы введем в эту строку "ПИАНИНО".
=== 3. Выбираем платформу ===
Также необязательный пункт. Служит только для того, чтобы пользователь при конфигурировании файла в реальном времени наблюдал предполагаемый расход оперативной памяти операционной системой. Для успокоения выберем платформу: 14-бит (PIC12, PIC16)(ht-picc). Теперь при изменении настроек мы автоматически в рамке **RAM statistic** будем видеть, сколько байтов в каком банке памяти израсходовано.
=== 4. Конфигурируем наш проект ===
Учитывая, что мы решили не использовать приоритеты (т.е. все задачи сделать равноприоритетными), можно установить галочку напротив пункта **Disable priority**. Это сократит размер кода ядра операционной системы и ускорит работу планировщика.
Далее, нам обязательно нужно выбрать количество задач ОС, которые будут работать одновременно. В нашем случае - 3 (по количеству задач, создаваемых сервисом OS_Task_Create; как уже было сказано раньше, 4-я задача у нас не является задачей ОС и располагается в обработчике прерывания).
Учитывая, что сама программа использует много переменных, есть смысл все системные переменные затолкать в какой-нибудь верхний банк памяти, например **bank2** (в поле **OSA variables bank**).
Теперь нам нужно сказать системе, что нам требуются два статических таймера для работы. В поле **Static timers** устанавливаем 2 и в таблице ниже вводим имена идентификаторов статических таймеров: **ST_KEYBOARD** и **ST_BUTTON**. Кроме того, учитывая, что нам не потребуются задержки длиннее 256 системных тиков, установим тип статического таймера **char**.
И последнее: для ускорения обработки сервиса OS_Timer установим галочку напротив пункта **Use in-line OS_Timer()**.
=== 5. Сохраняем и выходим ===
Жмем на кнопку **Save**, чтобы сохранить отредактированный файл конфигурации, и выходим из программы нажатием на кнопку **Exit**. Теперь, заглянув в созданный нами файл, мы увидим следующее:
/******************************************************************************/
//
// This file was generated by OSAcfg_Tool utility.
// Do not modify it to prevent data loss on next editing.
//
// PROJECT NAME: ПИАНИНО
// PLATFORM: HT-PICC 14-bit
//
/******************************************************************************/
#ifndef _OSACFG_H
#define _OSACFG_H
//------------------------------------------------------------------------------
// SYSTEM
//------------------------------------------------------------------------------
#define OS_TASKS 3 // Number of tasks that can be active at one time
#define OS_DISABLE_PRIORITY //
//------------------------------------------------------------------------------
// ENABLE CONSTANTS
//------------------------------------------------------------------------------
#define OS_USE_INLINE_TIMER // Make OS_Timer service as in-line function
//------------------------------------------------------------------------------
// BANKS
//------------------------------------------------------------------------------
#define OS_BANK_OS 2 // RAM bank to allocate all system variables
//------------------------------------------------------------------------------
// TYPES
//------------------------------------------------------------------------------
#define OS_STIMER_SIZE 1 // Size of static timers (1, 2 or 4)
//------------------------------------------------------------------------------
// STIMERS
//------------------------------------------------------------------------------
#define OS_STIMERS 2 // Number of static timers
enum OSA_STIMERS_ENUM
{
ST_KEYBOARD, // Для формирования задержек в Task_Keyboard
ST_BUTTON // Для формирования задержек в Task_Button
};
#endif
===== Прошивка контроллера =====
==== Сборка проекта ====
Для работы с проектом нам нужно иметь установленную интегрированную среду [[http://www.microchip.com/stellent/idcplg?IdcService=SS_GET_PAGE&nodeId=1406&dDocName=en019469&part=SW007002|MPLAB IDE]], установленный компилятор [[http://www.htsoft.com/microchip/products/compilers/picccompiler.php|HI-TECH PICC STD]] (PRO-версия не подойдет).
Скачиваем, если еще не скачали, **{{http://wiki.pic24.ru/lib/exe/fetch.php/osa/history/osa_90402.zip|файлы операционной системы OSA}}**, распаковываем этот архив на диск C: (должна получиться папка C:\OSA).
Распаковываем файл **{{http://wiki.pic24.ru/lib/exe/fetch.php/osa/piano.rar|piano.rar}}** в папку C:\TEST\PICKIT2. При этом внутри создастся папка **PIANO**. В MPLAB IDE открываем проект, в названии которого присутствует номер контроллера, который Вы собираетесь использовать: 886, 887, 690 или 88. Например, для демо-платы на базе 16F887 нам нужно открыть файл pk2_piano_887.mcp.
**Примечание.**// При распаковке в другую папку, отличную от C:\TEST\PICKIT2\PIANO, нужно будет через меню Project\Build options...\Project в закладке Directories в списке include-путей заменить путь к файлам проекта на тот, куда Вы распаковали файлы из архива.//
Выполняем сборку нажатием **Ctrl+F10**.
==== Прошивка ====
Здесь все просто:
- подключаем программатор;
- в меню "Programmer\Select" programmer выбираем PicKit2;
- В настройках "Programmer->Settings" выбираем <3-State on