From 17e94092a1e0700c48b7acd042b28bba6272b574 Mon Sep 17 00:00:00 2001 From: Testimony <70136570+Tessy8@users.noreply.github.com> Date: Wed, 25 Mar 2026 14:22:09 +0100 Subject: [PATCH] Add Raspberry Pi Pico port (#1232) * Add Raspberry Pi Pico port - Adds BACnet/IP with abstracted network layer - Adds BACnet MS/TP support using RS485/UART - Includes README with examples * Add Pico 2 MS/TP demo, CI, and docs --- .github/workflows/gcc.yml | 1 + Makefile | 6 + ports/pico/CMakeLists.txt | 131 +++++++++++++ ports/pico/README.md | 162 +++++++++++++++++ ports/pico/bip-init.c | 116 ++++++++++++ ports/pico/bip.c | 373 ++++++++++++++++++++++++++++++++++++++ ports/pico/bip.h | 131 +++++++++++++ ports/pico/bvlc-pico.c | 191 +++++++++++++++++++ ports/pico/bvlc-pico.h | 36 ++++ ports/pico/configure.sh | 32 ++++ ports/pico/dlenv.c | 108 +++++++++++ ports/pico/dlenv.h | 42 +++++ ports/pico/main.c | 66 +++++++ ports/pico/mstimer_init.c | 19 ++ ports/pico/mstimer_init.h | 25 +++ ports/pico/rs485.c | 229 +++++++++++++++++++++++ ports/pico/rs485.h | 52 ++++++ 17 files changed, 1720 insertions(+) create mode 100644 ports/pico/CMakeLists.txt create mode 100644 ports/pico/README.md create mode 100644 ports/pico/bip-init.c create mode 100644 ports/pico/bip.c create mode 100644 ports/pico/bip.h create mode 100644 ports/pico/bvlc-pico.c create mode 100644 ports/pico/bvlc-pico.h create mode 100755 ports/pico/configure.sh create mode 100644 ports/pico/dlenv.c create mode 100644 ports/pico/dlenv.h create mode 100644 ports/pico/main.c create mode 100644 ports/pico/mstimer_init.c create mode 100644 ports/pico/mstimer_init.h create mode 100644 ports/pico/rs485.c create mode 100644 ports/pico/rs485.h diff --git a/.github/workflows/gcc.yml b/.github/workflows/gcc.yml index 238e8e99..d8cdd198 100644 --- a/.github/workflows/gcc.yml +++ b/.github/workflows/gcc.yml @@ -238,6 +238,7 @@ jobs: arm-none-eabi-gcc --version make stm32f4xx-cmake make at91sam7s-cmake + make pico-cmake ports-avr: runs-on: ubuntu-latest diff --git a/Makefile b/Makefile index b222193d..8b252dde 100644 --- a/Makefile +++ b/Makefile @@ -395,6 +395,12 @@ dlmstp-linux: ports/linux/dlmstp.mak lwip: ports/lwip/Makefile $(MAKE) -s -C ports/lwip clean all +.PHONY: pico-cmake +pico-cmake: + cd ports/pico && ./configure.sh + [ -d ports/pico/build ] || mkdir -p ports/pico/build + cd ports/pico/build && cmake .. -DPICO_SDK_PATH=../external/pico-sdk && cmake --build . --clean-first -- -j$(shell nproc 2>/dev/null || echo 1) + .PHONY: pretty pretty: find ./src -iname *.h -o -iname *.c -exec \ diff --git a/ports/pico/CMakeLists.txt b/ports/pico/CMakeLists.txt new file mode 100644 index 00000000..d00dae7b --- /dev/null +++ b/ports/pico/CMakeLists.txt @@ -0,0 +1,131 @@ +cmake_minimum_required(VERSION 3.13) + +if (NOT DEFINED PICO_SDK_PATH AND DEFINED ENV{PICO_SDK_PATH}) + set(PICO_SDK_PATH "$ENV{PICO_SDK_PATH}") +endif() +if (DEFINED PICO_SDK_PATH) + get_filename_component( + PICO_SDK_PATH "${PICO_SDK_PATH}" REALPATH + BASE_DIR "${CMAKE_CURRENT_BINARY_DIR}") + set(ENV{PICO_SDK_PATH} "${PICO_SDK_PATH}") +endif() + +# Default this port to Pico 2. +if (NOT PICO_BOARD) + set(PICO_BOARD pico2 CACHE STRING "Pico SDK board for the BACnet Pico port" FORCE) +endif() +if (NOT DEFINED ENV{PICO_BOARD}) + set(ENV{PICO_BOARD} "${PICO_BOARD}") +endif() + +include(${PICO_SDK_PATH}/external/pico_sdk_import.cmake) + +project(bacnet_pico_port C CXX ASM) + +set(CMAKE_C_STANDARD 11) +set(CMAKE_CXX_STANDARD 17) + +pico_sdk_init() + +set(BACNET_ROOT ${CMAKE_CURRENT_LIST_DIR}/../..) +set(BACNET_SRC ${BACNET_ROOT}/src) +set(BACNET_CORE ${BACNET_SRC}/bacnet) +set(BACNET_BASIC ${BACNET_SRC}/bacnet/basic) + +set(BACNET_COMMON_SOURCES + ${BACNET_BASIC}/binding/address.c + ${BACNET_BASIC}/object/client/device-client.c + ${BACNET_BASIC}/npdu/h_npdu.c + ${BACNET_BASIC}/service/h_apdu.c + ${BACNET_BASIC}/service/h_cov.c + ${BACNET_BASIC}/service/h_dcc.c + ${BACNET_BASIC}/service/h_noserv.c + ${BACNET_BASIC}/service/h_rd.c + ${BACNET_BASIC}/service/h_rp.c + ${BACNET_BASIC}/service/h_rpm.c + ${BACNET_BASIC}/service/h_whohas.c + ${BACNET_BASIC}/service/h_whois.c + ${BACNET_BASIC}/service/h_wp.c + ${BACNET_BASIC}/service/s_iam.c + ${BACNET_BASIC}/service/s_ihave.c + ${BACNET_BASIC}/sys/datetime_mstimer.c + ${BACNET_BASIC}/sys/days.c + ${BACNET_BASIC}/sys/dst.c + ${BACNET_BASIC}/sys/debug.c + ${BACNET_BASIC}/sys/ringbuf.c + ${BACNET_BASIC}/sys/fifo.c + ${BACNET_BASIC}/sys/mstimer.c + ${BACNET_BASIC}/tsm/tsm.c + ${BACNET_CORE}/abort.c + ${BACNET_CORE}/bacaction.c + ${BACNET_CORE}/bacaddr.c + ${BACNET_CORE}/bacapp.c + ${BACNET_CORE}/bacdcode.c + ${BACNET_CORE}/bacdest.c + ${BACNET_CORE}/bacdevobjpropref.c + ${BACNET_CORE}/bacerror.c + ${BACNET_CORE}/bacint.c + ${BACNET_CORE}/bacreal.c + ${BACNET_CORE}/bacstr.c + ${BACNET_CORE}/datalink/cobs.c + ${BACNET_CORE}/datalink/crc.c + ${BACNET_CORE}/datalink/dlmstp.c + ${BACNET_CORE}/datalink/mstp.c + ${BACNET_CORE}/datalink/mstptext.c + ${BACNET_CORE}/datetime.c + ${BACNET_CORE}/dcc.c + ${BACNET_CORE}/hostnport.c + ${BACNET_CORE}/iam.c + ${BACNET_CORE}/ihave.c + ${BACNET_CORE}/indtext.c + ${BACNET_CORE}/memcopy.c + ${BACNET_CORE}/npdu.c + ${BACNET_CORE}/proplist.c + ${BACNET_CORE}/rd.c + ${BACNET_CORE}/reject.c + ${BACNET_CORE}/rp.c + ${BACNET_CORE}/rpm.c + ${BACNET_CORE}/timestamp.c + ${BACNET_CORE}/whohas.c + ${BACNET_CORE}/whois.c + ${BACNET_CORE}/wp.c +) + +set(BACNET_MSTP_SOURCES + ${BACNET_COMMON_SOURCES} + ${CMAKE_CURRENT_LIST_DIR}/dlenv.c + ${CMAKE_CURRENT_LIST_DIR}/rs485.c + ${CMAKE_CURRENT_LIST_DIR}/mstimer_init.c + ${CMAKE_CURRENT_LIST_DIR}/main.c +) + +add_executable(bacnet-pico-mstp ${BACNET_MSTP_SOURCES}) + +target_include_directories(bacnet-pico-mstp PRIVATE + ${CMAKE_CURRENT_LIST_DIR} + ${BACNET_SRC} +) + +target_compile_definitions(bacnet-pico-mstp PRIVATE + BACDL_MSTP + MAX_APDU=480 + BIG_ENDIAN=0 + PRINT_ENABLED=0 + MAX_TSM_TRANSACTIONS=1 + CRC_USE_TABLE + BACAPP_MINIMAL + BACAPP_TIMESTAMP + BACNET_STACK_DEPRECATED_DISABLE + BACNET_PROTOCOL_REVISION=16 +) + +target_link_libraries(bacnet-pico-mstp + pico_stdlib + hardware_uart + hardware_gpio + hardware_timer +) + +pico_enable_stdio_usb(bacnet-pico-mstp 1) +pico_enable_stdio_uart(bacnet-pico-mstp 0) +pico_add_extra_outputs(bacnet-pico-mstp) diff --git a/ports/pico/README.md b/ports/pico/README.md new file mode 100644 index 00000000..c6d9bed5 --- /dev/null +++ b/ports/pico/README.md @@ -0,0 +1,162 @@ +# BACnet Stack Port for Raspberry Pi Pico + +This directory contains the Raspberry Pi Pico port of the BACnet stack, supporting both BACnet/IP and BACnet MS/TP. + +## Overview + +This port provides: +- **BACnet/IP** support with abstracted network layer (Developer must provide Ethernet implementation) +- **BACnet MS/TP** support using UART (RS485) + +## Hardware Used + +The port is written against the Raspberry Pi Pico SDK. The same code is intended to build for boards that the Pico SDK exposes with compatible UART/GPIO support, such as: + +- `pico` +- `pico_w` +- `pico2` +- `pico2_w` + +The current `make pico-cmake` flow defaults to the Pico SDK board `pico2` for this port unless `-DPICO_BOARD=...` is passed to CMake. + +For the MS/TP side, the code currently assumes: + +- UART1 on GPIO 8 (`TX`) and GPIO 9 (`RX`) +- GPIO 10 as the RS485 transceiver `DE`/`RE` control pin +- An external 3.3 V RS485 transceiver module, for example a MAX3485- or SN65HVD-based module + +Typical wiring is: + +- Pico/Pico 2 GPIO 8 -> transceiver `DI` +- Pico/Pico 2 GPIO 9 -> transceiver `RO` +- Pico/Pico 2 GPIO 10 -> transceiver `DE` and `RE` +- Pico/Pico 2 `3V3` -> transceiver `VCC` +- Pico/Pico 2 `GND` -> transceiver `GND` +- Transceiver `A/B` -> BACnet MS/TP trunk + +No external hardware is needed for the automated CI build because the CI job only verifies that the Pico MS/TP target compiles. + +## Software Dependencies + +The external dependency required for this port is cloned by `ports/pico/configure.sh` into `ports/pico/external/`: + +- Raspberry Pi Pico SDK 2.2.0 + URL: + +The script checks out that SDK tag and initializes the SDK submodules needed by the build. + +## Build + +From the repository root: + +```sh +make pico-cmake +``` + +That target runs `ports/pico/configure.sh`, configures the CMake build in `ports/pico/build`, and builds the Pico port for `pico2`. + +To select another supported board, configure manually and pass `PICO_BOARD`: + +```sh +cd ports/pico +./configure.sh +cmake -S . -B build -DPICO_SDK_PATH="$PWD/external/pico-sdk" -DPICO_BOARD=pico2 +cmake --build build -- -j"$(nproc)" +``` + +## Using This Port + +### Implement Network Functions + +Developer must implement the following functions declared in `bip.h`: + +```c +bool bip_socket_init(uint16_t port); + +int bip_socket_send( + const uint8_t *dest_addr, + uint16_t dest_port, + const uint8_t *mtu, + uint16_t mtu_len); + +int bip_socket_receive( + uint8_t *buf, + uint16_t buf_len, + uint8_t *src_addr, + uint16_t *src_port); + +void bip_socket_cleanup(void); + +bool bip_get_local_network_info(uint8_t *local_addr, uint8_t *netmask); +``` + +These BACnet/IP hooks are examples of the interface you need to provide. The repository does not yet include a complete in-tree Pico Ethernet or Wi-Fi transport implementation, so BACnet/IP still requires board-specific network code behind `bip.h`. + +### Example Usage + +```c +#include "pico/stdlib.h" +#include "hardware/gpio.h" +#include "hardware/uart.h" +#include "mstimer_init.h" +#include "bip.h" +/* BACnet Stack Includes */ +#include "bacnet/datalink/datalink.h" +#include "bacnet/basic/services.h" +#include "bacnet/basic/object/device.h" +#include "bacnet/basic/tsm/tsm.h" +#include "bacnet/dcc.h" + +static struct mstimer DCC_Timer; + +int main() { + // Initialize Pico + stdio_init_all(); + systimer_init(); + + // Initialize BIP datalink + Device_Set_Object_Instance_Number(12345); + Device_Init(NULL); + address_init(); + + // Initialize BACnet/IP on port 0xBAC0 (47808) + if (!bip_init(0xBAC0)) { + return -1; + } + + /* set up confirmed service unrecognized service handler - required! */ + Send_I_Am(&Handler_Transmit_Buffer[0]); + + mstimer_set(&DCC_Timer, 1000); // 1 second timer + printf("BACnet BIP Initialized"); + + BACNET_ADDRESS src = {0}; + uint8_t rx_buf[480]; + const uint16_t max_pdu = sizeof(rx_buf); + + while (1) + { + if (mstimer_expired(&DCC_Timer)) + { + mstimer_reset(&DCC_Timer); + dcc_timer_seconds(1); + } + + uint16_t pdu_len = bip_receive(&src, rx_buf, max_pdu, 0); + if (pdu_len > 0) + { + npdu_handler(&src, rx_buf, pdu_len); + printf("%u bytes received from MAC %u\r\n", pdu_len, src.mac[0]); + for (int i=0;i + * + * SPDX-License-Identifier: GPL-2.0-or-later WITH GCC-exception-2.0 + * + *********************************************************************/ +#include +#include +#include +#include +#include "bacnet/bacdcode.h" +#include "bip.h" + +/** @file pico/bip-init.c Initializes BACnet/IP interface (Pico). */ + +static bool BIP_Debug = false; + +/** + * @brief Enabled debug printing of BACnet/IPv4 + */ +void bip_debug_enable(void) +{ + BIP_Debug = true; +} + +/** + * @brief Disable debug printing of BACnet/IPv4 + */ +void bip_debug_disable(void) +{ + BIP_Debug = false; +} + +/* gets an IP address by name, where name can be a + string that is an IP address in dotted form, or + a name that is a domain name + returns 0 if not found, or + an IP address in network byte order */ +long bip_getaddrbyname(const char *host_name) +{ + (void)host_name; + return 0; +} + +/** Gets the local IP address and local broadcast address from the system, + * and saves it into the BACnet/IP data structures. + */ +void bip_set_interface(void) +{ + uint8_t local_address[] = { 0, 0, 0, 0 }; + uint8_t broadcast_address[] = { 0, 0, 0, 0 }; + uint8_t netmask[] = { 0, 0, 0, 0 }; + uint8_t invertedNetmask[] = { 0, 0, 0, 0 }; + int i; + + /* Get network info from platform-specific function */ + if (!bip_get_local_network_info(local_address, netmask)) { + return; + } + + bip_set_addr(local_address); + + /* setup local broadcast address */ + for (i = 0; i < 4; i++) { + invertedNetmask[i] = ~netmask[i]; + broadcast_address[i] = (local_address[i] | invertedNetmask[i]); + } + + bip_set_broadcast_addr(broadcast_address); +} + +/** Initialize the BACnet/IP services at the given interface. + * @ingroup DLBIP + * -# Gets the local IP address and local broadcast address from the system, + * and saves it into the BACnet/IP data structures. + * -# Opens a UDP socket + * -# Configures the socket for sending and receiving + * -# Configures the socket so it can send broadcasts + * -# Binds the socket to the local IP address at the specified port for + * BACnet/IP (by default, 0xBAC0 = 47808). + * + * @note For Linux, ifname is eth0, ath0, arc0, and others. + * + * @param port [in] UDP port to use for BACnet/IP + * @return True if the socket is successfully opened for BACnet/IP, + * else False if the socket functions fail. + */ +bool bip_init(uint16_t port) +{ + /* Initialize the socket using platform-specific function */ + if (!bip_socket_init(port)) { + return false; + } + + bip_set_interface(); + bip_set_port(port); + + /* Mark socket as valid (0 is a valid socket ID for Pico) */ + bip_set_socket(0); + + return true; +} + +/** Cleanup and close out the BACnet/IP services by closing the socket. + * @ingroup DLBIP + */ +void bip_cleanup(void) +{ + if (bip_valid()) { + bip_socket_cleanup(); + } + + return; +} diff --git a/ports/pico/bip.c b/ports/pico/bip.c new file mode 100644 index 00000000..5d5d8ac5 --- /dev/null +++ b/ports/pico/bip.c @@ -0,0 +1,373 @@ +/************************************************************************** + * + * Copyright (C) 2005 Steve Karg + * Copyright (C) 2025 Testimony Adams + * + * SPDX-License-Identifier: GPL-2.0-or-later WITH GCC-exception-2.0 + * + *********************************************************************/ +#include +#include +#include +#include "bacnet/bacdcode.h" +#include "bacnet/bacint.h" +#include "bacnet/datalink/bvlc.h" +#include "bvlc-pico.h" +#include "bip.h" + +#if PRINT_ENABLED || DEBUG +#include +#endif + +/** @file bip.c Configuration and Operations for BACnet/IP */ + +#define MAX_SOCK_NUM 8 + +static uint8_t BIP_Socket = MAX_SOCK_NUM; +/* port to use - stored in network byte order */ +static uint16_t BIP_Port = 0; +/* IP Address - stored in network byte order */ +static uint8_t BIP_Address[4] = { 0, 0, 0, 0 }; +/* Broadcast Address - stored in network byte order */ +static uint8_t BIP_Broadcast_Address[4] = { 0, 0, 0, 0 }; + +/** Converter from uint8_t[4] type address to uint32_t + * + */ +uint32_t convertBIP_Address2uint32(const uint8_t *bip_address) +{ + return ((uint32_t)bip_address[0] << 24) | ((uint32_t)bip_address[1] << 16) | + ((uint32_t)bip_address[2] << 8) | ((uint32_t)bip_address[3]); +} + +/** Convert from uint32_t IPv4 address to uint8_t[4] address + * + */ +void convertUint32Address_2_uint8Address(uint32_t ip, uint8_t *address) +{ + address[0] = (uint8_t)(ip >> 24); + address[1] = (uint8_t)(ip >> 16); + address[2] = (uint8_t)(ip >> 8); + address[3] = (uint8_t)(ip >> 0); +} + +/** Setter for the BACnet/IP socket handle. + * + * @param sock_fd [in] Handle for the BACnet/IP socket. + */ +void bip_set_socket(uint8_t sock_fd) +{ + BIP_Socket = sock_fd; +} + +/** Getter for the BACnet/IP socket handle. + * + * @return The handle to the BACnet/IP socket. + */ +uint8_t bip_socket(void) +{ + return BIP_Socket; +} + +bool bip_valid(void) +{ + return (BIP_Socket < MAX_SOCK_NUM); +} + +void bip_set_addr(const uint8_t *net_address) +{ + uint8_t i; + for (i = 0; i < 4; i++) { + BIP_Address[i] = net_address[i]; + } +} + +/* returns network byte order */ +uint8_t *bip_get_addr(void) +{ + return BIP_Address; +} + +void bip_set_broadcast_addr(const uint8_t *net_address) +{ + uint8_t i; + for (i = 0; i < 4; i++) { + BIP_Broadcast_Address[i] = net_address[i]; + } +} + +uint8_t *bip_get_broadcast_addr(void) +{ + return BIP_Broadcast_Address; +} + +void bip_set_port(uint16_t port) +{ + BIP_Port = port; +} + +/* returns network byte order */ +uint16_t bip_get_port(void) +{ + return BIP_Port; +} + +static int bip_decode_bip_address( + const BACNET_ADDRESS *bac_addr, uint8_t *address, uint16_t *port) +{ + int len = 0; + + if (bac_addr) { + memcpy(address, &bac_addr->mac[0], 4); + memcpy(port, &bac_addr->mac[4], 2); + len = 6; + } + + return len; +} + +/** Function to send a packet out the BACnet/IP socket (Annex J). + * @ingroup DLBIP + * + * @param dest [in] Destination address (may encode an IP address and port #). + * @param npdu_data [in] The NPDU header (Network) information (not used). + * @param pdu [in] Buffer of data to be sent - may be null (why?). + * @param pdu_len [in] Number of bytes in the pdu buffer. + * @return Number of bytes sent on success, negative number on failure. + */ +int bip_send_pdu( + BACNET_ADDRESS *dest, + BACNET_NPDU_DATA *npdu_data, + uint8_t *pdu, + unsigned pdu_len) +{ + uint8_t mtu[BIP_MPDU_MAX] = { 0 }; + int mtu_len = 0; + int bytes_sent = 0; + uint8_t address[] = { 0, 0, 0, 0 }; + uint16_t port = 0; + uint8_t i; + + (void)npdu_data; + /* assumes that the driver has already been initialized */ + if (BIP_Socket >= MAX_SOCK_NUM) { + return -1; + } + + mtu[0] = BVLL_TYPE_BACNET_IP; + if ((dest->net == BACNET_BROADCAST_NETWORK) || + ((dest->net > 0) && (dest->len == 0)) || (dest->mac_len == 0)) { + /* broadcast */ + for (i = 0; i < 4; i++) { + address[i] = BIP_Broadcast_Address[i]; + } + port = BIP_Port; + mtu[1] = BVLC_ORIGINAL_BROADCAST_NPDU; +#ifdef DEBUG + fprintf( + stderr, "Send Broadcast NPDU to %d.%d.%d.%d:%d\n", address[0], + address[1], address[2], address[3], port); +#endif + } else if (dest->mac_len == 6) { + bip_decode_bip_address(dest, address, &port); + mtu[1] = BVLC_ORIGINAL_UNICAST_NPDU; +#ifdef DEBUG + fprintf( + stderr, "Send Unicast NPDU to %d.%d.%d.%d:%d\n", address[0], + address[1], address[2], address[3], port); +#endif + } else { + /* invalid address */ + return -1; + } + + mtu_len = 2; + mtu_len += encode_unsigned16( + &mtu[mtu_len], (uint16_t)(pdu_len + 4 /*inclusive */)); + memcpy(&mtu[mtu_len], pdu, pdu_len); + mtu_len += pdu_len; + +#ifdef DEBUG + fprintf(stderr, "MTU size %d\n", mtu_len); +#endif + + /* Send the packet using platform-specific function */ + bytes_sent = bip_socket_send(address, port, mtu, mtu_len); + + return bytes_sent; +} + +/** Implementation of the receive() function for BACnet/IP; receives one + * packet, verifies its BVLC header, and removes the BVLC header from + * the PDU data before returning. + * + * @param src [out] Source of the packet - who should receive any response. + * @param pdu [out] A buffer to hold the PDU portion of the received packet, + * after the BVLC portion has been stripped off. + * @param max_pdu [in] Size of the pdu[] buffer. + * @param timeout [in] The number of milliseconds to wait for a packet. + * @return The number of octets (remaining) in the PDU, or zero on failure. + */ +uint16_t bip_receive( + BACNET_ADDRESS *src, uint8_t *pdu, uint16_t max_pdu, unsigned timeout) +{ + int received_bytes = 0; + int max = 0; + uint16_t pdu_len = 0; + uint8_t src_addr[] = { 0, 0, 0, 0 }; + uint16_t src_port = 0; + uint16_t i = 0; + int function = 0; + + (void)timeout; /* unused parameter */ + + /* Make sure the socket is open */ + if (BIP_Socket >= MAX_SOCK_NUM) { + return 0; + } + + /* Receive packet using platform-specific function */ + received_bytes = bip_socket_receive(pdu, max_pdu, src_addr, &src_port); + + /* No data available */ + if (received_bytes <= 0) { + return 0; + } + + /* the signature of a BACnet/IP packet */ + if (pdu[0] != BVLL_TYPE_BACNET_IP) { + return 0; + } + + /* Erase up to 16 bytes after the received bytes as safety margin */ + max = (int)max_pdu - received_bytes; + if (max > 0) { + if (max > 16) { + max = 16; + } + memset(&pdu[received_bytes], 0, max); + } + + if (bvlc_for_non_bbmd(src_addr, &src_port, pdu, received_bytes) > 0) { + /* Handled, usually with a NACK. */ +#if PRINT_ENABLED + fprintf(stderr, "BIP: BVLC discarded!\n"); +#endif + return 0; + } + + function = pico_bvlc_get_function_code(); + if ((function == BVLC_ORIGINAL_UNICAST_NPDU) || + (function == BVLC_ORIGINAL_BROADCAST_NPDU)) { + /* ignore messages from me */ + if ((convertBIP_Address2uint32(src_addr) == + convertBIP_Address2uint32(BIP_Address)) && + (src_port == BIP_Port)) { + pdu_len = 0; + } else { + /* data in src->mac[] is in network format */ + src->mac_len = 6; + memcpy(&src->mac[0], &src_addr, 4); + memcpy(&src->mac[4], &src_port, 2); +#ifdef DEBUG + fprintf( + stderr, "BIP receive from %d.%d.%d.%d\n", src->mac[0], + src->mac[1], src->mac[2], src->mac[3]); +#endif + /* decode the length of the PDU - length is inclusive of BVLC */ + (void)decode_unsigned16(&pdu[2], &pdu_len); + /* subtract off the BVLC header */ + pdu_len -= 4; + if (pdu_len < max_pdu) { + /* shift the buffer to return a valid PDU */ + for (i = 0; i < pdu_len; i++) { + pdu[i] = pdu[4 + i]; + } + } else { + /* ignore packets that are too large */ + pdu_len = 0; +#if PRINT_ENABLED + fprintf(stderr, "BIP: PDU too large. Discarded!.\n"); +#endif + } + } + } else if (function == BVLC_FORWARDED_NPDU) { + memcpy(&src_addr, &pdu[4], 4); + memcpy(&src_port, &pdu[8], 2); + if ((convertBIP_Address2uint32(src_addr) == + convertBIP_Address2uint32(BIP_Address)) && + (src_port == BIP_Port)) { + /* ignore messages from me */ + pdu_len = 0; + } else { + /* data in src->mac[] is in network format */ + src->mac_len = 6; + memcpy(&src->mac[0], &src_addr, 4); + memcpy(&src->mac[4], &src_port, 2); + /* decode the length of the PDU - length is inclusive of BVLC */ + (void)decode_unsigned16(&pdu[2], &pdu_len); + /* subtract off the BVLC header */ + pdu_len -= 10; + if (pdu_len < max_pdu) { + /* shift the buffer to return a valid PDU */ + for (i = 0; i < pdu_len; i++) { + pdu[i] = pdu[4 + 6 + i]; + } + } else { + /* ignore packets that are too large */ + pdu_len = 0; + } + } + } + + return pdu_len; +} + +void bip_get_my_address(BACNET_ADDRESS *my_address) +{ + int i = 0; + + if (my_address) { + my_address->mac_len = 6; + memcpy(&my_address->mac[0], &BIP_Address, 4); + memcpy(&my_address->mac[4], &BIP_Port, 2); + my_address->net = 0; /* local only, no routing */ + my_address->len = 0; /* no SLEN */ + for (i = 0; i < MAX_MAC_LEN; i++) { + /* no SADR */ + my_address->adr[i] = 0; + } + } + + return; +} + +void bip_get_broadcast_address(BACNET_ADDRESS *dest) +{ + int i = 0; + + if (dest) { + dest->mac_len = 6; + memcpy(&dest->mac[0], &BIP_Broadcast_Address, 4); + memcpy(&dest->mac[4], &BIP_Port, 2); + dest->net = BACNET_BROADCAST_NETWORK; + dest->len = 0; /* no SLEN */ + for (i = 0; i < MAX_MAC_LEN; i++) { + /* no SADR */ + dest->adr[i] = 0; + } + } + + return; +} + +int bip_send_mpdu( + const BACNET_IP_ADDRESS *dest, const uint8_t *mtu, uint16_t mtu_len) +{ + int bytes_sent = 0; + + /* Send using platform-specific function */ + bytes_sent = bip_socket_send(dest->address, dest->port, mtu, mtu_len); + + return bytes_sent; +} diff --git a/ports/pico/bip.h b/ports/pico/bip.h new file mode 100644 index 00000000..c5140776 --- /dev/null +++ b/ports/pico/bip.h @@ -0,0 +1,131 @@ +/************************************************************************** + * + * Copyright (C) 2012 Steve Karg + * Copyright (C) 2025 Testimony Adams + * + * SPDX-License-Identifier: MIT + * + *********************************************************************/ +#ifndef BIP_H +#define BIP_H + +#include +#include +#include +#include "bacnet/bacdef.h" +#include "bacnet/npdu.h" +#include "bacnet/datalink/bvlc.h" + +/* specific defines for BACnet/IP over Ethernet */ +#define BIP_HEADER_MAX (1 + 1 + 2) +#ifndef BIP_MPDU_MAX +#define BIP_MPDU_MAX (BIP_HEADER_MAX + MAX_PDU) /* 1506 */ +#endif + +#define BVLL_TYPE_BACNET_IP (0x81) + +#ifdef __cplusplus +extern "C" { +#endif /* __cplusplus */ + +/* User must implement these platform-specific network functions */ + +/** + * @brief Initialize the UDP socket for BACnet/IP + * @param port - UDP port to bind to + * @return true on success, false on failure + */ +bool bip_socket_init(uint16_t port); + +/** + * @brief Send UDP packet + * @param dest_addr - destination IP address (4 bytes, network byte order) + * @param dest_port - destination port (host byte order) + * @param mtu - data buffer to send + * @param mtu_len - length of data + * @return number of bytes sent, or negative on error + */ +int bip_socket_send( + const uint8_t *dest_addr, + uint16_t dest_port, + const uint8_t *mtu, + uint16_t mtu_len); + +/** + * @brief Receive UDP packet (non-blocking) + * @param buf - buffer to store received data + * @param buf_len - maximum buffer size + * @param src_addr - pointer to store source IP (4 bytes, network byte order) + * @param src_port - pointer to store source port (host byte order) + * @return number of bytes received, 0 if no data, negative on error + */ +int bip_socket_receive( + uint8_t *buf, uint16_t buf_len, uint8_t *src_addr, uint16_t *src_port); + +/** + * @brief Close/cleanup the UDP socket + */ +void bip_socket_cleanup(void); + +/** + * @brief Get local network information + * @param local_addr - buffer for local IP (4 bytes) + * @param netmask - buffer for netmask (4 bytes) + * @return true on success + */ +bool bip_get_local_network_info(uint8_t *local_addr, uint8_t *netmask); + +/* BACnet/IP port functions */ +bool bip_init(uint16_t port); +void bip_set_interface(void); +void bip_cleanup(void); + +/* Convert uint8_t IPv4 to uint32 */ +uint32_t convertBIP_Address2uint32(const uint8_t *bip_address); +void convertUint32Address_2_uint8Address(uint32_t ip, uint8_t *address); + +/* common BACnet/IP functions */ +void bip_set_socket(uint8_t sock_fd); +uint8_t bip_socket(void); +bool bip_valid(void); +void bip_get_broadcast_address(BACNET_ADDRESS *dest); +void bip_get_my_address(BACNET_ADDRESS *my_address); + +/* function to send a packet out the BACnet/IP socket */ +int bip_send_pdu( + BACNET_ADDRESS *dest, + BACNET_NPDU_DATA *npdu_data, + uint8_t *pdu, + unsigned pdu_len); + +/* Function to send a packet out the BACnet/IP socket (Annex J) */ +int bip_send_mpdu( + const BACNET_IP_ADDRESS *dest, const uint8_t *mtu, uint16_t mtu_len); + +/* receives a BACnet/IP packet */ +uint16_t bip_receive( + BACNET_ADDRESS *src, uint8_t *pdu, uint16_t max_pdu, unsigned timeout); + +/* use host byte order for setting */ +void bip_set_port(uint16_t port); +uint16_t bip_get_port(void); + +/* use network byte order for setting */ +void bip_set_addr(const uint8_t *net_address); +uint8_t *bip_get_addr(void); + +/* use network byte order for setting */ +void bip_set_broadcast_addr(const uint8_t *net_address); +uint8_t *bip_get_broadcast_addr(void); + +/* gets an IP address by name */ +long bip_getaddrbyname(const char *host_name); + +void bip_debug_enable(void); +void bip_debug_disable(void); + +#ifdef __cplusplus +} +#endif /* __cplusplus */ + +#endif /* BIP_H */ diff --git a/ports/pico/bvlc-pico.c b/ports/pico/bvlc-pico.c new file mode 100644 index 00000000..f27bd3ff --- /dev/null +++ b/ports/pico/bvlc-pico.c @@ -0,0 +1,191 @@ +/** + * @file + * @authors Miguel Fernandes Testimony Adams + * + * @date 6 de Jun de 2013 + * @brief BACnet Virtual Link Control for Pico + */ + +#include +#include +#include + +#include "bvlc-pico.h" +#include "bip.h" +#include "bacnet/bacint.h" + +#define BVLC_RESULT 0 +#define BVLC_WRITE_BROADCAST_DISTRIBUTION_TABLE 1 +#define BVLC_READ_BROADCAST_DIST_TABLE 2 +#define BVLC_READ_BROADCAST_DIST_TABLE_ACK 3 +#define BVLC_FORWARDED_NPDU 4 +#define BVLC_REGISTER_FOREIGN_DEVICE 5 +#define BVLC_READ_FOREIGN_DEVICE_TABLE 6 +#define BVLC_READ_FOREIGN_DEVICE_TABLE_ACK 7 +#define BVLC_DELETE_FOREIGN_DEVICE_TABLE_ENTRY 8 +#define BVLC_DISTRIBUTE_BROADCAST_TO_NETWORK 9 +#define BVLC_ORIGINAL_UNICAST_NPDU 10 +#define BVLC_ORIGINAL_BROADCAST_NPDU 11 +#define BVLC_SECURE_BVLL 12 +#define BVLC_INVALID 255 + +#define BVLC_RESULT_SUCCESSFUL_COMPLETION 0x0000 +#define BVLC_RESULT_WRITE_BROADCAST_DISTRIBUTION_TABLE_NAK 0x0010 +#define BVLC_RESULT_READ_BROADCAST_DISTRIBUTION_TABLE_NAK 0x0020 +#define BVLC_RESULT_REGISTER_FOREIGN_DEVICE_NAK 0X0030 +#define BVLC_RESULT_READ_FOREIGN_DEVICE_TABLE_NAK 0x0040 +#define BVLC_RESULT_DELETE_FOREIGN_DEVICE_TABLE_ENTRY_NAK 0x0050 +#define BVLC_RESULT_DISTRIBUTE_BROADCAST_TO_NETWORK_NAK 0x0060 +#define BVLC_RESULT_INVALID 0xFFFF + +#define BVLL_TYPE_BACNET_IP (0x81) + +/** result from a client request */ +BACNET_BVLC_RESULT BVLC_Result_Code = BVLC_RESULT_SUCCESSFUL_COMPLETION; +/** The current BVLC Function Code being handled. */ +BACNET_BVLC_FUNCTION BVLC_Function_Code = BVLC_RESULT; /* A safe default */ + +/** Encode the BVLC Result message + * + * @param pdu - buffer to store the encoding + * @param result_code - BVLC result code + * + * @return number of bytes encoded + */ +static int bvlc_encode_bvlc_result(uint8_t *pdu, BACNET_BVLC_RESULT result_code) +{ + if (pdu) { + pdu[0] = BVLL_TYPE_BACNET_IP; + pdu[1] = BVLC_RESULT; + /* The 2-octet BVLC Length field is the length, in octets, + of the entire BVLL message, including the two octets of the + length field itself, most significant octet first. */ + encode_unsigned16(&pdu[2], 6); + encode_unsigned16(&pdu[4], (uint16_t)result_code); + } + + return 6; +} + +/** + * The common send function for bvlc functions, using b/ip. + * + * @param dest_addr - Points to an address containing the + * destination address (4 bytes, network byte order) + * @param dest_port - Destination port number + * @param mtu - the bytes of data to send + * @param mtu_len - the number of bytes of data to send + * @return Upon successful completion, returns the number of bytes sent. + * Otherwise, -1 shall be returned to indicate the error. + */ +static int bvlc_send_mpdu( + const uint8_t *dest_addr, + const uint16_t *dest_port, + uint8_t *mtu, + uint16_t mtu_len) +{ + int bytes_sent = 0; + + /* assumes that the driver has already been initialized */ + if (!bip_valid()) { + return -1; + } + + /* Send using platform-specific socket function */ + bytes_sent = bip_socket_send(dest_addr, *dest_port, mtu, mtu_len); + + return bytes_sent; +} + +/** Sends a BVLC Result + * + * @param dest_addr - destination address + * @param dest_port - destination port + */ +static void bvlc_send_result( + const uint8_t *dest_addr, + const uint16_t *dest_port, + BACNET_BVLC_RESULT result_code) +{ + uint8_t mtu[BIP_MPDU_MAX] = { 0 }; + uint16_t mtu_len = 0; + + mtu_len = (uint16_t)bvlc_encode_bvlc_result(&mtu[0], result_code); + bvlc_send_mpdu(dest_addr, dest_port, mtu, mtu_len); + + return; +} + +/** Note any BVLC_RESULT code, or NAK the BVLL message in the unsupported cases. + * Use this handler when you are not a BBMD. + * Sets the BVLC_Function_Code in case it is needed later. + * + * @param addr [in] Socket address to send any NAK back to. + * @param port [in] Socket port + * @param npdu [in] The received buffer. + * @param received_bytes [in] How many bytes in npdu[]. + * @return Non-zero BVLC_RESULT_ code if we sent a response (NAK) to this + * BVLC message. If zero, may need further processing. + */ +uint16_t bvlc_for_non_bbmd( + uint8_t *addr, uint16_t *port, uint8_t *npdu, uint16_t received_bytes) +{ + uint16_t result_code = 0; /* aka, BVLC_RESULT_SUCCESSFUL_COMPLETION */ + + /* To check the BVLC-function code, the buffer of received + * bytes has to be at least one byte long. */ + if (received_bytes >= 1) { + BVLC_Function_Code = npdu[1]; /* The BVLC function */ + switch (BVLC_Function_Code) { + case BVLC_RESULT: + if (received_bytes >= 6) { + /* This is the result of our foreign device registration */ + (void)decode_unsigned16(&npdu[4], &result_code); + BVLC_Result_Code = (BACNET_BVLC_RESULT)result_code; + fprintf(stderr, "BVLC: Result Code=%d\n", BVLC_Result_Code); + /* But don't send any response */ + result_code = 0; + } + break; + case BVLC_WRITE_BROADCAST_DISTRIBUTION_TABLE: + result_code = + BVLC_RESULT_WRITE_BROADCAST_DISTRIBUTION_TABLE_NAK; + break; + case BVLC_READ_BROADCAST_DIST_TABLE: + result_code = BVLC_RESULT_READ_BROADCAST_DISTRIBUTION_TABLE_NAK; + break; + case BVLC_REGISTER_FOREIGN_DEVICE: + result_code = BVLC_RESULT_REGISTER_FOREIGN_DEVICE_NAK; + break; + case BVLC_READ_FOREIGN_DEVICE_TABLE: + result_code = BVLC_RESULT_READ_FOREIGN_DEVICE_TABLE_NAK; + break; + case BVLC_DELETE_FOREIGN_DEVICE_TABLE_ENTRY: + result_code = BVLC_RESULT_DELETE_FOREIGN_DEVICE_TABLE_ENTRY_NAK; + break; + case BVLC_DISTRIBUTE_BROADCAST_TO_NETWORK: + result_code = BVLC_RESULT_DISTRIBUTE_BROADCAST_TO_NETWORK_NAK; + break; + default: + break; + } + } + + if (result_code > 0) { + bvlc_send_result(addr, port, result_code); + fprintf(stderr, "BVLC: NAK code=%d\n", result_code); + } + return result_code; +} + +/** Returns the current BVLL Function Code we are processing. + * We have to store this higher layer code for when the lower layers + * need to know what it is, especially to differentiate between + * BVLC_ORIGINAL_UNICAST_NPDU and BVLC_ORIGINAL_BROADCAST_NPDU. + * + * @return A BVLC_ function code, such as BVLC_ORIGINAL_UNICAST_NPDU. + */ +BACNET_BVLC_FUNCTION pico_bvlc_get_function_code(void) +{ + return BVLC_Function_Code; +} diff --git a/ports/pico/bvlc-pico.h b/ports/pico/bvlc-pico.h new file mode 100644 index 00000000..8e909f51 --- /dev/null +++ b/ports/pico/bvlc-pico.h @@ -0,0 +1,36 @@ +/** + * @file + * @authors Miguel Fernandes Testimony Adams + * + * @date 6 de Jun de 2013 + * @brief BACnet Virtual Link Control for Pico + */ + +#ifndef BVLC_PICO_H +#define BVLC_PICO_H + +#include +#include "bacnet/datalink/bvlc.h" + +#define BACNET_BVLC_RESULT uint8_t +#define BACNET_BVLC_FUNCTION uint8_t + +#ifdef __cplusplus +extern "C" { +#endif + +/* Maximum packet size for BACnet/IP */ +#ifndef BIP_MPDU_MAX +#define BIP_MPDU_MAX 1506 +#endif + +uint16_t bvlc_for_non_bbmd( + uint8_t *addr, uint16_t *port, uint8_t *npdu, uint16_t received_bytes); + +BACNET_BVLC_FUNCTION pico_bvlc_get_function_code(void); + +#ifdef __cplusplus +} +#endif + +#endif /* BVLC_PICO_H */ diff --git a/ports/pico/configure.sh b/ports/pico/configure.sh new file mode 100755 index 00000000..cbe8f651 --- /dev/null +++ b/ports/pico/configure.sh @@ -0,0 +1,32 @@ +#!/bin/bash +# configure.sh - Clone external dependencies required to build the Pico port + +set -e + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +EXTERNAL_DIR="${SCRIPT_DIR}/external" + +mkdir -p "${EXTERNAL_DIR}" + +PICO_SDK_DIR="${EXTERNAL_DIR}/pico-sdk" +PICO_SDK_URL="${PICO_SDK_URL:-https://github.com/raspberrypi/pico-sdk.git}" +PICO_SDK_REF="${PICO_SDK_REF:-2.2.0}" + +if [ -d "${PICO_SDK_DIR}/.git" ]; then + echo "Pico SDK already present at ${PICO_SDK_DIR}." + echo "Requested SDK ref: ${PICO_SDK_REF}" +else + echo "Cloning Raspberry Pi Pico SDK ${PICO_SDK_REF} ..." + git clone --branch "${PICO_SDK_REF}" --depth 1 "${PICO_SDK_URL}" \ + "${PICO_SDK_DIR}" + git -C "${PICO_SDK_DIR}" submodule update --init --depth 1 +fi + +echo "" +echo "External dependencies are ready in ${EXTERNAL_DIR}/" +echo "" +echo "To build:" +echo " cmake -S . -B build -DPICO_SDK_PATH=${PICO_SDK_DIR}" +echo " # Optional: override the default Pico 2 board selection" +echo " # cmake -S . -B build -DPICO_SDK_PATH=${PICO_SDK_DIR} -DPICO_BOARD=pico2_w" +echo " cmake --build build -- -j$(nproc)" diff --git a/ports/pico/dlenv.c b/ports/pico/dlenv.c new file mode 100644 index 00000000..565505dd --- /dev/null +++ b/ports/pico/dlenv.c @@ -0,0 +1,108 @@ +/************************************************************************** + * + * Copyright (C) 2025 Testimony Adams + * + * SPDX-License-Identifier: GPL-2.0-or-later WITH GCC-exception-2.0 + * + *********************************************************************/ + +#include "dlenv.h" +#include + +volatile struct mstp_port_struct_t MSTP_Port; + +/* Internal state associated with that port */ +static struct dlmstp_user_data_t MSTP_User_Data; +static uint8_t Input_Buffer[DLMSTP_MPDU_MAX]; +static uint8_t Output_Buffer[DLMSTP_MPDU_MAX]; + +static struct dlmstp_rs485_driver RS485_Driver = { + .init = rs485_init, + .send = rs485_bytes_send, + .read = rs485_byte_available, + .transmitting = rs485_rts_enabled, + .baud_rate = rs485_baud_rate, + .baud_rate_set = rs485_baud_rate_set, + .silence_milliseconds = rs485_silence_milliseconds, + .silence_reset = rs485_silence_reset +}; + +// TO BE IMPROVED +volatile bool g_mstp_have_token = false; +volatile uint8_t g_last_frame_type = 0; +volatile uint8_t g_last_src = 0; +volatile uint8_t g_last_dst = 0; + +static void mstp_frame_rx_complete_cb( + uint8_t src, uint8_t dst, uint8_t frame_type, uint8_t *buf, uint16_t len) +{ + (void)buf; + (void)len; + + g_last_src = src; + g_last_dst = dst; + g_last_frame_type = frame_type; + + // TO BE IMPROVED (For other frame types as needed) + if (frame_type == FRAME_TYPE_TOKEN) { + g_mstp_have_token = true; + } +} + +bool pico_dlenv_init(uint8_t mac_address) +{ + /* RS-485 low-level init (pins, UART config, etc.) */ + RS485_Driver.init(); + + /* Configure MSTP port structure that the core stack will drive */ + MSTP_Port.Nmax_info_frames = BACNET_MSTP_MAX_INFO_FRAMES; + MSTP_Port.Nmax_master = BACNET_MSTP_MAX_MASTER; + + MSTP_Port.InputBuffer = Input_Buffer; + MSTP_Port.InputBufferSize = sizeof(Input_Buffer); + MSTP_Port.OutputBuffer = Output_Buffer; + MSTP_Port.OutputBufferSize = sizeof(Output_Buffer); + + /* No ZeroConfig / slaves / auto-baud for this Pico port (you can tweak) */ + MSTP_Port.ZeroConfigEnabled = false; + MSTP_Port.SlaveNodeEnabled = false; + MSTP_Port.CheckAutoBaud = false; + + MSTP_Zero_Config_UUID_Init((struct mstp_port_struct_t *)&MSTP_Port); + + /* Hook the RS-485 driver into the MS/TP layer */ + MSTP_User_Data.RS485_Driver = &RS485_Driver; + MSTP_Port.UserData = &MSTP_User_Data; + + /* Start the MS/TP state machine using our port */ + if (!dlmstp_init((char *)&MSTP_Port)) { + return false; + } + + /* Apply logical MS/TP parameters */ + dlmstp_set_mac_address(mac_address); + dlmstp_set_baud_rate(RS485_BAUD_RATE); + // dlmstp_slave_mode_enabled_set(true); + dlmstp_set_max_master(BACNET_MSTP_MAX_MASTER); + dlmstp_set_max_info_frames(BACNET_MSTP_MAX_INFO_FRAMES); + dlmstp_set_frame_rx_complete_callback(mstp_frame_rx_complete_cb); + // TO BE IMPROVED (Add other callbacks as needed) + dlmstp_set_frame_not_for_us_rx_complete_callback(NULL); + dlmstp_set_invalid_frame_rx_complete_callback(NULL); + dlmstp_set_frame_rx_start_callback(NULL); + + return true; +} + +/* The stack may call this periodically; for this Pico MS/TP-only port + we don't need extra maintenance, so it's just a no-op. */ +void dlenv_maintenance_timer(uint16_t seconds) +{ + (void)seconds; +} + +/* Likewise, nothing special to clean up on embedded target */ +void dlenv_cleanup(void) +{ + /* no action */ +} diff --git a/ports/pico/dlenv.h b/ports/pico/dlenv.h new file mode 100644 index 00000000..15bb0c5c --- /dev/null +++ b/ports/pico/dlenv.h @@ -0,0 +1,42 @@ +/************************************************************************** + * + * Copyright (C) 2025 Testimony Adams + * + * SPDX-License-Identifier: GPL-2.0-or-later WITH GCC-exception-2.0 + * + *********************************************************************/ + +#ifndef DLENV_H +#define DLENV_H +#include +#include + +#include "bacnet/bacdef.h" +#include "bacnet/datalink/dlenv.h" +#include "bacnet/datalink/dlmstp.h" +#include "bacnet/datalink/mstp.h" +#include "bacnet/basic/sys/mstimer.h" + +#include "rs485.h" + +#define BACNET_MSTP_MAX_MASTER 5 +#define BACNET_MSTP_MAX_INFO_FRAMES 1 + +#ifdef __cplusplus +extern "C" { +#endif /* __cplusplus */ + +bool pico_dlenv_init(uint8_t mac_address); +void dlenv_maintenance_timer(uint16_t seconds); +void dlenv_cleanup(void); + +// TO BE IMPROVED +extern volatile bool g_mstp_have_token; +extern volatile uint8_t g_last_frame_type; +extern volatile uint8_t g_last_src; +extern volatile uint8_t g_last_dst; + +#ifdef __cplusplus +} +#endif /* __cplusplus */ +#endif diff --git a/ports/pico/main.c b/ports/pico/main.c new file mode 100644 index 00000000..a76f858a --- /dev/null +++ b/ports/pico/main.c @@ -0,0 +1,66 @@ +/************************************************************************** + * + * Copyright (C) 2025 Testimony Adams + * + * SPDX-License-Identifier: GPL-2.0-or-later WITH GCC-exception-2.0 + * + *********************************************************************/ + +#include + +#include "pico/stdlib.h" + +#include "dlenv.h" +#include "mstimer_init.h" + +#include "bacnet/basic/binding/address.h" +#include "bacnet/basic/object/device.h" +#include "bacnet/basic/services.h" +#include "bacnet/basic/tsm/tsm.h" +#include "bacnet/datalink/datalink.h" +#include "bacnet/npdu.h" + +static uint8_t PDUBuffer[MAX_MPDU + 16]; + +int main(void) +{ + uint16_t pdu_len = 0; + BACNET_ADDRESS src = { 0 }; + + stdio_init_all(); + systimer_init(); + + address_init(); + Device_Set_Object_Instance_Number(12345); + Device_Init(NULL); + + if (!pico_dlenv_init(2)) { + while (true) { + tight_loop_contents(); + } + } + + apdu_set_unrecognized_service_handler_handler(handler_unrecognized_service); + apdu_set_unconfirmed_handler(SERVICE_UNCONFIRMED_WHO_IS, handler_who_is); + apdu_set_unconfirmed_handler(SERVICE_UNCONFIRMED_WHO_HAS, handler_who_has); + apdu_set_confirmed_handler( + SERVICE_CONFIRMED_READ_PROPERTY, handler_read_property); + apdu_set_confirmed_handler( + SERVICE_CONFIRMED_READ_PROP_MULTIPLE, handler_read_property_multiple); + apdu_set_confirmed_handler( + SERVICE_CONFIRMED_WRITE_PROPERTY, handler_write_property); + apdu_set_confirmed_handler( + SERVICE_CONFIRMED_DEVICE_COMMUNICATION_CONTROL, + handler_device_communication_control); + + Send_I_Am(&Handler_Transmit_Buffer[0]); + + for (;;) { + pdu_len = datalink_receive(&src, &PDUBuffer[0], MAX_MPDU, 0); + if (pdu_len) { + npdu_handler(&src, &PDUBuffer[0], pdu_len); + } + tsm_timer_milliseconds(1); + tight_loop_contents(); + } +} diff --git a/ports/pico/mstimer_init.c b/ports/pico/mstimer_init.c new file mode 100644 index 00000000..eb931cbc --- /dev/null +++ b/ports/pico/mstimer_init.c @@ -0,0 +1,19 @@ +/************************************************************************** + * + * Copyright (C) 2025 Testimony Adams + * + * SPDX-License-Identifier: GPL-2.0-or-later WITH GCC-exception-2.0 + * + *********************************************************************/ + +#include "mstimer_init.h" + +void systimer_init(void) +{ + // +} + +unsigned long mstimer_now(void) +{ + return to_ms_since_boot(get_absolute_time()); +} diff --git a/ports/pico/mstimer_init.h b/ports/pico/mstimer_init.h new file mode 100644 index 00000000..0ba97ceb --- /dev/null +++ b/ports/pico/mstimer_init.h @@ -0,0 +1,25 @@ +/************************************************************************** + * + * Copyright (C) 2025 Testimony Adams + * + * SPDX-License-Identifier: GPL-2.0-or-later WITH GCC-exception-2.0 + * + *********************************************************************/ + +#ifndef SYSTIMER_H +#define SYSTIMER_H + +#include +#include "pico/time.h" +#include "bacnet/basic/sys/mstimer.h" + +#ifdef __cplusplus +extern "C" { +#endif /* __cplusplus */ +void systimer_init(void); + +#ifdef __cplusplus +} +#endif /* __cplusplus */ + +#endif diff --git a/ports/pico/rs485.c b/ports/pico/rs485.c new file mode 100644 index 00000000..a9d0b44f --- /dev/null +++ b/ports/pico/rs485.c @@ -0,0 +1,229 @@ +/************************************************************************** + * + * Copyright (C) 2025 Testimony Adams + * + * SPDX-License-Identifier: GPL-2.0-or-later WITH GCC-exception-2.0 + * + *********************************************************************/ + +/* BACnet Stack defines - first */ +#include "bacnet/bacdef.h" +/* BACnet Stack API */ +#include "bacnet/basic/sys/mstimer.h" +#include "bacnet/datalink/dlmstp.h" +#include "bacnet/basic/sys/fifo.h" +#include "rs485.h" +#include "mstimer_init.h" +#include + +// --- Static State Variables --- +static bool Rs485_RTS_Enabled = false; +static uint32_t Rs485_Baud_Rate = RS485_BAUD_RATE; +static volatile uint32_t Rs485_Bytes_Tx = 0; +static volatile uint32_t Rs485_Bytes_Rx = 0; + +/* amount of silence on the wire */ +static struct mstimer Silence_Timer; + +/** + * @brief Initialize the RS-485 UART and GPIO pins. + */ +void rs485_init(void) +{ + // 1. Initialize GPIO pins + gpio_set_function(RS485_TX_PIN, GPIO_FUNC_UART); + gpio_set_function(RS485_RX_PIN, GPIO_FUNC_UART); + + // Initialize DE/RE pin (RTS) as an output + gpio_init(RS485_DE_PIN); + gpio_set_dir(RS485_DE_PIN, GPIO_OUT); + rs485_rts_enable(false); // Set to receive (DE/RE low) + + // 2. Initialize UART + uart_init(RS485_UART_ID, Rs485_Baud_Rate); + uart_set_format(RS485_UART_ID, DATA_BIT, STOP_BIT, UART_PARITY_NONE); + uart_set_hw_flow(RS485_UART_ID, false, false); + uart_set_fifo_enabled(RS485_UART_ID, false); + + // 3. Reset silence timer + rs485_silence_reset(); + + // 4. Flush any junk data in the RX buffer from startup + while (uart_is_readable(RS485_UART_ID)) { + uart_getc(RS485_UART_ID); + } +} + +/** + * @brief Enables or disables the Request To Send (RTS) / Driver Enable (DE/RE) + * pin. + * @param enable True to enable transmitter (TX), False to enable receiver (RX). + */ +void rs485_rts_enable(bool enable) +{ + // DE=1: Driver Enable (TX mode) + // DE=0: Receiver Enable (RX mode) + gpio_put(RS485_DE_PIN, enable ? 1 : 0); + Rs485_RTS_Enabled = enable; +} + +/** + * @brief Returns the current state of the RTS/DE/RE pin. + * @return True if transmitter is enabled, False if receiver is enabled. + */ +bool rs485_rts_enabled(void) +{ + return Rs485_RTS_Enabled; +} + +/** + * @brief Attempts to read a single byte from the UART. + * @param data_register Pointer to store the received byte. + * @return True if a byte was received, False otherwise. + */ +bool rs485_byte_available(uint8_t *data_register) +{ + if (!uart_is_readable(RS485_UART_ID)) { + return false; + } + if (!data_register) { + // only checking availability — DO NOT CONSUME + // Edit to consume if needed + return true; + } + + *data_register = uart_getc(RS485_UART_ID); + Rs485_Bytes_Rx++; + rs485_silence_reset(); + return true; +} + +/** + * @brief Checks the UART hardware for a receive error (Framing, Parity, + * Overrun). + * @return True if a receive error is present, False otherwise. + */ +bool rs485_receive_error(void) +{ + // The RSR (Receive Status Register) holds error flags. + // It is part of the raw hardware registers, accessed via uart_get_hw(). + uart_hw_t *uart_hw = uart_get_hw(RS485_UART_ID); + + // RSR bits: 0: FE (Framing Error), 1: PE (Parity Error), 2: BE (Break + // Error), 3: OE (Overrun Error) + uint32_t rsr = uart_hw->rsr; + + // Clearing the RSR register is crucial after reading errors. + // Writing anything to it clears all error flags. + uart_hw->rsr = 0; + + // Check if any error bit is set + return (rsr != 0); +} + +/** + * @brief Sends a buffer of bytes over the UART. + * @param buffer Pointer to the data buffer. + * @param nbytes Number of bytes to send. + */ +void rs485_bytes_send(const uint8_t *buffer, uint16_t nbytes) +{ + // The BACnet stack will call rs485_rts_enable(true) before this function, + // and wait T_prop before sending. + // printf("TX %u bytes: ", nbytes); + // for (uint16_t i = 0; i < nbytes; i++) { + // printf("%02X ", buffer[i]); + // } + // printf("\r\n"); + rs485_rts_enable(true); + // TO BE IMPROVED (Implement custom_sleep) + sleep_us(200); + uart_write_blocking(RS485_UART_ID, buffer, nbytes); + uart_tx_wait_blocking(RS485_UART_ID); + sleep_us(200); + + // Update count and silence timer. + Rs485_Bytes_Tx += nbytes; + rs485_rts_enable(false); + rs485_silence_reset(); + + // The BACnet stack handles the post-transmit delay (T_prop + T_frame_gap) + // and calls rs485_rts_enable(false) to turn off the driver. +} + +/** + * @brief Returns the currently configured baud rate. + * @return The current baud rate. + */ +uint32_t rs485_baud_rate(void) +{ + return Rs485_Baud_Rate; +} + +/** + * @brief Sets a new baud rate for the UART. + * @param baud The new baud rate to set. + * @return True if the baud rate was successfully set, False otherwise. + */ +bool rs485_baud_rate_set(uint32_t baud) +{ + if (baud == 0) { + return false; + } + + // uart_set_baudrate returns the actual configured rate, which may be + // slightly off. We only care that the function executed successfully and we + // store the requested rate. + switch (baud) { + case 9600: + case 19200: + case 38400: + case 57600: + case 76800: + case 115200: + Rs485_Baud_Rate = baud; + uart_set_baudrate(RS485_UART_ID, Rs485_Baud_Rate); + break; + default: + return false; + break; + } + + return true; +} + +/** + * @brief Measures the duration of silence on the bus since the last byte (Tx or + * Rx). + * @return The duration of silence in milliseconds. + */ +uint32_t rs485_silence_milliseconds(void) +{ + return mstimer_elapsed(&Silence_Timer); +} + +/** + * @brief Resets the silence timer to the current time. + */ +void rs485_silence_reset(void) +{ + mstimer_set(&Silence_Timer, 0); +} + +/** + * @brief Gets the total number of bytes transmitted. + * @return The byte count. + */ +uint32_t rs485_bytes_transmitted(void) +{ + return Rs485_Bytes_Tx; +} + +/** + * @brief Gets the total number of bytes received. + * @return The byte count. + */ +uint32_t rs485_bytes_received(void) +{ + return Rs485_Bytes_Rx; +} diff --git a/ports/pico/rs485.h b/ports/pico/rs485.h new file mode 100644 index 00000000..5339539c --- /dev/null +++ b/ports/pico/rs485.h @@ -0,0 +1,52 @@ +/************************************************************************** + * + * Copyright (C) 2025 Testimony Adams + * + * SPDX-License-Identifier: GPL-2.0-or-later WITH GCC-exception-2.0 + * + *********************************************************************/ + +#ifndef RS485_H +#define RS485_H + +#include +#include +#include "pico/stdlib.h" +#include "hardware/uart.h" +#include "hardware/gpio.h" +#include "hardware/regs/uart.h" + +#define RS485_UART_ID uart1 +#define RS485_BAUD_RATE 38400 +#define RS485_TX_PIN 8 +#define RS485_RX_PIN 9 +#define RS485_DE_PIN 10 +#define DATA_BIT 8 +#define STOP_BIT 1 + +#ifdef __cplusplus +extern "C" { +#endif /* __cplusplus */ + +void rs485_rts_enable(bool enable); +bool rs485_rts_enabled(void); +bool rs485_byte_available(uint8_t *data_register); +bool rs485_receive_error(void); + +void rs485_bytes_send(const uint8_t *buffer, uint16_t nbytes); + +uint32_t rs485_baud_rate(void); +bool rs485_baud_rate_set(uint32_t baud); + +uint32_t rs485_silence_milliseconds(void); +void rs485_silence_reset(void); + +uint32_t rs485_bytes_transmitted(void); +uint32_t rs485_bytes_received(void); + +void rs485_init(void); + +#ifdef __cplusplus +} +#endif /* __cplusplus */ +#endif