Общее представление о системе тактирования


В прошлой статье мы научились создавать стабильные задержки с помощью простого таймера SysTick, а так же немного окунулись в механизм работы прерываний. Тогда мы принимали как постулат то, что тактируя таймер от источника HCLK – мы получаем 8 Мгц. Теперь настало время разобраться, откуда эти цифры.

Открыв документация и сделав переход к «Сигналы тактовой частоты MDR_RST_CLK» мы можем увидеть такую таблицу.

Встроенный RC генератор HSI
Генератор HSI вырабатывает тактовую частоту 8 МГц. Генератор автоматически запускается при появлении питания UСС.

Таким образом, после включения, контроллер тактируется от HSI. На блок схеме синим я выделил последовательность, которая демонстрирует изменения частоты на каждом блоке. По умолчанию модуль выбора источника частоты (MUX) настроен на прием с HSI. С частотой ничего не происходит и она, через HCLK (линия тактирования, совмещенная в том числе и с SysTick таймером) попадает в CPU_CLK без изменений. Но внутренний RS генератор нельзя назвать точным устройством. Его частота очень нестабильна. Для этих целей на плате установлен внешний кварцевый резонатор на 8 Мгц. Но как он называется?
Внешний генератор HSE
Генератор HSE предназначен для выработки тактовой частоты 2..16 МГц с помощью внешнего резонатора. Генератор запускается при появлении питания UCC…

Теперь нам нужно понять, как переключить контроллер с тактирования от HSI на HSE. Еще раз взглянем на блок схему. Красным цветом я обозначил логическое представление того, как будет проходить сигнал от внешнего кварцевого резонатора до линии тактирования ядра и HCLK.


Настройка регистров тактирования.

Теперь, когда мы имеем образное представление того, как «движется» тактирование, пора разобраться с регистрами тактирования. На схеме рядом с каждым блоком указаны биты определенных регистров, меняя которые можно менять «движение» и частоту тактирования. Данные биты находятся в регистре MDR_RST_CLK->CPU_CLOCK. Красным я обозначил те биты, которые нужно будет изменить. Синие же можно оставить так, как есть. Они уже установлены в правильном положении.

Далее нам нужно написать define-ы тех бит, значения которых мы будем менять.
#define HCLK_SEL(CPU_C3)       (1<<8)
#define CPU_C1_SEL(HSE)        (1<<1)

Так как все остальные биты равны нулю, то мы можем писать в регистр напрямую, не боясь стереть старые данные. В итоге выходит следующее.
RST_CLK->CPU_CLOCK  = CPU_C1_SEL(HSE)|HCLK_SEL(CPU_C3);

Казалось бы все. Но если мы зашьем это, то получим вот это.

С последующей невозможностью отладки. Я сразу же полез в документацию с описаниями ошибок. Но такой там не было. Как оказалось, я упустил одну важную специфическую деталь. В описании HSE было сказано:
Генератор HSE предназначен для выработки тактовой частоты 2..16 МГц с помощью внешнего резонатора. Генератор запускается при появлении питания UCC и сигнала разрешения HSEON в регистре HS_CONTROL.

Заглянем в этот регистр.

Пропишем еще 1 define и включим бит разрешения.
#define HS_CONTROL(HSE_ON)     (1<<0)
RST_CLK->HS_CONTROL = HS_CONTROL(HSE_ON);

Небольшое отступление.
Когда искал ошибку, снова вернулся на страницу тактирования периферии (рассматривали в предыдущей статье про SysTick), в которой нашел RST_CLK. По умолчанию он включен, но все таки я прописал его включение перед всеми манипуляциями с кварцевым резонатором.

После всех дополнений функция имела следующий вид.
#define HCLK_SEL(CPU_C3)       (1<<8)
#define CPU_C1_SEL(HSE)        (1<<1)
#define PCLK_EN(RST_CLK)       (1<<4)
#define HS_CONTROL(HSE_ON)     (1<<0)

