DHCP support for core device firmware

DHCP is enabled by setting the `ip` config entry to "use_dhcp". Reusing this
config field rather than creating a new one means that there is no ambiguity
over which config field takes precedence.

Adds a thread to configure the interface based on DHCP events
Adds a `Dhcpv4Socket` as a wrapper around smoltcp's version
Formalises the storage of the IP addresses so that we can update one in
another module.

There's also a workaround for the first DHCP discover packet frequently
going missing.

Signed-off-by: Michael Birtwell <michael.birtwell@oxionics.com>
This commit is contained in:
Michael Birtwell 2022-01-14 16:52:12 +00:00 committed by Sebastien Bourdeauducq
parent c60de48a30
commit 6ffb1f83ee
7 changed files with 169 additions and 30 deletions

View File

@ -42,6 +42,7 @@ Highlights:
* ``PCA9548`` I2C switch class renamed to ``I2CSwitch``, to accomodate support for PCA9547, and * ``PCA9548`` I2C switch class renamed to ``I2CSwitch``, to accomodate support for PCA9547, and
possibly other switches in future. Readback has been removed, and now only one channel per possibly other switches in future. Readback has been removed, and now only one channel per
switch is supported. switch is supported.
* The "ip" config option can now be set to "use_dhcp" to use DHCP to obtain an IP address.
Breaking changes: Breaking changes:

View File

