"Вы читаете о роботах и программировании и думаете: «Было бы здорово сделать что-то подобное самому!» Теми, кем эта идея овладевает чуть больше просто мыслей смотрят кто и как делал своего робота. Читают статьи, смотрят видео. На картинках все понятно. В видеороликах тоже обычно показываются уже готовые продукты, а также сжато показываются технологии их изготовления. И вроде бы то же всё понятно: отпилил, прикрутил, припаял, соединил, запрограммировал вон на той программе вот этим кодом."

вторник, 28 января 2020 г.

Занятие кружка 28.01.20

По техническим причинам заседание клуба не состоялось,  однако тема для разговора есть и она очень важна в контексте того,что мы с Вами обсуждали на прошлом заседании.  Тогда мы рассмотрели программирование микроконтроллера на основе создание устройства "Бегущий огонь"  и хоть оно не проявляет особой требовательности к  процессору и программе, однако на её примере мы рассмотрели  написание программы используя внутренние устройство микроконтроллера. Теперь,  когда мы убедились что стандартные функции для работы с дискретными сигналами работают в разы медленнее, а размер программы на порядок меньше, имеет смысл рассмотреть и другие способы управления сигналами на выводах микроконтроллера, используя регистры нашего микроконтроллера. Однако надо отдавать себе отчёт, что у этого способа есть плюсы и минусы. Основных плюсов два, скорость реакции программы её размер. К основным минусам можно отнести, то что рассматривая регистры конкретного микроконтроллера, мы неизбежно столкнётся, с тем что на других  МК структура регистров и их количество будет различаться, и как следствие программа становиться не переносимой. Другим значительным минусом становиться трудоемкость написания программы, что требует большей квалификации от программиста разработчика.
Тем не менее, мы знаем, что функции ввода-вывода цифровых сигналов digitalWriter() и digitalRead()   можно заменить на непосредственные операции с регистрами портов D,В,C. Теперь пора перейти к рассмотрению аналоговых сигналов, которые может обрабатывать микроконтроллер.
В отличие от остальных функций, ускорение функции analogRead() достигается не модификацией её исходного кода, а настройкой режима работы АЦП. Но обо всем по порядку и начнем с описания работы АЦП микроконтроллера Atmega 328P. 

Микроконтроллер Atmega 328P, на котором построен Arduino Uno содержит встроенный 10-битный аналого-цифровой преобразователь (далее АЦП, англ. ADC — Analog to Digital Converter) последовательного приближения. Именно он, как видно из названия, отвечает за оцифровку входящего аналогового сигнала.
Для осуществления корректного преобразования, АЦП необходимо эталонное значение напряжения, с которым будет сравниваться входящий аналоговый сигнал. Это эталонное значение называется источником опорного напряжения (ИОН). Микроконтроллер Atmega 328P позволяет в качестве ИОН использовать:
  • напряжение питания микроконтроллера — 5 В,
  • внутренний опорный источник на 1,1 В (на Atmega8 — 2,56 В),
  • напряжение на выводе AREF (внешний ИОН, референсное напряжение).


По умолчанию, у Arduino в качестве ИОН выступает напряжение пиитания МК — 5 В. Для использования в качестве ИОН других источников референсного внешнего напряжения у микроконтроллера есть дополнительный вход AREF. Перед преобразованием аналогового сигнала, при использовании внешнего ИОН необходимо вызвать функцию analogReference().

Сигнал, поданный на вход АЦП должен быть в границах заданного диапазона 0 (GND)...ИОН. 10-битный означает, что заданный диапазон будет разбит на значения, и входной сигнал будет оцифрован в соответствующее цифровое значение из диапазона 0...1023:



Запуск преобразования может осуществляться несколькими способами:
  • в ручном режиме — единичное преобразование
  • а автоматическом режиме — по сигналам из различных источников






Для управления АЦП существует 5 основных восьмибитных регистров: 


Регистр ADCSRA (ADC Control and Status Register A)