void HSE_Init (void)
{
	RST_CLK->PER_CLOK |= PCLK_EN(RST_CLK);                                                //Включаем тактирование контроллера тактовой частоты (по умолчанию включено).
	RST_CLK->HS_CONTROL = HS_CONTROL(HSE_ON)                                              //Разрешаем использование HSE генератора.
	RST_CLK->CPU_CLOCK  = CPU_C1_SEL(HSE)|HCLK_SEL(CPU_C3);                               //Настраиваем "путь" сигнала и включаем тактирование от HSE генератора.
}

Первый «костыль».


В тот момент, когда я искал ошибку запуска HSE в перечне ошибок, я наткнулся на следующий глюк.

Я не мог не запомнить его, так как планировал сразу же после включение тактирования от внешнего кварцевого резонатора выключить внутренний генератор. Данная ошибка присутствует во всех ревизиях микроконтроллера. Так что для нее придется лепить костыль. Скажу сразу. В моем случае мне так и не удалось отключить HSI. Несмотря на то, что я делал все так, как сказано в рекомендациях.
Изучим проблему и пути «решения» поподробнее.

Вот тот упомянутые регистры.


Пропишем define для нужных бит, а так же для тактирования RTC.
#define REG_0F(HSI_ON)        ~(1<<22)
#define RTC_CS(ALRF)           (1<<2)
#define PCLK(BKP)              (1<<27)

А теперь сделаем попытку осуществить это на практике.
#define HCLK_SEL(CPU_C3)       (1<<8)
#define CPU_C1_SEL(HSE)        (1<<1)
#define PCLK_EN(RST_CLK)       (1<<4)
#define HS_CONTROL(HSE_ON)     (1<<0)

#define REG_0F(HSI_ON)        ~(1<<22)
#define RTC_CS(ALRF)           (1<<2)
#define PCLK(BKP)              (1<<27)
#define CPU_C2_SEL(CPU_C2_SEL) (1<<2)

void HSE_Init (void)
{
	RST_CLK->PER_CLOK |= PCLK_EN(RST_CLK);                                                //Включаем тактирование контроллера тактовой частоты (по умолчанию включено).
	RST_CLK->HS_CONTROL = HS_CONTROL(HSE_ON)                                              //Разрешаем использование HSE генератора.
	RST_CLK->CPU_CLOCK  = CPU_C1_SEL(HSE)|HCLK_SEL(CPU_C3);                               //Настраиваем "путь" сигнала и включаем тактирование от HSE генератора.
	RST_CLK->PER_CLOK |= PCLK(BKP);                                                       //Включаем тактирование часов (для костыля).
	BKP->RTC_CS |= RTC_CS(ALRF);                                                          //Костыль для отключения HSI.
	BKP->REG_0F  = BKP->REG_0F&(REG_0F(HSI_ON));                                          //Отключаем HSI.
}

Отслеживаем выполнения программы. Это состояние регистров до попытки сбросить бит.

А это после.

По началу я подумал, что ошибся со смещением и просо не включил бит ALRF, но потом нажал на него в меню справа (прямое оправление регистрами) и ничего не получил. Подумал, что не правильно выбрал тактирование, но нажав на несколько других ячеек – получил отклик. Видимо, данный метод не решает эту проблему. При попытке отключить генератор путем последовательного нажатия ALRF и HSI_ON — мы окончательно разочаровываемся в самой идеи отключения генератора. Что ж. Это не сильно мешает на начальном этапе. Но в будущем это станет проблематично. Особенно если делать карманное устройство.

Получаем 16 Мгц.

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

Теперь, перед тем, как по линии CPU_C1 частота «попадает» в CPU_C2, она проходит через CPU PLL. Там она «умножается» на какое-то значение. Рассмотрим его регистры поподробнее.

PLL не требует включения бита тактирования. Так что можно сразу приступать к настройке регистра. Опять же пропишем нужные нам define-ы.
#define PLL_CONTROL_PLL_CPU_ON  (1<<2)                   //PLL включена.


