STM32F407(STM32F4-DISCOVERY) - Cách tiếp cận không chuẩn - Thư viện chuẩn phần 1. Thư viện chuẩn ngoại vi

Một lần nữa tôi muốn viết về sự khởi đầu đơn giản với STM32, chỉ lần này mà không sử dụng mẫu hoặc ví dụ của bất kỳ ai - kèm theo giải thích từng bước. Các bài viết sẽ được đánh số bước liên tục.

1. Cài đặt IAR

Xây dựng dự án trong IAR

1. Bộ tiền xử lý

  1. xóa bình luận

2. Trình biên dịch

3. Trình liên kết

3. Tạo dự án mới trong IAR

Sau khi khởi chạy IAR, một cửa sổ xuất hiện Trung tâm Thông tin, mà chúng tôi không cần. Nhấp vào menu Dự án -> Tạo dự án mới. Chọn chuỗi công cụ: ARM (có thể bạn sẽ không có bất kỳ thứ gì khác trong danh sách đó), Mẫu dự án: C -> main.

Trong cửa sổ bên trái (“Không gian làm việc”) click chuột phải chuột gọi menu và tạo nhóm mới(Thêm ->

Nhấp chuột phải vào CMSIS

Tới nhóm Khởi động

Chúng ta đã hoàn tất CMSIS.

Tới nhóm StdPeriphLib

Tới nhóm Người dùng

5. Thiết lập dự án

  1. Tùy chọn chung -> Mục tiêu ->
Chọn ST -> STM32F100 -> ST STM32F100xB. Đây là bộ điều khiển của chúng tôi. 2. Tùy chọn chung -> Cấu hình thư viện -> CMSIS: chọn hộp kiểm Sử dụng CMSIS. Vì vậy chúng ta sẽ sử dụng thư viện CMSIS được tích hợp sẵn trong trình biên dịch. Kể từ phiên bản 6.30, IAR bắt đầu được cung cấp CMSIS tích hợp và điều này có vẻ tốt hơn - nhưng nó gây ra một số nhầm lẫn với các dự án cũ hơn. 3. Trình biên dịch C/C++ ->
$PROJ_DIR$\

* Debugger –> Setup –> Driver: chọn ST–Link, vì đây là bộ lập trình được tích hợp sẵn trong bảng Discovery. Bây giờ chúng ta tự cấu hình bộ lập trình: * Trình gỡ lỗi -> ST–LINK -> Giao diện: chọn SWD (bộ lập trình trên bo mạch được kết nối với bộ điều khiển qua SWD, không qua JTAG). * Trình gỡ lỗi ->
#include "stm32f10x_conf.h" 

khoảng trống chính()
{
trong khi(1)
{
}
}

<1000000; i++);


vì(i=0; tôi<1000000; i++);

#include "stm32f10x_conf.h"

khoảng trống chính()
{





int tôi;
trong khi(1)
{

vì(i=0; tôi<1000000; i++);

<1000000; i++); } }

lưu trữ với dự án GPIO. May mắn thay, bạn có thể lưu dự án này và sử dụng nó làm mẫu để không phải thực hiện lại toàn bộ quá trình thiết lập. Toàn bộ chu trình: 1. Cổng I/O (/index.php/stm32-from_zero_to_rtos-2_timers/ "STM32 - từ 0 đến RTOS. 2: Hẹn giờ và ngắt") (/index.php/stm32-from_zero_to_rtos-3_timer_outputs/ " STM32 - từ đầu đến RTOS. 3: Đầu ra bộ hẹn giờ") [Một lần nữa tôi muốn viết về sự khởi đầu đơn giản với STM32, chỉ lần này mà không sử dụng mẫu hoặc ví dụ của bất kỳ ai - kèm theo lời giải thích về từng bước. Các bài viết sẽ được đánh số bước liên tục.

0. Chúng tôi giải nén bảng STM32VLDiscovery

Chúng tôi mua nó ở cửa hàng, nó có giá 600 rúp. Bạn sẽ cần cài đặt trình điều khiển trên bo mạch - tôi nghĩ điều này sẽ không gây ra bất kỳ khó khăn nào.

1. Cài đặt IAR

Chúng tôi sẽ làm việc trong IAR - một IDE tốt với trình biên dịch xuất sắc. Nó thiếu sự tiện lợi của việc viết mã - nhưng đối với mục đích của chúng tôi thì nó khá đủ. Tôi sử dụng IAR phiên bản 6.50.3, bạn biết lấy nó ở đâu.

2. Tải thư viện ngoại vi

Tôi không phải là người thích làm việc với các thanh ghi trong giai đoạn học tập. Do đó, tôi khuyên bạn nên tải xuống thư viện ngoại vi từ ST để có được các chức năng thuận tiện cho việc truy cập tất cả các cài đặt cần thiết.

Tạo một thư mục “STM32_Projects”, đặt thư mục Thư viện vào đó từ kho lưu trữ đã tải xuống (stsw-stm32078.zip/an3268/stm32vldiscovery_package), nó chứa CMSIS (thư viện từ ARM cho tất cả các bộ vi điều khiển Cortex, mô tả và địa chỉ của tất cả các tài nguyên) và STM32F10x_StdPeriph_Driver - một thư viện ngoại vi từ ST với tất cả các tính năng.

Chúng tôi cũng tạo một thư mục ở đó “1. GPIO”, đây sẽ là dự án đầu tiên của chúng tôi.

Cây thư mục được hiển thị trong hình. Hãy làm theo cách này nhé, vì sau này các đường dẫn tương đối trong cây này sẽ rất quan trọng.

Chà, để hiểu những gì chúng ta đang nói đến, hãy tải xuống tài liệu 1100 trang về các bộ điều khiển này.

Xây dựng dự án trong IAR

Cần phải hiểu rõ bản chất của quá trình lắp ráp dự án. Để thuận tiện, chúng tôi sẽ chia nó thành các giai đoạn.

1. Bộ tiền xử lý

Bộ tiền xử lý đi qua tất cả các tệp .c của dự án (cả main.c và tất cả các tệp trong không gian làm việc). Nó thực hiện như sau:

  1. xóa bình luận
  2. mở rộng các lệnh #include, thay thế chúng bằng nội dung của tệp được chỉ định. Quá trình này diễn ra đệ quy, bắt đầu từ tệp .c và nhập từng lệnh #include .h gặp phải và nếu chỉ thị #include cũng gặp trong tệp .h, bộ tiền xử lý cũng sẽ nhập chúng. Điều này dẫn đến một cây bao gồm. Xin lưu ý: nó không xử lý tình huống bao gồm kép, tức là cùng một tệp .h có thể được đưa vào nhiều lần nếu nó được #included ở nhiều vị trí trong dự án. Tình huống này cần được xử lý bằng defins.
  3. thực hiện thay thế macro - mở rộng macro
  4. thu thập các chỉ thị của trình biên dịch.

Bộ tiền xử lý tạo ra các tệp .i, khá thuận tiện khi tìm kiếm lỗi xây dựng - nếu chỉ vì tất cả các macro đều được tiết lộ đầy đủ trong đó. Việc lưu các tệp này có thể được bật trong cài đặt dự án.

Tại thời điểm này, trình xây dựng đã sẵn sàng biên dịch tất cả các tệp .c trong dự án - dưới dạng tệp .i. Chưa có kết nối nào giữa các tập tin.

2. Trình biên dịch

Sau khi đi qua bộ tiền xử lý, trình biên dịch sẽ tối ưu hóa và biên dịch từng tệp .i, tạo mã nhị phân. Đây là nơi bạn cần chỉ định loại bộ xử lý, bộ nhớ khả dụng, ngôn ngữ lập trình, mức độ tối ưu hóa và những thứ tương tự.

Trình biên dịch sẽ làm gì khi gặp một lệnh gọi hàm trong một số tệp .c không được mô tả trong tệp này? Anh ấy tìm kiếm nó trong các tiêu đề. Nếu tiêu đề nói rằng hàm nằm trong một tệp .c khác, thì nó chỉ để lại một con trỏ tới tệp khác này ở vị trí này.

Tại thời điểm này, trình xây dựng đã biên dịch tất cả các tệp .c của dự án thành các tệp .o. Chúng được gọi là các mô-đun được biên dịch. Bây giờ có các kết nối giữa các tệp dưới dạng con trỏ tại những nơi mà các hàm “nước ngoài” được gọi - nhưng đây vẫn là một số tệp khác nhau.

3. Trình liên kết

Hầu hết mọi thứ đã sẵn sàng, bạn chỉ cần kiểm tra tất cả các kết nối giữa các tệp - đi qua main.o và thay thế các con trỏ tới các chức năng của người khác - các mô-đun được biên dịch. Nếu một số chức năng từ các thư viện không được sử dụng, nó sẽ không được biên dịch ở giai đoạn trước hoặc sẽ không được trình liên kết thay thế ở bất kỳ đâu (tùy thuộc vào phương thức hoạt động của trình biên dịch mã). Trong mọi trường hợp, nó sẽ không được đưa vào mã nhị phân đã hoàn thành.

Trình liên kết cũng có thể thực hiện một số hành động cuối cùng trên tệp nhị phân, chẳng hạn như tính tổng kiểm tra của nó.

Dự án đầu tiên đang làm việc với các cổng I/O

3. Tạo dự án mới trong IAR

Sau khi khởi chạy IAR, một cửa sổ trung tâm thông tin xuất hiện mà chúng tôi không cần. Nhấp vào menu Dự án -> Tạo dự án mới. Chọn chuỗi công cụ: ARM (có thể bạn sẽ không có bất kỳ thứ gì khác trong danh sách đó), Mẫu dự án: C -> main.

Bây giờ bạn có một dự án C trống mới và tệp main.c.

4. Kết nối thư viện với dự án

Trong cửa sổ bên trái (“Không gian làm việc”), nhấp chuột phải vào menu và tạo một nhóm mới (Thêm -> Thêm nhóm), hãy gọi nó là CMSIS. Hãy tạo các nhóm StdPeriphLib, Startup và User theo cách tương tự. Bây giờ chúng ta thêm file vào nhóm (mình sẽ gạch chân tất cả các file để dễ theo dõi hơn).

Nhấp chuột phải vào CMSIS, Thêm, Thêm tệp - đi tới Thư viện/CMSIS/CM3, từ thư mục DeviceSupport/ST/STM32F10x (hỗ trợ chip) lấy system_stm32f10x.c (đây là mô tả về ngoại vi của cài đặt tinh thể và đồng hồ cụ thể). Trong thư mục CoreSupport (hỗ trợ kernel) cũng có core_cm3.c (đây là mô tả về lõi Cortex M3), nhưng chúng tôi sẽ không lấy nó - vì nó đã có trong trình biên dịch. Tôi sẽ viết thêm về điều này.

Tới nhóm Khởi động thêm tệp startup_stm32f10x_md_vl.s từ thư mục Libraries/CMSIS/CM3/DeviceSupport/ST/STM32F10x/startup/iar. Đây là những hành động cần được thực hiện khi khởi động. Hầu như hoàn toàn đây là về việc thiết lập các trình xử lý ngắt khác nhau (bản thân các trình xử lý sẽ ở xa hơn một chút). Ngoài ra còn có các tệp dành cho các tinh thể khác, nhưng chúng tôi quan tâm đến md_vl - điều này có nghĩa là mật độ trung bình (dung lượng bộ nhớ trung bình, cũng có những tinh thể có dung lượng nhỏ và lớn), dòng giá trị (dòng đánh giá - tinh thể STM32F100 chỉ nhằm mục đích đánh giá khả năng và chuyển sang các họ sau).

Chúng ta đã hoàn tất CMSIS.

Tới nhóm StdPeriphLib thêm các tệp stm32f10x_rcc.c và stm32f10x_gpio.c từ thư mục Libraries/STM32F10x_StdPeriph_Driver/src. Đầu tiên là chức năng làm việc với hệ thống đồng hồ và thứ hai là làm việc với các chân I/O.

Tới nhóm Người dùng kéo main.c của chúng tôi. Điều này không cần thiết nhưng nó đẹp hơn.

Cây dự án GPIO bây giờ trông như thế này:

Không gian làm việc đã sẵn sàng, chúng tôi sẽ không thêm bất cứ thứ gì vào đó nữa.

Tất cả những gì còn lại là đặt một tệp khác vào thư mục dự án để kết nối các tiêu đề với tất cả các tệp thư viện ngoại vi. Bạn có thể tự viết nó, nhưng việc lấy một cái làm sẵn sẽ dễ dàng hơn. Chúng tôi truy cập stsw-stm32078.zip/an3268/stm32vldiscovery_package/Project/Examples/GPIOToggle - ở đó chúng tôi lấy tệp stm32f10x_conf.h (cấu hình dự án) và đặt nó vào thư mục “1. GPIO". Đây là tập tin làm sẵn duy nhất mà chúng tôi lấy.

stm32f10x_conf.h chỉ là một tập hợp bao gồm các mô-đun cần thiết và các hàm xác nhận. Hàm này sẽ được gọi khi có lỗi khi làm việc với các hàm thư viện ngoại vi: ví dụ như bỏ một ít rác vào hàm GPIO_WriteBit thay vì GPIOC - tóm lại là ST đã chơi an toàn. Trong hàm này, bạn chỉ cần bắt đầu một vòng lặp vô hạn - while(1); Chúng ta vẫn cần vào stm32f10x_conf.h - để nhận xét các dòng bao gồm các tệp của các thiết bị ngoại vi không cần thiết, chỉ để lại stm32f10x_rcc.h, stm32f10x_gpio.h và misc.h - để chúng ta có thể tự viết nó.

5. Thiết lập dự án

Nhấp chuột phải vào tên dự án trong cửa sổ Workspace:

  1. Tùy chọn chung -> Mục tiêu -> Biến thể bộ xử lý: chọn “Thiết bị”, nhấn nút bên phải
Chọn ST -> STM32F100 -> ST STM32F100xB. Đây là bộ điều khiển của chúng tôi. 2. Tùy chọn chung -> Cấu hình thư viện -> CMSIS: chọn hộp kiểm Sử dụng CMSIS. Vì vậy chúng ta sẽ sử dụng thư viện CMSIS được tích hợp sẵn trong trình biên dịch. Kể từ phiên bản 6.30, IAR bắt đầu được cung cấp CMSIS tích hợp và điều này có vẻ tốt hơn - nhưng nó gây ra một số nhầm lẫn với các dự án cũ hơn. 3. Trình biên dịch C/C++ -> Bộ tiền xử lý. Ở đây chúng tôi viết đường dẫn đến các thư mục thư viện:
$PROJ_DIR$\
$PROJ_DIR$\..\Libraries\CMSIS\CM3\DeviceSupport\ST\STM32F10x
$PROJ_DIR$\..\Libraries\STM32F10x_StdPeriph_Driver\inc
Macro $PROJ_DIR$ có nghĩa là thư mục hiện tại(thư mục dự án) và.. - di chuyển lên một cấp cao hơn. Chúng tôi đã chỉ định các đường dẫn đến thư mục có mô tả về tinh thể, cũng như các tệp tiêu đề của thư viện ngoại vi, vì tất cả các tệp .c trong dự án đều bao gồm các tiêu đề của chúng và trình biên dịch phải biết nơi để tìm chúng. Tại đây, bạn cũng cần viết USE\_STDPERIPH\_DRIVER trong các ký hiệu được xác định. Điều này sẽ kết nối tập tin cần thiết cấu hình (ví dụ: stm32f10x_conf.h đã đề cập) cho dự án. Vì vậy, tab Preprocessor sẽ trông như thế này: * Debugger –> Setup –> Driver: chọn ST–Link, vì đây là bộ lập trình được tích hợp sẵn trong bảng Discovery. Bây giờ chúng ta tự cấu hình bộ lập trình: * Trình gỡ lỗi -> ST–LINK -> Giao diện: chọn SWD (bộ lập trình trên bo mạch được kết nối với bộ điều khiển qua SWD, không qua JTAG). * Trình gỡ lỗi -> Tải xuống: chọn hộp Sử dụng (các) bộ tải flash, “Tải chương trình cơ sở vào bộ nhớ flash.” Điều đó hợp lý, nếu không có nó sẽ không có gì ngập lụt.## 6. Viết code Đầu tiên mình sẽ viết code này sẽ làm gì. Anh ấy sẽ chứng minh điêu đơn giản, đèn LED nhấp nháy (PC8 trên bảng Discovery) kèm theo khoảng dừng trong vòng lặp vô tận. Đang kết nối tập tin tiêu đề cấu hình dự án, stm32f10x\_conf.h. Trong đó, chúng tôi tìm thấy dòng #include “stm32f10x\_exti.h” - đây là dòng 35 và nhận xét nó bằng hai dấu gạch chéo. Thực tế là dự án của chúng tôi sẽ không cần mô-đun EXTI. Tệp main.c đã có hàm int main và hành động duy nhất trong đó là trả về 0. Chúng tôi xóa dòng này (chúng tôi sẽ không trả về bất kỳ giá trị nào), thay đổi loại hàm thành void (vì lý do tương tự), và viết một vòng lặp vô hạn:
#include "stm32f10x_conf.h" 

khoảng trống chính()
{
trong khi(1)
{
}
}

### Khởi chạy mô-đun GPIO Các cổng đầu vào/đầu ra trong STM32 được gọi là GPIO - Đầu vào/Đầu ra mục đích chung. Đó là lý do tại sao chúng tôi đưa vào thư viện stm32f10x_gpio.c. Tuy nhiên, đây không phải là tất cả những gì chúng ta cần, một lý thuyết nhỏ: Tất cả các thiết bị ngoại vi trên chip đều bị tắt theo mặc định, cả từ nguồn và từ tần số đồng hồ. Để bật nó lên, bạn cần gửi tín hiệu đồng hồ. Điều này được quản lý bởi mô-đun RCC và có một tệp stm32f10x_rcc.c để làm việc với nó. Mô-đun GPIO bị treo trên bus APB2. Ngoài ra còn có AHB (một dạng tương tự của bus bộ xử lý-bắc cầu) và APB1 (cũng như APB2 - một dạng tương tự của bus cầu bắc-cầu nam). Vì vậy, điều đầu tiên chúng ta cần làm là kích hoạt xung nhịp mô-đun GPIOC. Đây là mô-đun chịu trách nhiệm về PORTC; còn có GPIOA, GPIOB, v.v. Việc này được thực hiện như sau: RCC\_APB2PeriphClockCmd(RCC\_APB2Periph_GPIOC, ENABLE); Thật đơn giản - chúng tôi gọi chức năng gửi tín hiệu đồng hồ từ bus APB2 đến mô-đun GPIOC và từ đó bật mô-đun này. Tất nhiên, chúng tôi làm điều này ngay từ đầu. hàm rỗng chủ yếu. Đây chỉ là những điều cơ bản bạn cần hiểu. Tôi cũng có một [bài viết chi tiết về mô-đun GPIO](/index.php/stm32-%e2%86%92-%d0%bf%d0%be%d1%80%d1%82%d1%8b- gpio / "STM32 → cổng GPIO"). ### Cấu hình mô-đun GPIOC Còn lại rất ít, bạn cần cấu hình mô-đun GPIOC. Chúng tôi cài đặt chân đầu ra (cũng có chức năng đầu vào và thay thế), điều chỉnh độ sắc nét của mặt trước (vì mục đích tương thích EM) và trình điều khiển đầu ra (kéo đẩy hoặc nguồn mở). Chúng tôi thực hiện việc này ngay sau khi khởi tạo cổng. GPIO\_InitTypeDef GPIO\_InitStructure; GPIO\_InitStructure.GPIO\_Speed ​​​= GPIO\_Speed\_2MHz; GPIO\_InitStructure.GPIO\_Mode = GPIO\_Mode\_Out_PP; GPIO\_InitStructure.GPIO\_Pin = GPIO\_Pin\_8; GPIO\_Init(GPIOC, &GPIO\_InitCấu trúc); Thôi vậy đó, sau này chân PC8 sẽ hoạt động như một đầu ra kéo đẩy với các cạnh tương đối nhẵn ( tần số tối đa chuyển mạch 2 MHz. Các cạnh sắc nét là 50 MHz). Chúng ta sẽ không nhận thấy độ mịn của mặt trước bằng mắt nhưng có thể nhìn thấy nó trên máy hiện sóng. ### Bật đèn LED Gọi hàm GPIO\_WriteBit(GPIOC, GPIO\_Pin\_8, Bit\_SET); Đèn LED sẽ bật. ### Bật và tắt nó trong một vòng lặp Trong vòng lặp while(1), chúng ta viết mã để bật, tạm dừng, tắt và tạm dừng lại:

GPIO_WriteBit(GPIOC, GPIO_Pin_8, Bit_SET);  vì(i=0; tôi<1000000; i++);

GPIO_WriteBit(GPIOC, GPIO_Pin_8, Bit_RESET);
vì(i=0; tôi<1000000; i++);

Do đó, toàn bộ tệp main.c trông như thế này:

#include "stm32f10x_conf.h"

khoảng trống chính()
{
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOC, ENABLE);

GPIO_InitTypeDef GPIO_InitStructure;
GPIO_InitStructure.GPIO_Speed ​​​​= GPIO_Speed_2 MHz;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP;
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_8;
GPIO_Init(GPIOC, &GPIO_InitCấu trúc);

GPIO_WriteBit(GPIOC, GPIO_Pin_8, Bit_SET);

int tôi;
trong khi(1)
{
GPIO_WriteBit(GPIOC, GPIO_Pin_8, Bit_SET);
vì(i=0; tôi<1000000; i++);

GPIO_WriteBit(GPIOC, GPIO_Pin_8, Bit_RESET); vì(i=0; tôi<1000000; i++); } }

## 7. Xuất phát nào! Chúng ta kết nối bo mạch STM32VLDiscovery với máy tính qua microUSB, nhấp vào nút Tải xuống và Gỡ lỗi trong IAR. Chương trình được tải lên bộ vi điều khiển (bạn sẽ thấy một cửa sổ có thanh tiến trình đóng nhanh - kích thước của chương trình quá nhỏ) và quá trình gỡ lỗi bắt đầu. IAR dừng ở lệnh đầu tiên của mã (điều này khá thuận tiện khi gỡ lỗi), bạn cần khởi động nó bằng nút Bắt đầu. Mọi thứ sẽ hoạt động - đèn LED PC8 màu xanh lam trên bo mạch STM32VLDiscovery Như thường lệ, bạn có thể tải xuống kho lưu trữ với dự án GPIO. May mắn thay, bạn có thể lưu dự án này và sử dụng nó làm mẫu để không phải thực hiện lại toàn bộ quá trình thiết lập. Toàn bộ chu trình: 1. Cổng I/O (/index.php/stm32-from_zero_to_rtos-2_timers/ "STM32 - từ 0 đến RTOS. 2: Hẹn giờ và ngắt") (/index.php/stm32-from_zero_to_rtos-3_timer_outputs/ " STM32 - từ 0 đến RTOS. 3: Đầu ra hẹn giờ")

](/index.php/stm32-from_zero_to_rtos-4_exti_nvic/ “STM32 - từ 0 đến RTOS. 4: Ngắt bên ngoài và NVIC”) 5. Cài đặt FreeRTOS

Chà, cho đến nay mọi thứ vẫn ổn, chỉ có bóng đèn và nút bấm là sẵn sàng. Bây giờ là lúc sử dụng các thiết bị ngoại vi nặng hơn - USB, UART, I2C và SPI. Tôi quyết định bắt đầu với USB - trình gỡ lỗi ST-Link (thậm chí cả trình gỡ lỗi thật của Discovery) kiên quyết từ chối gỡ lỗi bảng mạch của tôi, vì vậy việc gỡ lỗi trên các bản in qua USB là phương pháp gỡ lỗi duy nhất dành cho tôi. Tất nhiên, bạn có thể thông qua UART, nhưng đây là một loạt các dây bổ sung.

Tôi lại đi theo con đường dài - tôi đã tạo các khoảng trống tương ứng trong STM32CubeMX và thêm USB Middleware từ gói STM32F1Cube vào dự án của mình. Bạn chỉ cần kích hoạt đồng hồ USB, xác định các trình xử lý ngắt USB tương ứng và trau chuốt những điều nhỏ nhặt. Trong hầu hết các phần, tôi đã sao chép tất cả các cài đặt quan trọng của mô-đun USB từ STM32GENERIC, ngoại trừ việc tôi đã điều chỉnh một chút việc phân bổ bộ nhớ (họ sử dụng malloc và tôi sử dụng phân bổ tĩnh).

Dưới đây là một vài phần thú vị mà tôi đã chộp được. Ví dụ: để máy chủ (máy tính) hiểu được có thứ gì đó được kết nối với nó, thiết bị sẽ “làm biến dạng” đường dây USB D+ (được kết nối với chân A12). Sau khi nhìn thấy điều này, máy chủ bắt đầu thẩm vấn thiết bị xem nó là ai, nó có thể xử lý những giao diện nào, nó muốn giao tiếp ở tốc độ nào, v.v. Tôi thực sự không hiểu tại sao việc này cần phải được thực hiện trước khi khởi tạo USB, nhưng trong stm32duino, nó được thực hiện theo cách tương tự.

Giật USB

USBD_HandleTypeDef hUsbDeviceFS; void Reenumerate() ( // Khởi tạo pin PA12 GPIO_InitTypeDef pinInit; pinInit.Pin = GPIO_PIN_12; pinInit.Mode = GPIO_MODE_OUTPUT_PP; pinInit.Speed ​​​​= GPIO_SPEED_FREQ_LOW; HAL_GPIO_Init(GPIOA, &pinInit); // Báo cho máy chủ biết để liệt kê các thiết bị USB trên xe buýt HAL_GPIO_WritePin(GPIOA, GPIO_PIN_12, GPIO_PIN_RESET); for(unsigned int i=0; i<512; i++) {}; // Restore pin mode pinInit.Mode = GPIO_MODE_INPUT; pinInit.Pull = GPIO_NOPULL; HAL_GPIO_Init(GPIOA, &pinInit); for(unsigned int i=0; i<512; i++) {}; } void initUSB() { Reenumerate(); USBD_Init(&hUsbDeviceFS, &FS_Desc, DEVICE_FS); USBD_RegisterClass(&hUsbDeviceFS, &USBD_CDC); USBD_CDC_RegisterInterface(&hUsbDeviceFS, &USBD_Interface_fops_FS); USBD_Start(&hUsbDeviceFS); }


Một điểm thú vị khác là hỗ trợ bộ tải khởi động stm32duino. Để tải chương trình cơ sở lên, trước tiên bạn phải khởi động lại bộ điều khiển vào bộ nạp khởi động. Cách dễ nhất là nhấn nút đặt lại. Nhưng để làm điều này thuận tiện hơn, bạn có thể áp dụng kinh nghiệm về Arduino. Khi cây còn non, bộ điều khiển AVR chưa có hỗ trợ USB trên bo mạch; trên bo mạch đã có bộ chuyển đổi USB-UART. Tín hiệu DTR UART được kết nối với thiết lập lại vi điều khiển. Khi máy chủ gửi tín hiệu DTR, bộ vi điều khiển sẽ được khởi động lại vào bộ nạp khởi động. Hoạt động như bê tông cốt thép!

Trong trường hợp sử dụng USB thì chúng ta chỉ giả lập cổng COM. Theo đó, bạn cần phải tự khởi động lại vào bootloader. Bộ tải khởi động stm32duino, ngoài tín hiệu DTR, đề phòng, còn có một hằng số ma thuật đặc biệt (1EAF - tham chiếu đến Leaf Labs)

static int8_t CDC_Control_FS (uint8_t cmd, uint8_t* pbuf, uint16_t length) ( ... case CDC_SET_Control_LINE_STATE: dtr_pin++; //pin DTR được bật ngắt; ... static int8_t CDC_Receive_FS (uint8_t* Buf, uint32_t *Len) ( /* Four byte là gói ma thuật "1EAF" đưa MCU vào bộ nạp khởi động. */ if(*Len >= 4) ( /** * Kiểm tra xem đầu vào có chứa chuỗi "1EAF" không. * Nếu có, hãy kiểm tra xem DTR có đã được đặt, để đưa MCU vào chế độ bootloader. */ if(dtr_pin > 3) ( if((Buf == "1")&&(Buf == "E")&&(Buf == "A")&& (Buf == "F")) ( HAL_NVIC_SystemReset(); ) dtr_pin = 0; ) ) ... )

Trở lại: MiniArduino

Nói chung, USB đã hoạt động. Nhưng lớp này chỉ hoạt động với byte chứ không phải chuỗi. Đó là lý do tại sao các bản in gỡ lỗi trông rất xấu.

CDC_Transmit_FS((uint8_t*)"Ping\n", 5); // 5 là strlen(“Ping”) + 0 byte
Những thứ kia. Không có hỗ trợ nào cho đầu ra được định dạng - bạn không thể in một số hoặc tập hợp một chuỗi từ các mảnh. Các tùy chọn sau xuất hiện:

  • Vít trên printf cổ điển. Tùy chọn này có vẻ tốt, nhưng nó yêu cầu + 12kb phần sụn (bằng cách nào đó tôi vô tình gọi là sprintf)
  • Tìm hiểu cách triển khai printf của riêng bạn từ kho lưu trữ của bạn. Tôi đã từng viết cho AVR, có vẻ như việc triển khai này nhỏ hơn.
  • Đính kèm lớp Print từ Arduino vào phần triển khai STM32GENERIC
Tôi đã chọn tùy chọn thứ hai vì mã thư viện Adaf nhung GFX cũng dựa vào In, vì vậy tôi vẫn cần phải sửa nó. Ngoài ra, tôi đã có sẵn mã STM32GENERIC rồi.

Tôi đã tạo một thư mục MiniArduino trong dự án của mình với mục tiêu đặt số lượng mã yêu cầu tối thiểu vào đó để triển khai các phần giao diện arduino mà tôi cần. Tôi bắt đầu sao chép từng tệp một và xem xét những gì cần thiết cho các phần phụ thuộc khác. Vì vậy, tôi đã có được một bản sao của lớp Print và một số tệp ràng buộc.

Nhưng điều này là không đủ. Bằng cách nào đó vẫn cần kết nối lớp Print với các chức năng USB (ví dụ: CDC_Transmit_FS()). Để làm điều này, chúng tôi phải kéo lớp SerialUSB vào. Nó kéo theo lớp Stream và một phần khởi tạo GPIO. Bước tiếp theo là kết nối UART (Tôi có GPS được kết nối với nó). Vì vậy, tôi cũng đưa vào lớp SerialUART, lớp này kéo theo một lớp khởi tạo thiết bị ngoại vi khác từ STM32GENERIC.

Nói chung, tôi thấy mình trong tình huống sau. Tôi đã sao chép gần như tất cả các tệp từ STM32GENERIC sang MiniArduino của mình. Tôi cũng có bản sao thư viện USB và FreeRTOS của riêng mình (lẽ ra tôi cũng phải có bản sao của HAL và CMSIS, nhưng tôi quá lười). Đồng thời, tôi đã đánh dấu thời gian được một tháng rưỡi - kết nối và ngắt kết nối các phần khác nhau, nhưng đồng thời tôi vẫn chưa viết một dòng mã mới nào.

Rõ ràng là ý tưởng ban đầu của tôi về việc kiểm soát toàn bộ phần hệ thống đã không thành công lắm. Dù sao, một phần của mã khởi tạo nằm trong STM32GENERIC và có vẻ thoải mái hơn ở đó. Tất nhiên, tôi có thể cắt bỏ tất cả các phần phụ thuộc và viết các lớp trình bao bọc của riêng mình cho các nhiệm vụ của mình, nhưng điều này sẽ làm tôi chậm lại trong một tháng nữa - mã này vẫn cần được gỡ lỗi. Tất nhiên, điều này sẽ rất tốt cho tình huống khẩn cấp của bạn, nhưng bạn cần phải tiến về phía trước!

Vì vậy, tôi đã loại bỏ tất cả các thư viện trùng lặp và gần như toàn bộ lớp hệ thống của mình và quay lại STM32GENERIC. Dự án này đang phát triển khá năng động - có nhiều cam kết mỗi ngày. Ngoài ra, trong suốt một tháng rưỡi này, tôi đã nghiên cứu rất nhiều, đọc hầu hết Tài liệu tham khảo STM32, xem cách tạo ra các thư viện HAL và trình bao bọc STM32GENERIC cũng như nâng cao hiểu biết về bộ mô tả USB và thiết bị ngoại vi của bộ vi điều khiển. Nhìn chung, giờ đây tôi đã tự tin hơn nhiều vào STM32GENERIC so với trước đây.

Đảo ngược: I2C

Tuy nhiên, cuộc phiêu lưu của tôi không kết thúc ở đó. Vẫn còn UART và I2C (màn hình của tôi vẫn ở đó). Với UART mọi thứ khá đơn giản. Tôi vừa loại bỏ việc phân bổ bộ nhớ động và để các UART không sử dụng sẽ không ngốn hết bộ nhớ này, tôi chỉ cần nhận xét chúng.

Nhưng việc triển khai I2C trong STM32GENERIC có một chút vấn đề. Một điều rất thú vị ở đó, nhưng tôi phải mất ít nhất 2 buổi tối. Chà, hoặc đã dành 2 buổi tối để gỡ lỗi khó khăn cho các bản in - đó là cách bạn nhìn nhận nó.

Nói chung, việc triển khai màn hình chưa bắt đầu. Theo phong cách vốn đã truyền thống, nó không hoạt động và thế là xong. Những gì không hoạt động là không rõ ràng. Thư viện của màn hình (Adafruit SSD1306) dường như đã được thử nghiệm trong lần triển khai trước đó, nhưng vẫn không thể loại trừ các lỗi gây nhiễu. Sự nghi ngờ đổ dồn vào HAL và việc triển khai I2C từ STM32GENERIC.

Để bắt đầu, tôi đã nhận xét tất cả mã hiển thị và mã I2C, đồng thời viết phần khởi tạo I2C mà không cần bất kỳ thư viện nào, ở dạng HAL thuần túy

Khởi tạo I2C

GPIO_InitTypeDef GPIO_InitStruct; GPIO_InitStruct.Pin = GPIO_PIN_6|GPIO_PIN_7; GPIO_InitStruct.Mode = GPIO_MODE_AF_OD; GPIO_InitStruct.Pull = GPIO_PULLUP; GPIO_InitStruct.Speed ​​​​= GPIO_SPEED_HIGH; HAL_GPIO_Init(GPIOB, &GPIO_InitStruct); __I2C1_CLK_ENABLE(); hi2c1.Instance = I2C1; hi2c1.Init.ClockSpeed ​​​​= 400000; hi2c1.Init.DutyCycle = I2C_DUTYCYCLE_2; hi2c1.Init.OwnAddress1 = 0; hi2c1.Init.AddressingMode = I2C_ADDRESSINGMODE_7BIT; hi2c1.Init.DualAddressMode = I2C_DUALADDRESS_DISABLED; hi2c1.Init.OwnAddress2 = 0; hi2c1.Init.GeneralCallMode = I2C_GENERALCALL_DISABLED; hi2c1.Init.NoStretchMode = I2C_NOSTRETCH_DISABLED; HAL_I2C_Init(&hi2c1);


Tôi đã kết xuất trạng thái của các thanh ghi ngay sau khi khởi tạo. Tôi đã tạo kết xuất tương tự trong phiên bản đang hoạt động trên stm32duino. Đây là những gì tôi nhận được (có nhận xét cho chính mình)

Tốt (Stm32duino):

40005404: 0 0 1 24 - I2C_CR2: Đã kích hoạt ngắt lỗi, 36Mhz
40005408: 0 0 0 0 - I2C_OAR1: không có địa chỉ riêng

40005410: 0 0 0 AF - I2C_DR: thanh ghi dữ liệu

40005418: 0 0 0 0 - I2C_SR2: thanh ghi trạng thái

Xấu (STM32GENERIC):
40005400: 0 0 0 1 - I2C_CR1: Kích hoạt ngoại vi
40005404: 0 0 0 24 - I2C_CR2: 36Mhz
40005408: 0 0 40 0 ​​​​- I2C_OAR1: !!! Bit không được mô tả trong bộ thanh ghi địa chỉ
4000540C: 0 0 0 0 - I2C_OAR2: Thanh ghi địa chỉ riêng
40005410: 0 0 0 0 - I2C_DR: thanh ghi dữ liệu
40005414: 0 0 0 0 - I2C_SR1: thanh ghi trạng thái
40005418: 0 0 0 2 - I2C_SR2: đặt bit bận
4000541C: 0 0 80 1E - I2C_CCR: chế độ 400kHz
40005420: 0 0 0 B - I2C_TRISE

Sự khác biệt lớn đầu tiên là bit thứ 14 được đặt trong thanh ghi I2C_OAR1. Bit này hoàn toàn không được mô tả trong biểu dữ liệu và rơi vào phần dành riêng. Đúng, với lời cảnh báo là bạn vẫn cần phải viết một cái ở đó. Những thứ kia. Đây là một lỗi trong libmaple. Nhưng vì mọi thứ đều hoạt động ở đó nên đây không phải là vấn đề. Hãy đào sâu hơn nữa.

Một điểm khác biệt nữa là bit bận được thiết lập. Lúc đầu tôi không coi trọng anh ấy chút nào, nhưng nhìn về phía trước tôi sẽ nói rằng chính anh ấy là người báo hiệu vấn đề!.. Nhưng điều đầu tiên phải làm trước tiên.

Tôi đã lấy mã khởi tạo mà không cần bất kỳ thư viện nào.

Đang khởi tạo hiển thị

void sendCommand(I2C_HandleTypeDef * handler, uint8_t cmd) ( SerialUSB.print("Gửi lệnh "); SerialUSB.println(cmd, 16); uint8_t xBuffer; xBuffer = 0x00; xBuffer = cmd; HAL_I2C_Master_Transmit(xử lý, I2C1_DEVICE_ADDRESS<<1, xBuffer, 2, 10); } ... sendCommand(handle, SSD1306_DISPLAYOFF); sendCommand(handle, SSD1306_SETDISPLAYCLOCKDIV); // 0xD5 sendCommand(handle, 0x80); // the suggested ratio 0x80 sendCommand(handle, SSD1306_SETMULTIPLEX); // 0xA8 sendCommand(handle, 0x3F); sendCommand(handle, SSD1306_SETDISPLAYOFFSET); // 0xD3 sendCommand(handle, 0x0); // no offset sendCommand(handle, SSD1306_SETSTARTLINE | 0x0); // line #0 sendCommand(handle, SSD1306_CHARGEPUMP); // 0x8D sendCommand(handle, 0x14); sendCommand(handle, SSD1306_MEMORYMODE); // 0x20 sendCommand(handle, 0x00); // 0x0 act like ks0108 sendCommand(handle, SSD1306_SEGREMAP | 0x1); sendCommand(handle, SSD1306_COMSCANDEC); sendCommand(handle, SSD1306_SETCOMPINS); // 0xDA sendCommand(handle, 0x12); sendCommand(handle, SSD1306_SETCONTRAST); // 0x81 sendCommand(handle, 0xCF); sendCommand(handle, SSD1306_SETPRECHARGE); // 0xd9 sendCommand(handle, 0xF1); sendCommand(handle, SSD1306_SETVCOMDETECT); // 0xDB sendCommand(handle, 0x40); sendCommand(handle, SSD1306_DISPLAYALLON_RESUME); // 0xA4 sendCommand(handle, SSD1306_DISPLAYON); // 0xA6 sendCommand(handle, SSD1306_NORMALDISPLAY); // 0xA6 sendCommand(handle, SSD1306_INVERTDISPLAY); sendCommand(handle, SSD1306_COLUMNADDR); sendCommand(handle, 0); // Column start address (0 = reset) sendCommand(handle, SSD1306_LCDWIDTH-1); // Column end address (127 = reset) sendCommand(handle, SSD1306_PAGEADDR); sendCommand(handle, 0); // Page start address (0 = reset) sendCommand(handle, 7); // Page end address uint8_t buf; buf = 0x40; for(uint8_t x=1; x<17; x++) buf[x] = 0xf0; // 4 black, 4 white lines for (uint16_t i=0; i<(SSD1306_LCDWIDTH*SSD1306_LCDHEIGHT/8); i++) { HAL_I2C_Master_Transmit(handle, I2C1_DEVICE_ADDRESS<<1, buf, 17, 10); }


Sau một số nỗ lực, mã này đã có tác dụng với tôi (trong trường hợp này, nó có hình sọc). Điều này có nghĩa là sự cố nằm ở lớp I2C của STM32GENERIC. Tôi bắt đầu loại bỏ dần mã của mình, thay thế nó bằng những phần thích hợp từ thư viện. Nhưng ngay sau khi tôi chuyển mã khởi tạo mã pin từ mã triển khai sang mã thư viện, toàn bộ quá trình truyền I2C bắt đầu hết thời gian chờ.

Sau đó tôi nhớ lại khoảng thời gian bận rộn và cố gắng hiểu khi nào nó xảy ra. Hóa ra cờ bận xuất hiện ngay khi mã khởi tạo bật xung nhịp I2c. Những thứ kia. Mô-đun bật và ngay lập tức không hoạt động. Hấp dẫn.

Chúng tôi rơi vào khởi tạo

uint8_t * pv = (uint8_t*)0x40005418; // Đăng ký I2C_SR2. Đang tìm cờ BẬN SerialUSB.print("40005418 = "); SerialUSB.println(*pv, 16); // In 0 __HAL_RCC_I2C1_CLK_ENABLE(); SerialUSB.print("40005418 = "); SerialUSB.println(*pv, 16); //In 2


Ở trên mã này chỉ là khởi tạo các chân. Chà, phải làm gì - che phần gỡ lỗi bằng các bản in trên dòng và ở đó

Đang khởi tạo các chân STM32GENERIC

void stm32AfInit(const stm32_af_pin_list_type list, int size, const void *instance, GPIO_TypeDef *port, uint32_t pin, chế độ uint32_t, uint32_t pull) ( ... GPIO_InitTypeDef GPIO_InitStruct; GPIO_InitStruct.Pin = pin; GPIO_InitStruct.Mode = chế độ; GP IO_InitStruct. Kéo = kéo ;GPIO_InitStruct.Speed ​​​​= GPIO_SPEED_FREQ_VERY_HIGH;HAL_GPIO_Init(port, &GPIO_InitStruct); ... )


Nhưng thật xui xẻo - GPIO_InitStruct được điền chính xác. Chỉ của tôi hoạt động, nhưng cái này thì không. Thật sự, thần bí!!! Mọi thứ đều theo sách giáo khoa, nhưng không có gì hiệu quả. Tôi nghiên cứu mã thư viện từng dòng một, tìm kiếm bất cứ điều gì đáng ngờ. Cuối cùng tôi đã tìm thấy mã này (nó gọi hàm trên)

Một phần khởi tạo khác

void stm32AfI2CInit(const I2C_TypeDef *instance, ...) ( stm32AfInit(chip_af_i2c_sda, ...); stm32AfInit(chip_af_i2c_scl, ...); )


Bạn có thấy một lỗi trong đó? Và cô ấy là! Tôi thậm chí còn loại bỏ các tham số không cần thiết để làm cho vấn đề rõ ràng hơn. Nói chung, sự khác biệt là mã của tôi khởi tạo cả hai chân cùng một lúc trong một cấu trúc và mã STM32GENERIC từng cái một. Rõ ràng mã khởi tạo mã pin bằng cách nào đó ảnh hưởng đến cấp độ trên mã pin này. Trước khi khởi tạo, không có gì xuất ra trên chân này và điện trở sẽ tăng mức lên một. Tại thời điểm khởi tạo, vì lý do nào đó bộ điều khiển đặt số 0 trên nhánh tương ứng.

Thực tế này tự nó là vô hại. Nhưng vấn đề là việc hạ đường SDA trong khi nâng đường SCL là điều kiện khởi động cho bus i2c. Vì điều này, bộ thu điều khiển trở nên điên cuồng, đặt cờ BẬN và bắt đầu chờ dữ liệu. Tôi quyết định không rút ruột thư viện để thêm khả năng khởi tạo nhiều chân cùng một lúc. Thay vào đó, tôi chỉ hoán đổi 2 dòng này - quá trình khởi tạo màn hình đã thành công. Bản sửa lỗi đã được áp dụng vào STM32GENERIC.

Nhân tiện, trong libmaple, việc khởi tạo bus được thực hiện một cách thú vị. Trước khi bắt đầu khởi tạo các thiết bị ngoại vi i2c trên bus, trước tiên bạn phải thiết lập lại. Để thực hiện việc này, thư viện sẽ chuyển các chân sang chế độ GPIO bình thường và lắc các chân này nhiều lần, mô phỏng trình tự bắt đầu và dừng. Điều này giúp hồi sinh các thiết bị bị mắc kẹt trên xe buýt. Thật không may, không có điều tương tự trong HAL. Đôi khi màn hình của tôi bị kẹt và giải pháp duy nhất là tắt nguồn.

Đang khởi tạo i2c từ stm32duino

/** * @brief Đặt lại bus I2C. * * Việc đặt lại được thực hiện bằng cách bấm giờ cho đến khi bất kỳ phần phụ bị treo nào * giải phóng SDA và SCL, sau đó tạo điều kiện BẮT ĐẦU, sau đó là điều kiện STOP *. * * @param dev Thiết bị I2C */ void i2c_bus_reset(const i2c_dev *dev) ( /* Giải phóng cả hai dòng */ i2c_master_release_bus(dev); /* * Đảm bảo xe buýt rảnh bằng cách bấm giờ cho đến khi bất kỳ nô lệ nào giải phóng xe buýt *. */ while (!gpio_read_bit(sda_port(dev), dev->sda_pin)) ( /* Đợi bất kỳ đồng hồ nào kéo dài xong */ while (!gpio_read_bit(scl_port(dev), dev->scl_pin)) ; delay_us(10 ); /* Kéo xuống mức thấp */ gpio_write_bit(scl_port(dev), dev->scl_pin, 0); delay_us(10); /* Thả lên mức cao trở lại */ gpio_write_bit(scl_port(dev), dev->scl_pin, 1); delay_us(10); ) /* Tạo điều kiện bắt đầu rồi dừng */ gpio_write_bit(sda_port(dev), dev->sda_pin, 0); delay_us(10); gpio_write_bit(scl_port(dev), dev->scl_pin, 0); delay_us(10); gpio_write_bit(scl_port(dev), dev->scl_pin, 1); delay_us(10); gpio_write_bit(sda_port(dev), dev->sda_pin, 1); )

Lại nữa: UART

Tôi rất vui vì cuối cùng đã quay lại lập trình và tiếp tục viết các tính năng. Phần quan trọng tiếp theo là kết nối thẻ SD qua SPI. Bản thân điều này đã là một hoạt động thú vị, thú vị và đau đớn. Tôi chắc chắn sẽ nói riêng về nó trong bài viết tiếp theo. Một trong những vấn đề là tải CPU cao (>50%). Điều này đặt ra câu hỏi về hiệu quả năng lượng của thiết bị. Và thật bất tiện khi sử dụng thiết bị, bởi vì... Giao diện người dùng cực kỳ ngu ngốc.

Hiểu được vấn đề, tôi đã tìm ra nguyên nhân của việc tiêu tốn tài nguyên này. Tất cả công việc với thẻ SD diễn ra theo từng byte, sử dụng bộ xử lý. Nếu cần ghi một khối dữ liệu vào thẻ thì với mỗi byte, hàm gửi byte được gọi

Với (uint16_t i = 0; tôi< 512; i++) { spiSend(src[i]);
Không, nó không nghiêm trọng! Có DMA! Đúng, thư viện SD (thư viện đi kèm với Arduino) rất vụng về và cần được thay đổi, nhưng vấn đề mang tính toàn cầu hơn. Hình ảnh tương tự được quan sát trong thư viện màn hình và thậm chí việc nghe UART cũng được thực hiện thông qua một cuộc thăm dò. Nói chung, tôi bắt đầu nghĩ rằng việc viết lại tất cả các thành phần trong HAL không phải là một ý tưởng ngu ngốc.

Tất nhiên, tôi đã bắt đầu với một thứ đơn giản hơn - trình điều khiển UART nghe luồng dữ liệu từ GPS. Giao diện Arduino không cho phép bạn gắn vào ngắt UART và lấy các ký tự đến một cách nhanh chóng. Kết quả là, cách duy nhất để có được dữ liệu là thông qua việc bỏ phiếu liên tục. Tất nhiên, tôi đã thêm vTaskDelay(10) vào bộ xử lý GPS để giảm tải ít nhất một chút, nhưng trên thực tế thì đây chỉ là một cái nạng.

Tất nhiên, ý nghĩ đầu tiên là gắn DMA. Nó thậm chí sẽ hoạt động nếu không có giao thức NMEA. Vấn đề là trong giao thức này, thông tin chỉ chuyển động đơn giản và các gói (dòng) riêng lẻ được phân tách bằng ký tự ngắt dòng. Hơn nữa, mỗi dòng có thể có độ dài khác nhau. Bởi vì điều này, người ta không biết trước cần nhận bao nhiêu dữ liệu. DMA không hoạt động như vậy - số byte phải được đặt trước khi khởi tạo quá trình truyền. Tóm lại, DMA không còn cần thiết nữa nên chúng tôi đang tìm giải pháp khác.

Nếu nhìn kỹ vào thiết kế của thư viện NeoGPS, bạn có thể thấy thư viện chấp nhận dữ liệu đầu vào theo từng byte, nhưng các giá trị chỉ được cập nhật khi toàn bộ dòng đã đến (chính xác hơn là một loạt nhiều dòng ). Cái đó. sẽ không có gì khác biệt nếu nạp từng byte thư viện khi chúng được nhận hay sau đó cấp tất cả cùng một lúc. Vì vậy, bạn có thể tiết kiệm thời gian của bộ xử lý bằng cách lưu dòng nhận được vào bộ đệm và bạn có thể thực hiện việc này trực tiếp trong ngắt. Khi toàn bộ dòng được nhận, quá trình xử lý có thể bắt đầu.

Thiết kế sau đây xuất hiện

Lớp trình điều khiển UART

// Kích thước của bộ đệm đầu vào UART const uint8_t gpsBufferSize = 128; // Lớp này xử lý giao diện UART nhận ký tự từ GPS và lưu chúng vào lớp đệm GPS_UART ( // Xử lý phần cứng UART UART_HandleTypeDef uartHandle; // Nhận bộ đệm vòng uint8_t rxBuffer; dễ bay hơi uint8_t LastReadIndex = 0; dễ bay hơi uint8_t LastReceivedIndex = 0; / / Xử lý luồng GPS TaskHandle_t xGPSThread = NULL;


Mặc dù việc khởi tạo được sao chép từ STM32GENERIC nhưng nó hoàn toàn tương ứng với những gì CubeMX cung cấp

Khởi tạo UART

void init() ( // Đặt lại con trỏ (phòng trường hợp ai đó gọi init() nhiều lần) LastReadIndex = 0; LastReceivedIndex = 0; // Khởi tạo bộ điều khiển luồng GPS xGPSThread = xTaskGetCurrentTaskHandle(); // Cho phép tính giờ của ngoại vi tương ứng __HAL_RCC_GPIOA_CLK_ENABLE( ); __HAL_RCC_USART1_CLK_ENABLE(); // Khởi tạo ghim ở chế độ chức năng thay thế GPIO_InitTypeDef GPIO_InitStruct; GPIO_InitStruct.Pin = GPIO_PIN_9; //TX ghim GPIO_InitStruct.Mode = GPIO_MODE_AF_PP; GPIO_InitStruct.Speed ​​​​= GPIO_SPEED_FREQ_ CAO; HAL_GPIO_Init(GPIOA , &GPIO_InitStruct); GPIO_InitStruct .Pin = GPIO_PIN_10; //Chân RX GPIO_InitStruct.Mode = GPIO_MODE_INPUT; GPIO_InitStruct.Pull = GPIO_NOPULL; HAL_GPIO_Init(GPIOA, &GPIO_InitStruct); // Khởi tạo uartHandle.Instance = USART1; uartHandle.Init.BaudRate = 9600; uartHandle.Init. Độ dài từ = UART_WORDLENGTH_8B; uartHandle.Init.StopBits = UART_STOPBITS_1; uartHandle.Init.Parity = UART_PARITY_NONE; uartHandle.Init.Mode = UART_MODE_TX_RX; uartHandle.Init.HwFlowCtl = UART_HWControl_NONE; uartHandle.Init.OverSampling = UART_OVERSAMPLING_16; HAL_UART_Init(&uartHandle); // Chúng ta sẽ sử dụng ngắt UART để lấy dữ liệu HAL_NVIC_SetPriority(USART1_IRQn, 6, 0); HAL_NVIC_EnableIRQ(USART1_IRQn); // Chúng ta sẽ đợi một quyền char duy nhất được nhận ngay vào bộ đệm HAL_UART_Receive_IT(&uartHandle, rxBuffer, 1); )


Trên thực tế, không thể khởi tạo chân TX, nhưng uartHandle.Init.Mode có thể được đặt thành UART_MODE_RX - chúng tôi sẽ chỉ nhận nó. Tuy nhiên, hãy để như vậy - điều gì sẽ xảy ra nếu tôi cần cấu hình mô-đun GPS bằng cách nào đó và viết lệnh cho nó.

Thiết kế của lớp này có thể trông đẹp hơn nếu không có những hạn chế của kiến ​​trúc HAL. Vì vậy, chúng ta không thể đơn giản thiết lập chế độ, họ nói, chấp nhận mọi thứ, gắn trực tiếp vào ngắt và lấy các byte nhận được trực tiếp từ thanh ghi nhận. Chúng ta cần cho HAL biết trước số lượng và vị trí chúng ta sẽ nhận byte - chính các trình xử lý tương ứng sẽ ghi các byte nhận được vào bộ đệm được cung cấp. Với mục đích này, ở dòng cuối cùng của hàm khởi tạo có lệnh gọi HAL_UART_Receive_IT(). Vì độ dài của chuỗi không được biết trước nên chúng ta phải lấy từng byte một.

Bạn cũng cần khai báo tối đa 2 lệnh gọi lại. Một là trình xử lý ngắt, nhưng công việc của nó chỉ là gọi trình xử lý từ HAL. Hàm thứ hai là “gọi lại” của HAL cho biết byte đã được nhận và nó đã có trong bộ đệm.

Lệnh gọi lại UART

// Chuyển tiếp quá trình xử lý ngắt UART sang HAL extern "C" void USART1_IRQHandler(void) ( HAL_UART_IRQHandler(gpsUart.getUartHandle()); ) // HAL gọi lại lệnh gọi lại này khi nhận được char từ UART. Chuyển tiếp nó tới lớp bên ngoài "C" void HAL_UART_RxCpltCallback(UART_HandleTypeDef *uartHandle) ( gpsUart.charReceivedCB(); )


Phương thức charReceuredCB() chuẩn bị HAL để nhận byte tiếp theo. Nó cũng là người xác định rằng dòng đã kết thúc và điều này có thể được báo hiệu cho chương trình chính. Semaphore ở chế độ tín hiệu có thể được sử dụng làm phương tiện đồng bộ hóa, nhưng với những mục đích đơn giản như vậy, nên sử dụng thông báo trực tiếp.

Xử lý một byte nhận được

// Đã nhận được Char, chuẩn bị cho nội tuyến tiếp theo void charReceivedCB() ( char LastReceivedChar = rxBuffer; LastReceivedIndex++; HAL_UART_Receive_IT(&uartHandle, rxBuffer + (lastReceivedIndex % gpsBufferSize), 1); // Nếu nhận được ký hiệu EOL, hãy thông báo cho chuỗi GPS đó có sẵn để đọc if(lastReceivedChar == "\n") vTaskNotifyGiveFromISR(xGPSThread, NULL); )


Hàm phản hồi (chờ) là waitForString(). Nhiệm vụ của nó chỉ đơn giản là treo trên đối tượng đồng bộ hóa và chờ (hoặc thoát khi hết thời gian chờ)

Chờ đợi cuối dòng

// Đợi cho đến khi nhận được toàn bộ dòng bool waitForString() ( return ulTaskNotifyTake(pdTRUE, 10); )


Nó hoạt động như thế này. Chuỗi chịu trách nhiệm về GPS thường ngủ trong hàm waitForString(). Các byte đến từ GPS được bộ xử lý ngắt thêm vào bộ đệm. Nếu ký tự \n (cuối dòng) xuất hiện, thì ngắt sẽ đánh thức luồng chính, luồng này bắt đầu đổ byte từ bộ đệm vào trình phân tích cú pháp. Chà, khi trình phân tích cú pháp xử lý xong gói tin nhắn, nó sẽ cập nhật dữ liệu trong mô hình GPS.

luồng GPS

void vGPSTask(void *pvParameters) ( // Việc khởi tạo GPS phải được thực hiện trong luồng GPS vì trình xử lý luồng được lưu trữ // và được sử dụng sau này cho mục đích đồng bộ hóa gpsUart.init(); for (;;) ( // Đợi cho đến khi toàn bộ chuỗi được hoàn thành đã nhận if(!gpsUart.waitForString()) continue; // Đọc chuỗi đã nhận và phân tích char luồng GPS bằng char while(gpsUart.available()) ( int c = gpsUart.readChar(); //SerialUSB.write(c) ; gpsParser.handle(c); ) if(gpsParser.available()) ( GPSDataModel::instance().processNewGPSFix(gpsParser.read()); GPSDataModel::instance().processNewSatellitesData(gpsParser.satellites, gpsParser.sat_count ); ) vTaskDelay(10); ) )


Tôi đã gặp một khoảnh khắc rất không hề tầm thường mà tôi đã bị mắc kẹt trong vài ngày. Có vẻ như mã đồng bộ hóa được lấy từ các ví dụ, nhưng lúc đầu nó không hoạt động - nó đã làm hỏng toàn bộ hệ thống. Tôi nghĩ rằng sự cố nằm ở thông báo trực tiếp (chức năng xTaskNotifyXXX), tôi đã thay đổi nó thành các ẩn dụ thông thường nhưng ứng dụng vẫn bị lỗi.

Hóa ra bạn cần phải rất cẩn thận với mức độ ưu tiên ngắt. Theo mặc định, tôi đặt tất cả các ngắt ở mức ưu tiên 0 (mức cao nhất). Nhưng FreeRTOS có yêu cầu là mức độ ưu tiên phải nằm trong một phạm vi nhất định. Các ngắt có mức ưu tiên quá cao không thể gọi các hàm FreeRTOS. Chỉ các ngắt có cấu hình ưu tiênLIBRARY_MAX_SYSCALL_INTERRUPT_PRIORITY trở xuống mới có thể gọi các hàm hệ thống (một lời giải thích hay và ). Cài đặt mặc định này được đặt thành 5. Tôi đã thay đổi mức ưu tiên ngắt UART thành 6 và mọi thứ đều hoạt động.

Lại nữa: I2C qua DMA

Bây giờ bạn có thể làm điều gì đó phức tạp hơn, chẳng hạn như trình điều khiển màn hình. Nhưng ở đây chúng ta cần thực hiện một chuyến tham quan lý thuyết về bus I2C. Bản thân bus này không điều chỉnh giao thức truyền dữ liệu trên bus - bạn có thể ghi byte hoặc đọc chúng. Bạn thậm chí có thể viết và sau đó đọc trong một giao dịch (ví dụ: viết một địa chỉ và sau đó đọc dữ liệu tại địa chỉ này).

Tuy nhiên, hầu hết các thiết bị đều xác định giao thức cấp cao hơn theo cách giống nhau. thiết bị cung cấp cho người dùng một bộ thanh ghi, mỗi thanh ghi có địa chỉ riêng. Hơn nữa, trong giao thức truyền thông, (hoặc một số) byte đầu tiên trong mỗi giao dịch sẽ xác định địa chỉ của ô (thanh ghi) mà chúng ta sẽ đọc hoặc ghi thêm vào đó. Trong trường hợp này, cũng có thể trao đổi nhiều byte theo kiểu “bây giờ chúng tôi sẽ ghi/đọc nhiều byte bắt đầu từ địa chỉ này”. Tùy chọn cuối cùng là tốt cho DMA.

Thật không may, màn hình dựa trên bộ điều khiển SSD1306 cung cấp một giao thức - lệnh hoàn toàn khác. Byte đầu tiên của mỗi giao dịch là thuộc tính “lệnh hoặc dữ liệu”. Trong trường hợp lệnh, byte thứ hai là mã lệnh. Nếu một lệnh cần đối số, chúng sẽ được chuyển thành các lệnh riêng biệt theo sau lệnh đầu tiên. Để khởi tạo màn hình, bạn cần gửi khoảng 30 lệnh, nhưng chúng không thể gộp thành một mảng và gửi thành một khối. Bạn cần phải gửi chúng cùng một lúc.

Nhưng khi gửi một mảng pixel (bộ đệm khung), hoàn toàn có thể sử dụng dịch vụ DMA. Đây là những gì chúng tôi sẽ cố gắng.

Nhưng thư viện Adafruit_SSD1306 được viết rất vụng về và không thể dễ dàng sử dụng nó. Rõ ràng thư viện lần đầu tiên được viết để giao tiếp với màn hình thông qua SPI. Sau đó, ai đó đã thêm hỗ trợ I2C, nhưng hỗ trợ SPI vẫn được bật. Sau đó, ai đó bắt đầu thêm tất cả các loại tối ưu hóa cấp thấp và ẩn chúng sau ifdef. Kết quả là nó trở thành một mớ mã hỗ trợ các giao diện khác nhau. Vì vậy, trước khi đi xa hơn, cần phải dọn dẹp nó.

Lúc đầu, tôi cố gắng sắp xếp thứ tự này bằng cách đóng khung mã cho các giao diện khác nhau bằng ifdefs. Nhưng nếu tôi muốn viết mã giao tiếp với màn hình, sử dụng DMA và đồng bộ hóa qua FreeRTOS thì tôi sẽ không thể làm được gì nhiều. Nó sẽ chính xác hơn, nhưng mã này sẽ cần phải được viết trực tiếp vào mã thư viện. Vì vậy, tôi quyết định làm lại thư viện một lần nữa, tạo giao diện và đặt mỗi trình điều khiển vào một lớp riêng. Mã trở nên sạch hơn và có thể dễ dàng thêm hỗ trợ cho trình điều khiển mới mà không cần thay đổi thư viện.

Giao diện trình điều khiển hiển thị

// Giao diện cho trình điều khiển phần cứng // Adafruit_SSD1306 không hoạt động trực tiếp với phần cứng // Tất cả các yêu cầu liên lạc được chuyển tiếp đến lớp trình điều khiển ISSD1306Driver ( public: virtual voidbegin() = 0; virtual void sendCommand(uint8_t cmd) = 0 ; virtual void sendData(uint8_t * data, size_t size) = 0; );


Vì vậy, hãy đi thôi. Tôi đã hiển thị quá trình khởi tạo I2C. Không có gì thay đổi ở đó. Nhưng việc gửi lệnh trở nên dễ dàng hơn một chút. Bạn có nhớ khi tôi nói về sự khác biệt giữa giao thức đăng ký và giao thức lệnh cho thiết bị I2C không? Và mặc dù màn hình thực hiện giao thức lệnh nhưng nó có thể được mô phỏng khá tốt bằng giao thức đăng ký. Bạn chỉ cần tưởng tượng rằng màn hình chỉ có 2 thanh ghi - 0x00 cho lệnh và 0x40 cho dữ liệu. Và HAL thậm chí còn cung cấp chức năng cho kiểu chuyển tiền này

Gửi lệnh tới màn hình

void DisplayDriver::sendCommand(uint8_t cmd) ( HAL_I2C_Mem_Write(&handle, i2c_addr, 0x00, 1, &cmd, 1, 10); )


Lúc đầu, việc gửi dữ liệu không rõ ràng lắm. Mã gốc gửi dữ liệu theo gói nhỏ 16 byte

Mã gửi dữ liệu lạ

cho (uint16_t i=0; tôi


Tôi đã thử thử nghiệm với kích thước gói và gửi các gói lớn hơn, nhưng may mắn nhất thì màn hình của tôi bị nhàu nát. Chà, hoặc mọi thứ đã bị treo.

Hiển thị bị cắt



Lý do hóa ra rất tầm thường - tràn bộ đệm. Lớp Wire từ Arduino (ít nhất là STM32GENERIC) cung cấp bộ đệm riêng chỉ 32 byte. Nhưng tại sao chúng ta lại cần một bộ đệm bổ sung nếu lớp Adafruit_SSD1306 đã có sẵn? Hơn nữa, với HAL, việc gửi được thực hiện trên một dòng

Truyền dữ liệu chính xác

void DisplayDriver::sendData(uint8_t * data, size_t size) ( HAL_I2C_Mem_Write(&handle, i2c_addr, 0x40, 1, data, size, 10); )


Như vậy, một nửa cuộc chiến đã hoàn thành - chúng tôi đã viết trình điều khiển cho màn hình ở dạng HAL thuần túy. Nhưng trong phiên bản này, nó vẫn đòi hỏi nhiều tài nguyên - 12% bộ xử lý cho màn hình 128x32 và 23% cho màn hình 128x64. Việc sử dụng DMA thực sự được hoan nghênh ở đây.

Đầu tiên, hãy khởi tạo DMA. Chúng tôi muốn triển khai chuyển tiếp dữ liệu trong I2C số 1 và chức năng này tồn tại trên kênh DMA thứ sáu. Khởi tạo sao chép từng byte từ bộ nhớ sang thiết bị ngoại vi

Thiết lập DMA cho I2C

// kích hoạt đồng hồ bộ điều khiển DMA __HAL_RCC_DMA1_CLK_ENABLE(); // Khởi tạo DMA hdma_tx.Instance = DMA1_Channel6; hdma_tx.Init.Direction = DMA_MEMORY_TO_PERIPH; hdma_tx.Init.PeriphInc = DMA_PINC_DISABLE; hdma_tx.Init.MemInc = DMA_MINC_ENABLE; hdma_tx.Init.PeriphDataAlignment = DMA_PDATAALIGN_BYTE; hdma_tx.Init.MemDataAlignment = DMA_MDATAALIGN_BYTE; hdma_tx.Init.Mode = DMA_NORMAL; hdma_tx.Init.Priority = DMA_PRIORITY_LOW; HAL_DMA_Init(&hdma_tx); // Liên kết bộ xử lý DMA đã khởi tạo với bộ xử lý I2C __HAL_LINKDMA(&handle, hdmatx, hdma_tx); /* Khởi tạo ngắt DMA */ /* Cấu hình ngắt DMA1_Channel6_IRQn */ HAL_NVIC_SetPriority(DMA1_Channel6_IRQn, 7, 0); HAL_NVIC_EnableIRQ(DMA1_Channel6_IRQn);


Ngắt là một phần bắt buộc của thiết kế. Nếu không, hàm HAL_I2C_Mem_Write_DMA() sẽ bắt đầu giao dịch I2C nhưng không ai hoàn thành giao dịch đó. Một lần nữa, chúng ta đang giải quyết vấn đề thiết kế HAL cồng kềnh và nhu cầu có tới hai lệnh gọi lại. Mọi thứ hoàn toàn giống với UART. Một hàm là trình xử lý ngắt - chúng tôi chỉ chuyển hướng cuộc gọi đến HAL. Chức năng thứ hai là tín hiệu cho biết dữ liệu đã được gửi.

Trình xử lý ngắt DMA

extern "C" void DMA1_Channel6_IRQHandler(void) ( HAL_DMA_IRQHandler(displayDriver.getDMAHandle()); ) extern "C" void HAL_I2C_MemTxCpltCallback(I2C_HandleTypeDef *hi2c) ( displayDriver.transferCompletedCB(); )


Tất nhiên, chúng tôi sẽ không liên tục thăm dò I2C để xem quá trình chuyển tiền đã kết thúc chưa? Thay vào đó, bạn cần ngủ trên đối tượng đồng bộ hóa và đợi cho đến khi quá trình truyền hoàn tất

Truyền dữ liệu qua DMA với đồng bộ hóa

void DisplayDriver::sendData(uint8_t * data, size_t size) ( // Bắt đầu truyền dữ liệu HAL_I2C_Mem_Write_DMA(&handle, i2c_addr, 0x40, 1, data, size); // Đợi cho đến khi quá trình truyền hoàn tất ulTaskNotifyTake(pdTRUE, 100); ) void DisplayDriver::transferCompletedCB() ( // Tiếp tục hiển thị chuỗi vTaskNotifyGiveFromISR(xDisplayThread, NULL); )


Quá trình truyền dữ liệu vẫn mất 24 ms - đây gần như là thời gian truyền thuần túy 1 kB (kích thước bộ đệm hiển thị) ở tần số 400 kHz. Chỉ trong trường hợp này, hầu hết thời gian bộ xử lý chỉ ở chế độ ngủ (hoặc làm những việc khác). Tải CPU tổng thể giảm từ 23% xuống chỉ còn 1,5-2%. Tôi nghĩ con số này đáng để đấu tranh!

Lại nữa: SPI qua DMA

Việc kết nối thẻ SD qua SPI theo một nghĩa nào đó dễ dàng hơn - vào thời điểm này tôi đã bắt đầu cài đặt thư viện sdfat và ở đó những người tốt đã tách giao tiếp với thẻ thành một giao diện trình điều khiển riêng. Đúng, với sự trợ giúp của định nghĩa, bạn chỉ có thể chọn một trong 4 phiên bản trình điều khiển được tạo sẵn, nhưng điều này có thể dễ dàng bị lãng phí và được thay thế bằng cách triển khai của riêng bạn.

Giao diện trình điều khiển SPI để làm việc với thẻ SD

// Đây là cách triển khai tùy chỉnh của lớp Trình điều khiển SPI. Thư viện SdFat // sử dụng lớp này để truy cập thẻ SD qua SPI // // Mục đích chính của việc triển khai này là thúc đẩy truyền dữ liệu // qua DMA và đồng bộ hóa với các khả năng của FreeRTOS. lớp SdFatSPIDriver: public SdSpiBaseDriver ( // mô-đun SPI SPI_HandleTypeDef spiHandle; // Xử lý luồng GPS TaskHandle_t xSDThread = NULL; public: SdFatSPIDriver(); virtual void activate(); virtual void started(uint8_t chipSelectPin); virtual void deactive(); virtual void uint8_t nhận(); uint8_t ảo nhận(uint8_t* buf, size_t n); gửi void ảo(dữ liệu uint8_t); gửi void ảo(const uint8_t* buf, size_t n); void ảo select(); void void setSpiSettings(SPISettings spiSettings ); bỏ chọn khoảng trống ảo(); );


Như trước đây, chúng tôi bắt đầu với một điều gì đó đơn giản - với việc triển khai bằng gỗ sồi mà không cần bất kỳ DMA nào. Quá trình khởi tạo được CubeMX tạo một phần và được hợp nhất một phần với việc triển khai SPI của STM32GENERIC

Khởi tạo SPI

SdFatSPIDriver::SdFatSPIDriver() ( ) //void SdFatSPIDriver::activate(); void SdFatSPIDriver::begin(uint8_t chipSelectPin) ( // Bỏ qua chân CS đã qua - Trình điều khiển này hoạt động với một (void)chipSelectPin được xác định trước; // Khởi tạo trình xử lý luồng GPS xSDThread = xTaskGetCurrentTaskHandle(); // Cho phép bấm giờ của ngoại vi tương ứng __HAL_RCC_GPIOA_CLK_ENABLE() ; __HAL_RCC_SPI1_CLK_ENABLE(); // Khởi tạo ghim GPIO_InitTypeDef GPIO_InitStruct; GPIO_InitStruct.Pin = GPIO_PIN_5|GPIO_PIN_7; //MOSI & SCK GPIO_InitStruct.Mode = GPIO_MODE_AF_PP; GPIO_InitStruct.Speed ​​​​= GPIO_SPE ED_FREQ_HIGH;HAL_GPIO _Init(GPIOA, &GPIO_InitStruct);GPIO_InitStruct.Pin = GPIO_PIN_6; //MISO GPIO_InitStruct.Mode = GPIO_MODE_INPUT; GPIO_InitStruct.Pull = GPIO_NOPULL; HAL_GPIO_Init(GPIOA, &GPIO_InitStruct); GPIO_InitStruct.Pin = GPIO_PIN_4; //CS GPIO_InitStruct.Mode = GPIO_MODE_OUTPUT_PP; GP IO_InitS truct.Speed ​​​​= GPIO_SPEED_FREQ_HIGH;HAL_GPIO_Init (GPIOA, &GPIO_InitStruct); // Đặt chân CS ở mức Cao theo mặc định HAL_GPIO_WritePin(GPIOA, GPIO_PIN_4, GPIO_PIN_SET); // Khởi tạo SPI spiHandle.Instance = SPI1; spiHandle.Init.Mode = SPI_MODE_MASTER; spiHandle.Init.Direction = SPI_DIRECTION_2LINES; spiHandle.Init.DataSize = SPI_DATASIZE_8BIT; spiHandle.Init.CLKPolarity = SPI_POLARITY_LOW; spiHandle.Init.CLKPhase = SPI_PHASE_1EDGE; spiHandle.Init.NSS = SPI_NSS_SOFT; spiHandle.Init.BaudRatePrescaler = SPI_BAUDRATEPRESCALER_256; spiHandle.Init.FirstBit = SPI_FIRSTBIT_MSB; spiHandle.Init.TIMode = SPI_TIMODE_DISABLE; spiHandle.Init.CRCCalculation = SPI_CRCCALCULATION_DISABLE; spiHandle.Init.CRCPolynomial = 10; HAL_SPI_Init(&spiHandle); __HAL_SPI_ENABLE(&spiHandle); )


Thiết kế giao diện được thiết kế riêng cho Arduino với các chân được đánh số bằng một số. Trong trường hợp của tôi, không có ích gì khi thiết lập chân CS thông qua các tham số - Tôi có tín hiệu này được gắn chặt với chân A4, nhưng cần phải tuân thủ giao diện.

Theo thiết kế của thư viện SdFat, tốc độ của cổng SPI được điều chỉnh trước mỗi giao dịch. Những thứ kia. về mặt lý thuyết, bạn có thể bắt đầu giao tiếp với thẻ ở tốc độ thấp, sau đó tăng tốc độ lên. Nhưng tôi đã từ bỏ việc này và điều chỉnh tốc độ một lần trong phương thức start(). Vì vậy, các phương thức kích hoạt/hủy kích hoạt hóa ra trống rỗng. Tương tự như setSpiSettings()

Trình xử lý giao dịch tầm thường

void SdFatSPIDriver::activate() ( // Không cần kích hoạt đặc biệt ) void SdFatSPIDriver::deactivate() ( // Không cần hủy kích hoạt đặc biệt ) void SdFatSPIDriver::setSpiSettings(const SPISettings & spiSettings) ( // Bỏ qua cài đặt - chúng tôi đang sử dụng cài đặt tương tự cho tất cả các lần chuyển)


Các phương pháp điều khiển tín hiệu CS khá tầm thường

Kiểm soát tín hiệu CS

void SdFatSPIDriver::select() ( HAL_GPIO_WritePin(GPIOA, GPIO_PIN_4, GPIO_PIN_RESET); ) void SdFatSPIDriver::unselect() ( HAL_GPIO_WritePin(GPIOA, GPIO_PIN_4, GPIO_PIN_SET); )


Hãy chuyển sang phần thú vị nhất - đọc và viết. Việc triển khai bằng gỗ sồi đầu tiên không có DMA

Truyền dữ liệu không cần DMA

uint8_t SdFatSPIDriver::receive() ( uint8_t buf; uint8_t dummy = 0xff; HAL_SPI_TransmitReceive(&spiHandle, &dummy, &buf, 1, 10); return buf; ) uint8_t SdFatSPIDriver::receive(uint8_t* buf, size_t n) ( // TODO : Nhận qua DMA tại đây memset(buf, 0xff, n); HAL_SPI_Receive(&spiHandle, buf, n, 10); return 0; ) void SdFatSPIDriver::send(uint8_t data) ( HAL_SPI_Transmit(&spiHandle, &data, 1, 10); ) void SdFatSPIDriver::send(const uint8_t* buf, size_t n) ( // TODO: Truyền qua DMA tại đây HAL_SPI_Transmit(&spiHandle, (uint8_t*)buf, n, 10); )


Trong giao diện SPI, việc nhận và truyền dữ liệu diễn ra đồng thời. Để nhận được một cái gì đó bạn cần phải gửi một cái gì đó. Thông thường HAL thực hiện việc này cho chúng ta - chúng ta chỉ cần gọi hàm HAL_SPI_Receive() và nó tổ chức cả việc gửi và nhận. Nhưng trên thực tế, hàm này gửi rác nằm trong bộ đệm nhận.
Để bán thứ không cần thiết, trước tiên bạn phải mua thứ không cần thiết (C) Prostokvashino

Nhưng có một sắc thái. Thẻ SD rất thất thường. Họ không thích bị trao bất cứ thứ gì trong khi thẻ đang gửi dữ liệu. Do đó, tôi đã phải sử dụng hàm HAL_SPI_TransmitReceive() và gửi 0xffs một cách cưỡng bức trong khi nhận dữ liệu.

Hãy lấy số đo. Để một luồng ghi 1kb dữ liệu vào thẻ theo vòng lặp.

Mã kiểm tra để gửi luồng dữ liệu tới thẻ SD

uint8_t sd_buf; uint16_t i=0; uint32_t trước = HAL_GetTick(); while(true) ( ​​​bulkFile.write(sd_buf, 512); BulkFile.write(sd_buf, 512); i++; uint32_t cur = HAL_GetTick(); if(cur-prev >= 1000) ( prev = cur; usbDebugWrite( "Đã lưu %d kb\n", i); i = 0; ) )


Với phương pháp này, có thể ghi được khoảng 15-16kb mỗi giây. Không nhiều. Nhưng hóa ra tôi đã đặt bộ đếm gộp trước thành 256. Tức là vậy. Đồng hồ SPI được đặt ở mức thấp hơn nhiều so với thông lượng có thể. Về mặt thử nghiệm, tôi phát hiện ra rằng việc đặt tần số cao hơn 9 MHz (bộ chia tỷ lệ trước được đặt thành 8) là vô nghĩa - không thể đạt được tốc độ ghi cao hơn 100-110 kb/s (nhân tiện, trên một ổ đĩa flash khác). , vì lý do nào đó mà chỉ có thể ghi ở tốc độ 50-60 kb/s, còn ngày thứ ba thì thường chỉ là 40kb/s). Rõ ràng mọi thứ phụ thuộc vào thời gian chờ của ổ đĩa flash.

Về nguyên tắc, điều này là quá đủ, nhưng chúng tôi sẽ bơm dữ liệu qua DMA. Chúng tôi tiến hành theo sơ đồ đã quen thuộc. Trước hết, khởi tạo. Chúng tôi nhận và truyền qua SPI trên kênh DMA thứ hai và thứ ba tương ứng.

Khởi tạo DMA

// kích hoạt đồng hồ bộ điều khiển DMA __HAL_RCC_DMA1_CLK_ENABLE(); // Rx kênh DMA dmaHandleRx.Instance = DMA1_Channel2; dmaHandleRx.Init.Direction = DMA_PERIPH_TO_MEMORY; dmaHandleRx.Init.PeriphInc = DMA_PINC_DISABLE; dmaHandleRx.Init.MemInc = DMA_MINC_ENABLE; dmaHandleRx.Init.PeriphDataAlignment = DMA_PDATAALIGN_BYTE; dmaHandleRx.Init.MemDataAlignment = DMA_MDATAALIGN_BYTE; dmaHandleRx.Init.Mode = DMA_NORMAL; dmaHandleRx.Init.Priority = DMA_PRIORITY_LOW; HAL_DMA_Init(&dmaHandleRx); __HAL_LINKDMA(&spiHandle, hdmarx, dmaHandleRx); // Tx kênh DMA dmaHandleTx.Instance = DMA1_Channel3; dmaHandleTx.Init.Direction = DMA_MEMORY_TO_PERIPH; dmaHandleTx.Init.PeriphInc = DMA_PINC_DISABLE; dmaHandleTx.Init.MemInc = DMA_MINC_ENABLE; dmaHandleTx.Init.PeriphDataAlignment = DMA_PDATAALIGN_BYTE; dmaHandleTx.Init.MemDataAlignment = DMA_MDATAALIGN_BYTE; dmaHandleTx.Init.Mode = DMA_NORMAL; dmaHandleTx.Init.Priority = DMA_PRIORITY_LOW; HAL_DMA_Init(&dmaHandleTx); __HAL_LINKDMA(&spiHandle, hdmatx, dmaHandleTx);


Đừng quên kích hoạt ngắt. Đối với tôi, họ sẽ ưu tiên 8 - thấp hơn một chút so với UART và I2C

Định cấu hình ngắt DMA

// Thiết lập ngắt DMA HAL_NVIC_SetPriority(DMA1_Channel2_IRQn, 8, 0); HAL_NVIC_EnableIRQ(DMA1_Channel2_IRQn); HAL_NVIC_SetPriority(DMA1_Channel3_IRQn, 8, 0); HAL_NVIC_EnableIRQ(DMA1_Channel3_IRQn);


Tôi quyết định rằng chi phí chạy DMA và đồng bộ hóa cho các lần truyền ngắn có thể vượt quá lợi ích, vì vậy đối với các gói nhỏ (tối đa 16 byte), tôi đã bỏ tùy chọn cũ. Các gói dài hơn 16 byte được gửi qua DMA. Phương pháp đồng bộ hóa hoàn toàn giống như trong phần trước.

Chuyển tiếp dữ liệu qua DMA

const size_t DMA_TRESHOLD = 16; uint8_t SdFatSPIDriver::receive(uint8_t* buf, size_t n) ( memset(buf, 0xff, n); // Không sử dụng DMA cho các lần truyền ngắn if(n<= DMA_TRESHOLD) { return HAL_SPI_TransmitReceive(&spiHandle, buf, buf, n, 10); } // Start data transfer HAL_SPI_TrsnsmitReceive_DMA(&spiHandle, buf, buf, n); // Wait until transfer is completed ulTaskNotifyTake(pdTRUE, 100); return 0; // Ok status } void SdFatSPIDriver::send(const uint8_t* buf, size_t n) { // Not using DMA for short transfers if(n <= DMA_TRESHOLD) { HAL_SPI_Transmit(&spiHandle, buf, n, 10); return; } // Start data transfer HAL_SPI_Transmit_DMA(&spiHandle, (uint8_t*)buf, n); // Wait until transfer is completed ulTaskNotifyTake(pdTRUE, 100); } void SdFatSPIDriver::dmaTransferCompletedCB() { // Resume SD thread vTaskNotifyGiveFromISR(xSDThread, NULL); }


Tất nhiên, không có cách nào mà không bị gián đoạn. Mọi thứ ở đây đều giống như trong trường hợp của I2C

ngắt DMA

spiDriver SdFatSPIDriver bên ngoài; extern "C" void DMA1_Channel2_IRQHandler(void) ( HAL_DMA_IRQHandler(spiDriver.getHandle().hdmarx); ) extern "C" void DMA1_Channel3_IRQHandler(void) ( HAL_DMA_IRQHandler(spiDriver.getHandle().hdmatx); ) extern "C" void HAL_SPI_Tx CpltCallback (SPI_HandleTypeDef *hspi) ( spiDriver.dmaTransferCompletedCB(); ) extern "C" void HAL_SPI_RxCpltCallback(SPI_HandleTypeDef *hspi) ( spiDriver.dmaTransferCompletedCB(); )


Hãy khởi động và kiểm tra. Để không làm khổ ổ đĩa flash, tôi quyết định gỡ lỗi bằng cách đọc một tệp lớn chứ không phải bằng cách ghi. Ở đây tôi phát hiện ra một điểm rất thú vị: tốc độ đọc ở phiên bản không phải DMA là khoảng 250-260 kb/s, trong khi với DMA chỉ là 5!!! Hơn nữa, mức tiêu thụ CPU khi không sử dụng DMA là 3% và với DMA - 75-80%!!! Những thứ kia. kết quả hoàn toàn ngược lại với những gì được mong đợi.

Ngoại lệ khoảng 3%

Ở đây, tôi gặp một trục trặc buồn cười khi đo tải bộ xử lý - đôi khi chức năng này cho biết bộ xử lý chỉ được tải 3%, mặc dù tỷ lệ phần trăm lẽ ra phải đập liên tục mà không dừng lại. Trên thực tế, tải là 100% và chức năng đo lường của tôi hoàn toàn không được gọi - nó có mức ưu tiên thấp nhất và đơn giản là không có đủ thời gian cho nó. Do đó, tôi đã nhận được giá trị được ghi nhớ cuối cùng trước khi bắt đầu thực thi. Trong điều kiện bình thường chức năng hoạt động chính xác hơn.


Sau khi ghi mã trình điều khiển gần như từng dòng, tôi phát hiện ra một vấn đề: Tôi đã sử dụng sai chức năng gọi lại. Ban đầu, mã của tôi sử dụng HAL_SPI_Receive_DMA() và cùng với đó, lệnh gọi lại HAL_SPI_RxCpltCallback đã được sử dụng. Thiết kế này không hoạt động do sắc thái gửi 0xff đồng thời. Khi tôi thay đổi HAL_SPI_Receive_DMA() thành HAL_SPI_TransmitReceive_DMA(), tôi cũng phải thay đổi lệnh gọi lại thành HAL_SPI_TxRxCpltCallback(). Những thứ kia. trên thực tế, quá trình đọc đã diễn ra, nhưng do thiếu lệnh gọi lại nên tốc độ được điều chỉnh theo thời gian chờ là 100ms.

Sau khi sửa lỗi gọi lại, mọi thứ đã đâu vào đấy. Tải bộ xử lý giảm xuống 2,5% (hiện đã trung thực) và tốc độ thậm chí còn tăng lên 500kb/s. Đúng, bộ đếm gộp trước phải được đặt thành 4 - với bộ đếm gộp trước thành 2, các xác nhận sẽ tràn vào thư viện SdFat. Có vẻ như đây là giới hạn tốc độ của thẻ của tôi.

Thật không may, điều này không liên quan gì đến tốc độ ghi. Tốc độ ghi vẫn khoảng 50-60kb/s và tải bộ xử lý dao động trong khoảng 60-70%. Nhưng sau khi tìm hiểu suốt buổi tối và thực hiện các phép đo ở nhiều nơi khác nhau, tôi phát hiện ra rằng hàm send() của chính trình điều khiển của tôi (ghi một cung từ 512 byte) chỉ mất 1-2 mili giây, bao gồm cả thời gian chờ và đồng bộ hóa. Tuy nhiên, đôi khi xảy ra hiện tượng hết thời gian chờ và quá trình ghi kéo dài 5-7 mili giây. Nhưng vấn đề thực sự không nằm ở trình điều khiển mà ở logic làm việc với hệ thống tệp FAT.

Chuyển sang cấp độ tệp, phân vùng và cụm, nhiệm vụ ghi 512 vào tệp không quá tầm thường. Bạn cần đọc bảng FAT, tìm một vị trí trong đó để ghi cung, viết chính cung đó, cập nhật các mục trong bảng FAT, ghi các cung này vào đĩa, cập nhật các mục trong bảng tệp và thư mục, và một loạt những thứ khác. Nói chung, một lệnh gọi tới FatFile::write() có thể mất tới 15-20 mili giây và phần lớn thời gian này được thực hiện bởi công việc thực tế của bộ xử lý để xử lý các bản ghi trong hệ thống tệp.

Như tôi đã lưu ý, tải bộ xử lý khi ghi là 60-70%. Nhưng con số này cũng phụ thuộc vào loại hệ thống tệp (Fat16 hoặc Fat32), kích thước và theo đó, số lượng cụm này trên phân vùng, tốc độ của ổ đĩa flash, mức độ đông đúc và phân mảnh của phương tiện, việc sử dụng tên tập tin dài, và nhiều hơn nữa. Vì vậy, tôi yêu cầu bạn coi những phép đo này như một số loại số liệu tương đối.

Lại nữa: USB có bộ đệm đôi

Nó trở nên thú vị với thành phần này. Việc triển khai USB Serial từ STM32GENERIC ban đầu có một số thiếu sót và tôi quyết định tự viết lại nó. Nhưng trong khi tôi đang nghiên cứu cách hoạt động của USB CDC, đọc mã nguồn và nghiên cứu tài liệu, những người ở STM32GENERIC đã cải thiện đáng kể việc triển khai của họ. Nhưng điều đầu tiên trước tiên.

Vì vậy, việc triển khai ban đầu không phù hợp với tôi vì những lý do sau:

  • Tin nhắn được gửi đồng bộ. Những thứ kia. việc truyền dữ liệu theo từng byte thông thường từ GPS UART sang USB chờ từng byte riêng lẻ được gửi. Vì điều này mà tải của bộ xử lý có thể đạt tới 30-50%, tất nhiên là rất nhiều (tốc độ UART chỉ 9600)
  • Không có sự đồng bộ hóa. Khi in tin nhắn từ nhiều luồng, đầu ra là một chuỗi các tin nhắn ghi đè lên nhau một phần
  • Quá nhiều bộ đệm nhận và gửi. Một số bộ đệm được khai báo trong USB Middleware nhưng thực tế không được sử dụng. Một vài bộ đệm nữa được khai báo trong lớp SerialUSB, nhưng vì tôi chỉ sử dụng đầu ra nên bộ đệm nhận chỉ gây lãng phí bộ nhớ.
  • Cuối cùng, tôi chỉ thấy khó chịu với giao diện của lớp Print. Ví dụ: nếu tôi muốn hiển thị chuỗi “tốc độ hiện tại XXX km/h”, thì tôi cần thực hiện tối đa 3 cuộc gọi - cho phần đầu tiên của chuỗi, cho số và cho phần còn lại của chuỗi. Cá nhân tôi có tinh thần gần gũi hơn với bản in cổ điển. Các luồng bổ sung cũng được, nhưng bạn cần xem loại mã nào được trình biên dịch tạo ra.
Bây giờ, hãy bắt đầu với một điều đơn giản - gửi tin nhắn đồng bộ mà không cần đồng bộ hóa và định dạng. Trên thực tế, tôi đã sao chép mã từ STM32GENERIC một cách trung thực.

Triển khai “trực diện”

USBD_HandleTypeDef hUsbDeviceFS bên ngoài; void usbDebugWrite(uint8_t c) ( usbDebugWrite(&c, 1); ) void usbDebugWrite(const char * str) ( usbDebugWrite((const uint8_t *)str, strlen(str)); ) void usbDebugWrite(const uint8_t *buffer, size_t size ) ( // Bỏ qua việc gửi tin nhắn nếu USB không được kết nối if(hUsbDeviceFS.dev_state != USBD_STATE_CONFIGURED) return; // Truyền tin nhắn nhưng không quá thời gian chờ uint32_t timeout = HAL_GetTick() + 5; while(HAL_GetTick()< timeout) { if(CDC_Transmit_FS((uint8_t*)buffer, size) == USBD_OK) { return; } } }


Về mặt hình thức, đây không phải là mã đồng bộ, bởi vì nó không chờ dữ liệu được gửi. Nhưng chức năng này đợi cho đến khi dữ liệu trước đó được gửi đi. Những thứ kia. cuộc gọi đầu tiên sẽ gửi dữ liệu đến cổng và thoát ra, nhưng cuộc gọi thứ hai sẽ đợi cho đến khi dữ liệu được gửi trong cuộc gọi đầu tiên thực sự được gửi. Trong trường hợp hết thời gian chờ, dữ liệu sẽ bị mất. Ngoài ra, không có gì xảy ra nếu không có kết nối USB nào cả.

Tất nhiên, đây chỉ là sự chuẩn bị, bởi vì... việc thực hiện này không giải quyết được các vấn đề đã được xác định. Cần làm gì để làm cho mã này không đồng bộ và không bị chặn? Vâng, ít nhất là một bộ đệm. Nhưng khi nào cần chuyển bộ đệm này?

Tôi nghĩ rằng đáng để thực hiện một chuyến tham quan ngắn về nguyên tắc hoạt động của USB. Thực tế là chỉ có máy chủ mới có thể bắt đầu truyền trong giao thức USB. Nếu một thiết bị cần truyền dữ liệu đến máy chủ, dữ liệu sẽ được chuẩn bị trong bộ đệm PMA (Vùng bộ nhớ gói) đặc biệt và thiết bị sẽ đợi máy chủ nhận dữ liệu này. Hàm CDC_Transmit_FS() chuẩn bị bộ đệm PMA. Bộ đệm này nằm bên trong thiết bị ngoại vi USB chứ không nằm trong mã người dùng.

Thực lòng tôi muốn vẽ một bức tranh đẹp ở đây, nhưng tôi không biết làm cách nào để thể hiện nó một cách tốt nhất.

Nhưng sẽ thật tuyệt nếu thực hiện sơ đồ sau. Mã máy khách ghi dữ liệu vào bộ đệm lưu trữ (người dùng) nếu cần. Thỉnh thoảng chủ nhà đến và lấy đi mọi thứ đã tích lũy trong bộ đệm tại thời điểm đó. Điều này rất giống với những gì tôi đã mô tả trong đoạn trước, nhưng có một lưu ý quan trọng: dữ liệu nằm trong bộ đệm người dùng chứ không phải trong PMA. Những thứ kia. Tôi muốn thực hiện mà không cần gọi CDC_Transmit_FS(), dịch vụ này sẽ chuyển dữ liệu từ bộ đệm người dùng sang PMA và thay vào đó nhận lệnh gọi lại “ở đây máy chủ đã đến, yêu cầu dữ liệu”.

Thật không may, phương pháp này không thể thực hiện được trong thiết kế hiện tại của USB CDC Middleware. Chính xác hơn, điều đó có thể thực hiện được, nhưng bạn cần phải dấn thân vào việc triển khai trình điều khiển CDC. Tôi chưa đủ kinh nghiệm về giao thức USB để thực hiện việc này. Ngoài ra, tôi không chắc giới hạn thời gian của USB có đủ cho thao tác như vậy không.

May mắn thay, vào lúc đó tôi nhận thấy rằng STM32GENERIC đã đi vòng quanh một thứ như vậy. Đây là đoạn mã mà tôi đã sáng tạo lại từ chúng.

Bộ đệm đôi nối tiếp USB

#define USB_SERIAL_BUFFER_SIZE 256 uint8_t usbTxBuffer; dễ bay hơi uint16_t usbTxHead = 0; dễ bay hơi uint16_t usbTxTail = 0; dễ bay hơi uint16_t usbTransmit = 0; uint16_t transferContiguousBuffer() ( uint16_t count = 0; // Truyền dữ liệu liền kề đến cuối bộ đệm if (usbTxHead > usbTxTail) ( count = usbTxHead - usbTxTail; ) else ( count = sizeof(usbTxBuffer) - usbTxTail; ) CDC_Transmit_FS (&usbTxBuffer, count); return count; ) void usbDebugWriteInternal(const char *buffer, size_t size, bool Reverse = false) ( // Bỏ qua việc gửi tin nhắn nếu USB không được kết nối if(hUsbDeviceFS.dev_state != USBD_STATE_CONFIGURED) return; / / Truyền tin nhắn nhưng không quá timeout uint32_t timeout = HAL_GetTick() + 5; // Bảo vệ chức năng này khỏi nhiều lối vào MutexLocker lock(usbMutex); // Sao chép dữ liệu vào bộ đệm for(size_t i=0; i< size; i++) { if(reverse) --buffer; usbTxBuffer = *buffer; usbTxHead = (usbTxHead + 1) % sizeof(usbTxBuffer); if(!reverse) buffer++; // Wait until there is a room in the buffer, or drop on timeout while(usbTxHead == usbTxTail && HAL_GetTick() < timeout); if (usbTxHead == usbTxTail) break; } // If there is no transmittion happening if (usbTransmitting == 0) { usbTransmitting = transmitContiguousBuffer(); } } extern "C" void USBSerialTransferCompletedCB() { usbTxTail = (usbTxTail + usbTransmitting) % sizeof(usbTxBuffer); if (usbTxHead != usbTxTail) { usbTransmitting = transmitContiguousBuffer(); } else { usbTransmitting = 0; } }


Ý tưởng đằng sau mã này như sau. Mặc dù không thể bắt được thông báo “máy chủ đã đến và muốn có dữ liệu”, nhưng hóa ra vẫn có thể tổ chức cuộc gọi lại “Tôi đã gửi dữ liệu cho máy chủ, bạn có thể đổ dữ liệu tiếp theo”. Nó hóa ra là một loại bộ đệm đôi - trong khi thiết bị đang chờ dữ liệu được gửi từ bộ đệm PMA bên trong, mã người dùng có thể thêm byte vào bộ đệm lưu trữ. Khi quá trình gửi dữ liệu hoàn tất, bộ đệm lưu trữ sẽ được chuyển đến PMA. Tất cả những gì còn lại là tổ chức cuộc gọi lại này. Để thực hiện việc này, bạn cần điều chỉnh một chút chức năng USBD_CDC_DataIn()

Phần mềm trung gian USB đã nộp

uint8_t tĩnh USBD_CDC_DataIn (USBD_HandleTypeDef *pdev, uint8_t epnum) ( USBD_CDC_HandleTypeDef *hcdc = (USBD_CDC_HandleTypeDef*) pdev->pClassData; if(pdev->pClassData != NULL) ( hcdc->TxState = 0; USBSerialTransferCompleted CB();trả về USBD_OK ; ) khác ( return USBD_FAIL; ) )


Nhân tiện, hàm usbDebugWrite được bảo vệ bởi một mutex và sẽ hoạt động chính xác từ nhiều luồng. Tôi đã không bảo vệ hàm USBSerialTransferCompletedCB() - nó được gọi từ một ngắt và hoạt động trên các biến dễ thay đổi. Thành thật mà nói, có một lỗi ở đâu đó ở đây, các biểu tượng thỉnh thoảng bị nuốt chửng. Nhưng đối với tôi điều này không quan trọng để gỡ lỗi. Điều này sẽ không được gọi trong mã "sản xuất".

Ở đó một lần nữa: printf

Cho đến nay điều này chỉ có thể hoạt động với các chuỗi không đổi. Đã đến lúc thắt chặt tính chất tương tự printf(). Tôi không muốn sử dụng hàm printf() thực sự - nó đòi hỏi 12 kilobyte mã bổ sung và một “đống” mà tôi không có. Cuối cùng tôi đã tìm thấy trình ghi nhật ký gỡ lỗi mà tôi đã từng viết cho AVR. Việc triển khai của tôi có thể in chuỗi cũng như số ở định dạng thập phân và thập lục phân. Sau khi hoàn thiện và thử nghiệm thì kết quả như thế này:

Thực hiện printf đơn giản hóa

// việc triển khai sprintf mất hơn 10kb và thêm heap vào dự án. Tôi nghĩ rằng điều này // quá nhiều so với chức năng tôi cần // // Dưới đây là hàm kết xuất giống homebrew printf chấp nhận: // - %d cho các chữ số // - %x cho các số dưới dạng HEX // - %s cho chuỗi // - %% cho ký hiệu phần trăm // // Việc triển khai cũng hỗ trợ giá trị chiều rộng cũng như phần đệm bằng 0 // In số vào bộ đệm (theo thứ tự ngược lại) // Trả về số ký hiệu được in size_t PrintNum(unsigned int value , uint8_t cơ số, char * buf, uint8_t width, char padSymbol) ( //TODO kiểm tra số âm ở đây size_t len ​​​​= 0; // In số do ( char signature = value % cơ số; *(buf++) = chữ số< 10 ? "0" + digit: "A" - 10 + digit; value /= radix; len++; } while (value >0); //Thêm phần đệm bằng 0 while(len< width) { *(buf++) = padSymbol; len++; } return len; } void usbDebugWrite(const char * fmt, ...) { va_list v; va_start(v, fmt); const char * chunkStart = fmt; size_t chunkSize = 0; char ch; do { // Get the next byte ch = *(fmt++); // Just copy the regular characters if(ch != "%") { chunkSize++; continue; } // We hit a special symbol. Dump string that we processed so far if(chunkSize) usbDebugWriteInternal(chunkStart, chunkSize); // Process special symbols // Check if zero padding requested char padSymbol = " "; ch = *(fmt++); if(ch == "0") { padSymbol = "0"; ch = *(fmt++); } // Check if width specified uint8_t width = 0; if(ch >"0" && ch<= "9") { width = ch - "0"; ch = *(fmt++); } // check the format switch(ch) { case "d": case "u": { char buf; size_t len = PrintNum(va_arg(v, int), 10, buf, width, padSymbol); usbDebugWriteInternal(buf + len, len, true); break; } case "x": case "X": { char buf; size_t len = PrintNum(va_arg(v, int), 16, buf, width, padSymbol); usbDebugWriteInternal(buf + len, len, true); break; } case "s": { char * str = va_arg(v, char*); usbDebugWriteInternal(str, strlen(str)); break; } case "%": { usbDebugWriteInternal(fmt-1, 1); break; } default: // Otherwise store it like a regular symbol as a part of next chunk fmt--; break; } chunkStart = fmt; chunkSize=0; } while(ch != 0); if(chunkSize) usbDebugWriteInternal(chunkStart, chunkSize - 1); // Not including terminating NULL va_end(v); }


Việc triển khai của tôi đơn giản hơn nhiều so với thư viện, nhưng nó có thể làm mọi thứ tôi cần - in chuỗi, số thập phân và thập lục phân có định dạng (độ rộng trường, kết thúc số bằng số 0 ở bên trái). Nó chưa biết in số âm hay số dấu phẩy động nhưng cũng không khó để cộng. Sau này tôi có thể cho phép ghi kết quả vào bộ đệm chuỗi (như sprintf) chứ không chỉ vào USB.

Hiệu suất của mã này là khoảng 150-200 kb/s bao gồm cả truyền qua USB và phụ thuộc vào số lượng (độ dài) tin nhắn, độ phức tạp của chuỗi định dạng và kích thước của bộ đệm. Tốc độ này khá đủ để gửi vài nghìn tin nhắn nhỏ mỗi giây. Điều quan trọng nhất là các cuộc gọi không bị chặn.

Tệ hơn nữa: HAL cấp thấp

Về nguyên tắc, chúng ta có thể đã kết thúc ở đó, nhưng tôi nhận thấy rằng những người từ STM32GENERIC vừa mới thêm một HAL mới. Điều thú vị là có nhiều tệp xuất hiện dưới tên stm32f1xx_ll_XXXX.h. Họ tiết lộ một giải pháp thay thế và triển khai HAL ở cấp độ thấp hơn. Những thứ kia. một HAL thông thường cung cấp một giao diện cấp cao theo kiểu “lấy mảng này và chuyển nó cho tôi bằng giao diện này. Báo cáo hoàn thành bị gián đoạn.” Ngược lại, các tệp có chữ cái LL trong tên cung cấp giao diện cấp thấp hơn như “đặt các cờ này cho thanh ghi như vậy và như vậy”.

Sự huyền bí của thị trấn của chúng tôi

Sau khi xem các tệp mới trong kho STM32GENERIC, tôi muốn tải xuống bộ công cụ hoàn chỉnh từ trang web ST. Nhưng việc tìm kiếm trên Google chỉ dẫn tôi đến phiên bản HAL (STM32 Cube F1) 1.4, không chứa các tệp mới này. Bộ cấu hình đồ họa STM32CubeMX cũng cung cấp phiên bản này. Tôi đã hỏi các nhà phát triển của STM32GENERIC nơi họ lấy phiên bản mới. Thật ngạc nhiên, tôi đã nhận được một liên kết đến cùng một trang, chỉ bây giờ nó mới cung cấp tải xuống phiên bản 1.6. Google cũng bất ngờ bắt đầu “tìm” một phiên bản mới cũng như CubeMX được cập nhật. Chủ nghĩa thần bí và không có gì hơn!


Tại sao điều này là cần thiết? Trong hầu hết các trường hợp, giao diện cấp cao thực sự giải quyết được vấn đề khá tốt. HAL (Lớp trừu tượng phần cứng) hoàn toàn đúng như tên gọi của nó - nó trừu tượng hóa mã từ các thanh ghi bộ xử lý và phần cứng. Nhưng trong một số trường hợp, HAL hạn chế trí tưởng tượng của người lập trình, trong khi sử dụng các khái niệm trừu tượng ở cấp độ thấp hơn thì có thể thực hiện nhiệm vụ hiệu quả hơn. Trong trường hợp của tôi đây là GPIO và UART.

Hãy thử các giao diện mới. Hãy bắt đầu với bóng đèn. Thật không may, vẫn chưa có đủ ví dụ trên Internet. Chúng tôi sẽ cố gắng hiểu các chú thích mã cho các chức năng, may mắn thay mọi thứ đều theo thứ tự.

Rõ ràng những thứ cấp thấp này cũng có thể được chia thành 2 phần:

  • các hàm cấp cao hơn một chút theo kiểu HAL thông thường - đây là cấu trúc khởi tạo, vui lòng khởi tạo ngoại vi cho tôi.
  • Bộ định vị và bộ thu thập cấp độ thấp hơn một chút của các cờ hoặc thanh ghi riêng lẻ. Phần lớn các chức năng của nhóm này chỉ ở nội tuyến và tiêu đề.
Theo mặc định, những cái đầu tiên bị tắt bởi USE_FULL_LL_DRIVER. Chà, họ bị tàn tật và chết tiệt với họ. Chúng tôi sẽ sử dụng cái thứ hai. Sau một chút pháp sư, tôi đã có được trình điều khiển đèn LED này

Morgulka trên LL HAL

// Lớp đóng gói hoạt động với (các) đèn LED trên bo mạch // // Lưu ý: lớp này khởi tạo các chân tương ứng trong hàm tạo. // Có thể không hoạt động bình thường nếu các đối tượng của lớp này được tạo dưới dạng biến toàn cục LEDDriver ( const uint32_t pin = LL_GPIO_PIN_13; public: LEDDriver() ( //bật đồng hồ cho thiết bị ngoại vi GPIOC __HAL_RCC_GPIOC_IS_CLK_ENABLED(); // Khởi tạo PC 13 làm đầu ra LL_GPIO_SetPinMode(GPIOC, pin, LL_GPIO_MODE_OUTPUT); LL_GPIO_SetPinOutputType(GPIOC, pin, LL_GPIO_OUTPUT_PUSHPULL); LL_GPIO_SetPinSpeed(GPIOC, pin, LL_GPIO_SPEED_FREQ_LOW); ) void TurnOn() ( LL_GPIO_ResetOutput Pin(GPIOC, pin); ) void TurnOff () ( LL_GPIO_SetOutputPin(GPIOC , pin); ) void chuyển đổi() (LL_GPIO_TogglePin(GPIOC, pin); ) ); void vLEDThread(void *pvParameters) ( LEDDriver led; // Chỉ nhấp nháy một lần trong 2 giây cho (;;) ( vTaskDelay(2000); led.turnOn(); vTaskDelay(100); led.turnOff(); ) )


Mọi thứ đều rất đơn giản! Điều thú vị là ở đây bạn thực sự làm việc trực tiếp với các thanh ghi và cờ. Không có chi phí chung cho mô-đun HAL GPIO, mô-đun này tự biên dịch tối đa 450 byte và điều khiển chân từ STM32GENERIC, mất thêm 670 byte. Nói chung, ở đây, toàn bộ lớp với tất cả các lệnh gọi được đưa vào hàm vLEDThread, hàm này chỉ có kích thước 48 byte!

Tôi chưa cải thiện việc kiểm soát đồng hồ thông qua LL HAL. Nhưng điều này không quan trọng lắm, bởi vì... gọi __HAL_RCC_GPIOC_IS_CLK_ENABLED() từ HAL bình thường thực sự là một macro chỉ đặt một vài cờ trong một số thanh ghi nhất định.

Thật dễ dàng với các nút

Các nút thông qua LL HAL

// Gán các chân const uint32_t SEL_BUTTON_PIN = LL_GPIO_PIN_14; const uint32_t OK_BUTTON_PIN = LL_GPIO_PIN_15; // Khởi tạo các nút liên quan đến nội dung void initButtons() ( //bật đồng hồ cho thiết bị ngoại vi GPIOC __HAL_RCC_GPIOC_IS_CLK_ENABLED(); // Thiết lập chân nút LL_GPIO_SetPinMode(GPIOC, SEL_BUTTON_PIN, LL_GPIO_MODE_INPUT); LL_GPIO_SetPinPull(GPIOC, SEL_BUTTON_PIN, LL_ GPIO_PULL_DOWN); LL_GPIO_SetPinMode( Gpioc, ok_button_pin, ll_gpio_mode_input); / dob ouncing vtaskdelay ( DEBOUNCE_DUration ); if(LL_GPIO_IsInputPinSet(GPIOC, pin)) trả về true; ) trả về false; )


Với UART mọi thứ sẽ thú vị hơn. Hãy để tôi nhắc bạn về vấn đề này. Khi sử dụng HAL, việc nhận phải được “nạp lại” sau mỗi byte nhận được. Chế độ “lấy mọi thứ” không được cung cấp trong HAL. Và với LL HAL chúng ta sẽ thành công.

Việc thiết lập ghim không chỉ khiến tôi phải suy nghĩ kỹ mà còn khiến tôi phải xem lại Tài liệu tham khảo

Thiết lập chân UART

// Khởi tạo các chân ở chế độ chức năng thay thế LL_GPIO_SetPinMode(GPIOA, LL_GPIO_PIN_9, LL_GPIO_MODE_ALTERNATE); // Chân TX LL_GPIO_SetPinSpeed(GPIOA, LL_GPIO_PIN_9, LL_GPIO_SPEED_FREQ_HIGH); LL_GPIO_SetPinOutputType(GPIOA, LL_GPIO_PIN_9, LL_GPIO_OUTPUT_PUSHPULL); LL_GPIO_SetPinMode(GPIOA, LL_GPIO_PIN_10, LL_GPIO_MODE_INPUT); //Chân RX


Làm lại việc khởi tạo UART cho các giao diện mới

Khởi tạo UART

// Chuẩn bị khởi tạo LL_USART_Disable(USART1); // Khởi tạo LL_USART_SetBaudRate(USART1, HAL_RCC_GetPCLK2Freq(), 9600); LL_USART_SetDataWidth(USART1, LL_USART_DATAWIDTH_8B); LL_USART_SetStopBitsĐộ dài(USART1, LL_USART_STOPBITS_1); LL_USART_SetParity(USART1, LL_USART_PARITY_NONE); LL_USART_SetTransferDirection(USART1, LL_USART_DIRECTION_TX_RX); LL_USART_SetHWFlowCtrl(USART1, LL_USART_HWControl_NONE); // Chúng ta sẽ sử dụng ngắt UART để lấy dữ liệu HAL_NVIC_SetPriority(USART1_IRQn, 6, 0); HAL_NVIC_EnableIRQ(USART1_IRQn); // Kích hoạt ngắt UART khi nhận byte LL_USART_EnableIT_RXNE(USART1); // Cuối cùng kích hoạt thiết bị ngoại vi LL_USART_Enable(USART1);


Bây giờ gián đoạn. Trong phiên bản trước, chúng tôi có tối đa 2 hàm - một hàm xử lý ngắt và hàm thứ hai là gọi lại (từ cùng một ngắt) về byte đã nhận. Ở phiên bản mới chúng ta đã cấu hình ngắt chỉ nhận 1 byte nên sẽ nhận được byte ngay lập tức.

ngắt UART

// Lưu trữ byte nhận được nội tuyến void charReceivedCB(uint8_t c) ( rxBuffer = c; LastReceivedIndex++; // Nếu nhận được ký hiệu EOL, hãy thông báo cho luồng GPS rằng dòng đó có sẵn để đọc if(c == "\n") vTaskNotifyGiveFromISR(xGPSThread, NULL); ) bên ngoài "C" void USART1_IRQHandler(void) ( uint8_t byte = LL_USART_ReceiveData8(USART1); gpsUart.charReceivedCB(byte); )


Kích thước của mã trình điều khiển giảm từ 1242 xuống 436 byte và mức tiêu thụ RAM từ 200 xuống 136 (trong đó 128 là bộ đệm). Theo ý kiến ​​​​của tôi không tệ. Điều đáng tiếc duy nhất là đây không phải là phần háu ăn nhất. Có thể cắt giảm thứ gì đó khác một chút, nhưng hiện tại tôi không đặc biệt theo đuổi việc tiêu thụ tài nguyên - tôi vẫn còn chúng. Và giao diện HAL cấp cao hoạt động khá tốt trong trường hợp các thiết bị ngoại vi khác.

Nhìn phía sau

Mặc dù khi bắt đầu giai đoạn này của dự án, tôi còn nghi ngờ về HAL, nhưng tôi vẫn cố gắng viết lại tất cả công việc với các thiết bị ngoại vi: GPIO, UART, I2C, SPI và USB. Tôi đã đạt được tiến bộ lớn trong việc hiểu cách thức hoạt động của các mô-đun này và đã cố gắng truyền đạt kiến ​​thức trong bài viết này. Nhưng đây hoàn toàn không phải là bản dịch của Tài liệu tham khảo. Ngược lại, tôi đã làm việc trong bối cảnh của dự án này và chỉ ra cách bạn có thể viết trình điều khiển ngoại vi bằng HAL thuần túy.

Bài báo hóa ra là một câu chuyện ít nhiều tuyến tính. Nhưng trên thực tế, tôi đã có một số bữa ăn nửa buổi mà tôi đồng thời cưa theo những hướng hoàn toàn ngược lại. Vào buổi sáng, tôi có thể gặp vấn đề với hiệu suất của một số thư viện Arduino và quyết định viết lại mọi thứ trong HAL, và vào buổi tối, tôi phát hiện ra rằng ai đó đã thêm hỗ trợ DMA cho STM32GENERIC và tôi muốn quay lại . Hoặc, ví dụ, dành một vài ngày vật lộn với các giao diện Arduino, cố gắng hiểu cách truyền dữ liệu qua I2C thuận tiện hơn, trong khi trên HAL, việc này được thực hiện trong 2 dòng.

Nói chung là tôi đã đạt được điều mình mong muốn. Công việc chính với các thiết bị ngoại vi do tôi kiểm soát và viết bằng HAL. Arduino chỉ hoạt động như một bộ chuyển đổi cho một số thư viện. Đúng là vẫn còn một số đuôi. Bạn vẫn cần thu hết can đảm và xóa STM32GENERIC khỏi kho lưu trữ của mình, chỉ để lại một vài lớp thực sự cần thiết. Nhưng việc làm sạch như vậy sẽ không còn áp dụng cho bài viết này nữa.

Đối với Arudino và bản sao của nó. Tôi vẫn thích khuôn khổ này. Với nó, bạn có thể nhanh chóng tạo nguyên mẫu cho một thứ gì đó mà không thực sự bận tâm đến việc đọc hướng dẫn sử dụng và bảng dữ liệu. Về nguyên tắc, bạn thậm chí có thể tạo ra các thiết bị đầu cuối bằng Arduino nếu không có yêu cầu đặc biệt nào về tốc độ, mức tiêu thụ hoặc bộ nhớ. Trong trường hợp của tôi, những thông số này khá quan trọng nên tôi phải chuyển sang HAL.

Tôi bắt đầu làm việc trên stm32duino. Bản sao này thực sự đáng được chú ý nếu bạn muốn có Arduino trên STM32 và mọi thứ đều hoạt động tốt. Ngoài ra, họ còn theo dõi chặt chẽ mức tiêu thụ RAM và flash. Ngược lại, bản thân STM32GENERIC dày hơn và dựa trên HAL quái dị. Nhưng khuôn khổ này đang tích cực phát triển và sắp hoàn thành. Nói chung, tôi có thể đề xuất cả hai khung với một chút ưu tiên cho STM32GENERIC vì HAL và sự phát triển năng động hơn vào thời điểm hiện tại. Ngoài ra, Internet có đầy đủ các ví dụ về HAL và bạn luôn có thể tùy chỉnh thứ gì đó cho phù hợp với mình.

Tôi vẫn coi bản thân HAL với một mức độ ghê tởm nào đó. Thư viện quá cồng kềnh và xấu xí. Tôi chấp nhận thực tế là thư viện dựa trên C, đòi hỏi phải sử dụng tên dài của hàm và hằng. Tuy nhiên, đây không phải là một thư viện thú vị để làm việc. Đúng hơn, đó là một biện pháp cần thiết.

Được rồi, giao diện - bên trong cũng khiến bạn phải suy nghĩ. Các chức năng lớn với chức năng dành cho mọi trường hợp sẽ gây lãng phí tài nguyên. Hơn nữa, nếu bạn có thể xử lý mã thừa trong flash bằng cách tối ưu hóa thời gian liên kết, thì mức tiêu thụ RAM khổng lồ chỉ có thể được giải quyết bằng cách viết lại nó thành LL HAL.

Nhưng đó thậm chí không phải là điều đáng lo ngại, mà ở một số nơi, đó chỉ là sự coi thường tài nguyên. Vì vậy, tôi nhận thấy việc sử dụng quá nhiều bộ nhớ trong mã Middleware USB (chính thức thì nó không phải là HAL, nhưng được cung cấp như một phần của STM32Cube). Cấu trúc USB chiếm 2,5kb bộ nhớ. Hơn nữa, cấu trúc USBD_HandleTypeDef (544 byte) phần lớn lặp lại PCD_HandleTypeDef từ lớp dưới (1056 byte) - điểm cuối cũng được xác định trong đó. Bộ đệm thu phát cũng được khai báo ở ít nhất hai vị trí - USBD_CDC_HandleTypeDef và UserRxBufferFS/UserTxBufferFS.

Bộ mô tả thường được khai báo trong RAM. Để làm gì? Họ là không đổi! Gần 400 byte trong RAM. May mắn thay, một số bộ mô tả là không đổi (ít hơn 300 byte một chút). Mô tả là thông tin bất biến. Và ở đây có một mã đặc biệt để vá chúng, và một lần nữa, với một hằng số. Và thậm chí một cái đã được bao gồm ở đó. Vì lý do nào đó, các hàm như SetBuffer không chấp nhận bộ đệm không đổi, điều này cũng ngăn cản việc đưa bộ mô tả và một số thứ khác vào flash. Lý do là gì? Nó sẽ được sửa trong 10 phút!!!

Hoặc cấu trúc khởi tạo là một phần của bộ điều khiển đối tượng (ví dụ i2c). Tại sao lưu trữ cái này sau khi thiết bị ngoại vi được khởi tạo? Tại sao tôi cần con trỏ tới các cấu trúc không được sử dụng - ví dụ: tại sao tôi cần dữ liệu liên kết với DMA nếu tôi không sử dụng nó?

Và cũng có mã trùng lặp.

trường hợp USB_DESC_TYPE_CONFIGUration: if(pdev->dev_speed == USBD_SPEED_HIGH) ( pbuf = (uint8_t *)pdev->pClass->GetHSConfigDescriptor(&len); pbuf = USB_DESC_TYPE_CONFIGUration; ) else ( pbuf = (uint8_t *)pdev->pClass-> GetFSConfigDescriptor(&len); pbuf = USB_DESC_TYPE_CONFIGUration; ) ngắt;


Một chuyển đổi đặc biệt sang “loại Unicode”, có thể được thực hiện trong thời gian biên dịch. Hơn nữa, một bộ đệm đặc biệt được phân bổ cho việc này

Sự nhạo báng dữ liệu thống kê

ALIGN_BEGIN uint8_t USBD_StrDesc __ALIGN_END; void USBD_GetString(const char *desc, uint8_t *unicode, uint16_t *len) ( uint8_t idx = 0; if (desc != NULL) ( *len = USBD_GetLen(desc) * 2 + 2; unicode = *len; unicode = USB_DESC_TYPE_STRING ; trong khi (*desc != "\0") ( unicode = *desc++; unicode = 0x00; ) ) )


Không gây tử vong, nhưng nó khiến bạn tự hỏi liệu HAL có tốt như những người biện hộ viết về nó không? Chà, đây không phải là những gì bạn mong đợi từ một thư viện từ nhà sản xuất và được thiết kế dành cho các chuyên gia. Đây là những bộ vi điều khiển! Ở đây mọi người lưu giữ từng byte và từng micro giây đều quý giá. Và ở đây, bạn biết đấy, có một bộ đệm nặng nửa kg và chuyển đổi nhanh chóng các chuỗi không đổi. Điều đáng chú ý là hầu hết các ý kiến ​​đều áp dụng cho USB Middleware.

CẬP NHẬT: trong HAL 1.6, cuộc gọi lại hoàn tất chuyển giao I2C DMA cũng bị hỏng. Những thứ kia. Ở đó, mã tạo xác nhận khi dữ liệu được gửi qua DMA đã hoàn toàn biến mất, mặc dù nó được mô tả trong tài liệu. Có một cái để tiếp nhận, nhưng không có để truyền tải. Tôi đã phải chuyển về HAL 1.4 cho mô-đun I2C, may mắn thay có một mô-đun - một tệp.

Cuối cùng, tôi sẽ đưa ra mức tiêu thụ flash và RAM của các thành phần khác nhau. Trong phần Trình điều khiển, tôi đã cung cấp các giá trị cho cả trình điều khiển dựa trên HAL và trình điều khiển dựa trên LL HAL. Trong trường hợp thứ hai, các phần tương ứng từ phần HAL không được sử dụng.

Tiêu thụ bộ nhớ

Loại Tiểu thể loại .chữ .rodata .dữ liệu .bss
Hệ thống vectơ ngắt 272
trình xử lý ISR ​​giả 178
libc 760
phép toán nổi 4872
tội lỗi/cos 6672 536
chính & v.v. 86
Mã của tôi Mã của tôi 7404 833 4 578
printf 442
Phông chữ 3317
NeoGPS 4376 93 300
RTOS miễn phí 4670 4 209
Adafbean GFX 1768
Adafbean SSD1306 1722 1024
SdFat 5386 1144
Phần mềm trung gian USB Cốt lõi 1740 333 2179
CDC 772
Trình điều khiển UART 268 200
USB 264 846
I2C 316 164
SPI 760 208
Nút LL 208
LED LL 48
UART LL 436 136
Arduino gpio 370 296 16
linh tinh 28 24
In 822
HAL USB LL 4650
SysTick 180
NVIC 200
DMA 666
GPIO 452
I2C 1560
SPI 2318
RCC 1564 4
UART 974
đống (không thực sự được sử dụng) 1068
đống FreeRTOS 10240

Đó là tất cả. Tôi sẽ rất vui khi nhận được những nhận xét mang tính xây dựng cũng như những đề xuất nếu có điều gì ở đây có thể được cải thiện.

thẻ:

  • HAL
  • STM32
  • STM32cube
  • arduino
Thêm thẻ

Sự tương tác của mã người dùng với các thanh ghi lõi và ngoại vi của bộ vi điều khiển STM32 có thể được thực hiện theo hai cách: sử dụng thư viện chuẩn hoặc sử dụng bộ đoạn mã (gợi ý phần mềm). Việc lựa chọn giữa chúng phụ thuộc vào dung lượng bộ nhớ riêng của bộ điều khiển, tốc độ cần thiết và khung thời gian phát triển. Bài viết phân tích đặc điểm cấu trúc, ưu nhược điểm của các bộ đoạn mã cho bộ vi điều khiển họ STM32F1 và STM32L0 do STMicroelectronics sản xuất.

Một trong những lợi thế của việc sử dụng bộ vi điều khiển STMicroelectronics là có nhiều công cụ phát triển: tài liệu, bảng phát triển, phần mềm.

Phần mềm dành cho STM32 bao gồm phần mềm độc quyền do STMicroelectronics sản xuất, nguồn mở và phần mềm thương mại.

Phần mềm STMicroelectronics có những ưu điểm quan trọng. Trước hết, nó có sẵn để tải xuống miễn phí. Thứ hai, các thư viện phần mềm được trình bày dưới dạng mã nguồn - người dùng có thể tự sửa đổi mã, có tính đến các hạn chế nhỏ được mô tả trong thỏa thuận cấp phép.

Thư viện STMicroelectronics tuân thủ ANSI-C và có thể được chia theo mức độ trừu tượng (Hình 1):

  • CMSIS (Lớp truy cập ngoại vi lõi) - cấp độ đăng ký lõi và ngoại vi, thư viện ARM;
  • Lớp trừu tượng phần cứng – thư viện cấp thấp: thư viện ngoại vi tiêu chuẩn, bộ đoạn mã;
  • Middleware – thư viện cấp trung: hệ điều hành thời gian thực (RTOS), hệ thống tệp, USB, TCP/IP, Bluetooth, Display, ZigBee, Touch Sensing và các thứ khác;
  • Trường ứng dụng – thư viện cấp ứng dụng: giải pháp âm thanh, điều khiển động cơ, ô tô và công nghiệp.

Hình 1 cho thấy để tương tác với cấp độ CMSIS, STMicroelectronics cung cấp việc sử dụng hai công cụ chính - thư viện tiêu chuẩn và đoạn mã.

Thư viện tiêu chuẩn là một bộ trình điều khiển. Mỗi trình điều khiển cung cấp cho người dùng các chức năng và định nghĩa để làm việc với một khối ngoại vi cụ thể (SPI, USART, ADC, v.v.). Người dùng không tương tác trực tiếp với các thanh ghi cấp độ CMSIS.

Bộ đoạn mã là các ví dụ lập trình hiệu quả cao sử dụng quyền truy cập trực tiếp vào các thanh ghi CMSIS. Các nhà phát triển phần mềm có thể sử dụng việc triển khai các hàm từ các ví dụ này trong mã của riêng họ.

Mỗi phương pháp có ưu điểm và nhược điểm. Việc lựa chọn giữa chúng được thực hiện có tính đến dung lượng FLASH và RAM có sẵn, tốc độ cần thiết, thời gian phát triển, kinh nghiệm của lập trình viên và các trường hợp khác.

cấp độ CMSIS

Bộ vi điều khiển là một chip tương tự kỹ thuật số phức tạp bao gồm lõi xử lý, bộ nhớ, các thiết bị ngoại vi, bus kỹ thuật số, v.v. Tương tác với mỗi khối xảy ra bằng cách sử dụng các thanh ghi.

Theo quan điểm của lập trình viên, bộ vi điều khiển đại diện cho một không gian bộ nhớ. Nó không chỉ chứa RAM, FLASH và EEPROM mà còn chứa các thanh ghi chương trình. Mỗi thanh ghi phần cứng tương ứng với một ô nhớ. Vì vậy, để ghi dữ liệu vào một thanh ghi hoặc trừ giá trị của nó, người lập trình cần truy cập vào vị trí tương ứng trong không gian địa chỉ.

Một người có một số đặc điểm về nhận thức. Ví dụ, những cái tên tượng trưng được anh ta cảm nhận tốt hơn nhiều so với địa chỉ của các ô bộ nhớ. Điều này đặc biệt đáng chú ý khi một số lượng lớn tế bào được sử dụng. Trong bộ vi điều khiển ARM, số lượng thanh ghi và số lượng ô được sử dụng vượt quá một nghìn. Để làm cho mọi việc dễ dàng hơn, cần phải định nghĩa các con trỏ tượng trưng. Quyết định này được thực hiện ở cấp độ CMSIS.

Ví dụ: để đặt trạng thái của các chân cổng A, bạn cần ghi dữ liệu vào thanh ghi GPIOA_ODR. Điều này có thể được thực hiện theo hai cách - sử dụng một con trỏ có địa chỉ ô 0xEBFF FCFF với độ lệch 0x14 hoặc sử dụng một con trỏ có tên tượng trưng GPIOA và cấu trúc tạo sẵn xác định độ lệch. Rõ ràng, lựa chọn thứ hai dễ hiểu hơn nhiều.

CMSIS cũng thực hiện các chức năng khác. Nó được triển khai dưới dạng nhóm tệp sau:

  • startup_stm32l0xx.s chứa mã khởi động trình biên dịch mã cho Cortex-M0+ và bảng vectơ ngắt. Sau khi quá trình khởi tạo bắt đầu hoàn tất, điều khiển trước tiên được chuyển sang hàm SystemInit() (các giải thích sẽ được đưa ra bên dưới), sau đó đến hàm chính int main(void);
  • stm32l0xx.h chứa các định nghĩa cần thiết để thực hiện các thao tác bit cơ bản và định nghĩa về loại bộ vi xử lý được sử dụng;
  • system_stm32l0xx.c/.h. Sau lần khởi tạo đầu tiên, hàm SystemInit() được thực thi. Nó thực hiện thiết lập ban đầu các thiết bị ngoại vi hệ thống, thời gian của khối RCC;
  • stm32l0yyxx.h – tệp triển khai cho các bộ vi điều khiển cụ thể (ví dụ: stm32l051xx.h). Trong đó, các con trỏ ký tự, cấu trúc dữ liệu, hằng số bit và độ lệch được xác định.

Tương tác với CMSIS. Thư viện và đoạn mã tiêu chuẩn

Số lượng thanh ghi cho bộ vi điều khiển STM32 trong hầu hết các kiểu máy đều vượt quá một nghìn. Nếu bạn sử dụng quyền truy cập trực tiếp vào sổ đăng ký, mã người dùng sẽ không thể đọc được và hoàn toàn không thể sử dụng được để hỗ trợ và hiện đại hóa. Vấn đề này có thể được giải quyết bằng cách sử dụng thư viện ngoại vi tiêu chuẩn.

Thư viện ngoại vi tiêu chuẩn là một tập hợp các trình điều khiển cấp thấp. Mỗi trình điều khiển cung cấp cho người dùng một bộ chức năng để làm việc với thiết bị ngoại vi. Bằng cách này, người dùng sử dụng các chức năng thay vì truy cập trực tiếp vào các thanh ghi. Trong trường hợp này, cấp độ CMSIS bị ẩn khỏi lập trình viên (Hình 2a).

Cơm. 2. Tương tác với CMSIS bằng thư viện chuẩn (a) và đoạn mã (b)

Ví dụ: tương tác với các cổng I/O trong STM32L0 được triển khai bằng trình điều khiển được tạo ở dạng hai tệp: stm32l0xx_hal_gpio.h và stm32l0xx_hal_gpio.c. stm32l0xx_hal_gpio.h cung cấp các định nghĩa cơ bản về kiểu và hàm, còn stm32l0xx_hal_gpio.c cung cấp cách triển khai chúng.

Cách tiếp cận này có những ưu điểm khá rõ ràng (Bảng 1):

  • Tạo mã nhanh. Người lập trình không cần nghiên cứu danh sách các thanh ghi. Anh ta ngay lập tức bắt đầu làm việc ở cấp độ cao hơn. Ví dụ, để giao tiếp trực tiếp với cổng I/O trên STM32L0, bạn phải biết và có khả năng vận hành 11 thanh ghi điều khiển/trạng thái, hầu hết trong số đó có tới 32 bit có thể cấu hình được. Khi sử dụng trình điều khiển thư viện, chỉ cần thành thạo tám chức năng là đủ.
  • Sự đơn giản và rõ ràng của mã. Mã người dùng không bị tắc với tên đăng ký, nó có thể minh bạch và dễ đọc, điều này rất quan trọng khi làm việc với nhóm phát triển.
  • Mức độ trừu tượng cao. Khi sử dụng thư viện chuẩn, mã hóa ra khá độc lập với nền tảng. Ví dụ: nếu bạn thay đổi bộ vi điều khiển STM32L0 thành bộ vi điều khiển STM32F0, một số mã hoạt động với các cổng I/O sẽ không cần phải thay đổi chút nào.

Bảng 1. So sánh các phương pháp triển khai mã tùy chỉnh

Tham số so sánh Khi sử dụng tiêu chuẩn
thư viện ngoại vi
Khi sử dụng bộ đoạn mã
Kích thước mã trung bình tối thiểu
chi phí RAM trung bình tối thiểu
Hiệu suất trung bình tối đa
Khả năng đọc mã xuất sắc thấp
Mức độ độc lập của nền tảng trung bình ngắn
Tốc độ tạo chương trình cao thấp

Sự hiện diện của lớp vỏ bổ sung dưới dạng trình điều khiển cũng có những nhược điểm rõ ràng (Bảng 1):

  • Tăng khối lượng mã chương trình. Các chức năng được triển khai trong mã thư viện yêu cầu thêm dung lượng bộ nhớ.
  • Chi phí RAM tăng do tăng số lượng biến cục bộ và sử dụng cấu trúc dữ liệu cồng kềnh.
  • Giảm hiệu suất do tăng chi phí khi gọi các hàm thư viện.

Chính sự tồn tại của những thiếu sót này đã dẫn đến việc người dùng thường bị buộc phải tối ưu hóa mã - triển khai độc lập các chức năng để tương tác với CMSIS, tối ưu hóa các chức năng thư viện bằng cách loại bỏ tất cả những thứ không cần thiết, sao chép trực tiếp việc triển khai các chức năng thư viện vào mã của họ, sử dụng lệnh __INLINE để tăng tốc độ thực thi. Kết quả là, người ta đã dành thêm thời gian để tinh chỉnh mã.

STMicroelectronics, gặp gỡ các nhà phát triển một nửa, đã phát hành bộ sưu tập đoạn mã STM32SnippetsF0 và STM32SnippetsL0.

Các đoạn mã được bao gồm trong mã người dùng (Hình 2b).

Việc sử dụng đoạn mã mang lại những lợi ích rõ ràng:

  • tăng hiệu quả và tốc độ của mã;
  • giảm phạm vi của chương trình;
  • Giảm dung lượng RAM được sử dụng và tải trên ngăn xếp.

Tuy nhiên, điều đáng chú ý là những nhược điểm:

  • làm giảm tính đơn giản và rõ ràng của mã do “nhiễm” tên đăng ký và việc triển khai độc lập các chức năng cấp thấp;
  • sự biến mất của nền tảng độc lập.

Vì vậy, việc lựa chọn giữa thư viện chuẩn và đoạn mã là không rõ ràng. Trong hầu hết các trường hợp, điều đáng nói không phải là về sự cạnh tranh mà là về việc sử dụng lẫn nhau của chúng. Ở giai đoạn đầu, để nhanh chóng xây dựng mã “đẹp”, việc sử dụng trình điều khiển tiêu chuẩn là điều hợp lý. Nếu cần tối ưu hóa, bạn có thể chuyển sang các đoạn mã làm sẵn để không lãng phí thời gian phát triển các chức năng tối ưu của riêng mình.

Thư viện tiêu chuẩn của trình điều khiển và đoạn mã STM32F0 và STM32L0 (Bảng 2) có sẵn để tải xuống miễn phí trên trang web www.st.com.

Bảng 2. Thư viện cấp thấp cho STM32F10 và STM32L0

Khi làm quen kỹ hơn với các đoạn trích, cũng như với bất kỳ phần mềm nào, nên bắt đầu bằng cách xem xét các tính năng của thỏa thuận cấp phép.

Thỏa thuận cấp phép

Bất kỳ lập trình viên có trách nhiệm nào đều nghiên cứu kỹ thỏa thuận cấp phép trước khi sử dụng các sản phẩm phần mềm của bên thứ ba. Mặc dù thực tế là các bộ sưu tập đoạn trích do ST Microelectronics sản xuất không yêu cầu cấp phép và có sẵn để tải xuống miễn phí, điều này không có nghĩa là không có hạn chế đối với việc sử dụng chúng.

Thỏa thuận cấp phép đi kèm với tất cả các sản phẩm có thể tải xuống miễn phí do STMicroelectronics sản xuất. Sau khi tải xuống STM32SnippetsF0 và STM32SnippetsL0 trong thư mục gốc, bạn có thể dễ dàng tìm thấy tài liệu Thỏa thuận cấp phép MCD-ST Liberty SW V2.pdf, trong đó giới thiệu cho người dùng các quy tắc sử dụng phần mềm này.

Thư mục Project chứa các thư mục con với các ví dụ cho các thiết bị ngoại vi cụ thể, các dự án được tạo sẵn cho ARM Keil và EWARM, cũng như các tệp main.c.

Ra mắt và tính năng sử dụng bộ đoạn mã STM32SnippetsF0 và STM32SnippetsL0

Điểm đặc biệt của các bộ đoạn mã này là sự phụ thuộc vào nền tảng của chúng. Chúng được thiết kế để làm việc với các bảng cụ thể. STM32SnippetsL0 sử dụng bảng Discovery STM32L053 và STM32SnippetsF0 sử dụng bảng Discovery STM32F072.

Khi sử dụng bo mạch độc quyền, mã và thiết kế phải được sửa đổi, điều này sẽ được thảo luận chi tiết hơn ở phần cuối.

Để chạy ví dụ, bạn cần hoàn thành một số bước:

  • chạy dự án đã hoàn thành từ thư mục có ví dụ được yêu cầu. Để đơn giản, bạn có thể sử dụng các dự án tạo sẵn cho môi trường ARM Keil hoặc EWARM, nằm trong thư mục MDK-ARM\ và EWARM\ tương ứng;
  • bật nguồn cho bo mạch phát triển Discovery STM32L053 Discovery/STM32F072 Discovery;
  • Kết nối nguồn điện của bảng gỡ lỗi với PC bằng cáp USB. Nhờ trình gỡ lỗi ST-Link/V2 tích hợp sẵn, không cần lập trình viên bổ sung;
  • mở, cấu hình và chạy dự án;
    • Đối với ARM Keil:
      • Chủ đề mở;
      • biên dịch dự án – Dự án → Xây dựng lại tất cả các tệp mục tiêu;
      • tải nó vào bộ điều khiển – Gỡ lỗi → Bắt đầu/Dừng phiên gỡ lỗi;
      • chạy chương trình trong cửa sổ Debug → Run (F5).
    • Đối với EWARM:
      • Chủ đề mở;
      • biên dịch dự án – Dự án → Xây dựng lại tất cả;
      • tải nó vào bộ điều khiển – Dự án → Gỡ lỗi;
      • chạy chương trình trong cửa sổ Debug → Go (F5).
  • thực hiện thử nghiệm theo thuật toán được mô tả trong main.c.

Để phân tích mã chương trình, hãy xem xét một ví dụ cụ thể từ STM32SnippetsL0: Projects\LPUART\01_WakeUpFromLPM\.

Chạy một ví dụ cho LPUART

Một tính năng đặc biệt của các bộ vi điều khiển mới thuộc dòng STM32L0 dựa trên lõi Cortex-M0+ là khả năng thay đổi mức tiêu thụ một cách linh hoạt nhờ có nhiều cải tiến. Một trong những cải tiến này là sự xuất hiện của các thiết bị ngoại vi Nguồn điện thấp: bộ hẹn giờ LPTIM 16 bit và bộ thu phát LPUART. Các khối này có khả năng xung nhịp độc lập với xung nhịp của bus ngoại vi APB chính. Nếu cần giảm mức tiêu thụ điện năng, có thể giảm tần số hoạt động của bus APB (PCLK) và bản thân bộ điều khiển có thể được chuyển sang chế độ tiêu thụ điện năng thấp. Đồng thời, các thiết bị ngoại vi Low Power tiếp tục hoạt động với hiệu suất tối đa.

Hãy xem xét một ví dụ từ thư mục Projects\LPUART\01_WakeUpFromLPM\, xem xét khả năng hoạt động độc lập của LPUART ở chế độ tiêu thụ thấp.

Khi mở một dự án trong môi trường ARM Keil, chỉ có ba tệp được hiển thị: startup_stm32l053xx.s, system_stm32l0xx.c và main.c (Hình 4). Nếu thư viện chuẩn được sử dụng thì cần phải thêm các tệp trình điều khiển vào dự án.

Chức năng và phân tích cấu trúc tệp Main.c

Chương trình ví dụ đã chọn được thực hiện theo nhiều giai đoạn.

Sau khi bắt đầu, hàm SystemInit(), được triển khai trong system_stm32l0xx.c, sẽ được khởi chạy. Nó cấu hình các tham số của khối đồng hồ RCC (thời gian và tần số hoạt động). Tiếp theo, điều khiển được chuyển sang hàm main int main(void). Nó khởi tạo các thiết bị ngoại vi của người dùng - cổng đầu vào/đầu ra, LPUART - sau đó bộ điều khiển được chuyển sang chế độ STOP tiêu thụ thấp. Trong đó, ngoại vi và lõi thông thường bị dừng lại, chỉ LPUART hoạt động. Nó chờ bắt đầu truyền dữ liệu từ thiết bị bên ngoài. Khi bit bắt đầu đến, LPUART đánh thức hệ thống và nhận tin nhắn. Việc tiếp nhận đi kèm với sự nhấp nháy của đèn LED trên bảng gỡ lỗi. Sau đó, bộ điều khiển được chuyển về trạng thái STOP và chờ lần truyền dữ liệu tiếp theo nếu không phát hiện thấy lỗi.

Truyền dữ liệu xảy ra bằng cổng COM ảo và phần mềm bổ sung.

Hãy xem main.c từ dự án của chúng tôi. Tệp này là tệp C tiêu chuẩn. Tính năng chính của nó là tự ghi tài liệu - sự hiện diện của các nhận xét, giải thích và khuyến nghị chi tiết. Phần giải thích bao gồm một số phần:

  • tiêu đề cho biết tên tệp, phiên bản, ngày tháng, tác giả và giải thích ngắn gọn về mục đích;
  • mô tả trình tự thiết lập các thiết bị ngoại vi của hệ thống (các tính năng cụ thể của RCC): FLASH, RAM, hệ thống cấp nguồn và đồng hồ, bus ngoại vi, v.v.;
  • danh sách các tài nguyên vi điều khiển được sử dụng (MCU Resources);
  • giải thích ngắn gọn về cách sử dụng ví dụ này;
  • giải thích ngắn gọn về việc kiểm tra ví dụ và thuật toán triển khai nó (Cách kiểm tra ví dụ này).

Hàm int main(void) có dạng thu gọn và được trang bị các chú thích, trong Liệt kê 1, để rõ ràng hơn, chúng được dịch sang tiếng Nga.

Liệt kê 1. Ví dụ triển khai hàm main

int chính(void)
{
/* Khi bắt đầu thực hiện phần này, khi các đơn vị hệ thống đã được cấu hình trong hàm SystemInit(), được triển khai trong system_stm32l0xx.c. */
/*cấu hình các thiết bị ngoại vi */
Định cấu hình_GPIO_LED();
Định cấu hình_GPIO_LPUART();
Định cấu hình_LPUART();
Định cấu hình_LPM_Stop();
/*kiểm tra lỗi trong quá trình nhận */
while (!error) /* vòng lặp vô tận */
{
/*đợi LPUART sẵn sàng và chuyển sang chế độ STOP */
if((LPUART1->ISR & USART_ISR_REACK) == USART_ISR_REACK)
{
__WFI();
}
}
/* khi xảy ra lỗi */
SysTick_Config(2000); /* đặt khoảng thời gian ngắt bộ hẹn giờ hệ thống thành 1 ms */
trong khi(1);
}

Tệp main.c khai báo và xác định các chức năng cấu hình ngoại vi và hai chức năng xử lý ngắt. Hãy xem xét các tính năng của họ.

Ví dụ dưới đây sử dụng bốn hàm cấu hình (Liệt kê 2). Tất cả đều không có đối số và không trả về giá trị. Mục đích chính của chúng là nhanh chóng và với số lượng mã cần thiết ít nhất để khởi tạo các thiết bị ngoại vi. Điều này đạt được thông qua hai tính năng: sử dụng quyền truy cập trực tiếp vào các thanh ghi và sử dụng chỉ thị __INLINE (Liệt kê 3).

Liệt kê 2. Khai báo các hàm cấu hình ngoại vi

void Cấu hình_GPIO_LED(void);
void Cấu hình_GPIO_LPUART(void);
void Cấu hình_LPUART(void);
void Cấu hình_LPM_Stop(void);

Liệt kê 3. Ví dụ triển khai hàm __INLINE với quyền truy cập trực tiếp vào các thanh ghi LPUART

INLINE void Cấu hình_LPUART(void)
{
/* (1) Kích hoạt đồng hồ giao diện nguồn */
/* (2) Vô hiệu hóa thanh ghi bảo vệ sao lưu để cho phép truy cập vào miền đồng hồ RTC */
/* (3) Bật LSE */
/* (4) Đợi LSE sẵn sàng */
/* (5) Kích hoạt thanh ghi bảo vệ sao lưu để cho phép truy cập vào miền đồng hồ RTC */
/* (6) LSE được ánh xạ trên LPUART */
/* (7) Kích hoạt LPUART đồng hồ ngoại vi */
/* Cấu hình LPUART */
/* (8) lấy mẫu quá mức 16, 9600 baud */
/* (9) 8 bit dữ liệu, 1 bit bắt đầu, 1 bit dừng, không có tính chẵn lẻ, chế độ nhận, chế độ dừng */
/* (10) Đặt mức độ ưu tiên cho LPUART1_IRQn */
/* (11) Kích hoạt LPUART1_IRQn */
RCC->APB1ENR |= (RCC_APB1ENR_PWREN); /* (1) */
PWR->CR |= PWR_CR_DBP; /* (2) */
RCC->CSR |= RCC_CSR_LSEON; /* (3) */
trong khi ((RCC->CSR & (RCC_CSR_LSERDY)) != (RCC_CSR_LSERDY)) /*(4)*/
{
/* thêm thời gian ở đây để có một ứng dụng mạnh mẽ */
}
PWR->CR &=~ PWR_CR_DBP; /* (5) */
RCC->CCIPR |= RCC_CCIPR_LPUART1SEL; /* (6) */
RCC->APB1ENR |= RCC_APB1ENR_LPUART1EN; /*(7) */
LPUART1->BRR = 0x369; /* (số 8) */
LPUART1->CR1 = USART_CR1_UESM | USART_CR1_RXNEIE | USART_CR1_RE | USART_CR1_UE; /* (9) */
NVIC_SetPriority(LPUART1_IRQn, 0); /* (10) */
NVIC_EnableIRQ(LPUART1_IRQn); /* (mười một) */
}

Trình xử lý ngắt từ bộ đếm thời gian hệ thống và từ LPUART cũng sử dụng quyền truy cập trực tiếp vào các thanh ghi.

Do đó, việc giao tiếp với CMSIS được thực hiện mà không cần thư viện chuẩn. Mã hóa ra nhỏ gọn và hiệu quả cao. Tuy nhiên, khả năng đọc của nó sẽ giảm đi đáng kể do có quá nhiều quyền truy cập vào các thanh ghi.

Sử dụng đoạn trích trong quá trình phát triển của riêng bạn

Các bộ đoạn mã được đề xuất có những hạn chế: cần sử dụng bảng Discovery STM32L053 cho STM32SnippetsL0 và bảng Discovery STM32F072 cho STM32SnippetsF0.

Để sử dụng đoạn mã trong quá trình phát triển của mình, bạn sẽ cần thực hiện một số thay đổi. Trước tiên, bạn cần cấu hình lại dự án cho bộ xử lý mong muốn. Để thực hiện việc này, bạn cần thay đổi tệp khởi động startup_stm32l053xx.s thành tệp của bộ điều khiển khác và xác định hằng số bắt buộc: STM32L051xx, STM32L052xx, STM32L053xx, STM32L062xx, STM32L063xx, STM32L061xx, STM32F030, STM32F031, STM 32F051 và những người khác. Sau này, khi biên dịch stm32l0xx.h, tệp được yêu cầu với định nghĩa của các thiết bị ngoại vi của bộ điều khiển stm32l0yyxx.h sẽ tự động được đưa vào (stm32l051xx.h/stm32l052xx.h/stm32l053xx.h/stm32l061xx.h/stm32l062xx.h/stm32l063). Thứ hai, bạn cần chọn lập trình viên phù hợp trong cài đặt thuộc tính dự án. Thứ ba, thay đổi code của các hàm trong ví dụ nếu chúng không đáp ứng được yêu cầu của ứng dụng người dùng.

Phần kết luận

Các bộ đoạn mã và thư viện ngoại vi tiêu chuẩn do ST Microelectronics tạo ra không loại trừ lẫn nhau. Chúng bổ sung cho nhau, tăng thêm tính linh hoạt khi tạo ứng dụng.

Thư viện tiêu chuẩn cho phép bạn nhanh chóng tạo mã rõ ràng với mức độ trừu tượng cao.

Đoạn mã cho phép bạn cải thiện hiệu quả mã - tăng hiệu suất và giảm dung lượng bộ nhớ FLASH và RAM bị chiếm dụng.

Văn học

  1. Tóm tắt dữ liệu. STM32Đoạn F0. Gói chương trình cơ sở STM32F0xx Snippets. Rev. 1. – ST Vi Điện Tử, 2014.
  2. Tóm tắt dữ liệu. STM32Đoạn tríchL0. Gói chương trình cơ sở STM32F0xx Snippets. Rev. 1. – ST Vi Điện Tử, 2014.
  3. Thỏa thuận cấp phép MCD-ST Liberty SW V2.pdf Rơle điện cơ. Thông tin kĩ thuật. – ST Vi Điện Tử, 2011.
  4. Tóm tắt dữ liệu. 32L0538DISCOVERY Bộ khám phá dành cho bộ vi điều khiển STM32L053. Rev. 1. – ST Vi Điện Tử, 2014.
  5. http://www.st.com/.
Giới thiệu về ST Vi Điện Tử

Tôi đã chỉ ra rằng thư viện tiêu chuẩn đã được kết nối với hệ thống. Trên thực tế, CMSIS được kết nối - hệ thống biểu diễn cấu trúc tổng quát của MK, cũng như SPL - thư viện ngoại vi tiêu chuẩn. Chúng ta hãy nhìn vào từng người trong số họ:

CMSIS
Nó là một tập hợp các tệp tiêu đề và một tập hợp mã nhỏ để thống nhất và cấu trúc công việc với lõi và ngoại vi của MK. Trên thực tế, nếu không có những file này thì MK không thể hoạt động bình thường được. Bạn có thể lấy thư viện trên trang MK.
Theo mô tả, thư viện này được tạo ra để thống nhất các giao diện khi làm việc với bất kỳ MK nào thuộc họ Cortex. Tuy nhiên, trên thực tế, điều này chỉ đúng với một nhà sản xuất, tức là. Bằng cách chuyển sang bộ vi điều khiển của một công ty khác, bạn buộc phải nghiên cứu các thiết bị ngoại vi của nó gần như từ đầu.
Mặc dù các tệp liên quan đến lõi bộ xử lý của MK đều giống hệt nhau từ tất cả các nhà sản xuất (nếu chỉ vì chúng có cùng một mẫu lõi bộ xử lý - được cung cấp dưới dạng khối IP bởi ARM).
Do đó, làm việc với các phần của kernel như thanh ghi, lệnh, ngắt và bộ đồng xử lý là tiêu chuẩn cho tất cả mọi người.
Về phần ngoại vi, STM32 và STM8 (đột nhiên) gần như giống nhau, và điều này cũng đúng một phần với các MK khác do ST ra mắt. Trong phần thực hành, tôi sẽ chỉ ra việc sử dụng CMSIS dễ dàng như thế nào. Tuy nhiên, những khó khăn trong việc sử dụng nó gắn liền với việc mọi người ngại đọc tài liệu và hiểu thiết kế MK.

SPL
Thư viện ngoại vi tiêu chuẩn - thư viện ngoại vi tiêu chuẩn. Đúng như tên gọi, mục đích của thư viện này là tạo ra sự trừu tượng cho vùng ngoại vi của MK. Thư viện bao gồm các tệp tiêu đề trong đó các hằng số mà con người có thể đọc được để định cấu hình và làm việc với các thiết bị ngoại vi MK, cũng như các tệp mã nguồn được thu thập vào chính thư viện để hoạt động với các thiết bị ngoại vi.
SPL là một bản tóm tắt của CMSIS, cung cấp cho người dùng một giao diện chung cho tất cả các MCU không chỉ từ một nhà sản xuất mà nói chung là tất cả các MCU có lõi bộ xử lý Cortex-Mxx.
Người ta tin rằng nó thuận tiện hơn cho người mới bắt đầu, bởi vì... cho phép bạn không nghĩ về cách hoạt động của các thiết bị ngoại vi, nhưng chất lượng của mã, tính phổ biến của cách tiếp cận và ràng buộc của giao diện đặt ra những hạn chế nhất định đối với nhà phát triển.
Ngoài ra, chức năng của thư viện không phải lúc nào cũng cho phép bạn triển khai chính xác cấu hình của một số thành phần như USART (cổng nối tiếp đồng bộ-không đồng bộ chung) trong một số điều kiện nhất định. Trong phần thực hành, tôi cũng sẽ mô tả cách làm việc với phần này của thư viện.

Danh sách các bài viết sẽ giúp ngay cả người mới bắt đầu tìm hiểu bộ vi điều khiển STM32. Thông tin chi tiết về mọi thứ với các ví dụ từ đèn LED nhấp nháy đến điều khiển động cơ không chổi than. Các ví dụ sử dụng SPL tiêu chuẩn (Thư viện ngoại vi tiêu chuẩn).

Test board STM32F103, bộ lập trình ST-Link và phần mềm firmware cho Windows và Ubuntu.

VIC (Bộ điều khiển ngắt vectơ lồng nhau) – mô-đun điều khiển ngắt. Thiết lập và sử dụng ngắt. Ưu tiên ngắt quãng. Các ngắt lồng nhau.

ADC (bộ chuyển đổi tương tự sang số). Sơ đồ cung cấp điện và ví dụ về cách sử dụng ADC ở các chế độ khác nhau. Các kênh thông thường và được tiêm. Sử dụng ADC với DMA. Nhiệt kế bên trong. Cơ quan giám sát tương tự.

Bộ hẹn giờ mục đích chung. Tạo ra một ngắt đều đặn. Đo thời gian giữa hai sự kiện.

Thu tín hiệu bằng bộ hẹn giờ bằng ví dụ làm việc với cảm biến siêu âm HC-SR04

Sử dụng bộ hẹn giờ để làm việc với bộ mã hóa.

Tạo ra xung điện xung. Điều khiển độ sáng LED. Điều khiển truyền động servo (servos). Tạo âm thanh.