Введение
Мы продолжаем цикл статей по микроконтроллерам компании 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 приведена на рисунке.
Модуль использует следующие внешние сигналы:
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 выбирается установкой поля 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 |
Таймаут приема символа при установленном бите |
Idle Timeout |
IDTMOF |
ERRF |
Таймаут простоя линии при установленном бите |
Break Timeout |
BKTMOF |
ERRF |
Превышено время состояния "Break" при установленном бите |
Calibration Timeout |
CALTMOF |
ERRF |
Таймаут калибровки при установленном бите |
Общие события |
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 |
Приняты новые данные, доступные в регистре |
|
Transmit Data |
TXF |
Данные из регистра |
|
Transfer Complete |
TCF |
Отправлены все данные, включая теневой буфер |
На следующем рисунке показана схема формирования прерывания INT_URTx Interrupt Control при возникновении различных событий в модуле UART, а также показаны события, которые приводят только к установке флагов (в регистрах статуса URTx_STA
и URTx_STA2
).
Внутренние события UART General Event (UG), Line Status Event (LS) и состояние ошибки Error Event (ERR), в свою очередь, формируются по следующей схеме:
Статусные флаги, приводящие к прерыванию, доступны в регистре URTx_STA
. Статусные флаги, не приводящие к прерываниям, доступны в регистре URTx_STA2
, за исключением флага RXDF
, который доступен в регистре URTx_STA
. Кроме булевых флагов, в модуле имеются две числовые трехбитовые переменные: TX_LVL
— число байт в буфере, ожидающих отправки, и RX_LVL
— число несчитанных байт из буфера приема. Обе переменные относятся к регистру URTx_STA2
.
Для включения прерывания модуля UART необходимо:
Выбрать событие (или события) в регистре
URTx_INT
. Например, для разрешения прерывания по приему данных нужно установить битURTx_RX_IE
.Разрешить прерывание самого модуля установкой бита
URTx_IEA
в регистреURTx_INT
.Разрешить прерывание IRQ от модуля URTx в контроллере прерываний NVIC в регистре
CPU_ISER
. Например, для разрешения прерывания от URT0 (IRQ#20) нужно установить бит 20.
Прием и передача данных
Рассмотрим основной механизм приема и передачи данных в режиме асинхронного 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).
Параметр настройки |
Приемник |
Передатчик |
Размер, бит |
Значения |
---|---|---|---|---|
Длина символа |
|
|
2 |
0(*) - 8 бит,1 - 7 бит,2,3 - резерв |
Наличие бита чет/нечет |
|
|
1 |
0(*) - нет,1 - есть |
Полярность бита чет/нечет |
|
|
1 |
0(*) - чет,1 - нечет |
Длина стоп-бита |
|
|
2 |
0 - 0.5 бит,1(*) - 1 бит,2 - 1.5 бита,3 -2 бита |
Базовый вариант (модули URT4-URT7)
Базовый вариант UART (URT4-URT7) имеет следующие особенности:
только асинхронный режим работы UART,
настраиваемая длительность стоп-бита 1 или 2,
размер буфера данных 1 байт.
Функциональная схема базового варианта UART приведена на рисунке.
В базовом варианте отсутствуют:
аппаратный контроль потока на основе сигналов RTS/CTS,
выход управления внешним драйвером для однопроводного двунаправленного варианта интерфейса,
интервальный таймер TMO и, соответственно, возможность контроля таймаутов,
автоматическая калибровка скорости обмена,
прием и передача данных на основе DMA.
Схема управления тактированием базового варианта значительно проще:
Имеется возможность тактирования приемника и передатчика только от делителя частоты Baud-Rate Generator. Скорость обмена для передатчика и приемника рассчитывается аналогично расширенному варианту UART.
В базовом варианте реализованы следующие флаги, приводящие к прерыванию: UGF, TCF, ERRF, RXF, TXF, BRTF, PEF, FEF, ROVRF, а также только статусные флаги PAR и BUSYF.
Схема буферизации данных базового модуля приведена на следующем рисунке.
Блоки приема и передачи данных не содержат теневые буферы, и соответственно, не имеют настройки и флаги, с ними связанные. Регистры 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 внешнего порта.
Библиотека работы с модулем 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: АЦП и компаратор, а также встроенный температурный датчик.