用Hi3861-wifi联网下载播放wav音乐 - 基于Harmony2.0

Posted HarmonyOS技术社区

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了用Hi3861-wifi联网下载播放wav音乐 - 基于Harmony2.0相关的知识,希望对你有一定的参考价值。

要做的事情,各个击破:

  • 一、为Hi3861使用Harmony2.0新代码
  • 二、规划GPIO
  • 三、搞定Hi3861的按键中断(用来切换工作模式)
  • 四、使用Harmony的i2c接口,让OLED ssd1306显示内容
  • 五、搞定Hi3861的wifi的ap/sta模式
    • 用来设置其sta的密码(tcp server)
    • 用来联网下载音乐(tcp client),ntp校时(udp client)
    • 使用HarmonyOS保存信息和文件到Hi3861上
  • 六、为Hi3861找一个音频编解码芯片,这里选用wm8978
    • 搞定wm8978的硬件连接
    • 使用Harmony的i2s接口,让wm8978播放音乐
  • 七、结尾
    • 简易展示视频

一、 使用Harmony新代码

1. 下载

笔者使用的Ubuntu20.04的编译环境。Linux环境,简单干净顺手易用,且不会存在Windows大小写的问题。

详细的环境搭建,若需要,请参考笔者的另一篇博客: 用HarmonyOS点亮LED - 基于RISC-V Hi3861开发板

OpenHarmony release主干代码获取, 轻量/小型/标准系统(2.0 Canary)源码的获取:(repo的安装和其他代码获取方式,请参考 OpenHarmony / docs
)

repo init -u https://gitee.com/openharmony/manifest.git -b master --no-repo-verify
repo sync -c
repo forall -c \'git lfs pull\'

还有一件事,笔者得提一嘴。运行HarmonyOS系统的设备分三类,轻量系统类设备(参考内存≥128KB)、小型系统类设备(参考内存≥1MB)、标准系统类设备(参考内存≥128MB)。我们的Hi3861的RAM为352KB, 应属于轻量系统类设备。

2. 编译

// 进入源码根目录
cd master

// 初次使用,得设置一下代码路径和平台。设置以后就不用再设置了
hb set

// 编译之前清一下工作目录, 避免不必要的问题
hb clean

//  使用-f就可以强制所有文件都重新编译,避免不必要的冲突问题
// 默认编译的是debug版本,如果想让串口输出干净一点,就可以跟上 -b release 编译干净的发布版本
hb build -f -b release

3. 下载和烧录

若需要,请参考笔者的另一篇博客: 用HarmonyOS点亮LED - 基于RISC-V Hi3861开发板

二、GPIO的规划

我们框选的gpio可能和HarmonOS中Hi3861平台代码中默认初始化的gpio有冲突,我们在接下来的使用中要注意修改这样的冲突。

1. GPIO概述

GPIO 是可编程的通用输入/输出接口,用于生成和采集特定应用的输入或输出信号,
实现系统和外设之间的通信,方便系统对外设的控制。 Hi3861芯片GPIO符合AMBA2.0的APB协议。

2. GPIO接口具有以下功能特点:

  • 时钟源可选择: 工作模式晶体时钟 24M/40M、低功耗模式 32K 时钟。
  • 1 组 GPIO,共 15 个独立的可配置管脚。
  • 每个 GPIO 管脚都可单独控制传输方向。
  • 每个 GPIO 可以单独被配置为外部中断源。
  • GPIO 用作中断时有 4 种中断触发方式,中断时触发方式可配:
    • 上升沿触发
    • 下降沿触发
    • 高电平触发
    • 低电平触发
  • GPIO 上报一个中断, CPU 查询上报的 GPIO 编号。
  • 每个中断支持独立屏蔽的功能,脉冲中断支持可清除功能

三、搞定Hi3861的按键中断

1. 引脚GPIO05

结合我们的Hi3861模块,最好用的就是“USER"按键了, 查看原理图,其对应GPIO05

2. 冲突

查看系统外设接口初始化代码:

device/hisilicon/hispark_pegasus/sdk_liteos/app/wifiiot_app/init/app_io_init.c  +27

但是GPIO05在系统初始化代码中,和UART1冲突了,并且默认UART1是打开的。

2.1 我们操作liteos的配置菜单(推荐),关闭有冲突设置
// 进入hispark_pegasus的liteos目录
cd device/hisilicon/hispark_pegasus/sdk_liteos

// 执行以下命令,打开字符终端
bash build.sh  menuconfig

键盘上下键选择"BSP Settings", 按回车进入。在其子菜单中,使用空格键去掉"Enable uart1 IO mux config"前的"*"号即可。

然后按"ESC"一直退出,最后按提示按下“Y"来保存设置

重新编译后设置生效。

2.2 我们也可以手动暴力修改配置文件(不推荐):

device/hisilicon/hispark_pegasus/sdk_liteos/build/config/usr_config.mk

把 CONFIG_UART1_SUPPORT=y 去掉, 保存,即可
# CONFIG_UART1_SUPPORT is not set

3. 代码逻辑

这里我们采用下降沿触发按键中断的方式。
但默认设置GPIO05的点平为低,所以我们不但需要配置GPIO05为中断管脚,还要把它从CPU内部拉高到高电平。
这样根据电路图当按键按下时,才会有下降沿的产生,才会触发中断处理函数。

    1. 初始化gpio05
    1. 设置gpio05为输入
    1. 设置gpio05内部拉高
    1. 注册gpio05的中断,中断方式为边沿(下降沿)触发,绑定中断处理函数

注意:
可能当前版本的HarmonyOS整合代码比较仓促,IoT层的代码中没有拉高电平的接口,我们需要从"hi_io.h"中引用。

按键处理的示例代码如下:
applications/sample/wifi-iot/app/hi3861_car/hi3861_key.c

#include <stdio.h>
#include <unistd.h>

#include "ohos_init.h"
#include "cmsis_os2.h"
#include "iot_gpio.h"
#include "hi_io.h"

static void *KeyInterruptHandler(const char *arg)
{
    printf("[debug] %s(%d)\\n", __func__, __LINE__);
    (void)arg;
}

static void KeyEntry(void)
{
    /* USER KEY <-> GPIO_5  <->  uart1 rx */
    // 1. init gpio05
    IoTGpioInit(HI_IO_NAME_GPIO_5);

    // 2. set gpio05\'s direction as input
    IoTGpiosetDir(HI_IO_NAME_GPIO_5, IOT_GPIO_DIR_IN);

    // 3. because of the falling edge interruption, please set gpio2 pull up internal, and please include "hi_io.h"
    hi_io_set_pull(HI_IO_NAME_GPIO_5, HI_IO_PULL_UP);

    // 4. register gpio interruption function
    IoTGpioRegisterIsrFunc(HI_IO_NAME_GPIO_5, IOT_INT_TYPE_EDGE, IOT_GPIO_EDGE_FALL_LEVEL_LOW, (GpioIsrCallbackFunc)KeyInterruptHandler, NULL);
}

SYS_RUN(KeyEntry);

修改当前目录的gn文件,添加按键处理的源文件"hi3861_key.c"编译到静态库"hi3861_app"中:
applications/sample/wifi-iot/app/hi3861_car/BUILD.gn

static_library("hi3861_app") {
    sources = [
        "hi3861_led.c",
        "hi3861_key.c",
    ]

    include_dirs = [
        "//utils/native/lite/include",
        "//kernel/liteos_m/kal/cmsis",
        "//base/iot_hardware/peripheral/interfaces/kits"
    ]
}

修改应用程序根目录的gn文件,关联"hi3861_car"目录下的静态库"hi3861_app":
applications/sample/wifi-iot/app/BUILD.gn


import("//build/lite/config/component/lite_component.gni")

lite_component("app") {
    features = [
        "startup",
        "hi3861_car:hi3861_app"
    ]
}

编译代码,烧录代码并重启,观察"USER"按键按下是的串口输出。

[debug] KeyInterruptHandler(12)

按键基本代码搞掂嘞! 芜湖~

四、使用Harmony的i2c接口,让OLED ssd1306显示内容

1. I2C硬件连接和协议基本概述

I2C(Inter Integrated Circuit)总线是由Philips公司开发的一种简单、双向二线制同步串行总线。

I2C以主从方式工作,通常有一个主设备和一个或者多个从设备,主从设备通过SDA(SerialData)串行数据线以及SCL(SerialClock)串行时钟线两根线相连,如下图所示。

I2C数据的传输必须以一个起始信号作为开始条件,以一个结束信号作为传输的停止条件。I2C的起始信号和结束信号(起始和结束条件,都是建立在SCL为高电平的基础上)。

标准的i2c协议,开始传递数据之前需要有个起始信号(CLK为高时,SDA由高拉低),第1字节是由7位机地址和1位读写位(读:1, 写:0)组成的,读写位表明了i2c的数据传输方向。

一旦收到1位应答位,数据就根据读写位规定的方向一个字节一个字节的开始传输。主机可以产生一个停止信号来结束数据的传输(CLK为高时,SDA由低拉高)。

I2C的SDA数据是在SCL为高时保持稳定有效,SCL为低时,可以进行SDA数据的变换。

2. 写SSD1306的I2C寄存器

查看其datasheet手册, 我们发现操作ssd1306的写方式为:

  • 起始信号 + (7位设备地址+1位读写位) + 寄存器地址 + 一字节信息 + 结束信号
  • ssd1036的8位地址为: 0x78
  • 寄存器地址有两个(0x00/0x40):0x00 --- 后面信息为命令, 0x40: 后面信息为数据

3. HarmonyOS2.0 操作Hi3861的i2c接口

前面gpio规划的时候,我们使用的是GPIO0/1 的i2c1来作为SDA/SCL

3.1 默认HarmonOS中是没有打开i2c的驱动, 我们需要打开它
3.1.1 使用sdk配置菜单,配置驱动
device/hisilicon/hispark_pegasus/sdk_liteos/app/wifiiot_app/init/app_io_init.c  +50

    /* I2C MUX: */
#ifdef CONFIG_I2C_SUPPORT
    /* I2C IO复用也可以选择3/4; 9/10,根据产品设计选择 */
    hi_io_set_func(HI_IO_NAME_GPIO_0, HI_IO_FUNC_GPIO_0_I2C1_SDA);
    hi_io_set_func(HI_IO_NAME_GPIO_1, HI_IO_FUNC_GPIO_1_I2C1_SCL);
#endif

我们仍然需要到hispark_pegasus的sdk_liteos目录中使用“bash build.sh menuconfig"选配I2C的支持

// 进入hispark_pegasus的liteos目录
cd device/hisilicon/hispark_pegasus/sdk_liteos

// 执行以下命令,打开字符终端
bash build.sh  menuconfig

键盘上下键选择"BSP Settings", 按回车进入。在其子菜单中,使用空格键添加"i2c driver support"前的"*"号即可。

然后按"ESC"一直退出,最后按提示按下“Y"来保存设置

重新编译后设置生效。

3.1.2 我们也可以手动暴力修改配置文件(不推荐):

device/hisilicon/hispark_pegasus/sdk_liteos/build/config/usr_config.mk

// 添加 CONFIG_I2C1SUPPORT=y, 保存,即可
CONFIG_I2C1SUPPORT=y
3.2 完成i2c的正确初始化
/* i2c1  <-> gpio0/1 */
IoTGpioInit(HI_IO_NAME_GPIO_0);
IoTGpioInit(HI_IO_NAME_GPIO_1);

hi_io_set_func(HI_IO_NAME_GPIO_0, HI_IO_FUNC_GPIO_0_I2C1_SDA);
hi_io_set_func(HI_IO_NAME_GPIO_1, HI_IO_FUNC_GPIO_1_I2C1_SCL);

//设置baudrate:400kbps
IoTI2cInit(HI_I2C_IDX_1, USR_I2C_BAUDRATE);
// IoTI2cSetBaudrate(HI_I2C_IDX_0, USR_I2C_BAUDRATE);
3.3 i2c的读写接口

