Chace's Tech Blog

Reflections on my Programming Adventure

Code Walkthrough, Firmware running the Chacebot Microcontroller

21 Jun 2020

Chacebot uses an STM32F103C8T6 microcontroller to interpret PWM signals from a three channel RC receiver and streams the values over the serial port to control the drivetrain of the robot. This post contains an overview of the hardware setup and explains the firmware I implemented to make this embedded system work efficiently. The code I developed in this project is an extension from a chapter in Warren Gay’s book : Beginning STM32: Developing with FreeRTOS, libopencm3 and GCC, specifically Chapter 17 titled “PWM Input with Timer 4”.

Background

The reason why I purchased Warren’s book was so that I can learn more about embedded systems and implement this RC receiver interpreting functionality in a more elegant way than the lower hanging fruit alternative which was to use an Arduino. Using the arduino for this would have been very easy because the arduino platform has built in functions that allow you pick an analog input pin and will set up the relevant timers and scale the length of the pulse under the hood without having to write all of the code to do so. However this is not how this system would be deployed in industry because the Arduino platform is not scalable and it does not allow for multitasking. Everything in Arduino is synchronous meaning it only does one task at a time, therefore there is the possibility of missing input signals since it cannot monitor all RC input pins at the same time. This is where FreeRTOS comes in. Through Warren’s book I was able to learn how to implement preemptive multitasking which I will just refer to as a “task”. This allows for me to set up modular routines or “jobs” that can be initiated and run at the discretion of the microprocessor scheduler which is very efficient. Running multiple jobs this way does not always mean that they are done in parallel, however it does enable asynchronous behavior.

Chacebot RC Control Setup

Above, the chacebot STM32 setup is shown. This includes the off the shelf RC transmitter and reciever combo ($30 from amazon) which is used for any typical RC hobby application. The RC Receiver (little black box with an antenna) is mounted via VHB foam tape to a breadboard containing the STM32F103 microcontroller. The STM32 is wired to the RC receiver using jumper wires running Vcc (+5v), GND, and connections between channel 1, 2, 3 of the RC receiver to pins PB6, PA6, and PA0 respectively. The pinout diagram for the STM32 board below indicates the physical location of these pins:

STM32 Pinout

The code that follows in this post can be found in the Chacebot Firmware Github Repo and can be built and used by following the instructions at Warren Gay’s Tutorial Repo for the STM32. The only project in my repo is the RC controller and it can be found in this directory:

chacebot_firmware/ |- README.md |- libopencm3 |- FreeRTOSv10.0.1 |- libwwg |-rtos |- chacebot_rc_reciever/ Project Source Code Directory | |- main.c Main file where all the magic happens

Code

Starting with a top down approach, we will first look at the main function of the code and then dig into the details of what it contains. Here is what the main function looks like:

 
int main(void)
{
 
 rcc_clock_setup_in_hse_8mhz_out_72mhz();
 
 rcc_periph_clock_enable(RCC_GPIOC);
 gpio_set_mode(GPIOC, GPIO_MODE_OUTPUT_2_MHZ, GPIO_CNF_OUTPUT_PUSHPULL,
               GPIO13);
 gpio_clear(GPIOC, GPIO13); // LED off
 
 usb_start(1, 1);
 std_set_device(mcu_usb); // Use USB for std I/O
 
 xTaskCreate(task1, "steering", 100, NULL, 1, NULL);
 xTaskCreate(task2, "drive", 100, NULL, 1, NULL);
 xTaskCreate(task3, "status", 100, NULL, 1, NULL);
 xTaskCreate(task4, "send", 100, NULL, 1, NULL);
 vTaskStartScheduler();
 for (;;)
   ;
 return 0;
}

First we set up the main system clock speed of 72 MHz on the STM32 and then set up the built in LED. Next the usb interface is initialized. This interface is provided by Warren Gay which is very easy to use and allows us to send data from the board to a computer over the serial port. Finally there is the part where my code comes in (with inspiration from Mr. Gay). I start 4 tasks using the FreeRTOS preemptive scheduling facilities by calling the function “xTaskCreate(task code, task name, NULL, task priority, NULL). The two NULL pointers are to use the function without pvParameter and pxCreatedTask functionality which are optional. The first three tasks are pretty much identical and are used to set up and execute the timers to monitor PWM signals on three specific pins using their own timers. The last task is used to package up the pwm values stored in a stack variable and send them over the serial port. Lastly this main function calls “vTaskStartScheduler()” which initiates the tasks to run on the STM32. Each task has their own infinite loop just like the one seen at the end of this main function so that the program continues to interpret PWM data and stream it over the serial port forever. The reason for the redundant infinite loop in the main function allows for the program to never exit. Now lets take a look under the hood of one of the PWM timer tasks:

static void task1(void *args __attribute__((unused)))
{ //Steering Task
 
 rcc_periph_clock_enable(RCC_TIM4); // Need TIM4 clock
 
 // PB6 == TIM4.CH1
 rcc_periph_clock_enable(RCC_GPIOB);         // Need GPIOB clock
 gpio_set_mode(GPIOB, GPIO_MODE_INPUT,       // Input
               GPIO_CNF_INPUT_FLOAT, GPIO6); // PB6=TIM4.CH1
 
 // TIM4:
 timer_disable_counter(TIM4);
 rcc_periph_reset_pulse(RST_TIM4);
 nvic_set_priority(NVIC_DMA1_CHANNEL3_IRQ, 2);
 nvic_enable_irq(NVIC_TIM4_IRQ);
 timer_set_mode(TIM4, TIM_CR1_CKD_CK_INT, TIM_CR1_CMS_EDGE, TIM_CR1_DIR_UP);
 timer_set_prescaler(TIM4, 72 - 1);
 
 timer_ic_set_input(TIM4, TIM_IC2, TIM_IC_IN_TI1);
 timer_ic_set_filter(TIM4, TIM_IC_IN_TI1, TIM_IC_CK_INT_N_2);
 
 timer_slave_set_mode(TIM4, TIM_SMCR_SMS_RM);
 timer_slave_set_trigger(TIM4, TIM_SMCR_TS_TI1FP1);
 TIM_CCER(TIM4) &= ~(TIM_CCER_CC2P | TIM_CCER_CC2E);
 TIM_CCER(TIM4) |= TIM_CCER_CC2P | TIM_CCER_CC2E;
 timer_ic_enable(TIM4, TIM_IC2);
 timer_enable_irq(TIM4, TIM_DIER_CC2IE);
 timer_enable_counter(TIM4);
 
 for (;;)
 {
   vTaskDelay(20);
 }
}

There is a lot to swallow here but it is all mainly initializer stuff to support the timer and to configure how it scales the output to return the results we want. The STM32 has 4 general purpose timers and we are using three of them, one for each channel of the RC receiver. Channel one (steering) is wired to the timer in the example shown above which is timer 4 on pin PB6. Channel two (drive) and channel three (safety switch) are wired to timer 3 on pin PA6 and timer 2 on pin PA0 respectively. The “B” in PB6 stands for peripheral bus “B” which needs to be activated before any of the GPIO functionality on that bus is used. This is done with the call to rcc_periph_clock_enable(RCC_GPIOB) which starts the clock for that bus. You’ll also notice that this same method was called above it but with the argument of “RCC_TIM4” which sets another clock just for the timer. Embedded systems are super specific and if everything is not configured properly then nothing will work. It is also very tedious since there is such granularity to what has to be configured. After the clocks are enabled then the specific pin GPIO6 on the GPIOB bus is initialized as an input pin to be used by the timer.

After everything is initialized for this channel, the timer is further configured to start counting when the PWM signal rises and counts until it falls again. This length is the “Pulse Width” in Pulse Width Modulation (PWM). The timer counts in units of the system clock intervals and then needs to be converted to usable time (ms) which is done by calling timer_set_prescaler(TIM4, 72 - 1). The rest of the function calls are used to further configure how the timer handles noise with a filter and configures the interrupt service routine functionality that works kind of like a callback function to store the data in a stack variable. Lastly this task, just like all the others, has an infinite loop so that it keeps timing the length of PWM pulses for as long as the STM32 is powered. Next we will look into the interrupt service routine and the variable it stores our valuable RC controller command data in:

static volatile uint32_t STEERING = 0, DRIVE = 0, ENABLE = 0;
 
void tim4_isr(void)
{
 uint32_t sr = TIM_SR(TIM4);
 
 if (sr & TIM_SR_CC2IF)
 {
   STEERING = TIM_CCR2(TIM4);
   timer_clear_flag(TIM4, TIM_SR_CC2IF);
 }
}

Starting from the top, three unsigned (positive only) 32 bit integers are declared to zero, one for each of the RC channels. Then the timer 4 interrupt service routine is defined as void because it never returns anything. Instead it is called through the timer tasks, stores the appropriate channel’s PWM signal data into the corresponding variable, and then clears the timer flag so that it can start timing again. This is pretty simple right? For each channel we have a task that runs infinitely that records the amount of time passed since the last voltage spike on the pin until the voltage drops again and then stores that value into a variable which we will then send over the serial port to a computer to be used modularly to provide input to any application. Let’s take a look at the final piece of the puzzle which is the task for sending this data over the serial port:

static void task4(void *args __attribute__((unused)))
{ //Sending Task
 
 gpio_set(GPIOC, GPIO13); // LED on
 
 for (;;)
 {
   vTaskDelay(20);
   std_printf("<%u,%u,%u>\r\n", (unsigned)STEERING, (unsigned)DRIVE,
              (unsigned)ENABLE);
   if (ENABLE > 1500)
     gpio_clear(GPIOC, GPIO13); // LED on
 
   else if (ENABLE < 1500)
     gpio_set(GPIOC, GPIO13);
 }
}

When task4 is kicked off in the main function, the task initializes the board built in LED to shine by setting the pin to high with “gpio_set(GPIOC, GPIO13)”. Then the task enters it’s infinite loop that continuously writes to the serial buffer with the <STEERING,DRIVE,ENABLE> format. This print call includes the special “\r” and “\n” characters to indicate the end of the message and to write to a new line the next cycle. Lastly there is added functionality here that controls the LED indicator depending on the status of the ENABLE status channel value. If ENABLE (button clicked on controller) is above 1500 (ms) it will turn the LED on and if under 1500 will turn it off.

Thank you for reading and please let me know if you have any suggestions to how I could make improvements!

-Chace