Demonstrating SPARK with a Mars Rover (Part 1): Cyber-Physical Systems

A cyber-physical system leveraging SPARK offers a good way to develop and test safety-critical systems.
April 13, 2026
6 min read

The Ada SPARK programming language was designed to facilitate the creation of safety- and security-critical systems. It includes a variety of features like contracts that let developers specify software requirements and specifications.

AdaCore’s Ada Mars Rover Demo platform is a hands-on example of a cyber-physical system (CPS) in action. Like real-world rovers, it integrates embedded software with hardware components to sense its surroundings, process data, and act accordingly — all in real-time. Beyond showcasing the principles of CPS, the project is designed to highlight the strengths of the SPARK language and tools for developing high-integrity software in safety- and mission-critical domains.

What follows is the first of a four-part series that takes a deep dive into the use of SPARK with our Mars Rover Demo platform.

Using Cyber-Physical Systems

Cyber-physical systems are integrated solutions that combine computation with physical processes, blurring the boundaries between the digital and physical worlds.

They rely on tight interaction between software, networking, and mechanical or electronic components, often using sensors, actuators, and embedded systems to perceive, analyze, and influence their environment. Examples span a wide range of industries, including smart grids, autonomous vehicles, medical monitoring, industrial control systems, robotics, and avionics.

Because they coordinate sensing, computation, and actuation, CPS is an ideal framework for understanding the challenges and opportunities in high-integrity embedded development.

Writing a Safety Monitor for a Mars Rover!

As an initial step, a new hardware abstraction layer (HAL) was introduced on top of the existing HALs developed for the various pieces of hardware that comprise the Mars Rover. The result is a façade that provides a simplified and unified view of the Mars Rover hardware, a single point at which the hardware can be replaced by a simulator, and an opportunity to introduce the necessary SPARK contracts to enable proof for the rover software.

Once the team completed the work on the HAL, the entirety of the Rover software proved at SPARK silver. That is, SPARK proved the absence of run-time exceptions (AoRTE).

The team wanted to take this a step further and introduced a gold-level property — a functional property about the software that the team could imagine was traced to the Mars Rover requirements. A fairly obvious property suggests itself when considering the Rover: It shouldn’t crash into obstacles. So, the team formalized this property and proved it.

Formalizing the Safety Property

It’s perhaps a bit cheeky to suggest that it's possible to prove the Rover won’t crash into obstacles. Many things can go wrong over which the software has no control: an obstacle could roll into the Rover's path at the last minute; a hardware fault could leave both motors stuck at full power; the sonar could fail to detect an obstacle, etc. So, when the team says, “the Rover cannot crash,” it means the Rover won’t attempt to go forward when it knows there’s an obstacle.

Here’s how to write the safety requirement in natural language:

The Mars Rover shall not proceed straight forward (i.e., directly forward, without turning) when the distance to an obstacle is less than the Safety Threshold.

This maps naturally to a safety monitor: a function that inspects controller inputs and state and verifies that controller outputs remain acceptable. Here, the monitor considers the input representing obstacle distance and the outputs indicating forward motion (both motors with positive power and turn mode set to straight).

Looking at the HAL ("rover_hal.ads"), the Rover is expected to poll the sonar for distance:

  function Sonar_Distance return Unsigned_32
    with
      Pre => Initialized,
      Global => (HW_State, HW_Init),
      Side_Effects,
      Volatile_Function;

The Rover is expected to set the turn mode and the motor power for each wheel:

  procedure Set_Turn (Turn : Turn_Kind)
    with
      Pre  => Initialized,
      Post => Initialized,
      Global => (HW_State, HW_Init);
 
  procedure Set_Power (Side : Side_Id;
                       Pwr  : Motor_Power)
    with
      Pre  => Initialized,
      Post => Initialized,
      Global => (HW_State, HW_Init);

Given this information, the requirement can be formalized like this:

if Last_Sonar_Distance < Safety_Threshold and then
   Last_Turn = Straight
then
   Last_Power (Left) <= 0 and then
   Last_Power (Right) <= 0

where the `Last_` prefixes indicate some means to refer to whatever the last value that was retrieved (in the case of the sonar) or set (in the case of the turn and motor powers).

These properties can be validated by examining how the Rover’s controller modes are implemented. In “rover-remote_controlled.adb,” for example, there are sequences of calls to the HAL such as the following:

when Forward =>
  Set_Turn (Straight);
  Set_Power (Left, 100);
  Set_Power (Right, 100);

Providing Getters

If the HAL exposed getters, they could simply be used to express the formalized safety requirement. However, the HAL provides setters only. Getters, therefore, need to be introduced so that they can be referenced in the formalized requirement. SPARK permits this without requiring implementations of the getters, as will be shown. [Note: Other approaches are possible, but this is the most elegant. It follows DRY and avoids YAGNI (getters haven’t been needed to date, so they’re unlikely to be required for non-proof purposes).]

The Turn Getter

