vscode esp-idf 远程开发
远程不能直接使用vscode idf插件的命令行, 需要打开idf插件的配置,将其中的path等信息改成bat
运行一下.
示例如下:
set path=D:\r\esp\.espressif\tools\xtensa-esp32-elf\esp-2021r2-8.4.0\xtensa-esp32-elf\bin;D:\r\esp\.espressif\tools\xtensa-esp32s2-elf\esp-2021r2-8.4.0\xtensa-esp32s2-elf\bin;D:\r\esp\.espressif\tools\xtensa-esp32s3-elf\esp-2021r2-8.4.0\xtensa-esp32s3-elf\bin;D:\r\esp\.espressif\tools\riscv32-esp-elf\esp-2021r2-8.4.0\riscv32-esp-elf\bin;D:\r\esp\.espressif\tools\esp32ulp-elf\2.28.51-esp-20191205\esp32ulp-elf-binutils\bin;D:\r\esp\.espressif\tools\esp32s2ulp-elf\2.28.51-esp-20191205\esp32s2ulp-elf-binutils\bin ;D:\r\esp\.espressif\tools\cmake\3.20.3\bin;D:\r\esp\.espressif\tools\openocd-esp32\v0.10.0-esp32-20211111\openocd-esp32\bin;D:\r\esp\.espressif\tools\ninja\1.10.2;D:\r\esp\.espressif\tools\idf-exe\1.0.3;D:\r\esp\.espressif\tools\ccache\4.3\ccache-4.3-windows-64;D:\r\esp\.espressif\tools\dfu-util\0.9\dfu-util-0.9-win64;D:\r\esp\.espressif\tools\idf-git\2.30.1\cmd\;D:\r\esp\.espressif\python_env\idf4.4_py3.8_env\Scripts\;%PATH%
set IDF_PATH=d:\r\esp\esp-idf\
set IDF_TOOLS_PATH=D:\r\esp\.espressif
Set OPENOCD_SCRIPTS=D:\\r\\esp\\.espressif\\tools\\openocd-esp32\\v0.10.0-esp32-20211111/openocd-esp32/share/openocd/scripts
set IDF_CCACHE_ENABLE=1
还有一种方式是在esp-idf
下有一些export.bat/.sh
文件, 是用于配置idf环境的,不过可能需要修改.
通过 SSHFS windows manager
可以挂载远程硬盘. 参考文章
腾讯连连8266 SDK研究
项目地址: https://github.com/tencentyun/qcloud-iot-esp-wifi/tree/master/qcloud-iot-esp8266-demo
文档地址: https://cloud.tencent.com/document/product/1081/48370
8266项目的SDK又是基于腾讯的C SDK抽取的, C SDK地址: https://github.com/tencentyun/qcloud-iot-explorer-sdk-embedded-c
要修改的和供调用的
8266项目目录下components/qcloud_iot/qcloud_iot_c_sdk/includes
中的几个文件是要修改的参数和调用的api放置的地方.
还有三元组信息在 components/qcloud_iot/qcloud_iot_c_sdk/platform
下的HAL_Device_freertos.c
文件中. 居然不是在头文件里定义, 感觉挺糟糕的.
要调用的API: qcloud_iot_export.h
这个qcloud_iot_export.h
文件实际上include了所有在exports
目录里面的.h文件. 所有的接口API在其中描述.
参数修改 qcloud_iot_export_variables.h
MQTT的心跳时间240s建议改为200s. 默认240s与服务器端太一致了, 设备时钟慢一点就会被服务器踢出.
/* default MQTT keep alive interval (unit: ms) */
#define QCLOUD_IOT_MQTT_KEEP_ALIVE_INTERNAL (200 * 1000) // 原设置: 240*1000
参数修改 HAL_Device_freertos.c
#ifdef DEBUG_DEV_INFO_USED
/* product Id */
static char sg_product_id[MAX_SIZE_OF_PRODUCT_ID + 1] = "PRODUCT_ID"; // 修改
/* device name */
static char sg_device_name[MAX_SIZE_OF_DEVICE_NAME + 1] = "YOUR_DEV_NAME"; // 根据mac生成的话, 需要在代码中动态修改 . 似乎应该在HAL_SetDevInfo()调用前或者干脆就在这个函数里修改
/* device secret of PSK device */
static char sg_device_secret[MAX_SIZE_OF_DEVICE_SECRET + 1] = "YOUR_IOT_PSK";
/* region */
static char sg_region[MAX_SIZE_OF_PRODUCT_REGION + 1] = "ap-guangzhou";
#ifdef GATEWAY_ENABLED
/* sub-device product id */
static char sg_sub_device_product_id[MAX_SIZE_OF_PRODUCT_ID + 1] = "PRODUCT_ID";
/* sub-device device name */
static char sg_sub_device_name[MAX_SIZE_OF_DEVICE_NAME + 1] = "YOUR_SUB_DEV_NAME";
#endif
#ifdef DEV_DYN_REG_ENABLED
/* product secret for device dynamic Registration */
static char sg_product_secret[MAX_SIZE_OF_PRODUCT_SECRET + 1] = "YOUR_PRODUCT_SECRET"; // 动态验证需要改这儿
#endif
编译文件修改component.mk
如果在main
目录下自定义了新的文件夹用于放源码, 需要将这个文件夹加入编译路径 ,在component.mk
中, 如增加一行:
COMPONENT_SRCDIRS += ./lamploop
主流程
在samples
目录下有4个demo程序, 实际上是根据sdkconfig
里面的配置选择不同的demo进行编译. 以data_template_light
的主流程示例如下:
main.c中的app_main()创建qcloud_demo_task任务
-->演示获取WIFI信息
-->联网
-->设置时间服务器
-->调用主逻辑qcloud_iot_explorer_demo
-->qcloud_iot_explorer_demo在samples/data_template_light/light_data_template_sample.c中定义
-->初始化连接信息_setup_connect_init_params
-->构建物模型也就是所谓的数据模板IOT_Template_Construct
-->初始化数据模板_init_data_template
-->注册模板属性_register_data_template_property
-->注册模板动作_register_data_template_action
-->获取系统信息_get_sys_info, 并上报IOT_Template_Report_SysInfo_Sync
-->获取数据状态IOT_Template_GetStatus_sync
-->处理下行数据逻辑deal_down_stream_user_logic
-->使能ota任务enable_ota_task
-->初始化报告时钟
-->倒计时10秒
-->进入task的死循环
-->断开连接(!IOT_Template_IsConnected)超过20秒直接跳出死循环
-->判断固件在下载is_fw_downloading, 睡0.5秒后,进入下次循环
-->收到控制消息sg_control_msg_arrived
-->进入控制信令处理deal_down_stream_user_logic
-->回复服务器IOT_Template_ControlReply
-->构造变化属性的报告deal_up_stream_user_logic(pReportDataList, &ReportCount)
-->如果属性有变化, 则构造报告数组IOT_Template_JSON_ConstructReportArray
-->上传报告数组IOT_Template_Report
-->如果开启了事件处理, 则进行事件处理.
-->睡到1秒
-->如果各种原因跳出了循环(看到的原因只有断开连接)
-->关闭ota任务
-->销毁模板
根据乐鑫v3.1模板加入qcloud_iot模块的方式
下面基于乐鑫的v3.1example/project_template
项目基础上, 增加qcloud_iot功能
为解说方便, 将qcloud-iot-esp8266-demo
项目称之为qcloud
项目, 将将project_template
项目称之为template
项目
复制
- 要将qcloud项目
components\qcloud_iot\qcloud_iot_c_sdk
目录复制到template项目的components\qcloud_iot_c_sdk
,去掉一级目录qcloud_iot
. - 将
components\component.mk
复制过来 - 将
sdkconfig
复制过来, 或者进入make menuconfig
手动修改. 主要要改component config-->SSL-->mbedTLS
下的内容:
- (2560) TLS maximum OUTPUT message content length
- (2560) TLS maximum INPUT message content length
- TLS Protocol Role (Client)--->
在`TLS Key Exchange Methods`下修改:
- [*] Enable pre-shared-key ciphersuites
- [*] Enable PSK based ciphersuite modes
- [ ] Enable DHE-PSK based ciphersuite modes
- [ ] Enable ECDHE-PSK based ciphersuite modes
- [*] Enable RSA-PSK based ciphersuite modes
- [*] Enable RSA-only based ciphersuite modes
- [ ] Enable DHE-RSA based ciphersuite modes
深入文档
要深入了解文档,还是要看C-SDK项目的示例而不是8266-C-SDK的项目示例
qcloud-iot-explorer-sdk-embedded-c
视频教程
OTA分片下载例程更新
https://git.code.tencent.com/hubertxxu/qcloud_iot_explorer_esp8266
vscode远程开发
参考https://code.visualstudio.com/docs/remote/ssh-tutorial
远程开发的机器称为服务器, 如果是win10的话, 要安装并运行sshd服务, 并且服务使用powershell
作为命令行, 参见前一篇文章.
如果开发服务器在内网,可以使用中转穿透服务frp.
如果都装好了, 使用ssh也可以登录了, 如果是通过frp登录的话, 命令可能是ssh username@frp_server_ip -p 7777 -oPort=7000
, 其中7777是frpc客户端(也就是开发服务器)要求frps服务端开启的端口, 7000是frps固有服务端口.
在vscode上点击左下角, 选择connect to host...
输入ssh命令, 不需要输入-oPort=7000
部分,按说明还需要加一个-A
(干吗用的还不知道), 如: ssh username@frp_server_ip -p 7777 -A
然后就可以连接成功了.
如果远程开发服务器上有docker容器, 可以在vscode的cmd窗口进入docker容器, 命令如下:
docker exec -it 容器名 /bin/bash
如果容器里面没有bash
还可以把bash换成sh
有个很有意思的现象: 如果vscode连接的开发服务器必须同时开着sshd
和frpc
, 但是如果vscode已经连上了, sshd
就可以关掉了, 只开着frpc
就可以.这时候ssh
命令行已经无法连接上了. 似乎vscode借用了22端口,但连接上以后没有走sshd协议
windows10 开启ssh服务
参考https://winaero.com/enable-openssh-server-windows-10/#:~:text=Enable%20the%20OpenSSH%20Server%20in%20Windows%2010%201,on%20the%20Install%20button.%205%20Restart%20Windows%2010.
https://docs.microsoft.com/en-us/windows-server/administration/openssh/openssh_install_firstuse
- 打开"设置->应用->可选功能->添加功能", 搜索SSH, 将
SSH服务器
和SSH客户端
都安装. - 文章要求装完重启,但好像不重启也可以. 在
C:\windows\system32\OpenSSH
目录下就会安装ssh的相关文件, 包括sshd.exe
, 可以直接运行sshd就开启服务,也可以按文章的介绍在服务
中开启. - 运行菜单
services.msc
, 查看OpenSSH SSH server
项, 双击, 选择登录
页面,查看或修改可以登录的用户. 设置服务为自动
并开启运行. - 打开一个
cmd
,运行ssh-keygen -A
生成服务端key, 默认生成的位置在C:\ProgramData\ssh
. 这儿还存放sshd_config
文件, 如果没有就创建一个. - 运行
ssh-keygen
,生成本地ssh keyid_rsa
和id_rsa.pub
,位置在C:\Users\username/.ssh/
,这儿还存放authorized_keys
.authorized_keys
就是其他客户端的pub key的集合 - 在客户端运行
ssh-keygen
,生成本地ssh keyid_rsa
和id_rsa.pub
, 将id_rsa.pub
复制一份改名为authorized_keys
到服务端的C:\Users\username/.ssh/
目录 - 设置服务端开启哪个命令行, 可以在powershell里开启, 也可以打开
regedit
,New-ItemProperty -Path "HKLM:\SOFTWARE\OpenSSH" -Name DefaultShell -Value "C:\Windows\System32\WindowsPowerShell\v1.0\powershell.exe" -PropertyType String -Force
当然你也可以改成其他命令行如
cmd
或者bash
,但是如果要使用vscode远程的话必须设置为如上的powershell
总结: 涉及的目录和注册表有以下几项:
软件安装目录: C:\windows\system32\OpenSSH
服务key存放目录: C:\ProgramData\ssh
用户Key存放目录: C:\Users\username/.ssh/
服务名: OpenSSH SSH Server, 或 sshd
注册表项: HKLM:\SOFTWARE\OpenSSH, DefaultShell
frp 使用阿里云对SSH 内网穿透
官网示例是针对端口全开的公网ip, 而阿里云是有安全策略, 需要开放端口.
frp用起来算是及其简单惬意了.
- 阿里云服务器和内网SSH服务器分别下载frp release, 目前最新版本0.38.0
- 我用的阿里云服务器是CentOS, 其他linux也类似. 登录阿里云服务器这一步安装到
systemd
系统服务. 先进入frp解压目录, 然后是如下步骤:cp systemd/*.* /usr/lib/systemd/system/ cp frps /usr/bin/ chmod +x /usr/bin/frps mkdir /etc/frp cp frps.ini /etc/frp/ systemctl start frps systemctl status frps
最后一条命令可以看到frps是否启动成功.
可以看一下cat /etc/frp/frps.ini
的内容, 默认是:[common] bind_port = 7000
这时候可以看一下frps的端口占用:
#netstat -tunlp|grep frps tcp6 0 0 :::7000 :::* LISTEN 4928/frps
- 登录阿里云服务器后台
aliyun.com
, 在ECS服务器->实例->安全组
中的入方向
添加一条TCP记录, 端口号写一个区间:7000/7100
.后面frp配置的所有端口号都要在这个范围内才能访问. 授权对象为0.0.0.0/0
- SSH服务器解压缩frp, 修改目录中的
frpc.ini
,假设服务器地址是x.x.x.x
,如下:[common] server_addr = x.x.x.x server_port = 7000
[ssh]
type = tcp
local_ip = 127.0.0.1
local_port = 22
remote_port = 7001
运行`frpc`
5.这时候可以回到阿里云服务器看一下frps的端口占用,发现服务器打开了7001端口:
netstat -tunlp|grep frps
tcp6 0 0 :::7000 ::: LISTEN 4928/frps
tcp6 0 0 :::7001 ::: LISTEN 4928/frps
6. SSH客户端连接SSH服务器
ssh username@x.x.x.x -p 7001 -oPort=7000
内网穿透
https://zhuanlan.zhihu.com/p/303175108
本想用ngork, 然而注册总是不成功. 另一个natapp的页面看起来似乎运营实力一般,再加上免费版本有些限制, 所以就看了看其他的方式.
因为有个阿里云的服务器, 最后选择frp
行不通的方法
- 端口映射. 现在家庭和公司PPPoE获取的地址都已经是运营商的内部NAT地址了, 以100开头, 所以路由器端口映射行不通.
腾讯连连8266 SDK编译方式
注意: 固件编译只能通过 docker进行.
原因是:
- 乐鑫的最新8266 RTOS SDK v3.4腾讯连连的components不支持;
- 腾讯连连只支持8266 RTOS SDK v3.1, 而8266 RTOS SDK v3.1的文档中竟然没有提编译工具链, 而v3.4文档中的编译工具链gcc版本v8.4.0也不支持v3.1
- 腾讯开发人员用的编译工具链gcc版本是v5.2.0(
xtensa-lx106-elf-linux64-1.22.0-92-g8facf4c-5.2.0.tar.gz
), 所以干脆用了腾讯提供的docker img了.
docker img获取方式: docker pull hubertxxu/esp8266_build:0.1
, 由腾讯的xph
提供.
docker项目文件夹在/r/
下.
docker linux 版本:
uname -a
Linux bff9dfda8fd0 5.10.16.3-microsoft-standard-WSL2 #1 SMP Fri Apr 2 22:23:49 UTC 2021 x86_64 x86_64 x86_64 GNU/Linux
支持yum install
命令
docker容器挂载windows目录的启动方式:
docker run --name esp -v path/to/host/folder:/path/to/container/folder -dt hubertxxu/esp8266_build:0.1
挂载以后进行编译.
container修改esp工具链可访问性
chmod -R 777 /home/esp8266
2023/6/15更新: yum命令的源用不了了, 可以换Aliyun的
cd /etc
mv yum.repos.d yum.repos.d.backup
mkdir yum.repos.d
wget -O /etc/yum.repos.d/CentOS-Base.repo http://mirrors.aliyun.com/repo/Centos-8.repo
另外, dnf
是比yum更先进的包管理工具,可以换用dnf
启动ssh但却连不上IP...
使用了下面一长串的命令启动了ssh, 但是docker内的ip不是真实ip,host ping container是不通的. 我在host上建立了ftp server,通过container登录来看container的ip,发现竟然就是host的ip.
使用如下步骤开启了sshd
yum install net-tools
yum install openssh-server
yum install passwd
mkdir -p /var/run/sshd
ssh-keygen -A
然后编辑sshd配置
vi /etc/ssh/sshd_config
打开以下配置
Port 22
ListenAddress 0.0.0.0
PasswordAuthentication Yes
设置root密码
passwd
启动服务
/usr/sbin/sshd -D &
查看进程和端口占用
ps -A
netstat -nptl
本地连接测试
ssh localhost
Win10 Microsoft Store不能打开的问题
反映预测重要性的俗语
男怕入错行,女怕嫁错郎。
天下大势,浩浩汤汤。顺之者昌,逆之者亡。
C语言的几点思考
1. C语言使用enum做顺序执行状态机的思考
最近做完了通过8266 Qcloud AT做腾讯连连的台灯的项目, 使用enum做连接状态记录, 在做网络连接的过程中发现了一些问题, 这儿讨论一下.
命名
typedef enum _ESP_STATUS{
ESP_STATUS_INIT,
ESP_STATUS_READY,
ESP_STATUS_GOT_MAC,
ESP_STATUS_CONFIGED_NTP_SERVER,
ESP_STATUS_GOT_TIME,
ESP_STATUS_CONNECTED_MQTT,
ESP_STATUS_SUBED_MQTT,
ESP_STATUS_WIFI_DISCONNECTTED,
}ESP_STATUS;
上面这种enum用下划线, type去掉下划线, 并在枚举项内体现type名称的命名习惯从华为开始就确定了. 这个不需要讨论.
从上到下依次为按时间顺序会依次进入的状态, 这个也没有问题.
GOT_MAC
先动作再对象也是对的.
想讨论的READY/GOT/CONFIGED
这种过去时态. 我认为不如进行时态. 过去时态会造成当前状态与上一个状态在意义上的耦合. 因为READY以后就要获取MAC, 所以READY
不如改名为GETTING_MAC
, GOT_MAC
不如改名为CONFIGING_NTP_SERVER
. 也就是凡是要主动做操作的, 都用进行时态.
另外还有一种是处在状态中不主动动作, 而是被动等待状态改变(相当于什么也不做). 名称应改为WAITFOR_DO_XXX
, 如WAITFOR_RECONNECT_WIFI
.
修改如下:
typedef enum _ESP_STATUS{
ESP_STATUS_WAITFOR_READY,
ESP_STATUS_GETTING_MAC,
ESP_STATUS_CONFIGING_NTP_SERVER,
ESP_STATUS_GETTING_TIME,
ESP_STATUS_CONNECTING_MQTT,
ESP_STATUS_SUBING_MQTT,
ESP_STATUS_REPORTING_MQTT_PROPERTY,
ESP_STATUS_WAITFOR_CONNECT_WIFI
}ESP_STATUS;
其中, ESP_STATUS_SUBING_MQTT
又可以分为对SERVICE和PROPERTY分别SUB, 两者虽然没有先后之分, 但是仍应该人为规定先后顺序并放入枚举项中. 改为:
typedef enum _ESP_STATUS{
ESP_STATUS_INIT, // 这时候等待READY
ESP_STATUS_GETTING_MAC,
ESP_STATUS_CONFIGING_NTP_SERVER,
ESP_STATUS_GETTING_TIME,
ESP_STATUS_CONNECTING_MQTT,
ESP_STATUS_SUBING_MQTT_SERVICE,
ESP_STATUS_SUBING_MQTT_PROPERTY,
ESP_STATUS_REPORTING_MQTT_PROPERTY,
}ESP_STATUS;
2. C语言文件结构
.h文件
是用来放置public公有性质的内容, #define 常量
, typedef enum
, typedef struct
, extern 全局变量
, 公用函数声明返回值 函数值(参数列表);
. 不可以做c文件的私有全局变量声明, 也不可以做函数定义, 也不可以做c文件私有函数声明.
.c文件
用来放置公有和私有的全局变量定义, 私有函数声明, 共有和私有的函数定义.
.h
文件内部由于有了#ifndef 头文件名 #define 头文件名 #endif
的结构, 所以可以#include
其他.h
文件.
.c
文件任何时候都不应被其他文件#include
将1对同名.h
和.c
文件作为一个模块的话, 项目中为模块划分层次, 底层模块为上层提供服务, 设置基本共识. 需要注意的是上层模块可以#include下层模块, 但下层模块不能#include上层模块, 也就是说, 下层不应依赖上层, 上层的任何变化不应使下层模块无法使用, 无论上层如何变化下层都应提供一致的服务.
为进一步让上层解耦, 摆脱对特定实现方式的依赖, 下层可以定义标准接口性质的.h
文件, 上层#include和调用标准.h, 下层在不修改标准.h的情况下进行升级修改, 或者完全替换为另一种实现.
同层模块可以相互#include.
目前考虑, 项目基础配置处于最底层L0. 硬件资源驱动处于L1, 基于硬件驱动提供的服务处于L2, 操作系统处于L3, 基于操作系统提供的服务处于L4, 具体应用处于L5.
按这种划分, 以灯为例, 作表如下:
层数 | 层名 | 功能模块 |
---|---|---|
L5 | 应用层 | 灯的逻辑功能 |
L4 | 系统服务层 | 系统Tick的时钟 |
L3 | 系统层 | RTOS |
L2 | 硬件服务层 | 驱动回调 |
L1 | 硬件驱动层 | Uart, Adc |
L0 | 配置层 |
freeRTOS入门与坑
文档
amazon官方文档, 中文好评!https://docs.amazonaws.cn/freertos/index.html
官网: https://freertos.org/
官方文档,英文的
AWS上的文档,也是英文的
FreeRTOS与RT-Thread对比: 后者自带很多第三方协议, 如i2c/mqtt等等, 而FreeRTOS似乎要自己找自己组装进去.
根据其他乱七八糟文档的整理
-
任务:
xTaskCreate
建立多个任务, 然后使用vTaskStartScheduler
调度任务函数. 任务函数执行完成任务后应该进入等待. -
队列:
xQueueCreate
建立队列,xQueueReceive
接收队列.
根据FreeRTOS操作系统最全面使用指南 整理
似乎就主要3个功能: 任务调度/FIFO队列通讯/内存管理.
时间管理主要就是一个延迟函数vTaskDelay
下面看官网教程https://www.freertos.org/FreeRTOS-quick-start-guide.html的Developer Docs
部分. 描述Kernel部分.
编码标准
FreeRTOS用的是MISRA标准.
前缀
变量类型 | 前缀 |
---|---|
uint32_t | ul |
uint16_t | us |
uint8_t | uc |
有符号非标准int(如Type_t/TickType_t/size_t) | x |
无符号非标准int(如UbaseType_t) | ux |
enum类型 | e |
* 指针 | p |
函数类型 | 前缀 |
---|---|
文件内函数(类似private) | prv |
void空返回函数 | v |
其他返回值函数 | 参照变量前缀 |
宏也有前缀, 应含有或部分含有所在的文件名(小写).其他部分为大写, 如: configUSE_PREEMPTION
, 习惯了全大写的看起来怪怪的
移植层定义的适配类型
- TickType_t:如果configUSE_16_BIT_TICKS为非零(条件为真),TickType_t定义为无符号16位类型。如果configUSE_16_BIT_TICKS为零(条件为假),TickType_t定义为无符号32位类型。注:32位架构的微处理器应设置configUSE_16_BIT_TICKS为零。
- BaseType_t:定义为微处理器架构效率最高的数据类型。比如,在32位架构处理器上,BaseType_t应该定义为32位类型。在16位架构处理器上,BaseType_t应该定义为16位类型。如果BaseType_t定义为char,对于函数返回值一定要确保使用的是signed char,否则可能造成负数错误。
- UbaseType_t:这是一个无符号BaseType_t类型
其他风格
- 坚决不用
//
注释, 哈哈. 一行80列以内. - 4格缩进
#include
的顺序是标准库/自己的库/硬件特定的库#include
后面依次是#define
, 文件内函数声明(都加了static
), 文件内全局变量声明, 函数定义- 所有函数定义都有
/*-------*/
分行
任务task
FreeRTOS要做的跟Windows一样, 一个任务是一个窗口, 有自己的上下文和环境, 看上去就像独立运行的一样. 多个任务看起来就像是同时在运行, 实际上由任务调度去决定任务的切换, 每一时间只有一个任务占领CPU(当然Windows也是一样的)
任务状态
占领CPU的时候就是 running态, 暂时调度给其他任务就是ready态.
任务自己进入等待而把CPU主动让出来交Blocked态, 如调用了vBlockDelay()
或等待其他事件. 等到了就恢复ready,或者实在等不到(超时timeout了)也恢复ready
啥也不干的任务可以调用vTaskSuspend()
进入Suspended态, 跟Blocked区别就是不会超时. 只有调用xTaskResume()
才会回到ready态.
任务优先级
数字高的先运行. 0是最低数字. 当前运行的就是最高优先级的一个或多个ready任务. 多个任务同时的话就用时间片切换.
优先级原则
- Fixed priority固定优先级. 调度器不会更改优先级
- Preemptive抢占式. 最高优先级永远先运行. 如果一个任务优先级被中断由低调到最高, 它立刻把抢到CPU, 无论其他任务运行到哪儿了, 即便是一个时间片没有执行完.
- Round-robin同优先级循环切换
- Time sliced时间分片.一个tick中断一片时间.
要小心饿死低优先级任务
高优先级永远最先运行, 如果占着CPU不放, 低优先级永远不会运行. 所以要高风亮节, 等事的时候就把CPU让出来, 进入Blocked或者Suspended, 事儿来了再处理.
优先级其他
抢占式和时间分片这两个特性可以在FreeRTOSConfig.h
中关闭.
另外支持大小核(AMP)和同等多核(SMP)等CPU
任务实现
void vATaskFunction( void *pvParameters )
{
for( ;; )
{
-- Task application code here. --
}
/* Tasks must not attempt to return from their implementing
function or otherwise exit. In newer FreeRTOS port
attempting to do so will result in an configASSERT() being
called if it is defined. If it is necessary for a task to
exit then have the task call vTaskDelete( NULL ) to ensure
its exit is clean.
翻译: 任务别想着return, 新的port里这样做会导致configASSERT()
调用报错. 如果非得退出, 调用vTaskDelete( NULL ) */
vTaskDelete( NULL );
}
函数形态咯. for(;;)
的意思是任务一直执行, 直到vTaskDelete
, 相当于windows的×
. 任务里面是坚决不能有return
的, 否则就会报错.
一直执行的任务里面, 自己决定是不是要Blocked或者Suspended把CPU让出来. 所以这个for(;;)在裸机系统里面看起来像是个死锁, 实际上却是让任务保持运行的方式, 毕竟它不能return
任务函数的类型是TaskFunction_t
void vATaskFunction( void *pvParameters )
{
for( ;; )
{
/* Psudeo code showing a task waiting for an event
with a block time. If the event occurs, process it.
If the timeout expires before the event occurs, then
the system may be in an error state, so handle the
error. Here the pseudo code "WaitForEvent()" could
replaced with xQueueReceive(), ulTaskNotifyTake(),
xEventGroupWaitBits(), or any of the other FreeRTOS
communication and synchronisation primitives.
翻译: 下面伪码展示了任务等待事件的方式. 事件发生了就处理事件,
超时了就处理报错. 伪码WaitForEvent可以换成 xQueueReceive(),
ulTaskNotifyTake(), xEventGroupWaitBits()或其他通讯同步机制 */
if( WaitForEvent( EventObject, TimeOut ) == pdPASS )
{
-- Handle event here. --
}
else
{
-- Clear errors, or take actions here. --
}
}
/* As per the first code listing above. */
vTaskDelete( NULL );
}
任务使用 xTaskCreate()
xTaskCreateStatic()
创建, 使用vTaskDelete()
销毁.
Co-routines协程
超小RAM的MCU可以用协程, 32位的CPU一般就用不到了. 这说的是Arduino的那个8位机吗? 这儿也不介绍了, 一般这种情况估计都用裸机了.
任务/中断间通讯机制
Queues队列
- 队列是任务间以及中断和任务间的主要通讯方式. 采用的是先进先出FIFO, 跟排队一样, 先到先得.
- 发到队列的数据是复制的, 这和其他消息机制()类似, 也就是发出去以后, 不管对方收了没有, 自己这一份数据都可以改了, 类似于函数调用的实参传递.
- 中断使用队列和任务使用队列的API不一样, 方便识别.
- 读取空队列的任务将进入Blocking态, 直到收到消息. 发送消息给满队列的任务将进入Blocking态, 直到队列有空.
- 中断不允许调用以
fromISR
结尾的API函数. ISR意思是Interrupt Service Routine中断服务事务,就是中断处理函数.
Binary Semaphores二进制信号量
先说啥是信号量. 简单地说就是解决资源占用的方式, 跟十字路口的信号灯一样. 拿到绿灯的占用路口走, 给另一个方向的车辆红灯, 另一个方向的就等着(Blocking).有限的资源多个任务都想用, 就用信号量来解决. 也有点像停车场剩余车位数(计数信号量). 没车位的都在外面等着(Blocking).
这儿和这儿有中文讲解.
互斥和同步. 互斥就是一个球只有一个人拍, "排斥"了其他人拍球, 想玩的排队. 同步就是想做才得先小工洗菜,再大厨炒菜,再跑堂端菜, 都是菜, 但就是得按顺序处理, 没到操作步骤的也排着队等.这儿看到同步也是一种互斥, 除了等资源, 还得步骤到了.
我在处理腾讯连连的MQTT时可能就是一种同步机制, 需要按顺序处理ready->GOT IP->时间->MQTT.只不过我是用enum的不同状态表示的.
二进制信号量就是占用时加锁, 释放时解锁, 表明这两种状态的.
Task Notifications任务通知
有时候可以替代二进制信号量, 并且更轻量一些.
- 二进制信号量同时用于同步和互斥机制.
- 二进制信号量和mutexes互斥量很相似, 其差别只在于互斥有优先级继承, 而二进制信号量没有. 因此互斥量更适合互斥场景, 而二进制信号量更适合任务间(包括中断)同步场景.
- 二进制信号量相当于只有1位的队列Queue.
- 中断处理后才能执行的任务可以等待这个信号量, 而中断给出这个信号量,函数是
xSemaphoreGiveFromISR()
- 任务的优先级可以保证获取信号量以后的处理顺序(同步机制?).
Counting Semaphores计数信号量
同样
Task Notifications任务通知
有时候做轻量级替代.
我在MQTT处理对pub消息的回应时用了类似的计数机制. 为防止在连续发出的第二条pub指令之后, 实际执行pub之前, 收到了上一条pub指令的success消息, 将pub指令取消导致第二条pub没有执行(同时还要兼顾失败重发机制), 我在pub发出指令后增加计数/在success时减少计数, 只有计数为0时才完全停止pub.
- 相当于长度大于1的队列
- 可用于事件的计数. 计数值就是未处理事件的值
- 可用于资源管理. 跟停车场一个道理, 计数值表示空余车位的数量. 车走了加1车进来减1, 为0就等着.
Mutexes互斥量
用于对有限资源的访问.
互斥量借用完要自己还回去(只有1个且不会被消耗), 而二进制信号量用完不用还(持续被生产出来并且被消耗掉).
低优先级的任务持有互斥量时, 高优先级的再去获取, 会把低优先级的任务的优先级暂时提高到高优先级.
适合任务间确认谁访问资源, 不适合中断使用. 因为中断是不可能去等待任务给他返还可用性的.
Recursive Mutexes递归互斥量
可以被一个任务连续借用多次, 但也要被还回去这么多次, 其他任务才能用.
Task Notifications任务通知
算是新特性, 从v8.2.0开始引入, 在v10.4.0中增加了单任务多通知功能
- 每个任务有1个或1组(v10.4以后)通知. 任务A可以发直达信息(direct, 一个32位的值)到任务B的通知里, 发到以后任务B的通知会变成Pending.
- 任务B可以等待通知的Pending时Blocking.
- 任务A发了direct后还能更新direct的值. 可以不管任务B读了没有, 也可以等待任务B读完再更新, 可以改1位或多位, 也可以递增
- 任务B调用
xTaskNotifyWait()/xTaskNotifyWaitIndexed()
读了值以后, 通知状态就会变成Not Pending. - 任务B也可以直接调用
xTaskNotifyStateClear()/xTaskNotifyStateClearIndexed()
清除通知的Pending态 - 即便有v10.4以后有一组通知, 任务B同时也只能等一个通知的Pending, 而不能同时等多个通知的Pending
- 通知比之前那些方式要快45%, 并且使用RAM更少, 只是使用的时候有些限制: 1. 只有1个接受者; 2. 发送者一次发不完的时候不能block;
- 任务A使用
xTaskNotifyIndexed()
xTaskNotifyGiveIndexed()
发通知. 通知进入Pending态, 直到任务B调用xTaskNotifyWaitIndexed()
ulTaskNotifyTakeIndexed()
. 函数名里面的Indexed
可以去掉, 就对应的是Index==0
的通知(感觉是为了保持版本的兼容性) - 通知可部分替代二进制信号量/计数信号量/事件组/邮箱机制. 官方文档对此有专门章节做示例
Stream & Message Buffers流和消息缓冲区
10.0.0以后才有的特性
TBD. 这么新的特性估计暂时用不到, 晚些在研究.
Software Timers软时钟
- 是可选功能, 需要自行开启
- 分一次性时钟和自重启时钟
- 在时钟未溢出前还可以Reset时钟以重新计时
Event Bits (or flags) and Event Groups 事件位(标志)和事件组
- 事件位指示事件是否发生, 一组事件位就是事件组
- 可以在事件位变成1以前等待
静态和动态内存分配
- 很多前面说的对象都可以动态或者静态分配内存. 动态分配就是在运行时再分配, 静态分配就是在link的时候就分配好,还能指定分配的地址.
heap堆管理
最新版本有5种堆管理方式.
这部分感觉暂时也用不上, 看原文吧.
栈溢出检查
新建项目
直到vTaskStartScheduler()
调用后, 程序才表现得像个RTOS的样子.
调试
百度搜索了资料, 似乎只能通过打印信息调试, 搜索"FreeRTOS 断点调试"找不到结果.
Bing搜索"FreeRTOS breakpoint debug"还是能找到几条使用Keil设置断点调试的方式, 主要是说要在task里面设置断点, 并且不能用步入模式调试.
我设想一种调试方式, 即只针对task进行调试, 把task当做一个系统. 把FreeRTOS的API函数和硬件函数全部Stub来调试逻辑. 剩下的硬件逻辑和OS相关逻辑通过真机printf调试. 当然单元测试应该是可以用的.
在8266上开发腾讯连连,集成C SDK
似乎要先看这个FreeRTOS+lwIP 平台接入指引, 里面会以8266举例. 然后再看C SDK使用参考
另外还有个C SDK 移植接入指引不知道有用否.
SDK 说明及下载似乎包括了所有有关SDK的下载.
腾讯连连乐鑫Qcloud-8266-AT固件使用指南
本来用流程图做, 但发现流程图这个形式很难做, 箭头重复的太多: 【腾讯文档】腾讯连连ESP8266-QCloud-AT固件使用指南 ,大概原因是腾讯文档只能用直角箭头.
改用思维导图做.【腾讯文档】腾讯连连ESP8266-QCloud-AT固件使用指南
乐鑫产品型号对比
芯片
主要系列是按CPU划分的.
其中ESP32和S2/S3/C3已经是不同的产品了, 这样命名挺讨厌, 自己既是大类的名称又是小类的名称...
芯片系列
系列 | CPU |
---|---|
8266/8285 | Xtensa L106 |
ESP32 | Xtensa LX6 单核或双核 |
ESP32-S2 | Xtensa LX7单核 |
ESP32-S3 | Xtensa LX7双核 |
ESP32-C3 | RISC-V |
芯片后缀
后缀 | 意义 |
---|---|
FHn | n MB的flash |
Rn | n MB的PSRAM |
FNn | n MB的Flash |