Close

Moving the Thor robot: Stepper driving concept

A project log for Building the Thor robot

Building a 6-axis robot based on the Thor robot. When size DOES matter!

olaf-baeyensOlaf Baeyens 01/31/2017 at 19:261 Comment

Disclaimer: The code below is just a concept. Helpers to build out the frame structure. Namings will change

I am still working out the code to make the Thor robot move in very fluent moves. The challenge is that we have 7+ stepper motors!


The stepper motor controller has 3 bits to manipulate: Enable, direction and step.

Every time you make a Step pulse high, wait 1.9 µS then make it 0 again and wait 1.9 µS., one step has been executed.

The key here is to have one execution block (FIFO) that contains Execution information like this: MotorExecutionBlock.

Yes this is C++ and I intentionally have chosen to not use typical C+ notations in my namings. (e.g. bptrStartPos or iCurrentOffset)

//----------------------------------------------------------
// Motor execution Pointer
//----------------------------------------------------------
struct MotorExecutionPointer {
	// Stepper motor enable mask
	byte EnableMask = 0x00;

	// Stepper motor direction mask
	byte DirectionMask = 0x00;

	// FIFO start position pointer
	byte* StartPosPtr = nullptr;

	// FIFO end position pointer
	byte* EndPosPtr = nullptr;

	// FIFO current position
	int CurentOffset = 0;


	int BufferSize() const {
		return (EndPosPtr - StartPosPtr);
	}

	bool IsValidOffset(int offsetToTest) const {
		auto bufferSize = (EndPosPtr - StartPosPtr);
		if (offsetToTest < bufferSize) return true;
		if (offsetToTest >= 0) return true;
		return false;
	}
};
Important to realize that we only use pointers!

To reduce memory fragmentation and guarantee that we do have memory, we will have a global allocated FIFO buffer that will exist for the duration of the robot

If you think of it the only moment you need to change the direction and enable bit is when you start to execute the stepper playbook. Any change in direction means that it have a different MotorExecutionPointer.


The code to actually execute is currently MotorExecutionBlock..

struct MotorExecutionBlock {
	// Just for testing 
	LEDBlink* debugLED = nullptr;

	MotorExecutionPointer ExecutionInfo;

	GlobalBuffers* mainBuffer;

	explicit MotorExecutionBlock(GlobalBuffers* mainBuffer) : ExecutionInfo() {
		debugLED = new LEDBlink(200);
		this->mainBuffer = mainBuffer;
		if (mainBuffer) {
			if (mainBuffer->stepperCommandsPtr) {
				ExecutionInfo.StartPosPtr = mainBuffer->stepperCommandsPtr->buffer;
			}
		}
	}

	~MotorExecutionBlock() {
		mainBuffer = nullptr;
		if (debugLED) delete debugLED;
	}


	static void SendMotorEnableCommand(byte enable_command);
	static void SendMotorDirectionCommand(byte direction_command);
	void SendMotorStepCommand(byte step_command);
	void SetMotorStep(byte step_command);
	void ExecuteStep(unsigned long current_millis);


};
Ignore the debug LED, it helps me to debug my code using the Arduino nano LED. No need to have a massive killing robot that tries to kill you when you made a typo :-)

Also the Arduino nano forces me to create such a design that it fits in a very small memory footprint.


It is unclear at this point how I will divide my global memory buffer. This code above may be extended to also contain a maximum counter, so the execution blocks could be fixed in size. This code does not have this taken into account yet.


The execution code would be like this:

It is still a proof of concept but the idea is that every bit in a byte represents one motor step pulse. I only write the step pulse of it is a "1".

void MotorExecutionBlock::SendMotorEnableCommand(byte enable_command) {
	byte level = (enable_command >> 6) & 0x01;
	digitalWrite(X_AXIS_ENABLE, level);

	level = (enable_command >> 5) & 0x01;
	digitalWrite(Y_AXIS_ENABLE, level);

	level = (enable_command >> 4) & 0x01;
	digitalWrite(Z_AXIS_ENABLE, level);

	level = (enable_command >> 3) & 0x01;
	digitalWrite(E0_AXIS_ENABLE, level);

	level = (enable_command >> 2) & 0x01;
	digitalWrite(E1_AXIS_ENABLE, level);

	level = (enable_command >> 1) & 0x01;
	digitalWrite(E2_AXIS_ENABLE, level);

	level = (enable_command >> 0) & 0x01;
	digitalWrite(E3_AXIS_ENABLE, level);
}

void MotorExecutionBlock::SendMotorDirectionCommand(byte direction_command) {
	byte level = (direction_command >> 6) & 0x01;
	digitalWrite(X_AXIS_DIRECTION, level);

	level = (direction_command >> 5) & 0x01;
	digitalWrite(Y_AXIS_DIRECTION, level);

	level = (direction_command >> 4) & 0x01;
	digitalWrite(Z_AXIS_DIRECTION, level);

	level = (direction_command >> 3) & 0x01;
	digitalWrite(E0_AXIS_DIRECTION, level);

	level = (direction_command >> 2) & 0x01;
	digitalWrite(E1_AXIS_DIRECTION, level);

	level = (direction_command >> 1) & 0x01;
	digitalWrite(E2_AXIS_DIRECTION, level);

	level = (direction_command >> 0) & 0x01;
	digitalWrite(E3_AXIS_DIRECTION, level);
}


void MotorExecutionBlock::SendMotorStepCommand(byte step_command) {
	byte level = (step_command >> 6) & 0x01;
	digitalWrite(X_AXIS_STEP, level);

	level = (step_command >> 5) & 0x01;
	digitalWrite(Y_AXIS_STEP, level);

	level = (step_command >> 4) & 0x01;
	digitalWrite(Z_AXIS_STEP, level);

	level = (step_command >> 3) & 0x01;
	digitalWrite(E0_AXIS_STEP, level);

	level = (step_command >> 2) & 0x01;
	digitalWrite(E1_AXIS_STEP, level);

	level = (step_command >> 1) & 0x01;
	digitalWrite(E2_AXIS_STEP, level);

	level = (step_command >> 0) & 0x01;
	digitalWrite(E3_AXIS_STEP, level);

	Serial.println("SendMotorStepCommand");
	if (level) debugLED->UpdateLedState();

}

Code above is split up into 3 methods. One for the enable flag, another one for the direction flag and finally the real step.


void MotorExecutionBlock::SetMotorStep(byte step_command) {
    // Bit 1 means set the step
    SendMotorStepCommand(step_command);

    // wait 2 micro seconds (example below is 1000 micros seconds but for now...
    delayMicrosecond(2);

    // Clear the stepper bits preparing for the next sequence
    SendMotorStepCommand(0);

    // wait 2 micro seconds (example below is 1000 micros seconds but for now...
    delayMicrosecond(2);
}

For enable and direction commands we do not clear the flag. But for every motor stepper command we need to set the bit to zero again.


The final part in executing this FIFO buffer is this:

void MotorExecutionBlock::ExecuteStep(unsigned long current_millis) {
	if (!mainBuffer) return;

	auto startPos = ExecutionInfo.StartPosPtr;
	if (!startPos) return;

	auto currentOffset = ExecutionInfo.CurentOffset;

	if (!ExecutionInfo.IsValidOffset(currentOffset)) return;

	auto direction = ExecutionInfo.DirectionMask;
	auto enabledMask = ExecutionInfo.EnableMask;
	if (currentOffset == 0) {
		// First thing to do is to set the enabled bit and direction bits
		SendMotorEnableCommand(enabledMask);
		SendMotorDirectionCommand(direction);
	}

	// Now we execute the stepper bits
	if (mainBuffer->stepperCommandsPtr) {
		auto StepCommand = ExecutionInfo.StartPosPtr[currentOffset];
		SetMotorStep(StepCommand);
	}

	ExecutionInfo.CurentOffset++;
}
We basically have a Start Pointer and an offset (currentOffset) from that start pointer. When the currentOffset = 0 then it means that we did not set the enable and direction flags yet so do that now. (It does not matter of we do that all the time, but why waste CPU cycles?)

One thing that I forgot to tell was that this is Timer 1 interrupt driven. This means that ExecuteStep() is called every time Timer1 calls for an interrupt.

The cool thing about this mechanism is that I can also speed up and slow down without influencing the timings for debugging purposes.


Below is the rough idea how the Timer1 will be used as a heart beat to drive the stepper motors.

ISR(TIMER1_COMPA_vect) {
	arduinoControl->Enter();

	auto currentMillis = millis();
	motorProcessor->ExecuteStep();

	arduinoControl->Leave();
}

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

	auto a = getFreeSram();

	globalBuffers= new GlobalBuffers(1024);
	motorProcessor = new MotorProcessor(globalBuffers, 1000);

	arduinoControl->SetPinModes();
	arduinoControl->SetTimer1Frequency(timerFrequency);
}

void loop() {
}

What is mussing is UI controls, actually the calculation part that fills in the motor FIFO playbook, safety controls....

Safety could be handled by idling the Timer1 interrupt handler.

Changing the Timer 1 frequency can make the robot move slower.


I do not have code yet to be released.

Discussions

Olaf Baeyens wrote 01/31/2017 at 19:53 point

Just for the record: If I did not have a partner, personal life and a full professional job, this code would have been up and running in 2 weeks. :-) 

  Are you sure? yes | no