Управление сервоприводом с помощью ATtiny13A

Итак, мне удалось сломать свой дешевый сервопривод/ESC (электронный регулятор скорости) и я решил сделать свой собственный, используя ATtiny13A на какой-то перфборд. Но я столкнулся с некоторыми проблемами, заставляя сервопривод реагировать. Я предполагаю, что библиотека servo.h несовместима с ATtiny13A из-за несовместимых внутренних тактовых частот, не создающих правильных ШИМ-сигналов. И я не нашел для этого решения.

Я возился с библиотекой Servo8Bit.h, но это тоже, похоже, не работает (так как она предназначена для ATtiny45 и ATtiny85 ). Способен ли ATtiny13A вообще производить необходимые ШИМ-сигналы?

Любая помощь/ссылки на документацию будут оценены по достоинству!

Заранее спасибо.

Код:

#include <Servo.h>

Servo myservo;
int potpin = A3;
int val;    // variable
int Toggle = 2;

void setup() {
  Serial.begin(9600);
  pinMode(Toggle, INPUT);
  myservo.attach(1);  // Servo/ESC is attached to pb1
}

void loop() {
  int ToggleState = digitalRead(Toggle);
  Serial.println(ToggleState);
    val = analogRead(potpin); 
  
  if (ToggleState == 0) {
  val = map(val, 0, 1023, 0, 180);  //Servo motor
   myservo.write(val);                  
  delay(15);                              
}
else { 
  val= map(val, 0, 1023,1000,2000); //ESC
  myservo.write(val);                  
  delay(15); 
 }
} 

Circuit Project

, 👍2

Обсуждение

Servo8Bit использует 8-разрядный таймер, а ATtiny13A _does_ имеет 8-разрядный таймер. Не должно быть слишком сложно перенести эту библиотеку на этот микро. Изучите исходный код, затем изучите спецификацию tiny13A (в основном главу, посвященную таймеру). Тогда вы сможете легко увидеть, какие изменения необходимы., @Edgar Bonet

Вы уверены, что ATtiny на самом деле работает с той частотой, о которой вы думаете? У меня действительно были проблемы с синхронизацией с моим ATtiny85, когда я пытался использовать его с частотой 8 МГц. Предохранители на самом деле все еще были настроены на 1 МГц. Сначала мне пришлось записать загрузчик, чтобы правильно установить предохранители., @chrisl

Выполняется ли скетч мигания с ожидаемой скоростью?, @jsotola


1 ответ


1

Servo8Bit и ATtiny13(A)

Я посмотрел на Servo8Bit и подумал об этом примерно так же, как Эдгар Бонет в своем комментарии:

Servo8Bit полагается на 8-битный таймер, а ATtiny13A имеет 8-битный таймер. Не должно быть слишком сложно перенести эту библиотеку в этот микро. Изучите исходный код, затем изучите спецификацию tiny13A (в основном глава, посвященная таймеру). Тогда вы сможете легко увидеть, какие изменения необходимы.

Не обращая внимания на разницу во времени между ATTinyx5 и ATtiny13(A), я просто пошел вперед и посмотрел, смогу ли я заставить его скомпилироваться. И да, это довольно легко собрать. Это в основном состоит из добавления 0 к некоторым именам регистров оборудования. Плохая новость такова:

Sketch использует 1486 байт (145%) пространства для хранения программ. Максимум-1024 байта.
Глобальные переменные используют 31 байт (48%) динамической памяти, оставляя 33 байта для локальных переменных. Максимум-64 байта.

Скетч слишком большой; см. http://www.arduino.cc/en/Guide/Troubleshooting#size для советов по его сокращению.

Ошибка во время сборки: текстовый раздел превышает доступное пространство на плате

Так что, да, на это можно было надеяться. Библиотека использует ISR для поддержки ряда сервоприводов, таких как более распространенная библиотека сервоприводов, которую вы бы использовали на UNO. Я не рассматривал вопрос об уменьшении его до размера; я ожидаю, что это будет непрактично.

