board::mini

The ultimate vehicle-hacking platform

Apr 22, 2021

Project update 6 of 7

Showcase - A CAN-Controlled Word Clock

by Florian Eich

Perhaps you already know what a word clock is, but just in case you don’t:

Source: Doug’s Word Clocks

It’s a clock, but it shows the time as words, not as numbers or using a graphical visualization. And honestly - it is pretty neat, isn’t it? We think so!

Why are we telling you about word clocks, though? Well, we have a good friend and trusted advisor who is a mechanical engineer and decided he wanted to build one based on an online tutorial. He did do so, creating a frame milled from MDF board and a fairly prototype-style wire-up of WS2812 LEDs, all connected to a Raspberry Pi.

He then got in touch with us, and now we’re working together to make his word clock project even cooler. It will have a custom PCB for the LEDs that will include a connector for the mini::base. And that PCB will have the mini::pit schematics printed on it! More on that in a later post. Today, we want to look at a quick showcase we hacked together based on:

So let’s dive in!

The Setup

The following picture shows an overview of the setup we used, which is fairly simple. The word clock is in German, by the way. If you don’t speak German, consider this an opportunity to learn some in preparation for the next Oktoberfest! (Which will happen again, we promise…)

Before you say anything: yes, it’s true, there’s no real reason to use a CAN bus for this project. Nevertheless, it’s useful to add CAN into the mix as a way to highlight a few important aspects of the mini::base. And, anyway, it’s just fun to play with CAN bus when given the opportunity.

Brave New World

We know the world of embedded software is still enamored with C. There is, however, a brave new world called Rust, and we’ve been playing with it a whole bunch over the last two years. Mostly on Linux, as embedded was still pretty young when we first had a look. But it has come a long way recently, so we’ve decided to go all in and do as much as possible in Rust. Including this project!

There will be much more material from us regarding Rust on embedded. It’ll be great! For now though, let’s dive into a bit of code.

Timings Are Important When Things Move Fast

We have two pretty time critical components in this showcase: the WS2812 LEDs and CAN. But how?, you may ask. Aren’t the LEDs just LEDs, an the CAN bus mature enough not to have issues like this with is? Yeah, sort of. The WS2812 are controlled via a one-wire interface and since there is a bunch of information to be pushed through, speed and precision matter.

By default, the STM32F103CBT6 is clocked much lower than the advertised 72 MHz, so we’ll have to fix that:

let dp = pac::Peripherals::take().unwrap();
  let mut rcc = dp.RCC.constrain();

  let clocks = rcc.cfgr
                  .use_hse(8.mhz())
                  .sysclk(72.mhz())
                  .hclk(72.mhz())
                  .pclk1(36.mhz())
                  .pclk2(72.mhz())
                  .adcclk(12.mhz())
                  .freeze(&mut flash.acr);

What happened here? First, we acquired the device peripherals to lock them. Then we go ahead and find the Reset and Clock Control (RCC). In the configuration of the RCC, we can now set:

And finally, we call freeze to "log in" our settings and produce a Clocks struct containing all necessary information to work with the clocks.

"Back up," you say? No worries. Here’s where we got that from:

It’s from ST RM0008, the reference manual for the STM32F101 to STM32F107 MCUs (page 93), and it shows the clock tree of the MCUs. From there we, derive what we need to do in terms of frequencies (you are welcome to follow my lovely text-marker annotation), and then we drop those frequencies into the well documented Rust library.

There’s one last road block though.

Yes, We CAN

Setting up the CAN involves usually defining bunch of things as well, arguably the most important of which is the baud rate. CAN is pretty resilient, however it does also a need a fairly accurate clock - which we’ve provided via the use_hse call. So we set the baud rate like so:

// APB1 (PCLK1): 36MHz, Bit rate: 500kBit/s, Sample Point 88.9%
  // Value was calculated with http://www.bittiming.can-wiki.info/
  can1.modify_config().set_bit_timing(0x001e_0003);

And indeed, it works! At lower baud rates the CAN fails with the MCU going at 72 MHz, so below 250 the MCU has to be clocked down - however if you use our upcoming Rust library with the mini series, this will be taken care of!

Good! Now we need the time.

Havin’ a Good Time

The word clock needs the hour and the minute to do its thing. We need to get it there via CAN; that’s the goal we’ve set for ourselves.

The Linux date command gives us the date and time:

» date
Tue Apr 20 01:08:23 PM CEST 2021

The date command also understands options to output only date / time components:

» date +"%I"
01  # hour in AM/PM format
» date +"%M"
11  # minute in the hour

Sending this via CAN requires a CAN interface of some sort. We’re using the USBtin, of which we’ve made our own version, the usb-can. The README instructions we publish have all the necessary setup information for a serial CAN link over which we can speak using socketcan. We end up with the possibility to send CAN frames to an interface called slcan0 using a utility called cansend. Plugging our date magic into all of this, we get:

while true
do
  cansend slcan0 7ff#$(printf '%02x%02x' $(date +"%I") $(date +"%M"))
  sleep 5
done

This will send the hour and minute to the serial CAN interface on CAN ID 7ff every five seconds. There are of course more elegant solutions but this one works for us for now, so let’s stick with it.

The Receiving End

On the board, we now have to make sense of this CAN frame. First, we wait (blocking) for a new frame; then we decode it and finally we translate it to the board LEDs:

// blocking read - this is the code we loop
    if let Ok(frame) = block!(can.receive()) {
      // decode hour, minute from the CAN frame. there is only the one CAN
      // frame travelling on this bus, otherwise this code is more complex
      let (hour, minute) = match frame.data() {
        Some(data) => (data[0], data[1]),
        None => {
          defmt::error!("FATAL: unable to read time data");
          gridbox::exit();
        }
      };
      defmt::info!("received time: it is {}:{}", hour, minute);

      // reset word clock data
      reset_wclk(&mut wclk_data);

      match minute {
        0..=4 => set_word(&mut wclk_data, UHR, ON),
        5..=9 => {
          set_word(&mut wclk_data, FUENF, ON);
          set_word(&mut wclk_data, NACH, ON);
        }
        10..=14 => {
          set_word(&mut wclk_data, ZEHN, ON);
          set_word(&mut wclk_data, NACH, ON);
        }
        15..=19 => {
          set_word(&mut wclk_data, VIERTEL, ON);
          set_word(&mut wclk_data, NACH, ON);
        }
        20..=24 => {
          set_word(&mut wclk_data, ZWANZIG, ON);
          set_word(&mut wclk_data, NACH, ON);
        }
        25..=29 => {
          set_word(&mut wclk_data, FUENF, ON);
          set_word(&mut wclk_data, VOR, ON);
          set_word(&mut wclk_data, HALB, ON);
        }
        // ... it goes on here
      }
      // some more code
    }

The full code example is in the board-mini GitHub repo for you to study.

And then, finally, let’s see if it works!

It does! Hurray!

With that Fun Thing™, we’ll leave you for now. We hope you enjoyed this. And, if you did, please consider posting about it :) More soon!

Cheers!


Sign up to receive future updates for board::mini.

Subscribe to the Crowd Supply newsletter, highlighting the latest creators and projects