The ClockClock Project
Software Setup and Programming
Note: This example assumes you are using the latest version of the Arduino IDE on your desktop. If this is your first time using Arduino, please review our tutorial on installing the Arduino IDE. If you have not previously installed an Arduino library, please check out our installation guide.
The microcontroller I used was the RedBoard Turbo, but as I said before, any board with a Qwiic connector could likely be used.
First, I had to install the libraries for the board and the Qwiic RV8803 RTC and buttons. The buttons below will take you to the respective setup tutorials for each.
The code itself isn’t too complicated but the structure is something I end up doing for many designs.
Basically, I built up layers of abstraction until I got to an easy to use layer that made managing the numbers and performing animations easy.
The first layer is, of course, the FPGA. The FPGA gives us an interface to make a motor perform a certain number of steps with a fixed delay between them.
I used the Wire library built into Arduino to manage the I2C bus. This allowed me to make a simple function that would send a single animation.
language:c
void sendAnimation(uint8_t id, int16_t steps, uint16_t delay_cycles) {
int32_t p = currentPosition[id] + steps;
while (p < 0) p += FULL_CIRCLE;
currentPosition[id] = p % FULL_CIRCLE;
Wire.write(id);
Wire.write((uint8_t)(steps >> 8));
Wire.write((uint8_t)(steps & 0xFF));
Wire.write((uint8_t)(delay_cycles >> 8));
Wire.write((uint8_t)(delay_cycles & 0xFF));
}
Two special things here is that I have a constant FULL_CIRCLE
declared which is the number of steps in a full rotation. This is 4096 in my case.
This is used to update a global array of position values of the motors. By always using this function to send the animations, the position of the motors is known.
It isn’t very convenient to think of motion in terms of steps and delays. Instead, it is much easier to think of them in terms of degrees and duration. In other words, instead of thinking “take 2048 steps with 760 delay cycles between each step” it makes much more sense to think “turn 180 degrees over 4 seconds.”
I wrote a function that could take care of that translation.
language:c
void animate(uint8_t id, float deg, float duration) {
float steps = deg * FULL_CIRCLE / 360.0f;
if (steps > 32767 || steps < -32768) {
animate(id, deg / 2, duration / 2);
animate(id, deg / 2, duration / 2);
return;
}
float cycles = constrain(duration * 390625 / abs(steps), 760, 65535);
sendAnimation(id, (int16_t)steps, (uint16_t)cycles);
}
First, it turns the degrees into steps by again using the FULL_CIRCLE
constant. It then checks if there are too many steps for a single animation command and recursively calls itself with half the animation each.
The delay cycles are then calculated. The 390625 value is the number of cycles in a second (100,000,000 / 256 = 390,625). The cycles need to be bound to the range of 760 to 65535. The 760 minimum was empirically found by testing how fast the motors could reliably spin without skipping a step. It is also 8 seconds per revolution.
The upper bound is really high and shouldn’t ever be hit. It would take 687 seconds to do a full rotation. The current design can’t go slower than this. If you needed it to, you would have to change the FPGA’s pre-scaler or the size of the delay cycle value.
The final abstraction I needed was a function to simply tell the motor to move to a position. This would use the currentPosition
value to calculate the minimum rotation needed to get to that point. This is helpful when showing the actual time since I can just call it with all the positions the hands need to be in and don’t have to adjust for where they currently are.
language:c
void moveTo(uint8_t id, float pos, float duration) {
float curDeg = (float)currentPosition[id] * 360.0f / FULL_CIRCLE;
float angle = pos - curDeg ;
if (angle > 180)
angle = angle - 360;
if (angle < -180)
angle = 360 + angle;
animate(id, angle, duration);
}
It starts by calculating the angle the hand is currently at. Then it gets the angle it needs to move by subtracting the desired angle by the current angle.
This angle may end up being the long way around; the two if
statements check for this and switch it to the smaller of the two paths. For example, if the difference of the angles was 270, it would be better to move -90 degrees instead.
To show the actual time, I needed a map for all the digits. This was easy enough to do by simply drawing out how I wanted each number to look and writing down the angles for each hand.
I entered all these into a 2D array that could be used to look up any digit.
language:c
const float digitAngles[10][12] = {{270, 180, 0, 180, 270, 0, 90, 180, 0, 180, 90, 0}, // 0
{180, 180, 0, 180, 0, 0, 225, 225, 225, 225, 225, 225}, // 1
{270, 180, 270, 0, 270, 270, 90, 90, 90, 180, 90, 0}, // 2
{270, 180, 0, 180, 270, 0, 90, 90, 90, 90, 90, 90}, // 3
{180, 180, 0, 180, 0, 0, 180, 180, 90, 0, 225, 225}, // 4
{270, 270, 270, 180, 270, 0, 90, 180, 90, 0, 90, 90}, // 5
{270, 270, 270, 180, 270, 0, 90, 180, 0, 180, 90, 0}, // 6
{270, 180, 0, 180, 0, 0, 90, 90, 225, 225, 225, 225}, // 7
{270, 180, 270, 0, 0, 270, 90, 180, 90, 0, 0, 90}, // 8
{270, 180, 0, 180, 0, 0, 90, 180, 90, 0, 225, 225} // 9
};
With this, I could use the RTC to get the time and display the digits.
language:c
void showTime() {
uint8_t digits[4];
digits[0] = rtc.getMinutes() % 10;
digits[1] = rtc.getMinutes() / 10;
digits[2] = rtc.getHours() % 10;
digits[3] = rtc.getHours() / 10;
for (uint8_t d = 0; d < 4; d++) {
Wire.beginTransmission(0x50);
for (uint8_t m = 0; m < 12; m++) {
moveTo(m + 12 * d, digitAngles[digits[d]][m], 4.0f);
}
Wire.endTransmission();
}
}
It is worth mentioning that my code assumes the hands start at the 12:00 position (straight up). This is the 0 degree mark for all my positions.
In the Arduino loop()
function, I put code that would check the RTC every 100ms and update the time if it changed.
If the hour changed, I also had it perform an animation. I wrote three simple ones that it randomly selects to perform before settling on the new hour.
Inside loop()
I also check the state of the four buttons. My original plan was to use the Qwiic Button’s FIFO interface to keep track of each press, but I ran into a bug that I outlined here.
Instead, I ended up keeping track of the state myself and just used the isPressed()
function to check their current state.
When any individual button is pressed, I update the time. The four buttons allow me to set the minutes and hours independently.
I also added a feature where if you press both hour up and hour down at the same time, the hands will all move to the 12:00 position. This is incredibly helpful if you need to power off the clock or reprogram the Arduino as you don’t have to manually readjust every hand.
That about sums up the design. Take a simple interface and build upon it until it is useful.