Developing a Debugging Tool for Microcontrollers and Discussing Its Programming Approach

1. Overview

When writing code for STM32 microcontrollers, we often encounter situations where a specific function or variable needs to be debugged repeatedly. The common method is to modify the source code and download it to the microcontroller for debugging. This repetitive process is not only cumbersome but also affects the FLASH memory of the microcontroller due to frequent reprogramming. Therefore, we consider developing a small tool that can achieve the following:1) Control the microcontroller to execute the desired function via serial port, supporting up to 5 parameters of types char, short, int, float, and their unsigned types, along with corresponding pointers. Long and double types are not supported.2) For functions that involve string and array operations, the variable address must be written when calling the function after passing the array value to manipulate these variables. The function return value will also be displayed.3) Support arbitrary modifications to global variables.4) Support switching between decimal and hexadecimal.5) Automatic retransmission or closure of the serial port in case of communication timeout.It is recommended to use this tool in conjunction with KEIL for better results.This software is written in C# and runs in the .NET 4.5 environment.Let’s first take a look at the results; if you are interested, you can continue reading:1. Host Debugging SettingsDeveloping a Debugging Tool for Microcontrollers and Discussing Its Programming Approach2. Function CallDeveloping a Debugging Tool for Microcontrollers and Discussing Its Programming Approach3. Writing Global VariablesDeveloping a Debugging Tool for Microcontrollers and Discussing Its Programming Approach4. Communication Timeout HandlingDeveloping a Debugging Tool for Microcontrollers and Discussing Its Programming Approach2. Host Processing2.1 PrincipleAfter compiling STM32 with KEIL, we will find a .map file in the same folder as the .hex file. This .map file contains information about the addresses, sizes, and optimizations of functions and global variables in the source code. Here is a simplified .map file for reference:

Component: ARM Compiler 5.06 update 6 (build 750) Tool: armlink [4d35ed]

==============================================================================

Section Cross References

    startup_stm32f103xe.o(STACK) refers (Special) to heapauxi.o(.text) for __use_two_region_memory
    startup_stm32f103xe.o(HEAP) refers (Special) to heapauxi.o(.text) for __use_two_region_memory
    startup_stm32f103xe.o(RESET) refers (Special) to heapauxi.o(.text) for __use_two_region_memory
    startup_stm32f103xe.o(RESET) refers to startup_stm32f103xe.o(STACK) for __initial_sp

==============================================================================

Removing Unused input sections from the image.

    Removing main.o(.rev16_text), (4 bytes).
    Removing main.o(.revsh_text), (4 bytes).
    Removing main.o(.rrx_text), (6 bytes).
    Removing gpio.o(.rev16_text), (4 bytes).
    Removing gpio.o(.revsh_text), (4 bytes).

384 unused section(s) (total 34104 bytes) removed from the image.

==============================================================================

Image Symbol Table

    Local Symbols

    Symbol Name                              Value     Ov Type        Size  Object(Section)

    ../Core/Src/gpio.c                       0x00000000   Number         0  gpio.o ABSOLUTE
    ../Core/Src/main.c                       0x00000000   Number         0  main.o ABSOLUTE
    ../Core/Src/stm32f1xx_hal_msp.c          0x00000000   Number         0  stm32f1xx_hal_msp.o ABSOLUTE
    ../Core/Src/stm32f1xx_it.c               0x00000000   Number         0  stm32f1xx_it.o ABSOLUTE
    ../Core/Src/system_stm32f1xx.c           0x00000000   Number         0  system_stm32f1xx.o ABSOLUTE
    ../Core/Src/tim.c                        0x00000000   Number         0  tim.o ABSOLUTE
    ../Core/Src/usart.c                      0x00000000   Number         0  usart.o ABSOLUTE
    ../Drivers/STM32F1xx_HAL_Driver/Src/stm32f1xx_hal.c 0x00000000   Number         0  stm32f1xx_hal.o ABSOLUTE
    ../Drivers/STM32F1xx_HAL_Driver/Src/stm32f1xx_hal_cortex.c 0x00000000   Number         0  stm32f1xx_hal_cortex.o ABSOLUTE
    ../Drivers/STM32F1xx_HAL_Driver/Src/stm32f1xx_hal_dma.c 0x00000000   Number         0  stm32f1xx_hal_dma.o ABSOLUTE
    ../Drivers/STM32F1xx_HAL_Driver/Src/stm32f1xx_hal_exti.c 0x00000000   Number         0  stm32f1xx_hal_exti.o ABSOLUTE

    Global Symbols

    Symbol Name                              Value     Ov Type        Size  Object(Section)

    BuildAttributes$THM_ISAv4$P$D$K$B$S$PE$A:L22UL41UL21$X:L11$S22US41US21$IEEE1$IW$USESV6$~STKCKD$USESV7$~SHL$OSPACE$ROPI$EBA8$UX$STANDARDLIB$REQ8$PRES8$EABIv2 0x00000000   Number         0  anon$obj.o ABSOLUTE
    __ARM_use_no_argv                        0x00000000   Number         0  main.o ABSOLUTE
    __ARM_exceptions_init                     - Undefined Weak Reference
    __alloca_initialize                       - Undefined Weak Reference
    __arm_preinit_                            - Undefined Weak Reference
    __cpp_initialize__aeabi_                  - Undefined Weak Reference
    _terminate_alloc                          - Undefined Weak Reference
    _terminate_user_alloc                     - Undefined Weak Reference
    _terminateio                              - Undefined Weak Reference
    __Vectors_Size                           0x00000130   Number         0  startup_stm32f103xe.o ABSOLUTE
    __Vectors                                0x08000000   Data           4  startup_stm32f103xe.o(RESET)
    __Vectors_End                            0x08000130   Data           0  startup_stm32f103xe.o(RESET)
    __main                                   0x08000131   Thumb Code     8  __main.o(!!!main)
    in                                       0x2000001c   Data           4  main.o(.data)
    uin                                      0x20000020   Data           4  main.o(.data)
    uwTick                                   0x20000024   Data           4  stm32f1xx_hal.o(.data)
    uwTickPrio                               0x20000028   Data           4  stm32f1xx_hal.o(.data)
    uwTickFreq                               0x2000002c   Data           1  stm32f1xx_hal.o(.data)

==============================================================================

Memory Map of the image

  Image Entry point : 0x08000131

  Load Region LR_IROM1 (Base: 0x08000000, Size: 0x00002de8, Max: 0x00080000, ABSOLUTE, COMPRESSED[0x00002da8])

    Execution Region ER_IROM1 (Exec base: 0x08000000, Load base: 0x08000000, Size: 0x00002b94, Max: 0x00080000, ABSOLUTE)

    Exec Addr    Load Addr    Size         Type   Attr      Idx    E Section Name        Object

    0x08000000   0x08000000   0x00000130   Data   RO            3    RESET               startup_stm32f103xe.o
    0x08000130   0x08000130   0x00000008   Code   RO         2955  * !!!main             c_w.l(__main.o)
    0x08000138   0x08000138   0x00000034   Code   RO         3143    !!!scatter          c_w.l(__scatter.o)
    0x0800016c   0x0800016c   0x0000003a   Code   RO         3141    !!dczerorl          c_w.l(__dczerorl.o)
    0x080001a6   0x080001a6   0x00000002   PAD
    0x080001a8   0x080001a8   0x0000001c   Code   RO         3145    !!handler_zi        c_w.l(__scatter_zi.o)

    Execution Region RW_IRAM1 (Exec base: 0x20000000, Load base: 0x08002b94, Size: 0x00008bb0, Max: 0x00010000, ABSOLUTE, COMPRESSED[0x00000214])

    Exec Addr    Load Addr    Size         Type   Attr      Idx    E Section Name        Object

    0x20000000   COMPRESSED   0x00000010   Data   RW           18    .data               test.o
    0x20000010   COMPRESSED   0x00000014   Data   RW           78    .data               main.o
    0x20000024   COMPRESSED   0x00000009   Data   RW         1481    .data               stm32f1xx_hal.o
    0x2000002d   COMPRESSED   0x00000003   PAD
    0x20000030   COMPRESSED   0x00000004   Data   RW         2832    .data               system_stm32f1xx.o
    0x20000034   COMPRESSED   0x00000004   PAD
    0x20000038   COMPRESSED   0x0000021c   Data   RW         2910    .data               debug_revice.o

==============================================================================

Image component sizes

      Code (inc. data)   RO Data    RW Data    ZI Data      Debug   Object Name

       172          6          0          0          0       3002   debug_function.o
       580         98          0        540       2104       3763   debug_revice.o
        36          4          0          0          0        767   gpio.o
       288         24          0         20         50     486558   main.o
        64         26        304          0      32768        820   startup_stm32f103xe.o
       152         32          0          9          0       5977   stm32f1xx_hal.o
       304         22          0          0          0      29503   stm32f1xx_hal_cortex.o
       510         10          0          0          0       1927   stm32f1xx_hal_dma.o
       832         40          0          0          0       2092   stm32f1xx_hal_gpio.o
        84          8          0          0          0        918   stm32f1xx_hal_msp.o
      1784        110          0          0          0       6112   stm32f1xx_hal_rcc.o
      1260         44          0          0          0       9974   stm32f1xx_hal_tim.o
       160         22          0          0          0       2453   stm32f1xx_hal_tim_ex.o
      1844         10          0          0          0      11460   stm32f1xx_hal_uart.o
        66         12          0          0          0       4980   stm32f1xx_it.o
         2          0         24          4          0       1155   system_stm32f1xx.o
       134         10          0         16          0       6385   test.o
       192         18          0          0         72       1702   tim.o
       220         26          0          0         68       1778   usart.o

    ----------------------------------------------------------------------
      8702        522        362        596      35068     581326   Object Totals
         0          0         32          0          0          0   (incl. Generated)
        18          0          2          7          6          0   (incl. Padding)

    ----------------------------------------------------------------------

      Code (inc. data)   RO Data    RW Data    ZI Data      Debug   Library Member Name

        58          0          0          0          0          0   __dczerorl.o
         8          0          0          0          0         68   __main.o
         0          0          0          0          0          0   __rtentry.o
        12          0          0          0          0          0   __rtentry2.o
         6          0          0          0          0          0   __rtentry4.o
        52          8          0          0          0          0   __scatter.o
        28          0          0          0          0          0   __scatter_zi.o
        18          0          0          0          0         80   exit.o
         6          0          0          0          0        152   heapauxi.o
         0          0          0          0          0          0   indicate_semi.o
         2          0          0          0          0          0   libinit.o
         2          0          0          0          0          0   libinit2.o
         2          0          0          0          0          0   libshutdown.o
         2          0          0          0          0          0   libshutdown2.o
         8          4          0          0         96         68   libspace.o
        78          0          0          0          0         80   rt_memclr_w.o
         2          0          0          0          0          0   rtexit.o
        10          0          0          0          0          0   rtexit2.o
        12          4          0          0          0         68   sys_exit.o
        74          0          0          0          0         80   sys_stackheap_outer.o
         2          0          0          0          0         68   use_no_semi.o
       804         16          0          0          0        272   daddsub_clz.o
        90          4          0          0          0         92   dfixu.o
       156          4          0          0          0         92   dnaninf.o
        12          0          0          0          0         68   dretinf.o
       430          8          0          0          0        168   faddsub_clz.o
        62          4          0          0          0         84   ffixu.o
       140          4          0          0          0         84   fnaninf.o
        10          0          0          0          0         68   fretinf.o
         0          0          0          0          0          0   usenofp.o

    ----------------------------------------------------------------------
      2092         56          0          0         96       1592   Library Totals
         6          0          0          0          0          0   (incl. Padding)

    ----------------------------------------------------------------------

      Code (inc. data)   RO Data    RW Data    ZI Data      Debug   Library Name

       382         16          0          0         96        664   c_w.l
      1704         40          0          0          0        928   fz_ws.l

    ----------------------------------------------------------------------
      2092         56          0          0         96       1592   Library Totals

    ----------------------------------------------------------------------

==============================================================================

      Code (inc. data)   RO Data    RW Data    ZI Data      Debug   

     10794        578        362        596      35164     577922   Grand Totals
     10794        578        362        532      35164     577922   ELF Image Totals (compressed)
     10794        578        362        532          0          0   ROM Totals

==============================================================================

    Total RO  Size (Code + RO Data)                11156 (  10.89kB)
    Total RW  Size (RW Data + ZI Data)             35760 (  34.92kB)
    Total ROM Size (Code + RO Data + RW Data)      11688 (  11.41kB)

By closely observing, we can find that the .map file mainly consists of the following parts:

Component: ARM Compiler 5.06 update 6 (build 750) Tool: armlink [4d35ed]

==============================================================================

Section Cross References

==============================================================================

Removing Unused input sections from the image.

==============================================================================

Image Symbol Table

    Local Symbols

    Symbol Name                              Value     Ov Type        Size  Object(Section)
        
    Global Symbols

    Symbol Name                              Value     Ov Type        Size  Object(Section)
        

==============================================================================

Memory Map of the image

==============================================================================

Image component sizes

==============================================================================

      Code (inc. data)   RO Data    RW Data    ZI Data      Debug   

==============================================================================

    Total RO  Size (Code + RO Data)                
    Total RW  Size (RW Data + ZI Data)             
    Total ROM Size (Code + RO Data + RW Data)      

Our main concern is the information such as the addresses and sizes of functions and global variables, which can be found in the .map file under Image Symbol Table -> Global Symbols. Knowing these addresses, we only need to send the addresses of the functions and variables of interest to the microcontroller, which can then execute the corresponding functions via pointers. The entire host is developed based on this principle. The specific process is illustrated in the following diagram:

Developing a Debugging Tool for Microcontrollers and Discussing Its Programming Approach

2.2 Implementation of class Get_Map_Address_And_Size_Table — Extracting Addresses and Sizes of Functions and Global Variables from .map

The addresses and sizes of functions and global variables are located in the .map file under Image Symbol Table -> Global Symbols, consisting of Symbol Name, Value, Ov Type, Size, and Object(Section). Therefore, we first define a public struct Symbol to contain the above information:

public struct Symbol
{
   public String Symbol_Name;
   public uint Symbol_Address;
   public SymbolType Symbol_Type;
   public ushort Symbol_Size;
   public String Symbol_Section;
};

Next, we will use FileStream to obtain information from the .map file, locate it to Image Symbol Table -> Global Symbols, and read Symbol Name, Value, Ov Type, Size, and Object(Section) to assign values to symbol_table:

public void Create_Address_And_Size_Table(String filename){
    try
    {
        uint i;

        FileStream file_read = new FileStream(filename, FileMode.Open, FileAccess.Read);//Create a new file stream

        filelist = File.ReadAllLines(filename, Encoding.Default);//Read all lines of the file content into a string array.

        for (i = 0; i <= filelist.Length - 1; i++)              //Locate the interested position
        {
             if (filelist[i].Contains("Global Symbols"))
            {
               break;
            }
         }

        for (uint j = i; j <= filelist.Length - 1; j++)
       {
          if (filelist[j].Contains("Object(Section)"))
         {
            i = j + 1;
            break;
         }
       }

        if (i < filelist.Length - 1)                                //Get information
        {
            //Table_DeInit();
            Get_Symbol_Data(i);
         }

         file_read.Close();

    }
    catch (Exception ex)
    {
        MessageBox.Show(ex.Message);
     }
}

Get_Symbol_Data(i); is responsible for assigning Symbol Name, Value, Ov Type, Size, and Object(Section) from Image Symbol Table -> Global Symbols to symbol_table. Two points need to be noted:

1) In Global Symbols,

xxxxxx - Undefined Weak Reference

does not contain useful information and needs to be excluded, which can be done using the Contains(“- Undefined Weak Reference”) method.

2) The Symbol Name, Value, Ov Type, Size, and Object(Section) in Image Symbol Table -> Global Symbols are separated by spaces, so we can use

Split(new Char[] { ' ' }, StringSplitOptions.RemoveEmptyEntries);

to obtain the data set.

The function void Get_Symbol_Data(uint index) is as follows:

private void Get_Symbol_Data(uint index)
{
     table_length = 0;

     while (index <= filelist.Length - 1)
    {
        if (filelist[index].Equals(""))
        {
           index++;
           continue;
         }

        if(filelist[index].Contains("=") == false)
        {
            if (filelist[index].Contains("- Undefined Weak Reference"))  //Exclude - Undefined Weak Reference
           {
               index++;
               continue;
           }
          else
          {
                int str_index = 0;
                string[] split_str = filelist[index].Split(new Char[] { ' ' }, StringSplitOptions.RemoveEmptyEntries);//Get data set
                        
                symbol_table[table_length].Symbol_Name = split_str[str_index];
                str_index++;
                symbol_table[table_length].Symbol_Address = Convert.ToUInt32(split_str[str_index], 16);
                str_index++;
                if (split_str[str_index].Equals("Thumb"))
               {
                    symbol_table[table_length].Symbol_Type = SymbolType.Thumb_Code;
                }
               else if (split_str[str_index].Equals("Section"))
              {
                   symbol_table[table_length].Symbol_Type = SymbolType.Section;
              }
              else if (split_str[str_index].Equals("Number"))
             {
                    symbol_table[table_length].Symbol_Type = SymbolType.Number;
               }
              else if (split_str[str_index].Equals("Data"))
             {
                    symbol_table[table_length].Symbol_Type = SymbolType.Data;
              }
             str_index++;
             if (split_str[str_index].Equals("Code"))
              {
                    str_index++;
                    symbol_table[table_length].Symbol_Size = Convert.ToUInt16(split_str[str_index], 10);
                    str_index++;
                    symbol_table[table_length].Symbol_Section = split_str[str_index];
               }
              else
              {
                    symbol_table[table_length].Symbol_Size = Convert.ToUInt16(split_str[str_index], 10);
                    str_index++;
                    symbol_table[table_length].Symbol_Section = split_str[str_index];
               }
                        

              table_length = table_length + 1;

              index++;

              if (table_length >= table_len)
              {
                   break;
               }
          }
      else
      {
           break;
      }
}

The above is the main implementation method of class Get_Map_Address_And_Size_Table. With these two methods, we can obtain information about functions and global variables in the .map file.

2.3 Implementation of class Get_Function_Address_And_Size_Table — Obtaining the List of Required Functions

After obtaining the symbol_table containing information about functions and global variables, we need to get the list of functions we are interested in. In this host, the user needs to create a .function file. This file contains the list of functions that the user needs to debug. Generally, it is sufficient to directly copy the function declarations from the .h file. The host will then retrieve the function names, parameters, return types, and other parameters from this list, and finally query the function in symbol_table to obtain its address. This is the goal of class Get_Function_Address_And_Size_Table. In class Get_Function_Address_And_Size_Table, we first define:

public struct Function
{
      public String Function_List_Name;
      public String Function_Name;
      public uint Function_Address;
      public String Function_Parameter1;
      public String Function_Parameter2;
      public String Function_Parameter3;
      public String Function_Parameter4;
      public String Function_Parameter5;
      public String Function_Return;
      public uint Function_Parameter_Number;
  };

to conveniently store the information of the functions to be debugged. It is important to note that since structs in C# cannot directly define a fixed-length array like in C, we use the cumbersome method of defining five function parameter information as Function_ParameterX.

The most important function in class Get_Function_Address_And_Size_Table is void Get_Need_Function_Table(). This function retrieves the function list from the .function file and parses the function names, parameters, return types, and other parameters, assigning them to function_table.

private void Get_Need_Function_Table()
{
    uint index = 0;

    for (index = 0; index < table_length; index++)
   {
        string[] split_str = filelist[index].Split(new Char[] { ' ' }, StringSplitOptions.RemoveEmptyEntries);
        uint str_index = 0;
                                
        function_table[index].Function_List_Name = filelist[index];                                        
        if (split_str[str_index].Equals("unsigned") || split_str[str_index].Equals("signed"))             //Function_Return
        {
             function_table[index].Function_Return = split_str[str_index] + " " + split_str[str_index + 1];
             str_index = str_index + 2;
         }
         else
        {
             function_table[index].Function_Return = split_str[str_index];
             str_index++;
          }
                                
         if(split_str[str_index].Equals("*"))
         {
             function_table[index].Function_Return = function_table[index].Function_Return + split_str[str_index];
             str_index++;
         }

         if (split_str[str_index].Contains("*"))                                     //Function_Name 
        {
            function_table[index].Function_Return = function_table[index].Function_Return + "*";
            function_table[index].Function_Name = split_str[str_index].TrimStart(new char[1] { '*' });
            str_index++;
        }
       else
      {
            function_table[index].Function_Name = split_str[str_index];
       }
         
        string[] split_paramenter_str = new String[3];
        split_paramenter_str = function_table[index].Function_Name.Split(new Char[] { '(' }, StringSplitOptions.RemoveEmptyEntries);
        function_table[index].Function_Name = split_paramenter_str[0];
                
        string[] paramenter = filelist[index].Split(new Char[] { '(' }, StringSplitOptions.RemoveEmptyEntries);//Function_Parameter_Number
        String paramenter_string = paramenter[1];
        paramenter_string = paramenter_string.TrimEnd(new char[2] { ')', ';' });
        str_index = 0; 

         if(paramenter_string.Equals("") || paramenter_string.Equals(" ") || paramenter_string.Equals("void"))
         {
              function_table[index].Function_Parameter_Number = 0;

              function_table[index].Function_Parameter1 = "";
              function_table[index].Function_Parameter2 = "";
              function_table[index].Function_Parameter3 = "";
              function_table[index].Function_Parameter4 = "";
              function_table[index].Function_Parameter5 = "";
          }
          else if(paramenter_string.Contains(","))
          {
                   string[] s = paramenter_string.Split(new Char[] { ',' }, StringSplitOptions.RemoveEmptyEntries);

                   switch(s.Length)
                   {
                          case 2: function_table[index].Function_Parameter1 = Get_Data_Kind(s[0]); 
                                     function_table[index].Function_Parameter2 = Get_Data_Kind(s[1]);
                                     function_table[index].Function_Parameter3 = "";
                                     function_table[index].Function_Parameter4 = "";
                                     function_table[index].Function_Parameter5 = "";
                                     function_table[index].Function_Parameter_Number = 2;
                                     break;
                        case 3: function_table[index].Function_Parameter1 = Get_Data_Kind(s[0]);
                                   function_table[index].Function_Parameter2 = Get_Data_Kind(s[1]); 
                                   function_table[index].Function_Parameter3 = Get_Data_Kind(s[2]);
                                   function_table[index].Function_Parameter4 = "";
                                   function_table[index].Function_Parameter5 = "";
                                   function_table[index].Function_Parameter_Number = 3;
                                   break;
                         case 4: function_table[index].Function_Parameter1 = Get_Data_Kind(s[0]);
                                    function_table[index].Function_Parameter2 = Get_Data_Kind(s[1]); 
                                    function_table[index].Function_Parameter3 = Get_Data_Kind(s[2]); 
                                    function_table[index].Function_Parameter4 = Get_Data_Kind(s[3]);
                                    function_table[index].Function_Parameter5 = "";
                                    function_table[index].Function_Parameter_Number = 4;
                                    break;
                          case 5: function_table[index].Function_Parameter1 = Get_Data_Kind(s[0]);
                                     function_table[index].Function_Parameter2 = Get_Data_Kind(s[1]); 
                                     function_table[index].Function_Parameter3 = Get_Data_Kind(s[2]); 
                                     function_table[index].Function_Parameter4 = Get_Data_Kind(s[3]); 
                                     function_table[index].Function_Parameter5 = Get_Data_Kind(s[4]);
                                     function_table[index].Function_Parameter_Number = 8;
                                     break;
                    }
            }
            else
            {
                function_table[index].Function_Parameter_Number = 1;

                function_table[index].Function_Parameter1 = Get_Data_Kind(paramenter_string);
                function_table[index].Function_Parameter2 = "";
                function_table[index].Function_Parameter3 = "";
                function_table[index].Function_Parameter4 = "";
                function_table[index].Function_Parameter5 = "";
            }
  }

After obtaining the function_table list, we only need to use

for (uint i = 0; i < function_table.table_length; i++)
{
    index = map_table.Get_Index(function_table.function_table[i].Function_Name);
    addr = map_table.Get_Address(index);
    function_table.Set_Address(i, addr);
}

to store the relevant information of global variables.

2.5 Control Instructions

2.5.1 Command Words and Their Data Formats

Function sends command word:

Function return value command word:

Lower machine receives timeout command word:

Some may wonder why the address of STM32 is only 4 bytes, but in the command word, the address occupies 8 bytes. This relates to converting different types of data into bytes.

The trick to converting function parameters of different types into bytes is to use a union. By defining different types of variables and a char array of maximum word length in the union, we can easily obtain their distribution in memory. Initially, to accommodate double type function parameters, a double was defined in the “union”, resulting in a length of 8 bytes. The same method was used for function address conversion, so the address length in the command word also became 8 bytes. It is important to note that C# does not have the concept of unions, so we can only use structs and specify the starting address of variables to achieve the effect of C’s union:

public struct TypeUnion
{
   [FieldOffset(0)]
   public byte uc;
   [FieldOffset(0)]
   public sbyte sc;
   [FieldOffset(0)]
   public ushort us;
   [FieldOffset(0)]
   public short ss;
   [FieldOffset(0)]
   public uint ui;
   [FieldOffset(0)]
   public uint pointer;                                        //Pointer
   [FieldOffset(0)]
   public int si;
   [FieldOffset(0)]
   public float f;
   [FieldOffset(0)]
   public double d;
}

Since we cannot define char[8], we will also use static byte[] StructToBytes(object structObj) to obtain the corresponding variable’s memory distribution byte[8]

2.5.2 Sending Process of Debugging Functions and Global Variables

After pressing the function debugging send button, the function void SendFunctionButton_Click(object sender, EventArgs e) is triggered. The main process in this function is to check whether the serial port is open -> function parameter type conversion -> CRC check -> timeout judgment and retransmission. The function parameter type conversion is mainly completed by TypeUnion TypeTransfer(String type_s,String text_s). This function converts the incoming parameters into the corresponding data based on the parameter type using the Convert.ToXXX(text_s, f_base) method and directly assigns it to TypeUnion, which is a union variable, and then obtains the memory distribution byte[8] through static byte[] StructToBytes(object structObj).

The CRC check uses the CRC16 CITT algorithm. After filling the first 49 bytes, the last two bytes are initially set to 0, and a CRC check is performed to obtain the data, which is then assigned to the last two bytes.

2.5.3 Function Return Value Reception Process

After sending the function debugging command, the host will automatically wait until it receives a reply from the lower machine or reaches the set timeout. Using static object BytesToStuct(byte[] bytes, Type type), the first 8 bytes are converted into a TypeUnion variable. The CRC check also uses the CRC16 CITT algorithm. After filling the first 8 bytes, a CRC check is performed. If the check fails, a timeout handling is performed, and the function debugging command is resent after a certain time.

2.5.4 Timeout and Retransmission Handling

In actual serial data transmission and reception, it is inevitable to encounter data loss or interruption. For example, during this development, using a virtual serial port for data transmission and reception resulted in data loss:

Developing a Debugging Tool for Microcontrollers and Discussing Its Programming Approach

Developing a Debugging Tool for Microcontrollers and Discussing Its Programming Approach

Developing a Debugging Tool for Microcontrollers and Discussing Its Programming Approach

Clearly, the monitored data was correctly transmitted and received, but data was still lost, and the reason is unknown. Therefore, we can only implement timeout retransmission handling to cope with this situation. In the host, this is mainly handled through the function bool Is_Timeout().

private bool Is_Timeout()
 {
   bool timeout = false;
   ushort count_ = 0;

   while (SerialPort.BytesToRead < RETURN_MAX_LENTH)
   {
      System.Threading.Thread.Sleep(1);                   //Check every 1ms if all data has been received
      count_++;
      if (count_ > timeout_set)
     {
        break;
     }
   }

  if (count_ < timeout_set)                               //Data processing if not timed out
 {
     byte[] byteArray = new byte[RETURN_MAX_LENTH];
     SerialPort.Read(byteArray, 0, byteArray.Length);

     uint count = 0;

     for (uint i = 0; i < PARAMENT_MAX_LENTH; i++)           //If all 8 bytes received are 0xFF, it indicates that the lower machine did not correctly receive the data
    {
       if (byteArray[i] == 0xFF)
      {
          count++;
      }
    }
    if (count >= PARAMENT_MAX_LENTH)
    {
        timeout = true;
     }
     else
    {
       if (function_send)                                        //Get function return value
      {
          function_send = false;

          ushort crc1 = 0;
          crc1 = (ushort)byteArray[RETURN_MAX_LENTH - 2];
          crc1 = (ushort)(crc1 << 8);
          crc1 = (ushort)(crc1 | (ushort)byteArray[RETURN_MAX_LENTH - 1]);

          byte[] byte_Array = new byte[RETURN_MAX_LENTH - 2];
          for (uint i = 0; i < RETURN_MAX_LENTH - 2; i++)
         {
             byte_Array[i] = byteArray[i];
         }
         CRC16 c = new CRC16();
         ushort crc = c.GetCRC16(byte_Array);

         if(crc == crc1)
         {
            TypeUnion return_data = (TypeUnion)BytesToStuct(byte_Array, typeof(TypeUnion));
            String s = TypeTransferToString(function_table.function_table[select_function_index].Function_Return, return_data);

            RecivedTextBox.Text = s;
         }
         else
         {
            timeout = true;
         }
      }
   }
 }
 else                                                    //If the set timeout is exceeded, directly close the serial port and report an error
  {
     timeout = true;

      SerialPort.Close();
      ControlSerialButton.Text = "Open Serial Port";
      COMComboBox.Enabled = true;
      BaudRateComboBox.Enabled = true;
      ParityBitsComboBox.Enabled = true;
      StopBitComboBox.Enabled = true;
      DataBitsComboBox.Enabled = true;
                
       MessageBox.Show("Communication timeout! Serial port has been closed!");
    }

            return timeout;
}

3. Lower Machine Processing

3.1 Reception Handling

Initially, I planned to use DMA + idle interrupt to receive command words, but considering that some low-end microcontrollers do not have idle interrupts, and data loss during actual use can cause continuous waiting, I opted for a single-byte interrupt reception scheme. Upon receiving a fixed byte, the flag data_recived is set, and the data is copied out.

void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart)                                                //Receive interrupt{
      unsigned char i = 0;
        
      HAL_TIM_Base_Stop_IT(&htim3);
      __HAL_TIM_SET_COUNTER(&htim3, 0);
      data[data_length] = recv_data;
        
      data_length++;
        
      if(data_length < MAX_RECIVE_LENGTH)
     {
         HAL_TIM_Base_Start_IT(&htim3);
      }
     else
    {
          data_length = 0;
                        
          data_recived = 1;
                        
         for(i = 0;i < MAX_RECIVE_LENGTH;i++)
        {
          r_data[i] = data[i];
        }
     }
                
     HAL_UART_Receive_IT(&huart1, &recv_data, 1);
}

3.2 Timeout Handling

Due to data loss during actual data transmission and reception, the host may finish sending while the lower machine has not fully received, causing the lower machine to remain in a waiting state. To resolve this, a timer with a frequency of 200Hz is introduced. Upon entering the receive interrupt, the timer is first stopped and cleared, and after reading the received data, it is restarted. If data loss occurs, causing the lower machine to wait, a timer interrupt will be triggered. In the timer interrupt, the receive counter is cleared, and a timeout command is sent to the host.

void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim)                                                                        //Timeout interrupt, timeout period is 5ms, once timed out, send 8 bytes of 0xFF{
      unsigned char i = 0;
        
      HAL_TIM_Base_Stop_IT(&htim3);
      __HAL_TIM_SET_COUNTER(&htim3, 0);
        
      data_length = 0;
        
      for(i = 0;i < PARAMENT_MAX_LENTH;i++)
     {
         returndata[i] = 0xFF;
      }
                
      crc = crc16(returndata, PARAMENT_MAX_LENTH);        
      returndata[PARAMENT_MAX_LENTH] = crc >> 8;
      returndata[PARAMENT_MAX_LENTH + 1] = crc;
      HAL_UART_Transmit(&huart1, returndata, 10, 0xFF);
}

3.3 Function Pointers

The function debugging command sent by the host is implemented through the periodic call of void Recived_Command_Handle(void).

void Recived_Command_Handle(void){
      unsigned char i = 0;
      unsigned char j = 0;
                        
      if(data_recived)
      {
           data_recived = 0;
                        
           crc_16 = (r_data[49] << 8) | r_data[50];
                
           r_data[MAX_RECIVE_LENGTH - 2] = 0;
           r_data[MAX_RECIVE_LENGTH - 1] = 0;
           crc = crc16(r_data, MAX_RECIVE_LENGTH);                                                                                                                                                                                                                        //CRC_CITT check
                        
           if(crc == crc_16)
           {
               num = r_data[0];                                                                                                                                                                                                                                                                                                        //Get parameter count
                        
               for(i = 0;i < PARAMENT_MAX_LENTH;i++)                                                                                                                                                                                                                        //Get address
               {
                    addr.u_char[i] = r_data[1 + i];
                }
                                
               for(i = 0;i < 5;i++)                                                                                                                                                                                                                                                                                                //Get parameters
              {
                       for(j = 0;j < PARAMENT_MAX_LENTH;j++)
                      {
                           paramen[i].u_char[j] = r_data[(1 + PARAMENT_MAX_LENTH) + PARAMENT_MAX_LENTH*i + j];
                       }
                }
                                
             if(addr.ul != 0)                                                                                                                                                                                                                                                                                                                        //Get return value
             {
                   for(i = 0;i < PARAMENT_MAX_LENTH;i++)
                  {
                       return_data.u_char[i] = 0;
                   }
                                        
                  return_data = function(addr.ul,num,paramen);
              }
                                        
             for(i = 0;i < PARAMENT_MAX_LENTH;i++)
             {
                  returndata[i] = return_data.u_char[i];
              }
              crc = crc16(return_data.u_char, PARAMENT_MAX_LENTH);        
              returndata[PARAMENT_MAX_LENTH] = crc >> 8;
               returndata[PARAMENT_MAX_LENTH + 1] = crc;
                HAL_UART_Transmit(&huart1, returndata, 10, 0xFF);                                                                                                 
            }
       }
}

The function implementation is completed by parameter_kind_union function(unsigned int function_addr,unsigned char paramenter_num,parameter_kind_union *paramenter).

parameter_kind_union function(unsigned int function_addr,unsigned char paramenter_num,parameter_kind_union *paramenter){
    void *p = (void *)function_addr;
    parameter_kind_union return_data;
        
    switch(paramenter_num)
   {
         case 0: return_data.ull = (*(unsigned int(*)())p)();
                    break;
         case 1: return_data.ull = (*(unsigned int(*)())p)(PARAMENT_TRANSFER(paramenter[0]));
                    break;
         case 2: return_data.ull = (*(unsigned int(*)())p)(PARAMENT_TRANSFER(paramenter[0]),PARAMENT_TRANSFER(paramenter[1]));
                    break;
         case 3: return_data.ull = (*(unsigned int(*)())p)      (PARAMENT_TRANSFER(paramenter[0]),PARAMENT_TRANSFER(paramenter[1]),PARAMENT_TRANSFER(paramenter[2]));
                     break;
          case 4: return_data.ull = (*(unsigned int(*)())p)(PARAMENT_TRANSFER(paramenter[0]),PARAMENT_TRANSFER(paramenter[1]),PARAMENT_TRANSFER(paramenter[2]),PARAMENT_TRANSFER(paramenter[3]));
                      break;
           case 5: return_data.ull = (*(unsigned int(*)())p)(PARAMENT_TRANSFER(paramenter[0]),PARAMENT_TRANSFER(paramenter[1]),PARAMENT_TRANSFER(paramenter[2]),PARAMENT_TRANSFER(paramenter[3]),PARAMENT_TRANSFER(paramenter[4]));
                       break;
       }
        
           return return_data;
}
#define PARAMENT_TRANSFER(p)        (*(volatile unsigned int*)((unsigned int)&p))  

The operational meaning is as follows:

1) &p means to take the address of variable p;

2) (unsigned int)&p means to forcibly convert the obtained address to unsigned int;

3) (volatile unsigned int*)((unsigned int)&p) means to convert the number to a pointer of unsigned int type;

4) *(volatile unsigned int*)((unsigned int)&p) means to obtain the data within that address;

However, this may cause a problem, which is that for double and long type variables, obtaining values may lead to errors:

Developing a Debugging Tool for Microcontrollers and Discussing Its Programming Approach

Developing a Debugging Tool for Microcontrollers and Discussing Its Programming Approach

As can be seen, for the double type, the function parameter value only obtained the first 4 bytes of data, while the last 4 bytes of data were lost. Attempting to define

#define PARAMENT_TRANSFER(p)        (*(volatile unsigned long long*)((unsigned int)&p))

can correctly obtain the double parameter, but char and other types cannot be correctly retrieved:

Developing a Debugging Tool for Microcontrollers and Discussing Its Programming Approach

Developing a Debugging Tool for Microcontrollers and Discussing Its Programming Approach

Therefore, we temporarily use the first definition of PARAMENT_TRANSFER.

(*(unsigned int(*)())p)() is used to execute the function, similar to a callback function. It allows us to execute the specified function.

3.4 Modifying Global Variables

Data writing is implemented through void Set_Global_Data(unsigned int addr,unsigned char len,parameter_kind_union data). For arrays, this function is called in a loop, and a test retransmission feature is added.

Developing a Debugging Tool for Microcontrollers and Discussing Its Programming ApproachDeclaration:This article is authorized by the authorand reprinted from“21ic Electronics Network“, copyright belongs to the author. If there is any infringement, please contact us for deletion!

「If useful, please share」

👇Click Follow, Technologyto receive timely technical content!👇

Developing a Debugging Tool for Microcontrollers and Discussing Its Programming Approach

  • What is the relationship between Fourier Transform, Laplace Transform, and Z Transform? Why perform these transformations?

  • Those microcontrollers that were once very popular~

  • Can you imagine a Bluetooth chip that costs less than 2 yuan?

  • Why do domestic chips also use English to write “datasheet”?

  • Wonderful operational amplifier circuits

  • Why is it 50 ohms???

  • The most detailed basics of diodes

  • What is a BSP engineer?

Like, share, good articles need your support and encouragement<img src=”https://mmbiz.qpic.cn/mmbiz_gif/iapXlcghssnXZFQ8rtknqDZ2w6yJ1DP3smnzf8GWGRoZNI4XEqfqU011YKSMkfDKmOFNf9kHBTrjBSY3EH1aWMQ/640?wx_fmt=gif&wxfrom=5&wx_lazy=1″

Leave a Comment