micropython 移植k210_重构+优化 micropython 下 k210 (esp8285) 的 AT network 通信过程(附代码,顺便讲讲状态机实现)。...

本文介绍了在MicroPython环境下,针对K210的AT网络通信过程进行重构和优化,特别是针对二进制文件传输时可能出现的错误进行了处理。通过使用状态机(FSM)来管理数据接收,确保了在高速传输下的稳定性。文章详细阐述了状态机的设计思路,包括IDLE、IPD、DATA和EXIT四种状态,并提供了关键代码示例。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

回头再解释,先 mark 2020年4月28日。

2020年5月1日 今日有空,稍微更新一下,主要在这里备份一下相关的 code 和详细的问题分析和具体解释。

继着上次的 https://www.cnblogs.com/juwan/p/12389600.html 的内容,上次只定位了问题和简单解决了问题,调整了 Python 的 Code ,现在主要是旧 Code 不稳定,下载的文件容易出问题,容易因为二进制文件产生错误。

那么如何解决呢?

我回顾一下我的这 4 天的开发历程,第一天,基于旧接收框架重新整理 Code ,第二天准备到位开始测试,测试崩盘没有任何改造,镇定思痛,第三天重新上状态机架构,第四天大量测试,缝缝补补后符合预期执行,趋于完美。

先上逻辑分析仪捕获数据观察数据流,注意我压缩了很多写的过程,只交待重点,有理有据的代码让人信服,如果有漏洞也是细节的马虎,思路对了大体是不会有太大问题的。

在 HTTP 下载文件的时候,可以看到它接收数据是以 \r\n+IPD 开头的数据,如 '+IPD,3,1452:' or '+IPD,1452:' 两类,经过测试,确实是符合预期的返回指定长度的数据,但会在最后的结束的时候附带 CLOSED\r\n 数据表示传输结束。

这里我们注意到两个问题,以及旧代码为什么不能抵抗高速传输的异常数据所产生的错误,这在上次的分析就已经提到这个数据流的问题,我们先理清问题。

我们知道了传输的规则和传输的形式,例如 AT 指令 和 传输的数据 是混在同一条通道中的,这意味着二进制数据中一旦出现类似 AT 指令的数据必然导致后续的数据错乱,又或者是传输的缓冲区异常导致程序跑飞等等。

这些错误都在旧代码中体现了,这里不再细讲,直接开始编写新代码适用新的接口。

我们知道这个函数是可重入的,所以一开始在剥离基础框架的时候就要最大化的保留重入的接口,例如,接收数据完成后,将数据保留到一个缓冲区中,在下一次的重入的时候,取走足够数量的数据,如果不够,则继续接收数据,如下代码的设计。

// required data already in buf, just return data

size = Buffer_Size(&nic->buffer);

if (size >= out_buff_len) { // 请求的代码长度小于缓冲区的长度,直接返回数据

Buffer_Gets(&nic->buffer, (uint8_t *)out_buff, out_buff_len);

if (data_len) {

*data_len = out_buff_len;

}

size = size > out_buff_len ? out_buff_len : size;

frame_sum -= size; // 总体的缓冲区数据要对应减少,同时判断是否没有更多的数据了,如果为负数则报错。

// printk("Buffer_Size frame_sum %d size %d *data_len %d\n", frame_sum, size, *data_len);

if (frame_sum <= 0) {

Buffer_Clear(&nic->buffer);

if (*peer_closed) { //buffer empty, return EOF

return -2;

}

}

return out_buff_len;

}

围绕这个思路去,这个函数就形成一种非常适合 FSM 状态机的接口,同时必须要有 buffer 去保留上一次的状态与数据。

为此可以将其构建一种基础状态机模型,二话不说先上架构,保持 IDLE, IPD, DATA, EXIT 四种状态,本来有 CLOSED 状态的,后来想了一下还不到时候就移除了。

enum fsm_state { IDLE, IPD, DATA, EXIT } State = IDLE;

static uint8_t tmp_buf[1560 * 2] = {0}; // tmp_buf as queue 1560

uint32_t tmp_bak = 0, tmp_state = 0, tmp_len = 0, tmp_pos = 0;

mp_uint_t interrupt = 0, uart_recv_len = 0, res = 0;

while (State != EXIT) {

// wait any uart data

uart_recv_len = uart_rx_any(nic->uart_obj);

if (uart_recv_len > 0) {

if (tmp_len >= sizeof(tmp_buf)) { // lock across

// printk("lock across frame_len %d tmp_len %d\n", frame_len, tmp_len);

tmp_len = 0;

}

res = uart_stream->read(nic->uart_obj, tmp_buf + tmp_len, 1, &err);

// printk("%s | tmp_buf %s res %d err %d\n", __func__, tmp_buf, res, err);

if (res == 1 && err == 0) {

interrupt = mp_hal_ticks_ms();

// backup tmp_len to tmp_pos (tmp_pos - 1)

tmp_pos = tmp_len, tmp_len += 1; // buffer push

// printk("[%02X]", tmp_buf[tmp_pos]);

if (State == IDLE) {

continue;

}

if (State == IPD) {

continue;

}

if (State == DATA) {

// printk("%s | frame_len %d tmp_len %d tmp_buf[tmp_pos] %02X\n", __func__, frame_len, tmp_len, tmp_buf[tmp_pos]);

continue;

}

} else {

State = EXIT;

}

}

if (mp_hal_ticks_ms() - interrupt > timeout) {

break; // uart no return data to timeout break

}

if (*peer_closed) {

break; // disconnection

}

}

我们看到这个 FSM 的基础保证是,确保访问边界被控制住,防止程序指针异常访问导致跑飞(在我前一篇有所提及)。

if (tmp_len >= sizeof(tmp_buf)) { // lock across

// printk("lock across frame_len %d tmp_len %d\n", frame_len, tmp_len);

tmp_len = 0;

}

其次是超时机制,确保任何情况下都可以离开状态机。

if (mp_hal_ticks_ms() - interrupt > timeout) {

break; // uart no return data to timeout break

}

这样可以保障程序在任何场合下都不会陷入底层无法离开。

接下来就是当缓冲区有数据、数据接收正常、以及每次读取一字节成功的基础上,进行字符串的匹配和状态转移的分类处理。

// wait any uart data

uart_recv_len = uart_rx_any(nic->uart_obj);

if (uart_recv_len > 0) {

if (tmp_len >= sizeof(tmp_buf)) { // lock across

// printk("lock across frame_len %d tmp_len %d\n", frame_len, tmp_len);

tmp_len = 0;

}

res = uart_stream->read(nic->uart_obj, tmp_buf + tmp_len, 1, &err);

// printk("%s | tmp_buf %s res %d err %d\n", __func__, tmp_buf, res, err);

if (res == 1 && err == 0) {

interrupt = mp_hal_ticks_ms();

// backup tmp_len to tmp_pos (tmp_pos - 1)

tmp_pos = tmp_len, tmp_len += 1; // buffer push

// printk("[%02X]", tmp_buf[tmp_pos]);

if (State == IDLE) {

continue;

}

if (State == IPD) {

continue;

}

if (State == DATA) {

// printk("%s | frame_len %d tmp_len %d tmp_buf[tmp_pos] %02X\n", __func__, frame_len, tmp_len, tmp_buf[tmp_pos]);

continue;

}

} else {

State = EXIT;

}

}

这时候最后一个状态是用来离开的标记。

我们开始脑里模拟执行过程和假设,第一种状态就是空转(IDLE),基本上每个状态机都是从这里开始的。

在 IDLE 期间要做的事情就是清理无关的数据,如下代码,直到符合预期的怀疑对象出现为止。

if (State == IDLE) {

if (tmp_buf[tmp_pos] == '+') {

tmp_state = 1, tmp_bak = tmp_pos, State = IPD;

continue;

} else {

// printk("(%02X)", tmp_buf[tmp_pos]);

tmp_len -= 1; // clear don't need data, such as (0D)(0A)

continue;

}

}

如上代码,在空转状态下,遇到 '+' 出现后,会进行假设性的状态转移,如果不符合预期的数据会跌回 ILDE 状态,然后我们看看 IPD 状态的实现。

