CAN bus motor control

Hello,

I am using a UIROBOT UIM2040CM servo stepper motor. I would like to control using an arduino and CAN bus. Here's the documentation for the motor:
Manual_UIM342 2024.07.31 (1).pdf (1.9 MB)

I would like to please know the CanID for speed and how to send commands for speed as the manual is confusing and I haven't ever worked with CAN before.

#include <SPI.h>
#include <mcp_can.h>

// Define CAN pin connections
#define CAN_CS 10
MCP_CAN CAN(CAN_CS);  // Set CS to pin 10

void setup() {
    Serial.begin(9600);
    if (CAN.begin(MCP_ANY, 500000, MCP_8MHZ) == CAN_OK) {
        Serial.println("CAN bus initialized");
    } else {
        Serial.println("CAN bus initialization failed");
        while(1);
    }
    CAN.setMode(MCP_NORMAL);
}

void setMotorSpeed(int speed) {
    long unsigned int canId = 0x200;  // Replace with actual ID for speed control
    unsigned char msg[8];
    
    // Fill in the message with the correct command
    // Example: Assuming the first 2 bytes set the speed
    msg[0] = (speed >> 8) & 0xFF;  // High byte
    msg[1] = speed & 0xFF;         // Low byte

    CAN.sendMsgBuf(canId, 0, 8, msg);
}

void loop() {
    setMotorSpeed(500); // Set motor to 500 RPM
    delay(1000);        // Adjust as needed
}

Thanks,
Haneen

I tried this code with the MCP2515 module:

#include <CAN.h>

void setup() {
  Serial.begin(9600);
  while (!Serial);

  Serial.println("CAN Sender");

  // start the CAN bus at 500 kbps
  if (!CAN.begin(20E3)) {
    Serial.println("Starting CAN failed!");
    while (1);
  }
}

and I get this output:

" Entering Configuration Mode Successful!

Setting baud rate Failure...

CAN bus initialized "

Even when I change the baud rate, I am still getting baud rate failure.

I am using this schematic:

What crystal is on the CAN board and what baud are you trying to set? The slower the better when starting. With the power off measure the resistance between CAN-H and CAN-L, it should be about 60 Ohms.

Nice manual but we do not know which unit you have.

Crystal frequency is 8Mhz.

What do you mean by unit? My specific motor is UIM2040CM. I honestly dont know what baud rate to send, I just tried a range from 500-20 kbps. I would appreciate some guidance on this.

Whats weird is that the manual doesnt have info on this motor despite linking this manual on this motors page. I cant even find the CAN ID for this motor. I only see info for UIM342

You missed the "CM" in your post, that makes a big difference.

I can tell you to read a book but that is not what you want. I cannot give you an accurate answer as I cannot see, touch, feel, measure or anything else with your project. Your job is to relay back the needed information.

That is the number on the silver thing on the MPC2515 board.

Connect a ohm meter between these two points when everything is connected and the power is off.

I Have some meetings etc I will probably be back if a few hours.

I got 114 ohms.

It says YXC 16 OSDF

So turns out this is a 16 MHz crystal right?

image
Courtesy of chatgpt. Is this correct?

How do I know what data rate (baud) is the CAN bus?

You are missing a termination resistor. Open the CAN lines and measure each, they should be about 120 Ohms each, in parallel that gives you 60 which is the bus impedance. For the motor there should be a switch per the instructions. On the MPC2515 it is a jumper to the right of the CAN connector looking from the control strip. You show Apply Jumper, add one,
if it is missing, it just places a 120 Ohm resistor across the CAN bus.

This line "if (CAN.begin(MCP_ANY, 500000, MCP_8MHZ) == CAN_OK)" is assuming a 8 Mhz crystal, change the 8MHZ to 16MHZ and the baud should be correct. Currently it is 2X what you are setting it at.

Let us know how this works out.

Hi, @hansa11

Connect the gnd of the UNO to the gnd of the 2040CM.

Tom.. :smiley: :+1: :coffee: :australia:

In addition to the other replies fielded here, I thought I would try to help. I've used CAN bus a lot, even professionally. I'm no programmer (I'm an electrical engineer), so take the bit arithmetic with a grain of salt.

User gilshultz is correct when he said that when you measure across CAN HI and CAN LO you need 60 Ohms. Either you need to add a jumper between the two jumper pins on the CAN board, or if you've already done that, find where on the motor you need to do that. If you have two CAN converter boards, I would start by getting them to talk to each other (either on separate processors or on the same one- you can even use the same SPI bus as the converters have built in buffers. This will give you a good idea of what you need to do (especially on the hardware level) to get CAN working. Remember that each CAN converter board needs a jumper (one on either side of the CAN bus)! Once CAN is working, it's great, but getting it working I've found is a pain in the butt.

After you have communication between two boards down, you need to look at coding. I'm not going to lie, you chose a very hard motor to start off with. The CAN protocol for it is hard and you're probably going to fail a lot, but that's okay! That's how you learn.

First, I assume you're using Cory J. Fowler's CAN library (it's a very good library). You can download it on GitHub if you don't already have it. The datasheet doesn't explicitly say what the baud rate is, but I'd guess 1Mbps since on page 22 that's the first example they give. Don't worry about the TQ (time quanta) as the library takes care of that for you. Since I saw you have a 16MHz crystal, your initialization code would look like this:

if(CAN.begin(MCP_ANY, CAN_1000KBPS, MCP_16MHZ) == CAN_OK)
        Serial.println("CAN bus initialized");
else 
        Serial.println("CAN bus initialization failed");

CAN.setMode(MCP_NORMAL);

I should add that I took this code from a project I'm working on with an ESP32S3, not an Arduino, so the Arduino format might be slightly different, but it should be fundamentally the same.

Now, for the hard stuff. The motor uses extended CAN ID's, but it uses the data link layer to split that extended ID into its two separate parts. The normal CAN ID is 11 bits, and the extended is 29 bits. The extended CAN ID just adds another section in the ID part of the CAN message that is 18 bits long.


Typically, you wouldn't care about this, as on the application layer you'd have 29 bits for your ID. However, with this motor, they decided to split it up. The Standard ID (SID) is the ID of the motor. I'd guess it's probably 5 by default since that's what the example uses below where I took this screenshot. The Extended ID (EID) is the Control Word that the motor uses to determine what type of command you're sending it (this is specific to this motor, not all CAN bus communication is this complicated, I promise).

I'll break down their example of sending instructions here in a second. But first, if you have a Windows computer, if you open the calculator app that comes with Windows, and click on the hamburger menu in the upper left, you can change it to a programmer calculator and it'll help you with the bit arithmetic.

This is the example on page 23. The Producer ID is the ID of your Arduino (doesn't really apply here since your Arduino will read all the messages and in normal CAN you can't say who sent the message). But the Consumer ID is the ID of the motor. First, to calculate the SID, they do some bit arithmetic. Putting it in that calculator, 5<<1 is equal to 1010 in binary. ANDing that with 0x003F gets you 1010 (it's there in case your ID is over 6 bits long). That is then OR'd with0x100, which gets you 1 0000 1010 in binary, which is 10A in hexadecimal.

For the EID, 5 left bit shifted by 1 is 1010 in binary. ANDing that with 0x00C0 gets you 0 in binary (not entirely sure why they do this, someone smarter than me could probably explain). Anyways, 0<<8 is also 0. That OR'd with the Control Word (0x95 - given in the example), is 0x95 in hex.

Now, remember how I said that the extended ID just adds 18 extra bits to the standard ID? Well, the next part, where they calculate the CAN ID is doing just that- they shift the standard ID 18 bits, and OR the extended ID in (so that you end up with a 29 bit ID), giving you 0x4280095.

The DLC stands for Data Length Code, or how long the data you're sending is (in bytes). Since they're only sending one byte of data, that's a 1. The Data0 variable is the data you actually want to send.

In code, this is how I would do it (no guarantees this will work, I'm tired and haven't tested this).

#DEFINE MOTOR_CAN_ID 5 //put this at the top of your code, right below the libraries. Change the value to whatever you figure out the CAN ID is.

uint8_t send_can_message(uint16_t CW, uint8_t DLC) {
uint16_t SID = ((MOTOR_CAN_ID << 1) & 0x003F) | 0x0100;
uint32_t EID = (((MOTOR_CAN_ID << 1) & 0x00C0) << 8) | CW;
uint32_t CAN_ID = SID << 18 | EID;

byte sendStat = CAN.sendMsgBuf(CAN_ID, true, DLC, can_data);  //sendMsgBuf(CAN ID, extended true/false, data length, can data array). CAN data array can be a single number (like in the case of the example) or multiple numbers in an array.
if(sendStat == CAN_OK)
        return 1;
else 
        return 0;
}

You'd have to figure out what control word to send, and how you want to get the data to that function (might I suggest passing by reference or a global array?). Anyways, this should definitely give you a kick start in the right direction. Feel free to ask me any more questions! Sorry for the long post. But with your newfound knowledge, hopefully you can decipher the datasheet better!

1 Like

You just got a great course in CAN!

Hello @gilshultz @youdoitimbitter

Thanks for all the help.

I am now getting 60 Ohms!

The company got back to me with more information such as baud rate, CAN ID and how to send messages :


Based on this I have made corrections to the code ( I am trying to send a message for speed):

#include <mcp_can.h>
#include <SPI.h>

MCP_CAN CAN0(10);  // Set CS to pin 10

void setup()
{
  Serial.begin(115200);

  // Initialize MCP2515 running at 16MHz with a baudrate of 500kb/s and the masks and filters disabled.
  if(CAN0.begin(MCP_ANY, CAN_500KBPS, MCP_16MHZ) == CAN_OK)
    Serial.println("MCP2515 Initialized Successfully!");
  else
    Serial.println("Error Initializing MCP2515...");

  CAN0.setMode(MCP_NORMAL);  // Change to normal mode to allow messages to be transmitted
}

void loop() {
  // Data for setting jog speed to 10,000 pps
  byte data[8] = {0x10, 0x27, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00};
  
  // Send the CAN message with ID 0x0428009D (04 28 00 9D in hex)
  byte sndStat = CAN0.sendMsgBuf(0x0428009D, 0, 8, data);
  
  if(sndStat == CAN_OK) {
    Serial.println("Message Sent Successfully!");
  } else {
    Serial.println("Error Sending Message...");
  }

  delay(100);  // Wait 100ms before sending the next message
}

However, the motor is not moving and I am getting this message at the serial output:
image

So this means the message is being sent to the CAN bus...so why won't the motor work despite using the CAN message they sent me?

Unfortunately, I do not so I will need to troubleshoot with only this one.

Hello @hansa11 and @gilshultz , hope you are doing well!

The reason I asked if you had two CAN converter boards was so that you could troubleshoot getting your CAN bus to work, but since you have 60 Ohms and getting "Message Sent Successfully!" messages, I'd say you probably have it working (the library will only return that it sent the message if it gets an ACK from another device- I found that out the hard way when trying to send a full bus load worth of messages). Congrats!

I did notice a few things about the code you uploaded. First, you should only have to send a CAN message once with the message you want. The motor should remember what settings you send it. It's not like an analog output where you have to continuously tell the motor what you want to do.

Second, in your code where you send the message

byte sndStat = CAN0.sendMsgBuf(0x0428009D, 0, 8, data);

you have the extended true/false flag set to false. However, you are sending an extended ID. So, it should look like this:

byte sndStat = CAN0.sendMsgBuf(0x0428009D, 1, 8, data);

Third, I believe you are setting the speed (once you change that extended flag), but you aren't telling the motor to start. To do that, you will probably need to send this message:
image
That may just turn on the power to the motor but not move it, so you may need to send this message as well:
image

Fourth, you can typically just send 8 bytes of data all the time, but this motor may only want the bytes that have data in them. If you still aren't getting anywhere after trying my other suggestions, I would try setting the DLC to only what you are sending (ex. if you're sending 0x10 and 0x27 your DLC would be 2).

Another thing you can try is using the motor's built in error codes. The datasheet goes into detail on the error codes on page 20. If you add an IF statement checking to see if your CAN converter received any CAN messages to your loop, you could print out the data that the motor is sending back. I'm not sure if you know this (I just learned this a few months ago), but you can print hexadecimal to the serial monitor by using the following notation:

Serial.println(data[0],HEX);

I can't remember if type byte prints nicely on the serial monitor, so you may have to force the type like this:

Serial.println((uint8_t)data[0],HEX);

Let me know if you figured it out or if you have any additional questions!

2 Likes

Thank you very much for all the help! I will try this on Monday and get back to you!

THANKS SO MUCH!

THE MOTOR NOW WORKS!

However, I measured across CAN HI and CAN LO this morning and got 120 ohms for some reason. Also the motor is getting really hot. Is this normal?

This is the code that worked:

#include <mcp_can.h>
#include <SPI.h>

MCP_CAN CAN0(10);  // Set CS to pin 10

void setup()
{
  Serial.begin(115200);

  // Initialize MCP2515 running at 16MHz with a baudrate of 500kb/s and the masks and filters disabled.
  if(CAN0.begin(MCP_ANY, CAN_500KBPS, MCP_16MHZ) == CAN_OK)
    Serial.println("MCP2515 Initialized Successfully!");
  else
    Serial.println("Error Initializing MCP2515...");

  CAN0.setMode(MCP_NORMAL);  // Change to normal mode to allow messages to be transmitted
}

void loop() {
  // Additional CAN messages to ensure motor start
  byte driveOn[2] = {0x04, 0x01};  // Set drive on
  CAN0.sendMsgBuf(0x04280095, 1, 2, driveOn);  // Send drive on command
  
  byte startMotion[2] = {0x04, 0x00};  // Start motion
  CAN0.sendMsgBuf(0x04280096, 1, 2, startMotion);  // Send start motion command

  delay(50);  // Short delay to ensure the motor is ready before setting speed

  // Data for setting jog speed to 10,000 pps
  byte data[2] = {0x10, 0x27}; // Adjusted DLC to 2 since only two bytes are needed for speed

  // Send the CAN message with ID 0x0428009D (04 28 00 9D in hex) and extended flag set to true (1)
  byte sndStat = CAN0.sendMsgBuf(0x0428009D, 1, 2, data);
  
  if(sndStat == CAN_OK) {
    Serial.println("Message Sent Successfully!");
  } else {
    Serial.println("Error Sending Message...");
  }

  delay(100);  // Wait 100ms before sending the next message
}

Now I just need to find a way to convert PPS to RPM and I also need to implement a closed loop PID so I can get some feedback and determine accuracy. I would like the motor to stabilize and settle on a speed in less then 0.5 seconds.

That's great to hear!

As for the 120 Ohms when measuring resistance, was the CAN bus on when you measured? It might be a current-going-back-into-the-multimeter kind of thing. Or one of your resistors came off.

Where on the motor does it get hot? And how hot does it get? Does it get warm to the touch or does it burn you when you touch it? Motors do typically get warm when running (the industrial motors I work with have dedicated cooling systems to keep them cool). If it burns you when you touch it, check to make sure everything is wired up correctly (specifically the motor power), that you're feeding it the correct voltage, and that you're not trying to make it spin faster than it's rated for. If it still burns you when you touch it, I'd suggest getting in touch with the manufacturer and explaining your problem to them, as they'd be better suited to answering questions about that specific motor.

For your code, I'd recommend not sending every 100ms as I assume the motor has a controller that has some memory where your commands are sent so that you can send a message once and it will stay there until you change it again. Otherwise it looks good!

Sounds like you're going to get into digital control loops. Have fun! Keep in mind that if you're trying to follow a step function, you'll need a first order system, and if you're trying to follow a ramp, you'll need a second order system.

Everything was off when I measured it and the jumper was on. Though Tom mentioned I should connect the ground of the power supply and motor to arduino. They didn't mention this in the schematic.

It warm to the touch but it gradually gets warmer. It is hot around the casing. I gave it 24 V which is the max. I was also only spinning it at the default speed in the sample hex code they gave.

Will do!

I googled step function and this is what I got:

For my application, I need the motor speed to be instantaneous (like i set a speed and and the motor automatically moves at this speed). If this image is correct, then that means I need to use a step function right? I tried second order PID with a brushed DC motor where my graph looked like this:
image
Problem is it takes time to settle and it takes time to rise and oscillates in the beginning.

I just read this:

So apparently I don't need to develop my own PID code?? Some type of code is already implemented in the controller and I just need to enable it by setting 'IC[6] = 1`

So would I just put 'IC[6]=1' at the top of the code? Then I would get feedback automatically?

Also seems like I have to do some setup for the encoder and I have to pick the resolution?

Then I can use it here:

Hello,

I would like to please ask what the blue and red indicate on the hex code? How do I know what numbers to group into 1 byte?

For example do I send the CAN message to the arduino like this:

//Initial Configuration

// byte config[8] = {0x01,0x00,0x01,0x00,0x01,0x01,0x01,0x00};

// CAN0.sendMsgBuf(0x04280086, 1, 8, config);

Or 1 whole byte like this:

//Initial Configuration

byte config[8] = {0x0100010001010100};

CAN0.sendMsgBuf(0x04280086, 1, 1, config);

Asking this because the motor only runs when I send it all in one byte which is weird.

Sorry for not responding for a few days, work has been hectic.

This shouldn't matter (key word is shouldn't). It definitely won't hurt anything, though. You'll just be minimizing the amount of return current through the Arduino (which is a good thing!).

I would definitely talk to the manufacturer to make sure that is normal. I have a feeling it is, but just to be on the safe side so that you don't burn your motor up.

There are ways around that and different control methods, but that's getting into upperclass college and graduate level courses. Since it sounds like the motor has this built in, hopefully you don't have to worry about it!

No. I did some looking through the datasheet, and it doesn't really explain this very well. See my next paragraph as to how to set this.

So doing some digging in the datasheet, it looks like the red is your i value. The blue numbers are setting the value of i.

For example, for the QE command

To set the lines per revolution of the encoder, your d0 would be 0x04. If you look at the table above the examples, it tells you what each "i" value does. We can see that an "i" of 0x04 corresponds to "lines per revolution of encoder". Hence, your d0 (your "i" value) would be 0x04.
image
If you wanted to set the counts per revolution for closed loop control, your "i" would be 0x00.

The blue numbers are how you set that value ("Value (N)" on the table). So if you wanted to set the counts per revolution to 6000, you would whip out your windows calculator in programmer mode, and in decimal mode, type 6000. The hex field will fill out automatically to the corresponding hexadecimal value. In this case, 0x1770.

image

However, each CAN data byte can only send one byte of data, and that value is two bytes. So, they split it up. The last two values (70) they put in d1, and the first two values (17) they put in d2. You may think that's backwards, but CAN actually sends the data backwards- from d7 to d1. So to the controller, it actually receives the 17 before the 70.

You may be wondering what CAN ID you send this message to. They give you the control words at the top of the table, whether or not you want the motor to acknowledge the message. I would recommend requesting the motor acknowledge the message. I'm not sure if it would turn off the CAN acknowledge signal, or if the motor replies with an actual message back, I guess you'll have to play around with that. However, if it disables the CAN acknowledge bit, then your Arduino will say that the message didn't send properly (unless you put it in OneShot mode).

To calculate the CAN ID, use this formula that I gave you in a previous reply.

You would send the CAN message like this (aka split up):

The bottom one probably worked because it only sent a portion of that number.

I think I know what you're trying to set, and you can't do it all in one message like that. You'll have to send a message for each "i" value, setting the value with 0x00 or 0x01 in d1, and 0x00 in d2 (because all values are either 1 or 0). So you'll have to send 8 messages to set that up :slight_smile:

Hope this helps!

Turns out they have a software that can create those can message ID's as well as the data.

So no need to calculates CAN ID.

However, right now I am trying to read the speed using the Get Motor Status.

#include <mcp_can.h>
#include <SPI.h>

MCP_CAN CAN0(10);  // Set CS to pin 10

void setup() {
  Serial.begin(115200);

  // Initialize MCP2515 running at 16MHz with a baud rate of 500kb/s and the masks and filters disabled.
  if (CAN0.begin(MCP_ANY, CAN_500KBPS, MCP_16MHZ) == CAN_OK)
    Serial.println("MCP2515 Initialized Successfully!");
  else
    Serial.println("Error Initializing MCP2515...");

  CAN0.setMode(MCP_NORMAL);  // Change to normal mode to allow messages to be transmitted

  //initial configuration, set IC[6] = 0 for closed loop control
  byte config[3] = {0x06, 0x01, 0x00};  
  CAN0.sendMsgBuf(0x04280086, 1, 3, config);  

  // Send initial configuration, drive on, and start motion commands here...

  // Start Motion Command
  byte startMotion[2] = {0x00};  // Start motion
  CAN0.sendMsgBuf(0x04280096, 1, 1, startMotion);  // Send start motion command

  delay(50);  // Short delay to ensure the motor is ready before setting speed

  // Data for setting jog speed to 16,000 PPS or 3000 RPM
  byte data[4] = {0x00, 0x71, 0x02, 0x00}; 
  byte sndStat = CAN0.sendMsgBuf(0x0428009D, 1, 4, data);
  
  if (sndStat == CAN_OK) {
    Serial.println("Message Sent Successfully!");
  } else {
    Serial.println("Error Sending Message...");
  }

  delay(100);  // Wait 100ms before sending the next message
}

void loop() {
 // Request Motion Status
  byte motionStatus[1] = {0x01};  // Sending 1 byte
  byte reqStat = CAN0.sendMsgBuf(0x04280011, 1, 1, motionStatus);  // Set DLC to 1
  
  if (reqStat == CAN_OK) {
    Serial.println("Motion Status request sent successfully.");
  } else {
    Serial.println("Error sending Motion Status request.");
  }

  // Wait for the response
  unsigned long rxId;
  byte len = 0;
  byte buf[8];
  
  if (CAN0.checkReceive() == CAN_MSGAVAIL) {
    CAN0.readMsgBuf(&rxId, &len, buf);
    
    // Print received CAN ID
    Serial.print("Received CAN ID: 0x");
    Serial.println(rxId, HEX);

    // Print data bytes
    Serial.print("Data: ");
    for (int i = 0; i < len; i++) {
      Serial.print(buf[i], HEX);
      Serial.print(" ");
    }
    Serial.println();
  } else {
    Serial.println("No CAN message available.");
  }

  delay(500);  // Adjust delay as needed
}

I am getting this output:
Screenshot 2024-08-18 103530

I don't understand how to decode this to speed. The software for decoding won't help as it just say's explanation: ID:0, instruct.