developer | a29111c | 2023-01-12 10:20:04 +0800 | [diff] [blame] | 1 | diff --git a/drivers/net/phy/Kconfig b/drivers/net/phy/Kconfig |
| 2 | index 207046b..21a4497 100644 |
| 3 | --- a/drivers/net/phy/Kconfig |
| 4 | +++ b/drivers/net/phy/Kconfig |
| 5 | @@ -350,6 +350,11 @@ config AIROHA_EN8801SC_PHY |
| 6 | ---help--- |
| 7 | Currently supports the Airoha EN8801S PHY for MT7981 SoC. |
| 8 | |
| 9 | +config AIROHA_EN8811H_PHY |
| 10 | + tristate "Drivers for Airoha EN8811H 2.5G Gigabit PHY" |
| 11 | + ---help--- |
| 12 | + Currently supports the Airoha EN8811H PHY. |
| 13 | + |
| 14 | config ADIN_PHY |
| 15 | tristate "Analog Devices Industrial Ethernet PHYs" |
| 16 | help |
| 17 | diff --git a/drivers/net/phy/Makefile b/drivers/net/phy/Makefile |
| 18 | index 1b13c02..744b249 100644 |
| 19 | --- a/drivers/net/phy/Makefile |
| 20 | +++ b/drivers/net/phy/Makefile |
| 21 | @@ -71,6 +71,7 @@ ifdef CONFIG_AQUANTIA_PHY_FW_DOWNLOAD |
| 22 | ifdef CONFIG_HWMON |
| 23 | aquantia-objs += aquantia_hwmon.o |
| 24 | endif |
| 25 | +obj-$(CONFIG_AIROHA_EN8811H_PHY) += air_en8811h.o |
| 26 | obj-$(CONFIG_AQUANTIA_PHY) += aquantia.o |
| 27 | obj-$(CONFIG_AX88796B_PHY) += ax88796b.o |
| 28 | obj-$(CONFIG_AT803X_PHY) += at803x.o |
| 29 | diff --git a/drivers/net/phy/air_en8811h.c b/drivers/net/phy/air_en8811h.c |
| 30 | new file mode 100644 |
| 31 | index 0000000..cf564fc |
| 32 | --- /dev/null |
| 33 | +++ b/drivers/net/phy/air_en8811h.c |
| 34 | @@ -0,0 +1,702 @@ |
| 35 | +// SPDX-License-Identifier: GPL-2.0+ |
| 36 | + |
| 37 | +/* FILE NAME: air_en8811h.c |
| 38 | + * PURPOSE: |
| 39 | + * EN8811H phy driver for Linux |
| 40 | + * NOTES: |
| 41 | + * |
| 42 | + */ |
| 43 | + |
| 44 | +/* INCLUDE FILE DECLARATIONS |
| 45 | + */ |
| 46 | +#include <linux/kernel.h> |
| 47 | +#include <linux/errno.h> |
| 48 | +#include <linux/init.h> |
| 49 | +#include <linux/module.h> |
| 50 | +#include <linux/mii.h> |
| 51 | +#include <linux/phy.h> |
| 52 | +#include <linux/delay.h> |
| 53 | +#include <linux/ethtool.h> |
| 54 | +#include <linux/delay.h> |
| 55 | +#include <linux/version.h> |
| 56 | +#include <linux/firmware.h> |
| 57 | +#include <linux/crc32.h> |
| 58 | + |
| 59 | +#include "air_en8811h.h" |
| 60 | + |
| 61 | +#if (LINUX_VERSION_CODE < KERNEL_VERSION(4, 5, 0)) |
| 62 | +#define phydev_mdio_bus(_dev) (_dev->bus) |
| 63 | +#define phydev_addr(_dev) (_dev->addr) |
| 64 | +#define phydev_dev(_dev) (&_dev->dev) |
| 65 | +#else |
| 66 | +#define phydev_mdio_bus(_dev) (_dev->mdio.bus) |
| 67 | +#define phydev_addr(_dev) (_dev->mdio.addr) |
| 68 | +#define phydev_dev(_dev) (&_dev->mdio.dev) |
| 69 | +#endif |
| 70 | + |
| 71 | +MODULE_DESCRIPTION("Airoha EN8811H PHY drivers"); |
| 72 | +MODULE_AUTHOR("Airoha"); |
| 73 | +MODULE_LICENSE("GPL"); |
| 74 | + |
| 75 | +/* |
| 76 | +GPIO5 <-> BASE_T_LED0, |
| 77 | +GPIO4 <-> BASE_T_LED1, |
| 78 | +GPIO3 <-> BASE_T_LED2, |
| 79 | +*/ |
| 80 | +/* User-defined.B */ |
| 81 | +#define AIR_LED_SUPPORT |
| 82 | +#ifdef AIR_LED_SUPPORT |
| 83 | +static const AIR_BASE_T_LED_CFG_T led_cfg[3] = |
| 84 | +{ |
| 85 | + /* |
| 86 | + * LED Enable, GPIO, LED Polarity, LED ON, LED Blink |
| 87 | + */ |
| 88 | + {LED_ENABLE, AIR_LED0_GPIO5, AIR_ACTIVE_HIGH, BASE_T_LED0_ON_CFG, BASE_T_LED0_BLK_CFG}, /* BASE-T LED0 */ |
| 89 | + {LED_ENABLE, AIR_LED1_GPIO4, AIR_ACTIVE_HIGH, BASE_T_LED1_ON_CFG, BASE_T_LED1_BLK_CFG}, /* BASE-T LED1 */ |
| 90 | + {LED_ENABLE, AIR_LED2_GPIO3, AIR_ACTIVE_HIGH, BASE_T_LED2_ON_CFG, BASE_T_LED2_BLK_CFG}, /* BASE-T LED2 */ |
| 91 | +}; |
| 92 | +static const u16 led_dur = UNIT_LED_BLINK_DURATION << AIR_LED_BLK_DUR_64M; |
| 93 | +#endif |
| 94 | +/* User-defined.E */ |
| 95 | + |
| 96 | +/************************************************************************ |
| 97 | +* F U N C T I O N S |
| 98 | +************************************************************************/ |
| 99 | +#if 0 |
| 100 | +/* Airoha MII read function */ |
| 101 | +static int air_mii_cl22_read(struct mii_bus *ebus, unsigned int phy_addr,unsigned int phy_register) |
| 102 | +{ |
| 103 | + int read_data; |
| 104 | + read_data = mdiobus_read(ebus, phy_addr, phy_register); |
| 105 | + return read_data; |
| 106 | +} |
| 107 | +#endif |
| 108 | +/* Airoha MII write function */ |
| 109 | +static int air_mii_cl22_write(struct mii_bus *ebus, unsigned int phy_addr, unsigned int phy_register,unsigned int write_data) |
| 110 | +{ |
| 111 | + int ret = 0; |
| 112 | + ret = mdiobus_write(ebus, phy_addr, phy_register, write_data); |
| 113 | + return ret; |
| 114 | +} |
| 115 | + |
| 116 | +static int air_mii_cl45_read(struct phy_device *phydev, int devad, u16 reg) |
| 117 | +{ |
| 118 | + int ret = 0; |
| 119 | + int data; |
| 120 | + struct device *dev = phydev_dev(phydev); |
| 121 | + ret = phy_write(phydev, MII_MMD_ACC_CTL_REG, devad); |
| 122 | + if (ret < 0) { |
| 123 | + dev_err(dev, "phy_write, ret: %d\n", ret); |
| 124 | + return INVALID_DATA; |
| 125 | + } |
| 126 | + ret = phy_write(phydev, MII_MMD_ADDR_DATA_REG, reg); |
| 127 | + if (ret < 0) { |
| 128 | + dev_err(dev, "phy_write, ret: %d\n", ret); |
| 129 | + return INVALID_DATA; |
| 130 | + } |
| 131 | + ret = phy_write(phydev, MII_MMD_ACC_CTL_REG, MMD_OP_MODE_DATA | devad); |
| 132 | + if (ret < 0) { |
| 133 | + dev_err(dev, "phy_write, ret: %d\n", ret); |
| 134 | + return INVALID_DATA; |
| 135 | + } |
| 136 | + data = phy_read(phydev, MII_MMD_ADDR_DATA_REG); |
| 137 | + return data; |
| 138 | +} |
| 139 | + |
| 140 | +static int air_mii_cl45_write(struct phy_device *phydev, int devad, u16 reg, u16 write_data) |
| 141 | +{ |
| 142 | + int ret = 0; |
| 143 | + struct device *dev = phydev_dev(phydev); |
| 144 | + ret = phy_write(phydev, MII_MMD_ACC_CTL_REG, devad); |
| 145 | + if (ret < 0) { |
| 146 | + dev_err(dev, "phy_write, ret: %d\n", ret); |
| 147 | + return ret; |
| 148 | + } |
| 149 | + ret = phy_write(phydev, MII_MMD_ADDR_DATA_REG, reg); |
| 150 | + if (ret < 0) { |
| 151 | + dev_err(dev, "phy_write, ret: %d\n", ret); |
| 152 | + return ret; |
| 153 | + } |
| 154 | + ret = phy_write(phydev, MII_MMD_ACC_CTL_REG, MMD_OP_MODE_DATA | devad); |
| 155 | + if (ret < 0) { |
| 156 | + dev_err(dev, "phy_write, ret: %d\n", ret); |
| 157 | + return ret; |
| 158 | + } |
| 159 | + ret = phy_write(phydev, MII_MMD_ADDR_DATA_REG, write_data); |
| 160 | + if (ret < 0) { |
| 161 | + dev_err(dev, "phy_write, ret: %d\n", ret); |
| 162 | + return ret; |
| 163 | + } |
| 164 | + return 0; |
| 165 | +} |
| 166 | +/* Use default PBUS_PHY_ID */ |
| 167 | +/* EN8811H PBUS write function */ |
| 168 | +static int air_pbus_reg_write(struct phy_device *phydev, unsigned long pbus_address, unsigned long pbus_data) |
| 169 | +{ |
| 170 | + struct mii_bus *mbus = phydev_mdio_bus(phydev); |
| 171 | + int addr = phydev_addr(phydev); |
| 172 | + int ret = 0; |
| 173 | + ret = air_mii_cl22_write(mbus, (addr + 8), 0x1F, (unsigned int)(pbus_address >> 6)); |
| 174 | + AIR_RTN_ERR(ret); |
| 175 | + ret = air_mii_cl22_write(mbus, (addr + 8), (unsigned int)((pbus_address >> 2) & 0xf), (unsigned int)(pbus_data & 0xFFFF)); |
| 176 | + AIR_RTN_ERR(ret); |
| 177 | + ret = air_mii_cl22_write(mbus, (addr + 8), 0x10, (unsigned int)(pbus_data >> 16)); |
| 178 | + AIR_RTN_ERR(ret); |
| 179 | + return 0; |
| 180 | +} |
| 181 | + |
| 182 | +/* EN8811H BUCK write function */ |
| 183 | +static int air_buckpbus_reg_write(struct phy_device *phydev, unsigned long pbus_address, unsigned int pbus_data) |
| 184 | +{ |
| 185 | + int ret = 0; |
| 186 | + struct device *dev = phydev_dev(phydev); |
| 187 | + ret = phy_write(phydev, 0x1F, (unsigned int)4); /* page 4 */ |
| 188 | + if (ret < 0) { |
| 189 | + dev_err(dev, "phy_write, ret: %d\n", ret); |
| 190 | + return ret; |
| 191 | + } |
| 192 | + ret = phy_write(phydev, 0x10, (unsigned int)0); |
| 193 | + if (ret < 0) { |
| 194 | + dev_err(dev, "phy_write, ret: %d\n", ret); |
| 195 | + return ret; |
| 196 | + } |
| 197 | + ret = phy_write(phydev, 0x11, (unsigned int)((pbus_address >> 16) & 0xffff)); |
| 198 | + if (ret < 0) { |
| 199 | + dev_err(dev, "phy_write, ret: %d\n", ret); |
| 200 | + return ret; |
| 201 | + } |
| 202 | + ret = phy_write(phydev, 0x12, (unsigned int)(pbus_address & 0xffff)); |
| 203 | + if (ret < 0) { |
| 204 | + dev_err(dev, "phy_write, ret: %d\n", ret); |
| 205 | + return ret; |
| 206 | + } |
| 207 | + ret = phy_write(phydev, 0x13, (unsigned int)((pbus_data >> 16) & 0xffff)); |
| 208 | + if (ret < 0) { |
| 209 | + dev_err(dev, "phy_write, ret: %d\n", ret); |
| 210 | + return ret; |
| 211 | + } |
| 212 | + ret = phy_write(phydev, 0x14, (unsigned int)(pbus_data & 0xffff)); |
| 213 | + if (ret < 0) { |
| 214 | + dev_err(dev, "phy_write, ret: %d\n", ret); |
| 215 | + return ret; |
| 216 | + } |
| 217 | + ret = phy_write(phydev, 0x1F, 0); |
| 218 | + if (ret < 0) { |
| 219 | + dev_err(dev, "phy_write, ret: %d\n", ret); |
| 220 | + return ret; |
| 221 | + } |
| 222 | + return 0; |
| 223 | +} |
| 224 | + |
| 225 | +/* EN8811H BUCK read function */ |
| 226 | +static unsigned int air_buckpbus_reg_read(struct phy_device *phydev, unsigned long pbus_address) |
| 227 | +{ |
| 228 | + unsigned int pbus_data = 0, pbus_data_low, pbus_data_high; |
| 229 | + int ret = 0; |
| 230 | + struct device *dev = phydev_dev(phydev); |
| 231 | + ret = phy_write(phydev, 0x1F, (unsigned int)4); /* page 4 */ |
| 232 | + if (ret < 0) { |
| 233 | + dev_err(dev, "phy_write, ret: %d\n", ret); |
| 234 | + return PBUS_INVALID_DATA; |
| 235 | + } |
| 236 | + ret = phy_write(phydev, 0x10, (unsigned int)0); |
| 237 | + if (ret < 0) { |
| 238 | + dev_err(dev, "phy_write, ret: %d\n", ret); |
| 239 | + return PBUS_INVALID_DATA; |
| 240 | + } |
| 241 | + ret = phy_write(phydev, 0x15, (unsigned int)((pbus_address >> 16) & 0xffff)); |
| 242 | + if (ret < 0) { |
| 243 | + dev_err(dev, "phy_write, ret: %d\n", ret); |
| 244 | + return PBUS_INVALID_DATA; |
| 245 | + } |
| 246 | + ret = phy_write(phydev, 0x16, (unsigned int)(pbus_address & 0xffff)); |
| 247 | + if (ret < 0) { |
| 248 | + dev_err(dev, "phy_write, ret: %d\n", ret); |
| 249 | + return PBUS_INVALID_DATA; |
| 250 | + } |
| 251 | + |
| 252 | + pbus_data_high = phy_read(phydev, 0x17); |
| 253 | + pbus_data_low = phy_read(phydev, 0x18); |
| 254 | + pbus_data = (pbus_data_high << 16) + pbus_data_low; |
| 255 | + ret = phy_write(phydev, 0x1F, 0); |
| 256 | + if (ret < 0) { |
| 257 | + dev_err(dev, "phy_write, ret: %d\n", ret); |
| 258 | + return ret; |
| 259 | + } |
| 260 | + return pbus_data; |
| 261 | +} |
| 262 | + |
| 263 | +static int MDIOWriteBuf(struct phy_device *phydev, unsigned long address, const struct firmware *fw) |
| 264 | +{ |
| 265 | + unsigned int write_data, offset ; |
| 266 | + int ret = 0; |
| 267 | + struct device *dev = phydev_dev(phydev); |
| 268 | + ret = phy_write(phydev, 0x1F, (unsigned int)4); /* page 4 */ |
| 269 | + if (ret < 0) { |
| 270 | + dev_err(dev, "phy_write, ret: %d\n", ret); |
| 271 | + return ret; |
| 272 | + } |
| 273 | + ret = phy_write(phydev, 0x10, (unsigned int)0x8000); /* address increment*/ |
| 274 | + if (ret < 0) { |
| 275 | + dev_err(dev, "phy_write, ret: %d\n", ret); |
| 276 | + return ret; |
| 277 | + } |
| 278 | + ret = phy_write(phydev, 0x11, (unsigned int)((address >> 16) & 0xffff)); |
| 279 | + if (ret < 0) { |
| 280 | + dev_err(dev, "phy_write, ret: %d\n", ret); |
| 281 | + return ret; |
| 282 | + } |
| 283 | + ret = phy_write(phydev, 0x12, (unsigned int)(address & 0xffff)); |
| 284 | + if (ret < 0) { |
| 285 | + dev_err(dev, "phy_write, ret: %d\n", ret); |
| 286 | + return ret; |
| 287 | + } |
| 288 | + |
| 289 | + for (offset = 0; offset < fw->size; offset += 4) |
| 290 | + { |
| 291 | + write_data = (fw->data[offset + 3] << 8) | fw->data[offset + 2]; |
| 292 | + ret = phy_write(phydev, 0x13, write_data); |
| 293 | + if (ret < 0) { |
| 294 | + dev_err(dev, "phy_write, ret: %d\n", ret); |
| 295 | + return ret; |
| 296 | + } |
| 297 | + write_data = (fw->data[offset + 1] << 8) | fw->data[offset]; |
| 298 | + ret = phy_write(phydev, 0x14, write_data); |
| 299 | + if (ret < 0) { |
| 300 | + dev_err(dev, "phy_write, ret: %d\n", ret); |
| 301 | + return ret; |
| 302 | + } |
| 303 | + } |
| 304 | + ret = phy_write(phydev, 0x1F, (unsigned int)0); |
| 305 | + if (ret < 0) { |
| 306 | + dev_err(dev, "phy_write, ret: %d\n", ret); |
| 307 | + return ret; |
| 308 | + } |
| 309 | + return 0; |
| 310 | +} |
| 311 | + |
| 312 | +static int en8811h_load_firmware(struct phy_device *phydev) |
| 313 | +{ |
| 314 | + struct device *dev = phydev_dev(phydev); |
| 315 | + const struct firmware *fw; |
| 316 | + const char *firmware; |
| 317 | + int ret = 0; |
| 318 | + unsigned int crc32; |
| 319 | + u32 pbus_value = 0; |
| 320 | + |
| 321 | + ret = air_buckpbus_reg_write(phydev, 0x0f0018, 0x0); |
| 322 | + AIR_RTN_ERR(ret); |
| 323 | + pbus_value = air_buckpbus_reg_read(phydev, 0x800000); |
| 324 | + pbus_value |= BIT(11); |
| 325 | + ret = air_buckpbus_reg_write(phydev, 0x800000, pbus_value); |
| 326 | + AIR_RTN_ERR(ret); |
| 327 | + firmware = EN8811H_MD32_DM; |
| 328 | + ret = request_firmware_direct(&fw, firmware, dev); |
| 329 | + if (ret < 0) { |
| 330 | + dev_info(dev, "failed to load firmware %s, ret: %d\n", firmware, ret); |
| 331 | + return ret; |
| 332 | + } |
| 333 | + crc32 = ~crc32(~0, fw->data, fw->size); |
| 334 | + dev_info(dev, "%s: crc32=0x%x\n", firmware, crc32); |
| 335 | + /* Download DM */ |
| 336 | + ret = MDIOWriteBuf(phydev, 0x00000000, fw); |
| 337 | + if (ret < 0) { |
| 338 | + dev_info(dev, "MDIOWriteBuf 0x00000000 fail, ret: %d\n", ret); |
| 339 | + return ret; |
| 340 | + } |
| 341 | + release_firmware(fw); |
| 342 | + |
| 343 | + firmware = EN8811H_MD32_DSP; |
| 344 | + ret = request_firmware_direct(&fw, firmware, dev); |
| 345 | + if (ret < 0) { |
| 346 | + dev_info(dev, "failed to load firmware %s, ret: %d\n", firmware, ret); |
| 347 | + return ret; |
| 348 | + } |
| 349 | + crc32 = ~crc32(~0, fw->data, fw->size); |
| 350 | + dev_info(dev, "%s: crc32=0x%x\n", firmware, crc32); |
| 351 | + /* Download PM */ |
| 352 | + ret = MDIOWriteBuf(phydev, 0x00100000, fw); |
| 353 | + if (ret < 0) { |
| 354 | + dev_info(dev, "MDIOWriteBuf 0x00100000 fail , ret: %d\n", ret); |
| 355 | + return ret; |
| 356 | + } |
| 357 | + release_firmware(fw); |
| 358 | + |
| 359 | + pbus_value = air_buckpbus_reg_read(phydev, 0x800000); |
| 360 | + pbus_value &= ~BIT(11); |
| 361 | + ret = air_buckpbus_reg_write(phydev, 0x800000, pbus_value); |
| 362 | + AIR_RTN_ERR(ret); |
| 363 | + ret = air_buckpbus_reg_write(phydev, 0x0f0018, 0x01); |
| 364 | + AIR_RTN_ERR(ret); |
| 365 | + return 0; |
| 366 | +} |
| 367 | + |
| 368 | +#ifdef AIR_LED_SUPPORT |
| 369 | +static int airoha_led_set_usr_def(struct phy_device *phydev, u8 entity, int polar, |
| 370 | + u16 on_evt, u16 blk_evt) |
| 371 | +{ |
| 372 | + int ret = 0; |
| 373 | + if (AIR_ACTIVE_HIGH == polar) { |
| 374 | + on_evt |= LED_ON_POL; |
| 375 | + } else { |
| 376 | + on_evt &= ~LED_ON_POL ; |
| 377 | + } |
| 378 | + ret = air_mii_cl45_write(phydev, 0x1f, LED_ON_CTRL(entity), on_evt | LED_ON_EN); |
| 379 | + AIR_RTN_ERR(ret); |
| 380 | + ret = air_mii_cl45_write(phydev, 0x1f, LED_BLK_CTRL(entity), blk_evt); |
| 381 | + AIR_RTN_ERR(ret); |
| 382 | + return 0; |
| 383 | +} |
| 384 | + |
| 385 | +static int airoha_led_set_mode(struct phy_device *phydev, u8 mode) |
| 386 | +{ |
| 387 | + u16 cl45_data; |
| 388 | + int err = 0; |
| 389 | + struct device *dev = phydev_dev(phydev); |
| 390 | + cl45_data = air_mii_cl45_read(phydev, 0x1f, LED_BCR); |
| 391 | + switch (mode) { |
| 392 | + case AIR_LED_MODE_DISABLE: |
| 393 | + cl45_data &= ~LED_BCR_EXT_CTRL; |
| 394 | + cl45_data &= ~LED_BCR_MODE_MASK; |
| 395 | + cl45_data |= LED_BCR_MODE_DISABLE; |
| 396 | + break; |
| 397 | + case AIR_LED_MODE_USER_DEFINE: |
| 398 | + cl45_data |= LED_BCR_EXT_CTRL; |
| 399 | + cl45_data |= LED_BCR_CLK_EN; |
| 400 | + break; |
| 401 | + default: |
| 402 | + dev_err(dev, "LED mode%d is not supported!\n", mode); |
| 403 | + return -EINVAL; |
| 404 | + } |
| 405 | + err = air_mii_cl45_write(phydev, 0x1f, LED_BCR, cl45_data); |
| 406 | + AIR_RTN_ERR(err); |
| 407 | + return 0; |
| 408 | +} |
| 409 | + |
| 410 | +static int airoha_led_set_state(struct phy_device *phydev, u8 entity, u8 state) |
| 411 | +{ |
| 412 | + u16 cl45_data = 0; |
| 413 | + int err; |
| 414 | + |
| 415 | + cl45_data = air_mii_cl45_read(phydev, 0x1f, LED_ON_CTRL(entity)); |
| 416 | + if (LED_ENABLE == state) { |
| 417 | + cl45_data |= LED_ON_EN; |
| 418 | + } else { |
| 419 | + cl45_data &= ~LED_ON_EN; |
| 420 | + } |
| 421 | + |
| 422 | + err = air_mii_cl45_write(phydev, 0x1f, LED_ON_CTRL(entity), cl45_data); |
| 423 | + AIR_RTN_ERR(err); |
| 424 | + return 0; |
| 425 | +} |
| 426 | + |
| 427 | +static int en8811h_led_init(struct phy_device *phydev) |
| 428 | +{ |
| 429 | + |
| 430 | + unsigned long led_gpio = 0, reg_value = 0; |
| 431 | + u16 cl45_data = led_dur; |
| 432 | + int ret = 0, led_id; |
| 433 | + struct device *dev = phydev_dev(phydev); |
| 434 | + ret = air_mii_cl45_write(phydev, 0x1f, LED_BLK_DUR, cl45_data); |
| 435 | + AIR_RTN_ERR(ret); |
| 436 | + cl45_data >>= 1; |
| 437 | + ret = air_mii_cl45_write(phydev, 0x1f, LED_ON_DUR, cl45_data); |
| 438 | + AIR_RTN_ERR(ret); |
| 439 | + ret = airoha_led_set_mode(phydev, AIR_LED_MODE_USER_DEFINE); |
| 440 | + if (ret != 0) { |
| 441 | + dev_err(dev, "LED fail to set mode, ret %d !\n", ret); |
| 442 | + return ret; |
| 443 | + } |
| 444 | + for(led_id = 0; led_id < EN8811H_LED_COUNT; led_id++) |
| 445 | + { |
| 446 | + /* LED0 <-> GPIO5, LED1 <-> GPIO4, LED0 <-> GPIO3 */ |
| 447 | + if (led_cfg[led_id].gpio != (led_id + (AIR_LED0_GPIO5 - (2 * led_id)))) |
| 448 | + { |
| 449 | + dev_err(dev, "LED%d uses incorrect GPIO%d !\n", led_id, led_cfg[led_id].gpio); |
| 450 | + return -EINVAL; |
| 451 | + } |
| 452 | + ret = airoha_led_set_state(phydev, led_id, led_cfg[led_id].en); |
| 453 | + if (ret != 0) |
| 454 | + { |
| 455 | + dev_err(dev, "LED fail to set state, ret %d !\n", ret); |
| 456 | + return ret; |
| 457 | + } |
| 458 | + if (LED_ENABLE == led_cfg[led_id].en) |
| 459 | + { |
| 460 | + led_gpio |= BIT(led_cfg[led_id].gpio); |
| 461 | + ret = airoha_led_set_usr_def(phydev, led_id, led_cfg[led_id].pol, led_cfg[led_id].on_cfg, led_cfg[led_id].blk_cfg); |
| 462 | + if (ret != 0) |
| 463 | + { |
| 464 | + dev_err(dev, "LED fail to set default, ret %d !\n", ret); |
| 465 | + return ret; |
| 466 | + } |
| 467 | + } |
| 468 | + } |
| 469 | + reg_value = air_buckpbus_reg_read(phydev, 0xcf8b8) | led_gpio; |
| 470 | + ret = air_buckpbus_reg_write(phydev, 0xcf8b8, reg_value); |
| 471 | + AIR_RTN_ERR(ret); |
| 472 | + |
| 473 | + dev_info(dev, "LED initialize OK !\n"); |
| 474 | + return 0; |
| 475 | +} |
| 476 | +#endif /* AIR_LED_SUPPORT */ |
| 477 | +#if (LINUX_VERSION_CODE > KERNEL_VERSION(4, 5, 0)) |
| 478 | +static int en8811h_get_features(struct phy_device *phydev) |
| 479 | +{ |
| 480 | + int ret; |
| 481 | + struct device *dev = phydev_dev(phydev); |
| 482 | + dev_info(dev, "%s()\n", __func__); |
| 483 | + ret = air_pbus_reg_write(phydev, 0xcf928 , 0x0); |
| 484 | + AIR_RTN_ERR(ret); |
| 485 | + ret = genphy_read_abilities(phydev); |
| 486 | + if (ret) |
| 487 | + return ret; |
| 488 | + |
| 489 | + /* EN8811H supports 100M/1G/2.5G speed. */ |
| 490 | + linkmode_clear_bit(ETHTOOL_LINK_MODE_10baseT_Half_BIT, |
| 491 | + phydev->supported); |
| 492 | + linkmode_clear_bit(ETHTOOL_LINK_MODE_10baseT_Full_BIT, |
| 493 | + phydev->supported); |
| 494 | + linkmode_set_bit(ETHTOOL_LINK_MODE_100baseT_Half_BIT, |
| 495 | + phydev->supported); |
| 496 | + linkmode_set_bit(ETHTOOL_LINK_MODE_100baseT_Full_BIT, |
| 497 | + phydev->supported); |
| 498 | + linkmode_set_bit(ETHTOOL_LINK_MODE_1000baseT_Full_BIT, |
| 499 | + phydev->supported); |
| 500 | + linkmode_set_bit(ETHTOOL_LINK_MODE_2500baseX_Full_BIT, |
| 501 | + phydev->supported); |
| 502 | + return 0; |
| 503 | +} |
| 504 | +#endif |
| 505 | +static int en8811h_phy_probe(struct phy_device *phydev) |
| 506 | +{ |
| 507 | + int ret = 0; |
| 508 | + int reg_value, pid1 = 0, pid2 = 0; |
| 509 | + u32 pbus_value = 0, retry; |
| 510 | + struct device *dev = phydev_dev(phydev); |
| 511 | + ret = air_pbus_reg_write(phydev, 0xcf928 , 0x0); |
| 512 | + AIR_RTN_ERR(ret); |
| 513 | + pid1 = phy_read(phydev, MII_PHYSID1); |
| 514 | + if (pid1 < 0) |
| 515 | + return pid1; |
| 516 | + pid2 = phy_read(phydev, MII_PHYSID2); |
| 517 | + if (pid2 < 0) |
| 518 | + return pid2; |
| 519 | + dev_info(dev, "PHY = %x - %x\n", pid1, pid2); |
| 520 | + if ((EN8811H_PHY_ID1 != pid1) || (EN8811H_PHY_ID2 != pid2)) |
| 521 | + { |
| 522 | + dev_err(dev, "EN8811H dose not exist !\n"); |
| 523 | + return -ENODEV; |
| 524 | + } |
| 525 | + ret = en8811h_load_firmware(phydev); |
| 526 | + if (ret) |
| 527 | + { |
| 528 | + dev_err(dev,"EN8811H load firmware fail.\n"); |
| 529 | + return ret; |
| 530 | + } |
| 531 | + retry = MAX_RETRY; |
| 532 | + do { |
| 533 | + mdelay(300); |
| 534 | + reg_value = air_mii_cl45_read(phydev, 0x1e, 0x8009); |
| 535 | + if (EN8811H_PHY_READY == reg_value) |
| 536 | + { |
| 537 | + dev_info(dev, "EN8811H PHY ready!\n"); |
| 538 | + break; |
| 539 | + } |
| 540 | + retry--; |
| 541 | + } while (retry); |
| 542 | + if (0 == retry) |
| 543 | + { |
| 544 | + dev_err(dev, "EN8811H initialize fail ! reg: 0x%x\n", reg_value); |
| 545 | + return -EIO; |
| 546 | + } |
| 547 | + /* Mode selection*/ |
| 548 | + dev_info(dev, "EN8811H Mode 1 !\n"); |
| 549 | + ret = air_mii_cl45_write(phydev, 0x1e, 0x800c, 0x0); |
| 550 | + AIR_RTN_ERR(ret); |
| 551 | + ret = air_mii_cl45_write(phydev, 0x1e, 0x800d, 0x0); |
| 552 | + AIR_RTN_ERR(ret); |
| 553 | + ret = air_mii_cl45_write(phydev, 0x1e, 0x800e, 0x1101); |
| 554 | + AIR_RTN_ERR(ret); |
| 555 | + ret = air_mii_cl45_write(phydev, 0x1e, 0x800f, 0x0002); |
| 556 | + AIR_RTN_ERR(ret); |
| 557 | + |
| 558 | + /* Serdes polarity */ |
| 559 | + pbus_value = air_buckpbus_reg_read(phydev, 0xca0f8); |
| 560 | + pbus_value = (pbus_value & 0xfffffffc) | EN8811H_RX_POLARITY_NORMAL | EN8811H_TX_POLARITY_NORMAL; |
| 561 | + ret = air_buckpbus_reg_write(phydev, 0xca0f8, pbus_value); |
| 562 | + AIR_RTN_ERR(ret); |
| 563 | + pbus_value = air_buckpbus_reg_read(phydev, 0xca0f8); |
| 564 | + dev_info(dev, "0xca0f8= 0x%x\n", pbus_value); |
| 565 | + pbus_value = air_buckpbus_reg_read(phydev, 0x3b3c); |
| 566 | + dev_info(dev, "Version(0x3b3c)= %x\n", pbus_value); |
| 567 | +#if defined(AIR_LED_SUPPORT) |
| 568 | + ret = en8811h_led_init(phydev); |
| 569 | + if (ret < 0) |
| 570 | + { |
| 571 | + dev_err(dev, "en8811h_led_init fail. (ret=%d)\n", ret); |
| 572 | + return ret; |
| 573 | + } |
| 574 | +#endif |
| 575 | + dev_info(dev, "EN8811H initialize OK ! (%s)\n", EN8811H_FW_VERSION); |
| 576 | + return 0; |
| 577 | +} |
| 578 | + |
| 579 | +static int en8811h_get_autonego(struct phy_device *phydev, int *an) |
| 580 | +{ |
| 581 | + int reg; |
| 582 | + reg = phy_read(phydev, MII_BMCR); |
| 583 | + if (reg < 0) |
| 584 | + return -EINVAL; |
| 585 | + if (reg & BMCR_ANENABLE) |
| 586 | + *an = AUTONEG_ENABLE; |
| 587 | + else |
| 588 | + *an = AUTONEG_DISABLE; |
| 589 | + return 0; |
| 590 | +} |
| 591 | + |
| 592 | +static int en8811h_read_status(struct phy_device *phydev) |
| 593 | +{ |
| 594 | + int ret = 0, lpagb = 0, lpa = 0, common_adv_gb = 0, common_adv = 0, advgb = 0, adv = 0, reg = 0, an = AUTONEG_DISABLE, bmcr = 0; |
| 595 | + int old_link = phydev->link; |
| 596 | + u32 pbus_value = 0; |
| 597 | + struct device *dev = phydev_dev(phydev); |
| 598 | + ret = genphy_update_link(phydev); |
| 599 | + if (ret) |
| 600 | + { |
| 601 | + dev_err(dev, "ret %d!\n", ret); |
| 602 | + return ret; |
| 603 | + } |
| 604 | + |
| 605 | + if (old_link && phydev->link) |
| 606 | + return 0; |
| 607 | + |
| 608 | + phydev->speed = SPEED_UNKNOWN; |
| 609 | + phydev->duplex = DUPLEX_UNKNOWN; |
| 610 | + phydev->pause = 0; |
| 611 | + phydev->asym_pause = 0; |
| 612 | + |
| 613 | + reg = phy_read(phydev, MII_BMSR); |
| 614 | + if (reg < 0) |
| 615 | + { |
| 616 | + dev_err(dev, "MII_BMSR reg %d!\n", reg); |
| 617 | + return reg; |
| 618 | + } |
| 619 | + reg = phy_read(phydev, MII_BMSR); |
| 620 | + if (reg < 0) |
| 621 | + { |
| 622 | + dev_err(dev, "MII_BMSR reg %d!\n", reg); |
| 623 | + return reg; |
| 624 | + } |
| 625 | + if(reg & BMSR_LSTATUS) |
| 626 | + { |
| 627 | + pbus_value = air_buckpbus_reg_read(phydev, 0x109D4); |
| 628 | + if (0x10 & pbus_value) { |
| 629 | + phydev->speed = SPEED_2500; |
| 630 | + phydev->duplex = DUPLEX_FULL; |
| 631 | + } |
| 632 | + else |
| 633 | + { |
| 634 | + ret = en8811h_get_autonego(phydev, &an); |
| 635 | + if ((AUTONEG_ENABLE == an) && (0 == ret)) |
| 636 | + { |
| 637 | + dev_dbg(dev, "AN mode!\n"); |
| 638 | + dev_dbg(dev, "SPEED 1000/100!\n"); |
| 639 | + lpagb = phy_read(phydev, MII_STAT1000); |
| 640 | + if (lpagb < 0 ) |
| 641 | + return lpagb; |
| 642 | + advgb = phy_read(phydev, MII_CTRL1000); |
| 643 | + if (adv < 0 ) |
| 644 | + return adv; |
| 645 | + common_adv_gb = (lpagb & (advgb << 2)); |
| 646 | + |
| 647 | + lpa = phy_read(phydev, MII_LPA); |
| 648 | + if (lpa < 0 ) |
| 649 | + return lpa; |
| 650 | + adv = phy_read(phydev, MII_ADVERTISE); |
| 651 | + if (adv < 0 ) |
| 652 | + return adv; |
| 653 | + common_adv = (lpa & adv); |
| 654 | + |
| 655 | + phydev->speed = SPEED_10; |
| 656 | + phydev->duplex = DUPLEX_HALF; |
| 657 | + if (common_adv_gb & (LPA_1000FULL | LPA_1000HALF)) |
| 658 | + { |
| 659 | + phydev->speed = SPEED_1000; |
| 660 | + if (common_adv_gb & LPA_1000FULL) |
| 661 | + |
| 662 | + phydev->duplex = DUPLEX_FULL; |
| 663 | + } |
| 664 | + else if (common_adv & (LPA_100FULL | LPA_100HALF)) |
| 665 | + { |
| 666 | + phydev->speed = SPEED_100; |
| 667 | + if (common_adv & LPA_100FULL) |
| 668 | + phydev->duplex = DUPLEX_FULL; |
| 669 | + } |
| 670 | + else |
| 671 | + { |
| 672 | + if (common_adv & LPA_10FULL) |
| 673 | + phydev->duplex = DUPLEX_FULL; |
| 674 | + } |
| 675 | + } |
| 676 | + else |
| 677 | + { |
| 678 | + dev_dbg(dev, "Force mode!\n"); |
| 679 | + bmcr = phy_read(phydev, MII_BMCR); |
| 680 | + |
| 681 | + if (bmcr < 0) |
| 682 | + return bmcr; |
| 683 | + |
| 684 | + if (bmcr & BMCR_FULLDPLX) |
| 685 | + phydev->duplex = DUPLEX_FULL; |
| 686 | + else |
| 687 | + phydev->duplex = DUPLEX_HALF; |
| 688 | + |
| 689 | + if (bmcr & BMCR_SPEED1000) |
| 690 | + phydev->speed = SPEED_1000; |
| 691 | + else if (bmcr & BMCR_SPEED100) |
| 692 | + phydev->speed = SPEED_100; |
| 693 | + else |
| 694 | + phydev->speed = SPEED_UNKNOWN; |
| 695 | + } |
| 696 | + } |
| 697 | + } |
| 698 | + |
| 699 | + return ret; |
| 700 | +} |
| 701 | +static struct phy_driver en8811h_driver[] = { |
| 702 | +{ |
| 703 | + .phy_id = EN8811H_PHY_ID, |
| 704 | + .name = "Airoha EN8811H", |
| 705 | + .phy_id_mask = 0x0ffffff0, |
| 706 | + .probe = en8811h_phy_probe, |
| 707 | + .read_status = en8811h_read_status, |
| 708 | +#if (LINUX_VERSION_CODE > KERNEL_VERSION(4, 5, 0)) |
| 709 | + .get_features = en8811h_get_features, |
| 710 | + .read_mmd = air_mii_cl45_read, |
| 711 | + .write_mmd = air_mii_cl45_write, |
| 712 | +#endif |
| 713 | +} }; |
| 714 | + |
| 715 | +int __init en8811h_phy_driver_register(void) |
| 716 | +{ |
| 717 | + int ret; |
| 718 | +#if (LINUX_VERSION_CODE < KERNEL_VERSION(4, 5, 0)) |
| 719 | + ret = phy_driver_register(en8811h_driver); |
| 720 | +#else |
| 721 | + ret = phy_driver_register(en8811h_driver, THIS_MODULE); |
| 722 | +#endif |
| 723 | + if (!ret) |
| 724 | + return 0; |
| 725 | + |
| 726 | + phy_driver_unregister(en8811h_driver); |
| 727 | + return ret; |
| 728 | +} |
| 729 | + |
| 730 | +void __exit en8811h_phy_driver_unregister(void) |
| 731 | +{ |
| 732 | + phy_driver_unregister(en8811h_driver); |
| 733 | +} |
| 734 | + |
| 735 | +module_init(en8811h_phy_driver_register); |
| 736 | +module_exit(en8811h_phy_driver_unregister); |
| 737 | diff --git a/drivers/net/phy/air_en8811h.h b/drivers/net/phy/air_en8811h.h |
| 738 | new file mode 100644 |
| 739 | index 0000000..1c91627 |
| 740 | --- /dev/null |
| 741 | +++ b/drivers/net/phy/air_en8811h.h |
| 742 | @@ -0,0 +1,151 @@ |
| 743 | +#ifndef __EN8811H_H |
| 744 | +#define __EN8811H_H |
| 745 | + |
| 746 | +#define EN8811H_MD32_DM "EthMD32.dm.bin" |
| 747 | +#define EN8811H_MD32_DSP "EthMD32.DSP.bin" |
| 748 | + |
| 749 | +#define EN8811H_PHY_ID1 0x03a2 |
| 750 | +#define EN8811H_PHY_ID2 0xa411 |
| 751 | +#define EN8811H_PHY_ID ((EN8811H_PHY_ID1 << 16) | EN8811H_PHY_ID2) |
| 752 | +#define EN8811H_PHY_READY 0x02 |
| 753 | +#define MAX_RETRY 5 |
| 754 | + |
| 755 | +#define EN8811H_TX_POLARITY_NORMAL 0x1 |
| 756 | +#define EN8811H_TX_POLARITY_REVERSE 0x0 |
| 757 | + |
| 758 | +#define EN8811H_RX_POLARITY_REVERSE (0x1 << 1) |
| 759 | +#define EN8811H_RX_POLARITY_NORMAL (0x0 << 1) |
| 760 | + |
| 761 | + |
| 762 | +/* |
| 763 | +The following led_cfg example is for reference only. |
| 764 | +LED0 Link 2500/Blink 2500 TxRx (GPIO5) <-> BASE_T_LED0, |
| 765 | +LED1 Link 1000/Blink 1000 TxRx (GPIO4) <-> BASE_T_LED1, |
| 766 | +LED2 Link 100 /Blink 100 TxRx (GPIO3) <-> BASE_T_LED2, |
| 767 | +*/ |
| 768 | +/* User-defined.B */ |
| 769 | +#define BASE_T_LED0_ON_CFG (LED_ON_EVT_LINK_2500M) |
| 770 | +#define BASE_T_LED0_BLK_CFG (LED_BLK_EVT_2500M_TX_ACT | LED_BLK_EVT_2500M_RX_ACT) |
| 771 | +#define BASE_T_LED1_ON_CFG (LED_ON_EVT_LINK_1000M) |
| 772 | +#define BASE_T_LED1_BLK_CFG (LED_BLK_EVT_1000M_TX_ACT | LED_BLK_EVT_1000M_RX_ACT) |
| 773 | +#define BASE_T_LED2_ON_CFG (LED_ON_EVT_LINK_100M) |
| 774 | +#define BASE_T_LED2_BLK_CFG (LED_BLK_EVT_100M_TX_ACT | LED_BLK_EVT_100M_RX_ACT) |
| 775 | +/* User-defined.E */ |
| 776 | + |
| 777 | +/* CL45 MDIO control */ |
| 778 | +#define MII_MMD_ACC_CTL_REG 0x0d |
| 779 | +#define MII_MMD_ADDR_DATA_REG 0x0e |
| 780 | +#define MMD_OP_MODE_DATA BIT(14) |
| 781 | + |
| 782 | +#define EN8811H_FW_VERSION "1.1.3" |
| 783 | + |
| 784 | +#define LED_ON_CTRL(i) (0x024 + ((i)*2)) |
| 785 | +#define LED_ON_EN (1 << 15) |
| 786 | +#define LED_ON_POL (1 << 14) |
| 787 | +#define LED_ON_EVT_MASK (0x1ff) |
| 788 | +/* LED ON Event Option.B */ |
| 789 | +#define LED_ON_EVT_LINK_2500M (1 << 8) |
| 790 | +#define LED_ON_EVT_FORCE (1 << 6) |
| 791 | +#define LED_ON_EVT_LINK_DOWN (1 << 3) |
| 792 | +#define LED_ON_EVT_LINK_100M (1 << 1) |
| 793 | +#define LED_ON_EVT_LINK_1000M (1 << 0) |
| 794 | +/* LED ON Event Option.E */ |
| 795 | + |
| 796 | +#define LED_BLK_CTRL(i) (0x025 + ((i)*2)) |
| 797 | +#define LED_BLK_EVT_MASK (0xfff) |
| 798 | +/* LED Blinking Event Option.B*/ |
| 799 | +#define LED_BLK_EVT_2500M_RX_ACT (1 << 11) |
| 800 | +#define LED_BLK_EVT_2500M_TX_ACT (1 << 10) |
| 801 | +#define LED_BLK_EVT_FORCE (1 << 9) |
| 802 | +#define LED_BLK_EVT_100M_RX_ACT (1 << 3) |
| 803 | +#define LED_BLK_EVT_100M_TX_ACT (1 << 2) |
| 804 | +#define LED_BLK_EVT_1000M_RX_ACT (1 << 1) |
| 805 | +#define LED_BLK_EVT_1000M_TX_ACT (1 << 0) |
| 806 | +/* LED Blinking Event Option.E*/ |
| 807 | +#define LED_ENABLE 1 |
| 808 | +#define LED_DISABLE 0 |
| 809 | + |
| 810 | +#define EN8811H_LED_COUNT 3 |
| 811 | + |
| 812 | +#define LED_BCR (0x021) |
| 813 | +#define LED_BCR_EXT_CTRL (1 << 15) |
| 814 | +#define LED_BCR_CLK_EN (1 << 3) |
| 815 | +#define LED_BCR_TIME_TEST (1 << 2) |
| 816 | +#define LED_BCR_MODE_MASK (3) |
| 817 | +#define LED_BCR_MODE_DISABLE (0) |
| 818 | + |
| 819 | +#define LED_ON_DUR (0x022) |
| 820 | +#define LED_ON_DUR_MASK (0xffff) |
| 821 | + |
| 822 | +#define LED_BLK_DUR (0x023) |
| 823 | +#define LED_BLK_DUR_MASK (0xffff) |
| 824 | + |
| 825 | +#define UNIT_LED_BLINK_DURATION 1024 |
| 826 | + |
| 827 | +#define AIR_RTN_ON_ERR(cond, err) \ |
| 828 | + do { if ((cond)) return (err); } while(0) |
| 829 | + |
| 830 | +#define AIR_RTN_ERR(err) AIR_RTN_ON_ERR(err < 0, err) |
| 831 | + |
| 832 | +#define LED_SET_EVT(reg, cod, result, bit) do \ |
| 833 | + { \ |
| 834 | + if(reg & cod) { \ |
| 835 | + result |= bit; \ |
| 836 | + } \ |
| 837 | + } while(0) |
| 838 | + |
| 839 | +#define LED_SET_GPIO_SEL(gpio, led, val) do \ |
| 840 | + { \ |
| 841 | + val |= (led << (8 * (gpio % 4))); \ |
| 842 | + } while(0) |
| 843 | + |
| 844 | +#define INVALID_DATA 0xffff |
| 845 | +#define PBUS_INVALID_DATA 0xffffffff |
| 846 | + |
| 847 | +typedef struct AIR_BASE_T_LED_CFG_S |
| 848 | +{ |
| 849 | + u16 en; |
| 850 | + u16 gpio; |
| 851 | + u16 pol; |
| 852 | + u16 on_cfg; |
| 853 | + u16 blk_cfg; |
| 854 | +}AIR_BASE_T_LED_CFG_T; |
| 855 | +typedef enum |
| 856 | +{ |
| 857 | + AIR_LED2_GPIO3 = 3, |
| 858 | + AIR_LED1_GPIO4, |
| 859 | + AIR_LED0_GPIO5, |
| 860 | + AIR_LED_LAST |
| 861 | +} AIR_LED_GPIO; |
| 862 | + |
| 863 | +typedef enum { |
| 864 | + AIR_BASE_T_LED0, |
| 865 | + AIR_BASE_T_LED1, |
| 866 | + AIR_BASE_T_LED2, |
| 867 | + AIR_BASE_T_LED3 |
| 868 | +}AIR_BASE_T_LED; |
| 869 | + |
| 870 | +typedef enum |
| 871 | +{ |
| 872 | + AIR_LED_BLK_DUR_32M, |
| 873 | + AIR_LED_BLK_DUR_64M, |
| 874 | + AIR_LED_BLK_DUR_128M, |
| 875 | + AIR_LED_BLK_DUR_256M, |
| 876 | + AIR_LED_BLK_DUR_512M, |
| 877 | + AIR_LED_BLK_DUR_1024M, |
| 878 | + AIR_LED_BLK_DUR_LAST |
| 879 | +} AIR_LED_BLK_DUT_T; |
| 880 | + |
| 881 | +typedef enum |
| 882 | +{ |
| 883 | + AIR_ACTIVE_LOW, |
| 884 | + AIR_ACTIVE_HIGH, |
| 885 | +} AIR_LED_POLARITY; |
| 886 | +typedef enum |
| 887 | +{ |
| 888 | + AIR_LED_MODE_DISABLE, |
| 889 | + AIR_LED_MODE_USER_DEFINE, |
| 890 | + AIR_LED_MODE_LAST |
| 891 | +} AIR_LED_MODE_T; |
| 892 | + |
| 893 | +#endif /* End of __EN8811H_MD32_H */ |