Taurus & Pegasus

Taurus & Pegasus AI计算机视觉基础开发套件组装指导视频

海思社区 (hisilicon.com)

官方教程

官网老是进不去,这是gitee上的pdf

训练服务器获取文档

华为开发者学堂-HarmonyOS物联网开发课程 (huawei.com)

硬件资料/HiSpark_WiFi_IoT智能家居开发套件_原理图.rar

Hi3861LV100 AT命令 使用指南.pdf

<零> Pegasus环境配置与文件架构

跟着官方教程走,到了build步骤的时候出现报错

零2.png

或者是这样的

零0.png

这个问题教程里也有讲过:尽量将工程文件夹放在根目录下;实际上应该是:一定要将工程文件夹放在根目录下

之后就可以成功编译了

零1.png

Build.gn文件

BUILD.gn是一种用于描述软件构建信息的文件,它是GN(Generate Ninja)构建系统的一部分12。它的作用是:

  • 定义要编译的源文件、头文件路径、编译方式等。
  • 声明目标的依赖关系,如静态库、共享库、可执行文件等。
  • 将设置放在config中,如编译器标志、预处理器定义、包含目录等。
  • 生成Ninja构建文件,用于实际编译和链接代码。

BUILD.gn文件通常位于每个源码目录下,以及根目录下。官方demo的 app 文件夹下是要烧录到板子之中的代码,通过该目录下的 BUILD.gn 文件选择具体使用的 以及 源代码

1
2
3
4
5
6
7
8
9
# 导入一个名为lite_component的模板,用于定义OpenHarmony应用组件
import("//build/lite/config/component/lite_component.gni")

# 使用lite_component模板定义一个名为app的应用组件
lite_component("app") {
# 指定要依赖的其他目标列表,这里依赖了netDemo静态库目标
# [子文件夹名:子文件夹下声明的静态库]
features = [ "lwip_demo:netDemo", ]
}

app 文件夹下有存放着各种源码的子文件夹,上面这个例子就应该有着类似 lwip_demo 的子文件夹,在这里面存放着源码与自己的 BUILD.gn

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
# 定义一个静态库目标,名为netDemo
static_library("netDemo") {
# 指定要编译的源文件列表
sources = [ "lwip_tcp_server.c" ]

# 追加更多的源文件到列表中
sources += [
"demo_entry_cmsis.c",
"wifi_connecter.c",
]

# 指定要包含的头文件目录列表
include_dirs = [
"//utils/native/lite/include",
"//kernel/liteos_m/components/cmsis/2.0",
"//base/iot_hardware/interfaces/kits/wifiiot_lite",
"//base/iot_hardware/peripheral/interfaces/kits",
"//foundation/communication/wifi_lite/interfaces/wifiservice",
]
}

这里的sourcessources+添加的源文件没有本质的区别,都是指定要编译的源文件列表。只是为了方便阅读和维护,可以使用+号来追加文件,而不是一次性写出所有的文件。这样可以按照功能或模块来分组文件,也可以避免一行过长的问题,;例如

1
2
3
4
5
6
7
8
# 指定要编译的源文件列表
sources = [ "lwip_tcp_server.c" ]

# 追加来自demo_entry_cmsis目录的源文件
sources += [ "demo_entry_cmsis/*.c" ]

# 追加来自wifi_connecter目录的源文件
sources += [ "wifi_connecter/*.c" ]

<一> hello world

修改BUILD.gn来选择需要编译的工程文件

1
2
3
4
5
6
7
import("//build/lite/config/component/lite_component.gni")

lite_component("app") {
features = [
"hello_world_demo:helloWorld",
]
}

hello_world_demo:指的是需要编译的工程目录对应的文件夹名

helloWorld:指的是需要编译的代码中的BUILD.gn的静态库,名称为helloWorld;比如这里就是 ./hello_world_demo/BUILD.gn 中声明的库名

修改usr_config.mk文件来配置外设驱动

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

<二> Operating System

Thread

源码见仓库 Hello_Pegasus 中的 Thread_demo/thread.c

入口函数为 ThreadTestTask 创建了 rtosv2_thread_main 线程。在 rtosv2_thread_main 中创建了 threadTest 线程,threadTestwhile中不断打印; rtosv2_thread_main 没有while循环,从头到尾走一遍,调了几个API示例就没了。

1
2
3
4
5
6
7
8
9
10
11
12
13
/**
* @brief Creates an active thread.
*
* The priority ranges from 9 to 38. Select a proper priority as required.
* The maximum of tasks is LOSCFG_BASE_CORE_TSK_LIMIT(LOSCFG_BASE_CORE_TSK_LIMIT is defined in the traget_config.h).
* @param func Indicates the entry of the thread callback function.线程函数
* @param argument Indicates the pointer to the argument passed to the thread.开始参数
* @param attr Indicates the thread attributes.线程属性
* @return Returns the thread ID; returns NULL in the case of an error.
* @since 1.0
* @version 1.0
*/
osThreadId_t osThreadNew (osThreadFunc_t func, void *argument, const osThreadAttr_t *attr);

常用API

接口名称 函数说明
osThreadGetName 获取指定线程的名字
osThreadGetId 获取当前运行线程的线程ID
osThreadGetState 获取当前线程的状态
osThreadSetPriority 设置指定线程的优先级
osThreadGetPriority 获取当前线程的优先级
osThreadYield 将运行控制转交给下一个处于READY状态的线程
osThreadSuspend 挂起指定线程的运行
osThreadResume 恢复指定线程的运行
osThreadDetach 分离指定的线程(当线程终止运行时,线程存储可以被回收)
osThreadJoin 等待指定线程终止运行
osThreadExit 终止当前线程的运行
osThreadTerminate 终止指定线程的运行
osThreadGetStackSize 获取指定线程的栈空间大小
osThreadGetStackSpace 获取指定线程的未使用的栈空间大小
osThreadGetCount 获取活跃线程数
osThreadEnumerate 获取线程组中的活跃线程数

Timer

创建函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
/**
* @brief Creates and initializes a timer.
*
* This function creates a timer associated with the arguments callback function. The timer stays in the stopped state until OSTimerStart is used to start the timer.
* The timer precision is 1000 / LOSCFG_BASE_CORE_TICK_PER_SECOND ms(LOSCFG_BASE_CORE_TICK_PER_SECOND is defined in the traget_config.h).
* @param func Indicates the entry of the timer callback function.
* @param type Indicates the timer type.
* @param argument Indicates the pointer to the argument used in timer callback.
* @param attr Indicates the pointer to the timer attributes. This parameter is not used.
* @return Returns the timer ID; returns NULL in the case of an error.
* @since 1.0
* @version 1.0
*/
osTimerId_t osTimerNew (osTimerFunc_t func, osTimerType_t type, void *argument, const osTimerAttr_t *attr);

常用API

API名称 说明
osTimerNew 创建和初始化定时器
osTimerGetName 获取指定的定时器名字
osTimerStart 启动或者重启指定的定时器
osTimerStop 停止指定的定时器
osTimerIsRunning 检查一个定时器是否在运行
osTimerDelete 删除定时器

互斥锁

什么是临界资源,临界区? - 知乎 (zhihu.com)

