Секції .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, які лінкером збираються у виконуваний файл саме в такому порядку. Частина секцій має фіксоване призначення, частина віддана програмісту для використання в своїй програмі:

   .init0  Звідси починається виконання програми, якщо не замінено мітку __init.
   .init1
   .init2  Обнулення __zero_reg__, встановлення вказівника стеку
   .init3
   .init4  Ініціалізація .data та обнулення .bss.
   .init5
   .init6  Конструктори C++.
   .init7
   .init8
   .init9  Виклик main()

Секції без приміток про їх призначення віддано користувачеві.

Є ще «симетричні» секції .fini9.fini0 для «виконання» після завершення main(). В моїх програмах для вбудованих систем ніколи не відбувається виходу з функції main(), тому для мене вони не цікаві.
Додатково про ці секції можна почитати в документації на avr-libc.

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

.nolist
#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 в кінці) та розмістити її в потрібній секції:

#include <avr/io.h>

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.

З використанням макросу наведений приклад ініціалізації інтерфейсу зовнішньої пам’яті запишеться так:

#include <avr/io.h>

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.


Як завжди — якщо комусь щось не ясно по цій чи суміжній темі, можна спитати тут.


Ліцензія Creative Commons © Олександр Редчук aka ReAl, 2011
Цей твір ліцензовано за ліцензією Creative Commons Із зазначенням автора – Розповсюдження на тих самих умовах 3.0 Неадаптована.

Attached Files:

Leave a Reply

[flagcounter image]