A custom m328p based UART->PWM controller.

Hey. Thank you for any advice. It would be greatly appreciated.
PS: I know PWM driver ICs are available...

For a bit of practice (and also to see if I can do it basically) I am trying to make a 328p act as a PWM driver with a UART interface (could do i2c as well / instead of maybe).

I want to use it possibly later to make a basic control board for a quadruped.

I know I only need 12 channels for a quad...but I thought "hell...why not do 18".

The PORTC and PORTB pins respond well to my coding...they can be set to PWM Duty cycles as stated in the setup()...

PORTD on the other-hand gives me a load of rubbish...and I can not think why!

Any help would be greatly appreciated. I could push on and just use PORTB and PORTC but having the extra PWM ports would be nice!

Here are the cycles I want: (0->100% in 20% increments on each channel...6 per portX):

 PWMvalue[0] = dutyCycle(0);    //PB0
  PWMvalue[1] = dutyCycle(20);    //PB1
  PWMvalue[2] = dutyCycle(40);    //PB2
  PWMvalue[3] = dutyCycle(60);    //PB3
  PWMvalue[4] = dutyCycle(80);     //PB4
  PWMvalue[5] = dutyCycle(100);     //PB5
  PWMvalue[6] = dutyCycle(0);    //PD2
  PWMvalue[7] = dutyCycle(20);    //PD3
  PWMvalue[8] = dutyCycle(40);    //PD4
  PWMvalue[9] = dutyCycle(60);    //PD5
  PWMvalue[10] = dutyCycle(80);   //PD6
  PWMvalue[11] = dutyCycle(100);   //PD7
  PWMvalue[12] = dutyCycle(0);   //PC0
  PWMvalue[13] = dutyCycle(20);   //PC1
  PWMvalue[14] = dutyCycle(40);   //PC2
  PWMvalue[15] = dutyCycle(60);   //PC3
  PWMvalue[16] = dutyCycle(80);   //PC4
  PWMvalue[17] = dutyCycle(100);   //PC5

Full code:

/*
   An ~18 channel PWM software driver with Serial I/O

   Use a timer to increment a count.
   Every overflow (256 counts)...
   With a /8 clk pre-scaler...16MHz / 8 / 256 = 7.8125KHz per OVF

   Run the OVF_ISR:
   Set PORTA and PORTB to the 2 values generated in the main().

  PORTB - Gives 6 (PB6 and 7 used for crystal....)
  PORTD - Gives 6 (PD0 and PD1 are Rx and Tx UART)
  PORTC - Gives 6 (If not using I2C)

  Options could include dropping Rx and Tx for I2C
  Or dropping I2C for UART.

  Total = 18 pins for PWM + 2 for communications.

  Assuming Serial UART for command use:

  PORTB = XTAL XTAL PB5 PB4 PB3 PB2 PB1 PB0 = 6
  PORTD = PD7  PD6  PD5 PD4 PD3 PD2 Rx  Tx  = 6
  PORTC =  -   RST  PC5 PC4 PC3 PC2 PC1 PC0 = 6

  An array of bytes can hold the wanted value for each PWM
  channel 0-17. - PWMvalue[0-17]

  Timer2 will be used to tick.
  It will tick 0-255 and overflow.
  The number of overwlows is recorded.
  On each overflow, the pin-states are updated.
  After 256 overflows...the PWM phase resets.

  PWMout() will set the pins on an oveflow...

*/

volatile unsigned long overflows = 0;
volatile boolean overflowed = false;


volatile byte PWMvalue[18];   // An array in RAM to hold the PWM values.


void setup() {
  Serial.begin(230400);


  PWMvalue[0] = dutyCycle(0);    //PB0
  PWMvalue[1] = dutyCycle(20);    //PB1
  PWMvalue[2] = dutyCycle(40);    //PB2
  PWMvalue[3] = dutyCycle(60);    //PB3
  PWMvalue[4] = dutyCycle(80);     //PB4
  PWMvalue[5] = dutyCycle(100);     //PB5
  PWMvalue[6] = dutyCycle(0);    //PD2
  PWMvalue[7] = dutyCycle(20);    //PD3
  PWMvalue[8] = dutyCycle(40);    //PD4
  PWMvalue[9] = dutyCycle(60);    //PD5
  PWMvalue[10] = dutyCycle(80);   //PD6
  PWMvalue[11] = dutyCycle(100);   //PD7
  PWMvalue[12] = dutyCycle(0);   //PC0
  PWMvalue[13] = dutyCycle(20);   //PC1
  PWMvalue[14] = dutyCycle(40);   //PC2
  PWMvalue[15] = dutyCycle(60);   //PC3
  PWMvalue[16] = dutyCycle(80);   //PC4
  PWMvalue[17] = dutyCycle(100);   //PC5

  startTimer();

  DDRB = 0xFF; // Set port B as outputs.
  DDRD |= (1 << PD2) | (1 << PD3) | (1 << PD4) | (1 << PD5) | (1 << PD6) | (1 << PD7);
  DDRC |= (1 << PC0) | (1 << PC1) | (1 << PC2) | (1 << PC3) | (1 << PC4) | (1 << PC5);


  PORTB = 0;  // Turn all portB pins off.
  PORTD = 0;
}

void loop() {

  //Check for update values...
  if (Serial.available()) {
    processSerial();
  }
}