#include "hi_i2c.h"

unsigned int IoTI2cWrite(unsigned int id, unsigned short deviceAddr, const unsigned char *data, unsigned int dataLen)

unsigned int IoTI2cRead(unsigned int id, unsigned short deviceAddr, unsigned char *data, unsigned int dataLen)

4. ssd1306的初始化代码示例如下

具体的配置请参考其数据手册,

#include <stdio.h>
#include <unistd.h>

#include "ohos_init.h"
#include "cmsis_os2.h"
#include "iot_gpio.h"
#include "iot_i2c.h"
#include "hi_io.h"
#include "hi_i2c.h"
#include "./include/coding_tbl.h"
#include "./include/hi3861_i2c.h"

#define OLED_REG_CMD                0x00
#define OLED_REG_DATA               0x40
#define OLED_SSD1306_MAX_COLUMN     128
// 8 bits slave address
#define OLED_SSD1306_ADDRESS        0x78

static unsigned int OledSsd1306WriteOneByte(const unsigned char reg_addr, const unsigned char ch)
{
    unsigned char data[] = {reg_addr, ch};
    return IoTI2cWrite(USR_I2C_BUSNO, OLED_SSD1306_ADDRESS, data, sizeof(data));
}

static void InitOledSsd1306()
{
    usleep(100*1000);                               
    OledSsd1306WriteOneByte(OLED_REG_CMD, 0xAE);    //--display off
    OledSsd1306WriteOneByte(OLED_REG_CMD, 0x00);    //---set low column address
    OledSsd1306WriteOneByte(OLED_REG_CMD, 0x10);    //---set high column address
    OledSsd1306WriteOneByte(OLED_REG_CMD, 0x40);    //--set start line address
    OledSsd1306WriteOneByte(OLED_REG_CMD, 0xB0);    //--set page address
    OledSsd1306WriteOneByte(OLED_REG_CMD, 0x81);    // contract control
    OledSsd1306WriteOneByte(OLED_REG_CMD, 0xFF);    //--128  
    OledSsd1306WriteOneByte(OLED_REG_CMD, 0xA1);    //set segment remap 
    OledSsd1306WriteOneByte(OLED_REG_CMD, 0xA6);    //--normal / reverse
    OledSsd1306WriteOneByte(OLED_REG_CMD, 0xA8);    //--set multiplex ratio(1 to 64)
    OledSsd1306WriteOneByte(OLED_REG_CMD, 0x3F);    //--1/32 duty
    OledSsd1306WriteOneByte(OLED_REG_CMD, 0xC8);    //Com scan direction
    OledSsd1306WriteOneByte(OLED_REG_CMD, 0xD3);    //-set display offset
    OledSsd1306WriteOneByte(OLED_REG_CMD, 0x00);
    OledSsd1306WriteOneByte(OLED_REG_CMD, 0xD5);    //set osc division
    OledSsd1306WriteOneByte(OLED_REG_CMD, 0x80);
    OledSsd1306WriteOneByte(OLED_REG_CMD, 0xD8);    //set area color mode off
    OledSsd1306WriteOneByte(OLED_REG_CMD, 0x05);
    OledSsd1306WriteOneByte(OLED_REG_CMD, 0xD9);    //Set Pre-Charge Period
    OledSsd1306WriteOneByte(OLED_REG_CMD, 0xF1);
    OledSsd1306WriteOneByte(OLED_REG_CMD, 0xDA);    //set com pin configuartion
    OledSsd1306WriteOneByte(OLED_REG_CMD, 0x12);
    OledSsd1306WriteOneByte(OLED_REG_CMD, 0xDB);    //set Vcomh
    OledSsd1306WriteOneByte(OLED_REG_CMD, 0x30);
    OledSsd1306WriteOneByte(OLED_REG_CMD, 0x8D);    //set charge pump enable
    OledSsd1306WriteOneByte(OLED_REG_CMD, 0x14);
    OledSsd1306WriteOneByte(OLED_REG_CMD, 0xAF);    //--turn on oled panel
}

void OledSsd1306SetPos(unsigned char x, unsigned char y) 
{ 
    OledSsd1306WriteOneByte(OLED_REG_CMD, 0xb0+y);
    OledSsd1306WriteOneByte(OLED_REG_CMD, ((x&0xf0)>>4)|0x10);
    OledSsd1306WriteOneByte(OLED_REG_CMD, x&0x0f);
}

void OledSsd1306ShowChar(unsigned char x, unsigned char y, unsigned char ch, unsigned char size)
{          
    unsigned char c = 0;
    unsigned char i = 0;

    c = ch - \' \';

    if (x > (OLED_SSD1306_MAX_COLUMN-1)) {
        x = 0;
        y = y+2;
    }

    if (size == 16) {
        OledSsd1306SetPos(x, y);    
        for (i=0; i<8; i++) {
            OledSsd1306WriteOneByte(OLED_REG_DATA, F8X16[c*16+i]);
        }

        OledSsd1306SetPos(x, y+1);
        for (i=0; i<8; i++) {
            OledSsd1306WriteOneByte(OLED_REG_DATA, F8X16[c*16+i+8]);
        }
    } else {    
        OledSsd1306SetPos(x, y);
        for (i=0; i<6; i++) {
            OledSsd1306WriteOneByte(OLED_REG_DATA, F6x8[c][i]);
        }            
     }
}

void OledSsd1306ShowString(unsigned char x, unsigned char y, unsigned char *str, unsigned char size)
{
    unsigned char j=0;

    if (NULL == str)
         return;
    while (str[j] != \'\\0\') {        
        OledSsd1306ShowChar(x, y, str[j], size);
        x += 8;
        if (x > OLED_SSD1306_MAX_COLUMN) {
            x = 0;
            y += 2;
        }
        j++;
    }
}

void InitOledSsd1306UI(void)
{
    InitOledSsd1306();
    OledSsd1306FillScreen(0x00);
    usleep(10*1000); 
}

static void *OledTask(const char *arg)
{
    (void)arg;
    InitOledSsd1306UI();
    OledSsd1306ShowString(9, 2, "(C) HenryHao", 1);
}

static void UiEntry(void)
{
    osThreadAttr_t attr;
    attr.name = "OledTask";
    attr.attr_bits = 0U;
    attr.cb_mem = NULL;
    attr.cb_size = 0U;
    attr.stack_mem = NULL;
    attr.stack_size = 4096;
    attr.priority = 25;
    if (osThreadNew((osThreadFunc_t)OledTask, NULL, &attr) == NULL) {
        printf("[Error] Falied to create %s(%d)!\\n", __func__, __LINE__);
    }
}
SYS_RUN(UiEntry);

然后就是在相关的gn文件中创建关联,编译烧录运行,查看oled显示:

五、搞定Hi3861的wifi的ap/sta模式

  • ap模式配置
    • 简单的tcp server编程,用来接收设置
  • sta模式配置
    • 简单的tcp client编程,用来下载wav音乐
    • 简答的udp client编程,用来ntp校时
  • 使用HarmonyOS保存信息和文件到Hi3861上

1. 目标:

Hi3861是一款wifi模组,笔者决定小小地手刃一下这个模组,拉它到网络世界走两步。

注意:根据芯片手册说明,因为Hi3861在ap/sta模式共存的时候,只能支持ap/sta分时复用,且需要先开sta再开ap才能不会导致ap模式的信道收到影响。所以这里没有采用ap/sta共存的方式。两种模式是分开使用的。

  • 使用HarmonyOS保存信息和文件到Hi3861上
  • ap模式配置
    • 简单的tcp server编程,用来接收设置
  • sta模式配置
    • 简单的tcp client编程,用来下载wav音乐
    • 简答的udp client编程,用来ntp校时

2. 保存信息

如果Hi3861能联网的话,我们肯定是要配置路由器的wifi账号和密码的,为了避免重复输入以及连接新的wifi设备。我们肯定需要有地方存放我们的配置。这时候Hi3861自带的2M珍贵存储空间就派上用场了。

那么问题来了,HarmonyOS2.0的liteOS中怎么存信息,并掉电不丢失嘞?

a. 首先我们需要到使能文件系统 (Canary2.0 对Hi3861默认打开了文件系统)

需要到hispark_pegasus的sdk_liteos目录中使用“bash build.sh menuconfig"选配文件系统的支持。保存退出

b. 我们可以使用 "kv_store.h" 中的接口

只要flash空间够用,UtilsSetValue()接口存储的信息,系统正常重启和掉电后,信息不丢失。

需要 #include "kv_store.h"

    // 存储字符串到文件系统中,以关键字来存取
    UtilsSetValue("your_keyword", "the_string_you_wanna_stored");

    // 从文件系统中获取关键字对应的指定长度的信息到buffer中
    UtilsGetValue("your_keyword", tmp_buffer, tmp_buffer_len);

    // 从文件系统中彻底删除关键字对应的信息
    UtilsDeleteValue("your_keyword");
c. 我们还可以使用libc中open/read/write/close的接口,存取文件

因为Hi3861的自带flash空间有限,存储空间要谨慎使用。

当然,HarmonyOS liteOS也有自己的文件系统的api,有兴趣的读者可以自行研究一下,此处略。因为这里已经可以使用libc的接口。

//介绍,略,包含头文件,略。 详见 Linux man手册
open()
read()
write()
close()

注意:
Hi3861自带存贮资源和内存有限,不建议在工程中包含大数组头文件(4K以上的音频数组文件),如果使用静态的大数组文件在工程中,liteOS在编译的时候没问题,运行的时候会有莫名的崩溃现象,很难解决。

3. ap模式

说白了就是Hi3861自己相当于一个wifi热点,可以和其他同类热点组网,也可以供别人来连接。
目前没用用来组网,只是用来组成一些简单(非标准的)的restful api,用来设置一些参数(e.g.sta的wifi账密)

3.1 打开ap模式

AP模式示例代码:

#include "hi_wifi_api.h"
#include "lwip/ip_addr.h"
#include "lwip/netifapi.h"
#include "lwip/sockets.h"

static struct netif *g_lwip_netif = NULL;

int hi_wifi_start_softap(void)
{
    int ret;
    errno_t rc;
    char ifname[WIFI_IFNAME_MAX_SIZE + 1] = {0};
    int len = sizeof(ifname);
    hi_wifi_softap_config hapd_conf = {0};
    const unsigned char wifi_vap_res_num = APP_INIT_VAP_NUM;
    const unsigned char wifi_user_res_num = APP_INIT_USR_NUM;
    ip4_addr_t st_gw;
    ip4_addr_t st_ipaddr;
    ip4_addr_t st_netmask;
    //指定热点名称
    unsigned char ssid[] = "Henry-Hi3861";

    /* 
    因为在 device/hisilicon/hispark_pegasus/sdk_liteos/app/wifiiot_app/src/app_main.c 中已经对 wifi 做了初始化,
    所以这里注释了初始化wifi的代码
    */
    // ret = hi_wifi_init(wifi_vap_res_num, wifi_user_res_num);
    // if (ret != HISI_OK) {
    //     printf("hi_wifi_init\\n");
    //     return -1;
    // }

    rc = memcpy_s(hapd_conf.ssid, HI_WIFI_MAX_SSID_LEN + 1, ssid, sizeof(ssid));
    if (rc != EOK) {
        return -1;
    }

    // 无需密码即可访问
    hapd_conf.authmode = HI_WIFI_SECURITY_OPEN;
    hapd_conf.channel_num = 1;

    // 启动ap模式
    ret = hi_wifi_softap_start(&hapd_conf, ifname, &len);
    if (ret != HISI_OK) {
        printf("[Error] hi_wifi_softap_start\\n");
        return -1;
    }

    /* acquire netif for IP operation */
    g_lwip_netif = netifapi_netif_find(ifname);
    if (g_lwip_netif == NULL) {
        printf("[Error] %s: get netif failed\\n", __FUNCTION__);
        return -1;
    }
    //设置AP模式的ip地址等信息
    IP4_ADDR(&st_ipaddr, 192,168,1,1);      /* input your IP for example: 192.168.1.1*/
    IP4_ADDR(&st_gw, 192,168,1,1);          /* input your gateway for example: 192.168.1.1*/
    IP4_ADDR(&st_netmask, 255,255,255,0);   /* input your netmask for example: 255.255.255.0*/
    netifapi_netif_set_addr(g_lwip_netif, &st_ipaddr, &st_netmask, &st_gw);

    //启动dhcp功能,连接的子设备可获取ip
    netifapi_dhcps_start(g_lwip_netif, 0, 0);

    return 0;
}
3.2 启动tcp server,监听/读取网络连接请求。

