Posted on Leave a comment

Notes on RISC-V Assembly Language Programming – Part 5

28 January 2025

Today I was able to reach wch-ic.com. wch.cn and mounriver.com. No telling if it had anything to do with the approaching Lunar New Year as celebrated in China and many other parts of the world or not.

I have taken the opportunity to upgrade to the latest CH32V003 RM V1.7, dated 2024-03-11.

So now I’m back to wondering why the GNU RISC-V assembler treats pseudo-instructions differently based on the destination register involved. Specifically, why does it it produce this:

la sp, _eusrstack # initialize the stack pointer
0: 20005117 auipc sp,0x20005
4: 00010113 mv sp,sp
la sp, 0x20005000 # *** debug *** for comparison
8: 20005137 lui sp,0x20005
lui sp, %hi(_eusrstack) # initialize the stack pointer
c: 20005137 lui sp,0x20005
la t0, _eusrstack # *** debug *** for comparison
10: 20005297 auipc t0,0x20005
14: ff028293 addi t0,t0,-16 # 20005000 <_eusrstack>
la t0, 0x20005000 # *** debug *** for comparison
18: 200052b7 lui t0,0x20005

So one thing I see is that when I reference the linker variable _eusrstack, it always uses the auipc+mv combination, whereas when I use the immediate value 0x20005000, it uses the single instruction lui (load upper [20 bits] immediate).

I don’t know if it has anything to do with anything, but it now feels like the time has come to deploy my own linker script. Not to worry, it’s already written and around here… somewhere.

I found four of them, for the 003, 203, 208 (just a copy of the 003) and 307. I’ll use the 307 as a basis and make a few changes, as appropriate.

Now here’s an issue I had not previously considered. The 003 only offers one memory configuration: 16KB flash ‘rom’ and 2KB SRAM. So a single linker script applies to the entire family.

It turns out that this is also the case for the CH32X035 line, all the way from the smallest (F) to the largest (R) packages offered: 62KB flash ‘rom’ and 20KB SRAM. So for today I don’t need to figure out how to accurately specify memory capacities in linker scripts for the larger parts, such as the 203, 208 and 307. [TODO]

OK, today’s tangent is to find out why or how the CH32X035F8U6 device in QFN20 package offers 19 GPIO. I have a stack of these as realized as the WeAct core boards.

Well, it’s true! They are using the bottom pad as the single, only solitary ground reference connection. This is also the case for the CH32X035G8U6 device that I am currently examining.

I’m not sure how I feel about this. Mostly, I’m glad this tangent was so quickly ended.

Note: Just make really sure to properly ground these parts when laying out a PCB. This effectively rules out their use on single-sided PCBs.

Small changes to the linker script:

Update header and footer with current date and new filename
Change ENTRY(start) to ENTRY(_start)
Adjust flash and RAM sizes as needed

Now it’s time to change the project settings to use the newly-available linker file, which I have added to the CH32X035/lib folder.

The MRS2 build options seem to expect the linker script to reside within the project folder, which is how it was and was working just fine. The two options for the linker script in the ‘Open the file from:’ option were Project or Local Folder. I’ll try a relative reference in the Project option first: ../inc/CH32X035.ld

Well, it didn’t like it: “ld: read in flex scanner failed”

But nowhere do I see a reference to the linker script in the command line invocation. Now the sharp-eyed amongst you will have seen my (now) obvious error: I referenced the ‘inc’ folder, not the ‘lib’ folder.

Once again, it needed an addition level of redirection, because the source code is within a project sub folder, so the proper entry is “../../lib/CH32X035.ld” in the linker settings.