void startTimer() {

  TCCR2A = 0; // No PORT outputs...
  TCCR2B = 0; // Reset Timer Control Register...
  TCCR2B |= (1 << CS20); // Start Timer...No precale. 22:20 (001 = no scaling, 010 = /8 , 011 = /32, 100 = /64);
  TIMSK2 = 0;
  TIMSK2 |= (1 << TOIE2); // Overflow enabled when I bit in SREG enabled.

  TIFR2 = 0;

  SREG |= 0b10000000; // Enable I-bit in SREG...interrupts begun.
  TCNT2 = 0;
}

ISR(TIMER2_OVF_vect) {

  overflows++;
  overflowed = true;
  PWMout();   // Set the pins
  if (overflows > 255) {
    overflows = 0;
  }
}

byte PWMout() {
  /* The timer goes from 0-256 and then overflows.
      We can use say 256 overflows to determine what state
      eahc pin needs to be in. This gives "8 bit resoltuion...".
      For example, if we want PWM channel 0 (PORTB0) to be at 50%...
      it needs to be on for 128 over flows...then off for the rest.
      We can use an array to hold the "max" values for each channel (can be sent via serial!).
      When the number of overflows > "max value in index" then that pin is set 0.

        Assuming Serial UART for command use:


    BITS- = 7    6    5   4   3   2   1   0
    PORTB = XTAL XTAL PB5 PB4 PB3 PB2 PB1 PB0 = 6
    PORTD = PD7  PD6  PD5 PD4 PD3 PD2 Rx  Tx  = 6
    PORTC =  -   RST  PC5 PC4 PC3 PC2 PC1 PC0 = 6

  */



  unsigned int B = 0; // holder int for PORT B.
  unsigned int D = 0; // holder int for PORT D.
  unsigned int C = 0; // holder int for PORT C.

  // SET PORTB 0-5 (6 and 7 are OSCILLATOR pins).
  for (int x = 5; x >= 0; x--) {      // Read from pwmvalues[5]->[0]
    if (PWMvalue[x] < overflows) {   //If value <= overflows...make it HIGH.
      B += 1;
      B = B << 1;
    } else {                          //Otherwise make it low (shift along a zero).
      B = B << 1;
    }
  }
  unsigned int portB = B >> 1;

  //set PORTD...the next 6 array values and bits...
  for (byte x = 11; x > 5; x++) {  // Read from 11 to 5.
    if (PWMvalue[x] < overflows) {
      D += 1;
      D = D << 1;
    } else {
      D = D << 1;
    }
  }

  unsigned int portD = D << 2;   // Shift again...PORTD bit 0 and 1 are Tx and Rx...so leave as 0.

  // SET PORTC 0-5 (6 and 7 are NC and RST pins).
  for (byte x = 17; x > 11; x--) {      // Read from pwmvalues[17]->[11]
    if (PWMvalue[x] < overflows) {   //If value <= overflows...make it HIGH.
      C += 1;
      C = C << 1;
    } else {                          //Otherwise make it low (shift along a zero).
      C = C << 1;
    }
  }
  unsigned int portC = C >> 1;

  PORTC = ~portC;
  PORTD = ~portD;
  PORTB = ~portB;
//  Serial.print("Overflows:\t");
//  Serial.print(overflows);
//
//  Serial.print("\t B:");
//  printBinary(B);
//  Serial.print("\t D:");
//  Serial.print(D,BIN);
//  Serial.print("\t C:");
//  printBinary(C);
//  Serial.println("");


}

void processSerial() {
  //'A' = channel 0...'B' = 1 etc.
  //First byte is channel.
  // Second is value.

  byte channel = Serial.read() - 65;
  delay(1);
  byte val = Serial.read();
  Serial.print(channel);
  Serial.print(",");
  Serial.println(val);

  PWMvalue[channel] = val;
  Serial.flush();
}

byte dutyCycle(byte percent) {
  float range = 256.0;
  return (range / 100.0) * percent;
}

void printBinary(byte val) {
  for (byte mask = B10000000; mask; mask = mask >> 1) {
    if (mask & val) {
      Serial.print("1");
    } else {
      Serial.print("0");
    }
  }

}

Here is the output on my logic analyser:

PORTB:

PORTC:

PORTD....the garbage!:

This does not look right

  for (byte x = 11; x > 5; x++)

shouldn't it be x-- ?

Wouldn't it be much easier to enter values from 0-255 for the duty cycle so you can avoid the need for floating point maths. And it would be the same as how analogWrite() works

Your serial input code is not robust. Have a look at the examples in Serial Input Basics - simple reliable ways to receive data. There is also a parse example to illustrate how to extract numbers from the received text. And note that Serial.flush() waits until the output buffer is empty.

...R

Robin2:
This does not look right

  for (byte x = 11; x > 5; x++)

shouldn't it be x-- ?

Wouldn't it be much easier to enter values from 0-255 for the duty cycle so you can avoid the need for floating point maths. And it would be the same as how analogWrite() works

Your serial input code is not robust. Have a look at the examples in Serial Input Basics - simple reliable ways to receive data. There is also a parse example to illustrate how to extract numbers from the received text. And note that Serial.flush() waits until the output buffer is empty.

...R

Thanks! Stupid mistake ha!

I only quickly wrote the Serial thing as like a "test" to see it i were even feasible. Mind it is the next step.
The duty cycles as well...I will be using integer maths but did a quick dirty test again.

Thank you for the input :). PS: It works.