总线协议:GPIO模拟SMI(MDIO)协议(2):SMI协议软件实现

0 工具准备

TN1305 Technical note
IEEE802.3-2018
STM32F4xx中文参考手册

1 SMI协议软件实现-底层函数

基于HAL库以及stm32f407芯片使用GPIO模拟SMI接口时序实现SMI的底层函数。

1.1 软件SMI句柄设计

为了让软件SMI移植性更强,设计了SMI句柄,可以指定MDC和MDIO的端口。

typedef struct
{
    uint32_t MDC_PIN;
    uint32_t MDIO_PIN;
    GPIO_TypeDef  *MDC_PORT;
    GPIO_TypeDef  *MDIO_PORT;
} smi_portcfg_t;

1.2 SMI端口初始化

SMI端口初始化工作可以分为以下2步:
(1)根据端口使能对应GPIO时钟
(2)配置MDC为推挽输出、配置MDIO为上拉输入
(3)设置MDC为低电平
相关函数如下:

/**
 * @brief 使能SMI端口时钟
 * 
 * @param port 端口地址
 */
void smi_enable_clk(GPIO_TypeDef *port)
{
    switch ((uint32_t )port)
    {
        case GPIOA_BASE:
            __HAL_RCC_GPIOA_CLK_ENABLE();
            break;
        case GPIOB_BASE:
            __HAL_RCC_GPIOB_CLK_ENABLE();
            break;
        case GPIOC_BASE:
            __HAL_RCC_GPIOC_CLK_ENABLE();
            break;
        case GPIOD_BASE:
            __HAL_RCC_GPIOD_CLK_ENABLE();
            break;
        case GPIOE_BASE:
            __HAL_RCC_GPIOE_CLK_ENABLE();
            break;
        case GPIOF_BASE:
            __HAL_RCC_GPIOF_CLK_ENABLE();
            break;
        case GPIOG_BASE:
            __HAL_RCC_GPIOG_CLK_ENABLE();
            break;
        case GPIOH_BASE:
            __HAL_RCC_GPIOH_CLK_ENABLE();
            break;
        default:
            break;
    }
}
/**
 * @brief 初始化SMI的端口时钟、模式
 * 
 * @param smi_portcfg SMI配置句柄地址
 */
void smi_init_port(smi_portcfg_t *smi_portcfg)
{
    GPIO_InitTypeDef GPIO_Initure;
    g_smi_portcfg = *smi_portcfg;

    smi_enable_clk(g_smi_portcfg.MDC_PORT);
    smi_enable_clk(g_smi_portcfg.MDIO_PORT);

    /* 配置MDC为推挽输出 */
    GPIO_Initure.Pin = g_smi_portcfg.MDC_PIN;
    GPIO_Initure.Mode = GPIO_MODE_OUTPUT_PP;
    GPIO_Initure.Pull = GPIO_PULLUP;
    GPIO_Initure.Speed = GPIO_SPEED_HIGH;
    HAL_GPIO_Init(g_smi_portcfg.MDC_PORT, &GPIO_Initure);


    /* 配置MDIO为上拉输入 */
    GPIO_Initure.Pin = g_smi_portcfg.MDIO_PIN;
    GPIO_Initure.Mode = GPIO_MODE_INPUT;
    GPIO_Initure.Pull = GPIO_PULLUP;
    GPIO_Initure.Speed = GPIO_SPEED_HIGH;
    HAL_GPIO_Init(g_smi_portcfg.MDIO_PORT, &GPIO_Initure);

    smi_set_mdc_low();
}

1.3 SMI的MDIO输入/输出切换

为了提高软件SMI的兼容性,我们没有将MDIO端口设置为开漏输出(避免MDIO没有硬件上拉MDIO端口电平异常问题),而是在主机需要通过MDIO发送数据时配置MDIO为推挽输出,在主机需要通过MDIO接收数据时配置为上拉输入。相关函数如下:

/**
 * @brief 设置SMI的MDIO为推挽输出模式
 * 
 */
void smi_set_mdio_ouput(void)
{
    GPIO_InitTypeDef GPIO_Initure;
    /* 配置MDIO为推挽输出 */
    GPIO_Initure.Pin = g_smi_portcfg.MDIO_PIN;
    GPIO_Initure.Mode = GPIO_MODE_OUTPUT_PP;
    GPIO_Initure.Pull = GPIO_PULLUP;
    GPIO_Initure.Speed = GPIO_SPEED_HIGH;
    smi_set_mdio_high();
    HAL_GPIO_Init(g_smi_portcfg.MDIO_PORT, &GPIO_Initure);
}

