Kata // Hardware

The washing machine hardware is built around an STM32 Nucleo, with a choice of an ARM Cortex-M0 or Cortex-M4 core. The mix of peripherals and programming models creates many interesting design and TDD challenges.

Kata home page

Wishy Washy

What’s Here ?

This page aims to give you a functional overview of the hardware, and some samples on how to drive the peripherals needed to get your Washing Machine working. We don’t tell you much here about what you’re going to do with these things - that’s all over on the specification page  - only what the moving parts are, and how to make them move.

We show some sample fragments of code for driving the peripherals via the vendor’s HAL, mostly in C. Note that these are not suggested design, just minimum self-contained examples of one way of interacting with the hardware - you can choose to do it differently, even develop your own HAL from scratch.

Block Diagram

The hardware is designed to give a realistic mix of peripheral interactions, and programming models. Among these are:

  • Discrete GPIO input and output
  • Dealing with GPIO interrupts
  • UART serial I/O
  • A SPI interface to an LED driver
  • Reading sensor input data from multiple ADC channels
  • Pulse-Width-Modulated output to drive the “electric motor”
  • For timers, you have the option of RTOS software timers or a hardware timer

The hardware block diagram below shows a high-level view of the system architecture. The diagram can be zoomed with a click, but there’s also a PDF version that you can download and print 

A block diagram illustration of the washing machine architecture

There’s a full pinout and configuration model freely available  for the kata, including the HAL that’s generated from the model. If you want to modify the model or use a GUI to explore the configuration, you can download the STM32CubeMX tool for free  from the ST Microelectronics website (requires account login, download link is at very bottom of the page).

For the very impatient, here’s the key graphic that you need. The definitions of these peripherals, ports and pins can be found in the file main.h in the generated code.

A block diagram illustration of the washing machine architecture

There are some ICs on the board that do mundane-but-necessary jobs for the design to work, but aren’t a factor in the coding challenge. Briefly, these are a level shifter IC for bridging between 5V TTL and 3.3V CMOS logic levels, and an op-amp IC to support some analog elements of the design. These aren’t covered in the rest of the walkthrough, and they’re omitted in the block diagram above.

Discrete GPIO

Without question the easiest of the hardware interactions you’ll have to work with, the design uses basic GPIO input and output for:

  • Most of the discrete LED (ie single LED in its own package)
  • The three user buttons - although also see the next section.
  • The Door slide switch, simulating the function of the door being opened and closed

If you’re choosing to use the generated STM32 HAL, all of the pin and port definitions are in main.h, and you can use these definitions in your code (lightly edited for page fit - check actual definitions):

// Set the "door open" indicator pin high
HAL_GPIO_WritePin(
  CTL_DOOR_OPEN_GPIO_Port, CTL_DOOR_OPEN_Pin, GPIO_PIN_SET);

// ...and set the same pin back low
HAL_GPIO_WritePin(
  CTL_DOOR_OPEN_GPIO_Port, CTL_DOOR_OPEN_Pin, GPIO_PIN_RESET);

// Toggle the "filling" control indicator
HAL_GPIO_TogglePin(FILLING_GPIO_Port, FILLING_Pin);

Door switch

The slide switch that simulates the door being opened and closed can be read with a HAL function.

// Read the state of the "door switch" input pin
GPIO_PinState state =
  HAL_GPIO_ReadPin(DOOR_SWITCH_GPIO_Port, DOOR_SWITCH_Pin);

Again, this is one of the simpler hardware interactions, although a significant event in the specification. There’s no external interrupt configured for this pin, so you will have to poll it: it’s an intentional hardware design choice to have a different type of hardware interaction from the push buttons.

Buttons and interrupts

There’s a twist to the push-button GPIO. By default, these are configured to generate an external interrupt on a falling edge. So, although you can read from these GPIO pins to see current state, you also have to implement an ISR handler for the falling-edge event.

You can change this configuration if you’d prefer to use polling instead, but ISRs add some nuance to the problem, in the sense of safely managing system state in the face of polled events, timed events, and interrupts. Your call.

Using the HAL, you need to define functions corresponding to the interrupt vector table, for each of the three EXTI external interrupt lines defined in startup_stm32f411xe.s

.word     EXTI0_IRQHandler   /* This is the UP button ISR    */
.word     EXTI1_IRQHandler   /* This is the DOWN button ISR  */
.word     EXTI2_IRQHandler   /* This is the START button ISR */

You can ignore the others - these are defined as “weak” symbols. The HAL requires you to call a helper function to clear the pending interrupt. Here’s an example for EXTI0:

