Наиболее частые ошибки программирования

ID статьи: 24456
Дата последнего изменения: 30.09.2021 16:23:42

В данной статье собраны основные ошибки, которые возникают при работе с 32-разрядными микроконтроллерами.

Задание тактирования

Тактирование блока всегда должно задаваться ПЕРЕД настройкой его конфигурации!

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

Фрагмент кода 1 - Пример инициализации порта ввода-вывода

// 1 - Включаем тактирование порта С
RST_CLK_PCLKcmd (RST_CLK_PCLK_PORTC, ENABLE);

// 2 - Инициализируем порт в заданной конфигурации
PORT_StructInit(&GPIOInitStruct);
GPIOInitStruct.PORT_Pin = PORT_Pin_0;
GPIOInitStruct.PORT_OE = PORT_OE_OUT;
GPIOInitStruct.PORT_SPEED = PORT_SPEED_SLOW;
GPIOInitStruct.PORT_MODE = PORT_MODE_DIGITAL;

PORT_Init(MDR_PORTC, &GPIOInitStruct);

Программа работает с отладчиком, но не работает при подаче питания на МК

Если программа работает с отладчиком, но не работает после сброса по Reset  или после включения питания, необходимо проверить очередность включения тактирования и инициализации!

Это тот случай, когда тактирование в программе задано после настройки периферии. На примере настройки портов рассмотрим ход выполнения программы:

  1. После сброса по Reset или после включения питания запускается пользовательская программа, тактирование периферийных контроллеров по сбросу выключено.
  2. В программе происходит инициализация портов.
  3. В программе включается тактирование портов.

Так как тактирование портов не было включено на момент их инициализации, то настройка портов не была произведена. 

При запуске программы в режиме отладки происходит следующая последовательность действий:

  1. Отладчик подключается и перезапускает МК, при этом если в настройках отладчика выбран сброс только ядра, то тактирование периферийных блоков остаётся включённым с прошлого запуска.
  2. В программе происходит инициализация портов, которая выполняется успешно, так как тактирование портов включено. 
  3. В программе включается тактирование портов.

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

Переменные и флаги в прерываниях

Нельзя изменять значение переменной в основном потоке и в обработчике прерывания без атомарного доступа!

Например, простейшая операция " i++; " происходит в несколько этапов:

  1. В регистр ядра Rx загружается значение i из ячейки памяти.
  2. Значение регистра увеличивается на 1.
  3. Значение регистра сохраняется в ячейку памяти i.

В любой момент между этими шагами может возникнуть прерывание, в обработчике которого также возможно изменение переменной i. В этом случае при выходе из обработчика прерывания новое значение переменной i будет изменено в основной программе на шаге 3, при этом логика программы может быть нарушена.

В ядре Cortex M существуют операции для защищенного обращения к памяти LDREX и STREX. Информацию по их использованию можно найти, например, на официальном сайте Keil.

Вторым вариантом обращения к памяти может быть использование метода bit-band, который доступен для МК на базе Cortex M3/M4. Запись и стирание флагов в ячейках памяти происходит атомарными операциями типа чтение-модификация-запись.

Программирование Flash-памяти

Flash память МК может работать в двух режимах: в обычном режиме (доступ к памяти осуществляется через шины I Code и D Code) и в режиме программирования (доступ к памяти осуществляется через регистры контроллера Flash-памяти). В режиме программирования программный код должен выполняться из области системной шины (внешняя память) или ОЗУ. Выполнение программного кода из Flash-памяти в режиме программирования невозможно. При попытке доступа ядра к Flash-памяти, находящейся в режиме программирования, будет вызвано прерывание HardFault или BusFault в зависимости от настроек ядра (SCR регистры). Поэтому важно, чтобы при программировании Flash-памяти не возникало никаких прерываний, поскольку таблица векторов и обработчики прерываний по умолчанию расположены во Flash-памяти.

Чтобы запретить выбранное прерывание IRQn необходимо вызвать функцию NVIC_DisableIRQ(). При этом необходимо учитывать, что запрещение генерации запроса прерываний от системного таймера SysTick выполняется не в контроллере NVIC, а в регистре управления системным таймером.

Пример запрещения прерываний от системного таймера приведён в фрагменте кода 2.

Фрагмент кода 2 - Запрещение прерываний от системного таймера

SysTick->CTRL &= ~SysTick_CTRL_TICKINT_Msk;

Чтобы запретить сразу все прерывания, кроме HardFaut и NMI, необходимо выполнить специальную функцию, как показано в фрагменте кода 3.

Фрагмент кода 3 - Запрещение всех прерывания, кроме HardFaut и NMI

 __disable_irq();

После работы с Flash-памятью необходимо вернуть разрешение прерываний.

В режиме программирования функции работы с Flash-памятью должны выполняться из области системной шины (внешняя память) или ОЗУ!
Пример расположения программного кода в памяти ОЗУ приведён в статье Расположение функций в ОЗУ, программирование EEPROM.

Выводы, совмещенные с JTAG и SWD

Иногда возникает необходимость использовать выводы, совмещенные с интерфейсом JTAG и SWD (далее для краткости используется обозначение JTAG, но подразумевается JTAG и SWD). При использовании функций библиотеки SPL выводы, совмещенные с JTAG B, перенастроить не получится, так как для них по умолчанию установлена защита. Например, функция PORT_Init() проверяет конфигурируемые выводы на принадлежность к JTAG B и не даёт их переназначать. Разрешение данной проверки определено в файле MDR32F9Qx_config.h с помощью макроопределения USE_JTAG_B, строка 80, как показано в фрагменте кода 4.

Фрагмент кода 4 - Макроопределения защиты выводов, совмещённых с JTAG, в файле MDR32F9Qx_config.h

#if (defined(USE_MDR1986VE9x) || defined (USE_MDR1901VC1T))
  
/* #define USE_JTAG_A */
#define USE_JTAG_B

#endif

Библиотечный файл MDR32F9Qx_config.h защищен от записи, поэтому необходимо предварительно в свойствах файла снять атрибут "Только чтение" (Правая клавиша мыши -> Свойства -> Только чтение). Для снятия защиты с выводов, совмещённых с JTAG A или B необходимо закомментировать соответствующее макроопределение: USE_JTAG_A или USE_JTAG_B. После этого данными выводами можно управлять с помощью функций SPL.

Необходимо обратить внимание, что после переопределения выводов, совмещенных с JTAG, выбранный интерфейс JTAG работать не будет. Программа при запуске будет переопределять эти выводы, и подключиться к МК через данный интерфейс будет невозможно. Для связи с МК необходимо будет использовать либо другой интерфейс JTAG, либо интерфейс UART при старте МК в режиме "UART загрузчик".
Чтобы сохранить работоспособность интерфейса JTAG при старте МК, необходимо в начале функции main() вставить пустой цикл на пару секунд. Этот цикл даст некоторую задержку перед переопределением выводов, совмещённых с JTAG. За это время отладчик успеет перехватить управление и остановить исполнение программы. Таким образом сохраняется возможность подключиться к МК, даже если выводы JTAG в программе используются по другому назначению.

Запись в регистры порта, выводы которого совмещенные с JTAG и SWD

Если в программе не используются функции SPL, то необходимо учитывать, что при записи в регистры MDR_PORTx→RXTX и MDR_PORTx→OE биты выводов, совмещенных с JTAG, необходимо сбрасывать. Если этого не сделать, то работа интерфейса будет нарушена, а отладка невозможна.

В качестве примера можно посмотреть реализацию функций PORT_SetBits() или PORT_ResetBits() библиотеки SPL.

Смена тактовой частоты

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

  • Если требуется, переключить мультиплексор С3 на промежуточный источник тактирования, например, HSI.
  • Настроить генератор HSE и/или умножитель частоты PLL и дождаться, пока он выйдет в рабочий режим.
  • Настроить в контроллере Flash-памяти число тактов паузы Delay до переключения на более высокую частоту.
  • Настроить поля SelectRI и LOW в регистре MDR_BKP→REG_0E.
  • Переключить мультиплексор С3 на новый источник тактирования.

При переходе на более низкую частоту, изменение значения Delay и SelectRI, LOW производят после смены частоты.

Таким образом, при смене тактовой частоты необходимо соблюдать следующие правила:

До перехода на новую частоту с помощью переключения мультиплексора C3 необходимо, чтобы новая частота была полностью сформирована, и МК был полностью готов к работе на ней.
На момент смены частоты значения Delay и SelectRI, LOW должны соответствовать максимальной частоте из старого и нового значения.

Пример инициализации тактирования в МК серии 1986ВЕ9х

В фрагменте кода 5 приведена функция CPU_Initialize(), инициализирующая тактирование в МК серии 1986ВЕ9х от умножителя частоты PLL с использованием генератора HSE, который работает на внешнем кварцевом резонаторе. Для работы функции CPU_Initialize() в проект необходимо подключить библиотечные файлы MDR32F9Qx_rst_clk.c,  MDR32F9Qx_eeprom.c, MDR32F9Qx_power.c.

Фрагмент кода 5 - Инициализация тактирования в МК серии 1986ВЕ9х

#include <MDR32F9Qx_rst_clk.h>
#include <MDR32F9Qx_eeprom.h>
#include <MDR32F9Qx_power.h>

// Инициализация системы тактирования микроконтроллера 
void CPU_Initialize (void) 

   // Сброс настроек системы тактирования 
   RST_CLK_DeInit(); 
    
   // Инициализация генератора на внешнем кварцевом резонаторе (HSE = 8 МГц) 
   RST_CLK_HSEconfig (RST_CLK_HSE_ON); 
   if(RST_CLK_HSEstatus() != SUCCESS){
       while (1);
   }
    
   // Инициализация блока PLL 
   // Настройка источника и коэффициента умножения PLL 
   // CPU_C1_SEL = HSE_CLK, PLLCPUo = HSE_CLK * 10 = 8 МГц * 10 = 80 МГц
   RST_CLK_CPU_PLLconfig (RST_CLK_CPU_PLLsrcHSEdiv1, RST_CLK_CPU_PLLmul10);
   // Включение PLL
   RST_CLK_CPU_PLLcmd (ENABLE);  
   if(RST_CLK_CPU_PLLstatus() == ERROR) {
       while (1);
   }
   // Подключение PLL к системе тактирования 
   // (CPU_C2_SEL = PLLCPUo = 80 МГц)
   RST_CLK_CPU_PLLuse (ENABLE);
   // Настройка коэффициента деления блока CPU_C3_SEL 
   // (CPU_C3_SEL = CPU_C2) 
   RST_CLK_CPUclkPrescaler (RST_CLK_CPUclkDIV1);
   
   // Настройка числа тактов паузы Delay в контроллере Flash-памяти
   // Тактовая частота до 100 МГц - Delay = 3
   RST_CLK_PCLKcmd (RST_CLK_PCLK_EEPROM, ENABLE);
   EEPROM_SetLatency(EEPROM_Latency_3);
   RST_CLK_PCLKcmd (RST_CLK_PCLK_EEPROM, DISABLE);

   // Настройка параметров регулятора напряжения SelectRI и LOW в контроллере BKP
   // Тактовая частота 80 МГц
   RST_CLK_PCLKcmd(RST_CLK_PCLK_BKP, ENABLE);
   POWER_DUccMode(POWER_DUcc_upto_80MHz);
   
   // Переключение тактовой частоты процессора на CPU_C3 
   // (HCLK = CPU_C3) 
   RST_CLK_CPUclkSelection (RST_CLK_CPUclkCPU_C3);
}

При использовании функции printf() отладка не доходит до main()

Это особенность компилятора Keil, заменяющего функционал printf() на инструкцию программной остановки BKPT для реализации механизма semihosting. При старте программы низкоуровневые библиотеки Си также выполняют инструкцию BKPT, что приводит к остановке исполнения программы. Чтобы Keil не реализовывал механизм semihosting необходимо выполнить один из указанных пунктов:

1) Исключить вызов printf() из проекта.

2) Описать функции, перенаправляющие стандартный поток ввода-вывода в требуемый интерфейс МК, например, как показано в статьях Printf через ITM и Printf через UART.

3) В настройках проекта "Options for Target -> Target" выбрать опцию "Use MicroLIB", которая позволяет использовать оптимизированную по размеру кода стандартную библиотеку Си, в которой исключен механизм semihosting. Подробнее про MicroLIB описано на официальном сайте Keil.

Переход по абсолютному адресу приводит к исключению HardFault

Иногда требуется перейти в функцию, расположенную по известному адресу в памяти. Если в коде это будет выражено так, как показано в фрагменте кода 6 или 7, то произойдёт вызов исключения HardFault:

Фрагмент кода 6 - Некорректный переход по заданному адресу на языке Си

// Адрес функции в памяти
#define BASE_ADDR_FUNC_IN_RAM 0x20005000

// Указатель на функцию в памяти по известному адресу
typedef void (*funcptr)();
funcptr funcInRAM = (funcptr) (BASE_ADDR_FUNC_IN_RAM);

// Вызов функции
funcInRAM();

Фрагмент кода 7 - Некорректный переход по заданному адресу на языке ассемблер

LDR R0,=(0x20005000)
BX R0

Это происходит, потому что адрес перехода должен быть нечетным, чтобы указать ядру о переходе на инструкцию THUMB, а не ARM! Подробнее об этом описано на сайте ARM Info Center.

На самом деле при переходе в фрагментах кода 6 и 7 происходит исключение UsageFault, но данное исключение по сбросу запрещено, поэтому происходит вызов обработчика исключения HardFault. Разрешение исключений BusFault, MemManage fault и UsageFault выполняется в регистрах ядра SCB (System Control Block), как показано в фрагменте кода 8. О том, как работать с исключениями в Cortex-M3/M4 приведено в Application Note 209 от ARM.

Фрагмент кода 8 - Разрешение исключений BusFault, MemManage fault и UsageFault 

#define SCB_SHCSR_USGFAULTENA (1 << 18)
#define SCB_SHCSR_BUSFAULTENA (1 << 17)
#define SCB_SHCSR_MEMFAULTENA (1 << 16)

SCB->SHCSR |= SCB_SHCSR_USGFAULTENA;
SCB->SHCSR |= SCB_SHCSR_BUSFAULTENA;
SCB->SHCSR |= SCB_SHCSR_MEMFAULTENA;

Таким образом, чтобы переход на заданный адрес произошел корректно, необходимо указывать в нулевом бите адреса перехода единицу, как показано в фрагментах кода 9 и 10.

Фрагмент кода 9 - Корректный переход по заданному адресу на языке Си

// Адрес функции в памяти 
#define BASE_ADDR_FUNC_IN_RAM 0x20005000

// Указатель на функцию в памяти по известному адресу
typedef void (*funcptr)();
funcptr funcInRAM = (funcptr) (BASE_ADDR_FUNC_IN_RAM + 1);

// Вызов функции 
funcInRAM();
Фрагмент кода 10 - Корректный переход по заданному адресу на языке ассемблер
LDR R0,=(0x20005001)
BX R0

Контактная информация

Сайт:https://support.milandr.ru
E-mail:support@milandr.ru
Телефон: +7 495 221-13-55