Custom-Built Servo Tester

When evaluating and repairing servos you need a good servo tester. It needs to be portable, have an easy-to-read screen, have lots of adjustable parameters, and it should have an internal battery while still allowing an external power source. There are several testers on the market but none of them did everything I wanted. So I made my own servo tester. This was my first real project with the STM32F4 and it turned out great. I later ported it to the STM32F0 (since the F4 is overkill for this task) and it performed just as well.

There are four modes: sweep, three positions, two positions, or one position. The sweep mode is useful when checking for damaged (but not stripped) gears. The three-position and two-position modes are great for testing the motor -- it should be warm, not hot, after half an hour of cycling. Lastly, the one-position mode is helpful when setting up a new model.

Each mode has several parameters that can be adjusted. The number of cycles can be limited. The framerate (specified as a period) can be changed. A blinking underline specifies which parameter is currently selected. The buttons on the top adjust the parameter's value, while the buttons on the front change parameters. The rest should be obvious:

It was a fun project. Download the source code for the STM32F4 or STM32F0. See the README file in each archive for details.

Saving Some Money with the STM32F0

When you don't need lots of processing power the F0 is a great option. They're about one-third the price of the F4, while still having plenty of power for most hobby projects. The F0 Discovery board costs around $8 and the bare MCUs are around $2 - $4.

The linux stlink project doesn't fully support the F0 yet, but OpenOCD works well. I have found OpenOCD to be quicker and more stable, so I now use it for the F4 and F0.

Check out my earlier post on the F4 for information on setting up the compiler. Then download and build a version of the STM F0 library with makefiles:

$ mkdir ~/stm32f0 $ cd ~/stm32f0 $ git clone https://github.com/szczys/stm32f0-discovery-basic-template.git $ cd stm32f0-discovery-basic-template/Libraries/ $ make

Download the latest version of OpenOCD, then:

$ cd ~/stm32f0/openocd-0.6.0/ $ ./configure --enable-stlink $ make $ sudo make install $ sudo cp contrib/openocd.udev /etc/udev/rules.d/openocd.rules $ sudo udevadm control --reload-rules

You can use one of the examples in the library as a starting point, or download my empty project template and use it to get started.

STM32F4 Basics: Timers (Part 1)

These STM32F4 Basics posts aren't really tutorials so much as they are slightly-organized notes with short and contrived examples. Many aspects and nuances are ignored. Look through the documentation and headers referenced throughout this post to fill in the gaps. The Timer chapters in RM0090 would be a good place to start.

A timer can be used in several different ways:

  • As a timebase: a counter that can periodically run a block of code.
  • As a counter that tracks how often a pin has transitioned high or low.
  • For pulse-width modulation (output or input.)
  • ...

There are several clock sources, multiplexers, prescalers and clocks that come into play. The clock tree (RM0090, Section 5.2, p.85) should be kept in mind, and double-checked when things don't work as expected. The system_stm32f4xx.c file is used to setup this stuff.

Remember that IRQs will not go into effect until they are enabled in the NVIC. ISR names must match the names defined in startup_stm32f4xx.s.

There are 14 timers with varying abilities. (DM00037051, Table 3, p.29)

Timebases
The simplest timer starts at zero, counts up to some chosen number, then generates an update event. That event can be used to call an ISR. After that event the counter can start over again or stop. The rate at which the counter counts depends on its clock source. All timers can use the internal timer clock shown in the clock tree, and most timers can use other sources covered later on.

If the MCU is setup with a 168MHz system clock, an AHB prescaler of 1, and an APB prescaler of 4, the internal timer clock will run at 84MHz. A 16-bit prescaler is often used to slow down the timer. With a prescaler of 42000 there will be 2000 "ticks" per second -- two ticks per millisecond. The timer will count up to a 16-bit or 32-bit number called the "auto-reload" value, then generate an update event and reset the counter. With an auto-reload value of 2, and a prescaler of 42000, an update event will occur once per millisecond. The formula is:

Example:

// Illuminate the blue LED (D15) three seconds after power-on. #include "stm32f4xx.h" #include "core_cm4.h" int main() { RCC->AHB1ENR |= RCC_AHB1ENR_GPIODEN; // Enable GPIOD clock GPIOD->MODER |= GPIO_MODER_MODER15_0; // Enable output mode for D15 RCC->APB1ENR |= RCC_APB1ENR_TIM6EN; // Enable TIM6 clock TIM6->PSC = 41999; // Set prescaler to 41999 TIM6->ARR = 5999; // Set auto-reload to 5999 TIM6->CR1 |= TIM_CR1_OPM; // One pulse mode TIM6->EGR |= TIM_EGR_UG; // Force update TIM6->SR &= ~TIM_SR_UIF; // Clear the update flag TIM6->DIER |= TIM_DIER_UIE; // Enable interrupt on update event NVIC_EnableIRQ(TIM6_DAC_IRQn); // Enable TIM6 IRQ TIM6->CR1 |= TIM_CR1_CEN; // Enable TIM6 counter while(1) { } } void TIM6_DAC_IRQHandler() { if(TIM6->SR & TIM_SR_UIF != 0) // If update flag is set GPIOD->BSRRL = GPIO_BSRR_BS_15; // Set D15 high TIM6->SR &= ~TIM_SR_UIF; // Interrupt has been handled }

Externally-Clocked Counters
You can use a GPIO pin as the clock source instead of clocking the counter with some fraction of the system clock. The signal on that pin does not even need to be periodic. A clock pulse can be interpreted from each rising edge on that pin. The timer functions that a GPIO pin can accommodate vary from pin to pin. (DM00037051, Table 6, p.44) There are two external clock modes: external input and external trigger. The main differences are that the external trigger mode can have a prescaler, and there are fewer pins that can be used in that mode.

External Input Example:

// Count how often the blue pushbutton is pressed (how often A0 transitions high.) // The count is stored in TIM5->TIM_CNT, examine it in GDB with "print *(unsigned long*) 0x40000C24" // Unfortunately the pushbutton is not debounced in hardware and the software filters are not enough, // so the count can increment by more than one each time you press the button. #include "stm32f4xx.h" int main() { RCC->AHB1ENR |= RCC_AHB1ENR_GPIOAEN; // Enable GPIOA clock GPIOA->MODER |= GPIO_MODER_MODER0_1; // Enable AF mode for A0 GPIOA->AFR[0] |= 0b0010; // Select AF2 for A0 (TIM3/4/5) RCC->APB1ENR |= RCC_APB1ENR_TIM5EN; // Enable TIM5 clock TIM5->CCMR1 |= TIM_CCMR1_CC1S_0; // Input mode, map TI1 to IC1 TIM5->CCER &= ~(TIM_CCER_CC1P | TIM_CCER_CC1NP); // CC1P and CC1NP = 0 for rising edge TIM5->SMCR |= TIM_SMCR_SMS; // External clock mode 1 TIM5->SMCR |= TIM_SMCR_TS_0 | TIM_SMCR_TS_2; // Trigger selection: TI1 TIM5->CR1 |= TIM_CR1_CEN; // Enable TIM5 counter while(1) { } }

External Trigger Example:

// Count every fourth press of the blue pushbutton (pin A0 transitioning high.) // The count is stored in TIM2->TIM_CNT, examine it in GDB with "print *(unsigned long*) 0x40000024" // Unfortunately the pushbutton is not debounced in hardware and the software filters are not enough, // so the count will not be exact. #include "stm32f4xx.h" int main() { RCC->AHB1ENR |= RCC_AHB1ENR_GPIOAEN; // Enable GPIOA clock GPIOA->MODER |= GPIO_MODER_MODER0_1; // Enable AF mode for A0 GPIOA->AFR[0] |= 0b0001; // Select AF1 for A0 (TIM1/2) RCC->APB1ENR |= RCC_APB1ENR_TIM2EN; // Enable TIM2 clock TIM2->SMCR |= TIM_SMCR_ETPS_1; // Prescaler = 4 TIM2->SMCR &= ~TIM_SMCR_ETP; // Trigger on rising edge TIM2->SMCR |= TIM_SMCR_ECE; // External clock mode 2 TIM2->CR1 |= TIM_CR1_CEN; // Enable TIM2 counter while(1) { } }