/**
 * @brief 设置SMI的MDIO为上拉输入模式
 * 
 */
void smi_set_mdio_input(void)
{
    GPIO_InitTypeDef GPIO_Initure;
    /* 配置MDIO为上拉输入 */
    GPIO_Initure.Pin = g_smi_portcfg.MDIO_PIN;
    GPIO_Initure.Mode = GPIO_MODE_INPUT;
    GPIO_Initure.Pull = GPIO_PULLUP;
    GPIO_Initure.Speed = GPIO_SPEED_HIGH;
    HAL_GPIO_Init(g_smi_portcfg.MDIO_PORT, &GPIO_Initure);
}

1.4 SMI端口电平设置及读取

SMI的MDC端口只有输出功能,只需要设计MDC端口输出高、低电平函数即可。MDIO端口除了需要设计输出高、低电平函数外,还需要读取MDIO状态,需要设计MDIO读取函数。相关函数如下:

/**
 * @brief 设置MDC输出低电平
 * 
 */
void smi_set_mdc_low(void)
{
    HAL_GPIO_WritePin(g_smi_portcfg.MDC_PORT, g_smi_portcfg.MDC_PIN, GPIO_PIN_RESET);
}

/**
 * @brief 设置MDC输出高电平
 * 
 */
void smi_set_mdc_high(void)
{
    HAL_GPIO_WritePin(g_smi_portcfg.MDC_PORT, g_smi_portcfg.MDC_PIN, GPIO_PIN_SET);
}

/**
 * @brief 设置MDIO输出低电平
 * 
 */
void smi_set_mdio_low(void)
{
    HAL_GPIO_WritePin(g_smi_portcfg.MDIO_PORT, g_smi_portcfg.MDIO_PIN, GPIO_PIN_RESET);
}

/**
 * @brief 设置MDIO输出高电平
 * 
 */
void smi_set_mdio_high(void)
{
    HAL_GPIO_WritePin(g_smi_portcfg.MDIO_PORT, g_smi_portcfg.MDIO_PIN, GPIO_PIN_SET);
}

/**
 * @brief 获取MDIO输入电平
 * 
 * @return uint8_t 0-低电平 1-高电平
 */
uint8_t smi_read_mdio(void)
{
    return HAL_GPIO_ReadPin(g_smi_portcfg.MDIO_PORT, g_smi_portcfg.MDIO_PIN);
}

1.5 通过SMI写入n位数据

SMI协议规定了不同位长度的字段,为了灵活实现数据发送,设计了一个可以写入n位数据的函数:

/**
 * @brief 通过SMI写入n位数据
 * 
 * @param data 数据地址
 * @param count 数据长度(bit)
 */
void smi_write_n_bit(uint8_t *data, uint8_t count)
{
    int i;
    uint8_t writeData;
    for (i = 0; i < count; i++)
    {
        writeData = data[i / 8];
        smi_set_mdc_low();
        if ((writeData >> (7 - i % 8) & 0x01) == 0x01)
        {
            smi_set_mdio_high();
        }
        else
        {
            smi_set_mdio_low();
        }
        smi_delay;
        smi_set_mdc_high();
        smi_delay;
    }
    smi_set_mdc_low();
}

这里要注意:写入数据时,主机在下降沿切换数据,PHY设备在上升沿采样数据

1.6 通过SMI读取n位数据

SMI协议规定了不同位长度的字段,为了灵活实现数据读取,设计了一个可以读取n位数据的函数:

/**
 * @brief 通过SMI读取n位数据
 * 
 * @param data 数据地址
 * @param count 数据长度(bit)
 */
void smi_read_n_bit(uint8_t *data, uint8_t count)
{
    int i;
    for (i = 0; i < count; i++)
    {
        smi_set_mdc_low();
        smi_delay;
        if (smi_read_mdio() == 1)
        {
            data[i / 8] |= (1 << (7 - i % 8));
        }
        smi_set_mdc_high();
        smi_delay;
    }
    smi_set_mdc_low();
}

