*
This commit is contained in:
@@ -0,0 +1,454 @@
|
||||
/*
|
||||
* The ASCII protocol is a simpler, human readable alternative to the main native
|
||||
* protocol.
|
||||
* In the future this protocol might be extended to support selected GCode commands.
|
||||
* For a list of supported commands see doc/ascii-protocol.md
|
||||
*/
|
||||
|
||||
/* Includes ------------------------------------------------------------------*/
|
||||
|
||||
#include "odrive_main.h"
|
||||
#include "communication.h"
|
||||
#include "ascii_protocol.hpp"
|
||||
#include <utils.hpp>
|
||||
#include <fibre/cpp_utils.hpp>
|
||||
|
||||
#include "autogen/type_info.hpp"
|
||||
#include "communication/interface_can.hpp"
|
||||
|
||||
using namespace fibre;
|
||||
|
||||
/* Private macros ------------------------------------------------------------*/
|
||||
/* Private typedef -----------------------------------------------------------*/
|
||||
/* Global constant data ------------------------------------------------------*/
|
||||
/* Global variables ----------------------------------------------------------*/
|
||||
/* Private constant data -----------------------------------------------------*/
|
||||
|
||||
#define TO_STR_INNER(s) #s
|
||||
#define TO_STR(s) TO_STR_INNER(s)
|
||||
|
||||
/* Private variables ---------------------------------------------------------*/
|
||||
|
||||
#if HW_VERSION_MAJOR == 3
|
||||
static Introspectable root_obj = ODrive3TypeInfo<ODrive>::make_introspectable(odrv);
|
||||
#elif HW_VERSION_MAJOR == 4
|
||||
static Introspectable root_obj = ODrive4TypeInfo<ODrive>::make_introspectable(odrv);
|
||||
#endif
|
||||
|
||||
/* Private function prototypes -----------------------------------------------*/
|
||||
|
||||
/* Function implementations --------------------------------------------------*/
|
||||
|
||||
// @brief Sends a line on the specified output.
|
||||
template<typename ... TArgs>
|
||||
void AsciiProtocol::respond(bool include_checksum, const char * fmt, TArgs&& ... args) {
|
||||
char tx_buf[64];
|
||||
|
||||
size_t len = snprintf(tx_buf, sizeof(tx_buf), fmt, std::forward<TArgs>(args)...);
|
||||
|
||||
// Silently truncate the output if it's too long for the buffer.
|
||||
len = std::min(len, sizeof(tx_buf));
|
||||
|
||||
if (include_checksum) {
|
||||
uint8_t checksum = 0;
|
||||
for (size_t i = 0; i < len; ++i)
|
||||
checksum ^= tx_buf[i];
|
||||
len += snprintf(tx_buf + len, sizeof(tx_buf) - len, "*%u\r\n", checksum);
|
||||
} else {
|
||||
len += snprintf(tx_buf + len, sizeof(tx_buf) - len, "\r\n");
|
||||
}
|
||||
|
||||
// Silently truncate the output if it's too long for the buffer.
|
||||
len = std::min(len, sizeof(tx_buf));
|
||||
|
||||
sink_.write({(const uint8_t*)tx_buf, len});
|
||||
sink_.maybe_start_async_write();
|
||||
}
|
||||
|
||||
|
||||
// @brief Executes an ASCII protocol command
|
||||
// @param buffer buffer of ASCII encoded characters
|
||||
// @param len size of the buffer
|
||||
void AsciiProtocol::process_line(cbufptr_t buffer) {
|
||||
static_assert(sizeof(char) == sizeof(uint8_t));
|
||||
|
||||
// scan line to find beginning of checksum and prune comment
|
||||
uint8_t checksum = 0;
|
||||
size_t checksum_start = SIZE_MAX;
|
||||
for (size_t i = 0; i < buffer.size(); ++i) {
|
||||
if (buffer.begin()[i] == ';') { // ';' is the comment start char
|
||||
buffer = buffer.take(i);
|
||||
break;
|
||||
}
|
||||
if (checksum_start > i) {
|
||||
if (buffer[i] == '*') {
|
||||
checksum_start = i + 1;
|
||||
} else {
|
||||
checksum ^= buffer[i];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// copy everything into a local buffer so we can insert null-termination
|
||||
char cmd[MAX_LINE_LENGTH + 1];
|
||||
size_t len = std::min(buffer.size(), MAX_LINE_LENGTH);
|
||||
memcpy(cmd, buffer.begin(), len);
|
||||
cmd[len] = 0; // null-terminate
|
||||
|
||||
// optional checksum validation
|
||||
bool use_checksum = (checksum_start < len);
|
||||
if (use_checksum) {
|
||||
unsigned int received_checksum;
|
||||
int numscan = sscanf(&cmd[checksum_start], "%u", &received_checksum);
|
||||
if ((numscan < 1) || (received_checksum != checksum))
|
||||
return;
|
||||
len = checksum_start - 1; // prune checksum and asterisk
|
||||
cmd[len] = 0; // null-terminate
|
||||
}
|
||||
|
||||
|
||||
// check incoming packet type
|
||||
switch(cmd[0]) {
|
||||
case 'p': cmd_set_position(cmd, use_checksum); break; // position control
|
||||
case 'q': cmd_set_position_wl(cmd, use_checksum); break; // position control with limits
|
||||
case 'v': cmd_set_velocity(cmd, use_checksum); break; // velocity control
|
||||
case 'c': cmd_set_torque(cmd, use_checksum); break; // current control
|
||||
case 't': cmd_set_trapezoid_trajectory(cmd, use_checksum); break; // trapezoidal trajectory
|
||||
case 'f': cmd_get_feedback(cmd, use_checksum); break; // feedback
|
||||
case 'h': cmd_help(cmd, use_checksum); break; // Help
|
||||
case 'i': cmd_info_dump(cmd, use_checksum); break; // Dump device info
|
||||
case 's': cmd_system_ctrl(cmd, use_checksum); break; // System
|
||||
case 'r': cmd_read_property(cmd, use_checksum); break; // read property
|
||||
case 'w': cmd_write_property(cmd, use_checksum); break; // write property
|
||||
case 'u': cmd_update_axis_wdg(cmd, use_checksum); break; // Update axis watchdog.
|
||||
case 'e': cmd_encoder(cmd, use_checksum); break; // Encoder commands
|
||||
default : cmd_unknown(nullptr, use_checksum); break;
|
||||
}
|
||||
}
|
||||
|
||||
// @brief Executes the set position command
|
||||
// @param pStr buffer of ASCII encoded values
|
||||
// @param response_channel reference to the stream to respond on
|
||||
// @param use_checksum bool to indicate whether a checksum is required on response
|
||||
void AsciiProtocol::cmd_set_position(char * pStr, bool use_checksum) {
|
||||
unsigned motor_number;
|
||||
float pos_setpoint, vel_feed_forward, torque_feed_forward;
|
||||
|
||||
int numscan = sscanf(pStr, "p %u %f %f %f", &motor_number, &pos_setpoint, &vel_feed_forward, &torque_feed_forward);
|
||||
if (numscan < 2) {
|
||||
respond(use_checksum, "invalid command format");
|
||||
} else if (motor_number >= AXIS_COUNT) {
|
||||
respond(use_checksum, "invalid motor %u", motor_number);
|
||||
} else {
|
||||
Axis& axis = axes[motor_number];
|
||||
axis.controller_.config_.control_mode = Controller::CONTROL_MODE_POSITION_CONTROL;
|
||||
axis.controller_.input_pos_ = pos_setpoint;
|
||||
if (numscan >= 3)
|
||||
axis.controller_.input_vel_ = vel_feed_forward;
|
||||
if (numscan >= 4)
|
||||
axis.controller_.input_torque_ = torque_feed_forward;
|
||||
axis.controller_.input_pos_updated();
|
||||
axis.watchdog_feed();
|
||||
}
|
||||
}
|
||||
|
||||
// @brief Executes the set position with current and velocity limit command
|
||||
// @param pStr buffer of ASCII encoded values
|
||||
// @param response_channel reference to the stream to respond on
|
||||
// @param use_checksum bool to indicate whether a checksum is required on response
|
||||
void AsciiProtocol::cmd_set_position_wl(char * pStr, bool use_checksum) {
|
||||
unsigned motor_number;
|
||||
float pos_setpoint, vel_limit, torque_lim;
|
||||
|
||||
int numscan = sscanf(pStr, "q %u %f %f %f", &motor_number, &pos_setpoint, &vel_limit, &torque_lim);
|
||||
if (numscan < 2) {
|
||||
respond(use_checksum, "invalid command format");
|
||||
} else if (motor_number >= AXIS_COUNT) {
|
||||
respond(use_checksum, "invalid motor %u", motor_number);
|
||||
} else {
|
||||
Axis& axis = axes[motor_number];
|
||||
axis.controller_.config_.control_mode = Controller::CONTROL_MODE_POSITION_CONTROL;
|
||||
axis.controller_.input_pos_ = pos_setpoint;
|
||||
if (numscan >= 3)
|
||||
axis.controller_.config_.vel_limit = vel_limit;
|
||||
if (numscan >= 4)
|
||||
axis.motor_.config_.torque_lim = torque_lim;
|
||||
axis.controller_.input_pos_updated();
|
||||
axis.watchdog_feed();
|
||||
}
|
||||
}
|
||||
|
||||
// @brief Executes the set velocity command
|
||||
// @param pStr buffer of ASCII encoded values
|
||||
// @param response_channel reference to the stream to respond on
|
||||
// @param use_checksum bool to indicate whether a checksum is required on response
|
||||
void AsciiProtocol::cmd_set_velocity(char * pStr, bool use_checksum) {
|
||||
unsigned motor_number;
|
||||
float vel_setpoint, torque_feed_forward;
|
||||
int numscan = sscanf(pStr, "v %u %f %f", &motor_number, &vel_setpoint, &torque_feed_forward);
|
||||
if (numscan < 2) {
|
||||
respond(use_checksum, "invalid command format");
|
||||
} else if (motor_number >= AXIS_COUNT) {
|
||||
respond(use_checksum, "invalid motor %u", motor_number);
|
||||
} else {
|
||||
Axis& axis = axes[motor_number];
|
||||
axis.controller_.config_.control_mode = Controller::CONTROL_MODE_VELOCITY_CONTROL;
|
||||
axis.controller_.input_vel_ = vel_setpoint;
|
||||
if (numscan >= 3)
|
||||
axis.controller_.input_torque_ = torque_feed_forward;
|
||||
axis.watchdog_feed();
|
||||
}
|
||||
}
|
||||
|
||||
// @brief Executes the set torque control command
|
||||
// @param pStr buffer of ASCII encoded values
|
||||
// @param response_channel reference to the stream to respond on
|
||||
// @param use_checksum bool to indicate whether a checksum is required on response
|
||||
void AsciiProtocol::cmd_set_torque(char * pStr, bool use_checksum) {
|
||||
unsigned motor_number;
|
||||
float torque_setpoint;
|
||||
|
||||
if (sscanf(pStr, "c %u %f", &motor_number, &torque_setpoint) < 2) {
|
||||
respond(use_checksum, "invalid command format");
|
||||
} else if (motor_number >= AXIS_COUNT) {
|
||||
respond(use_checksum, "invalid motor %u", motor_number);
|
||||
} else {
|
||||
Axis& axis = axes[motor_number];
|
||||
axis.controller_.config_.control_mode = Controller::CONTROL_MODE_TORQUE_CONTROL;
|
||||
axis.controller_.input_torque_ = torque_setpoint;
|
||||
axis.watchdog_feed();
|
||||
}
|
||||
}
|
||||
|
||||
// @brief Sets the encoder linear count
|
||||
// @param pStr buffer of ASCII encoded values
|
||||
// @param response_channel reference to the stream to respond on
|
||||
// @param use_checksum bool to indicate whether a checksum is required on response
|
||||
void AsciiProtocol::cmd_encoder(char * pStr, bool use_checksum) {
|
||||
if (pStr[1] == 's') {
|
||||
pStr += 2; // Substring two characters to the right (ok because we have guaranteed null termination after all chars)
|
||||
|
||||
unsigned motor_number;
|
||||
int encoder_count;
|
||||
|
||||
if (sscanf(pStr, "l %u %i", &motor_number, &encoder_count) < 2) {
|
||||
respond(use_checksum, "invalid command format");
|
||||
} else if (motor_number >= AXIS_COUNT) {
|
||||
respond(use_checksum, "invalid motor %u", motor_number);
|
||||
} else {
|
||||
Axis& axis = axes[motor_number];
|
||||
axis.encoder_.set_linear_count(encoder_count);
|
||||
axis.watchdog_feed();
|
||||
respond(use_checksum, "encoder set to %u", encoder_count);
|
||||
}
|
||||
} else {
|
||||
respond(use_checksum, "invalid command format");
|
||||
}
|
||||
}
|
||||
|
||||
// @brief Executes the set trapezoid trajectory command
|
||||
// @param pStr buffer of ASCII encoded values
|
||||
// @param response_channel reference to the stream to respond on
|
||||
// @param use_checksum bool to indicate whether a checksum is required on response
|
||||
void AsciiProtocol::cmd_set_trapezoid_trajectory(char* pStr, bool use_checksum) {
|
||||
unsigned motor_number;
|
||||
float goal_point;
|
||||
|
||||
if (sscanf(pStr, "t %u %f", &motor_number, &goal_point) < 2) {
|
||||
respond(use_checksum, "invalid command format");
|
||||
} else if (motor_number >= AXIS_COUNT) {
|
||||
respond(use_checksum, "invalid motor %u", motor_number);
|
||||
} else {
|
||||
Axis& axis = axes[motor_number];
|
||||
axis.controller_.config_.input_mode = Controller::INPUT_MODE_TRAP_TRAJ;
|
||||
axis.controller_.config_.control_mode = Controller::CONTROL_MODE_POSITION_CONTROL;
|
||||
axis.controller_.input_pos_ = goal_point;
|
||||
axis.controller_.input_pos_updated();
|
||||
axis.watchdog_feed();
|
||||
}
|
||||
}
|
||||
|
||||
// @brief Executes the get position and velocity feedback command
|
||||
// @param pStr buffer of ASCII encoded values
|
||||
// @param response_channel reference to the stream to respond on
|
||||
// @param use_checksum bool to indicate whether a checksum is required on response
|
||||
void AsciiProtocol::cmd_get_feedback(char * pStr, bool use_checksum) {
|
||||
unsigned motor_number;
|
||||
|
||||
if (sscanf(pStr, "f %u", &motor_number) < 1) {
|
||||
respond(use_checksum, "invalid command format");
|
||||
} else if (motor_number >= AXIS_COUNT) {
|
||||
respond(use_checksum, "invalid motor %u", motor_number);
|
||||
} else {
|
||||
Axis& axis = axes[motor_number];
|
||||
respond(use_checksum, "%f %f",
|
||||
(double)axis.encoder_.pos_estimate_.any().value_or(0.0f),
|
||||
(double)axis.encoder_.vel_estimate_.any().value_or(0.0f));
|
||||
}
|
||||
}
|
||||
|
||||
// @brief Shows help text
|
||||
// @param pStr buffer of ASCII encoded values
|
||||
// @param response_channel reference to the stream to respond on
|
||||
// @param use_checksum bool to indicate whether a checksum is required on response
|
||||
void AsciiProtocol::cmd_help(char * pStr, bool use_checksum) {
|
||||
(void)pStr;
|
||||
respond(use_checksum, "Please see documentation for more details");
|
||||
respond(use_checksum, "");
|
||||
respond(use_checksum, "Available commands syntax reference:");
|
||||
respond(use_checksum, "Position: q axis pos vel-lim I-lim");
|
||||
respond(use_checksum, "Position: p axis pos vel-ff I-ff");
|
||||
respond(use_checksum, "Velocity: v axis vel I-ff");
|
||||
respond(use_checksum, "Torque: c axis T");
|
||||
respond(use_checksum, "");
|
||||
respond(use_checksum, "Properties start at odrive root, such as axis0.requested_state");
|
||||
respond(use_checksum, "Read: r property");
|
||||
respond(use_checksum, "Write: w property value");
|
||||
respond(use_checksum, "");
|
||||
respond(use_checksum, "Save config: ss");
|
||||
respond(use_checksum, "Erase config: se");
|
||||
respond(use_checksum, "Reboot: sr");
|
||||
}
|
||||
|
||||
// @brief Gets the hardware, firmware and serial details
|
||||
// @param pStr buffer of ASCII encoded values
|
||||
// @param response_channel reference to the stream to respond on
|
||||
// @param use_checksum bool to indicate whether a checksum is required on response
|
||||
void AsciiProtocol::cmd_info_dump(char * pStr, bool use_checksum) {
|
||||
// respond(use_checksum, "Signature: %#x", STM_ID_GetSignature());
|
||||
// respond(use_checksum, "Revision: %#x", STM_ID_GetRevision());
|
||||
// respond(use_checksum, "Flash Size: %#x KiB", STM_ID_GetFlashSize());
|
||||
respond(use_checksum, "Hardware version: %d.%d-%dV", odrv.hw_version_major_, odrv.hw_version_minor_, odrv.hw_version_variant_);
|
||||
respond(use_checksum, "Firmware version: %d.%d.%d", odrv.fw_version_major_, odrv.fw_version_minor_, odrv.fw_version_revision_);
|
||||
respond(use_checksum, "Serial number: %s", serial_number_str);
|
||||
}
|
||||
|
||||
// @brief Executes the system control command
|
||||
// @param pStr buffer of ASCII encoded values
|
||||
// @param response_channel reference to the stream to respond on
|
||||
// @param use_checksum bool to indicate whether a checksum is required on response
|
||||
void AsciiProtocol::cmd_system_ctrl(char * pStr, bool use_checksum) {
|
||||
switch (pStr[1])
|
||||
{
|
||||
case 's': odrv.save_configuration(); break; // Save config
|
||||
case 'e': odrv.erase_configuration(); break; // Erase config
|
||||
case 'r': odrv.reboot(); break; // Reboot
|
||||
case 'c': odrv.clear_errors(); break; // clear all errors and rearm brake resistor if necessary
|
||||
default: /* default */ break;
|
||||
}
|
||||
}
|
||||
|
||||
// @brief Executes the read parameter command
|
||||
// @param pStr buffer of ASCII encoded values
|
||||
// @param response_channel reference to the stream to respond on
|
||||
// @param use_checksum bool to indicate whether a checksum is required on response
|
||||
void AsciiProtocol::cmd_read_property(char * pStr, bool use_checksum) {
|
||||
char name[MAX_LINE_LENGTH];
|
||||
|
||||
if (sscanf(pStr, "r %255s", name) < 1) {
|
||||
respond(use_checksum, "invalid command format");
|
||||
} else {
|
||||
Introspectable property = root_obj.get_child(name, sizeof(name));
|
||||
const StringConvertibleTypeInfo* type_info = dynamic_cast<const StringConvertibleTypeInfo*>(property.get_type_info());
|
||||
if (!type_info) {
|
||||
respond(use_checksum, "invalid property");
|
||||
} else {
|
||||
char response[10];
|
||||
bool success = type_info->get_string(property, response, sizeof(response));
|
||||
respond(use_checksum, success ? response : "not implemented");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// @brief Executes the set write position command
|
||||
// @param pStr buffer of ASCII encoded values
|
||||
// @param response_channel reference to the stream to respond on
|
||||
// @param use_checksum bool to indicate whether a checksum is required on response
|
||||
void AsciiProtocol::cmd_write_property(char * pStr, bool use_checksum) {
|
||||
char name[MAX_LINE_LENGTH];
|
||||
char value[MAX_LINE_LENGTH];
|
||||
|
||||
if (sscanf(pStr, "w %255s %255s", name, value) < 1) {
|
||||
respond(use_checksum, "invalid command format");
|
||||
} else {
|
||||
Introspectable property = root_obj.get_child(name, sizeof(name));
|
||||
const StringConvertibleTypeInfo* type_info = dynamic_cast<const StringConvertibleTypeInfo*>(property.get_type_info());
|
||||
if (!type_info) {
|
||||
respond(use_checksum, "invalid property");
|
||||
} else {
|
||||
bool success = type_info->set_string(property, value, sizeof(value));
|
||||
if (!success) {
|
||||
respond(use_checksum, "not implemented");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// @brief Executes the motor watchdog update command
|
||||
// @param pStr buffer of ASCII encoded values
|
||||
// @param response_channel reference to the stream to respond on
|
||||
// @param use_checksum bool to indicate whether a checksum is required on response
|
||||
void AsciiProtocol::cmd_update_axis_wdg(char * pStr, bool use_checksum) {
|
||||
unsigned motor_number;
|
||||
|
||||
if (sscanf(pStr, "u %u", &motor_number) < 1) {
|
||||
respond(use_checksum, "invalid command format");
|
||||
} else if (motor_number >= AXIS_COUNT) {
|
||||
respond(use_checksum, "invalid motor %u", motor_number);
|
||||
} else {
|
||||
axes[motor_number].watchdog_feed();
|
||||
}
|
||||
}
|
||||
|
||||
// @brief Sends the unknown command response
|
||||
// @param pStr buffer of ASCII encoded values
|
||||
// @param response_channel reference to the stream to respond on
|
||||
// @param use_checksum bool to indicate whether a checksum is required on response
|
||||
void AsciiProtocol::cmd_unknown(char * pStr, bool use_checksum) {
|
||||
(void)pStr;
|
||||
respond(use_checksum, "unknown command");
|
||||
}
|
||||
|
||||
void AsciiProtocol::on_read_finished(ReadResult result) {
|
||||
if (result.status != kStreamOk) {
|
||||
return;
|
||||
}
|
||||
|
||||
for (;;) {
|
||||
uint8_t* end_of_line = std::find_if(rx_buf_, result.end, [](uint8_t c) {
|
||||
return c == '\r' || c == '\n' || c == '!';
|
||||
});
|
||||
|
||||
if (end_of_line >= result.end) {
|
||||
break;
|
||||
}
|
||||
|
||||
if (read_active_) {
|
||||
process_line({rx_buf_, end_of_line});
|
||||
} else {
|
||||
// Ignoring this line cause it didn't start at a new-line character
|
||||
read_active_ = true;
|
||||
}
|
||||
|
||||
// Discard the processed bytes and shift the remainder to the beginning of the buffer
|
||||
size_t n_remaining = result.end - end_of_line - 1;
|
||||
memmove(rx_buf_, end_of_line + 1, n_remaining);
|
||||
result.end = rx_buf_ + n_remaining;
|
||||
}
|
||||
|
||||
// No more new-line characters in buffer
|
||||
|
||||
if (result.end >= rx_buf_ + sizeof(rx_buf_)) {
|
||||
// If the line becomes too long, reset buffer and wait for the next line
|
||||
result.end = rx_buf_;
|
||||
read_active_ = false;
|
||||
}
|
||||
|
||||
TransferHandle dummy;
|
||||
rx_channel_->start_read({result.end, rx_buf_ + sizeof(rx_buf_)}, &dummy, MEMBER_CB(this, on_read_finished));
|
||||
}
|
||||
|
||||
void AsciiProtocol::start() {
|
||||
TransferHandle dummy;
|
||||
rx_channel_->start_read(rx_buf_, &dummy, MEMBER_CB(this, on_read_finished));
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
#ifndef __ASCII_PROTOCOL_HPP
|
||||
#define __ASCII_PROTOCOL_HPP
|
||||
|
||||
#include <fibre/async_stream.hpp>
|
||||
#include <fibre/../../stream_utils.hpp>
|
||||
|
||||
#define MAX_LINE_LENGTH ((size_t)256)
|
||||
|
||||
class AsciiProtocol {
|
||||
public:
|
||||
AsciiProtocol(fibre::AsyncStreamSource* rx_channel, fibre::AsyncStreamSink* tx_channel)
|
||||
: rx_channel_(rx_channel), sink_(*tx_channel) {}
|
||||
|
||||
void start();
|
||||
|
||||
private:
|
||||
void cmd_set_position(char * pStr, bool use_checksum);
|
||||
void cmd_set_position_wl(char * pStr, bool use_checksum);
|
||||
void cmd_set_velocity(char * pStr, bool use_checksum);
|
||||
void cmd_set_torque(char * pStr, bool use_checksum);
|
||||
void cmd_set_trapezoid_trajectory(char * pStr, bool use_checksum);
|
||||
void cmd_get_feedback(char * pStr, bool use_checksum);
|
||||
void cmd_help(char * pStr, bool use_checksum);
|
||||
void cmd_info_dump(char * pStr, bool use_checksum);
|
||||
void cmd_system_ctrl(char * pStr, bool use_checksum);
|
||||
void cmd_read_property(char * pStr, bool use_checksum);
|
||||
void cmd_write_property(char * pStr, bool use_checksum);
|
||||
void cmd_update_axis_wdg(char * pStr, bool use_checksum);
|
||||
void cmd_unknown(char * pStr, bool use_checksum);
|
||||
void cmd_encoder(char * pStr, bool use_checksum);
|
||||
|
||||
template<typename ... TArgs> void respond(bool include_checksum, const char * fmt, TArgs&& ... args);
|
||||
void process_line(fibre::cbufptr_t buffer);
|
||||
void on_write_finished(fibre::WriteResult result);
|
||||
void on_read_finished(fibre::ReadResult result);
|
||||
|
||||
fibre::AsyncStreamSource* rx_channel_ = nullptr;
|
||||
uint8_t* rx_end_ = nullptr; // non-zero if an RX operation has finished but wasn't handled yet because the TX channel was busy
|
||||
|
||||
uint8_t rx_buf_[MAX_LINE_LENGTH];
|
||||
bool read_active_ = true;
|
||||
|
||||
fibre::BufferedStreamSink<512> sink_;
|
||||
};
|
||||
|
||||
#endif // __ASCII_PROTOCOL_HPP
|
||||
@@ -0,0 +1,97 @@
|
||||
#pragma once
|
||||
|
||||
#include <stdint.h>
|
||||
#include <algorithm>
|
||||
#include <cstring>
|
||||
#include <iterator>
|
||||
|
||||
struct can_Message_t {
|
||||
uint32_t id = 0x000; // 11-bit max is 0x7ff, 29-bit max is 0x1FFFFFFF
|
||||
bool isExt = false;
|
||||
bool rtr = false;
|
||||
uint8_t len = 8;
|
||||
uint8_t buf[8] = {0, 0, 0, 0, 0, 0, 0, 0};
|
||||
} ;
|
||||
|
||||
struct can_Signal_t {
|
||||
const uint8_t startBit;
|
||||
const uint8_t length;
|
||||
const bool isIntel;
|
||||
const float factor;
|
||||
const float offset;
|
||||
};
|
||||
|
||||
struct can_Cyclic_t {
|
||||
uint32_t cycleTime_ms;
|
||||
uint32_t lastTime_ms;
|
||||
};
|
||||
|
||||
#include <iterator>
|
||||
template <typename T>
|
||||
constexpr T can_getSignal(can_Message_t msg, const uint8_t startBit, const uint8_t length, const bool isIntel) {
|
||||
uint64_t tempVal = 0;
|
||||
uint64_t mask = length < 64 ? (1ULL << length) - 1ULL : -1ULL;
|
||||
|
||||
if (isIntel) {
|
||||
std::memcpy(&tempVal, msg.buf, sizeof(tempVal));
|
||||
tempVal = (tempVal >> startBit) & mask;
|
||||
} else {
|
||||
std::reverse(std::begin(msg.buf), std::end(msg.buf));
|
||||
std::memcpy(&tempVal, msg.buf, sizeof(tempVal));
|
||||
tempVal = (tempVal >> (64 - startBit - length)) & mask;
|
||||
}
|
||||
|
||||
T retVal;
|
||||
std::memcpy(&retVal, &tempVal, sizeof(T));
|
||||
return retVal;
|
||||
}
|
||||
|
||||
template <typename T>
|
||||
constexpr void can_setSignal(can_Message_t& msg, const T& val, const uint8_t startBit, const uint8_t length, const bool isIntel) {
|
||||
uint64_t valAsBits = 0;
|
||||
std::memcpy(&valAsBits, &val, sizeof(val));
|
||||
|
||||
uint64_t mask = length < 64 ? (1ULL << length) - 1ULL : -1ULL;
|
||||
|
||||
if (isIntel) {
|
||||
uint64_t data = 0;
|
||||
std::memcpy(&data, msg.buf, sizeof(data));
|
||||
|
||||
data &= ~(mask << startBit);
|
||||
data |= valAsBits << startBit;
|
||||
|
||||
std::memcpy(msg.buf, &data, sizeof(data));
|
||||
} else {
|
||||
uint64_t data = 0;
|
||||
std::reverse(std::begin(msg.buf), std::end(msg.buf));
|
||||
std::memcpy(&data, msg.buf, sizeof(data));
|
||||
|
||||
data &= ~(mask << (64 - startBit - length));
|
||||
data |= valAsBits << (64 - startBit - length);
|
||||
|
||||
std::memcpy(msg.buf, &data, sizeof(data));
|
||||
std::reverse(std::begin(msg.buf), std::end(msg.buf));
|
||||
}
|
||||
}
|
||||
|
||||
template<typename T>
|
||||
void can_setSignal(can_Message_t& msg, const T& val, const uint8_t startBit, const uint8_t length, const bool isIntel, const float factor, const float offset) {
|
||||
T scaledVal = static_cast<T>((val - offset) / factor);
|
||||
can_setSignal<T>(msg, scaledVal, startBit, length, isIntel);
|
||||
}
|
||||
|
||||
template<typename T>
|
||||
float can_getSignal(can_Message_t msg, const uint8_t startBit, const uint8_t length, const bool isIntel, const float factor, const float offset) {
|
||||
T retVal = can_getSignal<T>(msg, startBit, length, isIntel);
|
||||
return (retVal * factor) + offset;
|
||||
}
|
||||
|
||||
template <typename T>
|
||||
float can_getSignal(can_Message_t msg, const can_Signal_t& signal) {
|
||||
return can_getSignal<T>(msg, signal.startBit, signal.length, signal.isIntel, signal.factor, signal.offset);
|
||||
}
|
||||
|
||||
template <typename T>
|
||||
void can_setSignal(can_Message_t& msg, const T& val, const can_Signal_t& signal) {
|
||||
can_setSignal(msg, val, signal.startBit, signal.length, signal.isIntel, signal.factor, signal.offset);
|
||||
}
|
||||
@@ -0,0 +1,467 @@
|
||||
|
||||
#include "can_simple.hpp"
|
||||
|
||||
#include <odrive_main.h>
|
||||
#include <functional>
|
||||
|
||||
bool CANSimple::init() {
|
||||
for (size_t i = 0; i < AXIS_COUNT; ++i) {
|
||||
if (!renew_subscription(i)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
bool CANSimple::renew_subscription(size_t i) {
|
||||
Axis& axis = axes[i];
|
||||
|
||||
// TODO: remove these two lines (see comment in header)
|
||||
node_ids_[i] = axis.config_.can.node_id;
|
||||
extended_node_ids_[i] = axis.config_.can.is_extended;
|
||||
|
||||
MsgIdFilterSpecs filter = {
|
||||
.id = {},
|
||||
.mask = (uint32_t)(0xffffffff << NUM_CMD_ID_BITS)};
|
||||
if (axis.config_.can.is_extended) {
|
||||
filter.id = (uint32_t)(axis.config_.can.node_id << NUM_CMD_ID_BITS);
|
||||
} else {
|
||||
filter.id = (uint16_t)(axis.config_.can.node_id << NUM_CMD_ID_BITS);
|
||||
}
|
||||
|
||||
if (subscription_handles_[i]) {
|
||||
canbus_->unsubscribe(subscription_handles_[i]);
|
||||
}
|
||||
|
||||
return canbus_->subscribe(
|
||||
filter, [](void* ctx, const can_Message_t& msg) {
|
||||
((CANSimple*)ctx)->handle_can_message(msg);
|
||||
},
|
||||
this, &subscription_handles_[i]);
|
||||
}
|
||||
|
||||
void CANSimple::handle_can_message(const can_Message_t& msg) {
|
||||
// Frame
|
||||
// nodeID | CMD
|
||||
// 6 bits | 5 bits
|
||||
uint32_t nodeID = get_node_id(msg.id);
|
||||
|
||||
for (auto& axis : axes) {
|
||||
if ((axis.config_.can.node_id == nodeID) && (axis.config_.can.is_extended == msg.isExt)) {
|
||||
do_command(axis, msg);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void CANSimple::do_command(Axis& axis, const can_Message_t& msg) {
|
||||
const uint32_t cmd = get_cmd_id(msg.id);
|
||||
axis.watchdog_feed();
|
||||
switch (cmd) {
|
||||
case MSG_CO_NMT_CTRL:
|
||||
break;
|
||||
case MSG_CO_HEARTBEAT_CMD:
|
||||
break;
|
||||
case MSG_ODRIVE_HEARTBEAT:
|
||||
// We don't currently do anything to respond to ODrive heartbeat messages
|
||||
break;
|
||||
case MSG_ODRIVE_ESTOP:
|
||||
estop_callback(axis, msg);
|
||||
break;
|
||||
case MSG_GET_MOTOR_ERROR:
|
||||
if (msg.rtr || msg.len == 0)
|
||||
get_motor_error_callback(axis);
|
||||
break;
|
||||
case MSG_GET_ENCODER_ERROR:
|
||||
if (msg.rtr || msg.len == 0)
|
||||
get_encoder_error_callback(axis);
|
||||
break;
|
||||
case MSG_GET_SENSORLESS_ERROR:
|
||||
if (msg.rtr || msg.len == 0)
|
||||
get_sensorless_error_callback(axis);
|
||||
break;
|
||||
case MSG_SET_AXIS_NODE_ID:
|
||||
set_axis_nodeid_callback(axis, msg);
|
||||
break;
|
||||
case MSG_SET_AXIS_REQUESTED_STATE:
|
||||
set_axis_requested_state_callback(axis, msg);
|
||||
break;
|
||||
case MSG_SET_AXIS_STARTUP_CONFIG:
|
||||
set_axis_startup_config_callback(axis, msg);
|
||||
break;
|
||||
case MSG_GET_ENCODER_ESTIMATES:
|
||||
if (msg.rtr || msg.len == 0)
|
||||
get_encoder_estimates_callback(axis);
|
||||
break;
|
||||
case MSG_GET_ENCODER_COUNT:
|
||||
if (msg.rtr || msg.len == 0)
|
||||
get_encoder_count_callback(axis);
|
||||
break;
|
||||
case MSG_SET_INPUT_POS:
|
||||
set_input_pos_callback(axis, msg);
|
||||
break;
|
||||
case MSG_SET_INPUT_VEL:
|
||||
set_input_vel_callback(axis, msg);
|
||||
break;
|
||||
case MSG_SET_INPUT_TORQUE:
|
||||
set_input_torque_callback(axis, msg);
|
||||
break;
|
||||
case MSG_SET_CONTROLLER_MODES:
|
||||
set_controller_modes_callback(axis, msg);
|
||||
break;
|
||||
case MSG_SET_LIMITS:
|
||||
set_limits_callback(axis, msg);
|
||||
break;
|
||||
case MSG_START_ANTICOGGING:
|
||||
start_anticogging_callback(axis, msg);
|
||||
break;
|
||||
case MSG_SET_TRAJ_INERTIA:
|
||||
set_traj_inertia_callback(axis, msg);
|
||||
break;
|
||||
case MSG_SET_TRAJ_ACCEL_LIMITS:
|
||||
set_traj_accel_limits_callback(axis, msg);
|
||||
break;
|
||||
case MSG_SET_TRAJ_VEL_LIMIT:
|
||||
set_traj_vel_limit_callback(axis, msg);
|
||||
break;
|
||||
case MSG_GET_IQ:
|
||||
if (msg.rtr || msg.len == 0)
|
||||
get_iq_callback(axis);
|
||||
break;
|
||||
case MSG_GET_SENSORLESS_ESTIMATES:
|
||||
if (msg.rtr || msg.len == 0)
|
||||
get_sensorless_estimates_callback(axis);
|
||||
break;
|
||||
case MSG_RESET_ODRIVE:
|
||||
NVIC_SystemReset();
|
||||
break;
|
||||
case MSG_GET_BUS_VOLTAGE_CURRENT:
|
||||
if (msg.rtr || msg.len == 0)
|
||||
get_bus_voltage_current_callback(axis);
|
||||
break;
|
||||
case MSG_CLEAR_ERRORS:
|
||||
clear_errors_callback(axis, msg);
|
||||
break;
|
||||
case MSG_SET_LINEAR_COUNT:
|
||||
set_linear_count_callback(axis, msg);
|
||||
break;
|
||||
case MSG_SET_POS_GAIN:
|
||||
set_pos_gain_callback(axis, msg);
|
||||
break;
|
||||
case MSG_SET_VEL_GAINS:
|
||||
set_vel_gains_callback(axis, msg);
|
||||
break;
|
||||
case MSG_GET_ADC_VOLTAGE:
|
||||
get_adc_voltage_callback(axis, msg);
|
||||
break;
|
||||
case MSG_GET_CONTROLLER_ERROR:
|
||||
get_controller_error_callback(axis);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
void CANSimple::nmt_callback(const Axis& axis, const can_Message_t& msg) {
|
||||
// Not implemented
|
||||
}
|
||||
|
||||
void CANSimple::estop_callback(Axis& axis, const can_Message_t& msg) {
|
||||
axis.error_ |= Axis::ERROR_ESTOP_REQUESTED;
|
||||
}
|
||||
|
||||
bool CANSimple::get_motor_error_callback(const Axis& axis) {
|
||||
can_Message_t txmsg;
|
||||
txmsg.id = axis.config_.can.node_id << NUM_CMD_ID_BITS;
|
||||
txmsg.id += MSG_GET_MOTOR_ERROR; // heartbeat ID
|
||||
txmsg.isExt = axis.config_.can.is_extended;
|
||||
txmsg.len = 8;
|
||||
|
||||
can_setSignal(txmsg, axis.motor_.error_, 0, 64, true);
|
||||
|
||||
return canbus_->send_message(txmsg);
|
||||
}
|
||||
|
||||
bool CANSimple::get_encoder_error_callback(const Axis& axis) {
|
||||
can_Message_t txmsg;
|
||||
txmsg.id = axis.config_.can.node_id << NUM_CMD_ID_BITS;
|
||||
txmsg.id += MSG_GET_ENCODER_ERROR; // heartbeat ID
|
||||
txmsg.isExt = axis.config_.can.is_extended;
|
||||
txmsg.len = 8;
|
||||
|
||||
can_setSignal(txmsg, axis.encoder_.error_, 0, 32, true);
|
||||
|
||||
return canbus_->send_message(txmsg);
|
||||
}
|
||||
|
||||
bool CANSimple::get_sensorless_error_callback(const Axis& axis) {
|
||||
can_Message_t txmsg;
|
||||
txmsg.id = axis.config_.can.node_id << NUM_CMD_ID_BITS;
|
||||
txmsg.id += MSG_GET_SENSORLESS_ERROR; // heartbeat ID
|
||||
txmsg.isExt = axis.config_.can.is_extended;
|
||||
txmsg.len = 8;
|
||||
|
||||
can_setSignal(txmsg, axis.sensorless_estimator_.error_, 0, 32, true);
|
||||
|
||||
return canbus_->send_message(txmsg);
|
||||
}
|
||||
|
||||
bool CANSimple::get_controller_error_callback(const Axis& axis) {
|
||||
can_Message_t txmsg;
|
||||
txmsg.id = axis.config_.can.node_id << NUM_CMD_ID_BITS;
|
||||
txmsg.id += MSG_GET_CONTROLLER_ERROR; // heartbeat ID
|
||||
txmsg.isExt = axis.config_.can.is_extended;
|
||||
txmsg.len = 8;
|
||||
|
||||
can_setSignal(txmsg, axis.controller_.error_, 0, 32, true);
|
||||
|
||||
return canbus_->send_message(txmsg);
|
||||
}
|
||||
|
||||
void CANSimple::set_axis_nodeid_callback(Axis& axis, const can_Message_t& msg) {
|
||||
axis.config_.can.node_id = can_getSignal<uint32_t>(msg, 0, 32, true);
|
||||
}
|
||||
|
||||
void CANSimple::set_axis_requested_state_callback(Axis& axis, const can_Message_t& msg) {
|
||||
axis.requested_state_ = static_cast<Axis::AxisState>(can_getSignal<int32_t>(msg, 0, 32, true));
|
||||
}
|
||||
|
||||
void CANSimple::set_axis_startup_config_callback(Axis& axis, const can_Message_t& msg) {
|
||||
// Not Implemented
|
||||
}
|
||||
|
||||
bool CANSimple::get_encoder_estimates_callback(const Axis& axis) {
|
||||
can_Message_t txmsg;
|
||||
txmsg.id = axis.config_.can.node_id << NUM_CMD_ID_BITS;
|
||||
txmsg.id += MSG_GET_ENCODER_ESTIMATES; // heartbeat ID
|
||||
txmsg.isExt = axis.config_.can.is_extended;
|
||||
txmsg.len = 8;
|
||||
|
||||
can_setSignal<float>(txmsg, axis.controller_.pos_estimate_linear_src_.any().value_or(0.0f), 0, 32, true);
|
||||
can_setSignal<float>(txmsg, axis.controller_.vel_estimate_src_.any().value_or(0.0f), 32, 32, true);
|
||||
|
||||
return canbus_->send_message(txmsg);
|
||||
}
|
||||
|
||||
bool CANSimple::get_sensorless_estimates_callback(const Axis& axis) {
|
||||
can_Message_t txmsg;
|
||||
txmsg.id = axis.config_.can.node_id << NUM_CMD_ID_BITS;
|
||||
txmsg.id += MSG_GET_SENSORLESS_ESTIMATES; // heartbeat ID
|
||||
txmsg.isExt = axis.config_.can.is_extended;
|
||||
txmsg.len = 8;
|
||||
|
||||
static_assert(sizeof(float) == sizeof(axis.sensorless_estimator_.pll_pos_));
|
||||
|
||||
can_setSignal<float>(txmsg, axis.sensorless_estimator_.pll_pos_, 0, 32, true);
|
||||
can_setSignal<float>(txmsg, axis.sensorless_estimator_.vel_estimate_.any().value_or(0.0f), 32, 32, true);
|
||||
|
||||
return canbus_->send_message(txmsg);
|
||||
}
|
||||
|
||||
bool CANSimple::get_encoder_count_callback(const Axis& axis) {
|
||||
can_Message_t txmsg;
|
||||
txmsg.id = axis.config_.can.node_id << NUM_CMD_ID_BITS;
|
||||
txmsg.id += MSG_GET_ENCODER_COUNT;
|
||||
txmsg.isExt = axis.config_.can.is_extended;
|
||||
txmsg.len = 8;
|
||||
|
||||
can_setSignal<int32_t>(txmsg, axis.encoder_.shadow_count_, 0, 32, true);
|
||||
can_setSignal<int32_t>(txmsg, axis.encoder_.count_in_cpr_, 32, 32, true);
|
||||
return canbus_->send_message(txmsg);
|
||||
}
|
||||
|
||||
void CANSimple::set_input_pos_callback(Axis& axis, const can_Message_t& msg) {
|
||||
axis.controller_.set_input_pos_and_steps(can_getSignal<float>(msg, 0, 32, true));
|
||||
axis.controller_.input_vel_ = can_getSignal<int16_t>(msg, 32, 16, true, 0.001f, 0);
|
||||
axis.controller_.input_torque_ = can_getSignal<int16_t>(msg, 48, 16, true, 0.001f, 0);
|
||||
axis.controller_.input_pos_updated();
|
||||
}
|
||||
|
||||
void CANSimple::set_input_vel_callback(Axis& axis, const can_Message_t& msg) {
|
||||
axis.controller_.input_vel_ = can_getSignal<float>(msg, 0, 32, true);
|
||||
axis.controller_.input_torque_ = can_getSignal<float>(msg, 32, 32, true);
|
||||
}
|
||||
|
||||
void CANSimple::set_input_torque_callback(Axis& axis, const can_Message_t& msg) {
|
||||
axis.controller_.input_torque_ = can_getSignal<float>(msg, 0, 32, true);
|
||||
}
|
||||
|
||||
void CANSimple::set_controller_modes_callback(Axis& axis, const can_Message_t& msg) {
|
||||
Controller::ControlMode const mode = static_cast<Controller::ControlMode>(can_getSignal<int32_t>(msg, 0, 32, true));
|
||||
axis.controller_.config_.control_mode = static_cast<Controller::ControlMode>(mode);
|
||||
axis.controller_.config_.input_mode = static_cast<Controller::InputMode>(can_getSignal<int32_t>(msg, 32, 32, true));
|
||||
axis.controller_.control_mode_updated();
|
||||
}
|
||||
|
||||
void CANSimple::set_limits_callback(Axis& axis, const can_Message_t& msg) {
|
||||
axis.controller_.config_.vel_limit = can_getSignal<float>(msg, 0, 32, true);
|
||||
axis.motor_.config_.current_lim = can_getSignal<float>(msg, 32, 32, true);
|
||||
}
|
||||
|
||||
void CANSimple::start_anticogging_callback(const Axis& axis, const can_Message_t& msg) {
|
||||
axis.controller_.start_anticogging_calibration();
|
||||
}
|
||||
|
||||
void CANSimple::set_traj_vel_limit_callback(Axis& axis, const can_Message_t& msg) {
|
||||
axis.trap_traj_.config_.vel_limit = can_getSignal<float>(msg, 0, 32, true);
|
||||
}
|
||||
|
||||
void CANSimple::set_traj_accel_limits_callback(Axis& axis, const can_Message_t& msg) {
|
||||
axis.trap_traj_.config_.accel_limit = can_getSignal<float>(msg, 0, 32, true);
|
||||
axis.trap_traj_.config_.decel_limit = can_getSignal<float>(msg, 32, 32, true);
|
||||
}
|
||||
|
||||
void CANSimple::set_traj_inertia_callback(Axis& axis, const can_Message_t& msg) {
|
||||
axis.controller_.config_.inertia = can_getSignal<float>(msg, 0, 32, true);
|
||||
}
|
||||
|
||||
void CANSimple::set_linear_count_callback(Axis& axis, const can_Message_t& msg) {
|
||||
axis.encoder_.set_linear_count(can_getSignal<int32_t>(msg, 0, 32, true));
|
||||
}
|
||||
|
||||
void CANSimple::set_pos_gain_callback(Axis& axis, const can_Message_t& msg) {
|
||||
axis.controller_.config_.pos_gain = can_getSignal<float>(msg, 0, 32, true);
|
||||
}
|
||||
|
||||
void CANSimple::set_vel_gains_callback(Axis& axis, const can_Message_t& msg) {
|
||||
axis.controller_.config_.vel_gain = can_getSignal<float>(msg, 0, 32, true);
|
||||
axis.controller_.config_.vel_integrator_gain = can_getSignal<float>(msg, 32, 32, true);
|
||||
}
|
||||
|
||||
bool CANSimple::get_iq_callback(const Axis& axis) {
|
||||
can_Message_t txmsg;
|
||||
txmsg.id = axis.config_.can.node_id << NUM_CMD_ID_BITS;
|
||||
txmsg.id += MSG_GET_IQ;
|
||||
txmsg.isExt = axis.config_.can.is_extended;
|
||||
txmsg.len = 8;
|
||||
|
||||
std::optional<float2D> Idq_setpoint = axis.motor_.current_control_.Idq_setpoint_;
|
||||
if (!Idq_setpoint.has_value()) {
|
||||
Idq_setpoint = {0.0f, 0.0f};
|
||||
}
|
||||
|
||||
static_assert(sizeof(float) == sizeof(Idq_setpoint->second));
|
||||
static_assert(sizeof(float) == sizeof(axis.motor_.current_control_.Iq_measured_));
|
||||
can_setSignal<float>(txmsg, Idq_setpoint->second, 0, 32, true);
|
||||
can_setSignal<float>(txmsg, axis.motor_.current_control_.Iq_measured_, 32, 32, true);
|
||||
|
||||
return canbus_->send_message(txmsg);
|
||||
}
|
||||
|
||||
bool CANSimple::get_bus_voltage_current_callback(const Axis& axis) {
|
||||
can_Message_t txmsg;
|
||||
|
||||
txmsg.id = axis.config_.can.node_id << NUM_CMD_ID_BITS;
|
||||
txmsg.id += MSG_GET_BUS_VOLTAGE_CURRENT;
|
||||
txmsg.isExt = axis.config_.can.is_extended;
|
||||
txmsg.len = 8;
|
||||
|
||||
static_assert(sizeof(float) == sizeof(vbus_voltage));
|
||||
static_assert(sizeof(float) == sizeof(ibus_));
|
||||
can_setSignal<float>(txmsg, vbus_voltage, 0, 32, true);
|
||||
can_setSignal<float>(txmsg, ibus_, 32, 32, true);
|
||||
|
||||
return canbus_->send_message(txmsg);
|
||||
}
|
||||
|
||||
bool CANSimple::get_adc_voltage_callback(const Axis& axis, const can_Message_t& msg) {
|
||||
can_Message_t txmsg;
|
||||
|
||||
txmsg.id = axis.config_.can.node_id << NUM_CMD_ID_BITS;
|
||||
txmsg.id += MSG_GET_ADC_VOLTAGE;
|
||||
txmsg.isExt = axis.config_.can.is_extended;
|
||||
txmsg.len = 8;
|
||||
|
||||
auto gpio_num = can_getSignal<uint8_t>(msg, 0, 8, true);
|
||||
if (gpio_num < GPIO_COUNT) {
|
||||
auto voltage = get_adc_voltage(get_gpio(gpio_num));
|
||||
can_setSignal<float>(txmsg, voltage, 0, 32, true);
|
||||
return canbus_->send_message(txmsg);
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
void CANSimple::clear_errors_callback(Axis& axis, const can_Message_t& msg) {
|
||||
odrv.clear_errors(); // TODO: might want to clear axis errors only
|
||||
}
|
||||
|
||||
uint32_t CANSimple::service_stack() {
|
||||
uint32_t nextServiceTime = UINT32_MAX;
|
||||
uint32_t now = HAL_GetTick();
|
||||
|
||||
// TODO: remove this polling loop and replace with protocol hook
|
||||
for (size_t i = 0; i < AXIS_COUNT; ++i) {
|
||||
bool node_id_changed = (axes[i].config_.can.node_id != node_ids_[i]) || (axes[i].config_.can.is_extended != extended_node_ids_[i]);
|
||||
if (node_id_changed) {
|
||||
renew_subscription(i);
|
||||
}
|
||||
}
|
||||
|
||||
struct periodic {
|
||||
const uint32_t& rate;
|
||||
uint32_t& last_time;
|
||||
bool (CANSimple::* callback)(const Axis& axis);
|
||||
};
|
||||
|
||||
for (auto& axis : axes) {
|
||||
std::array<periodic, 10> periodics = {{
|
||||
{axis.config_.can.heartbeat_rate_ms, axis.can_.last_heartbeat, &CANSimple::send_heartbeat},
|
||||
{axis.config_.can.encoder_rate_ms, axis.can_.last_encoder, &CANSimple::get_encoder_estimates_callback},
|
||||
{axis.config_.can.motor_error_rate_ms, axis.can_.last_motor_error, &CANSimple::get_motor_error_callback},
|
||||
{axis.config_.can.encoder_error_rate_ms, axis.can_.last_encoder_error, &CANSimple::get_encoder_error_callback},
|
||||
{axis.config_.can.controller_error_rate_ms, axis.can_.last_controller_error, &CANSimple::get_controller_error_callback},
|
||||
{axis.config_.can.sensorless_error_rate_ms, axis.can_.last_sensorless_error, &CANSimple::get_sensorless_error_callback},
|
||||
{axis.config_.can.encoder_count_rate_ms, axis.can_.last_encoder_count, &CANSimple::get_encoder_count_callback},
|
||||
{axis.config_.can.iq_rate_ms, axis.can_.last_iq, &CANSimple::get_iq_callback},
|
||||
{axis.config_.can.sensorless_rate_ms, axis.can_.last_sensorless, &CANSimple::get_sensorless_estimates_callback},
|
||||
{axis.config_.can.bus_vi_rate_ms, axis.can_.last_bus_vi, &CANSimple::get_bus_voltage_current_callback},
|
||||
}};
|
||||
|
||||
MEASURE_TIME(axis.task_times_.can_heartbeat) {
|
||||
for (auto& msg : periodics) {
|
||||
if (msg.rate > 0) {
|
||||
if ((now - msg.last_time) >= msg.rate) {
|
||||
if (std::invoke(msg.callback, this, axis)) {
|
||||
msg.last_time = now;
|
||||
}
|
||||
}
|
||||
|
||||
int nextAxisService = msg.last_time + msg.rate - now;
|
||||
nextServiceTime = std::min(nextServiceTime, static_cast<uint32_t>(std::max(0, nextAxisService)));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nextServiceTime;
|
||||
}
|
||||
|
||||
bool CANSimple::send_heartbeat(const Axis& axis) {
|
||||
can_Message_t txmsg;
|
||||
txmsg.id = axis.config_.can.node_id << NUM_CMD_ID_BITS;
|
||||
txmsg.id += MSG_ODRIVE_HEARTBEAT; // heartbeat ID
|
||||
txmsg.isExt = axis.config_.can.is_extended;
|
||||
txmsg.len = 8;
|
||||
|
||||
can_setSignal(txmsg, axis.error_, 0, 32, true);
|
||||
can_setSignal(txmsg, uint8_t(axis.current_state_), 32, 8, true);
|
||||
|
||||
// Motor flags
|
||||
uint8_t motorFlags = axis.motor_.error_ != 0;
|
||||
|
||||
// Encoder flags
|
||||
uint8_t encoderFlags = axis.encoder_.error_ != 0;
|
||||
|
||||
// Controller flags
|
||||
uint8_t controllerFlags =axis.controller_.error_ != 0;
|
||||
uint8_t trajDone = uint8_t(axis.controller_.trajectory_done_) << 7;
|
||||
controllerFlags |= trajDone;
|
||||
|
||||
can_setSignal(txmsg, motorFlags, 40, 8, true);
|
||||
can_setSignal(txmsg, encoderFlags, 48, 8, true);
|
||||
can_setSignal(txmsg, controllerFlags, 56, 8, true);
|
||||
|
||||
return canbus_->send_message(txmsg);
|
||||
}
|
||||
@@ -0,0 +1,113 @@
|
||||
#ifndef __CAN_SIMPLE_HPP_
|
||||
#define __CAN_SIMPLE_HPP_
|
||||
|
||||
#include "canbus.hpp"
|
||||
#include "axis.hpp"
|
||||
|
||||
class CANSimple {
|
||||
public:
|
||||
enum {
|
||||
MSG_CO_NMT_CTRL = 0x000, // CANOpen NMT Message REC
|
||||
MSG_ODRIVE_HEARTBEAT,
|
||||
MSG_ODRIVE_ESTOP,
|
||||
MSG_GET_MOTOR_ERROR, // Errors
|
||||
MSG_GET_ENCODER_ERROR,
|
||||
MSG_GET_SENSORLESS_ERROR,
|
||||
MSG_SET_AXIS_NODE_ID,
|
||||
MSG_SET_AXIS_REQUESTED_STATE,
|
||||
MSG_SET_AXIS_STARTUP_CONFIG,
|
||||
MSG_GET_ENCODER_ESTIMATES,
|
||||
MSG_GET_ENCODER_COUNT,
|
||||
MSG_SET_CONTROLLER_MODES,
|
||||
MSG_SET_INPUT_POS,
|
||||
MSG_SET_INPUT_VEL,
|
||||
MSG_SET_INPUT_TORQUE,
|
||||
MSG_SET_LIMITS,
|
||||
MSG_START_ANTICOGGING,
|
||||
MSG_SET_TRAJ_VEL_LIMIT,
|
||||
MSG_SET_TRAJ_ACCEL_LIMITS,
|
||||
MSG_SET_TRAJ_INERTIA,
|
||||
MSG_GET_IQ,
|
||||
MSG_GET_SENSORLESS_ESTIMATES,
|
||||
MSG_RESET_ODRIVE,
|
||||
MSG_GET_BUS_VOLTAGE_CURRENT,
|
||||
MSG_CLEAR_ERRORS,
|
||||
MSG_SET_LINEAR_COUNT,
|
||||
MSG_SET_POS_GAIN,
|
||||
MSG_SET_VEL_GAINS,
|
||||
MSG_GET_ADC_VOLTAGE,
|
||||
MSG_GET_CONTROLLER_ERROR,
|
||||
MSG_CO_HEARTBEAT_CMD = 0x700, // CANOpen NMT Heartbeat SEND
|
||||
};
|
||||
|
||||
CANSimple(CanBusBase* canbus) : canbus_(canbus) {}
|
||||
|
||||
bool init();
|
||||
uint32_t service_stack();
|
||||
|
||||
private:
|
||||
|
||||
bool renew_subscription(size_t i);
|
||||
bool send_heartbeat(const Axis& axis);
|
||||
|
||||
void handle_can_message(const can_Message_t& msg);
|
||||
|
||||
void do_command(Axis& axis, const can_Message_t& cmd);
|
||||
|
||||
// Get functions (msg.rtr bit must be set)
|
||||
bool get_motor_error_callback(const Axis& axis);
|
||||
bool get_encoder_error_callback(const Axis& axis);
|
||||
bool get_controller_error_callback(const Axis& axis);
|
||||
bool get_sensorless_error_callback(const Axis& axis);
|
||||
bool get_encoder_estimates_callback(const Axis& axis);
|
||||
bool get_encoder_count_callback(const Axis& axis);
|
||||
bool get_iq_callback(const Axis& axis);
|
||||
bool get_sensorless_estimates_callback(const Axis& axis);
|
||||
bool get_bus_voltage_current_callback(const Axis& axis);
|
||||
// msg.rtr bit must NOT be set
|
||||
bool get_adc_voltage_callback(const Axis& axis, const can_Message_t& msg);
|
||||
|
||||
// Set functions
|
||||
static void set_axis_nodeid_callback(Axis& axis, const can_Message_t& msg);
|
||||
static void set_axis_requested_state_callback(Axis& axis, const can_Message_t& msg);
|
||||
static void set_axis_startup_config_callback(Axis& axis, const can_Message_t& msg);
|
||||
static void set_input_pos_callback(Axis& axis, const can_Message_t& msg);
|
||||
static void set_input_vel_callback(Axis& axis, const can_Message_t& msg);
|
||||
static void set_input_torque_callback(Axis& axis, const can_Message_t& msg);
|
||||
static void set_controller_modes_callback(Axis& axis, const can_Message_t& msg);
|
||||
static void set_limits_callback(Axis& axis, const can_Message_t& msg);
|
||||
static void set_traj_vel_limit_callback(Axis& axis, const can_Message_t& msg);
|
||||
static void set_traj_accel_limits_callback(Axis& axis, const can_Message_t& msg);
|
||||
static void set_traj_inertia_callback(Axis& axis, const can_Message_t& msg);
|
||||
static void set_linear_count_callback(Axis& axis, const can_Message_t& msg);
|
||||
static void set_pos_gain_callback(Axis& axis, const can_Message_t& msg);
|
||||
static void set_vel_gains_callback(Axis& axis, const can_Message_t& msg);
|
||||
|
||||
// Other functions
|
||||
static void nmt_callback(const Axis& axis, const can_Message_t& msg);
|
||||
static void estop_callback(Axis& axis, const can_Message_t& msg);
|
||||
static void clear_errors_callback(Axis& axis, const can_Message_t& msg);
|
||||
static void start_anticogging_callback(const Axis& axis, const can_Message_t& msg);
|
||||
|
||||
static constexpr uint8_t NUM_NODE_ID_BITS = 6;
|
||||
static constexpr uint8_t NUM_CMD_ID_BITS = 11 - NUM_NODE_ID_BITS;
|
||||
|
||||
// Utility functions
|
||||
static constexpr uint32_t get_node_id(uint32_t msgID) {
|
||||
return (msgID >> NUM_CMD_ID_BITS); // Upper 6 or more bits
|
||||
};
|
||||
|
||||
static constexpr uint8_t get_cmd_id(uint32_t msgID) {
|
||||
return (msgID & 0x01F); // Bottom 5 bits
|
||||
}
|
||||
|
||||
CanBusBase* canbus_;
|
||||
CanBusBase::CanSubscription* subscription_handles_[AXIS_COUNT];
|
||||
|
||||
// TODO: we this is a hack but actually we should use protocol hooks to
|
||||
// renew our filter when the node ID changes
|
||||
uint32_t node_ids_[AXIS_COUNT];
|
||||
bool extended_node_ids_[AXIS_COUNT];
|
||||
};
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,43 @@
|
||||
#ifndef __CANBUS_HPP
|
||||
#define __CANBUS_HPP
|
||||
|
||||
#include "can_helpers.hpp"
|
||||
#include <variant>
|
||||
|
||||
struct MsgIdFilterSpecs {
|
||||
std::variant<uint16_t, uint32_t> id;
|
||||
uint32_t mask;
|
||||
};
|
||||
|
||||
class CanBusBase {
|
||||
public:
|
||||
typedef void(*on_can_message_cb_t)(void* ctx, const can_Message_t& message);
|
||||
struct CanSubscription {};
|
||||
|
||||
/**
|
||||
* @brief Sends the specified CAN message.
|
||||
*
|
||||
* @returns: true on success or false otherwise (e.g. if the send queue is
|
||||
* full).
|
||||
*/
|
||||
virtual bool send_message(const can_Message_t& message) = 0;
|
||||
|
||||
/**
|
||||
* @brief Registers a callback that will be invoked for every incoming CAN
|
||||
* message that matches the filter.
|
||||
*
|
||||
* @param handle: On success this handle is set to an opaque pointer that
|
||||
* can be used to cancel the subscription.
|
||||
*
|
||||
* @returns: true on success or false otherwise (e.g. if the maximum number
|
||||
* of subscriptions has been reached).
|
||||
*/
|
||||
virtual bool subscribe(const MsgIdFilterSpecs& filter, on_can_message_cb_t callback, void* ctx, CanSubscription** handle) = 0;
|
||||
|
||||
/**
|
||||
* @brief Deregisters a callback that was previously registered with subscribe().
|
||||
*/
|
||||
virtual bool unsubscribe(CanSubscription* handle) = 0;
|
||||
};
|
||||
|
||||
#endif // __CANBUS_HPP
|
||||
@@ -0,0 +1,238 @@
|
||||
#include "odrive_can.hpp"
|
||||
|
||||
#include <can.h>
|
||||
#include <cmsis_os.h>
|
||||
|
||||
#include "freertos_vars.h"
|
||||
#include "utils.hpp"
|
||||
|
||||
// Safer context handling via maps instead of arrays
|
||||
// #include <unordered_map>
|
||||
// std::unordered_map<CAN_HandleTypeDef *, ODriveCAN *> ctxMap;
|
||||
|
||||
|
||||
bool ODriveCAN::apply_config() {
|
||||
config_.parent = this;
|
||||
set_baud_rate(config_.baud_rate);
|
||||
return true;
|
||||
}
|
||||
|
||||
bool ODriveCAN::reinit() {
|
||||
HAL_CAN_Stop(handle_);
|
||||
HAL_CAN_ResetError(handle_);
|
||||
return (HAL_CAN_Init(handle_) == HAL_OK)
|
||||
&& (HAL_CAN_Start(handle_) == HAL_OK)
|
||||
&& (HAL_CAN_ActivateNotification(handle_, CAN_IT_RX_FIFO0_MSG_PENDING | CAN_IT_RX_FIFO1_MSG_PENDING | CAN_IT_TX_MAILBOX_EMPTY) == HAL_OK);
|
||||
}
|
||||
|
||||
bool ODriveCAN::start_server(CAN_HandleTypeDef* handle) {
|
||||
handle_ = handle;
|
||||
|
||||
handle_->Init.Prescaler = CAN_FREQ / config_.baud_rate;
|
||||
if (!reinit()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
auto wrapper = [](void* ctx) {
|
||||
((ODriveCAN*)ctx)->can_server_thread();
|
||||
};
|
||||
osThreadDef(can_server_thread_def, wrapper, osPriorityNormal, 0, stack_size_ / sizeof(StackType_t));
|
||||
thread_id_ = osThreadCreate(osThread(can_server_thread_def), this);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
void ODriveCAN::can_server_thread() {
|
||||
Protocol protocol = config_.protocol;
|
||||
|
||||
if (protocol & PROTOCOL_SIMPLE) {
|
||||
can_simple_.init();
|
||||
}
|
||||
|
||||
for (;;) {
|
||||
uint32_t status = HAL_CAN_GetError(handle_);
|
||||
if (status == HAL_CAN_ERROR_NONE) {
|
||||
uint32_t next_service_time = UINT32_MAX;
|
||||
|
||||
if (protocol & PROTOCOL_SIMPLE) {
|
||||
next_service_time = std::min(can_simple_.service_stack(), next_service_time);
|
||||
}
|
||||
|
||||
process_rx_fifo(CAN_RX_FIFO0);
|
||||
process_rx_fifo(CAN_RX_FIFO1);
|
||||
HAL_CAN_ActivateNotification(handle_, CAN_IT_RX_FIFO0_MSG_PENDING | CAN_IT_RX_FIFO1_MSG_PENDING | CAN_IT_TX_MAILBOX_EMPTY);
|
||||
|
||||
// wait at least 1ms to prevent busy-spin on failed sends
|
||||
osSemaphoreWait(sem_can, std::max(next_service_time, 1UL));
|
||||
} else if (status == HAL_CAN_ERROR_TIMEOUT) {
|
||||
HAL_CAN_ResetError(handle_);
|
||||
status = HAL_CAN_Start(handle_);
|
||||
if (status == HAL_OK)
|
||||
status = HAL_CAN_ActivateNotification(handle_, CAN_IT_RX_FIFO0_MSG_PENDING | CAN_IT_TX_MAILBOX_EMPTY);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Set one of only a few common baud rates. CAN doesn't do arbitrary baud rates well due to the time-quanta issue.
|
||||
// 21 TQ allows for easy sampling at exactly 80% (recommended by Vector Informatik GmbH for high reliability systems)
|
||||
// Conveniently, the CAN peripheral's 42MHz clock lets us easily create 21TQs for all common baud rates
|
||||
bool ODriveCAN::set_baud_rate(uint32_t baud_rate) {
|
||||
uint32_t prescaler = CAN_FREQ / baud_rate;
|
||||
if (prescaler * baud_rate == CAN_FREQ) {
|
||||
// valid baud rate
|
||||
config_.baud_rate = baud_rate;
|
||||
if (handle_) {
|
||||
handle_->Init.Prescaler = prescaler;
|
||||
return reinit();
|
||||
}
|
||||
return true;
|
||||
} else {
|
||||
// invalid baud rate - ignore
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
void ODriveCAN::process_rx_fifo(uint32_t fifo) {
|
||||
while (HAL_CAN_GetRxFifoFillLevel(handle_, fifo)) {
|
||||
CAN_RxHeaderTypeDef header;
|
||||
can_Message_t rxmsg;
|
||||
HAL_CAN_GetRxMessage(handle_, fifo, &header, rxmsg.buf);
|
||||
|
||||
rxmsg.isExt = header.IDE;
|
||||
rxmsg.id = rxmsg.isExt ? header.ExtId : header.StdId; // If it's an extended message, pass the extended ID
|
||||
rxmsg.len = header.DLC;
|
||||
rxmsg.rtr = header.RTR;
|
||||
|
||||
// TODO: this could be optimized with an ahead-of-time computed
|
||||
// index-to-filter map
|
||||
|
||||
size_t fifo0_idx = 0;
|
||||
size_t fifo1_idx = 0;
|
||||
|
||||
// Find the triggered subscription item based on header.FilterMatchIndex
|
||||
auto it = std::find_if(subscriptions_.begin(), subscriptions_.end(), [&](auto& s) {
|
||||
size_t current_idx = (s.fifo == 0 ? fifo0_idx : fifo1_idx)++;
|
||||
return (header.FilterMatchIndex == current_idx) && (s.fifo == fifo);
|
||||
});
|
||||
|
||||
if (it == subscriptions_.end()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
it->callback(it->ctx, rxmsg);
|
||||
}
|
||||
}
|
||||
|
||||
// Send a CAN message on the bus
|
||||
bool ODriveCAN::send_message(const can_Message_t &txmsg) {
|
||||
if (HAL_CAN_GetError(handle_) != HAL_CAN_ERROR_NONE) {
|
||||
return false;
|
||||
}
|
||||
|
||||
CAN_TxHeaderTypeDef header;
|
||||
header.StdId = txmsg.id;
|
||||
header.ExtId = txmsg.id;
|
||||
header.IDE = txmsg.isExt ? CAN_ID_EXT : CAN_ID_STD;
|
||||
header.RTR = CAN_RTR_DATA;
|
||||
header.DLC = txmsg.len;
|
||||
header.TransmitGlobalTime = FunctionalState::DISABLE;
|
||||
|
||||
uint32_t retTxMailbox = 0;
|
||||
if (!HAL_CAN_GetTxMailboxesFreeLevel(handle_)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return HAL_CAN_AddTxMessage(handle_, &header, (uint8_t*)txmsg.buf, &retTxMailbox) == HAL_OK;
|
||||
}
|
||||
|
||||
//void ODriveCAN::set_error(Error error) {
|
||||
// error_ |= error;
|
||||
//}
|
||||
|
||||
bool ODriveCAN::subscribe(const MsgIdFilterSpecs& filter, on_can_message_cb_t callback, void* ctx, CanSubscription** handle) {
|
||||
auto it = std::find_if(subscriptions_.begin(), subscriptions_.end(), [](auto& subscription) {
|
||||
return subscription.fifo == kCanFifoNone;
|
||||
});
|
||||
|
||||
if (it == subscriptions_.end()) {
|
||||
return false; // all subscription slots in use
|
||||
}
|
||||
|
||||
it->callback = callback;
|
||||
it->ctx = ctx;
|
||||
it->fifo = CAN_RX_FIFO0; // TODO: make customizable
|
||||
if (handle) {
|
||||
*handle = &*it;
|
||||
}
|
||||
|
||||
bool is_extended = filter.id.index() == 1;
|
||||
uint32_t id = is_extended ?
|
||||
((std::get<1>(filter.id) << 3) | (1 << 2)) :
|
||||
(std::get<0>(filter.id) << 21);
|
||||
uint32_t mask = (is_extended ? (filter.mask << 3) : (filter.mask << 21))
|
||||
| (1 << 2); // care about the is_extended bit
|
||||
|
||||
CAN_FilterTypeDef hal_filter;
|
||||
hal_filter.FilterActivation = ENABLE;
|
||||
hal_filter.FilterBank = &*it - &subscriptions_[0];
|
||||
hal_filter.FilterFIFOAssignment = it->fifo;
|
||||
hal_filter.FilterIdHigh = (id >> 16) & 0xffff;
|
||||
hal_filter.FilterIdLow = id & 0xffff;
|
||||
hal_filter.FilterMaskIdHigh = (mask >> 16) & 0xffff;
|
||||
hal_filter.FilterMaskIdLow = mask & 0xffff;
|
||||
hal_filter.FilterMode = CAN_FILTERMODE_IDMASK;
|
||||
hal_filter.FilterScale = CAN_FILTERSCALE_32BIT;
|
||||
|
||||
if (HAL_CAN_ConfigFilter(handle_, &hal_filter) != HAL_OK) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
bool ODriveCAN::unsubscribe(CanSubscription* handle) {
|
||||
ODriveCanSubscription* subscription = static_cast<ODriveCanSubscription*>(handle);
|
||||
if (subscription < subscriptions_.begin() || subscription >= subscriptions_.end()) {
|
||||
return false;
|
||||
}
|
||||
if (subscription->fifo != kCanFifoNone) {
|
||||
return false; // not in use
|
||||
}
|
||||
|
||||
subscription->fifo = kCanFifoNone;
|
||||
|
||||
CAN_FilterTypeDef hal_filter = {};
|
||||
hal_filter.FilterActivation = DISABLE;
|
||||
return HAL_CAN_ConfigFilter(handle_, &hal_filter) == HAL_OK;
|
||||
}
|
||||
|
||||
void HAL_CAN_TxMailbox0CompleteCallback(CAN_HandleTypeDef *hcan) {
|
||||
HAL_CAN_DeactivateNotification(hcan, CAN_IT_TX_MAILBOX_EMPTY);
|
||||
osSemaphoreRelease(sem_can);
|
||||
}
|
||||
void HAL_CAN_TxMailbox1CompleteCallback(CAN_HandleTypeDef *hcan) {
|
||||
HAL_CAN_DeactivateNotification(hcan, CAN_IT_TX_MAILBOX_EMPTY);
|
||||
osSemaphoreRelease(sem_can);
|
||||
}
|
||||
void HAL_CAN_TxMailbox2CompleteCallback(CAN_HandleTypeDef *hcan) {
|
||||
HAL_CAN_DeactivateNotification(hcan, CAN_IT_TX_MAILBOX_EMPTY);
|
||||
osSemaphoreRelease(sem_can);
|
||||
}
|
||||
void HAL_CAN_TxMailbox0AbortCallback(CAN_HandleTypeDef *hcan) {}
|
||||
void HAL_CAN_TxMailbox1AbortCallback(CAN_HandleTypeDef *hcan) {}
|
||||
void HAL_CAN_TxMailbox2AbortCallback(CAN_HandleTypeDef *hcan) {}
|
||||
void HAL_CAN_RxFifo0MsgPendingCallback(CAN_HandleTypeDef *hcan) {
|
||||
HAL_CAN_DeactivateNotification(hcan, CAN_IT_RX_FIFO0_MSG_PENDING);
|
||||
osSemaphoreRelease(sem_can);
|
||||
}
|
||||
void HAL_CAN_RxFifo0FullCallback(CAN_HandleTypeDef *hcan) {
|
||||
HAL_CAN_DeactivateNotification(hcan, CAN_IT_RX_FIFO1_MSG_PENDING);
|
||||
osSemaphoreRelease(sem_can);
|
||||
}
|
||||
void HAL_CAN_RxFifo1MsgPendingCallback(CAN_HandleTypeDef *hcan) {}
|
||||
void HAL_CAN_RxFifo1FullCallback(CAN_HandleTypeDef *hcan) {}
|
||||
void HAL_CAN_SleepCallback(CAN_HandleTypeDef *hcan) {}
|
||||
void HAL_CAN_WakeUpFromRxMsgCallback(CAN_HandleTypeDef *hcan) {}
|
||||
|
||||
void HAL_CAN_ErrorCallback(CAN_HandleTypeDef *hcan) {
|
||||
//HAL_CAN_ResetError(hcan);
|
||||
}
|
||||
@@ -0,0 +1,68 @@
|
||||
#ifndef __ODRIVE_CAN_HPP
|
||||
#define __ODRIVE_CAN_HPP
|
||||
|
||||
#include <cmsis_os.h>
|
||||
|
||||
#include "canbus.hpp"
|
||||
#include "can_simple.hpp"
|
||||
#include <autogen/interfaces.hpp>
|
||||
|
||||
#define CAN_CLK_HZ (42000000)
|
||||
#define CAN_CLK_MHZ (42)
|
||||
|
||||
// Anonymous enum for defining the most common CAN baud rates
|
||||
enum {
|
||||
CAN_BAUD_125K = 125000,
|
||||
CAN_BAUD_250K = 250000,
|
||||
CAN_BAUD_500K = 500000,
|
||||
CAN_BAUD_1000K = 1000000,
|
||||
CAN_BAUD_1M = 1000000
|
||||
};
|
||||
|
||||
class ODriveCAN : public CanBusBase, public ODriveIntf::CanIntf {
|
||||
public:
|
||||
struct Config_t {
|
||||
uint32_t baud_rate = CAN_BAUD_250K;
|
||||
Protocol protocol = PROTOCOL_SIMPLE;
|
||||
|
||||
ODriveCAN* parent = nullptr; // set in apply_config()
|
||||
void set_baud_rate(uint32_t value) { parent->set_baud_rate(value); }
|
||||
};
|
||||
|
||||
ODriveCAN() {}
|
||||
|
||||
bool apply_config();
|
||||
bool start_server(CAN_HandleTypeDef* handle);
|
||||
|
||||
Error error_ = ERROR_NONE;
|
||||
|
||||
Config_t config_;
|
||||
CANSimple can_simple_{this};
|
||||
|
||||
osThreadId thread_id_;
|
||||
const uint32_t stack_size_ = 1024; // Bytes
|
||||
|
||||
private:
|
||||
static const uint8_t kCanFifoNone = 0xff;
|
||||
|
||||
struct ODriveCanSubscription : CanSubscription {
|
||||
uint8_t fifo = kCanFifoNone;
|
||||
on_can_message_cb_t callback;
|
||||
void* ctx;
|
||||
};
|
||||
|
||||
bool reinit();
|
||||
void can_server_thread();
|
||||
bool set_baud_rate(uint32_t baud_rate);
|
||||
void process_rx_fifo(uint32_t fifo);
|
||||
bool send_message(const can_Message_t& message) final;
|
||||
bool subscribe(const MsgIdFilterSpecs& filter, on_can_message_cb_t callback, void* ctx, CanSubscription** handle) final;
|
||||
bool unsubscribe(CanSubscription* handle) final;
|
||||
|
||||
// Hardware supports at most 28 filters unless we do optimizations. For now
|
||||
// we don't need that many.
|
||||
std::array<ODriveCanSubscription, 8> subscriptions_;
|
||||
CAN_HandleTypeDef *handle_ = nullptr;
|
||||
};
|
||||
|
||||
#endif // __ODRIVE_CAN_HPP
|
||||
@@ -0,0 +1,99 @@
|
||||
|
||||
/* Includes ------------------------------------------------------------------*/
|
||||
|
||||
#include "communication.h"
|
||||
|
||||
#include "interface_usb.h"
|
||||
#include "interface_uart.h"
|
||||
#include "interface_can.hpp"
|
||||
#include "interface_i2c.h"
|
||||
|
||||
#include "odrive_main.h"
|
||||
#include "freertos_vars.h"
|
||||
#include "utils.hpp"
|
||||
|
||||
#include <cmsis_os.h>
|
||||
#include <memory>
|
||||
//#include <usbd_cdc_if.h>
|
||||
//#include <usb_device.h>
|
||||
//#include <usart.h>
|
||||
#include <gpio.h>
|
||||
|
||||
#include <type_traits>
|
||||
|
||||
/* Private defines -----------------------------------------------------------*/
|
||||
/* Private macros ------------------------------------------------------------*/
|
||||
/* Private typedef -----------------------------------------------------------*/
|
||||
/* Global constant data ------------------------------------------------------*/
|
||||
/* Global variables ----------------------------------------------------------*/
|
||||
|
||||
uint64_t serial_number;
|
||||
char serial_number_str[13]; // 12 digits + null termination
|
||||
|
||||
/* Private constant data -----------------------------------------------------*/
|
||||
/* Private variables ---------------------------------------------------------*/
|
||||
/* Private function prototypes -----------------------------------------------*/
|
||||
/* Function implementations --------------------------------------------------*/
|
||||
|
||||
void init_communication(void) {
|
||||
//printf("hi!\r\n");
|
||||
|
||||
// Dual UART operation not supported yet
|
||||
if (odrv.config_.enable_uart_a && odrv.config_.enable_uart_b) {
|
||||
odrv.misconfigured_ = true;
|
||||
}
|
||||
|
||||
if (odrv.config_.enable_uart_a && uart_a) {
|
||||
start_uart_server(uart_a);
|
||||
} else if (odrv.config_.enable_uart_b && uart_b) {
|
||||
start_uart_server(uart_b);
|
||||
}
|
||||
|
||||
start_usb_server();
|
||||
|
||||
if (odrv.config_.enable_i2c_a) {
|
||||
start_i2c_server();
|
||||
}
|
||||
|
||||
if (odrv.config_.enable_can_a) {
|
||||
odrv.can_.start_server(&hcan1);
|
||||
}
|
||||
}
|
||||
|
||||
#include <fibre/async_stream.hpp>
|
||||
|
||||
|
||||
extern "C" {
|
||||
int _write(int file, const char* data, int len) __attribute__((used));
|
||||
}
|
||||
|
||||
// @brief This is what printf calls internally
|
||||
int _write(int file, const char* data, int len) {
|
||||
fibre::cbufptr_t buf{(const uint8_t*)data, (const uint8_t*)data + len};
|
||||
|
||||
if (odrv.config_.uart0_protocol == ODrive::STREAM_PROTOCOL_TYPE_STDOUT ||
|
||||
odrv.config_.uart0_protocol == ODrive::STREAM_PROTOCOL_TYPE_ASCII_AND_STDOUT) {
|
||||
uart0_stdout_sink.write(buf);
|
||||
if (!uart0_stdout_pending) {
|
||||
uart0_stdout_pending = true;
|
||||
osMessagePut(uart_event_queue, 3, 0);
|
||||
}
|
||||
}
|
||||
|
||||
if (odrv.config_.usb_cdc_protocol == ODrive::STREAM_PROTOCOL_TYPE_STDOUT ||
|
||||
odrv.config_.usb_cdc_protocol == ODrive::STREAM_PROTOCOL_TYPE_ASCII_AND_STDOUT) {
|
||||
usb_cdc_stdout_sink.write(buf);
|
||||
if (!usb_cdc_stdout_pending) {
|
||||
usb_cdc_stdout_pending = true;
|
||||
osMessagePut(usb_event_queue, 7, 0);
|
||||
}
|
||||
}
|
||||
|
||||
return len; // Always pretend that we processed everything
|
||||
}
|
||||
|
||||
|
||||
#include "../autogen/function_stubs.hpp"
|
||||
|
||||
ODrive& ep_root = odrv;
|
||||
#include "../autogen/endpoints.hpp"
|
||||
@@ -0,0 +1,23 @@
|
||||
#ifndef COMMANDS_H
|
||||
#define COMMANDS_H
|
||||
|
||||
// TODO: resolve assert
|
||||
#define assert(expr)
|
||||
|
||||
#ifdef __cplusplus
|
||||
|
||||
#include <functional>
|
||||
#include <limits>
|
||||
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
#include <cmsis_os.h>
|
||||
|
||||
void init_communication(void);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
|
||||
#endif /* COMMANDS_H */
|
||||
@@ -0,0 +1,10 @@
|
||||
#ifndef __INTERFACE_CAN_HPP
|
||||
#define __INTERFACE_CAN_HPP
|
||||
|
||||
//#include <cmsis_os.h>
|
||||
//#include "odrive_main.h"
|
||||
//#include "can_helpers.hpp"
|
||||
//#include <communication/can/can_simple.hpp>
|
||||
//// Other protocol implementations here
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,88 @@
|
||||
|
||||
#include "interface_i2c.h"
|
||||
|
||||
#include <i2c.h>
|
||||
|
||||
#define I2C_RX_BUFFER_SIZE 128
|
||||
#define I2C_RX_BUFFER_PREAMBLE_SIZE 4
|
||||
#define I2C_TX_BUFFER_SIZE 128
|
||||
|
||||
I2CStats_t i2c_stats_;
|
||||
/*
|
||||
TODO: add support back
|
||||
|
||||
static uint8_t i2c_rx_buffer[I2C_RX_BUFFER_PREAMBLE_SIZE + I2C_RX_BUFFER_SIZE];
|
||||
static uint8_t i2c_tx_buffer[I2C_TX_BUFFER_SIZE];
|
||||
|
||||
class I2CSender : public PacketSink {
|
||||
public:
|
||||
int process_packet(const uint8_t* buffer, size_t length) {
|
||||
if (length >= 2 && (length - 2) <= sizeof(i2c_tx_buffer))
|
||||
memcpy(i2c_tx_buffer, buffer + 2, length - 2);
|
||||
return 0;
|
||||
}
|
||||
size_t get_free_space() { return SIZE_MAX; }
|
||||
} i2c1_packet_output;
|
||||
BidirectionalPacketBasedChannel i2c1_channel(i2c1_packet_output);
|
||||
*/
|
||||
void start_i2c_server() {
|
||||
// CAN H = SDA
|
||||
// CAN L = SCL
|
||||
//HAL_I2C_EnableListen_IT(&hi2c1);
|
||||
}
|
||||
/*
|
||||
void i2c_handle_packet(I2C_HandleTypeDef *hi2c) {
|
||||
size_t received = sizeof(i2c_rx_buffer) - hi2c->XferCount;
|
||||
if (received > I2C_RX_BUFFER_PREAMBLE_SIZE) {
|
||||
i2c_stats_.rx_cnt++;
|
||||
|
||||
write_le<uint16_t>(0, i2c_rx_buffer); // hallucinate seq-no (not needed for I2C)
|
||||
i2c_rx_buffer[2] = i2c_rx_buffer[4]; // endpoint-id = I2C register address
|
||||
i2c_rx_buffer[3] = i2c_rx_buffer[5] | 0x80; // MSB must be 1
|
||||
size_t expected_bytes = (TX_BUF_SIZE - 2) < I2C_TX_BUFFER_SIZE ? (TX_BUF_SIZE - 2) : I2C_TX_BUFFER_SIZE;
|
||||
write_le<uint16_t>(expected_bytes, i2c_rx_buffer + 4); // hallucinate maximum number of expected response bytes
|
||||
|
||||
i2c1_channel.process_packet(i2c_rx_buffer, received);
|
||||
|
||||
// reset receive buffer
|
||||
hi2c->pBuffPtr = I2C_RX_BUFFER_PREAMBLE_SIZE + i2c_rx_buffer;
|
||||
hi2c->XferCount = sizeof(i2c_rx_buffer) - I2C_RX_BUFFER_PREAMBLE_SIZE;
|
||||
}
|
||||
|
||||
|
||||
if (hi2c->State == HAL_I2C_STATE_BUSY_RX_LISTEN)
|
||||
hi2c->State = HAL_I2C_STATE_LISTEN;
|
||||
}
|
||||
|
||||
|
||||
void HAL_I2C_ListenCpltCallback(I2C_HandleTypeDef *hi2c) {
|
||||
i2c_handle_packet(hi2c);
|
||||
// restart listening for address
|
||||
HAL_I2C_EnableListen_IT(hi2c);
|
||||
}
|
||||
|
||||
void HAL_I2C_AddrCallback(I2C_HandleTypeDef *hi2c, uint8_t TransferDirection, uint16_t AddrMatchCode) {
|
||||
i2c_stats_.addr_match_cnt += 1;
|
||||
|
||||
i2c_handle_packet(hi2c);
|
||||
|
||||
if (TransferDirection == I2C_DIRECTION_TRANSMIT) {
|
||||
HAL_I2C_Slave_Sequential_Receive_IT(hi2c,
|
||||
I2C_RX_BUFFER_PREAMBLE_SIZE + i2c_rx_buffer,
|
||||
sizeof(i2c_rx_buffer) - I2C_RX_BUFFER_PREAMBLE_SIZE, I2C_FIRST_AND_LAST_FRAME);
|
||||
} else {
|
||||
HAL_I2C_Slave_Sequential_Transmit_IT(hi2c, i2c_tx_buffer, sizeof(i2c_tx_buffer), I2C_FIRST_AND_LAST_FRAME);
|
||||
}
|
||||
}
|
||||
|
||||
void HAL_I2C_ErrorCallback(I2C_HandleTypeDef *hi2c) {
|
||||
// ignore NACK errors
|
||||
if (!(hi2c->ErrorCode & (~HAL_I2C_ERROR_AF)))
|
||||
return;
|
||||
|
||||
i2c_stats_.error_cnt += 1;
|
||||
|
||||
// Continue listening
|
||||
HAL_I2C_EnableListen_IT(hi2c);
|
||||
}
|
||||
*/
|
||||
@@ -0,0 +1,25 @@
|
||||
#ifndef __INTERFACE_I2C_HPP
|
||||
#define __INTERFACE_I2C_HPP
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
#include <stdint.h>
|
||||
|
||||
struct I2CStats_t {
|
||||
uint8_t addr;
|
||||
uint32_t addr_match_cnt;
|
||||
uint32_t rx_cnt;
|
||||
uint32_t error_cnt;
|
||||
};
|
||||
|
||||
extern I2CStats_t i2c_stats_;
|
||||
|
||||
void start_i2c_server(void);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
|
||||
#endif // __INTERFACE_I2C_HPP
|
||||
@@ -0,0 +1,206 @@
|
||||
|
||||
#include "interface_uart.h"
|
||||
|
||||
#include "ascii_protocol.hpp"
|
||||
|
||||
#include <MotorControl/utils.hpp>
|
||||
|
||||
#include <fibre/async_stream.hpp>
|
||||
#include <fibre/../../legacy_protocol.hpp>
|
||||
#include <usart.h>
|
||||
#include <cmsis_os.h>
|
||||
#include <freertos_vars.h>
|
||||
#include <odrive_main.h>
|
||||
|
||||
#define UART_TX_BUFFER_SIZE 64
|
||||
#define UART_RX_BUFFER_SIZE 64
|
||||
|
||||
// DMA open loop continous circular buffer
|
||||
// 1ms delay periodic, chase DMA ptr around
|
||||
static uint8_t dma_rx_buffer[UART_RX_BUFFER_SIZE];
|
||||
static uint32_t dma_last_rcv_idx;
|
||||
|
||||
osThreadId uart_thread = 0;
|
||||
static UART_HandleTypeDef* huart_ = nullptr;
|
||||
const uint32_t stack_size_uart_thread = 4096; // Bytes
|
||||
|
||||
namespace fibre {
|
||||
|
||||
class Stm32UartTxStream : public AsyncStreamSink {
|
||||
public:
|
||||
Stm32UartTxStream(UART_HandleTypeDef* huart) : huart_(huart) {}
|
||||
|
||||
void start_write(cbufptr_t buffer, TransferHandle* handle, Callback<void, WriteResult> completer) final;
|
||||
void cancel_write(TransferHandle transfer_handle) final;
|
||||
void did_finish();
|
||||
|
||||
UART_HandleTypeDef *huart_;
|
||||
Callback<void, WriteResult> completer_;
|
||||
const uint8_t* tx_end_ = nullptr;
|
||||
};
|
||||
|
||||
class Stm32UartRxStream : public AsyncStreamSource {
|
||||
public:
|
||||
void start_read(bufptr_t buffer, TransferHandle* handle, Callback<void, ReadResult> completer) final;
|
||||
void cancel_read(TransferHandle transfer_handle) final;
|
||||
void did_receive(uint8_t* buffer, size_t length);
|
||||
|
||||
Callback<void, ReadResult> completer_;
|
||||
bufptr_t rx_buf_ = {nullptr, nullptr};
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
using namespace fibre;
|
||||
|
||||
void Stm32UartTxStream::start_write(cbufptr_t buffer, TransferHandle* handle, Callback<void, WriteResult> completer) {
|
||||
size_t chunk = std::min(buffer.size(), (size_t)UART_TX_BUFFER_SIZE);
|
||||
|
||||
completer_ = completer;
|
||||
tx_end_ = buffer.begin() + chunk;
|
||||
|
||||
if (handle) {
|
||||
*handle = reinterpret_cast<TransferHandle>(this);
|
||||
}
|
||||
|
||||
if (HAL_UART_Transmit_DMA(huart_, const_cast<uint8_t*>(buffer.begin()), chunk) != HAL_OK) {
|
||||
completer_ = nullptr;
|
||||
tx_end_ = nullptr;
|
||||
completer.invoke({kStreamError, buffer.begin()});
|
||||
}
|
||||
}
|
||||
|
||||
void Stm32UartTxStream::cancel_write(TransferHandle transfer_handle) {
|
||||
// not implemented
|
||||
}
|
||||
|
||||
void Stm32UartTxStream::did_finish() {
|
||||
const uint8_t* tx_end = tx_end_;
|
||||
tx_end_ = nullptr;
|
||||
completer_.invoke_and_clear({kStreamOk, tx_end});
|
||||
}
|
||||
|
||||
void Stm32UartRxStream::start_read(bufptr_t buffer, TransferHandle* handle, Callback<void, ReadResult> completer) {
|
||||
completer_ = completer;
|
||||
rx_buf_ = buffer;
|
||||
if (handle) {
|
||||
*handle = reinterpret_cast<TransferHandle>(this);
|
||||
}
|
||||
}
|
||||
|
||||
void Stm32UartRxStream::cancel_read(TransferHandle transfer_handle) {
|
||||
// not implemented
|
||||
}
|
||||
|
||||
void Stm32UartRxStream::did_receive(uint8_t* buffer, size_t length) {
|
||||
// This can be called even if there was no RX operation in progress
|
||||
|
||||
bufptr_t rx_buf = rx_buf_;
|
||||
|
||||
if (completer_ && rx_buf.begin()) {
|
||||
rx_buf_ = {nullptr, nullptr};
|
||||
size_t chunk = std::min(length, rx_buf.size());
|
||||
memcpy(rx_buf.begin(), buffer, chunk);
|
||||
completer_.invoke_and_clear({kStreamOk, rx_buf.begin() + chunk});
|
||||
}
|
||||
}
|
||||
|
||||
Stm32UartTxStream uart_tx_stream(huart_);
|
||||
Stm32UartRxStream uart_rx_stream;
|
||||
|
||||
LegacyProtocolStreamBased fibre_over_uart(&uart_rx_stream, &uart_tx_stream);
|
||||
|
||||
fibre::AsyncStreamSinkMultiplexer<2> uart_tx_multiplexer(uart_tx_stream);
|
||||
fibre::BufferedStreamSink<64> uart0_stdout_sink(uart_tx_multiplexer); // Used in communication.cpp
|
||||
AsciiProtocol ascii_over_uart(&uart_rx_stream, &uart_tx_multiplexer);
|
||||
|
||||
bool uart0_stdout_pending = false;
|
||||
|
||||
static void uart_server_thread(void * ctx) {
|
||||
(void) ctx;
|
||||
|
||||
if (odrv.config_.uart0_protocol == ODrive::STREAM_PROTOCOL_TYPE_FIBRE) {
|
||||
fibre_over_uart.start({});
|
||||
} else if (odrv.config_.uart0_protocol == ODrive::STREAM_PROTOCOL_TYPE_ASCII
|
||||
|| odrv.config_.uart0_protocol == ODrive::STREAM_PROTOCOL_TYPE_ASCII_AND_STDOUT) {
|
||||
ascii_over_uart.start();
|
||||
}
|
||||
|
||||
for (;;) {
|
||||
osEvent event = osMessageGet(uart_event_queue, osWaitForever);
|
||||
|
||||
if (event.status != osEventMessage) {
|
||||
continue;
|
||||
}
|
||||
|
||||
switch (event.value.v) {
|
||||
case 1: {
|
||||
// This event is triggered by the control loop at 8kHz. This should be
|
||||
// enough for most applications.
|
||||
// At 1Mbaud/s that corresponds to at most 12.5 bytes which can arrive
|
||||
// during the sleep period.
|
||||
|
||||
// Check for UART errors and restart receive DMA transfer if required
|
||||
if (huart_->RxState != HAL_UART_STATE_BUSY_RX) {
|
||||
HAL_UART_AbortReceive(huart_);
|
||||
HAL_UART_Receive_DMA(huart_, dma_rx_buffer, sizeof(dma_rx_buffer));
|
||||
dma_last_rcv_idx = 0;
|
||||
}
|
||||
// Fetch the circular buffer "write pointer", where it would write next
|
||||
uint32_t new_rcv_idx = UART_RX_BUFFER_SIZE - huart_->hdmarx->Instance->NDTR;
|
||||
if (new_rcv_idx > UART_RX_BUFFER_SIZE) { // defensive programming
|
||||
continue;
|
||||
}
|
||||
|
||||
// Process bytes in one or two chunks (two in case there was a wrap)
|
||||
if (new_rcv_idx < dma_last_rcv_idx) {
|
||||
uart_rx_stream.did_receive(dma_rx_buffer + dma_last_rcv_idx,
|
||||
UART_RX_BUFFER_SIZE - dma_last_rcv_idx);
|
||||
dma_last_rcv_idx = 0;
|
||||
}
|
||||
if (new_rcv_idx > dma_last_rcv_idx) {
|
||||
uart_rx_stream.did_receive(dma_rx_buffer + dma_last_rcv_idx,
|
||||
new_rcv_idx - dma_last_rcv_idx);
|
||||
dma_last_rcv_idx = new_rcv_idx;
|
||||
}
|
||||
} break;
|
||||
|
||||
case 2: {
|
||||
uart_tx_stream.did_finish();
|
||||
} break;
|
||||
|
||||
case 3: { // stdout has data
|
||||
uart0_stdout_pending = false;
|
||||
uart0_stdout_sink.maybe_start_async_write();
|
||||
} break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: allow multiple UART server instances
|
||||
void start_uart_server(UART_HandleTypeDef* huart) {
|
||||
huart_ = huart;
|
||||
uart_tx_stream.huart_ = huart;
|
||||
|
||||
// DMA is set up to receive in a circular buffer forever.
|
||||
// We dont use interrupts to fetch the data, instead we periodically read
|
||||
// data out of the circular buffer into a parse buffer, controlled by a state machine
|
||||
HAL_UART_Receive_DMA(huart_, dma_rx_buffer, sizeof(dma_rx_buffer));
|
||||
dma_last_rcv_idx = 0;
|
||||
|
||||
// Start UART communication thread
|
||||
osThreadDef(uart_server_thread_def, uart_server_thread, osPriorityNormal, 0, stack_size_uart_thread / sizeof(StackType_t) /* the ascii protocol needs considerable stack space */);
|
||||
uart_thread = osThreadCreate(osThread(uart_server_thread_def), NULL);
|
||||
}
|
||||
|
||||
void uart_poll() {
|
||||
if (uart_thread) { // the thread is only started if UART is enabled
|
||||
osMessagePut(uart_event_queue, 1, 0);
|
||||
}
|
||||
}
|
||||
|
||||
void HAL_UART_TxCpltCallback(UART_HandleTypeDef* huart) {
|
||||
if (huart == huart_) {
|
||||
osMessagePut(uart_event_queue, 2, 0);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
#ifndef __INTERFACE_UART_HPP
|
||||
#define __INTERFACE_UART_HPP
|
||||
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
#include <cmsis_os.h>
|
||||
#include "usart.h"
|
||||
|
||||
extern osThreadId uart_thread;
|
||||
extern const uint32_t stack_size_uart_thread;
|
||||
|
||||
void start_uart_server(UART_HandleTypeDef* huart);
|
||||
void uart_poll(void);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
|
||||
|
||||
#ifdef __cplusplus
|
||||
#include <fibre/../../stream_utils.hpp>
|
||||
extern fibre::BufferedStreamSink<64> uart0_stdout_sink;
|
||||
extern bool uart0_stdout_pending;
|
||||
#endif
|
||||
|
||||
#endif // __INTERFACE_UART_HPP
|
||||
@@ -0,0 +1,237 @@
|
||||
|
||||
#include "interface_usb.h"
|
||||
#include "ascii_protocol.hpp"
|
||||
|
||||
#include <MotorControl/utils.hpp>
|
||||
|
||||
#include <fibre/async_stream.hpp>
|
||||
#include <fibre/../../legacy_protocol.hpp>
|
||||
#include <usbd_cdc.h>
|
||||
#include <usbd_cdc_if.h>
|
||||
#include <usb_device.h>
|
||||
#include <cmsis_os.h>
|
||||
#include <freertos_vars.h>
|
||||
|
||||
#include <odrive_main.h>
|
||||
|
||||
osThreadId usb_thread;
|
||||
const uint32_t stack_size_usb_thread = 4096; // Bytes
|
||||
USBStats_t usb_stats_;
|
||||
|
||||
namespace fibre {
|
||||
|
||||
class Stm32UsbTxStream : public AsyncStreamSink {
|
||||
public:
|
||||
Stm32UsbTxStream(uint8_t endpoint_num) : endpoint_num_(endpoint_num) {}
|
||||
|
||||
void start_write(cbufptr_t buffer, TransferHandle* handle, Callback<void, WriteResult> completer) final;
|
||||
void cancel_write(TransferHandle transfer_handle) final;
|
||||
void did_finish();
|
||||
|
||||
const uint8_t endpoint_num_;
|
||||
bool connected_ = false;
|
||||
Callback<void, WriteResult> completer_;
|
||||
const uint8_t* tx_end_ = nullptr;
|
||||
};
|
||||
|
||||
class Stm32UsbRxStream : public AsyncStreamSource {
|
||||
public:
|
||||
Stm32UsbRxStream(uint8_t endpoint_num) : endpoint_num_(endpoint_num) {}
|
||||
|
||||
void start_read(bufptr_t buffer, TransferHandle* handle, Callback<void, ReadResult> completer) final;
|
||||
void cancel_read(TransferHandle transfer_handle) final;
|
||||
void did_finish();
|
||||
|
||||
const uint8_t endpoint_num_;
|
||||
bool connected_ = false;
|
||||
Callback<void, ReadResult> completer_;
|
||||
uint8_t* rx_end_ = nullptr;
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
using namespace fibre;
|
||||
|
||||
void Stm32UsbTxStream::start_write(cbufptr_t buffer, TransferHandle* handle, Callback<void, WriteResult> completer) {
|
||||
if (handle) {
|
||||
*handle = reinterpret_cast<TransferHandle>(this);
|
||||
}
|
||||
|
||||
if (!connected_) {
|
||||
completer.invoke({kStreamClosed, buffer.begin()});
|
||||
return;
|
||||
}
|
||||
|
||||
// Note on MTU: on the physical layer, a full speed device can transmit up
|
||||
// to 64 bytes of payload per bulk package. However a single logical
|
||||
// transfer can consist of multiple 64 byte packets terminated by a 0 byte
|
||||
// packet. Currently we don't implement this segmentation. Therefore we
|
||||
// must ensure that all packets are < 64 bytes, otherwise the host will wait
|
||||
// for more.
|
||||
if (buffer.size() >= USB_TX_DATA_SIZE) {
|
||||
completer.invoke({kStreamError, buffer.begin()});
|
||||
return;
|
||||
}
|
||||
|
||||
if (completer_ || tx_end_) {
|
||||
completer.invoke({kStreamError, buffer.begin()});
|
||||
return;
|
||||
}
|
||||
|
||||
completer_ = completer;
|
||||
tx_end_ = buffer.end();
|
||||
|
||||
if (
|
||||
#if HW_VERSION_MAJOR == 3 // TODO: remove preprocessor switch
|
||||
CDC_Transmit_FS
|
||||
#elif HW_VERSION_MAJOR == 4
|
||||
CDC_Transmit_HS
|
||||
#else
|
||||
#error "not supported"
|
||||
#endif
|
||||
(const_cast<uint8_t*>(buffer.begin()), buffer.size(), endpoint_num_) != USBD_OK) {
|
||||
tx_end_ = nullptr;
|
||||
completer_.invoke_and_clear({kStreamError, buffer.begin()});
|
||||
}
|
||||
}
|
||||
|
||||
void Stm32UsbTxStream::cancel_write(TransferHandle transfer_handle) {
|
||||
// not implemented
|
||||
}
|
||||
|
||||
void Stm32UsbTxStream::did_finish() {
|
||||
const uint8_t* tx_end = tx_end_;
|
||||
tx_end_ = nullptr;
|
||||
completer_.invoke_and_clear({connected_ ? kStreamOk : kStreamClosed, tx_end});
|
||||
}
|
||||
|
||||
void Stm32UsbRxStream::start_read(bufptr_t buffer, TransferHandle* handle, Callback<void, ReadResult> completer) {
|
||||
if (handle) {
|
||||
*handle = reinterpret_cast<TransferHandle>(this);
|
||||
}
|
||||
|
||||
if (!connected_) {
|
||||
completer.invoke({kStreamClosed, buffer.begin()});
|
||||
return;
|
||||
}
|
||||
|
||||
if (completer_ || rx_end_) {
|
||||
completer.invoke({kStreamError, buffer.begin()});
|
||||
return;
|
||||
}
|
||||
|
||||
completer_ = completer;
|
||||
rx_end_ = buffer.begin(); // the pointer is updated at the end of the transfer
|
||||
|
||||
if (USBD_CDC_ReceivePacket(&usb_dev_handle, buffer.begin(), buffer.size(), endpoint_num_) != USBD_OK) {
|
||||
rx_end_ = nullptr;
|
||||
completer_.invoke_and_clear({kStreamError, buffer.begin()});
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
void Stm32UsbRxStream::cancel_read(TransferHandle transfer_handle) {
|
||||
// not implemented
|
||||
}
|
||||
|
||||
void Stm32UsbRxStream::did_finish() {
|
||||
uint8_t* rx_end = rx_end_;
|
||||
rx_end_ = nullptr;
|
||||
completer_.invoke_and_clear({connected_ ? kStreamOk : kStreamClosed, rx_end});
|
||||
}
|
||||
|
||||
Stm32UsbTxStream usb_cdc_tx_stream(CDC_IN_EP);
|
||||
Stm32UsbTxStream usb_native_tx_stream(ODRIVE_IN_EP);
|
||||
Stm32UsbRxStream usb_cdc_rx_stream(CDC_OUT_EP);
|
||||
Stm32UsbRxStream usb_native_rx_stream(ODRIVE_OUT_EP);
|
||||
|
||||
LegacyProtocolStreamBased fibre_over_cdc(&usb_cdc_rx_stream, &usb_cdc_tx_stream);
|
||||
LegacyProtocolPacketBased fibre_over_usb(&usb_native_rx_stream, &usb_native_tx_stream, USB_TX_DATA_SIZE - 1); // See note on MTU above
|
||||
|
||||
fibre::AsyncStreamSinkMultiplexer<2> usb_cdc_tx_multiplexer(usb_cdc_tx_stream);
|
||||
fibre::BufferedStreamSink<64> usb_cdc_stdout_sink(usb_cdc_tx_multiplexer); // Used in communication.cpp
|
||||
AsciiProtocol ascii_over_cdc(&usb_cdc_rx_stream, &usb_cdc_tx_multiplexer);
|
||||
|
||||
bool usb_cdc_stdout_pending = false;
|
||||
|
||||
static void usb_server_thread(void * ctx) {
|
||||
(void) ctx;
|
||||
|
||||
for (;;) {
|
||||
osEvent event = osMessageGet(usb_event_queue, osWaitForever);
|
||||
|
||||
if (event.status != osEventMessage) {
|
||||
continue;
|
||||
}
|
||||
|
||||
usb_stats_.rx_cnt++;
|
||||
|
||||
switch (event.value.v) {
|
||||
case 1: { // USB connected event
|
||||
usb_cdc_tx_stream.connected_ = true;
|
||||
usb_native_tx_stream.connected_ = true;
|
||||
usb_cdc_rx_stream.connected_ = true;
|
||||
usb_native_rx_stream.connected_ = true;
|
||||
|
||||
fibre_over_usb.start({});
|
||||
|
||||
if (odrv.config_.usb_cdc_protocol == ODrive::STREAM_PROTOCOL_TYPE_FIBRE) {
|
||||
fibre_over_cdc.start({});
|
||||
} else if (odrv.config_.usb_cdc_protocol == ODrive::STREAM_PROTOCOL_TYPE_ASCII
|
||||
|| odrv.config_.usb_cdc_protocol == ODrive::STREAM_PROTOCOL_TYPE_ASCII_AND_STDOUT) {
|
||||
ascii_over_cdc.start();
|
||||
}
|
||||
} break;
|
||||
|
||||
case 2: { // USB disconnected event
|
||||
usb_cdc_tx_stream.connected_ = false;
|
||||
usb_native_tx_stream.connected_ = false;
|
||||
usb_cdc_rx_stream.connected_ = false;
|
||||
usb_native_rx_stream.connected_ = false;
|
||||
usb_cdc_tx_stream.did_finish();
|
||||
usb_native_tx_stream.did_finish();
|
||||
usb_cdc_rx_stream.did_finish();
|
||||
usb_native_rx_stream.did_finish();
|
||||
} break;
|
||||
|
||||
case 3: { // TX on CDC interface done
|
||||
usb_cdc_tx_stream.did_finish();
|
||||
} break;
|
||||
|
||||
case 4: { // TX on custom interface done
|
||||
usb_native_tx_stream.did_finish();
|
||||
} break;
|
||||
|
||||
case 5: { // RX on CDC interface done
|
||||
usb_cdc_rx_stream.did_finish();
|
||||
} break;
|
||||
|
||||
case 6: { // RX on custom interface done
|
||||
usb_native_rx_stream.did_finish();
|
||||
} break;
|
||||
|
||||
case 7: { // stdout has data
|
||||
usb_cdc_stdout_pending = false;
|
||||
usb_cdc_stdout_sink.maybe_start_async_write();
|
||||
} break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Called from CDC_Receive_FS callback function, this allows the communication
|
||||
// thread to handle the incoming data
|
||||
void usb_rx_process_packet(uint8_t *buf, uint32_t len, uint8_t endpoint_pair) {
|
||||
if (endpoint_pair == CDC_OUT_EP && usb_cdc_rx_stream.rx_end_) {
|
||||
usb_cdc_rx_stream.rx_end_ += len;
|
||||
osMessagePut(usb_event_queue, 5, 0);
|
||||
} else if (endpoint_pair == ODRIVE_OUT_EP && usb_native_rx_stream.rx_end_) {
|
||||
usb_native_rx_stream.rx_end_ += len;
|
||||
osMessagePut(usb_event_queue, 6, 0);
|
||||
}
|
||||
}
|
||||
|
||||
void start_usb_server() {
|
||||
// Start USB communication thread
|
||||
osThreadDef(usb_server_thread_def, usb_server_thread, osPriorityNormal, 0, stack_size_usb_thread / sizeof(StackType_t));
|
||||
usb_thread = osThreadCreate(osThread(usb_server_thread_def), NULL);
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
#ifndef __INTERFACE_USB_HPP
|
||||
#define __INTERFACE_USB_HPP
|
||||
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
#include <cmsis_os.h>
|
||||
#include <stdint.h>
|
||||
|
||||
extern osThreadId usb_thread;
|
||||
extern const uint32_t stack_size_usb_thread;
|
||||
|
||||
typedef struct {
|
||||
uint32_t rx_cnt;
|
||||
uint32_t tx_cnt;
|
||||
uint32_t tx_overrun_cnt;
|
||||
} USBStats_t;
|
||||
|
||||
extern USBStats_t usb_stats_;
|
||||
|
||||
void usb_rx_process_packet(uint8_t *buf, uint32_t len, uint8_t endpoint_pair);
|
||||
void start_usb_server(void);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
|
||||
|
||||
#ifdef __cplusplus
|
||||
#include <fibre/../../stream_utils.hpp>
|
||||
extern fibre::BufferedStreamSink<64> usb_cdc_stdout_sink;
|
||||
extern bool usb_cdc_stdout_pending;
|
||||
#endif
|
||||
|
||||
#endif // __INTERFACE_USB_HPP
|
||||
Reference in New Issue
Block a user