logo

Objectif
A première vue
Le schéma
L'alimentation
Le microcontrôleur
Le synthétiseur
Les ROMs
Code et memory map
Cartouche AVR
Cartouche CPLD
Convertir les sons
Créer le ROM
Circuits imprimés
Cartouche CPLD + Flash parallèle

Quelques semaines avant de me lancer dans ce projet, [Grapsus] est tombé chez Farnell sur un petit CPLD en QFP44, tolérant le 5V et peu cher : le EPM3032 d'Altera. Puisqu'il aurait pu finir comme mapper dans une cartouche, il me propose d'en commander pour moi. Un peu plus tard, après les avoir reçus, il veut simplifier leur utilisation en créant une breakout board sur mesure grâce à sa technique de gravure de circuits (dont je suis jaloux). Je reçois donc deux de ces CPLD ainsi qu'une breakout board de sa part, sans vraiment savoir ce que je vais en faire.

En me lamentant sur l'échec de la cartouche à base d'AVR, mon regard croise le petit circuit imprimé. L'espoir revient instantanément ! Comme je n'allais pas pouvoir m'en sortir avec de la mémoire série i2c, j'allais sûrement pouvoir m'en sortir en imitant l'architecture interne des VSM, c'est à dire avec un peu de logique et de la mémoire parallèle !

tms1000

Je monte alors le CPLD sur la carte, installe deux condensateurs de découplage, et réalise une autre carte sur de la bakélite de prototypage pour accueillir la mémoire parallèle et les régulateurs de tension.

N'ayant que des flash 29SF256 (32ko), je câble le support DIP28 de manière à fixer A14 à l'état bas et ainsi n'utiliser que les 16 premiers kilooctets.

Pour l'alimentation, comme on ne peut avoir que le -3.5V et VBAT (+6V) sur le connecteur cartouche, j'ai simplement choisi de mettre la masse des régulateurs sur le -3.5V. Un 78L05 abaisse les 9.5V à 5V pour les besoins de la flash, et un 1117-3.3 derrière fournit le 3.3V pour le CPLD.

J'installe cette fois de vrais diviseurs de tension sur les entrées, afin de ne pas brusquer le CPLD qui fait déjà l'effort de tolérer du 5V. J'ai choisi des résistances de valeurs assez élevées pour ne pas tirer trop fort les lignes au -3.5V : 68k et 28k permettent d'obtenir 2.7V avec 9.5V, un niveau largement acceptable pour le CPLD.Simulation

Le câblage se fait au fil Kynar, bien pratique dans cette situation quoiqu'un peu fragile. Je prévois un connecteur JTAG sur le bord droit de la carte.

Comme avec l'AVR, j'utilise un transistor externe (Q1) pour faire une sortie a collecteur ouvert sur le CPLD.

Comme la plupart des IDE pour CPLD, celui d'Altera (Quartus), permet de définir la logique de nombreuses manières différentes. Étant déjà passé par la case "saisie de schéma", j'ai voulu cette fois m'aventurer dans le Verilog. Pour ne pas aller trop vite, j'écris d'abord un simple compteur incremental base sur le signal ROMCLK et dont les sorties vont sur le bus d'adresse de la flash.

Premier problème : Lorsque je connecte le Byteblaster (programmateur Altera) au port JTAG, la dictée plante. Comme ce programmateur possède un buffer qui s'adapte au 3.3V, et que cette tension était bonne, le problème ne pouvait venir que de la masse.
J'ai mis quelques minutes pour me rendre compte que je court-circuitais ma "masse virtuelle" qui je le rappelle est en fait le -3.5V de la dictée, à la véritable masse de l'alimentation, qui passait par mon programmateur AVR qui était toujours branché et les ports USB de mon ordinateur ! La solution ? Débrancher le programmateur AVR : plus de boucle.

Deuxième problème : la première ligne d'adresse en sortie du CPLD, qui devait être à la fréquence de ROMCLK divisée par 2, ne ressemble à rien. Avec du code si simple, la cause de ce souci ne pouvait pas venir de bien loin : le CPLD ne devait sûrement pas bien recevoir ROMCLK. Pour mieux cerner le problème, je simplifie le code à l'extrême en faisant sortir une simple copie du signal. Surprise : le signal semble bien correspondre à ROMCLK... A l'exception d'un détail. En zoomant sur le front descendant de la réplique du signal, j'aperçois un sursaut très court, d'une centaine de nanosecondes, qui n'est pas présent sur le signal issu de la dictée.tms1000

Un doute m'envahit : n'y aurait-il pas de trigger de Schmitt sur les entrées du CPLD ? En effet, la pente descendante du vrai ROMCLK est bien plus longue que la pente montante. Le fait que la tension varie (trop) doucement provoquerait un état instable sur l'entrée et causerait ce sursaut. La documentation confirme le doute : il n'y a que de l'hystéresis sur les broches JTAG. La solution vient tout naturellement : intercaler un composant avec de telles entrées entre la dictée et le CPLD. J'ai pris un CD4093 (portes NAND) car je n'avais que ça sous la main, et problème résolu ! On voit ce circuit pendre avec la plupart de ses broches coupées sur le côté droit de la carte.

