current WIP code for PIO based PWM

This commit is contained in:
ZodiusInfuser 2022-02-11 11:40:04 +00:00
parent 7c523f82df
commit aca98ca747
16 changed files with 578 additions and 0 deletions

View File

@ -26,3 +26,4 @@ add_subdirectory(icp10125)
add_subdirectory(scd4x)
add_subdirectory(hub75)
add_subdirectory(uc8151)
add_subdirectory(servo)

View File

@ -0,0 +1 @@
include(servo.cmake)

274
drivers/servo/multi_pwm.cpp Normal file
View File

@ -0,0 +1,274 @@
#include "multi_pwm.hpp"
#include "common/pimoroni_common.hpp"
#include <cstdio>
#include "hardware/gpio.h"
namespace servo {
int data_dma_channel;
int ctrl_dma_channel;
static const uint BUFFER_SIZE = 64; // Set to 64, the maximum number of single rises and falls for 32 channels within a looping time period
struct alignas(8) Transition {
uint32_t mask;
uint32_t delay;
Transition() : mask(0), delay(0) {};
};
static const uint NUM_BUFFERS = 3;
struct Sequence {
uint32_t size;
Transition data[BUFFER_SIZE];
};
Sequence sequences[NUM_BUFFERS];
uint sequence_index = 0;
//Sequence loading_zone = {3, {Transition(), Transition(), Transition()}}; //Need 6 words to create loading zone behaviour with normal FIFO
Sequence loading_zone = {5, {Transition(), Transition(), Transition(), Transition(), Transition()}}; //Need 6 words to create loading zone behaviour with normal FIFO
bool enter_loading_zone = false;
const bool use_loading_zone = true;
uint loading_zone_size = 3;
uint gpio = 15;
void __isr pwm_dma_handler() {
// Clear the interrupt request.
dma_hw->ints0 = 1u << data_dma_channel;
// if(enter_loading_zone) {
// gpio_put(gpio+1, 1);
// uint32_t transitions = loading_zone.size * 2;
// uint32_t* buffer = (uint32_t *)loading_zone.data;
// dma_channel_set_trans_count(data_dma_channel, transitions, false);
// dma_channel_set_read_addr(data_dma_channel, buffer, true);
// enter_loading_zone = false;
// gpio_put(gpio+1, 0);
// }
// else {
gpio_put(gpio, 1);
uint32_t transitions = sequences[sequence_index].size * 2;
uint32_t* buffer = (uint32_t *)sequences[sequence_index].data;
dma_channel_set_trans_count(data_dma_channel, transitions, false);
dma_channel_set_read_addr(data_dma_channel, buffer, true);
// For some reason sequence 0 is output to the PIO twice, rather than once, despite the below line shifting the index along...
// ^ Seemed related to filling an 8 long fifo buffer. Reducing to 4 removed it.
sequence_index = (sequence_index + 1) % NUM_BUFFERS;
gpio_put(gpio, 0);
//}
}
/***
* From RP2040 datasheet
* *
* One disadvantage of this technique is that we dont start to reconfigure the channel until some time after the channel
makes its last transfer. If there is heavy interrupt activity on the processor, this may be quite a long time, and therefore
quite a large gap in transfers, which is problematic if we need to sustain a high data throughput.
This is solved by using two channels, with their CHAIN_TO fields crossed over, so that channel A triggers channel B when it
completes, and vice versa. At any point in time, one of the channels is transferring data, and the other is either already
configured to start the next transfer immediately when the current one finishes, or it is in the process of being
reconfigured. When channel A completes, it immediately starts the cued-up transfer on channel B. At the same time, the
interrupt is fired, and the handler reconfigures channel A so that it is ready for when channel B completes.
* */
MultiPWM::MultiPWM(PIO pio, uint sm, uint pin) : pio(pio), sm(sm) {
pio_program_offset = pio_add_program(pio, &multi_pwm_program);
gpio_init(gpio);
gpio_set_dir(gpio, GPIO_OUT);
gpio_init(gpio+1);
gpio_set_dir(gpio+1, GPIO_OUT);
for(uint i = pin; i < pin + 15; i++)
pio_gpio_init(pio, i);
pio_gpio_init(pio, 17);
pio_sm_set_consecutive_pindirs(pio, sm, pin, 17, true);
pio_sm_set_consecutive_pindirs(pio, sm, 17, 1, true);
pio_sm_config c = multi_pwm_program_get_default_config(pio_program_offset);
sm_config_set_out_pins(&c, pin, 17);
sm_config_set_sideset_pins(&c, 17);
sm_config_set_out_shift(&c, false, true, 32);
//sm_config_set_fifo_join(&c, PIO_FIFO_JOIN_TX); // Joining the FIFOs makes the DMA interrupts occur earlier than we would like
sm_config_set_fifo_join(&c, PIO_FIFO_JOIN_NONE);
float div = clock_get_hz(clk_sys) / 5000000;
sm_config_set_clkdiv(&c, div);
pio_sm_init(pio, sm, pio_program_offset, &c);
pio_sm_set_enabled(pio, sm, true);
data_dma_channel = dma_claim_unused_channel(true);
/*ctrl_dma_channel = dma_claim_unused_channel(true);
dma_channel_config ctrl_config = dma_channel_get_default_config(ctrl_dma_channel);
channel_config_set_transfer_data_size(&ctrl_config, DMA_SIZE_32);
//channel_config_set_read_increment(&ctrl_config, false);
//channel_config_set_write_increment(&ctrl_config, false);
channel_config_set_read_increment(&ctrl_config, true);
channel_config_set_write_increment(&ctrl_config, true);
channel_config_set_ring(&ctrl_config, true, 3); // 1 << 3 byte boundary on write ptr
channel_config_set_ring(&ctrl_config, false, 3); // 1 << 3 byte boundary on read ptr
dma_channel_configure(
ctrl_dma_channel,
&ctrl_config,
//The below two work
//&dma_hw->ch[data_dma_channel].al1_transfer_count_trig,
//&transfer_count,
//1,
//These two do not
//&dma_hw->ch[data_dma_channel].al3_read_addr_trig,
//&((uint32_t *)buffer),
&dma_hw->ch[data_dma_channel].al3_transfer_count, // Initial write address
&control_blocks[0],
2,
false
);*/
dma_channel_config data_config = dma_channel_get_default_config(data_dma_channel);
channel_config_set_bswap(&data_config, false);
channel_config_set_dreq(&data_config, pio_get_dreq(pio, sm, true));
channel_config_set_transfer_data_size(&data_config, DMA_SIZE_32);
channel_config_set_read_increment(&data_config, true);
//channel_config_set_chain_to(&data_config, ctrl_dma_channel);
//channel_config_set_ring(&data_config, false, 7);
dma_channel_configure(
data_dma_channel,
&data_config,
&pio->txf[sm],
NULL,
0,
false);
dma_channel_set_irq0_enabled(data_dma_channel, true);
// Configure the processor to run dma_handler() when DMA IRQ 0 is asserted
irq_set_exclusive_handler(DMA_IRQ_0, pwm_dma_handler);
irq_set_enabled(DMA_IRQ_0, true);
{
Sequence& seq = sequences[0];
Transition* trans = seq.data;
trans[0].mask = (1u << 0);
trans[0].delay = 1000 - 1;
trans[1].mask = (1u << 1);
trans[1].delay = 1000 - 1;
trans[2].mask = (1u << 1);
trans[2].delay = 1000 - 1;
trans[3].mask = 0;
trans[3].delay = (20000 - 3000) - 1;
//if(use_loading_zone)
//trans[4].delay -= loading_zone.size;
seq.size = 4;
if(use_loading_zone){
trans[seq.size - 1].delay -= loading_zone_size;
for(uint i = 0; i < loading_zone_size; i++) {
trans[seq.size].mask = 0;
trans[seq.size].delay = 0;
seq.size += 1;
}
}
}
{
Sequence& seq = sequences[1];
Transition* trans = seq.data;
trans[0].mask = (1u << 5);
trans[0].delay = 10000 - 1;
trans[1].mask = 0;
trans[1].delay = (20000 - 10000) - 1;
//if(use_loading_zone)
//trans[1].delay -= loading_zone.size;
seq.size = 2;
if(use_loading_zone){
trans[seq.size - 1].delay -= loading_zone_size;
for(uint i = 0; i < loading_zone_size; i++) {
trans[seq.size].mask = 0;
trans[seq.size].delay = 0;
seq.size += 1;
}
}
}
{
Sequence& seq = sequences[2];
Transition* trans = seq.data;
uint count = 0;
uint last = 14;
for(uint i = 0; i < last; i++) {
trans[i].mask = (1u << i);
trans[i].delay = 1000 - 1;
count += 1000;
}
trans[last].mask = 0;
trans[last].delay = (20000 - count) - 1;
//if(use_loading_zone)
// trans[last].delay -= loading_zone.size;
seq.size = last + 1;
if(use_loading_zone){
trans[seq.size - 1].delay -= loading_zone_size;
for(uint i = 0; i < loading_zone_size; i++) {
trans[seq.size].mask = 0;
trans[seq.size].delay = 0;
seq.size += 1;
}
}
}
// Manually call the handler once, to trigger the first transfer
pwm_dma_handler();
//dma_start_channel_mask(1u << ctrl_dma_channel);
}
MultiPWM::~MultiPWM() {
stop();
clear();
dma_channel_unclaim(data_dma_channel);
dma_channel_unclaim(ctrl_dma_channel);
pio_sm_set_enabled(pio, sm, false);
pio_remove_program(pio, &multi_pwm_program, pio_program_offset);
#ifndef MICROPY_BUILD_TYPE
// pio_sm_unclaim seems to hardfault in MicroPython
pio_sm_unclaim(pio, sm);
#endif
}
bool MultiPWM::start(uint fps) {
//add_repeating_timer_ms(-(1000 / fps), dma_timer_callback, (void*)this, &timer);
return true;
}
bool MultiPWM::stop() {
return true;//cancel_repeating_timer(&timer);
}
void MultiPWM::clear() {
//for (auto i = 0u; i < num_leds; ++i) {
// set_rgb(i, 0, 0, 0);
//}
}
}

