Silicon ChipMax’s Cool Beans cunning coding tips and tricks - March 2021 SILICON CHIP
  1. Outer Front Cover
  2. Contents
  3. Subscriptions: PE Subscription
  4. Subscriptions: PicoLog Cloud
  5. Back Issues: PICOLOG
  6. Publisher's Letter
  7. Feature: The Fox Report by Barry Fox
  8. Feature: Techno Talk by Mark Nelson
  9. Feature: Net Work by Alan Winstanley
  10. Project: Nutube Guitar Overdrive and Distortion Pedal by John Clarke
  11. Project: Programmable Thermal Regulator by Tim Blythman and Nicholas Vinen
  12. Project: Tunable HF Preamplifier with Gain Control by Charles Kosina
  13. Feature: Circuit Surgery by Ian Bell
  14. Feature: Make it with Micromite by Phil Boyce
  15. Feature: PICn’Mix by Mike Hibbett
  16. Feature: Max’s Cool Beans by Max the Magnificent
  17. Feature: Max’s Cool Beans cunning coding tips and tricks by Max the Magnificent
  18. Feature: AUDIO OUT by Jake Rothman
  19. PCB Order Form
  20. Advertising Index: TEACH-IN by Max the Magnificent

This is only a preview of the March 2021 issue of Practical Electronics.

You can view 0 of the 72 pages in the full issue.

Articles in this series:
  • (November 2020)
  • Techno Talk (December 2020)
  • Techno Talk (January 2021)
  • Techno Talk (February 2021)
  • Techno Talk (March 2021)
  • Techno Talk (April 2021)
  • Techno Talk (May 2021)
  • Techno Talk (June 2021)
  • Techno Talk (July 2021)
  • Techno Talk (August 2021)
  • Techno Talk (September 2021)
  • Techno Talk (October 2021)
  • Techno Talk (November 2021)
  • Techno Talk (December 2021)
  • Communing with nature (January 2022)
  • Should we be worried? (February 2022)
  • How resilient is your lifeline? (March 2022)
  • Go eco, get ethical! (April 2022)
  • From nano to bio (May 2022)
  • Positivity follows the gloom (June 2022)
  • Mixed menu (July 2022)
  • Time for a total rethink? (August 2022)
  • What’s in a name? (September 2022)
  • Forget leaves on the line! (October 2022)
  • Giant Boost for Batteries (December 2022)
  • Raudive Voices Revisited (January 2023)
  • A thousand words (February 2023)
  • It’s handover time (March 2023)
  • AI, Robots, Horticulture and Agriculture (April 2023)
  • Prophecy can be perplexing (May 2023)
  • Technology comes in different shapes and sizes (June 2023)
  • AI and robots – what could possibly go wrong? (July 2023)
  • How long until we’re all out of work? (August 2023)
  • We both have truths, are mine the same as yours? (September 2023)
  • Holy Spheres, Batman! (October 2023)
  • Where’s my pneumatic car? (November 2023)
  • Good grief! (December 2023)
  • Cheeky chiplets (January 2024)
  • Cheeky chiplets (February 2024)
  • The Wibbly-Wobbly World of Quantum (March 2024)
  • Techno Talk - Wait! What? Really? (April 2024)
  • Techno Talk - One step closer to a dystopian abyss? (May 2024)
  • Techno Talk - Program that! (June 2024)
  • Techno Talk (July 2024)
  • Techno Talk - That makes so much sense! (August 2024)
  • Techno Talk - I don’t want to be a Norbert... (September 2024)
  • Techno Talk - Sticking the landing (October 2024)
  • Techno Talk (November 2024)
  • Techno Talk (December 2024)
  • Techno Talk (January 2025)
  • Techno Talk (February 2025)
  • Techno Talk (March 2025)
  • Techno Talk (April 2025)
  • Techno Talk (May 2025)
  • Techno Talk (June 2025)
Max’s Cool Beans cunning coding tips and tricks I ’m quite excited as I start to pen these words because we are poised to gather a bunch of disparate concepts together and – at the end of this column – we are all going to say ‘Wow!’ (If you fail to say ‘Wow!’ with sufficient enthusiasm, I’ll have to send my dear old mother to visit you to explain the error of your ways.) Bits and bytes and nybbles, Oh my! The smallest piece of information that can be stored and manipulated inside a digital computer is a binary digit (a.k.a. ‘bit’), which can be used to embody two values. We typically think of these values as representing the numbers 0 and 1. As we discussed in my previous Tips and Tricks column (PE, February 2020), and assuming we are working with an Arduino microcontroller, these 0 and 1 values are equivalent to LOW and HIGH, respectively, when we are reading from or writing to the digital input/outputs (I/O). They are also equivalent to false and true, respectively, if we are treating them as Boolean logic values. Since a single bit is limited with regard to the amount of information it can represent, we usually find it more convenient to work with groups of bits. Some groupings are common, so we’ve given them special names. For example, the term ‘byte’ refers to an 8-bit group, while the term ‘nybble’ (or ‘nibble’– I prefer ‘nybble’!) refers to a 4-bit group. This means that two nybbles make a byte, thereby demonstrating that computer engineers do have a sense of humor, albeit one that’s not tremendously sophisticated. Generally speaking, humans find it hard to wrap our brains around large numbers presented in binary. For example, 10101100 in binary doesn’t mean much to most people, while its decimal equivalent of 172 is easier to comprehend. The binary number system has two digits, 0 and 1. The decimal number system has 10 digits, 0, 1, 2, 3, 4, 5, 6, 7, 8, and 9. For reasons that will be made clear in future columns, mapping (translating) values back and forth between binary and decimal is not as convenient as one might hope. We find it much more efficacious to use the hexadecimal number system, which boasts 16 digits, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, A, B, C, D, E, and F. The beauty of this system is that each hexadecimal digit maps directly onto a 4-bit binary nybble (Fig.5.) In previous Tips and Tricks columns (PE, August and September 2020) we discussed the difference between signed and unsigned integers. Also, the fact that in the case of the int (integer) data type, its size – ie, the number of bits used to represent it – depends on the computer you are working with. In the case of an Arduino Uno, for example, an int occupies two bytes (16 bits) of memory. Let’s suppose we declare an int called intA. Let’s further suppose that we assign it a value of 0xCA35, where the ‘0x’ prefix indicates that this is a hexadecimal value: AND and OR gates Decimal Hex adecimal B inary 0 0 0 0 0 0 At its lowest level, a digital computer 1 0 0 0 1 1 is composed of a humongous number 2 2 0 0 1 0 0 0 1 1 3 3 of on/off switches. These switches 4 4 0 1 0 0 can be implemented using a variety 5 5 0 1 0 1 6 0 1 1 0 6 of technologies, including mechanical, 7 7 0 1 1 1 electromechanical (relays), pneumat8 1 0 0 0 8 9 1 0 0 1 9 ic, and semiconductors (transistors). 1 0 A 1 0 1 0 These days, of course, transistors are 1 1 B 1 0 1 1 1 2 C 1 1 0 0 all the rage – and that’s what we will 1 1 0 1 1 3 D assume here – but who knows what 1 4 E 1 1 1 0 1 1 1 1 1 5 F may come tomorrow? The next level up is to gather small Fig.5. Mapping binary groups of transistors and use them to and hexadecimal. implement simple logic functions – a.k.a. primitive logic gates – and to then connect these gates together in cunning ways. The two gates we will focus on in this column are the AND and OR gates (Fig.7). Observe that we use the & character to represent the AND, while the | character is used to represent the OR. As we see, the output (y) from the AND gate is a 1 only if both of its inputs (a and b) are 1; otherwise, the output is 0. By comparison, the output of the OR gate is 1 if either of its inputs are 1. The && and || logical operators Previously, we were talking about AND and OR in the context of hardware; that is, the physical logic gates used to build the computer. Now we are going to consider related functions in software; that is, our programs. In last month’s Tips and Tricks (PE, February 2021), we introduced the concept of Boolean variables, which can be assigned values of false and true. We also noted that these variables are often treated similar to integers in that false equates to 0 and true equates to 1 or – to be more precise – true equates to any non-zero value. The && and || logical operators allow us to construct complex conditional statements. For example, assuming that we’ve declared integer variables called intA and intB, we could write a test as follows: if (((intA == 6) && (intB == 4)) == true)... Note that we don’t actually need all of the parentheses I’ve used here because the two == relational (comparison) operators on the left have a higher precedence than the && logical AND operator, so we could have written this as follows: if ((intA == 6 && intB == 4) == true)... int intA = 0xCA35; 16 b its (2 b ytes, 4 nyb b les) Using Fig.5 as a reference, we see that we’ve actually loaded intA with a binary value of 8 b its 8 b its 1100101000110101, as illustrated in Fig.6. 15 8 7 0 Note that, rather than writing all 16 bits together in the form 1100101000110101, we intA 1 1 0 0 1 0 1 0 0 0 1 1 0 1 0 1 may find it easier to think of them in terms of 4-bit nybbles and write them in the form C A 3 5 1100 1010 0011 0101 (this is what we will do in the remainder of this column). Fig.6. An example 16-bit value. 62 AN D a y & b a 0 0 1 1 b 0 1 0 1 y 0 0 0 1 OR a y | b a 0 0 1 1 b 0 1 0 1 y 0 1 1 1 Fig.7. AND and OR gates. Practical Electronics | March | 2021 However, if you aren’t sure about operator precedence (https://bit.ly/355G9rC), then it’s always best to use parentheses to force things to happen in the order you want them to, with the added advantage that parentheses usually make it easier for other people to work out what you are trying to achieve. Let’s think about how the above expression will be evaluated by the compiler, and therefore how it will be executed by the computer. If intA is equal to 6, then this sub-expression will return true (1), otherwise it will return false (0). Similarly, if intB is equal to 4, then this subexpression will return true (1), otherwise it will return false (0). The && operator will only return true (1) if both of its inputs are true (1), otherwise it will return false (0). Finally, we use the rightmost == operator to compare the result from the && to a value of true (1). Once again, this comparison will only return a value of true (1) if both of its inputs are true (1), otherwise it will return false (0). Actually, a little thought reveals we can simplify this test by writing it as follows: if ((intA == 6) && (intB == 4))... Although we are in danger of wandering off into the weeds, a little more thought reveals that – assuming we’ve also declared an integer variable called intY – the following would also be a valid statement: intY = intA && intB; In this case, if intA contains 0, the && operator will regard it as being false (0); otherwise, if intA contains a nonzero value, the && operator will regard it as being true (1). Similarly, for intB. As a result, the && operator will return true (1) only if both intA and intB contain non-zero values; otherwise, it will return false (0). And whatever value is returned from the && operator will be assigned to intY. We can think of the && logical operator as being a giant software AND (Fig.8a); similarly, we can think of the || logical operator as being a giant software OR (Fig.8b). The & and | bitwise operators intA intA 1 1 0 0 1 0 1 0 0 0 1 1 0 1 0 1 1 1 0 0 1 0 1 0 0 0 1 1 0 1 0 1 0 1 1 0 1 1 0 1 1 0 1 0 0 1 1 1 0 1 1 0 1 1 0 1 1 0 1 0 0 1 1 1 intB intB & B itwise OR 0 1 0 0 1 0 0 0 0 0 1 0 0 1 0 1 1 1 1 0 1 1 1 1 1 0 1 1 0 1 1 1 (a) B itwise AN D (b ) B itwise OR Fig.9. Visualising the & and | bitwise operators. operator uses a bunch of physical OR gates to perform an OR on each pair of corresponding bits. Assume that we declare three integers, intA, intB, and intY. Remember that we are also assuming that we are working with an Arduino Uno, so each of these integers will be two bytes (16 bits) wide. Suppose we assign a value of 0xCA35 in hexadecimal (1100 1010 0011 0101 in binary) to intA and a value of 0x6DA7 in hexadecimal (0110 1101 1010 0111 in binary) to intB. Now suppose we use the following statement in our program to perform a bitwise AND: (???? ???? ???? ???? in binary). Now suppose that we wish to perform a test to determine if bit 7 of intA is a 1 and, if so, perform some actions. We could achieve this using the bitwise AND operator and a reference value of 0x0080 (0000 0000 1000 0000) as follows: if ((intA & 0x0080) == true)... 15 The least significant bit of intA is 1, as is the least significant bit of intB, so an AND of these means the least significant bit of the result will also be a 1. Similar operations are performed for all of the other bits (Fig.9a), resulting in intY containing 0x4825 in hexadecimal (0100 1000 0010 0101 in binary). Now assume that we start with intA and intB containing the same values as before and we use the following statement in our program to perform a bitwise OR: intY = intA | intB; ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? 0 0 0 0 0 0 0 0 1 0 0 0 0 0 0 0 R eference value of 0x 008 0 & B itwise AN D 0 0 0 0 0 0 0 0 ? 0 0 0 0 0 0 0 (a) U sing & to test b it 7 of a 16 -b it value 15 0 intA 1 1 0 0 1 0 1 0 0 0 1 1 0 1 0 1 0 0 0 0 0 0 0 0 1 0 0 0 0 0 0 0 R eference value of 0x 008 0 | B itwise OR 1 1 0 0 1 0 1 0 1 0 1 1 0 1 0 1 (b ) U sing | to set b it 7 of a 16 -b it value 15 0 intA 1 1 0 0 1 0 1 0 0 0 1 1 0 1 0 1 In this case, we perform an OR operation on each pair of corresponding bits (Fig.9b), resulting in intY containing 0xEFB7 in hexadecimal (1110 1111 1011 0111 in binary). 1 1 1 1 1 1 1 1 0 1 1 1 1 1 1 1 R eference value of 0x FFF7F & B itwise AN D 1 1 0 0 1 0 1 0 0 0 1 1 0 1 0 1 Testing bits Suppose we know that intA currently contains a valid value, but we don’t actually know what this value is. Purely for the purpose of these discussions, we could visualise this as 0x???? in hexadecimal 0 intA intY = intA & intB; When we are writing programs, we can also use the & (bitwise AND) and | (bitwise OR) operators. The bitwise AND accepts two integer values as inputs and performs I s this true? I s this true? I s this true? I s this true? (ie, non-z ero) (ie, non-z ero) (ie, non-z ero) (ie, non-z ero) an AND on each pair of & & || L ogical AN D L ogical OR corresponding bits. In the computer, this is I f so, then the result is true (1), I f so, then the result is true (1), actually implemented otherwise the result is false (0) otherwise the result is false (0) using a bunch of physi(a) L ogical AN D (b ) L ogical OR cal AND gates. Similarly, the bitwise OR Fig.8. Visualising the && and || logical operators. Practical Electronics | March | 2021 | B itwise AN D (c) U sing & to clear b it 7 of a 16 -b it value 15 ? 0 ? intA ? ? ? ? ? ? ? ? ? ? ? ? ? ? 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 R eference value of 0x 000F & B itwise AN D 0 0 0 0 0 0 0 0 0 0 0 0 ? ? ? ? (d) U sing & as a mask to isolate the L S nyb b le Fig.10. Testing, setting, clearing, and masking bits. 63 A graphical representation of this operation is shown in Fig.10a. For every bit that is 0 in the reference value, the corresponding output bit from the bitwise AND is forced to 0. Since bit 7 in the reference value is 1, the corresponding output bit will reflect the value of bit 7 in intA. If bit 7 in intA is 0, the value returned from (intA & 0x0080) will be zero, which is considered to be false, so our test will boil down to if (false == true). Obviously, this will fail, and the statements encompassed by the if() will not be executed. Alternatively, if bit 7 in intA is 1, the value returned from (intA & 0x0080) will be 0x0080 – that is, non-zero – which is considered to be true, so the test will boil down to if (true == true). Obviously, this will pass, and the statements encompassed by the if() will therefore be executed. From our earlier discussions, we know that we could achieve the same effect using the following statement: if (intA & 0x0080)... Now suppose that we wish to perform the actions encompassed by the if() only if bit 7 of intA contains a 0. In this case, we could use the following statement: As a reminder, we used various combinations of masks and shifts to extract the three 8-bit red, green, and blue channels from a 32-bit color value (the most significant 8 bits aren’t used) in an earlier Cool Beans column (PE, October 2020). So, why are masking operations of interest to us here? Well, I’m glad you asked... Just call me ‘Lord of the Rings’ Returning to the rings of LEDs we’ve been discussing in our Cool Beans columns, when it comes to rotating a pixel around the ring, there’s a trick we can use if the number of pixels in the ring is a power of two. In the case of my Prognostication Engine project, for example, the rings each have 16 pixels, where 16 = 24. As we know, these pixels are numbered from 0 in decimal (0 in hexadecimal; 0000 in binary) to 15 in decimal (F in hexadecimal; 1111 in binary). Cast your mind back to the Chickens and eggs topic in this month’s Cool Beans column. Suppose we wished to employ this approach with the Prognostication Engine’s 16-pixel rings. With our newfound knowledge of masks, when it comes to incrementing the pointer in our clockwise rotation (file CBMar21-07.txt), we could replace the original modulo calculation (CurrentP = (CurrentP + 1) % NUM_P;) in our main loop() function with a masking operation as follows (the modified portion is shown in bold): if ((intA & 0x0080) == false)... Alternatively, we could achieve the same effect as follows: if (!(intA & 0x0080))... void loop () { // Turn old pixel off Neos.setPixelColor(RingXref[CurrentP], COLOR_OFF); We can read this latter version as ‘If not (intA & 0x0080) then...’ or ‘If the bitwise AND of intA and 0x0080 doesn’t return a true (non-zero) value, then…’ I know that this can be a tad confusing at first, but it won’t be long before you can read and write this sort of thing without thinking (too hard) about it. // Increment the pointer CurrentP = (CurrentP + 1) & MAX_P; Setting and clearing bits // Display the result Neos.show(); delay(PadDelay); Let’s suppose that intA contains some value like 0xCA35 (1100 1010 0011 0101) and we wish to set its bit 7 to 1 while leaving its other bits unchanged. We could achieve this using the bitwise OR operator and a reference value of 0x0080 (0000 0000 1000 0000) as follows. intA = intA | 0x0080; As illustrated in Fig.10b, the result is to leave intA containing 0xCAB5 (1100 1010 1011 0101). Now suppose that we change our minds, and we decide that we really want to clear bit 7 of intA to 0 while, once again, leaving the other bits unchanged. We could achieve this using the bitwise AND operator and a reference value of 0xFF7F (1111 1111 0111 1111) as follows. intA = intA & 0xFF7F; As illustrated in Fig.10c, the result, in this case, is to leave intA containing 0xCA35 (1100 1010 0011 0101). Masking bits As you will see, this is a logical extension of what we’ve seen before. As we did while testing bits, let’s suppose that we know that intA currently contains a valid value, but that we don’t actually know what this value is, so we will visualise this as 0x???? in hexadecimal (???? ???? ???? ???? in binary). Now suppose that we wish to ‘extract’ only the least-significant nybble and copy this into intY. We could achieve this as follows (Fig.10d): intY = intA & 0x000F; 64 // Turn new pixel on Neos.setPixelColor(RingXref[CurrentP], COLOR_ON); } How does this work? Well, if you look at my source code (file CB-Mar21-13.txt), you’ll see that we’ve defined NUM_P as 16. Since MAX_P is defined as (NUM_P – 1), this means that MAX_P is 15 in decimal (F in hexadecimal; 1111 in binary). For CurrentP equals 0 to CurrentP equals 14, (CurrentP + 1) will equal 1 to 15, respectively. In the context of our 16-bit integers, this corresponds to (CurrentP + 1) values of 0x0001 to 0x000F in hexadecimal, or 0000 0000 0000 0001 to 0000 0000 0000 1111 in binary. If we use the bitwise AND operator to perform a mask operation on these values with a reference value of MAX_P, which will be automatically promoted to 0x000F (0000 0000 0000 1111), then they will all pass through unscathed. Now consider what happens when CurrentP equals 15, which means (CurrentP + 1) will equal 16 in decimal (0x0010 in hexadecimal; 0000 0000 0001 0000 in binary). In this case, our bitwise AND will mask out the three most-significant nybbles and return 0. As a result, our count progresses from 0 to 15 and then returns to 0 again, which is just what we want. As an aside, you may have noticed that whenever we call the Neos.setPixelColor() function in the above code snippet, instead of the first argument being CurrentP, we’ve changed this to be RingXref[CurrentP].The reason for this is that, as discussed in a previous Cool Beans column (PE, January 2021), the physical pixels on the 16-element ring in my prototype aren’t oriented and connected the way we want them to be, so we use the RingXref[] array to translate what we have in our virtual world (in the form of CurrentP) into what we need in the real world. Practical Electronics | March | 2021 Finally, let’s consider using the mask technique for an anticlockwise rotation. Before we proceed, let’s remind ourselves that an integer in a computer occupies a limited number of bits (16 in the case of an Arduino Uno). So, what happens if our integer already contains 0xFFFF (1111 1111 1111 1111) and we add 1 to this? The result is an overflow condition where the integer ends up containing 0x0000 (0000 0000 0000 0000). Contrawise, if our integer contains 0x0000 (0000 0000 0000 0000) and we subtract 1, we get an underflow condition leaving the integer containing 0xFFFF (1111 1111 1111 1111). Now consider the new implementation of our anticlockwise rotation (file CB-Mar21-14.txt) and compare it to our original implementation (file CB-Mar21-08.txt). In this case, we can replace the original modulo calculation (CurrentP = (CurrentP + MAX_P) % NUM_P;) in our main loop() function with a masking operation as follows (the modified portion is shown in bold): void loop () { // Turn old pixel off Neos.setPixelColor(RingXref[CurrentP], COLOR_OFF); // Decrement the pointer CurrentP = (CurrentP - 1) & MAX_P; // Turn new pixel on Neos.setPixelColor(RingXref[CurrentP], COLOR_ON); // Display the result Neos.show(); delay(PadDelay); If you compare this to our clockwise version, you’ll see that all we’ve done is change the ‘+’ to a ‘–’. So, how does this one work? Well, for CurrentP equals 15 to CurrentP equals 1, (CurrentP - 1) will equal 14 to 0, respectively. This corresponds to (CurrentP - 1) values of 0x000E to 0x0000 in hexadecimal, or 0000 0000 0000 1110 to 0000 0000 0000 0000 in binary. As before, our bitwise AND mask operation will pass all of these values through unscathed. Now consider what happens when CurrentP equals 0, which means (CurrentP - 1) will equal 0xFFFF in hexadecimal; 1111 1111 1111 1111 in binary. In this case, when our bitwise AND masks out the three most-significant nybbles, the result will be 0x000F (0000 0000 0000 1111), which is 15 in decimal. As a result, our count progresses from 15 down to 0 and then returns to 15 again, which is what we are looking for. The advantage of the mask approach is that, in the case of a simple microcontroller, replacing a modulo division with a simple bitwise AND can potentially save a bunch of clock cycles. The disadvantage is that the modulo approach works with rings containing an arbitrary number of pixels, while the mask technique works only if the number of pixels is a power of two. What say you? Phew! We’ve certainly covered a lot of ground this month, but I hope you’ve found these discussions to be both interesting and useful. As always, I welcome your comments, questions, and suggestions. } Teach-In 8 CD-ROM Exploring the Arduino This CD-ROM version of the exciting and popular Teach-In 8 series has been designed for electronics enthusiasts who want to get to grips with the inexpensive, immensely popular Arduino microcontroller, as well as coding enthusiasts who want to explore hardware and interfacing. Teach-In 8 provides a one-stop source of ideas and practical information. The Arduino offers a remarkably effective platform for developing a huge variety of projects; from operating a set of Christmas tree lights to remotely controlling a robotic vehicle wirelessly or via the Internet. Teach-In 8 is based around a series of practical projects with plenty of information for customisation. The projects can be combined together in many different ways in order to build more complex systems that can be used to solve a wide variety of home automation and environmental monitoring problems. The series includes topics such as RF technology, wireless networking and remote web access. PLUS: PICs and the PICkit 3 – A beginners guide The CD-ROM also includes a bonus – an extra 12-part series based around the popular PIC microcontroller, explaining how to build PIC-based systems. EE FR -ROM CD ELECTRONICS TEACH-IN 8 £8.99 FREE CD-ROM SOFTWARE FOR THE TEACH-IN 8 SERIES FROM THE PUBLISHERS OF INTRODUCING THE ARDUINO • Hardware – learn about components and circuits • Programming – powerful integrated development system • Microcontrollers – understand control operations • Communications – connect to PCs and other Arduinos PLUS... PIC n’MIX PICs and the PICkit 3 - A beginners guide. The why and how to build PIC-based projects Teach In 8 Cover.indd 1 04/04/2017 12:24 PRICE £8.99 Includes P&P to UK if ordered direct from us SOFTWARE The CD-ROM contains the software for both the Teach-In 8 and PICkit 3 series. ORDER YOUR COPY TODAY! JUST CALL 01202 880299 OR VISIT www.epemag.com Practical Electronics | March | 2021 65