Авр программа – Adblock для Яндекс Браузера — Как убрать рекламу в Яндекс Браузере

Adblock для Яндекс Браузера — Как убрать рекламу в Яндекс Браузере

Меню

Меню

Главная Продукты Все продукты AdGuard для Windows AdGuard для Mac AdGuard для Android AdGuard для iOS Браузерное расширение AdGuard Другое AdGuard Home AdGuard DNS AdGuard для iOS Pro AdGuard Content Blocker Блог FAQ Обсудить Купить Купить Личный кабинет Поддержка RU Dansk Deutsch Dutch English Español Français Hrvatski Italiano Norsk Polski Português (BR) Português (PT) Slovenčina Slovenščina Srpski Svenska Türkçe Český Русский Українська فارسی 中文 (简体) 中文 (繁體) 日本語 한국어

AVR. Учебный курс. Скелет программы

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

Вообще, архитектура программ это отдельная тема и ближе к концу курса, в его Сишной части я подробней рассказываю о разных типах организации прошивки. Можешь забежать вперед и поглядеть, что да как.

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

Суперцикл
Все программы на микроконтроллерах обычно зацикленные. Т.е. у нас есть какой то главный цикл, который вращается непрерывно.

Структура же программы при этом следующая:

  • Макросы и макроопредения
  • Сегмент ОЗУ
  • Точка входа — ORG 0000
  • Таблица векторов — и вектора, ведущие в секцию обработчиков прерываний
  • Обработчики прерываний — тела обработчиков, возврат отсюда только по RETI
  • Инициализация памяти — а вот уже отсюда начинается активная часть программы
  • Инициализация стека
  • Инициализация внутренней периферии — программирование и запуск в работу всяких таймеров, интерфейсов, выставление портов ввода-вывода в нужные уровни. Разрешение прерываний.
  • Инициализация внешней периферии — инициализация дисплеев, внешней памяти, разных аппаратных примочек, что подключены к микроконтроллеру извне.
  • Запуск фоновых процессов — процессы работающие непрерывно, вне зависимости от условий. Такие как сканирование клавиатуры, обновление экрана и так далее.
  • Главный цикл — тут уже идет вся управляющая логика программы.
  • Сегмент ЕЕПРОМ


Начинается все с макросов, их пока не много, если что по ходу добавим.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
	.include "m16def.inc"   ; Используем ATMega16
 
;= Start macro.inc ========================================
   	.macro    OUTI          	
      	LDI    R16,@1
   	.if @0 < 0x40
      	OUT    @0,R16       
   	.else
      	STS      @0,R16
   	.endif
   	.endm
 
   	.macro    UOUT        
   	.if	@0 < 0x40
      	OUT	@0,@1         
	.else
      	STS	@0,@1
   	.endif
   	.endm
;= End 	macro.inc =======================================

.include «m16def.inc» ; Используем ATMega16 ;= Start macro.inc ======================================== .macro OUTI LDI R16,@1 .if @0 < 0x40 OUT @0,R16 .else STS @0,R16 .endif .endm .macro UOUT .if @0 < 0x40 OUT @0,@1 .else STS @0,@1 .endif .endm ;= End macro.inc =======================================

В оперативке пока ничего не размечаем. Нечего.

1
2
3
; RAM ===================================================
		.DSEG
; END RAM ===============================================

; RAM =================================================== .DSEG ; END RAM ===============================================

С точкой входа и таблицей векторов все понятно, следуя нашему давнему шаблону, берем его оттуда:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
; FLASH ======================================================
         .CSEG
         .ORG $000      ; (RESET) 
         RJMP   Reset
         .ORG $002
         RETI             ; (INT0) External Interrupt Request 0
         .ORG $004
         RETI             ; (INT1) External Interrupt Request 1
         .ORG $006
         RETI	      ; (TIMER2 COMP) Timer/Counter2 Compare Match
         .ORG $008
         RETI             ; (TIMER2 OVF) Timer/Counter2 Overflow
         .ORG $00A
         RETI	     ; (TIMER1 CAPT) Timer/Counter1 Capture Event
         .ORG $00C 
         RETI             ; (TIMER1 COMPA) Timer/Counter1 Compare Match A
         .ORG $00E
         RETI             ; (TIMER1 COMPB) Timer/Counter1 Compare Match B
         .ORG $010
         RETI             ; (TIMER1 OVF) Timer/Counter1 Overflow
         .ORG $012
         RETI             ; (TIMER0 OVF) Timer/Counter0 Overflow
         .ORG $014
         RETI             ; (SPI,STC) Serial Transfer Complete
         .ORG $016
         RETI    	     ; (USART,RXC) USART, Rx Complete
         .ORG $018
         RETI             ; (USART,UDRE) USART Data Register Empty
         .ORG $01A
         RETI             ; (USART,TXC) USART, Tx Complete
         .ORG $01C
         RETI	     ; (ADC) ADC Conversion Complete
         .ORG $01E
         RETI             ; (EE_RDY) EEPROM Ready
         .ORG $020
         RETI             ; (ANA_COMP) Analog Comparator
         .ORG $022
         RETI             ; (TWI) 2-wire Serial Interface
         .ORG $024
         RETI             ; (INT2) External Interrupt Request 2
         .ORG $026
         RETI             ; (TIMER0 COMP) Timer/Counter0 Compare Match
         .ORG $028
         RETI             ; (SPM_RDY) Store Program Memory Ready
 
	 .ORG   INT_VECTORS_SIZE      	; Конец таблицы прерываний

; FLASH ====================================================== .CSEG .ORG $000 ; (RESET) RJMP Reset .ORG $002 RETI ; (INT0) External Interrupt Request 0 .ORG $004 RETI ; (INT1) External Interrupt Request 1 .ORG $006 RETI ; (TIMER2 COMP) Timer/Counter2 Compare Match .ORG $008 RETI ; (TIMER2 OVF) Timer/Counter2 Overflow .ORG $00A RETI ; (TIMER1 CAPT) Timer/Counter1 Capture Event .ORG $00C RETI ; (TIMER1 COMPA) Timer/Counter1 Compare Match A .ORG $00E RETI ; (TIMER1 COMPB) Timer/Counter1 Compare Match B .ORG $010 RETI ; (TIMER1 OVF) Timer/Counter1 Overflow .ORG $012 RETI ; (TIMER0 OVF) Timer/Counter0 Overflow .ORG $014 RETI ; (SPI,STC) Serial Transfer Complete .ORG $016 RETI ; (USART,RXC) USART, Rx Complete .ORG $018 RETI ; (USART,UDRE) USART Data Register Empty .ORG $01A RETI ; (USART,TXC) USART, Tx Complete .ORG $01C RETI ; (ADC) ADC Conversion Complete .ORG $01E RETI ; (EE_RDY) EEPROM Ready .ORG $020 RETI ; (ANA_COMP) Analog Comparator .ORG $022 RETI ; (TWI) 2-wire Serial Interface .ORG $024 RETI ; (INT2) External Interrupt Request 2 .ORG $026 RETI ; (TIMER0 COMP) Timer/Counter0 Compare Match .ORG $028 RETI ; (SPM_RDY) Store Program Memory Ready .ORG INT_VECTORS_SIZE ; Конец таблицы прерываний

Обработчики пока тоже пусты, но потом добавим

1
2
; Interrupts ==============================================
; End Interrupts ==========================================

; Interrupts ============================================== ; End Interrupts ==========================================

Инициализация ядра. Память, стек, регистры:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
Reset:   	LDI R16,Low(RAMEND)		; Инициализация стека
	  	OUT SPL,R16			; Обязательно!!!
 
	  	LDI R16,High(RAMEND)
	  	OUT SPH,R16
 
; Start coreinit.inc
RAM_Flush:	LDI	ZL,Low(SRAM_START)	; Адрес начала ОЗУ в индекс
		LDI	ZH,High(SRAM_START)
		CLR	R16			; Очищаем R16
Flush:		ST 	Z+,R16			; Сохраняем 0 в ячейку памяти
		CPI	ZH,High(RAMEND)		; Достигли конца оперативки?
		BRNE	Flush			; Нет? Крутимся дальше!
 
		CPI	ZL,Low(RAMEND)		; А младший байт достиг конца?
		BRNE	Flush
 
		CLR	ZL			; Очищаем индекс
		CLR	ZH
		CLR	R0
		CLR	R1
		CLR	R2
		CLR	R3
		CLR	R4
		CLR	R5
		CLR	R6
		CLR	R7
		CLR	R8
		CLR	R9
		CLR	R10
		CLR	R11
		CLR	R12
		CLR	R13
		CLR	R14
		CLR	R15
		CLR	R16
		CLR	R17
		CLR	R18
		CLR	R19
		CLR	R20
		CLR	R21
		CLR	R22
		CLR	R23
		CLR	R24
		CLR	R25
		CLR	R26
		CLR	R27
		CLR	R28
		CLR	R29
; End coreinit.inc

Reset: LDI R16,Low(RAMEND) ; Инициализация стека OUT SPL,R16 ; Обязательно!!! LDI R16,High(RAMEND) OUT SPH,R16 ; Start coreinit.inc RAM_Flush: LDI ZL,Low(SRAM_START) ; Адрес начала ОЗУ в индекс LDI ZH,High(SRAM_START) CLR R16 ; Очищаем R16 Flush: ST Z+,R16 ; Сохраняем 0 в ячейку памяти CPI ZH,High(RAMEND) ; Достигли конца оперативки? BRNE Flush ; Нет? Крутимся дальше! CPI ZL,Low(RAMEND) ; А младший байт достиг конца? BRNE Flush CLR ZL ; Очищаем индекс CLR ZH CLR R0 CLR R1 CLR R2 CLR R3 CLR R4 CLR R5 CLR R6 CLR R7 CLR R8 CLR R9 CLR R10 CLR R11 CLR R12 CLR R13 CLR R14 CLR R15 CLR R16 CLR R17 CLR R18 CLR R19 CLR R20 CLR R21 CLR R22 CLR R23 CLR R24 CLR R25 CLR R26 CLR R27 CLR R28 CLR R29 ; End coreinit.inc

Всю эту портянку можно и нужно спрятать в inc файл и больше не трогать.

Секции внешней и внутренней инициализации переферии пока пусты, но ненадолго. Равно как и запуск фоновых программ. Потом я просто буду говорить, что мол добавьте эту ботву в секцию Internal Hardware Init и все 🙂

1
2
3
4
5
6
7
8
9
10
11
; Internal Hardware Init  ======================================
 
; End Internal Hardware Init ===================================
 
; External Hardware Init  ======================================
 
; End Internal Hardware Init ===================================
 
; Run ==========================================================
 
; End Run ======================================================

; Internal Hardware Init ====================================== ; End Internal Hardware Init =================================== ; External Hardware Init ====================================== ; End Internal Hardware Init =================================== ; Run ========================================================== ; End Run ======================================================

А теперь, собственно, сам главный цикл.

1
2
3
4
5
; Main =========================================================
Main:
 
		JMP	Main
; End Main =====================================================

; Main ========================================================= Main: JMP Main ; End Main =====================================================

Все процедуры располагаются в отдельной секции, не смешиваясь с главным циклом. Так удобней, потом можно их по частям вынести в библиотечные файлы и разделить исходник на несколько файлов. Но мы пока это делать не будем. Разделим их просто логически.

1
2
3
; Procedure ====================================================
 
; End Procedure ================================================

; Procedure ==================================================== ; End Procedure ================================================

Ну и вот тебе файлик с уже готовым проектом под этот шаблон

BASCOM-AVR

Среда разработки программного кода для микроконтроллеров серии AVR компании Atmel на языке, подобном стандартному Бейсику.

Семейство 8-битных AVR-микроконтроллеров имеет одинаковый набор команд, присущий RISC-архитектуре, что означает легкость портирования программ под разные чипы. Скорость выполнения команд в четыре раза быстрее, чем у PIC-микроконтроллеров от Microchip. Bascom-AVR (от слов Basic Compiler) является превосходным компилятором, который помимо основной функции написания программ и трансляции их в машинных код в формате .hex понятный микроконтроллеру, позволяет построчно проводить отладку-симуляцию и прошивать микросхемы прямо из среды разработки с помощью программатора, например, STK200/STK300 или внешних утилит, таких как USBASP_AVRDUDE_PROG. Имеется текстовый редактор и справочно-информационная система.

Bascom-AVR – пакет для быстрой разработки, сравнительно простой в освоении, подходящий тем, у кого нет времени и сил изучать программирование на С++. Код, создаваемый этой средой, отнюдь не изящен, но компактен и быстр в исполнении. Программный пакет поддерживает основные функции чипов AVR, такие как счетчики и таймеры, аналого-цифровое преобразование, широтно-импульсную модуляцию, UART, шину I2C. Чрезвычайно экономит время поддержки различных внешних устройств: кнопок, датчиков, графических индикаторов и небольших ЖК-дисплеев, цифробуквенных табло, клавиатур 3х4 или 4х4, клавиатур PS/2 и многого другого. Программы, написанные в Bascom-AVR, могут быть отлажены на моделях в Proteus или на макетных платах.

Перед скачиванием приложения с родного сайта необходимо знать, что Bascom выпущен в трех вариантах. Помимо Bascom-AVR, существует среда Bascom-LT для микросхем AT89Cx051 и Bascom-8051 для серии 8051. Основное отличие демо версии – отсутствие возможности компиляции программ размером более 4 КБ. Работать можно лишь с самыми младшими моделями линейки AVR-микроконтроллеров. Но, для знакомства с программой, этого более чем достаточно, потому что установка Bascom-AVR, настройка и написание программы – это не самое сложное. Как правило, большинство трудностей возникают из-за незнания особенностей используемого программного и аппаратного обеспечения. Любая интересующая справочная информация, мануалы и подробные описания команд выложены на сайте разработчиков, но, к сожалению, на английском языке.
Русского языка в программе нет. Работающий любительский перевод найти в сети очень сложно. Ознакомиться с уроками по Bascom-AVR можно здесь

