diff --git a/RELEASE_NOTES.rst b/RELEASE_NOTES.rst index a14fafd11..d592a2743 100644 --- a/RELEASE_NOTES.rst +++ b/RELEASE_NOTES.rst @@ -42,6 +42,7 @@ Highlights: * ``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 switch is supported. +* The "ip" config option can now be set to "use_dhcp" to use DHCP to obtain an IP address. Breaking changes: diff --git a/artiq/firmware/libboard_misoc/net_settings.rs b/artiq/firmware/libboard_misoc/net_settings.rs index 2663be5db..754f326fa 100644 --- a/artiq/firmware/libboard_misoc/net_settings.rs +++ b/artiq/firmware/libboard_misoc/net_settings.rs @@ -1,12 +1,15 @@ use core::fmt; -use smoltcp::wire::{EthernetAddress, IpAddress}; +use smoltcp::wire::{EthernetAddress, IpAddress, Ipv4Address}; use config; #[cfg(soc_platform = "kasli")] use i2c_eeprom; +pub const USE_DHCP: IpAddress = IpAddress::Ipv4(Ipv4Address::UNSPECIFIED); + + pub struct NetAddresses { pub hardware_addr: EthernetAddress, pub ipv4_addr: IpAddress, @@ -49,7 +52,13 @@ pub fn get_adresses() -> NetAddresses { } 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, _ => { #[cfg(soc_platform = "kasli")] diff --git a/artiq/firmware/runtime/Cargo.toml b/artiq/firmware/runtime/Cargo.toml index 241246821..85d05768c 100644 --- a/artiq/firmware/runtime/Cargo.toml +++ b/artiq/firmware/runtime/Cargo.toml @@ -27,9 +27,13 @@ board_misoc = { path = "../libboard_misoc", features = ["uart_console", "smoltcp logger_artiq = { path = "../liblogger_artiq" } board_artiq = { path = "../libboard_artiq" } 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"] } +[dependencies.smoltcp] +version = "0.8.0" +default-features = false +features = ["alloc", "medium-ethernet", "proto-ipv4", "proto-ipv6", "socket-tcp", "socket-dhcpv4"] + [dependencies.fringe] git = "https://git.m-labs.hk/M-Labs/libfringe.git" rev = "3ecbe5" diff --git a/artiq/firmware/runtime/dhcp.rs b/artiq/firmware/runtime/dhcp.rs new file mode 100644 index 000000000..0e7d3e438 --- /dev/null +++ b/artiq/firmware/runtime/dhcp.rs @@ -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 = 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(); + } +} diff --git a/artiq/firmware/runtime/main.rs b/artiq/firmware/runtime/main.rs index f8843352d..ab731a6b9 100644 --- a/artiq/firmware/runtime/main.rs +++ b/artiq/firmware/runtime/main.rs @@ -27,11 +27,12 @@ extern crate riscv; use core::cell::RefCell; 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}; #[cfg(has_ethmac)] use board_misoc::ethmac; +use board_misoc::net_settings::{NetAddresses, USE_DHCP}; #[cfg(has_drtio)] use board_artiq::drtioaux; use board_artiq::drtio_routing; @@ -58,6 +59,14 @@ mod session; mod moninj; #[cfg(has_rtio_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)] 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() { clock::init(); info!("ARTIQ runtime starting..."); @@ -141,31 +162,17 @@ fn startup() { smoltcp::iface::NeighborCache::new(alloc::collections::btree_map::BTreeMap::new()); let net_addresses = net_settings::get_adresses(); info!("network addresses: {}", net_addresses); - let interface = match net_addresses.ipv6_addr { - Some(addr) => { - let ip_addrs = [ - IpCidr::new(net_addresses.ipv4_addr, 0), - IpCidr::new(net_addresses.ipv6_ll_addr, 0), - 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 use_dhcp = if net_addresses.ipv4_addr == USE_DHCP { + info!("Will try to acquire an IPv4 address with DHCP"); + true + } else { + false }; + 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)] let drtio_routing_table = urc::Urc::new(RefCell::new( @@ -182,6 +189,10 @@ fn startup() { let mut scheduler = sched::Scheduler::new(interface); let io = scheduler.io(); + if use_dhcp { + io.spawn(4096, dhcp::dhcp_thread); + } + rtio_mgt::startup(&io, &aux_mutex, &drtio_routing_table, &up_destinations); io.spawn(4096, mgmt::thread); diff --git a/artiq/firmware/runtime/sched.rs b/artiq/firmware/runtime/sched.rs index 991569cd1..a5903c2d1 100644 --- a/artiq/firmware/runtime/sched.rs +++ b/artiq/firmware/runtime/sched.rs @@ -2,13 +2,13 @@ use core::mem; use core::result; -use core::cell::{Cell, RefCell}; +use core::cell::{Cell, RefCell, RefMut}; use alloc::vec::Vec; use fringe::OwnedStack; use fringe::generator::{Generator, Yielder, State as GeneratorState}; use smoltcp::time::Duration; use smoltcp::Error as NetworkError; -use smoltcp::wire::IpEndpoint; +use smoltcp::wire::{IpEndpoint, Ipv4Cidr, IpCidr}; use smoltcp::iface::{Interface, SocketHandle}; use io::{Read, Write}; @@ -16,6 +16,7 @@ use board_misoc::clock; use urc::Urc; use board_misoc::ethmac::EthernetDevice; use smoltcp::phy::Tracer; +use IPV4_INDEX; #[derive(Fail, Debug)] pub enum Error { @@ -273,6 +274,12 @@ impl<'a> Io<'a> { pub fn join(&self, handle: ThreadHandle) -> Result<(), Error> { 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)] @@ -563,3 +570,45 @@ impl<'a> Drop for TcpStream<'a> { 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 { + RefMut::map( + self.io.network.borrow_mut(), + |network| network.get_socket::(self.handle), + ) + } + + pub fn poll(&mut self) -> Option { + 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); + } +} diff --git a/doc/manual/installing.rst b/doc/manual/installing.rst index 3bc38cd41..6ee48c75c 100644 --- a/doc/manual/installing.rst +++ b/doc/manual/installing.rst @@ -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. +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. 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.