Προγραμματισμός Stm32 σε C και τυπικές βιβλιοθήκες. Έλεγχος σήματος CS. Σωστή μεταφορά δεδομένων

Η αλληλεπίδραση του κώδικα χρήστη με τους καταχωρητές του πυρήνα και της περιφέρειας των μικροελεγκτών STM32 μπορεί να πραγματοποιηθεί με δύο τρόπους: χρησιμοποιώντας τυπικές βιβλιοθήκες ή χρησιμοποιώντας σύνολα αποσπασμάτων (υπαινιγμοί λογισμικού). Η επιλογή μεταξύ τους εξαρτάται από την ποσότητα της μνήμης του ελεγκτή, την απαιτούμενη ταχύτητα και το χρονικό πλαίσιο ανάπτυξης. Το άρθρο αναλύει τα δομικά χαρακτηριστικά, τα πλεονεκτήματα και τα μειονεκτήματα των συνόλων αποσπασμάτων για μικροελεγκτές των οικογενειών STM32F1 και STM32L0 που παράγονται από την STMicroelectronics.

Ένα από τα πλεονεκτήματα της χρήσης μικροελεγκτών STMicroelectronics είναι ένα ευρύ φάσμα εργαλείων ανάπτυξης: τεκμηρίωση, πίνακες ανάπτυξης, λογισμικό.

Το λογισμικό για το STM32 περιλαμβάνει ιδιόκτητο λογισμικό που παράγεται από την STMicroelectronics, πηγές ανοιχτού κώδικα και εμπορικό λογισμικό.

Το λογισμικό STMicroelectronics έχει σημαντικά πλεονεκτήματα. Πρώτα απ 'όλα, είναι διαθέσιμο για δωρεάν λήψη. Δεύτερον, οι βιβλιοθήκες λογισμικού παρουσιάζονται με τη μορφή πηγαίων κωδίκων - ο χρήστης μπορεί να τροποποιήσει ο ίδιος τον κώδικα, λαμβάνοντας υπόψη μικρούς περιορισμούς που περιγράφονται στη συμφωνία άδειας χρήσης.

Οι βιβλιοθήκες STMicroelectronics συμμορφώνονται με το ANSI-C και μπορούν να διαιρεθούν κατά επίπεδο αφαίρεσης (Εικόνα 1):

  • CMSIS (Core Peripheral Access Layer) – επίπεδο μητρώου πυρήνα και περιφερειακού, βιβλιοθήκη ARM.
  • Hardware Abstraction Layer – βιβλιοθήκες χαμηλού επιπέδου: τυπικές περιφερειακές βιβλιοθήκες, σύνολα αποσπασμάτων.
  • Middleware – βιβλιοθήκες μεσαίου επιπέδου: λειτουργικά συστήματα σε πραγματικό χρόνο (RTOS), συστήματα αρχείων, USB, TCP/IP, Bluetooth, Display, ZigBee, Touch Sensing και άλλα.
  • Πεδίο εφαρμογής – βιβλιοθήκες επιπέδου εφαρμογής: λύσεις ήχου, ελέγχου κινητήρα, αυτοκινητοβιομηχανίας και βιομηχανικές λύσεις.

Το σχήμα 1 δείχνει ότι για την αλληλεπίδραση με το επίπεδο CMSIS, το STMicroelectronics προσφέρει τη χρήση δύο κύριων εργαλείων - τυπικών βιβλιοθηκών και αποσπασμάτων.

Η τυπική βιβλιοθήκη είναι ένα σύνολο προγραμμάτων οδήγησης. Κάθε πρόγραμμα οδήγησης παρέχει στο χρήστη λειτουργίες και ορισμούς για εργασία με ένα συγκεκριμένο μπλοκ περιφερειακών (SPI, USART, ADC κ.λπ.). Ο χρήστης δεν αλληλεπιδρά άμεσα με καταχωρητές επιπέδου CMSIS.

Τα σετ αποσπασμάτων είναι εξαιρετικά αποτελεσματικά παραδείγματα προγραμματισμού, χρησιμοποιώντας άμεση πρόσβαση σε μητρώα CMSIS. Οι προγραμματιστές λογισμικού μπορούν να χρησιμοποιήσουν υλοποιήσεις των συναρτήσεων από αυτά τα παραδείγματα στον δικό τους κώδικα.

Κάθε μέθοδος έχει πλεονεκτήματα και μειονεκτήματα. Η επιλογή μεταξύ τους γίνεται λαμβάνοντας υπόψη τη διαθέσιμη ποσότητα FLASH και RAM, την απαιτούμενη ταχύτητα, την περίοδο ανάπτυξης, την εμπειρία των προγραμματιστών και άλλες συνθήκες.

επίπεδο CMSIS

Ο μικροελεγκτής είναι ένα σύνθετο ψηφιακό-αναλογικό τσιπ που αποτελείται από έναν πυρήνα επεξεργαστή, μνήμη, περιφερειακές μονάδες, ψηφιακούς διαύλους κ.λπ. Η αλληλεπίδραση με κάθε μπλοκ πραγματοποιείται χρησιμοποιώντας καταχωρητές.

Από την άποψη των προγραμματιστών, ένας μικροελεγκτής αντιπροσωπεύει ένα χώρο μνήμης. Δεν περιέχει μόνο RAM, FLASH και EEPROM, αλλά και καταχωρητές προγραμμάτων. Κάθε καταχωρητής υλικού αντιστοιχεί σε ένα κελί μνήμης. Έτσι, για να γράψει δεδομένα σε έναν καταχωρητή ή να αφαιρέσει την τιμή του, ο προγραμματιστής πρέπει να έχει πρόσβαση στην αντίστοιχη θέση στο χώρο διευθύνσεων.

Ένα άτομο έχει κάποιες ιδιαιτερότητες αντίληψης. Για παράδειγμα, τα συμβολικά ονόματα γίνονται αντιληπτά από αυτόν πολύ καλύτερα από τις διευθύνσεις των κελιών μνήμης. Αυτό είναι ιδιαίτερα αισθητό όταν χρησιμοποιείται μεγάλος αριθμός κυττάρων. Στους μικροελεγκτές ARM, ο αριθμός των καταχωρητών, άρα και των κυψελών που χρησιμοποιούνται, ξεπερνά τις χίλιες. Για να διευκολύνουμε τα πράγματα, είναι απαραίτητο να ορίσουμε συμβολικούς δείκτες. Αυτός ο προσδιορισμός γίνεται σε επίπεδο CMSIS.

Για παράδειγμα, για να ορίσετε την κατάσταση των ακίδων της θύρας A, πρέπει να γράψετε δεδομένα στον καταχωρητή GPIOA_ODR. Αυτό μπορεί να γίνει με δύο τρόπους - χρησιμοποιήστε έναν δείκτη με διεύθυνση κελιού 0xEBFF FCFF με μετατόπιση 0x14 ή χρησιμοποιήστε έναν δείκτη με το συμβολικό όνομα GPIOA και μια έτοιμη δομή που ορίζει τη μετατόπιση. Προφανώς, η δεύτερη επιλογή είναι πολύ πιο κατανοητή.

Το CMSIS εκτελεί και άλλες λειτουργίες. Υλοποιείται ως η ακόλουθη ομάδα αρχείων:

  • Το startup_stm32l0xx.s περιέχει τον κωδικό εκκίνησης του assembler για το Cortex-M0+ και έναν πίνακα διανυσμάτων διακοπής. Μετά την ολοκλήρωση της εκκίνησης, ο έλεγχος μεταφέρεται πρώτα στη συνάρτηση SystemInit() (θα δοθούν επεξηγήσεις παρακάτω) και μετά στην κύρια συνάρτηση int main(void).
  • Το stm32l0xx.h περιέχει ορισμούς που είναι απαραίτητοι για την εκτέλεση βασικών λειτουργιών bit και έναν ορισμό του τύπου του μικροεπεξεργαστή που χρησιμοποιείται.
  • system_stm32l0xx.c/.h. Μετά την αρχική προετοιμασία, εκτελείται η συνάρτηση SystemInit(). Εκτελεί την αρχική ρύθμιση των περιφερειακών του συστήματος, τους χρονισμούς του μπλοκ RCC.
  • stm32l0yyxx.h – αρχεία υλοποίησης για συγκεκριμένους μικροελεγκτές (για παράδειγμα, stm32l051xx.h). Σε αυτά ορίζονται δείκτες χαρακτήρων, δομές δεδομένων, σταθερές bit και μετατοπίσεις.

Αλληλεπίδραση με το CMSIS. Τυπικές βιβλιοθήκες και αποσπάσματα

Ο αριθμός των καταχωρητών για μικροελεγκτές STM32 στα περισσότερα μοντέλα ξεπερνά τις χίλιες. Εάν χρησιμοποιείτε άμεση πρόσβαση σε μητρώα, ο κωδικός χρήστη θα γίνει μη αναγνώσιμος και εντελώς άχρηστος για υποστήριξη και εκσυγχρονισμό. Αυτό το πρόβλημα μπορεί να λυθεί χρησιμοποιώντας την τυπική περιφερειακή βιβλιοθήκη.

Η τυπική περιφερειακή βιβλιοθήκη είναι ένα σύνολο προγραμμάτων οδήγησης χαμηλού επιπέδου. Κάθε πρόγραμμα οδήγησης παρέχει στο χρήστη ένα σύνολο λειτουργιών για την εργασία με μια περιφερειακή μονάδα. Με αυτόν τον τρόπο ο χρήστης χρησιμοποιεί συναρτήσεις αντί να έχει άμεση πρόσβαση στα μητρώα. Σε αυτήν την περίπτωση, το επίπεδο CMSIS είναι κρυφό από τον προγραμματιστή (Εικόνα 2α).

Ρύζι. 2. Αλληλεπίδραση με το CMSIS χρησιμοποιώντας την τυπική βιβλιοθήκη (a) και τα αποσπάσματα (b)

Για παράδειγμα, η αλληλεπίδραση με τις θύρες I/O στο STM32L0 υλοποιείται χρησιμοποιώντας ένα πρόγραμμα οδήγησης που έχει τη μορφή δύο αρχείων: stm32l0xx_hal_gpio.h και stm32l0xx_hal_gpio.c. Το stm32l0xx_hal_gpio.h παρέχει τους βασικούς ορισμούς τύπων και συναρτήσεων και το stm32l0xx_hal_gpio.c παρέχει την υλοποίησή τους.

Αυτή η προσέγγιση έχει αρκετά προφανή πλεονεκτήματα (Πίνακας 1):

  • Γρήγορη δημιουργία κώδικα. Ο προγραμματιστής δεν χρειάζεται να μελετήσει τη λίστα των καταχωρητών. Αρχίζει αμέσως να εργάζεται σε υψηλότερο επίπεδο. Για παράδειγμα, για άμεση διασύνδεση με τη θύρα I/O του STM32L0, πρέπει να γνωρίζετε και να μπορείτε να χειρίζεστε έντεκα καταχωρητές ελέγχου/κατάστασης, οι περισσότεροι από τους οποίους έχουν έως και 32 διαμορφώσιμα bit. Όταν χρησιμοποιείτε το πρόγραμμα οδήγησης της βιβλιοθήκης, αρκεί να κυριαρχήσετε οκτώ λειτουργίες.
  • Απλότητα και σαφήνεια του κώδικα. Ο κωδικός χρήστη δεν είναι φραγμένος με ονόματα καταχωρητών, μπορεί να είναι διαφανής και ευανάγνωστος, κάτι που είναι σημαντικό όταν εργάζεστε με μια ομάδα ανάπτυξης.
  • Υψηλό επίπεδο αφαίρεσης. Όταν χρησιμοποιείτε την τυπική βιβλιοθήκη, ο κώδικας αποδεικνύεται ότι είναι αρκετά ανεξάρτητος από την πλατφόρμα. Για παράδειγμα, εάν αλλάξετε τον μικροελεγκτή STM32L0 σε μικροελεγκτή STM32F0, δεν θα χρειαστεί να αλλάξει καθόλου ο κώδικας που λειτουργεί με τις θύρες I/O.

Πίνακας 1. Σύγκριση μεθόδων εφαρμογής προσαρμοσμένου κώδικα

Παράμετρος σύγκρισης Όταν χρησιμοποιείτε τυπικό
περιφερειακές βιβλιοθήκες
Όταν χρησιμοποιείτε σύνολα αποσπασμάτων
Μέγεθος κώδικα μέση τιμή ελάχιστο
Κόστος RAM μέση τιμή ελάχιστο
Εκτέλεση μέση τιμή ανώτατο όριο
Αναγνωσιμότητα κώδικα έξοχος χαμηλός
Επίπεδο ανεξαρτησίας πλατφόρμας μέση τιμή μικρός
Ταχύτητα δημιουργίας προγράμματος υψηλός χαμηλός

Η παρουσία ενός πρόσθετου κελύφους με τη μορφή προγραμμάτων οδήγησης έχει επίσης προφανή μειονεκτήματα (Πίνακας 1):

  • Αύξηση του όγκου του κώδικα προγράμματος. Οι λειτουργίες που υλοποιούνται στον κώδικα βιβλιοθήκης απαιτούν επιπλέον χώρο στη μνήμη.
  • Αυξημένο κόστος RAM λόγω της αύξησης του αριθμού των τοπικών μεταβλητών και της χρήσης ογκωδών δομών δεδομένων.
  • Μειωμένη απόδοση λόγω αυξημένου κόστους κατά την κλήση λειτουργιών βιβλιοθήκης.

Ήταν η παρουσία αυτών των ελλείψεων που οδήγησε στο γεγονός ότι ο χρήστης συχνά αναγκαζόταν να βελτιστοποιήσει τον κώδικα - να εφαρμόσει ανεξάρτητα λειτουργίες για αλληλεπίδραση με το CMSIS, να βελτιστοποιήσει τις λειτουργίες της βιβλιοθήκης αφαιρώντας όλα τα περιττά πράγματα, να αντιγράψει υλοποιήσεις συναρτήσεων βιβλιοθήκης απευθείας στον κώδικά του. χρησιμοποιήστε οδηγίες __INLINE για να αυξήσετε την ταχύτητα εκτέλεσης. Ως αποτέλεσμα, δαπανήθηκε επιπλέον χρόνος για τη βελτίωση του κώδικα.

Η STMicroelectronics, συναντώντας τους προγραμματιστές στα μισά του δρόμου, κυκλοφόρησε συλλογές αποσπασμάτων STM32SnippetsF0 και STM32SnippetsL0.

Τα αποσπάσματα περιλαμβάνονται στον κωδικό χρήστη (Εικόνα 2β).

Η χρήση αποσπασμάτων παρέχει προφανή πλεονεκτήματα:

  • αύξηση της αποτελεσματικότητας και της ταχύτητας του κώδικα.
  • μείωση του πεδίου εφαρμογής του προγράμματος·
  • Μείωση της ποσότητας μνήμης RAM που χρησιμοποιείται και του φορτίου στη στοίβα.

Ωστόσο, αξίζει να σημειωθούν τα μειονεκτήματα:

  • μείωση της απλότητας και της σαφήνειας του κώδικα λόγω της «μόλυνσής» του με ονόματα καταχωρητών και της ανεξάρτητης υλοποίησης λειτουργιών χαμηλού επιπέδου·
  • εξαφάνιση της ανεξαρτησίας της πλατφόρμας.

Επομένως, η επιλογή μεταξύ της τυπικής βιβλιοθήκης και των αποσπασμάτων δεν είναι προφανής. Στις περισσότερες περιπτώσεις, αξίζει να μιλήσουμε όχι για τον ανταγωνισμό, αλλά για την αμοιβαία χρήση τους. Στα αρχικά στάδια, για να δημιουργήσετε γρήγορα «όμορφο» κώδικα, είναι λογικό να χρησιμοποιείτε τυπικά προγράμματα οδήγησης. Εάν είναι απαραίτητη η βελτιστοποίηση, μπορείτε να στραφείτε σε έτοιμα αποσπάσματα, ώστε να μην χάνετε χρόνο αναπτύσσοντας τις δικές σας βέλτιστες λειτουργίες.

Οι τυπικές βιβλιοθήκες προγραμμάτων οδήγησης και αποσπασμάτων STM32F0 και STM32L0 (Πίνακας 2) είναι διαθέσιμες για δωρεάν λήψη στον ιστότοπο www.st.com.

Πίνακας 2. Βιβλιοθήκες χαμηλού επιπέδου για STM32F10 και STM32L0

Μια πιο στενή γνωριμία με τα αποσπάσματα, όπως και με οποιοδήποτε λογισμικό, θα πρέπει να ξεκινήσει εξετάζοντας τα χαρακτηριστικά της σύμβασης άδειας χρήσης.

Συμφωνία άδειας

Κάθε υπεύθυνος προγραμματιστής, πριν χρησιμοποιήσει προϊόντα λογισμικού τρίτων, μελετά προσεκτικά συμφωνία άδειας. Παρά το γεγονός ότι οι συλλογές αποσπασμάτων που παράγονται από την ST Microelectronics δεν απαιτούν άδεια χρήσης και είναι διαθέσιμες για δωρεάν λήψη, αυτό δεν σημαίνει ότι δεν υπάρχουν περιορισμοί στη χρήση τους.

Η άδεια χρήσης περιλαμβάνεται με όλα τα προϊόντα με δυνατότητα λήψης ελεύθερα που κατασκευάζονται από την STMicroelectronics. Μετά τη λήψη των STM32SnippetsF0 και STM32SnippetsL0 στον ριζικό κατάλογο, είναι εύκολο να βρείτε το έγγραφο MCD-ST Liberty SW License Agreement V2.pdf, το οποίο εισάγει τον χρήστη στους κανόνες χρήσης αυτού του λογισμικού.

Ο φάκελος Project περιέχει υποκαταλόγους με παραδείγματα για συγκεκριμένες περιφερειακές μονάδες, έτοιμα έργα για ARM Keil και EWARM, καθώς και αρχεία main.c.

Εκκίνηση και δυνατότητες χρήσης συνόλων αποσπασμάτων STM32SnippetsF0 και STM32SnippetsL0

Ένα ιδιαίτερο χαρακτηριστικό αυτών των συνόλων αποσπασμάτων είναι η εξάρτησή τους από την πλατφόρμα. Έχουν σχεδιαστεί για να λειτουργούν με συγκεκριμένες σανίδες. Το STM32SnippetsL0 χρησιμοποιεί την πλακέτα Discovery STM32L053 και το STM32SnippetsF0 χρησιμοποιεί την πλακέτα Discovery STM32F072.

Όταν χρησιμοποιείτε ιδιόκτητους πίνακες, ο κώδικας και τα σχέδια πρέπει να τροποποιηθούν, αυτό θα συζητηθεί με περισσότερες λεπτομέρειες στην τελευταία ενότητα.

Για να εκτελέσετε το παράδειγμα, πρέπει να ολοκληρώσετε μια σειρά από βήματα:

  • εκτελέστε το ολοκληρωμένο έργο από τον κατάλογο με το απαιτούμενο παράδειγμα. Για απλότητα, μπορείτε να χρησιμοποιήσετε έτοιμα έργα για τα περιβάλλοντα ARM Keil ή EWARM, που βρίσκονται στο φάκελο MDK-ARM\ και EWARM\, αντίστοιχα.
  • ενεργοποιήστε την πλακέτα ανάπτυξης STM32L053 Discovery/STM32F072 Discovery.
  • Συνδέστε το τροφοδοτικό της πλακέτας εντοπισμού σφαλμάτων στον υπολογιστή χρησιμοποιώντας ένα καλώδιο USB. Χάρη στον ενσωματωμένο εντοπισμό σφαλμάτων ST-Link/V2, δεν απαιτείται επιπλέον προγραμματιστής.
  • άνοιγμα, διαμόρφωση και εκτέλεση του έργου.
    • Για το ARM Keil:
      • ανοιχτό έργο?
      • μεταγλώττιση του έργου – Έργο → Ανοικοδόμηση όλων των αρχείων προορισμού.
      • φορτώστε το στον ελεγκτή – Εντοπισμός σφαλμάτων → Έναρξη/Διακοπή συνεδρίας εντοπισμού σφαλμάτων.
      • εκτελέστε το πρόγραμμα στο παράθυρο Εντοπισμός σφαλμάτων → Εκτέλεση (F5).
    • Για το EWARM:
      • ανοιχτό έργο?
      • μεταγλώττιση του έργου – Έργο → Ανοικοδόμηση όλων.
      • φορτώστε το στον ελεγκτή – Έργο → Εντοπισμός σφαλμάτων.
      • εκτελέστε το πρόγραμμα στο παράθυρο Debug → Go (F5).
  • διενεργούν δοκιμές σύμφωνα με τον αλγόριθμο που περιγράφεται στην κύρια.γ.

Για να αναλύσετε τον κώδικα του προγράμματος, εξετάστε ένα συγκεκριμένο παράδειγμα από το STM32SnippetsL0: Projects\LPUART\01_WakeUpFromLPM\.

Εκτελώντας ένα παράδειγμα για το LPUART

Ένα ιδιαίτερο χαρακτηριστικό των νέων μικροελεγκτών της οικογένειας STM32L0 που βασίζονται στον πυρήνα Cortex-M0+ είναι η δυνατότητα δυναμικής αλλαγής της κατανάλωσης λόγω μεγάλου αριθμού καινοτομιών. Μία από αυτές τις καινοτομίες ήταν η εμφάνιση περιφερειακών Low Power: ο χρονοδιακόπτης LPTIM 16-bit και ο πομποδέκτης LPUART. Αυτά τα μπλοκ έχουν δυνατότητες χρονισμού που είναι ανεξάρτητες από το χρονισμό του κύριου περιφερειακού διαύλου APB. Εάν είναι απαραίτητο να μειωθεί η κατανάλωση ρεύματος, η συχνότητα λειτουργίας του διαύλου APB (PCLK) μπορεί να μειωθεί και ο ίδιος ο ελεγκτής μπορεί να τεθεί σε λειτουργία χαμηλής κατανάλωσης. Ταυτόχρονα, τα περιφερειακά χαμηλής ισχύος συνεχίζουν να λειτουργούν με τη μέγιστη απόδοση.

Ας εξετάσουμε ένα παράδειγμα από τον κατάλογο Projects\LPUART\01_WakeUpFromLPM\, το οποίο εξετάζει τη δυνατότητα ανεξάρτητης λειτουργίας του LPUART σε λειτουργία χαμηλής κατανάλωσης.

Όταν ανοίγετε ένα έργο στο περιβάλλον ARM Keil, εμφανίζονται μόνο τρία αρχεία: startup_stm32l053xx.s, system_stm32l0xx.c και main.c (Εικόνα 4). Εάν χρησιμοποιήθηκε η τυπική βιβλιοθήκη, θα ήταν απαραίτητο να προσθέσετε αρχεία προγραμμάτων οδήγησης στο έργο.

Λειτουργία και ανάλυση της δομής του αρχείου Main.c

Το επιλεγμένο παράδειγμα προγράμματος εκτελείται σε διάφορα στάδια.

Μετά την έναρξη, εκκινείται η συνάρτηση SystemInit(), που υλοποιείται στο system_stm32l0xx.c. Ρυθμίζει τις παραμέτρους του μπλοκ ρολογιού RCC (χρονισμούς και συχνότητες λειτουργίας). Στη συνέχεια, ο έλεγχος μεταφέρεται στην κύρια συνάρτηση int main(void). Αρχικοποιεί τα περιφερειακά του χρήστη - θύρες εισόδου/εξόδου, LPUART - μετά την οποία ο ελεγκτής μεταβαίνει στη λειτουργία STOP χαμηλής κατανάλωσης. Σε αυτό, η συνηθισμένη περιφέρεια και ο πυρήνας έχουν σταματήσει, λειτουργεί μόνο το LPUART. Περιμένει την έναρξη της μεταφοράς δεδομένων από την εξωτερική συσκευή. Κατά την άφιξη λίγο έναρξηΤο LPUART ξυπνά το σύστημα και λαμβάνει το μήνυμα. Η λήψη συνοδεύεται από τρεμόπαιγμα του LED της πλακέτας εντοπισμού σφαλμάτων. Μετά από αυτό, ο ελεγκτής επιστρέφει στην κατάσταση STOP και περιμένει για την επόμενη μεταφορά δεδομένων εάν δεν εντοπιστούν σφάλματα.

Η μεταφορά δεδομένων πραγματοποιείται χρησιμοποιώντας μια εικονική θύρα COM και πρόσθετο λογισμικό.

Ας δούμε το main.c από το έργο μας. Αυτό το αρχείο είναι ένα τυπικό αρχείο C. Το κύριο χαρακτηριστικό του είναι η αυτο-τεκμηρίωση - η παρουσία λεπτομερών σχολίων, εξηγήσεων και συστάσεων. Το επεξηγηματικό μέρος περιλαμβάνει διάφορες ενότητες:

  • έναν τίτλο που υποδεικνύει το όνομα του αρχείου, την έκδοση, την ημερομηνία, τον συγγραφέα και μια σύντομη εξήγηση του σκοπού·
  • περιγραφή της σειράς ρύθμισης των περιφερειακών του συστήματος (ειδικά χαρακτηριστικά RCC): FLASH, RAM, συστήματα τροφοδοσίας και χρονισμού, περιφερειακοί διαύλους κ.λπ.
  • κατάλογος πόρων μικροελεγκτή που χρησιμοποιούνται (MCU Resources).
  • μια σύντομη εξήγηση του τρόπου χρήσης αυτού του παραδείγματος.
  • μια σύντομη εξήγηση της δοκιμής του παραδείγματος και του αλγόριθμου για την υλοποίησή του (Πώς να δοκιμάσετε αυτό το παράδειγμα).

Η λειτουργία int main(void) έχει συμπαγή μορφή και είναι εξοπλισμένη με σχόλια, τα οποία στη Λίστα 1, για μεγαλύτερη σαφήνεια, μεταφράζονται στα ρωσικά.

Λίστα 1. Παράδειγμα υλοποίησης της κύριας συνάρτησης

int main (κενό)
{
/* Μέχρι την έναρξη της εκτέλεσης αυτού του τμήματος, όταν οι μονάδες συστήματος έχουν ήδη διαμορφωθεί στη συνάρτηση SystemInit(), που υλοποιείται στο system_stm32l0xx.c. */
/* διαμόρφωση περιφερειακών μονάδων */
Configure_GPIO_LED();
Configure_GPIO_LPUART();
Configure_LPUART();
Configure_LPM_Stop();
/* έλεγχος για σφάλματα κατά τη λήψη */
ενώ (!λάθος) /* ατελείωτος βρόχος */
{
/* περιμένετε να είναι έτοιμο το LPUART και μεταβείτε στη λειτουργία STOP */
if((LPUART1->ISR & USART_ISR_REACK) == USART_ISR_REACK)
{
__WFI();
}
}
/* όταν παρουσιαστεί σφάλμα */
SysTick_Config(2000); /* ρύθμιση της περιόδου διακοπής του χρονοδιακόπτη συστήματος σε 1 ms */
while(1);
}

Το αρχείο main.c δηλώνει και ορίζει τις περιφερειακές λειτουργίες διαμόρφωσης και δύο λειτουργίες χειρισμού διακοπών. Ας εξετάσουμε τα χαρακτηριστικά τους.

Το παρακάτω παράδειγμα χρησιμοποιεί τέσσερις συναρτήσεις διαμόρφωσης (Λίστα 2). Όλα δεν έχουν ορίσματα και δεν επιστρέφουν τιμές. Ο κύριος σκοπός τους είναι η γρήγορη και με τον μικρότερο αριθμό κωδικού που απαιτείται για την προετοιμασία των περιφερειακών. Αυτό επιτυγχάνεται μέσω δύο χαρακτηριστικών: της χρήσης άμεσης πρόσβασης σε μητρώα και της χρήσης της οδηγίας __INLINE (Λίστα 3).

Λίστα 2. Δήλωση λειτουργιών διαμόρφωσης περιφερειακών

void Configure_GPIO_LED(void);
void Configure_GPIO_LPUART(void);
void Configure_LPUART(void);
void Configure_LPM_Stop(void);

Λίστα 3. Παράδειγμα υλοποίησης της συνάρτησης __INLINE με άμεση πρόσβαση σε καταχωρητές LPUART

INLINE void Configure_LPUART(κενό)
{
/* (1) Ενεργοποίηση ρολογιού διεπαφής ισχύος */
/* (2) Απενεργοποιήστε τη δημιουργία αντιγράφων ασφαλείας του μητρώου προστασίας για να επιτρέψετε την πρόσβαση στον τομέα ρολογιού RTC */
/* (3) LSE στις */
/* (4) Περιμένετε έτοιμο LSE */
/* (5) Ενεργοποιήστε τη δημιουργία αντιγράφων ασφαλείας μητρώου προστασίας για να επιτρέψετε την πρόσβαση στον τομέα ρολογιού RTC */
/* (6) LSE χαρτογραφημένο στο LPUART */
/* (7) Ενεργοποίηση του περιφερειακού ρολογιού LPUART */
/* Διαμόρφωση LPUART */
/* (8) υπερδειγματοληψία κατά 16, 9600 baud */
/* (9) 8 bit δεδομένων, 1 bit έναρξης, 1 bit διακοπής, χωρίς ισοτιμία, λειτουργία λήψης, λειτουργία διακοπής */
/* (10) Ορισμός προτεραιότητας για LPUART1_IRQn */
/* (11) Ενεργοποίηση LPUART1_IRQn */
RCC->APB1ENR |= (RCC_APB1ENR_PWREN); /* (1) */
PWR->CR |= PWR_CR_DBP; /* (2) */
RCC->CSR |= RCC_CSR_LSEON; /* (3) */
ενώ ((RCC->CSR & (RCC_CSR_LSERDY)) != (RCC_CSR_LSERDY)) /*(4)*/
{
/* προσθέστε time out εδώ για μια ισχυρή εφαρμογή */
}
PWR->CR &=~ PWR_CR_DBP; /* (5) */
RCC->CCIPR |= RCC_CCIPR_LPUART1SEL; /* (6) */
RCC->APB1ENR |= RCC_APB1ENR_LPUART1EN; /*(7) */
LPUART1->BRR = 0x369; /* (8) */
LPUART1->CR1 = USART_CR1_UESM | USART_CR1_RXNEIE | USART_CR1_RE | USART_CR1_UE; /* (9) */
NVIC_SetPriority(LPUART1_IRQn, 0); /* (10) */
NVIC_EnableIRQ(LPUART1_IRQn); /* (έντεκα) */
}

Οι χειριστές διακοπής από το χρονόμετρο συστήματος και από το LPUART χρησιμοποιούν επίσης άμεση πρόσβαση σε καταχωρητές.

Έτσι, η επικοινωνία με το CMSIS πραγματοποιείται χωρίς τυπική βιβλιοθήκη. Ο κώδικας αποδεικνύεται συμπαγής και εξαιρετικά αποδοτικός. Ωστόσο, η αναγνωσιμότητά του θα επιδεινωθεί σημαντικά λόγω της πληθώρας προσβάσεων στα μητρώα.

Χρησιμοποιώντας αποσπάσματα στις δικές σας εξελίξεις

Τα προτεινόμενα σύνολα αποσπασμάτων έχουν περιορισμούς: είναι απαραίτητο να χρησιμοποιήσετε τον πίνακα εντοπισμού STM32L053 για το STM32SnippetsL0 και τον πίνακα εντοπισμού STM32F072 για τον πίνακα STM32SnippetsF0.

Για να χρησιμοποιήσετε αποσπάσματα στις εξελίξεις σας, θα χρειαστεί να κάνετε ορισμένες αλλαγές. Πρώτα, πρέπει να ρυθμίσετε εκ νέου το έργο για τον επιθυμητό επεξεργαστή. Για να το κάνετε αυτό, πρέπει να αλλάξετε το αρχείο εκκίνησης startup_stm32l053xx.s στο αρχείο άλλου ελεγκτή και να ορίσετε την απαιτούμενη σταθερά: STM32L051xx, STM32L052xx, STM32L053xx, STM32L062xx, STM32L032xLx06, STM32L063xLx06 31, STM32F051 και άλλα. Μετά από αυτό, κατά τη μεταγλώττιση του stm32l0xx.h, θα συνδεθεί αυτόματα απαιτούμενο αρχείομε ορισμό περιφερειακών ελεγκτών stm32l0yyxx.h (stm32l051xx.h/stm32l052xx.h/stm32l053xx.h/stm32l061xx.h/stm32l062xx.h/stm32l063). Δεύτερον, πρέπει να επιλέξετε τον κατάλληλο προγραμματιστή στις ρυθμίσεις ιδιοτήτων του έργου. Τρίτον, αλλάξτε τον κωδικό των συναρτήσεων από τα παραδείγματα εάν δεν πληρούν τις απαιτήσεις της εφαρμογής χρήστη.

συμπέρασμα

Σύνολα αποσπασμάτων και τυπικές περιφερειακές βιβλιοθήκες που παράγονται από την ST Microelectronics δεν αλληλοαποκλείονται. Συμπληρώνουν το ένα το άλλο, προσθέτοντας ευελιξία κατά τη δημιουργία εφαρμογών.

Η τυπική βιβλιοθήκη σάς επιτρέπει να δημιουργείτε γρήγορα καθαρό κώδικα με υψηλό επίπεδο αφαίρεσης.

Τα αποσπάσματα σάς επιτρέπουν να βελτιώσετε την αποτελεσματικότητα του κώδικα - να αυξήσετε την απόδοση και να μειώσετε την κατειλημμένη μνήμη FLASH και RAM.

Βιβλιογραφία

  1. Σύνοψη δεδομένων. STM32SnippetsF0. Πακέτο υλικολογισμικού STM32F0xx Snippets. Στροφή μηχανής. 1. – ST Microelectronics, 2014.
  2. Σύνοψη δεδομένων. STM32SnippetsL0. Πακέτο υλικολογισμικού STM32F0xx Snippets. Στροφή μηχανής. 1. – ST Microelectronics, 2014.
  3. Συμφωνία άδειας χρήσης MCD-ST Liberty SW V2.pdfΗλεκτρομηχανικά Ρελέ. Τεχνικές πληροφορίες. – ST Microelectronics, 2011.
  4. Σύνοψη δεδομένων. 32L0538DISCOVERY Κιτ Discovery για μικροελεγκτές STM32L053. Στροφή μηχανής. 1. – ST Microelectronics, 2014.
  5. http://www.st.com/.
Σχετικά με την ST Microelectronics

Γεια σε όλους. Όπως θυμάστε στο τελευταίο άρθρο, διαμορφώσαμε το πακέτο λογισμικού ώστε να λειτουργεί με μικροελεγκτές STM32 και μεταγλωττίσαμε το πρώτο πρόγραμμα. Σε αυτό το post θα εξοικειωθούμε με την αρχιτεκτονική αυτής της πλακέτας, τον μικροελεγκτή και τις διαθέσιμες βιβλιοθήκες για εργασία.

Παρακάτω είναι μια εικόνα του πίνακα STM32F3 Discovery , όπου: 1 — Αισθητήρας MEMS. Ψηφιακό γυροσκόπιο 3 αξόνων L3GD20. 2 - Σύστημα MEMS σε θήκη που περιέχει ψηφιακό γραμμικό επιταχυνσιόμετρο 3 αξόνων και ψηφιακό γεωμαγνητικό αισθητήρα 3 αξόνων LSM303DLHC. 4 – LD1 (PWR) – Τροφοδοτικό 3,3V. 5 – LD2 – κόκκινο/πράσινο LED. Η προεπιλογή είναι κόκκινο. Πράσινο σημαίνει επικοινωνία μεταξύ ST-LINK/v2 (ή V2-B) και υπολογιστή. Έχω ST-LINK/v2-B, καθώς και προσαρμοσμένη ένδειξη θύρας USB. 6. -LD3/10 (κόκκινο), LD4/9 (μπλε), LD5/8 (πορτοκαλί) και LD6/7 (πράσινο). Στην τελευταία ανάρτηση αναβοσβήσαμε το LD4 LED. 7. – Δύο κουμπιά: χρήστη USER και RESET. 8. - USB USER με υποδοχή Mini-B.

9 - USB Debugger/προγραμματιστής ST-LINK/V2. 1 0. - Μικροελεγκτής STM32F303VCT6. 11. — Εξωτερική γεννήτρια υψηλής συχνότητας 8 MHz. 12. – Θα πρέπει να υπάρχει μια γεννήτρια χαμηλής συχνότητας εδώ, δυστυχώς δεν είναι συγκολλημένη. 13. – SWD – διεπαφή. 14. – Τα jumper για την επιλογή προγραμματισμού εξωτερικών ή εσωτερικών ελεγκτών, στην πρώτη περίπτωση πρέπει να αφαιρεθούν. 15 – Jumper JP3 – ένα βραχυκυκλωτήρα σχεδιασμένο να συνδέει ένα αμπερόμετρο για τη μέτρηση της κατανάλωσης του ελεγκτή. Είναι σαφές ότι αν διαγραφεί, τότε η πέτρα μας δεν θα ξεκινήσει. 16. – STM32F103C8T6 υπάρχει μια πλακέτα εντοπισμού σφαλμάτων σε αυτήν. 17. — LD3985M33R Ρυθμιστής με χαμηλή πτώση τάσης και επίπεδο θορύβου, 150mA, 3,3V.

Τώρα ας ρίξουμε μια πιο προσεκτική ματιά στην αρχιτεκτονική του μικροελεγκτή STM32F303VCT6. Τα τεχνικά χαρακτηριστικά του: θήκη LQFP-100, πυρήνας ARM Cortex-M4, μέγιστη συχνότητα πυρήνα 72 MHz, χωρητικότητα μνήμης προγράμματος 256 KB, τύπος μνήμης προγράμματος FLASH, χωρητικότητα RAM SRAM 40 KB, RAM 8 KB, αριθμός εισόδων/εξόδων 87, διεπαφές ( CAN, I²C, IrDA, LIN, SPI, UART/USART, USB), περιφερειακά (DMA, I2S, POR, PWM, WDT), ADC/DAC 4*12 bit/2*12bit, τάση τροφοδοσίας 2 ... 3,6 V, θερμοκρασία λειτουργίας –40...+85 C. Στο παρακάτω σχήμα υπάρχει ένα pinout, όπου βλέπουμε 87 θύρες I/O, 45 από αυτές Normal I/Os (TC, TTa), 42 5-volt tolerant I /Os (FT, FTf) – συμβατό με 5 V (στην πλακέτα υπάρχουν ακροδέκτες 5V στα δεξιά, 3,3V στα αριστερά). Κάθε ψηφιακή γραμμή I/O μπορεί να χρησιμεύσει ως γενική γραμμή I/O.
ραντεβού ή εναλλακτική λειτουργία. Καθώς τα έργα προχωρούν, σταδιακά θα εξοικειωνόμαστε με την περιφέρεια.

Εξετάστε το μπλοκ διάγραμμα παρακάτω. Η καρδιά είναι ένας πυρήνας 32-bit ARM Cortex-M4 που λειτουργεί έως και 72 MHz. Διαθέτει ενσωματωμένη μονάδα FPU κινητής υποδιαστολής και μονάδα προστασίας μνήμης MPU, ενσωματωμένα κελιά ανίχνευσης μακροεντολών - Embedded Trace Macrocell (ETM), τα οποία μπορούν να χρησιμοποιηθούν για την παρακολούθηση της διαδικασίας εκτέλεσης του κύριου προγράμματος μέσα στον μικροελεγκτή. Έχουν τη δυνατότητα να εξάγουν συνεχώς αυτές τις παρατηρήσεις μέσω των επαφών ETM για όσο διάστημα η συσκευή λειτουργεί. NVIC (Ένθετος διανυσματικός ελεγκτής διακοπής) – μονάδα ελέγχου διακοπής. TPIU (Trace Port Interface Unit). Περιέχει Μνήμη FLASH–256 KB, SRAM 40 KB, RAM 8 KB. Μεταξύ του πυρήνα και της μνήμης υπάρχει μια μήτρα Bus, η οποία επιτρέπει στις συσκευές να συνδέονται απευθείας. Επίσης εδώ βλέπουμε δύο τύπους μήτρας διαύλου AHB και APB, όπου ο πρώτος είναι πιο παραγωγικός και χρησιμοποιείται για επικοινωνία υψηλής ταχύτητας εσωτερικά εξαρτήματα, και το τελευταίο είναι για περιφερειακά (συσκευές εισόδου/εξόδου). Ο ελεγκτής διαθέτει 4 12-bit ADC (ADC) (5Mbit/s) και έναν αισθητήρα θερμοκρασίας, 7 συγκριτές (GP Comparator1...7), 4 προγραμματιζόμενους λειτουργικούς ενισχυτές (OpAmp1...4) (PGA (Programmable Gain Array) ), 2 κανάλια DAC 12 bit (DAC), RTC (ρολόι πραγματικού χρόνου), δύο χρονόμετρα παρακολούθησης - ανεξάρτητα και με παράθυρο (WinWatchdog και Ind. WDG32K), 17 χρονόμετρα γενικής χρήσης και πολλαπλών λειτουργιών.

Σε γενικές γραμμές, εξετάσαμε την αρχιτεκτονική του ελεγκτή. Τώρα κοιτάξτε τις διαθέσιμες βιβλιοθήκες λογισμικού. Έχοντας κάνει μια επισκόπηση, μπορούμε να επισημάνουμε τα εξής: CMSIS, SPL και HAL. Ας δούμε το καθένα χρησιμοποιώντας ένα απλό παράδειγμα αναβοσβήνει ένα LED.

1). CMSIS(Cortex Microcontroller Software Interface Standard) - τυπική βιβλιοθήκη για Cortex®-M. Παρέχει υποστήριξη συσκευών και απλοποιεί τις διεπαφές λογισμικού. Το CMSIS παρέχει συνεπείς και απλές διεπαφές στον πυρήνα, τα περιφερειακά του και τα λειτουργικά συστήματα σε πραγματικό χρόνο. Η χρήση του είναι ένας επαγγελματικός τρόπος για να γράψετε προγράμματα, γιατί... περιλαμβάνει απευθείας εγγραφή σε μητρώα και, κατά συνέπεια, είναι απαραίτητη η συνεχής ανάγνωση και μελέτη των φύλλων δεδομένων. Ανεξάρτητο από τον κατασκευαστή του υλικού.
Το CMSIS περιλαμβάνει τα ακόλουθα στοιχεία:
- CMSIS-CORE: Συνεπής εκκίνηση συστήματος και περιφερειακή πρόσβαση.
- CMSIS-RTOS: Deterministic Real-Time Software Execution (Deterministic execution of real-time software);
— CMSIS-DSP: Γρήγορη υλοποίηση της επεξεργασίας ψηφιακού σήματος.
- CMSIS-Driver: Γενικές περιφερειακές διεπαφές για ενδιάμεσο λογισμικό και κώδικα εφαρμογής (Γενικές περιφερειακές διεπαφές για ενδιάμεσο λογισμικό και κώδικας εφαρμογής).
— CMSIS-Pack: Εύκολη πρόσβαση σε επαναχρησιμοποιήσιμα στοιχεία λογισμικού (Εύκολη πρόσβαση σε επαναχρησιμοποιήσιμα στοιχεία λογισμικού).
- CMSIS-SVD: Συνεπής προβολή της συσκευής και των περιφερειακών περιφερειακές συσκευές);
- CMSIS-DAP: Συνδεσιμότητα με υλικό αξιολόγησης χαμηλού κόστους. Λογισμικό εντοπισμού σφαλμάτων.

Για παράδειγμα, ας γράψουμε ένα πρόγραμμα - αναβοσβήνει ένα LED. Για αυτό χρειαζόμαστε τεκμηρίωση που να περιγράφει τα μητρώα. Στην περίπτωσή μου RM0316 Εγχειρίδιο αναφοράς STM32F303xB/C/D/E, STM32F303x6/8, STM32F328x8, STM32F358xC, STM32F398xE προηγμένες MCU βασισμένες σε ARM®, καθώς και περιγραφή του συγκεκριμένου σκέλους για το οποίο είναι DS9118: Cortex®-M4 32b MCU+FPU που βασίζεται σε ARM®, έως 256KB Flash+ 48KB SRAM, 4 ADC, 2 ch. DAC, 7 comp, 4 PGA, χρονόμετρα, 2,0-3,6 V.Αρχικά, θα χρονομετρήσουμε τη θύρα στο πρόγραμμα, επειδή Από προεπιλογή, τα πάντα είναι απενεργοποιημένα, γεγονός που επιτυγχάνει μειωμένη κατανάλωση ενέργειας. Ανοίξτε το εγχειρίδιο αναφοράς και δείτε την ενότητα Επαναφορά και έλεγχος ρολογιού, στη συνέχεια χάρτη εγγραφής RCC και δείτε ποιος καταχωρητής είναι υπεύθυνος για την ενεργοποίηση του IOPEEN

Ας περάσουμε στην περιγραφή του χρονισμού των περιφερειακών αυτού του μητρώου Καταχωρητής ενεργοποίησης περιφερειακού ρολογιού AHB (RCC_AHBENR), όπου βλέπουμε ότι αυτή η θύρα είναι κάτω από το 21ο bit. Ενεργοποιήστε το RCC->AHBENR|=(1<<21) . Далее сконфигурируем регистры GPIO. Нас интересует три: GPIOE_MODER и GPIOx_ODR . C помощью них повторим программу с предыдущей статьи, затактируем PE8. Первый отвечает за конфигурацию входа выхода, выбираем 01: General purpose output mode. GPIOE->MODER|=0×10000 . Το δεύτερο είναι για ενεργοποίηση του χαμηλού/υψηλού επιπέδου στο πόδι. Παρακάτω το πρόγραμμα:

#include "stm32f3xx.h " //Αρχείο κεφαλίδας μικροελεγκτή
ανυπόγραφο int i?
void delay() (
για (i=0;i<500000;i++);
}
int main (κενό) (
RCC->AHBENR|=(1<<21);
GPIOE->MODER|=0×10000;
ενώ (1)(
καθυστέρηση();
GPIOE->ODR|=0×100;
καθυστέρηση();
GPIOE->ODR&=~(0×100);
} }

2). SPL(Τυπική βιβλιοθήκη περιφερειακών)- αυτή η βιβλιοθήκη προορίζεται να συνδυάσει όλους τους επεξεργαστές της ST Electronics. Σχεδιασμένο για τη βελτίωση της φορητότητας του κώδικα και απευθύνεται κυρίως σε αρχάριους προγραμματιστές. Η ST εργάζεται για μια αντικατάσταση του SPL που ονομάζεται "χαμηλό στρώμα" που είναι συμβατό με το HAL. Τα προγράμματα οδήγησης Low Layer (LL) έχουν σχεδιαστεί για να παρέχουν ένα σχεδόν ελαφρύ επίπεδο, προσανατολισμένο στους ειδικούς, το οποίο είναι πιο κοντά στο υλικό από το HAL. Εκτός από το HAL, διατίθενται επίσης LL API. Ένα παράδειγμα του ίδιου προγράμματος στο SPL.

#περιλαμβάνω
#περιλαμβάνω
#περιλαμβάνω
#define LED GPIO_Pin_8
int main() (
μακρύ i?
GPIO_InitTypeDef gpio;
// Η μπλε λυχνία LED είναι συνδεδεμένη στη θύρα E, pin 8 (διαύλου AHB)
RCC_AHBPeriphClockCmd(RCC_AHBPeriph_GPIOE, ENABLE);
// Διαμόρφωση θύρας E (LED)
GPIO_StructInit(&gpio); //δηλώνει και προετοιμάζει μια μεταβλητή δομής δεδομένων
gpio.GPIO_Mode = GPIO_Mode_OUT;
gpio.GPIO_Pin = LED;
GPIO_Init(GPIOE, &gpio);
// LED που αναβοσβήνουν
ενώ (1) (
//Επί
GPIO_SetBits(GPIOE, LED);
για (i = 0; i< 500000; i++);
// Όλα εκτός
GPIO_ResetBits (GPIOE, LED);
για (i = 0; i< 500000; i++);
} }

Κάθε λειτουργία περιγράφεται στην τεχνική τεκμηρίωση UM1581 Εγχειρίδιο χρήστη Περιγραφή της τυπικής περιφερειακής βιβλιοθήκης STM32F30xx/31xx. Εδώ συνδέουμε τρία αρχεία κεφαλίδας που περιέχουν τα απαραίτητα δεδομένα, δομές, λειτουργίες ελέγχου επαναφοράς και συγχρονισμού, καθώς και για τη διαμόρφωση των θυρών εισόδου/εξόδου.

3). HAL- (Επίπεδο πρόσβασης υλικού, Επίπεδο αφαίρεσης υλικού)- Μια άλλη κοινή βιβλιοθήκη για ανάπτυξη. Με το οποίο κυκλοφόρησε και το πρόγραμμα CubeMX για τη διαμόρφωση που χρησιμοποιήσαμε στο τελευταίο άρθρο. Εκεί γράψαμε επίσης ένα πρόγραμμα για να αναβοσβήνει ένα LED χρησιμοποιώντας αυτήν τη βιβλιοθήκη. Όπως βλέπουμε στο παρακάτω σχήμα, ο κύβος δημιουργεί προγράμματα οδήγησης HAL και CMSIS. Λοιπόν, ας περιγράψουμε τα κύρια αρχεία που χρησιμοποιήθηκαν:
- system_stm32f3x.c και system_stm32f3x.h- παρέχει ελάχιστα σύνολα λειτουργιών για τη διαμόρφωση του συστήματος χρονισμού.
— core_cm4.h – παρέχει πρόσβαση σε καταχωρητές του πυρήνα και των περιφερειακών του.
- stm32f3x.h - αρχείο κεφαλίδας μικροελεγκτή.
— startup_system32f3x.s — κωδικός εκκίνησης, περιέχει έναν πίνακα διανυσμάτων διακοπής κ.λπ.

#include "main.h"
#include "stm32f3xx_hal.h"
void SystemClock_Config(void); /*Δήλωση συναρτήσεων διαμόρφωσης ρολογιού*/
static void MX_GPIO_Init(void); /*Αρχικοποίηση I/O*/
int main (κενό) (
/*Επαναφορά όλων των περιφερειακών, Εκκινεί τη διεπαφή Flash και το Systick.*/
HAL_Init();
/* Διαμόρφωση του ρολογιού συστήματος */
SystemClock_Config();
/* Αρχικοποίηση όλων των διαμορφωμένων περιφερειακών */
MX_GPIO_Init();
ενώ (1) (
HAL_GPIO_TogglePin(GPIOE, GPIO_PIN_8); //Αλλαγή της κατάστασης του ποδιού
HAL_Delay(100); )
}
void SystemClock_Config (άκυρο){
RCC_OscInitTypeDef RCC_OscInitStruct;
RCC_ClkInitTypeDef RCC_ClkInitStruct;

RCC_OscInitStruct.OscillatorType = RCC_OSCILLATORTYPE_HSI;
RCC_OscInitStruct.HSIState = RCC_HSI_ON;
RCC_OscInitStruct.HSICcalibrationValue = 16;
RCC_OscInitStruct.PLL.PLLSκατάσταση = RCC_PLL_NONE;
if (HAL_RCC_OscConfig (&RCC_OscInitStruct) != HAL_OK){

}
/**Εκκινεί τα ρολόγια των διαύλων CPU, AHB και APB */
RCC_ClkInitStruct.ClockType = RCC_CLOCKTYPE_HCLK|RCC_CLOCKTYPE_SYSCLK
|RCC_CLOCKTYPE_PCLK1|RCC_CLOCKTYPE_PCLK2;
RCC_ClkInitStruct.SYSCLKSource = RCC_SYSCLKSOURCE_HSI;
RCC_ClkInitStruct.AHBCLKDivider = RCC_SYSCLK_DIV1;
RCC_ClkInitStruct.APB1CLKDivider = RCC_HCLK_DIV1;
RCC_ClkInitStruct.APB2CLKDivider = RCC_HCLK_DIV1;
if (HAL_RCC_ClockConfig (&RCC_ClkInitStruct, FLASH_LATENCY_0) != HAL_OK){
_Error_Handler(__FILE__, __LINE__);
}
/**Διαμόρφωση του χρόνου διακοπής Systick*/
HAL_SYSTICK_Config(HAL_RCC_GetHCLKFreq()/1000);
/**Διαμόρφωση του Systick */
HAL_SYSTICK_CLKSourceConfig(SYSTICK_CLKSOURCE_HCLK);
/* Διαμόρφωση διακοπής SysTick_IRQn */
HAL_NVIC_SetPriority(SysTick_IRQn, 0, 0);
}
/** Διαμόρφωση ακίδων ως εξόδου αναλογικής εισόδου EVENT_OUT EXTI */
στατικό κενό MX_GPIO_Init (κενό){
GPIO_InitTypeDef GPIO_InitStruct;
/* GPIO Ports Clock Enable */
__HAL_RCC_GPIOE_CLK_ENABLE();
/*Διαμόρφωση επιπέδου εξόδου pin GPIO */
HAL_GPIO_WritePin (GPIOE, GPIO_PIN_8|GPIO_PIN_9|GPIO_PIN_10|GPIO_PIN_11
|GPIO_PIN_12|GPIO_PIN_13|GPIO_PIN_14|GPIO_PIN_15, GPIO_PIN_RESET);
/*Διαμόρφωση ακίδων GPIO: PE8 PE9 PE10 PE11 PE12 PE13 PE14 PE15 */
GPIO_InitStruct.Pin = GPIO_PIN_8|GPIO_PIN_9|GPIO_PIN_10|GPIO_PIN_11
|GPIO_PIN_12|GPIO_PIN_13|GPIO_PIN_14|GPIO_PIN_15;
GPIO_InitStruct.Mode = GPIO_MODE_OUTPUT_PP;
GPIO_InitStruct.Pull = GPIO_NOPULL;
GPIO_InitStruct.Speed ​​= GPIO_SPEED_FREQ_LOW;
HAL_GPIO_Init(GPIOE, &GPIO_InitStruct);
}
void _Error_Handler(char * αρχείο, int line){
ενώ (1) (
} }
#ifdef USE_FULL_ASSERT

Void assert_failed (αρχείο uint8_t*, γραμμή uint32_t){
}
#τέλος εαν
Εδώ, όπως και στο προηγούμενο παράδειγμα, μπορούμε να δούμε την περιγραφή κάθε λειτουργίας στην τεκμηρίωση, για παράδειγμα Εγχειρίδιο χρήστη UM1786 Περιγραφή των προγραμμάτων οδήγησης STM32F3 HAL και χαμηλού επιπέδου.

Μπορούμε να συνοψίσουμε ότι η πρώτη επιλογή, χρησιμοποιώντας το CMSIS, είναι λιγότερο επαχθής. Υπάρχει τεκμηρίωση για κάθε βιβλιοθήκη. Σε επόμενα έργα, θα χρησιμοποιήσουμε HAL και CMSIS χρησιμοποιώντας το πρόγραμμα διαμόρφωσης STCube και, εάν είναι δυνατόν, χρησιμοποιούμε καταχωρητές απευθείας, χωρίς περιτυλίγματα λογισμικού. Ας σταματήσουμε εκεί σήμερα. Στο επόμενο άρθρο θα δούμε τις βασικές αρχές κατασκευής ενός έξυπνου σπιτιού. Αντίο σε όλους.

Απαιτείται λογισμικό για την ανάπτυξη. Σε αυτό το άρθρο θα σας πω πώς να το ρυθμίσετε και να το συνδέσετε σωστά. Όλα τα εμπορικά περιβάλλοντα όπως το IAR EWARM ή το Keil uVision συνήθως εκτελούν αυτήν την ενσωμάτωση μόνα τους, αλλά στην περίπτωσή μας όλα θα πρέπει να ρυθμιστούν χειροκίνητα, ξοδεύοντας πολύ χρόνο σε αυτό. Το πλεονέκτημα είναι ότι έχετε την ευκαιρία να κατανοήσετε πώς λειτουργούν όλα από μέσα και στο μέλλον, να προσαρμόσετε τα πάντα με ευελιξία για τον εαυτό σας. Πριν ξεκινήσουμε τη ρύθμιση, ας δούμε τη δομή του περιβάλλοντος στο οποίο θα εργαστούμε:

Το Eclipse θα χρησιμοποιηθεί για την εύκολη επεξεργασία αρχείων υλοποίησης συναρτήσεων ( .ντο), αρχεία κεφαλίδας ( .h), καθώς και αρχεία assembler ( .ΜΙΚΡΟ). Με τον όρο «βολικό» εννοώ τη χρήση συμπλήρωσης κώδικα, επισήμανση σύνταξης, ανακατασκευή, πλοήγηση μέσω συναρτήσεων και των πρωτοτύπων τους. Τα αρχεία τροφοδοτούνται αυτόματα στους απαραίτητους μεταγλωττιστές, οι οποίοι δημιουργούν κώδικα αντικειμένου (σε αρχεία .ο). Μέχρι στιγμής, αυτός ο κώδικας δεν περιέχει απόλυτες διευθύνσεις μεταβλητών και συναρτήσεων και επομένως δεν είναι κατάλληλος για εκτέλεση. Τα αρχεία αντικειμένων που προκύπτουν συναρμολογούνται από έναν σύνδεσμο. Για να γνωρίζει ποια μέρη του χώρου διευθύνσεων να χρησιμοποιήσει, ο συλλέκτης χρησιμοποιεί ένα ειδικό αρχείο ( .ld), το οποίο ονομάζεται σενάριο σύνδεσης. Συνήθως περιέχει έναν ορισμό των διευθύνσεων ενοτήτων και των μεγεθών τους (τμήμα κώδικα αντιστοιχισμένο σε flash, μεταβλητή ενότητα αντιστοιχισμένη στη μνήμη RAM, κ.λπ.).

Τελικά, ο σύνδεσμος δημιουργεί ένα αρχείο .elf (Εκτέλεση και Συνδέσιμη Μορφή), το οποίο περιέχει, εκτός από οδηγίες και δεδομένα, πληροφορίες εντοπισμού σφαλμάτων που χρησιμοποιούνται από το πρόγραμμα εντοπισμού σφαλμάτων. Αυτή η μορφή δεν είναι κατάλληλη για κανονικό υλικολογισμικό που αναβοσβήνει με το πρόγραμμα vsprog, καθώς αυτό απαιτεί ένα πιο πρωτόγονο αρχείο εικόνας μνήμης (για παράδειγμα, Intel HEX - .hex). Για τη δημιουργία του, υπάρχει επίσης ένα εργαλείο από το σετ Sourcery CodeBench (arm-none-eabi-objcopy) και ενσωματώνεται τέλεια στο eclipse χρησιμοποιώντας το εγκατεστημένο πρόσθετο ARM.

Για να πραγματοποιηθεί ο ίδιος ο εντοπισμός σφαλμάτων, χρησιμοποιούνται τρία προγράμματα:

  1. eclipse, το οποίο επιτρέπει στον προγραμματιστή να χρησιμοποιεί οπτικά διόρθωση σφαλμάτων, να περνάει μέσα από γραμμές, να τοποθετεί τον δείκτη του ποντικιού πάνω από μεταβλητές για να δει τις τιμές τους και άλλες ευκολίες
  2. arm-none-eabi-gdb - Ο πελάτης GDB είναι ένα πρόγραμμα εντοπισμού σφαλμάτων που ελέγχεται κρυφά από eclips (μέσω stdin) ως απόκριση στις ενέργειες που καθορίζονται στο βήμα 1. Με τη σειρά του, το GDB συνδέεται με τον διακομιστή εντοπισμού σφαλμάτων OpenOCD και όλες οι εντολές εισόδου μεταφράζονται από το πρόγραμμα εντοπισμού σφαλμάτων GDB σε εντολές κατανοητές για το OpenOCD. Κανάλι GDB<->Το OpenOCD υλοποιείται χρησιμοποιώντας το πρωτόκολλο TCP.
  3. Το OpenOCD είναι ένας διακομιστής εντοπισμού σφαλμάτων που μπορεί να επικοινωνήσει απευθείας με τον προγραμματιστή. Εκτελείται μπροστά από τον πελάτη και περιμένει για σύνδεση TCP.

Αυτό το σχήμα μπορεί να σας φαίνεται αρκετά άχρηστο: γιατί να χρησιμοποιήσετε τον πελάτη και τον διακομιστή ξεχωριστά και να εκτελέσετε περιττή μετάφραση εντολών, εάν όλα αυτά μπορούσαν να γίνουν με ένα πρόγραμμα εντοπισμού σφαλμάτων; Το γεγονός είναι ότι μια τέτοια αρχιτεκτονική θεωρητικά επιτρέπει την άνετη ανταλλαγή πελάτη και διακομιστή. Για παράδειγμα, εάν πρέπει να χρησιμοποιήσετε έναν άλλο προγραμματιστή αντί του versaloon, ο οποίος δεν θα υποστηρίζει το OpenOCD, αλλά θα υποστηρίζει έναν άλλο ειδικό διακομιστή εντοπισμού σφαλμάτων (για παράδειγμα, texane/stlink για τον προγραμματιστή stlink - ο οποίος βρίσκεται στον πίνακα εντοπισμού σφαλμάτων STM32VLDiscovery), τότε απλά θα εκτελέσετε το OpenOCD αντί να εκκινήσετε τον επιθυμητό διακομιστή και όλα θα λειτουργούν, χωρίς πρόσθετα βήματα. Ταυτόχρονα, είναι δυνατή η αντίθετη κατάσταση: ας υποθέσουμε ότι θέλετε να χρησιμοποιήσετε το περιβάλλον IAR EWARM μαζί με το versaloon αντί του συνδυασμού Eclipse + CodeBench. Το IAR έχει το δικό του ενσωματωμένο πρόγραμμα-πελάτη εντοπισμού σφαλμάτων, το οποίο θα επικοινωνήσει με επιτυχία με το OpenOCD και θα το διαχειριστεί, καθώς και θα λάβει τα απαραίτητα δεδομένα ως απάντηση. Ωστόσο, όλα αυτά μερικές φορές παραμένουν μόνο στη θεωρία, καθώς τα πρότυπα επικοινωνίας μεταξύ πελάτη και διακομιστή δεν ρυθμίζονται αυστηρά και μπορεί να διαφέρουν σε ορισμένα σημεία, αλλά οι διαμορφώσεις που καθόρισα με το st-link+eclipse και το IAR+versaloon λειτούργησαν για μένα.

Συνήθως ο πελάτης και ο διακομιστής εκτελούνται στο ίδιο μηχάνημα και η σύνδεση με τον διακομιστή πραγματοποιείται στο localhost:3333(Για openocd), ή localhost:4242(για τεξάνιο/stlink st-util). Αλλά κανείς δεν σας εμποδίζει να ανοίξετε τη θύρα 3333 ή 4242 (και να προωθήσετε αυτήν τη θύρα στο δρομολογητή σε εξωτερικό δίκτυο) και οι συνάδελφοί σας από άλλη πόλη θα μπορούν να συνδέσουν και να διορθώσουν το υλικό σας. Αυτό το κόλποχρησιμοποιείται συχνά από ενσωματωτές που εργάζονται σε απομακρυσμένες τοποθεσίες όπου η πρόσβαση είναι περιορισμένη.

Ας αρχίσουμε

Εκκινήστε το eclipse και επιλέξτε File->New->C Project, επιλέξτε τον τύπο έργου ARM Linux GCC (Sorcery G++ Lite) και το όνομα "stm32_ld_vl" (Εάν έχετε STV32VLDiscovery, τότε θα ήταν πιο λογικό να το ονομάσετε "stm32_md_vl") :

Κάντε κλικ στο Finish και ελαχιστοποιήστε ή κλείστε το παράθυρο υποδοχής. Έτσι, το έργο δημιουργήθηκε και ο φάκελος stm32_ld_vl θα πρέπει να εμφανιστεί στον χώρο εργασίας σας. Τώρα πρέπει να γεμίσει με τις απαραίτητες βιβλιοθήκες.

Όπως καταλαβαίνετε από το όνομα του έργου, θα δημιουργήσω ένα έργο για την προβολή χάρακα γραμμή τιμής χαμηλής πυκνότητας(LD_VL). Για να δημιουργήσετε ένα έργο για άλλους μικροελεγκτές πρέπει να αντικαταστήσετε όλα τα αρχεία και να ορίσετε στο όνομα των οποίων υπάρχουν _LD_VL (ή_ld_vl) σε αυτά που χρειάζεστε, σύμφωνα με τον πίνακα:

Τύπος χάρακα Ονομασία Μικροελεγκτές (το x μπορεί να αλλάξει)
Γραμμή τιμής χαμηλής πυκνότητας _LD_VL STM32F100x4 STM32F100x6
Χαμηλή πυκνότητα _LD STM32F101x4 STM32F101x6
STM32F102x4 STM32F102x6
STM32F103x4 STM32F103x6
Γραμμή τιμής μεσαίας πυκνότητας _MD_VL STM32F100x8 STM32F100xB
Μεσαίας πυκνότητας
_MD
STM32F101x8 STM32F101xB
STM32F102x8 STM32F102xB
STM32F103x8 STM32F103xB
Γραμμή τιμής υψηλής πυκνότητας _HD_VL STM32F100xC STM32F100xD STM32F100xE
Υψηλής πυκνότητας _HD STM32F101xC STM32F101xD STM32F101xE
STM32F103xC STM32F103xD STM32F103xE
XL-πυκνότητα _XL STM32F101xF STM32F101xG
STM32F103xF STM32F103xG
Γραμμή συνδεσιμότητας _CL STM32F105xx και STM32F107xx

Για να κατανοήσετε τη λογική πίσω από τον πίνακα, πρέπει να είστε εξοικειωμένοι με την επισήμανση STM32. Δηλαδή, εάν έχετε VLDiscovery, τότε θα πρέπει να αντικαταστήσετε ό,τι είναι συνδεδεμένο με _LD_VL με _MD_VL, καθώς το τσιπ STM32F100RB, το οποίο ανήκει στη γραμμή τιμής Μέσης πυκνότητας, είναι κολλημένο στην ανακάλυψη.

Προσθήκη της βιβλιοθήκης τυπικών περιφερειακών CMSIS και STM32F10x στο έργο

CMSISΤο (Cortex Microcontroller Software Interface Standard) είναι μια τυποποιημένη βιβλιοθήκη για εργασία με μικροελεγκτές Cortex που υλοποιεί το επίπεδο HAL (Hardware Abstraction Layer), δηλαδή σας επιτρέπει να αφαιρέσετε από τις λεπτομέρειες της εργασίας με καταχωρητές, την αναζήτηση διευθύνσεων μητρώου χρησιμοποιώντας φύλλα δεδομένων. και τα λοιπά. Η βιβλιοθήκη είναι ένα σύνολο πηγαίων κωδίκων σε C και Asm. Το βασικό μέρος της βιβλιοθήκης είναι το ίδιο για όλα τα Cortex (είτε είναι ST, NXP, ATMEL, TI ή οποιοσδήποτε άλλος) και έχει αναπτυχθεί από την ARM. Το άλλο μέρος της βιβλιοθήκης είναι υπεύθυνο για περιφερειακά, τα οποία φυσικά διαφέρουν από κατασκευαστή σε κατασκευαστή. Επομένως, στο τέλος, η πλήρης βιβλιοθήκη εξακολουθεί να διανέμεται από τον κατασκευαστή, αν και το τμήμα του πυρήνα εξακολουθεί να μπορεί να ληφθεί ξεχωριστά από τον ιστότοπο της ARM. Η βιβλιοθήκη περιέχει ορισμούς διευθύνσεων, κώδικα αρχικοποίησης της γεννήτριας ρολογιού (εύκολα προσαρμόσιμος από ορισμούς) και οτιδήποτε άλλο εξοικονομεί από τον προγραμματιστή να εισάγει με μη αυτόματο τρόπο στα έργα του τον ορισμό διευθύνσεων όλων των ειδών περιφερειακών καταχωρητών και να προσδιορίζει τα bit των τιμών του αυτά τα μητρώα.

Αλλά τα παιδιά από το ST πήγαν παραπέρα. Εκτός από την υποστήριξη CMSIS, παρέχουν μια άλλη βιβλιοθήκη για το STM32F10x που ονομάζεται Βιβλιοθήκη τυπικών περιφερειακών(SPL), το οποίο μπορεί να χρησιμοποιηθεί επιπλέον του CMSIS. Η βιβλιοθήκη παρέχει ταχύτερη και πιο βολική πρόσβαση στα περιφερειακά και επίσης ελέγχει (σε ​​ορισμένες περιπτώσεις) τη σωστή λειτουργία των περιφερειακών. Επομένως, αυτή η βιβλιοθήκη ονομάζεται συχνά ένα σύνολο προγραμμάτων οδήγησης για περιφερειακές μονάδες. Συνοδεύεται από ένα πακέτο παραδειγμάτων, χωρισμένα σε κατηγορίες για διαφορετικά περιφερειακά. Η βιβλιοθήκη είναι επίσης διαθέσιμη όχι μόνο για το STM32F10x, αλλά και για άλλες σειρές.

Μπορείτε να κάνετε λήψη ολόκληρης της έκδοσης 3.5 του SPL+CMSIS εδώ: STM32F10x_StdPeriph_Lib_V3.5.0 ή στον ιστότοπο της ST. Αποσυμπιέστε το αρχείο. Δημιουργήστε τους φακέλους CMSIS και SPL στο φάκελο του έργου και ξεκινήστε την αντιγραφή των αρχείων στο έργο σας:

Τι να αντιγράψετε

Πού να αντιγράψετε (λαμβάνοντας υπόψη
ότι ο φάκελος του έργου είναι stm32_ld_vl)

Περιγραφή Αρχείου
Βιβλιοθήκες/CMSIS/CM3/
CoreSupport/ πυρήνας_cm3.γ
stm32_ld_vl/CMSIS/ πυρήνας_cm3.γ Περιγραφή του πυρήνα Cortex M3
Βιβλιοθήκες/CMSIS/CM3/
CoreSupport/ πυρήνας_cm3.h
stm32_ld_vl/CMSIS/core_cm3.h Κεφαλίδες περιγραφής πυρήνα

ST/STM32F10x/ system_stm32f10x.c
stm32_ld_vl/CMSIS/system_stm32f10x.c Λειτουργίες αρχικοποίησης και
έλεγχος ρολογιού
Βιβλιοθήκες/CMSIS/CM3/Υποστήριξη συσκευών/
ST/STM32F10x/ system_stm32f10x.h
stm32_ld_vl/CMSIS/system_stm32f10x.h Κεφαλίδες για αυτές τις συναρτήσεις
Βιβλιοθήκες/CMSIS/CM3/Υποστήριξη συσκευών/
ST/STM32F10x/ stm32f10x.h
stm32_ld_vl/CMSIS/stm32f10x.h Βασική περιγραφή των περιφερειακών
Βιβλιοθήκες/CMSIS/CM3/Υποστήριξη συσκευών/
ST/STM32F10x/startup/gcc_ride7/
startup_stm32f10x_ld_vl.s
stm32_ld_vl/CMSIS/startup_stm32f10x_ld_vl.S
(!!! Προσοχή επέκταση αρχείου CAPITAL S)
Διανυσματικό αρχείο πίνακα
διακόπτει και init-s on asm
Project/STM32F10x_StdPeriph_Template/
stm32f10x_conf.h
stm32_ld_vl/CMSIS/ stm32f10x_conf.h Πρότυπο για προσαρμογή
περιφερειακές μονάδες

inc/ *
stm32_ld_vl/SPL/inc/ * Αρχεία κεφαλίδας SPL
Libraries/STM32F10x_StdPeriph_Driver/
src/ *
stm32_ld_vl/SPL/src/ * Εφαρμογή SPL

Μετά την αντιγραφή, μεταβείτε στο Eclipse και κάντε Ανανέωση στο μενού περιβάλλοντος του έργου. Ως αποτέλεσμα, στον Project Explorer θα πρέπει να έχετε την ίδια δομή όπως στην εικόνα στα δεξιά.

Ίσως έχετε παρατηρήσει ότι στο φάκελο Libraries/CMSIS/CM3/DeviceSupport/ST/STM32F10x/startup/ υπάρχουν φάκελοι για διαφορετικά IDE (οι διαφορετικοί IDE χρησιμοποιούν διαφορετικούς μεταγλωττιστές). Επέλεξα το Ride7 IDE επειδή χρησιμοποιεί τον μεταγλωττιστή GNU Tools for ARM Embedded, ο οποίος είναι συμβατός με το Sourcery CodeBench.

Ολόκληρη η βιβλιοθήκη έχει ρυθμιστεί χρησιμοποιώντας έναν προεπεξεργαστή (χρησιμοποιώντας defines), αυτό θα σας επιτρέψει να λύσετε όλους τους απαραίτητους κλάδους στο στάδιο της μεταγλώττισης (ή μάλλον, ακόμη και πριν από αυτό) και να αποφύγετε το φόρτο στη λειτουργία του ίδιου του ελεγκτή (που θα ήταν παρατηρήθηκε εάν η διαμόρφωση πραγματοποιήθηκε στο RunTime). Για παράδειγμα, όλος ο εξοπλισμός είναι διαφορετικός για διαφορετικές γραμμές, και επομένως για να «γνωρίσει» η βιβλιοθήκη ποια γραμμή θέλετε να χρησιμοποιήσετε, σας ζητείται να αφαιρέσετε το σχολιασμό στο αρχείο stm32f10x.hένα από τα ορίζει (που αντιστοιχεί στη γραμμή σας):

/* #define STM32F10X_LD */ /*!< STM32F10X_LD: STM32 Low density devices */
/* #define STM32F10X_LD_VL */ /*!< STM32F10X_LD_VL: STM32 Low density Value Line devices */
/* #define STM32F10X_MD */ /*!< STM32F10X_MD: STM32 Medium density devices */

Και ούτω καθεξής...

Αλλά δεν συνιστώ να το κάνετε αυτό. Δεν θα αγγίξουμε τα αρχεία της βιβλιοθήκης προς το παρόν και θα τα ορίσουμε αργότερα χρησιμοποιώντας τις ρυθμίσεις του μεταγλωττιστή στο Eclipse. Και τότε το Eсlipse θα καλέσει τον μεταγλωττιστή με το κλειδί -D STM32F10X_LD_VL, το οποίο για τον προεπεξεργαστή είναι απολύτως ισοδύναμο με την κατάσταση αν δεν σχολιάσετε "#define STM32F10X_LD_VL". Έτσι, δεν θα αλλάξουμε τον κώδικα· ως αποτέλεσμα, εάν το επιθυμείτε, κάποια μέρα θα μπορείτε να μετακινήσετε τη βιβλιοθήκη σε ξεχωριστό κατάλογο και να μην την αντιγράψετε στο φάκελο κάθε νέου έργου.

Σενάριο σύνδεσης

Στο μενού περιβάλλοντος του έργου, επιλέξτε Νέο->Αρχείο->Άλλο->Γενικά->Αρχείο, Επόμενο. Επιλέξτε τον ριζικό φάκελο του έργου (stm32_ld_vl). Εισαγάγετε το όνομα αρχείου "stm32f100c4.ld" (ή "stm32f100rb.ld" για ανακάλυψη). Τώρα αντιγράψτε και επικολλήστε στο eclipse:

ENTRY(Reset_Handler) MEMORY ( FLASH (rx) : ORIGIN = 0x08000000, LENGTH = 16K RAM (xrw) : ORIGIN = 0x20000000, LENGTH = 4K ) _estack = ORIGIN(MTH) +MNGRA); MIN_HEAP_SIZE = 0; MIN_STACK_SIZE = 256; SECTIONS ( /* Διανυσματικός πίνακας διακοπής */ .isr_vector: ( . = ALIGN(4); KEEP(*(.isr_vector)) . = ALIGN(4); ) >FLASH /* Ο κωδικός προγράμματος και άλλα δεδομένα μεταφέρονται στο FLASH * / .text: ( . = ALIGN(4); /* Κωδικός */ *(.text) *(.text*) /* Σταθερές */ *(.rodata) *(.rodata*) /* ARM->Thumb και Thumb->ARM κωδικός κόλλας */ *(.glue_7) *(.glue_7t) KEEP (*(.init)) KEEP (*(.fini)) . = ALIGN(4); _etext = .; ) >FLASH . ARM.extab: ( *(.ARM.extab* .gnu.linkonce.armextab.*) ) >FLASH .ARM: ( __exidx_start = .; *(.ARM.exidx*) __exidx_end = .; ) >FLASH .ARM. χαρακτηριστικά: ( *(.ARM.χαρακτηριστικά) ) > FLASH .preinit_array: ( PROVIDE_HIDDEN (__preinit_array_start = .); KEEP (*(.preinit_array*)) PROVIDE_HIDDEN (__preinit_array_end = .:HID_EN >PROVIS_DE ray_start = .); KEEP (*(SORT(.init_array.*))) KEEP (*(.init_array*)) PROVIDE_HIDDEN (__init_array_end = .); ) >FLASH .fini_array: ( PROVIDE_HIDDEN (__fini_array_KEEP =); (.fini_array*)) KEEP (*(SORT(.fini_array.*))) PROVIDE_HIDDEN (__fini_array_end = .); ) >FLASH_sidata = .; /* Αρχικοποιημένα δεδομένα */ .data: AT (_sidata) ( . = ALIGN(4); _sdata = .; /* δημιουργία ενός καθολικού συμβόλου κατά την έναρξη των δεδομένων */ *(.data) *(.data*) . = ALIGN (4); _edata = .; /* ορίστε ένα καθολικό σύμβολο στο τέλος δεδομένων */ ) >RAM /* Μη αρχικοποιημένα δεδομένα */ . = ALIGN(4); .bss: ( /* Αυτό χρησιμοποιείται από την εκκίνηση για να αρχικοποιήσει την ενότητα .bss */ _sbss = .; /* ορίστε ένα καθολικό σύμβολο στο bss start */ __bss_start__ = _sbss; *(.bss) *(.bss *) *(ΚΟΙΝΟ) .= ALIGN(4); _ebss = .; /* ορίστε ένα καθολικό σύμβολο στο τέλος bss */ __bss_end__ = _ebss; ) >ΠΡΟΒΟΛΗ RAM (end = _ebss); PROVIDE(_end = _ebss); PROVIDE(__HEAP_START = _ebss); /* Ενότητα User_heap_stack, που χρησιμοποιείται για τον έλεγχο ότι έχει απομείνει αρκετή RAM */ ._user_heap_stack: ( . = ALIGN(4); . = . + MIN_HEAP_SIZE; . = . + MIN_STACK_SIZE; . = ALIGN(4); ) >RAM / ΑΠΟΡΡΙΨΗ/ : ( libc.a(*) libm.a(*) libgcc.a(*) ) )

Αυτό το λ Το σενάριο inker θα προορίζεται ειδικά για τον ελεγκτή STM32F100C4 (που έχει 16 KB φλας και 4 KB μνήμη RAM), εάν έχετε διαφορετικό, θα πρέπει να αλλάξετε τις παραμέτρους LENGTH των περιοχών FLASH και RAM στην αρχή του το αρχείο (για το STM32F100RB, το οποίο στο Discovery: Flash 128K και RAM 8K).

Αποθηκεύστε το αρχείο.

Ρύθμιση Build (C/C++ Build)

Μεταβείτε στο Project->Properties->C/C++ Build->Settings->Tool Settings και ξεκινήστε τη ρύθμιση των εργαλείων κατασκευής:

1) Πρόδρομος στόχος

Επιλέγουμε για ποιον πυρήνα Cortex θα λειτουργεί ο μεταγλωττιστής.

  • Επεξεργαστής: cortex-m3

2) ARM Sourcery Linux GCC C Compiler -> Προεπεξεργαστής

Προσθέτουμε δύο ορισμούς περνώντας τους από τον διακόπτη -D στον μεταγλωττιστή.

  • STM32F10X_LD_VL - ορίζει τον χάρακα (έγραψα για αυτόν τον ορισμό παραπάνω)
  • USE_STDPERIPH_DRIVER - ένδειξη Βιβλιοθήκη CMSISότι πρέπει να χρησιμοποιεί το πρόγραμμα οδήγησης SPL

3) ARM Sourcery Linux GCC C Compiler -> Κατάλογοι

Προσθήκη διαδρομών στη βιβλιοθήκη περιλαμβάνει.

  • "$(workspace_loc:/$(ProjName)/CMSIS)"
  • "$(workspace_loc:/$(ProjName)/SPL/inc)"

Τώρα, για παράδειγμα, αν γράψουμε:

#include "stm32f10x.h

Στη συνέχεια, ο μεταγλωττιστής πρέπει πρώτα να αναζητήσει το αρχείο stm32f10x.hστον κατάλογο του έργου (το κάνει πάντα αυτό), δεν θα το βρει εκεί και θα αρχίσει να ψάχνει στον φάκελο CMSIS, τη διαδρομή προς την οποία υποδείξαμε, και θα το βρει.

4) ARM Sourcery Linux GCC C Compiler -> Optimization

Ας ενεργοποιήσουμε τη βελτιστοποίηση λειτουργιών και δεδομένων

  • -λειτουργία-τμήματα
  • -fdata-ενότητες

Ως αποτέλεσμα, όλες οι συναρτήσεις και τα στοιχεία δεδομένων θα τοποθετηθούν σε ξεχωριστές ενότητες και ο συλλέκτης θα μπορεί να καταλάβει ποια τμήματα δεν χρησιμοποιούνται και απλά να τα πετάξει.

5) ARM Sourcery Linux GCC C Compiler -> Γενικά

Προσθέστε τη διαδρομή στο σενάριο σύνδεσης: "$(workspace_loc:/$(ProjName)/stm32f100c4.ld)" (ή όπως αλλιώς το αποκαλείτε).

Και ορίστε τις επιλογές:

  • Μην χρησιμοποιείτε τυπικά αρχεία εκκίνησης - μην τα χρησιμοποιείτε τυπικά αρχείαεκτόξευση.
  • Αφαιρέστε τα αχρησιμοποίητα τμήματα - αφαιρέστε τα αχρησιμοποίητα τμήματα

Αυτό είναι όλο, η ρύθμιση έχει ολοκληρωθεί. ΕΝΤΑΞΕΙ.

Έχουμε κάνει πολλά από τότε που δημιουργήθηκε το έργο και υπάρχουν ορισμένα πράγματα που μπορεί να έχει χάσει το Eclipse, επομένως πρέπει να του πούμε να επανεξετάσει τη δομή του αρχείου του έργου. Για να το κάνετε αυτό, πρέπει να κάνετε από το μενού περιβάλλοντος του έργου Ευρετήριο -> ανακατασκευή.

Hello LED στο STM32

Ήρθε η ώρα να δημιουργήσετε το κύριο αρχείο έργου: Αρχείο -> Νέο -> C/C++ -> Αρχείο προέλευσης. Επόμενο. Όνομα αρχείου Αρχείο πηγής: main.c.

Αντιγράψτε και επικολλήστε τα παρακάτω στο αρχείο:

#include "stm32f10x.h" uint8_t i=0; int main(void) ( RCC->APB2ENR |= RCC_APB2ENR_IOPBEN; // Ενεργοποίηση ρολογιού PORTB Periph RCC->APB1ENR |= RCC_APB1ENR_TIM2EN; // Ενεργοποίηση περιμετρικού ρολογιού TIM2 // Απενεργοποίηση JTAG για απελευθέρωση LED PIN RCCAFREN->APBCCAFRENB; AFIO->MAPR |= AFIO_MAPR_SWJ_CFG_JTAGDISABLE; // Διαγραφή PB4 και PB5 bit μητρώου ελέγχου GPIOB->CRL &= ~(GPIO_CRL_MODE4 | GPIO_CRL_CNF4 | GPIO_CRL_MODE5 | ως PB4.Pufig. Τραβήξτε στην έξοδο μέγ 10Mhz GPIOB->CRL |= GPIO_CRL_MODE4_0 | GPIO_CRL_MODE5_0; TIM2->PSC = SystemCoreClock / 1000 - 1; // 1000 tick/sec TIM2->ARR = 1000; // sec | TIMER_E // Ενεργοποίηση διακοπής tim2 TIM2->CR1 |= TIM_CR1_CEN; // Αριθμός έναρξης NVIC_EnableIRQ(TIM2_IRQn); // Ενεργοποίηση IRQ while(1); // Βρόχος Infinity ) void TIM2_IRQHandler(void) ( TIM2->SR &R_F=_T . GPIO_BSRR_BS5; // Ορισμός PB5 bit GPIOB->BSRR = GPIO_BSRR_BR4; // Επαναφορά PB4 bit ))

Αν και συμπεριλάβαμε τη βιβλιοθήκη SPL, δεν χρησιμοποιήθηκε εδώ. Όλες οι κλήσεις σε πεδία όπως RCC->APB2ENR περιγράφονται πλήρως στο CMSIS.

Μπορείτε να κάνετε Project -> Build All. Εάν όλα λύθηκαν, τότε το αρχείο stm32_ld_vl.hex θα πρέπει να εμφανιστεί στο φάκελο Debug του έργου. Δημιουργήθηκε αυτόματα από το elf από τα ενσωματωμένα εργαλεία. Αναβοσβήνουμε το αρχείο και βλέπουμε πώς τα LED αναβοσβήνουν με συχνότητα μία φορά το δευτερόλεπτο:

Vsprog -sstm32f1 -ms -oe -owf -I /home/user/workspace/stm32_ld_vl/Debug/stm32_ld_vl.hex -V "tvcc.set 3300"

Φυσικά, αντί για /home/user/workspace/ πρέπει να εισαγάγετε τη διαδρομή σας προς τον χώρο εργασίας.

Για STM32VLDdiscovery

Ο κώδικας είναι ελαφρώς διαφορετικός από αυτόν που έδωσα παραπάνω για τον πίνακα εντοπισμού σφαλμάτων μου. Η διαφορά έγκειται στις ακίδες στις οποίες «κρέμονται» τα LED. Αν στην πλακέτα μου ήταν PB4 και PB5, τότε στο Discovery ήταν PC8 και PC9.

#include "stm32f10x.h" uint8_t i=0; int main(void) ( RCC->APB2ENR |= RCC_APB2ENR_IOPCEN; // Ενεργοποίηση ρολογιού PORTC Periph RCC->APB1ENR |= RCC_APB1ENR_TIM2EN; // Ενεργοποίηση TIM2 Periph clock // Clear PC8 και PCOC-Priph bits-register GHH (GPIO_CRH_MODE8 | GPIO_CRH_CNF8 | GPIO_CRH_MODE9 | GPIO_CRH_CNF9); // Διαμόρφωση PC8 και PC9 ως έξοδο Push Pull σε μέγιστο 10Mhz GPIOC->CRH |= GPIO_CRH_MODE8_CRH |= GPIO_CRH_MODE8_CRH |= GPIO_CRH_MODE8_CNF9 1000 - 1; // 1000 τικ/δευτ TIM2->ARR = 1000; // 1 Διακοπή/δευτ. (1000/100) TIM2->DIER |= TIM_DIER_UIE; // Ενεργοποίηση διακοπής tim2 TIM2->CR1 |= TIM_CR1_CEN; // Μέτρηση έναρξης NVIC_EnableIRQ(TIM) Ενεργοποίηση IRQ while(1); // Βρόχος Infinity ) void TIM2_IRQHandler(void) ( TIM2->SR &= ~TIM_SR_UIF; //Clean UIF Flag if (1 == (i++ & 0x1)) ( GPIOC->BSRR = GPIO_BSRR_BS .

Στα Windows, μπορείτε να αναβοσβήσετε το προκύπτον hex(/workspace/stm32_md_vl/Debug/stm32_md_vl.hex) χρησιμοποιώντας το βοηθητικό πρόγραμμα από το ST.

Λοιπόν, κάτω βοηθητικό πρόγραμμα linux st-flash. ΑΛΛΑ!!! Το βοηθητικό πρόγραμμα δεν χρησιμοποιεί τη μορφή Intel HEX hex (η οποία δημιουργείται από προεπιλογή), επομένως είναι εξαιρετικά σημαντικό να επιλέξετε τη δυαδική μορφή στις ρυθμίσεις δημιουργίας εικόνας Flash:

Η επέκταση αρχείου δεν θα αλλάξει (θα παραμείνει εξαγωνική όπως ήταν), αλλά η μορφή του αρχείου θα αλλάξει. Και μόνο μετά από αυτό μπορείτε να κάνετε:

St-flash εγγραφή v1 /home/user/workspace/stm32_md_vl/Debug/stm32_md_vl.hex 0x08000000

Παρεμπιπτόντως, όσον αφορά την επέκταση και τη μορφή: συνήθως τα δυαδικά αρχεία επισημαίνονται με την επέκταση .bin, ενώ τα αρχεία σε μορφή Intel HEX ονομάζονται επέκταση .hex. Η διαφορά σε αυτές τις δύο μορφές είναι περισσότερο τεχνική παρά λειτουργική: η δυαδική μορφή περιέχει απλώς byte εντολών και δεδομένων που απλώς θα γραφτούν στον ελεγκτή από τον προγραμματιστή «ως έχει». Το IntelHEX, από την άλλη πλευρά, δεν έχει δυαδική μορφή, αλλά κείμενο: ακριβώς τα ίδια byte χωρίζονται σε 4 bit και παρουσιάζονται χαρακτήρα προς χαρακτήρα σε μορφή ASCII και χρησιμοποιούνται μόνο χαρακτήρες 0-9, A-F (bin και hex είναι αριθμητικά συστήματα με πολλαπλές βάσεις, δηλαδή 4 bit ανά bin μπορούν να αναπαρασταθούν ως ένα μόνο εξαψήφιο). Έτσι, η μορφή ihex είναι περισσότερο από 2 φορές το μέγεθος ενός κανονικού δυαδικού αρχείου (κάθε 4 bit αντικαθίστανται από ένα byte + αλλαγές γραμμής για εύκολη ανάγνωση), αλλά μπορεί να διαβαστεί σε ένα κανονικό πρόγραμμα επεξεργασίας κειμένου. Επομένως, εάν πρόκειται να στείλετε αυτό το αρχείο σε κάποιον ή να το χρησιμοποιήσετε σε άλλα προγράμματα προγραμματισμού, τότε καλό είναι να το μετονομάσετε σε stm32_md_vl.bin για να μην παραπλανήσετε όσους βλέπουν το όνομά του.

Ρυθμίσαμε λοιπόν την κατασκευή υλικολογισμικού για το stm32. Την επόμενη φορά θα σας πω πώς

Λοιπόν, μέχρι στιγμής όλα πάνε καλά, αλλά μόνο οι λάμπες και τα κουμπιά είναι έτοιμα. Τώρα ήρθε η ώρα να αναλάβετε βαρύτερα περιφερειακά - USB, UART, I2C και SPI. Αποφάσισα να ξεκινήσω με USB - το πρόγραμμα εντοπισμού σφαλμάτων ST-Link (ακόμα και το πραγματικό από το Discovery) αρνήθηκε πεισματικά να διορθώσει την πλακέτα μου, οπότε η διόρθωση σφαλμάτων σε εκτυπώσεις μέσω USB είναι η μόνη διαθέσιμη μέθοδος εντοπισμού σφαλμάτων. Μπορείτε, φυσικά, μέσω UART, αλλά αυτό είναι ένα σωρό πρόσθετα καλώδια.

Πήρα πάλι τη μεγάλη διαδρομή - δημιούργησα τα αντίστοιχα κενά στο STM32CubeMX και πρόσθεσα το USB Middleware από το πακέτο STM32F1Cube στο έργο μου. Απλά πρέπει να ενεργοποιήσετε το ρολόι USB, να ορίσετε τους αντίστοιχους χειριστές διακοπής USB και να γυαλίσετε τα μικροπράγματα. Κυρίως όλα σημαντικά Ρυθμίσεις USBΑντέγραψα τη μονάδα από το STM32GENERIC, με τη διαφορά ότι τροποποίησα ελαφρώς την εκχώρηση μνήμης (χρησιμοποιούσαν malloc και εγώ χρησιμοποίησα στατική κατανομή).

Εδώ είναι μερικά ενδιαφέροντα κομμάτια που άρπαξα. Για παράδειγμα, για να καταλάβει ο κεντρικός υπολογιστής (υπολογιστής) ότι κάτι είναι συνδεδεμένο σε αυτόν, η συσκευή «παραμορφώνει» τη γραμμή USB D+ (η οποία είναι συνδεδεμένη στον ακροδέκτη A12). Έχοντας δει αυτό, ο οικοδεσπότης αρχίζει να ανακρίνει τη συσκευή σχετικά με το ποια είναι, ποιες διεπαφές μπορεί να χειριστεί, με ποια ταχύτητα θέλει να επικοινωνήσει κ.λπ. Δεν καταλαβαίνω πραγματικά γιατί πρέπει να γίνει αυτό πριν από την προετοιμασία USB, αλλά στο stm32duino γίνεται με τον ίδιο τρόπο.

Τράβηγμα USB

USBD_HandleTypeDef hUsbDeviceFS; void Reenumerate() ( // Εκκίνηση της ακίδας PA12 GPIO_InitTypeDef pinInit; pinInit.Pin = GPIO_PIN_12; pinInit.Mode = GPIO_MODE_OUTPUT_PP; pinInit.Speed ​​= GPIO_SPEED_FREQ_Init/GPIO; για απαρίθμηση συσκευών USB ο δίαυλος HAL_GPIO_WritePin(GPIOA, GPIO_PIN_12, GPIO_PIN_RESET); for(unsigned int i=0; i<512; i++) {}; // Restore pin mode pinInit.Mode = GPIO_MODE_INPUT; pinInit.Pull = GPIO_NOPULL; HAL_GPIO_Init(GPIOA, &pinInit); for(unsigned int i=0; i<512; i++) {}; } void initUSB() { Reenumerate(); USBD_Init(&hUsbDeviceFS, &FS_Desc, DEVICE_FS); USBD_RegisterClass(&hUsbDeviceFS, &USBD_CDC); USBD_CDC_RegisterInterface(&hUsbDeviceFS, &USBD_Interface_fops_FS); USBD_Start(&hUsbDeviceFS); }


Ένα άλλο ενδιαφέρον σημείο είναι η υποστήριξη για τον bootloader stm32duino. Για να ανεβάσετε το υλικολογισμικό, πρέπει πρώτα να επανεκκινήσετε τον ελεγκτή στο bootloader. Ο ευκολότερος τρόπος είναι να πατήσετε το κουμπί επαναφοράς. Αλλά για να το κάνετε πιο βολικά, μπορείτε να υιοθετήσετε την εμπειρία του Arduino. Όταν τα δέντρα ήταν νεαρά, οι ελεγκτές AVR δεν είχαν ακόμη υποστήριξη USB, υπήρχε ένας προσαρμογέας USB-UART στην πλακέτα. Το σήμα DTR UART συνδέεται με την επαναφορά του μικροελεγκτή. Όταν ο κεντρικός υπολογιστής στέλνει το σήμα DTR, ο μικροελεγκτής επανεκκινείται στον φορτωτή εκκίνησης. Λειτουργεί σαν οπλισμένο σκυρόδεμα!

Στην περίπτωση χρήσης USB, προσομοιώνουμε μόνο μια θύρα COM. Αντίστοιχα, πρέπει να κάνετε επανεκκίνηση μόνοι σας στο bootloader. Ο bootloader stm32duino, εκτός από το σήμα DTR, για κάθε ενδεχόμενο, περιμένει επίσης μια ειδική μαγική σταθερά (1EAF - αναφορά στο Leaf Labs)

static int8_t CDC_Control_FS (uint8_t cmd, uint8_t* pbuf, uint16_t μήκος) ( ... περίπτωση CDC_SET_CONTROL_LINE_STATE: dtr_pin++; //DTR pin είναι ενεργοποιημένο σπάσιμο, ... στατικό int8_t CDC_Receive_Foint, *u*23) Το byte είναι το μαγικό πακέτο "1EAF" που τοποθετεί το MCU στο bootloader. */ if(*Len >= 4) ( /** * Ελέγξτε εάν η εισερχόμενη περιέχει τη συμβολοσειρά "1EAF". * Εάν ναι, ελέγξτε αν το DTR έχει έχει ρυθμιστεί, για να τεθεί το MCU σε λειτουργία bootloader. */ if(dtr_pin > 3) ( if((Buf == "1")&&(Buf == "E")&&(Buf == "A")&& (Buf == "F")) ( HAL_NVIC_SystemReset(); ) dtr_pin = 0; ) ) ... )

Επιστροφή: MiniArduino

Γενικά το USB λειτούργησε. Αλλά αυτό το επίπεδο λειτουργεί μόνο με byte, όχι με συμβολοσειρές. Γι' αυτό οι εκτυπώσεις εντοπισμού σφαλμάτων φαίνονται τόσο άσχημες.

CDC_Transmit_FS((uint8_t*)"Ping\n", 5); // 5 είναι ένα strlen("Ping") + μηδέν byte
Εκείνοι. Δεν υπάρχει καθόλου υποστήριξη για μορφοποιημένη έξοδο - δεν μπορείτε να εκτυπώσετε έναν αριθμό ή να συναρμολογήσετε μια συμβολοσειρά από κομμάτια. Προκύπτουν οι ακόλουθες επιλογές:

  • Βιδώστε το κλασικό printf. Η επιλογή φαίνεται να είναι καλή, αλλά απαιτεί +12 kb υλικολογισμικού (κάπως κατά λάθος κάλεσα το sprintf)
  • Ανακαλύψτε τη δική σας υλοποίηση του printf από το απόθεμά σας. Κάποτε έγραψα για το AVR, φαίνεται ότι αυτή η υλοποίηση ήταν μικρότερη.
  • Συνδέστε την κλάση Print από το Arduino στην υλοποίηση STM32GENERIC
Επέλεξα την τελευταία επιλογή επειδή ο κώδικας της βιβλιοθήκης Adafruit GFX βασίζεται επίσης στην εκτύπωση, οπότε πρέπει ακόμα να τον βιδώσω. Εξάλλου, είχα ήδη τον κωδικό STM32GENERIC στα χέρια μου.

Δημιούργησα έναν κατάλογο MiniArduino στο έργο μου με στόχο να βάλω εκεί την ελάχιστη απαιτούμενη ποσότητα κώδικα για να υλοποιήσω τα κομμάτια της διεπαφής arduino που χρειαζόμουν. Άρχισα να αντιγράφω ένα αρχείο κάθε φορά και να εξετάζω ποιες άλλες εξαρτήσεις χρειάζονταν. Έτσι κατέληξα με ένα αντίγραφο της κλάσης Print και πολλά δεσμευτικά αρχεία.

Αυτό όμως δεν είναι αρκετό. Ήταν ακόμα απαραίτητο να συνδεθεί με κάποιο τρόπο η κλάση Print με λειτουργίες USB (για παράδειγμα, CDC_Transmit_FS()). Για να γίνει αυτό, έπρεπε να σύρουμε στην κατηγορία SerialUSB. Τραβούσε την κλάση Stream και ένα κομμάτι της προετοιμασίας του GPIO. Το επόμενο βήμα ήταν να συνδέσω το UART (έχω συνδεδεμένο ένα GPS σε αυτό). Έφερα λοιπόν και την κλάση SerialUART, η οποία τράβηξε μαζί της ένα άλλο επίπεδο περιφερειακής προετοιμασίας από το STM32GENERIC.

Γενικά, βρέθηκα στην εξής κατάσταση. Αντέγραψα σχεδόν όλα τα αρχεία από το STM32GENERIC στο MiniArduino μου. Είχα επίσης το δικό μου αντίγραφο των βιβλιοθηκών USB και FreeRTOS (θα έπρεπε να είχα και αντίγραφα των HAL και CMSIS, αλλά ήμουν πολύ τεμπέλης). Ταυτόχρονα, εδώ και ενάμιση μήνα σημειώνω χρόνο - συνδέω και αποσυνδέω διαφορετικά κομμάτια, αλλά ταυτόχρονα δεν έχω γράψει ούτε μια γραμμή νέου κώδικα.

Έγινε σαφές ότι η αρχική μου ιδέα ήταν να πάρω τον έλεγχο όλων μέρος του συστήματοςΔεν βγαίνει και πολύ καλά. Τέλος πάντων, μέρος του κώδικα προετοιμασίας ζει στο STM32GENERIC και φαίνεται να είναι πιο άνετο εκεί. Φυσικά, θα μπορούσα να κόψω όλες τις εξαρτήσεις και να γράψω τις δικές μου τάξεις περιτυλίγματος για τις εργασίες μου, αλλά αυτό θα με επιβραδύνει για άλλον ένα μήνα - αυτός ο κώδικας χρειάζεται ακόμα εντοπισμό σφαλμάτων. Φυσικά, αυτό θα ήταν ωραίο για τη δική σας κατάσταση έκτακτης ανάγκης, αλλά πρέπει να προχωρήσετε!

Έτσι, πέταξα όλες τις διπλότυπες βιβλιοθήκες και σχεδόν ολόκληρο το επίπεδο συστήματος και επέστρεψα στο STM32GENERIC. Αυτό το έργο αναπτύσσεται αρκετά δυναμικά - αρκετές δεσμεύσεις την ημέρα με συνέπεια. Επιπλέον, κατά τη διάρκεια αυτού του ενάμιση μήνα μελέτησα πολύ, διάβασα το μεγαλύτερο μέρος του εγχειριδίου αναφοράς STM32, κοίταξα πώς κατασκευάστηκαν οι βιβλιοθήκες HAL και τα περιτυλίγματα STM32GENERIC και προχώρησα στην κατανόηση των περιγραφέων USB και των περιφερειακών μικροελεγκτών. Συνολικά, τώρα ήμουν πολύ πιο σίγουρος για το STM32GENERIC από πριν.

Αντίστροφη: I2C

Ωστόσο, οι περιπέτειές μου δεν τελείωσαν εκεί. Υπήρχαν ακόμα UART και I2C (η οθόνη μου μένει εκεί). Με το UART όλα ήταν πολύ απλά. Μόλις αφαίρεσα δυναμική κατανομήμνήμη, και έτσι ώστε τα αχρησιμοποίητα UART να μην καταναλώνουν αυτήν ακριβώς τη μνήμη, απλώς τα σχολίασα.

Αλλά η εφαρμογή του I2C στο STM32GENERIC ήταν λίγο πρόβλημα. Πολύ ενδιαφέρον, αλλά που μου πήρε τουλάχιστον 2 βράδια. Λοιπόν, ή δώσατε 2 βράδια σκληρού εντοπισμού σφαλμάτων σε εκτυπώσεις - έτσι το βλέπετε.

Γενικά, η υλοποίηση της οθόνης δεν ξεκίνησε. Στο ήδη παραδοσιακό στυλ, απλά δεν λειτουργεί και αυτό είναι. Το τι δεν λειτουργεί δεν είναι ξεκάθαρο. Η ίδια η βιβλιοθήκη της οθόνης (Adafruit SSD1306) φαίνεται να έχει δοκιμαστεί στην προηγούμενη εφαρμογή, αλλά δεν πρέπει να αποκλειστούν σφάλματα παρεμβολής. Οι υποψίες πέφτουν στο HAL και την υλοποίηση του I2C από το STM32GENERIC.

Αρχικά, σχολίασα όλη την οθόνη και τον κώδικα I2C και έγραψα μια προετοιμασία I2C χωρίς βιβλιοθήκες, σε καθαρό HAL

Αρχικοποίηση I2C

GPIO_InitTypeDef GPIO_InitStruct; GPIO_InitStruct.Pin = GPIO_PIN_6|GPIO_PIN_7; GPIO_InitStruct.Mode = GPIO_MODE_AF_OD; GPIO_InitStruct.Pull = GPIO_PULLUP; GPIO_InitStruct.Speed ​​= GPIO_SPEED_HIGH; HAL_GPIO_Init(GPIOB, &GPIO_InitStruct); __I2C1_CLK_ENABLE(); hi2c1.Instance = I2C1; hi2c1.Init.ClockSpeed ​​= 400000; hi2c1.Init.DutyCycle = I2C_DUTYCYCLE_2; hi2c1.Init.OwnAddress1 = 0; hi2c1.Init.AddressingMode = I2C_ADDRESSINGMODE_7BIT; hi2c1.Init.DualAddressMode = I2C_DUALADDRESS_DISABLED; hi2c1.Init.OwnAddress2 = 0; hi2c1.Init.GeneralCallMode = I2C_GENERALCALL_DISABLED; hi2c1.Init.NoStretchMode = I2C_NOSTRETCH_DISABLED; HAL_I2C_Init(&hi2c1);


Έριξα την κατάσταση των καταχωρητών αμέσως μετά την προετοιμασία. Έκανα το ίδιο dump σε μια λειτουργική έκδοση στο stm32duino. Αυτό πήρα (με σχόλια στον εαυτό μου)

Καλό (Stm32duino):

40005404: 0 0 1 24 - I2C_CR2: Ενεργοποιήθηκε η διακοπή σφάλματος, 36Mhz
40005408: 0 0 0 0 - I2C_OAR1: μηδενική διεύθυνση

40005410: 0 0 0 AF - I2C_DR: μητρώο δεδομένων

40005418: 0 0 0 0 - I2C_SR2: μητρώο κατάστασης

Κακό (STM32GENERIC):
40005400: 0 0 0 1 - I2C_CR1: Ενεργοποίηση περιφερειακών
40005404: 0 0 0 24 - I2C_CR2: 36Mhz
40005408: 0 0 40 0 ​​- I2C_OAR1: !!! Δεν περιγράφεται bit στο σύνολο καταχωρητών διευθύνσεων
4000540C: 0 0 0 0 - I2C_OAR2: Μητρώο δικής σας διεύθυνσης
40005410: 0 0 0 0 - I2C_DR: μητρώο δεδομένων
40005414: 0 0 0 0 - I2C_SR1: μητρώο κατάστασης
40005418: 0 0 0 2 - I2C_SR2: busy bit set
4000541C: 0 0 80 1E - I2C_CCR: Λειτουργία 400 kHz
40005420: 0 0 0 B - I2C_TRISE

Η πρώτη μεγάλη διαφορά είναι το 14ο bit που έχει οριστεί στον καταχωρητή I2C_OAR1. Αυτό το bit δεν περιγράφεται καθόλου στο φύλλο δεδομένων και εμπίπτει στην δεσμευμένη ενότητα. Είναι αλήθεια, με την προειδοποίηση ότι πρέπει ακόμα να γράψετε ένα εκεί. Εκείνοι. Αυτό είναι ένα σφάλμα στο libmaple. Αλλά επειδή όλα λειτουργούν εκεί, τότε δεν είναι αυτό το πρόβλημα. Ας σκάψουμε περαιτέρω.

Μια άλλη διαφορά είναι ότι το bit απασχολημένου έχει οριστεί. Στην αρχή δεν του έδωσα καμία σημασία, αλλά κοιτάζοντας μπροστά θα πω ότι ήταν αυτός που σήμανε το πρόβλημα!.. Πρώτα όμως πρώτα.

Έφτιαξα τον κώδικα προετοιμασίας χωρίς βιβλιοθήκες.

Εκκίνηση της οθόνης

void sendCommand (i2c_handletypedef * handle, uint8_t cmd) (serialusb.print ("εντολή αποστολής"), serialusb.println (cmd, 16); uint8_t xbuffer; xbuffer = 0x00; xbuffer = cmd;<<1, xBuffer, 2, 10); } ... sendCommand(handle, SSD1306_DISPLAYOFF); sendCommand(handle, SSD1306_SETDISPLAYCLOCKDIV); // 0xD5 sendCommand(handle, 0x80); // the suggested ratio 0x80 sendCommand(handle, SSD1306_SETMULTIPLEX); // 0xA8 sendCommand(handle, 0x3F); sendCommand(handle, SSD1306_SETDISPLAYOFFSET); // 0xD3 sendCommand(handle, 0x0); // no offset sendCommand(handle, SSD1306_SETSTARTLINE | 0x0); // line #0 sendCommand(handle, SSD1306_CHARGEPUMP); // 0x8D sendCommand(handle, 0x14); sendCommand(handle, SSD1306_MEMORYMODE); // 0x20 sendCommand(handle, 0x00); // 0x0 act like ks0108 sendCommand(handle, SSD1306_SEGREMAP | 0x1); sendCommand(handle, SSD1306_COMSCANDEC); sendCommand(handle, SSD1306_SETCOMPINS); // 0xDA sendCommand(handle, 0x12); sendCommand(handle, SSD1306_SETCONTRAST); // 0x81 sendCommand(handle, 0xCF); sendCommand(handle, SSD1306_SETPRECHARGE); // 0xd9 sendCommand(handle, 0xF1); sendCommand(handle, SSD1306_SETVCOMDETECT); // 0xDB sendCommand(handle, 0x40); sendCommand(handle, SSD1306_DISPLAYALLON_RESUME); // 0xA4 sendCommand(handle, SSD1306_DISPLAYON); // 0xA6 sendCommand(handle, SSD1306_NORMALDISPLAY); // 0xA6 sendCommand(handle, SSD1306_INVERTDISPLAY); sendCommand(handle, SSD1306_COLUMNADDR); sendCommand(handle, 0); // Column start address (0 = reset) sendCommand(handle, SSD1306_LCDWIDTH-1); // Column end address (127 = reset) sendCommand(handle, SSD1306_PAGEADDR); sendCommand(handle, 0); // Page start address (0 = reset) sendCommand(handle, 7); // Page end address uint8_t buf; buf = 0x40; for(uint8_t x=1; x<17; x++) buf[x] = 0xf0; // 4 black, 4 white lines for (uint16_t i=0; i<(SSD1306_LCDWIDTH*SSD1306_LCDHEIGHT/8); i++) { HAL_I2C_Master_Transmit(handle, I2C1_DEVICE_ADDRESS<<1, buf, 17, 10); }


Μετά από λίγη προσπάθεια, αυτός ο κώδικας λειτούργησε για μένα (σε αυτήν την περίπτωση, τράβηξε ρίγες). Αυτό σημαίνει ότι το πρόβλημα βρίσκεται στο επίπεδο I2C του STM32GENERIC. Άρχισα να αφαιρώ σταδιακά τον κωδικό μου, αντικαθιστώντας τον με τα κατάλληλα μέρη από τη βιβλιοθήκη. Αλλά μόλις άλλαξα τον κωδικό αρχικοποίησης pin από την εφαρμογή μου στην βιβλιοθήκη, ολόκληρη η μετάδοση I2C άρχισε να λήγει.

Μετά θυμήθηκα το κομμάτι απασχολημένου και προσπάθησα να καταλάβω πότε συμβαίνει. Αποδείχθηκε ότι η σημαία απασχολημένος εμφανίζεται μόλις ο κωδικός προετοιμασίας ενεργοποιήσει το ρολόι I2c. Εκείνοι. Η μονάδα ενεργοποιείται και αμέσως δεν λειτουργεί. Ενδιαφέρων.

Πέφτουμε στην αρχικοποίηση

uint8_t * pv = (uint8_t*) 0x40005418; //Καταχωρητής I2C_SR2. Αναζητάτε τη σημαία BUSY SerialUSB.print("40005418 = "); SerialUSB.println(*pv, 16); // Εκτυπώνει 0 __HAL_RCC_I2C1_CLK_ENABLE(); SerialUSB.print("40005418 = "); SerialUSB.println(*pv, 16); //Εκτυπώσεις 2


Πάνω από αυτόν τον κωδικό υπάρχει μόνο η προετοιμασία των pin. Λοιπόν, τι να κάνετε - καλύψτε τον εντοπισμό σφαλμάτων με εκτυπώσεις σε όλη τη γραμμή και εκεί

Αρχικοποίηση ακίδων STM32GENERIC

void stm32AfInit(const stm32_af_pin_list_type list, int μέγεθος, const void *instance, GPIO_TypeDef *port, uint32_t pin, uint32_t mode, uint32_t pull) ( ... GPIO_InitTypeDef GPIOIntStrucit; Struct.Mode = λειτουργία, GPIO_InitStruct.Pull = τράβηγμα; GPIO_InitStruct.Speed ​​= GPIO_SPEED_FREQ_VERY_HIGH; HAL_GPIO_Init(port, &GPIO_InitStruct); ...)


Αλλά κακή τύχη - το GPIO_InitStruct έχει συμπληρωθεί σωστά. Μόνο το δικό μου λειτουργεί, αλλά αυτό όχι. Πραγματικά μυστικέ!!! Όλα είναι σύμφωνα με το σχολικό βιβλίο, αλλά τίποτα δεν λειτουργεί. Μελέτησα τον κώδικα της βιβλιοθήκης γραμμή προς γραμμή, αναζητώντας οτιδήποτε ύποπτο. Τελικά βρήκα αυτόν τον κωδικό (καλεί την παραπάνω συνάρτηση)

Άλλο ένα κομμάτι αρχικοποίησης

void stm32AfI2CInit(const I2C_TypeDef *instance, ...) ( stm32AfInit(chip_af_i2c_sda, ...); stm32AfInit(chip_af_i2c_scl, ...); )


Βλέπετε κάποιο σφάλμα σε αυτό; Και αυτή είναι! Αφαίρεσα ακόμη και περιττές παραμέτρους για να γίνει πιο ξεκάθαρο το πρόβλημα. Γενικά, η διαφορά είναι ότι ο κώδικάς μου αρχικοποιεί και τα δύο pin ταυτόχρονα σε μια δομή, και τον κωδικό STM32GENERIC ένα προς ένα. Προφανώς ο κωδικός αρχικοποίησης pin επηρεάζει κατά κάποιο τρόπο το επίπεδο σε αυτόν τον ακροδέκτη. Πριν από την αρχικοποίηση, δεν βγαίνει τίποτα σε αυτόν τον ακροδέκτη και η αντίσταση ανεβάζει το επίπεδο στο ένα. Τη στιγμή της αρχικοποίησης, για κάποιο λόγο ο ελεγκτής θέτει μηδέν στο αντίστοιχο σκέλος.

Αυτό το γεγονός από μόνο του είναι ακίνδυνο. Αλλά το πρόβλημα είναι ότι το να χαμηλώσετε τη γραμμή SDA ενώ ανεβάζετε τη γραμμή SCL είναι μια συνθήκη έναρξης για το δίαυλο i2c. Εξαιτίας αυτού, ο δέκτης του ελεγκτή τρελαίνεται, ορίζει τη σημαία BUSY και αρχίζει να περιμένει για δεδομένα. Αποφάσισα να μην αποκόψω τη βιβλιοθήκη για να προσθέσω τη δυνατότητα προετοιμασίας πολλών ακίδων ταυτόχρονα. Αντίθετα, απλώς άλλαξα αυτές τις 2 γραμμές - η προετοιμασία της οθόνης ήταν επιτυχής. Η επιδιόρθωση υιοθετήθηκε στο STM32GENERIC.

Παρεμπιπτόντως, στο libmaple η προετοιμασία του διαύλου γίνεται με ενδιαφέροντα τρόπο. Πριν ξεκινήσετε την προετοιμασία των περιφερειακών i2c στο λεωφορείο, κάνετε πρώτα μια επαναφορά. Για να γίνει αυτό, η βιβλιοθήκη αλλάζει τις ακίδες σε κανονική λειτουργία GPIO και κουνάει αυτά τα πόδια αρκετές φορές, προσομοιώνοντας τις ακολουθίες έναρξης και διακοπής. Αυτό βοηθά στην αναζωογόνηση συσκευών που έχουν κολλήσει στο λεωφορείο. Δυστυχώς, δεν υπάρχει κάτι αντίστοιχο στο HAL. Μερικές φορές η οθόνη μου κολλάει και τότε η μόνη λύση είναι να απενεργοποιήσω την τροφοδοσία.

Εκκίνηση του i2c από το stm32duino

/** * @brief Επαναφορά ενός διαύλου I2C. * * Η επαναφορά ολοκληρώνεται με χρονισμό των παλμών έως ότου τυχόν εξαρτημένοι σκλάβοι * απελευθερώσουν το SDA και το SCL, και στη συνέχεια δημιουργώντας μια συνθήκη START και μετά μια συνθήκη STOP *. * * @param dev I2C συσκευή */ void i2c_bus_reset(const i2c_dev *dev) ( /* Απελευθερώστε και τις δύο γραμμές */ i2c_master_release_bus(dev); /* * Βεβαιωθείτε ότι ο δίαυλος είναι ελεύθερος χρονίζοντάς τον έως ότου οποιοσδήποτε σκλάβος απελευθερώσει το * δίαυλο. */ ενώ (!gpio_read_bit(sda_port(dev), dev->sda_pin)) ( /* Περιμένετε να τελειώσει οποιοδήποτε ρολόι εκτείνεται */ ενώ (!gpio_read_bit(scl_port(dev), dev->scl_pin)) ; delay_us(10 ); /* Τραβήξτε χαμηλό */ gpio_write_bit(scl_port(dev), dev->scl_pin, 0); delay_us(10); /* Release high again */ gpio_write_bit(scl_port(dev), dev->scl_pin, 1); delay_us(10); ) /* Δημιουργία συνθήκης έναρξης και διακοπής */ gpio_write_bit(sda_port(dev), dev->sda_pin, 0); delay_us(10); gpio_write_bit(scl_port(dev), dev->scl_pin, 0); delay_us(10); gpio_write_bit(scl_port(dev), dev->scl_pin, 1); delay_us(10); gpio_write_bit(sda_port(dev), dev->sda_pin, 1); )

Εκεί πάλι: UART

Χάρηκα που επιτέλους επέστρεψα στον προγραμματισμό και συνέχισα να γράφω χαρακτηριστικά. Το επόμενο μεγάλο κομμάτι ήταν η σύνδεση της κάρτας SD μέσω SPI. Αυτό από μόνο του είναι μια συναρπαστική, ενδιαφέρουσα και επίπονη δραστηριότητα. Σίγουρα θα μιλήσω για αυτό ξεχωριστά σε επόμενο άρθρο. Ένα από τα προβλήματα ήταν το υψηλό φορτίο της CPU (>50%). Αυτό έθεσε υπό αμφισβήτηση την ενεργειακή απόδοση της συσκευής. Και ήταν άβολο να χρησιμοποιήσετε τη συσκευή, επειδή... Το UI ήταν τρομερά ηλίθιο.

Κατανοώντας το θέμα, βρήκα τον λόγο για αυτή την κατανάλωση πόρων. Όλη η εργασία με την κάρτα SD έγινε byte byte, χρησιμοποιώντας τον επεξεργαστή. Εάν ήταν απαραίτητο να γράψετε ένα μπλοκ δεδομένων στην κάρτα, τότε για κάθε byte καλείται η λειτουργία αποστολής byte

Για (uint16_t i = 0; i< 512; i++) { spiSend(src[i]);
Όχι, δεν είναι σοβαρό! Υπάρχει DMA! Ναι, η βιβλιοθήκη SD (αυτή που συνοδεύει το Arduino) είναι αδέξια και πρέπει να αλλάξει, αλλά το πρόβλημα είναι πιο παγκόσμιο. Η ίδια εικόνα παρατηρείται και στη βιβλιοθήκη της οθόνης, ενώ ακόμη και η ακρόαση του UART έγινε μέσω δημοσκόπησης. Γενικά, άρχισα να σκέφτομαι ότι το να ξαναγράψω όλα τα στοιχεία στο HAL δεν είναι τόσο ανόητη ιδέα.

Ξεκίνησα, φυσικά, με κάτι πιο απλό - ένα πρόγραμμα οδήγησης UART που ακούει τη ροή δεδομένων από το GPS. Η διεπαφή Arduino δεν σας επιτρέπει να επισυνάψετε στη διακοπή UART και να αρπάξετε τους εισερχόμενους χαρακτήρες εν κινήσει. Ως αποτέλεσμα, ο μόνος τρόπος απόκτησης δεδομένων είναι μέσω συνεχών δημοσκοπήσεων. Φυσικά, πρόσθεσα το vTaskDelay(10) στον χειριστή GPS για να μειώσω τουλάχιστον λίγο το φορτίο, αλλά στην πραγματικότητα αυτό είναι δεκανίκι.

Η πρώτη σκέψη, φυσικά, ήταν να επισυνάψουμε DMA. Θα λειτουργούσε ακόμη και αν δεν υπήρχε το πρωτόκολλο NMEA. Το πρόβλημα είναι ότι σε αυτό το πρωτόκολλο, οι πληροφορίες απλώς ρέουν και τα μεμονωμένα πακέτα (γραμμές) διαχωρίζονται από έναν χαρακτήρα αλλαγής γραμμής. Επιπλέον, κάθε γραμμή μπορεί να έχει διαφορετικά μήκη. Εξαιτίας αυτού, δεν είναι γνωστό εκ των προτέρων πόσα δεδομένα πρέπει να ληφθούν. Το DMA δεν λειτουργεί έτσι - ο αριθμός των byte πρέπει να οριστεί εκ των προτέρων κατά την προετοιμασία της μεταφοράς. Εν ολίγοις, το DMA δεν χρειάζεται πλέον, οπότε ψάχνουμε άλλη λύση.

Εάν κοιτάξετε προσεκτικά τη σχεδίαση της βιβλιοθήκης NeoGPS, μπορείτε να δείτε ότι η βιβλιοθήκη δέχεται δεδομένα εισόδου byte προς byte, αλλά οι τιμές ενημερώνονται μόνο όταν φτάσει ολόκληρη η γραμμή (για την ακρίβεια, μια παρτίδα πολλών γραμμών ). Οτι. Δεν έχει καμία διαφορά εάν θα τροφοδοτηθούν τα byte της βιβλιοθήκης ένα κάθε φορά κατά τη λήψη τους ή στη συνέχεια όλα ταυτόχρονα. Έτσι, μπορείτε να εξοικονομήσετε χρόνο επεξεργαστή αποθηκεύοντας τη γραμμή που λάβατε σε ένα buffer και μπορείτε να το κάνετε απευθείας στη διακοπή. Όταν ληφθεί ολόκληρη η γραμμή, μπορεί να ξεκινήσει η επεξεργασία.

Προκύπτει το ακόλουθο σχέδιο

Κατηγορία οδηγών UART

// Μέγεθος buffer εισόδου UART const uint8_t gpsBufferSize = 128; // Αυτή η κλάση χειρίζεται τη διεπαφή UART που λαμβάνει χαρακτήρες από το GPS και τους αποθηκεύει σε μια κλάση προσωρινής μνήμης GPS_UART ( // Χειρισμός υλικού UART UART_HandleTypeDef uartHandle; // Λήψη προσωρινής μνήμης δακτυλίου uint8_t rxBuffer; πτητικό uint8_t lastReadIndex = 0; //Volat / Λήψη νήματος GPS TaskHandle_t xGPSThread = NULL;


Αν και η προετοιμασία έχει αντιγραφεί από το STM32GENERIC, αντιστοιχεί πλήρως σε αυτό που προσφέρει το CubeMX

Αρχικοποίηση UART

void init() ( // Επαναφορά δεικτών (ακριβώς σε περίπτωση που κάποιος καλέσει την init() πολλές φορές) lastReadIndex = 0; lastReceivedIndex = 0; // Αρχικοποίηση λαβής νήματος GPS xGPSThread = xTaskGetCurrentTaskHandle(); // Ενεργοποίηση χρονισμού peri_RCOPER_CL_ABHal ); __HAL_RCC_USART1_CLK_ENABLE(); // Καρφίτσες έναρξης σε λειτουργία εναλλακτικής λειτουργίας GPIO_InitTypeDef GPIO_InitStruct; GPIO_InitStruct.Pin = GPIO_PIN_9; //TX pin GPIO_InitStruct.Mode Struc.PIO_PIO O_SPEED_FRE Q_HIGH; HAL_GPIO_Init(GPIOA, &GPIO_InitStruct); GPIO_InitStruct .Pin = GPIO_PIN_10; //RX pin GPIO_InitStruct.Mode = GPIO_MODE_INPUT; GPIO_InitStruct.Pull = GPIO_NOPULL; HAL_GPIO_Init(GPIOA, &GPIO_InitStruct); //HBaandHle.Init; Rate = 9600, uart Handle.Init. WordLength = uart_wordlength_8b; uarthandle.init.stopbits = uart_stopbits_1; uarthandle.init.parity = uart_parity_none; uarthandle.init.mode = uart_mode_tx_rx; uarthandle.init.hwflowtl = uart_parity_none; uartHandle.Init.OverSampling = UART_OVERSAMPLING_16; HAL_UART_Init(&uartHandle); // Θα χρησιμοποιήσουμε διακοπή UART για τη λήψη δεδομένων HAL_NVIC_SetPriority(USART1_IRQn, 6, 0); HAL_NVIC_EnableIRQ(USART1_IRQn); // Θα περιμένουμε να ληφθεί ένα μόνο δικαίωμα χαρακτήρων απευθείας στο buffer HAL_UART_Receive_IT(&uartHandle, rxBuffer, 1); )


Στην πραγματικότητα, το pin TX δεν ήταν δυνατό να αρχικοποιηθεί, αλλά το uartHandle.Init.Mode θα μπορούσε να οριστεί σε UART_MODE_RX - θα το λάβουμε μόνο. Ωστόσο, ας είναι - τι γίνεται αν χρειαστεί να ρυθμίσω με κάποιο τρόπο Μονάδα GPSκαι γράψτε εντολές σε αυτό.

Ο σχεδιασμός αυτής της κατηγορίας θα μπορούσε να ήταν καλύτερος αν δεν υπήρχαν οι περιορισμοί της αρχιτεκτονικής HAL. Έτσι, δεν μπορούμε απλά να ρυθμίσουμε τη λειτουργία, λένε, να αποδεχόμαστε τα πάντα, να επισυνάπτουμε απευθείας στη διακοπή και να αρπάζουμε τα λαμβανόμενα byte απευθείας από τον καταχωρητή λήψης. Πρέπει να πούμε εκ των προτέρων στο HAL πόσα και πού θα λάβουμε byte - οι ίδιοι οι αντίστοιχοι χειριστές θα γράψουν τα ληφθέντα byte στο παρεχόμενο buffer. Για το σκοπό αυτό, στην τελευταία γραμμή της συνάρτησης αρχικοποίησης υπάρχει μια κλήση στο HAL_UART_Receive_IT(). Επειδή το μήκος της συμβολοσειράς είναι άγνωστο εκ των προτέρων, πρέπει να παίρνουμε ένα byte τη φορά.

Πρέπει επίσης να δηλώσετε έως και 2 επανακλήσεις. Το ένα είναι ένας χειριστής διακοπών, αλλά η δουλειά του είναι απλώς να καλέσει τον χειριστή από το HAL. Η δεύτερη συνάρτηση είναι η «επανάκληση» του HAL ότι το byte έχει ήδη ληφθεί και βρίσκεται ήδη στο buffer.

Επανακλήσεις UART

// Προώθηση της επεξεργασίας διακοπής UART στο εξωτερικό "C" HAL void USART1_IRQHandler(void) ( HAL_UART_IRQHandler(gpsUart.getUartHandle()); ) // Το HAL καλεί αυτήν την επανάκληση όταν λαμβάνει έναν χαρακτήρα από το UART. Προωθήστε το στην εξωτερική κλάση "C" void HAL_UART_RxCpltCallback(UART_HandleTypeDef *uartHandle) ( gpsUart.charReceivedCB(); )


Η μέθοδος charReceivedCB() προετοιμάζει το HAL για να λάβει το επόμενο byte. Είναι επίσης αυτό που καθορίζει ότι η γραμμή έχει ήδη τελειώσει και ότι αυτό μπορεί να σηματοδοτηθεί στο κύριο πρόγραμμα. Ένας σηματοφόρος σε λειτουργία σήματος θα μπορούσε να χρησιμοποιηθεί ως μέσο συγχρονισμού, αλλά για τόσο απλούς σκοπούς συνιστάται η χρήση άμεσων ειδοποιήσεων.

Επεξεργασία ενός ληφθέντος byte

// Λήφθηκε Χαρακτήρας, προετοιμαστείτε για το επόμενο ενσωματωμένο κενό charReceivedCB() ( char lastReceivedChar = rxBuffer; lastReceivedIndex++; HAL_UART_Receive_IT(&uartHandle, rxBuffer + (lastReceivedIndex % gpsBufferSize λήφθηκε το σύμβολο /, η γραμμή λήφθηκε εάν το σύμβολο EOLread a1), ; είναι διαθέσιμο για ανάγνωση εάν(lastReceivedChar == "\n") vTaskNotifyGiveFromISR(xGPSThread, NULL); )


Η συνάρτηση απόκρισης (αναμονής) είναι waitForString(). Η αποστολή του είναι απλώς να κρεμαστεί στο αντικείμενο συγχρονισμού και να περιμένει (ή να βγαίνει με χρονικό όριο)

Περιμένοντας το τέλος της γραμμής

// Περιμένετε μέχρι να ληφθεί ολόκληρη η γραμμή bool waitForString() ( return ulTaskNotifyTake(pdTRUE, 10); )


Δουλεύει κάπως έτσι. Το νήμα που είναι υπεύθυνο για το GPS συνήθως κοιμάται στη συνάρτηση waitForString(). Τα byte που προέρχονται από το GPS προστίθενται σε ένα buffer από τον χειριστή διακοπών. Εάν φτάσει ο χαρακτήρας \n (τέλος γραμμής), τότε η διακοπή ξυπνά το κύριο νήμα, το οποίο αρχίζει να χύνει bytes από το buffer στον αναλυτή. Λοιπόν, όταν ο αναλυτής ολοκληρώσει την επεξεργασία του πακέτου μηνυμάτων, θα ενημερώσει τα δεδομένα στο μοντέλο GPS.

Ροή GPS

void vGPSTask(void *pvParameters) ( // Η προετοιμασία του GPS πρέπει να γίνει εντός του νήματος GPS καθώς αποθηκεύεται η λαβή νήματος // και χρησιμοποιείται αργότερα για σκοπούς συγχρονισμού gpsUart.init(); για (;;) ( // Περιμένετε έως ότου ολοκληρωθεί η συμβολοσειρά λήφθηκε if(!gpsUart.waitForString()) συνέχεια; // Ανάγνωση ληφθέντων συμβολοσειρών και ανάλυση του χαρακτήρος της ροής GPS κατά char while(gpsUart.available()) ( int c = gpsUart.readChar(); //SerialUSB.write(c) ; gpsParser.handle(c); ) if(gpsParser.available()) ( GPSDataModel::instance().processNewGPSFix(gpsParser.read()); GPSDataModel::instance().processNewSatellitesData(gpsParser.Parser.satellites. ); ) vTaskDelay(10); ) )


Συνάντησα μια πολύ μη τετριμμένη στιγμή στην οποία είχα κολλήσει για αρκετές μέρες. Φαίνεται ότι ο κώδικας συγχρονισμού ελήφθη από τα παραδείγματα, αλλά στην αρχή δεν λειτούργησε - κατέρρευσε ολόκληρο το σύστημα. Νόμιζα ότι το πρόβλημα ήταν στις άμεσες ειδοποιήσεις (συναρτήσεις xTaskNotifyXXX), το άλλαξα σε κανονικούς σηματοφόρους, αλλά η εφαρμογή εξακολουθούσε να κολλάει.

Αποδείχθηκε ότι πρέπει να είστε πολύ προσεκτικοί με την προτεραιότητα διακοπής. Από προεπιλογή, έβαλα όλες τις διακοπές σε μηδέν (την υψηλότερη) προτεραιότητα. Αλλά το FreeRTOS έχει την απαίτηση οι προτεραιότητες να βρίσκονται εντός ενός δεδομένου εύρους. Οι διακοπές με πολύ υψηλή προτεραιότητα δεν μπορούν να καλέσουν λειτουργίες FreeRTOS. Μπορούν να καλέσουν μόνο διακοπές με ρύθμιση προτεραιότηταςLIBRARY_MAX_SYSCALL_INTERRUPT_PRIORITY και κάτω λειτουργίες του συστήματος(καλή εξήγηση και ). Αυτή η προεπιλεγμένη ρύθμιση έχει οριστεί στο 5. Άλλαξα την προτεραιότητα διακοπής UART σε 6 και όλα λειτούργησαν.

Εκεί πάλι: I2C μέσω DMA

Τώρα μπορείτε να κάνετε κάτι πιο περίπλοκο, όπως το πρόγραμμα οδήγησης οθόνης. Αλλά εδώ πρέπει να κάνουμε μια εκδρομή στη θεωρία του λεωφορείου I2C. Αυτός ο ίδιος ο δίαυλος δεν ρυθμίζει το πρωτόκολλο μεταφοράς δεδομένων στο δίαυλο - μπορείτε είτε να γράψετε byte είτε να τα διαβάσετε. Μπορείτε ακόμη να γράψετε και στη συνέχεια να διαβάσετε σε μία συναλλαγή (για παράδειγμα, να γράψετε μια διεύθυνση και μετά να διαβάσετε δεδομένα σε αυτήν τη διεύθυνση).

Ωστόσο, οι περισσότερες συσκευές ορίζουν το πρωτόκολλο υψηλότερου επιπέδου με τον ίδιο σχεδόν τρόπο. η συσκευή παρέχει στο χρήστη ένα σύνολο καταχωρητών, το καθένα με τη δική του διεύθυνση. Επιπλέον, στο πρωτόκολλο επικοινωνίας, το πρώτο byte (ή πολλά) σε κάθε συναλλαγή καθορίζει τη διεύθυνση του κελιού (μητρώου) στο οποίο θα διαβάσουμε ή θα γράψουμε περαιτέρω. Σε αυτήν την περίπτωση, είναι επίσης δυνατή η ανταλλαγή πολλών byte με το στυλ "τώρα θα γράφουμε/διαβάζουμε πολλά byte ξεκινώντας από αυτήν τη διεύθυνση". Η τελευταία επιλογή είναι καλή για DMA.

Δυστυχώς, η οθόνη που βασίζεται στον ελεγκτή SSD1306 παρέχει ένα εντελώς διαφορετικό πρωτόκολλο - εντολή. Το πρώτο byte κάθε συναλλαγής είναι το χαρακτηριστικό "command or data". Στην περίπτωση μιας εντολής, το δεύτερο byte είναι ο κωδικός εντολής. Εάν μια εντολή χρειάζεται ορίσματα, μεταβιβάζονται ως ξεχωριστές εντολές μετά την πρώτη. Για να αρχικοποιήσετε την οθόνη, πρέπει να στείλετε περίπου 30 εντολές, αλλά δεν μπορούν να τοποθετηθούν σε έναν πίνακα και να σταλούν σε ένα μπλοκ. Πρέπει να τα στέλνετε ένα-ένα.

Αλλά κατά την αποστολή μιας σειράς pixels (frame buffer), είναι πολύ πιθανό να χρησιμοποιήσετε υπηρεσίες DMA. Αυτό θα προσπαθήσουμε.

Αλλά η βιβλιοθήκη Adafruit_SSD1306 είναι γραμμένη πολύ αδέξια και είναι αδύνατο να την συμπιέσει κανείς λίγο αίμαδεν δουλεύει. Προφανώς η βιβλιοθήκη γράφτηκε για πρώτη φορά για να επικοινωνεί με την οθόνη μέσω SPI. Στη συνέχεια, κάποιος πρόσθεσε υποστήριξη I2C, αλλά η υποστήριξη SPI παρέμεινε ενεργοποιημένη. Στη συνέχεια, κάποιος άρχισε να προσθέτει κάθε είδους βελτιστοποιήσεις χαμηλού επιπέδου και να τις κρύβει πίσω από τα ifdefs. Ως αποτέλεσμα, αποδείχθηκε ότι ήταν noodles κώδικα υποστήριξης διαφορετικές διεπαφές. Έτσι, πριν προχωρήσω, έπρεπε να το χτενίσω.

Στην αρχή προσπάθησα να το βάλω σε μια σειρά πλαισιώνοντας τον κώδικα για διαφορετικές διεπαφές με ifdefs. Αλλά αν θέλω να γράψω κώδικα επικοινωνίας με την οθόνη, να χρησιμοποιήσω DMA και συγχρονισμό μέσω FreeRTOS, τότε δεν θα μπορώ να κάνω πολλά. Θα είναι πιο ακριβές, αλλά αυτός ο κωδικός θα πρέπει να γραφτεί απευθείας στον κώδικα της βιβλιοθήκης. Ως εκ τούτου, αποφάσισα να επεξεργαστώ ξανά τη βιβλιοθήκη για άλλη μια φορά, να δημιουργήσω μια διεπαφή και να βάλω κάθε πρόγραμμα οδήγησης σε ξεχωριστή τάξη. Ο κώδικας έγινε πιο καθαρός και θα ήταν δυνατή η ανώδυνη προσθήκη υποστήριξης για νέα προγράμματα οδήγησης χωρίς αλλαγή της ίδιας της βιβλιοθήκης.

Διεπαφή προγράμματος οδήγησης οθόνης

// Διεπαφή για πρόγραμμα οδήγησης υλικού // Το Adafruit_SSD1306 δεν λειτουργεί απευθείας με το υλικό // Όλα τα αιτήματα επικοινωνίας προωθούνται στην κατηγορία προγράμματος οδήγησης ISSD1306Driver ( public: virtual void begin() = 0; virtual void sendCommand(uint8_t cmd) = 0 ; εικονικό κενό sendData(uint8_t * δεδομένα, μέγεθος_t μέγεθος) = 0; );


Λοιπόν πάμε. Έχω ήδη δείξει την προετοιμασία I2C. Δεν έχει αλλάξει τίποτα εκεί. Αλλά η αποστολή της εντολής έγινε λίγο πιο εύκολη. Θυμάστε όταν μίλησα για τη διαφορά μεταξύ πρωτοκόλλων εγγραφής και εντολών για συσκευές I2C; Και παρόλο που η οθόνη υλοποιεί ένα πρωτόκολλο εντολών, μπορεί να προσομοιωθεί αρκετά καλά χρησιμοποιώντας ένα πρωτόκολλο καταχωρητή. Απλά πρέπει να φανταστείτε ότι η οθόνη έχει μόνο 2 καταχωρητές - 0x00 για εντολές και 0x40 για δεδομένα. Και το HAL παρέχει ακόμη και μια λειτουργία για αυτού του είδους τη μεταφορά

Αποστολή εντολής στην οθόνη

void DisplayDriver::sendCommand(uint8_t cmd) ( HAL_I2C_Mem_Write(&handle, i2c_addr, 0x00, 1, &cmd, 1, 10); )


Στην αρχή δεν ήταν πολύ σαφές για την αποστολή δεδομένων. Ο αρχικός κώδικας έστελνε δεδομένα σε μικρά πακέτα των 16 byte

Παράξενος κωδικός αποστολής δεδομένων

για (uint16_t i=0; i


Προσπάθησα να παίξω με το μέγεθος του πακέτου και να στείλω μεγαλύτερα πακέτα, αλλά στην καλύτερη περίπτωση είχα μια τσαλακωμένη οθόνη. Λοιπόν, ή όλα κρέμονταν.

Περικομμένη οθόνη



Ο λόγος αποδείχθηκε ασήμαντος - υπερχείλιση buffer. Η κλάση Wire από το Arduino (τουλάχιστον STM32GENERIC) παρέχει το δικό της buffer μόνο 32 byte. Αλλά γιατί χρειαζόμαστε ένα επιπλέον buffer εάν η κλάση Adafruit_SSD1306 έχει ήδη ένα; Επιπλέον, με το HAL η αποστολή γίνεται σε μία γραμμή

Σωστή μεταφορά δεδομένων

void DisplayDriver::sendData(uint8_t * data, size_t μέγεθος) ( HAL_I2C_Mem_Write(&handle, i2c_addr, 0x40, 1, data, size, 10); )


Έτσι, η μισή μάχη έχει τελειώσει - γράψαμε ένα πρόγραμμα οδήγησης για την οθόνη σε καθαρό HAL. Αλλά σε αυτήν την έκδοση εξακολουθεί να απαιτεί πόρους - 12% του επεξεργαστή για οθόνη 128x32 και 23% για οθόνη 128x64. Η χρήση του DMA είναι πραγματικά ευπρόσδεκτη εδώ.

Αρχικά, ας αρχικοποιήσουμε το DMA. Θέλουμε να εφαρμόσουμε την προώθηση δεδομένων στο I2C No. 1 και αυτή η συνάρτηση ζει στο έκτο κανάλι DMA. Ξεκινήστε την αντιγραφή byte-byte από τη μνήμη στα περιφερειακά

Ρύθμιση DMA για I2C

// Ενεργοποίηση ρολογιού ελεγκτή DMA __HAL_RCC_DMA1_CLK_ENABLE(); // Αρχικοποίηση DMA hdma_tx.Instance = DMA1_Channel6; hdma_tx.Init.Direction = DMA_MEMORY_TO_PERIPH; hdma_tx.Init.PeriphInc = DMA_PINC_DISABLE; hdma_tx.Init.MemInc = DMA_MINC_ENABLE; hdma_tx.Init.PeriphDataAlignment = DMA_PDATAALIGN_BYTE; hdma_tx.Init.MemDataAlignment = DMA_MDATAALIGN_BYTE; hdma_tx.Init.Mode = DMA_NORMAL; hdma_tx.Init.Priority = DMA_PRIORITY_LOW; HAL_DMA_Init(&hdma_tx); // Συσχετίστε την αρχικοποιημένη λαβή DMA με τη λαβή I2C __HAL_LINKDMA(&handle, hdmatx, hdma_tx); /* DMA interrupt init */ /* DMA1_Channel6_IRQn διαμόρφωση διακοπής */ HAL_NVIC_SetPriority(DMA1_Channel6_IRQn, 7, 0); HAL_NVIC_EnableIRQ(DMA1_Channel6_IRQn);


Οι διακοπές είναι απαραίτητο μέρος του σχεδιασμού. Διαφορετικά, η συνάρτηση HAL_I2C_Mem_Write_DMA() θα ξεκινήσει μια συναλλαγή I2C, αλλά κανείς δεν θα την ολοκληρώσει. Και πάλι έχουμε να κάνουμε με τον δυσκίνητο σχεδιασμό HAL και την ανάγκη για έως και δύο επανακλήσεις. Όλα είναι ακριβώς τα ίδια όπως με το UART. Μια συνάρτηση είναι ένας χειριστής διακοπών - απλώς ανακατευθύνουμε την κλήση στο HAL. Η δεύτερη λειτουργία είναι ένα σήμα ότι τα δεδομένα έχουν ήδη σταλεί.

Χειριστές διακοπής DMA

extern "C" void DMA1_Channel6_IRQHandler(void) ( HAL_DMA_IRQHandler(displayDriver.getDMAHandle()); ) extern "C" void HAL_I2C_MemTxCpltCallback(I2C_HandleTypeDef displayCompled(Bri2));


Φυσικά, δεν θα κάνουμε συνεχώς δημοσκοπήσεις στο I2C για να δούμε αν έχει ήδη τελειώσει η μεταφορά; Αντίθετα, πρέπει να κοιμάστε στο αντικείμενο συγχρονισμού και να περιμένετε μέχρι να ολοκληρωθεί η μεταφορά

Μεταφορά δεδομένων μέσω DMA με συγχρονισμό

void DisplayDriver::sendData(uint8_t * data, size_t μέγεθος) ( // Έναρξη μεταφοράς δεδομένων HAL_I2C_Mem_Write_DMA(&handle, i2c_addr, 0x40, 1, data, size); // Περιμένετε μέχρι να ολοκληρωθεί η μεταφορά ulTaskNotidifyTake, 1) DisplayDriver::transferCompletedCB() ( // Συνέχιση νήματος εμφάνισης vTaskNotifyGiveFromISR(xDisplayThread, NULL); )


Η μεταφορά δεδομένων διαρκεί ακόμα 24 ms - αυτός είναι σχεδόν καθαρός χρόνος μεταφοράς 1 kB (μέγεθος buffer οθόνης) στα 400 kHz. Μόνο που σε αυτή την περίπτωση, τις περισσότερες φορές ο επεξεργαστής απλώς κοιμάται (ή κάνει άλλα πράγματα). Το συνολικό φορτίο της CPU μειώθηκε από 23% σε μόλις 1,5-2%. Νομίζω ότι αυτή η φιγούρα άξιζε να αγωνιστεί!

Εκεί πάλι: SPI μέσω DMA

Η σύνδεση μιας κάρτας SD μέσω SPI ήταν κατά κάποιο τρόπο ευκολότερη - εκείνη τη στιγμή άρχισα να εγκαθιστώ τη βιβλιοθήκη sdfat και εκεί οι καλοί άνθρωποι είχαν ήδη διαχωρίσει την επικοινωνία με την κάρτα σε μια ξεχωριστή διεπαφή προγράμματος οδήγησης. Είναι αλήθεια ότι με τη βοήθεια των ορισμών μπορείτε να επιλέξετε μόνο μία από τις 4 έτοιμες εκδόσεις προγραμμάτων οδήγησης, αλλά αυτό θα μπορούσε εύκολα να χαθεί και να αντικατασταθεί με τη δική σας υλοποίηση.

Διεπαφή προγράμματος οδήγησης SPI για εργασία με κάρτα SD

// Αυτή είναι προσαρμοσμένη υλοποίηση της κλάσης SPI Driver. Η βιβλιοθήκη SdFat // χρησιμοποιεί αυτήν την κλάση για πρόσβαση στην κάρτα SD μέσω SPI // // Η κύρια πρόθεση αυτής της υλοποίησης είναι να οδηγήσει τη μεταφορά δεδομένων // μέσω DMA και να συγχρονίσει με τις δυνατότητες του FreeRTOS. κλάση SdFatSPIDriver: δημόσιο SdSpiBaseDriver ( // Μονάδα SPI SPI_HandleTypeDef spiHandle; // Χειρισμός νήματος GPS TaskHandle_t xSDThread = NULL; δημόσιο: SdFatSPIDriver(); εικονικό κενό activate(); virtual void activate(); virtual void activate(); virtual_voidt begin(uint) uint8_t receive(); εικονικό uint8_t λήψη (uint8_t* buf, size_t n); εικονικό κενό αποστολή (uint8_t δεδομένα); εικονικό κενό αποστολή (const uint8_t* buf, size_t n); εικονικό κενό select(); εικονικό κενό setSpiSettings(SPISettings spiSettings virtual void unselect(); );


Όπως και πριν, ξεκινάμε με κάτι απλό - με μια υλοποίηση δρυός χωρίς κανένα DMA. Η εκκίνηση δημιουργείται εν μέρει από το CubeMX και εν μέρει συγχωνεύεται με την υλοποίηση SPI του STM32GENERIC

Αρχικοποίηση SPI

SdFatSPIDriver::SdFatSPIDriver() ( ) //void SdFatSPIDriver::activate(); void SdFatSPIDriver::begin(uint8_t chipSelectPin) ( // Παράβλεψη περασμένης καρφίτσας CS - Αυτό το πρόγραμμα οδήγησης λειτουργεί με ένα προκαθορισμένο ένα (void)chipSelectPin; // Εκκίνηση χειρισμού νήματος GPS xSDThread = xTaskGetCurrentTaskHandle (////////). CLK_ENABLE() ; __HAL_RCC_SPI1_CLK_ENABLE(); // Καρφίτσες έναρξης GPIO_InitTypeDef GPIO_InitStruct; GPIO_InitStruct.Pin = GPIO_PIN_5|GPIO_PIN_7; //MOSI & SCK GPIO_InitStructrucPIOT. peed = GPIO_ SPEED_FREQ_HIGH;HAL_GPIO_Init(GPIOA, &GPIO_InitStruct);GPIO_InitStruct.Pin = GPIO_PIN_6; //MISO GPIO_InitStruct.Mode = GPIO_MODE_INPUT; GPIO_InitStruct.Pull = GPIO_NOPULL; HAL_GPIO_Init(GPIOA, &GPIO_InitStruct); GPIO_InitStruct./PIO_InitStruct. = GPIO_MODE_OUTPUT_PP; GPIO_InitStruct.Speed ​​= GPIO_SPEED_FREQ_HIGH;HAL_GPIO_Init( GPIOA, &GPIO_InitStruct); // Ορισμός CS pin High από προεπιλογή HAL_GPIO_WritePin(GPIOA, GPIO_PIN_4, GPIO_PIN_SET); // Init SPI spiHandle.Instance = SPI1; spiHandle.Init.Mode = SPI_MODE_MASTER; spiHandle.Init.Direction = SPI_DIRECTION_2LINES; spiHandle.Init.DataSize = SPI_DATASIZE_8BIT; spiHandle.Init.CLKΠολικότητα = SPI_POLARITY_LOW; spiHandle.Init.CLKPhase = SPI_PHASE_1EDGE; spiHandle.Init.NSS = SPI_NSS_SOFT; spiHandle.Init.BaudRatePrescaler = SPI_BAUDRATEPRESCALER_256; spiHandle.Init.FirstBit = SPI_FIRSTBIT_MSB; spiHandle.Init.TIMode = SPI_TIMODE_DISABLE; spiHandle.Init.CRCCυπολογισμός = SPI_CRCCALCULATION_DISABLE; spiHandle.Init.CRCPΠολυώνυμο = 10; HAL_SPI_Init(&spiHandle); __HAL_SPI_ENABLE(&spiHandle); )


Η σχεδίαση διεπαφής είναι προσαρμοσμένη για το Arduino με καρφίτσες αριθμημένες με έναν αριθμό. Στην περίπτωσή μου, δεν είχε νόημα να ρυθμίσω τον ακροδέκτη CS μέσω των παραμέτρων - έχω αυτό το σήμα αυστηρά συνδεδεμένο με τον ακροδέκτη A4, αλλά ήταν απαραίτητο να συμμορφωθώ με τη διεπαφή.

Με το σχεδιασμό της βιβλιοθήκης SdFat, η ταχύτητα της θύρας SPI προσαρμόζεται πριν από κάθε συναλλαγή. Εκείνοι. θεωρητικά, μπορείτε να ξεκινήσετε την επικοινωνία με την κάρτα σε χαμηλή ταχύτητα και μετά να την αυξήσετε. Αλλά το εγκατέλειψα και προσάρμοσα την ταχύτητα μία φορά στη μέθοδο start(). Έτσι, οι μέθοδοι ενεργοποίησης/απενεργοποίησης αποδείχθηκαν κενές. Το ίδιο με το setSpiSettings()

Ασήμαντοι χειριστές συναλλαγών

void SdFatSPIDriver::activate() ( // Δεν απαιτείται ειδική ενεργοποίηση ) void SdFatSPIDriver::deactivate() ( // Δεν απαιτείται ειδική απενεργοποίηση ) void SdFatSPIDriver::setSpiSettings(const SPISettings & spiSettings) ( - // Χρησιμοποιούμε τις ρυθμίσεις ίδιες ρυθμίσεις για όλες τις μεταφορές)


Οι μέθοδοι ελέγχου σήματος CS είναι αρκετά ασήμαντες

Έλεγχος σήματος CS

void SdFatSPIDriver::select() ( HAL_GPIO_WritePin(GPIOA, GPIO_PIN_4, GPIO_PIN_RESET); ) void SdFatSPIDriver::unselect() ( HAL_GPIO_WritePin(GPIOA, GPIO_PIN_PIOS);


Ας φτάσουμε στο διασκεδαστικό μέρος - ανάγνωση και γραφή. Η πρώτη πιο δρυς υλοποίηση χωρίς DMA

Μεταφορά δεδομένων χωρίς DMA

uint8_t SdFatSPIDriver::receive() ( uint8_t buf; uint8_t dummy = 0xff; HAL_SPI_TransmitReceive(&spiHandle, &dummy, &buf, 1, 10); return buf; ) uint8_t SdFatuntree* TODO : Λήψη μέσω DMA εδώ memset(buf, 0xff, n); HAL_SPI_Receive(&spiHandle, buf, n, 10); επιστροφή 0; ) void SdFatSPIDriver::send(uint8_t data) ( HAL_SPI_Transmit(&data0,1,); ) void SdFatSPIDriver::send(const uint8_t* buf, size_t n) ( // TODO: Μετάδοση μέσω DMA εδώ HAL_SPI_Transmit(&spiHandle, (uint8_t*)buf, n, 10); )


Στη διεπαφή SPI, η λήψη και η μετάδοση δεδομένων πραγματοποιείται ταυτόχρονα. Για να λάβετε κάτι πρέπει να στείλετε κάτι. Συνήθως το HAL το κάνει αυτό για εμάς - απλά καλούμε τη συνάρτηση HAL_SPI_Receive() και οργανώνει τόσο την αποστολή όσο και τη λήψη. Αλλά στην πραγματικότητα, αυτή η λειτουργία στέλνει σκουπίδια που ήταν στο buffer λήψης.
Για να πουλήσετε κάτι περιττό, πρέπει πρώτα να αγοράσετε κάτι περιττό (C) Prostokvashino

Υπάρχει όμως μια απόχρωση. Οι κάρτες SD είναι πολύ ιδιότροπες. Δεν τους αρέσει να τους δίνουν τίποτα ενώ η κάρτα στέλνει δεδομένα. Επομένως, έπρεπε να χρησιμοποιήσω τη συνάρτηση HAL_SPI_TransmitReceive() και να στείλω αναγκαστικά 0xffs κατά τη λήψη δεδομένων.

Ας κάνουμε μετρήσεις. Αφήστε ένα νήμα να γράψει 1 kb δεδομένων στην κάρτα σε βρόχο.

Κωδικός δοκιμής για την αποστολή ροής δεδομένων σε κάρτα SD

uint8_t sd_buf; uint16_t i=0; uint32_t prev = HAL_GetTick(); while(true) (​bulkFile.write(sd_buf, 512); bulkFile.write(sd_buf, 512); i++; uint32_t cur = HAL_GetTick(); if(cur-prev >= 1000) (prev = cur; usbDebugWrite( "Αποθηκευμένο %d kb\n", i); i = 0; ) )


Με αυτήν την προσέγγιση, μπορούν να καταγραφούν περίπου 15-16 kb ανά δευτερόλεπτο. Οχι πολύ. Αλλά αποδείχθηκε ότι έβαλα το prescaler στο 256. Δηλαδή. Ο χρονισμός SPI έχει ρυθμιστεί σε πολύ μικρότερη από την πιθανή απόδοση. Πειραματικά, ανακάλυψα ότι δεν έχει νόημα να ρυθμίσετε τη συχνότητα υψηλότερη από 9 MHz (το prescaler έχει ρυθμιστεί στα 8) - δεν μπορεί να επιτευχθεί ταχύτητα εγγραφής μεγαλύτερη από 100-110 kb/s (παρεμπιπτόντως, σε άλλη μονάδα flash , για κάποιο λόγο ήταν δυνατή μόνο η εγγραφή 50-60 kb/s, και στον τρίτο είναι γενικά μόνο 40 kb/s). Προφανώς όλα εξαρτώνται από τα χρονικά όρια της ίδιας της μονάδας flash.

Κατ 'αρχήν, αυτό είναι ήδη περισσότερο από αρκετό, αλλά πρόκειται να αντλήσουμε δεδομένα μέσω DMA. Προχωράμε σύμφωνα με το ήδη γνωστό σχήμα. Πρώτα απ 'όλα, αρχικοποίηση. Λαμβάνουμε και εκπέμπουμε μέσω SPI στο δεύτερο και τρίτο κανάλι DMA, αντίστοιχα.

Αρχικοποίηση DMA

// Ενεργοποίηση ρολογιού ελεγκτή DMA __HAL_RCC_DMA1_CLK_ENABLE(); // Κανάλι Rx DMA dmaHandleRx.Instance = DMA1_Channel2; dmaHandleRx.Init.Direction = DMA_PERIPH_TO_MEMORY; dmaHandleRx.Init.PeriphInc = DMA_PINC_DISABLE; dmaHandleRx.Init.MemInc = DMA_MINC_ENABLE; dmaHandleRx.Init.PeriphDataAlignment = DMA_PDATAALIGN_BYTE; dmaHandleRx.Init.MemDataAlignment = DMA_MDATAALIGN_BYTE; dmaHandleRx.Init.Mode = DMA_NORMAL; dmaHandleRx.Init.Priority = DMA_PRIORITY_LOW; HAL_DMA_Init(&dmaHandleRx); __HAL_LINKDMA(&spiHandle, hdmarx, dmaHandleRx); // Tx κανάλι DMA dmaHandleTx.Instance = DMA1_Channel3; dmaHandleTx.Init.Direction = DMA_MEMORY_TO_PERIPH; dmaHandleTx.Init.PeriphInc = DMA_PINC_DISABLE; dmaHandleTx.Init.MemInc = DMA_MINC_ENABLE; dmaHandleTx.Init.PeriphDataAlignment = DMA_PDATAALIGN_BYTE; dmaHandleTx.Init.MemDataAlignment = DMA_MDATAALIGN_BYTE; dmaHandleTx.Init.Mode = DMA_NORMAL; dmaHandleTx.Init.Priority = DMA_PRIORITY_LOW; HAL_DMA_Init(&dmaHandleTx); __HAL_LINKDMA(&spiHandle, hdmatx, dmaHandleTx);


Μην ξεχάσετε να ενεργοποιήσετε τις διακοπές. Για μένα θα πάνε με προτεραιότητα 8 - ελαφρώς χαμηλότερα από το UART και το I2C

Διαμόρφωση διακοπών DMA

// Setup DMA interrupts HAL_NVIC_SetPriority(DMA1_Channel2_IRQn, 8, 0); HAL_NVIC_EnableIRQ(DMA1_Channel2_IRQn); HAL_NVIC_SetPriority(DMA1_Channel3_IRQn, 8, 0); HAL_NVIC_EnableIRQ(DMA1_Channel3_IRQn);


Αποφάσισα ότι η επιβάρυνση της εκτέλεσης του DMA και του συγχρονισμού για σύντομες μεταφορές θα μπορούσε να υπερβεί το όφελος, επομένως για μικρά πακέτα (έως 16 byte) άφησα την παλιά επιλογή. Πακέτα μεγαλύτερα από 16 byte αποστέλλονται μέσω DMA. Η μέθοδος συγχρονισμού είναι ακριβώς η ίδια όπως στην προηγούμενη ενότητα.

Προώθηση δεδομένων μέσω DMA

const size_t DMA_TRESHOLD = 16; uint8_t SdFatSPIDriver::receive(uint8_t* buf, size_t n) ( memset(buf, 0xff, n); // Δεν χρησιμοποιείται DMA για σύντομες μεταφορές if(n<= DMA_TRESHOLD) { return HAL_SPI_TransmitReceive(&spiHandle, buf, buf, n, 10); } // Start data transfer HAL_SPI_TrsnsmitReceive_DMA(&spiHandle, buf, buf, n); // Wait until transfer is completed ulTaskNotifyTake(pdTRUE, 100); return 0; // Ok status } void SdFatSPIDriver::send(const uint8_t* buf, size_t n) { // Not using DMA for short transfers if(n <= DMA_TRESHOLD) { HAL_SPI_Transmit(&spiHandle, buf, n, 10); return; } // Start data transfer HAL_SPI_Transmit_DMA(&spiHandle, (uint8_t*)buf, n); // Wait until transfer is completed ulTaskNotifyTake(pdTRUE, 100); } void SdFatSPIDriver::dmaTransferCompletedCB() { // Resume SD thread vTaskNotifyGiveFromISR(xSDThread, NULL); }


Φυσικά, δεν υπάρχει τρόπος χωρίς διακοπές. Όλα εδώ είναι ίδια όπως στην περίπτωση του I2C

Το DMA διακόπτει

εξωτερικό SdFatSPIDriver spiDriver; extern "C" void DMA1_Channel2_IRQHandler(void) ( HAL_DMA_IRQHandler(spiDriver.getHandle().hdmarx); ) εξωτερικό "C" void DMA1_Channel3_IRQHandler(void) (HAL_DMA_IRQHandler)vod.Handler) SPI_ TxCpltCallback (SPI_HandleTypeDef *hspi) ( spiDriver.dmaTransferCompletedCB(); ) extern "C" void HAL_SPI_RxCpltCallback(SPI_HandleTypeDef *hspi) ( spiDriver.dmaTransfer)( spiDriver.dmaTransfer)


Ας ξεκινήσουμε και ας ελέγξουμε. Για να μην βασανίσω τη μονάδα flash, αποφάσισα να κάνω εντοπισμό σφαλμάτων διαβάζοντας ένα μεγάλο αρχείο και όχι γράφοντας. Εδώ ανακάλυψα ένα πολύ ενδιαφέρον σημείο: η ταχύτητα ανάγνωσης στην έκδοση χωρίς DMA ήταν περίπου 250-260 kb/s, ενώ με DMA ήταν μόνο 5!!! Επιπλέον, η κατανάλωση CPU χωρίς χρήση DMA ήταν 3%, και με DMA - 75-80%!!! Εκείνοι. το αποτέλεσμα είναι ακριβώς το αντίθετο από αυτό που αναμενόταν.

Εκτός θέματος περίπου 3%

Εδώ είχα ένα αστείο πρόβλημα με τη μέτρηση του φορτίου του επεξεργαστή - μερικές φορές η συνάρτηση έλεγε ότι ο επεξεργαστής ήταν φορτωμένος μόνο κατά 3%, αν και το ποσοστό θα έπρεπε να αλωνίζει χωρίς διακοπή. Στην πραγματικότητα, το φορτίο ήταν 100% και η λειτουργία μέτρησής μου δεν κλήθηκε καθόλου - έχει τη χαμηλότερη προτεραιότητα και απλά δεν υπήρχε αρκετός χρόνος για αυτό. Επομένως, έλαβα την τελευταία τιμή που θυμήθηκα πριν ξεκινήσει η εκτέλεση. Υπό κανονικές συνθήκες η λειτουργία λειτουργεί πιο σωστά.


Έχοντας καταγράψει τον κωδικό προγράμματος οδήγησης σχεδόν σε κάθε γραμμή, ανακάλυψα ένα πρόβλημα: χρησιμοποίησα λάθος λειτουργία επανάκλησης. Αρχικά, ο κώδικάς μου χρησιμοποιούσε το HAL_SPI_Receive_DMA() και μαζί με αυτό χρησιμοποιήθηκε η επιστροφή κλήσης HAL_SPI_RxCplt. Αυτό το σχέδιο δεν λειτούργησε λόγω της απόχρωσης με την ταυτόχρονη αποστολή του 0xff. Όταν άλλαξα το HAL_SPI_Receive_DMA() σε HAL_SPI_TransmitReceive_DMA(), έπρεπε επίσης να αλλάξω την επιστροφή κλήσης σε HAL_SPI_TxRxCpltCallback(). Εκείνοι. μάλιστα έγινε η ανάγνωση, αλλά λόγω έλλειψης επανακλήσεων η ταχύτητα ρυθμίστηκε με τάιμ άουτ 100ms.

Αφού διορθώθηκε η επανάκληση, όλα μπήκαν στη θέση τους. Το φορτίο του επεξεργαστή έπεσε στο 2,5% (τώρα ειλικρινές) και η ταχύτητα εκτινάχθηκε ακόμη και στα 500 kb/s. Είναι αλήθεια ότι το prescaler έπρεπε να ρυθμιστεί στο 4 - με το prescaler στο 2, οι ισχυρισμοί ξεχύθηκαν στη βιβλιοθήκη SdFat. Φαίνεται ότι αυτό είναι το όριο ταχύτητας της κάρτας μου.

Δυστυχώς, αυτό δεν έχει να κάνει με την ταχύτητα εγγραφής. Η ταχύτητα εγγραφής εξακολουθούσε να είναι περίπου 50-60 kb/s και το φορτίο του επεξεργαστή κυμαινόταν στο εύρος του 60-70%. Αλλά αφού έσκασα όλο το βράδυ και κάναμε μετρήσεις σε διαφορετικά σημεία, ανακάλυψα ότι η συνάρτηση send() του ίδιου του προγράμματος οδήγησης (που γράφει έναν τομέα 512 byte) διαρκεί μόλις 1-2 ms, συμπεριλαμβανομένης της αναμονής και του συγχρονισμού. Μερικές φορές, ωστόσο, εμφανίζεται κάποιου είδους timeout και η εγγραφή διαρκεί 5-7 ms. Αλλά το πρόβλημα στην πραγματικότητα δεν είναι στο πρόγραμμα οδήγησης, αλλά στη λογική της εργασίας με το σύστημα αρχείων FAT.

Ανεβαίνοντας στο επίπεδο των αρχείων, των κατατμήσεων και των συμπλεγμάτων, το έργο της εγγραφής 512 σε ένα αρχείο δεν είναι τόσο ασήμαντο. Πρέπει να διαβάσετε τον πίνακα FAT, να βρείτε μια θέση σε αυτόν για να γραφτεί ο τομέας, να γράψετε τον ίδιο τον τομέα, να ενημερώσετε τις εγγραφές στον πίνακα FAT, να γράψετε αυτούς τους τομείς στο δίσκο, να ενημερώσετε τις εγγραφές στον πίνακα αρχείων και καταλόγων, και ένα σωρό άλλα πράγματα. Γενικά, μια κλήση στο FatFile::write() θα μπορούσε να διαρκέσει έως και 15-20 ms, και ένα μεγάλο κομμάτι αυτού του χρόνου καταλαμβάνεται από την πραγματική εργασία του επεξεργαστή για την επεξεργασία εγγραφών στο σύστημα αρχείων.

Όπως έχω ήδη σημειώσει, το φορτίο του επεξεργαστή κατά την εγγραφή είναι 60-70%. Αλλά αυτός ο αριθμός εξαρτάται επίσης από τον τύπο του συστήματος αρχείων (Fat16 ή Fat32), το μέγεθος και, κατά συνέπεια, τον αριθμό αυτών των συμπλεγμάτων στο διαμέρισμα, την ταχύτητα της ίδιας της μονάδας flash, πόσο γεμάτο και κατακερματισμένο είναι τα μέσα, τη χρήση με μεγάλα ονόματα αρχείων και πολλά άλλα. Σας ζητώ λοιπόν να αντιμετωπίσετε αυτές τις μετρήσεις ως κάποιου είδους σχετικούς αριθμούς.

Εκεί πάλι: USB με διπλό buffering

Αποδείχθηκε ενδιαφέρον με αυτό το στοιχείο. Η αρχική υλοποίηση του USB Serial από το STM32GENERIC είχε μια σειρά από ελλείψεις και αποφάσισα να το ξαναγράψω για τον εαυτό μου. Αλλά ενώ μελετούσα πώς λειτουργεί το USB CDC, διάβαζα τον πηγαίο κώδικα και μελετούσα την τεκμηρίωση, τα παιδιά από το STM32GENERIC βελτίωσαν σημαντικά την εφαρμογή τους. Πρώτα όμως πρώτα.

Έτσι, η αρχική υλοποίηση δεν μου ταίριαζε για τους εξής λόγους:

  • Τα μηνύματα αποστέλλονται συγχρονισμένα. Εκείνοι. μια απλή μεταφορά δεδομένων byte προς byte από το GPS UART σε USB περιμένει να σταλεί κάθε μεμονωμένο byte. Εξαιτίας αυτού, το φορτίο του επεξεργαστή μπορεί να φτάσει έως και 30-50%, που είναι φυσικά πολύ (η ταχύτητα UART είναι μόνο 9600)
  • Δεν υπάρχει συγχρονισμός. Κατά την εκτύπωση μηνυμάτων από πολλαπλά νήματα, η έξοδος είναι ένα νουντλ από μηνύματα που αντικαθιστούν εν μέρει το ένα το άλλο
  • Υπέρβαση των buffer λήψης και αποστολής. Μερικά buffer δηλώνονται στο USB Middleware, αλλά δεν χρησιμοποιούνται στην πραγματικότητα. Δηλώνονται μερικά ακόμη buffer στην κλάση SerialUSB, αλλά επειδή χρησιμοποιώ μόνο έξοδο, το buffer λήψης απλώς σπαταλά τη μνήμη.
  • Τέλος, απλώς με ενοχλεί η διεπαφή της κλάσης Print. Εάν, για παράδειγμα, θέλω να εμφανίσω τη συμβολοσειρά "τρέχουσα ταχύτητα XXX km/h", τότε πρέπει να πραγματοποιήσω έως και 3 κλήσεις - για το πρώτο μέρος της συμβολοσειράς, για τον αριθμό και για την υπόλοιπη συμβολοσειρά. Προσωπικά, είμαι πιο κοντά στο πνεύμα με το κλασικό printf. Οι ροές Plus είναι επίσης εντάξει, αλλά πρέπει να εξετάσετε τι είδους κώδικας δημιουργείται από τον μεταγλωττιστή.
Προς το παρόν, ας ξεκινήσουμε με κάτι απλό - τη σύγχρονη αποστολή μηνυμάτων, χωρίς συγχρονισμό και μορφοποίηση. Μάλιστα, ειλικρινά αντέγραψα τον κωδικό από το STM32GENERIC.

Εφαρμογή "κατά μέτωπο"

εξωτερικό USBD_HandleTypeDef hUsbDeviceFS; void usbDebugWrite(uint8_t c) ( usbDebugWrite(&c, 1); ) void usbDebugWrite(const char * str) ( usbDebugWrite((const uint8_t *)str, strlen(str)); ) void usbDebugWrite(const char * str) ( usbDebugWrite((const uint8_t *)str, strlen(str)); ) void usbDebugWrite( size) ) ( // Αγνοήστε την αποστολή του μηνύματος εάν το USB δεν είναι συνδεδεμένο εάν(hUsbDeviceFS.dev_state != USBD_STATE_CONFIGURED) επιστρέψει; // Μεταδώστε το μήνυμα αλλά όχι περισσότερο από το timeout uint32_t timeout = HAL_GetTick() + 5; while(HAL_GetTick()< timeout) { if(CDC_Transmit_FS((uint8_t*)buffer, size) == USBD_OK) { return; } } }


Τυπικά, αυτός δεν είναι σύγχρονος κώδικας, γιατί δεν περιμένει να σταλούν δεδομένα. Αλλά αυτή η συνάρτηση περιμένει μέχρι να σταλούν τα προηγούμενα δεδομένα. Εκείνοι. η πρώτη κλήση θα στείλει δεδομένα στη θύρα και θα εξέλθει, αλλά η δεύτερη κλήση θα περιμένει μέχρι να σταλούν πραγματικά τα δεδομένα που αποστέλλονται στην πρώτη κλήση. Σε περίπτωση χρονικού ορίου, τα δεδομένα χάνονται. Επίσης, δεν συμβαίνει τίποτα εάν δεν υπάρχει καθόλου σύνδεση USB.

Φυσικά, πρόκειται μόνο για προετοιμασία, γιατί... αυτή η υλοποίηση δεν επιλύει τα προβλήματα που εντοπίστηκαν. Τι χρειάζεται για να γίνει αυτός ο κώδικας ασύγχρονος και μη αποκλειστικός; Λοιπόν, τουλάχιστον ένα buffer. Αλλά πότε να μεταφέρετε αυτό το buffer;

Νομίζω ότι αξίζει να κάνουμε μια σύντομη εκδρομή στις αρχές της λειτουργίας USB. Το γεγονός είναι ότι μόνο ο κεντρικός υπολογιστής μπορεί να ξεκινήσει τη μεταφορά στο πρωτόκολλο USB. Εάν μια συσκευή χρειάζεται να μεταφέρει δεδομένα στον κεντρικό υπολογιστή, τα δεδομένα προετοιμάζονται σε μια ειδική προσωρινή μνήμη PMA (Περιοχή μνήμης πακέτων) και η συσκευή περιμένει τον κεντρικό υπολογιστή να παραλάβει αυτά τα δεδομένα. Η συνάρτηση CDC_Transmit_FS() προετοιμάζει το buffer PMA. Αυτό το buffer ζει μέσα στο περιφερειακό USB και όχι στον κωδικό χρήστη.

Ήθελα ειλικρινά να σχεδιάσω μια όμορφη εικόνα εδώ, αλλά δεν μπορούσα να καταλάβω πώς να την εμφανίσω καλύτερα.

Αλλά θα ήταν ωραίο να εφαρμόσετε το ακόλουθο σχήμα. Ο κώδικας πελάτη εγγράφει δεδομένα σε μια προσωρινή μνήμη αποθήκευσης (χρήστη) όπως απαιτείται. Από καιρό σε καιρό έρχεται ο οικοδεσπότης και αφαιρεί ό,τι έχει συσσωρευτεί στο buffer εκείνη τη στιγμή. Αυτό μοιάζει πολύ με αυτό που περιέγραψα στην προηγούμενη παράγραφο, αλλά υπάρχει μια βασική προειδοποίηση: τα δεδομένα βρίσκονται στο buffer χρήστη και όχι στο PMA. Εκείνοι. Θα ήθελα να κάνω χωρίς να καλέσω το CDC_Transmit_FS(), το οποίο μεταφέρει δεδομένα από την προσωρινή μνήμη χρήστη στο PMA, και αντ' αυτού να συλλάβω την επιστροφή κλήσης "εδώ έφτασε ο κεντρικός υπολογιστής, ζητώντας δεδομένα".

Δυστυχώς, αυτή η προσέγγιση δεν είναι δυνατή στον τρέχοντα σχεδιασμό του USB CDC Middleware. Πιο συγκεκριμένα, μπορεί να είναι δυνατό, αλλά πρέπει να εμπλακείτε στην υλοποίηση του προγράμματος οδήγησης CDC. Δεν είμαι ακόμη αρκετά έμπειρος στα πρωτόκολλα USB για να το κάνω αυτό. Εξάλλου, δεν είμαι σίγουρος ότι τα χρονικά όρια USB είναι αρκετά για μια τέτοια λειτουργία.

Ευτυχώς, εκείνη τη στιγμή παρατήρησα ότι η STM32GENERIC είχε ήδη κάνει ποδήλατο σε κάτι τέτοιο. Εδώ είναι ο κώδικας που ξαναδούλεψα δημιουργικά από αυτά.

USB Serial Double Buffered

#define USB_SERIAL_BUFFER_SIZE 256 uint8_t usbTxBuffer; πτητικό uint16_t usbTxHead = 0; volatile uint16_t usbTxTail = 0; volatile uint16_t usbΜετάδοση = 0; uint16_t transmitContiguousBuffer() ( uint16_t count = 0; // Μεταδώστε τα συνεχόμενα δεδομένα μέχρι το τέλος της προσωρινής μνήμης εάν (usbTxHead > usbTxTail) ( count = usbTxHead - usbTxTail; ) else ( count =BTxTail; ) else ( count =BXT) FS (&usbTxBuffer, count); πλήθος επιστροφής; ) void usbDebugWriteInternal(const char *buffer, size_t size, bool reverse = false) ( // Αγνοήστε την αποστολή του μηνύματος εάν το USB δεν είναι συνδεδεμένο if(hUsbDeviceFS.dev_state != USBD_STATE_CONFIGURED) return / / Μεταδώστε το μήνυμα αλλά όχι περισσότερο από το χρονικό όριο λήξης uint32_t = HAL_GetTick() + 5; // Προστασία αυτής της συνάρτησης από πολλαπλές εισόδους MutexLocker locker(usbMutex); // Αντιγραφή δεδομένων στην προσωρινή μνήμη για(size_t i=0; i< size; i++) { if(reverse) --buffer; usbTxBuffer = *buffer; usbTxHead = (usbTxHead + 1) % sizeof(usbTxBuffer); if(!reverse) buffer++; // Wait until there is a room in the buffer, or drop on timeout while(usbTxHead == usbTxTail && HAL_GetTick() < timeout); if (usbTxHead == usbTxTail) break; } // If there is no transmittion happening if (usbTransmitting == 0) { usbTransmitting = transmitContiguousBuffer(); } } extern "C" void USBSerialTransferCompletedCB() { usbTxTail = (usbTxTail + usbTransmitting) % sizeof(usbTxBuffer); if (usbTxHead != usbTxTail) { usbTransmitting = transmitContiguousBuffer(); } else { usbTransmitting = 0; } }


Η ιδέα πίσω από αυτόν τον κώδικα είναι η εξής. Αν και δεν ήταν δυνατό να ληφθεί η ειδοποίηση "ο κεντρικός υπολογιστής έφτασε και θέλει δεδομένα", αποδείχθηκε ότι ήταν δυνατή η οργάνωση μιας επιστροφής κλήσης "Έστειλα τα δεδομένα στον κεντρικό υπολογιστή, μπορείτε να ρίξετε το επόμενο". Αποδεικνύεται ότι είναι ένα είδος διπλού buffer - ενώ η συσκευή περιμένει την αποστολή δεδομένων από την εσωτερική προσωρινή μνήμη PMA, ο κωδικός χρήστη μπορεί να προσθέσει byte στην προσωρινή μνήμη αποθήκευσης. Όταν ολοκληρωθεί η αποστολή δεδομένων, το buffer αποθήκευσης μεταφέρεται στο PMA. Το μόνο που μένει είναι να οργανωθεί αυτό ακριβώς το callback. Για να το κάνετε αυτό, πρέπει να τροποποιήσετε ελαφρώς τη συνάρτηση USBD_CDC_DataIn().

Αρχείο USB Middleware

static uint8_t USBD_CDC_DataIn (USBD_HandleTypeDef *pdev, uint8_t epnum) ( USBD_CDC_HandleTypeDef *hcdc = (USBD_CDC_HandleTypeDef*) pdev->pClassData; if(pClassLatate)dcT= 0; USBSerialTransferComp letedCB();επιστροφή USBD_OK ; ) αλλιώς (επιστροφή USBD_FAIL; ) )


Παρεμπιπτόντως, η συνάρτηση usbDebugWrite προστατεύεται από ένα mutex και θα πρέπει να λειτουργεί σωστά από πολλαπλά νήματα. Δεν προστάτεψα τη συνάρτηση USBSerialTransferCompletedCB() - καλείται από διακοπή και λειτουργεί σε πτητικές μεταβλητές. Ειλικρινά μιλώντας, υπάρχει ένα σφάλμα κάπου εδώ, τα σύμβολα καταπίνονται πολύ περιστασιακά. Αλλά για μένα αυτό δεν είναι κρίσιμο για τον εντοπισμό σφαλμάτων. Αυτό δεν θα καλείται στον κωδικό «παραγωγής».

Εκεί πάλι: printf

Μέχρι στιγμής αυτό το πράγμα μπορεί να λειτουργήσει μόνο με σταθερές χορδές. Ήρθε η ώρα να σφίξετε το ανάλογο printf(). Πραγματικός λειτουργία printf() Δεν θέλω να χρησιμοποιήσω - συνεπάγεται 12 kilobyte επιπλέον κώδικα και ένα "σωρό" που δεν έχω. Βρήκα επιτέλους το καταγραφικό εντοπισμού σφαλμάτων, το οποίο έγραψα κάποτε για το AVR. Η εφαρμογή μου μπορεί να εκτυπώσει συμβολοσειρές καθώς και αριθμούς σε δεκαδική και δεκαεξαδική μορφή. Μετά από λίγη ολοκλήρωση και δοκιμή, βγήκε κάπως έτσι:

Απλοποιημένη υλοποίηση printf

// Η υλοποίηση του sprintf διαρκεί περισσότερα από 10 kb και προσθέτει σωρό στο έργο. Νομίζω ότι αυτό είναι // πάρα πολύ για τη λειτουργικότητα που χρειάζομαι // // Παρακάτω είναι μια συνάρτηση ντάμπινγκ τύπου homebrew printf που δέχεται: // - %d για ψηφία // - %x για αριθμούς ως HEX // - %s για συμβολοσειρές // - %% για σύμβολο τοις εκατό // // Η υλοποίηση υποστηρίζει επίσης πλάτος τιμής καθώς και μηδενική συμπλήρωση // Εκτύπωση του αριθμού στην προσωρινή μνήμη (με αντίστροφη σειρά) // Επιστρέφει τον αριθμό των εκτυπωμένων συμβόλων size_t ΕκτύπωσηΑριθμός(μη υπογεγραμμένη τιμή int , uint8_t radix, char * buf, uint8_t πλάτος, char padSymbol) ( //TODO check negative εδώ size_t len ​​· 0; // Εκτυπώστε τον αριθμό do ( char ψηφίο = τιμή % radix; *(buf++) = ψηφίο< 10 ? "0" + digit: "A" - 10 + digit; value /= radix; len++; } while (value >0); //Προσθήκη μηδενικής πλήρωσης ενώ(len< width) { *(buf++) = padSymbol; len++; } return len; } void usbDebugWrite(const char * fmt, ...) { va_list v; va_start(v, fmt); const char * chunkStart = fmt; size_t chunkSize = 0; char ch; do { // Get the next byte ch = *(fmt++); // Just copy the regular characters if(ch != "%") { chunkSize++; continue; } // We hit a special symbol. Dump string that we processed so far if(chunkSize) usbDebugWriteInternal(chunkStart, chunkSize); // Process special symbols // Check if zero padding requested char padSymbol = " "; ch = *(fmt++); if(ch == "0") { padSymbol = "0"; ch = *(fmt++); } // Check if width specified uint8_t width = 0; if(ch >"0" && κεφ<= "9") { width = ch - "0"; ch = *(fmt++); } // check the format switch(ch) { case "d": case "u": { char buf; size_t len = PrintNum(va_arg(v, int), 10, buf, width, padSymbol); usbDebugWriteInternal(buf + len, len, true); break; } case "x": case "X": { char buf; size_t len = PrintNum(va_arg(v, int), 16, buf, width, padSymbol); usbDebugWriteInternal(buf + len, len, true); break; } case "s": { char * str = va_arg(v, char*); usbDebugWriteInternal(str, strlen(str)); break; } case "%": { usbDebugWriteInternal(fmt-1, 1); break; } default: // Otherwise store it like a regular symbol as a part of next chunk fmt--; break; } chunkStart = fmt; chunkSize=0; } while(ch != 0); if(chunkSize) usbDebugWriteInternal(chunkStart, chunkSize - 1); // Not including terminating NULL va_end(v); }


Η εφαρμογή μου είναι πολύ πιο απλή από τη βιβλιοθήκη, αλλά μπορεί να κάνει ό,τι χρειάζομαι - να εκτυπώνει συμβολοσειρές, δεκαδικούς και δεκαεξαδικούς αριθμούς με μορφοποίηση (πλάτος πεδίου, τελειώνοντας τον αριθμό με μηδενικά στα αριστερά). Δεν ξέρει ακόμη πώς να εκτυπώνει αρνητικούς αριθμούς ή αριθμούς κινητής υποδιαστολής, αλλά δεν είναι δύσκολο να προσθέσει. Αργότερα μπορεί να καταστήσω δυνατή την εγγραφή του αποτελέσματος σε buffer συμβολοσειρών (όπως το sprintf) και όχι μόνο σε USB.

Η απόδοση αυτού του κώδικα είναι περίπου 150-200 kb/s, συμπεριλαμβανομένης της μετάδοσης μέσω USB και εξαρτάται από τον αριθμό (μήκος) των μηνυμάτων, την πολυπλοκότητα της συμβολοσειράς μορφής και το μέγεθος του buffer. Αυτή η ταχύτητα είναι αρκετά αρκετή για να στείλει μερικές χιλιάδες μικρά μηνύματα ανά δευτερόλεπτο. Το πιο σημαντικό είναι ότι οι κλήσεις δεν μπλοκάρουν.

Ακόμα χειρότερα: Χαμηλού επιπέδου HAL

Κατ 'αρχήν, θα μπορούσαμε να είχαμε τελειώσει εκεί, αλλά παρατήρησα ότι τα παιδιά από το STM32GENERIC μόλις πρόσφατα πρόσθεσαν ένα νέο HAL. Το ενδιαφέρον με αυτό είναι ότι πολλά αρχεία εμφανίστηκαν με το όνομα stm32f1xx_ll_XXXX.h. Αποκάλυψαν μια εναλλακτική και χαμηλότερου επιπέδου εφαρμογή του HAL. Εκείνοι. ένα κανονικό HAL παρέχει μια διεπαφή αρκετά υψηλού επιπέδου με το στυλ «πάρε αυτόν τον πίνακα και περάστε τον σε εμένα χρησιμοποιώντας αυτήν τη διεπαφή. Αναφορά ολοκλήρωσης με διακοπή." Αντίθετα, τα αρχεία με τα γράμματα LL στο όνομα παρέχουν μια διεπαφή χαμηλότερου επιπέδου όπως "ρυθμίστε αυτές τις σημαίες για αυτόν και τον άλλο καταχωρητή".

Ο μυστικισμός της πόλης μας

Έχοντας δει τα νέα αρχεία στο αποθετήριο STM32GENERIC, ήθελα να κατεβάσω το πλήρες κιτ από τον ιστότοπο του ST. Αλλά το google με οδήγησε μόνο στην έκδοση 1.4 HAL (STM32 Cube F1), η οποία δεν περιέχει αυτά τα νέα αρχεία. Ο διαμορφωτής γραφικών STM32CubeMX προσέφερε επίσης αυτήν την έκδοση. Ρώτησα τους προγραμματιστές του STM32GENERIC από πού πήραν τη νέα έκδοση. Προς έκπληξή μου, έλαβα έναν σύνδεσμο προς την ίδια σελίδα, μόνο που τώρα προσφέρθηκε να κατεβάσω την έκδοση 1.6. Η Google άρχισε επίσης ξαφνικά να «βρίσκει» τη νέα έκδοση, καθώς και το ενημερωμένο CubeMX. Μυστικισμός και τίποτα παραπάνω!


Γιατί είναι απαραίτητο αυτό; Στις περισσότερες περιπτώσεις, μια διεπαφή υψηλού επιπέδου λύνει πραγματικά το πρόβλημα αρκετά καλά. Το HAL (Hardware Abstraction Layer) ανταποκρίνεται πλήρως στο όνομά του - αφαιρεί κώδικα από καταχωρητές επεξεργαστή και υλικού. Αλλά σε ορισμένες περιπτώσεις, το HAL περιορίζει τη φαντασία του προγραμματιστή, ενώ χρησιμοποιώντας αφαιρέσεις χαμηλότερου επιπέδου θα ήταν δυνατό να υλοποιηθεί η εργασία πιο αποτελεσματικά. Στην περίπτωσή μου αυτά είναι το GPIO και το UART.

Ας δοκιμάσουμε τις νέες διεπαφές. Ας ξεκινήσουμε με τους λαμπτήρες. Δυστυχώς, δεν υπάρχουν ακόμη αρκετά παραδείγματα στο Διαδίκτυο. Θα προσπαθήσουμε να κατανοήσουμε τα σχόλια κώδικα στις συναρτήσεις, ευτυχώς όλα είναι εντάξει.

Προφανώς αυτά τα πράγματα χαμηλού επιπέδου μπορούν επίσης να χωριστούν σε 2 μέρη:

  • Συναρτήσεις ελαφρώς υψηλότερου επιπέδου στο στυλ ενός κανονικού HAL - εδώ είναι η δομή αρχικοποίησης, παρακαλώ αρχικοποιήστε την περιφέρεια για μένα.
  • Ελαφρώς χαμηλότερο επίπεδο ρυθμιστές και λήπτες μεμονωμένων σημαιών ή μητρώων. Ως επί το πλείστον, οι λειτουργίες αυτής της ομάδας είναι ενσωματωμένες και μόνο για κεφαλίδες
Από προεπιλογή, τα πρώτα είναι απενεργοποιημένα από το USE_FULL_LL_DRIVER. Λοιπόν, είναι ανάπηροι και στο διάολο. Θα χρησιμοποιήσουμε το δεύτερο. Μετά από λίγο σαμανισμό πήρα αυτό το πρόγραμμα οδήγησης LED

Morgulka στο LL HAL

// Κλάση για ενθυλάκωση εργασίας με ενσωματωμένα LED // // Σημείωση: αυτή η κλάση προετοιμάζει τις αντίστοιχες ακίδες στον κατασκευαστή. // Ενδέχεται να μην λειτουργεί σωστά εάν τα αντικείμενα αυτής της κλάσης δημιουργηθούν ως καθολικές μεταβλητές LEDDriver ( const uint32_t pin = LL_GPIO_PIN_13; public: LEDDriver() ( //ενεργοποίηση ρολογιού στην περιφερειακή GPIOC __HAL_RCC_GPIOC_IS_CLK_ENABLED1 ως έξοδος/έξοδος PC); LL_GPIO_SetPinMode(GPIOC, pin, LL_GPIO_MODE_OUTPUT); LL_GPIO_SetPinOutputType(GPIOC, pin, LL_GPIO_OUTPUT_PUSHPULL); LL_GPIO_SetPinSpeed(GPIOC_GPIO_MODE_OUTPUT)(GPIOC_FPINOutputType) ( LL_GPIO_ResetO utputPin(GPIOC, pin); ) void turnOff() ( LL_GPIO_SetOutputPin(GPIOC , pin); ) void toggle() (LL_GPIO_TogglePin(GPIOC, pin); ) ); void vLEDThread(void *pvParameters) ( LEDDriver LED; // Απλώς αναβοσβήνει μία φορά στα 2 δευτερόλεπτα για (;;) ( vTaskDelay(2000); led.turnOn(); vTaskDelay(100); led.turnOff(); ) )


Όλα είναι πολύ απλά! Το ωραίο είναι ότι εδώ δουλεύεις πραγματικά με μητρώα και σημαίες απευθείας. Δεν υπάρχει επιβάρυνση για τη μονάδα HAL GPIO, η οποία μεταγλωττίζει έως και 450 byte, και τον έλεγχο καρφιτσών από το STM32GENERIC, το οποίο παίρνει άλλα 670 byte. Εδώ, γενικά, ολόκληρη η τάξη με όλες τις κλήσεις είναι ενσωματωμένη στη συνάρτηση vLEDThread, η οποία έχει μέγεθος μόνο 48 byte!

Δεν έχω βελτιώσει τον έλεγχο του ρολογιού μέσω του LL HAL. Αυτό όμως δεν είναι κρίσιμο, γιατί... Η κλήση __HAL_RCC_GPIOC_IS_CLK_ENABLED() από το κανονικό HAL είναι στην πραγματικότητα μια μακροεντολή που απλώς ορίζει μερικές σημαίες σε ορισμένους καταχωρητές.

Είναι εξίσου εύκολο με τα κουμπιά

Κουμπιά μέσω LL HAL

// Εκχώρηση pins const uint32_t SEL_BUTTON_PIN = LL_GPIO_PIN_14; const uint32_t OK_BUTTON_PIN = LL_GPIO_PIN_15; // Αρχικοποίηση κουμπιών που σχετίζονται με void initButtons() ( //ενεργοποίηση ρολογιού στην περιφερειακή GPIOC __HAL_RCC_GPIOC_IS_CLK_ENABLED(); // Ρύθμιση καρφιτσών κουμπιών LL_GPIO_SetPinMode(GPIOC, SEL_BUTTON_PINO_PINO_LLLS); (GPIOC, SEL_BUTTON_PIN, LL_GPIO_PULL_DOWN); LL_GPIO_SetPinMode(GPIOC , OK_BUTTON_PIN, LL_GPIO_MODE_INPUT); LL_GPIO_SetPinPull(GPIOC, OK_BUTTON_PIN, LL_GPIO_PULL_DOWN); ) // Κατάσταση κουμπιού ανάγνωσης (πραγματοποίηση πρώτης εκτόξευσης) inline bool getButint3PinTet(PinttonPinTuGPIN_TW) (GPIOC, pin)) ( // dobouncing vTaskDelay(DEBOUNCE_DURATION ); if(LL_GPIO_IsInputPinSet(GPIOC, pin)) επιστροφή true; ) return false;)


Με το UART όλα θα είναι πιο ενδιαφέροντα. Επιτρέψτε μου να σας υπενθυμίσω το πρόβλημα. Κατά τη χρήση του HAL, η λήψη έπρεπε να "επαναφορτίζεται" μετά από κάθε λαμβανόμενο byte. Η λειτουργία "πάρτε τα πάντα" δεν παρέχεται στο HAL. Και με το LL HAL πρέπει να πετύχουμε.

Η ρύθμιση των καρφιτσών όχι μόνο με έκανε να σκεφτώ δύο φορές, αλλά με έκανε επίσης να κοιτάξω το Εγχειρίδιο Αναφοράς

Ρύθμιση ακίδων UART

// Καρφίτσες έναρξης σε εναλλακτική λειτουργία λειτουργίας LL_GPIO_SetPinMode(GPIOA, LL_GPIO_PIN_9, LL_GPIO_MODE_ALTERNATE); //Pin TX LL_GPIO_SetPinSpeed(GPIOA, LL_GPIO_PIN_9, LL_GPIO_SPEED_FREQ_HIGH); LL_GPIO_SetPinOutputType(GPIOA, LL_GPIO_PIN_9, LL_GPIO_OUTPUT_PUSHPULL); LL_GPIO_SetPinMode(GPIOA, LL_GPIO_PIN_10, LL_GPIO_MODE_INPUT); //RX pin


Επαναεργασία προετοιμασίας UART για νέες διεπαφές

Αρχικοποίηση UART

// Προετοιμασία για προετοιμασία LL_USART_Disable(USART1); // Init LL_USART_SetBaudRate(USART1, HAL_RCC_GetPCLK2Freq(), 9600); LL_USART_SetDataWidth(USART1, LL_USART_DATAWIDTH_8B); LL_USART_SetStopBitsLength(USART1, LL_USART_STOPBITS_1); LL_USART_SetParity(USART1, LL_USART_PARITY_NONE); LL_USART_SetTransferDirection(USART1, LL_USART_DIRECTION_TX_RX); LL_USART_SetHWFlowCtrl(USART1, LL_USART_HWCONTROL_NONE); // Θα χρησιμοποιήσουμε διακοπή UART για τη λήψη δεδομένων HAL_NVIC_SetPriority(USART1_IRQn, 6, 0); HAL_NVIC_EnableIRQ(USART1_IRQn); // Ενεργοποίηση διακοπής UART στη λήψη byte LL_USART_EnableIT_RXNE(USART1); // Τέλος ενεργοποιήστε το περιφερειακό LL_USART_Enable(USART1);


Τώρα διακοπή. Στην προηγούμενη έκδοση, είχαμε έως και 2 λειτουργίες - η μία επεξεργαζόταν τη διακοπή και η δεύτερη ήταν μια επιστροφή κλήσης (από την ίδια διακοπή) σχετικά με το ληφθέν byte. Στη νέα έκδοση, διαμορφώσαμε τη διακοπή ώστε να λαμβάνει μόνο ένα byte, οπότε θα λάβουμε αμέσως το ληφθέν byte.

Διακοπή UART

// Αποθήκευση ληφθέντων byte inline void charReceivedCB(uint8_t c) ( rxBuffer = c; lastReceivedIndex++; // Εάν ληφθεί σύμβολο EOL, ειδοποιήστε το νήμα GPS ότι η γραμμή είναι διαθέσιμη για ανάγνωση if(c == "\n") vTaskNotifyGiveFromISR(xGPSTh NULL); ) extern "C" void USART1_IRQHandler(void) (uint8_t byte = LL_USART_ReceiveData8(USART1); gpsUart.charReceivedCB(byte); )


Το μέγεθος του κωδικού προγράμματος οδήγησης μειώθηκε από 1242 σε 436 byte και η κατανάλωση RAM από 200 σε 136 (εκ των οποίων τα 128 είναι buffer). Καθόλου άσχημα κατά τη γνώμη μου. Το μόνο κρίμα είναι ότι αυτό δεν είναι το πιο λαίμαργο μέρος. Θα ήταν δυνατό να κόψω κάτι άλλο λίγο, αλλά αυτή τη στιγμή δεν κυνηγώ ιδιαίτερα την κατανάλωση πόρων - τους έχω ακόμα. Και η διεπαφή HAL υψηλού επιπέδου λειτουργεί αρκετά καλά στην περίπτωση άλλων περιφερειακών.

Κοιτάζοντας πίσω

Αν και στην αρχή αυτής της φάσης του έργου ήμουν δύσπιστος για το HAL, κατάφερα να ξαναγράψω όλη τη δουλειά με τα περιφερειακά: GPIO, UART, I2C, SPI και USB. Έχω κάνει μεγάλη πρόοδο στην κατανόηση του πώς λειτουργούν αυτές οι ενότητες και προσπάθησα να μεταδώσω τη γνώση σε αυτό το άρθρο. Αλλά αυτό δεν είναι καθόλου μετάφραση του Εγχειριδίου Αναφοράς. Αντίθετα, δούλεψα στο πλαίσιο αυτού του έργου και έδειξα πώς μπορείς να γράψεις προγράμματα οδήγησης περιφερειακών σε καθαρό HAL.

Το άρθρο αποδείχθηκε μια λίγο πολύ γραμμική ιστορία. Αλλά στην πραγματικότητα, είχα πολλά brunch στα οποία πριόνισα ταυτόχρονα σε ακριβώς αντίθετες κατευθύνσεις. Το πρωί θα μπορούσα να αντιμετωπίσω προβλήματα με την απόδοση κάποιας βιβλιοθήκης Arduino και να αποφασίσω να ξαναγράψω τα πάντα στο HAL και το βράδυ θα ανακάλυπτα ότι κάποιος είχε ήδη προσθέσει υποστήριξη DMA στο STM32GENERIC και θα ήθελα να επιστρέψω . Ή, για παράδειγμα, περάσετε μερικές μέρες παλεύοντας με τις διεπαφές Arduino, προσπαθώντας να καταλάβετε πώς είναι πιο βολικό να μεταφέρετε δεδομένα μέσω I2C, ενώ στο HAL αυτό γίνεται σε 2 γραμμές.

Γενικά, πέτυχα αυτό που ήθελα. Η κύρια δουλειά με τα περιφερειακά είναι υπό τον έλεγχό μου και γραμμένη σε HAL. Το Arduino λειτουργεί μόνο ως προσαρμογέας για ορισμένες βιβλιοθήκες. Είναι αλήθεια ότι είχαν μείνει ακόμα μερικές ουρές. Πρέπει ακόμα να συγκεντρώσετε το θάρρος σας και να αφαιρέσετε το STM32GENERIC από το αποθετήριο σας, αφήνοντας μόνο μερικές πραγματικά απαραίτητες τάξεις. Αλλά τέτοιος καθαρισμός δεν θα ισχύει πλέον για αυτό το άρθρο.

Όσο για τον Αρουντίνο και τους κλώνους του. Μου αρέσει ακόμα αυτό το πλαίσιο. Με αυτό, μπορείτε να δημιουργήσετε γρήγορα κάτι πρωτότυπο χωρίς να ενοχλείτε τον εαυτό σας με την ανάγνωση εγχειριδίων και φύλλων δεδομένων. Κατ 'αρχήν, μπορείτε ακόμη και να δημιουργήσετε τελικές συσκευές με το Arduino, εάν δεν υπάρχουν ειδικές απαιτήσεις για ταχύτητα, κατανάλωση ή μνήμη. Στην περίπτωσή μου, αυτές οι παράμετροι είναι αρκετά σημαντικές, οπότε έπρεπε να μετακομίσω στο HAL.

Άρχισα να δουλεύω στο stm32duino. Αυτός ο κλώνος αξίζει πραγματικά προσοχή εάν θέλετε να έχετε ένα Arduino στο STM32 και να τα έχετε όλα καλά. Επιπλέον, παρακολουθούν στενά την κατανάλωση RAM και φλας. Αντίθετα, το ίδιο το STM32GENERIC είναι πιο χοντρό και βασίζεται στο τερατώδες HAL. Αλλά αυτό το πλαίσιο αναπτύσσεται ενεργά και πρόκειται να ολοκληρωθεί. Σε γενικές γραμμές, μπορώ να προτείνω και τα δύο πλαίσια με μια μικρή προτίμηση για το STM32GENERIC επειδή HAL και πιο δυναμική ανάπτυξη αυτή τη στιγμή. Επιπλέον, το Διαδίκτυο είναι γεμάτο από παραδείγματα για το HAL και μπορείτε πάντα να προσαρμόσετε κάτι που ταιριάζει στον εαυτό σας.

Εξακολουθώ να θεωρώ τον εαυτό του HAL με κάποιο βαθμό αηδίας. Η βιβλιοθήκη είναι πολύ ογκώδης και άσχημη. Κάνω περιθώρια για το γεγονός ότι η βιβλιοθήκη βασίζεται στο C, γεγονός που απαιτεί τη χρήση μεγάλων ονομάτων συναρτήσεων και σταθερών. Ωστόσο, αυτή δεν είναι μια βιβλιοθήκη με την οποία είναι διασκεδαστική η εργασία. Μάλλον, είναι ένα απαραίτητο μέτρο.

Εντάξει, η διεπαφή - τα εσωτερικά σε κάνουν επίσης να σκεφτείς. Τεράστιες λειτουργίες με λειτουργικότητα για όλες τις περιπτώσεις συνεπάγονται σπατάλη πόρων. Επιπλέον, εάν μπορείτε να αντιμετωπίσετε τον υπερβολικό κώδικα σε flash χρησιμοποιώντας τη βελτιστοποίηση χρόνου σύνδεσης, τότε η τεράστια κατανάλωση μνήμης RAM μπορεί να θεραπευθεί μόνο με την επανεγγραφή του στο LL HAL.

Αλλά δεν είναι καν αυτό που ενοχλεί, αλλά σε ορισμένα σημεία είναι απλώς η αδιαφορία για τους πόρους. Παρατήρησα λοιπόν την τεράστια υπερβολική χρήση μνήμης στον κώδικα USB Middleware (επισήμως δεν είναι HAL, αλλά παρέχεται ως μέρος του STM32Cube). Οι δομές USB καταλαμβάνουν 2,5 kb μνήμης. Επιπλέον, η δομή USBD_HandleTypeDef (544 byte) επαναλαμβάνει σε μεγάλο βαθμό το PCD_HandleTypeDef από το κατώτερο επίπεδο (1056 bytes) - τα τελικά σημεία ορίζονται επίσης σε αυτήν. Τα buffer πομποδέκτη δηλώνονται επίσης σε τουλάχιστον δύο μέρη - USBD_CDC_HandleTypeDef και UserRxBufferFS/UserTxBufferFS.

Οι περιγραφείς γενικά δηλώνονται στη μνήμη RAM. Για τι? Είναι σταθεροί! Σχεδόν 400 byte στη μνήμη RAM. Ευτυχώς, ορισμένοι από τους περιγραφείς είναι σταθεροί (λίγο λιγότερο από 300 byte). Οι περιγραφείς είναι αμετάβλητες πληροφορίες. Και εδώ υπάρχει ένας ειδικός κωδικός που τα διορθώνει, και, πάλι, με μια σταθερά. Και μάλιστα ένα που περιλαμβάνεται ήδη εκεί. Για κάποιο λόγο, συναρτήσεις όπως το SetBuffer δεν δέχονται μια σταθερή προσωρινή μνήμη, η οποία επίσης αποτρέπει την τοποθέτηση περιγραφέων και κάποιων άλλων πραγμάτων σε flash. Ποιός είναι ο λόγος? Θα διορθωθεί σε 10 λεπτά!!!

Ή, η δομή αρχικοποίησης είναι μέρος της λαβής αντικειμένου (για παράδειγμα i2c). Γιατί να το αποθηκεύσετε μετά την προετοιμασία του περιφερειακού; Γιατί χρειάζομαι δείκτες σε δομές που δεν χρησιμοποιούνται - για παράδειγμα, γιατί χρειάζομαι δεδομένα που σχετίζονται με το DMA εάν δεν το χρησιμοποιώ;

Και επίσης διπλός κώδικας.

περίπτωση USB_DESC_TYPE_CONFIGURATION: if(pdev->dev_speed == USBD_SPEED_HIGH) (pbuf = (uint8_t *)pdev->pClass->GetHSConfigDescriptor(&len); pbuf = USB_DESC_TYPE_CONFIGURATION; *-puint>pbuf> GetFSConfigDescriptor(&len); pbuf = USB_DESC_TYPE_CONFIGURATION; ) διακοπή;


Μια ειδική μετατροπή σε "τύπο Unicode", η οποία θα μπορούσε να γίνει στο χρόνο μεταγλώττισης. Επιπλέον, διατίθεται ειδικό buffer για αυτό

Χλευασμός των στατιστικών στοιχείων

ALIGN_BEGIN uint8_t USBD_StrDesc __ALIGN_END; void USBD_GetString (const char *desc, uint8_t *unicode, uint16_t *len) ( uint8_t idx = 0; if (desc != NULL) ( *len = USBD_GetLen(desc) * 2 + 2; unicode = *len; unicode = USBSTRING_DE ; ενώ (*desc != "\0") ( unicode = *desc++; unicode = 0x00; ) )


Όχι μοιραίο, αλλά σε κάνει να αναρωτιέσαι αν το HAL είναι τόσο καλό όσο γράφουν οι απολογητές γι' αυτό; Λοιπόν, αυτό δεν είναι αυτό που περιμένετε από μια βιβλιοθήκη του κατασκευαστή και σχεδιασμένη για επαγγελματίες. Αυτοί είναι μικροελεγκτές! Εδώ οι άνθρωποι εξοικονομούν κάθε byte και κάθε μικροδευτερόλεπτο είναι πολύτιμο. Και εδώ, ξέρετε, υπάρχει ένα buffer μισού κιλού και μετατροπή σταθερών χορδών κατά τη διάρκεια της πτήσης. Αξίζει να σημειωθεί ότι τα περισσότερα από τα σχόλια ισχύουν για το USB Middleware.

UPD: στο HAL 1.6 η επανάκληση ολοκληρώθηκε με μεταφορά I2C DMA επίσης έσπασε. Εκείνοι. Εκεί, ο κωδικός που δημιουργεί μια επιβεβαίωση κατά την αποστολή δεδομένων μέσω DMA έχει εξαφανιστεί εντελώς, αν και περιγράφεται στην τεκμηρίωση. Υπάρχει ένα για λήψη, αλλά όχι για μετάδοση. Έπρεπε να επιστρέψω στο HAL 1.4 για τη μονάδα I2C, ευτυχώς υπάρχει μια ενότητα - ένα αρχείο.

Τέλος, θα δώσω την κατανάλωση φλας και RAM διαφόρων εξαρτημάτων. Στην ενότητα Προγράμματα οδήγησης, έχω δώσει τιμές τόσο για προγράμματα οδήγησης που βασίζονται σε HAL όσο και για προγράμματα οδήγησης που βασίζονται σε LL HAL. Στη δεύτερη περίπτωση δεν χρησιμοποιούνται τα αντίστοιχα τμήματα από το τμήμα HAL.

Κατανάλωση μνήμης

Κατηγορία Υποκατηγορία .κείμενο .rodata .δεδομένα .bss
Σύστημα διάνυσμα διακοπής 272
εικονικοί χειριστές ISR 178
libc 760
float math 4872
αμαρτία/συν 6672 536
κύρια & κτλ 86
Ο Κωδικός μου Ο Κωδικός μου 7404 833 4 578
printf 442
Γραμματοσειρές 3317
NeoGPS 4376 93 300
FreeRTOS 4670 4 209
Adafruit GFX 1768
Adafruit SSD1306 1722 1024
SdFat 5386 1144
USB Middleware Πυρήνας 1740 333 2179
CDC 772
Οδηγοί UART 268 200
USB 264 846
I2C 316 164
SPI 760 208
Κουμπιά LL 208
LED LL 48
UART LL 436 136
Arduino gpio 370 296 16
διάφορα 28 24
Τυπώνω 822
HAL USB LL 4650
SysTick 180
NVIC 200
DMA 666
GPIO 452
I2C 1560
SPI 2318
RCC 1564 4
UART 974
σωρό (δεν χρησιμοποιείται πραγματικά) 1068
FreeRTOS Heap 10240

Αυτό είναι όλο. Θα χαρώ να λάβω εποικοδομητικά σχόλια, καθώς και συστάσεις εάν κάτι εδώ μπορεί να βελτιωθεί.

Ετικέτες:

  • HAL
  • STM32
  • STM32cube
  • arduino
Προσθέστε ετικέτες

Όταν μόλις ξεκινάτε να προγραμματίζετε μικροελεγκτές ή δεν έχετε κάνει προγραμματισμό για μεγάλο χρονικό διάστημα, δεν είναι εύκολο να κατανοήσετε τον κώδικα κάποιου άλλου. Ερωτήσεις "Τι είναι αυτό;" και "Από πού προέκυψε αυτό;" εμφανίζονται σχεδόν σε κάθε συνδυασμό γραμμάτων και αριθμών. Και όσο πιο γρήγορα γίνεται η κατανόηση της λογικής «τι; γιατί; και πού;», τόσο πιο εύκολο είναι να μελετήσετε τον κώδικα άλλων ανθρώπων, συμπεριλαμβανομένων παραδειγμάτων. Είναι αλήθεια ότι μερικές φορές για αυτό πρέπει να "περάσετε τον κώδικα" και να "ψάξετε μέσα από τα εγχειρίδια" για περισσότερες από μία ημέρες.

Όλοι οι μικροελεγκτές STM32F4xx έχουν αρκετά περιφερειακά. Σε κάθε περιφερειακή συσκευή μικροελεγκτή εκχωρείται μια συγκεκριμένη, συγκεκριμένη και μη μετατοπίσιμη περιοχή μνήμης. Κάθε περιοχή μνήμης αποτελείται από καταχωρητές μνήμης και αυτοί οι καταχωρητές μπορεί να είναι 8-bit, 16-bit, 32-bit ή κάτι άλλο, ανάλογα με τον μικροελεγκτή. Στον μικροελεγκτή STM32F4, αυτοί οι καταχωρητές είναι 32-bit και κάθε καταχωρητής έχει το δικό του σκοπό και τη δική του συγκεκριμένη διεύθυνση. Τίποτα δεν σας εμποδίζει να αποκτήσετε απευθείας πρόσβαση στα προγράμματά σας υποδεικνύοντας τη διεύθυνση. Σε ποια διεύθυνση βρίσκεται αυτός ή ο άλλος καταχωρητής και σε ποια περιφερειακή συσκευή ανήκει, υποδεικνύεται στην κάρτα μνήμης. Για STM32F4 μια τέτοια κάρτα μνήμης υπάρχει στο έγγραφο DM00031020.pdf, το οποίο μπορείτε να βρείτε στο st.com. Το έγγραφο καλείται

RM0090
Εγχειρίδιο αναφοράς
STM32F405xx/07xx, STM32F415xx/17xx, STM32F42xxx και STM32F43xxx προηγμένες MCU 32-bit βασισμένες σε ARM

Στο κεφάλαιο 2.3 Χάρτης μνήμηςστη σελίδα 64 ένας πίνακας ξεκινά με τις διευθύνσεις των περιοχών καταχώρισης και τη σχέση τους με την περιφερειακή συσκευή. Στον ίδιο πίνακα υπάρχει σύνδεσμος προς μια ενότητα με πιο λεπτομερή κατανομή μνήμης για κάθε περιφερειακό.

Ο πίνακας στα αριστερά δείχνει το εύρος των διευθύνσεων, στη μέση είναι το όνομα της περιφερειακής συσκευής και στην τελευταία στήλη βρίσκεται η πιο λεπτομερής περιγραφή της εκχώρησης μνήμης.

Έτσι, για τις θύρες εισόδου/εξόδου GPIO γενικού σκοπού στον πίνακα εκχώρησης μνήμης μπορείτε να διαπιστώσετε ότι οι διευθύνσεις εκχωρούνται για αυτές ξεκινώντας από 0x4002 0000. Η θύρα I/O γενικού σκοπού GPIOA καταλαμβάνει το εύρος διευθύνσεων από 0x4002 000 έως 0x4002 03FF. Η θύρα GPIOB καταλαμβάνει το εύρος διευθύνσεων 0x4002 400 - 0x4002 07FF. Και ούτω καθεξής.

Για να δείτε μια πιο λεπτομερή διανομή στην ίδια τη σειρά, πρέπει απλώς να ακολουθήσετε τον σύνδεσμο.

Υπάρχει επίσης ένας πίνακας εδώ, αλλά με χάρτη μνήμης για το εύρος διευθύνσεων GPIO. Σύμφωνα με αυτόν τον χάρτη μνήμης, τα πρώτα 4 byte ανήκουν στον καταχωρητή MODER, τα επόμενα 4 byte ανήκουν στον καταχωρητή OTYPER και ούτω καθεξής. Οι διευθύνσεις μητρώου μετρώνται από την αρχή του εύρους που ανήκει σε μια συγκεκριμένη θύρα GPIO. Δηλαδή, κάθε καταχωρητής GPIO έχει μια συγκεκριμένη διεύθυνση που μπορεί να χρησιμοποιηθεί κατά την ανάπτυξη προγραμμάτων για τον μικροελεγκτή.

Αλλά η χρήση διευθύνσεων μητρώου είναι άβολη για τους ανθρώπους και είναι γεμάτη με μεγάλο αριθμό σφαλμάτων. Επομένως, οι κατασκευαστές μικροελεγκτών δημιουργούν τυπικές βιβλιοθήκες που διευκολύνουν την εργασία με μικροελεγκτές. Σε αυτές τις βιβλιοθήκες, οι φυσικές διευθύνσεις συσχετίζονται με τον χαρακτηρισμό τους. Για STM32F4xx αυτές οι αντιστοιχίες καθορίζονται στο αρχείο stm32f4xx.h. Αρχείο stm32f4xx.hανήκει στη βιβλιοθήκη CMSISκαι βρίσκεται στο φάκελο Libraries\CMSIS\ST\STM32F4xx\Include\.

Ας δούμε πώς ορίζεται η θύρα GPIOA στις βιβλιοθήκες. Όλα τα άλλα καθορίζονται παρόμοια. Αρκεί να κατανοήσουμε την αρχή. Αρχείο stm32f4xx.hαρκετά μεγάλο και επομένως είναι καλύτερο να χρησιμοποιήσετε την αναζήτηση ή τις δυνατότητες που παρέχει η αλυσίδα εργαλείων σας.

Για τη θύρα GPIOA, βρίσκουμε τη γραμμή που αναφέρει το GPIOA_BASE

Το GPIOA_BASE ορίζεται μέσω του AHB1PERIPH_BASE

Το AHB1PERIPH_BASE ορίζεται με τη σειρά του μέσω του PERIPH_BASE

Και με τη σειρά του, το PERIPH_BASE ορίζεται ως 0x4000 0000. Αν κοιτάξετε τον χάρτη κατανομής μνήμης των περιφερειακών συσκευών (στην ενότητα 2.3 Χάρτης μνήμηςστη σελίδα 64), θα δούμε αυτή τη διεύθυνση στο κάτω μέρος του πίνακα. Οι καταχωρητές όλων των περιφερειακών του μικροελεγκτή STM32F4 ξεκινούν από αυτή τη διεύθυνση. Δηλαδή, το PERIPH_BASE είναι η αρχική διεύθυνση ολόκληρης της περιφέρειας των μικροελεγκτών STM32F4xx γενικά και του μικροελεγκτή STM32F407VG ειδικότερα.

Το AHB1PERIPH_BASE ορίζεται ως το άθροισμα των (PERIPH_BASE + 0x00020000). (δείτε εικόνες πίσω). Αυτή θα είναι η διεύθυνση 0x4002 0000. Στην κάρτα μνήμης, οι θύρες εισόδου/εξόδου GPIO γενικής χρήσης ξεκινούν από αυτήν τη διεύθυνση.

Το GPIOA_BASE ορίζεται ως (AHB1PERIPH_BASE + 0x0000), δηλαδή είναι η αρχική διεύθυνση της ομάδας καταχωρητών θυρών GPIOA.

Λοιπόν, η ίδια η θύρα GPIOA ορίζεται ως μια δομή καταχωρητών, η τοποθέτηση των οποίων στη μνήμη ξεκινά με τη διεύθυνση GPIOA_BASE (βλ. γραμμή #define GPIOA ((GPIO_TypeDef *) GPIOA_BASE).

Η δομή κάθε θύρας GPIO ορίζεται ως GPIO_TypeDef.

Έτσι, οι τυπικές βιβλιοθήκες, σε αυτήν την περίπτωση το αρχείο stm32f4xx.h, απλώς εξανθρωπίστε τη διευθυνσιοδότηση μηχανών. Εάν δείτε την καταχώρηση GPIOA->ODR = 1234, τότε αυτό σημαίνει ότι ο αριθμός 1234 θα γραφτεί στη διεύθυνση 0x40020014. Το GPIOA έχει αρχική διεύθυνση 0x40020000 και ο καταχωρητής ODR έχει διεύθυνση 0x14 από την αρχή του εύρους. οπότε το GPIOA->ODR έχει διεύθυνση 0x40020014.

Ή, για παράδειγμα, δεν σας αρέσει η καταχώρηση GPIOA->ODR, τότε μπορείτε να ορίσετε #define GPIOA_ODR ((uint32_t *) 0x40020014)και λάβετε το ίδιο αποτέλεσμα γράφοντας GPIOA_ODR = 1234;. Πόσο σκόπιμο είναι όμως αυτό; Εάν θέλετε πραγματικά να εισαγάγετε τις δικές σας ονομασίες, τότε είναι προτιμότερο απλώς να επαναπροσδιορίσετε τις τυπικές. Μπορείτε να δείτε πώς γίνεται αυτό στο αρχείο stm32f4_discovery.hΓια παράδειγμα, εδώ ορίζεται ένα από τα LED:

#define LED4_PIN GPIO_Pin_12
#define LED4_GPIO_PORT GPIOD
#define LED4_GPIO_CLK RCC_AHB1Periph_GPIOD

Μια πιο λεπτομερής περιγραφή της περιφέρειας του λιμένα μπορείτε να βρείτε στο stm32f4xx_gpio.h