1   Assignment

The goal of the semestral work is to create a digital motor controller. Your program will control the position of the motor according to the set-point given by the position of another motor, moved by hand (steer-by-wire). The set-point will be transferred between the two motor controllers using UDP messages. The actual state of the controller and its history will be published as live graphs over the HTTP protocol.

1.1   Instructions

  • Work in couples.

    Create a team by selecting your teammate in the BRUTE system.

  • Use version control system GIT.

    We expect commits from both of you in the repository. It is advised to host repository at GitLab FEL, for more information see this page.

  • Implement interrupt handler for reading the motor position.

    Motor position can be determined by decoding the IRC sensor signals. These signals are called A, B (phase-shifted A) and IRQ. The last signal can generate interrupts each time one of the A / B signals changes.

  • Implement a function for rotating the motor.

    Motor can be rotated by generating PWM signals on board output pins. PWM signal can be generated by writing to FPGA registers (see below).

  • Implement a PID controller to control the shaft position.

    You can read more about PID controller at Wikipedia. However, for your semestral work, you don't have to use the complete PID controller. Even the basic P controller is good enough.

  • Implement UDP communication from one board to another¹.

    For network communication between the boards, your code from previous assignment can be reused. It is recommended to use standard BSD sockets API.

  • Implement a simple web server for displaying live graphs of the motor¹.

    Live graphs served from the web server should display at least:

    • actual motor position (absolute value),
    • requested position (absolute value), and
    • current PWM duty cycle (in range –100%, +100%).

    The graphs should show at least 2 seconds of history with time resolution ≤ 5 ms.

Note ¹: Instructions marked with ¹ are not mandatory for a “low-point” solution.

Graphical representation of the instructions

2   Assessment

Semestral work assessment comprises the following independently assessed parts:

2.1   Motor control program (max. 18 points)

Item Points
IRC reading through interrupts (proper implementation without losing position) 1*
Hardware generated PWM at 20 kHz 0.5*
No busy-waiting, properly selected sampling period for motor 0.5*
P(ID) or better controller (no ad-hoc solution) 1*
Quality of regulation (no oscillations, fast response, minimal steady state error, ...) 0 – 2
Set-point specification (choose one of the following): *
— From second motor through UDP communication 6
— From terminal via keyboard 1
Controller "debugging" (choose one of the following): *
— Web page with live graphs 7

— ASCII "bargraph" of action value (PWM width). Something like:

-0.5 .....=====|..........
1

Note: Rows marked by * represent the minimum for “zápočet”.

2.2   Use of version control system (max. 4 points)

You have to use Git, a distributed version control system. See the instructions for using Git on our server. There will be two check dates:

  1. December 2, 2020 (for Wednesday labs) / December 3, 2020 (for Thursday labs), and
  2. when submitting the work.

We will evaluate the items from the table below.

Item (evaluation date) Points
Project is maintained using Git (1,2) 0*
At least one commit every week (1,2) 1
Commit messages accurately describe the content of the commit (2) 0.5
Content of each commit is reasonable in its size (i.e., minimal) (2) 0.5
No generated files (e.g. .o) are added in the repository (2) 1

It is required that both persons from the team have commits in the repository.

Note: Rows marked by * represent the minimum for “zápočet”.

2.3   Programmer's documentation (max. 3 points)

  • The goal is to learn tools that generate the documentation automatically from the source code (e.g. Doxygen or Sphinx + Breathe), not to write novels or learn how to create nice documents in LaTeX.
  • Do not modify the generated documentation, e.g., to insert figures. Have all the documentation, including the text description and references to figures, in your source code.
  • Look at example documentation created with Doxygen and the corresponding source code. Text-based part of the documentation is contained in file CANR_16X.H.

The documentation should contain:

Item Points
Short textual description of the application in the introduction (or on the main HTML page). 0.5
Instructions for compiling and running your application. 0.5
Screenshot of your web-based (or text-based) user interface 0.5
Data-Flow Diagram of your application i.e. how individual components (e.g. IRQ handler, controller, web-server) exchange data. Note: You need to draw this picture yourself; Doxygen cannot generate it automatically. 1
Description of global functions and variables (created by you) including the description of function parameters and return values. 0.5

3   Remote access

3.1   Web camera

Click at the image below to see live video stream of the motors. If things work correctly, you should see the LED blinking with period of 2 seconds.

Web camera showing all the motors.