Приложение Bascom-AVR предназначено для работы на базе платформы Windows 98, NT, 2000, XP, Vista и 7. Совместимость с последними версиями операционных систем полная.

Распространение программы: Freeware (бесплатная с ограничением на код 4 кБ) и Shareware (платная), цена — от 79 евро

Официальный сайт BASCOM-AVR: http://www.mcselec.com

Скачать демо версию Bascom-AVR

Обсуждение программы на форуме

AVR. Учебный курс. Подпрограммы и прерывания

Подпрограммы
Когда один и тот же участок кода часто повторяется, то разумно как то его вынести и использовать многократно. Это дает просто колоссальный выйгрыш по обьему кода и удобству программирования.

Вот, например, кусок кода, передающий в регистр UDR байты с некоторой выдержкой, выдержка делается за счет вращения бесконечного цикла:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
	.CSEG
	LDI R16,Low(RAMEND)	; Инициализация стека
	OUT SPL,R16		; Обязательно!!!
 
	LDI R16,High(RAMEND)
	OUT SPH,R16
 
	.equ	Byte 	= 50
	.equ 	Delay 	= 20
 
	LDI	R16,Byte	; Загрузили значение
Start:	OUT	UDR,R16		; Выдали его в порт
 
	LDI	R17,Delay	; Загрузили длительность задержки
M1:	DEC	R17		; Уменьшили на 1
	NOP			; Пустая операция
	BRNE	M1		; Длительность не равна 0? Переход если не 0
 
	OUT	UDR,R16		; Выдали значение в порт
 
	LDI	R17,Delay	; Аналогично
M2:	DEC	R17
	NOP
	BRNE	M2
 
	OUT	UDR,R16
 
	LDI	R17,Delay
M3:	DEC	R17
	NOP
	BRNE	M3
 
	RJMP	Start		; Зациклим программу

.CSEG LDI R16,Low(RAMEND) ; Инициализация стека OUT SPL,R16 ; Обязательно!!! LDI R16,High(RAMEND) OUT SPH,R16 .equ Byte = 50 .equ Delay = 20 LDI R16,Byte ; Загрузили значение Start: OUT UDR,R16 ; Выдали его в порт LDI R17,Delay ; Загрузили длительность задержки M1: DEC R17 ; Уменьшили на 1 NOP ; Пустая операция BRNE M1 ; Длительность не равна 0? Переход если не 0 OUT UDR,R16 ; Выдали значение в порт LDI R17,Delay ; Аналогично M2: DEC R17 NOP BRNE M2 OUT UDR,R16 LDI R17,Delay M3: DEC R17 NOP BRNE M3 RJMP Start ; Зациклим программу

Сразу напрашивается повторяющийся участок кода вынести за скобки.

1
2
3
4
	LDI	R17,Delay
M2:	DEC	R17
	NOP
	BRNE	M2

LDI R17,Delay M2: DEC R17 NOP BRNE M2

Для этих целей есть группа команд перехода к подпрограмме CALL (ICALL, RCALL, CALL)
И команда возврата из подпрограммы RET

В результате получается такой код:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
	.CSEG
	LDI R16,Low(RAMEND)	; Инициализация стека
	OUT SPL,R16		; Обязательно!!!
 
	LDI R16,High(RAMEND)
	OUT SPH,R16
 
	.equ	Byte 	= 50
	.equ 	Delay 	= 20
 
	LDI	R16,Byte	; Загрузили значение
Start:	OUT	UDR,R16		; Выдали его в порт
 
	RCALL 	Wait
 
	OUT	UDR,R16
	RCALL 	Wait
	OUT	UDR,R16
	RCALL 	Wait	
	OUT	UDR,R16
	RCALL 	Wait
	RJMP 	Start		; Зациклим программу. 
 
 
Wait: 	LDI	R17,Delay
M1:	DEC	R17
	NOP
	BRNE	M1
	RET

.CSEG LDI R16,Low(RAMEND) ; Инициализация стека OUT SPL,R16 ; Обязательно!!! LDI R16,High(RAMEND) OUT SPH,R16 .equ Byte = 50 .equ Delay = 20 LDI R16,Byte ; Загрузили значение Start: OUT UDR,R16 ; Выдали его в порт RCALL Wait OUT UDR,R16 RCALL Wait OUT UDR,R16 RCALL Wait OUT UDR,R16 RCALL Wait RJMP Start ; Зациклим программу. Wait: LDI R17,Delay M1: DEC R17 NOP BRNE M1 RET

Как видишь, программа резко сократилась в размерах. Теперь скопируй это в студию, скомпилируй и запусти на трассировку. Я хочу показать как работает команда RCALL и RET и при чем тут стек.

Вначале программа, как обычно, инициализирует стек. Потом загружает наши данные в регистры R16 и выдает первый байт в UDR… А потом по команде RCALL перейдет по адресу который мы присвоили нашей процедуре, поставив метку Wait в ее начале. Это понятно и логично, гораздо интересней то, что произойдет в этот момент со стеком.

До выполнения RCALL

Увеличить
Адрес команды RCALL в памяти, по данным PC = 0x000006, адрес следующей команды (OUT UDR,R16), очевидно, будет 0x000007. Указатель стека SP = 0x045F — конец памяти, где ему и положено быть в этот момент.

После RCALL

Увеличить
Смотри, в стек пихнулось число 0x000007, указатель сместился на два байта и стал 0x045D, а контроллер сделал прыжок на адрес Wait.

Наша процедура спокойно выполняется, как ей и положено, а по команде RET процессор достанет из стека наш заныченный адрес 0x000007 и прыгнет сразу же на команду OUT UDR,R16

Таким образом, где бы мы не вызвали нашу процедуру Wait — мы всегда вернемся к тому же месту откуда вызвали, точнее на шаг вперед. Так как при переходах в стеке сохраняется адрес возврата. А если испортить стек? Взять и засунуть туда еще что нибудь? Подправь процедуру Wait и добавь туда немного бреда, например, такого

1
2
3
4
5
6
7
8
Wait: 	LDI	R17,Delay
M1:	DEC	R17
	NOP
	BRNE	M1
 
	PUSH	R17		; Ой, я не специально!
 
	RET

Wait: LDI R17,Delay M1: DEC R17 NOP BRNE M1 PUSH R17 ; Ой, я не специально! RET

Перекомпиль и посмотри что будет =) Заметь, компилятор тебе даже слова не скажет. Мол все путем, дерзай 🙂

До команды PUSH R17 в стеке будет адрес возврата 00 07, так как в регистре R17 ,в данный момент, ноль, и этот ноль попадет в стек, то там будет уже 00 00 07.

А потом идет команда RET… Она глупая, ей все равно! RET тупо возьмет два первых верхних байта из стека и запихает их в Programm Counter.

И куда мы перейдем? Правильно — по адресу 00 00, в самое начало проги, а не туда откуда мы ушли по RCALL. А будь в R17 не 00, а что нибудь другое и попади это что-то в стек, то мы бы перешли вообще черт знает куда с непредсказуемыми последствиями. Это и называется срыв стека.

Но это не значит, что в подпрограммах нельзя пользоваться стеком в своих грязных целях. Можно!!! Но делать это надо с умом. Класть туда данные и доставать их перед выходом. Следуя железному правилу «Сколько положил в стек — столько и достань!», чтобы на выходе из процедуры для команды RET лежал адрес возврата, а не черти что.

Мозговзрывной кодинг
Да, а еще тут возможны стековые извраты. Кто сказал, что мы должны вернуться именно туда откуда были вызываны? =))) А если условия изменились и по итогам вычислений в процедуре нам ВНЕЗАПНО туда стало не надо? Никто не запрещает тебе нужным образом подправить данные в стеке, а потом сделать RET и процессор, как миленький, забросит тебя туда куда надо. Легко!

Более того, я когда учился в универе и сдавал лабы по ассемблеру, то лихо взрывал мозги нашему преподу такими конструкциями (там, правда, был 8080, но разница не велика, привожу пример для AVR):

1
2
3
4
5
6
7
8
9
	LDI	R17,low(M1*2)
	PUSH	R17
	LDI	R17,High(M1*2)
	PUSH	R17
 
; потом дофига дофига другого кода... для отвлечения
; внимания, а затем, в нужном месте, ВНЕЗАПНО
 
	RET

LDI R17,low(M1*2) PUSH R17 LDI R17,High(M1*2) PUSH R17 ; потом дофига дофига другого кода… для отвлечения ; внимания, а затем, в нужном месте, ВНЕЗАПНО RET

И происходил переход на метку M1, своего рода извратский аналог RJMP M1. А точнее IJMP, только вместо Z пары мы используем данные адреса загруженные в стек из любого другого регистра, иногда пригождается. Но без особой нужды таким извратом заниматься не рекомендую — запутывает программу будь здоров.

Но побалуйся обязательно, чтобы во всей красе прочувствовать стековые переходы.

Отлаженные и выверенные подпрограммы кода можно запихать в отдельный модуль и таскать их из проекта в проект, не изобретая каждый раз по велосипеду.

Иногда подпрограммы ошибочно называют функциями. Отличие подпрограммы от функции в том, что функция всегда имеет какое то значение на входе и выдает ответ на выходе, как в математике. Ассемблерная подпрограмма же не имеет таких механизмов и их приходится изобретать самому. Например, передавая в РОН или в ячейках ОЗУ.

Подпрограммы vs Макросы
Но не стоит маникально все повторяющиеся участки заворачивать в подпрограммы. Дело в том, что переход и возврат добавляют две команды, а еще у нас идет прогрузка стека на 2 байта. Что тоже не есть гуд. И если заменяется три-четыре команды, то овчинка с CALL-RET не стоит выделки и лучше запихать все в макрос.

Прерывания

Это аппаратные события. Ведь у микроконтроллера кроме ядра есть еще дофига периферии. И она работает параллельно с контроллером. Пока контроллер занимается вычислением или гоняет байтики по памяти — АЦП может яростно оцифровывать входное напряжение, USART меланхолично передавать или принимайть байтик, а EEPROMка неспеша записывать в свои тормозные ячейки очередной байт.

А когда периферийное устройство завершает свою работу оно поднимает флаг готовности. Мол, чувак, у меня все пучком, забирай результат. Процессор может проверить этот флаг в общем цикле и как то его обработать.

Но некоторые события в принципе не могут ждать, например USART — вовремя не обработаешь входящий байт, считай провафлил передачу, т.к. передающий девайс пошлет второй, ему плевать успел ты его обработать или нет. Для таких срочных дел есть прерывания.

У AVR этих прерываний с полтора десятка наберется, на каждое переферийное устройство по прерыванию, а на некотрые и не по одному. Например, у USART их целых три — Байт пришел, Байт ушел, Передача завершена.

Как это работает

Когда случается прерывание, то процессор тут же завершает текущую команду, пихает следующий адрес в стек (точно также как и при CALL) и переходит… А куда, собственно, он переходит?

А переходит он на фиксированный вектор прерывания. За каждым аппаратным прерыванием закреплен свой именной адрес. Все вместе они образуют таблицу векторов прерывания. Расположена она в самом начале памяти программ. Для Атмега16, используемой в Pinboard таблица прерываний выглядит так:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
RESET		0x0000	; Reset Vector
INT0addr	0x0002	; External Interrupt Request 0
INT1addr	0x0004	; External Interrupt Request 1
OC2addr		0x0006	; Timer/Counter2 Compare Match
OVF2addr	0x0008	; Timer/Counter2 Overflow
ICP1addr	0x000a	; Timer/Counter1 Capture Event
OC1Aaddr	0x000c	; Timer/Counter1 Compare Match A
OC1Baddr	0x000e	; Timer/Counter1 Compare Match B
OVF1addr	0x0010	; Timer/Counter1 Overflow
OVF0addr	0x0012	; Timer/Counter0 Overflow
SPIaddr		0x0014	; Serial Transfer Complete
URXCaddr	0x0016	; USART, Rx Complete
UDREaddr	0x0018	; USART Data Register Empty
UTXCaddr	0x001a	; USART, Tx Complete
ADCCaddr	0x001c	; ADC Conversion Complete
ERDYaddr	0x001e	; EEPROM Ready
ACIaddr		0x0020	; Analog Comparator
TWIaddr		0x0022	; 2-wire Serial Interface
INT2addr	0x0024	; External Interrupt Request 2
OC0addr		0x0026	; Timer/Counter0 Compare Match
SPMRaddr	0x0028	; Store Program Memory Ready

RESET 0x0000 ; Reset Vector INT0addr 0x0002 ; External Interrupt Request 0 INT1addr 0x0004 ; External Interrupt Request 1 OC2addr 0x0006 ; Timer/Counter2 Compare Match OVF2addr 0x0008 ; Timer/Counter2 Overflow ICP1addr 0x000a ; Timer/Counter1 Capture Event OC1Aaddr 0x000c ; Timer/Counter1 Compare Match A OC1Baddr 0x000e ; Timer/Counter1 Compare Match B OVF1addr 0x0010 ; Timer/Counter1 Overflow OVF0addr 0x0012 ; Timer/Counter0 Overflow SPIaddr 0x0014 ; Serial Transfer Complete URXCaddr 0x0016 ; USART, Rx Complete UDREaddr 0x0018 ; USART Data Register Empty UTXCaddr 0x001a ; USART, Tx Complete ADCCaddr 0x001c ; ADC Conversion Complete ERDYaddr 0x001e ; EEPROM Ready ACIaddr 0x0020 ; Analog Comparator TWIaddr 0x0022 ; 2-wire Serial Interface INT2addr 0x0024 ; External Interrupt Request 2 OC0addr 0x0026 ; Timer/Counter0 Compare Match SPMRaddr 0x0028 ; Store Program Memory Ready

Как видишь, это первые адреса флеша. На каждый вектор отводится по два байта в которые мы можем записать любую команду. Но что попало туда обычно не вписывают, а ставят сразу JMP на какую нибудь метку, где уже можно спокойно сделать все что душе угодно, не стесняясь размеров кода.

Запишем эту бодягу в цивильной форме, через ORG и добавим немного кода.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
         .CSEG
         .ORG $000        ; (RESET) 
         RJMP   Reset
         .ORG $002
         RETI             ; (INT0) External Interrupt Request 0
         .ORG $004
         RETI             ; (INT1) External Interrupt Request 1
         .ORG $006
         RETI		    ; (TIMER2 COMP) Timer/Counter2 Compare Match
         .ORG $008
         RETI             ; (TIMER2 OVF) Timer/Counter2 Overflow
         .ORG $00A
         RETI		    ; (TIMER1 CAPT) Timer/Counter1 Capture Event
         .ORG $00C
         RETI             ; (TIMER1 COMPA) Timer/Counter1 Compare Match A
         .ORG $00E
         RETI             ; (TIMER1 COMPB) Timer/Counter1 Compare Match B
         .ORG $010
         RETI             ; (TIMER1 OVF) Timer/Counter1 Overflow
         .ORG $012
         RETI             ; (TIMER0 OVF) Timer/Counter0 Overflow
         .ORG $014
         RETI             ; (SPI,STC) Serial Transfer Complete
         .ORG $016
         RJMP   RX_OK     ; (USART,RXC) USART, Rx Complete
         .ORG $018
         RETI             ; (USART,UDRE) USART Data Register Empty
         .ORG $01A
         RETI             ; (USART,TXC) USART, Tx Complete
         .ORG $01C
         RETI		    ; (ADC) ADC Conversion Complete
         .ORG $01E
         RETI             ; (EE_RDY) EEPROM Ready
         .ORG $020
         RETI             ; (ANA_COMP) Analog Comparator
         .ORG $022
         RETI             ; (TWI) 2-wire Serial Interface
         .ORG $024
         RETI             ; (INT2) External Interrupt Request 2
         .ORG $026
         RETI             ; (TIMER0 COMP) Timer/Counter0 Compare Match
         .ORG $028
         RETI             ; (SPM_RDY) Store Program Memory Ready
 
	 .ORG   INT_VECTORS_SIZE      	; Конец таблицы прерываний
 
;----------------------------------------------------------------------
; Это обработчик прерывания. Тут, на просторе, можно наворотить сколько
; угодно кода. 
RX_OK:	 IN 	R16,UDR		; Тут мы делаем что то нужное и полезное
 
	 RETI			; Прерывание завершается командой RETI 
;----------------------------------------------------------------------
 
 
Reset:  LDI R16,Low(RAMEND)	; Инициализация стека
	 OUT SPL,R16		; Обязательно!!!
 
	 LDI R16,High(RAMEND)
	 OUT SPH,R16
 
	 SEI			; Разрешаем прерывания глобально
	 LDI   R17,(1<<RXCIE)	; Разрешаем прерывания по приему байта
	 OUT 	UCSRB,R17
 
M1:	 NOP			
	 NOP
	 NOP
	 NOP
	 RJMP M1

.CSEG .ORG $000 ; (RESET) RJMP Reset .ORG $002 RETI ; (INT0) External Interrupt Request 0 .ORG $004 RETI ; (INT1) External Interrupt Request 1 .ORG $006 RETI ; (TIMER2 COMP) Timer/Counter2 Compare Match .ORG $008 RETI ; (TIMER2 OVF) Timer/Counter2 Overflow .ORG $00A RETI ; (TIMER1 CAPT) Timer/Counter1 Capture Event .ORG $00C RETI ; (TIMER1 COMPA) Timer/Counter1 Compare Match A .ORG $00E RETI ; (TIMER1 COMPB) Timer/Counter1 Compare Match B .ORG $010 RETI ; (TIMER1 OVF) Timer/Counter1 Overflow .ORG $012 RETI ; (TIMER0 OVF) Timer/Counter0 Overflow .ORG $014 RETI ; (SPI,STC) Serial Transfer Complete .ORG $016 RJMP RX_OK ; (USART,RXC) USART, Rx Complete .ORG $018 RETI ; (USART,UDRE) USART Data Register Empty .ORG $01A RETI ; (USART,TXC) USART, Tx Complete .ORG $01C RETI ; (ADC) ADC Conversion Complete .ORG $01E RETI ; (EE_RDY) EEPROM Ready .ORG $020 RETI ; (ANA_COMP) Analog Comparator .ORG $022 RETI ; (TWI) 2-wire Serial Interface .ORG $024 RETI ; (INT2) External Interrupt Request 2 .ORG $026 RETI ; (TIMER0 COMP) Timer/Counter0 Compare Match .ORG $028 RETI ; (SPM_RDY) Store Program Memory Ready .ORG INT_VECTORS_SIZE ; Конец таблицы прерываний ;———————————————————————- ; Это обработчик прерывания. Тут, на просторе, можно наворотить сколько ; угодно кода. RX_OK: IN R16,UDR ; Тут мы делаем что то нужное и полезное RETI ; Прерывание завершается командой RETI ;———————————————————————- Reset: LDI R16,Low(RAMEND) ; Инициализация стека OUT SPL,R16 ; Обязательно!!! LDI R16,High(RAMEND) OUT SPH,R16 SEI ; Разрешаем прерывания глобально LDI R17,(1<<RXCIE) ; Разрешаем прерывания по приему байта OUT UCSRB,R17 M1: NOP NOP NOP NOP RJMP M1

Теперь разберем эту портянку. Контроллер стартует с адреса 0000, это точка входа. Там мы сразу же делаем бросок на метку RESET. Если это не сделать, то контроллер пойдет выполнять команды из таблицы векторов, а они не для того там посажены. Да и не далеко он ускачет — без инициализации стека и наличия адреса возврата в нем первый же RETI вызовет коллапс. Ведь RETI работает почти также как и RET.

Поэтому сразу уносим оттуда ноги на RESET. Где первым делом инициализируем стек. А потом, командой SEI, разрешаем прерывания. И установкой бита в регистре периферии UCSRB включаем прерывание по приему байта.

Дальше зацикливаемся и ждем когда в приемный буффер USART извне свалится байт. Запускай это дело в эмуляцию и начинай трассировать по одной команде. Сначала, как я и говорил, проц прыгнет на метку Reset, потом выставит нужные значения и наглухо зациклится на

1
2
3
4
5
M1:	 NOP			
	 NOP
	 NOP
	 NOP
	 RJMP M1

M1: NOP NOP NOP NOP RJMP M1

До прихода байта. Но как же нам осуществить этот приход байта если весь наш эксперимент не более чем симуляция виртуального процессора в отладчике? А очень просто!

Вручную вписать этот байт в регистр и вручную же протыкать флаг, словно байт пришел. За прием байта отвечает флаг RXC в регистре периферии UCSRA, раздел USART. Найди там бит RXC и тыкни его, чтобы закрасился. Все, прерывание вроде как наступило.

Нажми F11, чтобы сделать еще один шаг по программе… Опа, стрелочка улетела в таблицу векторов, как раз на вектор

1
2
	.ORG $016
        RJMP   RX_OK     ; (USART,RXC) USART, Rx Complete

.ORG $016 RJMP RX_OK ; (USART,RXC) USART, Rx Complete

А оттуда уже прыжок сразу же на метку RX_OK, где мы забираем данные из регистра UDR в R17 и выходим из прерывания, по команде RETI.

При этом процессор вернется в наш бесконечный цикл, в то место откуда прерывался.

Вот, как это было, если по коду:

Увеличить

Вот, вроде теперь вопросов по выполнению быть не должно.

Разрешение и запрещение прерываний
Прерываний много, но по умолчанию они все запрещены. Во-первых, глобально — есть в процессоре в регистре SREG (о нем чуть позже) флаг I (interrupt) когда он равен 0, то все прерывания запрещены вообще, глобально, все без исключения.

Когда он равен 1, то прерывания глобально разрешены, но могут быть запрещены локально.

Устанавливается и сбрасывается этот флаг командами

  • SEI — разрешить прерывания
  • CLI — запретить прерывания (Да, по поводу моего ника, DI это, кстати, то же самое что и CLI, но для процессора Z80 😉 )

Кроме того, у каждого прерывания есть еще свой собственный бит локального разрешения. В примере с UDR это бит RXCIE (Receive Complete Interrupt Enable) и расположены они в портах ввода вывода

По дефолту, после старта, все прерывания запрещены локально и глобально. И их надо разрешать вручную, не забыв прописать обработчик прерывания.

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

Если все неиспользуемые прерывания заглушены командой RETI то ничего не пройзойдет — как пришел так и вернется обратно. Если же там ничего нет, то процессор будет выполнять эту пустоту пока не доберется до живого кода. Это может быть как переход на обработчик другого прерывания (ниже по таблице векторов) так и начало основного кода, тело обработчика прерывания, какая либо процедура, записанная до метки start. Да что угодно, что первой попадется.

Сам понимаешь, что в таком случае ни о какой корректной работе говорить не придется. Кстати, почти все сишные компиляторы, неиспользуемые обработчики глушат на RESET т.е. случайный вызов несуществующего прерывания приводит к сбросу.

С одной стороны это бага, с другой фича — так называемая ситуация максимизации ошибки. Т.е. большой фатальный глюк, обрушивающий всю программу нахрен, куда безопасней мелкой ошибки потому, что вылазит сразу же. А ошибка может поджидать годами и спокойно пересидеть период отладки, а вылезти уже в готовом устройстве. И непременно перед тем как заказчик выпишет тебе бабло за разработку 🙂

Итак, повторюсь для ясности. Прерывания по дефолту запрещены локально и глобально. Разрешают их по мере надобности. Причем делают это дважды — на локальном уровне и на глобальном.

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

Флаг этого события сбрасывается либо сам, при переходе к обработчику прерывания, либо при совершении какого-либо действия. Например, чтобы сбросить флаг события прерывания RxC надо считать байт из UDR. Протрассируй программу и сам увидишь, что сброс флага RxC происходит после выполнения команды

Либо флаг скидывают вручную — записью в этот флаг единицы. Не нуля! Единицы! Подробней в даташите на конкретную периферию.

Очередность прерываний
Но что будет если произошло одно прерывание и процессор ушел на обработчик, а в этот момент произошло другое прерывание?

А ничего не будет, при уходе на прерывание происходит аппаратный глобальный запрет всех других прерываний— просто сбрасывается флаг I, а по команде RETI, при возврате, этот флаг устанавливается в 1. В этом, кстати, отличие RET от RETI

Но! Никто не запрещает нам внутри обработчика поставить SEI и разрешить прерывания. При этом мы получим вложенные прерывания. Можно, но это опасно. Черевато переполнением стека и прочими гадостями. Так что это надо делать с твердым осознанием последствий.

Что тогда? Прерывание которое пришло во время обработки первого потеряется?

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

А теперь такая ситуация — прерывания запрещены, неважно по какой причине, и тут приходит два прерывания. Поднимает каждая свой флажок и оба ждут глобального разрешения. SEI!!! Кто пойдет первым? А первым пойдет то прерывание, чей вектор меньше по адресу, ближе к началу памяти. За ним второй.

В случае когда пришло несколько прерываний одного типа. Скажем, пока мы там ковырялись в обработчике, нам сбоку тут еще таймер три раза тикнул своим флагом, то обработается только одно событие, остальные могут потеряться.

Бег по граблям
Прерывания штука мощная, но опасная. С их помощью плодятся такие глюки, по сравнению с которыми стековые срывы так — семечки.

Вся засада багов из-за кривых прерываниях в том, что их практически невозможно отследить в отладчике.

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

Так что если у МК то понос, то золотуха, то программа петухом поет, а то молчит как рыба — знай, в 95% копать собаку надо в районе прерываний и их обработчиков.

Но если правильно написать прерывание, то багов оно не даст. Главное понимать в чем его опасность.

Грабли первые — спасай регистры!!!
Прерывание, когда оно разрешено, вызывается ВНЕЗАПНО, между двумя произвольными инструкциями кода. Поэтому очень важно, чтобы к моменту когда мы из прерывания вернемся все осталось как было.

Все регистры, используемые в обработчике прерываний, должны быть предварительно сохранены. Также должен быть сохранен регистр флагов SREG, в котором хранится результат логических операций. Результаты проще всего сохранять в стеке.

Приведу пример: Вот есть у нас обработчик прерывания который сравнивает байт на входе в USART и если он равен 10, выдает обратно отклик ‘t’ (ten в смысле).

1
2
3
4
5
6
7
8
9
10
11
RX_OK:
 
	IN	R16,UDR
	CPI	R16,10
	BREQ	Ten
	RJMP	Exit
 
Ten:	LDI	R17,'t'
	OUT	UDR,R17
 
Exit:

RX_OK: IN R16,UDR CPI R16,10 BREQ Ten RJMP Exit Ten: LDI R17,’t’ OUT UDR,R17 Exit:

И у нас есть код. Код скопипащен от балды из какого то проекта, даже пояснять не буду, не в нем суть.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
         . . . 	
         LPM    R18,Z
         CPI    R18,0
 
         BREQ   ExitStr
 
         SUBI   R16,65
         LSL    R16
 
         LDI    ZL,Low(Ltrs*2)
         LDI    ZH,High(Ltrs*2)
 
         ADD    ZL,R16
         ADC    ZH,R1
 
         . . .

. . . LPM R18,Z CPI R18,0 BREQ ExitStr SUBI R16,65 LSL R16 LDI ZL,Low(Ltrs*2) LDI ZH,High(Ltrs*2) ADD ZL,R16 ADC ZH,R1 . . .

Согласно идеи прерывания, наш обработчик может воткнуться между двумя любыми инструкциями.
Например, так:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
         . . . 	
         LPM    R18,Z
         CPI    R18,0
 
         BREQ   ExitStr
 
         SUBI   R16,65
