همانطور که اطلاع دارید قیمت میکروکنترلرهایی مثل stm32 که توانایی اجرای یک گرافیک مناسب رودارند مدت نسبتاً زیادی هست که افزایش پیداکرده و بعضاً نایاب شده، یک روش دیگر هم برای اجرای گرافیک استفاده از تراشههایی مثل ft800 بوده که آن ها هم متاسفانه با مشکل افزایش قیمت و نایاب شدن مواجه شده?♂️ اما توی این آموزش میخواهیم در مورد یک تراشه باقیمت خیلی مناسب و امکانات کافی برای جایگزینی صحبت کنیم، البته قبلاً هم در سایت در مورد تراشه f1c100s صحبت کرده بودیم و حتی یک برد اوپن سورس هم برای اون معرفی کرده بودیم اما آموزشهای قبلی بر پایه سیستمعامل لینوکس بود و این بار طی چند قسمت میخواهیم اجرای گرافیک بدون سیستمعامل (baremetal) رو آموزش بدیم، پس با ماهمراه باشید.
معرفی کتابخانه baremetal
اول از همه ما نیاز به یک کتابخانه داریم که بخشهای مختلف چیپ f1c100s رو راهاندازی کرده باشه، این کتابخانه اینجا در گیت هاب در دسترس هست.
کتابخانه شامل این بخشها هست:
- _arm926_: هدر فایلهای لازم برای پردازنده
- _drivers_: درایورهای بخشهای مختلف سختافزار
- _lib_: کتابخانههای لازم
- _tools_: ابزارهای موردنیاز
همچنین شامل چند نمونه کد مختلف برای استفاده از کتابخانه ها هست که لازمه یک توضیح کوتاه در مورد هر کدام بدیم:
- hello_led: سادهترین نمونه کد برای راهاندازی حداقلی (همون blink خودمون)
- lcd_test: رسم متن و شکل روی lcd
- lvgl_demo: راهاندازی کتابخانه گرافیکی lvgl
- tv_out_test: راهاندازی بخش خروجی تصویر چیپ (مثل lcd و خروجی pal)
- tv_in_test: راهاندازی ورودی تصویر pal و نمایش تصویر دریافتی روی lcd
- sd_card_test: خواندن تصویر از روی sd و نمایش روی lcd
- simple_loader: بوت لودر برای اجرای کد از روی spi flash
بررسی کد blink
ما برای اینکه تازهکاریم از hello led شروع میکنیم. در ادامه کد فایل main.c این پروژه رو میتونید بررسی کنید:
#include "main.h" #include <stdio.h> #include <string.h> #include <math.h> #include "system.h" #include "f1c100s_de.h" #include "f1c100s_timer.h" #include "f1c100s_intc.h" #include "f1c100s_gpio.h" #include "f1c100s_pwm.h" #include "arm32.h" /* * Simple demonstaration project. Controls 2 LEDs: * LED1(GPIOA1): Classical, arduino-style LED blinking with delay function. * LED2(GPIOA2): "breathing" effect, created by sinewave-modulated PWM. Uses timer interrupt to update PWM duty * */ void delay(uint32_t t); void timer_init(void); void timer_irq_handler(void); #define LED_FADE_STEPS 1000 #define LED_PWM_PERIOD 200 volatile uint8_t tick_flag = 0; uint16_t sin_values[LED_FADE_STEPS/2]; int main(void) { system_init(); // Initialize clocks, mmu, cache, uart, ... arm32_interrupt_enable(); // Enable interrupts printf("Hello, world!\r\n"); // LED1 GPIO initialization gpio_pin_init(GPIOE, 5, GPIO_MODE_OUTPUT, GPIO_PULL_NONE, GPIO_DRV_3); // LED2 GPIO + PWM initialization gpio_pin_init(GPIOA, 2, GPIO_MODE_AF3, GPIO_PULL_NONE, GPIO_DRV_3); pwm_init(PWM0, PWM_MODE_CONTINUOUS, 1, PWM_PSC_120); // 24M / 120 = 200kHz pwm_set_period(PWM0, LED_PWM_PERIOD); // 200k / 200 = 1kHz pwm_set_pulse_len(PWM0, 0); pwm_enable(PWM0); // Create a table with half-period of sin. Just to make sure math functions are working for (uint16_t i = 0; i < LED_FADE_STEPS/2; i++) { float arg = M_PI/(float)(LED_FADE_STEPS/2)*(float)i - (M_PI/2.f); // -pi/2 .. pi/2 float val = sinf(arg); // -1 .. 1 sin_values[i] = (uint16_t)((val/2.f+0.5f)*(float)LED_PWM_PERIOD); } timer_init(); while (1) { gpio_pin_set(GPIOE, 5); delay(200); gpio_pin_clear(GPIOE, 5); delay(200); } return 0; } void timer_init(void) { // Configure timer to generate update event every 1ms tim_init(TIM0, TIM_MODE_CONT, TIM_SRC_HOSC, TIM_PSC_1); tim_set_period(TIM0, 24000000UL / 1000UL); tim_int_enable(TIM0); // IRQ configuration intc_set_irq_handler(IRQ_TIMER0, timer_irq_handler); intc_enable_irq(IRQ_TIMER0); tim_start(TIM0); } void timer_irq_handler(void) { static uint16_t step = 0; static uint8_t dir = 0; tick_flag = 1; //printf("tim_irq!\r\n"); if (dir == 0) // First half of sinewave { step++; if (step == LED_FADE_STEPS/2-1) dir = 1; } else // Second half of sinewave { step--; if (step == 0) dir = 0; } // Update PWM pulse length pwm_set_pulse_len(PWM0, sin_values[step]); tim_clear_irq(TIM0); } void delay(uint32_t t) { while (1) { if (t == 0) return; if (tick_flag == 1) { tick_flag = 0; t--; } } }
هدر system.h کد system.c را فراخوانی میکند که وظیفه راهاندازی بخشهای اصلی سختافزاری را ازجمله واحد کلاک cpu، mmu، راهاندازی uart، کلاک نرمافزاری، تأخیر و… را بر عهده دارد.
هدرهایی که با f1c100 شروع میشوند همگی شامل راهاندازی توابع سطح پایین (low level) سختافزاری نظیر تایمر، وقفه، gpio، pwm و… هستند.
به دلیل اینکه ما برای تست از برد sinux f1 استفاده میکنیم و این برد یک led روی پین E5 داره، پینهای A1 رو به E5 تغییر دادیم و در ادامه داخل یک حلقه نامتناهی با دستور gpio_pin_set(GPIOE, 5) پایه شماره پنج از پورت f1c100، E رو بهعنوان خروجی و مقدار ولتاژ یک دیجیتال تعریف میکنیم. دستور gpio_pin_clear(GPIOE, 5) همان پایه پورت را بهعنوان خروجی و با مقدار دیجیتال صفر تعریف میکند. که درنهایت ما یک led چشمکزن با تأخیر 200 میلیثانیه داریم.
while (1) { gpio_pin_set(GPIOA, 1); delay(200); gpio_pin_clear(GPIOA, 1); delay(200); }
چون ما خیلی سطح پایین کار میکنیم تابع آمادهای برای تأخیر نداریم و خودمان باید delay رو بسازیم، برای این کار یک تایمر 1 میلیثانیه راهاندازی میشه:
void timer_init(void) { // Configure timer to generate update event every 1ms tim_init(TIM0, TIM_MODE_CONT, TIM_SRC_HOSC, TIM_PSC_1); tim_set_period(TIM0, 24000000UL / 1000UL); tim_int_enable(TIM0); // IRQ configuration intc_set_irq_handler(IRQ_TIMER0, timer_irq_handler); intc_enable_irq(IRQ_TIMER0); tim_start(TIM0); }
و در وقفه تایمر هر بار متغیر tick_flag رو برابر 1 میکنیم:
void timer_irq_handler(void) { static uint16_t step = 0; static uint8_t dir = 0; tick_flag = 1; //printf("tim_irq!\r\n"); if (dir == 0) // First half of sinewave { ...
بعد از اون توی تابع delay در یک حلقه دیگه منتظر ۱ شدن tick_flag میمونیم و بعد از اون بلافاصله صفرش میکنیم و به این صورت یک تابع delay داریم:
void delay(uint32_t t) { while (1) { if (t == 0) return; if (tick_flag == 1) { tick_flag = 0; t--; } } }
کامپایل کد
داخل توضیحات کتابخانه ابزار لازم برای کامپایل کد در سیستمعامل ویندوز و نرمافزار eclipse رو گفته، اما ما کدمون رو توی لینوکس میخواهیم کامپایل کنیم و از ubuntu استفاده میکنیم، بدون ide. ابتدا لازم هست کامپایلر gcc مناسب arm و برنامه make رو نصب کنید:
sudo apt install gcc-arm-none-eabi make
حالا به پوشه پروژه رفته و با دستور make برنامه را کامپایل میکنیم:
با اجرای دستورات بالا به صورت خودکار یک پوشه به نام build درست شده که در آن تمام فایل های لینک شده و فایل نهایی hello_led.bin موجود است.
اجرای کد
ما میتونیم برناممون رو از روی sd card و یا spi flash اجرا کنیم که البته در هر دو حالت نیاز به بوت لودر داریم، هرچند درنهایت بهتره که کدتون از روی spi flash اجرا بشه، اما با استفاده از sd card میتونید چند برنامه داشته باشید که هر کدوم رو خواستید توسط بوت لودر اجرا کنید. در ادامه هر دو روش رو توضیح دادیم.
اجرای کد از روی sd card
برای این روش لازم هست شما یک ایمیج که uboot داره رو روی sdcard داشته باشید، میتونید از این ایمیجی که آماده کردیم استفاده کنید و روی sd رایت کنید، بعد از اون هم فایل خروجی hello_led.bin
رو که در پوشه F1C100s_projects/hello_led/build
هست داخل تنها پارتیشن sd کپی میکنیم.
حالا باید sd رو داخل برد sinux f1 یا لیچی پای نانو قرار بدیم و پینهای سریال رو توسط مبدل سریال به سیستم متصل کنیم. بااتصال تغذیه باید چنین خروجی داشته باشید
U-Boot 2020.07 (Nov 04 2021 - 06:48:19 -0400) Allwinner Technology CPU: Allwinner F Series (SUNIV) Model: Allwinner F1C100s Generic Device DRAM: 64 MiB MMC: mmc@1c0f000: 0, mmc@1c10000: 1 Setting up a 480x272 lcd console (overscan 0x0) In: serial ... ... switch to partitions #0, OK mmc0 is current device Booting from MMC0... Wrong Image Format for bootm command ERROR: can't get kernel image! =>
خب حالا وارد uboot شدیم و با این دستورات میتونید برنامه رو اجرا کنید :
fatload mmc 0:2 80000000 hello_led.bin go 80000000
دستور اول فایلمون رو از پارتیشن دوم کارت حافظه به آدرس 80000000
رم انتقال میده و دستور بعدی باعث میشه تا cpu به این آدرس در رم پرش کنه و از اونجا ادامه بده.
در ادامه میتونید خروجی سریال و وضعیت led رو ببینید:
Booting from MMC0... Wrong Image Format for bootm command ERROR: can't get kernel image! => => => fatload mmc 0:2 80000000 hello_led.bin 26376 bytes read in 7 ms (3.6 MiB/s) => go 80000000 ## Starting application at 0x80000000 ... Hello, world!
اجرای کد از روی spi nor flash
برای این روش نیاز هست که ابتدا یک بوت لودر رو روی حافظه بریزیم و بعد از اون کدمون رو، تا بتونیم اجراش کنیم. برای پروگرام فلش هم میتونیم از طریق درگاه usb خود برد این کار رو انجام بدیم.
انجام این کار در ویندوز قبلاً در سایت توضیح دادهشده، برای استفاده در لینوکس ابتدا باید یکسری پکیج رو نصب کنیم:
sudo apt install pkg-config zlib1g-dev libusb-dev libusb-1.0-0-dev
حالا سورس کد ابزار sunxi-tools رو دانلود، کامپایل و نصب میکنیم:
git clone https://github.com/Icenowy/sunxi-tools.git -b f1c100s-spiflash cd sunxi-tools make sudo make install
برای اینکه در زمان انتقال فایل به برد با مشکل دسترسی مواجه نشیم لازمه که یک فایل با محتویات زیر در این آدرس ایجاد کنیم/etc/udev/rules.d/99-allwinnerdl.rules
ACTION!="add", SUBSYSTEM!="usb_device", GOTO="rules_end" ATTR{idProduct}=="efe8", ATTR{idVendor}=="1f3a", MODE="666" ATTR{idProduct}=="1010", ATTR{idVendor}=="1f3a", MODE="666" LABEL="rules_end"
مرحله بعدی بررسی روال بوت هست، همان طور که در تصویر پایین مشاهده میکنید اول cpu تلاش میکنه تا از روی sd card بوت بشه، اگر نتونست از روی spi flash و اگه نشد در آخر usb boot.
ما برای اینکه به مرحله آخر برسیم، میتونیم sd card رو دربیاریم، اما نیاز هست که spi flash رو هم رد کنیم، برای این کار در برد sinux f1 میتونید دکمه boot رو نگهدارید تا از spi flash لود نشه، اما برای برد licheepi nano باید یکم لحیمکاری کنید روی برد! (اینجا دقیقتر توضیح دادهشده).
حالا باید برنامه simpleloader رو کامپایل کنیم:
cd F1C100s_projects/simple_loader make
قبل از استفاده از simpleloader باید با استفاده از mksunxi هدر eGON.BT0 رو در این فایل اصلاح کنیم:
cd F1C100s_projects/_tools_/mksunxi/ gcc -o mksunxi mksunxi.c ./mksunxi ../../simple_loader/build/simple_loader.bin
درنهایت هم باید با ابزار sunxi-fel که بالاتر نصب کردیم اول لودر و بعدش هم برناممون رو روی spiflash به ترتیب در آدرسهای 0x00000 و 0x10000 بریزیم:
sunxi-fel -p spiflash-write 0x00000 mysimple-loader.bin sunxi-fel -p spiflash-write 0x10000 myproject-baremetal.bin
در قسمت بعد به سراغ اجرای LVGL به کمک این کتابخونه میریم.
لینک های دانلود:
ایمیج دارای uboot برای f1c100s
منبع: سیسوگ