Linux Embedded

Le blog des technologies libres et embarquées

Réalisez un adaptateur USB / Ethernet avec un microcontrôleur et des logiciels open-source

J’utilise souvent un adaptateur USB <-> Ethernet pour ajouter à ma machine de développement un port réseau supplémentaire et pouvoir ainsi facilement me connecter avec une IP statique sur une carte de développement.


 

 

Ce type d'équipement électronique est très probablement basé sur un ASIC spécialisé, qui réalise entièrement la fonction de façon “matérielle”, mais je vous propose aujourd’hui de construire la même fonction avec un microcontrôleur et des briques logicielles open-source.

Pour vous aider durant la lecture de cet article, le code source de ce projet est disponible sur le dépôt Github suivant:

https://github.com/remdzi/stm32-eth-usb

Le bus USB et la classe CDC-ECM

Un bus USB est constitué d’un "host" (l'hôte, votre PC par exemple) et de un ou plusieurs "devices" (périphériques, vos souris, clavier, etc). Chaque "device" implémente un ou plusieurs "endpoints" (EP) qui sont ses canaux de communication avec le "host". Les EP peuvent être de type "OUT" si l’échange des données se fait du "host" vers le "device", "IN" du "device" vers le "host" et "INOUT" si la communication est bi-directionnelle.

Lorsque vous connectez un "device" sur le bus, le "host" commence par la phase d’énumération qui consiste à lire le descripteur USB du "device" sur son EP0 qui est toujours de type "INOUT". Ce descripteur contient la fiche d’identité du "device" avec notamment son PID (ou "Product ID") et son VID (ou "Vendor ID") mais aussi le type de classe USB implémentée par le "device". Si le "device" annonce une classe USB standardisée alors le "host" connaît le rôle et le format des données supportées par chacun des autres EPs du "device" et peut donc charger un driver standard supportant cette classe. Si au contraire le "device" annonce une classe non standard alors c’est le couple PID/VID qui permet au "host" de reconnaître le "device" et de charger (s'il existe) le bon driver qui implémente la communication requise pour joindre les EPs du "device".

Il existe une multitude de classes USB standardisées, chacune étant destinée à un type de périphérique: "Human Interface Devices" (HID) pour les claviers et souris, USB "Video Class" pour les webcam, USB "Audio Class" pour les micros et casques, etc.

Pour un adaptateur USB <-> Ethernet, il existe la classe "Remote Network Driver Interface Specification" (RNDIS) mais elle est “propriétaire Microsoft”. Nous préférons donc utiliser la classe "Communication Device Class" / "Ethernet Control Module" (CDC-ECM), qui fait partie intégrante du standard USB. Notez qu’un "host" sous linux supporte aussi bien les devices "RNDIS" (rndis_host.c) que "CDC-ECM" (cdc_ether.c).

La classe "CDC-ECM", dont la spécification complète est disponible ici, utilise 3 EPs (en plus du EP0 "INOUT" obligatoire et utilisé pour l’énumération du "device"):

  • Un EP "OUT" pour l’émission des trames Ethernet depuis le "host". Le "device" recopie sur le bus Ethernet les trames qu’il reçoit sur cet EP.
  • Un EP "IN" pour la réception des trames Ethernet vers le "host". Le "device" recopie sur cet EP les trames qu’il reçoit du bus Ethernet.
  • Un autre EP "IN" sur lequel le "device" envoie vers le "host" diverses informations sur l’état du lien Ethernet. Par exemple, si un câble est connecté ou non.

La carte STM32 Nucleo F207

Pour réaliser notre adaptateur nous allons utiliser une carte STM32 NUCLEO-F207ZG qui est basée sur un microcontrôleur STM32F207 (cortex-m3) et dispose d’un port USB "OTG" et d’un port Ethernet 10/100. Comme toutes les cartes Nucleo, elle intègre également un ST-LINK qui permet de programmer et de débugger la carte.

USB "OTG" signifie "On-The-Go" qui est un port USB pouvant se reconfigurer pour jouer le rôle de "host" ou de "device". Dans notre cas nous l'utiliserons uniquement comme un "device".

Les différentes versions de l'USB définissent plusieurs débits possibles:

  • USB 1.0 :
    • Low speed (LS) = 1.5 Mbit/s (187.5 KB/s)
    • Full speed (FS) = 12 Mbit/s (1.5 MB/s)
  • USB 2.0 :
    • High speed (HS) = 480 Mbit/s (60 MB/s)
  • USB 3.0 :
    • SuperSpeed (SS) = 5 Gbit/s (625 MB/s)

Le STM32F207 intègre un contrôleur USB "FS" et un autre pour le "HS" mais malheureusement sur les cartes Nucleo, c'est le contrôleur "FS" qui est connecté au port USB. De plus, pour fonctionner en "HS" cela nécessiterait un PHY externe (le STM32 intègre un PHY "FS" et une interface pour un PHY "HS" externe) qui n'est pas présent sur la Nucleo. Nous devrons donc nous contenter d'un débit théorique max de 12Mbit/s.

CMSIS, HAL et middleware USB

Côté logiciel, nous allons utiliser les librairies open-source fournies par ST pour sa famille de microcontrôleur STM32:

  • La couche CMSIS ("Common Microcontroller Software Interface Standard)" qui contient notamment le code de démarrage,
  • La couche HAL ("Hardware Abstraction Layer") qui contient les drivers,
  • La couche "middleware" qui comporte entre autres l’implémentation des principales classes USB "host" et "device"

Le tout est rassemblé dans le dépôt STM32CubeF2 qui contient également des exemples d’utilisation qui nous seront bien utiles.

Mise en place de la chaîne de développement

Pour réaliser notre projet nous allons utiliser les outils classiques pour un développement sur microcontrôleur en "baremetal":

  • gcc dans sa version arm-none-eabi pour compiler et linker notre logiciel,
  • openocd pour flasher la carte et comme interface de debug,
  • gdb dans sa version multiarch comme frontend de debug,
  • CMake pour automatiser la construction du logiciel

On commence donc par installer ces outils sur notre machine de développement:

remdzi@remdzi-Latitude-5520:~$ sudo apt install cmake gcc-arm-none-eabi openocd gdb-multiarch

Compilation d’un projet minimal avec CMake, gcc et CMSIS

On crée un dossier pour notre projet:

remdzi@remdzi-Latitude-5520:~$ mkdir stm32-eth-usb
remdzi@remdzi-Latitude-5520:~$ cd stm32-eth-usb/

On y clone le package STM32CubeF2:

remdzi@remdzi-Latitude-5520:~/stm32-eth-usb$ git clone https://github.com/STMicroelectronics/STM32CubeF2.git
remdzi@remdzi-Latitude-5520:~/stm32-eth-usb$ cd STM32CubeF2/
remdzi@remdzi-Latitude-5520:~/stm32-eth-usb/STM32CubeF2$ git log
commit 197d5c760c4a1b0dea36797ebf782210125cb4d1 (HEAD -> master, origin/master, origin/HEAD)
Author: Ali Labbene <ali.labbene@st.com>
Date:   Mon Dec 19 17:28:16 2022 +0100

    Rename License.md  with capital letters

On initialise un fichier CMakeLists.txt avec le nommage du projet et l’activation du support des langages assembleur et C:

cmake_minimum_required(VERSION 3.15)

project(stm32-eth-usb ASM C)

On ajoute les directives pour la cross-compilation "baremetal" pour cibles "arm":

set(CMAKE_SYSTEM_NAME Generic)
set(CMAKE_SYSTEM_PROCESSOR ARM)
set(CMAKE_TRY_COMPILE_TARGET_TYPE STATIC_LIBRARY)

set(CMAKE_ASM_COMPILER arm-none-eabi-gcc)
set(CMAKE_C_COMPILER arm-none-eabi-gcc)

On définit la liste des fichiers source et le nom de notre exécutable:

set(SRC_FILES
   STM32CubeF2/Drivers/CMSIS/Device/ST/STM32F2xx/Source/Templates/gcc/startup_stm32f207xx.s
   STM32CubeF2/Drivers/CMSIS/Device/ST/STM32F2xx/Source/Templates/system_stm32f2xx.c
   
   main.c
   )

set(EXECUTABLE ${PROJECT_NAME}.elf)
add_executable(${EXECUTABLE} ${SRC_FILES})

Pour le moment, nous n’incluons que le strict minimum: le code démarrage du microcontrôleur, fourni par la couche CMSIS (startup_stm32f207xx.s et system_stm32f2xx.c) et notre main.c qui se contente d’exécuter une simple boucle infinie:

int main(void)
{
   while(1);
}

On n’oublie pas de définir le type de microcontrôleur utilisé. Ce #define est nécessaire à la compilation de la couche CMSIS:

target_compile_definitions(${EXECUTABLE} PRIVATE
        -DSTM32F207xx
        )

On fournit les chemins vers les fichiers d'en tête utiles à notre projet:

target_include_directories(${EXECUTABLE} PRIVATE
        STM32CubeF2/Drivers/CMSIS/Device/ST/STM32F2xx/Include
        STM32CubeF2/Drivers/CMSIS/Include
        )

Enfin, on précise les options de compilation et de link:

target_compile_options(${EXECUTABLE} PRIVATE  
        -mcpu=cortex-m3
        -mthumb
        -mfloat-abi=soft

        -fdata-sections
        -ffunction-sections

        -Wall

        -O3
        -g 
        )

target_link_options(${EXECUTABLE} PRIVATE
        -T${CMAKE_SOURCE_DIR}/STM32F207ZGTx_FLASH.ld

        -mcpu=cortex-m3
        -mthumb
        -mfloat-abi=soft

        -specs=nosys.specs
        -specs=nano.specs

        -Wl,-Map=${PROJECT_NAME}.map,--cref
        -Wl,--gc-sections
        )

Notre linker script STM32F207ZGTx_FLASH.ld sera initialisé à partir d’un des nombreux exemples fournis dans le package STM32CubeF2:

remdzi@remdzi-Latitude-5520:~/stm32-eth-usb$ cp STM32CubeF2/Projects/NUCLEO-F207ZG/Templates_LL/SW4STM32/NUCLEO-F207ZG/STM32F207ZGTx_FLASH.ld .

C’est optionnel, mais bien pratique: on peut aussi ajouter une commande permettant d’afficher l’occupation mémoire de notre exécutable:

add_custom_command(TARGET ${EXECUTABLE}
        POST_BUILD
        COMMAND arm-none-eabi-size ${EXECUTABLE})

Voilà notre projet minimal est prêt et on peut le compiler avec les commandes CMake classiques:

remdzi@remdzi-Latitude-5520:~/stm32-eth-usb$ mkdir build
remdzi@remdzi-Latitude-5520:~/stm32-eth-usb$ cd build/
remdzi@remdzi-Latitude-5520:~/stm32-eth-usb/build$ cmake ..
-- The ASM compiler identification is GNU
-- Found assembler: /usr/bin/cc
-- The C compiler identification is GNU 11.3.0
-- Detecting C compiler ABI info
-- Detecting C compiler ABI info - done
-- Check for working C compiler: /usr/bin/cc - skipped
-- Detecting C compile features
-- Detecting C compile features - done
-- Configuring done
-- Generating done
-- Build files have been written to: /home/remdzi/stm32-eth-usb/build
remdzi@remdzi-Latitude-5520:~/stm32-eth-usb/build$ make
Scanning dependencies of target my-nucleo-f207.elf
[ 25%] Building ASM object CMakeFiles/my-nucleo-f207.elf.dir/STM32CubeF2/Drivers/CMSIS/Device/ST/STM32F2xx/Source/Templates/gcc/startup_stm32f207xx.s.o
[ 50%] Building C object CMakeFiles/my-nucleo-f207.elf.dir/STM32CubeF2/Drivers/CMSIS/Device/ST/STM32F2xx/Source/Templates/system_stm32f2xx.c.o
[ 75%] Building C object CMakeFiles/my-nucleo-f207.elf.dir/main.c.o
[100%] Linking C executable my-nucleo-f207.elf
   text    data     bss     dec     hex filename
    720       8    1568    2296     8f8 my-nucleo-f207.elf
[100%] Built target my-nucleo-f207.elf

Flash et debug de la carte avec openocd et gdb

Pour flasher notre exécutable sur la carte, nous allons mettre oeuvre son interface ST-LINK avec openocd. Pour fonctionner, openocd a besoin de fichiers de configuration qui définissent l’interface de debug (ST-LINK) et la cible (stm32f2xx). En fouillant dans le répertoire d’installation d’openocd (/usr/share/openocd), on voit que les deux sont bel et bien supportés:

  • /usr/share/openocd/scripts/interface/stlink.cfg
  • /usr/share/openocd/scripts/target/stm32f2x.cfg

On voit même dans /usr/share/openocd/scripts/board, que de nombreuses cartes Nucleo sont directement supportées mais malheureusement pas la F2. Ce n’est pas grave, nous allons créer une config F2 en s’inspirant de la config F4. On crée donc un fichier st_nucleo_f2.cfg à la racine de notre projet et on le renseigne ainsi:

source [find interface/stlink.cfg]

transport select hla_swd

source [find target/stm32f2x.cfg]

reset_config srst_only

Après avoir connecté le port USB du ST-LINK de notre carte à notre machine de développement, nous pouvons démarrer la session de flash+debug avec openocd:

remdzi@remdzi-Latitude-5520:~/stm32-eth-usb/build$ openocd -f ../st_nucleo_f2.cfg -c init -c "reset halt" -c "flash write_image erase stm32-eth-usb.elf" -c "reset halt"
Open On-Chip Debugger 0.11.0
Licensed under GNU GPL v2
For bug reports, read
http://openocd.org/doc/doxygen/bugs.html
Info : The selected transport took over low-level target control. The results might differ compared to plain JTAG/SWD
srst_only separate srst_nogate srst_open_drain connect_deassert_srst

Info : clock speed 1000 kHz
Info : STLINK V2J40M27 (API v2) VID:PID 0483:374B
Info : Target voltage: 3.232941
Info : stm32f2x.cpu: hardware has 6 breakpoints, 4 watchpoints
Info : starting gdb server for stm32f2x.cpu on 3333
Info : Listening on port 3333 for gdb connections
target halted due to debug-request, current mode: Thread 
xPSR: 0x01000000 pc: 0x08000254 msp: 0x20020000
Info : device id = 0x201f6411
Info : flash size = 1024 kbytes
auto erase enabled
wrote 16384 bytes from file stm32-eth-usb.elf in 0.719839s (22.227 KiB/s)

target halted due to debug-request, current mode: Thread 
xPSR: 0x01000000 pc: 0x08000254 msp: 0x20020000
Info : Listening on port 6666 for tcl connections
Info : Listening on port 4444 for telnet connections

On démarre gdb…

remdzi@remdzi-Latitude-5520:~/stm32-eth-usb/build$ gdb-multiarch stm32-eth-usb.elf 
GNU gdb (Ubuntu 12.1-0ubuntu1~22.04) 12.1
Copyright (C) 2022 Free Software Foundation, Inc.
License GPLv3+: GNU GPL version 3 or later <http://gnu.org/licenses/gpl.html>
This is free software: you are free to change and redistribute it.
There is NO WARRANTY, to the extent permitted by law.
Type "show copying" and "show warranty" for details.
This GDB was configured as "x86_64-linux-gnu".
Type "show configuration" for configuration details.
For bug reporting instructions, please see:
<https://www.gnu.org/software/gdb/bugs/>.
Find the GDB manual and other documentation resources online at:
    <http://www.gnu.org/software/gdb/documentation/>.

For help, type "help".
Type "apropos word" to search for commands related to "word"...
Reading symbols from stm32-eth-usb.elf...
(gdb)

…et on se connecte au serveur de debug avec la commande “target remote localhost:3333”:

(gdb) target remote localhost:3333
Remote debugging using localhost:3333
Reset_Handler () at /home/remdzi/stm32-eth-usb/STM32CubeF2/Drivers/CMSIS/Device/ST/STM32F2xx/Source/Templates/gcc/startup_stm32f207xx.s:61
61   ldr   sp, =_estack     /* set stack pointer */
(gdb) 

On voit que la cible est bien arrêtée sur le vecteur de reset, qui pointe vers la première instruction du code de démarrage: celle qui est chargée d’initialiser le pointeur de pile.

On place un point d’arrêt avec le commande “b” (breakpoint) sur la fonction main():

(gdb) b main
Breakpoint 1 at 0x80002ac: file /home/remdzi/stm32-eth-usb/main.c, line 3.
Note: automatically using hardware breakpoints for read-only addresses.
(gdb) c
Continuing.
halted: PC: 0x080002ac

Breakpoint 1, main () at /home/remdzi/stm32-eth-usb/main.c:3
3    while(1);
(gdb)

On démarre l’exécution avec la commande “c” (comme continue):

(gdb) c
Continuing.
halted: PC: 0x080002ac

Breakpoint 1, main () at /home/remdzi/stm32-eth-usb/main.c:3
3    while(1);
(gdb)

On peut voir que l’exécution s’est bien arrêtée sur le breakpoint à l’entrée de la fonction main(). Si on relance l’exécution avec la commande c alors on retombe dans le breakpoint, etc, etc, on est bien dans notre boucle infinie.

Notre chaîne de compilation, flashage et débogage est donc bien fonctionnelle.

Printf() via une UART et la HAL

L’utilisation de gdb pour débugger un programme est très puissante mais il est aussi parfois utile de pouvoir envoyer des messages de debug depuis le code source avec un simple printf(). Nous allons utiliser une liaison série (UART) pour cela. Sur les cartes Nucleo l’UART3 du stm32 est reliée au ST-LINK, qui le redirige sur son port USB via une classe USB "CDC-ACM" ("Abstract Control Model" dont la spécification est disponible ici). C’est la raison pour laquelle, après avoir connecté le port USB du ST-LINK de notre carte Nucleo à notre machine de développement, nous pouvons voir apparaître un fichier /dev/ttyACM0.

remdzi@remdzi-Latitude-5520:~$ ls /dev/ttyACM*
/dev/ttyACM0

Pour interagir avec l’uart du stm32 nous allons intégrer les drivers fournis par ST avec la HAL.

On commence par ajouter à notre projet un fichier de configuration de la HAL qu’on initialise à partir du template fourni par ST:

remdzi@remdzi-Latitude-5520:~/stm32-eth-usb$ cp STM32CubeF2/Drivers/STM32F2xx_HAL_Driver/Inc/stm32f2xx_hal_conf_template.h stm32f2xx_hal_conf.h

Puis on y adapte la définition de la fréquence de l’oscillateur externe à celle de notre carte Nucleo qui est de 8MHz:

#if !defined  (HSE_VALUE) 
  #define HSE_VALUE                     8000000U       /*!< Value of the External oscillator in Hz */
#endif /* HSE_VALUE */

Dans notre main.c on peut alors initialiser la HAL et configurer la clock à 120MHz avec la fonction SystemClock_Config(), dont on trouve un exemple d’implémentation pour notre Nucleo dans STM32CubeF2/Projects/NUCLEO-F207ZG/Templates/Src/main.c:

int main(void)
{
   HAL_Init();
   
   /* Configure the system clock to 120 MHz */
   SystemClock_Config();
   
   while(1);
}

void SystemClock_Config(void)
{
   RCC_ClkInitTypeDef RCC_ClkInitStruct;
   RCC_OscInitTypeDef RCC_OscInitStruct;

   /* Enable HSE Oscillator and activate PLL with HSE as source */
   RCC_OscInitStruct.OscillatorType = RCC_OSCILLATORTYPE_HSE;
   RCC_OscInitStruct.HSEState = RCC_HSE_BYPASS;
   RCC_OscInitStruct.PLL.PLLState = RCC_PLL_ON;
   RCC_OscInitStruct.PLL.PLLSource = RCC_PLLSOURCE_HSE;
   RCC_OscInitStruct.PLL.PLLM = 8;
   RCC_OscInitStruct.PLL.PLLN = 240;
   RCC_OscInitStruct.PLL.PLLP = RCC_PLLP_DIV2;
   RCC_OscInitStruct.PLL.PLLQ = 5;
   HAL_RCC_OscConfig(&RCC_OscInitStruct);

   /* Select PLL as system clock source and configure the HCLK, PCLK1 and PCLK2 
      clocks dividers */
   RCC_ClkInitStruct.ClockType = (RCC_CLOCKTYPE_SYSCLK | RCC_CLOCKTYPE_HCLK | RCC_CLOCKTYPE_PCLK1 | RCC_CLOCKTYPE_PCLK2);
   RCC_ClkInitStruct.SYSCLKSource = RCC_SYSCLKSOURCE_PLLCLK;
   RCC_ClkInitStruct.AHBCLKDivider = RCC_SYSCLK_DIV1;
   RCC_ClkInitStruct.APB1CLKDivider = RCC_HCLK_DIV4;
   RCC_ClkInitStruct.APB2CLKDivider = RCC_HCLK_DIV2;
   HAL_RCC_ClockConfig(&RCC_ClkInitStruct, FLASH_LATENCY_3);
}

On implémente ensuite la routine d’initialisation de l’UART, que nous choisissons de configurer avec les paramètres classiques: 115200bps, 8bits, pas de contrôle de la parité:

void uart3_init(void)
{
   Uart3Handle.Instance        = USART3;
   Uart3Handle.Init.BaudRate   = 115200;
   Uart3Handle.Init.WordLength = UART_WORDLENGTH_8B;
   Uart3Handle.Init.StopBits   = UART_STOPBITS_1;
   Uart3Handle.Init.Parity     = UART_PARITY_NONE;
   Uart3Handle.Init.HwFlowCtl  = UART_HWCONTROL_NONE;
   Uart3Handle.Init.Mode       = UART_MODE_TX_RX;
   if (HAL_UART_Init(&Uart3Handle) != HAL_OK)
   {
      Error_Handler();
   }
}

Lorsque nous appelons la fonction HAL_UART_Init(), la HAL appelle elle-même la fonction HAL_UART_MspInit() que nous devons implémenter et qui se charge d'initialiser la clock et les GPIOs de notre UART:

void HAL_UART_MspInit(UART_HandleTypeDef* huart)
{
   GPIO_InitTypeDef GPIO_InitStruct = {0};
   if(huart->Instance==USART3)
   {
      /* Enable clocks */
      __HAL_RCC_USART3_CLK_ENABLE();
      __HAL_RCC_GPIOD_CLK_ENABLE();
  
      /* UART TX GPIO pin configuration  */
      GPIO_InitStruct.Pin       = GPIO_PIN_8;
      GPIO_InitStruct.Mode      = GPIO_MODE_AF_PP;
      GPIO_InitStruct.Pull      = GPIO_PULLUP;
      GPIO_InitStruct.Speed     = GPIO_SPEED_FREQ_VERY_HIGH;
      GPIO_InitStruct.Alternate = GPIO_AF7_USART3;
      HAL_GPIO_Init(GPIOD, &GPIO_InitStruct);

      /* UART RX GPIO pin configuration  */
      GPIO_InitStruct.Pin = GPIO_PIN_9;
      GPIO_InitStruct.Alternate = GPIO_AF7_USART3;
      HAL_GPIO_Init(GPIOD, &GPIO_InitStruct);
   }
}

Pour rediriger la sortie du printf() de la librairie standard (newlib) vers l’UART on redéfinit la fonction _write():

/* Printf() ouputs to uart3 */
int _write(int file, char *ptr, int len)
{
   HAL_UART_Transmit(&Uart3Handle, (uint8_t *)ptr, len, 0xFFFF);
   return len;
}

 Dans notre main.c il ne nous reste plus qu’à appeler l’initialisation de notre UART et à utiliser printf() pour le fameux “Hello World!”:

int main(void)
{
   HAL_Init();
   
   /* Configure the system clock to 120 MHz */
   SystemClock_Config();
   
   uart3_init();
   printf("Hello world!\r\n");
   
   while(1);
}

On ajoute au CMakeLists.txt les nouveaux sources et headers:

set(SRC_FILES
   …
   STM32CubeF2/Drivers/STM32F2xx_HAL_Driver/Src/stm32f2xx_hal.c
   STM32CubeF2/Drivers/STM32F2xx_HAL_Driver/Src/stm32f2xx_hal_cortex.c
   STM32CubeF2/Drivers/STM32F2xx_HAL_Driver/Src/stm32f2xx_hal_gpio.c
   STM32CubeF2/Drivers/STM32F2xx_HAL_Driver/Src/stm32f2xx_hal_rcc.c
   STM32CubeF2/Drivers/STM32F2xx_HAL_Driver/Src/stm32f2xx_hal_uart.c
   uart.c
   …
   )

target_include_directories(${EXECUTABLE} PRIVATE
   …
   STM32CubeF2/Drivers/STM32F2xx_HAL_Driver/Inc
   …
   )

Et on n’oublie pas de définir la constante symbolique USE_HAL_DRIVER nécessaire à la compilation de la HAL:

target_compile_definitions(${EXECUTABLE} PRIVATE
   …
   -DUSE_HAL_DRIVER
   …
   )

On peut alors recompiler notre logiciel et reflasher notre carte:

remdzi@remdzi-Latitude-5520:~/stm32-eth-usb/build$ cmake ..
-- Configuring done
-- Generating done
-- Build files have been written to: /home/remdzi/stm32-eth-usb/build
remdzi@remdzi-Latitude-5520:~/stm32-eth-usb/build$ make
Scanning dependencies of target stm32-eth-usb.elf
[ 10%] Building ASM object CMakeFiles/stm32-eth-usb.elf.dir/STM32CubeF2/Drivers/CMSIS/Device/ST/STM32F2xx/Source/Templates/gcc/startup_stm32f207xx.s.o
[ 20%] Building C object CMakeFiles/stm32-eth-usb.elf.dir/STM32CubeF2/Drivers/CMSIS/Device/ST/STM32F2xx/Source/Templates/system_stm32f2xx.c.o
[ 30%] Building C object CMakeFiles/stm32-eth-usb.elf.dir/STM32CubeF2/Drivers/STM32F2xx_HAL_Driver/Src/stm32f2xx_hal.c.o
[ 40%] Building C object CMakeFiles/stm32-eth-usb.elf.dir/STM32CubeF2/Drivers/STM32F2xx_HAL_Driver/Src/stm32f2xx_hal_cortex.c.o
[ 50%] Building C object CMakeFiles/stm32-eth-usb.elf.dir/STM32CubeF2/Drivers/STM32F2xx_HAL_Driver/Src/stm32f2xx_hal_gpio.c.o
[ 60%] Building C object CMakeFiles/stm32-eth-usb.elf.dir/STM32CubeF2/Drivers/STM32F2xx_HAL_Driver/Src/stm32f2xx_hal_rcc.c.o
[ 70%] Building C object CMakeFiles/stm32-eth-usb.elf.dir/STM32CubeF2/Drivers/STM32F2xx_HAL_Driver/Src/stm32f2xx_hal_uart.c.o
[ 80%] Building C object CMakeFiles/stm32-eth-usb.elf.dir/main.c.o
[ 90%] Building C object CMakeFiles/stm32-eth-usb.elf.dir/uart.c.o
[100%] Linking C executable stm32-eth-usb.elf
   text    data     bss     dec     hex filename
   8076     120    1656    9852    267c stm32-eth-usb.elf
[100%] Built target stm32-eth-usb.elf
remdzi@remdzi-Latitude-5520:~/stm32-eth-usb/build$ openocd -f ../st_nucleo_f2.cfg -c init -c "reset halt" -c "flash write_image erase stm32-eth-usb.elf" -c "reset run" -c exit
Open On-Chip Debugger 0.11.0
Licensed under GNU GPL v2
For bug reports, read
http://openocd.org/doc/doxygen/bugs.html
Info : The selected transport took over low-level target control. The results might differ compared to plain JTAG/SWD
srst_only separate srst_nogate srst_open_drain connect_deassert_srst

Info : clock speed 1000 kHz
Info : STLINK V2J40M27 (API v2) VID:PID 0483:374B
Info : Target voltage: 3.231373
Info : stm32f2x.cpu: hardware has 6 breakpoints, 4 watchpoints
Info : starting gdb server for stm32f2x.cpu on 3333
Info : Listening on port 3333 for gdb connections
target halted due to debug-request, current mode: Thread 
xPSR: 0x01000000 pc: 0x080011f8 msp: 0x20020000
Info : device id = 0x201f6411
Info : flash size = 1024 kbytes
auto erase enabled
wrote 16384 bytes from file stm32-eth-usb.elf in 0.765686s (20.896 KiB/s)

On vérifie sur notre port série que tout fonctionne comme espéré:

remdzi@remdzi-Latitude-5520:~/stm32-eth-usb/build$ stty -F /dev/ttyACM0 115200 cs8 -cstopb -parenb raw
remdzi@remdzi-Latitude-5520:~/stm32-eth-usb/build$ cat /dev/ttyACM0 
Hello world!

Voilà! la mise en place de notre chaîne de compilation, flash et debug est terminée, nous allons enfin pouvoir entrer dans le vif du sujet!

Attention tout de même, au fait que notre implémentation de la fonction _write() est un peu trop simpliste car la fonction HAL_UART_Transmit() est synchrone et donc bloquante. En conséquence, chaque appel à printf() est lui aussi bloquant le temps de transférer tous les octets sur le port série. Pour un message de 14 octets, comme le "Hello World!\r\n" ça représente 14 octets * 10 bits (start + 8 + sop) / 115200bps =~ 1200us, ce qui est clairement non négligeable à l'échelle du temps d'exécution d'un logiciel embarqué qui se voudrait temps-réel. De plus, le formatage des chaînes de caractère via printf() est lui aussi consommateur de ressources, aussi bien en terme de temps d'exécution que de consommation de la mémoire en heap et en stack. Donc utilisez les printf() en connaissance de cause, avec parcimonie pour ne pas créer plus de problèmes que vous ne tenteriez d'en résoudre avec vos messages de debug ;-).

Dans un prochain article nous pourrions présenter une implémentation plus efficace d'une liaison série de debug, mais aussi aller plus loin dans la mise en place de la chaîne de développement, ici volontairement minimaliste, afin de vous présenter des outils un peu plus "user friendly" que la simple ligne de commande. Dites-nous dans les commentaires si ces sujets vous intéressent !

Mise en oeuvre du contrôleur USB et de la classe CDC-ECM

Si on fouille dans la partie "middleware" USB du package STM32CubeF2, on y trouve les différentes classes USB supportées pour les "devices" :

remdzi@remdzi-Latitude-5520:~/stm32-eth-usb$ ls STM32CubeF2/Middlewares/ST/STM32_USB_Device_Library/Class/
AUDIO  CDC  CustomHID  DFU  HID  MSC  Template

On y voit bien une classe "CDC", mais c’est la subclasse "ACM", celle qui émule un port série et non pas celle ("ECM") qui émule un port Ethernet sur l’USB.

Si on fouille dans le "middleware" USB du package pour le F4, alors on y trouve le support de la classe "CDC-ECM" que l’on cherche. Pourquoi ST a-t-il omis la classe "CDC-ECM" dans la version actuelle du package F2 ? Je ne sais pas, mais le plus simple sera d’utiliser directement le package générique https://github.com/STMicroelectronics/stm32_mw_usb_device qui lui intègre toutes les classes fournies par ST, y compris la classe "CDC-ECM" https://github.com/STMicroelectronics/stm32_mw_usb_device/tree/master/Class/CDC_ECM .

On commence donc par cloner ce package "middleware" USB générique dans notre projet:

remdzi@remdzi-Latitude-5520:~/stm32-eth-usb$ git clone https://github.com/STMicroelectronics/stm32_mw_usb_device
remdzi@remdzi-Latitude-5520:~/stm32-eth-usb$ cd stm32_mw_usb_device/
remdzi@remdzi-Latitude-5520:~/stm32-eth-usb/stm32_mw_usb_device$ git log
commit 2022e75b01a499b17acd17d28691b1ed5bbef2dc (HEAD -> master, tag: v2.11.1, origin/master, origin/HEAD)
Author: slimih <hana.slimi@st.com>
Date:   Wed Oct 5 11:35:41 2022 +0100

    Release v2.11.1

Si on explore un peu le package, on voit que plusieurs templates sont fournis, que nous allons devoir adapter à notre projet:

remdzi@remdzi-Latitude-5520:~/stm32-eth-usb/stm32_mw_usb_device$ find -name *template*
…
./Class/CDC_ECM/Src/usbd_cdc_ecm_if_template.c
./Class/CDC_ECM/Inc/usbd_cdc_ecm_if_template.h
…
./Core/Src/usbd_desc_template.c
./Core/Src/usbd_conf_template.c
./Core/Inc/usbd_desc_template.h
./Core/Inc/usbd_conf_template.h

Le template usbd_desc sert à définir le descripteur USB de notre "device". Il permet, par exemple, de choisir le couple PID/VID mais aussi diverses chaînes de caractères de configuration:

#define USBD_VID                      0x0483
#define USBD_PID                      0xaaaa  /* Replace '0xaaaa' with your device product ID */
#define USBD_LANGID_STRING            0xbbb   /* Replace '0xbbb' with your device language ID */
#define USBD_MANUFACTURER_STRING      "xxxxx" /* Add your manufacturer string */
#define USBD_PRODUCT_HS_STRING        "xxxxx" /* Add your product High Speed string */
#define USBD_PRODUCT_FS_STRING        "xxxxx" /* Add your product Full Speed string */
#define USBD_CONFIGURATION_HS_STRING  "xxxxx" /* Add your configuration High Speed string */
#define USBD_INTERFACE_HS_STRING      "xxxxx" /* Add your Interface High Speed string */
#define USBD_CONFIGURATION_FS_STRING  "xxxxx" /* Add your configuration Full Speed string */
#define USBD_INTERFACE_FS_STRING      "xxxxx" /* Add your Interface Full Speed string */

On rapatrie le template usb_desc dans notre projet:

remdzi@remdzi-Latitude-5520:~/stm32-eth-usb$ cp stm32_mw_usb_device/Core/Inc/usbd_desc_template.h usbd_desc.h
remdzi@remdzi-Latitude-5520:~/stm32-eth-usb$ cp stm32_mw_usb_device/Core/Src/usbd_desc_template.c usbd_desc.c

Puis, on modifie quelques une des strings. Seules les strings “FS” (comme "Full Speed") nous intéressent puisque nous utiliserons le contrôleur USB "FS" et non "HS" (comme "High Speed").

#define USBD_MANUFACTURER_STRING      "Smile" /* Add your manufacturer string */
#define USBD_PRODUCT_FS_STRING        "NucleoF207 USB<->ETH adaptor" /* Add your product Full Speed string */

Le "middleware" USB fournit par ST n’est pas spécifique à la famille de microcontrôleur STM32.Il pourrait en théorie être utilisé avec n’importe quelle cible. Le template "usbd_conf" sert justement à interfacer le "middleware" USB avec les drivers USB de la cible. Dans notre cas, c’est avec la HAL STM32 qu’on va vouloir travailler. Heureusement, il existe déjà des exemples de "usbd_conf" adaptés à la HAL, dans le package STM32CubeF2.

On rapatrie un de ces exemples de "usbd_conf" dans notre projet:

remdzi@remdzi-Latitude-5520:~/stm32-eth-usb$ cp ./STM32CubeF2/Projects/STM322xG_EVAL/Applications/USB_Device/CDC_Standalone/Src/usbd_conf.c .
remdzi@remdzi-Latitude-5520:~/stm32-eth-usb$ cp ./STM32CubeF2/Projects/STM322xG_EVAL/Applications/USB_Device/CDC_Standalone/Inc/usbd_conf.h .

Le contrôleur USB du STM32 possède une RAM spécifique qui implémente les FIFO de transmission et de réception des différents "endpoints". La FIFO de réception est commune à tous les "endpoints" tandis que la FIFO de transmission de chaque "endpoint" est indépendante. Il est nécessaire de choisir la taille allouée pour chacune de ces FIFO, sans dépasser la taille totale de 0x140 mots de 32bits. Attention, les API de la HAL prennent en paramètre une longueur non pas en octets, mais bel et bien en mots de 32bits! On adapte notre usbd_conf.c pour allouer 0x80 mots de 32bits à la FIFO de réception et 0x40 mots de 32 bits à nos trois "endpoints" en transmission. Soit 0x140 mots au total:

USBD_StatusTypeDef  USBD_LL_Init (USBD_HandleTypeDef *pdev)
{
   …
   HAL_PCDEx_SetRxFiFo(&hpcd, 0x80);
   HAL_PCDEx_SetTxFiFo(&hpcd, 0, 0x40);
   HAL_PCDEx_SetTxFiFo(&hpcd, 1, 0x40);
   HAL_PCDEx_SetTxFiFo(&hpcd, 2, 0x40);

On active le support pour les “user string”, qui est nécessaire pour que le "host" puisse lire l’adresse MAC de notre "device":

#define USBD_SUPPORT_USER_STRING_DESC         1

Le template "usbd_cdc_ecm_if" sert lui d’interface entre l’implémentation de la classe CDC-ECM et notre application. C’est lui qui réalisera, entre autres, les callbacks de réception et de transmission des trames Ethernet transférées via l’USB.

On le recopier dans notre projet:

remdzi@remdzi-Latitude-5520:~/stm32-eth-usb$ cp stm32_mw_usb_device/Class/CDC_ECM/Src/usbd_cdc_ecm_if_template.c usbd_cdc_ecm_if.c
remdzi@remdzi-Latitude-5520:~/stm32-eth-usb$ cp stm32_mw_usb_device/Class/CDC_ECM/Inc/usbd_cdc_ecm_if_template.h usbd_cdc_ecm_if.h

Puis, on y place un printf() dans la callback de réception:

static int8_t CDC_ECM_Itf_Receive(uint8_t *Buf, uint32_t *Len)
{
   …
   printf("CDC_ECM_Itf_Receive %ld bytes\r\n", *Len);

Dans la tâche de fond de notre classe CDC-ECM, on n’oublie pas de libérer le buffer de réception après chaque réception USB, sinon notre contrôleur USB répondra indéfiniment par des NAK aux tentatives de transmission du "host". Pour le moment, nous ne faisons rien des données reçues:

static int8_t CDC_ECM_Itf_Process(USBD_HandleTypeDef *pdev)
{
…
  if (hcdc_cdc_ecm->LinkStatus != 0U)
  {
    if (  hcdc_cdc_ecm->RxState == 1U)
    {
       hcdc_cdc_ecm->RxState = 0U;
       hcdc_cdc_ecm->RxLength = 0U;
       (void)USBD_CDC_ECM_SetRxBuffer(&USBD_Device, UserRxBuffer);
       USBD_CDC_ECM_ReceivePacket(&USBD_Device);
       printf("USBD_CDC_ECM_SetRxBuffer\r\n");
…
}

Dans le main, on initialise notre "device" USB avec sa classe "CDC-ECM", on appelle cycliquement la tâche de fond (Process()) et on n’oublie pas définir le vecteur d’interruption de notre contrôleur USB:

int main(void)
{
   uint32_t u32PreviousTick = 0;

   …

   /* Init Device Library, add supported class and start the library. */
   if (USBD_Init(&USBD_Device, &Class_Desc, 0) != USBD_OK)
   {
     Error_Handler();
   }
   if (USBD_RegisterClass(&USBD_Device, &USBD_CDC_ECM) != USBD_OK)
   {
     Error_Handler();
   }
   if (USBD_CDC_ECM_RegisterInterface(&USBD_Device, &USBD_CDC_ECM_fops) != USBD_OK)
   {
     Error_Handler();
   }
   if (USBD_Start(&USBD_Device) != USBD_OK)
   {
     Error_Handler();
   }

   while(1)
   {
      if (HAL_GetTick() - u32PreviousTick > 1000)
      {
         u32PreviousTick += 1000;
         printf("One second loop!\r\n");
      }
      
      USBD_CDC_ECM_fops.Process(&USBD_Device);
   }
}

void OTG_FS_IRQHandler(void)
{
   HAL_PCD_IRQHandler(&hpcd);
}

On ajoute au CMakeLists.txt les nouveaux sources et headers:

set(SRC_FILES
   …
   STM32CubeF2/Drivers/STM32F2xx_HAL_Driver/Src/stm32f2xx_hal_pcd.c
   STM32CubeF2/Drivers/STM32F2xx_HAL_Driver/Src/stm32f2xx_hal_pcd_ex.c
   STM32CubeF2/Drivers/STM32F2xx_HAL_Driver/Src/stm32f2xx_ll_usb.c
   usbd_conf.c
   usbd_cdc_ecm_if.c
   usbd_desc.c
   stm32_mw_usb_device/Core/Src/usbd_core.c
   stm32_mw_usb_device/Core/Src/usbd_ctlreq.c
   stm32_mw_usb_device/Core/Src/usbd_ioreq.c
   stm32_mw_usb_device/Class/CDC_ECM/Src/usbd_cdc_ecm.c
   …
   )

target_include_directories(${EXECUTABLE} PRIVATE
   …
   stm32_mw_usb_device/Core/Inc
   stm32_mw_usb_device/Class/CDC_ECM/Inc
   …
   )

On peut alors recompiler notre application et reflasher notre board:

remdzi@remdzi-Latitude-5520:~/stm32-eth-usb/build$ make
Scanning dependencies of target stm32-eth-usb.elf
[  5%] Building ASM object CMakeFiles/stm32-eth-usb.elf.dir/STM32CubeF2/Drivers/CMSIS/Device/ST/STM32F2xx/Source/Templates/gcc/startup_stm32f207xx.s.o
[ 10%] Building C object CMakeFiles/stm32-eth-usb.elf.dir/STM32CubeF2/Drivers/CMSIS/Device/ST/STM32F2xx/Source/Templates/system_stm32f2xx.c.o
[ 15%] Building C object CMakeFiles/stm32-eth-usb.elf.dir/STM32CubeF2/Drivers/STM32F2xx_HAL_Driver/Src/stm32f2xx_hal.c.o
[ 20%] Building C object CMakeFiles/stm32-eth-usb.elf.dir/STM32CubeF2/Drivers/STM32F2xx_HAL_Driver/Src/stm32f2xx_hal_cortex.c.o
[ 25%] Building C object CMakeFiles/stm32-eth-usb.elf.dir/STM32CubeF2/Drivers/STM32F2xx_HAL_Driver/Src/stm32f2xx_hal_gpio.c.o
[ 30%] Building C object CMakeFiles/stm32-eth-usb.elf.dir/STM32CubeF2/Drivers/STM32F2xx_HAL_Driver/Src/stm32f2xx_hal_rcc.c.o
[ 35%] Building C object CMakeFiles/stm32-eth-usb.elf.dir/STM32CubeF2/Drivers/STM32F2xx_HAL_Driver/Src/stm32f2xx_hal_uart.c.o
[ 40%] Building C object CMakeFiles/stm32-eth-usb.elf.dir/STM32CubeF2/Drivers/STM32F2xx_HAL_Driver/Src/stm32f2xx_hal_pcd.c.o
[ 45%] Building C object CMakeFiles/stm32-eth-usb.elf.dir/STM32CubeF2/Drivers/STM32F2xx_HAL_Driver/Src/stm32f2xx_hal_pcd_ex.c.o
[ 50%] Building C object CMakeFiles/stm32-eth-usb.elf.dir/STM32CubeF2/Drivers/STM32F2xx_HAL_Driver/Src/stm32f2xx_ll_usb.c.o
[ 55%] Building C object CMakeFiles/stm32-eth-usb.elf.dir/usbd_conf.c.o
[ 60%] Building C object CMakeFiles/stm32-eth-usb.elf.dir/usbd_cdc_ecm_if.c.o
[ 65%] Building C object CMakeFiles/stm32-eth-usb.elf.dir/usbd_desc.c.o
[ 70%] Building C object CMakeFiles/stm32-eth-usb.elf.dir/stm32_mw_usb_device/Core/Src/usbd_core.c.o
[ 75%] Building C object CMakeFiles/stm32-eth-usb.elf.dir/stm32_mw_usb_device/Core/Src/usbd_ctlreq.c.o
[ 80%] Building C object CMakeFiles/stm32-eth-usb.elf.dir/stm32_mw_usb_device/Core/Src/usbd_ioreq.c.o
[ 85%] Building C object CMakeFiles/stm32-eth-usb.elf.dir/stm32_mw_usb_device/Class/CDC_ECM/Src/usbd_cdc_ecm.c.o
[ 90%] Building C object CMakeFiles/stm32-eth-usb.elf.dir/main.c.o
[ 95%] Building C object CMakeFiles/stm32-eth-usb.elf.dir/uart.c.o
[100%] Linking C executable stm32-eth-usb.elf
   text    data     bss     dec     hex filename
  23212     388    7160   30760    7828 stm32-eth-usb.elf
[100%] Built target stm32-eth-usb.elf
remdzi@remdzi-Latitude-5520:~/stm32-eth-usb/build$ openocd -f ../st_nucleo_f2.cfg -c init -c "reset halt" -c "flash write_image erase stm32-eth-usb.elf" -c "reset run" -c exit

Il n’est pas possible d’alimenter la carte Nucleo via son port USB "OTG". Il est donc nécessaire de l’alimenter via le premier câble USB connecté au ST-Link, puis de brancher un second câble entre votre machine de développement et le port USB "OTG" de la Nucleo.

Ceci fait les logs du kernel et la commande ip nous montrent que notre machine à correctement énuméré notre "device" USB, qui a bien été reconnu comme une classe "CDC-ECM" et par conséquent nous disposons maintenant d’une nouvelle interface réseau:

remdzi@remdzi-Latitude-5520:~/stm32-eth-usb/build$ sudo dmesg --follow
[190509.688199] usb 3-4: USB disconnect, device number 114
[190509.688327] cdc_ether 3-4:1.0 enx000202030000: unregister 'cdc_ether' usb-0000:00:14.0-4, CDC Ethernet Device
[190511.393527] usb 3-4: new full-speed USB device number 115 using xhci_hcd
[190511.544006] usb 3-4: New USB device found, idVendor=0483, idProduct=aaaa, bcdDevice= 2.00
[190511.544016] usb 3-4: New USB device strings: Mfr=1, Product=2, SerialNumber=3
[190511.544019] usb 3-4: Product: NucleoF207 USB<->ETH adaptor
[190511.544022] usb 3-4: Manufacturer: Smile
[190511.544024] usb 3-4: SerialNumber: 3772386D3335
[190511.548565] cdc_ether 3-4:1.0 eth0: register 'cdc_ether' at usb-0000:00:14.0-4, CDC Ethernet Device, 00:02:02:03:00:00
[190511.575734] cdc_ether 3-4:1.0 enx000202030000: renamed from eth0
remdzi@remdzi-Latitude-5520:~/stm32-eth-usb/build$ ip a
1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN group default qlen 1000
    link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
    inet 127.0.0.1/8 scope host lo
       valid_lft forever preferred_lft forever
    inet6 ::1/128 scope host 
       valid_lft forever preferred_lft forever
2: enp0s31f6: <NO-CARRIER,BROADCAST,MULTICAST,UP> mtu 1500 qdisc fq_codel state DOWN group default qlen 1000
    link/ether 00:be:43:7a:42:4a brd ff:ff:ff:ff:ff:ff
3: wlp0s20f3: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue state UP group default qlen 1000
    link/ether 10:a5:1d:9d:8e:d3 brd ff:ff:ff:ff:ff:ff
    inet 192.168.0.38/24 brd 192.168.0.255 scope global dynamic noprefixroute wlp0s20f3
       valid_lft 28527sec preferred_lft 28527sec
    inet6 2a01:e0a:bc3:3800:1927:8e80:4cd2:7c9c/64 scope global temporary dynamic 
       valid_lft 86326sec preferred_lft 71682sec
    inet6 2a01:e0a:bc3:3800:1b14:2309:6f3:72eb/64 scope global dynamic mngtmpaddr noprefixroute 
       valid_lft 86326sec preferred_lft 86326sec
    inet6 fe80::64ab:9bf5:2636:e00/64 scope link noprefixroute 
       valid_lft forever preferred_lft forever
4: enx000202030000: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc fq_codel state UNKNOWN group default qlen 1000
    link/ether 00:02:02:03:00:00 brd ff:ff:ff:ff:ff:ff
    inet6 fe80::50e:6007:c322:12d9/64 scope link noprefixroute 
       valid_lft forever preferred_lft forever

Si on observe nos messages de debug sur le port série, on voit que notre carte reçoit des demandes d’émission de trames Ethernet qui correspondent aux tentatives de découverte du réseau par notre machine de développement:

remdzi@remdzi-Latitude-5520:~/stm32-eth-usb/build$ cat /dev/ttyACM0 
Hello world!
CDC_ECM_Itf_Receive 346 bytes
USBD_CDC_ECM_SetRxBuffer
CDC_ECM_Itf_Receive 110 bytes
USBD_CDC_ECM_SetRxBuffer
CDC_ECM_Itf_Receive 130 bytes
USBD_CDC_ECM_SetRxBuffer
CDC_ECM_Itf_Receive 86 bytes
USBD_CDC_ECM_SetRxBuffer
CDC_ECM_Itf_Receive 90 bytes
USBD_CDC_ECM_SetRxBuffer
One second loop!

On peut observer avec Wireshark ces même tentatives d’émission de trame Ethernet:

 

Première victoire donc: le côté USB de notre adaptateur fonctionne correctement! Il ne nous reste plus qu’à transférer les trames reçues depuis l’USB vers l’Ethernet et vice-versa.

Mise en oeuvre de l’interface Ethernet de la carte Nucleo

Sur la carte Nucleo entre le contrôleur Ethernet du STM32 et le connecteur RJ45 se trouve un PHY de type LAN8742A. L’adresse du PHY est 0 comme imposé par la résistance de pull-down sur la broche RXER/PHYAD0:

On trouve quelques exemples de code autour de l’interface Ethernet dans le package STM32F2Cube, mais ils sont orientés vers l’intégration de la stack "lwip". Nous ne pouvons donc pas les réutiliser mais nous pouvons nous en inspirer.

On commence par initialiser le contrôleur Ethernet et le PHY en prenant soin d’utiliser la bonne adresse pour le PHY et de désactiver l'auto-négociation car la HAL la gère mal si aucun câble n’est connecté lors de l’initialisation…

void HAL_ETH_MspInit(ETH_HandleTypeDef* ethHandle)
{
  GPIO_InitTypeDef GPIO_InitStruct = {0};
  if(ethHandle->Instance==ETH)
  {
  /* USER CODE BEGIN ETH_MspInit 0 */

  /* USER CODE END ETH_MspInit 0 */
    /* Enable Peripheral clock */
    __HAL_RCC_ETH_CLK_ENABLE();

    __HAL_RCC_GPIOC_CLK_ENABLE();
    __HAL_RCC_GPIOA_CLK_ENABLE();
    __HAL_RCC_GPIOB_CLK_ENABLE();
    __HAL_RCC_GPIOG_CLK_ENABLE();

    GPIO_InitStruct.Pin = RMII_MDC_Pin|RMII_RXD0_Pin|RMII_RXD1_Pin;
    GPIO_InitStruct.Mode = GPIO_MODE_AF_PP;
    GPIO_InitStruct.Pull = GPIO_NOPULL;
    GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_VERY_HIGH;
    GPIO_InitStruct.Alternate = GPIO_AF11_ETH;
    HAL_GPIO_Init(GPIOC, &GPIO_InitStruct);

    GPIO_InitStruct.Pin = RMII_REF_CLK_Pin|RMII_MDIO_Pin|RMII_CRS_DV_Pin;
    GPIO_InitStruct.Mode = GPIO_MODE_AF_PP;
    GPIO_InitStruct.Pull = GPIO_NOPULL;
    GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_VERY_HIGH;
    GPIO_InitStruct.Alternate = GPIO_AF11_ETH;
    HAL_GPIO_Init(GPIOA, &GPIO_InitStruct);

    GPIO_InitStruct.Pin = RMII_TXD1_Pin;
    GPIO_InitStruct.Mode = GPIO_MODE_AF_PP;
    GPIO_InitStruct.Pull = GPIO_NOPULL;
    GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_VERY_HIGH;
    GPIO_InitStruct.Alternate = GPIO_AF11_ETH;
    HAL_GPIO_Init(RMII_TXD1_GPIO_Port, &GPIO_InitStruct);

    GPIO_InitStruct.Pin = RMII_TX_EN_Pin|RMII_TXD0_Pin;
    GPIO_InitStruct.Mode = GPIO_MODE_AF_PP;
    GPIO_InitStruct.Pull = GPIO_NOPULL;
    GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_VERY_HIGH;
    GPIO_InitStruct.Alternate = GPIO_AF11_ETH;
    HAL_GPIO_Init(GPIOG, &GPIO_InitStruct);

    /* Peripheral interrupt init */
    HAL_NVIC_SetPriority(ETH_IRQn, 0, 0);
    HAL_NVIC_EnableIRQ(ETH_IRQn);
  }
}

void eth_init(void)
{
  HAL_StatusTypeDef hal_eth_init_status;

/* Init ETH */

   uint8_t MACAddr[6] ;
  heth.Instance = ETH;
  heth.Init.AutoNegotiation = ETH_AUTONEGOTIATION_DISABLE;
  heth.Init.Speed = ETH_SPEED_100M;
  heth.Init.DuplexMode = ETH_MODE_FULLDUPLEX;
  heth.Init.PhyAddress = 0;
  MACAddr[0] = 0x00;
  MACAddr[1] = 0x02;
  MACAddr[2] = 0x02;
  MACAddr[3] = 0x03;
  MACAddr[4] = 0x00;
  MACAddr[5] = 0x00;
  heth.Init.MACAddr = &MACAddr[0];
  heth.Init.RxMode = ETH_RXINTERRUPT_MODE;
  heth.Init.ChecksumMode = ETH_CHECKSUM_BY_HARDWARE;
  heth.Init.MediaInterface = ETH_MEDIA_INTERFACE_RMII;

  hal_eth_init_status = HAL_ETH_Init(&heth);
  if (hal_eth_init_status != HAL_OK)
  {
    printf("ERROR: HAL_ETH_Init has failed\r\n");
  }

Le contrôleur Ethernet du STM32 intègre un DMA qui lui permet de transmettre ou recevoir plusieurs trames vers ou depuis un pool de buffers situé en RAM. Ce pool de buffers se configure au travers de descripteur DMA eux aussi situés en RAM:

  /* Initialize Tx Descriptors list: Chain Mode */
  HAL_ETH_DMATxDescListInit(&heth, DMATxDscrTab, &Tx_Buff[0][0], ETH_TXBUFNB);

  /* Initialize Rx Descriptors list: Chain Mode  */
  HAL_ETH_DMARxDescListInit(&heth, DMARxDscrTab, &Rx_Buff[0][0], ETH_RXBUFNB);

  /* Enable MAC and DMA transmission and reception */
  HAL_ETH_Start(&heth);
}

Pour savoir si un buffer de réception est plein, on scrute le statut des descripteurs DMA en réception en s’appuyant sur le fonction HAL_ETH_GetReceivedFrame. Attention, si la trame Ethernet reçue est plus grande qu’un des buffers RAM alloués au travers des descripteurs DMA, alors la trame sera divisée en segments et occupera plusieurs buffers. Dans notre implémentation, nous choisissons une taille de buffer > 1536 octets pour éviter d’avoir à gérer ce cas. On ajoute tout de même un printf() de debug pour détecter cette situation, le cas échéant: 

uint8_t * eth_receive(uint32_t * length)
{
  if (HAL_ETH_GetReceivedFrame(&heth) != HAL_OK)
     return NULL;

  if(heth.RxFrameInfos.SegCount != 1)
     printf("ERROR: heth.RxFrameInfos.SegCount = %ld\r\n", heth.RxFrameInfos.SegCount);
  *length = heth.RxFrameInfos.length;
  return (uint8_t *)heth.RxFrameInfos.buffer;
}

Une fois que la trame reçue a été traitée, on peut libérer le buffer en abaissant le flag ETH_DMARXDESC_OWN:

void eth_release_rx_buf(void)
{
  __IO ETH_DMADescTypeDef *dmarxdesc;
  int i;

  /* Release descriptors to DMA */
  /* Point to first descriptor */
  dmarxdesc = heth.RxFrameInfos.FSRxDesc;
  /* Set Own bit in Rx descriptors: gives the buffers back to DMA */
  for (i=0; i< heth.RxFrameInfos.SegCount; i++)
  {
     dmarxdesc->Status |= ETH_DMARXDESC_OWN;
     dmarxdesc = (ETH_DMADescTypeDef *)(dmarxdesc->Buffer2NextDescAddr);
  }

  /* Clear Segment_Count */
  heth.RxFrameInfos.SegCount =0;
}

En émission, on cherche un buffer libre en scrutant le flag ETH_DMATXDESC_OWN

uint8_t * eth_get_tx_buf(void)
{
  __IO ETH_DMADescTypeDef *DmaTxDesc = heth.TxDesc;
  uint8_t *buffer = (uint8_t *)(heth.TxDesc->Buffer1Addr);

  if((DmaTxDesc->Status & ETH_DMATXDESC_OWN) != (uint32_t)RESET)
  {  
     return NULL;
  }
  else
  {
     return buffer;
  }
}

Une fois le buffer récupéré, on peut le remplir puis activer sa transmission avec la fonction HAL_ETH_TransmitFrame():

void eth_send(uint32_t length)
{
  HAL_ETH_TransmitFrame(&heth, length);

Il ne nous reste plus qu’à adapter les callbacks de la classe CDC-ECM pour s’appuyer sur ces fonctions Ethernet, afin de réaliser un transfert des trames USB vers l'Ethernet et vice-versa. Il faut savoir que le payload des trames USB "CDC-ECM" n'est autre qu'une trame Ethernet. Si nous regardions leur contenu, nous y trouverions, entre autres, les adresses MAC de source et de destination, les data, etc. Il n'y a donc pas besoin de faire un formatage particulier lors du transfert entre l'USB et l'Ethernet.

Afin de ne pas avoir à recopier le buffer de réception USB dans les buffers de transmission Ethernet et vice-versa, on va configurer notre classe "CDC-ECM" pour directement travailler vers et depuis les buffers Ethernet. On réalise ainsi un transfert “zero copy”:

static int8_t CDC_ECM_Itf_Receive(uint8_t *Buf, uint32_t *Len)
{
…
  /* Call Eth buffer processing */
  hcdc_cdc_ecm->RxState = 1U;
…
}

static int8_t CDC_ECM_Itf_TransmitCplt(uint8_t *Buf, uint32_t *Len, uint8_t epnum)
{
…
  /* USB has completed the transmission of the ethernet frame received: 
  --> Release ethernet buffer */
  eth_release_rx_buf();
…
}

static int8_t CDC_ECM_Itf_Process(USBD_HandleTypeDef *pdev)
{
…
    if (hcdc_cdc_ecm->RxState == 1U)
    {
       /* USB has received a buffer
       --> Send it to ethernet */
       eth_send(hcdc_cdc_ecm->RxLength);
       hcdc_cdc_ecm->RxState = 2U;
    }
    else if (hcdc_cdc_ecm->RxState == 2U)
    {   
       /* Get a new ethernet tx buffer and configure USB to receive on it */
       eth_tx_buf = eth_get_tx_buf();
       if (eth_tx_buf != NULL)
       {
          hcdc_cdc_ecm->RxLength = 0;
          hcdc_cdc_ecm->RxState = 0U;
          (void)USBD_CDC_ECM_SetRxBuffer(&USBD_Device, eth_tx_buf);
          USBD_CDC_ECM_ReceivePacket(&USBD_Device);
       }
       else
       {
          printf("ERROR: CDC_ECM_Itf_Receive no more ETH TX buffer available !!!\r\n");
       }
    }
    
    if (hcdc_cdc_ecm->TxState == 0U)
    {
       /* Get an ethernet rx buffer and configure USB to send it */
       eth_rx_buf = eth_receive(&eth_rx_length);
       if (eth_rx_buf != NULL)
       {
         (void)USBD_CDC_ECM_SetTxBuffer(&USBD_Device, eth_rx_buf, eth_rx_length);
         USBD_CDC_ECM_TransmitPacket(&USBD_Device);
       }
    }
…
}

On n’oublie pas d’appeler notre fonction eth_init() dans le main et d’ajouter les nouveaux fichiers sources à la chaîne de compilation:

set(SRC_FILES
…
  STM32CubeF2/Drivers/STM32F2xx_HAL_Driver/Src/stm32f2xx_hal_eth.c
  eth.c
…
 )

Après avoir recompilé et flashé notre programme dans la carte, je branche un câble Ethernet entre la carte et la box de mon fournisseur d'accès et la commande "ip" me montre que mon interface réseau a bien récupéré une adresse IPv4 via DHCP:

remdzi@remdzi-Latitude-5520:~/stm32-eth-usb/build$ ip a
1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN group default qlen 1000
    link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
    inet 127.0.0.1/8 scope host lo
       valid_lft forever preferred_lft forever
    inet6 ::1/128 scope host 
       valid_lft forever preferred_lft forever
2: enp0s31f6: <NO-CARRIER,BROADCAST,MULTICAST,UP> mtu 1500 qdisc fq_codel state DOWN group default qlen 1000
    link/ether 00:be:43:7a:42:4a brd ff:ff:ff:ff:ff:ff
3: wlp0s20f3: <BROADCAST,MULTICAST> mtu 1500 qdisc noqueue state DOWN group default qlen 1000
    link/ether 10:a5:1d:9d:8e:d3 brd ff:ff:ff:ff:ff:ff
4: enx000202030000: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc fq_codel state UNKNOWN group default qlen 1000
    link/ether 00:02:02:03:00:00 brd ff:ff:ff:ff:ff:ff
    inet 192.168.0.28/24 brd 192.168.0.255 scope global dynamic noprefixroute enx000202030000
       valid_lft 42926sec preferred_lft 42926sec
    inet6 fe80::50e:6007:c322:12d9/64 scope link noprefixroute 
       valid_lft forever preferred_lft forever

L’émission et la transmission des trames Ethernet fonctionne donc correctement. Pour tester les performances, je lance le téléchargement d’un gros fichier (ISO Debian):

remdzi@remdzi-Latitude-5520:~/stm32-eth-usb/build$ wget https://cdimage.debian.org/debian-cd/current/amd64/iso-cd/debian-11.6.0-amd64-netinst.iso
--2023-01-04 15:44:48--  https://cdimage.debian.org/debian-cd/current/amd64/iso-cd/debian-11.6.0-amd64-netinst.iso
Résolution de cdimage.debian.org (cdimage.debian.org)… 194.71.11.163, 194.71.11.165, 194.71.11.173, ...
Connexion à cdimage.debian.org (cdimage.debian.org)|194.71.11.163|:443… connecté.
requête HTTP transmise, en attente de la réponse… 302 Found
Emplacement : https://gemmei.ftp.acc.umu.se/debian-cd/current/amd64/iso-cd/debian-11.6.0-amd64-netinst.iso [suivant]
--2023-01-04 15:44:48--  https://gemmei.ftp.acc.umu.se/debian-cd/current/amd64/iso-cd/debian-11.6.0-amd64-netinst.iso
Résolution de gemmei.ftp.acc.umu.se (gemmei.ftp.acc.umu.se)… 194.71.11.137, 2001:6b0:19::137
Connexion à gemmei.ftp.acc.umu.se (gemmei.ftp.acc.umu.se)|194.71.11.137|:443… connecté.
requête HTTP transmise, en attente de la réponse… 200 OK
Taille : 406847488 (388M) [application/x-iso9660-image]
Enregistre : ‘debian-11.6.0-amd64-netinst.iso’

debian-11.6.0-amd64-netinst.iso                  100%[=======================================================================
================================>] 388,00M  1,00MB/s    ds 6m 50s  

2023-01-04 15:51:38 (968 KB/s) - ‘debian-11.6.0-amd64-netinst.iso’ enregistré [406847488/406847488]

On obtient donc un débit d’environ 10Mbit/s, ce qui est très honorable comparé aux 12Mbit/s théoriques maximum de l’USB "Full Speed".

Conclusion

Nous avons réussi à construire notre propre adaptateur USB/Ethernet avec des librairies logicielles open-source disponibles sur Internet et seulement quelques dizaines de lignes de code pour réaliser l’intégration du tout.

Bien entendu, il y a encore de nombreux points à améliorer comme la gestion de l'auto-négociation, de l’état du lien ou des compteurs de statistiques. On pourrait aussi pousser la performance plus loin en utilisant les interruptions du contrôleur Ethernet ou un canal DMA pour celles de l’USB.

J'essaierai de traiter ces points, après la publication de cet article, quand je trouverai le temps d’aller plus loin. Donc pensez à garder un oeil sur le dépôt github du projet: https://github.com/remdzi/stm32-eth-usb

Laisser un commentaire

Votre adresse de messagerie ne sera pas publiée.