View File

@ -0,0 +1,40 @@
/**
* Copyright (c) 2020 Raspberry Pi (Trading) Ltd.
*
* SPDX-License-Identifier: BSD-3-Clause
*/
/*
This code is significantly modified from the PIO apa102 example
found here: https://github.com/raspberrypi/pico-examples/tree/master/pio/ws2812
*/
#pragma once
#include <math.h>
#include <cstdint>
#include "multi_pwm.pio.h"
#include "pico/stdlib.h"
#include "hardware/pio.h"
#include "hardware/dma.h"
#include "hardware/irq.h"
#include "hardware/clocks.h"
#include "hardware/timer.h"
namespace servo {
class MultiPWM {
public:
MultiPWM(PIO pio, uint sm, uint pin);
~MultiPWM();
bool start(uint fps=60);
bool stop();
void clear();
private:
PIO pio;
uint sm;
uint pio_program_offset;
};
}

View File

@ -0,0 +1,24 @@
;
; Copyright (c) 2020 Raspberry Pi (Trading) Ltd.
;
; SPDX-License-Identifier: BSD-3-Clause
;
.program multi_pwm
.side_set 1
.wrap_target
;pull side 0 ; Pull in the next DWord containing the pin states, and time counter
;out pins, 16 side 1 ; Immediately set the pins to their new state
;out y, 16 [1] side 0 ; Set the counter
pull side 0 ; Pull in the next DWord containing the pin states, and time counter
out pins, 32 side 1 ; Immediately set the pins to their new state
pull side 0
out y, 32 side 0 ; Set the counter
count_check:
jmp y-- delay side 0 ; Check if the counter is 0, and if so wrap around. If not decrement the counter and jump to the delay
.wrap
delay:
jmp count_check [3] side 1 ; Wait a few cycles then jump back to the loop

