Archives de catégorie : blog

Prise en main de la GateMateA1-EVB

En 2023, OLIMEX sortait une carte de développement à base de FPGA GateMate (CologneChip):

La GateMateA1-EVB est une carte bien moins cher que le kit de développement officiel et propose tout un tas d’interfaces intéressantes.

Vue de la carte GateMateA1-EVB

Elle était dans mes cartons depuis un certain temps et je n’avais pas encore pris le temps de la tester.

Le site officiel donne la liste de caractéristiques complètes:

  • CCGM1A1 FPGA with 20480 logic cells
  • PSRAM 64Mbit
  • RP2040 processor for programing and debugging
  • 2MB configuration Flash for RP2040
  • 4 buttons
  • USB-C for power supply and programming
  • PS2 connector
  • VGA connector
  • 4 Banks with signals with selectable levels 1.2V 1.8V 2.5V
  • PMOD with level shifters
  • UEXT with level shifters
  • Power LED
  • User LED
  • 4 sections configuration slide switch
  • Dimensions: 120 x 80 mm

Et le pilotage (communication, programmation alimentation debug) se fait via un connecteur USB-C connecté sur un microcontrôleur RP2040.

La plupart des signaux qui sortent sur les connecteurs sont branchés directement sur les bank du FPGA. Ils sont donc à la tension du bank : 1v2, 1v8 ou 2v5. Se sont des tensions assez faible dans les montages. Si l’on veut du 3v3 il faudra se rabattre sur les connecteurs Uext1 et Pmod1 qui possèdent des transformateurs de niveaux de tension bidirectionnels.

Les fichiers de développement de la carte (Kicad) ainsi que le code source d’exemple sont fournis sur un dépot git.

Bref, une belle carte libérée en perspective.

Mise en route

Sous Linux, au branchement de l’usb le message noyau suivant s’affiche:

[fabien:~/projets] $ sudo dmesg -ce
[janv.17 20:57] usb 1-1.3.3: new full-speed USB device number 38 using ehci-pci
[  +0,111290] usb 1-1.3.3: New USB device found, idVendor=1209, idProduct=c0ca, bcdDevice= 1.10
[  +0,000005] usb 1-1.3.3: New USB device strings: Mfr=1, Product=2, SerialNumber=3
[  +0,000002] usb 1-1.3.3: Product: DirtyJTAG
[  +0,000001] usb 1-1.3.3: Manufacturer: Jean THOMAS
[  +0,000001] usb 1-1.3.3: SerialNumber: 2600942311111956
[  +0,018771] cdc_acm 1-1.3.3:1.1: ttyACM0: USB ACM device
[  +0,000018] usbcore: registered new interface driver cdc_acm
[  +0,000002] cdc_acm: USB Abstract Control Model driver for USB modems and ISDN adapters

Nous voyons que le RP2040 nous expose un firmware DirtyJTAG ainsi qu’une UART ttyACM0.

Exemple : un analyseur logique

L’exemple qui est donné dans le manuel de référence de la carte est un analyseur logique open source développé par CologneChip : Gatemate ILA (Integrated Logic Analyser).

On va commencer par installer gatemate ILA :

cd /opt/gatemate/
git clone https://github.com/colognechip/gatemate_ila.git
cd gatemate_ila/app
python3 -m pip install -r requirements.txt 

Il faut bien sur installer également la chaîne de développement de cologne chip. Et modifier le fichier app/config.py pour y inscrir les paths complet des utilitaires suivants:

  • yosys
  • p_r
  • openFPGALoader
  • gtkwave

S’ils ne se trouvent pas dans le path général du projet. Pour ma part il n’y a que p_r qui est spécifique au gatemate et que je n’ai pas installé globalement. J’ai donc téléchargé le kit de dev sur le site de colognechip (10.01.2025) et modifié la ligne suivante dans app/config.py

PR = '/opt/gatemate/cc-toolchain-linux/bin/p_r/p_r'

Les modes de programmation du gatemate avec openFPGALoader sont donnés dans la doc.

Dans le même fichier, on modifie également les flags pour utiliser la sonde DirtyJTAG flashée dans le RP2040 :

UPLOAD_FLAGS = ' -b olimex_gatemateevb '        
CON_DEVICE = 'oli' # O.L.I. France inter, je suis pas petit ...

Dans un premier temps on va rester sur le bug du firmware qui oblige à appuyer sur le bouton reset à chaque fois qu’on configure le FPGA.

Le script qui contrôle l’ILA se nomme ILAcop pour ILA COntrol Program:

python3 ILAcop.py --help
usage: ILAcop.py [-h] [--version] [--clean] [--showdev] [-wd WORK_DIR] {config,start,reconfig} ...

GateMate ILA control program. With this script, you can configure and execute the ILA with a design under test (DUT).

options:
  -h, --help            show this help message and exit
  --version             show program's version number and exit
  --clean               Deletes all output files created by the program.
  --showdev             Outputs all found FTDI ports.
  -wd WORK_DIR          Folder from which Yosys should be started for the synthesis of the Design Under Test.

main_actions:
  {config,start,reconfig}

example usage:
python3 ILAcop.py [Commands]

Commands:
  config:   Configure the ILA.
             -vlog SOURCE    Paths to the Verilog source code files.
             -vhd SOURCE     Paths to the VHDL source code files.
             -t NAME         Top level entity of the design under test.
             -ccf SOURCE     Folder containing the .ccf file of the design under test. 
             -s SPEED        Configure ILA for best performance. Max Sample Width = 40, the number of samples depends on the sample width. 
             -f MHz          Defines the external clock frequency in MHz (default is 10.0 MHz).
             -sync LEVEL     Number of register levels via which the SUT are synchronised (default: 2)
             -d DELAY        ILA PLL Phase shift of sampling frequency. 0=0°, 1=90°, 2=180°, 3=270° (default: 2).
             -opt            Optimizes the design by deleting all unused signals before design evaluation.
          (optional) Subcommands config: 
                -create_json: Creates a JSON file in which the logic analyzer can be configured.
            NOTE: Without the subcommand the configurations are requested step by step via the terminal.
  
  reconfig: Configures the ILA based on a JSON file. With this option you have to specify a JSON file with -l [filename].json.
  
  start     Starts the communication to the ILA with the last uploaded config
    -s  The -s parameter prevents the FPGA from being reconfigured on restart.

Pour configurer le FPGA il semble devoir lancer la commande suivante (dans app/):

$ python3 ILAcop.py config -vlog ../example_dut/blink/src/ -t blink

#################################################################################################
#                   Cologne Chip GateMate ILA control program (ILAcop)                          #
# ********************************************************************************************* #
#    Copyright (C) 2023 Cologne Chip AG <support@colognechip.com>                               #
#    Developed by Dave Fohrn                                                                    #
#                                                                                               #
#    This program is free software: you can redistribute it and/or modify                       #
#    it under the terms of the GNU General Public License as published by                       #
#    the Free Software Foundation, either version 3 of the License, or                          #
#    (at your option) any later version.                                                        #
#                                                                                               #
#    This program is distributed in the hope that it will be useful,                            #
#    but WITHOUT ANY WARRANTY; without even the implied warranty of                             #
#    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the                              #
#    GNU General Public License for more details.                                               #
#                                                                                               #
#    You should have received a copy of the GNU General Public License                          #
#    along with this program.  If not, see <https://www.gnu.org/licenses/>.                     #
#                                                                                               #
# ********************************************************************************************* #
#################################################################################################


################# ccf File ##################
#                                           #
# blink.ccf                                 #
#                                           #
#############################################


############### verilog Files ###############
#                                           #
# blink.v                                   #
#                                           #
#############################################

Examine DUT ...

An error has occurred:
ERROR: Module `\CC_USR_RSTN' referenced in module `\blink' in cell `\usr_rstn_inst' is not part of the design.

yosys cmd: 
yosys -l /opt/gatemate/gatemate_ila/log/yosys_DUT.log -p " read -sv /opt/gatemate/gatemate_ila/example_dut/blink/src/blink.v; read_verilog -lib -specify +/gatemate/cells_sim.v +/gatemate/cells_bb.v; hierarchy -check -top blink; proc; flatten; tribuf -logic; deminout; write_verilog /opt/gatemate/gatemate_ila/app/config_design/blink_25-01-17_22-08-52_flat.v ; check;  alumacc; opt; memory -nomap; opt_clean; memory_libmap -lib +/gatemate/brams.txt; techmap -map +/gatemate/brams_map.v;  stat -width"

Mais pour le moment ça ne marche pas. Un mysterieux module CC_USR_RSTN n’est pas référencé.

Si on essais l’exemple en VHDL c’est pas beaucoup mieux, même si l’erreur est différente (pourtant j’ai bien ghdl installé sur ma machine):

$ python3 ILAcop.py config -vhd ../example_dut/blink/src/ -t blink

#################################################################################################
#                   Cologne Chip GateMate ILA control program (ILAcop)                          #
# ********************************************************************************************* #
#    Copyright (C) 2023 Cologne Chip AG <support@colognechip.com>                               #
#    Developed by Dave Fohrn                                                                    #
#                                                                                               #
#    This program is free software: you can redistribute it and/or modify                       #
#    it under the terms of the GNU General Public License as published by                       #
#    the Free Software Foundation, either version 3 of the License, or                          #
#    (at your option) any later version.                                                        #
#                                                                                               #
#    This program is distributed in the hope that it will be useful,                            #
#    but WITHOUT ANY WARRANTY; without even the implied warranty of                             #
#    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the                              #
#    GNU General Public License for more details.                                               #
#                                                                                               #
#    You should have received a copy of the GNU General Public License                          #
#    along with this program.  If not, see <https://www.gnu.org/licenses/>.                     #
#                                                                                               #
# ********************************************************************************************* #
#################################################################################################


################# ccf File ##################
#                                           #
# blink.ccf                                 #
#                                           #
#############################################


################ vhdl Files #################
#                                           #
# blink.vhd                                 #
#                                           #
#############################################

Examine DUT ...

An error has occurred:
ERROR: No such command: ghdl (type 'help' for a command overview)

yosys cmd: 
yosys -l /opt/gatemate/gatemate_ila/log/yosys_DUT.log -p " ghdl --warn-no-binding -C --ieee=synopsys /opt/gatemate/gatemate_ila/example_dut/blink/src/blink.vhd  -e blink; hierarchy -check -top blink; proc; flatten; tribuf -logic; deminout; write_verilog /opt/gatemate/gatemate_ila/app/config_design/blink_25-01-17_22-12-24_flat.v ; check;  alumacc; opt; memory -nomap; opt_clean; memory_libmap -lib +/gatemate/brams.txt; techmap -map +/gatemate/brams_map.v;  stat -width"

Dans le cas de verilog, le problème vient de la version de yosys utilisé. Si on modifie le fichier config.py pour pointer vers le yosys fourni par CologneChip ça fonctionne :

YOSYS = '/opt/gatemate/cc-toolchain-linux/bin/yosys/yosys'

On peut lancer la commande :

$ python3 ILAcop.py config -vlog ../example_dut/blink/src/ -t blink

#################################################################################################
#                   Cologne Chip GateMate ILA control program (ILAcop)                          #
# ********************************************************************************************* #
#    Copyright (C) 2023 Cologne Chip AG <support@colognechip.com>                               #
#    Developed by Dave Fohrn                                                                    #
#                                                                                               #
#    This program is free software: you can redistribute it and/or modify                       #
#    it under the terms of the GNU General Public License as published by                       #
#    the Free Software Foundation, either version 3 of the License, or                          #
#    (at your option) any later version.                                                        #
#                                                                                               #
#    This program is distributed in the hope that it will be useful,                            #
#    but WITHOUT ANY WARRANTY; without even the implied warranty of                             #
#    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the                              #
#    GNU General Public License for more details.                                               #
#                                                                                               #
#    You should have received a copy of the GNU General Public License                          #
#    along with this program.  If not, see <https://www.gnu.org/licenses/>.                     #
#                                                                                               #
# ********************************************************************************************* #
#################################################################################################


################# ccf File ##################
#                                           #
# blink.ccf                                 #
#                                           #
#############################################


############### verilog Files ###############
#                                           #
# blink.v                                   #
#                                           #
#############################################

