AVR раз, два, три... бай-ру   это просто!

Курс  Начинающим:   МикроКонтроллеры 
AVR "с нуля" на языке Си

- задача 04 -

 

Задача № 04

Цель задачи:

Разработать программу для измерения частоты сигнала ёмкостного датчика нагрузки: МК должен сохранять результат в памяти и отправлять на последовательный порт ПК.


Для выполнения задания необходимо:

- Компилятор Си для AVR ImageCraft 

- Программный эмулятор для AVR Visual Micro Lab

- Data Sheet на МК AVR AT90s2313 

- свободное время и желание.



Вступление...

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

Вот один из вариантов конструкции емкостного датчика нагрузки из двух металлических пластин с воздушным зазором

Я предлагаю такую конструкцию датчика:


Это металлический плоский ящик-опора на который действует измеряемая нагрузка. 

Металлический верх ящика является упругим элементом датчика и одновременно обкладкой измерительного конденсатора соединенной с общим проводом электрической схемы устройства - так достигается экранировка устройства от внешних электромагнитных полей. 

К верху ящика, с низу приклеена по краям, с зазором, металлическая пластина - вторая обкладка конденсатора - она остается плоской не зависимо от нагрузки. 

При нагружении ящика его верх деформируется и зазор между пластинами уменьшается - соответственно емкость конденсатора увеличивается. 

Значит уменьшается частота генератора  частото-задающая RC цепь которого состоит из этого конденсатора и дополнительного резистора. 

Частота делится делителем на 128 и передается на устройство фиксации. 

Электрическая часть датчика собрана на двух микросхемах по схеме:


Устройство питается от напряжения 5-15 вольт и выдает почти прямоугольный сигнал на выходе.

Резисторы 1 и 2 вместе с измерительным конденсатором задают частоту генерации. 

Период колебаний генератора чуть больше произведения R*C.

Частота на выходе устройства без нагрузки примерно 2000 Гц - учитывая делитель генератор должен выдавать 256 кГц.

Можно применить генератор низкой частоты без деления - но ВЧ генераторы более стабильны. 

Очень простая схема генератора может быть сделана на микросхемах таймерах 555 серии. 

Моя схема нравится мне тем, что одна из обкладок конденсатора связана с "общим проводом" и тем, что благодаря делителю частоты можно в широких пределах вирировать параметры системы в целом. 

Электрическая часть датчика смонтирована внутри ящика. 

 

Наверно по такой схеме можно реализовать не плохой и недорогой датчик ускорения!

только деформироваться будет тонкая внутренняя диафрагма а корпус будет жестким.

 

Работаем...

В задаче_03 мы написали программу которая каждые 20 мс по прерыванию от переполнения таймера_0 попадает в функцию - обработчик прерывания. 
можно модифицировать эту программу. 


но! повторение мать учения
давайте повторим процесс создания программы с "0".


Новая программа
должна будет каждые 20 мс прерываться и измерять период цифрового сигнала поступающего на ножку 6 МК PD2 (INT0) и выводить результат измерения по rs232 со скоростью 9600 на ножку 3 МК  PD1 (TxD)

Этих функций достаточно для отладки датчика с МК "в металле" в комбинации с софтом на ПК.

Сохранение в памяти - пока будет в виде упоминания прототипа соответствующей функции в Си программе для МК.

Как будем измерять период сигнала?

Давайте задействуем для этого еще одно прерывание - внешнее прерывание по спаду сигнала на ножке 6 МК, это бит PD2 (INT0)  (см. DataSheet ). 


Мы укажем МК что бы это прерывание происходило по спаду сигнала, т.е. при изменении уровня сигнала на ножке с "1" на "0" 


Возможны еще 3 варианта событий вызывающих это прерывание:

- фронт сигнала
- уровень лог. "0" 
- уровень лог. "1"  

Прерываваясь каждые 20 мс, мы будем разрешать прерывание INT0 - по его первому возникновению мы начнем отсчет, и продолжим счет до его второго возникновения. таким образом насчитанное число будет пропорционально периоду входного сигнала. 


ЛЕПОТА! 


Запретим прерывание по INT0 и отправим результат по rs232 в ПК, предварительно сохранив его (пока сохранения не будет). 

...и опять ждать отсчета очередных 20 мс. 

 

Пишем на Си ...



Давайте используем АпБилдер компилятора ICC

1) в секции CPU:

- "тагет" 2313  
- частота 3.6864
- ставим крестик на INT0 и выбираем событие "фоллин едж" по спаду значит прерывание будет.

2) в секции Ports:

- давайте сделаем три ножки PB0_PB2 выходами, авось пригодится.

3) в секции Taimer0:

- ставим 2 флажка - использовать таймер и прерываться по переполнению
- ставим частоту прерываний 50 Гц
- деление 1024 

4) в секции UART:

- ставим 2 флажка: использовать UART и разрешить передачу
- скорость 9600

5) в "опциях" выбираем " инклуд мэйн - т.е. вставить в текст программы функцию main()

5) "Ок"  - код заготовка есть.

 

Давайте уважать Си!  Не смотря на то, что компилятор делает по другому - будем перечислять прототипы используемых в программе функций. 

это удобно - особенно если захочешь использовать свой код через годик другой - гораздо легче разобраться что делает программа.



Значит так... модифицируем:

#include <io2313v.h>
#include <macros.h>

//////////////////////////
/// Прототипы функций

void port_init(void);
void timer0_init(void);
void timer0_ovf_isr(void);
void uart0_init(void);
void int0_isr(void);
void init_devices(void);


void get_period(void);
void send_result(void);
void save_result(void);


//////////////////////////

void main(void)
{
init_devices();

//insert your fcode here...

}

//////////////////////////

void get_period(void)
{

}

void send_result(void)
{

}

void save_result(void)
{

}

void port_init(void)
{
PORTB = 0xFF;
DDRB = 0x07;
PORTD = 0x7F;
DDRD = 0x00;
}

 

далее без изменений... 
только main убрать в низу.

 

Я добавил три функции:

void get_period(void);
void send_result(void);
void save_result(void);

что они делают, по порядку:

- измерить период сигнала
- отправить результат по rs232
- сохранить результат во внешнюю память 

Обязательно давайте функциям названия с намеком на то что они делают! Не бойтесь длинных названий - это нормальная практика...

 

Теперь сохраним этот файл под именем work04.c в папке work04, и создадим новый проект work04.prj   


Идем дальше...

Вместо //insert your code here... 
напишем:  while(1); 

Откомпилируем программу - все в норме - 9% ресурсов задействовано уже.

Кстати, надо внести поправочку в инициализацию МК - дело в том что прерывание должно быть отключено на старте и включать его мы будем по мере необходимости - т.е. непосредственно при измерении периода сигнала.

Как отключить это прерывание - нужно почитать DataSheet МК про устройство регистров: MCUCR (стр.26 DataSheet) и GIMSK - прошу, почитайте, это нужно вам!

я почитал и вам расскажу, что нужно при запуске в регистр GIMSK записать 0х00... это запретит прерывание INT0, сделайте это в функции void init_devices(void)

GIMSK = 0х00; // INT0 отключено
                      // 0x40 - включено

Значит когда нам понадобиться прерывание по INT0 - мы запишем в регистр GIMSK (стр.23 DataSheet) значение 0х40

вот так просто... 
включаем - выключаем, прерываниями МК управляем!




Итак МК стартовал и залетел в прерывание по переполнению таймера0 - давайте укажем ему что нужно делать в этом состоянии. 

Заполним "тело" функции - обработчика этого прерывания кодом программы на Си:

void timer0_ovf_isr(void)
{
// MK прервался и мы попали сюда! 
 
// Значит TIMER0 перескочил с 255 на 0

TCNT0 = 0xB8;
// в регистр TCNT0 записали 184
//нам ведь нужно считать со 184...

//..пошел отсчет очередных 20 мс

get_period();
//..измерить период

send_result();
//..отправить результат по rs232

save_result();
//.. сохранить в память
}



Перезапустили отсчет очередных 20 мС и попали в функцию "измерить период" - давайте сочиним и для этой функции "тело":

...у меня вот такое тело получилось:

void get_period(void)
{
// очистим флаг прерывания INT0
// записью "1" в бит_6 рег. GIFR
GIFR |= 0x40; // turn on bit6 

// 
см. Help раздел "Bit Twiddling" 
//       операции с битами - распечатайте!!!

// включить прерывание по спаду INT0
GIMSK = 0x40;

while(no_INT0); // ждем прерывания по
// очередному спаду сигнала

// мы здесь, значит прерывание случилось

no_INT0 = 1;
// восстановим признак ожидания INT0

GIFR |= 0x40; // очистим флаг INT0

// начнем счет периода сигнала
// до следующего спада сигнала


while((no_INT0) && (result < max_count))
{
result++; // увеличиваем на 1
}
// из цикла вылетим при прерывании INT0
// по очередному спаду или если период
// слишком большой то по достижении
// числа max_count


// ВЫКЛ. прерывание INT0
GIMSK = 0x00; // можно = 0;

no_INT0 = 1;
// восстановим признак ожидания INT0
}

эта функция свое дело сделала - переменная содержит число пропорциональное периоду входного сигнала.

Кстати о переменных - я применил две глобальные - т.е. видимые во всех функциях программы - такие переменные объявим сразу после
#include <macros.h> 

//////////////////////////
/// глобальные переменные


int result; // период сигнала - два байта

char no_INT0=1; // INT0 обнуляет эту переменную

Возможно, со временем их станет больше...


я не опытный программист по этому применяю в основном глобальные переменные - это не правильно, но мне так легче...

Функция - обработчик прерывания INT0 вот такая:

void int0_isr(void)
{

//external interupt on INT0
no_INT0 = 0;
// обнулили признак "no_INT0"
}

Здесь мы делаем признак no_INT0 нулем т.е. "ложно" - чтобы выскочить из цикла while в котором нас застало это прерывание. 


Вы заметили еще не определенное число max_count - это страховка срабатывающая при отсутствии сигнала или слишком низкой частоте входного сигнала 

Будем считать таким сигнал с частотой ниже 500 Гц. т.е. с периодом более 2 мС


Рассчитаем значение числа max_count

- цикл while со сложным условием займет наверно 9 тактов (уточним при эмуляции)
- МК тикает 3686.4 х 2 = 7373 раз за 2 мС 
- делим 7373 на 9 получим  819 если наши предположения по длительности цикла верны то за 2 мс мы насчитаем 820 а при частоте 2000 Гц мы насчитаем в 4 раза меньше т.е. 205. 

Возьмем для начала max_count = 900 а по результатам эмуляции подкорректируем это число:

# define max_count 900

напишем сразу после #include <macros.h> 

  
Теперь если результат получится 900 - значит сигнал или отсутствует или он слишком низкочастотный.

 

  



На очереди функция void send_result(void) 

вывод результата в последовательном виде на ножку 3 МК  PD1 (TxD).

выводить результат будем с новой строки, по два байта старший, затем, через пробел, младший байт:

void send_result(void)
{
temp = result; // спасли результат

low_result=(temp & 0x00FF); 
// уничтожили старшие 8 бит и
// получили младший байт результата


temp = result;
hi_result= temp >> 8; // сдвинули на 8 бит вправо
// получили стрший байт результата

putchar(0x0D); // на новую строку
// .. нет таблицы под рукой - могу ошибаться

hi_result = + 65; // вместо 0,1,2,3 мы получим 
                       // символы A B C D

putchar(hi_result); 
// вывест старший байт как A B C или D

putchar(' ');
// вывести пробел 

putchar(low_result);
// вывести младший байт как повезет

}

Все вроде хорошо, НО! если мы будем принимать данные в специальной программе, а если мы хотим получать данные в терминальной программе то возникнут траблы - грабли... 

