AVR4027 Трюки та поради щодо оптимізації Сі коду для 8-и розрядних AVR мікроконтролерів

- Введення в ядро ​​Atmel AVR і Atmel AVR GCC - Поради та трюки щодо зменшення розміру коду - Поради та трюки щодо зменшення часу виконання коду - Приклади застосування

1. Введення

AVR ядро ​​засноване на просунутій RISC архітектурі, оптимізованої для Сі коду. Це дозволяє розробляти хороші та дешеві продукти з широкою функціональністю.

Коли йдеться про оптимізацію, ми зазвичай маємо на увазі дві речі: розмір коду та швидкість його виконання. В даний час компілятори Сі мають різні варіанти оптимізації, що дозволяють розробникам отримувати ефективний код за одним з цих критеріїв.

Хороший Сі код дає компілятору більше можливості для його оптимізації. Однак у деяких випадках оптимізація коду за одним із критеріїв погіршує інший, тому розробник повинен шукати баланс між ними для задоволення своїх вимог. Розуміння деяких нюансів програмування на Сі для AVR дозволяє розробникам фокусувати свої зусилля у потрібному напрямку для досягнення ефективного коду.

У цій статті ми розглянемо рекомендації щодо програмування на Сі для компілятора avr-gcc. Однак ці поради можуть бути використані з іншими компіляторами.

2. Atmel AVR ядро ​​та Atmel AVR GCC

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

2.1 Архітектура 8-розрядного AVR

AVR використовує Гарвардську архітектуру з роздільною пам'яттю та шиною для програми та даних. Він має регістровий файл з 32 8-розрядних робочих регістрів загального призначення з часом доступуодин тактовий цикл. 32 робочих регістру – одне із ключів до ефективного Сі програмування. Ці регістри мають такі самі функції, як і традиційні акумулятори, тільки їх 32 штуки. За один такт AVR може передати два довільні регістри арифметичного логічного пристрою, виконати операцію та записати результат назад у регістровий файл.

Для більш детальної інформації прочитайте розділ “AVR CPU Core” у документації на мікроконтролери.

2.2 AVR GCC

GCC розшифровується як колекція компіляторів GNU (GNU Compiler Collection). GCC використовується для AVR мікроконтролерів називається AVR GCC.

AVR GCC має кілька рівнів оптимізації: -O0, -O1, -O2, -O3 та -Os. До кожного рівня дозволено свої параметри. Виняток становить рівень -O0 – для нього оптимізація повністю вимкнена. Крім дозволених для кожного рівня опцій, також можна включати окремі параметри оптимізації, щоб отримати специфічну обробку коду.

Для отримання повного списку параметрів і рівнів оптимізації зверніться до посібника з компіляторів GNU. Посібник можна знайти за посиланням нижче.

Крім компілятора, AVR GCC включає інші інструменти, які працюють разом для отримання кінцевого виконуваного додатка для AVR мікроконтролера. Ця група інструментів називається “Турчейном” (toolchain). Важливу функцію в цьому AVR тюрті виконує AVR-Libс, яка забезпечує функції стандартної Сі бібліотеки, а також безліч додаткових специфічних для AVR функцій і базовим стартовим кодом (startup code). Посібник на AVR Libc ви можете знайти за посиланням нижче.

2.3 Платформа розробки

Всі приклади цього документа тестувалися з використанням наступних інструментів: 1. Інтегроване середовищеРозробка: Atmel AVR Studio 5 (версія 5.0.1119) 2. AVR GCC тулчейн: AVR_8_bit_GNU_Toolchain_3.2.1_292 (gcc версії 4.5.1) 3. Мікроконтролер: Atmel ATmega88PA.

3. Поради та трюки щодо зменшення розміру коду

У цьому розділі ми перерахуємо деякі рекомендації щодо зменшення розміру коду. Для кожної поради буде дано опис та приклад.

3.1 Порада #1 - типи даних та розміри

Використовуйте найменший тип даних, здатний представити вашу змінну. Для читання 8-розрядного регістра, наприклад, не потрібно використовувати 2-байтну змінну, досить однобайтною.

Розміри типів даних для AVR можна переглянути в заголовку stdint.h і в таблиці 3-1.

Таблиця 3-1. Типи даних для AVR, описані в stdint.h

Майте на увазі, що деякі опції компілятора можуть вплинути на типи даних (наприклад, AVR-GCC має опцію –mint8, яка робить тип int 8-розрядним).

Два приклади коду таблиці 3-2 показують результат використання різних типів даних. Також у таблиці показані розміри коду, одержуваного під час побудови проекту з опцією оптимізації –Os (оптимізація для розміру).

Таблиця 3-2. Приклад використання різних типів даних.

коду

У лівому прикладі ми користуємося 2-х байтним типом даних для тимчасової змінної та значення, що повертається. У правому прикладі натомість використовується однобайтний тип char. Зчитування даних відбувається з 8-розрядного регістра ADCH, а значить такого розміру змінної цілком достатньо. Це дозволяє нам заощадити 2 байти пам'яті.

Зверніть увагу, що до запуску функції main() є початковий код ініціалізації (startup code), тому ці прості приклади Сі коду займають 90 байт.

3.2 Рада #2 - глобальні змінні та локальнізначення

Найчастіше не рекомендується використовувати глобальні змінні. Застосовуйте локальні змінні скрізь, де це можливо. Якщо змінна використовується лише у функції, її слід оголошувати всередині функції як локальну змінну.

Теоретично вибір між глобальними і локальними змінними повинен визначатися тим, як вона використовується.

