1337 lines
63 KiB
Plaintext
1337 lines
63 KiB
Plaintext
|
||
I am contemplating a do-over on this Raspberry Pi Bare Metal Programming
|
||
repo.
|
||
|
||
Bare Metal Programming simply means no operating system. Although we
|
||
could, we are not going to run off and make a gui based web browser
|
||
or anything like that. Bare metal is often used for things like
|
||
booting a computer or the software that runs an alarm clock or TV
|
||
remote control. We are of course going to do it here for fun and
|
||
education. The purpose of the Raspberry Pi is education, for every
|
||
million or so Python programmers we need a bare metal programmer. The
|
||
Raspberry Pi has pros and cons for use in learning bare metal
|
||
programming. On the pro side the peripherals are relatively easy to
|
||
program on the con side the vendor provided documentation is far from
|
||
the best I have seen.
|
||
|
||
Most of bare metal programming has to do with things other than writing
|
||
programs. Reading datasheets, programmers reference manuals, schematics
|
||
are all at the center of bare metal programming. You dont have to be
|
||
a computer engineer nor electrical engineer, if/when you do this
|
||
professionally then there should be electrical engineers that you work
|
||
very closely with, they do their thing, you do yours. Hopefully I can
|
||
hold your hand through the electrical part.
|
||
|
||
Some assembly language programming is required for bare metal
|
||
programming, the bulk of bare metal is C. One nice thing about bare
|
||
metal programming is that the programming itself does not have
|
||
to be that complicated. You need to have some programming experience
|
||
here, doesnt have to be assembly language nor C although C would help.
|
||
I will try to explain the assembly language, and the C should feel
|
||
relatively natural for an experienced programmer, just a matter of
|
||
syntax.
|
||
|
||
My statistic above about a million to one Python to bare metal
|
||
programmers is completely made up, but the percentage of bare metal
|
||
programmers to other forms is a very small number. This means for
|
||
example the documentation we need is read by a relatively small
|
||
number of people, it only has to be good enough, doesnt have to be
|
||
great. Likewise, more than the programming languages themselves
|
||
(generally C with some assembly language) we do have to beat the
|
||
programming tools into submission (assembler, compiler, linker) because
|
||
we are going to use them in a way that is equally rarely used.
|
||
|
||
The last word on bare metal programming in this introduction before
|
||
we go onto what you need is that unlike programming an application
|
||
on top of your favorite operating system, with bare metal programming
|
||
it is possible to destroy hardware. Sometimes you "let the smoke out"
|
||
(the joke is there is a finite amount of smoke in chips and if you
|
||
let even a little bit out the chip wont work) and sometimes you "brick"
|
||
the system. Bricking something in this context means that you have
|
||
done something fatal to the hardware that doesnt let the smoke out
|
||
but the board/product is not much more than a paperweight or a brick
|
||
you might use to hold a door open. On the good side, so far as we
|
||
know, you cannot brick a Raspberry Pi, if your program crashes you do
|
||
have the tools to fix it, in this case the tool is removing the sd card
|
||
and replacing the program that crashed with one that doesnt. With
|
||
hardware other than the raspberry pi, there are various levels of pain
|
||
for bricking a board sometimes you might be able to recover the board
|
||
with a JTAG debugger. Sometimes you can get a soldering iron out and
|
||
remove and replace some components. It is all part of the experience
|
||
unfortunately. With the raspberry pi if you are careful not to
|
||
short anything out (dont touch the board with metal items, dont set it
|
||
on metal items, basically dont create an electrical connection between
|
||
any two exposed bits of metal on the board) and when connecting the
|
||
serial interface below or other additional items we may talk about
|
||
you dont get those connections wrong, you shouldnt have any smoke or
|
||
bricking problems with your Raspberry Pi. I will not take any
|
||
responsibility for you damaging your hardware.
|
||
|
||
Take a deep breath, you CAN do this...
|
||
|
||
Naturally you will need a raspberry pi. I am probably going to use
|
||
my Model A for much of this since I added a reset button to it. I have
|
||
a number of Raspberry Pi boards, and for the most part this material
|
||
should work on all of them. If something board specific comes along,
|
||
we will deal with it then.
|
||
|
||
Looks like folks are retiring the Model A, Adafruit also showed the
|
||
Model A as retired.
|
||
|
||
https://www.sparkfun.com/products/retired/11837
|
||
|
||
The Model B that is the same pc board as my Model A, but has more stuff
|
||
on it (and costs a little more).
|
||
|
||
https://www.sparkfun.com/products/11546
|
||
|
||
The B+ has its led wired differently than the rest so you might have
|
||
some first programs not work but later can catch up.
|
||
|
||
Note that you dont have to sacrifice your linux install on your
|
||
Raspbery Pi to play with bare metal, renaming a file will preserve
|
||
that, as you will see.
|
||
|
||
|
||
Why they didnt start from the beginning with a micro sd slot I will
|
||
never understand, and the way the full sized sd slot sits so that
|
||
the card hangs way out the side. I have broken a number of sd cards
|
||
in those slots, this little adapter board is wonderful for converting
|
||
to a micro sd slot in a durable way. This board is not required but
|
||
you certainly have to have an sd card that fits in the board you are
|
||
using. It does not have to be a huge card (huge as in lots of
|
||
gigabytes) in fact we will be using three fairly small files and that
|
||
is it, early testing my old cards measured in megabytes didnt work
|
||
for some reason, and 2GB and maybe even 4GB cards are harder and
|
||
harder to find. But whatever the popular size is under $10 or so
|
||
should work just fine.
|
||
|
||
https://www.sparkfun.com/products/12824
|
||
|
||
I hate to do this but almost immediately you will need a serial
|
||
interface to the Raspberry Pi to continue this tutorial. Computers
|
||
in general do not ship with serial ports any more, and even if they
|
||
did you cant wire that directly up to this board, the voltage levels
|
||
are wrong (smoke will come out somewhere). The best solution is some
|
||
flavor of usb to serial and it has to be 3.3V not 5.0V (smoke). This
|
||
cable with an integrated usb to serial built in is ideal. You dont
|
||
have to shop at sparkfun, in the USA it is a great place for this kind
|
||
of stuff, and easy on the wallet as far as shipping goes, from the
|
||
picture the wires appear to be labelled, you can probably find these
|
||
usb to TTL 3.3v serial cables all kinds of places, ebay, etc. They
|
||
may not have labelled ends and if you are not experienced at electrical
|
||
engineering and have the tools (multimeter, maybe a scope, etc) you
|
||
dont want to just guess at it (smoke).
|
||
|
||
https://www.sparkfun.com/products/12977
|
||
|
||
You could go with other usb to serial and separately buy the usb
|
||
cable and the hook up wires, but that is more expensive. At the same
|
||
time if you stick with bare metal programming beyond the Raspberry
|
||
Pi, you will need tools like these in your toolbox. A uart/serial
|
||
port is still one of your primary debugging interfaces.
|
||
|
||
https://www.sparkfun.com/products/9873
|
||
https://www.sparkfun.com/products/9140
|
||
|
||
The first documents you will need are found here
|
||
|
||
You will want to go here
|
||
http://elinux.org/RPi_Hardware
|
||
And get the datasheet for the part
|
||
http://www.raspberrypi.org/wp-content/uploads/2012/02/BCM2835-ARM-Peripherals.pdf
|
||
(might be an old link, find the one on the wiki page)
|
||
And the schematic for the board
|
||
http://www.raspberrypi.org/wp-content/uploads/2012/04/Raspberry-Pi-Schematics-R1.0.pdf
|
||
http://www.raspberrypi.org/wp-content/uploads/2012/10/Raspberry-Pi-R2.0-Schematics-Issue2.2_027.pdf
|
||
(might be an old link, find the one on the wiki page)
|
||
As well as some documents from ARM.
|
||
|
||
The Raspberry Pi is centered around the Broadcom BCM2835 media
|
||
processor. ARM does not make chips they sell/license the source code
|
||
to their processor design, which is normally integrated into what is
|
||
called an SoC or System on Chip. Which means some useful peripherals
|
||
are added to the chip that might historically have been on separate
|
||
chips like a DDR (memory) controller, or a USB controller, PCIe, etc.
|
||
For power or size or economy of scale reasons the folks that buy ARM
|
||
processor cores generally need a processor to add to their chip and
|
||
it is easier sometimes to buy than make your own. Most folks dont
|
||
realize it and think that because almost every big box computer (server,
|
||
desktop or laptop) is Intel x86 based (or a clone) that x86 processors
|
||
dominate the world, not realizing that that same box has many other
|
||
processors inside, not all of them ARM's but some. For every x86 you
|
||
own or use you likely own or use many many ARM based products. This
|
||
chip from Broadcom is one of the myriad of ARM based products out there
|
||
fighting for a space in the various niche markets.
|
||
|
||
Be it an ARM based chip or some other the first thing a bare metal
|
||
programmer needs to do is figure out which processor you have. Simply
|
||
stating it is an ARM processor is not remotely enough. ARM has an ever
|
||
growing array of processor products. Some chip vendors are more
|
||
helpful than others at figuring this out. The BCM2835 document
|
||
mentioned above would normally be the place where you would find this
|
||
out, but in this case it does say ARM in the document but doesnt even
|
||
say ARM11 much less arm1176jzfs. Fortunately the Raspberry Pi
|
||
creators and community has the wiki page above which provides the
|
||
information we need. ARM has at least four different cores in the
|
||
ARM11 category this one is the ARM1176 specifically arm1176jzfs a bunch
|
||
of letters that mean something to ARM as to the features included. For
|
||
us that means wse can find one of the two documents we need from ARM.
|
||
Generally you start at
|
||
http://infocenter.arm.com
|
||
And along the left side you find the processor series, in this case
|
||
ARM11 processors. Expand that and see the ARM1136, ARM1156, ARM1176
|
||
and the MPCore. We want ARM1176. Our first goal here is to find
|
||
the Technical Reference Manual, TRM, for the core we are using. For
|
||
the moment this is an accurate link directly to that document
|
||
http://infocenter.arm.com/help/topic/com.arm.doc.ddi0301h/DDI0301H_arm1176jzfs_r0p7_trm.pdf
|
||
In the preface of the TRM it gives us a hint as to the ARM ARM we need
|
||
(ARM ARM = ARM Architectural Reference Manual).
|
||
|
||
ARM Architecture Reference Manual (ARM DDI 0406)
|
||
|
||
There used to be only one ARM ARM for the whole ARM world but the
|
||
architectural differences were such that they left the original ARM ARM
|
||
with the last architecture it supported and started creating new ones.
|
||
So back on the left of the page expand ARM Architecture and then expand
|
||
Reference Manuals.
|
||
|
||
Unfortunately the didnt tell usin the TRM which architecture name to
|
||
look for, so we have to fumble around a bit or do some Googling to find
|
||
that we need the ARMv7-AR Reference Manual. From that page it shows
|
||
|
||
This manual describes the instruction set, memory model, and programmers'
|
||
model for ARMv7 (A&R profile) compliant processors, including:
|
||
Cortex-A series
|
||
Cortex-R series
|
||
Qualcomm Scorpion.
|
||
It also describes the later ARMv6 architecture releases for ARM11
|
||
processors, and describes Thumb-2 and the TrustZone security
|
||
extensions.
|
||
|
||
If you get the manual through ARMs website they appear to require a
|
||
login. It is free other than giving up an email address which no doubt
|
||
you have or can create a gmail one or whatever.
|
||
|
||
https://silver.arm.com/download/download.tm?pv=1603196
|
||
|
||
So the r0p7 nomenclature means rev 0.7 the r is rev and the p is a period.
|
||
Now hopefully the Raspberry Pi folks who provided that link gave us the
|
||
right rev. Just because ARM has fixed some bugs in some rev and the
|
||
currently selling rev is some other number, any ARM based chip you are
|
||
using is built from a specific rev of that product and there are times
|
||
where a rev change generates different internal addressing or features
|
||
in the chip (certainly if you have access to the errata, you need to
|
||
be very careful to apply the correct errata to the right rev, far too
|
||
often are workarounds applied improperly to arm code causing more
|
||
problems for that software than solutions). The ARM1176JZF-S has only
|
||
the r0p7 rev of TRM. But look at the ARM11 MPCore TRM and see there
|
||
is an r1p0 and r2p0 and I know that if you use the wrong one there
|
||
you can have stuff not work. When in doubt take the newest one and
|
||
hope for the best, if you know for sure, then even if the ARM web page
|
||
marks that doc as Superseded, use that doc.
|
||
|
||
To add to the confusion wikipedia shows that the ARM1176 is architecture
|
||
version ARMv6Z. The part we care about is the ARMv6 part as you will
|
||
see soon.
|
||
|
||
So what was the point of that exercise? Well first off I gave you
|
||
many answers for finding info, but finding that stuff on your own is
|
||
a big part of bare metal programming. Sometimes the TRM but usually
|
||
the ARM ARM details the instruction set for that architecture. And yes
|
||
the ARM instruction sets are generally reverse compatible but ARM did
|
||
create some new isntruction sets that we might talk about. Each
|
||
architecture adds a few or more instructions. The original ARM ARM
|
||
became what is now the ARMv5 reference manual which covers ARMv4 and
|
||
ARMv5. ARMv5 is basically the same instruction set but the processor
|
||
added caches and an MMU which makes it significantly easier to run
|
||
an operating system like Linux for example. I want you to also
|
||
download the ARMv5 Architectural Reference Manual because it is a little
|
||
easier getting us started with booting the ARM. We need an instruction
|
||
set reference so we can write assembly language we need assembly language so we can manage booting the processor and
|
||
we need the manual to tell us how the processor boots. In ARM land
|
||
the archtecture manuals are the more common stuff across the
|
||
architecture version in question (the instruction set), and the
|
||
technical reference manual deals with specific processor core products
|
||
within that archtecture version (this one has an FPU that one has
|
||
a cache, etc), the various ARM11 processors for example are different
|
||
processor products basically within the ARMv6 architecture.
|
||
|
||
Really, the Raspberry Pi is not a bad introduction to bare metal
|
||
programming, but there has already been and will be more of these
|
||
nitty gritty details to work through. So all processors have a
|
||
procedure they follow for booting. The hardware folks worry about
|
||
supplying power and a clock or clocks to the processor and releasing
|
||
reset then the fun begins. Processors made by different companies
|
||
dont all follow the same rules, if you take the time to study a few
|
||
different ones you will see that they are as similar as they are
|
||
different. Generally you have some sort of non-volatile (meaning
|
||
doesnt forget when it is powered off) storage like a rom (flash) or
|
||
hard disk or something like that which holds the code that at a
|
||
minimum boots the processor up to the point that you can run fun
|
||
and interesting programs. The ARM processor used in the Raspberry
|
||
Pi as far as the ARM is concerned after reset starts running by
|
||
starting execution at address 0x00000000. And that is what we care
|
||
about. Normally the hardware folks will make the logic around
|
||
the ARM processor core such that when the ARM does a read from address
|
||
0x00000000 (and a lot more addresses that follow) that the chip
|
||
talks to some flash somewhere on or off chip to fetch the instructions.
|
||
But there may be some other address space maybe starting at 0x40000000
|
||
that the chip folks make read from ram. Your x86 computer for example
|
||
has a rom/flash with a bootloader and eventually that bootloader
|
||
reads from a hard disk and then boots the operating system from some
|
||
code on the hard disk that knows how to do that and so on. This is
|
||
all very typical a flash/rom that either contains the application or
|
||
operating system and some ram and if the flash doesnt contain everything
|
||
then it contains code that knows how to reach out to some other storage
|
||
and run the application or operating system.
|
||
|
||
The Raspberry Pi boot process is not what you normally find. Now
|
||
remember this chip was not designed to be a Raspberry Pi, it was meant
|
||
to be some sort of tablet or phone or set top box (ROKU) type product.
|
||
So that basically means it has video processing capabilities, and in
|
||
this case it has a relatively powerful (for its size and price)
|
||
graphics processor which itself is a completely independent processor
|
||
from the ARM. It has a completely different instruction set, it
|
||
has some normalish instructions but then a lot of floating point
|
||
computation capabilities and other things that help it do graphics
|
||
processing. Broadcom is generally extremely secretive about their
|
||
chips, and perhaps by plan or accident or against their will the
|
||
Raspberry Pi has drawn the proper attention to first cause the
|
||
GPU to be reverse engineered and then later for Broadcom to open
|
||
up a fair amount of information about that part of the chip. I didnt
|
||
look for this answer, but either built into logic or or there is some
|
||
on board flash or one time programmable rom that allows the GPU to
|
||
boot first, before the ARM. The GPU is what actually boots the
|
||
Raspberry Pi. Again either raw logic or a bootloader on chip the
|
||
first thing that we see is the sd card is read looking for a file
|
||
named bootcode.bin. That is a program written in the GPU's instruction
|
||
set. It performs some booting tasks like initializing the DDR
|
||
interface and other stuff. Then comes start.elf, also GPU code.
|
||
This is more of the embedded operating system that knows how to do
|
||
all the GPU video processing supported by this chip in case you wanted
|
||
to make a tablet or set top box out of this chip and wanted to play
|
||
videos. Then the GPU boots the ARM by going back to the sd card and
|
||
looking for a file named kernel.img which is an ARM binary. Although
|
||
there are ways to change this but the default is for the GPU to place
|
||
the bytes (ARM code) from that kernel.img file into ram (DRAM) at
|
||
a place that is address 0x00008000 to the ARM. So first off I thought
|
||
you said the ARM boots at address 0x00000000, second why are you playing
|
||
word games, the ARMs address rather than simply saying just address 0x8000.
|
||
Well the GPU also writes to the ARM's address 0x00000000 the instruction
|
||
or instructions needed for the ARM to jump to address 0x8000 causing
|
||
it to runthe program that was found on the sd card. Second, another
|
||
thing you dont normally see, is that the entire memory space is
|
||
shared between the ARM and the GPU. Depending on the generation
|
||
of Raspberry Pi you might have 256MBytes or 512, but all of that is
|
||
available to both processors almost equally. If both processors
|
||
try to access the same memory at the same time the GPU wins and gets
|
||
there first the ARM is held off to wait, otherwise if the ARM won
|
||
and the GPU waited then the video output would studder or get messed
|
||
up.
|
||
|
||
The BCM2835 manual linked above, page 5 has a picture with three
|
||
address spaces, VC CPU Bus Addresses (VC = Video Core or the GPU),
|
||
ARM Physical Addresses and ARM Virtual Addresses. The one we care
|
||
about is the middle one the ARM Physical Addresses, but also the
|
||
real map of the world is the left one the VC CPU Bus Addresses.
|
||
The first thing this picture is telling us (and this is a complicated
|
||
or perhaps at least confusing picture) is that however much RAM
|
||
we have (I may have called it DDR or DRAM) in the system, called SDRAM
|
||
in this picture, be it 256MBytes or 512MBytes or whatever, both the
|
||
ARM and VC/GPU have access to all of that ram. For the ARM that ram
|
||
starts at ARM address 0x00000000 and goes up to whatever amount the
|
||
system has. In the middle it is mared as SDRAM (for the ARM) and
|
||
VC SDRAM (optional), and there is a line in the middel that is vague,
|
||
determined by VC platform configuration. I dont keep track of this
|
||
constantly for every version, but it has typically been a 50/50
|
||
split, again something we can ask the VC/GPU bootloader to change
|
||
but for this discussion there is no need. So let's assume that
|
||
if our Raspberry Pi has 512MB then 256MBytes or address 0x00000000
|
||
to address 0x0FFFFFFF belongs to the ARM and the rest is for the GPU.
|
||
This chart is also showing us that in the GPU's address space that
|
||
ram is mapped certainly at addres 0xC0000000 and 0x00000000 and
|
||
0x40000000 and 0x80000000. That may seem strange to you but it is
|
||
very easy to do in hardware and you will see this over time in your
|
||
career. We dont really care about that since that is GPU side and
|
||
we are programming the ARM. The other information that matters here is
|
||
that the I/O base address for the peripherals starts at 0x20000000
|
||
in the ARM address space and that maps to the same stuff at address
|
||
0x7E000000 in the GPU address space. This manual uses 0x7E000000
|
||
based addresses throughout the document, but as ARM programmers we
|
||
need to see 0x7E001000 for example and replace the 7E with a 20 and
|
||
instead use address 0x20001000. Again this may all seem very strange
|
||
to you but is not uncommon and is generally easy to do in hardware.
|
||
So what we can see here is that the GPU has the ability to read
|
||
the kernel.img file (because it can get to the I/O Peripherals for
|
||
example one of which talks to the sd card) and it can copy that
|
||
data into its memory at 0xC0008000 which instantly becomes the
|
||
ARMs memory at address 0x00008000 since it is the same physical
|
||
memory. Then the GPU can write an instruction or two to its
|
||
address 0xC0000000 which is ARM's address 0x00000000 that will tell
|
||
the ARM processor to jump to address 0x8000. In addition since
|
||
this platform is intended to run Linux on the ARM side the bootloader
|
||
has a few more things to do before releasing reset on the ARM
|
||
and allowing it to run. If you have messed with Linux elsewhere
|
||
even on a laptop or desktop computer there are things that can be
|
||
passed to the kernel when it boots to change its behavior, in the
|
||
case of the ARM we might want to have the same kernel.img work on
|
||
both the 256MB Raspberry Pi and the 512MB Raspberry Pi so we need
|
||
to tell that kernel how much memory it has to work with. The scheme
|
||
used is to take some of that memory in the case of the Raspberry Pi
|
||
between 0x0000 and 0x8000 and put information like how much memory
|
||
and other parameters in a formatted table and when the kernel starts
|
||
it knows to look for that stuff. Eventually the GPU releases reset
|
||
on the ARM meaning it allows the ARM to run. Like a normal ARM
|
||
processor after a reset it looks for its first instruction at address
|
||
0x00000000 and that instruction says jump to address 0x00008000 and
|
||
all of the sudden the ARM is running the program that was basically
|
||
the file kernel.img. This is where we as bare metal programmers
|
||
take over. Instead of that kernel.img file being a linux kernel, we
|
||
can make it any program we want. The Raspberry Pi doesnt care, there
|
||
is no magic or encryption or secret handshake, whatever bytes we put
|
||
there the ARM will at least try to execute, if those bytes are
|
||
not ARM instructions it may crash but so be it that is us taking over
|
||
this platform. You can see the beauty here though, if we do have a
|
||
kernel.img file that is buggy or broken, all we have to do to fix it
|
||
is power off the Raspberry Pi, pull out the sd card and overwrite
|
||
the kernel.img file with something we hope is not broken and try
|
||
again.
|
||
|
||
Okay so lets actually get started. You need to open the ARMv5 ARM ARM,
|
||
chapter A2 the Programmers Model. Hopefully ARM doesnt change the
|
||
chapter numbers on me, but A2.6 Exceptions. In this document the
|
||
word exception means the processor is running along normally and
|
||
something happens to cause it to stop what it was doing and run
|
||
something else. The first one on the list is Reset, now the
|
||
very first reset after the power comes on the ARM wasnt doing anything
|
||
that we caused an exception to, but if it were possible (and probably
|
||
is) on this chip to have a reset while running then that exception
|
||
would do the same thing as the first reset after power on. This
|
||
table shows us that the Reset changes the processor to Supervisor mode
|
||
that just means that our programs are not limited we can run any
|
||
instruction we want and access any address we want. And that the
|
||
normal thing to do is start executing the instruction at address
|
||
0x00000000. From the manual:
|
||
|
||
"When an exception occurs, execution is forced from a fixed memory
|
||
address corresponding to the type of exception. These fixed addresses
|
||
are called the exception"
|
||
|
||
Execution is forced basically the processor is forced to run from the
|
||
address specified. That is how I know that the first instruction
|
||
executed after a reset is the instruction at address 0x00000000 the
|
||
processore is forced to do that.
|
||
|
||
Now if you have experience with this kind of stuff but maybe not
|
||
the ARM you might have noticed that address 0x00000004 is where
|
||
another exeception occurs and you may or may not know that the ARM
|
||
instructions are 32 bit or 4 bytes. So we have exactly one instruction
|
||
to react to a reset, if we were to use two instructions that
|
||
second instruction would be at address 0x00000004 and that second
|
||
instruction would be the first instruction for an undefined exception
|
||
which is when the ARM is asked to execute an instruction, machine code
|
||
that is not defined by that processor as an instruction.
|
||
|
||
The short answer is address 0x00000000 matters to us for booting an
|
||
ARM and we will learn that there are only two instructions we can
|
||
choose from that will do a jump and consume only 4 bytes.
|
||
|
||
This is where the "some assembly language required" starts, we have
|
||
to use assembly language so that we can place the exact instruction
|
||
we want in the right place or order to do things like this jump. On
|
||
the Raspberry Pi the GPU has placed the machine code for the instruction
|
||
we want at address 0x00000000 later we are going to mess with exceptions
|
||
for now the GPU did that for us. Now we are going to start with
|
||
assembly language and the quickly move to using C. Now if you know C or
|
||
know other programming languages you can image that there is some
|
||
software magic required before your programs first function actually
|
||
runs.
|
||
|
||
unsigned int myfun ( void )
|
||
{
|
||
int a=5;
|
||
return(a+7);
|
||
}
|
||
|
||
Now an optimizer will simply return 12 and not generate the extra code.
|
||
But pretend that didnt happen, to literally implement the above program
|
||
somebody has to set aside some storage for the variable a and somebody
|
||
has to fill that storage with the number 5 and THEN you can generate
|
||
some code that does the add and the return. So before we actually
|
||
get to our programs first operation, the add, there was other stuff
|
||
that had to happen, and that stuff has to happen in the world of
|
||
software. You might have heard the word stack and maybe have a vague
|
||
idea of what it means, with assembly language you get to see what
|
||
it really is (and it isnt all that magical). In C before the code
|
||
in the main() function actually executes, there is some bootstrap code
|
||
that is required and you get this chicken and egg problem, how do you
|
||
bootstrap C if you cant use C because you would need a bootstrap for
|
||
the C you are using to bootstrap C. That bootstrap has to be in
|
||
some other language, basically that other code is assembly language.
|
||
|
||
Before we get to that, please see the ARM_TOOLS file for ways to get
|
||
yourself a gnu based assembler, and linker initially then pretty soon
|
||
we need a C compiler as well. As far as this document is concerned
|
||
the exact name of the programs you have may vary but they will all
|
||
in theory all work the same and you can be on a Linux box or Windows
|
||
or MAC. Your assembler command line might be arm-none-eabi-as or
|
||
arm-elf-as or just as is what I am saying so you will need to
|
||
mentally substitute the names I use for the ones you have. See ARM_TOOLS.
|
||
|
||
|
||
Now that you have your assembler and linker, I am not going to go into
|
||
as much detail as I might like if this were purely about learning
|
||
assembly language. Processors are programmable logic, they are
|
||
programmable in the sense that they are designed to operate on machine
|
||
code. Machine code or machine language being blobs of bits that
|
||
define instructions that tell the processor what you want it to do.
|
||
The machine language for a particular processor is very well defined
|
||
in that it doesnt vary, the bit patterns for the instructions are
|
||
what they are. Now we can but it isnt easy or reliable to write
|
||
programs in binary bits, so as humans and programmers we take the
|
||
binary bit patterns and put names we can read and write. Naturally
|
||
to sell their product the inventor of the instruction set needs users
|
||
and to get users they will generally create the assembly language which
|
||
is the name of the human readable programming language whose syntax
|
||
represents the machine code instructions. They will also need to
|
||
make or get someone to make an assembler, which is the program that
|
||
takes the assembly language and converts it into machine code. And
|
||
typically a linker and a C compiler are the minimum tools needed to
|
||
get folks to use your processor. So they have defined an assembly
|
||
language, but that doesnt make it a worldwide standard, it could
|
||
have been invented on the fly by a single individual at the company and
|
||
imposed on the rest of us. The machine language is not changeable
|
||
but the assembly language is and it is not unheard of to have a
|
||
companies assembly language syntax changed. gnu for example has
|
||
changed a few subtle things with respect to most of the processors
|
||
they support with their assembler. Naturally as programmers we want
|
||
labor saving features to our programming tools and languages and
|
||
assembly language is no different. Look at the C function from above
|
||
|
||
unsigned int myfun ( void )
|
||
{
|
||
int a=5;
|
||
return(a+7);
|
||
}
|
||
|
||
The syntax unsigned, int, myfun, void, int and even the variable
|
||
name itself are not actually converted to actions we want the
|
||
processor to perform. They are part of the syntax that is there
|
||
to support us telling the processor what to do and assembly language
|
||
has labels and defines and other similar features. And that extra
|
||
stuff is another area where one assembler (software tool) may vary
|
||
from another. The short answer here is that the processor defines
|
||
the machine code or machine language and that cannot vary, but the
|
||
assembler, the tool that parses the assembly language program, defines
|
||
what the assembly language is and so long as the assembler generates
|
||
machine code that conforms to the processor the assembler can define
|
||
whatever programming language syntax it wants. You will soon see
|
||
that I try to write my code to lean toward portable and reusable and
|
||
try to avoid tool specific features because those things change
|
||
over time and those things are definitely not portable so you have
|
||
to re-write those portions more than the body of the program. A
|
||
weirdism you will see from me for example is that the assembly language
|
||
world almost universally uses a semicolon (;) to mark a comment, the
|
||
rest of the line after a semicolon is ignored as a comment. But
|
||
the gnu assembler folks (gas is a shortcut for gnu assembler) for the
|
||
ARM assembler defined the semicolon to separate instructions on the
|
||
same line. Assembly langauges almost universally only allow one
|
||
instruction per line, so this is pretty insane behavior by the gas
|
||
folks. They chose to use the @ sign to mark a comment, so my
|
||
weridism or protest or whatever is I often use ;@ for comments, there
|
||
was a time that I had access (the folks I worked for were willing to
|
||
pay for) the ARM tools from ARM and I was writing assembly back
|
||
and forth between ARM tools and GNU tools so if you try to make as
|
||
much of the code not have to be re-written the combination of ;@ will
|
||
give you a comment on both...
|
||
|
||
Registers, these are the variables of assembly language, different
|
||
processors have different numbers of them and different sizes sometimes
|
||
some are general purpose some are special purpose. Back to the
|
||
ARMv5 ARM ARM, section A2.3 Registers, now ARM tries to confuse us
|
||
by saying
|
||
|
||
The ARM processor has a total of 37 registers:
|
||
Thirty-one general-purpose registers
|
||
|
||
From an assembly language programmers perspective the ARM actually
|
||
has only 16 general purpose registers there names are r0,r1,r2,r3...
|
||
to r15. r15 is a special purpose register it is called the
|
||
program counter. Program counter is a generic processor term it
|
||
keeps track of the programs address. We talked above about
|
||
the first instruction after reset is address 0x00000000 then to
|
||
run on the Raspberry Pi we need that first instruction to jump or
|
||
branch to address 0x00008000 the program counter is the register that
|
||
that keeps track of those addresses for us. Probably all of our
|
||
Raspberry Pi ARM programs will start with an instruction at 0x0000 then
|
||
one at 0x8000 and one at 0x8004 and one at 0x8008 and at some point
|
||
we are going to jump or branch or something and go backwards or skip
|
||
some and so on. The program counter keeps track of that. All
|
||
processors have one usually they use the term program counter or PC,
|
||
but not always. And not all processor families let you access the
|
||
PC but ARM does. And you can mess yourself up if you try to modify
|
||
r15 that can and will make the processor change course to execute the
|
||
instruction at the address to changed r15 to so we have to be careful
|
||
with r15. The other 15 registers r0-r14 do not have that problem.
|
||
Now there are two other registers that are special in some way one is
|
||
because it is hardcoded by the logic for some of the instructions
|
||
the other is used as the stack pointer as a convention, you could
|
||
technically use another register as you will see but ARM inteded
|
||
r13 to be the stack pointer and we will get into what a stack is
|
||
and a stack pointer in a bit.
|
||
|
||
In the ARMv5 ARM ARM the same A2.3 Registers section Figure A2-1
|
||
Register organization
|
||
|
||
So what this is showing us is where that weird count of 37 registers
|
||
came from. Vertically we have these processor Modes, which is another
|
||
topic for later, but what it is trying to show here is for example
|
||
there is only one r0 register, when you switch modes you dont switch
|
||
to a different r0 there is only one r0. But for example there are
|
||
many r13 registers, there is one r13 shared by User and System mode
|
||
but Supervisor has its own r13 that is not the same, if you set
|
||
r13 to some value while in supervisor mode then you switch to user
|
||
mode and have an isntruction that uses r13 it will not have the
|
||
same value because it is a different r13 that gets wired in when
|
||
you switch modes. r14 the same, the cpsr/spsr which we will talk
|
||
about later. Fast interrupt mode has a bunch of registers that are
|
||
special to that mode and we will cover that later as well. For almost
|
||
all of this document assembly or C we are going to stay in supervisor
|
||
mode and we have 16 registers to worry about r0 to r15.
|
||
|
||
So chapters A3 and A4 in the ARMv5 ARM ARM begin to cover the
|
||
instruction set the machine code, ARM has also defined their
|
||
assembly language syntax here as well. When it comes to the
|
||
assembly language that has a one to one relationship with machine
|
||
language instructions the gnu assembler and this documentation are
|
||
in sync, if we hit a variation we will talk about it then. The
|
||
ARMv7 ARM ARM also defines the instruction set and being newer it
|
||
includes the ARMv4, v5, v6 and v7 instructions and for each will
|
||
tell you which architectures support that instruction. So using
|
||
the newer manual will help figure out which instructions were added
|
||
at what time. The older manual generally shows instructions that
|
||
are supported on all future processors (there are maybe one or a few
|
||
exceptions).
|
||
|
||
lets stick with the ARMv5 ARM ARM for a little longer, A4.1 is
|
||
the alphabetical list of ARM instructions, dont push down the thumb
|
||
instruction path just yet. So lets start by adding two numbers together
|
||
how about 5 and 7. In C we would might do something like
|
||
|
||
unsigned int a;
|
||
unsigned int b;
|
||
unsigned int c;
|
||
|
||
a = 5;
|
||
b = 7;
|
||
c = a + b;
|
||
|
||
For now we have complete freedom to use almost any general purpose
|
||
register (gpr) that we want for our programs (naturally avoiding r15).
|
||
|
||
So go to A4.1.35 MOV.
|
||
|
||
Under syntax we see
|
||
|
||
MOV{<cond>}{S} <Rd>, <shifter_operand>
|
||
|
||
And it describes each of these items Rd is the register we want to
|
||
put our number in (r0 - r15 the one we choose). The thing we are
|
||
moving into Rd, the shifter operand is generic here because there
|
||
are a number of different flavors of MOV that we can use. To find
|
||
these we follow the documents link and go to
|
||
|
||
Addressing Mode 1 -Data-processing operands on page A5-2,
|
||
|
||
The one we are going to use is
|
||
|
||
1.
|
||
#<immediate>
|
||
See Data-processing operands - Immediate on page A5-6.
|
||
|
||
The term immediate with respect to machine code means that the value
|
||
is found in the immediate area, basically the value is part of the
|
||
machine code. The short answer is that our first two instructions are
|
||
|
||
mov r0,#5
|
||
mov r1,#7
|
||
|
||
Some assemblers make you use capitals for the syntax, but we dont have
|
||
to for these ARM tools. We are not going to worry about the optional
|
||
{<cond>} and {S} parameters.
|
||
|
||
Our third and last instruction to perform this task is A4.1.3 ADD
|
||
|
||
ADD{<cond>}{S} <Rd>, <Rn>, <shifter_operand>
|
||
|
||
And to shortcut the hop through the document in this case the shifter
|
||
operand we are using is Rm another register, the instruction we want
|
||
is
|
||
|
||
add r2,r0,r1
|
||
|
||
Mentally read this instruction by replacing the commas
|
||
|
||
add r2=r0+r1
|
||
|
||
Our first ARM program
|
||
|
||
mov r0,#5
|
||
mov r1,#7
|
||
add r2,r0,r1
|
||
|
||
so lets assemble this code and then disassemble it.
|
||
|
||
arm-none-eabi-as fun.s -o fun.o
|
||
arm-none-eabi-objdump -D fun.o
|
||
|
||
fun.o: file format elf32-littlearm
|
||
|
||
|
||
Disassembly of section .text:
|
||
|
||
00000000 <.text>:
|
||
0: e3a00005 mov r0, #5
|
||
4: e3a01007 mov r1, #7
|
||
8: e0802001 add r2, r0, r1
|
||
|
||
The gnu tools work like most toolchains capable of more than tiny
|
||
projects, your source code files are compiled or assembled into
|
||
object files. Object files have the machine code for the instructions
|
||
plus some extra stuff to help the linker do its job. The code in an
|
||
object file doesnt know where in memory it is going to live that is
|
||
the linkers job. For example if we wanted these three instructions
|
||
to live starting at address 0x8000 the object file doesnt know that
|
||
the linker will be told to do that and the linked binary will
|
||
reflect the 0x8000 address. Since the object doesnt know this the
|
||
disassembly shows address 0x0000.
|
||
This e3a00005 is the machine code for mov r0, #5, we can go back
|
||
to the ARM ARM and see that the 32 bit machine code definition is
|
||
broken into a number of fields of which some are defined as either
|
||
zero or one and those bits forced to zero or one are the ones that
|
||
make this instruction a mov and not an add or some other instruction.
|
||
So we see from the doc
|
||
xxxx00x1101xxxxx....
|
||
and from the disassembly
|
||
111000111010....
|
||
|
||
xxxx00x1101xxxxx....
|
||
111000111010....
|
||
|
||
They match.
|
||
|
||
Also we see bits 15:12 are 0b0000 for the mov r0 instruction and that
|
||
matches what we programmed (0b0000 = r0). The second instruction
|
||
has 0b0001 in those bits which are also correct 0b0001 = r1, 0b0010 =
|
||
r2 and so on.
|
||
|
||
SBZ means Should Be Zero and those bits are also zero, although
|
||
should is not equal to must otherwise those bits would explicitly be
|
||
defined as zeros. Not for us to worry about right now but these
|
||
could be bits that are ignored by this instruciton in the processor
|
||
and maybe in the future these bits could be used to create a new
|
||
instruction where zeros is mov and something else is the new instruction.
|
||
|
||
Note that most folks are not going to teach assembly by talking you
|
||
through machine code as well. I find that at least loosly understanding
|
||
the machine code helps with the assembly language, it resolves many
|
||
otherwise unanswered questions, why cant I do this, why can I do that
|
||
and the answer being simple, because the instruction set, the machine
|
||
code does not permit it. As to the whys and why nots of the machine
|
||
code well the short answer there is it is because that is how the
|
||
designers of the processor desinged the instruction set, if you can
|
||
find and ask them go ahead but otherwise it is what it is, deal with it.
|
||
|
||
We can do this with the ADD instruction as well.
|
||
|
||
e0802001 add r2, r0, r1
|
||
|
||
xxxx00x0100xxxx document
|
||
111000001000xxx disassembly
|
||
|
||
Now just like in C there is more than one way to do things...
|
||
|
||
unsigned int a;
|
||
|
||
a = 5;
|
||
a = a + 7;
|
||
|
||
Our second program
|
||
|
||
mov r6,#5
|
||
add r6,r6,#7
|
||
|
||
assemble and disassemble:
|
||
|
||
arm-none-eabi-as fun.s -o fun.o
|
||
arm-none-eabi-objdump -D fun.o
|
||
|
||
fun.o: file format elf32-littlearm
|
||
|
||
|
||
Disassembly of section .text:
|
||
|
||
00000000 <.text>:
|
||
0: e3a06005 mov r6, #5
|
||
4: e2866007 add r6, r6, #7
|
||
|
||
The next thing we need to learn to aim for an interesting program on
|
||
hardware is to make a loop:
|
||
|
||
mov r0,#0
|
||
top:
|
||
add r0,r0,#1
|
||
cmp r0,#7
|
||
bne top
|
||
|
||
assemble and disassemble:
|
||
|
||
arm-none-eabi-as fun.s -o fun.o
|
||
arm-none-eabi-objdump -D fun.o
|
||
|
||
fun.o: file format elf32-littlearm
|
||
|
||
|
||
Disassembly of section .text:
|
||
|
||
00000000 <top-0x4>:
|
||
0: e3a00000 mov r0, #0
|
||
|
||
00000004 <top>:
|
||
4: e2800001 add r0, r0, #1
|
||
8: e3500007 cmp r0, #7
|
||
c: 1afffffc bne 4 <top>
|
||
|
||
|
||
Now the indentation doesnt matter just makes it a little easier to read.
|
||
|
||
text with a colon is a label just like in C, so top: is not an
|
||
instruction we will use it later. The mov and add we know, cmp is new.
|
||
Section A4.1.15 CMP shows us under Operation what is going on, for now
|
||
assume the condition code passed so we go into alu_out = Rn - shifter_operand.
|
||
in this case alu_out = r0 - 7. Then it gets into flags, the flag we
|
||
care about is the Z flag which says if alu_out == 0 then 1 else 0.
|
||
The first time we run through this loop r0 by the time it hits the
|
||
cmp instruction is equal to a 1 and 1 - 7 is not equal to 0 so the z
|
||
flag will be a 0.
|
||
|
||
We will come back to the cmp instruction, lets look at the bne
|
||
instruction, the first problem is there is no BNE listed in the
|
||
alphabetical list of instructions. What we are looking for is
|
||
A4.1.5 B,BL and now we have to talk about {<cond>}. bne is really
|
||
a B instruction with a condition code of NE and if we look at the
|
||
operation for this instruciton if the condition passes then
|
||
if L == 1 then, that is the BL instruction so we dont care about that,
|
||
so on to PC = PC + (SignExtend_30(signed_immed_24) << 2). Basically
|
||
if the condition code passes then we are modifying the pc, and
|
||
hopefully the modification is such that we branch (jump) back to the
|
||
top label, add one more to r0 and keep doing that until the condition
|
||
code doesnt pass. But how do I know it is going to do that?
|
||
|
||
A3.2 talks about the condition field. All of the ARM mode instructions
|
||
(thumb mode is later) start with a 4 bit condition field. Up until
|
||
now we have been operating with the default of AL or always encoded as
|
||
0b1110 which is such that the condition code always passes. For the
|
||
bne, ne is the condition code, and the description says Z clear, so the
|
||
ne codition code will pass if the Z flag is clear. The Z flag is
|
||
modified by the cmp instruction in this loop or lets say the Z flag
|
||
doesnt change after the cmp and before the bne. So cmp is defining
|
||
the state of the z flag for the bne instruction. And what we need
|
||
to do to get the z flag a zero (clear) then r0 - 7 has to equal zero
|
||
and that will happen when r0 = 7. So the first time through
|
||
r0 = 1, z is 1, bne (branch if not equal, branch if r0 is not equal to 7)
|
||
branches back to top, we add one more, r0 = 2, z is still 1, and this
|
||
continues for r0 = 3,4,5,6,7 and when r0 = 7 then z is 0 and the bne
|
||
does not modify the pc so the program will continue to whatever
|
||
instruction we program after bne.
|
||
|
||
Now if we change the program to this
|
||
|
||
mov r0,#0
|
||
top:
|
||
add r0,r0,#1
|
||
cmp r0,#7
|
||
b top
|
||
|
||
The b instruction is now unconditional it uses the default of always
|
||
as the condition so it always brances. The cmp can modify all the
|
||
flags it wants it wont change the branch.
|
||
|
||
So what are and where are flags. Flags are individual bits in a register
|
||
generically called the program status word. In section A2.5 ARM
|
||
calls them Program status registers. bit 30 is the Z flag, bit
|
||
31 the N flag, 29 is C and 28 is V the four that we generally deal with
|
||
and will worry about later. ARM has names for their program status
|
||
registers CPSR and SPSR. We care about and maybe sometimes use CPSR
|
||
the current program status word. SPSR is the saved program status
|
||
word and is used to save a copy of the CPSR in case we need to say
|
||
handle an interrupt and then return, if an interrupt happened between
|
||
the cmp and the bne above we dont want the interrupt to mess up
|
||
our Z flag. We will worry about interrupts later.
|
||
|
||
Next thing before we can play with hardware is I cheated a little. ARM
|
||
at least for what we are looking at uses fixed length instructions
|
||
in ARM mode (thumb is later) every instruction is exactly 32 bits or
|
||
4 bytes, no more no less. And you may have seen in A2.3 that the
|
||
registers are also 32 bits. And we have learned a enough about
|
||
machine code to know that we need some of those instruction bits to
|
||
tell the processor one instruction from another specifically the
|
||
mov instruction we saw that a bunch of the bits are consumed just
|
||
defining the parameters to the mov instruction, we moved an immediate
|
||
value of 5 and 7 and that worked fine, but what about a larger number
|
||
like 0x1234, or even worse 0x12345678 how could 0x12345678 possibly
|
||
fit in the 12 bit shifter operand?
|
||
|
||
mov r0,#0x12345678
|
||
|
||
arm-none-eabi-as fun.s -o fun.o
|
||
fun.s: Assembler messages:
|
||
fun.s:2: Error: invalid constant (12345678) after fixup
|
||
|
||
The answer is it cant. You cannot squeeze 32 bits into 12 bits without
|
||
losing some. Obviously there is a way to do this.
|
||
|
||
The assembly for this is
|
||
|
||
ldr r0,somenumber
|
||
...
|
||
somenumber:
|
||
.word 0x12345678
|
||
|
||
So the words (with no spaces) ending in a colon are labels. Labels
|
||
are simply addresses we dont know nor care what the actual address is
|
||
but to let the assembler do the work for us we give the label a name
|
||
and then somewhere else use that label to reference the address we
|
||
are interested in. Think about our function names in C those are just
|
||
labels and we expect the compiler and assembler and lastly linker
|
||
to finally give that label/function name an address so that other
|
||
code that wants to call it or jump to it or otherwise access that
|
||
address can. As programmers we use the label, we let the tools
|
||
do the hard work of figuring out how to get there.
|
||
|
||
if we look up the ldr instruction it stands for load register, load
|
||
is basically a read from some address. So somenumber is an address
|
||
we are asking the processor to read a word (a word is defined as 32
|
||
bits in the ARM world (intel x86 world it is 16 bits) see A2.1 Data
|
||
types) from the address somenumber and take the 32 bits you find
|
||
there and put them in register r0. The the label somenumber: tells
|
||
the assembler that when you are generating the machine code, whatever
|
||
address happens to be here in the program use that address for
|
||
somenumber wherever I have referenced that label. .word is a directive
|
||
to the assembler, it is not an instruciton, it tells the assembler I
|
||
want you to reserve a 32 bit memory location in the program and I want
|
||
you to put the value I have defined there. So the assembler is going
|
||
to put the 32 bit value at the address somenumber, it and/or the linker
|
||
will figure out what somenumber is and then ldr will know how to find
|
||
that 32 bit number. And there we go we can now load any 32 bit pattern
|
||
into a register.
|
||
|
||
Just to perhaps make this more clear
|
||
|
||
ldr r0,somenumber
|
||
top:
|
||
add r0,r0,#1
|
||
add r0,r0,#1
|
||
add r0,r0,#1
|
||
add r0,r0,#1
|
||
add r0,r0,#1
|
||
b top
|
||
somenumber:
|
||
.word 0x12345678
|
||
.word 0xABCD
|
||
|
||
assemble and disassemble which you know how to do now.
|
||
|
||
fun.o: file format elf32-littlearm
|
||
|
||
|
||
Disassembly of section .text:
|
||
|
||
00000000 <top-0x4>:
|
||
0: e59f0014 ldr r0, [pc, #20] ; 1c <somenumber>
|
||
|
||
00000004 <top>:
|
||
4: e2800001 add r0, r0, #1
|
||
8: e2800001 add r0, r0, #1
|
||
c: e2800001 add r0, r0, #1
|
||
10: e2800001 add r0, r0, #1
|
||
14: e2800001 add r0, r0, #1
|
||
18: eafffff9 b 4 <top>
|
||
|
||
0000001c <somenumber>:
|
||
1c: 12345678 eorsne r5, r4, #120, 12 ; 0x7800000
|
||
20: 0000abcd andeq sl, r0, sp, asr #23
|
||
|
||
|
||
I put the add instructions in there to give some space between
|
||
ldr and the address it was using. Now the ARM docs and the disassembly
|
||
are showing something interesting. Off to the right it tells us
|
||
the address is 1c which is the label somenumber.
|
||
|
||
What happened is the assembler is doing some math on the program
|
||
counter r15, it is saying add 20 to the program counter and then
|
||
use that as an address to read from memory, then take that value read
|
||
and put that in r0. Well 20 in decimal is 0x14 hex if this
|
||
instruction were really at address 0x000 then 0x0000+0x14 is 0x0014
|
||
but the number we want is at address 0x1C.
|
||
|
||
Well two things are going on. If you think about how a very simple
|
||
processor would have to work using the program counter as we have
|
||
loosly defined. The program counter would say the instruction
|
||
we want to execute is at address 0x0000 how it says that is that
|
||
register simply holds the address 0x0000. So the processor is ready
|
||
to execute the next instruction the pc is 0x0000 so it reads the
|
||
instruction 0xe59f0014 from memory. now what does the pc do? at
|
||
some point before it starts the next instruction at address 0x0004
|
||
it has to change from 0x0000 to 0x0004. Well many/most processors
|
||
do just that after reading (called fetching if you are reading
|
||
an instruction from memory) the instruction before actually executing
|
||
it they move the program counter so in this case that moves the
|
||
program counter to 0x0004. 0x0004 + 0x14 = 0x0018 we still are not
|
||
at the 0x001C where our data is and where the disassembler implied
|
||
it knew where our data is. That is the second thing going on, something
|
||
called pipelining. It is exactly similar to a production line,
|
||
you have stations along the production line the product is moved
|
||
from one station to another, each station performs a relatively simple
|
||
task on the product and the product moves on. Well a piplelined
|
||
processor does that as well. If you had say only one employee at the
|
||
assembly line then you could still have the assembly line but that
|
||
one employee could only do one of the tasks at a time. if there
|
||
were 100 tasks then it would take 100 steps and then they could start
|
||
over on the next product. But if you had 100 employees after
|
||
some time every station has a product in some partial state of
|
||
completion every step the first person starts the product from scratch
|
||
and every step the last person outputs a new product, so once all
|
||
the stations have filled up you get one product every step instead of
|
||
one product every 100 steps with the single employee. The 100
|
||
employees are working in parallel even though the production line is
|
||
serial. Well a processor has a few basic steps, first it has to fetch
|
||
the instruction from memory, then it has to decode it, look for those
|
||
fixed ones and zeros that tell it this is a mov instruction or an add
|
||
instruction or whatever. For the add we used above it then needs to
|
||
go get the operands it may have to go get r1 and then go get r2. And
|
||
then it actually executes, it does the add, then it saves the result
|
||
and done. The even simpler steps are fetch, decode, execute. Using
|
||
that simplistic model if we were to step through a mini assembly
|
||
line we would start with address zero entering the first station
|
||
the fetch, then the address 0x00 instruciton moves from the first
|
||
station to the second, decod. In parallel the 0x04 instruction is in
|
||
the first station execute. Then the next step the 0x00 instruction
|
||
moves to execute, 0x04 moves to decode and 0x08 moves to fetch. Fetch
|
||
in this case means the pc is 0x08 go fetch from 0x08. So when the
|
||
0x00 instruciton is executing the program counter is set to 0x08 the
|
||
address of the instruciton being fetched. That is two instructions
|
||
ahead not just the one we talked about before. That is the model
|
||
that ARM is operation on, when you execute an instruction the
|
||
program counter register is at an address two instructions ahead.
|
||
So when we execute the ldr instruction at address 0x00 that means
|
||
the program counter is two ahead, each is 0x04 so two ahead is
|
||
0x00+0x04+0x04 = 0x08. So if the pc is 0x0008 and we add the offset
|
||
of 0x14 we get 0x1C. Now here is the rub, that may have actually been
|
||
the tiny pipeline used in very early ARM processors, but for
|
||
reverse compatibility they preserved that two ahead rule for the PC,
|
||
but the actual logic we run on today has a much deeper pipeline and
|
||
how we dont get screwed up by having a program counter that is a bunch
|
||
of instructions ahead is the actual program counter used today
|
||
to keep track of fetching is not the same register we see as r15 it is
|
||
a hidden register, the logic we use today provides us with an r15 that
|
||
pretends to be the real pc but is actually a fake one two ahead. They
|
||
really had to do it that way. Had they known that down the road we
|
||
would not only have pipelined processors but much more complicated
|
||
processor internals and that they would no longer have to impose this
|
||
pc being adjusted by the pipeline, but instead would fake its value
|
||
I would like to think they would have simply faked the value as being
|
||
the address of the next instruciton 0x04 in this case not two after
|
||
0x08. And faked that address from the first pipelined processor to
|
||
the current pipelined processor.
|
||
|
||
Back to our problem of putting any value in a register.
|
||
|
||
|
||
ldr r0,somenumber
|
||
top:
|
||
add r0,r0,#1
|
||
add r0,r0,#1
|
||
add r0,r0,#1
|
||
add r0,r0,#1
|
||
add r0,r0,#1
|
||
b top
|
||
somenumber:
|
||
.word 0x12345678
|
||
.word 0xABCD
|
||
|
||
I added a few more lessons here. First off I put a branch before
|
||
the somenumber lable, what if I had not done that? Well what would
|
||
happen is the assembler would without a peep have assembled what I
|
||
told it to assemble:
|
||
|
||
|
||
ldr r0,somenumber
|
||
top:
|
||
add r0,r0,#1
|
||
add r0,r0,#1
|
||
add r0,r0,#1
|
||
add r0,r0,#1
|
||
add r0,r0,#1
|
||
somenumber:
|
||
.word 0x12345678
|
||
.word 0xABCD
|
||
|
||
|
||
|
||
fun.o: file format elf32-littlearm
|
||
|
||
|
||
Disassembly of section .text:
|
||
|
||
00000000 <top-0x4>:
|
||
0: e59f0010 ldr r0, [pc, #16] ; 18 <somenumber>
|
||
|
||
00000004 <top>:
|
||
4: e2800001 add r0, r0, #1
|
||
8: e2800001 add r0, r0, #1
|
||
c: e2800001 add r0, r0, #1
|
||
10: e2800001 add r0, r0, #1
|
||
14: e2800001 add r0, r0, #1
|
||
|
||
00000018 <somenumber>:
|
||
18: 12345678 eorsne r5, r4, #120, 12 ; 0x7800000
|
||
1c: 0000abcd andeq sl, r0, sp, asr #23
|
||
|
||
And if you look at that after that fifth add r0,r0,#1 the next
|
||
"instruction" is the bit pattern 0x12345678 and the processor would
|
||
fetch that pattern and try to execute it. And maybe that pattern is
|
||
an actual instruction or maybe not but no doubt it is not something
|
||
we meant to be an instruction. If you are going to do something like
|
||
this then you need to make sure you put that value somewhere that
|
||
is not in the execution path, but is close enough to the ldr in
|
||
this case so that the offset can be encoded in the instruction.
|
||
|
||
I also put the 0xABCD in there to illustrate a point, the
|
||
somenumber label resulted in the assembler deciding that that label
|
||
is at the address 0x18 in this last example. So a ldr of somenumber
|
||
gives us the value at that address which is 0x12345678, if we wanted
|
||
0xABCD just because it is a .word after the label doesnt mean it is
|
||
also at the same address, it cant be it is at address 0x1C or
|
||
somenumber+4. if we wanted to use this technique to load another
|
||
value that wont fit in the immediate field, then we need another
|
||
label.
|
||
|
||
ldr r0,hello
|
||
ldr r1,world
|
||
...
|
||
hello:
|
||
.word 0x12345678
|
||
world:
|
||
.word 0xABCD
|
||
|
||
And the gnu assembler will allow you to put the instruction or
|
||
directive on the same line, you dont have to use a separate line
|
||
|
||
ldr r0,hello
|
||
ldr r1,world
|
||
...
|
||
hello: .word 0x12345678
|
||
world: .word 0xABCD
|
||
|
||
Note .word is a gnu assembler specific directive I dont think that is
|
||
what the ARM assembler uses, it is not necessarily portable code.
|
||
|
||
Now both the ARM assembler and the GNU assembler have a nice little
|
||
program saving device for lazy programmers:
|
||
|
||
ldr r0,=0x12345678
|
||
ldr r1,=0xABCD
|
||
top:
|
||
add r0,r0,#1
|
||
add r0,r0,#1
|
||
add r0,r0,#1
|
||
add r0,r0,#1
|
||
add r0,r0,#1
|
||
b top
|
||
|
||
|
||
assemble and disassemble
|
||
|
||
fun.o: file format elf32-littlearm
|
||
|
||
|
||
Disassembly of section .text:
|
||
|
||
00000000 <top-0x8>:
|
||
0: e59f0018 ldr r0, [pc, #24] ; 20 <top+0x18>
|
||
4: e59f1018 ldr r1, [pc, #24] ; 24 <top+0x1c>
|
||
|
||
00000008 <top>:
|
||
8: e2800001 add r0, r0, #1
|
||
c: e2800001 add r0, r0, #1
|
||
10: e2800001 add r0, r0, #1
|
||
14: e2800001 add r0, r0, #1
|
||
18: e2800001 add r0, r0, #1
|
||
1c: eafffff9 b 8 <top>
|
||
20: 12345678 eorsne r5, r4, #120, 12 ; 0x7800000
|
||
24: 0000abcd andeq sl, r0, sp, asr #23
|
||
|
||
|
||
Generically the =something means the address of something. Whether or
|
||
not the thing after the equals is a label or a number the assembler
|
||
finds a location for you in a safe place (not in the execution path)
|
||
and then encodes a pc relative load (pc plus an offset). If the
|
||
thing after the equals is a label then the assembler (or linker) will
|
||
place the address in that location so that it can be loaded into
|
||
the register. By putting a number here we can cheat and get the
|
||
assembler to put that 32 bit value in our register. It is possible
|
||
that the assembler might not be able to find a place for our number
|
||
and that is where this shortcut can get you into trouble. Also
|
||
you dont get to control eactly where the number is placed so you
|
||
are giving up control to the assembler which is generally not what
|
||
an assembly language programmer wants to do.
|
||
|
||
So we can now put any bit pattern we want into a register, we can
|
||
loop, we roughly understand that ldr means load a register with
|
||
a value from an address. We also saw from the disassembly that we
|
||
can load from a register which holds an address, the ldr instructions
|
||
above are encoded as load from r15 plus an adjustment to r15. But we
|
||
can use another register.
|
||
|
||
ldr r0,=0x12345678
|
||
ldr r1,[r0]
|
||
|
||
The [brackets] mean a level of indirection, instead of the value r0
|
||
the bracket means the thing at the address in r0. The above code
|
||
means read from memory at address 0x12345678 and the value read put that
|
||
in r1.
|
||
|
||
There has to be a write instruciton as well right? Well load is a read
|
||
and store is a write, store something at an address.
|
||
|
||
|
||
ldr r0,=0x12345678
|
||
mov r2,#7
|
||
str r2,[r0]
|
||
|
||
This says write the number 7 to address 0x12345678.
|
||
|
||
Some magic that may or may not be obvious as a non-bare metal
|
||
programmer is that addresses dont only point at memory. The address
|
||
map for the ARM we saw a space starting at 0x20000000 where the
|
||
I/O peripherals live. Those peripherals are not ram the things at
|
||
those addresses which are defined in the rest of that Broadcom
|
||
manual. Reading and writing things in that address space cause
|
||
hardware stuff to happen.
|
||
|
||
Hopefully by now you have figured out that
|
||
|
||
int main ()
|
||
{
|
||
printf("Hello World!\n");
|
||
}
|
||
|
||
when run on your desktop or laptop is a massively complicated program
|
||
and obviously that is not at all an introduction program to bare
|
||
metal programming. The bare metal equivalent is turning on and/or
|
||
blinking an led.
|
||
|
||
(it should be painfully obvious that I wasnt kidding most of bare
|
||
metal is not programming but finding out the information from manuals
|
||
on what to program)
|
||
|
||
If/when you get a job as a bare metal programmer and work closely with
|
||
the hardware engineers they should already know but it is a good idea
|
||
to wire up an led to a general purpose I/O port and/or wire some
|
||
pads/test points to the general purpose I/O so that using an oscilloscope
|
||
or for your prototype board you can have an led added but that led
|
||
might not be on the production boards. The Raspberry Pi folks did
|
||
just that. You need to open one of the schematics mentioned above
|
||
I am looking at the rev 1 board. Now what we are looking for is a
|
||
symbol that has a triangle up against a line at the tip similar to the
|
||
symbol for fast forward or rewind on an mp3 player but with one
|
||
triangle not two. That is a diode symbol a light emitting diode
|
||
LED also has some sort of a lightning like symbol on or next to it
|
||
that indicates light comes out of it.
|
||
Sheet 04 of 05 upper middle of the page shows STATUS OK LED and
|
||
POWER ON LED and has a diode symbol with two arrows pointing out.
|
||
The things we care about from the schematic are following one wire
|
||
we see the signal name STATUS_LED_N and the other end the wire
|
||
is connected to +3V3 which they are indicating 3.3Volts which is the
|
||
amount of voltage that powers stuff on this board. Now from
|
||
middle school science class we know that if you want to turn the
|
||
light on you need to complete the circuit. To complete the circuit
|
||
in this case means one end of that wire needs to be on the power
|
||
voltage (3.3V) and the other end ground to make the power flow. If
|
||
one end is left hanging then no power flows no light, also you probably
|
||
didnt do this in middle school. If both ends are tied to 3.3V then no
|
||
power flows the light doesnt come on. So now go to the upper middle
|
||
left of Sheet 02 of 05. What you are looking for is status_led_n
|
||
is connected to a box labelled BCM2835 and the thing it is wired to
|
||
is GPIO 16. So we are done with the schematic for now, we can
|
||
mess with the status led by messing with gpio 16. In general and
|
||
true with this processor, if we make gpio 16 an output and if we write
|
||
a 0 to that gpio pin we will make it 0Volts or ground and that means
|
||
the electricity flows and the led comes on. If we write a 1 that makes
|
||
the pin 3.3Volts, no electricity flows the led goes off.
|
||
|
||
Now to the Broadcom BCM2835 manual, chapter 6 General Purpose I/O (GPIO).
|
||
There is a diagram there, and it is certainly not obvious what is
|
||
going on, but basically we will be messing with the Pin set and
|
||
clear registers which affect the output state, which work their
|
||
way left to the box on the left side which represents the gpio pin.
|
||
For safety reasons (dont let the smoke out) GPIO pins typically are
|
||
configured after reset as inputs.
|
||
|
||
So now we get serious. Remember this document uses the 0x7Exxxxxx
|
||
based addresses for peripherals but that 0x7E hs to be replaced with
|
||
0x20 for ARM. We need to make pin 16 an output. Fumbling around
|
||
in this chapter we see
|
||
|
||
"All pins reset to normal GPIO input operation."
|
||
|
||
So we know we need to change it from input to output. We also see
|
||
in Table 6-2 – GPIO Alternate function select register 0 it shows
|
||
a chart for FSEL9 that describes bit patterns for that three bit
|
||
field that controls the function for that gpio, input, output, and
|
||
the alternate functions. What we take away from this is that to make
|
||
a pin an output we need to set the three bits that control that
|
||
pin to the bit pattern 0b001.
|
||
|
||
Table 6-3 – GPIO Alternate function select register 1
|
||
|
||
Contains the bits FSEL16 which are not obviously connected to GPIO16 but
|
||
that is what they mean. The bits we need to change to 0b001 are bits
|
||
18 to 20 so 18 needs to be a 1, 19, a 0 and 20 a 0. Some peripherals
|
||
and/or some processors have a way that makes it easy to modify just
|
||
some of the bits in a register. This is not one of those cases we can
|
||
only access this register on complete 32 bit reads or writes. The
|
||
proper way to modify these bits is read the register, modify the three
|
||
bits then write the register back. The power on state for this register
|
||
is supposed to be all zeros (that is what the reset column means) so
|
||
we can cheat for the purpose of this example and just write the whole
|
||
register zeros for the other pins and 0b001 for gpio 16. That
|
||
means the value we need to write is 0x00040000. Now the address to
|
||
write to. Function select register 1, we go up a few pages to
|
||
6.1 Register View. GPFSEL1 at address 0x7E200004 for the VC which is
|
||
0x20200004 for the ARM.
|
||
|
||
Now that just makes 16 an output, now we need to control the state
|
||
of that pin a 0 or 1 (0 volts/ground or 3.3Volts). Fumble around some
|
||
more and we see the GPSETn registers, we can figure out from the
|
||
table above the n is either 0 or 1 GPSET0, GPSET1.
|
||
|
||
Table 6-8 – GPIO Output Set Register 0
|
||
|
||
If a bit is set in that register when we write to it then the GPIO
|
||
pin changes to a 0.
|
||
|
||
Table 6-9 – GPIO Output Set Register 1
|
||
|
||
If a bit is set in that register when we write to it then the GPIO
|
||
pin changes to a 1.
|
||
|
||
This is one of those cases where they have given us an easy way to
|
||
change one output without messing up the others while still being
|
||
limited to 32 bit writes.
|
||
|
||
The GPSET0 register is at ARM address 0x2020001C and the GPSET1
|
||
register is at ARM address 0x20200020.
|