Русский
Русский
English
Статистика
Реклама

Timer

Учим железки разговаривать, или ESP32 DAC и немного таймера

14.11.2020 20:09:26 | Автор: admin
В ходе разработки одного очень интересного устройства (эх, лишь бы силенок хватило) я решил, что будет неплохо, если устройство это будет говорящим. Как нельзя кстати здесь пригодилось наличие в целевом микроконтроллере, ESP32 компании Espressif Systems, двухканального 8-битного ЦАПа.

В этом туториале (если его можно так назвать) я покажу, как можно быстро и довольно просто организовать проигрывание аудиофайла силами микроконтроллера ESP32.

Немного теории

Как нам сообщает Википедия, ESP32 это серия недорогих микроконтроллеров с низким энергопотреблением. Они представляют собой систему на кристалле (SoC) с интегрированными контроллерами Wi-Fi и Bluetooth, и антеннами. Основаны на ядре Tensilica Xtensa LX6 в вариантах с одним и двумя ядрами. В систему интегрирован радиочастотный тракт. МК создан и разработан китайской компанией Espressif Systems, а производится компанией TSMC по техпроцессу 40 нм. Подробнее о возможностях чипа можно прочитать на странице в Википедии и в официальной документации.

Однажды в рамках освоения этого контроллера мне захотелось воспроизвести на нем звук. Поначалу я думал, что придется использовать ШИМ. Однако, внимательнее почитав документацию, я обнаружил наличие двух каналов 8-битного ЦАПа. Разумеется, это в корне меняло дело.

В Technical Reference сказано, что ЦАП в ESP32 построен на цепочке резисторов (видимо, имеется ввиду R2R цепочка) с применением некоего буфера. Выходное напряжение может изменяться в пределах от 0 вольт до напряжения питания (3,3 вольта) с разрешением 8 бит (то есть 256 значений). Преобразование двух каналов независимое. Также имеется встроенный генератор косинуса (CW generator) и поддерживается DMA.

Я решил пока что не лезть в DMA, ограничившись построением проигрывателя на основе таймера. Как известно, чтобы воспроизвести простейший WAV-файл формата PCM, достаточно с указанной в файле частотой дискретизации читать из него сырые данные и распихивать по каналам ЦАПа, предварительно приводя (при необходимости) разрядность данных к разрядности ЦАП. Мне повезло: у меня нашелся набор звуков в формате WAV PCM 8 bit 11025 Hz mono, выдранный из ресурсов одной старой игры. Значит, будем использовать только один канал ЦАП.

Также нам понадобится таймер, способный генерировать прерывания с частотой 11025 Гц. Согласно все тому же Technical Reference, ESP32 имеет на борту два модуля таймеров по два таймера в каждом, итого четыре таймера. Они имеют разрядность 64 бита, у каждого имеется 16-битный предделитель и возможность генерации прерывания по уровню или фронту.

От теории к практике

Вооружившись примером wave_gen из esp-idf, я отправился писать код. Я не стал упарываться созданием файловой системы: цель была получить звук, а не сделать из ESP32 полноценный плеер.

Для начала перегнал один из WAV-файлов в сишный массив. В этом мне очень помогла встроенная в Дебиан утилита xxd. Простой командой
$ xxd -i file.wav > file.c

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

