4
\$\begingroup\$

I am trying to implement software implemented PWM for controlling 4 ESCs using an atmega 16 microcontroller.

To achieve that I am sequentially generating the pulses for each ESC one by one in every period of the signal.

Here's the code -

#define TOTAL_ESC 4
int ESC_pulse[TOTAL_ESC];
int ESC_pins[TOTAL_ESC];
int currentESC;

int main(void)
{
    initESCs();
    initUSART();

    sei();
    while (1)
    {
    }
}

void initESCs()
{
    ESC_pulse[0] = ESC_pulse[1] = ESC_pulse[2] = ESC_pulse[3] = 2000;
    ESC_pins[0] = PIND4;
    ESC_pins[1] = PIND5;
    ESC_pins[2] = PIND6;
    ESC_pins[3] = PIND7;
    currentESC = TOTAL_ESC - 1;

    ICR1 = 39999;   //50Hz signal @ 16MHz clock
    OCR1A = 1000;

    DDRD |= 1<<ESC_pins[0] | 1<<ESC_pins[1] | 1<<ESC_pins[2] | 1<<ESC_pins[3];
    PORTD &= ~(1<<ESC_pins[0] | 1<<ESC_pins[1] | 1<<ESC_pins[2] | 1<<ESC_pins[3]);  

    TIMSK |= 1<<OCIE1A;
    TCCR1A |= 1<<WGM11;
    TCCR1B |= 1<<WGM13 | 1<<WGM12 | 1<<CS11;
}

ISR(TIMER1_COMPA_vect)
{
    PORTD &= ~(1<<ESC_pins[currentESC]);    //End prev ESC pulse

    if(currentESC == TOTAL_ESC - 1 && OCR1A != 1000)
    {
        OCR1A = 1000;
        return;
    }

    currentESC = (currentESC + 1) % TOTAL_ESC;

    PORTD |= 1<<ESC_pins[currentESC];   //Start next ESC pulse
    OCR1A += ESC_pulse[currentESC];
}

So, I am trying to generate 50Hz signals with 1ms - 2ms pulses. My CPU Clock is 16MHz and the timer clock is prescaled to 2MHz.

I have an array ESC_pulse for storing the width of the pulses for each ESC. The values in it will range from 2000 to 4000 for the 1ms to 2ms pulses that the ESCs require. The logic I am applying is that everytime the timer compare interrupt occurs I clear the last ESC's output pin and set the next one's and update the OCR1A value with current OCR1A value + current ESC's pulse width as stored in ESC_pulse.

With the hardware generated PWM using the timer my ESC is able to run the brushless DC motor. However, the software generated technique doesn't work. I don't have any equipment to actually see what the generated PWM signals are actually. All that happens is that the ESC generates beep sounds signifying no signal.

I am not sure what I am doing wrong here.

\$\endgroup\$
  • \$\begingroup\$ In this timing range with a little cleverness to coupling you can proably use a sound card as an oscilloscope. The signal will be distorted due to AC coupling but you should be able to figure out the timing. \$\endgroup\$ – Chris Stratton Oct 11 '16 at 14:39
  • \$\begingroup\$ How many possible steps do you need between 1ms and 2ms, i.e. how fine should your control be? \$\endgroup\$ – JimmyB Oct 11 '16 at 16:07
  • 2
    \$\begingroup\$ A lot of people responding to this question seem unaware that in a traditional RC application channels are updated one after the other in round-robbin fashion, not all at once. Updating them all at once is possible but it is not necessary. Also, continuing to update them even when they have not changed likely is required, as an ESC might be built to shut down as a safety measure if it does not receive regular updates. \$\endgroup\$ – Chris Stratton Oct 11 '16 at 17:53
4
\$\begingroup\$

The cause of the problem: OCR1A double buffering

Your program fails because OCR1A is double buffered in all PWM timer modes and you are using such a mode (Fast PWM, TOP = ICR1). When you write a new value to OCR1A, you don't actually change the value used by the timer hardware. Instead, the value stored in OCR1A gets copied to the separate "shadow register" actually used by the timer only once the counter value reaches TOP and restarts from zero again. This is very useful for generating glitch-free hardware PWM, but prevents what you are trying to do (multiple OCR1A updates per timer cycle).

Since this OCR1A update happens only once per timer cycle (at 50 Hz) and your interrupt code is supposed to generate 4 1000 μs - 2000 μs delays + one long delay, you end up with a PWM period of 100 ms (5 timer cycles) and a high time of ~20 ms.

