【OTA专题】13 APP工程移植Ymodem协议接收解包固件
本文详细介绍了Ymodem协议在串口DMA环境下的移植实现。主要内容包括:1)移植流程解析,重点说明在Boot和App模式下DMA数据搬运策略的差异;2)协议处理核心代码详解,包括数据包接收、解包和校验机制;3)关键实现技术,如空闲中断回调函数处理、DMA接收优化等。特别强调了通过队列机制实现异步数据接收,确保在文件传输过程中不影响其他应用任务的执行。该方案解决了传统串口中断方式对系统实时性的影响
目录
1 移植Ymodem协议流程(串口DMA)
Boot:
1.循环等待接收
2.不需要执行其他任务的
App:
1.除了下载任务本身之外,还有更多的应用层任务
2.使用DMA去搬运数据
3.数据传输的速度要尽可能的快
4.Ymodem协议数据包长度根据首字节确认
DMA搬运:
空闲中断
半满中断
全满中断
数据包的数据长度发帧前不能确定
需要通过第一个byte来确认数据长度
Ymodem协议的数据包一整包数据是一次发完的
局限性:
1.不知道第一次应该设置多少个DMA搬运的长度
133 或者 1029个byte
2.半满中断和全满中断不适合在这里使用
?: 如果第一次设置一个byte的DMA搬运,然后立即解析数据长度,再设置数据长度的DMA搬运。
!: 实际上,在解析第一个byte的时候,第二个byte就已经发送过来了,甚至第三个都发了,这样的话就
会导致我们的DMA搬运失败,一直缺少一个或两个数据。
3.使用空闲中断来达成目的
为什么要使用DMA?
1.我们通信的过程中,会有其他的App任务/线程在响应,这样的话DMA对应用任务的干扰就比较小,不可以使用死等的方式去接收数据
为什么不使用串口中断?
1.不停的在串口完成中断中去执行搬运数据到Ram的指令,这样的话就会直接干扰的App应用层的任务

数据处理流程图:

接收协议的途中,需要保持App的正常任务的执行效果
移植Ymodem协议

