Penggaruk paling umum saat menggunakan printf dalam program untuk mikrokontroler

Dari waktu ke waktu dalam proyek saya, saya harus menggunakan printf dalam hubungannya dengan port serial (UART atau abstraksi melalui USB, mensimulasikan port serial). Dan, seperti biasa, banyak waktu berlalu antara aplikasi dan saya berhasil sepenuhnya melupakan semua nuansa yang perlu diperhitungkan sehingga bekerja normal dalam proyek besar.

Dalam artikel ini, saya telah mengkompilasi nuansa top saya sendiri yang muncul saat menggunakan printf dalam program untuk mikrokontroler, diurutkan berdasarkan bukti dari yang paling jelas hingga sepenuhnya tidak jelas.

Pengantar singkat


Bahkan, untuk menggunakan printf dalam program untuk mikrokontroler, itu sudah cukup:
  • termasuk file header dalam kode proyek;
  • mendefinisikan kembali fungsi sistem _write untuk output ke port serial;
  • Jelaskan bertopik panggilan sistem yang diperlukan oleh penghubung (_fork, _wait, dan lainnya);
  • gunakan panggilan printf dalam proyek.

Padahal, tidak semuanya begitu sederhana.

Jelaskan semua bertopik, bukan hanya yang bekas.


Kehadiran sekelompok tautan yang tidak jelas ketika membangun proyek pada awalnya mengejutkan, tetapi setelah membaca sedikit, menjadi jelas apa dan mengapa. Di semua proyek saya, saya sedang menghubungkan submodule ini. Jadi, dalam proyek utama, saya mendefinisikan kembali hanya metode yang saya butuhkan (hanya _write dalam kasus ini), dan sisanya tetap tidak berubah.

Penting untuk dicatat bahwa semua bertopik harus fungsi C. Bukan C ++ (atau dibungkus dengan extern ā€œCā€). Kalau tidak, tata letak akan gagal (ingat perubahan nama selama perakitan dengan G ++).

Dalam _write datang 1 karakter


Terlepas dari kenyataan bahwa prototipe metode _write memiliki argumen yang melewati panjang pesan yang ditampilkan, ia memiliki nilai 1 (pada kenyataannya, kita sendiri akan membuatnya selalu menjadi 1, tetapi lebih lanjut tentang itu nanti).
int _write (int file, char *data, int len) { ... } 

