STM32F4 Papan Debug dalam Faktor Bentuk Raspberry Pi

gambar Selamat siang, Khabrovit sayang! Saya ingin memperkenalkan proyek saya kepada publik - papan debug kecil berdasarkan STM32, tetapi dalam faktor bentuk Raspberry Pi. Ini berbeda dari papan debug lain karena memiliki geometri yang kompatibel dengan kasing Raspberry Pi dan modul ESP8266 sebagai modem nirkabel. Dan juga tambahan bagus dalam bentuk konektor untuk kartu micro-SD dan penguat stereo. Untuk memanfaatkan semua kekayaan ini, saya mengembangkan perpustakaan tingkat tinggi dan program demo (dalam C ++ 11). Dalam artikel ini saya ingin menjelaskan secara rinci bagian perangkat keras dan perangkat lunak dari proyek ini.


Siapa yang bisa mendapat manfaat dari proyek ini? Mungkin, hanya untuk mereka yang ingin menyolder papan ini sendiri, karena saya tidak mempertimbangkan opsi apa pun untuk produksi skala kecil. Ini adalah hobi murni. Menurut pendapat saya, papan mencakup berbagai tugas yang mungkin timbul dalam kerangka kerajinan rumah kecil menggunakan WiFi dan suara.


Untuk mulai dengan, saya akan mencoba menjawab pertanyaan mengapa ini semua. Motivator utama proyek ini adalah sebagai berikut:


  • Pilihan platform STM32 adalah murni karena pertimbangan estetika - Saya suka rasio harga / kinerja, ditambah berbagai periferal, ditambah ekosistem pengembangan yang besar dan nyaman dari produsen pengontrol (sw4stm, cubeMX, perpustakaan HAL).
  • Tentu saja, ada banyak papan debug dari produsen pengontrol itu sendiri (Discovery, Nucleo), serta dari produsen pihak ketiga (misalnya, Olimex). Tetapi untuk mengulangi banyak dari mereka di rumah dalam faktor bentuk mereka adalah masalah bagi saya, setidaknya. Dalam versi saya, kami memiliki topologi dua lapisan sederhana dan komponen yang nyaman untuk penyolderan manual.
  • Untuk perangkat mereka, saya ingin memiliki casing yang layak untuk menutupi kualitas rendah dari elektronik di dalamnya. Setidaknya ada dua platform populer yang memiliki sejumlah besar kasus yang paling beragam: Arduino dan Raspberry Pi. Yang kedua menurut saya lebih nyaman dalam hal lokasi guntingan untuk konektor. Karena itu, sebagai donor untuk geometri dewan, saya memilihnya.
  • Pengontrol yang saya pilih di papan memiliki USB, SDIO, I2S, jaringan. Di sisi lain, antarmuka yang sama ini juga berguna untuk platform hobi rumah. Itulah sebabnya, selain controller dengan harness standar, saya menambahkan konektor USB, kartu SD, jalur audio (digital-to-analog converter dan amplifier), serta modul nirkabel berbasis ESP8266.

Sirkuit dan komponen


Sepertinya saya bahwa papan yang cukup bagus dengan karakteristik dan komponen berikut telah berubah:


  • STM32F405RG controller: ARM 32-bit Cortex-M4 dengan coprocessor matematika, frekuensi hingga 168 MHz, memori 1 Mb flash, RAM 196 Kb.
    Pin pengontrol yang digunakan
    Pengikatan pengontrol
  • Konektor SWD untuk memprogram pengontrol (6 pin).
  • Atur ulang tombol untuk reboot.
  • LED tiga warna. Di satu sisi, tiga pin pengontrol hilang. Di sisi lain, mereka masih akan hilang karena kontak yang terbatas pada konektor GPIO, dan untuk debugging LED seperti itu, masalahnya sangat berguna.
  • HSE frekuensi tinggi (16 MHz untuk clock core) dan LSE frekuensi rendah (32,7680 kHz untuk clock real-time) kuarsa.
  • Pin GPIO dengan pitch 2,54 mm kompatibel dengan papan prototyping.
  • Di tempat jack audio 3,5 mm dari Raspberry Pi, saya menempatkan konektor daya 5 volt. Sekilas, keputusan itu kontroversial. Tapi ada pro. Daya dari konektor USB secara opsional hadir (detail di bawah), tetapi ini adalah opsi yang buruk untuk men-debug sirkuit, karena waktu sebelum membakar port USB komputer dalam kasus ini bisa sangat singkat.

Sirkuit daya


  • Port mini-USB Di satu sisi, itu terhubung melalui chip perlindungan STF203-22.TCT ke port USB-OTG controller. Di sisi lain, pin daya VBUS terhubung ke konektor GPIO. Jika Anda menghubungkannya ke pin + 5V, papan akan diberi daya dari port USB.

Sirkuit USB


  • Konektor untuk kartu memori micro-SD dengan harness: resistor pull-up 47 kΩ, transistor manajemen daya ( P-channel MOSFET BSH205 ) dan LED hijau kecil di saluran listrik.

Garis kartu micro sd


Gerbang transistor terhubung ke pin PA15 pengontrol. Ini adalah kontak sistem dari pengontrol JTDI, yang menarik karena pada posisi awal itu dikonfigurasikan sebagai output dengan tegangan tingkat tinggi (pull-up). Karena SWD digunakan sebagai pengganti JTAG untuk pemrograman, kontak ini tetap bebas dan dapat digunakan untuk tujuan lain, misalnya, mengendalikan transistor. Ini nyaman - ketika daya diterapkan ke papan, kartu memori tidak aktif, untuk menyalakannya, Anda perlu menerapkan tingkat rendah untuk pin PA15.


  • Konverter digital-ke-analog berdasarkan UDA1334 . Chip ini tidak memerlukan sinyal clock eksternal, yang memudahkan penggunaannya. Data ditransmisikan melalui bus I2S. Di sisi lain, Datasheet merekomendasikan penggunaan kapasitor polar sebanyak 5 pada 47 μF. Ukuran penting dalam hal ini. Yang terkecil yang ternyata dibeli adalah tantalum dengan ukuran 1411, yang bahkan tidak murah. Namun, saya akan menulis tentang harga secara lebih rinci di bawah ini. Untuk daya analog, digunakan stabilizer liniernya sendiri, kekuatan bagian digital dihidupkan / dimatikan oleh transistor ganda.

Sirkuit DAC


  • Amplifier dua saluran berdasarkan pada dua chip 31AP2005 . Keuntungan utama mereka adalah sejumlah kecil komponen pengikat (hanya filter daya dan filter input). Output audio - 4 platform dengan pitch 2,54 mm. Bagi saya sendiri, saya belum memutuskan apa yang terbaik - opsi seadanya atau, seperti pada raspberry, colokan 3,5 mm. Sebagai aturan, 3,5 mm dikaitkan dengan headphone, dalam kasus kami, kami berbicara tentang menghubungkan speaker.

Penguat sirkuit


  • Modul terakhir adalah selendang ESP11 dengan pengikat (daya, soket pemrograman) sebagai modem WiFi. Kesimpulan papan UART terhubung ke controller dan secara bersamaan output ke konektor eksternal (untuk bekerja dengan papan langsung dari terminal dan pemrograman). Ada saklar daya (eksternal eksternal atau kontrol dari mikrokontroler). Ada LED tambahan untuk menunjukkan daya dan konektor "FLASH" untuk meletakkan papan dalam mode pemrograman.

Sirkuit ESP


Tentu saja, ESP8266 itu sendiri adalah pengontrol yang baik, tetapi masih kalah dengan STM32F4 baik dalam kinerja maupun periferal. Ya, dan ukuran dengan harga modul ini mengisyaratkan bahwa itu adalah unit modem tumpah untuk kakaknya. Modul ini dikendalikan oleh USRT menggunakan protokol teks AT .


Beberapa foto:


Mempersiapkan Modul ESP11


ESP8266 adalah hal yang terkenal. Saya yakin banyak yang sudah akrab dengannya, jadi panduan terperinci akan berlebihan di sini. Karena fitur skematis menghubungkan modul ESP11 ke papan, saya hanya akan memberikan panduan singkat bagi mereka yang ingin mengubah firmware-nya:


  • Saya akan menggunakan utilitas esptool untuk bekerja dengan ESP. Berbeda dengan utilitas standar dari pabrikan, esptool adalah platform independen.
  • Untuk memulai, nyalakan mode daya eksternal dengan jumper ESP-PWR (kami menutup kontak 1 dan 2), dan sambungkan modul ke komputer melalui adaptor USART-USB. Adaptor terhubung ke pin GRD / RX / TD. Kami menyediakan daya ke papan:
  • Kami memastikan bahwa adaptor dikenali oleh sistem operasi. Dalam contoh saya, saya menggunakan adaptor berbasis FT232, jadi dengan daftar perangkat itu harus terlihat sebagai FT232 Serial (UART) IC:
    > lsusb ... Bus 001 Device 010: ID 0483:3748 STMicroelectronics ST-LINK/V2 Bus 001 Device 009: ID 0403:6001 Future Technology Devices International, Ltd FT232 Serial (UART) IC ... 
  • ESP8266 sendiri berbeda dalam jumlah memori flash. Dalam praktiknya, dalam modul ESP11 yang sama, saya menjumpai 512 KB (4 Mbit) dan 1 MB (8 Mbit). Jadi, hal pertama yang perlu diperiksa adalah berapa banyak memori yang digunakan dalam modul yang digunakan. Matikan daya dari papan, dan letakkan modul ke mode pemrograman, tutup jumper "FLASH":


  • Nyalakan daya, jalankan esptool dengan parameter berikut

 > esptool.py --port /dev/ttyUSB0 flash_id Connecting.... Detecting chip type... ESP8266 Chip is ESP8266EX Uploading stub... Running stub... Stub running... Manufacturer: e0 Device: 4014 Detected flash size: 1MB Hard resetting... 

  • esptool melaporkan bahwa, dalam hal ini, kita berurusan dengan modul dengan memori 1 MB.
  • Untuk versi dengan 1 MB, Anda dapat menggunakan firmware terbaru, misalnya, ESP8266 AT Bin V1.6.1 . Tapi itu tidak cocok untuk versi dengan 4 Mbit, yang Anda perlu menggunakan sesuatu yang lebih tua, misalnya yang ini . Firmware terdiri dari beberapa file, alamat awal setiap file ditunjukkan dalam dokumen resmi ESP8266 AT Instruction Set . Alamat awal ini digunakan sebagai parameter utilitas esptool. Misalnya, untuk modul dengan 1 MB, parameter esptool akan terlihat seperti ini (semua file yang diperlukan harus diekstraksi terlebih dahulu dari arsip firmware dan dikumpulkan di direktori kerja)
     > esptool.py --port /dev/ttyUSB0 write_flash 0x00000 boot.bin 0x01000 user1.1024.new.2.bin 0x7E000 blank.bin 0xFB000 blank.bin 0xFC000 esp_init_data_default.bin 0xFE000 blank.bin 
  • Kami menyuplai daya ke board, menjalankan esptool dengan parameter yang ditentukan.
  • Setelah menyelesaikan skrip, matikan daya dari papan, buka jumper "FLASH", nyalakan kontrol daya dari mikrokontroler. Modul siap bekerja.

Perangkat lunak


Ada program pengujian di github . Dia melakukan yang berikut:


  • menampilkan pengontrol pada frekuensi maksimum (168 MHz)
  • mengaktifkan jam waktu nyata
  • mengaktifkan kartu SD dan membaca konfigurasi jaringan darinya. Pustaka FatFS digunakan untuk bekerja dengan sistem file.
  • membangun koneksi ke WLAN yang ditentukan
  • terhubung ke server NTP yang ditentukan dan meminta waktu saat itu darinya. Memimpin waktu.
  • memonitor status beberapa port yang ditentukan. Jika statusnya telah berubah, kirim pesan teks ke server TCP yang ditentukan.
  • ketika Anda mengklik tombol eksternal, ia membaca file * .wav yang ditentukan dari kartu SD dan memutarnya dalam mode asinkron (I2S menggunakan pengontrol DMA).
  • bekerja dengan ESP11 juga diimplementasikan dalam mode asinkron (sejauh ini tanpa DMA, hanya pada interupsi)
  • masuk melalui USART1 (pin PB6 / PB7)
  • dan, tentu saja, LED berkedip.

Di Habré ada banyak artikel yang ditujukan untuk pemrograman STM32 pada tingkat yang agak rendah (hanya dengan manajemen register atau CMSIS). Misalnya, dari yang relatif terakhir: satu , dua , tiga . Artikel-artikelnya, tentu saja, sangat berkualitas tinggi, tetapi pendapat subjektif saya adalah bahwa untuk pengembangan produk satu kali, pendekatan ini, mungkin, membenarkan dirinya sendiri. Tetapi untuk proyek hobi jangka panjang, ketika Anda ingin semuanya menjadi indah dan dapat dikembangkan, pendekatan ini terlalu rendah. Salah satu alasan popularitas Arduino tepatnya sebagai platform perangkat lunak, menurut pendapat saya, adalah bahwa penulis Arduino telah meninggalkan tingkat yang begitu rendah untuk arsitektur berorientasi objek. Oleh karena itu, saya memutuskan untuk pergi ke arah yang sama dan menambahkan lapisan berorientasi objek tingkat tinggi di perpustakaan HAL.


Dengan demikian, tiga tingkatan program diperoleh:


  • Pabrikan Libraries (HAL, FatFS, di masa depan USB-OTG) membentuk yayasan
  • Pustaka StmPlusPlus saya didasarkan pada yayasan ini. Ini termasuk satu set kelas dasar (seperti System, IOPort, IOPin, Timer, RealTimeClock, Usart, Spi, I2S), satu set kelas driver perangkat eksternal (seperti SdCard, Esp11, DcfReceiver, Dac_MCP49x1, AudioDac_UDA1334 dan sejenisnya), serta kelas layanan seperti pemutar asinkron WAV.
  • Berdasarkan perpustakaan StmPlusPlus, aplikasi itu sendiri sedang dibangun.

Adapun dialek bahasa. Meskipun saya agak kuno, saya tetap di C ++ 11. Standar ini memiliki beberapa fitur yang sangat berguna untuk mengembangkan firmware: kelas enum, memanggil konstruktor dengan kurung kurawal untuk mengontrol jenis parameter yang dilewati, dan wadah statis seperti std :: array. Ngomong-ngomong, di Habré ada artikel bagus tentang hal ini.


Perpustakaan StmPlusPlus


Kode pustaka lengkap dapat dilihat di github . Di sini saya hanya akan memberikan beberapa contoh kecil untuk menunjukkan struktur, ide dan masalah yang dihasilkan oleh ide ini.


Contoh pertama adalah kelas untuk polling secara berkala keadaan pin (misalnya, tombol) dan memanggil pawang ketika keadaan ini berubah:


 class Button : IOPin { public: class EventHandler { public: virtual void onButtonPressed (const Button *, uint32_t numOccured) =0; }; Button (PortName name, uint32_t pin, uint32_t pull, const RealTimeClock & _rtc, duration_ms _pressDelay = 50, duration_ms _pressDuration = 300); inline void setHandler (EventHandler * _handler) { handler = _handler; } void periodic (); private: const RealTimeClock & rtc; duration_ms pressDelay, pressDuration; time_ms pressTime; bool currentState; uint32_t numOccured; EventHandler * handler; }; 

