UI Module

UI Architecture

The User Inteface (UI) module is responsible for three things:

  1. Reading user input
  2. Displaying information to the LCD
  3. Managing a menu interface

Let’s look at the steps above in order.

Reading User Input

The user input will be through an encoder, EN11-HSM1BF20. The encoder can detect when the handle is rotated clockwise or counter-clockwise. The user can also press down on the head, and this will cause the encoder to act like a button.

From EN11-HSM1BF20 datasheet

The encoder has a quadrature output. As the image above shows, when the encoder is left alone, both A and B will be high. When the encoder is rotated clockwise, then B will go low, then A will go low, then B will go high, then A will go high. If the encoder is rotated counter clockwise, then the process is reversed: A will go low, then B will go low, then A will go high, then B will go high.

Interrupt Service Routine (ISR) for reading encoder

The ISR above handles reading the encoder. It is triggered by a pin change interrupt, which will occur when the encoder is rotated or pushed. The highlighted portion shows how the direction is determined: the code will wait until both A and B are low. This “arms” the ISR to determine the direction the next time the ISR is called. When the ISR is called again, it will read A and B and see which one is high. If A is high, then that means the encoder is turning clockwise. If B is high, then the encoder is turning counter-clockwise.

Wait a minute! The datasheet says that if B leads A when the encoder is turned clockwise! In order to keep the ISR as simple as possible, I coded it to assume A leads B when the encoder is turned clockwise. Then, the Encoder class, which reads the ISR’s output, will invert the direction as necessary.

Now let’s take a look at the button. The button by itself is trivial; if the button is pressed, then the signal will go low, when the button is released the signal will go high. Therefore, we can just read if the button is pushed or not. However, I wanted to implement something more advanced. I wanted the system to distinguish between a button press, and a long press. If the button is pushed quickly, then this will act like an OK or ENTER. If the button is pressed for a certain amount of time, then it will act like a BACK or CANCEL. This will greatly improve the user interface.

Pin change ISR (left)
get_pressed method (right)

The ISR is shown again above on the left. The highlighted section shows how the button is read. When a falling edge is detected on the button pin, which means the button has been pressed, the time is saved, a flag is set, and pressed is updated. When a rising edge is detected, which means the button is released, the flag is cleared.

The real magic happens on the right, which is the method used by the Encoder class to read the encoder. Here, if pressed hasn’t been updated, then there’s nothing to do. If pressed has been updated, then we know the user is trying to do a short press or a long press. This is where the flag comes in: if the flag is still set, that means the button is continuing to be pressed, so the code compares the current time to when the button was first pressed. If the two times are sufficiently different, then this is considered a long press, so the method returns LONG_PUSH. If the two times aren’t sufficiently different, then we’ll have to wait a bit to see if the user lets go early enough for it to be a short press, or holds on long enough to be a long press. Since the results are inconclusive, the method returns NO_PUSH for now. If the user lets go early enough to be considered a short press, then the ISR will clear the flag. Then, get_pressed will see that the flag has been cleared, and return PUSH. If sufficient time has passed for the push to be a long push, then the flag clearing is inconsequential since the method will already have determined that the push was a long push, and returned LONG_PUSH already.

Encoder debouncing / filtering

One quick note about the hardware. Above is the original debouncing circuit I designed; R10, R13 and R15 act as pull-ups, while R11, R12 and R14 are current limiting resistors in series with the encoder’s A, B and button pins. C14, C15 and C16 act as filtering capacitors. This circuit didn’t perform very well, so I did a little researched and discovered that this is not the recommended way to debounce encoders. I changed the circuit so that the button, A and B have 10 kΩ pull-ups on them, and then a RC filter (10 kΩ, 0.1 µF) between the signal and the GPIO pin on the microcontroller.

Displaying Info on LCD

To be clear, this section is interfacing with the LCD. How the system determines what text should be displayed will be discussed in the next section.

I really wanted to code everything from scratch, but I ran into a wall here. The problem is that I ordered this part from Amazon, and I couldn’t find much documentation for the product. Pretty much all the comments are “just use the Arduino library.” I did a little digging on my own and found a document that seemed helpful, but I couldn’t really get my code to work using that documentation. It’s possible I wasn’t following the document correctly, but since I wasn’t even sure if the document I found was even the right one for my LCD screen, I decided to swallow my pride and look for a third party library to use instead.

