Here's the video walkthrough, if you'd rather watch it:

Why I Started Using This Tool

The frame defines what a message looks like, but nothing sends it yet. I wanted the board to broadcast its frame on the local network at a steady interval, so any PC on the same subnet can listen without the board knowing its IP address. It's the same start, loop, and stop shape as the SW and HW alive tasks, so most of this file will look familiar. The new parts are the socket and the byte order.

What It Does

UDPTx_start opens a UDP socket, allows it to send to a broadcast address, fills in the destination address and port, and starts a thread. Each time around its loop, the thread takes a copy of the shared frame, converts every field to network byte order, and sends the 16 bytes with sendto(). UDPTx_stop stops the thread, closes the socket, and prints how many frames went out and how many failed. Here's the file, exactly as it is in the video:

#include "frame.h"
#include "udptx.h"

#include <stdio.h>
#include <string.h>
#include <errno.h>
#include <unistd.h>
#include <time.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <stdlib.h>

static UDPTx g_udptx = { .sock_fd = -1 };

/* Seconds from a clock that only ever moves forward. */
static time_t now_sec(void)
{
    struct timespec ts;

    // clock_gettime(CLOCK_MONOTONIC, &ts);
    return ts.tv_sec;
}

static void udptx_close(UDPTx *task)
{
    if (task->sock_fd >= 0)
    {
        close(task->sock_fd);
        task->sock_fd = -1;
    }
}

static void *udptx_thread(void *arg)
{
    UDPTx   *task = (UDPTx *)arg;
    Frame    frame;
    ssize_t  sent;
    UINT32   i;

    while (task->running)
    {
        /* Take a copy of the shared frame and put it in network byte order. */
        Frame_get(&frame);
        frame.header      = htonl(FRAME_HEADER);
        frame.ctrl_code   = htonl(FRAME_CTRL_READ);
        frame.control_reg = htonl(frame.control_reg);
        frame.status_reg  = htonl(frame.status_reg);

        sent = sendto(task->sock_fd, &frame, sizeof(frame), 0,
                      (struct sockaddr *)&task->dest, sizeof(task->dest));
        if (sent < 0)
        {
            task->error_count++;
            fprintf(stderr, "[UDPTx] sendto failed: %s header %u\n", strerror(errno), (unsigned int)frame.header);
        }
        else
        {
            task->sent_count++;
            printf("[UDPTx] sent frame %u header:  0x%04X\n", (unsigned int)task->sent_count, (unsigned int)frame.header);
        }

        /* Sleep one second at a time so Stop doesn't wait a full interval. */
        for (i = 0; i < task->interval_sec && task->running; i++)
        {
            sleep(1);
        }
    }

    return NULL;
}

INT32 UDPTx_start(void)
{
    INT32 rc;
    int   on = 1;

    if (g_udptx.running)
    {
        return 0;   /* already started, nothing to do */
    }

    g_udptx.sock_fd = socket(AF_INET, SOCK_DGRAM, 0);
    if (g_udptx.sock_fd < 0)
    {
        rc = errno;
        fprintf(stderr, "[UDPTx] socket failed: %s\n", strerror(rc));
        return rc;
    }

    /* Linux refuses to send to a broadcast address unless we ask first. */
    if (setsockopt(g_udptx.sock_fd, SOL_SOCKET, SO_BROADCAST, &on, sizeof(on)) < 0)
    {
        rc = errno;
        fprintf(stderr, "[UDPTx] SO_BROADCAST failed: %s\n", strerror(rc));
        udptx_close(&g_udptx);
        return rc;
    }

    memset(&g_udptx.dest, 0, sizeof(g_udptx.dest));
    g_udptx.dest.sin_family = AF_INET;
    g_udptx.dest.sin_port   = htons(FRAME_UDP_PORT);
    if (inet_pton(AF_INET, UDPTX_BCAST_ADDR, &g_udptx.dest.sin_addr) != 1)
    {
        fprintf(stderr, "[UDPTx] bad address %s\n", UDPTX_BCAST_ADDR);
        udptx_close(&g_udptx);
        return EINVAL;
    }

    g_udptx.interval_sec = UDPTX_INTERVAL_SEC;
    g_udptx.sent_count   = 0;
    g_udptx.error_count  = 0;
    g_udptx.start_time   = now_sec();
    g_udptx.running      = TRUE;

    printf("[UDPTx] started, broadcasting %u-byte frames to %s:%u every %u s\n",
           (unsigned int)sizeof(Frame), UDPTX_BCAST_ADDR,
           (unsigned int)FRAME_UDP_PORT, (unsigned int)g_udptx.interval_sec);

    rc = pthread_create(&g_udptx.thread_id, NULL, udptx_thread, &g_udptx);
    if (rc != 0)
    {
        g_udptx.running = FALSE;
        udptx_close(&g_udptx);
        fprintf(stderr, "[UDPTx] pthread_create failed: %s\n", strerror(rc));
        return rc;
    }

    return EXIT_SUCCESS;
}

void UDPTx_stop(void)
{
    if (!g_udptx.running)
    {
        return;     /* never started, or already stopped */
    }

    g_udptx.running = FALSE;
    pthread_join(g_udptx.thread_id, NULL);
    udptx_close(&g_udptx);

    printf("[UDPTx] stopped, sent %u frames, %u errors, uptime %ld s\n",
           (unsigned int)g_udptx.sent_count,
           (unsigned int)g_udptx.error_count,
           (long)(now_sec() - g_udptx.start_time));
}