младший байт результата может оказаться служебным непечатаемым символом (ищи и смотри таблицу символов) и тогда мы увидим только символ старшего байта - его я защитил добавлением числа 65, это превратит 0, 1, 2 и 3 (для чисел до 900 возможны только эти значения) в символы A B C и D. 


Как решить эту проблему? 


ЗАДАНИЕ
- напишите функцию, или придумайте алгоритм превращающей двух байтовое число в набор символов - цифр: например число 856 в символы '8' '5' и '6' - это поможет нам при эмуляции легко ориентироваться выводя числа на терминал в привычном десятичном виде.. 


Обратите внимание на новые переменные: 

char low_result; // младший байт результата
char hi_result;   // стрший байт результата
int temp;          // сохранитель результата 

 

Функцию сохранения результата 
в памяти оставляем пока пустой. 


Эмуляцию программы попробуйте сделать самостоятельно.

можете скачать рабочие файлы 
в архиве work04_1.zip










Пошли дальше...  еще вариант!

Для измерения периода более эффективно применение не задействованного Timer1  - это 16 битный таймер, давайте сделаем чтобы он считал со скоростью тиканья кварца и по первому прерыванию INT0 будем запускать таймер1 с 0х0000 а по второму останавливать и считывать два байта результата. 

 

Нам нужно узнать, как управлять этим таймером - читайте DataSheet все что касается Timer1.

Есть еще один способ научится управлять периферией - использовать АпБилдер - генерируя код для разных стартовых состояний Timer1 мы увидим какие биты и в каких регистрах им управляют.  

 

Запускаем ICC и АпБилдер. 

Установим все настройки какие были у нас и нажмем "Preview" - скопируйте текст в отдельный файл _no_timer1.txt для последующего просмотра. закройте окно кода.

Перейдем на закладку: Timer1 
- включите "Use Timer1"
- выберите коэф. деления "1" - таймер будет считать с частотой процессора. 

"Preview" код скопируйте в файл _timer1_on.txt 


Давайте посмотрим, что изменилось в коде: ага, появилась функция инициализации Timer1:

//TIMER1 initialisation - prescale:1
// desired value: 1Hz
// actual value: Out of range
void timer1_init(void)
{

TCCR1B = 0x00; //stop timer

//set count value

TCNT1H = 0x00 /*INVALID SETTING*/; 
TCNT1L = 0x00 /*INVALID SETTING*/;
// с какого числа считать ...
// мы будем с нуля считать

//set compare value

OCR1H = 0x00 /*INVALID SETTING*/;
OCR1L = 0x00 /*INVALID SETTING*/;
// режим сравнения - мы использовать не будем

TCCR1A = 0x00;

TCCR1B = 0x01; //start Timer
}

Смущают только: *INVALID SETTING* - придется почитать DataSheet на предмет допустимости таких значений. Изучите рисунок 30 - схему таймер1.

Эти сообщения вызваны тем что указав Билдеру коэф. пред. делителя "1" мы оставили значение частоты переполнений 1 Гц - что не возможно выполнить - вот он и ругается. 

Почитали: 0x00 являются допустимыми значениями регистров - такими они являются при запуске МК.


Смотрим регистры TCNT1H TCNT1L стр. 33 DataSheet - в них содержится собственно насчитанное значение!

Мы будем запускать счет с нуля по первому прерыванию INT0 , а по второму INT0 или по достижения числа max_count в цикле while(...) (это означает низкую частоту синала), остановим Timer1 и прочитаем в регистрах TCNT1H и TCNT1L результат - число пропорциональное периоду сигнала. 
  


Регистр TCCR1B стр. 32 DataSheet - три младших бита 0_2 этого регистра управляют запуском и коэф. деления см. таблицу на стр. 33 DataSheet - нас интересуют два сочетания этих бит: 

000 - остановить Timer1 

001 - запустить Timer1 с коэф деления "1"

Регистр TCCR1A мы не используем. 

Пожалуйста прочитайте и поймите, что связано с этим регистром в МК, это нужно вам!

 

Давайте включим в функцию инициализации "железа" в нашей программе, строчки посвященные Timer1:

void init_devices(void)
{
// нужно отключить прерывания 
// на время инициализации

CLI(); // запретить все прерывания

port_init();
timer0_init();
uart0_init();

TCCR1B = 0x00; 
// остановить таймер1

TCNT1H = 0x00; 
TCNT1L = 0x00; 
// с какого числа считать ...
// мы будем с нуля считать

TCCR1A = 0x00;

// Мы не запустили Таймер1 
// пока нам он не нужен


MCUCR = 0x02;

GIMSK = 0x00; // INT0 отключено
                     // 0x40 - включено
TIMSK = 0x02;

SEI(); // разрешить прерывания

}





Теперь давайте напишем новую функцию измерения периода сигнала, но для "мгновенного" старта и остановки таймера будем его включать - выключать инвертированием бита в функции - обработчике прерывания - соответственно начнем изменения с неё:

обработчик прерывания INT0 будет таким:

void int0_isr(void)
{

// инвертируем бит_1 регистра TCCR1B
TCCR1B^=0x01;
// если таймер1 стоял - он начал отсчет
// если таймер1 считал - он остановится 

// реакции на INT0 почти мгновенны!

Смотрим Help компилятора: 
Bit Twiddling  - нас интересует "flip bit"

no_INT0 = 0;
// обнулили признак " no_INT0"
}


Запуск и останов таймера1 произойдут с одинаковой задержкой после прерывания - значит результат будет пропорционален периоду сигнала с высокой точностью! 

Задержка - это время реакции МК на прерывание, плюс вызов функции, плюс "переворот" бита.  

Выше мы посчитали, что МК тикает 3686.4 х 2 = 7373 раз за 2 мС. каждый "тик" занимает (1/3.6864) мкС


Период входного сигнала будет равен:
(число в таймере1 / 3.6864 )   мкС





Перепишем функцию измерения периода сигнала: 

void get_period(void)
{
// очистим флаг прерывания INT0
// записью "1" в бит_6 рег. GIFR
GIFR |= 0x40; // turn on bit6 

"Флаг" необходимо "очистить" потому, что в AVR (так и в большинстве других МК) флаги выставляться по возникновении события не зависимо от того - разрешено ли прерывание! 

Это надо учитывать и, 
этим можно пользоваться
!


// включить прерывание по спаду INT0
GIMSK = 0x40;

while(no_INT0); // ждем прерывания по
// очередному спаду сигнала

// мы здесь, значит прерывание случилось
// Taimer1 считает с нуля

no_INT0 = 1;
// восстановим признак ожидания INT0

GIFR |= 0x40; // очистим флаг INT0

counter = 0;

// ждем следующего спада сигнала
// и следим за его длительностью

while((no_INT0) && (counter < max_count))
{
counter ++; // увеличиваем на 1
}
// !!! счетчик переменная "counter" !!!

// из цикла вылетим при прерывании INT0
// по очередному спаду или если период
// слишком большой то по достижении
// числа max_count


// ВЫКЛ. прерывание INT0
GIMSK = 0x00; // можно =0;

no_INT0 = 1;
// восстановим признак ожидания INT0

// возможно сигнал слишком низкочастотный и 
// мы вылетели раньше второго прерывания
// значит нужно остановить таймер1

TCCR1B = 0x00;

// теперь сохраним результат измерения:

low_result = TCNT1L; // младший байт результата
 
hi_result = TCNT1H; // стрший байт результатая


Важно! 

Первым читатьTCNT1L а затем TCNT1H 

При записи в этот регистр - последовательность обращения к 
байтам - противоположная

см. стр. 34 DataSheet - особенности в обращении к 16-битным устройствам AVR!

// восстановим ноль для следующего отсчета
TCNT1H = 0x00; 

TCNT1L = 0x00; 

}

В этой функции мы измерили период входного сигнала - теперь старший и младший байты результата хранятся в переменных low_result и hi_result, а переменная counter позволит нам судить о достаточности частоты сигнала - если она равна max_count значит сигнал слишком медленный. 

Timer1 - остановлен и сброшен в ноль. 

Мы готовы, как пионеры, к следующему измерению. 

 


Давайте модифицируем функцию вывода данных по rs232 - теперь результат будет выводится с новой строки в десятичном виде по 4 цифры, а при низкой частоте сигнала будет выводится сообщение "NO-F" 

Значит при эмуляции программы без входного сигнала мы должны получать в терминале сообщения "NO-F" каждые 20 мс.

Поживем, увидим...


Пишем функцию вывода по новой:

void send_result(void)
{
// если частота более 500 гц
 
if (counter < max_count)
    {

// получим результат как 2-байтовую величину

result = 0; // обнулили результат

result += hi_result; // прибавили старший байт

result << 8; // продвинули старший байт на
// 8 позиций в лево - на место старшего байта

result += low_result; // прибавили младший байт

// теперь result содержит результат измерения

// будем выводить его по одной цифре:

dig_out=0; // цифра для вывода по rs232

while (result >= 1000)
{
  result -= 1000;
// уменьшаем на 1000

  dig_out ++;
// посчитали тысячу
}

putchar(0x0D);
// перешли на новую строку

putchar(dig_out);
// вывели цифру "тысячи"

dig_out = 0; // обнулили

while (result >= 100)
{
  result -= 100;
// уменьшаем на 100

  dig_out ++;
// посчитали сотню
}

putchar(dig_out);
// вывели цифру "сотни"

dig_out = 0; // обнулили

while (result >= 10)
{
  result -= 10;
// уменьшаем на 10

  dig_out ++;
// посчитали десятки
}

putchar(dig_out);
// вывели цифру "десятки"

dig_out = 0; // обнулили

while (result >= 1)
{
  result -= 1;
// уменьшаем на 1

  dig_out ++;
// посчитали единицы
}

putchar(dig_out);
// вывели цифру "единицы"

   }

 

 

// если частота мене 500 гц
 
if (!(counter < max_count))
    {

// выводим посимвольно  "NO-F" с новой строки

putchar(0x0D);
// перешли на новую строку

putchar('N');
putchar('O');
putchar('-');
putchar('F');
// вывели надпись

    }

}
// конец функции


похоже сделали все что хотели...

скопируйте новый текст функции в программу 
и добавьте переменную:

char
dig_out; // цифра для вывода по rs232 



Откомпилировал - ошибок нет, 
использовано 44% ресурсов МК.



можете скачать рабочие файлы 
в архиве work04_2.zip






Эмуляция

Эмуляция не порадовала - программа не заработала, пришлось искать ошибки... Нудно и долго...

Однако все нашел:

1) Сколько раз читал и в коде АпБилдера написано прежде чем разрешить какое-то прерывание нужно:
 
- запретить все прерывания CLI();
- разрешить новое прерывание 
- вновь рзрешить все прерывания SEI();

Я этого не сделал и прерывание INT0 по сигналу не включалось - программа зависала на месте.


2) не учел что код чисел 1,2,3 ... это 49,50,51 ... посему в терминале шла всякая бяка... 

прибавил 48 во всех выводах цифр на терминал:
 
putchar(dig_out + 48);

2) не правильно записал операцию сдвига битов в лево на 8 позиций:

написал так: << 8;   

исправил на:  <<= 8;  


3) при низкочастотном сигнале зависали в первом while в функции get_period()  

- добавил и там вылет при большом периоде и еще добавил if () и goto для обработки этой ситуации 

4) подкорректировал число max_count по результату эмуляции на 315 - при этом измеряются периоды сигнала не превышающие 2000 мкС - в противном случае выводятся сообщения в терминал "NO_F"  


в общем программа ожила и заработала по предписанному алгоритму. 

скачайте исправленные файлы компилятора и эмулятора в архиве work04_3.zip

смотрите код - эмулируйте, можете изменять входную частоту, посчитайте и убедитесь в пропорциональности результата и периода входного сигнала. 

Кстати максимальная частота входного сигнала значительно больше  2 кГц - попробуйте ее определить!

Захотите зашить программу в МК
смотрите раздел курса о прошивании.