The rest will be covered in Part 2.

STM32F4 Basics: GPIOs

These STM32F4 Basics posts aren't really tutorials so much as they are slightly-organized notes with short and contrived examples. Many aspects and nuances are ignored. Look through the documentation and headers referenced throughout this post to fill in the gaps. The RCC and GPIO chapters in RM0090 would be a good place to start.

Working with registers means there will be lots of bitwise operations. Here's a cheat-sheet:
register |= (1 << bitNumber); // set a bit register &= ~(1 << bitNumber); // clear a bit register ^= (1 << bitNumber); // toggle a bit register & (1 << bitNumber) // check a bit: == 0 if bit was a zero, != 0 if bit was a one

The STM32F407VGT6 has 100 pins, and 80 of them can be used as general-purpose inputs or outputs. The GPIO pins are arranged into 5 channels (a, b, c, d, and e), with 16 pins each. Each channel has a separate clock on the AHB1 bus which must be enabled to use those pins.

Each pin has several qualities that are defined by setting bits in their corresponding registers:

  • Mode: input, output, analog or alternate function (SPI, USB, Timer, etc.)
  • Output type: push-pull or open-drain.
  • Output speed: 2MHz, 25MHz, 50MHz or 100MHz.
  • Pull-up or pull-down: none, pull-up or pull-down.
  • Alternate function low: sets the alternate function for pins 0 – 7.
  • Alternate function high: sets the alternate function for pins 8 – 15.

The reset (default) values for most pins are: input, push-pull, 2MHz, no pull-up/down, AF0. See RM0090 6.4.11 (p.153) for the few exceptions. Good coding practice would involve resetting the GPIO channel if subsequent code expects the reset values.

Note: Analog pins are not 5V tolerant.

Each pin can be used with one of the 15 possible alternate functions shown in RM0090 Figure 14 (p.141):

Any specific pin can only be used with certain alternate functions. See DM00037051 Table 8 (p.58) for a chart detailing what each pin can do. With the Discovery board some of the pins are already used by the on-board components, see UM1472 Table 5 (p.20) for details.

Read or write to each pin with the IO registers:

  • Input data: one bit per pin stores the value.
  • Output data: one bit per pin stores the value.
  • Bit set/reset: Set a bit in the lower half to set the pin, or set a bit in the higher half to clear the pin. This allows for atomic writes to individual pins.

Examples:

// Illuminate the four LEDs around the accelerometer (D12, D13, D14, D15) #include "stm32f4xx.h" int main() { RCC->AHB1RSTR |= RCC_AHB1RSTR_GPIODRST; // Reset GPIOD to ensure reset values exist RCC->AHB1RSTR = 0; // Exit reset state RCC->AHB1ENR |= RCC_AHB1ENR_GPIODEN; // Enable GPIOD clock GPIOD->MODER |= GPIO_MODER_MODER12_0 | GPIO_MODER_MODER13_0 | // Enable output mode for D12-D15 GPIO_MODER_MODER14_0 | GPIO_MODER_MODER15_0; GPIOD->BSRRL = GPIO_BSRR_BS_12 | GPIO_BSRR_BS_13 | // Set D12-D15 high GPIO_BSRR_BS_14 | GPIO_BSRR_BS_15; } // Illuminate the red LED (D14) while the blue pushbutton is held down (while A0 is high) #include "stm32f4xx.h" int main() { RCC->AHB1RSTR |= RCC_AHB1RSTR_GPIOARST | RCC_AHB1RSTR_GPIODRST; // Reset GPIOA and GPIOD RCC->AHB1RSTR = 0; // Exit reset state RCC->AHB1ENR |= RCC_AHB1ENR_GPIOAEN | RCC_AHB1ENR_GPIODEN; // Enable GPIOA and GPIOD clocks GPIOD->MODER |= GPIO_MODER_MODER14_0; // Enable output mode for D14 while(1) { if(GPIOA->IDR & GPIO_IDR_IDR_0 != 0) // If A0 is high GPIOD->BSRRL = GPIO_BSRR_BS_14; // Set D14 high else GPIOD->BSRRH = GPIO_BSRR_BS_14; // Set D14 low } }