Ну саму функцию включения.
#define PLL_CONTROL_PLL_CPU_ON  (1<<2)                                                  //PLL включена. 
void HSE_16Mhz_Init (void)                                                              //Сюда передаем частоту в разах "в 2 раза" например. 
{
	RST_CLK->PLL_CONTROL  = PLL_CONTROL_PLL_CPU_ON|(1<<8);                                //Включаем PLL, умножение в 2 раза.
  RST_CLK->HS_CONTROL = HS_CONTROL(HSE_ON);                                             //Разрешаем использование HSE генератора. 
	RST_CLK->CPU_CLOCK  = CPU_C1_SEL(HSE)|HCLK_SEL(CPU_C3)|CPU_C2_SEL(CPU_C2_SEL) ;       //Настраиваем "путь частоты" и включаем тактирование от HSE генератора.
}

Теперь мы можем добавить функцию в проект.
int main (void)
{
	Init_SysTick();                                 //Инициализируем системный таймер. 
	Led_init();                                     //Инициализируем ножку 0 порта C для светодиода. 
	
	PORTC->RXTX |= 1;
	Delay_ms (1000);
	PORTC->RXTX = 0;
	Delay_ms (1000);
	
	HSE_16Mhz_Init();
	
  while (1)
	{
		PORTC->RXTX |= 1;
		Delay_ms (1000);
		PORTC->RXTX = 0;
	  Delay_ms (1000);
	}
}

Хочу заметить, что я не просто так оставил 1 цикл мелькания светодиода. Это так называемая «программа спасения». Если в ходе экспериментов что-то пойдет не так, то после нажатия RESET будет целая секунда, чтобы прошить МК исправленной прошивкой.
После прошивки светодиод 2 раза мелькнет от внутреннего кварца, а потом в 2 раза быстрее от внешнего.

Теперь оптимизируем наш код. Использовать функцию ради 3-х строк – глупо. Тем более 2 из них повторяются и во второй функции. Поэтому предлагаю сделать их define-ами. Первая функция предстает перед нами в таком виде.
#define RST_CLK_ON_Clock()       RST_CLK->PER_CLOCK |= PCLK_EN(RST_CLK)                 //Включаем тактирование контроллера тактовой частоты (по умолчанию включено).
#define HSE_Clock_ON()           RST_CLK->HS_CONTROL = HS_CONTROL(HSE_ON)               //Разрешаем использование HSE генератора. 
#define HSE_Clock_OffPLL()       RST_CLK->CPU_CLOCK  = CPU_C1_SEL(HSE)|HCLK_SEL(CPU_C3);//Настраиваем "путь" сигнала и включаем тактирование от HSE генератора.

А из второй предлагаю сделать универсальную функцию переключения частоты. Для этого из предыдущей функции выкидываем настройку HSE и добавляем в регистр PLL_CONTROL еще бит перезапуска PLL. Чтобы при получении нового значения сразу начать тактироваться на нем. Функция начинает иметь следующий вид.
#define PLL_CONTROL_PLL_CPU_ON  (1<<2)                                                  //PLL включена. 
#define PLL_CONTROL_PLL_CPU_PLD (1<<3)                                                  //Бит перезапуска PLL.
void HSE_PLL (uint8_t PLL_multiply)                                                              //Сюда передаем частоту в разах "в 2 раза" например. 
{
	RST_CLK->PLL_CONTROL  = RST_CLK->PLL_CONTROL&(~(0xF<<8));                                      //Удаляем старое значение.
	RST_CLK->PLL_CONTROL |= PLL_CONTROL_PLL_CPU_ON|((PLL_multiply-1)<<8)|PLL_CONTROL_PLL_CPU_PLD;  //Включаем PLL и включаем умножение в X раз, а так же перезапускаем PLL.
	RST_CLK->CPU_CLOCK   |= HCLK_SEL(CPU_C3)|CPU_C2_SEL(CPU_C2_SEL)|CPU_C1_SEL(HSE);               //Настриваем "маршрут" частоты через PLL и включаем тактирование от HSE.
}

Внесем ее в нашу основную программу. Которую тоже приводим в порядок.
void Block (void)                                //Подпрограмма ожидания (защиты).
{
	PORTC->RXTX |= 1;
	Delay_ms (1000);
	PORTC->RXTX = 0;
	Delay_ms (1000);
}