Назначение битов регистра ADCSRA (ADC Control and Status Register A, регистр управления и состояния):
  • ADEN (ADC Enable, включение АЦП) — флаг, разрешающий использование АЦП, при сбросе флага во время преобразования процесс останавливается;
  • ADATE (ADC Auto Trigger Enable) — выбор режима работы АЦП. 0 – разовое преобразование (Single Conversion Mode); 1 – включение автоматического преобразования по срабатыванию триггера (заданного сигнала). Источник автоматического запуска задается битами ADTS[2:0] регистра ADCSRB. Возможны следующие варианты: 



  • ADSC (ADC Start Conversion, запуск преобразования) — флаг установленный в 1 запускает процесс преобразования.
    В режиме Single Conversion (ADATE=0) для запуска каждого нового преобразования этот флаг должен быть установлен в единицу. После завершения преобразования, флаг ADSC сбрасывается в 0.
    В режиме Free Running Mode (ADATE=1, ADTS[2:0]=000) этот флаг должен быть установлен в единицу один раз — для запуска первого преобразования, следующие происходят автоматически.
    Если установка флага ADSC происходит во время или после разрешения АЦП (ADEN), то перед запрашиваемым преобразованием происходит дополнительное (extended) преобразование, во время которого происходит инициализация АЦП. Сброс флага во время преобразования не оказывает никакого влияния на процесс.
  • ADIF (ADC Interrupt Flag) — флаг прерывания от компаратора
  • ADIE (ADC Interrupt Enable) — разрешение прерывания от компаратора
  • ADPS[2:0] (ADC Prescaler Select) — комбинацией битов задается частота преобразования АЦП по отношению к частоте МК. Выбранная частота влияет на точность — чем выше частота, тем ниже точность. АЦП тактируется через выбранный делитель от частоты ядра микроконтроллера. Значения битов:
 Частота работы МК Atmega 328P 16МГц. При настройках по умолчанию используется предделитель 128 (ADPS[2:0]=[111]), а это значит, что АЦП работает на частоте 16МГц/128=125КГц, что укладывается в данные даташита – 50-200КГц. Отсюда очень низкая скорость выполнения преобразования, но и самая высокая точность. Для того, чтобы ускорить работу АЦП необходимо уменьшить предделитель, но необходимо помнить, что чем выше частота, тем ниже точность преобразования. Экспериментально можно получить значение предделителя – 16 (ADPS[2:0]=[100]), при котором возможен компромисс 10-кратного прироста скорости, при сохранении точности.

 int pinIn = A0; // Пин аналогового входа
 void setup()
{
 pinMode(pinIn, INPUT);
   ADCSRA |= (1 << ADPS2); //Биту ADPS2 присваиваем единицу - коэффициент деления 16
   ADCSRA &= ~ ((1 << ADPS1) | (1 << ADPS0)); //Битам ADPS1 и ADPS0 присваиваем нули
}
 void loop()
 { 
   Serial.println(analogRead(pinIn));  
  delay(1000); 
}
 Замеры производительности показывают время выполнения функции — 16 мкс (вместо первоначальных 112 мкс):
 
Регистр ADMUX (ADC Multiplexer Selection Register)

Вспомним какие биты содержатся в регистре ADMUX:

Назначение битов регистра ADMUX:
  • REFS[1:0](Reference Selection Bit) — биты определяют источник опорного напряжения. Возможные значения:

  • ADLAR(ADC Left Adjust Result) — бит отвечающий за порядок записи битов результата в регистры ADCL и ADCH. В зависимости от того значения, которое присвоено биту ADLAR возможны 2 варианта:
          Если ADLAR=0, то 
 
          Если ADLAR=1, то
 
  •  MUX[3:0] (Multiplexer Selection Input) — биты выбора аналогового входа. Значения:
 














Продолжение следует
Использованы материалы :
https://codius.ru/articles/Arduino_%D1%83%D1%81%D0%BA%D0%BE%D1%80%D1%8F%D0%B5%D0%BC_%D1%80%D0%B0%D0%B1%D0%BE%D1%82%D1%83_%D0%BF%D0%BB%D0%B0%D1%82%D1%8B_%D0%A7%D0%B0%D1%81%D1%82%D1%8C_2_%D0%90%D0%BD%D0%B0%D0%BB%D0%BE%D0%B3%D0%BE_%D1%86%D0%B8%D1%84%D1%80%D0%BE%D0%B2%D0%BE%D0%B9_%D0%BF%D1%80%D0%B5%D0%BE%D0%B1%D1%80%D0%B0%D0%B7%D0%BE%D0%B2%D0%B0%D1%82%D0%B5%D0%BB%D1%8C_%D0%90%D0%A6%D0%9F_%D0%B8_analogRead


