Hệ thống loa phát thanh là kênh truyền thông khá phổ biến tại các vùng ngoại thành, nông thôn Việt Nam. Nhằm ứng dụng công nghệ 3G/4G vào hệ thống loa phát thanh với mục đích nâng cao chất lượng hoạt động và hiệu quả truyền tải thông tin, mình đã tham gia một nhóm thực hiện đề tài “Nghiên cứu, thiết kế và thi công hệ thống phát thanh thông tin qua mạng 3G/4G” tại Cộng đồng Kỹ thuật TAPIT. Nhóm mình đã hoàn thành đề tài này dưới sự hướng dẫn và hỗ trợ từ các anh chị trong cộng đồng.
Trong chuỗi bài viết này, chúng ta sẽ cùng xây dựng các chức năng cơ bản của hệ thống phát thanh thông tin sử dụng vi điều khiển STM32. Nhiều ngoại vi được áp dụng trong đề tài như UART, SPI, DAC, TIM (PWM)… nhằm phục vụ cho việc giao tiếp giữa vi điều khiển với các module bên ngoài như Module thẻ nhớ SD Card, Module SIM, Module khuếch đại âm thanh. Mục đích cuối cùng sẽ hướng dẫn các bạn thực hiện được chức năng phát âm thanh từ thẻ nhớ, lưu file âm thanh vào thẻ nhớ, giới thiệu cấu trúc hệ thống webserver và thực hiện Demo chức năng của hệ thống.
Các tài nguyên mình sẽ sử dụng trong chuỗi bài viết này:
- Board vi điều khiển STM32 Nucleo F303RE.
- Module thẻ nhớ SD Card, giao tiếp SPI.
- Module khuếch đại âm thanh PAM8403 Stereo 3W x 2 kèm một loa 3W – 4Ohm.
- Các phần mềm mình sử dụng: STM32CubeIDE và Hercules.
Hình 1. a – Board STM32 Nucleo F303RE b- Module thẻ nhớ c- Module PAM8403
Các phần chính trong chuỗi bài viết:
- Phát âm thanh bằng PWM và DAC.
- Giao tiếp thẻ nhớ SD Card bằng SPI và sử dụng hệ thống tệp FATFS.
- Cách truyền file qua giao tiếp UART và lưu vào thẻ nhớ.
- Phát file âm thanh từ thẻ nhớ và phát ra loa.
- Cấu trúc hệ thống webserver và Demo.
Phần 1. Phát âm thanh bằng PWM và DAC
Trong phần này, chúng ta sẽ cùng thực hiện giải pháp phát âm thanh bằng PWM và DAC. Ở đây, file âm thanh được sử dụng có định dạng .wav.
1. Giới thiệu về file âm thanh .wav
File .wav là một loại tệp âm thanh phổ biến, xuất hiện lần đầu trên hệ điều hành Windows. Phần mở rộng này là viết tắt của “waveform”. File .wav hỗ trợ nhiều kích thước âm thanh, tần số lấy mẫu và kênh âm thanh. Đặc điểm của nó là ghi lại các dạng sóng âm thanh tự nhiên mà không nén dữ liệu và có kích thước dữ liệu lớn. Chất lượng của âm thanh được khôi phục từ file .wav phụ thuộc vào kích thước của mẫu và tần số lấy mẫu âm thanh. Tần số lấy mẫu càng cao, chất lượng càng tốt nhưng kéo theo kích thước cũng lớn hơn.
Một số khái niệm cần nắm:
- Tần số lấy mẫu: số lượng mẫu âm thanh ghi lại trong mỗi giây, các tần số thường gặp như 8000, 12000, 16000, 22050, 44100,… đơn vị là Hz. Tần số lấy mẫu càng cao thì chất lượng âm thanh càng tốt.
- Số lượng bit lấy mẫu (độ chính xác lấy mẫu theo biên độ): là giá trị lấy mẫu, dùng để đo dao động của âm thanh, giá trị càng lớn thì độ phân giải càng cao, âm thanh tạo ra càng mạnh. Chúng ta thường gặp loại 8 bit và 16 bit.
- Số kênh: số kênh như kiểu mono (đơn âm) tức là chúng ta chỉ có thể nghe bằng một loa; kiểu stereo (âm thanh nổi) có thể phát ra hai loa cùng lúc.
Về cấu tạo: file .wav sử dụng cấu trúc định dạng RIFF, nó bao gồm ít nhất 3 khối: RIFF, FMT và DATA. Mỗi khối như vậy gọi là một chunk. Mỗi file đều bắt đầu bằng 44 byte thông tin, trong đó 4 byte đầu tiên mặc định là 52H 49H 46H 46H (RIFF).
File .wav mình sử dụng là loại tệp âm thanh 8bit, tần số lấy mẫu là 16kHz và chế độ mono. Các bạn có thể tạo cho mình một file âm thanh với giá trị thông số như mình trình bày vì nó đảm bảo chất lượng âm thanh vừa đủ nghe, mà kích thước dữ liệu lại không quá lớn. Các bạn vào link sau để chuyển đổi thành một file .wav: https://bit.ly/3qa7Jx4
Chọn tệp âm thanh của bạn, kéo thả vào. Sau đó, thiết lập các giá trị như trong hình dưới đây, và bấm nút Start Conversion để bắt đầu chuyển đổi. Các bạn chú ý là trong bài viết này, mình sẽ thực hiện phát âm thanh với tần số lấy mẫu 16kHz, kích thước âm thanh là 8bit. Nếu các bạn chọn khác đi thì trong phần cấu hình STM32 được mình trình bày bên dưới sẽ phải tính toán khác mới có thể phát được.
2. Sử dụng PWM để phát âm thanh
Các bạn có thể tham khảo bài viết hướng dẫn sử dụng tính năng PWM trên khối ngoại vi Timer tại đây.
Bước 1: Tạo mới một project, chọn vi điều khiển bạn đang sử dụng. Mình sử dụng board STM32F303RE Nucleo. Các bạn đặt tên cho project, sau đó nhấn Finish.
Chú ý, bộ nhớ Flash của mỗi vi điều khiển sẽ ảnh hưởng đến thời lượng lưu trữ âm. Với âm thanh 16kHz tức trong một giây âm thanh sẽ tiêu tốn 16kByte bộ nhớ để lưu trữ.
Bước 2: Tại mục System Core, chọn SYS, chọn Serial Wire để cấu hình chân nạp code.
Bước 3: Ở thẻ Clock Configuration, cấu hình xung clock là 72MHz. Để ý xung clock APB1 và APB2 đều là 72MHz, điều này ảnh hưởng đến việc tính toán đến chu kỳ PWM.
Bước 4: Cấu hình, sử dụng TIM1 dùng để xuất ra xung PWM, với tần số sóng mang mình chọn là 32KHz. Vì kích thước âm thanh của chúng ta là 8 bit, nên.
Cấu hình:
- Clock Source: Internal Clock
- Channel 1: PWM Generation CH1 CH1N
- Prescaler: 8
- Counter Period: 256 – 1
Bước 5: Cấu hình, sử dụng TIM2 để tạo ngắt timer có tần số bằng với giá trị tốc độ lấy mẫu của tệp âm thanh. Mình sử dụng tệp âm thanh là 16KHz.
Tức ngắt TIM2 xảy ra sau mỗi 62,5us. Xung clock cấp cho TIM2 là 72MHz, chọn PSC bằng 36. Suy ra ARR = 128.
Cấu hình:
- Clock Source: Internal Clock
- Prescaler: 36 – 1
- Counter Period: 128 – 1
- Bật ngắt: TIM2 globle interrupt
Bước 6: Bấm SAVE (Ctrl + S), hoặc biểu tượng sinh code để tạo mã chương trình.
Bước 7: Vào đường dẫn https://bit.ly/3qa7Jx4 để tạo một tệp .wav. Như đã nói, tuỳ theo bộ nhớ của vi điều khiển mà thời gian sẽ dài ngắn khác nhau. Cứ 1 giây chiếm khoảng 16KByte. Với vi điều khiển của mình, mình chỉ cắt 13 giây. Nhấn Start Conversion để tải xuống.
Bước 8: Chuyển file .wav vừa tải xuống thành file hex. Các bạn vào đường dẫn https://bit.ly/3qjG25e, kéo thả tệp vào và chuyển đổi.
Bước 9: Copy dữ liệu trong khung, paste vào ô hex (bytes) ở trang: https://bit.ly/3iUqF1e để biết được kích thước của tệp âm thanh:
Bước 10: Quay lại phần mềm STM32CubeIDE, tạo một file header (.h)
Bấm Finish.
Nội dung của file header như sau:
1 2 3 4 5 6 |
#define AudioDataStartAddr 44 //địa chỉ bắt đầu sau 44 byte thông tin #define AudioDataEndAddr 208078 //số byte của tệp .wav const unsigned char Audio_data[208078] = {0x52, 0x49, 0x46, 0x46, 0xC6, 0x2C, 0x03, 0x00, 0x57, 0x41, 0x56, 0x45, 0x66, 0x6D, 0x74, 0x20, 0x10, 0x00, 0x00, 0x00, 0x01, 0x00, 0x01, 0x00, 0x80, 0x3E, …… }; |
Bước 11: Thêm thư viện mới tạo và viết các đoạn mã sau vào file main.c.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 |
/* USER CODE BEGIN Includes */ #include "wav.h" /* USER CODE END Includes */ … /* USER CODE BEGIN PV */ uint16_t PWM_Value; uint32_t DataCnt; /* USER CODE END PV */ … /* USER CODE BEGIN PFP */ void PWM_SetData(TIM_TypeDef * TIMx, uint16_t PWM_Data) { TIMx -> CCR1 = PWM_Data; //Đặt giá trị cho thanh ghi CCR1, để xuất ra giá trị PWM } /* * Mỗi lần sinh ra ngắt của TIM2, giá trị DataCnt tăng lên 1 * Mỗi giây sẽ sinh ra 16000 lần ngắt, mỗi lần ngắt sẽ xuất ra trên chân PWM một giá trị lấy mẫu * Nếu DataCnt lớn hơn giá trị file .wav thì nó sẽ được gán về lại giá trị bắt đầu (byte 44) */ void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim) { switch ((uint32_t) (htim -> Instance)) { case (uint32_t) TIM1: break; case (uint32_t) TIM2: { DataCnt++; if(DataCnt >= AudioDataEndAddr) { DataCnt = AudioDataStartAddr; } PWM_Value = Audio_data[DataCnt]; PWM_SetData(TIM1, PWM_Value); } break; default: break; } } /* USER CODE END PFP */ … int main(void) { HAL_Init(); SystemClock_Config(); MX_GPIO_Init(); MX_DMA_Init(); MX_DAC1_Init(); MX_TIM6_Init(); /* USER CODE BEGIN 2 */ HAL_TIM_Base_Start_IT(&htim2); // khởi tạo ngắt cho TIM2 HAL_TIM_PWM_Start(&htim1, TIM_CHANNEL_1); //khởi tạo PWM cho TIM1 HAL_TIMEx_PWMN_Start(&htim1, TIM_CHANNEL_1); //khởi tạo chức năng PWM bổ sung TIM1 /* USER CODE END 2 */ while (1) { /* USER CODE END WHILE */ /* USER CODE BEGIN 3 */ } /* USER CODE END 3 */ } … |
Bước 12: Compile và nạp code. Các bạn nối dây với module khuếch đại âm thanh, trong đó GND sẽ nối với G của mạch khuếch đại, chân TIM1_CH1N nối với chân tín hiệu. Sau đó có thể nghe tín hiệu âm thanh phát ra là được.
Như vậy, mình đã hoàn thành phần hướng dẫn, phát file âm thanh định dạng .wav bằng PWM. Các bạn có thể phát triển hơn với chương trình cụ thể của các bạn. Ngay sau đây, mình sẽ trình bày, việc phát âm thanh bằng DAC thì sẽ như thế nào nha.
3. Sử dụng DAC để phát âm thanh
Để có thể sử dụng được DAC thì trước tiên, bạn hãy kiểm tra xem vi điều khiển của mình có hỗ trợ DAC hay không, vì một số board không có chức năng này. Ở phần này, mình vẫn sẽ tận dụng lại tệp header đã tạo trước đó. Mình sẽ tạo mới một project, các bước tạo mới các bạn có thể tự tiến hành hoặc xem lại các bước ở phần 2 nhỏ ở trên. Sau khi tạo project
Bước 1: System Core, chọn SYS, chọn Serial Wire.
Bước 2: Ở thẻ Clock Configuration, chọn max xung clock là 72MHz. Tuỳ vào board của bạn.
Bước 3: Cấu hình DAC, như hình vẽ
Chú ý, bật Timer 6 Trigger Out envent. Bật DMA của DAC lên.
Bước 4: Cấu hình TIM6, tần số lấy mẫu âm thanh của chúng ta là 16KHz, nên mình sẽ cấu hình Timer 6 như sau:
- Tick vào Activated
- Prescaler: 36 – 1
- Counter Period: 128 – 1
- Trigger Event Selection chọn Update Event
Bước 5: Thực hiện sinh code. Trong file main.c ta thêm các đoạn mã sau:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 |
/* USER CODE BEGIN Includes */ #include "wav.h" #include "string.h" #include "stdio.h" /* USER CODE END Includes */ … /* USER CODE BEGIN PD */ #define buf_size 1024 /* USER CODE END PD */ … /* USER CODE BEGIN PV */ uint8_t buf[buf_size] = {0}; //khai báo một bộ đệm, có kích thước bằng buf_size /* USER CODE END PV */ … // viết hàm phát âm thanh void playWave(void) { uint32_t dataLength = 0; uint8_t* dataAddr = NULL; dataLength = sizeof(Audio_data) - 44; //độ lớn của âm thanh thực = kích thước tổng - 44 byte header dataAddr = (unsigned char*) (Audio_data + 44); memset(buf, 0, buf_size); // xoá bộ đệm HAL_TIM_Base_Start(&htim6); // bật TIM6 while(1) { //Nếu kích thước của file .wav lớn hơn bộ đệm, thì copy từng khối dữ liệu có kích thước bằng buf_size vào bộ đệm, sử dụng DMA của DAC để phát ra âm thanh. if(dataLength >= buf_size) { memcpy(buf, dataAddr, buf_size); dataLength -= buf_size; //tổng kích thước – kích thước bộ đệm dataAddr += buf_size; //địa chỉ bắt đầu mới sẽ tăng lên sau mỗi lần copy xong HAL_DAC_Start_DMA(&hdac1, DAC_CHANNEL_1, (uint32_t*) buf, buf_size, DAC_ALIGN_8B_R); while(HAL_DAC_GetState(&hdac1) != HAL_DAC_STATE_READY); } else break; } HAL_TIM_Base_Stop(&htim6); //dừng TIM6 HAL_DAC_Stop_DMA(&hdac1, DAC_CHANNEL_1); //dừng DMA của DAC } … int main(void) { HAL_Init(); SystemClock_Config(); MX_GPIO_Init(); MX_DMA_Init(); MX_DAC1_Init(); MX_TIM6_Init(); /* USER CODE BEGIN 2 */ // goi hàm để phát, phát lại ba lần for(int i = 0; i < 3; i++) { playWave(); } /* USER CODE END 2 */ while (1) { /* USER CODE END WHILE */ /* USER CODE BEGIN 3 */ } /* USER CODE END 3 */ } |
Bước 6: Compile và nạp code. Các bạn nối dây với module khuếch đại âm thanh, trong đó GND sẽ nối với G của mạch khuếch đại, chân DAC1_OUT1 nối với chân tín hiệu. Sau đó có thể nghe tín hiệu âm thanh phát ra là được.
Nhìn chung, qua hai cách đều phát ra được âm thanh nghe rất là tốt. Tuy nhiên, do hạn chế của dung lượng bộ nhớ trong vi điều khiển, thời gian phát được cũng rất ngắn (dưới 15s – tuỳ vi điều khiển). Để khắc phục điều này, ta sẽ tiếp cận với các bộ nhớ ngoài lớn hơn như thẻ nhớ hoặc USB. Tiếp phần sau, mình sẽ hướng dẫn giao tiếp STM32 với module thẻ nhớ SD Card, cùng xây dựng các hàm quản lý file và làm việc với thư viện FATFS. Chúc các bạn thành công!
Nhóm thực tập tại Cộng đồng Kỹ thuật TAPIT
Hoàng Văn Bình