АЦП микроконтроллера ATmega8, цифровой вольтметр

Работа с АЦП микроконтроллера ATmega8

АЦП – аналогово-цифровой преобразователь (ADC- Analog-to-Digital Converter). Преобразует некий аналоговый сигнал в цифровой. Битность АЦП определяет точность преобразования сигнала. Время преобразования – соответственно скорость работы АЦП. АЦП встроен во многих микроконтроллерах семейства AVR и упрощает использование микроконтроллера во всяких схемах регулирования, где требуется оцифровывать некий аналоговый сигнал.
Рассмотрим принцип работы АЦП. Для преобразования нужен источник опорного напряжения и собственно напряжение, которое мы хотим оцифровать (напряжение, которое преобразуется должно быть меньше опорного). Также нужен регистр, где будет храниться преобразованное значение, назовем его Z. Входное напряжение = Опорное напряжение*Z/2^N, где N – битность АЦП. Условимся, что этот регистр, как у ATmega8, 10-ти битный. Преобразование в нашем случае проходит в 10 стадий. Старший бит Z9 выставляется в единицу.

Далее генерируется напряжение (Опорное напряжение*Z/1024), это напряжение, с помощью аналогового компаратора сравнивается с входным, если оно больше входного, бит Z9 становиться равным нулю, а если меньше – остается единицей. Далее переходим к биту Z8 и вышеописанным способом получаем его значения. После того, как вычисление регистра Z окончено, выставляется некий флаг, который сигнализирует, что преобразование закончено и можно считывать полученное значение. На точность преобразования могут очень сильно влиять наводки и помехи, а также скорость преобразования. Чем медленнее происходит преобразования – тем оно точней. С наводками и помехами следует бороться с помощью индуктивности и емкости, как советует производитель в даташите:
схема фильтрации шумов

В микроконтроллерах AVR как источник опорного напряжения может использоваться вывод AREF, или внутренние источники 2,56В или 1,23В. Также источником опорного напряжения может быть напряжение питания. В некоторых корпусах и моделях микроконтроллеров есть отдельные выводы для питания АЦП: AVCC и AGND. Выводы ADCn – каналы АЦП.

С какого канала будет оцифровываться сигнал можно выбрать с помощью мультиплексора.
Теперь продемонстрируем примером сказанное выше. Соорудим макет, который будет работать как вольтметр с цифровой шкалой. Условимся, что максимальное измеряемое напряжение будет 10В. Также пусть наш макет выводит на ЖКИ содержимое регистра ADC.

Схема подключения:


Обвязка микроконтроллера и ЖКИ WH1602A стандартна. X1 – кварцевый резонатор на 4 Мгц, конденсаторы С1,С2 – 18-20 пФ. R1-C7 цепочка на выводе reset по 10 кОм и 0,1 мкФ соответственно. Сигнальный светодиод D1 и ограничивающий резистор R2 200 Ом и R3 – 20 Ом. Регулировка контраста ЖКИ – VR1 на 10 кОм. Источник опорного напряжения мы будем использовать встроенный на 2,56В. С помощью делителя R4-R5 мы добьемся максимального напряжения 2,5В на входе PC0, при напряжении на щупе 10В. R4 – 3 кОм, R5 – 1 кОм, в их номиналу нужно отнестись тщательно, но если не возможности подобрать точно такие, можно сделать любой резистивный делитель 1:4 и программно подкорректировать показания, если это потребуется. Дроссель на 10мкГн и конденсатор на 0,1 мкФ для устранения шумов и наводок на АЦП на схеме не показан. Их наличие подразумевается само собой, если используется АЦП. Теперь дело за программой:

Программа на языке Си:

  1. #include <avr/io.h>
  2.  
  3. #define RS 2 //RS=PD2
  4. #define E 3 //E=PD3
  5.  
  6. #define TIME 10 //Константа временной задержки для ЖКИ
  7. //Частота тактирование МК - 4Мгц
  8.  
  9. #define R_division 3.837524 //=R4/R5 константа
  10.  
  11. unsigned int u=0; //Глобальная переменная с содержимым преобразования
  12.  
  13. void pause (unsigned int a)
  14. {
  15. unsigned int i;
  16. for (i=a;i>0;i--);
  17. }
  18.  
  19. void lcd_com (unsigned char lcd) //Передача команды ЖКИ
  20. {
  21. unsigned char temp;
  22.  
  23. temp=(lcd&~(1<<RS))|(1<<E); //RS=0 – это команда
  24. PORTD=temp; //Выводим на portD старшую тетраду команды, сигналы RS, E
  25. asm("nop"); //Небольшая задержка в 1 такт МК, для стабилизации
  26. PORTD=temp&~(1<<E); //Сигнал записи команды
  27.  
  28. temp=((lcd*16)&~(1<<RS))|(1<<E); //RS=0 – это команда
  29. PORTD=temp; //Выводим на portD младшую тетраду команды, сигналы RS, E
  30. asm("nop"); //Небольшая задержка в 1 такт МК, для стабилизации
  31. PORTD=temp&~(1<<E); //Сигнал записи команды
  32.  
  33. pause(10*TIME); //Пауза для выполнения команды
  34. }
  35.  
  36. void lcd_dat (unsigned char lcd) //Запись данных в ЖКИ
  37. {
  38. unsigned char temp;
  39.  
  40. temp=(lcd|(1<<RS))|(1<<E); //RS=1 – это данные
  41. PORTD=temp; //Выводим на portD старшую тетраду данных, сигналы RS, E
  42. asm("nop"); //Небольшая задержка в 1 такт МК, для стабилизации
  43. PORTD=temp&~(1<<E); //Сигнал записи данных
  44.  
  45. temp=((lcd*16)|(1<<RS))|(1<<E); //RS=1 – это данные
  46. PORTD=temp; //Выводим на portD младшую тетраду данных, сигналы RS, E
  47. asm("nop"); //Небольшая задержка в 1 такт МК, для стабилизации
  48. PORTD=temp&~(1<<E); //Сигнал записи данных
  49.  
  50. pause(TIME); //Пауза для вывода данных
  51. }
  52.  
  53. void lcd_init (void) //Иниализация ЖКИ
  54. {
  55. lcd_com(0x2c); //4-проводный интерфейс, 5x8 размер символа
  56. pause(100*TIME);
  57. lcd_com(0x0c); //Показать изображение, курсор не показывать
  58. pause(100*TIME);
  59. lcd_com(0x01); //Очистить DDRAM и установить курсор на 0x00
  60. pause (100*TIME);
  61. }
  62.  
  63. unsigned int getADC(void) //Считывание АЦП
  64. { unsigned int v;
  65.  
  66. ADCSRA|=(1<<ADSC); //Начать преобразование
  67.  
  68. while ((ADCSRA&_BV(ADIF))==0x00) //Дождатся окончания преобразования
  69. ;
  70.  
  71. v=(ADCL|ADCH<<8);
  72. return v;
  73. }
  74.  
  75. void write_data (unsigned int u)
  76. { unsigned char i;
  77. double voltage=0;
  78.  
  79. lcd_com(0x84); //Вывод регистра ADC на ЖКИ
  80. for (i=0;i<10;i++)
  81. if ((u&_BV(9-i))==0x00) lcd_dat (0x30);
  82. else lcd_dat (0x31);
  83.  
  84. lcd_com(0xc2);
  85. voltage= R_division*2.56*u*1.024; //Расчет напряжения
  86.  
  87. i=voltage/10000; //Выведение напряжения на ЖКИ
  88. voltage=voltage-i*10000;
  89. if (i!=0) lcd_dat(0x30+i);
  90.  
  91. i=voltage/1000;
  92. voltage=voltage-i*1000;
  93. lcd_dat(0x30+i);
  94.  
  95. lcd_dat(',');
  96.  
  97. i=voltage/100;
  98. voltage=voltage-i*100;
  99. lcd_dat(0x30+i);
  100.  
  101. i=voltage/10;
  102. voltage=voltage-i*10;
  103. lcd_dat(0x30+i);
  104.  
  105. lcd_dat('v');
  106. }
  107.  
  108. int main(void)
  109. {
  110. DDRD=0xfc;
  111.  
  112. pause(3000); //Задержка для включения ЖКИ
  113. lcd_init(); //Инициализация ЖКИ
  114.  
  115. lcd_dat('A'); //Пишем "ADC=" и "U=" на ЖКИ
  116. lcd_dat('D');
  117. lcd_dat('C');
  118. lcd_dat('=');
  119. lcd_com(0xc0);
  120. lcd_dat('U');
  121. lcd_dat('=');
  122.  
  123. ADCSRA=(1<<ADEN)|(1<<ADPS1)|(1<<ADPS0);
  124. //Включаем АЦП, тактовая частота бреобразователя =/8 от тактовой микроконтроллера
  125. ADMUX=(1<<REFS1)|(1<<REFS0)|(0<<MUX0)|(0<<MUX1)|(0<<MUX2)|(0<<MUX3);
  126. //Внутренний источник опорного напряжения Vref=2,56, входом АЦП является PC0
  127.  
  128. while(1)
  129. {
  130. u=getADC(); //Считываем данные
  131. write_data(u); //Выводим их на ЖКИ
  132. pause(30000);
  133. }
  134.  
  135. return 1;
  136. }

