понедельник, 19 мая 2014 г.

Двух канальный вольтметр на Attiny13 и 74CH595


Всем привет, наткнулся на просторах интернета на интересную ссылку, так как автор выгрузил схему, код, прошивку и файлы протеуса то немного поигрался:



Вот ещё видео материал к статье:


Ну и надоело программно играться приобрёл себе регистр сдвига 74CH595, вот распиновка из даташита микросхемы в DIP корпусе:

А вот для удобства сборки на макетке добавляю распиновку Attiny13 тоже в DIP корпусе:


Собираем схему:


Я уже тут писал как можно прошить Attiny13 при помощи Arduino, потом берём код из странички выше:

// Подключение LCD HD44780 к AVR через регистр сдвига

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

// Команды для управления портами LCD
/* RS */
#define RS1 registr(0x01, 1)
#define RS0 registr(0x01, 0)

/* E */
#define E1 registr(0x02, 1)
#define E0 registr(0x02, 0)

/* D4 */
#define D41 registr(0x04, 1)
#define D40 registr(0x04, 0)

/* D5 */
#define D51 registr(0x08, 1)
#define D50 registr(0x08, 0)

/* D6 */
#define D61 registr(0x10, 1)
#define D60 registr(0x10, 0)

/* D7 */
#define D71 registr(0x20, 1)
#define D70 registr(0x20, 0)

// Функция установки курсора в указанную точку
#define lcd_gotoxy(x, y) write_to_lcd(0x80|((x)+((y)*0x40)), 0)

// Функция передачи данных в регистр
void registr(unsigned char data, unsigned char WriteOrErase)
{
  volatile static unsigned char tempdata = 0;

  if(WriteOrErase == 1)
    tempdata = (tempdata|data);
  else
    tempdata &= ~(data);
  PORTB &= ~(1 << PB1); // ST_CP 0

  PORTB &= ~(1 << PB2); // SH_CP 0
  if(tempdata & 0x80)PORTB |= (1 << PB0); 
  else PORTB &= ~(1 << PB0);
  PORTB |= (1 << PB2); // SH_CP 1
  PORTB &= ~(1 << PB2); // SH_CP 0
  if(tempdata & 0x40)PORTB |= (1 << PB0); 
  else PORTB &= ~(1 << PB0);
  PORTB |= (1 << PB2); // SH_CP 1
  PORTB &= ~(1 << PB2); // SH_CP 0
  if(tempdata & 0x20)PORTB |= (1 << PB0); 
  else PORTB &= ~(1 << PB0);
  PORTB |= (1 << PB2); // SH_CP 1
  PORTB &= ~(1 << PB2); // SH_CP 0
  if(tempdata & 0x10)PORTB |= (1 << PB0); 
  else PORTB &= ~(1 << PB0);
  PORTB |= (1 << PB2); // SH_CP 1
  PORTB &= ~(1 << PB2); // SH_CP 0
  if(tempdata & 0x08)PORTB |= (1 << PB0); 
  else PORTB &= ~(1 << PB0);
  PORTB |= (1 << PB2); // SH_CP 1
  PORTB &= ~(1 << PB2); // SH_CP 0
  if(tempdata & 0x04)PORTB |= (1 << PB0); 
  else PORTB &= ~(1 << PB0);
  PORTB |= (1 << PB2); // SH_CP 1
  PORTB &= ~(1 << PB2); // SH_CP 0
  if(tempdata & 0x02)PORTB |= (1 << PB0); 
  else PORTB &= ~(1 << PB0);
  PORTB |= (1 << PB2); // SH_CP 1
  PORTB &= ~(1 << PB2); // SH_CP 0
  if(tempdata & 0x01)PORTB |= (1 << PB0); 
  else PORTB &= ~(1 << PB0);
  PORTB |= (1 << PB2); // SH_CP 1
  PORTB |= (1 << PB1); // ST_CP 1
}

// Функция передачи данных или команды в LCD
void write_to_lcd(char p, unsigned char rs)
{
  if(rs == 1) RS1; 
  else RS0;

  E1;

  if(p&0x10) D41; 
  else D40;
  if(p&0x20) D51; 
  else D50;
  if(p&0x40) D61; 
  else D60;
  if(p&0x80) D71; 
  else D70;
  E0;  
  _delay_ms(2);

  E1;

  if(p&0x01) D41; 
  else D40;
  if(p&0x02) D51; 
  else D50;
  if(p&0x04) D61; 
  else D60;
  if(p&0x08) D71; 
  else D70;
  E0;

  _delay_ms(2);
}

// Функция инициализации LCD
void lcd_init(void)
{
  write_to_lcd(0x02, 0); // Курсор в верхней левой позиции
  write_to_lcd(0x28, 0); // Шина 4 бит, LCD - 2 строки
  write_to_lcd(0x0C, 0); // Разрешаем вывод изображения, курсор не виден
  write_to_lcd(0x01, 0); // Очищаем дисплей
}

// Функция вывода строки
void lcd_puts(char *str)
{
  unsigned char i = 0;

  while(str[i])
    write_to_lcd(str[i++], 1);
}

// Функция вывода переменной
void lcd_num_to_str(unsigned int value, unsigned char nDigit)
{
  switch(nDigit)
  {
  case 4: 
    write_to_lcd((value/1000)+'0', 1);
  case 3: 
    write_to_lcd(((value/100)%10)+'0', 1);
  case 2: 
    write_to_lcd(((value/10)%10)+'0', 1);
  case 1: 
    write_to_lcd((value%10)+'0', 1);
  }
}

// Функция чтения АЦП
int readADC(unsigned char ch) 
{ 
  ADMUX = ch; // Выбираем канал АЦП

  ADCSRA |= (1 << ADSC); // Запускаем преобразование 
  while((ADCSRA & (1 << ADSC))); // Ждем окончания преобразования

  return(ADC*11/2); // Возвращаем значение АЦП 
}

int main(void)
{
  DDRB = 0b00000111; // Настраиваем входы/выходы

  ADCSRA |= (1 << ADEN) // Разрешение АЦП
    |(1 << ADPS2)|(1 << ADPS1); // Предделитель на 64

  ACSR |= (1 << ACD); // Выключаем аналаговый компаратор
  DIDR0 |= (1 << ADC3D)|(1 << ADC2D); // Отключаем неиспользуемые цифровые входы

  lcd_init(); // Инициализация дисплея

  write_to_lcd(0x01, 0); // Очищаем дисплей

  lcd_gotoxy(0, 0); // Выводим строки на LCD
  lcd_puts("U1 =   .  V");
  lcd_gotoxy(0, 1);
  lcd_puts("U2 =   .  V");

  while(1)
  {
    lcd_gotoxy(5, 0);
    lcd_num_to_str(readADC(2)/100, 2); // Выводим данные АЦП1 на LCD
    lcd_gotoxy(8, 0);
    lcd_num_to_str(readADC(2)%100/10, 1);
    lcd_gotoxy(5, 1);
    lcd_num_to_str(readADC(3)/100, 2); // Выводим данные АЦП2 на LCD
    lcd_gotoxy(8, 1);
    lcd_num_to_str(readADC(3)%100/10, 1);
    _delay_ms(50);
  }
}

И вставляем в Arduino IDE, жмём компилировать и что мы видим:



Среда "проглотила" код, а это значит что можно запрограммировать тиньку при помощи дуинки не имея никаких ISP программаторов.
Пока игрался с протеусом немного модифицировал код автора статьи:


// Подключение LCD HD44780 к AVR через регистр сдвига

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

ISR(ADC_vect) // обязательно должно быть
{
}

// Команды для управления портами LCD
/* RS */
#define RS1 registr(0x01, 1)
#define RS0 registr(0x01, 0)

/* E */
#define E1 registr(0x02, 1)
#define E0 registr(0x02, 0)

/* D4 */
#define D41 registr(0x04, 1)
#define D40 registr(0x04, 0)

/* D5 */
#define D51 registr(0x08, 1)
#define D50 registr(0x08, 0)

/* D6 */
#define D61 registr(0x10, 1)
#define D60 registr(0x10, 0)

/* D7 */
#define D71 registr(0x20, 1)
#define D70 registr(0x20, 0)

// Функция установки курсора в указанную точку
#define lcd_gotoxy(x, y) write_to_lcd(0x80|((x)+((y)*0x40)), 0)

