Compare commits

...

6 Commits

14 changed files with 383 additions and 46 deletions

3
.gitmodules vendored
View File

@ -1,6 +1,3 @@
[submodule "gateware/amaranth-boards"]
path = gateware/amaranth-boards
url = https://github.com/amaranth-lang/amaranth-boards
[submodule "gateware/jtagtap"]
path = gateware/jtagtap
url = git@github.com:davidlenfesty/jtagtap

View File

@ -9,3 +9,6 @@ edition = "2021"
riscv-rt = "0.11.0"
panic-halt = "0.2.0"
embedded-hal = "0.2.7"
[profile.release]
debug = true

View File

@ -1,7 +1,7 @@
MEMORY
{
RAM : ORIGIN = 0x01001000, LENGTH = 4K
FLASH : ORIGIN = 0x01000000, LENGTH = 4K
RAM : ORIGIN = 0x01002000, LENGTH = 4K
FLASH : ORIGIN = 0x01000000, LENGTH = 8K
}
REGION_ALIAS("REGION_TEXT", FLASH);

View File

@ -1,6 +1,6 @@
//! Quick and hacky ethernet thing to test
const LITEETH_BASE: u32 = 0x0200_0000;
const LITEETH_BASE: u32 = 0x0300_0000;
const ETHMAC_SRAM_WRITER_EV_PENDING: u32 = LITEETH_BASE + 0x810;
const ETHMAC_SRAM_WRITER_EV_ENABLE: u32 = LITEETH_BASE + 0x814;

104
firmware/src/i2c.rs Normal file
View File

@ -0,0 +1,104 @@
//! I2C driver for my amlib i2c interface implementation.
//!
//! See `gateware/i2c.py` for register information
/// TODO repr(C) a register bank, and add instances
// Using the blocking API because the peripheral requires fairly tight timing to operate
// correctly, and I don't feel like writing the gateware to resolve that.
use embedded_hal::blocking::i2c::{Write, Read, SevenBitAddress, Transactional, Operation};
use crate::{read_reg, write_reg};
use core::arch::asm;
// TODO I think there may be bus address semantics I'm not 100% on.
// There's a possiblity these addresses are wrong, and they need to be 4 bytes each
const CR: u32 = 0;
const SR: u32 = 1;
const DWR: u32 = 2;
const DRR: u32 = 3;
#[derive(Clone, Copy, Debug)]
pub enum Error {
/// Device is busy for some reason
Busy,
/// I2C bus returned a NACK
Nack,
}
pub struct AmlibI2c {
base_addr: u32,
}
impl AmlibI2c {
pub fn new(base_addr: u32) -> Self {
AmlibI2c { base_addr }
}
fn wait_while_busy(&self) {
unsafe {
while read_reg::<u32>(self.base_addr + SR) & 1 != 0 {
asm!("nop");
}
}
}
}
impl Write<SevenBitAddress> for AmlibI2c {
// TODO errors
type Error = Error;
fn write(&mut self, address: SevenBitAddress, bytes: &[u8]) -> Result<(), Self::Error> {
unsafe {
if (read_reg::<u32>(self.base_addr + SR) & 1) != 0 {
return Err(Error::Busy);
}
// START
write_reg(self.base_addr + CR, 0x01u32);
// Pre-load data w/ address (R/~W = 0)
write_reg(self.base_addr + DWR, (address << 1) as u32);
self.wait_while_busy();
// Send address byte
write_reg(self.base_addr + CR, 0x04u32);
if read_reg::<u32>(self.base_addr + SR) & 0x02 != 0 {
return Err(Error::Nack);
}
for byte in bytes {
// Write byte
write_reg(self.base_addr + DWR, *byte as u32);
self.wait_while_busy();
// Send byte once done sending the last byte
write_reg(self.base_addr + CR, 0x04u32);
}
self.wait_while_busy();
// STOP
write_reg(self.base_addr + CR, 0x02u32);
self.wait_while_busy();
Ok(())
}
}
}
impl Read<SevenBitAddress> for AmlibI2c {
// TODO errors
type Error = Error;
fn read(&mut self, address: SevenBitAddress, buffer: &mut [u8]) -> Result<(), Self::Error> {
Ok(())
}
}
impl Transactional for AmlibI2c {
type Error = Error;
fn exec<'a>(&mut self, address: u8, operations: &mut [Operation<'a>])
-> Result<(), Self::Error> {
Ok(())
}
}

