diff --git a/API.md b/API.md index cbe9cac..30aaf16 100644 --- a/API.md +++ b/API.md @@ -247,6 +247,17 @@ if ((packetId & mask) == id) { Returns `1` on success, `0` on failure. +This patched version also supports setting MCP2515 registers directly: + +``` +CAN.setFilterRegisters( + mask0, filter0, filter1, + mask1, filter2, filter3, filter4, filter5, + allowRollover); +``` + +Please see the MCP2515 data sheet for more details on how these work. + ## Other modes ### Loopback mode diff --git a/README.md b/README.md index 02f5667..889eecd 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,5 @@ # Arduino CAN -[![Build Status](https://travis-ci.org/sandeepmistry/arduino-CAN.svg?branch=master)](https://travis-ci.org/sandeepmistry/arduino-CAN) - An Arduino library for sending and receiving data using CAN bus. ## Compatible Hardware @@ -42,18 +40,12 @@ Requires an external 3.3V CAN transceiver, such as a [TI SN65HVD230](http://www. ## Installation -### Using the Arduino IDE Library Manager - -1. Choose `Sketch` -> `Include Library` -> `Manage Libraries...` -2. Type `CAN` into the search box. -3. Click the row to select the library. -4. Click the `Install` button to install the library. - -### Using Git +As this is a fork of https://github.com/sandeepmistry/arduino-CAN with +additional patches, you have to use git to install this version of the library: ```sh -cd ~/Documents/Arduino/libraries/ -git clone https://github.com/sandeepmistry/arduino-CAN CAN +cd ~/Documents/Arduino/libraries/ # ~/Arduino/libraries on Mac OS +git clone https://github.com/timurrrr/arduino-CAN CAN ``` ## API @@ -64,8 +56,6 @@ See [API.md](API.md). See [examples](examples) folder. -For OBD-II examples, checkout the [arduino-OBD2](https://github.com/sandeepmistry/arduino-OBD2) library's [examples](https://github.com/sandeepmistry/arduino-OBD2/examples). - ## License This library is [licensed](LICENSE) under the [MIT Licence](http://en.wikipedia.org/wiki/MIT_License). diff --git a/examples/CanBusMonitor/CanBusMonitor.ino b/examples/CanBusMonitor/CanBusMonitor.ino new file mode 100644 index 0000000..fc008c5 --- /dev/null +++ b/examples/CanBusMonitor/CanBusMonitor.ino @@ -0,0 +1,96 @@ +#include + +// This is a demo program that listens to messages on the CAN bus and prints them out to Serial. +// +// It was tested on Arduino Uno, Arduino Micro, Adafruit Feather nRF52832 and +// Adafruit ItsyBitsy nRF52840 Express, and should be trivial to tweak to +// support pretty much any other board with SPI. +// +// Connections: +// MCP | BOARD +// INT | Not used, can connect to Pin 9 +// SCK | SCK +// SI | MO +// SO | MI +// CS | Pin 7 +// GND | GND +// VCC | 3.3V + +const int CS_PIN = 7; +const int IRQ_PIN = 9; +const int QUARTZ_MHZ = 16; // Some MCP2515 boards have 8 MHz quartz. +const int SPI_MHZ = 16; + +void setup() { + Serial.begin(115200); + + uint32_t startTimeMs = millis(); + while (!Serial); + + Serial.println("Started!"); + + CAN.setClockFrequency(QUARTZ_MHZ * 1E6); + CAN.setSPIFrequency(SPI_MHZ * 1E6); + CAN.setPins(CS_PIN, IRQ_PIN); + + // Subaru BRZ uses a 500k baud rate. + while (!CAN.begin(500000)) { + Serial.println("Failed to connect to the CAN controller!"); + delay(1000); + } + + Serial.println("CAN controller connected"); +} + +// Forward declarations for helper functions. +void handle_message(uint32_t pid); +void print_report(); + +void loop() { + int packet_size = CAN.parsePacket(); + if (packet_size <= 0) { + return; + } + + if (CAN.packetRtr()) { + // Ignore RTRs for now. + return; + } + + uint8_t data[8] = {0}; + int data_length = 0; + while (data_length < packet_size && data_length < sizeof(data)) { + int byte_read = CAN.read(); + if (byte_read == -1) { + break; + } + + data[data_length++] = byte_read; + } + + uint32_t packet_id = CAN.packetId(); + handle_message(packet_id, data, data_length); +} + +void handle_message(uint32_t packet_id, uint8_t *data, int data_length) { + // Optional: add something like + // if (packet_id != 0x7E8) { + // return; + // } + // to only show a subset of messages that match a certain criteria. + + // TODO: Add something smart to avoid spamming Serial. + // For example, limit the number of messages printed over 10 seconds to 25? + + Serial.print("0x"); + Serial.print(packet_id, HEX); + Serial.print(", data:"); + for (int i = 0; i < data_length; i++) { + Serial.print(" "); + if (data[i] < 0x10) { + Serial.print("0"); // Add leading zero for readability. + } + Serial.print(data[i], HEX); + } + Serial.println(); +} diff --git a/examples/FakeSubaruBRZ/FakeSubaruBRZ.ino b/examples/FakeSubaruBRZ/FakeSubaruBRZ.ino new file mode 100644 index 0000000..b90b367 --- /dev/null +++ b/examples/FakeSubaruBRZ/FakeSubaruBRZ.ino @@ -0,0 +1,390 @@ +#include + +// This is a demo program that sends messages over the CAN bus in +// a way that resembles real messages you can receive if you listen +// to messages on the CAN bus of a 2013-2020 Subaru BRZ. +// +// DO NOT USE IT IN THE CAN NETWORK OF A REAL VEHICLE as it can cause unexpected +// side effects. +// +// It was tested on Arduino Uno, Arduino Micro, Adafruit Feather nRF52832 and +// Adafruit ItsyBitsy nRF52840 Express, and should be trivial to tweak to +// support pretty much any other board with SPI. +// +// Connections: +// MCP | BOARD +// INT | Not used, can connect to Pin 9 +// SCK | SCK +// SI | MO +// SO | MI +// CS | Pin 7 +// GND | GND +// VCC | 3.3V + +const int CS_PIN = 7; +const int IRQ_PIN = 9; +const int QUARTZ_MHZ = 16; // Some MCP2515 boards have 8 MHz quartz. +const int SPI_MHZ = 8; + +void setup() { + Serial.begin(115200); + + uint32_t startTimeMs = millis(); + while (!Serial && millis() - startTimeMs < 1000); + if (!Serial) { + Serial.println("Started!"); + } else { + // Whatever, noone's going to see anyways. + } + + CAN.setClockFrequency(QUARTZ_MHZ * 1E6); + CAN.setSPIFrequency(SPI_MHZ * 1E6); + CAN.setPins(CS_PIN, IRQ_PIN); + + // Subaru BRZ uses a 500k baud rate. + while (!CAN.begin(500000)) { + Serial.println("Failed to connect to the CAN controller!"); + delay(1000); + } + + Serial.println("CAN controller connected"); +} + +class FakeTpmsEcu { +public: + FakeTpmsEcu() { + has_continuation_frame = false; + original_frame_acked = false; + } + + void scheduleNextFrame(uint16_t pid, uint8_t *data, uint8_t len) { + if (has_continuation_frame) { + Serial.print("Scheduling new frame, even though there is one pending already. Original was "); + if (original_frame_acked) { + Serial.println("acked."); + } else { + Serial.println("NOT acked."); + } + } + has_continuation_frame = true; + original_frame_acked = false; + next_frame_pid = pid; + memcpy(next_frame_data, data, len); + next_frame_length = len; + } + + void handleFrameAck(uint8_t separation_time_millis = 0) { + if (!has_continuation_frame) { + return; + } + + original_frame_acked = true; + next_frame_timestamp_millis = millis() + separation_time_millis; + } + + void sendNextFrameIfNeeded() { + if (!has_continuation_frame || !original_frame_acked) { + return; + } + + // Unsigned math magic to check if "time_diff" is "negative": + unsigned long time_diff = millis() - next_frame_timestamp_millis; + if (time_diff >> (8 * sizeof(time_diff) - 1)) { + return; + } + + send_data(next_frame_pid, next_frame_data, next_frame_length); + has_continuation_frame = false; + original_frame_acked = false; + } + +private: + bool has_continuation_frame; + uint16_t next_frame_pid; + uint8_t next_frame_data[8]; + uint8_t next_frame_length; + + bool original_frame_acked; + unsigned long next_frame_timestamp_millis; +} fake_tpms_ecu; + +// Forward declaration for a helper. +void try_to_receive_data(); +void generate_payload(uint16_t pid, uint8_t *payload); +boolean send_data(uint16_t pid, uint8_t *payload, uint8_t len); + +// TODO: Refactor the loop() function. It should first try to see if there's anything to send, +// then if there's anything to receive. +void loop() { + // Many PIDs are sent 50 times per second. + uint16_t num_cycles_per_second = 50; + + // 0x18, 0x140, 0x141 and 0x142 are intentionally duplicated in this array as they are sent 100 times + // per second (double that for other PIDs) in the real car. + uint16_t pids[] = { + // These are sent 100 times per second: + 0x18, 0x140, 0x141, 0x142, + // These are sent 50 times per second: + 0xD0, 0xD1, 0xD2, 0xD3, 0xD4, 0x144, 0x152, 0x156, 0x280, + // TODO: These are actually sent less frequently than 50 times per second: + 0x282, // 16.7 times per second + 0x284, // 10 times per second + 0x360, // 20 times per second + // These are commented out so that we don't send way too many messages: + //0x361, // 20 times per second + //0x370, // 20 times per second + //0x372, // 10 times per second + // These are sent 100 times per second: + 0x18, 0x140, 0x141, 0x142, + }; + uint16_t num_messages_per_cycle = sizeof(pids) / sizeof(pids[0]); + uint16_t num_messages_per_second = + num_cycles_per_second * num_messages_per_cycle; + + unsigned long first_message_sent_micros = micros(); + unsigned long num_messages_sent = 0; + for (int i = 0; i < num_messages_per_cycle; i++, num_messages_sent++) { + uint16_t pid = pids[i]; + uint8_t payload[8]; + generate_payload(pid, payload); + + if (!send_data(pid, payload, 8)) { + Serial.println("Failed to send a message"); + } + + unsigned long next_message_time_micros = + first_message_sent_micros + + (num_messages_sent * 1000000) / num_messages_per_second; + if ((long)(micros() - next_message_time_micros) < 0) { + try_to_receive_data(); + fake_tpms_ecu.sendNextFrameIfNeeded(); + delayMicroseconds(10); + } + } +} + +boolean send_data(uint16_t id, uint8_t *payload, uint8_t len) { + if (!CAN.beginPacket(id)) { + Serial.println("beginPacket() failed."); + return false; + } + + CAN.write(payload, len); + if (!CAN.endPacket()) { + Serial.println("endPacket() failed."); + return false; + } + + return true; +} + +void try_to_receive_data() { + int packet_size = CAN.parsePacket(); + if (packet_size <= 0) { + return; + } + + if (CAN.packetRtr()) { + // Ignore RTRs. + return; + } + + uint32_t id = CAN.packetId(); + uint8_t data[8] = {0}; + int data_length = 0; + while (data_length < packet_size && data_length < sizeof(data)) { + int byte_read = CAN.read(); + if (byte_read == -1) { + break; + } + + data[data_length++] = byte_read; + } + + if (id == 0x7C0 && data[0] == 0x2 && data[1] == 0x21 && data[2] == 0x29) { + // 0x7C0 / 0x2129 — Returns fuel level in liters x2. + uint8_t response[8] = {0}; + response[0] = 0x3; + response[1] = 0x61; + response[2] = 0x29; + response[3] = 0x1C; + send_data(0x7C8, response, 8); + return; + } + + if (id == 0x7DF && data[0] == 0x2 && data[1] == 0x01 && data[2] == 0x0f) { + // 0x7DF / 0x010F — Returns (intake temperature in ºC + 40) + uint8_t response[8] = {0}; + response[0] = 0x3; + response[1] = 0x41; + response[2] = 0x0f; + response[3] = 40 + 36; // 36 ºC + send_data(0x7E8, response, 8); + return; + } + + if (id == 0x7DF && data[0] == 0x2 && data[1] == 0x01 && data[2] == 0x46) { + // 0x7DF / 0x0146 — Returns (air temperature in ºC + 40) + uint8_t response[8] = {0}; + response[0] = 0x3; + response[1] = 0x41; + response[2] = 0x46; + response[3] = 40 + 27; // 27 ºC + send_data(0x7E8, response, 8); + return; + } + + if (id == 0x750 && data_length >= 1 && data[0] == 0x2a) { + if (data_length >= 3 && data[1] == 0x02 && data[2] == 0x21) { + if (data[3] == 0x30) { + // TPMS pressures request. + uint8_t response[8] = {0}; + response[0] = 0x2a; + response[1] = 0x10; // "1" means "first frame in a sequence" + response[2] = 0x07; + response[3] = 0x61; + response[4] = 0x30; + response[5] = 0xAB; // FL tire pressure + response[6] = 0xAC; // FR tire pressure + response[7] = 0xAD; // RR tire pressure + send_data(0x758, response, 8); + + response[0] = 0x2a; + response[1] = 0x21; // "2" means "continuation frame", "1" means "first continuation frame". + response[2] = 0xAE; // RL tire pressure + response[3] = 0x00; + response[4] = 0x00; + response[5] = 0x00; + response[6] = 0x00; + response[7] = 0x00; + fake_tpms_ecu.scheduleNextFrame(0x758, response, 8); + } else if (data[3] == 0x16) { + // TPMS temperatures request. + uint8_t response[8] = {0}; + response[0] = 0x2a; + response[1] = 0x10; // "1" means "first frame in a sequence" + response[2] = 0x07; + response[3] = 0x61; + response[4] = 0x16; + response[5] = 40 + 21; // FL tire temperature: 21ºC + response[6] = 40 + 22; // FR tire temperature + response[7] = 40 + 23; // RR tire temperature + send_data(0x758, response, 8); + + response[0] = 0x2a; + response[1] = 0x21; // "2" means "continuation frame", "1" means "first continuation frame". + response[2] = 40 + 24; // RL tire temperature + response[3] = 0x00; + response[4] = 0x00; + response[5] = 0x00; + response[6] = 0x00; + response[7] = 0x00; + fake_tpms_ecu.scheduleNextFrame(0x758, response, 8); + } + } else if (data_length >= 3 && data[1] == 0x30 && data[2] == 0x00) { + fake_tpms_ecu.handleFrameAck(data[3]); + } + } +} + +void generate_payload(uint16_t pid, uint8_t *payload) { + memset(payload, /* value= */ 0, /* size= */ 8); + + switch (pid) { + case 0xD0: { + // 0xD0 contains the steering wheel angle and data from motion sensors. + + // Pretend that the steering wheel is turned by 123 degrees to the left + int16_t steering_angle_degrees = -123; + int16_t value = steering_angle_degrees * 10; + payload[0] = value & 0xFF; + payload[1] = (value >> 8) & 0xFF; + + // TODO: Verify the scale for this value. The current scale is suspicious, + // but matches real-world testing so far. Need to go to a skid pad to + // verify for sure. + int16_t rotation_clockwise_degrees_per_second = -70; + int16_t rotation_clockwise_radians_per_second_x180 = + (int16_t)(-3.14159 * rotation_clockwise_degrees_per_second); + payload[2] = rotation_clockwise_radians_per_second_x180 & 0xFF; + payload[3] = (rotation_clockwise_radians_per_second_x180 >> 8) & 0xFF; + + // TODO: decode what's in payload[4] and payload[5]. + + float lateral_acceleration_g = 0.3; + // Looks to be encoded in a way that +1 increment is +0.2 m/s2. + payload[6] = (int8_t)(9.80665 * lateral_acceleration_g / 0.2); + + float longitudinal_acceleration_g = 0.2; + // Looks to be encoded in a way that +1 increment is -0.1 m/s2. + // I know, it's strange that they use different scales for lat vs long. + payload[7] = (int8_t)(-9.80665 * longitudinal_acceleration_g / 0.1); + break; + } + + case 0xD1: { + // 0xD1 contains the speed, and the master brake cylinder pressure. + uint16_t speed_m_s = 10; // 36 km/h, ~22.4 mph. + // The encoding seems to be roughly radians per second x100. + // The coefficient was tuned by comparing the values against an external + // GPS from a session where I drove in a straight line on a highway at + // constant speed on cruise control. + uint16_t speed_value = (uint16_t)(speed_m_s * 63.72); + payload[2] = speed_value & 0xFF; + payload[3] = (speed_value >> 8) & 0xFF; + + // The units used for the master brake cylinder pressure are believed to + // be 1/128 kPa. + float brake_pressure_kPa = 1024; + payload[2] = (uint8_t)(brake_pressure_kPa / 128); + break; + } + + case 0xD4: { + // Wheel speed sensors / ABS sensors. + uint16_t value = (uint16_t)(10 * 61); + payload[0] = value & 0xFF; + payload[1] = (value >> 8) & 0xFF; + + value = (uint16_t)(10 * 62); + payload[2] = value & 0xFF; + payload[3] = (value >> 8) & 0xFF; + + value = (uint16_t)(10 * 63); + payload[4] = value & 0xFF; + payload[5] = (value >> 8) & 0xFF; + + value = (uint16_t)(10 * 64); + payload[6] = value & 0xFF; + payload[7] = (value >> 8) & 0xFF; + break; + } + + case 0x140: { + uint8_t accelerator_pedal_percent = 42; + payload[0] = accelerator_pedal_percent * 255 / 100; + + // The clutch pedal has two sensors: + // - 0% and >0% (used here) + // - 100% and <100% (haven't found yet) + // TODO: Find where data from the second sensor is. + bool clutch_down = false; + payload[1] = (clutch_down ? 0x80 : 0x00); + + // RPMs are believed to be encoded with just 14 bits. + uint16_t rpm = 3456; + payload[2] = rpm & 0xFF; + payload[3] = (rpm >> 8) & 0x3F; + break; + } + + case 0x360: { + uint8_t oil_temperature_celsius = 100; + payload[2] = oil_temperature_celsius + 40; + + uint8_t coolant_temperature_celsius = 90; + payload[3] = coolant_temperature_celsius + 40; + break; + } + } +} diff --git a/examples/FakeToyotaGR86/FakeToyotaGR86.ino b/examples/FakeToyotaGR86/FakeToyotaGR86.ino new file mode 100644 index 0000000..1be31b7 --- /dev/null +++ b/examples/FakeToyotaGR86/FakeToyotaGR86.ino @@ -0,0 +1,323 @@ +#include + +// This is a demo program that sends messages over the CAN bus in +// a way that resembles real messages you can receive if you listen +// to messages on the CAN bus of a 2022 Toyota GR86. +// +// DO NOT USE IT IN THE CAN NETWORK OF A REAL VEHICLE as it can cause unexpected +// side effects. +// +// It was tested on Arduino Uno, Arduino Micro, Adafruit Feather nRF52832 and +// Adafruit ItsyBitsy nRF52840 Express, and should be trivial to tweak to +// support pretty much any other board with SPI. +// +// Connections: +// MCP | BOARD +// INT | Not used, can connect to Pin 9 +// SCK | SCK +// SI | MO +// SO | MI +// CS | Pin 7 +// GND | GND +// VCC | 3.3V + +const int CS_PIN = 7; +const int IRQ_PIN = 9; +const int QUARTZ_MHZ = 16; // Some MCP2515 boards have 8 MHz quartz. +const int SPI_MHZ = 8; + +void setup() { + Serial.begin(115200); + + uint32_t startTimeMs = millis(); + while (!Serial && millis() - startTimeMs < 1000); + if (!Serial) { + Serial.println("Started!"); + } else { + // Whatever, noone's going to see anyways. + } + + CAN.setClockFrequency(QUARTZ_MHZ * 1E6); + CAN.setSPIFrequency(SPI_MHZ * 1E6); + CAN.setPins(CS_PIN, IRQ_PIN); + + // Subaru BRZ uses a 500k baud rate. + while (!CAN.begin(500000)) { + Serial.println("Failed to connect to the CAN controller!"); + delay(1000); + } + + Serial.println("CAN controller connected"); +} + +class FakeTpmsEcu { +public: + FakeTpmsEcu() { + has_continuation_frame = false; + original_frame_acked = false; + } + + void scheduleNextFrame(uint16_t pid, uint8_t *data, uint8_t len) { + if (has_continuation_frame) { + Serial.print("Scheduling new frame, even though there is one pending already. Original was "); + if (original_frame_acked) { + Serial.println("acked."); + } else { + Serial.println("NOT acked."); + } + } + has_continuation_frame = true; + original_frame_acked = false; + next_frame_pid = pid; + memcpy(next_frame_data, data, len); + next_frame_length = len; + } + + void handleFrameAck(uint8_t separation_time_millis = 0) { + if (!has_continuation_frame) { + return; + } + + original_frame_acked = true; + next_frame_timestamp_millis = millis() + separation_time_millis; + } + + void sendNextFrameIfNeeded() { + if (!has_continuation_frame || !original_frame_acked) { + return; + } + + // Unsigned math magic to check if "time_diff" is "negative": + unsigned long time_diff = millis() - next_frame_timestamp_millis; + if (time_diff >> (8 * sizeof(time_diff) - 1)) { + return; + } + + send_data(next_frame_pid, next_frame_data, next_frame_length); + has_continuation_frame = false; + original_frame_acked = false; + } + +private: + bool has_continuation_frame; + uint16_t next_frame_pid; + uint8_t next_frame_data[8]; + uint8_t next_frame_length; + + bool original_frame_acked; + unsigned long next_frame_timestamp_millis; +} fake_tpms_ecu; + +// Forward declaration for a helper. +void try_to_receive_data(); +void generate_payload(uint16_t pid, uint8_t *payload); +boolean send_data(uint16_t pid, uint8_t *payload, uint8_t len); + +// TODO: Refactor the loop() function. It should first try to see if there's anything to send, +// then if there's anything to receive. +void loop() { + // Many PIDs are sent 50 times per second. + uint16_t num_cycles_per_second = 50; + + // 0x40 and 0x41 are intentionally duplicated in this array as they are sent 100 times + // per second (double that for other PIDs) in the real car. + uint16_t pids[] = { + // These are sent 100 times per second: + 0x40, 0x41, + // These are sent 50 times per second, part 1: + 0x118, 0x138, 0x139, 0x13B, 0x13C, + // These are sent 100 times per second (duplicates): + 0x40, 0x41, + // These are sent 50 times per second, part 2: + 0x143, 0x146, + // TODO: These are actually sent less frequently than 50 times per second: + 0x241, // 20 times per second + 0x345, // 10 times per second + }; + uint16_t num_messages_per_cycle = sizeof(pids) / sizeof(pids[0]); + uint16_t num_messages_per_second = + num_cycles_per_second * num_messages_per_cycle; + + unsigned long first_message_sent_micros = micros(); + unsigned long num_messages_sent = 0; + for (int i = 0; i < num_messages_per_cycle; i++, num_messages_sent++) { + uint16_t pid = pids[i]; + uint8_t payload[8]; + generate_payload(pid, payload); + + if (!send_data(pid, payload, 8)) { + Serial.println("Failed to send a message"); + } + + unsigned long next_message_time_micros = + first_message_sent_micros + + (num_messages_sent * 1000000) / num_messages_per_second; + if ((long)(micros() - next_message_time_micros) < 0) { + try_to_receive_data(); + fake_tpms_ecu.sendNextFrameIfNeeded(); + delayMicroseconds(10); + } + } +} + +boolean send_data(uint16_t id, uint8_t *payload, uint8_t len) { + if (!CAN.beginPacket(id)) { + Serial.println("beginPacket() failed."); + return false; + } + + CAN.write(payload, len); + if (!CAN.endPacket()) { + Serial.println("endPacket() failed."); + return false; + } + + return true; +} + +void try_to_receive_data() { + int packet_size = CAN.parsePacket(); + if (packet_size <= 0) { + return; + } + + if (CAN.packetRtr()) { + // Ignore RTRs. + return; + } + + uint32_t id = CAN.packetId(); + uint8_t data[8] = {0}; + int data_length = 0; + while (data_length < packet_size && data_length < sizeof(data)) { + int byte_read = CAN.read(); + if (byte_read == -1) { + break; + } + + data[data_length++] = byte_read; + } + + if (id == 0x750 && data_length >= 1 && data[0] == 0x2a) { + if (data_length >= 3 && data[1] == 0x02 && data[2] == 0x21) { + if (data[3] == 0x30) { + // TPMS pressures request. + uint8_t response[8] = {0}; + response[0] = 0x2a; + response[1] = 0x10; // "1" means "first frame in a sequence" + response[2] = 0x07; + response[3] = 0x61; + response[4] = 0x30; + response[5] = 0xAB; // FL tire pressure + response[6] = 0xAC; // FR tire pressure + response[7] = 0xAD; // RR tire pressure + send_data(0x758, response, 8); + + response[0] = 0x2a; + response[1] = 0x21; // "2" means "continuation frame", "1" means "first continuation frame". + response[2] = 0xAE; // RL tire pressure + response[3] = 0x00; + response[4] = 0x00; + response[5] = 0x00; + response[6] = 0x00; + response[7] = 0x00; + fake_tpms_ecu.scheduleNextFrame(0x758, response, 8); + } else if (data[3] == 0x16) { + // TPMS temperatures request. + uint8_t response[8] = {0}; + response[0] = 0x2a; + response[1] = 0x10; // "1" means "first frame in a sequence" + response[2] = 0x07; + response[3] = 0x61; + response[4] = 0x16; + response[5] = 40 + 21; // FL tire temperature: 21ºC + response[6] = 40 + 22; // FR tire temperature + response[7] = 40 + 23; // RR tire temperature + send_data(0x758, response, 8); + + response[0] = 0x2a; + response[1] = 0x21; // "2" means "continuation frame", "1" means "first continuation frame". + response[2] = 40 + 24; // RL tire temperature + response[3] = 0x00; + response[4] = 0x00; + response[5] = 0x00; + response[6] = 0x00; + response[7] = 0x00; + fake_tpms_ecu.scheduleNextFrame(0x758, response, 8); + } + } else if (data_length >= 3 && data[1] == 0x30 && data[2] == 0x00) { + fake_tpms_ecu.handleFrameAck(data[3]); + } + } +} + +void generate_payload(uint16_t pid, uint8_t *payload) { + memset(payload, /* value= */ 0, /* size= */ 8); + + switch (pid) { + case 0x40: { + uint8_t accelerator_pedal_percent = 42; + payload[4] = accelerator_pedal_percent * 255 / 100; + payload[5] = payload[4]; + payload[6] = payload[4]; + + // The clutch pedal has two sensors: + // - 0% and >0% (used here) + // - 100% and <100% (haven't found yet) + // TODO: Find where data from the second sensor is. + bool clutch_down = false; + payload[1] = (clutch_down ? 0x80 : 0x00); + + // RPMs are believed to be encoded with just 14 bits. + uint16_t rpm = 3456; + payload[2] = rpm & 0xFF; + payload[3] = (rpm >> 8) & 0x3F; + break; + } + + + case 0x138: { + // Pretend that the steering wheel is turned by 123 degrees to the left + int16_t steering_angle_degrees = -123; + int16_t steering_value = steering_angle_degrees * 10; + payload[2] = steering_value & 0xFF; + payload[3] = (steering_value >> 8) & 0xFF; + + // TODO: Verify the scale for this value. The current scale is suspicious, + // but matches real-world testing so far. Need to go to a skid pad to + // verify for sure. + float yaw_rate_degrees_per_second = -12.3; + int16_t yaw_rate_value = (int16_t)(-yaw_rate_degrees_per_second / 0.2725); + payload[4] = yaw_rate_value & 0xFF; + payload[5] = (yaw_rate_value >> 8) & 0xFF; + break; + } + + case 0x139: { + uint16_t speed_m_s = 10; // 36 km/h, ~22.4 mph. + // The encoding seems to be roughly radians per second x100. + // The coefficient was tuned by comparing the values against an external + // GPS from a session where I drove in a straight line on a highway at + // constant speed on cruise control. + uint16_t speed_value = (uint16_t)(speed_m_s * 63.72); + payload[2] = speed_value & 0xFF; + payload[3] = (speed_value >> 8) & 0xFF; + + // The units used for the master brake cylinder pressure are believed to + // be 1/128 kPa. + float brake_pressure_kPa = 1024; + payload[4] = 0x0C; + payload[5] = (uint8_t)(brake_pressure_kPa / 128); + break; + } + + case 0x345: { + uint8_t oil_temperature_celsius = 100; + payload[3] = oil_temperature_celsius + 40; + + uint8_t coolant_temperature_celsius = 90; + payload[4] = coolant_temperature_celsius + 40; + break; + } + } +} diff --git a/examples/PidHistogram/PidHistogram.ino b/examples/PidHistogram/PidHistogram.ino new file mode 100644 index 0000000..8c11db2 --- /dev/null +++ b/examples/PidHistogram/PidHistogram.ino @@ -0,0 +1,190 @@ +#include + +// This is a demo program that listens to messages on the CAN bus and +// periodically prints a list of PIDs observed, with counts how many times they +// were observed. +// +// It was tested on Arduino Uno, Arduino Micro, Adafruit Feather nRF52832 and +// Adafruit ItsyBitsy nRF52840 Express, and should be trivial to tweak to +// support pretty much any other board with SPI. +// +// Connections: +// MCP | BOARD +// INT | Not used, can connect to Pin 9 +// SCK | SCK +// SI | MO +// SO | MI +// CS | Pin 7 +// GND | GND +// VCC | 3.3V + +const int CS_PIN = 7; +const int IRQ_PIN = 9; +const int QUARTZ_MHZ = 16; // Some MCP2515 boards have 8 MHz quartz. +const int SPI_MHZ = 8; + +// Defines the maximum number of unique CAN PIDs to keep track of between +// printing histrogram reports. Too many will make the output unreadable, too +// few will not give you the full picture. +const uint16_t MAX_NUM_CAN_PIDS = 32; + +// Interval in seconds between printing reports. +const uint32_t REPORT_INTERVAL_SECONDS = 1; + +struct pid_entry { + uint32_t pid; + uint32_t num_received_since_last_report; +}; +pid_entry observed_pids[MAX_NUM_CAN_PIDS]; // Keep sorted by PID. +uint16_t num_unique_observed_pids = 0; + +uint32_t total_received_since_last_report; +uint32_t last_report_printed_ms; + +void setup() { + Serial.begin(115200); + + uint32_t startTimeMs = millis(); + while (!Serial); + + Serial.println("Started!"); + + CAN.setClockFrequency(QUARTZ_MHZ * 1E6); + CAN.setSPIFrequency(SPI_MHZ * 1E6); + CAN.setPins(CS_PIN, IRQ_PIN); + + // Subaru BRZ uses a 500k baud rate. + while (!CAN.begin(500000)) { + Serial.println("Failed to connect to the CAN controller!"); + delay(1000); + } + + Serial.println("CAN controller connected"); +} + +// Forward declarations for helper functions. +void handle_message(uint32_t pid); +void print_report(); + +void loop() { + if (millis() - last_report_printed_ms > REPORT_INTERVAL_SECONDS * 1000) { + print_report(); + last_report_printed_ms = millis(); + } + + int packet_size = CAN.parsePacket(); + if (packet_size <= 0) { + return; + } + + if (CAN.packetRtr()) { + // Ignore RTRs for now. + return; + } + + handle_message(CAN.packetId()); +} + +void handle_message(uint32_t pid) { + total_received_since_last_report++; + + pid_entry *after_last = observed_pids + num_unique_observed_pids; + + // Use binary search to find the location where this PID should go in the + // sorted 'observed_pids' list. + // Unfortunately, it's not simple to use std::lower_bound() on Ardiuno AVR + // boards, so I had to recall some CS classes. Hopefully, no bugs :) + pid_entry *insertion_location = nullptr; + { + pid_entry *start_ptr = observed_pids; + pid_entry *end_ptr = after_last; + + while (start_ptr != end_ptr) { + if (start_ptr->pid >= pid) { + insertion_location = start_ptr; + break; + } + + // We can now assume start_ptr->pid < pid. + + if (start_ptr + 1 == end_ptr) { + // Narrowed the range down to one element that's smaller than pid. + insertion_location = start_ptr + 1; + break; + } + + // Otherwise, keep narrowing down. This is guaranteed to make progress + // as start_pid + 1 < end_ptr, and so (end_ptr - start_ptr) >= 2. + pid_entry *middle = start_ptr + (end_ptr - start_ptr) / 2; + if (middle->pid <= pid) { + start_ptr = middle; + } else { + end_ptr = middle; + } + } + + // Handle the special case of an empty list. + if (insertion_location == nullptr) { + insertion_location = start_ptr; + } + } + + if (insertion_location < after_last && insertion_location->pid == pid) { + // Found a match! + insertion_location->num_received_since_last_report++; + return; + } else if (num_unique_observed_pids == MAX_NUM_CAN_PIDS) { + // The list is already full, ignoring this PID. + return; + } + + // Move elements after insert_to_index by one element. Need to use + // memmove() instead of memcpy() as src and dst overlap. + memmove( + /* destination= */ insertion_location + 1, + /* source= */ insertion_location, + /* num_bytes= */ (after_last - insertion_location) * sizeof(pid_entry)); + + // Finally, insert the new entry for this PID. + num_unique_observed_pids++; + insertion_location->pid = pid; + insertion_location->num_received_since_last_report = 1; +} + +void print_report() { + if (num_unique_observed_pids == 0) { + Serial.println("No messages received!"); + return; + } + + Serial.print("Received "); + Serial.print(total_received_since_last_report); + Serial.print(" messages ("); + Serial.print(num_unique_observed_pids); + Serial.println(" unique PIDs) since last report:"); + + for (uint16_t i = 0; i < num_unique_observed_pids; i++) { + pid_entry *entry = &observed_pids[i]; + Serial.print(" PID: "); + Serial.print(entry->pid); + Serial.print(" (0x"); + Serial.print(entry->pid, HEX); + Serial.print(") received "); + Serial.print(entry->num_received_since_last_report); + Serial.println(" times."); + + // Reset the count for the next report. + entry->num_received_since_last_report = 0; + } + Serial.print("Interval between reports: "); + Serial.print(REPORT_INTERVAL_SECONDS); + Serial.print(" second"); + if (REPORT_INTERVAL_SECONDS != 1) { + Serial.print("s"); + } + Serial.println("."); + Serial.println(""); + + num_unique_observed_pids = 0; + total_received_since_last_report = 0; +} diff --git a/examples/ReceiverStressTest/ReceiverStressTest.ino b/examples/ReceiverStressTest/ReceiverStressTest.ino new file mode 100644 index 0000000..fac4f85 --- /dev/null +++ b/examples/ReceiverStressTest/ReceiverStressTest.ino @@ -0,0 +1,125 @@ +#include + +// This is a demo program that listens to messages on the CAN bus sent by a +// device using the code from the SenderStressTest example, and verifies that +// correct bytes were received. +// +// It was tested on Arduino Uno, Arduino Micro, Adafruit Feather nRF52832 and +// Adafruit ItsyBitsy nRF52840 Express, and should be trivial to tweak to +// support pretty much any other board with SPI. +// +// Connections: +// MCP | BOARD +// INT | Not used, can connect to Pin 9 +// SCK | SCK +// SI | MO +// SO | MI +// CS | Pin 7 +// GND | GND +// VCC | 3.3V + +const int CS_PIN = 7; +const int IRQ_PIN = 9; +const int QUARTZ_MHZ = 16; // Some MCP2515 boards have 8 MHz quartz. +const int SPI_MHZ = 10; +const long BAUD_RATE = 500 * 1E3; // 500k baud rate. + +void setup() { + Serial.begin(115200); + + uint32_t startTimeMs = millis(); + while (!Serial && millis() - startTimeMs < 5000); + if (!Serial) { + Serial.println("Started!"); + } else { + // Whatever, noone's going to see anyways. + } + + CAN.setClockFrequency(QUARTZ_MHZ * 1E6); + CAN.setSPIFrequency(SPI_MHZ * 1E6); + CAN.setPins(CS_PIN, IRQ_PIN); + + while (!CAN.begin(BAUD_RATE)) { + Serial.println("Failed to connect to the CAN controller!"); + delay(1000); + } + + Serial.println("CAN controller connected"); +} + +// Interval in seconds between printing reports. +const uint32_t REPORT_INTERVAL_SECONDS = 1; + +uint32_t last_stats_ms = millis(); +uint32_t num_messages_received; +uint32_t num_errors; + +void loop() { + if (millis() - last_stats_ms > REPORT_INTERVAL_SECONDS * 1000) { + Serial.print("Received in "); + Serial.print(REPORT_INTERVAL_SECONDS); + Serial.print(" seconds:\t"); + Serial.print(num_messages_received); + if (num_messages_received > 0) { + Serial.print(", errors:\t"); + Serial.print(num_errors); + Serial.print(" ("); + Serial.print(100.0 * num_errors / num_messages_received); + Serial.print("%)"); + } + Serial.println(""); + + last_stats_ms = millis(); + num_messages_received = 0; + num_errors = 0; + } + + int packet_size = CAN.parsePacket(); + if (packet_size <= 0) { + return; + } + + if (CAN.packetRtr()) { + // Ignore RTRs for now. + return; + } + + num_messages_received++; + + uint32_t pid = CAN.packetId(); + uint8_t data[8]; + int data_length = 0; + while (data_length < packet_size && data_length < sizeof(data)) { + int byte_read = CAN.read(); + if (byte_read == -1) { + break; + } + + data[data_length++] = byte_read; + } + + uint8_t expected_payload[8] = + { 0x12, 0x34, 0x56, 0x78, 0x9a, 0xbc, 0xde, pid }; + bool mismatch = data_length != 8; + for (int i = 0; i < 8; i++) { + if (data[i] != expected_payload[i]) { + mismatch = true; + } + } + + if (mismatch) { + num_errors++; + Serial.print("Unexpected data received! pid = "); + Serial.print(pid); + Serial.print(" (0x"); + Serial.print(pid, HEX); + Serial.print("), data: "); + for (int i = 0; i < data_length; i++) { + if (i != 0) { + Serial.print(" "); + } + Serial.print(data[i], HEX); + } + Serial.println(); + } +} diff --git a/examples/SenderStressTest/SenderStressTest.ino b/examples/SenderStressTest/SenderStressTest.ino new file mode 100644 index 0000000..6a7a8b4 --- /dev/null +++ b/examples/SenderStressTest/SenderStressTest.ino @@ -0,0 +1,89 @@ +#include + +// This is a demo program that sends as many messages over the CAN bus as it +// can, and can be used as a stress test for the hardware and a CAN receiver +// device. +// +// DO NOT USE IT IN THE CAN NETWORK OF A REAL VEHICLE as it can cause unexpected +// side effects. +// +// It was tested on Arduino Uno, Arduino Micro, Adafruit Feather nRF52832 and +// Adafruit ItsyBitsy nRF52840 Express, and should be trivial to tweak to +// support pretty much any other board with SPI. +// +// Connections: +// MCP | BOARD +// INT | Not used, can connect to Pin 9 +// SCK | SCK +// SI | MO +// SO | MI +// CS | Pin 7 +// GND | GND +// VCC | 3.3V + +const int CS_PIN = 7; +const int IRQ_PIN = 9; +const int QUARTZ_MHZ = 16; // Some MCP2515 boards have 8 MHz quartz. +const int SPI_MHZ = 10; +const long BAUD_RATE = 500 * 1E3; // 500k baud rate. + +void setup() { + Serial.begin(115200); + + uint32_t startTimeMs = millis(); + while (!Serial && millis() - startTimeMs < 5000); + if (!Serial) { + Serial.println("Started!"); + } else { + // Whatever, noone's going to see anyways. + } + + CAN.setClockFrequency(QUARTZ_MHZ * 1E6); + CAN.setSPIFrequency(SPI_MHZ * 1E6); + CAN.setPins(CS_PIN, IRQ_PIN); + + while (!CAN.begin(BAUD_RATE)) { + Serial.println("Failed to connect to the CAN controller!"); + delay(1000); + } + + Serial.println("CAN controller connected"); +} + +// Interval in seconds between printing reports. +const uint32_t REPORT_INTERVAL_SECONDS = 1; + +uint32_t last_stats_ms = millis(); +uint32_t num_messages_sent = 0; +uint32_t num_errors = 0; + +void loop() { + for (uint16_t pid = 1; pid <= 0xff; pid++) { + uint32_t current_time_ms = millis(); + if (current_time_ms - last_stats_ms > REPORT_INTERVAL_SECONDS * 1000) { + Serial.print("Packets sent over 1 second:\t"); + Serial.print(num_messages_sent); + Serial.print(", errors:\t"); + Serial.println(num_errors); + last_stats_ms = current_time_ms; + num_messages_sent = 0; + num_errors = 0; + } + + if (!CAN.beginPacket(pid)) { + Serial.println("beginPacket() failed."); + num_errors++; + continue; + } + + uint8_t payload[8] = { 0x12, 0x34, 0x56, 0x78, 0x9a, 0xbc, 0xde, pid }; + CAN.write(payload, 8); + if (CAN.endPacket()) { + num_messages_sent++; + } else { + Serial.println("endPacket() failed."); + num_errors++; + continue; + } + } +} diff --git a/library.properties b/library.properties index acf321f..4631c88 100644 --- a/library.properties +++ b/library.properties @@ -1,10 +1,10 @@ name=CAN version=0.3.1 -author=Sandeep Mistry -maintainer=Sandeep Mistry +author=Sandeep Mistry with patches by Timur Iskhodzhanov +maintainer=Timur Iskhodzhanov sentence=An Arduino library for sending and receiving data using CAN bus. paragraph=Supports Microchip MCP2515 based boards/shields and the Espressif ESP32's built-in SJA1000 compatible CAN controller. category=Communication -url=https://github.com/sandeepmistry/arduino-CAN +url=https://github.com/timurrrr/arduino-CAN architectures=* includes=CAN.h diff --git a/src/MCP2515.cpp b/src/MCP2515.cpp index aa46051..1d451ba 100644 --- a/src/MCP2515.cpp +++ b/src/MCP2515.cpp @@ -15,16 +15,19 @@ #define REG_CNF1 0x2a #define REG_CANINTE 0x2b + +// Whenever changing the CANINTF register, use BIT MODIFY instead of WRITE. #define REG_CANINTF 0x2c #define FLAG_RXnIE(n) (0x01 << n) #define FLAG_RXnIF(n) (0x01 << n) #define FLAG_TXnIF(n) (0x04 << n) -#define REG_RXFnSIDH(n) (0x00 + (n * 4)) -#define REG_RXFnSIDL(n) (0x01 + (n * 4)) -#define REG_RXFnEID8(n) (0x02 + (n * 4)) -#define REG_RXFnEID0(n) (0x03 + (n * 4)) +// There is a 4-register gap between RXF2EID0 and RXF3SIDH. +#define REG_RXFnSIDH(n) (0x00 + ((n + (n >= 3)) * 4)) +#define REG_RXFnSIDL(n) (0x01 + ((n + (n >= 3)) * 4)) +#define REG_RXFnEID8(n) (0x02 + ((n + (n >= 3)) * 4)) +#define REG_RXFnEID0(n) (0x03 + ((n + (n >= 3)) * 4)) #define REG_RXMnSIDH(n) (0x20 + (n * 0x04)) #define REG_RXMnSIDL(n) (0x21 + (n * 0x04)) @@ -51,6 +54,7 @@ #define FLAG_SRR 0x10 #define FLAG_RTR 0x40 #define FLAG_EXIDE 0x08 +#define FLAG_RXB0CTRL_BUKT 0x04 #define FLAG_RXM0 0x20 #define FLAG_RXM1 0x40 @@ -69,7 +73,7 @@ MCP2515Class::~MCP2515Class() { } -int MCP2515Class::begin(long baudRate) +int MCP2515Class::begin(long baudRate, bool stayInConfigurationMode) { CANControllerClass::begin(baudRate); @@ -80,8 +84,7 @@ int MCP2515Class::begin(long baudRate) reset(); - writeRegister(REG_CANCTRL, 0x80); - if (readRegister(REG_CANCTRL) != 0x80) { + if (!switchToConfigurationMode()) { return 0; } @@ -137,12 +140,15 @@ int MCP2515Class::begin(long baudRate) writeRegister(REG_CANINTE, FLAG_RXnIE(1) | FLAG_RXnIE(0)); writeRegister(REG_BFPCTRL, 0x00); writeRegister(REG_TXRTSCTRL, 0x00); + + // A combination of RXM1 and RXM0 is "Turns mask/filters off; receives any message". writeRegister(REG_RXBnCTRL(0), FLAG_RXM1 | FLAG_RXM0); writeRegister(REG_RXBnCTRL(1), FLAG_RXM1 | FLAG_RXM0); - writeRegister(REG_CANCTRL, 0x00); - if (readRegister(REG_CANCTRL) != 0x00) { - return 0; + if (!stayInConfigurationMode) { + if (!switchToNormalMode()) { + return 0; + } } return 1; @@ -161,86 +167,147 @@ int MCP2515Class::endPacket() return 0; } + // Currently, we don't need to use more than one TX buffer as we always wait + // until the data has been fully transmitted. For the same reason, we don't + // need to check in the beginning whether there is any data in the TX buffer + // pending transmission. The performance can be optimized by utilizing all + // three TX buffers of the MCP2515, but this will come at extra complexity. int n = 0; + // Pre-calculate values for all registers so that we can write them + // sequentially via the LOAD TX BUFFER instruction. + // TX BUFFER + uint8_t regSIDH; + uint8_t regSIDL; + uint8_t regEID8; + uint8_t regEID0; if (_txExtended) { - writeRegister(REG_TXBnSIDH(n), _txId >> 21); - writeRegister(REG_TXBnSIDL(n), (((_txId >> 18) & 0x07) << 5) | FLAG_EXIDE | ((_txId >> 16) & 0x03)); - writeRegister(REG_TXBnEID8(n), (_txId >> 8) & 0xff); - writeRegister(REG_TXBnEID0(n), _txId & 0xff); + regSIDH = _txId >> 21; + regSIDL = + (((_txId >> 18) & 0x07) << 5) | FLAG_EXIDE | ((_txId >> 16) & 0x03); + regEID8 = (_txId >> 8) & 0xff; + regEID0 = _txId & 0xff; } else { - writeRegister(REG_TXBnSIDH(n), _txId >> 3); - writeRegister(REG_TXBnSIDL(n), _txId << 5); - writeRegister(REG_TXBnEID8(n), 0x00); - writeRegister(REG_TXBnEID0(n), 0x00); + regSIDH = _txId >> 3; + regSIDL = _txId << 5; + regEID8 = 0x00; + regEID0 = 0x00; } + uint8_t regDLC; if (_txRtr) { - writeRegister(REG_TXBnDLC(n), 0x40 | _txLength); + regDLC = 0x40 | _txLength; } else { - writeRegister(REG_TXBnDLC(n), _txLength); + regDLC = _txLength; + } - for (int i = 0; i < _txLength; i++) { - writeRegister(REG_TXBnD0(n) + i, _txData[i]); + SPI.beginTransaction(_spiSettings); + digitalWrite(_csPin, LOW); + // Send the LOAD TX BUFFER instruction to sequentially write registers, + // starting from TXBnSIDH(n). + SPI.transfer(0b01000000 | (n << 1)); + SPI.transfer(regSIDH); + SPI.transfer(regSIDL); + SPI.transfer(regEID8); + SPI.transfer(regEID0); + SPI.transfer(regDLC); + if (!_txRtr) { + for (uint8_t i = 0; i < _txLength; i++) { + SPI.transfer(_txData[i]); } } + digitalWrite(_csPin, HIGH); + SPI.endTransaction(); - writeRegister(REG_TXBnCTRL(n), 0x08); + SPI.beginTransaction(_spiSettings); + digitalWrite(_csPin, LOW); + // Send the RTS instruction, which sets the TXREQ (TXBnCTRL[3]) bit for the + // respective buffer, and clears the ABTF, MLOA and TXERR bits. + SPI.transfer(0b10000000 | (1 << n)); + digitalWrite(_csPin, HIGH); + SPI.endTransaction(); + // Wait until the transmission completes, or gets aborted. + // Transmission is pending while TXREQ (TXBnCTRL[3]) bit is set. bool aborted = false; - while (readRegister(REG_TXBnCTRL(n)) & 0x08) { + // Read the TXERR (TXBnCTRL[4]) bit to check for errors. if (readRegister(REG_TXBnCTRL(n)) & 0x10) { - // abort - aborted = true; - + // Abort on errors by setting the ABAT bit. The MCP2515 will should the + // TXREQ bit shortly. We'll keep running the loop until TXREQ is cleared. modifyRegister(REG_CANCTRL, 0x10, 0x10); + aborted = true; } yield(); } if (aborted) { - // clear abort command + // Reset the ABAT bit. modifyRegister(REG_CANCTRL, 0x10, 0x00); } + // Clear the pending TX interrupt, if any. modifyRegister(REG_CANINTF, FLAG_TXnIF(n), 0x00); + // Report failure if either of the ABTF, MLOA or TXERR bits are set. + // TODO: perhaps we can reuse the last value read from this register // earlier? return (readRegister(REG_TXBnCTRL(n)) & 0x70) ? 0 : 1; } int MCP2515Class::parsePacket() { - int n; - - uint8_t intf = readRegister(REG_CANINTF); + SPI.beginTransaction(_spiSettings); + digitalWrite(_csPin, LOW); + SPI.transfer(0xb0); // RX STATUS + uint8_t rxStatus = SPI.transfer(0x00); + digitalWrite(_csPin, HIGH); + SPI.endTransaction(); - if (intf & FLAG_RXnIF(0)) { + int n; + if (rxStatus & 0x40) { n = 0; - } else if (intf & FLAG_RXnIF(1)) { + } else if (rxStatus & 0x80) { n = 1; } else { _rxId = -1; _rxExtended = false; _rxRtr = false; + _rxDlc = 0; + _rxIndex = 0; _rxLength = 0; return 0; } - _rxExtended = (readRegister(REG_RXBnSIDL(n)) & FLAG_IDE) ? true : false; - - uint32_t idA = ((readRegister(REG_RXBnSIDH(n)) << 3) & 0x07f8) | ((readRegister(REG_RXBnSIDL(n)) >> 5) & 0x07); + SPI.beginTransaction(_spiSettings); + digitalWrite(_csPin, LOW); + // Send READ RX BUFFER instruction to sequentially read registers, starting + // from RXBnSIDH(n). + SPI.transfer(0b10010000 | (n * 0x04)); + uint8_t regSIDH = SPI.transfer(0x00); + uint8_t regSIDL = SPI.transfer(0x00); + _rxExtended = (regSIDL & FLAG_IDE) ? true : false; + + // We could just skip the extended registers for standard frames, but that + // would actually add more overhead, and increase complexity. + uint8_t regEID8 = SPI.transfer(0x00); + uint8_t regEID0 = SPI.transfer(0x00); + uint8_t regDLC = SPI.transfer(0x00); + uint32_t idA = (regSIDH << 3) | (regSIDL >> 5); if (_rxExtended) { - uint32_t idB = (((uint32_t)(readRegister(REG_RXBnSIDL(n)) & 0x03) << 16) & 0x30000) | ((readRegister(REG_RXBnEID8(n)) << 8) & 0xff00) | readRegister(REG_RXBnEID0(n)); + uint32_t idB = + ((uint32_t)(regSIDL & 0x03) << 16) + | ((uint32_t)regEID8 << 8) + | regEID0; _rxId = (idA << 18) | idB; - _rxRtr = (readRegister(REG_RXBnDLC(n)) & FLAG_RTR) ? true : false; + _rxRtr = (regDLC & FLAG_RTR) ? true : false; } else { _rxId = idA; - _rxRtr = (readRegister(REG_RXBnSIDL(n)) & FLAG_SRR) ? true : false; + _rxRtr = (regSIDL & FLAG_SRR) ? true : false; } - _rxDlc = readRegister(REG_RXBnDLC(n)) & 0x0f; + + _rxDlc = regDLC & 0x0f; _rxIndex = 0; if (_rxRtr) { @@ -248,13 +315,16 @@ int MCP2515Class::parsePacket() } else { _rxLength = _rxDlc; - for (int i = 0; i < _rxLength; i++) { - _rxData[i] = readRegister(REG_RXBnD0(n) + i); + // Get the data. + for (uint8_t i = 0; i < _rxLength; i++) { + _rxData[i] = SPI.transfer(0x00); } } - modifyRegister(REG_CANINTF, FLAG_RXnIF(n), 0x00); - + // Don't need to unset the RXnIF(n) flag as this is done automatically when + // setting the CS high after a READ RX BUFFER instruction. + digitalWrite(_csPin, HIGH); + SPI.endTransaction(); return _rxDlc; } @@ -282,12 +352,17 @@ int MCP2515Class::filter(int id, int mask) // config mode writeRegister(REG_CANCTRL, 0x80); + // TODO: The requested mode must be verified by reading the OPMODE[2:0] bits (CANSTAT[7:5]) if (readRegister(REG_CANCTRL) != 0x80) { return 0; } for (int n = 0; n < 2; n++) { // standard only + // TODO: This doesn't look correct. According to the datasheet, the RXM0 and + // RMX1 should either both be unset (in which case filters are active), or + // both be unset (in which case all filters are ignored). + // Either way, it's unclear why we write to the same register twice here. writeRegister(REG_RXBnCTRL(n), FLAG_RXM0); writeRegister(REG_RXBnCTRL(n), FLAG_RXM0); @@ -306,6 +381,7 @@ int MCP2515Class::filter(int id, int mask) // normal mode writeRegister(REG_CANCTRL, 0x00); + // TODO: The requested mode must be verified by reading the OPMODE[2:0] bits (CANSTAT[7:5]) if (readRegister(REG_CANCTRL) != 0x00) { return 0; } @@ -313,6 +389,51 @@ int MCP2515Class::filter(int id, int mask) return 1; } +boolean MCP2515Class::setFilterRegisters( + uint16_t mask0, uint16_t filter0, uint16_t filter1, + uint16_t mask1, uint16_t filter2, uint16_t filter3, uint16_t filter4, uint16_t filter5, + bool allowRollover) +{ + mask0 &= 0x7ff; + filter0 &= 0x7ff; + filter1 &= 0x7ff; + mask1 &= 0x7ff; + filter2 &= 0x7ff; + filter3 &= 0x7ff; + filter4 &= 0x7ff; + filter5 &= 0x7ff; + + if (!switchToConfigurationMode()) { + return false; + } + + writeRegister(REG_RXBnCTRL(0), allowRollover ? FLAG_RXB0CTRL_BUKT : 0); + writeRegister(REG_RXBnCTRL(1), 0); + for (int n = 0; n < 2; n++) { + uint8_t mask = (n == 0) ? mask0 : mask1; + writeRegister(REG_RXMnSIDH(n), mask >> 3); + writeRegister(REG_RXMnSIDL(n), mask << 5); + writeRegister(REG_RXMnEID8(n), 0); + writeRegister(REG_RXMnEID0(n), 0); + } + + uint8_t filter_array[6] = + {filter0, filter1, filter2, filter3, filter4, filter5}; + for (int n = 0; n < 6; n++) { + uint8_t id = filter_array[n]; + writeRegister(REG_RXFnSIDH(n), id >> 3); + writeRegister(REG_RXFnSIDL(n), id << 5); + writeRegister(REG_RXFnEID8(n), 0); + writeRegister(REG_RXFnEID0(n), 0); + } + + if (!switchToNormalMode()) { + return false; + } + + return true; +} + int MCP2515Class::filterExtended(long id, long mask) { id &= 0x1FFFFFFF; @@ -320,12 +441,17 @@ int MCP2515Class::filterExtended(long id, long mask) // config mode writeRegister(REG_CANCTRL, 0x80); + // TODO: The requested mode must be verified by reading the OPMODE[2:0] bits (CANSTAT[7:5]) if (readRegister(REG_CANCTRL) != 0x80) { return 0; } for (int n = 0; n < 2; n++) { // extended only + // TODO: This doesn't look correct. According to the datasheet, the RXM0 and + // RMX1 should either both be unset (in which case filters are active), or + // both be unset (in which case all filters are ignored). + // Either way, it's unclear why we write to the same register twice here. writeRegister(REG_RXBnCTRL(n), FLAG_RXM1); writeRegister(REG_RXBnCTRL(n), FLAG_RXM1); @@ -344,6 +470,7 @@ int MCP2515Class::filterExtended(long id, long mask) // normal mode writeRegister(REG_CANCTRL, 0x00); + // TODO: The requested mode must be verified by reading the OPMODE[2:0] bits (CANSTAT[7:5]) if (readRegister(REG_CANCTRL) != 0x00) { return 0; } @@ -351,9 +478,24 @@ int MCP2515Class::filterExtended(long id, long mask) return 1; } +bool MCP2515Class::switchToNormalMode() { + // TODO: Should we use modifyRegister(REG_CANCTRL, 0xe0, 0x00) here instead? + writeRegister(REG_CANCTRL, 0x00); + return (readRegister(REG_CANCTRL) & 0xe0) == 0x00; +} + +bool MCP2515Class::switchToConfigurationMode() +{ + // TODO: Should we use modifyRegister(REG_CANCTRL, 0xe0, 0x80) here instead? + writeRegister(REG_CANCTRL, 0x80); + return (readRegister(REG_CANCTRL) & 0xe0) == 0x80; +} + int MCP2515Class::observe() { + // TODO: These should probably be 0x60, not 0x80. writeRegister(REG_CANCTRL, 0x80); + // TODO: The requested mode must be verified by reading the OPMODE[2:0] bits (CANSTAT[7:5]) if (readRegister(REG_CANCTRL) != 0x80) { return 0; } @@ -364,6 +506,7 @@ int MCP2515Class::observe() int MCP2515Class::loopback() { writeRegister(REG_CANCTRL, 0x40); + // TODO: The requested mode must be verified by reading the OPMODE[2:0] bits (CANSTAT[7:5]) if (readRegister(REG_CANCTRL) != 0x40) { return 0; } @@ -374,6 +517,7 @@ int MCP2515Class::loopback() int MCP2515Class::sleep() { writeRegister(REG_CANCTRL, 0x01); + // TODO: The requested mode must be verified by reading the OPMODE[2:0] bits (CANSTAT[7:5]) if (readRegister(REG_CANCTRL) != 0x01) { return 0; } @@ -384,6 +528,7 @@ int MCP2515Class::sleep() int MCP2515Class::wakeup() { writeRegister(REG_CANCTRL, 0x00); + // TODO: The requested mode must be verified by reading the OPMODE[2:0] bits (CANSTAT[7:5]) if (readRegister(REG_CANCTRL) != 0x00) { return 0; } @@ -407,6 +552,73 @@ void MCP2515Class::setClockFrequency(long clockFrequency) _clockFrequency = clockFrequency; } +void MCP2515Class::dumpImportantRegisters(Stream& out) { + out.print("TEC: "); + out.println(readRegister(0x1C), HEX); + out.print("REC: "); + out.println(readRegister(0x1D), HEX); + out.print("CANINTE: "); + out.println(readRegister(0x2B), HEX); + + out.print("CANINTF: "); + uint8_t regCANINTF = readRegister(0x2C); + out.print(regCANINTF, HEX); + if (regCANINTF & 0x80) { + out.print(" MERRF"); + } + if (regCANINTF & 0x40) { + out.print(" WAKIF"); + } + if (regCANINTF & 0x20) { + out.print(" ERRIF"); + } + if (regCANINTF & 0x10) { + out.print(" TX2IF"); + } + if (regCANINTF & 0x08) { + out.print(" TX1IF"); + } + if (regCANINTF & 0x04) { + out.print(" TX0IF"); + } + if (regCANINTF & 0x02) { + out.print(" RX1IF"); + } + if (regCANINTF & 0x01) { + out.print(" RX0IF"); + } + out.println(); + + out.print("EFLG: "); + uint8_t regEFLG = readRegister(0x2D); + out.print(regEFLG, HEX); + if (regEFLG & 0x80) { + out.print(" RX1OVR"); + } + if (regEFLG & 0x40) { + out.print(" RX0OVR"); + } + if (regEFLG & 0x20) { + out.print(" TXBO"); + } + if (regEFLG & 0x10) { + out.print(" TXEP"); + } + if (regEFLG & 0x08) { + out.print(" RXEP"); + } + if (regEFLG & 0x04) { + out.print(" TXWAR"); + } + if (regEFLG & 0x02) { + out.print(" RXWAR"); + } + if (regEFLG & 0x01) { + out.print(" EWARN"); + } + out.println(); +} + void MCP2515Class::dumpRegisters(Stream& out) { for (int i = 0; i < 128; i++) { @@ -433,7 +645,15 @@ void MCP2515Class::reset() digitalWrite(_csPin, HIGH); SPI.endTransaction(); - delayMicroseconds(10); + // From the data sheet: + // The OST keeps the device in a Reset state for 128 OSC1 clock cycles after + // the occurrence of a Power-on Reset, SPI Reset, after the assertion of the + // RESET pin, and after a wake-up from Sleep mode. It should be noted that no + // SPI protocol operations should be attempted until after the OST has + // expired. + // We sleep for 160 cycles to match the old behavior with 16 MHz quartz, and + // to be on the safe side for 8 MHz devices. + delayMicroseconds(ceil(160 * 1000000.0 / _clockFrequency)); } void MCP2515Class::handleInterrupt() diff --git a/src/MCP2515.h b/src/MCP2515.h index 2f0444f..40a1334 100644 --- a/src/MCP2515.h +++ b/src/MCP2515.h @@ -27,7 +27,11 @@ class MCP2515Class : public CANControllerClass { MCP2515Class(); virtual ~MCP2515Class(); - virtual int begin(long baudRate); + int begin(long baudRate, bool stayInConfigurationMode); + virtual int begin(long baudRate) { + return begin(baudRate, /* stayInConfigurationMode= */ false); + } + virtual void end(); virtual int endPacket(); @@ -38,9 +42,24 @@ class MCP2515Class : public CANControllerClass { using CANControllerClass::filter; virtual int filter(int id, int mask); + + // mask0 is applied to filter0 and filter1 for RXB0. + // mask1 is applied to filter2, filter3, filter4, filter5 for RXB1. + // allowRollover controls whether messages that would otherwise overflow RXB0 + // should be put in RXB1 instead. + // + // See the MCP2515 datasheet for more info. + boolean setFilterRegisters( + uint16_t mask0, uint16_t filter0, uint16_t filter1, + uint16_t mask1, uint16_t filter2, uint16_t filter3, uint16_t filter4, uint16_t filter5, + bool allowRollover); + using CANControllerClass::filterExtended; virtual int filterExtended(long id, long mask); + // TODO: add setFilterRegistersExtended(). + bool switchToNormalMode(); + bool switchToConfigurationMode(); virtual int observe(); virtual int loopback(); virtual int sleep(); @@ -50,6 +69,7 @@ class MCP2515Class : public CANControllerClass { void setSPIFrequency(uint32_t frequency); void setClockFrequency(long clockFrequency); + void dumpImportantRegisters(Stream& out); void dumpRegisters(Stream& out); private: