Loading Data from Program Memory

Program Memory, a.k.a. flash, can be loaded with constants like arrays and strings when programming your chip. Accessing Program Memory at runtime can be done with just two instructions, shown in the table below.

Mnemonic Description
lpm load program memory
spm store program memory

Storing data to Program Memory at runtime warrants a separate tutorial and is really only used for bootloader programs. Here we will look at the much simpler and more commonly used concept of loading data from Program Memory.

Defining Constants in Program Memory

Like we saw in the SRAM tutorial, space can be allocated in Program Memory for storing values. However, unlike SRAM, values can be written directly to flash by your programmer which you can access immediately. There are a few directives used for allocating space, shown in the table below.

Directive Description
.db define constant byte(s)
.dw define constant word(s)
.dd define constant double word(s)
.dq define constant quad word(s)

Defining a sequence of bytes can be done as

myBytes:  .db	0x00,0x01,0x02,0x03,0x04,0x05
myNums:   .db	0,1,2,3,4,5
myChars:  .db	'H','e','l','l','o','!'
myString: .db	"Hello!"

The same can be done with words (16-bits) using .dw

myWords:  .dw	0x0000,0x0001,0x0002,0x0003

Double words (32-bits) using .dd

myDword:  .dd	0xDEADBEEF

And quad words (64-bits) using .dq

myQword:  .dq	0x0123456789ABCDEF

Space allocated in Program Memory must be an even number of bytes. If it is not, the assembler will automatically append a zero byte as padding. The assembler will generate a warning to notify you but will still compile. For example, this will generate a warning

myBytes:  .db	0,1,2			; padding byte will be added

Loading Data from Program Memory

Loading data from Program Memory can only be done indirect using the Z pointer. This is shown below

	ldi	ZL,LOW(2*var)		; load 2*var
	ldi	ZH,HIGH(2*var)		; into Z pointer

	lpm	r4,Z			; load pmem var into r4

var: 	.db	3

Note that the address 2*var is loaded into the Z pointer rather than var. The reason for this is that Program Memory is word addressed. Thus each address in Program Memory holds two bytes. However, the Z pointer is byte addressed. As shown below, for every word address, there are two byte addresses.


The word address 0x0000 contains the byte addresses 0x00 and 0x01, word address 0x0001 contains byte addresses 0x02 and 0x03 and so on. Thus, a word address can be converted to its byte addresses by multiplying it by two for the lower byte, and multiplying it by two and adding one for the higher byte.

For example, the higher and lower bytes of a word stored at the label myWord would be accessed as

	ldi	ZL,LOW(2*myWord)	; load 2*myWord
	ldi	ZH,HIGH(2*myWord)	; into Z pointer

	lpm	r4,Z			; load lower byte of var into r4 (0x34)

	ldi	ZL,LOW(2*myWord)+1	; increment Z low

	lpm	r5,Z			; load upper byte of var into r5 (0x12)

myWord: .db	0x1234

Post Increment

The Z pointer can be incremented after each lpm by placing a + after it, e.g.

	lpm	r16,Z+			; load from program memory and increment Z

Using this, the above example of loading a word could be simplified as

	ldi	ZL,LOW(2*myWord)	; load 2*myWord
	ldi	ZH,HIGH(2*myWord)	; into Z pointer

	lpm	r4,Z+			; load lower byte of var into r4 (0x34)
	lpm	r5,Z			; load upper byte of var into r5 (0x12)

myWord: .db	0x1234

A Quick Example

Suppose you have an array defined in Program Memory and you want to transfer it to SRAM. The following program shows an example of how this could be done.

	.include "m328pdef.inc"

	.equ	numB 	= 20		; number of bytes in array

	.def	tmp	= r16		; define temp register
	.def	loopCt	= r17		; define loop count register

	.dseg
	.org	SRAM_START
sArr:	.BYTE	numB			; allocate bytes in SRAM for array

	.cseg
	.org	0x00
	ldi	XL,LOW(sArr)		; initialize X pointer
	ldi	XH,HIGH(sArr)		; to SRAM array address

	ldi	ZL,LOW(2*pArr)		; initialize Z pointer
	ldi	ZH,HIGH(2*pArr)		; to pmem array address

	ldi	loopCt,numB		; initialize loop count to number of bytes

arrLp:	lpm	tmp,Z+			; load value from pmem array
	st	X+,tmp			; store value to SRAM array
	dec	loopCt			; decrement loop count
	brne	arrLp			; repeat loop for all bytes in array

loop:	rjmp	loop			; infinite loop

pArr:	.db	0,1,2,3,4,5,6,7,8,9,\
		10,11,12,13,14,15,16,\
		17,18,19		; program memory array

The X pointer is used to access our array location in SRAM, so it is loaded with a Data Memory address

	ldi	XL,LOW(sArr)		; initialize X pointer
	ldi	XH,HIGH(sArr)		; to SRAM array address

The Z pointer is used to access our array location in Program Memory, so it is loaded with 2 times its Program Memory address

	ldi	ZL,LOW(2*pArr)		; initialize Z pointer
	ldi	ZH,HIGH(2*pArr)		; to pmem array address

We then setup a simple loop to iterate through the number of bytes in our array (which we know beforehand). We load individualy bytes from Program Memory into the tmp register and then store them to SRAM. You can see how pointer incrementing significantly simplifies the code

	ldi	loopCt,numB		; initialize loop count to number of bytes

arrLp:	lpm	tmp,Z+			; load value from pmem array
	st	X+,tmp			; store value to SRAM array
	dec	loopCt			; decrement loop count
	brne	arrLp			; repeat loop for all bytes in array

Note the \ in the Program Memory array definition. This is used as a line continuation to keep our code from getting too wide.

pArr:	.db	0,1,2,3,4,5,6,7,8,9,\
		10,11,12,13,14,15,16,\
		17,18,19		; program memory array
rjhcoding.com 2018