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: everysendto()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, everysendto()fails with "Permission denied", which is confusing if you don't know about it.- The destination address.
destis cleared withmemset, then gets the family, the port, and the address. The port goes throughhtons()because the socket API expects it in network byte order.inet_pton()turns the text address into the 4-byte binary form and returns1only on success, so a typo inUDPTX_BCAST_ADDRis 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,runninggoes back toFALSEand 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 innow_sec()is commented out, sotsis 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 inswalive.candhwalive.c. - The log shows the header byte-swapped. Both log lines print
frame.headerafter it has been throughhtonl(). The bytes in memory are now00 00 DE EF, and when the little-endian CPU reads them back as a number, it gets0xEFDE0000, not0xDEEF. The packet on the wire is correct, only the log is misleading. The fix is to printFRAME_HEADER, orntohl(frame.header), and to use%08X, since%04Xis 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.