View File

@ -4,12 +4,14 @@
extern crate panic_halt;
use core::{arch::asm, ptr::{write, read}};
use core::fmt::Write;
use embedded_hal::prelude::_embedded_hal_blocking_i2c_Write;
use riscv_rt::entry;
mod eth;
mod i2c;
mod uart;
// use `main` as the entry point of this application
// `main` is not allowed to return
@ -26,12 +28,15 @@ fn main() -> ! {
//};
let blink_period = 10_000_000u32;
let mut i2c = i2c::AmlibI2c::new(0x01003000);
let data = [0u8, 2u8];
i2c.write(0xAA, &data).unwrap();
//let mut i2c = i2c::AmlibI2c::new(0x0200_0000);
//let data = [0u8, 2u8];
//i2c.write(0xAA, &data).unwrap();
let mut uart = uart::AmlibUart::new(0x0200_0040);
loop {
//eth::tranmsit();
uart.write_str("Hello world!\r\n");
write_led(0);
busy_wait(blink_period);
write_led(1);
@ -48,7 +53,7 @@ fn busy_wait(num_nops: u32) {
}
fn write_led(val: u32) {
unsafe { write_reg(0x0100200, val); }
unsafe { write_reg(0x01003000, val); }
}
unsafe fn write_reg<T>(addr: u32, value: T) {

75
firmware/src/uart.rs Normal file
View File

@ -0,0 +1,75 @@
//! Quick and dirty uart driver
/// TODO repr(C) a register bank, and add instances
///
use core::ptr::{read_volatile, write_volatile};
use core::fmt::{Write};
// TODO these offsets may be wrong. I'm still unsure about CSR semantics
const REG_DIVISOR_OFFSET: u32 = 0;
const REG_SR_OFFSET: u32 = 2;
const REG_DR_OFFSET: u32 = 3;
pub const FLAG_SR_TX_FULL: u8 = (1 << 0);
pub const FLAG_SR_TX_EMPTY: u8 = (1 << 1);
pub const FLAG_SR_RX_FULL: u8 = (1 << 2);
pub const FLAG_SR_RX_EMPTY: u8 = (1 << 3);
pub enum Error {
TxFull,
RxEmpty,
}
pub struct AmlibUart {
base_addr: u32,
}
impl AmlibUart {
pub fn new(base_addr: u32) -> Self {
Self {
base_addr,
}
}
pub fn read_status(&mut self) -> u8 {
unsafe {
read_volatile((self.base_addr + REG_SR_OFFSET) as *const u8)
}
}
pub fn try_get_char(&mut self) -> Result<u8, Error> {
if self.read_status() & FLAG_SR_RX_EMPTY != 0 {
return Err(Error::RxEmpty);
}
unsafe {
Ok(read_volatile((self.base_addr + REG_DR_OFFSET) as *const u8))
}
}
pub fn try_put_char(&mut self, c: u8) -> Result<(), Error> {
if self.read_status() & FLAG_SR_TX_FULL != 0 {
return Err(Error::TxFull);
}
unsafe {
write_volatile((self.base_addr + REG_DR_OFFSET) as *mut u8, c);
}
Ok(())
}
}
// Blocking implementation of write
impl Write for AmlibUart {
fn write_str(&mut self, s: &str) -> core::fmt::Result {
for b in s.as_bytes() {
// It's okay to loop on this because we'll always clear the buffer
while let Err(Error::TxFull) = self.try_put_char(*b) {}
//self.try_put_char(*b);
}
Ok(())
}
}

@ -1 +0,0 @@
Subproject commit 1d82f2ece15ddcce964b9d3be1d13e8a343537eb

View File

@ -41,7 +41,7 @@ class I2C(Elaboratable):
# [3]: read - read a byte from the bus
# [4]: read_ack - ACK value that gets written out during a read operation
# [5]: read_ack_en - Hacky solution to determine if we want to save read_ack
self.CR = Element(6, Element.Access.W, name="CR")
self.CR = Element(6, Element.Access.W, name="I2C_CR")
# Status register
#
@ -49,27 +49,28 @@ class I2C(Elaboratable):
# [0]: busy - bus is busy operating
# [1]: ack - an ACK has been received from a bus slave
# [2]: read_ack - a convenience read field to see value of CR->read_ack
self.SR = Element(3, Element.Access.R, name="SR")
self.SR = Element(3, Element.Access.R, name="I2C_SR")
# Data write register
#
# Latches in data to be written when write signal is applied.
self.DWR = Element(8, Element.Access.W, name="DWR")
self.DWR = Element(8, Element.Access.W, name="I2C_DWR")
# Data read register
#
# Only presents valid data after 'read' has started, and once 'busy' is no longer asserted.
self.DRR = Element(8, Element.Access.R, name="DRR")
self.DRR = Element(8, Element.Access.R, name="I2C_DRR")
# Set up CSR bus
addr_width = ceil(log2(64)) # Support up to 64 registers just because
data_width = 8 # 32 bit bus
self._csr_mux = Multiplexer(addr_width=addr_width, data_width=data_width)
# TODO export the addresses of these somehow
self._csr_mux.add(self.CR)
self._csr_mux.add(self.SR)
self._csr_mux.add(self.DWR)
self._csr_mux.add(self.DRR)
# TODO export these addresses into some config file
cr_start, _stop = self._csr_mux.add(self.CR)
sr_start, _stop = self._csr_mux.add(self.SR)
dwr_start, _stop = self._csr_mux.add(self.DWR)
drr_start, _stop = self._csr_mux.add(self.DRR)
print(f"I2C added. CR 0x{cr_start:x}, SR 0x{sr_start:x}, DWR 0x{dwr_start:x}, DRR 0x{drr_start:x}")
self.bus = self._csr_mux.bus
# Set up I2C initiator submodule

View File

@ -2,7 +2,7 @@
from amaranth import *
from amaranth.sim import *
from amaranth_boards import colorlight_i9
from platforms import *
from amaranth_soc.wishbone import *
from amaranth_soc import csr
from amaranth_soc.csr.wishbone import WishboneCSRBridge
@ -122,7 +122,7 @@ class Core(Elaboratable):
fw = load_firmware_for_mem()
# Hook up memory space
self.rom = ROM(fw)
self.rom = ROM(size_bytes=8192, data=fw)
m.submodules.rom = self.rom
# Problem: not sure to handle how we do byte vs word addressing properly
# So doing this shift is a bit of a hacky way to impl anything
@ -143,10 +143,8 @@ class Core(Elaboratable):
# Create CSR bus and connect it to Wishbone
self.csr = csr.Decoder(addr_width=10, data_width=8)
m.submodules.csr = self.csr
print(f"CSR bus added at 0x{start:08x}")
# I2C (connected to DAC for VCO and ADC?)
Signal()
if platform is not None:
i2c_pads = platform.request("i2c")
else:
@ -158,17 +156,29 @@ class Core(Elaboratable):
m.d.comb += i2c_pads.scl.i.eq(1)
self.i2c = i2c.I2C(50e6, 100e3, i2c_pads)
m.submodules.i2c = self.i2c
self.csr.add(self.i2c.bus)
i2c_start, _stop, _step = self.csr.add(self.i2c.bus)
print(f"LED added to CSR at 0x{i2c_start}")
if platform is not None:
uart_pads = platform.request("uart", 1)
else:
uart_pads = None
# TODO spread sysclk freq through design
self.uart = uart.UART(50e6, 115_200, pins=uart_pads)
m.submodules.uart = self.uart
uart_start, _stop, _step = self.csr.add(self.uart.bus)
print(f"UART added to CSR at 0x{uart_start:x}")
self.csr_bridge = WishboneCSRBridge(self.csr.bus, data_width=32, name="CSR")
m.submodules.csr_bridge = self.csr_bridge
# TODO shouldn't have to hard-specify this address
start, _stop, _step = self.decoder.add(self.csr_bridge.wb_bus, addr=0x01003000)
start, _stop, _step = self.decoder.add(self.csr_bridge.wb_bus, addr=0x02000000)
print(f"CSR bus added at 0x{start:08x}")
# Ethernet
self.eth = LiteEth(self.eth_interface)
m.submodules.eth = self.eth
#start, _stop, _step = self.decoder.add(self.eth, addr=0x02000000)
start, _stop, _step = self.decoder.add(self.eth, addr=0x03000000)
print(f"LiteETH added at 0x{start:08x}")
# Connect arbiter to decoder
@ -187,6 +197,7 @@ class SoC(Elaboratable):
pass
def elaborate(self, platform):
# TODO pull I2C and UART into here instead of the "core"
if platform is not None:
clk25 = platform.request("clk25")
led_signal = platform.request("led")
@ -229,7 +240,7 @@ if __name__ == "__main__":
if args.build:
# Overrides are available via AMARANTH_<override_variable_name> env variable, or kwarg
# TODO fix platform so I don't have to manually specify MDIO signal
colorlight_i9.Colorlight_i9_Platform().build(SoC(), debug_verilog=args.gen_debug_verilog, nextpnr_opts="--router router1", add_preferences="LOCATE COMP \"top.eth.core.rgmii_eth_mdio\" SITE \"P5\";\n")
Colorlight_i9_Platform().build(SoC(), debug_verilog=args.gen_debug_verilog, nextpnr_opts="--router router1", add_preferences="LOCATE COMP \"top.eth.core.rgmii_eth_mdio\" SITE \"P5\";\n")
if args.test:
if args.save_vcd:

View File

@ -2,25 +2,28 @@ from amaranth import *
from amaranth_soc.wishbone import *
from amaranth_soc.memory import *
from math import log2, ceil
# TODO impl select
# We sub-class wishbone.Interface here because it needs to be a bus object to be added as a window to Wishbone stuff
class ROM(Elaboratable, Interface):
def __init__(self, data=None):
def __init__(self, *, size_bytes=4096, data=None):
#self.size = len(data)
self.data = Memory(width=32, depth=(4096 >> 2), init=data)
addr_width = ceil(log2(size_bytes >> 2))
self.data = Memory(width=32, depth=size_bytes >> 2, init=data)
self.r = self.data.read_port()
# Need to init Interface
Interface.__init__(self, addr_width=10, data_width=32, granularity=8)
Interface.__init__(self, addr_width=addr_width, data_width=32, granularity=8)
# This is effectively a "window", and it has a certain set of resources
# 12 = log2(4096)
memory_map = MemoryMap(addr_width=12, data_width=8)
memory_map = MemoryMap(addr_width=addr_width + 2, data_width=8)
# TODO need to unify how I deal with size
# In this case, one resource, which is out memory
memory_map.add_resource(self.data, name="rom_data", size=(4096 >> 2))
memory_map.add_resource(self.data, name="rom_data", size=size_bytes)
self.memory_map = memory_map
@ -52,21 +55,21 @@ class ROM(Elaboratable, Interface):
# TODO support read segmentation or whatever it's called, where you read/write certian bytes from memory
# Otherwise we can't store individual bytes, and this will wreck shit in weird ways.
class RAM(Elaboratable, Interface):
def __init__(self):
#self.size = len(data)
self.data = Memory(width=32, depth=(4096 >> 2))
def __init__(self, *, size_bytes=4096):
addr_width = ceil(log2(size_bytes >> 2))
self.data = Memory(width=32, depth=size_bytes >> 2)
self.r = self.data.read_port()
self.w = self.data.write_port()
# Need to init Interface
Interface.__init__(self, addr_width=10, data_width=32, granularity=8)
Interface.__init__(self, addr_width=addr_width, data_width=32, granularity=8)
# This is effectively a "window", and it has a certain set of resources
# 12 = log2(4096)
memory_map = MemoryMap(addr_width=12, data_width=8)
memory_map = MemoryMap(addr_width=addr_width + 2, data_width=8)
# TODO need to unify how I deal with size
# In this case, one resource, which is out memory
memory_map.add_resource(self.data, name="ram_data", size=(4096 >> 2))
memory_map.add_resource(self.data, name="ram_data", size=size_bytes)
self.memory_map = memory_map

View File

@ -0,0 +1 @@
from .colorlight_i9 import *

View File

@ -0,0 +1,136 @@
import os
import subprocess
from amaranth.build import *
from amaranth.vendor.lattice_ecp5 import *
from amaranth_boards.resources import *
__all__ = ["Colorlight_i9_Platform"]
class Colorlight_i9_Platform(LatticeECP5Platform):
device = "LFE5U-45F"
package = "BG381"
speed = "6"
default_clk = "clk25"
resources = [
Resource("clk25", 0, Pins("P3", dir="i"), Clock(25e6), Attrs(IO_TYPE="LVCMOS33")),
*LEDResources(pins="L2", invert = True,
attrs=Attrs(IO_TYPE="LVCMOS33", DRIVE="4")),
#*ButtonResources(pins="M13", invert = True,
# attrs=Attrs(IO_TYPE="LVCMOS33", PULLMODE="UP")),
UARTResource(0,
tx="E17",
rx="D18",
attrs=Attrs(IO_TYPE="LVCMOS33")
),
UARTResource(1,
tx="P16",
rx="L5",
attrs=Attrs(IO_TYPE="LVCMOS33")
),
UARTResource(2,
tx="J18",
rx="J16",
attrs=Attrs(IO_TYPE="LVCMOS33")
),
# SPIFlash (W25Q32JV) 1x/2x/4x speed
Resource("spi_flash", 0,
Subsignal("cs", PinsN("R2", dir="o")),
# Subsignal("clk", Pins("", dir="i")), # driven through USRMCLK
Subsignal("cipo", Pins("V2", dir="i")), # Chip: DI/IO0
Subsignal("copi", Pins("W2", dir="o")), # DO/IO1
Attrs(IO_TYPE="LVCMOS33")
),
# 2x ESMT M12L16161A-5T 1M x 16bit 200MHz SDRAMs (organized as 1M x 32bit)
# 2x WinBond W9816G6JH-6 1M x 16bit 166MHz SDRAMs (organized as 1M x 32bit) are lso reported
SDRAMResource(0,
clk="B9", we_n="A10", cas_n="A9", ras_n="B10",
ba="B11 C8", a="B13 C14 A16 A17 B16 B15 A14 A13 A12 A11 B12",
dq="D15 E14 E13 D12 E12 D11 C10 B17 B8 A8 C7 A7 A6 B6 A5 B5 "
"D5 C5 D6 C6 E7 D7 E8 D8 E9 D9 E11 C11 C12 D13 D14 C15",
attrs=Attrs(PULLMODE="NONE", DRIVE="4", SLEWRATE="FAST", IO_TYPE="LVCMOS33")
),
# Broadcom B50612D Gigabit Ethernet Transceiver
Resource("eth_rgmii", 0,
Subsignal("rst", PinsN("P4", dir="o")),
Subsignal("mdc", Pins("N5", dir="o")),
#Subsignal("mdio", Pins("P5", dir="io")),
Subsignal("tx_clk", Pins("U19", dir="o")),
Subsignal("tx_ctl", Pins("P19", dir="o")),
Subsignal("tx_data", Pins("U20 T19 T20 R20", dir="o")),
Subsignal("rx_clk", Pins("L19", dir="i")),
Subsignal("rx_ctl", Pins("M20", dir="i")),
Subsignal("rx_data", Pins("P20 N19 N20 M19", dir="i")),
Attrs(IO_TYPE="LVCMOS33")
),
# Broadcom B50612D Gigabit Ethernet Transceiver
Resource("eth_rgmii", 1,
Subsignal("rst", PinsN("P4", dir="o")),
Subsignal("mdc", Pins("N5", dir="o")),
#Subsignal("mdio", Pins("P5", dir="io")),
Subsignal("tx_clk", Pins("G1", dir="o")),
Subsignal("tx_ctl", Pins("K1", dir="o")),
Subsignal("tx_data", Pins("G2 H1 J1 J3", dir="o")),
Subsignal("rx_clk", Pins("H2", dir="i")),
Subsignal("rx_ctl", Pins("P2", dir="i")),
Subsignal("rx_data", Pins("K2 L1 N1 P1", dir="i")),
Attrs(IO_TYPE="LVCMOS33")
),
Resource("jtag", 0,
Subsignal("trst", Pins("J17", dir="i")),
Subsignal("tck", Pins("G18", dir="i")),
Subsignal("tms", Pins("H16", dir="i")),
Subsignal("tdo", Pins("H17", dir="o")),
Subsignal("tdi", Pins("H18", dir="i")),
Attrs(IO_TYPE="LVCMOS33")
),
Resource("i2c", 0,
Subsignal("sda", Pins("D16", dir="io")),
Subsignal("scl", Pins("F5", dir="io")), # Hacky stuff for now, amlib needs it to be io for some reason
Attrs(IO_TYPE="LVCMOS33")
),
]
connectors = []
# Connector("j", 1, "F3 F1 G3 - G2 H3 H5 F15 L2 K1 J5 K2 B16 J14 F12 -"),
# Connector("j", 2, "J4 K3 G1 - K4 C2 E3 F15 L2 K1 J5 K2 B16 J14 F12 -"),
# Connector("j", 3, "H4 K5 P1 - R1 L5 F2 F15 L2 K1 J5 K2 B16 J14 F12 -"),
# Connector("j", 4, "P4 R2 M8 - M9 T6 R6 F15 L2 K1 J5 K2 B16 J14 F12 -"),
# Connector("j", 5, "M11 N11 P12 - K15 N12 L16 F15 L2 K1 J5 K2 B16 J14 F12 -"),
# Connector("j", 6, "K16 J15 J16 - J12 H15 G16 F15 L2 K1 J5 K2 B16 J14 F12 -"),
# Connector("j", 7, "H13 J13 H12 - G14 H14 G15 F15 L2 K1 J5 K2 B16 J14 F12 -"),
# Connector("j", 8, "A15 F16 A14 - E13 B14 A13 F15 L2 K1 J5 K2 B16 J14 F12 -"),
# Connector("j", 19, " - M13 - - P11"),
#]
@property
def required_tools(self):
return super().required_tools + [
"ecpdap"
]
def toolchain_prepare(self, fragment, name, **kwargs):
overrides = dict(ecppack_opts="--compress")
overrides.update(kwargs)
return super().toolchain_prepare(fragment, name, **overrides)
def toolchain_program(self, products, name):
tool = os.environ.get("ECPDAP", "ecpdap")
with products.extract("{}.bit".format(name)) as bitstream_filename:
subprocess.check_call([tool, "program", bitstream_filename, "--freq", "10M"])
if __name__ == "__main__":
from .test.blinky import *
Colorlight_i9_Platform().build(Blinky(), do_program=True)

View File

@ -38,7 +38,7 @@ class UART(Elaboratable):
#
# Sets input/output baudrate to system clock / divisor. Resets to value
# that provides 115200 baud rate. Writes to this register clear FIFOs.
self.DIVISOR = Element(16, Element.Access.RW, name="DIVISOR")
self.DIVISOR = Element(16, Element.Access.RW, name="UART_DIVISOR")
# Status register.
#
@ -47,22 +47,23 @@ class UART(Elaboratable):
# [1]: txfifo_empty
# [2]: rxfifo_full
# [3]: rxfifo_empty
self.SR = Element(4, Element.Access.R, name="SR")
self.SR = Element(4, Element.Access.R, name="UART_SR")
# Data register.
#
# Writes push data into TX FIFO, and are discarded if full, reads pull
# data from RX FIFO, and are invalid if it is empty. Incoming bytes are discarded
# if the RX FIFO is full.
self.DR = Element(8, Element.Access.RW, name="DR")
self.DR = Element(8, Element.Access.RW, name="UART_DR")
# Set up CSR bus
addr_width = ceil(log2(64))
data_width = 32
data_width = 8
self._csr_mux = Multiplexer(addr_width=addr_width, data_width=data_width)
self._csr_mux.add(self.DIVISOR)
self._csr_mux.add(self.SR)
self._csr_mux.add(self.DR)
div_start, _stop = self._csr_mux.add(self.DIVISOR)
sr_start, _stop = self._csr_mux.add(self.SR)
dr_start, _stop = self._csr_mux.add(self.DR)
print(f"UART added. DIVISOR 0x{div_start:x}, SR 0x{sr_start:x}, DR 0x{dr_start:x}")
self.bus = self._csr_mux.bus
# Actual business logic
@ -94,6 +95,7 @@ class UART(Elaboratable):
m.submodules.csr_mux = self._csr_mux
# Hook up divisor to register.
# TODO do some validation and write a known good value if a dumb value was provided
m.d.comb += self.DIVISOR.r_data.eq(self._serial.divisor)
with m.If(self.DIVISOR.w_stb):
m.d.sync += self._serial.divisor.eq(self.DIVISOR.w_data)