Espruino:Примеры/Низкоуровневый доступ к различным компонентам STM32: различия между версиями

Материал из Онлайн справочника
Перейти к навигацииПерейти к поиску
Нет описания правки
 
(не показано 5 промежуточных версий 2 участников)
Строка 5: Строка 5:
=Низкоуровневый доступ к различным компонентам STM32<ref>[https://www.espruino.com/STM32+Peripherals www.espruino.com - Low-level STM32 Peripheral access]</ref>=
=Низкоуровневый доступ к различным компонентам STM32<ref>[https://www.espruino.com/STM32+Peripherals www.espruino.com - Low-level STM32 Peripheral access]</ref>=


Espruino значительно упрощает доступ к периферийным устройствам микроконтроллера, но сами эти устройства могут делать гораздо больше того, к чему открыт доступ в JavaScript.
Espruino значительно упрощает доступ к периферийным устройствам микроконтроллера, но сами эти устройства могут делать гораздо больше того, к чему открыт доступ в [[JavaScript]].


Если вы хотите выжать из этих компонентов максимум, с ними нужно работать напрямую. В этой статье мы расскажем, как получить доступ к таймеру, чтобы измерять продолжительность импульса на микроконтроллерах STM32, используемых в Espruino [https://www.espruino.com/Original Original] и [https://www.espruino.com/Pico Pico].
Если вы хотите выжать из этих компонентов максимум, с ними нужно работать напрямую. В этой статье мы расскажем, как получить доступ к таймеру, чтобы измерять продолжительность импульса на микроконтроллерах [[STM32]], используемых в [[Espruino]] [https://www.espruino.com/Original Original] и [https://www.espruino.com/Pico Pico].


==Документация==
==Документация==
Строка 23: Строка 23:
* [https://www.espruino.com/datasheets/STM32F401xD.pdf Даташит]
* [https://www.espruino.com/datasheets/STM32F401xD.pdf Даташит]


В этой статье речь пойдёт об Espruino Pico. Это довольно большие документы, но нам понадобится лишь маленькая их часть.
В этой статье речь пойдёт об [[Espruino Pico]]. Это довольно большие документы, но нам понадобится лишь маленькая их часть.


== Что происходит в таймере во время ШИМ ==
== Что происходит в таймере во время ШИМ ==


Для начала давайте взглянем в мануал на страницу 241 в раздел «Advanced-control timer (TIM1)». Нам надо выяснить, что происходит в таймере, когда мы используем analogWrite(A8, 0.5, {freq:10}).
Для начала давайте взглянем в мануал на страницу ''241'' в раздел ''«Advanced-control timer (TIM1)»''. Нам надо выяснить, что происходит в таймере, когда мы используем analogWrite(A8, 0.5, {freq:10}).


Парой страниц ниже есть схема таймера:
Парой страниц ниже есть схема таймера:


[[File:STM32_Peripherals_TIM1_1.png|400px]]
[[File:STM32_Peripherals_TIM1_1.png|center]]


Мы подкрасили жёлтым цветом элементы, используемые в analogWrite(A8, 0.5, {freq:10}).
Мы подкрасили жёлтым цветом элементы, используемые в analogWrite(A8, 0.5, {freq:10}).


Во-первых, Espruino проверит, какие периферийные устройства доступны на контакте A8. Эту информацию можно найти в даташите в разделе «Pinouts and pin description» («Распиновка и описание контактов») в таблице номер 8 под названием «STM32F401xD/xE pin definitions» (ищите контакт PA8). Информация там трудночитаемая, но более понятно об этом написано в этой статье об Espruino Pico. Найдите там раздел «Pinout» («Распиновка») и наведите мышку на надпись «PWM» рядом с контактом A8. В результате должно появиться сообщение «TIM1_CH1» – это 1-ый канал периферийного устройства TIM1.
Во-первых, [[Espruino]] проверит, какие периферийные устройства доступны на контакте '''A8'''. Эту информацию можно найти в даташите в разделе ''«Pinouts and pin description» («Распиновка и описание контактов»)'' в таблице номер ''8'' под названием ''«STM32F401xD/xE pin definitions»'' (ищите контакт PA8). Информация там трудночитаемая, но более понятно об этом написано в этой статье об [[Espruino Pico]]. Найдите там раздел ''«Pinout» («Распиновка»)'' и наведите мышку на надпись ''«PWM»'' рядом с контактом '''A8'''. В результате должно появиться сообщение ''«TIM1_CH1»'' – это 1-ый канал периферийного устройства '''TIM1'''.


Далее Espruino проверит, подаёт ли STM32 питание на TIM1 (по умолчанию питание отключено в целях экономии электроэнергии).
Далее [[Espruino]] проверит, подаёт ли [[STM32]] питание на '''TIM1''' (по умолчанию питание отключено в целях экономии электроэнергии).


Затем Espruino займётся настройкой самого контакта:
Затем [[Espruino]] займётся настройкой самого контакта:
* У чипов STM32F4 (вроде того, что установлен на Pico) у каждого контакта есть 4-битный регистр, связанный с так называемым регистром «альтернативной функции». На внутреннем уровне контакт может быть подсоединён к другим периферийным компонентам (в данном случае – к I2C3, TIM1 и USART1), и число, которое вы записываете в этот регистр, определяет то, к какому именно периферийному компоненту этот контакт будет подключен.  
* У чипов [[STM32F4]] (вроде того, что установлен на [[Pico]]) у каждого контакта есть 4-битный регистр, связанный с так называемым регистром ''«альтернативной функции»''. На внутреннем уровне контакт может быть подсоединён к другим периферийным компонентам (в данном случае – к '''I2C3''', '''TIM1''' и '''USART1'''), и число, которое вы записываете в этот регистр, определяет то, к какому именно периферийному компоненту этот контакт будет подключен.  
Если заглянуть в даташите в таблицу 9 «Alternate function mapping» («Альтернативные функции контактов»), то вы увидите, что для того, чтобы включить TIM1_CH1 на контакте PA8, вам понадобится AF01 – так что  
Если заглянуть в даташите в таблицу 9 ''«Alternate function mapping» («Альтернативные функции контактов»)'', то вы увидите, что для того, чтобы включить '''TIM1_CH1''' на контакте '''PA8''', вам понадобится '''AF01''' – так что [[Espruino]] запишет в этот регистр единицу.
Espruino запишет в этот регистр единицу.
* Чипы [[STM32F1]] (вроде того, что установлен на [[Espruino Original]]) чуть старее, и поэтому на них управлять альтернативными функциями чуть менее удобно. У каждого периферийного компонента есть регистр альтернативных функций, поэтому если вы, например, захотите включить '''TIM1_CH1N''' на контакте '''A7''', то в регистре альтернативных функций '''TIM1''' нужно задать '''«1»''' – но тогда '''TIM1_CH1''' переедет на контакт '''E9''' (вместе со всем остальным!).
* Чипы STM32F1 (вроде того, что установлен на Espruino Original) чуть старее, и поэтому на них управлять альтернативными функциями чуть менее удобно. У каждого периферийного компонента есть регистр альтернативных функций, поэтому если вы, например, захотите включить TIM1_CH1N на контакте A7, то в регистре альтернативных функций TIM1 нужно задать «1» – но тогда TIM1_CH1 переедет на контакт E9 (вместе со всем остальным!).


Настроив контакт, Espruino займётся настройкой самого таймера:
Настроив контакт, [[Espruino]] займётся настройкой самого таймера:
* Настроит предварительный делитель (предделитель) для тактовой частоты 84 МГц, которую генерирует Pico – так, чтобы 16-битный счётчик CNT отсчитывал от 0 до 65535 чуть меньше 10 раз в секунду (потому что мы задали частоту 10).
* Настроит предварительный делитель (предделитель) для тактовой частоты ''84 МГц'', которую генерирует [[Pico]] – так, чтобы 16-битный счётчик '''CNT''' отсчитывал ''от 0 до 65535'' чуть ''меньше 10 раз в секунду'' (потому что мы задали частоту 10).
* Настроит регистр автоматической перезагрузки (AutoReload Register или ARR) таким образом, чтобы он выполнил подстройку частоты. Но эта подстройка будет заключаться лишь в том, что отсчёт будет вестись до числа, которое будет чуть меньше 16-битного 65535 (одного лишь предделителя для точного результата недостаточно).
* Настроит регистр автоматической перезагрузки (AutoReload Register или ARR) таким образом, чтобы он выполнил подстройку частоты. Но эта подстройка будет заключаться лишь в том, что отсчёт будет вестись до числа, которое будет чуть меньше 16-битного 65535 (одного лишь предделителя для точного результата недостаточно).
* Задаст в регистре захвата/сравнения 1 (Capture/Compare register 1 или CCR1) значение регистра ARR, умноженное на коэффициент заполнения (в данном случае – 0.5).
* Задаст в регистре захвата/сравнения 1 (Capture/Compare register 1 или CCR1) значение регистра '''ARR''', умноженное на коэффициент заполнения (в данном случае – 0.5).
* Настроит регистр управления выводом (Output control) так, чтобы на канале TIM1_CH1 всегда была единица кроме случаев, когда значение в CNT меньше значения регистра CCR.
* Настроит регистр управления выводом (Output control) так, чтобы на канале '''TIM1_CH1''' всегда была единица кроме случаев, когда значение в '''CNT''' меньше значения регистра '''CCR'''.
* Включаем сам таймер, записав соответствующее значение в бит включения.
* Включаем сам таймер, записав соответствующее значение в бит включения.


Что ж, готово. Теперь контакт A8 будет генерировать квадратную волну в 10 Гц.
Что ж, готово. Теперь контакт '''A8''' будет генерировать квадратную волну в ''10 Гц''.


== Получение доступа к таймеру ==
== Получение доступа к таймеру ==
Строка 58: Строка 57:
Согласны, это была не самая лёгкая для усваивания информация. В принципе, в мануале все необходимые шаги подробно расписаны, но вам также не помешают знания о ШИМ (и желательно [https://www.espruino.com/PWM побольше]).
Согласны, это была не самая лёгкая для усваивания информация. В принципе, в мануале все необходимые шаги подробно расписаны, но вам также не помешают знания о ШИМ (и желательно [https://www.espruino.com/PWM побольше]).


Все действия выше Espruino выполняет при помощи модификации значений в регистрах периферийных устройств, а если точнее – в битах памяти в разных местах чипа, управляющих работой этих периферийных устройств.
Все действия выше [[Espruino]] выполняет при помощи модификации значений в регистрах периферийных устройств, а если точнее – в битах памяти в разных местах чипа, управляющих работой этих периферийных устройств.


В мануале есть раздел «12.4 TIM1 registers» – в нём описывается, какие биты за что отвечают.
В мануале есть раздел ''«12.4 TIM1 registers»'' – в нём описывается, какие биты за что отвечают.


Давайте получим доступ к регистру CRR1 – это регистр, в котором Espruino применяет коэффициент заполнения – и посмотрим, что он такое. Если заглянуть в подраздел «12.4.14 TIM1 capture/compare register 1 (TIMx_CCR1)», то там можно увидеть строчку «Address offset: 0x34» – это смещение адреса для TIM1.
Давайте получим доступ к регистру '''CRR1''' – это регистр, в котором [[Espruino]] применяет коэффициент заполнения – и посмотрим, что он такое. Если заглянуть в подраздел ''«12.4.14 TIM1 capture/compare register 1 (TIMx_CCR1)»'', то там можно увидеть строчку ''«Address offset: 0x34»'' – это смещение адреса для '''TIM1'''.


Теперь нам надо найти адрес самого TIM1. Перейдите в раздел «2.3 Memory map» в мануале и посмотрите в таблицу 1. Для TIM1 там должно быть написано «0x40010000 - 0x400103FF».
Теперь нам надо найти адрес самого '''TIM1'''. Перейдите в раздел ''«2.3 Memory map»'' в мануале и посмотрите в таблицу 1. Для '''TIM1''' там должно быть написано ''«0x40010000 - 0x400103FF»''.


Отсюда можно рассчитать, что адрес CCR1 – это «0x40010000 + 0x34», то есть «0x40010034». Теперь давайте вернёмся в раздел «TIM1 capture/compare register 1 (TIMx_CCR1)» – там говорится, что это 16-битное значение, так что мы воспользуемся функцией peek16().
Отсюда можно рассчитать, что адрес '''CCR1''' – это ''«0x40010000 + 0x34»'', то есть ''«0x40010034»''. Теперь давайте вернёмся в раздел ''«TIM1 capture/compare register 1 (TIMx_CCR1)»'' – там говорится, что это 16-битное значение, так что мы воспользуемся функцией peek16().


Во-первых, давайте напечатаем var CCR1 = 0x40010034, чтобы сохранить адрес, а затем peek16(CCR1) – в ответ должно прийти «0», ведь мы пока не запускали analogWrite().
Во-первых, давайте напечатаем var CCR1 = 0x40010034, чтобы сохранить адрес, а затем peek16(CCR1) – в ответ должно прийти «0», ведь мы пока не запускали analogWrite().


Возьмите макетную плату и подключите светодиод к контакту A8 при помощи резистора (примерно на 100 ом или вроде того).
Возьмите макетную плату и подключите светодиод к контакту '''A8''' при помощи резистора (примерно на [[100 Ом]] или вроде того).


[[File:STM32_Peripherals_breadboard_2.jpg|400px]]
[[File:STM32_Peripherals_breadboard_2.jpg|center]]


Затем запустите analogWrite(A8, 0.5, {freq:10}) – в результате светодиод должен начать мигать с частотой 10 раз в секунду, и всё это будет выполняться на аппаратном уровне.
Затем запустите analogWrite(A8, 0.5, {freq:10}) – в результате светодиод должен начать мигать с частотой 10 раз в секунду, и всё это будет выполняться на аппаратном уровне.


Снова вызовите peek16(CCR1), чтобы узнать значение CCR1 – теперь вам должно прийти значение «32307».
Снова вызовите peek16(CCR1), чтобы узнать значение '''CCR1''' – теперь вам должно прийти значение ''«32307».''


Теперь давайте увеличим частоту мерцания при помощи analogWrite(A8, 0.05, {freq:10}), а затем снова вызовем peek16(CCR1). Значение будет в 10 раз меньше – «3230».
Теперь давайте увеличим частоту мерцания при помощи analogWrite(A8, 0.05, {freq:10}), а затем снова вызовем peek16(CCR1). Значение будет в ''10 раз меньше – «3230»''.


Мы можем использовать это значение, чтобы менять коэффициент заполнения.
Мы можем использовать это значение, чтобы менять коэффициент заполнения.
Строка 84: Строка 83:
* poke16(CCR1, 40000) сделает так, что светодиод большую часть времени будет включен.
* poke16(CCR1, 40000) сделает так, что светодиод большую часть времени будет включен.


Зная, что число «32307» в CCR1 означает коэффициент заполнения 50%, мы можем догадаться, до какого числа будет вести отсчёт счётчик CNT – это будет примерно 2 x 32307. Но мы можем узнать и точное значение – для этого нужно посмотреть в регистр ARR. В мануале говорится, что смещение адреса у него – это «0x2C», поэтому давайте сначала попробуем вызвать var ARR = 0x4001002C, а затем peek16(ARR). Получится «64615» – примерно как мы и думали.
Зная, что число ''«32307»'' в '''CCR1''' означает коэффициент заполнения 50%, мы можем догадаться, до какого числа будет вести отсчёт счётчик '''CNT''' – это будет примерно ''2 x 32307''. Но мы можем узнать и точное значение – для этого нужно посмотреть в регистр '''ARR'''. В мануале говорится, что смещение адреса у него – это ''«0x2C»'', поэтому давайте сначала попробуем вызвать var ARR = 0x4001002C, а затем peek16(ARR). Получится ''«64615»'' – примерно как мы и думали.


Но это число можно поменять. Если сделать его меньше, то таймер будет вести отсчёт до более низкого значения, в результате чего светодиод будет мигать чаще.
Но это число можно поменять. Если сделать его меньше, то таймер будет вести отсчёт до более низкого значения, в результате чего [[светодиод]] будет мигать чаще.


Вызовите poke16(ARR, 30000) – теперь таймер должен отсчитывать в два раза быстрее. Но в то же время светодиод не будет мигать, а будет постоянно включен, потому что отсчёт будет вестись между 30000 и 0, а в CCR1 у нас задано 40000. Давайте зададим там что-то поменьше – например, poke16(CCR1, 1000) – и теперь светодиод начнёт моргать. Попробуйте поэкспериментировать с разными значениями для ARR, чтобы получить разные частоты.
Вызовите poke16(ARR, 30000) – теперь таймер должен отсчитывать в два раза быстрее. Но в то же время [[светодиод]] не будет мигать, а будет постоянно включен, потому что отсчёт будет вестись ''между 30000 и 0'', а в '''CCR1''' у нас задано ''40000''. Давайте зададим там что-то поменьше – например, poke16(CCR1, 1000) – и теперь светодиод начнёт моргать. Попробуйте поэкспериментировать с разными значениями для '''ARR''', чтобы получить разные частоты.


Теперь давайте попробуем прочесть значение счётчика во время его работы. Отсчёт ведётся очень быстро, так что давайте снова воспользуемся analogWrite(), чтобы замедлить его:
Теперь давайте попробуем прочесть значение счётчика во время его работы. Отсчёт ведётся очень быстро, так что давайте снова воспользуемся analogWrite(), чтобы замедлить его:


<syntaxhighlight lang="javascript" enclose="div">
<syntaxhighlight lang="javascript">
analogWrite(A8, 0, {freq:0.1})
analogWrite(A8, 0, {freq:0.1})
</syntaxhighlight>
</syntaxhighlight>


Теперь каждый новый отсчёт будет вестись в течение 10 секунд. В мануале видим, что счётчик CNT таймера TIM1 имеет смещение адреса 0x24 – давайте получим доступ к нему:
Теперь каждый новый отсчёт будет вестись в течение ''10 секунд''. В мануале видим, что счётчик '''CNT''' таймера '''TIM1''' имеет смещение адреса ''0x24'' – давайте получим доступ к нему:


<syntaxhighlight lang="javascript" enclose="div">
<syntaxhighlight lang="javascript">
var CNT = 0x40010024;
var CNT = 0x40010024;
peek16(CNT);
peek16(CNT);
</syntaxhighlight>
</syntaxhighlight>


Теперь несколько раз нажмите клавиши «Вверх» и Enter. Вы увидите в консоли примерно следующее:
Теперь несколько раз нажмите клавиши {{клавиша|«Вверх»}} и {{клавиша|Enter}}. Вы увидите в консоли примерно следующее:


<syntaxhighlight lang="javascript" enclose="div">
<syntaxhighlight lang="javascript">
>peek16(CNT);
>peek16(CNT);
=31807
=31807
Строка 129: Строка 128:
Всё это интересно, но не особо полезно. Но что если нам попробовать считать то, сколько раз было изменено состояние контакта?
Всё это интересно, но не особо полезно. Но что если нам попробовать считать то, сколько раз было изменено состояние контакта?


Давайте снова взглянем на схему, чтобы увидеть, какие компоненты TIM1 нам понадобятся:
Давайте снова взглянем на схему, чтобы увидеть, какие компоненты '''TIM1''' нам понадобятся:


[[File:STM32_Peripherals_TIM1_counter_3.png|400px]]
[[File:STM32_Peripherals_TIM1_counter_3.png|center]]


Итак, предположим, Espruino уже настроила таймер для analogWrite(). Что нам надо сделать?
Итак, предположим, [[Espruino]] уже настроила таймер для analogWrite(). Что нам надо сделать?
* Задать на контакте A8 правильное состояние.
* Задать на контакте '''A8''' правильное состояние.
* Включить детектор фронтов.
* Включить детектор фронтов.
* Сделать так, чтобы 3 мультиплексора направляли сигнал от A8 в нужное место.
* Сделать так, чтобы 3 мультиплексора направляли сигнал от '''A8''' в нужное место.
* Выполнить сброс предделителя (предделителя) частоты, чтобы он вообще не делал никакого деления.
* Выполнить сброс предделителя (предделителя) частоты, чтобы он вообще не делал никакого деления.
* Выполнить сброс значения в регистре ARR до «0», чтобы вести отсчёт от 0 до 65535.
* Выполнить сброс значения в регистре '''ARR''' до ''«0»'', чтобы вести отсчёт ''от 0 до 65535''.


Итак, вперёд. Во-первых, подключаем контакт A8 к контакту A5 (он находится прямо напротив A8). Чуть позже мы начнём отправлять с контакта A5 импульсы, чтобы протестировать таймер.
Итак, вперёд. Во-первых, подключаем контакт '''A8''' к контакту '''A5''' (он находится прямо напротив '''A8'''). Чуть позже мы начнём отправлять с контакта '''A5''' импульсы, чтобы протестировать таймер.


Теперь давайте зададим адреса регистров (я, опять же, скопировал их из мануала):
Теперь давайте зададим адреса регистров (я, опять же, скопировал их из мануала):


<syntaxhighlight lang="javascript" enclose="div">
<syntaxhighlight lang="javascript">
// Регистр управления слейв-режимом (Slave mode control или SMCR):
// Регистр управления слейв-режимом (Slave mode control или SMCR):
var SMCR = 0x40010008;
var SMCR = 0x40010008;
Строка 166: Строка 165:
=== Задаём на контакте A8 правильное состояние ===
=== Задаём на контакте A8 правильное состояние ===


Вообще, в A8 уже задано правильное состояние. Он работает в режиме альтернативной функции и при помощи analogWrite() подключен к TIM1. Нам этого достаточно.
Вообще, в '''A8''' уже задано правильное состояние. Он работает в режиме альтернативной функции и при помощи analogWrite() подключен к '''TIM1'''. Нам этого достаточно.


=== Включаем детектор фронтов ===
=== Включаем детектор фронтов ===


Найдите раздел «12.3.4 Clock selection», а в нём – подраздел «External clock source mode 1». В нём будет вот такая схема:
Найдите раздел ''«12.3.4 Clock selection»'', а в нём – подраздел ''«External clock source mode 1»''. В нём будет вот такая схема:


[[File:STM32_Peripherals_TIM1_mux_4.png|400px]]
[[File:STM32_Peripherals_TIM1_mux_4.png|center]]


Здесь показано, что входом служит TI2, но TI1 – это, по сути, то же самое. Мы просто поменяем в коде «2» на «1».
Здесь показано, что входом служит '''TI2''', но '''TI1''' – это, по сути, то же самое. Мы просто поменяем в коде ''«2»'' на ''«1»''.


Итак, нам надо:
Итак, нам надо:
* Настроить канал 1 на определение фронтов сигнала на входе TI1. Для этого в регистр CCMR1 нужно записать CC1S = ‘01’.
* Настроить канал 1 на определение фронтов сигнала на входе '''TI1'''. Для этого в регистр '''CCMR1''' нужно записать CC1S = ‘01’.
* Задать продолжительность работы фильтра, отредактировав биты IC1F[3:0] в регистре CCMR1 – нам фильтр не нужен вообще, так что пишем IC1F = 0000.
* Задать продолжительность работы фильтра, отредактировав биты '''IC1F[3:0]''' в регистре '''CCMR1''' – нам фильтр не нужен вообще, так что пишем IC1F = 0000.


В мануале в разделе «12.4.7 TIMx capture/compare mode register 1 (TIMx_CCMR1)» есть таблица, из которой видно, что CC1S – это два самых младших бита регистра CCMR1, а IC1F – биты с 4 по 7.  
В мануале в разделе ''«12.4.7 TIMx capture/compare mode register 1 (TIMx_CCMR1)»'' есть таблица, из которой видно, что '''CC1S''' – это два самых младших бита регистра '''CCMR1''', а '''IC1F''' – биты ''с 4 по 7''.  


Однако в том же разделе чуть ниже есть маленький текст, где говорится, что запись в биты CC1S можно делать, только когда канал выключен (CC1E = ‘0’ в TIMx_CCER).
Однако в том же разделе чуть ниже есть маленький текст, где говорится, что запись в биты '''CC1S''' можно делать, только когда канал выключен (CC1E = ‘0’ в TIMx_CCER).


Поскольку канал уже, очевидно, включен (потому что мы использовали его для ШИМ), нам нужно будет его выключить. Ищите в документации регистр CCER, нам понадобится в нём бит CC1E (это бит номер 0).
Поскольку канал уже, очевидно, включен (потому что мы использовали его для [[ШИМ]]), нам нужно будет его выключить. Ищите в документации регистр '''CCER''', нам понадобится в нём бит '''CC1E''' (это бит ''номер 0'').


Итак, нам нужно модифицировать лишь эти биты. Для этого берём текущее значение, при помощи символов «&» и «~» применяем маску к старым битам и записываем новые значения при помощи символа «|». Вот так:
Итак, нам нужно модифицировать лишь эти биты. Для этого берём текущее значение, при помощи символов ''«&»'' и ''«'' применяем маску к старым битам и записываем новые значения при помощи символа ''«|»''. Вот так:


<syntaxhighlight lang="javascript" enclose="div">
<syntaxhighlight lang="javascript">
// CC1E[0] = 0 (выключаем канал 1):
// CC1E[0] = 0 (выключаем канал 1):
poke16(CCER1, peek16(CCER1) & ~1);
poke16(CCER1, peek16(CCER1) & ~1);
Строка 195: Строка 194:
</syntaxhighlight>
</syntaxhighlight>


И затем задаём положительную полярность, записав в регистр CCER следующее:
И затем задаём положительную полярность, записав в регистр '''CCER''' следующее:


* CC1P=0
* CC1P=0
* CC1NP=0
* CC1NP=0


Согласно мануалу, CC1P – это бит 1, а CC1NP – это бит 3.
Согласно мануалу, '''CC1P''' – это бит 1, а '''CC1NP''' – это бит 3.


Но чуть ранее мы отключили канал, так что давайте также включим его:
Но чуть ранее мы отключили канал, так что давайте также включим его:


<syntaxhighlight lang="javascript" enclose="div">
<syntaxhighlight lang="javascript">
// CC1P=0, CC1NP=0 (определяем передний фронт),  
// CC1P=0, CC1NP=0 (определяем передний фронт),  
// CC1E[0] = 1 (включаем канал 1):
// CC1E[0] = 1 (включаем канал 1):
Строка 213: Строка 212:


На этой же странице мануала говорится, что нам надо:
На этой же странице мануала говорится, что нам надо:
* Настроить таймер в режим внешней частоты 1, записав SMS=111 в регистр SMCR.
* Настроить таймер в режим внешней частоты ''1'', записав '''SMS=111''' в регистр '''SMCR'''.
* Сделать TI1 источником входящего триггера, записав TS=101 в регистр SMCR
* Сделать '''TI1''' источником входящего триггера, записав '''TS=101''' в регистр '''SMCR'''


'''Примечание:''' Вообще, в мануале говорится, чтобы мы записали TS=110, чтобы сделать источником входящего триггера TI2, но нам нужен TI1.
{{Примечание1|1=Вообще, в мануале говорится, чтобы мы записали '''TS=110''', чтобы сделать источником входящего триггера '''TI2''', но нам нужен '''TI1'''.}}


SMS – это биты с 0 по 2, а TS – это биты с 4 по 6, поэтому:
'''SMS''' – это биты ''с 0 по 2'', а '''TS''' – это биты ''с 4 по 6'', поэтому:


<syntaxhighlight lang="javascript" enclose="div">
<syntaxhighlight lang="javascript">
// SMS[2:0]=111 (внешняя частота), TS[6:4]=101 (CH1 как триггер):
// SMS[2:0]=111 (внешняя частота), TS[6:4]=101 (CH1 как триггер):
poke16(SMCR, (peek16(SMCR) & ~0b1110111) | 0b1010111);
poke16(SMCR, (peek16(SMCR) & ~0b1110111) | 0b1010111);
Строка 226: Строка 225:


В мануале также говорится, чтобы мы:
В мануале также говорится, чтобы мы:
* Включили счётчик, записав CEN=1 в регистр CR1.
* Включили счётчик, записав '''CEN=1''' в регистр '''CR1'''.


Но Espruino уже сделала это за нас при помощи analogWrite():
Но [[Espruino]] уже сделала это за нас при помощи analogWrite():


=== Выполняем сброс регистров ===
=== Выполняем сброс регистров ===
Строка 234: Строка 233:
Для этого понадобится вот такой простой код:
Для этого понадобится вот такой простой код:


<syntaxhighlight lang="javascript" enclose="div">
<syntaxhighlight lang="javascript">
// В предделителе частоты задаём «0»,
// В предделителе частоты задаём «0»,
// чтобы использовать каждый такт:
// чтобы использовать каждый такт:
Строка 243: Строка 242:


Но в мануале также говорится следующее:
Но в мануале также говорится следующее:
* В регистре PSC содержится значение, которое будет загружаться в активный регистр предделителя при каждом событии обновления (включая ситуацию, когда в регистре TIMx_EGR будет выполняться сброс счётчика при помощи бита UG или при настройке контроллера триггеров при помощи «режима сброса»).
* В регистре '''PSC''' содержится значение, которое будет загружаться в активный регистр предделителя при каждом событии обновления (включая ситуацию, когда в регистре '''TIMx_EGR''' будет выполняться сброс счётчика при помощи бита '''UG''' или при настройке контроллера триггеров при помощи ''«режима сброса»'').


Поэтому нам также нужно будет сделать следующее:
Поэтому нам также нужно будет сделать следующее:


<syntaxhighlight lang="javascript" enclose="div">
<syntaxhighlight lang="javascript">
// Выполняем запись в бит UG[0],
// Выполняем запись в бит UG[0],
// чтобы выполнить сброс счётчика и обновить предделитель:
// чтобы выполнить сброс счётчика и обновить предделитель:
Строка 255: Строка 254:
Всё это вместе будет выглядеть следующим образом:
Всё это вместе будет выглядеть следующим образом:


<syntaxhighlight lang="javascript" enclose="div">
<syntaxhighlight lang="javascript">
// Регистр управления слейв-режимом (Slave mode control или SMCR):
// Регистр управления слейв-режимом (Slave mode control или SMCR):
var SMCR = 0x40010008;  
var SMCR = 0x40010008;  
Строка 293: Строка 292:
</syntaxhighlight>
</syntaxhighlight>


Загляните в содержимое счётчика при помощи peek16(CNT) – должно вернуть «0».
Загляните в содержимое счётчика при помощи peek16(CNT) – должно вернуть ''«0»''.


Теперь попробуйте включить/выключить контакт A5 (ранее мы подключили его к A8) при помощи digitalWrite(A5,1) и digitalWrite(A5,0) – это должно увеличить значение счётчика.
Теперь попробуйте включить/выключить контакт '''A5''' (ранее мы подключили его к '''A8''') при помощи digitalWrite(A5,1) и digitalWrite(A5,0) – это должно увеличить значение счётчика.


Снова проверьте счётчик при помощи peek16(CNT) – теперь должно вернуть «1».
Снова проверьте счётчик при помощи peek16(CNT) – теперь должно вернуть ''«1»''.


Теперь давайте попробуем сгенерировать много импульсов:
Теперь давайте попробуем сгенерировать много импульсов:


<syntaxhighlight lang="javascript" enclose="div">
<syntaxhighlight lang="javascript">
for (var i=0;i<1000;i++) {
for (var i=0;i<1000;i++) {
   digitalWrite(A5,1);digitalWrite(A5,0);
   digitalWrite(A5,1);digitalWrite(A5,0);
Строка 307: Строка 306:
</syntaxhighlight>
</syntaxhighlight>


Снова проверьте счётчик при помощи peek16(CNT) – теперь должно вернуть «1001».
Снова проверьте счётчик при помощи peek16(CNT) – теперь должно вернуть ''«1001»''.


Чтобы измерить частоту, можно воспользоваться функцией setInterval(). С её помощью можно узнать, насколько ежесекундно меняется значение в CNT – это и будет частотой квадратной волны на контакте A8.
Чтобы измерить частоту, можно воспользоваться функцией setInterval(). С её помощью можно узнать, насколько ежесекундно меняется значение в '''CNT''' – это и будет частотой квадратной волны на контакте '''A8'''.
TIM1 – это 16-битный таймер, что делает его применение немного ограниченным. Впрочем, есть и другие таймеры вроде TIM2 и TIM5, которые работают точно так же (просто поменяйте базовый адрес «0x40010000» на базовый адрес другого таймера), но зато оснащены 32-битными счётчиками. Единственное, нужно будет использовать peek32() вместо peek16() для считывания значения счётчика.
 
'''TIM1''' – это 16-битный таймер, что делает его применение немного ограниченным. Впрочем, есть и другие таймеры вроде '''TIM2''' и '''TIM5''', которые работают точно так же (просто поменяйте базовый адрес ''«0x40010000»'' на базовый адрес другого таймера), но зато оснащены 32-битными счётчиками. Единственное, нужно будет использовать peek32() вместо peek16() для считывания значения счётчика.


== Захват на входе ==
== Захват на входе ==


Ещё одна полезная функция счётчика CNT – это захват на входе. В этом режиме вы можете задать для CNT любую частоту – при помощи analogWrite(A8, 0.5, {freq : my_frequency}) – но каждый раз, когда бит будет менять состояние, значение CNT будет сохранено в соответствующий регистр CCR.
Ещё одна полезная функция счётчика '''CNT''' – это захват на входе. В этом режиме вы можете задать для '''CNT''' любую частоту – при помощи analogWrite(A8, 0.5, {freq : my_frequency}) – но каждый раз, когда бит будет менять состояние, значение '''CNT''' будет сохранено в соответствующий регистр '''CCR'''.


Чтобы реализовать это, мы воспользуемся вот этой конфигурацией таймера:
Чтобы реализовать это, мы воспользуемся вот этой конфигурацией таймера:


[[File:STM32_Peripherals_TIM1_capture_5.png|400px]]
[[File:STM32_Peripherals_TIM1_capture_5.png|center]]


В мануале об этом рассказывается в соответствующем разделе «12.3.6 Input capture mode» («Режим захвата на входе»), так что здесь некоторые подробности будут опущены.
В мануале об этом рассказывается в соответствующем разделе ''«12.3.6 Input capture mode» («Режим захвата на входе»)'', так что здесь некоторые подробности будут опущены.


Всё, что нам нужно будет сделать, это:
Всё, что нам нужно будет сделать, это:
* Включить и настроить счётчик (предделитель, ARR) при помощи analogWrite()
* Включить и настроить счётчик (предделитель, '''ARR''') при помощи analogWrite()
* Перенастроить канал 1 на прослушку событий, а не генерацию ШИМ
* Перенастроить ''канал 1'' на прослушку событий, а не генерацию ШИМ


Первый шаг, очевидно, делается просто. А чтобы выполнить второй шаг мы украдём немного из кода, который написали в разделе выше, но с одной маленькой модификацией. В этот раз мы воспользуемся предделителем IC1PS канала 1 – нам нужно будет задать в нём «0», чтобы отключить его. Для этого мы лишь зададим «0» в двух битах регистра CCMR1:
Первый шаг, очевидно, делается просто. А чтобы выполнить второй шаг мы украдём немного из кода, который написали в разделе выше, но с одной маленькой модификацией. В этот раз мы воспользуемся предделителем '''IC1PS''' ''канала 1'' – нам нужно будет задать в нём ''«0»'', чтобы отключить его. Для этого мы лишь зададим ''«0»'' в двух битах регистра '''CCMR1''':


<syntaxhighlight lang="javascript" enclose="div">
<syntaxhighlight lang="javascript">
// Регистр статуса (SR):
// Регистр статуса (SR):
var SR= 0x40010010;
var SR= 0x40010010;
Строка 354: Строка 354:
Вот и всё. Увидеть увеличение счётчика можно при помощи peek16(CNT) – число, возвращаемое этой функцией, должно постоянно меняться.
Вот и всё. Увидеть увеличение счётчика можно при помощи peek16(CNT) – число, возвращаемое этой функцией, должно постоянно меняться.


Но если проверить значение в регистре захвата/сравнения 1 при помощи функции peek16(CCR1), она будет возвращать одно и то же число. Кроме того, при помощи peek16(SR)&2 можно узнать, было ли прерывание на CCR1 (при помощи бита CC1IF) – вам должно вернуть «0».
Но если проверить значение в регистре захвата/сравнения 1 при помощи функции peek16(CCR1), она будет возвращать одно и то же число. Кроме того, при помощи peek16(SR)&2 можно узнать, было ли прерывание на '''CCR1''' (при помощи бита '''CC1IF''') – вам должно вернуть ''«0»''.


Но теперь – с условием, что контакты A8 и A5 по-прежнему подключены друг к другу – мы можем подать на A8 единицу и ноль и посмотреть, что получится:
Но теперь – с условием, что контакты '''A8''' и '''A5''' по-прежнему подключены друг к другу – мы можем подать на '''A8''' единицу и ноль и посмотреть, что получится:


<syntaxhighlight lang="javascript" enclose="div">
<syntaxhighlight lang="javascript">
digitalWrite(A5,1);digitalWrite(A5,0);
digitalWrite(A5,1);digitalWrite(A5,0);
</syntaxhighlight>
</syntaxhighlight>
Строка 364: Строка 364:
Теперь peek16(SR)&2 вернёт ненулевое значение, показывая, что что-то было поймано. И если мы заглянем в регистр захвата/сравнения при помощи peek16(CCR1), то увидим, что его значение изменилось.
Теперь peek16(SR)&2 вернёт ненулевое значение, показывая, что что-то было поймано. И если мы заглянем в регистр захвата/сравнения при помощи peek16(CCR1), то увидим, что его значение изменилось.


Акт считывания значения в регистре CCR1 задаст в бите CC1IF значение «0», поэтому peek16(SR)&2 теперь тоже будет возвращать «0».
Акт считывания значения в регистре '''CCR1''' задаст в бите '''CC1IF''' значение ''«0»'', поэтому peek16(SR)&2 теперь тоже будет возвращать ''«0»''.


Но что если перед тем, как мы успеем прочесть последнее значение, таймер сработает дважды? Это мы тоже сможем определить – при помощи бита CC1OF и функции peek16(SR)&512.
Но что если перед тем, как мы успеем прочесть последнее значение, таймер сработает дважды? Это мы тоже сможем определить – при помощи бита '''CC1OF''' и функции peek16(SR)&512.


Возможно, этот бит уже показывает переполнение, но его можно обнулить при помощи poke16(SR,peek16(SR)&~512). И теперь, если мы подадим на контакт два импульса, не считывая значение в CCR1, в бите переполнения CC1OF будет «1».
Возможно, этот бит уже показывает переполнение, но его можно обнулить при помощи poke16(SR,peek16(SR)&~512). И теперь, если мы подадим на контакт два импульса, не считывая значение в '''CCR1''', в бите переполнения '''CC1OF''' будет ''«1»''.


== Ёмкостный тактильный датчик ==
== Ёмкостный тактильный датчик ==


Режим захвата на входе может быть ещё более полезен, если использовать его вместе с двумя каналами одного и того же таймера. Давайте воспользуемся контактом A10 на Pico (TIM1_CH3) для генерирования обычной ШИМ, но также активируем режим захвата/сравнения на контакте A8 при помощи кода выше. Это позволит нам измерить, сколько времени пройдёт между началом генерирования ШИМ на A10 и изменением состояния входного контакта A8 на единицу.
Режим захвата на входе может быть ещё более полезен, если использовать его вместе с двумя каналами одного и того же таймера. Давайте воспользуемся контактом '''A10''' на [[Pico]] ('''TIM1_CH3''') для генерирования обычной [[ШИМ]], но также активируем режим захвата/сравнения на контакте '''A8''' при помощи кода выше. Это позволит нам измерить, сколько времени пройдёт между началом генерирования [[ШИМ]] на '''A10''' и изменением состояния входного контакта '''A8''' на единицу.


А это (помимо прочего) может пригодиться при создании тактильного датчика.
А это (помимо прочего) может пригодиться при создании тактильного датчика.


Отключите от макетной платы всё, кроме Pico, а затем подключите между контактами A10 и A8 резистор на 1 млн ом (подойдёт и 100 кОм, но результат буде чуть хуже). Контакт A10 – это маленькое контактное отверстие, находящееся на противоположном от USB-разъёма конце платы (см. схему в разделе «Pinout» на этой странице), так что вам будет достаточно просто вставить ножку резистора в это отверстие. Теперь возьмите 10-сантиметровый провод, согните его в петлю и подключите к контакту A8. Итак, теперь к нашей схеме подключены очень мощный резистор и очень слабый конденсатор (петля из провода). Но если поднести к проводу руку, ёмкость этого самодельного конденсатора увеличится.
Отключите от макетной платы всё, кроме [[Pico]], а затем подключите между контактами '''A10''' и '''A8''' [[резистор]] на [[1 МОм]] (подойдёт и [[100 кОм]], но результат буде чуть хуже). Контакт '''A10''' – это маленькое контактное отверстие, находящееся на противоположном от USB-разъёма конце платы (см. схему в разделе ''«Pinout»'' на этой странице), так что вам будет достаточно просто вставить ножку [[резистор]]а в это отверстие. Теперь возьмите 10-сантиметровый провод, согните его в петлю и подключите к контакту '''A8'''. Итак, теперь к нашей схеме подключены очень мощный резистор и очень слабый конденсатор (петля из провода). Но если поднести к проводу руку, ёмкость этого самодельного конденсатора увеличится.


И что всё это значит? Увеличение ёмкости конденсатора означает, что теперь на его зарядку и разрядку будет требоваться больше времени (что делается через резистор, подключенный к A10). И при помощи режима захвата на входе мы можем это определить (не тратя время на вычисления в JavaScript).
И что всё это значит? Увеличение ёмкости конденсатора означает, что теперь на его зарядку и разрядку будет требоваться больше времени (что делается через резистор, подключенный к '''A10'''). И при помощи режима захвата на входе мы можем это определить (не тратя время на вычисления в [[JavaScript]]).


Просто загрузите на Espruino вот этот код:
Просто загрузите на [[Espruino]] вот этот код:


<syntaxhighlight lang="javascript" enclose="div">
<syntaxhighlight lang="javascript">
// Регистр статуса (Status Register или SR):
// Регистр статуса (Status Register или SR):
var SR= 0x40010010;
var SR= 0x40010010;
Строка 412: Строка 412:
</syntaxhighlight>
</syntaxhighlight>


Этот код идентичен коду из раздела «Захват на входе», но теперь:
Этот код идентичен коду из раздела ''«Захват на входе»'', но теперь:
* Частота составляет 1000 Гц
* Частота составляет ''1000 Гц''
* Квадратная волна также генерируется на контакте A10
* Квадратная волна также генерируется на контакте '''A10'''
* Чтение регистра CCR1 выполняется во вспомогательной функции
* Чтение регистра '''CCR1''' выполняется во вспомогательной функции


Итак, если теперь вызвать getCap(), то в ответ будет получено число, зависящее от мощности используемого резистора и ёмкости провода-конденсатора.
Итак, если теперь вызвать getCap(), то в ответ будет получено число, зависящее от мощности используемого резистора и ёмкости провода-конденсатора.
Строка 421: Строка 421:
Но если вызвать getCap() в момент поднесения руки к проводу (на расстояние меньше сантиметра), то возвращаемое значение должно увеличиться. А если поместить getCap() в функцию setInterval() вот так...
Но если вызвать getCap() в момент поднесения руки к проводу (на расстояние меньше сантиметра), то возвращаемое значение должно увеличиться. А если поместить getCap() в функцию setInterval() вот так...


<syntaxhighlight lang="javascript" enclose="div">
<syntaxhighlight lang="javascript">
// Это значение должно быть чуть выше того,
// Это значение должно быть чуть выше того,
// которое функция getCap() возвращает,
// которое функция getCap() возвращает,
Строка 436: Строка 436:
=См.также=
=См.также=


{{ads}}
 


=Внешние ссылки=
=Внешние ссылки=
Строка 442: Строка 442:
<references />
<references />


{{Навигационная таблица/Espruino}}
{{Навигационная таблица/Портал/Espruino}}
{{Навигационная таблица/Телепорт}}

Текущая версия от 19:55, 23 мая 2023

Перевод: Максим Кузьмин
Проверка/Оформление/Редактирование: Мякишев Е.А.


Низкоуровневый доступ к различным компонентам STM32[1]

Espruino значительно упрощает доступ к периферийным устройствам микроконтроллера, но сами эти устройства могут делать гораздо больше того, к чему открыт доступ в JavaScript.

Если вы хотите выжать из этих компонентов максимум, с ними нужно работать напрямую. В этой статье мы расскажем, как получить доступ к таймеру, чтобы измерять продолжительность импульса на микроконтроллерах STM32, используемых в Espruino Original и Pico.

Документация

Лучший источник документации по периферийным компонентам чипа – это сам производитель, то есть ST. Нам понадобятся мануал и даташит для используемого чипа:

Для Espruino Original:

Для Espruino Pico:

В этой статье речь пойдёт об Espruino Pico. Это довольно большие документы, но нам понадобится лишь маленькая их часть.

Что происходит в таймере во время ШИМ

Для начала давайте взглянем в мануал на страницу 241 в раздел «Advanced-control timer (TIM1)». Нам надо выяснить, что происходит в таймере, когда мы используем analogWrite(A8, 0.5, {freq:10}).

Парой страниц ниже есть схема таймера:

Мы подкрасили жёлтым цветом элементы, используемые в analogWrite(A8, 0.5, {freq:10}).

Во-первых, Espruino проверит, какие периферийные устройства доступны на контакте A8. Эту информацию можно найти в даташите в разделе «Pinouts and pin description» («Распиновка и описание контактов») в таблице номер 8 под названием «STM32F401xD/xE pin definitions» (ищите контакт PA8). Информация там трудночитаемая, но более понятно об этом написано в этой статье об Espruino Pico. Найдите там раздел «Pinout» («Распиновка») и наведите мышку на надпись «PWM» рядом с контактом A8. В результате должно появиться сообщение «TIM1_CH1» – это 1-ый канал периферийного устройства TIM1.

Далее Espruino проверит, подаёт ли STM32 питание на TIM1 (по умолчанию питание отключено в целях экономии электроэнергии).

Затем Espruino займётся настройкой самого контакта:

  • У чипов STM32F4 (вроде того, что установлен на Pico) у каждого контакта есть 4-битный регистр, связанный с так называемым регистром «альтернативной функции». На внутреннем уровне контакт может быть подсоединён к другим периферийным компонентам (в данном случае – к I2C3, TIM1 и USART1), и число, которое вы записываете в этот регистр, определяет то, к какому именно периферийному компоненту этот контакт будет подключен.

Если заглянуть в даташите в таблицу 9 «Alternate function mapping» («Альтернативные функции контактов»), то вы увидите, что для того, чтобы включить TIM1_CH1 на контакте PA8, вам понадобится AF01 – так что Espruino запишет в этот регистр единицу.

  • Чипы STM32F1 (вроде того, что установлен на Espruino Original) чуть старее, и поэтому на них управлять альтернативными функциями чуть менее удобно. У каждого периферийного компонента есть регистр альтернативных функций, поэтому если вы, например, захотите включить TIM1_CH1N на контакте A7, то в регистре альтернативных функций TIM1 нужно задать «1» – но тогда TIM1_CH1 переедет на контакт E9 (вместе со всем остальным!).

Настроив контакт, Espruino займётся настройкой самого таймера:

  • Настроит предварительный делитель (предделитель) для тактовой частоты 84 МГц, которую генерирует Pico – так, чтобы 16-битный счётчик CNT отсчитывал от 0 до 65535 чуть меньше 10 раз в секунду (потому что мы задали частоту 10).
  • Настроит регистр автоматической перезагрузки (AutoReload Register или ARR) таким образом, чтобы он выполнил подстройку частоты. Но эта подстройка будет заключаться лишь в том, что отсчёт будет вестись до числа, которое будет чуть меньше 16-битного 65535 (одного лишь предделителя для точного результата недостаточно).
  • Задаст в регистре захвата/сравнения 1 (Capture/Compare register 1 или CCR1) значение регистра ARR, умноженное на коэффициент заполнения (в данном случае – 0.5).
  • Настроит регистр управления выводом (Output control) так, чтобы на канале TIM1_CH1 всегда была единица кроме случаев, когда значение в CNT меньше значения регистра CCR.
  • Включаем сам таймер, записав соответствующее значение в бит включения.

Что ж, готово. Теперь контакт A8 будет генерировать квадратную волну в 10 Гц.

Получение доступа к таймеру

Согласны, это была не самая лёгкая для усваивания информация. В принципе, в мануале все необходимые шаги подробно расписаны, но вам также не помешают знания о ШИМ (и желательно побольше).

Все действия выше Espruino выполняет при помощи модификации значений в регистрах периферийных устройств, а если точнее – в битах памяти в разных местах чипа, управляющих работой этих периферийных устройств.

В мануале есть раздел «12.4 TIM1 registers» – в нём описывается, какие биты за что отвечают.

Давайте получим доступ к регистру CRR1 – это регистр, в котором Espruino применяет коэффициент заполнения – и посмотрим, что он такое. Если заглянуть в подраздел «12.4.14 TIM1 capture/compare register 1 (TIMx_CCR1)», то там можно увидеть строчку «Address offset: 0x34» – это смещение адреса для TIM1.

Теперь нам надо найти адрес самого TIM1. Перейдите в раздел «2.3 Memory map» в мануале и посмотрите в таблицу 1. Для TIM1 там должно быть написано «0x40010000 - 0x400103FF».

Отсюда можно рассчитать, что адрес CCR1 – это «0x40010000 + 0x34», то есть «0x40010034». Теперь давайте вернёмся в раздел «TIM1 capture/compare register 1 (TIMx_CCR1)» – там говорится, что это 16-битное значение, так что мы воспользуемся функцией peek16().

Во-первых, давайте напечатаем var CCR1 = 0x40010034, чтобы сохранить адрес, а затем peek16(CCR1) – в ответ должно прийти «0», ведь мы пока не запускали analogWrite().

Возьмите макетную плату и подключите светодиод к контакту A8 при помощи резистора (примерно на 100 Ом или вроде того).

Затем запустите analogWrite(A8, 0.5, {freq:10}) – в результате светодиод должен начать мигать с частотой 10 раз в секунду, и всё это будет выполняться на аппаратном уровне.

Снова вызовите peek16(CCR1), чтобы узнать значение CCR1 – теперь вам должно прийти значение «32307».

Теперь давайте увеличим частоту мерцания при помощи analogWrite(A8, 0.05, {freq:10}), а затем снова вызовем peek16(CCR1). Значение будет в 10 раз меньше – «3230».

Мы можем использовать это значение, чтобы менять коэффициент заполнения.

  • poke16(CCR1, 0) задаст коэффициент заполнения «0» и выключит светодиод.
  • poke16(CCR1, 40000) сделает так, что светодиод большую часть времени будет включен.

Зная, что число «32307» в CCR1 означает коэффициент заполнения 50%, мы можем догадаться, до какого числа будет вести отсчёт счётчик CNT – это будет примерно 2 x 32307. Но мы можем узнать и точное значение – для этого нужно посмотреть в регистр ARR. В мануале говорится, что смещение адреса у него – это «0x2C», поэтому давайте сначала попробуем вызвать var ARR = 0x4001002C, а затем peek16(ARR). Получится «64615» – примерно как мы и думали.

Но это число можно поменять. Если сделать его меньше, то таймер будет вести отсчёт до более низкого значения, в результате чего светодиод будет мигать чаще.

Вызовите poke16(ARR, 30000) – теперь таймер должен отсчитывать в два раза быстрее. Но в то же время светодиод не будет мигать, а будет постоянно включен, потому что отсчёт будет вестись между 30000 и 0, а в CCR1 у нас задано 40000. Давайте зададим там что-то поменьше – например, poke16(CCR1, 1000) – и теперь светодиод начнёт моргать. Попробуйте поэкспериментировать с разными значениями для ARR, чтобы получить разные частоты.

Теперь давайте попробуем прочесть значение счётчика во время его работы. Отсчёт ведётся очень быстро, так что давайте снова воспользуемся analogWrite(), чтобы замедлить его:

analogWrite(A8, 0, {freq:0.1})

Теперь каждый новый отсчёт будет вестись в течение 10 секунд. В мануале видим, что счётчик CNT таймера TIM1 имеет смещение адреса 0x24 – давайте получим доступ к нему:

var CNT = 0x40010024;
peek16(CNT);

Теперь несколько раз нажмите клавиши  «Вверх»  и  ↵ Enter . Вы увидите в консоли примерно следующее:

>peek16(CNT);
=31807
>peek16(CNT);
=33273
>peek16(CNT);
=34583
>peek16(CNT);
=35900
>peek16(CNT);
=37161
>peek16(CNT);
=38458
>peek16(CNT);
=39767
>peek16(CNT);

Как видите, отсчёт ведётся от меньшего числа к большему.

Подсчёт того, сколько раз менялось состояние контакта

Всё это интересно, но не особо полезно. Но что если нам попробовать считать то, сколько раз было изменено состояние контакта?

Давайте снова взглянем на схему, чтобы увидеть, какие компоненты TIM1 нам понадобятся:

Итак, предположим, Espruino уже настроила таймер для analogWrite(). Что нам надо сделать?

  • Задать на контакте A8 правильное состояние.
  • Включить детектор фронтов.
  • Сделать так, чтобы 3 мультиплексора направляли сигнал от A8 в нужное место.
  • Выполнить сброс предделителя (предделителя) частоты, чтобы он вообще не делал никакого деления.
  • Выполнить сброс значения в регистре ARR до «0», чтобы вести отсчёт от 0 до 65535.

Итак, вперёд. Во-первых, подключаем контакт A8 к контакту A5 (он находится прямо напротив A8). Чуть позже мы начнём отправлять с контакта A5 импульсы, чтобы протестировать таймер.

Теперь давайте зададим адреса регистров (я, опять же, скопировал их из мануала):

// Регистр управления слейв-режимом (Slave mode control или SMCR):
var SMCR = 0x40010008;
// Регистр генерации событий (Event generation или EGR):
var EGR = 0x40010014;
// Регистр режима захвата/сравнения (CCMR1):
var CCMR1 = 0x40010018;
// Регистр включения каналов в режиме захвата/сравнения 
// (Capture/compare enable или CCER):
var CCER = 0x40010020;
// Счётчик (CNT):
var CNT = 0x40010024;
// Предделитель частоты (PSC):
var PSC = 0x40010028;
// Регистр автоматической перезагрузки (ARR):
var ARR = 0x4001002C;

Что ж, а теперь всё по порядку.

Задаём на контакте A8 правильное состояние

Вообще, в A8 уже задано правильное состояние. Он работает в режиме альтернативной функции и при помощи analogWrite() подключен к TIM1. Нам этого достаточно.

Включаем детектор фронтов

Найдите раздел «12.3.4 Clock selection», а в нём – подраздел «External clock source mode 1». В нём будет вот такая схема:

Здесь показано, что входом служит TI2, но TI1 – это, по сути, то же самое. Мы просто поменяем в коде «2» на «1».

Итак, нам надо:

  • Настроить канал 1 на определение фронтов сигнала на входе TI1. Для этого в регистр CCMR1 нужно записать CC1S = ‘01’.
  • Задать продолжительность работы фильтра, отредактировав биты IC1F[3:0] в регистре CCMR1 – нам фильтр не нужен вообще, так что пишем IC1F = 0000.

В мануале в разделе «12.4.7 TIMx capture/compare mode register 1 (TIMx_CCMR1)» есть таблица, из которой видно, что CC1S – это два самых младших бита регистра CCMR1, а IC1F – биты с 4 по 7.

Однако в том же разделе чуть ниже есть маленький текст, где говорится, что запись в биты CC1S можно делать, только когда канал выключен (CC1E = ‘0’ в TIMx_CCER).

Поскольку канал уже, очевидно, включен (потому что мы использовали его для ШИМ), нам нужно будет его выключить. Ищите в документации регистр CCER, нам понадобится в нём бит CC1E (это бит номер 0).

Итак, нам нужно модифицировать лишь эти биты. Для этого берём текущее значение, при помощи символов «&» и «~» применяем маску к старым битам и записываем новые значения при помощи символа «|». Вот так:

// CC1E[0] = 0 (выключаем канал 1):
poke16(CCER1, peek16(CCER1) & ~1);
// CC1S[1:0]=01 (передний фронт), IC1F[7:4]=0 (без фильтра):
poke16(CCMR1, (peek16(CCMR1) & ~0b11110011) | (0b00000001));

И затем задаём положительную полярность, записав в регистр CCER следующее:

  • CC1P=0
  • CC1NP=0

Согласно мануалу, CC1P – это бит 1, а CC1NP – это бит 3.

Но чуть ранее мы отключили канал, так что давайте также включим его:

// CC1P=0, CC1NP=0 (определяем передний фронт), 
// CC1E[0] = 1 (включаем канал 1):
poke16(CCER, peek16(CCER) & ~(0b1011) | (0b0001));

Делаем так, чтобы 3 мультиплексора направляли сигнал от A8 в нужное место

На этой же странице мануала говорится, что нам надо:

  • Настроить таймер в режим внешней частоты 1, записав SMS=111 в регистр SMCR.
  • Сделать TI1 источником входящего триггера, записав TS=101 в регистр SMCR
Примечание

Вообще, в мануале говорится, чтобы мы записали TS=110, чтобы сделать источником входящего триггера TI2, но нам нужен TI1.

SMS – это биты с 0 по 2, а TS – это биты с 4 по 6, поэтому:

// SMS[2:0]=111 (внешняя частота), TS[6:4]=101 (CH1 как триггер):
poke16(SMCR, (peek16(SMCR) & ~0b1110111) | 0b1010111);

В мануале также говорится, чтобы мы:

  • Включили счётчик, записав CEN=1 в регистр CR1.

Но Espruino уже сделала это за нас при помощи analogWrite():

Выполняем сброс регистров

Для этого понадобится вот такой простой код:

// В предделителе частоты задаём «0»,
// чтобы использовать каждый такт:
poke16(PSC, 0);
// Задаём в регистре ARR полный диапазон значений:
poke16(ARR, 65535);

Но в мануале также говорится следующее:

  • В регистре PSC содержится значение, которое будет загружаться в активный регистр предделителя при каждом событии обновления (включая ситуацию, когда в регистре TIMx_EGR будет выполняться сброс счётчика при помощи бита UG или при настройке контроллера триггеров при помощи «режима сброса»).

Поэтому нам также нужно будет сделать следующее:

// Выполняем запись в бит UG[0],
// чтобы выполнить сброс счётчика и обновить предделитель:
poke16(EGR, 1);

Всё это вместе будет выглядеть следующим образом:

// Регистр управления слейв-режимом (Slave mode control или SMCR):
var SMCR = 0x40010008; 
// Регистр генерации событий (Event generation или EGR):
var EGR = 0x40010014;
// Регистр режима захвата/сравнения 1 (CCMR1):
var CCMR1 = 0x40010018;
// Регистр включения каналов в режиме захвата/сравнения 
// (Capture/compare enable или CCER):
var CCER = 0x40010020;
// Счётчик (CNT):
var CNT = 0x40010024;
// Предделитель частоты (PSC):
var PSC = 0x40010028;
// Регистр автоматической перезагрузки (ARR):
var ARR = 0x4001002C;

// Включаем ШИМ на контакте A8 (TIM1 CH1 – таймер 1, канал 1):
analogWrite(A8,0.5,{freq:10});
// CC1E = 0 (выключаем канал 1):
poke16(CCER, peek16(CCER) & ~1);
// CC1S[1:0]=01 (передний фронт), IC1F[7:4]=0 (без фильтра):
poke16(CCMR1, (peek16(CCMR1) & ~0b11110011) | (0b00000001));
// CC1P=0, CC1NP=0 (определяем передний фронт), 
// CC1E[0] = 1 (включаем канал 1):
poke16(CCER, peek16(CCER) & ~(0b1011) | (0b0001));
// SMS[2:0]=111 (внешняя частота), TS[6:4]=101 (CH1 как триггер):
poke16(SMCR, (peek16(SMCR) & ~0b1110111) | 0b1010111);
// В предделителе частоты задаём «0»,
// чтобы использовать каждый такт:
poke16(PSC, 0);
// Задаём в регистре ARR полный диапазон значений:
poke16(ARR, 65535);
// Выполняем запись в бит UG[0],
// чтобы выполнить сброс счётчика и обновить предделитель:
poke16(EGR, 1);

Загляните в содержимое счётчика при помощи peek16(CNT) – должно вернуть «0».

Теперь попробуйте включить/выключить контакт A5 (ранее мы подключили его к A8) при помощи digitalWrite(A5,1) и digitalWrite(A5,0) – это должно увеличить значение счётчика.

Снова проверьте счётчик при помощи peek16(CNT) – теперь должно вернуть «1».

Теперь давайте попробуем сгенерировать много импульсов:

for (var i=0;i<1000;i++) {
  digitalWrite(A5,1);digitalWrite(A5,0);
}

Снова проверьте счётчик при помощи peek16(CNT) – теперь должно вернуть «1001».

Чтобы измерить частоту, можно воспользоваться функцией setInterval(). С её помощью можно узнать, насколько ежесекундно меняется значение в CNT – это и будет частотой квадратной волны на контакте A8.

TIM1 – это 16-битный таймер, что делает его применение немного ограниченным. Впрочем, есть и другие таймеры вроде TIM2 и TIM5, которые работают точно так же (просто поменяйте базовый адрес «0x40010000» на базовый адрес другого таймера), но зато оснащены 32-битными счётчиками. Единственное, нужно будет использовать peek32() вместо peek16() для считывания значения счётчика.

Захват на входе

Ещё одна полезная функция счётчика CNT – это захват на входе. В этом режиме вы можете задать для CNT любую частоту – при помощи analogWrite(A8, 0.5, {freq : my_frequency}) – но каждый раз, когда бит будет менять состояние, значение CNT будет сохранено в соответствующий регистр CCR.

Чтобы реализовать это, мы воспользуемся вот этой конфигурацией таймера:

В мануале об этом рассказывается в соответствующем разделе «12.3.6 Input capture mode» («Режим захвата на входе»), так что здесь некоторые подробности будут опущены.

Всё, что нам нужно будет сделать, это:

  • Включить и настроить счётчик (предделитель, ARR) при помощи analogWrite()
  • Перенастроить канал 1 на прослушку событий, а не генерацию ШИМ

Первый шаг, очевидно, делается просто. А чтобы выполнить второй шаг мы украдём немного из кода, который написали в разделе выше, но с одной маленькой модификацией. В этот раз мы воспользуемся предделителем IC1PS канала 1 – нам нужно будет задать в нём «0», чтобы отключить его. Для этого мы лишь зададим «0» в двух битах регистра CCMR1:

// Регистр статуса (SR):
var SR= 0x40010010;
// Регистр включения каналов в режиме захвата/сравнения 
// (Capture/compare enable или CCER):
var CCER = 0x40010020;
// Регистр режима захвата/сравнения 1 (CCMR1):
var CCMR1 = 0x40010018;
// Счётчик:
var CNT = 0x40010024;
// Регистр захвата/сравнения 1 (CCR1):
var CCR1 = 0x40010034;
// Включаем ШИМ на контакте A8 (TIM1 CH1 – таймер 1, канал 1):
analogWrite(A8,0.5,{freq:10});
// CC1E = 0 (выключаем канал 1)
poke16(CCER, peek16(CCER) & ~1);
// CC1S[1:0]=01 (передний фронт),
// IC3PSC[3:2]=00 (без предделителя), IC1F[7:4]=0 (без фильтра):
poke16(CCMR1, (peek16(CCMR1) & ~0b11111111) | (0b00000001));
// CC1P=0, CC1NP=0 (определяем передний фронт),
// CC1E[0] = 1 (включаем канал 1):
poke16(CCER, peek16(CCER) & ~(0b1011) | (0b0001));

Вот и всё. Увидеть увеличение счётчика можно при помощи peek16(CNT) – число, возвращаемое этой функцией, должно постоянно меняться.

Но если проверить значение в регистре захвата/сравнения 1 при помощи функции peek16(CCR1), она будет возвращать одно и то же число. Кроме того, при помощи peek16(SR)&2 можно узнать, было ли прерывание на CCR1 (при помощи бита CC1IF) – вам должно вернуть «0».

Но теперь – с условием, что контакты A8 и A5 по-прежнему подключены друг к другу – мы можем подать на A8 единицу и ноль и посмотреть, что получится:

digitalWrite(A5,1);digitalWrite(A5,0);

Теперь peek16(SR)&2 вернёт ненулевое значение, показывая, что что-то было поймано. И если мы заглянем в регистр захвата/сравнения при помощи peek16(CCR1), то увидим, что его значение изменилось.

Акт считывания значения в регистре CCR1 задаст в бите CC1IF значение «0», поэтому peek16(SR)&2 теперь тоже будет возвращать «0».

Но что если перед тем, как мы успеем прочесть последнее значение, таймер сработает дважды? Это мы тоже сможем определить – при помощи бита CC1OF и функции peek16(SR)&512.

Возможно, этот бит уже показывает переполнение, но его можно обнулить при помощи poke16(SR,peek16(SR)&~512). И теперь, если мы подадим на контакт два импульса, не считывая значение в CCR1, в бите переполнения CC1OF будет «1».

Ёмкостный тактильный датчик

Режим захвата на входе может быть ещё более полезен, если использовать его вместе с двумя каналами одного и того же таймера. Давайте воспользуемся контактом A10 на Pico (TIM1_CH3) для генерирования обычной ШИМ, но также активируем режим захвата/сравнения на контакте A8 при помощи кода выше. Это позволит нам измерить, сколько времени пройдёт между началом генерирования ШИМ на A10 и изменением состояния входного контакта A8 на единицу.

А это (помимо прочего) может пригодиться при создании тактильного датчика.

Отключите от макетной платы всё, кроме Pico, а затем подключите между контактами A10 и A8 резистор на 1 МОм (подойдёт и 100 кОм, но результат буде чуть хуже). Контакт A10 – это маленькое контактное отверстие, находящееся на противоположном от USB-разъёма конце платы (см. схему в разделе «Pinout» на этой странице), так что вам будет достаточно просто вставить ножку резистора в это отверстие. Теперь возьмите 10-сантиметровый провод, согните его в петлю и подключите к контакту A8. Итак, теперь к нашей схеме подключены очень мощный резистор и очень слабый конденсатор (петля из провода). Но если поднести к проводу руку, ёмкость этого самодельного конденсатора увеличится.

И что всё это значит? Увеличение ёмкости конденсатора означает, что теперь на его зарядку и разрядку будет требоваться больше времени (что делается через резистор, подключенный к A10). И при помощи режима захвата на входе мы можем это определить (не тратя время на вычисления в JavaScript).

Просто загрузите на Espruino вот этот код:

// Регистр статуса (Status Register или SR):
var SR= 0x40010010;
// Регистр включения каналов в режиме захвата/сравнения 
// (Capture/compare enable или CCER):
var CCER = 0x40010020;
// Регистр режима захвата/сравнения (CCMR1):
var CCMR1 = 0x40010018;
// Счётчик:
var CNT = 0x40010024;
// Регистр захвата/сравнения 1 (CCR1):
var CCR1 = 0x40010034;
var PSC = 0x40010028;

// Включаем ШИМ на контакте A8 (TIM1 CH1 – таймер 1, канал 1)
analogWrite(A8,0.5,{freq:1000});
// Включаем ШИМ на контакте A10 (TIM1 CH3 – таймер 1, канал 3)
analogWrite(A10,0.5,{freq:1000});
// CC1E = 0 (выключаем канал 1):
poke16(CCER, peek16(CCER) & ~1);
// CC1S[1:0]=01 (передний фронт),
//  IC3PSC[3:2]=00 (без предделителя), IC1F[7:4]=0 (без фильтра):
poke16(CCMR1, (peek16(CCMR1) & ~0b11111111) | (0b00000001));
// CC1P=0, CC1NP=0 (определяем передний фронт),
// CC1E[0] = 1 (включаем канал 1):
poke16(CCER, peek16(CCER) & ~(0b1011) | (0b0001));

function getCap() { return peek16(CCR1); }

Этот код идентичен коду из раздела «Захват на входе», но теперь:

  • Частота составляет 1000 Гц
  • Квадратная волна также генерируется на контакте A10
  • Чтение регистра CCR1 выполняется во вспомогательной функции

Итак, если теперь вызвать getCap(), то в ответ будет получено число, зависящее от мощности используемого резистора и ёмкости провода-конденсатора.

Но если вызвать getCap() в момент поднесения руки к проводу (на расстояние меньше сантиметра), то возвращаемое значение должно увеличиться. А если поместить getCap() в функцию setInterval() вот так...

// Это значение должно быть чуть выше того,
// которое функция getCap() возвращает,
// когда рука не поднесена к проводу-конденсатору:
var thresh = 300;

setInterval(function () {
  digitalWrite(LED1, getCap() > thresh);
}, 10);

...то у нас получится ёмкостный тактильный датчик!

См.также

Внешние ссылки