Содержание

MDR_Delay - Функция задержки и особенности её реализации в ассемблере

При реализации работы с любой периферией часто требуется обеспечить некоторую задержку между выполняемыми операциями. Для реализации задержки можно использовать следующие ресурсы:

Системный таймер используется системами реального времени, и использовать его для реализации задержки в библиотеке уже нельзя. Периферийные таймеры обычно используются для захвата внешних фронтов и генерации ШИМ в рабочих проектах. Отбирать ради задержки целый таймер не разумно, их и так зачастую не хватает. Блок DWT есть только в Cortex-M3 (1986ВЕ9х) и Cortex-M4 (1986ВЕ8Т, "Электросила"). Поэтому самым доступным решением является реализация задержки программным путем.

Программная реализация задержки на СИ

При реализации задержки удобно делать проверку на передачу нулевого количества циклов. Если в вызываемом коде количество циклов задержки высчитывается по какой-то формуле, то может случиться, что результат расчета будет нулевой, и на вход функции придет 0, что приведет к зацикливанию функции на очень долгий период - пересчитать придется 0хFFFFFFFF циклов.

Ниже представлены два варианта реализации задержки. Volatile использован для того, чтобы компилятор не решил, что декремент локальной переменной не имеет смысла и стоит от него избавиться.

void MDR_DelayA(volatile uint32_t Ticks)
{
  if (Ticks)
    while(--Ticks);
}

//  ===============  Compiler V6 -O0  ============    //  ===============  Compiler V5 -O0  ================
    39: void MDR_DelayA(volatile uint32_t Ticks)           39: void MDR_DelayA(volatile uint32_t Ticks)  
