Продолжаем цикл про основы работы STM32MXCube и программированию микроконтроллеров STM32.
Часть 1.
Часть 2.
В прошлых частях мы освоили базовые настройки микроконтроллера, работу с GPIO, таймером, DMA и DAC. В этой части мы познакомимся с ADC и USB.
Небольшое вступление к третьей части
Вначале я хочу сказать, что с этой части я буду использовать отладочную плату NUCLEO-F767ZI. Эта плата более доступна, чем STM32F746G Discovery, использует микроконтроллер в корпусе LQFP144, а не BGA, и сама плата более удобна для встраивания в разные DIY-проекты. Она имеет Ethernet и USB, а также JTAG-отладчик. Недостатком платы является отсутствие LCD, но он нам пока не нужен.
Хотя плата имеет другой микроконтроллер, все проекты из предыдущих частей переносятся на неё почти без изменений (нужно только поменять номера пинов). Также следует учесть, что на этой плате микроконтроллер тактируется источником 8 МГц. Кварц для тактирования микроконтроллера предусмотрен схемой, но не распаян, сигнал 8 МГц снимается с JTAG-отладчика. Если ваш проект использует интерфейс USB, то желательно включать тактирование от HSE, а не от внутреннего RC-осциллятора, так как RC-осциллятор не обладает достаточной точностью и стабильностью частоты. При попытке включить RC-осциллятор при наличии в проекте USB, STM32CubeMX выдаст предупреждение и предложит переключиться на HSE (то есть на внешний высокостабильный источник тактирования). На практике USB-интерфейс всё равно работает, даже от RC, но лучше не рисковать.
Я перевёл на эту плату проекты из предыдущих частей и залил их на гитхаб .
В комментариях к предыдущим частям были вопросы по поводу IDE. STM32CubeMX позволяет автоматически создавать проекты для различных IDE: IAR (EWARM), MDK ARM v4, MDK ARM v5, Atollic TRUEStudio, SW4STM32 и др. Я пользуюсь Atollic TRUEStudio, который доступен для скачивания с официального сайта бесплатно.
Также я проверил материал из предыдущих частей и внёс ряд поправок.
Хочу поблагодарить Shamrel за ценные комментарии к предыдущей части.
USB VCP
Одним из самых простых режимов работы USB является режим VCP — Virtual COM Port. Настройка работы с ним потребует от вас минимальных усилий.
В STM32CubeMX находим на вкладке Pinout раздел USB_OTG_FS и устанавливаем Mode=Device_Only:
В разделе USB_DEVICE устанавливаем Class For FS IP в режим CDC VCP (Communication Device Class Virtual Com Port):
Теперь нужно настроить конфигурацию тактирования так, чтобы частота USB составляла 48 MHz:
Идём дальше, на вкладку Configuration, и отключаем параметр VBUS Sensing:
Генерируем код и открываем проект в IDE.
Находим файл usbd_cdc_if.c и в него вставляем следующее:
static int8_t CDC_Receive_FS (uint8_t* Buf, uint32_t *Len)
{
/* USER CODE BEGIN 6 */
USBD_CDC_SetRxBuffer(&hUsbDeviceFS, &Buf[0]);
CDC_Transmit_FS(Buf, *Len);
USBD_CDC_ReceivePacket(&hUsbDeviceFS);
return (USBD_OK);
/* USER CODE END 6 */
}
и
uint8_t CDC_Transmit_FS(uint8_t* Buf, uint16_t Len)
{
uint8_t result = USBD_OK;
/* USER CODE BEGIN 7 */
USBD_CDC_SetTxBuffer(&hUsbDeviceFS, Buf, Len);
result = USBD_CDC_TransmitPacket(&hUsbDeviceFS);
/* USER CODE END 7 */
return result;
}
Здесь реализован режим эха: всё, что приходит в порт, мы немедленно отправляем обратно.
Компилируем и прошиваем микроконтроллер. Затем подключаем разъём User USB платы к компьютеру. Система должна обнаружить новый COM-порт.
Для Linux: проверяем ls /dev/tty*, появилось устройство /dev/ttyACM0. Проверяем, и здесь нас ждут ещё сюрприз: отказано в доступе. Нужно добавить себя в группу dialout:
sudo adduser user dialout
(где user — ваше имя пользователя)
Для работы с устройством в Windows вам понадобится скачать и установить драйвер. Для работы в OS X и Linux специальный драйвер не нужен.
Запускаем (например) Putty, настраиваем параметры порта. Они должны совпадать с параметрами, указанными в свойствах порта (см. «диспетчер устройств/порты»).
Пробуем открыть порт в Putty и что-то послать в порт:
Если порт не открывается, можно попробовать выйти из режима отладки в IDE и перезапустить плату. Всё должно заработать.
Как мы увидели, работа с USB в режиме виртуального COM-порта очень проста. Единственный недостаток этого режима — очень низкая скорость передачи данных. Интерфейс USB в режиме Full Speed обеспечивает до 12 Мбит/c, в режиме High Speed — до 480 Мбит/c, но VCP ограничивает скорость жалкими 128 кбит/c.
Можно сделать высокую скорость передачи данных, но пока отложим это до следующего раза.
АЦП
Сейчас попробуем запустить АЦП, получить с него значения и отправить на компьютер, реализовав очень простой (и очень медленный) «осциллограф». Чтобы было интереснее, мы подадим на АЦП синусоидальный сигнал, сформированный ЦАП. Так как мы уже делали это в прошлой части, я просто скопирую код в новый проект (с небольшими изменениями, которые большой роли не играют).
Сначала немного об АЦП, встроенном в микроконтроллер. Микроконтроллер STM32F767ZI имеет три 12-разрядных АЦП, типа SAR (последовательного приближения), имеющие производительность до 2 MSPS (млн. выборок в секунду). Этот тип АЦП отличается высокой скоростью преобразования, но меньшей точностью, чем сигма-дельта АЦП. Вход опорного напряжения VREF соединён с VDDA, и, через индуктивность, с VDD. Таким образом, опорное напряжение в нашем случае равно 3,3В. Особенностью SAR ADC является использование на входе схемы выборки-хранения, содержащей конденсатор. В момент выборки значения сигнала конденсатор подключается ко входу и заряжается до величины входного сигнала. Если источник сигнала будет иметь слишком большое внутреннее сопротивление, конденсатор не успеет зарядиться полностью, и мы получим заниженное значение. Этот и другие моменты использования АЦП изложены в [1].
АЦП данного микроконтроллера имеет множество режимов работы [2], мы рассмотрим только один из них. Попробуем получить одновременно два значения сигнала с двух АЦП, строго синхронно, и записать их в буфер через DMA.
Итак, создаём новый проект, добавляем в него уже готовый код для генерации синусоиды на DAC и для USB VCP (через него мы будем отсылать данные на компьютер). Далее (проводами) соединяем выход ЦАП с входами АЦП1 и АЦП2. Для того, чтобы как-то различать сигналы на аналоговых входах, я соединил АЦП1 c ЦАП напрямую, а АЦП2 — через делитель напряжения на переменном резисторе, чтобы можно было менять амплитуду сигнала.
Также следует учесть, что в микроконтроллерах STM32 используются АЦП последовательного приближения (SAR), которые потребляют от источника сигнала довольно большой ток в момент измерения, и требуют источника сигнала с низким импедансом.
Рис. 1. Схема выборки-хранения SAR ADC (не из STM32, но совершенно аналогичный)
В моменты выборки сигнала конденсаторы (рис. 1) подключаются ко входу АЦП и должны зарядиться до полного уровня сигнала за очень короткое время, потребляя при этом значительный ток. Если источник сигнала будет иметь большое сопротивление, они не успеют зарядиться, и показания АЦП будут неверными. На практике это означает, что мы должны в большинстве случаев использовать внешний буферный усилитель. Так как сегодня мы сосредоточимся на программных аспектах задачи, мы можем обойтись без усилителя, но следует помнить, что без усилителя показания АЦП будут существенно искажены, и в реальных проектах он нужен.
Рис. 2. Схема выборки-хранения вызывает провалы уровня сигнала на входе АЦП.
К сожалению, в документации STM32 эти вопросы рассмотрены слабо, но я могу порекомендовать руководство [3].
Если читателям будет интересно, я могу рассмотреть основы схемотехники аналоговых узлов сопряжения сигналов с АЦП в следующей статье.
Нам нужны будут в нашем проекте два таймера. Один из них будет задавать период работы ЦАП, второй — АЦП. Настроим аналого-цифровой преобразователь на работу в двухканальном режиме с одновременной выборкой. Выборка будет происходить по таймеру TIM2. Полученные значения будут складываться в буфер с помощью DMA.
Мы будем использовать однократный режим работы DMA (есть также циклический, с ним мы уже познакомились при изучении ЦАП). После того, как буфер заполнится значениями с АЦП, мы копируем его содержимое в другой буфер (с некоторой обработкой), передаём его через USB и запускаем процесс снова. Также для отладки и индикации режима работы мы используем два порта GPIO, к которым подключены светодиоды.
Итак, у нас создан проект, в который мы добавили DAC и таймер TIM1. Ещё нам нужно добавить ADC1 (вход IN9), ADC2 (вход IN12) и таймер TIM2. Также нам понадобится USB_OTG_FS.
Настраиваем ADC1 на работу в режиме одновременной выборки, с запуском по таймеру 2:
ADC2 при этом настраивается автоматически:
Настраиваем DMA:
Обращаем внимание, что размер передаваемых данных Word, а не Half Word, т.к. за один раз передаются данные с двух АЦП, упакованные в 32-битное слово.
Настраиваем таймер TIM2:
USB настраиваем так же, как мы это уже делали. Генерируем код.
Я не буду здесь расписывать весь исходник проекта, остановлюсь лишь на ключевых моментах. Запуск цепочки таймер-ADC-DMA:
//start adc
HAL_ADC_Start(&hadc2);
HAL_ADCEx_MultiModeStart_DMA(&hadc1, (uint32_t *)adc_buf, ADC_BUF_SIZE);
HAL_TIM_Base_Start_IT(&htim2);
Обработчик прерывания:
void HAL_ADC_ConvCpltCallback(ADC_HandleTypeDef* hadc)
{
HAL_TIM_Base_Stop(&htim2);
HAL_ADC_Stop(&hadc2);
HAL_ADCEx_MultiModeStop_DMA(&hadc1);
const int threshold = (1 << 11);
//find a trigger condition
if(state == STARTED)
{
HAL_GPIO_WritePin(GPIOB, GPIO_PIN_7, 1);
for(int i = 0; i < (ADC_BUF_SIZE - VCP_BUF_SIZE); i++)
{
uint32_t sample0_0 = adc_buf[i] & 0x00000FFF;
uint32_t sample0_1 = adc_buf[i + 1] & 0x00000FFF;
if(sample0_0 < threshold && sample0_1 >= threshold) //the trigger condition
{
memcpy(vcp_buf, adc_buf + i + 1, VCP_BUF_SIZE * sizeof(uint32_t));
vcp_buf[0] |= 0x80000000; //mark the first sample in the frame
CDC_Transmit_FS((uint8_t*)vcp_buf, VCP_BUF_SIZE * sizeof(uint32_t));
}
}
HAL_GPIO_WritePin(GPIOB, GPIO_PIN_7, 0);
}
//restart adc
HAL_ADC_Start(&hadc2);
HAL_ADCEx_MultiModeStart_DMA(&hadc1, (uint32_t*)adc_buf, ADC_BUF_SIZE);
HAL_TIM_Base_Start_IT(&htim2);
//led flashing
static int cnt = 0;
if((cnt++) % 128 == 0)
{
HAL_GPIO_TogglePin(GPIOB, GPIO_PIN_14); //Toggle the state of pin
}
}
Почему мы не можем передать через USB непосредственно содержимое исходного буфера? Так как скорость VCP слишком мала, мы не сможем передавать весь поток данных с АЦП. Мы захватываем кусок сигнала, передаём его «наверх», потом захватываем следующий кусок и т. д. Если мы не предпримем специальных мер, то в порт будут передаваться случайные фрагменты исходного сигнала. Поэтому нужно сделать программный аналог «триггера», как у цифрового осциллографа. Мы будем передавать в порт не рандомный фрагмент, захваченный АЦП, а кусок сигнала после возникновения некоторого условия. Таким условием может быть пересечение сигналом некоторого уровня в направлении снизу вверх: sample0_0 < threshold && sample0_1 >= threshold, где threshold — порог срабатывания.
Именно для этого мы делаем буфер АЦП в два раза больше буфера VCP, и просматриваем его до середины в поисках такого условия. Если условие не наступило, не отправляем в порт ничего, и запускаем следующий цикл АЦП.
DMA упаковывает сигналы с двух АЦП в одно 32-битное слово. Не будем менять этот формат, просто добавим единицу в старший разряд первого отчёта в буфере, чтобы ПО верхнего уровня могло распознать начало «кадра»:
vcp_buf[0] |= 0x80000000;
Для отображения сигнала на компьютере я написал маленькую программу на C#:
Она в основном собрана из компонентов в Visual Studio и содержит минимум кода.
Её исходники также доступны на Github.
Что дальше
В следующей части мы рассмотрим интерфейс Ethernet и немного операционную систему реального времени FreeRTOS.
Ссылки
Исходники проектов к всему циклу статей можно скачать на github. Все проекты сделаны для платы Nucleo F767ZI и используют IDE Atollic TRUEStudio.
[1] AN2834 Application note. How to get the best ADC accuracy in STM32 microcontrollers
[2] AN3116 Application note. STM32’s ADC modes and their applications
[3] Cookbook for SAR ADC Measurements. Freescale Semiconductor. AN4373
Благодарю за внимание, о замеченных ошибках и опечатках прошу сообщать в личку. Продолжение следует.
Автор: 32bit_me