Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

WIP: Add first draft ethernet support #17

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Add first draft ethernet support
dtwood committed Jul 7, 2020

Verified

This commit was signed with the committer’s verified signature.
florianduros Florian Duros
commit 88fc66944550bc2f876baef10bc6f399d9088877
20 changes: 20 additions & 0 deletions tm4c129x-hal/Cargo.toml
Original file line number Diff line number Diff line change
@@ -15,10 +15,15 @@ repository = "https://github.com/thejpster/tm4c-hal/tm4c129x-hal"
edition = "2018"

[dependencies]
bare-metal = "0.2.4"
cortex-m = "0.6"
nb = "0.1"
tm4c129x = "0.9"

[dependencies.byteorder]
version = "1"
default-features = false

[dependencies.cast]
version = "0.2"
default-features = false
@@ -27,6 +32,17 @@ default-features = false
version = "0.2"
features = ["unproven"]

[dependencies.smoltcp]
version = "0.6"
default_features = false
features = [
"proto-ipv4",
"proto-ipv6",
"socket-raw",
"socket-udp",
"socket-tcp",
]

[dependencies.void]
version = "1.0"
default-features = false
@@ -35,5 +51,9 @@ default-features = false
version = "0.3.0"
path = "../tm4c-hal"

[dependencies.vcell]
version = "0.1.0"
features = ["const-fn"]

[features]
rt = ["tm4c129x/rt"]
3,993 changes: 3,993 additions & 0 deletions tm4c129x-hal/src/edes.rs

Large diffs are not rendered by default.

101 changes: 101 additions & 0 deletions tm4c129x-hal/src/edes_old.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
#![allow(dead_code)]

pub const DES0_TX_CTRL_OWN: u32 = 2147483648;
pub const DES0_TX_CTRL_INTERRUPT: u32 = 1073741824;
pub const DES0_TX_CTRL_LAST_SEG: u32 = 536870912;
pub const DES0_TX_CTRL_FIRST_SEG: u32 = 268435456;
pub const DES0_TX_CTRL_DISABLE_CRC: u32 = 134217728;
pub const DES0_TX_CTRL_DISABLE_PADDING: u32 = 67108864;
pub const DES0_TX_CTRL_ENABLE_TS: u32 = 33554432;
pub const DES0_TX_CTRL_REPLACE_CRC: u32 = 16777216;
pub const DES0_TX_CTRL_CHKSUM_M: u32 = 12582912;
pub const DES0_TX_CTRL_NO_CHKSUM: u32 = 0;
pub const DES0_TX_CTRL_IP_HDR_CHKSUM: u32 = 4194304;
pub const DES0_TX_CTRL_IP_HDR_PAY_CHKSUM: u32 = 8388608;
pub const DES0_TX_CTRL_IP_ALL_CKHSUMS: u32 = 12582912;
pub const DES0_TX_CTRL_END_OF_RING: u32 = 2097152;
pub const DES0_TX_CTRL_CHAINED: u32 = 1048576;
pub const DES0_TX_CTRL_VLAN_M: u32 = 786432;
pub const DES0_TX_CTRL_VLAN_NONE: u32 = 0;
pub const DES0_TX_CTRL_VLAN_REMOVE: u32 = 262144;
pub const DES0_TX_CTRL_VLAN_INSERT: u32 = 524288;
pub const DES0_TX_CTRL_VLAN_REPLACE: u32 = 786432;
pub const DES0_TX_STAT_TS_CAPTURED: u32 = 131072;
pub const DES0_TX_STAT_IPH_ERR: u32 = 65536;
pub const DES0_TX_STAT_ERR: u32 = 32768;
pub const DES0_TX_STAT_JABBER_TO: u32 = 16384;
pub const DES0_TX_STAT_FLUSHED: u32 = 8192;
pub const DES0_TX_STAT_PAYLOAD_ERR: u32 = 4096;
pub const DES0_TX_STAT_CARRIER_LOST: u32 = 2048;
pub const DES0_TX_STAT_NO_CARRIER: u32 = 1024;
pub const DES0_TX_STAT_TX_L_COLLISION: u32 = 512;
pub const DES0_TX_STAT_E_COLLISION: u32 = 256;
pub const DES0_TX_STAT_VLAN_FRAME: u32 = 128;
pub const DES0_TX_STAT_COL_COUNT_M: u32 = 120;
pub const DES0_TX_STAT_COL_COUNT_S: u32 = 3;
pub const DES0_TX_STAT_E_DEFERRAL: u32 = 4;
pub const DES0_TX_STAT_UNDERFLOW: u32 = 2;
pub const DES0_TX_STAT_DEFERRED: u32 = 1;
pub const DES1_TX_CTRL_SADDR_MAC1: u32 = 2147483648;
pub const DES1_TX_CTRL_SADDR_M: u32 = 1610612736;
pub const DES1_TX_CTRL_SADDR_NONE: u32 = 0;
pub const DES1_TX_CTRL_SADDR_INSERT: u32 = 536870912;
pub const DES1_TX_CTRL_SADDR_REPLACE: u32 = 1073741824;
pub const DES1_TX_CTRL_BUFF2_SIZE_M: u32 = 536805376;
pub const DES1_TX_CTRL_BUFF1_SIZE_M: u32 = 8191;
pub const DES1_TX_CTRL_BUFF2_SIZE_S: u32 = 16;
pub const DES1_TX_CTRL_BUFF1_SIZE_S: u32 = 0;
pub const DES0_RX_CTRL_OWN: u32 = 2147483648;
pub const DES0_RX_STAT_DEST_ADDR_FAIL: u32 = 1073741824;
pub const DES0_RX_STAT_FRAME_LENGTH_M: u32 = 1073676288;
pub const DES0_RX_STAT_FRAME_LENGTH_S: u32 = 16;
pub const DES0_RX_STAT_ERR: u32 = 32768;
pub const DES0_RX_STAT_DESCRIPTOR_ERR: u32 = 16384;
pub const DES0_RX_STAT_SRC_ADDR_FAIL: u32 = 8192;
pub const DES0_RX_STAT_LENGTH_ERR: u32 = 4096;
pub const DES0_RX_STAT_OVERFLOW: u32 = 2048;
pub const DES0_RX_STAT_VLAN_TAG: u32 = 1024;
pub const DES0_RX_STAT_FIRST_DESC: u32 = 512;
pub const DES0_RX_STAT_LAST_DESC: u32 = 256;
pub const DES0_RX_STAT_TS_AVAILABLE: u32 = 128;
pub const DES0_RX_STAT_RX_L_COLLISION: u32 = 64;
pub const DES0_RX_STAT_FRAME_TYPE: u32 = 32;
pub const DES0_RX_STAT_WDOG_TIMEOUT: u32 = 16;
pub const DES0_RX_STAT_RX_ERR: u32 = 8;
pub const DES0_RX_STAT_DRIBBLE_ERR: u32 = 4;
pub const DES0_RX_STAT_CRC_ERR: u32 = 2;
pub const DES0_RX_STAT_MAC_ADDR: u32 = 1;
pub const DES0_RX_STAT_EXT_AVAILABLE: u32 = 1;
pub const DES1_RX_CTRL_DISABLE_INT: u32 = 2147483648;
pub const DES1_RX_CTRL_BUFF2_SIZE_M: u32 = 536805376;
pub const DES1_RX_CTRL_BUFF2_SIZE_S: u32 = 16;
pub const DES1_RX_CTRL_END_OF_RING: u32 = 32768;
pub const DES1_RX_CTRL_CHAINED: u32 = 16384;
pub const DES1_RX_CTRL_BUFF1_SIZE_M: u32 = 8191;
pub const DES1_RX_CTRL_BUFF1_SIZE_S: u32 = 0;
pub const DES4_RX_STAT_TS_DROPPED: u32 = 16384;
pub const DES4_RX_STAT_PTP_VERSION2: u32 = 8192;
pub const DES4_RX_STAT_PTP_TYPE_ETH: u32 = 4096;
pub const DES4_RX_STAT_PTP_TYPE_UDP: u32 = 0;
pub const DES4_RX_STAT_PTP_MT_M: u32 = 3840;
pub const DES4_RX_STAT_PTP_MT_NONE: u32 = 0;
pub const DES4_RX_STAT_PTP_MT_SYNC: u32 = 256;
pub const DES4_RX_STAT_PTP_MT_FOLLOW_UP: u32 = 512;
pub const DES4_RX_STAT_PTP_MT_DELAY_REQ: u32 = 768;
pub const DES4_RX_STAT_PTP_MT_DELAY_RESP: u32 = 1024;
pub const DES4_RX_STAT_PTP_MT_PDELAY_REQ: u32 = 1280;
pub const DES4_RX_STAT_PTP_MT_PDELAY_RESP: u32 = 1536;
pub const DES4_RX_STAT_PTP_MT_PDELAY_RFU: u32 = 1792;
pub const DES4_RX_STAT_PTP_MT_ANNOUNCE: u32 = 2048;
pub const DES4_RX_STAT_PTP_MT_SIGNALLING: u32 = 2560;
pub const DES4_RX_STAT_PTP_MT_RESERVED: u32 = 3840;
pub const DES4_RX_STAT_IPV6: u32 = 128;
pub const DES4_RX_STAT_IPV4: u32 = 64;
pub const DES4_RX_STAT_IP_CHK_BYPASSED: u32 = 32;
pub const DES4_RX_STAT_IP_PAYLOAD_ERR: u32 = 16;
pub const DES4_RX_STAT_IP_HEADER_ERR: u32 = 8;
pub const DES4_RX_STAT_PAYLOAD_M: u32 = 7;
pub const DES4_RX_STAT_PAYLOAD_UNKNOWN: u32 = 0;
pub const DES4_RX_STAT_PAYLOAD_UDP: u32 = 1;
pub const DES4_RX_STAT_PAYLOAD_TCP: u32 = 2;
pub const DES4_RX_STAT_PAYLOAD_ICMP: u32 = 3;
1,068 changes: 1,068 additions & 0 deletions tm4c129x-hal/src/ephy.rs

Large diffs are not rendered by default.

439 changes: 439 additions & 0 deletions tm4c129x-hal/src/ethernet.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,439 @@
use crate::edes::rdes::RDES;
use crate::edes::tdes::TDES;
use crate::edes_old::*;
use crate::sysctl;
use byteorder::ByteOrder;
use core::convert::TryInto;
use smoltcp::phy::{self, ChecksumCapabilities, Device, DeviceCapabilities};
use smoltcp::time::Instant;
use tm4c129x::EMAC0;

pub struct EphyReg(pub u8);

impl EphyReg {
pub fn get(&self, emac0: &mut tm4c129x::EMAC0, phy_addr: u8) -> u16 {
let reg_addr = self.0;

assert!(phy_addr < 32);
assert!(reg_addr < 32);

assert!(emac0.miiaddr.read().miib().bit_is_clear());

unsafe {
emac0.miiaddr.modify(|_, w| {
w.pla().bits(phy_addr);
w.mii().bits(reg_addr);

w.miiw().clear_bit();
w.miib().set_bit();

w
});
}

while emac0.miiaddr.read().miib().bit_is_set() {}

emac0.miidata.read().data().bits()
}

pub fn set(&self, emac0: &mut tm4c129x::EMAC0, phy_addr: u8, value: u16) {
let reg_addr = self.0;

assert!(phy_addr < 32);
assert!(reg_addr < 32);

assert!(emac0.miiaddr.read().miib().bit_is_clear());

unsafe {
emac0.miidata.write(|w| w.bits(value.into()));

emac0.miiaddr.modify(|_, w| {
w.pla().bits(phy_addr);
w.mii().bits(reg_addr);

w.miiw().set_bit();
w.miib().set_bit();

w
});
}

while emac0.miiaddr.read().miib().bit_is_set() {}
}
}

const ETHERNET_MTU: usize = 1500;
const NUM_TX_DESCRIPTORS: usize = 2;
const NUM_RX_DESCRIPTORS: usize = 10;

static RX_DESCRIPTORS: [RDES; NUM_RX_DESCRIPTORS] = [
RDES::new(),
RDES::new(),
RDES::new(),
RDES::new(),
RDES::new(),
RDES::new(),
RDES::new(),
RDES::new(),
RDES::new(),
RDES::new(),
];
static TX_DESCRIPTORS: [TDES; NUM_TX_DESCRIPTORS] = [TDES::new(), TDES::new()];

static mut RX_BUFFERS: [[u8; ETHERNET_MTU]; NUM_RX_DESCRIPTORS] =
[[0; ETHERNET_MTU]; NUM_RX_DESCRIPTORS];
static mut TX_BUFFERS: [[u8; ETHERNET_MTU]; NUM_TX_DESCRIPTORS] =
[[0; ETHERNET_MTU]; NUM_TX_DESCRIPTORS];

pub struct EthernetDevice {
emac0: EMAC0,
next_rx_descriptor: &'static RDES,
next_tx_descriptor: &'static TDES,
}

impl EthernetDevice {
pub fn new(
lock: &sysctl::PowerControl,
clocks: sysctl::Clocks,
nvic: &mut cortex_m::peripheral::NVIC,
mut emac0: EMAC0,
ephy: crate::ephy::EPHY,
) -> EthernetDevice {
sysctl::control_power(
lock,
sysctl::Domain::Emac0,
sysctl::RunMode::Run,
sysctl::PowerState::On,
);
sysctl::control_power(
lock,
sysctl::Domain::Ephy0,
sysctl::RunMode::Run,
sysctl::PowerState::On,
);

emac0.dmabusmod.modify(|_, w| w.swr().set_bit());

while emac0.dmabusmod.read().swr().bit_is_set() {}

emac0.pc.modify(|_, w| {
// EMAC_PHY_TYPE_INTERNAL
w.phyext().clear_bit();
w.pintfs().imii();
// EMAC_PHY_INT_MDIX_EN
w.mdixen().set_bit();

// EMAC_PHY_AN_100B_T_FULL_DUPLEX
w.anen().set_bit();
w.anmode()._100fd();

w
});

let pc = emac0.pc.read();
if pc.phyext().bit_is_clear() {
sysctl::reset(lock, sysctl::Domain::Ephy0);
for _ in 0..10000 {
cortex_m::asm::nop();
}
}

// TI's register definitions seem to disagree with the datasheet here - this
// register should be RW, and also doesn't seem to have the CLKEN field we need.
// For now just assert that the bit is already set to the value we expect.
if pc.pintfs().is_rmii() {
// emac0.cc.modify(|_, w| w.clken().set_bit());
assert!(emac0.cc.read().bits() & 0x00010000 == 0x00010000);
} else {
// emac0.cc.modify(|_, w| w.clken().clear_bit());
assert!(emac0.cc.read().bits() & 0x00010000 == 0);
}

sysctl::reset(lock, sysctl::Domain::Emac0);

for _ in 0..1000 {
cortex_m::asm::nop();
}

// Make sure that the DMA software reset is clear before continuing.
while emac0.dmabusmod.read().swr().bit_is_set() {}

emac0.dmabusmod.reset();

unsafe {
emac0.miiaddr.modify(|_, w| {
w.cr().bits(if clocks.sysclk.0 < 20_000_000 {
panic!()
} else if clocks.sysclk.0 < 35_000_000 {
0x8
} else if clocks.sysclk.0 < 60_000_000 {
0xc
} else if clocks.sysclk.0 < 100_000_000 {
0x0
} else if clocks.sysclk.0 < 150_000_000 {
0x4
} else {
panic!()
})
});
}

// Disable all the MMC interrupts as these are enabled by default at reset.
unsafe {
emac0.mmcrxim.write(|w| w.bits(0xffffffff));
emac0.mmctxim.write(|w| w.bits(0xffffffff));
}

emac0.cfg.modify(|_, w| {
// w.saddr().bits(0x02);
w.cst().set_bit();
w.ifg()._96();
w.dupm().set_bit();
w.fes().set_bit();
w.ipc().set_bit();
w.acs().set_bit();
w.bl()._1024();

w
});

emac0.wdogto.reset();

emac0.dmaopmode.write(|w| {
w.rsf().set_bit();
w.tsf().set_bit();
w.ttc()._64();
w.rtc()._64();

w
});

unsafe {
for i in 0..NUM_TX_DESCRIPTORS {
TX_DESCRIPTORS[i].tdes0.write(|w| {
w.bits(
DES0_TX_CTRL_LAST_SEG
| DES0_TX_CTRL_FIRST_SEG
| DES0_TX_CTRL_CHAINED
| DES0_TX_CTRL_IP_ALL_CKHSUMS,
)
});
TX_DESCRIPTORS[i]
.tdes1
.write(|w| w.bits(DES1_TX_CTRL_SADDR_INSERT));
TX_DESCRIPTORS[i]
.tdes2
.write(|w| w.bits(&mut TX_BUFFERS[i] as *mut _ as *mut _ as u32));
TX_DESCRIPTORS[i].tdes3.write(|w| {
w.bits(if i == NUM_TX_DESCRIPTORS - 1 {
&TX_DESCRIPTORS[0]
} else {
&TX_DESCRIPTORS[i + 1]
} as *const _ as u32)
});
}

for i in 0..NUM_RX_DESCRIPTORS {
RX_DESCRIPTORS[i].rdes0.write(|w| w.own().set_bit());
RX_DESCRIPTORS[i].rdes1.write(|w| {
w.bits(
DES1_RX_CTRL_CHAINED | ((ETHERNET_MTU as u32) << DES1_RX_CTRL_BUFF1_SIZE_S),
)
});
RX_DESCRIPTORS[i]
.rdes2
.write(|w| w.bits(&mut RX_BUFFERS[i][0] as *mut u8 as u32));
RX_DESCRIPTORS[i].rdes3.write(|w| {
w.bits(if i == (NUM_RX_DESCRIPTORS - 1) {
&RX_DESCRIPTORS[0]
} else {
&RX_DESCRIPTORS[i + 1]
} as *const _ as u32)
});
}

emac0
.rxdladdr
.write(|w| w.bits(&RX_DESCRIPTORS as *const _ as u32));
emac0
.txdladdr
.write(|w| w.bits(&TX_DESCRIPTORS as *const _ as u32));
}

{
unsafe {
let mac_addr = [0x00u8, 0x1A, 0xB6, 0x00, 0x02, 0x74];

emac0.addr0h.write(|w| {
w.addrhi()
.bits(byteorder::LittleEndian::read_u16(&mac_addr[4..]))
});
emac0.addr0l.write(|w| {
w.addrlo()
.bits(byteorder::LittleEndian::read_u32(&mac_addr[..4]))
});
}
}

while ephy.bmsr.read(&mut emac0).linkstat().bit_is_clear() {}

emac0.framefltr.modify(|_, w| {
w.ra().set_bit();
w.pr().set_bit();

w
});

unsafe {
emac0.dmaim.write(|w| w.bits(0xffff_ffff));
emac0.ephyim.write(|w| w.bits(0xffff_ffff));
}

emac0.dmaopmode.modify(|_, w| {
w.sr().set_bit();
w.st().set_bit();

w
});
emac0.cfg.modify(|_, w| {
w.re().set_bit();
w.te().set_bit();

w
});

nvic.enable(tm4c129x::Interrupt::EMAC0);

EthernetDevice {
emac0,
next_rx_descriptor: &RX_DESCRIPTORS[0],
next_tx_descriptor: &TX_DESCRIPTORS[0],
}
}
}

impl<'a> Device<'a> for EthernetDevice {
type RxToken = RxToken<'a>;
type TxToken = TxToken<'a>;

fn receive(&'a mut self) -> Option<(Self::RxToken, Self::TxToken)> {
if self.next_rx_descriptor.rdes0.read().own().bit_is_set() {
return None;
}
if self.next_tx_descriptor.tdes0.read().own().bit_is_set() {
return None;
}

Some((
RxToken {
emac0: &self.emac0,
descriptor_pointer: &mut self.next_rx_descriptor,
},
TxToken {
emac0: &self.emac0,
descriptor_pointer: &mut self.next_tx_descriptor,
},
))
}

fn transmit(&'a mut self) -> Option<Self::TxToken> {
if self.next_tx_descriptor.tdes0.read().own().bit_is_set() {
return None;
}

Some(TxToken {
emac0: &self.emac0,
descriptor_pointer: &mut self.next_tx_descriptor,
})
}

fn capabilities(&self) -> DeviceCapabilities {
let mut cap = DeviceCapabilities::default();

cap.max_transmission_unit = ETHERNET_MTU;
cap.max_burst_size = Some(NUM_TX_DESCRIPTORS);

cap.checksum = ChecksumCapabilities::default();
// cap.checksum.ipv4 = Checksum::None;
// cap.checksum.ipv6 = Checksum::None;
// cap.checksum.udp = Checksum::None;
// cap.checksum.tcp = Checksum::None;
// cap.checksum.icmpv4 = Checksum::None;
// cap.checksum.icmpv6 = Checksum::None;

cap
}
}

pub struct RxToken<'a> {
emac0: &'a EMAC0,
descriptor_pointer: &'a mut &'static RDES,
}

impl<'a> phy::RxToken for RxToken<'a> {
fn consume<R, F>(self, _timestamp: Instant, f: F) -> smoltcp::Result<R>
where
F: FnOnce(&mut [u8]) -> smoltcp::Result<R>,
{
let descriptor = *self.descriptor_pointer;

// We own the receive descriptor so check to see if it contains a valid frame.
if descriptor.rdes0.read().bits() & DES0_RX_STAT_ERR == DES0_RX_STAT_ERR {
descriptor.rdes0.write(|w| w.own().set_bit());
return Err(smoltcp::Error::Checksum);
}

// We have a valid frame. First check that the "last descriptor" flag is set. We
// sized the receive buffer such that it can always hold a valid frame so this
// flag should never be clear at this point but...
if descriptor.rdes0.read().bits() & DES0_RX_STAT_LAST_DESC != DES0_RX_STAT_LAST_DESC {
descriptor.rdes0.write(|w| w.own().set_bit());
return Err(smoltcp::Error::Truncated);
}

let len = ((descriptor.rdes0.read().bits() & DES0_RX_STAT_FRAME_LENGTH_M)
>> DES0_RX_STAT_FRAME_LENGTH_S) as usize;
assert!(len <= ETHERNET_MTU);
let data =
unsafe { core::slice::from_raw_parts_mut(descriptor.rdes2.read().bits() as *mut u8, len) };

let result = f(data);

descriptor.rdes0.write(|w| w.own().set_bit());
self.emac0.rxpolld.write(|w| w);
*self.descriptor_pointer = unsafe { &*(descriptor.rdes3.read().bits() as *const _) };
result
}
}

pub struct TxToken<'a> {
emac0: &'a EMAC0,
descriptor_pointer: &'a mut &'static TDES,
}

impl<'a> phy::TxToken for TxToken<'a> {
fn consume<R, F>(self, _timestamp: Instant, len: usize, f: F) -> smoltcp::Result<R>
where
F: FnOnce(&mut [u8]) -> smoltcp::Result<R>,
{
let descriptor = *self.descriptor_pointer;

assert!(len <= ETHERNET_MTU);

let data = unsafe {
core::slice::from_raw_parts_mut(descriptor.tdes2.read().bits() as *mut u8, len)
};
let result = f(data);

unsafe {
descriptor
.tdes1
.write(|w| w.tbs1().bits(len.try_into().unwrap()));
}

descriptor.tdes0.modify(|_, w| w.own().set_bit());
self.emac0.txpolld.write(|w| w);
*self.descriptor_pointer = unsafe { &*(descriptor.tdes3.read().bits() as *const _) };
result
}
}
6 changes: 4 additions & 2 deletions tm4c129x-hal/src/lib.rs
Original file line number Diff line number Diff line change
@@ -21,13 +21,15 @@
//! [`f3`]: https://docs.rs/f3/~0.5.1
#![no_std]
#![deny(missing_docs)]
#![deny(warnings)]
#![allow(deprecated)]

pub use tm4c129x::{self, CorePeripherals, Peripherals};
pub use tm4c_hal::{bb, delay, time};

pub mod edes;
mod edes_old;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What's edes_old for?

pub mod ephy;
pub mod ethernet;
pub mod gpio;
pub mod i2c;
pub mod prelude;