Beginner trying to set up timer IRQ in assembler: how to print CSRs in GDB?


(S) #1

I am trying to set up a machine timer interrupt using the following code (on the HiFive1 RevB on Linux with the freedom-e-SDK on the CLI. I just post the most relevant parts here; it’s written in assembler for the most part because that’s what I am trying to learn…):

main.c


#include <stdio.h>
#include "super_blink.h"

int main() {
		setup_GPIO();
		setup_timer_irq();

		// set a delay so the first interrupt handler can fire
		delay(DELAY);

		while (1) {
			// the idea being that delay sets the timer
			// for the first interrupt. In the interrupt
			// routine, we set the next delay so all we need
			// to do here is to loop endlessly
		}
}

setup_timer_irq.S

.section .text
.align 2
.global setup_timer_irq
.include "gpio.inc"
.include "memory_map.inc"

setup_timer_irq:
	addi sp, sp, -16 # Allocate stack frame
	sw ra, 12(sp)    # save return address to the stack

	lw t0, irq_handler  # load address of irq_handler 
	slli t0, t0, 1      # shift address left by one; LSB will be 0 which is direct mode (what we want)
	csrw mtvec, t0      # write handler address and mode

  # Here we cannot use csrsi because it can only set up to 5 bits
	li t0, 0x88     # set 4rd and 8th bit
	csrw mie, t0    # activate machine timer interrupt

	# write high value to the timercmp just to be sure we don't
	# trigger an interrupt by mistake
	li t0, 0x7FFFFFFF  # high number
	li t1, 0xFFFFFFFF  # high number
	li t2, MTIMECMP    # load address of timecmp register
	sw t0, 4(t2)       # store the first number to the upper part first
	sw t1, 0(t2)       # then the second to the lower part

	csrsi mstatus, 0x4  # set 3rd bit to activate machine interrupts.

	lw ra, 12(sp)   # load return address
	addi sp, sp, 16 # deallocate stack frame
	ret

irq_handler:
	# save only the registers we use in this function
	addi sp, sp, -28 # Allocate stack frame
	sw ra, 24(sp)    # save return address to the stack
	sw t0, 20(sp)    # save temporary
	sw t1, 16(sp)    # save temporary
	sw t2, 12(sp)    # save temporary
	sw t3, 8(sp)     # save temporary
	sw a0, 4(sp)     # save first argument to function

	li t0, GPIO_CTRL_ADDR      # load base GPIO address
	lw t1, GPIO_OUTPUT_VAL(t0) # load state

	beqz t1, switchOnRed  # if none is blinking yet, blink the red one
	li t3, GPIO_RED_LED
	beq t1, t3, switchOnBlue   # if red is on, switch on blue LED
	li t3, GPIO_BLUE_LED
	beq t1, t3, switchOnGreen   # if blue is on, switch on green LED
	# here, neither the red or blue are on, so the one that is on
	# must be the green one. In that case we just fall through to
	# switch on the red LED

switchOnRed:
	li t2, GPIO_RED_LED   # load value indicating red LED
	j exit

switchOnBlue:
	li t2, GPIO_BLUE_LED   # load value indicating blue LED
	j exit

switchOnGreen:
	li t2, GPIO_GREEN_LED   # load value indicating green LED
	j exit


exit:
	sw t2, GPIO_OUTPUT_VAL(t0)   # write new LED values to the right address
	li a0, 200      # load the desired delay value (in ms) as the first arg to the function
	jal delay       # call delay function to set new delay

	lw ra, 24(sp)   # load return address
	lw t0, 20(sp)   # restore temporary
	lw t1, 16(sp)   # restore temporary
	lw t2, 12(sp)   # restore temporary
	lw t3, 8(sp)    # restore temporary
	lw a0, 4(sp)    # restore first argument to function
	addi sp, sp, 28 # deallocate stack frame
	mret            # exit IRQ handler

(note that this is most likely buggy since I have no prior practical experience with either embedded development nor assembler)

When I run this on the Hifive1 Rev B, nothing happens. Connecting with gdb through J-Link shows me that the code is hanging in the while loop (as expected). When I set a break point in the irq_handler: code it never gets hit though.

In order to debug this I want to print the CSR register values for MIE and MSTATUS etc, but gdb only prints regular registers, not the CSRs it seems:

main () at first.c:14
14                      while (1) {
(gdb) inf reg
ra             0x20010190       0x20010190 <main+18>
sp             0x80000560       0x80000560
gp             0x800008d8       0x800008d8
tp             0x0      0x0
t0             0x200bff8        33603576
t1             0x23b    571
t2             0x1c03   7171
fp             0x80000570       0x80000570
s1             0x2000080        33554560
a0             0xc8     200
a1             0x20010ed0       536940240
a2             0x20010ed4       536940244
a3             0x1      1
a4             0x10028  65576
a5             0x0      0
a6             0x1f     31
a7             0x0      0
s2             0x2000080        33554560
s3             0x0      0
s4             0x0      0
s5             0x0      0
s6             0x0      0
s7             0x0      0
s8             0x0      0
s9             0x0      0
s10            0x0      0
s11            0x0      0
t3             0x2004000        33570816
t4             0x0      0
t5             0x0      0
t6             0x0      0
pc             0x20010190       0x20010190 <main+18>
(gdb) p $t1
$1 = 571
(gdb) p $mie
$2 = void
(gdb) p $mstatus
$3 = void

Does anyone know how I can get gdb to print the CSRs I am interested in? Other pointers about how to debug my code are also welcome of course :stuck_out_tongue:


Can't access CSRs on HiFive1 rev B from Freedom Studio (via J-Link OB)
#2

I didn’t go through the code properly yet but are you trying to sleep using the CLINT with TIMECMP registers and WFI?

If so you can check out how it’s implemented (fairly close to ASM level) in rust in the e310x-hal crate.

You can see it used in this example

I think you should be able to read rust/llvm’s ASM output for the details too.


(S) #3

Yes, except that I don’t use the WFI instruction but an infinite ‘while’ loop in C code. I should probably change that…

Thanks for the info!

At the moment, my interrupt handler does not even run so I assume that something with my interrupt setup is wrong. I cannot seem to check the state of the IRQ-relevant CSRs though because GDB doesn’t want to show me their state.

As soon as I figure out why my ISR is not being called, I can debug and compare it with rust/llvm’s ASM output…


(S) #4

I just saw that when I upload my binary to the board using J-Link I get the following message:

CSR access via abs. commands: No

This points to me this thread: Can't access CSRs on HiFive1 rev B from Freedom Studio (via J-Link OB)

If I interpret this thread correctly, that could mean that accessing the CSRs through J-Link is currently not possible because their firmware doesn’t support it. That would be a real PITA.


#5

Oddly enough I never tried reading the CSR yet since things worked out for my code at least wrt. the CSR stuff so far.

I’ve went through your code and the main difference I can see is that you try to use the interrupt handler set to MTVEC. The code in the rust crate sleeps using WFI call and does not set MTVEC at all, so the led blinking part is handled in main() (this makes the CPU cool too since it doesn’t busy-loop).

Few things I see (and I’m also very new to ASM here):

  1. in setup_timer_irq: you seem to be setting MTIMECMP to a constant?
  2. in exit: you jump to delay which I don’t see defined anywhere?

Again I’m new to RISC-V assembly and assembly in general so I might’ve missed something obvious but those two pop up for me.


(S) #6

Yes, the idea being that if I set it too low, the timer irq fires before I can set up the mtvec. Not sure that would really be an issue. I now realized that the mtimecmp CSR will not be set to 0 on reset which I will have to keep in mind…

I didn’t expect anyone helping me to debug the code, so I didn’t include it. The code is here (likely buggy):

.section .text
.align 2
.global delay
.include "memory_map.inc"


# a0 is delay-in-milliseconds argument

delay:
	addi sp, sp, -16 # Allocate stack frame
	sw ra, 12(sp)    # save return address to the stack

	li t0, MTIME     # load the timer register
	lw t1, 0(t0)     # load low value of the timer
	lw t2, 4(t0)     # load high value of the timer

	li t3, MTIME_FREQUENCY # get clock freq (approx.)
	mul t3, t3, a0         # multiply milliseconds with freq
	add t4, t1, t3         # target mtime is now in t4
	li t0, MTIMECMP        # load address of MTIMECMP register
	sw t4, 0(t0)           # store target time to MTIMECMP register. This only stores 32 bits so I am not sure if that is correct...
	sw t2, 4(t0)           # set higher 32bits of MTIMECMP

	lw ra, 12(sp)   # load return address
	addi sp, sp, 16 # deallocate stack frame
	ret

The idea here is to set the LEDs as well as the delay in the ISR. All the work (== setting the leds) should thus be done in there and the main loop just waits for IRQs to happen.

Today I have been trying to debug the issue by running the following function in the C main loop and inspecting the CSRs this way:

wait_for_irq:
	csrr t0, mip
	csrr t1, mcause
	csrr t2, mtvec
	csrr t3, mstatus
	csrr t4, mie
	wfi
	ret

That way I could confirm that mtvec is set to the right address (the ISR) but while a timer IRQ seems to be pending according to mip, mstatus does not have the MIE bit set which indicates that we are still in the IRQ handling context (?). Here are the registers when reaching ‘ret’ in wait_for_irq:

ra             0x20010192       0x20010192 <main+20>
sp             0x80000560       0x80000560
gp             0x800008d8       0x800008d8
tp             0x0      0x0
t0             0x880    2176
t1             0x0      0
t2             0x40020448       1073873992
fp             0x80000570       0x80000570
s1             0x2000080        33554560
a0             0xc8     200
a1             0x20010f08       536940296
a2             0x20010f0c       536940300
a3             0x1      1
a4             0x10028  65576
a5             0x0      0
a6             0x1f     31
a7             0x0      0
s2             0x2000080        33554560
s3             0x0      0
s4             0x0      0
s5             0x0      0
s6             0x0      0
s7             0x0      0
s8             0x0      0
s9             0x0      0
s10            0x0      0
s11            0x0      0
t3             0x1800   6144
t4             0x88     136
t5             0x0      0
t6             0x0      0
pc             0x200102a6       0x200102a6 <wait_for_irq+20>

mcause also is zero which seems suspicious, but mie is set correctly. I will have to investigate some more…


#7

You are writing 0x4 to mstatus, which does nothing. The Machine Interrupt Enable bit is 0x8. Or you could write it as 1<<3 if that makes it clearer.


(S) #8

Argh, you are right. The documentation says that bit 3 has to be set but they count from 0 which would make it bit 4 -> I have to write 0x8 not 0x4. I will change that and see if that brings me closer to my goal. Thanks!


#9

I see you are a beginner. You should know that numbering bits from zero is practically universal in the computer industry. :grinning:


(S) #10

hehe, I got it right in the mie register at least…

Now mstatus is set correctly but the ISR still does not seem to be called correctly. I am still investigating and will post again here as soon as I find more issues.


#11
  1. You are doing an ‘lw’ of irq_handler. This does not load the address of irq_handler, but the contents of that location! Use the pseudo-op ‘la’ instead.
  2. Also note that irq_handler has to be aligned on a 64-byte boundary, so precede it with an “.align 6”.
  3. The correct way to return from an interrupt handler is with the ‘mret’ instruction. This re-enables interrupts and then branches to the address in the ‘mepc’ register. (The calling of the ISR is not like a normal routine call with ‘jal’.)

I had to re-read chapters 8, 9, and 10 of the FE310 manual very closely, multiple times, as well as the RISC-V Priviledged Instruction spec, before I got my interrupt code to work.


(S) #12

I already figured that out during my debugging session on the weekend and changed it. That didn’t make the code work yet though.

I wasn’t sure how to do this. Is there another way to ensure this alignment other than specifying this assembler instruction/directive? I assume that has to be set before the irq_handler label. Is that correct?

I am using the mret instruction to return from irq_handler already. That should be the only place where I need it, I think.


#13

The .align pseudo instruction should cause the insertion of sufficient zero bytes to end up on an address with the requested alignment - in this case 2^6 or 64 bytes. That is how the Gnu Assembler does it - I don’t know about embedded in C or anything. All of my RISC-V experience is in assembler.

The code would look like this:

  .align  6
isr:
   lw   etc etc

(KaruppuSwamy Thangaraj) #14

Here is my timer interrupt code. Though it is C, it is indeed assembly code in it.

Overview of code:

  1. Enable global interrupt
  2. Register Interrupt handler for direct mode.
  • In interrupt handler, check cause of interrupt. If it is timer interrupt, switch on blue LED and disable timer interrupt.
  1. Initialize Timer counter and compare registers
  2. Enable Timer interrupt
  3. Wait in while(1)

Complete code: https://github.com/swamytk/risc-v/tree/master/hifive1/02-interrupt_timer


(Liviu Ionescu) #15

BTW, for completeness, your assembly startup code should also set the GP before calling C, and preferably clear the BSS and initialise the DATA section. For C++ it must also call the static constructors.



(KaruppuSwamy Thangaraj) #16

Thanks for the help. In further following demo programs I am going to implement them.


(Liviu Ionescu) #17

Having a correct GP is mandatory for all programs; the compiler might optimise some memory accesses and make them relative to GP, if not set you’ll get undefined behaviour.


(KaruppuSwamy Thangaraj) #18

Thanks for educating. Understood the importance, will do it properly.


(Liviu Ionescu) #19

Another tip: the Eclipse .settings folder may include some non portable content (like absolute paths) which should not be stored in the repository; sharing it with someone using a different platform and even folder structure is not useful.


(KaruppuSwamy Thangaraj) #20

Thanks, let me filter out those config files :slight_smile: