Skip to content

Commit

Permalink
Add compiler support for AVR architecture (Arduino) (#14393)
Browse files Browse the repository at this point in the history
Co-authored-by: Sijawusz Pur Rahnama <[email protected]>
  • Loading branch information
ysbaddaden and Sija authored May 2, 2024
1 parent 89f2e43 commit 7ecb968
Show file tree
Hide file tree
Showing 11 changed files with 382 additions and 35 deletions.
198 changes: 198 additions & 0 deletions spec/std/llvm/avr_spec.cr
Original file line number Diff line number Diff line change
@@ -0,0 +1,198 @@
require "spec"

{% if flag?(:interpreted) && !flag?(:win32) %}
# TODO: figure out how to link against libstdc++ in interpreted code (#14398)
pending LLVM::ABI::AVR
{% skip_file %}
{% end %}

require "llvm"

{% if LibLLVM::BUILT_TARGETS.includes?(:avr) %}
LLVM.init_avr
{% end %}

private def abi
triple = "avr-unknown-unknown-atmega328p"
target = LLVM::Target.from_triple(triple)
machine = target.create_target_machine(triple)
machine.enable_global_isel = false
LLVM::ABI::AVR.new(machine)
end

private def test(msg, &block : LLVM::ABI, LLVM::Context ->)
it msg do
abi = abi()
ctx = LLVM::Context.new
block.call(abi, ctx)
end
end

class LLVM::ABI
describe AVR do
{% if LibLLVM::BUILT_TARGETS.includes?(:avr) %}
describe "align" do
test "for integer" do |abi, ctx|
abi.align(ctx.int1).should be_a(::Int32)
abi.align(ctx.int1).should eq(1)
abi.align(ctx.int8).should eq(1)
abi.align(ctx.int16).should eq(1)
abi.align(ctx.int32).should eq(1)
abi.align(ctx.int64).should eq(1)
end

test "for pointer" do |abi, ctx|
abi.align(ctx.int8.pointer).should eq(1)
end

test "for float" do |abi, ctx|
abi.align(ctx.float).should eq(1)
end

test "for double" do |abi, ctx|
abi.align(ctx.double).should eq(1)
end

test "for struct" do |abi, ctx|
abi.align(ctx.struct([ctx.int32, ctx.int64])).should eq(1)
abi.align(ctx.struct([ctx.int8, ctx.int16])).should eq(1)
end

test "for packed struct" do |abi, ctx|
abi.align(ctx.struct([ctx.int32, ctx.int64], packed: true)).should eq(1)
end

test "for array" do |abi, ctx|
abi.align(ctx.int16.array(10)).should eq(1)
end
end

describe "size" do
test "for integer" do |abi, ctx|
abi.size(ctx.int1).should be_a(::Int32)
abi.size(ctx.int1).should eq(1)
abi.size(ctx.int8).should eq(1)
abi.size(ctx.int16).should eq(2)
abi.size(ctx.int32).should eq(4)
abi.size(ctx.int64).should eq(8)
end

test "for pointer" do |abi, ctx|
abi.size(ctx.int8.pointer).should eq(2)
end

test "for float" do |abi, ctx|
abi.size(ctx.float).should eq(4)
end

test "for double" do |abi, ctx|
abi.size(ctx.double).should eq(8)
end

test "for struct" do |abi, ctx|
abi.size(ctx.struct([ctx.int32, ctx.int64])).should eq(12)
abi.size(ctx.struct([ctx.int16, ctx.int8])).should eq(3)
abi.size(ctx.struct([ctx.int32, ctx.int8, ctx.int8])).should eq(6)
end

test "for packed struct" do |abi, ctx|
abi.size(ctx.struct([ctx.int32, ctx.int8], packed: true)).should eq(5)
end

test "for array" do |abi, ctx|
abi.size(ctx.int16.array(10)).should eq(20)
end
end

describe "abi_info" do
{% for bits in [1, 8, 16, 32, 64] %}
test "int{{bits}}" do |abi, ctx|
arg_type = ArgType.direct(ctx.int{{bits}})
info = abi.abi_info([ctx.int{{bits}}], ctx.int{{bits}}, true, ctx)
info.arg_types.size.should eq(1)
info.arg_types[0].should eq(arg_type)
info.arg_types[0].kind.should eq(LLVM::ABI::ArgKind::Direct)
info.return_type.should eq(arg_type)
info.return_type.kind.should eq(LLVM::ABI::ArgKind::Direct)
end
{% end %}

test "float" do |abi, ctx|
arg_type = ArgType.direct(ctx.float)
info = abi.abi_info([ctx.float], ctx.float, true, ctx)
info.arg_types.size.should eq(1)
info.arg_types[0].should eq(arg_type)
info.arg_types[0].kind.should eq(LLVM::ABI::ArgKind::Direct)
info.return_type.should eq(arg_type)
info.return_type.kind.should eq(LLVM::ABI::ArgKind::Direct)
end

test "double" do |abi, ctx|
arg_type = ArgType.direct(ctx.double)
info = abi.abi_info([ctx.double], ctx.double, true, ctx)
info.arg_types.size.should eq(1)
info.arg_types[0].should eq(arg_type)
info.arg_types[0].kind.should eq(LLVM::ABI::ArgKind::Direct)
info.return_type.should eq(arg_type)
info.return_type.kind.should eq(LLVM::ABI::ArgKind::Direct)
end

test "multiple arguments" do |abi, ctx|
args = 9.times.map { ctx.int16 }.to_a
info = abi.abi_info(args, ctx.int8, false, ctx)
info.arg_types.size.should eq(9)
info.arg_types.each(&.kind.should eq(LLVM::ABI::ArgKind::Direct))
end

test "multiple arguments above registers" do |abi, ctx|
args = 5.times.map { ctx.int32 }.to_a
info = abi.abi_info(args, ctx.int8, false, ctx)
info.arg_types.size.should eq(5)
info.arg_types[0].kind.should eq(LLVM::ABI::ArgKind::Direct)
info.arg_types[1].kind.should eq(LLVM::ABI::ArgKind::Direct)
info.arg_types[2].kind.should eq(LLVM::ABI::ArgKind::Direct)
info.arg_types[3].kind.should eq(LLVM::ABI::ArgKind::Direct)
info.arg_types[4].kind.should eq(LLVM::ABI::ArgKind::Indirect)
end

test "struct args within 18 bytes" do |abi, ctx|
args = [
ctx.int8, # rounded to 2 bytes
ctx.struct([ctx.int32, ctx.int32]), # 8 bytes
ctx.struct([ctx.int32, ctx.int32]), # 8 bytes
]
info = abi.abi_info(args, ctx.void, false, ctx)
info.arg_types.size.should eq(3)
info.arg_types[0].kind.should eq(LLVM::ABI::ArgKind::Direct)
info.arg_types[1].kind.should eq(LLVM::ABI::ArgKind::Direct)
info.arg_types[2].kind.should eq(LLVM::ABI::ArgKind::Direct)
end

test "struct args over 18 bytes" do |abi, ctx|
args = [
ctx.int32, # 4 bytes
ctx.struct([ctx.int32, ctx.int32]), # 8 bytes
ctx.struct([ctx.int32, ctx.int32]), # 8 bytes
]
info = abi.abi_info(args, ctx.void, false, ctx)
info.arg_types.size.should eq(3)
info.arg_types[0].kind.should eq(LLVM::ABI::ArgKind::Direct)
info.arg_types[1].kind.should eq(LLVM::ABI::ArgKind::Direct)
info.arg_types[2].kind.should eq(LLVM::ABI::ArgKind::Indirect)
end

test "returns struct within 8 bytes" do |abi, ctx|
rty = ctx.struct([ctx.int32, ctx.int32])
info = abi.abi_info([] of Type, rty, true, ctx)
info.return_type.kind.should eq(LLVM::ABI::ArgKind::Direct)
end

test "returns struct over 8 bytes" do |abi, ctx|
rty = ctx.struct([ctx.int32, ctx.int32, ctx.int8])
info = abi.abi_info([] of Type, rty, true, ctx)
info.return_type.kind.should eq(LLVM::ABI::ArgKind::Indirect)
end
end
{% end %}
end
end
22 changes: 20 additions & 2 deletions src/compiler/crystal/codegen/target.cr
Original file line number Diff line number Diff line change
Expand Up @@ -51,10 +51,14 @@ class Crystal::Codegen::Target

def pointer_bit_width
case @architecture
when "x86_64", "aarch64"
when "aarch64", "x86_64"
64
else
when "arm", "i386", "wasm32"
32
when "avr"
16
else
raise "BUG: unknown Target#pointer_bit_width for #{@architecture} target architecture"
end
end

Expand All @@ -64,6 +68,8 @@ class Crystal::Codegen::Target
64
when "arm", "i386", "wasm32"
32
when "avr"
16
else
raise "BUG: unknown Target#size_bit_width for #{@architecture} target architecture"
end
Expand Down Expand Up @@ -93,6 +99,7 @@ class Crystal::Codegen::Target
def executable_extension
case
when windows? then ".exe"
when avr? then ".elf"
else ""
end
end
Expand Down Expand Up @@ -181,6 +188,10 @@ class Crystal::Codegen::Target
environment_parts.any? &.in?("gnueabihf", "musleabihf")
end

def avr?
@architecture == "avr"
end

def to_target_machine(cpu = "", features = "", optimization_mode = Compiler::OptimizationMode::O0,
code_model = LLVM::CodeModel::Default) : LLVM::TargetMachine
case @architecture
Expand All @@ -197,6 +208,13 @@ class Crystal::Codegen::Target
if cpu.empty? && !features.includes?("fp") && armhf?
features += "+vfp2"
end
when "avr"
LLVM.init_avr

if cpu.blank?
# the ABI call convention, codegen and the linker need to known the CPU model
raise Target::Error.new("AVR targets must declare a CPU model, for example --mcpu=atmega328p")
end
when "wasm32"
LLVM.init_webassembly
else
Expand Down
5 changes: 4 additions & 1 deletion src/compiler/crystal/compiler.cr
Original file line number Diff line number Diff line change
Expand Up @@ -481,10 +481,13 @@ module Crystal
elsif program.has_flag? "wasm32"
link_flags = @link_flags || ""
{"wasm-ld", %(wasm-ld "${@}" -o #{Process.quote_posix(output_filename)} #{link_flags} -lc #{program.lib_flags}), object_names}
elsif program.has_flag? "avr"
link_flags = @link_flags || ""
link_flags += " --target=avr-unknown-unknown -mmcu=#{@mcpu} -Wl,--gc-sections"
{DEFAULT_LINKER, %(#{DEFAULT_LINKER} "${@}" -o #{Process.quote_posix(output_filename)} #{link_flags} #{program.lib_flags}), object_names}
else
link_flags = @link_flags || ""
link_flags += " -rdynamic"

{DEFAULT_LINKER, %(#{DEFAULT_LINKER} "${@}" -o #{Process.quote_posix(output_filename)} #{link_flags} #{program.lib_flags}), object_names}
end
end
Expand Down
4 changes: 4 additions & 0 deletions src/compiler/crystal/semantic/flags.cr
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,10 @@ class Crystal::Program

flags.add "bsd" if target.bsd?

if target.avr? && (cpu = target_machine.cpu.presence)
flags.add cpu
end

flags
end
end
74 changes: 43 additions & 31 deletions src/intrinsics.cr
Original file line number Diff line number Diff line change
Expand Up @@ -4,45 +4,57 @@ lib LibIntrinsics
{% if flag?(:interpreted) %} @[Primitive(:interpreter_intrinsics_debugtrap)] {% end %}
fun debugtrap = "llvm.debugtrap"

{% if flag?(:bits64) %}
{% if flag?(:avr) %}
{% if compare_versions(Crystal::LLVM_VERSION, "15.0.0") < 0 %}
{% if flag?(:interpreted) %} @[Primitive(:interpreter_intrinsics_memcpy)] {% end %}
fun memcpy = "llvm.memcpy.p0i8.p0i8.i64"(dest : Void*, src : Void*, len : UInt64, is_volatile : Bool)

{% if flag?(:interpreted) %} @[Primitive(:interpreter_intrinsics_memmove)] {% end %}
fun memmove = "llvm.memmove.p0i8.p0i8.i64"(dest : Void*, src : Void*, len : UInt64, is_volatile : Bool)

{% if flag?(:interpreted) %} @[Primitive(:interpreter_intrinsics_memset)] {% end %}
fun memset = "llvm.memset.p0i8.i64"(dest : Void*, val : UInt8, len : UInt64, is_volatile : Bool)
fun memcpy = "llvm.memcpy.p0i8.p0i8.i16"(dest : Void*, src : Void*, len : UInt16, is_volatile : Bool)
fun memmove = "llvm.memmove.p0i8.p0i8.i16"(dest : Void*, src : Void*, len : UInt16, is_volatile : Bool)
fun memset = "llvm.memset.p0i8.i16"(dest : Void*, val : UInt8, len : UInt16, is_volatile : Bool)
{% else %}
{% if flag?(:interpreted) %} @[Primitive(:interpreter_intrinsics_memcpy)] {% end %}
fun memcpy = "llvm.memcpy.p0.p0.i64"(dest : Void*, src : Void*, len : UInt64, is_volatile : Bool)

{% if flag?(:interpreted) %} @[Primitive(:interpreter_intrinsics_memmove)] {% end %}
fun memmove = "llvm.memmove.p0.p0.i64"(dest : Void*, src : Void*, len : UInt64, is_volatile : Bool)

{% if flag?(:interpreted) %} @[Primitive(:interpreter_intrinsics_memset)] {% end %}
fun memset = "llvm.memset.p0.i64"(dest : Void*, val : UInt8, len : UInt64, is_volatile : Bool)
fun memcpy = "llvm.memcpy.p0.p0.i16"(dest : Void*, src : Void*, len : UInt16, is_volatile : Bool)
fun memmove = "llvm.memmove.p0.p0.i16"(dest : Void*, src : Void*, len : UInt16, is_volatile : Bool)
fun memset = "llvm.memset.p0.i16"(dest : Void*, val : UInt8, len : UInt16, is_volatile : Bool)
{% end %}
{% else %}
{% if compare_versions(Crystal::LLVM_VERSION, "15.0.0") < 0 %}
{% if flag?(:interpreted) %} @[Primitive(:interpreter_intrinsics_memcpy)] {% end %}
fun memcpy = "llvm.memcpy.p0i8.p0i8.i32"(dest : Void*, src : Void*, len : UInt32, is_volatile : Bool)
{% if flag?(:bits64) %}
{% if compare_versions(Crystal::LLVM_VERSION, "15.0.0") < 0 %}
{% if flag?(:interpreted) %} @[Primitive(:interpreter_intrinsics_memcpy)] {% end %}
fun memcpy = "llvm.memcpy.p0i8.p0i8.i64"(dest : Void*, src : Void*, len : UInt64, is_volatile : Bool)

{% if flag?(:interpreted) %} @[Primitive(:interpreter_intrinsics_memmove)] {% end %}
fun memmove = "llvm.memmove.p0i8.p0i8.i64"(dest : Void*, src : Void*, len : UInt64, is_volatile : Bool)

{% if flag?(:interpreted) %} @[Primitive(:interpreter_intrinsics_memset)] {% end %}
fun memset = "llvm.memset.p0i8.i64"(dest : Void*, val : UInt8, len : UInt64, is_volatile : Bool)
{% else %}
{% if flag?(:interpreted) %} @[Primitive(:interpreter_intrinsics_memcpy)] {% end %}
fun memcpy = "llvm.memcpy.p0.p0.i64"(dest : Void*, src : Void*, len : UInt64, is_volatile : Bool)

{% if flag?(:interpreted) %} @[Primitive(:interpreter_intrinsics_memmove)] {% end %}
fun memmove = "llvm.memmove.p0.p0.i64"(dest : Void*, src : Void*, len : UInt64, is_volatile : Bool)

{% if flag?(:interpreted) %} @[Primitive(:interpreter_intrinsics_memset)] {% end %}
fun memset = "llvm.memset.p0.i64"(dest : Void*, val : UInt8, len : UInt64, is_volatile : Bool)
{% end %}
{% else %}
{% if compare_versions(Crystal::LLVM_VERSION, "15.0.0") < 0 %}
{% if flag?(:interpreted) %} @[Primitive(:interpreter_intrinsics_memcpy)] {% end %}
fun memcpy = "llvm.memcpy.p0i8.p0i8.i32"(dest : Void*, src : Void*, len : UInt32, is_volatile : Bool)

{% if flag?(:interpreted) %} @[Primitive(:interpreter_intrinsics_memmove)] {% end %}
fun memmove = "llvm.memmove.p0i8.p0i8.i32"(dest : Void*, src : Void*, len : UInt32, is_volatile : Bool)
{% if flag?(:interpreted) %} @[Primitive(:interpreter_intrinsics_memmove)] {% end %}
fun memmove = "llvm.memmove.p0i8.p0i8.i32"(dest : Void*, src : Void*, len : UInt32, is_volatile : Bool)

{% if flag?(:interpreted) %} @[Primitive(:interpreter_intrinsics_memset)] {% end %}
fun memset = "llvm.memset.p0i8.i32"(dest : Void*, val : UInt8, len : UInt32, is_volatile : Bool)
{% else %}
{% if flag?(:interpreted) %} @[Primitive(:interpreter_intrinsics_memcpy)] {% end %}
fun memcpy = "llvm.memcpy.p0.p0.i32"(dest : Void*, src : Void*, len : UInt32, is_volatile : Bool)
{% if flag?(:interpreted) %} @[Primitive(:interpreter_intrinsics_memset)] {% end %}
fun memset = "llvm.memset.p0i8.i32"(dest : Void*, val : UInt8, len : UInt32, is_volatile : Bool)
{% else %}
{% if flag?(:interpreted) %} @[Primitive(:interpreter_intrinsics_memcpy)] {% end %}
fun memcpy = "llvm.memcpy.p0.p0.i32"(dest : Void*, src : Void*, len : UInt32, is_volatile : Bool)

{% if flag?(:interpreted) %} @[Primitive(:interpreter_intrinsics_memmove)] {% end %}
fun memmove = "llvm.memmove.p0.p0.i32"(dest : Void*, src : Void*, len : UInt32, is_volatile : Bool)
{% if flag?(:interpreted) %} @[Primitive(:interpreter_intrinsics_memmove)] {% end %}
fun memmove = "llvm.memmove.p0.p0.i32"(dest : Void*, src : Void*, len : UInt32, is_volatile : Bool)

{% if flag?(:interpreted) %} @[Primitive(:interpreter_intrinsics_memset)] {% end %}
fun memset = "llvm.memset.p0.i32"(dest : Void*, val : UInt8, len : UInt32, is_volatile : Bool)
{% if flag?(:interpreted) %} @[Primitive(:interpreter_intrinsics_memset)] {% end %}
fun memset = "llvm.memset.p0.i32"(dest : Void*, val : UInt8, len : UInt32, is_volatile : Bool)
{% end %}
{% end %}
{% end %}

Expand Down
16 changes: 16 additions & 0 deletions src/llvm.cr
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,22 @@ module LLVM
{% end %}
end

def self.init_avr : Nil
return if @@initialized_avr
@@initialized_avr = true

{% if LibLLVM::BUILT_TARGETS.includes?(:avr) %}
LibLLVM.initialize_avr_target_info
LibLLVM.initialize_avr_target
LibLLVM.initialize_avr_target_mc
LibLLVM.initialize_avr_asm_printer
LibLLVM.initialize_avr_asm_parser
LibLLVM.link_in_mc_jit
{% else %}
raise "ERROR: LLVM was built without AVR target"
{% end %}
end

def self.init_webassembly : Nil
return if @@initialized_webassembly
@@initialized_webassembly = true
Expand Down
Loading

0 comments on commit 7ecb968

Please sign in to comment.