AVR. Учебный Курс. Отладка программ. Часть 3

Метод 3. USART (Работа с последовательными интерфейсами)
Пожалуй самым популярным отладочным интерфейсом является все же USART. Во-первых, он поддерживается аппаратно почти всеми микроконтроллерами. Во-вторых, он прост в использовании и требует всего один/два сигнальных провода, а в третьих, для связи с компом не надо городить никаких специфичных девайсов. В худшем случае UART-USB или UART-RS232 конвертер на FT232RL или MAX232.
 

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

1
2
3
4
5
6
7
8
9
10
11
12
; Usart INIT
		.equ 	XTAL = 8000000 	
		.equ 	baudrate = 9600  
		.equ 	bauddivider = XTAL/(16*baudrate)-1
 
uart_init:	LDI 	R16, low(bauddivider)
		OUT 	UBRRL,R16
		LDI 	R16, high(bauddivider)
		OUT 	UBRRH,R16
 
		LDI 	R16,0
		OUT 	UCSRA, R16

 

1
2
3
; Прерывания запрещены, прием-передача разрешен.
	LDI 	R16, (1<<RXEN)|(1<<TXEN)|(0<<RXCIE)|(0<<TXCIE)|(0<<UDRIE)
	OUT 	UCSRB, R16

 

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

Для отправки надо записать наше маркерное число в регистр UDR, пусть это будет код буквы «А».
 

1
2
	LDI	R16,'A'
	OUT	UDR,R16

 

Я даже проверку на занятость UDR не делаю. Для одиночного байта, если у нас отправки по UART нет и участок не зациклен, этого вполне достаточно — регистр будет свободен. Разумеется в реальной программе такого делать не следует, но для отладочной затычки вполне сойдет.
 

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

1
2
3
4
	LDI	R16,'A'
	SBIS 	UCSRA,UDRE		; Пропуск если нет флага готовности
	RJMP	PC-1 			; ждем готовности - флага UDRE
 	OUT	UDR, R16		; шлем байт

 

Можно в макрос это запихать:
 

1
2
3
4
5
6
7
8
	.MACRO	DEB_SEND
	PUSH	R16
	LDI	R16,@0
	SBIS	UCSRA,UDRE
	RJMP	PC-1
	OUT	UDR,R16
	POP	R16
	.ENDM

 

Заодно R16 в стеке спрячем, так что макрос можно юзать где угодно, словно команду:

1
	DEB_SEND 'A'

 

Но это затормозит выполнение программы, могут полететь тайминги. Так что если у вас есть какие то участки жестко зависимые от времени (скажем тайминги протокола 1-wire), то данная затычка даст сбой.
 

Выводить можно любую инфу. Например содержание нужных регистров, содержание переменных, ячеек памяти, состояния битов регистров периферии. Что хотим поглядеть — то и тащим. Надо только загрузить это в UDR. А поскольку там могут быть произвольные значения, то глядеть их лучше через терминалку способную показывать в хексах. Например, моя любимая Terminal v1.9b.
 

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

Еще настоятельно рекомендую заиметь спец литеру отправляемую в терминал сразу же после инициализации USART, еще до перехода к основной программе. У меня это обычно буква «R». От «Reset». Это позволяет поймать момент перезагрузки контроллера.
 

Например, глючила у меня программа. Все работало нормально, но при попытке принять байт все начинало странно глючить. Все облазил, код весь до дыр проглядел. По коду все должно работать… Наконец воткнул в код OUTI UDR,’R’ еще до вхождения в main. Опаньки — а контроллер то сбрасывается!
 

А почему контроллер может сбрасываться? Сбой по питанию я исключил сразу — не было там ничего такого, что бы могло дергаться. RESET был подтянут через 10кОм и особо тоже не вихлялся. Что еще?
 

