Skip to content

Vehicle Data & VHAL

From sensor to pixel: the complete path

One cabin temperature reading, followed through every hop — thermistor, ECU, CAN frame, kernel driver, vendor service, VHAL, Binder, Car Service, app, screen — with the mechanism named at each boundary.

Advanced10 minArchitecture · VHAL · Binder · Debugging

The architecture overview showed the layers. This page follows a single value through all of them, naming the actual mechanism at every boundary — because "the layers talk to each other" is not enough to debug with.

The value: cabin temperature, driver's side. It starts as a voltage and ends as the characters 21.5° on a screen.

Eight hops. The mechanism changes at every one.Physical sensora thermistor in the cabinvoltageECUreads it, scales it, broadcastsCAN frame, 8 bytesCAN controller + driverkernel receives the frameinterrupt → SocketCANVendor vehicle serviceapplies factor/offset, maps to a propertyread() on a socketVehicle HALcaches it, publishes a VehiclePropValuein-process callCar Servicepermission check, fan-out to subscribersbinder transactionYour apponChangeEvent on a binder threadbinder transactionUI thread → SurfaceFlingerthe number is finally drawnpost() then compositionTreble boundary
Eight hops, eight different mechanismsNotice the right-hand column. The mechanism changes at every boundary — a voltage becomes a bus frame becomes an interrupt becomes a socket read becomes a Binder transaction. Each change is a place a value can be corrupted or lost.

Hop 1 — A thermistor, and a voltage#

A thermistor in the cabin changes resistance with temperature. An puts a known voltage across it and measures what comes back through an analogue-to-digital converter.

The ECU now has a number — say 2048 out of 4095. That is not a temperature. It is a raw ADC reading that only means something with the thermistor's curve applied.

Hop 2 — The ECU broadcasts a CAN frame#

The ECU converts its reading into the units the vehicle's network specification says it should send, packs it into a small message, and broadcasts it.

A CAN frame, conceptually
ID:     0x1A4          ← which message this is (not who sent it)
Length: 8 bytes
Data:   [ 0x8F 0x02 0x00 0x00 0x1B 0x00 0x00 0x00 ]
          └─┬──┘
            cabin temperature lives in bytes 0-1

The two bytes 0x8F 0x02 are 655 as a little-endian 16-bit value. The vehicle's signal database says this signal is raw × 0.1 − 40, so 655 means 25.5 °C.

Hop 3 — The kernel receives it#

The CAN transceiver on the board raises an interrupt. A kernel driver reads the frame out of the controller's buffer and hands it to the networking stack.

On Linux this is usually SocketCAN, which presents CAN as a network interface, so user-space code reads frames from a socket.

Watching raw frames on a development board
ip link set can0 up type can bitrate 500000
candump can0
 
# can0  1A4   [8]  8F 02 00 00 1B 00 00 00

Hop 4 — The vendor service translates#

Now we cross into software somebody wrote for this vehicle. This layer is not in AOSP — it is the car maker's or their supplier's.

Its job is to turn bus traffic into the values the promised.

What this layer actually does
void OnCanFrame(const CanFrame& frame) {
    if (frame.id != 0x1A4) return;
 
    // Extract the bits this signal occupies
    uint16_t raw = frame.data[0] | (frame.data[1] << 8);
 
    // Apply the scaling from the signal database. Exactly once.
    float celsius = raw * 0.1f - 40.0f;
 
    publisher_.Update(kHvacTemperatureCurrent, kAreaDriver, celsius);
}

Hop 5 — The Vehicle HAL publishes#

The stores the value in its cache and, if anyone is subscribed, publishes a change event.

Publishing a value
VehiclePropValue update{
    // CLOCK_BOOTTIME nanoseconds — nanoseconds since the device booted.
    // Not wall clock. Not milliseconds.
    .timestamp = elapsedRealtimeNano(),
    .areaId = SEAT_ROW_1_LEFT,
    .prop = toInt(VehicleProperty::HVAC_TEMPERATURE_CURRENT),
    .status = VehiclePropertyStatus::AVAILABLE,
};
update.value.floatValues = {25.5f};
 
property_change_callback_(std::vector{std::move(update)});
Checking what the HAL believes
adb shell dumpsys car_service --get-property 0x11600203 -a 1

Hop 6 — Binder into Car Service#

The VHAL is a separate process from . The change event crosses that boundary as a transaction — the mechanism from the previous module.

CarPropertyService receives it and does four things:

  1. Updates its own cache, so a later get does not need to reach the HAL.
  2. Checks which apps are subscribed to this property and area.
  3. Filters by user — a background user's app should not receive events it should not see.
  4. Fans out one HAL event to every subscribed app.
Who is subscribed, and how fast
adb shell dumpsys car_service --services CarPropertyService

Hop 7 — Binder into your app#

Another process boundary, another Binder transaction. Your callback runs.

Where your code re-enters the story
override fun onChangeEvent(value: CarPropertyValue<*>) {
    // This is a BINDER THREAD, not your main thread.
    if (value.status != CarPropertyValue.STATUS_AVAILABLE) return
 
    pendingTemperature = value.value as Float
    scheduleRender()      // hand off; do not draw here
}

Hop 8 — Onto the screen#

Your handler posts to the main thread, the view or composable updates, SurfaceFlinger composites the frame, and the display shows it.

Rendering at the rate the eye needs, not the rate data arrives
private fun scheduleRender() {
    // A gauge at 60 fps does not need 60 Hz of data. Interpolate between
    // samples instead of redrawing on every event.
    if (!renderScheduled) {
        renderScheduled = true
        mainHandler.post { render(pendingTemperature); renderScheduled = false }
    }
}

The whole path as a debugging tool#

This is the real payoff. When a value is wrong, you now have eight places to look and a command for most of them.

HopQuestionHow to check
1–2Is the ECU sending anything?candump can0
2Is the raw value right?Compare against the signal database
3Does the kernel see frames?ip -s link show can0
4Did the vendor layer scale it correctly?Compare candump with the next row
5What does the HAL hold?dumpsys car_service --get-property
5Is the HAL even running?lshal | grep vehicle
6Does Car Service know the property?dumpsys car_service --list-properties
6Is anything blocked by policy?dmesg | grep avc
7Does the app hold the permission?dumpsys package <pkg>
7–8Is the app rendering what it received?Log the callback value

Timing: where the milliseconds go#

For a driver-visible value, the whole path usually needs to complete in under about 200 ms. A rough breakdown:

HopTypical cost
ECU sample and broadcast interval10–100 ms
Kernel receiveunder 1 ms
Vendor translationunder 1 ms
VHAL publishunder 1 ms
Binder to Car Service1–5 ms
Car Service fan-out1–5 ms
Binder to app1–5 ms
Render16 ms (one frame)
Measuring it properly
adb shell perfetto -o /data/misc/perfetto-traces/t -t 20s \
  sched freq binder_driver am wm gfx view hal

binder_driver is the category that makes the two IPC hops visible. Without it, a trace shows your app waiting and gives no clue why.

What changes on a virtualised head unit#

On a consolidated cockpit computer, hop 3 is different: instead of a CAN driver, Android is a guest under a and receives values from another guest through shared memory or a virtio channel.

Next#

Building a property of your own that travels this whole path.

References & further reading

Code links target the main branch on cs.android.com. AOSP moves — if a path 404s, search the symbol instead.