Skip to content

ESP32C3 Library Compilation Uses -march=rv32imac - Is This Expected? #5114

@luoliwoshang

Description

@luoliwoshang

Hello! 👋

First, thank you to the TinyGo team for the excellent work on ESP32C3 support! While studying the code, I noticed something about the compilation flags that I'm not sure if this is intentional design or something that could be improved, and I'd like to ask the team about it.

🔍 What I Observed

When compiling for ESP32C3, I found:

  • User code and CGo: Uses -march=rv32imc (without atomic extension a) ✅
  • C libraries (compiler-rt/picolibc): Uses -march=rv32imac (with atomic extension a) ❓

Since ESP32C3 doesn't support the RISC-V atomic extension, I'm not sure if this difference is intentional.

📝 Full Reproduction Steps

1. Create Test Program

cat > test.go << 'EOF'
package main

func main() {
    println("Hello ESP32C3")
}
EOF

2. Clear Cache (to see full compilation process)

# macOS
rm -rf ~/Library/Caches/tinygo/*riscv32*

# Linux
rm -rf ~/.cache/tinygo/*riscv32*

3. Build with -x flag and view compilation commands

# Using xiao-esp32c3 target
tinygo build -target=xiao-esp32c3 -x -o test.elf test.go 2>&1 | tee build.log

# View -march parameters
grep "march=" build.log | head -10

4. My Actual Output

In my environment (TinyGo dev branch, commit 13bb59c3), I saw this output:

# Compiler-RT compilation command (example)
clang -Werror -Wall -std=c11 -nostdlibinc \
  -c -Oz -gdwarf-4 \
  -ffunction-sections -fdata-sections \
  -Wno-macro-redefined \
  --target=riscv32-unknown-none \
  -fdebug-prefix-map=... \
  -resource-dir=.../lib/clang/19 \
  -mcpu=generic-rv32 \
  -mabi=ilp32 \
  -march=rv32imac \              ← Contains 'a' (atomic)
  -fforce-enable-int128 \
  -o .../absvdi2.c.o \
  .../compiler-rt/lib/builtins/absvdi2.c

In the full log, 493 files all used -march=rv32imac.

🤔 My Questions

Background

What I understand:

  1. ESP32C3 does not support the RISC-V atomic extension (features configured as -a)
  2. targets/esp32c3.json correctly configures cflags: ["-march=rv32imc"]
  3. PR ESP32-C3: implement atomics #2146 implemented excellent software atomic operations (via interrupt disabling)

My Questions

  1. Is this the intended design?

    • Is there a special reason for C libraries to use rv32imac?
    • Is it related to caching or other architectural considerations?
  2. Does this cause actual problems?

    • Do Compiler-RT/Picolibc generate atomic instructions on ESP32C3?
    • Or do software atomic operations already fully cover this scenario?
  3. Why this difference?

    • I traced it to hardcoded values in builder/library.go:169-170:
      case "riscv32":
          args = append(args, "-march=rv32imac", "-fforce-enable-int128")
    • While user code uses config.CFlags() which reads from target JSON

📊 Detailed Analysis (for reference)

Code Location

Hardcoded location (builder/library.go:169-170):

switch compileopts.CanonicalArchName(target) {
case "riscv32":
    args = append(args, "-march=rv32imac", "-fforce-enable-int128")
    // This hardcode applies to all riscv32 targets
}

Target JSON Configuration (targets/esp32c3.json):

{
    "inherits": ["riscv32"],
    "features": "+32bit,+c,+m,+zmmul,-a,...",  // Explicitly disables atomic
    "cflags": ["-march=rv32imc"]               // Correct parameter
}

Historical Context (my speculation)

I looked at the git history:

  • This hardcode comes from commit ad022ef2 on 2019-11-30
  • At that time, there was only HiFive1 (SiFive FE310), which supports the atomic extension
  • So hardcoding rv32imac was correct for all targets at that time

But after ESP32C3 was added in 2021 (without atomic support), this hardcode may no longer be appropriate.

Why It Might Still Work (my understanding)

  1. TinyGo implements complete software atomic operations
  2. Compiler-RT/Picolibc probably don't trigger code paths that need hardware atomic instructions
  3. ESP32C3 typically runs single-core, so atomic operation needs are minimal

❓ Questions for the Team

Core Question

Is this difference in compilation parameters intentional design, or something that could be improved?

Specifically, I'd Like to Understand

  1. Design Intent

    • What's the consideration for using hardcoded -march for library compilation?
    • Is it related to the caching mechanism (LibraryPath)?
  2. Actual Impact

    • Do Compiler-RT/Picolibc generate hardware atomic instructions in the current implementation?
    • Could this parameter mismatch cause problems in practice?
  3. Does It Need Fixing

    • If this is indeed an issue, I can try to submit a PR
    • I've thought of a few possible approaches:
      • Option A: Check features to decide whether to include a
      • Option B: Let library compilation also read target JSON's cflags
      • Option C: Add special case handling for ESP32C3
  4. Other Targets

    • Will there be more RISC-V chips without atomic support in the future?
    • Does this pattern apply to them as well?

🧪 My Test Environment

TinyGo version: dev (commit 13bb59c3)
Target: xiao-esp32c3
LLVM: 19
OS: macOS

Recommended Reproduction Commands

# 1. Create test file
cat > test.go << 'EOF'
package main

func main() {
    println("Hello ESP32C3")
}
EOF

# 2. Clear cache
rm -rf ~/Library/Caches/tinygo/*riscv32*  # macOS
# or
rm -rf ~/.cache/tinygo/*riscv32*           # Linux

# 3. Build and save log
tinygo build -target=xiao-esp32c3 -x -o test.elf test.go 2>&1 | tee build.log

# 4. View compiler-rt's -march parameters
grep "compiler-rt.*march=" build.log | head -5

# 5. Count usage of rv32imac
grep -c "march=rv32imac" build.log

📎 Related References

  • PR ESP32-C3: implement atomics #2146: "ESP32-C3: implement atomics"
  • Commit ad022ef2: Where -march=rv32imac was originally introduced
  • Commit cb147b94: Added ESP32C3 support
  • builder/library.go:169-170: Current hardcoded location

💭 My Thoughts

I'm not sure if this is really a problem because:

  • ✅ ESP32C3 currently works fine
  • ✅ Software atomic operations are already implemented
  • ❓ But from an architectural consistency perspective, libraries and user code using different instruction sets feels a bit odd

So I'd like to hear the team's perspective first! If this does need improvement, I'd be happy to try contributing code. If this is intentional design, I'd also like to understand the reasoning so I can learn more 😊


Thank you for reading! Looking forward to the team's guidance! 🙏

If you need more information or testing, please let me know!

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions