diff --git a/11-lorawan/Makefile b/11-lorawan/Makefile new file mode 100644 index 0000000..c83a7cf --- /dev/null +++ b/11-lorawan/Makefile @@ -0,0 +1,68 @@ +# ------------ [Task 1.9] -------------------- +# Replace these values with your own from the registered device +# No spaces between the quotes +CFLAGS += -DCONFIG_LORAMAC_APP_EUI_DEFAULT=\"0000000000000000\" +CFLAGS += -DCONFIG_LORAMAC_DEV_EUI_DEFAULT=\"XXXXXXXXXXXXXXXX\" +CFLAGS += -DCONFIG_LORAMAC_APP_KEY_DEFAULT=\"XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX\" +# ----------------------------------------- + +APPLICATION = lorawan_example + +# If no BOARD is found in the environment, use this default: +BOARD ?= lab-feather-nrf52840-sense + +# This has to be the absolute path to the RIOT base directory: +RIOTBASE ?= $(CURDIR)/../RIOT + +# include SAUL to interact with onboard sensors and actuators +USEMODULE += saul + +# include and auto-initialize all available sensors +USEMODULE += saul_default + +# Include board's default network devices and auto-initialization of GNRC +# interfaces +USEMODULE += netdev_default +USEMODULE += auto_init_gnrc_netif + +# GNRC modules to build and receive packets +USEMODULE += gnrc_pktbuf +USEMODULE += gnrc_netif_hdr +USEMODULE += gnrc_netapi +USEMODULE += gnrc_netreg +USEMODULE += gnrc_neterr + +# Add support for GNRC LoRaWAN +USEMODULE += gnrc_lorawan + +# Include OD to print received packets +USEMODULE += od + +# Some workarounds are needed in order to get the tutorial running on +# some computers. +-include ../lab_workarounds.mk + +# we use ztimer to read sensors periodically +USEMODULE += ztimer +USEMODULE += ztimer_sec + +# We want to use OTAA join procedure +CFLAGS += -DCONFIG_LORAMAC_DEFAULT_JOIN_PROCEDURE_OTAA + +# Bring the LoRa radio driver +USEMODULE += sx127x + +ifeq ($(BOARD),lab-feather-nrf52840-sense) +# Indicate how we connected the LoRa radio to the microcontroller +CFLAGS += -DSX127X_PARAM_SPI_NSS=GPIO_PIN\(0,27\) #B +CFLAGS += -DSX127X_PARAM_RESET=GPIO_PIN\(0,26\) #C +CFLAGS += -DSX127X_PARAM_DIO0=GPIO_PIN\(0,6\) #A +CFLAGS += -DSX127X_PARAM_DIO1=GPIO_PIN\(1,8\) #E +CFLAGS += -DSX127X_PARAM_DIO2=GPIO_PIN\(0,7\) #D +CFLAGS += -DSX127X_PARAM_DIO3=GPIO_UNDEF +CFLAGS += -DSX127X_PARAM_DIO4=GPIO_UNDEF +CFLAGS += -DSX127X_PARAM_DIO5=GPIO_UNDEF +CFLAGS += -DSX127X_PARAM_PASELECT=SX127X_PA_BOOST +endif + +include $(RIOTBASE)/Makefile.include diff --git a/11-lorawan/README.md b/11-lorawan/README.md new file mode 100644 index 0000000..3c76f4b --- /dev/null +++ b/11-lorawan/README.md @@ -0,0 +1,308 @@ +# LoRaWAN + +LoRaWAN is a Media Access Control (MAC) layer protocol built on top of LoRa modulation. +It is a software layer which defines how devices use the LoRa hardware, for example when they transmit, and the format of messages. The LoRaWAN protocol is developed and maintained by the LoRa Alliance. + +The Things Network is powered by The Things Stack, which is a LoRaWAN network server that receives messages from LoRaWAN devices. + +To change to this directory from a different exercise, use the following command in the terminal. + +```sh +$ cd ../11-lorawan +``` + +## Task 1 + +1. With your browser access The Things Network at [https://www.thethingsnetwork.org/](https://www.thethingsnetwork.org/), +and create an account for yourself by clicking on the "Sign Up" button. + +2. Once you have an account, navigate to the console at [https://console.cloud.thethings.network/](https://console.cloud.thethings.network/), and choose the "Europe 1" cluster. + + ![console_access](assets/console_access.png) + +3. On your console, start the creation of a new application, by clicking "Create application". + + ![console_create](assets/console_create.png) + +4. Set your application ID and description, then confirm with "Create application". + + ![create_application](assets/create_application.png) + +5. From the panel that shows your application overview, click on "+ Register end device", inside the "End devices" section. + + ![application_overview](assets/application_overview.png) + +6. To register the device, choose to "Enter end device specifics manually", so we can provide the configuration. Follow this configuration, make sure to "Show advanced activation, LoRaWAN class and cluster settings": + + | Parameter | Value | + | --------- | ----- | + | Frequency plan | "Europe 863-870 MHz (SF9 for RX2 - recommended)" | + | LoRaWAN version | "LoRaWAN Specification 1.0.3" | + | Regional Parameters version | "RP001 Regional Parameters 1.0.3 revision A" | + | Activation mode | "Over the air activation (OTTA)" | + | Additional LoRaWAN class capabilities | "None (class A only)" | + | Use network's default MAC settings | True | + | Join EUI | `00 00 00 00 00 00 00 00` | + + ![register_device](assets/register_device.png) + +7. Once you entered the Join EUI, click on "Confirm". Generate the DevEUI and AppKey by clicking on "Generate". +8. Enter a unique name for your device, and click on "Register end device" +9. From the device overview panel, copy the "AppEUI", "DevEUI", and "AppKey" values, and replace them in the `Makefile` of this exercise. + +## Task 2 + +1. To be able to send data, we first need to determine which network interface to use. We can iterate the register of network interfaces, until we find the one that has a LoRa device (in our case the device will have also a IEEE802.15.4 radio as a second interface). We need to implement `find_lorawan_network_interface` in `main.c`. The function should return a pointer to the first found lora interface. We start by initializing local variables: + ```C + netif_t *netif = NULL; + uint16_t device_type = 0; + ``` +We need to iterate the register and for each interface check the device type. In case we finish the iteration and didn't find a valid interface, the returned value should be `NULL`: + ```C + do { + netif = netif_iter(netif); + if (netif == NULL) { + puts("No network interface found"); + break; + } + netif_get_opt(netif, NETOPT_DEVICE_TYPE, 0, &device_type, sizeof(device_type)); + } while (device_type != NETDEV_TYPE_LORA); + + return netif; + ``` + +2. Now that we found the correct interface, we need to join the network. We'll be using the [OTAA join method](https://www.thethingsnetwork.org/docs/lorawan/end-device-activation/#over-the-air-activation-in-lorawan-10x). + We'll implement the `join_lorawan_network` function, which receives the interface that was found before. + The procedure is: + - iteratively attempt to join the network. + - wait for a few seconds. + - check the result. + + ```C + netopt_enable_t status; + uint8_t data_rate = 5; + + while (1) { + status = NETOPT_ENABLE; + printf("Joining LoRaWAN network...\n"); + ztimer_now_t timeout = ztimer_now(ZTIMER_SEC); + netif_set_opt(netif, NETOPT_LINK, 0, &status, sizeof(status)); + + while (ztimer_now(ZTIMER_SEC) - timeout < 15000) { + /* Wait for a while to allow the join process to complete */ + ztimer_sleep(ZTIMER_SEC, 1); + + netif_get_opt(netif, NETOPT_LINK, 0, &status, sizeof(status)); + if (status == NETOPT_ENABLE) { + printf("Joined LoRaWAN network successfully\n"); + + /* Set the data rate */ + netif_set_opt(netif, NETOPT_LORAWAN_DR, 0, &data_rate, sizeof(data_rate)); + + /* Disable uplink confirmation requests */ + status = NETOPT_DISABLE; + netif_set_opt(netif, NETOPT_ACK_REQ, 0, &status, sizeof(status)); + return; + } + } + } + ``` + +3. Finally, we can send data via LoRaWAN whenever we read a new temperature value. For this, implement `send_lorawan_packet`. + First, get both bytes representing the temperature value: + + ```C + data[0] = temperature->val[0] >> 8; // High byte + data[1] = temperature->val[0] & 0xFF; // Low byte + ``` + + Then, create a new packet buffer for our data: + ```C + packet = gnrc_pktbuf_add(NULL, &data, sizeof(data), GNRC_NETTYPE_UNDEF); + if (packet == NULL) { + puts("Failed to create packet"); + return -1; + } + ``` + + As the send operation doesn't block our thread, we need to subscribe to the events + generated by the payload snippet. This way, we can wait until the network stack has + processed our packet and confirmed its transmission. We use the error reporting API. + + ```C + if (gnrc_neterr_reg(packet) != 0) { + puts("Failed to register for error reporting"); + gnrc_pktbuf_release(packet); + return -1; + } + ``` + + Now, we need a packet header: + ```C + header = gnrc_netif_hdr_build(NULL, 0, &address, sizeof(address)); + if (header == NULL) { + puts("Failed to create header"); + gnrc_pktbuf_release(packet); + return -1; + } + ``` + + As a last step, we add the header to the buffer: + ```C + packet = gnrc_pkt_prepend(packet, header); + netif_header = (gnrc_netif_hdr_t *)header->data; + netif_header->flags = 0x00; + ``` + + We can now trigger the transmission of the packet: + + ```C + result = gnrc_netif_send(container_of(netif, gnrc_netif_t, netif), packet); + + if (result < 1) { + printf("error: unable to send\n"); + gnrc_pktbuf_release(packet); + return 1; + } + ``` + + We want to wait until the network stack confirms that the packet has been + sent. This indication will arrive to the message queue. + + ```C + /* wait for transmission confirmation */ + msg_receive(&msg); + if (msg.type != GNRC_NETERR_MSG_TYPE) { + printf("error: unexpected message type %" PRIu16 "\n", msg.type); + return -1; + } + if (msg.content.value != GNRC_NETERR_SUCCESS) { + printf("error: unable to send, error: (%" PRIu32 ")\n", msg.content.value); + return -1; + } + + return 0; + ``` + +4. We're sending our temperature encoded in a particular way. To actually see the data + in the correct format on the "Live Data" tab of the things network dashboard, we need + to implement a payload formatter. This will receive all uplink data, and allow us to + operate on it to parse it according to our application logic. + + Navigate the the "Payload formatters" tab of the dashboard, and select + "Custom Javascript formatter" in the "Formatter type" dropdown. In the formatter code, + place the following snippet, which takes the individual bytes, arranges them into a + two-bytes integer, and scales it to deg. Celsius. + + ```js + function decodeUplink(input) { + return { + data: { + temperature: (input.bytes[0] << 8 | input.bytes[1]) / 100.0 + }, + warnings: [], + errors: [] + }; + } + ``` + +Upon boot, your node should be able to join the LoRaWAN network and start sending the +temperature data from its sensor. + +## Task 3 + +We can already send data to the LoRaWAN server, now we want to be able to receive +downlinks from it. In typical deployments, the nodes will be sending more uplinks than +receiving downlinks. In fact, our node is of type A, which means that a downlink is only +possible right after an uplink. + +To keep our firmware responsive, we're going to use a thread to handle the packet +receptions. If you want more details on RIOT threads, check the +[exercise 06](../06-threads/README.md). The reception thread will create a message queue, +and register it to the network stack, so it gets notified when new events occur. Every +time a new packet is received, we'll print its content. + +1. We first create the reception thread. We can simply call `thread_create` after we +have joined the network. + + ```C + kernel_pid_t rx_pid = thread_create(_rx_thread_stack, sizeof(_rx_thread_stack), + THREAD_PRIORITY_MAIN - 1, + THREAD_CREATE_STACKTEST, rx_thread, NULL, + "lorawan_rx"); + if (-EINVAL == rx_pid) { + puts("Failed to create reception thread"); + return -1; + } + ``` + +2. We need to make sure that the network stack notifies the thread of events. +For this, we register to the GNRC netreg with the thread's PID. + + ```C + gnrc_netreg_entry_t entry = GNRC_NETREG_ENTRY_INIT_PID(GNRC_NETREG_DEMUX_CTX_ALL, + rx_pid); + gnrc_netreg_register(GNRC_NETTYPE_UNDEF, &entry); + ``` + +3. Now that the network stack will send messages to the thread, we need to be prepared to +receive them. We'll be using one of RIOT's synchronization primitives: +[messages](https://doc.riot-os.org/group__core__msg.html). First, define the queue where +messages are received. Define this at the beginning of `main.c`. + + ```C + static msg_t _rx_msg_queue[RX_QUEUE_SIZE]; + ``` + +4. Inside the reception thread function (`rx_thread`), initialize the queue once, outside +the infinite loop. + + ```C + msg_init_queue(_rx_msg_queue, RX_QUEUE_SIZE); + ``` + +5. In the loop, we want to wait for new messages. We can call `msg_receive`, which will +block the thread until a new message arrives to the queue. + + ```C + msg_receive(&msg); + ``` + +6. Whenever we receive a new message of the type `GNRC_NETAPI_MSG_TYPE_RCV`, it means that +a new packet has been received, and we need to process it. + + ```C + if (msg.type == GNRC_NETAPI_MSG_TYPE_RCV) { + puts("Received data"); + gnrc_pktsnip_t *pkt = msg.content.ptr; + _print_received_packet(pkt); + } + ``` + +7. For this exercise we'll print the packet to STDOUT. In a real application you'd parse +the content to act upon it. Each packet in the network stack will be represented as a +linked list of packet snippets. We need to iterate them and print the one that has the +payload data. We can identify this snippet by the type `GNRC_NETTYPE_UNDEF`. Implement +the function `_print_received_packet`. + + ```C + /* iterate over all packet snippets */ + while (snip != NULL) { + /* LoRaWAN payload will have 'undefined' type */ + if (snip->type == GNRC_NETTYPE_UNDEF) { + od_hex_dump(((uint8_t *)pkt->data), pkt->size, OD_WIDTH_DEFAULT); + } + snip = snip->next; + } + + /* always release the packet buffer to prevent memory leaks */ + gnrc_pktbuf_release(pkt); + ``` + +8. We should be able to receive downlinks from the application now! To schedule a new +downlink message (which is sent after the next uplink), navigate to your device's +dashboard in the things network, and click in the 'Messaging' tab. There, set a payload +in the `Payload` field. The values need to be encoded in hexadecimal. Keep the packet a +few bytes long (e.g., `55 66`). Click on 'Schedule downlink'. + + ![downlink](assets/downlink.png) diff --git a/11-lorawan/assets/application_overview.png b/11-lorawan/assets/application_overview.png new file mode 100644 index 0000000..22ef134 Binary files /dev/null and b/11-lorawan/assets/application_overview.png differ diff --git a/11-lorawan/assets/console_access.png b/11-lorawan/assets/console_access.png new file mode 100644 index 0000000..9a24cf1 Binary files /dev/null and b/11-lorawan/assets/console_access.png differ diff --git a/11-lorawan/assets/console_create.png b/11-lorawan/assets/console_create.png new file mode 100644 index 0000000..bc8e9bb Binary files /dev/null and b/11-lorawan/assets/console_create.png differ diff --git a/11-lorawan/assets/create_application.png b/11-lorawan/assets/create_application.png new file mode 100644 index 0000000..f9d58c9 Binary files /dev/null and b/11-lorawan/assets/create_application.png differ diff --git a/11-lorawan/assets/downlink.png b/11-lorawan/assets/downlink.png new file mode 100644 index 0000000..c8dfb04 Binary files /dev/null and b/11-lorawan/assets/downlink.png differ diff --git a/11-lorawan/assets/live_data.png b/11-lorawan/assets/live_data.png new file mode 100644 index 0000000..5b7c2b9 Binary files /dev/null and b/11-lorawan/assets/live_data.png differ diff --git a/11-lorawan/assets/register_device.png b/11-lorawan/assets/register_device.png new file mode 100644 index 0000000..4c068e7 Binary files /dev/null and b/11-lorawan/assets/register_device.png differ diff --git a/11-lorawan/main.c b/11-lorawan/main.c new file mode 100644 index 0000000..638f219 --- /dev/null +++ b/11-lorawan/main.c @@ -0,0 +1,306 @@ +/* + * Copyright (C) 2025 HAW Hamburg + * + * This file is subject to the terms and conditions of the GNU Lesser + * General Public License v2.1. See the file LICENSE in the top level + * directory for more details. + */ +#include +#include +#include +#include + +#include "board.h" + +#include "net/netdev.h" +#include "net/netif.h" + +#include "net/gnrc/pktbuf.h" +#include "net/gnrc/pkt.h" +#include "net/gnrc/netreg.h" +#include "net/gnrc/netif/hdr.h" + +#include "saul_reg.h" +#include "phydat.h" + +#include "od.h" +#include "msg.h" +#include "thread.h" + +#include "ztimer.h" + +/* Interval between data transmissions, in seconds */ +#define SEND_INTERVAL_SEC 1 + +/* Size of reception message queue */ +#define QUEUE_SIZE 8 + +/* Stack for reception thread */ +static char _rx_thread_stack[THREAD_STACKSIZE_DEFAULT]; + +/* [TASK 3.3: Message queue for reception thread] */ +static msg_t _rx_msg_queue[QUEUE_SIZE]; + +static msg_t _tx_msg_queue[QUEUE_SIZE]; + +/** + * @brief Find the LoRaWAN network interface in the registry. + * @return Pointer to the LoRaWAN network interface, or NULL if not found. + */ +static netif_t *_find_lorawan_network_interface(void); + +/** + * @brief Join the LoRaWAN network using OTAA. + * @param netif Pointer to the LoRaWAN network interface. + * + * This function will attempt to join the LoRaWAN network using Over-The-Air + * Activation (OTAA). It will keep retrying until a successful join is achieved. + */ +static void _join_lorawan_network(const netif_t *netif); + +/** + * @brief Send a LoRaWAN packet with temperature data. + * @param netif Pointer to the LoRaWAN network interface. + * @param temperature Pointer to the temperature data to be sent. + * + * @retval 0 on success + * @retval -1 on failure + */ +static int _send_lorawan_packet(const netif_t *netif, const phydat_t *temperature); + +/** + * @brief Print to STDOUT the received packet. + * @param pkt Pointer to the received packet. + */ +static void _print_received_packet(gnrc_pktsnip_t *pkt); + +static netif_t *_find_lorawan_network_interface(void) +{ + /* [TASK 2.1: implement function to identify lorawan interface here]*/ + netif_t *netif = NULL; + uint16_t device_type = 0; + + do { + netif = netif_iter(netif); + if (netif == NULL) { + puts("No network interface found"); + break; + } + netif_get_opt(netif, NETOPT_DEVICE_TYPE, 0, &device_type, sizeof(device_type)); + } while (device_type != NETDEV_TYPE_LORA); + + return netif; +} + +static void _join_lorawan_network(const netif_t *netif) +{ + assert(netif != NULL); + + /* [TASK 2.2: implement join function here ]*/ + netopt_enable_t status; + uint8_t data_rate = 5; + + while (1) { + status = NETOPT_ENABLE; + printf("Joining LoRaWAN network...\n"); + ztimer_now_t timeout = ztimer_now(ZTIMER_SEC); + netif_set_opt(netif, NETOPT_LINK, 0, &status, sizeof(status)); + + while (ztimer_now(ZTIMER_SEC) - timeout < 10000) { + /* Wait for a while to allow the join process to complete */ + ztimer_sleep(ZTIMER_SEC, 1); + + netif_get_opt(netif, NETOPT_LINK, 0, &status, sizeof(status)); + if (status == NETOPT_ENABLE) { + printf("Joined LoRaWAN network successfully\n"); + + /* Set the data rate */ + netif_set_opt(netif, NETOPT_LORAWAN_DR, 0, &data_rate, sizeof(data_rate)); + + /* Disable uplink confirmation requests */ + status = NETOPT_DISABLE; + netif_set_opt(netif, NETOPT_ACK_REQ, 0, &status, sizeof(status)); + return; + } + } + } +} + +static int _send_lorawan_packet(const netif_t *netif, const phydat_t *temperature) +{ + assert(netif != NULL); + assert(temperature != NULL); + + int result; + gnrc_pktsnip_t *packet; + gnrc_pktsnip_t *header; + gnrc_netif_hdr_t *netif_header; + uint8_t address = 1; + msg_t msg; + uint8_t data[2]; + + /* [TASK 2.3] implement function to send data via lorawan */ + data[0] = temperature->val[0] >> 8; // High byte + data[1] = temperature->val[0] & 0xFF; // Low byte + + packet = gnrc_pktbuf_add(NULL, &data, sizeof(data), GNRC_NETTYPE_UNDEF); + if (packet == NULL) { + puts("Failed to create packet"); + return -1; + } + + if (gnrc_neterr_reg(packet) != 0) { + puts("Failed to register for error reporting"); + gnrc_pktbuf_release(packet); + return -1; + } + + header = gnrc_netif_hdr_build(NULL, 0, &address, sizeof(address)); + if (header == NULL) { + puts("Failed to create header"); + gnrc_pktbuf_release(packet); + return -1; + } + + packet = gnrc_pkt_prepend(packet, header); + netif_header = (gnrc_netif_hdr_t *)header->data; + netif_header->flags = 0x00; + + result = gnrc_netif_send(container_of(netif, gnrc_netif_t, netif), packet); + if (result < 1) { + printf("error: unable to send\n"); + gnrc_pktbuf_release(packet); + return -1; + } + + /* wait for transmission confirmation */ + msg_receive(&msg); + if (msg.type != GNRC_NETERR_MSG_TYPE) { + printf("error: unexpected message type %" PRIu16 "\n", msg.type); + return -1; + } + if (msg.content.value != GNRC_NETERR_SUCCESS) { + printf("error: unable to send, error: (%" PRIu32 ")\n", msg.content.value); + return -1; + } + + return 0; +} + +void *rx_thread(void *arg) +{ + (void)arg; + msg_t msg; + + /* [TASK 3.4: initialize the message queue] */ + msg_init_queue(_rx_msg_queue, QUEUE_SIZE); + + while (1) { + /* [TASK 3.5: wait until we get a message]*/ + msg_receive(&msg); + + if (msg.type == GNRC_NETAPI_MSG_TYPE_RCV) { + puts("Received data"); + gnrc_pktsnip_t *pkt = msg.content.ptr; + _print_received_packet(pkt); + } + } + + /* never reached */ + return NULL; +} + +static void _print_received_packet(gnrc_pktsnip_t *pkt) +{ + assert(pkt != NULL); + + gnrc_pktsnip_t *snip = pkt; + + /* [TASK 3.7: iterate over all packet snippets] */ + while (snip != NULL) { + /* LoRaWAN payload will have 'undefined' type */ + if (snip->type == GNRC_NETTYPE_UNDEF) { + od_hex_dump(((uint8_t *)pkt->data), pkt->size, OD_WIDTH_DEFAULT); + } + snip = snip->next; + } + + /* always release the packet buffer to prevent memory leaks */ + gnrc_pktbuf_release(pkt); +} + +int main(void) +{ + int result; + netif_t *netif = NULL; + + /* Sleep so that we do not miss this message while connecting */ + ztimer_sleep(ZTIMER_SEC, 3); + + /* initialize message queue */ + msg_init_queue(_tx_msg_queue, QUEUE_SIZE); + + /* get the on-board temperature sensor */ + saul_reg_t *temp_sensor = saul_reg_find_type(SAUL_SENSE_TEMP); + if (!temp_sensor) { + puts("No temperature sensor present"); + return 1; + } + + /* find the LoRaWAN network interface and connect */ + netif = _find_lorawan_network_interface(); + if (netif == NULL) { + puts("No LoRaWAN network interface found"); + return -1; + } + + _join_lorawan_network(netif); + + /* [TASK 3.1: create the reception thread] */ + kernel_pid_t rx_pid = thread_create(_rx_thread_stack, sizeof(_rx_thread_stack), + THREAD_PRIORITY_MAIN - 1, + THREAD_CREATE_STACKTEST, rx_thread, NULL, + "lorawan_rx"); + if (-EINVAL == rx_pid) { + puts("Failed to create reception thread"); + return -1; + } + + /* [TASK 3.2: receive LoRaWAN packets in our reception thread] */ + gnrc_netreg_entry_t entry = GNRC_NETREG_ENTRY_INIT_PID(GNRC_NETREG_DEMUX_CTX_ALL, + rx_pid); + gnrc_netreg_register(GNRC_NETTYPE_UNDEF, &entry); + + /* record the starting time */ + ztimer_now_t last_wakeup = ztimer_now(ZTIMER_SEC); + + while (1) { + /* read a temperature value from the sensor */ + phydat_t temperature; + int dimensions = saul_reg_read(temp_sensor, &temperature); + if (dimensions < 1) { + puts("Error reading a value from the device"); + break; + } + + /* dump the read value to STDIO */ + phydat_dump(&temperature, dimensions); + + /* [TASK 2.3: send sensor data via LoRaWAN ] */ + puts("Sending temperature data via LoRaWAN..."); + result = _send_lorawan_packet(netif, &temperature); + if (result != 0) { + puts("Failed to send LoRaWAN packet"); + } else { + printf("Sent LoRaWAN packet successfully\n"); + } + + printf("%d\n", temperature.val[0]); + + /* wait a bit */ + printf("Waiting for %d seconds...\n", SEND_INTERVAL_SEC); + ztimer_periodic_wakeup(ZTIMER_SEC, &last_wakeup, SEND_INTERVAL_SEC); + } + + return 0; +} diff --git a/lab-feather-nrf52840-sense/Kconfig b/lab-feather-nrf52840-sense/Kconfig new file mode 100644 index 0000000..a2cf404 --- /dev/null +++ b/lab-feather-nrf52840-sense/Kconfig @@ -0,0 +1,16 @@ +# Copyright (c) 2023 HAW Hamburg +# +# This file is subject to the terms and conditions of the GNU Lesser +# General Public License v2.1. See the file LICENSE in the top level +# directory for more details. + +config BOARD + default "lab-feather-nrf52840-sense" if BOARD_LAB_FEATHER_NRF52840_SENSE + +config BOARD_LAB_FEATHER_NRF52840_SENSE + bool + default y + select BOARD_COMMON_NRF52 + select CPU_MODEL_NRF52840XXAA + +source "$(RIOTBOARD)/common/nrf52/Kconfig" diff --git a/lab-feather-nrf52840-sense/Makefile b/lab-feather-nrf52840-sense/Makefile new file mode 100644 index 0000000..f8fcbb5 --- /dev/null +++ b/lab-feather-nrf52840-sense/Makefile @@ -0,0 +1,3 @@ +MODULE = board + +include $(RIOTBASE)/Makefile.base diff --git a/lab-feather-nrf52840-sense/Makefile.dep b/lab-feather-nrf52840-sense/Makefile.dep new file mode 100644 index 0000000..c52f9ec --- /dev/null +++ b/lab-feather-nrf52840-sense/Makefile.dep @@ -0,0 +1,13 @@ +ifneq (,$(filter saul_default,$(USEMODULE))) + USEMODULE += apds9960 + USEMODULE += bmp280_i2c + USEMODULE += lis3mdl + USEMODULE += lsm6dsxx + USEMODULE += saul_gpio + USEMODULE += sht3x + USEMODULE += ws281x +endif + +# include common nrf52 dependencies +include $(RIOTBOARD)/common/nrf52/bootloader_nrfutil.dep.mk +include $(RIOTBOARD)/common/nrf52/Makefile.dep diff --git a/lab-feather-nrf52840-sense/Makefile.features b/lab-feather-nrf52840-sense/Makefile.features new file mode 100644 index 0000000..1f4969f --- /dev/null +++ b/lab-feather-nrf52840-sense/Makefile.features @@ -0,0 +1,12 @@ +CPU_MODEL = nrf52840xxaa + +# Put defined MCU peripherals here (in alphabetical order) +FEATURES_PROVIDED += periph_i2c +FEATURES_PROVIDED += periph_spi +FEATURES_PROVIDED += periph_uart +FEATURES_PROVIDED += periph_usbdev + +# Various other features (if any) +FEATURES_PROVIDED += highlevel_stdio + +include $(RIOTBOARD)/common/nrf52/Makefile.features diff --git a/lab-feather-nrf52840-sense/Makefile.include b/lab-feather-nrf52840-sense/Makefile.include new file mode 100644 index 0000000..0d3f071 --- /dev/null +++ b/lab-feather-nrf52840-sense/Makefile.include @@ -0,0 +1,31 @@ +CFLAGS += -DCLOCK_HFXO_ONBOOT=1 + +PROGRAMMER ?= uf2conv + +UF2CONV_FLAGS = -f 0xADA52840 + +ifeq (uf2conv,$(PROGRAMMER)) + + # Using uf2conv implies using the UF2 bootloader + # + # It has a static MBR at the first 4k, and a 38k UF2 Bootloader at + # the end, leaving 972k for the application. This overwrites any SoftDevice, + # but that's what the minimal working example does as well. + ROM_OFFSET = 0x1000 + ROM_LEN = 0xf3000 + + # Driver can take some time to get mounted + PREFLASH_DELAY ?= 3 + include $(RIOTMAKE)/tools/usb_board_reset.mk +endif + +PROGRAMMERS_SUPPORTED += uf2conv + +# HACK: replicate dependency resolution in Makefile.dep, only works +# if `USEMODULE` or `DEFAULT_MODULE` is set by the command line or in the +# application Makefile. +ifeq (,$(filter stdio_%,$(DISABLE_MODULE) $(USEMODULE))) + RIOT_TERMINAL ?= jlink +endif + +include $(RIOTBOARD)/common/nrf52/Makefile.include diff --git a/lab-feather-nrf52840-sense/include/board.h b/lab-feather-nrf52840-sense/include/board.h new file mode 100644 index 0000000..fbdfc32 --- /dev/null +++ b/lab-feather-nrf52840-sense/include/board.h @@ -0,0 +1,110 @@ +/* + * Copyright (C) 2020 Freie Universität Berlin + * Copyright (C) 2023 HAW Hamburg + * + * This file is subject to the terms and conditions of the GNU Lesser + * General Public License v2.1. See the file LICENSE in the top level + * directory for more details. + */ + +/** + * @ingroup boards_feather-nrf52840-sense + * @{ + * + * @file + * @brief Board specific configuration for the Adafruit Feather nRF52840 + * Sense + * + * @author Martine S. Lenders + * @author Michel Rottleuthner + */ + +#ifndef BOARD_H +#define BOARD_H + +#include "cpu.h" +#include "board_common.h" +#include "periph/gpio.h" + +#ifdef __cplusplus +extern "C" { +#endif + +/** + * @name LED pin configuration + * @{ + */ +#define LED0_PIN GPIO_PIN(1, 9) +#define LED1_PIN GPIO_PIN(1, 10) + +#define LED_PORT (NRF_P1) +#define LED0_MASK (1 << 9) +#define LED1_MASK (1 << 10) +#define LED_MASK (LED0_MASK | LED1_MASK) + +#define LED0_ON (LED_PORT->OUTSET = LED0_MASK) +#define LED0_OFF (LED_PORT->OUTCLR = LED0_MASK) +#define LED0_TOGGLE (LED_PORT->OUT ^= LED0_MASK) + +#define LED1_ON (LED_PORT->OUTSET = LED1_MASK) +#define LED1_OFF (LED_PORT->OUTCLR = LED1_MASK) +#define LED1_TOGGLE (LED_PORT->OUT ^= LED1_MASK) +/** @} */ + +/** + * @name Button pin configuration + * @{ + */ +#define BTN0_PIN GPIO_PIN(1, 2) +#define BTN0_MODE GPIO_IN_PU +/** @} */ + +/** + * @name BMP280 sensor configuration + * @{ + */ +#define BMX280_PARAM_I2C_DEV I2C_DEV(0) /**< I2C device */ +/** @} */ + +/** + * @name LIS3MDL 3-axis magnetometer + * @{ + */ +#define LIS3MDL_PARAM_I2C I2C_DEV(0) /**< I2C device */ +#define LIS3MDL_PARAM_ADDR (0x1C) /**< I2C address */ +/** @} */ + +/** + * @name SHT30 temperature and humidity sensor + * @{ + */ +#define SHT3X_PARAM_I2C_DEV I2C_DEV(0) /**< I2C device */ +#define SHT3X_PARAM_I2C_ADDR (SHT3X_I2C_ADDR_1) /**< I2C address */ +/** @} */ + +/** + * @name LSM6DSXX accelerometer sensor configuration + * @{ + */ +#define LSM6DSXX_PARAM_I2C I2C_DEV(0) +#define LSM6DSXX_PARAM_ADDR (0x6A) +/** @} */ + +/** + * @name WS281x RGB LEDs configuration + * @{ + */ +#ifndef WS281X_PARAM_PIN +#define WS281X_PARAM_PIN GPIO_PIN(0, 16) /**< GPIO pin connected to the data pin of the first LED */ +#endif +#ifndef WS281X_PARAM_NUMOF +#define WS281X_PARAM_NUMOF (1U) /**< Number of LEDs chained */ +#endif +/** @} */ + +#ifdef __cplusplus +} +#endif + +#endif /* BOARD_H */ +/** @} */ diff --git a/lab-feather-nrf52840-sense/include/gpio_params.h b/lab-feather-nrf52840-sense/include/gpio_params.h new file mode 100644 index 0000000..564a932 --- /dev/null +++ b/lab-feather-nrf52840-sense/include/gpio_params.h @@ -0,0 +1,61 @@ +/* + * Copyright (C) 2020 Freie Universität Berlin + * Copyright (C) 2023 HAW Hamburg + * + * This file is subject to the terms and conditions of the GNU Lesser + * General Public License v2.1. See the file LICENSE in the top level + * directory for more details. + */ + +/** + * @ingroup boards_feather-nrf52840-sense + * @{ + * + * @file + * @brief Configuration of SAUL mapped GPIO pins + * + * @author Martine S. Lenders + * @author Michel Rottleuthner + */ + +#ifndef GPIO_PARAMS_H +#define GPIO_PARAMS_H + +#include "board.h" +#include "saul/periph.h" + +#ifdef __cplusplus +extern "C" { +#endif + +/** + * @brief LED configuration + */ +static const saul_gpio_params_t saul_gpio_params[] = +{ + { + .name = "LED Red (D13)", + .pin = LED0_PIN, + .mode = GPIO_OUT, + .flags = (SAUL_GPIO_INIT_CLEAR), + }, + { + .name = "LED Blue (Conn)", + .pin = LED1_PIN, + .mode = GPIO_OUT, + .flags = (SAUL_GPIO_INIT_CLEAR), + }, + { + .name = "UserSw", + .pin = BTN0_PIN, + .mode = BTN0_MODE, + .flags = SAUL_GPIO_INVERTED, + }, +}; + +#ifdef __cplusplus +} +#endif + +#endif /* GPIO_PARAMS_H */ +/** @} */ diff --git a/lab-feather-nrf52840-sense/include/periph_conf.h b/lab-feather-nrf52840-sense/include/periph_conf.h new file mode 100644 index 0000000..536fdb5 --- /dev/null +++ b/lab-feather-nrf52840-sense/include/periph_conf.h @@ -0,0 +1,95 @@ +/* + * Copyright (C) 2020 Freie Universität Berlin + * Copyright (C) 2023 HAW Hamburg + * + * This file is subject to the terms and conditions of the GNU Lesser + * General Public License v2.1. See the file LICENSE in the top level + * directory for more details. + */ + +/** + * @ingroup boards_feather-nrf52840-sense + * @{ + * + * @file + * @brief Peripheral configuration for the Adafruit Feather nRF52840 + * Sense + * + * @author Martine S. Lenders + * @author Michel Rottleuthner + * + */ + +#ifndef PERIPH_CONF_H +#define PERIPH_CONF_H + +#include "periph_cpu.h" +#include "cfg_rtt_default.h" +#include "cfg_timer_default.h" + +#ifdef __cplusplus +extern "C" { +#endif + +#define CLOCK_HFCLK (32U) /* 32MHz crystal */ +#define CLOCK_LFCLK (CLOCK_LFCLKSRC_SRC_Synth) /* LFCLK Source derived from HFCLK */ + +/** + * @name UART configuration + * @{ + */ +static const uart_conf_t uart_config[] = { + { + .dev = NRF_UARTE0, + .rx_pin = GPIO_PIN(0, 24), + .tx_pin = GPIO_PIN(0, 25), +#ifdef MODULE_PERIPH_UART_HW_FC + .rts_pin = GPIO_UNDEF, + .cts_pin = GPIO_UNDEF, +#endif + .irqn = UARTE0_UART0_IRQn, + }, +}; + +#define UART_0_ISR (isr_uart0) + +#define UART_NUMOF ARRAY_SIZE(uart_config) +/** @} */ + +/** + * @name SPI configuration + * @{ + */ +static const spi_conf_t spi_config[] = { + { + .dev = NRF_SPIM0, + .sclk = 14, + .mosi = 13, + .miso = 15, + } +}; + +#define SPI_NUMOF ARRAY_SIZE(spi_config) +/** @} */ + +/** + * @name I2C configuration + * @{ + */ +static const i2c_conf_t i2c_config[] = { + { + .dev = NRF_TWIM1, + .scl = 11, + .sda = 12, + .speed = I2C_SPEED_NORMAL + } +}; +#define I2C_NUMOF ARRAY_SIZE(i2c_config) +/** @} */ + +#ifdef __cplusplus +} +#endif + +#endif /* PERIPH_CONF_H */ +/** @} */ diff --git a/lab-feather-nrf52840-sense/reset.c b/lab-feather-nrf52840-sense/reset.c new file mode 100644 index 0000000..68fe19f --- /dev/null +++ b/lab-feather-nrf52840-sense/reset.c @@ -0,0 +1,41 @@ +/* + * Copyright (C) 2020 Inria + * + * This file is subject to the terms and conditions of the GNU Lesser + * General Public License v2.1. See the file LICENSE in the top level + * directory for more details. + */ + +/** + * @ingroup boards_feather-nrf52840 + * @{ + * @file + * @brief Implementation for managing the nrfutil bootloader + * + * @author Alexandre Abadie + * + * @} + */ + +#ifdef MODULE_USB_BOARD_RESET + +#define USB_H_USER_IS_RIOT_INTERNAL + +#include "cpu.h" +#include "usb_board_reset.h" + +/* Set the value used by the bootloader to select between boot in + application and boot in bootloader mode. */ +#define NRF52_DOUBLE_TAP_MAGIC_NUMBER (0x57) + +void usb_board_reset_in_bootloader(void) +{ + NRF_POWER->GPREGRET = NRF52_DOUBLE_TAP_MAGIC_NUMBER; + + /* Going with a hard reset rather than a pm_off, as that might be altered + * to do *anything* -- which is not safe any more now that we've forsaken + * the RAM content */ + NVIC_SystemReset(); +} + +#endif /* MODULE_USB_BOARD_RESET */ diff --git a/lab_workarounds.mk b/lab_workarounds.mk index d607edf..1320341 100644 --- a/lab_workarounds.mk +++ b/lab_workarounds.mk @@ -5,3 +5,5 @@ PREFLASH_DELAY=10 # The ZTIMER_MSEC seems to be broken on some compilers so we force docker # to ensure a working compiler. BUILD_IN_DOCKER ?= 1 + +EXTERNAL_BOARD_DIRS += $(CURDIR)/..