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.
Trong chuỗi bài viết này, mình sẽ giới thiệu đến các bạn một số khái niệm, các hàm chức năng thường sử dụng và hướng dẫn ví dụ thực hành xử lý chuỗi trên vi điều khiển STM32.
Phần 1. Một số khái niệm và các hàm chức năng thường gặp
1. Ký tự và chuỗi (Character & string)
Ký tự (character) là thành phần cơ bản nhất của một văn bản gồm những chữ cái, chữ số và các ký hiệu được sắp xếp với nhau tạo thành các chuỗi ký tự. Ký tự được sử dụng trong mã chương trình gọi là hằng ký tự, mỗi hằng ký tự tương ứng với mỗi giá trị số nguyên trong bảng mã ASCII, hằng ký tự thì được đặt trong cặp dấu nháy đơn ‘’. Ví dụ như hằng ký tự ‘a’ có giá trị nguyên tương ứng là 97, ký tự ‘A’ có giá trị 65, ký tự ‘0’ có giá trị 48 trong bảng mã ASCII. Một số hằng ký tự đặc biệt thường được sử dụng xuống dòng (‘\n’), về đầu hàng (‘\r’), dấu tab (‘\t’), in dấu \ (‘\\’), in dấu “ (‘\”’), in dấu ‘ (‘\’’), in dấu % (%%),…
Chuỗi (string) là một tập hợp của các ký tự được kết thúc bởi ký tự NULL (‘\0’). Trong ngôn ngữ lập trình, chuỗi bao gồm các ký tự hằng được gọi là chuỗi hằng, các chuỗi hằng thì được đặt trong cặp dấu ngoặc kép. Có thể truy cập đến một chuỗi thông qua một con trỏ (pointer) trỏ đến ký tự đầu tiên của chuỗi. Giá trị của một chuỗi chính là địa chỉ của ký tự bắt đầu của chuỗi đó. Ví dụ chuỗi: “Lap trinh xu ly chuoi”, “Vi dieu khien 32 – bit”, “TAPIT Community”
Một mảng ký tự hoặc một biến con trỏ kiểu char có thể được dùng để khai báo một chuỗi:
1 2 |
char animal[] = "cat"; //cách 1 const char *animalPtr = "cat"; //cách 2 |
Cả hai cách khởi tạo một biến chuỗi là “cat”. Chuỗi đầu tiên sẽ tạo ra một mảng gồm có 4 phần tử để chứa 4 ký tự là ‘c’, ‘a’, ‘t’ và ‘\0’. Cách thứ 2 tạo ra một con trỏ trỏ đến chuỗi được lưu “cat” tại vùng nhớ chỉ đọc. Nếu bạn muốn chỉnh sửa nội dung các ký tự trong chuỗi thì nên khai báo theo cách thứ nhất là sử dụng mảng. Bởi vì, cách khai báo sử dụng biến con trỏ kiểu char trình biên dịch sẽ đặt nó vào trong bộ nhớ chỉ đọc và giá trị không thể sửa đổi được thông qua việc sử dụng con trỏ.
Lưu ý 1: khi khai báo một mảng để chứa một chuỗi thì kích thước của mảng đó phải đủ lớn, tối thiểu bằng độ dài của chuỗi và ký tự kết thúc. Nếu khai báo một mảng có số phần tử nhỏ hơn kích thước của chuỗi sẽ dẫn đến lỗi khi khai báo. Như cách khai báo thứ nhất ở trên, trình biên dịch sẽ tự động xác định kích thước của mảng dựa trên chuỗi khởi tạo. Có thể khai báo lại bằng cách thêm số phần tử của mảng vào như sau:
1 2 |
char animal[4] = "cat"; char animal[4] = {'c', 'a', 't', '\0'}; |
Lưu ý 2: Ứng dụng chương trình hệ thống nhúng, khi nhận dữ liệu ở dạng chuỗi, thì chúng ta cần phải khai báo một bộ đệm (là một mảng) đủ lớn để chứa dữ liệu nhận được. Nếu nhận về một chuỗi có kích thước lớn hơn số phần tử khai báo của bộ đệm thì sẽ gây tràn bộ đệm, dữ liệu dư ra có thể ghi đè lên vùng nhờ phía sau gây ảnh hưởng đến dữ liệu chương trình. Các bạn có thể xây dựng các thuật toán để có thể tối ưu quá trình nhận dữ liệu để không xảy ra tình trạng tràn bộ đệm như trên bằng cách kiểm tra điều kiện chỉ số mảng nhỏ hơn kích thước mảng trước khi ghi dữ liệu hoặc thiết kế bộ đệm sử dụng thuật toán Ring Buffer,…
2. Các hàm xử lý chuỗi thường dùng
Các hàm xử lý chuỗi thường dùng để xử lý các ký tự và chuỗi được hỗ trợ bởi các thư viện hệ thống như <stdio.h>, <string.h>, <stdlib.h>, chúng ta sẽ thêm các thư viện này vào để có thể sử dụng các hàm được xây dựng sẵn.
2.1. Hàm tìm độ dài chuỗi
Hàm strlen() được hỗ trợ bởi thư viện <string.h>, sử dụng để xác định độ dài của chuỗi s. Số ký tự đứng trước ký tự NULL kết thúc chuỗi sẽ được trả về.
size_t strlen(const char *s);
1 2 3 4 5 6 7 8 9 10 |
#include <string.h> #include <stdio.h> int main(void) { char string1[] = "This is a string"; char string2[4] = "TAPIT"; printf("strlen(string1) = %d\n", strlen(string1)); printf("strlen(string2) = %d\n", strlen(string2)); } |
Kết quả:
1 2 |
strlen(string1) = 16 strlen(string2) = 5 |
2.2. Hàm sprintf
Hàm sprintf() được hỗ trợ bởi thư viện <stdio.h> được sử dụng để chuyển đổi các định dạng dữ liệu khác nhau thành một mảng ký tự. Kết quả trả về của hàm này là số ký tự được ghi vào mảng.
int sprintf(char *s, const char *format,…);
1 2 3 4 5 6 7 8 9 10 11 |
#include <string.h> #include <stdio.h> #define BUFFER_SIZE 50 int main(void) { char buffer[BUFFER_SIZE] = {0}; float temp = 33.1f; int humi = 77; sprintf(buffer, "Temperature: %2.2f - Humidity: %d", temp, humi); printf("%s", buffer); } |
Kết quả:
1 |
Temperature: 33.10 - Humidity: 77 |
Ở ví dụ trên, giá trị nhiệt độ ở dạng số thực, còn độ ẩm ở dạng số nguyên. Hàm sprintf() được sử dụng để tạo chuỗi trong đó chứa thông tin về nhiệt độ và độ ẩm từ các định dạng kiểu dữ liệu số.
Lưu ý: khi in một số thực dùng định dạng %f thì nên chỉ định thêm số ký tự sau dấu phẩy muốn in ra, vì nếu không thì nó sẽ in ra rất nhiều chữ số ở phần thập phân. Ở ví dụ trên, mình sử dụng %2.2f, tức là mình muốn in ra 2 chữ số ở phần nguyên và 2 chữ số ở phần thập phân.
2.3. Sao chép chuỗi
Thư viện <string.h> cung cấp hàm strcpy() và hàm strncpy() để thực hiện việc sao chép chuỗi.
char *strcpy(char *s1, const char *s2);
char *strncpy(char *s1, const char *s2, size_t n);
Hàm strcpy() sao chép s2 là một chuỗi vào mảng ký tự s1, bạn phải tự đảm bảo rằng kích thước của mảng s1 phải đủ lớn để chứa thêm chuỗi s2 và ký tự kết thúc.
Hàm strncpy() cũng giống như strcpy() nhưng còn kèm theo số phần tử cần sao chép từ chuỗi s2 vào mảng s1. Hàm strncpy() không dựa vào ký tự NULL kết thúc của chuỗi s2 và cũng không sao chép ký tự kết thúc của s2 mà chỉ sao chép n số lượng phần tử theo yêu cầu.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
#include <string.h> #include <stdio.h> #define BUFFER_SIZE1 25 #define BUFFER_SIZE2 9 int main(void) { char a[] = "Xin chao tat ca cac ban"; char b[BUFFER_SIZE1]; char c[BUFFER_SIZE2]; printf("Chuoi trong mang a: %s\n", a); strcpy(b, a); printf("Chuoi trong mang b: %s\n", b); strncpy(c, a, BUFFER_SIZE2 - 1); c[BUFFER_SIZE2 - 1] = '\0'; printf("Chuoi trong mang c: %s\n", c); } |
Kết quả:
1 2 3 |
Chuoi trong mang a: Xin chao tat ca cac ban Chuoi trong mang b: Xin chao tat ca cac ban Chuoi trong mang c: Xin chao |
Ở ví dụ trên, hàm strcpy() được sử dụng để sao chép toàn bộ chuỗi trong mảng a vào mảng b, hàm strncpy() được sử dụng để sao chép 8 ký tự của mảng a vào mảng c. Một ký tự NULL (‘\0’) được thêm vào mảng c bởi vì hàm strncpy() không sao chép ký tự kết thúc do đối số thứ 3 là số phần tử cần sao chép truyền vào nhỏ hơn độ dài của chuỗi a.
2.4. Nối chuỗi
Thư viện <string.h> cung cấp các hàm strcat() và strncat() để thực hiện việc nối chuỗi.
char *strcat(char *s1,const char *s2);
char *strncat(char *s1, const char *s2, size_t n);
Hàm strcat() sẽ thêm chuỗi s2 vào cuối mảng ký tự s1. Ký tự đầu tiên của s2 sẽ thay thế cho ký tự kết thúc của s1. Bạn phải tự đảm bảo rằng mảng s1 phải đủ lớn để lưu trữ thêm chuỗi s2 và ký tự kết thúc của nó sau khi sao chép chuỗi s2 vào mảng s1.
Hàm strncat() sẽ thêm một số lượng các ký tự mong muốn từ chuỗi s2 vào mảng s1. Một ký tự kết thúc sẽ được thêm tự động vào kết quả sau khi thực hiện.
1 2 3 4 5 6 7 8 9 10 11 12 |
#include <string.h> #include <stdio.h> int main(void) { char s1[15] = "Hello "; char s2[] = "World"; char s3[30] = ""; printf("s1 = %s, s2 = %s\n", s1, s2); printf("strcat(s1, s2) = %s\n", strcat(s1, s2)); printf("strncat(s3, s1, 5) = %s\n", strncat(s3, s1, 5)); printf("strcat(s3, s1) = %s\n", strcat(s3, s1)); } |
Kết quả:
1 2 3 4 |
s1 = Hello , s2 = World strcat(s1, s2) = Hello World strncat(s3, s1) = Hello strcat(s3, s1) = HelloHello World |
Một số khái niệm và các hàm chức năng xử lý chuỗi thường gặp đã được mình giới thiệu tại phần này. Các bạn cùng đón xem các phần tiếp theo vào thời gian đến.
Bạn nhớ theo dõi fanpage facebook và kênh youtube của TAPIT để nhận thông báo mới nhất về các bài viết, video chia sẻ kiến thức nhé!
Phần 2: Các hàm chức năng thường gặp (tiếp theo)
Phần 3: Thực hành xử lý chuỗi trên vi điều khiển STM32
Chúc các bạn thành công!
H.V.Binh, N.T.Nhien & N.H.N.Thuong