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

Why I Started Using This Tool

With the SW alive and HW alive tasks running, the board can tell me it's alive through a terminal and an LED. The next step is telling a PC on the network, and eventually letting the PC send commands back. Before writing any socket code, I wanted to pin down the one thing both sides have to agree on: the exact bytes in a message. That's the frame.

What It Does

frame.h defines a fixed 16-byte packet and the constants that identify it. frame.c holds the one shared copy of that packet and two functions, Frame_get and Frame_set, that read and write it safely from any thread. The UDPTx task in the next post will read it, and a future UDPRx task will update it. Here's the header:

#ifndef FRAME_H
#define FRAME_H

#include "mytype.h"

#define FRAME_UDP_PORT      5001u
#define FRAME_HEADER        0xDEEFu     /* "id of our frame:this is our frame" */
#define FRAME_CTRL_READ     0x01u       /* board -> HOST PC: status report */
#define FRAME_CTRL_WRITE    0x02u       /* PC HOST  -> board: apply control_reg */

typedef struct frame_s
{
    UINT32  header;
    UINT32  ctrl_code;
    UINT32  control_reg;
    UINT32  status_reg;
} Frame;

/* The shared frame: UDPTx reads it, UDPRx will update it. Thread safe. */
void Frame_get(Frame *out);
void Frame_set(const Frame *in);

#endif /* FRAME_H */

And the source:

#include <pthread.h>
#include "frame.h"

static Frame           g_frame = { .header = FRAME_HEADER };
static pthread_mutex_t g_frame_lock = PTHREAD_MUTEX_INITIALIZER;

/* Copy the whole shared frame out, so it's never half-updated. */
void Frame_get(Frame *out)
{
    pthread_mutex_lock(&g_frame_lock);
    *out = g_frame;
    pthread_mutex_unlock(&g_frame_lock);
}

/* Replace the whole shared frame. */
void Frame_set(const Frame *in)
{
    pthread_mutex_lock(&g_frame_lock);
    g_frame = *in;
    pthread_mutex_unlock(&g_frame_lock);
}

Four UINT32 fields, 16 bytes, no surprises

Every field is a UINT32 from the fixed-size types header. That's the whole reason those typedefs exist: a packet that goes over the network has to be the same size on the ARM board and on the PC, and int or long don't promise that. Four 32-bit fields in a row also means the compiler has no reason to add padding, so sizeof(Frame) is 16 and the struct in memory is exactly what goes on the wire.

  • header: always FRAME_HEADER (0xDEEF). It's a marker that says "this is one of our frames". If something else ever sends a packet to the same port, the receiver can drop it the moment the header doesn't match.
  • ctrl_code: what kind of message this is. FRAME_CTRL_READ is the board reporting its state to the PC. FRAME_CTRL_WRITE will be the PC asking the board to apply a new control_reg.
  • control_reg: the value the PC wants the board to act on.
  • status_reg: the value the board reports back.

The constants

FRAME_UDP_PORT is 5001, the port both sides use. The u suffix on every constant makes it unsigned, so comparing it with a UINT32 field never mixes signed and unsigned values. Keeping the port, the marker, and the control codes in one header means the sender, the receiver, and anything I write on the PC side all read from the same list.

One shared frame, one lock

g_frame is the single copy of the current frame, private to frame.c. The designated initializer sets header to FRAME_HEADER and everything else to 0, so even before anyone calls Frame_set, the frame is valid. The mutex is set up with PTHREAD_MUTEX_INITIALIZER, which means there's no init function to remember to call.

Why copy the whole struct

Frame_get and Frame_set don't hand out a pointer to g_frame. They copy all 16 bytes in or out while holding the lock. Without the lock, the sender could read control_reg from the old frame and status_reg from the new one if another thread was halfway through updating it. Copying under the lock means every reader gets a frame that was whole at one moment in time. Copying 16 bytes is cheap, the lock is held for almost no time, and callers never see the mutex at all, so they can't forget to unlock it.

What this file doesn't do: byte order

The shared frame is stored in the board's own byte order, which is little-endian on the DE10-Nano's ARM core. Networks use big-endian. frame.c deliberately doesn't deal with that. Converting is the sender's job, right before the bytes leave, which is exactly what the UDPTx task does in the next post. Keeping the shared copy in native order means any task can read status_reg and use it as a normal number.

Final Verdict

This is the smallest file in the project so far, and it's the one everything on the network depends on. Fixed-size fields make the packet the same on both ends, a header marker and a control code make it easy to recognize, and copying the whole struct under a mutex makes it safe to share between tasks. If you're building your own board-to-PC protocol, start here: agree on the bytes first, then write the sockets.