STM32F407(STM32F4-DISCOVERY) - Cách tiếp cận không chuẩn - Thư viện chuẩn phần 1. Kết nối thư viện ngoại vi tiêu chuẩn với bất kỳ họ STM32 nào

Cho đến thời điểm này, chúng tôi đã sử dụng thư viện kernel tiêu chuẩn - CMSIS. Để định cấu hình một cổng ở chế độ hoạt động mong muốn, chúng tôi phải chuyển sang tìm thanh ghi chịu trách nhiệm cho một chức năng nhất định, đồng thời tìm kiếm trong một tài liệu lớn để biết thông tin khác liên quan đến quá trình này. Mọi thứ sẽ còn phức tạp và thường xuyên hơn khi chúng ta bắt đầu làm việc với bộ đếm thời gian hoặc ADC. Số lượng thanh ghi ở đó lớn hơn nhiều so với số lượng cổng I/O. Việc cấu hình thủ công mất rất nhiều thời gian và làm tăng nguy cơ mắc lỗi. Vì vậy, nhiều người thích làm việc với thư viện ngoại vi tiêu chuẩn - StdPeriph. Nó cho cái gì? Thật đơn giản - mức độ trừu tượng tăng lên, bạn không cần phải đi sâu vào tài liệu và suy nghĩ về các thanh ghi trong hầu hết các phần. Trong thư viện này, tất cả các chế độ hoạt động và thông số của ngoại vi MK đều được mô tả dưới dạng cấu trúc. Bây giờ, để định cấu hình một thiết bị ngoại vi, bạn chỉ cần gọi hàm khởi tạo thiết bị với cấu trúc đã điền.

Dưới đây là hình ảnh minh họa sơ đồ các mức độ trừu tượng.

Chúng tôi đã làm việc với CMSIS (nơi "gần" nhất với lõi) để hiển thị cách hoạt động của bộ vi điều khiển. Bước tiếp theo là thư viện chuẩn mà chúng ta sẽ học cách sử dụng ngay bây giờ. Tiếp theo là trình điều khiển thiết bị. Chúng được hiểu là các tệp *.c \ *.h cung cấp giao diện phần mềm thuận tiện để điều khiển mọi thiết bị. Ví dụ: trong khóa học này, chúng tôi sẽ cung cấp cho bạn trình điều khiển cho chip max7219 và mô-đun WiFi Esp8266.

Một dự án tiêu chuẩn sẽ bao gồm các tệp sau:


Tất nhiên, trước tiên, đây là các tệp CMSIS cho phép thư viện chuẩn hoạt động với kernel, chúng ta đã nói về chúng rồi. Thứ hai, các tập tin thư viện chuẩn. Và thứ ba, tập tin người dùng.

Các tệp thư viện có thể được tìm thấy trên trang dành riêng cho MK mục tiêu (đối với chúng tôi là stm32f10x4), trong phần Tài nguyên thiết kế(trong CooCox IDE, các tệp này được tải xuống từ kho lưu trữ của môi trường phát triển). Mỗi thiết bị ngoại vi tương ứng với hai tệp - tiêu đề (*.h) và mã nguồn (*.c). Bạn có thể tìm thấy mô tả chi tiết trong tệp hỗ trợ, nằm trong kho lưu trữ cùng với thư viện trên trang web.

  • stm32f10x_conf.h - tệp cấu hình thư viện. Người dùng có thể kết nối hoặc ngắt kết nối các mô-đun.
  • stm32f10x_ppp.h - tệp tiêu đề ngoại vi. Thay vì ppp có thể có gpio hoặc adc.
  • stm32f10x_ppp.c - trình điều khiển thiết bị ngoại vi được viết bằng ngôn ngữ C.
  • stm32f10x_it.h - tệp tiêu đề bao gồm tất cả các trình xử lý ngắt có thể có (nguyên mẫu của chúng).
  • stm32f10x_it.c là tệp mã nguồn mẫu chứa thường trình dịch vụ ngắt (ISR) dành cho các tình huống ngoại lệ trong Cortex M3. Người dùng có thể thêm ISR của riêng mình cho các thiết bị ngoại vi được sử dụng.

Thư viện tiêu chuẩn và các thiết bị ngoại vi có quy ước về cách đặt tên hàm và ký hiệu.

  • PPP là từ viết tắt của các thiết bị ngoại vi, chẳng hạn như ADC.
  • Các tệp hệ thống, tiêu đề và mã nguồn - bắt đầu bằng stm32f10x_.
  • Các hằng số được sử dụng trong một tệp được xác định trong tệp đó. Các hằng số được sử dụng trong nhiều tệp được xác định trong tệp tiêu đề. Tất cả các hằng số trong thư viện ngoại vi thường được viết bằng chữ UPPER.
  • Các thanh ghi được coi là hằng số và còn được gọi là chữ cái VỐN.
  • Tên hàm dành riêng cho thiết bị ngoại vi bao gồm từ viết tắt, chẳng hạn như USART_SendData() .
  • Để định cấu hình từng thiết bị ngoại vi, cấu trúc PPP_InitTypeDef được sử dụng và được chuyển đến hàm PPP_Init().
  • Để khởi tạo lại (đặt giá trị thành mặc định), bạn có thể sử dụng hàm PPP_DeInit().
  • Chức năng cho phép bạn bật hoặc tắt các thiết bị ngoại vi được gọi là PPP_Cmd().
  • Chức năng bật/tắt ngắt được gọi là PPP_ITConfig.

Bạn có thể xem lại danh sách đầy đủ trong tệp hỗ trợ thư viện. Bây giờ hãy viết lại đèn LED nhấp nháy bằng thư viện ngoại vi tiêu chuẩn!

Trước khi bắt đầu công việc, chúng ta hãy xem file stm32f10x.h và tìm dòng:

#xác định USE_STDPERIPH_DRIVER

Nếu bạn định cấu hình dự án từ đầu bằng cách sử dụng các tệp thư viện từ kho lưu trữ đã tải xuống, thì bạn sẽ cần bỏ ghi chú dòng này. Nó sẽ cho phép bạn sử dụng thư viện tiêu chuẩn. Định nghĩa này(macro) sẽ ra lệnh cho bộ tiền xử lý bao gồm tệp stm32f10x_conf.h:

#ifdef USE_STDPERIPH_DRIVER #include "stm32f10x_conf.h" #endif

Tập tin này chứa các mô-đun. Nếu bạn chỉ cần những cái cụ thể, hãy tắt những cái còn lại, điều này sẽ tiết kiệm thời gian trong quá trình biên dịch. Như bạn có thể đoán, chúng tôi cần các mô-đun RTC và GPIO (tuy nhiên, trong tương lai chúng tôi cũng sẽ cần _bkp.h, _flash, _pwr.h, _rtc.h, _spi.h, _tim.h, _usart.h):

#include "stm32f10x_flash.h" // cho init_pll() #include "stm32f10x_gpio.h" #include "stm32f10x_rcc.h"

Giống như lần trước, trước tiên bạn cần kích hoạt tính năng xung nhịp của cổng B. Việc này được thực hiện bằng hàm được khai báo trong stm32f10x_rcc.h:

Void RCC_APB2PeriphClockCmd(uint32_t RCC_APB2Periph, Trạng thái chức năng NewState);

Enum FunctionState được định nghĩa trong stm32f10x.h:

Typedef enum (DISABLE = 0, ENABLE = !DISABLE) Trạng thái chức năng;

Hãy khai báo cấu trúc để thiết lập nhánh của chúng ta (bạn có thể tìm thấy nó trong tệp stm32f10x_gpio.h):

Đèn LED GPIO_InitTypeDef;

Bây giờ chúng ta phải điền vào nó. Chúng ta hãy xem nội dung của cấu trúc này:

Cấu trúc Typedef ( uint16_t GPIO_Pin; GPIOSpeed_TypeDef GPIO_Speed; GPIOMode_TypeDef GPIO_Mode; ) GPIO_InitTypeDef;

Tất cả các bảng liệt kê và hằng số cần thiết có thể được tìm thấy trong cùng một tệp. Khi đó hàm init_leds() được viết lại sẽ có dạng sau:

Void led_init() ( // Kích hoạt xung nhịp RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB, ENABLE); // Khai báo cấu trúc và điền vào nó GPIO_InitTypeDef LED; LED.GPIO_Pin = GPIO_Pin_0; LED.GPIO_Speed ​​​= GPIO_Speed_2MHz; LED.GPIO_Mode = GPIO_Mode_ Out_PP; // Khởi tạo cổng GPIO_Init( GPIOB, &LED); )

Hãy viết lại hàm main():

Int main(void) ( led_init(); while (1) ( GPIO_SetBits(GPIOB, GPIO_Pin_0); delay(10000000); GPIO_ResetBits(GPIOB, GPIO_Pin_0); delay(10000000); ) )

Việc chính là cảm nhận thứ tự khởi tạo: bật đồng hồ ngoại vi, khai báo cấu trúc, điền cấu trúc, gọi phương thức khởi tạo. Các thiết bị ngoại vi khác thường được cấu hình theo cách tương tự.

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 chặng đường dài- tạo các khoảng trống tương ứng trong STM32CubeMX, thêm USB Middleware từ gói STM32F1Cube vào dự án của tôi. 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 giảm thiểu khối lượng bắt buộc mã để triển khai các phần của 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 là kiểm soát tất cả phần hệ thống Nó không hoạt động tốt 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 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 để vậy - nếu tôi cần cấu hình bằng cách nào đó mô-đun GPS và viết lệnh vào đó.

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, họ nói, chúng ta không thể đơn giản thiết lập chế độ, 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 chức năng hệ thống(giải thích tốt 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, chúng ta hãy đ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ực 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”. Hóa ra nó 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ẻ

Phần mềm cần thiết để phát triển. Trong bài viết này tôi sẽ cho bạn biết cách cấu hình và kết nối nó một cách chính xác. Tất cả các môi trường thương mại như IAR EWARM hoặc Keil uVision thường tự thực hiện việc tích hợp này, nhưng trong trường hợp của chúng tôi, mọi thứ sẽ phải được cấu hình thủ công, tốn nhiều thời gian cho nó. Ưu điểm là bạn có cơ hội hiểu cách mọi thứ hoạt động từ bên trong và trong tương lai, có thể tùy chỉnh linh hoạt mọi thứ cho chính mình. Trước khi bắt đầu thiết lập, hãy xem cấu trúc của môi trường mà chúng ta sẽ làm việc:

Eclipse sẽ được sử dụng để chỉnh sửa các tệp triển khai hàm một cách thuận tiện ( .c), các tệp tiêu đề ( .h), cũng như các tập tin biên dịch ( .S). Khi nói “tiện lợi”, ý tôi là việc sử dụng tính năng hoàn thiện mã, tô sáng cú pháp, tái cấu trúc, điều hướng qua các hàm và nguyên mẫu của chúng. Các tập tin được tự động đưa vào các trình biên dịch cần thiết để tạo ra mã đối tượng (trong các tập tin .o). Cho đến nay mã này không chứa địa chỉ tuyệt đối các biến và hàm và do đó không phù hợp để thực thi. Các tệp đối tượng kết quả được tập hợp lại với nhau bằng một trình liên kết. Để biết phần nào của không gian địa chỉ sẽ được sử dụng, bộ sưu tập sử dụng một tệp đặc biệt ( .ld), được gọi là tập lệnh liên kết. Nó thường chứa định nghĩa về địa chỉ phần và kích thước của chúng (phần mã được ánh xạ tới flash, phần biến được ánh xạ tới RAM, v.v.).

Cuối cùng, trình liên kết tạo ra một tệp .elf (Định dạng có thể thực thi và có thể liên kết), ngoài các hướng dẫn và dữ liệu còn chứa thông tin gỡ lỗi được trình gỡ lỗi sử dụng. Định dạng này không phù hợp để flash chương trình cơ sở thông thường bằng chương trình vsprog, vì định dạng này yêu cầu tệp hình ảnh bộ nhớ nguyên thủy hơn (ví dụ: Intel HEX - .hex). Để tạo ra nó, cũng có một công cụ từ bộ Sourcery CodeBench (arm-none-eabi-objcopy) và nó tích hợp hoàn hảo vào Eclipse bằng cách sử dụng plugin ARM đã cài đặt.

Để tự thực hiện việc gỡ lỗi, ba chương trình được sử dụng:

  1. Bản thân nhật thực, cho phép lập trình viên sử dụng tính năng gỡ lỗi một cách “trực quan”, duyệt qua các dòng, di chuột qua các biến để xem giá trị của chúng và các tiện ích khác
  2. arm-none-eabi-gdb - Máy khách GDB là trình gỡ lỗi được điều khiển ẩn bởi eclips (thông qua stdin) để phản hồi các hành động được chỉ định ở bước 1. Đổi lại, GDB kết nối với máy chủ OpenOCD Debug và tất cả các lệnh đầu vào được trình gỡ lỗi GDB dịch thành các lệnh có thể hiểu được đối với OpenOCD. kênh GDB<->OpenOCD được triển khai bằng giao thức TCP.
  3. OpenOCD là một máy chủ gỡ lỗi có thể giao tiếp trực tiếp với lập trình viên. Nó chạy trước máy khách và chờ kết nối TCP.

Lược đồ này có vẻ khá vô dụng đối với bạn: tại sao lại sử dụng máy khách và máy chủ riêng biệt và thực hiện dịch lệnh không cần thiết, nếu tất cả điều này có thể được thực hiện bằng một trình gỡ lỗi? Thực tế là về mặt lý thuyết, kiến ​​trúc như vậy cho phép trao đổi thuận tiện giữa máy khách và máy chủ. Ví dụ: nếu bạn cần sử dụng một lập trình viên khác thay vì Versaloon, nó sẽ không hỗ trợ OpenOCD, nhưng sẽ hỗ trợ một máy chủ Debug đặc biệt khác (ví dụ: texane/stlink dành cho lập trình viên stlink - nằm trong bảng gỡ lỗi STM32VLDiscovery), thì bạn sẽ chỉ chạy OpenOCD thay vì khởi chạy máy chủ cần thiết và mọi thứ sẽ hoạt động mà không cần bất kỳ chuyển động bổ sung nào. Đồng thời, tình huống ngược lại có thể xảy ra: giả sử bạn muốn sử dụng môi trường IAR EWARM cùng với Versaloon thay vì kết hợp Eclipse + CodeBench. IAR có ứng dụng khách Gỡ lỗi tích hợp sẵn, ứng dụng này sẽ liên hệ thành công với OpenOCD và quản lý nó, cũng như nhận dữ liệu cần thiết để phản hồi. Tuy nhiên, tất cả những điều này đôi khi chỉ còn trên lý thuyết, vì các tiêu chuẩn liên lạc giữa máy khách và máy chủ không được quy định chặt chẽ và có thể khác nhau ở một số nơi, nhưng các cấu hình tôi đã chỉ định với st-link+eclipse và IAR+versaloon đã hoạt động hiệu quả với tôi.

Thông thường máy khách và máy chủ chạy trên cùng một máy và kết nối đến máy chủ xảy ra tại máy chủ cục bộ: 3333(Đối với openocd), hoặc máy chủ cục bộ:4242(đối với texane/stlink st-util). Nhưng không ai ngăn cản bạn mở cổng 3333 hoặc 4242 (và chuyển tiếp cổng này trên bộ định tuyến tới mạng bên ngoài) và đồng nghiệp của bạn từ thành phố khác sẽ có thể kết nối và gỡ lỗi phần cứng của bạn. Thủ thuật này thường được sử dụng bởi những người nhúng làm việc tại các địa điểm từ xa nơi quyền truy cập bị hạn chế.

Bắt đầu nào

Khởi chạy nhật thực và chọn Tệp->Mới->Dự án C, chọn loại dự án ARM Linux GCC (Sorcery G++ Lite) và tên "stm32_ld_vl" (Nếu bạn có STV32VLDiscovery, thì sẽ hợp lý hơn nếu đặt tên là "stm32_md_vl") :

Nhấp vào Kết thúc và thu nhỏ hoặc đóng cửa sổ Chào mừng. Vậy là dự án đã được tạo và thư mục stm32_ld_vl sẽ xuất hiện trong không gian làm việc của bạn. Bây giờ nó cần được lấp đầy với các thư viện cần thiết.

Như các bạn hiểu từ tên project thì mình sẽ tạo một project cho view thước kẻ dòng giá trị mật độ thấp(LD_VL). Để tạo một dự án cho các bộ vi điều khiển khác, bạn phải thay thế tất cả các tệp và xác định tên của nó _LD_VL (hoặc_ld_vl) đến những thứ bạn cần, theo bảng:

Loại thước chỉ định Vi điều khiển (x có thể thay đổi)
Dòng giá trị mật độ thấp _LD_VL STM32F100x4 STM32F100x6
Mật độ thấp _LD STM32F101x4 STM32F101x6
STM32F102x4 STM32F102x6
STM32F103x4 STM32F103x6
Dòng giá trị mật độ trung bình _MD_VL STM32F100x8 STM32F100xB
Mật độ trung bình
_MD
STM32F101x8 STM32F101xB
STM32F102x8 STM32F102xB
STM32F103x8 STM32F103xB
Dòng giá trị mật độ cao _HD_VL STM32F100xC STM32F100xD STM32F100xE
Mật độ cao _HD STM32F101xC STM32F101xD STM32F101xE
STM32F103xC STM32F103xD STM32F103xE
Mật độ XL _XL STM32F101xF STM32F101xG
STM32F103xF STM32F103xG
Đường kết nối _CL STM32F105xx và STM32F107xx

Để hiểu logic đằng sau bảng, bạn phải làm quen với nhãn STM32. Nghĩa là, nếu bạn có VLDdiscovery, thì bạn sẽ phải thay thế mọi thứ được kết nối bằng _LD_VL bằng _MD_VL, vì chip STM32F100RB, thuộc dòng giá trị Mật độ trung bình, được hàn vào khám phá.

Thêm Thư viện thiết bị ngoại vi tiêu chuẩn CMSIS và STM32F10x vào dự án

CMSIS(Tiêu chuẩn giao diện phần mềm vi điều khiển Cortex) là một thư viện được tiêu chuẩn hóa để làm việc với các bộ vi điều khiển Cortex thực hiện cấp độ HAL (Lớp trừu tượng phần cứng), nghĩa là nó cho phép bạn trừu tượng hóa các chi tiết làm việc với các thanh ghi, tìm kiếm địa chỉ thanh ghi bằng cách sử dụng biểu dữ liệu, vân vân. Thư viện là một bộ mã nguồn bằng C và Asm. Phần cốt lõi của thư viện giống nhau đối với tất cả Cortex (Có thể là ST, NXP, ATMEL, TI hoặc bất kỳ ai khác) và được phát triển bởi ARM. Phần còn lại của thư viện chịu trách nhiệm về phần ngoại vi, phần này tự nhiên khác với nhà sản xuất khác nhau. Vì vậy, cuối cùng, thư viện hoàn chỉnh vẫn được nhà sản xuất phân phối, mặc dù phần kernel vẫn có thể được tải xuống riêng biệt từ trang web ARM. Thư viện chứa các định nghĩa địa chỉ, mã khởi tạo bộ tạo đồng hồ (có thể tùy chỉnh thuận tiện theo định nghĩa) và mọi thứ khác giúp lập trình viên không phải đưa vào dự án của mình một cách thủ công định nghĩa địa chỉ của tất cả các loại thanh ghi ngoại vi và xác định các bit của các giá trị của những thanh ghi này.

Nhưng những người đến từ ST đã đi xa hơn. Bên cạnh hỗ trợ CMSIS, họ còn cung cấp một thư viện khác cho STM32F10x có tên Thư viện thiết bị ngoại vi tiêu chuẩn(SPL), có thể được sử dụng ngoài CMSIS. Thư viện cung cấp nhanh hơn và dễ dàng truy cậpđến ngoại vi, đồng thời kiểm soát (trong một số trường hợp) hoạt động chính xác của ngoại vi. Đó là lý do tại sao dữ liệu thư viện thường được gọi là bộ trình điều khiển cho các mô-đun ngoại vi. Nó đi kèm với một gói ví dụ, được chia thành các danh mục dành cho các thiết bị ngoại vi khác nhau. Thư viện không chỉ có sẵn cho STM32F10x mà còn có sẵn cho các dòng sản phẩm khác.

Bạn có thể tải xuống toàn bộ SPL+CMSIS phiên bản 3.5 tại đây: STM32F10x_StdPeriph_Lib_V3.5.0 hoặc trên trang web ST. Giải nén kho lưu trữ. Tạo thư mục CMSIS và SPL trong thư mục dự án và bắt đầu sao chép các tệp vào dự án của bạn:

Sao chép cái gì

Sao chép ở đâu (xem xét
thư mục dự án là stm32_ld_vl)

Mô tả tập tin
Thư viện/CMSIS/CM3/
Hỗ trợ cốt lõi/ core_cm3.c
stm32_ld_vl/CMSIS/ core_cm3.c Mô tả lõi Cortex M3
Thư viện/CMSIS/CM3/
Hỗ trợ cốt lõi/ core_cm3.h
stm32_ld_vl/CMSIS/core_cm3.h Tiêu đề mô tả hạt nhân

ST/STM32F10x/ system_stm32f10x.c
stm32_ld_vl/CMSIS/system_stm32f10x.c Các hàm khởi tạo và
điều khiển đồng hồ
Thư viện/CMSIS/CM3/Hỗ trợ thiết bị/
ST/STM32F10x/ system_stm32f10x.h
stm32_ld_vl/CMSIS/system_stm32f10x.h Tiêu đề cho các chức năng này
Thư viện/CMSIS/CM3/Hỗ trợ thiết bị/
ST/STM32F10x/ stm32f10x.h
stm32_ld_vl/CMSIS/stm32f10x.h Mô tả cơ bản về thiết bị ngoại vi
Thư viện/CMSIS/CM3/Hỗ trợ thiết bị/
ST/STM32F10x/khởi động/gcc_ride7/
startup_stm32f10x_ld_vl.s
stm32_ld_vl/CMSIS/startup_stm32f10x_ld_vl.S
(!!! Chú ý đuôi file CAPITAL S)
Tệp bảng vectơ
ngắt và init-s trên asm
Dự án/STM32F10x_StdPeriph_Template/
stm32f10x_conf.h
stm32_ld_vl/CMSIS/ stm32f10x_conf.h Mẫu để tùy chỉnh
mô-đun ngoại vi

inc/ *
stm32_ld_vl/SPL/inc/ * Tệp tiêu đề SPL
Thư viện/STM32F10x_StdPeriph_Driver/
src/ *
stm32_ld_vl/SPL/src/ * Triển khai SPL

Sau khi sao chép, hãy vào Eclipse và thực hiện Làm mới trong menu ngữ cảnh của dự án. Kết quả là trong Project Explorer, bạn sẽ có cấu trúc giống như trong hình bên phải.

Bạn có thể nhận thấy rằng trong thư mục Libraries/CMSIS/CM3/DeviceSupport/ST/STM32F10x/startup/ có các thư mục dành cho các IDE khác nhau (các IDE khác nhau sử dụng các trình biên dịch khác nhau). Tôi chọn Ride7 IDE vì nó sử dụng trình biên dịch GNU Công cụ cho ARM Embedded, tương thích với Sourcery CodeBench của chúng tôi.

Toàn bộ thư viện được định cấu hình bằng bộ tiền xử lý (sử dụng định nghĩa), điều này sẽ cho phép bạn giải quyết tất cả các nhánh cần thiết ở giai đoạn biên dịch (hay đúng hơn là trước nó) và tránh tải trong hoạt động của chính bộ điều khiển (có thể là được quan sát nếu cấu hình được thực hiện trong RunTime). Ví dụ: tất cả các thiết bị đều khác nhau đối với các dòng khác nhau và do đó, để thư viện “biết” bạn muốn sử dụng dòng nào, bạn được yêu cầu bỏ ghi chú trong tệp stm32f10x.h một trong những định nghĩa (tương ứng với dòng của bạn):

/* #xác định STM32F10X_LD */ /*!< STM32F10X_LD: STM32 Low density devices */
/* #xác định STM32F10X_LD_VL */ /*!< STM32F10X_LD_VL: STM32 Low density Value Line devices */
/* #define STM32F10X_MD */ /*!< STM32F10X_MD: STM32 Medium density devices */

Và như thế...

Nhưng tôi không khuyên bạn nên làm điều này. Hiện tại, chúng tôi sẽ không chạm vào các tệp thư viện và chúng tôi sẽ xác định nó sau bằng cách sử dụng các cài đặt trình biên dịch trong Eclipse. Và sau đó Eсlipse sẽ gọi trình biên dịch bằng phím -D STM32F10X_LD_VL, điều này đối với bộ tiền xử lý hoàn toàn tương đương với tình huống nếu bạn không chú ý "#xác định STM32F10X_LD_VL". Vì vậy, chúng tôi sẽ không thay đổi mã, do đó, nếu muốn, một ngày nào đó bạn sẽ có thể chuyển thư viện sang một thư mục riêng và không sao chép nó vào thư mục của từng dự án mới.

Tập lệnh liên kết

Trong menu ngữ cảnh của dự án, chọn Mới->Tệp->Khác->Chung->Tệp, Tiếp theo. Chọn thư mục gốc của dự án (stm32_ld_vl). Nhập tên tệp "stm32f100c4.ld" (hoặc "stm32f100rb.ld" để khám phá). Bây giờ sao chép và dán vào Eclipse:

NHẬP(Reset_Handler) BỘ NHỚ ( FLASH (rx) : ORIGIN = 0x08000000, LENGTH = 16K RAM (xrw): ORIGIN = 0x20000000, LENGTH = 4K ) _estack = ORIGIN(RAM) + LENGTH(RAM); MIN_HEAP_SIZE = 0; MIN_STACK_SIZE = 256; SECTIONS ( /* Bảng vectơ ngắt */ .isr_vector: ( . = ALIGN(4); KEEP(*(.isr_vector)) . = ALIGN(4); ) >FLASH /* Mã chương trình và dữ liệu khác đi vào FLASH * / .text: ( . = ALIGN(4); /* Mã */ *(.text) *(.text*) /* Hằng số */ *(.rodata) *(.rodata*) /* ARM->Thumb và Thumb->ARM keo mã */ *(.glue_7) *(.glue_7t) KEEP (*(.init)) KEEP (*(.fini)) . = ALIGN(4); _etext = .; ) >FLASH . ARM.extab: ( *(.ARM.extab* .gnu.linkonce.armextab.*) ) >FLASH .ARM: ( __exidx_start = .; *(.ARM.exidx*) __exidx_end = .; ) >FLASH .ARM. thuộc tính: ( *(.ARM.attributes) ) > FLASH .preinit_array: ( PROVIDE_HIDDEN (__preinit_array_start = .); GIỮ (*(.preinit_array*)) PROVIDE_HIDDEN (__preinit_array_end = .); ) >FLASH .init_array: ( PROVIDE_HIDDEN (__init_array_start = .); GIỮ (*(SORT(.init_array.*))) GIỮ (*(.init_array*)) PROVIDE_HIDDEN (__init_array_end = .); ) >FLASH .fini_array: ( PROVIDE_HIDDEN (__fini_array_start = .); GIỮ (* (.fini_array*)) GIỮ (*(SORT(.fini_array.*))) PROVIDE_HIDDEN (__fini_array_end = .); ) >FLASH_sidata = .; /* Dữ liệu được khởi tạo */ .data: AT (_sidata) ( . = ALIGN(4); _sdata = .; /* tạo một ký hiệu toàn cục khi bắt đầu dữ liệu */ *(.data) *(.data*) . = ALIGN (4); _edata = .; /* xác định ký hiệu chung ở cuối dữ liệu */ ) >RAM /* Dữ liệu chưa được khởi tạo */ . = CÁNH(4); .bss: ( /* Điều này được phần khởi động sử dụng để khởi tạo phần .bss */ _sbss = .; /* xác định ký hiệu chung khi bắt đầu bss */ __bss_start__ = _sbss; *(.bss) *(.bss *) *(COMMON) .= ALIGN(4); _ebss = .; /* xác định ký hiệu chung ở cuối bss */ __bss_end__ = _ebss; ) >RAM CUNG CẤP(end = _ebss); CUNG CẤP(_end = _ebss); CUNG CẤP(__HEAP_START = _ebss); /* Phần User_heap_stack, dùng để kiểm tra xem còn đủ RAM không */ ._user_heap_stack: ( . = ALIGN(4); . = . + MIN_HEAP_SIZE; . = . + MIN_STACK_SIZE; . = ALIGN(4); ) >RAM / BỎ QUA/ : ( libc.a(*) libm.a(*) libgcc.a(*) ) )

L này Tập lệnh inker sẽ dành riêng cho bộ điều khiển STM32F100C4 (có 16 KB flash và 4 KB RAM), nếu bạn có tập lệnh khác, bạn sẽ phải thay đổi tham số LENGTH của vùng FLASH và RAM ở đầu tệp (đối với STM32F100RB, trong Discovery: Flash 128K và RAM 8K).

Lưu các tập tin.

Thiết lập bản dựng (Bản dựng C/C++)

Đi tới Dự án->Thuộc tính->Bản dựng C/C++->Cài đặt->Cài đặt công cụ và bắt đầu thiết lập các công cụ xây dựng:

1) Mục tiêu tiền nhiệm