16
drivers/servo/servo.cmake Normal file
View File

@ -0,0 +1,16 @@
set(DRIVER_NAME servo)
add_library(${DRIVER_NAME} INTERFACE)
target_sources(${DRIVER_NAME} INTERFACE
${CMAKE_CURRENT_LIST_DIR}/multi_pwm.cpp
)
target_include_directories(${DRIVER_NAME} INTERFACE ${CMAKE_CURRENT_LIST_DIR})
target_link_libraries(${DRIVER_NAME} INTERFACE
pico_stdlib
hardware_pio
hardware_dma
)
pico_generate_pio_header(${DRIVER_NAME} ${CMAKE_CURRENT_LIST_DIR}/multi_pwm.pio)

View File

@ -40,3 +40,4 @@ add_subdirectory(pico_wireless)
add_subdirectory(plasma2040)
add_subdirectory(badger2040)
add_subdirectory(interstate75)
add_subdirectory(servo2040)

View File

@ -0,0 +1,2 @@
include(servo2040_functions.cmake)
include(servo2040_rainbow.cmake)

View File

@ -0,0 +1,13 @@
set(OUTPUT_NAME servo2040_functions)
add_executable(${OUTPUT_NAME} servo2040_functions.cpp)
target_link_libraries(${OUTPUT_NAME}
pico_stdlib
servo2040
button
)
# enable usb output
pico_enable_stdio_usb(${OUTPUT_NAME} 1)
pico_add_extra_outputs(${OUTPUT_NAME})