ШИМ... вроде того.

Прежде чем рассмотреть возможность изменения Servo8Bit, я подумал, что имеет смысл просто использовать счетчик таймера непосредственно для получения сигнала. Проблема в том, что обычный сервосигнал имеет длину 20 мс и полностью управляет им в окне 1 мс. Если вы попытаетесь уместить весь 20-миллиметровый кадр в один 8-битный временной интервал, то в итоге получите довольно дерьмовое угловое управление. Таким образом, делать это немного болезненно без 16-битного счетчика таймера. Но я думаю, что это игра.

Итак, ниже-грубая и едва проверенная попытка. Код, который следует, является конечным автоматом внутри вектора переполнения OCR0B, который планирует рост и падение ребер, является "нормальным" режимом. Просто хочу предупредить вас, что на самом деле у меня нет ATtiny13(A) под рукой. И на самом деле у меня тоже нет серво-руки. Но у меня есть древний объем памяти и ATtiny85 и возможность компиляции для ATtiny13(A), а также калькулятор для расчета разницы 8 МГц против 9,6 МГц.

Итак, вот что я придумал , что я думаю (но не знаю) будет работать, если вы скомпилируете и загрузите с помощью MCUDude MicroCore, ATtiny13(A) с выбранным внутренним генератором 9,6 МГц.

Он рассчитан только на работу с 1 сервоприводом. Расширение его до двух должно быть легко выполнимо. Выходя за пределы этого, вы, вероятно, столкнетесь с проблемами.

Код:

// +---------------------------------------------------------------------------------------+
// |                                                                                       |
// |                                 +-------------+                                       |
// |                                 |             |                                       |
// |                                 |      A      |                                       |
// |  PCINT5 ADC0 dW  /RESET  PB5 *--|-1    T    8-|--* VCC                                |
// |                                 |      T      |                                       |
// |  PCINT3 ADC3      CLKI   PB3 *--|-2    I    7-|--* PB2  SCK   ADC1 T0         PCINT2  |
// |                                 |      N      |                                       |
// |  PCINT4 ADC2             PB4 *--|-3    Y    6-|--* PB1  MISO  AIN1 OC0B  INT0 PCINT1  |
// |                                 |      1      |                                       |
// |                          GND *--|-4    3    5-|--* PB0  MOSI  AIN0 OC0A       PCINT0  |
// |                                 |      A      |                                       |
// |                                 |             |                                       |
// |                                 +-------------+                                       |
// |                                                                                       |
// +---------------------------------------------------------------------------------------+

#if !defined(__AVR_ATtiny13__) && !defined(__AVR_ATtiny13A__)
#error This code is written specifically for ATTiny13(A) devices.
#endif

#if F_CPU != 9600000
#error  This code is written specifically for 9.6 MHz
#endif



//
//
//
#include <avr/io.h>
#include <avr/interrupt.h>


//
//
//
constexpr uint8_t TCCR0A_config(
  int com0b // two bits
) {
  return
      (    0 << COM0A1) // normal
    | (    0 << COM0A0) //   port operation
    | (com0b << COM0B0) // bits 5 AND 4
    | (    0 <<      3) // reserved
    | (    0 <<      2) // reserved
    | (    0 <<  WGM01) //  lower two bits
    | (    0 <<  WGM00);//     of Normal (mode 0);
}


//
//
//
constexpr uint8_t TCCR0A_config_for_go_low()  {return TCCR0A_config(0x2);}
constexpr uint8_t TCCR0A_config_for_go_high() {return TCCR0A_config(0x3);}


