Часы ATtiny 2313, DS1302, TM1637 на ассемблере

Вступление

Решив увеличить свои знания не в «ширину», а в «глубину» получился этот проект. Захотелось реализовать на практики полученные знания по ассемблеру на чипах AVR семейства tiny. В наличии был модуль часов реального времени ds1302, микроконтроллер ATtiny2313A и модуль на контроллере TM1637 с четырьмя элементами для отображения с точками и двоеточием. А готовых и понятных примеров на ассемблере в русскоязычном интернете очень и очень мало!

Задача: собрать устройство «Цифровые часы», которое, как вы могли догадаться, будет отображать текущее время и дату, и иметь будильник. Управляться (установка данных) с помощью силы мысли или кнопок. Иметь как можно малый малый размер и автономное питание. И цена не должна сильно превышать имеющиеся аналоги, а лучше быть дешевле!

Имеющиеся устройства

Микроконтроллер ATTiny2313a (ссылка)

Модуль часов реального

времени DS1302 (ссылка)

Модуль 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:

ds1302

Ножки X1 и X2 — подключение кварцевого резонатора на 32,768 кГц. VCC1 — подключение батарейки, VCC2 — основное питание. Ножка RST отвечает за «включениевыключение» контроллера. Когда на ней логическая единица, микроконтроллер принимает и передаёт данные, когда ноль — нет. Ножка SCLK это линия тактирования. Ножка I/O — приём и передача данных. Обращу ваше внимание, что данные передаются младшим битом вперёд, а так же в упакованном формате. По спецификации DS1302 использует  VCC2, если его напряжение больше, чем на VCC1 + 0,2 В. Я выбрал источником питания только VCC2, т.к. устройство и так работает от аккумуляторов. Далее рассмотрим алгоритм работы контроллера на примерах.

Чтение минут Запись часов
Считали 08 минут. Записали 18 часов.

 Алгоритм чтения данных:

  1. Устанавливаем ножку RST (включаем контроллер)
  2. Передаём первый байт — байт адреса регистра. В нашем случае — это 0x83.
  3. Считываем байт данных. На картинке — это 0x08
  4. Сбрасываем ножку RST (выключаем контроллер)

 Алгоритм записи данных:

  1. Устанавливаем ножку RST (включаем контроллер)
  2. Передаём первый байт — адрес регистра. В нашем случае — это 0x84.
  3. Передаём второй данных. На картинке — это 0x18
  4. Сбрасываем ножку 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
C0H
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       Включить дисплей

Разберём пример:

Байт команды данных Байт команды адреса Байт данных

 Алгоритм записи данных (установка значения одного элемента):

  1. Сигнал START
  2. Передаём байт команды данных. В нашем примере — это 0x44.
  3. Ожидание бита ответа
  4. Сигнал STOP
  5. Сигнал START
  6. Передаём байт адреса регистра. В нашем случае — это второй элемент на дисплее, его адрес — 0xC1
  7. Ожидание бита ответа
  8. Передаём байт данных
  9. Ожидание бита ответа
  10. Сигнал 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 используют не одно байт-слово, а два, то есть размер прошивки становиться больше. Теперь по самим переменным:

  1. BYTE — используется при отправке и приёме данных с контроллеров
  2. timer0_counter — счетчик для TIM0 при отображении двоеточия. Двоеточие моргает раз в пол секунды. Таймер TIM0 8-ми битный и для него пол секунды это много, по этому я использую счетчик для подсчёта срабатываний прерываний. Само прерывание срабатывает раз в  240 мс. Через два таких прерывания двоеточие устанавливается или убирается. 
  3. timer0_counter_alarm_unlock — счетчик снятия блокировки будильника. Что такое блокировка будильника? Будильник срабатывает, если он установлен, блокировка будильника снята, время совпало. После срабатывания будильника он отключится или вручную, или через некоторое время. Когда будильник отключился, время-то ещё может совпадать! И для того, что бы не было повторного включения есть данный счетчик. После, примерно, 31 секунды блокировка снимается.
  4. CONST_ZERO — регистр с постоянным нулём. Используется для уменьшения объёма прошивки и самого исходного кода.
  5. alarm — регистр, указывающий включён будильник или нет
  6. alarm_lock — регистр, указывающий включена блокировка будильника или нет
  7. clock_mode — текущий режим clock_mode
  8. mode — текущий режим mode

Следующие переменные хранятся в памяти EEPROM:

  1. bcd_alarm_hours — упакованный формат часов будильника
  2. bcd_alarm_minutes — упакованный формат минут будильника

Место в EPPROM я выбрал уже в конце написания кода. Наверно, больше для разнообразия, чтобы было задействовано как можно больше функций микроконтроллера. Переменные можно спокойно перенести в области RAM памяти.

Переменные RAM памяти:

  1. В переменных с bcd_seconds по bcd_year хранятся данные в упакованном виде по их именам. bcd_seconds — секунды, bcd_minutes — минуты. Такой уровень английского, думаю, есть у каждого.
  2. В переменных с 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