A ce stade, j'espérais que ROMCLK était le seul signal à traiter de la sorte. Après un rapide coup d'œil au bus et à M1/M0, je fus rassuré de voir que ces signaux n'avaient pas de telle pente.Speak and spell diagram

Je reprends donc mon code avec le compteur, pour tester le bus d'adresse côté flash, et rencontre deux incohérences sur les lignes A4 et A11. Je ne remarque pas que ces deux broches sont opposées sur le support de la flash et passe une bonne demi-heure à chercher d'où vient le problème.

Je m'aperçois finalement qu'il y a un court circuit entre ces deux lignes, et qu'il est causé par une mauvaise coupure de piste sur la plaque bakélite. Stupide erreur de précipitation. Une fois proprement coupée, j'étais sûr que toutes les lignes d'adresse fonctionnaient. Cependant, quelque chose m'inquiétait à nouveau : lorsque je synthétisais mon code avec le compteur 14 bits, Quartus m'indiquait que je consommais 27 des 32 macrocellules disponibles du CPLD. Même pas un quart de la fonctionnalité, et seulement 5 restantes. Je me mentais en espérant que le tout allait tenir.

Peu importe, je me lance dans le code pour simuler du mieux possible le comportement des VSM. Pour cela, ModelSim (livre avec Quartus) m'a été d'une très grande aide en me permettant de visualiser le comportement des sorties et des signaux internes en fonction des entrées, sans avoir à programmer le CPLD "en réel" et à le câbler/recâbler sans arrêt.

En assignant tous les signaux nécessaires aux broches du CPLD, je m'aperçois que j'ai été très chanceux pour une fois, le compte tombe pile-poil : 34 broches utilisées sur 34 broches disponibles. Après des dizaines d'essais et l'arrachage de mes cheveux restants, j'arrive à une simulation qui ressemble beaucoup aux chronogrames des datasheets. Le code utilise finalement 45 des 64 macrocellules ainsi que divers signaux et registres internes. Le plus notable est "intaddress", un registre 23 bits qui contient l'adresse du prochain bit (oui bit, pas octet) à lire.

Simulation

A: Front montant de ROMCLK, M1 et gotnop sont hauts, on latche 4 bits d'adresse et on passe gotnop a 0 afin d'ignorer M1 au prochain front montant de ROMCLK.
B: Front montant de ROMCLK, M1 et M0 sont bas, on a un NOP donc on remet gotnop a 1.
C: Front montant de ROMCLK avec M1 et gotnop haut encore, on décale intaddress de 4 bits a droite et on latch 4 nouveaux bits d'adresse. On remet gotnop a 0.
D: Derniers bits d'adresse (les 4MSBs, soit le chip select), le bus d'adresse de la flash sort maintenant la bonne adresse.
E: Front montant de ROMCLK avec M0 haut, première commande de lecture après un adressage, c'est un dummy read. On en profite pour mettre le compteur de bits dans intaddress a 0 et charger un octet depuis la flash dans datab (10100101). On met dummy a 1 pour signifier que le dummy read est passe.
F: Front montant de ROMCLK avec M0 et dummy hauts, on compare les 4 MSB de intaddress avec le chip select en dur (a 0010), si il correspond on prépare le bit a répondre (outp a 1) et on incremente intaddress.
G: Front descendant de M0, on répond le bit #0 sur bus8o (soit un "1"). A noter que dans le code final, cette sortie est inversée car elle est a collecteur ouvert (Q1).
H: Front montant de ROMCLK, M1 et M0 bas: premier NOP après une lecture, on décale datab d'un bit vers la droite pour préparer l'éventuelle prochaine lecture.
I: Front montant de ROMCLK avec M0 haut, nouvelle lecture.
J: Front descendant de M0, on répond le bit #1 sur bus8o (soit un "0").
K: Front montant de ROMCLK avec M0 haut, nouvelle lecture. Il faut recharger un nouvel octet depuis la flash, datab est recharge (10010011).
L: Front descendant de M0, on répond le bit #0 sur bus8o (soit un "1").
M: Front montant de M1: On relâche le bus quoi qu'il arrive, en mettant bus8o a 0.

Le seul problème avec ce code, c'est qu'avec ces signaux asynchrones et le gros compteur 23 bits, il se synthétise en 45 macrocellules. Le CPLD n'en a que 32. Je prie alors pour qu'une version plus fournie existe dans le même boîtier et miracle : le EPM3064 existe bel et bien. Je le commande, le remplace sur la carte, et balance le fusemap.

J'écris un ROM minimal avec un seul mot et ses données LPC, programme la flash avec, l'installe sur la carte, et allume la dictée. Sueur froide juste avant d'appuyer sur le bouton "Module select", qui passe. Je sais alors au moins que la définition logique est assez bonne pour répondre les magic bytes.
Je lance une série de mots, et j'entends "SPELL BAMBOULA". La victoire est mienne !

 

