Tag: Breadboard

  • Display time dynamically on a I2C LCD using a Blackpill

    Display time dynamically on a I2C LCD using a Blackpill

    Goal

    This project is the second iteration of my first project where I displayed the time generated on the STM32 to Putty. Instead of just displaying to Putty, this time I wanted to display the time and date to a LCD. Pretty simple overall, nothing too fancy. I do want to configure it though so that the display updates dynamically so that I don’t refresh the whole LDC causing flashing. The process to do this ended up being quite a bit more complicated than I expected but after a lot of Googling and trial and error, I was able to get it figured out. Like my other article I found CircuitGator on YouTube extremely helpful. You can find the video I used to configure i2c here.

    STMCube Configuration

    The only setting that needs to be configured in the GUI is RTC under the timers tab. In the settings page for RTC all that needs to be configured is the check box for activating the clock source and the calendar. For ease of use you can also configure the parameters so that the code generates with the correct time. This can be changed manually in the main.c file as well if desired. I also changed the HCLK in the Clock configuration tab to 100Mhz to maximize CPU performance but this is not required.

    After these changes the code can be generated.

    Code Generation

    Dynamic LCD Updating

    Before I get into the code and what each section does, I want to give a brief explanation of the overall process of how I’m interacting with the LCD and why. Instead of refreshing and re-printing all of the data to the LCD, I only want to update the portion that changes. For instance the section that displays the date will only change once a day. So instead of reprinting that each second, I only reprint it if it changes. To achieve this I have separate processing for each value on the LCD. Without this processing the LCD takes a lot longer to change and flashes each second. With processing it will update each second and only changes the values that increased.

    Driver Installation

    Now that the GUI settings have been configured it is time to do the fun part. Before we start making the code you have to import the i2c driver files into your src and inc directories. These are both under %Projectname%>Divers>STM32Fxx_HAL_Driver. You can just copy and past via the GUI. The driver files can be found here. The .c file goes in the Src folder and the .h file goes into the Inc folder. Once copied, open liquidcyrstal_i2c.h in the Inc folder and verify the #include statement matches your STM chip. So if you have a STM32F4 it should be #include "stm32f4xx_hal.h". And STM32F1 would be #include "stm32f1xx_hal.h" You will also want to scroll down till you see where see the i2c address being defined. For me, I used a Inland 1602 LCD so I had to update my address to #define DEVICE_ADDR (0x27 << 1). The 0x27 portion is all you should need to change. You can get this number from your LCDs datasheet or use a i2c address finder program which is what I did.

    Main.c Configuration

    Now that the driver has been imported we can configure the main.c file. We first start with our includes which import the library needed for our while loop and private code to word.

    /* USER CODE BEGIN Includes */
    #include "stm32f4xx_hal.h"  // Required for i2c calls
    #include <stdio.h>  //Required for snprintf
    #include <string.h> // Required for strncmp
    /* USER CODE END Includes */

    Private User Variables

    Next we can declare our private Variables. This is where my code may get a bit messy. The “buffer#” buffers are used to store and print to the LCD. The corresponding “buffer#temp” buffer is used in the strcmp function to compare string for changes. This is apart of how I only refresh the part of the LCD that changes.

    /* USER CODE BEGIN PV */
    char buffer[5];
    char buffertemp[5];   // Used for String Comparison
    char buffer1[5];
    char buffer1temp[5];  // Used for String Comparison
    char buffer2[5];
    char buffer2temp[5];  // Used for String Comparison
    char buffer3[5];
    char buffer3temp[5];  // Used for String Comparison
    char buffer4[5];
    char buffer4temp[5];  // Used for String Comparison
    char buffer5[5];
    char buffer5temp[5];  // Used for String Comparison
    /* USER CODE END PV */

    User Code 2

    Once we have declared our character buffers we can now begin interacting with the LCD. This portion of code runs once during the boot and does not continue to loop/change after start. RTC Time and Date functions define the sDate and sTime calls so that we can use them in the while loop and get their sub values. After this I can then initialize and clear the LCD and type the labels for each row. These are typed once at boot and not refreshed. If they were typed in the while loop it would keep refreshing with the same text and cause flashing and increased latency.

      /* USER CODE BEGIN 2 */
      RTC_TimeTypeDef sTime;
      RTC_DateTypeDef sDate;
    
      HD44780_Init(2);         // Initialize LCD
       HD44780_Clear();        // Clear LCD of data
       HD44780_Backlight();    // Enable backlight
       HD44780_SetCursor(0,0);    // Set location to row 1
       HD44780_PrintStr("Time:"); // Type "Time:" on row 1
       HD44780_SetCursor(0,1);    // Set location to row 2
       HD44780_PrintStr("Date:"); // Type "Date:" to row 2
      /* USER CODE END 2 */

    While loop

    The last section we needed to work on is the while loop. This part is two separate ‘sections.’ They are in the same loop statement but the first section does the processing and comparison for the time while the second section does the date. It is quite a long statement but when you break it down its quite simple. The first portion uses HAL to get the time and date. It grabs this data in binary and assigns it to sTime and sDate. This grabs updated data and assigns it to the values to later be read in the next section. To make the next sections easier to understand, I will only explain how I get the seconds from RTC and display it to the LCD. Each variable like the month or hour follow the same logic, just in their own statements.

    To get display the seconds I first use snprintf to grab and format the data. The data is grabbed by sTime.Seconds and because I included the "%02d" it formats it as 2 characters. For example 2s would be 02 and 10 would be 10. This integer is then assigned to the corresponding temp buffer which in this case is buffer2temp. This buffer does not get sent to the LCD but is instead used for comparison. Assume this function ran and assigned the value 06 to buffer2temp for the next section

    Now that the RTC binary info has been converted to a string and stored in buffer2temp I can use the strcmp function which compares the existing/previously printed variable(buffer2) with the most recently generated variable stored in buffer2temp. Upon the initial first run buffer2 has no value so it is considered different during comparison. For the sake of this explanation we will assume it has been running for 5s so the value last printed was 05. This means buffer2 currently has a value of 05. When the strcmp runs it compares buffer2(05) with buffer2temp(06). Since this value is different it initiates the code in the “else” section. If these values are equal no action is taken and the loop continues. When no action is taken the LCD does not update the correspond section displayed on the LCD. But since in this example the else section was activated I run another snprinf statement. This is the same function as the one that created the temp value but instead stores it in buffer2. I then use HD44780_SetCursor to set the cursor position. Since I want to print on the 7th character in the first line I use column 6 and row 0. HD44780_PrintStr is then called and is assigned the value buffer2 so that it can then be printed to the LCD.

    This same logic loop is used for printing the hours, minutes, seconds, date, month and year. Looking back on this there are definitely things I could have done more efficiently, but as a starting place for interacting and printing to the LCD it does work. If I were to rework this I would probably just assign buffer2temp to buffer2 instead of doing a duplicate snprintf statement in the strcmp loop.

    The final line in the while loop is HAL_delay(). All this does is tell the loop to restart every 1000ms.

        /* USER CODE BEGIN 3 */
    	  // Get the current time and date from RTC
    	  HAL_RTC_GetTime(&hrtc, &sTime, RTC_FORMAT_BIN);
    	  HAL_RTC_GetDate(&hrtc, &sDate, RTC_FORMAT_BIN);
    
    	  // Format time and date into a string
    	  snprintf(buffertemp, sizeof(buffertemp), "%02d:",
    	  	sTime.Hours);
    	  snprintf(buffer1temp, sizeof(buffer1temp), "%02d:",
    	  	sTime.Minutes);
    	  snprintf(buffer2temp, sizeof(buffer2temp), "%02d",
    	  	sTime.Seconds);
    	  if (strcmp(buffer, buffertemp) == 0) {
    	          // Strings are equal
    	      }
    	      else {
    	    	  snprintf(buffer, sizeof(buffer), "%02d:",
    	    			  sTime.Hours);
    	    	   HD44780_SetCursor(6,0);
    	    	   HD44780_PrintStr(buffer);
    	      }
    	  if (strcmp(buffer1, buffer1temp) == 0) {
    	          // Strings are equal
    	      }
    	      else {
    	    	  snprintf(buffer1, sizeof(buffer1), "%02d:",
    	    			  sTime.Minutes);
    	    	   HD44780_SetCursor(9,0);
    	    	   HD44780_PrintStr(buffer1);
    	      }
    	  if (strcmp(buffer2, buffer2temp) == 0) {
    	          // Strings are equal
    	      }
    	      else {
    	    	  snprintf(buffer2, sizeof(buffer2), "%02d",
    	    			  sTime.Seconds);
    	    	   HD44780_SetCursor(12,0);
    	    	   HD44780_PrintStr(buffer2);
    	      }
    
    	  snprintf(buffer3temp, sizeof(buffer3temp), "%02d-",
    	  	sDate.Month);
    	  snprintf(buffer4temp, sizeof(buffer4temp), "%02d-",
    	  	sDate.Date);
    	  snprintf(buffer5temp, sizeof(buffer5temp), "%02d",
    	  	sDate.Year + 2000);
    	  if (strcmp(buffer3, buffer3temp) == 0) {
    	          // Strings are equal
    	      }
    	      else {
    	    	  snprintf(buffer3, sizeof(buffer3), "%02d-",
    	    			  sDate.Month);
    	    	   HD44780_SetCursor(6,1);
    	    	   HD44780_PrintStr(buffer3);
    	      }
    	  if (strcmp(buffer4, buffer4temp) == 0) {
    	          // Strings are equal
    	      }
    	      else {
    	    	  snprintf(buffer4, sizeof(buffer4), "%02d-",
    	    			  sDate.Date);
    	    	   HD44780_SetCursor(9,1);
    	    	   HD44780_PrintStr(buffer4);
    	      }
    	  if (strcmp(buffer5, buffer5temp) == 0) {
    	          // Strings are equal
    	      }
    	      else {
    	    	  snprintf(buffer5, sizeof(buffer5), "%02d",
    	    			  sDate.Year + 2000);
    	    	   HD44780_SetCursor(12,1);
    	    	   HD44780_PrintStr(buffer5);
    	      }
    	  HAL_Delay (1000);
      }
      /* USER CODE END 3 */

    Physical Setup

    The diagram above is the layout/wiring scheme I used for this setup. The whole configuration is powered by a USB plugged into the Blackpill. The LCD required 5v and can be driven via the 5v header on the Blackpill. One important thing to note is the the SDA and SCL pins for the i2c connection need to have pull up resistors. I connected these resistors to the 5v line from the Blackpill. I didn’t know this at first and all I could do was power on the LCD. I couldn’t get any communication without the pull up resistors, not even the i2c address. All I had on hand at the time were a 22kOhm and a 10kOhm resistor so they are what I used. These are a lot larger than most people use but they worked for me in a pinch. I would used a 4.7kOhm or similar if I were to redo this design.

    Conclusion

    I enjoyed doing this project. Getting to work with the LCD and working with the i2c driver taught me a lot about how to work with the STM32 and how the coding works. I am by no means an expert of using a STM32 but getting to work with buffers and creating the majority of this code my self was fun. It was neat to learn the snprintf and strcmp functions which I had no experience with before this. It was also a good learning experience when I had to figure out how to refresh the LCD without refreshing the whole thing. The final goal I have for working with the RTC on the STM32 is to calibrate/discipline it with me 9483 NetClock in my rack. Once I have that finished I will make a small housing to turn it into a standard desk clock with PPS accuracy…at least that’s the theory.