Sự phức tạp của lập trình nhúng
Đây là bước đầu tiên để có thể thấy các vấn đề nhúng từ lập trình máy PC, học cách sử dụng các ý tưởng lập trình nhúng, đó là bước thứ hai, sử dụng ý tưởng PC và ý tưởng nhúng để kết hợp và áp dụng chúng vào các dự án thực tế, sau đó Đây là bước thứ ba. Nhiều người bạn đã chuyển từ lập trình PC sang lập trình nhúng. Ở Trung Quốc, một vài người bạn lập trình nhúng đã tốt nghiệp chuyên ngành máy tính. Tất cả họ đều tốt nghiệp chuyên ngành điều khiển tự động và điện tử. Những đôi giày trẻ em này có kinh nghiệm thực tế mạnh mẽ, nhưng thiếu kiến thức lý thuyết, một phần lớn những đôi giày trẻ em tốt nghiệp ngành khoa học máy tính đi đến các ứng dụng cấp cao hơn như trò chơi trực tuyến và trang web độc lập với hệ điều hành. Tôi cũng miễn cưỡng tham gia vào ngành công nghiệp nhúng. Họ có kiến thức lý thuyết mạnh mẽ, nhưng họ thiếu mạch và kiến thức liên quan khác. Họ cần học một số kiến thức cụ thể khi học nhúng, điều này khó hơn.
Mặc dù tôi chưa thực hiện khảo sát ngành, từ những gì tôi đã thấy và tuyển dụng, các kỹ sư tham gia vào ngành nhúng hoặc thiếu kiến thức lý thuyết hoặc kinh nghiệm thực tế. Hiếm khi làm cả hai. Lý do vẫn là vấn đề của giáo dục đại học Trung Quốc. Vấn đề này sẽ không được thảo luận ở đây để tránh chiến tranh ngôn từ. Tôi muốn liệt kê một vài ví dụ từ thực tiễn của tôi. Khơi dậy sự chú ý của mọi người đối với một số vấn đề khi thực hiện các dự án được nhúng.
câu hỏi đầu tiên:
Một đồng nghiệp đã phát triển trình điều khiển cổng nối tiếp theo uC / OS-II, và trình điều khiển và giao diện được phát hiện có vấn đề trong thử nghiệm. Một chương trình giao tiếp được phát triển trong ứng dụng và trình điều khiển cổng nối tiếp cung cấp một hàm để truy vấn các ký tự trong bộ đệm trình điều khiển: GetRxBuffCharNum (). Lớp trên cần chấp nhận một số ký tự nhất định trước khi có thể phân tích gói. Mã được viết bởi một đồng nghiệp được thể hiện như sau trong mã giả:
bExit = SAI;
làm {
if (GetRxBuffCharNum ()> = 30)
bExit = ReadRxBuff (buff, GetRxBuffCharNum ());
} while (! bExit);
Mã này phán đoán rằng có hơn 30 ký tự trong bộ đệm hiện tại và đọc tất cả các ký tự trong bộ đệm vào bộ đệm cho đến khi đọc thành công. Logic là rõ ràng, và sự suy nghĩ là rõ ràng. Nhưng mã này không hoạt động đúng. Nếu nó là trên PC, chắc chắn không có vấn đề gì, và nó hoạt động bất thường. Nhưng nó thực sự không được biết đến trong nhúng. Đồng nghiệp rất chán nản và không biết tại sao. Hãy đến để hỏi tôi để giải quyết vấn đề. Khi tôi thấy mã, tôi hỏi anh ta, GetRxBuffCharNum () được triển khai như thế nào? Mở nó và xem:
không dấu GetRxBuffCharNum (void)
{
cpu_register reg;
số không dấu;
reg = interrupt_disable ();
num = gRxBuffCharNum;
interrupt_enable (reg);
trả lại (num);
}
Rõ ràng, vì trong vòng lặp, giữa interrupt_disable () và interrupt_enable () là một khu vực quan trọng toàn cầu, tính toàn vẹn của gRxBufCharNum được đảm bảo. Tuy nhiên, vì trong vòng lặp do {} while () bên ngoài, CPU thường xuyên đóng và mở các ngắt, thời gian này rất ngắn. Trong thực tế, CPU có thể không đáp ứng với ngắt UART bình thường. Tất nhiên, điều này có liên quan đến tốc độ truyền của uart, kích thước của bộ đệm phần cứng và tốc độ của CPU. Tốc độ baud chúng tôi sử dụng rất cao, khoảng 3Mbps. Tín hiệu khởi động uart và tín hiệu dừng chiếm một bit. Một byte cần tiêu thụ 10 chu kỳ. Tốc độ truyền của 3Mbps cần khoảng 3,3us để truyền một byte. Có bao nhiêu lệnh CPU có thể thực thi 3.3us? Một ARM 100 MHz có thể thực hiện khoảng 150 hướng dẫn. Kết quả là, mất bao lâu để đóng ngắt? Nói chung, ARM yêu cầu nhiều hơn 4 hướng dẫn để tắt ngắt và hơn 4 hướng dẫn để bật. Mã nhận được ngắt uart thực sự là hơn 20 hướng dẫn. Do đó, theo cách này, có thể có lỗi thiếu dữ liệu truyền thông, được phản ánh ở cấp hệ thống, đó là giao tiếp không ổn định.
Sửa đổi mã này thực sự rất đơn giản, cách dễ nhất là sửa đổi nó từ mức cao. đó là:
bExit = SAI;
làm {
DelayUs (20); // Trì hoãn 20us, thường được nhận ra bằng lệnh lặp trống
num = GetRxBuffCharNum ();
nếu (num> = 30)
bExit = ReadRxBuff (buff, num);
} while (! bExit);
Theo cách này, CPU có thời gian để thực thi mã bị gián đoạn, do đó tránh việc thực thi kịp thời mã bị gián đoạn gây ra bởi các ngắt thường xuyên và mất thông tin được tạo. Trong các hệ thống nhúng, hầu hết các ứng dụng RTOS đều không có trình điều khiển cổng nối tiếp. Khi tự thiết kế mã, sự kết hợp giữa mã và nhân không được xem xét đầy đủ. Gây ra các vấn đề sâu trong mã. RTOS được gọi là RTOS vì phản ứng nhanh với các sự kiện, phản ứng nhanh với các sự kiện phụ thuộc vào tốc độ phản hồi của CPU đối với các ngắt. Trình điều khiển được tích hợp cao với kernel trong các hệ thống như Linux và chạy trong trạng thái kernel cùng nhau. Mặc dù RTOS không thể sao chép cấu trúc của Linux, nhưng nó có ý nghĩa tham khảo nhất định.
Có thể thấy rõ từ ví dụ trên rằng các nhà phát triển nhúng cần phải hiểu rõ tất cả các khía cạnh của mã.
Ví dụ thứ hai:
Các đồng nghiệp lái một con chip nối tiếp song song 14094. Tín hiệu nối tiếp được mô phỏng bởi IO vì không có phần cứng chuyên dụng. Một đồng nghiệp đã viết một trình điều khiển tình cờ, và sau khi gỡ lỗi trong 3 hoặc 4 ngày, vẫn còn một vấn đề. Tôi thực sự không thể chịu đựng được nữa, vì vậy tôi đã đi và xem xét. Tín hiệu song song được kiểm soát đôi khi là bình thường và đôi khi bất thường. Tôi đã xem mã, mã giả có lẽ là:
cho (i = 0; i <8; i ++)
{
SetData ((dữ liệu >> i) & 0x1);
SetClockHigh ();
cho (j = 0; j <5; j ++);
SetClockLow ();
}
Gửi 8 bit dữ liệu theo thứ tự từ bit0 đến bit7 ở mỗi mức cao. Nó nên bình thường. Không thể thấy vấn đề ở đâu? Tôi đã suy nghĩ kỹ về nó, đọc bảng dữ liệu 14094 và tôi hiểu. Hóa ra 14094 yêu cầu mức cao của đồng hồ kéo dài trong 10 ns và mức thấp kéo dài trong 10 ns. Mã này thực hiện độ trễ thời gian ở mức cao, nhưng không phải là độ trễ ở mức thấp. Nếu ngắt được chèn giữa các mức thấp để làm việc, thì mã này là OK. Nhưng nếu CPU được thực thi khi CPU không bị gián đoạn ở mức thấp, nó sẽ không hoạt động bình thường. Vì vậy, nó tốt và xấu.
Việc sửa đổi cũng tương đối đơn giản:
cho (i = 0; i <8; i ++)
{
SetData ((dữ liệu >> i) & 0x1);
SetClockHigh ();
cho (j = 0; j <5; j ++);
SetClockLow ();
cho (j = 0; j <5; j ++);
}
Điều này là hoàn toàn bình thường. Nhưng đây vẫn là một mã không thể được ghép tốt, bởi vì một khi trình biên dịch tối ưu hóa, nó có thể gây ra mất hai vòng lặp trễ này. Nếu nó bị mất, yêu cầu kéo dài ở mức thấp ở mức cao là 10ns không thể được đảm bảo và nó sẽ không hoạt động bình thường. Do đó, đối với mã thực sự di động, vòng lặp này phải được tạo thành DelayNs nano giây (10);
Giống như Linux, khi bật nguồn, trước tiên hãy đo thời gian thực hiện lệnh nop và bao nhiêu lệnh nop thực thi 10ns. Thực hiện các hướng dẫn nop nhất định. Sử dụng trình biên dịch để ngăn chặn các hướng dẫn biên dịch tối ưu hóa hoặc các từ khóa đặc biệt để ngăn các vòng lặp trì hoãn được tối ưu hóa bởi trình biên dịch. Như trong GCC
__voliverse__ __asm __ ("nop;");
Có thể thấy rõ từ ví dụ này rằng viết một mã tốt đòi hỏi nhiều kiến thức. bạn nói gì?
Các hệ thống nhúng thường không có hỗ trợ hệ điều hành hoặc do chúng có hỗ trợ hệ điều hành, nhưng do nhiều hạn chế khác nhau, hệ điều hành cung cấp rất ít chức năng. Do đó, rất nhiều mã không thể được sử dụng như lập trình PC. Hôm nay tôi sẽ nói về phân bổ bộ nhớ và phân mảnh bộ nhớ, có thể quen thuộc với mọi người. Tuy nhiên, trong các hệ thống nhúng, điều đáng sợ nhất là sự phân mảnh bộ nhớ, đây cũng là kẻ giết người số một về sự ổn định của hệ thống. Tôi đã từng thực hiện một dự án. Có rất nhiều mallocs và giải phóng trong hệ thống, với các kích cỡ khác nhau, từ hơn 60 byte đến 64KB. Sử dụng RTOS làm hỗ trợ. Vào thời điểm đó, tôi có hai lựa chọn, một là sử dụng malloc và không có thư viện hệ thống C, và thứ hai là sử dụng phân bổ bộ nhớ cố định do hệ điều hành cung cấp. Thiết kế hệ thống của chúng tôi yêu cầu hoạt động ổn định trong hơn 3 tháng. Trên thực tế, nó đã đi xuống sau 6 ngày hoạt động liên tục. Nhiều vấn đề đã bị nghi ngờ và quyết định cuối cùng là phân bổ bộ nhớ. Thực tế, đó là một thời gian dài. Sau khi một lượng lớn bộ nhớ được phân bổ, bộ nhớ hệ thống bị phân mảnh và không thể liên tục. Mặc dù có một không gian rộng lớn, không gian liên tục không thể được phân bổ. Khi có một không gian rộng để áp dụng, đó chỉ có thể là thời gian chết. Để làm cho hệ thống đáp ứng các yêu cầu thiết kế ban đầu, chúng tôi đã mô phỏng toàn bộ phần cứng trên PC, chạy mã nhúng trên PC và malloc quá tải và tự do thực hiện một chương trình thống kê phức tạp. Hành vi bộ nhớ của hệ thống thống kê. Sau khi chạy được vài ngày, dữ liệu đã được trích xuất và phân tích. Mặc dù bộ nhớ được yêu cầu rất đắt, nhưng vẫn có một số quy tắc. Chúng tôi phân loại những cái dưới 100 byte thành một loại, loại có 512B thành một loại và loại có 1KB thành một loại. Lớp, 2KB được phân loại thành một loại và 64KB được phân loại thành một loại. Đếm số lượng của mỗi loại và thêm 30% cho cơ sở ban đầu. Làm cho một ứng dụng bộ nhớ cố định làm tăng đáng kể thời gian để hệ thống chạy ổn định và liên tục. Nhúng là như thế này, không sợ phương thức ban đầu, nhưng hiệu suất không theo yêu cầu.
Sự cố tràn bộ nhớ, sự cố tràn bộ nhớ, hệ thống nhúng khủng khiếp hơn hệ thống PC! Thường bị tràn mà không nhận thấy. Thật khó để nghĩ, đặc biệt đối với những người mới bắt đầu sử dụng C / C ++, những người không quen thuộc với con trỏ và không thể kiểm tra chúng. Vì hệ thống PC có MMU, khi bộ nhớ bị cắt ngang nghiêm trọng, nó được bảo vệ bởi MMU, điều này sẽ không gây ra hậu quả thảm họa nghiêm trọng. Tuy nhiên, các hệ thống nhúng thường không có MMU, rất khác nhau và mã hệ thống vẫn có thể chạy nếu chúng bị phá hủy. Chỉ có Chúa và CPU đó biết nó đang chạy cái gì. Chúng ta hãy xem mã này:
char * strcpy (char * mệnh, const char * src)
{
khẳng định (mệnh! = NULL && src! = NULL);
trong khi (* src! = '�')
{
* mệnh ++ = * src ++;
}
* mệnh = '�';
trở về (mệnh);
}
Mã này là một mã sao chép chuỗi, được viết như thế này trên PC, về cơ bản là ổn. Nhưng có một thứ được nhúng để coi chừng, đó là src thực sự kết thúc bằng a'� '. Nếu không, nó sẽ là một bi kịch. Khi nào nó sẽ kết thúc? Ồ, chỉ có ông già của Chúa biết. Nếu mã này có thể chạy tình cờ, đừng mong chương trình chạy bình thường. Bởi vì vùng nhớ được chỉ ra bởi mệnh gần như bị phá hủy. Để tương thích với thư viện C / C ++ tiêu chuẩn, thực sự không có cách nào tốt, vì vậy vấn đề này chỉ có thể để người lập trình kiểm tra.
giống hệt nhau
memcpy (mệnh, src, n);
Vấn đề tương tự với bản sao bộ nhớ, hãy cẩn thận khi chuyển một giá trị âm cho n. Đây là số lượng byte được sao chép và các giá trị âm được ép buộc thành dương. Nó trở thành một con số tích cực lớn, khiến tất cả ký ức sau định mệnh bị phá hủy ...
Con trỏ bộ nhớ trong hệ thống nhúng phải được kiểm tra nghiêm ngặt trước khi có thể sử dụng và kích thước của bộ nhớ cũng phải được gỡ lỗi nghiêm ngặt. Nếu không, bi kịch là khó tránh. Chẳng hạn như một con trỏ hàm, mặc dù NULL, 0 được gán trong nhúng. Nếu là ARM, thậm chí không có lỗi bất thường và nó được đặt lại trực tiếp, bởi vì việc gọi con trỏ hàm này thậm chí còn làm cho mã chạy từ 0. Và 0 là vị trí của mã đầu tiên chạy sau khi ARM được bật. Điều này đặc biệt đúng trên ARM7. Loại bi kịch này bi thảm hơn nhiều so với trên PC và MMU phải đưa ra một lỗi hướng dẫn không xác định. Khơi dậy sự chú ý của các lập trình viên. Trong nhúng, tất cả còn lại để lập trình viên tìm thấy.
Tràn bộ nhớ xảy ra tại bất kỳ thời điểm vô ý nào. Bạn phân bổ bao nhiêu heap cho toàn bộ hệ thống front-end và back-end (hoặc hệ điều hành)? Làm thế nào lớn là ngăn xếp? Trong trường hợp bình thường, độ sâu cuộc gọi của hệ thống (tối đa là bao nhiêu) và nó chiếm bao nhiêu ngăn xếp? Chỉ nhìn vào chức năng chính xác của chương trình là không đủ. Bạn cũng cần phải đếm các tham số này. Nếu không, miễn là có tràn. Nó gây tử vong cho hệ thống. Hệ thống nhúng đòi hỏi thời gian làm việc liên tục dài và đòi hỏi sự ổn định và độ tin cậy. Phải mất một thời gian để nghiền cẩn thận các hệ thống này.
Việc gỡ lỗi hệ thống nhúng thường rất phức tạp, các phương tiện khả dụng không nhiều như lập trình PC và chi phí phát triển lớn hơn nhiều so với hệ thống PC. Các phương pháp gỡ lỗi chính cho các hệ thống nhúng chỉ là theo dõi một bước được đại diện bởi JTAG và printf để tiêu diệt Đại Pháp.
Hai phương pháp sửa lỗi này không thể giải quyết được vấn đề trong hệ thống nhúng. Jtag yêu cầu trình gỡ lỗi phải có thiết bị gỡ lỗi (có thể tốn kém) và kết nối với hệ thống đích. Sử dụng phần mềm như GDB Client để đăng nhập vào thiết bị gỡ lỗi và theo dõi chương trình đang chạy. Thành thật mà nói, phương pháp này là phương pháp gỡ lỗi cuối cùng để nhúng và nó cũng là một phương pháp gỡ lỗi tốt hơn. Tuy nhiên, vẫn còn một số thiếu sót. Khi có quá nhiều điểm dừng, giới hạn phần cứng bị vượt quá. Một số CPU cấp thấp không hỗ trợ nhiều điểm dừng hơn. Do đó, cần sử dụng JTAG để sử dụng mô phỏng phần mềm hoặc bẫy phần mềm (ngắt mềm hoặc ngoại lệ). Thực hiện các điểm dừng. Cơ chế phức tạp hơn. Nói một cách đơn giản, 1. Nó không thể được gỡ lỗi trong một thời gian dài và không ổn định; 2. Nó có thể ảnh hưởng đến hành vi của chương trình tại thời điểm hoạt động, thông qua hiệu ứng thời gian. Sau khi hệ thống JTAG được kết nối, các điểm dừng được triển khai bằng phần cứng sẽ không ảnh hưởng đến tốc độ của hệ thống, nhưng các điểm dừng được triển khai bằng phần mềm phải hy sinh một số hiệu suất. Độ tin cậy phải được thỏa hiệp