пятница, 24 января 2020 г.

Занятие кружка от 24.01.20

Занятие кружка было посвящено созданию электронного устройства бегущий огонь. Программа управления создавалась с опорой на регистры микроконтроллера и на языке С. Основой любой программы - алгоритм. Для разработки алгоритма была использована ИС ДРАКОН. Дракон схема в основу, которой  легла структура стандартного скретча, включает в себя главную функция int main(), которая  разбита на две ветки  ветка SETUP и ветка LOOP.

В левой ветке SETUP  (настройка портов на вход и выход) используются регистры  ввода вывода:
DDRB = 0xff; //вывод сигнала на светодиоды
DDRD = 0x02;//вход кнопки
PORTB = 0x3f; //высокий сигнал на выводах порта В
PORTD |= 1;//высокий сигнал 
Кроме того инициализируются переменные сдвига
А = 0;
B = 5;
t = 0; //временная  задержка
В ветке LOOP  развилка обусловлена наличием кнопки которая меняет направление сдвига(перехода к следующему светодиоду). 
Предложенная дракон схема была реализована и проверена на макете в виртуальной лаборатории. В ходе отладки программы были обнаружены следующие ошибки: 
На схеме сборки, в виртуальной лаборатории кнопка была подключена не верно, так же ошибка  была в условии нажатия кнопки  - было "PORTB  & (1 <<PD1)" надо "PORTD &(1 <<PD1)", из-за отказа использовать схему задержки была ошибка в метках адресации безусловного перехода.
Физически собранное устройство тоже содержало несколько ошибок, которые надо устранить на следующем занятии.
Однако предложенная схема далеко не идеальна так ветках содержится одинаковый код, а все различие только в направлении выбора следующего светодиода(сдвига), чтобы оптимизировать задачу была предложена дракон-схема которая предполагает развилку, обусловленную нажатием кнопки, поместить непосредственно перед операцией сдвига и она будет только давать выбор в переходе к следующему биту порта В, также необходимо переделать условие которое определяет поджог крайнего светодиода (было  "A == 6" стало "(A == 6) or (A == -1)"). Кроме того такая модернизация сделает ненужной переменную B.

Листинг программы

    // ИС Дракон. Маршрутный транслятор.

    // Y:\home\deniska\Документы\др длкр\новая мигалка1.drt
    // 1. Схема - Гном, Изменение 25.01.2020 9:47:04
    // 4. Модуль, Начало
#include <avr/io.h>

    // Y:\home\deniska\Документы\др длкр\новая мигалка1.drt
    // 2. Схема - процедура, Изменение 25.01.2020 10:20:37, Начало