互斥锁(Mutex)是一种用来保证多个线程或进程之间对共享资源的互斥访问的同步机制。只有一个线程或进程可以获得互斥锁,这意味着互斥锁有所有权的概念,只有拥有互斥锁的线程或进程才能释放它1。
互斥锁可以用来实现临界区(Critical Section),即一段只能被一个线程或进程执行的代码。一个线程或进程在进入临界区之前,必须先请求获得互斥锁,如果互斥锁已经被占用,那么该线程或进程就会阻塞等待,直到互斥锁被释放。一个线程或进程在离开临界区之后,必须释放互斥锁,以便其他线程或进程可以进入临界区2。
互斥锁有两种类型:本地互斥锁和命名系统互斥锁。如果你使用一个接受名字的构造函数来创建一个互斥锁对象,它就会与操作系统中同名的对象关联。命名系统互斥锁在操作系统中是可见的,可以用来同步不同进程的活动。你可以创建多个代表同一个命名系统互斥锁的对象,也可以使用OpenExisting方法来打开一个已存在的命名系统互斥锁。本地互斥锁只存在于你的进程中。它可以被你的进程中任何拥有该本地互斥锁对象引用的线程使用。每个互斥锁对象都是一个单独的本地互斥锁3。

哲学家就餐问题1:这是一个经典的并发编程问题,有五个哲学家围坐在一张圆桌旁,每两个哲学家之间有一只叉子,每个哲学家要么思考要么吃饭,吃饭时需要同时拿起左右两只叉子。如果使用互斥锁来保护每只叉子,那么可能出现死锁的情况,即每个哲学家都拿起了左边的叉子,然后等待右边的叉子,导致无法进餐。一种解决方案是让每个哲学家按照编号顺序拿起叉子,即先拿编号较小的叉子,再拿编号较大的叉子,这样可以避免环路等待的条件。
丈夫与妻子模型:这是一个简单的信号量问题,有一个丈夫和一个妻子共享一个洗手间,洗手间只能容纳一个人。如果使用互斥锁来保护洗手间,那么可以实现互斥访问,即只有一个人可以进入洗手间,另一个人必须等待。当一个人用完洗手间后,必须释放互斥锁,以便另一个人可以进入。

死锁是指两个或多个进程互相等待对方占用的资源而无法继续执行的状态。死锁的产生必须满足四个条件:互斥、请求和保持、不剥夺、环路等待。要避免死锁,可以破坏这四个条件中的任何一个。一个通俗的例子是:有两把钥匙和两扇门,每把钥匙只能打开一扇门。如果两个人分别拿了一把钥匙,但是都想打开另一扇门,就会形成死锁1。
饥饿是指一个进程因为无法获得所需的资源而长时间等待的现象。饥饿可能是由于资源分配不公平、优先级调度不合理、进程阻塞等原因导致的。一个通俗的例子是:有一个餐厅只有一个服务员,他总是优先服务点菜最多的客人,而忽略了点菜少的客人。这样就会导致点菜少的客人饿肚子。
忙等是指一个进程在等待某个条件成立时,不断地检查该条件是否成立,而不释放处理器或让出CPU给其他进程的行为。忙等会浪费CPU资源和时间片,降低系统效率。一个通俗的例子是:有一个人在等待电梯到达自己的楼层时,不断地按电梯按钮,而不去做其他事情。
信号量是一种用于实现进程间同步和互斥的机制。信号量是一个整数变量,可以通过P操作(减一)和V操作(加一)来修改。当信号量为零时,P操作会阻塞等待,直到信号量变为正数。当信号量为正数时,V操作会唤醒等待的进程。通过初始化信号量为1或0,可以实现互斥锁或条件变量的功能。一个通俗的例子是:有一个公共厕所只有一个坑位,有多个人想要上厕所。可以设置一个信号量为1,表示坑位是否空闲。每个人在上厕所前要执行P操作,如果坑位空闲就占用坑位并将信号量减为0;如果坑位占用就等待信号量变为1。每个人在上完厕所后要执行V操作,释放坑位并将信号量加为123。

创建函数

1
2
3
4
5
6
7
8
9
**/**
* @brief Creates and initializes a mutex.
*
* @param attr Indicates the pointer to the mutex attributes. This parameter is not used.
* @return Returns the mutex ID; returns NULL in the case of an error.
* @since 1.0
* @version 1.0
*/
osMutexId_t osMutexNew (const osMutexAttr_t *attr);**

常用API

osMutexGetName 获得指定互斥锁的名字
osMutexAcquire 获得指定的互斥锁的访问权限,若互斥锁已经被锁,则返回超时
osMutexRelease 释放指定的互斥锁
osMutexGetOwner 获得指定互斥锁的所有者线程
osMutexDelete 删除指定的互斥锁

信号量

关于信号量的基本知识这篇文章写得很清楚

(42条消息) 操作系统——信号量(理解什么是信号量,信号量如何解决同步互斥问题,信号量一些注意点)_五斤w的博客-CSDN博客

(42条消息) 操作系统_五斤w的博客-CSDN博客

创建函数

1
2
3
4
5
6
7
8
9
10
11
/**
* @brief Creates and initializes a semaphore object.
*
* @param max_count 最大令牌数,Indicates the maximum number of available tokens that can be applied for.
* @param initial_count 初始令牌数,Indicates the initial number of available tokens.
* @param attr Indicates the pointer to the semaphore attributes. This parameter is not used.
* @return Returns the semaphore ID; returns NULL in the case of an error.
* @since 1.0
* @version 1.0
*/
osSemaphoreId_t osSemaphoreNew (uint32_t max_count, uint32_t initial_count, const osSemaphoreAttr_t *attr);
API名称 说明
osSemaphoreNew 创建并初始化一个信号量
osSemaphoreGetName 获取一个信号量的名字
osSemaphoreAcquire 获取一个信号量的令牌,若获取不到,则会超时返回
osSemaphoreRelease 释放一个信号量的令牌,但是令牌的数量不超过初始定义的的令牌数
osSemaphoreGetCount 获取当前的信号量令牌数
osSemaphoreDelete 删除一个信号量

例程

典型的生产者-消费者问题,其中有一个共享的缓冲区,生产者线程向缓冲区中放入产品,消费者线程从缓冲区中取出产品。为了避免缓冲区溢出或者空闲,需要用两个信号量来控制缓冲区的状态。

empty信号量表示缓冲区中空闲的位置的数量,它的初始值是BUFFER_SIZE,表示一开始缓冲区是空的。当生产者线程想要向缓冲区中放入一个产品时,它需要先获取一个empty信号量的令牌,如果empty信号量有剩余的令牌,说明缓冲区还有空闲的位置,那么生产者线程就可以获取一个令牌并继续执行,如果empty信号量没有剩余的令牌,说明缓冲区已经满了,那么生产者线程就需要等待直到有消费者线程释放令牌或者超时返回。当生产者线程向缓冲区中放入一个产品后,它需要释放一个filled信号量的令牌,表示缓冲区中有一个产品可以被消费。

filled信号量表示缓冲区中已经放入的产品的数量,它的初始值是0,表示一开始缓冲区是没有产品的。当消费者线程想要从缓冲区中取出一个产品时,它需要先获取一个filled信号量的令牌,如果filled信号量有剩余的令牌,说明缓冲区还有产品可以被消费,那么消费者线程就可以获取一个令牌并继续执行,如果filled信号量没有剩余的令牌,说明缓冲区已经空了,那么消费者线程就需要等待直到有生产者线程释放令牌或者超时返回。当消费者线程从缓冲区中取出一个产品后,它需要释放一个empty信号量的令牌,表示缓冲区中有一个空闲的位置可以被填充。

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
void producer_thread(int *arg)
{
(int)arg;
empty_id = osSemaphoreNew(BUFFER_SIZE, BUFFER_SIZE, NULL);
filled_id = osSemaphoreNew(BUFFER_SIZE, 0U, NULL);
while (1) {
osSemaphoreAcquire(empty_id, osWaitForever);
product_number++;
printf("[Semp Test]%s produces a product, now product number: %d.\r\n",
osThreadGetName(osThreadGetId()), product_number);
osDelay(OS_DELAY);
osSemaphoreRelease(filled_id);
}
}

