1
0
mirror of https://github.com/richardghirst/PiBits.git synced 2024-11-28 12:24:11 +01:00

Add first cut at a user space implementation

This commit is contained in:
Richard Hirst 2013-01-24 20:04:46 +00:00
parent 2190b84df4
commit ac658da6f9
3 changed files with 542 additions and 29 deletions

View File

@ -4,7 +4,7 @@ INSTALL_PATH := /lib/modules/$(shell /bin/uname -r)/kernel/drivers/misc/servobla
CROSS_OPTS :=
.PHONY: all install install_autostart
all: servoblaster.ko
all: servod servoblaster.ko
servoblaster.ko: servoblaster.c servoblaster.h
@[ -d ${KERNEL_TREE} ] || { echo "Edit Makefile to set KERNEL_TREE to point at your kernel"; exit 1; }
@ -14,6 +14,9 @@ servoblaster.ko: servoblaster.c servoblaster.h
servodemo: servodemo.c servoblaster.h
gcc -Wall -g -O2 -o servodemo servodemo.c -Wl,--export-dynamic `pkg-config --cflags gtk+-3.0 gmodule-export-2.0` `pkg-config --libs gtk+-3.0 gmodule-export-2.0`
servod: servod.c
gcc -Wall -g -O2 -o servod servod.c
install: servoblaster.ko
@sudo cp $(PWD)/udev_scripts/servoblaster /lib/udev
@sudo cp $(PWD)/udev_scripts/20-servoblaster.rules /etc/udev/rules.d
@ -33,5 +36,5 @@ install_autostart: install
clean:
make -C ${KERNEL_TREE} ${CROSS_OPTS} M=$(PWD) clean
rm -f servodemo
rm -f servod servodemo

View File