Срыв стека, если произошел переход за конец кода, тоже похож на сброс — программа уходит за конец памяти и возвращается к нулевому адресу. Прошарил в отладчике и этот момент — нет срыва. Но может срыв не сразу, а спустя несколько итераций? Поставил .ORG на предпоследний адрес флеша и прописал туда маркер — не вылазит, значит срыва стека нет.
Да и код был прост как мычание, нечему там глючить. Накидал отладочных маркеров по коду, засылающих байты в строго определенном порядке — в терминалке высветилась фраза «УБЕЙ СИБЯ АПСТОЛ» — значит прога работает четко по алгоритму.
 

Заглянул под панельку… Вот в чем засада — медная ворсинка от провода МГТФ попала между RXD и RST (На меге8 они совсем рядом) и при приходе байта в порт дрыганья на RXD сбрасывали контроллер. Соблюдайте чистоту на рабочем месте! Не допетри я тогда поставить на перезагрузку маркер, так я бы до утра ковырялся с кодом. А так я резко сузил круг возможных косяков и ушло не более десяти минут на отлов этого бага.

 

Управление кодом, добыча произвольных данных
Но не едиными маркерами сыт боец USART’овой отладки.
 

Мы же можем не только слать, но и принимать! А принятое обрабатывать! Так что нам мешает делать полноценные брейкпоинты?
 

Скажем, такие:

1
2
3
4
5
	PUSH	R16
	SBIS	UCSRA,RXC
	RJMP	PC-1
	IN	R16,UDR		; Читаем UDR, чтобы сбросить RXC флаг
	POP	R16

 

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

В сочетании с маркерами дает еще и информацию о том, где мы встали. Но можно же и большее заиметь!
 

Что нам мешает сделать небольшой обработчик команд? Скажем, если мы послали R — выдаст значение нужного регистра, если М — ячейки памяти, I — байта из IO, а G — пойдет дальше:
 

Пример макроса может быть таким:
 

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
	.MACRO	DEB_CMD
	PUSH	R16		; Сохраняем регистр и флаги в стек
	IN	R16,SREG
	PUSH	R16
 
	SBIS	UCSRA,RXC
	RJMP	PC-1
	IN	R16,UDR		; Читаем UDR, чтобы сбросить RXC флаг
 
				; Определяем что там пришло
	CPI	R16,'R'
	BREQ	PC+0x07		; BREQ REGISTER
 
	CPI	R16,'M'
	BREQ	PC+0x07		; BREQ	MEMORY
 
	CPI	R16,'I'
	BREQ	PC+0x09 	; BREQ	IO
 
	CPI	R16,'G'
	BREQ	PC+0x0A		; BREQ GONEXT
 
 
	OUT	UDR,@0		; REGISTER - отправляем в UDR значение регистра
	RJMP	PC+0x0008	; Проверка на пустоту UDR тут скорей всего не нужна.
				; Т.к. пока впечатаешь ручкам в терминалку байт, наверняка 
				; все что хотело уйти уже уйдет и UDR опустошится.
 
	LDS	R16,@1		; MEMORY - шлем значение из памяти
	OUT	UDR,R16
	RJMP	PC+0x0004
 
	IN	R16,@2		; IO - шлем значение из порта ввода-вывода.
	OUT	UDR,R16
 
	POP	R16		; GONEXT - достаем все сохраненное из стека и идем дальше
	OUT	SREG,R16
	POP	R16
	.ENDM

 

Как видишь, макрос массово использует относительные переходы (иначе при вставке двух макросов метки будут дублироваться). Так что модифицировать его нужно очень осторожно. Высчитывая команды. Но есть спрособ проще — написать его вначале с обычными метками, скомпилить, запустить отладку, потом открыть дизассемблер (view->disassembler) и поглядеть там смещения.
 

Используется просто — вставляешь в код такую штуку:
 

1
	DEB_CMD R15,0x0000,PORTD

 

