Хабрахабр

Автоматизация тибетских поющих чаш с помощью «Ардуино». Шаговый двигатель вместо монаха. Беспроводное программирование

И ПЕРЕДАЧА БОЖЕСТВЕННОЙ ВОЛИ СИГНАЛОВ ТОЧНОГО ВРЕМЕНИ ЧЕРЕЗ ESP8266.
ЧАСТЬ ЧЕТВЕРТАЯ

Сначала я увидел статью на «Гиктаймс» про шторы управляемые шаговым двигателем. Так уж всё совпало. Затем взгляд мой упал на поющую чашу, которая пылилась на полке уже лет пять. Вспомнил, что такой же двигатель валяется у меня без дела второй год. А затем в голову начали приходить разные умные мысли…

А хотелось мне чем-то заниматься параллельно, а чаша пусть бы в это время звучала сама. Нет, конечно иногда по настроению, я брал сию чашу в руки и некоторое время извлекал из нее разного рода чарующие звуки, но это было не совсем-то чего мне хотелось. Осталось только немного побыдлокодить. Понятно что тысячу лет назад на это потребовался бы отдельный раб человек, лет триста назад — хитроумный заводной механизм, а сейчас… Ну, а сейчас у нас есть и шаговый двигатель и плата «Arduino ProMini» и прочая незамысловатая электроника. А с ними пусть общается ESP8266, она это умеет. И заодно сделать так, чтобы чаша эта тибетская заодно отбивала точное время — зря, что ли наплодили столько серверов точного времени.

Итак…
Есть поющая чаша с колотушкой.

Автоматически. Надо сделать так чтобы колотушка била о край чаши. И чтобы просто отбивала время как старинные часы, но с современной точностью. Также с возможностью удаленного управления (и перепрограммирования!).

Смотреть лучше со звуком. Забегая вперед, покажу, что в итоге получилось.

Но начнём по порядку. Сначала мне надо было понять, как будет выглядеть и работать механика. За электронику и программное обеспечение я был спокоен — позади три статьи про то, как управляться с ардуинками на расстоянии.

Главным движущимся элементом должен был стать простенький шаговый двигатель 28YBJ-48 и мне надо было понять, сможет ли он справиться с колотушкой.

Надо было только обеспечить отдельное питание на 5 вольт и запасом на 200-300 мА, потому как преобразователя на самой ардуинке вам однозначно не хватит. Само подключение двигуна к ардуинке труда не представляет, благо, что продавался он с готовым драйвером ULN2003. Потом по любым четырем цифровым портам (я взял PB1, PB2, PB3, PB4) передаем следующие битовые тетрады в количестве восьми штук.

PORTB=0b00000010;//четыре старших бита не используются PORTB=0b00000110; PORTB=0b00000100; PORTB=0b00001100; PORTB=0b00001000; PORTB=0b00011000; PORTB=0b00010000; PORTB=0b00010010;

Для вращения в противоположном направлении передаем эти же тетрады, но в обратном порядке.

PORTB=0b00010010; PORTB=0b00010000; PORTB=0b00011000; PORTB=0b00001000; PORTB=0b00001100; PORTB=0b00000100; PORTB=0b00000110; PORTB=0b00000010;

Единственное, возникает вопрос, с какой скоростью передавать данные. Понятно, что чем чаще, тем быстрее будет вращаться вал двигателя, но до какого предела? В описании есть загадочная частота 100 Гц, но что именно она подразумевает — период полного цикла или каждый полубайт отдельно?

Максимально мне удалось разогнать эту частоту до 147 Гц, при которой вал двигателя делал оборот примерно за секунду или две. В процессе экспериментов выяснилось, что видимо, имелась в виду частота смены именно тетрад. Но для моей колотушки, вроде, в принципе, подходила. Точно не измерял, но можете судить сами, что особой резвостью данная модель с данным редуктором не отличается.

В постах посвященных этому движку утверждалось, что мол, рукой не остановишь. Но ведь нам важна не только скорость (вернее, даже не очень важна) сколько сила, с которой двигатель может воздействовать на рабочее тело. Как выяснилось, сам вал, да, не остановишь, но уже небольшой рычаг (а я решил использовать рычажную систему) длиной буквально 10 см, останавливается и очень просто буксует даже при небольшом местном воздействии.

Звук был слишком слабый. Поэтому первоначальный самый простой вариант, когда рычаг прикрученный к валу толкает колотушку на подвесе, которая соответственно колотит по чаше, не прошёл. В этом варианте рычаг тянул за собой колотушку до угла примерно в 30 градусов относительно направления к центру Земли, а затем расцеплялся с ней и отправлял в путь до чаши. Поэтому я решил призвать на помощь гравитацию (ту самую «бессердечную суку» по выражению Шелдона Купера). Механизм расцепления был сделан на магните, установленном на конце рычага. Получивший звук очень понравился, как мне, так и моим соседям снизу. Потом я сделал помогающий механический ограничитель — поперечную планку, с которой колотушка встречалась вблизи крайней точки подъема. По мере подъема сила тяжести побеждала магнитную и замок расцеплялся. Здесь движку помогала гравитация, поэтому усилие для расцепления требовалось совсем небольшое. Двигатель же продолжал вращение, рычаг тянул и принудительно расцеплял магнитный замок.

Его я купил уже давно и периодически пользовался его детальками для своих поделок. Сама конструкция была собрана на базе деталек детского конструктора «Эйфелева башня». Башня, конечно, получилась не Эйфелева, но на мой взгляд отнюдь не хуже:)

Почти Эйфелева Башня

Всё заработало прекрасно, но с единственным минусом — звук был всегда одной и то же силы. Для отбития времени это нормально, но в свободном режиме хотелось бы слышать не только разные по времени паузы, но и разные по силе звуки. Поэтому пришлось применить электромагнит, тоже завалявшийся весьма кстати. Обычные магниты тоже пригодились — столбик из пяти мелких магнитов я использовал как демпфер укрощающий колебания колотушки после удара о чашу.

Поэтому электромагнит переехал на колотушку. Сначала я установил его на конец рычага, но конструкция оказалась громоздкой хлипкой и ненадёжной. Пришлось на небольшой макетке разместить простой транзисторный ключ. Потреблял он примерно 300 мА и естественно, управлять им от порта ардуино было невозможно.

R1 – 560 Ом, VD1 – 1N4007, VT1 – BD139

В итоге, у меня, как обычно, появилась возможность программировать ардуинку по беспроводному каналу и также удаленно с ней общаться, обмениваясь данными, чем я в итоге успешно и воспользовался. Главную электронную часть я собрал на «Arduino ProMini» и модуле ESP8266-07, прошивку которого я выполнил полностью шаг за шагом по моей старой статье. На схеме изображена, правда, «Arduino Nano» по историческим причинам, но подключение её ничем не отличается.

Итак, что же я возжелал и затем воплотил в программном коде.

  1. При включении система должна самостоятельно перейти в режим часов.
  2. На компьютере (смартфоне) должно быть приложение для изменения режимов работы и передачи необходимых данных.
  3. Режимы должны быть несложные — часы, случайное бумканье и ручное управление.

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

Собранные электронные часы у меня уже были.

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

Засмеют ведь. Я даже хотел запилить статью на эту тему пару лет назад, но, посмотрев, сколько раз это уже делалось, передумал. Как я уже упоминал ранее в статьях, программки для ESP8266 я пишу на языке LUA. А вот в контексте данного поста разбор их работы вполне уместен. Так получилось.

Поэтому код загруженный в тот модуль ESP был таким.

uart.setup(0,9600,8,0,1,0) timezone = 3 -- москва tmr.alarm(1,5000,0,function() -- try once connect to NTP-server sk=net.createUDPSocket() sk:send(123,"130.149.17.21",string.char( 227, 0, 6, 236, 0,0,0,0,0,0,0,0, 49, 78, 49, 52, 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0)) sk:on("receive", function(sck, payload) ntp = payload:byte(41) * 128 * 256 * 256 + payload:byte(42) * 128 * 256 + payload:byte(43) * 128 + payload:byte(44) /2 + timezone * 1800 hour =ntp % 43200 / 1800 minute = ntp % 1800 / 30 secund = ntp % 60 uart.write(0,hour) uart.write(0,minute) uart.write(0,secund) sk:close() end ) end)

Суть там простая. Однократно (или нет) вызывается функция учреждающая UDP клиент, который обращается к серверу точного времени и просит у того соответственно точное время. В ответ сервер вываливает тридцать два байта, из которых надо выудить искомые четыре байта данных. К сожалению, это искомое представляет собой не минуты и часы, а количество секунд прошедших на данный момент аж с 1 января 1900 года. Поэтому потом вам придётся из четырёх байт этих секунд разными сложными манипуляциями вычислять текущее время.

