I’m looking for help getting DSHOT set up on a SAMD21 microcontroller, Arduino Zero clone. DSHOT is a brushless motor control protocol and uses PWM at fixed frequency and a variable duty cycle of either 1/3 to 2/3 to signify 0s and 1s. Here I’m using a DSHOT test frequency of 200KHz as pulses line up clearly on a scope. There are 16 bits in a DSHOT packet but in the following I’m using 7 data bits to make images clearer.
I believe that the way to do this is to use a lookup table of duty cycles with DMA, as described by MartinL in:
https://forum.arduino.cc/t/ideas-for-pwm-with-repeating-pattern/847923
This page has been a great starting point. I’ve taken this as a basis entering a DMA test lookup table of 1/3 and 2/3 PWM duty cycles as:
[ 2/3 1/3 2/3 2/3 1/3 1/3 1/3 0 0 0]
Note that I’ve padded the lookup table to 10 elements with 0 duty cycle pulses so that one DSHOT packet can be discriminated from the next. Eventually I’ll need to stop and restart the PWM to accomplish this in a more versatile way (using a timer as I think interrupts are unreliable at 200kHz). This is implemented in the code:
// Set up DSHOT on SAMD21 using DMA with PWM at 300000Hz
#define FREQUENCYDSHOT 200000 // Frequency of DSHOT PWM
// Time PWM was started and stopped
uint32_t TStart=0, TStop=0;
volatile uint16_t LookUp[10]; // DMA PWM lookup table
typedef struct // DMAC descriptor structure
{
uint16_t btctrl;
uint16_t btcnt;
uint32_t srcaddr;
uint32_t dstaddr;
uint32_t descaddr;
} dmacdescriptor ;
volatile dmacdescriptor wrb[12] __attribute__ ((aligned (16))); // Write-back DMAC descriptors
dmacdescriptor descriptor_section[12] __attribute__ ((aligned (16))); // DMAC channel descriptors
dmacdescriptor descriptor __attribute__ ((aligned (16))); // Place holder descriptor
uint16_t PERIOD=48000000/FREQUENCYDSHOT-1;
uint8_t NELEM=10; // Number of elements in DMA lookup table: 7 plus 3 zero pads
void setup()
{
// Set the PWM lookup table [2/3 1/3 2/3 2/3 1/3 1/3 1/3 0 0 0]
// PWM should look like this:
// _--__-_--_--__-__-__-_________
LookUp[0]=2*PERIOD/3;
LookUp[1]= PERIOD/3;
LookUp[2]=2*PERIOD/3; LookUp[3]=2*PERIOD/3;
LookUp[4]= PERIOD/3; LookUp[5]= PERIOD/3; LookUp[6]= PERIOD/3;
LookUp[7]= 0; LookUp[8]=0; LookUp[9]=0;
DMAC->BASEADDR.reg = (uint32_t)descriptor_section; // Set the descriptor section base address
DMAC->WRBADDR.reg = (uint32_t)wrb; // Set the write-back descriptor base adddress
DMAC->CTRL.reg = DMAC_CTRL_DMAENABLE | DMAC_CTRL_LVLEN(0xf); // Enable the DMAC and priority levels
DMAC->CHID.reg = DMAC_CHID_ID(0); // Select DMAC channel 0
// Set DMAC channel 0 to priority level 0 (lowest), to trigger on TCC0 overflow and to trigger every beat
DMAC->CHCTRLB.reg = DMAC_CHCTRLB_LVL(0) | DMAC_CHCTRLB_TRIGSRC(TCC0_DMAC_ID_OVF) | DMAC_CHCTRLB_TRIGACT_BEAT;
descriptor.descaddr = (uint32_t)&descriptor_section[0]; // Set up a circular descriptor
descriptor.srcaddr = (uint32_t)&LookUp[0] + NELEM * sizeof(uint16_t); // Read the current value in the LookUp
descriptor.dstaddr = (uint32_t)&TCC0->CC[3].reg; // Copy it into the TCC0 counter comapare 0 register
descriptor.btcnt = NELEM; // This takes the number of LookUp entries = NELEM beats
descriptor.btctrl = DMAC_BTCTRL_BEATSIZE_HWORD | DMAC_BTCTRL_SRCINC | DMAC_BTCTRL_VALID; // Copy 16-bits (HWORD), increment the source and flag discriptor as valid
memcpy(&descriptor_section[0], &descriptor, sizeof(dmacdescriptor)); // Copy to the channel 0 descriptor
GCLK->GENDIV.reg = GCLK_GENDIV_DIV(1) | // Divide the 48MHz clock source by divisor 1: 48MHz/1=48MHz
GCLK_GENDIV_ID(4); // Select Generic Clock (GCLK) 4
GCLK->GENCTRL.reg = GCLK_GENCTRL_IDC | // Set the duty cycle to 50/50 HIGH/LOW
GCLK_GENCTRL_GENEN | // Enable GCLK4
GCLK_GENCTRL_SRC_DFLL48M | // Set the 48MHz DFLL48M clock source
GCLK_GENCTRL_ID(4); // Select GCLK4
while (GCLK->STATUS.bit.SYNCBUSY); // Wait for synchronization
GCLK->CLKCTRL.reg = GCLK_CLKCTRL_CLKEN | // Select GCLK4 as a clock source for TCC0 and TCC1
GCLK_CLKCTRL_GEN_GCLK4 | // Select GCLK4
GCLK_CLKCTRL_ID_TCC0_TCC1; // Select GCLK4 as a co TCC0 and TCC1
// Enable the port multiplexer for the digital pins D7
PORT->Group[g_APinDescription[7].ulPort].PINCFG[g_APinDescription[7].ulPin].bit.PMUXEN = 1;
// Connect the TCC0 timer to the port output D7
PORT->Group[g_APinDescription[7].ulPort].PMUX[g_APinDescription[7].ulPin >> 1].reg |= PORT_PMUX_PMUXO_F; //| PORT_PMUX_PMUXE_F;
// Normal (single slope) PWM operation: timers countinuously count up to PER register value and then is reset to 0
TCC0->WAVE.reg = TCC_WAVE_WAVEGEN_NPWM; // Setup single slope PWM on TCC0
while (TCC0->SYNCBUSY.bit.WAVE); // Wait for synchronization
TCC0->PER.reg = PERIOD; // Set the frequency of the PWM on TCC0 to 25kHz
while(TCC0->SYNCBUSY.bit.PER); // Wait for synchronization
// Divide the 48MHz signal by 1 giving 48MHz (20.83ns) TCC0 timer tick and enable the outputs
TCC0->CTRLA.reg = //TCC_CTRLA_PRESCSYNC_PRESC | // Set timer to overflow on the prescaler rather than GCLK
TCC_CTRLA_PRESCALER_DIV1; // Divide GCLK4 by 1
TCC0->CTRLA.bit.ENABLE = 1; // Enable the TCC0 timer
while (TCC0->SYNCBUSY.bit.ENABLE); // Wait for synchronization
DMAC->CHID.reg = DMAC_CHID_ID(0); // Select DMAC channel 0
DMAC->CHCTRLA.reg |= DMAC_CHCTRLA_ENABLE; // Enable DMAC channel 0
TStart=micros(); // Time PWM started
}
void loop(){}
I’m having a couple of problems right now. First I notice that the very first PWM pulse does not include the 1st lookup table value. Instead the DMA transfer seems to start on the second value. I can easily work around this but I’m obviously doing something wrong.
The 2nd problem is that I seem to be getting 100% duty cycle pulses (5.0uS in plot) when the trace should be zero.
I’d appreciate any pointers to help me address or understand the cause of these problems.
sjwalker66