Edits: 15/6/19 - embedded YouTube video rather than just have a link.
9/7/19 - updated link to part 10
Whilst waiting for the new PCBs to be manufactured and delivered, I started on the Interface for monitoring the supply. I prototyped this in Part Seven, so I know the fundamental code works. To create the interface I want requires the use of 4D Systems Workshop 4 IDE so that I can make use of the graphical environment.
I’ve added a video of the working interface below, but first it’s worth writing about my experience with the 4Duino and Workshop 4.
Workshop 4 IDE
I use an iMac, so first off this isn’t going to work natively. It turns out it won’t run under Crossover (Wine) either so in order to install, I had to first install a Virtualisation tool. Reading through their forum, it would appear that some people have had issues with VirtualBox and its handling of USB connected devices so I ended up with Parallels - at least I can work without actually licensing Windows 10.
With that out of the way, installing Workshop 4 is easy enough. For the 4Duino, there are two options: Basic Graphics and Extended Graphics. Basic allows you to write a program using the Picaso processor graphical primitives - which is what you would do if using the Arduino IDE. Extended Graphics allows you to use built-in widgets to design a UI, create skeleton code, and then write the rest of the code around it - very similar to other graphical-based IDEs.
At least, that’s the theory; mostly, it’s very frustrating: the documentation is poor and what information there is, is scattered across multiple PDFs, example projects and forum postings. Ultimately, I had to scratch my head and figure it out and it took two days really to get what you see in the video. I've been programming since the early 1980s.
The Extended Graphics environment is really an image creator. To give you an example. I started with a gauge widget, similar to a car’s speedometer with the mileage trip meter at the bottom, to display values from 0V to 16V in mV increments. Drag and drop it on the screen, change some colours, set the value range etc - things you'd expect to do. From this, Extended Graphics creates 16,000 images - at runtime it would use the value used to display to pull the right image of the SD card! Crazy. That’s when you figure out what might be happening under the covers - there’s no documentation describing any of this!
The properties you can set on the widgets are very limited as well: you have no ability to change values at runtime, e.g. with something like aLabelWidget.setTextColor(RED). If you want red, set it at design time and live with it. If you want green as well, add another widget directly on top that is green and toggle the display between the red and green in code. If it allows you to use a different colour of course: for example, custom digits (the object you see in the screenshot) that are orange? Forget it - the widget doesn’t allow you to set colours so like Henry Ford, you can have any colour you like as long as it’s green! Decimal Points? Forget it - you have to drop a units widget, a static text string for the DP, and a fractions widget (see, for example, the Power and temperature digits in the screenshot)! You could use a different digits widget (LEDDigits - see the Volt and Amp values) that does allow you to change its colour and DP - but it has a minimum size below which it won’t display correctly and that minimum size is quite large. Also, you can’t ask it to display a float - you have to ask it to display an Int where you ‘know’ where the DP is. So for 12.345mV ask it to show 12345.
I could (and probably should) just have used text with fonts and colours but gosh darnit I wanted my money's worth! And I quite liked the LED digit look.
Figuring this out is a matter of trial-and-error and looking at other examples. There’s little to no documentation on these. If you want to create your own widgets, you need Workshop 4 Pro which is a (not cheap) paid upgrade. Or draw them yourself from primitives or as static images in a different image editor.
These graphical images, then, are created and stored on a microSD card. The starter kit comes with a 4GB microSD card - the max size allowed - but you must connect this direct to your computer, you can’t upload images via the 4Duino. They do not provide a SD Adapter in the starter kit so if you don’t have one, there’s more money to fork out.
The skeleton code it generates is minimal and, again, leaves you scratching your head - did I mention none of this is documented? Here’s an example: you want to use custom digits - this is the code you get: LedDigitsDisplay(Display, hndl, numx, iiCustomdigits4, 262, 3, 1, 9, 0) ; Go figure what this means (the first 4 parameters are easy enough to figure out) - the IDE won’t find that function for you so you have to dig around the file system to find it (when you do at least breathe a sigh of relief that there is a comment to tell you what the parameters mean.) You can’t name these widgets. In the design tool you can provide an alias name but it isn’t used in the code itself - fortunately it does add it as a comment at least. But yeh, don’t work with a variable caseTemperature, work with iiCustomdigits4.
It comes with quirks you need to figure out: the compiler will deal with “%%Display%%.DefineResetLine ;” but not “%%Display%%.DefineResetLine;”. It will also throw errors for no clear reason that, after a couple of hours, you guess at forcing a recreation of the widgets, even though they haven't changed, because you’ve tried everything else.
The code editor is pretty frustrating to use as well. It’s not like any other IDE you might be used to. You can write code, and it does colour it, but there doesn’t appear to be any in-built syntax checking, code completion, function lookup, etc etc. You know, basic useful features you’d think would be standard in the 21st century.
So could I recommend it?
If you’re an experienced programmer you will work it out based on problem fixing gleaned over years; if you’re not, I think you will struggle with this and it will depend on how motivated you are to not give up and just use a bog standard text interface (for which cheaper solutions exist.) The product is a good one - built in touch LCD, wifi, SD reader - but let down by extremely poor documentation and getting starting guides. Workshop 4 is not a great tool, Extended Graphics isn’t anywhere near as good as they make out on their website, and their other programming environments aren’t available yet on the 4Duino.
I got there, but it shouldn’t have taken me 2 days to create what you see below, especially as I only really needed to create the UI based on the prototype I’d created earlier.
Interface in Action
I’ve created this video to show it working. Some notes:
- iMon current display is not connected with the Amp current display as I have no LT3081 wired up, I’m just using a trimmer to change the voltage between 0V and 600mV (which would represent 3A)
- Similarly, the LT3081 temperatures aren’t connected, the code is reading a false analog value from their relevant pin and displaying it. But it gives you an idea.
- My code only reads the temperatures once a second (but V, A and P continuously) so they are a little slower to update on the video. I did this on purpose to prioritise Voltage and Amperage updates.
- The temperatures that cause the orange/red boxes to appear around a reading (for warning and overheating) are arbitrary low for demonstration purpose. The LTs displayed temperature (from false analog readings) are high enough to have a permanent red box around them; in the video you will see the Mosfet rise high enough to get an orange box around it (27C).
- The meter image and thermometer image are static.
- I have a 10Ohm 100W resistor to use as a load, but my other components are very low wattage (I know, I let the smoke out of two trimmers accidentally!) So the Amp range is very limited as I can’t drive the power load through the components I have (I’ve already let the smoke out of two trimmers accidentally!)
I will need to tweak the code as I get the build up and running. I’m thinking that the Arduino analog reading for iMon will be too low resolution for a start, but I’m also thinking about the Volts and Amps display - you can see them drifting around. I’m wondering whether I should put some hysteresis allowance in the code but this would make the updates ‘slow’ when either are being changed through the 10-turn potentiometers: I’d definitely be interested in some ideas/help on this. They may be more stable in the proper build of course because I expect the trimmer I’m using on the breadboard isn’t the greatest quality.
Anyway, here it is (I couldn't work out how to embed the you tube video I'm afraid.) I sound awful.
The Code
I've put this up on Github - the whole Workshop 4 project - and just the code file. It's also below in all its glory.
// // NB! This is a file generated from the .4Dino file, changes will be lost // the next time the .4Dino file is built // #define RESETLINE 30 #define DisplaySerial Serial1 #include "YapsGraphicalConst.h" #include "Picaso_Serial_4DLib.h" #include "Picaso_LedDigitsDisplay.h" #include "Picaso_Const4D.h" Picaso_Serial_4DLib Display(&DisplaySerial); #include <Wire.h> // For SDA/SCL connections #define THERMISTORNOMINAL 10000.0 // resistance at 25C #define TEMPERATURENOMINAL 25.0 // degrees centigrade #define NUMSAMPLES 5 // Improve calculation efficiency by averaging reads over a number of samples #define IMONSCALINGFACTOR 200.0 // 200uA drawn per amp #define HIGHTEMPLIMIT 80.0 // Limit in Centigrade at which things may be getting too hot. #define WARNINGTEMPLIMIT 100.0 // Limit in Centigrade at which things need to be monitored. // Analog connections - references in schematic #define CASETHERMISTOR A0 #define RECTIFIERTHERMISTOR A1 #define MOSFETTHERMISTOR A2 #define LT3081U3TEMP A3 #define LT3081U4TEMP A4 #define IMONCURRENT A5 // Resistors - measured with DMM - references in schematic, update on final build #define CASERESISTOR 9940 #define RECTIFIERRESISTOR 10060 #define MOSFETRESISTOR 9750 word hndl; unsigned long timeLast = 0; // time thermistors were last read const unsigned long period = 1000; // delay between reads of thermistors void setup() { pinMode(RESETLINE, OUTPUT); // Display reset pin digitalWrite(RESETLINE, 1); // Reset Display, using shield delay(100); // wait for it to be recognised digitalWrite(RESETLINE, 0); // Release Display Reset, using shield delay(3000); // give display time to startup // now start display as Serial lines should have 'stabilised' DisplaySerial.begin(200000) ; // Hardware serial to Display, same as SPE on display is set to Display.TimeLimit4D = 5000 ; // 5 second timeout on all commands Display.Callback4D = mycallback ; Display.gfx_ScreenMode(LANDSCAPE); // change manually if orientation change // Look for SD card and mount Display.putstr("Mounting...\n"); if (!(Display.file_Mount())) { while(!(Display.file_Mount())) { Display.putstr("Drive not mounted..."); delay(200); Display.gfx_Cls(); delay(200); } } hndl = Display.file_LoadImageControl("YAPS~62A.dat", "YAPS~62A.gci", 1); // put your setup code here, to run once: initialiseDisplay(); setupINA260(); } // end Setup **do not alter, remove or duplicate this line** void loop() { // Display VIP from INA260 and the LT3081 reported current DisplayVoltage(); DisplayCurrent(); DisplayiMonCurrent(); DisplayPower(); // Display all temperatures only once a period to prioritse VIP unsigned long timeNow = millis(); if (timeNow - timeLast >= period) { DisplayCaseTemperature(); DisplayRectifierTemperature(); DisplayMosfetTemperature(); DisplayLT3081U3Temperature(); DisplayLT3081U4Temperature(); timeLast = timeNow; } } /* ************************************************************************** Display Functions ************************************************************************** */ /* Reads the Case thermistor, calculates a temperature and displays the value */ void DisplayCaseTemperature() { float reading; float resistance; float temperature; // Obtain reading from thermistor and convert to temperature reading = ReadAnalog(CASETHERMISTOR); resistance = CASERESISTOR * (1024.0 / reading - 1.0); temperature = Temperature(resistance); int units = temperature; // drop the fraction element int fractions = (temperature - units) * 100; // convert fractions into units LedDigitsDisplay(Display, hndl, units, iiCustomdigits6, 105, 3, 1, 9, 0) ; // CaseTempUnits LedDigitsDisplay(Display, hndl, fractions, iiCustomdigits8, 137, 2, 1, 9, 0) ; // CaseTempFractions FlagTemperatureIfNecessary(temperature, 141, 35, 125); } /* Reads the Rectifier thermistor, calculates a temperature and displays the value */ void DisplayRectifierTemperature() { float reading; float resistance; float temperature; // Obtain reading from thermistor and convert to temperature reading = ReadAnalog(RECTIFIERTHERMISTOR); resistance = RECTIFIERRESISTOR * (1024.0 / reading - 1.0); temperature = Temperature(resistance); int units = temperature; // drop the fraction element int fractions = (temperature - units) * 100; // convert fractions into units LedDigitsDisplay(Display, hndl, units, iiCustomdigits5, 105, 3, 1, 9, 0) ; // RectifierTempUnits LedDigitsDisplay(Display, hndl, fractions, iiCustomdigits9, 137, 2, 1, 9, 0) ; // RectifierTempFractions FlagTemperatureIfNecessary(temperature, 173, 35, 125); } /* Reads the Mosfet thermistor, calculates a temperature and displays the value */ void DisplayMosfetTemperature() { float reading; float resistance; float temperature; // Obtain reading from thermistor and convert to temperature reading = ReadAnalog(MOSFETTHERMISTOR); resistance = MOSFETRESISTOR * (1024.0 / reading - 1.0); temperature = Temperature(resistance); int units = temperature; // drop the fraction element int fractions = (temperature - units) * 100; // convert fractions into units LedDigitsDisplay(Display, hndl, units, iiCustomdigits7, 105, 3, 1, 9, 0) ; // MosfetTempUnits LedDigitsDisplay(Display, hndl, fractions, iiCustomdigits10, 137, 2, 1, 9, 0) ; // MosfetTempFractions FlagTemperatureIfNecessary(temperature, 205, 35, 125); } /* Reads the LT3081 Voltage, converts to a temperature and displays the value */ void DisplayLT3081U3Temperature() { int reading = ReadAnalog(LT3081U3TEMP); // Should return a value somewhere between 0 and 306.9 (representing 0 - 1.5V) float vOut = reading * (5.0 / 1023.0); // Convert to a voltage between 0.01mV and 5V (actually 1.5V max) float temperature = vOut * 100.0; // Convert to temperature value 10mV = 1C int units = temperature; // drop the fraction element int fractions = (temperature - units) * 100; // convert fractions into units LedDigitsDisplay(Display, hndl, units, iiCustomdigits12, 263, 3, 1, 9, 0) ; // LT3081U3TempUnits LedDigitsDisplay(Display, hndl, fractions, iiCustomdigits11, 295, 2, 1, 9, 0) ; // LT3081U3TempFractions FlagTemperatureIfNecessary(temperature, 141, 170, 148); } void DisplayLT3081U4Temperature() { int reading = ReadAnalog(LT3081U4TEMP); // Should return a value somewhere between 0 and 306.9 (representing 0 - 1.5V) float vOut = reading * (5.0 / 1023.0); // Convert to a voltage between 0.01mV and 5V (actually 1.5V max) float temperature = vOut * 100.0; // Convert to temperature value 10mV = 1C int units = temperature; // drop the fraction element int fractions = (temperature - units) * 100; // convert fractions into units LedDigitsDisplay(Display, hndl, units, iiCustomdigits13, 263, 3, 1, 9, 0) ; // LT3081U4TempUnits LedDigitsDisplay(Display, hndl, fractions, iiCustomdigits14, 295, 2, 1, 9, 0) ; // LT3081U4TempFractions FlagTemperatureIfNecessary(temperature, 173, 170, 148); } /* Reads the iMon voltage, converts to a current and displays the value */ void DisplayiMonCurrent() { int reading = ReadAnalog(IMONCURRENT); // Should return a value somewhere between 0 and 123 (representing 0 - 600mV) float vOut = reading * (5.0 / 1023.0); // Convert to a voltage between 0 and 5V. float current = (vOut / IMONSCALINGFACTOR) * 1000.0; // Convert to current value int units = current; LedDigitsDisplay(Display, hndl, units, iiCustomdigits3, 248, 1, 1, 9, 0) ; // iMonUnits int fractions = (current - units) * 1000; LedDigitsDisplay(Display, hndl, fractions, iiCustomdigits4, 262, 3, 1, 9, 0) ; // iMonFractions } /* Draw a coloured rectangle if the temperature needs to be flagged */ void FlagTemperatureIfNecessary(float temperature, word top, word left, word width) { word colour; if (temperature >= HIGHTEMPLIMIT) { colour = RED; } else if (temperature >= WARNINGTEMPLIMIT) { colour = ORANGE; } else { colour = BLACK; } Display.gfx_Rectangle(left, top, left + width, top + 25, colour); } /* ************************************************************************** Analog reading Functions ************************************************************************** */ /* * Obtain the actual Vcc value of the 4Duino from an internal reference source. * The nominal 5V Vin can fluctuate a lot and affect analog read accuracy. */ long readVCC() { long result; // Read 1.1V reference against AVcc ADMUX = _BV(REFS0) | _BV(MUX4) | _BV(MUX3) | _BV(MUX2) | _BV(MUX1); delay(2); // Wait for Vref to settle ADCSRA |= _BV(ADSC); // Convert while (bit_is_set(ADCSRA, ADSC)); result = ADCL; result |= ADCH << 8; result = 1126400L / result; // Calculate Vcc (in mV); 1126400 = 1.1*1024*1000 return result; } /* Return an averaged reading from the Analog pin */ float ReadAnalog(int pin) { uint8_t i; float average; float samples[NUMSAMPLES]; for (i = 0; i < NUMSAMPLES; i++) { int value = analogRead(pin); float supply = readVCC() / 1000.0; // obtain vcc at time of read float valueCorrected = supply / 5 * value; // improve the accuracy of the analog reading samples[i] = valueCorrected; delay(10); // spread readings out otherwise they are likely to be the same value or very close } average = 0; for (i = 0; i < NUMSAMPLES; i++) { average += samples[i]; } return average / NUMSAMPLES; } /* ************************************************************************** SDA/SCL Reading Functions ************************************************************************** */ void DisplayVoltage() { double dVoltage = 0.0; // ask for voltage readings Wire.beginTransmission(0x40); // Address Wire.write(0x02); // Bus Voltage Register Wire.endTransmission(); dVoltage = ReadData(1.25); // Voltage is accurate to 1.25V // Display int units = dVoltage * 1000; // Extended Graphics digits only displays units with an implied DP if (units < 0) { units = 0; } LedDigitsDisplay(Display, hndl, units, iLeddigits1+1, 8, 5, 4, 28, 1) ; // Volts } void DisplayCurrent() { double dCurrent = 0.0; // ask for current readings Wire.beginTransmission(0x40); // Address Wire.write(0x01); // Bus Voltage Register Wire.endTransmission(); dCurrent = ReadData(1.25); // Current is accurate to 1.25mA // Display int units = dCurrent * 1000; // Extended Graphics digits only displays units with an implied DP if (units < 0) { units = 0; } LedDigitsDisplay(Display, hndl, units, iLeddigits2+1, 203, 4, 4, 28, 0) ; // Amps } void DisplayPower() { double dPower = 0.0; // ask for power readings Wire.beginTransmission(0x40); // Address Wire.write(0x03); // Bus Voltage Register Wire.endTransmission(); dPower = ReadData(10.0); // Power is accurate to 10mW // Display - custom digits only displays whole numbers with no DP so need to split int units = dPower; // drop the fraction element int fractions = (dPower - units) * 1000; // convert fractions into units LedDigitsDisplay(Display, hndl, units, iiCustomdigits1, 64, 2, 1, 9, 0) ; // PowerUnits LedDigitsDisplay(Display, hndl, fractions, iiCustomdigits2, 87, 3, 1, 9, 0) ; // PowerFractions } /* * Read NUMSAMPLES values from the preset register (for V, I or P) and average them. * The LSB size represents the LSB size as stated in the datasheet. */ double ReadData(float lsbSize) { long total = 0; int value = 0; double dValue = 0.0; total = 0; for(int i = 0; i < NUMSAMPLES; i++){ Wire.requestFrom(0x40, 2); // Read two bytes delay(5); if (2 <= Wire.available()) { value = Wire.read(); // receive high byte (overwrites previous reading) value = value << 8; // shift high byte to be high 8 bits value |= Wire.read(); // receive low byte as lower 8 bits total += value; } } // Turn into high precision, average them and convert to result dValue = total; dValue = ((dValue * lsbSize) / 5.000) / 1000.000; return dValue; } /* ************************************************************************** Calculation Functions ************************************************************************** */ /* Calculate the temperature based on the Steinhart-Hart equation. Coefficients are from the datasheet An alternative would be to hold a table of resistances/temperatures from the datasheet. Must consider memory usage. */ float Temperature(float r2) { float temperature; double coeffA = 3.354016e-03; double coeffB = 2.541522e-04; double coeffC = 3.730922e-06; double coeffD = -7.881561e-08; double logR2 = 0.0; logR2 = log(r2/THERMISTORNOMINAL); temperature = (1.0 / (coeffA + coeffB*logR2 + coeffC*logR2*logR2 + coeffD*logR2*logR2*logR2)); temperature -= 273.15; return temperature; } /* ************************************************************************** Supporting Functions ************************************************************************** */ /* Start up the display within initial graphics and values. */ void initialiseDisplay() { Display.gfx_Cls(); Display.img_Show(hndl,iStatictext1); // VoltText Display.img_SetWord(hndl, iMeter1, IMAGE_INDEX, 5); // where frame is 0 to 16 (for a displayed 0 to 16) Display.img_Show(hndl,iMeter1); // Meter image Display.img_Show(hndl,iStatictext3); // AmpText Display.img_Show(hndl, iLeddigits1); // Volts show all digits at 0, only do this once Display.img_Show(hndl, iLeddigits2); // Amps show all digits at 0, only do this once Display.gfx_Line(2, 100, 318, 100, SLATEGRAY); // Line1 Display.img_Show(hndl,iStatictext4); // PowerText Display.img_Show(hndl, iCustomdigits1); // PowerUnits show all digits at 0, only do this once Display.img_Show(hndl,iStatictext5); // PowerDP Display.img_Show(hndl, iCustomdigits2); // PowerFractions show all digits at 0, only do this once Display.img_Show(hndl,iStatictext6); // WattsText Display.img_Show(hndl,iStatictext7); // iMonText Display.img_Show(hndl, iCustomdigits3); // iMonUnits show all digits at 0, only do this once Display.img_Show(hndl,iStatictext8); // iMonDP Display.img_Show(hndl, iCustomdigits4); // iMonFractions show all digits at 0, only do this once Display.img_Show(hndl,iStatictext9); // iMonAmpText Display.gfx_Line(2, 127, 318, 127, SLATEGRAY); // Line2 Display.img_SetWord(hndl, iThermometer1, IMAGE_INDEX, 80); // where frame is 0 to 140 (for a displayed -1 to -1) Display.img_Show(hndl,iStatictext2); // CaseCText Display.img_Show(hndl,iThermometer1); // Thermometer1 Display.img_Show(hndl,iStatictext10); // CaseText Display.img_Show(hndl, iCustomdigits6); // CaseTempUnits show all digits at 0, only do this once Display.img_Show(hndl,iStatictext15); // CaseDP Display.img_Show(hndl, iCustomdigits8); // CaseTempFractions show all digits at 0, only do this once Display.img_Show(hndl,iStatictext11); // RectifierText Display.img_Show(hndl, iCustomdigits5); // RectifierTempUnits show all digits at 0, only do this once Display.img_Show(hndl,iStatictext16); // RectifierDP Display.img_Show(hndl, iCustomdigits9); // RectifierTempFractions show all digits at 0, only do this once Display.img_Show(hndl,iStatictext12); // MosfetText: Display.img_Show(hndl, iCustomdigits7); // MosfetTempUnits show all digits at 0, only do this once Display.img_Show(hndl,iStatictext17); // MosfetDP Display.img_Show(hndl, iCustomdigits10); // MosfetTempFractions show all digits at 0, only do this once Display.img_Show(hndl,iStatictext14); // LT3081u3Text Display.img_Show(hndl,iStatictext18); // LT3081U3DP Display.img_Show(hndl,iStatictext13); // LT3081u4Text Display.img_Show(hndl,iStatictext19); // LT3081U4DP Display.img_Show(hndl, iCustomdigits12); // LT3081U3TempUnits show all digits at 0, only do this once Display.img_Show(hndl, iCustomdigits11); // LT3081U3TempFractions show all digits at 0, only do this once Display.img_Show(hndl, iCustomdigits13); // LT3081U4TempUnits show all digits at 0, only do this once Display.img_Show(hndl, iCustomdigits14); // LT3081U4TempFractions show all digits at 0, only do this once } void setupINA260() { // write to the INA260: Address:Register:High Byte:Low Byte // See datasheet for values Wire.begin(); Wire.beginTransmission(0x40); //address set by A1 and A0 pins on INA260 Wire.write(0x00); // config register Wire.write(0x61); // high byte -> take 1 average //Wire.write(0x65); // high byte -> take 4 averages Wire.write(0xb7); // low byte -> conversion time for voltage and current = 4.156 ms // (average out the data for low noise), and operate in continous mode Wire.endTransmission(); } /* routine to handle Serial errors */ void mycallback(int ErrCode, unsigned char Errorbyte) { #ifdef LOG_MESSAGES const char *Error4DText[] = {"OK\0", "Timeout\0", "NAK\0", "Length\0", "Invalid\0"} ; LOG_MESSAGES.print(F("Serial 4D Library reports error ")) ; LOG_MESSAGES.print(Error4DText[ErrCode]) ; if (ErrCode == Err4D_NAK) { LOG_MESSAGES.print(F(" returned data= ")) ; LOG_MESSAGES.println(Errorbyte) ; } else LOG_MESSAGES.println(F("")) ; while (1) ; // you can return here, or you can loop #else // Pin 13 has an LED connected on most Arduino boards. Just give it a name #define led 13 while (1) { digitalWrite(led, HIGH); // turn the LED on (HIGH is the voltage level) delay(200); // wait for a second digitalWrite(led, LOW); // turn the LED off by making the voltage LOW delay(200); // wait for a second } #endif }
Next: Part Ten - The Control Stage and Initial Functional Testing
Top Comments