Секції .init в avr-gcc
Для програм, написаних мовою С чи С++, часто буває зручно, а іноді просто необхідно проініціалізувати якісь ресурси мікроконтролера до початку роботи фунцкії main()
(для C++ — до початку роботи конструкторів статичних об’єктів). В багатьох системах програмування для цього використовується функція на зразок low_level_init()
, яка викликається з С-шного «пускача» (start-up module, в більшості випадків пишеться на асембелрі) і має бути визначеною десь в проекті. Якщо такої функції нема, тобто програміст її не написав у даному проекті, то з бібліотеки береться коротка «затичка» (stub), яка просто нічого не робить.
У avr-gcc це зроблено дещо по-іншому. Використовуються можливості системи програмування по обробці секцій (сегментів, sections, segments) програми. Вашій увазі пропонується невеличкий приклад з детальним описом.
Коли пишеш на мовах рівня «трохи вищого від найнижчого», деякі деталі реалізації можуть бути просто невідомими. «Не тому, що поняття наші вузькі, а тому, що ці речі не входять в коло наших понять» © Козьма Прутков.
Для тих, хто не знайомий з поняттям секції, коротке пояснення:
Секції — це механізм поділу програми на частини, ортогональний її поділу на файли. Поділ на файли з точки зору компілятора є «фізичним», різні частини програми пишуться в різних файлах. Секції дають «логічний» поділ, однотипні частини з кожного файлу з’єднуються лінкером при збиранні програми для розміщення у відповідних регіонах пам’яті. Як правило це код (секція
.text
), проініціалізовані дані (.data
) та непроініціалізовані дані, які буде занулено при старті програми (.bss
). Далі можуть бути різні доповнення, наприклад, проініціалізовані дані, які не повинні мінятися програмою (.rodata
), тобто змінні, позначені в С кваліфікаторомconst
. У вбудованих системах часто зустрічається секція даних, які програма при статі не повинна ініціалізувати (.noinit
).Таке збирання логічно-схожих фрагментів дозволяє як ефективніше використовувати пам’ять системи, так і забезпечити додатковий контроль за використанням секцій. Наприклад, в персональних комп’ютерах можна код програми помістити в регіон пам’яті, в який заборонено запис, а її дані — в регіон, з якого заборонено виконання програми. При використанні постійних запам’ятовуючих пристроїв (EPROM, Flash)
.text
заноситься в них. Секцію.noinit
можна розмістити в пам’яті, яка має резервне живлення від батареї — в ній опиняться всі такі фрагменти з усіх файлів проекту.Цікаво виходить — поділ на файли, який є «фізичним» з точки зору компілятора, є більше «логічним» з точки зору програміста, «логіка» програми розбивається на модулі, які кодуються в різних файлах. А от поділ на секції .text, .data, .noinit з точки зору програміста якраз «фізичний», описує розміщення елементів програми в різних частинах мікроконтролера.
Механізм збирання секцій з частин, розкиданих по файлах проекту, задіяно в avr-gcc для ініціалізації програми. Існує десять секцій з іменами від .init0
до .init9
, які лінкером збираються у виконуваний файл саме в такому порядку. Частина секцій має фіксоване призначення, частина віддана програмісту для використання в своїй програмі:
.init1
.init2 Обнулення __zero_reg__, встановлення вказівника стеку
.init3
.init4 Ініціалізація .data та обнулення .bss.
.init5
.init6 Конструктори C++.
.init7
.init8
.init9 Виклик main()
Секції без приміток про їх призначення віддано користувачеві.
Є ще «симетричні» секції .fini9
… .fini0
для «виконання» після завершення main()
. В моїх програмах для вбудованих систем ніколи не відбувається виходу з функції main()
, тому для мене вони не цікаві.
Додатково про ці секції можна почитати в документації на avr-libc.
При роботі на асемблері просто оголошується потрібна секція і в ній пишеться код, наприклад, для ввімкнення інтерфейсу зовнішньої пам’яті ще до встановлення вказівника стеку:
#include <avr/io.h>
.list
.section .init1,"ax",@progbits
ldi r16, (1 << SRE)
out _SFR_IO_ADDR(MCUCR), r16
ldi r16, (4 << SRL0) | (1 << SRW00)
sts _SFR_MEM_ADDR(XMCRA), r16
ldi r16, (1 << XMBK)
sts _SFR_MEM_ADDR(XMCRB), r16
Код з секцій .init*
виконується послідовно. Вони просто зливаються так, наче було написано один довгий шматок програми, call
та ret
не використовуються. Якщо якоїсь секції не буде в усіх файлах проекту і вона не прилінкуєтсья з бібліотеки (як фрагменти .init4
для ініціалізації секцій .data
та .bss
), то вона буде просто пропущена у вихідному файлі лінкера.
C оперує поняттям функції, написати С-код інакше, як в тілі якоїсь функції, не вийде. Але gcc має великий набір атрибутів для функцій. Ми можемо оголосити функцію naked
(без зберігання/відновлення використаних регістрів та без команди ret
в кінці) та розмістити її в потрібній секції:
void init_xmem_interface(void) __attribute__ ((section (".init1"), naked));
void init_xmem_interface(void)
{
MCUCR = (1 << SRE);
XMCRA = (4 << SRL0) | (1 << SRW00);
XMCRB = (1 << XMBK);
}
Отже, в avr-gcc ми можемо мати будь-яку кількість функцій типу low_level_init()
та ще й керувати порядком їх виконання відносно інших дій startup-коду та між собою. Цей механізм фактично дає можливість доповнювати функціонал startup-коду без його переписування — кожен модуль програми може додати до startup свій код ініціалізації. Для цього у файлі крім функцій, які викликатимуться з інших модулів програми, можна розмістити ще одну чи навіть декілька init-функцій. Просте підключення модуля до програми автоматично згенерує код його ініціалізації. Тобто цей механізм подібен до конструкторів статичних об’єктів C++ (які самі ж ним і користуються, дивись секцію .init6
).
До цього повідомлення прикріплено проект для Code::Blocks та avr-gcc, в якому в секції .init5
ініціалізується модуль UART і в кінці цієї ініціалізації на термінал видається текстовий рядок (файл src/uart/uart.c):
21 22 23 24 25 26 27 28 29 30 | INIT(5) { UCSR0A = 1 << U2X0; UCSR0C = (1<<UCSZ01) | (1<<UCSZ00); UBRR0L = (uint8_t) BAUD_DIVIDER(UART0_BAUD,1); UBRR0H = (uint8_t) (BAUD_DIVIDER(UART0_BAUD,1) >> 8); UCSR0B = (1<<RXEN0) | (1<<TXEN0); DRIVER(TXD0,OUT); uart_putstr_P(PSTR("\nUART module is initialised\n")); } |
Все це виконується до початку роботи main()
, ба більше — код, розміщений в секціях з .init6
по .init8
вже зможе користуватися видачею на термінал.
У наведеному фрагменті використовується макрос INIT() — коли починаєш користуватися такою ініціалізацією більш-менш інтенсивно, виникає зрозуміле бажання менше писати і перестати видумувати функціям ініціалізації унікальні в проекті імена. Ось я і написав колись відповідні макроси (вони знаходяться у файлі src/c_lib/avr/avrgcc_macros.h прикладу).
81 82 83 84 85 86 87 | #define INIT_CODE(_number_,_name_) \ __attribute__ ((section(".init" #_number_), naked, used)) \ static void init_##_number_##_##_name_ (void) #define INIT_CODE_(n,l) INIT_CODE(n,l) #define INIT(_number_) INIT_CODE_(_number_,__LINE__) |
В рядку 81 задається макрос, який приймає номер init-секції та ім’я для функції. Єдиний сенс в унікальних в межах проекту іменах — можливість швидко знайти потрібний фрагмент у дизасембльованому .elf. На практиці я використовую лише макрос INIT()
з 87-го рядка. Цей макрос приймає лише номер ініт-секції, а унікальне в межах файлу ім’я отримується з номера рядка, в якому цей макрос було використано. Необхідність в унікальності в межах проекту знімається оголошенням функції як static
. Після цього компілятор намагається викинути функцію, бо не бачить її виклику в межах файлу, це ми йому забороняємо атрибутом used
.
З використанням макросу наведений приклад ініціалізації інтерфейсу зовнішньої пам’яті запишеться так:
INIT(1)
{
MCUCR = (1 << SRE);
XMCRA = (4 << SRL0) | (1 << SRW00);
XMCRB = (1 << XMBK);
}
Невеликі зауваження:
- Як згадано в параграфі Using Sections in C Code, С-код, який може залежати від обнулення регістра
__zero_reg__
, слід розміщувати в ініт-секціях з номером, більшим за 2, бо лише в секції.init2
виконується обнулення цього регістра. - Так само лише в секції
.init2
виконуєтсья ініціалізація вказівника стеку. Необхідно слідкувати, щоб занесені в секцію.init1
функції не використовували змінних на стекові та щоб з них не викликалися інші функції.
Обговорення теми та інші приклади вкористання .init-секцій можна знайти на форумі electronix
bss и avr-gcc
Atmega1280 + внешняя SRAM
уникальный идентификатор
Такий механізм використовується досить часто. Наприклад, в компіляторах Borland C спеціальні прагми — #pragma startup NUMBER
та #pragma exit NUMBER
— заносять вказівник на функцію та її «пріоритет» NUMBER
в секції для виконання до запуску main() та після виходу з неї. Саме таким механізмом, як вже було сказано, користується C++ для «виклику» конструкторів та деструкторів статичних об’єктів.
І цей механізм можна використати для власних потреб, створюючи необхідні секції та викориcтовуючи можливості лінкера. Але про це — в наступній статті, присвяченій використанню секції в gcc.
Як завжди — якщо комусь щось не ясно по цій чи суміжній темі, можна спитати тут.
© Олександр Редчук aka ReAl, 2011 Цей твір ліцензовано за ліцензією Creative Commons Із зазначенням автора – Розповсюдження на тих самих умовах 3.0 Неадаптована. |
Attached Files:
- avr-gcc .init* sections demo
avr-gcc, ATmega168. Code::Blocks project with external Makefile. Can be used from command line or with other IDE