char hellowifiiot[] = "\\
<html>\\
<head>\\
<title>HarmonyOS Hi3861</title>\\
</head>\\
<body>\\
%s\\
</body>\\
</html>";

static int url_handler(const int client, char* url)
{
    int cnt = 0;
    char tmp[URL_FREGMENT_MAX_CNT][URL_FREGMENT_MAX_LENGTH] = {};
    char dst[URL_FREGMENT_MAX_CNT][URL_FREGMENT_MAX_LENGTH] = {};

    strcpy(dst[0], url);

    cnt = split(tmp, URL_FREGMENT_MAX_CNT, dst[0], " ");

    memset(dst, 0, sizeof(dst));
    cnt = split(dst, URL_FREGMENT_MAX_CNT, tmp[1], "//");

    char read_tmp_buf[128] = {}, msg[1024] = {};
    if (0 == strcasecmp("ssid", dst[0])) {
        UtilsSetValue(g_data.tag[USR_WIFI_SSID].name, dst[1]);
    } else if (0 == strcasecmp("passwd", dst[0])) {
        UtilsSetValue(g_data.tag[USR_WIFI_PASSWD].name, dst[1]);
    } 
    UtilsGetValue(g_data.tag[USR_WIFI_SSID].name, g_data.tag[USR_WIFI_SSID].value, UTILS_TAG_BUFFER_SIZE);
    UtilsGetValue(g_data.tag[USR_WIFI_PASSWD].name, g_data.tag[USR_WIFI_PASSWD].value, UTILS_TAG_BUFFER_SIZE);

    strcat(read_tmp_buf, "WIFI SSID: ");
    strcat(read_tmp_buf, g_data.tag[USR_WIFI_SSID].value);
    strcat(read_tmp_buf, " \\n");
    strcat(read_tmp_buf, "WIFI PASSWD: ");
    strcat(read_tmp_buf, g_data.tag[USR_WIFI_PASSWD].value);
    strcat(read_tmp_buf, " \\n");
    sprintf(msg, hellowifiiot, read_tmp_buf);

    write(client, msg, sizeof(msg) - 1);

    return 0;
}

static void http_task(void)
{
    int tcp_server_sockfd, client, size;
    struct sockaddr_in address, remotehost;
    /* create a TCP socket */
    if ((tcp_server_sockfd = socket(AF_INET, SOCK_STREAM, 0)) < 0) {
        printf("[Error] can not create socket\\n");
        return;
    }

    /* bind to port 80 at any inteRFace */
    address.sin_family = AF_INET;
    address.sin_port = htons(80);
    address.sin_addr.s_addr = INADDR_ANY;

    if (bind(tcp_server_sockfd, (struct sockaddr *)&address, sizeof(address)) < 0) {
        printf("[Error] can\'t bind socket\\n");
        close(tcp_server_sockfd);
        return;
    }

    /* listen for connections (TCP listen backlog = 1) */
    listen(tcp_server_sockfd, 1);
    size = sizeof(remotehost);
    while (1) {
        client = accept(tcp_server_sockfd, (struct sockaddr *)&remotehost, (socklen_t *)&size);
        if (client >= 0) {
                int buflen = 1024;
                int ret;
                unsigned char recv_buffer[1024];
                char buf[1024] = {};

                /* Read in the request */
                ret = read(client, recv_buffer, buflen);
                if (ret <= 0) {
                    close(client);
                    printf("[Error] read failed\\r\\n");
                    return;
                }

                url_handler(client, (char*)recv_buffer);

                /* Close connection client */
                close(client);
        } else {
            close(client);
        }
    }
    close(tcp_server_sockfd);
}
3.3 restful api访问

我们在手机或者电脑连上我们的Hi3861的wifi热点(Henry-Hi3861)

在浏览器的地址栏输入:

// 保存一下sta模式的wifi账密(账号:HUAWEI-Bamboo, 密码:abc123456)
http://192.168.1.1/ssid/HUAWEI-Bamboo
http://192.168.1.1/passwd/abc123456

然后我们读取网络数据的时候,再利用"UtilsGetValue()"来保存sta的账密设置,等到sta模式开启的时候,就可以拿到我们想要的wifi账密联网了。
这里也可以利用HarmonyOS应用端(北向开发)来通信,可以编写一个手机app来替代我们的restful api的访问形式。

4. sta模式

目前就是来联网,进行ntp时间更新 + 下载wav格式的音乐来播放

4.1 sta模式开启示例代码

#include "lwip/sockets.h"
#include "hi_wifi_api.h"
#include "lwip/ip_addr.h"
#include "lwip/netifapi.h"

int hi_wifi_start_connect(void)
{
    int ret = 0;
    hi_wifi_assoc_request assoc_req = {0};

    ret |= UtilsGetValue(g_data.tag[USR_WIFI_SSID].name, assoc_req.ssid, HI_WIFI_MAX_SSID_LEN);
    ret |= UtilsGetValue(g_data.tag[USR_WIFI_PASSWD].name, assoc_req.key, HI_WIFI_MAX_KEY_LEN);

    assoc_req.auth = HI_WIFI_SECURITY_WPA2PSK;

    usleep(2000*1000);

    ret |= hi_wifi_sta_connect(&assoc_req);
    if (ret != HISI_OK) {
        printf("[debug] wifi sta connect failed.\\n");
    }

    usleep(3000*1000);

    hi_wifi_status connect_status = {0};
    hi_wifi_sta_get_connect_info(&connect_status);
    printf("[debug-wifi] ssid: %s\\n", connect_status.ssid);
    printf("[debug-wifi] bssid: %s\\n", connect_status.bssid);
    printf("[debug-wifi] channel: %d\\n", connect_status.channel);
    printf("[debug-wifi] status: %d\\n", connect_status.status);

    return ret;
}

int hi_wifi_start_sta(void)
{
    hi_u32 event_bit;
    int ret;
    char ifname[WIFI_IFNAME_MAX_SIZE + 1] = {0};
    int len = sizeof(ifname);
    unsigned int  num = WIFI_SCAN_AP_LIMIT;

    /* use sta mode at first , if can\'t we just wait */

    // app_main.c already has initialized it
    ret = hi_wifi_deinit();
    if (ret != HISI_OK) {
        printf("[Error] failed to deinit wifi\\n");
    }
    const unsigned char wifi_vap_res_num = APP_INIT_VAP_NUM;
    const unsigned char wifi_user_res_num = APP_INIT_USR_NUM;
    ret = hi_wifi_init(wifi_vap_res_num, wifi_user_res_num);
    if (ret != HISI_OK) {
        printf("[Error] failed to reinit wifi\\n");
    }

    ret = hi_wifi_sta_start(ifname, &len);
    if (ret != HISI_OK) {
        printf("[Error] %s %s(%d)\\n", __FILE__, __func__, __LINE__);
    } else {
        /* register call back function to receive wifi event, etc scan results event,
        * connected event, disconnected event. */
        ret = hi_wifi_register_event_callback(wifi_wpa_event_cb);
        if (ret != HISI_OK) {
            printf("[Error] register wifi event callback failed\\n");
        } else {
            /* acquire netif for IP operation */
            g_lwip_netif = netifapi_netif_find(ifname);
            if (g_lwip_netif == NULL) {
                printf("[Error] %s: get netif failed\\n", __FUNCTION__);
            } else {
                /* start scan, scan results event will be received soon */
                ret = hi_wifi_sta_scan();
                if (ret != HISI_OK) {
                    printf("[Error] %s %s(%d)\\n", __FILE__, __func__, __LINE__);
                } else {
                    sleep(3);   /* sleep 3s, waiting for scan result. */

                    /* if received scan results, select one SSID to connect */
                    ret = hi_wifi_start_connect();
                }
            }
        }
    }
    return ret;
}
4.2 成功联网后就可以访问网络啦

访问百度以及从国家授时中心更新时间,就可以随便造了。

wav文件我是自己传到自己的阿里对象存贮空间里,然后就可以随便低频率访问了。
ntp 授时的话,就是udp通信,比较简单。下载的话,就是tcp client,都可以走标准的libc接口。

需要注意的是:

  • 在下载大文件的时候,需要关闭看门狗,不然系统会重启。下载好了再开了开门狗就是了。
  • 然后就是读写buffer,不要开太大,静态1024字节就够了, 一段一段读就好。malloc的空间有时候分配不到,会导致下载失败。

    ret = hi_wifi_start_sta();
    if (ret == 0) {
    #include <hi_watchdog.h>
    
    // get_info_from("www.baidu.com");
    
    hi_watchdog_disable();
    http_download("https://xiansheng-csdn.oss-cn-hongkong.aliyuncs.com/HarmonyOS/Hi3861/hi3861_net/wm8978_test.wav");
    hi_watchdog_enable();
    usleep(10*1000);
    
    g_data.timestramp = get_ntp_time("ntp.ntsc.ac.cn");
    }
    hi_wifi_stop_sta();

串口调试信息显示已经拿到了音频文件(而且实际我们已经存到flash中了),并且通过ntp获取到了中国国家授时中心的时间

六、为Hi3861找一个i2s接口的音频编解码芯片,播放wav格式音乐

  • 选用wm8978,搞定wm8978的硬件连接
  • 并让wm8978播放音乐

选用wm8978是因为手边上有一个stm32的探索者开发板,直接就地取材好了。而且wm8978的性能不赖,有时间细调的话,录音、播放效果很赞,支持3D环绕。感觉后面可以做蓝牙音响和录音笔了(额...,还是把自己拍醒好了,简直就像痴人说梦...)

1. wm8978的硬件连接

活不好多说,开干。按照最开始的gpio规划,我们连接一下Hi3861的i2c & i2s接口到 对应的wm8978片子上(以后有空我们再单独打板做一个好了,前期开发比较穷,先做个小手术,凑一凑好了)。风枪吹掉了stm32的cpu,避免上面的软件和硬件对wm8978的影响。

注意:

  • 别用质量不好接触不良的杜邦线,量不到时钟波形的时候心都凉了。
  • i2s的 datain 和 dataout 管脚别接反了,否则会痛苦好几个钟头找不到不发声儿的问题。
  • 在要放弃的时候,坚持一下,还有机会胜利的

2. i2s驱动

我们仍然需要到hispark_pegasus的sdk_liteos目录中使用“bash build.sh menuconfig"选配I2S的支持

// 进入hispark_pegasus的liteos目录
cd device/hisilicon/hispark_pegasus/sdk_liteos

// 执行以下命令,打开字符终端
bash build.sh  menuconfig

键盘上下键选择"BSP Settings", 按回车进入。在其子菜单中,使用空格键添加"i2s driver support"前的"*"号即可。

然后按"ESC"一直退出,最后按提示按下“Y"来保存设置

重新编译后设置生效。

如果你去看app_main.c中的i2s的初始化你会发现,刚好和我们规划的i2s的接口对应了起来。所以,i2s的gpio初始化,我们就不用做了。

3. wm8978的初始化

注意:

  • i2c读wm8978不好使,所以我们可以用一个表来存我们写过的数据
  • 以下配置目前只是让它发声,要想声音更好听还得继续调试配置

#include "iot_i2c.h"
#include "hi_i2c.h"
#include "../../include/common.h"
#include "codec_wm8978.h"

#define I2C_MASTER_TX_BUF_DISABLE 0 /*!< I2C master doesn\'t need buffer */
#define I2C_MASTER_RX_BUF_DISABLE 0 /*!< I2C master doesn\'t need buffer */
#define ACK_CHECK_EN 0x1            /*!< I2C master will check ack from slave*/
#define ACK_CHECK_DIS 0x0            /*!< I2C master will not check ack from slave */
#define ACK_VAL 0x0                    /*!< I2C ack val */
#define NACK_VAL 0x1                /*!< I2C nack val */

// wm8978 register val buffer zone (total 58 registers 0 to 57), occupies 116 bytes of memory
// Because the IIC wm8978 operation does not support read operations, so save all the register values in the local
// Write wm8978 register, synchronized to the local register values, register read, register directly back locally stored val.
// Note: wm8978 register val is 9, so use unsigned short storage.

static unsigned short wm8978_register_tbl[] = {
    0X0000, 0X0000, 0X0000, 0X0000, 0X0050, 0X0000, 0X0140, 0X0000,
    0X0000, 0X0000, 0X0000, 0X00FF, 0X00FF, 0X0000, 0X0100, 0X00FF,
    0X00FF, 0X0000, 0X012C, 0X002C, 0X002C, 0X002C, 0X002C, 0X0000,
    0X0032, 0X0000, 0X0000, 0X0000, 0X0000, 0X0000, 0X0000, 0X0000,
    0X0038, 0X000B, 0X0032, 0X0000, 0X0008, 0X000C, 0X0093, 0X00E9,
    0X0000, 0X0000, 0X0000, 0X0000, 0X0003, 0X0010, 0X0010, 0X0100,
    0X0100, 0X0002, 0X0001, 0X0001, 0X0039, 0X0039, 0X0039, 0X0039,
    0X0001, 0X0001
};

unsigned int wm8978_reg_write(const unsigned char reg, const unsigned short val)
{
    unsigned char data[] = {(reg<<1) | ((val >> 8) & 0x01), (val&0xff)};

    wm8978_register_tbl[reg] = val;

    return IoTI2cWrite(HI_I2C_IDX_1, WM8978_DEVICE_ADDR, data, sizeof(data));
}

unsigned short wm8978_reg_read(const unsigned char reg)
{
    #if 0
    unsigned char data[2] = {reg<<1, 0};
    unsigned short val = (unsigned short)-1;

    if (0 != IoTI2cRead(HI_I2C_IDX_1, WM8978_DEVICE_ADDR, data, sizeof(data)))
        ;
        // printf("[Error] %s %s(%d)\\n", __FILE__, __func__, __LINE__);
    else
        val = ((data[0] & 0x1)<<8) | data[1];

    return val;
    #else 
        return wm8978_register_tbl[reg];
    #endif
}

/* 设置I2S工作模式
fmt: 
    0,LSB(右对齐);
    1,MSB(左对齐);
    2,飞利浦标准I2S;
    3,PCM/DSP;
len: 
    0, 16位;
    1, 20位;
    2, 24位;
    3, 32位;
*/
void wm8978_config_i2s(unsigned char fmt, unsigned char len)
{
    fmt &= 0X03;
    len &= 0X03;
    wm8978_reg_write(4, (fmt<<3) | (len<<5));   //WM8978工作模式设置
}

/* wm8978 config adc/dac: 
使能(1)/关闭(0)
*/
void wm8978_config_adda(const unsigned char adc, const unsigned char dac)
{
    unsigned short val;

    //adc
    val = wm8978_reg_read(2);
    if (adc) val |= 3<<0;       //R2最低2个位设置为1,开启ADCR&ADCL
    else val &= ~(3<<0);        //R2最低2个位清零,关闭ADCR&ADCL.
    wm8978_reg_write(2, val);

    //dac
    val = wm8978_reg_read(3);
    if (dac) val |= 3<<0;       //R3低2位设为1,开启DACR&DACL
    else val &= ~(3<<0);        //R3低2位清零,关闭DACR&DACL.
    wm8978_reg_write(3, val);
}

/* wm8978 输入通道配置:
mic: MIC开启(1)/关闭(0)
linein: Line In开启(1)/关闭(0)
aux: aux开启(1)/关闭(0)
*/
void wm8978_config_input(const unsigned char mic, const unsigned char linein, const unsigned char aux)
{
    unsigned short val;
    val = wm8978_reg_read(2);
    if (mic) val |= 3<<2;               //开启INPPGAENR, INPPGAENL(MIC的PGA放大)
    else val &= ~(3<<2);                //关闭INPPGAENR, INPPGAENL.
    wm8978_reg_write(2, val);

    val = wm8978_reg_read(44);
    if (mic) val |= 3<<4 | 3<<0;        //开启LIN2INPPGA, LIP2INPGA, RIN2INPPGA, RIP2INPGA.
    else val &= ~(3<<4 | 3<<0);         //关闭LIN2INPPGA,LIP2INPGA,RIN2INPPGA,RIP2INPGA.
    wm8978_reg_write(44, val);

    wm8978_gain_mic(30);

    if (linein) wm8978_gain_linein(5);  //LINE IN 0dB增益
    else wm8978_gain_linein(0);         //关闭LINE IN

    if (aux) wm8978_gain_aux(7);        //AUX 6dB增益
    else wm8978_gain_aux(0);            //关闭AUX输入
}

/*wm8978 输出配置
dac: DAC输出(放音), 开启(1)/关闭(0)
bps: Bypass输出(录音,包括MIC,LINE IN,AUX等)开启(1)/关闭(0)
*/
void wm8978_config_output(unsigned char dac, unsigned char bps)
{
    unsigned short val = 0;
    if (dac) val |= 1<<0;       //DAC输出使能
    if (bps) {
        val |= 1<<1;            //BYPASS使能
        val |= 5<<2;            //0dB增益
    }
    wm8978_reg_write(50, val);  // Left
    wm8978_reg_write(51, val);  // Right
}

/* 设置喇叭音量
voll: 左声道音量(0~63)
*/
void wm8978_config_vol_speaker(unsigned char voll, unsigned char volr)
{
    voll &= 0X3F;
    volr &= 0X3F;
    if (voll == 0)
        voll |= 1<<6;                    //音量为0时, 直接mute
    if (volr == 0)
        volr |= 1<<6;
    wm8978_reg_write(54, voll);          //喇叭左声道音量设置
    wm8978_reg_write(55, volr | (1<<8)); //喇叭右声道音量设置,同步更新(SPKVU=1)
}

unsigned int wm8978_init(void)
{   
    usleep(30*1000);
    // Power sequence 
    //1. Turn on external power supplles, and wait for supply voltage to settle down
    wm8978_reg_write(0, 0); //soft reset wm8978
    usleep(20*1000);
    // 2. mute all analogue outputs
    // 3. set l/r mix enable, and dac enable l/r, in R3
    wm8978_reg_write(3, 0X6C);  //LOUT2,ROUT2输出使能(喇叭工作),RMIX,LMIX使能
    // 4. set BUFIOEN = 1, VMIDSEL[1:0] to required value in register R1, wait for he VMID supply to settle
    // 5. set BIASEN = 1, in R1
    wm8978_reg_write(1, 0X1B);  //MICEN设置为1(MIC使能),BIASEN设置为1(模拟器工作),VMIDSEL[1:0]设置为:11(5K)
    // 6. set L/ROUT1EN = 1 in R2
    wm8978_reg_write(2, 0X1B0); //ROUT1,LOUT1输出使能(耳机可以工作),BOOSTENR,BOOSTENL使能
    // 7. enable other mixers as required, and other outputs, and remain registers

    //以下为通用设置
    wm8978_reg_write(6, 0);     //MCLK由外部提供
    wm8978_reg_write(43, 1<<4); //INVROUT2反向,驱动喇叭
    wm8978_reg_write(47, 1<<8); //PGABOOSTL,左通道MIC获得20倍增益
    wm8978_reg_write(48, 1<<8); //PGABOOSTR,右通道MIC获得20倍增益
    wm8978_reg_write(49, 1<<1); //TSDEN,开启过热保护
    wm8978_reg_write(10, 1<<3); //SOFTMUTE关闭,128x采样,最佳SNR
    wm8978_reg_write(14, 1<<3); //ADC 128x采样率

    wm8978_config_i2s(2, 0);

    wm8978_config_adda(1, 1);
    wm8978_config_input(1, 1, 0);
    wm8978_config_output(1, 0);
    wm8978_config_vol_speaker(30, 30);
    wm8978_config_vol_headset(30, 30); //0-63
}

4. 使用i2s接口向wm8978中写数据, 播放

我们直接使用libc的文件读接口打开我们下载好的音频文件,跳过wav数据头,播放pcm格式的数据即可。
这样我们就可以播放我们下载的音乐了。嘿嘿

#include <hi_i2s.h>

int wm8978_player(char* filename)
{
    int ret = 0;
    #define WM8978_BUFFER_SIZE 1024
    unsigned char buf[WM8978_BUFFER_SIZE+1] = {};
    wav_header_t header = {};
    uint32_t play_len = 0;
    // unsigned char* play_buf = NULL;

    //创建文件描述符
    int fd = open(filename, O_RDONLY);
    if (fd < 0) {
        ret = -EACCES;
        USR_ERROR_MSG("Open(%s) failed\\n", filename);
        return ret;
    } else {
        USR_DEBUG_MSG("Open(%s) successfully\\n", filename);
    }

    read(fd, &header, sizeof(wav_header_t));
    play_len = header.chunk_size - 0x2c;

    hi_watchdog_disable();
    while (1) {
        memset(buf, 0, sizeof(buf));
        read(fd, buf, sizeof(buf));
        hi_i2s_write(buf, WM8978_BUFFER_SIZE, 1000);
        if(play_len < WM8978_BUFFER_SIZE) 
            break;
        play_len -= WM8978_BUFFER_SIZE;
    }
    close(fd);
    hi_watchdog_enable();
    usleep(500*1000);
    return ret;
}

void* I2sTask(const void *arg)
{
    (void)arg;
    int ret = -1;

    hi_i2s_attribute i2s_cfg = {
        .sample_rate = HI_I2S_SAMPLE_RATE_8K,
        .resolution = HI_I2S_RESOLUTION_16BIT,
    };
    usleep(3000 * 1000);
    ret = hi_i2s_deinit();
    if (ret != HI_ERR_SUCCESS) {
        USR_ERROR_MSG("Failed to deinit i2s!\\n");
    }
    usleep(2000 * 1000);
    ret = hi_i2s_init(&i2s_cfg);
    if (ret != HI_ERR_SUCCESS) {
        USR_ERROR_MSG("Failed to reinit i2s!\\n");
    }

    wm8978_init();

    usleep(1000*1000);

    UtilsGetValue(g_data.tag[USR_AUDIO_FILE_NAME].name, g_data.tag[USR_AUDIO_FILE_NAME].value, UTILS_TAG_BUFFER_SIZE);

    wm8978_player(g_data.tag[USR_AUDIO_FILE_NAME].value);

    return (void*)ret;
}

七、结尾

好了,这就是笔者的Hi3861联网播放的功能的基本雏形。

祝大家玩得开心 ~_ ~

简易视频

(如果没有预览,大家可以直接点击视频链接):视频链接

想了解更多关于鸿蒙的内容,请访问:

51CTO和华为官方战略合作共建的鸿蒙技术社区

https://harmonyos.51cto.com/#bkwz

::: hljs-center

:::

以上是关于用Hi3861-wifi联网下载播放wav音乐 - 基于Harmony2.0的主要内容,如果未能解决你的问题,请参考以下文章

用C语言播放mp3格式的音乐

WAV格式如何播放?

Python PyQt5 | Hi音乐 v0.1.0 正式版发布

如何用c语言插入(背景)音乐

如果机子里没有播放器,有记事本HTML做网页,用<bgsound="音乐.wav">命令时,IE浏览器能播放歌曲吗

SD卡WAV音乐播放器(quartus11.0)(FAT32)(DE2-115)