Запускаете UART передатчик и валите в него тремя байтами вычисленное время — часы, минуты и секунды. Дальше, всё проще.

И я снова вставил этот код, уже в свой LUA загрузчик (ссылка), как раз в то место, где подключение к сети WI-FI уже осуществлено, но дальнейшая работа ещё не началась.

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


function InstrProgrammingEnable () -- instruction for MC "enable programming" p=0
while p<31 do
p=p+1 pin=8 gpio.write(pin, gpio.LOW)
spi.send(1, 0xAC,0x53)
read = spi.recv( 1, 8)
spi.send(1,0,0)
gpio.write(pin, gpio.HIGH) if (string.byte(read)== 83) then --print("connection established") p=33 if(p==31) then --print("no connection") end end end
end function ProgrammingDisable ()
pin=2--END OF ESET FOR MK GPIO4
gpio.mode(pin, gpio.INPUT) pin=8 gpio.mode(pin, gpio.INPUT) -- CE chip enable not used GPIO15 pin=5--CLK MASTER for SPI GPIO14 used
gpio.mode(pin, gpio.INPUT) pin=6--MISO MASTER for SPI GPIO 12 may not used
gpio.mode(pin, gpio.INPUT) pin=7--MOSI MASTER for SPI //GPIO13 used
gpio.mode(pin, gpio.INPUT)
end --PROGRAMMING ENABLE function ProgrammingEnable ()
pin=2-- RESET FOR MK
gpio.mode(pin, gpio.OUTPUT)
gpio.write(pin, gpio.LOW) pin=2--POZITIV FOR 4MSEC RESET FOR MK
gpio.mode(pin, gpio.OUTPUT)
gpio.write(pin, gpio.HIGH) tmr.delay(4)
gpio.mode(pin, gpio.OUTPUT)
gpio.write(pin, gpio.LOW) tmr.delay(25000)
end function InstrFlashErase() --FFFFFFFFFFFFFFFFFF
pin=8 gpio.write(pin, gpio.LOW)
spi.send(1,0xAC,0x80,0,0)
gpio.write(pin, gpio.HIGH)
tmr.delay(15000) pin=2--RESET FOR MK
gpio.mode(pin, gpio.OUTPUT)
gpio.write(pin, gpio.HIGH)
tmr.delay(20000)
gpio.write(pin, gpio.LOW) --print( "FLASH is erased")
InstrProgrammingEnable () end function InstrStorePAGE(H, address, data)
pin=8 gpio.write(pin, gpio.LOW)
spi.send(1,H,0,address,data)
gpio.write(pin, gpio.HIGH)
tmr.delay(500)
end function InstrWriteFLASH(page_address_low,page_address_high)
pin=8 gpio.write(pin, gpio.LOW)
spi.send(1,0x4C,page_address_high,page_address_low,0)
gpio.write(pin, gpio.HIGH)
tmr.delay(5000)-- иногда не прописываются флэш при малых задержках
end function Programming (payload) pin=8--CS MASTER for SPI
gpio.mode(pin, gpio.OUTPUT, gpio.PULLUP)
pin=4--LED LIGHTS ON LOW
gpio.mode(pin, gpio.OUTPUT)
gpio.write(pin, gpio.LOW)
--print(string.len(payload))
page_count = 7 -- пишем 1 килобайт for k =0 ,page_count ,1 do--quantity of pages for i=0 , 127, 2 do-- -1 address = i/2 data=payload:byte(i+1+128*k) if data == nil then data = 0xff end InstrStorePAGE(0x40,address,data) -- tmr.delay(100)-- otherwise not in time write data =payload:byte(i+1+1+128*k) if data == nil then data = 0xff end InstrStorePAGE(0x48,address,data)
-- tmr.delay(100) end page_address_low=bit.band(k ,3)*64 -- 3 это двоичное 11
page_address_high=k/4+frame1024*2 tmr.delay(1000)
InstrWriteFLASH(page_address_low,page_address_high)
tmr.wdclr()
end pin=4--LED
gpio.mode(pin, gpio.OUTPUT)
gpio.write(pin, gpio.HIGH)
end --MAIN BLOCK wifi.setmode(wifi.STATION)
--wifi.sta.config("mixa","M1sh8111") -- set SSID and password of your access point
station_cfg=
tmr.delay(30000)
station_cfg.ssid="mixa"
tmr.delay(30000)
station_cfg.pwd="M1sh8111"
tmr.delay(30000)
wifi.sta.config(station_cfg)
tmr.delay(30000)
wifi.sta.connect()
tmr.delay(1000000) --print(wifi.sta.status())
--print(wifi.sta.getip()) while ( wifi.sta.status()~=1 ) do
if( wifi.sta.status()==5)
then
break
end
end uart.setup(0,9600,8,0,1,0) -- добавление блока для получения NTP времени и отправка ея на AVR
timezone = 3 -- москва tmr.alarm(1,5000,0,function() -- try once connect to NTP-server sk=net.createUDPSocket() sk:send(123,"130.149.17.21",string.char( 227, 0, 6, 236, 0,0,0,0,0,0,0,0, 49, 78, 49, 52, 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0)) sk:on("receive", function(sck, payload) ntp = payload:byte(41) * 128 * 256 * 256 + payload:byte(42) * 128 * 256 + payload:byte(43) * 128 + payload:byte(44) /2 + timezone * 1800 hour =ntp % 43200 / 1800 minute = ntp % 1800 / 30 secund = ntp % 60 uart.write(0,100)--перевод AVR в режим часов uart.write(0,hour) uart.write(0,minute) uart.write(0,secund) sk:close() end ) end) prog_address=""; sv=net.createServer(net.TCP,30)
tmr.delay(100) --print("SERVER READY") sv:listen(40000,function(c)--Главный сервер, работает всегда c:on("receive", function(c, payload) --print(payload) if (payload =="program\r\n") then c:send("ready\r\n") --print("ready for program\r\n") tmr.wdclr() spi.setup(1, spi.MASTER, spi.CPOL_LOW, spi.CPHA_LOW, spi.DATABITS_8,80,spi.FULLDUPLEX) -- настройка SPI 320 примерно 115 000 кБод ProgrammingEnable ()--------------------------------------------------------------------- на 80 еще работает это 1 мбит tmr.delay(100) InstrProgrammingEnable () tmr.delay(100) InstrFlashErase() tmr.delay(100) frame1024=0--номер переданного фрейма st=net.createServer(net.TCP,30)--Сервер для приема файла программы и трансляции ее в AWR, выключается командой stop program st:listen(40001,function(c) c:on("receive", function(c, payload) tmr.wdclr() Programming (payload) frame1024=frame1024+1 end) end) end if (payload =="data\r\n") then tmr.wdclr() c:send("ready\r\n") -- print("ready for data\r\n") c:on("receive", function(c, prog_address_payload) prog_address=prog_address_payload--получаем IP адрес UDP хоста для отправки к нему данных -- print(prog_address) c:send(prog_address) srv=net.createUDPSocket()-- Сервер для приема данных , выключается командой data stop srv:listen(50000) -- uart.setup(0,9600,8,0,1,0) srv:on("receive", function(srv, pl) -- принимаем данные с компьютера по UDP pl=pl*1 -- print(pl) uart.write(0,pl) -- отправляем их по UART на AVR end) uart.on("data", 1, function(data) -- принимаем данные по UART из AVR srv:send(50000,prog_address,data) -- отправляем их по UDP на компьютер end, 0) tmr.wdclr() end) end if (payload =="stop data\r\n") -- здесь закрываем ненужные уже сервера then ready = false if(srv~=nil) then srv:close() -- print("stop data") end collectgarbage() end if (payload =="stop program\r\n") then if(st~=nil) then st:close() frame1024=0 ProgrammingDisable () -- print("stop program") end collectgarbage() end end) end)

