Trong nội dung bài viết, chúng ta cùng nhau tìm hiểu về những vấn đề về mặt kỹ thuật thường gặp trong các dự án khi đi làm
1. Platform driver và device driver
Platform driver
Platform driver là những driver thao tác trực tiếp lên thanh ghi của các module trên vi điều khiển.
Người lập trình viên khi muốn sử dụng platform driver phải mất nhiều thời gian đọc hiểu phần cứng, tìm hiểu về mục đích của các thanh ghi cần sử dụng. Ví dụ chỉ với công việc điều khiển đèn LED, chúng ta phải tìm hiểu về các thanh ghi có liên quan trong module GPIO như base_address, set_dataout, set_output,…
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
// Set output cho chân GPIO31 #define GPIO_ADDR_BASE 0x44E07000 #define ADDR_SIZE (0x1000) #define GPIO_SETDATAOUT_OFFSET 0x194 #define GPIO_CLEARDATAOUT_OFFSET 0x190 #define GPIO_OE_OFFSET 0x134 #define LED ~(1 << 31) [...] uint32_t reg_data = 0; // map địa chỉ vật lý vào virtual memory base_addr = ioremap(GPIO_ADDR_BASE, ADDR_SIZE); // set output cho GPIO reg_data = readl_relaxed(base_addr + GPIO_OE_OFFSET); reg_data &= LED; writel_relaxed(reg_data, base_addr + GPIO_OE_OFFSET); |
Hơn thế nữa, soucre code khi này không có tính linh hoạt, chỉ có thể sử dụng trên 1 con chip cụ thể do các giá trị địa chỉ thanh ghi trên các con chip là không giống nhau.
Chính vì vậy, để tiện lợi trong việc lập trình, những platform driver thường sẽ cung cấp API cho người khác sử dụng. Bản chất các API này cũng sẽ tác động tới các thanh ghi trên chip. Để xem được các API này, ta vào trong folder của drivers trong kernel của SoC, rồi tìm các file “omap” có ở từng module – file do nhà sản xuất chip cung cấp và chỉ dùng để điều khiển các module cho SoC (System on Chip)
Device driver
Device driver là những driver sử dụng API được cung cấp bởi platform driver
Ví dụ: set mode output cho chân GPIO31 tương tự với ví dụ trên
1 2 3 4 5 6 7 8 9 10 11 12 |
[...] if (gpio_is_valid(31) == false) { pr_err("GPIO %d is not valid\n", 31); return -1; } if(gpio_request(31,"GPIO_31") < 0) { pr_err("ERROR: GPIO %d request\n", 31); return -1; } gpio_direction_output(31, 0); |
Trong file gpio-omap.c do hãng sản xuất cung cấp, tương ứng với hàm gpio_set_direction_output(), hàm omap_set_gpio_direction() sẽ được gọi để tương tác với thanh ghi
1 2 3 4 5 6 |
static void omap_set_gpio_direction(struct gpio_bank *bank, int gpio, int is_input) { bank->context.oe = omap_gpio_rmw(bank->base + bank->regs->direction, BIT(gpio), is_input); } |
Qua ví dụ trên, ta có thể thấy người lập trình viên không cần phải quan tâm quá sâu đến các thanh ghi của module và không cần phải define địa chỉ của các thanh ghi. Từ đó có thể tiết kiệm rất nhiều thời gian để phát triển sản phẩm.
Ngoài ra, device driver còn có thể di chuyển code sang các con chip khác mà vẫn có thể hoạt động được.
Tóm lại, trong thực tế, đa số khi viết driver cho các ngoại vi như camera, LCD,sensor… viết theo device driver. Khi các thiết bị kết nối với SoC, để sử dụng các module kết nối với SoC, ta sẽ sử dụng các API sẵn có do nhà sản xuất cung cấp.
2. Quy tắc 4 bước trong lập trình driver trên Linux
Thông thường, người lập trình viết driver và người viết ứng dụng trên tầng application là 2 người khác nhau. Chính vì vậy, để driver và application có thể liên kết được với nhau, Kernel cung cấp các chuẩn riêng cho từng thiết bị có thể kết nối vào trong SoC
Để xem chi tiết các tiêu chuẩn cho từng thiết bị, ta có xem folder Documentation trong kernel. Ví dụ với tiêu chuẩn dành cho đèn LED:
![](https://vinalinux.com.vn/wp-content/uploads/2023/11/image-47.png)
Quy tắc 4 bước:
- Nghiên cứu đọc tài liệu phần cứng: đảm bảo chúng ta có thể lập trình driver cho device đó ở dạng thuần vi điều khiển
- Tìm hiểu chuẩn giao tiếp giữa application với driver của loại device đó: Các tầng app giao tiếp với device theo chuẩn nào? Cần tạo ra file nào? File đó nằm ở đâu? Ghi dữ liệu vào file?… (Do tất cả ứng dụng giao tiếp với driver đều thông qua file)
- Tìm template driver tương ứng với device đó
- Lập trình Driver cho device
3. Interrupt Handling
Trong phần này, chúng ta cùng tìm hiểu về cách viết một driver sử dụng ngắt trên GPIO của chip
Trước khi muốn lập trình ngắt cho các chân GPIO của chip, ta cần phải tìm hiểu tài liệu, tra cứu chân ngắt dành cho GPIO đó.
Trong Linux, hàm ngắt có nguyên mẫu hàm để tra về số hiệu IRQ tương ứng:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 |
int gpiod_to_irq(const struct gpio_desc *desc) { struct gpio_chip *gc; int offset; /* * Cannot VALIDATE_DESC() here as gpiod_to_irq() consumer semantics * requires this function to not return zero on an invalid descriptor * but rather a negative error number. */ if (!desc || IS_ERR(desc) || !desc->gdev || !desc->gdev->chip) return -EINVAL; gc = desc->gdev->chip; offset = gpio_chip_hwgpio(desc); if (gc->to_irq) { int retirq = gc->to_irq(gc, offset); /* Zero means NO_IRQ */ if (!retirq) return -ENXIO; return retirq; } #ifdef CONFIG_GPIOLIB_IRQCHIP if (gc->irq.chip) { /* * Avoid race condition with other code, which tries to lookup * an IRQ before the irqchip has been properly registered, * i.e. while gpiochip is still being brought up. */ return -EPROBE_DEFER; } #endif return -ENXIO; } EXPORT_SYMBOL_GPL(gpiod_to_irq); |
Xét ví dụ sử dụng ngắt để bật tắt LED với mỗi lần bấm trên beaglebone black
- Chân GPIO0_31: chức năng button
- Chân GPIO0_20: chức năng đèn LED
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 |
#include <linux/module.h> #include <linux/kernel.h> #include <linux/time.h> #include <linux/io.h> #include <linux/delay.h> #include <linux/time.h> #include <linux/delay.h> #include <linux/timer.h> #include <linux/slab.h> #include <linux/gpio.h> #include <linux/interrupt.h> #define GPIO0_31 (31) #define GPIO0_20 (20) #define BUTTON_DEV "example_of_irq" int irq = -1; /** * Hàm xử lý ngắt * irq: số hiệu ngắt * data: dữ liệu truyền vào */ static irqreturn_t button_isr(int irq, void *data) { static int count; if (count % 2 == 0) // bật LED gpio_set_value(GPIO0_20, 1); else // tắt LED gpio_set_value(GPIO0_20, 0); count++; // trả về giá trị báo xử lý ngắt thành công return IRQ_HANDLED; } int init_module(void) { int ret = -1; pr_info("Hello world driver is loaded\n"); //Tra cuu so hieu ngat cua chan gpio gpio_request(GPIO0_31,"GPIO0_31"); irq = gpio_to_irq(GPIO0_31); // đăng kí 1 hàm handler "button_isr" với 1 chân ngắt của hệ thống gpio31 ret = request_irq(irq, button_isr, IRQF_TRIGGER_RISING, "button_gpio31", BUTTON_DEV); ret = gpio_request(GPIO0_20,"GPIO_20"); gpio_direction_output(GPIO0_20, 0); return 0; } void cleanup_module(void) { pr_info("hello world driver is unloaded\n"); gpio_set_value(GPIO0_20, 0); // giải phóng ngắt vì ngay cả khi rmmod thì nó vẫn còn trong bộ nhớ free_irq(irq, BUTTON_DEV); } MODULE_LICENSE("GPL"); MODULE_AUTHOR("AUTHOR"); MODULE_DESCRIPTION("GPIO led kernel module"); |
Tương tự để chạy driver trên, ta cần load vào kernel sử dụng câu lệnh insmod và rmmod sau khi đã biên dịnh thành công với file “.ko”
4. Remote Debug trên VSCode
Kỹ thuật remote debug là một kỹ thuật phổ biến và rất hữu ích trong lĩnh vực Linux Embedded
Trong các dự án thực tế, người lập trình viên thường lập trình ở máy tính chạy hệ điều hành Linux hoặc trên máy ảo Ubuntu, mà chương trình đó chạy trên board. Chính vì vậy, remote debug là cần thiết. Ngoài ra, máy ảo Ubuntu thường kết nối mới máy tính Window thông qua các kết nối như SSH, EtherNet,…
![](https://vinalinux.com.vn/wp-content/uploads/2023/11/image-48.png)
Để có thể remote debug, trên board sẽ chạy gdb server và nhận file binary và yêu cầu debug từ Ubuntu. Ubuntu sẽ chạy gdb client truyền lệnh debug sang gdb server. Trên Window là nơi chạy VSCode – dùng để lập trình và xem thông tin trong quá trình debug.
Ta xét ví dụ debug một chương trình “debug.c” tính tổng sau đây:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 |
#include <stdio.h> #include <string.h> int sum(int i, int n) { int sum = 0; for (i = 0; i < n; i++) sum = sum + i; // gây ra lỗi sai tính tổng để debug sum--; return sum; } int main() { int i = 0; int n = 100; int val = 0; val = sum(i, n); printf("sum = %d", val); return 0; } |
Trên máy Ubuntu, ta thực hiện biên dịch “debug.c” với cmd:
1 |
gcc -g -o debug debug.c |
Sau đó, ta thực hiện coppy file binary “debug” thu được sang board để run chương trình. Trên board ta thực hiện cmd sau để run gdbserver:
1 2 3 4 |
gdbserver: 6000 ./debug // 6000 – địa chỉ port trùng với bên gdb client // debug – file run chương trình |
gdbserver sẽ lắng nghe các câu lệnh debug đến từ gdb client thông qua port 6000
Ta coi VScode như một client gửi các lệnh debug xuống board. Để debug được trên VSCode chúng ta làm theo các bước sau:
- Vào phần Run and Debug:
![](https://vinalinux.com.vn/wp-content/uploads/2023/11/image-49.png)
- Chọn “create a launch.json” và cấu hình file như sau:
1 2 3 4 5 6 7 8 9 10 11 |
{ "version": "0.2.0", "name": "C Launch", "MIMode": "gdb", "miDebuggerPath": "/usr/bin/gdb", "miDebuggerServerAddress": "192.168.7.2:6000", "program": "/home/loind/work/learn_Linux/unit11_general/debug/debug", "cwd": "/home/loind/work/learn_Linux/unit11_general/debug", "configurations": [] } |
miDebuggerPath: chứa đường dẫn của gdb trong board
miDebuggerServerAddress: địa chỉ của port kèm theo port
program: đường dẫn file bin của chương trình
cwd: đường dẫn tới thư mục chứa chương trình
- Ấn vào “Run and Debug” để bắt đầu debug:
![](https://vinalinux.com.vn/wp-content/uploads/2023/11/image-50.png)
Ta có thể đặt break point vào bất cứ câu lện nào và có thể add các biến vào Watch và thay đổi giá trị của biến