Введение

Мы продолжаем цикл статей по микроконтроллерам компании Megawin на ядре Cortex-M0. В предыдущей статье были рассмотрены: методика разработки и прошивки на основе gcc и OpenOCD, контроллер flash-памяти, работа с GPIO. В этой статье будут рассмотрены: периферийные модули UART, обработчики прерываний UART, метод отладки кода в ОЗУ МК с базовой частью инициализации во flash-памяти, механизм системных вызовов, тактирование МК от различных источников.

Модули UART МК MG32F02

Общие сведения

Микроконтроллеры серии MG32F02 включают два варианта модулей UART: расширенный (URT0-URT2) и базовый (URT4-URT7). В МК MG32F02A032 имеются только два расширенных модуля URT0 и URT1. Строго говоря, расширенный вариант корректнее называть USART, т.е. универсальный синхронный-асинхронный приёмопередатчик, поскольку он поддерживает множество режимов работы, в том числе с синхронизацией передачи данных.

Все модули UART имеют следующие особенности:

  • полный дуплексный режим работы,

  • скорость обмена до 6 Мбит/с,

  • длина символа 7 или 8 бит,

  • передискретизация (oversampling) при приёме с коэффициентом 4-32,

  • поддержка генерации и проверки бита четности/нечетности,

  • отдельное разрешение работы приемника и передатчика,

  • возможность перемены местами выводов RX и TX,

  • возможность отдельного изменения полярности сигналов RX и TX.

В режиме SPI (только для расширенного варианта UART) максимальная частота синхросигнала URTx_CLK составляет 16 МГц для MG32F02A032, 18 МГц для MG32F02A128/A064 и MG32F02U128/U064.

Расширенный вариант (модули URT0-URT2)

Функциональные возможности

Расширенный вариант UART (модули URT0-URT2) имеет следующие особенности:

  • асинхронный и синхронный режим работы,

  • режимы SPI master и slave, SmartCard, LIN, мультипроцессорный,

  • настраиваемый порядок бит в символе (MSB или LSB),

  • настраиваемая длительность стоп-бита 0.5, 1, 1.5 или 2,

  • детектирование сигнала RX по одной или трем выборкам,

  • таймер для определения таймаута событий Idle/RX/Break/Calibration,

  • "вход" и "выход" из режима "mute" в мультипроцессорном режиме работы,

  • размер буфера данных 4 байта с доступом ко всему буферу как 32-битному слову,

  • поддержка автоматического определения скорости обмена,

  • мультипроцессорный режим (с адресацией) в роди ведущего или ведомого,

  • поддержка формата IrDA,

  • аппаратный контроль потока на основе сигналов RTS и CTS,

  • формирование сигнала управления передачей для двунаправленного интерфейса (например, RS-485),

  • аппаратное определение ошибки при приеме и передаче в режиме SmartCard,

  • прием и передача данных на основе DMA,

  • частичная аппаратная поддержка программного контроля потока XON/XOFF.

Отметим, что расширенные модули UART в МК MG32F02A032 отличаются от остальных моделей схемой тактирования.

Функциональная схема расширенного варианта UART МК MG32F02A128/U128/A064/U064 приведена на рисунке.

Функциональная схема расширенного UART
Функциональная схема расширенного UART

Модуль использует следующие внешние сигналы:

  • URTx_CLK — тактовый сигнал для синхронного UART или режима SPI,

  • URTx_RX — принимаемые данные RX для режима UART или MISO для режима SPI master,

  • URTx_TX — передаваемые данные TX для режима UART или MOSI для режима SPI master,

  • URTx_NSS — входящий/исходящий сигнал выбора кристалла (chip select) для режима SPI slave/master соответственно,

  • URTx_CTS — входящий сигнал аппаратного управления потоком UART CTS,

  • URTx_RTS — исходящий сигнал аппаратного управления потоком UART RTS,

  • URTx_DE — сигнал "передача" для внешнего драйвера двунаправленного интерфейса,

  • URTx_BRO — выход тактового генератора,

  • URTx_TMO — выход интервального таймера таймаута TMO.

Модуль имеет основной узел управления тактированием Clock Control, принимающий исходный тактовый сигнал с делителя частоты Baud-Rate Generator. Блок Mode Control управляет режимом работы модуля. Блок Data Control управляет приемом и передачей данных и общей буферизацией. Блок Receiver Control обеспечивает физический уровень приема данных, детектирование ошибок, а также декодирование сигнала в режиме IrDA. Блок Transmitter Control обеспечивает физический уровень передачи данных, а также кодирование сигнала в режиме IrDA. Прием и передача данных будут рассмотрены далее.

Блок Event Detector определяет различные состояния модуля, включая возможные ошибки при приеме и передаче данных, а блок Interrupt Control формирует на их основе общий сигнал прерывания INT_URTx. Оба модуля будут рассмотрен далее. 16-разрядный интервальный таймер TMO предназначен для выдерживания временных интервалов и обнаружения таймаутов согласно выбранному режиму работы и настройкам. Таймер TMO может использоваться для других задач отдельно от модуля UART, даже, если модуль UART выключен.

Блок Hardware Flow Control обеспечивает аппаратный контроль потока на основе сигналов RTS/CTS. Контроль потока на основе символов XON/XOFF может быть реализован только программно, аппаратно имеется лишь возможность приостановить отправку данных (бит URTx_TX_HALT регистра URTx_CR2).

Блок Drive Enable Timing Control предназначены для управления внешним драйвером двунаправленного интерфейса (например, RS-485). Блок SPI NSS Timing Control обеспечивает управление сигналом NSS в режиме SPI. Блок Multi-Processor Control отвечает за мультипроцессорный режим, а блок Baud-Rate Calibration за калибровку частоты работы модуля UART.

Для включения модуля UART необходимо:

  • включить тактирование модуля URTx в регистре CSC_APB0 установкой соответствующего бита CSC_URTx_EN (предварительно нужно разблокировать возможность записи через регистр CSC_KEY),

  • установить бит URTx_EN в регистре URTx_CR0.

Схема управления тактированием

Схема управления тактированием Clock Control МК MG32F02A128/U128/A064/U064 приведена на следующем рисунке.

Схема управления тактированием расширенного UART
Схема управления тактированием расширенного UART

Первичный источник тактовых импульсов для расширенного модуля UART выбирается установкой поля URTx_CK_SEL регистра URTx_CLK из числа следующих:

  • сигнал CK_URTx_PR с выхода подсистемы тактирования CSC,

  • общесистемный сигнал CK_LS тактирования периферии,

  • выходной сигнал таймера TM00 TM00_TRGO,

  • выходной сигнал модуля NCO (Numerically Controlled Oscillator) NCO_P0 (кроме МК MG32F02A032).

Сигнал CK_URTx_PR формируется из сигнала CK_APB или CK_AHB в зависимости от значения бита CSC_URTx_CKS регистра CSC_CKS1. Сигнал CK_LS формируется из сигналов CK_ILRCO, CK_XOSC или CK_EXT в зависимости от значения поля CSC_LS_SEL регистра CSC_CR0 (см. первую статью цикла).

