Учебный курс. АЦП микроконтроллера AVR или как подключить 4 кнопки к одному выводу. Часть 1

05/10/2009 - 21:00

АЦП микроконтроллера AVR

   Микроконтроллер общается с внешним миром посредством портов ввода/вывода. В общем случае он может “воспринимать” только цифровые сигналы – логический ноль или логическую единицу. Например, для микроконтроллера ATmega8535 при напряжении питания 5 В логический ноль – это напряжение от 0 до 1,3 В, а логическая единица – от 1,8 до 5 В. Довольно часто возникает потребность измерять напряжения, которые могут принимать любое значение в диапазоне от 0 до напряжения питания. Для этих целей в составе микроконтроллеров AVR есть аналого-цифровой преобразователь (АЦП).
  Не вдаваясь в подробности внутреннего устройства АЦП, представим его в виде черного ящика. На вход АЦП подается непрерывный аналоговый сигнал, а на выходе получается последовательность цифровых значений. АЦП имеет много характеристик, но в качестве основных можно назвать  разрешающую способность, абсолютную точность, предельную частоту дискретизации и диапазон входных напряжений.
   Разрешающая способность (разрешение)  – это способность АЦП различать два значения входного сигнала. Определяется как величина обратная максимальному числу кодовых комбинаций на выходе АЦП. У AVRа АЦП 10-ти разрядный. Максимальное число кодовых комбинаций будет равно 210 = 1024. Разрешающая способность равна 1/1024 от всей шкалы допустимых входных напряжений.
  Для работы АЦП необходим источник опорного напряжения (ИОН). Для него это эталон, по отношению к которому он измеряет входные сигналы. Микроконтроллеры AVR позволяют в качестве ИОНа использовать напряжение питания, внутренний опорный источник на 2,56 В и напряжение на выводе AREF (внешний ИОН).
   Напряжение питания в нашей схеме 5 В, тогда 1/1024 от всей шкалы это 5 * 1/1024 = 0,0048 В или примерно 5 мВ. С таким шагом (это называется шаг квантования) АЦП будет измерять входное напряжение. Если два ближайших значения сигнала на входе АЦП будут отличаться между собой на величину < 5 мВ, АЦП воспримет их как одинаковые. На практике разрешающая способность АЦП ограничена его шумами.
   Абсолютная точность – отклонение реального преобразования от идеального. Это составной результат нескольких погрешностей АЦП. Выражается в количестве младших значащих разрядов (LSB - least significant bit) АЦП. Для AVRа абсолютная погрешность АЦП  =  ±2LSB. Для нашего примера абсолютная точность будет равна 2 * 5 мВ = ±10 мВ.
  Предельная частота дискретизации определяет быстродействие АЦП и измеряется в герцах или количестве выборок в секунду (SPS – samples per second). Для микроконтроллеров AVR эта величина равна 15 kSPS (килло семплов в секунду). Практически АЦП AVRа работает и быстрее, но при этом  его точность ухудшается.
   Теорема Котельникова (теорема Найквиста-Шеннона, теорема о выборке) гласит, что аналоговый сигнал имеющий ограниченный спектр, может быть восстановлен однозначно и без потерь по своим дискретным отсчётам, если частота выборки (дискретизации) превышает максимальную частоту спектра  сигнала более чем в 2 раза. Выражаясь по-простому - если вам нужно оцифровать аналоговый сигнал с полосой спектра 0 - 7 КГц, то в идеальном случае частота дискретизации должна быть > удвоенной максимальной частоты спектра этого сигнала, то есть > 14 КГц. На практике все намного сложнее. Перед входом АЦП всегда ставят НЧ фильтр, чтобы ограничить спектр сигнала, а частота дискретизации выбирается еще более высокой. 
   Диапазон входных напряжений – это минимальное и максимальное значение напряжения, которое можно подавать на вход АЦП. Для микроконтроллера AVR он равен 0 – Vcc (напряжение питания)