Далее я закомментировал первые 44 байта массива заголовок WAV-файла. Попутно я разобрал его по полям и узнал всю необходимую мне информацию о нем:
const uint8_t sound_wav[] = {//  0x52, 0x49, 0x46, 0x46,// chunk "RIFF"//  0xaa, 0xb4, 0x01, 0x00,// chunk length//  0x57, 0x41, 0x56, 0x45,// "WAVE"//  0x66, 0x6d, 0x74, 0x20,// subchunk1 "fmt"//  0x10, 0x00, 0x00, 0x00,// subchunk1 length//  0x01, 0x00,// audio format PCM//  0x01, 0x00,// 1 channel, mono//  0x11, 0x2b, 0x00, 0x00,// sample rate//  0x11, 0x2b, 0x00, 0x00,// byte rate//  0x01, 0x00,// bytes per sample//  0x08, 0x00,// bits per sample per channel//  0x64, 0x61, 0x74, 0x61,// subchunk2 "data"//  0x33, 0xb4, 0x01, 0x00,// subchunk2 length, bytes

Отсюда видно, что наш файл имеет один канал, частоту дискретизации 11025 герц и разрешение 8 бит на семпл. Заметим, что если бы я захотел анализировать заголовок программно, то мне нужно было бы учитывать порядок байт: в WAV это Little-endian, то есть младшим байтом вперед.

В итоге я создал тип структуры для хранения информации о звуке:
typedef struct _audio_info{uint32_t sampleRate;uint32_t dataLength;const uint8_t *data;} audio_info_t;

И создал собственно экземпляр структуры, заполнив ее следующим образом:
const audio_info_t sound_wav_info ={11025, // sampleRate111667, // dataLengthsound_wav // data};

В этой структуре поле sampleRate это значение одноименного поля заголовка, поле dataLength это значение поля subchunk2 length, а поле data это указатель на массив с данными.

Далее я подключил заголовочные файлы
#include "driver/timer.h"#include "driver/dac.h"

и создал прототипы функций для инициализации таймера и обработчика его прерывания Alarm, как это сделано в примере wave_gen:
static void IRAM_ATTR timer0_ISR(void *ptr){}static void timerInit(){}

После чего принялся за наполнение функции инициализации.
Таймеры в ESP32 тактируются в конечном итоге от APB_CLK_FREQ, равного 80 МГц:

driver/timer.h:
#define TIMER_BASE_CLK   (APB_CLK_FREQ)  /*!< Frequency of the clock on the input of the timer groups */

soc/soc.h:
#define  APB_CLK_FREQ    ( 80*1000000 )       //unit: Hz

Чтобы получить значение счетчика, при котором нужно генерировать прерывание Alarm, нужно частоту тактирования таймера поделить на значение предделителя, а затем на требуемую частоту, с которой должно срабатывать прерывание (у нас это 11025 Гц). В обработчик прерывания мы будем передавать указатель на структуру с данными, которые мы хотим воспроизводить.

Таким образом, функция инициализации таймера получает следующий вид:
static void timerInit(){timer_config_t config = {.divider = 8, // Предделитель.counter_dir = TIMER_COUNT_UP, // Считать вверх.counter_en = TIMER_PAUSE, // Состояние - пауза.alarm_en = TIMER_ALARM_EN, // Включить прерывание Alarm.intr_type = TIMER_INTR_LEVEL, // Прерывание по уровню.auto_reload = 1, // Автоматически перезапускать счет};// Применить конфигESP_ERROR_CHECK(timer_init(TIMER_GROUP_0, TIMER_0, &config));// Установить начальное значение счетчикаESP_ERROR_CHECK(timer_set_counter_value(TIMER_GROUP_0, TIMER_0, 0x00000000ULL));// Установить значение счетчика для срабатывания прерывания AlarmESP_ERROR_CHECK(timer_set_alarm_value(TIMER_GROUP_0, TIMER_0, TIMER_BASE_CLK / config.divider / sound_wav_info.sampleRate));// Разрешить прерыванияESP_ERROR_CHECK(timer_enable_intr(TIMER_GROUP_0, TIMER_0));// Зарегистрировать обработчик прерыванияtimer_isr_register(TIMER_GROUP_0, TIMER_0, timer0_ISR, (void *)&sound_wav_info, ESP_INTR_FLAG_IRAM, NULL);// Запустить таймерtimer_start(TIMER_GROUP_0, TIMER_0);}

Частота тактирования таймера не делится нацело на 11025, какой бы предделитель мы не установили. Поэтому я подобрал такой делитель, при котором частота максимально близка к требуемой.

Теперь переходим к написанию обработчика прерывания. Здесь все просто: берем очередной байт из массива, скармливаем его ЦАПу, продвигаемся по массиву дальше. Однако прежде всего нужно очистить флаги прерываний таймера и перезапустить прерывание Alarm:
static uint32_t wav_pos = 0;static void IRAM_ATTR timer0_ISR(void *ptr){// Очистить флаги прерыванийtimer_group_clr_intr_status_in_isr(TIMER_GROUP_0, TIMER_0);// Перезапустить прерывание Alarmtimer_group_enable_alarm_in_isr(TIMER_GROUP_0, TIMER_0);audio_info_t *audio = (audio_info_t *)ptr;if (wav_pos >= audio->dataLength) wav_pos = 0;dac_output_voltage(DAC_CHANNEL_1, *(audio->data + wav_pos));wav_pos ++;}

Да, работа со встроенным ЦАПом в ESP32 сводится к вызову одной встроенной функции dac_output_voltage (на самом деле нет).

Собственно, все. Теперь нужно внутри функции app_main() разрешить работу нужного нам канала ЦАП и инициализировать таймер:
void app_main(void){            ESP_ERROR_CHECK(dac_output_enable(DAC_CHANNEL_1));    timerInit();

Собираем, прошиваем, слушаем :) В принципе, можно подключить динамик напрямую к ножке контроллера играть будет. Но лучше все же воспользоваться усилителем. Я использовал завалявшуюся у меня в закромах TDA7050.

На этом все. Да, когда у меня наконец запело, я тоже подумал, что все оказалось гораздо проще, чем я думал. Однако может быть, эта статья чем-нибудь поможет тем, кто только начал осваивать ESP32.

Может быть, когда-нибудь (и если эта недо-статья кому-нибудь понравится) я заведу ЦАП ESP32 с использованием DMA. Там все еще интереснее, потому что в этом случае работать придется со встроенным модулем I2S.
Подробнее..

Systemd для продолжающих. Part 1 Запуск юнитов по временным событиям

02.01.2021 20:04:09 | Автор: admin

Всем привет! В последнее время я вплотную занимаюсь исследованием возможностей systemd и решил поделиться результатом исследований с сообществом, в виде небольшого (или большого, как пойдёт ;-) цикла статей. Итак первым (уже нет) номером нашей программы будет запуск юнитов по различным событиям происходящим во время работы ОС. В качестве исследовательской платформы будет выступать Manjaro Linux c systemd v247.2. И... да. Некоторые события, вынудили меня написать внеочередную статью, которая взлетела на вершину хит-парада, а опрос показал, что тема актуальна и вызывает интерес, так что погнали!

Пролог

Systemd система инициализации большинства современных систем на основе ядра Linux, обладает просто безграничными возможностями и не ограничивается обычным запуском демонов. Достаточно посмотреть на объёмы штатной документации, описывающей её возможности:

pacman -Ql $(pacman -Qsq systemd|xargs)|egrep '^systemd\s|^systemd-sysvcompat\s'|egrep "man/man[1|5|8]/[[:print:]]*\.gz"|wc -l278

И это только маны описывающие конфиги, пользовательские и администраторские утилиты systemd. Если же не ограничивать поиск практической частью, то цифра будет ещё более устрашающей:

pacman -Ql $(pacman -Qsq systemd|xargs)|egrep '^systemd\s|^systemd-sysvcompat\s'|wc -l1852

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

Disclamer: Хоть в официальной документации и манах почти не используется такое понятие как триггеры (хотя и используется triggered by), но все те штуки которые описаны в этой и следующей статье, по сути, именно ими и являются. Это сущности которые срабатывают по каким-либо событиям, поэтому не удивляйтесь, если я, авторским произволом, буду использовать этот термин.

Часть первая, очевидная. Таймеры.

Все мы знаем старый, добрый cron, во всех его проявлениях. Созданный ещё в 80-х, он, в том или ином виде, дожил до нашего времени облачных сервисов. Так-же мы все знаем его ограничения. Например одной строчкой невозможно заставить крон запускать произвольный бинарник/скрипт раз в полтора часа, начная с часа ночи, приходится описывать такое событие двумя строчками. Что-бы обойти ограничения классического крона, в systemd были придуманы такие триггеры как таймеры (юниты с окончанием *.timer) умеющие запускать произвольные сервисы или группы сервисов (*.target) периодически; по наступлении какого-либо времени; по выходу системы из спящего режима; по календарному событию (наподобие того как это делает другой ветеран Unix утилит, команда at), а так-же по другим событиям, не связанными, напрямую, со временем.

Для начала что запускаем. Возьмём, для примера, таймер man-db.timer из комплекта поставки одноимённого пакета:

$ cat /usr/lib/systemd/system/man-db.timer[Unit]Description=Daily man-db regenerationDocumentation=man:mandb(8)[Timer]OnCalendar=dailyAccuracySec=12hPersistent=true[Install]WantedBy=timers.target

Простой, коротенький таймер. Но в чём-же дело, почему не указано что мы запускаем? Всё нормально! По умолчанию, если в секции [Timer] отсутствует параметр Unit=, с указанием запускаемого юнита, systemd будет искать одноимённый *.service юнит. Проверяем!

$ cat /usr/lib/systemd/system/man-db.service[Unit]Description=Daily man-db regenerationDocumentation=man:mandb(8)ConditionACPower=true[Service]Type=oneshot# Recover from deletion, per FHS.ExecStart=+/usr/bin/install -d -o root -g root -m 0755 /var/cache/man# Expunge old catman pages which have not been read in a week.ExecStart=/usr/bin/find /var/cache/man -type f -name *.gz -atime +6 -delete# Regenerate man database.ExecStart=/usr/bin/mandb --quietUser=rootNice=19IOSchedulingClass=idleIOSchedulingPriority=7

Да, вот он сервис который ежедневно пересоздаёт базу данных страниц руководства. Сервис стартует начиная с 00:00 (OnCalendar=daily) , с точностью 12 часов (AccuracySec=12h), то-есть он может сработать в любой момент между полуночью и полднем, в зависимости от загрузки системы:

$ systemctl status man-db.timer  man-db.timer - Daily man-db regeneration     Loaded: loaded (/usr/lib/systemd/system/man-db.timer; disabled; vendor preset: disabled)     Active: active (waiting) since Thu 2020-12-31 23:18:59 MSK; 1 day 19h ago    Trigger: Sun 2021-01-03 00:00:00 MSK; 5h 30min left   Triggers:  man-db.service       Docs: man:mandb(8)дек 31 23:18:59 dell-lnx systemd[1]: Started Daily man-db regeneration.

Минимальная точность у параметра AccuracySec= 1us! Чем больше этот параметр, тем меньше нагрузка на систему. Если параметр отсутствует, то по умолчанию (указано в /etc/systemd/system.conf: DefaultTimerAccuracySec=) он равен одной минуте. Ладно, это всё лирика, давайте быстренько пробежимся по другим возможным параметрам секции [Timer], а на сладкое оставим параметры задания времени в OnCalendar= и других временнх параметрах.

Монотонные таймеры, для периодических событий

  • OnBootSec= Таймер сработает через указанное время после старта системы.

  • OnStartupSec= Для системных таймеров действие аналогично предыдущему, для пользовательских таймеров, это время после первого логина пользователя в систему.

  • OnActiveSec= Один из параметров для периодического запуска. Через какое время, после реального времени срабатывания таймера, запускать юнит.

  • OnUnitActiveSec= Триггер будет ориентироваться на время последнего запуска целевого юнита.

  • OnUnitInactiveSec= Триггер будет ориентироваться на последнее время завершения работы целевого юнита. Хорошо для долгоиграющих сервисов. Бэкапы и вот это вот всё. Все эти таймеры можно комбинировать между собой и с таймером OnCalendar=.

Прочие параметры

  • RandomizedDelaySec= Этакий рандомный джиттер. Перед срабатыванием добавляется случайный таймаут от нуля, до заданного значения. По умолчанию -- отключено.

  • OnClockChange=, OnTimezoneChange= Булевые параметры, определяющие будет-ли таймер реагировать на перевод системных часов или смену временной зоны. По умолчанию, оба параметра, false.

  • Persistent= Записывать-ли на диск состояние таймера сразу после запуска юнита. Актуально для параметра OnCalendar=. По умолчанию false.

  • WakeSystem= Ещё один логический параметр. Действует на монотонные таймеры. По умолчанию отключён. Логика следующая. При отключённом параметре все монотонные таймеры запоминают своё состояние, перед уходом системы в спящий режим и встают на паузу. После выхода системы из спящего режима, отсчёт продолжается с того момента с которого система ушла в спячку. Если-же параметр поставить в true, то таймеры продолжают работать и в спящем режиме (должно поддерживаться и железом) и по наступлении события выводят систему из спячки и запускают юнит.

  • RemainAfterElapse= Последняя крутилка, по умолчанию true Смысл этого параметра примерно следующий, После срабатывания таймера он остаётся загруженным, но если поставить false, то после срабатывания таймер выгружается и перестаёт отслеживать время. Хорошо для одноразовых юнитов (Transient Units) о которых мы поговорим в одной из следующих статей. Или для таймеров которые должны сработать один раз, как это делают задания старой, доброй at.

Таймстампы, диапазоны, тестирование, примеры

Ну и наконец зачем это всё. Чем хороши таймеры, так это тем, что всяческие сложные временные метки задаются гораздо проще чем в cron и задача запуска сервиса раз в полтора часа, начиная с часа ночи, выглядит куда приятнее. В самом простом случае она будет выглядеть так:

[Unit]Description=Test timer[Timer]OnCalendar=01:00OnActiveSec=1.5h

Ну это слишком просто. Например мы хотим что-б наш юнит запускался каждую пятницу 13-е OnCalendar=Fri *-*-13 12:00:00 Полный формат календарной формы выглядит так: Mon 2025-12-01 00:00:00.000000 Europe/Moscow Поэтому мы можем запускать таймер по времени другого часового пояса (по умолчанию текущий) Например хотим что-б таймер прислал нам уведомление, что Камчатка уже отпраздновала Новый год: OnCalendar=yearly Asia/Kamchatka Нормализованная форма будет выглядеть так(эти строчки указывают на одно и то-же время):
OnCalendar=*-01-01 00:00:00 Asia/Kamchatka Алиасы (и их эквиваленты в нормализованной форме) могут быть такими:

                       minutely  *-*-* *:*:00                         hourly  *-*-* *:00:00                          daily  *-*-* 00:00:00                        monthly  *-*-01 00:00:00                         weekly  Mon *-*-* 00:00:00                         yearly  *-01-01 00:00:00                      quarterly  *-01,04,07,10-01 00:00:00                                                                         semiannually  *-01,07-01 00:00:00

Примеры валидных таймстампов:

таймстамп с @ epoch time
        Fri 2012-11-23 11:12:13  Fri 2012-11-23 11:12:13            2012-11-23 11:12:13  Fri 2012-11-23 11:12:13        2012-11-23 11:12:13 UTC  Fri 2012-11-23 19:12:13                     2012-11-23  Fri 2012-11-23 00:00:00                       12-11-23  Fri 2012-11-23 00:00:00                       11:12:13  Fri 2012-11-23 11:12:13                          11:12  Fri 2012-11-23 11:12:00                            now  Fri 2012-11-23 18:15:22                          today  Fri 2012-11-23 00:00:00                      today UTC  Fri 2012-11-23 16:00:00                      yesterday  Fri 2012-11-22 00:00:00                       tomorrow  Fri 2012-11-24 00:00:00      tomorrow Pacific/Auckland  Thu 2012-11-23 19:00:00                       +3h30min  Fri 2012-11-23 21:45:22                            -5s  Fri 2012-11-23 18:15:17                      11min ago  Fri 2012-11-23 18:04:22                    @1395716396  Tue 2014-03-25 03:59:56

Здесь представлены таймстампы как для OnCalendar=, так и для монотонных таймеров.

Перечисления и диапазоны:

Боольшой список примеров
      Sat,Thu,Mon..Wed,Sat..Sun  Mon..Thu,Sat,Sun *-*-* 00:00:00          Mon,Sun 12-*-* 2,1:23  Mon,Sun 2012-*-* 01,02:23:00                        Wed *-1  Wed *-*-01 00:00:00               Wed..Wed,Wed *-1  Wed *-*-01 00:00:00                     Wed, 17:48  Wed *-*-* 17:48:00    Wed..Sat,Tue 12-10-15 1:2:3  Tue..Sat 2012-10-15 01:02:03                    *-*-7 0:0:0  *-*-07 00:00:00                          10-15  *-10-15 00:00:00            monday *-12-* 17:00  Mon *-12-* 17:00:00      Mon,Fri *-*-3,1,2 *:30:45  Mon,Fri *-*-01,02,03 *:30:45           12,14,13,12:20,10,30  *-*-* 12,13,14:10,20,30:00                12..14:10,20,30  *-*-* 12..14:10,20,30:00      mon,fri *-1/2-1,3 *:30:45  Mon,Fri *-01/2-01,03 *:30:45                 03-05 08:05:40  *-03-05 08:05:40                       08:05:40  *-*-* 08:05:40                          05:40  *-*-* 05:40:00         Sat,Sun 12-05 08:05:40  Sat,Sun *-12-05 08:05:40               Sat,Sun 08:05:40  Sat,Sun *-*-* 08:05:40               2003-03-05 05:40  2003-03-05 05:40:00     05:40:23.4200004/3.1700005  *-*-* 05:40:23.420000/3.170001                 2003-02..04-05  2003-02..04-05 00:00:00           2003-03-05 05:40 UTC  2003-03-05 05:40:00 UTC                     2003-03-05  2003-03-05 00:00:00                          03-05  *-03-05 00:00:00                         hourly  *-*-* *:00:00                          daily  *-*-* 00:00:00                      daily UTC  *-*-* 00:00:00 UTC                        monthly  *-*-01 00:00:00                         weekly  Mon *-*-* 00:00:00        weekly Pacific/Auckland  Mon *-*-* 00:00:00 Pacific/Auckland                         yearly  *-01-01 00:00:00                       annually  *-01-01 00:00:00                          *:2/3  *-*-* *:02/3:00

Да. Микро и наносекунды тоже поддерживаются, а ещё очень удобная функция конца месяца и счётчик:

  • *-*~01 Первый день с конца каждого месяца (он-же последний день месяца).

  • *-05~05 27-e мая каждого года (31-5).

  • Mon *-12~07/1 Последний понедельник декабря.

  • Mon *-12-01/3 Третий понедельник декабря.

Проверять таймстампы на валидность можно при помощи утилиты systemd-analyze:

$ systemd-analyze calendar 'Mon *-12-01/1'  Original form: Mon *-12-01/1              Normalized form: Mon *-12-01/1 00:00:00         Next elapse: Mon 2021-12-06 00:00:00 MSK       (in UTC): Sun 2021-12-05 21:00:00 UTC       From now: 11 months 2 days left$ systemd-analyze timespan 1.5hOriginal: 1.5h            s: 5400000000   Human: 1h 30min$ systemd-analyze timestamp 01:00:30.9999  Original form: 01:00:30.9999              Normalized form: Sat 2021-01-02 01:00:30 MSK       (in UTC): Fri 2021-01-01 22:00:30 UTC   UNIX seconds: @1609538430.999900                From now: 18h ago 

Вот так, в принципе, всё просто, логично и красиво. И разумеется напочитать:

man systemd.timerman systemd.timeman systemd-system.confman systemd-analyzeman tzselect

Список статей серии

  1. Почему хабражители предпочитают велосипеды, вместо готовых решений? Или о systemd, part 0

  2. Systemd для продолжающих. Part 1 Запуск юнитов по временным событиям

Подробнее..

АЦП преобразования в указанные моменты времени на STM32

22.02.2021 22:16:08 | Автор: admin

Доброго времени суток. В этом посте я расскажу, как мне удалось заставить STM32F407VET6 измерять аналоговые сигналы в указанные моменты времени с помощью DMA.

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

Для упрощения и ограничения задачи условимся, что

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

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

  • разница по времени между двумя последовательными измерениями может быть любой;

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

Учтем также следующие особенности STM32:

  • STM32 обладает DMA(DMA1) контроллером, который может избавить MCU от перекладывания данных из регистра ADC в RAM память по окончанию преобразования;

  • ADC преобразование может быть вызвано по некоторым событиям в том числе: TIMx_UP, TIMx_CCRy.

Таким образом каждое последующее преобразование может быть вызвано когда у таймера, который может триггерить ADC, срабатывает Capture/Compare или Update события . После окончания преобразования в работу вступает DMA1 и перекладывает измеренное значение в память MCU.

Конечно, можно на Capture/Compare событие активировать прерывание и в этом прерывании записывать новое значение в этот же CCR. Минусом этого подхода являются затраты микроконтроллера на обслуживание каждого прерывания (сохранение и восстановление стека для работы прерывания) а также сопутствующее ему отклонение времени измерения. И чем больше измерений должно быть сделано, тем больше будет отклонение. Конечно, можно учесть время входа и выход из прерывания, но это слишком сложно да и не нужно, если есть способ проще и лучше.

Обоснование использования CCR

Далее буду рассказывать про работу с Capture/Compare Register. С Update событием тоже можно сделать, принцип останется такой же, но регистры будут другие. Плюс использования CCR я вижу в том, что таймер всегда переполняется с одной и той же частотой.

С другой стороны ту же самую работу по обновлению значения в TIMx->CCRy может взять на себя ещё один DMA(DMA2), если настроить его таким образом, что каждое событие по CCRy помимо запуска ADC также бы вызывало и обновление этого CCRy с помощью DMA2. Такой подход полностью освобождает нас от использования прерываний (всю работу берут на себя DMA2, ADC и DMA1) необходим лишь только массив значений CCR, который будет предоставлен DMA2 для отправки их в TIMx->CCRy. Поэтому же и все измерения произойдут в точно указанные промежутки времени.

Получается для работы алгоритма, должны соблюдаться следующие пункты:

  • ADC должен быть настроен на преобразование по внешнему событию от CCRy, каждое последующее преобразование должно ждать соответствующего ему CCR события;

  • После окончания преобразования DMA1 должен перекладывать преобразованное значение из регистра ADC в память MCU;

  • DMA2 должен срабатывать по тому же событию от CCR что и ADC. По каждому событию DMA2 должен записывать последующее значение в TIMx->CCRy регистр из массива чисел заданного при его настройке.

  • Первый момент времени должен быть записан в CCR с помощью MCU, а не DMA.

Графически это будет выглядеть так:

Настройка периферии

Чтобы не описывать каждый пункт по отдельности, на рисунках укажу, какие параметры периферии должны быть установлены. Также скажу, что вся работа над проектом велась в STM32CubeIDE.

1. Для ADC должен быть установлен Scan Conversion Mode, выбран канал(IN1), настроен триггер и DMA1, который будет перекладывать готовые измерения в память.

2. Настройка таймера. Установку значений предделителя и AutoReload Register я опущу, т.к. они будут зависеть от вашего проекта. Для данного примера я экспериментально подобрал такие значения, которые бы позволили мне провести наглядный эксперимент, который я опишу ниже. Также для TIM3 необходимо добавить DMA, и указать его направление - из памяти в периферию, а также поставить галочку в пункте Increment Address для Memory. Прерывания для TIM3 устанавливались опционально для того, чтобы проверить, что в CCR каждый раз записываются новое значение с помощью DMA. Также в настройках конфигурации Output Compare CH1 может быть заменен на Output Compare No Output если нет необходимости отображать состояние сравнение CCR регистра с CNT на пине MCU.

Установка режима Toogle on match в TIM3 Output Compare Channel 1 позволяет отрабатывать каждое событие по CCR в ADC. Только в таком режиме со связкой в ADC : Trigger detection on both the rising and failling edges мне удалось заставить ADC запускать каждое преобразование.

3. На этом настройка периферии закончена, но для экспериментальной проверки включим DAC, который будет генерировать значения для оцифровывания, а пины микроконтроллера DAC и ADC IN1 соединим друг с другом, а также с каналом осциллографа.

С настройкой периферии разобрались, теперь необходимо написать код и провести эксперимент.

Код

Запишем нулевое значение в DAC и включим его. Далее в тестовом примере будет написана функция, которая постепенно увеличивает выходное значение DAC до достижения им потолка (4095).

HAL_DAC_SetValue(&hdac, DAC_CHANNEL_1, DAC_ALIGN_12B_R, 0u);__HAL_DAC_ENABLE(&hdac, DAC_CHANNEL_1);

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

Инициализация значений CCR
uint16_t ccValues[MEASUREMENT_COUNT];ccValues[0] = 115;ccValues[1] = 252;ccValues[2] = 388;ccValues[3] = 475;ccValues[4] = 582;ccValues[5] = 646;ccValues[6] = 727;ccValues[7] = 871;ccValues[8] = 993;ccValues[9] = 1062;ccValues[10] = 1211;ccValues[11] = 1339;ccValues[12] = 1425;ccValues[13] = 1466;ccValues[14] = 1541;ccValues[15] = 1669;ccValues[16] = 1818;ccValues[17] = 1872;ccValues[18] = 1963;ccValues[19] = 2000;

Далее необходимо положить первое значение из массива в регистр CCR1, а также на всякий случай остановим таймер и сбросим его счетчик. Опционально включаются прерывания таймера по CC1 и заморозка счетчика в режиме отладки.

htim3.Instance->CCR1 = ccValues[0];HAL_TIM_Base_Stop(&htim3);htim3.Instance->CNT = 0;__HAL_TIM_ENABLE_IT(&htim3, TIM_IT_CC1);__HAL_DBGMCU_FREEZE_TIM3();

После этого запускаем преобразования ADC с DMA. ADC теперь будет ждать события от таймера.

HAL_ADC_Start_DMA(&hadc3, measuredAdcValues, MEASUREMENT_COUNT);

И следом запускается таймер с DMA2, при этом количество транзакций должно быть на единицу меньше, т.к. первое число уже находится в регистре CCR1.

HAL_TIM_OC_Start_DMA(&htim3, TIM_CHANNEL_1, &ccValues[1], MEASUREMENT_COUNT - 1u);

Осталось запустить на железке и проверить.

Проверка на железе

Подключив два щупа осциллографа к MCU (один к пину DAC/ADC IN1, другой к выходу TIM3_CH1) Можем наблюдать следующее изображение:

Желтый - сигнал с DAC, зеленый - выход TIM3_CH1Желтый - сигнал с DAC, зеленый - выход TIM3_CH1

На осциллограмме каждое переключение зеленого сигнала является моментом измерения. Можно увидеть, что присутствует разброс по времени между измерениями.

Теперь перенесем измеренные значения ADC на график и посмотрим, что получилось:

Результат измерения, по горизонтальной оси значения CCR, по вертикальной оцифрованные значения DACРезультат измерения, по горизонтальной оси значения CCR, по вертикальной оцифрованные значения DAC

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

Я попытался повторить тот же самый трюк на STM32F103C8T6, но к сожалению мне не удалось добиться такого же эффекта. Как только происходит первое событие по сравнению с CCR, MCU тот час же пытается оцифровать сразу все запрошенное количество, не дожидаясь следующих событий. Если у кого-то получится это реализовать, я с радостью добавлю в статью.

Выводы

Используя АЦП настроенный на преобразование по запросу от таймера, DMA1, который перекладывает оцифрованные значения в память, а также DMA2 для настройки следующего момента измерения удалось реализовать механизм, который позволяет измерять сигнал в произвольно заданные моменты времени не расходуя вычислительных ресурсов микроконтроллера. Если таймер синхронизировать с таймером ШИМ, или использовать этот же таймер, который генерирует ШИМ, то можем легко измерять сигнал точно в нужный момент.

Update 1:
Ссылка на тестовый пример на github.

Подробнее..

Категории

Последние комментарии

  • Имя: Макс
    24.08.2022 | 11:28
    Я разраб в IT компании, работаю на арбитражную команду. Мы работаем с приламы и сайтами, при работе замечаются постоянные баны и лаги. Пацаны посоветовали сервис по анализу исходного кода,https://app Подробнее..
  • Имя: 9055410337
    20.08.2022 | 17:41
    поможем пишите в телеграм Подробнее..
  • Имя: sabbat
    17.08.2022 | 20:42
    Охренеть.. это просто шикарная статья, феноменально круто. Большое спасибо за разбор! Надеюсь как-нибудь с тобой связаться для обсуждений чего-либо) Подробнее..
  • Имя: Мария
    09.08.2022 | 14:44
    Добрый день. Если обладаете такой информацией, то подскажите, пожалуйста, где можно найти много-много материала по Yggdrasil и его уязвимостях для написания диплома? Благодарю. Подробнее..
© 2006-2024, personeltest.ru