Выбранный тактовый сигнал (обозначен на схеме как CK_URTx) поступает на блок BR (Baud-Rate Generator), состоящий из 6-битного предделителя частоты (4-битного для МК MG32F02A032) и 8-битного счетчика. Предделитель формирует сигнал URTx_CKO, используемый для тактирования счетчика, а также как внешний тактовый сигнал синхронного обмена. В регистре URTx_RLR в поле URTx_PSR задается коэффициент деления предделителя Kp, а в поле URTx_RLR — коэффициент деления счетчика Kc. Реальные значения коэффициентов будут на единицу больше. 8-битный счетчик CNT формирует внутренний сигнал CK_URTx_INT, который далее используется для тактирования передатчика и приемника.

Тактирование передатчика и приемника может осуществляться также от выходных сигналов таймеров TM01_TRGO или TM10_TRGO, и от внешнего синхросигнала URTx_ECK. Выбор источника для сигнала тактирования передатчика CK_URTx_TX определяется полем URTx_TX_CKS, а источника для сигнала тактирования приемника CK_URTx_RX — полем URTx_RX_CKS регистра URTx_CLK. Для приемника сигнал CK_URTx_RX будет определять частоту передискретизации (oversampling), с которой берутся отсчеты сигнала RX для выявления шума и предотвращения ложных срабатываний. Конечный сигнал тактирования передатчика со скоростью обмена Btx формируется после дополнительного делителя, коэффициент которого Ktxos определяется 5-битным полем URTx_TXOS_NUM регистра URTx_CR1. Конечный сигнал тактирования приемника со скоростью обмена Brx формируется после дополнительного делителя, коэффициент которого Krxos определяется 5-битным полем URTx_RXOS_NUM регистра URTx_CR1.

Итоговая скорость обмена Btx для передатчика будет определяться как

Btx = Furt / ( (Kp+1)·(Kc+1)·(Ktxos+1) ),

а скорость обмена для приемника Brx как

Brx = Furt / ( (Kp+1)·(Kc+1)·(Krxos+1) ),

где Furt — частота сигнала CK_URTx.

Блок BR может использоваться отдельно от модуля UART как счетчик общего назначения, даже, если модуль UART выключен. При этом предделитель и счетчик могут работать в объединенном режиме как один счетчик (бит URTx_BR_MDS регистра URTx_CLK нужно установить в 1). В любом случае, если блок BR применяется, должен быть установлен бит URTx_BR_EN в регистре URTx_CLK.

События и прерывания модуля

Модуль детектирует события, приведенные в таблице, где также указаны соответствующие флаги событий и флаги, приводящие к генерации прерывания INT_URTx.

Название

Флагсобытия

Флагпрерывания

Описание

Общие состояния ошибки

ERRF

Error flag

Parity Error

PEF

ERRF

Ошибка бита четности/нечетности

Frame Error

FEF

ERRF

Ошибка стоп-бита

Receive Data Overrun

ROVRF

ERRF

Переполнение приемного буфера

Receive Noised Character

NCEF

ERRF

Детектирован шум при чтении бита символа

TX Error

TXEF

ERRF

Ошибка формата кадра (стоп-бита) для режимов SmartCard и LIN

Transmit data underrun

TUDRF

ERRF

Нет данных для отправки в режиме SPI

Состояния ошибки при работе таймера TMO

ERRF

Error flag

Receive Timeout

RXTMOF

ERRF

Таймаут приема символа при установленном бите URTx_RXTMO_EN

Idle Timeout

IDTMOF

ERRF

Таймаут простоя линии при установленном бите URTx_IDTMO_EN

Break Timeout

BKTMOF

ERRF

Превышено время состояния "Break" при установленном бите URTx_BKTMO_EN

Calibration Timeout

CALTMOF

ERRF

Таймаут калибровки при установленном бите URTx_CALTMO_EN

Общие события

UGF

UART general event

Slave Address Match

SADRF

UGF

Принят кадр с собственным адресом

Baud-Rate Timer Timeout

BRTF

UGF

Таймаут тактового таймера BR (Baud-Rate Timer)

Timeout Timer Timeout

TMOF

UGF

Таймаут интервального таймера TMO (Timeout Timer)

Calibration Complete

CALCF

UGF

Процесс калибровки завершен

Статус линии

LSF

Line status

Break Condition Detect

BKF

LSF

Обнаружено состояние "Break" (принята длинная последовательность нулевых битов)

Idle Line Detect

IDLF

LSF

Выдержан "Idle" интервал между символами

CTS Change

CTSF

LSF

Состояние сигнала CTS изменилось

NSS Change

NSSF

LSF

Состояние сигнала NSS изменилось на неактивное в режиме SPI slave

Receive Data

RXF

Приняты новые данные, доступные в регистре URTx_RDAT

Transmit Data

TXF

Данные из регистра URTx_TDAT переданы на отправку

Transfer Complete

TCF

Отправлены все данные, включая теневой буфер

На следующем рисунке показана схема формирования прерывания INT_URTx Interrupt Control при возникновении различных событий в модуле UART, а также показаны события, которые приводят только к установке флагов (в регистрах статуса URTx_STA и URTx_STA2).

Схема формирования прерывания модуля UART
Схема формирования прерывания модуля UART

Внутренние события UART General Event (UG), Line Status Event (LS) и состояние ошибки Error Event (ERR), в свою очередь, формируются по следующей схеме:

Схема формирования UART General
Схема формирования UART General

Статусные флаги, приводящие к прерыванию, доступны в регистре URTx_STA. Статусные флаги, не приводящие к прерываниям, доступны в регистре URTx_STA2, за исключением флага RXDF, который доступен в регистре URTx_STA. Кроме булевых флагов, в модуле имеются две числовые трехбитовые переменные: TX_LVL — число байт в буфере, ожидающих отправки, и RX_LVL — число несчитанных байт из буфера приема. Обе переменные относятся к регистру URTx_STA2.

Для включения прерывания модуля UART необходимо:

  1. Выбрать событие (или события) в регистре URTx_INT. Например, для разрешения прерывания по приему данных нужно установить бит URTx_RX_IE.

  2. Разрешить прерывание самого модуля установкой бита URTx_IEA в регистре URTx_INT.

  3. Разрешить прерывание IRQ от модуля URTx в контроллере прерываний NVIC в регистре CPU_ISER. Например, для разрешения прерывания от URT0 (IRQ#20) нужно установить бит 20.

Прием и передача данных

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

Схема буферизации данных расширенного модуля UART
Схема буферизации данных расширенного модуля UART

Для включения блока передачи данных необходимо установить бит URTx_TX_EN в регистре URTx_CR2. Передача данных начинается автоматически после того, как программа запишет передаваемые данные в регистр URTx_TDAT. В расширенной версии модуля этот регистр 32-битный, поэтому можно загружать в него 1, 2 или сразу 4 байта на отправку (должны использоваться машинные инструкции STRB, STRH или STR соответственно). Для передачи трех байт можно воспользоваться регистром URTx_TDAT3 (только в 32-битном режиме, т.е. через инструкцию STR).

Перед непосредственной отправкой в линию TX данные передаются в промежуточный "теневой" буфер TX Shadow Buffer, после чего устанавливается флаг TXF, что может быть использовано для генерации прерывания или опроса готовности передачи. Последующая запись в регистр URTx_TDAT автоматически сбрасывает этот флаг. Оставшееся число байт, не переданных из регистра URTx_TDAT в теневой буфер, можно получить из поля URTx_TNUM регистра URTx_CR4. Число байт, еще не переданных в линию TX из теневого буфера, отображается в поле URTx_TX_LVL регистра URTx_STA2. Когда отправлены все данные из регистра URTx_TDAT и теневого буфера, устанавливается флаг TCF, что может быть использовано в двунаправленном режиме работы модуля.

При реализации библиотечной функции отправки одного байта перед записью в регистр URTx_TDAT можно проверять значение поля URTx_TNUM или URTx_TX_LVL. Значение 0 будет говорить о готовности модуля к отправке очередных данных. Полагаться на активный флаг TXF в этом случае будет некорректно, поскольку до первой отправки (после инициализации модуля) он будет сброшен. Если же его проверять перед выходом из функции, то будет впустую потрачено время на ожидание.

Для включения блока приема данных необходимо установить бит URTx_RX_EN в регистре URTx_CR2. Принимаемые символы с линии RX из сдвигового регистра RX Shuft Buffer передаются в промежуточный приемный теневой буфер RX Shadow Buffer. Далее данные передаются в конечный буфер для считывания — регистр URTx_RDAT, после чего активируется флаг RXF. В случае получения новых данных при заполненном теневом буфере активируется флаг ROVRF (переполнение буфера). Число байт, ожидающих передачу из теневого буфера в буфер данных, отображается в поле URTx_RX_LVL регистра URTx_STA2. Число байт, переданных из теневого буфера в регистр URTx_RDAT и еще невосстребованных, отображается в поле URTx_RNUM регистра URTx_CR4.

Таким образом, модуль активирует флаг RXF как только полученные данные станут доступны программе, после чего их можно прочитать из регистра URTx_RDAT. В расширенной версии модуля этот регистр также 32-битный, поэтому сразу можно прочитать 1, 2 или 4 байта. После чтения данных из регистра флаг RXF автоматически сбрасывается.

Формат кадра задается отдельно для приемника и передатчика в регистре URTx_CR1. Основные параметры настройки с указанием полей регистра приведены в следующей таблице. Символом "*" отмечены значения по-умолчанию (формат 8N1).

Параметр настройки

Приемник

Передатчик

Размер, бит

Значения

Длина символа

URTx_RXDSIZE

URTx_TXDSIZE

2

0(*) - 8 бит,1 - 7 бит,2,3 - резерв

Наличие бита чет/нечет

URTx_RXPAR_EN

URTx_TXPAR_EN

1

0(*) - нет,1 - есть

Полярность бита чет/нечет

URTx_RXPAR_POL

URTx_TXPAR_POL

1

0(*) - чет,1 - нечет

Длина стоп-бита

URTx_RXSTP_LEN

URTx_TXSTP_LEN

2

0 - 0.5 бит,1(*) - 1 бит,2 - 1.5 бита,3 -2 бита

Базовый вариант (модули URT4-URT7)

Базовый вариант UART (URT4-URT7) имеет следующие особенности:

  • только асинхронный режим работы UART,

  • настраиваемая длительность стоп-бита 1 или 2,

  • размер буфера данных 1 байт.

Функциональная схема базового варианта UART приведена на рисунке.

Функциональная схема базового UART
Функциональная схема базового UART

В базовом варианте отсутствуют:

  • аппаратный контроль потока на основе сигналов RTS/CTS,

  • выход управления внешним драйвером для однопроводного двунаправленного варианта интерфейса,

  • интервальный таймер TMO и, соответственно, возможность контроля таймаутов,

  • автоматическая калибровка скорости обмена,

  • прием и передача данных на основе DMA.

Схема управления тактированием базового варианта значительно проще:

Схема управления тактированием базового UART
Схема управления тактированием базового UART

Имеется возможность тактирования приемника и передатчика только от делителя частоты Baud-Rate Generator. Скорость обмена для передатчика и приемника рассчитывается аналогично расширенному варианту UART.

В базовом варианте реализованы следующие флаги, приводящие к прерыванию: UGF, TCF, ERRF, RXF, TXF, BRTF, PEF, FEF, ROVRF, а также только статусные флаги PAR и BUSYF.

Схема буферизации данных базового модуля приведена на следующем рисунке.

Схема буферизации данных базового модуля UART
Схема буферизации данных базового модуля UART

Блоки приема и передачи данных не содержат теневые буферы, и соответственно, не имеют настройки и флаги, с ними связанные. Регистры URTx_TDAT и URTx_RDAT 8-битные и имеют лишь возможность байтового обращения. Формат кадра базового варианта UART задается аналогично расширенному варианту.

Все регистры базового варианты в рамках имеющихся функций идентичны по формату, адресу и названию регистрам расширенного варианта, что дает большие удобства при разработке библиотеки UART.

Аппаратная часть

В данном эксперименте на схеме подключения МК MG32F02A032 добавлен светодиод D2 для вспомогательной индикации, в том числе, для состояния "Hard Fault". Также показано подключение кварцевого резонатора Z1 на частоту 12 МГц (тип HC49S, С=30 пФ). Номиналы конденсаторов C5,C6 под конкретный резонатор выбираются согласно даташиту на МК. На схеме показаны выводы подключения порта UART. Со стороны ПК можно использовать любой UART-USB адаптер, например FTDI, CH340, или даже просто припаять микросхему моста MA112 от Megawin (есть в корпусе SOP16). При подключении нужно учитывать, что напряжение лог. "1" на выводе RXD не должно превышать напряжения питания МК 3,3 В. Вывод МК RXD подключается к выходу TX внешнего порта, а вывод МК TXD — к входу RX внешнего порта.

Схема подключения МК MG32F02A032
Схема подключения МК MG32F02A032

Библиотека работы с модулем UART

Перейдем к рассмотрению кода для работы с UART в обычном асинхронном режиме без контроля потока. Поскольку тестируемым является МК MG32F02A032, в распоряжении имеется только расширенный вариант UART. Приводимый код также должен быть работоспособен на базовом UART в более старших МК.

Инициализация порта UART с номером port_no выполняется функцией uart_init():

void uart_init(uint8_t port_no) {
  register uint32_t da=(uint32_t)port_no*0x10000;

  // Pins:
  switch (port_no) {
    case 0:
      *(volatile uint16_t*)PC_CR0_h0 = (0xA << 12) | 2; // PC0 -> URT0_TX, push pull output
      *(volatile uint16_t*)PC_CR1_h0 = (0xA << 12) | (1 << 5) | 3; // PC1 -> URT0_RX, pull-up resister enable, digital input
      break;
    case 1:
      *(volatile uint16_t*)PB_CR10_h0 = (0x7 << 12) | 2; // PB10 -> URT1_TX, push pull output
      *(volatile uint16_t*)PB_CR11_h0 = (0x7 << 12) | (1 << 5) | 3; // PB11 -> URT1_RX, pull-up resister enable, digital input
      break;
  }

  // Clock source:
  *((volatile uint16_t*)CSC_KEY_h0) = 0xA217; // unlock access to CSC regs
  // (0x4C010022)
  *(volatile uint16_t*)CSC_APB0_h1 = (1 << port_no); // CSC_URTx_EN = 1
   // Leave default APB clock settings: CSC_URTx_CKS (bit 16) = CK_APB (0)
  //*(volatile uint32_t*)CSC_CKS1_w = 0;
  *((volatile uint16_t*)CSC_KEY_h0) = 0x1111; // lock access to CSC regs

  // UART Global Enable (0x52000010)
  *(volatile uint32_t*)(URT0_CR0_w+da) = 1; // URTx_EN = 1

  // Frame format (0x52000014)
  *(volatile uint32_t*)(URT0_CR1_w+da) =
      (3 << 24) | // URT0_TXOS_NUM --- TX data oversampling samples select. The valid value is from 3 to 31 for oversampling samples from 4 to 32.
      (3 << 8) | // URT0_RXOS_NUM --- RX data oversampling samples select. The valid value is from 3 to 31 for oversampling samples from 4 to 32.
      (1 << 22) | (0 << 18) | (0 << 17) | (1 << 6); // tx_stopbit=1, parity=off, 8 bits, rx_stopbit=1

  // Baud (assume CK_URT0 = 12 MHz ) (0x52000024)
  *(volatile uint16_t*)(URT0_RLR_w+da) = 
      (1 << 8) | // URT0_PSR
       12; // URT0_RLP

  // Result: 115200 baud

  // Включаем Baud-Rate Generator, поскольку используем тактирование от него (CK_URTx_INT)
  *(volatile uint32_t*)(URT0_CLK_w+da) = (1 << 24); // URT0_BR_EN = 1

  // Settings (0x52000018)
  *(volatile uint32_t*)(URT0_CR2_w+da) = (1 << 3) | (1 << 2) ; // URT0_TX_EN=1 , URT0_RX_EN=1

}

Поскольку в серии MG32F02 все модули URT имеют одинаковый набор регистров с базовым адресом 0x520n0000, где n — номер URTn, можно сразу написать код для работы со всеми модулями с номерами n=0-2,4-7 (URT3 в рассматриваемых актуальных МК серии не реализован). Поэтому далее в коде используется обращение к регистрам вида (URT0_CR0_w+da), где da=port_no*0x10000.

В начале происходит настройка выводов RXD (цифровой вход с подтяжкой) и TXD (push-pull выход). В данной части конфигурируются только первые два модуля 0 и 1. В реальном проекте эту часть можно вынести за пределы библиотеки, поскольку распределение выводов может быть самым разнообразным. На схеме ранее были показаны выводы RXD0 и TXD0. Выводы модуля 1 предполагаются следующие: RXD1 — PB11, TXD1 — PB10.

Далее происходит включение тактирования соответствующего модуля в регистре CSC_APB0. Здесь удобно использовать выражение типа (1 << port_no), поскольку биты, ответственные за включение тактирования URT0-URT7, расположены последовательно. В качестве источника тактирования всего модуля выбираем сигнал CK_URTx_PR от системного синхросигнала CK_APB шины APB, поэтому в поле URTx_CK_SEL регистра URTx_CLK оставляем значение по-умолчанию 0, в поле CSC_URTx_CKS регистра CSC_CKS1 также оставляем значение 0.

Следующим шагом разрешаем работу модуля в регистре URTx_CR0 установкой бита 0. После этого можно приступить к настройке скорости обмена. Здесь нужно учитывать, что в приведенных ранее формулах имеются ограничения на значения параметров:

  • URTx_PSR ( Kp ) — 0-15 для MG32F02A032, 0-63 для остальных МК;

  • URTx_RLR ( Kc ) — 0-255;

  • URTx_TXOS_NUM ( Ktxos ) и URTx_RXOS_NUM ( Krxos ) — в пределах 3-31.

В User Guide (с. 290) приводится таблица примеров выбора параметров для различных тактовых частот и скоростей обмена. В библиотеке DFP имеется функция MID_URT_SetConfig() для вычисления параметров в runtime, но в нашем простом примере мы укажем подобранные константные значения для частоты тактирования 12 МГц и скорости обмена 115200 бит/с: Kp= 1, Kc= 12, Ktxos= 3, Krxos= 3. Расчетная скорость обмена составляет 115385 бит/с, что дает погрешность всего 0,16 %. Далее включаем блок BR установкой бита URTx_BR_EN в регистре URTx_CLK.

В конце функции включаем узлы передачи и приема в регистре URTx_CR2. Специальную настройку формата кадра не выполняем, поскольку нас будет устраивать вариант по-умолчанию (8N1).

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

void uart_tx(uint8_t port_no, uint8_t d) {
  register uint32_t da=(uint32_t)port_no*0x10000;
  while ( (*(volatile uint8_t*)(URT0_STA2_b3+da) & 0x70)); // ждем, пока URT0_TX_LVL != 0
  *(volatile uint8_t*)(URT0_TDAT_b0+da) = d;   // отправляем байт (0x52000034) <= d
}

Вначале ожидаем, когда теневой буфер передачи будет свободным (поле URTx_TX_LVL), затем передаем один байт через регистр URTx_TDAT. Это блокирующая функция, однако мы ожидаем только возможность отправки, а не факт передачи байта, поэтому она не должна вносить существенные задержки.

Функция приема одного символа может быть такой:

uint8_t uart_rx(uint8_t port_no) {
  register uint32_t da=(uint32_t)port_no*0x10000;
  while ( (*(volatile uint8_t*)(URT0_STA_b0+da) & 0x40) ==0); // waiting URT0_RXF==1
  return *(volatile uint8_t*)(URT0_RDAT_b0+da);
}

Вначале ожидаем, когда активизируется флаг RXF, после чего считываем символ из регистра URTx_RDAT. Это также блокирующая функция, поэтому ее разумно использовать в прерывании по наступлению события приема данных.

Минимальная часть библиотеки в принципе готова. Можно добавить пару традиционных функций для удобства отправки данных:

void uart_send(uint8_t port_no, void* buf, uint32_t len) {
  uint32_t i;
  for (i=0; i<len; i++) uart_tx(port_no, *((uint8_t*)buf+i));
}


void uart_puts(uint8_t port_no, const char* s, uint32_t newline) {
  uint32_t i;
  uint8_t b;
  for (i=0; s[i]!=0; i++) {
    uart_tx(port_no, s[i]);
  }
  for (i=2; i; i--) {
    b=(newline & 0xFF);
    if (b) uart_tx(port_no, b); else break;
    newline >>= 8;
  }
}

Функция uart_send() отправляет в UART произвольную последовательность байт с указанием ее длины. Функция uart_puts() предназначена для отправки Си-строки с завершающим нулевым байтом, причем в конце можно одним целочисленным аргументом добавить любую последовательность из 1-2 байт (символ "новой строки") согласно следующим определениям (см. прилагаемый файл uart.h):

#define UART_NEWLINE_NONE 0x00000000
#define UART_NEWLINE_LF   0x0000000A
#define UART_NEWLINE_CR   0x0000000D
#define UART_NEWLINE_CRLF 0x00000A0D
#define UART_NEWLINE_LFCR 0x00000D0A

Метод отладки в ОЗУ

Традиционный процесс отладки микропрограммы предполагает каждый раз загрузку во flash-память МК всей "прошивки", включающей таблицу векторов, все необходимые сторонние библиотеки, собственный наработанный код, даже, если в программе изменилась только одна строка или один байт. Простота этого метода очевидна — программисту не надо особо задумываться над компоновкой, достаточно каждый раз при изменении кода нажимать только одну кнопку в IDE (Compile/Build/Flash) и в конечном итоге на кристалле окажется отлаженная "прошивка", которую можно включать в конечное изделие.

Однако, при каждой "прошивке" выполняется стирание всех занимаемых ею страниц, что повышает износ flash-памяти. Износ можно существенно снизить, если в ПО программатора будет функция постраничного сравнения существующей информации с новой и выдача команд на стирание и запись только в случае несовпадения. В любом случае каждый раз хотя бы одна страница, содержащая отлаживаемый код, должна быть стерта, даже если компоновщик не изменит положение большинства остальных функций и констант. При увеличении размера "прошивки" возрастает и время загрузки, хотя на современных ARM-ядрах это не так критично из-за высокой скорости работы интерфейсов JTAG/SWD.

Поскольку ЦПУ Cortex-M0 позволяет выполнять код не только из flash-памяти, но и из ОЗУ, напрашивается метод частичной отладки кода в области ОЗУ. Суть метода в следующем: во flash-память помещается базовая часть "прошивки", включающая таблицу векторов, обработчики прерываний, а также, при необходимости, некоторую уже отлаженную часть кода. После сброса МК базовая часть определяет, имеется ли в ОЗУ отлаживаемая часть программы, и в случае её наличия передает ей управление. Образ программы в ОЗУ загружается стандартными командами SWD-программатора.

Таким образом, формируются два совершенно отдельных загружаемых в МК образа:

  • базовая часть, которую далее будем называть supervisor (кратко svr),

  • отлаживаемая часть, которую далее будем называть application (кратко app).

Базовая часть может находиться в областях AP или ISP, может являться единственной во flash-памяти или активироваться после работы бутлоадера (кода области ISP). Основная инициализации МК может выполняться как в части svr, так и в app.

Метод отладки в ОЗУ оправдывает себя в следующих случаях:

  • необходимо максимально сократить износ flash-памяти МК на этапе разработки (например, при ограниченном количестве доступных экземпляров МК),

  • необходимо существенно сократить время загрузки прошивки,

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

  • отлаживаемая часть "с запасом" помещается в ОЗУ.

Поскольку каждая из частей svr и app собирается независимо (возможно применение даже разных компиляторов) и при линковке одной части будут неизвестны адреса объектов другой части, предстоит решить следующие задачи:

  • обеспечить вызов функций svr из отлаживаемой части,

  • обеспечить вызов функций app из базовой части кода (например, вызов обработчика прерывания, который сам находится в стадии разработки),

  • обеспечить доступ к переменным svr части со стороны app,

  • обеспечить доступ к переменным app части со стороны svr,

  • распределить адресное пространство ОЗУ между обеими частями, а также стеком и областью DMA.

Реализация

Базовая часть (Supervisor)

Одним из способов решения первых четырех задачи является применение механизма системных вызовов, подробно описанного в упомянутой ранее книге Джозефа Ю "The Definitive Guide to ARM ® Cortex ® -M0 and Cortex-M0+ Processors. Second Edition". Базовая часть берет на себя функцию обслуживания системного вызова, реализуя обработчик исключения SVCall. Отлаживаемая часть использует функционал базовой части путем обращения к ней через машинную инструкцию SVC, в которой кодируется номер вызова от 0 до 255. В системный вызов можно также передать аргументы через регистры или стек.

Рассмотрим реализацию предлагаемого метода на основе проекта megawin, описанного в предыдущей статье цикла. Базовая часть svr включает следующие файлы:

  • startup.c — код для генерации таблицы векторов прерываний и части функций их обработки;

  • init.c — код инициализации МК;

  • svr.c — код обработчиков исключений (прерываний);

  • main.c — главная часть, включающая только функцию main();

  • ulib.h и ulib.c — вспомогательные функции проекта, заменяющие функционал стандартной библиотеки;

  • api.h — файл с определениями программного интерфейса (API), предоставляемого для отлаживаемой части.

Файлы startup.c и init.c были рассмотрены ранее и остаются без изменения. Приведем основной файл базовой части (супервизора) svr.c:

#include "svr.h"
#include "api.h"
#include "ulib.h"
#include "MG32x02z__RegAddress.h"


/// IRQ Handler type
typedef void(*handler_t)();

/// IRQ Handlers
volatile handler_t hdlr[32];


__attribute__ ((naked))
void HardFault_Handler() {
  // Включаем мигание светодиодом на PB3
  *(volatile uint16_t*)PB_CR3_h0 = 0x0002; // PB3 -> push-pull output
  while (1) {
    *(volatile uint16_t*)PB_SC_h0 = 0x0008; // set bit 3
    delay_ms(100);
    *(volatile uint16_t*)PB_SC_h1 = 0x0008; // clear bit 3
    delay_ms(100);
  }
}


__attribute__ ((naked))
void SVC_Handler() {
// From Yiu J.:
// Stack frame contains:
// r0, r1, r2, r3, r12, r14, the return address and xPSR
// - Stacked R0 = svc_args[0]
// - Stacked R1 = svc_args[1]
// - Stacked R2 = svc_args[2]
// - Stacked R3 = svc_args[3]
// - Stacked R12 = svc_args[4]
// - Stacked LR = svc_args[5]
// - Stacked PC = svc_args[6]
// - Stacked xPSR= svc_args[7]
  // Используем только MSP, проверку бита 2 LR опускаем
  asm(
    "mrs    r0,msp\n"
    "b      SVC_Handler_main\n"
  );
}


/// Установка обработчика прерываний
__attribute__ ((interrupt))
void SVC_Handler_main(uint32_t* sp) {
  switch ( ((uint8_t*)sp[6])[-2] ) {
    case SVC_HANDLER_UNSET:   hdlr[sp[0]]=0; break;
    case SVC_HANDLER_SET:     hdlr[sp[0]]=sp[1]; break;
  }
}


__attribute__ ((interrupt))
void URT0_IRQHandler() {
  if (hdlr[20]) hdlr[20]();
}


__attribute__ ((interrupt))
void URT123_IRQHandler() {
  if (hdlr[21]) hdlr[21]();
}

В файле определяется тип обработчика любого прерывания handler_t и создается массив hdlr из максимального числа ISR (32). Тип и массив пока больше нигде не используются и поэтому объявляются внутри svr.c.

При выполнении инструкции SVC im8 ( im8 — 8-битный номер вызова) ядро Cortex-M0 формирует специальный фрейм стека, содержащий сохраненные значения регистров R0, R1, R2, R3, R12, LR (R14), PC и xPSR (флаговый регистр состояния). После этого в регистр LR записывается специальное кодовое значение EXC_RETURN, содержащее флаги для работы механизма выхода из исключения, и управление передается в SVC_Handler(). Одним из флагов является бит 2, который указывает, из какого стека нужно восстанавливать регистры:

  • 0 — основной стек по указателю MSP (Main Stack Pointer),

  • 1 — стек процесса по указателю PSP (Process Stack Pointer).

Возвращение в прерванную программу происходит автоматически при загрузке в регистр PC значения LR: ЦПУ, получив значение EXC_RETURN, "понимает", что это не обычный адрес возврата (используется значение вида 0xFFFFFFF*, которое не может указывать на валидный адрес исполняемой области памяти), а специальный код выхода из исключения, восстанавливает значения регистров из фрейма стека. Поскольку при этом восстанавливается и значение регистра PC, работа прерванной программы возобновляется. Такой "трюк" позволяет оформлять обработчики прерываний как обычные функции, в том числе на Си.

Поскольку стек задач в нашем случае не используется, функция SVC_Handler() (объявлена как naked) просто копирует значение указателя основного стека MSP в доступный для Си-кода регистр R0 (без анализа бита 2 в EXC_RETURN) и передает управление фактическому обработчику исключения SVCall — функции SVC_Handler_main(uint32_t* sp). Эта функция через регистр R0 принимает единственный аргумент — указатель стека, относительно которого и надо обращаться к фрейму стека. Номер системного вызова в наборе инструкций Thumb кодируется младшим байтом кода инструкции SVC и легко доступен как sp[6])[-2]. В примере предусмотрены два системных вызова, определенные в файле api.h:

#include <stdint.h>

#define APP_ORIGIN      0x20000000
#define APP_SIGNATURE   0x46c046c0

#define SVC1(no,arg0)                 asm("mov r0,%0\n" "svc %1\n" :: "r"(arg0), "I"(no) : "r0")
#define SVC2(no,arg0,arg1)            asm("mov r0,%0\n" "mov r1,%1\n" "svc %2\n" :: "r"(arg0), "r"(arg1), "I"(no) : "r0","r1")
#define SVC3(no,arg0,arg1,arg2)       asm("mov r0,%0\n" "mov r1,%1\n" "mov r2,%2\n" "svc %3\n" :: "r"(arg0), "r"(arg1), "r"(arg2), "I"(no) : "r0","r1","r2")
#define SVC4(no,arg0,arg1,arg2,arg3)  asm("mov r0,%0\n" "mov r1,%1\n" "mov r2,%2\n" "mov r3,%3\n" "svc %4\n" :: "r"(arg0), "r"(arg1), "r"(arg2), "r"(arg3), "I"(no) : "r0","r1","r2","r3")


/// syscalls
enum IOSystemCalls {
  SVC_HANDLER_UNSET =                  0,
  SVC_HANDLER_SET   =                  1
};

Вызов SVC_HANDLER_UNSET (0) обнуляет адрес функции обработчика, чтобы заблокировать обращение к ней. Номер IRQ задается первым аргументом через регистр R0. Вызов SVC_HANDLER_SET (1) устанавливает обработчик, адрес которого задается вторым аргументом через регистр R1. Для удобства в файле также определены макросы SVC1() - SVC4(), позволяющие в основной программе легко обращаться к системному вызову с указанием его номера и аргументов в количестве 1-4 соответственно.