>>>>>>>>>>>Прерывание >>>>>>>>>>>
RX_OK:
 
	IN	R16,UDR
	CPI	R16,10
	BREQ	Ten
	RJMP	Exit
 
Ten:	LDI	R17,'t'
	OUT	UDR,R17
 
Exit:	RETI
<<<<<<<<<<< Возврат <<<<<<<<<<<<<
         LSL    R16
 
         LDI    ZL,Low(Ltrs*2)
         LDI    ZH,High(Ltrs*2)
 
         ADD    ZL,R16
         ADC    ZH,R1
         . . .

. . . LPM R18,Z CPI R18,0 BREQ ExitStr SUBI R16,65 >>>>>>>>>>>Прерывание >>>>>>>>>>> RX_OK: IN R16,UDR CPI R16,10 BREQ Ten RJMP Exit Ten: LDI R17,’t’ OUT UDR,R17 Exit: RETI <<<<<<<<<<< Возврат <<<<<<<<<<<<< LSL R16 LDI ZL,Low(Ltrs*2) LDI ZH,High(Ltrs*2) ADD ZL,R16 ADC ZH,R1 . . .

До входа в прерывание, после команды SUBI R16,65 в R16 было какое то число из которого вычли 65.
И дальше с ним должны были провернуть операцию логического сдвига LSL R16, но тут вклинился обработчик, где в R16 записалось значение из UDR.

И мы выпали из обработчика перед командой LSL R16, но в R16 уже мусор какой то. Совершенно не те данные, что мы планировали.

Естественно вся логика работы от такого издевательства порушилась и возник глюк. Но стоит прерыванию прийти чуть раньше, на одну микросекунду — как глюк исчезнет, т.к. SUBI R16,65 будет уже после прерывания и логика не будет порушена, но может возникнуть другой глюк.

Полная лотерея, повезет не повезет. Может вылезти сразу, а может и через год идеальной работы, а потом также сгинет и сиди чеши репу что же это было — сбой по питанию, бага, или таракан по плате пробежал неудачно.

Чтобы такого не было в обязательно порядке надо сохранять регистры и SREG на входе в прерывание и доставать на выходе.

У нас тут, в обработчике, используется R17 и R16 и SREG (в него помещается результат работы команды CPI). Вот их и сохраним.

Выглдеть это будет так:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
RX_OK:	PUSH	R16		; Сохранили R16
	IN	R16,SREG	; Достали SREG в R16
	PUSH	R16		; Утопили его в стеке
	PUSH	R17		; Туда же утопили R17
 
; Теперь можно со спокойной совестью работу работать. 
 
	IN	R16,UDR
	CPI	R16,10
	BREQ	Ten
	RJMP	Exit
 
Ten:	LDI	R17,'t'
	OUT	UDR,R17
 
; А на выходе вернем все как было. 
; Достаем в обратном порядке
 
Exit:	POP	R17
	POP	R16
	OUT	SREG	R16
	POP	R16
	RETI			; Спокойно выходим. Регистры вернул как было.

RX_OK: PUSH R16 ; Сохранили R16 IN R16,SREG ; Достали SREG в R16 PUSH R16 ; Утопили его в стеке PUSH R17 ; Туда же утопили R17 ; Теперь можно со спокойной совестью работу работать. IN R16,UDR CPI R16,10 BREQ Ten RJMP Exit Ten: LDI R17,’t’ OUT UDR,R17 ; А на выходе вернем все как было. ; Достаем в обратном порядке Exit: POP R17 POP R16 OUT SREG R16 POP R16 RETI ; Спокойно выходим. Регистры вернул как было.

Как же выносить данные из прерываний? Да по разному, можно в память сохранять, можно для этого спец регистр заиметь и знать, что он может в любой момент измениться в прерывании.

Грабли вторые — не тормози!!!
Прерывания отвлекают процессор от основных дел, более того, они блокируют другие прерывания. Поэтому в прерывании главное все сделать максимально быстро и свалить. Никаких циклов задержки, никаких долгоиграющих процедур. Никаких ожиданий аппаратного события. СКОРОСТЬ! СКОРОСТЬ! СКОРОСТЬ! Вот что должно тобой руководить при написании обработчика.

Заскочил — сделал — выскочил!

Но такая красивая схема возможна далеко не всегда. Иногда бывает надо по прерыванию делать и медленные вещи. Например, прием байтов из интерфейса и запись их в какую нибудь медленную память, вроде EEPROM. Как тогда быть?

А тут делают проще. Цель прерывания — во что бы то ни стало среагировать на событие именно в тот момент, когда оно пришло, чтобы не прозевать. А вот обрабатывать его прямо сейчас обычно и не обязательно.
Заскочил в обработчик, схватил быстро тухнущие данные, перекинул их в буффер где им ничего не угрожает, поставил где-нибудь флажок отметку «мол там байт обработать надо» и вышел. Все! Остальное пусть сделает фоновая программа в порядке общей очереди.

Либо, если иначе никак нельзя, разрешай прерывания внутри обработчика, но смотри чтобы не возникли рекурсивные прерывания, когда один и тот же обработчик разрешает прерывания сам себе, образуя множественные вложенные вызовы с зарыванием в стек.

Грабли третьи — атомарный доступ
Есть ряд операций которые должны выполняться неразрывно. Например, чтение 16ти разрядных регистров таймера.

Ядро то у нас восьми-разрядное, поэтому 16ти разрядный регистр таймера считывается в два приема сначала младший байт, а потом старший. И вот в этом месте нельзя ни в коем случае допускать, чтобы между считыванием младшего и старшего было прерывание.

Т.к. после выхода из прерывания таймер уже может много чего натикать и информация уже будет не актуальная.

Поэтому перед чтением делаем CLI, а после SEI.

Также нельзя разрешать прерывания если одна и та же многоходовая операция делается и в прерывании и в главном цикле.

Например, прерывание хватает байты из АЦП и пишет их в буффер (ОЗУ), образуя связные цепочки данных. Главный же цикл периодически из этого буффера данные читает. Так вот, на момент чтения буффера из памяти, надо запрещать прерывания, чтобы никто не посмел в этот буффер что-нибудь записать.
Иначе мы можем получить невалидные данные — половина байтов из старого замера, половина из нового.

Причем, во многих случаях не обязательно глобально блокировать все прерывания вообще, достаточно заблокировать то локальное прерывание, которое может нам нагадить.

Грабли четвертые — не блокируй!!!
Третие грабли нужно внимательно обходить, но в паранойю впадать тоже не следует. Тупо запрещать прерывания везде, где мерещится бага, не стоит. Иначе можно прозевать события, а это плохо. Нет, обработчик то выполнится, но будет это уже не актуально — хороша ложка к обеду.

Обязательно погоняй в отладчике код (да хотя бы кучу NOP) с разными прерываниями. Чтобы понять и прочувствовать как ведут себя прерывания.

Старая версия статьи. Чисто поржать 🙂

Оболочки для USBASP | Электроника для всех