download Projet Quartus avec source Verilog

Déclaration des entrées/sorties du module (appelle "snsrom"): romclk, m0, m1, le bus 4 bits et les 8 bits de données provenant de la flash en entrée. Le bus d'adresse pour la flash et la réponse sur ADD8 en sorties.

module snsrom
(
	input romclk,		// Horloge 160kHz venant du synthe
	input m0,			// Commande VSM
	input m1,			// Commande VSM
	input [3:0] bus,	// ADD8/4/2/1
	output reg bus8o,	// Sortie bit (collecteur ouvert sur ADD8)
	output reg [13:0] address,	// Adresse flash
	input [7:0] data	// Donnees flash
);

Déclaration des registres internes: gotnop est un flag pour savoir si on est passe par un nop (m0 et m1 a 0), dummy pour savoir si on a reçu un dummy read (si on l'attend ou si on l'a reçu), outp est un flag pour savoir si on doit sortir un bit au prochain front descendant de m0 ou pas. intaddress est l'adresse courante au niveau bit (4 de chip select, 14 d'adresse octet, 3 d'adresse bit). datab est le registre a décalage pour la sortie des bits.

reg gotnop;	// On a eu un nop ?
reg dummy;	// On a eu un dummy read ?
reg outp;	// On doit sortir un bit ?
reg [22:0] intaddress;	// __CC CCAA AAAA AAAA AAAA BBB, C=Chip select, A=Addresse, B=Numero du bit
reg [7:0] datab;			// Registre a decalage donnees

Les événements asynchrones: quand on a un front descendant sur m0 et que outp est a 1, on sort un bit. Quand on a un front montant sur m1, on lâche le bus, quoi qu'il arrive.

always @(negedge m0 or posedge m1)
begin
	if (m1 == 1)
	begin
		bus8o <= 1'b0;		// Lacher le bus
	end
	else if (outp == 1 && gotnop == 1)
	begin
		if (datab[0] == 1)	// Sortir bit (inverse car collecteur ouvert)
			bus8o <= 1'b0;
			else
			bus8o <= 1'b1;
	end
end

Les événements synchrones:

Si on a un nop, on met gotnop a 1. Si outp est a 1, on est sur le premier nop après avoir sorti un bit, on shift datab a droite une fois pour préparer le suivant et passe outp a 0.

always @(posedge romclk)	// Devrait etre negedge, mais romclk est inverse par le CD4093
begin
	if ({m1,m0} == 2'b00)	// 00: Nop
	begin
		gotnop = 1;
		if (outp == 1)			// Premier nop apres un read bit ?
		begin
			datab[6:0] = datab[7:1];	// Oui: shifter
			outp = 0;
		end
	end

Si on reçoit la commande set address et que gotnop est a 1, on shift intaddress a droite de 4 et on coince la valeur sur le but dans les MSB. On remet dummy gotnop et outp a 0.

else if ({m1,m0} == 2'b10)	// 10: Set address
	begin
		if (gotnop == 1)		// Actif qu'apres un nop
		begin
			intaddress[18:3] = intaddress[22:7];	// Shift de l'adresse x4
			intaddress[22:19] = bus[3:0];			// Charger les MSB
		end
		dummy = 0;
		gotnop = 0;
		outp = 0;
	end

Si on reçoit la commande read bit, il faut préparer les données pour le prochain front descendant de m0. Si c'est la première commande read bit après un set d'adresse, alors c'est un dummy read: on charge datab avec l'octet que la flash donne, on remet a zéro le compteur de bits dans intaddress et on indique que le dummy read est passe en mettant dummy a 1.
Si c'est un read bit avec le dummy read passe, on incremente le numéro de bit (l'adresse) et on charge un nouveau un nouvel octet si on est au bit 1 (et pas 0 car la flash ne sort pas le byte de manière instantanée). On compare les MSB de l'adresse pour faire un chip select match et on met outp a 1 si c'est le cas.

else if ({m1,m0} == 2'b01)	// 01: Read bit
	begin
		if (dummy == 0)
		begin
			datab[7:0] = data[7:0];	// Dummy read, charger octet et compteur de bit a zero
			intaddress[2:0] = 0;
			dummy = 1;
		end
		else
		begin
			intaddress[20:0] = intaddress[20:0] + 1;	// Vraie lecture, pre-incrementer l'addresse
			if (intaddress[2:0] == 1)
				datab[7:0] = data[7:0];		// Charger un nouvel octet si on est au bit 1
			if (intaddress[20:17] == 4'b0010) outp = 1;	// Si le chip select en dur correspond, preparer la sortie
		end
	end
end

On attribue les bits 3 a 16 du registre intaddress aux lignes d'adresse de la flash (on ne prend pas les 4 MSB qui servent de chip select ni les 3 LSB qui servent de compteur de bit).

always @*
begin
	address[13:0] = intaddress[16:3];
end

endmodule
footer
symbol symbol symbol symbol symbol