Далее в файле svr.c определяются обработчики прерываний URT0_IRQHandler() и URT123_IRQHandler(). Если до этого был задан ненулевой указатель фактического обработчика (из части app), то выполняется его вызов. Замечание: поскольку в нашем проекте не используется стандартная библиотека, переменные секции .bss не инициализируются (нулем), так что устанавливать обработчик надо до разрешения прерываний соответствующего модуля (по крайней мере, до фактического срабатывания прерывания).

Дополнительно в файле svr.c реализован обработчик аппаратного исключения HardFault, который включает мигание светодиода D2 с частотой примерно 5 Гц, что может быть полезно при отладке кода.

Файл main.c содержит только функцию main():

#include "MG32x02z__RegAddress.h"
#include "api.h"
#include "ulib.h"


__attribute__ ((noreturn))  __attribute__ ((naked)) // omit prologue/epilogue sequences (garbage push/pop instructions)
void main (void) {

  if (*((volatile uint32_t*)(APP_ORIGIN)) == APP_SIGNATURE) {
    asm("BX %0" : : "r"((APP_ORIGIN+4) | 1)); // Set bit 0 for Thumb !!!
  }

  *(volatile uint16_t*)PB_CR3_h0 = 0x0002; // PB3 -> push-pull output
  while (1) {
    *(volatile uint16_t*)PB_SC_h0 = 0x0008; // set bit 3
    delay_ms(100);
    *(volatile uint16_t*)PB_SC_h1 = 0x0008; // clear bit 3
    delay_ms(900);
  }

}