View File

@ -0,0 +1,76 @@
#include <stdio.h>
#include <math.h>
#include <cstdint>
#include "pico/stdlib.h"
#include "servo2040.hpp"
#include "common/pimoroni_common.hpp"
#include "button.hpp"
/*
A simple balancing game, where you use the MSA301 accelerometer to line up a band with a goal on the strip.
This can either be done using:
- Angle mode: Where position on the strip directly matches the accelerometer's angle
- Velocity mode: Where tilting the accelerometer changes the speed the band moves at
When the goal position is reached, a new position is randomly selected
Press "A" to change the game mode.
Press "B" to start or stop the game mode.
Press "Boot" to invert the direction of the accelerometer tilt
*/
using namespace pimoroni;
using namespace plasma;
using namespace servo;
// Set how many LEDs you have
const uint N_LEDS = 6;
// The speed that the LEDs will start cycling at
const uint DEFAULT_SPEED = 10;
// How many times the LEDs will be updated per second
const uint UPDATES = 60;
// WS28X-style LEDs with a single signal line. AKA NeoPixel
WS2812 led_bar(N_LEDS, pio0, 0, servo2040::LED_DAT);
Button user_sw(servo2040::USER_SW, Polarity::ACTIVE_LOW, 0);
int main() {
stdio_init_all();
led_bar.start(UPDATES);
sleep_ms(5000);
MultiPWM pwms(pio1, 0, 0);
int speed = DEFAULT_SPEED;
float offset = 0.0f;
while(true) {
bool sw_pressed = user_sw.read();
if(sw_pressed) {
speed = DEFAULT_SPEED;
}
speed = std::min((int)255, std::max((int)1, speed));
offset += float(speed) / 2000.0f;
for(auto i = 0u; i < led_bar.num_leds; ++i) {
float hue = float(i) / led_bar.num_leds;
led_bar.set_hsv(i, hue + offset, 1.0f, 0.5f);
}
//pwms.update(true);
// Sleep time controls the rate at which the LED buffer is updated
// but *not* the actual framerate at which the buffer is sent to the LEDs
sleep_ms(1000 / UPDATES);
}
}

View File

@ -0,0 +1,13 @@
set(OUTPUT_NAME servo2040_rainbow)
add_executable(${OUTPUT_NAME} servo2040_rainbow.cpp)
target_link_libraries(${OUTPUT_NAME}
pico_stdlib
servo2040
button
)
# enable usb output
pico_enable_stdio_usb(${OUTPUT_NAME} 1)
pico_add_extra_outputs(${OUTPUT_NAME})