//
// 50 Гц (частота сервопривода); период 20 мс
//
// Ниже вы увидите цифры 150, 300 и 3000.
// Причина этих чисел в том, что я решил использовать a /64
// прескалер таймера.  На частоте 9,6 МГц, с прескалером a /64, 1 миллисекунда
// is 150 отсчетов таймера таймера.  В сервоприводе есть фиксированная высота 1 МС,
за которой следует переменная часть от 0 до 1 мс.  Услужливо подсказала цифра 150
// вписывается в 8-битные регистры таймера, а также дает разумный
// количество приращений к сервоуправлению.  Для 180 градусов-150 шагов
// составляет 1,2 градуса.
//
// Максимальное высокое время-это фиксированное высокое время 1 мс и максимальное
/ переменное высокое время, в общей сложности 2 МС, или 300 тиков таймера, вот о чем это
число.
//
// 3000-это количество тиков таймера для полного 20-миллиметрового сигнального кадра,
// то есть 20 * 150 = 3000
//
//
// Вывод цифры 150:
//
// На частоте 9,6 МГц и /64 прескалера
//
//        1 second         64 mcu_cycles   A_MS_DURATION timer_counts
//  -------------------- X ------------- X ---------------
//  9_600_000 mcu_cycles   1 timer_count    0.001 seconds
//
//
//       1      64    A_MS_DURATION
//  --------- X -- X  -----
//  9_600_000   1     0.001
//
//
//     64       A_MS_DURATION
//  --------- X -----------
//  9_600_000      0.001
//
//
//     64 x A_MS_DURATION
//  --------------------
//        9_600
//
//
//  A_MS_DURATION
//  -------------
//      150
//

static constexpr uint8_t  ONE_MS_WORTH_OF_TICKS    = 150;
static constexpr uint8_t  MAXIMUM_HIGH_PERIOD      = ONE_MS_WORTH_OF_TICKS * 2;
static constexpr uint16_t TWENTY_MS_WORTH_OF_TICKS = 3000;
static constexpr uint16_t MAX_WASTE_CYCLES         = 0x80;

static volatile uint8_t g_pulse_width = ONE_MS_WORTH_OF_TICKS / 2;  // default to mid-range.



