Control a missile launcher with Nunchuk

Requirements

Setup your BeagleBone Black

Download kernel sources

First, create your working directory :

$ mkdir $HOME/bbb_nunchuk
$ cd $HOME/bbb_nunchuk

Then, clone the mainline Linux tree and fetch last stable version and build the kernel :

$ git clone https://github.com/RobertCNelson/bb-kernel
$ cd bb-kernel
$ git checkout origin/am33x-v4.1 -b tmp
$ ./build_kernel.sh

Serial communication

First, connect the Beagle to your USB-TTL Serial adapter. Connect the ground wire to the pin closest to the power supply connector (let’s call it pin 1), TX to the pin 4 and RX to the pin 5. Then, install picocom and add your user to dialout group as root and start serial communication :

$ sudo adduser $USER dialout
$ picocom -b 115200 /dev/ttyUSB0

Now, reset your board. Press a key quickly in the picocom terminal to stop the U-boot countdown and You should then see the U-Boot prompt

Setup Ethernet communication

The next step is to configure U-boot and your workstation to let your board download files, such as the kernel image and Device Tree Binary (DTB), using the TFTP protocol through an Ethernet cable. First, install TFTP server :

$ sudo apt-get install tftpd-hpa

The, configure your Ethernet iface via Network manager or via command-line :

$ nano /etc/network/interfaces

Choose your interface and write your configuration file, for this example, our interface is eth0 :

iface eth0 inet static
address 192.168.0.1
netmask 255.255.255.0

Now, it’s time to configure networking on U-Boot’s side. Back to the U-Boot command line, set the below environment variables:

setenv ipaddr 192.168.0.100
setenv serverip 192.168.0.1

And Save these settings to the eMMC storage on the board :

saveenv

It's time to test your TFTP server. Create a small text file in /var/lib/tftpboot :

$ touch /var/lib/tftpboot/textfile.txt
$ echo "Test" > /var/lib/tftpboot/textfile.txt

Then, from U-Boot, do:

tftp 0x81000000 textfile.txt

The tftp command should have downloaded the textfile.txt file from your development workstation into the board’s memory at location 0x81000000 (this location is part of the board DRAM). You can verify that the download was successful by dumping the contents of the memory:

md 0x81000000

We are now ready to load and boot a Linux Kernel.

Root File System

First, you have to download a root file system :

$ wget -c https://rcn-ee.com/rootfs/eewiki/barefs/debian-8.2-bare-armhf-2015-09-07.tar.xz

Then, extract FS from archive and rename it to nfs :

$ tar -xvf debian-8.2-minimal-armhf-2015-09-07.tar.xz
$ tar -xvf debian-8.2-minimal-armhf-2015-09-07/armhf-rootfs-debian-jessie.tar
$ mkdir nfs
$ cp -fr debian-8.2-minimal-armhf-2015-09-07/armhf-rootfs-debian-jessie nfs

Your Root File System is ready !

Device Tree for the Nunchuk support

By default, second I2C interface is disabled into Kernel from Robert Nelson but we want use it for our Nunchuk. Moreover, we have to write a little driver for our Nunchuk ! First, you have to edit arch/arm/boot/dts/am335x-boneblack.dts file and add few lines :

/* Do not add those lines. */
/ {
        model = "TI AM335x BeagleBone Black";
        compatible = "ti,am335x-bone-black", "ti,am335x-bone", "ti,am33xx";
};
/* Copy &i2c1.... after this block */
 
&i2c1 {
          pinctrl-names = "default";
          pinctrl-0 = <&i2c1_pins>;
          status = "okay";
          clock-frequency = <0x186a0>;
          nunchuk: nunchuk@52 {
                      model = "nunchuk";
                      compatible = "nintendo,nunchuk";
                      reg = <0x52>;
          };
          i2c1_pins: pinmux_i2c1_pins {
                    pinctrl-single,pins = <
                      0x158 (PIN_INPUT_PULLUP | MUX_MODE2) 
                      0x15c (PIN_INPUT_PULLUP | MUX_MODE2) 
                                        >;
        };
 
};