Now it is rightfully complaining about an “undefined reference to `_eusrstack'”. So I will make the proper adjustment, substituting ‘end_of_RAM’ for ‘_eusrstack’ and see what happens.

Hey, it works. Waddaya know? Also, the mysterious ‘2048’ bytes of BSS are gone from the size totals summary, as they were a nod to the 2K allocated to the stack in the original linker script.

To get an assembler listing that includes the source code but does not include a lengthy list of all the defined symbols (there are a LOT of them from the include file), use these options:

NOT the 'Generate assembler listing..." from Assembler/Misc
NOT the 'Generate assembler listing..." from Compiler/Misc
Only these three (3) from  GNU RISC-V Cross Create Flash Listing/General
    Create extended listing
    Display source
    Disassemble

A very tidy listing, indeed.

Note: Leave some sort of debugging information available, i.e., don’t use Debugging: None option, or guess what? It won’t let you debug! I mean, it does, but doesn’t show you where you are at the moment as you step through the code.

So that was a nice step in the right direction. I can now delete the ‘Ld/‘ project sub-folder and the provided linker script, and the project still builds, flashes, runs and debugs as expected.

So the behavior of the assembler remains the same: the references to the ‘end_of_RAM’ value from the linker script are treated as addresses and handled one way while providing explicit immediate values are handled another way.

Adding the PROVIDE() function to the end_of_RAM variable makes no difference:

PROVIDE(end_of_RAM = ORIGIN(RAM) + LENGTH(RAM));

It worked without it, so I’m leaving it out for now.

So here is the linker script as it stands at the moment:

/* filename: CH32X035.ld
   linker file for CH32X035
   28 January 2025 - Dale Wheat */

ENTRY(_start)

end_of_RAM = ORIGIN(RAM) + LENGTH(RAM);

MEMORY {
    FLASH   (rx)    : ORIGIN = 0x00000000, LENGTH = 62K
    RAM     (rwx)   : ORIGIN = 0x20000000, LENGTH = 20K
}

SECTIONS {
    .entry  : { *.entry . = ALIGN(4); } >FLASH
    .system_vector  : { *.system_vector } >FLASH
    .device_vector  : { *.device_vector } >FLASH
    .start  : { *.start } >FLASH
    .main   : { *.main } >FLASH
    .text   : { *(.text*) *(.rodata*) . = ALIGN(4); } >FLASH
    .data   : { start_of_data = .; *(.data*) *(.sdata*) . = ALIGN(4); end_of_data = .; sidata = LOADADDR(.data); } >RAM AT>FLASH
    .bss    : { start_of_bss = .; *(.bss*) *(.sbss*) . = ALIGN(4); end_of_bss = .; } >RAM
    .noinit (NOLOAD) : { *(.noinit*) } >RAM
}

/* CH32X035.ld [end-of-file] */

There are some reasons for the way things are in this script, especially in the SECTIONS section. First, my bizarre way of thinking created a solution for sometimes having and sometimes not having a vector table at the very beginning of memory. This involves having a dummy slot reserved at the very first location, i.e., address 0x00000000, that is either a jump over the table, if it exists, or nothing if no table is present. In reality, at the moment, the first two 32 bit slots are ‘reserved’ and could be used for some sort of very terse initialization code, for the vector tabled applications of the future.

That all alludes to the ‘.entry’ section. Next would normally come the .system_vector and .device_vector segments, should they be required.

Now the really sneaky parts come into view. The .start segment contains the _start ‘function’ which is, in a C language program, a ‘naked’ function that does some initialization and then ‘falls through’ to the .main segment. This begins the start of the main() function of a normal C language program. Otherwise, the next segment defined is the .text segment where I put variously sub-segmented components, such as (from the converted include file):

.text 0 # needs to be at address 0

    j start

.text 10 # system vectors
.text 20 # device vectors

This mechanism helps make sure all the pieces fall in the right order when linked.

I promise the ‘cleverness’ ends there. I’m not a fan of ‘cleverness’ in computer code.

Back to unravelling the mystery. I already have a solution but I’m not liking it enough to let it go.

I need to convert the ‘address’ of the end_of_RAM ‘symbol’ into just a scalar value.

My feeble attempts to locate an answer on the interwebs has left me quite dissatisfied. Now I am thinking that there has to be another method to convey this information that we already know and does not change into a stylistically acceptable mechanism.

Does the SVD contain the extents of the memory? No, it does not.

So I have decided to hard-code the value in the device include file like this:

# device memory extents

END_OF_RAM = 0x20005000

I could eventually add other memory values such as the beginning and end of flash, etc.

This produces the desired effect in the assembled output listing:

la sp, END_OF_RAM # initialize the stack pointer
0: 20005137 lui sp,0x20005
la sp, 0x20005000 # *** debug *** for comparison
4: 20005137 lui sp,0x20005
lui sp, %hi(END_OF_RAM) # initialize the stack pointer
8: 20005137 lui sp,0x20005
la t0, END_OF_RAM # *** debug *** for comparison
c: 200052b7 lui t0,0x20005
la t0, 0x20005000 # *** debug *** for comparison
10: 200052b7 lui t0,0x20005

Note: This won’t work on the 003 as it only has 2K of RAM so it ‘ends’ at 0x20000800, which cannot be loaded as a 20 bit immediate value. This won’t be a problem for some of the new -00x parts that have been announced, as they come with 4KB of SRAM.

Leave a Reply