Skip to content

Project Updates

Development Boards to Go

I have a somewhat long trip coming up, and I won't be taking my workshop with me. Still, I want to be able to work on this project while I'm traveling. The decision I came to was to build a couple of development boards that are generic versions that I can use while I travel. I can slot these into the backplane, and pack just a single 12V power supply.

Goals

The project consists of two kinds of things: boards that slot into the chassis and contain some amount of UI, and endpoints that affect the world around them. I'm pretty sure I can build a single board and get most of what I need from that via some I2C magic. The hope is that this lets me have enough to work on most things while I'm away from home without having to carry a lot of extra things with me.

Universal Card

The card needs some basic components:

  • DIN 41612 backplane connector to get power and send CAN-FD signals.
  • WeAct STM32G474 Long MCU module
  • Some status LEDs for LED.

I'm planning to have the following I/O on the board:

For 1 each of the buttons, I'll use a physical debounce (discussed below). For the other, I'll do it in software. Let's see how that plays out.

In addition, because the card is fed with 5V from the backplane, we'll need a 3.3V LDO regulator, probably a 1117-style in fixed output.

Relay Card

The relay card is going to be a bit more "interesting" in a lot of ways. Because I want to be able to (potentially) handle mains voltage (110-120VAC in the US), I have to be careful and take quite a few precautions.

The board will have an AC relay (Omron G2RL-1A-E-ASI DC5) driven via a DRDC3105F. Power in will come via an C13 connector and provided out through a NEMA 5-15 receptacle.

Physical Debounce

While it's possible to do debounce in software, and quite common today to do that with the surfeit of CPU cycles most people have available, I feel like seeing what it would be like to use a physical debounce circuit. First, what the heck is switch bouncing and why do you need to debounce it? I'm gonna borrow some diagrams from this article to tell the story.

First, let's take a look at what it looks like without debouncing on a normal SPST switch:

Image showing both a switch with a pull-up resistor, and the behavior
of it with both clean and dirty bounces

You can see how, due to the mechanical nature of the switch, there can be some bouncing of the output, even with a pull-up resistor. Now, let's take a look at it when you add an RC circuit:

Switch shown with an RC circuit on the output, and charts showing the
discharge and charge cycle

Here we use an RC circuit where the resistor controls the charge and discharge of the capacitor, and a Schmitt trigger to ensure that we get a clean transition. The general guidance is that 20ms is plenty of time for the switch to settle, so if we aim for a 20ms time constant (τ) on the RC circuit, we get:

\[\begin{align} \tau & = R\times C \\ & = 100K \times 220nF \\ & = 0.022 \end{align}\]

Close enough for Seattle.

For the Schmitt trigger, I'm just going to use a 74LVC1G14 which are available in a small SOT package.

For the SPDT switch, however, I want to use an alternate strategy based on NAND gates in an SR latch configuration. The Cadillac of debounce, as it were.

Debounce circuit showing an SPDT switch connected to a dual NAND
gate

For the NAND gate, I'm going to use the basic 74HC00, which unfortunately in the form I have is in an SOIC-14 package. But I paid less than $0.09 each, so who is complaining?

A New Board is Born

Where Have You Been?

The short answer is "around", but the real answer is more complicated. Due to :gestures at world:, I haven't really been mentally in a place where I can spend time working on things like this project. After a few months of just zoning out, I've realized I need the structure of a project to keep me focused, and so I'm trying to pick back up where I left off.

A long PCB is inserted into a
protoboard When last we talked, I was talking about a modular base board built around an STM32H5, but I realized that there was another alternative ... build it around an existing board. So, that's my plan now. This removes a lot of design work, yes, but at least for the initial design, it lets me focus on some other things. I've chosen to use a board from WeAct Studio. These are the people behind the famous Blue Pill and Black Pill STM32 boards. They also make one built around an STM32G474CEU6, which is a 170MHz ARM Cortex-M4 with 128KB of RAM, 512KB of Flash, and 2 CAN-FD buses available. It's in a somewhat long 48-pin package. If you want to buy one, you can find them on your neighborhood massive Chinese marketplace.

Since I'm planning to write all the software on top of Zephyr, and this board isn't in their already extensive list of boards, I needed to add it to that. So, into the breach we go.

First, I read the documentation. Since the SOC is supported already thanks to a ton of work from STMicroelectronics, it really just meant customizing for this specific instance on this board. To do that, I needed to name the board, and then create a bunch of files. The name I chose was stm32g474_long under the weact vendor directory. Those files are:

board.yml

a YAML file describing the high-level meta data of the boards such as the boards names and the SOC.

weact_stm32h474_long.dts

The hardware description in devicetree format. This declares your SOC, connectors, and any other hardware components such as LEDs, buttons, sensors, or communication peripherals (USB, etc).

Kconfig.weact_stm32h474_long

This is the base software configuration for selecting SOC and other board and SOC related settings. Kconfig settings outside of the board and SOC tree must not be selected. To select general Zephyr Kconfig settings the Kconfig file must be used.

board.cmake

Provides information on flashing and debugging the board

weact_stm32h474_long.yaml

A bunch of metadata that's mostly used by the test runner to help understand what tests to run. Note, I don't know why this one ends in yaml, but the other in yml, nor whether it matters, but I just copied the style in the documentation.

weact_stm32h474_long_defconfig

Some Kconfig-style presets for building any application. These can be overridden in the application.

Rather than start from a clean slate, I actually decided to "borrow" some of the already made board configs from an existing WeAct board, the STM32G431 model. This gave me a good head start on what I needed to do.

So let's go through the files. I hope my explanations are both reasonably accurate and passably consumable. I'm learning this as I go along, and so this is just my current knowledge.

board.yml

1
2
3
4
5
board:
  name: weact_stm32g474_long
  vendor: weact
  socs:
    - name: stm32g474xx

This is pretty obvious, but the name of the SOC (line 5) is something you have to find in the existing repository. Some of the naming isn't necessarily obvious to me, but it was pretty easy to figure out. Once you get to the right processor family it's not too bad.

weact_stm32h474_long.dts

This is the biggest of all of them, so let's take it from the top (post copyright). First, this is a devicetree format, which is also used Linux. I can't say as I consider myself to know it yet, but I've started to get a feel for it. I've elided empty lines along the way.

 7
 8
 9
10
 /dts-v1/;
 #include <st/g4/stm32g474Xe.dtsi>
 #include <st/g4/stm32g474r(b-c-e)tx-pinctrl.dtsi>
 #include <zephyr/dt-bindings/input/input-event-codes.h>

First, we set the version of the DTS (devicetree) schema that's in use, then we include some existing files to set up some presets.

12
13
14
15
16
17
18
19
20
21
22
23
 / {
     model = "WeAct STM32G474CEU6 Long board";
     compatible = "weact,stm32g474_long";

     chosen {
         zephyr,console = &lpuart1;
         zephyr,shell-uart = &lpuart1;
         zephyr,sram = &sram0;
         zephyr,flash = &flash0;
         zephyr,canbus = &fdcan2;
         zephyr,code-partition = &slot0_partition;
     };

The first thing to know is that the start of line 12 is the start of the definition of a node, in this case the root (/) node. Like most languages, everything inside the curly brackets is part of that definition.

The /chosen node sets up some defaults. For example, line 17 says that the node lpuart1 is the default Zephyr console. The & is used a lot like C to grab the "address of" something else. They're called phandles. In this case, lpuart1 is the low power UART. Low power means it can function in low power mode when the low-speed (32.768KHz) clock is in effect. It is, however, limited to 9600bps at that time.

25
26
27
28
29
30
31
     leds {
         compatible = "gpio-leds";
         blue_led: led_0 {
             gpios = <&gpioc 6 GPIO_ACTIVE_HIGH>;
             label = "User LED";
         };
     };

First we set up the on-board LED, in this case, an annoying blue one. Walking through this, line 25 creates the /leds node to contain all the LEDs on the board. It then says it's compatible with the gpio-leds hardware feature, and creates a specific instance /leds/led_0 that also has an label with a different name, /leds/blue_led (line 27). On line 28 it sets up the GPIO pins for that LED. Since the LED is attached to pin PC6, we know that it's part of the C cluster of GPIO pins (&gpioc), and then pin #6 in that cluster. Finally, we tell it that the pin is considered active, when it's high. Zephyr can tell the difference between active high and active low pins. The <> defines an array, here with 3 elements.

Note we are referencing phandles that are defined outside of our file and have been brought in via the earlier includes.

33
34
35
36
37
38
39
     pwmleds {
         compatible = "pwm-leds";

         blue_pwm_led {
             pwms = <&pwm3 1 PWM_MSEC(20) PWM_POLARITY_NORMAL>;
         };
     };

Next, what's an LED that you can't dim? Useless, that's what!

So, as above, we explain that it's compatible with the pwm-leds feature, and name the node /pwmleds/blue_pwm_led. We then have to set up the timer that specifically drives the pulse-width modulated (PWM) signal.

The hidden world of timers

This is hidden in things like Arduino, but in Zephyr and most other embedded systems, you have to know which timers are able to be connected to which GPIO pins. This is often considered an "alternate function" of the pin. On STM32 there are multiple times, and each timer can have multiple "channels". There's various restrictions on them, but that's a topic for another time.

Here, we say this PWM is on channel 1 of timer 3, with a nominal period of 20ms, and "normal" polarity, meaning it's active high.

41
42
43
44
45
46
47
48
     gpio_keys {
         compatible = "gpio-keys";
         user_button: button {
             label = "User";
             gpios = <&gpioc 13 GPIO_ACTIVE_LOW>;
             zephyr,code = <INPUT_KEY_0>;
         };
     };

Now we define some fancy "keys". This is part of the gpio-keys feature, which allows you to map GPIO pins into a set of input events, rather than just GPIO events. Think of it as mapping the keys on a keyboard. There are other more advanced options for key matrices. The properties are similar to earlier, except this button is active when it's low (pulled to ground), and then we say that it emits 0 into the input stream when pressed.

50
51
52
53
54
55
56
57
58
59
60
     aliases {
         led0 = &blue_led;
         mcuboot-led0 = &blue_led;
         pwm-led0 = &blue_pwm_led;
         sw0 = &user_button;
         watchdog0 = &iwdg;
         die-temp0 = &die_temp;
         volt-sensor0 = &vref;
         volt-sensor1 = &vbat;
     };
 };

Here we set up a bunch of aliases that can be used to refer to things. These work exactly like any other phandle, so referencing &led0 is the same as referencing &blue_led.

Now comes the beast of a thing .. the clock tree. This is something that if you've never worked low-level (below Arduino, for example), you're probably completely unfamiliar with. There are multiple clock sources, phase-locked loops, and derivative clocks that drive everything. There's an excellent tutorial on the STM32 support forum.

62
63
64
 &clk_lsi {
     status = "okay";
 };

Here we just tell Zephyr that the low-speed internal clock is "okay", meaning you can use it. You'll see this notation somewhat frequently.

67
68
69
 &clk_hsi48 {
     status = "okay";
 };

Here we do the same thing for the high-speed internal clock, which runs at 48MHz.

70
71
72
73
 &clk_hse {
     clock-frequency = <DT_FREQ_M(8)>;
     status = "okay";
 };
Now here's where it gets interesting. Here we have to tell the system what the high-speed external clock is. It just gets a repeated pulse from the crystal, but it has no idea what that frequency is, so we tell it that it's 8MHz.

I got this wrong initially as I misread the schematic, and it resulted in some challenges. I like to use a LED blink program as the initial test because it sorts out the clocks as well as GPIO, and when it was initially set to be a 1s cycle, it was taking 3.something seconds. Something was off.

It turned out that I had both clk_hse and pll wrong. I had copied them from another SOC which has a different target clock rate, and different oscillator frequency. There was definitely some trial and error to sort that out as I also had to make sure I really understood the STM32 clock tree.

75
76
77
78
79
80
81
82
83
 &pll {
     div-m = <2>;
     mul-n = <85>;
     div-p = <2>;
     div-q = <2>;
     div-r = <2>;
     clocks = <&clk_hse>;
     status = "okay";
 };

One of the first things that is done is to use a phase-locked loop to create a derivative highly accurate clock signal from the input. We tell it that we want to use &clk_hse we just defined as the source (there's another separate thing going on for the low-speed clock that's out of scope here). Then we have to provide all the multipliers and divisors that it needs to generate the output clock, which are documented in the chip reference manual, but also some information is in this post. The best way to get these is to use STM32CubeMX and its clock solver to get it all right.

To get an idea of how complex the clock system is, here's a quick screenshot of the clock configuration section of the app:

A screenshot from STM32CubeMX's clock configuration screen

85
86
87
88
89
90
91
 &rcc {
     clocks = <&pll>;
     clock-frequency = <DT_FREQ_M(170)>;
     ahb-prescaler = <1>;
     apb1-prescaler = <1>;
     apb2-prescaler = <1>;
 };

Here we configure the reset and clock control (RCC) circuit, which is one of the main sources that distributes and times things across the rest of the MCU. We tell it to get its source from the PLL, and that the target frequency is 170MHz. We then provide any scalers needed for the various buses inside the MCU.

Why so many knobs?

You might ask, why is this so damned complicated? The main reason is power management. If you're worrying about power consumption, say because you're battery powered, slower clocks use less power. A lot less. Being able to tune clocks across the system to only use the "minimum" clock to get the job done lets you seriously dial down the power consumption of the MCU.

In our case, we don't really care because everything in mains-powered, and we are only talking about milliamps. But in a battery-powered system, you could reduce the power consumption by a factor of 5 or more.

