SoftPWM via DMA and no CPU cycles
universam10 Fri Jul 07, 2017 11:31 am
Here is a proof of concept that it actually works, and the cool thing is indeed it takes (almost) no CPU cycles when the duty cycles dont get changed. :P The interrupt takes some cycles honestly, but almost none if no updates happen. So it doesnt matter if I do pwm on one or 16 pins.
I’m using double buffering of 32bit for all 16pins of a port, so the buffer required is 2*resolution*4 bytes.
I’ve tested on a F1 all 16 pins of PortC successfully up to a pulse of 4us, which is a division of period frequency and resolution. So for instance for 8bit resolution 980 Hz would work.
I’m not sure if it makes too much sense or not, eager to hear what you think. It could be extended to a real library of course and support other Ports. Also the question is where the limits are regarding the frequency.
BTW., I wonder is it Software PWM any more since its done via hardware DMA … :mrgreen:
#include <Arduino.h>
#include <libmaple/dma.h>
#include <dma_private.h>
#define RESOLUTION 255 // PWM resolution
#define FREQUENCY 500 // PWM frequency
#if 1000000 / RESOLUTION / FREQUENCY < 4
#error did not work for me
#endif
class DMASoftPWM
{
public:
DMASoftPWM();
void begin(gpio_dev *port);
void setPinMode(uint8_t pin, bool enable);
void writePWM(uint8_t pin, uint16_t val);
uint32_t buffer[RESOLUTION * 2];
private:
static DMASoftPWM *anchor;
static void marshall() { anchor->DMAEvent(); }
inline void fillBuffer(uint16_t ptr);
uint16_t pinVal[16];
uint16_t pinmask;
uint8_t refresh;
void DMAEvent();
dma_tube_config tube_config;
};
DMASoftPWM::DMASoftPWM()
{
anchor = this;
}
void DMASoftPWM::begin(gpio_dev *port)
{
refresh = 2;
dma_init(DMA1);
tube_config.tube_src = buffer;
tube_config.tube_src_size = DMA_SIZE_32BITS;
tube_config.tube_dst = (uint32_t *)&GPIOC->regs->BSRR; // Load pointer to porta clear/set
tube_config.tube_dst_size = DMA_SIZE_32BITS;
tube_config.tube_nr_xfers = RESOLUTION * 2;
tube_config.tube_flags = DMA_CFG_SRC_INC | DMA_CFG_CIRC | DMA_CFG_CMPLT_IE | DMA_CFG_HALF_CMPLT_IE; // Source pointer increment,circular mode
tube_config.target_data = 0;
tube_config.tube_req_src = DMA_REQ_SRC_TIM2_CH3; // DMA request source.
dma_set_priority(DMA1, DMA_CH1, DMA_PRIORITY_VERY_HIGH);
dma_tube_cfg(DMA1, DMA_CH1, &tube_config); // Attach the tube to channel 1 (timer2 ch3)
dma_attach_interrupt(DMA1, DMA_CH1, DMASoftPWM::marshall);
dma_enable(DMA1, DMA_CH1);
//TIMER setup
Timer2.pause();
Timer2.setPeriod(10000000UL / FREQUENCY / RESOLUTION);
Timer2.setChannel3Mode(TIMER_OUTPUT_COMPARE);
Timer2.setCompare(TIMER_CH3, 1);
Timer2.refresh();
TIMER2_BASE->DIER = TIMER_DIER_CC3DE;
Timer2.resume();
}
void DMASoftPWM::fillBuffer(uint16_t ptr)
{
for (uint16_t step = 1; step <= RESOLUTION; step++)
{
buffer[ptr] = pinmask << 16;
for (uint8_t p = 0; p < 16; p++)
{
if (pinmask & (BIT(p)) && pinVal[p] >= step)
buffer[ptr] |= BIT(p);
}
ptr++;
}
refresh--;
}
void DMASoftPWM::DMAEvent()
{
dma_irq_cause event = dma_get_irq_cause(DMA1, DMA_CH1);
if (refresh == 0) // no update so just keep the mem
return;
switch (event)
{
case DMA_TRANSFER_COMPLETE: // now setting the upper half
fillBuffer(RESOLUTION);
break;
case DMA_TRANSFER_HALF_COMPLETE: //now setting the lower half
fillBuffer((uint16_t)0);
break;
case DMA_TRANSFER_ERROR:
ASSERT(0);
break;
case DMA_TRANSFER_DME_ERROR:
ASSERT(0);
break;
case DMA_TRANSFER_FIFO_ERROR:
ASSERT(0);
break;
}
}
void DMASoftPWM::setPinMode(uint8_t pin, bool enable)
{
pinMode(pin, OUTPUT);
if (enable)
pinmask |= digitalPinToBitMask(pin);
else
pinmask &= ~digitalPinToBitMask(pin);
}
void DMASoftPWM::writePWM(uint8_t pin, uint16_t val)
{
pinVal[pin] = val;
refresh = 2;
}
DMASoftPWM *DMASoftPWM::anchor = NULL;
DMASoftPWM softPWMPortC;
void setup()
{
Serial.begin(115200);
Serial.println("starting usb serial");
softPWMPortC.begin(GPIOC);
softPWMPortC.setPinMode(PC13, true);
}
void loop()
{
if (Serial.available())
{
int pin = Serial.parseInt();
int val = Serial.parseInt();
while (Serial.available())
Serial.read();
softPWMPortC.writePWM(pin, val);
Serial.print(pin);
Serial.print(':');
Serial.println(val);
#ifdef DEBUGBUFFER
delay(100);
for (int u = 0; u < RESOLUTION * 2; u++)
Serial.println(softPWMPortC.buffer[u], BIN);
#endif
}
static uint32_t sweep;
static uint16_t t = 0;
if (millis() - sweep > 1000 / RESOLUTION)
{
sweep = millis();
t = ++t % RESOLUTION;
softPWMPortC.writePWM(13, t);
}
}