Если сравнить АЦП с линейкой, то минимальное и максимальное значение шкалы - диапазон входных напряжений, количество делений линейки– число кодовых комбинаций (число уровней квантования), расстояние между двумя соседними делениями  - шаг квантования, а шаг квантования деленный на диапазон входных напряжений – разрешающая способность.


Пояснения к схеме

   Для знакомства с модулем АЦП микроконтроллера AVR я выбрал простую, но интересную схему. Взгляните на рисунок. Четыре кнопки подключены к делителю напряжения (резисторы R8…R11). При нажатии они коммутируют на вход нулевого канала АЦП разные напряжения. Измеряя эти напряжения в прерывании АЦП и определяя в какой диапазон они попадают, микроконтроллер будет распознавать номер нажатой кнопки.
   Резистор R7 нужен чтобы вход АЦП не “болтался в воздухе”, когда все кнопки отпущены. Если этого не сделать АЦП будет ловить помехи. Номинал резистора R7 выбран большим, чтобы не оказывать влияние на делитель напряжения.
   R6 и C7 - низкочастотный фильтр для защита от дребезга кнопок и помех. (Вообще НЧ фильтры перед АЦП обычно используются для защиты от такого явления как наложение, но это отдельная история). Дополнительно резистор R6 играет функцию токоограничения, без него при нажатии кнопки S4 вывод микроконтроллера напрямую соединялся бы с плюсом питания. А это нежелательно.
   Для индикации номера нажатой кнопки в схеме используются 4 светодиода.
   Микроконтроллер - ATMega8535. Описание регистров АЦП ниже по тексту приводится именно для него. Для других микроконтроллеров могут быть некоторые отличия.
схема подключения кнопок к АЦП AVRа

Задача

Для приведенной выше схемы, написать программу определяющую номер нажатой кнопки.

Алгоритм программы такой:

Основная программа
   Инициализация портов
   Инициализация АЦП
   Разрешение прерываний
   Бесконечный цикл
   {
    Если кнопка нажата, зажечь нужный светодиод
    Если нет, погасить все светодиоды
   }
 
Обработчик прерывания АЦП
     Считать напряжение на входе АЦП
     Определить в какой диапазон оно попадает
     Записать номер кнопки в буфер

 

Начальный код программы


//программирование микроконтроллеров AVR на Си - осваиваем АЦП

#include <ioavr.h>
#include <intrinsics.h>
 
//макрос для запуска преобразования
#define StartConvAdc()  ADCSRA |= (1<<ADSC)

int main(void)
{

//ион - напряжение питания, выравнивание влево, нулевой канал
ADMUX = (0<<REFS1)|(1<<REFS0)|(1<<ADLAR)|(0<<MUX3)|(0<<MUX2)|(0<<MUX1)|(0<<MUX0);
//вкл. ацп, режим одиночного преобр., разрешение прерывания,частота преобр. = FCPU/128
ADCSRA = (1<<ADEN)|(0<<ADSC)|(0<<ADATE)|(1<<ADIE)|(1<<ADPS2)|(1<<ADPS1)|(1<<ADPS0);


__enable_interrupt
();
StartConvAdc();
  while(1)
  {

    //какой-нибудь код

  }
  return 0;
}

//обработчик прерывания АЦП
#pragma vector=ADC_vect
__interrupt void adc_my(void)
{
   unsigned char AdcBuf = ADCH;
 
  //какой-нибудь код

  StartConvAdc();
}


Пояснения к коду

Макрос для запуска преобразования

StartConvAdc() – это макрос для запуска преобразования АЦП. Он устанавливает бит ADSC в регистре управления и состояния ADCSRA.
 
Инициализация АЦП

Чтобы запустить модуль АЦП, его нужно предварительно настроить. За это отвечают три регистра:
Регистр управления мультиплексором - ADMUX
Регистр управления и состояния - ADCSRA
Регистр специальных функций - SFIOR

