Khi lập trình vi điều khiển, nhiều trường hợp chúng ta cần làm việc với các dữ liệu ở dạng chuỗi ký tự, ví dụ như đóng gói các dữ liệu thành một chuỗi để lưu trữ hoặc gửi đi, nhận dữ liệu, bóc tách dữ liệu từ các bản tin nhận được để sử dụng. Xử lý chuỗi là một chủ đề quan trọng khi lập trình vi điều khiển, nhất là khi các hệ thống nhúng, IoTs ngày càng phát triển dẫn đến việc trao đổi dữ liệu giữa các thiết bị ngày càng nhiều. Xử lý chuỗi là một thử thách với rất nhiều bạn sinh viên / kỹ sư phần mềm nhúng – những người mới bắt đầu. Với tài liệu này, các kiến thức cơ bản liên quan đến khai báo, lưu trữ, xử lý chuỗi sẽ được tổng hợp lại và được mô tả, hướng dẫn thông qua những ví dụ, tình huống cụ thể, thường gặp.
Tiếp tục với phần 1 – Một số khái niệm và các hàm chức năng, chúng ta cùng đến với phần 2 của chuỗi bài viết.
Phần 2. Các hàm chức năng thường gặp (tt)
2.5. So sánh chuỗi
Hàm strcmp() và strncmp() nằm trong thư viện <string.h> được sử dụng để so sánh hai chuỗi.
int strcmp(const char *s1, const char *s2);
int strncmp(const char *s1, const char *s2, size_t n);
Hàm strncmp() được dùng để so sánh chuỗi s1 với chuỗi s2. Hàm trả về 0, nhỏ hơn 0 hoặc lớn hơn 0 nếu s1 bằng, nhỏ hơn hoặc lớn hơn s2.
Hàm strncmp() so sánh đến n ký tự của chuỗi s1 với chuỗi s2. Kết quả trả về cũng là 0, nhỏ hơn 0 hoặc lớn hơn 0 nếu s1 bằng, nhỏ hơn hoặc lớn hơn s2.
Việc so sánh chuỗi thực chất là so sánh từng ký tự của s1 với s2 theo mã số của các ký tự trong bảng mã ASCII.
1 2 3 4 5 6 7 8 9 10 11 |
#include <string.h> #include <stdio.h> int main(void) { const char *s1 = "Nguyen Van A"; const char *s2 = "Nguyen Van A"; const char *s3 = "Nguyen Thi B"; printf("strcmp(s1, s2) = %d\nstrcmp(s1, s3) = %d\nstrcmp(s3, s1) = %d\n\n", strcmp(s1, s2), strcmp(s1, s3), strcmp(s3, s1)); printf("strncmp(s1, s2, 7) = %d\nstrncmp(s1, s3, 8) = %d\nstrncmp(s3, s1, 8) = %d\n", strncmp(s1, s2, 7), strncmp(s1, s3, 8), strncmp(s3, s1, 8)); } |
Kết quả:
1 2 3 4 5 6 7 |
strcmp(s1, s2) = 0 strcmp(s1, s3) = 1 strcmp(s3, s1) = -1 strncmp(s1, s2, 7) = 0 strncmp(s1, s3, 8) = 1 strncmp(s3, s1, 8) = -1 |
2.6. Hàm strstr
Hàm strstr() được sử dụng để tìm ra vị trí xuất hiện đầu tiên của chuỗi s2 trong chuỗi s1. Nếu chuỗi được tìm thấy, một con trỏ trỏ đến vị trí của s2 trong s1 được trả về, ngược lại thì trả về con trỏ NULL.
char* strstr(const char *s1, const char *s2);
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
#include <string.h> #include <stdio.h> int main(void) { const char *s1 = "Tapit Community"; const char *s2 = "Com"; const char *s3 = "MCU"; if(strstr(s1, s2) != NULL) printf("strstr(s1, s2) = %s\n", strstr(s1, s2)); else { printf("Khong tim thay chuoi s2 trong s1\n"); } if(strstr(s1, s3) != NULL) printf("strstr(s1, s3) = %s\n", strstr(s1, s3)); else { printf("Khong tim thay chuoi s3 trong s1\n"); } } |
Kết quả:
1 2 |
strstr(s1, s2) = Community Khong tim thay chuoi s3 trong s1 |
Ở ví dụ trên, chuỗi s2 được tìm thấy trong chuỗi s1, giá trị trả về của hàm strstr() sau khi thực hiện trỏ đến vị trí ký tự x, khi gặp lệnh in ra màn hình nó sẽ in ra là xyzt. Chuỗi s3 không tìm thấy trong chuỗi s1, nên hàm trả về một con trỏ NULL, điều kiện của câu lệnh if thứ 2 không đúng nên chương trình in ra không tìm thấy chuỗi s3 trong s1.
2.7. Hàm strtok
Hàm strtok() được sử dụng để ngắt chuỗi s1 thành các token được ngăn cách nhau bởi các dấu delim s2 (thường là các chấm câu, dấu cách nhưng cũng có thể là bất kỳ ký tự nào).
char* strtok(char *s1, const char *s2);
Nếu thực hiện ngắt nhiều lần thì ở lần đầu tiên, s1 được dùng làm đối số thứ nhất của hàm strtok(), ở những lần gọi hàm tiếp theo thì truyền NULL thay cho s1. Một con trỏ trỏ đến token hiện tại được trả về sau mỗi lần gọi hàm. Nếu không còn token nào thì NULL được trả về.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
#include <string.h> #include <stdio.h> int main(void) { char s1[] = "Lap_trinh_xu_ly_chuoi"; char *token = NULL; token = strtok(s1, "_"); //Lap while(token != NULL) { printf("%s\n", token); token = strtok(NULL, "_"); } } |
Kết quả:
1 2 3 4 5 |
Lap trinh xu ly chuoi |
2.8. Các hàm thao tác với bộ nhớ
Các hàm thao tác với bộ nhớ xem bộ nhớ như là các mảng ký tự và sử dụng các hàm thao tác với bộ nhớ giúp chúng ta có thể thao tác với một vùng nhớ mà không cần quan tâm đến kiểu dữ liệu mà chúng ta lưu trữ tại vùng nhớ đó. Nhờ vào việc các tham số này được khai báo với con trỏ kiểu void thì chúng có thể thao tác bộ nhớ với bất kỳ kiểu dữ liệu nào. Bởi vì con trỏ kiểu void thì không thể được tham chiếu, nên mỗi hàm nhận được đối số kích thước chỉ định số ký tự mà hàm sẽ xử lý. Và các hàm trên thì không kiểm tra đến ký tự kết thúc chuỗi.
Hàm memcpy() sao chép n ký tự của khối dữ liệu được trỏ bởi s2 vào trong khối dữ liệu được trỏ bởi s1. Một con trỏ tới kết quả của khối dữ liệu được trả về.
char* memcpy(void *s1, const void *s2, size_t n);
Hàm memcpy() có thể nhận một con trỏ với bất kỳ kiểu dữ liệu nào. Kết quả của hàm thì không xác định nếu cả hai khối dữ liệu chồng lên nhau trong bộ nhớ. Hàm memcpy() sẽ hiệu quả hơn so với strcpy() khi bạn biết được số lượng ký tự của chuỗi mà bạn sao chép.
1 2 3 4 5 6 7 8 9 10 11 12 13 |
#include <string.h> #include <stdio.h> int main(void) { char s1[24]; char s2[20]; char s3[] = "Copy string with memcpy"; memcpy(s1, s3, 24); printf("memcpy(s1, s2, 24): %s\n", s1); memcpy(s2, s3, 20); printf("memcpy(s2, s3, 20): %s\n", s2); } |
Kết quả:
1 2 |
memcpy(s1, s2, 24): Copy string with memcpy memcpy(s2, s3, 20): Copy string with mem |
Hàm memcmp() so sánh n ký tự của khối dữ liệu được trỏ bởi s1 và s2. Kết quả trả về là 0, nhỏ hơn 0 hoặc lớn 0 nều s1 bằng, nhỏ hơn hoặc lớn hơn s2.
int memcmp(const void *s1, const void *s2, size_t n);
1 2 3 4 5 6 7 8 9 10 11 12 13 |
#include <string.h> #include <stdio.h> int main(void) { char s1[] = "ABCDefg"; char s2[] = "ABCDEFG"; printf("%s%s\n%s%s\n\n%s%2d\n%s%2d\n%s%2d\n", "s1 = ", s1, "s2 = ", s2, "memcmp(s1, s2, 4) = ", memcmp(s1, s2, 4), "memcmp(s1, s2, 7) = ", memcmp(s1, s2, 7), "memcmp(s2, s1, 7) = ", memcmp(s2, s1, 7)); } |
Kết quả:
1 2 3 4 5 6 |
s1 = ABCDefg s2 = ABCDEFG memcmp(s1, s2, 4) = 0 memcmp(s1, s2, 7) = 1 memcmp(s2, s1, 7) = -1 |
Hàm memset() sao chép ký tự c (được chuyển thành kiểu unsigned char) vào n ký tự đầu tiên của khối dữ liệu được trỏ bởi s. Một con trỏ trỏ tới s được trả về.
void *memset(void *s, int c, size_t n);
Hàm memset() có thể được sử dụng để set các phần tử của mảng về 0 thay vì sử dụng vòng lặp để duyệt từng phần tử và gán từng phần tử của mảng bằng 0.
1 2 3 4 5 6 7 8 9 |
#include <string.h> #include <stdio.h> int main(void) { char string[] = "This is a string"; printf("string = %s\n\n", string); printf("memset(string, 'a', 10): %s\n", (char *) memset(string, 'a', 10)); } |
Kết quả:
1 2 3 |
string = This is a string memset(string, 'a', 10): aaaaaaaaaastring |
3. Ví dụ thực hành trên vi điều khiển
Ở phần trên, thì chúng ta đã tìm hiểu các hàm và một số ví dụ cơ bản bằng C. Trong phần này sẽ thực hiện trên một dòng vi điều khiển phổ biến là STM32. Đối với Arduino, các bạn có thể tham khảo bài viết chia sẻ về xử lý chuỗi, giao tiếp Serial tại đây.
Ở phần này, mình sẽ đặt ra một bài tập để luyện tập các hàm xử lý chuỗi với vi điều khiển STM32 như sau:
Vi điều khiển (device) và máy tính giao tiếp với nhau qua giao tiếp UART thông qua giao diện terminal (ví dụ Hercules). Trên thiết bị sẽ có một mã ID của thiết bị và có 2 đèn led (LED1 và LED2). Trên máy tính, thông qua giao diện terminal có thể cấu hình và đọc được ID của thiết bị, thay đổi trạng thái của các đèn LED. Viết chương trình điều khiển đơn giản sử dụng các lệnh sau để giao tiếp giữa máy tính và thiết bị:
- Cú pháp cài đặt mã ID cho thiết bị và phản hồi:
- Máy tính: +SET_DEV_ID:<ID>\r\n ; ID dài 6 ký tự.
- Thiết bị: +OK\r\n hoặc +ERROR\r\n
- Lấy mã ID của thiết bị
- Máy tính: +GET_DEV_ID\r\n
- Thiết bị: +ID:123456\r\nOK\r\n hoặc +ERROR:undefined\r\n hoặc +ERROR
- Máy tính: +GET_DEV_ID\r\n
- Cấu hình trạng thái của đèn LED
- Máy tính: +SET_LED_ STATE:n,s\r\n
- n là đèn led muốn cấu hình (n = 1, 2)
- s là trạng thái muốn cấu hình, s = 0: tắt LED, s = 1: bật LED.
- Thiết bị: +OKE\r\n hoặc +ERROR\r\n
- Máy tính: +SET_LED_ STATE:n,s\r\n
- Lấy trạng thái của đèn LED
- Máy tính: +GET_LED_STATE:n\r\n
- n là đèn LEDmuốn lấy trạng thái (n = 1, 2)
- Thiết bị: +LED:n,s\r\nOK\r\n
- n là đèn LEDmuốn lấy cấu hình (n = 1, 2)
- s là trạng thái (ON, OFF).
- Máy tính: +GET_LED_STATE:n\r\n
Tại phần cuối của chuỗi bài này, chúng ta sẽ phân tích bài toán và chương trình tham khảo, mời các bạn tìm hiểu tại đây!
Chúc các bạn thành công!
H.V.Binh, N.T.Nhien & N.H.N.Thuong