The fix is to configure the timer to a non-PWM mode. The mode best suited for your program is clear timer on compare match (CTC, TOP = ICR1) which works nearly identically but doesn't double buffer OCR1A and OCR1B. The WGM bits found in TCCR1A and TCCR1B should be set to 1100 to achieve this (see the datasheet for details).

Other minor issues:

  • If you access a variable in an interrupt service routine, you need to declare the variable volatile. Leaving the keyword out (among other things) allows the compiler to do optimizations which assume that only the main program flow can modify state.

  • PIND4, PIND5, PIND6 and PIND7 as used by your code are not found in the register definitions provided by AVR-GCC. The more generic macros PINx, PORTx and DDRx are declared in "portpins.h".

  • The sequencing logic could be written in clearer fashion. Your current code has 4 explicit states in the sequence (one for each output pin), of which the last is used for two compare matches: The first for setting the channel 4 output low, and the second for waiting until the sequence should restart. This is weirdly done by using a specific OCR1A value (1000) as a flag. This had me scratching my head for a while.

  • This might be opinion based, but I'd use uint8_t, int16_t and friends rather than e.g unsigned char or short in embedded software. This way you and others know exactly how big your variables are, and they are less verbose as well.

  • Your code sample should include <avr/io.h> and <avr/interrupt.h>, and it should provide a function declaration for void initESCs(). initUSART() is superfluous.

\$\endgroup\$
  • \$\begingroup\$ This is a great answer as you've taken the essentially sound idea attempted in the question, explained why it does not quite work as intended, and proposed how to fix it. \$\endgroup\$ – Chris Stratton Oct 11 '16 at 23:12
  • \$\begingroup\$ Thanks. I haven't tried the solution yet. But, now I know why it didn't work. I should have read the datasheet properly! And actually I didn't put all of the code. Skipped the include part and the USART related code. Thanks again @jms. \$\endgroup\$ – Saurav Deb Purkayastha Oct 12 '16 at 15:59
  • \$\begingroup\$ The OCR1A value is actually irrelevant. I could have just kept it 0 instead of 1000. It was just a desperate attempt to find out what was going wrong in the code. \$\endgroup\$ – Saurav Deb Purkayastha Oct 12 '16 at 16:06
5
\$\begingroup\$

If I understand your code correctly, you have Timer1 in Fast PWM mode using OCR1A to measure PWM duty time and ICR1 for the period. When OCR1A matches the current timer value it triggers an interrupt. You then reload it with a longer time, the idea being that it will match the timer 1~2ms later for the next servo pulse.

The problem with this technique is that in PWM modes the output compare register is doubled-buffered and synchronized to the PWM period, so writing to it during the current PWM cycle will only have affect in the next cycle. This is described in the datasheet on page 98:-

The OCR1x Register is double buffered when using any of the twelve Pulse Width Modulation (PWM) modes... The double buffering synchronizes the update of the OCR1x Compare Register to either TOP or BOTTOM of the counting sequence. The synchronization prevents the occurrence of odd-length, non-symmetrical PWM pulses

So instead of getting an interrupt 1~2ms after you write to OCR1A, you get it 20ms + 1~2ms later.

I'm not sure if it's possible to do it your way using a non-PWM timer mode, but it might be easier to just use a basic timer to time each pulse separately, then add up all the times and subtract from 20ms to get the final pause time.

Many modern ESCs can handle frequencies of 250Hz or higher, so you might even get away with just pushing the pulses out as fast as possible one after the other.

\$\endgroup\$
  • \$\begingroup\$ So the solution could be to just omit that TCCR1A |= 1<<WGM11; \$\endgroup\$ – JimmyB Oct 12 '16 at 9:52
  • \$\begingroup\$ Yes. That will put the timer in CTC mode, which doesn't double-buffer OCR1A. \$\endgroup\$ – Bruce Abbott Oct 12 '16 at 12:27
