середу, 31 жовтня 2012 р.

CooCox CoIDE

Про оболонку CoIDE від CooCox вже згадувалося у попередньому пості. Вона проста та зручна, і вартує уваги, особливо для початківців у роботі з контролерами ARM Cortex-M. Поміж недоліків -- "Windows only", відсутність підтримки C++ "з коробки", при чому останнє легко виправити (хоча б частково).

Думаю, варто розповісти про неї детальніше. Щоправда, базові аспекти роботи, з одного боку, менш-більш інтуїтивні, з іншого, описані:
  • Частина статті "STM32: Урок 1 - Настраиваем IDE" від Robocraft. В основному вона про підготовку власного середовища на базі Eclipse, але зачіпає і роботу з CoIDE. Навіть якщо поки що самостійно "збирати" середовище не планується, варто її прочитати, щоб краще розуміти механізми роботи IDE, їх взаємодію з компілятором та програматорами
  • "Необходимый софт" від EasySTM32 -- налаштування і робота з CoIDE. Створення проекту, компіляція, прошивка
Тому нижче зосереджуся на деяких нюансах -- прикручування підтримки С++, робота із репозиторієм,  всілякі дрібні потенційні "граблі". Розглядаю CoIDE останньої,  на момент написання, версії, 1.5.1.

[Update від листопаду 2015 року. Частина, присвячена С (Newlib) та С++ runtime містить багато неточностей та помилок. Існує більш детальний та точний варіант: "Стандартна бібліотека C та SemiHosting (на прикладі STM32 і CoIDE)" і "C++ із ARM GCC + STM32 (+ CoIDE)", відповідно. Крім того, див. пост про нову версію CoIDE, 2.0.3 beta: "CoIDE 2".]


Інсталяція

Оболонку можна скачати тут. Компілятор, (gcc), слід заінсталювати окремо. Для ARM існує декілька його збірок, із різним набором комплектації допоміжними засобами. Кілька слів про вибір збірки є на сайті IDE. По замовчуванню оболонка заточена під ARM GCC, тому краще зупинитися саме на ній. 

Налаштування, в значній мірі, зводиться до вказання шляху до папки bin вибраного "toolchain"-а в Project --> "Select Toolchain Path". (Скріншоти див. у статтях, згаданих вище, а краще взагалі запустити самостійно).

Альтернативний спосіб інсталяції, рекомендований розробниками -- скористатися їхнім менеджером пакетів, CoCenter. Крім власне оболонки, він дозволяє інсталювати та оновлювати інші програмні розробки CooCox, зокрема, програму для роботи з програматорами CoFlash, і їхню вбудовувану операційну систему CoOS.
Зауваження: під час оновлення CoIDE сам CoCenter має завершитися, однак іноді він залишається в пам'яті і оновлення не може відбутися. В такому випадку слід його припильнувати і прибити вручну,  Task Manager-ом.

Створення проекту та робота з ним

  1. Project --> "New Project". Питається ім'я проекту, яке буде іменем його директорії, тому варто вибирати без пробілів, і обмежитися лише латинськими літерами, цифрами, дефісом та підкресленням. Пропонує помістити його в папку <CoIDE_root>/CoIDE/workspace. Звичайно, краще вибрати більш адекватне місце, групуючи проекти по завданням, що вирішуються, інакше скоро в "workspace" збереться цілий смітник між собою не пов'язаних проектів.
  2. Оболонка запропонує вибрати (із репозиторію) виробника контролера, з яким буде працювати проект.
  3. Після вибору виробника слід вибрати конкретний мікроконтролер. (Наприклад, якщо працювати з платою STM32VLDiscovery, то виробник ST(Microelectronics), контролер STM32F100RB).
  4. Наступний крок -- вибір компонент, з якими працюватиме проект. Поміж них CMSIS, частини SPL для роботи із різноманітною периферією, бібліотека периферії від CooCox, CoOS, semihosting, стандартна C-бібліотека на базі Newlib, (також див. короткий опис з прикладами реалізації низькорівневих системних функцій на OSDev), користувацькі компоненти, тощо. Залежності між компонентами задовольняються автоматично. Наприклад, якщо вибрати "GPIO", в проект додадуться також "CMSIS core", "CMSIS boot" (про цю компоненту див. нижче), "RCC". Короткий опис компонент виводиться в панелі справа. Доступні в репозиторії ще дві цікаві вкладки - "Drivers", з драйверами для зовнішніх пристроїв та "Others" з різними корисними компонентами, типу драйвера файлової системи FAT. Увага, компоненти можуть затерти ваші файли в проекті! Особливо цим грішать всілякі шаблони від сторонніх розробників (main.c затерти - тільки так). Так само, видалення компоненти із проекту без попередження стирає її файли, навіть якщо вони були змінені.
  5. Проект створено, можна приступати до редагування. По замовчуванню створюється файл main.c, який за бажання можна перейменувати. Компіляція: Project --> Build, іконка в тулбарі, або просто F7. Прошивка в контролер: "Flash"-->"Program download". Якщо програматор підтримує (ST-Link на STM32VLDiscovery підтримує), працює і відладка -- breakpoint-и, покрокове виконання, перегляд змінних, тощо. Єдине, потрібно вказати, з яким програматором працювати: "Debug" --> "Debug configuration". Список підтримуваних програматорів великий. Якщо працювати із описаною в попередньому пості STM32VLDiscovery, слід вибрати ST-Link, Port: SWD, Max Clock -- 2M. Якщо планується використання semihosting, тут же можна його дозволити: "Semihosting Enable". На жаль, це налаштування не може бути глобальним -- потрібно повторювати у кожному проекті. Завжди можна повернутися до репозиторію і додати/прибрати компоненти. Звичайно, якщо забрати компоненти, які використовуються у вашому коді, він перестане компілюватися або лінкуватися. Тип контролера, щоправда, так просто змінити не можна -- простіше створити новий проект.

Організація проекту


Розглянемо, на прикладі нового проекту, в який було додано GPIO (разом із залежностями). 

1 -- компоненти, які використовуються. Зверніть увагу на приклади до них. Клікнувши на компоненті можна їх проглянути.

2-9 -- файли і папки, що знаходяться у папці проекту. Розглянемо їх детальніше.

8 -- найпростіше, власне файл нашої програми.

4 -- об'єктні файли та бінарники скомпільованого проекту. Варто звернути увагу на test_project_1.txt  -- асемблерний лістинг, test_project_1.map -- карту пам'яті, test_project_1.elf -- те, що прошиватиметься в контролер та history.xml  -- історію компіляції.

2 -- файли CMSIS, додані компонентою "CMSIS core". Нагадую, для простоти, більшість бібліотек для контролерів постачаються у вигляді вихідних текстів і компілюються разом із кодом програми. Зокрема, CoIDE автоматично копіює їх в папку проекту.

5 -- файли SPL.

Група 3, створена компонентою CMSIS_boot, хитріша -- у ній зібрано трішки різнорідні файли. stm32f10x.h, system_stm32f10x.h, system_stm32f10x.c -- частина CMSIS, що стосується вибраного нами контролера.

system_conf.h -- конфігурація для CMSIS. Зокрема, містить заготовку для ввімкнення перевірки параметрів виклику функцій:

/* Uncomment the line below to expanse the "assert_param" macro 
in the Standard Peripheral Library drivers code */
  /* #define USE_FULL_ASSERT    1 */


Нарешті, найбільш важливий із автоматично створених файлів: startup_stm32f10x_md_vl.c. Це файл ініціалізації контролера. Перш ніж перейти до виконання функції main() потрібно виконати певну підготовчу роботу. Різні оболонки організовують її по різному! Зокрема, згаданий startup_stm32f10x_md_vl.c від CoIDE по замовчуванню робить наступне:

1. Виділяє пам'ять для стеку, вказує лінкеру секцію, в яку виділену пам'ять помістити:

/*----------Stack Configuration-----------------------------------------------*/  
#define STACK_SIZE       0x00000100      /*!< Stack size (in Words)           */
__attribute__ ((section(".co_stack")))
unsigned long pulStack[STACK_SIZE];

Розмір по замовчуванню може бути недостатнім, особливо коли використовуються функції типу printf. 

2. Оголошує таблицю переривань із обробниками по замовчуванню (щоб не займати багато місця одноманітними простирадлами тексту, всюди нижче наводжу лише її частину). Робиться це, на перший погляд, заплутано, але завдяки тому простіше писати власне програму.  Спершу обробники декларуються як weak символи ("слабкі")  -- вони будуть замінені, якщо символ з таким же іменем трапиться в програмі. Тобто, щоб написати свій обробник переривань, наприклад для USART3, достатньо оголосити в програмі функцію з іменем USART3_IRQHandler, яка не приймає параметрів і нічого не повертає. Вона замінить "слабкий" символ, (який, як показано нижче, посилається на обробник по замовчуванню). Увага: інші оболонки-IDE можуть називати ці обробники трішки по іншому -- див. відповідну їх документацію.

/*----------Macro definition--------------------------------------------------*/  
#define WEAK __attribute__ ((weak))           


/*----------Declaration of the default fault handlers-------------------------*/  
/* System exception vector handler */
void WEAK  Reset_Handler(void);
void WEAK  NMI_Handler(void);
void WEAK  HardFault_Handler(void);
void WEAK  MemManage_Handler(void);
void WEAK  BusFault_Handler(void);
void WEAK  UsageFault_Handler(void);
/*---------- Skipped ---------------------*/ 
/*---------- Skipped ---------------------*/
void WEAK  USART3_IRQHandler(void);
void WEAK  EXTI15_10_IRQHandler(void);
void WEAK  RTCAlarm_IRQHandler(void);
void WEAK  CEC_IRQHandler(void); 
void WEAK  TIM6_DAC_IRQHandler(void);
void WEAK  TIM7_IRQHandler(void);

Декларуються решта важливих функцій -- main (власне, програма), SystemInit з CMSIS, обробник переривання reset -- початкова ініціалізація, обробник всіх інших переривань по замовчуванню:
.
 
 /*----------Function prototypes-----------------------------------------------*/  
