Here's the video walkthrough, if you'd rather watch it:
Why I Started Using This Tool
After the UDPTx episode my DE10-Nano was broadcasting its frame every second, and I could watch it in Python and in Wireshark. But it was a one-way conversation. The whole reason the frame has a control register is so the PC can tell the board what to do, and nothing on the board was listening yet. I switch between a desktop and a laptop on my bench, so I wanted any of my PCs to be able to send a command without recompiling the board. I also wanted those commands addressed to the board itself, not broadcast to everything on the network. When I sat down to write the receive side, one question stopped me: how do I stop a thread that is asleep waiting for data that may never come?
What It Does
udprx.h / udprx.c is a fourth task in the same Start / loop / Stop style as SW
alive, HW alive, and UDPTx. It:
- Binds a UDP socket to port 5001, the same
FRAME_UDP_PORTthe TX task broadcasts on. - Blocks in
recvfrom(). The thread sleeps inside the kernel and uses no CPU until a datagram arrives. - Accepts frames from any PC, and notes who sent each one. The PC should send straight to the board's IP address (unicast), not broadcast.
- Checks what it is. It must be 16 bytes, start with
0xDE10, and be aWRITEframe. - Updates the shared frame's
control_regwith the get, change, set pattern from episode #13. - Calls TODO hook functions with the new control register, so the next step only has to fill them in.
On the board you'll see:
[UDPRx] listening on UDP port 5001, accepting WRITE frames from any PC
[UDPRx] from 192.168.1.100 control 0x00000000 -> 0x00000001
[UDPRx] TODO: write 0x00000001 to FPGA
[UDPRx] from 192.168.1.101 control 0x00000001 -> 0x000000A5
[UDPRx] TODO: write 0x000000A5 to FPGA
...
[UDPRx] stopped, 3 frames applied, 61 dropped, 0 errors, uptime 60 s
Don't worry about the 61 dropped frames. We'll see exactly where they come from.
Sending vs listening: bind()
In episode #14 I said the TX socket didn't need bind(), because the kernel picks any free
local port for a socket that only sends. A receiving socket is different. The PC sends
to "board IP, port 5001", so our socket must claim port 5001, or the kernel has nowhere to deliver the
datagram and just throws it away. That's what bind() does: it ties the socket to a local
address and port.
We bind to INADDR_ANY, which means "any of my network interfaces". The board doesn't need
to know its own IP address, and that's handy, because DHCP can change it.
Blocking recvfrom
recvfrom() is the receiving mirror of sendto():
ssize_t recvfrom(int fd, void *buf, size_t len, int flags,
struct sockaddr *src_addr, socklen_t *addrlen);
- It blocks by default. If no datagram is waiting, the thread is put to sleep until one arrives. That's the whole point of giving RX its own thread: it can wait as long as it likes while main, SW alive, HW alive, and UDPTx keep running.
- It returns the number of bytes in the datagram, or
-1with the reason inerrno. - It fills in
src_addrwith the sender's IP and port. That's how we know who sent it, so we can log it, and later reply to it. addrlengoes in and comes out. You set it tosizeof(from)before every call, and the kernel writes back how much it used. Forgetting to reset it inside the loop is a classic bug.
With UDP, one recvfrom returns exactly one datagram. If the PC sends 16 bytes, we get those
16 bytes in one piece, never half a frame.
Any PC can send, but send to the board's IP
UDP has no login and no connection. Any PC that can reach port 5001 can send us a frame, and for this
project that's exactly what I want. I don't want to recompile the board every time I switch laptops. So
there's no sender filter. We just record the sender's address from
recvfrom and print it next to every command, so the log always shows who changed what.
What we do ask of the PC is to send to the board's own IP address, for example
192.168.1.50:5001. That's called unicast: one sender, one receiver. The PC
could broadcast its command to 255.255.255.255, and because we bind to
INADDR_ANY the board would still receive it. But then every device on the network gets a
copy, and if you ever have two boards on the bench, a broadcast would switch both of them. Unicast means
"this command is for this board".
Here's the surprise that explains those dropped frames: the board hears its own
broadcasts. UDPTx sends to 255.255.255.255:5001, and our RX socket is listening on
port 5001 of the same machine, so every second a status frame arrives from the board itself. We don't
need an IP check to get rid of it. It's a READ frame, and RX only acts on
WRITE frames, so the control-code check drops it. That's also why we count those drops
quietly instead of printing a line for each one. Printing every second would bury the log.
Be honest with yourself about what "any PC" means, though: anyone on your network can switch your board. On a bench network that's fine. Anywhere else you'd want something like a shared key and a checksum.
ntohl: the mirror of htonl
The PC sends every field big-endian (network order), just like our TX task does. The ARM core is
little-endian, so we convert each field we read with ntohl(), "network to host long". It's
the exact mirror of htonl(). Skip it, and a control value of 1 arrives as
0x01000000.
udprx.h
#ifndef UDPRX_H
#define UDPRX_H
#include <pthread.h>
#include <time.h>
#include <netinet/in.h>
#include "mytype.h"
typedef struct udprx_s
{
pthread_t thread_id;
volatile BOOLEAN running;
INT32 sock_fd;
time_t start_time;
UINT32 rx_count;
UINT32 drop_count;
UINT32 error_count;
} UDPRx;
INT32 UDPRx_start(void);
void UDPRx_stop(void);
#endif /* UDPRX_H */
Compared with UDPTx:
- No
dest, and no IP address at all. TX needed to know where to send. RX doesn't need to know anyone in advance, becauserecvfromtells it who sent each frame. - No
interval_sec. RX doesn't run on a timer. It runs when data arrives. - Three counters.
rx_countcounts frames we actually applied,drop_countcounts frames we rejected, anderror_countcounts real socket errors. When something goes wrong, those three numbers tell you where to look.
udprx.c
#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 "frame.h"
#include "udprx.h"
static UDPRx g_udprx = { .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 udprx_close(UDPRx *task)
{
if (task->sock_fd >= 0)
{
close(task->sock_fd);
task->sock_fd = -1;
}
}
/* ---- TODO hooks: called when new control data arrives ---- */
/* control_reg is what the PC wants written into the FPGA. */
static void udprx_todo_fpga_write(UINT32 control_reg)
{
/* TODO: write control_reg into the FPGA. */
printf("[UDPRx] TODO: write 0x%08X to FPGA\n", (unsigned int)control_reg);
}
/* One place to handle new control data. More hooks can be added here later. */
static void udprx_on_control(UINT32 control_reg)
{
udprx_todo_fpga_write(control_reg);
}
static void *udprx_thread(void *arg)
{
UDPRx *task = (UDPRx *)arg;
Frame rx;
Frame frame;
struct sockaddr_in from;
socklen_t from_len;
ssize_t got;
UINT32 old_reg;
char ip[INET_ADDRSTRLEN];
while (task->running)
{
/* Blocks here until a datagram arrives, or until Stop wakes us up. */
from_len = sizeof(from);
got = recvfrom(task->sock_fd, &rx, sizeof(rx), 0,
(struct sockaddr *)&from, &from_len);
if (!task->running)
{
break; /* woken up by UDPRx_stop */
}
if (got < 0)
{
if (errno != EINTR)
{
task->error_count++;
fprintf(stderr, "[UDPRx] recvfrom failed: %s\n", strerror(errno));
}
continue;
}
/* Any PC may send. Keep its address as text for the log. */
inet_ntop(AF_INET, &from.sin_addr, ip, sizeof(ip));
/* 1. Exactly one frame, with our header. */
if (got != (ssize_t)sizeof(Frame) || ntohl(rx.header) != FRAME_HEADER)
{
task->drop_count++;
printf("[UDPRx] bad frame from %s (%ld bytes)\n", ip, (long)got);
continue;
}
/* 2. Only WRITE frames carry new control data.
This also quietly drops our own READ broadcasts. */
if (ntohl(rx.ctrl_code) != FRAME_CTRL_WRITE)
{
task->drop_count++;
continue;
}
/* Good frame: get, change, set. */
Frame_get(&frame);
old_reg = frame.control_reg;
frame.control_reg = ntohl(rx.control_reg);
Frame_set(&frame);
task->rx_count++;
printf("[UDPRx] from %s control 0x%08X -> 0x%08X\n", ip,
(unsigned int)old_reg, (unsigned int)frame.control_reg);
udprx_on_control(frame.control_reg);
}
return NULL;
}
INT32 UDPRx_start(void)
{
INT32 rc;
int on = 1;
struct sockaddr_in local;
if (g_udprx.running)
{
return 0; /* already started, nothing to do */
}
g_udprx.sock_fd = socket(AF_INET, SOCK_DGRAM, 0);
if (g_udprx.sock_fd < 0)
{
rc = errno;
fprintf(stderr, "[UDPRx] socket failed: %s\n", strerror(rc));
return rc;
}
/* Let us restart the app straight away without "Address already in use". */
if (setsockopt(g_udprx.sock_fd, SOL_SOCKET, SO_REUSEADDR, &on, sizeof(on)) < 0)
{
rc = errno;
fprintf(stderr, "[UDPRx] SO_REUSEADDR failed: %s\n", strerror(rc));
udprx_close(&g_udprx);
return rc;
}
/* Claim port 5001 on every interface. */
memset(&local, 0, sizeof(local));
local.sin_family = AF_INET;
local.sin_port = htons(FRAME_UDP_PORT);
local.sin_addr.s_addr = htonl(INADDR_ANY);
if (bind(g_udprx.sock_fd, (struct sockaddr *)&local, sizeof(local)) < 0)
{
rc = errno;
fprintf(stderr, "[UDPRx] bind to port %u failed: %s\n",
(unsigned int)FRAME_UDP_PORT, strerror(rc));
udprx_close(&g_udprx);
return rc;
}
g_udprx.rx_count = 0;
g_udprx.drop_count = 0;
g_udprx.error_count = 0;
g_udprx.start_time = now_sec();
g_udprx.running = TRUE;
printf("[UDPRx] listening on UDP port %u, accepting WRITE frames from any PC\n",
(unsigned int)FRAME_UDP_PORT);
rc = pthread_create(&g_udprx.thread_id, NULL, udprx_thread, &g_udprx);
if (rc != 0)
{
g_udprx.running = FALSE;
udprx_close(&g_udprx);
fprintf(stderr, "[UDPRx] pthread_create failed: %s\n", strerror(rc));
return rc;
}
return 0;
}
void UDPRx_stop(void)
{
if (!g_udprx.running)
{
return; /* never started, or already stopped */
}
g_udprx.running = FALSE;
/* The thread is probably asleep in recvfrom. Wake it up. */
shutdown(g_udprx.sock_fd, SHUT_RDWR);
pthread_join(g_udprx.thread_id, NULL);
udprx_close(&g_udprx);
printf("[UDPRx] stopped, %u frames applied, %u dropped, %u errors, uptime %ld s\n",
(unsigned int)g_udprx.rx_count,
(unsigned int)g_udprx.drop_count,
(unsigned int)g_udprx.error_count,
(long)(now_sec() - g_udprx.start_time));
}
UDPRx_start, step by step
The same "fail early, undo only what's done" pattern as UDPTx_start:
- Already running? Return
0. socket(AF_INET, SOCK_DGRAM, 0)gives us an IPv4 UDP socket.SO_REUSEADDR. Without it, if you stop and restart the app quickly,bindcan sometimes fail withAddress already in use. It's a one-line quality-of-life fix.bind()toINADDR_ANY:5001. Notehtonsfor the port andhtonlfor the address. Both go into the struct in network order. If another program is already holding port 5001, this is where you find out.- Reset the counters, set
running = TRUE, thenpthread_create. Same order as TX: the flag is set before the thread exists.
The thread loop
Each time round the loop:
- Reset
from_lenand callrecvfrom. The thread sleeps here until a datagram arrives. - Check
runningfirst. If Stop woke us up, leave straight away. got < 0is a real error. Count it and go round again.EINTRjust means a signal interrupted the wait, so it's not counted.- Note who sent it. There's no sender check, because any PC is welcome.
inet_ntopturns the sender's 4-byte address into text like192.168.1.100, only so we can print it. - Filter 1, size and header. It must be exactly 16 bytes and start with
0xDE10. Anything else is someone else's traffic, or a bug in our sender. - Filter 2, the control code. Only
FRAME_CTRL_WRITEframes change the control register. EveryREADframe is ignored, and that includes the board's own broadcast, which arrives here once a second. - Get, change, set. Copy the shared frame out, remember the old
control_reg(only for the log line), put in the new one, and write the whole frame back. We deliberately don't copy the PC'sstatus_reg. Status belongs to the board, and the PC doesn't get to write it. - Call
udprx_on_control(new).
The TODO hooks: control goes to the FPGA, status comes from it
The user LED is already handled by the HW alive task, so the control register isn't for the LED. The DE10-Nano is an ARM plus an FPGA on one chip, and our two registers map onto it like this:
| Register | Direction | Job |
|---|---|---|
control_reg |
PC โ board โ FPGA | A value the PC wants written into the FPGA |
status_reg |
FPGA โ board โ PC | A value read back from the FPGA and broadcast by UDPTx |
This episode only builds the receive half of that path:
udprx_on_control(control_reg)is the one place new control data arrives. Today it just calls one hook. When there's more to do with the value later, it gets added here and the receive loop stays the same.udprx_todo_fpga_write(control_reg)is the stub. For now it only prints what it would write. It's called for every acceptedWRITEframe, even if the value didn't change. That way, sending the same value again re-writes the FPGA, which is handy if the FPGA was reloaded.status_regisn't touched by RX at all. Filling it with values read from the FPGA is a separate job. Whatever does it will use the sameFrame_get/ changestatus_reg/Frame_setpattern thatmain.cuses today, and UDPTx will broadcast it with no changes.
Right now the hook only prints a TODO line, and that's on purpose. We can test the whole
receive path, header check, byte order, and shared frame, before touching the FPGA. When the FPGA write
is filled in, none of the networking code has to change.
One thing to remember: the hooks run on the RX thread. If a hook blocks for a long time, no new frames get received until it returns. Keep them quick.
UDPRx_stop and the blocking problem
Here's the catch with a blocking recvfrom. In the other tasks, Stop sets
running = FALSE, and within a second the thread wakes from sleep(1), sees the
flag, and exits. The RX thread isn't sleeping on a timer. It's sleeping inside recvfrom,
waiting for a datagram that might never come. If we only clear the flag,
pthread_join would wait forever and the app would hang on exit.
So after clearing the flag we call shutdown(sock_fd, SHUT_RDWR). On Linux
this marks the socket as shut down and wakes up any thread blocked on it. recvfrom returns
straight away, the loop sees running == FALSE, and the thread exits. On an unconnected UDP
socket, shutdown itself returns -1 with ENOTCONN, but it still does
the wake-up, so we ignore the return value.
Then it's the same order as every task before it: join first, then close the socket,
then print the totals. Why not just close() the socket to wake the thread? Because closing a
file descriptor while another thread is still using it is a race. The number can get reused by the next
open() somewhere else in the program.
The other common answer is a receive timeout: setsockopt(..., SO_RCVTIMEO,
...) with one second makes recvfrom give up every second, just like
sleep(1) in the other tasks. It works, but the thread then wakes up every second for
nothing. With shutdown the thread stays truly asleep until there's real work.
Wiring it into main.c
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include "frame.h"
#include "swalive.h"
#include "hwalive.h"
#include "udptx.h"
#include "udprx.h"
int main(void)
{
Frame frame;
setvbuf(stdout, NULL, _IOLBF, 0);
if (SWAlive_start() != 0)
{
fprintf(stderr, "could not start SW alive task\n");
return EXIT_FAILURE;
}
if (HWAlive_start() != 0)
{
fprintf(stderr, "could not start HW alive task\n");
SWAlive_stop();
return EXIT_FAILURE;
}
/* Status bit 0 = "I'm running". */
Frame_get(&frame);
frame.status_reg = 0x1u;
Frame_set(&frame);
if (UDPTx_start() != 0)
{
fprintf(stderr, "could not start UDP TX task\n");
HWAlive_stop();
SWAlive_stop();
return EXIT_FAILURE;
}
if (UDPRx_start() != 0)
{
fprintf(stderr, "could not start UDP RX task\n");
UDPTx_stop();
HWAlive_stop();
SWAlive_stop();
return EXIT_FAILURE;
}
printf("Main running for 60 s.\n");
sleep(60);
UDPRx_stop();
UDPTx_stop();
HWAlive_stop();
SWAlive_stop();
return EXIT_SUCCESS;
}
- Start order, stop order reversed. RX starts last and stops first, and every failure path stops only what already started.
- 60 seconds instead of 20, so you have time to send a few commands from the PC.
- The Makefile needs no changes.
$(wildcard src/*.c)picks upudprx.c.
A nice side effect: control_reg lives in the shared frame, and UDPTx broadcasts that frame
every second. So once the PC writes a new value, it shows up in listen.py and in Wireshark
within a second. That's a free "command received" confirmation. Sending that value from the PC with a
small send.py is the next post.
Final Verdict
The part that taught me the most wasn't receiving data. It was stopping. My first version just cleared
the flag like the other tasks, and the app hung on exit because the thread was still waiting in
recvfrom. shutdown() fixed that cleanly, and it's worth knowing that just
clearing a flag doesn't work for any thread that blocks. The second lesson was seeing my own broadcasts
show up as dropped frames. It looked like a bug until I understood it, and now the drop counter is one of
my favourite debugging tools. I also like stopping at TODO hooks. I could test the whole receive path
from my PC before touching the FPGA, and the FPGA write only has to be filled into one function. To sum
up: bind to INADDR_ANY:5001, reset from_len before every
recvfrom, filter on what the frame is rather than who sent it, ntohl what you
receive, and wake a blocked thread with shutdown() before you join it.