In this part, we tell to kernel that we want activate the second I2C and that there is a device named “nunchuk” who is registered as 0x52 on the I2C bus. It seems everything is okay, now you can compile your kernel !

Kernel compiling and booting

In this part, we will see how to compile a linux kernel for an embedded system like BeagleBone Black. Go back to $HOME/bbb_nunchuk/bb-kernel/KERNEL and install a cross-compiling toolchain from Linaro, a very popular source for ARM toolchains :

$ sudo apt-get install gcc-arm-linux-gnueabi

Now, we will edit .config file. This file describes how your kernel will be compiled. You can set several options according to your needs. For our main mission, we have to specify some options. Edit the .config file, search keys and edit their values :

CONFIG_ROOT_NFS=y
CONFIG_INPUT_POLLDEV=y
CONFIG_INPUT_EVDEV=y

Now, we will compile your kernel :

make ARCH=arm CROSS_COMPILE=arm-linux-gnueabi- -j4

Once compilation is finished, copy zImage and am335x-boneblack.dtb to /var/lib/tftpboot :

$ cp arch/arm/boot/zImage /var/lib/tftpboot/
$ cp arch/arm/boot/dts/am335x-boneblack.dtb /var/lib/tftpboot/

Now, you will setup the NFS server, first, install the nfs-kernel-server package. Once installed, edit the /etc/exports as root to add the folowing line :

/home/<user>/bbb_nunchuk/nfs/modules/nfsroot
*(rw,no_root_squash,no_subtree_check)

Make sure that the path and the options are on the same line. Also make sure that there is no space between the * and the NFS options, otherwise default options will be used for this IP address, causing your root filesystem to be read-only. Then, restart the NFS server :

$ sudo /etc/init.d/nfs-kernel-server restart

First, boot the board to the U-Boot prompt. Before booting the kernel, we need to tell it which console to use and that the root filesystem should be mounted over NFS, by setting some kernel parameters.

Do this by setting U-boot’s bootargs environment variable (all in just one line, pay attention to the O character, like ”OMAP”, in ttyO0):

setenv bootargs root=/dev/nfs rw ip=192.168.0.100 console=ttyO0,115200n8
nfsroot=192.168.0.1:/home/<user>/bbb_nunchuk/nfs/modules/nfsroot
saveenv

Now, download the kernel image and the device tree blob through tftp :

tftp 0x81000000 zImage
tftp 0x82000000 am335x-boneblack.dtb

And now, boot your kernel:

bootz 0x81000000 - 0x82000000

If everything goes right, you should reach a login prompt !

Write Nunchuk driver

Let's write Nunchuk driver as module ! First, you have to go to $HOME/bb-kernel/nfs/home/debian/ and create a directory named nunchuk then go to nunchuk. First, create a Makefile and a file named nunchuk.c. Fill your Makefile with folowings :

ifneq ($(KERNELRELEASE),)
obj-m := nunchuk.o
else
KDIR := $(HOME)/bbb_nunchuk/bb-kernel/KERNEL
all:
        $(MAKE) -C $(KDIR) M=$$PWD
endif

Let's write our driver ! Edit your nunchuk.c file :

nunchuk.c
#include <linux/init.h>
#include <linux/module.h>
#include <linux/i2c.h>
#include <linux/slab.h>
#include <linux/delay.h>
#include <linux/input.h>
#include <linux/input-polldev.h>
#include <linux/platform_device.h>
 
struct				nunchuk_dev {
  struct input_polled_dev	*polled_input;
  struct i2c_client		*i2c_client;
};
 
static int	handle_error(int ret)
{
  printk("Nunchuk Error handled : %d", ret);
  return (0);
}
 
