Hi All,
I have been trying to wrap my brain around Due PWM functionality. In several examples in the Arduino forums and elsewhere, I have seen reference to registers called PWM_TCR, PWM_TPR, PWM_TCNR, PWM_TPNR, however I have not been able to find any reference to any of these registers in the data sheet.
I'm using PlatformIO under ESS Code (Linux) and when I enter those register names (prefixed with REG_ or PWM->) Code finds them and can display the definitions, and the code compiles. So they exist. But where are they documented?
All advice gratefully received.
In the board description in boards.h you will find:
boardname.build.mcu=esp8266
boardname.build.core=esp8266
This tells the build process the name of the core and the type of processor. Each core has its own Arduino.h file. Deep in the files included by Arduino.h there is an include file that declares the processor registers and fields based on the processor type.
For the 'avr' processors, the declarations are in the compiler library 'avr/io.h' file, which includes files specific to a chip or family of chips.
In the esp8266 core it seems to be the esp8266_peri.h file that contains the register declarations.
johnwasser:
In the board description in boards.h you will find:boardname.build.mcu=esp8266
boardname.build.core=esp8266
He's not using an 8266. He's using a Due, which uses an Atmel SAM3X8e ARM-based chip.
Regards,
Ray L.
Well well well.....PWM_TCR, PWM_TPR, PWM_TCNR, PWM_TPNR are registers used with a PDC DMA for the PWM peripheral, and yes you can't explicitly find them with these names in the datasheet. However, you will find them in the header file:
Note: Sometimes these registers for PDC DMA are labelled differently, e.g. PDC_PWM->PERIPH_TPR
Thanks for all the replies, much appreciated.
My problem is not finding the definitions, Code found them for me and will helpfully open the file where they are defined by pressing PF12. fwiw they are in component_pwm.h, buried very deep in the Arduino libraries.
Thanks especially to @ard_newbie who pointed me in the right direction, I think. The registers appear to be associated with the DMA controller, not the PWM controller specifically. In fact it was an example you posted here (Arduino Due PWM Interrupt - Arduino Due - Arduino Forum) that I have been working my way through trying to understand how Due registers work, and where I stumbled over PWM_TPR et al.
I am trying to use the PWM function to create a PPM (Pulse Position Modulation) output stream, which is (in my case) a stream of nine pulses, each with a different period and duty. The total time of the "frame" of nine pulses is fixed at 22.5ms. The high part of the pulse is variable between 1000us and 2000us, except for the last pulse which is 22500 less the sum of the periods for each of the previous eight pulses. The low part of each pulse is always 300us.
I have already done this on an Arduino Nano using the registers and an ISR, and it works very well. But now I want to reproduce that on the Due, and I also like the idea of using DMA to control the pulse width values.
A question that has been bugging me. I understand that the combination of PREA and DIVA sets the frequency of the PWM output, but as I am changing the period on every pulse, isn't that modifying the frequency on every pulse? so the PREA and DIVA values are somewhat irrelevant? If not I don't understand what I should be setting these values to. I have to say that in initial testing, the PREA and DIVA values have a radical effect on the output. I have set the CPRD2 to 2000, and CDTY2 to 1000, and if the PREA/DIVA combo is set to CLK4 and 525 I get an output signal with period 3000us and duty 50%. Other combos give wildly different results, some up to 96ms, and with no pattern I've been able to discern. I've done a lot of trial and error testing, and so far the number of trials exactly equals the number of errors.
Lots more study to do. Lucky I'm stuck in lockdown and have nothing better to do!
All advice gratefully received.
Once a PWM channel has been initialized with a given Frequency / Duty cycle in setup(), you can update Frequency and/or Duty cycle with PWM_CPRDUPD/PWM_DTYUPD in loop().
After each new PWM_CPRDUPD/PWM_DTYUPD, the new waveform is output exactly right after the actual period with the previous settings. You are not supposed to update PREA/DIVA in loop().
IMO, a PDC DMA is not the solution for your project since PDC DMA for PWM is designed to output waveforms of several PWM synchro channels.
There are lots of example sketches in the DUE sub forum to update PWM Frequency and/or Duty cycle.
Thanks for the tip. I'll have a look.
Do you absolutely require PWM to generate the PPM signal? I'm using a TC, instead, to output the same PPM signal with different parameters on Due pins 11 and 12.
I have no idea whether the attached code will translate directly to PlatformIO, but the concept should still work. You'll need a function to load the ppm_channels[] array with data. DMA is up to you.
Jon
/*
The original code came from a post on the Arduino forum.
It was written to output on the Due D2 pin. I needed D11 since the
"shield" I'd designed used that pin.
There's a mini-menu in the serial monitor to demonstrate some of the
features I need to support.
Pin D11 is used as PPM to an RF deck and has a driver transistor.
D12 is intended as an aux output for flight sim. Since the sim and
RF deck could have different requirements for polarity and separation
pulse, they need to be independently programmable.
Use an o'scope triggered on pin D8 to watch the PPM waveforms on D11 or D12.
*/
/*
Register A is loaded with a value for the channel separation pulse, typically around 400uS.
Different radio brands have different specs, so this value is user programmable. The register
A value does not change during the generation of PPM.
An interrupt is attached to counter = Register C. Register C is later loaded with the actual
time of a single channel cycle from an array containing nine different values representing the
eight different channels and the “leftover” time separating PPM frames. Polarity of the waveform
is determined by ACPA and ACPC bits in TC_CMR. One will set A and the other will clear A. For
this example, we’ll use ACPA_SET and ACPC_CLEAR.
When the counter = Register C, an NVIC interrupt occurs, the signal will clear (ACPC_CLEAR).
The counter begins counting from zero until it reaches the value of Register A and the signal
sets (ACPA_SET). The counter continues until it reaches the value in Register C, where the
signal again clears (ACPC_CLEAR) and the NVIC interrupt is triggered. In the interrupt routine,
Register C is loaded with the next value from the PPM array. When all channel values in the
array have been processed, the final array element value is calculated from the known frame
time period and the sum of eight channel values (frameTime – sum).
Register B, in the same counter, can hold a different compare value, as well as use a different
BCPB and BCPC configuration. This can allow for a different channel separator pulsewidth,
as well as matching or opposing polarity, while maintaining the frame timing and “data”.
*/
bool d11Normal = 1; //1 is normal, 0 is inverted
bool d12Normal = 1;
uint32_t ppm_channels[8];
uint32_t Frame; //time of frame repetition
uint32_t Sum; //running total of each channel's time during a frame
// timer is clocked at 42MHz, so 42 ticks per us
uint32_t periods[]={1000*42,1000*42,1000*42,1000*42,1000*42,1000*42,1000*42,1000*42,1000*42};
uint32_t num_periods=8+1; // number of channels +1
uint32_t d11Sep = 0.5*1000*42; // channel separation pulse for D11
uint32_t d12Sep = 0.4*1000*42; // channel separation pulse for D12
//********************************************************************
void TC8_Handler()
{
static bool toggle = 0;
uint32_t dummy = TC2->TC_CHANNEL[2].TC_SR; // vital - reading this clears some flag otherwise you get infinite interrupts
static uint16_t i=0;
TC2->TC_CHANNEL[2].TC_RC = periods[i++];
i %= num_periods;
if (i == 0) { //for o'scope sync. Also indicates interrupt is triggering...
toggle = !toggle;
digitalWrite(8,toggle);
}
}
//********************************************************************
void setup(){
Serial.begin(115200);
PMC->PMC_WPMR = 0x504D4300; //power management controller write protect
PMC->PMC_PCER1 |= PMC_PCER1_PID35; //PMC Peripheral Clock Enable Register 1, set PID35 high
PIOD->PIO_PDR |= PIO_PDR_P7 | PIO_PDR_P8; // disable PIO, enable peripherals for D11 & D12
PIOD->PIO_ABSR |= PIO_ABSR_P7 | PIO_PDR_P8; // select peripherals for D11 & D12
TC2->TC_WPMR = 0x54494D00; // enable write to registers, can comment & still runs!
TC2->TC_CHANNEL[2].TC_CMR = TC_CMR_TCCLKS_TIMER_CLOCK1 // channel mode register base configuration
| TC_CMR_EEVT_XC0
| TC_CMR_WAVSEL_UP_RC
| TC_CMR_WAVE;
if (d11Normal) {
TC2->TC_CHANNEL[2].TC_CMR |= (TC_CMR_ACPA_SET | TC_CMR_ACPC_CLEAR); // channel mode register - D11 normal
}
else {
TC2->TC_CHANNEL[2].TC_CMR |= (TC_CMR_ACPA_CLEAR | TC_CMR_ACPC_SET); // channel mode register - D11 inverted
}
if (d12Normal) {
TC2->TC_CHANNEL[2].TC_CMR |= (TC_CMR_BCPB_SET | TC_CMR_BCPC_CLEAR); // channel mode register - D12 normal
}
else {
TC2->TC_CHANNEL[2].TC_CMR |= (TC_CMR_BCPB_CLEAR | TC_CMR_BCPC_SET); // channel mode register - D12 inverted
}
TC2->TC_CHANNEL[2].TC_RC = 100000000; // initialize counter period with some value
TC2->TC_CHANNEL[2].TC_CCR = TC_CCR_SWTRG | TC_CCR_CLKEN; // start counter
TC2->TC_CHANNEL[2].TC_IER = TC_IER_CPCS; // enable interrupt on counter = rc
TC2->TC_CHANNEL[2].TC_IDR = !TC_IDR_CPCS; // disable other interrupts
TC2->TC_CHANNEL[2].TC_RA = d11Sep; // channel separation pulse for D11
TC2->TC_CHANNEL[2].TC_RB = d12Sep; // channel separation pulse for D12
Frame = 22 *1000*42; // ppm frame length = 22ms
//my tx generates values in half-microseconds: counts of 2000 to 4000 translate to 1000uS to 2000uS
ppm_channels[0]= 2000; // channel 1, 1.0mS
ppm_channels[1]= 2000; // channel 2, 1.0mS
ppm_channels[2]= 3000; // channel 3, 1.5mS
ppm_channels[3]= 4000; // channel 4, 2.0mS
ppm_channels[4]= 2000; // channel 5, 1.0mS
ppm_channels[5]= 2000; // channel 6, 1.0mS
ppm_channels[6]= 2000; // channel 7, 1.0mS
ppm_channels[7]= 2000; // channel 8, 1.0mS
NVIC_EnableIRQ(TC8_IRQn); // enable TC2 interrupts
Serial.println(" i\tinverted PPM on D12");
Serial.println(" n\tnormal PPM on D12");
Serial.println(" I\tinverted PPM on D11");
Serial.println(" N\tnormal PPM on D11");
Serial.println(" k\tkill PPM");
Serial.println(" r\trestart PPM");
}
//********************************************************************
void loop(){
char serIn = ' ';
// Calculate the 8 channels
Sum = 0;
for (int i = 0; i < 8; i++) {
periods[i] = ppm_channels[i] * 21;
Sum += periods[i];
}
// Calculate the sync frame
periods[8] = Frame - Sum;
serIn = Serial.read();
switch (serIn) {
case 'i': //D12 (aux) inverted PPM
TC2->TC_CHANNEL[2].TC_CMR &= ~(TC_CMR_BCPB_SET | TC_CMR_BCPC_CLEAR); //zero out TIOB compare bits, maintain current TIOA compare bits
TC2->TC_CHANNEL[2].TC_CMR |= (TC_CMR_BCPB_CLEAR | TC_CMR_BCPC_SET); //channel mode register - D12 inverted
d12Normal = 0;
break;
case 'n': //D12 (aux) normal PPM
TC2->TC_CHANNEL[2].TC_CMR &= ~(TC_CMR_BCPB_CLEAR | TC_CMR_BCPC_SET); //zero out TIOB compare bits, maintain current TIOA compare bits
TC2->TC_CHANNEL[2].TC_CMR |= (TC_CMR_BCPB_SET | TC_CMR_BCPC_CLEAR); //channel mode register - D12 normal
d12Normal = 1;
break;
case 'I': //D11 (RF module) inverted PPM
TC2->TC_CHANNEL[2].TC_CMR &= ~(TC_CMR_ACPA_SET | TC_CMR_ACPC_CLEAR); //zero out TIOA compare bits, maintain current TIOB compare bits
TC2->TC_CHANNEL[2].TC_CMR |= (TC_CMR_ACPA_CLEAR | TC_CMR_ACPC_SET); //channel mode register - D11 inverted
d11Normal = 0;
break;
case 'N': //D11 (RF module) normal PPM
TC2->TC_CHANNEL[2].TC_CMR &= ~(TC_CMR_ACPA_CLEAR | TC_CMR_ACPC_SET); //zero out TIOA compare bits, maintain current TIOB compare bits
TC2->TC_CHANNEL[2].TC_CMR |= (TC_CMR_ACPA_SET | TC_CMR_ACPC_CLEAR); //channel mode register - D11 normal
d11Normal = 1;
break;
case 'k': //kill PPM
TC2->TC_CHANNEL[2].TC_CCR = !TC_CCR_SWTRG | TC_CCR_CLKDIS | !TC_CCR_CLKEN; // stop counter
break;
case 'r': //restart PPM
PIOD->PIO_PDR |= PIO_PDR_P7 | PIO_PDR_P8; // disable PIO, enable peripherals for D11 & D12
PIOD->PIO_ABSR |= PIO_ABSR_P7 | PIO_ABSR_P8; // select peripherals for D11 & D12
TC2->TC_CHANNEL[2].TC_CCR = TC_CCR_SWTRG | TC_CCR_CLKEN;
break;
default:
break;
}
}
@stratosfear thanks for your code. I've had a quick look (very late here) and it seems to be the answer to a maiden's prayer, as my dear old mum used to say. Done a couple of test runs and it is looking spot on. Great work!
Re using PWM, I used PWM on my Nano and Mega2560 to do the same thing a while back, so thought that concept would translate to the Due, so that's why I was looking at PWM. But happy to use whatever works!
Happy camper now.