extern int main(void);           /*!< The entry point for the application.    */
extern void SystemInit(void);    /*!< Setup the microcontroller system(CMSIS) */
void Default_Reset_Handler(void);   /*!< Default reset handler                */
static void Default_Handler(void);  /*!< Default exception handler            */

Виділяється пам'ять для таблиці переривань:

/**
  *@brief The minimal vector table for a Cortex M3.  Note that the proper constructs
  *       must be placed on this to ensure that it ends up at physical address
  *       0x00000000.  
  */
__attribute__ ((section(".isr_vector")))
void (* const g_pfnVectors[])(void) =
{       
  /*----------Core Exceptions-------------------------------------------------*/
  (void *)&pulStack[STACK_SIZE-1],     /*!< The initial stack pointer         */
  Reset_Handler,                /*!< Reset Handler                            */
  NMI_Handler,                  /*!< NMI Handler                              */
  HardFault_Handler,            /*!< Hard Fault Handler                       */
  MemManage_Handler,            /*!< MPU Fault Handler                        */
  BusFault_Handler,             /*!< Bus Fault Handler                        */
/*---------- Skipped ---------------------*/ 
/*---------- Skipped ---------------------*/ 
  USART3_IRQHandler,            /*!< 39: USART3                               */
  EXTI15_10_IRQHandler,         /*!< 40: EXTI Line 15..10                     */
  RTCAlarm_IRQHandler,          /*!< 41: RTC Alarm through EXTI Line          */
  CEC_IRQHandler,               /*!< 42: HDMI-CEC                             */  
  0,0,0,0,0,0,                  /*!< Reserved                                 */
  0,0,0,0,0,                    /*!< Reserved                                 */ 
  TIM6_DAC_IRQHandler,           /*!< 54: TIM6 and DAC underrun                */
  TIM7_IRQHandler,               /*!< 55: TIM7                                 */
  (void *)0xF108F85F            /*!< Boot in RAM mode                         */
};

Перший її елемент -- початок стеку, далі -- адреси обробників відповідних переривань. Після запуску вона повинна знаходиться за нульовою адресою -- там її шукатиме контролер, однак потім її можна перемістити за допомогою регістра VTOR. Власне, функція SystemInit з system_stm32f10x.c це і робить (наприклад, так: SCB->VTOR = FLASH_BASE | VECT_TAB_OFFSET). Щоб наведена вище структура потрапила саме за цією адресою, вказується секція .isr_vector. Розташування цієї секції в пам'яті задається скриптом лінкера link.ld (див. далі).

Щоб всі обробники вказували на той же обробник по замовчуванню, використовується компілеро-залежний трюк:

/**
  *@brief Provide weak aliases for each Exception handler to the Default_Handler. 
  *       As they are weak aliases, any function with the same name will override 
  *       this definition.
  */
#pragma weak Reset_Handler = Default_Reset_Handler  
#pragma weak NMI_Handler = Default_Handler
#pragma weak HardFault_Handler = Default_Handler
#pragma weak MemManage_Handler = Default_Handler
#pragma weak BusFault_Handler = Default_Handler
/*---------- Skipped ---------------------*/ 
/*---------- Skipped ---------------------*/ 
#pragma weak USART3_IRQHandler = Default_Handler
#pragma weak EXTI15_10_IRQHandler = Default_Handler
#pragma weak RTCAlarm_IRQHandler = Default_Handler
#pragma weak CEC_IRQHandler = Default_Handler
#pragma weak TIM6_DAC_IRQHandler = Default_Handler
#pragma weak TIM7_IRQHandler = Default_Handler

3. Оголошуються адреси різних областей пам'яті, які формуватимуться лінкером:

/*----------Symbols defined in linker script----------------------------------*/  
extern unsigned long _sidata;    /*!< Початкова адреса для значень, якими
                                      ініціалізуватиметься секція .data       */
extern unsigned long _sdata;     /*!< Початкова адреса секції .data           */    
extern unsigned long _edata;     /*!< Кінцева адреса секції .data             */    
extern unsigned long _sbss;      /*!< Початкова адреса секції .bss            */
extern unsigned long _ebss;      /*!< Кінцева адреса секції .bss              */      
extern void _eram;               /*!< Кінець оперативної (RAM) пам'яті        */

4. Задається обробник по замовчуванню для всіх переривань (крім reset):

/**
  * @brief  This is the code that gets called when the processor receives an 
  *         unexpected interrupt.  This simply enters an infinite loop, 
  *         preserving the system state for examination by a debugger.
  * @param  None
  * @retval None  
  */
static void Default_Handler(void) 
{
  /* Go into an infinite loop. */
  while (1) 
  {
  }
}
який просто входить в безмежний цикл.

5. Задається функція початкової ініціалізації:

/**
  * @brief  This is the code that gets called when the processor first
  *         starts execution following a reset event. Only the absolutely
  *         necessary set is performed, after which the application
  *         supplied main() routine is called. 
  * @param  None
  * @retval None
  */
