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

Схема

вольтметр на микроконтроллере

Логика программы

  В начале программы выполняются функции инициализации. Настраивается и запускается АЦП, конфигурируется порт, к которому подключен индикатор, и настраивается таймер Т0. Затем разрешаются прерывания, и микроконтроллер выполняет  бесконечный цикл. В цикле опрашивается программный буфер АЦП и вычисляется значение напряжения. Вычисленное значение передается функции индикатора, которая преобразует его в двоично-десятичные цифры, затем в коды цифр индикатора и записывает их в массив (буфер).
   Параллельно основной программе вызываются прерывания АЦП и таймера Т0. АЦП работает в режиме однократного преобразования, с внутренним опорным источником напряжения на 2,56В. Выравнивание вправо, используются все 10 разрядов. Результат преобразования АЦП накапливается 8 раз в переменной, усредняется и записывается в программный буфер.
   В прерывании таймера Т0 происходит его перезапуск и вызывается функция обновления индикатора. Она гасит текущий отображаемый разряд и зажигает следующий.

Структура проекта

Проект состоит из 3-ех программных модулей.
main.c – основная программа
adc.c – функции для работы с АЦП
indicator.c – драйвер семисегментного 4-ех разрядного индикатора.

Драйвер семисегментного индикатора

   В заголовочном файле indicator.h с помощью директивы #define определены порты и номера выводов микроконтроллера, к которому подключен индикатор.

//куда подключены сегменты
#define PORT_IND PORTB
#define DDR_IND DDRB

#define SEG_A 0
#define SEG_B 1
#define SEG_C 2
#define SEG_D 3
#define SEG_E 4
#define SEG_F 5
#define SEG_G 6
#define SEG_DP 7

//куда подключены упр. выводы
#define PORT_TR PORTD
#define DDR_TR DDRD

#define NUM1 0
#define NUM2 1
#define NUM3 2
#define NUM4 3

Для работы с индикатором используется три функции – функция инициализации, функция преобразования и функция обновления индикатора.

void IND_Init(void);
void IND_Output(unsigned int value, unsigned char comma);
void IND_Update(void);

Прототипы функций находятся в заголовочном файле, а их реализации  в сишном.

Функция инициализации

//количество разрядов индикатора
#define AMOUNT_NUM 4

//буфер семисегментного индикатора
unsigned char
buf[AMOUNT_NUM];

void IND_Init(void)
{
  PORT_IND = 0xff;
  DDR_IND = 0xff;
 
  PORT_TR |= (1<<NUM1)|(1<<NUM2)|(1<<NUM3)|(1<<NUM4);
  DDR_TR |= (1<<NUM1)|(1<<NUM2)|(1<<NUM3)|(1<<NUM4);

  for(unsigned char i = 0; i < AMOUNT_NUM; i++) buf[i] = 0;
}

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

Функция преобразования

//массив для преобразования десятичных чисел в коды индикатора
unsigned char number[] =
{
  (1<<SEG_A)|(1<<SEG_B)|(1<<SEG_C)|(1<<SEG_D)|(1<<SEG_E)|(1<<SEG_F)|(0<<SEG_G), //0
  (0<<SEG_A)|(1<<SEG_B)|(1<<SEG_C)|(0<<SEG_D)|(0<<SEG_E)|(0<<SEG_F)|(0<<SEG_G), //1
  (1<<SEG_A)|(1<<SEG_B)|(0<<SEG_C)|(1<<SEG_D)|(1<<SEG_E)|(0<<SEG_F)|(1<<SEG_G), //2
  (1<<SEG_A)|(1<<SEG_B)|(1<<SEG_C)|(1<<SEG_D)|(0<<SEG_E)|(0<<SEG_F)|(1<<SEG_G), //3   
  (0<<SEG_A)|(1<<SEG_B)|(1<<SEG_C)|(0<<SEG_D)|(0<<SEG_E)|(1<<SEG_F)|(1<<SEG_G), //4
  (1<<SEG_A)|(0<<SEG_B)|(1<<SEG_C)|(1<<SEG_D)|(0<<SEG_E)|(1<<SEG_F)|(1<<SEG_G), //5
  (1<<SEG_A)|(0<<SEG_B)|(1<<SEG_C)|(1<<SEG_D)|(1<<SEG_E)|(1<<SEG_F)|(1<<SEG_G), //6
  (1<<SEG_A)|(1<<SEG_B)|(1<<SEG_C)|(0<<SEG_D)|(0<<SEG_E)|(0<<SEG_F)|(0<<SEG_G), //7
  (1<<SEG_A)|(1<<SEG_B)|(1<<SEG_C)|(1<<SEG_D)|(1<<SEG_E)|(1<<SEG_F)|(1<<SEG_G), //8
  (1<<SEG_A)|(1<<SEG_B)|(1<<SEG_C)|(1<<SEG_D)|(0<<SEG_E)|(1<<SEG_F)|(1<<SEG_G)  //9
};

void IND_Output(unsigned int value, unsigned char comma)
{
  unsigned char tmp;
  for(unsigned char i = 0; i < AMOUNT_NUM; i++){
    tmp = value % 10;
    buf[i] = number[tmp];
    value = value/10;
  }

  if (comma < AMOUNT_NUM) {
    buf[comma] |= 1<<(SEG_DP);
  }
    
}

   Эта функция преобразует переданное ей 16-ти разрядное число (value) в коды цифр индикатора и записывает в программный буфер. Кроме этого она устанавливает в определенном разряде индикатора запятую. Номер разряда определяется переменной comma.
  Преобразование выполняется следующим образом. Переданное функции число делится по модулю 10. Результатом выполнения этой операции будет остаток от деления числа на 10, что позволяет нам как бы отделить младший разряд десятичного числа от его остальной части. Используя результат деления по модулю в качестве индекса массива number, мы преобразуем это число в код цифры индикатора.

Для ясности небольшой пример.Возьмем число 123.

Первое выполнение цикла for  - i = 0
tmp = value % 10 = 123 % 10 = 3
buf[0] = number[3]
value = value/10 = 123/10 = 12

Второе выполнение цикла for  - i = 1
tmp = value % 10 = 12 % 10 = 2
buf[1] = number[2]
value = value/10 = 12/10 = 1

Третье  выполнение цикла for  - i = 2
tmp = value % 10 = 1 % 10 = 1
buf[2] = number[1]
value = value/10 = 1/10 = 0

Четвертое (последнее) выполнение цикла for  - i = 3
tmp = value % 10 = 0 % 10 = 0
buf[3] = number[0]
value = value/10 = 0/10 = 0

   Коды цифр индикатора в массиве number записаны с помощью макроопределений. Это позволяет подключать выводы сегментов индикатора к порту микроконтроллера в любом порядке. 

Функция обновления индикатора

//макросы для настройки драйвера индикатора под другую схему
#define LightOutAll()  PORT_TR &= ~((1<<NUM1)|(1<<NUM2)|(1<<NUM3)|(1<<NUM4))
#define BurnDigit(port, digit) port |= (1<<digit) 
#define ValueBuf() buf[count]


void IND_Update(void)
{
  static unsigned char count = 0;
 
  //гасим все разряды
  PORT_IND = 0;
  LightOutAll();

  //зажигаем соответствующий разряд
  if (count == 0) BurnDigit(PORT_TR, NUM1);
  else if (count == 1) BurnDigit(PORT_TR, NUM2);
  else if (count == 2) BurnDigit(PORT_TR, NUM3);
  else BurnDigit(PORT_TR, NUM4);
 
  //выводим код цифры в порт
  PORT_IND = ValueBuf();
 
  count++;
  if (count == AMOUNT_NUM) count = 0;
}

  Эта функция гасит текущий активный разряд семисегментного индикатора и зажигает следующий – “открывает” соответствующий транзистор и выводит в порт, к которому подключены сегменты индикатора, код цифры из массива (буфера).
  Несколько выражений используемых в функции определены с помощью директивы #define - LightOutAll(), BurnDigit(port, digit), ValueBuf(). Это позволяет быстро и безболезненно настроить драйвер под другой тип индикатора или другую схему включения. Тут в принципе возможно 4 варианта:

индикатор с общим катодом подключенный напрямую к микроконтроллеру
#define LightOutAll()  PORT_TR |= (1<<NUM1)|(1<<NUM2)|(1<<NUM3)|(1<<NUM4)
#define BurnDigit(port, digit) port &= ~(1<<digit)  
#define ValueBuf() buf[count]

индикатор с общим катодом подключенный к микроконтроллеру с помощью транзисторов
#define
LightOutAll()  PORT_TR &= ~((1<<NUM1)|(1<<NUM2)|(1<<NUM3)|(1<<NUM4))
#define BurnDigit(port, digit) port |= (1<<digit)  
#define ValueBuf() buf[count]

индикатор с общим анодом подключенный напрямую к микроконтроллеру
#define LightOutAll()  PORT_TR &= ~((1<<NUM1)|(1<<NUM2)|(1<<NUM3)|(1<<NUM4))
#define BurnDigit(port, digit) port |= (1<<digit)  
#define ValueBuf() ~buf[count]

индикатор с общим анодом подключенный к микроконтроллеру с помощью транзисторов
#define LightOutAll()  PORT_TR |= (1<<NUM1)|(1<<NUM2)|(1<<NUM3)|(1<<NUM4)
#define BurnDigit(port, digit) port &= ~(1<<digit)  
#define ValueBuf() ~buf[count]

Полный текст драйвера светодиодного семисегментного индикатора

Indicator.h
Indicator.c

Как вычисляется напряжение

   Диапазон входных напряжений АЦП определяется источником опорного напряжения (ИОН). В нашем случае используется внутренний ИОН на 2,56 В. Для расширения диапазона измеряемых напряжений я поставил перед АЦП резистивный делитель. Он  рассчитан таким образом, чтобы при напряжении в 30 В на входе делителя напряжение на входе АЦП не превышало 2,56 В.

Uadc = Uin*R14/(R14 + R15) = 30В*82 /(82 + 910) = 2,48 В

Чтобы вычислить напряжение на входе вольтметра нужно результат преобразования АЦП пересчитать в напряжение и домножить на коэффициент

(R14 + R15)/R14 = 992/82

Напряжение на входе АЦП - Uadc вычисляется по формуле:

Uadc = value * 2,56/(2^n – 1),

 где n – разрядность АЦП, value – цифровое значение напряжения

Отсюда напряжение на входе вольтметра будет равно:

Uin = (value * 2,56 * 992)/(82 * 1023) = (value * 30,96)/1023

Чтобы не связываться с типом float и при этом иметь возможность вычислять напряжение с точностью до 2-ух знаков после запятой, числитель этой формулы я домножил на 100

Uin = (value * 3096)/1023

Тест вольтметра

   Когда программа была завершена, мне стало интересно, насколько точные показания будет давать вольтметр по сравнению с мультиметром. Я подключил вольтметр к источнику питания и в диапазоне напряжений от 0 до 30 В снял его показания.
   Результаты вы можете видеть в таблице. Первая колонка – это напряжение выставляемое на источнике питания, вторая - показания мультиметра Fluke, а третья – цифровой вольтметр на ATmega8.


Файлы

IAR.Вольтметр на микроконтроллере
WinAvr.Вольтметр на микроконтроллере
CodeVision.Вольтметр на микроконтроллере
Проект для Proteus`a.Вольтметр на микроконтроллере

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