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

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

gxau0jnutvametc-igezo5zkpvk.jpeg

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

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

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

tkd_hy9acctcthkplro1s8fs20k.jpeg

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

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

vlktsnvvvzlckt-wjpsmztiqtqs.jpeg

Само подключение двигуна к ардуинке труда не представляет, благо, что продавался он с готовым драйвером ULN2003. Надо было только обеспечить отдельное питание на 5 вольт и запасом на 200–300 мА, потому как преобразователя на самой ардуинке вам однозначно не хватит. Потом по любым четырем цифровым портам (я взял 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 градусов относительно направления к центру Земли, а затем расцеплялся с ней и отправлял в путь до чаши. Получивший звук очень понравился, как мне, так и моим соседям снизу. Механизм расцепления был сделан на магните, установленном на конце рычага. По мере подъема сила тяжести побеждала магнитную и замок расцеплялся. Потом я сделал помогающий механический ограничитель — поперечную планку, с которой колотушка встречалась вблизи крайней точки подъема. Двигатель же продолжал вращение, рычаг тянул и принудительно расцеплял магнитный замок. Здесь движку помогала гравитация, поэтому усилие для расцепления требовалось совсем небольшое.
Сама конструкция была собрана на базе деталек детского конструктора «Эйфелева башня». Его я купил уже давно и периодически пользовался его детальками для своих поделок. Башня, конечно, получилась не Эйфелева, но на мой взгляд отнюдь не хуже:)

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

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

vsyvgb5eqgcdqze2hyhxesgya9k.jpeg

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

sm2jw69rz-nfepnkzp99bebnn9i.png

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

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

cebu6dvlctxn46bgg2cu5k6cnlc.png

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

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

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

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

lw5ueutxi1xyqmu9bz8r6vpy-e0.jpeg

И главной их фишкой пригодившейся мне сейчас, было их умение таскать точное время с 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 всё остальное, но как говорится:» один раз, не п… ».

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

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

По идее, в построении часов на микроконтроллере AVR нет ничего сложного. Самые оголтелые конструкторы даже пихают для этого в схему часовой кварц на 32768 Гц. Но на самом деле этого делать не надо. По сути, часовой кварц необходим, чтобы кратно секунде формировать прерывание и будить спящий (обратите внимание) микроконтроллер. Если у вас устройство работает постоянно, а часы обычно так и делают, то ставить дополнительный кварц к уже имеющемуся и отнимать под него две ноги ввода-вывода безрассудно. Вполне можно использовать кварцевый резонатор, который уже есть, на восемь там или шестнадцать мегагерц. Его точности квантования вам хватит за глаза, а посчитать одну секунду таймером-счетчиком тоже будет несложно.
На самом деле в AVR микроконтроллере всё для этого уже имеется. Как известно, входной тактирующий сигнал (допустим 8 МГц) поступает внутри чипа (допустим AVRmega328P как самого ходового для ардуинок) на, так называемый предделитель, где он по желанию программиста может делиться дальше (обычно на 8, 64, 256, 1024). А уже затем он поступает на какой-нибудь таймер-счетчик (допустим Т1), который начинает тут же инкрементироваться.
Итак, возьмем 8 МГц и поделим на 256. Получим соответственно частоту тактирования счетчика 31250 Гц. Соответственно, коль скоро счетчик Т1 шестнадцатиразрядный и может считать соответственно аж до 65535, то досчитать до 31250 он как раз успеет за одну секунду. Что нам и надо. Кроме этого наш таймер имеет еще один очень полезный регистр сравнения. Если мы туда как раз и запишем число 31250, то при определенных условиях оно будет постоянно сравниваться с содержимым счетчика Т1 и наконец, когда сравняется, счетчик выдаст сигнал прерывания, мол нате, держите вашу секунду.
Получается удобно, но, к сожалению, не совсем точно. Ибо счетчик наш будет считать с ошибкой квантования 256/ 8 000 000, что дает немаленькую ошибку исчисления одной секунды в целых 32 микросекунды. А это приводит к ошибке 2,8 секунды в сутки (0,000032×3600 * 24).
А вот, если мы поделим исходные 8 МГц на меньшую величину, например на 64, то точность квантования возрастет в 4 раза до 8 мкс и уменьшит итоговую ошибку до 0,33 секунд в сутки. Но, к сожалению, в этом случае счетчику надо будет досчитать до 125 000, а такое число в шестнадцати разрядный регистр не войдёт. Придётся писать в регистр сравнения число поменьше (62500 ещё влазит)) и добавлять цикл в самой программе, где одна секунда будет считаться уже не по одному, а по двум прерываниям.
Но это мы взяли случай идеальный, а реальный кварцевый резонатор, особенно установленный на плате «made in China» способен принести вам немало сюрпризов. Нет, вообще, если смотреть по даташитам на стандартные кварцы, то теоретически не всё так плохо.

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

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

p0bkca_wthxj3_9hx9ttr6ivzbw.png

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

Берём Arduino ProMini.

r4b5e6ggnkxbbgmeefim9cv5-8i.jpeg

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

k_iziqqu4ag5ywb2rm39qlvi8ng.jpeg

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

Как оказалось, ардуино платы обладают неприятной особенностью — вывод, к которому подсоединён кварцевый резонатор, не имеет выхода на гребенку выводов, хотя и соответствует порту PB7. Типа, раз порт занят под кварц, то и нефиг к нему цепляться. А просто к ноге микроконтроллера весьма затруднительно подцепится щупом осциллографа, ибо поверхностный монтаж и шаг 0,5 мм между выводами. Но даже присоединение к нужной ножке мне ничего не дало. То ли потому, что неправильно тыкал, то ли тыкал совсем не туда, поскольку вывод кварцевого резонатора, может быть, совсем не есть вывод тактового генератора и вообще, он внутри самого микроконтроллера. Поэтому пришлось идти обходными путями — поставить предделитель на минимальный коэффициент деления — единицу, записать в регистр сравнения ноль, чтобы прерывание дергалось сразу же и ввести микроконтроллер в специальный режим, в котором ножка порта PB1 меняет свое логическое состояние при каждом таком прерывании.
Логически рассуждая, при включении платы Arduino Nano 16 МГц, на выходе этого порта должен появиться меандр с частотой 8 МГц.
Что и случилось. Осциллограф показал частоту 8. 002 31 МГц. Причем последний разряд жил своей жизнью и я так и не понял, то ли не хватает точности осциллографа, то ли это так плавает частота кварцевого генератора. Больше походило на второе.
Хорошей термостабильностью там тоже не пахло. Если на плату подышать (может, кстати, емкости от влажности ещё едут?) или поднести (издалека) паяльник, то кварц мог отъехать сразу на полсотни герц. И эти измерения ещё загрублены в два раза, поскольку исходная частота 16 МГц.
Таким образом, в ардуино платах (по крайней мере тех, что китайского происхождения) добиться точности более 200 Гц при тактовой частоте 16 МГц невозможно. Что и дает нам предельную точность часов собранных на таких платах не более чем в одну секунду в сутки. И это ещё хорошо.

Потому что есть китайские клоны Arduino UNO, уже упомянутые мною ранее, с которыми, вообще всё плохо. А они весьма распространены, ибо дешевы и удобны.
Так вот, у них частота может отличаться от задекларированной более чем на сотню килогерц! Что даже для самых плохих китайских кварцев как-то нехарактерно.
Загадка начинается ещё с того, что на самом кварце написано 12 МГц! И в описаниях продажников тоже.

5labpjeqa-grafww3mkdqnnawb4.jpeg

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

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

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

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

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

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


/*
 * Tibetian_Bowl.c
 *
 * Created: 07.06.2018 0:29:57
 *  Author: User
 */ 


#define F_CPU 8000000

#include 


#include 

#include // стандартные целые числа
#include  // математика
#include  //стандартный ввод-вывод
#include 

#include 
#include 


#include 

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;i140)
        {       PORTD |=(1<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;i21)|(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 режим ручной.

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

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

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

clfok4_1edajbwxdondtz9h9dpk.png

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

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

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

© Habrahabr.ru