Hier mijn ervaringen tot nu toe met een van ST's nieuwere producten, de 400MHz dual precision floating point STM32H7x3 series microcontroller. Ik wilde wat gaan spelen met digitale audiobewerking, plus een printje ontwerpen met al dit spul erop. Voor digitale audio heeft ST twee soorten periferie ter beschikking staan: De serial audio interface (SAI) en de wat traditionelere Serial Pheriphiral Interconnect (SPI) poorten. Mijn doel was 1 I2S ingang, en 3 I2S uitgangen, elk stereo uitgevoerd. Zowel de SAI als de SPI poort zouden dit moeten kunnen. Uiteindelijk gekozen voor de SPI poort, omdat ik hier op andere platformen ook ervaring mee heb.
Omdat de configuratie van deze uitgebreide microcontroller best ingewikkeld kan zijn, heeft ST een mooi hulpprogramma hier voor bedacht: STM32CubeMX. Door middel van deze configuatietool moet het simpel zijn om een project op te starten met de juiste settings voor alle interfaces. Een grafische weergave van je microcontroller, met alle geconfigureerde pinnen, alle instellingen netjes in overzichtelijke keuzemenu's. Tot zover alles top dus.
Een DAC en ADC op een SPI poort is natuurlijk een hele kluif om bezig te houden. Mijn gekozen sampling rate van 96kHz houdt in dat 192000 keer per seconde een 32 bit waarde naar de SPI poort moet worden gestuurd, en evenveel data er vanaf komt. Omdat ik mijn cycles liever ergens anders aan besteed, ben ik gedoken in DMA oftewel Direct Memory Access. Dit is een mechanisme om een stuk geheugen te verplaatsen naar een ander stuk geheugen. Omdat de SPI poorten geïmplementeerd zijn als geheugenaddressen, is dit ideaal om, zonder tussenkomst van de processor, de SPI poorten te voorzien van data. De SPI poort genereerd, als zijn buffer leeg begint te lopen, DMA requests, en de DMA controller zorgt ervoor dat nieuwe gegevens op het dataregister adres worden geschreven.
Toverwoord hierin is Circular Buffering. Een geheugenplaats in een toegewezen blok wordt naar het dataregister van de SPI poort geschreven, en iedere keer wordt een opeenvolgende geheugenlocatie gekozen door middel van een incremental pointer. Bij het einde van mijn gekozen blok gaat de pointer weer terug naar het begin, en zo gaat dit proces door tot in het oneindige. De truc is om je geheugenblok op het juiste koment te vullen met nieuwe gegevens. Om deze reden kan de DMA controller een interrupt genereren als deze op de helft is gekomen van mijn geheugenblok (half Transfer Complete), en bij het einde van het blok (Transfer Complete). Bij Half transfer kan de eerste helft worden voorzien van nieuwe data, en bij Transfer Complete de tweede helft.
In de eerste instantie, bij het configureren van de DMA, deed deze niets, en het heeft mij veel lezen gekost om tot de oorzaak te komen waarom dit zo was. ST heeft een mooie reference manual samengesteld voor deze serie microcontrollers, en het zal best ergens tussen deze 3179 pagina's staan. Ik heb dus gelezen dat zowel de bron als doel adressen het gehele gebied van de 4GB geheugenbereik konden omvatten (in paragraaf 15.3.8). Echter, in een schematische weergave op pagina 100 (figuur 1) komen we erachter dat deze DMA controller op de D2 AHB bus gesitueerd is. De microcontroller heeft namelijk verschillende gegevensbussen. De AXI bus is de meest snelle, en is direct verbonden met de CPU. Onder andere de SD kaart, SDRAM en Quad SPI interface zijn verbonden met deze supersnelle databus.
In het wat langzamere segement hebben we andere poorten zoals mijn SPI, SAI, I2C en een hele hoop andere interfaces. Het geheugen op deze bus die de DMA kan gebruiken, beperkt zich tot 2 SRAMs van 128KB en een van 32KB. Dat is heel wat anders dan die beloofde 4GB aanstuurbaar bereik! tot slot hebben we nog een low power, low speed D3 AHB, speciaal bedoeld om de controller in een low power omgeving te kunnen laten werken. Elk van deze domeinen heeft een eigen DMA controller, en eigen stukjes SRAM geheugen. deze staan aangegeven in het linker script (.LD bestand):
code:
/* Specify the memory areas */
MEMORY
{
DTCMRAM (xrw) : ORIGIN = 0x20000000, LENGTH = 128K
RAM_D1 (xrw) : ORIGIN = 0x24000000, LENGTH = 512K
RAM_D2 (xrw) : ORIGIN = 0x30000000, LENGTH = 288K
RAM_D3 (xrw) : ORIGIN = 0x38000000, LENGTH = 64K
ITCMRAM (xrw) : ORIGIN = 0x00000000, LENGTH = 64K
FLASH (rx) : ORIGIN = 0x8000000, LENGTH = 2048K
}
Mijn SRAM was dus het RAM_D2 geheugen, die op de juiste D2 AHB bus zat. Echter, hier stopt de pret niet. Om correct door de DMA to kunnen worden gelezen moest het toegewezen geheugenblok ook op een 32 bit grenswaarde liggen. Uiteindelijk waren de definities voor dit geheugen dus:
c code:
#define __SECTION_RAM_D2 __attribute__((section(".RAM_D2"))) __attribute__((aligned (4))) /* AHB SRAM (D2 domain): */
Nadat het geheugen helemaal was uitgezocht, werkte de initialisatie van de DMA controller. Met behulp van een oscilloscoop kan ik nu nagaan hoe mijn mooie sinusgolf uit de DAC komt. Althans, dat was de bedoeling. Een hele warboel kwam eruit, alles behalve mijn verwachte mooie sinus. Een aantal redenen waren hier oorzaak van. We beginnen met..
nummer 1: Het blijkt dat het dataregister van de SPI poort, bij handmatige aansturing, prima 32 bit waardes accepteerd. Maar in geval van de DMA controller is 16 bit schijnbaar het maximum. De DMA controller leest dus een 32 bit waarde uit mijn geheugen, en schrijft deze naar de SPI in 2 16 bit waardes. Dit proces heet packing, en gaat automatisch ALS je de juiste data breedtes instelt. Het heeft een tijdje geduurdt voordat ik erachter kwam dat dit dus 16 bits (HALF_WORD) voor de SPI poort was.
Nummer 2: Endianness. Deze term duidt op de manier waarop de CPU gegevens opslaat in het geheugen. In Big Endian wordt een hex waarde van 0x34a2b710 weggeschreven als 0x34a2b710 in het geheugen. echter, deze ARM processor (en heel veel andere CPU's) is Little Endian! De waarde in het geheugen is in dit geval dus 0x10b7a234. Omdat de databreedte van mijn SPI poort 16 bit was, worden half-word waardes juist vertaald, maar een 32 bit waarde die ik gebruik, dus niet. De waardes moesten op 16 bit niveau conventioneel worden opgeslagen, maar de twee 16 bit halfword waardes in een 32 bit word moesten dus verwisseld worden. 0xa23410b7 dus. De oplossing is simpel maar voor je erachter bent hoe de data nu eigenlijk geïnterpreteerd wordt, ben je wel even bezig:
c code:
signed long shortswap(signed long val){
return (val << 16) | ((val >> 16) & 0xffff);
}
Nummer 3: De datacache. In een processorsysteem met hogere klokfrequentie is het gebruik van een geheugencache uiterst effectief: Een cache is een klein stukje geheugen dicht in de buurt van de CPU waar de processor snel toegang tot heeft. Tijdens het vollopen van de cache worden dan waardes teruggeschreven naar het langzamere SRAM. Als de DMA controller moet lezen van dit SRAM moet je dus wel zorgen dat de data dus daar staat, en dus niet in dat cache geheugen! Nadat je dus nieuwe waardes in de circular buffer geschreven hebt, moet je de data cache flushen met het commando:
c code:
SCB_CleanDCache();
Na het implementeren van deze drie dingen kreeg ik mooie sinusvormpjes uit mijn DAC. Hoera! Maar we zijn er nog niet. Een ADC stuurt data naar dezelfde SPI poort, en deze willen we dus ook uitlezen om zo verwerkt te kunnen worden in mijn programma. In de standaard HAL (Hardware Abstraction Libraries) drivers stond geen functie die dit deed, maar in de extended versie van de I2S driver stonde de HAL_I2SEx_TransmitReceive_DMA functie die ik nodig heb. Het is mij tot nu toe nog niet gelukt om via deze functie gegevens uit de SPI's RXDR register te krijgen. Hardwarematig werkt alles prima, want als ik handmatig datzelfde RXDR register uitlees, krijg ik mooi wisselende waardes (vier stuks van 32 bits). Daarna is alles wat ik eruit krijg 0. Andere mensen hebben dezelfde of andere problemen, en ik vermoed dan ook dat de drivers voor deze microcontroller nog vol in ontwikkeling zitten.