В предыдущей части мы знакомились с основами работы в связке System Workbench for STM32 + STM32CubeMX. Теперь познакомимся с работой UART и ADC.

Для работы нам потребуется проект, созданный по прошлой статье. Если у вас его нет, то по-быстрому изучите первую часть, чтобы понять основы.

Также нам потребуется терминальная программа для общения с виртуальным COM портом. Вы можете использовать любой терминал, который вам по вкусу. Если у вас нет такой программы, то я рекомендую вам Terminal_by_zloiMOZG, на основе которого и будут все примеры в статье. Терминал не требует установки.

Ну, приступим.

  1. UART.

Для работы с UART нам потребуется подключить джмаперы PA10 и PA9 в положения USART1_RX и USART1_TX соответственно. Таким образом, мы подключили RX и TX выходы к преобразователю USB-UART. Так как на нашей отладочной плате стоит USB-HUB, то и отладки, и работа с UART будут происходить через 1 подключённый кабель.

После того, как вы переключили джамперы, откройте ваш  проект в System Workbench for STM32, составленный по предыдущему уроку, а также откройте проект STM32CubeMX.

Первым делом в STM32CubeMX нам следует включить USART1 в асинхронном режиме. Для этого в левом столбце найдите его и включите нужный режим, как на скриншоте:

Поле того, как вы включили USART1 у вас должны стать активными пины PA9 и PA10, как показано на скриншоте:

Теперь нам следует настроить режимы работы UARTа. Для этого в STM32CubeMX переходим в закладку Configuration и в колонке Connectivity нажать на иконку нашего USART1. После нажатия перед нами появится окно настройки. На данный момент нас интересуют только 2 закладки данного окошка, а именно Parameter Settings и NVIC Settings. В окне Parameter Settings выставляются основные параметры передачи данных, а в окне NVIC Settings включаются или выключаются прерывания данного UART. Выставите параметры как показано на скриншотах:

Как вы обратили внимание, мы активировали прерывания USART1.

После того, как все настройки сделаны, давайте попробуем сгенерировать наш проект, как его генерировать, было рассказано в предыдущей части. После этого переходим в System Workbench for STM32 с нашим кодом, жмём F5 для того, чтобы обновить проект и подтянуть изменения, сделанные STM32CubeMX . В функцию main должен добавиться вызов функции инициализации USART1.

Теперь можно попробовать написать самый простой тест для ком порта, а именно будем отправлять в цикле каждую секунду переменную, которую будем увеличивать на “1” после каждой отправки.

Делается это очень просто. В функции main и в бесконечном цикле while(1) пишем:

volatile u_int8_t test[1];
while (1)
{
HAL_UART_Transmit_IT(&huart1,(uint8_t*)test,(uint16_t)1);
test[0]++;
HAL_Delay(1000);

Что мы тут делаем?

  1. Объявляем одномерный массив test, с размером элемента 1 байт. Делается это потому, что функция HAL_UART_Transmit_IT может работать только с массивами.
  2. Собственно отправляем нашу переменную test по UART.
  3. Увеличиваем значение переменной на “1”.
  4. Ждём одну секунду. После этого процесс повторяется.

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

Если у вас получился такой же результат, значит первый тест мы сделали успешно. Теперь пробуем научиться правильно отправлять и принимать данные через UART в STM32.

*немного о “правильности”*

Очень часто в уроках по STM32 можно встретить рекомендации о том, как использовать прерывания. В 99% случаев вам будут говорить: “Для того, чтобы обработать прерывание, вам надо писать свой код в файле stm32f4xx_it.c. Найдите там требуемый хендлер и пишите код.” . Для нашего случая, когда мы используем USART1 вам было бы предложено использовать void USART1_IRQHandler(void).

Это НЕПРАВИЛЬНО! 

Дело в том, что ARM архитектура предполагает всего 1 вектор прерывания на ВСЕ события, которые могут произойти для определённого интерфейса или модуля. То есть в данный хендлер мы попадём как в случае удачной отправки или приёма, так и в случае возникновения ошибки. Для того, чтобы узнать по какому поводу случилось прерывание, мы можем либо по хардкору прочитать нужный регистр и понять из него, что же случилось. Ну или можем воспользоваться написанными за нас колбеками по разным событиям, которые можно для нас воспринимать как вектор прерывания по определённому событию. По сути в библиотеке HAL уже реализован “разбор полётов”, по какому поводу произошло прерывание, и уже написаны отдельные колбеки для разных событий. Найти колбеки для UART можно в файле stm32f4xx_hal.c. Самый простой способ найти – это нажать Ctrl+F и в поиске ввести “__weak” примерно так:

 

 

Чтобы добавить работу с колбекам в ваш код, просто скопируйте функцию только без приписки  __weak, и вставьте её в область “USER CODE” перед функцией main. Примерно так:

 

 

Как вы можете обратить внимание, в самих колбеках я уже вставил проверку на то, какой UART стал причиной попадания в колбек. Как можно догадаться, колбек у нас тоже “один на всех”. В случае, когда мы используем, к примеру, только один UART, нам это вообще никак не мешает. Но если используемых UARTов уже два, вот тут нам и пригодится знание о том, кто же у нас был причиной попадания в колбек. МЫ работаем с USART1 и соответственно, проверяя в колбеке  if(huart == &huart1){*user code* } мы можем точно убедится, что событие произошло именно по нужной нам причине.

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

 

Для приёма данных по UART у нас есть функция:

HAL_UART_Receive_IT(&huart1,&receive_val,(uint16_t)1);

Работа этой функции немного не очевидна для новичков. Итак, в данной функции мы сообщаем, что когда-то нам должно прийти какое-то кол-во байт (конкретно в этом примере мы сообщаем, что в USART1 (&huart1,) должны придти данные, их надо положить в переменную (или массив, данная функция поддерживает работу и с тем и с тем) receive_val. Ожидаемый объём данных равен одному байту ((uint16_t)1)). И теперь мы попадём в HAL_UART_RxCpltCallback только тогда, когда нам придёт именно запрошенное кол-во байт. Данная функция не блокирует работу контроллера. Мы как бы сообщили, что ждём посылку, и всё. МК начал её ждать, в это время занимаясь выполнением другого кода. Как только данные  в нужном кол-ве придут, у нас сработает колбек, и мы уже сможем их обработать, забрав из переменной или массива receive_val. Но надо сразу понять, что если мы ожидаем ещё данные, нам следует опять вызвать функцию HAL_UART_Receive_IT для того, чтобы “запросить” ещё данных. То есть если мы постоянно ждём ещё данных, мы постоянно их запрашиваем, каждый раз после прихода очередной порции.

Мне кажется, вам уже не терпится попробовать принять данные. Но ведь просто так гонять чиселки не так интересно. Давайте напишем код, в котором контроллер должен будет отвечать на определённый байт (например байт должен быть равен числу 100), пришедший по UART байтом, в котором будет записано значение с одного из каналов АЦП.

А для этого изучим, как же АЦП работает.

2. ADC

Для настройки АЦП в первую очередь нам следует переключить джампер PA0 на припаянный к отладочной плате переменный резистор. В том, как это сделать, вам поможет шелкография на плате. Напомню, что речь идёт о отладочных платах STM32F4EvaluationBoard и STM32F7EvaluationBoard. Если у вас другая отладочная плата, то подключите переменный резистор к пину PA0 любым удобным вам способом в режиме делителя напряжения.

После того, как вы переключили джампер или подключили переменный резистор, откройте проект в STM32CubeMX, в котором мы работали с UART, и включите ADC1 поставив галочку около IN0. В результате у вас должно получится вот так:

 

Теперь переходим в закладку Configuration и в колонке Analog жмём на иконку ADC1.

 

 

В появившемся окошке делаем настройки как показано на скриншотах:

Как можно понять из скриншотов, мы настроили АЦП на выдачу результата в формате 1 байта, каждое преобразование должно вызывать программно. А так же мы включили прерывания.

После этого мы генерируем код, и возвращаемся к System Workbench for STM32. Обновляем проект, как это было описано выше, после чего у нас добавляется  инициализация ADC1.

 

Также нам следует добавить в код колбек срабатывающий по окончанию преобразования АЦП, с обязательно проверкой на то, что сработал колбек именно по причине события, вызванного нашим АЦП.

 

void HAL_UART_TxCpltCallback(UART_HandleTypeDef *huart){
if(huart == &huart1){

}

}

Итак, как же работает АЦП.

Для того, что бы начать преобразование, нам нужно вызвать функцию HAL_ADC_Start_IT(&hadc1); . Эта функция запустит преобразование АЦП, в нашем случае единичное, так как мы настроили это в STM32CubeMX. То есть после того, как будет произведено 1 преобразование, мы получим 1 результат, АЦП прекратит работу до тех пор, пока мы ещё раз не вызовем функцию HAL_ADC_Start_IT(&hadc1); . Есть в АЦП ещё режим постоянного преобразования, когда данные он берёт с канала с определённой, настроенной ранее, частотой. В этом случае, если мы сделаем старт, АЦП будет молотить до тех пор, пока мы ему не скажем ХВАТИТ при помощи функции HAL_ADC_Stop_IT(&hadc1); .

Так как функция у нас с припиской _IT, это значит что АЦП будет работать, вызывая прерывание после окончания каждого преобразования. Как вы могли заметить, при вызове функции мы не передаём массив, куда надо складывать полученные значения. Чтобы получить значение АЦП из регистра в переменную или элемент массива, нужно присвоить переданное значение функции HAL_ADC_GetValue(&hadc1); То есть сделать вот так:

Как видно, мы делаем это в колбеке, который вызывается по окончанию преобразования, так как именно тогда мы можем точно знать, что АЦП закончил преобразование, и мы можем забрать актуальное значение. На данном скриншоте показано, как в нулевой элемент массива n кладётся результат преобразования ADC1.

 

Как работает АЦП, мы немного разобрались. Давайте попробуем написать программу, которая будет отправлять нам результат 1го преобразования АЦП после того, как по UARTу придёт байт равный 100.

Первое, что нам понадобится – это ряд переменных:

А так же код прописанный в колбеках:

 

 

Ну и конечно код в функции main:

Немного о том, что тут происходит:

После запуска микроконтроллера и инициализации всего, что нужно, мы вызываем функцию HAL_UART_Receive_IT(&huart1,&receive_val,(uint16_t)1); , тем самым говоря, что ждём прихода одного байта по UART и хотим, чтобы пришедший байт был положен в переменную receive_val.  После чего в бесконечном цикле мы постоянно смотрим, не стала ли равна единице переменная status_val, а равной “1” она станет, когда сработает HAL_UART_RxCpltCallback, который, как очевидно, сработает только после того, как нам придёт 1 байт по UART. В данном колбеке мы убедимся, что пришёл байт именно из нужного UART, если это так, то присвоим переменной status_val значение “1”, а так же скажем UARTу что мы ждём ещё 1 байт, при помощи уже известной функции HAL_UART_Receive_IT(&huart1,&receive_val,(uint16_t)1);.

 

Как только в бесконечном цикле мы увидим, что status_val стал равен единице, мы подними ножку PE0, на которой у нас висит синий светодиод, тем самым заставив его светится. После этого мы сравним пришедшее по UART значение, лежащие в переменной receive_val с нужным нам значением “100”, и если это так, то мы подождём 10мс. Это нужно для того, что бы светодиод успел нормально зажечься и как бы моргнул. После этого присвоим переменной ADC_flag значение “busy” и запустим преобразование вызвав функцию HAL_ADC_Start_IT(&hadc1);.

 

В цикле while(ADC_flag == busy); мы ждём пока переменная ADC_flag не перестанет быть равной busy. А перестанет она, когда мы попадём в HAL_ADC_ConvCpltCallback, где мы как изменим значение переменной, так и заберём результат преобразования АЦП.

 

После этого в ещё одном цикле while (UART_status_val == busy); мы проверим, не занят ли наш UART в данный момент какой-либо отправкой. Соответственно, ждать будем пока не перестанет быть занятым. Состояние переменной UART_status_val изменяется в HAL_UART_TxCpltCallback.

 

Ну и соответственно, убедившись, что всё готово для передачи данных, вызываем функцию отправки данным в UART. HAL_UART_Transmit_IT(&huart1,(uint8_t*)n,(uint16_t)1);

Конечно, не забыв поставить флаг занятости UART, обнулив переменную с перешедшими по UART данными и  погасив светодиод:

UART_status_val = busy;
receive_val = 0;
HAL_GPIO_WritePin(LED_Blue_GPIO_Port,LED_Blue_Pin,GPIO_PIN_RESET);

 

Запустив нашу программу на отладочной плате и запустив терминальную программу, вы можете увидеть, как в ответ на отправленное число “100” нам приходит значение АЦП. То, что это число – именно значение АЦП можно убедиться, покрутив соответствующий переменный резистор. А также у вас будет кратковременно моргать синий светодиод.

В терминале это выглядит примерно так:

 

Тут я 4 раза отправил число 100, предварительно вращая переменный резистор.

 

Также вы можете отметить, что если вы отправите любое, отлично от “100” значение, то в ответ в терминал данные не придут, а синий светодиод загорится ярким светом, и будет гореть ровно до тех пор, пока вы опять не отправите значение “100”.

 

На этом всё. В следующий раз мы рассмотрим работу ЦАП. Очень желательно, чтобы для этого у вас был вольтметр или осциллограф.

Проект с кодом, написанным для этой статьи можно скачать тут. Распакованную папку следует подключать к «System Workbench for STM32» как «workspace».