======Пример Echo и смена скорости UART====== Для изучения настройки UART в микроконтроллерах "Миландр" давайте реализуем режим эхо. Микроконтроллер в ответ на принятое слово по UART будет выдавать это же слово обратно. При этом, при посылке заданной последовательности символов реализуем возможность смены скорость обмена. Это аналогично смене скорости обмена в Bootloader-e (см. [[prog:uart:loader_uart|Тестируем Bootloader в режиме UART]]), только у нас скорость будет меняться правильно, без сбоев. Проект реализуем на микроконтроллере 1986ВЕ1Т. Благодаря SPL этот проект может быть легко пересобран для других микроконтроллеров "Миландр". Проект целиком доступен на [[https://github.com/StartMilandr/2.1-UART_Echo_Rate|GitHub]], здесь же разберем только основные настройки. ===== Создание проекта===== Проект создается аналогично статье - [[prog:start:new_project|Создаем новый проект]]. Для работы с UART нам потребуется подключить блоки - Startup, EEPROM, PORT, RST_CLK, UART. Код работы с UART мы вынесем в отдельный файл - //Uart.c//. Код задания тактирования вынесем в файл //Clock.c//. Это позволит нам в следующих проектах использовать функции реализованные в данных файлах, что позволяет избегать дублирования кода и ускоряет разработку. Для подключения реализованных в *.с файлах функций создадим заголовочные файлы - //Uart.h// и //Clock.h//. Основной функционал примера как всегда будет реализован в //main.c//. В итоге дерево проекта будет выглядеть так {{prog:uart:uart_rates_prj.png}} =====Основной код - main.c===== Думаю, код достаточно прост и в особых комментариях не нуждается. Сначала настраивается частота ядра на 128 МГц, реализация вынесена в //Clock.c//, поэтому подключаем //Clock.h//. Затем настраивается блок UART и включаются прерывания от него. Реализация так же вынесена в //Uart.c//, подключаем //Uart.h//. #include #include "Clock.h" #include "Uart.h" // Перечень возможных задач typedef enum {tskNoTask, tskChangeRate} UART_Task; // Текущая задача UART_Task ActiveTask = tskNoTask; // Частоты для теста смена скорости const uint32_t UART_Rates[] = {9600, 56000, 115200}; // Тактовая частота ядра #define PLL_MUL 16 // = RST_CLK_CPU_PLLmul16 + 1 #define CPU_FREQ HSE_Value * PLL_MUL // 8MHz * 16 = 128MHz int main(void) { // Тактирование ядра Clock_Init_HSE_PLL(PLL_MUL - 1); // Инициализация UART UART_Initialize(UART_Rates[2]); UART_InitIRQ(1); while (1); } //... продолжение ниже Далее микроконтроллер крутится в бесконечном цикле, пока не возникнет прерывание от UART. На демо-плате есть возможность задать джамперами подключение к внешнему разъему RS-232 интерфейса UART1 или UART2 . {{prog:uart:1986ve1t_uart12.png}} В коде, выбор используемого UART_X происходит в файле //Uart.h//. Для универсальности оба обработчика прерываний (//UART1_IRQHandler()// и //UART2_IRQHandler()//) вызывают одну и ту же функцию обработки //UART_Handler_RX_TX()//. При таком решении не нужно править код //main.c// при смене интерфейса в файле //Uart.h//. //... продолжение void UART_Handler_RX_TX(void) { uint16_t receivedData; // Обработка прерывания по Приему данных if (UART_GetITStatusMasked (UART_X, UART_IT_RX) == SET) { // Сброс прерывания UART_ClearITPendingBit (UART_X, UART_IT_RX); // Получаем данные и отвечаем - ЭХО receivedData = UART_ReceiveData (UART_X); UART_SendData (UART_X, receivedData); // Если активная задача - смена скорости if (ActiveTask == tskChangeRate) { ActiveTask = tskNoTask; // Если индекс скорости в заданных пределах, то меняем скорость if (receivedData < 3) UartSetBaud(UART_Rates[receivedData], CPU_FREQ); } // При получении символа 'R', следующим байтом ожидаем индекс новой скорости if (receivedData == 'R') ActiveTask = tskChangeRate; } // Обработка прерывания от Передачи данных if (UART_GetITStatusMasked(UART_X, UART_IT_TX) == SET) { // Сброс прерывания UART_ClearITPendingBit (UART_X, UART_IT_TX); } } void UART1_IRQHandler (void) { UART_Handler_RX_TX(); } void UART2_IRQHandler (void) { UART_Handler_RX_TX(); } // Конец Обработчик прерывания один на прием и передачу слова, поэтому в //UART_Handler_RX_TX()// необходимо проверять, что явилось источником текущего прерывания. В нашем примере вся работа происходит в прерывании по приему. Каждое принятое слово посылается обратно, это так называемый режим "Эхо". Далее, это же принятое слово обрабатывается и проверяется, является ли оно командой. В нашем примере под командой воспринимается символ //'R'//, за которым должен придти индекс новой скорости обмена. Этот индекс скорости также эхом отправляется назад и затем новая скорость применяется в UART - функция UartSetBaud(). Поскольку UART настроен на обмен 8-ми битными словами, то для передачи конкретной скорости (например, значения 115200) потребуется приемка нескольких байт, а это усложнит код. Поэтому я ограничил доступные скорости в примере массивом UART_Rates[] = {9600, 56000, 115200}. Количество значений для нашего примера не принципиально, их можно сделать и больше. Сейчас важно запомнить, что скоростей всего 3, следовательно, в МК можно будет посылать команды //'R'// с индексами - 0, 1 и 2. =====Настройка UART - Uart.c===== В файле //Uart.h// макроопределением USE_UART2 выбирается, какой интерфейс будет использоваться в //Uart.c//. В нашем примере мы выставили джамперами подключение к выводу RS-232 интерфейса Uart2, поэтому приведу здесь часть файла //Uart.h//, описывающую необходимые для данного случая настройки. #define USE_UART2 #ifdef USE_UART2 #define UART_X MDR_UART2 #define UART_IRQ UART2_IRQn #define UART_CLOCK RST_CLK_PCLK_UART2 #define UART_CLOCK_TX RST_CLK_PCLK_PORTD #define UART_CLOCK_RX RST_CLK_PCLK_PORTD #define UART_PORT_TX MDR_PORTD #define UART_PORT_PinTX PORT_Pin_13 #define UART_PORT_FuncTX PORT_FUNC_MAIN #define UART_PORT_RX MDR_PORTD #define UART_PORT_PinRX PORT_Pin_14 #define UART_PORT_FuncRX PORT_FUNC_MAIN #endif Эти определения используются далее в функциях работы с UART в //Uart.c//. #include #include #include #include "Uart.h" // Инициализация модуля UART void UART_Initialize (uint32_t uartBaudRate) { // Структура для инициализации линий ввода-вывода PORT_InitTypeDef GPIOInitStruct; // Структура для инициализации модуля UART UART_InitTypeDef UARTInitStruct; // Разрешение тактирования портов и модуля UART RST_CLK_PCLKcmd (UART_CLOCK | UART_CLOCK_TX | UART_CLOCK_RX , ENABLE); // Общая конфигурация линий ввода-вывода PORT_StructInit (&GPIOInitStruct); GPIOInitStruct.PORT_SPEED = PORT_SPEED_MAXFAST; GPIOInitStruct.PORT_MODE = PORT_MODE_DIGITAL; // Конфигурация и инициализация линии для приема данных GPIOInitStruct.PORT_FUNC = UART_PORT_FuncRX; GPIOInitStruct.PORT_OE = PORT_OE_IN; GPIOInitStruct.PORT_Pin = UART_PORT_PinRX; PORT_Init (UART_PORT_RX, &GPIOInitStruct); // Конфигурация и инициализация линии для передачи данных GPIOInitStruct.PORT_FUNC = UART_PORT_FuncTX; GPIOInitStruct.PORT_OE = PORT_OE_OUT; GPIOInitStruct.PORT_Pin = UART_PORT_PinTX; PORT_Init (UART_PORT_TX, &GPIOInitStruct); // Конфигурация модуля UART UARTInitStruct.UART_BaudRate = uartBaudRate; // Скорость передачи данных UARTInitStruct.UART_WordLength = UART_WordLength8b; // Количество битов данных в сообщении UARTInitStruct.UART_StopBits = UART_StopBits1; // Количество STOP-битов UARTInitStruct.UART_Parity = UART_Parity_No; // Контроль четности UARTInitStruct.UART_FIFOMode = UART_FIFO_OFF; // Включение/отключение буфера UARTInitStruct.UART_HardwareFlowControl = UART_HardwareFlowControl_RXE // Аппаратный контроль за передачей и приемом данных | UART_HardwareFlowControl_TXE; // Инициализация модуля UART UART_Init (UART_X, &UARTInitStruct); // Выбор предделителя тактовой частоты модуля UART UART_BRGInit (UART_X, UART_HCLKdiv1); // Выбор источников прерываний (прием и передача данных) UART_ITConfig (UART_X, UART_IT_RX | UART_IT_TX, ENABLE); // Разрешение работы модуля UART UART_Cmd (UART_X, ENABLE); } Описывать инициализацию не буду, думаю здесь все понятно. Сначала настраиваются порты GPIO, через которые работает UART2, затем выставляются параметры работы самого UART2. Включение прерываний я вынес в отдельную функцию, чтобы была возможность отдельного использования в будущем. void UART_InitIRQ(uint32_t priority) // priority = 1 { // Назначение приоритета аппаратного прерывания от UART NVIC_SetPriority (UART_IRQ, priority); // Разрешение аппаратных прерываний от UART NVIC_EnableIRQ (UART_IRQ); } ====Смена скорости UART==== Наибольший интерес представляет функция смены скорости. Вот ее код void UartSetBaud(uint32_t baudRate, uint32_t freqCPU) { uint32_t divider = freqCPU / (baudRate >> 2); uint32_t CR_tmp = UART_X->CR; uint32_t LCR_tmp = UART_X->LCR_H; // Так сделано в Bootloader - сбоит! // while ( !(UART_X->FR & UART_FLAG_TXFE) ); // wait FIFO empty // Так работает! while ( (UART_X->FR & UART_FLAG_BUSY) ); // wait BUSY UART_X->CR = 0; UART_X->IBRD = divider >> 6; UART_X->FBRD = divider & 0x003F; UART_X->LCR_H = LCR_tmp; UART_X->CR = CR_tmp; } В этой функции есть сразу несколько особенностей: ===Особенность 1=== Для выставления скорости обмена по UART необходимо знать скорость работы ядра, поэтому этот параметр так же передается в данную функцию. Дело в том, что в SPL нет штатной функции смены скорости UART, и эта скорость задается только при вызове UART_Init(). В UART_Init() частота ядра высчитывается программно функцией RST_CLK_GetClocksFreq(), файл //MDR32F9Qx_rst_clk.c//. Для этого используются: - значений всех ключей тракта тактирования - значения HSE_Value заданного в //MDR32F9Qx_config.h//. Функция RST_CLK_GetClocksFreq() весьма объемна и вычисляет так же частоты USB, ADC, RTCHSI, RTCHSE. Поэтому использование этой функции я считаю слишком накладным и предпочел задать частоту CPU снаружи. Для демо-платы используются резонаторы 8МГц, поэтому значение HSE_Value верно: #define HSE_Value ((uint32_t)8000000) **ВАЖНО!** Если на пользовательской плате для HSE используется резонатор c частотой отличной от 8 МГц, то необходимо указать эту частоту в значении HSE_Value, файл //MDR32F9Qx_config.h//. Иначе делители будут считаться не верно и обмен по UART будет работать на неверной частоте! ===Особенность 2=== **ВАЖНО!** При записи новых делителей необходимо соблюдать последовательность записи регистров - **IBRD, FBRD** и завершающий **LCR_H!** Как указано в спецификации, данные регистры образуют общий 30-разрядный регистр, который обновляется по стробу, формируемому при записи LCR_H. То есть, при смене скорости запись в регистр LCR_H должна быть завершающей! ===Особенность 3=== При работе с Bootloader ([[prog:uart:loader_uart|Тестируем Bootloader в режиме UART]]) мы видели ситуации, когда при запросе смены скорости, подтверждение команды приходило уже на новой скорости. Согласно спецификации же, сначала должно придти подтверждение на старой скорости, а затем произойти смена скорости UART. Причем даже при записи новой скорости сразу за записью отправляемых данных в регистр UARTx->DR, аппаратная часть должна сначала завершить пересылку, а затем сменить скорость. Но так не происходит. По моим тестам при смене скорости подтверждение приходит то на новой, то на старой скорости. Ответ на старой скорости приходит значительно реже. Это говорит о неких временных факторах (может быть гонках фронтов), которые влияют на поведение блока. По некоторым исходникам с форума смена скорости в начальном загрузчике происходит так: // Обработчик команды смены скорости в main case CMD_BAUD : { u32 tmp = UartReceiveParam(Uart); // Считывание новой скорости if ( tmp == ~0L ) { err = ERR_CHN; break; } UartSendByte(Uart, CMD_BAUD); // Uart->DR = CMD_BAUD; UartSetBaud(Uart, tmp); break; } // Смена скорости void UartSetBaud(_uart * uart, u32 divider) { while ( !(uart->FR & mask_UART_FR_TXFE) ); // Ответ CMD_BAUD "пролетает" FIFO и уходит в передатчик uart->CR = 0; // Выключение UART - передача ответа не успела пройти uart->IBRD = divider >> 6; uart->FBRD = divider & 0x3F; uart->LCR_H = (3 << offs_UART_LCR_H_WLEN); uart->CR = UART_MODE_TEST; } Перед сменой скорости логично использовать проверку с ожиданием, что UART уже все отправил. В Bootloader - e для этого используется флаг опустошения буфера. while ( !(UART_X->FR & UART_FLAG_TXFE) ); Но при записи одиночного слова в DR оно сразу проскакивает FIFO и уходит в передатчик, т.е. данный флаг не дает необходимой задержки перед сменой скорости. Поэтому я попробовал использовать флаг занятости UART и, как показали дальнейшие тесты, такой вариант работает. while ( (UART_X->FR & UART_FLAG_BUSY) ); Если бы в начальном загрузчике исправить код, то, вероятно, смена скорости работала бы согласно спецификации. =====Настройка тактирования - Clock.c===== Настройка тактирования реализована в файле //Clock.c//, функция Clock_Init_HSE_PLL(). Данная функция настраивает частоту ядра на работу от генератора HSE c использованием умножителя PLL, которые передается во входном параметре. В примере используется максимальный коэффициент умножения PLL_MUL = 16 (//main.c//). #include #include #include #include #include "Clock.h" void Clock_Init_HSE_PLL(uint32_t PLL_Mul) // 128 MHz { RST_CLK_DeInit(); /* Enable HSE (High Speed External) clock */ RST_CLK_HSEconfig(RST_CLK_HSE_ON); while (RST_CLK_HSEstatus() != SUCCESS); /* Configures the CPU_PLL clock source */ RST_CLK_CPU_PLLconfig(RST_CLK_CPU_PLLsrcHSEdiv1, PLL_Mul); /* Enables the CPU_PLL */ RST_CLK_CPU_PLLcmd(ENABLE); while (RST_CLK_CPU_PLLstatus() == ERROR); /* Enables the RST_CLK_PCLK_EEPROM */ RST_CLK_PCLKcmd(RST_CLK_PCLK_EEPROM, ENABLE); /* Sets the code latency value */ if (PLL_Mul * HSE_Value < 25E+6) EEPROM_SetLatency(EEPROM_Latency_0); else if (PLL_Mul * HSE_Value < 50E+6) EEPROM_SetLatency(EEPROM_Latency_1); else if (PLL_Mul * HSE_Value < 75E+6) EEPROM_SetLatency(EEPROM_Latency_2); else if (PLL_Mul * HSE_Value < 100E+6) EEPROM_SetLatency(EEPROM_Latency_3); else if (PLL_Mul * HSE_Value < 125E+6) EEPROM_SetLatency(EEPROM_Latency_4); else //if (PLL_Mul * HSE_Value <= 150E+6) EEPROM_SetLatency(EEPROM_Latency_5); // Additional Supply Power if (PLL_Mul * HSE_Value < 40E+6) SetSelectRI(RI_till_40MHz); else if (PLL_Mul * HSE_Value < 80E+6) SetSelectRI(RI_till_80MHz); else SetSelectRI(RI_over_80MHz); /* Select the CPU_PLL output as input for CPU_C3_SEL */ RST_CLK_CPU_PLLuse(ENABLE); /* Set CPUClk Prescaler */ RST_CLK_CPUclkPrescaler(RST_CLK_CPUclkDIV1); /* Select the CPU clock source */ RST_CLK_CPUclkSelection(RST_CLK_CPUclkCPU_C3); } Код легче понять, если ориентироваться на картинку тракта тактирования из спецификации. Я немного ее упростил и показал соответствие ключей функциям SPL. {{prog:uart:ve1_clock.png}} В основном код состоит из включения мультиплексоров С1, С2, С3 и PLL. Но есть два важных момента. - Выборка команд из EEPROM не может происходить быстрее, чем 25МГц, поэтому необходимо выставлять задержку доступа к EEPROM при работе ядра на больших частотах. Ядро останавливается на время этой задержки, пока считывается очередная порция инструкций. За раз из EEPROM извлекается 16 байт, где может быть закодировано от 4 до 8 инструкций процессора. Установка задержки происходит функцией EEPROM_SetLatency(), при этом предварительно на блок EEPROM подается тактирование. - При работе на высоких частотах необходимо регулировать параметры внутреннего регулятора напряжения ядра(LDO). В спецификации это параметры SelectRI и LOW в регистре REG_0E, которые выбираются по частоте ядра и всегда должны быть равны. Раздельное управление этими параметрами возможно, но, согласно спецификации, не рекомендуется. Если используется много периферийных блоков, то случается, что без выставления данных параметров МК сбоит, поэтому я выставляю эти параметры всегда. Выставлением данных параметров занимается функция SetSelectRI(). typedef enum { RI_till_10KHz, RI_till_200KHz, RI_till_500KHz, RI_till_1MHz, RI_Gens_Off, RI_till_40MHz, RI_till_80MHz, RI_over_80MHz } SelectRI; void SetSelectRI(SelectRI extraI) { uint32_t temp; RST_CLK_PCLKcmd(RST_CLK_PCLK_BKP, ENABLE); temp = MDR_BKP->REG_0E & 0xFFFFFFC0; temp |= (extraI << 3) | extraI; MDR_BKP->REG_0E = temp; } В приведенном списке SelectRI присутствует значение RI_Gens_Off. Оно выставляется, когда тактирование снаружи микросхемы задается не резонатором, а внешним генератором. В таком варианте необходимость во внутреннем генераторе отпадает и частота напрямую (режим ByPass) идет на вход схемы тактирования, мультиплексор С1 вход HSE. //Напомню, что резонатор генерирует синусоидальный сигнал, который далее генератором преобразуется в прямоугольные импульсы.// =====Проверка работоспособности примера===== Посылать и получать данные по UART будем так же, как мы делали в статье [[prog:uart:loader_uart|Тестируем Bootloader в режиме UART]]. То есть используем программу Terminal v1.9b. Аналогично изложенному в указанной статье, создадим три макроса для смены скорости. // макрос М1 - смена скорости на 9600 R$00 // макрос М2 - смена скорости на 56000 R$01 // макрос М3 - смена скорости на 115200 R$02 Для проверки эхо будем посылать символ 'A'. {{prog:uart:uart_rates_75.png}} Согласно картинке, алгоритм проверки получился такой: - Красная часть - работа на скорости 115200 * Выставляем начальную скорость 115200 и прочие параметры согласно настройкам UART в МК. Нажимаем Connect. * Посылаем символ 'A', в ответ получаем символ 'A'. Эхо работает. * Запускаем макрос //М1//, в МК уходит посылка //R$00//, возвращается //R<0>//. Эхо подтверждение пришло, скорость в МК теперь другая. - Синяя часть - работа на скорости 9600 * Выставляем скорость на 9600 и посылаем 'A', получаем ответ 'A'. Скорость сменилась, эхо работает. * Запускаем макрос //М2//, в МК уходит посылка //R$01//, возвращается бинарные //52// и //01//. Бинарные данные показываются в правом столбце. Наблюдение бинарных данных удобнее, поскольку не все числа имеют читабельное символьное представление. - Зеленая часть - работа на скорости 56000 * Выставляем скорость на 56000 и посылаем 'A', получаем ответ 'A'. Скорость сменилась, эхо работает. * Запускаем макрос //М3//, в МК уходит посылка //R$02//, возвращается бинарные //52// и //02//. - Красная часть - возвращение на скорость 115200 * Выставляем скорость на 115200 и посылаем 'A', получаем ответ 'A'. Скорость сменилась, эхо работает. Все работает так, как запланировано. В итоге мы получили пример реализующий эхо-режим в UART2 и научились переключать скорость обмена.