Вот уже почти два года активно использую USBasp в качестве основного программатора. Все мне в нем нравится, кроме прошивающей программы — avrdude консольная, а мне под каждую прошивку писать свой батник лениво.
Да и начинающим порой разобраться в прорве его ключей бывает сложно. Так что будем натягивать на него ГУЙ ака Графически Удобный Йнтерфейс. Их существует с пол десятка я же отобрал наиболее удачные, на мой взгляд, оболочки.

Мной долгое вовсю юзался GUI от yourdevice.net.

Оболочка удобная. Люблю ее за то, что нельзя по невнимательности угробить фьюзами контроллер, т.к. оболочка сразу же загружает дефолтные значения битов. А потом их уже по мере необходимости заменяешь на те что надо. Все остальное же делается в два клика.

Также есть оболочка написанная на Java: Burn-o-Mat — красивая, удобная. Но жууутко тормозная.

У меня на компе (весьма древнем) она вообще еле шевелится. Зато кроссплатформенная.

Еще нашлась дивная программка Khazama AVR Programmer созданная неким арабом.

Вполне неплохо работает, выставление fuse битов похоже на AVRProg идущий в составе студии.

Но чего мне не хватало так это окна с кексами. Как в UniProf или в PonyProg. Люблю я пофтыкать в колонки хексов. Медитативное занятие. Сидишь и в уме дизассемблируешь потихоньку 🙂 По знакомым адресам узнаешь где у тебя что записано. Как память распределяется… В общем, это низкоуровневый Дзен. Да и просто полезно визуально поглядеть сколько у тебя осталось еще свободных ячеек. Или изменил одну команду, а перекомпилиовать лень — поправил прям в хексе. Ну, а глянуть в дамп епрома так это вообще святое — я обычно туда какие нибудь логи люблю выгружать, а потом программатором зырю. AVRDUDE выдает intel hex который не очень удобен для просмотра — мусор слева и справа от дампа отвлекает, а хекс редактор открывать лень… Короче, одним словом — хочу :))))

И вот недавно один индус отжег и родил мега прогу eXtreme Burner — AVR. Причем это не оболочка на AVRDUDE это полноценная программа, заточенная на работу с USBAsp.

Проект еще совсем нов, поддерживаются далеко не все контроллеры, FUSE биты задаются числами. Не очень удобно, зато точно не перепутаешь единцу с нулем. Мне нравится, буду юзать!

USB программатор AVR — USBAsp

Вид сверху

!!! ЭТЕНШН !!!
Появилась схема USB программатора которая НЕ требует предварительной прошивки управляющего микроконтроллера.

Так как у многих уже давным давно нет ни COM ни LPT порта, то я решил выложить схему USB программатора для AVR. Это будет широко известный в узких кругах USBASP. Схема простая как три копейки, но COM или LPT порт все же потребуется — для того, чтобы прошить управляющий контроллер. Так что можешь сходить к другану. Программатор строится на контроллере ATMega48 или ATMega8. Нужна именно 8 или 48, без всяких индексов L. Так как у нас требуется частота выше чем 8 Мгц.

Сборка

Так как я стараюсь не выкладывать непроверенные решения, то я повторил этот программатор. Чисто для себя, поприколу. Подобрал наиболее компактную схему и перевел ее в формат Sprint Layout. Изготовил печатную плату, стравил. Засверловал и напаял компоненты. Микросхему рекомендую ставить на панельку.

Прошивка программатора
Далее замыкаем перемычку J1 и J2 и подключаем к разьему стандартный последовательный программатор, да хоть тот же программатор Громова. Программатор должен иметь свое питание, иначе нужно подать его на схему.

И заливаем в проц прошивку. Для ATMega8 одна прошивка, для ATmega48 другая. Дальше нужно выставить биты конфигурации.

Для ATMega48:
Старший байт FUSE выставляется как 0хDD, младший 0xFF. На картинке я привел скриншот из UniProf с правильно расставлеными битами конфигурации для контроллера ATMega48.

Если применяется контроллер ATmega8, то байты FUSE таковы:
Старший 0xC9, младший 0xEF

Настройка в работу
После прошивки нужно снять перемычку J1 и все, можно втыкать в комп. Сразу же должно обнаружитсья USB устройство. Скармливаем ему дрова и у нас в системе появляется новый девайс — USBAsp. Если система ругается на драйвера, говорит, что это не драйвер, а фуфел какой то. Значит контроллер либо криво прошился, либо ты забыл снять перемычку J1.

Перемычка J3 используется для прошивки контроллеров у которых частота не превышает 1.5 МГц. Я ее поставил, без нее у меня мега 8 не хотела определяться. Потом подправил меге Fuse биты, чтобы она заработала на 8 Мгц, перемычку не снял, но работает. Слышал, что подправили и теперь перемычку можно не дергать туда сюда.

Красный светодиод показывает, что программатор подключен к USB и запитан. Зеленый, что идет обращение к прошиваемому контроллеру.

Прошивающий софт
Все, теперь можно подключать к программатору провод и тыкать им в прошиваемые контроллеры.
Единственная софтина которая поддерживает этот программатор это AVR-чувак, она же дудка, она же AVRDUDE. Мощнейший консольный программатор. Не стоит пугаться его консольности, во первых батники никто не отменял, а во вторых не него существует несколько оболочек.

ВНИМАНИЕ! В той GUI оболочке что находится в архиве ИНВЕРСНЫЕ FUSE!!! То есть если в даташите написано, что дефолтные SCKEL3..0 = 0100 то тут будет показан 1011!!! Короче, как в PoniProg. Чего эти утырки так вертят эти несчастные FUSE я понять не могу, хоть бы предупреждали, а то бы залочил кристалл нахрен.

Вот, пример командной строки для прошивки через USBAsp — Записываем main.hex во флеш ATmega8:

   avrdude -c usbasp -p atmega8 -U flash:w:main.hex
В архив usbasp.rar я сложил все файлы необходимые для этого программатора:
  • Прошивка для ATMega 48 и ATMega 8
  • Драйвер для винды
  • Схема
  • Печатная плата в формате Sprint Layout
  • Фотки
  • AVRDUDE
  • GUI к AVRDUDE

UPD:
Для тех у кого вдруг пишет, что архив битый, я выложил то же самое в ZIP —USBASP.ZIP

Проверено — работает! Пользуйтесь 🙂

Страничка автора USBASP — там обновления прошивок, драйверов и варианты разводок плат.

Страничка разработчика GUI оболочки для AVRDude

!!!WARNING!!!
Тут появилась подтвержденная инфа, что новая прошивка (с оригинального сайта автора) может не работать на некоторых компах. У меня в архивах лежит старая прошивка, от 2007 или даже 2006 года. Она может не работать на самых новых компах. Короче, не работает — попробуй другую версию прошивки. С сайта автора или из моего архива.

З.Ы.
Также существует программатор AVR910, работающий также через USB и имеющий практически идентичную конструкцию. Чем он лучше/хуже я не знаю. Но можете попробовать сделать его. А я в скором времени выложу описание изготовления и использования JTAG адаптера для внутрисхемной отладки AVR.

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *