Неудача с часами на ATMega48

в 15:17, , рубрики: arduino, avr, diy или сделай сам, ЖК индикаторы, схемотехника, часы для гиков, Электроника для начинающих

Преотличнейшие часы на завалявшемся жк-индикаторе и супермикросхеме ATMega48. Но не получились.

Неудача с часами на ATMega48 - 1

То есть, часы, конечно, работают, но, увы, проработают они недолго.

Давным-давно в коробке в дальней тумбочке болталась у меня пара семисегментных жк-индикаторов. И так же давно мне хотелось взять их в оборот и соорудить на основе одного из них часы. Давным-давно: это, в буквальном смысле, семь лет. Именно тогда, в 2011 году возник у меня интерес к электронике. Не думая долго, заказал я тогда всякой всячины в одном хорошем интернет магазине (нет, не на Али; не уверен, что он тогда уже был). Но как-то у меня не заладилось творить вечное. После нескольких протравленных плат, забросил я это развлечение и забыл.

И вот, когда с Али пришла посылка, содержавшая макетки, пакетик 595-х в dip-корпусах, Tiny RTC на ds1307, и, что самое важное, USBasp, пришло время вернуться к старой задумке. Из старой заначки у меня был ATMega48, тот который о 28 ног, lm7805, всякая мелочевка в виде резисторов/конденсаторов/кнопок, и, собственно, индикатор на 40 ногах.

Вообще говоря, изначально планировалось использовать AtTiny13, которые у меня тоже валяются, но прикинув и так, и этак, я не придумал как обойтись его 5 ногами что бы и на индикатор выводить, и две кнопки читать, и с часами по i2c общаться. С 28 ногами меги, экономить и извращаться с объединением ног уже не приходится. Хотя, конечно, и в этом случае без 595-х не обойтись. Но если с тини я планировал выводить все 32 бита данных для индикатора последовательно, то с мегой можно выводить картинку сразу на все четыре микросхемы параллельно.

Неудача с часами на ATMega48 - 2
Все 595-е спрятаны под индикатором. На фото видны выводы корпуса микросхемы.

595 было использовано именно четыре штуки, потому что индикатор, хотя и имеет 40 ног, но на сегменты выведены только 32. Увы, но это только 3.5 индикатор, то есть у него 3 полноценных цифры, плюс единица. Есть символ секунд, но никакого обозначения для AM/PM. Но уж что есть. Заказывать более продвинутый индикатор, не собрав ни разу в жизни ничего на жк-индикаторах, мне бы не хотелось.

Неудача с часами на ATMega48 - 3
Рабочая документация на индикатор. Пришлось тестером выяснять какая ножка какому сегменту соответствует.

Ну а дальше дело техники. Схемы никогда не было, но там все очевидно. Надо было только выделить четыре ноги на вход буферов, общую ногу для индикатора, ногу для SCLK/RCLK, ногу на OE, две ноги для i2c, две ноги для кнопок. Все это было, конечно, неправильно. Почему я решил, что OE надо заводить на контроллер, а SCLK объединять с RCLK — уже и сам не вспомню. Надо было делать как раз наоборот. А общий провод индикатора на контроллере не нужен вообще, можно было обойтись одним из выводов первой 595-й (что я тоже, в итоге, сделал).

Неудача с часами на ATMega48 - 4
Провода. Много их. Вид изнутри.

Неудача с часами на ATMega48 - 5
Вид снаружи.

