在处理STM32以太网配置时,我们常常感到困惑,但事实上,大多数内容都有明确的规则。这其中,包括自动协商结果下的位配置、时钟的开启需求、缓冲区地址的设定以及各种值的正确调整等,这些都是影响网络通信效果的关键因素。

DM和FES位的配置依据

以太网配置中,DM和FES位的设置至关重要。发送线和接收线在网线中是分开的,发送线由白橙(正线)和橙(负线)双绞线组成,接收线则由白绿(正线)和绿(负线)双绞线构成。若集线器上仅连接两根网线,碰撞风险极低,此时双工模式可灵活配置,既可以是半双工,也可以是全双工。半双工模式下,不允许同时进行发送和接收。然而,若集线器上连接三台或更多电脑,碰撞风险增加,此时只能选择半双工模式。所有这些配置都需要根据实际网络连接情况,对DM和FES位进行合理调整。

若不依照这些规定进行设置,通信过程中很可能会遇到麻烦,例如数据传输可能会受到阻碍。

RCC中ETH时钟的打开

在RCC中配置ETH时钟时,即便只是单纯发送数据,也必须同时启用ETH的三个RCC时钟,其中MACRX时钟是不可或缺的。这相当于一个既定的步骤,必须严格执行。这与以太网外设的DMA功能有关,这里的DMA是独立专用的,与DMA1、DMA2等其他外设的DMA无关,这一点需要特别注意。许多人可能会忽视启动MACRX时钟的必要性,这可能会在后续操作中引发问题,比如如果没有启用MACRX时钟,将无法触发发送完成的中断。

RCC->AHBENR |= RCC_AHBENR_ETHMACEN | RCC_AHBENR_ETHMACTXEN | RCC_AHBENR_ETHMACRXEN;

配置时务必格外小心,务必确保不遗漏任何一项关键环节。

发送描述符缓冲区地址

描述符所指向的缓冲区地址TDES2和TDES3,必须位于SRAM内,并且无需对齐。不论TBS1或TBS2的大小,其取值均可为0至8191之间的任何整数。在lwip协议栈中,若q指向Flash区域,数据就必须复制至SRAM。否则,帧发送将注定失败。

开发人员若对这方面不甚了解,常常会犯错,随后便陷入对问题所在感到困惑的境地。

uint8_t tbuf[768];
uint16_t tbuf_used = 0;
desc_tx_ptr[1] = q->len;
desc_tx_ptr[2] = (uint32_t)q->payload;
if ((desc_tx_ptr[2] & 0xffff0000) != 0x20000000)
{
  // must be in the 64KB SRAM
  printf("Data 0xx isn't in SRAM!\n", desc_tx_ptr[2]);
  desc_tx_ptr[2] = (uint32_t)tbuf + tbuf_used;
  memcpy((void *)desc_tx_ptr[2], q->payload, q->len);
  tbuf_used += q->len;
}

FS和LS值的注意事项

正确配置FS和LS的值至关重要。尤其是发送描述符列表被循环使用时,切记要清除FS和LS位。正如示例程序所展示,切勿移除那两个else分支的配置。这些细节就如同设备中的小零件,若缺失任何一个,设备的正常运行可能就无法得到保障。

对此,许多人可能并不重视,认为并不关键。然而实际上,这却可能给程序的稳定运行带来隐患。

if (q == p)
  desc_tx_ptr[0] |= ETH_TDES0_FS;
else
  desc_tx_ptr[0] &= ~ETH_TDES0_FS; // 注意: 描述符是循环使用的, 因此不要忘记清除FS和LS位
if (q->next == NULL)
  desc_tx_ptr[0] |= ETH_TDES0_LS;
else
  desc_tx_ptr[0] &= ~ETH_TDES0_LS;

配置错误的结果

在处理以太网配置时若出现失误,问题会显现得十分明显。比如,若未开启MACRX时钟,发送完毕中断便无法触发。又或者,若STM32中的以太网速度(FES位)和半/全双工模式(DM位)未正确配置,发送完毕中断可以触发,但接收完毕中断则不行。这些错误现象对于我们排查故障至关重要,它们能帮助我们判断配置问题的大致位置。

配置过程中,务必时刻记住这些可能遇到的问题,如此一来,便能最大程度地减少走弯路的可能性。

#include 
#include 
#include "ETH.h"
// 要发送的数据包内容(数据链路层)
uint8_t packet[] = {
  0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x42, 0x52, 0x4d, 0x4e, 0x45, 0x54, 0x08, 0x06, 0x00, 0x01,
  0x08, 0x00, 0x06, 0x04, 0x00, 0x01, 0x42, 0x52, 0x4d, 0x4e, 0x45, 0x54, 0xc0, 0xa8, 0x1e, 0x0a, 
  0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xc0, 0xa8, 0x1e, 0x0a
}; // 192.168.30.10
// 发送、接收描述符
uint32_t desc_tx[10][4];
uint32_t desc_rx[10][4];
// 接收缓冲区
uint8_t rx_buffer[10][2][256];
// 以16进制格式显示数据包内容
void dump_data(const void *data, uint16_t len)
{
  const uint8_t *p = data;
  while (len--)
    printf("X", *p++);
  printf("\n");
}
// 用于printf, 项目属性必须勾选Use MicroLIB后才能用
int fputc(int ch, FILE *fp)
{
  if (fp == stdout)
  {
    if (ch == '\n')
    {
      while ((USART3->SR & USART_SR_TXE) == 0);
      USART3->DR = '\r';
    }
    while ((USART3->SR & USART_SR_TXE) == 0);
    USART3->DR = ch;
  }
  return ch;
}
/* 读写PHY上的寄存器, 具体请参阅DP83848手册中的寄存器表, 以及STM32参考手册中的29.4.1 Station management interface: SMI */
/* PHY模块的地址为0x01 */
uint16_t read_reg(uint8_t addr)
{
  ETH->MACMIIAR = (1 << ETH_MACMIIAR_PA_Pos) | (addr << ETH_MACMIIAR_MR_Pos) | ETH_MACMIIAR_CR_Div42 | ETH_MACMIIAR_MB;
  while (ETH->MACMIIAR & ETH_MACMIIAR_MB);
  return ETH->MACMIIDR;
}
void write_reg(uint8_t addr, uint16_t value)
{
  ETH->MACMIIDR = value;
  ETH->MACMIIAR = (1 << ETH_MACMIIAR_PA_Pos) | (addr << ETH_MACMIIAR_MR_Pos) | ETH_MACMIIAR_CR_Div42 | ETH_MACMIIAR_MW | ETH_MACMIIAR_MB;
  while (ETH->MACMIIAR & ETH_MACMIIAR_MB);
}
/* 查看PHY各寄存器的值 (串口发送b) */
void read_regs(void)
{
  uint8_t i;
  for (i = 0x00; i <= 0x1d; i++)
    printf("Register 0xx: 0xx\n", i, read_reg(i));
}
/* 查看desc_rx的内容 (串口发送c) */
void disp_rx(void)
{
  uint8_t i;
  for (i = 0; i < sizeof(desc_rx) / sizeof(desc_rx[0]); i++)
    printf("Buffer %d: 0xx 0xx 0xx 0xx\n", i, desc_rx[i][0], desc_rx[i][1], desc_rx[i][2], desc_rx[i][3]);
}
// STM32官方的ETH库en.stsw-stm32045(官网可下载)中的ETH_Init函数可自动完成这一步骤
void auto_negotiation(void)
{
  uint16_t value;
  while ((read_reg(DP83848_BMSR) & DP83848_BMSR_LS) == 0); // 等待网线插好
  
  value = read_reg(DP83848_BMCR);
  if ((value & DP83848_BMCR_ANE) == 0) // 若DP83848外部接线没有接成自动协商模式
    write_reg(DP83848_BMCR, value | DP83848_BMCR_ANE); // 则手动执行自动协商
  while ((read_reg(DP83848_BMSR) & DP83848_BMSR_ANC) == 0); // 等待自动协商完毕
  
  // 根据自动协商结果配置MACCR寄存器
  value = read_reg(DP83848_PHYSTS);
  if (value & DP83848_PHYSTS_DS)
    ETH->MACCR |= ETH_MACCR_DM;
  if ((value & DP83848_PHYSTS_SS) == 0)
    ETH->MACCR |= ETH_MACCR_FES;
}
int main(void)
{
  uint8_t i;
  
  RCC->APB1ENR = RCC_APB1ENR_USART3EN | RCC_APB1ENR_UART5EN;
  RCC->APB2ENR = RCC_APB2ENR_AFIOEN | RCC_APB2ENR_IOPAEN | RCC_APB2ENR_IOPBEN | RCC_APB2ENR_IOPCEN | RCC_APB2ENR_IOPDEN;
  
  /* 请参阅 29.3 Ethernet pins */
  GPIOA->CRL = 0x44444f44; // PA2: MDIO复用开漏输出(因为已经接了外部上拉电阻)
  GPIOA->CRH = 0x8884444b; // PA8: MCO复用推挽输出
  GPIOB->CRH = 0x44bbbb44; // PB10: 串口3的发送引脚, PB11~13: TX_EN, TXD0~1, 全部设为复用推挽输出
  GPIOC->CRL = 0x444444b4; // PC1: MDC复用推挽输出
  // 其余I/O均为默认值
  // 串口5的接收引脚PD2为浮空输入(默认值, 无需配置)
  
  // 给DP83848提供50MHz时钟后, 网卡接口上的灯才会亮, PA8要和PA1接到一起
  RCC->CFGR2 |= RCC_CFGR2_PLL3MUL_3; // PLL3CLK: 50MHz
  RCC->CR |= RCC_CR_PLL3ON;
  while ((RCC->CR & RCC_CR_PLL3RDY) == 0);
  RCC->CFGR |= RCC_CFGR_MCO_3 | RCC_CFGR_MCO_1 | RCC_CFGR_MCO_0; // MCO(PA8)=PLL3CLK
  
  AFIO->MAPR = AFIO_MAPR_MII_RMII_SEL | AFIO_MAPR_ETH_REMAP; // 使用RMII接口, 打开ETH Remap
  RCC->AHBENR |= RCC_AHBENR_ETHMACEN | RCC_AHBENR_ETHMACTXEN | RCC_AHBENR_ETHMACRXEN; // ETH的三个时钟必须全部打开, 否则ETH_DMA将无法工作
  
  // 用USART3发送字符(PB10), UART5接收字符(PD2)
  USART3->BRR = 312;
  USART3->CR1 = USART_CR1_UE | USART_CR1_TE;
  UART5->BRR = 312;
  UART5->CR1 = USART_CR1_UE | USART_CR1_RE | USART_CR1_RXNEIE; // 开串口接收中断
  NVIC_EnableIRQ(UART5_IRQn);
  printf("STM32F107VC Ethernet\n");
  
  // PHY自动协商(非常重要!!!!)
  // 根据自动协商结果配置ETH_MACCR的DM(duplex mode)和FES(fast ethernet speed)位
  auto_negotiation();
  
  ETH->MACCR |= ETH_MACCR_TE | ETH_MACCR_RE; // 允许发送/接收
  ETH->DMAIER = ETH_DMAIER_NISE | ETH_DMAIER_AISE | ETH_DMAIER_TIE | ETH_DMAIER_RIE | ETH_DMAIER_RBUIE; // 打开发送/接收完毕中断, 以及接收缓冲满的中断
  NVIC_EnableIRQ(ETH_IRQn);
  
  desc_tx[0][0] = ETH_TDES0_OWN | ETH_TDES0_IC | ETH_TDES0_TER | ETH_TDES0_LS | ETH_TDES0_FS; // IC必须为1, 否则无法触发发送完成中断
  desc_tx[0][1] = sizeof(packet); // 大小(任意)
  desc_tx[0][2] = (uint32_t)packet; // 要发送的数据包 (必须在SRAM中的任意地址, 不能在Flash中!!!)
  if ((desc_tx[0][2] & 0xffff0000) != 0x20000000)
    printf("Error: Data must be in SRAM!\n");
  ETH->DMATDLAR = (uint32_t)desc_tx; // 设置发送描述符的首地址
  
  // 初始化接收描述符
  for (i = 0; i < sizeof(desc_rx) / sizeof(desc_rx[0]); i++)
  {
    desc_rx[i][0] = ETH_RDES0_OWN;
    desc_rx[i][1] = (sizeof(rx_buffer[0][0]) << ETH_RDES1_RBS2_Pos) | (sizeof(rx_buffer[0][0]) << ETH_RDES1_RBS1_Pos);
    desc_rx[i][2] = (uint32_t)rx_buffer[i][0];
    desc_rx[i][3] = (uint32_t)rx_buffer[i][1];
  }
  desc_rx[i - 1][1] |= ETH_RDES1_RER;
  ETH->DMARDLAR = (uint32_t)desc_rx;
  
  printf("ETH->DMATDLAR=0xx, ETH->DMARDLAR=0xx\n", ETH->DMATDLAR, ETH->DMARDLAR); // 描述符的首地址必须是32位对齐!
  
  ETH->DMAOMR |= ETH_DMAOMR_TSF | ETH_DMAOMR_RSF | ETH_DMAOMR_ST | ETH_DMAOMR_SR; // 开始发送和接收
  while (1)
    __WFI();
}
void ETH_IRQHandler(void)
{
  printf("ETH->DMASR=0xx\n", ETH->DMASR);
  if (ETH->DMASR & ETH_DMASR_TS) // 发送完毕
  {
    ETH->DMASR = ETH_DMASR_NIS | ETH_DMASR_TS; // 写1清除标志位
    printf("Transmitted! desc_tx[0]: 0xx, 0xx, 0xx, 0xx\n", desc_tx[0][0], desc_tx[0][1], desc_tx[0][2], desc_tx[0][3]);
  }
  else if (ETH->DMASR & ETH_DMASR_RS) // 接收完毕
  {
    ETH->DMASR = ETH_DMASR_NIS | ETH_DMASR_RS;
    printf("Received!\n");
  }
  else if (ETH->DMASR & ETH_DMASR_RBUS) // 接收缓冲满
  {
    ETH->DMASR = ETH_DMASR_AIS | ETH_DMASR_RBUS;
    printf("Receive buffer unavailable!\n");
  }
}
void UART5_IRQHandler(void)
{
  uint8_t data = UART5->DR;
  if (data == 'a')
    printf("ETH->DMASR=0xx\n", ETH->DMASR);
  else if (data == 'b')
    read_regs();
  else if (data == 'c')
    disp_rx();
}

配置以太网的实际操作

在实际操作中,必须全面考虑上述各个要点。例如,在硬件连接时,必须严格遵守网线收发线的连接规范。至于软件配置,需开启必要的时钟,并对各个位进行相应的设置。在众多工程现场,一旦遭遇以太网通信难题,就必须逐一排查这些问题。不论是新手工程师还是经验丰富的技术人员,都不可对这些要点掉以轻心。

谈及此事,你是否在配置STM32以太网时遇到了棘手的问题?期待大家能分享各自的遭遇。若觉得这篇文章有所助益,不妨点个赞,将它分享出去。

// 补充stm32f10x.h中缺少的一些寄存器位的定义
#define _BV(n) (1u << (n))
#define ETH_MACMIIAR_PA_Pos 11
#define ETH_MACMIIAR_MR_Pos 6
#define ETH_TDES0_OWN _BV(31)
#define ETH_TDES0_IC _BV(30)
#define ETH_TDES0_LS _BV(29)
#define ETH_TDES0_FS _BV(28)
#define ETH_TDES0_TER _BV(21)
#define ETH_TDES0_TCH _BV(20)
#define ETH_RDES0_OWN _BV(31)
#define ETH_RDES1_DIC _BV(31)
#define ETH_RDES1_RBS2_Pos 16
#define ETH_RDES1_RER _BV(15)
#define ETH_RDES1_RCH _BV(14)
#define ETH_RDES1_RBS1_Pos 0
// DP83848中的一些寄存器位
#define DP83848_BMCR 0x00 // Basic Mode Control Register
#define DP83848_BMCR_ANE _BV(12) // Auto-Negotiation Enable
#define DP83848_BMSR 0x01 // Basic Mode Status Register
#define DP83848_BMSR_ANC _BV(5) // Auto-Negotiation Complete
#define DP83848_BMSR_LS _BV(2) // Link Status
#define DP83848_PHYSTS 0x10 // PHY Status Register
#define DP83848_PHYSTS_DS _BV(2) // Duplex
#define DP83848_PHYSTS_SS _BV(1) // Speed10