Вольтамперметр на Мега8

mir0tv0rec
Offline
Зарегистрирован: 19.09.2018

Доброго времени. Делаю вольтамперметр для электронной нагрузки. У нее 4 канала с шунтами. Соответственно решил собрать простенький показометр Вольт, Ампер, Ватт (4 канала тока и 1 напряжения). Но вылезла непонятка с АЦП. Почему-то в 0 канале АЦП показания зависят от показаний канала 4 (Вольты). Грешу на код, т.к. при изменении числа отсчетов (строка 31), изменяется и величина этих показаний и равна ADC0 (ток 1 канал) = ADC4 (напряжение) / кол-во замеров. Причем при подключении делителя показания этого канала и "левые" суммируются. Выложил функцию обработки АЦП, переменные. В основном цикле только вывод на дисплей.

#define U_REF   2.56f    // опорное напряжение для мега8а (внутренний ИОН или TL431)
#define RSH1    0.1f     // сопротивление токового шунта 1 линии
#define RSH2    0.1f     // сопротивление токового шунта 2 линии
#define RSH3    0.1f     // сопротивление токового шунта 3 линии
#define RSH4    0.1f     // сопротивление токового шунта 4 линии
#define RNVD    3600    // сопротивление резистора делителя напряжения к минусу (Ом)
#define RPVD    100000  // сопротивление резистора делителя напряжения к плюсу  (Ом)

volatile uint8_t messCount = 0;     // счетчик кол-во измерений АЦП
volatile uint8_t analogPin = 0;     // номер входа АЦП
volatile uint8_t recovMUX = 0;      // переменная для сохранения исходного значения регистра ADMUX
volatile uint16_t voltADCTmp = 0;   // вр. перем. для получения ср. ариф. АЦП
volatile float voltage = 0;         // напряжение
volatile float amperage = 0;        // общий ток
volatile uint8_t trueValue = false; // флаг переключения канала АЦП

volatile uint16_t ADC_V = false;  // отладка
volatile uint16_t ADC_A1 = false; // отладка
volatile uint16_t ADC_A2 = false; // отладка
volatile uint16_t ADC_A3 = false; // отладка
volatile uint16_t ADC_A4 = false; // отладка

volatile uint16_t voltADCRes[5];     // массив для хранание значений с АЦП (4 линии тока и напряжение)
ISR(ADC_vect)
{
	uint16_t result = ADCL | ADCH << 8;    // Получаем 10-битный результат
  
	if (trueValue)
	{
	  // 10 измерений для исключения скачков
	  if (messCount < 10)
	  {        
		  voltADCTmp += result;
		  messCount++;
	  }// end if 
	  else
	  {
		  voltADCRes[analogPin] = voltADCTmp / messCount; // усредняем результат, присваиваем значения с АЦП ячейкам массива 
      voltADCTmp = 0;  // обнуляем  
		  messCount = 0;   // обнуляем
      analogPin++;     // Перебираем входные пины по кругу (А0...А4 - их может быть больше)
      if (analogPin > 4)
      {
        amperage = U_REF / 1024 * (voltADCRes[0] / RSH1 + voltADCRes[1] / RSH2 + voltADCRes[2] / RSH3 + voltADCRes[3] / RSH4);
        voltage = U_REF / 1024 * voltADCRes[4] * (RPVD + RNVD) / RNVD;
        analogPin = 0; // Все нужные перебрали...возвращаемся к первому
        
        ADC_A1 = voltADCRes[0]; // отладка
        ADC_A2 = voltADCRes[1]; // для
        ADC_A3 = voltADCRes[2]; // вывода
        ADC_A4 = voltADCRes[3]; // на
        ADC_V = voltADCRes[4];  // экран (1602)
        
        for (uint8_t i = 0; i < 4; i++) voltADCRes[i] = 0;  // обнуляем ячейки массива показаний АЦП
        
      }//end if
      ADMUX = recovMUX | (analogPin & 0x07);  // Устанавливаем новый вход для преобразования
      trueValue = false;                      // Устанавливаем флаг смены входного пина - следующее прерывание пропускаем
    }// end else
  }// end if (trueValue)
  else trueValue = true;   // Первый раз пропускаем считывание и устанавливаем флаг на чтение в следующий раз
}// ISR(ADC_vect)

 

Дим-мычъ
Offline
Зарегистрирован: 20.03.2021

Возможно ,ошибка в стр.57. Надо не просто добавить новое значение, но и убрать старое, т.е. избежать наложения новых битов на старые через "ИЛИ"

rkit
Offline
Зарегистрирован: 23.11.2016

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

mir0tv0rec
Offline
Зарегистрирован: 19.09.2018

Переменная recovMUX содержит исходное состояние регистра ADMUX. Здесь как раз регистру присваивается начальное состояние и через ИЛИ активируется нужный порт АЦП через мультиплексор.

Дим-мычъ
Offline
Зарегистрирован: 20.03.2021

mir0tv0rec пишет:
Переменная recovMUX содержит исходное состояние регистра ADMUX. Здесь как раз регистру присваивается начальное состояние и через ИЛИ активируется нужный порт АЦП через мультиплексор.

Я думал так :если recovMUX = 0; то не совсем ясно, зачем он нужен. Тогда ADMUX = analogPin;

а если recovMUX != 0; то

 recovMUX &= 0xF8;
 ADMUX = recovMUX | analogPin; 

 

mir0tv0rec
Offline
Зарегистрирован: 19.09.2018

При инициализации сохраняю состояние в эту переменную (настройки ИОН, старшие 3 бита), сейчас не смогу код выложить, пишу с телефона.

rkit
Offline
Зарегистрирован: 23.11.2016

Для тупых, или слепых, или даже не знаю каких, повторю:

rkit пишет:

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

Дим-мычъ
Offline
Зарегистрирован: 20.03.2021

mir0tv0rec пишет:
При инициализации сохраняю состояние в эту переменную (настройки ИОН, старшие 3 бита), сейчас не смогу код выложить, пишу с телефона.

Ну да, если только при инициализации, и если при этом первые три бита = 0, то этот вариант отпадает и моё предположение не верно.

mir0tv0rec
Offline
Зарегистрирован: 19.09.2018

rkit пишет:

Для тупых, или слепых, или даже не знаю каких, повторю:

rkit пишет:

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


А какие там требования по входному току, если все входы АЦП заземлены? Честно говоря не нашел ничего про ток, но по крайней мере в том дш, который у меня есть.

wdrakula
wdrakula аватар
Offline
Зарегистрирован: 15.03.2016

mir0tv0rec пишет:
А какие там требования по входному току, если все входы АЦП заземлены?

;)

Заземлены? Ну это же всё меняет!

-------------

Ркит, как всегда, лаконичен не в меру. АЦП в мегах рассчитан на выходное сопротивление источника до 10К. Просто поставь повторитель на 358-ом ОУ и будет счастье! (если 358 подходит, конечно).

В принципе можно  переделывать код под высокоомный выход. Но работа будет неустойчивой. Повторитель - проще.

asam
asam аватар
Offline
Зарегистрирован: 12.12.2018

wdrakula пишет:

В принципе можно  переделывать код под высокоомный выход. Но работа будет неустойчивой. Повторитель - проще.

 

АЦП в меге не слишком точный, а главное не слишком стабильный. Если уж ставить дополнительные детали, так лучше добавить ADS 1015. Cтоит недорого, меряет гораздо точнее и стабильнее. Есть встроенный усилитель регулируемый програмно. Не шибко быстрый, правда, десятки, максимум сотни выборок в секунду, но для вольтметра больше и не надо. 

mir0tv0rec
Offline
Зарегистрирован: 19.09.2018

В данный момент токовые входы заземлены, вход напряжения подключен через делитель 3.6 и 100к, т.е в 10к все входит. Здесь какая-то программная ошибка, которую я не могу найти, т.к. значение 0 канала АЦП ровно в "кол-во измерений" меньше показаний АЦП 4 канала. Т.е. если к примеру на 4 канале 523 и кол-во измерений 10, но 0 канал показывает 52. Если кол-во измерений уменьшаю до 5, то АЦП0 показывает 104. Все остальные каналы также заземлены и стоят на 0. Даже, если подклячаю любой из каналов к делитель, то 4 канал на них никак не влияет, только на 0.
1015 есть, но ставить его на планировал изначально, т.к хочу по максимуму сделать простой показометр, сверх точность мне не нужна.

asam
asam аватар
Offline
Зарегистрирован: 12.12.2018

 

Если это только для показометр и кроме измерений ниечего особо не делает, то зачем использовать прерывания?

Если же хочется прерываний, то делать внутри него рассчеты с умножением и делением, да еще float, крайне нежелательно. Это надо делать в основной программе по флагу выставленному в прерывании. Иначе рискуете нарваться пропуск прерывания. Что у вас скорее всего и происходит. На какой частоте работает АЦП? 

Ну и весь код надо выкладывать. Может там чего не так.

Желательно, после переключения канала, первый результат отбросить.

 

rkit
Offline
Зарегистрирован: 23.11.2016

mir0tv0rec пишет:
3.6 и 100к, т.е в 10к все входит

нобелевка по матетике

Green
Offline
Зарегистрирован: 01.10.2015

rkit пишет:

mir0tv0rec пишет:
3.6 и 100к, т.е в 10к все входит

нобелевка по матетике


Да там даже не входное важно, оно будет влиять на точность измерения, КМК... От зависимости каналов я бы попробовал двойное чтение, дабы "перезарядить" конденсатор АЦП. Т.е., первое измерение канала отбрасываешь, а со вторым работаешь. Не? 

mir0tv0rec
Offline
Зарегистрирован: 19.09.2018

Я имел ввиду, что резистор между выводом и землёй 3.6к, т.е. в диапазон до 10к, укладывается. Как я понял из даташита, это указано для того, чтобы ёмкость вывода успевала разряжаться между измерениями и не влияла на результаты измерений.
Про float в прерывании тоже думал, что ему там не место, но пока вывод у меня через делай сделан, потому пока так. Думаю тоже сделать все расчеты в основной программе, и вообще уйти от прерываний. Никаких обработок кнопок я не планирую, ну может какую-нибудь калибровку при включении. А так, будет просто вывод на дисплей.

Upper
Offline
Зарегистрирован: 23.06.2020

ASAM спрашивал - У вас АЦП на какую частоту настроено? Сколько выборок в секунду?

mir0tv0rec
Offline
Зарегистрирован: 19.09.2018

125кГц. Тут, как мне показалось, при переключении каналов АЦП с 4 на 0, переменная 0 канала, хватает 1 измерение 4 канала, но не пойму почему... Попробую поиграться с количеством каналов и последовательностью. Возьму за измерение напряжения 0 канал, а на ток с 1 по 4. И посмотрю, что будет. Такое подключение и частоту АЦП применял уже в нескольких проектах и всё работает нормально(причем там там сопротивление источника напряжение на пару порядков выше рекомендованного 10к), но там максимум 2 канала было

KindMan
Offline
Зарегистрирован: 19.12.2018

В 54 строке всё правильно?

mir0tv0rec
Offline
Зарегистрирован: 19.09.2018

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

Upper
Offline
Зарегистрирован: 23.06.2020

mir0tv0rec пишет:
125кГц.

Попробуйте перенести переключение каналов в начало блока (до float вычислений).

mir0tv0rec
Offline
Зарегистрирован: 19.09.2018

Перенес обработку АЦП из прерывания, все стало нормально. Теперь все каналы работают, как нужно:
 

#include <avr/io.h>
#include <avr/interrupt.h>
#include <util/delay.h>

#define E_ON    PORTD |= 0b00001000 // установка линии E в 1
#define E_OFF   PORTD &= 0b11110111 // установка линии E в 0
#define RS_ON   PORTD |= 0b00000100 // установка линии RS в 1 (данные)
#define RS_OFF  PORTD &= 0b11111011 // установка линии RS в 0 (команда)
#define U_REF   2.56f    // опорное напряжение для мега8а (внутренний ИОН или TL431)
#define RSH1    0.1f     // сопротивление токового шунта 1 линии
#define RSH2    0.1f     // сопротивление токового шунта 2 линии
#define RSH3    0.1f     // сопротивление токового шунта 3 линии
#define RSH4    0.1f     // сопротивление токового шунта 4 линии
#define RNVD    3600    // сопротивление резистора делителя напряжения к минусу (Ом)
#define RPVD    100000  // сопротивление резистора делителя напряжения к плюсу  (Ом)

volatile uint8_t toPrint = false;    // флаг вывода на дисплей

uint8_t messCount = 0;      // счетчик кол-во измерений АЦП
uint8_t analogPin = 0;      // номер входа АЦП
uint8_t recMUX = 0;         // переменная для сохранения исходного значения регистра ADMUX
uint16_t voltADCTmp = 0;    // вр. перем. для получения ср. ариф. АЦП
float voltage = 0;          // напряжение
float amperage = 0;         // общий ток
uint8_t ADC_read = false;   // флаг переключения канала АЦП
uint8_t ADC_result = false; // флаг готовности результатов с АЦП

uint16_t ADC_V = false;  // отладка
uint16_t ADC_A1 = false; // отладка
uint16_t ADC_A2 = false; // отладка
uint16_t ADC_A3 = false; // отладка
uint16_t ADC_A4 = false; // отладка

uint16_t voltADCRes[5];     // массив для хранание значений с АЦП (4 линии тока и напряжение)

char mess1[] = "Electronic load"; //
char mess2[] = "ver. 1.0";        // строки для
char volt[] = "U=    V";          // вывода на 
char amper[] = "I=    A";         // экран
char watt[] = "Power(W)";         //


float wattage = 0;      // мощность

//=========== Функции ====================

// Отправка полубайта на дисплей
//
void SendHalfByte(unsigned char c)
{
  c <<= 4;
  E_ON; //включаем линию Е
  _delay_us(50);
  PORTD &= 0b00001111; //стираем информацию на входах DB4-DB7, остальное не трогаем
  PORTD |= c;
  E_OFF;  //выключаем линию Е
  _delay_us(50);
  
}//end SendHalfByte(unsigned char c)

//Отправка байта на дисплей
//
void SendByte(unsigned char c, unsigned char mode)
{
  if (mode == 0) RS_OFF;
  else RS_ON;
  unsigned char hc = 0;
  hc = c >> 4;
  SendHalfByte(hc);
  SendHalfByte(c);
}//end void SendByte(unsigned char c, unsigned char mode)

// Отправка символа на дисплей
//
void SendChar(unsigned char c)
{
  SendByte(c, 1);
}

// Установка позиции курсора
//
void SetPos(unsigned char x, unsigned y)
{
  char adress;
  adress = (0x40 * y + x) | 0b10000000;
  SendByte(adress, 0);
}

// Инициализация дисплея
//
void LCDIni()
{
  _delay_ms(15); //Ждем 15 мс (стр 45)
  SendHalfByte(0b00000011);
  _delay_ms(4);
  SendHalfByte(0b00000011);
  _delay_us(100);
  SendHalfByte(0b00000011);
  _delay_ms(1);
  SendHalfByte(0b00000010);
  _delay_ms(1);
  SendByte(0b00101000, 0); //4бит-режим (DL=0) и 2 линии (N=1)
  _delay_ms(1);
  SendByte(0b00001100, 0); //включаем изображение на дисплее (D=1), курсоры никакие не включаем (C=0, B=0)
  _delay_ms(1);
  SendByte(0b00000110, 0); //курсор (хоть он у нас и невидимый) будет двигаться влево
  _delay_ms(1);
}

// Очистка дисплея
//
void ClearLCD()
{
  SendByte(0b00000001, 0);
  _delay_us(1500);
}

// Вывод строки на дисплей
//
void StrLCD (char str1[])
{  
  wchar_t n;
  for(n = 0; str1[n] != '\0'; n++)
  SendChar(str1[n]);
}

void Tim1_Init()
{
  TCCR1B |= (1 << WGM12); // устанавливаем режим СТС (сброс по совпадению)
  TIMSK |= (1 << OCIE1A); // устанавливаем бит разрешения прерывания 1ого счетчика по совпадению с OCR1A(H и L)
  OCR1AH = 0b00011000;    // записываем в регистр число для сравнения
  OCR1AL = 0b01101010;    // 6250
  TCCR1B |= (1 << CS12);  // установим делитель (х/256)
  
}//end void Tim1_Init()

ISR(TIMER1_COMPA_vect)
{
  toPrint = true; // флаг вывода на дисплей
}//end ISR(TIMER1_COMPA_vect)


