2

So, I managed to fry my cheap servo/ESC (electronic speed controller) and decided to make my own using an ATTiny13A on some perfboard. But, I have run into some issues getting the servo to respond. I'm assuming the servo.h library is not compatible with the ATTiny13A because of incompatible internal clock speeds not creating the right PWM signals. And I haven't found a solution for this.

I have messed around with the Servo8Bit.h library, but this also doesn't seem to work (as its designed for the ATTiny45 and the ATTiny85 ). Is the ATTiny13A even able to produce the needed PWM signals?

Any help/links to documentation would be appreciated!

Thanks in advance.

Code:

#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

3
  • 1
    Servo8Bit relies on an 8-bit timer, and the ATtiny13A does have an 8-bit timer. It should not be too hard to port this library to that micro. Study the source code, then study the datasheet of the tiny13A (mostly the chapter dedicated to the timer). You should then be able to easily see what changes are needed. – Edgar Bonet Apr 25 at 18:40
  • Are you sure, that the ATtiny is actually running at the frequency you think it does? I did have timing problems with my ATtiny85, whem trying to use it with 8MHz. The fuses actually were still set for 1MHz. I first had to burn the bootloader to set the fuses correctly. – chrisl Apr 25 at 19:06
  • does a blink sketch run at an expected speed? – jsotola Apr 25 at 20:56
1

Servo8Bit and ATTiny13(A)

I had a look at the Servo8Bit and had pretty much the same thoughts on this as Edgar Bonet in his comment:

Servo8Bit relies on an 8-bit timer, and the ATtiny13A does have an 8-bit timer. It should not be too hard to port this library to that micro. Study the source code, then study the datasheet of the tiny13A (mostly the chapter dedicated to the timer). You should then be able to easily see what changes are needed.

Ignoring the timing differences between the ATTinyx5 and ATTiny13(A), I just went ahead and saw if I could get it to compile. And yeah, it's pretty easy to compile. This mostly consists of adding 0 to some hardware register names. The bad news is:

Sketch uses 1486 bytes (145%) of program storage space. Maximum is 1024 bytes.
Global variables use 31 bytes (48%) of dynamic memory, leaving 33 bytes for local variables. Maximum is 64 bytes.

Sketch too big; see http://www.arduino.cc/en/Guide/Troubleshooting#size for tips on reducing it.

Error during build: text section exceeds available space in board

So, yeah, that was a bit much to hope for, I guess. The library does use ISRs to support a number of servos kind of like the more common servo library that you'd use on an UNO. I haven't looked into chopping it down to size; I expect that will be impractical.

PWM... sort of.

Prior to considering modifying Servo8Bit, I kind of thought it made sense to just use the timer counter directly to produce a waveform. The problem is the conventional servo signal is 20ms long, and does all of its control in a 1ms window. If you try to fit the whole 20ms frame into a single 8bit timing interval, what you end up with is pretty crappy angular control. So, doing it is a bit painful without a 16-bit timer counter. But, I guess that's the game.

So, the below is a crude and barely tested attempt at it. The code that follows is state machine inside the OCR0B overflow vector that schedules the rising an falling edges is "normal" mode. Just to warn you, I don't actually have an ATTiny13(A) on hand. And in fact I don't happen to have a servo hand either. But I do have an ancient storage scope and an ATTiny85 and the ability to compile for ATTiny13(A), and a calculator for working out the 8 MHz vs 9.6 MHz difference.

So here's what I came up with, which I think (but don't know) will work if you compile and upload using MCUDude MicroCore, ATTiny13(A) with the 9.6 MHz internal oscillator chosen.

It's only designed to operate 1 servo. Extending it to two should be easily doable. Going beyond that you're likely run into problems.

Code:

// +---------------------------------------------------------------------------------------+
// |                                                                                       |
// |                                 +-------------+                                       |
// |                                 |             |                                       |
// |                                 |      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 Hz (hobby servo frame frequency); period 20 ms
//
// In the below you'll see the numbers 150 and 300 and 3000.
// The reason for these numbers is that I've chosen to to use a /64
// timer prescaler.  At 9.6 MHz, with a /64 prescaler, 1 millisecond
// is 150 timer timer counts.  There's as fixed 1 MS high in the servo,
// followed by 0 to 1ms variable portion.  The number 150 helpfully
// fits within the 8-bit timer registers and also gives a reasonable
// number of increments to the servo control.  For 180 degrees, 150 increments
// is 1.2 degrees.
//
// The maximum high time is the fixed 1ms high time and the maximum
// variable high time, totalling 2 MS, or 300 timer ticks, that's what that
// number is about.
//
// 3000 is the number of timer ticks for a full 20ms signal frame,
// that is 20 * 150 = 3000.
//
//
// Deriving the 150 figure:
//
//  At 9.6 MHz and /64 prescaler
//
//        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

  //
  // The ISR is a state machine with four macro states.
  //
  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
  };

  //
  // Macro state variable
  //
  static t_signal_generator_state g_signal_generator_state =
    t_signal_generator_state::timing_to_variable_high_period;

  // low_time_remaining is an extended state variable
  // for the timing_to_start_of_pulse state
  static uint16_t low_time_remaining;


  switch (g_signal_generator_state) {
    case t_signal_generator_state::timing_to_variable_high_period:
      // We just went high at the OCR0B value that triggered this interrupt
      // and we need to retrigger ((MAXIMUM_HIGH_PERIOD==300) - 256 == 44)
      // timing cycles later such that there are 256 timing cycles left of the
      // 2ms (maximum) high part of the frame.
      OCR0B += (MAXIMUM_HIGH_PERIOD - 256);  // 2ms worth of timer nicks minus the resolution of our timer

      // We're remaining HIGH as we head into the variable portion of the HIGH period.
      g_signal_generator_state = t_signal_generator_state::timing_to_set_pulse_width;
      break;
    case t_signal_generator_state::timing_to_set_pulse_width:
      // We're retrieving the global variable which holds a (potentially new) value from 0 to 150
      // representing a span of 0ms to  1ms (the full range of the servo), but
      // we're starting out the variable pulse a head of time, part way through the always-on first ms.
      // There are (150 - 44) timer cycles, that's 1 ms worth of timer cycles
      // where the timer remains high to finish out first millisecond of HIGH.
      // So we add this to the requested variale pulse width time.
      l_pulse_width = g_pulse_width + (ONE_MS_WORTH_OF_TICKS - 44);

      // Set adjust pulse width
      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:

On the scope, this produces servo signal that should sweep a servo on pin OCR0B (PB1). Something like 4 to 5 seconds going one way and same going back.

If I put an ATTiny13A on an order sometime and manage to dig out a servo myself, I'll actually test it. It may be a very long time before I can do that though. But the results on the scope look good. Admittedly everything is scaled by 9.6/8.0. It should be accurate on a ATTiny13(A). I verified that it compiles for it. The workings of the thing are explained in the code as I can.

For comparison:

Sketch uses 490 bytes (47%) of program storage space. Maximum is 1024 bytes.
Global variables use 10 bytes (15%) of dynamic memory, leaving 54 bytes for local variables. Maximum is 64 bytes.

That's still a good chunk of what little code space you have, but it should be enough to read a switch and potentiometer. This is using the timer the device has, so millis()/delay() usage are kind of out-the-window, I think, I didn't try to see what happen when you call them or study their code in MicroCore. You could recreate your own millis() by making a timer overflow interrupt and it would overflow at about 1.7 ms intervals. As you can see, I just used __builtin_avr_delay_cycles to keep things simple.

If you, or anyone else, gets it working on at ATTiny13 before I try it myself, I'm curious to know that it worked (or didn't).

5
  • +1. A few comments: 1. MAX_WASTE_CYCLES = 256 would reduce the interrupt load and remove the need for OCR0B += MAX_WASTE_CYCLES;. 2. Possible race condition: if low_time_remaining is 1 and the interrupt is slow or delayed, TCCR0A could be updated too late. This could be mitigated by putting the variable part of the low time (low_time_remaining % MAX_WASTE_CYCLES) at the beginning of the low period. 3. The include file <util/delay.h> provides a macro for writing the fixed delay in a more convenient form: _delay_ms(32.25);. The generated code is the same. – Edgar Bonet Apr 28 at 8:41
  • Yeah, I was considering bumping MAX_WASTE_CYCLES all the way up (and probably giving it a better name). The original plan, I think, was to find a value to advance OCR0B by that such that the final pass through the that state somewhere TCNT0 and OCR0B half the timer ticks (0x80) ahead the rising edge, so it was scheduling that rising at at a more idea moment. So that constant was supposed to be something like FINAL_APPROACH_LENGTH_TO_HIGH_EDGE, but I just never got to doing that. That sounds like what you're describing with the modulus. – timemage Apr 28 at 12:08
  • I was hoping to find a way to do without without division (modulus), like find some happy medium that was close enough on both extremes. But yeah, the way it is now it probably makes more sense to put it all the way up to 0xFF. – timemage Apr 28 at 12:12
  • Modulus 256 is cheap: it is achieved automatically by casting low_time_remaining to uint8_t. – Edgar Bonet Apr 28 at 12:41
  • I'm aware with regard to modulo 256. I'm probably not explaining myself well or I'm just not awake enough to be thinking about it. When I get back into looking at the code, I'll look at your comments again, and it may get updated in exactly the way you're thinking. I don't have that much time into that part of the problem, to the point where you may have actually spent more time thinking about than I have. – timemage Apr 28 at 12:47

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.