龙芯嵌入式:菜鸡大学生折腾Loongarch32嵌入式记

工欲善其事,必先利其器。这篇Blog将着重介绍如何从零开始构建一个loongarch32-unknown-elfsf目标的工具链,并利用其构建一个龙芯1C102单片机的点灯程序。

💡 下文若未特殊指定,默认 Host 主机架构为x86_64-linux-gnu

在目前(2026年1月2日),GCC 官方仍然没有合并 Loongarch32 平台目标的分支,因此,我们只能找到 GCC 16.0 的源代码,并 patch 上 Loongarch32 平台的补丁。对于 binutils 和 libc,我们则可以很轻松地找到官方或第三方支持 Loongarch32 平台的分支,对于 picolibc 来说,官方甚至就已经支持了该平台。

笔者找到的 gcc 源代码主要来自三个途径:第一是龙芯官方发布的源代码包,第二是 gcc 官方的不稳定分支,第三是龙芯开源社区提供的预编译的工具链以及 patch 过的 gcc 源代码。

龙芯官方工具链存在的“玄学”问题

我们首先就 LoongIDE 上的工具链进行研究。根据工具链的文件名来看,该工具链的编译目标为 loongarch32-newlib-elf ,甚至不是一个标准的三元组;且对于任何一个可开发的目标(龙芯 1C102, 1C103 等 MCU)来说,该类 MCU 皆是软浮点,并不具备 FPU 浮点寄存器,而 loongarch32-newlib-elf 目标却隐式指定该目标为双精度浮点,这可能并不是我们所想要的。

为了验证该工具链的可用性以及获知编译该工具链时指定的编译选项,笔者开了一个 Windows 7 虚拟机。但经过笔者的测试,在龙芯官方演示该 IDE 所使用的 Windows 7 旗舰版环境之下,loongarch32-newlib-elf-gcc.exe 几乎无法运行,或者该程序编译时本身存在问题,使得运行时根本不会产生输出,且后期笔者进行了一些深度测试,可以确定的是该工具链根本不具备编译能力。

后来,笔者又通过龙芯开发者社区获取到了某一较为流行的工具链。然而,根据该工具链的文件名与编译输出,我们却发现:该工具链的编译目标似乎并非是裸机,而是loongarch32-linux-gnuf64 。因此,根据社区上的信息以及笔者的测试,我们可以得到下面三个结论:

  1. 龙芯单片机所需编译器的三元组并不唯一,且可能的匹配情况非常多;
  2. 龙芯单片机本身的链接过程可能并不会涉及到 libc 的启动代码与 _start 段;
  3. 在刷写程序时,是否为软浮点或硬浮点对程序本身并不会产生影响。

因此,在指定编译工具链时,我们或许并不用考虑 ABI 浮点的情况,但为了程序未来可能会需要的稳定性,我们应当编译一个裸机的、软浮点的 ELF 工具链,对应的三元组应当为 loongarch32-unknown-elfsf

💡 loongarch32-unknown-elf 仍然为双精度浮点,为了采用软浮点,我们应当采用loongarch32-unknown-elfsf 。这不是一个标准三元组,但在 gcc 16.0 版本中,官方的 configure 脚本已经弃用了 ABI 的显式指定,因此,我们必须通过该三元组指定软浮点 ABI。

且由于我们采取了交叉编译的方式,我们必须将编译分为四个阶段( Stages ):

  • Stage 0. 编译 binutils,得到汇编器、链接器与其他重要工具;
  • Stage 1. 编译一个初步可用的 GCC,用于编译libc;
  • Stage 2. 使用 Stage1 获得的 GCC,编译 picolibc;
  • Stage 3. 使用 Stage2 获得的 libc,重新编译 gcc,并链接 libc。

binutils 的编译( Stage 0 )

经过笔者的测试,官方分支的 binutils 的 configure 脚本根本无法识别 loongarch32 架构。我们使用社区上已经打好了 patch 的一个 binutils-gdb 分支:

1
2
3
git clone https://github.com/cloudspurs/binutils-gdb
mkdir binutils-build
cd binutils-build

运行 configure 脚本,配置 binutils 的编译选项:

1
2
3
../binutils-gdb/configure --target=loongarch32-unknown-elfsf \
--prefix=/opt/loongarch32-unknown-elfsf \
--disable-multilib

这里的 —disable-multilib 是为了禁用多架构选项。

若脚本运行无误,我们直接开始编译:

1
2
make -j32
sudo make install

然后,我们在 /opt/loongarch32-unknown-elfsf/bin 中就可以找到 binutils 以及 gdb 的可执行文件了。同时,为了方便接下来的操作,我们还需要将该路径添加进 PATH 中。

GCC (仅 C 编译器)的编译( Stage 1 )

这里,我们使用第三方打上了 Loongarch32 patch 的 GCC 分支:

1
2
3
git clone https://github.com/cloudspurs/gcc
mkdir gcc-bootstrap
cd gcc-bootstrap

由于此时我们根本没有 libc,我们无法得到 C++ 的标准库 libstdc++ ,因此在本阶段内我们只能编译一个最基本的 C 语言编译器,且该编译器几乎完全不可以用于链接,只会用到预处理、编译、汇编这三个步骤。因此,我们不需要保证生成的文件是可执行文件,只需要保证生成的文件对应的汇编指令是正常编译而来的,且静态库之间可以相互链接即可。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
../gcc/configure \                                                           
--target=loongarch32-unknown-elfsf \
--prefix=/opt/loongarch32-unknown-elfsf \
--enable-languages=c \
--without-headers \
--disable-shared \
--disable-threads \
--disable-libatomic \
--disable-libgomp \
--disable-libquadmath \
--disable-libssp \
--disable-libstdcxx \
--disable-multilib

我们这里启用了 —without-headers ,是为了防止编译器将我们 Host 平台的头文件当成了目标平台的头文件,从而导致我们不希望出现的链接错误。且我们禁用了大量 GCC 编译时可能会顺带着一起编译或链接的库,仍然是因为在 Stage 1,我们并不具备编译这些库的能力,因为我们甚至连 libc 都没有。在配置 GCC 编译选项时,我们仍然启用了 —disable-multilib ,禁用了其他架构的编译选项,从而保证我们编译出的文件更加具有专一性,在 Stage 2 编译 libc 时,把因为架构问题出错的可能性进一步降低。

若脚本运行无误,我们直接开始编译:

1
2
make -j32
sudo make install

由于我们禁用了太多编译时可能会顺带着编译进去的库或其他支持,整个编译过程只会持续大约十分钟左右。接下来,我们便可以在 /opt/loongarch32-unknown-elfsf/bin 中找到 GCC。

Picolibc 的编译( Stage 2 )

经过了 Stage 1,我们获得了一个具有初步功能的 GCC,同时也具有了初步编译的能力。为了获得一个具有完整功能的 GCC,我们就必须编译一个 libc,使得 GCC 能够正常完成链接。由于我们的开发对象的 FLASH 体积较小,且开销有限,我们选择比 newlib 更为轻量的 Picolibc。且 Picolibc 官方已经将 Loongarch32 平台的支持合并进入主线,而 newlib 则仍然没有合并。因此,在这里,我们直接使用 Picolibc 的官方分发即可。

1
2
3
git clone https://github.com/picolibc/picolibc
mkdir picolibc-build
cd picolibc-build

为了编译 Picolibc,且由于 Picolibc 使用了 meson 构建系统,我们还需要配置交叉编译选项。

1
vim ../picolibc/cross.tmpl

将其修改:

1
2
3
4
5
6
7
8
9
10
[binaries]
c = 'loongarch32-unknown-elfsf-gcc'
ar = 'loongarch32-unknown-elfsf-ar'
strip = 'loongarch32-unknown-elfsf-strip'

[host_machine]
system = 'none'
cpu_family = 'loongarch32'
cpu = 'loongarch32'
endian = 'little'

修改后开始配置编译选项并使用 ninja 进行编译:

1
2
3
4
5
meson setup ../picolibc \
--cross-file ../picolibc/cross.tmpl \
--prefix=/opt/loongarch32-unknown-elfsf/loongarch32-unknown-elfsf \
-Dmultilib=false
ninja

但是最终出现了报错:

1
2
3
4
5
6
7
8
FAILED: [code=1] picocrt/crt0-semihost.o.p/machine_loongarch_crt0.c.o 
loongarch32-unknown-elfsf-gcc -Ipicocrt/crt0-semihost.o.p -Ipicocrt -I../picolibc/picocrt -Inewlib/libc/machine/loongarch -I../picolibc/newlib/libc/machine/loongarch -Inewlib/libc/stdio -I../picolibc/newlib/libc/stdio -Inewlib/libc/locale -I../picolibc/newlib/libc/locale -I. -I../picolibc -Inewlib/libc/include -I../picolibc/newlib/libc/include -Isemihost/common -I../picolibc/semihost/common -fdiagnostics-color=always -D_FILE_OFFSET_BITS=64 -Wall -Winvalid-pch -Wextra -std=c18 -Os -g -ffunction-sections -D_LIBC -fno-builtin -ffreestanding -DMACHINE_qemu -DCRT0_EXIT -DCRT0_SEMIHOST -MD -MQ picocrt/crt0-semihost.o.p/machine_loongarch_crt0.c.o -MF picocrt/crt0-semihost.o.p/machine_loongarch_crt0.c.o.d -o picocrt/crt0-semihost.o.p/machine_loongarch_crt0.c.o -c ../picolibc/picocrt/machine/loongarch/crt0.c
/tmp/cckjcHqE.s: Assembler messages:
/tmp/cckjcHqE.s:282: 错误:CFI instruction used without previous .cfi_startproc
/tmp/cckjcHqE.s:384: 错误:CFI instruction used without previous .cfi_startproc
/tmp/cckjcHqE.s:387: 错误:CFI instruction used without previous .cfi_startproc
[1055/1058] Linking static target newlib/libc.a
ninja: build stopped: subcommand failed.

笔者推测,这极有可能是因为 Loongarch32 的各个编译系统仍然处于上游测试期,Picolibc 对 Loongarch32 平台的汇编支持并不是特别完善。但是我们也注意到,报错原因在于 CFI 表达式的使用方式出现了问题,而 CFI 表达式本身则是用于 Picolibc 的调试,且我们在编译的时候很可能并不会用到 Picolibc 的启动指令,而是我们自己写的 _start 段。如果去掉,影响可能不会特别大。因此,我们尝试直接修改 ../picolibc/picocrt/machine/loongarch/crt0.c ,并去掉其中的所有 CFI 段。

1
vim ../picolibc/picocrt/machine/loongarch/crt0.c
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
/*
* SPDX-License-Identifier: BSD-3-Clause
*
* Copyright (c) 2024 Jiaxun Yang <<jiaxun.yang@flygoat.com>
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions
* are met:
*
* 1. Redistributions of source code must retain the above copyright
* notice, this list of conditions and the following disclaimer.
*
* 2. Redistributions in binary form must reproduce the above
* copyright notice, this list of conditions and the following
* disclaimer in the documentation and/or other materials provided
* with the distribution.
*
* 3. Neither the name of the copyright holder nor the names of its
* contributors may be used to endorse or promote products derived
* from this software without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
* "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
* LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
* FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
* COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT,
* INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
* (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
* SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
* HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
* STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED
* OF THE POSSIBILITY OF SUCH DAMAGE.
*/

#include <stddef.h>
#include "../../crt0.h"

static void __used __section(".init") _cstart(void)
{
__start();
}

#ifdef CRT0_SEMIHOST
#include <unistd.h>
#include <stdio.h>

#define NUM_REG 32

#if __loongarch_grlen == 32
#define FMT "%08lx"
#define SD "st.d"
#define ADDI "addi.d"
#else
#define FMT "%016lx"
#define SD "st.d"
#define ADDI "addi.w"
#endif

struct fault {
unsigned long r[NUM_REG];
unsigned long csr_era;
unsigned long csr_badvaddr;
unsigned long csr_crmd;
unsigned long csr_prmd;
unsigned long csr_euen;
unsigned long csr_ecfg;
unsigned long csr_estat;
};

static const char * const names[NUM_REG] = {
"zero", "ra", "tp", "sp", "a0", "a1", "a2", "a3", "a4", "a5", "a6",
"a7", "t0", "t1", "t2", "t3", "t4", "t5", "t6", "t7", "t8", "r21",
"fp", "s0", "s1", "s2", "s3", "s4", "s5", "s6", "s7", "s8",
};

static void __used __section(".init") _ctrap(struct fault *fault)
{
int r;
printf("LoongArch fault\n");
for (r = 0; r < NUM_REG; r++)
printf("\tx%d %-5.5s%s 0x" FMT "\n", r, names[r], r < 10 ? " " : "", fault->r[r]);
printf("\tera: 0x%lx\n", fault->csr_era);
printf("\tbadvaddr: 0x%lx\n", fault->csr_badvaddr);
printf("\tcrmd: 0x%lx\n", fault->csr_crmd);
printf("\tprmd: 0x%lx\n", fault->csr_prmd);
printf("\teuen: 0x%lx\n", fault->csr_euen);
printf("\tecfg: 0x%lx\n", fault->csr_ecfg);
printf("\testat: 0x%lx\n", fault->csr_estat);
_exit(1);
}

#define _PASTE(r) #r
#define PASTE(r) _PASTE(r)

void __section(".init") __used __attribute((aligned(0x40))) _trap(void)
{
/* Build a known-working C environment */
__asm__("csrwr $sp, 0x30\n" /* Save sp to ksave0 */
"la.abs $sp, __heap_end\n");

/* Make space for saved registers */
__asm__(ADDI " $sp, $sp, %0\n"
::"i"(-sizeof(struct fault)));

/* Save registers on stack */
#define SAVE_REG(num) \
__asm__(SD " $r%0, $sp, %1" ::"i"(num), \
"i"((num) * sizeof(unsigned long) + offsetof(struct fault, r)))

SAVE_REG(0);
SAVE_REG(1);
SAVE_REG(2);
SAVE_REG(3);
SAVE_REG(4);
SAVE_REG(5);
SAVE_REG(6);
SAVE_REG(7);
SAVE_REG(8);
SAVE_REG(9);
SAVE_REG(10);
SAVE_REG(11);
SAVE_REG(12);
SAVE_REG(13);
SAVE_REG(14);
SAVE_REG(15);
SAVE_REG(16);
SAVE_REG(17);
SAVE_REG(18);
SAVE_REG(19);
SAVE_REG(20);
SAVE_REG(21);
SAVE_REG(22);
SAVE_REG(23);
SAVE_REG(24);
SAVE_REG(25);
SAVE_REG(26);
SAVE_REG(27);
SAVE_REG(28);
SAVE_REG(29);
SAVE_REG(30);
SAVE_REG(31);

#define SAVE_CSR(name, reg) \
__asm__("csrrd $t0, " PASTE(reg)); \
__asm__(SD " $t0, $sp, %0" ::"i"(offsetof(struct fault, name)))

/*
* Save the trapping frame's stack pointer that was stashed in ksave0
* and tell the unwinder where we can find the return address (csr_era).
*/
__asm__("csrrd $ra, 0x6\n" SD " $ra, $sp, %0\n"
"csrrd $t0, 0x30\n" SD " $t0, $sp, %0\n"
::"i"(offsetof(struct fault, csr_era)),
"i"(offsetof(struct fault, r[3])));
SAVE_CSR(csr_badvaddr, 0x7);
SAVE_CSR(csr_crmd, 0x0);
SAVE_CSR(csr_prmd, 0x1);
SAVE_CSR(csr_euen, 0x2);
SAVE_CSR(csr_ecfg, 0x4);
SAVE_CSR(csr_estat, 0x5);

/*
* Pass pointer to saved registers in first parameter register
*/
__asm__("move $a0, $sp");

/* Enable FPU (just in case) */
#ifndef __loongarch_soft_float
__asm__("li.w $t0, 0x1\n" /* FPEN */
"csrxchg $t0, $t0, 0x2\n"); /* EUEN */
#endif
__asm__("la.pcrel $t0, _ctrap\n"
"jirl $ra, $t0, 0\n");
}
#endif
void __section(".text.init.enter") __used _start(void)
{
__asm__("la.abs $sp, __stack\n");

#ifndef __loongarch_soft_float
__asm__("li.w $t0, 0x1\n" /* FPEN */
"csrxchg $t0, $t0, 0x2\n"); /* EUEN */
#endif

#ifdef CRT0_SEMIHOST
__asm__("la.abs $t0, _trap");
__asm__("csrwr $t0, 0xc");
#endif
__asm__("la.pcrel $t0, _cstart\n"
"jirl $ra, $t0, 0\n");
}