В функции проверяется наличие отлаживаемой части в ОЗУ. Если она присутствует, ей передается управление через инструкцию BX, иначе включается мигание светодиода D2 с периодом 1 с и скважностью 10.

Отлаживаемая часть (Application)

Часть app имеет очень простой ld-скрипт mg32f02a032_app.ld:

SECTIONS {
	. = 0x20000000;
	.text : {
	  KEEP(*(.app_sign))
	  KEEP(*(.app))
	  *(.text)
  }
  . = 0x20000800;
  .data : {
    *(.data)
  }
  .bss : {
    *(.bss)
  }
}

Машинный код (секция .text) помещается в начало области ОЗУ с адреса 0x20000000 и имеет ограничение в размере 2048 байт. В самое начало области помещается константа с именем app_sign, определенная в основном файле отлаживаемой части app.c как переменная типа uint32_t. Константа представляет собой 4-байтовую сигнатуру, по которой базовая часть определяет наличие в ОЗУ части app. Значение сигнатуры 0x46c046c0 соответствует двум машинным инструкциям NOP, поэтому базовая часть может передавать управление на начало секции .text или отступив 4 байта, после которых начинается главная функция app().

Секции данных .data и .bss начинаются с адреса 0x20000800 и ограничиваются сверху началом области RAM базовой части svr с адреса 0x20000A00, т.е. максимальный размер для данных и переменных части app в рассматриваемом проекте составляет 512 байт (см. ld-скрипт в предыдущей статье цикла).

Отлаживаемая часть app включает следующие файлы:

  • app.c — основной файл со стартовой функцией app();

  • api.h — файл с определениями программного интерфейса, предоставляемого базовой частью через системные вызовы;

  • ulib.h и ulib.c — вспомогательные функции проекта, заменяющие функционал стандартной библиотеки;

  • uart.h и uart.c — библиотека для работы с модулем UART.

Рассмотрим основной файл app.c:

#include "MG32x02z__RegAddress.h"
#include "ulib.h"
#include "api.h"
#include "uart.h"

#define PORT 0


// Mark first word with signature "Application is present" (nop; nop: 0x46c046c0)
__attribute__ ((section (".app_sign"))) __attribute__ ((__used__))
static uint32_t  app_sign=APP_SIGNATURE;