static char	*nunchuk_read_registers(struct i2c_client *client, int size)
{
  char		buff[6] = {0x00};
  int		ret = 0;
  char		*buffi;
 
  buffi = kmalloc(size, GFP_KERNEL);
  mdelay(10);
  if ((ret = i2c_master_send(client, buff, 1)) < 0) {
    handle_error(ret);
    return (NULL);
  }
  mdelay(10);
  if ((ret = i2c_master_recv(client, buffi, 6)) < 0) {
    handle_error(ret);
    return (NULL);
  }
  return (buffi);
}
 
void			nunchuk_poll(struct input_polled_dev *dev)
{
  int			cPressed = 0, zPressed = 0;
  char			*buff_ret;
  struct nunchuk_dev	*nunchuk;
  char			coordX, coordY;
  char			ax, ay, az;
 
  nunchuk = dev->private;
  /* Read data provided by nunchuk */
  buff_ret = nunchuk_read_registers(nunchuk->i2c_client, 6);
  zPressed = !((buff_ret[5] & 0x1));
  cPressed = !((buff_ret[5] & 0x2) >> 1);
  coordX = buff_ret[0];
  coordY = buff_ret[1];
  ax = (buff_ret[2] << 2) & ((buff_ret[5] >> 2) & 0x3);
  ay = (buff_ret[3] << 2) & ((buff_ret[5] >> 4) & 0x3);
  az = (buff_ret[4] << 2) & ((buff_ret[5] >> 6) & 0x3);
 
  /* Notify new event */
  input_event(nunchuk->polled_input->input,
	      EV_KEY, BTN_Z, zPressed);
  input_event(nunchuk->polled_input->input,
	      EV_KEY, BTN_C, cPressed);
 
  input_event(nunchuk->polled_input->input,
	      EV_ABS, ABS_X, coordX);
  input_event(nunchuk->polled_input->input,
	      EV_ABS, ABS_Y, coordY);
  input_event(nunchuk->polled_input->input,
	      EV_ABS, ABS_RX, ax);
  input_event(nunchuk->polled_input->input, 
	      EV_ABS, ABS_RY, ax);
  input_event(nunchuk->polled_input->input, 
	      EV_ABS, ABS_RZ, ay);
 
  input_sync(nunchuk->polled_input->input);
  kfree(buff_ret);
}
 
static int			nunchuk_probe(struct i2c_client *client, const struct i2c_device_id *id)
{
  int				ret = 0;
  char				buff[] = {0xF0, 0x55};
  struct input_polled_dev	*polled_input;
  struct input_dev		*input;
  struct nunchuk_dev		*nunchuk;
 
 
  polled_input = input_allocate_polled_device();
  if (!(nunchuk = devm_kzalloc(&client->dev, sizeof(struct nunchuk_dev), GFP_KERNEL))) {
    dev_err(&client->dev, "Failed to allocate memory\n");
    return (-ENOMEM);
  }
  if ((ret = i2c_master_send(client, buff, 2)) < 0) //Send initialization frame to device
    return (handle_error(ret));
  udelay(1000);
  /* Second initialization */
  buff[0] = 0xFB;
  buff[1] = 0x00;
  if ((ret = i2c_master_send(client, buff, 2)) < 0)
    return (handle_error(ret));
 
  nunchuk->i2c_client = client;
  nunchuk->polled_input = polled_input;
  polled_input->private = nunchuk;
  i2c_set_clientdata(client, nunchuk);
  input = polled_input->input;
  input->dev.parent = &(client->dev);
 
  input->name = "Wii Nunchuk";
  input->id.bustype = BUS_I2C;
  /* Set input events for buttons */
  set_bit(EV_KEY, input->evbit); 
  set_bit(BTN_C, input->keybit);
  set_bit(BTN_Z, input->keybit);
  /* Set input events for joystick */
  set_bit(EV_ABS, input->evbit);
  set_bit(ABS_X, input->keybit);
  set_bit(ABS_Y, input->keybit);
  set_bit(ABS_RX, input->keybit);
  set_bit(ABS_RY, input->keybit);
  set_bit(ABS_RZ, input->keybit);
 
  input_set_abs_params(input, ABS_X, 0, 255, 2, 4);
  input_set_abs_params(input, ABS_Y, 0, 255, 2, 4);
  input_set_abs_params(input, ABS_RX, 0, 0x3ff, 4, 8);
  input_set_abs_params(input, ABS_RY, 0, 0x3ff, 4, 8);
  input_set_abs_params(input, ABS_RZ, 0, 0x3ff, 4, 8);
 
  polled_input->poll = nunchuk_poll;
  polled_input->poll_interval = 50;
 
  if ((ret = input_register_polled_device(polled_input))) {
    pr_info("Error while registering polled_input.../n");
    input_free_polled_device(polled_input);
    return (0);
  }
  return (0);
}
 
static int nunchuk_remove(struct i2c_client *client)
{
  struct nunchuk_dev	*nunchuk;
 
  nunchuk = i2c_get_clientdata(client);
  input_unregister_polled_device(nunchuk->polled_input);
  input_free_polled_device(nunchuk->polled_input);
  pr_info("Nunchuk Module removed\n");
  return (0);
}
 
static const struct i2c_device_id nunchuk_id[] = {
  { "nunchuk", 0 },
  { }
};
 
MODULE_DEVICE_TABLE(i2c, nunchuk_id);
 
static const struct of_device_id nunchuk_dt_ids[] = {
  { .compatible = "nitendo,nunchuk", }, //Allow to identify our nunchuk by it compatible string
  { }
};
MODULE_DEVICE_TABLE(of, nunchuk_dt_ids);
 
static struct i2c_driver nunchuk_driver = {
  .probe = nunchuk_probe, //Handler for device detection
  .remove = nunchuk_remove, //Handler for module deletion
  .id_table = nunchuk_id,
  .driver = {
    .name = "nunchuk",
    .owner = THIS_MODULE,
    .of_match_table = of_match_ptr(nunchuk_dt_ids),
  },
};
 
module_i2c_driver(nunchuk_driver); //Register our driver Kernel side
MODULE_LICENSE("GPL");

Then, compile your module :

$ make ARCH=arm CROSS_COMPILE=arm-linux-gnueabi-

Go to BBB side and go to /home/debian/nunchuk/ and insert your module :

# insmod nunchuk.ko

The module tells you how the nunchuk is identified (e.g /etc/input/event0).

User Space program

Now, we will write a little program to read our input provided by our driver.

LibUSB

To communicate with our missile launcher, we have to use libusb. First, download latest libusb, on your workstation, go to $HOME/bbb_nunchuk/bb-kernel/nfs/home/debian/nunchuk :

$ wget -c http://netix.dl.sourceforge.net/project/libusb/libusb-1.0/libusb-1.0.9/libusb-1.0.9.tar.bz2
$ tar -jxvf libusb-1.0.9.tar.bz2

Once archive is extracted go to libusb-1.0.9/libusb and make the object files :

$ make ARCH=arm CROSS_COMPILE=arm-linux-gnueabi-

The program

Now, we can write the program :

nunchukbin.c
#include <inttypes.h>
#include <math.h>
#include <stdio.h>
#include <sys/time.h>
#include <string.h>
#include <unistd.h>
#include <fcntl.h>
#include <errno.h>
#include <termios.h>
#include <linux/input.h>
#include <libusb.h>
 
#define _POSIX_C_SOURCE			200809L
 
#define ML_VENDOR_ID			0x0416
#define ML_DEVICE_ID			0x9391
 
#define ML_ACTION_FIRE			0x10
#define ML_ACTION_MOVE_DOWN		0x1
#define ML_ACTION_MOVE_LEFT		0x8
#define ML_ACTION_MOVE_RIGHT		0x4
#define ML_ACTION_MOVE_UP		0x2
#define ML_ACTION_STOP			0x0
 
#define ML_TIME_FIRING			4300
#define ML_X_DEADZONE			15
#define ML_Y_DEADZONE			15
 
static struct libusb_device_handle	*devh;
 
int		mlbin_init_usb(void)
{
  libusb_device	**list;
  libusb_device *device = NULL;
  int		count, ret, i;
 
  ret = libusb_init(NULL);
 
  if (ret < 0) {
    perror("");
    printf("Couldn't initialize libusb => %d.\n", ret);
    goto error;
  }
 
  count = libusb_get_device_list(NULL, &list);
  if (count < 0) {
    printf("Couldn't get device list\n");
    goto list_error;
  }
 
  for (i = 0; i < count; i++) { //Browse USB Devices list to find Missile Launcher
    struct libusb_device_descriptor desc;
 
    device = list[i];
    libusb_get_device_descriptor(device, &desc);
    printf("Found a new device : %x:%x\n",
	   desc.idVendor, desc.idProduct);
    if (desc.idVendor == ML_VENDOR_ID &&
	desc.idProduct == ML_DEVICE_ID) //Check Missile Launcher identity
      break;
    device = NULL;
  }
 
  if (!device) {
    printf("Couldn't find the device\n");
    goto not_found_error;
  }
 
  ret = libusb_open(device, &devh);
  if (ret) {
    printf("Couldn't open device: %d\n", ret);
    goto open_dev_error;
  }
 
  libusb_detach_kernel_driver(devh, 0);
  ret = libusb_claim_interface(devh, 0);
  if (ret < 0) {
    printf("Couldn't claim the interface : %d.\n", ret);
    goto if_error;
  }
 
  libusb_free_device_list(list, count);
  printf("Interface setup.\n");
  return 0;
 
 if_error:
  libusb_close(devh);
 detach_error:
 open_dev_error:
 not_found_error:
  libusb_free_device_list(list, count);
 list_error:
  libusb_exit(NULL);
 error:
  exit(1);
}
 
int	mlbin_free_usb(void)
{
  libusb_release_interface(devh, 0);
  libusb_close(devh);
  libusb_exit(NULL);
  return 0;
}
 
void	send_data(unsigned char action)
{
  unsigned char data[] = {0x5f, action, 0xe0, 0xff, 0xfe};
 
  libusb_control_transfer(devh, 0x21, 0x09, 0, 0, data, 5, 300);
}
 
int			startup(int ac, char **argv, int *fd, int *calibration, int *verbose)
{
  int			i = 1;
 
  if (ac < 2) {
    printf("Usage : %s <Input iface> [--calibration] [-v]\n", argv[0]);
    return (0);
  }
  if ((*fd = open(argv[1], O_RDWR | O_NOCTTY | O_NDELAY)) == -1) {
    printf("Unable to open %s\n", argv[1]);
    return (0);
  }
  while (i < ac) {
    if (!strcmp(argv[i], "--calibration"))
      *calibration = 1;
    else if (!strcmp(argv[i], "-v"))
      *verbose = 1;
    i++;
  }
  if (*calibration) {
    printf("Mode : Calibration\n");
    printf("Press Nunchuk C button for start and exactly repress C when you hear second click\n");
  } else {
    printf("Mode : Game\n");
    printf("Change ML orientation with joystick\nLaunch missile one by one by clicking C button\nLaunch every missiles by pressing Z button\n");
  }
  return (1);
}
 
void			calibration_mode(struct input_event *evnt, int *fire, int *calibration)
{
  //Calibration allows to synchronise missile launcher mechanism
  if (evnt->code == 306 && evnt->value && !(*fire)) {
    send_data(ML_ACTION_FIRE);
    *fire = 1;
    printf("Starting calibration ...\n");
  } else if (evnt->code == 306 && evnt->value && *fire) {
    send_data(ML_ACTION_STOP);
    *fire = 0;
    printf("Calibration finished ...\n");
    *calibration = 0;
  }
}
 
