LINUX.ORG.RU

Осваиваем STM32 снизу: часть 5 - Мигаем на C

 ,


0

1

Часть 1 Часть 2 Часть 3 Часть 4 Часть 5 Часть 6 Часть 7 Часть 8 Часть 9

Часть 5: Мигаем на C

Эта часть будет комбинацией частей 3 и 4. Мы перепишем код из части 3 на C, используя «инфраструктуру» для сборки из части 4 и познакомимся с некоторыми не всегда очевидными моментами, которые надо помнить при работе с микроконтроллером из кода на C.

Скрипт линкера и файл reset_exception_handler.s не поменяются, приведём лишь код программы на C:

blinkc.c:

#include <stdint.h>

static void enable_port_clock(void);
static void configure_pin(void);
static void toggle_pin(void);

void start(void)
{
    enable_port_clock();
    configure_pin();

    // blink loop
    for (;;)
    {
        // wait loop
        for (uint32_t delay = 1000000; delay > 0; delay--)
        {
        }

        toggle_pin();
    }
}

static void enable_port_clock(void)
{
    // enable I/O port C clock
    uint32_t rcc_base_address = 0x40021000;
    uint32_t rcc_apb2enr_address = rcc_base_address + 0x18;
    uint32_t rcc_apb2enr_iopcen = 1 << 4;

    uint32_t *rcc_apb2enr_pointer = (uint32_t *)rcc_apb2enr_address;
    uint32_t rcc_apb2enr_value = *rcc_apb2enr_pointer;
    rcc_apb2enr_value |= rcc_apb2enr_iopcen;
    *rcc_apb2enr_pointer = rcc_apb2enr_value;
}

static void configure_pin(void)
{
    // configure PC13 as open-drain output with 2 MHz speed
    uint32_t gpioc_base_address = 0x40011000;
    uint32_t gpioc_crh_address = gpioc_base_address + 0x04;
    uint32_t gpiox_crh_mode13_0 = 1 << 20;
    uint32_t gpiox_crh_mode13_1 = 1 << 21;
    uint32_t gpiox_crh_cnf13_0 = 1 << 22;
    uint32_t gpiox_crh_cnf13_1 = 1 << 23;

    uint32_t *gpioc_crh_pointer = (uint32_t *)gpioc_crh_address;
    uint32_t gpioc_crh_value = *gpioc_crh_pointer;
    gpioc_crh_value &= ~gpiox_crh_mode13_0;
    gpioc_crh_value |= gpiox_crh_mode13_1;
    gpioc_crh_value |= gpiox_crh_cnf13_0;
    gpioc_crh_value &= ~gpiox_crh_cnf13_1;
    *gpioc_crh_pointer = gpioc_crh_value;
}

static void toggle_pin(void)
{
    // toggle PC13
    uint32_t gpioc_base_address = 0x40011000;
    uint32_t gpioc_odr_address = gpioc_base_address + 0x0c;
    uint32_t gpiox_odr_odr13 = 1 << 13;

    uint32_t *gpioc_odr_pointer = (uint32_t *)gpioc_odr_address;
    uint32_t gpioc_odr_value = *gpioc_odr_pointer;
    gpioc_odr_value ^= gpiox_odr_odr13;
    *gpioc_odr_pointer = gpioc_odr_value;
}

Очень важный нюанс: нужно обратить внимание, чтобы эта программа компилировалась без оптимизации, с флагом -O0. Если всё сделано верно, то make clean flash прошьёт программу на микроконтроллер, и он замигает светодиодом.

Если же эту программу скомпилировать с флагом -O3 (не забудьте выполнить make clean), то произойдёт странное событие. После запуска микроконтроллер включит светодиод, да так и оставит его включённым. Чтобы разобраться, в чём дело, попробуем дизассемблировать то, что нам скомпилировал gcc:


blinkc.o:     file format elf32-littlearm


Disassembly of section .text:

00000000 <start>:
   0:	4906      	ldr	r1, [pc, #24]	@ (1c <start+0x1c>)
   2:	4807      	ldr	r0, [pc, #28]	@ (20 <start+0x20>)
   4:	684b      	ldr	r3, [r1, #4]
   6:	6982      	ldr	r2, [r0, #24]
   8:	f423 0310 	bic.w	r3, r3, #9437184	@ 0x900000
   c:	f042 0210 	orr.w	r2, r2, #16
  10:	f443 03c0 	orr.w	r3, r3, #6291456	@ 0x600000
  14:	6182      	str	r2, [r0, #24]
  16:	604b      	str	r3, [r1, #4]
  18:	e7fe      	b.n	18 <start+0x18>
  1a:	bf00      	nop
  1c:	40011000 	.word	0x40011000
  20:	40021000 	.word	0x40021000

Впечатляющая работа компилятора, 57 строк кода на C он скомпилировал всего в 10 машинных инструкций. Приведём аналогичный псевдокод:

r1 := 0x40011000;
r0 := 0x40021000;
r3 := memory[r1 + 4];
r2 := memory[r0 + 24];
r1[20] := 0;
r1[23] := 0;
r2[5] := 1;
r3[21] := 1;
r3[22] := 1;
memory[r0 + 24] := r2;
memory[r1 + 4] := r3;
loop: goto loop

Видно, что имеется код для инициализации порта C и настройки вывода PC13, однако наш цикл мигания куда-то пропал. Оказывается компилятор решил, что наш цикл абсолютно бесполезный и если его убрать, то программа будет работать гораздо быстрей. Не будем вдаваться в дебри стандарта C и неопределённого поведения, просто скажем, что на всех переменных, которые работают с памятью, нужно ставить volatile. В этом случае компилятор будет знать, что мы не просто так байты туда-сюда пересылаем, а делаем важную работу и не будет удалять наш код.

Исправленный код:

#include <stdint.h>

static void enable_port_clock(void);
static void configure_pin(void);
static void toggle_pin(void);

void start(void)
{
    enable_port_clock();
    configure_pin();

    // blink loop
    for (;;)
    {
        // wait loop
        for (volatile uint32_t delay = 1000000; delay > 0; delay--)
        {
        }

        toggle_pin();
    }
}

static void enable_port_clock(void)
{
    // enable I/O port C clock
    uint32_t rcc_base_address = 0x40021000;
    uint32_t rcc_apb2enr_address = rcc_base_address + 0x18;
    uint32_t rcc_apb2enr_iopcen = 1 << 4;

    volatile uint32_t *rcc_apb2enr_pointer = (uint32_t *)rcc_apb2enr_address;
    uint32_t rcc_apb2enr_value = *rcc_apb2enr_pointer;
    rcc_apb2enr_value |= rcc_apb2enr_iopcen;
    *rcc_apb2enr_pointer = rcc_apb2enr_value;
}

static void configure_pin(void)
{
    // configure PC13 as open-drain output with 10 MHz speed
    uint32_t gpioc_base_address = 0x40011000;
    uint32_t gpioc_crh_address = gpioc_base_address + 0x04;
    uint32_t gpiox_crh_mode13_0 = 1 << 20;
    uint32_t gpiox_crh_mode13_1 = 1 << 21;
    uint32_t gpiox_crh_cnf13_0 = 1 << 22;
    uint32_t gpiox_crh_cnf13_1 = 1 << 23;

    volatile uint32_t *gpioc_crh_pointer = (uint32_t *)gpioc_crh_address;
    uint32_t gpioc_crh_value = *gpioc_crh_pointer;
    gpioc_crh_value &= ~gpiox_crh_mode13_0;
    gpioc_crh_value |= gpiox_crh_mode13_1;
    gpioc_crh_value |= gpiox_crh_cnf13_0;
    gpioc_crh_value &= ~gpiox_crh_cnf13_1;
    *gpioc_crh_pointer = gpioc_crh_value;
}

static void toggle_pin(void)
{
    // toggle PC13
    uint32_t gpioc_base_address = 0x40011000;
    uint32_t gpioc_odr_address = gpioc_base_address + 0x0c;
    uint32_t gpiox_odr_odr13 = 1 << 13;

    volatile uint32_t *gpioc_odr_pointer = (uint32_t *)gpioc_odr_address;
    uint32_t gpioc_odr_value = *gpioc_odr_pointer;
    gpioc_odr_value ^= gpiox_odr_odr13;
    *gpioc_odr_pointer = gpioc_odr_value;
}

Этот код работает при любой оптимизации.

Помните, что компилятор C ничего не знает о том, что при записи чего-то в память вы работаете с периферийным устройством. Для него эти операции выглядят, как обычная работа с оперативной памятью и без дополнительных усилий он может оптимизировать и выбрасывать ваши инструкции, если посчитает, что это приведёт к более быстрому коду. В спорных случаях не стесняйтесь проверять скомпилированный код дизассемблером.

Полный код доступен на гитхабе.

★★★★

Проверено: hobbit ()
Последнее исправление: vbr (всего исправлений: 4)

Здесь тот же самый нюанс с некорректным комментарием относительно частоты: 10 MHz vs 2 MHz.

Barracuda72 ★★
()
Вы не можете добавлять комментарии в эту тему. Тема перемещена в архив.