corygrant / CANBoard_FW

Firmware - CAN enabled IO board
MIT License
6 stars 4 forks source link

Emulate Haltech IO Box #9

Open JZXPacky opened 1 year ago

JZXPacky commented 1 year ago

Hi guys,

Would it be possible for these to be configured to emulate the 2 Haltech IO Box 12's? I've got CAN data for the TX side from Haltech themselves.

Their box has 4 x AVI's, 4 x DPO's, and 4 x DPI's. Obviously I'd only be able to use the inputs as I haven't got the RX data from them, but I doubt it would be too hard to work out. IO Box A CAN data IO Box B CAN data

JZXPacky commented 1 year ago

I should add that their CAN operates at 1MBit, page 10 off the attached Dash Protocol mentions this. Not sure if you can do 1MBit yet? Haltech CAN Dash Protocol-2022-08-09.pdf

corygrant commented 1 year ago

@JZXPacky yes I think that's definitely possible. Changing the CAN prescaler to 2 on line 430 of main.c will change it to 1MBit: hcan.Init.Prescaler = 2;

This website is a great tool to find the correct prescaler and TimeSeq http://www.bittiming.can-wiki.info/ Select ST bxCAN and set the clock rate to 36MHz.

The messages can be modified by changing void TxCanMsgs()

This section sets the ID, DLC and bytes

//=======================================================
//Build Msg 0 (Analog inputs 1-4 millivolts)
//=======================================================
CanTxHeader.StdId = CAN_BASE_ID + nCanBaseIdOffset + 0;
CanTxHeader.DLC = 8; //Bytes to send
CanTxData[0] = ((float)anIn[0] / 4096) * 4850; //Scaled to 4.85v - voltage divider on input, 3.3V = 4.85V on input
CanTxData[1] = ((float)(anIn[0] >> 8) / 4096) * 4850;
CanTxData[2] = ((float)anIn[1] / 4096) * 4850;
CanTxData[3] = ((float)(anIn[1] >> 8) / 4096) * 4850;
CanTxData[4] = ((float)anIn[2] / 4096) * 4850;
CanTxData[5] = ((float)(anIn[2] >> 8) / 4096) * 4850;
CanTxData[6] = ((float)anIn[3] / 4096) * 4850;
CanTxData[7] = ((float)(anIn[3] >> 8) / 4096) * 4850;

And then it's transmitted here:

//=======================================================
//Send CAN msg
//=======================================================
if(HAL_CAN_AddTxMessage(&hcan, &CanTxHeader, CanTxData, &CanTxMailbox) != HAL_OK){
  Error_Handler();
}

Having a trace of the CAN data will help a lot to get it formatted properly, and to verify the CANBoard is sending the data correctly.

JZXPacky commented 1 year ago

@corygrant I appreciate you taking the time to get back to me. I'm going to order a batch of boards in the next week or so, unfortunately JLCPCB are out of the STM32's.

Can you give me more details on how to write this to the board too? I understand you need the cable, but where does this connect to and what software are you using?

This is a first for me, I do IT for a living but haven't delved this far although have always wanted to.

corygrant commented 1 year ago

@JZXPacky it uses a TagConnect style connection. It's the 6x pads with 3 holes around it. They sell quite a few different models but I use the ST Link with their cable. The connector has spring pins in it so you don't have to have a permanent connector on the board.

For software, I use ST Cube IDE. But, the firmware could be modified to use cmd line tools.

It's definitely a different world! I program industrial machines at work, so this is all quite a bit different.

JZXPacky commented 1 year ago

@corygrant thanks for that, I couldn't see the connection for the life of me in the renders for some reason. Now I know what I'm looking for it stands out.

JZXPacky commented 1 year ago

Hi @corygrant I've finally received all my bits and pieces and was after a bit of guidance if you don't mind. I ended up getting the ST LInk V3 Minie and a cable, have my boards, and installed the CubeIDE, monitor, and programmer softwares.

With the info I supplied in my first post, would what I need to change in the TxCanMsgs section to get the right information sent? I am familiar with coding in other languages (Infrastructure guy here) but have never done C.

Appreciate any help you can give me.

JZXPacky commented 1 year ago

Also with connecting to the board to program it, does it need to have power to the 12v and ground to work? I'm assuming that the JTAG connector doesn't actually power the device. Do my settings look good for connecting in Cube Programmer too?

image

corygrant commented 1 year ago

Yes you'll need to power it with 12 and ground, the ST Link doesn't supply any power. You'll want Port to be SWD instead of JTAG.

You'll want to change the values in TxCanMsgs to format the data in the way that you want. anIn[] is the array of uint16_t raw analog input values, so those need to be scaled and mapped to the data bytes of the CAN message.

It looks like they're scaling the analog inputs to 5V, currently I'm scaling to 4850mV due to the voltage divider on the input. 4.85V is the maximum value because at that voltage there is 3.3V on the input pin, so you'll want to scale to 4.85V. They're using volts, where I'm using millivolts. So maybe they are just showing 0,1,2,3,4,5V? Without any decimals? That doesn't seem very useful though.

For anIn[0], I'm mapping that to the data bytes 0 and 1, hence the bit shift by 8 on data byte 1.
I'm not exactly sure what they're showing in that table for their data mapping, 0:3-1:0 doesn't make much sense to me. If that's byte0.bit3 to byte1.bit0 that is only 6 bits, but they're using a scale of 4096, which is 12 bits.

If you have a capture of the data coming from the device it should be pretty easy to see what they're doing.

For the digital pulsed inputs you'll have to create your own function for that as I'm not handling any pulsed inputs in my code. There are many ways to do it, so some google searching should lead you to some examples of how to calculate duty cycle and period from a digital input.

JZXPacky commented 1 year ago

Yeah I'm going to have to play with it to see how I go. Should the LED light up when power is applied, before the code is written to the device?

I've not got a copy of the data stream, but I have seen another project where someone is using an arduino based board to emulate the Haltech IO Expander.

https://github.com/tolunaygul/IObox-emulator-haltech/blob/main/emulator-v2.ino

Not sure if this is useful to you at all.

corygrant commented 1 year ago

Yes, the LED goes directly to 3.3V so it should always be on when power is applied.

That does help, it looks like the message is actually 16 bit. The 0:3-1:0 must be pointing towards the data only being 12 bit so the real analog data never populates the first 4 bits.

A few things to note from that Arduino project:

JZXPacky commented 1 year ago

I think there might be something incorrect with mine, as the LED isn't coming up. If it is backwards (which it shouldn't be), will the board still work?

This is the final part placement from JLCPCB they sent through. Everything looks correct according to your HW instructions.

image

I'll have a look at the code when I get a chance, but I will more than likely have some more questions.

Yes the keep alive is needed from what I understand.

corygrant commented 1 year ago

Yes it's possible that the LED may be backwards. I remember I had this issue with another board from JLC and their support contacted me saying they thought I had it backwards. In the end, it turned out that they use a very strange symbol for diodes.

You can check power at the big tab on U2 (in the image above). There should be 3.3V there. If there is, then the LED is backwards.

Let me know and I'll update the picture in the HW section.

JZXPacky commented 1 year ago

Ok so it looks like the LED is backwards. I've got 3.3V on that pad for U2.

Just got my STLINK V3 Mini to talk to the board. My JTAG cable is different to the one you linked, so wasn't sure if the pinout was correct. The V3 has an STDC14 connector and the cable has an adaptor board from RJ11 that has an STDC10 connector. Turns out I had the ribbon backwards on one of the connectors. It all connects now in Cube Programmer.

Now to work out what to do with the code to get the right messages.

JZXPacky commented 1 year ago

@corygrant So I've had a look at the code, but I'm a bit lost.

Is there any chance you can help me adapt your code to give the correct CAN messages I need? With regards to the DPI's, I was only planning on using these as switch inputs anyway, so that wouldn't be much of an issue.

corygrant commented 1 year ago

Try flipping the bytes and using 0x2C1 as the ID

CanTxHeader.StdId = 0x2C1;
CanTxHeader.DLC = 8; //Bytes to send
CanTxData[0] = ((float)(anIn[0] >> 8) / 4096) * 4850;
CanTxData[1] = ((float)anIn[0] / 4096) * 4850;
CanTxData[2] = ((float)(anIn[1] >> 8) / 4096) * 4850;
CanTxData[3] = ((float)anIn[1] / 4096) * 4850;
CanTxData[4] = ((float)(anIn[2] >> 8) / 4096) * 4850;
CanTxData[5] = ((float)anIn[2] / 4096) * 4850;
CanTxData[6] = ((float)(anIn[3] >> 8) / 4096) * 4850;
CanTxData[7] = ((float)anIn[3] / 4096) * 4850;

The keep alive would be:

CanTxHeader.StdId = 0x2C7;
CanTxHeader.DLC = 5; //Bytes to send
CanTxData[0] = 0x10;
CanTxData[1] = 0x09;
CanTxData[2] = 0x0A;
CanTxData[3] = 0x00;
CanTxData[4] = 0x00;

All together it would be:

  CanTxHeader.StdId = 0x2C1;
  CanTxHeader.DLC = 8; //Bytes to send
  CanTxData[0] = ((float)(anIn[0] >> 8) / 4096) * 4850;
  CanTxData[1] = ((float)anIn[0] / 4096) * 4850;
  CanTxData[2] = ((float)(anIn[1] >> 8) / 4096) * 4850;
  CanTxData[3] = ((float)anIn[1] / 4096) * 4850;
  CanTxData[4] = ((float)(anIn[2] >> 8) / 4096) * 4850;
  CanTxData[5] = ((float)anIn[2] / 4096) * 4850;
  CanTxData[6] = ((float)(anIn[3] >> 8) / 4096) * 4850;
  CanTxData[7] = ((float)anIn[3] / 4096) * 4850;

  //=======================================================
  //Send CAN msg
  //=======================================================
  if(HAL_CAN_AddTxMessage(&hcan, &CanTxHeader, CanTxData, &CanTxMailbox) != HAL_OK){
    Error_Handler();
  }

  osDelay(CAN_TX_MSG_SPLIT);

  CanTxHeader.StdId = 0x2C7;
  CanTxHeader.DLC = 5; //Bytes to send
  CanTxData[0] = 0x10;
  CanTxData[1] = 0x09;
  CanTxData[2] = 0x0A;
  CanTxData[3] = 0x00;
  CanTxData[4] = 0x00;

  //=======================================================
  //Send CAN msg
  //=======================================================
  if(HAL_CAN_AddTxMessage(&hcan, &CanTxHeader, CanTxData, &CanTxMailbox) != HAL_OK){
    Error_Handler();
  }

  osDelay(CAN_TX_MSG_SPLIT);

Currently I'm sending all of the messages every 50ms. They're showing 50Hz, which is 1/50 = 20ms. Change the define value of CAN_TX_DELAY to 20, that will set the messages to every 20ms You may have to reduce the CAN_TX_MSG_SPLIT to be something lower, like 1 or 2ms. That value is the time between transmitting messages.

#define CAN_TX_DELAY 20
#define CAN_TX_MSG_SPLIT 5

No idea if that will work, but it's worth a shot!

JZXPacky commented 1 year ago

That's great, I'll have a bit of a play with it now.

I'm going through the Arduino code on the Repo I linked before, and it looks like they send the data at the following intervals

unsigned long task1Interval = 20; // 30ms interval for analogue value sending
unsigned long task2Interval = 100;// 50ms interval for keep aliv frame
unsigned long task3Interval = 10; // 3 second interval for task 3
unsigned long task4Interval = 40; // 4 second interval for task 4
unsigned long task1Millis = 0;    // storage for millis counter
unsigned long task2Millis = 0;    // storage for millis counter
unsigned long task3Millis = 0;    // storage for millis counter
unsigned long task4Millis = 0;    // storage for millis counter

These are called like below.

void loop() {

  unsigned long currentMillis = millis();  // Get current time in milliseconds

  // Execute task 1 every 1 second
  if (currentMillis - task1Millis >= task1Interval) {
    task1Millis = currentMillis;
    SendKeepAlive();
  }

  // Execute task 2 every 5 seconds
  if (currentMillis - task2Millis >= task2Interval) {
    task2Millis = currentMillis;
    SendAnalogValues();
  }

  // Execute task 3 every 3 seconds
  if (currentMillis - task3Millis >= task3Interval) {
    task3Millis = currentMillis;
    DriveDigitalPin();
  }

  // Execute task 4 every 4 seconds
  if (currentMillis - task4Millis >= task4Interval) {
    task4Millis = currentMillis;
    task4();
  }

With regards to the inputs, can they not see full 5V? I'm looking at how they are scaling, and it seems like it will take the full 5V (it's Arduino Uno based and these will take 5V).

  scaledvalue1 = map(analogRead(A0), 1023, 0, 0, 4095);  // read analogue value and scale to 12 bit
  scaledvalue2 = map(analogRead(A1), 1023, 0, 0, 4095);  // read analogue value and scale to 12 bit
  scaledvalue3 = map(analogRead(A2), 1023, 0, 0, 4095);  // read analogue value and scale to 12 bit
  scaledvalue4 = map(analogRead(A3), 1023, 0, 0, 4095);  // read analogue value and scale to 12 bit

  canMsg.m2C1.data.AVI1_V = scaledvalue1;  // add scaled analogue value to message
  canMsg.m2C1.data.AVI2_V = scaledvalue2;  // add scaled analogue value to message
  canMsg.m2C1.data.AVI3_V = scaledvalue3;  // add scaled analogue value to message
  canMsg.m2C1.data.AVI4_V = scaledvalue4;  // add scaled analogue value to message

  //constuct the can message with correct bytes
  byte SendAnalogue[8] = {
    canMsg.m2C1.bytes[1],
    canMsg.m2C1.bytes[0],
    canMsg.m2C1.bytes[3],
    canMsg.m2C1.bytes[2],
    canMsg.m2C1.bytes[5],
    canMsg.m2C1.bytes[4],
    canMsg.m2C1.bytes[7],
    canMsg.m2C1.bytes[6]
  };

  CAN0.sendMsgBuf(0x2C1, 0, 8, SendAnalogue);  // send the can message onto the bus
}

I've also noticed that there is different CAN ID's for AVI's, DPO's, DPI's, and the keepalive

IO Box B
AVI 1-4 = 0x2C1
DPO 1-2 = 0x2D1
DPO 2-4 = 0x2D3
DPI 1-2 = 0x2C3
DPI 2-4 = 0x2C5
Keepalive = 0x2C7

I might have to split up the functions a bit, and write something to handle the DPI's like you mentioned.

Sorry for all the questions, just trying to get a further grasp on it and how to put it all together.

corygrant commented 1 year ago

Correct, the inputs won't get all the way to 5V due to the voltage divider on the analog inputs. The 4.7K and 10K resistors will drop a 5V input to 3.4V at the microcontroller, which is over the reading range (max 3.3V). At 4.85V, the input will be at 3.3V which is equal to the max value of 4095. Hence the scaling to 4.85V instead of 5V

In reality a 3.3V input won't be exactly 4095, but it will be close. There's a way to make that more accurate using VREFINT and VREFINT_CAL, but I didn't think that was necessary for this device. I originally intended the analog inputs to be used with rotary selectors or pots, so no need for high analog accuracy.

JZXPacky commented 1 year ago

Ok, that makes sense. I might just end up using this for switch inputs once I get the code working and then all I need is a voltage greater than 1V on the input, so will feed them with maybe 3.3V (or something below 4.85V). It will allow me to hook up some factory buttons for various functions, and move some other things off of the main ECU inputs.