Overview
UFS stands for Universal Flash Storage, and is a replacement (or enhancement) over SD protocol for storage. The hardware is present for 2-3 years in phones now but most likely lacks any actual storage attached, so it wasn’t really necessary to concern oneself with UFS for mainlining up until recently.
It contains a core driver + a phy driver, similar to how USB works. In the case of billie2, the phy is listed as compatible = "qcom,ufs-phy-qmp-v3-660";
in downstream, and mainline only has v3
(or sdm845, where it was first preset (possibly)). So we’ll have to add v3-660
support.
Setup
The UFS PHY drivers live in drivers/phy/qualcomm/phy-qcom-qmp.c
in Mainline (if they are qmp, mine is). If you look at the implementation of sdm845 (closest cousin) you’ll see a bunch of properties, including:
- 4 init tables:
serdes
,tx
,rx
,pcs
that contain register + value clocks
,vregs
(regulators/supplies)regs
– a small table with register remapsnlanes
— max number of children in DTstart_ctrl
,pwddn_ctrl
value to put in certain registers for start/stopis_dual_lane_phy
— this is somewhat completely orthogonal tonlanes
, at least as far as code is concerned, and has to do with hardware initialization of one vs two lanesno_pcs_sw_reset
— whether reset happens via software (write to some register in driver), or “hardware” (using areset
property in DT).
Initialization Sequence
On downstream there is table_a
and table_b
, and on downstream there isn’t explicitly mentioned, but if you follow the code, you’ll see that serdes
, tx
, rx
, pcs
are used for initialization. The difference is that on downstream, all registers are specified with absolute offsets defined in the header drivers/phy/qualcomm/phy-qcom-ufs-qmp-v3-660.h
:
/* QCOM UFS PHY control registers */
#define COM_BASE 0x000
#define COM_OFF(x) (COM_BASE + x)
#define COM_SIZE 0x1C0
#define TX_BASE 0x400
#define TX_OFF(x) (TX_BASE + x)
#define TX_SIZE 0x128
#define RX_BASE 0x600
#define RX_OFF(x) (RX_BASE + x)
#define RX_SIZE 0x1FC
#define PHY_BASE 0xC00
#define PHY_OFF(x) (PHY_BASE + x)
#define PHY_SIZE 0x1B4
On mainline however, these offsets are not set in the code. Instead they are specified as separate reg values:
/* example from qcom/msm8996.dtsi */
ufsphy: phy@627000 {
/* ... */
ufsphy_lane: lanes@627400 {
reg = <0x627400 0x12c>,
<0x627600 0x200>,
<0x627c00 0x1b4>;
#phy-cells = <0>;
};
};
All this means, that registers on Downstream are defined via absolute offset, and on mainline they are defined with relative offset (from the start of the block), but you have to know which block they belong to, so that is why they are split into 4 tables.
Note that the mainline approach, while a bit cleaner (no macros in the register declaration) is less flexible — i.e you can’t mix writes to registers from different blocks (at least not easily).
So to transform DS to ML, one has to take the A+B tables, (B table is just one register+value), sort them by type (register names helpfully have a meaningful prefix, like QSERDES_RX_
for rx
registers, UFS_PHY_
is pcs
), and then put them in the corresponding table in mainline. I did check the downstream v3 and mainline sdm845 and the values follow this rule with minuscule exceptions.
Clocks and vregs
These are easy. Clocks — just copied the sdm845 setup, after verifying that downstream 845 had same clocks as my downstream. Regulators (vregs) seem to be the same on all phys so just copied those as well.
Lanes
Somewhat confusingly, there is nlanes
and is_dual_lane_phy
which seem redundant (based on name), but they actually describe different semantic lanes. The dual_lane_phy
is related to additional rx/tx register banks (so more elements in the reg property shown above), where nlanes
dictates the number of ufsphy_lane
nodes. So is_dual_lane_phy
is about lower-level lanes, and nlanes
is higher-level lanes, (in multiples of 1 or 2 depending on num of lower level lanes).
In our specific case there is 1 lower-level lane (is_dual_lane_phy = false
) and I can see only one higher level lane as well (so nlanes = 1
). To be fair even the other nlanes = 2
platforms still have one specified in DT, so I can’t verify this theory. It could be carried over from other phys (like usb phys), and the phy nlanes are wrongly specified as two because somebody didn’t think it through… IDK.
Registers and misc values
The register table and start/power down values are mostly the same as 845 values.
The QPHY_PCS_READY_STATUS
is set under pcs
registers with BIT(0)
, same as ufs_qcom_phy_qmp_v3_660_start_serdes
function in downstream.
QPHY_PCS_READY_STATUS
is used with PCS_READY
bit to poll until ready. Same as downstream ufs_qcom_phy_qmp_v3_660_is_pcs_ready
.
QPHY_POWER_DOWN_CONTROL
is used with .pwrdn_ctrl
to shut things down. Downstream fn is ufs_qcom_phy_qmp_v3_660_power_control
.
Troubleshooting
There wasn’t much to troubleshoot other than figuring out that init-tables need to be regrouped. Having 845 downstream code to compare to implemented mainline driver solved this mystery.
In general it’s a good idea to verify every single register value and find how it’s used. Never blindly copy code and hope it works.