// Функция передачи данных в регистр
void registr(byte data, byte WriteOrErase)
{
  volatile static byte tempdata = 0;

  if(WriteOrErase == 1)
    tempdata = (tempdata|data);
  else
    tempdata &= ~(data);
  PORTB &= ~(1 << PB1); // ST_CP 0

  PORTB &= ~(1 << PB2); // SH_CP 0
  if(tempdata & 0x80)PORTB |= (1 << PB0); 
  else PORTB &= ~(1 << PB0);
  PORTB |= (1 << PB2); // SH_CP 1
  PORTB &= ~(1 << PB2); // SH_CP 0
  if(tempdata & 0x40)PORTB |= (1 << PB0); 
  else PORTB &= ~(1 << PB0);
  PORTB |= (1 << PB2); // SH_CP 1
  PORTB &= ~(1 << PB2); // SH_CP 0
  if(tempdata & 0x20)PORTB |= (1 << PB0); 
  else PORTB &= ~(1 << PB0);
  PORTB |= (1 << PB2); // SH_CP 1
  PORTB &= ~(1 << PB2); // SH_CP 0
  if(tempdata & 0x10)PORTB |= (1 << PB0); 
  else PORTB &= ~(1 << PB0);
  PORTB |= (1 << PB2); // SH_CP 1
  PORTB &= ~(1 << PB2); // SH_CP 0
  if(tempdata & 0x08)PORTB |= (1 << PB0); 
  else PORTB &= ~(1 << PB0);
  PORTB |= (1 << PB2); // SH_CP 1
  PORTB &= ~(1 << PB2); // SH_CP 0
  if(tempdata & 0x04)PORTB |= (1 << PB0); 
  else PORTB &= ~(1 << PB0);
  PORTB |= (1 << PB2); // SH_CP 1
  PORTB &= ~(1 << PB2); // SH_CP 0
  if(tempdata & 0x02)PORTB |= (1 << PB0); 
  else PORTB &= ~(1 << PB0);
  PORTB |= (1 << PB2); // SH_CP 1
  PORTB &= ~(1 << PB2); // SH_CP 0
  if(tempdata & 0x01)PORTB |= (1 << PB0); 
  else PORTB &= ~(1 << PB0);
  PORTB |= (1 << PB2); // SH_CP 1
  PORTB |= (1 << PB1); // ST_CP 1
}

// Функция передачи данных или команды в LCD
void write_to_lcd(char p, byte rs)
{
  if(rs == 1) RS1; 
  else RS0;

  E1;

  if(p&0x10) D41; 
  else D40;
  if(p&0x20) D51; 
  else D50;
  if(p&0x40) D61; 
  else D60;
  if(p&0x80) D71; 
  else D70;
  E0;  
  _delay_ms(2);

  E1;

  if(p&0x01) D41; 
  else D40;
  if(p&0x02) D51; 
  else D50;
  if(p&0x04) D61; 
  else D60;
  if(p&0x08) D71; 
  else D70;
  E0;

  _delay_ms(2);
}

// Функция инициализации LCD
void lcd_init(void)
{
  write_to_lcd(0x02, 0); // Курсор в верхней левой позиции
  write_to_lcd(0x28, 0); // Шина 4 бит, LCD - 2 строки
  write_to_lcd(0x0C, 0); // Разрешаем вывод изображения, курсор не виден
  write_to_lcd(0x01, 0); // Очищаем дисплей
}

// Функция вывода строки
void lcd_puts(char *str)
{
  byte i = 0;

  while(str[i])
    write_to_lcd(str[i++], 1);
}

// Функция вывода переменной
void lcd_num_to_str(unsigned int value, byte nDigit)
{
  switch(nDigit)
  {
    //case 4: // для чего мне 4 цифры, мне и двух хватит
    //  write_to_lcd((value/1000)+'0', 1);
    //case 3: 
    // write_to_lcd(((value/100)%10)+'0', 1);
  case 2: 
    write_to_lcd(((value/10)%10)+'0', 1);
  case 1: 
    write_to_lcd((value%10)+'0', 1);
  }
}

// Функция чтения АЦП
unsigned int readADC(byte Channel) 
{ 
  uint8_t l,h;
  //ADMUX = (1<<REFS0) | pin & 3; //Setup ADC
  ADMUX = (ADMUX & _BV(REFS0)) | Channel & 3; //Setup ADC, preserve REFS0
  ADCSRA |= _BV(ADSC); 
  while(ADCSRA & (1<<ADSC)); //Wait for conversion
  l = ADCL;  //Read and return 10 bit result
  h = ADCH;
  return (h << 8)|l; // Возвращаем значение АЦП 
}