if (tmp_pos - tmp_bak > 12) { // Over the length of the '+IPD,3,1452:' or '+IPD,1452:'

tmp_state = 0, State = IDLE;

continue;

}

if (0 < tmp_state && tmp_state < 5) {

// printk("(%d, %02X) [%d, %02X]\n", tmp_pos, tmp_buf[tmp_pos], tmp_pos - tmp_bak, ("+IPD,")[tmp_pos - tmp_bak]);

if (tmp_buf[tmp_pos] == ("+IPD,")[tmp_pos - tmp_bak]) {

tmp_state += 1; // tmp_state 1 + "IPD," to tmp_state 5

} else {

tmp_state = 0, State = IDLE;

}

continue;

}

if (tmp_state == 5 && tmp_buf[tmp_pos] == ':')

{

tmp_state = 6, State = IDLE;

tmp_buf[tmp_pos + 1] = '\0'; // lock tmp_buf

// printk("%s | is `IPD` tmp_bak %d tmp_len %d command %s\n", __func__, tmp_bak, tmp_len, tmp_buf + tmp_bak);

char *index = strstr((char *)tmp_buf + tmp_bak + 5 /* 5 > '+IPD,' and `+IPD,325:` in tmp_buf */, ",");

int ret = 0, len = 0;

if (index) { // '+IPD,3,1452:'

ret = sscanf((char *)tmp_buf + tmp_bak, "+IPD,%hhd,%d:", &mux_id, &len);

if (ret != 2 || mux_id < 0 || mux_id > 4 || len <= 0) {

; // Misjudge or fail, return, or clean up later

} else {

tmp_len = tmp_bak, tmp_bak = 0; // Clean up the commands in the buffer and roll back the data

frame_len = len, State = DATA;

}

} else { // '+IPD,1452:'

ret = sscanf((char *)tmp_buf + tmp_bak, "+IPD,%d:", &len);

if (ret != 1 || len <= 0) {

; // Misjudge or fail, return, or clean up later

} else {

tmp_len = tmp_bak, tmp_bak = 0; // Clean up the commands in the buffer and roll back the data

frame_len = len, State = DATA;

}

}

continue;

}

由于这是第三次重构的代码,所以里面加了很多保护措施,但实际上这些措施不一定会遇到,它会随着各种高频传输调试中触发,代码要能够抵抗这些异常数据。

