From d8df9b762587be93825f80ed43143c4cfdbb154a Mon Sep 17 00:00:00 2001 From: Richard Hirst Date: Sun, 19 Aug 2012 17:11:33 +0100 Subject: [PATCH] Added ServoBlaster --- README.md | 11 +- ServoBlaster/Kbuild | 10 + ServoBlaster/Makefile | 14 ++ ServoBlaster/README.txt | 82 ++++++++ ServoBlaster/rock.sh | 37 ++++ ServoBlaster/servoblaster.c | 380 +++++++++++++++++++++++++++++++++++ ServoBlaster/servoblaster.h | 0 ServoBlaster/servoblaster.ko | Bin 0 -> 8909 bytes ServoBlaster/servodemo.c | 4 + 9 files changed, 537 insertions(+), 1 deletion(-) create mode 100644 ServoBlaster/Kbuild create mode 100644 ServoBlaster/Makefile create mode 100644 ServoBlaster/README.txt create mode 100755 ServoBlaster/rock.sh create mode 100644 ServoBlaster/servoblaster.c create mode 100644 ServoBlaster/servoblaster.h create mode 100644 ServoBlaster/servoblaster.ko create mode 100644 ServoBlaster/servodemo.c diff --git a/README.md b/README.md index fce556b..8434ea9 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,11 @@ PiBits -====== \ No newline at end of file +====== + +Here you'll find various RaspberryPi related projcts: + +ServoBlaster: A kernel driver that lets you control 8 (or more) servos from your RaspberryPi. + +Panalyzer: A Pi based Logic Analyzer. This can be found in a separate repository alongside this one, but is mentioned here for completeness. + + + diff --git a/ServoBlaster/Kbuild b/ServoBlaster/Kbuild new file mode 100644 index 0000000..f1d9454 --- /dev/null +++ b/ServoBlaster/Kbuild @@ -0,0 +1,10 @@ +obj-m = servoblaster.o + +KERNEL_TREE := /home/richard/Pi/git/linux + +module: + make -C ${KERNEL_TREE} ARCH=arm CROSS_COMPILE=/usr/bin/arm-linux-gnueabi- M=$(PWD) modules + +clean: + make -C ${KERNEL_TREE} ARCH=arm CROSS_COMPILE=/usr/bin/arm-linux-gnueabi- M=$(PWD) clean + diff --git a/ServoBlaster/Makefile b/ServoBlaster/Makefile new file mode 100644 index 0000000..623dea7 --- /dev/null +++ b/ServoBlaster/Makefile @@ -0,0 +1,14 @@ +KERNEL_TREE := /home/richard/Pi/git/linux + +all: servoblaster.ko servodemo + +servoblaster.ko: servoblaster.c servoblaster.h + make -C ${KERNEL_TREE} ARCH=arm CROSS_COMPILE=/usr/bin/arm-linux-gnueabi- M=$(PWD) modules + +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` + +clean: + make -C ${KERNEL_TREE} ARCH=arm CROSS_COMPILE=/usr/bin/arm-linux-gnueabi- M=$(PWD) clean + rm -f servodemo + diff --git a/ServoBlaster/README.txt b/ServoBlaster/README.txt new file mode 100644 index 0000000..bb80779 --- /dev/null +++ b/ServoBlaster/README.txt @@ -0,0 +1,82 @@ + 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. + +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 +the position of the servo. The pulse should be repeated approximately every +20ms, although pulse frequency is not critical. The pulse width is critical, +as that translates directly to the servo position. + +The driver creates a device file, /dev/servoblaster, in to which you can send +commands. The command format is "=", where servo +number is a number from 0 to 7 inclusive, and servo position is the pulse width +you want in units of 10us. So, if you want to set servo 3 to a pulse width of +1.2ms you could do this at the shell prompt: + +echo 3=120 > /dev/servoblaster + +120 is in units os 10us, so that is 1200us, or 1.2ms. + +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 +arbitrary postion when you load the driver. Once you know where you want your +servos positioned, write a value to /dev/servoblaster to enable the respective +output. When the driver is unloaded it attempts to shut down the outputs +cleanly, rather than cutting some pulse short and causing a servo position to +jump. + +The driver allocates a timeslot of 2.5ms to each output (8 servos resulting in +a cycle time of 20ms). A servo output is set high at the start of its 2.5ms +timeslot, and set low after the appropriate delay. There is then a further +delay to take us to the end of that timeslot before the next servo output is +set high. This way there is only ever one servo output active at a time, which +helps keep the code simple. + +The driver works by setting up a linked list of DMA control blocks with the +last one linked back to the first, so once initialized the DMA controller +cycles round continuously and the driver does not need to get involved except +when a pulse width needs to be changed. For a given servo there are four DMA +control blocks; the first transfers a single word to the GPIO 'set output' +register, the second transfers some number of words to the PWM FIFO to generate +the required pulse width time, the third transfers a single word to the GPIO +'clear output' register, and the fourth transfers a number of words to the PWM +FIFO to generate a delay up to the end of the 2.5ms timeslot. + +While the driver does use the PWM peripheral, it only uses it to pace the DMA +transfers, so as to generate accurate delays. The PWM is set up such that it +consumes one word from the FIFO every 10us, so to generate a delay of 1.2ms the +driver sets the DMA transfer count to 480 (1200/10*4, as the FIFO is 32 bits +wide). The PWM is set to request data as soon as there is a single word free +in the FIFO, so there should be no burst transfers to upset the timing. + +I used Panalyzer running on one Pi to mointor the servo outputs from a second +Pi. The pulse widths and frequencies seem very stable, even under heavy SD +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. + +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 +driving those pins externally. + +I would of course recommend some buffering between the GPIO outputs and the +servo controls, to protect the Pi. That said, I'm living dangerously and doing +without :-) If you just want to experiment with a small servo you can probably +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! + + +Richard Hirst August 2012 + diff --git a/ServoBlaster/rock.sh b/ServoBlaster/rock.sh new file mode 100755 index 0000000..8ed67a7 --- /dev/null +++ b/ServoBlaster/rock.sh @@ -0,0 +1,37 @@ +#!/bin/bash + +# I have four servos driving four 'legs' on a robot. This script makes +# the legs go though something like a walking motion. It actually just +# skids back and fore on the spot at the moment, becuase it needs knees +# and lower legs before it can really lift up its feet and take a real +# step forward. This is just a quick demo though.. + +ss() +{ + echo $1 > /dev/servoblaster +} + +p[0]=132 +p[1]=164 +p[2]=150 +p[3]=170 + +step=2 +cnt=0 +while true; do + for i in 0 1 2 3; do + ss $i=${p[$i]} + p[$i]=$[ p[$i] + step ] + sleep 0.05 + done + cnt=$[ cnt + step ] + if [ $step -gt 0 ]; then + if [ $cnt -ge 20 ]; then + step=-2 + fi + else + if [ $cnt -lt -20 ]; then + step=2 + fi + fi +done diff --git a/ServoBlaster/servoblaster.c b/ServoBlaster/servoblaster.c new file mode 100644 index 0000000..a6b1c80 --- /dev/null +++ b/ServoBlaster/servoblaster.c @@ -0,0 +1,380 @@ +/* + * servoblaster.c Multiple Servo Driver for the RaspberryPi + * Copyright (c) 2012 Richard Hirst + * + * 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. + */ + +/* + * If you want the device node created automatically create these two + * files, and make /lib/udev/servoblaster executable (chmod +x): + * + * ============= /etc/udev/rules.d/20-servoblaster.rules ============= + * SUBSYSTEM=="module", DEVPATH=="/module/servoblaster", RUN+="/lib/udev/servoblaster" + * =================================================================== + * + * ===================== /lib/udev/servoblaster ====================== + * #!/bin/bash + * + * if [ "$ACTION" = "remove" ]; then + * rm -f /dev/servoblaster + * elif [ "$ACTION" = "add" ]; then + * major=$( sed -n 's/ servoblaster//p' /proc/devices ) + * [ "$major" ] && mknod -m 0666 /dev/servoblaster c $major 0 + * fi + * + * exit 0 + * =================================================================== + */ + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include "servoblaster.h" + +#define GPIO_LEN 0xb4 +#define DMA_LEN 0x24 +#define PWM_BASE (BCM2708_PERI_BASE + 0x20C000) +#define PWM_LEN 0x28 +#define CLK_BASE (BCM2708_PERI_BASE + 0x101000) +#define CLK_LEN 0xA8 + +#define GPFSEL0 (0x00/4) +#define GPFSEL1 (0x04/4) +#define GPSET0 (0x1c/4) +#define GPCLR0 (0x28/4) + +#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) +// I think this means it requests as soon as there is one free slot in the FIFO +// which is what we want as burst DMA would mess up our timing.. +#define PWMDMAC_THRSHLD ((15<<8)|(15<<0)) + +#define DMA_CS (BCM2708_DMA_CS/4) +#define DMA_CONBLK_AD (BCM2708_DMA_ADDR/4) +#define DMA_DEBUG (BCM2708_DMA_DEBUG/4) + +#define BCM2708_DMA_END (1<<1) // Why is this not in mach/dma.h ? +#define BCM2708_DMA_NO_WIDE_BURSTS (1<<26) + +static int dev_open(struct inode *, struct file *); +static int dev_close(struct inode *, struct file *); +static ssize_t dev_read(struct file *, char *, size_t, loff_t *); +static ssize_t dev_write(struct file *, const char *, size_t, loff_t *); +static long dev_ioctl(struct file *, unsigned int, unsigned long); + +static struct file_operations fops = +{ + .open = dev_open, + .read = dev_read, + .write = dev_write, + .release = dev_close, + .unlocked_ioctl = dev_ioctl, + .compat_ioctl = dev_ioctl, +}; + +// Map servo channels to GPIO pins +static uint8_t servo2gpio[] = { + 4, // P1-7 + 17, // P1-11 +#ifdef PWM0_ON_GPIO18 + 1, // P1-5 (GPIO-18, P1-12 is currently PWM0, for debug) +#else + 18, // P1-12 +#endif + 21, // P1-13 + 22, // P1-15 + 23, // P1-16 + 24, // P1-18 + 25, // P1-22 +}; +#define NUM_SERVOS (sizeof(servo2gpio)/sizeof(servo2gpio[0])) + +// Structure of our control data, stored in a 4K page, and accessed by dma controller +struct ctldata_s { + struct bcm2708_dma_cb cb[NUM_SERVOS * 4]; // gpio-hi, delay, gpio-lo, delay, for each servo output + uint32_t gpiodata[NUM_SERVOS]; // set-pin, clear-pin values, per servo output + uint32_t pwmdata; // the word we write to the pwm fifo +}; + +static struct ctldata_s *ctl; +static unsigned long ctldatabase; +static volatile uint32_t *gpio_reg; +static volatile uint32_t *dma_reg; +static volatile uint32_t *clk_reg; +static volatile uint32_t *pwm_reg; + +static dev_t devno; +static struct cdev my_cdev; +static int my_major; + +// Wait until we're not processing the given servo (actually wait until +// we are not processing the low period of the previous servo, or the +// high period of this one). +static int wait_for_servo(int servo) +{ + local_irq_disable(); + for (;;) { + int cb = (dma_reg[DMA_CONBLK_AD] - ((uint32_t)ctl->cb & 0x7fffffff)) / sizeof(ctl->cb[0]); + + if (servo > 0) { + if (cb < servo*4-2 || cb > servo*4+2) + break; + } else { + if (cb > 2 && cb < NUM_SERVOS*4-2) + break; + } + local_irq_enable(); + set_current_state(TASK_INTERRUPTIBLE); + if (schedule_timeout(1)) + return -EINTR; + local_irq_disable(); + } + // Return with IRQs disabled!!! + return 0; +} + +int init_module(void) +{ + int res, i, s; + + res = alloc_chrdev_region(&devno, 0, 1, "servoblaster"); + if (res < 0) { + printk(KERN_WARNING "ServoBlaster: Can't allocated device number\n"); + return res; + } + my_major = MAJOR(devno); + cdev_init(&my_cdev, &fops); + my_cdev.owner = THIS_MODULE; + my_cdev.ops = &fops; + res = cdev_add(&my_cdev, MKDEV(my_major, 0), 1); + if (res) { + printk(KERN_WARNING "ServoBlaster: Error %d adding device\n", res); + unregister_chrdev_region(devno, 1); + return res; + } + + ctldatabase = __get_free_pages(GFP_KERNEL, 0); + printk(KERN_INFO "ServoBlaster: Control page is at 0x%lx\n", ctldatabase); + if (ctldatabase == 0) { + printk(KERN_WARNING "ServoBlaster: alloc_pages failed\n"); + cdev_del(&my_cdev); + unregister_chrdev_region(devno, 1); + return -EFAULT; + } + ctl = (struct ctldata_s *)ctldatabase; + gpio_reg = (uint32_t *)ioremap(GPIO_BASE, GPIO_LEN); + dma_reg = (uint32_t *)ioremap(DMA_BASE, DMA_LEN); + clk_reg = (uint32_t *)ioremap(CLK_BASE, CLK_LEN); + pwm_reg = (uint32_t *)ioremap(PWM_BASE, PWM_LEN); + + memset(ctl, 0, sizeof(*ctl)); + + // Set all servo control pins to be outputs + for (i = 0; i < NUM_SERVOS; i++) { + int gpio = servo2gpio[i]; + int fnreg = gpio/10 + GPFSEL0; + int fnshft = (gpio %10) * 3; + gpio_reg[GPCLR0] = 1 << gpio; + gpio_reg[fnreg] = (gpio_reg[fnreg] & ~(7 << fnshft)) | (1 << fnshft); + } +#ifdef PWM0_ON_GPIO18 + // Set pwm0 output on gpio18 + gpio_reg[GPCLR0] = 1 << 18; + gpio_reg[GPFSEL1] = (gpio_reg[GPFSEL1] & ~(7 << 8*3)) | ( 2 << 8*3); +#endif + + // Build the DMA CB chain + for (s = 0; s < NUM_SERVOS; s++) { + int i = s*4; + // Set gpio high + ctl->gpiodata[s] = 1 << servo2gpio[s]; + ctl->cb[i].info = BCM2708_DMA_NO_WIDE_BURSTS | BCM2708_DMA_WAIT_RESP; + ctl->cb[i].src = (uint32_t)(&ctl->gpiodata[s]) & 0x7fffffff; + // We clear the GPIO here initially, so outputs go to 0 on startup + // Once someone writes to /dev/servoblaster we'll patch it to a 'set' + ctl->cb[i].dst = ((GPIO_BASE + GPCLR0*4) & 0x00ffffff) | 0x7e000000; + ctl->cb[i].length = sizeof(uint32_t); + ctl->cb[i].stride = 0; + ctl->cb[i].next = (uint32_t)(ctl->cb + i + 1) & 0x7fffffff; + // delay + i++; + ctl->cb[i].info = BCM2708_DMA_NO_WIDE_BURSTS | BCM2708_DMA_WAIT_RESP | BCM2708_DMA_D_DREQ | BCM2708_DMA_PER_MAP(5); + ctl->cb[i].src = (uint32_t)(&ctl->pwmdata) & 0x7fffffff; + ctl->cb[i].dst = ((PWM_BASE + PWM_FIFO*4) & 0x00ffffff) | 0x7e000000; + ctl->cb[i].length = sizeof(uint32_t) * 100; // default 1000us high + ctl->cb[i].stride = 0; + ctl->cb[i].next = (uint32_t)(ctl->cb + i + 1) & 0x7fffffff; + // Set gpio lo + i++; + ctl->cb[i].info = BCM2708_DMA_NO_WIDE_BURSTS | BCM2708_DMA_WAIT_RESP; + ctl->cb[i].src = (uint32_t)(&ctl->gpiodata[s]) & 0x7fffffff; + ctl->cb[i].dst = ((GPIO_BASE + GPCLR0*4) & 0x00ffffff) | 0x7e000000; + ctl->cb[i].length = sizeof(uint32_t); + ctl->cb[i].stride = 0; + ctl->cb[i].next = (uint32_t)(ctl->cb + i + 1) & 0x7fffffff; + // delay + i++; + ctl->cb[i].info = BCM2708_DMA_NO_WIDE_BURSTS | BCM2708_DMA_WAIT_RESP | BCM2708_DMA_D_DREQ | BCM2708_DMA_PER_MAP(5); + ctl->cb[i].src = (uint32_t)(&ctl->pwmdata) & 0x7fffffff; + ctl->cb[i].dst = ((PWM_BASE + PWM_FIFO*4) & 0x00ffffff) | 0x7e000000; + ctl->cb[i].length = sizeof(uint32_t) * 150; // default 1500us, to give 2.5ms per servo + ctl->cb[i].stride = 0; + ctl->cb[i].next = (uint32_t)(ctl->cb + i + 1) & 0x7fffffff; + } + // Point last cb back to first one so it loops continuously + ctl->cb[NUM_SERVOS*4-1].next = (uint32_t)(ctl->cb) & 0x7fffffff; + + // Initialise PWM + pwm_reg[PWM_CTL] = 0; + udelay(10); + clk_reg[PWMCLK_DIV] = 0x5A000000 | (32<<12); // set pwm div to 32 (19.2MHz/32 = 600KHz) + clk_reg[PWMCLK_CNTL] = 0x5A000011; // Source=osc and enable + pwm_reg[PWM_RNG1] = 6; // 600KHz/6 = 10us per FIFO write + udelay(10); + ctl->pwmdata = 1; // Give a pulse of one clock width for each fifo write + 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] = BCM2708_DMA_RESET; + udelay(10); + dma_reg[DMA_CS] = BCM2708_DMA_INT | BCM2708_DMA_END; + dma_reg[DMA_CONBLK_AD] = (uint32_t)(ctl->cb) & 0x7fffffff; + dma_reg[DMA_DEBUG] = 7; // clear debug error flags + dma_reg[DMA_CS] = 0x10880001; // go, mid priority, wait for outstanding writes + + return 0; +} + + +void cleanup_module(void) +{ + int servo; + + // Take care to stop servos with outputs low, so we don't get + // spurious movement on module unload + for (servo = 0; servo < NUM_SERVOS; servo++) { + // Wait until we're not driving this servo + if (wait_for_servo(servo)) + break; + // Patch the control block so it stays low + ctl->cb[servo*4+0].dst = ((GPIO_BASE + GPCLR0*4) & 0x00ffffff) | 0x7e000000; + local_irq_enable(); + } + // Wait 20ms to be sure it has finished it's cycle an all outputs are low + msleep(20); + // Now we can kill everything + dma_reg[DMA_CS] = BCM2708_DMA_RESET; + pwm_reg[PWM_CTL] = 0; + udelay(10); + free_pages(ctldatabase, 0); + iounmap(gpio_reg); + iounmap(dma_reg); + iounmap(clk_reg); + iounmap(pwm_reg); + cdev_del(&my_cdev); + unregister_chrdev_region(devno, 1); +} + + +static int dev_open(struct inode *inod,struct file *fil) +{ + return 0; +} + +static ssize_t dev_read(struct file *filp,char *buf,size_t count,loff_t *f_pos) +{ + return 0; +} + +static ssize_t dev_write(struct file *filp,const char *buf,size_t count,loff_t *f_pos) +{ + int servo; + int cnt; + int n; + char str[32]; + char dummy; + + cnt = count < 32 ? count : 31; + if (copy_from_user(str, buf, cnt)) { + return -EFAULT; + } + str[cnt] = '\0'; + n = sscanf(str, "%d=%d\n%c", &servo, &cnt, &dummy); + if (n != 2) { + printk(KERN_WARNING "ServoBlaster: Failed to parse command (n=%d)\n", n); + return -EINVAL; + } + if (servo < 0 || servo >= NUM_SERVOS) { + printk(KERN_WARNING "ServoBlaster: Bad servo number %d\n", servo); + return -EINVAL; + } + if (cnt < 0 || cnt > 249) { + printk(KERN_WARNING "ServoBlaster: Bad value %d\n", cnt); + return -EINVAL; + } + if (wait_for_servo(servo)) + return -EINTR; + if (cnt == 0) { + ctl->cb[servo*4+0].dst = ((GPIO_BASE + GPCLR0*4) & 0x00ffffff) | 0x7e000000; + } else { + ctl->cb[servo*4+0].dst = ((GPIO_BASE + GPSET0*4) & 0x00ffffff) | 0x7e000000; + ctl->cb[servo*4+1].length = cnt * sizeof(uint32_t); + ctl->cb[servo*4+3].length = (250 - cnt) * sizeof(uint32_t); + } + local_irq_enable(); + + return count; +} + +static int dev_close(struct inode *inod,struct file *fil) +{ + return 0; +} + +static long dev_ioctl(struct file *file, unsigned int cmd, unsigned long arg) +{ + return -EINVAL; +} + +MODULE_DESCRIPTION("ServoBlaster, Multiple Servo Driver for the RaspberryPi"); +MODULE_AUTHOR("Richard Hirst "); +MODULE_LICENSE("GPL v2"); + diff --git a/ServoBlaster/servoblaster.h b/ServoBlaster/servoblaster.h new file mode 100644 index 0000000..e69de29 diff --git a/ServoBlaster/servoblaster.ko b/ServoBlaster/servoblaster.ko new file mode 100644 index 0000000000000000000000000000000000000000..afd9fcba6b48ffbb5960dfb5697fd50a15cf59ca GIT binary patch literal 8909 zcmds64Qw3Oai05;L`#%)l&R>4MEqn^2<=Fhlf+82E85hbMMrdemgH6q0xy?$OLDFI z)83vewMl%ulVjPDTmJ}7kl5iRb`mFOPz6wfz;z44He}T?S^*8*6cv)aJ8!MXY9cjC zV7E!wZ+74ERwpYVg8%{Ypw%}sZ{EzjdGp@wK0dg8@Qy$rKwb+_H#In-OV>F)b9x-; zre(B*7EV^G^n*1&4j&(^QU97B|9Wcb9imE|UU>HG@3U_SS<^P({f{Pp@k+Hwtryw% z!dRs`XS`BZ}a?4PiM#Ur)bhfdZP5}lhzyQ@oxUTgtkIjXiA# zIBqK2?i}K*zrwTIO2^I;-CGZfC+*-`*p4Nv=hjuK=ccA!55>0DR?zPHJ5y7S^4#D3 z#Js9vz5u*zeIj^$nmy824po zP2+XU>AJ}i4Qipv;OxXWxC2G`Gsd9coYb(|9mrGVLRI;vxY|@&rTdbnJ zI(F1fMUFbYIp=d=TL{>S9kWBUi`QkvveP?jIn>UP*n~ZZzHsa*yR^P+72%^rln(J+77&&C;q|U>Uc+8m zVE-%EAm*LhJz+dwhW4EqswL^L2^F9h9KUyyQw%#yNh zDcb|tX(@YS@r1n>e3*_ou}^}JgKmkB*8-E3`aG*r4~UEQ9J;6awDF47X;i8=qDH!N zMLV>=Y&|TtSfTxozg-d~>x0H?)y^9pu7*|?-@Z6CwIVDKJA7{WNvSralxo-))xhI; zP&=?)4Oai99b9xw1`G*^YpY7+kMqJB+wnfyB0-d88&l$1pBH#ab8W^VJD^ z2;;gzFaJrUz8d%jWY;`Vvid>43i{Enl&k^JY-dE@kKjoT<*TavO?QM7>lV!WTf^oo}V%d%Z$KW$%mF*m8a|rPvAH`U?_K(Qd zUaWPn?dAHV>q<5WoM)nJ4+Pxx+=IS;oCkZ6)8`{)yBGA_c%@ze-36UUyyUDO=RAO% zvn|nHvfEdpR?yG;k86zO{jhxj`-Sa$vXzjX@yU2!2Uk|=rnI}jGu>b z0(e}w_Ijzq?#4NA;qP#MVy&LRc=pjkZd^?N9DDPKl>ZOtqNI7hH0v&mS@m9=m1pIC zK7xJgwMz~m#OTP#2<1pnBqC^1h^{cv6dHH85QTZ)5735QeSFa( zDhB9CI$0P8A$!EgBrr*R$|Abu`!u*hLKKBNH&A*fJ`aJC4F!J;e&bXE#H&XH%i<%@yw3K0ze+L1Wc3AZg?@`0txmZ$WDmdR^^Y@4D;220^qc*|#b=e^mmeXj9K^ z{y=UOcOY0TDa@?MsYYHCh&!!jlA=3Q>dv_HldmbDybbn`lXZkSH`~GFp#iSNuef_dS+DWyD@KQn&;MR z#x$}i%yLu{dS0leh#g#&ItcA6a^ggwux4J2IQemGa^0@R!*!CxQ^eDY`e{e)bfIo` z+d1nKluBvwZ~q*^vT^fxurkr)H^}W`|Rl7C`#T`6Z^1u`;^fF&d8aEPj^_{Qi0C`nrEBdEfJ2S)?dwE&Z38k^QaOS1Kg$e(nFb{&I&<0NJ!s zz=3zA*3~7S-*Us2d_JLOMy{^&n|f|9uYNP17}J~+$kfx?ERUcs{MYB+Gz~SODg5K6 z8~gLh%r>43sw3KWzIi;}erDWGM)pRr|1@=2$IsaVdM2qPaycbuteu$&tNUBu#{SC` zlym}zsHtR}pS+n3{sU<>|0e!{!uX?b8^$c!@!;o}c6{t!I(hDf(cbQjU;pou-+b}z zPpsT_|EJU7=9YpZTz`|x!^VtNH{T4GPT^8O zfSj{|=FV*iToYIvZVxO8EDbFSEDx**elWN?K+*i6w5bjQO~b`8k217W)YJ}|6veAs z6a=fW&}H@HL9p4RYN`~)i-w*V$$A2|wl>=1sN)QC>mTn?q$u9|pliw<%|@|NqA5MI zFPlkdQFBbsEAGn+MKf7bbGnBMdMdd_Pf~O^pC^8&O=}s1=O`e>Yn;R04OcZyLmw`f zS{@0Qal0Oxc!i?5azqc%!n>5$cul!=;JL=3-2YB2#k*IK=mTJRJnOaKStWUNIjtSS zAxSfM_Sg@`~lYK=5F~9?5V|&c<3Fo(W3U8_cGOu}l6$t$EFju(q<$182^*COB z_4C_13wx7uAe7Cs_aJ!IMr_IoBl~0QC;EZJ^Afob zSRP^IIFZ}Vj}e`gctK(qF|mA^#MKhV7-8RJyo;zP`J)n#G44Sevi~@c=XHV+&vzNI zcrP*DhB&4C3?pK)B(|XFS-(|cL?VBmavtK0$om-ME}UhIi1!%dR;&Z#W}@dM{;k9l z5?^9OoTnHukJCVo^Nd6+Ki!Nqc3RxsKR077c#p9hs>D3k7xV3s=err^`M!qxh2*;= z-!1vol8;HgNAi5X!{hlbhsSdZf=6l`WdF>we@6Dt$o?7GKO_5RboB9u14kb}K7!@g zOdJm*#}UMyW&c4OW-l>rLw+$fxCP8_Xd!}P8C@7Sz{5&dJsggq&#o zy#It`dkY$!phC{98ln*K$=PTjCp&(gPoYK&(dCC)h;S(RS_oP7&oE3Er#Rn@7MLF4 z2?-sAXf=U&Nhodv)TNq&*T&!hc7ravP2 zaUixmp~Rp+uPHyR^fct$#_$BNzU;rzTAKJa`l9UQN()dMJqKjk=l2OanE{|1`Z$Ls%3(0hG+9kkcDUO4{4o&fXxh2*)idH(aT zPLF}!Ao z|Enf`(ez$$*S))m-`K?OXyWf{;sVnKwhNiki zB9+Z+j!4fY%oIJK>bR228j54X>3G1o@jF z-glwDe&a?O$>#EuOsjlJN$d(SWwMk$q@>mRvj!7ek1 zocERJyW`~hMo)aj1(e@`b0c@f73uVyr@Z`