UART Data Communication

In UART communication, TX process and RX process are usually handled differently. When transmitting data, you first construct a full packet of data. Then you send it rather relaxed manner. You can let the DMA handle the transfer, or you can use blocking call outside of any time critical function. Most of SDK provide such functions by default.

When receiving data however, you do not know the size or the validity of the packet beforehand. So you have to check each byte and verify it against the protocol as it arrives. Thus it is best to handle the RX task in an UART interrupt callback function instead of using functions provided by SDK.

In STM32Cube framework, you first activate UART RX interrupt using LL.

  412 /** Initialize SerialComm
  413  */
  414 void SerialComm_Init()
  415 {
  416     LL_USART_EnableIT_RXNE(huart1.Instance);
  417 }

Then write an interrupt handler for UART RX interrupt. In STM32Cube convention, a skeleton code for this interrupt handler is populated in stm32xxxx_it.c file.

  126 void USART1_IRQHandler(void)
  127 {
  128     if(LL_USART_IsActiveFlag_RXNE(USART1) && LL_USART_IsEnabledIT_RXNE(USART1))
  129     {
  130         SerialComm_RxRoutine();
  131     }
  132 }

In acutual RX routine, each incoming byte is checked by the packet decoder, SerialComm_Decoder and loaded into one of the two pingpong buffers whenever a valid packet is received.

  419 void SerialComm_RxRoutine()
  420 {
  421     pkt_status status;
  422 
  423     status = SerialComm_Decoder(LL_USART_ReceiveData8(huart1.Instance),
  424             UartIsrBuf);
  425 
  426     if(status == PKT_RECEIVED)
  427     {
  428         // switch ping pong buffer
  429         if(UartIsrBuf == UartRxBuf1)
  430         {
  431             UartIsrBuf = UartRxBuf2;
  432             UartRxBuf = UartRxBuf1;
  433         }
  434         else
  435         {
  436             UartIsrBuf = UartRxBuf1;
  437             UartRxBuf = UartRxBuf2;
  438         }
  439         // raise flag
  440         bPktReceived = true;
  441     }
  442 }

Then you need another task that checks the contents of this pingpong buffer and generates events when new packet is loaded.

  386 void UartRxTask()
  387 {
  388     int i;
  389     uint8_t event[EVT_QWIDTH];
  390 
  391     // packet received
  392     if(bPktReceived)
  393     {
  394         // event id
  395         event[0] = EVT_UART_RXPKT;
  396         // event data size
  397         event[1] = UartRxBuf[1];
  398 
  399         // copy the payload
  400         for(i = 0; i< UartRxBuf[1]; i++)
  401         {
  402             event[2+i] = UartRxBuf[2+i];
  403         }
  404     
  405         // register the event
  406         Evt_EnQueue(event);
  407         // clear the flag
  408         bPktReceived = 0;
  409     }
  410 }

This task runs as a software timer routine in regular interval, whose frequency should be higher than packet rate that the protocol defines.

  107     // start a timer routine: 10msec period, perpetual
  108     result = UsrTimer_Set(10, 0, UartRxTask);

This seemingly redundant UartRxTask is necessary to avoid race condition since EvtQueue is designed to be used by software timer callback for posting event. If Evt_EnQueue function is called from SerialComm_RxRoutine, a callback of the UART RX interrupt directly it can be disturbed by SysTick interrupt whose priority is higher.

Pushbutton Event Handler: Part 4

Pushbutton Handler

The handler routine is started by calling PushButton_Init function. This function registers PushButton_Routine at the end as a callback of the software timer.

   35 void PushButton_Init(uint8_t mask)
   36 {
   37     int i ;
   38 
   39     // clear data
   40     pp.old_state = pp.new_state = 0;
   41     pp.mask = mask;
   42     pp.mode = 0;
   43     pp.flag = false;
   44 
   45     // clear log
   46     for(i = 0; i < 8; i++)
   47     {
   48         PushButton_ClearLog(i);
   49     }
   50 
   51     // register pushbutton main routine
   52     UsrTimer_Set(PUSHBTN_TMR_PERIOD, 0, PushButton_Routine);
   53 }

The PushButton_Routine is then called at every 80msec and tracks the button state. If the button state is ON for at least three consecutive polling (80 x 3 = 240msec) then it declares CLICK by posting an event. These timing can be set to fit the bouncing characteristic of the mechanical switch.

   71 /// PushButton_Routine timer period in msec
   72 #define PUSHBTN_TMR_PERIOD      80
   73 /// Criteria for determination of short click and long click
   74 #define PUSHBTN_TO_SHORT        3       // 3 * PUSHBTN_TMR_PERIOD
   75 #define PUSHBTN_TO_LONG         10      // 10 * PUSHBTN_TMR_PERIOD
   76 #define PUSHBTN_TO_MAX          255     // maximum duration count

The PushButton_Routine, distinguishes single, double, triple click and long click (push and hold) and generates event accordingly, which in turn collected by the event handler in the main loop.

You can also change the mode from click detection to up/down detection, in which event is constantly generated when you hold the button down.

(source code)

FatFs with FLASH Memory

STM32Cube framework provides similar interface for FatFS as USB MSC discussed before. Again the same argument applies to this case about the basic unit of read/write operation on a flash memory. In this case, the sector size and the cluster size of the FatFs should be equal to the sector size of FLASH memory, namely 4KB.

This can be set in the file ffconf.h, in which a lot of customization can be done by changing the definitions. In particular, following definitions should be set.

  204 #define _MIN_SS    4096  /* 512, 1024, 2048 or 4096 */
  205 #define _MAX_SS    4096  /* 512, 1024, 2048 or 4096 */

In the file user_diskio.c, USER_read and USER_write functions can be implemented as the same way as before. Here we assume that the count is always one for the simplicity. In fact this can be bigger than one if you call f_read() with the buffer size larger than the sector size (4096). However given the limited memory capacity of a mcu, that is not very realistic. And by similar token pdrv variable can be ignored.

    1 /**
    2   * @brief  Reads Sector(s) 
    3   * @param  pdrv: Physical drive number (0..)
    4   * @param  *buff: Data buffer to store read data
    5   * @param  sector: Sector address (LBA)
    6   * @param  count: Number of sectors to read (1..128)
    7   * @retval DRESULT: Operation result
    8   */
    9 DRESULT USER_read (
   10     BYTE pdrv,      /* Physical drive nmuber to identify the drive */
   11     BYTE *buff,     /* Data buffer to store read data */
   12     DWORD sector,   /* Sector address in LBA */
   13     UINT count      /* Number of sectors to read */
   14 )
   15 {
   16   /* USER CODE BEGIN READ */
   17     SFlash_ReadSector(sector, (uint8_t*)buff);
   18     return RES_OK;
   19   /* USER CODE END READ */
   20 }
   21 
   22 /**
   23   * @brief  Writes Sector(s)  
   24   * @param  pdrv: Physical drive number (0..)
   25   * @param  *buff: Data to be written
   26   * @param  sector: Sector address (LBA)
   27   * @param  count: Number of sectors to write (1..128)
   28   * @retval DRESULT: Operation result
   29   */
   30 #if _USE_WRITE == 1
   31 DRESULT USER_write (
   32     BYTE pdrv,          /* Physical drive nmuber to identify the drive */
   33     const BYTE *buff,   /* Data to be written */
   34     DWORD sector,       /* Sector address in LBA */
   35     UINT count          /* Number of sectors to write */
   36 )
   37 { 
   38   /* USER CODE BEGIN WRITE */
   39     SFlash_WriteSector(sector, (uint8_t*)buff);
   40     /* USER CODE HERE */
   41     return RES_OK;
   42   /* USER CODE END WRITE */
   43 }
   44 #endif /* _USE_WRITE == 1 */

If ioctl is to be used, implement corresponding parts in the USER_ioctl function.

   84 #if _USE_IOCTL == 1
   85 DRESULT USER_ioctl (
   86     BYTE pdrv,      /* Physical drive nmuber (0..) */
   87     BYTE cmd,       /* Control code */
   88     void *buff      /* Buffer to send/receive control data */
   89 )
   90 {
   91   /* USER CODE BEGIN IOCTL */
   92     DRESULT res = RES_ERROR;
   93 
   94     switch (cmd)
   95     {
   96     case CTRL_SYNC :
   97         res = RES_OK;
   98         break;
   99 
  100     case GET_SECTOR_COUNT :
  101         *(DWORD*)buff = SFLASH_NUM_SECTORS;
  102         res = RES_OK;
  103         break;
  104 
  105     case GET_SECTOR_SIZE :
  106         *(WORD*)buff = SFLASH_SECTOR_SIZE;
  107         res = RES_OK;
  108         break;
  109 
  110     case GET_BLOCK_SIZE :
  111         *(DWORD*)buff = SFLASH_SECTOR_SIZE;
  112         res = RES_OK;
  113         break;
  114 
  115     default:
  116         res = RES_PARERR;
  117         break;
  118     }
  119 
  120     return res;
  121   /* USER CODE END IOCTL */
  122 }
  123 #endif /* _USE_IOCTL == 1 */

FatFs allows users to not only read and write files but also to create a file system on a raw media. Unfortunately in this particular case, the capacity of the media is too small (1MB) for the FatFs to compute correct parameters. Thus its f_mkfs function failed. However, as shown before the file system can be created by the host system if it is connected to the host a s USB MSC device.

(source code)

Pushbutton Event Handler: Part 3

Event Queue

In the two step process described at the Part 1, short and fast process should transfer information of certain event to the long and slow process. This can be done simply by raising a flag. Alternatively it can be done more systematically by using a data structure such as a ring buffer.

You can create a ring buffer whose element size is few byte long and use it as a queue to convey short information about events. Let us call it an event queue. As a minimum, you need two functions, put and get (or enqueue and dequeue).

   66 #define EVT_QDEPTH              (8)
   67 /** The maximum size of the event data. It consists of one byte of event code
   68  * with variable length of data bytes.
   69  */
   70 #define EVT_QWIDTH              (16)
   71 
   72 /// Register a new event
   73 bool Evt_EnQueue(uint8_t *event);
   74 /// Checkout the oldest event
   75 bool Evt_DeQueue(uint8_t *event);
   76 /// Initialize the event queue
   77 void Evt_InitQueue(void);

As for the usage, for the sake of argument, let us the first byte of a queue element denote the category of event and the next byte detailed type. Then we can define event data structure for pushbutton handler like below.

    4 /** Pushbutton input event
    5  *
    6  * Event Data: (EVT_SRC)(EVT_TYPE)
    7  * 
    8  *  * EVT_SRC: id of the pushbutton that generated the event
    9  *  * EVT_TYPE: type of the event such as single click, double click, 
   10  */
   11 #define PBTN_INPUT              0x10        ///< event Source: pushbutton input
   12 
   13 #define PBTN_SCLK               0x01        ///< event Type: single click 
   14 #define PBTN_LCLK               0x02        ///< event Type: long click
   15 #define PBTN_DCLK               0x03        ///< event Type: double click
   16 #define PBTN_TCLK               0x04        ///< event Type: triple click
   17 #define PBTN_DOWN               0x05        ///< event Type: button state down
   18 #define PBTN_ENDN               0x06        ///< event Type: button state changed to up

When event is detected by the fast process (PushButton_Routine), it pushes relevant information to the queue.

  112         // up-down mode
  113         if(((pp.mode >> i) & 0x01)  == PUSHBTN_MODE_UDOWN)
  114         {
  115             // the button pressed
  116             if(((pp.new_state >> i) & 0x01) == 0x01)
  117             {
  118                 event[0] = EVT_PBTN_INPUT;
  119                 event[1] = (uint8_t)(i+1);
  120                 event[2] = PBTN_DOWN;
  121     
  122                 // post the event as long as the button is pressed down
  123                 Evt_EnQueue(event);
In the main loop, event handler (slow process) keeps checking the queue and processes it whenever new event is posted.
  115     while (1)
  116     {
  117         /* USER CODE END WHILE */
  118 
  119         /* USER CODE BEGIN 3 */
  120 
  121         // check event queue
  122         if(Evt_DeQueue(event))
  123         {
  124             switch(event[0])
  125             {
  126             // pushbutton event ================================================
  127             // event[1]: button id
  128             // event[2]: PBTN_SCLK, _DCLK, _TCLK, _LCLK, _DOWN, _ENDN
  129             case EVT_PBTN_INPUT:
  130 
  131                 if(event[2] == PBTN_SCLK)
  132                 {
  133                     UartPrintf("\r\nButton %d: single click.", event[1]);
  134                 }

(timing consideration) Since the software timer is based on 1 msec SysTick, each callback should do its job much faster than 1 msec, preferably within a few millisecond. So slow routines such as ADC conversion or SPI transaction should be strictly avoided.

(race condition) The event queue is shared by the slow processes and the fast processes, where the fast processes preempt the slow processes since they are the callback of SysTick interrupt. To avoid race condition, Software timer should be paused when slow process access the queue.

   63 bool Evt_DeQueue(uint8_t *event)
   64 {
   65     uint8_t i;
   66     bool flag = false;
   67 
   68     // disable all timers
   69     UsrTimer_Enable(false);
   70 
   71     // queue is not empty
   72     if(evt_queue.tail != evt_queue.head)
   73     {
   74         // copy event bytes into the buffer
   75         for(i = 0; i < EVT_QWIDTH; i++)
   76         {
   77             event[i] = evt_queue.buff[evt_queue.tail][i];
   78         }
   79         // move to the next position
   80         evt_queue.tail = ADVANCE_QPTR(evt_queue.tail);
   81         // set flag
   82         flag = true;
   83     }
   84 
   85     // enable all timers
   86     UsrTimer_Enable(true);
   87 
   88     // return with the flag
   89     return flag;
   90 }

(source code)

Pushbutton Event Handler: Part 2

Software Timer

In typical hardware timer use cases, you set up a timer with certain settings such as interval then assign a callback function that is called when the timer expires and let the function do certain task. You can instead write a timer callback function to call other callback functions, i.e. cascade the callback functions. This way, you can hook up multiple callback functions to a timer with arbitrary interval. Original timer now works as a base timer but the callback function works as a software timer.

Cortex-M processors have generic 1 millisecond timer called SysTick. Write a SysTick callback function that handles host of other callback functions with intervals of multiple of 1msec. For example, you can make the SysTick callback function call pushbutton state check routine every 300msec, as well as many other routines that are called at various intervals.

In STM32Cube framework, SysTick interrupt handler calls the function HAL_SYSTICK_IRQHandler (Warning: This was broken in the version 5.0.0 of STM32CubeMX. So you need to do it manually), which in turn calls HAL_SYSTICK_Callback function.

    1 /**
    2 * @brief This function handles System tick timer.
    3 */
    4 void SysTick_Handler(void)
    5 {
    6     /* USER CODE BEGIN SysTick_IRQn 0 */
    7 
    8     /* USER CODE END SysTick_IRQn 0 */
    9     HAL_IncTick();
   10     HAL_SYSTICK_IRQHandler();
   11     /* USER CODE BEGIN SysTick_IRQn 1 */
   12 
   13     /* USER CODE END SysTick_IRQn 1 */
   14 }

Thus it is a good place to put your software timer routine.

  355 /** SysTick callback function override.
  356  */
  357 void HAL_SYSTICK_Callback()
  358 {
  359     // UsrTimer_Routine will have 1msec resolution
  360     UsrTimer_Routine();
  361 }

Following example registers a function that toggles LED at every 100msec.

    1 void TestCallback()
    2 {
    3     HAL_GPIO_TogglePin(LED_GPIO_Port, LED_Pin);
    4 }
    5 
    6 main()
    7 {
    8     // start software timer routine
    9     UsrTimer_Init();
   10 
   11     // register a callbackfunction with 100msec interval
   12     UsrTimer_Set(100, 0, TestCallback);

Software timer implemented in the sample project file support following functions

   43 /// Initialize all timers
   44 void UsrTimer_Init();
   45 /// Enable or disable main routine
   46 void UsrTimer_Enable(bool flag);
   47 /// Clear the timer
   48 void UsrTimer_Clear(uint32_t index);
   49 /// Pause the timer 
   50 void UsrTimer_Pause(uint32_t index);
   51 /// Resume the timer
   52 void UsrTimer_Resume(uint32_t index);
   53 /// Main timer routine
   54 void UsrTimer_Routine(void);
   55 /// Set a new timer with the callback function
   56 int UsrTimer_Set(uint32_t interval, uint32_t duration, usrtimer_callback f);

(source code)