用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内部拉高到高电平。
这样根据电路图当按键按下时,才会有下降沿的产生,才会触发中断处理函数。
-
- 初始化gpio05
-
- 设置gpio05为输入
-
- 设置gpio05内部拉高
-
- 注册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的初始化代码示例如下
具体的配置请参考其数据手册, 点击下载coding_tbl.h
#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>\
<h1>%s</h1>\
</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联网播放的功能的基本雏形。
祝大家玩得开心 ~_ ~
简易视频
(如果没有预览,大家可以直接点击视频链接):
<video src=“https://xiansheng-csdn.oss-cn-hongkong.aliyuncs.com/HarmonyOS/Hi3861/hi3861_net/wm8978_play_sample.mp4” controls=“controls” width=“500” height=“300”></video>
看着楼主很完调试了一遍,谢谢分享,有学习到。
非常棒的一篇帖子,感谢楼主分享
学习学习~~
写得蛮详细,学习了
有源码吗?
期待啊!!!我想了解一下,get_info_from("www.baidu.com");
g_data.timestramp =get_ntp_time("ntp.ntsc.ac.cn"); 这个两个函数怎么实现的?
棒,这么详细