int main (void)
{
	Init_SysTick();                                 //Инициализируем системный таймер. 
	Led_init();                                     //Инициализируем ножку 0 порта C для светодиода. 
	Block();                                        //Подпрограмма ожидания (защиты).
	HSE_Clock_ON();                                 //Разрешаем использование HSE генератора. 
	HSE_PLL(2);                                     //Включаем тактирование с умножением 2

  while (1)
	{
		PORTC->RXTX |= 1;
		Delay_ms (1000);
		PORTC->RXTX = 0;
	        Delay_ms (1000);
	}
}

В качестве заключения, напишем программу, которая после каждого цикла будет менять коэффициент умножения увеличивая свою скорость вплоть до 10, получая 80 Мгц.
void Block (void)                                //Подпрограмма ожидания (защиты).
{
	PORTC->RXTX |= 1;
	Delay_ms (1000);
	PORTC->RXTX = 0;
	Delay_ms (1000);
}

int main (void)
{
	Init_SysTick();                                 //Инициализируем системный таймер. 
	Led_init();                                     //Инициализируем ножку 0 порта C для светодиода. 
	Block();                                        //Подпрограмма ожидания (защиты).
	HSE_Clock_ON();                                 //Разрешаем использование HSE генератора. 
	HSE_PLL(2);                                     //Включаем тактирование с умножением 2
	
	uint8_t PLL_Data = 1;                           //Здесь храним коэффициент умножения. 
        while (1)
	{
		PORTC->RXTX |= 1;
		Delay_ms (1000);
		PORTC->RXTX = 0;
	        Delay_ms (1000);
		if (PLL_Data<10) PLL_Data++; else PLL_Data=1; //Если не перешли максимум - умножаем еще. Перешли - с начала.
                HSE_PLL(PLL_Data); 
	}

Видео работы программы.


Восстановление платы.


После первого неудачного опыта с настройкой тактовой частоты, плата перестала отвечать. Так как я не предусмотрел защитную программу (посчитав ее не нужной), я начал искать пути реализации. В комментариях к одной из предыдущих статей vertu77 предложил 3 способа восстановления платы в случае неправильной настройки портов.
Если все-таки JTAG порт умер — что делать:
1. Тогда может пригодится другой JTAG порт. Если он не разведен на плате, можно припаяться только к ножкам SW на другом порту (меньше возни)
2. Залить прошивку через UART (штатный загрузчик)
3. Использовать особенности зашитой программы — успеть прошить новую заливку после подачи питания до первого мигания. В варианте автора это практически невозможно. В реальных программах часто программа запускается от внутреннего генератора, затем инициализируется внешний кварц. Если при этом используется бесконечный цикл ожидания — можно отпаять кварц и прошиться до первого мигания.

Но в моем случае оба JTAG-а не реагировали (завсали при попытке программирования МК), защитной программы не было, а USART загрузчик был очень далеко. Да и не хотелось тратить слишком много времени на восстановление. Так был придуман четвертый способ восстановления. Нужно переключить BOOT переключатели в режим EXT_ROM/JTAG_B, подключиться к JTAG_B и зашить код с прошивкой, в которой предусмотрена защитная программа. В моем случае я просто добавил цикл ожидания в одну секунду перед настройкой кварцевого резонатора. Так что после каждого неудачного опыта достаточно было нажать на RESET и успеть войти в режим отладки заново.


Файлы к проектам.

Комментарии (7)


  1. Amomum
    10.04.2015 18:12
    +3

    Попробуйте вставлять код как код, а не как картинки. Или прячьте его под спойлеры. Статья просто тонет в картинках.


    1. Vadimatorikda Автор
      10.04.2015 19:05

      Спасибо, исправил. Как оказалось, так даже проще.


  1. vertu77
    10.04.2015 23:48

    Интересно про четвертый способ восстановления. Припоминаю, что у меня была похожая ситуация — после перебора всех положений перемычек процессор ожил. Подумал что случайность, что-то сам перепутал. Похоже что нет — получается что режим 010 «круче» с точки зрения прошивки чем режим 000?


  1. Vadimatorikda Автор
    11.04.2015 02:42
    -1

    Не могу понять значение «круче». Когда мы переставляем перемычке в режим 010, то выполнение программы при отладке происходит из ОЗУ. Так как в настройках мы указали адреса Flash, то нажав на Debag, контроллер очищает все ячейки памяти Flash (тоже указали в настройках), прошивает нашей программой, переставляет указатель на начало ОЗУ и начинает «выполнение программы». Так как мы ничего в ОЗУ не писали, то у нас там будут нули. Таким образом, мы получаем программу из кучи нулей и перепрошивку Flash. Отладка Flash, как я понял, в режиме 010, не возможна. Как вариант, изменить значения адреса в настройках. Чтобы программа грузилась не во Flash, а в ОЗУ. Это бы значительно увеличило ресурс контроллера. В нашем контроллере приличное количество ОЗУ. Так что, чисто теоретически, это возможно. Попробую сам сделать это. Напишу в одной из следующих статей.


  1. vertu77
    11.04.2015 16:03
    +1

    Спасибо. Позволю, в свою очередь, немного уточнить — как теперь понял это я.
    Дано — программа запускается из внутреннего ПЗУ, но в силу ошибок (неверное тактирование, блокировка пинов, еще что-то) после запуска не дает корректно работать с портом JTAG_B отладчику.
    Выбираем режим 010 — после перезагрузки «процессор пытается начать выполнять программу из внешней памяти, установленной на внешней системной шине». Тут я думаю имеется в виду скорее внешнее ПЗУ чем ОЗУ, то есть это режим не микроконтроллера, а микропроцессора. Так как там ничего нет, JTAG порт остается рабочим. Дальше уже дело внутрисхемного отладчика, работа его ничем не отличается от режима 000. Он через JTAG порт загружает в ОЗУ с адреса 0x20000000 размером 0x0800 «algorithm for download». Загрузчик запускается в ОЗУ и делает то, что мы его попросили в настройках — например «Erase full chip» с адреса 0x08000000, то есть очищаем внутреннее ПЗУ.
    Как отлаживаться, загружая прошивку в ОЗУ — не знаю. Видел упоминания, что есть такая возможность. Думаю, это будет полезно многим.
    Сам делал на этом МК загрузку отдельных модулей программы в ОЗУ из ПЗУ после старта. Keil позволяет делать это достаточно легко. Это, в частности, необходимо если мы захотим использовать часть ПЗУ для хранения пользовательских данных.


    1. vertu77
      11.04.2015 16:07

      Дополню себя — жаль что этот способ неприменим, если плате разведен только JTAG_A.


      1. Vadimatorikda Автор
        11.04.2015 16:31

        после запуска не дает корректно работать с портом JTAG_B отладчику.

        Не дает работать с обоими портами. И JTAG_A и JTAG_B.
        Выбираем режим 010 — после перезагрузки «процессор пытается начать выполнять программу из внешней памяти, установленной на внешней системной шине». Тут я думаю имеется в виду скорее внешнее ПЗУ чем ОЗУ, то есть это режим не микроконтроллера, а микропроцессора. Так как там ничего нет, JTAG порт остается рабочим.

        Хммммм… Я только неделю разбираюсь с данным контроллером. Вы поставили меня в тупик. Скорее всего, вы правы. Прошу простить мою ошибку.
        Он через JTAG порт загружает в ОЗУ с адреса 0x20000000 размером 0x0800 «algorithm for download». Загрузчик запускается в ОЗУ и делает то, что мы его попросили в настройках — например «Erase full chip» с адреса 0x08000000, то есть очищаем внутреннее ПЗУ.

        Тоже, честно сказать, не знаю. В низкоуровневой отладке не разбирался.
        Как отлаживаться, загружая прошивку в ОЗУ — не знаю. Видел упоминания, что есть такая возможность. Думаю, это будет полезно многим.
        Сам делал на этом МК загрузку отдельных модулей программы в ОЗУ из ПЗУ после старта. Keil позволяет делать это достаточно легко. Это, в частности, необходимо если мы захотим использовать часть ПЗУ для хранения пользовательских данных.

        У меня по плану стоят еще 4 статьи на тему периферии. После этого я хочу подробнее разобраться как раз таки с памятью и ее разделами.
        Спасибо за ваш вопрос. Теперь я знаю, о чем будет 5-я статья после этой.