Регистр ADMUX

   Модулю АЦП для работы нужен источник опорного напряжения (ИОН). За выбор ИОНа отвечают  биты REFS1, REFS0. В нашем случае опорное напряжение это напряжение питания (именно поэтому мы завели вывод AVcc  на +5В) поэтому REFS1 - 0, REFS0 - 1
   АЦП микроконтроллера AVR 10-ти разрядный, а сам камень 8-ми. Поэтому результат преобразования хранится в двух регистрах (ADCH, ADCL). Бит ADLAR задает направление выравнивания результата преобразования. 0 – выравнивание вправо (в ADCH заняты 2 младших бита,  ADCL занят весь), 1 – выравнивание влево (ADCH занят весь, в ADCL только 2 старших бита).  В нашем примере не требуется высокой точности преобразования, поэтому было бы удобно выровнять результат влево и работать только с регистром ADCH. Устанавливаем ADLAR - 1
  Чисто физически в микроконтроллере AVR всего один АЦП. Но перед ним стоит мультиплексор, который позволяет подключать любой из 8-ми выводов порта к входу АЦП. Номер выбранного в данный момент канала задается битами ADMUX3,  ADMUX2,  ADMUX1,  ADMUX0. Мы используем нулевой канал, поэтому все биты равны 0.
 
//ион - напряжение питания, выравнивание влево, нулевой канал
ADMUX = (0<<REFS1)|(1<<REFS0)|(1<<ADLAR)|(0<<MUX3)|(0<<MUX2)|(0<<MUX1)|(0<<MUX0);

Регистр ADCSRA

   Чтобы АЦП заработал его надо включить, то есть установить бит ADEN – 1
    Запуск преобразования осуществляется установкой бита ADSC -1. Во время инициализации мы не будем запускать АЦП, поэтому ADSC – 0
   АЦП может работать в двух режимах: одиночное преобразование - когда каждое преобразование запускается программно и непрерывное - когда преобразование запускается один раз программно, а перезапускается автоматически. В нашем примере преобразование одиночное, бит ADATE – 0.
   Когда АЦП закончил преобразование, он подает запрос на прерывание. Чтобы разрешить прерывание нужно установить бит ADIE – 1
  Модулю АЦП для работы требуется тактовый сигнал. Он формируется из тактового сигнала микроконтроллера путем деления на фиксированные коэффициенты. Для установки коэффициентов предделителя предназначены биты ADSP2, ADSP1, ADSP0. Поскольку мы будем опрашивать с помощью АЦП кнопки, а это очень медленное устройство,  выбираем самый большой коэффициент (то есть самую низкую частоту). Все биты в 1.

//вкл. ацп, режим одиночного преобр., разрешение прерывания, F преобр. = FCPU/128
  ADCSRA = (1<<ADEN)|(0<<ADSC)|(0<<ADATE)|(1<<ADIE)|(1<<ADPS2)|(1<<ADPS1)|(1<<ADPS0);

Про все биты рассказал, кроме ADIF. ADIF - это флаг прерывания. Он устанавливается аппаратно, когда преобразование завершено.

Регистр SFIOR

   Биты ADTS2, ADTS1, ADTS0 определяют источник, запускающий процедуру преобразования. Устанавливать эти биты нужно только в режиме непрерывного преобразования. У нас режим одиночный, поэтому регистр не трогаем.
 
Обработчик прерывания АЦП

#pragma vector=ADC_vect
__interrupt void adc_my(void)
{
  unsigned char AdcBuf = ADCH;

  //какой-нибудь код

  StartConvAdc();
}

  Здесь все должно быть понятно. Синтаксис обработчика прерывания разбирался. ADCH – старший регистр данных модуля АЦП. StartConvAdc() – запуск преобразования перед выходом из обработчика.
   Для особо нетерпеливых выкладываю файлы проекта. АЦП микроконтроллера AVR с чего начать.
 
 