int main()
{
int A, t;

    // ==================== Шампур

    // 5. Заголовок / main()

    // 6. Ветка / setup

    // 7. Действие / настройка портов
DDRB = 0xff;
PORTB = 0xff;
DDRD = 0x00;
PORTD = 0x00;

    // 8. Действие / переменная сдвига
A = 0;

    // 9. Действие / установка задержки на 0
t = 0;

    // 10. Адрес -> 11. Ветка / telo

    // ==================== Шампур

    // 11. Цикл Ветка / telo

L12: ;
    // 12. Действие / зажигаем светодиод
PORTB &= ~(1 << A);

    // 13. Вставка / задержка
goto L1_27;

L14: ;
    // 14. Действие / гасим предыдущий
PORTB |= 1 << A;

    // 15. Вопрос / кнопка нажата? == Да
if (PIND & (1<<PD1)) goto L20;

    // 16. Действие / сдвиг
A++;

L17: ;
    // 17. Вопрос / горит последний светодиод == Да
if ((A == 6) or (A == -1)) goto L21;

    // 18. Вставка / задержка
goto L2_27;

    // 19. Цикл Адрес -> 11. Ветка / telo

    // ==================== Шампур

L20: ;
    // 20. Действие / сдвиг
A--;
goto L17;

    // ==================== Шампур

L21: ;
    // 21. Вопрос / кнопка нажата? == Да
if (PIND & (1<<PD1)) goto L23;

    // 22. Действие / минимальная переменная сдвига
A = 0;
goto L2_27;

    // ==================== Шампур

L23: ;
    // 23. Действие / максимальная переменная сдвига
A = 5;
goto L2_27;

    // ==================== Шампур

    // 24. Ветка / aut

    // 25. Конец / Конец

    // Y:\home\deniska\Документы\др длкр\новая мигалка1.drt
    // 3. Схема - вставка, Изменение 25.01.2020 9:50:37, Начало

    // ==================== Шампур

    // 26. Заголовок / задержка

L1_27: ;
    // 27. Цикл Действие / счетчик задержки
t++;

    // 28. Вопрос / счетчик равен задержке? == Нет
if (!(t == 32000)) goto L1_27;

    // 29. Действие / обнуление счетчика
t=0;

    // 30. Конец / Конец
goto L14;

    // Y:\home\deniska\Документы\др длкр\новая мигалка1.drt
    // 3. Схема - вставка, Изменение 25.01.2020 9:50:37, Начало

    // ==================== Шампур

    // 26. Заголовок / задержка

L2_27: ;
    // 27. Цикл Действие / счетчик задержки
t++;

    // 28. Вопрос / счетчик равен задержке? == Нет
if (!(t == 32000)) goto L2_27;

    // 29. Действие / обнуление счетчика
t=0;

    // 30. Конец / Конец
goto L12;

    // 2. Схема, Конец
//
}

    // 4. Модуль, Конец
//


четверг, 23 января 2020 г.

Регистры, байты, биты (материал для кружка1)


В этом уроке мы научимся работать напрямую с регистрами микроконтроллера. Зачем? Я думаю в первую очередь это нужно для того, чтобы понимать чужой код и переделывать его под себя, потому что прямая работа с регистрами в скетчах из Интернета встречается довольно часто.
Начнём с того, что вспомним, где мы пишем код: Arduino IDE. Несмотря на большое количество не всегда оправданного негатива в сторону этой программы, она очень крутая. Помимо кучи встроенных инструментов и поддержки “репозиториев” от сторонних разработчиков, Arduino IDE позволяет нам программировать плату на разных языках программирования. Это некая условность, но по сути получается три языка:
  • C++, который мы изучили в рамках уроков на сайте;
  • Ассемблер (ассемблерные вставки) – прямая работа с микроконтроллером, очень сложный язык;
  • Язык регистров микроконтроллера. Условно назовём его отдельным языком, хоть он таковым и не является.
Что такое регистр? Тут всё весьма просто: регистры это грубо говоря глобальные переменные, информацию из которых мы можем как считать, так и изменить, это сверхбыстрые блоки оперативной памяти объёмом 1 байт, находящиеся рядом с ядром МК и периферией. В регистрах микроконтроллера хранятся “настройки” для различной его периферии: таймеры-счётчики, порты с пинами, АЦП, шина UART, I2C, SPI и прочее железо, встроенное в МК. Меняя регистр, мы даём практически прямую команду микроконтроллеру, что и как нужно сделать. Запись в регистр занимает 1 такт, то есть 0.0625 микросекунды. Это ОЧЕНЬ быстро. Названия (имена) регистров фиксированные, все их описания можно найти в даташите на микроконтроллер. Работа с регистрами очень непростая, если вы не выучили их все наизусть. А названия у них обычно нечитаемые, аббревиатуры. Так называемые “Ардуиновские” функции собственно и занимаются тем, что работают с регистрами, оставляя нам удобную, понятную и читаемую функцию.
Итак, с точки зрения кода, регистр – это переменная, которую мы можем читать и писать. В микроконтроллерах серии ATmega регистры 8 битные (вроде бы поэтому сам микроконтроллер и считается 8-ми битным), то есть регистр это (грубо) переменная типа byte, меняя которую, можно на низком уровне конфигурировать работу микроконтроллера. Насколько мы знаем, байт это число от 0 до 255, получается каждый регистр имеет 255 настроек? Нет, логика здесь совсем другая: байт это 8 бит, принимающих значение 0 и 1, то есть один регистр хранит 8 настроек, которые можно включить/выключить. Именно так и происходит конфигурация микроконтроллера на низком уровне. Давайте для примера рассмотрим один из регистров таймера 1, под названием TCCR1B. Картинка из даташита на ATmega328p:
Регистр TCCR1B, как и положено здоровому байту, состоит из 8 бит. Почти каждый его бит имеет имя (кроме 5-го, в этом МК он не занят). Что значит каждый бит и регистр – самым подробным образом расписано в даташите на микроконтроллер. Названия битов являются чем-то вроде констант, все их имена “заняты”, и создать переменную, совпадающую с названием бита, нельзя, ровно как попытаться изменить значение бита, обратившись к его имени.
  • int WGM12; // приведёт к ошибке
  • CS11 = 5; // приведёт к ошибке
Так что если вы случайно назовёте переменную так, как называется регистр или бит – вы получите ошибку. Но это вряд ли случится, названия у регистров и битов не очень вменяемые. Но считать значение бита по его названию – можно! Причём его значение будет равно его номеру в регистре, считая справа. CS11 равен 1, WGM13 равен 4, ICNC1 равен 7, также эти значения указаны в таблице даташита (над таблицей). Думаю здесь всё понятно: есть регистр (байт), имеющий уникальное имя и состоящий из 8 бит, каждый бит тоже имеет уникальное имя, по которому можно получить номер этого бита в байте его регистра. Осталось понять, как этим всем пользоваться.

Запись/чтение регистра


Существует довольно таки много общепринятых способов установки битов в регистрах, мы рассмотрим их все, чтобы столкнувшись с одним из них вы знали, что это вообще такое и как оно работает. Значит всё что нам нужно – это установка нужного бита в регистре в состояние 0 или 1, в этом по сути и заключается работа с регистром. Рекомендую прочитать урок по битовым операциям, в котором максимально подробно разобрано всё, что касается манипуляций с битами.
Давайте вернёмся к регистру таймера, который я показывал выше, и попробуем его сконфигурировать. Первый способ, это явное задание всего байта сразу, со всеми единицами и нулями. Сделать это можно так:
  • TCCR1B = 0b01010101
Таким образом мы включили и выключили нужные биты сразу, одним махом. Как вы помните из урока о типах данных и чисел, микроконтроллеру всё равно, в какой системе исчисления вы с ним работаете, то есть число 0b01010101 у нас в двоичной системе, в десятичной это будет 85, а в шестнадцатеричной – 0x55. И вот эти три варианта абсолютно одинаковы с точки зрения результата:
  • TCCR1B = 0b01010101;
  • TCCR1B = 85;
  • TCCR1B = 0x55;
Только на первый можно посмотреть и сразу понять, что где стоит. Чего не скажешь про остальные два. Очень часто в чужих скетчах встречается такая запись, и это не очень комфортно.
Гораздо чаще бывает нужно “прицельно” изменить один бит в байте, и тут на помощь приходят логические (битовые) функции и макросы. Рассмотрим все варианты, во всех из них BYTE это байт-регистр, и BIT это номер бита, считая с правого края. То есть BIT это цифра от 0 до 7, либо название бита из даташита.
Установка бита в 1Установка бита в 0Описание
BYTE |= (1 << BIT);BYTE &= ~(1 << BIT);Использование битового сдвига <<
BYTE |= (2^BIT);BYTE &= ~(2^BIT);Используем 2 в степени <номер бита> (пример не рабочий!)
BYTE |= bit(BIT);BYTE &= ~bit(BIT);Используем ардуиновский макрос bit(), заменяющий сдвиг
BYTE |= _BV(BIT);BYTE &= ~_BV(BIT);Используем встроенную функцию _BV(), опять же аналог сдвига
sbi(BYTE, BIT);cbi(BYTE, BIT);Используем общепринятые макросы sbi и cbi
bitSet(BYTE, BIT);bitClear(BYTE, BIT);Используем ардуиновские функции bitSet() и bitClear()
Что хочу сказать по перечисленным вариантам: они все по сути являются одним и тем же, а именно – первым, просто обёрнуты в другие функции и макросы. Время выполнения всех вариантов одинаково, т.к. макро-функции не делают лишних действий, а приводят все способы к первому, со сдвигом и |= и &=. Все эти способы вы можете встретить в скетчах из интернета, это факт. Лично мне больше всего нравится ардуиновский bitSet и bitClear, потому что они имеют читаемое название и заранее сидят в библиотеке. Что касается sbi и cbi – то для их использования нужно в самом начале документа (среди остальных дефайнов) создать макро для этих функций:
  • #define cbi(sfr, bit) (_SFR_BYTE(sfr) &= ~_BV(bit))
  • #define sbi(sfr, bit) (_SFR_BYTE(sfr) |= _BV(bit))
И после этого можно пользоваться sbi и cbi
Давайте рассмотрим пример, где просто подёргаем TCCR1B разными способами:
  • // для использования sbi и cbi
  • #define cbi(sfr, bit) (_SFR_BYTE(sfr) &= ~_BV(bit))
  • #define sbi(sfr, bit) (_SFR_BYTE(sfr) |= _BV(bit))

  • void setup() {
  • TCCR1B = 0; // обнулили регистр
  • bitSet(TCCR1B, CS11); // включили бит №1
  • cbi(TCCR1B, CS11); // вЫключили бит №1
  • TCCR1B |= _BV(4); // включили бит №4
  • TCCR1B |= (1 << WGM12); // включили бит №3
  • TCCR1B &= ~_BV(WGM13); // вЫключили бит №4
  • bitClear(TCCR1B, 3); // вЫключили бит №3
  • }
Можно ещё добавить вариант, где в одной строчке можно “прицельно” установить несколько битов:
  • void setup() {
  • TCCR1B = 0; // обнулили регистр
  • // ставим бит 1, 3 и 4(WGM13)
  • TCCR1B |= _BV(1) | _BV(3) | _BV(WGM13);
  • }
Я думаю тут всё понятно, давайте теперь попробуем “прицельно” прочитать бит  из регистра:
Чтение битаОписание
(BYTE >> BIT) & 1Вручную через сдвиг
bitRead(BYTE, BIT)Ардуиновская макро-функция bitRead
Два рассмотренных способа возвращают 0 или 1 в зависимости от состояния бита. Пример:
  • void setup() {
  • TCCR1B = 0; // обнулили регистр
  • bitSet(TCCR1B, CS12); // включили бит №2
  • Serial.begin(9600); // открыли порт
  • Serial.println(bitRead(TCCR1B, 2)); // получили 1
  • }
Теперь вы готовы к любой встрече с регистрами!

16-бит регистры


У Ардуинок (AVR) встречаются также 16-битные регистры, которые на деле разделены на два 8-битных, например регистры АЦП ADCH и ADCL. АЦП у нас 10 битный, но регистры – 8 битные, поэтому часть (8  бит) хранится в одном регистре (ADCL), а остальное (2 бита) – в другом (ADCH). Смотрите, как это выглядит в виде таблицы:
Вопрос: как нам принять это самое 10 битное число, если оно разбито на два разных регистра? Очень просто, используя сдвиг:
  • int val = ADCL + (ADCH << 8);
Читать нужно с младшего регистра. Как только мы читаем младший регистр (первый), у МК полностью блокируется доступ к всему регистру, пока не будет прочитан старший. Если прочитать сначала старший – значение младшего может быть утеряно.
Обратная задача: есть опять же мнимый 16-битный регистр (состоящий из двух 8-битных), в который нам нужно записать значение. Например сдвоенный регистр ICR1H и ICR1L, вот таблица:
Микроконтроллер может работать только с одним байтом, а как нам записать двухбайтное число? А вот так: разбить число на два байта (при помощи ардуиновских функций highByte() и lowByte() ), и эти байты записать в соответствующие регистры. Пример:
  • uint16_t val = 1500; // просто число типа int
  • ICR1H = highByte(val); // пишем старший байт
  • ICR1L = lowByte(val); // пишем младший байт

  • // читаем байты обратно и "склеиваем" в int
  • byte val_1 = ICR1L;
  • byte val_2 = ICR1H;
  • uint16_t value = val_1 + (val_2 << 8);
Записывать нужно со старшего байта. Как только мы записываем младший байт (последний) – МК “защелкивает” оба регистра в память, соответственно если сначала записать младший – в старшем будет 0, и последующая запись старшего будет проигнорирована.