No post sobre a configuração do PYNQ tem uma seção chamada “Organizando os arquivos do overlay”. Ela existe porque o PYNQ só carrega o overlay se o .hwh tiver exatamente o mesmo nome do .bit, e o Vivado coloca os dois em lugares bem diferentes do projeto.
Na prática, cada vez que eu gerava um bitstream novo o ritual era o mesmo: achar o .bit dentro de impl_1, achar o .hwh dentro de hw_handoff, copiar os dois para uma pasta, renomear, e mandar para a placa por Samba ou scp. Não é difícil. É chato, e quando dá errado o erro aparece só lá no Jupyter.
Então fiz uma CLI para facilitar um pouco esse processo.
O que ele faz
O dpynq é uma CLI em Go que pega os arquivos de um projeto Vivado (.bit, .hwh, .xsa), renomeia todos para o nome base do .bit e envia para a placa por SSH/SFTP. Se o bitstream se chama system_wrapper.bit, chegam na placa system_wrapper.bit, system_wrapper.hwh e system_wrapper.xsa, que é o formato que o PYNQ espera.
O uso mais simples é rodar o comando na raiz do projeto Vivado:
dpynqOs padrões já batem com a imagem do PYNQ (IP 192.168.2.99, usuário xilinx), mas dá para mudar tudo por flag:
dpynq --host 192.168.2.99 --user xilinx --project meu_projetoO destino padrão é /home/<user>/jupyter_notebooks/<project>/, ou seja, os arquivos já aparecem no Jupyter. Além dessas, existem --port, --dir, --dest, --password, --key, --timeout, --no-color e --version.
Para instalar, no Linux e no macOS:
curl -fsSL https://raw.githubusercontent.com/xoesae/dpynq/main/install.sh | shQuem tem Go instalado pode usar go install github.com/xoesae/dpynq@latest.
Por que Go
Eu queria um único binário que funcionasse no Windows, no macOS e no Linux sem pedir nada além dele mesmo. Um script em shell resolveria no meu Linux, mas dependeria de find, ssh e scp, e no Windows isso já vira um problema.
Go resolve isso de graça com CGO_ENABLED=0 o binário sai estático, e a cross-compilação é só trocar GOOS e GOARCH. SSH e SFTP vêm de bibliotecas em Go puro (golang.org/x/crypto/ssh e pkg/sftp), então não existe dependência do ssh do sistema.
Quem publica as versões é o GoReleaser. Quando eu crio uma tag v*, um workflow do GitHub Actions compila para Linux (amd64 e arm64), macOS (amd64 e arm64) e Windows (amd64), e sobe tudo na release junto com um checksums.txt.
E também porque continuo meus estudos sobre Go 😁
Achando os arquivos certos
O .bit é o tranquilo: o dpynq procura em <projeto>.runs/impl_1/*.bit e, se não achar, aceita qualquer .bit que esteja num diretório impl_1.
O .hwh dá mais trabalho, porque o caminho depende do nome do block design e da versão do Vivado. O nome do BD sai do próprio bitstream: <bd>_wrapper.bit vira <bd>, e qualquer outro nome cai no padrão system. Com o nome em mãos, a busca segue esta ordem:
<projeto>.gen/sources_1/bd/<bd>/hw_handoff/<bd>.hwh, onde o Vivado 2020.2 em diante coloca o arquivo- o mesmo caminho em
.srcs, para versões anteriores - qualquer
<bd>.hwhno projeto - qualquer
.hwh
A primeira versão tinha o nome system fixo. Funcionava no meu projeto e em quase todos os tutoriais, mas num projeto com dois block designs ela podia mandar o .hwh de um junto com o .bit do outro. O .hwh descreve os IPs e os endereços do hardware, então o PYNQ passaria a enxergar um hardware que não é o que está na FPGA. É o pior tipo de bug: não quebra na hora do deploy.
O .xsa é opcional. Se existir até dois níveis abaixo da raiz, vai junto, se não, o dpynq só avisa com um [INFO] e segue.
Autenticação
A imagem do PYNQ vem com usuário e senha xilinx, então o caminho óbvio seria pedir a senha sempre. Mas quem usa SSH no dia a dia já tem chave, e digitar senha a cada deploy cansa rápido.
A ordem que ficou é:
- chaves: as do
ssh-agente as chaves sem senha em~/.ssh(id_ed25519,id_ecdsa,id_rsa). Com--key, só a indicada - senha:
--password, depois a variávelDPYNQ_PASSWORD, depois um prompt no terminal.
O prompt só aparece se as chaves falharem. Rodando ssh-copy-id xilinx@192.168.2.99 uma vez, o dpynq nunca mais pede senha.
A variável de ambiente existe porque --password fica visível no ps e no histórico do shell. Para scripts e CI, DPYNQ_PASSWORD é bem menos exposta. Já chaves protegidas por senha o dpynq não tenta abrir: a mensagem pede para adicioná-las ao agent com ssh-add.
MaxAuthTries
Um detalhe curioso que acabei esbarrando é que o servidor SSH limita quantas tentativas de autenticação cada conexão pode fazer, cada chave oferecida conta como uma tentativa.
Se a mesma chave está no agent e em ~/.ssh, ela seria oferecida duas vezes. Com algumas chaves no agent, as tentativas acabam antes de chegar na senha, e o usuário recebe um erro de autenticação mesmo com a senha certa. Por isso cada chave entra uma vez só, comparando a chave pública:
1add := func(ss ...ssh.Signer) {2 for _, s := range ss {3 k := string(s.PublicKey().Marshal())4 if !seen[k] {5 seen[k] = true6 signers = append(signers, s)7 }8 }9}A chave do host não é verificada
O dpynq usa ssh.InsecureIgnoreHostKey(), ou seja, não confere se a placa é quem diz ser.
Bom, para meu caso de uso isso não importa, a placa fica na rede local (literalmente ao meu lado), e a chave do host muda toda vez que o cartão SD é regravado, o que acontece bastante quando se mexe com PYNQ. Verificar a chave faria o dpynq falhar justamente no uso normal.
Um servidor SSH dentro do teste
Para testar o deploy sem placa, os testes sobem um servidor SSH/SFTP de verdade dentro do próprio teste, com um ssh-agent em memória e HOME e SSH_AUTH_SOCK isolados. Assim dá para testar chave no agent, chave no disco, senha errada e prompt sem tocar no ~/.ssh de ninguém.
O finder é testado com 11 layouts de projeto montados em diretórios temporários. A suíte roda com -race e tem cerca de 79% de cobertura (96% no finder, 91% no deploy).
Conclusão
O dpynq surgiu de uma necessidade minha que começou como um script e virou o projeto onde eu mais aprendi sobre alguns detalhes de SSH e Golang.
Código e sugestões no GitHub.