void consumer_thread(int *arg)
{
(int)arg;
while (NUM) {
osSemaphoreAcquire(filled_id, osWaitForever);
product_number--;
printf("[Semp Test]%s consumes a product, now product number: %d.\r\n",
osThreadGetName(osThreadGetId()), product_number);
osDelay(OS_DELAY_S);
osSemaphoreRelease(empty_id);
}
}

总结起来就是生产者productor根据空闲位置empty_id获取继续生产的资格

1
osSemaphoreAcquire(empty_id, osWaitForever);

只能使空闲位置empty_id减少,使产品数量filled_id增加

1
osSemaphoreRelease(filled_id);

消费者consumer根据产品数量filled_id获取消费资格

1
osSemaphoreAcquire(filled_id, osWaitForever);

只能使产品数量filled_id减少,使空闲位置empty_id增加

1
osSemaphoreRelease(empty_id);

消息队列

API名称 说明
osMessageQueueNew 创建和初始化一个消息队列
osMessageQueueGetName 返回指定的消息队列的名字
osMessageQueuePut 向指定的消息队列存放1条消息,如果消息队列满了,那么返回超时
osMessageQueueGet 从指定的消息队列中取得1条消息,如果消息队列为空,那么返回超时
osMessageQueueGetCapacity 获得指定的消息队列的消息容量
osMessageQueueGetMsgSize 获得指定的消息队列中可以存放的最大消息的大小
osMessageQueueGetCount 获得指定的消息队列中当前的消息数
osMessageQueueGetSpace 获得指定的消息队列中还可以存放的消息数
osMessageQueueReset 将指定的消息队列重置为初始状态
osMessageQueueDelete 删除指定的消息队列

创建函数

1
2
3
4
5
6
7
8
9
/**
* @brief Creates and initializes a message queue.
*
* @param msg_count 队列长度:Indicates the number of messages in the message queue.
* @param msg_size 队列消息大小:Indicates the size of messages in the message queue.
* @param attr Indicates the pointer to the message queue attributes. This parameter is not used.
* @return Returns the message queue ID; returns NULL in the case of an error.
*/
osMessageQueueId_t osMessageQueueNew (uint32_t msg_count, uint32_t msg_size, const osMessageQueueAttr_t *attr);

发送消息

1
2
3
4
5
6
7
8
9
10
11
/**
* @brief Places a message in a message queue.
*
* @param mq_id 消息队列ID:Indicates the message queue ID, which is obtained using osMessageQueueNew.
* @param msg_ptr 消息指针:Indicates the pointer to the buffer for storing the message to be placed in the message queue.
* @param msg_prio 消息优先级(此参数没用)Indicates the priority of the message to be placed in the message queue. This parameter is not used.
* @param timeout Indicates the timeout duration.
表示超时时间,如果消息队列已满,函数会等待一段时间,直到有空间可以放入消息或者超时返回。
* @return Returns the CMSIS-RTOS running result.
*/
osStatus_t osMessageQueuePut (osMessageQueueId_t mq_id, const void *msg_ptr, uint8_t msg_prio, uint32_t timeout);

获取消息

1
2
3
4
5
6
7
8
9
10
/**
* @brief Obtains a message in a message queue.
*
* @param mq_id 队列ID:Indicates the message queue ID, which is obtained using osMessageQueueNew.
* @param msg_ptr 存放取得消息的指针:Indicates the pointer to the buffer for storing the message to be retrieved from the message queue.
* @param msg_prio (无用)Indicates the pointer to the buffer for storing the priority of the message to be retrieved from the message queue. This parameter is not used.
* @param timeout Indicates the timeout duration.
* @return Returns the CMSIS-RTOS running result.
*/
osStatus_t osMessageQueueGet (osMessageQueueId_t mq_id, void *msg_ptr, uint8_t *msg_prio, uint32_t timeout);

可以看出,消息队列是一种先进先出的数据结构,不区分发送者和接收者线程的身份,只按照消息的到达顺序进行存取,即使有多个发布者和接收者线程共用这个消息队列的时候也是如此。如果要指定某个线程的功能,可以在消息的内容中加入标识符或者使用其他的通信机制。

<三> 基本外设

GPIO

API名称 说明
unsigned int GpioInit(void); GPIO模块初始化
unsigned int GpioSetDir(WifiIotGpioIdx id, WifiIotGpioDir dir); 设置GPIO引脚方向,id参数用于指定引脚,dir参数用于指定输入或输出
unsigned int GpioSetOutputVal(WifiIotGpioIdx id, WifiIotGpioValue val); 设置GPIO引脚的输出状态,id参数用于指定引脚,val参数用于指定高电平或低电平
unsigned int IoSetFunc(WifiIotIoName id, unsigned char val); 设置引脚功能,id参数用于指定引脚,val用于指定引脚功能
unsigned int GpioDeinit(void); 解除GPIO模块初始化

每个IO口具体的可以配置的功能可以参考 iot_gpio_ex.h,里面有各个IO口的宏定义;通过 IoSetFunc( ) 配置IO口功能

GPIO中断

API名称 说明
unsigned int GpioGetInputVal(WifiIotGpioIdx id, WifiIotGpioValue *val); 获取GPIO引脚状态,id参数用于指定引脚,val参数用于接收GPIO引脚状态
unsigned int IoSetPull(WifiIotIoName id, WifiIotIoPull val); 设置引脚上拉或下拉状态,id参数用于指定引脚,val参数用于指定上拉或下拉状态
unsigned int GpioRegisterIsrFunc(WifiIotGpioIdx id, WifiIotGpioIntType intType, WifiIotGpioIntPolarity intPolarity, GpioIsrCallbackFunc func, char *arg); 注册GPIO引脚中断,id参数用于指定引脚,intType参数用于指定中断触发类型(边缘触发或水平触发),intPolarity参数用于指定具体的边缘类型(下降沿或上升沿)或水平类型(高电平或低电平),func参数用于指定中断处理函数,arg参数用于指定中断处理函数的附加参数
typedef void (GpioIsrCallbackFunc) (char arg); 中断处理函数原型,arg参数为附加参数,可以不适用(填NULL),或传入指向用户自定义类型的参数
unsigned int GpioUnregisterIsrFunc(WifiIotGpioIdx id); 解除GPIO引脚中断注册,id参数用于指定引脚

example

1
2
3
4
5
6
7
8
9
IoTGpioInit(LED_TEST_GPIO);
IoTGpioSetDir(LED_TEST_GPIO, IOT_GPIO_DIR_OUT);

//GPIO5链接按键
IoTGpioInit(IOT_GPIO_KEY);
IoSetFunc(IOT_GPIO_KEY, 0);//GPIO模式
IoTGpioSetDir(IOT_GPIO_KEY, IOT_GPIO_DIR_IN);//输入
IoSetPull(IOT_GPIO_KEY, IOT_IO_PULL_UP);//上拉
IoTGpioRegisterIsrFunc(IOT_GPIO_KEY, IOT_INT_TYPE_EDGE, IOT_GPIO_EDGE_FALL_LEVEL_LOW, OnButtonPressed, NULL);

