JQ8900-16P语音模块与天空星GD32F407串口通信及一线控制移植实战
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.3VJQ8900的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为TXPA10为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模块的驱动移植。