The obvious solution for converting any program to assembly is... to
compile it! You can disassemble the resulting binary with a command
like:
avr-objdump -S -C the_program.elf > the_program.lss
This will output a listing of the program, with the original source
interspersed with an hex dump of the binary and the corresponding
disassembly.
The part I am really stuck on is the analogRead() function.
Here it goes:
000003a6 <analogRead>:
3a6: 8e 30 cpi r24, 0x0E ; 14
3a8: 08 f0 brcs .+2 ; 0x3ac <analogRead+0x6>
3aa: 8e 50 subi r24, 0x0E ; 14
3ac: 87 70 andi r24, 0x07 ; 7
3ae: 20 91 00 01 lds r18, 0x0100 ; 0x800100 <__data_start>
3b2: 90 e4 ldi r25, 0x40 ; 64
3b4: 29 9f mul r18, r25
3b6: 90 01 movw r18, r0
3b8: 11 24 eor r1, r1
3ba: 82 2b or r24, r18
3bc: 80 93 7c 00 sts 0x007C, r24 ; 0x80007c <__TEXT_REGION_LENGTH__+0x7e007c>
3c0: 80 91 7a 00 lds r24, 0x007A ; 0x80007a <__TEXT_REGION_LENGTH__+0x7e007a>
3c4: 80 64 ori r24, 0x40 ; 64
3c6: 80 93 7a 00 sts 0x007A, r24 ; 0x80007a <__TEXT_REGION_LENGTH__+0x7e007a>
3ca: 80 91 7a 00 lds r24, 0x007A ; 0x80007a <__TEXT_REGION_LENGTH__+0x7e007a>
3ce: 86 fd sbrc r24, 6
3d0: fc cf rjmp .-8 ; 0x3ca <analogRead+0x24>
3d2: 80 91 78 00 lds r24, 0x0078 ; 0x800078 <__TEXT_REGION_LENGTH__+0x7e0078>
3d6: 20 91 79 00 lds r18, 0x0079 ; 0x800079 <__TEXT_REGION_LENGTH__+0x7e0079>
3da: 90 e0 ldi r25, 0x00 ; 0
3dc: 92 2b or r25, r18
3de: 08 95 ret
Now, in order to more easily make sense of it, you need to know what all
those magic numbers mean. The command
avr-nm -nC the_program.elf
will dump the symbols associated with every RAM and flash address. In
order to tell those apart, 0x800000 is added to every RAM address. This
will tell you that, in this specific program, the RAM address 0x0100
is actually the address of the variable analog_reference
from the
Arduino core library. Thus, you can replace the line
3ae: 20 91 00 01 lds r18, 0x0100 ; 0x800100 <__data_start>
by
lds r18, analog_reference
Then you can use the datasheet to get the names of the memory-mapped IO
registers, and the names of the bits from those registers. I have done
all this for you, so here is the cleaned-up disassembly of
analogRead()
:
analogRead:
cpi r24, 14
brcs 1f
subi r24, 14
1: andi r24, 0x07
lds r18, analog_reference
ldi r25, 1<<REFS0
mul r18, r25
movw r18, r0
clr r1
or r24, r18
sts _SFR_MEM_ADDR(ADMUX), r24
lds r24, _SFR_MEM_ADDR(ADCSRA)
ori r24, 1<<ADSC
sts _SFR_MEM_ADDR(ADCSRA), r24
2: lds r24, _SFR_MEM_ADDR(ADCSRA)
sbrc r24, ADSC
rjmp 2b
lds r24, _SFR_MEM_ADDR(ADCL)
lds r18, _SFR_MEM_ADDR(ADCH)
ldi r25, 0
or r25, r18
ret
If you compare this with the source code of analogRead()
, it
should now make perfect sense.
I am trying to implement a program in assembly
Now, if you want to write the assembly yourself, that's a whole
different story. If you allow yourself the use of the Arduino core, then
you can simply
ldi r24, 14 ; pin A0 is pin 14
call analogRead
If you don't want to rely on external libraries, and really write
everything yourself, it's going to be a lot harder. I suggest you do
this as a two-step process:
Step 1: Learn how to program the ATmega328P in plain C, at the
register level, using no library other than the avr-libc. You can start
by reading this short tutorial about direct port access.
But then you should quickly move on to studying the
datasheet and the manual of the avr-libc. Yes,
that's a lot to digest. I hope you didn't expect this to be quick and
easy. But note that, on those documents, you can skip all the chapters
related to functions you are not going to use.
I am really stuck on is the analogRead() function.
In the specific case of your example program, you don't need a version
of analogRead()
that can read any pin with any reference. You can
instead write a simpler version that just reads the currently selected
pin using the currently selected reference:
int simple_analog_read(void)
{
ADCSRA |= 1<<ADSC; // start the converion
while (ADCSRA & (1<<ADSC)) continue; // wait till it's done
return ADC; // return the reading
}
Of course, this assumes you have appropriately initialized the ADC.
int voltage = sensorValue * (5.0 / 1023.0);
You really, really don't want to implement something like this in
assembly. The AVR hardware supports neither floating point nor
divisions. All this is handled by the compiler through supporting
libraries, and they are not simple. What you really want is something
like
int voltage = (sensorValue * 5) >> 10;
which is incidentally more correct, as the scaling factor is 5/1024, not
5/1023 (see the datasheet).
Step 2: Learn assembly, and rewrite your low-level C code in
assembly. If your C code is low-level enough this should not be too
hard. As a learning aid, you can ask the compiler to do it for you and
then try to make sense of what it did. If you just try to compile the
simple_analog_read()
function I wrote above you will see the
translation to assembly is almost trivial.