Проверка корректности адресов в памяти на Cortex-M0/M3/M4/M7

    Привет, Хабр!

    По поводу случившегося на днях послабления режима, возмущения в комментариях одного соседнего поста о том, что статьи про микроконтроллеры — сплошь мигание светодиодом, а также безвременной гибели моего стандалон-блога, восстанавливать который мне пока лень, переложу сюда полезный материал об одном прискорбно мало освещаемом прессой трюке в работе с ядрами Cortex-M — проверке произвольных адресов на валидность.


    Одна из весьма полезных и при этом почему-то в готовом виде нигде не описанных возможностей на микроконтроллерах Cortex-M (всех) — это возможность проверки корректности адреса в памяти. С её помощью можно определять размеры флэша, ОЗУ и EEPROM, определять наличие на конкретном процессоре конкретной периферии и регистров, прибивать упавшие процессы при сохранении общей работоспособности ОС и т.п.

    В штатном режиме при попадании в несуществующий адрес на Cortex-M3/M4/M7 вызывается исключение BusFault, а при отсутствии его обработчика эскалируется до HardFault. На Cortex-M0 «детализированных» исключений (MemFault, BusFault, UsageFault) нет, а любые сбои сразу эскалируются до HardFault.

    В общем случае игнорировать HardFault нельзя — он может быть следствием аппаратного сбоя, например, и дальнейшее поведение устройства станет непредсказуемым. Но в частном случае это делать можно и нужно.

    Cortex-M3 и Cortex-M4: неслучившийся BusFault


    На Cortex-M3 и выше проверка валидности адреса делается достаточно просто — надо запретить все исключения (кроме, очевидно, немаскируемых) через регистр FAULTMASK, отключить конкретно обработку BusFault, а потом ткнуть в проверяемый адрес и посмотреть, не взвёлся ли флажок BFARVALID в регистре BFAR, то есть Bus Fault Address Register. Если взвёлся — у вас только что был BusFault, т.е. адрес некорректный.

    Код выглядит так, все дефайны и функции из стандартного (не вендорского) CMSIS, так что должен работать на любом M3, M4 или M7:

    bool cpu_check_address(volatile const char *address)
    {
        /* Cortex-M3, Cortex-M4, Cortex-M4F, Cortex-M7 are supported */
        static const uint32_t BFARVALID_MASK = (0x80 << SCB_CFSR_BUSFAULTSR_Pos);
        bool is_valid = true;
    
        /* Clear BFARVALID flag by writing 1 to it */
        SCB->CFSR |= BFARVALID_MASK;
    
        /* Ignore BusFault by enabling BFHFNMIGN and disabling interrupts */
        uint32_t mask = __get_FAULTMASK();
        __disable_fault_irq();
        SCB->CCR |= SCB_CCR_BFHFNMIGN_Msk;
    
        /* probe address in question */
        *address;
    
        /* Check BFARVALID flag */
        if ((SCB->CFSR & BFARVALID_MASK) != 0)
        {
            /* Bus Fault occured reading the address */
            is_valid = false;
        }
    
        /* Reenable BusFault by clearing  BFHFNMIGN */
        SCB->CCR &= ~SCB_CCR_BFHFNMIGN_Msk;
        __set_FAULTMASK(mask);
    
        return is_valid;
    }

    Cortex-M0 и Cortex-M0+


    С Cortex-M0 и Cortex-M0+ всё сложнее, как я сказал выше, у них нет BusFault и всех соответствующих регистров, а исключения сразу эскалируются до HardFault. Поэтому выход один — сделать так, чтобы обработчик HardFault смог понять, что исключение было вызвано преднамеренно, и вернуться обратно в вызвавшую его функцию, передав туда некий флажок, указывающий, что HardFault был.

    Делается это сугубо на ассемблере. В примере ниже регистр R5 устанавливается в 1, а в регистры R1 и R2 записываются два «магических числа». Если после попытки загрузить значение по проверяемому адресу случится HardFault, то он должен проверить значения R1 и R2, и при обнаружении в них нужных чисел установить R5 в ноль. В сишный код значение R5 передаётся через специальную переменную, жёстко привязанную к этому регистру, в ассемблер проверяемый адрес — в неявной форме, мы просто знаем, что в arm-none-eabi первый параметр функции кладётся в R0.

    bool cpu_check_address(volatile const char *address)
    {
        /* Cortex-M0 doesn't have BusFault so we need to catch HardFault */
        (void)address;
        
        /* R5 will be set to 0 by HardFault handler */
        /* to indicate HardFault has occured */
        register uint32_t result __asm("r5");
    
        __asm__ volatile (
            "ldr  r5, =1            \n" /* set default R5 value */
            "ldr  r1, =0xDEADF00D   \n" /* set magic number     */
            "ldr  r2, =0xCAFEBABE   \n" /* 2nd magic to be sure */
            "ldrb r3, [r0]          \n" /* probe address        */
        );
    
        return result;
    }

    Код обработчика HardFault в простейшем виде выглядит так:

    __attribute__((naked)) void hard_fault_default(void)
    {
        /* Get stack pointer where exception stack frame lies */
        __asm__ volatile
        (
            /* decide if we need MSP or PSP stack */
            "movs r0, #4                        \n" /* r0 = 0x4                   */
            "mov r2, lr                         \n" /* r2 = lr                    */
            "tst r2, r0                         \n" /* if(lr & 0x4)               */
            "bne use_psp                        \n" /* {                          */
            "mrs r0, msp                        \n" /*   r0 = msp                 */
            "b out                              \n" /* }                          */
            " use_psp:                          \n" /* else {                     */
            "mrs r0, psp                        \n" /*   r0 = psp                 */
            " out:                              \n" /* }                          */
    
            /* catch intended HardFaults on Cortex-M0 to probe memory addresses */
            "ldr     r1, [r0, #0x04]            \n" /* read R1 from the stack        */
            "ldr     r2, =0xDEADF00D            \n" /* magic number to be found      */
            "cmp     r1, r2                     \n" /* compare with the magic number */
            "bne     regular_handler            \n" /* no magic -> handle as usual   */
            "ldr     r1, [r0, #0x08]            \n" /* read R2 from the stack        */
            "ldr     r2, =0xCAFEBABE            \n" /* 2nd magic number to be found  */
            "cmp     r1, r2                     \n" /* compare with 2nd magic number */
            "bne     regular_handler            \n" /* no magic -> handle as usual   */
            "ldr     r1, [r0, #0x18]            \n" /* read PC from the stack        */
            "add     r1, r1, #2                 \n" /* move to the next instruction  */
            "str     r1, [r0, #0x18]            \n" /* modify PC in the stack        */
            "ldr     r5, =0                     \n" /* set R5 to indicate HardFault  */
            "bx      lr                         \n" /* exit the exception handler    */
            " regular_handler:                  \n"
    
            /* here comes the rest of the fucking owl */
        )

    В момент ухода в обработчик исключения Cortex скидывает регистры, которые гарантированно будут испорчены обработчиком (R0-R3, R12, LR, PC…), в стек. Первый фрагмент — он уже есть в большинстве готовых обработчиков HardFault, кроме написанных под чистый bare metal — определяет, в какой именно стек: при работе в ОС это может быть либо MSP, либо PSP, и у них разные адреса. В bare metal проектах обычно априори устанавливается стек MSP (Main Stack Pointer), без проверки — ибо PSP (Process Stack Pointer) там быть не может в силу отсутствия процессов.

    Определив нужный стек и положив его адрес в R0, мы читаем из него значения R1 (смещение 0x04) и R2 (смещение 0x08), сравниваем с магическими словами, если оба совпадают — читаем из стека значение PC (смещение 0x18), добавляем к нему 2 (2 байта — размер инструкции на Cortex-M*) и сохраняем обратно в стек. Если этого не сделать, при возвращении из обработчика мы окажемся на той же инструкции, которая собственно и вызвала исключение, и будем вечно бегать по кругу. Добавление 2 перемещает нас на следующую инструкцию в момент возвращения.

    * Upd. В комментариях возник вопрос о размере инструкций на Cortex-M, вынесу правильный ответ сюда: в данном случае краш вызывает инструкция LDRB, которая в архитектуре ARMv7-M присутствует в двух вариантах — 16-битном и 32-битном. Второй вариант будет выбран, если выполнено хотя бы одно из условий:

    • автор явно указал инструкцию LDRB.W вместо LDRB (у нас — нет)
    • используются регистры выше R7 (у нас — R0 и R3)
    • указано смещение величиной больше 31 байта (у нас смещения нет)


    Во всех остальных случаях (т.е. когда операнды подходят под формат 16-битной версии инструкции) ассемблер обязан выбрать 16-битный вариант.

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


    Далее пишем 0 в R5, что служит индикатором попадания в HardFault. Регистры после R3 до специальных регистров в стек не сохраняются и при выходе из обработчика никак не восстанавливаются, поэтому портить или не портить их — на нашей совести. В данном случае R5 с 1 на 0 мы меняем целенаправленно.

    Возвращение из обработчика прерывания делается строго одним способом. При входе в обработчик в регистр LR пишется специальное значение под названием EXC_RETURN, которое для выхода из обработчика надо записать в PC — и не просто записать, а сделать это командной POP или BX (то есть «mov pc, lr», например, не работает, хотя по первому разу вам может показаться, что работает). BX LR выглядит как попытка перехода по бессмысленному адресу (в LR будет лежать что-то вида 0xFFFFFFF1, не имеющее никакого отношения к реальном адресу процедуры, в которую нам надо вернуться), но в реальности процессор, увидев это значение в PC (куда оно ляжет автоматически), сам восстановит регистры из стека и продолжит выполнять нашу процедуру — со следующей после вызвавшей HardFault процедуры благодаря тому, что PC в этом стеке мы руками увеличили на 2.

    Прочитать про все смещения и команды можно понятно где, разумеется.

    Ну или если магических чисел не видно, то всё перейдёт к regular_handler, после которого идёт обычная процедура обработки HardFault — как правило, это сишная функция, печатающая в консоль значения регистров, решающая, что дальше делать с процессором и т.п.

    Определение размера ОЗУ


    Использование всего этого просто и понятно. Хотим написать прошивку, которая работает на нескольких микроконтроллерах с разным объёмом ОЗУ, при этом каждый раз используя ОЗУ по полной программе?

    Да легко:

    static uint32_t cpu_find_memory_size(char *base, uint32_t block, uint32_t maxsize) {
        char *address = base;
        do {
            address += block;
            if (!cpu_check_address(address)) {
                break;
            }
        } while ((uint32_t)(address - base) < maxsize);
    
        return (uint32_t)(address - base);
    }
    
    uint32_t get_cpu_ram_size(void) {
        return cpu_find_memory_size((char *)SRAM_BASE, 4096, 80*1024);
    }

    maxsize здесь нужен затем, что на максимально возможном объёме ОЗУ между ним и следующим блоком адресов может не быть зазора, на котором сломается cpu_check_address. В данном примере это 80 КБ. Все адреса прощупывать также нет смысла — достаточно по даташиту посмотреть, каков минимально возможный шаг между двумя моделями контроллера, и поставить его в качестве block.

    Программный переход в загрузчик, расположенный неизвестно где


    Иногда можно делать и более заковыристые трюки — например, представьте, что вам хочется программно прыгнуть на штатный фабричный бутлоадер STM32, чтобы перейти в режим обновления прошивки по UART или USB, не утруждая себя написанием своего бутлоадера.

    Бутлоадер у STM32 лежит в области под названием System Memory, на которую и надо перейти, но есть одна проблема — у этой области разные адреса не то что на разных сериях процессоров, а на разных моделях одной серии (с эпической табличкой можно ознакомиться в AN2606 на страницах с 22 по 26). При внесении же соответствующего функционала в платформу вообще, а не просто в конкретное изделие, хочется универсальности.

    В файлах CMSIS адрес начала System Memory также отсутствует. Определить его по Bootloader ID не представляется возможным, т.к. это проблема курицы и яйца — Bootloader ID лежит в последнем байте System Memory, что возвращает нас к вопросу об адресе.

    Однако, если мы посмотрим на карту памяти STM32, то увидим примерно такую картину:


    Нас в данном случае интересует окружение System Memory — например, сверху лежит однократно программируемая область (есть не во всех STM32) и Option bytes (есть во всех). Эта структура наблюдается не то что в разных моделях, а в разных линейках STM32, с разницей лишь в наличии OTP и наличии зазора в адресах между системной памятью и опциями.

    Но для нас в данном случае важнее всего то, что адрес начала Option Bytes есть в штатных заголовках CMSIS — он там называется OB_BASE.

    Дальнейшее просто. Пишем функцию поиска первого валидного или невалидного адреса вниз или вверх от указанного:

    char *cpu_find_next_valid_address(char *start, char *stop, bool valid) {
        char *address = start;
        while (true) {       
            if (address == stop) {
                return NULL;
            }
            
            if (cpu_check_address(address) == valid) {
                return address;
            }
            
            if (stop > start) {
                address++;
            } else {
                address--;
            }
        };
    
        return NULL;
    }

    И ищем вниз от Option bytes сначала конец то ли системной памяти, то ли примыкающих к ней OTP, а потом и начало системной памяти — в два захода:

    /* System memory is the valid area next _below_ Option bytes */
    char *a, *b, *c;
    a = (char *)(OB_BASE - 1);
    b = 0;
    
    /* Here we have System memory top address */
    c = cpu_find_next_valid_address(a, b, true);
    
    /* Here we have System memory bottom address */
    c = cpu_find_next_valid_address(c, b, false) + 1;

    И без особого труда оформляем это в функцию, находящую начало системной памяти и прыгающую на него, то есть — запускающую бутлоадер:

    static void jump_to_bootloader(void) __attribute__ ((noreturn));
    
    /* Sets up and jumps to the bootloader */
    static void jump_to_bootloader(void) {
        /* System memory is the valid area next _below_ Option bytes */
        char *a, *b, *c;
        a = (char *)(OB_BASE - 1);
        b = 0;
        
        /* Here we have System memory top address */
        c = cpu_find_next_valid_address(a, b, true);
        
        /* Here we have System memory bottom address */
        c = cpu_find_next_valid_address(c, b, false) + 1;
        
        if (!c) {
            NVIC_SystemReset();
        }
        
        uint32_t boot_addr = (uint32_t)c;
        
        uint32_t boot_stack_ptr = *(uint32_t*)(boot_addr);
        uint32_t dfu_reset_addr = *(uint32_t*)(boot_addr+4);
    
        void (*dfu_bootloader)(void) = (void (*))(dfu_reset_addr);
    
        /* Reset the stack pointer */
        __set_MSP(boot_stack_ptr);
    
        dfu_bootloader();
        while (1);
    }

    От конкретной модели процессора тут зависит… да ничего не зависит. Логика не будет работать на моделях, у которых между OTP и системной памятью есть дырка — но я не проверял, есть ли такие вообще. Будете активно работать с OTP — проверьте.

    Прочие же хитрости относятся лишь к обычной процедуре вызова бутлоадера из вашего кода — не забудьте сброcить указатель стека и вызывайте процедуру ухода в бутлоадер до того, как инициализируете в процессоре периферию, тактовые частоты и т.д.: в силу своего минимализма бутлоадер может забивать на инициализацию периферии и ожидать, что она находится в состоянии по умолчанию. Неплохим вариантом вызова бутлодера из произвольного места вашей программы является запись в RTC Backup Register или просто в известный адрес в памяти магического числа, программная перезагрузка и проверка на первых этапах инициализации этого числа.

    P.S. Так как все адреса в карте памяти процессора выровнены в худшем случае по 4, описанную выше процедуру кратно ускорит идея шагать по ним с шагом 4 байта вместо одного.

    Важное замечание


    N.B.: обратите внимание, что на конкретном контроллере валидность конкретного адреса не обязательно говорит о фактическом присутствии функционала, который по этому адресу может располагаться. Например, адрес регистра, управляющего каким-то опциональным периферийным блоком, может быть валиден, хотя сам блок в данной модели отсутствует. Со стороны производителя здесь возможны самые интересные подвохи, обычно уходящие корнями в использование для разных моделей процессоров одних и тех же кристаллов. Тем не менее, в большинстве случаев указанные процедуры работают и оказываются весьма полезны.
    Поделиться публикацией

    Комментарии 40

      0
      Написано интересно и хорошо, мол, меняйте таблицу прерываний под себя, но у F0 меняется не только размер ОЗУ от серии к серии, но и периферия (переносишь с F030 под F042, скорей всего придется менять UART, DMA, и т.п., а значит, переносимость кода данные методы не гарантируют и код так и остается в рамках одной серии. А в рамках подсерии VL/MD/HD и так проблем нет.
      +«рваное» ОЗУ у старших моделей.

      Значит, все равно править под конкретный МК.
        +2
        а) во всём тексте нет ни единого упоминания таблицы прерываний
        б) переносимость кода между двумя произвольными моделями контроллеров вам вообще ничто и никто в этом мире не гарантирует, хотя вообще-то при необходимости сделать программно определяемыми несколько адресов нужной периферии ничто не мешает
        0
        Хорошая пасхалка про сову в коде :)

        Вообще, спуск до ассемблера навевает воспоминания о MS-DOS и робких попытках писать код на асме с 80x87 FPU. «Когда железки были большими»…
          +1
          Ну там на самом деле после совы должен идти стандартный обработчик хардфолта, примеров которых в интернете десятки, если не сотни — обычно они просто регистры процессора печатают. Так что реальной шутки поменьше, чем в известной картинке.
            +1
            Мне больше интересно зачем прятать происходящее от компилятора и потом с ним «воевать».

            То есть почему вторая функция выглядит как выглядит, а не как-то примерно так:
            bool cpu_check_address(volatile const char *address) {
                /* Cortex-M0 doesn't have BusFault so we need to catch HardFault */
                /* R5 will be set to 0 by HardFault handler */
                /* to indicate HardFault has occured */
                register bool result __asm("r5") = 1; /* Set default return value */
                register uint32_t sign1 __asm("r1") = 0xDEADF00D; /* set magic number */
                register uint32_t sign2 __asm("r2") = 0xCAFEBABE; /* 2nd magic to be sure */
                uint32_t scratch;
            
                __asm__ (
                    "ldrb %[scratch], [%[address]]  \n" /* probe address        */
                    :"=r"(result),[scratch]"=l"(scratch)
                    :[address]"l"(address),"r"(result),"r"(sign1), "r"(sign2)
                );
            
                return result;
            }


            Есть какой-то тайный смысл в том, чтобы, вместо того, чтобы описать всё явно, опираться на знания API и прочего?

            Не первый раз вижу вещи, написанные в подобном «хрупком» стиле и всегда удивляюсь: зачем?
              0
              Никакого. Но в ближайшие лет десять-двадцать этот API никуда не денется.
                0
                Зато компилятор может какой-нибудь временный результат в r0 положить вдруг. Что превратит весь ваш пример в тыкву.

                Или «в ближайшие лет десять-двадцать» вы и компилятор обновлять не будете?
                  0
                  Во-первых, вот прямо в этом коде никаких «временных результатов» нет.

                  Во-вторых, если вы считаете, что ваш код всё описывает явно и не зависит от обновлений API и компилятора, то вы ошибаетесь.
                    0
                    Во-первых, вот прямо в этом коде никаких «временных результатов» нет.
                    Потому я назвал его не «неправильным», а «хрупким». Временные результаты легко могут появиться если компилятор решит его заинлацнить — и да, атрибут inline для этого не требуется.

                    Во-вторых, если вы считаете, что ваш код всё описывает явно и не зависит от обновлений API и компилятора, то вы ошибаетесь.
                    А конкретнее? Представить себе, что какой-то компилятор, после соответствующего обновления станет использовать, скажем, другую инструкцию ldrb можно... нужно было использовать ldrb.n для надёжности... Что-то ещё?

                    Так-то от ошибок в компилятора никто не застрахован... Но нарываться-то зачем?
                      0
                      LDRB.N или LDRB.W в явном виде, разумеется. И дело тут не в ошибках компилятора, а в том, что если вы не полагаетесь на использование им только нижних регистров, то надо либо все передаваемые в LDRB переменные надо привязать явно к конкретным регистрам, либо указать конкретную инструкцию LDRB — а то их в кортексе три разных.
                        0
                        Я не полагаюсь на использование им нижних регистров — я это заказал явно ("l" vs "r" — это как раз оно).
                0
                Скажите, а насколько важно в подобном коде объявлять register? Это действительно нужно или дань традиции?
                  +1

                  Это нужно, что бы GCC понял, что это не переменные, а алиасы регистров.

                    +1
                    Это совсем другая конструкция нежели стандартный модификатор register.

                    register typename varname __asm("registername"); — это цельная конструкция для описания переменной, живущей в определённом регистре.

                    Она действительно немного похожа на обычный register — но имеет заметно другой смысл.
              +1
              Если между сбросом флага bus fault и его проверкой произойдет какое-нибудь прерывание и в его ISR произойдет обращение к несуществующему адресу, то а) это факт будет пропущен системой, и б) основная процедура вернет ошибочный вердикт о валидности пробного адреса
                0
                Да, там стоит запретить прерывания.
                  0
                  Это уменьшает вероятность проблем, но не исключает их. NMI handler, например, может вызываться в STM32 при сбое тактирования (если включен CSS).
                0
                В такой статье надо перечислять на каких конкретно чипах тестировалось.
                В Kinetis на Cortex-M хаотичное сканирование периферии не инициализированной должным образом приведет к сбросу, без всякого bus fault.
                  +1
                  Я извиняюсь, они там сломали базовые функции ядра кортекса? А как и, главное, зачем?
                  0
                  В общем случае размер инструкции на Cortex-M может быть не только 2, но и 4 байта. Поэтому проверку валидности адресов лучше выполнять в специальной функции, и убедиться в дизассемблере, что инструкция, выполняющая чтение по проверяемому адресу, действительно имеет размер 2 байта.
                    0
                    При чём тут вообще размер инструкции?
                      0
                      Из статьи:
                      читаем из стека значение PC (смещение 0x18), добавляем к нему 2 (2 байта — размер инструкции на Cortex-M) и сохраняем обратно в стек.
                        0
                        В данном случае вариативности в размере инструкции не будет, это ldrb, она 16-битная.
                          0
                          В той же таблице есть и 32-битный вариант ldrb. Если я правильно понял и правильно помню — это вариант той же инструкции с условным выполнением.
                            +1
                            Не совсем. Там для регистров выше R7 инструкция 32-битная (LDRB.W), а с R0 по R7 — подходит и та, и другая, но ассемблер в этом случае обязан выбрать 16-битную, если автор в явном виде не указал LDRB.W.

                            P.S. Добавил в текст поста комментарий.
                              0
                              Ну вот — если компилятор использует для адресации регистр выше R7 — к PC нужно прибавлять 4. Получается inline — функции для проверки валидности использовать нельзя — в разных местах, куда они будут встроены компилятором, для адресации могут использоваться разные регистры. Нужно реализовывать это в одном месте, и отключать для этой функции оптимизацию, чтобы она компилировалась всегда одинаково.
                                0
                                Компилятор к инструкции «ldrb r3, [r0]» никакого отношения не имеет и ничего в ней использовать не может — в ней вообще компилировать нечего, это ассемблер.

                                Оптимизацию в связи с этим отключать не надо.
                                  0
                                  А, да, прошу прощения, невнимательно прочитал статью — не обратил внимание, что ассемблер используется не только в обработчике HardFault.
                    0
                    Смутил такой способ сброса флага:
                    SCB->CFSR |= BFARVALID_MASK;

                    Тем более, что далее он проверяется:
                    if ((SCB->CFSR & BFARVALID_MASK) != 0)
                      +3
                      Это довольно частый способ сброса флагов в контроллерах — записью в них единицы. Так что нет, не опечатка.

                      http://infocenter.arm.com/help/index.jsp?topic=/com.arm.doc.dui0552a/Cihcfefj.html

                      The UFSR bits are sticky. This means as one or more fault occurs, the associated bits are set to 1. A bit that is set to 1 is cleared to 0 only by writing 1 to that bit, or by a reset.
                        0
                        A bit that is set to 1 is cleared to 0 only by writing 1 to that bit

                        т.е. не только «sticky», но еще и «xor» при записи!
                          0
                          Где вы там xor увидели???
                      0
                      А какое у этой проверки практическое применение, помимо проверки размера RAM?
                        0
                        Определение объёмов ОЗУ, флэша, EEPROM, наличия периферии на лету.

                        Берём, например, STM32L1. У него после старта надо все ножки вручную перевести в режим аналогового входа.

                        Вот как определить, «все» — это какие конкретно, если у нас одна и та же прошивка работает на нескольких моделях контроллера с разным числом портов?
                          0
                          Думаю, что про ножки не совсем корректно. Ведь у STM32F100 серии люди часто находят полнофункциональный третий UART или возможность принять 128 Кб прошивку при официальном наличии 64 Кб Flash. И ничто не мешает ножкам реально существовать, но остаться невыведенными наружу.

                          И, кстати, это я бухтел в комментариях про статьи о мигании светодиодом, поэтому спасибо за информативную статью!
                            +1
                            Регистры GPIOG и GPIOF есть только в 144-ножечных корпусах, попытка постучать в них на других кристаллах кончается басфолтом.

                            Соответственно, перед постукиванием проверить, туда ли мы стучим — самый простой способ написания универсальной процедуры.
                            0
                            Но почему бы не использовать для этого регистры DBGMCU_IDCODE и FLASH_SIZE?
                              +1
                              Потому что через полчаса попыток разобраться, что там чему соответствует и как это всё декодировать, а также таскать с собой, в вашем сердце будет ярким пламенем гореть ненависть к отделу разработки ST Microelectronics.



                              И это мы ещё не дошли до попытки без натурных экспериментов выяснить, можно ли вообще по даташитам и RM установить, в каких конкретно процессорах таки есть GPIOG и GPIOF, а в каких нет (короткий ответ: нельзя).
                                0
                                Да, эти регистры не дают полной информации о контроллере. Но ненавидеть за это отдел разработки ST — это черезчур, ведь контроллеры в целом хорошие. Но я бы не стал полагаться на проверку корректности адресов — ведь в очередной ревизии ST может решить, что проще перемаркировать контроллер, урезав его там где выявлены дефекты — и тогда проверка введёт в заблуждение. Я бы использовал для идентификации сигнатуру во флэше, получаемую отдельно от прошивки.
                                  0
                                  Вопрос не в полноте информации, вопрос в том, что ST в своих чипах — не только в микроконтроллерах — очень непоследовательны в деталях реализации.

                                  Вот здесь мы в одной только серии контроллеров для одного только элементарного параметра видим три варианта реализации, понять которые нельзя, их надо запомнить.

                        Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

                        Самое читаемое