Konstruktor mendefinisikan semua parameter tombol:


 Button::Button (PortName name, uint32_t pin, uint32_t pull, const RealTimeClock & _rtc, duration_ms _pressDelay, duration_ms _pressDuration): IOPin{name, pin, GPIO_MODE_INPUT, pull, GPIO_SPEED_LOW}, rtc{_rtc}, pressDelay{_pressDelay}, pressDuration{_pressDuration}, pressTime{INFINITY_TIME}, currentState{false}, numOccured{0}, handler{NULL} { // empty } 

Jika menangani peristiwa seperti itu bukan prioritas, maka menggunakan interupsi jelas berlebihan. Oleh karena itu, berbagai skenario penekanan (misalnya, satu tekan atau tahan) diimplementasikan dalam prosedur periodik, yang harus secara berkala dipanggil dari kode program utama. secara berkala menganalisis perubahan status dan secara sinkron memanggil pengendali virtual onButtonPressed, yang harus diimplementasikan dalam program utama:


 void Button::periodic () { if (handler == NULL) { return; } bool newState = (gpioParameters.Pull == GPIO_PULLUP)? !getBit() : getBit(); if (currentState == newState) { // state is not changed: check for periodical press event if (currentState && pressTime != INFINITY_TIME) { duration_ms d = rtc.getUpTimeMillisec() - pressTime; if (d >= pressDuration) { handler->onButtonPressed(this, numOccured); pressTime = rtc.getUpTimeMillisec(); ++numOccured; } } } else if (!currentState && newState) { pressTime = rtc.getUpTimeMillisec(); numOccured = 0; } else { duration_ms d = rtc.getUpTimeMillisec() - pressTime; if (d < pressDelay) { // nothing to do } else if (numOccured == 0) { handler->onButtonPressed(this, numOccured); } pressTime = INFINITY_TIME; } currentState = newState; } 

Keuntungan utama dari pendekatan ini adalah keragaman logika dan kode untuk mendeteksi suatu peristiwa dari prosesnya. Ini bukan HAL_GetTick yang digunakan untuk menghitung waktu, yang, karena tipenya (uint32_t), disetel ulang dengan melimpah setiap 2 ^ 32 milidetik (setiap 49 hari). Saya mengimplementasikan kelas saya sendiri RealTimeClock, yang menghitung milidetik sejak awal program, atau menyalakan pengontrol seperti uint64_t, yang menghasilkan sekitar 5 ^ 8 tahun.


Contoh kedua adalah bekerja dengan antarmuka perangkat keras, yang ada beberapa di controller. Misalnya, SPI. Dari sudut pandang program utama, sangat mudah untuk memilih hanya antarmuka yang diinginkan (SPI1 / SPI2 / SPI3), dan semua parameter lain yang bergantung pada antarmuka ini akan dikonfigurasikan oleh konstruktor kelas.


 class Spi { public: const uint32_t TIMEOUT = 5000; enum class DeviceName { SPI_1 = 0, SPI_2 = 1, SPI_3 = 2, }; Spi (DeviceName _device, IOPort::PortName sckPort, uint32_t sckPin, IOPort::PortName misoPort, uint32_t misoPin, IOPort::PortName mosiPort, uint32_t mosiPin, uint32_t pull = GPIO_NOPULL); HAL_StatusTypeDef start (uint32_t direction, uint32_t prescaler, uint32_t dataSize = SPI_DATASIZE_8BIT, uint32_t CLKPhase = SPI_PHASE_1EDGE); HAL_StatusTypeDef stop (); inline HAL_StatusTypeDef writeBuffer (uint8_t *pData, uint16_t pSize) { return HAL_SPI_Transmit(hspi, pData, pSize, TIMEOUT); } private: DeviceName device; IOPin sck, miso, mosi; SPI_HandleTypeDef *hspi; SPI_HandleTypeDef spiParams; void enableClock(); void disableClock(); }; 

Parameter pin dan parameter antarmuka disimpan secara lokal di kelas. Sayangnya, saya memilih opsi implementasi yang tidak sepenuhnya berhasil, ketika pengaturan parameter tergantung pada antarmuka tertentu diimplementasikan secara langsung:


 Spi::Spi (DeviceName _device, IOPort::PortName sckPort, uint32_t sckPin, IOPort::PortName misoPort, uint32_t misoPin, IOPort::PortName mosiPort, uint32_t mosiPin, uint32_t pull): device(_device), sck(sckPort, sckPin, GPIO_MODE_AF_PP, pull, GPIO_SPEED_HIGH, false), miso(misoPort, misoPin, GPIO_MODE_AF_PP, pull, GPIO_SPEED_HIGH, false), mosi(mosiPort, mosiPin, GPIO_MODE_AF_PP, pull, GPIO_SPEED_HIGH, false), hspi(NULL) { switch (device) { case DeviceName::SPI_1: #ifdef SPI1 sck.setAlternate(GPIO_AF5_SPI1); miso.setAlternate(GPIO_AF5_SPI1); mosi.setAlternate(GPIO_AF5_SPI1); spiParams.Instance = SPI1; #endif break; ... case DeviceName::SPI_3: #ifdef SPI3 sck.setAlternate(GPIO_AF6_SPI3); miso.setAlternate(GPIO_AF6_SPI3); mosi.setAlternate(GPIO_AF6_SPI3); spiParams.Instance = SPI3; #endif break; } spiParams.Init.Mode = SPI_MODE_MASTER; spiParams.Init.DataSize = SPI_DATASIZE_8BIT; spiParams.Init.CLKPolarity = SPI_POLARITY_HIGH; spiParams.Init.CLKPhase = SPI_PHASE_1EDGE; spiParams.Init.FirstBit = SPI_FIRSTBIT_MSB; spiParams.Init.TIMode = SPI_TIMODE_DISABLE; spiParams.Init.CRCCalculation = SPI_CRCCALCULATION_DISABLE; spiParams.Init.CRCPolynomial = 7; spiParams.Init.NSS = SPI_NSS_SOFT; } 

Skema yang sama mengimplementasikan prosedur enableClock dan disableClock, yang tidak dapat diperluas dan buruk portabel untuk pengontrol lainnya. Dalam hal ini, lebih baik menggunakan templat di mana parameter templat adalah nama antarmuka HAL (SPI1, SPI2, SPI3), parameter pin (GPIO_AF5_SPI1), dan sesuatu yang mengontrol jam on / off. Ada artikel yang menarik tentang topik ini, meskipun meninjau pengontrol AVR, yang, bagaimanapun, tidak membuat perbedaan mendasar.


Awal dan akhir transfer dikendalikan oleh dua metode start / stop:


 HAL_StatusTypeDef Spi::start (uint32_t direction, uint32_t prescaler, uint32_t dataSize, uint32_t CLKPhase) { hspi = &spiParams; enableClock(); spiParams.Init.Direction = direction; spiParams.Init.BaudRatePrescaler = prescaler; spiParams.Init.DataSize = dataSize; spiParams.Init.CLKPhase = CLKPhase; HAL_StatusTypeDef status = HAL_SPI_Init(hspi); if (status != HAL_OK) { USART_DEBUG("Can not initialize SPI " << (size_t)device << ": " << status); return status; } /* Configure communication direction : 1Line */ if (spiParams.Init.Direction == SPI_DIRECTION_1LINE) { SPI_1LINE_TX(hspi); } /* Check if the SPI is already enabled */ if ((spiParams.Instance->CR1 & SPI_CR1_SPE) != SPI_CR1_SPE) { /* Enable SPI peripheral */ __HAL_SPI_ENABLE(hspi); } USART_DEBUG("Started SPI " << (size_t)device << ": BaudRatePrescaler = " << spiParams.Init.BaudRatePrescaler << ", DataSize = " << spiParams.Init.DataSize << ", CLKPhase = " << spiParams.Init.CLKPhase << ", Status = " << status); return status; } HAL_StatusTypeDef Spi::stop () { USART_DEBUG("Stopping SPI " << (size_t)device); HAL_StatusTypeDef retValue = HAL_SPI_DeInit(&spiParams); disableClock(); hspi = NULL; return retValue; } 

Bekerja dengan antarmuka perangkat keras menggunakan interupsi . Kelas mengimplementasikan antarmuka I2S menggunakan pengontrol DMA. I2S (Inter-IC Sound) adalah perangkat keras dan perangkat lunak tambahan pada SPI, yang dengan sendirinya, misalnya, memilih frekuensi jam dan mengontrol saluran tergantung pada protokol audio dan bit rate.


Dalam hal ini, kelas I2S diwarisi dari kelas "port", yaitu, I2S adalah port dengan properti khusus. Beberapa data disimpan dalam struktur HAL (plus untuk kenyamanan, dikurangi untuk jumlah data). Beberapa data ditransfer dari kode utama melalui tautan (misalnya, struktur irqPrio).


 class I2S : public IOPort { public: const IRQn_Type I2S_IRQ = SPI2_IRQn; const IRQn_Type DMA_TX_IRQ = DMA1_Stream4_IRQn; I2S (PortName name, uint32_t pin, const InterruptPriority & prio); HAL_StatusTypeDef start (uint32_t standard, uint32_t audioFreq, uint32_t dataFormat); void stop (); inline HAL_StatusTypeDef transmit (uint16_t * pData, uint16_t size) { return HAL_I2S_Transmit_DMA(&i2s, pData, size); } inline void processI2SInterrupt () { HAL_I2S_IRQHandler(&i2s); } inline void processDmaTxInterrupt () { HAL_DMA_IRQHandler(&i2sDmaTx); } private: I2S_HandleTypeDef i2s; DMA_HandleTypeDef i2sDmaTx; const InterruptPriority & irqPrio; }; 

Konstruktornya menetapkan semua parameter statis:


 I2S::I2S (PortName name, uint32_t pin, const InterruptPriority & prio): IOPort{name, GPIO_MODE_INPUT, GPIO_NOPULL, GPIO_SPEED_FREQ_LOW, pin, false}, irqPrio{prio} { i2s.Instance = SPI2; i2s.Init.Mode = I2S_MODE_MASTER_TX; i2s.Init.Standard = I2S_STANDARD_PHILIPS; // will be re-defined at communication start i2s.Init.DataFormat = I2S_DATAFORMAT_16B; // will be re-defined at communication start i2s.Init.MCLKOutput = I2S_MCLKOUTPUT_DISABLE; i2s.Init.AudioFreq = I2S_AUDIOFREQ_44K; // will be re-defined at communication start i2s.Init.CPOL = I2S_CPOL_LOW; i2s.Init.ClockSource = I2S_CLOCK_PLL; i2s.Init.FullDuplexMode = I2S_FULLDUPLEXMODE_DISABLE; i2sDmaTx.Instance = DMA1_Stream4; i2sDmaTx.Init.Channel = DMA_CHANNEL_0; i2sDmaTx.Init.Direction = DMA_MEMORY_TO_PERIPH; i2sDmaTx.Init.PeriphInc = DMA_PINC_DISABLE; i2sDmaTx.Init.MemInc = DMA_MINC_ENABLE; i2sDmaTx.Init.PeriphDataAlignment = DMA_PDATAALIGN_HALFWORD; i2sDmaTx.Init.MemDataAlignment = DMA_MDATAALIGN_HALFWORD; i2sDmaTx.Init.Mode = DMA_NORMAL; i2sDmaTx.Init.Priority = DMA_PRIORITY_LOW; i2sDmaTx.Init.FIFOMode = DMA_FIFOMODE_ENABLE; i2sDmaTx.Init.FIFOThreshold = DMA_FIFO_THRESHOLD_FULL; i2sDmaTx.Init.MemBurst = DMA_PBURST_SINGLE; i2sDmaTx.Init.PeriphBurst = DMA_PBURST_SINGLE; } 

Awal transfer data dikendalikan oleh metode start, yang bertanggung jawab untuk mengonfigurasi parameter port, clocking antarmuka, mengkonfigurasi interupsi, memulai DMA, memulai antarmuka itu sendiri dengan parameter transmisi yang ditentukan.


 HAL_StatusTypeDef I2S::start (uint32_t standard, uint32_t audioFreq, uint32_t dataFormat) { i2s.Init.Standard = standard; i2s.Init.AudioFreq = audioFreq; i2s.Init.DataFormat = dataFormat; setMode(GPIO_MODE_AF_PP); setAlternate(GPIO_AF5_SPI2); __HAL_RCC_SPI2_CLK_ENABLE(); HAL_StatusTypeDef status = HAL_I2S_Init(&i2s); if (status != HAL_OK) { USART_DEBUG("Can not start I2S: " << status); return HAL_ERROR; } __HAL_RCC_DMA1_CLK_ENABLE(); __HAL_LINKDMA(&i2s, hdmatx, i2sDmaTx); status = HAL_DMA_Init(&i2sDmaTx); if (status != HAL_OK) { USART_DEBUG("Can not initialize I2S DMA/TX channel: " << status); return HAL_ERROR; } HAL_NVIC_SetPriority(I2S_IRQ, irqPrio.first, irqPrio.second); HAL_NVIC_EnableIRQ(I2S_IRQ); HAL_NVIC_SetPriority(DMA_TX_IRQ, irqPrio.first + 1, irqPrio.second); HAL_NVIC_EnableIRQ(DMA_TX_IRQ); return HAL_OK; } 

Prosedur berhenti melakukan yang sebaliknya:


 void I2S::stop () { HAL_NVIC_DisableIRQ(I2S_IRQ); HAL_NVIC_DisableIRQ(DMA_TX_IRQ); HAL_DMA_DeInit(&i2sDmaTx); __HAL_RCC_DMA1_CLK_DISABLE(); HAL_I2S_DeInit(&i2s); __HAL_RCC_SPI2_CLK_DISABLE(); setMode(GPIO_MODE_INPUT); } 

Ada beberapa fitur menarik di sini:


  • Interupsi yang digunakan dalam hal ini didefinisikan sebagai konstanta statis. Ini minus untuk portabilitas ke pengontrol lain.
  • Pengaturan kode semacam itu memastikan bahwa pin port selalu dalam status GPIO_MODE_INPUT saat tidak ada transmisi. Ini merupakan nilai tambah.
  • Prioritas interupsi ditransfer dari luar, yaitu, ada peluang bagus untuk mengatur peta prioritas interupsi di satu tempat kode utama. Ini juga merupakan nilai tambah.
  • Prosedur berhenti menonaktifkan pencatatan jam kerja DMA1. Dalam hal ini, penyederhanaan ini dapat memiliki konsekuensi yang sangat negatif jika orang lain terus menggunakan DMA1. Masalahnya diselesaikan dengan membuat daftar konsumen yang terpusat dari perangkat tersebut, yang akan bertanggung jawab untuk pengaturan waktu.
  • Penyederhanaan lain - prosedur mulai tidak membawa antarmuka ke keadaan semula jika terjadi kesalahan (ini adalah minus, tetapi mudah diperbaiki). Pada saat yang sama, kesalahan dicatat lebih detail, yang merupakan nilai tambah.
  • Saat menggunakan kelas ini, kode utama harus mencegat SPI2_IRQn dan DMA1_Stream4_IRQn menyela dan memastikan bahwa penangan processI2SInterrupt dan processDmaTxInterrupt yang sesuai dipanggil.

Program utama


Program utama ditulis menggunakan perpustakaan yang dijelaskan di atas dengan cukup sederhana:


 int main (void) { HAL_Init(); IOPort defaultPortA(IOPort::PortName::A, GPIO_MODE_INPUT, GPIO_PULLDOWN); IOPort defaultPortB(IOPort::PortName::B, GPIO_MODE_INPUT, GPIO_PULLDOWN); IOPort defaultPortC(IOPort::PortName::C, GPIO_MODE_INPUT, GPIO_PULLDOWN); // System frequency 168MHz System::ClockDiv clkDiv; clkDiv.PLLM = 16; clkDiv.PLLN = 336; clkDiv.PLLP = 2; clkDiv.PLLQ = 7; clkDiv.AHBCLKDivider = RCC_SYSCLK_DIV1; clkDiv.APB1CLKDivider = RCC_HCLK_DIV8; clkDiv.APB2CLKDivider = RCC_HCLK_DIV8; clkDiv.PLLI2SN = 192; clkDiv.PLLI2SR = 2; do { System::setClock(clkDiv, FLASH_LATENCY_3, System::RtcType::RTC_EXT); } while (System::getMcuFreq() != 168000000L); MyApplication app; appPtr = &app; app.run(); } 

Di sini kita menginisialisasi pustaka HAL, mengkonfigurasi semua pin pengontrol dengan input (GPIO_MODE_INPUT / PULLDOWN) secara default. Kami mengatur frekuensi controller, mulai jam (termasuk jam real-time dari kuarsa eksternal). Setelah itu, sedikit bergaya Java, kita membuat instance dari aplikasi kita dan memanggil metode menjalankannya, yang mengimplementasikan semua logika aplikasi.


Di bagian terpisah, kita harus mendefinisikan semua interupsi yang digunakan. Karena kita menulis dalam C ++, dan interupsi adalah hal-hal dari dunia C, kita perlu menutupi mereka sesuai:


 extern "C" { void SysTick_Handler (void) { HAL_IncTick(); if (appPtr != NULL) { appPtr->getRtc().onMilliSecondInterrupt(); } } void DMA2_Stream3_IRQHandler (void) { Devices::SdCard::getInstance()->processDmaRxInterrupt(); } void DMA2_Stream6_IRQHandler (void) { Devices::SdCard::getInstance()->processDmaTxInterrupt(); } void SDIO_IRQHandler (void) { Devices::SdCard::getInstance()->processSdIOInterrupt(); } void SPI2_IRQHandler(void) { appPtr->getI2S().processI2SInterrupt(); } void DMA1_Stream4_IRQHandler(void) { appPtr->getI2S().processDmaTxInterrupt(); } void HAL_I2S_TxCpltCallback(I2S_HandleTypeDef *channel) { appPtr->processDmaTxCpltCallback(channel); } ... } 

Kelas MyApplication menyatakan semua perangkat yang digunakan, memanggil konstruktor untuk semua perangkat ini, dan juga mengimplementasikan penangan peristiwa yang diperlukan:


 class MyApplication : public RealTimeClock::EventHandler, class MyApplication : public RealTimeClock::EventHandler, WavStreamer::EventHandler, Devices::Button::EventHandler { public: static const size_t INPUT_PINS = 8; // Number of monitored input pins private: UsartLogger log; RealTimeClock rtc; IOPin ledGreen, ledBlue, ledRed; PeriodicalEvent heartbeatEvent; IOPin mco; // Interrupt priorities InterruptPriority irqPrioI2S; InterruptPriority irqPrioEsp; InterruptPriority irqPrioSd; InterruptPriority irqPrioRtc; // SD card IOPin pinSdPower, pinSdDetect; IOPort portSd1, portSd2; SdCard sdCard; bool sdCardInserted; // Configuration Config config; // ESP Esp11 esp; EspSender espSender; // Input pins std::array<IOPin, INPUT_PINS> pins; std::array<bool, INPUT_PINS> pinsState; // I2S2 Audio I2S i2s; AudioDac_UDA1334 audioDac; WavStreamer streamer; Devices::Button playButton; ... 

Faktanya, semua perangkat yang digunakan dinyatakan secara statis, yang berpotensi menyebabkan peningkatan memori yang digunakan, tetapi sangat menyederhanakan akses ke data. Dalam konstruktor kelas MyApplication, perlu memanggil perancang semua perangkat, setelah itu, pada saat prosedur dijalankan dimulai, semua perangkat mikrokontroler yang digunakan akan diinisialisasi:


  MyApplication::MyApplication () : // logging log(Usart::USART_1, IOPort::B, GPIO_PIN_6, GPIO_PIN_7, 115200), // RTC rtc(), ledGreen(IOPort::C, GPIO_PIN_1, GPIO_MODE_OUTPUT_PP), ledBlue(IOPort::C, GPIO_PIN_2, GPIO_MODE_OUTPUT_PP), ledRed(IOPort::C, GPIO_PIN_3, GPIO_MODE_OUTPUT_PP), heartbeatEvent(rtc, 10, 2), mco(IOPort::A, GPIO_PIN_8, GPIO_MODE_AF_PP), // Interrupt priorities irqPrioI2S(6, 0), // I2S DMA interrupt priority: 7 will be also used irqPrioEsp(5, 0), irqPrioSd(3, 0), // SD DMA interrupt priority: 4 will be also used irqPrioRtc(2, 0), // SD card pinSdPower(IOPort::A, GPIO_PIN_15, GPIO_MODE_OUTPUT_PP, GPIO_PULLDOWN, GPIO_SPEED_HIGH, true, false), pinSdDetect(IOPort::B, GPIO_PIN_3, GPIO_MODE_INPUT, GPIO_PULLUP), portSd1(IOPort::C, /* mode = */GPIO_MODE_OUTPUT_PP, /* pull = */GPIO_PULLUP, /* speed = */GPIO_SPEED_FREQ_VERY_HIGH, /* pin = */GPIO_PIN_8 | GPIO_PIN_9 | GPIO_PIN_10 | GPIO_PIN_11 | GPIO_PIN_12, /* callInit = */false), portSd2(IOPort::D, /* mode = */GPIO_MODE_OUTPUT_PP, /* pull = */GPIO_PULLUP, /* speed = */GPIO_SPEED_FREQ_VERY_HIGH, /* pin = */GPIO_PIN_2, /* callInit = */false), sdCard(pinSdDetect, portSd1, portSd2), sdCardInserted(false), // Configuration config(pinSdPower, sdCard, "conf.txt"), //ESP esp(rtc, Usart::USART_2, IOPort::A, GPIO_PIN_2, GPIO_PIN_3, irqPrioEsp, IOPort::A, GPIO_PIN_1), espSender(rtc, esp, ledRed), // Input pins pins { { IOPin(IOPort::A, GPIO_PIN_4, GPIO_MODE_INPUT, GPIO_PULLUP), IOPin(IOPort::A, GPIO_PIN_5, GPIO_MODE_INPUT, GPIO_PULLUP), IOPin(IOPort::A, GPIO_PIN_6, GPIO_MODE_INPUT, GPIO_PULLUP), IOPin(IOPort::A, GPIO_PIN_7, GPIO_MODE_INPUT, GPIO_PULLUP), IOPin(IOPort::C, GPIO_PIN_4, GPIO_MODE_INPUT, GPIO_PULLUP), IOPin(IOPort::C, GPIO_PIN_5, GPIO_MODE_INPUT, GPIO_PULLUP), IOPin(IOPort::B, GPIO_PIN_0, GPIO_MODE_INPUT, GPIO_PULLUP), IOPin(IOPort::B, GPIO_PIN_1, GPIO_MODE_INPUT, GPIO_PULLUP) } }, // I2S2 Audio Configuration // PB10 --> I2S2_CK // PB12 --> I2S2_WS // PB15 --> I2S2_SD i2s(IOPort::B, GPIO_PIN_10 | GPIO_PIN_12 | GPIO_PIN_15, irqPrioI2S), audioDac(i2s, /* power = */ IOPort::B, GPIO_PIN_11, /* mute = */ IOPort::B, GPIO_PIN_13, /* smplFreq = */ IOPort::B, GPIO_PIN_14), streamer(sdCard, audioDac), playButton(IOPort::B, GPIO_PIN_2, GPIO_PULLUP, rtc) { mco.activateClockOutput(RCC_MCO1SOURCE_PLLCLK, RCC_MCODIV_5); } 

Sebagai contoh, pengendali acara untuk mengklik tombol yang memulai / berhenti memainkan file WAV:


  virtual void MyApplication::onButtonPressed (const Devices::Button * b, uint32_t numOccured) { if (b == &playButton) { USART_DEBUG("play button pressed: " << numOccured); if (streamer.isActive()) { USART_DEBUG(" Stopping WAV"); streamer.stop(); } else { USART_DEBUG(" Starting WAV"); streamer.start(AudioDac_UDA1334::SourceType:: STREAM, config.getWavFile()); } } } 

Dan akhirnya, metode jalankan utama menyelesaikan konfigurasi perangkat (misalnya, menetapkan MyApplication sebagai pengendali event), dan memulai loop tanpa akhir, di mana ia secara berkala mengakses perangkat yang memerlukan perhatian berkala:


 void MyApplication::run () { log.initInstance(); USART_DEBUG("Oscillator frequency: " << System::getExternalOscillatorFreq() << ", MCU frequency: " << System::getMcuFreq()); HAL_StatusTypeDef status = HAL_TIMEOUT; do { status = rtc.start(8 * 2047 + 7, RTC_WAKEUPCLOCK_RTCCLK_DIV2, irqPrioRtc, this); USART_DEBUG("RTC start status: " << status); } while (status != HAL_OK); sdCard.setIrqPrio(irqPrioSd); sdCard.initInstance(); if (sdCard.isCardInserted()) { updateSdCardState(); } USART_DEBUG("Input pins: " << pins.size()); pinsState.fill(true); USART_DEBUG("Pin state: " << fillMessage()); esp.assignSendLed(&ledGreen); streamer.stop(); streamer.setHandler(this); streamer.setVolume(1.0); playButton.setHandler(this); bool reportState = false; while (true) { updateSdCardState(); playButton.periodic(); streamer.periodic(); if (isInputPinsChanged()) { USART_DEBUG("Input pins change detected"); ledBlue.putBit(true); reportState = true; } espSender.periodic(); if (espSender.isOutputMessageSent()) { if (reportState) { espSender.sendMessage(config, "TCP", config.getServerIp(), config.getServerPort(), fillMessage()); reportState = false; } if (!reportState) { ledBlue.putBit(false); } } if (heartbeatEvent.isOccured()) { ledGreen.putBit(heartbeatEvent.occurance() == 1); } } } 

Sedikit eksperimen


— . — 168 MHz. , , 172 MHz 180 MHz, , , MCO. , USART I2S, , , HAL.



. github . - , Mouser ( ). 37 . . , STM Olimex, .



. , :


  • ( ). , , . : 4 8 . PLL, .
  • , . 47 μF . , .
  • SWD . - , . .
  • . SMD , . 3 .

Dokumentasi


github GPL v3:



Terima kasih atas perhatian anda!

Source: https://habr.com/ru/post/id413101/


All Articles