CẨM NANG STM32 CĂN BẢN CẦN BIẾT – USB Serial

Thiết bị USB Serial

Với MCU được flash và cắm vào hệ thống, bạn cần truy cập nó trên hệ điều hành của bạn như một thiết bị serial. Nó thay đổi theo hệ điều hành, điều này làm phức tạp hơn vấn đề. Mã nguồn MCU được tìm thấy trong thư mục sau:

$ cd ~/stm32f103c8t6/rtos/usbcdcdemo
$ make clobber
$ make
$ make flash

Các bước tiếp theo sẽ build và flash code vào thiết bị MCU của bạn. Các phần sau đây sẽ mô tả chi tiết về phần máy tính trong một kết nối USB.

Thiết bị nối tiếp USB Linux

Trong Linux, với STM32 được flash và cắm vào cổng USB, bạn có thể sử dụng lệnh lsusb để xem các thiết bị được kết nối:

$ lsusb

Bus 002 Device 003: ID 0483:5740 STMicroelectronics STM32F407

Trong ví dụ này, tôi chỉ có một thiết bị. Đừng lo lắng về ký hiệu STM32F407. Đây chỉ là mô tả được cung cấp cho ID thiết bị 0483:5740 mà ST Microelectronics đã đăng ký. Nhưng làm thế nào để bạn tìm ra đường dẫn thiết bị để sử dụng? Hãy thử những điều sau đây sau khi cắm cáp của bạn:

$ dmesg | grep 'USB ACM device'
[  709.468447] cdc_acm 2-7:1.0: ttyACM0: USB ACM device

Điều này rõ ràng là không thuận tiện cho người dùng, nhưng từ đây bạn thấy rằng tên thiết bị là /dev/ttyACM0. Liệt kê (listing) xác nhận điều này:

$ dmesg | grep 'USB ACM device'
[  709.468447] cdc_acm 2-7:1.0: ttyACM0: USB ACM device

Vấn đề tiếp theo là có quyền sử dụng thiết bị. Lưu ý rằng nhóm của thiết bị là dialout. Thêm chính bạn vào nhóm dialout (thay ID người dùng của riêng bạn):

$ sudo usermod -a -G dialout fred

Đăng xuất và đăng nhập lại để xác minh rằng bạn có nhóm chính xác:

$ ls -l /dev/ttyACM0
crw-rw---- 1 root dialout 166, 0 Jan 25 23:38 /dev/ttyACM0

Là thành viên của nhóm dialout giúp bạn không phải sử dụng quyền truy cập root để truy cập thiết bị serial.

Thiết bị USB Serial trong MacOS

Có lẽ cách đơn giản nhất để tìm thiết bị USB trong MacOS là chỉ cần liệt kê các thiết bị được gọi:

$ ls -l /dev/cu.*

crw-rw-rw- 1 root wheel 35,  1  6 Jan 15:14 /dev/cu.Bluetooth-Incoming-Port

crw-rw-rw- 1 root wheel 35,  3  6 Jan 15:14 /dev/cu.FredsiPhone-Wireless

crw-rw-rw- 1 root wheel 35, 45 26 Jan 00:01 /dev/cu.usbmodemFD12411

Đối với bản demo USB, thiết bị mới sẽ xuất hiện dưới dạng đường dẫn /dev/cu.usbmodemFD12411. Mã thiết bị có thể khác nhau, vì vậy hãy tìm cu.usbmodem trong tên đường dẫn. Lưu ý là tất cả phải được cấp quyền.

Thiết bị USB Serial trong Windows

Các thiết bị serial trong Windows hiển thị dưới dạng thiết bị COM trong Trình quản lý Thiết bị (Device Manager) khi cáp được cắm và trình điều khiển được cài đặt. Hình 7-5 là một ảnh chụp màn hình ví dụ.

Hình 7-5. Ví dụ thư mục Trình quản lý thiết bị Window