Di Internet, Anda sering dapat melihat penerapan metode ini:
Implementasi fungsi _write yang sering
 int uart_putc( const char ch) { while (USART_GetFlagStatus(USART2, USART_FLAG_TC) == RESET); {} USART_SendData(USART2, (uint8_t) ch); return 0; } int _write_r (struct _reent *r, int file, char * ptr, int len) { r = r; file = file; ptr = ptr; #if 0 int index; /* For example, output string by UART */ for(index=0; index<len; index++) { if (ptr[index] == '\n') { uart_putc('\r'); } uart_putc(ptr[index]); } #endif return len; } 


Implementasi semacam itu memiliki kelemahan sebagai berikut:
  • produktivitas rendah;
  • ketidakamanan streaming;
  • ketidakmampuan untuk menggunakan port serial untuk tujuan lain;


Performa rendah


Performa lambat disebabkan oleh pengiriman byte menggunakan sumber daya prosesor: Anda harus memantau register status daripada menggunakan DMA yang sama. Untuk mengatasi masalah ini, Anda dapat menyiapkan buffer untuk pengiriman terlebih dahulu, dan saat menerima karakter akhir baris (atau mengisi buffer) kirim. Metode ini membutuhkan memori buffer, tetapi secara signifikan meningkatkan kinerja dengan pengiriman yang sering.
Contoh implementasi _write dengan buffer
 #include "uart.h" #include <errno.h> #include <sys/unistd.h> extern mc::uart uart_1; extern "C" { //      uart. static const uint32_t buf_size = 254; static uint8_t tx_buf[buf_size] = {0}; static uint32_t buf_p = 0; static inline int _add_char (char data) { tx_buf[buf_p++] = data; if (buf_p >= buf_size) { if (uart_1.tx(tx_buf, buf_p, 100) != mc_interfaces::res::ok) { errno = EIO; return -1; } buf_p = 0; } return 0; } // Putty  \r\n    //    . static inline int _add_endl () { if (_add_char('\r') != 0) { return -1; } if (_add_char('\n') != 0) { return -1; } uint32_t len = buf_p; buf_p = 0; if (uart_1.tx(tx_buf, len, 100) != mc_interfaces::res::ok) { errno = EIO; return -1; } return 0; } int _write (int file, char *data, int len) { len = len; //   . if ((file != STDOUT_FILENO) && (file != STDERR_FILENO)) { errno = EBADF; return -1; } //     //   \n. if (*data != '\n') { if (_add_char(*data) != 0) { return -1; } } else { if (_add_endl() != 0) { return -1; } } return 1; } } 

Di sini, objek uart, uart_1, bertanggung jawab untuk mengirim langsung menggunakan dma. Objek menggunakan metode FreeRTOS untuk memblokir akses pihak ketiga ke objek pada saat mengirim data dari buffer (mengambil dan mengembalikan mutex). Jadi, tidak ada yang bisa menggunakan objek uart saat mengirim dari utas lainnya.
Beberapa tautan:
  • _write kode fungsi sebagai bagian dari proyek nyata di sini
  • antarmuka kelas uart ada di sini
  • implementasi antarmuka kelas uart di bawah stm32f4 di sini dan di sini
  • Instansiasi kelas UART sebagai bagian dari proyek di sini


Ketidakamanan streaming


Implementasi ini juga tetap tidak terlindungi, karena tidak ada yang mengganggu aliran FreeRTOS yang berdekatan untuk mulai mengirim saluran lain ke printf dan dengan demikian menggiling buffer yang saat ini sedang dikirim (mutex di dalam uart melindungi objek dari digunakan dalam aliran yang berbeda, tetapi data tidak dikirimkan kepada mereka. ) Jika ada risiko bahwa printf dari thread lain akan dipanggil, maka diperlukan untuk mengimplementasikan objek layer yang akan memblokir akses ke printf sepenuhnya. Dalam kasus khusus saya, hanya satu utas yang berinteraksi dengan printf, jadi komplikasi tambahan hanya akan mengurangi kinerja (penangkapan dan pelepasan mutex di dalam lapisan) secara konstan.

Ketidakmampuan untuk menggunakan port serial untuk tujuan lain


Karena kami mengirim hanya setelah seluruh string diterima (atau buffer penuh), alih-alih objek uart, Anda dapat memanggil metode konverter ke beberapa antarmuka tingkat atas untuk transfer paket berikutnya (misalnya, pengiriman dengan jaminan sesuai dengan protokol transmisi yang mirip dengan paket modbus transaksi). Ini akan memungkinkan Anda untuk menggunakan satu uart baik untuk menampilkan informasi debug, dan, misalnya, untuk interaksi pengguna dengan konsol manajemen (jika tersedia di perangkat). Cukup menulis desompresor di sisi penerima.

Secara default, output float tidak berfungsi


Jika Anda menggunakan newlib-nano, maka secara default printf (dan juga semua turunannya seperti sprintf / snprintf ... dan lainnya) tidak mendukung output dari nilai float. Ini mudah dipecahkan dengan menambahkan bendera tautan berikut ke proyek.
 SET(LD_FLAGS -Wl,-u,vfprintf; -Wl,-u,_printf_float; -Wl,-u,_scanf_float; "_") 

Lihat daftar lengkap bendera di sini .

Program membeku di suatu tempat di perut printf


Ini adalah satu lagi kekurangan di flag-flag linker. Agar firmware dapat dikonfigurasi dengan versi perpustakaan yang diinginkan, Anda harus secara eksplisit menentukan parameter prosesor.
 SET(HARDWARE_FLAGS -mthumb; -mcpu=cortex-m4; -mfloat-abi=hard; -mfpu=fpv4-sp-d16;) SET(LD_FLAGS ${HARDWARE_FLAGS} "_") 

Lihat daftar lengkap bendera di sini .

printf memaksa mikrokontroler untuk masuk ke kesalahan yang sulit


Setidaknya ada dua alasan:
  • masalah tumpukan;
  • masalah dengan _sbrk;

Masalah tumpukan


Masalah ini benar-benar memanifestasikan dirinya ketika menggunakan FreeRTOS atau OS lainnya. Masalahnya adalah menggunakan buffer. Paragraf pertama mengatakan bahwa dalam _write masing-masing 1 byte. Agar ini terjadi, Anda harus melarang penggunaan buffering dalam kode Anda sebelum menggunakan printf untuk pertama kalinya.
 setvbuf(stdin, NULL, _IONBF, 0); setvbuf(stdout, NULL, _IONBF, 0); setvbuf(stderr, NULL, _IONBF, 0); 

Dari deskripsi fungsi, berikut bahwa salah satu dari nilai berikut ini dapat diatur dengan cara yang sama:
 #define _IOFBF 0 /* setvbuf should set fully buffered */ #define _IOLBF 1 /* setvbuf should set line buffered */ #define _IONBF 2 /* setvbuf should set unbuffered */ 

Namun, ini dapat menyebabkan limpahan tumpukan tugas (atau interupsi jika Anda tiba-tiba orang yang sangat buruk yang memanggil printf dari interupsi).

Secara teknis murni, adalah mungkin untuk mengatur tumpukan dengan sangat hati-hati untuk setiap aliran, tetapi pendekatan ini membutuhkan perencanaan yang cermat dan sulit untuk menangkap kesalahan yang dibawanya. Solusi yang jauh lebih sederhana adalah menerima masing-masing satu byte, menyimpannya dalam buffer sendiri, dan kemudian output dalam format yang diperlukan, diuraikan sebelumnya.

Masalah dengan _sbrk


Masalah ini bagi saya pribadi yang paling implisit. Jadi apa yang kita ketahui tentang _sbrk?
  • Rintisan lain yang perlu diimplementasikan untuk mendukung sebagian besar perpustakaan standar;
  • diperlukan untuk mengalokasikan memori pada heap;
  • digunakan oleh semua jenis metode perpustakaan seperti malloc, gratis.

Secara pribadi, dalam proyek-proyek saya di 95% kasus saya menggunakan FreeRTOS dengan metode redefinisi baru / delete / malloc yang menggunakan banyak FreeRTOS. Jadi ketika saya mengalokasikan memori, saya yakin bahwa alokasi tersebut ada pada tumpukan FreeRTOS, yang memakan jumlah memori yang telah ditentukan sebelumnya di area bss. Anda dapat melihat layer di sini . Jadi, murni secara teknis, seharusnya tidak ada masalah. Suatu fungsi seharusnya tidak dipanggil. Tapi mari kita berpikir, jika dia menelepon, lalu di mana dia akan mencoba untuk mendapatkan ingatannya?

Ingat tata letak RAM proyek "klasik" untuk mikrokontroler:
  • . data;
  • .bss;
  • ruang kosong
  • tumpukan awal.

Dalam data, kami memiliki data awal objek global (variabel, struktur, dan bidang proyek global lainnya). Dalam bss, bidang global yang memiliki nilai nol awal dan, dengan hati-hati, sekelompok FreeRTOS. Ini hanya sebuah array dalam memori. dengan mana metode dari file heap_x.c kemudian bekerja. Berikutnya adalah ruang kosong, setelah itu (atau lebih tepatnya dari ujung) adalah tumpukan. Karena FreeRTOS digunakan dalam proyek saya, maka tumpukan ini hanya digunakan sampai penjadwal dimulai. Dan, dengan demikian, penggunaannya, dalam banyak kasus, terbatas pada collobyte (pada kenyataannya, biasanya batas 100 byte).

Tapi di mana, kemudian, mana memori dialokasikan menggunakan _sbrk? Lihatlah variabel mana yang dia gunakan dari skrip tautan.
 void *__attribute__ ((weak)) _sbrk (int incr) { extern char __heap_start; extern char __heap_end; ... 

Sekarang kita menemukannya di skrip linker (skrip saya sedikit berbeda dari yang disediakan oleh st, namun bagian ini hampir sama di sana):
 __stack = ORIGIN(SRAM) + LENGTH(SRAM); __main_stack_size = 1024; __main_stack_limit = __stack - __main_stack_size; ...  flash,    ... .bss (NOLOAD) : ALIGN(4) { ... . = ALIGN(4); __bss_end = .; } >SRAM __heap_start = __bss_end; __heap_end = __main_stack_limit; 

Artinya, ia menggunakan memori antara tumpukan (1 kb dari 0x20020000 turun dengan 128 kb RAM) dan bss.

Dimengerti Tapi dia punya definisi ulang metode malloc, gratis, dan lainnya. Gunakan _sbrk setelah semua itu tidak perlu? Ternyata, suatu keharusan. Selain itu, metode ini tidak menggunakan printf, tetapi metode untuk mengatur mode buffering - setvbuf (atau lebih tepatnya _malloc_r, yang tidak dinyatakan sebagai fungsi yang lemah di perpustakaan. Tidak seperti malloc, yang dapat dengan mudah diganti).

Karena saya yakin bahwa sbrk tidak digunakan, saya meletakkan banyak FreeRTOS (bagian bss) di dekat stack (karena saya tahu pasti bahwa stack digunakan 10 kali lebih sedikit dari yang dibutuhkan).

Solusi untuk Masalah 3:
  • indentasi antara bss dan stack;
  • menimpa _malloc_r sehingga _sbrk tidak dipanggil (pisahkan satu metode dari pustaka);
  • menulis ulang sbrk via malloc dan gratis.

Saya memilih opsi pertama, karena tidak mungkin untuk mengganti dengan aman _malloc_r standar (yang ada di dalam libg_nano.a (lib_a-nano-mallocr.o)) (metode ini tidak dinyatakan sebagai __attribute__ ((lemah)), tetapi untuk mengecualikan hanya fungsi tunggal dari bi-library Saya tidak berhasil menghubungkan). Saya benar-benar tidak ingin menulis ulang sbrk untuk satu panggilan.

Solusi terakhir adalah mengalokasikan partisi terpisah dalam RAM untuk tumpukan awal dan _sbrk. Ini memastikan bahwa bagian-bagian tidak ditumpuk satu sama lain selama fase pengaturan. Di dalam sbrk ada juga cek untuk keluar dari bagian. Saya harus membuat koreksi kecil sehingga ketika mendeteksi transisi di luar negeri, aliran akan menggantung dalam loop sementara (karena penggunaan sbrk hanya terjadi pada tahap awal inisialisasi dan harus diproses pada tahap debugging perangkat).
Mem.ld yang dimodifikasi
 MEMORY { FLASH (RX) : ORIGIN = 0x08000000, LENGTH = 1M CCM_SRAM (RW) : ORIGIN = 0x10000000, LENGTH = 64K SRAM (RW) : ORIGIN = 0x20000000, LENGTH = 126K SBRK_HEAP (RW) : ORIGIN = 0x2001F800, LENGTH = 1K MAIN_STACK (RW) : ORIGIN = 0x2001FC00, LENGTH = 1K } 


Perubahan ke section.ld
 __stack = ORIGIN(MAIN_STACK) + LENGTH(MAIN_STACK); __heap_start = ORIGIN(SBRK_HEAP); __heap_end = ORIGIN(SBRK_HEAP) + LENGTH(SBRK_HEAP); 

Anda dapat melihat mem.ld dan section.ld di proyek kotak pasir saya di komit ini .

UPD 07/12/2019: memperbaiki daftar flag untuk printf yang berfungsi dengan nilai float. Saya mengoreksi tautan ke CMakeLists yang berfungsi dengan kompilasi yang dikoreksi dan tata letak bendera (ada nuansa dengan fakta bahwa bendera harus terdaftar satu per satu dan melalui ";", sementara pada satu baris atau pada baris yang berbeda tidak masalah).

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


All Articles