Chúng tôi chọn lõi Cortex mà trình biên dịch sẽ hoạt động.

  • Bộ xử lý: Cortex-m3

2) Trình biên dịch ARM Sourcery Linux GCC C -> Bộ tiền xử lý

Chúng ta thêm hai định nghĩa bằng cách chuyển chúng qua khóa chuyển -D tới trình biên dịch.

  • STM32F10X_LD_VL - định nghĩa thước đo (tôi đã viết về định nghĩa này ở trên)
  • USE_STDPERIPH_DRIVER - thông báo cho thư viện CMSIS rằng nó nên sử dụng trình điều khiển SPL

3) Trình biên dịch ARM Sourcery Linux GCC C -> Thư mục

Thêm đường dẫn vào thư viện bao gồm.

  • "$(workspace_loc:/$(ProjName)/CMSIS)"
  • "$(workspace_loc:/$(ProjName)/SPL/inc)"

Bây giờ, ví dụ, nếu chúng ta viết:

#include "stm32f10x.h

Sau đó, trình biên dịch trước tiên phải tìm tệp stm32f10x.h trong thư mục dự án (anh ấy luôn làm điều này), anh ấy sẽ không tìm thấy nó ở đó và sẽ bắt đầu tìm kiếm trong thư mục CMSIS, đường dẫn mà chúng tôi đã chỉ ra và sẽ tìm thấy nó.

4) Trình biên dịch ARM Sourcery Linux GCC C -> Tối ưu hóa

Hãy kích hoạt tối ưu hóa chức năng và dữ liệu

  • -ffunction-phần
  • -fdata-phần

Do đó, tất cả các chức năng và thành phần dữ liệu sẽ được đặt trong các phần riêng biệt và người thu thập sẽ có thể hiểu phần nào không được sử dụng và chỉ cần vứt chúng đi.

5) Trình biên dịch ARM Sourcery Linux GCC C -> Chung

Thêm đường dẫn đến tập lệnh liên kết của chúng tôi: “$(workspace_loc:/$(ProjName)/stm32f100c4.ld)” (hoặc bất cứ tên nào bạn gọi nó).

Và thiết lập các tùy chọn:

  • Không sử dụng các tệp bắt đầu tiêu chuẩn - không sử dụng các tệp bắt đầu tiêu chuẩn.
  • Xóa những phần không sử dụng - xóa những phần không sử dụng

Thế là xong, quá trình thiết lập đã hoàn tất. ĐƯỢC RỒI.

Chúng tôi đã làm rất nhiều việc kể từ khi dự án được tạo và có một số điều Eclipse có thể đã bỏ sót, vì vậy chúng tôi cần yêu cầu nó xem xét lại cấu trúc tệp của dự án. Để làm điều này từ danh mục dự án cần phải được thực hiện Lập chỉ mục -> xây dựng lại.

Xin chào đèn LED trên STM32

Đã đến lúc tạo ra tập tin chính dự án: Tệp -> Mới -> C/C++ -> Tệp nguồn. Kế tiếp. Tên tệp Tệp nguồn: main.c.

Sao chép và dán phần sau vào tập tin:

#include "stm32f10x.h" uint8_t i=0; int main(void) ( RCC->APB2ENR |= RCC_APB2ENR_IOPBEN; // Kích hoạt PORTB Đồng hồ ngoại vi RCC->APB1ENR |= RCC_APB1ENR_TIM2EN; // Kích hoạt đồng hồ ngoại vi TIM2 // Tắt JTAG để giải phóng mã PIN LED RCC->APB2ENR |= RCC_APB2ENR_AFIOEN; AFIO->MAPR |= AFIO_MAPR_SWJ_CFG_JTAGDISABLE; // Xóa các bit thanh ghi điều khiển PB4 và PB5 GPIOB->CRL &= ~(GPIO_CRL_MODE4 | GPIO_CRL_CNF4 | GPIO_CRL_MODE5 | GPIO_CRL_CNF5); // Định cấu hình PB.4 và PB.5 dưới dạng Kéo đẩy ở đầu ra tối đa 10Mhz GPIOB->CRL |= GPIO_CRL_MODE4_0 | GPIO_CRL_MODE5_0; TIM2->PSC = SystemCoreClock / 1000 - 1; // 1000 tích tắc/giây TIM2->ARR = 1000; // 1 Ngắt/1 giây TIM2->DIER |= TIM_DIER_UIE; // Kích hoạt ngắt tim2 TIM2->CR1 |= TIM_CR1_CEN; // Đếm bắt đầu NVIC_EnableIRQ(TIM2_IRQn); // Kích hoạt IRQ while(1); // Vòng lặp vô cực ) void TIM2_IRQHandler(void) ( TIM2->SR &= ~TIM_SR_UIF ; // Làm sạch cờ UIF if (1 == (i++ & 0x1)) ( GPIOB->BSRR = GPIO_BSRR_BS4; // Đặt bit PB4 GPIOB->BSRR = GPIO_BSRR_BR5; // Đặt lại bit PB5) khác ( GPIOB->BSRR = GPIO_BSRR_BS5; // Đặt GPIOB bit PB5->BSRR = GPIO_BSRR_BR4; // Đặt lại bit PB4 ) )

Mặc dù chúng tôi đã đưa vào thư viện SPL nhưng nó không được sử dụng ở đây. Tất cả lệnh gọi đến các trường như RCC->APB2ENR đều được mô tả đầy đủ trong CMSIS.

Bạn có thể thực hiện Dự án -> Xây dựng tất cả. Nếu mọi thứ đều ổn thì tệp stm32_ld_vl.hex sẽ xuất hiện trong thư mục Gỡ lỗi của dự án. Nó được tạo tự động từ elf bằng các công cụ tích hợp. Chúng tôi flash tệp và xem cách đèn LED nhấp nháy với tần suất một lần mỗi giây:

Vsprog -sstm32f1 -ms -oe -owf -I /home/user/workspace/stm32_ld_vl/Debug/stm32_ld_vl.hex -V "tvcc.set 3300"

Đương nhiên, thay vì /home/user/workspace/ bạn phải nhập đường dẫn đến không gian làm việc.

Dành cho STM32VLDiscovery

Mã này hơi khác so với mã tôi đưa ra ở trên cho bảng gỡ lỗi của mình. Sự khác biệt nằm ở các chân mà đèn LED “treo”. Nếu trên bo mạch của tôi là PB4 và PB5 thì trên Discovery là PC8 và PC9.

#include "stm32f10x.h" uint8_t i=0; int main(void) ( RCC->APB2ENR |= RCC_APB2ENR_IOPCEN; // Kích hoạt đồng hồ ngoại vi PORTC RCC->APB1ENR |= RCC_APB1ENR_TIM2EN; // Kích hoạt đồng hồ ngoại vi TIM2 // Xóa các bit thanh ghi điều khiển PC8 và PC9 GPIOC->CRH &= ~ (GPIO_CRH_MODE8 | GPIO_CRH_CNF8 | GPIO_CRH_MODE9 | GPIO_CRH_CNF9); // Định cấu hình PC8 và PC9 làm đầu ra Kéo đẩy ở mức tối đa 10Mhz GPIOC->CRH |= GPIO_CRH_MODE8_0 | GPIO_CRH_MODE9_0; TIM2->PSC = SystemCoreClock / 1000 - 1; // 1000 tích tắc/giây TIM2->ARR = 1000; // 1 Ngắt/giây (1000/100) TIM2->DIER |= TIM_DIER_UIE; // Cho phép ngắt tim2 TIM2->CR1 |= TIM_CR1_CEN; // Đếm bắt đầu NVIC_EnableIRQ(TIM2_IRQn); // Bật IRQ while(1); // Vòng lặp vô cực ) void TIM2_IRQHandler(void) ( TIM2->SR &= ~TIM_SR_UIF; // Xóa cờ UIF nếu (1 == (i++ & 0x1)) ( GPIOC->BSRR = GPIO_BSRR_BS8 ; // Đặt lại bit PC8 GPIOC->BSRR = GPIO_BSRR_BR9; // Đặt lại bit PC9 ) else ( GPIOC->BSRR = GPIO_BSRR_BS9; // Đặt lại bit PC9 GPIOC->BSRR = GPIO_BSRR_BR8; // Đặt lại bit PC8 ) )

Trong Windows, bạn có thể flash hex(/workspace/stm32_md_vl/Debug/stm32_md_vl.hex) kết quả bằng tiện ích từ ST.

Tốt lắm tiện ích linux st-flash. NHƯNG!!! Tiện ích không sử dụng định dạng hex Intel HEX (được tạo theo mặc định), vì vậy việc chọn định dạng nhị phân trong cài đặt tạo ảnh Flash là cực kỳ quan trọng:

Phần mở rộng của tệp sẽ không thay đổi (nó vẫn giữ nguyên dạng hex), nhưng định dạng tệp sẽ thay đổi. Và chỉ sau đó bạn có thể làm:

St-flash write v1 /home/user/workspace/stm32_md_vl/Debug/stm32_md_vl.hex 0x08000000

Nhân tiện, về phần mở rộng và định dạng: thông thường các tệp nhị phân được đánh dấu bằng phần mở rộng .bin, trong khi các tệp ở định dạng Intel HEX được gọi là phần mở rộng .hex. Sự khác biệt ở hai định dạng này mang tính kỹ thuật hơn là chức năng: định dạng nhị phân chỉ chứa các byte hướng dẫn và dữ liệu sẽ được lập trình viên ghi “nguyên trạng” vào bộ điều khiển. Mặt khác, IntelHEX không có định dạng nhị phân mà là định dạng văn bản: chính xác các byte giống nhau được chia thành 4 bit và được trình bày theo từng ký tự ở định dạng ASCII và chỉ sử dụng các ký tự 0-9, A-F (bin và hex là các hệ thống số có nhiều cơ số, nghĩa là 4 bit trên mỗi thùng có thể được biểu diễn dưới dạng một chữ số hex). Vì vậy, định dạng ihex có kích thước lớn hơn 2 lần so với định dạng thông thường tập tin nhị phân(cứ 4 bit được thay thế bằng byte + ngắt dòng để dễ đọc), nhưng nó có thể được đọc trong trình soạn thảo văn bản thông thường. Vì vậy, nếu bạn định gửi tệp này cho ai đó hoặc sử dụng nó trong các chương trình lập trình khác thì nên đổi tên thành stm32_md_vl.bin để không gây nhầm lẫn cho những người nhìn vào tên của nó.

Vì vậy, chúng tôi đã thiết lập bản dựng chương trình cơ sở cho stm32. Lần sau tôi sẽ chỉ cho bạn cách

Khi bạn mới bắt đầu lập trình vi điều khiển hoặc đã lâu chưa lập trình, việc hiểu được code của người khác là điều không dễ dàng. Câu hỏi "Đây là gì?" và "Cái này đến từ đâu?" xuất hiện trên hầu hết mọi sự kết hợp của chữ cái và số. Và việc hiểu logic "cái gì? tại sao? và ở đâu?" càng nhanh thì việc nghiên cứu mã của người khác, bao gồm cả các ví dụ, càng dễ dàng hơn. Đúng vậy, đôi khi để làm được điều này, bạn phải “nhảy qua mã” và “xem qua hướng dẫn sử dụng” trong hơn một ngày.

Tất cả các bộ vi điều khiển STM32F4xx đều có khá nhiều thiết bị ngoại vi. Mỗi thiết bị ngoại vi của bộ vi điều khiển được gán một vùng bộ nhớ cụ thể, cụ thể và không thể định vị lại. Mỗi vùng bộ nhớ bao gồm các thanh ghi bộ nhớ và các thanh ghi này có thể là 8 bit, 16 bit, 32 bit hoặc loại khác, tùy thuộc vào bộ vi điều khiển. Trong bộ vi điều khiển STM32F4, các thanh ghi này là 32 bit và mỗi thanh ghi có mục đích riêng và địa chỉ cụ thể riêng. Không có gì ngăn cản bạn truy cập chúng trực tiếp trong chương trình của bạn bằng cách chỉ ra địa chỉ. Thanh ghi này hoặc thanh ghi kia được đặt tại địa chỉ nào và nó thuộc về thiết bị ngoại vi nào được ghi trong thẻ nhớ. Đối với STM32F4, thẻ nhớ như vậy có trong tài liệu DM00031020.pdf, có thể tìm thấy trên st.com. Tài liệu đó được gọi là

RM0090
Hướng dẫn tham khảo
STM32F405xx/07xx, STM32F415xx/17xx, STM32F42xxx và STM32F43xxx MCU 32-bit dựa trên ARM tiên tiến

Trong chuong 2.3 Bản đồ bộ nhớở trang 64, một bảng bắt đầu bằng địa chỉ của các khu vực đăng ký và mối liên kết của chúng với thiết bị ngoại vi. Trong cùng một bảng có một liên kết đến một phần phân bổ bộ nhớ chi tiết hơn cho từng thiết bị ngoại vi.

Bảng bên trái hiển thị phạm vi địa chỉ, ở giữa là tên của thiết bị ngoại vi và ở cột cuối cùng là nơi mô tả chi tiết hơn về phân bổ bộ nhớ.

Vì vậy đối với các cổng I/O mục đích chung GPIO trong bảng cấp phát bộ nhớ, bạn có thể thấy rằng các địa chỉ được cấp cho chúng bắt đầu từ 0x4002 0000. Cổng I/O mục đích chung GPIOA chiếm phạm vi địa chỉ từ 0x4002 000 đến 0x4002 03FF. Cổng GPIOB chiếm dải địa chỉ 0x4002 400 - 0x4002 07FF. Và như thế.

Để xem phân phối chi tiết hơn trong chính phạm vi đó, bạn chỉ cần nhấp vào liên kết.

Ở đây cũng có một bảng nhưng có bản đồ bộ nhớ cho dải địa chỉ GPIO. Theo bản đồ bộ nhớ này, 4 byte đầu tiên thuộc về thanh ghi MODER, 4 byte tiếp theo thuộc về thanh ghi OTYPER, v.v. Địa chỉ đăng ký được tính từ đầu phạm vi thuộc về một cổng GPIO cụ thể. Nghĩa là, mỗi thanh ghi GPIO có một địa chỉ cụ thể có thể được sử dụng khi phát triển chương trình cho vi điều khiển.

Nhưng việc sử dụng địa chỉ đăng ký gây bất tiện cho con người và có nhiều lỗi. Do đó, các nhà sản xuất bộ vi điều khiển tạo ra các thư viện tiêu chuẩn giúp làm việc với bộ vi điều khiển dễ dàng hơn. Trong các thư viện này, địa chỉ vật lý được liên kết với ký hiệu chữ cái của chúng. Đối với STM32F4xx, các tương ứng này được chỉ định trong tệp stm32f4xx.h. Tài liệu stm32f4xx.h thuộc về thư viện CMSIS và nằm trong thư mục Libraries\CMSIS\ST\STM32F4xx\Include\.

Hãy xem cổng GPIOA được xác định như thế nào trong các thư viện. Mọi thứ khác được xác định tương tự. Chỉ cần hiểu nguyên tắc là đủ. Tài liệu stm32f4xx.h khá lớn và do đó tốt hơn nên sử dụng tìm kiếm hoặc các khả năng mà chuỗi công cụ của bạn cung cấp.

Đối với cổng GPIOA, chúng tôi tìm thấy dòng đề cập đến GPIOA_BASE

GPIOA_BASE được xác định thông qua AHB1PERIPH_BASE

AHB1PERIPH_BASE lần lượt được xác định thông qua PERIPH_BASE

Và lần lượt, PERIPH_BASE được định nghĩa là 0x4000 0000. Nếu bạn nhìn vào bản đồ phân bổ bộ nhớ của các thiết bị ngoại vi (trong phần 2.3 Bản đồ bộ nhớở trang 64), chúng ta sẽ thấy địa chỉ này ở cuối bảng. Các thanh ghi của tất cả các thiết bị ngoại vi của bộ vi điều khiển STM32F4 đều bắt đầu từ địa chỉ này. Tức là PERIPH_BASE là địa chỉ bắt đầu của toàn bộ ngoại vi của bộ vi điều khiển STM32F4xx nói chung và bộ vi điều khiển STM32F407VG nói riêng.

AHB1PERIPH_BASE được định nghĩa là tổng của (PERIPH_BASE + 0x00020000). (xem hình ảnh phía sau). Đây sẽ là địa chỉ 0x4002 0000. Trong thẻ nhớ, các cổng đầu vào/đầu ra GPIO cho mục đích chung bắt đầu tại địa chỉ này.

GPIOA_BASE được định nghĩa là (AHB1PERIPH_BASE + 0x0000), nghĩa là nó là địa chỉ bắt đầu của nhóm thanh ghi cổng GPIOA.

Chà, bản thân cổng GPIOA được định nghĩa là cấu trúc của các thanh ghi, vị trí của chúng trong bộ nhớ bắt đầu bằng địa chỉ GPIOA_BASE (xem dòng #define GPIOA ((GPIO_TypeDef *) GPIOA_BASE).

Cấu trúc của mỗi cổng GPIO được định nghĩa là GPIO_TypeDef.

Vì vậy, các thư viện tiêu chuẩn, trong trường hợp này là tập tin stm32f4xx.h, chỉ đơn giản là nhân bản hóa địa chỉ máy. Nếu bạn thấy mục GPIOA->ODR = 1234, thì điều này có nghĩa là số 1234 sẽ được ghi vào địa chỉ 0x40020014. GPIOA có địa chỉ bắt đầu là 0x40020000 và thanh ghi ODR có địa chỉ 0x14 tính từ đầu phạm vi, vì vậy GPIOA->ODR có địa chỉ 0x40020014.

Hoặc, ví dụ: bạn không thích mục GPIOA->ODR, thì bạn có thể xác định #define GPIOA_ODR ((uint32_t *) 0x40020014) và nhận được kết quả tương tự bằng cách viết GPIOA_ODR = 1234;. Nhưng điều này có ích lợi thế nào? Nếu bạn thực sự muốn giới thiệu các ký hiệu của riêng mình, thì tốt hơn là bạn chỉ cần gán lại các ký hiệu tiêu chuẩn. Bạn có thể xem cách thực hiện việc này trong tệp stm32f4_discovery.h Ví dụ: đây là cách xác định một trong các đèn LED ở đó:

#define LED4_PIN GPIO_Pin_12
#define LED4_GPIO_PORT GPIOD
#define LED4_GPIO_CLK RCC_AHB1Periph_GPIOD

Hơn miêu tả cụ thể ngoại vi của các cảng nằm ở stm32f4xx_gpio.h

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 analog sang kỹ thuật 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 (servo). Tạo âm thanh.