void			game_mode(struct input_event evnt, int *fire, struct timeval *tv,
				  int *fire_time, int *plus_time)
{
  //Check every actions provided by input iface
  if (evnt.code == 306 && evnt.value) { //Z Button pressed
    if (*fire) {
      send_data(ML_ACTION_STOP);
      *fire = 0;
    } else {
      gettimeofday(tv, NULL);
      *fire_time = (tv->tv_sec) * 1000 + (tv->tv_usec) / 1000; //Get current time
      *plus_time = ML_TIME_FIRING; //Firing while 4,2s
      send_data(ML_ACTION_FIRE);
      *fire = 1;
    }
  } else if (evnt.code == 309 && evnt.value) { //C Button pressed
    if (*fire) {
      send_data(ML_ACTION_STOP);
      *fire = 0;
    } else {
      gettimeofday(tv, NULL);
      *fire_time = (tv->tv_sec) * 1000 + (tv->tv_usec) / 1000;
      send_data(ML_ACTION_FIRE);
      *plus_time = ML_TIME_FIRING * 3; //Same as one firing but while 12,6s for 3 missiles
      *fire = 1;
    }	
  } else if (evnt.type == 3 && !evnt.code) { //X-axis Joystick moved
    if (evnt.value < (127 - ML_X_DEADZONE))
      send_data(ML_ACTION_MOVE_LEFT);
    else if (evnt.value > (127 + ML_X_DEADZONE))
      send_data(ML_ACTION_MOVE_RIGHT);
    else 
      send_data(ML_ACTION_STOP);
  } else if (evnt.type == 3 && evnt.code) { //Y-axis Joystick moved
    if (evnt.value < (127 - ML_Y_DEADZONE))
      send_data(ML_ACTION_MOVE_DOWN);
    else if (evnt.value > (127 + ML_Y_DEADZONE))
      send_data(ML_ACTION_MOVE_UP);
    else
      send_data(ML_ACTION_STOP);
  }
}
 
void			launch_app(int fd, int calibration, int verbose)
{
  struct input_event	evnt;
  int			fire = 0, n;
  int			fire_time = 0;
  struct timeval	tv;
  int			plus_time = 0;
 
  while (1) {
    gettimeofday(&tv, NULL);
    //Stop fire when ML fired one missile, based on time, 4,2 sec for one missile
    if (fire && ((tv.tv_sec) * 1000 + (tv.tv_usec) / 1000) >= fire_time + plus_time) {
      send_data(ML_ACTION_STOP);
      fire = 0;
    }
    n = read(fd, &evnt, sizeof(evnt)); //Read data received by ML
    if (n > 0) {
      if (calibration)
	calibration_mode(&evnt, &fire, &calibration);
      else {
	game_mode(evnt, &fire, &tv, &fire_time, &plus_time);
	if (verbose) //Debug Verbose
	  printf("Readed : Type : %d - Code : %d - Value : %d\n", 
		 evnt.type, evnt.code, evnt.value);
      }  
    }
  }
}
 
int			main(int ac, char *argv[])
{
  int			fd;
  int			calibration = 0, verbose = 0;
 
  //Check usage, set calibration and verbose flags and open input device
  if (!startup(ac, argv, &fd, &calibration, &verbose)) 
    return (0);
 
  mlbin_init_usb(); //LibUSB initialisation and search device
  send_data(ML_ACTION_STOP);
 
  launch_app(fd, calibration, verbose); //Launch main loop
 
  mlbin_free_usb();
  close(fd);
  return (0);
}

Then, compile your program and link it with libusb object files :

$ arm-linux-gnueabi-gcc --static nunchukbin.c ./libusb-1.0.9/libusb/*.o -I ./libusb-1.0.9/libusb -lpthread -lrt -o nunchuk

Now, you can go to BBB side and test your new program :

./nunchuk /dev/input/event1
beagleboneblack/nuncuhk.txt · Last modified: 2015/11/12 04:48 by jbertomeu
Recent changes RSS feed Creative Commons License Donate Minima Template by Wikidesign Driven by DokuWiki