@ -1,12 +1,15 @@
use core::fmt; use core::fmt;
use smoltcp::wire::{EthernetAddress, IpAddress}; use smoltcp::wire::{EthernetAddress, IpAddress, Ipv4Address};
use config; use config;
#[cfg(soc_platform = "kasli")] #[cfg(soc_platform = "kasli")]
use i2c_eeprom; use i2c_eeprom;
pub const USE_DHCP: IpAddress = IpAddress::Ipv4(Ipv4Address::UNSPECIFIED);
pub struct NetAddresses { pub struct NetAddresses {
pub hardware_addr: EthernetAddress, pub hardware_addr: EthernetAddress,
pub ipv4_addr: IpAddress, pub ipv4_addr: IpAddress,
@ -49,7 +52,13 @@ pub fn get_adresses() -> NetAddresses {
} }
let ipv4_addr; let ipv4_addr;
match config::read_str("ip", |r| r.map(|s| s.parse())) { match config::read_str("ip", |r| r.map(|s| {
if s == "use_dhcp" {
Ok(USE_DHCP)
} else {
s.parse()
}
})) {
Ok(Ok(addr)) => ipv4_addr = addr, Ok(Ok(addr)) => ipv4_addr = addr,
_ => { _ => {
#[cfg(soc_platform = "kasli")] #[cfg(soc_platform = "kasli")]

View File

@ -27,9 +27,13 @@ board_misoc = { path = "../libboard_misoc", features = ["uart_console", "smoltcp
logger_artiq = { path = "../liblogger_artiq" } logger_artiq = { path = "../liblogger_artiq" }
board_artiq = { path = "../libboard_artiq" } board_artiq = { path = "../libboard_artiq" }
proto_artiq = { path = "../libproto_artiq", features = ["log", "alloc"] } proto_artiq = { path = "../libproto_artiq", features = ["log", "alloc"] }
smoltcp = { version = "0.8.0", default-features = false, features = ["alloc", "medium-ethernet", "proto-ipv4", "proto-ipv6", "socket-tcp"] }
riscv = { version = "0.6.0", features = ["inline-asm"] } riscv = { version = "0.6.0", features = ["inline-asm"] }
[dependencies.smoltcp]
version = "0.8.0"
default-features = false
features = ["alloc", "medium-ethernet", "proto-ipv4", "proto-ipv6", "socket-tcp", "socket-dhcpv4"]
[dependencies.fringe] [dependencies.fringe]
git = "https://git.m-labs.hk/M-Labs/libfringe.git" git = "https://git.m-labs.hk/M-Labs/libfringe.git"
rev = "3ecbe5" rev = "3ecbe5"

View File

@ -0,0 +1,59 @@
use board_misoc::clock;
use sched;
use sched::Dhcpv4Socket;
use smoltcp::socket::Dhcpv4Event;
use smoltcp::wire::{Ipv4Address, Ipv4Cidr};
pub fn dhcp_thread(io: sched::Io) {
let mut socket = Dhcpv4Socket::new(&io);
let mut last_ip: Option<Ipv4Cidr> = None;
let mut done_reset = false;
let start_time = clock::get_ms();
loop {
// A significant amount of the time our first discover isn't received
// by the server. This is likely to be because the ethernet device isn't quite
// ready at the point that it is sent. The following makes recovery from
// that faster.
if !done_reset && last_ip.is_none() && start_time + 1000 < clock::get_ms() {
info!("Didn't get initial IP in first second. Assuming packet loss, trying a reset.");
socket.reset();
done_reset = true;
}
if let Some(event) = socket.poll() {
match event {
Dhcpv4Event::Configured(config) => {
// Only compare the ip address in the config with previous config because we
// ignore the rest of the config i.e. we don't do any DNS or require a default
// gateway.
let changed = if let Some(last_ip) = last_ip {
if config.address != last_ip {
info!("IP address changed {} -> {}", last_ip, config.address);
true
} else {
false
}
} else {
info!("Acquired IP address: None -> {}", config.address);
true
};
if changed {
last_ip = Some(config.address);
io.set_ipv4_address(&config.address);
}
}
Dhcpv4Event::Deconfigured => {
if let Some(ip) = last_ip {
info!("Lost IP address {} -> None", ip);
io.set_ipv4_address(&Ipv4Cidr::new(Ipv4Address::UNSPECIFIED, 0));
last_ip = None;
}
// We always get one of these events at the start, ignore that one
}
}
}
// We want to poll after every poll of the interface. So we need to
// do a minimal yield here.
io.relinquish().unwrap();
}
}

View File

@ -27,11 +27,12 @@ extern crate riscv;
use core::cell::RefCell; use core::cell::RefCell;
use core::convert::TryFrom; use core::convert::TryFrom;
use smoltcp::wire::{IpCidr, HardwareAddress}; use smoltcp::wire::{HardwareAddress, IpAddress, IpCidr, Ipv4Address};
use board_misoc::{csr, ident, clock, spiflash, config, net_settings, pmp, boot}; use board_misoc::{csr, ident, clock, spiflash, config, net_settings, pmp, boot};
#[cfg(has_ethmac)] #[cfg(has_ethmac)]
use board_misoc::ethmac; use board_misoc::ethmac;
use board_misoc::net_settings::{NetAddresses, USE_DHCP};
#[cfg(has_drtio)] #[cfg(has_drtio)]
use board_artiq::drtioaux; use board_artiq::drtioaux;
use board_artiq::drtio_routing; use board_artiq::drtio_routing;
@ -58,6 +59,14 @@ mod session;
mod moninj; mod moninj;
#[cfg(has_rtio_analyzer)] #[cfg(has_rtio_analyzer)]
mod analyzer; mod analyzer;
mod dhcp;
// Fixed indexes for the IP addresses so that they can be modified with some
// semblance of confidence
pub const IPV4_INDEX: usize = 0;
pub const IPV6_LL_INDEX: usize = 1;
pub const IPV6_INDEX: usize = 2;
const IP_ADDRESS_STORAGE_SIZE: usize = 3;
#[cfg(has_grabber)] #[cfg(has_grabber)]
fn grabber_thread(io: sched::Io) { fn grabber_thread(io: sched::Io) {
@ -87,6 +96,18 @@ fn setup_log_levels() {
} }
} }
pub fn get_ip_addrs(net_addresses: &NetAddresses) -> [IpCidr; IP_ADDRESS_STORAGE_SIZE] {
let mut storage = [
IpCidr::new(IpAddress::Ipv4(Ipv4Address::UNSPECIFIED), 0); IP_ADDRESS_STORAGE_SIZE
];
storage[IPV4_INDEX] = IpCidr::new(net_addresses.ipv4_addr, 0);
storage[IPV6_LL_INDEX] = IpCidr::new(net_addresses.ipv6_ll_addr, 0);
if let Some(ipv6) = net_addresses.ipv6_addr {
storage[IPV6_INDEX] = IpCidr::new(ipv6, 0);
}
storage
}
fn startup() { fn startup() {
clock::init(); clock::init();
info!("ARTIQ runtime starting..."); info!("ARTIQ runtime starting...");
@ -141,31 +162,17 @@ fn startup() {
smoltcp::iface::NeighborCache::new(alloc::collections::btree_map::BTreeMap::new()); smoltcp::iface::NeighborCache::new(alloc::collections::btree_map::BTreeMap::new());
let net_addresses = net_settings::get_adresses(); let net_addresses = net_settings::get_adresses();
info!("network addresses: {}", net_addresses); info!("network addresses: {}", net_addresses);
let interface = match net_addresses.ipv6_addr { let use_dhcp = if net_addresses.ipv4_addr == USE_DHCP {
Some(addr) => { info!("Will try to acquire an IPv4 address with DHCP");
let ip_addrs = [ true
IpCidr::new(net_addresses.ipv4_addr, 0), } else {
IpCidr::new(net_addresses.ipv6_ll_addr, 0), false
IpCidr::new(addr, 0)
];
smoltcp::iface::InterfaceBuilder::new(net_device, vec![])
.hardware_addr(HardwareAddress::Ethernet(net_addresses.hardware_addr))
.ip_addrs(ip_addrs)
.neighbor_cache(neighbor_cache)
.finalize()
}
None => {
let ip_addrs = [
IpCidr::new(net_addresses.ipv4_addr, 0),
IpCidr::new(net_addresses.ipv6_ll_addr, 0)
];
smoltcp::iface::InterfaceBuilder::new(net_device, vec![])
.hardware_addr(HardwareAddress::Ethernet(net_addresses.hardware_addr))
.ip_addrs(ip_addrs)
.neighbor_cache(neighbor_cache)
.finalize()
}
}; };
let interface = smoltcp::iface::InterfaceBuilder::new(net_device, vec![])
.hardware_addr(HardwareAddress::Ethernet(net_addresses.hardware_addr))
.ip_addrs(get_ip_addrs(&net_addresses))
.neighbor_cache(neighbor_cache)
.finalize();
#[cfg(has_drtio)] #[cfg(has_drtio)]
let drtio_routing_table = urc::Urc::new(RefCell::new( let drtio_routing_table = urc::Urc::new(RefCell::new(
@ -182,6 +189,10 @@ fn startup() {
let mut scheduler = sched::Scheduler::new(interface); let mut scheduler = sched::Scheduler::new(interface);
let io = scheduler.io(); let io = scheduler.io();
if use_dhcp {
io.spawn(4096, dhcp::dhcp_thread);
}
rtio_mgt::startup(&io, &aux_mutex, &drtio_routing_table, &up_destinations); rtio_mgt::startup(&io, &aux_mutex, &drtio_routing_table, &up_destinations);
io.spawn(4096, mgmt::thread); io.spawn(4096, mgmt::thread);

View File

@ -2,13 +2,13 @@
use core::mem; use core::mem;
use core::result; use core::result;
use core::cell::{Cell, RefCell}; use core::cell::{Cell, RefCell, RefMut};
use alloc::vec::Vec; use alloc::vec::Vec;
use fringe::OwnedStack; use fringe::OwnedStack;
use fringe::generator::{Generator, Yielder, State as GeneratorState}; use fringe::generator::{Generator, Yielder, State as GeneratorState};
use smoltcp::time::Duration; use smoltcp::time::Duration;
use smoltcp::Error as NetworkError; use smoltcp::Error as NetworkError;
use smoltcp::wire::IpEndpoint; use smoltcp::wire::{IpEndpoint, Ipv4Cidr, IpCidr};
use smoltcp::iface::{Interface, SocketHandle}; use smoltcp::iface::{Interface, SocketHandle};
use io::{Read, Write}; use io::{Read, Write};
@ -16,6 +16,7 @@ use board_misoc::clock;
use urc::Urc; use urc::Urc;
use board_misoc::ethmac::EthernetDevice; use board_misoc::ethmac::EthernetDevice;
use smoltcp::phy::Tracer; use smoltcp::phy::Tracer;
use IPV4_INDEX;
#[derive(Fail, Debug)] #[derive(Fail, Debug)]
pub enum Error { pub enum Error {
@ -273,6 +274,12 @@ impl<'a> Io<'a> {
pub fn join(&self, handle: ThreadHandle) -> Result<(), Error> { pub fn join(&self, handle: ThreadHandle) -> Result<(), Error> {
self.until(move || handle.terminated()) self.until(move || handle.terminated())
} }
pub fn set_ipv4_address(&self, addr: &Ipv4Cidr) {
self.network.borrow_mut().update_ip_addrs(|addrs| {
addrs.as_mut()[IPV4_INDEX] = IpCidr::Ipv4(*addr);
})
}
} }
#[derive(Clone)] #[derive(Clone)]
@ -563,3 +570,45 @@ impl<'a> Drop for TcpStream<'a> {
self.io.network.borrow_mut().remove_socket(self.handle); self.io.network.borrow_mut().remove_socket(self.handle);
} }
} }
pub struct Dhcpv4Socket<'a> {
io: &'a Io<'a>,
handle: SocketHandle,
}
impl<'a> Dhcpv4Socket<'a> {
fn new_lower(io: &'a Io<'a>) -> SocketHandle {
let socket = smoltcp::socket::Dhcpv4Socket::new();
io.network.borrow_mut().add_socket(socket)
}
pub fn new(io: &'a Io<'a>) -> Self {
Self {
io,
handle: Self::new_lower(io)
}
}
fn lower(&mut self) -> RefMut<smoltcp::socket::Dhcpv4Socket> {
RefMut::map(
self.io.network.borrow_mut(),
|network| network.get_socket::<smoltcp::socket::Dhcpv4Socket>(self.handle),
)
}
pub fn poll(&mut self) -> Option<smoltcp::socket::Dhcpv4Event> {
self.lower().poll()
}
pub fn reset(&mut self) {
self.lower().reset()
}
}
impl<'a> Drop for Dhcpv4Socket<'a> {
fn drop(&mut self) {
let mut network = self.io.network.borrow_mut();
network.remove_socket(self.handle);
}
}

View File

@ -268,6 +268,12 @@ In other cases, install OpenOCD as before, and flash the IP (and, if necessary,
For Kasli devices, flashing a MAC address is not necessary as they can obtain it from their EEPROM. For Kasli devices, flashing a MAC address is not necessary as they can obtain it from their EEPROM.
Alternatively you can set the "ip" config field to "use_dhcp" to have the device use DHCP to obtain an IP address on
boot. e.g. ::
$ artiq_mkfs flash_storage.img -s ip use_dhcp
$ artiq_flash -t [board] -V [variant] -f flash_storage.img storage start
Check that you can ping the device. If ping fails, check that the Ethernet link LED is ON - on Kasli, it is the LED next to the SFP0 connector. As a next step, look at the messages emitted on the UART during boot. Use a program such as flterm or PuTTY to connect to the device's serial port at 115200bps 8-N-1 and reboot the device. On Kasli, the serial port is on FTDI channel 2 with v1.1 hardware (with channel 0 being JTAG) and on FTDI channel 1 with v1.0 hardware. Check that you can ping the device. If ping fails, check that the Ethernet link LED is ON - on Kasli, it is the LED next to the SFP0 connector. As a next step, look at the messages emitted on the UART during boot. Use a program such as flterm or PuTTY to connect to the device's serial port at 115200bps 8-N-1 and reboot the device. On Kasli, the serial port is on FTDI channel 2 with v1.1 hardware (with channel 0 being JTAG) and on FTDI channel 1 with v1.0 hardware.
If you want to use IPv6, the device also has a link-local address that corresponds to its EUI-64, and an additional arbitrary IPv6 address can be defined by using the ``ip6`` configuration key. All IPv4 and IPv6 addresses can be used at the same time. If you want to use IPv6, the device also has a link-local address that corresponds to its EUI-64, and an additional arbitrary IPv6 address can be defined by using the ``ip6`` configuration key. All IPv4 and IPv6 addresses can be used at the same time.