93
94
95
96
97
zephyr_udc0: &usb {
    pinctrl-0 = <&usb_dm_pa11 &usb_dp_pa12>;
    pinctrl-names = "default";
    status = "okay";
};
We now enter the heavy IO section of the devicetree, and we need to start by talking about pin control. This is a deep topic, and is also specific to the individual MCU architecture. Diving in, every GPIO (general purpose IO) pin has an entire little active complex circuit inside it that not only drives it but does a huge amount of configuration. Pin control is what decides if the pin is just a regular IO pin, or if it's using an alternate function (AF), like ADC or DAC. It determines if it's an input or output pin, if it's pulled up or down, or left floating. It determines whether it's an open drain output or configured in push-pull. It's a whole collection of switches, resistors, and other components to let you plop a value in an IO register and have it all magically reconfigure. There's a great introduction to how all this works (in Linux, but it's very much the same in Zephyr) in this presentation.

Here we're creating a node usb on the phandles specifically for the USB AF on pins PA11 and PA12 as the first (0th) configuration of pin control, which in this case is labeled as "default" and marked as OK.

 99
100
101
102
103
104
105
106
107
108
109
110
111
112
&usart1 {
     pinctrl-0 = <&usart1_tx_pc4 &usart1_rx_pc5>;
     pinctrl-names = "default";
     current-speed = <115200>;
     status = "okay";
 };

 &lpuart1 {
     pinctrl-0 = <&lpuart1_tx_pa2 &lpuart1_rx_pa3>;
     pinctrl-1 = <&analog_pa2 &analog_pa3>;
     pinctrl-names = "default", "sleep";
     current-speed = <115200>;
     status = "okay";
 };

Something similar is happening here, but in the lpuart1 we have something different. We have 2 different configurations (0, 1) with different names (aliases basically) of "default" and "sleep". We're saying that in the normal running mode, this is the TX and RX pins for the low-power UART #1, but when the chip is asleep, it flips to a regular analog I/O, which allows for easier wake-up.

114
115
116
117
118
 &i2c1 {
     pinctrl-0 = <&i2c1_scl_pb8 &i2c1_sda_pb9>;
     pinctrl-names = "default";
     status = "okay";
 };

...

120
121
122
123
124
125
 &spi1 {
     pinctrl-0 = <&spi1_sck_pa5 &spi1_miso_pa6 &spi1_mosi_pa7>;
     pinctrl-names = "default";
     cs-gpios = <&gpiob 6 (GPIO_ACTIVE_LOW | GPIO_PULL_UP)>;
     status = "okay";
 };

Here we're defining an SPI set of pins. The cs-gpios defines the pin PB6 as the chip select line for the SPI port, marks it as active when it's low, and then attaches a pull-up resistor to it internally to hold it inactive by default.

127
128
129
130
131
132
133
134
135
136
137
138
139
 &spi2 {
     pinctrl-0 = <&spi2_nss_pb12 &spi2_sck_pb13
              &spi2_miso_pb14 &spi2_mosi_pb15>;
     pinctrl-names = "default";
     status = "okay";
 };

 &spi3 {
     pinctrl-0 = <&spi3_nss_pa15 &spi3_sck_pc10
              &spi3_miso_pc11 &spi3_mosi_pc12>;
     pinctrl-names = "default";
     status = "okay";
 };

More of the same here, but without the chip select defined yet. This can be done in the user's code.

141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
 &timers2 {
     status = "okay";

     pwm2: pwm {
         status = "okay";
         pinctrl-0 = <&tim2_ch1_pa5>;
         pinctrl-names = "default";
     };
 };

 &timers3 {
     st,prescaler = <10000>;
     status = "okay";
     pwm3: pwm {
         status = "okay";
         pinctrl-0 = <&tim3_ch1_pb4>;
         pinctrl-names = "default";
     };
 };

 stm32_lp_tick_source: &lptim1 {
     clocks = <&rcc STM32_CLOCK_BUS_APB1 0x80000000>,
         <&rcc STM32_SRC_LSI LPTIM1_SEL(1)>;
     status = "okay";
 };

We need to set up our timers, and we configure them for use as PWM timers as well. The st,prescaler is an STM32-specific property that says that the timer should be "scaled" down by 1000x.

We also define a low-power tick source leveraging the low-power timer #1. Unfortunately, I don't actually understand the clocks property and copied it from another definition. I have no idea if it works.

167
168
169
170
171
 &rtc {
     clocks = <&rcc STM32_CLOCK_BUS_APB1 0x00000400>,
          <&rcc STM32_SRC_LSI RTC_SEL(2)>;
     status = "okay";
 };

The STM32 has a real-time clock (RTC) that keeps a time and date running as long as it's not in the most aggressive sleep state. Again, copied, because the clocks property escapes my understanding and searching.

173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
 &flash0 {
     partitions {
         compatible = "fixed-partitions";
         #address-cells = <1>;
         #size-cells = <1>;

         boot_partition: partition@0 {
             label = "mcuboot";
             reg = <0x00000000 DT_SIZE_K(34)>;
         };
         slot0_partition: partition@8800 {
             label = "image-0";
             reg = <0x00008800 DT_SIZE_K(240)>;
         };
         slot1_partition: partition@44800 {
             label = "image-1";
             reg = <0x00044800 DT_SIZE_K(234)>;
         };
         /* Set 4Kb of storage at the end of the 512Kb of flash */
         storage_partition: partition@7f000 {
             label = "storage";
             reg = <0x0007f000 DT_SIZE_K(4)>;
         };
     };
 };

We need to break the flash storage into a few pieces here. We define 4 partitions for the boot loader, two versions of the code, and a 4Kb storage area that you could lay a filesystem abstraction on top of, or just store things directly into.

199
200
201
202
203
204
205
206
207
208
209
 &iwdg {
     status = "okay";
 };

 &rng {
     status = "okay";
 };

 &die_temp {
     status = "okay";
 };

Just a few peripherals, the watchdog timer (iwdg), random number generator (rng), and die temperature sensor (die_temp). We simply mark them as available and ready.

211
212
213
214
215
216
217
218
219
220
221
222
223
 &adc1 {
     pinctrl-0 = <&adc1_in1_pa0>;
     pinctrl-names = "default";
     st,adc-clock-source = <SYNC>;
     st,adc-prescaler = <4>;
     status = "okay";
 };

 &dac1 {
     pinctrl-0 = <&dac1_out1_pa4>;
     pinctrl-names = "default";
     status = "okay";
 };

We wire up pin PA0 to ADC #1 channel 1. We tie it to the APB bus clock (st,adc-clock-source), rather than letting it be independent, and then we scale it down by a factor of 4 (st,adc-prescaler).

225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
 &fdcan1 {
     clocks = <&rcc STM32_CLOCK_BUS_APB1 0x02000000>,
          <&rcc STM32_SRC_HSE FDCAN_SEL(0)>;
     pinctrl-0 = <&fdcan1_rx_pa11 &fdcan1_tx_pa12>;
     pinctrl-names = "default";
     status = "okay";
 };

 &fdcan2 {
     clocks = <&rcc STM32_CLOCK_BUS_APB1 0x02000000>,
              <&rcc STM32_SRC_HSE FDCAN_SEL(0)>;
     pinctrl-0 = <&fdcan2_rx_pb12 &fdcan2_tx_pb13>;
     pinctrl-names = "default";
     status = "okay";
 };

The STM3G474 has 3 CAN-FD (or is it FDCAN? I see it written both ways), and so we configure two of them. At this point, this should be mostly understandable.

241
242
243
244
245
246
247
 &vref {
     status = "okay";
 };

 &vbat {
     status = "okay";
 };

And finally, we enable the VREF+ and VBAT nodes on the chip. This allows them to be measured with the ADC.

Kconfig.weact_stm32h474_long

1
2
config BOARD_WEACT_STM32G474_LONG
    select SOC_STM32G474XX
Here we set the base software configuration for selecting SoC and other board and SoC related settings.

board.cmake

1
2
3
4
5
board_runner_args(dfu-util "--device=0483:df11" "--alt=0" "--dfuse")

include(${ZEPHYR_BASE}/boards/common/dfu-util.board.cmake)
include(${ZEPHYR_BASE}/boards/common/openocd.board.cmake)
include(${ZEPHYR_BASE}/boards/common/blackmagicprobe.board.cmake)

Now we setup some things to make programming the board easier. You can get the values for --device and --alt by running the dfu-util -l when the board is plugged in and in DFU mode (for this one, that means holding down the BOOT0 button while pressing the NRST button). You'll get something like this:

$ dfu-util -l
dfu-util 0.11

Copyright 2005-2009 Weston Schmidt, Harald Welte and OpenMoko Inc.
Copyright 2010-2021 Tormod Volden and Stefan Schmidt
This program is Free Software and has ABSOLUTELY NO WARRANTY
Please report bugs to http://sourceforge.net/p/dfu-util/tickets/

Found DFU: [0483:df11] ver=0200, devnum=1, cfg=1, intf=0, path="1-1", alt=2, name="@OTP Memory   /0x1FFF7000/01*1024 e", serial="205939885533"
Found DFU: [0483:df11] ver=0200, devnum=1, cfg=1, intf=0, path="1-1", alt=1, name="@Option Bytes   /0x1FFF7800/01*048 e/0x1FFFF800/01*048 e", serial="205939885533"
Found DFU: [0483:df11] ver=0200, devnum=1, cfg=1, intf=0, path="1-1", alt=0, name="@Internal Flash   /0x08000000/256*02Kg", serial="205939885533"

Looking at this we can see the vendor and device ID in brackets ([0483:df11]), and the alt targets.

weact_stm32h474_long.yaml

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
identifier: weact_stm32g474_long
name: WeAct Studio STM32G474 Long Board
type: mcu
arch: arm
toolchain:
  - zephyr
  - gnuarmemb
  - xtools
ram: 128
flash: 512
supported:
  - nvs
  - pwm
  - i2c
  - gpio
  - usb device
  - counter
  - spi
  - watchdog
  - adc
  - dac
  - dma
  - can
  - rtc
vendor: weact

This metadata is used primarily by the test runner to help determine which parts of the test suite to run. The reason is you could target multiple boards with different capabilities, and then adjust both your application and the testing to meet them.

weact_stm32h474_long_defconfig

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
CONFIG_CLOCK_CONTROL=y
CONFIG_PINCTRL=y

CONFIG_ARM_MPU=y
CONFIG_HW_STACK_PROTECTION=y

CONFIG_GPIO=y

CONFIG_SERIAL=y
CONFIG_UART_INTERRUPT_DRIVEN=y
CONFIG_CONSOLE=y
CONFIG_UART_CONSOLE=y

Finally, we set some default Kconfig settings for every project that they can override. You can learn more about each of these, although I think they're mostly self-evident, from this search interface.

Problems I Ran Into and Open Questions

I ran into a few problems along the way and have a few open questions.

  • As mentioned earlier, the clock tree was the hardest part that I ran into. This is also why I like an LED blink script to test the board with. It's the simplest thing to run, but it exposes timing issues quite quickly. You might not find a small error, but you'll definitely find most of them.
  • I still don't really understand a lot of the phandles for things like clocks. This is just a complex topic, and my search foo isn't finding the information I need to grok it. I'll get there eventually.
  • It's unclear to me how you would start with a clean slate. Definitely starting from a similar board helped enormously.
  • Eventually, I want to figure out how to add a new SOC. They're also defined through the device tree paradigm.

I will be putting this up on Github at some point, but need to write some documentation first, and make sure that I've shaken it out some, before I subject the world to it. My intention is to submit it to the Zephyr project for inclusion in future releases.

Posted on Github

I have posted this on Github, and it's been renamed as the core board since it turns out both forms of the board have the same pinouts just arranged slightly differently.

To do is cleaning it up and submitting it to the Zephyr project.

Here's the blink script that I used.

1
2
3
4
5
6
7
#include <stdio.h>
#include <stdlib.h>
#include <zephyr/drivers/gpio.h>
#include <zephyr/drivers/uart.h>
#include <zephyr/kernel.h>
#include <zephyr/sys/printk.h>
#include <zephyr/usb/usb_device.h>

Just normal includes to get access to everything.

16
17
18
19
20
21
22
23
/* The devicetree node identifier for the "led0" alias. */
#define LED0_NODE DT_ALIAS(led0)

/*
 * A build error on this line means your board is unsupported.
 * See the sample documentation for information on how to fix this.
 */
static const struct gpio_dt_spec led = GPIO_DT_SPEC_GET(LED0_NODE, gpios);

Here's where all that device tree work comes into play. We first resolve the led0 alias, and then we get the GPIO spec for it. This abstraction means that you can have a bunch of different boards with slightly different wiring that don't require any software changes. For many uses, it is unfortuantely, maybe an abstraction that obscures how things work. I'm definitely of a mixed mood about it.

25
26
27
28
29
30
31
32
33
34
35
36
37
int main(void)
{
    int ret;
    int sleep_ms;

    if (!gpio_is_ready_dt(&led)) {
        return 0;
    }

    ret = gpio_pin_configure_dt(&led, GPIO_OUTPUT_ACTIVE);
    if (ret < 0) {
        return 0;
    }

As befits an embedded system, there's no parameters passed to main. I mean, where would they come from?

We then make sure that there's a GPIO pin attached to the led variable, and then configure it to be an output pin.

39
40
    ret = usb_enable(NULL);
    printk("usb_enable: %i", ret);

Here we enable the USB functionality. I ran into some issues here, because according to the docs, this should return 0 on success, and something negative on failure. But I got negative values no matter what, and yet the USB was working. On the heap of things to dig into more.

I had originally wanted to loop here to wait until the console is attached. For that, I just use minicom, and it's executed on my M1 Macbook Air as minicom -D /dev/tty.usbmodem1101 -b 115200. Not really sure that the baud rate matters, but I used it anyway.

42
43
44
45
46
47
48
49
50
51
52
53
54
    while (1) {
        ret = gpio_pin_toggle_dt(&led);
        if (ret < 0) {
            return 0;
        }

        printk("LED state: %s\n", gpio_pin_get(led.port, led.pin) ? "ON" : "OFF");

        // 1000ms = 1s
        k_msleep(1000);
    }
    return 0;
}

Finally, we throw the program into an infinite loop and toggle the pin off and on, printing out the current state of the pin and sleeping 1 second, leading to 1 second on and 1 second off. The program is marginally more complicated than an Arduino sketch, but that's largely because Arduino's framework hides many things from you. Up to a point, this is a huge win, until it's not. For my overly complicated project, I wanted to have it all exposed. I'll be diving into more advanced features of Zephyr later like pub/sub, threading, sensor framework, etc.

And with that, we're kinda done? Somewhat done? The magic smoke didn't come out of the chip, at least!

Oh No, Not That Kind of Oscillation

OK, so maybe things are not happy in switched mode land. In addition to the issues identified in the previous post, things are, to be fair, not good. The first issue is, I think, a minor one. I accidentally misread the datasheet and the P part doesn't have a soft-start pin, but a power good pin. This didn't turn out to be a major issue, but more an annoyance.

The more substantial issue is this ...

A horrifying scope trace

That is, to use a technical term, not good. Not good at all.

Channel 1 (yellow) is the (attempted) 5V output. Channel 2 (cyan) is the 11V input. As you can see from the squiggly nature of the traces, things are unwell in the land. Just a few of the horrors:

  1. The output is swinging 3 volts, mostly around 5V, at least? This is not a good thing, and is what we might call "excessive ripple". Also, the ripple has ripple, which, if you're not Xzibit, is probably not a good thing.
  2. The "ripple" is at 3.7kHz, which is a strange frequency that I can't seem to figure out the origin of.
  3. The input is ... well, I wouldn't expect it to be swinging a full 0.5V in either direction from the 11V setting that it was at.

Further digging into it, just doesn't lead to any rational understanding of the situation. All the capacitors measure reasonably with an LCR meter (in circuit, so accuracy isn't great). Putting it under load, when you get above 2A, the ripple shrinks suddenly to approximately 150mV, which, while still high, is much more sane.

I'm honestly flummoxed.

First Oscillation

One of the things that this project is doing is pushing my skills designing hardware. An area I had largely avoided until now was switch mode power supplies (SMPS). For this project, I am planning to use +12V as the base power rail, +5V as the main distribution rail on the backplane for all the cards, and then local regulation down to +3.3V. The +5V to +3.3V is totally doable (and maybe even preferrably done) with a low dropout (LDO) linear regulator. But the +12V to +5V is a bit more as we're talking about 5-6A of current needed.

SMPS are typically very efficient, and I was hoping to use that to my advantage. It's not that I really need the efficiency, but it's like with machining, you always work to tight tolerances so when you need them, you know how. TI has an amazing tool for building out power circuits, and with some knob twiddling, it spit out a design based around the TPS566238, a wee little QFN package (3mm) that would have a target efficiency of 95%. Perfect!

In this post, I'm hoping to dive into a bit of the design and some logic around it all.

The Centerpiece

Before digging into the specific implementation, let's talk about the chip that it is built around. The TPS566238 is a quite modern regulator for an SMPS. On the surface, the implementation is pretty simple:

Typical application for TPS566238 with the IC, an inductor, and some
passives

That's it, right? Well, almost, but not quite as we'll see in a bit.

As I said earlier, part of the driver is the efficiency of the design, which you can see here:

Efficiency v load chart for
TPS566238

The part can support up to 6A of output current, which makes it perfect for the use case. In addition, it has something like a 50µA quiescent current. Quiescent current being, very roughly, the "overhead" of the chip, or the power that's used that isn't contributing to the switching and outputs. It's more than that, but that's a close enough definition for these purposes.

The Beginning and the End

Schematic of a DIN connector with links to power
rails

It all starts with power coming in at +12V, and going out at +5V. Since the project is built around a DIN 41612-based backplane, we need to tie all that together. As you can see from the schematic to the right, we are using quite a few pins for power. The typical DIN 41612 connector is rated at 2A per contact, so with 8 pins (4 at the "top", and 4 at the "bottom"), that gives us a capacity of 16A, and while we'll need to derate the capacity, we won't need to do so more than 15-20%, leaving us plenty of excess current-carrying capacity.

Additionally, there are quite a few ground pins spread out through the connector. I think it's Rick Hartley that recommends almost 50% grounds, I figured that since I was not doing anything truly high-speed, that about 25% would be more than enough.

Now that we have both a source and sink for current, let's dig into the meat of the design.

Theory to Reality

The "typical application" shown in many data sheets is a highly simplified version of the reality, but it's not misleadingly so. What I ended up with, in the first round at least, was this:

Schematic of the main switch mode
circuit

You'll see along the schematic, that there's a lot of text boxes with various specs in them. These exist because, quite honestly, I wanted to keep my thought process and decisions localized to the schematic for future me to use. A few things to call out:

  1. I upped the inductor from 1.5 to 2.2uH, while keeping the DC resistance (DCR) and saturation current (IDC) roughly the same. The part I chose was Pulse BMQ part. This is a shielded inductor with an DCR of 8.5mΩ and an IDC of 12.5A. The additional inductance allows for a lower minimum input voltage.
  2. I used a similar mix of capacitor packages as recommended, although slightly fewer 0402 packages. Smaller packages reduce the loop inductance of the circuit. I also tried to streamline the bill of materials (BOM) a bit by using multiple of certain capacitors rather than one large one. Everything on the board is either Murata or TDK X7R, with the exception of one X5R composition.
  3. The feedback (FB) circuit is built with 1% Vishay CRCW thick-film resistors. They're about $0.01, or less, in any quantity.
  4. I didn't do anything with the enable (EN) pin. The datasheet is clear that if left unconnected, the internal pull-up resistor will activate the chip.

That's it. Other than the tiny passives, it's not a super complicated circuit.

Blinking (or not) Lights

Everything needs more lights, although in this case, they shouldn't be blinking. The last little bit of the circuit was a test point and some LEDs:

Schematic with LEDs

Again, as before, all the math and calculations around current-limiting resistors is included in the schematic. The LTST-C190KGKT are small 0603 LEDs. While I could have gone bigger, I'm trying to push myself to work with smaller components, as that's just the direction the industry is going in.

The Result

I shipped the designs off to JLCPCB, which is the contract manufacturer I use almost exclusively, and after getting the boards back, I did a janky job of soldering up everything, and identified a few design issues:

  1. The QFN package is stupidly small. Like, just insane. But, I did manage to get it soldered the first time by applying a tiny bit of solder to the pads and then using a hot air reflow station.
  2. The default footprint for the inductor is just barely too small, and required some finessing to get it to actually solder to the PCB.
  3. I put an 0402 right up against the inductor, and that was just stupidly hard to solder. I went through 3-4 of them (cheap!) before I finally got something kinda not terrible.

Multiple probes attached to a vertical
PCB

On first power up on a bench power supply with 12V and current heavily limited, there was no smoke, which, given my inexperience here, and my lousy soldering, is quite surprising. It even generated 5V.

It also generated not great output (about 500mV ripple) at around 3kHz and a rather annoying 17kHz audible squeal which I could never quite identify the source of. I think it was some kind of reverse microphonics with the inductor because I could press on it and get the volume to change. Fun! I think the output ripple might be related to not having much load on it, as I couldn't find my programmable load in the basement. SMPS don't really like being unloaded, and a 2W resistor didn't help much there.

One final area I was worried about was whether or not temperature would be an issue, so I let it run with what little load I could generate for a while, and watched it on an IR camera.

Infrared snapshot of the board, 2 components identified at
34C

As you can see, both the inductor and the regulator/controller both stayed in a very safe range of 34.4-3.45°C. I'll need to verify this under load to have any confidence in the design, but that means finding my programmable load.

Once I've got that, I need to run down those two issues. But, hey, for a first SMPS ever, using tiny freaking parts, there was, in fact, no smoke in the smoke test. So, for now, I'm happy.

A Modular Base Board

One of the things that really delighted me with the kha project is the use of a common PCB for the rack, and having it designed in such a way as to allow many different applications. I decided I wanted to do the same thing, but obviously with my own spin on it.

Goals

The design has the following goals:

  1. A single re-usable PCB that can be used for 90% of the use cases.
  2. Flexibility in programming and debugging
  3. Local power regulation that leverages the +5V rail on the backplane.
  4. Easy ability to change the run-time configuration.

Subsystems

I am using the term "subsystem" quite loosely here. Let's think of it more as just different distinct parts of the base PCB that can be populated as needed, and used as needed. Obviously some, like power, will be there all the time, but others may only be present in a small number of situations.

Power Regulation

The main power rails of the backplane are +12V and +5V, as is tradition. But that's not what "modern" microcontrollers run on. They run on, at most, 3.3V (typically spelled "3v3"), and most can run on much lower voltage. The STM32H5 series, for example, can run on anything from 1.71 to 3.6V. To simplify things (noise gets even more fun to deal with as the potential drops), we'll run the board's main rail on 3.3V.

To get from 5V to 3.3V there are two options. First, we can use a linear regulator in the form of a low-dropout (LDO) regulator. We could also use a local switched mode power supply (SMPS). To choose, I think it's worth thinking about two factor: what's the voltage drop, and what's the power requirements.

Since we're going from 5 → 3.3V, that's only 1.7V, which isn't a huge drop, and well within the capabilities of either type of power supply. For power, let's look at what the worst case might be for that 3.3V rail:

Component Qty mA/ea Total Note
STM32H503 1 200 200 ∑IVVDD
ATA6561 1 0.35 0.35 CAN transceiver
LED 16 20 320 Typical If
ESP32-C3-MINI-1 1 350 350 TX 802.11b @ 20.5dBm

That gets us a total of around 520.35mA for most situations, but 870.35 with an ESP32 with its WiFi radio lit up. Let's call that 900mA. Note, this doesn't include the 5V drive for the CAN transceiver, for example, which is typically 50mA when dominant, but is taken directly from the backplane rail. Most other STM32 CPU are actually lower ∑IVVDD (the maximum current present across all VDD pins) by 50-80mA.

Schematic for a very simple LDO circuit with the
AP2114

Since there's not a massive amount of current, the plan is to use an AP2114-3.3 fixed-output LDO regulator. This is a super simple device that requires a very simple circuit to implement, as shown to the right, which can deliver 1,000mA (1A). The only addition is a tiny voltage divider to drive the EN (enable) pin high from the +5V rail. The total cost for the circuit, assuming 10 instances of it, is:

Component Qty Unit Total
AP2114H-3.3 1 $0.332 $0.332
4.7uF MLCC 0805 capacitor 2 $0.104 $0.208
0603 100mW ±1% SMD resistor 2 $0.01 $0.02

That gets us to a whopping $0.56USD for the regulator circuit.

We do need to think about junction temperature, so let's do a quick calculation of the "worst case" of a full 1A of power.

\[\begin{align} P &= (V_{in} - V_{out}) \times I_{out} + (V_{in} \times I_{Q}) \\ &= (5 - 3.3)\times 1 + (5\times 0.065) \\ &= 1.7 + 0.325 \\ &= 2.025\textrm{W} \end{align} \]

So the most we might dissipate is 2W at full load. So, what does that do to the junction temperature? Let's assume a 40C wost-case ambient temperature (TA) and a TO-252-2 (that's what the H in the part number refers to) package.

\[\begin{align} T_J &= T_A + (\theta_{JA}\times P) \\ &= 40 + (35 * 2.025) \\ &= 110.875\textrm{C} \end{align} \]

The device has a thermal shutdown (TOTSD) of 160C, so we are safely within the margins of the device. This is before we take into account the use of the ground plane as a heat sink off the package tab.

STDC (STM32 JTAG/SWD and VCP)

Samtec FTSH-107-01-L-DV-K-A connector

Every board needs the ability to be programmed and debugged. The standard in the STM32 ecosystem is the STDC connector, which provides multiple functions simultaneously, and is typically exposed via a 0.500" pitch 14-pin connector like the one shown here.

The pin-out according to UM2448 is:

Pin Function Pin Function
1 NC 2 NC
3 VCC 4 JTMS/SWDIO
5 GND 6 JCLK/SWCLK
7 GND 8 JTDO/SWO
9 JTCLK 10 JTDI
11 GND Detect 12 NRST
13 VCP_RX 14 VCP_TX

The pins are for JTAG (J pins) or SWD (SW pins), along with the virtual communications port (serial port) on the VCP pins.

Tag-Connect TC2070 connector and cable showing the spring-loaded pins
used

Unfortunately, those Samtec connectors aren't cheap, running close to $4USD each. Instead, I'm planning to leverage the Tag-Connect system, which uses a set of spring-loaded pins to touch contacts on the board that don't require a dedicated header to be installed. The TC2060-IDC-050 model fits the STDC use case perfectly.

For this situation, it's just a special footprint on the PCB. That footprint looks something like this:

PCB footprint with 14 small circles and 3
holes

They're, effectively, free. And, if you use ENIG finish, very durable.

Serial Console (maybe?)

This is one I'm not completely sure of. There's a "serial port" on the VCP part of the STDC header shown above, and this is probably all that's needed. Still, I keep thinking maybe I should expose a dedicated serial port for ... reasons that I cannot explain, other than "more ports better".

USB Connector (maybe?)

This is also a maybe for me. It's not really clear what the use for the connector would be, and it's also unclear if it would make sense for it to be either a host or peripheral connector as well. I can imagine there being the possibility of an interesting use case of attaching devices to the USB bus, however, so I'll likely include one. Whether it'll be a USB micro or a USB-C is yet-to-be-determined.

MicroSD Socket

One thing that I do want to include is a micro SD card connector. This will allow for future expansion of both memory, but also potentially allowing the SD card to contain the configuration for the board, which might allow every board to be flashed with identical firmware.

This will likely be done with a CUI MSD-1-A connector.

Connector Tree

JST PH 4-pin connector

This is the pièce de résistance that I am lifting from the original design. The basic gist of it is that you pull GPIO/I2C/SPI signals out to something like an 8-pin connector, then you fan it out to 2 smaller connectors, and then potentially fan it out further to even smaller connectors. This allows you to pick and choose what you need. You don't even need to put all the connectors on the board unless you need them, saving money further.

For a connector, I'm using the ubiquitous JST PH that you see everywhere. For example, in one variant, it's used for Adafruit's STEMMA connectors. It's widely used, distributed, and a very durable connector.

Power Connectors

One of the things that I was thinking of doing is providing a bank of power connectors. These could be used to provide 3.3V, 5V, or 12V to front-panel devices directly from the board. They don't need to carry a lot of current and I want to use a small connector, so I'm thinking of using the same JST PH connectors as the signal tree, but in different sizes so that hopefully I won't accidentally plug a signal into a 12V power source.

This is an area that I just haven't fully thought through yet. For example, the ubiquitous OLED screens need, typically a 5-12V input that is then used locally to boost to the drive voltage. EPD (e-ink) screens typically leverage 3.3V to feed the driver circuit. I'm not even sure there's a real use-case for 12V here, which could simplify things.

ESP32 Footprint

ESP32 module with castellated
pins

There are a few use cases where I can imagine wanting to have a WiFi "modem" available. To do that, I'm intending to place the necessary pads on the PCB for an ESP32-C3-WROOM-1 module. This is a RISC-V-based module. The package is only 16x13mm, and includes a PCB antenna. This will likely be against the upper edge of the card so that the copper exclusion zone necessary for the antenna doesn't interfere with the main function of the board.

This will be hung off one of the UART channels of the STM32 microcontroller, and programmed with the ESP32 AT firmware, which presents a full TCP/IP stack, with TLS, HTTP, and even MQTT support, over a crazy modified Hayes AT command set. I believe it maxes out (or maybe just defaults to) 115,200bps, which should be fine for most any real application like checking network time or web APIs. If that ends up being not enough, there is an SPI interface option that can run 10x faster.

Status LEDs

Every computer needs blinking lights. This much is indisputable. So every board will have a few selection of lights for power rail status, MCU status, and maybe CAN bus status. These will likely just be 0603 or 0805 surface mount LEDs on the PCB itself. There may, however, be a front-panel LED that is just a "power good" LED. That would be connected to a dedicated connector on the PCB.

Reset Switch

Panasonic EVQ-P7A01P tactile SMD
switch

Finally, every computer needs a physical reset switch. In this case, tied to the NRST pin on the controller. When this gets pulled low, the processor goes into system reset. The plan is to use these small surface-mount tactile switches at the very front of the PCB that will be able to be pressed via a small rod (paperclip, naturally) through a small hole in the front panel.

Next Steps

I have a first prototype of the connector tree part of the board I've done, but I'm not happy with it. I'm planning to go back to the drawing board once I pick a specific MCU so that I can optimize the design to maximize the flexibility of alternate functions on various pins. In addition, the initial design didn't have any power on the connectors, so you had to depend on the MCU to source power, which isn't a great way to drive a lot of things.

CAN on 8p8c Connectors

8p8c is not RJ45

Pedantic Warning You will often hear people call the modular connectors that are used for Ethernet "RJ45", but that standard is a very specific thing that is not actually what you're using. The correct term is an 8p8c connector, which refers to the 8 positions and 8 contacts.

I have often done this myself, but for the purposes of being technically correct, I will try my best to stick to the proper designation.

For the project, I want to connect the string of devices over CANbus. To do that, I intend to leverage the ubiquitous 8p8c that you see all over the place in Ethernet.

Goals

There were a few goals in mind:

  1. Use existing standards and easily available connectors and cables
  2. Deliver power over the same cable as the signal
  3. Provide reasonable noise immunity

The noise immunity is pretty easy given this is a differential signal and we are going to use twisted pairs to twist the high and low signal together. Looking at an oscilloscope trace of the CAN signal from this TI application note, you can see the differential nature:

An oscilloscope plot showing a single-ended signal converted to
differential signal

Connector Signal Mapping

For the ends, I chose to use a rather boring layout, but I wanted to make sure CAN was twisted together, and that each of the +12V wires was also twisted with a ground.

%3X1X18p8cplug8-pin1CAN_H2CAN_L3GND4+12V5+12V6GND7GND8+12V

Cable Wiring

The wiring is standard Ethernet wiring, following the T568A standard. The reality is that it doesn't matter whether you use T568A or T568B, quite honestly, but I just picked A because it's the most common and what the typical patch cable is wired for.

Category 5/5e is more than adequate for the signalling that CAN or CAN FD use. The general rating for Category 5 is 100MHz, and if we look at a quality cable, like the Belden 1224, we can see that it has the following characteristics:

Max DCR Max DCR Imbalance Max Capacitance Insertion Loss(100MHz) Fitted Impedence
89Ω/km 3% 33pF/100m 21dB/100m 100±15Ω

And it is, in fact, rated to 350MHz, far in excess of what's needed.

The makes the overall wiring looks like this:

%3X1X18p8cplug8-pinCAN_H1CAN_L2GND3+12V4+12V5GND6GND7+12V8W1W1cat5e8x24 AWG (0.25 mm²) X1:1:CAN_H     1:WHGN    X2:1:CAN_HX1:2:CAN_L     2:GN    X2:2:CAN_LX1:3:GND     3:WHOG    X2:3:GNDX1:4:+12V     4:BU    X2:4:+12VX1:5:+12V     5:WHBU    X2:5:+12VX1:6:GND     6:OG    X2:6:GNDX1:7:GND     7:WHBN    X2:7:GNDX1:8:+12V     8:BN    X2:8:+12V X1:e--W1:wX1:e--W1:wX1:e--W1:wX1:e--W1:wX1:e--W1:wX1:e--W1:wX1:e--W1:wX1:e--W1:wX2X28p8cplug8-pin1CAN_H2CAN_L3GND4+12V5+12V6GND7GND8+12VW1:e--X2:wW1:e--X2:wW1:e--X2:wW1:e--X2:wW1:e--X2:wW1:e--X2:wW1:e--X2:wW1:e--X2:w

Power Handling

Voltage Smoltage

I haven't actually settled on the voltage to use on the cabling. For now, I am using +12V, but moving up to +48V would (basically) quadruple the available power without requiring any additional copper. It does, however, somewhat complicate the individual device power supplies.

At this point, I'm planning to run +12V across the cable to allow for most things to be powered off the central device. Given I use solid rather than stranded cables, we can the standard AWG wire gauge ratings of 3.5A on a 24 AWG single-core wire.

Now we can't do the math quite yet, as we need to derate the wire based on two different factors, according to the NEC:

  1. Ambient operating temperature
  2. More than 3 wires in a cable

So, if we do the math for operating at 40C (104F), and with the 3 power carrying conductors in the cable, we get derating factors of 0.88 and 0.80 respectively. So...

\[\begin{align} P &= 3 (3.5A \times 0.88 \times 0.80) \\ &= 3 (2.464) \\ &= 7.392A \end{align} \]

So that means we can do 7.392 * 12 = 88.7W of power on the cable. But there's a catch. The current most advanced power-over-ethernet (PoE) standard, IEEE 802.3bt-2018, says that each pair of conductors is designed to handle 960mA, which would actually put us at (generously!) 960 × 2 or 1.92A, or 23W at 12V.

This is the reason I'm thinking of switching to driving the cable with 48V, which would let me move up to 92W, which is about what the standard allows. So, perhaps I will change to a 48V distribution, which will require something like an LM5576.

We'll burn that bridge when we come to it.

Quick Note About Diagrams

The diagrams above were done with an amazing tool called Wireviz, and embedded into the MkDocs site you're reading using a little hacky Markdown extension I wrote.

A Bus is a Bus, Of Course, Of Course

One of the defining features of the project is using a modular backplane in a subrack enclosure. I figured we can dive into some of the details so far.

Design Principles

When I was looking at how kha structured the bus, I decided to pull a few ideas from what they did:

  1. Leverage DIN 41612 for the physical format of the bus. This is a very well understood and documented specification, and it has been battle-tested over many decades.
  2. Simplify the backplane routing requirement.
  3. Carry both +12V and +5V. Can be locally regulated down to the needed voltage.

Physical Connector

I decided to use the DIN 41612 connector because it is an absolute tank. It has been used, at least, since the late 1980s in dozens and dozens of critical systems. It's also relatively inexpensive and not very finicky to work with.

View of the connector

Diagram showing a straight receptacle and a right-angle
plug

While kha used a 32-pin connector (2 rows of 16 pins), I've decided to use a 64-pin connector. The reason is that this allows me to run many more ground lines across the connector, and also double up the power. I am using the DIN 41612 connector in it's type B configuration. This means it has 2 rows of pins/contacts, labeled "a" and "b". The numbering scheme is basically A1/B1-A32/B32.

There are many other arrangements that are used in other systems, and many have 3 or 4 rows, but this is far more than enough for this setup. It also keeps it from even starting to look like VMEbus, which uses a 3 row 96-pin version.

Pin numbering on a DIN 41612

In the type B configuration, the receptacle (female) connector is on the backplane and the plug (male) is on the card at a right angle. While you can get connectors reversed, I didn't see any reason to deviate from this arrangement.

The connectors also have 2.8mm holes for M2.5 screws that allow them to be very securely attached to the PCBs, without needing to put any strain on the solder joints.

Signal Assignment

To simplify the routing enormously, I've decided to keep the "a" and "b" rows identical. This allows for the bus to be routed straight across the backplane. The "signals" can be broken down into a few groups:

  • Ground
  • Power rails, both +12V and +5V
  • CAN bus (differential encoding)
  • User-defined signals (BUS_*)

This breaks down to this layout, where ground is heavily interspersed with the signals to help ensure reasonable signal integrity.

Pin # Row A Row B
1 GND GND
2 +12 +12
3 +12 +12
4 +5 +5
5 +5 +5
6 GND GND
7 CAN_H CAN_H
8 GND GND
9 CAN_L CAN_L
10 GND GND
11 BUS_00 BUS_00
12 BUS_01 BUS_01
13 GND GND
14 BUS_02 BUS_02
15 BUS_03 BUS_03
16 GND GND
17 BUS_04 BUS_04
18 BUS_05 BUS_05
19 GND GND
20 BUS_06 BUS_06
21 BUS_07 BUS_07
22 GND GND
23 BUS_08 BUS_08
24 BUS_09 BUS_09
25 GND GND
26 GND GND
27 GND GND
28 +5 +5
29 +5 +5
30 +12 +12
31 +12 +12
32 GND GND

Power Capacity and Derating

One of the things I wanted to do is be able to carry 6-8A of current on both the +12V and +5V rails. The standard for the connector specifies 2A per pin, but you also need to derate this number for use in the real-world. If we look at Harting's derating curve, we get this:

A derating curve for DIN 41612 connectors from
Harting

Most standards talk about "room temperature" at 20C, but it's typically necessary to actually use a higher temperature in the real world. The resistance of the copper along with all the other components will raise the ambient temperature. If we assume 35C (95F), then we can see on the curve that we can expect about 1.8A. As I showed above, we are using 8 pins per rail, which gives us a safe load capacity of 14.4A, well above what we are targeting.

We are actually safe, for this definition of safe at least, up to 98C (208F), which is a temperature unlikely to occur in my house, even without air conditioning in Seattle.

PCB Layout

The layout is actually pretty simple, and only needed 2 layers for the initial run. Depending on what signal integrity looks like, I may convert to 4 layers, but because I was able to keep the entire back as a ground, basically, I have reasonable confidence. The top of the board looks like this:

Top of PCB showing 7 slots

Cliff  FCR7350 connector

You can see in the design how everything goes across horizontally, which means there's no need to really "route" anything creatively. The ground is tied together on both sides as the plated through holes connect the two sides.

It has a 8C8P connector for CAN, two Cliff FCR7350 4mm safety banana jacks for +12V and test points that can be used to check power and signals. I also put all the signals on the silkscreen.

All mechanicals are in alignment with Eurocard and VMEbus standards.

You can find the KiCad files in GitHub.

Willkommen. Bienvenue. Welcome. C'mon in.

I'm going to try and do something new with this project, and that's to document the messy convoluted process that my brain goes through to do anything. Historically, I've largely kept that all to myself, and not surfaced anything until the end, if even then.

Inspiration

The inspiration for this project was a post on Hackaday, which I can no longer find, which pointed to this project, which was a wildly over-engineered home automation project based around 19" subrack and a custom protocol built on top of RS-485. You can find the full project on GitHub, and there's even an amazing deep dive YouTube video

The absurd over-engineering really appealed to me as a project to tackle, and a way to push my embedded hardware and software skills way further than I have historically. I cannot emphasize enough just how much I've been inspired by their work. Some things I've lifted nearly wholesale are:

  • Leveraging a subrack. This gives a physical structure to the system, but also lets me build a backplane-based system as well.
  • Eurocard sized modules, and use of the DIN 41612-style connectors.
  • Doubling up the connector pins to simplify the backplane routing. This means that both the a and b rows are the same signal, allowing for a straight line bus.
  • Leveraging 8P8C for connectors on the network.
  • Designing a main Eurocard base for a vast majority of the work. The idea of having a single PCB that can be adapted to a bunch of different uses is very appealing
  • Putting power for the backplane into a slot.
  • Leaning into a specific "vibe" for everything that has a bit of an alternate timeline feel to it.

There's also this project that applies SCADA to the situation, but this just didn't "vibe" with me as an approach.

Goals

When it comes down to it, I have absolutely no need for this project. It doesn't solve a gap in my life, as I have HomeKit for a lot of things. Instead, I have a few goals with the project:

  1. Learn. This is the #1 goal by a wide margin. So what do I want to learn?
  2. Designing with modern surface-mount technology, and that includes working with fine-pitch packages, tiny (0402) passives, and various leadless packages.
    • Build some understanding of modern power supply systems, such as switch mode power supplies.
    • Dig into modern embedded frameworks.
    • Push my ability to do the full integration from top to bottom of a project.
    • How to design seriously resource constrained protocols.
  3. Build something silly and a bit absurd. Nobody needs this, so lets have some fun.

In addition, there's some stretch goals/features I hope to (eventually) explore:

  • A mechanism to do updates across all the nodes with a single experience. Thinking about how I might use CAN to update the firmware on everything, perhaps when a USB drive with the proper data in the proper structure is inserted. This will be non-trivial, but also I'm going to skip the insane security complexity because this is not a thing on the Internet. If that ever changes, then that consideration will have to change. mcuboot offers some capabilities here that I hope to leverage.
  • Integrating WiFi for a needlessly complicated time source. This will likely be done using an ESP32 series module as an co-processor.
  • Integrate self-monitoring/analysis into the designs. This would allow the MCU to also check everything about itself. Perhaps using an analog switch like the 74HC4067 connected to the ADC within the MCU, and a digital power monitor, like the INA series from TI. This is inspired by how a lot of test equipment can verify its own function.

Decisions So Far

Part of the process of getting started for me is doing a ton of research and then making some critical decisions that shape the project's overall strategy.

Eurocard/Subrack/Backplane Structure

A Rant About Standards

One of the most exhausting parts of doing this project has been trying to find the international standards that I've mentioned. I've been fortunate through some search-fu to find copies of the PDFs that are typically very expensive. Go support Carl Malamud and the fundamental work he's doing to make law and standards more accessible.

I really like this decision in the inspiration project. I had been thinking about building a modular sensor system in this format, and this gave me a better idea to work with in this case. I'm intending to use a 3U (133mm, 5.3") subrack, and am exploring a couple of different widths. The standard width (19", but also called 84HP) is the easiest to get, but it's a bit large for what I'm trying to do. I'm also planning to use a 175mm (6.89") standardized depth because this isn't that deep for home use, and doesn't necessarily require it to be put into a physical rack (which I don't have space for).

Fortunately, subrack products aren't particularly expensive, and I've been 3d printing mock-ups to explore the physical form factor better.

For the cards themselves, I'm intending to use the base Eurocard specification, which is 100x160mm. I'm trying to figure out if it's possible to move to the 100mm depth, which would both result in a smaller footprint, but also fit within the "cheap" sizes for most of the Chinese PCB manufacturers, which would reduce the cost overall.

A DIN 41612 rectangular backplane connector with 2 rows of 32 contacts
each

For the backplane, I'm using VMEbus for mechanical inspiration, but have designed my own signaling assignments because I'm not building a general purpose bus, but one designed for very specific purposes that benefits from decades of changes in the microcontroller ecosystem. The connectors will be type B DIN 41612 with 64 contacts in 2 rows. The Harting part is shown to the right. The connectors also have a 2A per contact current carrying capacity, which means they can be used to cary a decent amount of power.

STM32 Microcontrollers

kha is built on the AVR microcontroller ecosystem. This is a very well regarded 8-bit MCU platform that has been around for quite some time. It's widely used in hobbyist and embedded projects. That would be a great choice, but I decided to go with the ubiquitous STM32 series of ARM-based microcontrollers. You might know them from the Blue Pill and Black Pill boards. ST has a bewilderingly broad product set to work from, and have a competitive price point for something with wildly more capability.

I've settled on exploring 3 different chips:

Chip Core Flash RAM CAN Price (25 unit)
STM32G0B1RE M0+ @ 64Mhz 256Kb 144Kb 2 x CAN-FD $5.5140
STM32F072RB M0 @ 48MHz 128Kb 16Kb 1 x CAN $4.8544
STM32H503RB M33 @ 250MHz 128Kb 32Kb 1 x CAN-FD $3.3728

As you can see they're all over the place. The H5 series is by far the fastest one, and the cheapest, which is curious. I've picked up Nucleo dev boards to start exploring.

While I'm going to be designing my own boards, I intend to leverage the Nucleo board schematics as a starting point for best-practices. This is a good thing.

CAN Bus

The first area where I've deviated from my inspiration is I intend to use CAN (and potentially CAN-FD). I've been working with CAN some in the robotics space, and it has a lot of nice capabilities. The inspiration used RS-485 (which is another great choice), but this also required them to implement a lot of low-level protocol capabilities that come for free with CAN.

One of the open questions is whether to use another abstraction on top of CAN. If I use "regular" CAN, there's only 8 bytes available in each packet. That may be plenty. CAN-FD allows 64 bytes. But there are other options that can extend that to nearly limitless. Some options are:

These come with higher-level abstractions, but I think they might just be too much for this specific case. I think, given the use case, 8 bytes is probably plenty.

Zephyr

I struggled with the framework to build all the software in. I've done a bunch of things, quite successfully, with Arduino, but I felt like I needed to stretch my skills, and work in something a bit more robust. I initially thought about using FreeRTOS along with ST's STM32Cube software, but I'll be honest... Eclipse gives me hives.

So, after looking at ThreadX, and a few others, I decided to build on top of Zephyr for a couple reasons:

  1. Huge board and chip support, and it's well supported by the vendors as well, and not just hobbyists.
  2. Gigantic (although nowhere near Arduino scale) ecosystem of modules and code.
  3. Excellent high-level primitives to build on top of, such as robust threading and IPC support, network stacks, sensor framework, and numerous other modern components.
  4. Even comes with things like a built in robust shell capability.

I have had a bit of a challenge wrapping my head around the workspace concept they use, but I've, I think, gotten past that now. I still don't have development working on my Windows machine, and I'd like to be able to use any platform to develop.

Where I Am Today

I am very early. I have:

  • Acquired a few test boards and CAN transceivers to work with before I commit to any specific PCB design.
  • Designed a breakout board for the Nucleo format to get IO onto JST PH connectors.
  • Designed an initial 7-slot backplane to work with. This is subject to change in the future, and I'll need a bigger one for the full size project, but this is a good start.
  • Designed a bunch of reusable schematic components that will show up all over the place.
  • Built a few toy applications in Zephyr to try and better understand both the APIs, but also the odd build infrastructure that they use. I still haven't gotten it working with my Windows 11 desktop, but it works fine on my MacBook.
  • Designed an initial switch mode power supply around the TPS566238 controller chip. Simulations say this should hit 95% efficiency, if not a bit above that. But, it's a dumb package, a 3x2mm VQFN. I have a backup design I've worked out built around the TPS566242, which comes in a marginally "better" SOT-53 package, with efficiency targeted around 92%. Fortunately, the passives are all the same between the two, just arranged slightly differently. This will be used to regulate the 12V backplane voltage to 5V for distribution along side. The individual boards will regulate down to their 3.3V as needed, leveraging an AP2113K-3.3 low dropout (LDO) linear regulator.
  • Figured out a bunch of initial connector pin outs.
  • Figured out an initial minimal protocol for communications.

Right now, these aren't in the repository, but over the next few days, I'll be moving them into here.

So, there's the big messy start to this adventure.