Перейти к содержанию

zhevak

Members
  • Постов

    17
  • Зарегистрирован

  • Посещение

Информация

  • Город
    Берёзовский

Электроника

  • Стаж в электронике
    Более 20 лет
  • Сфера радиоэлектроники
    микроконтроллеры
  • Оборудование
    осциллограф LeCroy322, паяльная станция Lukey 701, мультиметр RichMeters 102, логический анализатор Saleae

Посетители профиля

Блок последних пользователей отключён и не показывается другим пользователям.

Достижения zhevak

Новобранец

Новобранец (2/14)

  • Месяц на форуме
  • 10 постов на форуме
  • Преданный
  • Неделя на форуме

Последние значки

6

Репутация

  1. zhevak

    Systick

    Системный таймер SysTick состоит из четырёх регистров: Все регистры 32-разрядные. Однако в регистре управления используется всего пять бит, а в регистре состояния -- так вообще один бит. Базовый адрес регистров Системного таймера 0xE000F000. Регистры располагаются относительно базового адреса со смещением, которое приведено в таблице. Таким образом, узнать текущее состояние счетчика можно обратившись по адресу 0xE000F008. uint32_t value; ... value = *((uint32_t *) 0xE000F008); // Получить текущее значение счётчика Но лучше пользоваться файлом core_riscv.h, в котором уже есть определения регистров таймера. Заметьте, что поскольку Системный таймер относится не к периферийным устройствам микроконтроллера, а принадлежит ядру, то определение его регистров находится в указанном файле, а не в файле ch32v00x.h. Кроме того, искать описание системного таймера также нужно не в толстой книжке ("CH32V003 Reference Manual"), а в тоненькой — "KingKeV2 Microprocessor Manual" (смотрите главу 3, страница 16). Входная частота у Системного таймера либо HCLK, либо HCLK/8 задаётся установкой бита STCLK в регистре CTRL. Поэтому он позволяет охватить весьма внушительный диапазон времён -- практически от одно такта ядра (да-да, это какое-то безумие!) и почти до 12 минут при максимальной тактовой частоте ядра 48 МГц. А если тактовая частота ядра будет меньше, то и временнОй интервал, формируемый чисто аппаратно, будет ещё длиннее. Куда уж ещё больше-то! И в прям какое-то безумие! Таймер считает до значения, заданного в регистре CMP. При равенстве счетчика таймера (регистр CNT) и регистра сравнения (CMP) взводится флаг CNTIF (регистр состояния SR) и, если установлен бит STIE в регистре управления (CTRL), то возникает прерывание. Кроме того, если в регистре управления CTRL установлен бит STRE, счетчик таймера сбросится и начнётся отсчет нового периода. Вот пример кода, в котором используется системный таймер для генерации односекундных промежутков времени. static volatile bool _fSystick; // Флаг сработки Системного таймера // Обработчик прерывания от системного таймера __attribute__((interrupt("WCH-Interrupt-fast"))) void Systick_Handler(void) { _fSystick = true; // Поднимаю програмный флаг SysTick->SR = 0; // Очищаю аппаратный флаг } // Настраиваю Ситемный таймер на генерацию односекундных интервалов void sysytem_initSystick(void) { _fSystick = false; NVIC_Enable(Systick_IRQn); // Включаю прерывание от Системного таймера Systick->SR = 0 ; Systick->CMP = (24000000 - 1); // Тактовая частота ядра: 24 МГц Systick->CNT = 0; Systick->CTRL = 0x0000000F; // Запускаю Системный таймер } // Определяет сработал и таймер bool sysytem_isTimeout(void) { bool tmp; __disable_irq(); { // Защищённая секция tmp = _fSystick; // делаю временную копию флага _fSystick = false; // Принудительно сбрасываю флаг } // Защищённая секция __enable_irq(); return tmp; } int main(void) { ... __enable_irq(); // Разрешаю прерывания while (true) // Главный цикл программы { ... if (system_ifTimeout()) { // Сделать что-то полезное по прошествии заданного промежутка времени ... } } } Наверно нужно пояснить, зачем нужно было писать такой странный код с использование программного флага. Вообще, если полезная работа, которая должна выполняться по сигналу таймера, небольшая и не содержит вызовов функций, то надобности в программном флаге нет никакой. Нужно тупо написать код в обработчик прерывания и более ни о чём не беспокоится. Пример такой лёгкой работы -- управление светодиодом. Если же в предстоит по таймеру отрабатывать много кода, который будет работать много времени, то в этом случае лучше не "грузить" прерывание от таймера, а вынести обработку тяжёлого кода куда-нибудь в главный регистр. Собственно, это продемонстрировано в примере кода, приведённого выше. Ещё один скользкий момент -- почему использование вызовов функций из прерывания дурной опасный код. Дело в том, что многие библиотеки (и вполне может так случится, что и код ваших функций) написаны без оглядки на использование их во многопоточных программах. Если это так, то программа неминуемо потерпит крах. Вы можете удивится -- откуда в вашей программе несколько потоков? Это элементарно! Если вы используете прерывания, то как минимум их будет два или больше. Один поток исполнения -- это главный поток, это главный цикл в вашей программе. Второй поток -- это любое прерывание. В случае примера, приведённого выше -- прерывание от системного таймера. А теперь представьте, что в главном цикле программы совершается какая-то обработка данных какой-то, допустим проверенной-перепроверенной библиотечной функцией, которая не рассчитана на многопоточность. В какой-то момент времени, когда выполняется эта функция, возникает прерывание. Внутренне состояние функции остаётся, как говорят, в неконсистентном состоянии. Ладно, если из прерывания такая функция не будет вызываться, то ничего страшного не произойдёт. А сели вы через какое-то время решите доработать свой проект, позабыв, что есть нюансы, и в каком-либо прерывании вызовите свою безобидную функцию, в которой (ах, склероз!) затесалась та самая библиотечная функция... Что произойдёт? А произойдёт следующее. Обработчик прерывания отработает без вопросов. Опасная функция будет вызвана и она тоже отработает без вопросов -- сделает всё, как положено. Но возвращаясь из прерывания нам нужно будет продолжить выполнение прерванной функции. По факту окажется, что её данные испорчены вызовом её же копии из прерывания. Функция потерпит крах. Конечно, если вы чётко понимаете, что делает ваша программа в каждый момент времени, то большой беды нет. Опасность есть, но она купирована вашим пониманием. Поэтому, если нет опыта в написании крутых программ, то лучше всю обработку данных совершать в главном цикле, а извещать его о произошедших в системе тех или иных событиях, лучше примерно таким способом, который приведён в примере.
  2. zhevak

    Режим одиночного импульса

    Я не знаю. Я не знаток этой экосистемы. Я, конечно, иногда использую платы Arduino в своих проектах, но использую только как аппаратную часть, которая имеет уже готовые к применению компоненты -- источник питания, USB-UART - интерфейс для связи с компом, ну и сам процессор с кварцем. Это всё, что мне обычно надо от Ардуино. Я не пользую ни среду разработки, ни тем более их библиотеки. И поэтому, что они там делают, я понятия не имею.
  3. zhevak

    Среда разработки MounRiver Studio

    Интегрированная среда разработки MountRiver Studio предназначена для работы с проектами на базе микроконтроллеров CH32V. Среда выпускается для трёх основных платформ — для Виндовс, для Линукса и для Макоси. Причём, версии для Линукса выходят с небольшой задержкой и, как мне видится, выходят за авторством не фирмы, а некоторого сообщества. Это я так думаю, так как в версии для Винды среда называется MountRiver Studio, а в версии для Линукса — MountRiver Studio Community. Я не особо копался, но на первый взгляд эти версии не особо друг от друга отличаются. Я поигрался и с той, и с другой. Обе работают, по их работе вопросов не возникает. Где брать эту среду? Опять там же, откуда забирали тулчейн — с сайта mounriver.com, вот, ссылка: http://www.mounriver.com/download Только на это раз нужно тыкать мышкой здесь (, а не там, где был курсор мышки на картинке в предыдущей статье). Вот, снимок экрана для ориентации: Получив сжатый архив его нужно развернуть. Как это сделать, написано в предыдущей статье про тулчейн. Всё нужно делать примерно так же. Сложного ничего нет. После раскрытия архива нужно зайти в директорий MounRiver_Studio_Community_Linux_x64_V130/beforeinstall и запустить на выполнение скрипт start.sh. После этого можно запускать среду разработки. Исполняемый файл находится в директории MounRiver_Studio_Community_Linux_x64_V130/MRS_Community и называется MounRiver Studio_Community. Вот, его и нужно запустить. Конечно, каждый раз ползать по директориям в поисках программы, которую нужно стартануть, это как-то не по-Виндовому. По-виндовому — это когда все операции выполняешь мышкой. Поэтому для любителей Винды и тех, кто ещё не освоился в приёмах работы в Линуксе, объясняю как сделать себе приятно. Идём в директорий .local/share/applications и там создаём файл mrs.desktop: , вот, с таким содержимым: На всякий случай дам текстовое представление этого файла чтобы желающие могли закопипастить его прямо отсюда, а не набивать текст руками. [Desktop Entry] Type=Application Name=MounRiver Studio Community Version=1.50 Encoding=UTF-8 Comment=Среда разработки для CH32V Exec=/home/alex/bin/MounRiver_Studio_Community_Linux_x64_V150/MRS_Community/MounRiver\ Studio_Community %u Icon=/home/alex/bin/MounRiver_Studio_Community_Linux_x64_V150/MRS_Community/icon.xpm MimeType=x-scheme-handler/eclipse+command;x-scheme-handler/eclipse+mpc; Categories=Development;Application; Terminal=false Не ошибитесь при создании файла! Я развернул среду разработки в директории /home/alex/bin/, а у вас будет скорее всего что-то своё. Кроме того, обратите внимание на экранирующий символ обратной косой черты («\») и пробел за ним, которые находятся в средине названия программы: MounRiver\ Studio_Community В названии программы присутствует пробел, поэтому, что бы название воспринималось правильно (а не как два независимых названия) нужно этот пробел учесть. Существует два способа. Первый — это перед пробелом «воткнуть» экранирующий символ. Второй — всю строку после знака равенства взять в кавычки или в апострофы. После того, как в директории появится этот файл, в главном меню должен отобразиться пункт «Программирование», а в нём подпункт «MounRiver Studio_Community» Ну, вот, теперь можно топать по этому пункту меню и долго-долго ждать, пока среда разработки (созданная созданная, к стати, на базе Eclipse, если кто заметил это по значку в главном меню) загрузится. Скажу честно, я не часто пользуюсь средами разработки. Эту среду разработки я установил только для того чтобы «выудить» из неё файлы с исходными текстами, необходимые для создания проекта. Там есть одна заветная папочка, а в ней — вкусняшки! Поскольку я в данный момент работаю с CH32V003F4P6, то мне нужен соответствующий файл. В этом сжатом архиве содержатся исходные файлы, необходимые для создания проектов. Нужно их извлечь из архива с помощью команды $ unzip CH32V003F4P6.zip Но прежде чем это сделать, этот файл лучше перенести куда-нибудь в отдельный директорий, иначе извлечённые из него поддиректории будут помещены рядом с этим и другими файлами. Просто потом запутаетесь. Честно говоря, код в этих исходниках не очень качественный. Эти файлы нужны нам как основа для проектов и как база для понимания некоторых вещей. На базе этих файлов мы будем создавать свои файлы и потом подключать их в свои проекты. Мне также не нравится сама суть создания программного обеспечения с помощью этих файлов, точнее способ создания проектов, в которых участвуют эти файлы. Ну, я уже в каких-то статьях приводил цифры создания «голых» проектов. Я это делал с помощью Makefile, пролистайте немного назад блог, если это вам интересно. Там были цифры примерно 400 и 600 байт за пустую программу, которая ничего не делает. Если делать такую же пустую программу в среде разработки, делать «по-правильному», то получается программа, объём которой примерно 6-7 килобайт. То есть примерно раз в 10 больше. Это не удивительно. В программу подключаются многие библиотеки со многими функциями и структурами данных, которые могут и не участвовать в проекте, и которые довольно-таки сложно откинуть из проекта. Конечно, менеджеры разных мастей вам объяснят, что это как бы «ядро» вашей программы, и дальнейший рост объёма кода будет значительно меньший. Ну, то есть, если вы дописали немного своего кода, то размер программы увеличится на пару килобайт. Не существенно. Тем более, что до исчерпания размера флеш-памяти ещё далеко. Тот же функционал, реализованный, без этого «ядра» и без (советского) сервиса, навязанного в нагрузку, добавит к имеющимся 400-600 байтам примерно 1-1.5 килобайта. То есть даже меньше, чем в первом случае. Таким образом, общий объём кода составит примерно 2 килобайта вместо 10 килобайт. На первый взгляд экономия странная, и мне сложно объяснить, чем этот подход лучше. Но есть единственный аргумент — если вы не плохо разбираетесь в программировании, то посмотрите на предоставленные исходники. Это ж ужас какой-то! Честно говоря, я не хочу их напрямую использовать в своих (даже не коммерческих) проектах. Ну его нахрен! Тот код в исходниках, который я просмотрел, он вполне рабочий. Но, блин, это что-то! Примеры? Их есть у меня! В количествах. Возьмём для примера файл system_ch32v00x.c. Здесь на фрагменте осуществляется задание источника и и частоты тактирования. Почему бы две конструкции — строки 26-40 и 48-60 — не объединить в одну? А вот это что такое: А потом — вот ещё: и ещё примерно так же пять раз! Чё за винегрет такой из кода сотворили и тщательно перемешали? А самое главное — зачем? Можно же гораздо проще и самое главное — понятнее было написать. Ладно идем дальше. На этом же снимке экрана, обратите внимание на строку 175. А-а? Каково? Хотите ещё «индусского кода?» — Да, пожалуйста! Строка 254 — что это? Откуда взялось число 15? Понятно, что это 15- бит. Но что это? Разбираясь в этих исходниках я нашел, что это есть константное значение AFIO_PCFR1_PA12_REMAP, которое определено в файле ch32v00x.h. Точно так же вместо числа 0x04 в строке 290 следует подставить имя RCC_SWS_HSE. Авторы же используют в программе такие имена как RCC_SW, FLASH_ACTLR_LATENCY_0 и другие. Что мешало подставить вместо чисел имена? Не понятно. Хорошо. Идём дальше. Строка 268 — что такое RESET? Занимаюсь поиском этого имени и нахожу, что это есть, вот, что: Ага. Теперь понятно. Это есть флаг состояния. Но, блин, какое отношение этот флаг имеет к значению замаскированных битов регистра? Ну, хорошо. Допустим, авторы считают, что имеет. Поэтому код именно такой. А что делать со строками 268-275? Почему бы сразу не присваивать переменной HSEStatus значение этого флага и не изгаляться с числами 0x00 и 0x01, которые ещё и сдобрили преобразованием типов. Ладно. Проехали! А что делать с блоком 268-293? На ккккой хрен это нужно было выделять в виде отдельного блока? Почему нельзя было сразу внести этот код в строки 269-271 ? И как вишенка на торте — строки 296-299! Типа внешний тактовый генератор навернулся, но мы не знаем что делать. Юзверь может, сам добавить сюда свой код обработки ошибки. Если, конечно, юзверь сюда залезет и увидит это говнокод. Ну, молодцы. Чо! И такого дерьма по всем файлам наложено. Оно, конечно, как-то работает. Но ну его нафик. Нафик! Поэтому, если вы не (прости-госпади!) ардуинщики, то однозначно нужно пилить свой код. Чем мы и займёмся далее. Точнее — я займусь. Но буду ли я выкладывать свои записи в общий доступ, это зависит от того — нужны ли они кому-то. Пока вижу, что не нужны. А, ну и ладно! Статью можно обсудить в Телеграм-группе https://t.me/ch32v
  4. zhevak

    Распиновка

    Спасибо за сигнал! По ходу, китайцы перенесли документ в другое место. А не, ни фига! Это моя оплошность -- не удосужился обратить внимание, что точка в конце предложения со ссылкой вошла в состав ссылки. Сейчас поправлю. Доставляет перевод на русский с этих китайских кракозябр -- "404: Текст вашего запроса не совпадает с текстом вашего запроса". И понимай как хочешь!
  5. zhevak

    Распиновка

    По началу, когда я только-только начал осваивать эти бестии, я ничего толком не понял — где какая нога у них. Таблица назначения выводов Table 2.1 на странице 11 , приведённая в описании по CH32V0003, не столько проясняет ситуацию, сколько ещё больше запутывает. По началу, когда не понимаешь, что они там вообще хотели сказать, это какой-то трэш! В самом конце этой статьи я раскрыл "секрет" производителя. К стати, для тех, кто , как и я , пользуется давно этим описанием, — второй половине мая вышла подновлённая версия https://www.wch-ic.com/downloads/CH32V003DS0_PDF.html. Она, к слову сказать, табличку не изменила — как были вопросы, так они там и остались. В общем, проблема неясности в распиновке существует давно. У меня чуть ли не сразу возникло желание разрисовать распиновку и выложить в общий доступ. Но как это обычно бывает, с одной стороны не было полной уверенности, а с другой — матушка-лень. Я даже умудрился в поисках правды одну из микросхем «захлопнуть» (об этом я писал где-то ранее). А вот то, что выводы можно подписать частично (а не полностью расписать все функции, которые может обеспечит та или иная ножка), я как-то не допёр. Это сделал некий товарищ mockthebear из далёкой Бразилии (Sao Paulo), за что, собственно, ему большое Российское СПАСИБО за подсказку! (Какой же я тормоз!) На рисунках ниже приведена цоколёвка микроконтроллеров в корпусах TSSOP20 и TSOP8 (SOP8). К сожалению, товарищ mockthebear эпически ошибся при указании выводов питания и общего провода у корпуса TSSOP20, и мне не удалось с ним связаться, чтобы указать ему на его промах. Я взял за основу его рисунки, героически (а то как же ещё!) исправил его ошибку и немного добавил своих описания функций на ножках. В общем, на рисунках ошибок нет. Если что, то пинайте меня. Программатор WCH-LinkE следует подключать к выводу 18 (порт PD1) у микросхемы в корпусе TSSOP20. А у микросхемы в корпусе SOP8 для этого дела выделена нога 8 (порт PD4). Вывод на программаторе обозначен как SWDIO/TMS. Ну, питание и землю — само собой, разберётесь сами. В своё время, когда я только начинал осваивать CH32V003, мне сильно не хватало понимания чего куда подключать. В поисках правильного решения я перебирал разные варианты и фиксировал их (как временное решение) на листочке. Потом всё как-то устаканилось. А вот листочек у меня так и сохранился в виде черновика. Для меня он своё назначение давно уже отработал. Публикую его содержимое только с целью, что, возможно, кому-то это может и пригодится. Теперь пару слов по таблицам, приведённым в описании на микросхемы. Начну с того, что кремниевый чип один и тот же для всех корпусов! Никто не будет делать разные чипы под разные корпуса, потому как это крайне дорого. Поскольку чип одинаковый для всех корпусов, а ног у корпусов -- разное количество, то нужно из ситуации как-то выходить. Самое простое решение -- не разваривать (не соединять тонкими золотыми проволочками) выводы чипа и ножки корпуса. Но есть и другая практика! Поскольку незапрограммированные ноги (выводы портов) как правило после сброса находятся в 3-ем состоянии, то никто не мешает, допустим, два вывода на чипе разварить на одну ножку корпуса. Например, у корпуса SOP8 ножек совсем минимум, а богатый функционал периферийных устройств не хотелось бы потерять. Поэтому на ножку 1 предприимчивые китайцы приварили сразу два вывода порта -- PD6 и PA1. А вот на ножку 8 Приварено сразу аж три порта -- PD1, PD4 и PD5. Поскольку порты ввода-вывода (GPIO) мультиплексируются с выводами периферийных устройств, то получается, что на "совмещённых" выводах корпуса также можно организовать ту или иную функцию периферийного устройства. Иначе говоря, богатая внутренняя периферия сохраняется. Не вся, но большая часть -- да. Другой вопрос -- опасное ли это дело? Ну как сказать... Объединённые ножки -- это всего лишь способ, инструмент. Такой же инструмент, как острый скальпель. Опасен ли скальпель? Это зависит в чьих руках он окажется. Если в руках ребёнка, то -- да, опасен. Если в руках хирурга, то -- нет. Таким образом, если вы не будете конфигурировать порты или выводы периферийных устройств, которые разварены на одну и ту же ногу, оба на выход, то ничего страшного не произойдёт. И даже наоборот! Если вы понимаете, как это работает, то вы можете один порт или периферийное устройство сконфигурировать на выход, а другое -- на вход. Я, правда, не очень понимаю, где такое решение может использоваться (или пригодится), но теоретически такое решение возможно, поскольку ничего не "сломает" в микроконтроллере.
  6. zhevak

    Режим одиночного импульса

    Продолжаю публикацию материалов на тему работы с таймером. У таймеров, которые встроены в CH32V003, имеется один полезный режим -- режим одиночного импульса (OPM, One Pulse Mode). Этот режим работы характеризуется тем, что таймер создаёт одиночный кусочек времени, а не генерирует периодический сигнал. Иначе говоря, мы стартуем таймер, он один раз отрабатывает заданное время и на этом всё заканчивается. Чтобы повторить процедуру, нужно ещё раз стартануть таймер. В задаче, код которой приведён ниже, таймер отрабатывает промежуток времени, равный 500 мс. По окончании этого промежутка таймер поднимает флаг UIF и возбуждает прерывание. Кроме этого, у таймера, как по аналогии с предыдущей статьёй, также задействованы два канала захвата/сравнения -- 3-й и 4-й. Они также отрабатывают промежутки времени 75 и 125 мс. На этот раз управление светодиодами (которые используются для диагностики и отладки) несколько изменилось. Добавился третий светодиод, который индицирует отрабатываемый таймером промежуток времени (500 мс). Код обработчика прерывания выглядит следующим образом: __attribute__((interrupt("WCH-Interrupt-fast"))) void TIM2_IRQHandler(void) { // Отрабатываю период if ((TIM2->DMAINTENR & TIM_UIE) && (TIM2->INTFR & TIM_UIF)) { TIM2->INTFR &= ~TIM_UIF; // Очищаю флаг UIF led0_off(); // Гашу светодиод (500 мс) ... // Выполняю другую полезную работу, связанную с окончанием полного периода } // Отрабатываю короткий промежуток врмени if ((TIM2->DMAINTENR & TIM_CC3IE) && (TIM2->INTFR & TIM_CC3IF)) { TIM2->INTFR &= ~TIM_CC3IF; // Очищаю флаг CC3IF led2_off(); // Гашу светодиод (75 мс) ... // Выполняю другую полезную работу, связанную с окончанием короткого промежутка времени } // Отрабатываю длинный промежуток времени if ((TIM2->DMAINTENR & TIM_CC4IE) && (TIM2->INTFR & TIM_CC4IF)) { TIM2->INTFR &= ~TIM_CC4IF; // Очищаю флаг CC4IF led1_off(); // Гашу светодиод (125 мс) ... // Выполняю другую полезную работу, связанную с окончанием длинного промежутка времени } } Светодиоды включаются в функции запуска таймера: void tim2_start(void) { TIM2->CTLR1 |= TIM_CEN; // Запускаю формирование одиночного импульса // Включаю все светодиоды led0_on(); led1_on(); led2_on(); } Функция инициализации таймера (по сравнению с примером из предыдущей статьи) тоже изменилась. void tim2_init(void) { RCC->APB1PCENR |= RCC_TIM2EN; // Включаю тактирование модуля таймера TIM2->CTLR1 = TIM_OPM; // Режим одиночного импульса TIM2->PSC = 24000 - 1; // Таймер будет считать со скоростью 1 кГц TIM2->ATRLR = 500; // Период 500 мс TIM2->CH3CVR = 25; // Короткий импульс 25 мс TIM2->CH4CVR = 125; // Длинный импульс 125 мс TIM2->INTFR = 0; // Сбрасываю все флаги // Разрешаю прерывания от 3-го и 4-го каналов, а также по окончанию счёта таймера TIM2->DMAINTENR |= (TIM_CC4IE | TIM_CC3IE | TIM_UIE); NVIC_EnableIRQ(TIM2_IRQn); // Разрешаю в системе прерывание от таймера TIM2 } Как видите, режим одиночного импульса задаётся с помощью установки флага OPM в управляющем регистре CTLR1. Теперь функция main() должна выглядеть как-то так: int main(void) { uint32_t flag = 0; system_init(); leds_init(); tim2_init(); // Настраиваю таймер TIM2 на генерацию одиночных импульсов __enable_irq(); while (true) { if (system_1s() == 1) { // Сюда мы попадаем один раз в секунду if (flag == 1) { flag = 0; led4_off(); } else { flag = 1; led4_on(); tim2_start(); // Запускаю таймер } } } } В начале функции main() происходит инициализация таймера на работу в режиме одиночных импульсов. В главном цикле программы периодически вызывается функция system_1s(), которая возвращает признак истечения односекундного интервала времени. Эта функция в этой статье не описана. Секундные интервалы времени для этой функции вырабатываются системным таймером SysTick. Согласно этим интервалам меняется значение переменной flag и через каждые две секунду происходит запуск таймера TIM2. Вообще это не принципиально по какому критерию запускать таймер TIM2. Можно, например, по нажатию кнопки, а интервалы формировать с целью подавления дребезга.
  7. zhevak

    Формирование интервалов времени

    Это короткая заметка про то, как с помощью аппаратного таймера, который имеется в микроконтроллере CH32V003, сформировать пару сигналов. Поставленная задача чем-то напоминает задачу ШИМ (широтно-импульсной модуляции). Но в отличие от классической ШИМ-задачи, в описываемой здесь задаче не используется модуляция (то есть ширина импульса не меняется) и не предполагается использование (аппаратного) сигнала, который обычно выводится на ножки МК и который подаётся на управляющие выводы силовых транзисторов, например, в блоке питания. Хотя, на самом деле в решении поставленной задачи, используются выводы портов и на них действительно выводится сигнал, но это не является целью программного кода, а сделано лишь для индикации того, что программный код работает правильно -- как задумано. Вот временная диаграмма того, что нужно получить в программе: Здесь LED1 и LED2 -- это состояние светодиодов, которые подключены на ножки МК. Ещё раз: светодиоды нужны -- это только для отладки, только для индикации правильности процессов. Мне нужно сфомировать два промежутка времени. Один с короткой продолжительностью (75 мс), другой -- с длительной продолжительностью (125 мс). Оба промежутка времени должны начинаться в одно и тоже время. Период повторения промежутков времени -- 500 мс. Я буду использовать таймер TIM2. Период повторения будет формироваться счётчиком таймера CNT, который будет тактироваться входным сигналом с частотой 1 кГц. Входную частоту счетчика я получу из тактовой частоты шины AHB1, на которой "висит" этот счётчик. Тактовая частота ядра (SysClock) в проекте 24 МГц. Делитель частоты для шины AHB1 равен единице, то есть тактовая частота таймера -- тоже 24 МГц. У счётчика таймера имеется предделитель. Чтобы из входной частоты таймера (24 МГц) получить входную частоту для счётчика таймера (1 кГц) необходимо в предделитель загрузить значение, равное (24000 - 1). В результате, если запустить таймер, его счётчик начнет считать со скоростью 1 кГц. Чтобы сформировать период, равный 500 мс, необходимо в регистр автозагрузки записать число 500. Счетчик увеличивает своё значение каждую миллисекунду (частота 1 кГц), поэтому ему нужно досчитать до 500. После этого счётчик сбросится в ноль, поднимет в регистре INTFR флаг прерывания UIF и начнёт следующий период. Для формирования заданных промежутков времени я использую регистры захвата/сравнения CH3CVR и CH4CVR. В регистр CH3CVR я записываю число 75, а в регистр CH4CVR -- число 125. Поскольку счётчик инкрементируется со скоростью один раз в миллисекунду, то третьего канала (CH3) возникнет прерывание на 75 миллисекунде, а от четвёртого канала (CH4) -- на 125 миллисекунде. Вот код инициализации таймера TIM2: void init_tim2(void) { RCC->APB1PCENR |= RCC_TIM2EN; // Включаю тактирование модуля таймера NVIC_EnableIRQ(TIM2_IRQn); // Разрешаю прерывания от таймера TIM2 TIM2->PSC = 24000 - 1; // Таймер будет считать со скоростью 1 кГц TIM2->ATRLR = 500; // Период 500 мс TIM2->CH3CVR = 25; // Короткий импульс 25 мс TIM2->CH4CVR = 125; // Длинный импульс 125 мс TIM2->INTFR = 0; // На всякий случай сбрасываю все флаги // Разрешаю прерывания от 3-го и 4-го каналов, а также прерывание от счётчика TIM2->DMAINTENR |= (TIM_CC4IE | TIM_CC3IE | TIM_UIE); TIM2->CTLR1 = TIM_CEN; // И наконец запускаю таймер в работу } Формирование промежутков осуществляется в обработчике прерываний от таймера. Обработчик прерывания от таймера TIM2 может обслуживать несколько прерываний. В моём случае это три прерывания -- одно от счётчика и два от каналов захвата/сравнения. Вот как это делается: __attribute__((interrupt("WCH-Interrupt-fast"))) void TIM2_IRQHandler(void) { // Прерывание от счётчика. Отрабатываю период. if ((TIM2->DMAINTENR & TIM_UIE) && (TIM2->INTFR & TIM_UIF)) { TIM2->INTFR &= ~TIM_UIF; // Снимаю флаг прерывания // Начало периода. Включаю оба светодиода. led1_on(); // Светодиоды нужны только для отладки кода программы led2_on(); ... // Выполняю некоторую полезную работу } // Прерывание от канала CH3. Отрабатываю короткий промежуток времени. if ((TIM2->DMAINTENR & TIM_CC3IE) && (TIM2->INTFR & TIM_CC3IF)) { TIM2->INTFR &= ~TIM_CC3IF; // Снимаю флаг прерывания led1_off(); // Выключаю светодиод LED1 ... // Выполняю некоторую полезную работу } // Прерывание от канала CH4. Отрабатываю длинный промежуток времени. if ((TIM2->DMAINTENR & TIM_CC4IE) && (TIM2->INTFR & TIM_CC4IF)) { TIM2->INTFR &= ~TIM_CC4IF; // Снимаю флаг прерывания led2_off(); // Выключаю светодиод LED2 ... // Выполняю некоторую полезную работу } } В начале периода обработчик прерывания зажигает оба светодиода. Каждый светодиод гасится своим каналом согласно заданному промежутку времени. Не пытайтесь понять назначение этой задачи и не пытайтесь оптимизировать её решение. Задача несколько синтезированная. Реальная задача, из которой получилась эта статья, несколько иная. Если бы я описывал реальную задачу, то потерялась бы суть и простота описания работы с таймером. Поэтому я упростил задачу на столько, что она почти выродилась. Мне было важно показать, что работать с таймером не так уж тяжело. В реальной задаче мне нужны времена не в миллисекундах, а в микросекундах. Но это сути не меняет. Да, я понимаю, что время входа в прерывание может составлять около микросекунды. Но в реальной задаче допускается джиттер до примерно 5 мкс. И, как я уже говорил, в реальной задаче не используется вывод сигналов на порты. Реальная задача работает не с внешними сигналами, а со значениями внутренних переменных. На всякий случай сообщу, что на Telegram имеется канал по микроконтроллерам CH32V t.me/ch32v003 и есть группа https://t.me/ch32v, в которой можно пообщаться с коллегами.
  8. zhevak

    Как затактировать систему

    То решение, какое предлагает фирменный фреймфорк (или как это китайцы называют у себя на фирме) мне, честно говоря, вообще не нравится. Я уже писал об этом. Там, в их коде полно ошибок, а, кроме того, компилятор генерирует много ненужного кода. Я вообще не понимаю, зачем нужно использовать структуру GPIO_InitStructure разработчику, который, знает состав портов ввода-вывода и как они работают. Вот, пример, кода который, я взял из фреймворка: void GPIO_Toggle_INIT(void) { GPIO_InitTypeDef GPIO_InitStructure = {0}; RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOD, ENABLE); GPIO_InitStructure.GPIO_Pin = GPIO_Pin_0; GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP; GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz; GPIO_Init(GPIOD, &GPIO_InitStructure); } Возможно кому-то это и нравится, но я считаю, что это какой-то ужас. Вместо того, чтобы одной командой включить тактирование порта, а другой сконфигурировать все известные на данный момент биты, — создаётся структура. Затем инициализируются её поля. Потом вызывается функция GPIO_Init(). Вы заглядывали во внутрь, что делает функция GPIO_Init()? — Нет?!! Не поленитесь, загляните! Получите массу удовольствия. Или инфаркт. И как изощрённое издевательство — не поленитесь просмотреть код настройки тактирования, который находится в файлах system_ch32v00x.c и system_ch32v00x.h. Я думаю, после этого вы поймёте моё негодование. Понятно, что весь этот «сервис» создаёт огромную кучу бессмысленного и беспощадного кода. Вы спросите — а для чего это всё так сделано? Точного ответа я не знаю, но предполагаю, что в основе лежит вполне благородная цель — типа чтобы программист не изучал регистры, а пользовался нашим замечательным фреймворком. Скорее всего предполагается, что такое решение сэкономит время разработки программы, а значит -- не сильно увеличит цену изделия. Ну, не знаю... сэкономит ли. А с другой стороны. Ну, хорошо. Теперь программисту не надо изучать регистры и назначение битов. А как насчёт того, что вместо этого программисту придётся изучать сам фреймворк? Нет ли в этом подходе того, что вместо изучения одной (базовой) сущности, нужно изучать другую (производную) сущность? Ладно, если производная сущность окажется проще. А если нет? А если, тем паче, — что-то пойдёт не так? Тогда программисту придётся изучать "обе две" сущности — и фреймворк, и регистры. Вот, счастье-то! Короче, хотите быть ардуинщиком — будьте им! Никто вам не мешает! А кто уважительно относится к технике, того прошу за мной! Итак, ниже я провожу код инициализации тактирования от встроенного генератора. В этом коде я не использую PLL, поэтому ядро микроконтроллера будет работать на частоте 24 МГц. Поскольку частота не высокая, то флеш-памяти не потребуется дополнительный такт для работы. void system_init(void) { RCC->CTLR |= RCC_HSION; // Включаю HSI (24 МГц) RCC->CFGR0 &= 0x00000000; // Отключаю MCO, пределители в исходное сотояние, HSI RCC->CTLR &= ~(RCC_PLLON | RCC_CSSON | RCC_HSEON); // Выключаю PLL и HSE RCC->CTLR &= ~RCC_HSEBYP; // Выключаю байпас RCC->INTR = (RCC_CSSC | RCC_PLLRDYC | RCC_HSERDYC | RCC_HSIRDYC | RCC_LSIRDYC); // Сбрасываю флаги FLASH->ACTLR &= ~FLASH_ACTLR_LATENCY; FLASH->ACTLR |= FLASH_ACTLR_LATENCY_0; // Цикл обращения к флеш-памяти RCC->CFGR0 |= RCC_HPRE_DIV1; // HCLK = SYSCLK = 24 MHz } Вот такой рабочий код размером в полтора десятка строк. Если мне понадобится, к примеру, затактировать систему от внешнего кварцевого резонатора или увеличить частоту в два раза, я тупо сделаю копию этого кода и изменю нужные мне параметры, а исходный код (чтобы не потерялся) просто закомментирую. И не надо говнокодить никаких заумныйх решений на все случаи жизни. Ни к чему это. Потом, спустя какое-то время, я могу снова вернуться к этим исходным кодам и выбрать то, что мне будет нужно в следующем проекте. Разумеется, рядом с кодом нужно разместить комментарии. Во первых, это красиво. (с) А во вторых, код таким образом получается наглядный, понятный, осязаемый. В отличие от говнокода из фреймворка. Ведь при таком подходе будет сразу понятно, что этот код делает. Мне остаётся только добавить, что функция system_init() вызывается из ассемблерного файла startup_ch32v00x.S. Вот, несколько последних строк этого файла: ... la t0, _start ori t0, t0, 3 csrw mtvec, t0 jal system_init la t0, main csrw mepc, t0 mret Да, это ассемблер RISC-V. Это не важно, что вы сейчас ничего не понимаете, что тут происходит. Я как-нибудь потом вернусь к этому файлу. Сейчас важно чтобы вы увидели команду вызова функции system_init() и команду вызова функции main(). Заметьте, что сначала вызывается функция system_init(). А функция main() начинает работать, когда оборудование уже настроено.
  9. zhevak

    Минимальный проект

    Здесь я описываю минимальный проект на базе микроконтроллера CH32V003, с которого можно легко стартовать и развивать свои проекты. Проект не идеальный, но вполне рабочий. Основная фишка проекта заключается в том, что он минимальный. То есть вполне осязаемый, в него не надо долго «залазить» и изучать. Дополнительным действием к проекту является установка самого тулчейна из файла MRS_Toolchain_Linux_X64_V170.tar.xz. Как это сделать, я уже описал где-то в предыдущих статьях. На своём компе я установил тулчейн в поддиректорий bin в домашнем директории. Ну, вот так, наверное, будет понятнее — /home/alex/bin/MRS_Toolchain_Linux_x64_V1.70. Это не особенно важно — куда вы его установите. Но это принципиально для указания пути (PATH) к нему в Makefile. Иначе говоря, если вы установите тулченйн в какое-то иное место, то вам нужно будет подкорректировать Makefile. Ещё раз подчеркну, что я не считаю, что проект, который я здесь привожу в качестве примера, является идеальным решением. Но он — рабочий. Берите его за основу и доводите до своих требований. Проект состоит из нескольких (системных) файлов (которые по сути не желательно изменять) и одного поддиректория с исходными файлами. «Не желательно» — это не означает, что нельзя ни в коем случае. Если понимаете, что делаете, то делайте! "Поддиректорий с исходниками" — это поддиректорий исключительно только с ваши файлами. Причём, я определил в этот поддиректорий как *.с, так и *.h файлы. Я считаю, что когда проекты не очень большие, то создавать два поддиректория (один для исходников, другой — для заголовочных файлов) не имеет смысла. Структура получается развесистая и «скакать» по этим поддиректориям будет не очень удобно. А когда файлов немного (в пределах 2-3 десятков и меньше), то их лучше держать в одном месте. К стати сказать, в русскоязычном интернете почему-то неправильно говорят -- "хидерные" файлы, а не "хэдерные". Возможно, это происходит из-за того, что люди не учили английский язык в школе или учили, но абы как. Английское слово "head" ("голова"), ведь наверняка все знают. Более того, я подохреваю, что и произносят его правильно -- "хэд". А, вот, сделать логическую связку -- на это кое-кому ума уже, наверно, не хватает. Ладно! Это так, между делом. Вот тут репозиторий проекта — https://github.com/zhevak/start-ch32v. Я посчитал, что удалить ненужный код проще, чем дописать новый, и добавил в проект моргание светодиодиками. Проект стал чуточку больше, но зато стал «живым», а не абстрактно-мёртвым. Таким образом, после компиляции, сборки и заливки пользователь сразу увидит результат того, что технологии, предложенные в проекте, работают.
  10. zhevak

    CH32V003

    (Имеется ввиду -- чем программировать CH32V.) Я через USART заливал прошивку наверно раза два. И делал я это в Линуксе из RivaStudio (или как она там у них правильно называется). Из консоли заливать через USART не пробовал, поскольку как-то сразу перешёл на работы с МК через SWIO. Опятя же в Линуксе. Ну, то есть, в Линуксе работаь с МК принципиально возможно. Я ж в Линуксе работаю. Другое дело, что я не получил достаточно опыта заливки бинарника через USART, чтобы на эту тему что-то умное сказать. Пусть это сделают те, кто лучше меня разбирается в вопросе. Да. Это пока так. Но со временем, я в этом уверен, появится народный CHLink. Свято место пусто не бывает. Если фирма WCH сохранит низкие цены на свои камни, то они неизбежно войдут в арсенал разработчиков со всеми вытекающими последствиями. Про "фирменный" (от фирмы WCH) OpenOCD я ещё просто не успел здесь разместить материал. Я только-только начал перетаскивать на cxem.net свои статьи с wordpress. Это только первые две с половиной статьи, которые я по незнанию специфики cxem.net выполнил как одну. Со временем я постараюсь сюда перетащить и другие свои статьи. Документации конкретно по CV32V. Спасает положение то, что периферия совпадает с периферией STM32F. Китайская документация по CH32V -- не полная (в сравнении с составом документации на STM32), написана таким корявым языком, что "прям, ой, мама!". Ну и до кучи там полно чисто технических ошибок. Я об этом уже сказал в своей статье.
  11. zhevak

    CH32V003

    Чем программировать CH32V Смешной вопрос! Чем вообще программируются STM32? У кого достаточно финансовых возможностей, тот покупает дорогие фирменные программаторы. Я, как и многие другие разработчики, использую китайские «свистки» по 150 рублей. (Это они раньше столько стоили. Сколько стоят сейчас — я не знаю. Уже давно не покупал. Для работы с STM32 я всё ещё пользуюсь свистком, купленным лет пять назад.) Беда, однако, в том, что ST-Link не подходит для работы с CH32V. Ну, хорошо. А чем же тогда программировать эти китайские штучки? Чем-чем — дак, почти таким же китайским «свистком», название которому WCH-LinkE. Но я не стал заморачиваться только на «свисток», а прикупил полный набор — «свисток», отладочные платы и сами микросхемы. В «суповой» набор за полторы тысячи рублей (если быть более точным — за 1515,69) входят — собственно, сам «свисток», две отладочных платы (с CH32V003 и с CH32V203) и по пять штук тех и других микросхем. Причём, что интересно, всё это «бохацтво» пришло в довольно-таки приличной упаковке, а образцы микросхем — так вообще были упакованы в пластиковые коробочки на подобие из-под ювелирных изделий или из-под наручных часов. В общем, мелочь — а приятно! Программатор собран на базе микроконтроллера тоже RISC-V — CH32V305F8. (Характеристики этого микроконтроллера не смотрел, но думаю, что они будут повкуснее CH32V203.) У меня есть предположение, что такой программатор можно повторить. Но… какой в этом смысл? Дешевле китайского-то сделать всё равно не получится. Быстрее и проще купить ещё один, если этот сдохнет. <--- Я пока не совсем разобрался со спецификой блога. Я так и не понял, как публиковать в блоге другие свои статьи. Видимо, их нужно размещать здесь же. Это немного странно. Но по мере освоения, я приведу блог в нормальное состояние. Несколько последовательных по смыслу статей будут размещены на этом месте. <--- CH32V003. Формирование временнЫх интервалов Делать так, как описано в этой короткой статье, я бы не рекомендовал. Эта статья предназначена не ради готового примера для применения в каких-либо коммерческих программах, а ради "первой ступеньки" в освоении модуля таймера. Таймеры в STM32 и в CH32V по сравнению с другими микроконтроллерами (например, MSP430, ATMEGA и другими) сильно навороченные, и разобраться сходу, как с ними работать, -- довольно-таки трудно. По себе сужу. Документация в интернете в основном представлена на английском языке. На русском тоже есть, но есть один момент. Документации по таймерам конкретно для CH32V нет. Хотя таймеры в CH32V и STM32 очень похоже, но состав, названия регистров, названия битов по отношению к STM32 несколько различаются. Поэтому у разработчиков возникают определённые трудности, которые выливаются в затягивание сроков разработки программ. Представленный в статье пример помогает быстрее начать с таймером работать. В микроконтроллерах CH32V реализованы два таймера -- таймер общего назначения (General Purpose) TIM2 и продвинутый таймер (Advanced) TIM1. В примере используется таймер общего назначения TIM2, но представленный код пригоден и для продвинутого таймера TIM1. У таймера много функций, которые он может выполнять. Начать освоения таймера лучше с самой простой функции -- формирование временнЫх промежутков. Что это значит? Допустим, мы пишем программу, которая выполняет какие-то действия (например, измеряет температуру). Поскольку программа измеряет температуру значительно быстрее, чем температура обычно меняется, то измерять температуру каждую миллисекунду нет смысла. Допустим, что нам нужно измерять температуру с периодом один раз в секунду. Само же время измерения и время передачи полученного значения температуры во внешний мир (на LCD или по последовательному каналу в компьютер) составляет 10 мс. В этом случае главный цикл программы будет выглядеть как-то так: int main(void) { ... tim_init(); // Настраиваю таймер // Главный цикл программы while (1) { temperature = get_temperature(); // Измеряю температуру send_value(temperature); // Передаю показания wait(); // Жду секунду } } В функции tim_init() производится настройка таймера на формирование секундных промежутков времени, а функция wait() тупо останавливявает выполнение программы до начала следующего промежутка времени. Вот, эти-то функции мы сейчас и рассмотрим более подробно. Начнём с функции tim_init(). Перед тем как взаимодействовать с таймером нужно разрешить его работу, или другими словами подать на него тактирование. Это делается одной командой: RCC->APB1PCENR |= RCC_TIM2EN; // Включаю таймер Следующим шагом нужно настроить работу таймера. Допустим, что тактовая частота SysClock, на которой работает ядро микроконтроллера, равно 24 МГц, а предделитель для шины мы не используем (то есть тактовая частота шины APB1 тоже 24 МГц). Тогда оставшийся код инициализации таймера будет выглядеть так: TIM2->INTFR = 0; // Предочистка TIM2->PSC = 24000 - 1; // Входная частота таймера = 1 кГц TIM2->ATRLR = 1000; // Соответствует одной секунде TIM2->CTLR1 = TIM_CEN; // Запускаю таймер в работу У каждого таймера есть свой предделитель. Он делить входную частоту на заданное значение и потом подает её на счетчик таймера. В нашем случае мы записываем коэффициент деления 24000 в регистр предделителя (PSC). Это значит, что после предделителя частота, котораяубдет поступать на счётчик таймера, будет равна 1 кГц. У каждого счётчика так же имеется регистр автозагрузки. Работа этого регистра зависит от направления счёта счетчика -- увеличивает ли счетчик свое значение или же уменьшает. Значение из этого регистра либо загружается в счётчик каждый раз при достижении счётчиком нулевого значения, либо наоборот -- при достижении счётчиком значения, равного записанному в регистре PSC, счётчик обнуляется. В обоих случая счётчик формирует событие UIF, которое мы и будем отслеживать в функции wait(). Код функции wait() ещё проще: void wait(void) { while (!(TIM2->INTFR & TIM_UIF)) ; // Ожидаю поднятия флага UIF TIM2->INTFR = 0; // Сбрасываю флаг } Как можно понять из приведённого кода, функция тормозит выполнение программы до тех пор, пока не сработает таймер и не будет взведён флаг UIF. После этого происходит очистка этого флага и программа может продолжить своё выполнение. Еще раз отмечу, что не смотря на то, что программа, построенная по предложенному способу, будет вполне рабочей, делать так не надо. Код программы был приведён только в учебных целях. С чего-то же нужно начинать? CH32V003. Генератор временнЫх интервалов По жизни намного чаще, чем описанный в предыдущей статье формирователь задержки, требуется формирователь временнЫх отметок, или другими словами -- таймер. Таймер, в нашем контексте, -- это такая бестия, которая периодически с заданным интервалом прерывает выполнение основной программы. Будем работать с таймером TIM2. Ранее я уже говорил, что периферия CH32V и периферия STM32 сильно совпадают. И это есть хорошо! Но, как обычно, есть нюансы! Количество и состав регистров таймера TIM2 у CH32V и STM32 совпадает на 100 %, хотя названия регистров сильно расходятся. Например, регистр управления у STM32 называется CR1, в то время как у CH32V он носит имя CTLR1. Или, вот, ещё например, регистр генерации событий -- у STM32 он называется EGR, а у CH32V -- SWEVGR. Не знаю, зачем это было нужно делать китайцам, но по моему мнению они сделали неверный шаг. Что же касается названий битов в регистрах, то в документации (на STM32 и на CH32V) они совпадают полностью. Но одно дело pdf-ка и совсем другое дело хэдерные файлы. Не знаю, насколько это оказалось дурным, но в данном случае ход китайцев мне понравился. Например, бит разрешения работы таймера у STM32 называется длинно -- TIM_CR1_CEN, а у китайцев этот же бит называется проще -- TIM_CEN. Во всяком случае мне показалось, что писать наименования "китайских" битов легче. Сравните: TIM2->CR1 = TIM_CR1_CEN; // STM32 и TIM2->CTLR1 = TIM_CEN; // CH32V Однако, давайте вернёмся к теме нарезки времени на кусочки определённой длительности. Обработчик прерывания от таймера TIM2 должен выглядеть следующим образом: __attribute__((interrupt("WCH-Interrupt-fast"))) void TIM2_Handler(void) { TIM2->INTFR &= ~TIM_UIF; // Очистить флаг прерывания ... // Выполнить какую-то полезную работу } В отличие от программного кода для STM32 в программах для CH32V нужно обязательно перед объявлением обработчика прерывания добавлять строку: __attribute__((interrupt("WCH-Interrupt-fast"))) Ну и не забывайте, что один обработчик может обслуживать целую кучу родственных прерываний. Например, в этом обработчике обслуживается прерывание, которое возбуждается в следствие События Подновления таймера. Кроме этого прерывания таймер может возбуждать также прерывание при возникновении условия захвата, при возникновении условия сравнении. У каждого прерывания свой флаг, который нужно сбросить при входе в обработчик прерывания. Поэтому чтобы в обработчике прерывания реагировать на своё прерывание, а не пытаться обслужить родственное, нужно по правильному писать так: __attribute__((interrupt("WCH-Interrupt-fast"))) void TIM2_Handler(void) { if (((TIM2->DMAINTENR & TIM_UIE) != 0) && ((TIM2->INTFR & TIM_UIF) != 0)) { // Это прерываение разрешено и флаг прерывания поднят TIM2->INTFR &= ~TIM_UIF; // Очистить флаг прерывания ... // Выполнить какую-то полезную работу } ... // Обслужить другие прерывания от таймера } Но если в вашей программе используется только одно прерывание (как в нашем примере), то такую проверку можно и не выполнять. С прерыванием разобрались. Теперь нужно написать код, который правильно настроит (инициализирует) таймер TIM2. Этот код выглядит так: void init_tim2(void) { RCC->APB1PCENR |= RCC_TIM2EN; // Включаю тактирование модуля таймера NVIC_EnableIRQ(TIM2_IRQn); // Разрешаю прерывания от таймера TIM2 TIM2->PSC = 24000 - 1; // Предделитель, тактовая частота счётчика 1 кГц TIM2->ATRLR = 50; // Интервал перываний 50 мс TIM2->INTFR = 0; // Сбрасываю все флаги TIM2->DMAINTENR = TIM_UIE; // Разрешаю прерывание по событию "Подновление таймера" TIM2->CTLR1 = TIM_CEN; // И наконец разрешаю работу таймера TIM2 } В результате мы получили программу, которая 20 раз в секунду прерывает работу основного цикла программы. Как видите, писать программу для CH32V не сложнее, чем для STM32.
×
×
  • Создать...