View File

@ -0,0 +1,69 @@
#include <stdio.h>
#include <math.h>
#include <cstdint>
#include "pico/stdlib.h"
#include "servo2040.hpp"
#include "common/pimoroni_common.hpp"
#include "button.hpp"
/*
A simple balancing game, where you use the MSA301 accelerometer to line up a band with a goal on the strip.
This can either be done using:
- Angle mode: Where position on the strip directly matches the accelerometer's angle
- Velocity mode: Where tilting the accelerometer changes the speed the band moves at
When the goal position is reached, a new position is randomly selected
Press "A" to change the game mode.
Press "B" to start or stop the game mode.
Press "Boot" to invert the direction of the accelerometer tilt
*/
using namespace pimoroni;
using namespace plasma;
// Set how many LEDs you have
const uint N_LEDS = 6;
// The speed that the LEDs will start cycling at
const uint DEFAULT_SPEED = 10;
// How many times the LEDs will be updated per second
const uint UPDATES = 60;
// WS28X-style LEDs with a single signal line. AKA NeoPixel
WS2812 led_bar(N_LEDS, pio0, 0, servo2040::LED_DAT);
Button user_sw(servo2040::USER_SW, Polarity::ACTIVE_LOW, 0);
int main() {
stdio_init_all();
led_bar.start(UPDATES);
int speed = DEFAULT_SPEED;
float offset = 0.0f;
while(true) {
bool sw_pressed = user_sw.read();
if(sw_pressed) {
speed = DEFAULT_SPEED;
}
speed = std::min((int)255, std::max((int)1, speed));
offset += float(speed) / 2000.0f;
for(auto i = 0u; i < led_bar.num_leds; ++i) {
float hue = float(i) / led_bar.num_leds;
led_bar.set_hsv(i, hue + offset, 1.0f, 0.5f);
}
// Sleep time controls the rate at which the LED buffer is updated
// but *not* the actual framerate at which the buffer is sent to the LEDs
sleep_ms(1000 / UPDATES);
}
}

View File

@ -27,3 +27,4 @@ add_subdirectory(pico_rgb_keypad)
add_subdirectory(pico_wireless)
add_subdirectory(plasma2040)
add_subdirectory(badger2040)
add_subdirectory(servo2040)

View File

@ -0,0 +1 @@
include(servo2040.cmake)

View File

@ -0,0 +1,6 @@
add_library(servo2040 INTERFACE)
target_include_directories(servo2040 INTERFACE ${CMAKE_CURRENT_LIST_DIR})
# Pull in pico libraries that we need
target_link_libraries(servo2040 INTERFACE pico_stdlib plasma servo)

View File

@ -0,0 +1,40 @@
#pragma once
#include "pico/stdlib.h"
#include "ws2812.hpp"
#include "multi_pwm.hpp"
namespace servo2040 {
const uint SERVO_1 = 0;
const uint SERVO_2 = 1;
const uint SERVO_3 = 2;
const uint SERVO_4 = 3;
const uint SERVO_5 = 4;
const uint SERVO_6 = 5;
const uint SERVO_7 = 6;
const uint SERVO_8 = 7;
const uint SERVO_9 = 8;
const uint SERVO_10 = 9;
const uint SERVO_11 = 10;
const uint SERVO_12 = 11;
const uint SERVO_13 = 12;
const uint SERVO_14 = 13;
const uint SERVO_15 = 14;
const uint SERVO_16 = 15;
const uint SERVO_17 = 16;
const uint SERVO_18 = 17;
const uint LED_DAT = 18;
const uint ADC_ADDR_0 = 22;
const uint ADC_ADDR_1 = 24;
const uint ADC_ADDR_2 = 25;
const uint USER_SW = 23;
const uint SENSE = 29; // The pin used for the board's sensing features
constexpr float ADC_GAIN = 69;
constexpr float ADC_OFFSET = 0.0145f;
constexpr float SHUNT_RESISTOR = 0.003f;
}