我们看到 if (tmp_buf[tmp_pos] == ("+IPD,")[tmp_pos - tmp_bak]) { 的循环表示的就是 strstr 的匹配,但这里的匹配我稍微偷懒了一下简写了代码。

if (0 < tmp_state && tmp_state < 5) {

// printk("(%d, %02X) [%d, %02X]\n", tmp_pos, tmp_buf[tmp_pos], tmp_pos - tmp_bak, ("+IPD,")[tmp_pos - tmp_bak]);

if (tmp_buf[tmp_pos] == ("+IPD,")[tmp_pos - tmp_bak]) {

tmp_state += 1; // tmp_state 1 + "IPD," to tmp_state 5

} else {

tmp_state = 0, State = IDLE;

}

continue;

}

因为整个环境没有基础容器的支持,例如队列,所以我只能通过代码中的索引去模拟队列执行,保障整个执行的有序状态,当匹配完成后,会从 IPD 进入 DATA 态,此时意味着之后的数据都被视为数据,长度以 IPD 指示的为准。

tmp_len = tmp_bak, tmp_bak = 0; // Clean up the commands in the buffer and roll back the data

frame_len = len, State = DATA;

进入 DATA 态的时候就要思考终止条件的状态了,有两种可能,一种是标识的长度数据走完了,这表示应该要转移状态到 ILDE 了,但期望从这个状态中发现终止条件。

if (frame_len < 0) {

if (frame_len == -1 && tmp_buf[tmp_pos] == 'C') { // wait "CLOSED\r\n"

frame_bak = frame_len;

continue;

}

if (tmp_state == 6 && tmp_buf[tmp_pos] == '\r') {

tmp_state = 7;

continue;

}

if (tmp_state == 7 && tmp_buf[tmp_pos] == '\n') {

if (frame_len == -2) { // match +IPD EOF (\r\n)

tmp_state = 0, State = IDLE;

// After receive complete, confirm the data is enough

size = Buffer_Size(&nic->buffer);

// printk("%s | size %d out_buff_len %d\n", __func__, size, out_buff_len);

if (size >= out_buff_len) { // data enough

// printk("%s | recv out_buff_len overflow\n", __func__);

State = EXIT;

}

} else if (frame_len == -8 && frame_len == -1) {

// Get "CLOSED\r\n"

peer_just_closed = *peer_closed = true;

frame_bak = 0, tmp_state = 0, State = EXIT;

} else {

tmp_state = 6;

}

continue;

}

// 存在异常,没有得到 \r\n 的匹配,并排除 CLOSED\r\n 的指令触发的可能性,意味着传输可能越界出错了 \r\n ,则立即回到空闲状态。

if (frame_len <= -1 && frame_bak != -1) {

// printk("%s | tmp_state %d frame_len %d tmp %02X\n", __func__, tmp_state, frame_len, tmp_buf[tmp_pos]);

State = IDLE;

continue;

}

}

如我所假设的两种状态 \r\n 和 CLOSED\r\n 这两类,但还有最终保障措施,假设无法符合我的预期的结尾,要尽快回归 ILDE 重新进入匹配,否则数据将一片混乱,这个只能保障 1 、 2 字节的误差,对于缓冲区溢出复写的情况无法抵抗。

到这里我们还要注意到很多地方可能会出错和溢出,比如有如下考虑:

上层 Python 接收数据的变量溢出,无法继续 read 更多,积压数据未处理。

原始串口缓冲区溢出后,循环缓冲复写。

内部状态机解析的数据缓冲长度(已经被保护了,但出错了会导致其中的一帧丢失)

状态机解析后的数据缓冲变量写入可能会失败,夹在 C 与 Python 之间交互的一层缓冲区,主要用来供应 socket 层的数据。

因此这里面的任何一个环境出错,都可能导致 HTTP 的原始下载数据出错,但所辛都可以完整下好不会出现指令与二进制数据混淆的情况了。

基于这个接收框架,还可以进一步的拓展出 多 socket 的通信 和 其他 AT 指令的通信接口,匹配状态和解析后分类到各自对应的容器即可。

旧接口函数抵抗异常的设计不够多,所以之后测试完整了后,可以基于新的接口继续发展新功能。

我的测试结果如下:

在串口波特率 921600(1M)约 92 kb/s 的下载速度下载 384kb kmodel 模型文件的过程。

若是不写入 SD 卡的基础上,完美接收完毕,耗时 8s 。

若是写入我的 256M 的路边野卡,完整下载时 10s,无任何损坏,但受到网络波动则会出现下载的文件中会存在失败的帧,下载过程的好坏参半。

在串口波特率 576000(5M)约 57 kb/s 的下载速度下载 5M kmodel 模型文件的过程。

写入SD卡的过程,下载 5M 文件无错且完整,较为稳定,少有失败的情况。

从这个测试的结果来看,将 SD 卡写入数据是一个影响很大的变数,要么将其读写分离加缓冲在其中,减少 SD 卡带来的负面影响,优先把 HTTP 的数据带回内存之中。

这和串口缓冲区有关,如现在假定的 10k 字节,而 921600 (92kb/s),意味着 read 数据要在 10k/x:100kb/s, x = 0.1s 内取走全部数据,否则 IO 不匹配,缓冲区会溢出,之后的数据都会被放弃从而形成接收数据的断层。

python 的 code 执行模型如下

while True:

tmp = wifi.read()

file.write(tmp)

所以再下一次的 wifi.read() 因符合下述读写分离模型,这只是伪代码。不用太计较。

# thread 0

while True:

tmp = wifi.read()

sleep(0.1)

# thread 1

while True:

file.write(tmp)

sleep(x)

这次我们必须在硬件的内存和 SD 卡的写入数据中做一个选择,至少保证 SD 写入速度应大于 100kb/s 从而保持 IO 的速率基本一致,减少下载文件的异常。

其他可能存在的问题,就比如有时候发送数据会请求失败,还不知道是哪个死角没考虑到,也许之后就会有人发现了吧

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值