ISR(TIM0_COMPB_vect) {
  static uint8_t l_pulse_width; // this caches g_pulse_width
                                // and is updated from g_pulse_width
                                // only between frames

  //
  // ISR-это конечный автомат с четырьмя макросостояниями.
  //
  enum class t_signal_generator_state : uint8_t {
    timing_to_variable_high_period,
    timing_to_set_pulse_width,
    calculate_time_to_start_of_next_pulse_and_waste_a_bit,
    timing_to_start_of_pulse
  };

  //
  // Переменная состояния макроса
  //
  static t_signal_generator_state g_signal_generator_state =
    t_signal_generator_state::timing_to_variable_high_period;

  // low_time_remaining-это расширенная переменная состояния
  // для состояния
  static uint16_t low_time_remaining;


  switch (g_signal_generator_state) {
    case t_signal_generator_state::timing_to_variable_high_period:
      // Мы только что поднялись на значение OCR0B, которое вызвало это прерывание
      // и нам нужно перезапустить ((MAXIMUM_HIGH_PERIOD==300) - 256 = = 44)
      // временные циклы позже, такие, что осталось 256 временных циклов от
      // 2 мс (максимум) верхней части кадра.
      OCR0B += (MAXIMUM_HIGH_PERIOD - 256);  // 2ms worth of timer nicks minus the resolution of our timer

      // Мы остаемся ВЫСОКИМИ, когда входим в переменную часть периода МАКСИМУМА
      g_signal_generator_state = t_signal_generator_state::timing_to_set_pulse_width;
      break;
    case t_signal_generator_state::timing_to_set_pulse_width:
      // Мы получаем глобальную переменную, которая содержит (потенциально новое) значение от 0 до 150
      // представляющий диапазон от 0 мс до 1 мс (полный диапазон сервопривода), но
      // мы начинаем переменный импульс в голове времени, частично через всегда включенную первую мс.
      // Есть (150-44) цикла таймера, то есть 1 мс циклов таймера
      // где таймер остается высоким, чтобы закончить первую миллисекунду МАКСИМУМА.
      // Таким образом, мы добавляем это к запрошенному времени ширины импульса variale.
      l_pulse_width = g_pulse_width + (ONE_MS_WORTH_OF_TICKS - 44);

      // Установить регулировку ширины импульса
      OCR0B += l_pulse_width;

      // We'll be going low and the end of the variable high period.
      TCCR0A = TCCR0A_config_for_go_low();

      g_signal_generator_state = t_signal_generator_state::calculate_time_to_start_of_next_pulse_and_waste_a_bit;
      break;

    case t_signal_generator_state::calculate_time_to_start_of_next_pulse_and_waste_a_bit:
      // We just went low at the OCR0B value.

      // Calculate remaining timer cycles to the beginning of the next pulse
      // accounting for the MAX_WASTE_CYCLES we're about to waste in this state.
      low_time_remaining = (TWENTY_MS_WORTH_OF_TICKS - 44 - MAX_WASTE_CYCLES) - l_pulse_width;
      OCR0B += MAX_WASTE_CYCLES;

      // We will remain LOW since the pulse is done and we've not finished out the 20ms period
      g_signal_generator_state = t_signal_generator_state::timing_to_start_of_pulse;
      break;

  case t_signal_generator_state::timing_to_start_of_pulse:
      // Continue retriggering the timer until we've used up 20ms total,
      // in increments of not more than MAX_WASTE_CYCLES.

      if (low_time_remaining > MAX_WASTE_CYCLES) {
        low_time_remaining -= MAX_WASTE_CYCLES;
        OCR0B += MAX_WASTE_CYCLES;
        // We'll need to wast more, so we're remaining LOW.
      } else {
        OCR0B += low_time_remaining;
        low_time_remaining = 0;

        // This is out last round through this state to finish out the 20ms
        // window, so we'll be returning to HIGH.
        TCCR0A = TCCR0A_config_for_go_high();

        g_signal_generator_state = t_signal_generator_state::timing_to_variable_high_period;
      }
      break;
  }
}


//
//
//
void servo_init() {
  const auto SREG_saved = SREG; cli();

  DDRB |= 1U << 1;  // Servo data pin attached on Attiny PB1 aka (OCR0B)

  // select no clock source / stop counter
  TCCR0B &= ~(
      (1U << CS02)
    | (1U << CS01)
    | (1U << CS00)
  );

  // reset counter
  TCNT0 = 0x00;
  OCR0B = 0x80;

  TIMSK0 =
      (0 <<      7) // reserved
    | (0 <<      6) // reserved
    | (0 <<      5) // reserved
    | (0 <<      4) // reserved
    | (1 << OCIE0B) // we *ARE*  using output-compare B interrupt
    | (0 << OCIE0A) // we aren't using output compare A interrupt
    | (0 <<  TOIE0) //             nor overflow interrupt
    | (0 <<      0);// reserved


  TIFR0 = 1U << OCF0A;


  TCCR0A = TCCR0A_config_for_go_high();

  TCCR0B =
      (0 << FOC0A) // We're not doing any
    | (0 << FOC0B) //   output compare forcing.
    | (0 <<     5) // Reserved.
    | (0 <<     4) // Reserved.
    | (0 << WGM02) // High order bit of Normal mode (0)
    | (0 <<  CS02) // Clock source
    | (1 <<  CS01) //    prescaled
    | (1 <<  CS00);//      by 64.

  SREG = SREG_saved;
}


//
//
//
void setup() {
  servo_init();
}


//
//
//
void loop() {
  for (g_pulse_width = 0; g_pulse_width < 150; ++g_pulse_width) {
    __builtin_avr_delay_cycles(F_CPU >> 5);
  }
  for (; g_pulse_width > 0; --g_pulse_width) {
    __builtin_avr_delay_cycles(F_CPU >> 5);
  }
}