Программа проста. В начале мы инициализируем порты ввода/вывода. Для того, чтобы служить входом АЦП, пин PC0 должен работать на вход. Далее проводим инициализацию ЖКИ и АЦП. Инициализация АЦП заключается в его включении битом ADEN в регистре ADCSRA. И выбора частоты преобразования битами ADPS2, ADPS1, ADPS0 в том же регистре. Также выбираем источник опорного напряжения, биты REFS1 REFS0 в регистре ADMUX и вход АЦП: биты MUX0,MUX1,MUX2, MUX3 (в нашем случаем входом АЦП является PC0, поэтому MUX0.3=0). Далее, в вечном цикле, начинаем преобразования установкой бита ADSC в регистре ADCSRA. Дожидаемся окончания преобразования (бит ADIF в ADCSRA становиться равным 1). Далее вынимаем данные из регистра ADC и выводим их на ЖКИ. Вынимать данные из ADC нужно в такой последовательности: v=(ADCL+ADCH*256); если использовать v=(ADCH*256+ADCL); - в упор не работает.

Так-же есть хитрость, чтобы не работать с дробными числами. Когда производиться вычисления входного напряжения в вольтах. Мы просто будем хранить наше напряжения в милливольтах. Например, значение переменной voltage 4234 означает, что мы имеем 4,234 вольта. Вообще операции с дробными числами кушают очень много памяти микроконтроллера (наша прошивка вольтметра весит чуть больше 4 килобайт, это половина памяти программ ATmega8!), их рекомендуется использовать только при особой необходимости. Вычисления входного напряжения в милливольтах просто: voltage=R_division*2.56*u*1.024;
Здесь R_division – коэффициент резистивного делителя R4-R5. Так, как реальный коэффициент делителя может отличаться от расчетного, то наш вольтметр будет врать. Но подкорректировать это просто. С помощью тестера меряем некое напряжение, получаем X вольт, а наш вольтметр пускай показывает Y вольт. Тогда R_division = 4*X/Y, если Y больше X и 4*Y/X если X больше Y. На этом настройка вольтметра завершена, и им можно пользоваться.

Скачать прошивку в виде проекта под AVR Studio 4.
Как работает вольтметр можно ознакомиться на видео:

Видео работы устройства:

Также можно доработать свой блок питания. Вставив в него цифровой вольтметр-амперметр на ЖКИ и защиту от перегрузки (для измерения тока нам понадобиться мощный шунт сопротивлением порядка 1 Ом).
фото блока питания
В свой блок питания я встроил еще защиту от перегрузки, когда ток превышает 2А, то пьезо пищалка начинает усердно пищать, сигнализируя о перегрузке:

Зачем в этом выражении

Зачем в этом выражении voltage=R_division*2.56*u*1.024;
1,024 ? Кто может объяснить?

Если в этой формуле совсем другие величины. Входное напряжение = Опорное напряжение*Z/2^N, где N – битность АЦП.

Начало статьи совсем не

Начало статьи совсем не понятное.

Код заработал? В программе

Код заработал? В программе разобрался?
Если да, то все ок )))

точнее - "огромная куча"

точнее - "огромная куча"

Это к чему комментарий?

Это к чему комментарий?

Прошивка МК

Очень полезная статья. А прошивочкой не поделитесь для блока питания с измерением тока и напряжения как на фото и втором ролике? Заранее спасибо.

"Вынимать данные из ADC нужно

"Вынимать данные из ADC нужно в такой последовательности: v=(ADCL+ADCH*256); если использовать v=(ADCH*256+ADCL); - в упор не работает."
Логично, потому что из даташита:
"ADCL must be read first, then ADCH."

АЦП мк atmega8

Cобрал схемку в протеусе но она почемуто неработает -(ножки мк переключаются но на индикаторе ничегно непоказывает...)
можете подсказать в чём проблема?
просто при отладке неочень удобно каждый раз перепрошивать при изменении какогото параметра...

Constant

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

В протеусе проекты,

В протеусе проекты, размещенные на этом сайте могут работать некорректно. Так как это не тестировалось, а сразу все делалось в железе.

Могу подсказать: выкинь нафиг

Могу подсказать:
выкинь нафиг протеус, собери в железяке!

Вольтметр на мк

Спасибо большое за статью. Крайне полезная. Только вот не могу понять где должны быть умомянутые дроссель и кондер для устранения шумов и наводок на АЦП. И еще вопрос, если мне необходимо мерить не постоянное напряжение, то схема остается той же? По сути у меня есть датчик давления масла в двигателе, который выдает от 0 до 5В. Естественно напряжение будет меняться в зависимости от оборотов двигателя.
Заранее большое спасибо!

Дроссель и кондер для

Дроссель и кондер для устранения шумов на первой картинке. Угу в случае твоего датчика давления можно юзать эту схему без изменений (только делитель переделать лучше).

Вопросы

Просветите, пожалуйста, несколько моментов:
1 как долго происходит обработка АЦП? Сколько тактов?
2 Если используется более одного АЦП показания считываются сначала с ADC0 потом с ADC1, соответственно затрачивается вдвое больше времени? или это происходит одновременно?
3 Можно ли применять разные опорные напряжения для разных адаков, например 2,56 для нулевого (измерять напряжение ) и 1,23 для другого (измерять ток с достаточной точностью и малой просадкой)?

И последний вопрос немного в сторону: не встречался ли более менее толковый даташит на мегу8 на русише?

Спасибо!

1) Время преобразования =

1) Время преобразования = (Разрядность АЦП+1)*время такта для АЦП, которое задается делителем. Чем длиннее преобразование - тем больше точность (до некоторого порога).
2) Одновременно встроенный АЦП не может преобразовывать более одного сигнала. Т.е. надо вдвое больше времени.
3) Можно применять разные опорные напряжения для разных каналов. Меряем на одном канале, меняем настройки АЦП, меряем на втором канале, снова меняем настройки.

Неа, русского даташита под mega8 не видел. Только аккуратно стоит русскими даташитами пользоваться, все-таки не официал. Часто встречался что проще было перевести с инглиша, чем понять машинный русский перевод документации.

Расскажите подробней на

Расскажите подробней на примере, как корректировать R_division(коефициент резистивного делителя R4-R5). Не понятно, откуда взялась четверка и когда что на что делить.

Нужно знать какое

Нужно знать какое максимальное напряжение мы хотим вольтметром измерять, пускай это U. Исходя из этого подбирается делитель с такими условиями, что когда на щупе U, то на входе АЦП МК 2.56В. То есть, например, если мы хотим мерять от 0 до 20В, то нам нужен делитель: R4/R5 = (20-2,56)/2,56=6,8125=R_division.
Теперь подбираем резисторы для делителя и немножко корректируем R_division (так как может не оказаться нужных номиналов резисторов).

Хорошо, делитель я

Хорошо, делитель я рассчитал.
Меряю напряжение тестером - 14 В,
вольметром на МК - 12,5 В.
Как скорректировать эту погрешность?
Другими словами как провести калибровку вольтметра на МК?

Именяй r_division, добейся

Именяй r_division, добейся одинаковых показаний вольтметра и тестера

А как сделать индикацию на

А как сделать индикацию на 7-сегментных индикаторах?

http://avrlab.com/node/130

Подскажите как переделать на

Подскажите как переделать на максимальное напряжение 130V и ток 40А. и при достижении установленного напряжения (95-120V) срабатывало реле отключения нагрузки. Буду очень благодарен.

А какое напряжение?

А какое напряжение? Переменное? Постоянное?

Если постоянное, то проще всего сделать резистивный делитель. Например 20 кОм и 510 кОм. При достижении напруги 120В в АЦП будет 0х039А (если источник опорного АЦП 2,56В). Алгоритм работы простой: если не требуются сверхскорость реакции, то ставим вечный цикл, в котором будет опрашится АЦП, и если значение в ADC вышло 0х039А, то что-то делаем с реле. Вот.

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

Чтобы не было ложных срабатываний нужно развязать аналоговую и цифровую землю. И сравнивать с 0х039А среднее арифметическое нескольких десятков измерений АЦП. Чтобы отсечь резкие всплески.

Подскажите пожалуйста

Каков диапозон входного напряжения ADC? Можно ли его менять и если да, то каким образом?

Спасибо!

Диапазон входного напряжения

Диапазон входного напряжения в встроенном АЦП микроконтроллера составляет от и до Vref.

Vref задается комбинацией битов REFS1, REFS0.
таблица refs
В нашем случае это внутренний генератор напряжения 2,56В. Т.е. диапазон входных напряжения от 0 до 2,56В. Также можно задать диапазон от 0 до питающего напряжения микроконтроллера или от нуля до напряжения на выводе Vref микроконтроллера. (Это рассуждения для микроконтроллера atmega8)

А схему БП посмотреть можно?

А схему БП посмотреть можно?

Можно, сам БП старше меня

Можно, сам БП старше меня :)
Вот схема силовой части:

Микроконтроллерная часть:

А это первоисточник, статья из Радио №6 за 1986

ссыли обновите

ссылки на блок питания обновите

Сдвиг

Добрый день.
Функцию getADC, мне кажется логичнее сделать так.

  1.  
  2. unsigned int getADC(void) {return (unsigned int) (ADCH<<8)|(ADCL);}

Также
  1.  
  2. ADCSRA|=(1<<ADSC);
и
  1. while ((ADCSRA&_BV(ADIF))==0x00);

лучше наверно тоже в неё положить.

Ага, она так более понятно

Ага, она так более понятно выглядит, исправил на:

  1. unsigned int getADC(void) //Считывание АЦП
  2. { unsigned int v;
  3. ADCSRA|=(1<<ADSC); //Начать преобразование
  4. while ((ADCSRA&_BV(ADIF))==0x00); //Дождаться окончания преобразования
  5. v=(ADCL|ADCH<<8); return v; }

неплохо было бы еще вывод

неплохо было бы еще вывод значений по UART передавать

А зачем блоку питания или

А зачем блоку питания или вольтметру UART? Для какого-то логгера-регулятора это уместно, а вот для вольтметра не вижу необходимости.

Ну если захотелось UART :), то просто добавим инициализацию 9600 8N1, перед вечным циклом:

  1. UBRRH = 0x00; //Set bitrate 9600 bit/sec at 4Mhz tick
  2. UBRRL = 0x19;
  3. UCSRA = 0x00;
  4. UCSRB = (1<<TXEN);
  5. UCSRC = (1<<URSEL)|(1<<UCSZ1)|(1<<UCSZ0);

также добавим такую функцию:
  1. void USART_transmit(unsigned char data )
  2. {while ( !( UCSRA & (1<<UDRE)) ); UDR = data;}

которую будем вызывать 2 раза после получения данных с АЦП, так как нам нужно передать 10 бит:
  1. USART_transmit((u/256)&0x0003);
  2. USART_transmit(u&0x00ff);