Vor einiger Zeit wollte ich Assembler lernen, und nachdem ich die einschlägige Literatur gelesen hatte, war es Zeit zum Üben. Eigentlich wird es weiter diskutiert. Zuerst übte ich auf Arduino Uno (Atmega328p), jetzt entschied ich mich weiterzumachen und nahm STM32 auf. STM32F103C8 fiel mir tatsächlich in die Hände und es werden weitere Experimente durchgeführt.
Die Werkzeuge
Ich habe die folgenden Tools verwendet:
- Notepad ++ - zum Schreiben von Code
- GNU Assembler Compiler
- STM32 ST-LINK-Dienstprogramm + ST-LINK V2 - zum Flashen des Codes auf dem Mikrocontroller und zum Debuggen
Starten Sie
Der Hauptzweck der Assemblersprache ist für mich das Lernen. Da Sie nie wissen, wo Sie auf ein anderes interessantes Problem stoßen können, wurde beschlossen, alles von Grund auf neu zu schreiben. Die Hauptaufgabe bestand darin, die Funktionsweise des Interrupt-Vektors zu verstehen. Im Gegensatz zu Atmega in STM32 enthält der Interrupt-Vektor keine Sprunganweisungen:
jmp main
Darin sind bestimmte Adressen geschrieben, und während der Unterbrechung ersetzt der Prozessor selbst die im Vektor im PC-Register angegebene Adresse. Hier ist ein Beispiel für meinen Interrupt-Vektor:
.org 0x00000000 SP: .word STACKINIT RESET: .word main NMI_HANDLER: .word nmi_fault HARD_FAULT: .word hard_fault MEMORY_FAULT: .word memory_fault BUS_FAULT: .word bus_fault USAGE_FAULT: .word usage_fault .org 0x000000B0 TIMER2_INTERRUPT: .word timer2_interupt + 1
Ich möchte den Leser darauf aufmerksam machen, dass die erste Zeile nicht der Rücksetzvektor ist, sondern die Werte, mit denen der Stapel initialisiert wird. Unmittelbar danach gibt es einen Rücksetzvektor, gefolgt von 5 obligatorischen Interruptvektoren (NMI_HANDLER - USAGE_FAULT).
Entwicklung
Das erste, woran ich festhielt, war die ARM-Assembler-Syntax. Sogar während des Studiums des Interrupt-Vektors stieß ich auf Hinweise darauf, dass ARM zwei Arten von Anweisungen hat: Daumen und nicht Daumen. Und dieser Cortex-M3 (STM32F103C8, nämlich Cortex-M3) unterstützt nur einen Satz von Thumb-Anweisungen. Ich habe Anweisungen streng nach der Dokumentation geschrieben, aber aus irgendeinem Grund hat der Assembler sie beschimpft.
nicht verschobenes Register erforderlich
Es stellte sich heraus, dass zu Beginn des Programms
.syntax vereinheitlicht
Dies teilt dem Assembler mit, dass Sie Thumb- und Nicht-Thumb-Anweisungen gleichzeitig verwenden können.
Als nächstes stieß ich auf die deaktivierten Standard-GPOI-Ports. Damit sie funktionieren, müssen Sie unter anderem die entsprechenden Werte in den RCC-Registern (Reset and Clock Control) festlegen. Ich habe PORT C verwendet. Es kann durch Setzen von Bit 4 (Nummerierung der Bits von Grund auf neu) in RCC_APB2ENR (Peripherie-Clock-Enable-Register 2) aktiviert werden.
Weitere blinkende LED. Zunächst müssen Sie wie in Arduino einen Pin für die Aufnahme setzen. Dies erfolgt über GPIOx_CRL (Steuerregister niedrig) oder GPIOx_CRH (Steuerregister hoch). Hier muss aufgehoben werden, dass für jeden Pin 4 Bits in einem dieser Register (32-Bit-Register) verantwortlich sind. 2 Bit (MODEy) bestimmen die maximale Datenrate und die 2 Bit (CNF) -Pin-Konfiguration. Ich habe PORT C Pin 14 verwendet, dafür habe ich die Bits [25:24] = 10 und die Bits [27:26] = 00 im GPIOx_CRH-Register gesetzt.
Damit die Diode brennt, müssen Sie das entsprechende Bit in GPIOx_ODR (Ausgangsdatenregister) setzen. In meinem Fall Bit 14. Dies könnte dieses einfache Beispiel beenden, indem eine Verzögerungsfunktion erstellt und alles in eine Schleife gestellt wird, aber ich konnte dies nicht tun. Ich habe beschlossen, Timer-Interrupts einzustellen ... Wie sich herausstellte, war es vergebens, vor allem, weil die Timer für diese Art von Aufgabe zu schnell sind.
Ich werde die Timer-Einstellungen, die sich für den Code auf
Github interessieren, nicht im Detail beschreiben. Die Idee war einfach: Senden Sie den Prozessor in einem Zyklus in den Leerlauf, beenden Sie den Leerlauf per Timer, um die LED ein- und auszuschalten, und erneut in den Leerlauf. Aber der Timer arbeitete viel schneller als ich es geschafft hatte, all das zu tun, weshalb ich einen zusätzlichen Zähler eingeben musste.
Der Zähler ist eine 32-Bit-Variable, die sich im SRAM befinden sollte. Und dann wartete ein weiterer Rechen auf mich. Als ich in Atmega programmierte, eine Variable in SRAM zu platzieren, stellte ich über .org die Adresse des Anfangs des Speichers ein, an dem der Datenblock tatsächlich platziert wurde. Nachdem ich ein wenig über die Speicherinitialisierung gelesen habe, bin ich mir nicht sicher, ob dies korrekt war, aber es hat funktioniert. Und ich habe beschlossen, dasselbe mit STM32 zu drehen. Die Speicherstartadresse in STM32F103C8 lautet 0x20000000. Und als ich .org an dieser Adresse gemacht habe, habe ich eine 512-MB-Binärdatei erhalten. Dies schickte mich ein paar Nächte, um Handbücher zu rauchen. Ich verstehe immer noch nicht 100%, wie das funktioniert, aber soweit ich verstehe, platziert der Abschnitt .data die Werte, mit denen die Variablen initialisiert werden müssen, in einer ausführbaren Datei, aber zur Laufzeit muss der Programmierer die Variablenwerte im Speicher initialisieren. Korrigieren Sie mich bitte, wenn ich falsch liege. Am Ende habe ich eine Variable wie diese erstellt:
.section .bss .offset 0x20000000 flash_counter: .word
Initialisiert es zu Beginn der Hauptfunktion und die LED blinkt. Ich hoffe dieser Artikel hilft jemandem. Wenn Sie Fragen haben, beantworte ich diese gerne.