void Default_Reset_Handler(void)
{
  /* Initialize data and bss */
  unsigned long *pulSrc, *pulDest;

  /* Copy the data segment initializers from flash to SRAM */
  pulSrc = &_sidata;

  for(pulDest = &_sdata; pulDest < &_edata; )
  {
    *(pulDest++) = *(pulSrc++);
  }
  
  /* Zero fill the bss segment.  This is done with inline assembly since this
     will clear the value of pulDest if it is not kept in a register. */
  __asm("  ldr     r0, =_sbss\n"
        "  ldr     r1, =_ebss\n"
        "  mov     r2, #0\n"
        "  .thumb_func\n"
        "  zero_loop:\n"
        "    cmp     r0, r1\n"
        "    it      lt\n"
        "    strlt   r2, [r0], #4\n"
        "    blt     zero_loop");
  
  /* Setup the microcontroller system. */
  SystemInit();
    
  /* Call the application's entry point.*/
  main();
}
яка копіює початкові значення ініціалізованих змінних (секція .data), заповнює неініціалізовані змінні нулями (секція .bss), викликає функцію ініціалізації SystemInit з CMSIS. Потім,  нарешті, викликається main(), запускаючи основну програму. Увага, інші компілятори/оболонки можуть проводити ініціалізацію по іншому, наприклад, не зануляти неінінціалізовані змінні з секції .bss.

Важливо: редагувати цей файл можна, іноді навіть треба, однак додавання-видалення компонент може затерти його!

Файли 7 і 9 на рисунку вище, link.ld i memory.ld
Це скрипти лінкера. Детальніше про те, що це таке, для чого потрібне, і т.д., можна почитати, наприклад, на OSDev. Тут коротко розглянемо лише вміст конкретних скриптів, згенерованих CoIDE. Головний, link.ld:

/* Оголошення форматів, що підтримуються */
OUTPUT_FORMAT ("elf32-littlearm", "elf32-bigarm", "elf32-littlearm")
/* Додати поточний каталог до шляху пошуку бібліотек */
SEARCH_DIR(.)
/* Включається файл, який задає карту пам'яті, див. далі */
INCLUDE "memory.ld"

/* Оголошення секцій пам'яті */ 
SECTIONS 
{ 
    .text : 
    { 
        /* Вказує лінкеру зібрати всі секції .isr_vector.* за цією адресою 
         * Ця інструкція перша, тому вони попадуть на початок Flash (ROM),
         * де, завдяки SystemInit, процесор і шукатиме таблицю переривань*/
        KEEP(*(.isr_vector .isr_vector.*)) 
        /* Програмний код -- секція .text */
        *(.text .text.* .gnu.linkonce.t.*) 
        /* Секція, потрібна для зв'язку ARM i Thumb інструкцій.
         * Так як котролер підтримує лише Thumb, не потрібна, але IDE її 
         * створює для порядку. Детальніше див., наприклад, 
         * http://gcc.gnu.org/ml/gcc-help/2009-03/msg00306.html */          
        *(.glue_7t) *(.glue_7)                        
        /* Секція констант */
        *(.rodata .rodata* .gnu.linkonce.r.*)                                  
    } > rom /* Інструкція лінкеру всі ці секції помістити в Flash (ROM) */
    
    .ARM.extab : 
    {
    /* Інформація для розгортання виключень (unwinding exceptions) */
        *(.ARM.extab* .gnu.linkonce.armextab.*)
    } > rom
    
    .ARM.exidx :
    {   
    /* Інформація для розгортання виключень (unwinding exceptions) */
        *(.ARM.exidx* .gnu.linkonce.armexidx.*)
    } > rom
    /* Див., наприклад, ELF for the ARM Architecture - SimpleMachines, 
     * http://simplemachines.it/doc/aaelf.pdf */
    
    /* Вирівняти адресу після попередніх секцій на 4 байти */
    . = ALIGN(4); 
    /* Зберігається кінець пам'яті коду */
    _etext = .;
    /* Зберігається початок даних для ініціалізації змінних */
    _sidata = .; 
            
    /*  Секція .data -- ініціалізованих змінних.
    Початкові значення збережено в ROM за адресою _etext,
    але самі змінні будуть в RAM, із відповідними адресами!
    */
    .data : AT (_etext) 
    { 
    /* Адреса початку секції -- в змінну _sdata*/
        _sdata = .; 
    /* Зібрати все, оголошене в коді як .data */
        *(.data .data.*) 
    /* Вирівняти адресу кінця секції на 4 байти */
        . = ALIGN(4); 
    /* Адреса кінця секції, з врахуванням вирівнювання, в _edata */
        _edata = . ;        
    } > ram /* Секцію помістити в RAM */

    /* .bss -- неініціалізовані дані. _sbss і _ebss 
     * аналогічно до _sdata і _edata */ 
    .bss (NOLOAD) : 
    {         
        _sbss = . ; 
        *(.bss .bss.*) 
        *(COMMON) 
        . = ALIGN(4); 
        _ebss = . ; 
    } > ram
    
    /* Секція стеку */
    .co_stack (NOLOAD):
    {
        . = ALIGN(8);
    /* Зібрати все, оголошене в коді як .co_stack */
        *(.co_stack .co_stack.*)
    } > ram
       
    . = ALIGN(4); 
    /* Кінець задіяної пам'яті */
    _end = . ; 
}