3.2   Access to on-board web server

The following command connects to the board and redirects local TCP port 8080 to port 80 on the board:

ssh -L 17476:/run/psr-hw/wrproxy -L 8080:/run/psr-hw/www -o "ExitOnForwardFailure yes" «login»@rtime.felk.cvut.cz

At the beginning, the command prints which motor you're connected to:

Welcome to boardproxy
Connecting to board mzapo14
IP address: 10.35.95.63, motor 14
sterm: Connected.
sterm: Use '<Enter>~.' sequence to exit.

U-Boot SPL 2017.01-03169-gf9643f7-dirty (Feb 12 2017 - 23:24:47)
...

If you don't want to type this long command all over again, add the following to ~/.ssh/config and run only ssh mzpsr:

Host mzpsr
     User YOUR_LOGIN
     Hostname rtime.felk.cvut.cz
     LocalForward 17476 /run/psr-hw/wrproxy
     LocalForward 8080  /run/psr-hw/www
     ExitOnForwardFailure yes

3.3   Moving the motor “by hand”

The assignment expects that you will move one motor by hand. With remote boards, this is hardly possible. To move the motor when connecting remotely, we prepared a small VxWorks module, which can be invoked via VxWorks kernel shell command and moves the motor. It should be quite easy for you to program such a module yourself, but to make the start easier, we give it to you, but only in binary form. Use the module as follows:

  1. Download the module mot.out and place it somewhere on your disk.

  2. Create a download configuration in your Wind River Workbench project that will download the module to the board and execute it each time you connect to the board. See the screenshot below.

    Download configuration for mot.out
  3. From the shell, run mot <POSITION>, where <POSITION> is a target position for motor movement:

    -> mot 200
    starting position: 1
    final position: 195
    value = 0 = 0x0
    

    The motor moves approximately to the specified position.

To ensure that the module will not conflict with your programs, it accesses only the FPGA registers (writes to PWM registers and reads IRC registers). It does not create any kernel objects (tasks), does not use interrupts and does not change any kernel settings or hardware settings (timer frequencies etc.).

4   Hints

4.1   Things to avoid

(you should know why from lectures)

  • Calling blocking functions (e.g. printf()) in interrupt handlers.
  • Using floating point operations in interrupt handlers.

4.2   Working with floating point numbers

You must include the VX_FP_TASK option when creating a task that does any of the following:

  • Performs floating-point operations.
  • Calls any function that returns a floating-point value.
  • Calls any function that takes a floating-point value as an argument.

For example:

tid = taskSpawn ("tMyTask", 90, VX_FP_TASK, 20000, myFunc, 2387, 0, 0, 0, 0, 0, 0, 0, 0, 0);

Some routines perform floating-point operations internally. The VxWorks documentation for each of these routines clearly states the need to use the VX_FP_TASK option.

4.3   Resources

Details about connection between the motor and the board

4.4   FPGA registers

This section describes the hardware registers that you can use to control the motors. These registers are implemented in the FPGA, which is a part of the Zynq chip. The motor can be connected to PMOD1 or to PMOD2 connectors. The set of registers for both connectors is the same, only the base address is different. Base addresses are:

  • 0x43c20000 for the PMOD1 connector
  • 0x43c30000 for the PMOD2 connector

The address of the register is the sum of the above base address and an offset from tables below.

Control Register (CR) [RW]

offset 0x0000

bit 6

PWM_ENABLE – PWM Generator Enable

  • when 0, PWM outputs are controlled by bits PWM_F_DIRECT and PWM_R_DIRECT
  • when 1, PWM is controlled by PWM generator
bit 5 PWM_R_DIRECT – Direct Control of PWM R Output
bit 4 PWM_F_DIRECT – Direct Control of PWM F Output

Status Register (SR) [RO]

offset 0x0004

bit 10 IRC_IRQ_MON – actual value of IRC IRQ signal
bit 9 IRC_B_MON – actual value of IRC input B
bit 8 IRC_A_MON – actual value of IRC input A

PWM Period Setup Register (PWM_PERIOD) [RW]

offset 0x0008

bit 29..0 defines a period of PWM generated waveform. Basic unit is 10 ns, (clock 100 MHz). Maximum possible period is about 5 sec. The register can be rewritten asynchronously even when PWM is enabled; If the new period is shorter than actual ongoing one the period is shortened immediately.

PWM Duty Control Register (PWM_DUTY) [RW]

offset 0x000C

bit 31

DUTY_DIR_R – request negative polarity of output voltage

  • when 1 then output PWM R is asserted on start of next PWM period and is hold active for clock count defined by DUTY field
  • when 0 then PWM R output is always low
bit 30

DUTY_DIR_F – request positive polarity of output voltage

  • when 1 then output PWM F is asserted on start of next PWM period and is hold active for clock count defined by DUTY field
  • when 0 then PWM F output is always low)
bit 29..0 DUTY – number of 100 MHz clock intervals to hold output active in every period. If set during ongoing period before previous DUTY limit resets outputs then new value is applied immediately.

4.5   IRC sensor IRQ handling

To decode the IRC signal reliably, it is necessary to handle the IRQs generated by motor. For that, one needs not only to use the registers described above, but also configure GPIO interrupts, because the FPGA delivers motor interrupts via GPIO. The code below shows how to connect an interrupt service routine (irc_isr) to the hardware IRQ generated by the motor hardware. It uses some definitions from xlnx_zynq7k.h header file, which is a part of the BSP. To successfully include this file, it is necessary to modify search path in project properties "Build Properties->Paths". Add there a line containing:

-I$(WIND_BASE)/target/config/xlnx_zynq7k
#include <taskLib.h>
#include <stdio.h>
#include <kernelLib.h>
#include <semLib.h>
#include <intLib.h>
#include <iv.h>

#include <xlnx_zynq7k.h>

#define REGISTER(base, offs) (*((volatile UINT32 *)((base) + (offs))))
#define BIT(i) ((1) << (i))


// MOTOR macros
// See section FPGA registers for more information.

#define PMOD1_BASE 0x43c20000
#define PMOD2_BASE 0x43c30000

#define MOTOR_BASE PMOD1_BASE

// GPIO register definitions
// See Zynq-7000 Technical Reference Manual for more information
//     Section: 14.2.4 Interrupt Function (pg. 391, pg. 1348).

// Pin on GPIO selected for interrupt
// Note: Each bit in a register refers to one pin. Setting some bit to `1`
//       also means which pin is selected.
#define MOTOR_IRQ_PIN BIT(2)

// Setting a bit in DIRM to `1` makes the corresponding pin behave as an output,
// for `0` as input.
// Note: So setting this to `MOTOR_IRQ_PIN` means, that this pin is an output
//       (which it is not so do not do it!).
//       This is similar with other GPIO/INT registers.
#define GPIO_DIRM         REGISTER(ZYNQ7K_GPIO_BASE, 0x00000284)

// Writing 1 to a bit enables IRQ from the corresponding pin.
#define GPIO_INT_ENABLE   REGISTER(ZYNQ7K_GPIO_BASE, 0x00000290)

// Writing 1 to a bit disables IRQ from the corresponding pin.
#define GPIO_INT_DISABLE  REGISTER(ZYNQ7K_GPIO_BASE, 0x00000294)

// Bits read as `1` mean that the interrupt event has occurred on a corresponding pin.
// Writing `1` clears the bits, writing `0` leaves the bits intact.
#define GPIO_INT_STATUS   REGISTER(ZYNQ7K_GPIO_BASE, 0x00000298)

// Setting TYPE to `0` makes interrupt level sensitive, `1` edge sensitive.
#define GPIO_INT_TYPE     REGISTER(ZYNQ7K_GPIO_BASE, 0x0000029c)

// Setting POLARITY to `0` makes interrupt active-low (falling edge),
//                     `1` active-high (raising edge).
#define GPIO_INT_POLARITY REGISTER(ZYNQ7K_GPIO_BASE, 0x000002a0)

// Setting ANY to `1` while TYPE is `1` makes interrupts act on both edges.
#define GPIO_INT_ANY      REGISTER(ZYNQ7K_GPIO_BASE, 0x000002a4)

// FPGA register definition
#define MOTOR_SR REGISTER(MOTOR_BASE, 0x4)
#define MOTOR_SR_IRC_A_MON 8
#define MOTOR_SR_IRC_B_MON 9


volatile unsigned irq_count;

void irc_isr(void)
{
        bool irc_a = (MOTOR_SR & BIT(MOTOR_SR_IRC_A_MON)) != 0;
        bool irc_b = (MOTOR_SR & BIT(MOTOR_SR_IRC_B_MON)) != 0;
        // ...
        irq_count++;
        GPIO_INT_STATUS = MOTOR_IRQ_PIN; /* clear the interrupt */
}

void irc_init(void)
{
        GPIO_INT_STATUS = MOTOR_IRQ_PIN; /* reset status */
        GPIO_DIRM = 0x0;                 /* set as input */
        GPIO_INT_TYPE = MOTOR_IRQ_PIN;   /* interrupt on edge */
        GPIO_INT_POLARITY = 0x0;         /* rising edge */
        GPIO_INT_ANY = 0x0;              /* ignore falling edge */
        GPIO_INT_ENABLE = MOTOR_IRQ_PIN; /* enable interrupt on MOTOR_IRQ pin */

        intConnect(INUM_TO_IVEC(INT_LVL_GPIO), irc_isr, 0);
        intEnable(INT_LVL_GPIO);         /* enable all GPIO interrupts */
}

void irc_cleanup(void)
{
        GPIO_INT_DISABLE = MOTOR_IRQ_PIN;

        intDisable(INT_LVL_GPIO);
        intDisconnect(INUM_TO_IVEC(INT_LVL_GPIO), irc_isr, 0);
}

/*
 * Entry point for DKM.
 */
void motor(void)
{
        TASK_ID st;

        irc_init();

        while (1) {
            printf("IRQ count: %u\n", irq_count);
            sleep(1);
        }

        irc_cleanup();
}

4.6   Web server

4.6.1   Skeleton of a simple web server

Example of communication between WWW browser and server

Web server skeleton:

#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <netdb.h>
#include <stdio.h>
#include <sockLib.h>
#include <string.h>

#define SERVER_PORT     80 /* Port 80 is reserved for HTTP protocol */
#define SERVER_MAX_CONNECTIONS  20

void www(int argc, char *argv[])
{
  int s;
  int newFd;
  struct sockaddr_in serverAddr;
  struct sockaddr_in clientAddr;
  int sockAddrSize;

  sockAddrSize = sizeof(struct sockaddr_in);
  bzero((char *) &serverAddr, sizeof(struct sockaddr_in));
  serverAddr.sin_family = AF_INET;
  serverAddr.sin_port = htons(SERVER_PORT);
  serverAddr.sin_addr.s_addr = htonl(INADDR_ANY);

  s=socket(AF_INET, SOCK_STREAM, 0);
  if (s<0)
  {
    printf("Error: www: socket(%d)\n", s);
    return;
  }


  if (bind(s, (struct sockaddr *) &serverAddr, sockAddrSize) == ERROR)
  {
    printf("Error: www: bind\n");
    return;
  }

  if (listen(s, SERVER_MAX_CONNECTIONS) == ERROR)
  {
    perror("www listen");
    close(s);
    return;
  }

  printf("www server running\n");

  while(1)
  {
    /* accept waits for somebody to connect and the returns a new file descriptor */
    if ((newFd = accept(s, (struct sockaddr *) &clientAddr, &sockAddrSize)) == ERROR)
    {
      perror("www accept");
      close(s);
      return;
    }

    /* The client connected from IP address inet_ntoa(clientAddr.sin_addr)
       and port ntohs(clientAddr.sin_port).

       Start a new task for each request. The task will parse the request
       and sends back the response.

       Don't forget to close newFd at the end */
  }
}

4.6.2   Using fdopen() to generate server responses

The easiest way to generate webserver responses is to use fdopen() function to "convert" the file descriptor returned by accept() to a FILE pointer. Then you can use all standard libc functions such as printf() to generate the response. For example:

FILE *f = fdopen(newFd, "w");
fprintf(f, "HTTP/1.0 200 OK\r\n\r\n");
fprintf(f, "Current time is %ld.\n", time(NULL));
fclose(f);

4.6.3   How to refresh a web page periodically?

There are many ways. One is to use javascript code in the onload attribute of the body element as in the example below. This example reloads the page approximately every 100 milliseconds and shows actual time in milliseconds.

<html>
  <head>
    <title>Test</title>
  </head>
  <body onload="setTimeout(function(){location.reload()}, 100);">
    <script>document.write(Date.now());</script>
  </body>
</html>

4.6.4   Drawing graphs as SVG

Look at this example for how to include SVG graphics in the html page.

Direct link to the SVG source

4.7   FAQ

Q: Why do not I receive any characters from keyboard in my program?

A: VxWorks shell "eats up" entered characters. Type exit to quit the shell.