|
| 1 | +From 07e943ccf6b98e941a39cd1f74db250e862151f0 Mon Sep 17 00:00:00 2001 |
| 2 | +From: Nishanth Sampath Kumar <nissampa@cisco.com> |
| 3 | +Date: Fri, 27 Mar 2026 14:48:36 -0700 |
| 4 | +Subject: [PATCH] at24: add SMBus fallback read/write for 16-bit addressed |
| 5 | + EEPROMs on PIIX4 adapters |
| 6 | + |
| 7 | +AMD PIIX4 SMBus adapters expose I2C_FUNC_SMBUS_BYTE_DATA and |
| 8 | +I2C_FUNC_SMBUS_WORD_DATA but do NOT support I2C_FUNC_I2C or |
| 9 | +I2C_FUNC_SMBUS_I2C_BLOCK. The existing at24 driver relies on |
| 10 | +regmap_get_i2c_bus() which requires one of those two capabilities; |
| 11 | +for reg_bits=16 (AT24_FLAG_ADDR16 EEPROMs) it therefore returns |
| 12 | +-ENOTSUPP and the EEPROM is inaccessible on these adapters. |
| 13 | + |
| 14 | +This patch detects the PIIX4 case (has BYTE_DATA + WORD_DATA but |
| 15 | +lacks I2C and BLOCK) and enables a lightweight SMBus-only fallback |
| 16 | +path: |
| 17 | + |
| 18 | + READ: i2c_smbus_write_byte_data(addr_hi, addr_lo) sets the |
| 19 | + 16-bit address pointer; repeated i2c_smbus_read_byte() |
| 20 | + calls fetch successive bytes (EEPROM auto-increments). |
| 21 | + |
| 22 | + WRITE: i2c_smbus_write_word_data(addr_hi, |
| 23 | + cpu_to_le16((data<<8)|addr_lo)) encodes the 16-bit address |
| 24 | + and data byte in one SMBus WORD write. |
| 25 | + |
| 26 | +When the fallback is active, regmap_config.reg_bits is forced to 8 |
| 27 | +so that regmap_get_i2c_bus() can succeed (regmap_smbus_byte is |
| 28 | +selected); all actual I/O bypasses regmap and goes through the two |
| 29 | +new helpers at24_smbus_read() / at24_smbus_write(). |
| 30 | + |
| 31 | +This fixes EEPROM access for Cisco 8000-series platforms that use |
| 32 | +the AMD SP5 SoC on-die PIIX4 SMBus controller to reach platform |
| 33 | +EEPROMs with 16-bit (2-byte) addressing. |
| 34 | + |
| 35 | +Signed-off-by: Nishanth Sampath Kumar <nissampa@cisco.com> |
| 36 | +--- |
| 37 | + drivers/misc/eeprom/at24.c | 134 ++++++++++++++++++++++++++++++++++++++++++++- |
| 38 | + 1 file changed, 133 insertions(+), 1 deletion(-) |
| 39 | + |
| 40 | +--- a/drivers/misc/eeprom/at24.c 2026-03-19 15:09:21.518103630 -0700 |
| 41 | ++++ b/drivers/misc/eeprom/at24.c 2026-03-19 15:10:18.279047438 -0700 |
| 42 | +@@ -88,6 +88,10 @@ |
| 43 | + struct regulator *vcc_reg; |
| 44 | + void (*read_post)(unsigned int off, char *buf, size_t count); |
| 45 | + |
| 46 | ++ /* SMBus-only fallback for 16-bit addressed EEPROMs on PIIX4 adapters */ |
| 47 | ++ bool smbus_addr16_fallback; |
| 48 | ++ struct i2c_client *smbus_client; |
| 49 | ++ |
| 50 | + /* |
| 51 | + * Some chips tie up multiple I2C addresses; dummy devices reserve |
| 52 | + * them for us. |
| 53 | +@@ -343,6 +347,101 @@ |
| 54 | + return count; |
| 55 | + } |
| 56 | + |
| 57 | ++/* forward declaration needed by at24_smbus_write below */ |
| 58 | ++static size_t at24_adjust_write_count(struct at24_data *at24, |
| 59 | ++ unsigned int offset, size_t count); |
| 60 | ++ |
| 61 | ++/* |
| 62 | ++ * SMBus primitive fallback for 16-bit addressed EEPROMs. |
| 63 | ++ * |
| 64 | ++ * PIIX4 SMBus supports BYTE_DATA and WORD_DATA but NOT I2C_FUNC_I2C or |
| 65 | ++ * I2C_FUNC_SMBUS_I2C_BLOCK, so regmap_get_i2c_bus() returns -ENOTSUPP |
| 66 | ++ * for reg_bits=16. We bypass regmap entirely for the I/O path: |
| 67 | ++ * |
| 68 | ++ * READ : write_byte_data(addr_hi, addr_lo) sets the 16-bit EEPROM |
| 69 | ++ * address pointer, then read_byte() fetches bytes one-by-one |
| 70 | ++ * (EEPROM auto-increments the pointer after each read). |
| 71 | ++ * |
| 72 | ++ * WRITE: write_word_data(addr_hi, cpu_to_le16((data<<8)|addr_lo)) |
| 73 | ++ * encodes the address and data byte in one SMBus word write. |
| 74 | ++ */ |
| 75 | ++static ssize_t at24_smbus_read(struct at24_data *at24, char *buf, |
| 76 | ++ unsigned int offset, size_t count) |
| 77 | ++{ |
| 78 | ++ struct i2c_client *client = at24->smbus_client; |
| 79 | ++ unsigned long timeout, read_time; |
| 80 | ++ u8 addr_hi = (offset >> 8) & 0xff; |
| 81 | ++ u8 addr_lo = offset & 0xff; |
| 82 | ++ ssize_t ret; |
| 83 | ++ size_t i; |
| 84 | ++ |
| 85 | ++ count = at24_adjust_read_count(at24, offset, count); |
| 86 | ++ |
| 87 | ++ timeout = jiffies + msecs_to_jiffies(at24_write_timeout); |
| 88 | ++ do { |
| 89 | ++ read_time = jiffies; |
| 90 | ++ |
| 91 | ++ /* Set 16-bit address pointer: command=addr_hi, value=addr_lo */ |
| 92 | ++ ret = i2c_smbus_write_byte_data(client, addr_hi, addr_lo); |
| 93 | ++ if (ret < 0) |
| 94 | ++ goto retry; |
| 95 | ++ |
| 96 | ++ /* Read bytes sequentially -- EEPROM auto-increments pointer */ |
| 97 | ++ for (i = 0; i < count; i++) { |
| 98 | ++ ret = i2c_smbus_read_byte(client); |
| 99 | ++ if (ret < 0) |
| 100 | ++ goto retry; |
| 101 | ++ buf[i] = (u8)ret; |
| 102 | ++ } |
| 103 | ++ dev_dbg(&client->dev, "smbus read %zu@%u --> ok\n", count, offset); |
| 104 | ++ return count; |
| 105 | ++retry: |
| 106 | ++ usleep_range(1000, 1500); |
| 107 | ++ } while (time_before(read_time, timeout)); |
| 108 | ++ |
| 109 | ++ return -ETIMEDOUT; |
| 110 | ++} |
| 111 | ++ |
| 112 | ++static ssize_t at24_smbus_write(struct at24_data *at24, const char *buf, |
| 113 | ++ unsigned int offset, size_t count) |
| 114 | ++{ |
| 115 | ++ struct i2c_client *client = at24->smbus_client; |
| 116 | ++ unsigned long timeout, write_time; |
| 117 | ++ size_t i; |
| 118 | ++ int ret; |
| 119 | ++ |
| 120 | ++ /* Write one byte at a time (page writes need raw I2C transfers) */ |
| 121 | ++ count = at24_adjust_write_count(at24, offset, count); |
| 122 | ++ |
| 123 | ++ for (i = 0; i < count; i++, offset++) { |
| 124 | ++ timeout = jiffies + msecs_to_jiffies(at24_write_timeout); |
| 125 | ++ do { |
| 126 | ++ write_time = jiffies; |
| 127 | ++ /* |
| 128 | ++ * command = addr[15:8] |
| 129 | ++ * word LSB = addr[7:0] |
| 130 | ++ * word MSB = data byte |
| 131 | ++ * EEPROM: set 16-bit address then write one data byte. |
| 132 | ++ */ |
| 133 | ++ ret = i2c_smbus_write_word_data(client, |
| 134 | ++ (offset >> 8) & 0xff, |
| 135 | ++ cpu_to_le16(((u16)(u8)buf[i] << 8) | |
| 136 | ++ (offset & 0xff))); |
| 137 | ++ if (!ret) { |
| 138 | ++ dev_dbg(&client->dev, "smbus write 1@%u --> ok\n", |
| 139 | ++ offset); |
| 140 | ++ break; |
| 141 | ++ } |
| 142 | ++ usleep_range(1000, 1500); |
| 143 | ++ } while (time_before(write_time, timeout)); |
| 144 | ++ |
| 145 | ++ if (ret) |
| 146 | ++ return (i > 0) ? (ssize_t)i : -ETIMEDOUT; |
| 147 | ++ } |
| 148 | ++ |
| 149 | ++ return count; |
| 150 | ++} |
| 151 | ++ |
| 152 | + static ssize_t at24_regmap_read(struct at24_data *at24, char *buf, |
| 153 | + unsigned int offset, size_t count) |
| 154 | + { |
| 155 | +@@ -353,6 +452,9 @@ |
| 156 | + regmap = at24_translate_offset(at24, &offset); |
| 157 | + count = at24_adjust_read_count(at24, offset, count); |
| 158 | + |
| 159 | ++ if (at24->smbus_addr16_fallback) |
| 160 | ++ return at24_smbus_read(at24, buf, offset, count); |
| 161 | ++ |
| 162 | + /* adjust offset for mac and serial read ops */ |
| 163 | + offset += at24->offset_adj; |
| 164 | + |
| 165 | +@@ -411,6 +513,10 @@ |
| 166 | + |
| 167 | + regmap = at24_translate_offset(at24, &offset); |
| 168 | + count = at24_adjust_write_count(at24, offset, count); |
| 169 | ++ |
| 170 | ++ if (at24->smbus_addr16_fallback) |
| 171 | ++ return at24_smbus_write(at24, buf, offset, count); |
| 172 | ++ |
| 173 | + timeout = jiffies + msecs_to_jiffies(at24_write_timeout); |
| 174 | + |
| 175 | + do { |
| 176 | +@@ -601,6 +707,7 @@ |
| 177 | + bool i2c_fn_i2c, i2c_fn_block; |
| 178 | + unsigned int i, num_addresses; |
| 179 | + struct at24_data *at24; |
| 180 | ++ bool smbus_addr16_fallback; |
| 181 | + bool full_power; |
| 182 | + struct regmap *regmap; |
| 183 | + bool writable; |
| 184 | +@@ -611,6 +718,23 @@ |
| 185 | + i2c_fn_block = i2c_check_functionality(client->adapter, |
| 186 | + I2C_FUNC_SMBUS_WRITE_I2C_BLOCK); |
| 187 | + |
| 188 | ++ /* |
| 189 | ++ * Detect SMBus-only adapters (e.g. AMD PIIX4) that have BYTE_DATA + |
| 190 | ++ * WORD_DATA but lack I2C_FUNC_I2C and I2C_FUNC_SMBUS_I2C_BLOCK. |
| 191 | ++ * For 16-bit addressed EEPROMs, bypass regmap and use SMBus helpers. |
| 192 | ++ */ |
| 193 | ++ smbus_addr16_fallback = false; |
| 194 | ++ if (!i2c_fn_i2c && !i2c_fn_block) { |
| 195 | ++ bool has_byte = i2c_check_functionality(client->adapter, |
| 196 | ++ I2C_FUNC_SMBUS_BYTE_DATA); |
| 197 | ++ bool has_word = i2c_check_functionality(client->adapter, |
| 198 | ++ I2C_FUNC_SMBUS_WORD_DATA); |
| 199 | ++ if (has_byte && has_word) { |
| 200 | ++ smbus_addr16_fallback = true; |
| 201 | ++ dev_dbg(dev, "SMBus-only adapter: enabling 16-bit addr SMBus fallback\n"); |
| 202 | ++ } |
| 203 | ++ } |
| 204 | ++ |
| 205 | + cdata = i2c_get_match_data(client); |
| 206 | + if (!cdata) |
| 207 | + return -ENODEV; |
| 208 | +@@ -681,6 +805,13 @@ |
| 209 | + regmap_config.val_bits = 8; |
| 210 | + regmap_config.reg_bits = (flags & AT24_FLAG_ADDR16) ? 16 : 8; |
| 211 | + regmap_config.disable_locking = true; |
| 212 | ++ /* |
| 213 | ++ * Force reg_bits=8 when the SMBus fallback path is active so that |
| 214 | ++ * regmap_get_i2c_bus() can pick regmap_smbus_byte and succeed on PIIX4. |
| 215 | ++ * The actual 16-bit address I/O is done entirely by our helpers. |
| 216 | ++ */ |
| 217 | ++ if (smbus_addr16_fallback && (flags & AT24_FLAG_ADDR16)) |
| 218 | ++ regmap_config.reg_bits = 8; |
| 219 | + |
| 220 | + regmap = devm_regmap_init_i2c(client, ®map_config); |
| 221 | + if (IS_ERR(regmap)) |
| 222 | +@@ -700,6 +831,8 @@ |
| 223 | + at24->num_addresses = num_addresses; |
| 224 | + at24->offset_adj = at24_get_offset_adj(flags, byte_len); |
| 225 | + at24->client_regmaps[0] = regmap; |
| 226 | ++ at24->smbus_addr16_fallback = smbus_addr16_fallback; |
| 227 | ++ at24->smbus_client = client; |
| 228 | + |
| 229 | + at24->vcc_reg = devm_regulator_get(dev, "vcc"); |
| 230 | + if (IS_ERR(at24->vcc_reg)) |
0 commit comments