再次运行:

1
2
ninja
sudo ninja install

现在没有报错了,且我们的 libc 已经正确地安装到了目标位置上。

完整 GCC 的编译( Stage 3 )

现在,有了 libc,我们已经具备了编译 C++ 标准库乃至其他运行时库的能力。

1
2
mkdir gcc-final
cd gcc-final

配置编译选项:

1
2
3
4
5
6
7
8
9
../gcc/configure \
--target=loongarch32-unknown-elfsf \
--prefix=/opt/loongarch32-unknown-elfsf \
--enable-languages=c,c++ \
--with-newlib \
--with-sysroot=/opt/loongarch32-unknown-elfsf/loongarch32-unknown-elfsf \
--disable-shared \
--disable-threads \
--disable-multilib

在这里,我们仍然禁用了线程,因为它需要借助 glibc 以及 Linux 用户态,然而这在我们的开发对象上是几乎完全不可能实现的。且我们将 Picolibc 的安装位置设置为了 sysroot,这是为了告诉编译器需要在这个路径之下寻找链接库。我们直接开始编译并安装:

1
2
make -j32
sudo make install

在经历了大约二十分钟左右的编译之后,完整的 GCC 就被安装在了交叉编译工具链的路径上,至此,我们的交叉编译环境就算是搭建完成了。

总结

通过交叉编译工具链的构建过程中发现的种种社区现象,我们可以得出下列结论:

  1. 龙芯嵌入式的开发在 x86_64 平台上的支持仍然较少,大多数软件仍然在 Loongarch64 平台上;
  2. 龙芯嵌入式在软件方面并不存在一个官方统一的工具链或硬件抽象层(HAL),配置起来相当麻烦,但也给这个架构带来了相当大的可玩性;
  3. 龙芯的软件平台虽然在适配 x86_64 架构这方面存在着强大的社区支持,但在 x86_64 架构上的反适配却支持明显不够。

龙芯架构的 MCU 虽然有着可观的参数,但它所具备的软件支持却远远不足。笔者也因为该原因,在该平台上开发时必须手写一份 HAL,存在着极大的不便,但也使得笔者的能力得到了进一步提升。至于龙芯架构的开发方式,笔者决定于下一个 Blog 进行深入讲解。