分类 未分类 下的文章

原文https://pyserial.readthedocs.io/en/latest/tools.html
安装pyserial

python -m pip install pyserial

使用Miniterm工具

python -m serial.tools.miniterm COM5 115200

cmd命令行窗口支持ESC颜色

python -m serial.tools.miniterm COM5 115200 -f direct

远程不能直接使用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可以挂载远程硬盘. 参考文章

项目地址: 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项目

复制

  1. 要将qcloud项目components\qcloud_iot\qcloud_iot_c_sdk目录复制到template项目的components\qcloud_iot_c_sdk,去掉一级目录qcloud_iot.
  2. components\component.mk复制过来
  3. 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

参考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连接的开发服务器必须同时开着sshdfrpc, 但是如果vscode已经连上了, sshd就可以关掉了, 只开着frpc就可以.这时候ssh命令行已经无法连接上了. 似乎vscode借用了22端口,但连接上以后没有走sshd协议

参考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

  1. 打开"设置->应用->可选功能->添加功能", 搜索SSH, 将SSH服务器SSH客户端都安装.
  2. 文章要求装完重启,但好像不重启也可以. 在C:\windows\system32\OpenSSH目录下就会安装ssh的相关文件, 包括sshd.exe, 可以直接运行sshd就开启服务,也可以按文章的介绍在服务中开启.
  3. 运行菜单services.msc, 查看OpenSSH SSH server项, 双击, 选择登录页面,查看或修改可以登录的用户. 设置服务为自动并开启运行.
  4. 打开一个cmd,运行ssh-keygen -A生成服务端key, 默认生成的位置在C:\ProgramData\ssh. 这儿还存放sshd_config文件, 如果没有就创建一个.
  5. 运行ssh-keygen ,生成本地ssh keyid_rsaid_rsa.pub,位置在C:\Users\username/.ssh/,这儿还存放authorized_keys. authorized_keys就是其他客户端的pub key的集合
  6. 在客户端运行ssh-keygen ,生成本地ssh keyid_rsaid_rsa.pub, 将id_rsa.pub复制一份改名为authorized_keys到服务端的C:\Users\username/.ssh/目录
  7. 设置服务端开启哪个命令行, 可以在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

官网示例是针对端口全开的公网ip, 而阿里云是有安全策略, 需要开放端口.

frp用起来算是及其简单惬意了.

  1. 阿里云服务器和内网SSH服务器分别下载frp release, 目前最新版本0.38.0
  2. 我用的阿里云服务器是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
  3. 登录阿里云服务器后台aliyun.com, 在ECS服务器->实例->安全组中的入方向添加一条TCP记录, 端口号写一个区间:7000/7100.后面frp配置的所有端口号都要在这个范围内才能访问. 授权对象为0.0.0.0/0
  4. 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

行不通的方法

  1. 端口映射. 现在家庭和公司PPPoE获取的地址都已经是运营商的内部NAT地址了, 以100开头, 所以路由器端口映射行不通.

注意: 固件编译只能通过 docker进行.

原因是:

  1. 乐鑫的最新8266 RTOS SDK v3.4腾讯连连的components不支持;
  2. 腾讯连连只支持8266 RTOS SDK v3.1, 而8266 RTOS SDK v3.1的文档中竟然没有提编译工具链, 而v3.4文档中的编译工具链gcc版本v8.4.0也不支持v3.1
  3. 腾讯开发人员用的编译工具链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

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 配置层

文档

amazon官方文档, 中文好评!https://docs.amazonaws.cn/freertos/index.html
官网: https://freertos.org/
官方文档,英文的
AWS上的文档,也是英文的

FreeRTOS与RT-Thread对比: 后者自带很多第三方协议, 如i2c/mqtt等等, 而FreeRTOS似乎要自己找自己组装进去.

根据其他乱七八糟文档的整理

根据深入浅出,FreeRTOS新手+入门学习笔记整理.

  • 任务:
    xTaskCreate建立多个任务, 然后使用vTaskStartScheduler调度任务函数. 任务函数执行完成任务后应该进入等待.

  • 队列:
    xQueueCreate建立队列, xQueueReceive接收队列.

根据FreeRTOS操作系统最全面使用指南 整理
似乎就主要3个功能: 任务调度/FIFO队列通讯/内存管理.
时间管理主要就是一个延迟函数vTaskDelay

下面看官网教程https://www.freertos.org/FreeRTOS-quick-start-guide.htmlDeveloper 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调试. 当然单元测试应该是可以用的.

芯片

主要系列是按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