The control block from udptx.h

udptx.h follows the same shape as hwalive.h. The UDPTx struct holds everything the task needs: sock_fd for the socket, dest (a struct sockaddr_in) for where frames go, running and thread_id for the thread, interval_sec for the pace, sent_count and error_count for statistics, and start_time for uptime. The header also defines UDPTX_BCAST_ADDR, the subnet's broadcast address as a string, and UDPTX_INTERVAL_SEC. As with led_fd in HW alive, sock_fd starts at -1, because 0 is a real file descriptor and a socket is just another file descriptor on Linux.

udptx_close: one place to clean up

Every error path in Start and the normal path in Stop need to close the socket, so that's one small helper. It only closes a descriptor that's actually open and then sets it back to -1, so calling it twice is harmless.

UDPTx_start: socket, permission, address, thread

  • socket(AF_INET, SOCK_DGRAM, 0) creates an IPv4 UDP socket. UDP has no connection and no handshake: every sendto() is one packet, and nothing waits for a reply. That's exactly what a status broadcast needs.
  • SO_BROADCAST. Linux won't let a socket send to a broadcast address unless you turn this option on first. Without it, every sendto() fails with "Permission denied", which is confusing if you don't know about it.
  • The destination address. dest is cleared with memset, then gets the family, the port, and the address. The port goes through htons() because the socket API expects it in network byte order. inet_pton() turns the text address into the 4-byte binary form and returns 1 only on success, so a typo in UDPTX_BCAST_ADDR is caught at start instead of silently sending nowhere.
  • Fill in the struct, then create the thread. Same rule as SW and HW alive: everything the thread reads is set before pthread_create(), and if thread creation fails, running goes back to FALSE and the socket is closed.

Each failure undoes only what succeeded before it, and returns the errno value so main() can report it. There's the same small inconsistency as in hwalive.c: success returns EXIT_SUCCESS where a plain 0 would match the rest.

The thread: copy, convert, send

Each pass starts with Frame_get(&frame), which copies the shared frame into a local variable under the mutex from the last post. From there the thread works on its own copy, so it never holds the lock while it's sending.

Then every field goes through htonl(), "host to network long". The ARM core stores a 32-bit value with its lowest byte first (little-endian), and network protocols put the highest byte first (big-endian). Without the conversion, 0x0000DEEF would leave the board as EF DE 00 00, and a PC would have to know it came from a little-endian machine to read it. With htonl(), it goes out as 00 00 DE EF and anyone can read it. The thread also writes header and ctrl_code from the constants instead of trusting the shared copy, so a frame from this task always says "this is our frame, and it's a read report".

sendto() sends all 16 bytes to dest. On failure it bumps error_count and logs the reason. It doesn't stop the thread, for the same reason as the HW alive LED write: if the network drops for a moment, I want the task to keep trying. Then it sleeps in one-second slices so Stop never waits more than about a second.

UDPTx_stop: join, then close

Stop clears running, joins the thread, and only then closes the socket. Closing it first would let the thread's next sendto() hit a closed descriptor, or a different file that reused the same number. After the join, g_udptx belongs to main() alone.

Two things I'll clean up

I left the code exactly as it was in the video, because both of these are worth seeing:

  • The uptime is garbage. The clock_gettime() call in now_sec() is commented out, so ts is never filled in and the function returns whatever happened to be on the stack. The uptime in the stop message is meaningless, and reading an uninitialized variable is undefined behavior in C. The fix is to uncomment the line, as it is in swalive.c and hwalive.c.
  • The log shows the header byte-swapped. Both log lines print frame.header after it has been through htonl(). The bytes in memory are now 00 00 DE EF, and when the little-endian CPU reads them back as a number, it gets 0xEFDE0000, not 0xDEEF. The packet on the wire is correct, only the log is misleading. The fix is to print FRAME_HEADER, or ntohl(frame.header), and to use %08X, since %04X is only a minimum width and doesn't match a 32-bit value.

Running it

Start it after the two heartbeats in main() and stop it first on the way out, so tasks shut down in reverse order:

if (UDPTx_start() != 0)
{
    fprintf(stderr, "could not start UDPTx task\n");
    HWAlive_stop();
    SWAlive_stop();
    return EXIT_FAILURE;
}

/* ... */

UDPTx_stop();
HWAlive_stop();
SWAlive_stop();

The terminal shows the start line with the broadcast address, port 5001, and the 16-byte frame size, then one line per frame. Notice the header in those lines reads 0xEFDE0000, which is the second bug above:

[UDPTx] sent frame 1 header:  0xEFDE0000
[UDPTx] sent frame 2 header:  0xEFDE0000
[UDPTx] sent frame 3 header:  0xEFDE0000

A terminal can only tell me that sendto() returned success. To see what actually arrived on the network, I need to look at the packets themselves, which is the next post.

Final Verdict

Once the task pattern is in place, adding a network sender is mostly about three socket details: turn on SO_BROADCAST before sending to a broadcast address, put the port through htons() and every multi-byte field through htonl(), and copy the shared data out before sending so the lock is never held across a system call. The two bugs are a good reminder too: a log line can be wrong while the data is right, and a commented-out line can quietly turn a number into noise. If you're following along, uncomment clock_gettime() before you run it.