JQ8900-16P语音模块与天空星GD32F407串口通信及一线控制移植实战
最近在做一个需要语音提示的项目,用到了JQ8900-16P这个语音播报模块。我发现很多朋友在把它和GD32F407这类单片机搭配使用时,对串口和一线控制两种方式的移植有点摸不着头脑。今天我就结合在天空星开发板上的实际移植经验,手把手带大家走一遍完整的流程,从硬件连接到代码编写,再到功能验证,保证你能一次搞定。
咱们这篇教程的目标很明确:让你能在天空星GD32F407开发板上,通过串口或者一线控制的方式,随心所欲地控制JQ8900模块播放指定的语音。无论你是刚接触嵌入式的新手,还是想快速集成语音功能的老手,跟着步骤走,都能轻松实现。
1. 认识JQ8900-16P语音模块
在动手写代码之前,咱们先花几分钟了解一下要用的“主角”——JQ8900-16P模块。这能帮你更好地理解后面的操作。
JQ8900-16P是一个集成了语音芯片和SPI Flash的模块,最大的优点就是更换语音内容特别方便。传统的语音芯片往往需要专用的上位机软件来烧录音频文件,而这个模块直接把内部的SPI Flash模拟成了一个U盘。你只需要用USB线把它连到电脑上,电脑就会把它识别为一个可移动磁盘,然后像拷贝文件到U盘一样,把准备好的音频文件拖进去就行了,非常省事。
模块支持三种控制方式:
- 单独IO控制:每个语音文件对应一个IO引脚,拉低引脚就播放对应的语音。适合简单场景,但占用IO口多。
- 一线串行控制:只用一根数据线,通过特定的时序脉冲来发送控制指令。节省IO资源,但需要单片机用代码精确模拟时序。
- 两线串口控制:就是我们最常用的UART串口通信,只需要TX、RX两根线,按照固定的协议格式发送指令即可。稳定可靠,开发简单。
注意:模块的IO电平是3.3V,供电电压范围是2.8V-5.5V,和咱们的天空星开发板(3.3V系统)可以直接连接,不需要电平转换。
关于语音文件,有个小细节要注意:拷贝到模块里的音频文件,需要按5位数字的格式来命名,比如00001.mp3、00002.mp3……00010.mp3。模块出厂时已经自带了一些测试语音。另外,注意别放太大的文件,避免SPI Flash空间不够。
2. 硬件连接:把模块和开发板“接起来”
硬件连接是第一步,千万不能接错。天空星GD32F407开发板上有多个串口,我们这里选择USART1来连接JQ8900模块。
串口模式连接(两线控制):这是最常用的方式,连接非常简单:
- JQ8900的TX引脚-> 接GD32F407的PA3 (USART1_RX)
- JQ8900的RX引脚-> 接GD32F407的PA2 (USART1_TX)
- JQ8900的VCC引脚-> 接开发板的3.3V
- JQ8900的GND引脚-> 接开发板的GND
这里要理解一下,模块的TX(发送端)要接到单片机的RX(接收端),模块的RX要接到单片机的TX,这样数据才能交叉流通。
一线串行模式连接:如果你想尝试一线控制来节省一个IO口,可以这样接:
- JQ8900的APP引脚-> 接GD32F407的PC2(或其他任意一个GPIO)
- 电源和地线的接法和上面一样。
连接好之后,就可以上电了。
3. 工程搭建与基础串口驱动
在开始写JQ8900的专用代码前,我们需要一个稳定的串口通信基础。原始资料里提供了完整的bsp_uart.c和bsp_uart.h文件,用于配置USART0(通常用来连接电脑做调试打印)。我们直接把它移植到工程里就行。
移植步骤很简单,和添加其他外设驱动文件一样:
- 在你的工程目录下(比如
Drivers/BSP文件夹),新建bsp_uart.c和bsp_uart.h。 - 将原始内容提供的两段代码分别复制进去。
- 在工程管理软件(如Keil)中,把
bsp_uart.c文件添加到你的项目,并包含头文件路径。
这个基础串口驱动主要做了几件事:
- 配置了USART0的引脚(PA9为TX,PA10为RX)和通信参数(波特率可设,8位数据,1位停止,无校验)。
- 实现了发送单个字符和字符串的函数。
- 实现了中断接收功能,数据放在缓冲区
g_recv_buff里,并通过g_recv_complete_flag标志位通知主程序。 - 重写了
fputc函数,这样我们就可以直接用printf函数通过串口0打印信息到电脑,方便调试。
有了这个调试串口,我们就能在后续开发中通过printf实时查看程序运行状态,非常有用。
4. 编写JQ8900语音模块的驱动代码
核心部分来了,我们要创建JQ8900模块的专属驱动。主要工作集中在两个文件:bsp_jq8900.h(头文件,定义引脚和函数)和bsp_jq8900.c(源文件,实现函数)。
4.1 引脚与宏定义 (bsp_jq8900.h)
头文件里,我们首先要定义模块连接到的硬件资源。
#ifndef _BSP_JQ8900_H_ #define _BSP_JQ8900_H_ #include "gd32f4xx.h" #include "string.h" #include "board.h" //是否开启串口0调试,查看模块回显数据 #define DEBUG 1 #define JQ8900_RX_LEN_MAX 250 // 接收缓冲区最大长度 /**************************** 串口配置 ****************************/ // 我们使用 USART1 与 JQ8900 通信 #define RCU_JQ8900_TX RCU_GPIOA // 串口TX引脚(PA2)的时钟 #define RCU_JQ8900_RX RCU_GPIOA // 串口RX引脚(PA3)的时钟 #define RCU_JQ8900_USART RCU_USART1 // 串口1的时钟 #define PORT_JQ8900_TX GPIOA // 串口TX的端口 #define PORT_JQ8900_RX GPIOA // 串口RX的端口 #define GPIO_JQ8900_TX GPIO_PIN_2 // 串口TX的引脚:PA2 #define GPIO_JQ8900_RX GPIO_PIN_3 // 串口RX的引脚:PA3 #define BSP_JQ8900_AF GPIO_AF_7 // USART1的复用功能编号是7 #define JQ8900_USART USART1 // 使用的串口 #define JQ8900_USART_IRQ USART1_IRQn // 串口1中断号 #define JQ8900_USART_IRQHandler USART1_IRQHandler // 串口1中断服务函数名 /**************************** 一线串行引脚定义 ****************************/ #define RCU_JQ8900_APP RCU_GPIOC // 一线控制引脚时钟 #define PORT_JQ8900_APP GPIOC // 一线控制引脚端口 #define GPIO_JQ8900_APP GPIO_PIN_2 // 一线控制引脚:PC2 // 函数声明 void JQ8900_Init(void); void JQ8900_USART_send_String(unsigned char *str, unsigned int len); void SendData(unsigned char addr); #endif这里的关键是正确定义了USART1对应的引脚PA2和PA3,以及它们的复用功能GPIO_AF_7。一线控制引脚我们暂定为PC2,你可以根据实际接线修改。
4.2 串口初始化与通信函数 (bsp_jq8900.c)
接下来是具体的实现。首先是串口初始化函数JQ8900_USART_Init,它的作用和之前的usart_gpio_config类似,但专门针对连接JQ8900的USART1。
void JQ8900_USART_Init(unsigned int bund) { /* 开启相关时钟 */ rcu_periph_clock_enable(RCU_JQ8900_USART); // USART1时钟 rcu_periph_clock_enable(RCU_JQ8900_TX); // GPIOA时钟 rcu_periph_clock_enable(RCU_JQ8900_RX); /* 配置PA2和PA3为复用功能 */ gpio_af_set(PORT_JQ8900_TX, BSP_JQ8900_AF, GPIO_JQ8900_TX); gpio_af_set(PORT_JQ8900_RX, BSP_JQ8900_AF, GPIO_JQ8900_RX); /* 配置PA2和PA3为复用模式、上拉 */ gpio_mode_set(PORT_JQ8900_TX, GPIO_MODE_AF, GPIO_PUPD_PULLUP, GPIO_JQ8900_TX); gpio_mode_set(PORT_JQ8900_RX, GPIO_MODE_AF, GPIO_PUPD_PULLUP, GPIO_JQ8900_RX); /* 配置PA2和PA3为推挽输出,速度50MHz */ gpio_output_options_set(PORT_JQ8900_TX, GPIO_OTYPE_PP, GPIO_OSPEED_50MHZ, GPIO_JQ8900_TX); gpio_output_options_set(PORT_JQ8900_RX, GPIO_OTYPE_PP, GPIO_OSPEED_50MHZ, GPIO_JQ8900_RX); /* 配置串口参数:波特率、数据位、停止位、校验位 */ usart_deinit(JQ8900_USART); // 先复位串口 usart_baudrate_set(JQ8900_USART, bund); // 设置波特率,JQ8900默认9600 usart_parity_config(JQ8900_USART, USART_PM_NONE); // 无校验 usart_word_length_set(JQ8900_USART, USART_WL_8BIT); // 8位数据 usart_stop_bit_set(JQ8900_USART, USART_STB_1BIT); // 1位停止 /* 使能串口及其收发功能 */ usart_enable(JQ8900_USART); usart_transmit_config(JQ8900_USART, USART_TRANSMIT_ENABLE); usart_receive_config(JQ8900_USART, USART_RECEIVE_ENABLE); /* 配置中断:接收缓冲区非空中断和空闲中断 */ usart_interrupt_enable(JQ8900_USART, USART_INT_RBNE); usart_interrupt_enable(JQ8900_USART, USART_INT_IDLE); nvic_irq_enable(JQ8900_USART_IRQ, 2, 2); // 设置中断优先级 }初始化函数里,我使能了“接收缓冲区非空”和“空闲”两个中断。空闲中断特别有用,它能在一帧数据接收完成后(总线空闲)产生中断,让我们知道一串指令发完了,可以处理了。
然后是发送函数,我们实现了发送单个字节和发送字符串的函数:
void JQ8900_USART_Send_Bit(unsigned char ch) { usart_data_transmit(JQ8900_USART, ch); while(RESET == usart_flag_get(JQ8900_USART, USART_FLAG_TBE)); // 等待发送完成 } void JQ8900_USART_send_String(unsigned char *str, unsigned int len) { while(len--) { JQ8900_USART_Send_Bit(*str++); } }JQ8900_USART_send_String函数需要你传入字符串指针和长度,它会逐个字节发送出去。
4.3 一线串行通信模拟
一线控制模式是JQ8900的特色,它只用一根线,通过高低电平的不同持续时间来代表数据0和1。我们需要用单片机的一个GPIO口来精确模拟这个时序。
根据数据手册,一线通信的时序规则如下:
- 起始信号:先拉高至少500us,然后拉低至少4ms(建议4ms)。
- 数据位:每个数据位由两个脉冲表示。数据1是“高电平1300us + 低电平500us”,数据0是“高电平500us + 低电平1300us”。
- 字节间隔:两个字节之间,数据线要保持高电平,建议延时10ms以上。
我们来看代码实现:
void SendData(unsigned char addr) { unsigned char i; // 1. 起始信号:拉高500us gpio_bit_write(PORT_JQ8900_APP, GPIO_JQ8900_APP, SET); delay_us(500); // 2. 起始引导码:拉低4ms gpio_bit_write(PORT_JQ8900_APP, GPIO_JQ8900_APP, RESET); delay_ms(4); // 3. 发送8位数据,从最低位开始 for(i = 0; i < 8; i++) { gpio_bit_write(PORT_JQ8900_APP, GPIO_JQ8900_APP, SET); // 每个位都以高电平开始 if(addr & 0x01) // 判断当前最低位是1还是0 { // 发送 bit 1: 高电平1300us, 低电平500us delay_us(1300); gpio_bit_write(PORT_JQ8900_APP, GPIO_JQ8900_APP, RESET); delay_us(500); } else { // 发送 bit 0: 高电平500us, 低电平1300us delay_us(500); gpio_bit_write(PORT_JQ8900_APP, GPIO_JQ8900_APP, RESET); delay_us(1300); } addr >>= 1; // 数据右移,准备发送下一位 } // 4. 发送结束,拉高总线,并延时超过10ms gpio_bit_write(PORT_JQ8900_APP, GPIO_JQ8900_APP, SET); delay_ms(10); }这个SendData函数就是一线通信的核心。调用它时,传入一个字节的指令(比如播放第1首是0x01),它就会按照上述时序在APP引脚上产生对应的波形。这里对延时精度要求比较高,delay_us和delay_ms函数需要准确。
4.4 模块初始化与中断服务函数
最后,我们把串口初始化和一线控制引脚的初始化封装成一个总的初始化函数:
void JQ8900_Init(void) { JQ8900_USART_Init(9600); // 使用默认波特率9600初始化串口 // 初始化一线串行控制引脚PC2为输出模式 rcu_periph_clock_enable(RCU_JQ8900_APP); gpio_mode_set(PORT_JQ8900_APP, GPIO_MODE_OUTPUT, GPIO_PUPD_PULLUP, GPIO_JQ8900_APP); gpio_output_options_set(PORT_JQ8900_APP, GPIO_OTYPE_PP, GPIO_OSPEED_50MHZ, GPIO_JQ8900_APP); gpio_bit_write(PORT_JQ8900_APP, GPIO_JQ8900_APP, SET); // 初始化为高电平 }中断服务函数JQ8900_USART_IRQHandler用于接收模块可能返回的数据(比如播放状态),其逻辑和基础串口驱动里的类似,这里就不重复贴代码了。它会把接收到的数据存到JQ8900_RX_BUFF缓冲区,并设置完成标志JQ8900_RX_FLAG。
5. 功能验证:让模块“开口说话”
驱动写好了,最后一步就是在主函数里调用它们,测试模块是否正常工作。
#include "board.h" #include "bsp_jq8900.h" int main(void) { // 串口控制指令示例:下一曲 (指令格式: 0xAA 长度 命令 校验和) uint8_t send_buff[] = {0xAA, 0x06, 0x00, 0xB0}; int i = 0; nvic_priority_group_set(NVIC_PRIGROUP_PRE2_SUB2); // 设置中断优先级分组 board_init(); // 开发板基础初始化(系统时钟等) usart_gpio_config(115200U); // 初始化调试串口USART0,波特率115200 printf("System Start!\r\n"); // 通过串口0打印启动信息 JQ8900_Init(); // 初始化JQ8900模块(串口和一线控制引脚) while(1) { // 测试方法一:一线串口控制(需注释掉下面的串口测试代码) // 这段代码会依次播放第1到第9首语音 /* for(i = 1; i < 10; i++) { SendData(0x0a); // 一线指令:开始播放 SendData(i); // 播放第 i 首 SendData(0x0b); // 一线指令:结束 delay_ms(2000); // 等待2秒,播放完或间隔 } */ // 测试方法二:两线串口控制 // 发送“下一曲”指令,模块会播放下一首 JQ8900_USART_send_String(send_buff, 4); delay_ms(2000); // 每隔2秒发送一次“下一曲”指令 } }在上面的代码中,我提供了两种测试方法:
- 一线控制测试:通过
SendData函数发送三个字节的一线指令(0x0a,曲目号,0x0b),可以指定播放某一首。 - 串口控制测试:通过
JQ8900_USART_send_String发送一个4字节的串口指令{0xAA, 0x06, 0x00, 0xB0},这个指令是让模块播放“下一曲”。
提示:JQ8900的串口控制指令有固定的格式,通常是
0xAA(帧头)+长度+命令+校验和。具体的命令集需要查阅模块的数据手册。上面用的0xAA,0x06,0x00,0xB0是一个通用的“下一曲”指令。
实际现象:如果你使用串口测试代码,并且模块里存有多首语音,上电后你应该会听到模块每隔2秒就播放下一首语音,循环播放。这就说明我们的串口通信移植成功了!如果你想测试一线控制,只需要把串口测试代码注释掉,取消一线控制代码的注释,并确保APP引脚(PC2)正确连接到模块的APP引脚即可。
整个移植过程最关键的就是引脚定义别弄错,时序模拟要精确,指令格式要对得上。遇到问题,首先用万用表检查硬件连接,然后用调试串口多打印些信息,一步步排查。希望这篇教程能帮你顺利搞定JQ8900模块的驱动移植。