і карта пам'яті для конкретного мікроконтролера, memory.ld:

/**
  *******************************************************************
  * Chip: STM32F100RB
  * NOTE: This file is generated by CoIDE and Included by *link.ld
  *       It is used to describe which memory regions may be used
  *       by the linker.
  *******************************************************************
  * Internal memory map
  *   Region      Start           Size
  *   flash0      0x08000000      0x00020000
  *   sram0       0x20000000      0x00002000
  *******************************************************************
*/

MEMORY
{
    rom (rx)  : ORIGIN = 0x08000000, LENGTH = 0x00020000
    ram (rwx) : ORIGIN = 0x20000000, LENGTH = 0x00002000
}

_eram = 0x20000000 + 0x00002000;


Нарешті, файл 6, build.xml, містить інформацію про те, як проект компілювати. Редагувати його вручну, зазвичай, (про один важливий виняток -- нижче), не потрібно, достатньо скористатися вкладкою Overview вкладки Configuration:

Зокрема, тут вказуються папки для пошуку *.h файлів, важливі змінні препроцесора (такі як USE_STDPERIPH_DRIVER та STM32F100RB), адреси Flash (ROM) i RAM, скрипт лінкера, опції компілятора та лінкера. На жаль, опції компіляції -- єдині для всіх файлів проекту. Компілятору і лінкеру, зокрема, передається архітектура контролера, Cortex-M3, система команд, thumb, рівень оптимізації (на прикладі вище -- O0). Значення решти опцій можна довідатися в документації gcc.

Newlib i semihosting

Newlib -- реалізація стандартної бібліотеки C, призначена для використання у вбудовуваних системах, зокрема -- мікроконтролерах. Їй, звичайно, потрібна мінімальна підтримка з боку операційної системи чи її аналогу, тому переносячи її в нове середовище, слід реалізувати ряд функцій -- open, close, read, write, lseek, link, unlink, stat, fstat, для роботи з файлами, sbrk для виділення програмі додаткової пам'яті (на цю функцію покладається malloc), execve, fork, getpid, kill, times, wait, _exit, для роботи з процесами та environ -- вказівник на змінні середовища. Якщо дане середовище не підтримує файлів чи процесів, відповідні функції можуть просто встановлювати відповідним чином errno та повертати код помилки:

int open(const char *name, int flags, int mode) {
    return -1;
}

Детальніше можна почитати в офіційній документації та на OSDev.

Компілятор, який рекомендується використовувати з CoIDE, містить newlib, але, так як конкретний контролер та середовище, у якому вона виконуватиметься, наперед не відомий, реалізації відповідних функцій винесені окремо. В репозиторії, в підрозділі COMMON, є компонента "C Library -- Implement the minimal functionality required to allow newlib to link", яка додасть до проекту файл syscalls/syscalls.c, із заглушками для частини перерахованих вище функцій. Файл зовсім куций, краще доповнити його хоча б так, як описано за посиланнями вище, але, навіть у вихідному вигляді, завдяки йому можна менш-більш успішно лінкуватися із Newlib.

Маючи Newlib, маємо практично всі стандартні функції C. Наприклад, сімейство printf. Однак printf вимагає, щоб підтримувалися close, fstat, isatty, lseek, read, sbrk, write. Іноді це може бути проблемою -- складно, вимагає великих зусиль або просто незручно. Щоб спростити задачу, репозиторій CoIDE включає урізану реалізацію декількох функцій сімейства printf (vsnprintf, snprintf, vsprintf, vprintf, fprintf, printf, sprintf, puts), яка компактніша ніж стандартна (хоча і досить сумісна із нею) та потребує реалізації лише однієї функції -- void PrintChar(char c).

Наприклад, якщо реалізувати її так:

void PrintChar(char c)
{
    USART_SendData(USART2, (uint8_t) c);
    /* Loop until the end of transmission */
    while (USART_GetFlagStatus(USART2, USART_FLAG_TC) == RESET)
    {}
}

(перед тим додавши пакет USART в репозиторії і включивши stm32f10x_usart.h), все, що друкуватиме printf передаватиметься через USART2.

Крім того, як вже згадувалося раніше, за допомогою semihosting можна виводити стрічки, передані з контролера, безпосередньо у консоль відладчика! Тобто, якщо додати пакет "Semihosting", включити в printf.c файл semihosting.h, PrintChar реалізувати так:

void PrintChar(char c)
{
    SH_SendChar(c);
}

Тоді програма:

#include <stdio.h>

int main(void)
{
    printf("Number is %i\n", 42);
    while(1)
    {
    }
}
дасть наступний результат:
Іноді дуже зручно!

Увага:semihosting слід дозволити в налаштуваннях -- Debug->Debug configuration, для кожного проекту окремо.
Увага: на жаль, CoIDE-шний printf не підтримує floating-point.
Увага: semihosting використовує внутрішні буфери, результат не завжди зразу видно в консолі відладчика. Тому слід робити flush, або виводити в кінці символ '\n', який дає той же ефект.

