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: alwaysFRAME_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_READis the board reporting its state to the PC.FRAME_CTRL_WRITEwill be the PC asking the board to apply a newcontrol_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.