unsigned int analogNoiseReducedRead(byte analogChannel)  { 
  unsigned int reading;

  ADCSRA |= _BV( ADIE );             //Set ADC interrupt
  set_sleep_mode(SLEEP_MODE_ADC);    //Set sleep mode
  reading = readADC(analogChannel);   //Start reading
  sleep_enable();                    //Enable sleep
  do
  {                                  //Loop until reading is completed
    sei();                           //Enable interrupts
    sleep_mode();                    //Go to sleep
    cli();                           //Disable interrupts
  } 
  while(((ADCSRA&(1<<ADSC))!= 0)); //Loop if the interrupt that woke the cpu was something other than the ADC finishing the reading
  sleep_disable();                   //Disable sleep
  ADCSRA &= ~ _BV( ADIE );           //Clear ADC interupt
  sei();                             //Enable interrupts
  return(reading);
}

unsigned int readADCOversampled(byte analogChannel)  { // оверсемплинг для увеличения разрядности АЦП
  unsigned long aSum = 0;   
  for(int i = 512; i > 0; i--){ // делаем 512 выборок АЦП
    aSum = aSum + analogNoiseReducedRead(analogChannel); 
  }
  return aSum >> 7; // получается что функция возвращает 0...4092
}

int main(void)
{
  DDRB = 0b00000111; // Настраиваем входы/выходы

  ADCSRA |= (1 << ADEN) // Разрешение АЦП
    |(1 << ADPS2)|(1 << ADPS1); // Предделитель на 64

  ACSR |= (1 << ACD); // Выключаем аналаговый компаратор
  DIDR0 |= (1 << ADC3D)|(1 << ADC2D); // Отключаем неиспользуемые цифровые входы

  lcd_init(); // Инициализация дисплея

  //write_to_lcd(0x01, 0); // Очищаем дисплей, уже есть

  lcd_gotoxy(0, 0); // Выводим строки на LCD
  lcd_puts("V1   .  "); // использую 8 символов и 2 строчки
  lcd_gotoxy(0, 1);
  lcd_puts("V2   .  ");

  while(1)
  {
    //ADMUX |= _BV(REFS0);
    unsigned int v1 = readADCOversampled(2); 
    //ADMUX &= ~_BV(REFS0);
    unsigned int v2 = readADCOversampled(3); 

    if(v1 > 3999){ // ну зачем видеть больше 39.99?
      v1 = 3999; // так прикольней
    }
    if(v2 > 3999){
      v2 = 3999;
    }
    lcd_gotoxy(3, 0);
    lcd_num_to_str(v1/100, 2); // Выводим данные АЦП1 на LCD
    lcd_gotoxy(6, 0);
    lcd_num_to_str(v1%100, 2);
    lcd_gotoxy(3, 1);
    lcd_num_to_str(v2/100, 2); // Выводим данные АЦП2 на LCD
    lcd_gotoxy(6, 1);
    lcd_num_to_str(v2%100, 2);
    //_delay_ms(25); // ненужно ждать, оверсемплинг довольно таки
    // сильно тормозит камень
  }
}


Добавив несколько функций и слегка доработав код, в частности урезал отображаемую информация под бюджетный дисплей 8 символов 2 строчки.
Итак, прошьём и посмотрим как работает:



На фото параллельно питанию тиньки стоит конденсатор на 220 мкФ - для уменьшения дрожания сотых.

Итак как видим работает, осталось рассчитать делитель и собрать на текстолите.
В отличии от кода автора статьи у меня выводиться как десятые так и сотые вольта, погрешность ожидаю получить не больше чем 0.5 % что сопоставимо с обычным китайским мультиметром.
Фишкой такой схемы является малое потребление, сама по себе тинька довольно таки экономичная, вот график из даташита потребления на разных частотах и напряжении:


Как видим на частоте в 9.6 MHz и питании в 5 В тиня будет потреблять примерно 2-3 мА а при напряжении в 3.3 В - 1-2 мА, причём на той же частоте. Если же частоту опустить до 1.2 МГц выставив фьюзы чтобы был включен делитель на 8, а как известно что потребление напрямую зависит от частоты камня то то при напряжении питания в 5 В тиня судя по графику будет кушать совсем мало.
Только что замерял сколько кушает миллиампер данная схема с регистром сдвига и LCD на основе модуля HD44780 (без подсветки) - 5 мА, неплохо должен сказать.


Комментариев нет:

Отправить комментарий