//  vim:sw=2:ts=2:et:nowrap:ft=cpp:

На области это производит сервосигнал, который должен подметать сервопривод на выводе OCR0B (PB1). Что-то вроде 4-5 секунд в одну сторону и обратно.

Если я когда-нибудь поставлю ATtiny13A на заказ и сумею самостоятельно откопать серво, я действительно проверю его. Но может пройти очень много времени, прежде чем я смогу это сделать. Но результаты на прицеле выглядят неплохо. По общему признанию, все масштабируется на 9,6/8,0. Это должно быть точно на ATtiny13(A). Я проверил, что он компилируется для него. Работа этой штуки объясняется в коде, насколько я могу.

Для сравнения:

Sketch использует 490 байт (47%) пространства для хранения программ. Максимум 1024 байта.
Глобальные переменные используют 10 байт (15%) динамической памяти, оставляя 54 байта для локальных переменных. Максимум-64 байта.

Это все еще хороший кусок того небольшого кодового пространства, которое у вас есть, но его должно быть достаточно, чтобы прочитать переключатель и потенциометр. Это использование таймера, который есть у устройства, поэтому использование millis()/delay ()-это своего рода окно, я думаю, я не пытался увидеть, что происходит, когда вы вызываете их или изучаете их код в микрокорре. Вы можете воссоздать свой собственный millis (), сделав прерывание переполнения таймера, и оно будет переполняться примерно с интервалом 1,7 мс. Как вы можете видеть, я просто использовал __builtin_avr_delay_cycles, чтобы все было просто.

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

,

+1. Несколько комментариев: 1. MAX_WASTE_CYCLES = 256 уменьшит нагрузку на прерывание и устранит необходимость в " OCR0B += MAX_WASTE_CYCLES;". 2. Возможное условие гонки: если "low_time_remaining" равно 1, а прерывание медленное или задерживается, "TCR0A" может быть обновлен слишком поздно. Это можно было бы смягчить, поместив переменную часть низкого времени ("low_time_remaining % MAX_WASTE_CYCLES") в начало низкого периода. 3. Включаемый файл <util/delay.h>предоставляет макрос для записи фиксированной задержки в более удобной форме: _delay_ms(32.25);`. Сгенерированный код такой же., @Edgar Bonet

Да, я подумывал о том, чтобы полностью увеличить "MAX_WASTE_CYCLES" (и, вероятно, дать ему лучшее название). Первоначальный план, я думаю, состоял в том, чтобы найти значение для продвижения "OCR0B" таким образом, чтобы окончательный проход через это состояние где-то "TCNT0" и "OCR0B" на половину таймера (0x80) опережал восходящий край, поэтому планировалось, что рост произойдет в более подходящий момент. Так что эта константа должна была быть чем-то вроде "FINAL_APPROACH_LENGTH_TO_HIGH_EDGE", но я просто никогда не делал этого. Это похоже на то, что вы описываете с помощью модуля., @timemage

Я надеялся найти способ обойтись без деления (модуля), например, найти какую-нибудь счастливую среду, которая была бы достаточно близка к обеим крайностям. Но да, в том виде, в каком он есть сейчас, вероятно, имеет больше смысла довести его до 0xFF., @timemage

Модуль 256 дешев: он достигается автоматически путем приведения "low_time_remaining" к "uint8_t"., @Edgar Bonet

Я в курсе относительно модуля 256. Я, вероятно, плохо объясняю себя, или я просто недостаточно проснулся, чтобы думать об этом. Когда я вернусь к просмотру кода, я снова посмотрю ваши комментарии, и они могут обновиться именно так, как вы думаете. У меня не так много времени на эту часть проблемы, до такой степени, что вы, возможно, на самом деле потратили на размышления больше времени, чем я., @timemage