示例demo实现了通过按键控制led亮、灭、闪烁间切换

PWM输出

unsigned int PwmInit(WifiIotPwmPort port); PWM模块初始化
unsigned int PwmStart(WifiIotPwmPort port, unsigned short duty, unsigned short freq); 开始输出PWM信号
unsigned int PwmStop(WifiIotPwmPort port); 停止输出PWM信号
unsigned int PwmDeinit(WifiIotPwmPort port); 解除PWM模块初始化
unsigned int PwmSetClock(WifiIotPwmClkSource clkSource); 设置PWM模块时钟源

example

1
2
3
4
5
6
7
#define IOT_PWM_BEEP        9
IoTGpioInit(IOT_PWM_BEEP);
IoSetFunc(IOT_PWM_BEEP, 5);
IoTGpioSetDir(IOT_PWM_BEEP, IOT_GPIO_DIR_OUT);
IoTPwmInit(IOT_PWM_PORT_PWM0);
IoTPwmStart(IOT_PWM_PORT_PWM0, 50, 4000); /* 占空比50 / 4000,频率4000 */
IoTPwmStop(IOT_PWM_PORT_PWM0);

具体的IO口及其功能配置可参考iot_gpio_ex.h,这里是将GPIO9配置为5号功能,即PWM0_OUT

ADC输入

1
unsigned int AdcRead(WifiIotAdcChannelIndex channel, unsigned short *data, WifiIotAdcEquModelSel equModel, WifiIotAdcCurBais curBais, unsigned short rstCnt);

输入参数是通道channel,存数据的的指针*data,ADC方程模型equModel,参考电压curBaisrstnt表示从复位到转换开始的时间计数,每个计数相当于334纳秒,取值范围是0到0xFF0。

方程模型

1
2
3
4
5
6
7
8
9
10
11
12
typedef enum {
/** One-equation model */
IOT_ADC_EQU_MODEL_1,
/** Two-equation model */
IOT_ADC_EQU_MODEL_2,
/** Four-equation model */
IOT_ADC_EQU_MODEL_4,
/** Eight-equation model */
IOT_ADC_EQU_MODEL_8,
/** Button value */
IOT_ADC_EQU_MODEL_BUTT,
} IotAdcEquModelSel;

方程模型是用来设置ADC的分辨率的,也就是ADC能够区分多少个不同等级的模拟输入电压。分辨率越高,表示ADC能够更精确地转换模拟信号,但也需要更多的时间和资源。方程模型的枚举类型有四个选项,分别对应一元、二元、四元和八元方程模型。一元方程模型是最简单的,它只需要一个方程就能确定ADC的输出值。二元方程模型需要两个方程,四元方程模型需要四个方程,八元方程模型需要八个方程。方程的数量越多,表示ADC的分辨率越高,但也越复杂。

复位rstcnt

(bing说的,仅供参考)

rstCnt是用来设置从复位到转换开始的时间计数的,也就是ADC在复位后需要等待多久才能开始转换模拟信号。这个参数可以用来调节ADC的采样速率,也就是每秒钟能转换多少次模拟信号。rstCnt的值越大,表示等待的时间越长,采样速率越低。rstCnt的值越小,表示等待的时间越短,采样速率越高。rstCnt的单位是ADC时钟周期,每个周期相当于334纳秒。rstCnt的取值范围是0到0xFF0,也就是0到4080个时钟周期。

参考电压

1
2
3
4
5
6
7
8
9
10
11
12
typedef enum {
/** Automatic control */
IOT_ADC_CUR_BAIS_DEFAULT,
/** Automatic control */
IOT_ADC_CUR_BAIS_AUTO,
/** Manual control (AVDD = 1.8 V) */
IOT_ADC_CUR_BAIS_1P8V,
/** Manual control (AVDD = 3.3 V) */
IOT_ADC_CUR_BAIS_3P3V,
/** Button value */
IOT_ADC_CUR_BAIS_BUTT,
} IotAdcCurBais;

参考电压是用来作为ADC转换的标准的,也就是ADC能够测量的最大输入电压。参考电压越高,表示ADC能够接受更大范围的模拟信号,但也会降低转换的精度。参考电压可以是内部或外部提供的,也可以是自动控制的。模拟电源控制模式是用来设置参考电压的来源和值的。模拟电源控制模式的枚举类型有五个选项,分别对应默认、自动、1.8V、3.3V和按钮值。默认和自动表示由系统自动选择合适的参考电压,1.8V和3.3V表示手动选择固定的参考电压,按钮值表示无效的选项。

I2C通信

API名称 说明
IoTI2cInit(unsigned int id, unsigned int baudrate); 用指定的波特速率初始化I2C设备
IoTI2cDeinit(unsigned int id); 取消初始化I2C设备
IoTI2cWrite(unsigned int id, unsigned short deviceAddr, const unsigned char *data, unsigned int dataLen); 将数据写入I2C设备
IoTI2cRead(unsigned int id, unsigned short deviceAddr, unsigned char *data, unsigned int dataLen); 从I2C设备中读取数据
IoTI2cSetBaudrate(unsigned int id, unsigned int baudrate); 设置I2C设备的波特率

<四> TCP通讯

一篇看懂 | TCP原理详细图解 - 知乎 (zhihu.com)

TCP协议详解 - 知乎 (zhihu.com)

socket套接字详解(TCP与UDP) - 知乎 (zhihu.com)

例程代码使用了lwip库来实现tcp通信的功能。

lwip是一个轻量级的tcp/ip协议栈,适用于嵌入式系统。

client

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
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
// 包含标准输入输出库
#include <stdio.h>
// 包含lwip库中的sockets头文件,提供了socket编程的接口
#include "lwip/sockets.h"
// 包含net_demo头文件,定义了一些函数原型和宏
#include "net_demo.h"

// 定义一个全局变量g_request,是一个字符串数组,存放了要发送给服务器的数据
static char g_request[] = "Hello,I am Lwip";
// 定义一个全局变量g_response,是一个字符串数组,存放了从服务器接收到的数据
static char g_response[128] = "";

// 定义一个函数TcpClientTest,用于作为客户端与服务器进行tcp通信
// 参数host是服务器的IP地址,参数port是服务器的端口号
void TcpClientTest(const char* host, unsigned short port)
{
// 调用socket函数创建一个TCP套接字,返回一个套接字描述符sockfd
//套接字是网络编程中的一种通信机制,是支持TCP/IP的网络通信的基本操作单元,可以看做是不同主机之间的进程进行双向通信的端点
//简单的说就是通信的两方的一种约定,用套接字中的相关函数来完成通信过程。
// 参数AF_INET表示使用IPv4协议族,参数SOCK_STREAM表示使用TCP协议,参数0表示使用默认协议
int sockfd = socket(AF_INET, SOCK_STREAM, 0); // TCP socket

// 定义一个结构体变量serverAddr,用于存放服务器的地址信息
struct sockaddr_in serverAddr = {0};
// 设置地址族为AF_INET,与套接字参数一致
serverAddr.sin_family = AF_INET; // AF_INET表示IPv4协议
// 设置端口号为port,并转换为网络字节序(大端序)
//主机字节序是指不同的计算机系统存储数据的方式,有大端字节序和小端字节序之分。网络字节序是指TCP/IP协议规定的数据传输的方式,采用大端字节序。
serverAddr.sin_port = htons(port); // 端口号,从主机字节序转为网络字节序
// 调用inet_pton函数将主机IP地址从“点分十进制”字符串转化为标准格式(32位整数),并存放在serverAddr.sin_addr中
// 如果转换成功则返回正数,如果失败则返回负数或0,并关闭套接字
if (inet_pton(AF_INET, host, &serverAddr.sin_addr) <= 0) { // 将主机IP地址从“点分十进制”字符串 转化为 标准格式(32位整数)
printf("inet_pton failed!\r\n");
lwip_close(sockfd);
}

// 调用connect函数尝试和目标主机建立连接,连接成功会返回0 ,失败返回 -1
// 参数sockfd是套接字描述符,参数serverAddr是目标主机的地址信息,参数sizeof(serverAddr)是地址信息的长度
if (connect(sockfd, (struct sockaddr *)&serverAddr, sizeof(serverAddr)) < 0) {
printf("connect failed!\r\n");
lwip_close(sockfd);
}
// 如果连接成功,则打印提示信息
printf("connect to server %s success!\r\n", host);

// 建立连接成功之后,这个TCP套接字描述符sockfd就具有了“连接状态”,发送、接收对端都是connect参数指定的目标主机和端口
// 调用send函数发送数据给服务器,返回值retval是实际发送的字节数
// 参数sockfd是套接字描述符,参数g_request是要发送的数据缓冲区,参数sizeof(g_request)是要发送的数据长度,参数0表示使用默认标志
ssize_t retval = send(sockfd, g_request, sizeof(g_request), 0);
// 如果返回值小于0,则表示发送失败,并关闭套接字
if (retval < 0) {
printf("send g_request failed!\r\n");
lwip_close(sockfd);
}
// 如果发送成功,则打印提示信息
printf("send g_request{%s} %ld to server done!\r\n", g_request, retval);

// 调用recv函数接收服务器发送过来的数据,并存放在g_response缓冲区中,返回值retval是实际接收到的字节数
// sockfd是套接字描述符,g_response是要接收的数据缓冲区,sizeof(g_response)是要接收的数据长度,参数0表示使用默认标志
retval = recv(sockfd, &g_response, sizeof(g_response), 0);
// 如果返回值小于等于0,则表示接收失败或者完成,并关闭套接字
if (retval <= 0) {
printf("send g_response from server failed or done, %ld!\r\n", retval);
lwip_close(sockfd);
}
// 如果接收成功,则在g_response末尾添加一个空字符'\0'作为字符串结束符,并打印提示信息
g_response[retval] = '\0';
printf("recv g_response{%s} %ld from server done!\r\n", g_response, retval);
// 关闭套接字
lwip_close(sockfd);
}

// 定义一个函数NetDemoTest,用于调用TcpClientTest函数进行测试
// 参数port是服务器端口号,参数host是服务器IP地址
void NetDemoTest(unsigned short port, const char* host)
{
(void) host;
// 打印提示信息
printf("TcpClientTest start\r\n");
printf("I will connect to %s\r\n", host);
// 调用TcpClientTest函数进行tcp通信测试
TcpClientTest(host, port);
// 打印提示信息
printf("TcpClientTest done!\r\n");
}

接收函数

1
retval = recv(sockfd, &g_response, sizeof(g_response), 0);

关于接收函数,我问了newbing一些问题,这是它的回答。

如果接收成功的话,retval就是接收到的数据字节数吗?

是的,如果接收成功的话,retval就是接收到的数据长度,单位是字节。recv函数的返回值是一个ssize_t类型的整数,它表示实际接收到的字节数。如果返回值等于0,表示对方已经关闭了连接;如果返回值小于0,表示发生了错误,可以用errno变量来查看错误码。

retval小于零或等于零分别有什么含义吗?

如上所述,retval小于零表示发生了错误,retval等于零表示对方已经关闭了连接。一些常见的错误码有:

  • EAGAIN或EWOULDBLOCK:表示套接字是非阻塞的,而当前没有可用的数据,需要稍后再试。
  • EBADF:表示套接字描述符无效。
  • ECONNREFUSED:表示远程主机拒绝了连接请求。
  • EFAULT:表示缓冲区指针指向无效的内存地址。
  • EINTR:表示接收过程被信号中断。
  • EINVAL:表示传递了无效的参数。
  • ENOMEM:表示内存不足。
  • ENOTCONN:表示套接字没有连接。
  • ENOTSOCK:表示套接字描述符不是一个套接字。
  • ETIMEDOUT:表示连接超时。

接收到的数据是被自动接收然后存放在某个缓冲区里吗?如果我不调用这个函数,发送者是否会接收到这个数据的ACK呢?

是的,接收到的数据是被自动接收然后存放在内核为每个套接字维护的一个缓冲区里。recv函数只是从缓冲区中读取数据,并将其复制到用户指定的缓冲区中。如果用户不调用recv函数,那么内核缓冲区可能会被填满,导致TCP流量控制机制生效,使得发送者降低发送速率或停止发送。发送者是否会接收到这个数据的ACK取决于内核缓冲区是否有空间来存放新到达的数据。如果有空间,那么内核会发送ACK给发送者;如果没有空间,那么内核会延迟发送ACK或发送窗口为零的ACK给发送者。

我是否可以将发送和接受分别使用两个线程实现呢?

是的,你可以将发送和接受分别使用两个线程实现。这样可以实现全双工通信,即同时进行发送和接受操作。你需要为每个线程传递相同的套接字描述符,并在每个线程中调用send和recv函数。你还需要注意同步和互斥问题,避免多个线程同时操作同一个资源。

<五> MQTT协议

MQTT协议,终于有人讲清楚了 - 知乎 (zhihu.com)

JSON 语法 | 菜鸟教程 (runoob.com)

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
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
#include <hi_task.h>
#include <string.h>
#include <hi_wifi_api.h>
#include <hi_mux.h>
#include <hi_io.h>
#include <hi_gpio.h>
#include "iot_config.h"
#include "iot_log.h"
#include "iot_main.h"
#include "iot_profile.h"
#include "ohos_init.h"
#include "cmsis_os2.h"

/* attribute initiative to report */
#define TAKE_THE_INITIATIVE_TO_REPORT
#define ONE_SECOND (1000)
/* oc request id */
#define CN_COMMADN_INDEX "commands/request_id="
#define WECHAT_SUBSCRIBE_LIGHT "light"
#define WECHAT_SUBSCRIBE_LIGHT_ON_STATE "1"
#define WECHAT_SUBSCRIBE_LIGHT_OFF_STATE "0"
// < 全局变量,表示灯的状态,-1表示未初始化,0表示关,1表示开
int g_ligthStatus = -1;

// < 这是一个函数指针类型,用于回调函数的参数
typedef void (*FnMsgCallBack)(hi_gpio_value val);

// < 这是一个结构体类型,用于存储一些功能回调的相关信息
typedef struct FunctionCallback {
hi_bool stop; // < 是否停止
hi_u32 conLost; // < 连接丢失次数
hi_u32 queueID; // < 队列ID
hi_u32 iotTaskID; // < IoT任务ID
FnMsgCallBack msgCallBack; // < 消息回调函数指针
}FunctionCallback;
FunctionCallback g_functinoCallback; // < 定义一个全局变量

