Click the "Little White Learns Vision" above, choose to add "Star" or "Top"
Heavyweight content delivered to you at the first time

From the perspective of underlying hardware, I analyzed how the CPU recognizes and reads code. The content is so wonderful that after reading it, I felt that many things I learned suddenly connected together. I share it with everyone.
First, let’s talk about semiconductors. What are semiconductors? They are something that lies between conductors and insulators, like diodes. Related article: Basic knowledge about diodes.

Current can flow from terminal A to terminal C, but not the other way around. You can think of it as a device that prevents current from flowing backward.
When terminal C is 10V and terminal A is 0V, the diode can be considered disconnected.
When terminal C is 0V and terminal A is 10V, the diode can be considered a wire, resulting in a continuous flow of current from terminal A to terminal C, leading to the final result of A = C = 10V.
Wait, wasn’t it said that terminal C is 0V and terminal A is 10V? How did it turn into A = C = 10V? You can understand this as the initial state; when it stabilizes, it will become A = C = 10V.
Sorry for the liberal arts students, if you really don’t understand, just ask your high school physics teacher. If you can’t understand, just remember that in this case, it is equivalent to a wire.
Using this property of semiconductors, we can create some interesting circuits, such as the AND gate.
In this case, if either terminal A or terminal B is 0V, then terminal Y will directly connect to the 0V place, causing terminal Y to also become 0V. Only when both terminals A and B are 10V, there will be no current flowing between Y and AB, and terminal Y will also be 10V.
We call this device the AND gate, where a place with voltage is counted as 1, and a place with 0 voltage is counted as 0. As for how many volts, that doesn’t matter. This means that both A and B must input 1 for the output Y to be 1; if either A or B is 0, the output Y will be 0.
There are also OR gates, NOT gates, and XOR gates, which are similar. The OR gate outputs 1 if any input is 1; it outputs 0 only if both inputs are 00.
NOT gates are easy to understand; they output 0 for input 1 and output 1 for input 0.
XOR gates are a bit harder to understand, but it’s just like this: if the input is 01 or 10, it outputs 1; if the input is 00 or 11, it outputs 0 (i.e., if two identical values are input, it outputs 0; if two different values are input, it outputs 1).
These gates can be made using diodes or transistors, but I won’t demonstrate how to do it. Students interested can try it themselves. Of course, in practice, they are not made with diodes or transistors because they consume too much power. They are actually made with field-effect transistors (also known as MOSFETs).

Then we can use gate circuits to make a CPU. Of course, making a CPU is still quite difficult. Let’s start with something simple: an adder. Related article: How does the CPU perform digital addition?

The inputs A and B can only be 0 or 1, meaning this adder can compute 0+0, 1+0, or 1+1.
The output S is the result, while C indicates whether a carry has occurred; binary 1+1=10, so at this time C=1, S=0.
After spending a lot of effort, isn’t it particularly rewarding to calculate 1+1?
Then let’s go further and calculate 1+2 (binary 01+10), and we discover a new problem: the second bit needs to handle the possibility of carry from the first bit, so we need to design a full adder.
It’s too troublesome to draw this every time, so let’s simplify it.
That is, there are 3 inputs and 2 outputs, which are the two numbers to be added and the carry from the previous bit, then input the result and whether there is a carry. Then we connect this full adder in series:

Now we have a 4-bit adder that can calculate 4-bit numbers, which means 15+15, already reaching kindergarten level. Isn’t that impressive?
After completing the adder, let’s make a multiplier. Of course, multiplying any decimal number is a bit tricky, so let’s start with multiplying by 2.
Multiplying by 2 is simple; for a binary number, we just add a 0 at the end to multiply by 2. For example:
5=101 (2)
10=1010 (2)
We just need to move all inputs one position forward and add a zero at the lowest position to multiply by 2. I won’t draw the specific logic circuit diagram; you just need to know how it works.
What about multiplying by 3? Simple, shift once (multiply by 2) and then add once. For multiplying by 5? Shift twice (multiply by 4) and then add once.
So generally, simple CPUs do not have multiplication; multiplication is implemented through a combination of shifting and addition in software. This is a bit far-fetched, so let’s continue making the CPU.
Now suppose you have an 8-bit adder and a module for shifting 1 bit. By connecting them, you can calculate (A+B)×2. Exciting, you’re almost at the level of a primary school student.
What if I want to calculate A×2+B? Simple, just change the wiring of the adder and shift module; connect A to the shift module first, then to the adder.
You mean I have to rewire after changing a program?
What do you think?
In fact, programming is just plugging wires back and forth. Surprised? Unexpected?

Early computers were programmed like this; it took a few minutes to calculate, but wiring took several days. For articles related to wiring programming, I recommend this one: A domestic master manually soldered to create a CPU. Moreover, wiring is a meticulous and patient job, so programmers back then were all beautiful girls in uniforms, just like in the photos. Don’t you feel like you’re born at the wrong time?
Wiring is also a tiring job. So we need to improve it, allowing the CPU to add or multiply by 2 according to instructions. We introduce two modules: one called flip-flop (FF), which seems to be called a trigger in Chinese, as shown in the following diagram.
The role of this module is to store 1 bit of data. For example, the RS type FF above, R is Reset; inputting 1 clears it. S is Set; inputting 1 saves 1. When both RS are input as 0, it will continuously output the content saved earlier.
We use FF to save intermediate data (which can also be intermediate states or something else); 1 bit is definitely not enough, but we can parallelize it, using 4 or 8 to save 4-bit or 8-bit data. This is called a register. The other is called MUX, which is called a selector in Chinese. The following figure shows a selector.

This one is simple; if sel inputs 0, it outputs the data of i0, whatever i0 is, it outputs whatever. Similarly, if sel inputs 1, it outputs the data of i1. Of course, selectors can be made long, like this four-input one; I won’t go into details about the specific principles, but just looking at the logic diagram, you can understand it; just know that this thing exists. The following figure shows a four-input one-output selector.

With this, we can design an activation pin for the adder and the multiply-by-2 module (shifting).
This activation pin inputs 1 to activate the module; inputting 0 does not activate it. This way, we can control whether data flows into the adder or the shifting module.
So we design the CPU with 8 input pins, 4 instruction bits, and 4 data bits.
We then design 3 instructions:
-
0100, data read into register
-
0001, add data to register, result saved to register
-
0010, register data shifts left by one (multiply by 2)
Why design it this way? As mentioned earlier, we can design an activation pin for each module. Then we can connect the second, third, and fourth input pins of the instruction to the activation pins of the register, adder, and shifter.
Thus, when we input the instruction 0100, the register input gets activated, and the other modules are not activated, so the data is stored in the register. Similarly, if we input the instruction 0001, the adder starts working, and we can perform the addition operation.
Here we can simply answer the first small question of this problem: Why can the CPU understand these binary numbers?
Why can the CPU understand? Because the wires inside the CPU are connected this way. You input a binary number, activating several specified modules in the CPU and changing the connection methods of these modules, ultimately producing a result.
Several possible questions may arise:
Q: The CPU may have thousands of small modules; can a 32-bit/64-bit instruction control that many?
A: In the CPU we are using as an example, there are only 3 modules connected directly. In a real CPU, there will be a decoder that translates instructions into the required forms.
Q: What happens if I input the instruction 0011 into your example CPU?
A: Of course, both the adder and the shifter will be activated simultaneously, resulting in unpredictable consequences. In simple terms, because you used an instruction that was not designed, the consequences are your own responsibility. In a real CPU, doing this would likely cause a crash, but there will definitely be various protective designs.
Careful friends may notice a problem: Your designed instruction [0001, add data to the register, result saved to the register] cannot be done in one step, right?
After all, there is also a write-back process. In reality, the simple CPU we designed executes an instruction in about three steps: read the instruction, execute the instruction, and write to the register.
The classic RISC design divides it into 5 steps: fetch instruction (IF), decode instruction (ID), execute instruction (EX), memory operation (MEM), and write register (WB). Some instructions of the x86 CPU we usually use may take nearly 20 steps.
You can understand it as having a switch; when you press it, the CPU takes a step. The faster you press, the faster the CPU moves. Hey? Have you heard of an idea? Young man, that idea is very dangerous! Not to mention whether you can press that fast. In modern CPUs, it’s only about 2GHz, so you can press it about 20 billion times a second.
Even if you can press that fast, although the speed increases, power consumption will greatly increase, heat will rise, and stability will decrease. There are indeed such practices in the industry, called overclocking, but beginners are not recommended to try it.
So how does the CPU know what step it is at? Didn’t we introduce FF earlier? This can not only be used to store intermediate data but also to store intermediate states, that is, where it is.
The specific design involves FSM (finite-state machine) theory and how to implement it using FF. This is also a very important part and is a must-know for exams, but it’s not closely related to the topic, so I won’t elaborate on it here.
Let’s continue with the previous discussion. Now we have 3 instructions. Let’s try calculating (1+4)×2+3.
0100 0001; store 1 in the register
0001 0100; add 4 to the number in the register
0010 0000; multiply by 2
0001 0011; add three
Great! With this computer, we should be able to defeat all kindergarten kids and dominate the big class. Moreover, we are using 4 bits; if we switch to an 8-bit CPU, we can easily beat lower-grade elementary students!
In fact, using programs to control the CPU is a pretty advanced idea. Before this, the CPU of computers was designed separately.
In 1969, a Japanese company called BUSICOM wanted to create a programmable calculator, and the American company responsible for designing the CPU thought it was quite silly to redesign the CPU each time, so both sides hit it off and launched the world’s first microprocessor, the 4004, in 1970.
This architecture changed the world, and the American company that designed the CPU gradually became an industry giant. Oh, by the way, it’s called Intel, yes, that one.
Let’s organize the program we just created:
“01000001000101000010000000010011”
You input this into the CPU, and I’ll get ready to go to kindergarten and show off.
What!? By the time we finish inputting, those kids can count on their fingers?
There’s no help; machine language is just so inhuman. Oh, I forgot to mention that this language composed only of 01 is called machine language (machine code), the only language the CPU can understand. However, if you let people read machine language, they will definitely turn into a scholar in a second; no one can stand it.
So let’s improve it. However, to be fair, just 30 years ago, directly inputting 01 was still quite common.
So we write our machine language program:
0100 0001; store 1 in the register
0001 0100; add 4 to the number in the register
0010 0000; multiply by 2
0001 0011; add three
And rewrite it as:
MOV 1; store 1 in the register
ADD 4; add 4 to the number in the register
SHL 0; multiply by 2 (since our designed multiplier can only multiply by 2 for now, this 0 is a placeholder)
ADD 3; add three
Isn’t it easier to read? This is called assembly language.
The benefit of assembly language is that it corresponds one-to-one with machine language.
This means that the assembly we write can be perfectly rewritten into machine language to directly command the CPU for low-level development; we can also dump data from memory and display it in assembly language form for easy debugging.
Assembly language greatly enhances the readability and development efficiency of machine language, but it is still too obscure for humans, so we invented high-level languages to express data structures and algorithms in a syntax closer to human language.
For example, many languages can write this:
a=(1+4)*2+3;
Of course, the computer doesn’t recognize this; we need to translate it into a form the computer understands. This process is called compilation, and the tool used for this is called a compiler.
The specifics of how to convert high-level languages into assembly language/machine language could fill a book, so we’ll just give a simple example.
We convert:
(1+4)*2+3
Into:
1, 4, +, 2, *, 3, +
This writing method is called postfix notation, also known as Reverse Polish Notation. In contrast, the notation we usually use is called infix notation, like 1+4. Postfix notation is written as 1, 4, +.
The benefit of converting to this format is that it eliminates the influence of operator precedence and parentheses, allowing for straightforward calculations.
The specifics of how to convert can be found in a book on compiler principles, so I won’t elaborate here.
After converting to this form, we can change it into assembly language.
Starting from the beginning, we first have 1, a number, so we store it in the register:
MOV 1
Next is 4, +, so we add:
ADD 4
Then is 2, *, so we multiply (since our designed multiplier can only multiply by 2 for now, this 0 is a placeholder):
SHL 0
Finally, is 3, +, so we add:
ADD 3
Finally, we organize the translated assembly:
MOV 1
ADD 4
SHL 0
ADD 3
We can then convert it into machine language and run it on our designed simple CPU.
In fact, by this point, we should have clarified the issue: how the things written in C language are translated into binary, and how the computer runs this binary.
However, the questioner also mentioned the relationship between the stack and hardware, so let me elaborate a bit more.
Actually, the stack is a data structure unrelated to the CPU. However, the stack is such a commonly used data structure that CPUs will optimize for it. To allow our CPU to use the stack, we add a few components.
First, add a set of registers. Now we have two sets of registers, which we call A and B.
Second, add two instructions, RDA/RDB and WRA/WRB, which read data from a specified memory address into registers A/B and write the contents of registers A/B to a specified address, respectively.
By the way, let me mention memory. Memory has an address bus and a data bus. For example, if you want to store the number 1100 at address 0011, you connect 1100 to the data bus and 0011 to the address bus. Once everything is ready, you press the switch (yes, the switch we mentioned earlier), and it’s stored.
What does DDR memory mean? It means that when you press this switch, you store one number, and before you lift it, you update the address and data, then release it, and BAM! Another number goes in. This means that normal memory stores one piece of data when you press once; now it stores two pieces of data when you press once, which is called Double Data Rate (DDR).
After adding these instructions, we find that if we control each instruction pin to activate a module, there aren’t enough pins left. So we need to add a decoder.
Thus, we choose to use the second bit as a pin to select whether to choose the register. If it is 0, the third and fourth bits can normally activate the shifter and adder; if it is 1, it only activates the register without activating the shifter and adder, then uses the fourth bit to decide whether it is register A or B. This changes to:
-
0100, data read into register A
-
0101, data read into register B (we define the assembly instruction as MOVB)
-
0001, data added to register A, result saved to register A
-
0011, data added to register B, result saved to register B (we define the assembly instruction as ADDB)
-
0010, register A data shifts left by one (multiply by 2)
Finally, we can use the first bit to control whether to perform memory operations. If the first bit is 1, it also doesn’t activate the shifter and adder modules, then uses the third pin to control whether to read or write. This gives us:
-
1100, read the address data of register B into register A (we define the assembly instruction as RD)
-
1110, write the data of register A to the specified address of register B (we define the assembly instruction as WR)
After adding a decoder, the activation condition for the adder changes from p4 to (NOT (p1 OR p2)) AND p4.
The input for the adder is determined by the third pin; 0 is register A, and 1 is register B. This is simple instruction decoding.
Of course, we can also choose to design a new set of instructions without downward compatibility. However, in reality, this would likely lead to chaos, so you can imagine the heavy historical burden that x86 carries.
At this point, if we want to use a stack, we first initialize the stack address:
0101 1000; MOVB 16; define the stack base address as 1000
Then, for pushing to the stack, for example, to push the numbers 3 and 4:
1111 0011; WR 03; write 3 to memory, address 1000
0011 0001; ADDB 01; stack address +1
1111 0100; WR 04; write 4 to memory, address 1001
0011 0001; ADDB 01; stack address +1
Thus, we have saved 3 and 4 onto the stack.
For popping from the stack, the process is reversed:
0011 1111; ADDB -1; stack address -1
1101 0000; RD 00; read content into register A, 00 is a placeholder
0011 1111; ADDB -1; stack address -1
1101 0000; RD 00; read content into register A, 00 is a placeholder
Thus, we sequentially obtain the values 4 and 3.
So, pushing and popping from the stack is actually just writing data to a specified memory location; the CPU actually doesn’t know what you are doing. Related article: A classic explanation of the stack in C language. Of course, we can also let the CPU know.
Next, let’s improve it further by adding a register SP to the CPU and defining two instructions: PUSH and POP. These actions are to write data to the address of SP, then SP=SP+1, and POP does the reverse.
What are the benefits of this? The advantage is that instructions like PUSH/POP consume very little and are very fast. The stack data structure is used very frequently in various programs, so designing it as a dedicated instruction can greatly improve efficiency.
Of course, the premise is that the compiler knows this instruction and has optimized it, so the same program (written in C language) will produce different results depending on the compilation parameters (turning on/off certain features), and the efficiency of running on different hardware will also differ.
For example, in ancient times, MMX, today’s SSE4.2, AVX-512, powerful, right? Especially powerful, but whether the programs you usually use support it is another matter. How to support it? Recompile it.
At this point, the advantages of open source become apparent; recompiling is very convenient. For closed source, you have to rely on the author’s kindness.
For most people, computers are black boxes, and it’s difficult for us to understand how they work. This question is hard to explain clearly in just a few sentences because it’s a chain reaction, with each link being abstract, and each link is worth two credits, with no limit to how much can be elaborated.
This leads to even those who have studied computer science may not have a clear and definite thought process. I hope to explain this matter from start to finish in as short a length and as simple a language as possible, hoping to clarify some doubts for everyone. Regarding the combination of software and hardware, I also recommend this article: How does code control hardware?
Transferred from STM32 Embedded Development
This article is for academic sharing only. If there is any infringement, please contact to delete it.
Download 1: OpenCV-Contrib Extension Module Chinese Tutorial
Reply "Extension Module Chinese Tutorial" in the background of "Little White Learns Vision" public account to download the first OpenCV extension module tutorial in Chinese, covering installation of extension modules, SFM algorithms, stereo vision, target tracking, biological vision, super-resolution processing, and more than twenty chapters of content.
Download 2: Python Vision Practical Project 52 Lectures
Reply "Python Vision Practical Project" in the background of "Little White Learns Vision" public account to download 31 vision practical projects including image segmentation, mask detection, lane line detection, vehicle counting, adding eyeliner, license plate recognition, character recognition, emotion detection, text content extraction, facial recognition, etc., to help quickly learn computer vision.
Download 3: OpenCV Practical Project 20 Lectures
Reply "OpenCV Practical Project 20 Lectures" in the background of "Little White Learns Vision" public account to download 20 practical projects based on OpenCV, achieving advanced learning in OpenCV.
Discussion Group
Welcome to join the reader group of the public account to communicate with peers. Currently, we have WeChat groups for SLAM, 3D vision, sensors, autonomous driving, computational photography, detection, segmentation, recognition, medical imaging, GAN, algorithm competitions, etc. (which will gradually be subdivided in the future). Please scan the WeChat number below to join the group, with the note: "nickname + school/company + research direction", for example: "Zhang San + Shanghai Jiao Tong University + Vision SLAM". Please follow the format; otherwise, it will not be approved. After adding successfully, you will be invited to the relevant WeChat group based on your research direction. Please do not send advertisements in the group; otherwise, you will be removed from the group. Thank you for your understanding~