From 8356c2f03d384960382bac0c0905dee272a91b29 Mon Sep 17 00:00:00 2001 From: nick Date: Thu, 7 Sep 2017 14:15:58 -0400 Subject: [PATCH] rpi: Adding PWM support The Pi doesn't have userspace level PWM support, but you can mmap the raw registers and configure it that way, which is what this patch is doing. Tested with Raspberry Pi 3 B. I am able to set the duty cycle, change the frequency, and set the pulse width. Signals verified with logic analyzer. Note that the only accessible pwm is on GPIO18, which is also used by the audio subsystem. Signed-off-by: Nick Crast Signed-off-by: Brendan Le Foll --- src/arm/raspberry_pi.c | 274 ++++++++++++++++++++++++++++++++++++++--- 1 file changed, 254 insertions(+), 20 deletions(-) diff --git a/src/arm/raspberry_pi.c b/src/arm/raspberry_pi.c index b7a12f0..743f9f7 100644 --- a/src/arm/raspberry_pi.c +++ b/src/arm/raspberry_pi.c @@ -23,14 +23,15 @@ * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ +#include +#include +#include #include #include -#include #include -#include -#include "common.h" #include "arm/raspberry_pi.h" +#include "common.h" #define PLATFORM_NAME_RASPBERRY_PI_B_REV_1 "Raspberry Pi Model B Rev 1" #define PLATFORM_NAME_RASPBERRY_PI_A_REV_2 "Raspberry Pi Model A Rev 2" @@ -52,22 +53,251 @@ #define PLATFORM_RASPBERRY_PI3_B 9 #define MMAP_PATH "/dev/mem" #define BCM2835_PERI_BASE 0x20000000 -#define BCM2835_GPIO_BASE (BCM2835_PERI_BASE + 0x200000) #define BCM2836_PERI_BASE 0x3f000000 -#define BCM2836_GPIO_BASE (BCM2836_PERI_BASE + 0x200000) #define BCM2835_BLOCK_SIZE (4 * 1024) #define BCM2836_BLOCK_SIZE (4 * 1024) +#define BCM2837_PERI_BASE (0x3F000000) +#define BCM2837_BLOCK_SIZE (4 * 1024) #define BCM283X_GPSET0 0x001c #define BCM283X_GPCLR0 0x0028 #define BCM2835_GPLEV0 0x0034 #define MAX_SIZE 64 -// MMAP +#define GPIO_OFFSET (0x200000) +#define PWM_OFFSET (0x20C000) +#define CLOCK_OFFSET (0x101000) + +// PWM +#define PWM_BASE_FREQ (19200000) + +// Base register addresses +#define PWM_CTL (0) +#define PWM_RNG1 (4) +#define PWM_DAT1 (5) +#define PWMCLK_CNTL (40) +#define PWMCLK_DIV (41) +// Register addresses offsets divided by 4 (register addresses are word (32-bit) aligned +#define PWM_RNG (4095) +#define DEFAULT_PERIOD_US 500 + +#define MIN_PERIOD_US 400 +#define MAX_PERIOD_US 500000 + +static volatile unsigned* clk_reg = NULL; +static volatile unsigned* gpio_reg = NULL; +static volatile unsigned* pwm_reg = NULL; + + +// GPIO MMAP static uint8_t* mmap_reg = NULL; static int mmap_fd = 0; static int mmap_size; static unsigned int mmap_count = 0; static int platform_detected = 0; +static uint32_t peripheral_base = BCM2835_PERI_BASE; +static uint32_t block_size = BCM2835_BLOCK_SIZE; + +static bool pwm_enabled = false; + +/** +* Calculate the PWM clk divisor given a desired period +*/ +static inline uint32_t +calc_divisor(uint32_t desired_period_us) +{ + float desired_freq = 1.0 / (desired_period_us / 1000000.0); + float divisor = PWM_BASE_FREQ / (desired_freq * PWM_RNG); + return (uint32_t)(divisor + .5); // Round to nearest integer +} + +/** +* Memory map an arbitrary address +*/ +static volatile unsigned* +mmap_reg_addr(unsigned long base_addr) +{ + int mem_fd = 0; + void* reg_addr_map = MAP_FAILED; + + /* open /dev/mem.....need to run program as root i.e. use sudo or su */ + if (!mem_fd) { + if ((mem_fd = open("/dev/mem", O_RDWR | O_SYNC)) < 0) { + perror("can't open /dev/mem"); + return NULL; + } + } + + /* mmap IO */ + reg_addr_map = + mmap(NULL, // Any adddress in our space will do + block_size, // Map length + PROT_READ | PROT_WRITE | PROT_EXEC, // Enable reading & writting to mapped memory + MAP_SHARED | MAP_LOCKED, // Shared with other processes + mem_fd, // File to map + base_addr // Offset to base address + ); + + if (reg_addr_map == MAP_FAILED) { + perror("mmap error"); + close(mem_fd); + return NULL; + } + + if (close(mem_fd) < 0) { // No need to keep mem_fd open after mmap + // i.e. we can close /dev/mem + perror("couldn't close /dev/mem file descriptor"); + return NULL; + } + return (volatile unsigned*) reg_addr_map; +} + +/** +* Memory map PWM registers +*/ +static int8_t +mmap_regs() +{ + // Already mapped! + if (clk_reg || gpio_reg || pwm_reg) { + return; + } + + clk_reg = mmap_reg_addr(peripheral_base + CLOCK_OFFSET); + gpio_reg = mmap_reg_addr(peripheral_base + GPIO_OFFSET); + pwm_reg = mmap_reg_addr(peripheral_base + PWM_OFFSET); + + // If all 3 weren't mapped correctly + if (!(clk_reg && gpio_reg && pwm_reg)) { + return -1; + } + + return 0; +} + +static void +pwm_stop() +{ + // stop clock and waiting for busy flag doesn't work, so kill clock + *(clk_reg + PWMCLK_CNTL) = 0x5A000000 | (1 << 5); + usleep(10); + + // wait until busy flag is set + while ((*(clk_reg + PWMCLK_CNTL)) & 0x00000080) { + } + + *(pwm_reg + PWM_CTL) = 0; + usleep(10); +} + +static void +pwm_start() +{ + // source=osc and enable clock + *(clk_reg + PWMCLK_CNTL) = 0x5A000011; + + // start PWM1 in mark space mode + *(pwm_reg + PWM_CTL) |= ((1 << 7) | (1 << 0)); + + usleep(10); +} + + +static void +config_pwm1(uint32_t divisor) +{ + /*GPIO 18 in ALT5 mode for PWM0 */ + // Let's first set pin 18 to input + // taken from #define INP_GPIO(g) *(gpio+((g)/10)) &= ~(7<<(((g)%10)*3)) + *(gpio_reg + 1) &= ~(7 << 24); + // then set it to ALT5 function PWM0 + // taken from #define SET_GPIO_ALT(g,a) *(gpio+(((g)/10))) |= + // (((a)<=3?(a)+4:(a)==4?3:2)<<(((g)%10)*3)) + *(gpio_reg + 1) |= (2 << 24); + + pwm_stop(); + + // set divisor + *(clk_reg + PWMCLK_DIV) = 0x5A000000 | (divisor << 12); + + // disable PWM & start from a clean slate + *(pwm_reg + PWM_CTL) = 0; + + // needs some time until the PWM module gets disabled, without the delay the PWM module crashs + usleep(10); + + // set the number of counts that constitute a period with 0 for 20 milliseconds = 320 bits + *(pwm_reg + PWM_RNG1) = PWM_RNG; + usleep(10); + + // If the user has pwm set to enabled + if (pwm_enabled) { + pwm_start(); + } +} + + +// This shouldn't be a float, since it's called in mraa_pwm_read_duty, which +// seems to return the duty cycle in us +float +mraa_raspberry_pi_duty_cycle_read_replace(mraa_pwm_context dev) +{ + uint32_t duty_counts = *(pwm_reg + PWM_DAT1); + return (((float) duty_counts) / PWM_RNG) * dev->period; +} + + +mraa_result_t +mraa_raspberry_pi_pwm_period_us_replace(mraa_pwm_context dev, int period) +{ + config_pwm1(calc_divisor(period / 1000)); + dev->period = period; + return MRAA_SUCCESS; +} + +// Used in pwm.c, duty is actually just the raw counts +// Shouldn't be a float +mraa_result_t +mraa_raspberry_pi_pwm_write_duty_replace(mraa_pwm_context dev, float duty) +{ + // We are given duty as a percentage of the period + // Convert it to be a percentage of the range + float duty_pct = duty / dev->period; + *(pwm_reg + PWM_DAT1) = (uint32_t)(duty_pct * PWM_RNG); + return MRAA_SUCCESS; +} + +mraa_result_t +mraa_raspberry_pi_pwm_enable_replace(mraa_pwm_context dev, int enable) +{ + if (enable == 1) { + pwm_enabled = true; + pwm_start(); + } else { + pwm_enabled = false; + pwm_stop(); + } + + return MRAA_SUCCESS; +} + +mraa_result_t +mraa_raspberry_pi_pwm_initraw_replace(mraa_pwm_context dev, int pin) +{ + if (pin != 0) { + return MRAA_ERROR_INVALID_PARAMETER; + } + + if (mmap_regs() != 0) { + return MRAA_ERROR_UNSPECIFIED; + } + + config_pwm1(DEFAULT_PERIOD_US); + *(pwm_reg + PWM_DAT1) = 0; + dev->period = DEFAULT_PERIOD_US; + + return MRAA_SUCCESS; +} + mraa_result_t mraa_raspberry_pi_spi_init_pre(int index) @@ -188,13 +418,8 @@ mraa_raspberry_pi_mmap_setup(mraa_gpio_context dev, mraa_boolean_t en) return MRAA_ERROR_INVALID_HANDLE; } - if (platform_detected == PLATFORM_RASPBERRY_PI2_B_REV_1) { - mmap_reg = (uint8_t*) mmap(NULL, BCM2836_BLOCK_SIZE, PROT_READ | PROT_WRITE, - MAP_FILE | MAP_SHARED, mmap_fd, BCM2836_GPIO_BASE); - } else { - mmap_reg = (uint8_t*) mmap(NULL, BCM2835_BLOCK_SIZE, PROT_READ | PROT_WRITE, - MAP_FILE | MAP_SHARED, mmap_fd, BCM2835_GPIO_BASE); - } + mmap_reg = (uint8_t*) mmap(NULL, block_size, PROT_READ | PROT_WRITE, MAP_FILE | MAP_SHARED, + mmap_fd, peripheral_base + GPIO_OFFSET); if (mmap_reg == MAP_FAILED) { syslog(LOG_ERR, "raspberry mmap: failed to mmap"); mmap_reg = NULL; @@ -271,11 +496,15 @@ mraa_raspberry_pi() b->platform_name = PLATFORM_NAME_RASPBERRY_PI2_B_REV_1; platform_detected = PLATFORM_RASPBERRY_PI2_B_REV_1; b->phy_pin_count = MRAA_RASPBERRY_PI2_B_REV_1_PINCOUNT; + peripheral_base = BCM2836_PERI_BASE; + block_size = BCM2836_BLOCK_SIZE; } else if (strstr(line, "a02082") || strstr(line, "a020a0") || strstr(line, "a22082") || strstr(line, "a32082")) { b->platform_name = PLATFORM_NAME_RASPBERRY_PI3_B; platform_detected = PLATFORM_RASPBERRY_PI3_B; b->phy_pin_count = MRAA_RASPBERRY_PI3_B_PINCOUNT; + peripheral_base = BCM2837_PERI_BASE; + block_size = BCM2837_BLOCK_SIZE; } else { b->platform_name = PLATFORM_NAME_RASPBERRY_PI_B_REV_1; platform_detected = PLATFORM_RASPBERRY_PI_B_REV_1; @@ -293,7 +522,7 @@ mraa_raspberry_pi() if (!tweakedCpuinfo) { // See Documentation/devicetree/bindings/arm/bcm/brcm,bcm2835.txt // for the values - const char *compatible_path = "/proc/device-tree/compatible"; + const char* compatible_path = "/proc/device-tree/compatible"; if (mraa_file_contains(compatible_path, "raspberrypi,model-b")) { b->platform_name = PLATFORM_NAME_RASPBERRY_PI_B_REV_1; platform_detected = PLATFORM_RASPBERRY_PI_B_REV_1; @@ -340,9 +569,9 @@ mraa_raspberry_pi() b->aio_count = 0; b->adc_raw = 0; b->adc_supported = 0; - b->pwm_default_period = 500; - b->pwm_max_period = 2147483; - b->pwm_min_period = 1; + b->pwm_default_period = DEFAULT_PERIOD_US; + b->pwm_max_period = MAX_PERIOD_US; + b->pwm_min_period = MIN_PERIOD_US; if (b->phy_pin_count == 0) { free(b); @@ -392,6 +621,11 @@ mraa_raspberry_pi() b->adv_func->i2c_init_pre = &mraa_raspberry_pi_i2c_init_pre; b->adv_func->gpio_mmap_setup = &mraa_raspberry_pi_mmap_setup; b->adv_func->spi_frequency_replace = &mraa_raspberry_pi_spi_frequency_replace; + b->adv_func->pwm_init_raw_replace = &mraa_raspberry_pi_pwm_initraw_replace; + b->adv_func->pwm_write_replace = &mraa_raspberry_pi_pwm_write_duty_replace; + b->adv_func->pwm_period_replace = &mraa_raspberry_pi_pwm_period_us_replace; + b->adv_func->pwm_read_replace = &mraa_raspberry_pi_duty_cycle_read_replace; + b->adv_func->pwm_enable_replace = &mraa_raspberry_pi_pwm_enable_replace; strncpy(b->pins[0].name, "INVALID", MRAA_PIN_NAME_SIZE); b->pins[0].capabilities = (mraa_pincapabilities_t){ 0, 0, 0, 0, 0, 0, 0, 0 }; @@ -450,9 +684,10 @@ mraa_raspberry_pi() b->pins[11].gpio.mux_total = 0; strncpy(b->pins[12].name, "GPIO18", MRAA_PIN_NAME_SIZE); - b->pins[12].capabilities = (mraa_pincapabilities_t){ 1, 1, 0, 0, 0, 0, 0, 0 }; + b->pins[12].capabilities = (mraa_pincapabilities_t){ 1, 1, 1, 0, 0, 0, 0, 0 }; b->pins[12].gpio.pinmap = pin_base + 18; b->pins[12].gpio.mux_total = 0; + b->pins[12].pwm.pinmap = 0; if (platform_detected == PLATFORM_RASPBERRY_PI_B_REV_1) { strncpy(b->pins[13].name, "GPIO21", MRAA_PIN_NAME_SIZE); @@ -593,8 +828,7 @@ mraa_raspberry_pi() if ((platform_detected == PLATFORM_RASPBERRY_PI_A_PLUS_REV_1) || (platform_detected == PLATFORM_RASPBERRY_PI_B_PLUS_REV_1) || (platform_detected == PLATFORM_RASPBERRY_PI2_B_REV_1) || - (platform_detected == PLATFORM_RASPBERRY_PI3_B) || - (platform_detected == PLATFORM_RASPBERRY_PI_ZERO)) { + (platform_detected == PLATFORM_RASPBERRY_PI3_B) || (platform_detected == PLATFORM_RASPBERRY_PI_ZERO)) { strncpy(b->pins[27].name, "ID_SD", MRAA_PIN_NAME_SIZE); b->pins[27].capabilities = (mraa_pincapabilities_t){ 1, 0, 0, 0, 0, 0, 0, 0 };