Можна скористатися і повноцінним printf з Newlib. Для цього потрібно видалити з проекту "Retarget printf". Воно перепитається, чи справді потрібно видалити компоненту, від якої залежить використана компонента "Semihosting", слід погодитися (Semihosting залишиться). Тоді у файлі syscall.c реалізувати _write так:

int _write(int file, char *ptr, int len)
{
  int txCount;
  for ( txCount = 0; txCount < len; txCount++)
  {
      SH_SendChar(ptr[txCount]);
  }
  return len;
}

Результат виконання головної програми, наведеної вище, буде тим же. Плюс такого підходу -- більш стандартний printf, мінус -- зростання розміру коду (майже на 30Кб).


Використання С++

На жаль, підтримки C++ в CoIDE немає, і поки не планується... Однак, не все так страшно -- Eclipse підтримує C++, треба просто трішки їй допомогти.

Знову ж, на жаль, в CoIDE опції, що передаються компілятору, єдині для всіх файлів проекту (див. ілюстрацію вище, зокрема "файл 6", build.xml). Це породжує проблему -- компілювати всі файли як C++ не можна, вони чисто Сі-шні, вибрати режим C++ тільки для деяких файлів теж не вдасться. Доводиться покластися на здатність gcc вибирати, компілювати програму як C чи як C++, в залежності від розширення. В найпростішому випадку -- перейменувати main.c в main.cpp (засобами CoIDE -- за допомогою контекстного меню на файлі, або натиснувши F2, коли файл вибрано в дереві проекту). Крім того, слід перевірити в build.xml, чи IDE компілюватиме файли з розширенням .cpp. У ньому має бути щось таке:

<fileset casesensitive="false" dir=".">
        <include name="**/*.c"/>
        <include name="**/*.cpp"/>
        <include name="**/*.s"/>
</fileset>
Якщо рядка з *.cpp немає, слід його додати, але при цьому пам'ятати, що оболонка може автоматично перезаписувати build.xml.

