The firmware has drastically changed between this post and the next. I'm not updating what I've changed here, and left the firmware attached. For those interested, it can show the evolution of a design in progress. The biggest change is that when I was reading the examples on the website and the logs of the IDE, I thought I could send TMCL commands to the motor. But that's not the case. You have to implement the TMLC datagram to TMC2300 datagram translation in your firmware. It's not difficult - Trinamic has a full-fledged example on GitHub.
I'm road testing Trinamic TMC2300 EVAL KIT (2-PH Stepper Motor). It's their latest design that targets battery powered devices. In this post: a custom program to control the driver over UART. This series of posts is an attempt to use the TMC2300 in a real design. I'm using a Hercules microcontroller in stead of the landungsbrücke to control a motor. Firmware is in C++ |
Several things are happening at the same time here. I'm splitting it up in chunks to keep it concise.
- I'm using a Texas Instruments Hercules microcontroller as host.
- Communication to the TMC3200 is using UART, and
- You send and receive 9 byte datagrams - with CRC (see Trinamic Stepper Motor Controller TMC2300 - UART Interface).
- I'm using objects to talk to the motor (see Hercules Safety Microcontroller: Use C++ with HALCoGen C Projects)
- For the raw UART driver code, I use the basic Hercules HALCoGen api, sending byte per byte
The Objects:
- a main class called CPPMain. It's role is to bridge from the functional style HALCoGen framework to C++, and to run the program.
- an abstract TMCLDatagram base class, that knows Trinamic's TMC Language definitions, the datagram structure and CRC logic. it does not know anything Hercules (or even the physical layer used).
- the HerculesTMCLDatagram class, child of the one above. This one knows were using UART, and knows the HALCoGen API.
- an abstract CommInit class. You pass a child of that class to the init() function.
- The HerculesSciInit class, based on the one above, a structure to hold UART config parameters.
At the begin of the program, a HerculesTMCLDatagram class is instantiated, then initialised and (in a later post) used.
The CPPMain class
Header:
class CPPMain { public: CPPMain(); virtual ~CPPMain(); static void main(); };
Implementation:
#include <cpp/CPPMain.h> #include "gio.h" #include "sci.h" #include "trinamic/HerculesTMCLDatagram.h" extern "C" void CPPMain() { CPPMain::main(); } HerculesTMCLDatagram datagram(1U); CPPMain::CPPMain() { } CPPMain::~CPPMain() { } void CPPMain::main() { gioInit(); sciInit(); HerculesSciInit initStruct; initStruct._sci = scilinREG; datagram.init(&initStruct); // put test code here while(1) { } }
The TMCL Base Classes
Header:
struct CommInit { }; class TMCLDatagram { public: //Opcodes of all TMCL commands that can be used in direct mode const uint32_t TMCL_ROR = 1; const uint32_t TMCL_ROL = 2; const uint32_t TMCL_MST = 3; const uint32_t TMCL_MVP = 4; const uint32_t TMCL_SAP = 5; const uint32_t TMCL_GAP = 6; const uint32_t TMCL_STAP = 7; const uint32_t TMCL_RSAP = 8; const uint32_t TMCL_SGP = 9; const uint32_t TMCL_GGP = 10; const uint32_t TMCL_STGP = 11; const uint32_t TMCL_RSGP = 12; const uint32_t TMCL_RFS = 13; const uint32_t TMCL_SIO = 14; const uint32_t TMCL_GIO = 15; const uint32_t TMCL_SCO = 30; const uint32_t TMCL_GCO = 31; const uint32_t TMCL_CCO = 32; //Options for MVP commandds const uint32_t MVP_ABS = 0; const uint32_t MVP_REL = 1; const uint32_t MVP_COORD = 2; //Options for RFS command const uint32_t RFS_START = 0; const uint32_t RFS_STOP = 1; const uint32_t RFS_STATUS = 2; //Result codes for GetResult const uint32_t TMCL_RESULT_OK = 0; const uint32_t TMCL_RESULT_NOT_READY = 1; const uint32_t TMCL_RESULT_CHECKSUM_ERROR = 2; TMCLDatagram(uint8_t address); virtual ~TMCLDatagram(); virtual void init(CommInit *commInit) = 0; virtual void open() = 0; virtual void close() = 0; //Send a binary TMCL command //e.g. SendCmd( TMCLDatagram::TMCL_MVP, TMCLDatagram::MVP_ABS, 1, 50000); will be MVP ABS, 1, 50000 //Parameters: Command: the TMCL command (see the constants at the begiining of this file) // Type: the "Type" parameter of the TMCL command (set to 0 if unused) // Motor: the motor number (set to 0 if unused) // Value: the "Value" parameter (depending on the command, set to 0 if unused) virtual void sendCmd(uint32_t command, uint32_t Type, uint32_t motor, int32_t value); //Read the result that is returned by the module //Parameters: Status: variable to hold the status returned by the module (100 means okay) // Value: variable to hold the value returned by the module //Return value: TMCLDatagram::TMCL_RESULT_OK: result has been read without errors // TMCLDatagram::TMCL_RESULT_NOT_READY: not enough bytes read so far (try again) // TMCLDatagram::TMCL_RESULT_CHECKSUM_ERROR: checksum of reply packet wrong virtual uint32_t getResult(uint8_t &status, int32_t &value); private: uint8_t _address; protected: virtual void physicalSendCmd(uint8_t *txBuffer) = 0; virtual uint32_t physicalGetResult(uint8_t *txBuffer) = 0; };
Implementation:
This code has drastically changed after I published this. It's now using the CRC and raw datagram generation/interpretation for the TMC2300. The revised code is attached to the next blog post.
TMCLDatagram::TMCLDatagram(uint8_t address) : _address(address) { } TMCLDatagram::~TMCLDatagram() { } void TMCLDatagram::sendCmd(uint32_t command, uint32_t type, uint32_t motor, int32_t value) { uint8_t txBuffer[9]; uint32_t i; txBuffer[0] =_address; txBuffer[1] = command; txBuffer[2] = type; txBuffer[3] = motor; txBuffer[4] = value >> 24; txBuffer[5] = value >> 16; txBuffer[6] = value >> 8; txBuffer[7] = value & 0xff; txBuffer[8] =0; for(i = 0; i < 8; i++) { txBuffer[8]+=txBuffer[i]; } physicalSendCmd(txBuffer); } uint32_t TMCLDatagram::getResult( uint8_t &status, int32_t &value) { uint8_t rxBuffer[9]; uint8_t checksum; uint32_t i; if (physicalGetResult(rxBuffer)) { checksum = 0; for(i = 0; i < 8; i++) { checksum += rxBuffer[i]; } if (checksum != rxBuffer[8]) { return TMCL_RESULT_CHECKSUM_ERROR; } // todo: what if this is not for us? // *Address=RxBuffer[0]; status = rxBuffer[2]; value = (rxBuffer[4] << 24) | (rxBuffer[5] << 16) | (rxBuffer[6] << 8) | rxBuffer[7]; } else { return TMCL_RESULT_NOT_READY; } return TMCL_RESULT_OK; }
Hercules UART Specific Classes
Header:
struct HerculesSciInit: public CommInit { sciBASE_t * _sci; }; class HerculesTMCLDatagram: public TMCLDatagram { public: HerculesTMCLDatagram(uint8_t address); virtual ~HerculesTMCLDatagram(); virtual void init(CommInit *commInit); virtual void open(); virtual void close(); private: sciBASE_t * _sci; protected: virtual void physicalSendCmd(uint8_t *txBuffer); virtual uint32_t physicalGetResult(uint8_t *txBuffer); };
Implementation:
HerculesTMCLDatagram::HerculesTMCLDatagram(uint8_t address) : TMCLDatagram(address), _sci((sciBase *) NULL) { } HerculesTMCLDatagram::~HerculesTMCLDatagram() { } void HerculesTMCLDatagram::init(CommInit *commInit) { // Hercules initialisation is done in HALCoGen _sci = (static_cast<HerculesSciInit *>(commInit))->_sci; } void HerculesTMCLDatagram::open() { // Hercules does not have to open SCI } void HerculesTMCLDatagram::close() { // Hercules does not have to close SCI } void HerculesTMCLDatagram::physicalSendCmd(uint8_t *txBuffer) { uint32_t i; for(i = 0; i < 8; i++) { while (!sciIsTxReady(_sci)) {} sciSendByte(_sci, txBuffer[i]); // send out text } } uint32_t HerculesTMCLDatagram::physicalGetResult(uint8_t *txBuffer) { uint32_t i; // todo: this hangs if no data received for(i = 0; i < 9; i++) { while (!sciIsRxReady(_sci)) {} txBuffer[i] = sciReceiveByte(_sci); // receive text } return i; }
OO Do's and Dont's
Because the HALCoGen framework is intended for C, and C++ static objects are constructed early, there is a watchout.
It's not adviced to dynamically allocate objects on an automotive microcontroller (unless you include an approach in your Misra strategy), so I'm creating objects statically.
The constructor of these objects are called before the main() function and before the HALCoGen init is complete, so hardware isn't initialised yet.
Don't do anything hardware related in the constructors. Use an init() function for late initialisation.
I've made the HerculesTMCLDatagram class a child of the TMCLDatagram class.
The TMCLDatagram's physicalRead() and physicalWrite() functions are pure virtual, implemented by HerculesTMCLDatagram.
There are other options, such as putting reads and writes in a separate opbject that's passed to TMCLDatagram, or (ducking for cover) multiple inheritance.
Not too happy with the initialisation approach where the base class expects a CommInit object and the derived class a HerculesSciInit. There are pros and cons. Discuss.
... and in this post and the attached project I'm mixing 8 and 32 bit unsigned integers. I'm going to use 8 for those that take a single space n the transmit buffer. It will be updated in the next post.
Hercules Peripherals Configuration
Initialisation and configuration of the peripheral drivers is done in HALCoGen. I did not try to abstract that into objects.
The Code Composer Studio and HALCoGen project are attached.
In a next post I have to address the I/O pins (I can reuse the classes I ported from MBED for another blog series). Then I can try talking to the driver...