Examine DUT ...


############# Block RAM in use ##############
#                                           #
# CC_BRAM_20K in use: 0                     #
# CC_BRAM_40K in use: 0                     #
#                                           #
#############################################


############ Found PLL instance #############
#                                           #
# Pll name  = pll_inst                      #
# Frequency = 25 MHz                        #
#                                           #
#############################################

########### Found CC_USR_RSTN ###########

!!!!!!!!!!!!!!!!!!!!!!!!!!!!! NOTE !!!!!!!!!!!!!!!!!!!!!!!!!!!!!
!                                                              !
! Now you will be guided through the configuration of the ILA. !
! Entering 'e' exits the process and generates a configurable  !
! JSON file for the given DUT.                                 !
! Enter 'p' for 'previous' to backtrack a step.                !
!                                                              !
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!


!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! NOTE !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
!                                                                       !
! In the following, a clock source for the ILA should be selected.      !
! Usually, the same clk signal that clocks the tested signals suffices. !
!                                                                       !
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!


Here are the possible ways to provide a clock to the ILA:

 1 = Use an external clk input signal.
 2 = Use an additional PLL with a freely selectable frequency (additional net of the global Mesh are required).
 3 = Use a signal generated by a PLL from your design.

Please choose between 1 and 3: 3

########### PLL instances signals ###########
#                                           #
# pll_inst : 25 Mhz                         #
#                                           #
#  0 = CLK0                                 #
#  1 = CLK180                               #
#  2 = CLK270                               #
#  3 = CLK90                                #
#                                           #
#                                           #
#############################################


Attention! If you choose an output signal of a PLL that you will not use in your design, an additional net of Global Mesh is required! 

Choose a clock signal: 0

!!!!!!!!!!!!!!!!!!!!! User controllable reset !!!!!!!!!!!!!!!!!!!!!
!                                                                 !
! The ILA can hold the DUT in reset until capture starts.         !
! This makes it possible to capture the start process of the DUT. !
! Attention, the ila treats the signal as active LOW.             !
!                                                                 !
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!

The following options are available:

 1 = Use an external reset input signal.
 2 = Deactivate this function.
 3 = Use the ouput signal from the CC_USR_RSTN primitive in your design. (The functionality of the CC_USR_RSTN primitive is still given).

Please choose between 1 and 3: 2

!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! NOTE !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
!                                                                                                 !
! You will be prompted to select signals for analysis from those found in your design under test. !
!                                                                                                 !
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!


------------------------- blink --------------------------
+----+-------------------+--------+----------+-----------+
|  # | name              | range  | selected | hierarchy |
+----+-------------------+--------+----------+-----------+
|  1 | LED_ctrl          |   1    |    []    |           |
|  2 | clk               |   1    |    []    |           |
|  3 | clk0              |   1    |    []    |           |
|  4 | clk180            |   1    |    []    |           |
|  5 | clk270            |   1    |    []    |           |
|  6 | clk90             |   1    |    []    |           |
|  7 | counter           | [24:0] |    []    |           |
|  8 | led               | [7:0]  |    []    |           |
|  9 | rst               |   1    |    []    |           |
| 10 | usr_pll_lock      |   1    |    []    |           |
| 11 | usr_pll_lock_stdy |   1    |    []    |           |
| 12 | usr_ref_out       |   1    |    []    |           |
+----+-------------------+--------+----------+-----------+

## Number of selected bits to be analysed ###
#                                           #
# 0 (max. 2400)                             #
#                                           #
#############################################

Select signals to be analyzed (0 = finish): 7

!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! NOTE !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
!                                                                                  !
! Define a range for the vector to be analyzed.                                    !
!  you can do this in the following ways:                                          !
!   1) Press enter to analyze the entire vector                                    !
!   2) Define an area of the vector. (The area should be within the vector area):  !
!        e.g.: '[1:0]'                                                             !
!   3) Individual signals:                                                         !
!        e.g.: '1'                                                                 !
!   4) Any combination of areas and individual signals                             !
!        e.g.: '9, [7:5], 3, [1:0]'                                                !
! define Signals in descending order!                                              !
!                                                                                  !
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!

reg [24:0] counter: 


------------------------- blink --------------------------
+----+-------------------+--------+----------+-----------+
|  # | name              | range  | selected | hierarchy |
+----+-------------------+--------+----------+-----------+
|  1 | LED_ctrl          |   1    |    []    |           |
|  2 | clk               |   1    |    []    |           |
|  3 | clk0              |   1    |    []    |           |
|  4 | clk180            |   1    |    []    |           |
|  5 | clk270            |   1    |    []    |           |
|  6 | clk90             |   1    |    []    |           |
|  7 | counter           | [24:0] |  ['A']   |           |
|  8 | led               | [7:0]  |    []    |           |
|  9 | rst               |   1    |    []    |           |
| 10 | usr_pll_lock      |   1    |    []    |           |
| 11 | usr_pll_lock_stdy |   1    |    []    |           |
| 12 | usr_ref_out       |   1    |    []    |           |
+----+-------------------+--------+----------+-----------+

## Number of selected bits to be analysed ###
#                                           #
# 25 (max. 2400)                            #
#                                           #
#############################################

Select signals to be analyzed (0 = finish): 1

------------------------- blink --------------------------
+----+-------------------+--------+----------+-----------+
|  # | name              | range  | selected | hierarchy |
+----+-------------------+--------+----------+-----------+
|  1 | LED_ctrl          |   1    |  ['A']   |           |
|  2 | clk               |   1    |    []    |           |
|  3 | clk0              |   1    |    []    |           |
|  4 | clk180            |   1    |    []    |           |
|  5 | clk270            |   1    |    []    |           |
|  6 | clk90             |   1    |    []    |           |
|  7 | counter           | [24:0] |  ['A']   |           |
|  8 | led               | [7:0]  |    []    |           |
|  9 | rst               |   1    |    []    |           |
| 10 | usr_pll_lock      |   1    |    []    |           |
| 11 | usr_pll_lock_stdy |   1    |    []    |           |
| 12 | usr_ref_out       |   1    |    []    |           |
+----+-------------------+--------+----------+-----------+

## Number of selected bits to be analysed ###
#                                           #
# 26 (max. 2400)                            #
#                                           #
#############################################

Select signals to be analyzed (0 = finish): 8

!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! NOTE !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
!                                                                                  !
! Define a range for the vector to be analyzed.                                    !
!  you can do this in the following ways:                                          !
!   1) Press enter to analyze the entire vector                                    !
!   2) Define an area of the vector. (The area should be within the vector area):  !
!        e.g.: '[1:0]'                                                             !
!   3) Individual signals:                                                         !
!        e.g.: '1'                                                                 !
!   4) Any combination of areas and individual signals                             !
!        e.g.: '9, [7:5], 3, [1:0]'                                                !
! define Signals in descending order!                                              !
!                                                                                  !
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!

wire [7:0] led: 


------------------------- blink --------------------------
+----+-------------------+--------+----------+-----------+
|  # | name              | range  | selected | hierarchy |
+----+-------------------+--------+----------+-----------+
|  1 | LED_ctrl          |   1    |  ['A']   |           |
|  2 | clk               |   1    |    []    |           |
|  3 | clk0              |   1    |    []    |           |
|  4 | clk180            |   1    |    []    |           |
|  5 | clk270            |   1    |    []    |           |
|  6 | clk90             |   1    |    []    |           |
|  7 | counter           | [24:0] |  ['A']   |           |
|  8 | led               | [7:0]  |  ['A']   |           |
|  9 | rst               |   1    |    []    |           |
| 10 | usr_pll_lock      |   1    |    []    |           |
| 11 | usr_pll_lock_stdy |   1    |    []    |           |
| 12 | usr_ref_out       |   1    |    []    |           |
+----+-------------------+--------+----------+-----------+

## Number of selected bits to be analysed ###
#                                           #
# 34 (max. 2400)                            #
#                                           #
#############################################

Select signals to be analyzed (0 = finish): 0

!!!!!!!!!!!!!!!!!!! Note !!!!!!!!!!!!!!!!!!!!
!                                           !
! The capture duration must be defined.     !
! The maximum duration depends on:          !
!  - available ram                          !
!  - width of the sample                    !
!  - sampling frequency                     !
! FIFO Cascade (Width x Depth)              !
! FIFO (Input Width x Depth)                !
!                                           !
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!

------Please choose one of the following durations: -------
+----+---------+---------------+--------------+-----------+
|  # | smp_cnt | duration [us] | FIFO Cascade |      FIFO |
+----+---------+---------------+--------------+-----------+
|  1 |    1024 |         40.96 |        1 x 1 | 40 x 1024 |
|  2 |    2048 |         81.92 |        1 x 2 | 40 x 1024 |
|  3 |    3072 |        122.88 |        1 x 3 | 40 x 1024 |
|  4 |    4096 |        163.84 |        4 x 1 | 10 x 4096 |
|  5 |    5120 |         204.8 |        1 x 5 | 40 x 1024 |
|  6 |    6144 |        245.76 |        1 x 6 | 40 x 1024 |
|  7 |    8192 |        327.68 |        4 x 2 | 10 x 4096 |
|  8 |   12288 |        491.52 |        4 x 3 | 10 x 4096 |
|  9 |   16384 |        655.36 |        4 x 4 | 10 x 4096 |
| 10 |   20480 |         819.2 |        4 x 5 | 10 x 4096 |
| 11 |   24576 |        983.04 |        4 x 6 | 10 x 4096 |
+----+---------+---------------+--------------+-----------+

Total Capture duration (choose between 1 and 11): 1

############# Capture duration ##############
#                                           #
# Sample count = 1024                       #
# Capture duration = 40.96 us               #
#                                           #
#############################################


Enter the number of capture samples before trigger activation (between 0 and 250): 10

###### Capture duration before Trigger ######
#                                           #
# Sample count = 10                         #
# Capture duration = 0.52 us                #
#                                           #
#############################################


!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! Note !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
!                                                                                   !
! You can override an input or input-vector of your top-level entity using the ILA. !
! Please note that the input will no longer be connected to the FPGA's IO pins.     !
!                                                                                   !
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!


Would you like to implement the input control feature? (y/N): 

!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! Note !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
!                                                                         !
! There are two default triggers that can be set for exactly one signal:  !
!  'rising edge' and 'falling edge'                                       !
! There is also an optional trigger: pattern compare                      !
! With this option, a pattern can be set across the entire bit width,     !
!  determining for each bit whether it should be '1', '0', or 'dc'        !
!  (don't care) to activate the trigger.                                  !
! If this function is activated, more hardware is required for the ILA    !
!  and the maximum possible sampling frequency may be reduced.            !
!                                                                         !
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!


Would you like to implement the function for comparing bit patterns? (y/N): 

############ Signals under test #############
#                                           #
# LED_ctrl                                  #
# [24:0] counter                            #
# [7:0] led                                 #
#                                           #
#############################################

Execute Synthesis...
Output permanently saved to: /opt/gatemate/gatemate_ila/log/yosys.log

Execute Implementation...
Output permanently saved to: /opt/gatemate/gatemate_ila/log/impl.log

################# Configuration File ##################
#                                                     #
# save_config/ila_config_blink_25-01-18_07-19-48.json #
#                                                     #
#######################################################

Error configuring the device
Traceback (most recent call last):
  File "/opt/gatemate/gatemate_ila/app/ILAConfig.py", line 1721, in upload
    dev.set_configuration()
  File "/usr/lib/python3/dist-packages/usb/core.py", line 915, in set_configuration
    self._ctx.managed_set_configuration(self, configuration)
  File "/usr/lib/python3/dist-packages/usb/core.py", line 113, in wrapper
    return f(self, *args, **kwargs)
  File "/usr/lib/python3/dist-packages/usb/core.py", line 158, in managed_set_configuration
    self.managed_open()
  File "/usr/lib/python3/dist-packages/usb/core.py", line 113, in wrapper
    return f(self, *args, **kwargs)
  File "/usr/lib/python3/dist-packages/usb/core.py", line 131, in managed_open
    self.handle = self.backend.open_device(self.dev)
  File "/usr/lib/python3/dist-packages/usb/backend/libusb1.py", line 804, in open_device
    return _DeviceHandle(dev)
  File "/usr/lib/python3/dist-packages/usb/backend/libusb1.py", line 652, in __init__
    _check(_lib.libusb_open(self.devid, byref(self.handle)))
  File "/usr/lib/python3/dist-packages/usb/backend/libusb1.py", line 604, in _check
    raise USBError(_strerror(ret), ret, _libusb_errno[ret])
usb.core.USBError: [Errno 13] Access denied (insufficient permissions)

During handling of the above exception, another exception occurred:

Traceback (most recent call last):
  File "/opt/gatemate/gatemate_ila/app/ILAcop.py", line 277, in <module>
    if not ILA_config_instance.upload():
  File "/opt/gatemate/gatemate_ila/app/ILAConfig.py", line 1726, in upload
    if dev.is_kernel_driver_active(intf.bInterfaceNumber):
  File "/usr/lib/python3/dist-packages/usb/core.py", line 1107, in is_kernel_driver_active
    self._ctx.managed_open()
  File "/usr/lib/python3/dist-packages/usb/core.py", line 113, in wrapper
    return f(self, *args, **kwargs)
  File "/usr/lib/python3/dist-packages/usb/core.py", line 131, in managed_open
    self.handle = self.backend.open_device(self.dev)
  File "/usr/lib/python3/dist-packages/usb/backend/libusb1.py", line 804, in open_device
    return _DeviceHandle(dev)
  File "/usr/lib/python3/dist-packages/usb/backend/libusb1.py", line 652, in __init__
    _check(_lib.libusb_open(self.devid, byref(self.handle)))
  File "/usr/lib/python3/dist-packages/usb/backend/libusb1.py", line 604, in _check
    raise USBError(_strerror(ret), ret, _libusb_errno[ret])
usb.core.USBError: [Errno 13] Access denied (insufficient permissions)

Il semble que je n’ai pas configuré correctement l’usb. Il faut ajouter une règle udev pour le DirtyJtag :

$ lsusb
...
Bus 001 Device 043: ID 1209:c0ca Generic Jean THOMAS DirtyJTAG
...

On ouvre ajoute un fichier de règle :

sudo vim /etc/udev/rules.d/99-usb-serial.rules

SUBSYSTEM=="usb", ATTRS{idVendor}=="1209", ATTRS{idProduct}=="c0ca", MODE="0666"

Puis on recharge udev:

sudo udevadm control --reload-rules
sudo udevadm trigger

Le câble est maintenant accessible sans être en sudo :

openFPGALoader --cable dirtyJtag --detect
Jtag frequency : requested 6000000Hz -> real 6000000Hz
index 0:
	idcode 0x20000001
	manufacturer colognechip
	family GateMate Series
	model  GM1Ax
	irlength 6

On va plus loin cette fois, mais il ne trouve pas la carte :

...
################# Configuration File ##################
#                                                     #
# save_config/ila_config_blink_25-01-18_07-47-38.json #
#                                                     #
#######################################################


Upload to FPGA Board...
Execute openFPGALoader command:
openFPGALoader  -b olimex_gatemateevb /opt/gatemate/gatemate_ila/p_r_out/ila_top_25-01-18_07-47-38_00.cfg


Error: 

Error: cannot find board 'olimex_gatemateevb'

Il semble que ma version d’openFPGALoader soit trop vieille, et une sombre histoire de libstdc++ m’empêche d’utiliser correctement le binaire fourni par CologneChip.

Mettons donc à jour les sources officielles:

$ cd /opt/openFPGALoader
$ git pull -r
$ rm -rf build
$ mkdir build
$ cd build/
$ cmake ..
$ cmake --build .
$ make -j10
$ sudo make install

Cette fois le fichier de dump vcd est créé et une fenête gtkwave s’ouvre sur les chronogrammes saisis !

$ python3 ILAcop.py start

#################################################################################################
#                   Cologne Chip GateMate ILA control program (ILAcop)                          #
# ********************************************************************************************* #
#    Copyright (C) 2023 Cologne Chip AG <support@colognechip.com>                               #
#    Developed by Dave Fohrn                                                                    #
#                                                                                               #
#    This program is free software: you can redistribute it and/or modify                       #
#    it under the terms of the GNU General Public License as published by                       #
#    the Free Software Foundation, either version 3 of the License, or                          #
#    (at your option) any later version.                                                        #
#                                                                                               #
#    This program is distributed in the hope that it will be useful,                            #
#    but WITHOUT ANY WARRANTY; without even the implied warranty of                             #
#    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the                              #
#    GNU General Public License for more details.                                               #
#                                                                                               #
#    You should have received a copy of the GNU General Public License                          #
#    along with this program.  If not, see <https://www.gnu.org/licenses/>.                     #
#                                                                                               #
# ********************************************************************************************* #
#################################################################################################


Upload to FPGA Board...
writeTDI: read: usb bulk read failed [Errno 110] Operation timed out

############ CONFIGURATION NOTE #############
#                                           #
# Trigger at sample no.: 10                 #
# Defined analysis frequency: 25000000 Hz   #
#                                           #
#############################################


--- All Signals ----
+----+-------------+
|  # |        Name |
+----+-------------+
|  0 |      led[0] |
|  1 |      led[1] |
|  2 |      led[2] |
|  3 |      led[3] |
|  4 |      led[4] |
|  5 |      led[5] |
|  6 |      led[6] |
|  7 |      led[7] |
|  8 |  counter[0] |
|  9 |  counter[1] |
| 10 |  counter[2] |
| 11 |  counter[3] |
| 12 |  counter[4] |
| 13 |  counter[5] |
| 14 |  counter[6] |
| 15 |  counter[7] |
| 16 |  counter[8] |
| 17 |  counter[9] |
| 18 | counter[10] |
| 19 | counter[11] |
| 20 | counter[12] |
| 21 | counter[13] |
| 22 | counter[14] |
| 23 | counter[15] |
| 24 | counter[16] |
| 25 | counter[17] |
| 26 | counter[18] |
| 27 | counter[19] |
| 28 | counter[20] |
| 29 | counter[21] |
| 30 | counter[22] |
| 31 | counter[23] |
| 32 | counter[24] |
| 33 |    LED_ctrl |
+----+-------------+

##### current ILA runtime configuration #####
#                                           #
# Number of sequences: 1                    #
#                                           #
#  Sequences Number: 1                      #
#     trigger activation: falling edge      #
#     trigger signal:     led[0]            #
#                                           #
#############################################



0 -- exit
1 -- change Trigger
2 -- start capture
3 -- reset ILA (resets the config of the ILA)

Enter your choice: 2

################# start Capture #################
#                                               #
# Waiting for device. Press Enter to interrupt. #
#                                               #
#################################################


############### Duration between captures ##############
#                                                      #
# Duration between start and first trigger: 0.009355 s #
#                                                      #
########################################################


############### create vcd file ###############
#                                             #
# vcd_files/ila_blink_25-01-18_20-54-13_0.vcd #
#                                             #
###############################################


Press Enter to continue

Pas mal !

Par contre plus aucune LED ne s’allument hormis la rouge de l’alimentation. Il est possible que le fichier de contraintes ne soit par correct. Le fichier se trouve dans le répertoire source de l’exemple :

Fichier /opt/gatemate/gatemate_ila/example_dut/blink/src/blink.ccf:

Pin_in   "clk"  Loc = "IO_SB_A8" | SCHMITT_TRIGGER=true;
Pin_out  "led[0]"                   Loc = "IO_EB_B1";                           # LED D1
Pin_out  "led[1]"                   Loc = "IO_EB_B2";                           # LED D2
Pin_out  "led[2]"                   Loc = "IO_EB_B3";                           # LED D1
Pin_out  "led[3]"                   Loc = "IO_EB_B4";                           # LED D2
Pin_out  "led[4]"                   Loc = "IO_EB_B5";                           # LED D1
Pin_out  "led[5]"                   Loc = "IO_EB_B6";                           # LED D2
Pin_out  "led[6]"                   Loc = "IO_EB_B7";                           # LED D1
Pin_out  "led[7]"                   Loc = "IO_EB_B8";                           # LED D2

Si l’on regarde la carte, les IO_EB_Bx se trouvent sur le bank BANK_EB1 qui ne sont pas connecté à des leds.

Notez que pour ouvrir les schéma de la carte, kicad 6 ne suffit pas. Sur Linux Mint on peut installer kicad 8 avec les commandes suivante:

$  sudo add-apt-repository ppa:kicad/kicad-8.0-releases
$  sudo apt update
$  sudo apt install kicad

La led sur le schema:

Se trouve sur le port IO_SB_B6 du gatemate:

Il faut donc en changer pour le pin de la LED FPGA_LED1 histoire d’en voir au moins une clignoter.

Pin_in   "clk"  Loc = "IO_SB_A8" | SCHMITT_TRIGGER=true;
Pin_out  "led[0]"                   Loc = "IO_EB_B1";                           # LED D1
Pin_out  "led[1]"                   Loc = "IO_EB_B2";                           # LED D2
Pin_out  "led[2]"                   Loc = "IO_EB_B3";                           # LED D1
Pin_out  "led[3]"                   Loc = "IO_EB_B4";                           # LED D2
#Pin_out  "led[4]"                   Loc = "IO_EB_B5";                           # LED D1
Pin_out  "led[4]"                   Loc = "IO_SB_B6";                           # FPGA_LED1 
Pin_out  "led[5]"                   Loc = "IO_EB_B6";                           # LED D2
Pin_out  "led[6]"                   Loc = "IO_EB_B7";                           # LED D1
Pin_out  "led[7]"                   Loc = "IO_EB_B8";                           # LED D2 

On modifie le fichier ccf puis on clean le projet pour le relancer:

$ python3 ILAcop.py  --clean
$ python3 ILAcop.py config -vlog ../example_dut/blink/src/ -t blink
$ python3 ILAcop.py start

Bon pour le coup j’ai été un peu vite et j’ai du relancer toute la config. Mais on peut faire un clean sans avoir à tout reconfigurer et relancer la dernière configuration:

$ python3 ILAcop.py reconfig -l save_config/ila_config_blink_25-01-18_21-31-30.json

Cette fois la LED verte du FPGA_LED1 clignote.

It’s blinking \o/

CLEAR: Le FPGA libéré

On en parlait il y a deux ans sur ce blog, mais ça y est, c’est arrivé !

Ce soir dans ma boite aux lettres j’ai eu le plaisir d’y découvrir un kit de développement du premier eFPGA opensource : le CLEAR.

Voici le kit de développement du CLEAR fraîchement sortie de son emballage

Attention, on ne parle pas ici d’un simple FPGA qui aurait sa chaîne de développement libérée comme nous commençons à en trouver quelques un sur le marché aujourd’hui.

Non, ici c’est le silicium même qui a été conçu au moyen d’outils open sources. La gravure même des transistors.

Alors certes, il ne possède que 64 CLB et est gravé en 130 nm. Mais cela valide tout de même le sérieux du projet openFPGA.

Maintenant que le FPGA est libéré, une question se pose à propos de ce blog : Doit il continuer ? 😉

Traitement numérique du signal, prise de notes

On dit souvent que pour bien apprendre un sujet en informatique il faut écrire une doc. Pour des besoins pro j’ai du me re-mettre au traitement numérique du signal. Je commence en général par un bouquin et un projet. Pour le projet comme c’est du pro je c’est à ma discrétion, par contre pour le bouquin je me suis plongé dans le livre de Richard G.Lyons «Understanding digital signal processing» qui a le mérite d’être richement illustré de graphes et d’équations avec beaucoup d’explications visuelles et «avec les mains».

Du bon soleil pour apprendre

L’idée de cette note est donc de faire des exercices en rapport avec ce qui est dans ce livre mais pas que, le tout de manière pratique en python et de voir les implications que ça peut avoir avec les FPGA.

Un signal discret

Dans un premier temps nous aurons besoin de numpy et pylab en python3

import numpy as np
import pylab as plt

Le signal de base est une sinusoïde. Pour représenter un signal de 1 Hertz en python on va d’abord créer un tableau d’un certain nombre de valeur de 0 à 1 secondes :

# Freq
f0 = 1
# 40 points de 0 à 39
t = np.linspace(0, 1, 40)

Puis calculer le sinus

y = np.sin(2*math.pi*f0*t)

Signal qu’il est facile de «plotter» ensuite :

plt.plot(t, y)
plt.show()

Ce qui nous donne cette belle courbe de sinus :

D’après Richard il ne faut pas relier les points

Mais pour bien se représenter un signal numérique il ne faut pas relier les points. Il vaut mieux mettre des points avec des lignes verticales comme ceci :

fix, ax = plt.subplots()
ax.stem(t, y, 'b', markerfmt="b.")
plt.show()

Ce qui nous donne la figure suivante :

Les points non reliés entre eux donnent une meilleur vision d’un signal numérique (discret)

Cette dernière figure illustre bien la notion d’échantillonnage avec une fréquence d’échantillonnage fs de 40Hertz (temps en secondes et 40 points) soit :

# Freq
f0 = 1
# Temps total
T = 1
# Nombre de points:
N = 40
# Fréquence d’échantillonnage :
print(f"fs = {N/T} Hertz")
# fs = 40.0 Hertz

Ici, la fréquence d’échantillonnage (40Hertz) est largement supérieur à la fréquence du signal enregistré (1 Hertz). On peut s’amuser maintenant à monter la fréquence du signal à la fréquence de Nyquist :

f0 = 5
f0 = 10
f0 = 20

Ce que nous dit Nyquist, c’est qu’avec tous les signaux ci-dessus, il est possible de retrouver la sinusoïde du début. Mais si on augmente encore la fréquence on obtient un repliement du spectre.

f0 = 30
f0 = 40

On peut ajouter le l’analyse de spectre en augmentant également le nombre de points mesuré :

# Freq
f0 = 1
# Temps total
T = 1
# Nombre de points:
N = 100
# 100 points de 0 à 99
t = np.linspace(0, T, N)
y = np.sin(2*np.pi*f0*t)
fix, ax = plt.subplots(1,2)
ax[0].stem(t, y, 'b', markerfmt="b.")
ax[1].magnitude_spectrum(y, Fs=N/T, ds="steps-mid")
plt.show()
(f0=1) À gauche le signal sur 100 points, à droite l’analyse des fréquences

Ondelettes

Pour faire une ondelette (wavelet) on multiplie un cosinus (périodique) avec une gaussienne (exp(-t²/2)) :

# Décalage en seconde:
retard = 5
y = np.cos(2*np.pi*f0*t)*np.exp(-np.power(t-retard,2)/2)
La fréquence du signal est toujours de 1Hz mais le signal n’est plus périodique à l’infini

La vidéo suivante explique tout ce que vous avez toujours voulu savoir sur les ondelettes.

Si on change la fréquence du signal, en passant à 2Hz par exemple. On se rend compte que l’échantillonnage tronque les maximum locaux :

La courbe n’est plus symétrique

Ce qui casse la symétrie de la courbe.

Hilbert avec scipy

La transformée de hilbert permet de calculer la partie imaginaire du signal réel. Le package python nommé scipy inclue la fonction qui la calcule.

[...]
from scipy import signal
[...]
# morlet wavelet
y = np.cos(2*np.pi*f0*t)*np.exp(-np.power(t-retard,2)/2)

himg = signal.hilbert(y).imag
hreal = signal.hilbert(y).real
[...]
On distingue bien la partie réel (le signal d’origine) et la partie imaginaire en orange

Comme nous avons la partie réelle et la partie imaginaire de notre signal, il est possible désormais de calculer son module pour en tirer l’enveloppe:

# morlet wavelet
y = np.cos(2*np.pi*f0*t)*np.exp(-np.power(t-retard,2)/2)

himg = signal.hilbert(y).imag
hreal = signal.hilbert(y).real

habs = np.sqrt(np.power(himg, 2) + np.power(hreal, 2))
L’enveloppe du signal en vert (habs)

Si l’on diminue la fréquence d’échantillonnage (division par 10) on remarque que l’enveloppe ne passe plus par les maximums. La transformée de Hilbert semble les avoirs tout de même déduit :

Avec une fréquence de pulsation de 2 Hertz et une division de la fréquence échantillonnage par 10 on constate que l’enveloppe demeure alors que les maximums réel et imaginaire sont tronqués.

Peut-on calculer l’enveloppe sans racine carré et carré ?

habs = np.abs(himg) + np.abs(hreal)
Hmmm ça marche moins bien sans carré (courbe rouge)

Et juste avec des carrés ?

hsquare= np.power(himg, 2) + np.power(hreal, 2)
Hmm c’est pas magique non plus

Transformée de Fourrier

Mais que faites vous encore sur ce blog ! Vite allez visionner l’excellente vidéo de 3Blue1Brown qui parle de la transformée de Fourrier avec force de graphes et de dessins. Vous ne verrez plus la transformée de fourrier comme avant 😉

Sinon y a aussi cette formule trouvée sur twitter qui est vraiment très parlante :

Trouvée sur twitter

Jusqu’à présent, pour calculer et afficher la transformée de fourrier de notre signal, nous nous sommes servi exclusivement de la fonction magnitude_spectrum() inclue dans pylab. C’est intéressant pour avoir un aperçu du spectre, mais ça ne permet pas de dire que l’on maîtrise ça.

Nombre complexes en python

Python permet visiblement d’utiliser nativement des nombres complexes avec ‘j’ à condition d’y mettre un nombre devant :

In [2]: j                                                                                                                                                                                                          
---------------------------------------------------------------------------
NameError                                 Traceback (most recent call last)
<ipython-input-2-3eedd8854d1e> in <module>
----> 1 j

NameError: name 'j' is not defined

In [3]: 1j                                                                                                                                                                                                         
Out[3]: 1j

In [4]: 0.02j                                                                                                                                                                                                      
Out[4]: 0.02j

In [5]: -3j                                                                                                                                                                                                        
Out[5]: (-0-3j)

In [7]: np.exp(1j)                                                                                                                                                                                                 
Out[7]: (0.5403023058681398+0.8414709848078965j)

Tentons donc de calculer la transformée de fourrier en mode «brute de force» pour voir:


#temps: 0 points de 0 à N-1
t = np.linspace(0, T, N)
# morlet wavelet
y = np.cos(2*np.pi*f0*t)*np.exp(-np.power(t-retard,2)/2)

# transformée de fourrier
freqs = np.array([])
for k in range(N):
    listexp = [y[n]*np.exp(1j*2*np.pi*k*n/N) for n in range(N)]
    xk = (1/N)*np.array(listexp).sum()
    freqs = np.append(freqs, xk)

#fréquence: 0 points de 0 à N-1
k = np.linspace(0, T, N)
Visiblement nous avons un problème «complexe»

Là ce que nous venons de calculer est la version complexe de la transformée de fourrier dont pylab nous «plot» la partie réelle. Voyons voir le module :

fourrier_module = np.sqrt(np.power(freqs.imag, 2) + np.power(freqs.real, 2))
Mais pourquoi ce deuxième pic ?

Nous avons donc toujours deux pics, sachant que le second pic est au delà de la fréquence de Nyquist (Fs=10Hz) et semble «normal».

Par contre nous avons un facteur 2 entre le calcul de magnitude de python et celui que l’on vient de calculer.

Peut-être parce que la formule de l’image est celle de la transformée inverse ? La transformée discrète donnée dans le livre est plutôt celle là :

Page 60: On retrouve encore moins le facteur 2 sur cette formule.

Voyons voir avec cette nouvelle formule :

# transformée de fourrier
freqs = np.array([])
for k in range(N):
    listexp = [y[n]*np.exp(-1j*2*np.pi*k*n/N) for n in range(N)]
    xk = np.array(listexp).sum()
    freqs = np.append(freqs, xk)

fourrier_module = np.sqrt(np.power(freqs.imag, 2) + np.power(freqs.real, 2))
Bon c’est pas un facteur 2 cette fois!

Si on veut «matcher» la courbe de magnitude il faut ajouter un facteur 2/N au calcul du module :

fourrier_module = (2/N)*np.sqrt(np.power(freqs.imag, 2) + np.power(freqs.real, 2))
Avec le facteur (2/N) on obtient une superposition des courbes.

cos – sin

Pour faire entrer le calcul de la transformée dans un FPGA, l’exponentielle d’un complexe n’est pas super pratique. Décomposons donc en différence cos-sin avec la formule d’Euler, on devrait obtenir le même résultat:

# transformée de fourrier
freqs = np.array([])
for k in range(N):
    listexp = []
    for n in range(N):
        angle = 2*np.pi*k*n/N
        listexp.append(y[n]*(np.cos(angle) - 1j*np.sin(angle)))
    xk = np.array(listexp).sum()
    freqs = np.append(freqs, xk)

Et nous obtenons exactement le même graphe qu’avant.

C’est sans surprise qu’on obtient la même chose en sommant indépendamment partie réelle et partie imaginaire :

# transformée de fourrier
freqs_real = np.array([])
freqs_img = np.array([])
for k in range(N):
    listreal = []
    listimg = []
    for n in range(N):
        angle = 2*np.pi*k*n/N
        listreal.append(y[n]*(np.cos(angle)))
        listimg.append(y[n]*(-np.sin(angle)))
    xkreal = np.array(listreal).sum()
    xkimg = np.array(listimg).sum()
    freqs_real = np.append(freqs_real, xkreal)
    freqs_img = np.append(freqs_img, xkimg)

fourrier_module = (2/N)*np.sqrt(np.power(freqs_img, 2) + np.power(freqs_real, 2))

Nous permettant au passage de dégager le ‘j’ des nombres complexes qui ne passe pas très bien dans un FPGA.

Des entiers ou des virgules fixes

Pour le moment c’était facile: on avait les flottant de python. Seulement voilà, dans un FPGA, les flottants ne sont pas simple. Nous avons besoin de fixer la taille (en bits) des variables/registres utilisés. Il faut également fixer la position de la virgule si l’on souhaite simplifier le calcul.

Le second problème nous vient des fonctions sin() et cos() qui ne sont pas calculables simplement. L’astuce consiste à pré-calculer les valeurs et les stocker dans une table qui ira remplir une ROM du FPGA.

Pour gérer des entiers en virgule fixe et de taille hétérogène on installera le module fxpmath :

$ git clone https://github.com/francof2a/fxpmath.git
$ cd fxpmath/
$ python -m pip install -e .

Pour commencer on va passer le signal ‘y’ en entier signé sur 16bits avec

# morlet wavelet
y = np.cos(2*np.pi*f0*t)*np.exp(-np.power(t-retard,2)/2)

YTYPE="S1.15"
ysint = Fxp(y, dtype=YTYPE)

Le signal se trouvant entre -1 et 1 nous choisirons un format signé sur 16 bits avec tous les chiffres derrière la virgule ‘S1.15’.

Pour les calculs intermédiaires on va rester sur du signé 16 bits mais avec la virgule au milieu cette fois, soit ‘S8.8’:

# transformée de fourrier
freqs_real = np.array([])
freqs_img = np.array([])
for k in range(N):
    listreal = []
    listimg = []
    for n in range(N):
        angle = Fxp(2*fixpi*Fxp(k, dtype=DTYPE)*Fxp(n, dtype=DTYPE)/N,
                dtype=DTYPE)
        listreal.append(Fxp(y[n]*( np.cos(angle)), dtype=DTYPE))
        listimg.append (Fxp(y[n]*(-np.sin(angle)), dtype=DTYPE))
    xkreal = Fxp(np.array(listreal).sum(), dtype=DTYPE)
    xkimg  = Fxp(np.array(listimg ).sum(), dtype=DTYPE)
    print(f"Freq {k} -> {xkreal}({xkreal.dtype}) + {xkimg}j ({xkimg.dtype})")
    freqs_real = np.append(freqs_real, xkreal)
    freqs_img = np.append(freqs_img, xkimg)

#fourrier_module = (2/N)*np.sqrt(np.power(freqs_img, 2) + np.power(freqs_real, 2))
fourrier_power = Fxp(Fxp(freqs_img*freqs_img, dtype=DTYPE) + Fxp(freqs_real*freqs_real, dtype=DTYPE), dtype=DTYPE)
fourrier_module = Fxp((2/N)*Fxp(np.sqrt(fourrier_power), dtype=DTYPE), dtype=DTYPE)

La première surprise de cette méthode est le temps de calcul: on passe d’un calcul de la transformée quasi instantanée à un calcul qui prend presque une minute.

La seconde surprise vient avec le «bruit haute fréquence» qui apparaît dans le résultat et le second pic qui disparaît.

Avec les calculs intermédiaire en S8.8 on parvient à la même courbe, modulo le bruit en haute fréquences.

Le problème de ce bruit vient de l’arrondi calculé sur Pi, si on ajuste la virgule de Pi comme ceci :

# Frequence du signal
Sf0 = 2

f0 = (Sf0 * Fs)/T

# Décalage en seconde:
retard = 5

#temps: 0 points de 0 à N-1
t = np.linspace(0, T, N)
# morlet wavelet
y = np.cos(2*np.pi*f0*t)*np.exp(-np.power(t-retard,2)/2)

YTYPE="S1.15"
ysint = Fxp(y, dtype=YTYPE)

DTYPE="S8.8"
D2TYPE="S16.16"

fixpi = Fxp(np.pi, dtype="U3.13")

# transformée de fourrier
freqs_real = np.array([])
freqs_img = np.array([])
for k in range(N):
    listreal = []
    listimg = []
    for n in range(N):
        angle = Fxp(2*fixpi*Fxp(k*n, dtype="U16.0")/N, dtype=D2TYPE)
        listreal.append(Fxp(y[n], dtype=YTYPE)*Fxp( np.cos(angle), dtype=YTYPE))
        listimg.append (Fxp(y[n], dtype=YTYPE)*Fxp(-np.sin(angle), dtype=YTYPE))
    xkreal = Fxp(np.array(listreal).sum(), dtype=DTYPE)
    xkimg  = Fxp(np.array(listimg ).sum(), dtype=DTYPE)
    print(f"Freq {k}/{Fs} ({k*T/N}) -> {np.sqrt(xkreal*xkreal + xkimg*xkimg)})")
    freqs_real = np.append(freqs_real, xkreal)
    freqs_img = np.append(freqs_img, xkimg)

fourrier_power = Fxp(Fxp(freqs_img*freqs_img, dtype=DTYPE) + Fxp(freqs_real*freqs_real, dtype=DTYPE), dtype=DTYPE)
fourrier_module = Fxp((2/N)*Fxp(np.sqrt(fourrier_power), dtype=DTYPE), dtype=DTYPE)

Avec 128 échantillons (2^7)

Par contre on a un décalage de fréquence avec la fonction magnitude_spectrum() de pylab :

Mais d’où vient ce décalage ?

Ce décalage provient de l’axe des x qui n’est pas le même pour le calcul de python et le calcul maison. En effet, notre calcul «à la main» s’étend sur tout l’espace «de nyquist» (0 à N-1) alors que la fonction magnitude_spectrum() n’affiche le spectre que sur la moitée.

Pour recentrer tout ça on peut simplement récupérer la table des fréquences fournie par magnitude_spectrum() et l’utiliser comme axe des x dans l’affichage de notre spectre :

#...
magnitude, freqs, _ = ax[1].magnitude_spectrum(y, Fs=N/T, ds="steps-mid", label=f"fft (Fs={Fs} Hz)")
#...
ax[1].plot(freqs, fourrier_module[:len(freqs)], label = "freqs (calculée)")
La fréquence est maintenant correcte, reste un problème de magnitude. Problème d’arrondi ?

Et nous obtenons la bonne fréquence pour les deux modes de calculs. Reste maintenant un problème de magnitude maximum, est-ce un problème d’arrondi de la virgule fixe ? Possible.

Ressources

Le code de cet article se trouve sur le dépôt github suivant.

Déballage de la TangNano9k

La boite de la Tang Nano 9K

Ça y est, le colis contenant la carte de développement Tang Nano 9k est arrivé. Contrairement à la TangNano4k je ne l’ai pas reçue rapidement, il a fallu attendre environ deux mois. Mais elle est bien là maintenant.

La TangNano9k sortie de sa boite

La petite carte, muni d’un littlebee GW1NR-LV9 est tout de même plus grande que les petites sœur. Cette carte est plus «grande» que la 4k mais elle possède autant de PSRAM (64Mbits) et ne possède pas de cœur ARM. La carte possède cependant plus de LUT.

Tang Nano 9K, Tang Nano 4k et Tang Nano

La fiche descriptive d’aliexpress propose un tableau comparatif des trois cartes disponible pour environ $15.

Table de comparaison des différentes TangNano proposé sur aliexpress

Au branchement, les deux UART du BL702 apparaissent :

[1874406.879770] usb 1-1.1.3: new full-speed USB device number 96 using xhci_hcd
[1874406.980305] usb 1-1.1.3: not running at top speed; connect to a high speed hub
[1874406.981788] usb 1-1.1.3: New USB device found, idVendor=0403, idProduct=6010, bcdDevice= 5.00
[1874406.981794] usb 1-1.1.3: New USB device strings: Mfr=1, Product=2, SerialNumber=3
[1874406.981797] usb 1-1.1.3: Product: JTAG Debugger
[1874406.981800] usb 1-1.1.3: Manufacturer: SIPEED
[1874406.981803] usb 1-1.1.3: SerialNumber: FactoryAIOT Pro
[1874406.991536] ftdi_sio 1-1.1.3:1.0: FTDI USB Serial Device converter detected
[1874406.991586] usb 1-1.1.3: Detected FT2232C
[1874406.992995] usb 1-1.1.3: FTDI USB Serial Device converter now attached to ttyUSB0
[1874406.993170] ftdi_sio 1-1.1.3:1.1: FTDI USB Serial Device converter detected
[1874406.993213] usb 1-1.1.3: Detected FT2232C
[1874406.993538] usb 1-1.1.3: FTDI USB Serial Device converter now attached to ttyUSB1

Et les 6 LED rouges clignotent en même temps.

Pinout de la TangNano9k trouvé sur la fiche aliexpress

Le part number du FPGA est GW1NR-LV9QN88PC6/I5 et peut être sélectionné pour la synthèse et le placement-routage avec le logiciel de gowin version 1.9.8.03.

[Article à éditer]

Ressources:

Sortie de la version 0.6.0 du configurateur de FPGA openFPGALoader

[Dépêche parue initialement sur LinuxFR]

openFPGALoader est un utilitaire en ligne de commande, écrit en C++ et sous licence Apache 2.0. Il permet de configurer des FPGA de toutes marques. L’objectif du projet est de pouvoir prendre en charge tous les FPGA du marché ainsi que tous les adaptateurs et sondes de configuration.

Sommaire

La chaîne de développement sur FPGA

Pour développer sur un FPGA, nous avons besoin d’un ensemble de logiciels et de formats spécifiques. La chaîne de développement sur FPGA peut se résumer par la figure ci-dessous:

Chaîne de développement FPGA

L’architecture du composant est décrite dans un langage de description (HDL pour Hardware Description Language) matériel. Cette description est convertie en un schéma électronique (Netlist) par un procédé appelé «synthèse». Les composants de la Netlist sont ensuite placés dans la matrice du FPGA (placement) puis connectés ensemble (Routage) pour former le composant décrit au début.
Toutes ces informations sont ensuite décrites dans un fichier de configuration appelé bitstream (propriétaire). Et enfin, le fichier est transféré au FPGA pour le configurer.

À l’origine, toutes ces opérations sont réalisées par des logiciels privateurs, et les formats sont verrouillés. Quand on parle de libération des FPGA on aimerait bien sûr que toute la chaîne puisse être réalisée avec des logiciels libres et des formats ouverts. Mais le point le plus bloquant évoqué est souvent le format du bitstream, qui est LE maillon le plus critique de la chaîne jalousement gardé secret par (presque) tous.

Toutes ces étapes ont désormais des projets open source qui sont suffisamment avancés pour pouvoir développer sur FPGA librement. À condition de bien choisir le modèle.

Chargement du bitstream ou « configuration »

On en oublie souvent la dernière étape consistant à télécharger le bitstream dans le FPGA. Pourtant cette étape est également dépendante du constructeur qui propose l’adaptateur à vil prix (souvent une sonde USB-JTAG). Et le logiciel est en général intégré au lourd IDE du constructeur (binaire x86) qu’il n’est pas toujours facile de configurer sur sa distribution Linux et encore moins sur une architecture exotique (raspberryPi, Risc-V, …).

Le dessein d’openFPGALoader est donc d’être « l’anneau pour les programmer tous ». Pour cela il faut :

Avec cette version 0.6.0, le logiciel peut être considéré comme mature. C’est tellement devenu une référence qu’il est même intégré dans quelques distributions Linux, dans buildroot. Le logiciel fonctionne également sur Mac et Windows avec cependant plus de problème du fait du passage par le pilote usb zadig.

C’est aujourd’hui un automatisme pour configurer un FPGA, de le tester d’abord avec openFPGALoader. Avant même d’utiliser l’outil constructeur. Le logiciel apporte un confort d’utilisation et de configuration qui n’a rien à envier aux autres.

Quelques exemples

Pour illustrer, un peu l’utilisation d’openFPGALoader, supposons que nous ayons notre bitstream permettant de faire clignoter la led de la carte Tang Nano 4K. L’avantage de cette carte est que l’adaptateur de programmation est inclus et que tout passe par le même port USB.
Une fois la carte branchée on peut commencer par détecter le FPGA avec --detect :

    $ openFPGALoader --detect
    write to ram
    Jtag frequency : requested 6.00MHz   -> real 6.00MHz  
    index 0:
        idcode 0x100981b
        manufacturer Gowin
        family GW1NSR
        model  GW1NSR-4C
        irlength 8

Le format de bitstream pour les gowin possède l’extension fs, on peut le configurer directement en donnant simplement le nom du fichier en argument :

    $ openFPGALoader led_test/project/impl/pnr/led_test.fs
    write to ram
    Jtag frequency : requested 6.00MHz   -> real 6.00MHz  
    Parse file Parse led_test/project/impl/pnr/led_test.fs: 
    Done
    DONE
    Jtag frequency : requested 2.50MHz   -> real 2.00MHz  
    erase SRAM Done
    Flash SRAM: [==================================================] 100.00%
    Done
    SRAM Flash: Success

Et si le bitstream nous satisfait on l’écrira dans la mémoire « flash » avec l’option -f pour qu’il se reconfigure à chaque allumage.

    $ openFPGALoader ide/gbhdmi/impl/pnr/gbhdmi.fs -f
    write to flash
    Jtag frequency : requested 6.00MHz   -> real 6.00MHz  
    Parse file Parse ide/gbhdmi/impl/pnr/gbhdmi.fs: 
    Done
    DONE
    Jtag frequency : requested 2.50MHz   -> real 2.00MHz  
    erase SRAM Done
    erase Flash Done
    write Flash: [==================================================] 100.00%
    Done
    CRC check: Success

Les fichiers de configuration à télécharger dans le FPGA peuvent être assez volumineux pour certain gros FPGA. Les sondes de configuration n’étant pas toujours très rapide, il est intéressant de pouvoir envoyer le bitstream compressé.
Cette option est bien sûr supporté par openFPGALoader.

Plutôt que de prendre le nom du fichier bitstream en argument, il est également possible de récupérer un «flux» sur l’entrée standard:

    cat /path/to/bitstream.ext | openFPGALoader --file-type ext [options]

Méthode très pratique si l’on souhaite configurer son FPGA via le réseau par exemple:

    # Carte connectée au FPGA
    nc -lp port | openFPGALoader --file-type xxx [option

    # Ordinateur distant
    nc -q 0 host port < /path/to/bitstream.ext

Et si vous trouvez que cette dépêche manque (scandaleusement) de TapTempo, sachez qu’openFPGALoader fonctionne très bien sur le FPGA ECP5 présent sur la carte colorlight pour y configurer le bitstream TapTempo en VHDL :

    $ openFPGALoader taptempo.bit 
    Open file taptempo.bit DONE
    Parse file DONE
    Enable configuration: DONE
    SRAM erase: DONE
    Loading: [==================================================] 100.000000%
    Done
    Disable configuration: DONE

Pour conclure

À l’heure où cette dépêche est mise sous presse, openFPGALoader a sorti une version mineure 0.6.1 (principalement pour réduire le nombre d’« assets » dans l’archive.

openFPGALoader est maintenant bien installé dans la constellation des logiciels libres pour développer sur FPGA. Même s’il n’a pas encore atteint la version 1.0, il est désormais tout à fait mature pour une utilisation en « production ». Il méritait bien une dépêche sur LinuxFR.

Aller plus loin

Sortie de GHDL version 1.0.0

[Dépêche initialement parue sur LinuxFR]

GHDL est un logiciel écrit en ADA permettant de faire l’analyse, la compilation, la simulation ainsi que la synthèse du VHDL. Le VHDL, quant à lui, est un langage de description matériel très utilisé dans le développement sur FPGA ou ASIC. À l’origine, GHDL est un « side-project » de Tristan Gingold lui permettant de se faire la main avec ADA.

GHDL est devenu l’outil indispensable pour faire de la simulation VHDL aujourd’hui. Après presque 20 ans de développement, voici que sort en version 1.0.0 le logiciel de simulation VHDL nommé GHDL. En prime, GHDL s’offre un nouveau logo:

logo GHDL

Peu d’informations ont filtré sur cette sortie pour le moment. On peut soupçonner que ce soit une sortie anniversaire pour marquer les 20 ans du logiciel. Il n’en reste pas moins que GHDL est devenu un maillon indispensable dans l’écosystème opensource du monde FPGA et des ASIC.

Cette version 1.0.0 supporte désormais complètement les standards 1987, 1993 et 2002 du langage défini par l’IEEE. Le support de VHDL 2008 est noté comme partiel pour le moment.

Depuis quelques années, le développement du projet s’est accéléré et supporte de mieux en mieux les projets tierces comme Yosys bien sûr mais également CocoTB pour les testbenchs écrit en Python ainsi que les standards de vérification comme UVVM, OSVVM, VUnit issues du standard d’accelera.

Un support partiel d’un langage de PSL (Properties Specification Language) est également inclus. Il permet de décrire les propriétés du système pour faire de la vérification formelle.

Et surtout, il est possible de faire de la Synthèse. Certes, l’extension ghdl-yosys-plugin est encore en développement, mais l’exemple TapTempo l’a montré : Il est tout à fait possible de faire de la synthèse VHDL avec.

Cette version mature est surtout une occasion de mettre en valeur cet outil indispensable dans le monde du développement numérique (gateware).

Aller plus loin

Portage de TapTempo en VHDL

[Dépêche publiée initialement sur LinuxFr]

Ayant préparé tout le matériel pour faire du TapTempo en Verilog, il était trop tentant de réaliser la même chose en VHDL. L’occasion de se plonger dans ce langage de description matériel concurrent du Verilog.
L’occasion également de parler des avancées de GHDL, un simulateur libre du VHDL, et désormais également capable de faire la synthèse en conjonction avec Yosys.

Pour comprendre TapTempo dans la culture moulesque de LinuxFr.org, il est conseillé d’aller faire un petit tour sur la page wiki homonyme.

Sommaire

Comme l’indique le titre de cette dépêche, nous allons effectuer un « portage » du projet TapTempo, qui était écrit en Verilog, vers le language VHDL. Nous aurons donc l’avantage de profiter d’une architecture déjà conçue. Et comme une partie des stimulus de simulation (banc de test) ont été écrit en Python avec CocoTB, nous allons pouvoir les reprendre (presque) tels quels pour simuler notre version VHDL.

Vue d’ensemble du matériel pour TapTempo

Le VHDL

Le VHDL – pour VHSIC Hardware Description Language – est un langage de description matérielle issu d’une commande du ministère de la Défense américaine, c’est donc en toute logique qu’il soit surtout populaire… en Europe !

Le VHDL s’inspire fortement du langage ADA pour décrire le comportement des circuits intégrés numériques. Cela permet de décrire un modèle que l’on peut ensuite simuler au moyen d’un simulateur.
Le simulateur VHDL OpenSource le plus connu est GHDL, initialement développé par le français Tristan Gringold comme une surcouche à GCC.
Il existe également un simulateur opensource nommé nvc, mais il est moins mature que GHDL. Les autres simulateurs OpenSource, comme FreeHDL ou VerilatorVHDL, sont plus anecdotiques.

À partir d’une source VHDL il est également possible de générer un schéma de portes et de bascules logiques au moyen d’un logiciel de synthèse. Pendant longtemps, les seuls logiciels libres de synthèse HDL ciblaient le Verilog (OdinII et Yosys). Mais depuis cette année une extension GHDL est apparue pour Yosys. Si cette extension n’est pas encore vraiment stabilisée, elle n’en reste pas moins parfaitement utilisable comme nous allons le voir dans cet exemple.

Le VHDL est très hiérarchique, on décrit des modules avec leurs entrées-sorties que l’on assemble ensuite à la manière d’un schéma bloc dans un composant «top».

Dans le cas de TapTempo, l’interface du module «top» est déclarée dans une entité comme ceci :

entity taptempo is
    port (
        clk_i : in std_logic;
        btn_i : in std_logic;
        pwm_o : out std_logic
    );
end entity taptempo;

VHDL n’est pas sensible à la casse ! C’est quelque chose qui est très perturbant mais dans l’exemple ci-dessus Entity est exactement identique à entity ou ENTITY. En pratique, le simulateur mettra tout en minuscule.

Le module possède deux entrées : l’horloge (clk_i) et le bouton (btn_i) ainsi qu’une sortie pwm (pwm_o) pour l’affichage. Il est possible de définir des paramètres pour la génération du module en utilisant le mot clef generic de la même manière que le port. Pour plus de simplicité nous choisirons plutôt de mettre tous les paramètres dans un package séparé (nommé taptempo_pkg.vhd), à la manière des include du C/C++.

Le type std_logic est utilisé pour représenter les états que peut prendre un signal. En simulation, il peut prendre 9 valeurs différentes :
0 : 0 logique;
1 : 1 logique;
Z : Haute impédence;
X : inconnue, plusieurs pilotes en conflit -> erreur;
L : 0 faible, peut-être vu comme une résistance de tirage à la masse (pull-down);
H : 1 faible, peut-être vu comme une résistance de tirage à Vcc (pull-up);
W : signal faible inconnu, quand il y a un conflit entre L et H mais qu’un troisième pilote fort (0 ou 1) pourrait tout de même mettre le signal à une valeur déterminable;
- : pas d’importance;
U : non initialisé.

Dans la pratique on évitera d’utiliser toutes ces valeurs si on veut limiter les problèmes à la synthèse. Par exemple les valeurs ‘L’ et ‘H’ ne peuvent être synthétisées « à l’intérieur » d’un FPGA, les résistances de tirage ne sont disponibles que sur les entrées sorties.
Un bus de données bidirectionnel à l’extérieur du FPGA devra être séparé en deux bus dans le FPGA : un pour chaque direction.

En réalité on se limite à ‘0’ et ‘1’. Les valeurs ‘U’ et ‘X’ apparaissent dans les traces de simulation et nous avertissent d’un problème avec notre code (simulation ou synthèse).

Le type std_logic peut être étendu en tableau avec std_logic_vector très pratique pour représenter des bus.

Architecture de TapTempo

L’architecture global de TapTempo qui est exactement la même que celle de la version Verilog est présenté dans le schéma ci-dessous:

schema blocs taptempo

Le tempo en entrée est donné par une touche de morse

Schéma de la touche morse

qui peut aisément être remplacé par un simple bouton poussoir (c’est d’ailleurs monté en parallèle du bouton de la colorlight). Cette entrée est synchronisée avec l’horloge du FPGA au moyen d’une double bascule en série pour éviter la métastabilité :

    -- Synchronize btn
    btn_sync_p: process (clk_i, rst)
    begin
        if rst = '1' then
            btn_s <= '0';
            btn_old <= '0';
        elsif rising_edge(clk_i) then
            btn_s <= btn_old;
            btn_old <= btn_i;
        end if;
    end process btn_sync_p;

Le module debounce se charge ensuite de filtrer les rebonds et transfert le signal au module de mesure de la période d’appuis.

Cette période divise ensuite une constante de temps représentant le nombre de cycle de timepulse pour donner la fréquence en battement par minute (bpm).

Pour être « affichée », cette valeur est transformée en un signal périodique avec un rapport cyclique proportionnel au battement bpm.

Ce signal cyclique s’affiche ensuite très bien au moyen d’un voltmètre à aiguille comme celui-ci :

Photo du voltmètre à aiguille

La graduation qui nous intéresse ici est celle qui va de 0 à 250.

Les paramètres dans un package : taptempo_pkg

Pour configurer les différentes constantes et définir des fonctions utiles, on utilise un « package » nommé taptempo_pkg.vhd qui sera inclus dans tous les modules du projet :

use work.taptempo_pkg.all;

La déclaration d’un package en VHDL s’effectue en deux temps :

  • on déclare d’abord les «objets» que l’on va utiliser dans le package :
-- La déclaration de ce que le package rend visible
package taptempo_pkg is

    constant CLK_PER_NS : natural;
    constant BPM_MAX : natural;
    constant BPM_SIZE : natural;
    constant TP_CYCLE : natural;
    constant BTN_PER_MAX : natural;
    constant BTN_PER_SIZE : natural;
    constant BTN_PER_MIN : natural;
    constant MIN_US : natural;
    constant MAX_COUNT : natural;
    constant DEBOUNCE_PER_US: natural;
    constant DEB_MAX_COUNT : natural;
    constant DEB_MAX_COUNT_SIZE : natural;

    constant ZEROS : std_logic_vector(31 downto 0);

    -- Usefull function for register size
    function log2ceil(m : integer) return integer;

end package taptempo_pkg;
  • Puis on définit leurs valeurs et comportement dans le corps (body) :
package body taptempo_pkg is

    constant CLK_PER_NS : natural := 40;
    constant BPM_MAX : natural := 250;

    -- period of tp in ns
    constant TP_CYCLE : natural := 5120;

    -- Debounce period in us
    constant DEBOUNCE_PER_US: natural := 50_000;
    constant DEB_MAX_COUNT : natural := (1000 * (DEBOUNCE_PER_US / TP_CYCLE));
    constant DEB_MAX_COUNT_SIZE : natural := log2ceil(DEB_MAX_COUNT);

    -- constant MIN_NS : natural := 60000000000;
    constant MIN_US : natural := 60_000_000;

    constant BPM_SIZE : natural := log2ceil(BPM_MAX + 1);

    constant BTN_PER_MAX : natural := 1000 * (MIN_US / TP_CYCLE);
    constant BTN_PER_SIZE : natural := log2ceil(BTN_PER_MAX + 1);
    constant BTN_PER_MIN : natural := 1000 * (MIN_US / TP_CYCLE) / BPM_MAX;
    constant MAX_COUNT : natural := TP_CYCLE / CLK_PER_NS;

    constant ZEROS : std_logic_vector(31 downto 0) := x"00000000";

    function log2ceil(m : integer) return integer is
    begin
      for i in 0 to integer'high loop
            if 2 ** i >= m then
                return i;
            end if;
        end loop;
    end function log2ceil;

end package body;

Vous noterez la lourdeur d’avoir à déclarer le type de la constante dans le package avant de donner sa valeur dans le body. Les mauvaises langues diront que ça n’est pas beaucoup plus lourd que le C++ et ses doubles fichier (*.h et *.cpp) pour définir une classe.

Un cadenceur (timer) pour tout le système : Timepulse

Nous allons avoir besoin de compter le temps dans plusieurs blocs du projet. L’horloge câblée sur la colorlight est de 25Mhz. Comme nous n’avons pas besoin de précisions à la quarantaine de nanoseconde nous allons utiliser un compteur global pour générer des « pulses » toutes les 5,12µs. Ces pulses seront utilisés en entrée des blocs ayant besoin de compter le temps. Cela réduira la taille des compteurs puisqu’ils n’auront pas à repartir de l’horloge globale.

Le code est suffisamment concis pour que nous puissions le reproduire ici dans son intégralité.

library ieee;
use ieee.std_logic_1164.all;
use ieee.numeric_std.all;

use work.taptempo_pkg.all;

entity timepulse is
    port (
        -- clock and reset
        clk_i : in std_logic;
        rst_i : in std_logic;
        -- timepulse output
        tp_o : out std_logic
    );
end entity timepulse;

architecture RTL of timepulse is
    signal counter : natural range 0 to MAX_COUNT + 1;
begin
    tp_o <= '1' when (counter = 0) else '0';

    counter_p: process (clk_i, rst_i)
    begin
        if rst_i = '1' then
            counter <= 0;
        elsif rising_edge(clk_i) then
            if counter < MAX_COUNT then
                counter <= counter + 1;
            else
                counter <= 0;
            end if;
        end if;
    end process counter_p;
end architecture RTL;

Tout module VHDL est constitué d’une partie entité entity pour décrire les interfaces d’entrées-sorties. Ici nous avons les deux entrées horloge clk_i et reset rst_i caractéristiques d’un système synchrone. L’unique sortie est le pulse généré toutes les 5,12µs.

La description du fonctionnement du module est donnée ensuite dans le bloc « architecture ». En plus des signaux d’entrées-sorties, il est possible de déclarer des signaux interne au bloc. Nous avons déclaré ici le signal counter qui est un entier naturel borné de 0 à MAX_COUNT + 1. La possibilité de borner les signaux que l’on déclare en integer permet d’aider l’outil de synthèse à générer un bus de taille adaptée quand on ne souhaite pas déclarer un std_logic_vector avec une taille explicite.

Attention, un signal n’a pas la même signification qu’une variable dans un programme procédural. Une variable prend une seule valeur qui changera au cours de l’exécution du programme. Un signal doit être vu comme une liste des différentes valeurs que prendra le signal au cours de la simulation. La première valeur de la liste sera son état initial, et à chaque événement sur le signal on ajoutera une valeur à la liste jusqu’à la fin de la simulation.

Nous aurions également pu déclarer des constantes à cet endroit, mais comme dit dans l’introduction, une constante étant un paramètre modifiable nous avons préféré le mettre dans le package taptempo_pkg.

Le corps de l’architecture de timepulse possède deux « process » concurrents qui s’exécutent en parallèle. Le premier est une assignation continue :

    tp_o <= '1' when (counter = 0) else '0';

Il fait passer tp_o à ‘1’ quand le compteur counter atteint la valeur 0. Ce « process » est activé à chaque évenement/changement sur le signal counter.

Le second process est plus étoffé :

    counter_p: process (clk_i, rst_i)
    begin
    --...
    end process counter_p;

Ce process est exécuté à chaque événement surgissant sur les signaux déclarés dans la liste de sensibilité (ici clk_i et rst_i). Le contenu du process est quant à lui exécuté de manière séquentielle.

        if rst_i = '1' then
            --...
        elsif rising_edge(clk_i) then
            --...
        end if;

Cette structure décrit une bascule D qui sera bien reconnue comme telle par le logiciel de synthèse.
Un reset asynchrone ré-initialise le compteur à 0:

            counter <= 0;

Et sur un front montant de l’horloge

        elsif rising_edge(clk_i) then

on incrémente le compteur jusqu’à son maximum.

            if counter < MAX_COUNT then
                counter <= counter + 1;
            else
                counter <= 0;
            end if;

Notez l’utilisation de l’opérateur d’affectation non bloquant <= qui n’affectera la valeur qu’à la fin de l’exécution du process.

Si par exemple nous avons le code suivant dans un process :

    counter <= 10;
    counter <= 5;

La valeur effective de counter à la fin de l’exécution de process sera 5. Et à aucun moment la valeur 10 n’apparaîtra dans counter.

Pour visualiser les signaux de sortie du module on pourra se rendre dans le répertoire cocotb/test_timepulse du projet et lancer le test en donnant ghdl comme simulateur:

$ cd test_timepulse
$ SIM=ghdl make

Le même principe pourra être appliqué pour simuler tous les autres modules de TapTempo.

La simulation consiste à laisser passer le temps pendant une miliseconde :

@cocotb.test()
async def double_push_test(dut):
    trg = TestTimePulse(dut)
    trg.display_config()
    trg.log.info("Running test!")
    await trg.reset()
    await Timer(1, units="ms")

La simulation génère un fichier nommé timepulse.fst visible avec gtkwave :

$ gtkwave timepulse.fst

Comme visible dans l’image ci-dessous.

Visualisation de la simulation timepulse avec gtkwave

Filtrage des rebonds : debounce

Le manipulateur morse génère des rebonds, le filtrage avait initialement été réglé à 20 ms sur la version Verilog. Pour être vraiment tranquille nous le monterons à 50 ms, mais c’est particulièrement long comme période. Comme pour le reste, cette configuration se trouve donc dans le package taptempo_pkg.vhd.

Le module debounce va se charger de filtrer les rebonds au moyen d’une machine à états et d’un compteur.

entity debounce is
    port (
        -- clock and reset
        clk_i : in std_logic;
        rst_i : in std_logic;
        -- inputs
        tp_i  : in std_logic;
        btn_i : in std_logic;
        -- outputs
        btn_o : out std_logic
    );
end entity;

Le comptage des pulsations du module timepulse s’effectue dans un process :

counter_p: process (clk_i, rst_i)
begin
    if rst_i = '1' then
        counter <= 0;
    elsif rising_edge(clk_i) then
        if tp_i = '1' then
            if (state_reg = s_cnt_high) or (state_reg = s_cnt_low) then
                counter <= counter + 1;
            else
                counter <= 0;
            end if;
        end if;
    end if;
end process counter_p;

La machine à états est constituée de 4 états :

    type t_state is (s_wait_low, s_wait_high, s_cnt_high, s_cnt_low);
    signal state_reg : t_state;

Deux états d’attentes et deux états de comptages. Dans un état d’attente s_wait_ on attend un front du bouton. Quand un front survient on passe dans un état de comptage s_cnt_.

Dans un état de comptage, le compteur s’incrémente et passe à l’état d’attente lorsqu’il arrive à son maximum.

De cette manière, seuls les fronts intervenant dans un état d’attente sont pris en compte. Une fois la machine à états dans un état de comptage, plus aucun front du bouton n’est « écouté ».

Mesure de la période d’appuis : percount

Le module percount (period counter) va compter le temps d’une période entre deux appuis sur le bouton.

Si on passe sur les signaux désormais bien connus clk_i, rst_i et tp_i; l’entrée du module est btn_i qui représente la valeur du bouton (appuyé ou non) sans rebond.

Les deux signaux de sortie sont :
– Un vecteur btn_per_o représentant la valeur de la période mesurée
– Un signal btn_per_valid donnant la validité de la valeur précédente. La valeur de btn_per_o est considérée comme bonne uniquement si btn_per_valid est à ‘1’.

Dans un premier process détecte les fronts descendants du bouton :

    btn_fall <= btn_old and (not btn_i);

    btn_p: process (clk_i, rst_i)
    begin
        if rst_i = '1' then
            btn_old <= '0';
        elsif rising_edge(clk_i) then
            btn_old <= btn_i;
        end if;
    end process btn_p;

Puis dans un second process on compte le temps et on remet à 0 le compteur quand un front descendant du bouton surgit :

        --...
        elsif rising_edge(clk_i) then
            if btn_fall = '1' then
                counter_valid <= '1';
            elsif counter_valid = '1' then
                counter <= 0;
                counter_valid <= '0';
            elsif (tp_i = '1') and (counter < BTN_PER_MAX) then
                counter <= counter + 1;
            end if;
        end if;
        -- ...

Conversion période/fréquence (division) : per2bpm

C’est la partie « dure » du système, il va falloir faire une division d’une constante par la période mesurée.

Pour éviter d’avoir à utiliser les blocs multiplieurs dédiés de l’ECP5 et rester « portable » nous allons poser cette division comme on le ferait à la main.

Pour ce faire, on va mettre le dividende dans un registre nommé remainder (pour le reste) :

remainder <= std_logic_vector(to_unsigned((MIN_US / TP_CYCLE) * 1000, remainder'length));

Et le diviseur dans un registre divisor:

if to_integer(unsigned(btn_per_i)) < BTN_PER_MIN then
    divisor <= std_logic_vector(to_unsigned(BTN_PER_MIN, BTN_PER_SIZE)) & ZEROS(DIVIDENTWIDTH-1 downto 0);
else
    divisor <= btn_per_i & ZEROS(DIVIDENTWIDTH-1 downto 0);
end if;

Notez la la valeur minimum de btn_per_i qui nous évitera les problèmes de division par 0 ainsi que des valeurs de bpm trop élevées.

La division est cadencée par une machine à états de 3 états :

type t_state is (s_init, s_compute, s_result);
signal state_reg : t_state;

Un état initial pour démarrer, un état de calcul et un état durant lequel la valeur de sortie est considérée comme bonne.

L’algorithme de division consiste en une série de décalage à droite du diviseur :

divisor <= "0" & divisor(REGWIDTH-1 downto 1);

Et de décalages à gauche du quotient :

if (unsigned(divisor) <= unsigned(remainder)) then
    remainder <= std_logic_vector(unsigned(remainder) - unsigned(divisor));
    quotient <= quotient(REGWIDTH-2 downto 0) & "1";
else
    quotient <= quotient(REGWIDTH-2 downto 0) & "0";
end if;

Avec ajout d’un ‘1’ ou d’un ‘0’ en fonction de la valeur du diviseur et du reste. Lorsque le reste est supérieur au diviseur, on le soustrait du diviseur et on ajoute un bit ‘1’ au moment du décalage du quotient. Sinon on ajoute le bit ‘0’ sans toucher au registre de reste (remainder).

Les registres remainder, divisor et quotient sont des std_logic_vector qui ne représente rien d’autre qu’un « paquet de bits » pour le langage. Si l’on souhaite effectuer des opérations de comparaison ou des additions soustraction dessus il faut donc dire au VHDL que ses bits représentent un nombre (ici non signé) en les « castant » avec unsigned(). Et si l’on souhaite assigner le résultat à un std_logic_vector il faut « caster » à nouveau avec std_logic_vector. Ces multiples conversion de type peuvent vite devenir un casse-tête quand on fait du VHDL.

L’esperluette & est ici un opérateur de concaténation de deux vecteurs std_logic_vector.

Le résultat de la division est donné en continu par les bits de poids faible du registre divisor :

bpm_o <= quotient(BPM_SIZE-1 downto 0);
bpm_valid <= '1' when state_reg = s_result else '0';

bpm_o est considéré comme valide lorsque l’état de la machine à états est s_result.

Sortie «affichage» en rapport cyclique : pwmgen

La sortie « d’affichage » de la valeur consiste à générer un signal périodique pwm_o dont le rapport cyclique est proportionnel à la valeur de bpm_o.

On va pour cela compter de 0 à BPM_MAX :

-- count
count_p: process (clk_i, rst_i)
begin
    if rst_i = '1' then
        count <= BPM_MAX;
    elsif rising_edge(clk_i) then
        if tp_i = '1' then
            if count = 0 then
                count <= BPM_MAX;
            else
                count <= count - 1;
            end if ;
        end if ;
    end if;
end process count_p;

Tant que la valeur du compteur se trouvera en dessous d’un seuil pwmthreshold elle sera à ‘1’, une fois le seuil dépassé elle passera à ‘0’:

-- pwm output
pwm_o <= '1' when (count < pwmthreshold) else '0';

Ce seuil ne doit pas changer de valeur pendant le comptage, on va donc devoir utiliser un registre intermédiaire bpm_reg pour stocker la valeur de bpm_i d’entrée :

-- Latching bpm_i on bpm_valid
bpm_register_p: process (clk_i, rst_i)
begin
    if rst_i = '1' then
        bpm_reg <= BPM_RESET;
        pwmthreshold <= BPM_RESET;
    elsif rising_edge(clk_i) then
        if bpm_valid = '1' then
            bpm_reg <= to_integer(unsigned(bpm_i));
        end if;
        if (count = BPM_MAX) then
            pwmthreshold <= bpm_reg;
        end if;
    end if;
end process bpm_register_p;

Notez, à nouveau, la conversion d’un std_logic_vector (bpm_i) vers un entier naturel (bpm_reg) par le double « cast » to_integer(unsigned())

Synthèse, placement-routage et configuration

Nous voici arrivé à la véritable information de cette dépêche : il est désormais possible de faire de la synthèse VHDL avec un logiciel libre.
Le VHDL est le langage avec lequel les étudiants français abordent le monde du FPGA en général. Je l’ai personnellement beaucoup pratiqué en utilisant ghdl pour la simulation et gtkwave pour visualiser les chronogrammes. Mais jusqu’à cette année, j’ai toujours dû passer sur les monstres propriétaires fournis gratuitement par les fabricants de FPGA pour la partie synthèse. Monstres de plusieurs dizaines de Giga-octets à télécharger, souvent précompilé pour une architecture 32 ou 64 bits mais pas les deux, incluant des machines virtuelles java et autres librairies graphique bugués quand on change la langue du système (coucou le MIG de Vivado). Quand ils ne sont tout simplement pas compatibles Linux (de plus en plus rare cependant). Bref, la synthèse VHDL n’était pas une sinécure.

La simulation n’était pas en reste non plus, car si GHDL fonctionnait plutôt bien, il n’était pas des plus rapides à l’époque. Et impossible de faire de la simulation mixte en mélangeant du VHDL et du Verilog.

Impossible également d’utiliser la formule 1 de la simulation qu’est Verilator, un simulateur un peu spécial qui converti son design en un objet C++, le testbench étant ensuite écrit comme du C++ «normal». Le rapport de performance est d’au moins 100 fois plus rapide.

Pour être honnête, signalons qu’il existe bien le logiciel français Alliance, mais c’est très orienté ASIC, et je n’ai jamais réussi à le faire fonctionner correctement. Peut-être que si des développeurs d’Alliance traînent sur LinuxFR, ils pourront nous en parler.

Bref, la lumière est arrivée cette année avec le développement du greffon ghdl pour yosys ainsi que les avancées à toute vapeur de la partie «synthèse» de ghdl. Comme nous allons le voir dans la section suivante.

GHDL + Yosys, la lune de miel

Pour pouvoir synthétiser TapTempo il va falloir compiler et installer les trois projet suivant :

  • GHDL : Le logiciel de simulation ainsi que de synthèse (ne pas oublier d’option de synthèse dans la configuration avant compilation) ;
  • Yosys : le logiciel de synthèse Verilog, véritable couteau suisse pour le FPGA et plus si affinité ;
  • ghdl-yosys-plugin : le greffon qui permet de compiler une librairie ghdl.so à copier dans le répertoire YOSYS_PREFIX/share/yosys/plugins/ de yosys.

Il est fortement conseillé de prendre les dernières versions du code des projets ci-dessus et de les compiler « à la main » car ces projets avancent vite et bougent beaucoup, les paquets des différentes distributions linux sont déjà largement obsolètes.

Une fois installé on peut lancer yosys avec le greffon ghdl comme ceci :

$ yosys -m ghdl

[...]

 Yosys 0.9+3686 (git sha1 bc085761, clang 8.0.0-svn345496-1~exp1+0~20181029105533.852~1.gbpf10f36 -fPIC -Os)

yosys>

Puis charger nos sources VHDL avec la commande ghdl, en précisant bien le nom du « top » que l’on souhaite élaborer avec l’option -e.

yosys> ghdl --std=08 debounce.vhd  per2bpm.vhd  percount.vhd  pwmgen.vhd  rstgen.vhd  taptempo_pkg.vhd  taptempo.vhd  timepulse.vhd -e taptempo
1. Executing GHDL.
Importing module taptempo.
Importing module rstgen.
Importing module timepulse.
Importing module debounce.
Importing module percount.
Importing module per2bpm.
Importing module pwmgen_250.

Et c’est tout ! Maintenant on peut reprendre les commandes et la procédure que l’on avait utilisée dans le cas de TapTempo en Verilog pour faire la synthèse, le placement routage et le chargement.

Notez que comme les sources ont été chargées et parsé dans yosys, il est parfaitement possible de le convertir en Verilog grâce à la commande write_verilog:

yosys> write_verilog taptempo_converted.v

2. Executing Verilog backend.
Dumping module `\debounce'.
Dumping module `\per2bpm'.
Dumping module `\percount'.
Dumping module `\pwmgen_250'.
Dumping module `\rstgen'.
Dumping module `\taptempo'.
Dumping module `\timepulse'.

La version Verilog ainsi générée pourra être simulée avec verilator ou icarus par exemple pour faire de la simulation mixte, ou tout simplement pour accélérer la simulation dans le cas de verilator.

Pour synthétiser sur la colorlight qui est munie d’un FPGA ECP5 de chez Lattice, on utilisera la commande synth_ecp5 avec une sortie netlist au format json.

yosys>  synth_ecp5 -json taptempo.json
[...]
=== taptempo ===

   Number of wires:                540
   Number of wire bits:           1515
   Number of public wires:         540
   Number of public wire bits:    1515
   Number of memories:               0
   Number of memory bits:            0
   Number of processes:              0
   Number of cells:                785
     CCU2C                         109
     L6MUX21                         9
     LUT4                          452
     PFUMX                          25
     TRELLIS_FF                    190

2.50. Executing CHECK pass (checking for obvious problems).
Checking module taptempo...
Found and reported 0 problems.

2.51. Executing JSON backend.

Arrivé à cette étape il peut être intéressant de comparer avec la version Verilog les ressources utilisées par le même projet :

$ yosys
yosys> read_verilog debounce.v  per2bpm.v  percount.v  pwmgen.v  rstgen.v  taptempo.v  timepulse.v
yosys> synth_ecp5 -json taptempo.json
...
=== taptempo ===

   Number of wires:                487
   Number of wire bits:           1435
   Number of public wires:         487
   Number of public wire bits:    1435
   Number of memories:               0
   Number of memory bits:            0
   Number of processes:              0
   Number of cells:                751
     CCU2C                         105
     L6MUX21                         1
     LUT4                          441
     PFUMX                          13
     TRELLIS_FF                    191

8.50. Executing CHECK pass (checking for obvious problems).
Checking module taptempo...
Found and reported 0 problems.

8.51. Executing JSON backend.

Nous obtenons un design légèrement plus petit avec la version Verilog, mais les ordres de grandeurs sont tout de même respecté.

Placement routage avec NextPnR

NextPnR est un logiciel de placement routage qui prend le schéma (netlist) de cellules «primitives» généré par le logiciel de synthèse et associe chaque cellule à une cellule disponible dans le FPGA. NextPnR effectue également le routage qui consiste à établir les connexions entre les différentes cellules.

C’est également à cette étape que l’on va préciser la configuration du FPGA (taille, IO…). En plus du fichier json de synthèse nous donnerons un second fichier de configuration des IO nommé taptempo.lpf décrivant nos trois signaux d’entrées-sortie :

LOCATE COMP "clk_i" SITE "P6";
IOBUF PORT "clk_i" IO_TYPE=LVCMOS33;
FREQUENCY PORT "clk_i" 25 MHZ;

LOCATE COMP "btn_i" SITE "M13";
IOBUF PORT "btn_i" IO_TYPE=LVCMOS33;

LOCATE COMP "pwm_o" SITE "P4";
IOBUF PORT "pwm_o" IO_TYPE=LVCMOS33;

Toutes les commandes de synthèse données ici sont bien évidemment disponibles dans un Makefile sur le dépot. Pour faire le placement routage nous pourrions taper la commande suivante :

$ nextpnr-ecp5 --25k --package CABGA256 --speed 6 --json taptempo.json --textcfg taptempo_out.config --lpf taptempo.lpf --freq 25

Qui ne donnera pas le fichier de configuration nommé bitstream permettant de configurer le FPGA !

Car les spécifications des FPGA sont gardées jalousement par les constructeurs, et il faut des heures et des heures d’ingénieries inverses pour venir à bout de ces informations. Travail qui a été effectué via le projet Trellis et qui nous permet de convertir la sortie texte précédente taptempo_out.config en un bitstream reconnu par l’EPC5 :

$ ecppack taptempo_out.config taptempo.bit

Et l’on décroche enfin le Saint Grââl permettant de configurer la colorlight : le bitstream taptempo.bit.

En avant la musique avec openFPGALoader

Arrivé à cette étape il serait vraiment dommage d’être contraint de relancer l’ide proprio du constructeur juste pour télécharger le bitstream dans le FPGA via une sonde USB-Jtag !

C’est là que l’on peut dégainer le projet openFPGALoader qui a pour ambition de permettre la configuration de tous les FPGA existant avec toutes les sondes disponibles sur le marché.

$ openFPGALoader taptempo.bit 
Open file taptempo.bit DONE
Parse file DONE
Enable configuration: DONE
SRAM erase: DONE
Loading: [==================================================] 100.000000%
Done
Disable configuration: DONE

Et voila, on peut maintenant taper taper taper jusqu’au bout de la nuit.

Conclusion

Le VHDL est très verbeux, les évolutions du langage ont tenté de corriger un peu le tir mais cela reste tout de même verbeux. Certaine caractéristiques comme l’insensibilité à la casse font un peu penser à un langage d’un autre âge. Cependant, l’héritage du langage Ada fait de VHDL un langage très strict et déterministe de part sa conception contrairement au Verilog.

Le typage fort peut-être considéré à première vu comme un défaut ralentissant l’écriture du code. Mais il n’en est rien, après avoir souffert de « compiler » votre porte-gramme pour la simulation, vous aurez l’agréable surprise de voir votre système fonctionner parfaitement sur le FPGA du (presque) premier coup.

Le vocabulaire VHDL est très vaste et on se contente en général des structures de code connue dont on sait qu’elles « synthétiseront » correctement, ce qui donne une impression de ne jamais pouvoir atteindre la maîtrise du langage.

Il y a quelques années je m’étais posé la question de la popularité du VHDL par rapport au Verilog. En effet, même si le VHDL est presque aussi bien supporté que le Verilog par les outils des constructeurs, ça n’était pas le cas des logiciels libres. C’est encore largement le cas aujourd’hui, même certain logiciels non libre supportent en priorité le Verilog. Le constructeur Gowin par exemple ne permettait que la synthèse Verilog avec son outil maison. Il fallait installer le logiciel tier synplify de synopsis pour pouvoir accéder à la synthèse VHDL.

Cette extension de ghdl pour Yosys change la donne. Car, comme nous l’avons vu, il est possible de l’utiliser pour convertir son projet en Verilog et avoir accès à tous l’écosystème libre Verilog. Il est également possible de faire de la vérification formelle pour le VHDL.

Avoir la compétence VHDL dans son CV est une assez bonne idée car c’est souvent par ce mot que l’on résume le développement ASIC/FPGA/SoC. En Europe, le VHDL est très apprécié de l’industrie et particulièrement de l’industrie de défense.

Mais si c’est juste pour mesurer le tempo, ce n’est peut-être pas la voie la plus rapide et la plus simple 😉

Aller plus loin

Un CPU RISC-V avec des instructions «maison» reconfigurable à volonté

Le standard RISC-V permet l’ajout d’extension «maison» par le constructeur. Cela fait longtemps qu’on ne propose plus l’ajout de puce à coté du processeur pour ajouter des instructions (comme le coprocesseur arithmétique sur les 386), la perte en bande passante est vraiment trop grande.

Le chinois Andes technology et le Français Menta s’associe pour proposer une solution eFPGA permettant de synthétiser «à la demande» ces instructions maison.

La société Menta va-t-elle publier la spécification du «bitstream» permettant de configurer cette partie ? Rien n’est moins sûr, mais on l’encourage à faire comme quicklogic.

L’annonce de partenariat par Andes technologie et Menta.

Stakeholder Consultation on the Role of Open Source Software (OSS) and Open Source Hardware (OSH)

A message from Andrew Katz on linkedin :

«I’m leading the Open Hardware section of a European Commission study on open source software and hardware. This is really exciting and important work, as the final report (due early next year) will be used to form European Commission policy on both open source software and open hardware for the next decade. #opensource #openhardware

As part of the study, we’re asking people to complete a survey, which you can find here:

https://inno.limequery.com/436575

You don’t have to be based in the EU to participate, and you don’t have to complete all questions.

If you have an interest in the future of Open Source Software or Hardware, please take a look, and please forward to any contacts or colleagues whom you think may also be interested.

Thank you!»