void EXTI0_IRQHandler(void) {
  // Do whatever you need to do here...

  // ...and now clear the pending interrupt
  HAL_GPIO_EXTI_IRQHandler(GPIO_PIN_0);
}

The STM32CubeMX tool will spit out these definitions for you, and it’s up to you to decide (a) what they do and (b) where these should live in your code.

A final word on ISRs - if you’re choosing to use FreeRTOS for this project, remember to use the ...FromISR() variants, eg. xQueueSendToBackFromISR().

Analog “sensor” potentiometers

The ADC peripheral on the STM32 is very sophisticated. There are multiple modes of operation, to suit different types of application. Covering the ADC peripheral in-depth is beyond the scope of this material, so we’re going to look at a basic mode of operation only, as configured in the starter HAL project. You’re free to tweak this. ST Micro publish an application note  that surveys these different modes, and applications.

The STM32F411 has a single ADC peripheral that’s able to scan 16 different channels. By default, the sample HAL has the ADC peripheral configured to scan the two channels corresponding to SENSOR_FILL (which is ADC_CHANNEL_6) and SENSOR_TEMP (which is ADC_CHANNEL_7) in the pinout diagram earlier.

Using interrupts at the end of each channel scan requires you to implement an IRQ handler, and also call (yet another) magic HAL function which will invoke a callback, and then clear the pending interrupt:

extern ADC_HandleTypeDef hadc1;

// This is just necessary boilerplate to let the HAL work
void ADC_IRQHandler(void) { HAL_ADC_IRQHandler(&hadc1); }

void HAL_ADC_ConvCpltCallback(ADC_HandleTypeDef *hadc) {
  uint32_t level = HAL_ADC_GetValue(hadc);
  // Do whatever you need to do with the value now...
}

Also in your code, you need to drive the conversion by telling the peripheral to make a conversion. In this case, we’re starting it in interrupt mode, which result in the ISR above being called when the conversion is complete:

HAL_ADC_Start_IT(adc_handle);

By default in the HAL starter project, the ADC peripheral is configured in what ST Micro call either Scan Single Conversion Mode or Multichannel Single Mode (yes, actually in the same document). The summary is this: you explicitly initiate each conversion in your code rather than the peripheral running in the background, and each conversion you do will scan a successive channel in a rank, then wrap back around to the start.

We only have two channels, so in effect the conversions will alternate between SENSOR_FILL / ADC_CHANNEL_6 and SENSOR_TEMP / ADC_CHANNEL_7 for each call to HAL_ADC_Start_IT().

7-Segment LED displays and drivers

The four-digit segment displays are driven by a MAX7221 LED driver. You can read the datasheet on the Maxim site . You interact with the LED driver chip via a 3-wire SPI connection. This is a unidirectional connection, with a MOSI line from the MCU to the LED driver, no MISO line back: fire and forget.

It’s important to know that the NSS line is software-controlled: you need to drive the NSS line low prior to transferring data to select the LED driver IC, then back high when completed. If the NSS isn’t low, the transmission will be ignored, and there’s no way of detecting this condition in code.

If you’re using the STM32 HAL, the MAX7221 LED driver expects a pair of bytes making up a single 16-bit “command”. The HAL SPI configuration is for the SPI default of 8-bit transfer, and the HAL_SPI_Transmit function is used to transfer two bytes in a single call:

uint8_t low_intensity[] = {0x0a, 0x00};
extern SPI_HandleTypeDef hspi2;

void set_low_intensity_display() {

  // Drive NSS low to select device
  HAL_GPIO_WritePin(SPI_NSS_GPIO_Port, SPI_NSS_Pin, GPIO_PIN_RESET);

  // Sync transfer of 2 x 8-bit packets, with 10ms timeout
  HAL_SPI_Transmit(&hspi2, low_intensity, 2u, 10u);

  // Drive the NSS pin high again to deselect LED driver device
  HAL_GPIO_WritePin(SPI_NSS_GPIO_Port, SPI_NSS_Pin, GPIO_PIN_SET);
}

You can connect a logic analyser to the the 3 SPI lines directly on the Nucleo headers to verify the output signals: SPI_NSS is PB12, SPI_CLK is PB10 and SPI_MOSI is PC3.

PWM, low-pass filter and circular LED display

The “electric motor” that turns the washing machine is powered by a PWM signal. A 100% duty cycle represents the motor running at max power, and 0% is the motor stopped.