SPARK provides the capability to model the behavior of getters using uninterpreted functions and abstract state.

An uninterpreted function is a ghost function that’s declared but never implemented. Abstract state indicates to SPARK that the getter is associated with state, which may change as a consequence of invoking the setter. These constructs are introduced as follows:

First, the abstract state is introduced:

 Abstract_State => (HW_Init,
                    (HW_State with Synchronous),
                    (Turn_State with Ghost))

Then, the getter function is declared like this:

  function Get_Turn return Turn_Kind
    with
      Pre => Initialized,
      Global => (HW_Init, Turn_State),
      Ghost,
      Import;

This gives the means to talk about the last turn value that was set. Then it can be tied to the model of a getter to the setter, which may be done by updating the setter as such:

  procedure Set_Turn (Turn : Turn_Kind)
    with
      Pre  => Initialized,
      Post => Initialized and then
              Get_Turn = Turn,
      Global => (Input  => (HW_State, HW_Init),
                 In_Out => Turn_State);

The abstract state is used to tell SPARK, through the Global annotation, that the setter changed something that the getter needs to know about. And in the postcondition of “Set_Turn,” it’s specified that when proof code calls “Get_Turn”; the value returned will equal the value given in the formal parameter “Turn.” This declares that “Get_Turn” behaves like a getter should.

The Power Getter

It might be assumed that the getter for motor power can be written in exactly the same way, like this:

  procedure Set_Power (Side : Side_Id;
                       Pwr  : Motor_Power)
    with
      Pre  => Initialized,
      Post => Initialized and then
              Get_Power (Side) = Pwr,
      Global => (Input  => (HW_State, HW_Init),
                 In_Out => Power_State);
 
  function Get_Power (Side : Side_Id) return Motor_Power
    with
      Pre => Initialized,
      Global => (HW_Init, Power_State),
      Ghost,
      Import;

Unfortunately, a problem then arises when attempting to prove the following:

Set_Power (Left, 0);
pragma Assert (Get_Power (Left) = 0);
Set_Power (Right, 0);
pragma Assert (Get_Power (Left)  = 0);
pragma Assert (Get_Power (Right) = 0);

The first and third assertions can be proved, but not the second. What’s going on?

Looking closely at “Set_Power,” SPARK has been told how “Get_Power” will behave when called with Side specified — but not that “Get_Power” will continue to return the same value as before when called with any other Side specified. That this should be true is obvious, but without having stated it, SPARK will not assume it. (This is the safe approach; SPARK isn’t doing anything wrong here.)

It’s necessary to adjust “Set_Power” so that it will maintain that relationship. Since there are only two sides, the simplest approach is to do this:

  procedure Set_Power (Side : Side_Id;
                       Pwr  : Motor_Power)
    with
      Pre  => Initialized,
      Post => Initialized and then
              Get_Power (Side) = Pwr and then
 
              (if Side = Left then
                 Get_Power (Right) = Get_Power (Right)'Old
               else
                 Get_Power (Left) = Get_Power (Left)'Old),
      Global => (Input  => (HW_State, HW_Init),
                 In_Out => Power_State);

The Distance Getter

For completeness, here’s the getter for “Sonar_Distance” (there’s nothing surprising going on here):

  function Sonar_Distance return Unsigned_32
    with
      Pre    => Initialized,
      Post   => Get_Sonar_Distance = Sonar_Distance'Result,
      Global => (Input => (HW_State, HW_Init),
                 In_Out => Distance_State),
      Side_Effects,
      Volatile_Function;
 
  function Get_Sonar_Distance return Unsigned_32
    with
     Pre    => Initialized,
     Global => (HW_Init, Distance_State),
     Ghost,
     Import;

More About Ada and SPARK

Getty Images
ada_spark_promo_getty_txc
Ada and SPARK are often used to develop applications that require high reliability and no bugs.
Dreamstime
ada_and_spark_basics_txc_dreamstime
This TechXchange includes articles about the Ada/SPARK programming language.
Adacore
ada2012_promo
Ada 2012 introduced contracts that provide a way to specify how a program should work.
Dreamstime
developing_software_using_ada_txc_dreamstime
Ada and SPARK are typically used for developing highly reliable software and it can reduce the cost of doing so.

About the Author

Tony Aiello

Tony Aiello

Product Manager, AdaCore

Tony Aiello is a Product Manager at AdaCore. Currently, he manages SPARK Pro and GNAT Pro for Rust, with a hand in UX and application of AI to AdaCore’s products. Tony has been principal investigator of multiple United States Air Force research projects that focus on the enhancement and application of formal methods to Air Force-relevant applications.

Previously, Tony led the SSI and QGen teams, led research directions, and was Head of Product at AdaCore. Before joining AdaCore, Tony was a Principal Scientist at, and briefly President of, Dependable Computing, a small research and development group focused on safety and formal methods.

Sign up for our eNewsletters
Get the latest news and updates

Voice Your Opinion!

To join the conversation, and become an exclusive member of Electronic Design, create an account today!