An RTEMS-based satellite groundstation controller that coordinates GPS positioning, TLE database management, pass prediction, radio control, and antenna control for automated satellite tracking.
It is meant to run on a BeagleBone Black SBC with the assistance of an I/O cape that I created, but the cape is entirely optional.
I've also created firmware that you can run on a Pi Pico, STM32, ESP32, or similar device to simulate a GPS, a radio, or a rotator. This way you can test the functionality of the controller if you do not have the rest of the necessary hardware.
This controller runs on embedded systems using RTEMS (Real-Time Executive for Multiprocessor Systems) and provides:
- GPS Integration: Parses NMEA sentences (GGA/RMC) from a GPS receiver for precise location and time
- TLE Database: Manages Two-Line Element sets for satellite orbit data with automatic HTTPS updates
- Pass Prediction: Calculates upcoming satellite passes using SGP4/SDP4 propagation
- Antenna Control: Commands antenna rotator for automated satellite tracking using GS-232A protocol
- Doppler Tracking: Real-time Doppler shift calculation for radio frequency correction
The system uses multiple RTEMS tasks communicating via message queues:
| Task | Priority | Function |
|---|---|---|
| Pass Executor | 10 | Controls rotator and radio during satellite passes |
| Controller | 20 | Main coordination, pass scheduling |
| Rotator Command | 25 | Sends GS-232A commands to antenna rotator |
| Radio Command | 26 | Sends Doppler-corrected CAT commands to radio |
| Rotator Status | 30 | Parses rotator position responses |
| Radio Status | 35 | Parses radio CAT protocol responses |
| Radio Frequency | 36 | Sends periodic CAT query commands to radio |
| Antenna | 40 | Polls antenna rotator position periodically |
| GPS | 50 | Reads NMEA data, extracts position/time |
| Pass Calculator | 60 | Computes upcoming satellite passes |
| TLE Updater | 70 | Downloads and updates satellite TLE database |
| Status | 80 | Periodic system status logging |
Tasks are synchronized using binary semaphores with priority inheritance for shared resources (UART, TLE database, state).
The TLE Updater task automatically downloads Two-Line Element data from a configurable URL (default: Celestrak amateur radio satellites). The process:
- Waits for GPS time to be valid (required for HTTPS certificate validation)
- Downloads TLE data via HTTPS
- Filters satellites by configured NORAD IDs
- Parses and validates TLE data, initializes SGP4 state for each satellite
- Stores in fixed-size database (up to 64 satellites)
- Repeats at configurable interval (default: 6 hours)
The Pass Calculator task predicts upcoming satellite passes:
- Scans the prediction window (default: 60 minutes ahead) in 60-second steps
- For each satellite, uses SGP4 to propagate position and calculate look angles
- Detects visibility transitions using binary search for 1-second precision
- Finds maximum elevation using ternary search
- Filters passes below minimum elevation threshold
- Stores passes in a priority queue (sorted by elevation for overlapping passes)
- Sends discovered passes to the Controller task
When a pass is scheduled, the Pass Executor task controls tracking:
State Machine:
- IDLE - Waiting for pass command from Controller
- PREPOSITIONING - Moving antenna to AOS (Acquisition of Signal) position
- WAITING_AOS - Antenna positioned, waiting for satellite to rise
- TRACKING - Active real-time satellite tracking
- COMPLETING - Pass ended, returning antenna to park position
During Tracking:
- Polls satellite position at configurable interval (default: 100ms)
- Calculates azimuth, elevation, and Doppler shift using SGP4
- Sends rotator commands when position changes exceed threshold (default: 1 degree)
- Sends Doppler-corrected frequency commands to radio when shift exceeds threshold (default: 1 kHz)
- Supports separate uplink and downlink frequencies with independent Doppler correction
The Rotator Command task receives position commands and sends GS-232A protocol commands:
W<aaa> <eee>- Move to azimuth/elevation (e.g., "W180 045")S- Stop movementC2- Query current position (sent by Antenna Location task)
Position responses are parsed by the Rotator Status task.
The Radio Command task receives frequency and mode commands from the Pass Executor and sends Yaesu CAT protocol commands to the radio. During satellite passes:
- At pass start: Sets VFO mode based on satellite's signal mode (e.g., DATA-FM for FSK/AFSK)
- During tracking: Sends Doppler-corrected frequencies at configurable intervals
Doppler Correction:
- Downlink (satellite TX → ground RX):
corrected_freq = base_freq × doppler_factor - Uplink (ground TX → satellite RX):
corrected_freq = base_freq × (2.0 - doppler_factor)
The doppler factor is calculated from the satellite's radial velocity (range rate). When the satellite approaches, the ground station receives higher frequencies and must transmit lower frequencies; when receding, the opposite applies.
VFO Assignment:
By default, VFO-A is used for downlink (ground receive) and VFO-B for uplink (ground transmit). This can be configured via downlink_vfo and uplink_vfo in the [pass] section.
- RTEMS 7 toolchain and BSP installed at
$HOME/rtems/7 - Python 3.6+ (for RTEMS Source Builder)
- Standard build tools (gcc, g++, make, etc.)
The application requires the RTEMS 7 toolchain and a Board Support Package (BSP). Follow these steps to build and install them.
On Debian/Ubuntu:
sudo apt-get install build-essential gcc g++ gdb git python3 python3-dev \
python3-pip unzip pax bison flex texinfo libncurses5-dev zlib1g-dev \
libexpat1-dev libpython3-devmkdir -p $HOME/rtems/src
cd $HOME/rtems/src
git clone https://gitlab.rtems.org/rtems/tools/rtems-source-builder.git rsb
cd rsb
git checkout 7This builds the cross-compiler, binutils, GDB, and other tools:
cd $HOME/rtems/src/rsb/rtems
../source-builder/sb-set-builder --prefix=$HOME/rtems/7 7/rtems-armThis takes approximately 25-30 minutes and requires about 10GB of temporary disk space.
cd $HOME/rtems/src
git clone https://gitlab.rtems.org/rtems/rtos/rtems.git
cd rtems
git checkout 7
# Add the toolchain to your PATH
export PATH=$HOME/rtems/7/bin:$PATHChoose and build a BSP for your target hardware:
cd $HOME/rtems/src/rtems
./waf bspdefaults --rtems-bsps=arm/beagleboneblack > config.ini
./waf configure --prefix=$HOME/rtems/7
./waf
./waf installThis project uses rtems-libbsd for networking and SD card support. A custom fork is required for BeagleBone Black that fixes driver issues in the official rtems-libbsd release.
Clone the custom fork:
cd $HOME/rtems/src
git clone -b 7-freebsd-14-beaglebone-black https://github.com/vaelen/rtems-libbsd.git
cd rtems-libbsd
git submodule init
git submodule update rtems_wafBuild and install:
./waf configure --prefix=$HOME/rtems/7 \
--rtems-bsps=arm/beagleboneblack \
--buildset=buildset/bbb.ini
./waf
./waf installThe buildset/bbb.ini configuration enables SD card and networking support while disabling components that are not needed or cause issues on BeagleBone Black.
For technical details about the BeagleBone Black fixes, see sdcard.md and networking.md.
ls $HOME/rtems/7/arm-rtems7/You should see a directory for the installed BSP (e.g., beagleboneblack).
# Initialize submodule (first time only)
git submodule init
git submodule update rtems_waf
# Configure for BeagleBone Black
./waf configure --prefix=$HOME/rtems/7 --rtems-bsps=arm/beagleboneblack
# Build
./waf
# Clean
./waf cleanThe executable is built at build/arm-rtems7-<bsp>/controller.exe (e.g., build/arm-rtems7-beagleboneblack/controller.exe).
The recommended development workflow uses TFTP boot for fast iteration:
-
Set up the TFTP server (one-time):
# Install TFTP server sudo apt install tftpd-hpa # Create TFTP directory with proper permissions sudo mkdir -p /srv/tftp sudo chown $USER:$USER /srv/tftp sudo chmod 755 /srv/tftp # Edit /etc/default/tftpd-hpa and set: # TFTP_DIRECTORY="/srv/tftp" # TFTP_OPTIONS="--secure --create" # Restart service sudo systemctl restart tftpd-hpa sudo systemctl enable tftpd-hpa
-
Prepare SD card for U-Boot (one-time):
Create a small FAT32 partition on an SD card and add a
uEnv.txtfile:bootdelay=3 ipaddr=192.168.68.15 serverip=192.168.68.23 netmask=255.255.252.0 uenvcmd=run boot boot=tftp 0x80800000 controller.img; ext4load mmc 1:1 0x88000000 /boot/dtbs/4.19.94-ti-r42/am335x-boneblack.dtb; bootm 0x80800000 - 0x88000000
Adjust
ipaddrandserveripfor your network. Insert the SD card into the BBB. -
Deploy and run:
# Build and deploy to TFTP server ./deploy.sh --build # Or just deploy (if already built) ./deploy.sh
Power cycle or reset the BeagleBone Black. It will automatically:
- Load the firmware via TFTP
- Execute the RTEMS application
| Device | IP Address | Netmask |
|---|---|---|
| Workstation | 192.168.68.23 | 255.255.252.0 |
| BBB | 192.168.68.15 | 255.255.252.0 |
- "TFTP error: 'Permission denied'": Check TFTP directory permissions
- "TFTP error: 'File not found'": Run
./deploy.shto copy the executable - No network response: Verify Ethernet cable and IP addresses
- U-Boot doesn't read uEnv.txt: Ensure SD card has FAT32 partition and is inserted
- GPS receiver outputting NMEA 0183 at 9600 baud (8N1)
- Antenna rotator with GS-232A serial control interface
- Radio with serial control interface (Yaesu CAT protocol)
An emulator for these devices can be created using a Pi Pico, ESP32, or STM32 microcontroller using the serial-device-emulator firmware.
The following table shows the physical P9 header pins used for each UART:
| Device | UART | Function | Pin | Ball Name | Mux Mode |
|---|---|---|---|---|---|
| Console | UART0 | - | J1 | (debug header) | - |
| GPS | UART1 | RXD | P9.26 | uart1_rxd | 0 |
| GPS | UART1 | TXD | P9.24 | uart1_txd | 0 |
| Rotator | UART2 | RXD | P9.22 | spi0_sclk | 1 |
| Rotator | UART2 | TXD | P9.21 | spi0_d0 | 1 |
| Radio | UART4 | RXD | P9.11 | gpmc_wait0 | 6 |
| Radio | UART4 | TXD | P9.13 | gpmc_wpn | 6 |
Notes:
- UART0 is the debug console, directly accessible via the J1 serial header
- UART3 is not used (pins conflict with other functions)
- UART4 is mapped to
/dev/ttyS3in software - Pin muxing is configured in
console-config.c
[16:53:09] INFO [STATUS ] === System Status ===
[16:53:09] INFO [STATUS ] GPS Time: 2026-01-06 16:53:09 UTC
[16:53:09] INFO [STATUS ] GPS Pos: 35.5871, 139.4900, 55m
[16:53:09] INFO [STATUS ] Antenna: az=345.0 el=9.0 deg
[16:53:09] INFO [STATUS ] Radio: VFO-A active, mode=???, preamp=IPO
[16:53:09] INFO [STATUS ] Radio: VFO-A=14.074000 MHz, VFO-B=7.074000 MHz
[16:53:09] INFO [STATUS ] TLE: 12 satellites loaded
[16:53:09] INFO [STATUS ] Passes: 4 upcoming
[16:53:09] INFO [STATUS ] Satellite NORAD AOS LOS AOS Az LOS Az MaxEl
[16:53:09] INFO [STATUS ] -------------------- ----- -------- -------- ------ ------ ------
[16:53:09] INFO [STATUS ] CUBEBUG-2 (LO-74) 39440 17:01:27 17:11:25 9.4° 197.5° 76.6°
[16:53:09] INFO [STATUS ] Down:437.445 FSK
[16:53:09] INFO [STATUS ] ISS (ZARYA) 25544 17:06:01 17:12:57 333.9° 82.3° 16.2°
[16:53:09] INFO [STATUS ] Down:145.825 AFSK, Up:145.825 AFSK
[16:53:09] INFO [STATUS ] FUNCUBE-1 (AO-73) 39444 17:23:24 17:31:34 38.7° 151.7° 18.8°
[16:53:09] INFO [STATUS ] Down:145.935 ???
[16:53:09] INFO [STATUS ] UWE-3 39446 17:44:48 17:54:15 1.9° 209.6° 45.2°
[16:53:09] INFO [STATUS ] Down:436.395 FSK
[16:53:09] INFO [STATUS ] Network: cpsw0
[16:53:09] INFO [STATUS ] IPv4: 192.168.68.17
[16:53:09] INFO [STATUS ] =====================The controller reads configuration from an INI-style file on the SD card. If no configuration file exists, defaults are used.
/mnt/sd/config.ini
; Satellite Tracking Controller Configuration
[serial]
gps_device = /dev/ttyS1
gps_baud = 9600
gps_flow_control = false
rotator_device = /dev/ttyS2
rotator_baud = 9600
rotator_flow_control = false
radio_device = /dev/ttyS3
radio_baud = 38400
radio_flow_control = false
[observer]
latitude = 35.5872
longitude = 139.4901
altitude = 52.0
[files]
tle_path = /mnt/sd/sats.tle
[system]
log_level = INFO
status_interval = 30
[network]
enabled = true
interface = cpsw0
ipv4_enabled = true
ipv4_dhcp = true
ipv4_address = 192.168.1.100
ipv4_netmask = 255.255.255.0
ipv4_gateway = 192.168.1.1
ipv6_enabled = true
ipv6_dhcp = true
ipv6_slaac = true
[tle]
url = https://celestrak.org/NORAD/elements/gp.php?GROUP=amateur&FORMAT=tle
satellites = 25544, 43017, 43770
update_interval = 6
[pass]
prediction_window = 60
min_elevation = 5.0
min_schedule_elevation = 15.0
prep_time = 300
calc_interval = 300
status_display_count = 5
tracking_poll_ms = 100
rotator_threshold = 1.0
doppler_threshold = 1.0
preposition_margin = 30
max_tle_age = 3.0
max_pass_duration = 20.0
downlink_vfo = A
uplink_vfo = B
[tracking]
; Satellite communication channels: NORAD|DIR|FREQ_KHZ|MODE|BAUD|ENCODING
; DIR: Up (ground TX) or Down (ground RX)
; MODE: CW, USB, LSB, AM, FM, RTTY, FT8, FSK, AFSK, GFSK, GMSK, OQPSK
; ENCODING: CW, Voice, FT8, RTTY, AX.25, CCSDS (optional)
track = 25544|Down|145825|AFSK|1200|AX.25
track = 25544|Up|145825|AFSK|1200|AX.25
track = 46507|Down|435600|FSK|9600|AX.25
track = 50466|Down|435575|CW|22|CW
[modes]
; Signal mode to radio mode mappings (override defaults)
; Format: signal_mode = radio_mode
; Signal modes: CW, USB, LSB, AM, FM, RTTY, FT8, FSK, AFSK, GFSK, GMSK, OQPSK
; Radio modes: LSB, USB, CW, FM, AM, RTTY-L, CW-R, DATA-L, RTTY-U, DATA-FM, FM-N, DATA-U, AM-N, C4FM
AFSK = DATA-FM
FSK = DATA-FM
GMSK = DATA-FM| Key | Description | Default |
|---|---|---|
gps_device |
GPS serial port device path | /dev/ttyS1 |
gps_baud |
GPS baud rate | 9600 |
gps_flow_control |
Enable hardware flow control | false |
rotator_device |
Rotator serial port device path | /dev/ttyS2 |
rotator_baud |
Rotator baud rate | 9600 |
rotator_flow_control |
Enable hardware flow control | false |
radio_device |
Radio serial port device path | /dev/ttyS3 |
radio_baud |
Radio baud rate | 38400 |
radio_flow_control |
Enable hardware flow control | false |
| Key | Description | Default |
|---|---|---|
latitude |
Observer latitude in degrees (N positive) | 0.0 |
longitude |
Observer longitude in degrees (E positive) | 0.0 |
altitude |
Observer altitude in meters | 0.0 |
Note: GPS location always overrides these settings when a GPS receiver is connected.
| Key | Description | Default |
|---|---|---|
tle_path |
Path to TLE database file | /mnt/sd/sats.tle |
| Key | Description | Default |
|---|---|---|
log_level |
Logging level: DEBUG, INFO, WARN, ERROR | INFO |
status_interval |
Status output interval in seconds | 30 |
| Key | Description | Default |
|---|---|---|
enabled |
Enable network interface | true |
interface |
Network interface name | cpsw0 |
ipv4_enabled |
Enable IPv4 | true |
ipv4_dhcp |
Use DHCP for IPv4 | true |
ipv4_address |
Static IPv4 address (if DHCP off) | (empty) |
ipv4_netmask |
Subnet mask (if DHCP off) | (empty) |
ipv4_gateway |
Default gateway (if DHCP off) | (empty) |
ipv6_enabled |
Enable IPv6 | true |
ipv6_dhcp |
Use DHCPv6 | true |
ipv6_slaac |
Use SLAAC for IPv6 | true |
ipv6_address |
Static IPv6 address | (empty) |
ipv6_prefix_len |
IPv6 prefix length | 64 |
ipv6_gateway |
IPv6 gateway | (empty) |
| Key | Description | Default |
|---|---|---|
url |
URL for TLE download (HTTPS) | Celestrak amateur radio satellites |
satellites |
Comma-separated list of NORAD IDs to track | (empty = all satellites in file) |
update_interval |
Hours between TLE updates | 6 |
Default URL: https://celestrak.org/NORAD/elements/gp.php?GROUP=amateur&FORMAT=tle
| Key | Description | Default |
|---|---|---|
prediction_window |
How far ahead to predict passes (minutes) | 60 |
min_elevation |
Minimum elevation for pass detection (degrees) | 5.0 |
min_schedule_elevation |
Minimum max-elevation to schedule a pass (degrees) | 15.0 |
prep_time |
Time before AOS to start preparation (seconds) | 300 |
calc_interval |
How often to recalculate passes (seconds) | 300 |
status_display_count |
Number of upcoming passes to show in status | 5 |
tracking_poll_ms |
Tracking loop update interval (milliseconds) | 100 |
rotator_threshold |
Minimum position change to command rotator (degrees) | 1.0 |
doppler_threshold |
Minimum Doppler change to update radio (kHz) | 1.0 |
preposition_margin |
Time before AOS to complete prepositioning (seconds) | 30 |
max_tle_age |
Maximum TLE age before skipping satellite (days) | 3.0 |
max_pass_duration |
Maximum pass duration before warning (minutes) | 20.0 |
downlink_vfo |
VFO for downlink/receive (A or B) | A |
uplink_vfo |
VFO for uplink/transmit (A or B) | B |
Defines satellite frequencies and modulation modes for Doppler-corrected radio control during passes. Each track= line specifies a communication channel.
Format: track = NORAD|DIR|FREQ_KHZ|MODE|BAUD|ENCODING
| Field | Description | Required |
|---|---|---|
NORAD |
Satellite NORAD catalog ID | Yes |
DIR |
Direction: Up (ground TX) or Down (ground RX) |
Yes |
FREQ_KHZ |
Frequency in kHz (e.g., 145825 for 145.825 MHz) | Yes |
MODE |
Signal modulation mode | Yes |
BAUD |
Baud rate (optional, 0 for CW/voice) | No |
ENCODING |
Data encoding protocol (optional) | No |
Signal Modes: CW, USB, LSB, AM, FM, RTTY, FT8, FSK, AFSK, GFSK, GMSK, OQPSK
Encoding Types: CW, Voice, FT8, RTTY, AX.25, CCSDS
Example:
[tracking]
; ISS APRS digipeater (VHF FM packet)
track = 25544|Down|145825|AFSK|1200|AX.25
track = 25544|Up|145825|AFSK|1200|AX.25
; GREENCUBE 435 MHz digipeater
track = 46507|Down|435600|FSK|9600|AX.25
; CW beacon
track = 50466|Down|435575|CW|22|CWNotes:
- Satellites with no
track=lines use built-in defaults (ISS, GREENCUBE, etc.) - Multiple channels per satellite are supported (uplink + downlink)
- NORAD IDs in
track=lines are automatically added to the TLE download filter - If a
[tracking]section exists with anytrack=lines, defaults are not applied
Maps satellite signal modulation modes to radio operating modes for automatic mode setting during passes. These mappings determine which radio mode (e.g., DATA-FM, USB) is used when tracking a satellite with a specific signal mode (e.g., AFSK, FSK).
Format: signal_mode = radio_mode
Default Mappings:
| Signal Mode | Default Radio Mode |
|---|---|
| CW | CW |
| USB | USB |
| LSB | LSB |
| AM | AM |
| FM | FM |
| RTTY | RTTY-L |
| FT8 | DATA-U |
| FSK | DATA-FM |
| AFSK | DATA-FM |
| GFSK | DATA-FM |
| GMSK | DATA-FM |
| OQPSK | DATA-FM |
Radio Modes (Yaesu FT-991A): LSB, USB, CW, FM, AM, RTTY-L, CW-R, DATA-L, RTTY-U, DATA-FM, FM-N, DATA-U, AM-N, C4FM
Example:
[modes]
; Use narrow FM for digital modes
FSK = FM-N
AFSK = FM-N
; Use USB for weak signal digital
GMSK = DATA-U- Comments start with
;or# - Boolean values accept:
true/false,yes/no,1/0,on/off - GPS location always overrides
[observer]settings when a GPS receiver is connected - Changes to the config file require a reboot to take effect
- Supported baud rates: 1200, 2400, 4800, 9600, 19200, 38400, 57600, 115200
The SD card must be formatted with a FAT filesystem (FAT16 or FAT32). The controller automatically:
- Mounts the SD card at
/mnt/sd - Loads
config.iniif present (uses defaults if not found)
See sdcard.md for technical details on SD card support.
sattrack_controller/
├── controller.c # Main application, task creation and coordination
├── init.c # RTEMS configuration
├── pass.c/h # Pass prediction, TLE management, pass execution
├── radio.c/h # Radio control tasks (CAT protocol)
├── sgp4.c/h # SGP4/SDP4 satellite propagation
├── nmea.c/h # NMEA 0183 parser
├── config.c/h # Configuration system
├── shared.h # Shared types, message definitions, IPC objects
├── log.c/h # Logging system
├── priority_queue.c/h # Pass priority queue
├── https_client.c/h # HTTPS client for TLE download
├── date.h # Date/time utilities (third-party)
├── wscript # Waf build configuration
├── deploy.sh # Deploy to TFTP server for BBB
└── docs/
└── NMEA.md # NMEA sentence format reference
Implementation of the SGP4/SDP4 satellite propagation algorithms based on the Vallado reference implementation. Supports:
- TLE parsing with checksum validation
- Satellite position/velocity propagation
- Coordinate transforms (ECI, ECEF, Geodetic, ENU)
- Look angle calculations (azimuth, elevation, range)
- Greenwich Sidereal Time calculation
Parses NMEA 0183 sentences from GPS receivers:
- Supports all GNSS talker IDs (GP, GN, GA, GB, GL, GQ)
- GGA sentences for position and altitude
- RMC sentences for date and time
- Checksum validation
- Non-blocking buffered reads
MIT License - See individual source files for details.
Andrew C. Young andrew@vaelen.org