Jump to content

Mainline:Broadcom Kona/Clocks

From dissonant.dev wiki

The Broadcom Kona clock manager consists of multiple CCUs (clock control units), which each manage their own set of clocks - whether they're enabled/disabled, their frequencies and other per-clock settings - as well as some peripheral voltage settings (likely a DVFS configuration).

This doc will not explain much about the clocks themselves (gating, hyst, selectors, etc.) - for that, see the source code of the mainline driver, which explains these things much better than this doc ever could.

TODOs

  • Figure out how voltage and peripheral voltage settings work, and what they affect
  • Document connection to the power/PI manager

Clock types

There are multiple types of clocks handled by the CCUs.

Reference clocks

Fixed clocks used as clock sources for other clocks.

Bus clocks

Presumably handle frequencies for internal buses. They must be initialized before any of the peripheral clocks can be used.

Some peripheral clocks have corresponding bus clocks (see sdio1 (peripheral) and sdio1_ahb (bus), uartb (peri) and uartb_apb (bus)). Some bus clocks are used as the primary clock for specific blocks (see usb_otg_ahb).

Peripheral clocks

The primary clocks used for various elements of the SoC.

PLL clocks and PLL channels clocks

These work completely differently from bus/peri clocks. Their primary use is as a configurable clock source - notably, they're used to power the CPU's clock.

Core clock

The CPU's clock.

CCUs

Policies

Each CCU has 4 policies. Policies control whether specific clocks can be gated (enabled/disabled), as well as the frequencies of specific clocks and some voltages, as seen in the frequency/voltage control sections below.

According to the mainline driver, these policies are: "Deep Sleep", "Economy", "Normal" and "Turbo", where policy 2 is the default (question is, is it 2 counting from 1 or 0?).

Policy engine

Before making any changes to the policy or one of the controls below, the policy engine must first be stopped. Restarting the policy engine will apply the changes to it.

Starting the policy engine is done by writing to the policy control offset, either:

  • the ATL (active load) bit for synchronous requests (we wait for the frequency and voltage to stabilize);
  • or clearing it for asynchronous requests, going into target mode (where the voltage ramps in the background, then the target load is copied to the active load triggering a frequency change).

The above is more or less abridged from the mainline driver's docs:

        /*
         * If it's a synchronous request, we'll wait for the voltage
         * and frequency of the active load to stabilize before
         * returning.  To do this we select the active load by
         * setting the ATL bit.
         * 
         * An asynchronous request instead ramps the voltage in the
         * background, and when that process stabilizes, the target
         * load is copied to the active load and the CCU frequency
         * is switched.  We do this by selecting the target load
         * (ATL bit clear) and setting the request auto-copy (AC bit
         * set).
         * 
         * Note, we do NOT read-modify-write this register. 
         */

and is somewhat related to downstream's ccu_set_policy_ctrl.

Getting the active policy

There's a way to use the policy debug registers to get the active policy, as seen in ccu_policy_dbg_get_act_policy, which is called through ccu_get_active_policy. This is used to get the frequency ID to use for the bus clocks, as seen below.

TODO: How do you set the policy? My guess is that it's managed by the policy engine based on some kind of dependency; either that, or it's dictated by the power manager. TODO.

Frequency control

ccu_set_freq_policy / ccu_get_freq_policy, ccu_clk->freq_tbl[freq_id][freq_tbl_index]

Each CCU with a policy has a "policy_freq" register, which stores the IDs of the frequency policies to use for each CCU policy - this is written in ccu_set_freq_policy. On init, it reads the first 4 members of ccu_clk->freq_policy and writes them as the defaults to the registers.

Every frequency policy has an ID from 0 to 7. The downstream driver has tables representing what frequencies each policy entails.

Frequency tables

The selected frequency policy controls which set of frequencies is used in the CCU. It acts as an index to a second table, .freq_tbl, which in turn contains the frequencies that that specific frequency policy sets up. Each CCU has a specific number of frequencies in these tables.

Effectively, a frequency table looks sort of like this (written in a simplified, Python-like syntax for easier understanding):

ccu_freq_list0 = [26000000,26000000,26000000,26000000]
ccu_freq_list1 = [52000000,52000000,52000000,5200000]
...
ccu_freq_list6 = [208000000,104000000,104000000,1040000000]

ccu.freq_tbl = [
    ccu_freq_list0, ccu_freq_list1, ..., ccu_freq_list6
]
# actual number of frequencies in table is specific to each CCU

This is used by bus clocks - which frequencies correspond to which clocks is shown by the .freq_tbl_index member of the clock info struct. Clocks that have this value set to >= 0 specify that they're connected to that specific frequency; clocks with a value of -1 use a source clock or internal dividers instead.

# A bus clock will perform an operation like so:

current_policy = ccu.get_active_policy()  # CCU policy ID, from 0 to 3
freq_id = ccu.get_freq_policy(current_policy)  # read frequency ID (aka which table to use) for the current policy
ccu.freq_table[freq_id][bus_clk.freq_tbl_index]

Core clocks and freq policy

The second user of this is core clocks - in the core CCU, the frequency policy is used to determine which clock source is used for the CPU - see "Core clocks" section for details.

PI manager and freq policy

The PI manager is also able to set this through the pi_set_ccu_freq function, which is only used if CONFIG_CHANGE_POLICY_FOR_DFS is not defined (and it is not defined in the Grand Neo downstream kernel). See "Power Islands" section.

Voltage control

ccu_set_voltage / ccu_get_voltage

This is a DVFS (dynamic voltage/frequency scaling) setup that connects a specific frequency ID to a specific voltage ID.

Every frequency ID has a corresponding voltage ID in the voltage table, which are defined in the .freq_volt member of the CCU clock struct in downstream, and are written at initialization time to the CCU VLT registers (VLT0_3 and VLT4_7). Which voltage is selected depends on which frequency ID is selected.

No code in downstream appears to set/get these voltage tables after init, except for the debug code. Most likely, the default values are fine.

Peripheral voltage control

ccu_set_peri_voltage / no get function

Actually completely different from the voltage control, though they share the same voltage IDs in downstream.

There are two modes - NORMAL and HIGH. The voltage ID to use is written to the .vlt_peri_offset register.

As the name suggests, the peripheral voltage setting is used by peripheral clocks, which can select the voltage level - NORMAL or HIGH - to use by writing a bit using the .volt_lvl_mask, as done in the peri_clk_set_voltage_lvl function in downstream.

Relation to Power Islands

The Power Islands, besides being controlled mostly by the power manager, also have connections to the CCUs:

  • Every PI has one or more CCUs assigned to it (.ccu_id and .num_ccu_id in pi_mgr)
  • Every PI also has its own OPPs which get converted to CCU frequency IDs as per the .opp_info tables
  • PI reset offsets are located in the root CCU

Porting downstream clock struct to mainline

In downstream, look for a struct of type peri_clk for your clock. In mainline, open clk-bcm21664.c.

Here's a list of mainline clock struct members, and their downstream counterparts ((->bit) signifies that this mask must be converted to a bit shift first - see the RDB, there's a "shift" value next to the mask)

  • .clocks: CLOCKS(), fill with names of source clocks as seen in clock_source struct above your clock struct; put each name between double quotes.
  • .gate: depends on the gate type:
    • HW_SW_GATE[_AUTO] - these have a gating_sel_mask; use the _AUTO if AUTO_GATE is present in the .flags; in order: .clk_gate_offset, .stprsts_mask (->bit), .gating_sel_mask (->bit), .clk_en_mask (->bit)
    • {HW,SW}_ONLY_GATE - these have no gating_sel_mask; if .flags contains AUTO_GATE, then HW, else SW; in order: .clk_gate_offset, .stprsts_mask (->bit), .clk_en_mask (->bit)
  • .hyst: HYST(), in order: .clk_gate_offset, .hyst_val_mask (->bit), .hyst_en_mask (->bit)
  • .sel: SELECTOR(), in order: .clk_div.pll_select_offset, .pll_select_shift, width of .pll_select_mask
  • .div: DIVIDER(), in order: .clk_div.div_offset, .div_shift, width of .div_mask
  • .trig: TRIGGER(), in order: .clk_div.div_trig_offset, .div_trig_mask (->bit)
  • .policy: POLICY(), in order: the CCU's policy_mask{1/2}_offset (depending on .mask_set), .policy_bit_mask (->bit)

TPIU is some CoreSight thing. PTI is likely MIPS PTI (Parallel Trace Interface).