/* CPU Sleep time Set */
unsigned int TaskMsleep(unsigned int ms)
{
if (ms <= 0) {
return HI_ERR_FAILURE;
}
return hi_sleep((hi_u32)ms);
}

// < 这是一个设备配置初始化的函数,用于设置GPIO_9引脚的功能和输出值
static void DeviceConfigInit(hi_gpio_value val)
{
hi_io_set_func(HI_IO_NAME_GPIO_9, HI_IO_FUNC_GPIO_9_GPIO); // < 设置GPIO_9引脚为GPIO功能
hi_gpio_set_dir(HI_GPIO_IDX_9, HI_GPIO_DIR_OUT); // < 设置GPIO_9引脚为输出方向
hi_gpio_set_ouput_val(HI_GPIO_IDX_9, val); // < 设置GPIO_9引脚的输出值为val
}

// < 这是一个设备消息回调函数的注册函数,用于将回调函数指针赋值给全局变量
static int DeviceMsgCallback(FnMsgCallBack msgCallBack)
{
g_functinoCallback.msgCallBack = msgCallBack;
return 0;
}

// < 这是一个微信控制设备消息的函数,用于根据val值初始化设备配置
static void wechatControlDeviceMsg(hi_gpio_value val)
{
DeviceConfigInit(val);
}

// < 这是一个MQTT消息接收回调函数,当有消息到达时会被调用
// < qos:数据可靠性 topic:订阅主题 payload参数是一个json字符串,包含了设备状态信息
static void DemoMsgRcvCallBack(int qos, const char *topic, const char *payload)
{
IOT_LOG_DEBUG("RCVMSG:QOS:%d TOPIC:%s PAYLOAD:%s\r\n", qos, topic, payload);
/* 云端下发命令后,板端的操作处理 */
if (strstr(payload, WECHAT_SUBSCRIBE_LIGHT) != NULL) { // < 如果payload中包含了WECHAT_SUBSCRIBE_LIGHT字符串
if (strstr(payload, WECHAT_SUBSCRIBE_LIGHT_OFF_STATE) != NULL) { // < 如果payload中包含了WECHAT_SUBSCRIBE_LIGHT_OFF_STATE字符串
wechatControlDeviceMsg(HI_GPIO_VALUE1); // < 调用微信控制设备消息函数,传入HI_GPIO_VALUE1作为参数
g_ligthStatus = HI_FALSE; // < 将灯的状态设置为关
} else { // < 否则
wechatControlDeviceMsg(HI_GPIO_VALUE0); // < 调用微信控制设备消息函数,传入HI_GPIO_VALUE0作为参数
g_ligthStatus = HI_TRUE; // < 将灯的状态设置为开
}
}
return HI_NULL;
}

/* publish sample */
hi_void IotPublishSample(void)
{
/* reported attribute */
WeChatProfile weChatProfile = {
.subscribeType = "type",//订阅类型,表示设备的类型,比如温度计、湿度计、电机、灯等。
.status.subState = "state",//订阅状态,表示设备是否订阅了云端的服务,比如接收命令或发送数据。
.status.subReport = "reported",//订阅报告,表示设备是否向云端报告了自己的属性信息。
.status.reportVersion = "version",//表示设备属性信息的版本号,用于区分不同的报告。
.status.Token = "clientToken",//客户端令牌,表示设备与云端通信的身份标识,用于验证和授权。
/* report motor */
.reportAction.subDeviceActionMotor = "motor",
.reportAction.motorActionStatus = 0, /* 0 : motor off */
/* report temperature */
.reportAction.subDeviceActionTemperature = "temperature",
.reportAction.temperatureData = 30, /* 30 :temperature data */
/* report humidity */
.reportAction.subDeviceActionHumidity = "humidity",
.reportAction.humidityActionData = 70, /* humidity data */
/* report light_intensity */
.reportAction.subDeviceActionLightIntensity = "light_intensity",
.reportAction.lightIntensityActionData = 60, /* 60 : light_intensity */
};

/* report light */
if (g_ligthStatus == HI_TRUE) { // < 如果灯的状态是开
weChatProfile.reportAction.subDeviceActionLight = "light";
weChatProfile.reportAction.lightActionStatus = 1; /* 1: light on */
} else if (g_ligthStatus == HI_FALSE) { // < 如果灯的状态是关
weChatProfile.reportAction.subDeviceActionLight = "light";
weChatProfile.reportAction.lightActionStatus = 0; /* 0: light off */
} else { // < 否则
weChatProfile.reportAction.subDeviceActionLight = "light";
weChatProfile.reportAction.lightActionStatus = 0; /* 0: light off */
}
/* profile report */
IoTProfilePropertyReport(CONFIG_USER_ID, &weChatProfile); // < 调用IoTProfilePropertyReport函数,传入用户ID和weChatProfile结构体指针作为参数,向云端报告设备属性信息
}

// < 这是一个示例主任务入口函数,在这里我们会设置wifi/cjson/mqtt准备好并等待有任何工作要做的while循环中
static hi_void *DemoEntry(const char *arg)
{
WifiStaReadyWait(); // < 等待wifi连接成功
cJsonInit(); // < 初始化cjson库
IoTMain(); // < 调用IoTMain函数,初始化MQTT客户端并连接到服务器
/* 云端下发回调 */
IoTSetMsgCallback(DemoMsgRcvCallBack); // < 调用IoTSetMsgCallback函数,将DemoMsgRcvCallBack函数指针作为参数传入,设置MQTT消息接收回调函数
/* 主动上报 */
#ifdef TAKE_THE_INITIATIVE_TO_REPORT
while (1) {
/* 用户可以在这调用发布函数进行发布,需要用户自己写调用函数 */
IotPublishSample(); // 发布例程
#endif
TaskMsleep(ONE_SECOND);
}
return NULL;
}

// < 这是一个示例入口函数,在这里我们创建了一个任务,
// 所有的工作都在demo_entry中完成了
#define CN_IOT_TASK_STACKSIZE 0x1000
#define CN_IOT_TASK_PRIOR 25
#define CN_IOT_TASK_NAME "IOTDEMO"

static void AppDemoIot(void)
{
osThreadAttr_t attr;
IoTWatchDogDisable();

attr.name = "IOTDEMO";
attr.attr_bits = 0U;
attr.cb_mem = NULL;
attr.cb_size = 0U;
attr.stack_mem = NULL;
attr.stack_size = CN_IOT_TASK_STACKSIZE;
attr.priority = CN_IOT_TASK_PRIOR;

if (osThreadNew((osThreadFunc_t)DemoEntry, NULL, &attr) == NULL) {
printf("[mqtt] Falied to create IOTDEMO!\n");
}
}

SYS_RUN(AppDemoIot);