-1
\$\begingroup\$
  • The first thing you need to do is to get yourself ANY kind of scope. You can buy a DSO138 -- I've bought and assembled several of these to give away to friends. They are cheap and they may work for your needs. (It samples at \$1\:\textrm{MHz}\$ and only supports about \$200\:\textrm{kHz}\$, but that may be okay enough for what you have ahead.) Or you can get something a little fancier that hooks up to a PC, like a Hantek 6022BE. Or you can just buy a cheap, used oscilloscope. There are a number of sites for looking for good prices, many of which I'm sure you know about. But the point here is that if you are working on code to do something important to you like this, you really NEED to have something that allows you to observe and measure what you are producing. And your needs are pedestrian, so it does not cost you an arm and a leg. Probably less than the cost of one ESC controller. Worth doing.
  • The ATmega16 has four PWMs. One PWM on each of two 8-bit timers and only two more PWMs on the 16-bit Timer1. So I get why you are trying to do this in software, using Timer1. There's no other way to go.

What I do NOT see in your code is a queue of any kind. And you need one, I think.

Here's how I'd approach this as a software design solution:

  1. Decide on a specific resolution for my four PWM outputs. I'm not an expert on ESC controllers and I don't know what resolution they use when interpreting the pulse width they receive. But I think I can infer from your use of 1000 and 2000 to suggest that you want precision down to the microsecond. So, in short, you can set \$1.001\:\textrm{ms}\$ and set \$1.583\:\textrm{ms}\$, but you cannot set \$1.6057\:\textrm{ms}\$.
  2. Once you know the required resolution, you can set up Timer1 to provide that resolution. This just sets up the timer's counter rate. (It doesn't cause any interrupts, by itself. That happens when you set up the capture/compares.) In this case, I will assume you have set up Timer1 to count at a rate of \$1\:\textrm{MHz}\$. (No, I'm not going to go research the datasheet on the ATmega16 and try to work out what you actually did do in your code above. Instead, I'm telling you what you need to do.)
  3. Now that the resolution is determined and Timer1 is configured properly to provide a \$1\:\textrm{MHz}\$ counter rate, you set up what is called a 'delta queue.' You can read about them in Douglas Comer's (the inventor) book on XINU, dated into the mid 1980's, if you are interested in a source of the idea. You will need four entries in the queue. One for each of your PWM output pins, individually.
  4. Let's call the \$20\:\textrm{ms}\$ interval thread, \$P_0\$. The other four will be \$P_1\dots P_4\$.
  5. You have two capture/compare registers available. It would probably be more convenient to use both. One of them gets assigned to \$P_0\$ for its regular \$20\:\textrm{ms}\$ event. Let's assign OCR1A to \$P_0\$, so set that value to 20000 and set it up for auto-reload, if possible. (If not, do it in software.) This will set up the basic \$20\:\textrm{ms}\$ timing.
  6. \$P_0\$ will do the following: (a) Drive all PWM outputs HIGH. (b) Insert each of \$P_1\dots P_4\$ into the queue, which by now is always empty, based upon their period values. (c) Clear the timer counter. (d) Load OCR1B with the timer value of the first entry in the delta queue and enable the OCR1B interrupt event. (e) Exit. (It may need to do something to enable the next interrupt event. But that's up to you to figure out.)
  7. On the OCR1B event, you drive the associated pin LOW. Remove the current entry from the queue. While there are remaining entries in the queue and while the remaining 'ticks' (delta value) are exactly zero, drive the associated pin LOW and remove the entry from the queue. Now, you are guaranteed one of two cases: (a) there are no remaining queue entries -- in this case, just return as there is nothing more to do. (b) there are remaining queue entries and the top one holds a non-zero timer value -- load this value into OCR1B and make sure it can re-interrupt, then just return.

That it. The whole process. The delta queue is set up to hold a 'tick' value, which is the count in microseconds for the next event to occur. When inserting an entry into the queue, you subtract all the 'ticks' (delta value) in prior entries before inserting. So let me demonstrate with an example to make this clear.

Suppose the timing value for \$P_1\dots P_4\$ are: 1573, 2000, 1206, and 1573. Then the queue would look like (not including the associated pin, which is also required here):

$$\begin{array}{lrcl} & delta & OCR1B & Thread\\ 1 & 1206 & 1206 & P_3 \\ 2 & 367 & 1573 & P_1 \\ 3 & 0 & 1573 & P_4 \\ 4 & 427 & 2000 & P_2 \end{array}$$

That's listed in queue order. However, I'd create a five element array with indices from 0 to 4, with [0] assigned as the queue's \$head\$ pointer. So the above queue would like the following in array order:

$$\begin{array}{lcrc} & next & delta & OCR1B\\ 0 & 3 & n/a & n/a \\ 1 & 4 & 367 & 1573 \\ 2 & 0 & 427 & 2000 \\ 3 & 1 & 1206 & 1206 \\ 4 & 2 & 0 & 1573 \end{array}$$

When \$P_0\$ sets about inserting all four into the queue (which is empty when \$P_0\$ starts, since all of \$P_1 \dots p_4\$ have by this time all expired), that is the resulting queue when \$P_0\$ exits and returns from its interrupt event. But just before exiting, as mentioned, \$P_0\$ will load the first OCR1B value in the queue (1206 here) and place that into OCR1B.

When the OCR1B event triggers, the first entry in the queue tells the code which pin to drive LOW. Then the entry is removed from the queue. For the first OCR1B event, this removes the 1206 entry, leaving the delta value of 367 value, which is not zero. So the OCR1B value of 1573 is now loaded into OCR1B.

When the next event occurs, the OCR1B event will now drive that associated pin LOW, as well, and remove that entry from the queue. Now the next entry has a delta value of 0 in it. Because of that, it MUST mean that there is another pin to drive LOW, so the OCR1B event continues and drives that pin LOW as well and removes that entry from the queue. At this point, there is only one remaining entry in the queue, which has the delta value 427, which is also non-zero. So the code now loads the OCR1B value of 2000 into the compare register, OCR1B, and exits.

The last OCR1B event now triggers and the code drives that pin LOW, as well, and removes the entry. There are no more entries. So the code just exits. It's all done and the only thing remaining is to wait for the OCR1A event to re-occur.

That's the process to follow, I think.

Here's an example of how I might set up the queues for this, most especially illustrating how to insert into the delta queue. The function qinsert() does this job. I'd have \$P_0\$ call qinsert() for each of the four PWMs, as part of its job.

uint8_t qp[5];          /* prior in queue reference */
uint8_t qn[5];          /* next in queue reference */
uint16_t qk[5];         /* delta value */
uint16_t qv[5];         /* OCR1B value */
uint8_t qpin[5];        /* pin position 0..7 */

void qinit( void ) {

    qn[0]= qp[0]= 0;
    qk[0]= 0xFFFF;

    return;
}

uint8_t qinsert( uint8_t node, uint16_t key ) {
    uint8_t prv= 0, nxt;
    uint16_t nxtkey;

        qv[node]= key; /* optional, depending on overall design */
        for ( nxt= qn[prv]; (nxtkey= qk[nxt]) < key; prv= nxt, nxt= qn[nxt] )
            key -= nxtkey;
        if ( nxt != 0 )
            qk[nxt]= nxtkey - key;
        qk[node]= key;
        qp[node]= prv;
        qn[node]= nxt;
        qp[nxt]= qn[prv]= node;

    return node;
}

uint8_t qunlink( uint8_t node ) {
    uint8_t prv= qp[node], nxt= qn[node];

        qn[prv]= nxt;
        qp[nxt]= prv;

    return node;
}
\$\endgroup\$
  • 1
    \$\begingroup\$ To analyze digital signals a logic analyzer is preferrable over an oscilloscope. \$\endgroup\$ – JimmyB Oct 11 '16 at 16:00
  • \$\begingroup\$ @JimmyB I know. I have an MSO, as well. But they are expensive. Sometimes beggars can't be choosers. \$\endgroup\$ – jonk Oct 11 '16 at 16:04
  • \$\begingroup\$ Microsecond resolution may be a little over the top. He'll probably get away with much less, maybe even only 32 steps. That'd give us some room for software PWM (500 CPU clocks per increment). \$\endgroup\$ – JimmyB Oct 11 '16 at 16:04
  • 1
    \$\begingroup\$ In fact a moderate speed USB-ased logic analyzer is a lot cheaper than a scope. More importantly claim that there is no queue is effectively false, because there is something far more appropriate. The ISR is clearly written to cycle through the four channels, with a pulse to update each in turn. There might be issues in the implementation details, but the idea is right. Your proposal is severely overcomplicated and it is not even clear that is will necessarily do what is required, which is to update each channel at a fairly regular rate. \$\endgroup\$ – Chris Stratton Oct 11 '16 at 17:48
  • 2
    \$\begingroup\$ No, what you wrote is a bad idea that does not really fit the application. A queue is appropriate when you do not know what you are going to need to do; here you (should) know exactly what you need to do, which is provide regular outputs, be they updated or unchanged. Hence all the list structure is a waste of CPU time, memory resources, and adds unnecessary failure modes. Bad engineering, plain and simple. \$\endgroup\$ – Chris Stratton Oct 11 '16 at 17:57

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.