Introduction

Hardware Basics

Project Using STM32G431KB as Example. Test hardware is NUCLEO-G431KB.

STM32CubeMX File Included.

Hardware using jumper pin only to test out all functionalities.

Build This Project

More Docker Details Visit https://github.com/jasonyang-ee/STM32-Dockerfile

  • Auto Docker Build Command

    docker run -rm -v {Your_Local_Full_Path}:/home jasonyangee/stm32-builder:ubuntu-latest https://github.com/jasonyang-ee/STM32G431KB
    

    Run

  • Manual CMake Build Command

    cmake -B build -G Ninja
    cmake --build build
    

Pin Map

Use /CubeMX/CubeMX.ioc for complete info

diagram map

Folder Structure

This project uses HAL. Code generation using CubeMX, and the generated code structure is untouched for /Core /Driver /Middlewares.

The only modification is that main.c and main.h are moved into /Application for better workflow.

The c++ layer is done in /Application. All c++ class object are to represent each device or feature. Then, all objects are decleared globally in Instances.hpp. This will allow controlling class (State Machine or FreeRTOS) to easily access those instances and mutate data.

Serial UART

Serial COM is the base of all debugging and user interface for this project. It is used in interrupt mode only. DMA is not necessary because of my string buffer.

Machnism

A STL string buffer is used to cache all pending to send data. This is gambling on that all data will be send on a fast scheduler to minimize the risk of stack overflow.

This is really convineant that one do not have to worry about data size to exceed TX buffer size.

The receiving buffer size is the only thing must be pre-determined which is easy. That is just your command max length.

In short, both RX and TX buffer size are share defined as below.

SerialCOM.hpp

#ifndef UART_BUFFER
#define UART_BUFFER 64
#endif

A better practice maybe using ETL library where string max size can be defined. But, whatever.

Use of UART Object

  • sendString(std::string) and sendNumber(any_number)to add data to buffer.
  • sendLn() for fast formatting.
  • sendOut() to initiate transmission. Recall this funtion in HAL_UART_TxCpltCallback to send all buffer.
  • Start of the Instance:

Before main():

SerialCOM serialCOM{};

Inside main():

MX_USART2_UART_Init();
serialCOM.setPort(&huart2);
HAL_UARTEx_ReceiveToIdle_IT(&huart2, serialCOM.m_rx_data, UART_BUFFER);

LED

LED is the 2nd debugging interface. Running it in breathing / blink / rapid mode will help you to determine if system is at fault.

And it is fun to stare at LED anyway.

Machnism

LED is controlled by PWM. PWM is controlled by period and duty cycle. period is the time it takes to complete one cycle. duty cycle is the time it takes to complete one cycle in high state.

PWM

  • Prescaler x Overflow = Clock Source = System Clock / Desired PWM Frequency

  • If wanting period be 100 for ease of duty cycle setting:

  • 32Mhz / 1KHz / 100 = 320

  • => prescaler: 320-1, period: 100-1, PWM Frequency: 1KHz

  • If wanting max precision with using max period:

  • 32Mhz / 1KHz / (if > 65535) then divide 65535 (16bit period)

  • => prescaler: 1-1, period: 32000-1, PWM Frequency: 1KHz

  • Use clock source: internal clock (APBx)

Interrupt Via PWM OC (output compare) signal

  • Enable PWM global interrupt
  • Run HAL_TIM_PWM_Start_IT(&htimX, TIM_CHANNEL_X) in main()
  • Define void PWM_PulseFinishedCallback(TIM_HandleTypeDef *htim)
  • Run led_user.scheduler() in the PWM_PulseFinishedCallback

LED Class

  • Init object LED led_user{period, dimmer, Interrupt Frequency}
  • Passthogh channel CCR with led_user.setCCR(&htimX.Instance->CCRX);
  • Ready to use on(), off(), toggle(), set(), breath(), blink(), rapid().

CLI

I am using lwshell. Credit to https://github.com/MaJerle/lwshell

Machnism

  • Direct usage without FreeRTOS:

    • Inside HAL_UARTEx_RxEventCallback():
      cli.setSize(Size);
      cli.parse();
      
  • Start of the Instance:

    • Before main():

      CLI cli{};
      
    • Inside main():

      cli.init();
      

C++ Wrapper

Additionally, I made a c++ wrapper class to use lwshell.

This is how you do all the functions and use this hardware as playground. All playground feature should be accessed via this Command Line Interface.

CLI input and output using UART (SerialCOM) as described above.

To expend commands:

  1. Add a static function that returns its run result

    static int32_t led(int32_t, char**);
    
  2. Register this function to be called when command is parsed in the RX.

    lwshell_register_cmd("led", &CLI::led, NULL);
    
  3. Complete your command function.

    REMEMBER TO return int32_t; ELSE SYSTEM WILL GET STUCK.

Customization

Finally, we can populate our static custom command functions.

You will need to define the command and register it in init().