发送函数

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
#define CN_PROFILE_TOPICFMT_TOPIC            "$shadow/operation/19VUBHD786/mqtt"
//向微信小程序发送设备的属性信息。
int IoTProfilePropertyReport(char *deviceID, WeChatProfile *payload)
{
int ret = -1;
char *topic;
char *msg;

// 函数有两个参数:deviceID和payload。
// deviceID是一个字符串,表示设备的唯一标识符;
// payload是一个WeChatProfile类型的指针,表示设备的属性信息。
if ((deviceID == NULL) || (payload== NULL)) {
return ret;
}
// 函数内部定义了三个局部变量:ret,topic和msg。
// ret是一个整数,用于存储发送结果;
// topic是一个字符串指针,用于存储发送的主题;
// msg是一个字符串指针,用于存储发送的消息。
topic = MakeTopic(CN_PROFILE_TOPICFMT_TOPIC, deviceID, NULL);
// 调用MakeTopic函数,根据CN_PROFILE_TOPICFMT_TOPIC和deviceID生成一个主题,并赋值给topic。
// CN_PROFILE_TOPICFMT_TOPIC是一个宏定义,表示主题的格式;
// MakeTopic函数是一个自定义函数,用于拼接字符串。
if (topic == NULL) {
return;
}
msg = MakeProfileReport(payload);
// 调用MakeProfileReport函数,根据payload生成一个消息,并赋值给msg。
// MakeProfileReport函数是一个自定义函数,用于将WeChatProfile类型的结构体转换为JSON格式的字符串。
if ((topic != NULL) && (msg != NULL)) {
ret = IotSendMsg(0, topic, msg);
// 如果topic和msg都不为空,则调用IotSendMsg函数,向微信小程序发送消息,并将返回值赋给ret。
// IotSendMsg函数是一个自定义函数,用于通过MQTT协议发送消息。
}

hi_free(0, topic);
cJSON_free(msg);
// 释放topic和msg占用的内存。

return ret;
// 函数返回一个整数,表示发送结果。如果成功,返回0;如果失败,返回-1。
}

<六> MQTT通讯实验

小程序环境配置

跟着教程走,在cloudfunctions选择环境之前会新建一个云环境,但是选择的时候却没有这个选项。关闭工程重新打开即可刷新。

工程源码阅读

首先看工程配置。souce中的多个c文件被编译为静态库供父级使用;在父级的BUILD文件中调用app_demo_iot,故其为app层入口。

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
// iottencent_demo/BUILD.gn
static_library("appDemoIot") {
sources = [
"app_demo_iot.c",
"cjson_init.c",
"iot_hmac.c",
"iot_log.c",
"iot_main.c",
"iot_profile.c",
"iot_sta.c",
]

include_dirs = [
"./",
"//utils/native/lite/include",
"//kernel/liteos_m/kal/cmsis",
"//base/iot_hardware/peripheral/interfaces/kits",
"//device/hisilicon/hispark_pegasus/sdk_liteos/third_party/lwip_sack/include/lwip",
"//third_party/cJSON",
"//device/hisilicon/hispark_pegasus/sdk_liteos/third_party/mbedtls/include/mbedtls",
"//foundation/communication/wifi_lite/interfaces/wifiservice",
"//device/hisilicon/hispark_pegasus/sdk_liteos/third_party/paho.mqtt.c/include/mqtt",
"//device/hisilicon/hispark_pegasus/sdk_liteos/third_party/libcoap/include/coap2",
]

defines = [ "WITH_LWIP" ]
}

iot_profile

1
#define CN_PROFILE_TOPICFMT_TOPIC            "$shadow/operation/Pegasus/Board1"
  • $shadow表示设备影子的主题前缀,设备影子是一种用于存储和同步设备状态的机制。
  • /operation表示操作类型的主题分隔符,操作类型包括获取、更新、删除等。
  • /Pegasus/Board1表示设备的标识符,用于区分不同的设备。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
//定义一个函数,用于上报物联网设备的配置信息,其实就是将wechatprofile转化为话题消息并发送
int IoTProfilePropertyReport(char *deviceID, WeChatProfile *payload){
int ret = -1; //定义一个变量,表示返回值,默认为-1,表示失败
char *topic; //定义一个变量,表示主题字符串
char *msg; //定义一个变量,表示消息字符串
if ((deviceID == NULL) || (payload== NULL)) { //如果参数为空
return ret; //返回-1,表示失败
}
topic = MakeTopic(CN_PROFILE_TOPICFMT_TOPIC, deviceID, NULL); //调用MakeTopic()函数,根据设备ID和一个固定的主题格式,生成一个主题字符串,并赋值给topic变量
if (topic == NULL) { //如果主题字符串为空
return ret; //返回-1,表示失败
}
msg = MakeProfileReport(payload); //调用MakeProfileReport()函数,根据设备的配置信息结构体,生成一个JSON格式的消息字符串,并赋值给msg变量
if ((topic != NULL) && (msg != NULL)) { //如果主题字符串和消息字符串都不为空
ret = IotSendMsg(0, topic, msg); //调用IotSendMsg()函数,将消息发送到主题,并将返回值赋值给ret变量
}
hi_free(0, topic); //释放主题字符串占用的内存
cJSON_free(msg); //释放消息字符串占用的内存
return ret; //返回ret变量
}

通信数据格式

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
//定义一个结构体类型,表示设备影子的状态信息
typedef struct {
const char *subState; //表示订阅状态,有sub和unsub两种取值
const char *subReport; //表示上报状态,有report和unreport两种取值
const char *reportVersion; //表示上报版本,用于同步设备影子的版本
const char *Token; //表示令牌,用于验证设备身份
}WeChatProfileStatus;

//定义一个结构体类型,表示设备的上报信息
typedef struct {
int lightActionStatus; //表示灯的动作状态,有0和1两种取值,分别表示关和开
int motorActionStatus; //表示电机的动作状态,有0和1两种取值,分别表示停止和运行
int temperatureData; //表示温度数据,单位是摄氏度
int humidityActionData; //表示湿度数据,单位是百分比
int lightIntensityActionData; //表示光照强度数据,单位是勒克斯
const char *subDeviceActionLight; //表示灯的动作指令,有on和off两种取值,分别表示开和关
const char *subDeviceActionMotor; //表示电机的动作指令,有start和stop两种取值,分别表示运行和停止
const char *subDeviceActionTemperature; //表示温度的动作指令,有get和set两种取值,分别表示获取和设置
const char *subDeviceActionHumidity; //表示湿度的动作指令,有get和set两种取值,分别表示获取和设置
const char *subDeviceActionLightIntensity; //表示光照强度的动作指令,有get和set两种取值,分别表示获取和设置
}WeChatProfileReporte;

//定义一个结构体类型,表示设备影子的完整信息
typedef struct {
const char *subscribeType; //表示订阅类型,有status、report、all三种取值,分别表示订阅状态、上报、全部
WeChatProfileStatus status; //表示设备影子的状态信息结构体
WeChatProfileReporte reportAction; //表示设备的上报信息结构体
}WeChatProfile;

app_demo_iot.c

主函数创建了一个线程,注册消息接收回调函数,并且可以不断发送消息汇报当前状态。

回调函数解析数据,并依此控制led亮灭。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
IoTSetMsgCallback(DemoMsgRcvCallBack);//回调函数的注册

static void DemoMsgRcvCallBack(int qos, const char *topic, const char *payload)
{
IOT_LOG_DEBUG("RCVMSG:QOS:%d TOPIC:%s PAYLOAD:%s\r\n", qos, topic, payload);
/* 云端下发命令后,板端的操作处理 */
if (strstr(payload, WECHAT_SUBSCRIBE_LIGHT) != NULL) {
if (strstr(payload, WECHAT_SUBSCRIBE_LIGHT_OFF_STATE) != NULL) {
wechatControlDeviceMsg(HI_GPIO_VALUE1);
g_ligthStatus = HI_FALSE;
} else {
wechatControlDeviceMsg(HI_GPIO_VALUE0);
g_ligthStatus = HI_TRUE;
}
}
return HI_NULL;
}

strstr() 函数搜索一个字符串在另一个字符串中的第一次出现。

  • 找到所搜索的字符串,则该函数返回第一次匹配的字符串的地址;
  • 如果未找到所搜索的字符串,则返回NULL。

需要注意的是收到消息后第一时间触发的回调函数并不是这个函数,查看DEBUG信息可知

[DEBUG][MsgRcvCallBack] RCVMSG:QOS:0 TOPIC:GNJOUT1LQ6/Board1/data PAYLOAD:{“light”:0}

[DEBUG][ProcessQueueMsg] QUEUEMSG:QOS:0 TOPIC:GNJOUT1LQ6/Board1/data PAYLOAD:{“light”:0}

[DEBUG][DemoMsgRcvCallBack] RCVMSG:QOS:0 TOPIC:GNJOUT1LQ6/Board1/data PAYLOAD:{“light”:0}

系统先通过MsgRcvCallBack()ProcessQueueMsg()处理后,再将信息给到我们自己定义的回调函数中

iot_main

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
//定义一个枚举类型,表示消息的类型
typedef enum {
EN_IOT_MSG_PUBLISH = 0, //发布消息
EN_IOT_MSG_RECV, //接收消息
}EnIotMsg;

//定义一个结构体类型,表示消息的内容
typedef struct {
EnIotMsg type; //消息的类型
int qos; //消息的服务质量
const char *topic; //消息的主题
const char *payload; //消息的有效载荷
}IoTMsg_t;

//定义一个结构体类型,表示应用程序的回调信息
typedef struct {
bool stop; //是否停止运行
unsigned int conLost; //连接丢失的次数
unsigned int queueID; //消息队列的ID
unsigned int iotTaskID; //物联网任务的ID
fnMsgCallBack msgCallBack; //消息回调函数的指针
MQTTClient_deliveryToken tocken; //发布消息的令牌
}IotAppCb_t;

//声明一个全局变量,存储应用程序的回调信息
static IotAppCb_t gIoTAppCb;

/**
* @brief Obtains a message in a message queue.
*
* @param mq_id Indicates the message queue ID, which is obtained using osMessageQueueNew.
* @param msg_ptr Indicates the pointer to the buffer for storing the message to be retrieved from the message queue.
* @param msg_prio Indicates the pointer to the buffer for storing the priority of the message to be retrieved from the message queue. This parameter is not used.
* @param timeout Indicates the timeout duration.
* @return Returns the CMSIS-RTOS running result.
* @since 1.0
* @version 1.0
*/
osStatus_t osMessageQueueGet (osMessageQueueId_t mq_id, void *msg_ptr, uint8_t *msg_prio, uint32_t timeout);

/**
* When you want to send some messages to the iot server(including the response message),
* please call this api
* @param qos: the mqtt qos,:0,1,2
* @param topic: the iot mqtt topic
* @param payload: the mqtt payload
*
* @return 0 success while others failed
*
* @instruction: if success means we write the message to the queue susccess,
* not means communicate with the server success
*/
int IotSendMsg(int qos, const char *topic, const char *payload);

接收到信息的“底层”回调函数

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
#define CN_TOPIC_SUBSCRIBE_NUM     (sizeof(gDefaultSubscribeTopic) / sizeof(const char *))
static int MsgRcvCallBack(char *context, char *topic, int topicLen, MQTTClient_message *message)
{
IoTMsg_t *msg;
char *buf;
unsigned int bufSize;
int topiLen = topicLen;

if (topiLen == 0) {
topiLen = strlen(topic);
}
bufSize = topiLen + 1 + message->payloadlen + 1 + sizeof(IoTMsg_t);
buf = hi_malloc(0, bufSize);
if (buf != NULL) {
msg = (IoTMsg_t *)buf;
buf += sizeof(IoTMsg_t);
bufSize -= sizeof(IoTMsg_t);
msg->qos = message->qos;
msg->type = EN_IOT_MSG_RECV;
(void)memcpy_s(buf, bufSize, topic, topiLen);
buf[topiLen] = '\0';
msg->topic = buf;
buf += topiLen + 1;
bufSize -= (topiLen + 1);
(void)memcpy_s(buf, bufSize, message->payload, message->payloadlen);
buf[message->payloadlen] = '\0';
msg->payload = buf;
IOT_LOG_DEBUG("RCVMSG:QOS:%d TOPIC:%s PAYLOAD:%s\r\n", msg->qos, msg->topic, msg->payload);
if (IOT_SUCCESS != osMessageQueuePut(gIoTAppCb.queueID, &msg, 0, CN_QUEUE_WAITTIMEOUT)) {
IOT_LOG_ERROR("Wrie queue failed\r\n");
hi_free(0, msg);
}
}

MQTTClient_freeMessage(&message);
MQTTClient_free(topic);
return 1;
}
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
//定义一个函数,用于处理消息队列中的消息
static int MqttProcessQueueMsg(MQTTClient client, IoTMsg_t *msg, MQTTClient_message pubMsg){
int ret = 0;
switch (msg->type) { //根据消息的类型执行不同的操作
case EN_IOT_MSG_PUBLISH: //如果消息类型是发布消息
pubMsg.payload = (void *)msg->payload; //将消息的有效载荷赋值给发布消息结构体
pubMsg.payloadlen = (int)strlen(msg->payload); //将消息的长度赋值给发布消息结构体
pubMsg.qos = msg->qos; //将消息的服务质量赋值给发布消息结构体
pubMsg.retained = 0; //将消息的保留标志赋值给发布消息结构体
ret = MQTTClient_publishMessage(client, msg->topic, &pubMsg, &gIoTAppCb.tocken); //调用MQTT客户端对象的发布消息方法,将消息发送到指定的主题
if (ret != MQTTCLIENT_SUCCESS) { //如果发送失败
IOT_LOG_ERROR("MSGSEND:failed\r\n"); //打印错误日志
}
IOT_LOG_DEBUG("MSGSEND:SUCCESS\r\n"); //打印调试日志
gIoTAppCb.tocken++; //将令牌加一
break;
case EN_IOT_MSG_RECV: //如果消息类型是接收消息
if (gIoTAppCb.msgCallBack != NULL) { //如果应用程序回调信息中有消息回调函数的指针
gIoTAppCb.msgCallBack(msg->qos, msg->topic, msg->payload); //调用该函数,并将消息的服务质量、主题和有效载荷作为参数传递
}
break;
default: //如果消息类型是其他值
break;
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// <use this function to deal all the comming message
static int ProcessQueueMsg(MQTTClient client)
{
unsigned int ret;
unsigned int msgSize;
IoTMsg_t *msg;
unsigned int timeout;
MQTTClient_message pubmsg = MQTTClient_message_initializer;

timeout = CN_QUEUE_WAITTIMEOUT;
do {
msg = NULL;
msgSize = sizeof(hi_pvoid);
ret = osMessageQueueGet(gIoTAppCb.queueID, &msg, &msgSize, timeout);
if (msg != NULL) {
IOT_LOG_DEBUG("QUEUEMSG:QOS:%d TOPIC:%s PAYLOAD:%s\r\n", msg->qos, msg->topic, msg->payload);
MqttProcessQueueMsg(client, msg, pubmsg);
hi_free(0, msg);
}
timeout = 0; // < continous to deal the message without wait here
} while (ret == IOT_SUCCESS);

return 0;
}

遇到问题

理论上来说板子会根据云端信息控制灯的亮灭,但实际上云端只能接受信息更新状态,并不能控制板子(会提示发布失败)