// First function in application
__attribute__ ((section(".app"))) // put function in the begin of .text after signature word "app_sign"
__attribute__ ((noreturn))
void app() {
  char s[4]="< >";

  uart_init(PORT);
  uart_puts(PORT,"Hello",UART_NEWLINE_CRLF);
  while (1) {
    s[1]=uart_rx(PORT);
    uart_puts(PORT,s,UART_NEWLINE_CRLF);
  }

}

В самом начале объявляется статическая переменная app_sign, содержащая сигнатуру APP_SIGNATURE, определенную в файле api.h. Компоновщик помещает её в самое начало области .text. Далее в бинарный образ помещается главная функция app(). В данном случае мы не можем её объявить с атрибутом naked, поскольку при этом компилятор фактически перестает работать со стеком и выделять в нём память под локальные переменные. Поэтому пока приходится мириться с потерей по крайней мере 4 байт во flash, в которые записываются бесполезные инструкции PUSH и POP "пролога" и "эпилога", а также нескольких 32-битных слов в ОЗУ (в стеке).

Задача отлаживаемой части: в цикле ожидать данные из модуля UART, при получении одного символа обрамлять его угловыми скобками < и >, после чего сформированную строку отправлять обратно в UART, добавляя символы новой строки CR (0x0D) и LF (0x0A).

В начале функции app() выполняется инициализация порта uart_init(), затем в UART оправляется контрольная строка приветствия "Hello", после чего в цикле считывается один символ через функцию uart_rx() и выдается ответ через uart_puts().

Сборка и тестирование

Для сборки последующих примеров необходимо обновить файл premake5.lua. Основные изменения коснулись описания целей (ключевое слово project), которых теперь стало две:

project "svr"
  kind "ConsoleApp"
  language "C"
  files {"src/init.c", "src/startup.c", "src/main.c", "src/svr.c", "src/ulib.c"}
  linkoptions {"-nostdlib"}
  linkoptions { "-Wl,--gc-sections"}
  linkoptions {"-T mg32f02a032_svr.ld"}

project "app"
  kind "ConsoleApp"
  language "C"
  files {"src/app.c", "src/uart.c", "src/ulib.c"}
  linkoptions {"-nostdlib"}
  linkoptions { "-Wl,--gc-sections"}
  linkoptions {"-T mg32f02a032_app.ld"}

Обе части собираются без использования стандартной библиотеки, для чего добавлена опция линкера -nostdlib. Однако при оптимизации -Os компилятор может генерировать вызов вспомогательных функций, которые мы не хотим линковать. Поэтому в секции workspace понижен уровень оптимизации до -O3 и добавлена опция -ffreestanding:

buildoptions {"-mthumb", "-mcpu="..MCPU, "-Wall", "-O3", "-g", "-fno-common", "-ffunction-sections", "-fdata-sections", "-ffreestanding"}

Генерация make-файлов выполняется командой

premake5 gmake

Сборка частей svr и app выполняется соответственно командами

make svr
make app

Команда make clean удаляет производные файлы для обеих целей.

Итоговый размер секций .text получился следующий: часть srv — 472 байта , часть app — 386 байт. "Прошивка" базовой части в область AP flash выполняется в telnet-соединении OpenOCD с помощью команды (должна быть подключена библиотека mg32f02_mem_ap.tcl)

mem_ap_flash $DIR/bin/svr.bin 0x18000000

где $DIRабсолютный путь к каталогу проекта (чтобы не зависеть от каталога запуска самого OpenOCD).

Загрузка отлаживаемой части в ОЗУ выполняется с помощью штатной команды OpenOCD load_image с последующим сбросом МК:

halt
load_image $DIR/bin/app.bin 0x20000000 bin
reset

Здесь последним аргументом в команде указывается формат загружаемого файла (bin).

Для удобства можно использовать shell-скрипты run_svr:

#!/bin/bash
TELNET="telnet 127.0.0.1 4444"
DIR=`pwd`
(echo "mem_ap_flash $DIR/bin/svr.bin 0x18000000"; sleep 3;) | $TELNET

и run_app:

#!/bin/bash
TELNET="telnet 127.0.0.1 4444"
DIR=`pwd`
(echo "halt"; sleep 0.2; echo "load_image $DIR/bin/app.bin 0x20000000 bin"; sleep 0.2; echo "reset"; sleep 0.2;) | $TELNET

Для проверки работы МК можно использовать любую терминальную программу на ПК, например minicom в ОС Linux. Необходимо установить скорость обмена 115200 бит/с, длину символа 8 бит, формат без контроля четности с одним стоп-битом.
В результате в терминале будет примерно следующее:

Обмен данными МК с терминалом
Обмен данными МК с терминалом

Аналогично можно проверить работоспособность кода с модулем URT1. Для этого достаточно исправить определение номера порта:

#define PORT 1

и пересобрать часть app.

Применение прерывания модуля UART

Теперь усложним задачу, добавив обработчик прерываний по получению данных из модуля URT0. В файле app.c добавим функцию обработки прерывания uart_hdl() и изменим функцию app():

void uart_hdl() {
  char s[4]="< >";
  s[1]=uart_rx(PORT);
  uart_puts(PORT,s,UART_NEWLINE_CRLF);
}


__attribute__ ((section(".app"))) // put function in the begin of .text after signature word "app_sign"
__attribute__ ((noreturn))
void app() {

  *(volatile uint16_t*)PB_CR2_h0 = 0x0002; // PB2 -> push-pull output

  uart_init(PORT);
  SVC2(SVC_HANDLER_SET,20,uart_hdl);

  // включаем прерывания в модуле URT0
  *(volatile uint8_t*)URT0_INT_b0 = 0x40 | 0x01; // URT0_RX_IE | URT0_IEA
  // включаем прерывание в модуле NVIC
  *(volatile uint32_t*)CPU_ISER_w = (1 << 20); // SETENA 20

  uart_puts(PORT,"Hello",UART_NEWLINE_CRLF);

  while (1) {
    *(volatile uint16_t*)PB_SC_h0 = 0x0004; // set bit 2
    delay_ms(250);
    *(volatile uint16_t*)PB_SC_h1 = 0x0004; // clear bit 2
    delay_ms(250);
  }

}

Здесь те действия, которые в прошлом примере были в цикле, перенесены в обработчик uart_hdl(). В основном же цикле происходит только управление светодиодом D1. После инициализации модуля выполняется системный вызов SVC_HANDLER_SET, который устанавливает в качестве конечного обработчика прерывания модуля URT0 прикладную функцию uart_hdl(), код которой находится в ОЗУ.

Можно собрать и запустить часть app. В терминале убеждаемся, что UART МК работает так же, как и в прошлом примере. При этом параллельно мигает светодиод D1, показывая, что основная задача выполняется и ISR не блокирует ядро МК на длительное время.

Приведем фрагмент функции app() для модуля URT1 (макрос PORT нужно также установить в 1):

  uart_init(PORT);
  SVC2(SVC_HANDLER_SET,21,uart_hdl);

  // включаем прерывания в модуле URT1
  *(volatile uint8_t*)URT1_INT_b0 = 0x40 | 0x01; // URT1_RX_IE | URT1_IEA
  // включаем прерывание в модуле NVIC
  *(volatile uint32_t*)CPU_ISER_w = (1 << 21); // SETENA 21

Управление тактированием МК

В заключении рассмотрим некоторые практические примеры конфигурации схемы тактирования МК, которая была рассмотрена в первой статье цикла.

МК серии MG32F02 имеют возможность вывода одного из основных тактовых сигналов на вывод ICKO (всегда PC0). Конкретный сигнал выбирается мультиплексором по значению поля CSC_CKO_SEL регистра CSC_CKO. Частота конечного выводимого сигнала может быть ниже в 1, 2, 4 или 8 раз, в зависимости от настройки делителя в поле CSC_CKO_DIV регистра CSC_CKO. Следующая функция настраивает вывод сигнала CK_MAIN без деления частоты:

// CK_ICKO output through PC0 pin
void setup_icko() {
  *(volatile uint16_t*)PC_CR0_h0 = (0x1 << 12) | 2; // PC0 -> ICKO, push pull output
  *(volatile uint16_t*)CSC_KEY_h0 = 0xA217; // unlock access to CSC regs
  *(volatile uint32_t*)CSC_CKO_w = (0x0 << 4) | 1; // CK_MAIN, DIV=1, CSC_CKO_EN = 1
  *(volatile uint16_t*)CSC_KEY_h0 = 0x1111; // lock access to CSC regs
}

Функцию можно вызывать из отлаживаемой части и использовать для контроля частоты МК на этапе отладки.

До сих пор мы использовали настройки тактирования по-умолчанию, т.е. от внутреннего RC-генератора IHRCO с частотой 12,0000 МГц. Если при работе с модулем UART требуется скорость передачи настроить как можно ближе к стандартным значениям, нужно выбирать тактирование от IHRCO генератора с частотой 11,0592 МГц, которая будет кратна стандартным значениям. Например, отношение 11059200/115200 будет равно целому числу 96, которое нетрудно получить настройками делителей ( Kp= 1, Kc= 11, Ktxos= 3). Следующая функция переключает частоту генератора IHRCO на 11,0592 МГц:

void setup_ihrco() {
  *(volatile uint16_t*)CSC_KEY_h0 = 0xA217; // unlock access to CSC regs
  *(volatile uint32_t*)CSC_CR0_w |= (1 << 18); // CSC_IHRCO_SEL = 1 (11.0592 MHz)
  *(volatile uint16_t*)CSC_KEY_h0 = 0x1111; // lock access to CSC regs
}

Встроенным RC-генераторам, как известно, свойственны невысокая точность и низкая стабильность частоты. Для большей точности и температурной стабильности рекомендуется применять кварцевый генератор. Следующая функция включает встроенный кварцевый генератор XOSC на внешнем резонаторе, подключаемом к выводам XIN и XOUT согласно приведенной ранее схеме:

void setup_xosc() {
  uint32_t d;
  // Setup pins XIN (PC13) & XOUT (PC14):
  *(volatile uint16_t*)PC_CR13_h0 = (1 << 12); // PC13 -> XIN
  *(volatile uint16_t*)PC_CR14_h0 = (1 << 12); // PC14 -> XOUT
  while (! (*(volatile uint8_t*)CSC_STA_b3 & 2)); // waiting CSC_XOSCF == 1 (XOSC ready)
  *(volatile uint16_t*)CSC_KEY_h0 = 0xA217; // unlock access to CSC regs
  d=*(volatile uint32_t*)CSC_CR0_w;
  d &= ~(3 << 10); // clear bits 10,11
  d |= (1 << 10); // set CSC_HS_SEL = 0b01 (XOSC)
  *(volatile uint32_t*)CSC_CR0_w = d;
  *(volatile uint16_t*)CSC_KEY_h0 = 0x1111; // lock access to CSC regs
}

Вначале задаются функции XIN и XOUT для выводов подключения резонатора PC13 и PC14. После этого внутренний генератор XOSC автоматически запускается (отдельного бита разрешения генератора не предусмотрено). Затем ожидается готовность генератора (активизация флага CSC_XOSCF). После этого в регистре CSC_CR0 устанавливается XOSC в качестве источника системного сигнала тактирования CK_HS (поле CSC_HS_SEL устанавливается в 1).

Генератор XOSC может работать на частотах до 24 МГц. Если нужна тактовая частота выше, чем частота XOSC, необходимо использовать блок умножения частоты на основе PLL. Следующая функция включает PLL c умножением частоты в 2 раза:

// Включение умножения частоты на основе PLL (x2)
void setup_pll() {
  uint16_t d;
  *(volatile uint16_t*)CSC_KEY_h0 = 0xA217; // unlock access to CSC regs

  // CSC_PLLI_DIV = 2, CK_PLLI = 6 MHz (CK_HS/2)
  // CSC_PLLO_DIV = 0 , CK_PLLO = 24 MHz (CK_PLL/4)
  *(volatile uint8_t*)CSC_DIV_b0 = 0b00000001;

  // CSC_PLL.CSC_PLL_MUL = 0, PLL_MULL = 16 (DEFAULT), CK_PLL = 96 MHz (CK_PLII*16)

  *(volatile uint8_t*)CSC_CR0_b0 |= (1 << 5); // CSC_PLL_EN = 1

  while (! (*(volatile uint8_t*)CSC_STA_b0 & (1 << 6))); // waiting CSC_PLLF == 1 (PLL ready)

  d=*(volatile uint16_t*)CSC_CR0_h0;
  d &= ~(3 << 14); // clear bits 14,15
  d |= (2 << 14); // CSC_MAIN_SEL = 2 (CK_PLLO)
  *(volatile uint16_t*)CSC_CR0_h0 = d;

  *(volatile uint16_t*)CSC_KEY_h0 = 0x1111; // lock access to CSC regs
}

Исходным сигналом является CK_HS с частотой 12 МГц. Входной делитель блока PLL делит частоту на 2 (поле CSC_PLLI_DIV), чтобы попасть в допустимый для PLL МК MG32F02A032 диапазон 5-7 МГц. Полученная частота 6 МГц умножается на 16 в блоке PLL. Сигнал CK_PLL с частотой 96 МГц проходит выходной делитель на 4 (поле CSC_PLLO_DIV) и итоговый сигнал CK_PLLO с частотой 24 МГц выбирается как основной (CK_MAIN) в регистре CSC_CR0.

Все рассмотренные функции по управлению тактированием (находятся в файле app.c) можно запускать как из базовой части svr, так и из ОЗУ из части app, что позволяет экспериментировать с МК без стирания flash-памяти.

Листинг дизассемблера частей svr и app можно просматривать с помощью прилагаемых сценариев lst_svr и lst_app. Все файлы, рассматриваемые в статье, собраны в прилагаемом архиве.

На этом мы завершаем третью статью цикла. В следующий раз рассмотрим аналоговую часть МК серии MG32F02: АЦП и компаратор, а также встроенный температурный датчик.

Полезные ссылки

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