Matthias Hertel copyright notice

I’m using Matthias Hertel’s library. The link to his website is here. Originally, this code was written for the Arduino, so Matthias’ code uses Arduino libraries. Since I’m not using the Arduino IDE or libraries, I had to modify the code. The highlights are:

  • Removed references to Print.h (get rid of header and inheritance)
    • Define print in CPP file
    • write is no longer virtual method
  • Removed references to Arduino.h
    • Replace delayMicroseconds with _delay_us (from AVR library util/delay.h)
  • Removed references to Wire.h
    • In _write2Wire, replace calls to Wire with calls to PCF8574 class (which is based on HAL_TWI)
    • Remove references to Wire methods in set up
  • Remove delay in _sendNibble

Managing a Menu Interface

I’ve never written what I would consider to be a “good” user interface before. Every time I tried, it became spaghetti code. I could never really explain what I did to others, or to my future self, which made maintaining and expanding such messy code a disaster. I wanted to try to fix that with this project, so here goes.

I have two files for managing the menu interface: User_Interace.cpp and Screen.cpp. User_Interface.cpp is responsible for printing text to the LCD screen, and passing user input (from the encoder) to the current screen. Note that User_Interface.cpp is NOT responsible for figuring out what to print; it merely prints what it’s told to print. Likewise, User_Interface.cpp is NOT responsible for figuring out what actions to take based on a certain input; it merely passes the user input along to another part of the code. I designed the system this way because the text to print to the LCD, and what actions to take given user input is context dependent; that is to say, both of those things depend on what screen you’re on. For example, user input might scroll text, or it might change a value, depending on what screen you’re on.

That’s where Screen.cpp comes in. Screen.cpp is responsible for determining what text should be printed, and what to do with user input. Note that Screen.cpp isn’t responsible for actually printing text, and is only responsible for knowing what SHOULD be printed. Screen.cpp takes care of the context dependent part of the user interface; for some screens, rotating the encoder does nothing. But for other screens, rotating the encoder moves a cursor or changes a value. How do we manage that?

In my attempt to make the code maintainable and easy to expand, I’ve decided to use inheritance. Each screen (for example, main menu screen, adjust mode screen, adjust target value screen) will inherit from the base class Screen. Before looking at the code, let’s see how the user interface will look:

UI Screens

The VL screen shows the status of the variable load: voltage, current, power, and resistance. It also shows what mode the load regulator is in (OFF, CC, CP, CR or CV), as well as the target value (in amps, watts, ohms or volts). Lastly, the screen also shows the load’s temperature and fan speed. This is the screen on start-up. The short press on the encoder will enable or disable the load regulator. Long press will take you to the main menu screen, which will allow you to access all other screens. They are:

  • LR Mode screen: lets user select operating mode (OFF, CC, CP, CR, CV). If OFF is selected, then return to main menu. If any other mode is selected, then go to LR Val screen.
  • LR Val screen: allows user to adjust target value of selected mode. For example, in CC mode, allows user to select amount of current to flow through the load. If the current mode is OFF, then user it sent to LR Mode screen. After user has selected target value, return to main menu and update Load Regulator with new mode and / or target value.
  • TR Val screen: allows user to select fan speed, or let it be chosen automatically based on load temperature. After user has made a selection, return to main menu.
  • Info screen: displays information about the system. User does short press to return to main menu screen.

The image of the flow of the screens, as well as the list above shows that what the next screen is depends on the current screen. So Screen.cpp will need to tell User_Interface.cpp what the next screen should be.

Now that we know how the screens are interconnected, let’s look at the code and inheritance:

Screen class in header file

I’m only going to point out a few key points:

  • text is the variable that contains any and all text relevant to the screen. For example, the main menu is composed of 5 lines, which are “Back”, “Set mode”, “Set target”, “Fan control” and “Info”. These 5 strings will be stored in text.
  • update_screen_chars is what User_Interface calls to figure out what should be printed to the LCD screen. For example, since main menu is composed of 5 lines of text, but the LCD only has four lines, update_screen_chars will have to figure out what 4 lines should be printed. This is determined by using a cursor that moves around the screen.
  • update_text and handle_input are virtual functions. This means that these methods can be redefined by whatever class inherits from the Screen class. This is important because how text is updated, and how user input is handled, is screen dependent, so it makes sense that the derived classes override them.

Essentially, the base class provides basic methods used by all screens, as well as methods that User_Interface can call for all screens.

So now, two questions remain. How do you program a screen, and how do you jump between screens? Let’s look at the first question.

Here’s the code for the main menu screen:

Code for Main Menu screen

There’s three parts. First is the constructor; it just sets up some data members, and then loads defined strings into text (“Back”, “Set mode”, etc.). Next is re-defining update_text. For the main menu, the text doesn’t change, so there’s nothing to do here. “Back” is always going to be “Back”. Lastly, we re-define handle_input. Here, user input is passed in (CW, CCW, short push or long push), and a screen is returned. If no input is provided, then nothing happens and the method returns main menu screen. CW and CCW move the cursor around, and long press causes the method to return VL screen. If the user does a short press, then the code determines where the cursor is, and uses that to determine what screen should be returned.

But what about update_text? Let’s look at a second screen: TR Val screen.

update_text for TR Val screen

Above is the snippet of code for updating text. index is increased or decreased by rotating the encoder. The screen should display “AUTO” if index is 0, and then it should display “5%”, “10%”, etc. (up to 100%) based on the value of index. Since this requires the string to be displayed to change, update_text is not an empty function. Here, you can see that text is updated using strcpy and strcat.

Great! Now we know how to create a screen: make a constructor, then re-define the virtual functions. Now how do we jump between screens?

update_screen method in User_Interface

update_screen is the method main calls in User_Interface. This method is where User_Interface prints text to the LCD, passes input to the screen, and jumps between screens. Let’s focus on the last part first.

In the image above, the highlighted section shows how jumping between screens is done. Looks pretty simple, right? handle_input will return what the next screen should be, and this goes into a switch statement. Based on this, cur_screen, a pointer to the Screen class, is updated to point to VL screen, or the main menu screen, etc. One nuance I’d like to point out, however, is that cur_screen is a pointer to the base class, not the derived class (VL_Screen, Info_Screen, etc.). This is another advantage of using inheritance. If you wanted to have a single pointer to point to an integer, float, and character, you would constantly have to type-cast it. However, since all screens derive from the base class, you don’t have to type-cast cur_screen since it is a pointer to the base class. This makes jumping between screens straight forward!

We’re almost done. By now, we have Screen.cpp which handles the flow chart of screens, as well as handling user input. Now all that’s left is actually printing strings to the LCD. Should be pretty straight forward, right? For example, for the main menu, all you have to do is print “Back”, then “Set mode”, etc. and then exit the function, right? Unfortunately not.

Remember that the LCD is hooked up to the microcontroller through I2C / TWI. The current monitor ADC is also on this bus, and it must be accessed at 60 Hz, or every 16.7 ms. Printing a whole screen to the LCD takes way longer than 17 ms, so if we were to bulk print to the LCD, then we would prevent the system from accessing the current monitor ADC, reducing performance. In order to prevent this, every time update_screen is called, only one character is printed to the LCD. Then, when update_screen is called again, the next character is printed. This way, communication to the ADC and LCD are interleaved, preventing one from interrupting or interfering with the other. I used a logic analyzer and confirmed that this was the case:

Markers (A1, A2, B1, etc.) indicate reading ADC

As you can see above, when the LCD screen is not being updated, the I2C bus is solely used to read the ADC every 17 ms. However, when the LCD screen needs to be updated, the microcontroller does both at the same time. The ADC is still read every 17 ms, as indicated by the markers. Besides this, however, when the I2C bus isn’t being used, the microcontroller fills the unused time to communicate with the LCD, updating the screen.

One very last thing. I tried clearing the screen every second or so in order to update the text on the LCD screen, but I noticed that causes the screen to flicker. Since this is very annoying, I decided to just fill up the 20 x 4 character array each time, which means that all transactions will be for 80 characters. This increases how long it takes to write a single screen, but as the image above shows, the length of time to write a screen isn’t a problem since it doesn’t interfere with the ADC.

Thanks for reading through this long post. Hopefully this gives insight into how the user interface works!

Leave a comment

Design a site like this with WordPress.com
Get started