И при попадании в это место МК остановится и будет ждать приказа. По команде G — пойдет дальше. По команде R — даст содержание регистра R15, по команде M — содержание ячейки ОЗУ с адресом 0х0000, а по команде I — значение из порта PORTD. Порты, регистры, ячейки памяти вписываешь те которые хочешь посмотреть. Если нужно что то одно, то пишешь в ненужные поля любые подходящие значения. Ну или делаешь узкоспециализированные макросы под память, регистр и порт ВВ.
 

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

Более того, никто не запрещает написать простейший (или сложный) командный обработчик с шахматами и поэтессами, позволяющий указывать непосредственно в передаваемой команде какой ресурс надо забрать. Или что и в какой регистр записать, куда перейти… В общем, этакий микро SoftICE (если кто еще помнит этот легендарный отладчик).
 

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

Оставляю это на самостоятельное изучение :)
 

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

22 thoughts on “AVR. Учебный Курс. Отладка программ. Часть 3”

  1. Блин! Как же я раньше-то не додумался отладчик сделать? Привык в VB, привык что там много_чего есть, теперь мучаюсь, а НАДО =) DI HALT, спасибо за идею, и за статью.

    1. Кидайте заявки на программу внутрь этого комментария: что там должно быть, что хочется, чтобы было, как лучше это сделать. Будет время — напишу, выложу с исходниками.

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

    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
    
    ====================== Отправляем текст ==============================================
    		LDI	R16,Low(m1*2)
    		STS	$60,R16
    		LDI	R16,High(m1*2)
    		STS	$61,R16
    		RCALL	sent_txt
     
    ====================== Идём в_JOB_ывать дальше =======================================
     
     
    ======================= Экшин в прерывании ===========================================
     
    sent_txt:           ; Передача завершена
    		PUSH	R16
    		IN	R16,SREG
    		PUSH	R16
    		PUSH	R30
    		PUSH	R31
     
     
    		LDS	ZL,$60
    		LDS	ZH,$61
     
     
    		LPM 	R16, Z+
    		CPI	R16,0
    		BREQ	END_SENT_TXT
     
    		OUT	UDR,R16
    		STS	$60,ZL
    		STS	$61,ZH
     
     
    END_SENT_TXT:
     
    		POP	R31
    		POP	R30
    		POP	R16
    		OUT	SREG,R16
    		POP	R16
    		RETI
     
     
     
    m1: .db "Заебали лунатики!!!!",00
    m2: .db "Заебали лунатики!!!!",00
    m3: .db "Заебали лунатики!!!!",00
    m4: .db "Заебали лунатики!!!!",00
    m5: .db "Заебали лунатики!!!!",00
  3. Возможно стоит собрать себе небольшой набор полезных отладочных функций типа: вывод стека на терминал, условный бряк, минимальный аналог printf, hex/dec/bin2char* и т.п. При отсутствии jtag и невозможности подключить светодиод, может помочь.

  4. Я последнее время собрал себе http://www.mikrocontroller.net/topic/92704 вот такое устройство на ЖКИ 320х240. Подключаю его вместо терминала прямо к железке.. Есть у меня библиотечка которая умеет послать туда текст или число… Есть вариант который умеет посылать текст или число на определённые координаты на мониторчику. Думал о том чтобы приделать кнопки для обратной связи — но как то руки не дошли :) Часто возникает проблема с синхронизацией скорости когда мега работает на внутриннем генераторе. Приходится доля начала находить подходящую константу для скорости. http://wowa.cz/alex/100_5294.JPG и http://wowa.cz/alex/100_5293.JPG — это фото первого прототипа. щас это уже в коробочке :)

  5. а как работает LDI R16, (1<<RXEN)|(1<<TXEN)|(0<<RXCIE)|(0<<TXCIE)|(0<<UDRIE)
    Ясно что меняет задает биты, но не ясно как работает, последовательность написания RXEN-UDRIE важна? В описании регистра UCSRB другая. И как меняются биты этой командой тоже не ясно. Или это макрос для компилятора?

    1. Команда вида 1<<RXEN сдвигает единичку влево на столько, какое значение прописано в RXEN. Соответственно 0<<RXCIE сдвигает ноль на значение RXCIE. То есть, по датащиту RXEN = 4 (это значение сохранено в инклуде с определениями выводами), то 1<<RXEN в итоге даст 00010000. Знак | — это побитное OR. В общем в результате мы будем иметь (1<<RXEN) = 00010000, (1<<TXEN) = 00001000 все остальное это 00000000 (не важно, насколько мы нулик будем двигать), а 00010000|00001000 = 00011000 (побитно складываем оба числа). Получается LDI R16, (1<<RXEN)|(1<<TXEN)|(0<<RXCIE)|(0<<TXCIE)|(0<<UDRIE) == LDI R16, 00011000 — всё как по даташиту!

  6. Последовательность не важна.
    Это не макрос.
    Если бы ты сделал так: ldi r16,0x55 или ldi r16,0b01010101, то потом сам бы не разобрался, что ты засунул в регистр. А так, когда потом сам читаешь и даже если другой человек читает твой код, понятно:
    1<<RXEN — Прием UART разрешен.
    1<<TXEN — Передача UART разрешена.
    0<<RXCIE — Прерывание по окончании приема запрещено
    0<<TXCIE — Прерывание по окончании передачи запрещено.
    0<<UDRIE — не помню. Лень в datasheet лезть. Сам не маленький.
    То есть LDI R16, (1<<RXEN)|(1<<TXEN)|(0<<RXCIE)|(0<<TXCIE)|(0<<UDRIE) — это запись в регистр битов: 1<<RXEN — установка бита RXEN, 0<<RXCIE — сброс бита RXCIE.

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


    Ring:
    rcall GetByte
    cpi t2, ‘R’
    brne Ring ;Переход на следующую строку для сравнения
    rcall GetByte ;Принимаем символ с ПК
    cpi t2, ‘I’
    brne Ring ;Переход на следующую строку для сравнения
    rcall GetByte ;Принимаем символ с ПК
    cpi t2, ‘N’
    brne Ring ;Переход на следующую строку для сравнения
    rcall GetByte ;Принимаем символ с ПК
    cpi t2, ‘G’
    brne Ring ;Переход на следующую строку для сравнения
    rcall GetByte ;Принимаем символ с ПК
    cpi t2, 13
    brne Ring

    ;######################################################
    ;Выполнение команды после правильного приема «RING»
    ;######################################################

    GetByte: ;Подпрограмма приема символа по USART
    sbis UCSRA, RXC ;Проверка на заполнение буфера передачи
    rjmp GetByte ;Ожидаем заполение буфера
    in t2, UDR ;Сохраняем символ в t2
    ret

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

      1. Приведенный код не коректен, согласен, он отрабатывает только одну команду «RING» и конечно же по тупому, остальные остаются не у дел. Мне всего-то надо принимать 5-10 команд. В ветке http://easyelectronics.ru/avr-uchebnyj-kurs-peredacha-dannyx-cherez-uart.html в каментах нашел
        Цитата «Думаю проще чем XOR тут ничего не придумать. Зажираешь строку в буффер. Дальше берешь и начинаешь этот буффер циклично XORить на ту строку с которой сравниваешь. Прям побайтно. Если в результате всей операции у тебя получился 0 значит строка та самаяи в бит в бит совпадает со сравниваемой.»
        Только еще добавить окончание команды например вводом Enter (0x0D.По приему Enter переходим в процедуру сравнения и в обратном порядке (начиная от 0х0D) вынимаем из буфера по одному символу и сравнениваем с зарание внесенными во flash. Как организовать цикличный буфер на асме пока не знаю. Ограничился 10 символами, если приходят дальше, то заполняться начинают опять с начального адреса буфера.

    2. Как идея — можно все принятые сигналы по прерыванию засовывать во что-нибудь типа массива, а в качестве последнего символа использовать спец-символ, (/0, например) — чтобы увидя его пойти в процедурку выяснение, чего же там напринимали. Очень понятно можно решить такую проблему на С, но DI HALT, конечно же прав, гораздо разумнее использовать однобайтные команды — плюсов гораздо больше. А чтобы было ясно, что какая команда выполняет, в Flash можно записать описания символов (прямо строками), и выдавать их в УАРТ, например, по команде «?».

      1. Я пытаюсь работать с GSM телефоном, так он при включении выдает несколько символов (разобрать не смог надо подбирать скорость). И один из символов может случайным образом совпасть с командой, что приведет к выполнению. Управление по одному принятому символу не так интересно.
        Надо изначально ставить планку цели повыше, чтобы по ходу движения учиться.
        Пока научился только включать, выключать и перезагружать программно, поднимать трубку на входящий звонок и принимать звонок в одного забитого номера (правда совсем не красиво, зато работает)

        1. Таак, стоп. Ты пытаешься чтоль вирутальную машину вкорячить? Тема вообщет не об этом, а об отладке кода что в МК зашит.

          А отладку связи с телефоном можно и на терминале проверить. Не обязательно вживую.

    3. У меня была подобная проблема. Сделал так: сначала идёт заголовок (2 байта), потом команда (1 байт), потом параметр (1 байт), потом контрольная сумма (команда XOR параметр).
      На стороне МК ждётся первый символ заголовка, если потом и второй совпал — начинаем считывать команду, параметр, и поле контрольной суммы. Потом расчитываем (команда XOR параметр) и сравниваем с ним. Если совпало — значит всё пучком, и выполняем команду.

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      
      char command=0;                                             // Для хранения номера комманды
      char parametr=0;                                            // Для хранения параметра комманды   
      char CRC=0;                                                 // Для хранения CRC-поля
      char chrBeginCommand[]="c_";                                // Признак начала команды ('c_')
       
      char GetCommand(void){
      // Возвращает true при приёме комманды
      command=0;
      parametr=0;
       
           do  {}                                    // Читаем поток до появление 1го символа "признака начала команды"
           while (getchar()!=chrBeginCommand[0]);
       
            if(getchar()==chrBeginCommand[1]){       // Если совпал 2й символ, то ..
               // Значит от ПК пришла комманда
               command=getchar();                    // Читаем номер комманды
               parametr=getchar();                   // Читаем параметр комманды
               CRC=getchar();                        // Читаем CRC поле    
       
               return (CRC==(command ^ parametr));   // Вычисляем CRC
               //return (CRC==((command || parametr) &amp;&amp; (!command || !parametr)));   // Вычисляем CRC
             }       
      }

      А в основном цикле — CASE.

  8. В usbasp ещё uart был выведен на разъём. Только не работал вроде… Если в атмегу в usbasp добавить USB->UART(скорее всего, придётся не одновременно). Или добавить USB хаб и, например FT232RL. И на своих девайсах в ISP добавлять UART.. Было-бы, имхо, удобно.

  9. Заранее извиняюсь. Вполне возможно данная тема уже есть но я мог её просто не найти в силу разных причин.
    Делаю устройство с которого нужно передавать данные через UART.
    Раньше всё камутировалось от самого программатора (аналог USBBit) что упрощало мою жизнь.
    но теперь по расчётам USB питания просто не хватит да и давно уже нужно переходить на внешнее питание, и материнскую плату насиловать не очень хочется.
    Питание устройства будет также 5В(от старого компьютерного БП).
    Вопрос: как лучше всего развязать эти два питания программатора и самой схемы (заранее спасибо)

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

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

Ваш e-mail не будет опубликован.

Перед отправкой формы:
Human test by Not Captcha