Comments   

# skiff 2009-10-04 06:22
Спасибо тебе Pashgan за статью!
# Frf 2009-11-08 15:31
Действительно хорошая статья.Спасибо!
# alexandershahbazov 2009-12-08 21:51
Побольше про экономию ножек .
У меня одни ATmega8 .
Например про буферные микросхемы .
В статье "Учебный курс. Подключение lcd к микроконтроллер у. Получение первых результатов " все идет напрямую на ножки . После этого мало ножек остается
для других дел . Я до сих пор делал
напрямую при подключении ЖКИ на ATmega16
хотя и знал про 74HC164 .
# Guest 2010-01-11 00:01
А как вывести данные с АЦП на LCD?
# Pashgan 2010-01-14 06:12
Считать результат преобразования АЦП, перевести в двоично-десятич ные числа, затем в символы и вывести на lcd. В разделе "Библиотеки для IARa" есть для этого исходник - bcd.rar
# Guest 2010-04-28 09:10
Pashgan,добрый день, прочитал Ваши статьи, и все очень понравилось.
Я вообще начинающий в этом деле. И хотелось бы узнать, а как можно сделать так, чтобы считывать сигналы не с одного входа АЦП, а, например, с 2х входов? Сильно ли при этом изменится программа?
Спасибо!
# Pashgan 2010-04-29 14:49
misha, не сильно. В прерывание АЦП нужно добавить код, который будет менять номер канала. Он задается с помощью регистра ADMUX.
# Guest 2010-04-29 16:11
Pashgan, просто вот не особо понятно, что как в теле прерывания ацп, где происходит присваивание результата преобр-я в какой-то регистр, например на PORTC, и там же ,получается, прописывается уже ADMUX с другим каналом. А тогда где нужно прописать в программе, чтобы происходила запись результата 2го преобразования в другой регистр, например PORTB? (( чтобы не было путаницы?
спасибо !!!
# Pashgan 2010-05-03 14:02
Misha, сделай тему на форуме. Я там выложу какой-нибудь код.
# Guest 2010-07-01 18:20
Quote:
Дополнительно резистор R7 играет функцию токоограничения, без него при нажатии кнопки S4 вывод микроконтроллера напрямую соединялся бы с плюсом питания.
А R6 разве не ограничит ток при нажатии S4?
# Pashgan 2010-07-30 16:12
ага.. опечатался, исправил.
# Botan 2010-08-19 13:53
Добрый день, Misha тему на форуме так и не создал, тогда я с вашего позволения это сделаю.
# Яков 2011-02-25 14:41
Благодарю Автору за статью все просто и доходчиво побольше бы таких пока не дочитал до конца но весь вечер впереди :-)
# Pashgan 2011-02-27 09:38
Спасибо
# Guardian 2011-03-06 21:28
как можно использовать ацп для преобразования одновременно двух источников
# Димка 2011-07-10 18:39
У меня компилятор дал ошибку identifier "ADCSRA" is undefined что это может быть?
# Димка 2011-07-12 10:41
Во, нашёл причину, вместо ADCSRA надо писать ADCSR - эта хрень на mega8
# sadmi 2011-08-23 04:17
Спасибо, благодаря статье разобрался с АЦП. Но не могли Вы рассказать про вариант с непрерывным преобразованием ?
# Artempre 2011-11-26 22:39
Pashgan Спасибо тебе огромное! Я уверился что Си всё таки не сложнее китайского языка! Ранее было наоборот! :-)
# Асель 2012-01-15 06:04
полезная инфа без лишнего красноречия.спа сибо!помогли
# MS 2012-03-19 14:24
Спасибо тебе ГИГАНТСКОЕ!!! ПРОСТО ТЕРА - ЗЕТА СПАСИБО!!! =))
# Bac9 2012-03-20 08:18
А можно как нибудь на один порт только 2 кнопки подключить? У меня напряжение в цепи 12 вольт.
# radiolomaster 2012-12-26 17:55
Подскажите пожалуйста, вот я хочу перенести эту программу на AT90S4434, у него некоторые регистры по другому называются, но я вроде все поменял как надо. Но у этого контроллера нет возможности сдвигать результат влево или вправо, т.е. ADCH всегда содержит 2 старших бита, а ADCL - 8 младших. У меня результат выводится на 7-сегментный индикатор. Так вот, если в индикатор отправлять значение из ADCH - все работает, только на индикаторе выводится число от 0 до 3, при изменении напряжения от 0 до 5 В. А если отправлять туда значение ADCL - то результат выводится один раз при запуске, и потом не реагирует на изменение напряжения. Измеряет вроде правильно - от 0 до 255 число выводится. Это все проверялось в протеусе, в железе пока не собирал. Протеус ругается "adc result lost due pending read", и если не отключить предупреждения то схема вообще виснет. Думал может конфликтует с таймером (он управляет индикатором), но нет, с выключенным таймером тоже ругается.
# Pashgan 2012-12-26 18:47
Тогда сохраняй значение ADCH и ADCL в 16-ти разрядную переменную и в ней сдвигай результат.
# radiolomaster 2012-12-27 17:45
Так тоже ругается, как только пытаюсь прочитать значение из ADCL ругается.
# radiolomaster 2012-12-27 18:25
А, я кажется разобрался, нужно читать не ADCL и ADCH, а регистр ADC, который состоит из этих двух. Тогда все работает. Но все-таки не понятно почему нельзя читать только ADCL, а ADCH - можно.
# Pashgan 2012-12-27 18:30
При чтении регистров данных АЦП нужно соблюдать определенный порядок. Сначала читается ADCL, затем ADCH. Когда читается АDC, компилятор на самом деле вставляет на ассемблере код чтения ADCL и ADCH в нужной последовательно сти.
# radiolomaster 2012-12-27 18:38
Да, точно, если сначала прочитать ADCL, а затем ADCH то все работает, а отдельно ADCL не хочет. Просто если например значение старших разрядов не интересует, то их все-равно придется прочитать.
Спасибо большое!
# РусланР. 2013-05-27 22:46
Да исправьте уже наконец ссылку на вторую страницу. Она ведет на первую страницу.
# Илья21 2013-08-13 09:41
Очень хорошо все описано! Спасибо автору, ведь на таких статьях выростают настоящие технари и программисты!!!
# Иван Смит 2013-09-22 12:14
RC фильтр у меня вызывает сомнения... у него частота среза ~8kHz, при том, что "период" дребезга 20мс. Это действительно работает?
# Pashgan 2013-09-23 00:28
Работает. В коде еще сделана программная защита от дребезга.
# eugen 2014-01-05 08:30
У меня все равно не хочет на вторую страницу переходить(а хотелось бы дочитать!
# Pashgan 2014-01-07 05:30
Поправил.
# марс 2014-12-16 06:30
сп огромное хоть чеуть стало ясно с ацп а можно еще про шим и т.д.
# 12val12 2015-01-26 14:26
удобно использовать блоки кнопок от телевизоров
схема такая .90% кнопок от тв однопроводные и идут на вход ацп MICOM TV
1-4 кОм от порта к +5В
а модуль кнопок корпус к корпусу и выходс кнопок на порт
# jackbl 2016-12-29 13:48
В реальной схеме логично было бы R11 исключить, а R7 сделать 2К. Это бы снизило энергопотреблен ие при не нажатых кнопках.
# Василий 2017-08-27 19:03
Не подскажете, как должны быть сконфигурирован ы выводы порта (в данном случае PORTC) которые потом подключаются к АЦП? как вход или как выход? заранее благодарю

У вас недостаточно прав для комментирования.