The BBC micro:bit only has 3 main GPIO (general purpose input-output) pins for connecting to the outside world, and that’s probably not going to be enough. If you use a breakout connector, however, you can access a whole load more.
I took a different approach, though, deciding instead to use a pure matrix of keys. Although you can buy cheap matrix keypads, I happened to have a large bag of small push-buttons, so I decided to wire up my own matrix using a breadboard and some wire. Normal calculators and computer keyboards use matrices to reduce the amount of wiring needed to connect physical buttons to computational systems, and so in doing this I learnt a bit about how they work at a fundamental level.
Wiring up the keypad was fun and quite therapeutic as a little break between Zoom calls. You can’t see in the picture but there are lots of tiny jumper wires underneath the buttons as well as the longer green wires. The buttons are staggered on the breadboard to make sure I only make electrical connections where I want.
The left leg of each button is wired together in a column. I then wired together the right leg of each button in rows. Now this looks like a lot of pins are needed, but imagine if you wired up each button separately, you’d need 12 pins for a 4×3 arrangement of buttons. Using a scanned matrix means you only need 7 pins, one for each row and column:
You can see circuit diagram of a 4×4 matrix here to get a better idea of the kind of wiring needed.
You have to be a bit careful which micro:bit pins you choose as some are used for other things like the display. I only need digital pins, however, as my keyboard scanner is going to send a digital signal output down each row in turn and then scan using a digital input every column in that row to see if any of the keys are pressed, completing the circuit and allowing the digital signal to pass.
I picked pins 0, 1, 2 and 8 for my rows and 16, 13 and 14 for the columns. Pins 13 and 14 are also used for an SPI interface, but I’m not using SPI here so I can just use them as GPIO pins. I’m probably also going to need a 4×4 matrix to add operator keys eventually, but for that I’m going to need a bigger breadboard and this started out as a proof of concept which I didn’t really expect to work.
How the code works
For my proof of concept, I wrote a simple MakeCode program (code at the foot of this post) to send a digital write signal out on each row in turn, and then if a signal is incoming on any column, put the relevant number of symbol on the display.
I connected it up, flashed the program – and it worked!
A few caveats: it’s slow. You have to hold each button down for quite a long time before it registers. This is partly because a MakeCode ‘forever’ block introduces a small delay into your program. You can get round that by putting a ‘while true’ block around everything inside the forever block, but one added bonus of the small delay is that you can see the scanning happen in the simulator:
The program could also be improved by using functions to scan the columns and there’s a lot of work to do to transfer this into a working program that uses keypresses to do anything useful like a calculator, but this was an interesting activity to learn how to make and program a keypad from scratch and learn some fundamentals of how software and hardware combine at quite a low level.
It’s coded in MakeCode blocks and, although it only has 4 instructions in its very reduced instruction set, it behaves in many ways like the microprocessors as used in the very first home computers, pioneering machines like the Altair 8800, the MOS Technology / Commodore Kim-1 and the Science of Cambridge MK14:
Instead of being programmed with a high-level language like MakeCode, Scratch, Python or BASIC, its instructions are given using binary codes.
Each binary code represents an instruction, an instruction with a piece of data attached (in this case an address where it should fetch some data from), or just a piece of data.
When the program is run, the CPU fetches each instruction in turn from memory.
It then decodes the instruction – it decides what the instruction means.
The CPU next executes the instruction – it carries it out.
It keeps track of where it is in the program using a program step counter and adds 1 to the program counter every time it needs to get a new instruction.
It keeps fetching, decoding and executing instructions in order from memory until it reaches a halt instruction.
In effect, all our very simple CPU can do is add two numbers together and display the answer in binary using LEDs. It’s little more than a calculator, and not a very good one as it can’t subtract, multiply or divide.
It’s restricted to only 4 instructions because of the way I designed each word:
I limited myself to a 5-bit binary word, rather than the 8 bits as more normally used in early computer systems, because I wanted to be able to show the contents of each memory location on a row of the micro:bit’s LED display, with a lit LED representing a 1 and an unlit LED representing a 0.
Because I decided to store the operand (data, in this case an address) in the last 3 bits of instructions, that only left the first two bits to encode the actual instruction – the opcode.
This is clearly a bad idea, but if I still want to keep a 5-bit word to fit the display, I can split the opcode from the operand, and that’s what version 2 of my micro:bit CPU project does.
New CPU – new instructions
Now in my new design when the CPU fetches and decodes an instruction that needs an operand (some data or an address where it can find the data), it interprets the next address in memory as data, not an instruction. It does this by reading its contents and increasing the program step counter by 2 instead of by 1.
Now we have 5 whole bits to encode instructions, we can have not 4 but 31 instructions. Strictly speaking we could have 32, but I decided to treat myself and make 00000 a no operation opcode, an instruction that does nothing.
31 sounds like a lot, but even early 8-bit processors like the 6502 used in early home computers had over 100 instructions. To keep this project simple, but still do something more useful, I decided to use the minimum number of instructions I needed to implement a simple algorithm for multiplication:
This algorithm multiples 6 (held in variable x) by 7 (held in y) by adding 6 to variable a 7 times. It also deducts 1 from y each time, and when y reaches zero, the loop stops and it shows the answer on the LED display output,
To make this work in my micro:bit CPU, as well as the addition I can already do, I’m going to need the ability to reduce a variable by 1. Reducing something by 1 is called decrementing. Being able to increment and decrement numbers is useful when doing repetitive tasks, as we’ll see.
This very simple algorithm, however, requires two more things missing from our original micro:bit CPU:
The ability to loop over sets of instructions.
The ability to make a decision based on a condition – in this case, if y is zero, break out of the loop.
The second of these, the ability to change which code is executed depending on the result of a calculation, is fundamentally what makes a computer a computer, rather than just an adding machine. This ability to branch, to take different paths though sets of instructions, is also what separated Charles Babbage’s Analytical Engine from the simpler Difference Engine and made the Analytical Engine, although it was never built, theoretically a general-purpose computer.
For simplicity, I’ve decided to put the loop and the ability to jump out of a loop together in one instruction: JUMP IF NOT ZERO.
When any decrement instruction results in the number 0, a flag is set. Microprocessors have words of memory set aside to store several flags: these are single bits that record things like reaching zero (a zero flag), going into negative numbers (a negative flag) or numbers being too big to store in one word (a carry flag).
Our new CPU program uses a MakeCode variable zeroFlag as its zero flag. It’s a Boolean variable, which means like a microprocessor’s flag, it can only have two values, true or false, 1 or 0.
Here’s our new instruction set:
I could have added some registers or a stack to store data, and I may do that in a future version, but to keep things simple I only made one – A, or the accumulator. You can store numbers in it or add numbers to it. You can also increment or decrement contents of memory locations – so in effect you can use as many memory locations as variables or registers as you like.
As I develop this, I’ll probably add the ability to subtract numbers from the accumulator and the ability to store the accumulator in memory locations as well as store and add numbers directly.
We don’t use all of these instructions in our multiplication program, though, Here it is, a program to multiply 5 by 6 by adding 5 six times:
It adds the number 5 into our accumulator. It finds the number 5 by looking in the memory location specified in the next bit of memory: it tells the CPU to fetch the number from memory location 8.
Next it decreases the contents of location 9 by 1. This is so it can keep track of how many 5s we’ve added.
The instruction in memory location 4 tells it to jump back to location 0 if the zero flag has not been set. It keeps doing this until the zero flag is set when memory location 9 reaches zero.
Next it outputs the result, the number in the accumulator, as a binary number using the LEDs on the bottom row of the display. It then reaches the HALT instruction and stops decoding and executing any further instructions or data,
What’s the clock speed?
My micro:bit CPU doesn’t have a clock to keep fetching instructions, it relies on you to keep pressing button B to step through the program. Press button A to put the CPU in execute mode so that instead of just showing the contents of memory on the top row of LEDs, it will execute any instructions as well.
Try it out for yourself in the simulator below. Press button A to put it in program execute mode. You’ll hear a high tone as it starts. Keep pressing button B and you’ll see it loop around the same locations – it keeps adding 5 to the accumulator and reducing memory location 9 by 1 each time it does this. It adds 5 six times, and 5 lots of 6 is 5 x 6 or 30. When memory location 9 reaches 0, it shows the result of 5×6 in binary on the bottom row of the display and when it gets to the halt instruction it plays a low tone.
You can keep pressing button B but the program won’t execute – the program run LED has gone out and it won’t decode or execute any instructions any more.
Here’s the code that makes it work – quite a complex program, but I’ve tried to use functions where I can to make it easier to follow.
Open it in the MakeCode editor and see if you can create your own program for the improved micro:bit CPU!
Last week I made a binary adding machine out of 3 micro:bits to learn a bit about how computers work deep inside.
This week I’ve been inspired by Ben Eater’s 8-bit computer project. I highly recommend his videos if you want to learn how computers work at a really fundamental level. He has one project where he builds a simple computer on a breadboard using a 6502 processor, the same processor used by the first computer I ever used, my big brother’s Kim-1, as well as more famous machines like the Commodore PET, Commodore 64, Apple 2, BBC Micro, Atari games consoles and the Nintendo Entertainment System.
Very early (and relatively inexpensive) home computers in the 1970s, like the Kim-1 or the Science of Cambridge MK-14, were not even like the home computers of the 1980s. These were single-board computers, uncased like a Raspberry Pi is today but they didn’t, initially at least, hook up to your TV, nor did they have a typewriter keyboard. They just had hexadecimal calculator-like number keypads and simple LED displays of the kind you also found on the calculators of the day.
You didn’t program them in a high-level, easy-to-read language like BASIC or Python, either. You programmed them in assembly language: short very simple commands (usually in the form of 3-letter ‘mnemonics’) that each had their own hexadecimal number value that you entered using the keypad. This was very hard, slow and required a lot of planning and patience, but it meant that you were writing code that ran very quickly indeed on the ‘bare metal’ of the CPU (central processing unit). BASIC programs running on the same processors like the 6502 ran much more slowly, because your English-like BASIC commands had to be translated into something the processor could understand (machine code) every time it ran.
A very simple assembly language program might look like this:
start LDX #$FF ; load X with $FF = 255
loop DEX ; X = X - 1
BNE loop ; if X not zero then goto loop
RTS ; return
All that program does is count down from 255 to 0 in a register – it doesn’t even output it.
Although it’s much harder to read that assembly language program than a BASIC or Python program, it’s easier to read than machine code. The processor can only understand numbers, so to actually enter the program above into a singe-board computer like the Kim-1 would require you to translate each instruction into its equivalent number code, so your program would become a string of hexadecimal numbers like
A2 FF CA D0 FD 60
that you had to enter using the calculator keypad. You would do the translation by hand using a chart, or if you were lucky and had a more advanced computer, a program called an assembler would translate the assembly language to machine code for you.
Ben Eater’s breadboard computers are like this, and as well as his 6502-based project, he also makes his own processor using logic gates on breadboards to build a really simple computer, and that’s what fired my imagination. Could you create your own processor with a small instruction set using a micro:bit?
This is what I came up with.
A micro:bit CPU
I decided my micro:bit CPU would be a 5-bit computer, rather than 8-bit as you might expect. This is because I want to show the contents of memory, instructions and so on using the LEDs on the micro:bit’s display, which is made up of 5 rows of 5.
The top row of LEDs shows the program counter as a binary number. Dark LEDs are zeroes, lit LEDs are ones. Here the number is 00010 in binary, or step 2 in base 10. This counts up as we step through the program.
Unlike a real CPU, we’re not going to use a clock to automatically step through instructions, we’re going to do this manually by pressing button B.
The middle row shows the contents of the memory at the address shown on the top row, again as a binary number. So here we see that memory location 00010 contains 10000.
The bottom row is the output. This is blank until we write something to it, which as it happens this program just has, because 10000 is an instruction code to write the contents of a register (like a variable) to the display output.
How do we know 10000 means ‘write contents of register A to the output display’? Because that’s how our micro:bit processor is designed. Even simple microprocessors have dozens of instructions, but ours is only going to have 4:
It’s only got 4 because remember I’ve decided only to use a 5-bit word, and if I’m going to be able to include any meaningful address data I think I need at least 3 bits to store the address (the red Xs in the table above). That only leaves 2 bits for actual instructions.
You can think of the first two binary digits as the opcode (operation code, or instruction), and the last 3 digits as the operand (the data that will be manipulated by the opcode).
I’ve decided my processor can load the contents of a memory slot into a register called A. This is, in effect, a variable. It can also add the contents of another memory location to it. It can output the contents of A. Finally, the Halt instruction tells it to stop executing the program. We need this because we want to use memory slots after our program code to contain data like the numbers we’re going to add, and we don’t want the processor trying to execute the data as if it were an instruction.
It loads the contents of memory location 4 into register A. It then adds the contents of location 5 to it, shows it on the display output and stops. In effect, it’s adding 12 and 11 and showing the result as a binary number on the micro:bit’s LED display.
Try it out in the simulator
The code for this project is in this HEX file (right click and ‘save link as…’ or ‘save target as…’ to download it). You will (at the time of writing) need to load it in the beta version of MakeCode because it uses some new blocks.
Press button B to step through each memory location to check the program. Reset the simulator and press button A to switch on execute mode. You should see the execute flag light come on. Now when you step through the program using button B it will execute any data as instructions until it reaches a Halt instruction. Keep pressing button B and you should see the number 23 appear in the bottom row as binary 10111.
A little bit of history repeating
4 instructions is a very reduced instruction set, but Reduced Instruction Set Computers (RISC) were, and are, a thing. In the late 20th century, people realised that processors with reduced instruction sets were faster and used less power, meaning you could run them on batteries in portable devices. One company doing this was a British one called ARM. They designed the processor that went in early mobile computers like the Apple Newton. Today almost every smartphone has an ARM-designed CPU in it, as indeed does the BBC micro:bit.
You may not know that the A in ARM originally stood for Acorn: Acorn RISC Machines. Acorn made the BBC Micro, a computer widely-used in UK schools in the 1980s. It was also quite a popular home computer, and it used the same 6502 processor used by the Apple, Commodore and Atari computers of the time. The BBC Micro was part of a digital literacy scheme including TV programmes, and this was echoed by the 2016 BBC Make It Digital campaign which introduced the micro:bit. So ARM were involved in both projects, and indeed they are founding partners of the Micro:bit Educational Foundation.
How does it work?
I’ve used what is still at the time of writing a beta version of MakeCode for this project because it allows functions that pass back parameters. I go into a little more detail in the video, but the main elements are:
the memory is made up of binary numbers stored as text strings in an array
the PgmCounter variable tracks where you are in the program – this is a normal base 10 MakeCode variable
functions convert between base 10 (denary) integers and binary number strings and vice versa, returning a value
a function displays any binary number string on any specified row on the LED display
the CPU function contains the code that fetches, decodes and executes instructions just like in a real processor
Here’s the function that converts from binary to base 10. The CPU function uses this to translate binary memory addresses and data into base 10 so we can use normal MakeCode maths functions and array addresses. Let’s unpack how this works.
The binary number is held as a text string, so the loops steps though every character for however long the number is. The easiest way to convert from binary to base 10 is to identify the place value for each digit and add them up:
The function does just that. It keeps a running total of the base 10 value in the dec variable.
Notice that each place value (1, 2, 4, 8, 16 etc) can be written as powers of 2. 1 is 2 to the power of 0, 2 is 2 to the power of 1, 4 is 2 to the power of 2, then 3 and 4. The function uses that fact to do the conversion in very few blocks of code.
It uses char from text at index to look at each binary character in term.
If it finds a ’1′ character, it uses its place in the binary number string to work work out which power of 2 to add to the base 10 total.
Because it’s going from left-to-right, from most significant (largest) bit to smallest, we need to a bit of jiggery-pokery to reverse the list and work out the power needed.
(This is a simplified and improved version of the function actually used in the project).
I wasn’t sure how to do this, so I wrote out a table a bit like this to see if I could spot the pattern and find how to convert the location of a digit to its place value as expressed as powers of 2. If we can spot a pattern, we can write a compact bit of code to do the conversion:
So, we need to convert the character string index to the power, convert 0 to 4, 1 to 3, 2 to 2 and so on – and it would be neat to do this for any length string.
The pattern I spotted was that the power is equal to the length of the binary number string minus the index plus 1. We have to add 1 because string character indices are zero-indexed – the first character is character number 0.
Let me know if you have ideas for improving this or if you find it. useful for understanding or teaching how CPUs and simple systems work.
One idea I have is to split the instruction and address data, allowing many more instructions – 32 in fact – and the ability to create much more useful programs that have more maths functions and which can include ‘if’ statements by jumping to different memory locations depending on the results of calculations.
You may also like…
If this sparked your interest, check out this awesome project: https://github.com/veryalien/mycobit. It’s a 4-bit micro:bit CPU, written in Python, that you can program using the micro:bit’s own buttons, so it’s self-contained and doesn’t need a computer. It has a useful instruction set and gives access to GPIO pins. I look forward very much to playing with this, and perhaps magpie-ing some instruction set ideas for the next version of my MakeCode micro:bit CPU.
And check this out! I just learned about another micro:bit CPU program written in Python (and indeed in Croatian) by Ivan Bosnić that uses actual written opcodes / mnemonics: https://github.com/bosnivan/CPPU. If you don’t speak Croatian, you can still work out what the instructions do.
I’m going to how to show you to turn two or more micro:bits in to a binary adding machine, using the same code on every micro:bit.
The ability to add two numbers together is a fundamental building block of any calculating machine. If you can add, you can multiply as multiplication is repeated addition. If you can add, you can probably subtract, and that means you can do division as well, as division can be thought of as repeated subtraction.
But the ability to add numbers is ALSO fundamental to building computers. As well as solving maths problems, adding numbers enables computers to find data in memory, step through lists of instructions as well as more exciting things like moving a space ship across your screen in a game.
Electronic circuits that can do sums – and do them very quickly – are called adders. Lots of them are wired together to form the Arithmetic Logic Unit in the processor at the heart of every computer, tablet, phone and many electronic devices like TVs and washing machines.
Humans usually count in a system based around the number 10 – because we have 10 fingers. We use 10 different symbols, 0 through to 9 to record numbers. Each column is worth 10 times more than the one to its right. This is called base 10, or denary.
Computers, on the other, er, hand, use the binary system, which just uses two symbols: 0 and 1. This is because computers are made up of millions of tiny switches, and a switch can be used to store binary numbers: off means 0, and on means 1. This is why the on/off power symbol used on many devices is a zero with a 1 inside!
So switches form the circuits that make up computers, by making logic gates. Logic gates take simple binary inputs, on or off, 0 or 1, and make different outputs. Some are simple, like AND gates. These give an output only if both its inputs are switched on.
The OR gate, on the other hand, will give an output if EITHER of its inputs are turned on.
The EXCLUSIVE OR gate gives an output if either input is switched on – but not both.
Why do you need to know that? Because logic gates like these make up computer memory but also are the building blocks of the Arithmetic Logic Unit, the part of the processor that does all the maths, all the number crunching.
If you connect up an AND logic gate and an XOR gate, you can use it to add two binary numbers together. This is called a half adder. Each switch is a 0 or 1 depending on whether it’s turned on or off. The lights show the sum – if a light’s on it’s 1, if it’s off it’s 0.
You’ll see that both switches off – 0 + 0 – means no lights are lit – binary 00.
If either switch A or B is, the sum light is lit, but not the carry – binary 01.
If both A and B are switched on, we’re adding 1+1. The answer is 10. Why 10? Well it doesn’t mean ten, it means no units, and 1 two – the answer to 1+1 is 2!
Of course a micro:bit contains thousands of actual logic gates, so it IS a bit crazy to turn one into a single logic gate, but it’s interesting to code it, and connect all the parts together to make the most fundamental part of any computer system.
That’s only adding two single-digits together, though, and it uses a lot of micro:bits. So I’ve made another project – a full adder on a single micro:bit.
A full adder has not two, but three inputs – A, B and a Carry In. As well as the two numbers you’re adding, the Carry In takes the Carry Out from a neighbouring adder, just as you add in any numbers you carry when doing column addition in base 10 arithmetic.
So you can add together as many full adders as you like to allow you to add really big numbers together. Every single adder you, er, add, makes the largest number you can add twice as big.
So, here’s how you connect it together: use as many micro:bits as you like, and the same code goes on every one. Turn them on their side. Pin 1 is the Carry Out, so connect it to pin 0 – the Carry In – on the micro:bit to its left and keep going until you run out of micro:bits.
That leaves us with one carry out left over, no matter how many micro:bits we use. But we don’t need to let it go to waste! If you have an LED lying around, connect its long leg (the anode) to the final carry out, and its short leg to GND. While you’re there, connect all the GND pins together, to complete the electrical circuit.
You could power each micro:bit individually from batteries or USB, and indeed it’s safer to do that, but I’ve been a bit cheeky here. I’ve only powered the far micro:bit on the right, and powered all the others from it by joining their 3 volt pins together. If you connect many more than 3 together this probably wouldn’t work.
Here’s how to use it.
The top row of A switches make up the first number we’re going to add. The LED dot in the top right of each display shows you if the switch is on or off, it toggles every time you press it.
The row of B switches across the bottom make up the second number, and the LED in the bottom right shows you if it’s on or off.
To add 1 + 1 set the top and bottom rows to 001 and you can see the result: 010 in binary, which is 2 in base 10.
You’ll see another light has lit up on the micro:bit in the 2s column. That’s to show that it’s received the carry bit from its neighbour.
Ok, let’s do some more adding. 2 + 2. That 010 and 010 in binary. And the answer is 100, we’ve got no units, no twos and one four.
How about 5 + 5? That’s 101 and 101 – the answer is ten of course in base 10. We only have 3 micro:bit numbers, but notice our carry LED is now lit on the far left, meaning the number is 1010 in binary. One eight, and one two. 8 + 2 = 10. The right answer!
Let’s look at how my micro:bit full adder works. The same program goes on every micro:bit in the adder, no matter how many you have, just like a real adder uses identical circuits chained together.
It uses two variables to store the value of A and B, but instead of giving them numerical values of 0 or 1, I’m using TRUE or FALSE. TRUE means 1 and FALSE means 0. I set both to FALSE, off, zero at the start of the program.
Calling 0 false and 1 true is also common when talking about logic gates and circuits. The charts that show how logic gates work are called truth tables.
Another reason for using true and false for 1 and 0 here is that it makes it really easy to flip each variable’s value when you press a button with ‘set A to not A’ and ‘set B to not B’ – if each variable is 0 or 1, it flips to the other value when you press the button.
To keep the code easy to follow, I’ve made a function – a subroutine – to turn the relevant LEDs on and off to show the values of A and B.
As with many computer programs, the real business goes on inside the infinite loop, the forever block in MakeCode.
I’ve used another function here as a subroutine – this one keeps checking if it’s receiving any carry bits from next door. If it is, it lights the carry in LED and sets the carryIn variable to True. If not, it turns the LED off and carryIn’s value will be False.
Now, the real heart of the program is in the big if… then… block.
BFH (Broadcasting From Home) is quite the thing these days, so I thought I’d make a web version of the clocks used in radio studios. These spell out the time in easy-to-read words, and remove the brain-gymnastics required to turn an analogue or digital clock into something you’d actually say out loud.
Some radio presenters are legendary for their time-telling skills, other less so. I once worked with one who forgot his co-presenter’s name as he introduced her on air at the start of the programme, but frankly I’m amazed that anyone talking to millions of people at stupid o’clock in the morning can remember their own name, let alone tell the time.