这里要注意:主机在读操作时(从TA字段开始),MDIO数据在上升沿被切换,在下降沿被主机采样(在发送完寄存器地址后,第1个下降沿就是TA的首个bit

2 SMI协议软件实现-协议层函数

SMI协议层的函数和底层函数解耦,这里设计了2个函数分别按照SMI协议读、写PHY设备的寄存器。

2.1 读取PHY设备寄存器

 /**
 * @brief 读取PHY设备的寄存器值
 * 
 * @param phyAddr PHY设备地址
 * @param regAddr 寄存器地址
 * @return uint16_t 寄存器值
 */
uint16_t smi_read_reg(uint8_t phyAddr, uint8_t regAddr)
{
    // 切换MDIO为推挽输出模式
    smi_set_mdio_ouput();

    // 前导码:32bit 1b
    uint32_t preambleData = SMI_FRAME_PREAMBLE_VALUE;
    smi_write_n_bit((uint8_t *)&preambleData, SMI_FRAME_PREAMBLE_BIT_LEN);

    // 帧起始:2bit 01b
    uint8_t stData = SMI_FRAME_ST_VALUE << 6;
    smi_write_n_bit(&stData, SMI_FRAME_ST_BIT_LEN);

    // 操作:2bit 10b(读取)
    uint8_t opData = SMI_FRAME_OP_READ_VALUE << 6;
    smi_write_n_bit(&opData, SMI_FRAME_OP_BIT_LEN);

    // PHY地址(5bit)+ 寄存器地址(5bit)
    uint8_t praddr[2] = {0};
    praddr[0] = (phyAddr << 5);
    praddr[0] |= ((regAddr & 0x1C) >> 2);
    praddr[1] = ((regAddr & 0x3) << 6);
    
    smi_write_n_bit(praddr, SMI_FRAME_PRADDR_BIT_LEN);

    // 切换MDIO为上拉输入模式
    smi_set_mdio_input();

    // 获取MDIO的TA状态,为0直接读取16bit数据
    uint8_t taData = 0;
    smi_read_n_bit(&taData, SMI_FRAME_READ_TA_BIT_LEN);

    if ((taData & 0x40) != 0x00)
    {
        printf("%s", SMI_DEBUG_PRINTF == 1 ? "Error : TA bit2 value is 1\r\n" : "");
    }
    // 读取数据,16bit
    uint8_t regValue[2] = {0};
    smi_read_n_bit(&regValue[0], 16);

    // 切换MDIO为上拉输入模式,驱动MDIO为高阻态
    smi_set_mdio_input();

    return (regValue[0] << 8) | (regValue[1]);
}

该函数按照SMI读寄存器协议,完成对PHY设备寄存器的读取。

2.2 设置PHY设备寄存器

/**
 * @brief 设置PHY设备的寄存器值
 * 
 * @param phyAddr PHY设备地址
 * @param regAddr 寄存器地址
 * @param regVal 寄存器值
 */
void smi_set_reg(uint8_t phyAddr, uint8_t regAddr, uint16_t regVal)
{
    // 切换MDIO为推挽输出模式
    smi_set_mdio_ouput();

    // 前导码:32bit 1b
    uint32_t preambleData = SMI_FRAME_PREAMBLE_VALUE;
    smi_write_n_bit((uint8_t *)&preambleData, SMI_FRAME_PREAMBLE_BIT_LEN);

    // 帧起始:2bit 01b
    uint8_t stData = SMI_FRAME_ST_VALUE << 6;
    smi_write_n_bit(&stData, SMI_FRAME_ST_BIT_LEN);

    // 操作:2bit 10b(读取)
    uint8_t opData = SMI_FRAME_OP_WRITE_VALUE << 6;
    smi_write_n_bit(&opData, SMI_FRAME_OP_BIT_LEN);

    // PHY地址(5bit)+ 寄存器地址(5bit)
    uint8_t praddr[2] = {0};
    praddr[0] = (phyAddr << 5);
    praddr[0] |= ((regAddr & 0x1C) >> 2);
    praddr[1] = ((regAddr & 0x3) << 6);
    smi_write_n_bit(praddr, SMI_FRAME_PRADDR_BIT_LEN);

    // 设置MDIO的TA状态,2bit,10b
    uint8_t taData = SMI_FRAME_WRITE_TA;
    smi_write_n_bit(&taData, SMI_FRAME_WRITE_TA_BIT_LEN);

    // 设置,16bit
    uint8_t regValue[2] = {0};
    regValue[0] = regVal >> 8;
    regValue[1] = regVal & 0xFF;
    smi_write_n_bit(&regValue[0], 16);

    // 切换MDIO为上拉输入模式,驱动MDIO为高阻态
    smi_set_mdio_input();
}

该函数按照SMI写寄存器协议,完成对PHY设备寄存器的设置。

3 工程文件下载地址

工程文件已经上传至CSDN,有需要的可以直接下载。
https://download.csdn.net/download/kevin1499/88768490

你可能感兴趣的:(总线协议,物联网,以太网,SMI,MDIO,总线协议)