0x20000A6A B003      ADD           sp,sp,#0x0C             40: { 
0x20000A6C 4770      BX            lr                  0x2000069E B501      PUSH          {r0,lr}
0x20000A6E 0000      MOVS          r0,r0                   41:   if (Ticks) 
    40: {                                              0x200006A0 9800      LDR           r0,[sp,#0x00]
0x20000A70 B082      SUB           sp,sp,#0x08         0x200006A2 B128      CBZ           r0,0x200006B0
0x20000A72 4601      MOV           r1,r0                   42:     while(--Ticks); 
0x20000A74 9001      STR           r0,[sp,#0x04]       0x200006A4 BF00      NOP           
    41:   if (Ticks)                                   0x200006A6 9800      LDR           r0,[sp,#0x00]
0x20000A76 9801      LDR           r0,[sp,#0x04]       0x200006A8 1E40      SUBS          r0,r0,#1
0x20000A78 2800      CMP           r0,#0x00            0x200006AA 9000      STR           r0,[sp,#0x00]
0x20000A7A 9100      STR           r1,[sp,#0x00]       0x200006AC 2800      CMP           r0,#0x00
0x20000A7C D009      BEQ           0x20000A92          0x200006AE D1FA      BNE           0x200006A6
0x20000A7E E7FF      B             0x20000A80              43: } 
    42:     while(--Ticks);                            0x200006B0 BD08      POP           {r0,pc}
0x20000A80 E7FF      B             0x20000A82        
0x20000A82 9801      LDR           r0,[sp,#0x04]
0x20000A84 3801      SUBS          r0,r0,#0x01
0x20000A86 9001      STR           r0,[sp,#0x04]
0x20000A88 2800      CMP           r0,#0x00
0x20000A8A D001      BEQ           0x20000A90
0x20000A8C E7FF      B             0x20000A8E
0x20000A8E E7F8      B             0x20000A82
0x20000A90 E7FF      B             0x20000A92
    43: } 
0x20000A92 B002      ADD           sp,sp,#0x08
0x20000A94 4770      BX            lr

//  ===============  Compiler V6 -Oz  ===========      //  ===============  Compiler V5 -O2  ================
    39: void MDR_DelayA(volatile uint32_t Ticks)           39: void MDR_DelayA(volatile uint32_t Ticks) 
0x200005E8 B001      ADD           sp,sp,#0x04             40: { 
0x200005EA 4770      BX            lr                  0x2000061C B501      PUSH          {r0,lr}
    40: {                                                  41:   if (Ticks) 
0x200005EC B081      SUB           sp,sp,#0x04         0x2000061E 9800      LDR           r0,[sp,#0x00]
0x200005EE 9000      STR           r0,[sp,#0x00]       0x20000620 2800      CMP           r0,#0x00
    41:   if (Ticks)                                   0x20000622 D002      BEQ           0x2000062A
0x200005F0 9800      LDR           r0,[sp,#0x00]       0x20000624 1E40      SUBS          r0,r0,#1
0x200005F2 B118      CBZ           r0,0x200005FC           42:     while(--Ticks); 
    42:     while(--Ticks);                            0x20000626 9000      STR           r0,[sp,#0x00]
0x200005F4 9800      LDR           r0,[sp,#0x00]       0x20000628 E7FA      B             0x20000620
0x200005F6 3801      SUBS          r0,r0,#0x01             43: } 
0x200005F8 9000      STR           r0,[sp,#0x00]       0x2000062A BD08      POP           {r0,pc}
0x200005FA D1FB      BNE           0x200005F4          
    43: }                                              
0x200005FC B001      ADD           sp,sp,#0x04      
0x200005FE 4770      BX            lr

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

static void MDR_DelayB(uint32_t Ticks)
{
  for (; Ticks > 0; Ticks--);
}

//  ===============  Compiler V6 -O0  ===========     //  ===============  Compiler V5 -O0  ================
   104: void MDR_DelayB(uint32_t Ticks)                   45: void MDR_DelayB(volatile uint32_t Ticks) 
0x20000A98 2800      CMP           r0,#0x00               46: { 
0x20000A9A D002      BEQ           0x20000AA2         0x200006B2 B501      PUSH          {r0,lr}
0x20000A9C BF00      NOP                                  47:   for (; Ticks > 0; Ticks--); 
0x20000A9E 1E40      SUBS          r0,r0,#1           0x200006B4 E002      B             0x200006BC
0x20000AA0 D1FD      BNE           0x20000A9E         0x200006B6 9800      LDR           r0,[sp,#0x00]
0x20000AA2 4770      BX            lr                 0x200006B8 1E40      SUBS          r0,r0,#1
    46: {                                             0x200006BA 9000      STR           r0,[sp,#0x00]
0x20000AA4 B082      SUB           sp,sp,#0x08        0x200006BC 9800      LDR           r0,[sp,#0x00]
0x20000AA6 4601      MOV           r1,r0              0x200006BE 2800      CMP           r0,#0x00
0x20000AA8 9001      STR           r0,[sp,#0x04]      0x200006C0 D1F9      BNE           0x200006B6
    47:   for (; Ticks > 0; Ticks--);                     48: } 
0x20000AAA 9100      STR           r1,[sp,#0x00]      0x200006C2 BD08      POP           {r0,pc}
0x20000AAC E7FF      B             0x20000AAE         
0x20000AAE 9801      LDR           r0,[sp,#0x04]
0x20000AB0 2800      CMP           r0,#0x00
0x20000AB2 D005      BEQ           0x20000AC0
0x20000AB4 E7FF      B             0x20000AB6
0x20000AB6 E7FF      B             0x20000AB8
0x20000AB8 9801      LDR           r0,[sp,#0x04]
0x20000ABA 3801      SUBS          r0,r0,#0x01
0x20000ABC 9001      STR           r0,[sp,#0x04]
0x20000ABE E7F6      B             0x20000AAE
    48: } 
0x20000AC0 B002      ADD           sp,sp,#0x08
0x20000AC2 4770      BX            lr

//  ===============  Compiler V6 -Oz  ===========     //  ===============  Compiler V5 -O2  ================
   113: void MDR_DelayB(volatile uint32_t Ticks)      45: void MDR_DelayB(volatile uint32_t Ticks)
0x20000600 2800      CMP           r0,#0x00               46: { 
0x20000602 D002      BEQ           0x2000060A         0x2000062C B501      PUSH          {r0,lr}
0x20000604 BF00      NOP                                  47:   for (; Ticks > 0; Ticks--); 
0x20000606 1E40      SUBS          r0,r0,#1           0x2000062E 9800      LDR           r0,[sp,#0x00]
0x20000608 D1FD      BNE           0x20000606         0x20000630 E001      B             0x20000636
0x2000060A 4770      BX            lr                 0x20000632 1E40      SUBS          r0,r0,#1
    46: {                                             0x20000634 9000      STR           r0,[sp,#0x00]
0x2000060C B081      SUB           sp,sp,#0x04        0x20000636 2800      CMP           r0,#0x00
0x2000060E E001      B             0x20000614         0x20000638 D1FB      BNE           0x20000632
    47:   for (; Ticks > 0; Ticks--);                     48: } 
0x20000610 9800      LDR           r0,[sp,#0x00]      0x2000063A BD08      POP           {r0,pc}
0x20000612 3801      SUBS          r0,r0,#0x01        
0x20000614 9000      STR           r0,[sp,#0x00]
0x20000616 9800      LDR           r0,[sp,#0x00]
0x20000618 2800      CMP           r0,#0x00
0x2000061A D1F9      BNE           0x20000610
    48: } 
0x2000061C B001      ADD           sp,sp,#0x04
0x2000061E 4770      BX            lr

(Сборка происходила под 1986ВЕ8Т, т.е. Cortex-M4. С вытаскиванием кода мог и накосячить, но общий вид понятен.)

Как видно по ассемблерному коду, длительность исполнения функций сильно зависит от опций компилятора и от самого компилятора. Складывается впечатление, что компилятор Compiler V6, который LVVM, мог бы быть оптимальней. Ведь код у Compiler V5 получается оптимальнее даже без оптимизации.

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

Программная реализация задержки на ASM

За основу ассемблерного варианта взят код от Compiler V5, потому что он короче и понятнее. Убрано обращение к памяти с загрузкой значения в регистр (LDR) и выгрузкой значения в память (STR). Учтено, что входной параметр функции, согласно соглашению языка Си, передан через регистр R0. Т.е. в R0 в функцию пришло количество циклов, которые необходимо исполнить. Исполнение крутится на двух инструкциях в теле __NextLoop в количестве Ticks раз, декрементируя регистр R0, пока он не станет нулем. Код такой:

__asm void MDR_DelayASM_RAM(uint32_t Ticks)
{
           CMP   r0,#0x00
           BEQ   __Exit
           NOP   
__NextLoop SUBS  r0,r0,#1
           BNE   __NextLoop
__Exit BX  lr
}

Важно, чтобы название меток начиналось с начала строки, иначе компилятор не понимает, что это метка! Данный код собирается без проблем Compiler V5. Но Compiler V6 собирать данный код отказался. Собрать код удалось в таком варианте:

  __attribute__((naked)) void MDR_DelayASM_RAM(uint32_t Ticks)
  {
    __asm("CMP   r0,#0x00");
    __asm("BEQ   __Exit");
    __asm("NOP");
    __asm("__NextLoop: SUBS  r0,r0,#1");
    __asm("BNE   __NextLoop");
    __asm("__Exit: BX  lr");
  }

Код аналогичен версии для Compiler V5, только метки выделены двоеточием и каждая строка включена в кавычки. Кроме этого Compiler V6 не понимает атрибут __asm, для него необходимо указывать __attribute__((naked)), чтобы компилятор не вставлял Push и Pop для данной функции. Про атрибут написано тут __attribute__((naked)) function attribute

Данный код для Compiler V6 можно записать еще так:

__attribute__((naked)) void MDR_DelayASM(uint32_t Ticks)
{
  __asm(
  "  CMP   r0,#0x00     ;\n"
  "  BEQ   1f           ;\n"
  "  NOP                ;\n"
  "0:                   ;\n"
  "  SUBS  r0,r0,#1     ;\n"
  "  BNE   0b           ;\n"
  "1:                   ;\n"
  "  BX  lr             ;\n"
  );
}

Здесь выявились свои особенности. Метки должны находиться на отдельной строке и они не могут быть заданы словами, только цифрами. Для прыжка на метку необходимо указывать, в какую сторону будет переход, нашел здесь. Например:

Есть прочие способы писать ассемблерный код, но разбираться в них пока не возникло необходимости. Оставлю ссылки, которые могут быть полезны при задании input/output операндов - ARM Description, Forum example.

Реализация задержки через блок DWT

Блок Data Watchpoint and Trace Unit входит в состав отладочного модуля ядер Cortex-M3 и Cortex-M4. В данном блоке есть счетчик, который считает такты ядра и который используется отладчиком при трассировке. Перед работой данный блок необходимо включить, и после этого можно работать со счетчиком данного блока. Код:

#define    regDWT_CYCCNT    *(volatile unsigned long *)0xE0001004
#define    regDWT_CONTROL   *(volatile unsigned long *)0xE0001000
#define    regSCB_DEMCR     *(volatile unsigned long *)0xE000EDFC
  
#define    CoreDebug_DEMCR_TRCENA_Msk   0x01000000UL
#define    DWT_CTRL_CYCCNTENA_Msk       0x1UL

void MDR_DelayDWT_Init(void)
{
  //разрешаем использовать счётчик
  regSCB_DEMCR |= CoreDebug_DEMCR_TRCENA_Msk;
  //обнуляем значение счётного регистра
  regDWT_CYCCNT  = 0;
  //запускаем счётчик
  regDWT_CONTROL |= DWT_CTRL_CYCCNTENA_Msk; 
}

void MDR_DelayDWT(uint32_t clockCountCPU)
{    
  uint32_t t0 = regDWT_CYCCNT;
  while ((regDWT_CYCCNT - t0) < (clockCountCPU));
}

Проверка работы функций

Для проверки работы функций задержки был реализован проект, который доступен на GitHub в рамках разрабатываемой нами библиотеки.

В проекте переключается пин светодиода микроконтроллера с задержкой в 3мс. Каждая функция реализует один период, т.е. формируется длительность нуля и единицы.

  while (1)
  { 
    //  Asm Delay
    MDRB_LED_Set(MDRB_LED_1, 0);
    MDR_DelayASM(asmDelay);
    MDRB_LED_Set(MDRB_LED_1, 1);
    MDR_DelayASM(asmDelay);

    //  Asm Delay from RAM
    MDRB_LED_Set(MDRB_LED_1, 0);
    MDR_DelayASM_RAM(asmDelayRAM);
    MDRB_LED_Set(MDRB_LED_1, 1);
    MDR_DelayASM_RAM(asmDelayRAM);
    
    //  C Delay
    MDRB_LED_Set(MDRB_LED_1, 0);
    MDR_DelayC(cDelay);
    MDRB_LED_Set(MDRB_LED_1, 1);
    MDR_DelayC(cDelay);

#if DWT_AVAILABLE
    //  WDT Delay
    MDRB_LED_Set(MDRB_LED_1, 0);
    MDR_DelayDWT(dwtDelay);
    MDRB_LED_Set(MDRB_LED_1, 1);
    MDR_DelayDWT(dwtDelay);    
#endif

    //  New Cycle
    MDRB_LED_Set(MDRB_LED_1, 0);    
    MDR_DelayC(cDelay);
    MDR_DelayC(cDelay);    
    MDRB_LED_Set(MDRB_LED_1, 1);
    MDR_DelayC(cDelay);    
    MDR_DelayC(cDelay);    
  }

Функции MDR_DelayASM и MDR_DelayASM_RAM - это уже упомянутые функции на ассемблере, но MDR_DelayASM_RAM исполняется из ОЗУ. MDR_DelayC - это функция с циклом While, также упомянутая выше. MDR_DelayDWT - функция с использованием DWT.

На вход этих функций подается количество циклов, которые функция должна исполнить, чтобы выдержать необходимую задержку. Количество циклов считается по данной формуле:

  #define LOOP_CYCLES_ASM       3
  #define LOOP_CYCLES_ASM_RAM   9
  #define LOOP_CYCLES_C         8
  #define LOOP_CYCLES_DWT       1

  #define DELAY_US    3000
  #define US_TO_LOOPS(uSec, CPU_FregHz, LoopCycles)  ((uint32_t)((double)(uSec) * (CPU_FregHz) / LoopCycles / 1000000 ))

  uint32_t asmDelay     = US_TO_LOOPS(DELAY_US, freqCPU_Hz, LOOP_CYCLES_ASM);
  uint32_t asmDelayRAM  = US_TO_LOOPS(DELAY_US, freqCPU_Hz, LOOP_CYCLES_ASM_RAM);
  uint32_t cDelay       = US_TO_LOOPS(DELAY_US, freqCPU_Hz, LOOP_CYCLES_C);
  uint32_t dwtDelay     = US_TO_LOOPS(DELAY_US, freqCPU_Hz, LOOP_CYCLES_DWT);  

Параметры LOOP_CYCLES_ASM и прочие задают, сколько тактов CPU занимает исполнение одного цикла задержки. Действительно, если задержку в секундах умножить на частоту CPU, что есть количество тактов в секунду, то это будет количество тактов за секунду. Если задержка задается в микросекундах, то необходимо добавить деление на 106. Но каждый цикл занимает несколько тактов, поэтому, поделив все еще на и количество тактов в цикле (LOOP_CYCLES_ASM), получим необходимое количество циклов.

Значения LOOP_CYCLES_ASM, LOOP_CYCLES_ASM_RAM, LOOP_CYCLES_C, LOOP_CYCLES_DWT подобраны так, чтобы на осциллографе наблюдались импульсы с полупериодом, равным задержке, т.е. 3мс. Далее менялись компиляторы и настройки компиляторов, чтобы посмотреть, как будет при этом изменяться наблюдаемый на осциллографе сигнал. Стоит отметить, что вариант с DWT не исполняется программно, поэтому LOOP_CYCLES_DWT всегда равен 1.

На каждой осциллограмме ниже представлен один и тот же сигнал, только маркерами выделен тот участок, для которого осциллограф указывает период. Период, соответственно, состоит из двух задержек, т.е. период должен получаться по 6 мс.

Осциллограммы показывают, что ассемблерная реализация задержки не зависит от настроек компилятора, в то время как программная реализация на языке Си может различаться значительно. Но реализация на ассемблере зависит от того, из какой памяти исполняется код - из ОЗУ или EEPROM (об этом ниже). Реализация на DWT ожидаемо не зависит ни от чего.

Результаты на Cortex M0/M1/M3/M4

Чтобы проверить, как будут отрабатывать функции на разных платформах проект был запущен на 1986ВЕ4У, 1986ВЕ1Т, 1986ВЕ93У, 1986ВЕ8Т. Запуск проводился только под Compiler V5, но зато от двух тактовых частот - от HSI и на максимальной частоте, получаемой для каждого МК на демоплате через PLL от HSE.

Задержка в 3мс взята достаточно большая, чтобы на ее фоне код переключения пина терялся по значимости. Для этого сначала измерялась задержка на пару порядков меньше, и затем измерялось еще несколько значений задержки, чтобы убедиться что на осциллографе импульсы меняются линейно, т.е. вклад задержки от переключения пина мал. Но там, где код компилируется без оптимизации, некоторое влияние от кода переключения пина может быть.

1986ВЕ9х

Микроконтроллер 1986ВЕ9х имеет ядро Cortex-M3. Задержки LOOP_CYCLES подбирались так, чтобы в режиме MAX_CLK(-O2) на осциллографе получались заданные задержки в ~3мс. При смене настроек компилятора "ARM Compiler V5" задержки изменяются так:

LOOP_CYCLES HSI(-O0) HSI(-O2) MAX_CLK(-O2) MAX_CLK(-O0)
ASM 3 3.09ms 3.09ms 3.00ms 3.03ms
ASM_RAM 9 3.09ms 3.09ms 3.00ms 3.03ms
C 6 4.08ms 3.06ms 3.00ms 4.01ms
DWT 1 3.09ms 3.09ms 3.00ms 3.03ms

Если сравнить строки ASM и ASM_RAM то видно, что для получения задержки в ~3мс необходимо рассчитывать количество циклов исходя из того, что исполнение одного цикла из памяти EEPROM занимает 3 такта ядра, в то время как исполнение из ОЗУ занимает 9 тактов. Это вызывает недоумение, ведь ассемблерный цикл выражен всего двумя инструкциями, как они могут занимать 9 тактов? И почему из более медленной EEPROM исполнение происходит быстрее!?

Это объясняется, скорее всего, так:
Действительно, ассемблерный код не обращается за памятью данных, значит шина данных не работает. Но выборка инструкций должна происходить, значит шина инструкций должна работать. Но флеш память имеет ускоритель, в который считывается сразу строка памяти, т.е. 4-ре 32-разрядных значения. Каждая инструкция THUMB занимает 2 байта, т.е. потенциально за одно чтение из флеш памяти выбирается сразу 8 инструкций. Если все инструкции цикла поместились в буфер ускорителя, т.е. уже считаны из памяти, то шина не обращается к памяти, а, следовательно, такты тратятся только на исполнение инструкций. Вот и получается, что цикл исполняется за 3 инструкции. 1 такт на декремент и 2 такта на прыжок на новый цикл.

В случае с ОЗУ шина инструкций не простаивает, а вычитывает инструкции. Кроме этого, есть еще трехступенчатый конвейер, который осуществляет предвыборку инструкций. В целом, видимо, это приводит к тому, что цикл занимает 9 тактов.

(Не факт что данное объяснение правильное, но других предположений пока нет.)

Выяснилось, что если в код добавить 4-ре инструкции NOP, то исполнение из флеш памяти также начинает занимать 9 тактов, как и при запуске из ОЗУ.

//  При значении 8 на осциллографе длительность начинает соответствовать необходимым 3мс.
#define LOOP_CYCLES_ASM       8

__attribute__((naked)) void MDR_DelayASM(uint32_t Ticks)
{
  __asm(
  "  CMP   r0,#0x00     ;\n"
  "  BEQ   1f           ;\n"
  "  NOP                ;\n"
  "0:                   ;\n"
  "  NOP                ;\n"
  "  NOP                ;\n"
  "  SUBS  r0,r0,#1     ;\n"
  "  NOP                ;\n"
  "  NOP                ;\n"
  "  BNE   0b           ;\n"
  "1:                   ;\n"
  "  BX  lr             ;\n"
  );
}

Итого, инструкций в цикле 6-сть, плюс две инструкции в конвейере предвыборки. Что сопоставимо со строкой выборки из флеш памяти на 8-мь инструкций. Т.е. скорее всего ускорение действительно возникает благодаря флеш-ускорителю.

1986ВЕ1Т

Микроконтроллер 1986ВЕ1Т - это ядро-аналог Cortex-M1, блока DWT в данном ядре нет. Задержки на осциллографе при LOOP_CYCLES получились следующие (Compiler V5):

LOOP_CYCLES HSI(-O0) HSI(-O2) MAX_CLK(-O2) MAX_CLK(-O0)
ASM 4 3.03ms 3.03ms 3.03ms 3.03ms
ASM_RAM 8 3.03ms 3.03ms 3.03ms 3.03ms
C 9 4.33ms 3.03ms 3.03ms 4.33ms

Код на СИ сильно зависит от оптимизации. Код на ассемблере более стабилен. Код из флеш памяти исполняется быстрее, вероятно, здесь тот же эффект - проявляется влияние ускорителя флеш. Но ассемблерный код занимает уже не 3 инструкции, а 4-ре.

1986ВЕ4У

Микроконтроллер 1986ВЕ4У - это ядро Cortex-M0. Здесь результаты получились следующие (Compiler V5):

LOOP_CYCLES HSI(-O0) HSI(-O2) MAX_CLK(-O2) MAX_CLK(-O0)
ASM 8 3.21ms 3.21ms 2.94ms 2.94ms
ASM_RAM 4 3.21ms 3.21ms 2.94ms 2.94ms
C 14 3.96ms 3.21ms 2.94ms 3.52ms

Выводы такие же - код на Си сильно зависит от опций компилятора. Здесь также видно, что HSI в данном МК был не совсем 8МГц, потому что результаты при тактировании от HSI и HSE больше различаются, чем в ранее расcмотренных микроконтроллерах.

Самое интересное, что в 1986ВЕ4У исполнение из флеш памяти занимает больше времени чем из ОЗУ. Это скорее всего говорит о том, что флеш не имеет ускорителя. Действительно, в 1986ВЕ4У строка памяти не состоит из 4-х значений по 32 бита. Читается за раз всего одно 32-битное слово, видимо, потому что частота данного МК небольшая, всего до 36МГц, и поэтому ускоритель тут не нужен. Вообще организации памяти в этом МК значительно отличается от описанных выше микроконтроллеров, как и архитектура ядра. Почему исполнение из ОЗУ происходит быстрее, чем уже привычные 8 тактов, не понятно.

1986ВЕ8Т

Микроконтроллер 1986ВЕ8Т - это ядро Cortex-M4. Задержки на осциллографе (Compiler V5):

LOOP_CYCLES HSI(-O0) HSI(-O2) MAX_CLK(-O2) MAX_CLK(-O0)
ASM 9 3.34ms 3.34ms 3.00ms 3.03ms
ASM_RAM 9 3.34ms 3.34ms 3.00ms 3.03ms
C 14 3.58ms 2.34ms 3.03ms 3.21ms
DWT 1 3.34ms 3.34ms 3.00ms 3.00ms

Здесь тоже значительно различаются результаты при тактировании от HSI и HSE0. Видимо внутренний HSI далек от 8МГц. Код запускался только из ОЗУ, поэтому строчки ASM и ASM_RAM ожидаемо совпадают. Запускать код из ОТР накладно.

Резюме

В текущий момент уже написано достаточно много примеров под "Pack V6", многие из которых используют функцию MDR_Delay, написанную на СИ. Но функция на СИ оказалась сильно не точна, поэтому будет заменена на ассемблерный вариант. Перепроверить все примеры достаточно кропотливая задача, уже сейчас количество сборок превышает 260 штук, т.к. каждый проект собирается под несколько микроконтроллеров. Возможно, в каких-то проектах это приведет к утере работоспособности, будем их исправлять по мере выявления.

В файлы MDR_ConfigXX.h добавлены значения LOOP_CYCLES для каждого МК, с которыми функция MDR_Delay будет работать по умолчанию в новых проектах. На примере MDR_ConfigVE9х.h это сделано так:

// =========================   MDR_Delay ===========================
//  Выбор реализации для MDR_Delay: 
//    По умолчанию используется ассемблерный вариант, универсальный
//    Вариант на Си сильно зависит от опций компилятора
//    Вариант на DWT есть только в Cortex-M3/M4 и требует предварительное включение вызовом MDR_Delay_Init().
#define   USE_MDR_DELAY_ASM
//#define   USE_MDR_DELAY_C
//#define   USE_MDR_DELAY_DWT

//  Исполнение функции задержки из ОЗУ / EEPROM происходит за разное количество тактов CPU. 
//  Данными параметрами можно уточнить сколько тактов CPU занимает один цикл задержки в MDR_Funcs, для повышения точности.
#define DELAY_LOOP_CYCLES_ASM       3
#define DELAY_LOOP_CYCLES_ASM_RAM   9
#define DELAY_LOOP_CYCLES_C         6
#define DELAY_LOOP_CYCLES_C_RAM    12
#define DELAY_LOOP_CYCLES_DWT       1


#ifdef USE_MDR_DELAY_C
  #define DELAY_LOOP_CYCLES         DELAY_LOOP_CYCLES_C
  #define DELAY_LOOP_CYCLES_RAM     DELAY_LOOP_CYCLES_C_RAM
#elif defined USE_MDR_DELAY_DWT  
  #define DELAY_LOOP_CYCLES         DELAY_LOOP_CYCLES_DWT
  #define DELAY_LOOP_CYCLES_RAM     DELAY_LOOP_CYCLES_WDT_RAM
#else
  #define DELAY_LOOP_CYCLES         DELAY_LOOP_CYCLES_ASM
  #define DELAY_LOOP_CYCLES_RAM     DELAY_LOOP_CYCLES_ASM
#endif

Активное значение USE_MDR_DELAY_ASM / USE_MDR_DELAY_C / USE_MDR_DELAY_DWT выбирает, какая реализация будет у MDR_Delay(). Сами реализации находятся в MDR_Func.c:

//  MDR_ConfigVEx.h: Выбор текущего варианта MDR_Delay - задержка в DELAY_LOOPS
#if defined (USE_MDR_DELAY_C)
  #define MDR_Delay_Init()    UNUSED(0)
  #define MDR_Delay()         MDR_DelayС
#elif defined (USE_MDR_DELAY_DWT)
  #define MDR_Delay_Init      MDR_DelayDWT_Init
  #define MDR_Delay           MDR_DelayDWT
#else
  #define MDR_Delay_Init()    UNUSED(0)
  #define MDR_Delay           MDR_DelayASM
#endif

Функция MDR_Delay_Init() необходима лишь для реализации с DWT, но для взаимозаменяемости реализаций эту функцию необходимо вставлять куда-нибудь в начало кода, до первого использования MDR_Delay().