Конечно, это идет вразрез с моей концепцией, где ESP8266 это чистый беспроводной мост, а микроконтроллер ATMEL всё остальное, но как говорится: « один раз, не п… ».

Во-первых, нечего нагружать сеть, а во-вторых, ATMEL теоретически позволяет отсчитывать секунды с неплохой точностью. Итак, начальное точное время мы получили (напрямую от NTP сервера или опосредованно через приложение на компьютере, не важно), дальше хотелось бы считать время самим. А вот практически, встречаются подводные камни. Теоретически, да.

Небольшое отступление про часы реального времени на AVR.

Самые оголтелые конструкторы даже пихают для этого в схему часовой кварц на 32768 Гц. По идее, в построении часов на микроконтроллере AVR нет ничего сложного. По сути, часовой кварц необходим, чтобы кратно секунде формировать прерывание и будить спящий (обратите внимание) микроконтроллер. Но на самом деле этого делать не надо. Вполне можно использовать кварцевый резонатор, который уже есть, на восемь там или шестнадцать мегагерц. Если у вас устройство работает постоянно, а часы обычно так и делают, то ставить дополнительный кварц к уже имеющемуся и отнимать под него две ноги ввода-вывода безрассудно. Его точности квантования вам хватит за глаза, а посчитать одну секунду таймером-счетчиком тоже будет несложно.

Как известно, входной тактирующий сигнал (допустим 8 МГц) поступает внутри чипа (допустим AVRmega328P как самого ходового для ардуинок) на, так называемый предделитель, где он по желанию программиста может делиться дальше (обычно на 8, 64, 256, 1024). На самом деле в AVR микроконтроллере всё для этого уже имеется. А уже затем он поступает на какой-нибудь таймер-счетчик (допустим Т1), который начинает тут же инкрементироваться.

Получим соответственно частоту тактирования счетчика 31250 Гц. Итак, возьмем 8 МГц и поделим на 256. Что нам и надо. Соответственно, коль скоро счетчик Т1 шестнадцатиразрядный и может считать соответственно аж до 65535, то досчитать до 31250 он как раз успеет за одну секунду. Если мы туда как раз и запишем число 31250, то при определенных условиях оно будет постоянно сравниваться с содержимым счетчика Т1 и наконец, когда сравняется, счетчик выдаст сигнал прерывания, мол нате, держите вашу секунду. Кроме этого наш таймер имеет еще один очень полезный регистр сравнения.

Ибо счетчик наш будет считать с ошибкой квантования 256/ 8 000 000, что дает немаленькую ошибку исчисления одной секунды в целых 32 микросекунды. Получается удобно, но, к сожалению, не совсем точно. Но, к сожалению, в этом случае счетчику надо будет досчитать до 125 000, а такое число в шестнадцати разрядный регистр не войдёт. А это приводит к ошибке 2,8 секунды в сутки (0,000032 * 3600 * 24).
А вот, если мы поделим исходные 8 МГц на меньшую величину, например на 64, то точность квантования возрастет в 4 раза до 8 мкс и уменьшит итоговую ошибку до 0,33 секунд в сутки. Придётся писать в регистр сравнения число поменьше ( 62500 ещё влазит) ) и добавлять цикл в самой программе, где одна секунда будет считаться уже не по одному, а по двум прерываниям.

Нет, вообще, если смотреть по даташитам на стандартные кварцы, то теоретически не всё так плохо. Но это мы взяли случай идеальный, а реальный кварцевый резонатор, особенно установленный на плате «made in China» способен принести вам немало сюрпризов.

У него есть нестабильность собственной настройки в 25 ppm (или иными словами 25 миллионных частей), то есть он будет резонировать на частоте не 8 МГц, а, к примеру, на частоте 8, 0002 МГц, что даст нам целых 2,1 секунды ошибки в сутки. Как мы видим, кварц средней ценовой категории ведет себя вполне прилично. Такой кварц также может плавать по температуре 5-10 ppm на градус, но при комнатных условиях работы устройства, ошибка тоже будет небольшой. Но это постоянная погрешность и ее можно учесть. Или десять. Есть еще такой фактор как старение, но он совсем мизерный и меняет характеристики кварца до состояния хоть какой-то заметности, ну может, лет за пять.

И вот мы радостные берём какой-нибудь китайский клон ардуино, к примеру ARDUINO UNO.

Отставание в час на минуту? Запускаем на нём тестовую программку подсчета времени и опупеваем. Вторая плата Arduino UNO? Легко! Ничуть не лучше.

Берём Arduino ProMini.

Ошибка уменьшилась до двадцати секунд в час. А вот здесь получше, да. Ну, уже сравнимо с механическими часами с кукушкой.

Последняя плата, которая оказалась у меня под рукой была Arduino Nano.

И она единственная показала более-менее вменяемые результаты.

Плату надо настраивать и я, вздохнув, полез за осциллографом. Но даже с такой платой, используя только теоретические построения, сами понимаете, точные часы не сделаешь.

