Вступление
Решив увеличить свои знания не в «ширину», а в «глубину» получился этот проект. Захотелось реализовать на практики полученные знания по ассемблеру на чипах AVR семейства tiny. В наличии был модуль часов реального времени ds1302, микроконтроллер ATtiny2313A и модуль на контроллере TM1637 с четырьмя элементами для отображения с точками и двоеточием. А готовых и понятных примеров на ассемблере в русскоязычном интернете очень и очень мало!
Задача: собрать устройство «Цифровые часы», которое, как вы могли догадаться, будет отображать текущее время и дату, и иметь будильник. Управляться (установка данных) с помощью силы мысли или кнопок. Иметь как можно малый малый размер и автономное питание. И цена не должна сильно превышать имеющиеся аналоги, а лучше быть дешевле!
Имеющиеся устройства
Микроконтроллер ATTiny2313a (ссылка) |
Модуль часов реального времени DS1302 (ссылка) |
Модуль TM1637 (ссылка) |
Режимы и кнопки
Циферблат у часов четырёхэлементный, будем отображать данные с помощью режимов. Для переключения режимов используем кнопку. Назовём её «Clock_mode / Set».
Режимы («Clock_mode») я выбрал такие:
- Clock_mode = 0 — отображение времени. Пример: 23:26
- Clock_mode = 1 — отображение минут и секунд. Пример: 26:01
- Clock_mode = 2 — отображение даты. Пример: 1310
Устанавливаем ещё одну кнопку, назовём её «Mode». При нажатии на неё начинают моргать по очереди: часы, минуты, число, месяц, год и т.д. Что моргает в текущий момент, то и увеличивается на единицу с помощью кнопки «Clock_mode / Set». Независимо от того, что сейчас отображается (какой режим Clock_mode), первое нажатие кнопки «Mode» будет всегда одинаково. То есть отображается сейчас дата (Clock_mode = 2), нажимаем на кнопку «Mode», отображается время и начинает моргать её первая часть, то есть часы. Нажимаем «Mode» ещё раз и, уже, моргают минуты и т. д.
Режимы «Mode»:
- Mode = 0 — нормальный режим. Отображается то, что выбрано кнопкой «Clock_mode / Set».
- Mode = 1 — моргают часы. Пример: 12:00
- Mode = 2 — моргают минуты. Пример: 12:00
- Mode = 3 — моргает число месяца. Пример: 21.07
- Mode = 4 — моргает номер месяца. Пример: 21.07
- Mode = 5 — моргает год. Пример: 2033
- Mode = 6 — моргает вкл.\выкл будильника. Пример: A-ON или A-OF
- Mode = 7 — моргают часы будильника. Пример: 07:30
- Mode = 8 — моргают минуты будильника. Пример: 07:30
- Mode = 9 — моргают и часы и минуты. В данный режим микроконтроллер переходит только программно, когда срабатывает будильник!
Обращу внимание на дату. В месяцах максимальное значение числа разное. А ещё есть високосные годы. Високосный год - каждый четвёртый, каждый сотый год не является високосным! Устанавливая дату, мы должны учитывать выше написанное. Ещё можно выставить 31-е число 1-го месяца, а потом сменить месяц на 2-й. Это тоже учитываем. Ограничения для года в своём коде я установил такие: 2000 — 2099, думаю больше/меньше не надо, но всё в ваших руках.
Схема подключения
Модуль с циферблатом и модуль времени я подключил к порту PORTB с нулевой ножки. Выход на зуммер подключён к ноге PB2, т.к. звук будет генерироваться от таймера TIM0. Кнопки подключаем к ножкам внешних прерываний: «Mode» к INT0, «Clock_mode / Set» к INT1. Изначально планировалось три кнопки. Третья кнопка регулировала бы яркость дисплея, но так как устройство работает автономно было решено настроить минимальную яркость и убрать кнопку. Осциллограф нужен только для отладки программы.
Работа модулей
DS1302
DS1302 работает на собственном 3-х проводном протоколе. Имеет следующие регистры:
Название регистров | Адрес чтения | Адрес записи | 7 бит | 6 бит | 5 бит | 4 бит | 3 бит | 2 бит | 1 бит | 0 бит |
Примечание |
Секунды | 0x81 | 0x80 | CH | старший разряд | младший разряд | 00 - 59 | |||||
Минуты | 0x83 | 0x82 | 0 | старший разряд | младший разряд | 00 - 59 | |||||
Часы | 0x85 | 0x84 | 1 | 0 | AM/PM | старший | младший разряд | 00 - 12 | |||
0 | старший разряд | 00 - 23 | |||||||||
День | 0x87 | 0x86 | 0 | 0 | старший разряд | младший разряд | 01 - 31 | ||||
Месяц | 0x89 | 0x88 | 0 | 0 | 0 | старший разряд | младший разряд | 01 - 12 | |||
День недели | 0x8B | 0x8A | 0 | 0 | 0 | 0 | 0 | число | 01 - 07 | ||
Год | 0x8D | 0x8C | старший разряд | младший разряд | 00 - 99 | ||||||
Защита от записи | 0x8F | 0x8E | WP | 0 | 0 | 0 | 0 | 0 | 0 | 0 | Флаги управления чипом |
Управление | 0x91 | 0x90 | TCS3 | TCS2 | TCS1 | TCS0 | DS1 | DS0 | RS1 | RS0 | Флаги управления устройством заряда малым током |
Пакетная передача | 0xBF | 0xBE | Пакет из 8-ми регистров: секунды - защита от записи | ||||||||
Свободные регистры |
0xC1 0xFD |
0xC0 0xFC |
Регистры могут использоваться для хранения данных |
||||||||
Пакетная передача | 0xFF | 0xFE | Пакет из 31-го свободного регистра |
Назначение флагов:
CH | (Clock Halt) - флаг отключения часов: значение «1» - останавливает часы, значение «0» - запускает. |
WP | (Write-Protect) - флаг защиты от записи: значение «1» - запрещает запись данных в регистры модуля, значение «0» - разрешает. |
TCS | (Trickle Charger Select) - флаги TCS3, TCS2, TCS1, TCS0 включения устройства заряда малым током. |
DS | (Diode Select) - флаги DS1, DS0 подключения диодов в устройстве заряда малым током. |
RS | (Resistor Select) - флаги RS1, RS0 подключения резисторов в устройстве заряда малым током. |
Распиновка DS1302:
Ножки X1 и X2 - подключение кварцевого резонатора на 32,768 кГц. VCC1 - подключение батарейки, VCC2 - основное питание. Ножка RST отвечает за "включение\выключение" контроллера. Когда на ней логическая единица, микроконтроллер принимает и передаёт данные, когда ноль - нет. Ножка SCLK это линия тактирования. Ножка I/O - приём и передача данных. Обращу ваше внимание, что данные передаются младшим битом вперёд, а так же в упакованном формате. По спецификации DS1302 использует VCC2, если его напряжение больше, чем на VCC1 + 0,2 В. Я выбрал источником питания только VCC2, т.к. устройство и так работает от аккумуляторов. Далее рассмотрим алгоритм работы контроллера на примерах.
Считали 08 минут. | Записали 18 часов. |
Алгоритм чтения данных:
- Устанавливаем ножку RST (включаем контроллер)
- Передаём первый байт - байт адреса регистра. В нашем случае - это 0x83.
- Считываем байт данных. На картинке - это 0x08
- Сбрасываем ножку RST (выключаем контроллер)
Алгоритм записи данных:
- Устанавливаем ножку RST (включаем контроллер)
- Передаём первый байт - адрес регистра. В нашем случае - это 0x84.
- Передаём второй данных. На картинке - это 0x18
- Сбрасываем ножку RST (выключаем контроллер)
Задержки между операциями я подбирал практически. В моём случае они, приблизительно, равны 10 мкс. Перед отправкой данных выставляется ножка I/O в соответствующее состояние, далее по переднему фронту SCLK, DS1302 считывает данные. При чтении данные считываются по заднему фронту SCLK.
Листинг файла ds1302.asm.
;======================================================== ; Подпрограммы начала и конца передачи данных ;======================================================== DS1302_send_start: sbi PORT_DS1302, DS1302_CE rcall MCU_wait_10mks ret DS1302_send_stop: cbi PORT_DS1302, DS1302_SCLK cbi PORT_DS1302, DS1302_IO cbi PORT_DS1302, DS1302_CE rcall MCU_wait_10mks ret ;======================================================== ; Отправка байта из регистра BYTE ;======================================================== DS1302_send_byte: push r16 push BYTE ;------------------------- Вывод DAT на выход cbi PORT_DS1302, DS1302_IO sbi DDR_DS1302, DS1302_IO ;------------------------- Счётчик цикла clr r16 ;------------------------- Начало цикла DS1302_while_send: cpi r16, 0x08 brsh DS1302_while_send_end cbi PORT_DS1302, DS1302_SCLK rcall MCU_wait_10mks lsr BYTE brcc DS1302_cbi_send_bit DS1302_sbi_send_bit: sbi PORT_DS1302, DS1302_IO rjmp DS1302_while_send_bit DS1302_cbi_send_bit: cbi PORT_DS1302, DS1302_IO DS1302_while_send_bit: ;------------------------- Отправка бита sbi PORT_DS1302, DS1302_SCLK rcall MCU_wait_10mks inc r16 rjmp DS1302_while_send ;------------------------- Выход из цикла DS1302_while_send_end: pop BYTE pop r16 ret ;======================================================== ; Получение байта, результат в регистр BYTE ;======================================================== DS1302_receive_byte: push r16 ;------------------------- Вывод DAT на вход cbi PORT_DS1302, DS1302_IO cbi DDR_DS1302, DS1302_IO ;------------------------- Счётчик цикла clr r16 clr BYTE ;------------------------- Начало цикла DS1302_while_receive: cpi r16, 0x07 brsh DS1302_while_receive_end rcall DS1302_receive_byte_write_bit lsr BYTE sbi PORT_DS1302, DS1302_SCLK rcall MCU_wait_10mks inc r16 rjmp DS1302_while_receive ;------------------------- Выход из цикла и получение последнего бита DS1302_while_receive_end: rcall DS1302_receive_byte_write_bit sbi PORT_DS1302, DS1302_SCLK pop r16 ret DS1302_receive_byte_write_bit: cbi PORT_DS1302, DS1302_SCLK rcall MCU_wait_10mks sbic PIN_DS1302, DS1302_IO ori BYTE, 0x80 sbis PIN_DS1302, DS1302_IO andi BYTE, 0x7f ret ;======================================================== ; Считать данные с ds1302 пакетом ;======================================================== DS1302_read_package_data: push r17 push r16 push r15 push XH push XL push YH push YL ldi XH, high(bcd_seconds) ldi XL, low(bcd_seconds) ldi YH, high(tm_s1) ldi YL, low(tm_s1) rcall DS1302_send_start ldi BYTE, 0xBF rcall DS1302_send_byte clr r17 DS1302_read_package_data_while_receive: cpi r17, 0x07 brsh DS1302_read_package_data_while_receive_end rcall DS1302_receive_byte st X+, BYTE push r17 mov r17, BYTE rcall conv_ds1302_to_tm1637 st Y+, r16 st Y+, r15 pop r17 inc r17 rjmp DS1302_read_package_data_while_receive DS1302_read_package_data_while_receive_end: rcall DS1302_send_stop pop YL pop YH pop XL pop XH pop r15 pop r16 pop r17 ret
Изначально функция DS1302_read_package_data была совершенно другой. В ней считывались отдельно минуты, часы, дата с месяцем и год. Использовав пакетное чтение размер прошивки хорошо уменьшился, но в оперативной памяти есть данные которые не используются. Так же пакетное чтение прерывается на седьмом байте приёма, т.к. данные регистра "защита от записи" не используются в данном проекте.
TM1637
Распиновка модуля:
У него свой 2-х проводной интерфейс. CLK - линия тактирования. DIO - приём\передача данных. Когда CLK на высоком уровне, и DIO меняется с высокого на низкий - начинается ввод данных. Когда CLK находится на высоком уровне и DIO изменяется от низкого к высокому, ввод данных заканчивается. Данные передаются, так же как и с DS1302, младшим битом вперёд.
У микроконтроллера TM1637 есть три вида команд: команды данных, команды адреса и команды контроля дисплея. Они отличаются двумя старшими битами.
Команды данных:
Бит 7 | Бит 6 | Бит 5 | Бит 4 | Бит 3 | Бит 2 | Бит 1 | Бит 0 | Функция | Описание |
0 | 1 | 0 | 0 | 0 | Режим чтения\записи | Запись данных в регистр дисплея | |||
0 | 1 | 1 | 0 | Чтение key scan данных | |||||
0 | 1 | 0 | Режим адресации | Автоматический инкремент адреса | |||||
0 | 1 | 1 | Фиксированный адрес | ||||||
0 | 1 | 0 | Режим тестирования | Нормальный режим | |||||
0 | 1 | 1 | Тестовый режим |
Что такое тестовый режим я в документации не нашёл.
Команды адреса:
Бит 7 | Бит 6 | Бит 5 | Бит 4 | Бит 3 | Бит 2 | Бит 1 | Бит 0 | Адрес дисплея | |
1 | 1 | 0 | 0 | 0 | 0 | 0 |
|
||
1 | 1 | 0 | 0 | 0 | 1 | C1H | |||
1 | 1 | 0 | 0 | 1 | 0 | C2H | |||
1 | 1 | 0 | 0 | 1 | 1 | C3H | |||
1 | 1 | 0 | 1 | 0 | 0 | C4H | |||
1 | 1 | 0 | 1 | 0 | 1 | C5H |
В нашем случае элементов для отображения 4-и, значит и адреса у нас C0H - C3H.
Команды контроля дисплея:
Бит 7 | Бит 6 | Бит 5 | Бит 4 | Бит 3 | Бит 2 | Бит 1 | Бит 0 | Функция | Описание |
1 | 0 | 0 | 0 | 0 | 0 | Настройки яркости | Ширина импульса устанавливается как 1/16 | ||
1 | 0 | 0 | 0 | 1 | Ширина импульса устанавливается как 2/16 | ||||
1 | 0 | 0 | 1 | 0 | Ширина импульса устанавливается как 4/16 | ||||
1 | 0 | 0 | 1 | 1 | Ширина импульса устанавливается как 10/16 | ||||
1 | 0 | 1 | 0 | 0 | Ширина импульса устанавливается как 11/16 | ||||
1 | 0 | 1 | 0 | 1 | Ширина импульса устанавливается как 12/16 | ||||
1 | 0 | 1 | 1 | 0 | Ширина импульса устанавливается как 13/16 | ||||
1 | 0 | 1 | 1 | 1 | Ширина импульса устанавливается как 14/16 | ||||
1 | 0 | 0 |
Включение выключение дисплея |
Выключить дисплей | |||||
1 | 0 | 1 | Включить дисплей |
Разберём пример:
Байт команды данных | Байт команды адреса | Байт данных |
Алгоритм записи данных (установка значения одного элемента):
- Сигнал START
- Передаём байт команды данных. В нашем примере - это 0x44.
- Ожидание бита ответа
- Сигнал STOP
- Сигнал START
- Передаём байт адреса регистра. В нашем случае - это второй элемент на дисплее, его адрес - 0xC1
- Ожидание бита ответа
- Передаём байт данных
- Ожидание бита ответа
- Сигнал STOP
Разберём этот алгоритм. Сигнал START - при высоком уровне CLK, DIO устанавливается с высокого на низкий. Сигнал STOPT - при высоком уровне CLK, DIO устанавливается с низкого на высокий. Ожидание бита ответа - после приёма 8-ми бит, контроллер TM1637 устанавливает на DIO низкий уровень как сигнал о том, что он всё принял корректно. Первый байт - это байт команды, 0x44 - 0b0100 0100. Смотрим по табличке "Команды данных" и определяем, что режим адресации устанавливается в фиксированное значение. Далее следует байт команды адреса, в котором указывается адрес регистра дисплея для отображения. 0xC1 - это второй элемент на дисплее. Следующий байт -это сами данные, которые надо отобразить на дисплее. Теперь подробнее про то, что нужно передавать для отображения того или иного числа или буквы.
Выше нарисован один элемент дисплея разбитый на 8-мь сегментов. Какой бит установлен, тот сегмент и будет гореть на дисплее. Для отображения единицы (бит 1 и бит 2) нужно передать значение 0b00000110. Для отображения нуля - 0b00111111. Седьмой бит отвечает за точку на дисплеях с точками. У меня дисплей и с двоеточием. Для зажигания двоеточия нужно установить 7-й бит у второго элемента. На схеме Proteus я использовал два вида дисплеев - с точками и двоеточием. 7-й бит второго элемента зажигает второю точку, а не двоеточие. Для двоеточия нужно использовать 4-й элемент. На примере выше число 0x86 - это единица с включенным двоеточием, т.к. байт адреса 0xC1 - это второй элемент на дисплее. Если бы мы на примере выше в байте команды установили не фиксированный режим адреса, а автоматический, то нужно было бы после команды адреса передать столько байт, сколько элементов подряд на дисплее мы хотели бы установить.
Передача команд контроля дисплея аналогична примеру выше, только передаётся всего один байт. Не забываем включить дисплей в своей программе. У меня команда включения отправляется каждый раз при установки значений в регистрах дисплея. Хочу обратить внимание, что, в моём случае, в программа Proteus яркость в самом малом значении вообще не отображается!
Листинг файла tm1637.asm
;======================================================== ; Подпрограммы начала и конца передачи данных ;======================================================== TM1637_start: sbi PORT_TM1367, TM1637_CLK sbi PORT_TM1367, TM1637_DATA nop cbi PORT_TM1367, TM1637_DATA ret TM1637_stop: cbi PORT_TM1367, TM1637_CLK cbi PORT_TM1367, TM1637_DATA nop sbi PORT_TM1367, TM1637_CLK sbi PORT_TM1367, TM1637_DATA ret ;======================================================== ; Отправка байта из регистра BYTE ;======================================================== TM1637_send_byte: push r16 push BYTE ;------------------------- Счётчик цикла clr r16 ;------------------------- Начало цикла TM1637_while_send: cpi r16, 0x08 brsh TM1637_wait_ACK cbi PORT_TM1367, TM1637_CLK lsr BYTE brcc TM1637_cbi_send_bit TM1637_sbi_send_bit: sbi PORT_TM1367, TM1637_DATA rjmp TM1637_while_send_bit TM1637_cbi_send_bit: cbi PORT_TM1367, TM1637_DATA TM1637_while_send_bit: ;------------------------- Отправка бита, задержки в один nop на 1МГ хватает nop sbi PORT_TM1367, TM1637_CLK nop inc r16 cbi PORT_TM1367, TM1637_CLK nop cbi PORT_TM1367, TM1637_DATA rjmp TM1637_while_send ;------------------------- Ожидания бита ответа TM1637_wait_ACK: sbic PIN_TM1367, TM1637_DATA rjmp TM1637_wait_ACK sbi DDR_TM1367, TM1637_DATA sbi PORT_TM1367, TM1637_CLK nop cbi PORT_TM1367, TM1637_CLK pop BYTE pop r16 ret ;======================================================== ; Отображение данных ;======================================================== TM1637_display: push BYTE ;------------------------- Команда записи в регистр дисплея c автоматическим инкрементом адреса rcall TM1637_start ldi BYTE, 0x40 rcall TM1637_send_byte rcall TM1637_stop ;------------------------- Начальный адрес - первый символ дисплея rcall TM1637_start ldi BYTE, 0xC0 rcall TM1637_send_byte ;------------------------- Запись данных для каждого регистра дисплея mov BYTE, TM1637_char1 rcall TM1637_send_byte mov BYTE, TM1637_char2 rcall TM1637_send_byte mov BYTE, TM1637_char3 rcall TM1637_send_byte mov BYTE, TM1637_char4 rcall TM1637_send_byte rcall TM1637_stop pop BYTE ret ;======================================================== ; Отображение времени ;======================================================== TM1637_display_time: lds TM1637_char1, tm_h1 lds TM1637_char2, tm_h2 sbr TM1637_char2, 0b10000000 lds TM1637_char3, tm_m1 lds TM1637_char4, tm_m2 rcall TM1637_display ret ;======================================================== ; Отображение секунд ;======================================================== TM1637_display_seconds: lds TM1637_char1, tm_m1 lds TM1637_char2, tm_m2 sbr TM1637_char2, 0b10000000 lds TM1637_char3, tm_s1 lds TM1637_char4, tm_s2 rcall TM1637_display ret ;======================================================== ; Отображение даты ;======================================================== TM1637_display_date: lds TM1637_char1, tm_d1 lds TM1637_char2, tm_d2 lds TM1637_char3, tm_mt1 lds TM1637_char4, tm_mt2 rcall TM1637_display ret ;======================================================== ; Отображение года ;======================================================== TM1637_display_year: lds TM1637_char1, tm_y1 lds TM1637_char2, tm_y2 lds TM1637_char3, tm_y3 lds TM1637_char4, tm_y4 rcall TM1637_display ret ;======================================================== ; Отображение времени будильника ;======================================================== TM1637_display_alarm: lds TM1637_char1, tm_ah1 lds TM1637_char2, tm_ah2 sbr TM1637_char2, 0b10000000 lds TM1637_char3, tm_am1 lds TM1637_char4, tm_am2 rcall TM1637_display ret ;======================================================== ; Отображение режима будильника (вкл.\ выкл.) ;======================================================== TM1637_display_alarm_mode: push r17 ldi r17, char_A mov TM1637_char1, r17 ldi TM1637_char2, char_minus ldi TM1637_char3, char_0 sbrc alarm, 0 ldi TM1637_char4, char_N sbrs alarm, 0 ldi TM1637_char4, char_F rcall TM1637_display pop r17 ret ;======================================================== ; Моргание ; Режимы моргания: ; 1-й и 2-й элементы r17==1 ; 3-й и 4-й элементы r17==2 ; значение 1-го моргающего элемента == r18 ; значение 2-го моргающего элемента == r19 ;======================================================== TM1637_blink_pair: push r19 push r18 push r17 cpi r17, 0x01 breq TM1637_blink_pair_first cpi r17, 0x02 breq TM1637_blink_pair_second rjmp TM1637_blink_pair_end TM1637_blink_pair_first: eor TM1637_char1, r18 eor TM1637_char2, r19 rjmp TM1637_blink_pair_end TM1637_blink_pair_second: eor TM1637_char3, r18 eor TM1637_char4, r19 rjmp TM1637_blink_pair_end TM1637_blink_pair_end: rcall TM1637_display pop r19 pop r18 pop r17 ret
Теперь немного о самом проекте. Код переписывался несколько раз (прям очень несколько раз). В начале не хватало памяти, потом добавлялись новые функции и опять не хватало памяти... Проект состоит из 9-ти файлов:
- def_equ.inc - файл описаний. В нём указано какие ножки задействовать для того или иного контроллера, предделители для таймеров, постоянные символы для TM1637 и др.
- main.asm - основной файл
- initialization.asm - файл инициализации микроконтроллера
- ds1302.asm - всё, что связано c DS1302. Листинг выше
- tm1637.asm - всё, что связано c TM1637. Листинг выше
- interrupts_vector.asm - файл вектора прерываний
- interrupts.asm - файл самих прерываний
- alarm.asm - всё, что связано с будильником
- lib.asm - файл, в который вошли остальные функции
Листинг файла def_equ.inc
;------------------------- Предделители для таймеров .equ kdel1 = 487 ; (0,5 сек. для 16-битного таймера на 1МГц) .equ kdel2 = 58593 ; (60 сек. для 16-битного таймера на 1МГц) .equ kdel3 = 233 ; (240 мс для 8-битного таймера на 1МГц) .equ kdel4 = 125 ; (1000 Гц для 8-битного таймера на 1МГц) ;------------------------- Регистры/описания для работы с tm1637 .def TM1637_char1 = r6 .def TM1637_char2 = r23 .def TM1637_char3 = r24 .def TM1637_char4 = r25 .equ PORT_TM1367 = PORTB .equ DDR_TM1367 = DDRB .equ PIN_TM1367 = PINB .equ TM1637_CLK = PB0 .equ TM1637_DATA = PB1 ;------------------------- Постоянные символы для tm1637 .equ char_A = 0b01110111 .equ char_minus = 0b01000000 .equ char_3_dash = 0b01001001 .equ char_N = 0b00110111 .equ char_F = 0b01110001 .equ char_O = 0b00111111 .equ char_0 = char_O .equ char_1 = 0b00000110 .equ char_2 = 0b01011011 .equ char_3 = 0b01001111 .equ char_4 = 0b01100110 .equ char_5 = 0b01101101 .equ char_6 = 0b01111101 .equ char_7 = 0b00000111 .equ char_8 = 0b01111111 .equ char_9 = 0b01101111 ;------------------------- Режимы Mode для tm1637 .equ mode_0 = 0x00 .equ mode_1 = 0x01 .equ mode_2 = 0x02 .equ mode_3 = 0x03 .equ mode_4 = 0x04 .equ mode_5 = 0x05 .equ mode_6 = 0x06 .equ mode_7 = 0x07 .equ mode_8 = 0x08 .equ mode_9 = 0x09 ;------------------------- Регистры/описания для работы с ds1302 .equ PORT_DS1302 = PORTB .equ DDR_DS1302 = DDRB .equ PIN_DS1302 = PINB .equ DS1302_SCLK = PB3 .equ DS1302_IO = PB4 .equ DS1302_CE = PB5 ; RST ;------------------------- Режимы Clock_mode для ds1302 .equ clock_mode_0 = 0x00 .equ clock_mode_1 = 0x01 .equ clock_mode_2 = 0x02 ;------------------------- Регистры/описания для работы с кнопками .equ PORT_BUTTON_MODE= PORTD .equ DDR_BUTTON_MODE = DDRD .equ PIN_BUTTON_MODE = PIND .equ BUTTON_MODE = PD2 .equ PORT_BUTTON_SET = PORTD .equ DDR_BUTTON_SET = DDRD .equ PIN_BUTTON_SET = PIND .equ BUTTON_SET = PD3 ;------------------------- Регистры/описания для зуммера .equ PORT_BUZZER = PORTB .equ DDR_BUZZER = DDRB .equ PIN_BUZZER = PINB .equ BUZZER = PB2
Пытался сделать так, чтобы можно было "безболезненно" поменять порты и ножки для контроллеров и зуммера. Режимы mode и clock_mode - для удобства. Символы для TM1637 для наглядности и для универсальности. Когда в коде встречается несколько раз один символ, здесь его проще менять, да и в коде понятно, что это за символ. Замечу, что я делал код под скорость микроконтроллера в 1 МГц.
Листинг файла main.asm
.include "tn2313adef.inc" .include "def_equ.inc" ;======================================================== ; Глобальные переменные в РОН ;======================================================== .def CONST_ZERO = r0 Постоянный ноль .def timer0_counter = r1 Счетчик для TIM0 при отображении двоеточия .def timer0_counter_alarm_unlock = r2 счетчик для разблокировки будильника .def alarm = r3 Будильник выключен == 0, включён == 1 .def alarm_lock = r4 Блокировка будильника, блокирован == 1, разблокирован == 0. Блокировка будильника на N-ное время после его отключения, чтобы он не зацикливался. .def clock_mode = r20 Хранение состояния текущего отображения (время == 0, минуты и секунды == 1, дата == 2) .def mode = r21 Хранение позиции элемента, который устанавливается (ничего не устанавливается == 0, часы == 1, минуты == 2, число == 3, месяц == 4, год == 5, будильник вкл\выкл == 6, часы будильника == 7, минуты будильника == 8, моргать временем во время сигнала будильника == 9) .def BYTE = r22 Байт передачи\приём данных на\из ds1302 ;======================================================== ; Глобальные переменные в EEPROM ;======================================================== .eseg Выбираем сегмент EEPROM .org 0x00 Устанавливаем текущий адрес сегмента ;------------------------- Хранение данных в упакованном формате для будильника bcd_alarm_hours: .db 0x07 ; по умолчанию поставил на 07:30 bcd_alarm_minutes: .db 0x30 ;======================================================== ; Глобальные переменные в RAM ;======================================================== .dseg Выбираем сегмент RAM .org 0x60 Устанавливаем текущий адрес сегмента ;------------------------- Хранение данных в упакованном формате от DS1302 bcd_seconds: .byte 1 bcd_minutes: .byte 1 bcd_hours: .byte 1 bcd_day: .byte 1 bcd_month: .byte 1 bcd_week_day: .byte 1 bcd_year: .byte 1 ------------------------- Хранение подготовленных данных для отображения на TM1637 поэлементно tm_s1: .byte 1 Десятки секунд tm_s2: .byte 1 Единицы секунд tm_m1: .byte 1 Десятки минут tm_m2: .byte 1 Единицы минут tm_h1: .byte 1 Десятки часов tm_h2: .byte 1 Единицы часы tm_d1: .byte 1 Десятки дня месяца tm_d2: .byte 1 Единицы дня месяца tm_mt1: .byte 1 Десятки месяца tm_mt2: .byte 1 Единицы месяца tm_wd1: .byte 1 Десятки день недели tm_wd2: .byte 1 Единицы день недели tm_y3: .byte 1 Десятки года tm_y4: .byte 1 Единицы года ;-------------------------- Последовательность следующих переменных важна! tm_y1: .byte 1 Тысячи года, тут будет фиксированное значение дальше по коду tm_y2: .byte 1 Сотни года, тут будет фиксированное значение дальше по коду tm_ah1: .byte 1 Десятки часов будильника tm_ah2: .byte 1 Единицы часов будильника tm_am1: .byte 1 Десятки минут будильника tm_am2: .byte 1 Единицы минут будильника ;======================================================== ; Начало программного кода ;======================================================== .cseg Выбор сегмента программного кода .org 0 Установка текущего адреса на ноль start: .include "interrupts_vector.asm" .include "interrupts.asm" .include "initialization.asm" -------------------------- Очистка всех регистров, кроме регистра r0 и Z clr CONST_ZERO ldi ZH, 0 ldi ZL, 2 ldi r17, 29 mov r1, r17 clear_reg: dec r1 breq clear_reg_end st Z+, CONST_ZERO rjmp clear_reg clear_reg_end: -------------------------- Очистка RAM под наши переменные ldi ZL, 0x60 ldi r17, 28 У нас 27 переменных = 28 минус первый декремент в цикле clear_ram: dec r17 breq clear_ram_end st Z+, CONST_ZERO rjmp clear_ram clear_ram_end: -------------------------- Первые две цифры отображения года (тысячи и сотни) для TM1637 ldi ZH, high(tm_y1) ldi ZL, low(tm_y1) ldi r17, char_2 st Z+, r17 ldi r17, char_0 st Z+, r17 -------------------------- Считывание времени будильника из EEPROM для отображения на дисплее TM1637 ldi r17, bcd_alarm_hours rcall EEPROM_read rcall conv_ds1302_to_tm1637 st Z+, r16 st Z+, r15 ldi r17, bcd_alarm_minutes rcall EEPROM_read rcall conv_ds1302_to_tm1637 st Z+, r16 st Z, r15 -------------------------- Яркость на минимум. В Proteus с минимальной яркостью ничего не отображается!!! ldi BYTE, 0 sbr BYTE, 0x88 rcall TM1637_start rcall TM1637_send_byte rcall TM1637_stop -------------------------- Считать данные с DS1302 rcall DS1302_read_package_data инициализация DS1302 - первая команда в DS1302 после запуска МК игнорируется (по крайней мере у меня), без данной строки не сработает следующая команда ;-------------------------- Инициализация DS1302 - разрешение на запись и включение часов! ldi BYTE, 0x8E rcall DS1302_send_start rcall DS1302_send_byte ldi BYTE, 0x00 rcall DS1302_send_byte rcall DS1302_send_stop ldi BYTE, 0x80 rcall DS1302_send_start rcall DS1302_send_byte ldi BYTE, 0x00 rcall DS1302_send_byte rcall DS1302_send_stop sei Разрешение прерываний -------------------------- Основной бесконечный цикл в ожидании прерывания main: sleep rjmp main .include "alarm.asm" .include "ds1302.asm" .include "tm1637.asm" .include "lib.asm" ;-------------------------- Массив максимального числа в месяцах. Убрано в конец кода. max_day_in_month: .db 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31
В начале идёт описание РОН, которые используются как глобальные переменные. Изначально это были переменные RAM, но, когда я стал, в очередной раз, оптимизировать код, то выбрал им место в РОН. Операции с памятью такие как sts, lds используют не одно байт-слово, а два, то есть размер прошивки становиться больше. Теперь по самим переменным:
- BYTE - используется при отправке и приёме данных с контроллеров
- timer0_counter - счетчик для TIM0 при отображении двоеточия. Двоеточие моргает раз в пол секунды. Таймер TIM0 8-ми битный и для него пол секунды это много, по этому я использую счетчик для подсчёта срабатываний прерываний. Само прерывание срабатывает раз в 240 мс. Через два таких прерывания двоеточие устанавливается или убирается.
- timer0_counter_alarm_unlock - счетчик снятия блокировки будильника. Что такое блокировка будильника? Будильник срабатывает, если он установлен, блокировка будильника снята, время совпало. После срабатывания будильника он отключится или вручную, или через некоторое время. Когда будильник отключился, время-то ещё может совпадать! И для того, что бы не было повторного включения есть данный счетчик. После, примерно, 31 секунды блокировка снимается.
- CONST_ZERO - регистр с постоянным нулём. Используется для уменьшения объёма прошивки и самого исходного кода.
- alarm - регистр, указывающий включён будильник или нет
- alarm_lock - регистр, указывающий включена блокировка будильника или нет
- clock_mode - текущий режим clock_mode
- mode - текущий режим mode
Следующие переменные хранятся в памяти EEPROM:
- bcd_alarm_hours - упакованный формат часов будильника
- bcd_alarm_minutes - упакованный формат минут будильника
Место в EPPROM я выбрал уже в конце написания кода. Наверно, больше для разнообразия, чтобы было задействовано как можно больше функций микроконтроллера. Переменные можно спокойно перенести в области RAM памяти.
Переменные RAM памяти:
- В переменных с bcd_seconds по bcd_year хранятся данные в упакованном виде по их именам. bcd_seconds - секунды, bcd_minutes - минуты. Такой уровень английского, думаю, есть у каждого.
- В переменных с tm_s1 по tm_am2 хранятся преобразованные данные для дисплея поразрядно. tm_m1 десятки для минут, tm_m2 - единицы для минут и т.д.
Обращаю внимание, что последовательность переменных с tm_y1 по tm_am2 важна, т.к. далее по коду идет "шагание" по оперативной памяти последовательно по данным переменным. Дальше подключаются необходимые файлы. Потом происходит обнуление всех регистров и оперативной памяти, которая выделена под имеющиеся переменные. Это необходимо для стабильной работы в моменты сброса микроконтроллера. Потом запись значений по умолчанию в переменные, в которых храниться тысячи и сотни для отображения года. Если помните диапазон годов 2000 - 2099. Первые два символа не меняются. Дальше считывание данных с EEPROM. Затем разрешение прерываний и первое считывание данных с DS1302. И, наконец, основной цикл программы с уходом в сон. Сон - для экономии энергии. Весь код выполняется на прерываниях, а между ними можно и "поспать". И в конце файла - массив c максимальным значением числа в каждом месяце, что бы исключить попытки установки недействительных данных.
Листинг файла initialization.asm
;======================================================== ; Модуль инициализации ;======================================================== init: ;-------------------------- Инициализация стека ldi r17, RAMEND ; Выбор адреса вершины стека out SPL, r17 ; Запись его в регистр стека ;--------------------------- Инициализация компаратора ldi r17, 0x80 ; Выключение компаратора out ACSR, r17 ;-------------------------- Инициализация Главного предделителя ldi r17, 0x80 ; out CLKPR, r17 ; ldi r17, 0x03 ; Записываем 3 в регистр r17 out CLKPR, r17 ; Записываем это число в CLKPR, указывая, что мы делим частоту на 8. ;-------------------------- Инициализация портов ВВ по умолчанию (на вход с подтягивающим резистором) ser r17 out PORTA, r17 out PORTB, r17 out PORTD, r17 out DDRA, CONST_ZERO out DDRB, CONST_ZERO out DDRD, CONST_ZERO ;-------------------------- Инициализация порта зуммера cbi PORT_BUZZER, BUZZER sbi DDR_BUZZER, BUZZER ;-------------------------- Инициализация портов ВВ для чипа TM1367 sbi DDR_TM1367, TM1637_CLK sbi DDR_TM1367, TM1637_DATA ;-------------------------- Инициализация портов ВВ для чипа DS1302 sbi DDR_DS1302, DS1302_CE sbi DDR_DS1302, DS1302_SCLK ;------------------------- Вывод DS1302_DAT устанавливается при отправке или чтении ds1302 ;-------------------------- Инициализация портов ВВ для кнопок cbi DDR_BUTTON_MODE, BUTTON_MODE cbi DDR_BUTTON_SET, BUTTON_SET ;-------------------------- Инициализация таймеров ldi r17, (1 << WGM12) | (1 << CS12) | (0 << CS11) | (1 << CS10) ; Выбор режима таймера (СТС, предделитель = 1024) out TCCR1B, r17 rcall change_tim1_to_normal_mode rcall change_tim0_to_normal_mode ;--------------------------- Разрешаем прерывание от таймеров ldi r17, (1 << OCIE1A) | (1 << OCIE0A) out TIMSK, r17 ;--------------------------- Разрешаем прерывание INT0 и INT1 по заднему фронту и режим сна (idle) ldi r17, (1 << ISC01) | (0 << ISC00) | (1 << ISC11) | (0 << ISC10) | (1 << SE) out MCUCR, r17 ldi r17, (1 << INT0) | (1 << INT1) out GIMSK, r17
Всё предельно просто. Поясню только установку портов ВВ по умолчанию. Так как есть "висячие" ножки микроконтроллера я их перевожу в состояние входа с подтягивающим резистором. Так будет правильно с точки зрения экономии энергии. Висячие ножки улавливают всё подряд и тратят на это заряд аккумулятора, а данный режим этому препятствует.
Листинг файлов ds1302.asm и tm1637.asm был выше.
Листинг interrupts_vector.asm:
;======================================================== ; Вектор прерываний ;======================================================== rjmp init ; Переход на начало программы rjmp _INT0 ; Внешнее прерывание 0 rjmp _INT1 ; Внешнее прерывание 1 reti ; Прерывание по захвату таймера T1 rjmp _TIM1 ; Прерывание по совпадению T1 A reti ; Прерывание по переполнению T1 reti ; Прерывание по переполнению T0 reti ; Прерывание UART прием завершен reti ; Прерывание UART регистр данных пуст reti ; Прерывание UART передача завершена reti ; Прерывание по компаратору reti ; Прерывание по изменению на любом контакте reti ; Таймер/счетчик 1. Совпадение B rjmp _TIM0 ; Таймер/счетчик 0. Совпадение A reti ; Таймер/счетчик 0. Совпадение B reti ; USI Стартовая готовность reti ; USI Переполнение reti ; EEPROM Готовность reti ; Переполнение охранного таймера reti ; PCINT1 Handler reti ; PCINT2 Handl
Листинг interrupts.asm:
;======================================================== ; Подпрограмма при нажатии кнопки Mode ;======================================================== _INT0: push r17 push r16 _INT0_wait_release: rcall MCU_wait_20ms sbic PIN_BUTTON_MODE, BUTTON_MODE rjmp Mode_pressed_end rcall DS1302_read_package_data ;-------------------------- Сброс переменной clock_mode clr clock_mode ;-------------------------- Инкремент переменной mode inc mode ;-------------------------- Выбор режима mode cpi mode, mode_1 breq Mode_pressed_1 cpi mode, mode_2 breq Mode_pressed_2 cpi mode, mode_3 breq Mode_pressed_3 cpi mode, mode_4 breq Mode_pressed_4 cpi mode, mode_5 breq Mode_pressed_5 cpi mode, mode_6 breq Mode_pressed_6 cpi mode, mode_7 breq Mode_pressed_7 cpi mode, mode_8 breq Mode_pressed_8 cpi mode, mode_9 brsh Mode_pressed_0 rjmp Mode_pressed_end ;------------------------- MODE 0 Mode_pressed_0: rcall TM1637_display_time ;------------------------- Если день был установлен больше, чем максимальный в этом месяце, то данный код это исправляет. ;------------------------- Пример: выставили 31 число, потом 9 месяц (а в сентябре 30 дней). При переходе в режим ;------------------------- mode == 0 устанавливается 30 число! lds r17, bcd_day rcall bcd8bin mov r16, r17 rcall get_max_day cp r16, r17 brlo Mode_pressed_0_end ldi BYTE, 0x86 rcall DS1302_send_start rcall DS1302_send_byte rcall bin8bcd mov BYTE, r17 rcall DS1302_send_byte rcall DS1302_send_stop Mode_pressed_0_end: rcall change_tim1_to_normal_mode clr mode rjmp Mode_pressed_end ;------------------------- MODE 1 Mode_pressed_1: rcall TM1637_display_time rcall change_tim1_to_blink_mode rjmp Mode_pressed_end ;------------------------- MODE 2 Mode_pressed_2: rcall TM1637_display_time rjmp Mode_pressed_end ;------------------------- MODE 3 или 4 Mode_pressed_3: Mode_pressed_4: rcall TM1637_display_date rjmp Mode_pressed_end ;------------------------- MODE 5 Mode_pressed_5: rcall TM1637_display_year rjmp Mode_pressed_end ;------------------------- MODE 6 Mode_pressed_6: rcall TM1637_display_alarm_mode rjmp Mode_pressed_end ;------------------------- MODE 7 или 8 Mode_pressed_7: Mode_pressed_8: rcall TM1637_display_alarm Mode_pressed_end: pop r16 pop r17 reti ;======================================================== ; Подпрограмма при нажатии кнопки Clock mode \ Set ;======================================================== _INT1: push r17 _INT1_wait_release: rcall MCU_wait_20ms sbic PIN_BUTTON_SET, BUTTON_SET rjmp Clock_mode_end cpi mode, mode_0 breq Clock_mode_clock_mode_pressed ;-------------------------- Режим Set rcall inc_circle rjmp Clock_mode_end ;-------------------------- Режим Clock mode Clock_mode_clock_mode_pressed: inc clock_mode cpi clock_mode, clock_mode_2+1 brsh Clock_mode_reset rjmp Clock_mode_pre_end Clock_mode_reset: clr clock_mode Clock_mode_pre_end: ;-------------------------- Чтобы данные сразу отобразились ldi r17, high(kdel2) out TCNT1H, r17 ldi r17, low(kdel2-10) out TCNT1L, r17 Clock_mode_end: pop r17 reti ;======================================================== ; Прерывание таймера T1 ;======================================================== _TIM1: push r17 push r18 push r19 ;-------------------------- Проверка режимов Mode cpi mode, mode_0 breq rcall_TIM1_mode_0 cpi mode, mode_1 breq rcall_TIM1_mode_1 cpi mode, mode_2 breq rcall_TIM1_mode_2 cpi mode, mode_3 breq rcall_TIM1_mode_3 cpi mode, mode_4 breq rcall_TIM1_mode_4 cpi mode, mode_5 breq rcall_TIM1_mode_5 cpi mode, mode_6 breq rcall_TIM1_mode_6 cpi mode, mode_7 breq rcall_TIM1_mode_7 cpi mode, mode_8 breq rcall_TIM1_mode_8 cpi mode, mode_9 breq rcall_TIM1_mode_9 rjmp _TIM1_end ;-------------------------- MODE 0 rcall_TIM1_mode_0: rcall _TIM1_mode_0 rjmp _TIM1_end ;-------------------------- MODE 1 rcall_TIM1_mode_1: lds r18, tm_h1 lds r19, tm_h2 ldi r17, 0x01 rcall TM1637_blink_pair rjmp _TIM1_end ;-------------------------- MODE 2 rcall_TIM1_mode_2: lds r18, tm_m1 lds r19, tm_m2 ldi r17, 0x02 rcall TM1637_blink_pair rjmp _TIM1_end ;-------------------------- MODE 3 rcall_TIM1_mode_3: lds r18, tm_d1 lds r19, tm_d2 ldi r17, 0x01 rcall TM1637_blink_pair rjmp _TIM1_end ;-------------------------- MODE 4 rcall_TIM1_mode_4: lds r18, tm_mt1 lds r19, tm_mt2 ldi r17, 0x02 rcall TM1637_blink_pair rjmp _TIM1_end ;-------------------------- MODE 5 rcall_TIM1_mode_5: lds r18, tm_y3 lds r19, tm_y4 ldi r17, 0x02 rcall TM1637_blink_pair rjmp _TIM1_end ;-------------------------- MODE 6 rcall_TIM1_mode_6: rcall alarm_blink rjmp _TIM1_end ;-------------------------- MODE 7 rcall_TIM1_mode_7: lds r18, tm_ah1 lds r19, tm_ah2 ldi r17, 0x01 rcall TM1637_blink_pair rjmp _TIM1_end ;-------------------------- MODE 8 rcall_TIM1_mode_8: lds r18, tm_am1 lds r19, tm_am2 ldi r17, 0x02 rcall TM1637_blink_pair rjmp _TIM1_end ;-------------------------- MODE 9 rcall_TIM1_mode_9: clr ZH ldi ZL, low(tm_m1) ld r18, Z+ ld r19, Z+ ldi r17, 0x02 rcall TM1637_blink_pair ld r18, Z+ ld r19, Z ldi r17, 0x01 rcall TM1637_blink_pair rcall change_tim0_to_buzzer_mode _TIM1_end: cpi mode, mode_9 breq _TIM1_end_end rcall alarm_check _TIM1_end_end: pop r19 pop r18 pop r17 reti ;======================================================== ; Прерывание таймера T0 ;======================================================== _TIM0: push r17 push BYTE in r17, OCR0A cpi r17, kdel4 breq _TIM0_end inc timer0_counter_alarm_unlock sbrc timer0_counter_alarm_unlock, 7 ; 128 * 240 мс ~ 31 с clr alarm_lock cpi clock_mode, clock_mode_1 breq _TIM0_display_seconds ;-------------------------- Если сейчас не Mode 0 и не Clock_mode 0 выходим из прерывания push mode add mode, clock_mode pop mode brne _TIM0_end ;-------------------------- Счетчик для двоеточия inc timer0_counter mov r17, timer0_counter cpi r17, 0x04 brne _TIM0_end clr timer0_counter ;-------------------------- Для экономии работы микроконтроллеров. Изменяется только значение 2-го элемента на дисплее, а не всех!!! rcall TM1637_start ldi BYTE, 0x44 rcall TM1637_send_byte rcall TM1637_stop rcall TM1637_start ldi BYTE, 0xC1 rcall TM1637_send_byte lds BYTE, tm_h2 ldi r17, 0x80 eor BYTE, r17 sts tm_h2, BYTE rcall TM1637_send_byte rcall TM1637_stop _TIM0_end: pop BYTE pop r17 reti _TIM0_display_seconds: rcall DS1302_read_package_data rcall TM1637_display_seconds rjmp _TIM0_end ;======================================================== ; Подпрограмма выбора режима mode_0 ;======================================================== _TIM1_mode_0: ;-------------------------- Считать данные с ds1302 пакетом rcall DS1302_read_package_data cpi clock_mode, clock_mode_1 breq _TIM1_mode_0_end cpi clock_mode, clock_mode_2 breq _TIM1_date_mode _TIM1_time_mode: rcall TM1637_display_time ret _TIM1_date_mode: rcall TM1637_display_date ret _TIM1_mode_0_end: ret
Это самый большой файл! Но описать его не так уж долго. Прерывания INT0 и INT1 в одноимённых процедурах. При работе с кнопками используются задержки для борьбы с дребезгом контактов. Для кнопок, которые я использую - 20 мкс. вполне хватает. Прерывание TIM1 раз в минуту (для экономии аккумулятора) считывает данные с DS1302 и выводит их на дисплей, но это только в режиме mode_0. В других режимах данное прерывание отвечает за частоту моргания элементов на дисплее, которые устанавливаются или во время будильника. Прерывание TIM0 отвечает за мигание двоеточием, за сброс регистра блокировки будильника и за "подачу звука" на ногу OC0A во время срабатывания будильника.
Листинг файла alarm.asm:
;======================================================== ; Подпрограммы для моргания 3-м и 4-м элементами ; в режиме включения\выключения будильника ;======================================================== alarm_blink: push r17 push r18 push r19 sbrc alarm, 0 ldi r19, char_N sbrs alarm, 0 ldi r19, char_F ldi r18, char_O ldi r17, 0x02 rcall TM1637_blink_pair pop r19 pop r18 pop r17 ret ;======================================================== ; Инкремент для будильника ;======================================================== inc_cicle_alarm: push r17 push r18 push r19 push BYTE mov r19, CONST_ZERO ; минимальное число для инкремента во всех ниже случаях cpi mode, mode_6 breq inc_circle_alarm_switch cpi mode, mode_7 breq inc_circle_alarm_hour cpi mode, mode_8 breq inc_circle_alarm_minutes rjmp inc_cicle_alarm_end ;-------------------------- Вкл.\выкл. будильника inc_circle_alarm_switch: mov r17, alarm ldi r18, 2 rcall _inc mov alarm, r17 rcall TM1637_display_alarm_mode clr alarm_lock ; снять блокировку будильника rjmp inc_cicle_alarm_end ;-------------------------- Выставление часов будильника inc_circle_alarm_hour: ldi r17, bcd_alarm_hours rcall EEPROM_read rcall bcd8bin ldi r18, 24 rcall _inc rcall bin8bcd ldi r18, bcd_alarm_hours rcall EEPROM_write rcall conv_ds1302_to_tm1637 sts tm_ah1, r16 sts tm_ah2, r15 rcall TM1637_display_alarm rjmp inc_cicle_alarm_end ;-------------------------- Выставление минут будильника inc_circle_alarm_minutes: ldi r17, bcd_alarm_minutes rcall EEPROM_read rcall bcd8bin ldi r18, 60 rcall _inc rcall bin8bcd ldi r18, bcd_alarm_minutes rcall EEPROM_write rcall conv_ds1302_to_tm1637 sts tm_am1, r16 sts tm_am2, r15 rcall TM1637_display_alarm inc_cicle_alarm_end: pop BYTE pop r19 pop r18 pop r17 ret ;======================================================== ; Включить будильник ;======================================================== alarm_on: push r18 push r17 push r16 out GIMSK, CONST_ZERO ; отключить прерывания от кнопок rcall TM1637_display_time ldi mode, mode_9 ; mode_9 включается только во время срабатывания будильника rcall change_tim1_to_blink_mode rcall change_tim0_to_buzzer_mode sei ; т.к. процедура "alarm_on" вызывается из прерывания TIM1, то до этого момента прерывания отключены ;-------------------------- ~ 1 мин. 10 сек. Данный цикл + его команды + прерывание на таймер с его командами. Нужно, чтобы будильник сам выключился через время, а не звонил вечно, до самого обеда! ldi r17, 5 ser r16 alarm_on_wait_loop_L: rcall MCU_wait_20ms in r18, PIND andi r18, 0x0C cpi r18, 0x0C brlo alarm_off dec r16 brne alarm_on_wait_loop_L alarm_on_wait_loop_H: ser r16 dec r17 brne alarm_on_wait_loop_L ;-------------------------- Отключение будильника через время или по нажатию любой кнопки alarm_off: cli clr mode clr clock_mode rcall change_tim1_to_normal_mode rcall change_tim0_to_normal_mode rcall TM1637_display_time clr timer0_counter_alarm_unlock ldi r16, 0x01 ; Блокировка будильника mov alarm_lock, r16 ;-------------------------- Разрешить прерывания от кнопок ldi r17, (1 << INT0) | (1 << INT1) out GIMSK, r17 sei pop r16 pop r17 pop r18 ret ;======================================================== ; Подпрограмма проверки будильника ;======================================================== alarm_check: push r18 push r17 sbrs alarm, 0 rjmp to_alarm_end ; Если будильник отключён, то выходим из процедуры sbrc alarm_lock, 0 rjmp to_alarm_end ; Если будильник блокирован, то выходим из процедуры ldi r17, bcd_alarm_hours ; Адрес переменной "bcd_alarm_hours" из EEPROM в регистр r17 rcall EEPROM_read ; Результат в регистр r17 lds r18, bcd_hours ; Текущие часы в регистр r18 cp r17, r18 ; Сравниваем текущие часы с часами будильника brne to_alarm_end ; Если текущие часы не совпадают с часами будильника - выходим из процедуры ldi r17, bcd_alarm_minutes ; Адрес переменной "bcd_alarm_minutes" из EEPROM в регистр r17 rcall EEPROM_read ; Результат в регистр r17 lds r18, bcd_minutes ; Текущие минуты в регистр r18 cpse r17, r18 ; Сравниваем текущие минуты с минутами будильника rjmp to_alarm_end ; Если текущие минуты не совпадают с минутами будильника - выходим из процедуры rcall alarm_on ; Если всё совпало - включить будильник to_alarm_end: pop r17 pop r18 ret
Здесь всего 4-и процедуро-функции. Проверка будильника - звонить сейчас или нет. Включение и там же отключение будильника. Моргание элементов в режиме установки будильника (вкл.\ выкл. ). И функция для инкремента данных будильника. Она очень похожа на такую же функцию в файле lib.asm за исключением того, что данные будильника не передаются на контроллер DS1302.
Листинг файла lib.asm:
;======================================================== ; Преобразование данных с ds1302 в числа для 4-х сегментного дисплея tm1637 ; запись результата в регистры r16:r15 вход - регистр r17 ; выход - r16:r15 ;======================================================== conv_ds1302_to_tm1637: push r17 ------------------------- преобразуем младший разряд в регистр r15 mov r18, r17 andi r18, 0x0f rcall bin_to_tm1637_digit mov r15, r18 ------------------------- преобразуем старший разряд в регистр r16 mov r18, r17 andi r18, 0xf0 swap r18 rcall bin_to_tm1637_digit mov r16, r18 pop r17 ret ;------------------------- Подфункция преобразования. ;------------------------- Данные из регистра r18 преобразуются в числовое ;------------------------- значение для 7-сегментного индикатора, результат в ;------------------------- регистр r18 bin_to_tm1637_digit: cpi r18, 0x00 breq _d0 cpi r18, 0x01 breq _d1 cpi r18, 0x02 breq _d2 cpi r18, 0x03 breq _d3 cpi r18, 0x04 breq _d4 cpi r18, 0x05 breq _d5 cpi r18, 0x06 breq _d6 cpi r18, 0x07 breq _d7 cpi r18, 0x08 breq _d8 cpi r18, 0x09 breq _d9 rjmp _dx ------------------------- Подпрограммы для чисел 0..9 _d0: ldi r18, char_0 rjmp bin_to_tm1637_digit_end _d1: ldi r18, char_1 rjmp bin_to_tm1637_digit_end _d2: ldi r18, char_2 rjmp bin_to_tm1637_digit_end _d3: ldi r18, char_3 rjmp bin_to_tm1637_digit_end _d4: ldi r18, char_4 rjmp bin_to_tm1637_digit_end _d5: ldi r18, char_5 rjmp bin_to_tm1637_digit_end _d6: ldi r18, char_6 rjmp bin_to_tm1637_digit_end _d7: ldi r18, char_7 rjmp bin_to_tm1637_digit_end _d8: ldi r18, char_8 rjmp bin_to_tm1637_digit_end _d9: ldi r18, char_9 rjmp bin_to_tm1637_digit_end _dx: ldi r18, char_3_dash bin_to_tm1637_digit_end: ret ;======================================================== ; Подпрограммы инкремента с установленными границами ; входящее\исходящее значение == r17, r19 == min, r18 == max + 1 ;======================================================== _inc: inc r17 cp r17, r18 brsh _inc_reset rjmp _inc_end _inc_reset: mov r17, r19 _inc_end: ret ;======================================================== ; Подпрограммы инкремента с установленными границами ; и вызовом подпрограммы ;======================================================== inc_circle: push r18 push r19 push XL push XH push ZL push ZH mov r19, CONST_ZERO -------------------------- Выбор режима mode cpi mode, mode_1 breq inc_circle_hour cpi mode, mode_2 breq inc_circle_minutes cpi mode, mode_3 breq inc_circle_day cpi mode, mode_4 breq inc_circle_month cpi mode, mode_5 breq inc_circle_year cpi mode, mode_6 breq rcall_inc_cicle_alarm cpi mode, mode_7 breq rcall_inc_cicle_alarm cpi mode, mode_8 breq rcall_inc_cicle_alarm rjmp inc_circle_end -------------------------- Часы inc_circle_hour: ldi XH, high(bcd_hours) ldi XL, low(bcd_hours) ldi r18, 24 ldi BYTE, 0x84 ldi ZH, high(TM1637_display_time) ldi ZL, low(TM1637_display_time) rcall inc_circle_ext rjmp inc_circle_end -------------------------- Минуты inc_circle_minutes: ldi XH, high(bcd_minutes) ldi XL, low(bcd_minutes) ldi r18, 60 ldi BYTE, 0x82 ldi ZH, high(TM1637_display_time) ldi ZL, low(TM1637_display_time) rcall inc_circle_ext rjmp inc_circle_end -------------------------- День месяца inc_circle_day: ldi XH, high(bcd_day) ldi XL, low(bcd_day) ldi r19, 1 rcall get_max_day inc r17 mov r18, r17 ldi BYTE, 0x86 ldi ZH, high(TM1637_display_date) ldi ZL, low(TM1637_display_date) rcall inc_circle_ext rjmp inc_circle_end -------------------------- Месяц inc_circle_month: ldi XH, high(bcd_month) ldi XL, low(bcd_month) ldi r19, 1 ldi r18, 13 ldi BYTE, 0x88 ldi ZH, high(TM1637_display_date) ldi ZL, low(TM1637_display_date) rcall inc_circle_ext rjmp inc_circle_end -------------------------- Год inc_circle_year: ldi XH, high(bcd_year) ldi XL, low(bcd_year) ldi r18, 100 ldi BYTE, 0x8C ldi ZH, high(TM1637_display_year) ldi ZL, low(TM1637_display_year) rcall inc_circle_ext rjmp inc_circle_end -------------------------- Будильник rcall_inc_cicle_alarm: rcall inc_cicle_alarm -------------------------- Конец инкрементации inc_circle_end: pop ZH pop ZL pop XH pop XL pop r19 pop r18 ret ;======================================================== ; Преобразование 8-битного двоичного ; значения в упакованный BCD формат ; Входящее\исходящее значение == r17 ;======================================================== bin8bcd: push r18 push r16 .def digitL = r18 .def digitH = r16 mov digitL, r17 clr digitH bin8bcd_loop: subi digitL, 0x0a brmi bin8bcd_end inc digitH rjmp bin8bcd_loop bin8bcd_end: subi digitL, -0x0a swap digitH mov r17, digitH andi digitL, 0x0f or r17, digitL .undef digitL .undef digitH pop r19 pop r16 ret ;======================================================== ; Преобразование 8-битного упакованного ; значения в двоичный формат ; Входящее\исходящее значение == r17 ;======================================================== bcd8bin: push r18 .def result = r17 .def tens = r18 mov tens, r17 andi tens, 0xf0 swap tens andi result, 0x0f bcd8bind_loop: dec tens brmi bcd8bin_end subi result, -0x0a rjmp bcd8bind_loop bcd8bin_end: .undef result .undef tens pop r18 ret ;======================================================== ; Инкремент переменной по адресу X. ; Верхняя граница - max_digit (r18). ; Нижняя граница - min_digit (r19). ; Адрес регистра для DS1302 в регистре BYTE. ; Непрямой вызов подпрограммы по адресу Z ;======================================================== inc_circle_ext: push r17 ld r17, X rcall bcd8bin inc r17 cp r17, r18 brsh inc_circle_ext_reset rjmp inc_circle_ext_end inc_circle_ext_reset: mov r17, r19 inc_circle_ext_end: rcall DS1302_send_start rcall DS1302_send_byte rcall bin8bcd mov BYTE, r17 rcall DS1302_send_byte rcall DS1302_send_stop rcall DS1302_read_package_data icall pop r17 ret ;======================================================== ; Нахождение максимального дня текущего месяца ; с учётом високосных лет ; Исходящее значение == r17 ;======================================================== get_max_day: push ZH push ZL push YH push YL ldi ZH, high(max_day_in_month*2) ldi ZL, low(max_day_in_month*2) lds r17, bcd_month rcall bcd8bin dec r17 ldi YH, 0 mov YL, r17 add ZL, YL adc ZH, YH lpm r17, Z rcall leap_year pop YL pop YH pop ZL pop ZH ret ;======================================================== ; Определение високосного года и перезапись Если год високосный и сейчас 2-й месяц, ; меняется значение r17 = 29 ;======================================================== leap_year: push r16 ------------------------- Сохраняем значение r17 в r16, чтобы его не изменить в случае, ------------------------- если сейчас не февраль високосного года mov r16, r17 ------------------------- Если установлен не февраль, проверка на високосный год не нужна lds r17, bcd_month cpi r17, 0x02 brne leap_year_end ------------------------- Каждый сотый год не високосный. В нашем случая, только нулевой год lds r17, bcd_year cpi r17, 0x00 breq leap_year_end ------------------------- Если один из двух младших битов или оба установлены ------------------------- число не кратно 4-м. Год високосный rcall bcd8bin sbrc r17, 1 rjmp leap_year_end sbrc r17, 0 rjmp leap_year_end ------------------------- Максимальное значение числа месяца = 29 ldi r16, 29 leap_year_end: mov r17, r16 pop r16 ret ;======================================================== ; Подпрограммы формирования задержки ;======================================================== MCU_wait_10mks: 10 мкс + время на команды push r17 ldi r17, 10 MCU_wait_loop: dec r17 brne MCU_wait_loop pop r17 ret MCU_wait_20ms: приблизительно 20 мс + время команд push r17 push r16 ldi r17, 80 ser r16 MCU_wait_loop_L: dec r16 brne MCU_wait_loop_L MCU_wait_loop_H: ser r16 dec r17 brne MCU_wait_loop_L pop r16 pop r17 ret ;======================================================== ; Замена предделителя в TIM1 ;======================================================== change_tim1_to_blink_mode: push r17 ldi r17, high(kdel1) меняем предделитель на 0,5 сек., чтобы моргало то, что мы меняем в режимах mode 1 - mode 8 или во время работы будильника out OCR1AH, r17 ldi r17, low(kdel1) out OCR1AL, r17 ldi r17, high(kdel1) чтобы сразу отобразилось out TCNT1H, r17 ldi r17, low(kdel1-10) out TCNT1L, r17 pop r17 ret ;======================================================== ; Замена предделителя в TIM1 ;======================================================== change_tim1_to_normal_mode: push r17 ldi r17, high(kdel2) 60 сек. out OCR1AH, r17 ldi r17, low(kdel2) out OCR1AL, r17 ldi r17, high(kdel2) чтобы сразу отобразилось out TCNT1H, r17 ldi r17, low(kdel2-10) out TCNT1L, r17 pop r17 ret ;======================================================== ; Смена режима TIM0 ;======================================================== change_tim0_to_normal_mode: push r17 ldi r17, (1 << WGM01) Выбор режима таймера СТС out TCCR0A, r17 ldi r17, (1 << CS02) | (0 << CS01) | (1 << CS00) ; Выбор предделителя = 1024 out TCCR0B, r17 ldi r17, kdel3 out OCR0A, r17 pop r17 ret ;======================================================== ; Смена режима TIM0 ;======================================================== change_tim0_to_buzzer_mode: push r17 ldi r17, (1 << WGM01) | (0 << COM0A1) | (1 << COM0A0) out TCCR0A, r17 ldi r17, (0 << CS02) | (1 << CS01) | (0 << CS00) out TCCR0B, r17 ldi r17, kdel4 out OCR0A, r17 pop r17 ret ;======================================================== ; Запись в EEPROM память ; адрес - r18 ; данные для записи - r17 ;======================================================== EEPROM_write: push r17 push r18 sbic EECR, EEPE rjmp EEPROM_write out EECR, CONST_ZERO out EEARL, r18 out EEDR, r17 sbi EECR, EEMPE sbi EECR, EEPE pop r18 pop r17 ret ;======================================================== ; Чтение из EEPROM памяти ; адрес - r17 ; возвращаемые данные - r17 ;======================================================== EEPROM_read: sbic EECR, EEPE rjmp EEPROM_read out EEARL, r17 sbi EECR, EERE in r17, EEDR ret
В данном файле описаны функции которые не вошли в другие файлы. Здесь такие функции как преобразование бинарного числа в упакованный вид и обратно, преобразование данных для дисплея TM1637, инкремент числа с заданными границами, определение максимального числа месяца и високосность года, процедуры задержки МК, процедуры смены режимов таймеров, и функции чтения и записи памяти EEPROM.
Код можно уменьшить за счёт удаления многих команд push и pop, но если потом что-то изменить, то можно долго искать логическую ошибку в программе. По этому, в каждой функции я помещал значение регистров в стек и возвращал их при выходе. Регистров - только, которые изменяются в данной функции и не являются регистрами возвращаемого значения. Если посмотреть файл main.asm, то видно что в коде используются глобальные переменные, хранящиеся в регистрах и их можно переписать в любой функции! Я неоднократно уменьшал код из-за большого размера прошивки и многие места являются "нечеловекочетаемыми", если следовать логики ООП. Но какое тут ООП - это же ассемблер!
Fuse биты:
3D модель платы в программе KiCad:
Замеры потребления тока:
В режиме отображения время потребление тока менее 2 мА. Аккумулятор я выбрал типа 18650. В моей модели аккумулятора заявлено 19800 мАч. 19800 / 2 / 24 / 30 = 13,75 месяцев!!! Но это в теории. И я не думаю, что в китайских аккумуляторах с алиэкспресса будет столько мАч. На практике ещё посмотрим...
P.S.
Кварц на разработанной плате так и не удалось запустить, по этому разработка корпуса приостановлена.
Обновлена прошивка. Сделан упор на работу от сети, так что экономии питания больше нет. Добавлены и исправлены следующие позиции:
- Проект разбит на большее количество файлов для более удобной работы с кодом;
- Добавлен режим регулировки яркости дисплея;
- Настройки будильника (состояние вкл./выкл., часы, минуты) и уровень яркости теперь хранятся в памяти EEPROM;
- При любом "моргании" (любом режиме кроме отображения текущего времени, а так же и в режиме будильника) часы пробудут ~60 сек. Дальше идёт возврат в режим отображения текущего времени;
- При включении идет режим теста устройства - небольшая "анимация" и звук будильника;
- Не сбрасываются секунды при включении устройства.
Старые исходники не менялись для тех, кому необходима автономная работа устройства. Ссылка на новые исходники.
Список радиоэлементов
Обозначение | Тип | Номинал | Количество | Примечание | Магазин | Мой блокнот |
---|---|---|---|---|---|---|
U2 | МК AVR 8-бит | ATtiny2313A | 1 | Поиск в магазине Отрон | ||
U1, U3, U5 | TM1637 | модуль со светодиодным дисплеем | 1 | Поиск в магазине Отрон | ||
U3 | Часы реального времени (RTC) | DS1302 | 1 | Поиск в магазине Отрон | ||
Q1 | Биполярный транзистор | S9014-B | 1 | Поиск в магазине Отрон | ||
X1 | кварцевый резонатор | 32.768 кГЦ | 1 | Поиск в магазине Отрон | ||
BUZ1 | пьезоэлектрический звуковой излучатель | 1 | Поиск в магазине Отрон | |||
RV1 | Подстроечный резистор | 2 кОм | 1 | Поиск в магазине Отрон | ||
R0, R1 | Резистор | 10 кОм | 2 | Поиск в магазине Отрон | ||
MODE, CLOCK MODE / SET | Кнопка | Кнопка без фиксации | 2 | Поиск в магазине Отрон | ||
Аккумулятор 18650 | 3.7В 19800мАч | 1 | Поиск в магазине Отрон | |||
Скачать список элементов (PDF)
Прикрепленные файлы:
- Atmel_studio.zip (77 Кб)
- datasheets.zip (5825 Кб)
- Proteus.zip (120 Кб)
Комментарии (30) | Я собрал (0) | Подписаться
Для добавления Вашей сборки необходима регистрация
[Автор]
Поэтому время работы будет значительно меньше.
Для возможности горячей замены аккумулятора на заряженный, я бы предусмотрел конденсатор на пару-тройку тысяч микрофарад, ну либо прямо в устройстве их заряжать. МК и модуль 5 вольт должны выдерживать. И лучше поставить для этого нечто типа модуля на 4056.
[Автор]
[Автор]
[Автор]
Маленькие замечания по схемотехнике.
* Всё же просится конденсатор по питанию, хотя бы небольшая керамика.
* Так подключать нагрузку к NPN транзистору не желательно. Излучатель с резистором перебросьте на коллектор.
* Про внешние подтяжки для кнопок промолчу, видимо антидребезг вы реализуете программно. Но внутренние не всегда помогают.
[Автор]
[Автор]
[Автор]
[Автор]
[Автор]
[Автор]
[Автор]
[Автор]
[Автор]
[Автор]
[Автор]