Sunday, January 19, 2014

Raspberry Pi GPIO pins to stepper controller

I picked up a 3-axis beefy NEMA-23 stepper kit from ebay for around $240.  It comes with a Longs Motor DM542A stepper controller, which seems to be more or less identical to a lot of other brands.



The kit came with a board that takes commands from a PC parallel port (driven by eg., linuxcnc), and apparently the timing is such that USB-parallel adapters don't work well; you need a hardware parallel port.

I wasn't particularly excited to track down a machine old enough to have a parallel port and lug it around with my project, so I looked into whether the GPIO pins on my Raspberry Pi would be suitable to drive the DIR (direction) and PUL (pulse / step) pins directly.  I found some general discussion, but not much that was concrete.



I think I originally picked up NOOBS to put on the SD card, but it's been a while and I don't remember exactly.  But it came up with a menu and I picked the recommended option of Raspbian wheezy, which it downloaded and installed.  Installation was a breeze, and the machine has been working great for me.  (I'm writing this post on it, even, although it's pretty laggy if I try to open multiple tabs in the browser).

Most of the skepticism I saw about using the GPIO pins centered around the need for an RTOS to ensure that the process toggling the pulse line can do so at the required intervals without getting held up for extra milliseconds.  When I ran "uname -a", I noticed "PREEMPT", which means that it already has some (but perhaps not the most extreme variety?) kernel patches for hard timing.  Awesome!

Voltages were another concern: the stepper driver wants 5v logic inputs, but the raspi runs at 3.3v.  Fortunately, this doesn't seem to be a problem either; the driver board seems perfectly happy with 3.3v input.

At first I though my driver wasn't working right when I wired it up -- I got no power light.  But it turns out the ENBL line needs to be pulled down rather than up for the board to work.

I also found that the board doesn't seem to have the - side of each input tied together, so I had to wire up the ENBL-, DIR- and PUL- inputs all to ground.

I looked over the pinout for P1 on the raspi to figure out which GPIO lines to use, and didn't find any major reasons to use any particular ones; seems like almost all of them have alternate uses that I mostly don't think I'll need.  So I used pins 1, 3, 5, 7 and 9, corresponding to +3.3v, GPIO-0, GPIO-1, GPIO-4 and GND on my rev. 1 board.

My first stab at controlling the stepper used the python example code.  Note that this code uses board pin numbers rather than the GPIO numbering:


#!/usr/bin/python

import RPi.GPIO as GPIO
import time

# use P1 header pin numbering convention
GPIO.setmode(GPIO.BOARD)

# Set up the GPIO channels - one input and one output
GPIO.setup(3, GPIO.OUT)
GPIO.setup(5, GPIO.OUT)
GPIO.setup(7, GPIO.OUT)

GPIO.output(3, GPIO.HIGH)

while True:
  GPIO.output(5, GPIO.HIGH)
  time.sleep(0.001)
  GPIO.output(5, GPIO.LOW)
  time.sleep(0.001)

This worked just fine, although as expected, it'd glitch every few seconds, audible as a stuttering sound in the stepper.  Glitching got worse when I loaded down the system by doing things like loading web pages.

Next I tried the C example code to see if it'd do better.  It might have been somewhat better, but still had noticeable glitching.  

I found excellent wiki page with example code for using the RTOS patches, and mashed it up with the GPIO example code, and it works great!

So it looks like the principle is sound.  The remaining task is to get it integrated with some sort of g-code interpreter (update: I ported grbl to raspi), at which point we'll have a $35 CNC controller with HDMI output that can drive step/direction pins on stepper controllers directly, without having to use parallel ports and adapter boards.

// Compile with:
// gcc -o rt_gpio rt_gpio.c -lrt

#include <stdlib.h>
#include <stdio.h>
#include <time.h>
#include <sched.h>
#include <sys/mman.h>

#include <string.h>

// defines for using the realtime kernel
#define MY_PRIORITY (49) /* we use 49 as the PRREMPT_RT use 50
                            as the priority of kernel tasklets
                            and interrupt handler by default */

#define MAX_SAFE_STACK (8*1024) /* The maximum stack size which is
                                   guaranteed safe to access without
                                   faulting */

#define NSEC_PER_SEC    (1000000000) /* The number of nsecs per sec. */


// defines for talking to the gpio pins

// Access from ARM Running Linux

#define BCM2708_PERI_BASE        0x20000000
#define GPIO_BASE                (BCM2708_PERI_BASE + 0x200000) /* GPIO controll
er */


#include <stdio.h>
#include <stdlib.h>
#include <fcntl.h>
#include <sys/mman.h>
#include <unistd.h>

#define PAGE_SIZE (4*1024)
#define BLOCK_SIZE (4*1024)

int  mem_fd;
void *gpio_map;

// I/O access
volatile unsigned *gpio;

// GPIO setup macros. Always use INP_GPIO(x) before using OUT_GPIO(x) or SET_GPIO_ALT(x,y)
#define INP_GPIO(g) *(gpio+((g)/10)) &= ~(7<<(((g)%10)*3))
#define OUT_GPIO(g) *(gpio+((g)/10)) |=  (1<<(((g)%10)*3))

#define SET_GPIO_ALT(g,a) *(gpio+(((g)/10))) |= (((a)<=3?(a)+4:(a)==4?3:2)<<(((g)%10)*3))

#define GPIO_SET *(gpio+7)  // sets   bits which are 1 ignores bits which are 0
#define GPIO_CLR *(gpio+10) // clears bits which are 1 ignores bits which are 0

void setup_io();


void stack_prefault(void) {

        unsigned char dummy[MAX_SAFE_STACK];

        memset(dummy, 0, MAX_SAFE_STACK);
        return;
}

int main(int argc, char* argv[])
{
        struct timespec t;
        struct sched_param param;
        int interval = 1000000; // ns

        /* Declare ourself as a real time task */

        param.sched_priority = MY_PRIORITY;
        if(sched_setscheduler(0, SCHED_FIFO, &param) == -1) {
                perror("sched_setscheduler failed");
                exit(-1);
        }

        /* Lock memory */

        if(mlockall(MCL_CURRENT|MCL_FUTURE) == -1) {
                perror("mlockall failed");
                exit(-2);
        }

        /* Pre-fault our stack */

        stack_prefault();

        clock_gettime(CLOCK_MONOTONIC ,&t);
        /* start after one second */
        t.tv_sec++;

  int g,rep;

  // Set up gpi pointer for direct register access
  setup_io();

 /************************************************************************\
  * You are about to change the GPIO settings of your computer.          *
  * Mess this up and it will stop working!                               *
  * It might be a good idea to 'sync' before running this program        *
  * so at least you still have your code changes written to the SD-card! *
 \************************************************************************/

#define DIR (0)
#define STEP (1)

  INP_GPIO(DIR); // must use INP_GPIO before we can use OUT_GPIO
  OUT_GPIO(DIR);
  INP_GPIO(STEP);
  OUT_GPIO(STEP);

  GPIO_SET = 1 << DIR;

  int state = 0;
        while(1) {
                /* wait until next shot */
                clock_nanosleep(CLOCK_MONOTONIC, TIMER_ABSTIME, &t, NULL);

                /* do the stuff */
    if (state) {
      GPIO_SET = 1 << STEP;
    } else {
      GPIO_CLR = 1 << STEP;
    }
    state = !state;

                /* calculate next shot */
                t.tv_nsec += interval;

                while (t.tv_nsec >= NSEC_PER_SEC) {
                       t.tv_nsec -= NSEC_PER_SEC;
                        t.tv_sec++;
                }
        }
}

//
// Set up a memory regions to access GPIO
//
void setup_io()
{
   /* open /dev/mem */
   if ((mem_fd = open("/dev/mem", O_RDWR|O_SYNC) ) < 0) {
      printf("can't open /dev/mem \n");
      exit(-1);
   }

   /* mmap GPIO */
   gpio_map = mmap(
      NULL,             //Any adddress in our space will do
      BLOCK_SIZE,       //Map length
      PROT_READ|PROT_WRITE,// Enable reading & writing to mapped memory
      MAP_SHARED,       //Shared with other processes
      mem_fd,           //File to map
      GPIO_BASE         //Offset to GPIO peripheral
   );

   close(mem_fd); //No need to keep mem_fd open after mmap

   if (gpio_map == MAP_FAILED) {
      printf("mmap error %d\n", (int)gpio_map);//errno also set!
      exit(-1);
   }

   // Always use volatile pointer!
   gpio = (volatile unsigned *)gpio_map;


} // setup_io

9 comments:

Unknown said...

Hello,

I'am glad to see your post, like you I was looking for a while on internet to find how can I use my RPI to make my CNC.

Can you add more details? like How do you run it? How do you "bridge" LinuxCNC with the RPI.

Best regards,

Unknown said...

Hi there, have you got any updates on your tests?
I've picked up your idea,I have a similar set, and soldered a db-25 to a ribbon cable and tried to test the comms betwen the rpi and a driver trough the interface board. That way I could use my hardware setup, with parallel interface, and plug it to the Pi without changing anything.
Electronicaly I think it's possible, and you get 3,3v turn in to 5v to control the drivers, but I got stuck in the C language programing...

Unknown said...

Hello, great job, could you publish your schematics how you connect the raspberry Pi to the nema motor driver, Merci

Anonymous said...

I don't remember exactly how I did it, but I believe that the ENBL-, ENBL+, DIR-, PUL- all got connected to the raspi's ground, and then PUL+ and DIR+ went to the GPIO pins.

maker said...

Hi!, thanks forthe code. But I couldn't complile it, seems something is missing regarding "amp" variable, is there something missing? Thanks!

Lunkwill said...

"&amp;" is a way of writing "&" in HTML, and blogger got confused going back and forth between plain and HTML editing. Should be fixed now.

Unknown said...

Hello, do you use only​ Raspi with its GPIO's o with Arduino? Do you can load a g-code with much operations or only 1 instruction?

Lunkwill said...

This project never got past the "proof of concept" stage, so you're probably better off with an Arduino.

Unknown said...

My proyect has less concept: https://www.youtube.com/watch?v=16uzCy-xsO0

Only want move the motors X and Y by GPIO with GCODE

I don't want use nothing (Arduino, RAMPS, ...or Raspi) if can use only a router with GPIO's.


But first I want use this proyect in Raspi and after adapt it to a Router with OpenWRT.

Very Thanks, and need help