Типа, раз порт занят под кварц, то и нефиг к нему цепляться. Как оказалось, ардуино платы обладают неприятной особенностью – вывод, к которому подсоединён кварцевый резонатор, не имеет выхода на гребенку выводов, хотя и соответствует порту PB7. Но даже присоединение к нужной ножке мне ничего не дало. А просто к ноге микроконтроллера весьма затруднительно подцепится щупом осциллографа, ибо поверхностный монтаж и шаг 0,5 мм между выводами. Поэтому пришлось идти обходными путями – поставить предделитель на минимальный коэффициент деления — единицу, записать в регистр сравнения ноль, чтобы прерывание дергалось сразу же и ввести микроконтроллер в специальный режим, в котором ножка порта PB1 меняет свое логическое состояние при каждом таком прерывании.
Логически рассуждая, при включении платы Arduino Nano 16 МГц, на выходе этого порта должен появиться меандр с частотой 8 МГц. То ли потому, что неправильно тыкал, то ли тыкал совсем не туда, поскольку вывод кварцевого резонатора, может быть, совсем не есть вывод тактового генератора и вообще, он внутри самого микроконтроллера.

Осциллограф показал частоту 8. Что и случилось. Причем последний разряд жил своей жизнью и я так и не понял, то ли не хватает точности осциллографа, то ли это так плавает частота кварцевого генератора. 002 31 МГц. Больше походило на второе.

Если на плату подышать (может, кстати, емкости от влажности ещё едут?) или поднести (издалека) паяльник, то кварц мог отъехать сразу на полсотни герц. Хорошей термостабильностью там тоже не пахло. И эти измерения ещё загрублены в два раза, поскольку исходная частота 16 МГц.

Что и дает нам предельную точность часов собранных на таких платах не более чем в одну секунду в сутки. Таким образом, в ардуино платах (по крайней мере тех, что китайского происхождения) добиться точности более 200 Гц при тактовой частоте 16 МГц невозможно. И это ещё хорошо.

А они весьма распространены, ибо дешевы и удобны. Потому что есть китайские клоны Arduino UNO, уже упомянутые мною ранее, с которыми, вообще всё плохо.

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

И в описаниях продажников тоже. Загадка начинается ещё с того, что на самом кварце написано 12 МГц!

Если вы включите на плате последовательный порт UART, то вы сами в этом убедитесь. Но там не 12 МГц, это совершенно точно. А настроенный на частоту 16 МГц – будет. Поскольку UART настроенный на эту частоту у вас работать не будет. У первой платы частота генератора была 15. Да более того, я лично смотрел осциллограммы на обоих имеющихся у меня платах Arduino Uno. 8661 МГц. 8784 МГц, а у второй 15.

Поэтому предположение, что там внутри не кварц, а плохо настроенная RC-цепочка, не оправдалось. Но потом внезапно оказалось, что кварц 12 МГц не имеет прямого отношения к AVR микроконтроллеру, а предназначен для работы последовательного порта с компьютером по USB (чтобы скетчи загружать). Но он очень маленький и надписи на нём нет. А нужный нам кварц гораздо менее велик размерами и находится рядом с чипом микроконтроллера.

Но, видимо, в Китае возможно всё. В итоге я так и не смог понять, как и где можно найти кварцевые резонаторы настолько мерзейшего качества. Ладно, софт можно и даже нужно написать самому, а что вот делать с качеством самих модулей? И как-то я задумался о смельчаках использующих ардуинки для серьезных дел. Судя по всему, из электронных компонентов китайцы пихают в них всё самое дешёвое и забракованное.

Программа «Поющая чаша”для AVR.

В итоге, победив все трудности с точным исчислением времени, я написал следующий код для своей Arduino ProMini

Программа на С для микроконтроллера AVRmega328P

/* * Tibetian_Bowl.c * * Created: 07.06.2018 0:29:57 * Author: User */ #define F_CPU 8000000 #include <avr/io.h> #include <avr/interrupt.h> #include <stdint.h>// стандартные целые числа
#include <math.h> // математика
#include <stdio.h> //стандартный ввод-вывод
#include <avr/eeprom.h> #include <stdbool.h>
#include <setjmp.h> #include <stdlib.h> volatile bool change_mode = false; volatile bool boom =false;
volatile bool go_ahead=true;
volatile bool go_back=false;
volatile bool gerkon=false; volatile uint8_t latency=2;// максимально возможная скорость при latency = 1 volatile uint8_t hour=12;
volatile uint8_t hour24=12;// переменная для перевода времени в формат 12
volatile uint8_t minute=0;
volatile uint8_t secund=0;
volatile uint8_t power=0;
volatile uint8_t pause_between_boom=0; volatile uint8_t first_byte=0;
volatile uint8_t second_byte=0;
volatile uint8_t third_byte=0;
volatile uint8_t firth_byte=0;
volatile uint8_t fifth_byte=0;
volatile uint8_t cSREG; ISR(USART_RX_vect) { // пишем в буфер пятибайтовую последовательность, где // первый байт – код команды, остальные данные или нули. if (first_byte==0) { first_byte=UDR0; change_mode=true; goto ret; } if (second_byte==0) { second_byte=UDR0; goto ret; } if (third_byte==0) { third_byte=UDR0; goto ret; } if (firth_byte==0) { firth_byte=UDR0; goto ret; } if (fifth_byte==0) { fifth_byte=UDR0; goto ret; } cSREG=UDR0; ret: return; } ISR(PCINT1_vect )//PC2 int 10 //вход сигнала с геркона
{ if (go_ahead) { UDR0=44; // ошибка позиционирования авария код ошибки 44 } if (go_back) { gerkon=true; } } ISR(TIMER1_COMPA_vect)
{ // здесь инкрементируем счетчик секунд и делаем часы secund++; if (secund ==60) { secund=0; minute++; if(minute==60) { minute=0; hour++; if(hour==12) { hour=1;// чтобы било не более 12 раз } hour24++; if(hour24==24) { hour24=1; } boom=true; } } } void time_delay(long dell)// передается время для задержки в миллисекундах { long i; dell=dell*796;//частота кварца 8 мгц for(i=0;i<dell;i++){;;}; sei();// если убрать сеи , то почему-то программа компилируется без функции.WTF ??????????????????????
} void turn_onkward()// функция для продвижения рычага к чаше
{ uint8_t legnth=170;// длина хода рычага (от 0 до 170) for(uint16_t i =0;i<=legnth;i++) { go_ahead=true; PORTB=0b00000010;// выдаем значения для работы шагового двигателя time_delay(latency); PORTB=0b00000110; time_delay(latency); PORTB=0b00000100; time_delay(latency); PORTB=0b00001100; time_delay(latency); PORTB=0b00001000; time_delay(latency); PORTB=0b00011000; time_delay(latency); PORTB=0b00010000; time_delay(latency); PORTB=0b00010010; time_delay(latency); if (i>140) { PORTD |=(1<<PORTD2);// транзисторный ключ управляющий ЭМ , 1 - включение магнита } } time_delay(100); go_ahead=false; } void turn_backward(uint8_t pause, uint8_t force_of_sound)//функция для продвижения рычага от //чаши
//передается время паузы между ударами в секундах
{ uint8_t legnth=170;// длина хода рычага в абстрактных единицах (от 0 до 170) for(uint16_t i =0;i<=legnth;i++) { go_back=true; PORTB=0b00010010; time_delay(latency); PORTB=0b00010000; time_delay(latency); PORTB=0b00011000; time_delay(latency); PORTB=0b00001000; time_delay(latency); PORTB=0b00001100; time_delay(latency); PORTB=0b00000100; time_delay(latency); PORTB=0b00000110; time_delay(latency); PORTB=0b00000010;//16 ms на весь цикл, если latency = 2 time_delay(latency); if (i==force_of_sound*17) { PORTD &=~(1<<PORTD2);// транзисторный ключ управляющий ЭМ , 0 - выключение магнита } if (gerkon) { gerkon=false; break; } } time_delay(50); time_delay(pause*1000);// время паузы перед противоходом в с go_back=false; } void sound(uint8_t force,uint8_t pause) // передается сила удара по чаше от 1 до 10 в целых числах и интервал между следующим ударом в секундах { turn_onkward(); turn_backward(pause,force); } int main(void)
{ sei(); //инициализация UART на 9600 при частоте кварца 8 мгц time_delay(2000);//первую секунду полторы, esp может высыпать в консоль какой-то мусор UCSR0A=0; UCSR0B=0b10011000;//разрешение прерывaния UART UCSR0C=0b00000110; UBRR0L=51;// 8 мгц 9600 СКОРОСТЬ UART UBRR0H=0; //инициализация внешнего прерывания INT0 на порту С2 номер прерывания 10 // порт на вход для прерывания от геркона PCICR|=(1<<PCIE1);// разрешение группы прерываниС14-8 PCMSK1|=(1<<PCINT10);// разрешение конкретного прерывания INT10 DDRC&=~(1<<PORTC2); DDRB=0b00111110;//PB1-PB4 выходы для шагового двигателя, PB5 выход на светодиод на плате DDRD=0b00000100; // PD2 на выход для управления электромагнитом //SET INTERRUPT FROM TIMER1 AND SET TIMER1 GTCCR=0;//RESET PRESCALER TCCR1A=0;//I/O NORMAL WORK TCCR1C=0; TCCR1B=0B00001100;//1/256 PRESCALING AND CTC MODE TCNT1H=0;//RESET TIMER1 TCNT1L=0; TIMSK1=0B00000010;//SET COMPARE A INTERRUPT ENABLED OCR1AH=0x79;//SET TIME CONSTANT IN COMPARE REGISTER OCR1AL=0xa7;// 31143 для ДАННОГО КВАРЦА 7 972 608 Герц TCCR0B=0b00000010;//запуск 8 битного счетчика для генерации случайных чисел от 0 до 255 while (1) { begining: time_delay(1000); if (first_byte!=0) { UDR0=first_byte;// отправляем обратно на комп первый байт. Если это код команды (100,101,102) то все зашибись } if (first_byte==100)//это режим часы (неважно с компа или с NTP сервера { hour=second_byte;//получаем часы if (hour>12)// перевод на отбитие не более 12 ударов (24 устанешь считать) { hour=hour-12; } if (hour==0) { hour=12; } minute=third_byte;//получаем минуты secund=firth_byte;//получаем секунды power=fifth_byte;//получаем силу звука first_byte=0;// обнуляем буфер second_byte=0; third_byte=0; firth_byte=0; fifth_byte=0; change_mode=false; goto clock_mode; } if (first_byte==101)//это случайный режим { power=second_byte; pause_between_boom=third_byte; first_byte=0; second_byte=0; third_byte=0; firth_byte=0; fifth_byte=0; change_mode=false; goto random_mode; } if (first_byte==102)//ручное управление { power=second_byte; first_byte=0; second_byte=0; third_byte=0; firth_byte=0; fifth_byte=0; change_mode=false; goto hand_mode; } //если ни одна команда не разпознана, обнуляем и запускаем все сначала first_byte=0; second_byte=0; third_byte=0; firth_byte=0; fifth_byte=0; goto begining; clock_mode: while(change_mode==false) { if (boom)// отбитие часов { for(uint8_t i =0;i<hour;i++) { if ((hour24>21)|(hour24<10))//ночное время { sound(3,0);// сила удара 10 (макс), пауза 0 секунда boom=false; } else { sound(power,0);// сила удара 10 (макс), пауза 0 секунда boom=false; } } } } goto begining; random_mode: while(change_mode==false) { uint8_t random_power = TCNT0;// берем показания младшего байта счетчик Т1 uint8_t random_pause = TCNT1L;// берем показания младшего байта счетчик Т1 random_pause=TCNT0;// берем показания младшего байта счетчик Т1 random_power=random_power/25; if (random_power<5) { random_power=random_power+2;// чтобы сильно слабенько не звучало } random_pause=(random_pause/25)+pause_between_boom; UDR0=random_pause; time_delay(100); sound(random_power,random_pause); } goto begining; hand_mode: sound(power,0); goto begining; } }

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

100 режим часов
101 режим случайный
102 режим ручной.

Как уже упоминалось, ESP модуль цепляется к сети, тащит оттуда с NTP сервера точное время и отправляет его на микроконтроллер. Поскольку AVR всё равно, откуда команда, то первой после включения срабатывает команда от ESP8266. По прерыванию таймера-счетчика Т1 считаются секунды, минуты и часы и в случае необходимости вызываются функции для приведения в движение туда-обратно шагового двигателя, дабы отбить время. Таким образом, сначала ардуинка переходит в режим отбития часов.

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

Приложение для компьютера.

Основано всё так же, на тех же самых старых программах, здесь меняется только визуальное представление.

Затем по необходимости отправляется нужная команда управления и сопутствующие данные в виде UDP пакетов. Всё также поднимается канал связи с AVR через HTTP и UDP соединения. И таки да, иногда (редко) AVR их путает. Конечно, правильнее было бы развести управление и данные по разным каналам, но, во-первых, для этого надо править код LUA в загрузчике, а во-вторых, смысла в этом нет никакого, поскольку на микроконтроллер и команды и данные поступают по одному и тому же UART. Но это не страшно, так как если, микроконтроллер не распознает команду, то он её и не выполнит, да ещё наябедничает про это приложению на компе, которое в свою очередь предложит вам повторить ввод.

Код доступен на Гитхабе.

ПОСТСКРИПТУМ

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

Теги
Показать больше

Похожие статьи

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *

Кнопка «Наверх»
Закрыть