Самое интересное во всем этом прожекте: код вывода на индикатор. Тонкость в том, что на жк-индикатор нельзя просто так подать напряжение и забыть. Надо около ста раз в секунду менять полярность между сегментами и общим контактом, что бы все было красиво и приятно глазу. В качестве среды разработки, не мудрствуя лукаво, я использовал Arduino IDE, лишь чуть помучавшись в паре моментов. Во-первых, пришлось использовать пакет MiniCore ( https://github.com/MCUdude/MiniCore ), ведь писать пришлось не для большой готовой ардуины, а для слабого и голого ATMega48. Во-вторых, прямая попытка использовать встроенную библиотеку для i2c ни к чему не привела. По какой-то не вполне понятной причине, работать оно не пожелало, пришлось найти другую библиотеку, а потом и обрезать ее для своих нужд.

Как оказалось, 4 килобайта — это очень мало. Особенно, если писать на C. Возможно, если бы я решился перейти на ассемблер, это ограничение не давило бы так серьезно, но помня мои прошлые эксерсизы в писании на ассемблере для AVR, желания у меня такого не возникло. А на C писать — много места надо. Чуть скобку ненароком поставил — сотня байт в трубу.

Собственно, комментированный код

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

#define cbi(sfr, bit)   (_SFR_BYTE(sfr) &= ~_BV(bit))
#define sbi(sfr, bit)   (_SFR_BYTE(sfr) |= _BV(bit))

// на самом деле у меня ds1307, но адрес тот же
#define DS3231_I2C_ADDRESS 0x68

#define LCD_PORT PORTD
#define LCD_DDR DDRD
#define LCD_DATA1 PD0
#define LCD_DATA2 PD1
#define LCD_DATA3 PD2
#define LCD_DATA4 PD3
#define LCD_SCLK PD4
#define LCD_OE PD5
#define LCD_COM PD6

#define KEY1_PORT PORTB
#define KEY1_DDR DDRB
#define KEY1_PIN PINB
#define KEY1 PB0
#define KEY2_PORT PORTD
#define KEY2_DDR DDRD
#define KEY2_PIN PIND
#define KEY2 PD7

#define DS1307SQ PB1
#define DS1307SQ_INT PCIE0
#define DS1307SQ_PORT PORTB
#define DS1307SQ_DDR DDRB
#define DS1307SQ_PIN PINB
#define DS1307SQ_VEC PCINT0_vect
#define DS1307SQ_MSK PCMSK0

// были планы. но, во-первых, лениво, во-вторых все равно уже не лезет
#define LM335_PORT PORTC
#define LM335_DDR DDRC
#define LM335 PC3

// это фреймбуфер
unsigned char lcd_buf[4];

#define MODE_MAIN 0
#define MODE_CALENDAR 1
#define MODE_YEAR 2
#define MODE_TERMOMETER 3 // нет
#define MODE_VOLTMETER 4 // нет
#define MODE_SET_MINUTE 5
#define MODE_SET_HOUR 6
#define MODE_SET_DAY 7
#define MODE_SET_MONTH 8
#define MODE_SET_YEAR 9
#define MODE_SECOND 10
#define MODE_DEBUG 11 // не используется

// таймауты в виде количества десятков циклов по 10ms (грубо)
#define MODE_TIMEOUT 20 // 2 секунды
#define MODE_TIMEOUT_SET 100 // 10 секунд
#define KEY_TIMEOUT 10 // 1 секунда

byte mode = MODE_MAIN;
byte mode_timeout = 0;

byte key1_press = 0;
byte key1_time = 0;
byte key2_press = 0;
byte key2_time = 0;

byte cycle_count_10 = 0; // счетчик десятков циклов
byte even_10 = 0; // меняется раз в 1/10 секунды

// последние данные от ds1307
byte second;
byte minute;
byte hour;
byte dayOfWeek;
byte dayOfMonth;
byte month;
byte year;

volatile byte need_render_int = 1;
uint8_t porthistory = 0xFF;
volatile uint8_t debug_value = 0;

byte twi_problems = 0;

// раз в секунду ds1307 дергает линию SQ
// я это ловлю, читаю время и перерисовываю экран
ISR(DS1307SQ_VEC) {
  uint8_t changedbits = DS1307SQ_PIN ^ porthistory;
  porthistory = DS1307SQ_PIN;
  if (changedbits & _BV(DS1307SQ) && porthistory & _BV(DS1307SQ)) {
    need_render_int = 1;
  }
}

// рисует цифру в фреймбуфере в нужной позиции
void lcd_num(char pos, char num) {
  unsigned char buf = 0b01110110;
  if (pos < 1 || pos > 3) {
    return;
  }
  switch (num) {
    case 0:
      // а почему это у меня 3-я позиция обрабатывается иначе, чем две другие?
      // правильно, потому что я криворукий косоглаз, который в трех проводках 
      // постоянно путается :-(
      if (pos == 3) {
        buf = 0b11101110;
      } else {
        buf = 0b11100111;
      }
      break;
    case 1:
      if (pos == 3) {
        buf = 0b10001000;
      } else {
        buf = 0b10000001;
      }
      break;
    case 2:
      buf = 0b11010110;
      break;
    case 3:
      if (pos == 3) {
        buf = 0b11011100;
      } else {
        buf = 0b11010011;
      }
      break;
    case 4:
      if (pos == 3) {
        buf = 0b10111000;
      } else {
        buf = 0b10110001;
      }
      break;
    case 5:
      if (pos == 3) {
        buf = 0b01111100;
      } else {
        buf = 0b01110011;
      }
      break;
    case 6:
      if (pos == 3) {
        buf = 0b01111110;
      } else {
        buf = 0b01110111;
      }
      break;
    case 7:
      if (pos == 3) {
        buf = 0b11001000;
      } else {
        buf = 0b11000001;
      }
      break;
    case 8:
      if (pos == 3) {
        buf = 0b11111110;
      } else {
        buf = 0b11110111;
      }
      break;
    case 9:
      if (pos == 3) {
        buf = 0b11111100;
      } else {
        buf = 0b11110011;
      }
      break;
  }
  lcd_buf[pos] = buf;
}

// самоочевидные функции
void lcd_one(bool e) {
  if (e) {
    lcd_buf[0] |= (1 << 0);
  } else {
    lcd_buf[0] &= ~(1 << 0);
  }
}
void lcd_sec(bool e) {
  if (e) {
    lcd_buf[0] |= (1 << 7);
  } else {
    lcd_buf[0] &= ~(1 << 7);
  }
}
void lcd_minus(bool e) {
  if (e) {
    lcd_buf[0] |= (1 << 1);
  } else {
    lcd_buf[0] &= ~(1 << 1);
  }
}
void lcd_plus(bool e) {
  if (e) {
    lcd_buf[0] |= (1 << 6);
  } else {
    lcd_buf[0] &= ~(1 << 6);
  }
}
void lcd_lo(bool e) {
  if (e) {
    lcd_buf[0] |= (1 << 5);
  } else {
    lcd_buf[0] &= ~(1 << 5);
  }
}
void lcd_over(bool e) {
  if (e) {
    lcd_buf[0] |= (1 << 4);
  } else {
    lcd_buf[0] &= ~(1 << 4);
  }
}
void lcd_dot(int pos, bool e) {
  int pos_buf;
  if (pos == 1) {
    pos_buf = 3;
  } else if (pos == 2) {
    pos_buf = 2;
  } else if (pos == 3) {
    pos_buf = 1;
  } else {
    return;
  }
  if (pos_buf == 3) {
    if (e) {
      lcd_buf[pos_buf] |= (1 << 0);
    } else {
      lcd_buf[pos_buf] &= ~(1 << 0);
    }
  } else {
    if (e) {
      lcd_buf[pos_buf] |= (1 << 3);
    } else {
      lcd_buf[pos_buf] &= ~(1 << 3);
    }
  }
}

// дергается из основного цикла 100 раз в секунду
// выдает данные на 595-е, каждый раз меняя полярность
void lcd_refresh() {
  unsigned char data1 = lcd_buf[0];
  unsigned char data2 = lcd_buf[1];
  unsigned char data3 = lcd_buf[2];
  unsigned char data4 = lcd_buf[3];
  byte reverse = data1 & (1 << 3); // вот тут хранится бит текущей полярности
  if (reverse) { // и если он стоит, переворачиваем биты
    data1 = ~data1;
    data2 = ~data2;
    data3 = ~data3;
    data4 = ~data4;
  }
  for (int i = 0; i < 8; i++) {
    // берем данные из фреймбуфера побитно и выставляем на выводах контроллера
    if (data1 & (1 << i)) {
      LCD_PORT |= _BV(LCD_DATA1);
    } else {
      LCD_PORT &= ~_BV(LCD_DATA1);
    }
    if (data2 & (1 << i)) {
      LCD_PORT |= _BV(LCD_DATA2);
    } else {
      LCD_PORT &= ~_BV(LCD_DATA2);
    }
    if (data3 & (1 << i)) {
      LCD_PORT |= _BV(LCD_DATA3);
    } else {
      LCD_PORT &= ~_BV(LCD_DATA3);
    }
    if (data4 & (1 << i)) {
      LCD_PORT |= _BV(LCD_DATA4);
    } else {
      LCD_PORT &= ~_BV(LCD_DATA4);
    }
    // SCLK 595-х вверх
    sbi(LCD_PORT, LCD_SCLK);
    // SCLK 595-х вниз
    cbi(LCD_PORT, LCD_SCLK);
  }
  // еще раз дергаем SCLK
  // а все потому что у меня SCLK связан с RCLK и надо дернуть еще раз, что бы на выводах
  // оказалось то что мне нужно.
  // и все это, вообще-то, неправильно. надо было на контроллер выводить отдельно 
  // SCLK и RCLK, а OE тупо сажать на землю (см. даташит на 74HC595)
  // но и так сойдет.
  sbi(LCD_PORT, LCD_SCLK);
  cbi(LCD_PORT, LCD_SCLK);
  // включаем общий контакт жк-шки в нужной полярности
  // вообще-то у моей жк-шки два общих контакта, пины 1 и 40
  // (почему-то не соединенных между собой; теряюсь в догадках зачем так)
  // и второй контакт заведен на 4 вывод первой 595-й, так что провод к 
  // контроллеру немного лишний, но так уж распаялось
  if (reverse) {
    sbi(LCD_PORT, LCD_COM);
  } else {
    cbi(LCD_PORT, LCD_COM);
  }
  // переключаем полярность для следующего цикла
  lcd_buf[0] ^= (1 << 3);
}

// рисуем в фреймбуфере что надо и когда надо, в соответствии с текущим режимом
void do_render() {
  lcd_buf[0] = 0;
  lcd_buf[1] = 0;
  lcd_buf[2] = 0;
  lcd_buf[3] = 0;
  if (twi_problems) {
    lcd_lo(1);
  }
  if (mode == MODE_MAIN) {
    lcd_num(3, minute % 10);
    lcd_num(2, minute / 10);
    byte hour1 = (hour <= 12) ? hour : (hour % 12);
    lcd_num(1, hour1 % 10);
    if (hour1 >= 10) {
      lcd_one(1);
    }
    // мигалка секунд одну секунду горит, другую не горит. эстетично.
    // собственно ради нее я и возился с линией SQ и прерыванием,
    // что бы оно мигало равномерно, и что бы интерференция между часами и 
    // циклами контроллера этому не мешала
    if (second % 2) {
      lcd_sec(1);
    }
  } else if (mode == MODE_CALENDAR) {
    lcd_num(3, dayOfMonth % 10);
    lcd_num(2, dayOfMonth / 10);
    lcd_num(1, month % 10);
    if (month >= 10) {
      lcd_one(1);
    } else {
      lcd_one(0);
    }
    lcd_dot(2, 1);
  } else if (mode == MODE_YEAR) {
    lcd_num(3, year % 10);
    lcd_num(2, year / 10);
    lcd_buf[1] = 0b10110011; // это буква y. типа
  } else if (mode == MODE_TERMOMETER) {
  } else if (mode == MODE_VOLTMETER) {
  } else if (mode == MODE_SET_MINUTE) {
    if (even_10) {
      lcd_num(3, minute % 10);
      lcd_num(2, minute / 10);
    }
    byte hour1 = (hour <= 12) ? hour : (hour % 12);
    lcd_num(1, hour1 % 10);
    if (hour1 >= 10) {
      lcd_one(1);
    } else {
      lcd_one(0);
    }
    lcd_sec(1);
  } else if (mode == MODE_SET_HOUR) {
    lcd_num(3, minute % 10);
    lcd_num(2, minute / 10);
    if (even_10) {
      byte hour1 = (hour <= 12) ? hour : (hour % 12);
      lcd_num(1, hour1 % 10);
      if (hour1 >= 10) {
        lcd_one(1);
      } else {
        lcd_one(0);
      }
      if (hour > 12) {
        lcd_over(1);
      } else {
        lcd_over(0);
      }
    }
    lcd_sec(1);
  } else if (mode == MODE_SET_DAY) {
    if (even_10) {
      lcd_num(3, dayOfMonth % 10);
      lcd_num(2, dayOfMonth / 10);
    }
    lcd_num(1, month % 10);
    if (month >= 10) {
      lcd_one(1);
    } else {
      lcd_one(0);
    }
    lcd_dot(2, 1);
  } else if (mode == MODE_SET_MONTH) {
    lcd_num(3, dayOfMonth % 10);
    lcd_num(2, dayOfMonth / 10);
    if (even_10) {
      lcd_num(1, month % 10);
      if (month >= 10) {
        lcd_one(1);
      } else {
        lcd_one(0);
      }
    }
    lcd_dot(2, 1);
  } else if (mode == MODE_SET_YEAR) {
    if (even_10) {
      lcd_num(3, year % 10);
      lcd_num(2, year / 10);
    }
    lcd_buf[1] = 0b10110011;
  } else if (mode == MODE_SECOND) {
    lcd_sec(1);
    lcd_num(3, second % 10);
    lcd_num(2, second / 10);
  } else if (mode == MODE_DEBUG) {
    byte d = debug_value;
    lcd_num(3, d % 10);
    d /= 10;
    lcd_num(2, d % 10);
    lcd_num(2, d / 10);
  }
}

int main(void)
{
  // мумбо-юмбо на тему энерго сохранения.
  // помогает, приблизительно, на никак.
  // ACSR = (1<<ACD);
  ADCSRA = (0<<ADEN);
  PRR = (1<<PRTIM0) | (1<<PRTIM1) | (1<<PRTIM2) | (1<<PRSPI) | (1<<PRADC) | (1<<PRUSART0);

  // 
  DDRB = 0x00;
  PORTB = 0xff;
  DDRC = 0x00;
  PORTC = 0xff;
  DDRD = 0x00;
  PORTD = 0xff;

  lcd_buf[0] = 0x0;
  lcd_buf[1] = 0x0;
  lcd_buf[2] = 0x0;
  lcd_buf[3] = 0x0;

  twi_begin();

  // светодиодик для отладки. у меня же usbasp, так что отладочной консоли нет
  // и, надо сказать, светодиодик очень помог
  // DDRB |= _BV(PB7);
  // PORTB |= _BV(PB7); // off

  // инициализация портов жк-шки
  LCD_DDR |= (_BV(LCD_DATA1) | _BV(LCD_DATA2) | _BV(LCD_DATA3) | _BV(LCD_DATA4) | _BV(LCD_SCLK) | _BV(LCD_OE) | _BV(LCD_COM));
  cbi(LCD_PORT, LCD_SCLK);
  cbi(LCD_PORT, LCD_OE);

  // кнопки
  KEY1_DDR &= ~(_BV(KEY1));
  KEY1_PORT |= _BV(KEY1);
  KEY2_DDR &= ~(_BV(KEY2));
  KEY2_PORT |= _BV(KEY2);

  // ежесекундный привет от ds1307 и его обработчик
  DS1307SQ_DDR &= ~_BV(DS1307SQ);
  DS1307SQ_PORT |= _BV(DS1307SQ);
  PCICR |= _BV(DS1307SQ_INT);
  DS1307SQ_MSK |= _BV(DS1307SQ);
  sei();

  // ds1307 без батарейки теряет данные и, будучи подключен вновь к питанию, даже не тикает
  // что бы он затикал, необходимо выставить хоть какое-то время
  // setDS3231time(30,40,21,6,11,3,18);

  while(1)
  {
    byte need_render = 0;
    byte need_date_set = 0;

    // пришел привет от ds1307, зафиксированный обработчиком прерываний
    // читаем
    if (need_render_int) {
      byte rc;
      if (mode == MODE_MAIN) {
        rc = readDS3231time_hms(&second, &minute, &hour);
      } else {
        rc = readDS3231time(&second, &minute, &hour, &dayOfWeek, &dayOfMonth, &month, &year);
      }
      if ( ! rc && twi_problems) {
        twi_problems -= 1;
      } else {
        twi_problems += rc;
      }
      need_render = 1;
      need_render_int = 0;
    }

    // выполняется раз в 1/10 секунды
    if ( ! cycle_count_10) {
      even_10 = ! even_10;

      // если включен неосновной режим, уменьшаем счетчик 
      if (mode_timeout > 0) {
        mode_timeout -= 1;
      }
      // и если счетчик кончился, возвращаемся в основной режим
      if (mode != MODE_MAIN && ! mode_timeout) {
        mode = MODE_MAIN;
        need_render = 1;
      }
      // если мы в режиме установки времени, мигаем 10 раз в секунду
      if (mode == MODE_SET_MINUTE || mode == MODE_SET_HOUR || mode == MODE_SET_DAY || mode == MODE_SET_MONTH || mode == MODE_SET_YEAR) {
        need_render = 1; 
      }

      // читаем кнопки
      byte key1_down = (KEY1_PIN & _BV(KEY1)) ? 0 : 1;
      byte key2_down = (KEY2_PIN & _BV(KEY2)) ? 0 : 1;
      if (key1_down || key2_down || key1_press || key2_press) {
        need_render = 1;
      }
      if (key1_down && key1_press) {
        key1_time += 1;
      }
      if (key2_down && key2_press) {
        key2_time += 1;
      }
      // и хитрым образом переключаем режимы
      if (key1_down && ! key1_press) {
          if (mode == MODE_SET_MINUTE) {
            mode = MODE_SET_HOUR;
            mode_timeout = MODE_TIMEOUT_SET;
          } else if (mode == MODE_SET_HOUR) {
            mode = MODE_SET_DAY;
            mode_timeout = MODE_TIMEOUT_SET;
          } else if (mode == MODE_SET_DAY) {
            mode = MODE_SET_MONTH;
            mode_timeout = MODE_TIMEOUT_SET;
          } else if (mode == MODE_SET_MONTH) {
            mode = MODE_SET_YEAR;
            mode_timeout = MODE_TIMEOUT_SET;
          } else if (mode == MODE_SET_YEAR) {
            mode = MODE_MAIN;
          } else if (mode == MODE_MAIN) {
            mode = MODE_SECOND;
            mode_timeout = MODE_TIMEOUT_SET;
          } else if (mode == MODE_SECOND) {
            mode = MODE_MAIN;
            mode_timeout = 0;
          }
      } else if ( ! key1_down && key1_press) {
        if (key1_time >= KEY_TIMEOUT) {
        } else {
        }
      } else if (key1_down && key1_press) {
        if (key1_time >= KEY_TIMEOUT) {
          if (mode == MODE_MAIN || mode == MODE_SECOND) {
            mode = MODE_SET_MINUTE;
            mode_timeout = MODE_TIMEOUT_SET;
          }
        } else {
        }
      }
      if (key2_down && ! key2_press) {
        if (mode == MODE_MAIN) {
          mode = MODE_CALENDAR;
          mode_timeout = MODE_TIMEOUT;
        } else if (mode == MODE_CALENDAR) {
          mode = MODE_YEAR;
          mode_timeout = MODE_TIMEOUT;
        } else if (mode == MODE_YEAR) {
          mode = MODE_MAIN;
          mode_timeout = 0;
        } else if (mode == MODE_SET_MINUTE) {
          minute += 1;
          mode_timeout = MODE_TIMEOUT_SET;
          need_date_set = 1;
        } else if (mode == MODE_SET_HOUR) {
          hour += 1;
          mode_timeout = MODE_TIMEOUT_SET;
          need_date_set = 1;
        } else if (mode == MODE_SET_DAY) {
          dayOfMonth += 1;
          mode_timeout = MODE_TIMEOUT_SET;
          need_date_set = 1;
        } else if (mode == MODE_SET_MONTH) {
          month += 1;
          mode_timeout = MODE_TIMEOUT_SET;
          need_date_set = 1;
        } else if (mode == MODE_SET_YEAR) {
          year += 1;
          mode_timeout = MODE_TIMEOUT_SET;
          need_date_set = 1;
        } else if (mode == MODE_SECOND) {
            second = 0;
            need_date_set = 1;
            mode_timeout = MODE_TIMEOUT_SET;
        }
      } else if ( ! key2_down && key2_press) {
        if (key2_time >= KEY_TIMEOUT) {
        } else {
        }
      } else if (key2_down && key2_press) {
        if (key2_time >= KEY_TIMEOUT) {
          if (mode == MODE_SET_MINUTE) {
            minute += 1;
            mode_timeout = MODE_TIMEOUT_SET;
            need_date_set = 1;
          } else if (mode == MODE_SET_HOUR) {
            hour += 1;
            mode_timeout = MODE_TIMEOUT_SET;
            need_date_set = 1;
          } else if (mode == MODE_SET_DAY) {
            dayOfMonth += 1;
            mode_timeout = MODE_TIMEOUT_SET;
            need_date_set = 1;
          } else if (mode == MODE_SET_MONTH) {
            month += 1;
            mode_timeout = MODE_TIMEOUT_SET;
            need_date_set = 1;
          } else if (mode == MODE_SET_YEAR) {
            year += 1;
            mode_timeout = MODE_TIMEOUT_SET;
            need_date_set = 1;
          }
        } else {
        }
      }
      key1_press = key1_down;
      if ( ! key1_press) {
        key1_time = 0;
      }
      key2_press = key2_down;
      if ( ! key2_press) {
        key2_time = 0;
      }
    }

    if (need_date_set) {
      // корректируем время
      // вообще я не очень понял как ds1307 проверяет валидность установки времени,
      // но, вроде бы, пока проблем не замечено
      if (minute > 59) {
        minute = 0;
      }
      if (hour > 23) {
        hour = 0;
      }
      if (dayOfMonth > 31) {
        dayOfMonth = 1;
      }
      if (month > 12) {
        month = 1;
      }
      if (year > 99) {
        year = 0;
      }
      // записываем время в ds1307
      setDS3231time(second, minute, hour, dayOfWeek, dayOfMonth, month, year);
    }

    // собственно, рисуем фреймбуфер по необходимости
    if (need_render) {
      do_render();
    }

    // обновляем экран
    cli();
    lcd_refresh();
    sei();

    _delay_ms(10);

    cycle_count_10 += 1;
    if (cycle_count_10 >= 10) {
      cycle_count_10 = 0;
    }
  }

  return 0;
}

// это я взял где-то в другом месте
byte decToBcd(byte val)
{
  return( (val/10*16) + (val%10) );
}
byte bcdToDec(byte val)
{
  return( (val/16*10) + (val%16) );
}

// запись времени в ds1307
void setDS3231time(byte second, 
                   byte minute, 
                   byte hour, 
                   byte dayOfWeek, 
                   byte dayOfMonth, 
                   byte month, 
                   byte year)
{
  twi_write((uint8_t) DS3231_I2C_ADDRESS, (uint8_t) 0, (uint8_t) decToBcd(second));
  twi_write((uint8_t) DS3231_I2C_ADDRESS, (uint8_t) 1, (uint8_t) decToBcd(minute));
  twi_write((uint8_t) DS3231_I2C_ADDRESS, (uint8_t) 2, (uint8_t) decToBcd(hour));
  twi_write((uint8_t) DS3231_I2C_ADDRESS, (uint8_t) 3, (uint8_t) decToBcd(dayOfWeek));
  twi_write((uint8_t) DS3231_I2C_ADDRESS, (uint8_t) 4, (uint8_t) decToBcd(dayOfMonth));
  twi_write((uint8_t) DS3231_I2C_ADDRESS, (uint8_t) 5, (uint8_t) decToBcd(month));
  twi_write((uint8_t) DS3231_I2C_ADDRESS, (uint8_t) 6, (uint8_t) decToBcd(year));
  // выставляем частоту на выходе SQ 1Hz
  // это надо сделать хотя бы только один раз, что бы ds1307 не устроил дос 
  // в обработчике прерываний на частоте 8kHz
  twi_write((uint8_t) DS3231_I2C_ADDRESS, (uint8_t) 7, (uint8_t) 0b00010000);
}

// чтение времени из ds1307
byte readDS3231time(byte *second,
                    byte *minute,
                    byte *hour,
                    byte *dayOfWeek,
                    byte *dayOfMonth,
                    byte *month,
                    byte *year)
{
  byte rc = twi_read(DS3231_I2C_ADDRESS, 0, 7);
  if (rc) return 1;
  *second = bcdToDec(twi_receive() & 0x7f);
  *minute = bcdToDec(twi_receive());
  *hour = bcdToDec(twi_receive() & 0x3f);
  *dayOfWeek = bcdToDec(twi_receive());
  *dayOfMonth = bcdToDec(twi_receive());
  *month = bcdToDec(twi_receive());
  *year = bcdToDec(twi_receive());
}

// чтение только секунд, минут и часов, то есть, для основного режима
byte readDS3231time_hms(byte *second, byte *minute, byte *hour) {
  byte rc = twi_read(DS3231_I2C_ADDRESS, 0, 3);
  if (rc) return 1;
  *second = bcdToDec(twi_receive() & 0x7f);
  *minute = bcdToDec(twi_receive());
  *hour = bcdToDec(twi_receive() & 0x3f);
}

/*
 * Код ниже, на самом деле, взят тут: http://dsscircuits.com/articles/arduino-i2c-master-library
 * Переработан и урезан до самого минимума, ибо иначе оно вместе с моим кодом в 4k не лезло.
 */
/*
  This library is free software; you can redistribute it and/or
  modify it under the terms of the GNU Lesser General Public
  License as published by the Free Software Foundation; either
  version 2.1 of the License, or (at your option) any later version.

  This library is distributed in the hope that it will be useful,
  but WITHOUT ANY WARRANTY; without even the implied warranty of
  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
  Lesser General Public License for more details.

  You should have received a copy of the GNU Lesser General Public
  License along with this library; if not, write to the Free Software
  Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA
*/

#define START           0x08
#define REPEATED_START  0x10
#define MT_SLA_ACK  0x18
#define MT_SLA_NACK 0x20
#define MT_DATA_ACK     0x28
#define MT_DATA_NACK    0x30
#define MR_SLA_ACK  0x40
#define MR_SLA_NACK 0x48
#define MR_DATA_ACK     0x50
#define MR_DATA_NACK    0x58
#define LOST_ARBTRTN    0x38
#define TWI_STATUS      (TWSR & 0xF8)
#define SLA_W(address)  (address << 1)
#define SLA_R(address)  ((address << 1) + 0x01)

#define MAX_BUFFER_SIZE 32

uint8_t twi_bytesAvailable = 0;
uint8_t twi_bufferIndex = 0;
uint8_t twi_totalBytes = 0;
uint16_t twi_timeOutDelay = 0;
uint8_t twi_returnStatus;
uint8_t twi_nack;
uint8_t twi_data[MAX_BUFFER_SIZE];

void twi_begin()
{
  sbi(PORTC, 4);
  sbi(PORTC, 5);
  cbi(TWSR, TWPS0);
  cbi(TWSR, TWPS1);
  TWBR = ((F_CPU / 100000) - 16) / 2;
  TWCR = _BV(TWEN) | _BV(TWEA); 
}
uint8_t twi_read(uint8_t address, uint8_t registerAddress, uint8_t numberBytes)
{
  twi_bytesAvailable = 0;
  twi_bufferIndex = 0;
  if(numberBytes == 0){numberBytes++;}
  twi_nack = numberBytes - 1;
  twi_returnStatus = 0;
  twi_returnStatus = twi_start();
  if(twi_returnStatus){return(twi_returnStatus);}
  twi_returnStatus = twi_sendAddress(SLA_W(address));
  if(twi_returnStatus)
  {
    if(twi_returnStatus == 1){return(2);}
    return(twi_returnStatus);
  }
  twi_returnStatus = twi_sendByte(registerAddress);
  if(twi_returnStatus)
  {
    if(twi_returnStatus == 1){return(3);}
    return(twi_returnStatus);
  }
  twi_returnStatus = twi_start();
  if(twi_returnStatus)
  {
    if(twi_returnStatus == 1){return(4);}
    return(twi_returnStatus);
  }
  twi_returnStatus = twi_sendAddress(SLA_R(address));
  if(twi_returnStatus)
  {
    if(twi_returnStatus == 1){return(5);}
    return(twi_returnStatus);
  }
  for(uint8_t i = 0; i < numberBytes; i++)
  {
    if( i == twi_nack )
    {
      twi_returnStatus = twi_receiveByte(0);
      if(twi_returnStatus == 1){return(6);}
      if(twi_returnStatus != MR_DATA_NACK){return(twi_returnStatus);}
    }
    else
    {
      twi_returnStatus = twi_receiveByte(1);
      if(twi_returnStatus == 1){return(6);}
      if(twi_returnStatus != MR_DATA_ACK){return(twi_returnStatus);}
    }
    twi_data[i] = TWDR;
    twi_bytesAvailable = i+1;
    twi_totalBytes = i+1;
  }
  twi_returnStatus = twi_stop();
  if(twi_returnStatus)
  {
    if(twi_returnStatus == 1){return(7);}
    return(twi_returnStatus);
  }
  return(twi_returnStatus);
}
uint8_t twi_write(uint8_t address, uint8_t registerAddress, uint8_t data)
{
  twi_returnStatus = 0;
  twi_returnStatus = twi_start(); 
  if(twi_returnStatus){return(twi_returnStatus);}
  twi_returnStatus = twi_sendAddress(SLA_W(address));
  if(twi_returnStatus)
  {
    if(twi_returnStatus == 1){return(2);}
    return(twi_returnStatus);
  }
  twi_returnStatus = twi_sendByte(registerAddress);
  if(twi_returnStatus)
  {
    if(twi_returnStatus == 1){return(3);}
    return(twi_returnStatus);
  }
  twi_returnStatus = twi_sendByte(data);
  if(twi_returnStatus)
  {
    if(twi_returnStatus == 1){return(3);}
    return(twi_returnStatus);
  }
  twi_returnStatus = twi_stop();
  if(twi_returnStatus)
  {
    if(twi_returnStatus == 1){return(7);}
    return(twi_returnStatus);
  }
  return(twi_returnStatus);
}
uint8_t twi_receive()
{
  twi_bufferIndex = twi_totalBytes - twi_bytesAvailable;
  if(!twi_bytesAvailable)
  {
    twi_bufferIndex = 0;
    return(0);
  }
  twi_bytesAvailable--;
  return(twi_data[twi_bufferIndex]);
}
uint8_t twi_start()
{
  unsigned long startingTime = millis();
  TWCR = (1<<TWINT)|(1<<TWSTA)|(1<<TWEN);
  while (!(TWCR & (1<<TWINT)))
  {
    if(!twi_timeOutDelay){continue;}
    if((millis() - startingTime) >= twi_timeOutDelay)
    {
      twi_lockUp();
      return(1);
    }

  }
  if ((TWI_STATUS == START) || (TWI_STATUS == REPEATED_START))
  {
    return(0);
  }
  if (TWI_STATUS == LOST_ARBTRTN)
  {
    uint8_t bufferedStatus = TWI_STATUS;
    twi_lockUp();
    return(bufferedStatus);
  }
  return(TWI_STATUS);
}
uint8_t twi_sendAddress(uint8_t i2cAddress)
{
  TWDR = i2cAddress;
  unsigned long startingTime = millis();
  TWCR = (1<<TWINT) | (1<<TWEN);
  while (!(TWCR & (1<<TWINT)))
  {
    if(!twi_timeOutDelay){continue;}
    if((millis() - startingTime) >= twi_timeOutDelay)
    {
      twi_lockUp();
      return(1);
    }

  }
  if ((TWI_STATUS == MT_SLA_ACK) || (TWI_STATUS == MR_SLA_ACK))
  {
    return(0);
  }
  uint8_t bufferedStatus = TWI_STATUS;
  if ((TWI_STATUS == MT_SLA_NACK) || (TWI_STATUS == MR_SLA_NACK))
  {
    twi_stop();
    return(bufferedStatus);
  }
  else
  {
    twi_lockUp();
    return(bufferedStatus);
  } 
}
uint8_t twi_sendByte(uint8_t i2cData)
{
  TWDR = i2cData;
  unsigned long startingTime = millis();
  TWCR = (1<<TWINT) | (1<<TWEN);
  while (!(TWCR & (1<<TWINT)))
  {
    if(!twi_timeOutDelay){continue;}
    if((millis() - startingTime) >= twi_timeOutDelay)
    {
      twi_lockUp();
      return(1);
    }

  }
  if (TWI_STATUS == MT_DATA_ACK)
  {
    return(0);
  }
  uint8_t bufferedStatus = TWI_STATUS;
  if (TWI_STATUS == MT_DATA_NACK)
  {
    twi_stop();
    return(bufferedStatus);
  }
  else
  {
    twi_lockUp();
    return(bufferedStatus);
  } 
}
uint8_t twi_receiveByte(uint8_t ack)
{
  unsigned long startingTime = millis();
  if(ack)
  {
    TWCR = (1<<TWINT) | (1<<TWEN) | (1<<TWEA);

  }
  else
  {
    TWCR = (1<<TWINT) | (1<<TWEN);
  }
  while (!(TWCR & (1<<TWINT)))
  {
    if(!twi_timeOutDelay){continue;}
    if((millis() - startingTime) >= twi_timeOutDelay)
    {
      twi_lockUp();
      return(1);
    }
  }
  if (TWI_STATUS == LOST_ARBTRTN)
  {
    uint8_t bufferedStatus = TWI_STATUS;
    twi_lockUp();
    return(bufferedStatus);
  }
  return(TWI_STATUS); 
}
uint8_t twi_stop()
{
  unsigned long startingTime = millis();
  TWCR = (1<<TWINT)|(1<<TWEN)| (1<<TWSTO);
  while ((TWCR & (1<<TWSTO)))
  {
    if(!twi_timeOutDelay){continue;}
    if((millis() - startingTime) >= twi_timeOutDelay)
    {
      twi_lockUp();
      return(1);
    }

  }
  return(0);
}
void twi_lockUp()
{
  TWCR = 0;
  TWCR = _BV(TWEN) | _BV(TWEA);
}

А дальше начались проблемы. Вернее, проблема выявилась одна, но глобальная — энергопотребление. Запустив часы на тестовой макетке, уже с частотой 1MHz, выяснил, что получается около 6mA. Предпринятые меры к уменьшению этого значения ни к чему не привели. Пытался, например, на время паузы уменьшать частоту контроллера до предела: в результате получил проблемы с USBasp-ом и крайне скромную экономию электричества.

Штука в том, что все рекомендации встреченные мною на тему энергопотребления AVR-ок замечательно описываются фразой "спите глубже". Но контроллеру в часах нельзя спать! Ему денно и нощно, 100 раз в секунду необходимо обновлять экран. А один только вывод из сна занимает как минимум 65ms, требуемые на стабилизацию осциллятора, что кратно больше требуемого промежутка между обновлениями экрана.

Возможно, есть какое-то неизвестное мне шаманство, при помощи которого можно в условиях часов применить сон для контроллера, но пока что я сдался, воткнул 7805 с кроной, и ожидаю остановку часов где-то через неделю.

Но! У меня же есть еще один такой же индикатор. Может быть, если взять какой-то более приспособленный к этой задаче контроллер, удастся создать, наконец, часы моей мечты? Я был бы признателен, если бы уважаемая публика подсказала мне куда следует смотреть для решения этой задачи. STM8/32? MSP430? Или, все-таки, шаманить с режимами AVR?

Неудача с часами на ATMega48 - 6

Автор: dmiceman

Источник

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js