diff --git a/examples/PPSIn/PPSIn.ino b/examples/PPSIn/PPSIn.ino new file mode 100644 index 000000000..fcab65763 --- /dev/null +++ b/examples/PPSIn/PPSIn.ino @@ -0,0 +1,64 @@ +// SPDX-FileCopyrightText: (c) 2023 Jens Schleusner +// SPDX-License-Identifier: MIT + +// PPSOut generates a Pulse per Second using the IEEE1588-Timer on Pin 24 +// We read the pulse back on channels 0 and 2 to measure the timer delay +// Connect Oscilloscope to Pin 24 +// Connect Pins 24(out), 15(in), 35(in) +// +// This file is part of the QNEthernet library. + +#include +using namespace qindesign::network; + +void setup() { + + Serial.begin(2000000); + Serial.println("Setup EthernetIEEE1588"); + + qindesign::network::Ethernet.begin(); + qindesign::network::EthernetIEEE1588.begin(); + + //PPS-Out on Channel 1 + IOMUXC_SW_MUX_CTL_PAD_GPIO_AD_B0_12 = 6; //ENET_1588_EVENT1_OUT, IOMUX: ALT6, Teensy Pin: 24 + EthernetIEEE1588.setChannelCompareValue(1, 0); //Compare at counter value 0 + EthernetIEEE1588.setChannelMode(1, qindesign::network::EthernetIEEE1588.TimerChannelModes::kPulseHighOnCompare); //enable Channel0 positive pulse + EthernetIEEE1588.setChannelOutputPulseWidth(1, 25); //Generate a Pulse width of 25 25MHz clock cycles (1us) + + //PPS-IN + attachInterruptVector(IRQ_ENET_TIMER, interrupt_1588_timer); //Configure Interrupt Handler + NVIC_ENABLE_IRQ(IRQ_ENET_TIMER); //Enable Interrupt Handling + + //PPS-In on Channel 2 + IOMUXC_SW_MUX_CTL_PAD_GPIO_AD_B1_03 = 4; //ENET_1588_EVENT2_IN, IOMUX: ALT4, Teensy Pin: 15 + EthernetIEEE1588.setChannelMode(2, qindesign::network::EthernetIEEE1588.TimerChannelModes::kCaptureOnRising); //enable Channel2 rising edge trigger + EthernetIEEE1588.setChannelInterruptEnable(2, true); //Configure Interrupt generation + + //PPS-In on Channel 0 + IOMUXC_SW_MUX_CTL_PAD_GPIO_B1_12 = 3; //ENET_1588_EVENT0_IN, IOMUX: ALT3, Teensy Pin: 35 + IOMUXC_ENET0_TIMER_SELECT_INPUT = 0b10; // route B1_12 to 1588 timer (pg 796, Rev. 3) + EthernetIEEE1588.setChannelMode(0, qindesign::network::EthernetIEEE1588.TimerChannelModes::kCaptureOnFalling); + EthernetIEEE1588.setChannelInterruptEnable(0, true); + + + Serial.printf("TCSR Register state: ENET_TCSR0 %08" PRIX32 "h ENET_TCSR1 %08" PRIX32 "h ENET_TCSR2 %08" PRIX32 "h\n", ENET_TCSR0, ENET_TCSR1, ENET_TCSR2); // (pg 2247, Rev. 3) +} + +void loop() { + // put your main code here, to run repeatedly: + +} + +static void interrupt_1588_timer() { + uint32_t t; + if(EthernetIEEE1588.getAndClearChannelStatus(0)){ + EthernetIEEE1588.getChannelCompareValue(0,t); + Serial.printf("Timer0 Falling Edge: %d\n\n", t); + } + if (EthernetIEEE1588.getAndClearChannelStatus(2)) { + EthernetIEEE1588.getChannelCompareValue(2,t); + Serial.printf("Timer2 Rising Edge: %d\n", t); + } + asm("dsb"); // allow write to complete so the interrupt doesn't fire twice +} + diff --git a/examples/PPSOut/PPSOut.ino b/examples/PPSOut/PPSOut.ino new file mode 100644 index 000000000..d114201ac --- /dev/null +++ b/examples/PPSOut/PPSOut.ino @@ -0,0 +1,41 @@ +// SPDX-FileCopyrightText: (c) 2023 Jens Schleusner +// SPDX-License-Identifier: MIT + +// PPSOut generates a Pulse per Second using the IEEE1588-Timer +// Connect Oscilloscope to Pins 14,24,34 +// Connect LED to Pin 14 +// +// This file is part of the QNEthernet library. + +#include +using namespace qindesign::network; + +void setup() { + + Serial.begin(2000000); + Serial.println("Setup EthernetIEEE1588"); + + qindesign::network::Ethernet.begin(); + qindesign::network::EthernetIEEE1588.begin(); + + IOMUXC_SW_MUX_CTL_PAD_GPIO_B1_13 = 3; //ENET_1588_EVENT0_OUT, IOMUX: ALT3, Teensy Pin: 34 + EthernetIEEE1588.setChannelCompareValue(0, 0); //Compare at counter value 0 + EthernetIEEE1588.setChannelMode(0, qindesign::network::EthernetIEEE1588.TimerChannelModes::kPulseHighOnCompare); //enable Channel0 positive pulse + EthernetIEEE1588.setChannelOutputPulseWidth(0, 25); //Generate a Pulse width of 25 25MHz clock cycles (1us) + + IOMUXC_SW_MUX_CTL_PAD_GPIO_AD_B0_12 = 6; //ENET_1588_EVENT1_OUT, IOMUX: ALT6, Teensy Pin: 24 + EthernetIEEE1588.setChannelCompareValue(1, 1000); //Compare at counter value 1000 + EthernetIEEE1588.setChannelOutputPulseWidth(1, 10); //Generate a Pulse width of 10 25MHz clock cycles (400ns) + EthernetIEEE1588.setChannelMode(1, qindesign::network::EthernetIEEE1588.TimerChannelModes::kPulseLowOnCompare); //enable Channel1 negative pulse + + IOMUXC_SW_MUX_CTL_PAD_GPIO_AD_B1_02 = 4; //ENET_1588_EVENT2_OUT, IOMUX: ALT4, Teensy Pin: 14 + EthernetIEEE1588.setChannelCompareValue(2, 500 * 1000 * 1000); //Compare at counter 500ms + EthernetIEEE1588.setChannelMode(2, qindesign::network::EthernetIEEE1588.TimerChannelModes::kClearOnCompareSetOnOverflow); //enable Channel2 for 50/50 On-Off Signal + + Serial.printf("TCSR Register state: ENET_TCSR0 %08" PRIX32 "h ENET_TCSR1 %08" PRIX32 "h ENET_TCSR2 %08" PRIX32 "h\n", ENET_TCSR0, ENET_TCSR1, ENET_TCSR2); // (pg 2247, Rev. 3) +} + +void loop() { + // put your main code here, to run repeatedly: + +} diff --git a/examples/SNTPClientWithTimestamps/SNTPClientWithTimestamps.ino b/examples/SNTPClientWithTimestamps/SNTPClientWithTimestamps.ino new file mode 100644 index 000000000..f698ad55c --- /dev/null +++ b/examples/SNTPClientWithTimestamps/SNTPClientWithTimestamps.ino @@ -0,0 +1,168 @@ +// SPDX-FileCopyrightText: (c) 2021-2022 Shawn Silverman +// SPDX-License-Identifier: MIT + +// SNTPClientWithTimestamps demonstrates, using a simple SNTP client, +// how to send and receive timestamped packets. Think of this example +// as a vehicle to show these concepts and not as an SNTP client. +// +// This file is part of the QNEthernet library. + +// C++ includes +#include + +#include +#include + +using namespace qindesign::network; + +// -------------------------------------------------------------------------- +// Configuration +// -------------------------------------------------------------------------- + +constexpr uint32_t kDHCPTimeout = 10000; // 10 seconds +constexpr uint16_t kNTPPort = 123; + +// 01-Jan-1900 00:00:00 -> 01-Jan-1970 00:00:00 +constexpr uint32_t kEpochDiff = 2'208'988'800; + +// Epoch -> 07-Feb-2036 06:28:16 +constexpr uint32_t kBreakTime = 2'085'978'496; + +// -------------------------------------------------------------------------- +// Program state +// -------------------------------------------------------------------------- + +// UDP port. +EthernetUDP udp; + +// Buffer. +uint8_t buf[48]; + +// -------------------------------------------------------------------------- +// Main program +// -------------------------------------------------------------------------- + +// Program setup. +void setup() { + Serial.begin(115200); + while (!Serial && millis() < 4000) { + // Wait for Serial to initialize + } + stdPrint = &Serial; // Make printf work (a QNEthernet feature) + printf("Starting...\n"); + + uint8_t mac[6]; + Ethernet.macAddress(mac); + printf("MAC = %02x:%02x:%02x:%02x:%02x:%02x\n", + mac[0], mac[1], mac[2], mac[3], mac[4], mac[5]); + + printf("Starting Ethernet with DHCP...\n"); + if (!Ethernet.begin()) { + printf("Failed to start Ethernet\n"); + return; + } + if (!Ethernet.waitForLocalIP(kDHCPTimeout)) { + printf("Failed to get IP address from DHCP\n"); + return; + } + + IPAddress ip = Ethernet.localIP(); + printf(" Local IP = %u.%u.%u.%u\n", ip[0], ip[1], ip[2], ip[3]); + ip = Ethernet.subnetMask(); + printf(" Subnet mask = %u.%u.%u.%u\n", ip[0], ip[1], ip[2], ip[3]); + ip = Ethernet.gatewayIP(); + printf(" Gateway = %u.%u.%u.%u\n", ip[0], ip[1], ip[2], ip[3]); + ip = Ethernet.dnsServerIP(); + printf(" DNS = %u.%u.%u.%u\n", ip[0], ip[1], ip[2], ip[3]); + + EthernetIEEE1588.begin(); + + // Start UDP listening on the NTP port + udp.begin(kNTPPort); + + // Send an SNTP request + + memset(buf, 0, 48); + buf[0] = 0b00'100'011; // LI=0, VN=4, Mode=3 (Client) + + // Set the Transmit Timestamp + uint32_t t = Teensy3Clock.get(); + if (t >= kBreakTime) { + t -= kBreakTime; + } else { + t += kEpochDiff; + } + buf[40] = t >> 24; + buf[41] = t >> 16; + buf[42] = t >> 8; + buf[43] = t; + + // Send the packet with a timestamp + printf("Sending SNTP request to the gateway..."); + EthernetIEEE1588.timestampNextFrame(); + if (!udp.send(Ethernet.gatewayIP(), kNTPPort, buf, 48)) { + printf("ERROR."); + } + printf("\n"); + + // Get the timestamp of the transmitted packet + timespec ts; + while (!EthernetIEEE1588.readAndClearTxTimestamp(ts)) { + // Wait for the timestamp + } + printf("TX timestamp: %ld.%09ld s\n", ts.tv_sec, ts.tv_nsec); +} + +// Main program loop. +void loop() { + int size = udp.parsePacket(); + if (size != 48 && size != 68) { + return; + } + + // Get the timestamp of the received packet + timespec ts; + if (udp.timestamp(ts)) { + printf("RX timestamp: %ld.%09ld s\n", ts.tv_sec, ts.tv_nsec); + } + + if (udp.read(buf, 48) != 48) { + printf("Not enough bytes\n"); + return; + } + + // See: Section 5, "SNTP Client Operations" + int mode = buf[0] & 0x07; + if (((buf[0] & 0xc0) == 0xc0) || // LI == 3 (Alarm condition) + (buf[1] == 0) || // Stratum == 0 (Kiss-o'-Death) + !(mode == 4 || mode == 5)) { // Must be Server or Broadcast mode + printf("Discarding reply\n"); + return; + } + + uint32_t t = (uint32_t{buf[40]} << 24) | + (uint32_t{buf[41]} << 16) | + (uint32_t{buf[42]} << 8) | + uint32_t{buf[43]}; + if (t == 0) { + printf("Discarding reply\n"); + return; // Also discard when the Transmit Timestamp is zero + } + if ((t & 0x80000000U) == 0) { + // See: Section 3, "NTP Timestamp Format" + t += kBreakTime; + } else { + t -= kEpochDiff; + } + + // Set the RTC and time + Teensy3Clock.set(t); + setTime(t); + + // Print the time + tmElements_t tm; + breakTime(t, tm); + printf("SNTP reply: %04u-%02u-%02u %02u:%02u:%02u\n", + tm.Year + 1970, tm.Month, tm.Day, + tm.Hour, tm.Minute, tm.Second); +} diff --git a/examples/TimerAdjustment/TimerAdjustment.ino b/examples/TimerAdjustment/TimerAdjustment.ino new file mode 100644 index 000000000..4a189f721 --- /dev/null +++ b/examples/TimerAdjustment/TimerAdjustment.ino @@ -0,0 +1,46 @@ +// SPDX-FileCopyrightText: (c) 2023 Jens Schleusner +// SPDX-License-Identifier: MIT +// +// This file is part of the QNEthernet library. + +#include +using namespace qindesign::network; + +IntervalTimer myTimer; + +int adjustment = 0; +timespec tmlast; + +void setup() { + + Serial.begin(2000000); + Serial.println("Setup EthernetIEEE1588"); + + qindesign::network::Ethernet.begin(); + qindesign::network::EthernetIEEE1588.begin(); + + myTimer.begin(timerInterrupt, 1000000); + EthernetIEEE1588.readTimer(tmlast); + +} + +void timerInterrupt() { + timespec tm; + EthernetIEEE1588.readTimer(tm);//read current timer value + int lastadjustment=adjustment; + adjustment += 100; //increase timer spped by 100ns per second + EthernetIEEE1588.adjustFreq(adjustment); + + int diff = (static_cast(tm.tv_sec) - static_cast(tmlast.tv_sec)) * (1000 * 1000 * 1000) + (static_cast(tm.tv_nsec) - static_cast(tmlast.tv_nsec)); + diff -= (1000 * 1000 * 1000); + + Serial.printf("Adjustment:%d nsps ATINC %08" PRIX32 "h ATCOR %d Diff %d\n",lastadjustment,ENET_ATINC,ENET_ATCOR,diff); + //Serial.printf("%d %d\n", lastadjustment, diff); + + tmlast = tm; + +} + +void loop() { + +} diff --git a/src/QNEthernet.h b/src/QNEthernet.h index cf46cfe77..5f0352f40 100644 --- a/src/QNEthernet.h +++ b/src/QNEthernet.h @@ -20,6 +20,7 @@ #include "lwip/prot/ethernet.h" #include "qnethernet/QNEthernetClient.h" #include "qnethernet/QNEthernetFrame.h" +#include "qnethernet/QNEthernetIEEE1588.h" #include "qnethernet/QNEthernetServer.h" #include "qnethernet/QNEthernetUDP.h" #include "qnethernet/QNMDNS.h" @@ -507,6 +508,9 @@ class EthernetClass final { // Instance for interacting with the library. STATIC_INIT_DECL(EthernetClass, Ethernet); +// Instance for using IEEE 1588 functions. +extern EthernetIEEE1588Class &EthernetIEEE1588; + #if QNETHERNET_CUSTOM_WRITE // stdout output. extern Print *stdoutPrint; diff --git a/src/lwipopts.h b/src/lwipopts.h index f8f9858c4..85f23e102 100644 --- a/src/lwipopts.h +++ b/src/lwipopts.h @@ -271,8 +271,13 @@ void qnethernet_hal_check_core_locking(const char *file, int line, // #define PBUF_LINK_ENCAPSULATION_HLEN 0 // #define PBUF_POOL_BUFSIZE LWIP_MEM_ALIGN_SIZE(TCP_MSS+PBUF_IP_HLEN+PBUF_TRANSPORT_HLEN+PBUF_LINK_ENCAPSULATION_HLEN+PBUF_LINK_HLEN) // #define LWIP_PBUF_REF_T u8_t -// #define LWIP_PBUF_CUSTOM_DATA -// #define LWIP_PBUF_CUSTOM_DATA_INIT(p) +#define LWIP_PBUF_CUSTOM_DATA \ + u8_t timestampValid; \ + struct timespec timestamp; +#define LWIP_PBUF_CUSTOM_DATA_INIT(p) \ + (p)->timestampValid = 0; \ + (p)->timestamp.tv_sec = 0; \ + (p)->timestamp.tv_nsec = 0; // Network Interfaces options #define LWIP_SINGLE_NETIF 1 /* 0 */ diff --git a/src/qnethernet/QNEthernetFrame.cpp b/src/qnethernet/QNEthernetFrame.cpp index ab4977220..e2db60a0d 100644 --- a/src/qnethernet/QNEthernetFrame.cpp +++ b/src/qnethernet/QNEthernetFrame.cpp @@ -63,6 +63,11 @@ err_t EthernetFrameClass::recvFunc(struct pbuf *const p, } frame.receivedTimestamp = timestamp; + frame.hasTimestamp = p->timestampValid; + if (frame.hasTimestamp) { + frame.timestamp = p->timestamp; + } + // Increment the size if (EthernetFrame.inBufSize_ != 0 && EthernetFrame.inBufTail_ == EthernetFrame.inBufHead_) { @@ -125,6 +130,10 @@ int EthernetFrameClass::parseFrame() { // Pop (from the tail) frame_ = inBuf_[inBufTail_]; + inBuf_[inBufTail_].data.clear(); + inBuf_[inBufTail_].hasTimestamp = false; + inBuf_[inBufTail_].timestamp.tv_sec = 0; + inBuf_[inBufTail_].timestamp.tv_nsec = 0; inBuf_[inBufTail_].clear(); inBufTail_ = (inBufTail_ + 1) % inBuf_.size(); inBufSize_--; @@ -235,6 +244,15 @@ void EthernetFrameClass::setReceiveQueueCapacity(const size_t capacity) { inBuf_.shrink_to_fit(); } +bool EthernetFrameClass::timestamp(timespec ×tamp) const { + // NOTE: This is not "concurrent safe" + if (frame_.hasTimestamp) { + timestamp = frame_.timestamp; + return true; + } + return false; +} + // -------------------------------------------------------------------------- // Transmission // -------------------------------------------------------------------------- diff --git a/src/qnethernet/QNEthernetFrame.h b/src/qnethernet/QNEthernetFrame.h index cb662b10e..71bcb9493 100644 --- a/src/qnethernet/QNEthernetFrame.h +++ b/src/qnethernet/QNEthernetFrame.h @@ -13,6 +13,7 @@ // C++ includes #include #include +#include #include #include @@ -191,6 +192,11 @@ class EthernetFrameClass final : public Stream, public internal::PrintfChecked { // with the receive function if called from an ISR. void setReceiveQueueCapacity(size_t capacity); + // Gets the IEEE 1588 timestamp for the received frame and assigns it to the + // `timestamp` parameter, if available. This returns whether the received + // frame has a timestamp. + bool timestamp(timespec ×tamp) const; + // Returns the receive queue capacity. size_t receiveQueueCapacity() const { return inBuf_.size(); @@ -221,6 +227,8 @@ class EthernetFrameClass final : public Stream, public internal::PrintfChecked { private: struct Frame final { std::vector data; + volatile bool hasTimestamp = false; + timespec timestamp{0, 0}; volatile uint32_t receivedTimestamp = 0; // Approximate arrival time // Clears all the data. diff --git a/src/qnethernet/QNEthernetIEEE1588.cpp b/src/qnethernet/QNEthernetIEEE1588.cpp new file mode 100644 index 000000000..6f71c0014 --- /dev/null +++ b/src/qnethernet/QNEthernetIEEE1588.cpp @@ -0,0 +1,91 @@ +// SPDX-FileCopyrightText: (c) 2022 Shawn Silverman +// SPDX-License-Identifier: MIT + +// QNEthernetIEEE1588.cpp contains the EthernetIEEE1588 implementation. +// This file is part of the QNEthernet library. + +#include "QNEthernetIEEE1588.h" +#include "lwip_driver.h" + +namespace qindesign { +namespace network { + +// Define the singleton instance. +EthernetIEEE1588Class EthernetIEEE1588Class::instance_; + +// A reference to the singleton. +EthernetIEEE1588Class &EthernetIEEE1588 = EthernetIEEE1588Class::instance(); + +void EthernetIEEE1588Class::begin() const { + enet_ieee1588_init(); +} + +void EthernetIEEE1588Class::end() const { + enet_ieee1588_deinit(); +} + +bool EthernetIEEE1588Class::readTimer(timespec &t) const { + return enet_ieee1588_read_timer(&t); +} + +bool EthernetIEEE1588Class::writeTimer(const timespec &t) const { + return enet_ieee1588_write_timer(&t); +} + +bool EthernetIEEE1588Class::offsetTimer(int64_t ns) const{ + return enet_ieee1588_offset_timer(ns); +} + +void EthernetIEEE1588Class::timestampNextFrame() const { + enet_ieee1588_timestamp_next_frame(); +} + +bool EthernetIEEE1588Class::readAndClearTxTimestamp( + struct timespec ×tamp) const { + return enet_ieee1588_read_and_clear_tx_timestamp(×tamp); +} + +bool EthernetIEEE1588Class::adjustTimer(uint32_t corrInc, + uint32_t corrPeriod) const { + return enet_ieee1588_adjust_timer(corrInc, corrPeriod); +} + +bool EthernetIEEE1588Class::adjustFreq(double nsps) const { + return enet_ieee1588_adjust_freq(nsps); +} + +bool EthernetIEEE1588Class::setChannelMode(int channel, + TimerChannelModes mode) const { + return enet_ieee1588_set_channel_mode(channel, static_cast(mode)); +} + +bool EthernetIEEE1588Class::setChannelOutputPulseWidth(int channel, + int pulseWidth) const { + return enet_ieee1588_set_channel_output_pulse_width(channel, + pulseWidth); +} + +bool EthernetIEEE1588Class::setChannelCompareValue(int channel, + uint32_t value) const { + return enet_ieee1588_set_channel_compare_value(channel, value); +} + +bool EthernetIEEE1588Class::getChannelCompareValue(int channel, + uint32_t &value) const { + return enet_ieee1588_get_channel_compare_value(channel, &value); +} + +bool EthernetIEEE1588Class::getAndClearChannelStatus(int channel) const { + return enet_ieee1588_get_and_clear_channel_status(channel); +} + +bool EthernetIEEE1588Class::setChannelInterruptEnable(int channel, bool enable) const { + return enet_ieee1588_set_channel_interrupt_enable(channel, enable); +} + +EthernetIEEE1588Class::operator bool() const { + return enet_ieee1588_is_enabled(); +} + +} // namespace network +} // namespace qindesign diff --git a/src/qnethernet/QNEthernetIEEE1588.h b/src/qnethernet/QNEthernetIEEE1588.h new file mode 100644 index 000000000..f7bd66fed --- /dev/null +++ b/src/qnethernet/QNEthernetIEEE1588.h @@ -0,0 +1,123 @@ +// SPDX-FileCopyrightText: (c) 2022 Shawn Silverman +// SPDX-License-Identifier: MIT + +// QNEthernetIEEE1588.h defines an interface to IEEE 1588 functions. +// This file is part of the QNEthernet library. + +#ifndef QNE_ETHERNETIEEE1588_H_ +#define QNE_ETHERNETIEEE1588_H_ + +// C++ includes +#include +#include + +namespace qindesign { +namespace network { + +// Provides an API for raw Ethernet frames, similar to the UDP API. +class EthernetIEEE1588Class final { + public: + enum class TimerChannelModes { + kDisable = 0, + kCaptureOnRising = 1, + kCaptureOnFalling = 2, + kCaptureOnBoth = 3, + kSoftwareCompare = 4, + kToggleOnCompare = 5, + kClearOnCompare = 6, + kSetOnCompare = 7, + kClearOnCompareSetOnOverflow = 10, + kSetOnCompareClearOnOverflow = 11, + kPulseLowOnCompare = 14, + kPulseHighOnCompare = 15, + }; + + // Accesses the singleton instance. + static EthernetIEEE1588Class &instance() { + return instance_; + } + + // EthernetIEEE1588Class is neither copyable nor movable. + EthernetIEEE1588Class(const EthernetIEEE1588Class &) = delete; + EthernetIEEE1588Class &operator=(const EthernetIEEE1588Class &) = delete; + + // Starts the IEEE 1588 timer. + void begin() const; + + // Stops the IEEE 1588 timer. + void end() const; + + // Reads the current IEEE 1588 timer value. This returns whether successful. + bool readTimer(timespec &t) const; + + // Writes the current IEEE 1588 timer value. This returns whether successful. + bool writeTimer(const timespec &t) const; + + // Adds an offset to the current IEEE 1588 timer value. This returns whether successful. + bool offsetTimer(int64_t ns) const; + + // Tells the driver to timestamp the next transmitted frame. This should be + // called before functions like `EthernetUDP::endPacket()`, + // `EthernetUDP::send()`, and any of the `EthernetFrame` send functions. + void timestampNextFrame() const; + + // Attempts to retrieve the timestamp of the last transmitted frame and + // returns whether one is available. If available and the parameter is not + // NULL then the timestamp is assigned to `*timestamp`. This clears the + // timestamp state so that a subsequent call will return false. + // + // This will always returns false if `EthernetIEEE1588.timestampNextFrame()` + // was not called before this. + bool readAndClearTxTimestamp(timespec ×tamp) const; + + // Adjusts the raw correction settings. The increment must be in the range + // 0-127 and the period must be in the range 0-(2^31-1), zero meaning + // no correction. This will return whether successful. + bool adjustTimer(uint32_t corrInc, uint32_t corrPeriod) const; + + // Adjusts the correction frequency in nanoseconds per second. To slow down + // the timer, specify a negative value. To speed it up, specify a positive + // value. This returns whether successful. + bool adjustFreq(double nsps) const; + + // Sets the channel mode for the given channel. This does not set the output + // compare pulse modes. This returns whether successful. + // + // This will return false for an unknown channel or if the mode is one of the + // output compare pulse modes. + bool setChannelMode(int channel, TimerChannelModes mode) const; + + // Sets the output compare pulse width for the given channel. + // The pulse width must be in the range 1-32. This returns whether successful. + bool setChannelOutputPulseWidth(int channel, + int pulseWidth) const; + + // Sets the channel compare value. This returns whether successful. + bool setChannelCompareValue(int channel, uint32_t value) const; + + // Gets the channel compare value. This returns whether successful. + bool getChannelCompareValue(int channel, uint32_t &value) const; + + // Retrieves and then clears the status for the given channel. This will + // return false for an unknown channel. + bool getAndClearChannelStatus(int channel) const; + + // Enables or disables timer interrupt generation for a channel. This will + // return false for an unknown channel. + bool setChannelInterruptEnable(int channel, bool enable) const; + + // Tests if the IEEE 1588 timer has been started. + operator bool() const; + + private: + EthernetIEEE1588Class() = default; + ~EthernetIEEE1588Class() = default; + + // The singleton instance. + static EthernetIEEE1588Class instance_; +}; + +} // namespace network +} // namespace qindesign + +#endif // #ifndef QNE_ETHERNETIEEE1588_H_ diff --git a/src/qnethernet/QNEthernetUDP.cpp b/src/qnethernet/QNEthernetUDP.cpp index c5ead12e6..831acdd26 100644 --- a/src/qnethernet/QNEthernetUDP.cpp +++ b/src/qnethernet/QNEthernetUDP.cpp @@ -67,6 +67,10 @@ void EthernetUDP::recvFunc(void *const arg, struct udp_pcb *const pcb, pNext = pNext->next; } } + packet.hasTimestamp = p->timestampValid; + if (packet.hasTimestamp) { + packet.timestamp = p->timestamp; + } packet.addr = *addr; packet.port = port; packet.receivedTimestamp = timestamp; @@ -256,6 +260,11 @@ void EthernetUDP::stop() { listening_ = false; listenReuse_ = false; + packet_.addr = *IP_ANY_TYPE; + packet_.port = 0; + packet_.hasTimestamp = false; + packet_.timestamp.tv_sec = 0; + packet_.timestamp.tv_nsec = 0; packet_.clear(); } @@ -314,6 +323,10 @@ int EthernetUDP::parsePacket() { // Pop (from the tail) packet_ = inBuf_[inBufTail_]; + inBuf_[inBufTail_].data.clear(); + inBuf_[inBufTail_].hasTimestamp = false; + inBuf_[inBufTail_].timestamp.tv_sec = 0; + inBuf_[inBufTail_].timestamp.tv_nsec = 0; inBuf_[inBufTail_].clear(); inBufTail_ = (inBufTail_ + 1) % inBuf_.size(); inBufSize_--; @@ -378,6 +391,15 @@ IPAddress EthernetUDP::remoteIP() { #endif // LWIP_IPV4 } +bool EthernetUDP::timestamp(timespec ×tamp) const { + // NOTE: This is not "concurrent safe" + if (packet_.hasTimestamp) { + timestamp = packet_.timestamp; + return true; + } + return false; +} + // -------------------------------------------------------------------------- // Transmission // -------------------------------------------------------------------------- diff --git a/src/qnethernet/QNEthernetUDP.h b/src/qnethernet/QNEthernetUDP.h index bfe5fd17a..020075327 100644 --- a/src/qnethernet/QNEthernetUDP.h +++ b/src/qnethernet/QNEthernetUDP.h @@ -13,6 +13,7 @@ // C++ includes #include #include +#include #include #include @@ -193,6 +194,11 @@ class EthernetUDP : public UDP, return listening_; } + // Gets the IEEE 1588 timestamp for the received packet and assigns it to the + // `timestamp` parameter, if available. This returns whether the received + // packet has a timestamp. + bool timestamp(timespec ×tamp) const; + // Sets the differentiated services (DiffServ, DS) field in the outgoing IP // header. The top 6 bits are the differentiated services code point (DSCP) // value, and the bottom 2 bits are the explicit congestion notification @@ -253,6 +259,8 @@ class EthernetUDP : public UDP, ip_addr_t addr = *IP_ANY_TYPE; volatile uint16_t port = 0; volatile uint32_t receivedTimestamp = 0; // Approximate arrival time + volatile bool hasTimestamp = false; + timespec timestamp{0, 0}; // Clears all the data. void clear(); diff --git a/src/qnethernet/drivers/driver_teensy41.c b/src/qnethernet/drivers/driver_teensy41.c index 50d1ef55e..cc84eb49b 100644 --- a/src/qnethernet/drivers/driver_teensy41.c +++ b/src/qnethernet/drivers/driver_teensy41.c @@ -14,10 +14,13 @@ #include #include #include +#include #include #include #include +#include +#include #include "lwip/arch.h" #include "lwip/err.h" @@ -306,6 +309,15 @@ static bool s_linkSpeed10Not100 = false; static bool s_linkIsFullDuplex = false; static bool s_linkIsCrossover = false; +// Is Ethernet initialized? +static bool isInitialized = false; + +// IEEE 1588 +static volatile uint32_t ieee1588Seconds = 0; // Since the timer was started +static volatile bool doTimestampNext = false; +static volatile bool hasTxTimestamp = false; +static volatile struct timespec txTimestamp = {0, 0}; + // Forward declarations static void enet_isr(void); @@ -648,6 +660,16 @@ static struct pbuf *low_level_input(volatile enetbufferdesc_t *const pBD) { arm_dcache_delete(pBD->buffer, MULTIPLE_OF_32(p->tot_len)); #endif // !QNETHERNET_BUFFERS_IN_RAM1 pbuf_take(p, pBD->buffer, p->tot_len); + p->timestampValid = ((pBD->status & kEnetRxBdLast) != 0); + if (p->timestampValid) { + enet_ieee1588_read_timer(&p->timestamp); + if ((unsigned long)p->timestamp.tv_nsec < pBD->timestamp) { + // The timer has wrapped around + p->timestamp.tv_sec--; + } + p->timestamp.tv_nsec = pBD->timestamp; + } + LINK_STATS_INC(link.recv); } else { LINK_STATS_INC(link.drop); LINK_STATS_INC(link.memerr); @@ -683,6 +705,14 @@ static inline void update_bufdesc(volatile enetbufferdesc_t *const pBD, kEnetTxBdLast | kEnetTxBdReady; + hasTxTimestamp = false; // The timestamp isn't yet available + if (doTimestampNext) { + doTimestampNext = false; + pBD->extend1 |= kEnetTxBdTimestamp; + } else { + pBD->extend1 &= ~kEnetTxBdTimestamp; + } + ENET_TDAR = ENET_TDAR_TDAR; if (pBD->status & kEnetTxBdWrap) { @@ -723,6 +753,18 @@ static void enet_isr(void) { ENET_EIR = ENET_EIR_RXF; atomic_flag_clear(&s_rxNotAvail); } + + if ((ENET_EIR & ENET_EIR_TS_TIMER) != 0) { + ENET_EIR = ENET_EIR_TS_TIMER; + ieee1588Seconds++; + } + + if ((ENET_EIR & ENET_EIR_TS_AVAIL) != 0) { + ENET_EIR = ENET_EIR_TS_AVAIL; + hasTxTimestamp = true; + txTimestamp.tv_sec = ieee1588Seconds; + txTimestamp.tv_nsec = ENET_ATSTMP; + } } // Checks the link status and returns zero if complete and a state value if @@ -970,6 +1012,10 @@ FLASHMEM bool driver_init(void) { attachInterruptVector(IRQ_ENET, &enet_isr); NVIC_ENABLE_IRQ(IRQ_ENET); + // Set the IEEE 1588 timestamp to zero, in case it's used but not enabled + ENET_ATVR = 0; + ieee1588Seconds = 0; + // Last few things to do ENET_EIR = 0x7fff8000; // Clear any pending interrupts before setting ETHEREN atomic_flag_test_and_set(&s_rxNotAvail); @@ -992,6 +1038,19 @@ FLASHMEM bool driver_init(void) { void unused_interrupt_vector(void); // startup.c FLASHMEM void driver_deinit(void) { + struct netif *s_netif = enet_netif(); + + isInitialized = false; + + enet_ieee1588_deinit(); + netif_set_addr(s_netif, IP4_ADDR_ANY4, IP4_ADDR_ANY4, IP4_ADDR_ANY4); + netif_set_link_down(s_netif); + netif_set_down(s_netif); + + // Restore state + // grrrr: the following should already have been done in enet_deinit +// memset(s_mac, 0, sizeof(s_mac)); + // Something about stopping Ethernet and the PHY kills performance if Ethernet // is restarted after calling end(), so gate the following two blocks with a // macro for now @@ -1202,4 +1261,292 @@ bool driver_set_incoming_mac_address_allowed(const uint8_t mac[ETH_HWADDR_LEN], #endif // !QNETHERNET_ENABLE_PROMISCUOUS_MODE +// -------------------------------------------------------------------------- +// IEEE 1588 functions +// -------------------------------------------------------------------------- + +#define ENET_ATCR_SLAVE ((uint32_t)(1U << 13)) +#define ENET_ATCR_CAPTURE ((uint32_t)(1U << 11)) +#define ENET_ATCR_RESTART ((uint32_t)(1U << 9)) +#define ENET_ATCR_PINPER ((uint32_t)(1U << 7)) +#define ENET_ATCR_Reserved ((uint32_t)(1U << 5)) // Spec says always write a 1 +#define ENET_ATCR_PEREN ((uint32_t)(1U << 4)) +#define ENET_ATCR_OFFRST ((uint32_t)(1U << 3)) +#define ENET_ATCR_OFFEN ((uint32_t)(1U << 2)) +#define ENET_ATCR_EN ((uint32_t)(1U << 0)) + +#define ENET_ATCOR_COR_MASK (0x7fffffffU) +#define ENET_ATINC_INC_MASK (0x00007f00U) +#define ENET_ATINC_INC_CORR(n) ((uint32_t)(((n) & 0x7f) << 8)) +#define ENET_ATINC_INC(n) ((uint32_t)(((n) & 0x7f) << 0)) + +#define NANOSECONDS_PER_SECOND (1000 * 1000 * 1000) +#define F_ENET_TS_CLK (25 * 1000 * 1000) + +#define ENET_TCSR_TMODE_MASK (0x0000003cU) +#define ENET_TCSR_TMODE(n) ((uint32_t)(((n) & 0x0f) << 2)) + +#define ENET_TCSR_TPWC_MASK ((uint32_t)(0x1f << 11)) +#define ENET_TCSR_TPWC(n) ((uint32_t)(((n) & 0x1f) << 11)) + +#define ENET_TCSR_TF ((uint32_t)(1U << 7)) + +#define ENET_TCSR_TIE_MASK ((uint32_t)(1U << 6)) +#define ENET_TCSR_TIE(n) ((uint32_t)(((n) & 0x01) << 6)) + +void enet_ieee1588_init() { + ENET_ATCR = ENET_ATCR_RESTART | ENET_ATCR_Reserved; // Reset timer + ENET_ATPER = NANOSECONDS_PER_SECOND; // Wrap at 10^9 + ENET_ATINC = ENET_ATINC_INC(NANOSECONDS_PER_SECOND / F_ENET_TS_CLK); + ENET_ATCOR = 0; // Start with no corr. + while ((ENET_ATCR & ENET_ATCR_RESTART) != 0) { + // Wait for bit to clear before being able to write to ATCR + } + + // Reset the seconds counter to zero + ieee1588Seconds = 0; + + // Enable the timer and periodic event + ENET_ATCR = ENET_ATCR_PINPER | ENET_ATCR_Reserved | ENET_ATCR_PEREN | + ENET_ATCR_EN; + + ENET_EIMR |= ENET_EIMR_TS_AVAIL | ENET_EIMR_TS_TIMER; +} + +void enet_ieee1588_deinit() { + ENET_EIMR &= ~(ENET_EIMR_TS_AVAIL | ENET_EIMR_TS_TIMER); + ENET_ATCR = ENET_ATCR_Reserved; +} + +bool enet_ieee1588_is_enabled() { + return ((ENET_ATCR & ENET_ATCR_EN) != 0); +} + +bool enet_ieee1588_read_timer(struct timespec *t) { + if (t == NULL) { + return false; + } + + ATOMIC_BLOCK(ATOMIC_RESTORESTATE) { + t->tv_sec = ieee1588Seconds; + + ENET_ATCR |= ENET_ATCR_CAPTURE; + while ((ENET_ATCR & ENET_ATCR_CAPTURE) != 0) { + // Wait for bit to clear + } + t->tv_nsec = ENET_ATVR; + + // The timer could have wrapped while we were doing stuff + // Leave the interrupt set so that our internal timer will catch it + if ((ENET_EIR & ENET_EIR_TS_TIMER) != 0) { + t->tv_sec++; + } + } + + return true; +} + +bool enet_ieee1588_write_timer(const struct timespec *t) { + if (t == NULL) { + return false; + } + + ATOMIC_BLOCK(ATOMIC_RESTORESTATE) { + ieee1588Seconds = t->tv_sec; + ENET_ATVR = t->tv_nsec; + } + + return true; +} + +bool enet_ieee1588_offset_timer(int64_t ns){ + struct timespec tm; + if(!enet_ieee1588_read_timer(&tm)){ + return false; + } + int64_t t = (((int64_t)tm.tv_sec) * NANOSECONDS_PER_SECOND) + ((int64_t)tm.tv_nsec); + t += ns; + tm.tv_nsec = t % NANOSECONDS_PER_SECOND; + tm.tv_sec = t / NANOSECONDS_PER_SECOND; + return enet_ieee1588_write_timer(&tm); +} + +void enet_ieee1588_timestamp_next_frame() { + doTimestampNext = true; +} + +bool enet_ieee1588_read_and_clear_tx_timestamp(struct timespec *timestamp) { + ATOMIC_BLOCK(ATOMIC_RESTORESTATE) { + if (hasTxTimestamp) { + hasTxTimestamp = false; + if (timestamp != NULL) { + timestamp->tv_sec = txTimestamp.tv_sec; + timestamp->tv_nsec = txTimestamp.tv_nsec; + } + return true; + } + } + return false; +} + +bool enet_ieee1588_adjust_timer(uint32_t corrInc, uint32_t corrPeriod) { + if (corrInc >= 128 || corrPeriod >= (1U << 31)) { + return false; + } + CLRSET(ENET_ATINC, ENET_ATINC_INC_MASK, ENET_ATINC_INC_CORR(corrInc)); + ENET_ATCOR = corrPeriod & ENET_ATCOR_COR_MASK; + return true; +} + +bool enet_ieee1588_adjust_freq(double nsps) { + if (nsps == 0) { + ENET_ATCOR = 0; + return true; + } + + uint32_t inc = NANOSECONDS_PER_SECOND / F_ENET_TS_CLK; + + if (nsps < 0) { + // Slow down + inc--; + nsps = -nsps; + } else { + // Speed up + inc++; + } + return enet_ieee1588_adjust_timer(inc, round(F_ENET_TS_CLK / nsps)); +} + +// Channels + +static volatile uint32_t *tcsrReg(int channel) { + switch (channel) { + case 0: return &ENET_TCSR0; + case 1: return &ENET_TCSR1; + case 2: return &ENET_TCSR2; + case 3: return &ENET_TCSR3; + default: + return NULL; + } +} + +static volatile uint32_t *tccrReg(int channel) { + switch (channel) { + case 0: return &ENET_TCCR0; + case 1: return &ENET_TCCR1; + case 2: return &ENET_TCCR2; + case 3: return &ENET_TCCR3; + default: + return NULL; + } + +} + +bool enet_ieee1588_set_channel_mode(int channel, int mode) { + if (channel < 0 || channel > 3){ + return false; + } + + if (mode < 0 || mode > 15 || mode == 8 || mode == 12 || mode == 13) {//Check for reserverd modes + return false; + } + volatile uint32_t *tcsr = tcsrReg(channel); + if (tcsr == NULL) { + return false; + } + + uint32_t state = *tcsr; // Backup current state + *tcsr = 0; + while ((*tcsr & ENET_TCSR_TMODE_MASK) != 0) { + // Check until the channel is disabled + } + CLRSET(state,ENET_TCSR_TMODE_MASK,ENET_TCSR_TMODE(mode)); + *tcsr = state; + while (*tcsr != state) { + // Check until the channel is enabled + } + + return true; +} + +bool enet_ieee1588_set_channel_output_pulse_width(int channel, + int pulseWidth) { + if (channel < 0 || channel > 3){ + return false; + } + + if (pulseWidth < 1 || 32 < pulseWidth) { + return false; + } + + volatile uint32_t *tcsr = tcsrReg(channel); + if (tcsr == NULL) { + return false; + } + uint32_t state = *tcsr; // Backup current state + *tcsr = 0; + while ((*tcsr & ENET_TCSR_TMODE_MASK) != 0) { + // Check until the channel is disabled + } + CLRSET(state,ENET_TCSR_TPWC_MASK,ENET_TCSR_TPWC(pulseWidth - 1)); + *tcsr = state; + while (*tcsr != state) { + // Check until the channel is enabled + } + return true; +} + +bool enet_ieee1588_set_channel_compare_value(int channel, uint32_t value) { + if (channel < 0 || channel > 3) { + return false; + } + volatile uint32_t *tccr = tccrReg(channel); + if (tccr == NULL) { + return false; + } + *tccr = value; + return true; +} + +bool enet_ieee1588_get_channel_compare_value(int channel, uint32_t *value) { + if (channel < 0 || channel > 3) { + return false; + } + volatile uint32_t *tccr = tccrReg(channel); + if (tccr == NULL) { + return false; + } + *value = *tccr; + return true; +} + +bool enet_ieee1588_get_and_clear_channel_status(int channel) { + if (channel < 0 || channel > 3) { + return false; + } + volatile uint32_t *tcsr = tcsrReg(channel); + if (tcsr == NULL) { + return false; + } + if ((*tcsr & ENET_TCSR_TF) != 0) { + *tcsr |= ENET_TCSR_TF; + ENET_TGSR = (1 << channel); + return true; + } + return false; +} + +bool enet_ieee1588_set_channel_interrupt_enable(int channel, bool enable){ + if (channel < 0 || channel > 3) { + return false; + } + + volatile uint32_t *tcsr = tcsrReg(channel); + if (tcsr == NULL) { + return false; + } + CLRSET(*tcsr,ENET_TCSR_TIE_MASK,ENET_TCSR_TIE(enable)); + return true; +} + #endif // QNETHERNET_INTERNAL_DRIVER_TEENSY41 diff --git a/src/qnethernet/lwip_driver.h b/src/qnethernet/lwip_driver.h index 40c5dff83..d46d7c37a 100644 --- a/src/qnethernet/lwip_driver.h +++ b/src/qnethernet/lwip_driver.h @@ -12,6 +12,7 @@ #include #include #include +#include #include "lwip/ip_addr.h" #include "lwip/netif.h" @@ -297,6 +298,96 @@ bool enet_leave_group(const ip4_addr_t *group); #endif // !QNETHERNET_ENABLE_PROMISCUOUS_MODE && LWIP_IPV4 +// -------------------------------------------------------------------------- +// IEEE 1588 functions +// -------------------------------------------------------------------------- + +// Initializes and enables the IEEE 1588 timer and functionality. The internal +// time is reset to zero. +void enet_ieee1588_init(); + +// Deinitializes and stops the IEEE 1588 timer. +void enet_ieee1588_deinit(); + +// Tests if the IEEE 1588 timer is enabled. +bool enet_ieee1588_is_enabled(); + +// Reads the IEEE 1588 timer. This returns whether successful. +// +// This will return false if the argument is NULL. +bool enet_ieee1588_read_timer(struct timespec *t); + +// Writes the IEEE 1588 timer. This returns whether successful. +// +// This will return false if the argument is NULL. +bool enet_ieee1588_write_timer(const struct timespec *t); + +// Adds an offset to the current timer value. Uses the read and write +// functions under the hood. This returns whether successful. +bool enet_ieee1588_offset_timer(int64_t ns); + +// Tells the driver to timestamp the next transmitted frame. +void enet_ieee1588_timestamp_next_frame(); + +// Returns whether an IEEE 1588 transmit timestamp is available. If available +// and the parameter is not NULL then it is assigned to `*timestamp`. This +// clears the timestamp state so that a subsequent call will return false. +// +// This function is used after sending a packet having its transmit timestamp +// sent. Note that this only returns the latest value, so if a second +// timestamped packet is sent before retrieving the timestamp for the first +// then this will return the second timestamp (if already available). +bool enet_ieee1588_read_and_clear_tx_timestamp(struct timespec *timestamp); + +// Directly adjust the correction increase and correction period. To adjust the +// timer in "nanoseconds per second", see `enet_ieee1588_adjust_freq`. This +// returns whether successful. +// +// This will return false if: +// 1. The correction increment is not in the range 0-127, or +// 2. The correction period is not in the range 0-(2^31-1). +bool enet_ieee1588_adjust_timer(uint32_t corrInc, uint32_t corrPeriod); + +// Adjust the correction in nanoseconds per second. This uses +// `enet_ieee1588_adjust_timer()` under the hood. +bool enet_ieee1588_adjust_freq(double nsps); + +// Sets the channel mode for the given channel. This does not set the output +// compare pulse modes. This returns whether successful. +// +// This will return false if: +// 1. The channel is unknown, +// 2. The mode is one of the output compare pulse modes, or +// 3. The mode is a reserved value or unknown. +bool enet_ieee1588_set_channel_mode(int channel, int mode); + +// Sets the output compare pulse mode and pulse width for the given channel. +// This returns whether successful. +// +// This will return false if: +// 1. The channel is unknown, +// 2. The pulse width is not in the range 1-32. +bool enet_ieee1588_set_channel_output_pulse_width(int channel, + int pulseWidth); + +// Sets the channel compare value. This returns whether successful. +// +// This will return false for an unknown channel. +bool enet_ieee1588_set_channel_compare_value(int channel, uint32_t value); + +// Gets the channel compare value. This returns whether successful. +// +// This will return false for an unknown channel. +bool enet_ieee1588_get_channel_compare_value(int channel, uint32_t *value); + +// Retrieves and then clears the status for the given channel. This will +// return false for an unknown channel. +bool enet_ieee1588_get_and_clear_channel_status(int channel); + +// Enables or disables timer interrupt generation for a channel. This will +// return false for an unknown channel. +bool enet_ieee1588_set_channel_interrupt_enable(int channel, bool enable); + #ifdef __cplusplus } // extern "C" #endif // __cplusplus