Trong ví dụ này, thiết bị USB được gắn với cổng COM3 của Windows. Nếu bạn đang sử dụng Cygwin trong Window, tên đường dẫn thiết bị là /dev/ttyS2 (số của cổng COM trừ đi 1).

USB GPIO

Serie STMF103 chỉ hỗ trợ USB trên các chân GPIO PA11 (USB_DM) và PA12 (USB_DP). Không có cấu hình thay thế cho USB. Hơn nữa, không cần phải cấu hình PA11 và PA12, bởi vì chúng được tự động tiếp nhận khi thiết bị ngoại vi USB được bật. Đây là thiết bị ngoại vi duy nhất mà tôi biết rằng hoạt động theo cách này và là một chi tiết nhỏ ẩn trong hướng dẫn tham khảo RM0008 về cấu hình thay thế. Tuy nhiên, bạn cần bật xung nhịp cho GPIOA và thiết bị ngoại vi USB.

=> Bài viết được trích từ sách : Cẩm nang STM32 (tập 1)

Mã nguồn demo

Trước khi chạy phần mềm demo được cung cấp, hãy xem xét một số phần code liên quan đến USB được tìm thấy trong thư mục (một lần nữa):

$ cd ~/stm32f103c8t6/rtos/usbcdcdemo

Code sẽ được thảo luận được tìm thấy trong mô-đun nguồn usbcdc.c. Bản kê 7-1 minh họa việc khởi tạo code cho thiết bị ngoại vi USB, sử dụng trình điều khiển libopencm3 và FreeRTOS cho các hàng đợi dữ liệu.

Bản kê 7-1. Hàm usb_start() để khởi tạo USB

0386: void
0387: usb_start(void) {
0388:   usbd_device *udev = 0;
0389:
0390:   usb_txq = xQueueCreate(128,sizeof(char));
0391:   usb_rxq = xQueueCreate(128,sizeof(char));
0392:
0393:   rcc_periph_clock_enable(RCC_GPIOA);
0394:   rcc_periph_clock_enable(RCC_USB);
0395:
0396:   // PA11=USB_DM, PA12=USB_DP
0397:   udev = usbd_init(&st_usbfs_v1_usb_driver,&dev,&config,
0398:       usb_strings,3,
0399:       usbd_control_buffer,sizeof(usbd_control_buffer));
0400:
0401:   usbd_register_set_config_callback(udev,cdcacm_set_config);
0402:
0403:   xTaskCreate(usb_task,"USB",200,udev,configMAX_PRIORITIES-1,NULL);
0404: }

Các dòng 390 và 391 tạo các hàng đợi FreeRTOS, sẽ được sử dụng để giao tiếp đến và đi từ dòng USB, theo thứ tự tương ứng.

Vì việc cho phép thiết bị ngoại vi USB tự động tiếp nhận các chân GPIO PA11 và PA12, tất cả những gì chúng ta phải làm là kích hoạt GPIO và xung USB trong các dòng 393 và 394. Sau khi thực hiện xong, chương trình libopencm3 usbd_init() thực hiện phần còn lại trong dòng 397 đến 399.

Khi thiết bị ngoại vi được khởi tạo, callback cdcacm_set_config() được đăng ký trong dòng 401. Cuối cùng, một tác vụ FreeRTOS được tạo ra trong dòng 403 để phục vụ các event USB.

cdcacm_set_config()

Khi bộ điều khiển máy chủ tiếp xúc với thiết bị ngoại vi USB, nó sẽ gọi đến hàm gọi lại được minh họa trong Bản kê 7-2 để cấu hình/tái cấu hình thiết bị USB CDC.

Bản kê 7-2. Gọi lại cdcadm_set_config()