2 Ymodem接收解包代码详解
ymodem.h
/* Define to prevent recursive inclusion -------------------------------------*/
#ifndef _YMODEM_H_
#define _YMODEM_H_
/* Includes ------------------------------------------------------------------*/
#include "stdint.h"
/* Exported types ------------------------------------------------------------*/
/* Exported constants --------------------------------------------------------*/
#define PACKET_SEQNO_INDEX (1)
#define PACKET_SEQNO_COMP_INDEX (2)
#define PACKET_HEADER (3)
#define PACKET_TRAILER (2)
#define PACKET_OVERHEAD (PACKET_HEADER + PACKET_TRAILER)
#define PACKET_SIZE (128)
#define PACKET_1K_SIZE (1024)
#define FILE_NAME_LENGTH (256)
#define FILE_SIZE_LENGTH (16)
#define SOH (0x01) /* start of 128-byte data packet */
#define STX (0x02) /* start of 1024-byte data packet */
#define EOT (0x04) /* end of transmission 结束传输 */
#define ACK (0x06) /* acknowledge 确认*/
#define NAK (0x15) /* negative acknowledge 无确认 */
#define CA (0x18) /* two of these in succession aborts transfer 其中两个相继中止传输 */
#define CRC16 (0x43) /* 'C' == 0x43, request 16-bit CRC 请求CRC */
#define ABORT1 (0x41) /* 'A' == 0x41, abort by user */
#define ABORT2 (0x61) /* 'a' == 0x61, abort by user */
#define NAK_TIMEOUT (0x100000)//无确认超时时间
#define MAX_ERRORS (3)//最大错误数量
Ymodem_Receive
#define YMODEM_UART &huart1
/**
* @brief Receive a file using the ymodem protocol
* @param buf: Address of the first byte
* @retval The size of the file
*/
uint8_t packet_data[PACKET_1K_SIZE + PACKET_OVERHEAD], file_size[FILE_SIZE_LENGTH], * file_ptr, * buf_ptr;
int32_t i, j, packet_length, session_done, file_done, packets_received, errors, session_begin, size = 0;
int32_t Ymodem_Receive(uint8_t* buf)
{
for (session_done = 0, errors = 0, session_begin = 0; ;)//初始化变量,进入循环
{
for (packets_received = 0, file_done = 0, buf_ptr = buf; ;)
{
// 接收一整包数据到数据缓冲区
switch (Receive_Packet(packet_data, &packet_length, 2000))
{
// 通过Receive_Packet的返回值判断包的状态
case 0:
errors = 0;
switch (packet_length) // 通过Receive_Packet()返回的包长来判断包的状态
{
/* Abort by sender */
case - 1:
Send_Byte(ACK);
return 0;
/* End of transmission */
case 0:
Send_Byte(ACK);
file_done = 1;
break;
/* Normal packet */
default:
if ((packet_data[PACKET_SEQNO_INDEX] & 0xff) != (packets_received & 0xff))
{
Send_Byte(NAK);
}
else
{
if (packets_received == 0) // 文件的第一包数据(文件的信息)
{
/* Filename packet */
if (packet_data[PACKET_HEADER] != 0)
{
/* Filename packet has valid data */
for (i = 0, file_ptr = packet_data + PACKET_HEADER; (*file_ptr != 0) && (i < FILE_NAME_LENGTH);)
{
file_name[i++] = *file_ptr++;
}
file_name[i++] = '\0';
for (i = 0, file_ptr ++; (*file_ptr != ' ') && (i < FILE_SIZE_LENGTH);)
{
file_size[i++] = *file_ptr++;
}
file_size[i++] = '\0';
Str2Int(file_size, &size);
Send_Byte(ACK);
Send_Byte(CRC16);
}
/* Filename packet is empty, end session */
else
{
Send_Byte(ACK);
file_done = 1;
session_done = 1;
break;
}
}
/* Data packet */
else // 文件的后续数据包(真正的数据包)
{
// 将数据搬运至外部buffer进行其他处理(比如写入外部flash)
memcpy(buf_ptr, packet_data + PACKET_HEADER, packet_length);
/* write data into external flash (W25Q64) */
// W25Q64_WriteData(buf_ptr, packet_length); // TODO
#if 0 /* write data into internal flash */
RamSource = (uint32_t)buf;
for (j = 0;(j < packet_length) && (FlashDestination < BACKUP_APP_FLASH_ADDR + size);j += 4)
{
/* Program the data received into STM32F10x Flash */
// FLASH_ProgramWord(FlashDestination, *(uint32_t*)RamSource);
Flash_Write(FlashDestination, *(uint32_t*)RamSource);
if (*(uint32_t*)FlashDestination != *(uint32_t*)RamSource)
{
/* End session */
Send_Byte(CA);
Send_Byte(CA);
return -2;
}
FlashDestination += 4;
RamSource += 4;
}
#endif /* write data into internal flash */
Send_Byte(ACK);
}
packets_received ++;
session_begin = 1;
}
}
break;
case 1:
Send_Byte(CA);
Send_Byte(CA);
return -3;
default:
if (session_begin > 0)
{
errors++;
// log_e("errors++");
}
if (errors > MAX_ERRORS)
{
Send_Byte(CA);
Send_Byte(CA);
log_e("errors > MAX_ERRORS");
return 0;
}
// 如果文件还没开始接收,每次超时,会发送'C'
Send_Byte(CRC16);
break;
}
if (file_done != 0)
{
break;
}
}
if (session_done != 0)
{
break;
}
}
return (int32_t)size; // 返回文件的长度
}
Receive_Packet
static uint16_t sg_u16_uart_rec_len = 0;
/**
* @brief Receive a packet from sender
* @param data
* @param length 0: end of transmission
* -1: abort by sender
* >0: packet length
* @param timeout
* @retval 0: normally return
* -1: timeout or packet error
* 1: abort by user
*/
static int32_t Receive_Packet (uint8_t *data, int32_t *length, uint32_t timeout)
{
uint16_t i, packet_size;
*length = 0;
// 接收一整包数据
if (Receive_Bytes(data, REC_MAX_NUM, timeout) != 0)
{
return -1;
}
switch (*data) // 检测第一个字节
{
case SOH:
packet_size = PACKET_SIZE;
break;
case STX:
packet_size = PACKET_1K_SIZE;
break;
case EOT:
return 0;
case CA: // 如果第一个字节是CA,则检测下一个字节,如果也是CA则终止传输
// if ((Receive_Byte(&c, timeout) == 0) && (c == CA))
if (*(data + 1) == CA)
{
*length = -1;
return 0;
}
else
{
log_e("Receive_Packet error");
return -1;
}
case ABORT1:
case ABORT2:
return 1;
default:
log_e("Receive_Packet error");
return -1;
}
if (data[PACKET_SEQNO_INDEX] != ((data[PACKET_SEQNO_COMP_INDEX] ^ 0xff) & 0xff))
{
return -1;
}
// 校验搬运长度和接收长度是否一致
if (sg_u16_uart_rec_len != (packet_size + PACKET_OVERHEAD))
{
return -1;
}
*length = packet_size; // 通过参数返回包长(128 or 1024)
return 0;
}
Receive_Bytes 与 Send_Byte
extern QueueHandle_t Q_Ymodem_Rec_Len; // Queue for Ymodem receive length
static uint16_t sg_u16_uart_rec_len = 0;
/**
* @brief Receive bytes from sender
* @param data_buffer: Character
* @param length: length of receive bytes
* @param timeout: Timeout
* @retval 0: Byte received
* -1: Timeout
*/
static int32_t Receive_Bytes(uint8_t *data_buffer, uint16_t length, uint32_t timeout)
{
// 接收指定长度的数据到缓冲区
HAL_UARTEx_ReceiveToIdle_DMA(YMODEM_UART, data_buffer, length);
// check the queue is valid
if (Q_Ymodem_Rec_Len == NULL) {
log_e("error: Queue Q_Ymodem_Rec_Len is NULL!");
return -1;
}
// 等待空闲中断发送消息队列
if (pdTRUE == xQueueReceive(Q_Ymodem_Rec_Len, &sg_u16_uart_rec_len, timeout))
{
return 0;
}
return -1;
}
/**
* @brief Send a byte
* @param c: Character
* @retval 0: Byte sent
*/
static uint32_t Send_Byte (uint8_t c)
{
HAL_UART_Transmit(YMODEM_UART, &c, 1, 10);
return 0;
}
空闲中断回调函数
/**
* @brief Reception Event Callback (Rx event notification called after use of advanced reception service).
* @param huart UART handle
* @param Size Number of data available in application reception buffer (indicates a position in
* reception buffer until which, data are available)
* @retval None
*/
void HAL_UARTEx_RxEventCallback(UART_HandleTypeDef* huart, uint16_t Size)
{
BaseType_t xHigherPriorityTaskWoken = pdFALSE;
// 判断是串口1
if (huart->Instance == USART1)
{
// 确定是串口空闲中断触发的回调函数,获取已经传输值
// 为了在空闲中断中能够获取空闲标志,需要在串口中断服务函数中注释掉“清除空闲标志”的部分
if (1 == __HAL_UART_GET_FLAG(huart, UART_FLAG_IDLE))
{
// 通过队列将接收长度发送到 Q_Ymodem_Rec_Len 队列
if (pdTRUE == xQueueSendFromISR(Q_Ymodem_Rec_Len, &Size, 0))
{
xHigherPriorityTaskWoken = pdTRUE; // 发送完毕后我们进行手动切换任务
}
HAL_UART_DMAStop(huart);
}
// DMA传输完成后的操作
if (REC_MAX_NUM == Size) // DMA全满中断
{
// 使用空闲中断完成任务,不应该进入全满中断
log_e("error: DMA_Cplt_Callback!");
}
// 触发任务切换
portYIELD_FROM_ISR(xHigherPriorityTaskWoken);
}
}
需要注意的是,为了在空闲中断中能够获取空闲标志,需要在串口中断服务函数中注释掉“清除空闲标志”的部分(因为开启串口空闲DMA接收后会默认开启DMA半满中断和DMA全满中断,而我们现在只需要空闲中断来处理数据,这样做是为了区分半满中断和空闲中断,至于全满中断,我们每包数据的最大长度是确定的,所以每次接收可以设置一个大于这个最大长度的值来避免触发全满中断:如最大值是1029,则设置串口空闲DMA接收1030字节)。
在串口中断服务函数中注释掉“清除空闲标志”的部分:

OTA线程任务
/* Variable -----------------------------------*/
/* task attributes */
QueueHandle_t Q_Ymodem_Rec_Len = NULL; // Queue for Ymodem receive length
osThreadId_t OTA_TaskHandle;
const osThreadAttr_t OTA_Task_attributes = {
.name = "OTA_Task",
.stack_size = 128 * 4,
.priority = (osPriority_t)osPriorityNormal1,
};
/* Ymodem */
static int32_t sg_file_size = 0; // receive file size
uint8_t rec_data[REC_MAX_NUM] = { 0 }; // receive data buffer
/* Functions ----------------------------------*/
void OTA_Task(void* argument)
{
// HAL_UARTEx_ReceiveToIdle_DMA(&huart1, rec_cmd, REC_MAX_NUM);
// 1.create queue
Q_Ymodem_Rec_Len = xQueueCreate(1, sizeof(uint16_t));
for (;;)
{
// 2.receive data
sg_file_size = Ymodem_Receive(rec_data);
if (0 != sg_file_size)
{
log_i("file_size = [%d]", sg_file_size);
sg_file_size = 0;
}
osDelay(1);
}
}
然后在 MX_FREERTOS_Init() 函数中
OTA_TaskHandle = osThreadNew(OTA_Task, NULL, &OTA_Task_attributes);
创建任务就好了。
openvela 操作系统专为 AIoT 领域量身定制,以轻量化、标准兼容、安全性和高度可扩展性为核心特点。openvela 以其卓越的技术优势,已成为众多物联网设备和 AI 硬件的技术首选,涵盖了智能手表、运动手环、智能音箱、耳机、智能家居设备以及机器人等多个领域。
更多推荐


所有评论(0)