Самодельный метроном

В качестве эталона музыкального темпа музыканты применяют метроном - прибор, отмечающий равные промежутки времени. Я вплотную столкнулся с этим, как выяснилось, совершенно необходимым инструментом, когда начал брать уроки игры на ударной установке. И, естественно, сразу же захотел сделать такой прибор своими руками. В результате получилось устройство со следующими характеристиками:

1. Диапазон частот от 20 до 200 ударов в минуту;
2. Клик четвертями, восьмыми, шестнадцатыми и триолями;
3. Запоминание последних настроек темпа и длительности;
4. Возможность приостановить работу.

Устройство питается от гальванического элемента типа "Крона", снабжено ЖК-индикатором и управляется посредством нефиксируемых кнопок. С точки зрения схемотехники и программирования оно интересно применением модуля АЦП для идентификации задействованной кнопки и использованием прерываний, таймера TMR0 и встроенной EEPROM. Основа прибора - микроконтроллер pic12f675.На схеме показаны все основные узлы метронома: блок питания (а), управляемый генератор звуковой частоты (УГЗЧ) (б) и управляющий модуль (в). Принцип действия прибора прост: микроконтроллер формирует ведущий сигнал, выставляя на выход GP4 поочерёдно высокий и низкий логические уровни. Этот сигнал через резистор R1 поступает на базу транзистора VT1, имитируя цепь положительной обратной связи автогенератора. Разберём каждый узел подробнее.

Схема питания (а) построена на стабилизаторе КР142ЕН5А. Вместо него можно использовать микросхему 7805 или даже 78L05 (последняя даёт на выходе не более 100 мА, но для питания логики и ЖК-индикатора нашего прибора этого достаточно). Отдельно выведена линия +9 В напрямую с гальванического элемента - для питания УГЗЧ.

УГЗЧ (б) представляет собой автогенератор с фазосдвигающей RC-цепью, у которой вместо одного из элементов применена индуктивность (намотка капсюля BF1). Цепь положительной обратной связи разорвана для подключения к управляющему сигналу. УГЗЧ питается током с напряжением 9 В. Вместо этого автогенератора можно применить любой другой управляемый мультивибратор - тут уже дело вкуса. Главное, чтобы он мог управляться уровнями TTL-логики (5 В).

Сердце блока управления (в) - микроконтроллер pic12f675. Через сдвиговый регистр 74HC595 к нему подключен ЖК-индикатор Winstar WH1602A со всей необходимой обвязкой (см. соотв. документ). К ножке GP2/AN2, работающей как аналоговый вход, через резистор R1 подсоединён ступенчатый делитель напряжения на резисторах R3...R6. В зависимости от того, какая кнопка зажата (SW1...SW4), на вход АЦП МК подаётся напряжение 5/(4/(5-SW)) вольт, где SW - номер кнопки на схеме. Например, если задействована кнопка SW2, на вход АЦП будет подано 3.75 В. Программа анализирует уровень входного аналогового сигнала и в зависимости от него выполняет то или иное действие. Управляющий сигнал, сгенерированный микроконтроллером, выставляется на ножку GP4. Незадействованная ножка GP3 заведена на землю, дабы не ловить всякие наводки.

Код прошивки. Язык C, среда MPLAB X IDE, компилятор XC8 с максимальным уровнем оптимизации

#include <pic12f675.h>

#define _XTAL_FREQ 4000000 // 4MHz, макрос требуется файлу htc.h
#include <htc.h> // Для задержек __delay_...

#define RS_SHCP GPIO0 //Сдвоенные сигналы: тактовый и тип данных
#define DS GPIO1
#define E_STCP GPIO5 //Сдвоенные сигналы: стробирующий и защёлка

unsigned char I = 80; //Кол-во ударов в минуту, 20..200
unsigned int I2 = 750; //Кол-во мс на один удар, 3000..300
static bit correction = 0; //Битовый флаг, указывающий, была ли нажата хоть одна кнопка; 0..1
unsigned int x = 0; //Значение с АЦП, 0..1023
static bit eeflag = 0; //Битовый флаг, указывающий, были ли изменены значения, сохраняемые в EEPROM; 0..1
unsigned char M = 0; //Метр, 0..3 = четверть (0), восьмая (1), триоль (2), шестнадцатая (3)
unsigned int _t = 0; //Временная метка
unsigned int a = 650; //Предвычисленная длина тишины между пиками для умолчальных значений I и M

//Чтение байта из памяти
unsigned char eeprom_readbyte(unsigned char adr){ //Память адресуется до 0x7F (128 байт)
while (WR); //ждём
EEADR = adr; // адрес ячейки
RD = 1; // чтение
return(EEDATA);
}

//Запись байта в память микроконтроллера
void eeprom_writebyte(unsigned char adr, unsigned char d){
while (WR); // проверка окончания предыдущей записи
EEADR = adr; // адрес ячейки
EEDATA = d; // данные
WREN = 1; // разрешить запись в eeprom
GIE = 0; // запрет прерываний
// обязательная последовательность
EECON2 = 0x55;
EECON2 = 0xAA;
WR = 1; // запись
WREN = 0; // запретить запись в eeprom
GIE = 1; // разрешить прерывания
}

//Функция отправки байта в ЖКИ через сдвиговый регистр
void lcd_write(unsigned char c, unsigned char sig_rs){
E_STCP = 0; //Здесь Е нас не интересует, может быть любым
unsigned char i = 7;
do{
DS = (c >> i) & 1; //Очередной бит данных
RS_SHCP = 0; //Здесь важна SHCP, RS может быть любым
RS_SHCP = 1; //Здесь важна SHCP, RS может быть любым
}while(i--);
E_STCP = 1; //Защёлкнули данные и выставили высокий уровень стробирующего сигнала. Интересуют оба.
RS_SHCP = sig_rs; //Теперь можем пренебрегать SH. Выставили нужный уровень сигнала RS
__delay_us(200); //Теперь имеем сигналы DS (DB7-DB0), для создания которых отработали SH и ST, и сигнал RS
E_STCP = 0; //Начинаем выполнять операцию записи в ЖКИ, STCP не интересует
}

//Вывод массива байт на ЖКИ
void lcd_print(const unsigned char *c){
unsigned char p;
while (p=*c++){
lcd_write(p, 1);
}
}

//Соответствие между параметром M и его представлением на ЖКИ
void setM(unsigned char m){
if(m == 0){
lcd_print("1/4 "); //Любые четыре символа для обозначения четверти
}else if(m == 1){
lcd_print("1/8 "); //Восьмая
}else if(m == 2){
lcd_print("1/12"); //Триоль
}else if(m == 3){
lcd_print("1/16"); //Шестнадцатая
}
}

//Инициализация ЖКИ
void lcd_init(){
__delay_ms(15);

lcd_write(0b00111100, 0); //Шина 8 бит, 2 строки, 5х8 точек - Function Set
__delay_ms(40);

lcd_write(0b00000001, 0); //Очистка экрана - Clear Display
__delay_ms(2);

lcd_write(0b00000110, 0); //Инкремент - Entry Mode Set
__delay_ms(40);

lcd_write(0b00001100, 0); //Включили дисплей и курсор, мигание курсора отключили - Display ON/OFF Control
__delay_ms(40);

lcd_write(0b10000000, 0); //Очистили дисплей - Set DDRAM Address
__delay_ms(2);
}

//Обработчик прерываний - главная часть программы
void interrupt meter(){
//Если получено прерывание по переполнению TMR0
if(T0IF){
T0IF = 0;
TMR0 = 6;
_t++; //В мс

if(_t == 1){
//С первой мс выставляем высокий уровень
GPIO4 = 1;
}

if(_t == 100){
//На сотой мс выставляем низкий уровень. Получили сигнал продолжительностью 100 мс, отмечающий четверти
GPIO4 = 0;
}

//Если помимо четвертей нужно отмечать восьмые, триоли или шестнадцатые; сигналы продолжительностью 50 мс
if(M > 0){

//Восьмые (a - длительность паузы)
if(_t == (100 + a)){
GPIO4 = 1;
}
if(_t == (100 + a + 50)){
GPIO4 = 0;
}

//Триоли
if(_t == (100 + a + 50 + a)){
GPIO4 = 1;
}
if(_t == (100 + a + 50 + a + 50)){
GPIO4 = 0;
}

//Шестнадцатые
if(_t == (100 + a + 50 + a + 50 + a)){
GPIO4 = 1;
}
if(_t == (100 + a + 50 + a + 50 + a + 50)){
GPIO4 = 0;
}

}

//Завершена одна четвертная нота и начинается следующая
if(_t == I2){ //msec
_t = 0;
}
}
}

main(){
OSCCAL = 0x80; //Установить внутренний осциллятор на среднюю частоту
TRISIO = 0b11001100; //Установить GPIO0, GPIO1, GPIO4, GPIO5 на выход, остальные - на вход.
GPIO = 0b00000000; //На всех GPIO установить логический ноль
CMCON = 0b00000111; //Отключить компаратор
ADCON0 = 0b10001000; //Правое выр., опорн. Vdd, канал 2, ожидание, модуль выключен
ANSEL = 0b00010100; //Предделитель /8 (TAD = 2 мкс), AN2
TMR0 = 6; //Начальное значение TMR0
OPTION_REG = 0b11000001; //Предделитель TMR0 /4 и фронты сигнала
INTCON = 0b10000000; //Настройка прерываний

lcd_init();

//Прочитать из памяти сохранённое значение темпа
I = eeprom_readbyte(0x00);
I2 = (60000 / I);

//Прочитать из памяти сохранённое значение длительности
M = eeprom_readbyte(0x01);

//Вывести на экран текущее значение темпа
lcd_write((I/100)+'0', 1);
lcd_write(((I/10)%10)+'0', 1);
lcd_write((I%10)+'0', 1);

//Вывести на экран строку " уд/м Длит."
const char str[14] = {0x20, 0x79, 0xe3, 0x2f, 0xbc, 0x20, 0x20, 0x20, 0xe0, 0xbb, 0xb8, 0xbf, 0x2e, '\0'};
lcd_print(str);

lcd_write(0b11001100, 0); //Set DDRAM, address 0x4D-1 (позиция курсора)
setM(M); //Значение длительности вывести на экран

//Вычислить расстояние между пиками, в мс
a = ((I2 - 100) - (50 * M)) / (1 + M);

//Разрешить прерывания от TMR0
T0IE = 1;

//Вечный цикл
while(1){
x = 0;
__delay_ms(2);
ADON = 1; //Включили модуль
__delay_ms(15); //Заряжается Chold
GO_DONE = 1; //Начало преобразования
while(GO_DONE); //Конец преобразования
x = (ADRESH<<8) + ADRESL;
ADON = 0;

//Нажата кнопка SW4
if(x > 200 && x < 400){
//Уменьшаем темп
if(I > 20) I--;
eeflag = 1;
correction = 1;
__delay_ms(150);
}

//SW3
if(x > 400 && x < 600){
//Увеличиваем темп
if(I < 200) I++;
eeflag = 1;
correction = 1;
__delay_ms(150);
}

//SW2
if(x > 600 && x < 800){
//Приостановка/продолжение отсчёта
T0IE = ~T0IE; //Если разрешены прерывания от TMR0 - запретить, и наоборот
GPIO4 = T0IE; //Если прерывания запрещены - обнулить значение на ножке GP4, а если прерывания разрешены - начать рисовать первый пик
lcd_write(0b10000000 + 0x40, 0);
if(!T0IE){
//Слово "Приостановка"
const unsigned char pause[14] = {0xa8, 0x70, 0xb8, 0x6f, 0x63, 0xbf, 0x61, 0xbd, 0x6f, 0xb3, 0xba, 0x61, '\0'};
lcd_print(pause);
}else{
//Стереть слово "Приостановка"
unsigned char len = 11;
do{
lcd_print(" ");
}while(len--);
}
__delay_ms(250);
}

//SW1
if(x > 800){
//Изменение длительности/пульсации
M = (M+1)%4;
if(M == 4) M = 0;
eeflag = 1;
correction = 1;
__delay_ms(250);
}

if(correction){
//Если была нажата хоть одна кнопка
correction = 0;
lcd_write(0b10000000, 0); //Set DDRAM, address 0x00 (позиция курсора)
lcd_write(((I/100))+'0', 1);
lcd_write(((I/10)%10)+'0', 1);
lcd_write((I%10)+'0', 1);

lcd_write(0b11001100, 0); //Set DDRAM, address 0x4D-1 (позиция курсора)
setM(M);

I2 = (60000 / I);

a = ((I2 - 100) - (50 * M)) / (1 + M);
}

if(eeflag){
//Если нужно перезаписать значения в EEPROM
eeflag = 0;
eeprom_writebyte(0x00, I);
eeprom_writebyte(0x01, M);
}
}
}

Программа занимает 99% программной памяти этого МК. Конфигурационное слово 0x3184. Во время прошивки нужно задать начальные значения в EEPROM - 0x50 по адресу 0x00 и 0x00 по адресу 0x01. Код хорошо прокомментирован, я остановлюсь лишь на некоторых нюансах. Начальное значение таймера TMR0 не случайно равно шести. Этот счётчик восьмибитный, считает от 0 до 255, после чего вызывается прерывание по переполнению, счётчик сбрасывается в ноль и начинает новый отсчёт. В МК частота тактирования делится на 4. Для заданной нами частоты 4 МГц один такт происходит за 1/(4000000/4) = 1 мкс. Один тик таймера совершается за четыре такта процессора (= 4 мкс), потому что мы используем делитель /4 для TMR0. К моменту переполнения таймер отсчитает (4 мкс * 256 тиков) = 1024 мкс, а нам нужно, чтобы он отсчитал ровно 1000. Поэтому мы и задаём начальное знчение таймера 6, чтобы до переполнения он успел сделать только 250 тиков, а это (250 * 4) = 1000 мкс или 1 мс.

Чтобы составить строку из русских символов и за раз целиком вывести её на экран, а не использовать многократную запись lcd_write(<очередной байт>, 1), пришлось предварительно заполнять байтовый массив в соответствии с таблицей символов в даташите на ЖКИ.

Метроном управляется четырьмя кнопками "на лету" (чтобы изменить какую-то настройку, его не нужно останавливать). Изменения вступают в силу сразу же. При каждом нажатии кнопок, отвечающих за настройку темпа и длительности, новое значение сохраняется во встроенную энергонезависимую память.

Формы выходного сигнала при различных настройках длительности (четверти: Та-Та-Та-Та; восьмые: Та-ки-Та-ки-Та-ки-Та-ки; триоли: Та-ки-то-Та-ки-то-Та-ки-то-Та-ки-то; шестнадцатые: Та-ка-ди-ми-Та-ка-ди-ми-Та-ка-ди-ми-Та-ка-ди-ми соответственно):
2014-12-26