Example Command with Sub Commands:

  • Inside init():

    lwshell_register_cmd("led", &CLI::led, NULL);
    
  • Function Define:

    static int32_t CLI::led(int32_t argc, char** argv) {
    	const char* led_help =
    		"\nLED Functions:\n"
    		"  on\tTurns ON LED\n"
    		"  off\tTurns OFF LED\n"
    		"  breath\tLED in breath effect mode\n"
    		"  blink\tLED in slow blink mode\n"
    		"  rapid\tLED in fast blink mode\n\n";
    
    	if (!strcmp(argv[1], "help")) serialCOM.sendString(led_help);
    	if (!strcmp(argv[1], "on")) led_user.on();
    	if (!strcmp(argv[1], "off")) led_user.off();
    	if (!strcmp(argv[1], "breath")) led_user.breath();
    	if (!strcmp(argv[1], "blink")) led_user.blink();
    	if (!strcmp(argv[1], "rapid")) led_user.rapid();
    	return 0;
    }
    

FreeRTOS

C++ Wrapper

I made a c++ wrapper class to use FreeRTOS. That's the main idea here.

To expend your task:

  1. Define the task handler and function
    TaskHandle_t parse_Handle;
    void parse();
    
  2. Use lamda to create the task
    auto t1 = [](void *arg) { static_cast<Thread *>(arg)->parse(); };
    xTaskCreate(t1, "cli parsing", 256, this, -2, &parse_Handle);
    
  3. Trap the task into its own tiny while() like the one in your main()
    void Thread::parse() {
    	while (1) {
    		vTaskSuspend(NULL);
    		cli.parse();
    	}
    }
    
  4. Use task control (i.e. queue, notify, suspend, resume) to run your app and feature.
    xTaskResumeFromISR(thread.parse_Handle);
    

Use of FreeRTOS Wrapper Class

The task are created in the object initialization using Lamda due to C++ wrapping.

  • In object initializer:
    auto t1 = [](void *arg) { static_cast<Thread *>(arg)->parse(); };
    xTaskCreate(t1, "cli parsing", 256, this, -2, &parse_Handle);
    

Each of the corresponding task are decleared with its handle typle.

  • In header:
    TaskHandle_t parse_Handle;
    void parse();
    

FreeRTOS Debugging

  • Install VS Code Extension: RTOS Views (mcu-debug.rtos-views)
  • Use a hardware timer with 20KHz setting. In this projcet timer 7 is used.
  • Include the follwing code to enable full debugging feature.

In FreeRTOSConfig.h:

#include "tim.h"					// Using 20kHz hardware timer
#define configUSE_TRACE_FACILITY 1			// Show RTOS task ID stats
#define configUSE_STATS_FORMATTING_FUNCTIONS 1		// Enable using vTaskList()
#define configRECORD_STACK_HIGH_ADDRESS 1		// Show RTOS stack stats
#define configGENERATE_RUN_TIME_STATS 1
#define portCONFIGURE_TIMER_FOR_RUN_TIME_STATS() MX_TIM7_Init()
#define portGET_RUN_TIME_COUNTER_VALUE() htim7.Instance->CNT

In MX_TIM7_Init():

/* USER CODE BEGIN TIM7_Init 2 */
__TIM7_CLK_ENABLE();
HAL_TIM_Base_Init(&htim7);
HAL_TIM_Base_Start(&htim7);

State Machine

Flash

The goal is to save and load user configurations for application. Writting from the back of the flash memory.

Porting to different hardware requires user to edit Flash.hpp and Flash.cpp

Refer to the Reference Manual and find out the end of flash address. Then, update those two variable.

// G432KB has 128Kb of flash
// Same as (0x0803FFFF + 1 - 0x8); if using datasheet provided value
const uint32_t m_address_end{0x08040000 - 0x8};
const uint8_t m_page_total{127};

The configuration data are packaged into a struct and union aligned to uint64_t array for double word HAL flash writing function. The pack and unpack is done manually by user.

You will need to edit this section to fit your application needs.

// Edit config_arr_size and Config content.
#define config_arr_size 2
union Config_Arr {
	struct Config {
		uint32_t led_level;
		uint32_t led_scale;
	} config;

	uint64_t config_arr[config_arr_size];
};

In the public function Save() and Load(), you will need to manually pack and unpack your configuration to/from each of your instances.

Then, you will call the private function Write() and Read() to do flash access.

Below example saves and loads two configuration from LED usage.

void Flash::Save() {
    Flash::Config_Arr current_config{};
    current_config.config.led_level = led_user.getLevel();
    current_config.config.led_scale = led_user.getScale();
    Write(current_config.config_arr, config_arr_size);
}

void Flash::Load() {
    Flash::Config_Arr loaded_config{};
    Read(&loaded_config, config_arr_size);
	led_user.setLevel(loaded_config.config.led_level);
    led_user.setScale(loaded_config.config.led_scale);
}

The memory save example:

DAC

DAC using interrupt mode as of now.

Two function is required to run with HAL:

  • HAL_DAC_Start(m_port, m_channel);
  • HAL_DAC_SetValue(m_port, m_channel, m_alignment, count_value);

HAL_DAC_Start() will call HAL_Delay(), and if used before vTaskStartScheduler(), then it will mess up with FreeRTOS causing thread to stuck in loop.

Solutin is to move HAL_DAC_Start() into setState(). But, this likey will cause minor delay in runtime since every change of DAC value will call HAL_DAC_Start() again.

Later we should implement a DMA or RTOS init() thread to be run once on system boot.

ADC

Author