Random Facts About ARM, x86, RISC-V, AVR and MIPS Microprocessors
Interesting technical differences and features of today’s popular microprocessors (CPUs)
We are seeing a bit of a golden age for Microprocessors. For years the landscape has been dominated by Intel. Now with ARM dominating the mobile space and taking over Apple laptops (M1 SoC) as well as entering Amazon server farms (Graviton2), there is a shakeup.
This has brought me down a rabbit hole as I have attempted to learn more about the current state of microprocessor (CPU) development. Where is it going? Is x86 going to be killed off? Will ARM dominate everything?
Perhaps even ARM lives on borrowed time, and the future is really RISC-V? What about AVR and MIPS processors? Where do they fit into the picture? Why is MIPS even relevant and still talked about? Didn’t the Silicon Graphics that used MIPS chips die way back in the 1990s?
As I have tried to find answers to these kinds of questions I have dug up a lot of random facts that are not easily organized while they are still interesting to know about.
ARM Can Conditionally Execute Single Instructions
If you have programmed x86 assembly or say some classic 6502 then you are used to making comparisons or arithmetic operations which update the status flags such as the Zero
N and Overflow
Then you use some kind of jump instruction which looks at one of these status flags to make a conditional jump to another part of the code. Here is an example written in x86 code:
CMP AX, 42
MOV BX, 12
MOV BX, 33
This is mostly a useless program:
- We are checking if the
AXregister is equal to 42 using the CoMPare instruction
CMP. This sets the status flags.
- Afterwards we use the Jump if Equal
JEinstruction to jump to the label
AXcontained the number 42.
- If that was the case we put 33 into register
BXwith the MOVe
MOVinstruction. Notice how we need to use a bunch of jumps to make sure
MOV BX, 12is only run when they are not equal and nothing else.
With ARM a this kind of code becomes much simpler, due to conditional instructions:
CMP r6, #42
LDREQ r3, #33
LDRNE r3, #12
Here we compare register
r6 with the number 42. If they are equal then the Zero register
Z is set to 1. But that is not the only effect. ARM has a special conditional register which gets set every time a number comparison is made.
This is useful because in the ARM architecture, every instruction can be conditionally executed. A conditional instruction is made by adding a two letter suffix such as
LT etc, which corresponds to the comparison operators =, ≠, >, <.
So a normal load instruction that should always be executed is written
LDR, while one that should only be executed if last comparison result was equal would be
LDREQ. A load for not equal would be
This applies to any instruction. So a normal unconditional add is written
ADD while an add which should only be run if last result was not equal would be written as
In addition we can add an
S to various operations to cause them update the conditional register.
SUBS r3, r6, #42
ADDEQ r3, #33
ADDNE r3, #12
Above I am adding 33 to register r3 if r6 was equal to 42. If they where not equal I am adding 12.
SUB is the normal subtraction, while
SUBS updates the conditional flag. In effect it has the same effect as
CMP except the result is stored in register r3.
Why Does ARM have Conditional Execution?
Branching is quite bad for a pipelined microprocessor. In pipelining instructions are queued up and the execution of one instruction begins before the execution of an earlier one is done. With a Jump the previous instructions in the pipeline where just a waste. We need to flush the cache. With conditional execution we avoid branching.
Sometimes we have to jump of course, but this cuts down the need for branching a lot.
Several other RISC architectures actually have conditional execution.
Here is an interesting discussion of conditional execution on ARM. Among other things how if-statements can be implemented on ARM.
RISC-V Does Not Have Conditional Execution
Now it may seem conditional execution is a really neat and smart idea, but it turns out the designers of the newer RISC-V instruction set did not like the idea:
Their argument is that by using conditional flags you create a dependency between instructions in the pipeline. E.g. if one instruction sets the conditional execution flag, the follow instruction should perhaps not be executed.
MIPS and RISC-V Don’t have Status Flags
Normal processors tend to have negative, zero, overflow and carry flags. However MIPS and the new RISC-V instruction set architecture does not have that.
So how do they manage to perform branching then based on a condition?
In intel x86 you would normally use two instructions like this to get conditional branching:
CMP AX, 42
On RISC-V in contrast this is moved into one instruction.
BEQ x1, x2, equal
What is interesting with RISC-V instructions is that they are very regularized. As far as I can tell, every instruction takes 3 operands (arguments).
This turns out to be very practical for branching. You can compare two registers such as
x2 with each other and depending on result you can jump to a label,
equal in this case.
You can find an overview over these branch instruction on page 110 of the RISC-V Manual.
Here are a couple of other examples:
BGT rs, rt, offset // branch if rs > rt
BLT rs, rt, offset // branch if rs < rt
rs I believe stands for source register and
rt is target register. These could be registers such as
MIPS uses very similar logic. You do branches with instructions like this:
BEQ $t0, $t1, offet # branch if $t0 == $t1
BGEZ $t0, offset # branch if $t0 ≥ 0
One difference is that the MIPS branch instructions are not as generic as the the RISC-V ones. You can only compare if arbitrary registers are equal or not. Otherwise you compare a single register to 0.
RISC-V has a simple trick up its sleeve which I believe lets them easily generalize a lot of instructions.
MIPS, RISC-V and ARM A64 Have a Register Which is Always ZERO!
While this seems like an odd choice, that one of the registers is zero no matter what you do to it, it gives a lot of opportunities to generalize and simplify.
For instance RISC-V has a pseudo instruction called
BGEZ just like MIPS. That is a pseudo instruction just means you can write it in your assembler but, it actually turn into a different but equivalent instruction. These two statements are equal:
BGEZ rs, offset
BGE rs, x0, offset
x0 is always zero, you are comparing the source register
rs to zero before branching to
Many instructions have a way of turning a value negative with a
NEG instruction. RISC-V provides a pseudo instruction for this:
NEG rd, rs
SUB rd, x0, rs // rd = x0 - rs = 0 - rs
So you see this makes it possible to have pretty much every instruction work with 3 operands. When you don’t need an operand in a particular case, you can simply use
x0 in that case. To avoid writing the
x0 each time one just provided pseudo instructions.
This choice was borrowed from MIPS (Evolution of RISC-1, precursor to RISC-V). In fact this choice elegantly solves so many problems that the 64-bit version of the ARM instruction-set A64, also adopted this idea. But there is a twist. ARM uses
x31, the last register as their zero register and calls it
xzr . Consider the move instruction in ARM say you want to so
x1 ← 42, you write:
MOV x1, 42
But this is actually a pseudo instruction. What do I mean by pseudo? It is not a real instruction reckognized by the CPU. It is just a shorthand for another instruction:
ORR x1, xzr, 42
What this means is actually a logical OR instruction. You could think about it as working like this:
x1 ← xzr || 42
In RISC-V move is done in a very similar fashion except the immediate add instruction is used. What does immediate mean? It means you add a number embedded in the instruction, rather than fetching value from some memory location.
LI x1, 42
This stands for Load Immediate (LI), and is an pseudo instruction which often implemented by multiple instructions. But in the simple case an add is used:
ADDI x1, x0, 42
If you want to move a value from one register to another then this is always just a simple add instruction:
MV x1, x2
This move instruction is a pseudo instruction implemented as:
ADDI x1, x2, 0
You can read more about RISC-V pseudo instructions here, on page 110.
MIPS is very Popular for Teaching, But Why?
One thing that has kept popping up as I have been digging around about microprocessors is that there is so much material and discussion about MIPS instruction set and architecture.
This seemed really odd to me, as MIPS as far as I knew was last used in Silicon Graphics workstations which are no longer around.
Why would universities not talk about more modern architectures still used such as ARM, AVR or similar?
Here are some interesting things I learned about that:
- MIPS is derives from RISC-1 instruction set which was developed in academia for teaching. In fact RISC-V is really just the fifth generation of RISC-1 by the same professors.
- MIPS architecture is so simple that students can learn to design a MIPS CPU themselves. Pretty impressive!
- ARM instruction set is apparently quite crufty and not that clean. It is also not RISC like in a clean way. Many old ARM choices did not stand the test of time as well as MIPS. ARM64 instruction set is apparently more similar to old MIPS than old ARM.
- Lots of educational material and books exist that teach MIPS. Much less for other architectures.
- While I have been an AVR fan, it is apparently not a great instruction set to learn. The idea is to use a high level language compiler. E.g. few Arduino developers seem to use Assembly.
- Donald Knuth’s famous book series “The Art of Computer Programming” has all algorithms explained through a made up RISC like microprocessor named MMIX, which is heavily based on MIPS.
MIPS is not the dead end I imagine. In fact most CPUs are not as dead as they often seem. Many CPUs no longer used in computers are used in microcontrollers, routers, cars, fridges or whatever. Places you don’t normally think of as having CPUs inside them. MIPS is no different. There may be no more Silicon Graphics workstations, but plenty of hardware such as routers use MIPS.
In fact a very popular line of microcontrollers often competing with AVR (CPU used in Arduino) is called PIC. The 32-bit PIC, called PIC32 actually uses the MIPS instruction set.
Paul Boddie wrote down his experience with programming a PIC32 microcontroller on a breadboard using an Arduino as a programmer.
AVR was Designed by two Students in Trondheim, Norway
As a Norwegian I think it is a kind of cool little detail that a widely used chip like AVR (in Arduino e.g.) was designed in Norway. It is not really a country you think about as originating much of anything related to electronics. One is more likely to associate that with the US, South Korea, Japan or the UK.
It was made by Alf-Egil Bogen and Vegard Wollan while they where students at the main technical university in Norway NTNU.
Since then apparently 500 million AVR microcontrollers have been shipped to customers.
RISC-V has Extendible Instruction Set
One of the big deal about RISC-V is that they don’t have one instruction set you got to support. Instead their instruction set comes with a base with a bunch of extensions which different hardware makers can choose to implement.
In fact hardware makers are encouraged to add their our extensions.
What is interesting with RISC-V is that the instruction set format is designed with this in mind, so you can clearly separate extensions from each other.
Functional Thinking in Assembly Instruction Sets
If you are a software developer you have probably followed the debate around object-oriented programming and functional programming. Although the competition is really between imperative and functional programming.
Functional programming gained prominence in latter years because as we failed to boost the performance of individual cores we have relied on making multiple CPU cores.
This requires that programs must be able to run code in parallel on multiple cores.
That is very hard to achieve if there are poorly defined dependencies between these chunks of code. That is hard to avoid in when coding imperative, because you mutate global state all the time, which can indirectly affect independent parts of the code.
A key idea in functional programming is to not modify state, but only produce output based on input, and not some other state.
Interestingly you see this same thinking applied to the design of modern RISC CPUs. They don’t characterize it as such, but as a programmer I cannot help but think about it in those terms.
With pipelines and out of order execution you got lots of instructions being executed in parallel which could potentially depend on each other.
This means you really benefit from functional thinking. You want as little global state as possible. The status registers such as Zero, Negative, Overflow etc flags are a form of global state.
Hence it makes sense that RISC-V and MIPS did not want to use them. You want each instruction being executed in the CPU to have as little dependency on state of the CPU as possible. You don’t want one instruction to be able to change the global state of the CPU that another instruction may depend on.
That is fine when everything happens in a nice serial fashion. Then an instruction can depend on state changes from earlier commands. But when everything is happening out of order, global state changes become a total mess.