Локальні змінні зазвичай розміщуються в регістрах або стеку. Коли функція викликається, локальні змінні задіяні. Коли функція завершує роботу, локальні змінні можуть бути видалені.

Два приклади у таблиці 3-3 показують ефект застосування глобальних та локальних змінних.

Таблиця 3-3. Приклад глобальних та локальних змінних.

поради

У лівому прикладі ми оголошуємо однобайтову глобальну змінну. Avr-size утиліта показує, що ми використовуємо 104 байти пам'яті програм і один байт пам'яті даних при оптимізації Os.

У правому прикладі ми оголошуємо локальну змінну всередині main() функції і код зменшується до 84 байтів, а оперативна пам'ять не використовується зовсім.

3.3 Порада #3 - індекс циклу

Цикли широко використовуються під час програмування мікроконтролерів. У Сі існують три типи циклів – “while()<>”, “for()<>“ та “do<>while()”. Якщо увімкнено оптимізацію –Os, компілятор оптимізуватиме цикли автоматично, щоб зменшити розмір коду. Однак ми можемо самі трохи зменшити його.

Якщо використовується цикл “do<>while()”, то інкремент та декремент індексної змінної циклу даватиме код різного розміру. Зазвичай ми використовуємо інкременти, щоб підраховувати кількість циклів від нуля до максимального значення. Проте ефективніше робити навпаки – рахувати від максимальногозначення до нуля, тобто використовувати декремент.

При інкременті, у кожній ітерації циклу необхідно виконувати інструкцію порівняння індексної змінної з максимальним значенням. Коли ми використовуємо декремент, ця інструкція не потребує. Щойно індексна змінна досягне нуля, у регістрі SREG встановиться прапор Z.

У таблиці 3-4 наведено приклади коду, що використовує цикл “do <>while()” з інкрементом та декрементом індексної змінної.

Таблиця 3-4. Приклад do<>while() циклів з інкрементом і декрементом індексної змінної

трюки

Щоб мати прозоріший Сі код, ми записали цей приклад як “dowhile(count);”, а не “do<>while(count--);”, як зазвичай пишуть у книгах по Сі. Однак у обох випадках розмір коду буде однаковим.

3.4 Рада #4 – об'єднання циклів

Іноді цикли реалізовані однотипно і це може спричинити довгий список ітерацій. Об'єднавши вирази та оператори цих циклів в один, ми можемо зменшити загальну кількість циклів у коді. При цьому зменшиться як розмір коду, так і час виконання. У таблиці 3-5 можна бачити приклад використання об'єднання циклів.

Таблиця 3-5. Приклад об'єднання циклів.

щодо

3.5 Порада #5 – константи у програмній пам'яті

Глобальні змінні, таблиці та масиви, які ніколи не змінюються, повинні розміщуватись у флеш пам'яті мікроконтролера. Це дозволяє заощаджувати оперативну пам'ять.

Щоб розмістити дані в програмній пам'яті та зчитувати їх звідти, AVR-Libs надає макроси “PROGMEM” та “pgm_read_byte”. Ці макроси визначені у заголовному файлі pgmspace.h. Приклад таблиці 3-6 показує, як можна зберегти ОЗУ, перемістивши рядок у програмну пам'ять.

Таблиця3-6. Приклад констант у програмній пам'яті.

avr4027

Після того, як ми помістили константи в програмну пам'ять, зменшилося і її використання та ОЗУ. Однак з'явилися невеликі накладні витрати, пов'язані з читанням даних, оскільки читання з флеш пам'яті виконується повільніше, ніж з оперативної пам'яті.

Якщо дані, що зберігаються у флеш пам'яті, використовуються в коді кілька разів, ми можемо отримати менший код, використовуючи додаткову тимчасову змінну замість багаторазового виклику макросу pgm_read_byte.

У заголовному файлі pgmspace.h є кілька макросів і функцій для збереження та читання різних типів даних у пам'яті програм. Для отримання більш детальної інформації зверніться до посібника на avr-libc.

3.6 Порада #6 – типи доступу: static

Для глобальних даних використання ключового слова static не завжди можливе. Якщо глобальні змінні оголошені з ключовим словом static, вони можуть бути доступні лише у файлі, у якому вони визначені. Це запобігає випадковому використанню змінної в інших файлах.

Статичними можна оголошувати також функції. У цьому випадку область видимості функції теж буде обмежена файлом, в якому вона оголошена, і викликати її можна буде тільки звідти. З цих причин компілятор простіше оптимізувати такі функції.

Якщо статична функція викликається у файлі лише один раз і оптимізація дозволена (-O1, -O2, -O3, -Os), компілятор зробить цю функцію вбудованої.

Таблиця 3-7. Приклад використання типів доступу – статична функція.

поради

Зверніть увагу, якщо функція викликається кілька разів, вона не буде вбудовуватися, тому що це збільшить код більше, ніж прямий виклик функції.

3.7 Порада #7 – низькорівневі асемблерні інструкції

Асемблерні команди завжди кращі за оптимізований код. Єдиний недолік асемблерного коду - це нестерпний синтаксис, так що це в більшості випадків його не рекомендується використовувати.

Щоб покращити читання та портованість коду, можна використовувати асемблерні макроси. Такими макросами можна замінювати функції, які генеруються в 2-3 асемблерні рядки. У таблиці 3-8 показаний приклад використання асемблерного макросу замість функції.

Таблиця 3-8. Приклад використання низькорівневих асемблерних інструкцій.

щодо

Для більш детальної інформації щодо використання асемблера з мовою Сі на AVR, ознайомтеся з розділом “Inline Assembler Cookbook” у посібнику avr-libc.