File Descriptors
Overview
The NetBurner environment provides a unified I/O abstraction layer through file descriptors (FDs), seamlessly integrating the RTOS, TCP/IP stack, and hardware peripherals into a standard POSIX-like interface. A file descriptor serves as a handle to various I/O resources including network sockets, serial ports, system peripherals, and custom I/O devices.
This unified approach enables consistent I/O operations across different resource types using standard functions like read(), write(), select(), and close().
File Descriptor Architecture
Application Layer
│
v
┌─────────────────────────────────────────────────────┐
│ File Descriptor Interface │
└──────┬──────────────────────────────────────────────┘
│
v
┌─────────────────────────────────────────────────────┐
│ File Descriptor Table (0-254) │
├─────────────────────────────────────────────────────┤
│ Standard I/O │ Serial │ Network │ Expansion │
│ (0-2) │ (3-4) │ (5-128) │ (129-250) │
└──────┬─────────┴─────┬────┴─────┬─────┴──────┬──────┘
│ │ │ │
v v v v
┌────────┐ ┌─────────┐ ┌─────┐ ┌──────────┐
│ stdio │ │ UART │ │ TCP │ │ Custom │
│ │ │ Ports │ │Stack│ │ Drivers │
└────────┘ └─────────┘ └─────┘ └──────────┘
int read(int fd, char *buf, int nbytes)
Read data from a file descriptor (fd).
int close(int fd)
Close the specified file descriptor and free the associated resources.
int select(int nfds, fd_set *readfds, fd_set *writefds, fd_set *errorfds, unsigned long timeout)
Wait for events to occur on one or more I/O resources associated with a set of file descriptors (fds)...
int write(int fd, const char *buf, int nbytes)
Write data to the stream associated with a file descriptor (fd). Can be used to write data to stdio,...
File Descriptor Allocation
NetBurner provides a maximum of 255 file descriptors (0-254), organized into specific ranges:
Allocation Map
┌────────────────────────────────────────────────────────────┐
│ File Descriptor Space │
├─────────┬──────────┬──────────────────┬────────────────────┤
│ 0-2 │ 3-4 │ 5-128 │ 129-250 │
├─────────┼──────────┼──────────────────┼────────────────────┤
│ Standard│ Serial │ TCP │ Expansion │
│ I/O │ Ports │ Sockets │ Pool │
├─────────┼──────────┼──────────────────┼────────────────────┤
│ 3 │ 2 │ 124 │ 122 │
│ FDs │ FDs │ FDs │ FDs │
└─────────┴──────────┴──────────────────┴────────────────────┘
Fixed Fixed 32 TCP Available for
System UART 0-1 Connections Custom Use
Detailed Breakdown
| Range | Count | Purpose | Description |
| 0-2 | 3 | Standard I/O | stdin (0), stdout (1), stderr (2) |
| 3-4 | 2 | Serial Ports | UART 0 (fd=3), UART 1 (fd=4) |
| 5-128 | 124 | TCP Sockets | Network connections (32 sockets) |
| 129-250 | 122 | Expansion | Custom drivers, additional UARTs, buffers |
Expansion Pool Usage
The expansion range (129-250) can accommodate:
Expansion File Descriptors (129-250)
┌──────────────┐ ┌──────────────┐ ┌──────────────┐
│ External │ │ Additional │ │ Custom │
│ UARTs │ │ TCP Sockets │ │ Buffers │
│ │ │ │ │ │
│ SPI-to- │ │ Beyond the │ │ Ring │
│ Serial │ │ 32 default │ │ Buffers │
└──────────────┘ └──────────────┘ └──────────────┘
┌──────────────┐ ┌──────────────┐ ┌──────────────┐
│
I2C │ │ Custom │ │ Virtual │
│ Devices │ │ Protocols │ │ Devices │
│ │ │ │ │ │
│
I2C bus │ │ Proprietary │ │ Memory │
│ access │ │ interfaces │ │ streams │
└──────────────┘ └──────────────┘ └──────────────┘
I2C Peripheral Class.
Definition i2c.h:213
Creating Custom I/O Drivers
Driver Architecture
Custom file descriptors enable integration of any I/O device into the standard file descriptor framework.
Custom Driver Implementation Flow
┌─────────────────────────────────────────────────────┐
│ Your Custom Driver │
├─────────────────────────────────────────────────────┤
│ │
│ ┌──────────────┐ ┌──────────────┐ ┌───────────┐ │
│ │ function │ │ function │ │ function │ │
│ └──────┬───────┘ └──────┬───────┘ └─────┬─────┘ │
│ │ │ │ │
│ └─────────────────┴────────────────┘ │
│ │ │
│ v │
│ ┌─────────────────────────┐ │
│ │ IoExpandStruct │ │
│ │ • Function pointers │ │
│ │ • Extra data pointer │ │
│ └───────────┬─────────────┘ │
└────────────────────────┼────────────────────────────┘
│
v
┌──────────────────────┐
│ Returns: fd handle │
└──────────────────────┘
│
v
┌──────────────────────┐
│ File Descriptor │
│ Now Available │
└──────────────────────┘
int GetExtraFD(void *extra_data, struct IoExpandStruct *pFuncs)
Returns a file descriptor for the structure passed as the IoExpandStruct. FreeExtraFd( ) will release...
Core Header File
State Management Functions
Functions to control the state of your file descriptor:
State Control Functions
┌─────────────────────┐ ┌─────────────────────┐
│ SetDataAvail() │ │ ClrDataAvail() │
│ Data ready to
read │ │ No data available │
└─────────────────────┘ └─────────────────────┘
┌─────────────────────┐ ┌─────────────────────┐
│ SetWriteAvail() │ │ ClrWriteAvail() │
└─────────────────────┘ └─────────────────────┘
┌─────────────────────┐ ┌─────────────────────┐
│ SetHaveError() │ │ ClrHaveError() │
│ Error condition │ │ No error │
└─────────────────────┘ └─────────────────────┘
Purpose:** These functions update the state of your file descriptor for use with I/O functions such as select(), read(), and write().
IoExpandStruct Definition
The core structure for custom file descriptor implementation:
struct IoExpandStruct
{
int (*read)(int fd, char *buf, int nbytes);
int (*write)(int fd, const char *buf, int nbytes);
int (*close)(int fd);
void *extra;
};
Structure Members
| Member | Type | Purpose |
read | Function pointer | Implements read operations for your device |
write | Function pointer | Implements write operations for your device |
close | Function pointer | Cleanup and release resources |
extra | void* | Optional pointer to device-specific data |
Data Flow
Application Your Driver
│ │
│
read(fd, buf, nbytes) │
├─────────────────────────────────>│
│ v
│ ┌────────────────────────┐
│ │ Your
read() function │
│ │ • Access device │
│ │ • Fill buffer │
│ │ • Return bytes
read │
│ └─────────────┬──────────┘
│ │
│<─────────────────────────────────┤
│ │
│
write(fd, buf, nbytes) │
├─────────────────────────────────>│
│ v
│ ┌────────────────────────┐
│ │ Your
write() function │
│ │ • Write to device │
│ │ • Return bytes written │
│ └─────────────┬──────────┘
│ │
│<─────────────────────────────────┤
│ Returns: bytes written │
File Descriptor Management API
Core Functions
int GetExtraFD(
void *extra_data,
struct IoExpandStruct *pFuncs);
void FreeExtraFd(int fd)
Free a file descriptor and associated resources.
void * GetExtraData(int fd)
Returns the extra structure value from IoExpandStruct associated with the file descriptor.
Function Details
GetExtraFD()
Purpose:** Allocate a file descriptor for your custom I/O device.
Parameters:**
extra_data: Optional pointer to device-specific data
pFuncs: Pointer to IoExpandStruct containing your function implementations
Returns:** File descriptor handle (int), or -1 on failure
Usage Example:**
IoExpandStruct myDeviceFuncs = {
.read = myReadFunction,
.write = myWriteFunction,
.close = myCloseFunction,
.extra = &myDeviceData
};
int fd =
GetExtraFD(&myDeviceData, &myDeviceFuncs);
if (fd < 0) {
printf("Failed to allocate file descriptor\n");
}
GetExtraData()
Purpose:** Retrieve the custom data pointer associated with a file descriptor.
Parameters:**
FreeExtraFd()
Purpose:** Release a file descriptor back to the available pool.
Parameters:**
void myCloseFunction(int fd) {
if (device) {
device->shutdown();
}
}
TCP Socket Notifications
Callback System
NetBurner provides a callback mechanism for TCP socket events:
TCP Event Flow
TCP Socket
│
│ Data arrives or
│ Error occurs
v
┌───────────────────────────────┐
│ System TCP Stack │
└───────────────┬───────────────┘
│
│ Trigger
v
┌───────────────────────────────┐
│ tcp_read_notify_handler │
│ Your callback function │
└───────────────┬───────────────┘
│
v
┌───────────────────────────────┐
│ Your Application Logic │
│ • Process new data │
│ • Handle error condition │
└───────────────────────────────┘
Callback Definition
typedef void (tcp_read_notify_handler)(int tcp_fd);
void RegisterTCPReadNotify(int tcpFd, tcp_notify_handler *notifyHandler)
Register a callback for read event notifications.
Callback Parameters
When your callback is invoked:
| Condition | tcp_fd Value | Meaning |
| Data available | fd >= 0 | Socket has new data to read |
| Error state | fd < 0 | TCP connection has entered error state |
Implementation Example
void myTcpNotifyHandler(int tcp_fd) {
if (tcp_fd >= 0) {
char buffer[256];
int bytesRead =
read(tcp_fd, buffer,
sizeof(buffer));
if (bytesRead > 0) {
processIncomingData(buffer, bytesRead);
}
} else {
printf("TCP socket error detected\n");
handleTcpError();
}
}
void setupTcpSocket(int tcp_fd) {
}
Using select() with File Descriptors
Multiplexing Multiple I/O Sources
One of the most powerful features of the file descriptor system is the ability to wait on multiple I/O sources simultaneously using select().
┌─────────────────────────────────────────────────────┐
│ Application Thread │
│ │
│ │ │
│ │ Blocks until activity │
│ v │
│ ┌──────────────────────────────────────────────┐ │
│ │ Waiting on Multiple FDs: │ │
│ │ │ │
│ │ fd3 (Serial) fd5 (TCP) fd129 (Custom) │ │
│ │ │ │ │ │ │
│ └──────┼────────────┼────────────┼─────────────┘ │
│ │ │ │ │
└─────────┼────────────┼────────────┼─────────────────┘
│ │ │
v v v
┌───────┐ ┌───────┐ ┌───────┐
│ UART │ │ TCP │ │Custom │
│Device │ │Socket │ │Driver │
└───────┘ └───────┘ └───────┘
select() Usage Pattern
#include <iosys.h>
void monitorMultipleSources() {
int serial_fd = 3;
int tcp_fd = 5;
int custom_fd = 129;
fd_set readfds;
while (1) {
int max_fd = custom_fd;
int result =
select(max_fd + 1, &readfds, NULL, NULL, NULL);
if (result > 0) {
handleSerialData(serial_fd);
}
handleTcpData(tcp_fd);
}
handleCustomDevice(custom_fd);
}
}
}
}
int FD_ISSET(int fd, fd_set *pfds)
A fd_set (file descriptor set) holds a set of file descriptors (fds). This function checks whether or...
void FD_SET(int fd, fd_set *pfds)
A fd_set (file descriptor set) holds a set of file descriptors (fds). This function sets or adds a sp...
void FD_ZERO(fd_set *pfds)
Clear (set to 0) a fd_set (file descriptor set) so no file descriptors (fds) are selected.
Mixing Device Types
You can combine any file descriptor types in a single select() call:
Mixed Device Selection
┌────────────────────────┐
└───────────┬────────────┘
│
┌───────────┴───────────┐
│ │
v v
┌───────────────┐ ┌───────────────┐
│ Network │ │ Serial │
│ Sockets │ │ Ports │
│ │ │ │
│ TCP │ │ UART 0 │
│ UDP │ │ UART 1 │
└───────┬───────┘ └───────┬───────┘
│ │
└───────────┬───────────┘
│
v
┌───────────────────────┐
│ Custom Devices │
│ │
│ External UART │
│ Ring Buffers │
│ Virtual Devices │
└───────────────────────┘
Complete Implementation Example
Custom Ring Buffer Device
Here's a complete example of implementing a ring buffer as a file descriptor:
#include <iointernal.h>
#include <nbrtos.h>
typedef struct {
char buffer[1024];
int read_pos;
int write_pos;
int count;
OS_CRIT critical_section;
} RingBufferDevice;
int ringBufferRead(int fd, char *buf, int nbytes) {
RingBufferDevice *dev = (RingBufferDevice*)
GetExtraData(fd);
if (!dev) return -1;
OSCritEnter(&dev->critical_section, CRIT_FLAG_NONE);
int bytes_to_read = (nbytes < dev->count) ? nbytes : dev->count;
int bytes_read = 0;
for (int i = 0; i < bytes_to_read; i++) {
buf[i] = dev->buffer[dev->read_pos];
dev->read_pos = (dev->read_pos + 1) % 1024;
dev->count--;
bytes_read++;
}
if (dev->count == 0) {
ClrDataAvail(fd);
}
OSCritLeave(&dev->critical_section);
return bytes_read;
}
int ringBufferWrite(int fd, const char *buf, int nbytes) {
RingBufferDevice *dev = (RingBufferDevice*)
GetExtraData(fd);
if (!dev) return -1;
OSCritEnter(&dev->critical_section, CRIT_FLAG_NONE);
int space_available = 1024 - dev->count;
int bytes_to_write = (nbytes < space_available) ? nbytes : space_available;
int bytes_written = 0;
for (int i = 0; i < bytes_to_write; i++) {
dev->buffer[dev->write_pos] = buf[i];
dev->write_pos = (dev->write_pos + 1) % 1024;
dev->count++;
bytes_written++;
}
if (dev->count > 0) {
SetDataAvail(fd);
}
if (dev->count < 1024) {
SetWriteAvail(fd);
} else {
ClrWriteAvail(fd);
}
OSCritLeave(&dev->critical_section);
return bytes_written;
}
int ringBufferClose(int fd) {
RingBufferDevice *dev = (RingBufferDevice*)
GetExtraData(fd);
if (dev) {
delete dev;
}
return 0;
}
int createRingBufferFd() {
RingBufferDevice *device = new RingBufferDevice;
device->read_pos = 0;
device->write_pos = 0;
device->count = 0;
OSCritInit(&device->critical_section);
IoExpandStruct funcs = {
.read = ringBufferRead,
.write = ringBufferWrite,
.close = ringBufferClose,
.extra = device
};
if (fd >= 0) {
SetWriteAvail(fd);
}
return fd;
}
Usage of Custom Device
void useCustomRingBuffer() {
int ring_fd = createRingBufferFd();
if (ring_fd < 0) {
printf("Failed to create ring buffer\n");
return;
}
const char *message = "Hello, Ring Buffer!";
write(ring_fd, message, strlen(message));
char readBuffer[256];
int bytesRead =
read(ring_fd, readBuffer,
sizeof(readBuffer));
readBuffer[bytesRead] = '\0';
printf("Read from ring buffer: %s\n", readBuffer);
}
Best Practices
Resource Management
File Descriptor Lifecycle
Creation Use Cleanup
│ │ │
v v v
┌─────────────┐ ┌─────────────┐ ┌─────────────┐
│ resources │ │ │ │ Release FDs │
└─────────────┘ └─────────────┘ └─────────────┘
Guidelines
- Always Release File Descriptors
- Call
FreeExtraFd() in your close function
- Prevent descriptor exhaustion
- Thread Safety
- Use critical sections for shared data
- Protect state changes with
OSCritEnter/Leave
- State Management
- Update availability flags appropriately
- Use
SetDataAvail() when data arrives
- Use
ClrDataAvail() when buffer empties
- Error Handling
- Return appropriate error codes
- Use
SetHaveError() for error conditions
- Check return values from all functions
- Resource Initialization
- Initialize all structure members
- Verify allocations succeed
- Set initial state flags correctly
Troubleshooting
Common Issues
| Problem | Cause | Solution |
select() never returns | State flags not set | Call SetDataAvail() when data ready |
| File descriptor allocation fails | Pool exhausted | Release unused FDs with FreeExtraFd() |
| Data corruption | Missing critical sections | Protect shared data with OSCritEnter/Leave |
| Memory leaks | Not freeing resources | Implement proper cleanup in close function |
Debugging Tips
Debug Flow
┌─────────────────────────────────────┐
│ Add debug output to functions │
│ │
│ printf("FD %d: read %d bytes\n", │
│ fd, bytes_read); │
└─────────────────┬───────────────────┘
│
v
┌─────────────────────────────────────┐
│ Verify state flag updates │
│ │
│ printf("Setting data avail\n"); │
│ SetDataAvail(fd); │
└─────────────────┬───────────────────┘
│
v
┌─────────────────────────────────────┐
│ Check extra data pointer │
│ │
│ if (!extra)
└─────────────────────────────────────┘
Summary
Key Concepts
File Descriptor System:**
- Unified I/O abstraction for all device types
- POSIX-like interface for consistent access
255 total descriptors with defined ranges
Custom Driver Implementation:**
- Define
read(), write(), and close() functions
- Use
IoExpandStruct to register functions
Manage state with Set/Clr functions
Multiplexing:**
- Use
select() to wait on multiple sources
- Mix network, serial, and custom devices
Efficient event-driven programming
Best Practices:**
- Always release file descriptors
- Implement thread-safe operations
- Update state flags correctly
- Handle errors gracefully
Quick Reference
#include <iointernal.h>
SetDataAvail(fd); ClrDataAvail(fd);
SetWriteAvail(fd); ClrWriteAvail(fd);
SetHaveError(fd); ClrHaveError(fd);
The file descriptor system provides a powerful and flexible foundation for integrating diverse I/O resources into your NetBurner applications.