The current motor duty cycle is shown by the circular LED display: this is essentially a voltmeter bar graph that reflects the duty cycle. To make this work with the analog LED driver, the PWM signal passes through an op-amp buffer to an RC (resistor-capacitor) low-pass filter, which smooths the PWM signal.

There’s a trim potentiometer R11 that you might need to adjust slightly to make sure that all 10 LEDs are lit when the duty cycle is 100%. It’s also worth noting that the RC low-pass filter component values were chosen to give a perceptible ramp-up-and-down effect as the duty cycle changes. There’s no code involved in this effect, only a couple of passive components and a dubious sense of aesthetics.

In the HAL initialisation code for the PWM timer, is this magic value in main.c, function MX_TIM2_Init(void):

htim2.Init.Period = 1000U;

This sets, in effect, the “dynamic range” of the PWM signal. You can set values for the comparator using the HAL macro __HAL_TIM_SET_COMPARE():

void ramp_up_pwm() {

  // Duty cycle @ 10%
  __HAL_TIM_SET_COMPARE(&htim2, TIM_CHANNEL_1, 100U);
  HAL_Delay(half_second);

  // ...and 50%...
  __HAL_TIM_SET_COMPARE(&htim2, TIM_CHANNEL_1, 500U);
  HAL_Delay(half_second);

  // ...and max duty cycle now...
  __HAL_TIM_SET_COMPARE(&htim2, TIM_CHANNEL_1, 1000U); 
  HAL_Delay(half_second);
}

Beyond the PWM output, we’re entirely into analog-land, so things are imprecise - the analog level response to the PWM output is not exactly linear. A Period of 1000 with these comparison values works effectively for increasing the power in 2 X LED increments:

enum power {
  stopped = 0,
  min = 100U,
  low = 250U,
  medium = 500U,
  high = 750U,
  max = 1000U)
};

Serial communications

TODO

5V power supply

The board needs to be powered by an external USB source via the USB Type B connector. The USB connection to the Nucleo - if you’re even using it - won’t work on its own. The board draws more power than can sensibly be supplied by the ST Nucleo’s onboard power supply, and a surprising amount of the design needs a 5V supply (mainly down to limited choice of LED driver ICs in DIP packages).

The jumper JP5 must be set to E5V for the external power supply to be used. From the ST Nucleo 64 manual:

Selecting the power source by setting JP5

SWD debug

Dual-target TDD means you’ll spend less time trying to debug your program logic on the target device. But, mysteries still need to be solved, and running your code on both workstation and target is what makes it dual.

You have three main options here:

  • You can use the Nucleo’s own built-in ST-Link debugger over a USB connection, and open-source tools like OpenOCD  to bridge a GDB debug session to the firmware. One of the reasons the Nucleo was selected as the platform was to make this available to people without professional debug gear.

  • You can replace the Nucleo’s built-in ST-Link firmware with Segger’s propietary debug firmware ST-LINK On-Board  , and then use an evaluation license of Segger tools for programming and debugging the target device. Note that you don’t get the unlimited flash breakpoints feature, this is only possible with some models of J-Link.

  • If you have professional JTAG/SWD debug hardware (Segger J-Link  for example), you can connect this to the standard 20-pin JTAG header. Note that the design requires an SWD interface, so you’ll need to configure the right debug interface in your tools of choice.

Any of these options should work fine for this kata, but if you have a professional debug probe, it would be obvious and sensible to make this your first choice.

The magic string for connection from the J-Link commander tool is:

JLinkExe -si SWD -Device STM32F411RE -AutoConnect 1 -Speed 4000

Note that only the 100-pin variants of the STM32F411 MCU support dedicated streaming trace pins (PE2-PE6), but not on this 64-pin variant. We’re planning trace support in the next generation of the hardware design, though.

You have the choice of using Segger’s GDB server and either command-line gdb or gdbtui, or configure your IDE’s integrated GDB debug to connect to the Segger GDB server (tip: this second option works very well indeed for JetBrains CLion .

OpenOCD

If you’re using OpenOCD , you’ll need to refer to the OpenOCD documentation  for building and installing the tool, but the good news is that the Nucleo F411 is well supported. A possible configuration is as simple as this:

source [find board/st_nucleo_f4.cfg]
init
reset run

If you want a convenience script that does no more than flash the board, this is a reasonable starting point:

source [find board/st_nucleo_f4.cfg]

proc newboot { } {
 init
 reset halt
 flash write_image erase mywashingmachine.hex
 reset run
}

Be aware that you’ll be limited to 6 hardware breakpoints only, and that the debugging experience is considerably slower than using a more powerful debug probe.