//==== инициализируем АЦП ====
//
void ADC_Init()
{
  ACSR = 0x00;      // сбрасываем регистр компаратора
  ACSR |= 1 << ACD; // выключаем питание компаратора
  
  ADCSRA = 0; // Сбрасываем регистр ADCSRA
  ADMUX |= (1 << REFS1) | (1 << REFS0);  // Задаем ИОН внутренний 2.56В (для АТМега8)

  recMUX = ADMUX; // Запоминаем состояние регистра - из него мы будем формировать маску для смены входного пина вход A0
  
  ADCSRA |= (1 << ADEN)                                 // Включаем АЦП
         | (1 << ADPS2) | (1 << ADPS1) | (1 << ADPS0);  // Устанавливаем предделитель - 128(8МГц / 128 = 62.5КГц)
}//end ADC_Init()


//==== обработчик прерывания ====
//==== по завершению преобразования АЦП ====
//
void ReadADC()
{
  uint16_t result = ADCL | ADCH << 8;    // Получаем 10-битный результат
  
  if (ADC_read)
  {
    // 10 измерений для исключения скачков
    if (messCount < 10)
    {        
      voltADCTmp += result; // аккумулируем показания АЦП канала
      messCount++;  // инкрементируем счетчик кол-во снятых показаний
    }// end if 
    else
    {
      voltADCRes[analogPin] = voltADCTmp / messCount; // усредняем результат, присваиваем значения с АЦП ячейкам массива 
      voltADCTmp = 0;  // обнуляем  
      messCount = 0;   // обнуляем
      analogPin++;     // Перебираем входные пины по кругу (А0...А4 - их может быть больше)
      if (analogPin > 4)
      {
        amperage = U_REF / 1024 * (voltADCRes[0] / RSH1 + voltADCRes[1] / RSH2 + voltADCRes[2] / RSH3 + voltADCRes[3] / RSH4);
        voltage = U_REF / 1024 * voltADCRes[4] * (RPVD + RNVD) / RNVD;
        analogPin = 0; // Все нужные перебрали...возвращаемся к первому
        ADC_result = true;
      }//end if
      ADMUX = recMUX | (analogPin & 0x07);  // Устанавливаем новый вход для преобразования
      ADC_read = false;                      // Устанавливаем флаг смены входного пина - следующее прерывание пропускаем
    }// end else
  }// end if (ADC_read)
  else ADC_read = true;   // Первый раз пропускаем считывание и устанавливаем флаг на чтение в следующий раз
}// end void ReadADC()

// Инициализация портов МК для управления дисплеем
//
void PortIni()
{
  PORTD = 0x00; // низкий уровень
  DDRD = 0xFF;  // порт D на выход
}//end PortIni()


int main()
{
  PortIni();  //Инициализируем порты
  LCDIni();   //Инициализируем дисплей
  ClearLCD();
  SetPos(0,0);
  StrLCD(mess1);
  SetPos(4,1);
  StrLCD(mess2);
  _delay_ms(3000);
  ClearLCD();   
  ADC_Init();  // запускаем АЦП
  Tim1_Init(); // запускаем таймер1
  
  sei();  // разрешаем прерывания глобально
   
  // Основной цикл программы
  while(1)
    {
      
      // если есть результаты преобразования АЦП
      if (!(ADCSRA & (1 << ADSC)))
      {
        ReadADC();  // Считываем и обрабатываем данные с АЦП
        ADCSRA |= (1 << ADSC);  // Запускаем преобразование
      }//end if
      
      if (ADC_result && toPrint)
      {
        char strConv[6];  //
        uint8_t simbNum = 5;
        SetPos(0, 0);
        StrLCD(volt);
        if (voltage < 10) simbNum = 2;
        else simbNum = 1;
        dtostrf(voltage, 4, simbNum, strConv);
        SetPos(2, 0);
        StrLCD(strConv);
      
        if (amperage < 10) simbNum = 2;
        else simbNum = 1;
        SetPos(0, 1);
        StrLCD(amper);
        dtostrf(amperage, 4, simbNum, strConv);
        SetPos(2, 1);
        StrLCD(strConv);
      
        SetPos(8, 0);
        StrLCD(watt);
        wattage = voltage * amperage;
        if (wattage < 100) simbNum = 2;
        else simbNum = 1;
        dtostrf(wattage, 5, simbNum, strConv);
        SetPos(9, 1);
        StrLCD(strConv);
        ADC_result = false;
        toPrint = false;
      }//end if
    }//end while(1)
}//end main()

Осталось прикрутить калибровку. Всем спасибо за помощь!