0030: // True when USB configured:
0031: static volatile bool initialized = false;
...
0252: static void
0253: cdcacm_set_config(
0254:   usbd_device *usbd_dev,
0255:   uint16_t wValue __attribute__((unused))
0256: ) {
0257:
0258:   usbd_ep_setup(usbd_dev,
0259:       0x01,
0260:       USB_ENDPOINT_ATTR_BULK,
0261:       64,
0262:       cdcacm_data_rx_cb);
0263:   usbd_ep_setup(usbd_dev,
0264:       0x82,
0265:       USB_ENDPOINT_ATTR_BULK,
0266:       64,
0267:       NULL);
0268:   usbd_register_control_callback(
0269:       usbd_dev,
0270:       USB_REQ_TYPE_CLASS | USB_REQ_TYPE_INTERFACE,
0271:       USB_REQ_TYPE_TYPE | USB_REQ_TYPE_RECIPIENT,
0272:       cdcacm_control_request);
0273:
0274:   initialized = true;
0275: }

Từ dòng 258 đến 262, có thể thấy rằng gọi lại cdcacm_data_rx_cb() được đăng ký để nó có thể nhận dữ liệu. Từ máy chủ, đây là cổng OUT, do đó được chỉ định là điểm cuối 0x01 (điểm cuối 1 OUT).

Tiếp theo, các dòng 263 đến 267 đăng ký một điểm cuối khác, được coi là cổng IN từ góc nhìn của bộ điều khiển máy chủ. Do đó, điểm cuối 2 IN được chỉ định với bit cao trong hằng số 0x82.

Cuối cùng, các yêu cầu điều khiển sẽ gọi hàm callback cdcacm_control_request() được đăng ký trong các dòng 268 đến 272.

Cuối cùng, biến Boolean được khởi tạo initialized đặt thành true trong dòng 274 để các tác vụ khác có thể biết trạng thái sẵn sàng của kiến trúc hạ tằng USB.

cdc_control_request()

Kiến trúc hạ tầng USB sử dụng hàm callback cdcacm_control_request() để thực hiện với các thông báo riêng (Bản kê 7-3). Trình điều khiển này phản ứng với hai loại thông báo req->bRequest, đầu tiên trong số đó là để đáp ứng sự thiếu hụt Linux (dòng 203 đến 209).

Bản kê 7-3. Hàm callback cdcacm_control_request()

0190: static int
0191: cdcacm_control_request(
0192:   usbd_device *usbd_dev __attribute__((unused)),
0193:   struct usb_setup_data *req,
0194:   uint8_t **buf __attribute__((unused)),
0195:   uint16_t *len,
0196:   void (**complete)(
0197:     usbd_device *usbd_dev,
0198:     struct usb_setup_data *req
0199:   ) __attribute__((unused))
0200: ) {
0201:
0202:   switch (req->bRequest) {
0203:   case USB_CDC_REQ_SET_CONTROL_LINE_STATE:
0204:       /*
0205:        * The Linux cdc_acm driver requires this to be implemented
0206:        * even though it's optional in the CDC spec, and we don't
0207:        * advertise it in the ACM functional descriptor.
0208:        */
0209:       return 1;
0210:   case USB_CDC_REQ_SET_LINE_CODING:
0211:       if ( *len < sizeof(struct usb_cdc_line_coding) ) {
0212:           return 0;
0213:       }
0214:       return 1;
0215:   }
0216:   return 0;
0217: }

Các dòng từ 210 đến 214 kiểm tra chiều dài của một cấu trúc và trả về fail nếu độ dài nằm ngoài dòng (dòng 212). Ngược lại, trả về 1 cho biết trạng thái “đã xử lý” (dòng 214).

cdcacm_data_rx_cb()

Hàm callback này được gọi bởi kiến trúc hạ tầng USB khi dữ liệu đã được gửi qua bus đến MCU STM32. Điều đầu tiên được thực hiện trong dòng 228 là xác định dung lượng bộ đệm còn lại được gán cho biến rx_avail. Nếu không có đủ dung lượng trống, hàm gọi lại chỉ trả về, như ở dòng 233. Máy chủ sau đó sẽ lại gửi cùng một dữ liệu.

Nếu có chỗ cho dữ liệu, chúng ta quyết định dung lượng là bao nhiêu trong dòng 236. Các hàm gọi đến usbd_ep_ read_packet() trong dòng 239 khi đó có được một số hoặc tất cả các dữ liệu nhận được. Các dòng 241 đến 244 gửi nó đến hàng đợi nhận cho tác vụ nhận. Xem Bản kê 7-4.

Bản kê 7-4. Hàm Callback USB Nhận

0222: static void
0223: cdcacm_data_rx_cb(
0224:   usbd_device *usbd_dev,
0225:   uint8_t ep __attribute__((unused))
0226: ) {
0227:   // How much queue capacity left?
0228:   unsigned rx_avail = uxQueueSpacesAvailable(usb_rxq);
0229:   char buf[64];    // rx buffer
0230:   int len, x;
0231:
0232:   if ( rx_avail <= 0 )
0233:       return;      // No space to rx
0234:
0235:   // Bytes to read
0236:   len = sizeof buf < rx_avail ? sizeof buf : rx_avail;
0237:
0238:   // Read what we can, leave the rest:
0239:   len = usbd_ep_read_packet(usbd_dev,0x01,buf,len);
0240:
0241:   for ( x=0; x<len; ++x ) {
0242:       // Send data to the rx queue
0243:       xQueueSend(usb_rxq,&buf[x],0);
0244:   }
0245: }

=> xem thêm : Lập trình Arm STM32

Tác vụ USB

Tác vụ mà chúng ta đã tạo để xử lý USB là vòng lặp vô tận bắt đầu từ dòng 284. Vòng lặp phải gọi chương trình điều khiển libopencm3 thường xuyên usbd_poll() đủ để liên kết USB được duy trì bởi máy chủ. Điều này được thực hiện ở đầu vòng lặp trong dòng 285 của Bản kê 7-5. 

Bản kê 7-5. Hàm usb_task()

0278: static void
0279: usb_task(void *arg) {
0280:   usbd_device *udev = (usbd_device *)arg;
0281:   char txbuf[32];
0282:   unsigned txlen = 0;
0283:
0284:   for (;;) {
0285:       usbd_poll(udev); /* Allow driver to do its thing */
0286:       if ( initialized ) {
0287:           while ( txlen < sizeof txbuf
0288:              && xQueueReceive(usb_txq,&txbuf[txlen],0)
                           == pdPASS )
0289:               ++txlen; /* Read data to be sent */
0290:           if ( txlen > 0 ) {
0291:               if ( usbd_ep_write_packet(udev,0x82,
                                     txbuf,txlen) != 0 )
0292:                   txlen = 0; /* Reset if sent ok */
0293:           } else  {
0294:               taskYIELD(); /* Then give up CPU */
0295:           }
0296:       }
0297:   }
0298: }

Biến bool khả biến có tên initialized được kiểm tra trong dòng 286. Cho đến khi initialized vẫn còn là true, các lệnh gọi USB khác như usbd_ep_write_packet() không được thực hiện.

Sau khi trình điều khiển đã khởi tạo, kiểm tra hàng đợi phát được thực hiện trong các dòng 287 đến 289. Sẽ có tối đa các ký tự đợi lấy từ hàng chờ được gửi đi. Việc gửi dữ liệu USB xảy ra trong các dòng 290 đến 292. Nếu không có ký tự để truyền, lệnh FreeRTOS gọi tới taskYIELD() được thực hiện để cung cấp cho một tác vụ khác tài nguyên CPU.

Từ đây, bạn có thể thấy rằng mục đích của tác vụ này chỉ đơn giản là gửi bất kỳ byte hàng đợi của dữ liệu đến máy chủ USB. Việc nhận dữ liệu xảy ra từ một nơi khác.

=> Sách Arduino, ESP8266, STM32 : Sách tự động hóa