Сходу не скомпілюється навіть найпростіша програма -- лінкер скаржитиметься на відсутність символів з іменами __exidx_*:
 [cc] c:/embedded/coocox/arm-2011.03-coocox/bin/../lib/gcc/arm-none-eabi/4.5.2/thumb2\libgcc.a(unwind-arm.o): In function `get_eit_entry':
       [cc] unwind-arm.c:(.text+0x136): undefined reference to `__exidx_end'
       [cc] unwind-arm.c:(.text+0x13a): undefined reference to `__exidx_start'
       [cc] unwind-arm.c:(.text+0x13e): undefined reference to `__exidx_start'
       [cc] unwind-arm.c:(.text+0x142): undefined reference to `__exidx_end'


Ці змінні потрібні для роботи із виключеннями (exceptions). Однак, виключення на контролерах використовувати, так чи так, не рекомендується! Вони складні у реалізації, потребують підтримку з боку середовища, у якому програма виконується і багато ресурсів. Одна із дуже небагатьох "фішок" C++, яка може впливати на ефективність програми, навіть якщо не використовується у коді. Тому краще їх просто заборонити -- додати ключ "-fno-exceptions" до опцій компілятора (Project->Configuration->вкладка Overview->Command->Compiler).

Якщо бажання добитися підтримки виключень все ж є, можна зробити так. Додати до скрипта лінкера link.ld відповідну секцію:

__exidx_start = .;
.ARM.exidx :
{
    *(.ARM.exidx* .gnu.linkonce.armexidx.*)
} > rom
__exidx_end = .;

а потім спробувати прикрутити підтримку часу виконання (run-time support) виключень C++, наприклад, за допомогою libcxxrt. Деякі підказки див. на OSDev: "C++ Exception Support". Якщо цікаво -- пишіть, розберемося.

Код CMSIS і SPL цілком C-шний, не містить захисту від включення у C++-файли. Тому "захищати" їх потрібно самостійно, інакше буде купа матюків від компілятора:

extern "C"
{
#include <stm32f10x.h>
#include <stm32f10x_gpio.h>
#include <stm32f10x_rcc.h>
#include <semihosting.h>
}

Аналогічно робиться, щоб відключити заплутування імен при оголошенні переривань, які заміщатимуть "weak" функції по замовчуванню:

extern "C"{
void SysTick_Handler(void) {
    // Тіло функції
}
}

В принципі, після цього в програмі вже можна більш чи менш успішно використовувати конструкції C++. Однак, для повноцінної підтримки потрібно ще дещо:

1. Додати ініціалізацію статичних об'єктів.
1a. Вставити в link.ld, внизу секції text, такі рядки:

.text : 
{ 
 KEEP(*(.isr_vector .isr_vector.*)) 
      *(.text .text.* .gnu.linkonce.t.*)  
      *(.glue_7t) *(.glue_7)                        
      *(.rodata .rodata* .gnu.linkonce.r.*)  
/* Опис секції .text, див. вище */
/* C++ Static constructors/destructors (eabi) */
 . = ALIGN(4);
 KEEP(*(.init))

 . = ALIGN(4);
 __preinit_array_start = .;
 KEEP (*(.preinit_array))
 __preinit_array_end = .;

 . = ALIGN(4);
 __init_array_start = .;
 KEEP (*(SORT(.init_array.*)))
 KEEP (*(.init_array))
 __init_array_end = .;

 . = ALIGN(4);
 KEEP(*(.fini))

 . = ALIGN(4);
 __fini_array_start = .;
 KEEP (*(.fini_array))
 KEEP (*(SORT(.fini_array.*)))
 __fini_array_end = .;

 /* C++ Static constructors/destructors (elf) */
 . = ALIGN(4);
 _ctor_start = .;
 KEEP (*crtbegin.o(.ctors))
 KEEP (*(EXCLUDE_FILE (*crtend.o) .ctors))
 KEEP (*(SORT(.ctors.*)))
 KEEP (*crtend.o(.ctors))
 _ctor_end = .;

 . = ALIGN(4);
 KEEP (*crtbegin.o(.dtors))
 KEEP (*(EXCLUDE_FILE (*crtend.o) .dtors))
 KEEP (*(SORT(.dtors.*)))
 KEEP (*crtend.o(.dtors))
 } > rom /* Кінець опису секції .text */

1b. Додати до startup_stm32f10x_md_vl.c оголошення відповідних змінних, визначених лінкером згідно інструкцій вище, код ініціалізації, (call_constructors):

extern unsigned long __preinit_array_start;
extern unsigned long __preinit_array_end;
extern unsigned long __init_array_start;
extern unsigned long __init_array_end;
extern unsigned long _ctor_start;
extern unsigned long _ctor_end;
static void call_constructors(unsigned long *start, unsigned long *end) __attribute__((noinline));
static void call_constructors(unsigned long *start, unsigned long *end)
{
   unsigned long *i;
   void (*funcptr)();
   for ( i = start; i < end; i++)
   {
     funcptr=(void (*)())(*i);
     funcptr();
   }
}

та в Default_Reset_Handler, перед викликом main(), здійснити ініціалізацію:

//Call C++ global constructors
   call_constructors(&__preinit_array_start, &__preinit_array_end);
   call_constructors(&__init_array_start, &__init_array_end);
   call_constructors(&_ctor_start, &_ctor_end); 
 
   main();

УВАГА: Код, написаний вище, не є коректним для багатопоточного середовища! Особливо це стосується локальних статичних змінних, звичайно. "Потокобезпечний" варіант вимагає окремого розгляду.

2. Вказати, що робити компілятору, яко відбудеться виклик чисто віртуальної функції:

#include <exception> // містить оголошення std::terminate

void __cxa_pure_virtual(void) {
    // Можна повідомити про помилку, через USART, чи засобами semihosting
    std::terminate();
}

void __cxa_deleted_virtual(void) {
    // Можна повідомити про помилку, через USART, чи засобами semihosting
    std::terminate();
}

Зрозуміло, що цей код слід вставляти в файл, що компілюватиметься як C++! Замість terminate можна викликати й abort(), однак стандартний terminate можна замінити на якийсь свій обробник, abort -- ні, а іноді потрібно перевести систему в безпечний стан, перш ніж зависнути (типова реакція контролерів на помилки :).

3. Реалізувати оператори new, new[], delete, delete[] (але пам'ятати про обережне поводження із ними! -- пам'яті дуууже мало):

#include <stdlib.h>

void * operator new(size_t size)
{
  return malloc(size);
}

void operator delete(void * ptr)
{
   free(ptr);
}

void * operator new[](size_t size)
{
     return malloc(size);
}

void operator delete[](void * ptr)
{
     free(ptr);
}

Звичайно, це найтривіальніша (хоча і робоча) реалізація. Вона покладається на malloc i free з Newlib, які, у свою чергу, залежать від _sbrk, приклад реалізації якої є в згаданому вище syscalls/syscalls.c. Зауваження: захист free від виклику із нульовим вказівником не потрібна -- згідно стандарту, free(NULL) повинна просто нічого не робити.

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

#include <cxxabi.h>
using __cxxabiv1::__guard;

int __cxa_guard_acquire(__guard *g)
{
    return !*(char *)(g);
};
void __cxa_guard_release (__guard *g) {
    *(char *)g = 1;
};
void __cxa_guard_abort (__guard *)
{};


але вона не забороняє переривання, тому повністю коректною не є. (При чому, просто так заборонити переривання на невизначений час небезпечно, тому побудувати коректну реалізацію не так просто).
5. Стандартну STL використати буде складно. Однак, відмовлятися від STL, як такого, не хотілося б -- вона містить багато потенційно корисних (якщо акуратно!) речей. Тому варто спробувати скористатися менш-більш стандартними, (але далеко не 100%), реалізаціями, які достатньо економні, щоб їх можна було використовувати на контролерах. Одна із таких -- uClibc++ (див. також її "підготовлений для Ардуїно" варіант). Поки що в цю тему не заглиблюватимуся, якщо цікаво -- пишіть. Сподіваюся присвятити їй окремий пост.
Описані вище елементи підтримки C++ розглядалися та реалізовувалися зовсім поверхнево та не завжди повністю коректно. Щоб ознайомитися з проблематикою детальніше, див. наступні джерела:
  • Статті "C++", "C++ Bare Bones", "C++ Exception Support" з OSDev -- сайту про написання власних операційних систем. Він, в принципі, орієнтований на x86-сумісні машини, однак загальні принципи та сама ситуація ("гола машина", без сервісів операційної системи) достатньо схожа на написання програм для контролера (які і є самі собі OS).
  • "Поддержка C++ на avr в gcc". ARM-и не AVR, звичайно, але і на них потрібно реалізувати ті ж мінімальні засоби підтримки C++, часто -- майже тим же чином, що і для AVR.
  • Аналогічно, розгляд тематики C++ для AVR на форумі avrfreaks: "avr-c++ micro how-to".
  • Декілька "ітеративних" обговорень використання C++ на форумі CoIDE: "C++ class", "CooCox and C++"-1, "CooCox and C++"-2. (Всі три теми доповнюють одна одну, перекриваючись лише частково).
  • "Static locals and threadsafety in g++".
Майже мінімальний проект, із підтримкою C++ можна скачати тут. Він включає запропонований в п.1-п.4 код (в файлі startup_cxx.cpp), відповідні зміни до link.ld та конфігурації. Використано Newlib-івський printf. Для економії пам'яті програм варто замінити його на "Retargetable printf".

Нюанси та граблі

Вирішив окремо зібрати всілякі дрібнички, які, однак, можуть коштувати багато хвилин чи годин роботи.
  • По замовчуванню код в CoIDE компілюватиметься згідно стандрату C89. Тобто, for(int i= ...) не скомпілюється! Слід або оголошувати змінну i окремо, або ввімкнути підтримку C99. Всі файли CoIDE компілює із тими ж опціями, тому ввімкнути можна тільки глобально, однак проблем із CMSIS чи STL це не створить -- вони сумісні як з С89 так і з С99. Ввімкнути її можна, передавши компілятору (Project->Configuration->вкладка Overview->Command->Compiler) ключ -std=c99. Якщо потрібна підтримка ключового слова asm, яке є розширенням в порівнянні з ANSI C, треба використати -std=gnu99.
  • Шляхи в проекті абсолютні :-( Тому, якщо його перемістити кудись, потрібно їх, в файлах проекту, редагувати вручну.
  • Директорії bin/obj при компіляції спочатку видаляються повністю. Тому, якщо її видалити, із тих чи інших причин не вдасться, компіляція не відбудеться.
  • Компоненти можуть затерти ваші файли в проекті! Особливо цим грішать всілякі шаблони від сторонніх розробників (main.c чи startup_stm32f10x_md_vl.c затерти - тільки так). Аналогічно, видалення компонент знищить їх файли без попередження, навіть якщо вони  були змінені.
  • На жаль, CoIDE-шний printf не підтримує floating-point. (Стандартний, правда, теж має із ними проблеми...)
  • Semihosting використовує внутрішні буфери, результат не завжди видно зразу. Тому слід робити flush, або виводити в кінці символ '\n', який дає той же ефект.
  • Перш ніж використовувати, semihosting слід дозволити в налаштуваннях -- Debug->Debug configuration, для кожного проекту окремо.

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

6 коментарів:

  1. Недавно купив собі STM32F4-Discovery, теж вирішив побавитись. Хочу в CooCox поджужити через STM32F050F4P6 SIM800 з PA6H. І тут таку класну статтю знаходжу, та ще й українською мовою! Дивлюся, а автор статті ще й астрономію любить, як і я :). Думаю, було б круто, як би автор статті ще був би зі Львова, дивлюсь в профіль - так і є! :) Можливо варта було б познайомитись ближче... мій сайт по астрономії: http://starcontact.net

    ВідповістиВидалити
    Відповіді
    1. Могу помочь :) Но проще сделать на STM32F103-107 проце, у них хоть аппаратные USART и SPI есть. А также оперативки и памяти программ побольше.

      Видалити
  2. Володимир, дякую за теплі слова, і нагадування, що я не один такий!

    Звичайно, можна познайомитися. :-) Я, правда, "в цьому світі" поки мало присутній -- фактично, від середини осені "зайвих" ресурсів писати щось сюди не було (старі наробітки не рахую). Але воно потроху змінюється.

    На жаль, із F0 ніколи не стикався. Про CoIDE можливо писатиму знову -- в лютому вони нарешті додали підтримку STM32F3 (на базі якого є дуже симпатична недорога плата: http://www.st.com/web/catalog/tools/FM116/SC959/SS1532/PF254044 ) та підтримку С++. До того і з ти, і з тим все рівно можна було працювати, але то було далеко не те.

    ВідповістиВидалити
  3. Indrekis і як ми можемо познайомитися ближче? Напишіть на мій емейл vuk@ukr.net свій телефон. Я подзвоню, якщо можна.

    p.s.
    Дякую Сергію за допомогу! ;) STM32F050 поміняли на STM32F100.

    ВідповістиВидалити