@ -1,10 +1,10 @@
ServoBlaster
This is a Linux kernel driver for the RaspberryPi, which provides an interface
to drive multiple servos via the GPIO pins. You control the servo postions by
sending commands to the driver saying what pulse width a particular servo
output should use. The driver maintains that pulse width until you send a new
command requesting some other width.
This is software for the RaspberryPi, which provides an interface to drive
multiple servos via the GPIO pins. You control the servo postions by sending
commands to the driver saying what pulse width a particular servo output should
use. The driver maintains that pulse width until you send a new command
requesting some other width.
Currently is it configured to drive 8 servos. Servos typically need an active
high pulse of somewhere between 0.5ms and 2.5ms, where the pulse width controls
@ -22,20 +22,21 @@ echo 3=120 > /dev/servoblaster
120 is in units of 10us, so that is 1200us, or 1.2ms.
Upon reading, the device file provides feedback as to what position each servo
is currently set. For example, after starting the driver and running the
previous command, you would see:
If you set a servo width to 0 it turns off the servo output, without changing
the current servo position.
pi@raspberrypi ~ $ cat /dev/servoblaster
0 0
1 0
2 0
3 120
4 0
5 0
6 0
7 0
pi@raspberrypi ~ $
The code supports 8 servos, the control signals of which should be connected
to P1 header pins as follows:
Servo number GPIO number Pin in P1 header
0 4 P1-7
1 17 P1-11
2 18 P1-12
3 21 P1-13
4 22 P1-15
5 23 P1-16
6 24 P1-18
7 25 P1-22
When the driver is first loaded the GPIO pins are configure to be outputs, and
their pulse widths are set to 0. This is so that servos don't jump to some
@ -75,14 +76,6 @@ card use. This is expected, because the pulse generation is effectively
handled in hardware and not influenced by interrupt latency or scheduling
effects.
Please read the driver source for more details, such as which GPIO pin maps to
which servo number. The comments at the top of servoblaster.c also explain how
to make your system create the /dev/servoblaster device node automatically when
the driver is loaded. Alternatively running "make install" in the driver source
directory will also create the necessary files. Further to this, running
"make install_autostart" will create those files, plus perform the necessary
changes to make servoblaster be automatically loaded at boot.
The driver uses DMA channel 0, and PWM channel 1. It makes no attempt to
protect against other code using those peripherals. It sets the relevant GPIO
pins to be outputs when the driver is loaded, so please ensure that you are not
@ -95,6 +88,74 @@ take the 5 volts for it from the header pins on the Pi, but I find that doing
anything non-trivial with four servos connected pulls the 5 volts down far
enough to crash the Pi!
There are two implementions of ServoBlaster; a kernel module based one, and
a user space daemon. The kernel module based one is the original, and is
more mature. The user space daemon implementation is much more convenient to
use but is less well tested and does not have all the features of the kernel
based one. I would recommend you try the user space implementation first, as
it is likely to be easier to get going.
Details specific to each implementation are provided in separate sections
below.
The user space daemon
---------------------
To use this daemon grab the servod.c source and Makefile and:
$ make servod
$ sudo ./servod
Number of servos: 8
Servo cycle time: 20000us
Pulse width units: 10us
Maximum width value: 249 (2490us)
$
The prompt will return immediately, and servod is left running in the
background. You can check it is running via the "ps ax" command.
If you want to stop servod, the easiest way is to run:
$ sudo killall servod
Features not currently supported in the user space implementation:
- does not support the timeout= option to turn off servo outputs after
some specified delay. You must set a servo width to 0 to turn off an
output, if you want to.
- you cannot read /dev/servoblaster to see the current servo settings
The kernel space implementation
-------------------------------
Upon reading /dev/servoblaster, the device file provides feedback as to what
position each servo is currently set. For example, after starting the driver
and sending the command "3=120", you would see:
pi@raspberrypi ~ $ cat /dev/servoblaster
0 0
1 0
2 0
3 120
4 0
5 0
6 0
7 0
pi@raspberrypi ~ $
Please read the driver source for more details. The comments at the top of
servoblaster.c also explain how to make your system create the
/dev/servoblaster device node automatically when the driver is loaded.
Alternatively running "make install" in the driver source directory will also
create the necessary files. Further to this, running "make install_autostart"
will create those files, plus perform the necessary changes to make
servoblaster be automatically loaded at boot.
If you wish to compile the module yourself, the approach I took was to run
rpi-update to get the latest kernel from github, then follow the instructions
on the wiki (http://elinux.org/RPi_Kernel_Compilation) to compile the kernel,
@ -157,5 +218,5 @@ servos, see his sbcontrol.sh script here:
http://www.raspberrypi.org/phpBB3/viewtopic.php?f=37&t=15011&start=25#p187675
Richard Hirst <richardghirst@gmail.com> August 2012
Richard Hirst <richardghirst@gmail.com> January 2013

449
ServoBlaster/servod.c Normal file
View File

@ -0,0 +1,449 @@
/*
* servod.c Multiple Servo Driver for the RaspberryPi
* Copyright (c) 2013 Richard Hirst <richardghirst@gmail.com>
*
* This program provides very similar functionality to servoblaster, except
* that rather than implementing it as a kernel module, servod implements
* the functionality as a usr space daemon.
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License version 2 as
* published by the Free Software Foundation.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, write to the Free Software
* Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
*/
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <errno.h>
#include <stdarg.h>
#include <stdint.h>
#include <signal.h>
#include <time.h>
#include <sys/time.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <sys/mman.h>
static uint8_t servo2gpio[] = {
4, // P1-7
17, // P1-11
18, // P1-12
21, // P1-13
22, // P1-15
23, // P1-16
24, // P1-18
25, // P1-22
};
#define NUM_SERVOS (sizeof(servo2gpio)/sizeof(servo2gpio[0]))
#define DEVFILE "/dev/servoblaster"
#define PAGE_SIZE 4096
#define PAGE_SHIFT 12
// CYCLE_TIME_US is the pulse cycle time per servo, in microseconds.
// Typically it should be 20ms, or 20000us.
// SAMPLE_US is the pulse width increment granularity, again in microseconds.
// Setting SAMPLE_US too low will likely cause problems as the DMA controller
// will use too much memory bandwidth. 10us is a good value, though you
// might be ok setting it as low as 2us.
#define CYCLE_TIME_US 20000
#define SAMPLE_US 10
#define SERVO_TIME_US (CYCLE_TIME_US/NUM_SERVOS)
#define SERVO_SAMPLES (SERVO_TIME_US/SAMPLE_US)
#define SERVO_MIN 0
#define SERVO_MAX (SERVO_SAMPLES - 1)
#define NUM_SAMPLES (CYCLE_TIME_US/SAMPLE_US)
#define NUM_CBS (NUM_SAMPLES*2)
#define NUM_PAGES ((NUM_CBS * 32 + NUM_SAMPLES * 4 + \
PAGE_SIZE - 1) >> PAGE_SHIFT)
#define DMA_BASE 0x20007000
#define DMA_LEN 0x24
#define PWM_BASE 0x2020C000
#define PWM_LEN 0x28
#define CLK_BASE 0x20101000
#define CLK_LEN 0xA8
#define GPIO_BASE 0x20200000
#define GPIO_LEN 0x100
#define DMA_NO_WIDE_BURSTS (1<<26)
#define DMA_WAIT_RESP (1<<3)
#define DMA_D_DREQ (1<<6)
#define DMA_PER_MAP(x) ((x)<<16)
#define DMA_END (1<<1)
#define DMA_RESET (1<<31)
#define DMA_INT (1<<2)
#define DMA_CS (0x00/4)
#define DMA_CONBLK_AD (0x04/4)
#define DMA_DEBUG (0x20/4)
#define GPIO_FSEL0 (0x00/4)
#define GPIO_SET0 (0x1c/4)
#define GPIO_CLR0 (0x28/4)
#define GPIO_LEV0 (0x34/4)
#define GPIO_PULLEN (0x94/4)
#define GPIO_PULLCLK (0x98/4)
#define GPIO_MODE_IN 0
#define GPIO_MODE_OUT 1
#define PWM_CTL (0x00/4)
#define PWM_DMAC (0x08/4)
#define PWM_RNG1 (0x10/4)
#define PWM_FIFO (0x18/4)
#define PWMCLK_CNTL 40
#define PWMCLK_DIV 41
#define PWMCTL_MODE1 (1<<1)
#define PWMCTL_PWEN1 (1<<0)
#define PWMCTL_CLRF (1<<6)
#define PWMCTL_USEF1 (1<<5)
#define PWMDMAC_ENAB (1<<31)
#define PWMDMAC_THRSHLD ((15<<8)|(15<<0))
typedef struct {
uint32_t info, src, dst, length,
stride, next, pad[2];
} dma_cb_t;
struct ctl {
uint32_t sample[NUM_SAMPLES];
dma_cb_t cb[NUM_CBS];
};
typedef struct {
uint8_t *virtaddr;
uint32_t physaddr;
} page_map_t;
page_map_t *page_map;
static uint8_t *virtbase;
static volatile uint32_t *pwm_reg;
static volatile uint32_t *clk_reg;
static volatile uint32_t *dma_reg;
static volatile uint32_t *gpio_reg;
static void set_servo(int servo, int width);
static void
gpio_set_mode(uint32_t pin, uint32_t mode)
{
uint32_t fsel = gpio_reg[GPIO_FSEL0 + pin/10];
fsel &= ~(7 << ((pin % 10) * 3));
fsel |= mode << ((pin % 10) * 3);
gpio_reg[GPIO_FSEL0 + pin/10] = fsel;
}
static void
gpio_set(int pin, int level)
{
if (level)
gpio_reg[GPIO_SET0] = 1 << pin;
else
gpio_reg[GPIO_CLR0] = 1 << pin;
}
static void
udelay(int us)
{
struct timespec ts = { 0, us * 1000 };
nanosleep(&ts, NULL);
}
static void
terminate(int dummy)
{
int i;
if (dma_reg && virtbase) {
for (i = 0; i < NUM_SERVOS; i++)
set_servo(i, 0);
udelay(CYCLE_TIME_US);
dma_reg[DMA_CS] = DMA_RESET;
udelay(10);
}
unlink(DEVFILE);
exit(1);
}
static void
fatal(char *fmt, ...)
{
va_list ap;
va_start(ap, fmt);
vfprintf(stderr, fmt, ap);
va_end(ap);
terminate(0);
}
static uint32_t
mem_virt_to_phys(void *virt)
{
uint32_t offset = (uint8_t *)virt - virtbase;
return page_map[offset >> PAGE_SHIFT].physaddr + (offset % PAGE_SIZE);
}
static void *
map_peripheral(uint32_t base, uint32_t len)
{
int fd = open("/dev/mem", O_RDWR);
void * vaddr;
if (fd < 0)
fatal("servod: Failed to open /dev/mem: %m\n");
vaddr = mmap(NULL, len, PROT_READ|PROT_WRITE, MAP_SHARED, fd, base);
if (vaddr == MAP_FAILED)
fatal("servod: Failed to map peripheral at 0x%08x: %m\n", base);
close(fd);
return vaddr;
}
static void
set_servo(int servo, int width)
{
struct ctl *ctl = (struct ctl *)virtbase;
dma_cb_t *cbp = ctl->cb + servo * SERVO_SAMPLES * 2;
uint32_t phys_gpclr0 = 0x7e200000 + 0x28;
uint32_t phys_gpset0 = 0x7e200000 + 0x1c;
uint32_t *dp = ctl->sample + servo * SERVO_SAMPLES;
int i;
uint32_t mask = 1 << servo2gpio[servo];
dp[width] = mask;
if (width == 0) {
cbp->dst = phys_gpclr0;
} else {
for (i = width - 1; i > 0; i--)
dp[i] = 0;
dp[0] = mask;
cbp->dst = phys_gpset0;
}
}
static void
make_pagemap(void)
{
int i, fd, memfd, pid;
char pagemap_fn[64];
page_map = malloc(NUM_PAGES * sizeof(*page_map));
if (page_map == 0)
fatal("servod: Failed to malloc page_map: %m\n");
memfd = open("/dev/mem", O_RDWR);
if (memfd < 0)
fatal("servod: Failed to open /dev/mem: %m\n");
pid = getpid();
sprintf(pagemap_fn, "/proc/%d/pagemap", pid);
fd = open(pagemap_fn, O_RDONLY);
if (fd < 0)
fatal("servod: Failed to open %s: %m\n", pagemap_fn);
if (lseek(fd, (uint32_t)virtbase >> 9, SEEK_SET) !=
(uint32_t)virtbase >> 9) {
fatal("servod: Failed to seek on %s: %m\n", pagemap_fn);
}
for (i = 0; i < NUM_PAGES; i++) {
uint64_t pfn;
page_map[i].virtaddr = virtbase + i * PAGE_SIZE;
// Following line forces page to be allocated
page_map[i].virtaddr[0] = 0;
if (read(fd, &pfn, sizeof(pfn)) != sizeof(pfn))
fatal("servod: Failed to read %s: %m\n", pagemap_fn);
if (((pfn >> 55) & 0x1bf) != 0x10c)
fatal("servod: Page %d not present (pfn 0x%016llx)\n", i, pfn);
page_map[i].physaddr = (uint32_t)pfn << PAGE_SHIFT | 0x40000000;
}
close(fd);
close(memfd);
}
static void
setup_sighandlers(void)
{
int i;
// Catch all signals possible - it is vital we kill the DMA engine
// on process exit!
for (i = 0; i < 64; i++) {
struct sigaction sa;
memset(&sa, 0, sizeof(sa));
sa.sa_handler = terminate;
sigaction(i, &sa, NULL);
}
}
static void
init_ctrl_data(void)
{
struct ctl *ctl = (struct ctl *)virtbase;
dma_cb_t *cbp = ctl->cb;
uint32_t phys_pwm_fifo_addr = 0x7e20c000 + 0x18;
uint32_t phys_gpclr0 = 0x7e200000 + 0x28;
int servo, i;
memset(ctl->sample, 0, sizeof(ctl->sample));
for (servo = 0 ; servo < NUM_SERVOS; servo++) {
for (i = 0; i < SERVO_SAMPLES; i++)
ctl->sample[servo * SERVO_SAMPLES + i] = 1 << servo2gpio[servo];
}
for (i = 0; i < NUM_SAMPLES; i++) {
cbp->info = DMA_NO_WIDE_BURSTS | DMA_WAIT_RESP;
cbp->src = mem_virt_to_phys(ctl->sample + i);
cbp->dst = phys_gpclr0;
cbp->length = 4;
cbp->stride = 0;
cbp->next = mem_virt_to_phys(cbp + 1);
cbp++;
// Delay
cbp->info = DMA_NO_WIDE_BURSTS | DMA_WAIT_RESP | DMA_D_DREQ | DMA_PER_MAP(5);
cbp->src = mem_virt_to_phys(ctl); // Any data will do
cbp->dst = phys_pwm_fifo_addr;
cbp->length = 4;
cbp->stride = 0;
cbp->next = mem_virt_to_phys(cbp + 1);
cbp++;
}
cbp--;
cbp->next = mem_virt_to_phys(ctl->cb);
}
static void
init_hardware(void)
{
struct ctl *ctl = (struct ctl *)virtbase;
// Initialise PWM
pwm_reg[PWM_CTL] = 0;
udelay(10);
clk_reg[PWMCLK_CNTL] = 0x5A000006; // Source=PLLD (500MHz)
udelay(100);
clk_reg[PWMCLK_DIV] = 0x5A000000 | (50<<12); // set pwm div to 50, giving 10MHz
udelay(100);
clk_reg[PWMCLK_CNTL] = 0x5A000016; // Source=PLLD and enable
udelay(100);
pwm_reg[PWM_RNG1] = SAMPLE_US * 10;
udelay(10);
pwm_reg[PWM_DMAC] = PWMDMAC_ENAB | PWMDMAC_THRSHLD;
udelay(10);
pwm_reg[PWM_CTL] = PWMCTL_CLRF;
udelay(10);
pwm_reg[PWM_CTL] = PWMCTL_USEF1 | PWMCTL_PWEN1;
udelay(10);
// Initialise the DMA
dma_reg[DMA_CS] = DMA_RESET;
udelay(10);
dma_reg[DMA_CS] = DMA_INT | DMA_END;
dma_reg[DMA_CONBLK_AD] = mem_virt_to_phys(ctl->cb);
dma_reg[DMA_DEBUG] = 7; // clear debug error flags
dma_reg[DMA_CS] = 0x10880001; // go, mid priority, wait for outstanding writes
}
static void
go_go_go(void)
{
FILE *fp;
if ((fp = fopen(DEVFILE, "r+")) == NULL)
fatal("servod: Failed to open %s: %m\n", DEVFILE);
char *lineptr = NULL, nl;
size_t linelen;
for (;;) {
int n, width, servo;
if ((n = getline(&lineptr, &linelen, fp)) < 0)
continue;
//fprintf(stderr, "[%d]%s", n, lineptr);
n = sscanf(lineptr, "%d=%d%c", &servo, &width, &nl);
if (n !=3 || nl != '\n') {
fprintf(stderr, "Bad input: %s", lineptr);
} else if (servo < 0 || servo >= NUM_SERVOS) {
fprintf(stderr, "Invalid servo number %d\n", servo);
} else if (width < SERVO_MIN || width > SERVO_MAX) {
fprintf(stderr, "Invalid width %d\n", width);
} else {
set_servo(servo, width);
}
}
}
int
main(int argc, char **argv)
{
int i;
printf("Number of servos: %5d\n", NUM_SERVOS);
printf("Servo cycle time: %5dus\n", CYCLE_TIME_US);
printf("Pulse width units: %5dus\n", SAMPLE_US);
printf("Maximum width value: %5d (%dus)\n", SERVO_MAX,
SERVO_MAX * SAMPLE_US);
setup_sighandlers();
dma_reg = map_peripheral(DMA_BASE, DMA_LEN);
pwm_reg = map_peripheral(PWM_BASE, PWM_LEN);
clk_reg = map_peripheral(CLK_BASE, CLK_LEN);
gpio_reg = map_peripheral(GPIO_BASE, GPIO_LEN);
virtbase = mmap(NULL, NUM_PAGES * PAGE_SIZE, PROT_READ|PROT_WRITE,
MAP_SHARED|MAP_ANONYMOUS|MAP_NORESERVE|MAP_LOCKED,
-1, 0);
if (virtbase == MAP_FAILED)
fatal("servod: Failed to mmap physical pages: %m\n");
if ((unsigned long)virtbase & (PAGE_SIZE-1))
fatal("servod: Virtual address is not page aligned\n");
make_pagemap();
for (i = 0; i < NUM_SERVOS; i++) {
gpio_set(servo2gpio[i], 0);
gpio_set_mode(servo2gpio[i], GPIO_MODE_OUT);
}
init_ctrl_data();
init_hardware();
unlink(DEVFILE);
if (mkfifo(DEVFILE, 0666) < 0)
fatal("servod: Failed to create %s: %m\n", DEVFILE);
if (chmod(DEVFILE, 0666) < 0)
fatal("servod: Failed to set permissions on %s: %m\n", DEVFILE);
if (daemon(0,1) < 0)
fatal("servod: Failed to daemonize process: %m\n");
go_go_go();
return 0;
}