Ви не увійшли.
І ніяких бібліотек.
А SMCR, по-вашому, де визначено? На avr-libc і весь ардуіно фреймворк побудований, так що це як раз із бібліотекою. Тільки замість бібліотечної функції чи макроса прямий доступ до регістра. Цілком можна і так, якщо не планується компілювати для контролерів, у яких sleep modes визначаються не через SMCR.
Цікаво, що ще пару днів тому я так вже робив. Не запрацювало з-за того, що забув /n/t. Переглянувши бібліотеку зрозумів, у чому був косяк.
Дивно, бо для єдиної інструкції у виразі в asm це не має значення. А для кількох інструкцій без \n була б синтаксична помилка.
Не зрозумів роль /n/t
\n для асемблера, бо компілятор спочатку зклеює сусідні строкові літерали, потім передає асемблеру. А асемблеру потрібно розрізняти рядки з мнемоніками. Але для єдиної інструкції це не обовʼязково.
\t для краси, щоби при виводі асемблерного лістингу наступний рядок був з відступом.
З ваших відповідей я зрозумів, чого не вистачає. Треба зупиняти процессор!
Так. Може це не єдиний, але, мабуть, найпростіший спосіб синхронізувати виконання коду з перериванням.
Обробник переривання не може почати виконуватись посередині 2- чи 3-тактової інструкції. А організувати цикл чи розгалуження без таких інструкцій неможливо. Щоб забезпечити незмінність затримки між виникненням запиту на переривання та виконанням коду, потрібно, щоб запит виникав, коли процесор знаходиться в режимі idle, тобто виконує інструкцію sleep.
Нове питання: як це зробити з Arduino IDE?
Що потрібно зробити, написано в даташиті, розділи 9.3 Idle Mode, 9.11.1 SMCR – Sleep Mode Control Register.
Як це можна зробити засобами avr-libc, написано в avr/sleep.h. Цей хідер для вашої версії avr-libc лежить у вас на диску в тулчейні.
Якщо коротко (по запису, не по виконанню):
// Один раз на початку:
set_sleep_mode(SLEEP_MODE_IDLE);
...
// Для входу в режим очікування:
sleep_mode();
Але це оверхед з перестраховкою. Можна простіше:
// Один раз на початку:
set_sleep_mode(SLEEP_MODE_IDLE);
sleep_enable();
...
// Для входу в режим очікування:
sleep_cpu();
IDE тут ні до чого, код писати можна хоч у блокноті. Чи ви маєте на увазі, зробити засобами фреймворка ардуіно? По-перше, зручно використовувати 16-бітний TIMER1 в режимі CTC, бо при 16 МГц одна горизонтальна лінія відповідає циклу таймера в 508 тактів. По-друге, потрібно як мінімум вимкнути переривання від TIMER0, який ардуіно фреймворк використовує для своїх потреб.
При спробі вивести 18й символ (а місце для нього є і на екрані, і на осциллограмі) таймер збивається.
Що значить "збивається"?
Таке враження, що, якщо обробка переривання не завершується за 4 мкс до наступного переривання, то це має негативний вплив.
4 мкс - це 64 такта. Спробую вгадати, ви помістили в обробник чималий шматок коду, який потребує зберігання/відновлення всіх регістрів. 32 інструкції pop по 2 такта в епілозі обробника, от вам і 4 мкс.
По-хорошому, обробник має складатись з єдиної інструкції reti: тільки розбудити процесор зі сплячки і завершитись.
Покажіть же, як ініціалізуєте таймер, що виконується в обробнику, як синхронізуєте переривання з рештою коду.
Хто-небудь може щось прояснити?
При наявній на даний момент інформації - можуть хіба що екстрасенси-телепати.
Колись учили ассемблер x86, там все якось більш зрозуміло було
x86 CISC, регістрів менше, а інструкцій більше. А тут RISC, все навпаки
Цікаві досліди
Записуємо 0 після i14? наче в С коді не було. Чи це вже щось наступне?
Старший байт 16-бітної змінної. У автора там наче б то індекс для масиву зі шрифтом.
Для демонстрації, мабуть, красивіше так:
$ avr-objdump -dzrwC --no-address --no-show-raw-insn --visualize-jumps test.o
...
<foo()>:
lds r24, 0x0000 ; 0x<__SREG__+0x7fffc1> R_AVR_16 serialBuf+0x1
bst r24, 2
eor r24, r24
bld r24, 0
sts 0x0000, r24 ; 0x<__SREG__+0x7fffc1> R_AVR_16 i14
sts 0x0000, r1 ; 0x<__SREG__+0x7fffc1> R_AVR_16 i14+0x1
Або просто продукувати лістинг компілятором
$ avr-g++ -mmcu=atmega328p -Os -S -o - test.cc
Але з -flto після лінковки кінцевий результат може бути зовсім іншим, так що це дуже приблизно.
lds r24, 0x0000 ; завантажили в r24 те що лежало в RAM за адресою 0х0000? serialBuf? старший байт?
Це дизасемблинг обʼєктного файлу (.o) до релокації, в ньому всі адреси нульові. При лінковці вже лінкер підставить потрібні адреси. Там буде адреса другого байта
sts 0x0000, r24 ; зберігаємо 0/1 в 0х0000? назад в serialBuf?
Нє, там буде інша адреса. Тут має бути зрозуміліше: https://godbolt.org/z/zKqrK7szG
sts 0x0000, r1 ; а що в r1??
За конвенцією в r1 завжди нуль. Саме тому після інструкцій mul, а також в преамбулі ISR компілятор його обнуляє. Є сумніви щодо доцільності такого рішення, але вже так.
А що робить lds-sts - незрозуміло.
Просто завантажує (Load Direct from Data Space) та вивантажує (Store Direct to Data Space) значення регістрів з/в памʼять. Так, в нерелокованому коді нульові адреси збивають з пантелику.
Цікаво, що навіть GCC 15.1.0 такий фрагмент:
i14 = (serialBuf & 0x400) == 0 ? 0 : 1;
компілює в короткий 9-тактовий код, а еквівалентний йому
i14 = (serialBuf & 0x400) ? 1 : 0;
в довгий, схожий на той що генерує 7.3.0.
Намагаюся створити фрагмент програми, час виконання якого не буде залежати від оброблюваних значень. Зіштовхнувся з цікавим ефектом.
if((serialBuf & 0x200) == 0) {i15 = 0;} else {i15 = 1; __asm__("nopnt");} if((serialBuf & 0x400) == 0) {i14 = 0; __asm__("nopnt""nopnt""nopnt");} else {i14 = 1;}
Час виконання кожного з рядків стабільний, незалежно від значення serialBuf. Але для досягнення такої стабільності треба додавати різну кількість NOPів і, що ще цікавіше, в різні гілки.
По-хорошому, такі фрагменти потрібно одразу писати на асемблері, чи на вбудованому, чи в окремому файлі. Воно-то можна зкомпонувати вирази мовою високого рівня і підібрати необхідну кількість нопів. Але при подальших змінах в коді, чи при компіляції іншою версією, або з іншими опціями згенерований код може стати іншим, і все попливе.
Наприклад, такий код (еквівалентний рядку вашого):
uint32_t serialBuf;
uint16_t i14;
__attribute__((naked)) void foo()
{
i14 = ((serialBuf & 0x400) == 0) ? 0 : 1;
}
Або, те ж саме:
i14 = !!(serialBuf & 0x400);
GCC версії 15.1.0 з опціями "-mmcu=atmega328p -Os" компілює в
00000000 <_Z3foov>:
0: 80 91 00 00 lds r24, 0x0000 ; 0x800000 <__SREG__+0x7fffc1>
4: 82 fb bst r24, 2
6: 88 27 eor r24, r24
8: 80 f9 bld r24, 0
a: 80 93 00 00 sts 0x0000, r24 ; 0x800000 <__SREG__+0x7fffc1>
e: 10 92 00 00 sts 0x0000, r1 ; 0x800000 <__SREG__+0x7fffc1>
9 тактів, ніякої залежності від значень.
А GCC версія 7.3.0 з ардуінівського тулчейна з тими ж опціями компілює в
00000000 <_Z3foov>:
0: 80 91 00 00 lds r24, 0x0000 ; 0x800000 <__SREG__+0x7fffc1>
4: 90 91 00 00 lds r25, 0x0000 ; 0x800000 <__SREG__+0x7fffc1>
8: a0 91 00 00 lds r26, 0x0000 ; 0x800000 <__SREG__+0x7fffc1>
c: b0 91 00 00 lds r27, 0x0000 ; 0x800000 <__SREG__+0x7fffc1>
10: 2a e0 ldi r18, 0x0A ; 10
12: b6 95 lsr r27
14: a7 95 ror r26
16: 97 95 ror r25
18: 87 95 ror r24
1a: 2a 95 dec r18
1c: 01 f4 brne .+0 ; 0x1e <_Z3foov+0x1e>
1e: 81 70 andi r24, 0x01 ; 1
20: 99 27 eor r25, r25
22: 90 93 00 00 sts 0x0000, r25 ; 0x800000 <__SREG__+0x7fffc1>
26: 80 93 00 00 sts 0x0000, r24 ; 0x800000 <__SREG__+0x7fffc1>
Дивіться дизассемблером, що генерується в кожному окремому випадку. Контекст виклику також істотно впливає на результат.
Можна за допомогою "avr-objdump -dz firmware.elf". Якщо збираєте власним Makefile, то генерацію асемблерного лістингу автоматизувати просто. В platformio також можна.
Опцією -S можна тимчасово вказати компілятору, щоб замість обʼєктного продукував асемблерний файл і на тому зупинявся.
Можна зберігати проміжні результати компіляції опцією -save-temps.
Для швидкого аналізу коду, що генерується, можна користустуватись godbolt.org. Тільки avr-gcc 7.3.0 чомусь там не знайшов.
Також майте на увазі, що link time optimization (опція -flto) може кардинально змінити код в результуючому .elf порівняно з тим, що в обʼєктному файлі. Тому кінцевий результат потрібно дивитись objdump'ом на .elf.
Звичайно, можна попередньо розібрати слово по байтах, але на це витрачається неприпустимо багато часу.
По суті, на обробку даних у вас є тільки час вертикального зворотнього "ходу променю".
Якщо горизонтальна синхронізація (HSync) генерується апаратно таймером по Output Compare Match, то це безперервний інтервал в 1.4 мс, тобто більше 22000 тактів. Плюс декілька десятків тактів під час кожного горизонтального синхроімпульсу та back porch.
Якщо HSync програмна на GPIO, то цей інтервал потрібно розбивати на безперервні фрагменти не більше як десь по 450 тактів.
Можливо компілятор і сам здогадатися розгорнути цикл, без підказок.
З дефолтовим -Os навряд чи.
dimich пише:#pragma GCC unroll 25
Нічого не змінюється, включно з розміром скомпільованого файлу.
Дивно. Ви ж це безпосередньо перед циклом "for" написали, а не десь на початку?
А, мабуть ви користуєтесь ардуінівським тулчейном з GCC 7.3.0. Ця прагма зʼявилась в GCC 8.
Просто я собі для platformio зробив пакунок atmelavr, який використовує avr-gcc, встановлений в системі (на даний момент 15.1.0), а не з їхнього репозиторію, який вони самі тягнуть з ардуїнівського з версією GCC часів Давньої Греції. Тоді вибачаюсь, не подумав перед тим як радити, що у людей це може не працювати.
При формуванні порожнього екранного рядка визначаються 25 символів, які будуть відображатися в наступних N рядках і в змінні і0 ... і24 записуються стартові адреси описів відповідних символів в кодовій таблиці.
А, здається, зрозумів. Ви тримаєте таблицю заздалегідь порахованих зміщень. Ну, можна й так, якщо ресурсу вистачає. У мене був символьний буфер на весь екран, здається 25x8.5 при шрифті 8x14, і коди символів з нього вибирались на кожній ітерації.
Хвіст, що може тягнутися за символом враховано. Останній біт завжди нульовий
Якщо тільки цифри, то можна було б зробити ширину символа 4 пікселя. Тоді в рядок в два рази більше влізе. Та й висоту не обовʼязково кратну 8. Ну то таке, вам постановка задачі та пріорітети фіч видніше.
бекслеші чомусь з'їдаються
Баг форума. Їх потрібно дублювати. І після попереднього перегляду також. Дуже незручно.
SPDR = codePage[i0++]; __asm__("nopnt""nopnt""nopnt""nopnt""nopnt""nopnt");// бекслеші чомусь з'їдаються
...
SPDR = codePage[i24++]; __asm__("nopnt""nopnt""nopnt""nopnt""nopnt""nopnt");// бекслеші чомусь з'їдаютьсяЯкщо використовувати цикл (звичайно, без NOPів, з'являється додаткова затримка, впоратись з якою не вдається
Можна перед циклом вказати #pragma GCC unroll N, де N дорівнює або більше кількості ітерацій.
#pragma GCC unroll 25
for (uint8_t i = 0; i < 25; i++) {
...
}
Тоді компілятор має повністю розгорнути цикл, якщо кількість ітерацій відома на момент компіляції.
Ітерація цикла з перевіркою умови в рантаймі на AVR виконується мінімум за 3 такта. А у вас ще й пост-інкремент різних змінних, для яких компілятор не має змоги застосувати інструкцію LD з пост-інкрементом.
Було б корисно подивитись дизасемблером, що там компілятор нагенерував, і порахувати такти.
Мені невідома логіка побудови растру у вашій програмі, тому не дуже розумію, навіщо на кожну колонку окрема змінна. Може це теж можна оптимізувати. Памʼятаю, мені вдавалось зекономити декілька тактів, змінивши формат зберігання шрифта на interlaced (переплетений?): зазвичай шрифт зберігається по порядку символів: спочатку N байт першого символа, де N - висота в пікселях, потім N байт другого і т.д. А у мене формат був: спочатку 256 байт верхнього "поверха" кожного символа, потім 256 другого зверху "поверха" і т.д. Це дозволило позбутись множення на висоту символа на кожній ітерації. Але не знаю, чи доцільно це застосовувати у вашій реалізації.
Якщо у вас там тільки цифри, то інтервал в 125 нс (1 "віртуальний" піксель) не має сильно заважати. Тільки ж AVR'івський контролер SPI при CPHA=1 "тягне" останній біт до наступного байта, тобто буде не просто темний проміжок, а "розтягнуті" крайні пікселі. А при CPHA=0 сигнал потрібно інвертувати. Хоча й при використанні USART SPI теж потрібен зовнішній інвертор.
В SPI мені вдалося звести інтервал між суміжними байтами до 0,125 наносекунди.
Ви не помилились з приставкою? 0,125 наносекунди чи мікро?
Ось щойно перевірив: при затримці між записами в SPDR 17 тактів дані виводяться нормально, інтервал 125 нс. При затримці 16 тактів виводиться нуль. При затримці 15 і менше запис в регістр взагалі ігнорується.
А USART в режимі SPI хіба не буде додавати стартові і стопові біти?
Не буде, на то він і SPI-режим.
Я спробував виводити послідовний код через SPI. Виводить 8 біт за мікросекунду, а потім ще мікросекунду чекає. Чи можна вплинути на цю зайву затримку?
Так покажіть код, яким виводите. Мабуть же там ще якісь інструкції виконуються.
На "звичайному" SPI завжди будуть проміжки в один чи два такта між байтами. Принаймні мені так і не вдалось їх позбутись. Але не ціла мікросекунда, то щось не так в коді.
Щоб отримати безперервний потік, потрібно виводити через USART в режимі SPI. Звісно, вивод буде на TX пін, а не на MOSI.
Хм, здається, я здогадуюсь, чому так мало козявок з HDMI. Це ж проприєтарний стандарт. За кожний розʼєм на виробі потрібно ліцензійний збір сплачувати.
А можна приклад козявки з HDMI?
Raspberry Pi Zero?
Прийом інформації вже реалізовано - для цього вистачило періоду і тривалості рядкових синхроімпульсів.
Якщо девайс сам обирає коли приймати, тобто є мастером шини, то такий варіант цілком робочий.
Наразі триває боротьба за формування відеосигналу в форматі 640*480*60 Гц
Для атмеги є реалізації, наприклад.
Нажаль, свою реалізацію навряд чи вже знайду, грався з цим років 20 тому.
Є цікаві рішення і на інших платформах.
Розглядали інші варіанти? Будь-яка козявка з апаратним HDMI-контролером значно спростила би реалізацію. А якщо потреба відображати саме на VGA, то можна через готовий перехідник.
Щодо практичної доцільності - то автору видніше. А щодо ненормальності я би посперечався. Якраз такі задачі дають розуміння нутрощів як конкретної платформи, так і в загальному. І розуміння предметної області. Без такого розуміння тільки скетчі ліпити з готових бібліотек за вказівками AI.
Та й порція ендогенного дофамінчику природнім шляхом - це приємно.
Хм. Виводити дані через spi, порахувати такти, щоб наступний байт завантажувався зразу після вивантаження- тоді можна або без розривів, або з мінімальними.
Саме так і реалізується.
Але проблема- пам'яті не вистачить.
Якщо виводити текст, то багато пам'яті не потрібно: шрифт на флешці лежить. За 16 тактів можна багато чого встигнути
Схоже, що це найкраще пояснення того, що відбувається.
Але приведений вами код в такі інструкції не скомпілиться, бо у вас же пишеться весь регістр PORTB. Компілятор не знає, що з нього потрібен тільки один біт, а решта ігнорується. Хіба що значення після кожного зсуву заздалегідь завантажились в 7 регістрів.
Можливо, компілювався схожий, але дещо відмінний від цього код. Потрібно дивитись дизасемблером, що там відбувається насправді.
Blue pill? Так в ньому інша система команд повинна бути
і gpio трохи інакше влаштовано мабуть.
Нє, Blue Pill - то ж наче STM32.
Колись попалась в руки "атмега", корпус явно перемаркований, таймінги дивні, не відповідають даташиту. Деякі фʼюзи не шились, ще якісь дивності з периферією були. Міряв споживання в різних режимах - на атмегу зовсім не схоже, і пульсації струму 72 МГц. Хоча шилась по-AVRівськи і AVRівський байткод виконувала. Потім перестала по ISP відповідати. Думав, може SPIEN фʼюз злетів. Вирішив 12-вольтовим програматором до неї достукатись, вона і згоріла нафіг.
Можна подивитися як влаштована ліба для адресних світлодіодів, там досить швидко біти виводяться.
Для довільних даних швидше як 2 такта на біт все одно не отримати. А якщо чисто програмно, не використовуючи SPI чи UART, то ще час на завантаження саміх даних в регістри.
Мені вдавалось колись генерувати зображення на атмезі, і VGA, і PAL. Але фіксовані фрагменти по пікселю за такт, або довільні дані по 2 такта. При тактовій 20 МГц це було щось біля 300 з чимось пікселів по горизонталі.
І на форумі був пост про генерацію відео, але там було stm32.
Там була генерація апаратним LTDC.
Ще може бути, що ваш компілятор виявився розумніший, і згенерував щось типу
ldi r24,lo8(1)
out 0x5,r24
out 0x5,__zero_reg__
out 0x5,r24
out 0x5,__zero_reg__
out 0x5,r24
out 0x5,__zero_reg__
out 0x5,r24
out 0x5,__zero_reg__
Це 8 біт за 0.5 мкс. Але ж це константні дані. Для знакогенерації чи інших фіксованих картинок ок, для довільного зображення вже так не буде.
Там виводиться байт 0b01010101 молодшим бітом вперед
При константному значенні цей код компілиться в пари ldi/out. Кожна з цих інструкцій на atmega328p виконується за один такт. При 16 МГц один біт за 125 нс, 8 біт за 1 мкс за 937.5 нс.
Як давно осцилограф калібрували?
Або може у вас якийсь китайський емулятор атмеги на 32-бітному ядрі та 72 МГц, таке теж трапляється.
не знаю, як вставити фото екрана осциллографа
Внизу під вікном редагування "Завантаження". Там "Upload a file". Потім відкриваєте завантажений файл, копіюєте його адресу і вставляєте в текст.