При написании программ постоянно приходится использовать разные стандартные конструкции вроде циклов, операторов выбора, перехода, сравнения. Всякие там if-then-else или case-switch. В высокоуровневых языках это все уже готово, а на ассемблере приходится изобретать каждый раз заново.
Впрочем, такие вещи пишутся один раз, а дальше просто по наезженной тропинке применяются везде, где потребуется. Заодно и оптимизируются по ходу процесса.
▌Условие if-then-else
Тут проще всего методом последовательной проверки проложить маршрут до нужного блока кода. Приведу пример:
1 2 3 4 5 6 7 8 9 | if (А>=B) { action_a } else { action_b } next_action |
Как это проще всего сделать на ассемблере?
Считаем, что А в R16,B в R17, а полезные действия, которые могли бы быть там, заменяем на NOP,NOP,NOP.
Сравнение делается путем вычитания. По итогам вычитания выставляются флаги. Заем (когда A стало меньше B) означает, что условие не выполнилось.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 | CP R16,R17 ; Сравниваем два значения BRCS action_b ; когда A>=B флага С не будет ; Перехода не произойдет action_a: NOP ; и выполнится действие А NOP NOP RJMP next_action ; Но чтобы действие B не произошло ; в ходе естественного выполнения ; кода -- его надо перепрыгнуть. action_b: NOP ; Действие B NOP NOP next_action: NOP NOP NOP |
Но надо учитывать тот момент, что в случае А=B флаг С не вылезет, зато будет флаг Z. Но переход то у нас исходя из какого-нибудь одного флага (по С=0 или по С=1). И, исходя из выбора команды для построения конструкции if-then-else (BRCC или BRCS), будет разная трактовка результата условия if (А>=B) в случае А=B.
В одном случае (нашем), где переход идет на else по С=1, А=B будет эквивалентно А>B — флага C не будет. И в том и другом случае услове if (А>=B) даст True и переход по BRCS на then.
Если перестроить конструкцию наизнанку, через команду BRCC то условия (А>=B) уже не получится. При A=B не будет флага С и произойдет переход по BRCC на else.
Для создания строгих неравенств нужна проверка на ноль.
Теперь А больше B:
1 2 3 4 5 6 7 8 9 | if (А>B) { action_a } else { action_b } next_action |
Получили:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 | CP R16,R17 ; Сравниваем два значения BREQ action_b ; Если равно (флаг Z), то переход сразу. ; Потом проверяем второе условие. BRCS action_b ; когда A>B флага С не будет ; Перехода не произойдет action_a: NOP ; и выполнится действие А NOP NOP RJMP next_action ; Но чтобы действие B не произошло ; в ходе естественного выполнения ; кода -- его надо перепрыгнуть. action_b: NOP ; Действие B NOP NOP next_action: NOP NOP NOP |
Сложно? Думаю нет.
Усложним еще раз наше условие:
1 2 3 4 5 6 7 8 9 | if (C<A AND A<B) { action_a } else { action_b } next_action |
Считаем, что A=R16, B=R17, С=R18
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 | ;IF CP R16,R18 ; Сравниваем С и А BREQ action_b ; Переход сразу на ELSE если С=А BRCS action_b ; Если А оказалось меньше, то будет С ; и сразу выходим отсюда на else ; Но если C<A то идем дальше и проверяем ; Второе условие (A<B) CP R16,R17 ; Сравниваем два значения ; BREQ action_b ; Переход на ELSE если B=А. Команда BREQ специально ; закомментирована -- она тут не нужна. Я ее поставил ; для наглядности. Ведь в случае А=B ; флага С не будет и однозначно будет переход по BRCC. BRCC action_b ; Когда A>B флага С не будет ; А переход по условию С clear сработает. ;THEN action_a: NOP ; Выполнится действие А NOP NOP RJMP next_action ; Но чтобы действие B не произошло ; в ходе естественного выполнения ; кода -- его надо перепрыгнуть. ;ELSE action_b: NOP ; Действие B NOP NOP next_action: NOP NOP NOP |
Просто же! Вот так вот, комбинируя условия, можно размотать любую логическую конструкцию.
▌Битовые операции
Но, пожалуй, самые распространенные операции в контроллере — битовые. Включить выключить какой-нибудь параметр, инвертировать его, проверить есть или нет. Да масса случаев где это нужно. Основная масса операций идет через битовые маски.
Есть замечательные команды SBI и CBI первая ставит указанный бит в порту, вторая сбрасывает. Например:
CBI PORT,7 — обнулить 7й бит в регистре ввода-вывода PORT
SBI PORT,6 — выставить 6й бит в регистре ввода-вывода PORT
Все замечательно, но работают эти команды только в пределах первых 31 адресов в пространстве регистров ввода вывода. Для мега16 это:
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 | .equ EEARH = 0x1f .equ EEDR = 0x1d .equ EECR = 0x1c .equ PORTA = 0x1b .equ DDRA = 0x1a .equ PINA = 0x19 .equ PORTB = 0x18 .equ DDRB = 0x17 .equ PINB = 0x16 .equ PORTC = 0x15 .equ DDRC = 0x14 .equ PINC = 0x13 .equ PORTD = 0x12 .equ DDRD = 0x11 .equ PIND = 0x10 .equ SPDR = 0x0f .equ SPSR = 0x0e .equ SPCR = 0x0d .equ UDR = 0x0c .equ UCSRA = 0x0b .equ UCSRB = 0x0a .equ UBRRL = 0x09 .equ ACSR = 0x08 .equ ADMUX = 0x07 .equ ADCSRA = 0x06 .equ ADCH = 0x05 .equ ADCL = 0x04 .equ TWDR = 0x03 .equ TWAR = 0x02 .equ TWSR = 0x01 .equ TWBR = 0x00 |
И ни байтом дальше. А с более старшими адресами — облом.
Ну ничего, что нам мешает взять и записать в регистр ввода-вывода (далее РВВ, а то я уже задолбался). Этот бит самому?
Только то, что доступ там к порту идет не побитный, а сразу целыми байтами.
Казалось бы, в чем проблема? Надо выставить биты 1 и 3 в регистре TWCR, например, взял да записал двоичное число 00001010 и выставил.
1 | LDI R16,1<<3|1<<1 |
Да, можно и так, но если при этом другие биты в этом регистре равны нулю, а то ведь мы их все накроем сразу.
В таком случае, алгоритм у нас такой:
- Взять старое значение
- Подправить в нем биты
- Записать обратно
Запишем это в коде. Взять значение просто:
1 | IN R16,TWCR |
А установить нужные биты нам поможет битовая маска и операция OR. В результате этой операции, там где в битовой маске были нули будут те же значения, что и раньше. А где в маске 1 возникнут единички.
1 | ORI R16,1<<3|1<<1 ; Битовая маска 00001010 |
А затем записать уже измененный байт обратно
1 | OUT TWCR,R16 |
Сброс битов тоже можно сделать через битовую маску, но уже операция AND. И там где мы хотим сбросить нам надо поставить в маске 0, а где не хотим менять 1
Удобней делать инверсную маску. Для инверсии маски применяется операция побитового НЕ ~
Выглядеть будет так:
1 2 3 | IN R16,TWCR ANDI R16,~(1<<3|1<<1) OUT TWCR,R16 |
Обрати внимание, для сброса у нас внутри конструкции используется 1. Ведь байт то потом инвертируется!
Для инверсии битов применяется маска по XOR. Нули в этой маске не меняют биты, а единичка по XOR бит инвертирует.
Смотри сам, вверху произвольное число, внизу маска:
1 2 3 4 | 1100 1100 XOR 1111 0000 _________ 0011 1100 |
Правда есть одно ограничение — нет команды XOR маски по числу, поэтому через регистр.
1 2 3 4 | IN R17,TWCR LDI R16, 3<<1|2<<1 ; Маска EOR R17,R16 ; Ксорим OUT TWCR,R17 ; Сгружаем обратно |
Можно увязать всю ботву в макросы и тогда будет совсем хорошо :) Единственно, при использовании макросов, особенно когда они содержат внутри промежуточные регистры, нельзя забывать про эти регистры. А то в ходе исполнения макроса содержимое этих регистров меняется и если про это забыть можно получить кучу глюков :)
Но от такой напасти можно и защититься, например стеком. К примеру, изобретем мы свою команду XRI — XOR регистра с числом.
Макрос может быть таким:
1 2 3 4 | .MACRO XRI LDI @2,@1 EOR @0,@2 .ENDM |
А вызов макроса
1 | XRI R16,mask,R17 |
Последний параметр — промежуточный регистр. Указываем его вручную т.к. мало ли какой из регистров нам будет удобней в данный момент.
А можно сделать с прогрузом стека, тогда промежуточные регистры вообще не нужны. Но будет выполняться дольше, занимать больше памяти и требовать стек.
1 2 3 4 5 6 | .MACRO XRI PUSH R16 LDI R16,@1 EOR @0,R16 POP R16 .ENDM |
Вызов, в этом случае
1 | XRI R17,Mask |
Но нельзя будет поксорить R16, т.к. его значение все равно затрет при выгрузке из стека.
Также масками можно смело отрезать неиспользованные биты. Например, в регистре TWSR первые два бита это настройки, а остальные — код состояние интерфейса TWI. Так зачем нам эти два бита настройки каждый раз учитывать? Отдавили их по AND через маску 11111100 да дело в шляпе. Или сдвинули вправо.
▌Сдвиги
Ну тут просто — можно двигать биты в РОН влево и вправо. Причем сдвиг бывает двух типов.
LSR Rn — логический вправо, при этом слева в байт лезут нули.
LSL Rn — логический влево, при этом справа в байт лезут нули.
и через перенос.
ROL и ROR
При этом байт уходящий за край попадает в флаг С, а флаг С вылезает с другого края байта. Получается как бы 9 битная циклическая карусель. Где одна команда дает один шаг влево или вправо.
Как применить? Ну способы разные, например, посчитать число единиц в байте.
Смотри как просто:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | CLR R18 ; Сюда будем считать единички. Сбросим пока LDI R17,9 ; Счетчиком циклов будет LDI R16,0xAA ; В этом байте будем считать единички CLC ; Сбросим флаг С, чтобы не мешался. Loop: DEC R17 ; уменьшим счетчик BREQ End ; если весь байт дотикали - выход. ROL R16 ; Сдвигаем байт BRCC Loop ; если нет 1 в С, еще одну итерацию INC R18 ; Сосчтитали единичку RJMP Loop End: NOP |
Еще через флаг С удобно проверять крайние биты, по быстрому, чтобы не заморачиваться с масками (хотя выигрыша ни по времени, ни по коду не будет).
1 2 | ROL R16 BRCS Label ; Переход если бит 7 в R16 есть. |
или
1 2 | ROR R16 BRCS Laber ; Переход если бит 0 в R16 есть. |
▌Циклы
Ну тут все просто — либо сначала, либо потом условие, а затем тело цикла и все окольцовано переходами.
Например, выполнить цикл 20 раз.
1 2 3 4 5 6 7 8 9 10 | LDI R17,20 ; Счетный регистр Loop: NOP NOP NOP NOP NOP DEC R17 ; Уменьшаем счетчик BRNE Loop |
; Переход если не ноль (нет флага Z)
Главное следить за тем, чтобы счетный регистр нигде не запоролся в теле цикла. Иначе получим глюк. Такой глюк, конечно, легко вылавливается трассировкой, но далеко не факт, что он всплывает сразу.
Еще один пример цикла — тупые задержки. Почему я их называю тупыми? Да потому, что они сами ничего не делают и другим не дают. Крайне не рекомендую их использовать, но для простеньких программ подойдет.
Основная идея такой задержки загрузить контроллер бессмысленной работой, чтобы нащелкать как можно больше тактов. У каждого такта есть своя длительность, так что длительность задержки можно формировать от микросекунд, до десятков лет.
Вот решение в лоб:
1 2 3 | LDI R16, Delay Loop: DEC R16 BRNE loop |
Процессор потратит примерно Delay*2 тактов на бессмысленные операции. При длительности такта (на 8мгц) 1.25Е-7 секунд максимальная выдержка будет 6.4E-5 секунды. Немного, но, например, хватит на то чтобы дисплей прожевал отданый ему байт и не подавился следующим.
Если надо больше задержки, то делаются вложенные циклы с несколькими счетчиками. Тогда суммарная длительность выдержки перемножается и уже на трех вложенных циклах можно получить около 2 секунд. Ну, а на четырех уже 536 секунд.
Но есть более красивое решение, нежели вложенные циклы — вычитание многобайтного числа с переносом.
1 2 3 4 5 6 7 8 9 | LDI R16,LowByte ; Грузим три байта LDI R17,MidleByte ; Нашей выдержки LDI R18,HighByte loop: SUBI R16,1 ; Вычитаем 1 SBCI R17,0 ; Вычитаем только С SBCI R18,0 ; Вычитаем только С BRCC Loop ; Если нет переноса - переход. |
В результате, вначале у нас отнимается 1 из первого числа, а перенос возникает только тогда, когда заканчивается очередной байт. Получается, что из R16 отнимают на каждой итерации, из R17 на каждой 256 итерации, а из R18 на каждой 65535 итерации.
А всего на таких трех регистрах можно замутить тупняк на 256*256*256 тактов. И никто не мешает в том же ключе навешать еще регистров, вплоть до R31 =) И длительность задержки будет куда точней, т.к. в отличии от вложенных циклов, команда BRCC всегда будет выполняться за 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 | .DSEG Counter: .byte 3 ; Наш счетчик циклов. Три байта. .CSEG MainLoop: Do_One: NOP Do_Two: NOP Do_Three: NOP LDS R16,Counter ; Грузим наш счетчик LDS R17,Counter+1 LDS R18,Counter+2 SUBI R16,1 ; Вычитаем 1 SBCI R17,0 ; Вычитаем только С SBCI R18,0 ; Вычитаем только С BRCC DoNothing ; Не натикало? Переход ; Натикало! YES: NOP ; Делаем то, ради чего тупили NOP NOP LDI R16,LowByte ; Грузим три байта LDI R17,MidleByte ; Нашей выдержки LDI R18,HighByte DoNothing: STS Counter,R16 ; Сохраняем обратно в память STS Counter+1,R17 STS Counter+2,R18 RJMP MainLoop |
Обрати внимание, что другие операции Do_one, Do_Two, Do_Three не ждут когда натикают, они выполняются в каждую итерацию. А операция требующая задержку ее получает, не тормозя всю программу! И таких таймеров можно налепить сколько угодно, пока оперативка не кончится.
Более того, такой цикловой счетчик сам может быть алгоритмозадающим механизмом. Счетчик тикает каждый прогон главного цикла, а каждая операция в этом главном цикле ждет именно своего «номера» в главном счетчике. Получается этакая программная «шарманка», где в качестве барабана с гвоздями выступает наш счетчик.
Но это уже конечные автоматы. К ним я, возможно, вернусь позже. Еще не решил стоит освещать эту тему. А то могу и увлечься. люблю я их :)
▌Горыныч
Писал я тут библиотечку для подключения LCD к AVR. А чтобы не вкуривать в команды контроллера дисплея я распотрошил Сишный код, дабы подглядеть какими байтами и в каком порядке надо кормить контроллер, чтобы он вышел на нужный режим.
Попутно сварганил один прикольный трюк, который я называю Горыныч. Это когда мы несколько функций объединяем в одну многоголовую — с несколькими точками входа и одной точкой выхода.
Итак, мы имеем две функции обращения к LCD — запись данных и запись команд.
О! Вот эти ребята:
1 2 3 4 5 6 7 8 9 10 11 12 13 | CMD_WR: CLI RCALL BusyWait CBI CMD_PORT,RS CBI CMD_PORT,RW SBI CMD_PORT,E LCD_PORT_OUT OUT DATA_PORT,R17 RCALL LCD_Delay CBI CMD_PORT,E LCD_PORT_IN SEI RET |
Нененененене Девид Блейн!!!!
1 2 3 4 5 6 7 8 9 10 11 12 13 | DATA_WR: CLI RCALL BusyWait SBI CMD_PORT,RS CBI CMD_PORT,RW SBI CMD_PORT,E LCD_PORT_OUT OUT DATA_PORT,R17 RCALL LCD_Delay CBI CMD_PORT,E LCD_PORT_IN SEI RET |
Сейчас я вам покажу особую, оптимизаторскую, магию!
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 | CMD_WR: CLI RCALL BusyWait CBI CMD_PORT,RS RJMP WR_END DATA_WR: CLI RCALL BusyWait SBI CMD_PORT,RS WR_END: CBI CMD_PORT,RW SBI CMD_PORT,E LCD_PORT_OUT OUT DATA_PORT,R17 RCALL LCD_Delay CBI CMD_PORT,E LCD_PORT_IN SEI RET |
Ты что наделал Блейн!!! Ты зачем функцию скукожил??? Нука раскукожь ее обратно! У меня теперь функция беби сайз!!!
Ну, а теперь, для сравнения, поглядим как это сделано в LCD.c весь исходник я сюда копировать не буду, только то, что сделано у меня, ну и всю условную компиляцию я тоже выкину.
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 | void lcdControlWrite(u08 data) { lcdBusyWait(); cbi(LCD_CTRL_PORT, LCD_CTRL_RS); cbi(LCD_CTRL_PORT, LCD_CTRL_RW); sbi(LCD_CTRL_PORT, LCD_CTRL_E); outb(LCD_DATA_DDR, 0xFF); outb(LCD_DATA_POUT, data); LCD_DELAY; LCD_DELAY; cbi(LCD_CTRL_PORT, LCD_CTRL_E); outb(LCD_DATA_DDR, 0x00); outb(LCD_DATA_POUT, 0xFF); } void lcdDataWrite(u08 data) { lcdBusyWait(); sbi(LCD_CTRL_PORT, LCD_CTRL_RS); cbi(LCD_CTRL_PORT, LCD_CTRL_RW); sbi(LCD_CTRL_PORT, LCD_CTRL_E); outb(LCD_DATA_DDR, 0xFF); outb(LCD_DATA_POUT, data); LCD_DELAY; LCD_DELAY; cbi(LCD_CTRL_PORT, LCD_CTRL_E); outb(LCD_DATA_DDR, 0x00); outb(LCD_DATA_POUT, 0xFF); } |
Собственно, строчка в строчку — те же яйца только в профиль.
Что подтверждает, сказанный мной в первом посте, тезис, что не важно на каком языке писать, на си или на ассемблере — главное знать что в какие порты пихать.
Но на асме я могу свернуть свою прогу почти вдвое, не потеряв ни байта и ни такта, а на сях придется для этого вводить внутреннию функцию, которая захавает оперативку на стек, потребует кучу тактов на переходы и вообще будет выглядеть убого.
И финальная процедура инициализации на сях и на макроасме:
1 2 3 4 5 6 7 8 9 10 | void lcdInit() { lcdInitHW(); lcdControlWrite(LCD_FUNCTION_DEFAULT); lcdControlWrite(1<<LCD_CLR); lcdControlWrite(1<<LCD_ENTRY_MODE | 1<<LCD_ENTRY_INC); lcdControlWrite(1<<LCD_ON_CTRL | 1<<LCD_ON_DISPLAY ); lcdControlWrite(1<<LCD_HOME); lcdControlWrite(1<<LCD_DDRAM | 0x00); } |
Ну и у меня:
1 2 3 4 5 6 7 8 | .MACRO INIT_LCD RCALL InitHW WR_CMD 0x38 WR_CMD 0x01 WR_CMD 0x06 WR_CMD 0x0F WR_CMD 0x02 .ENDM |
Конструкции вида (1<<LCD_HOME) обусловены тем, что Сишный исходник подразумевает конфигурацию LCD — это универсализация кода.
Я тоже мог бы сбодяжить на макросах такое, но мне было лень. Поэтому я просто забил байты. По сути дела разницы нет. Правильно написанный ассемблерный код на очень многих задачах (особенно мелких, вроде этого LCD) куда более компактный и быстрый, без снижения читабельности.
Конечно что либо крупное, вроде того же USB я на Асме делать не буду, тут проще взять готовый код. Хотя… чем черт не шутит, может заморочусь и напишу свой крошечный USB драйвер.
Кстати, о готовом коде. В самой студии, в папке первого компилятора есть неплохая пачка примеров работы с периферией:
▌Готовые примеры кода
Искать это все в Atmel\AVR Tools\AvrAssembler\Appnotes\
Там, помимо привычных нам уже def.inc будут и avr***.asm файлы. Вот это примеры и есть. Там все подробно раскомментировано, правда на английском. И вот что там есть:
- AVR100.asm — работа с EEPROM.
- AVR102.asm — блочное копирование из программной памяти в ОЗУ и из ОЗУ в ОЗУ.
- AVR108.asm — загрузка из программной памяти (пример того, как совать контстанты в код и грузить их оттуда).
- AVR128.asm — работа с компаратором.
- AVR200.asm — умножение и деление.
- AVR200b.asm — умножение и деление вариант 2.
- AVR201.asm — умножение и деление с аппаратными командами.
- AVR202.asm — 16 битная арифметика (сложение. вычитание, умножение).
- AVR204.asm — BCD арифметика. Т.е. на двоично десятичных числах. Нужна для вывода цифр на разные дисплеи.
- AVR220.asm — пузырьковая сортировка.
- AVR222.asm — фильтр с усредняющим скользящим окном. Штука для сглаживания данных, например, с АЦП.
- AVR236.asm — CRC проверка памяти программ.
- AVR240.asm — 4 на 4 матричная клавиатура, с пробуждением от нажатий.
- AVR242.asm — 4 на 4 матричная клавиатура и четырех разрядным 7 сегментным индикатором (часы, таймеры и прочие девайсы такого толка).
- AVR300.asm — i2c Мастер. Простенький, на ожиданиях.
- AVR302.asm — i2c Подчиненный. Тоже простенький.
- AVR304.asm — Программный полудуплексный UART.
- AVR305.asm — Программный полудуплексный UART версия 2.
- AVR320.asm — программный SPI мастер.
- AVR400.asm — аналого-цифровой преобразователь из ШИМ и компаратора. У меня подобный пример тоже в курсе описан.
- AVR401.asm — точный 8 разрядный аналого-цифровой преобразователь. Нужна внешняя обвязка.
- AVR410.asm — декодер RC5 протокола. Один из стандартных протоколов ИК пультов ДУ
- AVR910.asm — ISP программатор.
Оптимизация -O0 стояла в avr-gcc? У меня самописный драйвер hd44780 с поддержкой чтения добавляет 84 байта при -0s. Тут, конечно, надо учитывать, что код всей прошивки — 3244 Кб. На первых порах, когда ты добавляешь #инклады, размер может резко ползти вверх, но потом этот рост прекратиться.
Да на -O0. При -Os код ужимается где то до 5кб. Но при Os не зная что да как легко получить полностью нерабочий код. Т.к. в этом режиме компилер может код так заоптимизировать, что хрен потом концы найдешь. У меня помню по неопытности был прикол, когда я пол дня убил пытаясь понять чего у меня протокол не работает ,а оказывается компилер загнал проверку бита куда то в память и сравнивал ее при проверке с сохраненным значением. Поставил volatile сразу же заработало, но для того чтобы вкурить в эти грабли пришлось кучу документации взрыть.
Ну так то самописный драйвер! Я бы тоже ручками уложил в крошечный обьем. Но в таких случаях предпочитаю нафигачить на асме.
Использование volatile в таких случая формально указано в стандарте. Оптимизатор — тоже машина, ею управлять надо умеючи. :)
Ну да ладно, не будем холиворить.
Так о чем и речь, что при программировании на Сях под МК вылазит куча специфичных тонкостей, завязанных именно на МК, без знания которых ничего не получится сделать. А адекватной документации на предмет того, где да как нужно делать я еще не встречал. Обрывки на форумах не в счет.
volatile в C99, если не раньше, был введён как раз для таких ситуаций.
Но специфика есть, не спорю. Правда она вся хорошо описывается в документации.
Книгу по Си для МК знаю только одну: «Программирование на языке С для AVR и PIC микроконтроллеров. Ю.А.Шпак.».
Но если взять, например, «Микроконтроллеры AVR семейства Mega. Руководство пользователя. А.В.Евстиеев», то там идут примеры как на Асме, так и на Си. В даташитах Atmel’а тоже с недавних пор стали так делать.
Видел я эти примеры и в Евстифееве и в даташитах. Там тот же ассемблер тока вид со стороны Си. Они слишком малы (считаные строчки), чтобы вявить такие приколы с оптимизацией.
у него они скорее как для примера.
и кстати, описывают общий вариант без оптимизаций. я это, например, заметил, когда смотрел его работу с еепромом для меги. всё правильно, но можно сократить )
Проверил размер своего драйвера «в чистом» виде — 950 байт.
сколько пишу на микроконтроллерах (микропроцессорах начиная от вм80, z80 и конечно же x86) больше всего люблю асм… (я не говорю о большом софте, а об девайсах на этих контроллерах и проциках)…
ассемблер — более прозрачен что-ли.. или склад ума у меня такой, что линейное программирование для меня проще :(
даже сейчас девайс на меге32 с несколькими протоколами обмена по rs232, mmc+lcd siemens s65 + rtc — все на асме…
p.s. хотя иногда хочется плюнуть и переписать на си (вроде быстрее и проще), но пару попыток это «проще» похоронили в зародыше.
p.p.s. вот такой вот сумбур получился ;)
Такая же фигня :))))
Когда в начале 90х я, поработав уже почти десяток лет на Ассемблере (для 8080, 8086, 8048, Z80) и нескольких BASICах, а также DBASE III+ и CLIPPER, немножко FORT, решил попробовать что — нибудь более серьезное, естественно, начал с C++ 3.0. Про него ходила молва, что он шибко крутой, ужасно мощный и гибкий, но скрытный, как закопанный в землю шланг. В общем, «Настоящие программисты пишут только на C» !!!
Первое, на что обратил внимание, — бедность встроенного набора функций. Даже для элементарного консольного ввода — вывода уже нужна библиотека (stdio.h, если не ошибаюсь). Не было даже такого элементарного понятия, как СТРОКА (string). Вместо нее — используй одномерный символьный массив, не забывая добавлять в конце символы ВК и ПС. Программку намахал быстро, откомпилировалась без проблем, но чтобы нормально заработала, пришлось попотеть. То вешалась без обьяснения причин, то вообще делала непонятно что.
Для сравнения попробовал написать то же самое на Борланд Паскале 7.0. Написал, компилятор ткнул меня несколько раз во всякие точки и запятые, ругнулся на несоответствие типов данных, после исправления запускаю, и — О, ЧУДО !!! Все работает! и именно так, как надо!
С тех пор я нафиг забросил C, писал на Паскале, затем Дельфи (1, 2, 3, 4, 5, 6…). Работа у меня в последние лет 12 в основном связана с обработкой текстовой информации (биллинговые записи, базы данных, всевозможная статистика, автоматизация отчетности).
Конечно, за эти годы С тоже не стоял на месте, многое позаимствовав у других языков, но что — то меня к нему не тянет. А тем более использовать его в микроконтроллерах для пересылки регистр — регистр, чтобы он перед этим на всякий случай сохранил кучу информации в стеке, потом долго возвращал ее обратно…
Правда, языки высокого уровня с микроконтроллерами упрощают такие вещи, как инициализация UART, ADC, I2C, LCD и прочего. Удобно, например, дать одну команду «PWM1_Init(500);» — и ШИМ1 уже настроен на работу на частоте 500гц, вместо того чтобы распихивать битики по десятку служебных регистров.
Или, например:
USART_Init(9600);//-инициализирует USART (9600 baud rate, 1 stop bit, no parity).
А сколько пришлось бы распихать вручную? Или: USART_Write(‘#’); — и символ ушел в линию. А как вам такое:
program ADC_USART;
var temp_res : word;
begin
USART_Init(19200); // initalize USART (19200 baud rate, 1 stop bit, no parity…)
ANSEL := $04; // configure AN2 pin as analog input
TRISA := $FF;
ANSELH := 0; // configure other AN pins as digital I/O
while TRUE do
begin
temp_res := ADC_Read(2) shr 2; //read 10-bit ADC from AN2 and discard 2 LS bits
USART_Write(temp_res); // send ADC reading as byte
Delay_ms(100);
end;
end.
Две команды — и напряжение батареи измерено и ушло в RS232 !
Ради этого стоит написать такие вещи на МикроПаскале или МикроБэйсике, откомпилировать, отладить, а потом взять ассемблерный листинг и дописать на ассемблере все остальное, попутно проанализировав и оптимизировав код, выданный компилятором до этого. Часто это намного ускоряет работу. Например вчера, не шибко напрягаясь, я так проверил работу двух ШИМ, UART, ввод и обработку команд платой управления двигателями своего робота, о чем и похвастался ночью в комментах в теме о робототехнике. Там я написал подробнее, повторяться не буду.
У меня под всякие иниты куча макросов обычно пишется :) Так что инклюд файл с макросами и дальше тот же самый UART_INIT_9600 :)
В конечном итоге так и получается, просто я с микроконтроллерами работаю в последние годы периодически, для души, и не успел еще накопить свою библиотечку макросов, хотя уже кое — что вырисовывается. Конечно, если бы я занимался ими на работе, в серьезных разработках, как было до развала страны, было бы гораздо проще. А так то времени нет, то стимула, то просто лень… Вот и приходится использовать то, что ускоряет и упрощает написание, пока азарт не пропал, и обьемы свободной памяти позволяют. Вот уж наворочу побольше, тогда и займусь оптимизацией и кода, и самого алгоритма, пока же — удерживаясь в рамках поставленной задачи, хочу накопить побольше готовых, функционально законченных, отлаженных кусков, потому что давно уже заметил: чем больше функциональность программы, тем легче добавлять новое. Иногда пара строк или дополнительное условие в уже работающей программе дают такие новые возможности, которые на начальном этапе потребовали бы несколько страниц.
Например, сейчас я уже пытаюсь запустить контроль напряжения батареи на своей плате, для начала запуская кнопочкой на одной из линий порта B, с выдачей результата по RS232. Правда, по ходу выяснилось, что удобнее бало бы использовать для измерения не A4, а например, А0, но два года назад я об этом не подумал, решив при разработке схемы, что подойдет и A4. Но у микроконтроллеров редко входы бывают равноценными, что-нибудь да вылезет.
Но проблема не очень велика и в принципе решаема, в пределах моих требований.
Зато есть возможность шевельнуть мозгой, и получить удовольствие от процесса.
Re: «Первое, на что обратил внимание, — бедность встроенного набора функций. Даже для элементарного консольного ввода — вывода уже нужна библиотека (stdio.h, если не ошибаюсь). Не было даже такого элементарного понятия, как СТРОКА (string). Вместо нее — используй одномерный символьный массив, не забывая добавлять в конце символы ВК и ПС. Программку намахал быстро, откомпилировалась без проблем, но чтобы нормально заработала, пришлось попотеть. То вешалась без обьяснения причин, то вообще делала непонятно что.»
А что понимается под встроенным набором функций? Так была же библиотека, так? И в названии указано «Standart I/O» (stdio.h). Чем плохо то, что набор функций подключаются опционально?
Чем неудобны нуль-терминированные строки?
Кстати рекомендую сходить на http://code.google.com/p/vector06cc/ они там Вектор эмулируют в АВРке и он у них работает и с ЖК в том числе.
А что тут удивительного? Процессор Вектора обеспечивал около 400 тыс. операций/сек, и то только на самых коротких, типа MOV A,C. Для AVR это частота порядка 400 — 500 кгц. Кто сейчас такую использует? Для периферии многие микросхемы семейства MCS 51 имеют полноценную системную шину данных и адреса, да еще с отдельными командами для устройств ввода — вывода. И адресация внешней памяти не меньше тех же 64кб. А некоторые так такие обьемы уже внутри имеют. В общем, аппаратно все, что было в 8080 — есть. Система команд тоже похожа, из за чего мне, например, в 80х годах после 8080 легко было освоить 8048. Но кроме того: добавлены команды косвенной адресации, циклы со счетчиком, и даже умножение и деление! А это тоже повышает эффективность кода. Да, еще битовые операции забыл! Так что с современными микроконтроллерами можно многое натворить. Жалко только, что это никому не нужно. Проще нефть продавать, пока не кончилась, а все остальное покупать у китайцев… По крайней мере у нас, в Казахстане, электронщику делать нечего… Программисты еще нужны, и то часто только чтобы научить остальных работников пользоваться 1C или LOTUS. А это уже не программирование, а ликбез.
Да я в принципе в теме :)
Просто проект показался созвучным тому что задумал dihalt поэтому и дал ссылку вдруг еше что полезное найдется, посмотреть чужие реализации иногда не вредно
Вектор работает в FPGA, никакой AVR-ки там нету (хоть я и хотел бы иметь мягкую и компактную АВР для эмулятора дисковода).
Ох, не знаю что ты там делал, но явно что-то не то. У меня на готовых библиотеках «Hello world» весит меньше двух килобайт. И то, можно оптимизировать её под свои нужды.
А вообще не могу без улыбки читать комменты выше… Глупо опускать какой-то язык, не умея писать на нём.
Так у тебя hd44780 работал на готовом драйвере?
Ну да. А что, не должен? Я его еще немного дописал для своих нужд — вообще шоколадно получилось.
А какую библиотеку ты взял? Они же разные есть. Я потащил себе кода на все случаи жизни с http://hubbard.engr.scu.edu/avr/avrlib/
Заюзал библиотеку lcd.c оттуда. Можешь сам посмотреть во что она компилется на Os и О0. Выглядит монструозно.
З.Ы.
Хеллоу ворлд должен быть не более 1к. В идеале байт 300-400.
Ту, которая юзается в примерах WinAVR. Там основной минимум. Если убрать все навороты, которые я дописал, то оно будет совсем мелким. Посмотрел то, что по твоей ссылке — там даже какие-то шрифты зачем-то засунули. Если бы ты нашел такой модуль на асме, он весил бы столько же. А оптимизацию ниже O2 я никогда не юзаю, смысла нет.
Хм, а где в WinAVR примеры? Щас перерыл доки которые с ним шли чет не нашел ничего вылазящего из стандартной поставки его библиотек.
WinAVR\doc\avr-libc\examples\stdiodemo
Саму работу с stdio можно выкинуть нафиг :)
Ааа вот она где порылась. Я туда даже не заглядывал.
Ну не скажи… Взял сейчас свой код и собрал с -O2 — 3558 байт, с -Os — 3244 байт. На мой взгляд, это весьма существенно. При этом никаких подводных камней с -Os у меня ни разу не было.
Сейчас интереса ради попробовал на МикроПаскале. (Для PIC16F873):
program LSD;
var text : string[16];
begin
text := ‘Hello, World !’;
Lcd_Config(PORTB,3,2,1,0,PORTB,4,7,5); // Lcd_Init
LCD_Cmd(LCD_CLEAR); // clear display
LCD_Cmd(LCD_CURSOR_OFF); // turn cursor off
LCD_Out(1,1, text); // print text to LCD, 1st row, 1st column
end.
Результат — 616 байт (15%) ROM, 54 байта (30%) RAM
И на симуляторе, и в натуре — все работает.
Добавил еще пару строк:
Delay_ms(1000); // 1 second delay
LCD_Out(2,3,’mikroPascal’); // print text to LCD, 2nd row, 3rd column
Результат — 676 байт (16%) ROM, 66 байт (37%) RAM.
Все заняло минут 15, большее время из них ушло на подключение проводков от индикатора к порту B. И пока еще никакой оптимизации не использовал.
А то: «Os, О0…»
А нафига делать конфиг ножек в исполняемой программе? Это же можно и нужно делать в макросах.
А какой смысл, если я использовал готовую команду (одну!) Паскаля для настройки. Опять же, при другой раскладке ног достаточно перечислить их в самой команде. Вот ее синтаксис из Help:
procedure Lcd_Config(var data_port : byte; db3, db2, db1, db0 : byte; var ctrl_port : byte; rs, ctrl_rw, enable : byte);
То есть я сразу могу указать любые ноги любого порта, остальное сделает компилятор. Но главной целью было поглядеть, какой окажется величина кода, безо всяких оптимизаций, чтобы знать, на что ориентироваться. Так что пока хваленый C в пролете, не смотря на его продвинутую оптимизацию.
Попутно проверил еще один кирпичик к своей программе робота. Быстро и удобно. И всего пять команд, при всей универсальности: Любой текст, в любую строку, с любой позиции, вместе с инициализацией, очисткой и управлением курсором. 5 строк! Куда еще проще — то, чтобы, например, быстро проверить индикатор? Другое дело, когда надо будет уже впихнуть это в готовую сложную программу, я гляну в ассемблерном листинге, что также выдал компилятор Паскаля, какие байтики куда пихаются, и напишу те же макросы или подпрограммки на асме, только уже не столь универсальные (Для экономии ПЗУ).
А вообще, был приятно удивлен полученным результатом. Ожидал, что будет больше, чем на крутом C, да еще с оптимально подобранными оптимизаторами… Правда, тайно надеялся, что ненамного. Но чтоб такое!… Теперь у меня тем более не появится стимула осваивать C для микроконтроллеров. Тем более начитавшись того, что листинги на C одной фирмы не понимаются компиляторами другой, или даже той же, но другой версии… Нет, такой хоккей нам не нужен!
«какой окажется величина кода, безо всяких оптимизаций» — там настройки оптимизации имеются, надеюсь?
«Другое дело, когда надо будет уже впихнуть это в готовую сложную программу, я гляну в ассемблерном листинге, что также выдал компилятор Паскаля, какие байтики куда пихаются, и напишу те же макросы или подпрограммки на асме,»
А почему не сразу на асме, пользуясь документацией?
Я уже обьяснял, что, читая эти комменты, решил просто для сравнения посмотреть, сколько будет на Паскале. Конкретно этим компилятором МикроПаскаля я пользуюсь всего с неделю, (раньше пробовал другие компиляторы и С, и Паскаля для микроконтроллеров, но ни один не понравился). Про Микропаскаль же я знал года с 2005, но руки не доходили попробовать. Недавно на каком — то форуме прочитал хорошие отзывы о нем, вот и решил попробовать. А до этого для PIC и AVR использовал asm, как более привычный, да и экономный. Но когда надо быстро проверить разные функции платы контроллера, и размеры программы не имеют пока значения, (Такая ситуация у меня сейчас с платой, сделанной 2 года назад и заброшенной из за семейных обстоятельств), оказалось, что МикроПаскаль вне конкуренции.
Несколько строчек — и уже работает терминал, или АЦП, или ШИМ. На Asmе на это ушло бы на порядок больше времени, а жизнь коротка, и интересного в ней еще так много…
Управления опциями оптимизации в МикроПаскале я пока не нашел, но судя по результатам, он и так достаточно оптимален. Размер кода не так уж и велик, а скорость написания и интуитивность понимания — выше всяких похвал. Работаю с ним недолго, но явных ляпов пока не обнаружил. Кстати, у той же фирмы есть еще и МикроСи, и МикроБэйсик, отдельно для PIC, AVR, и еще каких — то контроллеров. Если будет время, попробую проверить еще и их, как на PIC, так и AVR, хотя бы до Меги 32.
«Тем более начитавшись того, что листинги на C одной фирмы не понимаются компиляторами другой, или даже той же, но другой версии…»
Прекрасно понимаются, если разработчик придерживается стандарта, заданного «свыше». Основная проблема начинается в библиотеках (и Паскаль тут не исключение). Если библиотеки IAR и avr-libc ещё более-менее взаимозаменяемы, то у множества малоизвестных компиляторов — нет.
В 3 строчке снизу должно быть «подключение». Работаю на клаве вслепую, иногда пальцы промахиваются. Жалко, нет редактирования…
Для примера на CodeVisionAVR v1 то же самое и статистика. Библиотека встроенная, время 10 минут.
Исходник сокращённый:
#include
// Alphanumeric LCD Module functions
#asm
.equ __lcd_port=0x18 ;PORTB
#endasm
#include
void main(void)
{
lcd_init(16);
while (1)
{
lcd_gotoxy(0,0);
lcd_putsf(«Hello, World !»);
};
}
Статистика самого компилятора:
Data Stack size: 256 byte(s)
Estimated Data Stack usage: 7 byte(s)
Global variables size: 4 byte(s)
Hardware Stack size: 764 byte(s)
Heap size: 0 byte(s)
EEPROM usage: 0 byte(s) (0,0% of EEPROM)
Program size: 291 words (7,1% of FLASH)
..и асма:
ATmega8 memory use summary [bytes]:
Segment Begin End Code Data Used Size Use%
—————————————————————
[.cseg] 0x000000 0x000246 556 26 582 8192 7.1%
[.dseg] 0x000060 0x000164 0 4 4 1024 0.4%
[.eseg] 0x000000 0x000000 0 0 0 512 0.0%
вполне оптимально.
Для сравнения решил попробовать МикроCи той же фирмы, на том же PIC16F873, с аналогичной программой.
char *text = «Hello, World !»;
void main() {
Lcd_Init(&PORTB); // Lcd_Init
LCD_Cmd(LCD_CLEAR); // Clear display
LCD_Cmd(LCD_CURSOR_OFF); // Turn cursor off
LCD_Out(1,1, text); // Print text to LCD, 1st row, 1st column
}
Результат — 295 байт (7%) ROM, 40 байт (22%) RAM
Добавил пару строк:
Delay_ms(1000);
LCD_Out(2,6,»mikroE»); // Print text to LCD, 2nd row, 6th column
Результат — 345 байт (8%) ROM, 47 байт (26%) RAM
Стало уже интересней… И никакой оптимизации…
Надо будет еще, например, на Меге 8 для сравнения проверить МикроПаскаль и МикроСи, а также заодним интереса ради МикроБэйсик, тоже на PIC и AVR…
Для сравнения попробовал еще МикроБэйсик той же фирмы, на том же PIC16F873, с аналогичной программой.
program Lcd_Test
dim text as string[16]
main:
text = «Hello, World !»
Lcd_Config(PORTB,3,2,1,0,PORTB,4,7,5) ‘ Lcd_Init
LCD_Cmd(LCD_CLEAR) ‘ Clear display
LCD_Cmd(LCD_CURSOR_OFF) ‘ Turn cursor off
LCD_Out(1,1, text) ‘ Print text to LCD, 1st row, 1st column
Delay_ms(1000)
LCD_Out(2,4,»mikroBasic») ‘ Print text to LCD, 2nd row, 6th column
end.
Результат — 675 байт (16%) ROM, 65 байт (36%) RAM.
Очень интересно. Похоже на то, что версии библиотек разные.
Если бы не было оптимизации — прошивка была бы в 15-20 Кбайт. Она есть, но не настраивается.
Да откуда там 15, максимум 5
Если компилятор будет класть всё «как есть» — просто танком заменяя функции на ассемблерные вставки — можно и 15 получить.
Для примера — мой драйвер hd44780 без оптимизации занимает 9454 байт, супротив 950 байт с рабочей оной.
Сорри за оффтоп но хелп!.. прерывания не пашут почему то ни в авр студио ни в реальном мк вот код
.INCLUDE «m16def.inc»
.CSEG
.org 0
rjmp main
.org OVF0addr rjmp Tmr0_OVF ;Overflow0 Interrupt Vector Address
main:
…
OUTI TCNT0,0
OUTI TCCR0,2
OUTI TIMSK,1<<TOIE0
LOOP:
nop
rjmp LOOP
Tmr0_OVF:
…
SEI забыл в самом начале программы. По дефолту прерывания запрещены.
Почитал еще раз все комментарии, и жутко стало. Вспомнил, как я, бывало, лет 20 назад писал программки для своих контроллеров, и в основном размер был 2-4 кб…
И это при одновременной и независимой работе в реальном времени с несколькими телеграфными каналами, накоплением и выдачей статистики непосредственно после измерений, или по запросу за заданный интервал, или автоматически через задаваемые отрезки времени (например, раз в два часа)… Да что там мои программы, даже операционная система CP/M 80 была всего 6,5 Кб! (Все три ее секции, вместе с BIOS)! А тут драйвер паршивенького индикатора на пару коротеньких строчек — на несколько килобайт, да еще с разными оптимизациями. Вот до чего техника дошла!
У меня есть несколько приборчиков с этим индикатором (частотомеры, измерители LCF), в основном повторение готовых конструкций с Интернета, и размер кода у них обычно меньше 2 килобайт.
Так чего же надо было напихать в этот паршивый «драйвер на C», чтобы он стал 2-3, а то и 5, (и даже 15)кб? Это же ведь не WINDOWS! Богато жить стали. Не глядя пару килобайт туда, пару сюда, мало памяти — поставим монстрика на сотню ног, хоть и мелких…
И уже лень мозгой шевельнуть, лучше с опциями оптимизации компилятора поиграться. А ведь когда — то, почти 40 лет назад, Луноходы чуть ли не на лампах делали, и ничего — по Луне по полгода бегали…
Интересно, а сейчас кто-нибудь на территории бывшего Союза смог бы такое смастерить, хотя бы на современной элементной базе? Да чтоб не на пару дней в комнате, а с месяц по Луне? Куда катимся… Стыдно, мужики. И за державу обидно.
«И уже лень мозгой шевельнуть, лучше с опциями оптимизации компилятора поиграться.»
Что вы подразумеваете под «шевелением мозгой»?
Да просто написать простенькую подрограммку или макрос на ассемблере. Привыкли, как в Виндах: Чуть что — сразу давай готовый драйвер, вместо того, чтобы просто в нужном месте несколько команд написать. Конечно, универсальность хороша, удобно иметь сразу все на все случаи жизни. Да только во что это выливается, мы уже видим. Чего уж такого сложного в работе с этим индикатором, чтобы тратить на него несколько килобайт? (Ведь это ТЫСЯЧИ команд!). Но в реале всего — то надо настроить ему режим при старте, да затем писать в него время от времени, зачастую даже не в конкретную позицию, а строку целиком, когда работает внутренний счетчик в индикаторе, только пихай ему байты. Читать из символьного индикатора — вообще какой смысл? проще сделать буфер вывода в программе. Вот и получается, что драйвер как таковой и не нужен. Несколько команд инициализации в начале и простенькая функция вывода строки на дисплей. Кинул ей инфу в буфер или указал адрес первого символа в ПЗУ — и несколько команд в простеньком цикле перекидают все в индикатор. Это же не картинки рисовать.
Можно использовать чтение, чтобы проверить присутствие модуля как такового.
Когда идёт проверка флага занятости, то почему бы и не прочитать текущий адрес, если его уже и так выдадут?
«Да просто написать простенькую подрограммку или макрос на ассемблере.»
Я так понимаю, вы предлагаете для каждого нового проекта переписывать код управления заново?
Не так уж он велик, этот код. Просто последовательность из нескольких команд, можно просто выделить в редакторе несколько строчек и вставить в новую программу. Если нужно часто, можно эти строчки сохранить в отдельном файле. Что уж в этом сложного, если вы действительно делаете сложные вещи? А простую программу вообще иногда проще написать заново, чем приспосабливать готовые куски. Ну, а если программы как таковой еще нет, а нужно всего лишь быстро проверить уже готовое железо, как у меня сейчас, — так вообще никаких драйверов не надо, на том же МикроПаскале, к примеру, написал кусочек с парой команд — проверил индикатор, еще несколько команд — RS232, еще несколько команд — проверил ШИМ, и т.д.
Когда же буду писать окончательно уже рабочую программу, скорее всего, все же буду использовать ассемблер, поглядывая иногда на листинги, скомпилированные Паскалем, чтобы меньше лазить по документации (У меня от нее уже глаза болят, зрение сдавать стало. Четверть века пялился на мониторы, часто довольно хреновые). Вот опробую еще I2C, пищалку, ИК локаторы, одометры, еще кое — что из периферии, раскидаю все по прерываниям, — и начну уже писать все начисто. Правда, надо еще уголь привезти и печки отремонтировать, да и отпуск через неделю кончится, а работу еще никто не отменял, и до пенсии еще 7 лет пахать (у нас мужикам в 63года).
А чем «копипастинг» отличается от инкладов в самом ассемблере? Думаю, что ничем. А набор инкладов — уже библиотека. Чем она будет лучше самописной библиотеки на любом другом языке?
В отличие от обсуждавшихся библиотек на С — размером! Десятки ассемблерных команд вместо тысяч (Судя по размерам драйвера LCD в C).
Обсуждавшиеся библиотеки на Си — это не самописные библиотеки на Си, о коих говорилось постом выше, хотя я и не привязывался к конкретному ЯВУ.
Дык драйвер, который я юзаю, только это и делает — инициализация, да запись. Ну ещё я работу с несколькими буферами прикрутил, что в принципе при желании можно убрать…
Не забывай, что у АВР жутко громоздкая система команд. Все команды минимум двубайтные — какой нибудь сраный NOP и тот занимает два байта. А если уж переход куда-нибудь, то целых четыре байта. Плюс ко всему нет переходов с проверкой. Т.е. вначале ты проверь, а потом переходи. Я тут поглядел на свой код который у меня на AT89C2051 был так я в шоке был. Мне там память в 2кб безграничной казалась, а тут тока начал уже и 2кб пролетели. Пишу на асме и зажимаю каждый байт, а все равно.
Кстати, да. Почему-то совершенно забыл про то, что с развитием МК растёт и средний размер команд. Не знаешь, как с этим делом у PIC’ов?
У PIC12 — 12 бит, PIC16 — 14бит, 33-35команд.
У PIC17 — 16 бит, 58 команд (добавлен встроенный аппаратный CAN).
PIC18 и выше — пока не интересовался. Пока мне PIC16F877 за глаза.
Все команды — 1 слово. Выполнение — 1 цикл (4такта). Команды сравнения и перехода — 2 цикла (8 тактов). Все команды условных переходов при совпадении или несовпадении условия (есть для обоих вариантов) просто пропускают следующую команду, выполняя вместо нее NOP. Обычно, пропускаемая команда содержит переход. Внутренняя структура делает ненужным сохранение чего-либо в стеке, кроме адреса возврата и флагов, что делается автоматически, поэтому и стек небольшой (у PIC16 — 8 вложений, обычно хватает. У Intel 8048 было аналогично, мне хватало).
Команды условных переходов есть как с проверкой байт, так и бит, а также автодекрементные и автоинкрементные циклы.
Хоть и прошло 6 лет, но тему ещё читают… Так что несколько поправок:
> У PIC12 — 12 бит, PIC16 — 14бит, 33-35команд.
PIC12 — это те же PIC16, только восьминогие (6 I/O).
Контроллеры с 12-битной системой команд маркируются иначе: это PIC16F5xx (и PIC12F5xx), а также почти все PIC10 (шестиножки).
А ещё в семействе PIC16/PIC12 есть теперь подсемейство enhanced mid-range с усовершенствованной архитектурой, там количество команд увеличено в полтора раза при сохранении 14-битной осноы. Это семейство маркируется PIC16F1xxx (а также PIC12F1xxx, PIC16F1xxxx, PIC12F1xxxx). Значительно усовершенствована косвенная адресация, расширено адресное пространство, добавлено сложение / вычитание с переносом, три команды сдвига (в старом ядре сдвиг был только циклический), переход по смещению (в старом только на абсолютный адрес), и ещё несколько мелочей.
> У PIC17 — 16 бит, 58 команд (добавлен встроенный аппаратный CAN). PIC18 и выше — пока не интересовался.
PIC17 — это «альфа-версия» PIC18.
> Внутренняя структура делает ненужным сохранение чего-либо в стеке, кроме адреса возврата и флагов, что делается автоматически, поэтому и стек небольшой
Флаги не сохраняются, только адрес возврата.
В старом ядре PIC нужно самому сохранять рабочий регистр и флаги (STATUS), в новом это (а также регистры косвенной адресации и указатели банков памяти программы и данных) сохраняется и восстанавливается автоматически. Поскольку система прерываний одноуровневая, хватает одной области, стек не используется.
Размер стека в расширенном ядре увеличен до 16.
Что бросается в глаза при сравнении: у PIC гораздо меньше команд, чем у AVR, а рабочий регистр всего один (а не 32). Зато вся память управляется как регистры. То есть вместо 5 бит аж 7 бит кода команды выделено под адрес, что позволяет адресовать 128 адресов; переключение банков памяти (в старом ядре 4, в новом 32) позволяет ценой дополнительного слова адресовать всю память.
Вот немогу понять где сохраняется конфигурация после инициализации устройства. Например SWG писал Lcd_Config(PORTB,3,2,1,0,PORTB,4,7,5); // Lcd_Init. Но ведь где то надо хранить все эти параметры? Пытался найти эти либы в WinAVR — не вышло (. Предполагаю что настройки закидываются в стек — но если так — то необходимо почистить за собой потом — типо lcd_destoy(). Вообщем вопрос — как же на самом деле настройки храняться?
Конфиг просто записывается в регистры портов и сидит там. Т.к. кроме LCD к ним ничего не подключено, то пускай там и остаются. Стек при этом не используется :)
Ну тогда еще нюансик. Конфиг пускай сидит в регистрах портов ). Но ведь оно должно где то должно помнить что lcd инициализированн именно на PORTB — а не на каком то другом. А там помойму для этого места не хватит…
Так это в дефайнах задается. И потом, при компиляции, просто выходит так, что прога написана исключительно для LCD на порту Б
Когнитивный диссонанс.
CP R16,R17
BRCS action_b
action_a: NOP
; Сравниваем два значения
; когда A>B флага С не будет
; Перехода не произойдет
; и выполнится действие А
Но ведь если R17=R16 переход вроде как тоже не происходит, флаг C=0 и мы попадаем в action_a. А по условию if (А>B) при A=B мы должны выполнять action_b.
Такая же фигня с BRCS и при (А>=B), и (C<A AND A<B). Мозг взорван.
Так и есть. Вариант А=В должен обрабатываться дополнительным условием на флаг Z
Вот, поправил статью. Так стало куда логичней.
Для четких неравенств не обязательно проверять на ноль, достаточно СP.
И фраза «разная трактовка результата условия if (А>B) в случае А=B» сносит крышу немного.
Следуя правилу «Критикуешь — предлагай» опишу момент с ветвлением if then так, как я его понял, будучи новичком в асме:
Рассмотрим результат операции CP R16,R17.
1) R16>R17
Результат вычитания R16-R17 больше нуля, перенос не происходит и флаг С=0.
2) R16=R17
Результат вычитания R16-R17 равен нулю, перенос не происходит и флаг С=0.
3) R16=R17 мы можем воспользоваться командой ветвления BRCC и перейти к действию при С=0.
А для четкого неравенства if (A меньше B) then (action_a) else (action_b) ассемблерный вариант будет выглядеть так:
Для конструкции if (A<B>=B) и (A<B) уместно дать читателю задание переписать код для (AB), и более сложных неравенств. Это позволит закрепить понимание работы CP и ветвлений в целом. Ответ можно разместить в комментариях либо в конце статьи.
Блин, на тэг нарвался.
Для конструкции if (A меньше B) then (action_a) else (action_b):
Кстати, если говорить от задачках, то после описания кода при (A больше или равно B) и (A меньше B) уместно дать читателю задание переписать код для (A меньше или равно B), (A больше B), и более сложных неравенств. Это позволит закрепить понимание работы CP и ветвлений в целом. Ответ можно разместить в комментариях либо в конце статьи.
Можно и на одном СР я же добавил в вариант где C<A AND A<B пример работы через BRCC, где при определеном построении конструкции проверка на 0 не нужна.
Мне кажется в варианте где С в сравнении А и С BREQ тоже можно убрать, достаточно лишь поменять местами операнды в CP. Также и в примере для (A больше B)
Можно, но я не стал. Пусть будут разные варианты.
Вставлю свои 5 копеек насчет проверки величин
1. If(A>=B){action_a};
{main}
CP A,B
BRLO MAIN (BRLO если безнаковые величины BRLT знаковые например если сравнивать 10>-14)
Action_a
MAIN:
Мне все таки нравятся команды brlo и brsh хотя они идентичны brcs и brcc .Они дают представление какие операции мы проводим с числами
Да еще насчет сброса битов
ANDI R16,~(1<<3|1<<1)
Есть такая команда как cbr которое делает то же самое
А как проверить крайние биты с использованием битовой маски так же быстро, как сдвигом через С?
Хм. Ну разве что
ANDI R16,0b10000000
BREQ Zero
Можно подправить, хотя не принципиально
>> нет команды XOR маски по числу, поэтому через регистр.
>> IN R17,TWCR
>> LDI R16, 3<<1|2<<1 ; Маска
наверно хотели так:
LDI R16, 1<<3|1<<2
Цитата:
ORI R16,1<<3|1<<1 ; Битовая маска 00001001
Сколько не крутил, но 1<<3|1<<1 дает битовую маску 00001010. Или я что-то не понимаю. Сильно задумался…
Вы правы, это я у меня опечатка. Щас пофиксю.
Доброго дня суток!
Объясните пожалуйста. Перед использованием команды CP флаг С в sreg нужно сбрасывать? (или весь sreg в стек пихать)?
Ведь возможен вариант, когда флаг С будет установлен еще до выполнения команды СР, и тогда последующий переход по BR.. будет неправильным.
Нет не нужно. СP ставит флаг или снимает его в зависимости от результата операции.
«ORI R16,1<<3|1<<1 ; Битовая маска 00001001»
разве битовая маска в этом случае будет не 00001010?
Ты прав. Надо пофиксить
прошу помочь разобраться с задержками, разобранными в примерах
LDI R16, Delay
Loop: DEC R16
BRNE loop
в данной программе delay мы сами подбираем для получения нужной задерки или что?
LDI R16,LowByte
LDI R17,MidleByte
LDI R18,HighByte
loop: SUBI R16,1 ; Вычитаем 1
SBCI R17,0 ; Вычитаем только С
SBCI R18,0 ; Вычитаем только С
BRCC Loop ; Если нет переноса — переход.
Сами подбираем
LDI R16,LowByte
LDI R17,MidleByte
LDI R18,HighByte
loop: SUBI R16,1
SBCI R17,0
SBCI R18,0
BRCC Loop
аналогичный вопрос по этому коду. Значения lowByte, midbyte,highbyte тоже как-то подбираются? и как вы посчитали что задержка будет 256*256*256 почему не 65535?
Выставляются исходя из необходимой задержки и тактовой частоты.
Потому что 65535 это всего два байта, а у нас три.
А т.е. это как бы максимальная задержка для трех байт?! Когда во всех разрядах будут 1? или я чего-то не понимаю?!
Да
спасибо
а как применять эти задержки, где их используют?
Там где нужна задержка. Случаев миллион, хотя чаще всего лучше использовать системный таймер и диспетчеризацию, т.к. тупая задержка по сути дела вешает контроллер на время отработки. Это дальше описано будет.
Может быть конечно не в тему но куда еще написать я не нашел. Есть два вопроса:
использование .if .else в конструкциях макросов на асме — это вроде Си. И если можно про другие варианты Сишных комбинаций в асме. Информации по ним не нашел никакой.
И второй вопрос с подключением файлов к проекту через .include . Допустим есть какой-то фрагмент (например delay ) который выполняет «тупую» задержку, и есть фрагмент который выполняет «стартовую инициализацию портов, таймеров » (например init). Я тут загнался попробовать их оформить как подключаемые файлы .include . Во первых я не понял какое расширение давать файлам .INC или .ASM работают оба (?). А вот дальше самое интересное. Если include init.asm поставить в самом конце программы то инициализация зацикливается поскольку
rjmp init вызывает подключенный файл init.asm и при выходе из него программа оказывается опять на той же строке rjmp init. Если include init.asm прописать после всех векторов прерываний то вроде все нормально. Теперь про .include «delay.asm» . Эту строчку приходится прописывать в самом конце программы и встречающаяся строка rcall DELAY (в основном теле) перебрасывает в подключеный файл откуда выполнение благополучно возвращается в основное тело. Если же .include «delay.asm» прописать не в конце а в начале или в середине или после rcall delay то выполнение туда заходит и от туда не выходит. Объяснение нахожу пока только одно это разница в командах перед метками rjmp и rcall …. пока строчил кажись начал понимать. но всеже как и с каким расширением подключать. вот текст проги
.include «M16def.inc»
.def temp=r16
.org $000
rjmp init
.org $012
rjmp T0_OVF
.include «init.asm»
LOOP:
sleep
nop
rjmp LOOP
T0_OVF:
clr temp
out PORTD,temp
rcall DELAY
ser temp
out PORTD,temp
ldi temp,0xFf
out TCNT0,temp
reti
.include «delay.asm»
init.asm
INIT:
ldi temp,low(RAMEND)
out SPL,temp
ldi temp,high(RAMEND)
out SPH,temp
clr temp
out DDRB, temp
ldi temp,0x01
out PORTB,temp
ser temp
out DDRD,temp
out PORTD,temp
ldi temp,0x20
out MCUCR, temp
ldi temp,0x01
out TIMSK, temp
ldi temp,0x07
out TCCR0,temp
sei
ldi temp,0xFC
out TCNT0,temp
delay.asm
DELAY:
Ldi r19,10
ldi r20,255
ldi r21,255
dd:
dec r21
brne dd
dec r20
brne dd
dec r19
brne dd
ret
1. Читай описание ассеблера AVR. Есть в справке внутри студии. По F1 вызывается :) Там все директивы компилятора. Их немного. К си они не имеют отношения, хотя похожи.
2. Инклюд тупо берет кусок текста из файла на который ссылается и вставляет вместо себя. Соответственно где воткнешь там этот кусок ссылаемого кода и будет. На расширение ему пофигу, он по дефолту считает, что там текстовый файл.
Спасибо DI теперь понял
Доброго времени суток!
Вопрос по типовым конструкциям, конкретно случай if (A>B) {action_a} else {action_b}
Допустим, есть такой код:
LDI R16, -3
LDI R17, 10
CP R16, R17 ; if R16 > R17
BRCS false
BREQ false
true: NOP
RJMP true
false: NOP
RJMP false
R16 меньше R17, следовательно программа должна перейти к метке false.
Но -3 — 10 не дает ни флага Z, ни флага C, поэтому при неправильном условии выполняется метка true.
Не правильней ли здесь будет заменить BRCS на BRMI?
А надо различать знаковые и беззнаковые операции. Они вычисляются совсем по разному и применяются в разных случаях. Потому как знаковое -х будет больше беззнакового х на один старший разряд (уходящий под знак)
Н-да… Немилосердно заставлять новичков сразу думать ассемблером. ИМХО, два первых листинга должны быть проиллюстрированы блок-схемами. Т.е. ты же сначала объясняешь сам принцип ветвления, а потом показываешь его реализацию на языке. Значит надо принцип проиллюстрировать графически, а потом показать как «дерево» вытягивается в линейный код. У тебя это лихо получается жирными стрелочками. Вообще блок-схемы архиполезная вещь. Хоть программисты, даже начинающие, и относятся к ним презрительно и считают придурью преподавателей информатики. Конечно, рисовать всю программу в виде блок-схемы бессмысленно, но отдельные куски можно и нужно! Сразу видно как кусок работает, как лучше задать граничные условия тех же циклов и ветвей, где кроются грабли и каковы пути оптимизации.
а на сях придется для этого вводить внутреннию функцию, которая захавает оперативку на стек, потребует кучу тактов на переходы и вообще будет выглядеть убого.
Inline-функция или макрос сработают наравне с ручным ассемблерным слиянием этих процедур, мне кажется.
Привет, Di! Пару вопросов по примеру с цикловым счетчиком. Неочевидны некоторые вещи.
1. Значения counter (+1,+2) должны быть предварительно загружены и именно они определяют задержку куска Yes?
2. Зачем в каждой итерации писать из регистров в оперативку и из оперативки обратно в регистры?
3. Зачем вообще этот counter, если мы можем просто до главного цикла записать счетчик в регистр и с каждой итерацией уменьшая его проверять наши тики до входа в затупленный участок кода.
4. Сбрасывать счетчик разве нельзя напрямую записать опять же значения в регистр. Делай четыре -это do nothing у тебя?
Короче затупил с этим примером)
1. Да, само собой. Где то мы их должны заполнить данными. Сохранив в память.
2. Регистры слишком ценный ресурс чтобы держать там такую фигню.
3. Ну таписано же зачем. Чтобы не тупить в задержках, а прощелкать его мимо, если время не вышло и не ждать. А как распологать дело десятое. Можно так. можно эдак. Я не заморачивался.
4. Делай 4 там нету. Так что да, do nothing там дальше должно быть «делай четыре»
Данные пишем в память, чтобы не занимать регистры. Чтобы в do one, two и так далее мы могли использовать весь регистровый набор и не париться. Получается этот кусок кода как бы независим ни от кого. Взял данные из памяти, отработал, положил обратно в память, регистры освободил.
Приветствую! Еще немного об этом примере.
1. Величина задержки находится в определениях lowbyte, midlebyte и highbyte. То есть до входа в главгый цикл эти значения надо скинуть в counter1,2?
2. По логике кода регистры разве не нужно куда-либо сохранять, в стек например, перед записью в них счетчика?
3. То есть , если в других участках кода регистры r16,r17,r18 не используются, то оперативку для счетчика можно не использовать?
1. Да
2. Если они где то ранее использованы то да. Но обычно, уходя из процедуры, подметаешь за собой и в регистрах уже нет ничего важного или критичного. Другое дело прерывания, там совсем другой расклад.
3. Да.
Здравствуйте, вопрос мелко-теоретический:
У арифметических флагов какая дальнобойность? Пока проц не наткнётся на следующую арифметическую команду?
Я-то раньше считал, что флаги C и Z сбрасываются сразу же после следующей ЛЮБОЙ команды, а у DI написано:
CP R16,R17
BREQ action_b
BRCS action_b
Вот уж не думал, что Carry будет ещё жив, к моменту выполнения BRCS.
А что по этому поводу говорит эмулятор в AVR Studio? :)
Да как-то не обращал внимания. Ну установился флаг, ну произошёл условный переход, а дальше внимание ушло в другое место.
А вообще, действительно, надо было проверить.
Флаг живет до тех пор, пока его кто-нибудь не изменит. Т.е. до следующей команды меняющей флаг. Причем конкретно этот флаг. Какая команда какой флаг меняет смотри в даташите.
Спасибо, значит, жить станет чуточку легче :)
Ну, а справку по командам я, кроме как в даташите, нигде больше и не смотрю теперь.
CLC — это лишний такт, вроде, ROL сам должен позаботиться о флаге или я ошибаюсь?