Arduino Pulser

Overview

PyPi module N/A
git repository https://bitbucket.org/arrizza-public/arduino-pulser
git command git clone git@bitbucket.org:arrizza-public/arduino-pulser.git
Verification Report https://arrizza.com/web-ver/arduino-pulser-report.html
Version Info
  • Ubuntu 20.04 focal, Python 3.10
  • Ubuntu 22.04 jammy, Python 3.10

Summary

Pulse a GPIO accurately from an ISR. Use a ruby script to control the pulse rate.

To control a stepper motor, a series of pulses is sent to it. The goal of this project is to generate a stream of pulses from a GPIO pin at an accurate rate. So when I attach that GPIO pin to a Stepper Motor controller, the series of pulses will turn the stepper at a very accurate rate. For now the rate is controlled by simple commands from putty or from a ruby script.

There is a lower bound to how slowly we can pulse the stepper. See Arduino Accurate Pulser for a much better solution. Eventually, there may be an LCD panel that can control the pulse stream see Arduino LCD Test and Arduino Touch

This project also shows one way to do unit testing on an Arduino. It uses Google Test to run some test cases. See Google Test in Arduino Tools and Other Setup to set it up.

Setup

The setup sets up the Serial Port for communication see Arduino Serial Input and Arduino Serial Output.

It then initializes the Pulser and the Parser.

Pulser

The Pulser is used to perform the pulsing of the GPIO pin at a regular interval.

Pulser Init

The pulser_init function:

  • initializes GPIO pin 10
  • initializes all statistics to 0s
  • sets up Timer2 to run every 2ms, and sets up a separate prescaler to cause the overall timer interval to be 100ms.

Pulser Go!

When the Pulser gets a request to Go! from the Parser:

  • it temporarily disables interrupts
  • makes sure the GPIO pin is off
  • sets up a short array of slots to spread the pulses evenly across multiple ticks.
  • re-enables interrupts

The Pulser uses an array to spread the requested number of pulses across a longer time period. The current code uses an array with only 5 slots in it. Each slot is visited when the ISR interrupt occurs, in this case every 100ms.

A final version of this would use a larger number, say 1000 slots (or longer) in the array and each slot visited every 100ms. This would spread the total number of requested pulses across 100ms * 1000 = 100 seconds.

Right now the per-slot pulse calculation is simple: the total number of pulses divided by the number of slots in the pulse array. This works OK if the total number of pulses is evenly divisible by the number of slots. For example if the number of slots is 5 and the requested number of pulses is 10, then the number of pulses to do per slot is 2.

It doesn't work very well for other values. For example if the requested number of pulses is 9, then 9 / 5 is 1 in integer arithmetic. (I have to use integer arithmetic since I can't do a partial pulse!) So in this case, I end up only pulsing 1 time per slot and with 5 slots, that's only 5 times instead of the 9 that was requested. See Arduino Accurate Pulser and Low Rate Peristaltic Pump#pulse-distribution for a solution.

Pulser Stop!

When the Pulser gets a request to Stop! from the Parser:

  • it temporarily disables interrupts
  • makes sure the GPIO pin is off
  • saves the current number of pulses to last
  • saves the current number of ticks to last
  • clears the pulse array - sets all slots to 0
  • re-enables interrupts

Pulser ISR

The ISR is triggered on every Timer2 interrupt. Currently, that's set to 100ms.

The ISR routine does the following:

  • uses the prescaler to determine if it should pulse or not. If not, it simply returns
  • increments the number of ticks. In this case, each tick count means 100ms has elapsed.
  • it uses the value in the current slot to pulses the GPIO pin that many times. In other words, it is possible to achieve higher motor speeds by pulsing the pin and therefore the stepper, more times every tick.
  • Each pulse is 10uS
  • It increments the slot index to the next slot. If it is past the end of the array, it rolls back to the zeroth slot.

On an oscilloscope, this sequence shows up as a sequence of very fast pulses followed by a period of dead space. For "g100" set:

  • horizontal 5uS/div or 10uS/div
  • vertical to 2V
  • you may need to adjust the trigger as well

g10

If you give a Go! command with more pulses the width of the very fast pulses sequence gets wider.

g100

Note: that the pulse width is 10uS. This may or may not be long enough for a particular stepper motor.

Also, each pulse immediately follows the previous pulse. The stepper motor may require a short reset after each pulse.

Parser

The Parser is used to parse the incoming commands from the ruby script (see auto_test.rb). The parser can accept the following commands:

  • g : Go! start pulsing using n pulses in the pulse array
  • s : Stop! stop pulsing and capture the pulse count and elapsed time
  • n : Numbers! return statistics at the current moment

Note: are ignored

Parser Go!

Get the total number of pulses across all slots of the pulse array. is in decimal format.

Example: g100 will spread 100 pulses evenly through all slots in the pulse array.

It returns the per slot value and the total number of slots on success.

 g100
 ACK 20 5

Note: currently there is no bounds checking. You could enter "g1234567890" and it would accept it without errors or warnings. This number could, for example, overflow the variables used to convert it to an internal Arduino integer variable. Or it could overflow the maximum value an array slot can hold.

Some odd-ball things are checked. For example "g-10" will give you a warning about the "-".

More complex scenarios can cause a domino effect of errors. In "g-10" causes a warning about "-", but then the Parser resets itself and since "1" is not a valid command, it gives you another warning and resets itself. see [#Notes] for an alternative method.

Parser Stop!

Stops the pulsing by clearing the pulse array

Example:

 s
 ACK

Parser Numbers!

Returns statistics about the current pulsing

Example:

n
ACK 10 0 234 0<lf>

The statistics are:

  • Current number of pulses since Go! This can be 0 if Stop! was sent.
  • Current number of timer ticks since Go! This can be 0 if Stop! was sent.
  • The number of pulses when Stop! was last sent
  • The number of timer ticks when Stop! was last sent
  • The tick time in milliseconds. This is the ISR interrupt timeout. Currently, it's set at 100ms.

Main Loop

The pulsing is done in the ISR, so main() simple looks at the serial port for anything to do.

Notes

A simpler parsing scheme is to:

  • get a character from the serial port, if any
  • if it's not a , save it into a buffer otherwise parse the buffer in its entirety In this way, it is possible to throw out the entire buffer on an invalid command etc.

On the other hand this technique has problems too:

  • how big should the buffer be? Have to add checks for buffer overflow.
  • When do you tell the caller there was a buffer overflow? What if there is no incoming ? Simply sending out a string, while a stream of incoming characters is coming in may not work since the caller may not be listening at that time.
  • it's not as nice when using putty since the characters are "ignored" up until enter is pressed. It works better from ruby though.

- John Arrizza