Programming: Arduino
Check out my other articles:
- Galaga: Failed Attempt at writing a Game
- Sudoku
- Blockade / Snake
- Farkle
- Minesweeper
- Blackjack
- ADC Graph
ADC Graph
After writing a few games I thought it was time to write something a little more useful and an ADC Graph application seemed like the perfect idea. Inspired by the oscilloscope project that comes with the SmartGPU2 sample code, I set about to build something that was both useful and took advantage of the SmartGPU2's screen. The end result works on both the 3.5" 480 x 320 and 4.3" 420 x 272 screens.
The Arduino UNO comes equipped with four 10bit analogue to digital converters that convert a signal between 0V and 5V to a corresponding number between 0 and 1023. More advanced Arduino models sport better analogue to digital features than the Uno - the Mega comes with 16 x 10bit ADCs whereas the Due comes with 12 x 12bit ADCs (where 12bit provides a range from 0 to 4095). The code below code easily be converted to support these models if needed ..
ADC Graph in Action
Right from the start I wanted the application to be configurable - allowing the user to select the number of series to display and to be able to adjust key components of the graph overall and the series individually. When the application starts, the user is presented with a setup screen as shown below.
The screen is split into two major sections that allow configuration of the graph itself (on the right) and up to four data series (on the left). The series correspond to the analogue pins 0 - 3 on the Arduino and can be enabled or disabled by clicking the checkbox adjacent to the series name. The series name can be altered by selecting the heading, the colour changed and the graph type changed between a bar or a line graph.
When altering the series name, a keyboard pops up allowing entry. This functionality is based on the keyboard sketch provided in the sample code with the SmartGPU2. I have changed the code to present the keyboard as a pop-up as well as fixed a few minor bugs.
Once configured, the data collection and graphing can be launched by clicking the OK button. Depending on the options chosen, a legend will be draw on the right hand side of the screen with details of each series - including the input voltage and digital equivalent value between 0 and 1023 - updated in real time.
A single graph can have a combination of bar and line graphs, as shown below. Optionally, the data can be offset by 2.5V in the Y axis effectively changing the scale from -512 to 512.
Code Fragments
The following code highlights areas of interest in the application. It is not intended to be a tutorial or a complete solution. I have provided the complete code for download and you may use or modify it as required.
A Better Mouse Trap?
The SmartGPU2 library comes with a collection of GUI components - checkboxes, sliders and so forth - so why did I feel the need to rewrite some of them? The answer is that the standard controls all render in white and I wanted to be able to render them in grey to indicate that they were disabled. Secondly, the control set does not include a radio button which I wanted to represent a set of mutually exclusive options and - finally - the provided checkbox does not render well at its smallest size (16 x 16 pixels).
The screen images above show the effect that I was after. When an option (Sereis 3 and 4 in the left hand image) is selected the sub options are rendered in a light colour to indicate they too are available for selection. When the option is deslected, the sub options are greyed out and unavailable to select.
A Better Checkbox
I decided to make my controls fixed sized - unlike the SmartGPU2 versions that are scalable - and this made the task really simple, as shown below. The draw_objCheckbox() function accepts parameters for the upper left coordinates, the rendering colour and the checkbox state. The function draws the outer square (lines 003 - 004) and if selected the check mark (lines 008 - 014). If the checkbox is unchecked, the centre of the square is cleared to black (line 019 - 020) allowing the checkbox to be redrawn over itself when its state is changed.
001 002 003 004 005 006 007 008 009 010 011 012 013 014 015 016 017 018 019 020 021 022 023 |
void draw_objCheckbox(int x, int y, int colour, ACTIVE isActive) { lcd.drawRectangle(x + 1, y + 1, x + CHK_SIZE - 1, y + CHK_SIZE - 1, colour, UNFILL); if (isActive == SELECTED) { lcd.drawLine(x +4, y +3,x + CHK_SIZE -3, y + CHK_SIZE -4,colour); lcd.drawLine(x +3, y +4,x + CHK_SIZE -4, y + CHK_SIZE -3,colour); lcd.drawLine(x +4, y +4,x + CHK_SIZE -4, y + CHK_SIZE -4,colour); lcd.drawLine(x +3, y + CHK_SIZE -4, x + CHK_SIZE -4, y +3, colour); lcd.drawLine(x +4, y + CHK_SIZE -3, x + CHK_SIZE -3, y +4, colour); lcd.drawLine(x +4, y + CHK_SIZE -4, x + CHK_SIZE -4, y +4, colour); } else { lcd.drawRectangle(x + 3, y + 3, x + CHK_SIZE - 3, y + CHK_SIZE - 3, BLACK, FILL); } } |
Detecting that the checkbox was clicked and toggling the value can be performed by the code below. The checkbox state is stored in a variable of type ACTIVE (line 001) which can hold a value of SELECTED or DESELECTED. If the checkbox is clicked, the value is toggled (line 005) and the object redraw (line 006).
001 002 003 004 005 006 007 008 |
ACTIVE chkValue = SELECTED; if (checkbox clicked) { chkValue = (chkValue == SELECTED ? DESELECTED : SELECTED); draw_objCheckbox(x, y, chkValue); } |
A Better Radio Button
The draw_objRadioButton is simplicity itself. The outer circle is draw in the selected colour but not filled - the inner circle is drawn in a solid colour - either the nominated colour or black - depending on the provided state. Like the draw_objCheckbox() function, this object can be drawn over itself as its state is changed.
001 002 003 004 005 006 007 |
void draw_objRadioButton(int x, int y, int colour, ACTIVE isActive) { lcd.drawCircle(x + 8, y + 8, 8, colour, UNFILL); lcd.drawCircle(x + 8, y + 8, 5, (isActive == SELECTED ? colour : BLACK), FILL); } |
Sample code to render and update a group of radio buttons is shown below. Lines 003 - 009 defines a function to render a set of radio buttons whose value is stored in the value optValue. When rendering each radio button, the value of the variable is compared to its ordinal value in the group to determine whether to render the radio button in a selected or deselected state
When a radio button is clicked, the optValue variable is updated with the index of the radio button in the group and the group itself redrawn.
001 002 003 004 005 006 007 008 009 010 011 012 013 |
int optValue = 0; void draw_objRadioButtons() { draw_objRadioButton(x0, y0, (optValue == 0, SELECTED, DESELECTED); draw_objRadioButton(x1, y1, (optValue == 1, SELECTED, DESELECTED); draw_objRadioButton(x2, y2, (optValue == 2, SELECTED, DESELECTED); } if (radio button 0 clicked) { optValue = 0; draw_objRadioButtons(); } if (radio button 1 clicked) { optValue = 1; draw_objRadioButtons(); } if (radio button 2 clicked) { optValue = 2; draw_objRadioButtons(); } |
A Better Colour Picker
The draw_objColourPicker is a control I made up that allows a user to select a colour from a palette. It is simply rendered as an outer border with a solid, colour swab in the middle. When clicked, the colour selection palette is revealed (see below).
001 002 003 004 005 006 007 |
void draw_objColourPicker(int x, int y, int border, int selected) { lcd.drawRectangle(x, y, x + CHK_SIZE, y + CHK_SIZE, border, UNFILL); lcd.drawRectangle(x + 2, y + 2, x + CHK_SIZE - 2, y + CHK_SIZE - 2, selected, FILL); } |
The colour picker displays a palette of colours in a 4 x 3 grid with the existing, nominated colour pre-selected. The chooseColour() function is invoked when one of the four colour pickers are selected. In addition to the display coordinates the series number is passed and this is used to determine what colour to highlight initially (lines 014 - 021). The colour swabs are rendered in lines 026 to 043 and then the selected cells is highlighted in lines 048 to 052 via a call to the highlightColour() function (lines 001 - 006).
The code then loops waiting for the user to select a colour (lines 054 - 079) before breaking out of the loop. Finally, the chosen colour is reassigned to the correct series variable (lines 081 - 088) before returning.
001 002 003 004 005 006 007 008 009 010 011 012 013 014 015 016 017 018 019 020 021 022 023 024 025 026 027 028 029 030 031 032 033 034 035 036 037 038 039 040 041 042 043 044 045 046 047 048 049 050 051 052 053 054 055 056 057 058 059 060 061 062 063 064 065 066 067 068 069 070 071 072 073 074 075 076 077 078 079 080 081 082 083 084 085 086 087 088 089 090 |
void highlightColour(int x, int y) { lcd.drawRectangle(x - 3, y - 3, x + 25, y + 25, GREY6, UNFILL); lcd.drawRectangle(x - 4, y - 4, x + 26, y + 26, GREY6, UNFILL); } void chooseColour(int x, int y, int series) { int selectedColour = 0; point.x = 0; point.y = 0; switch (series) { case 1: selectedColour = series1_Colour; break; case 2: selectedColour = series2_Colour; break; case 3: selectedColour = series3_Colour; break; case 4: selectedColour = series4_Colour; break; } // Draw border and drop shadow .. lcd.drawRectangle(x, y, x + 128, y + 98, BLACK, FILL); lcd.drawRectangle(x, y, x + 128, y + 98, GREY6, UNFILL); lcd.drawRectangle(x + 129, y + 3, x + 131, y + 98 + 3, GREY3, FILL); lcd.drawRectangle(x + 3, y + 99, x + 131, y + 101, GREY3, FILL); // Draw first row of colours .. lcd.drawRectangle(x + 8, y + 8, x + 30, y + 30, WHITE, FILL); lcd.drawRectangle(x + 38, y + 8, x + 60, y + 30, RED, FILL); lcd.drawRectangle(x + 68, y + 8, x + 90, y + 30, GREEN, FILL); lcd.drawRectangle(x + 98, y + 8, x + 120, y + 30, BLUE, FILL); // Draw second row of colours .. lcd.drawRectangle(x + 8, y + 38, x + 30, y + 60, YELLOW, FILL); .. // Highlight the selected colour .. if (selectedColour == WHITE) { highlightColour(x + 8, y + 8); } if (selectedColour == RED) { highlightColour(x + 38, y + 8); } if (selectedColour == GREEN) { highlightColour(x + 68, y + 8); } if (selectedColour == BLUE) { highlightColour(x + 98, y + 8); } ... while (true) { if (lcd.touchScreen(&point) == VALID) { if (point.x >= x + 8 && point.x <= x + 30 && point.y >= y + 8 && point.y <= y + 30) { selectedColour = WHITE; break; } else if (point.x >= x + 38 && point.x <= x + 60 && point.y >= y + 8 && point.y <= y + 30) { selectedColour = RED; break; } else if (point.x >= x + 68 && point.x <= x + 90 && point.y >= y + 8 && point.y <= y + 30) { selectedColour = GREEN; break; } else if .. } delay(100); } switch (series) { case 1: series1_Colour = selectedColour; break; case 2: series2_Colour = selectedColour; break; case 3: series3_Colour = selectedColour; break; case 4: series4_Colour = selectedColour; break; } } |
As with the checkbox and radio button, the code for drawing the colour pickers is simple with all four controls being refreshed simultaneously. When the user selects any of the colour pickers, they are presented with the selector and on exit the four controls are refreshed. Simple.
001 002 003 004 005 006 007 008 009 010 011 012 013 014 015 016 017 018 |
int series1_Colour = RED; int series2_Colour = BLUE; int series3_Colour = GREEN; int series4_Colour = YELLOW; void drawPickers() { draw_objColourPicker(x1, y1, GREY7, series1_Colour); draw_objColourPicker(x2, y2, GREY7, series2_Colour); draw_objColourPicker(x3, y3, GREY7, series3_Colour); draw_objColourPicker(x4, y4, GREY7, series4_Colour); } if (colour selector 1 checked) { chooseColour(x, y, 1); drawPickers(); } if (colour selector 2 checked) { chooseColour(x, y, 2); drawPickers(); } if (colour selector 3 checked) { chooseColour(x, y, 3); drawPickers(); } if (colour selector 4 checked) { chooseColour(x, y, 4); drawPickers(); } |
Possible Improvements
Where to start as there are so many possible improvements that could be made .. the code is quite complete but could do with many enhancements. If anyone wants to complete any of the ideas below, I am happy to incorporate them into the distributable with full credits to you!
- Save the configuration to the EEPROM. Obviously if someone was using this code for a real application (!) they would want the configuration they set initially to be applied after a restart. A quick look at the code and accompanying code sampled in Sudoku game will show you how to do this. Additional code in the Farkle code shows how to save and restore string / character arrays to the EEPROM.
- Allow the Y scale to be adjusted. I have assumed that signals will take advantage of the full 0V to 5V range however you may be measuring smaller signals that range from say 0V to 3V. The ability to adjust the scale of the Y axis would allow these signals to be rendered across the full height of the screen.
- Provide two rendering patterns - the existing approach which repaints the graph while shifting it to the left for each new data point or update the screen as an oscilloscope does by progressively updating the screen from left to right as data is read. This approach can be seen in the oscilloscope project that comes with the SmartGPU2 sample code. This second approach removes a lot of the flashing as the screen is repainted as only a small section of the screen is refreshed each time.
Wiring Diagram
The diagram below show the wiring diagram when using a standard Arduino Uno (or compatible) and the SmartGPU2. Click on the image to see a detailed drawing.
Downloads
Feel free to download, modify and adapt this code to your needs. Please credit me if you use some or all of this code in your own project. If you alter the code to fix bugs or add functionality, please submit them back to me and I will add them to the download.
Arduino Source Code |