The analog and alternate-function modes will be covered in later posts.

Hardware Debugging with GDB and the STM32F4

Debugging small software projects can be done with something as simple as a carefully placed printf() call, but with firmware you have no terminal or GUI to display the feedback. While the STM32F4 and many other MCUs support UARTs or USB, adding that overhead may not be the best option. The STlink utility paired with GDB provides an easy way to debug firmware.

With the development board plugged in, run st-util to open a connection to the board. This will need to remain open for the duration of the debugging process, so use a separate terminal window or hide the output and run the process in the background:

$ st-util $ st-util > /dev/null &

GDB is the actual debugger and it will interact with st-util to communicate with the microcontroller. Start GDB with flags that tell it to hide the warranty notice and connect to the st-util server. If the MCU has already been flashed you can specify the ELF when starting GDB. Otherwise, leave that off and use the load command after starting GDB to upload the firmware:

$ arm-none-eabi-gdb -silent -ex 'target extended-remote localhost:4242' firmware.elf $ arm-none-eabi-gdb -silent -ex 'target extended-remote localhost:4242' (gdb) load firmware.elf

When st-util starts it will establish a connection to the board and pause firmware execution. After starting GDB you can add brakepoints or watchpoints, then continue firmware execution and wait for those points to be triggered. Execution stops when any brakepoint or watchpoint is triggered, allowing you to interactively evaluate expressions, read values and step through code line-by-line.

The GDB User Manual is well written and covers all of the details. Below is a summary of the more commonly used commands when working with microcontrollers.

Brakepoints and Watchpoints
Brakepoints will pause execution before some point in the code. They can brake on a function call or line number. A condition can be used to brake only if some boolean expression evaluates as true (non-zero.) Use break or b to add a breakpoint.

break func1 # brake before all calls to func1() break file.c:func1 # brake before all calls to func1() from file.c break 190 # brake before line 190 of the current file break func1 if var1 == 5 # brake before all calls to func1() when var1 is equal to 5

Watchpoints will pause execution after a variable is read or written to. They are particularly helpful when you do not know what code is altering a value. Use watch to break after writes, rwatch to break after reads, and awatch to break after reads or writes. Watchpoints are automatically deleted when the variable goes out of scope.

watch var1 # brake after var1 is written to rwatch var1 # brake after var1 is read from awatch var1 # brake after var1 is read or written to

When a brakepoint or watchpoint is created it is assigned a number. List them all with info brakepoints. Remove one with delete or clear. Disable one with disable and enable one with enable.

info brakepoints # list all brakepoints and watchpoints with their numbers disable n # disable brakepoint or watchpoint number n enable n # enable brakepoint or watchpoint number n delete n # remove brakepoint or watchpoint number n clear func1 # remove all breakpoints for func1() clear 190 # remove all breakpoints for line 190 of the current file

Automatically run debugger commands when reaching a breakpoint or watchpoint with commands. It will apply to the most recently set breakpoint/watchpoint, or specify a number to apply it to a different point.

break func2 # add breakpoint for func2() commands # debugger commands applied to most recently defined breakpoint print var1 print var2 end commands n # debugger commands applied to breakpoint/watchpoint number n print var3 end

Interactive Commands
Resume execution with continue or c.

Pause execution with Ctrl-C.

Execute one line, without stepping into a function call, with next or n.

Execute one line, stepping into a function call, with step or s.

Print a stacktrace with backtrace or bt.

Evaluate an expression with print or p. It will be displayed in the same datatype as in the code. Flags can be used to change how it's displayed: /t for binary, /x for hexadecimal, etc. See Section 10.5 of the GDB User Manual for a complete list.

print var1 # print the value of var1 print /t *(uint32_t*) 0x40023830 # print the value of the RCC AHB1ENR register as binary

Exit the debugger with Ctrl-D, quit or q.

< Prev  3  Next >