设计与开发

单片机crc AOVX资产监测设备之华大单片机Boot软件升级

小编 2025-08-08 设计与开发 23 0

AOVX资产监测设备之华大单片机Boot软件升级

近期笔者在研究如何从软件方面提升资产监测设备的性能,从而提升用户的使用体验感。

资产监测设备的主要作用是帮助用户监测设备的实时位置、实时状态避免运动中的货物出现丢失等情况。AOVX环境监测设备在智慧物流方面发挥了重要的作用,例如在运输贵重货物过程中,只需要将该设备安装在货物中,用户即可远程了解货物的位置、货物是否出现暴力扔件、以及避免货物在运输过程中的丢失。

资产监测设备中的华大单片机,对该设备的工作发挥了重要的作用。笔者的软件团队对单片机进行了软件升级。

具体流程如下:

具体流程如下:/**

** \brief 上位机数据帧解析及处理**** \param [in] None**** \retval Ok APP程序升级完成,并接受到跳转至APP命令** \retval OperationInProgress 数据处理中** \retval Error 通讯错误********************************************************************************/en_result_t Modem_Process(void){uint8_t u8Cmd, u8FlashAddrValid, u8Cnt, u8Ret;uint16_t u16DataLength, u16PageNum, u16Ret;uint32_t u32FlashAddr, u32FlashLength, u32Temp;

if (enFrameRecvStatus == FRAME_RECV_PROC_STATUS) //有数据帧待处理, enFrameRecvStatus值在串口中断中调整

{

u8Cmd = u8FrameData[PACKET_CMD_INDEX]; //获取帧指令码

if (PACKET_CMD_TYPE_DATA == u8FrameData[PACKET_TYPE_INDEX]) //如果是数据指令

{

u8FlashAddrValid = 0u;

u32FlashAddr = u8FrameData[PACKET_ADDRESS_INDEX] + //读取地址值

(u8FrameData[PACKET_ADDRESS_INDEX + 1] << 8) +

(u8FrameData[PACKET_ADDRESS_INDEX + 2] << 16) +

(u8FrameData[PACKET_ADDRESS_INDEX + 3] << 24);

if ((u32FlashAddr >= (FLASH_BASE + BOOT_SIZE)) && (u32FlashAddr < (FLASH_BASE + FLASH_SIZE))) //如果地址值在有效范围内

{

u8FlashAddrValid = 1u; //标记地址有效

}

}

switch (u8Cmd) //根据指令码跳转执行

{

case PACKET_CMD_HANDSHAKE : //握手帧 指令码

u8FrameData[PACKET_RESULT_INDEX] = PACKET_ACK_OK; //返回状态为:正确

Modem_SendFrame(&u8FrameData[0], PACKET_INSTRUCT_SEGMENT_SIZE); //发送应答帧给上位机

break;

case PACKET_CMD_ERASE_FLASH : //擦除flash 指令码

if ((u32FlashAddr % FLASH_SECTOR_SIZE) != 0) //如果擦除地址不是页首地址

{

u8FlashAddrValid = 0u; //标记地址无效

}

if (1u == u8FlashAddrValid) //如果地址有效

{

u32Temp = u8FrameData[PACKET_DATA_INDEX] + //获取待擦除flash尺寸

(u8FrameData[PACKET_DATA_INDEX + 1] << 8) +

(u8FrameData[PACKET_DATA_INDEX + 2] << 16) +

(u8FrameData[PACKET_DATA_INDEX + 3] << 24);

u16PageNum = FLASH_PageNumber(u32Temp); //计算需擦除多少页

for (u8Cnt=0; u8Cnt<u16PageNum; u8Cnt++) //根据需要擦除指定数量的扇区

{

u8Ret = Flash_EraseSector(u32FlashAddr + (u8Cnt * FLASH_SECTOR_SIZE));

if (Ok != u8Ret) //如果擦除失败,反馈上位机错误代码

{

u8FrameData[PACKET_RESULT_INDEX] = PACKET_ACK_ERROR;

break;

}

}

if (Ok == u8Ret) //如果全部擦除成功,反馈上位机成功

{

u8FrameData[PACKET_RESULT_INDEX] = PACKET_ACK_OK;

}else //如果擦除失败,反馈上位机错误超时标志

{

u8FrameData[PACKET_RESULT_INDEX] = PACKET_ACK_TIMEOUT;

}

}

else //地址无效,反馈上位机地址错误

{

u8FrameData[PACKET_RESULT_INDEX] = PACKET_ACK_ADDR_ERROR;

}

Modem_SendFrame(&u8FrameData[0], PACKET_INSTRUCT_SEGMENT_SIZE); //发送应答帧到上位机

break;

case PACKET_CMD_APP_DOWNLOAD : //数据下载 指令码

if (1u == u8FlashAddrValid) //如果地址有效

{

u16DataLength = u8FrameData[FRAME_LENGTH_INDEX] + (u8FrameData[FRAME_LENGTH_INDEX + 1] << 8)

- PACKET_INSTRUCT_SEGMENT_SIZE; //获取数据包中的数据长度(不包含指令码指令类型等等)

if (u16DataLength > PACKET_DATA_SEGMENT_SIZE) //如果数据长度大于最大长度

{

u16DataLength = PACKET_DATA_SEGMENT_SIZE; //设置数据最大值

}

u8Ret = Flash_WriteBytes(u32FlashAddr, (uint8_t *)&u8FrameData[PACKET_DATA_INDEX], u16DataLength); //把所有数据写入flash

if (Ok != u8Ret) //如果写数据失败

{

u8FrameData[PACKET_RESULT_INDEX] = PACKET_ACK_ERROR; //反馈上位机错误 标志

}

else //如果写数据成功

{

u8FrameData[PACKET_RESULT_INDEX] = PACKET_ACK_OK; //反馈上位机成功 标志

}

}

else //如果地址无效

{

u8FrameData[PACKET_RESULT_INDEX] = PACKET_ACK_ADDR_ERROR; //反馈上位机地址错误

}

Modem_SendFrame(&u8FrameData[0], PACKET_INSTRUCT_SEGMENT_SIZE); //发送应答帧到上位机

break;

case PACKET_CMD_CRC_FLASH : //查询flash校验值 指令码

if (1u == u8FlashAddrValid) //如果地址有效

{

u32FlashLength = u8FrameData[PACKET_DATA_INDEX] +

(u8FrameData[PACKET_DATA_INDEX + 1] << 8) +

(u8FrameData[PACKET_DATA_INDEX + 2] << 16) +

(u8FrameData[PACKET_DATA_INDEX + 3] << 24); //获取待校验flash大小

if ((u32FlashLength + u32FlashAddr) > (FLASH_BASE + FLASH_SIZE)) //如果flash长度超出有效范围

{

u8FrameData[PACKET_RESULT_INDEX] = PACKET_ACK_FLASH_SIZE_ERROR; //反馈上位机flash尺寸错误

}else

{

u16Ret = Cal_CRC16(((unsigned char *)u32FlashAddr), u32FlashLength);//读取flash指定区域的值并计算crc值

u8FrameData[PACKET_FLASH_CRC_INDEX] = (uint8_t)u16Ret; //把crc值存储到应答帧

u8FrameData[PACKET_FLASH_CRC_INDEX+1] = (uint8_t)(u16Ret>>8);

u8FrameData[PACKET_RESULT_INDEX] = PACKET_ACK_OK; //反馈上位机成功 标志

}

}

else //如果地址无效

{

u8FrameData[PACKET_RESULT_INDEX] = PACKET_ACK_ADDR_ERROR; //反馈上位机地址错误

}

Modem_SendFrame(&u8FrameData[0], PACKET_INSTRUCT_SEGMENT_SIZE+2); //发送应答帧到上位机

break;

case PACKET_CMD_JUMP_TO_APP : //跳转至APP 指令码

Flash_EraseSector(BOOT_PARA_ADDRESS); //擦除BOOT parameter 扇区

u8FrameData[PACKET_RESULT_INDEX] = PACKET_ACK_OK; //反馈上位机成功

Modem_SendFrame(&u8FrameData[0], PACKET_INSTRUCT_SEGMENT_SIZE); //发送应答帧到上位机

return Ok; //APP更新完成,返回OK,接下来执行跳转函数,跳转至APP

case PACKET_CMD_APP_UPLOAD : //数据上传

if (1u == u8FlashAddrValid) //如果地址有效

{

u32Temp = u8FrameData[PACKET_DATA_INDEX] +

(u8FrameData[PACKET_DATA_INDEX + 1] << 8) +

(u8FrameData[PACKET_DATA_INDEX + 2] << 16) +

(u8FrameData[PACKET_DATA_INDEX + 3] << 24); //读取上传数据长度

if (u32Temp > PACKET_DATA_SEGMENT_SIZE) //如果数据长度大于最大值

{

u32Temp = PACKET_DATA_SEGMENT_SIZE; //设置数据长度为最大值

}

Flash_ReadBytes(u32FlashAddr, (uint8_t *)&u8FrameData[PACKET_DATA_INDEX], u32Temp); //读flash数据

u8FrameData[PACKET_RESULT_INDEX] = PACKET_ACK_OK; //反馈上位机成功 标志

Modem_SendFrame(&u8FrameData[0], PACKET_INSTRUCT_SEGMENT_SIZE + u32Temp);//发送应答帧到上位机

}

else //如果地址无效

{

u8FrameData[PACKET_RESULT_INDEX] = PACKET_ACK_ADDR_ERROR; //反馈上位机地址错误 标志

Modem_SendFrame(&u8FrameData[0], PACKET_INSTRUCT_SEGMENT_SIZE); //发送应答帧到上位机

}

break;

case PACKET_CMD_START_UPDATE : //启动APP更新(此指令正常在APP程序中调用)

u8FrameData[PACKET_RESULT_INDEX] = PACKET_ACK_OK; //反馈上位机成功 标志

Modem_SendFrame(&u8FrameData[0], PACKET_INSTRUCT_SEGMENT_SIZE); //发送应答帧到上位机

break;

}

enFrameRecvStatus = FRAME_RECV_IDLE_STATUS; //帧数据处理完成,帧接收状态恢复到空闲状态

}

return OperationInProgress; //返回,APP更新中。。。

}

同样从事软件工作相关的用户,对资产监测设备感兴趣的话可以参考该文档。

如何写一个健壮且高效的串口接收程序?

学单片机的大概最先、最常写的通信程序应该就是串口程序了,但是如何写出一个健壮且高效的串口接收程序呢?接下来鱼鹰将根据多年的开发经验教你如何编写串口接收程序(可在公众号获取个人编写的串口接收源码)。

本篇文章包含以下内容,很长,但干货满满,就看你能吸收多少了:

1、 传入参数指针

2、 互斥锁释放顺序

3、 数据帧检查

4、 串口空闲

5、 通信吞吐量

内容很多,鱼鹰慢慢写,道友您也请慢慢看。

为了更好的理解接下来的知识点,鱼鹰将设计一个串口框架,让道友心中有一个参考方向。

本篇重点在于解决如何写一个健壮、高效的串口接收数据,发送与接收处理过程略讲。

帧格式

先聊聊帧格式,一般来说,一个数据帧有以下几部分内容:

帧头

帧头用于分辨一个数据帧的起始,这个帧头必须足够特殊才行,因为它是分辨一个帧的起始,那么什么样的帧头是足够特殊的数据呢?保证这个数据在一个帧内最好只出现一次的数据,那就是帧头,比如 0x55、0xAA 之类的。而且最好有两个字节以上,这样帧头才更加独一无二。

但是数据域内的数据你是没办法保障不包含和帧头一样的数据。

那么如果不凑巧,除了帧头外 其他部分也有这样的两个字节的帧头,那会出现什么问题?

几乎不会出现问题。 因为一般来说数据都是一帧一帧发送的,只要你前面的数据帧传输正确,那么即使下一帧的数据中有和帧头一样的数据(包括帧头)也没有问题,因为帧头判断已经在开始就判断成功了,就不会继续判断后面的数据是否是帧头了。

那么为什么说是几乎,因为如果上一帧数据接收错误,那么程序必须再找一次帧头才行(单字节接收时是如此,采用空闲中断的话就不需要这么麻烦),这就导致找帧头的时候在帧头数据之外寻找了,很可能这些数据就有帧头。

但是即使帧头数据之外的假帧头真的存在,也没关系,还有第二重保障,那就是校验,即使找到了一个错误的帧头,那么数据校验这一关也很难过去,所以放宽心。

如果校验也凑巧通过了,那还有第三重保障:帧尾。应该到不了这里吧,毕竟这比中彩票还难。

又要上一帧数据接收错误,还要当前帧除了帧头之外还有帧头,另外你还能跳过校验的检查(还有功能字、长度信息的检查),太难了。所以只要通过了这些检查,你就可以认为这个数据帧是可用的了。所以一帧数据接收错误,导致的问题最多只是丢失了这帧数据,对后续接收是不会有影响的(前提是你这个接收程序设计的足够好),发送端在发送超时后再发送一次即可,所以重发机制很重要

事实上,如果你采用串口空闲中断 ,帧头、帧尾都可以不用,但一般来说,帧头都会保留,帧尾可以不需要,这是为了当单片机没有串口空闲中断时考虑,当然也可能有其他考虑,所以帧头得保留。

功能字

功能字主要用于说明该数据帧的功能,当然也可以作为函数指针的索引,一个索引值代表了一个具体功能,据此可找到对应的功能函数。

比如,设计一个函数指针数组,通过功能字进行索引,进而跳转到对应的功能函数中处理。

特别注意的是,设计功能字的时候,要考虑兼容性,对数据帧的功能进行划分,不要想到一个算一个,功能字也不要随便安排,不然在以后增加数据帧的时候会很麻烦。

比如说,只有一个字节的功能字,前四位作为一个大类,后四位作为大类中具体类。这样就可以将系统数据通信帧分为 16 个大类,每个大类下有 16 个可用的具体类,当你增加功能字的时候,就可以根据你的设计来确定属于哪个大类了,然后再插入进去。这样在管理、维护这些通信数据时你会发现很方便。

这个思想其实在 ARM 内核的中断系统和设计 uCOS II 任务优先级的时候都有,而鱼鹰在设计项目的通信协议的时候就是运用了这些思想。

(图片来源于《权威指南》)

长度

长度信息也是一个非常关键的数据,别小看了它,因为它,鱼鹰用了将近一个星期的时间才把一个 HardFaul 问题解决了,虽然这个程序 bug 不是我写的(鱼鹰一直用的是串口空闲接收方式,这个 bug 自然而然就跳过了),但确实很容易出错。

因为它是决定了你这个数据域长度的关键信息(一般长度信息代表数据域的长度,而不包含其它部分长度),也是这个数据帧的长度信息(加上固定字节长度就是帧长度了),更是接收程序还要接收多少数据的关键信息(对于空闲中断接收方式不算关键,这里的不关键是指不会造成程序异常问题)。

比如说你的程序刚好将帧头、帧尾、功能字判断完毕,然后中断程序因为种种原因导致没有及时接收串口数据,那么你可能得到的就是错误的数据,然后这个错误的长度数据就可能导致你的栈帧或者全局变量被破坏(单字节接收情况下就可能出现,因为鱼鹰碰到过),这是很严重的事情。所以在接收数据域的数据之前一定一定 要判断这个长度信息(空闲中断除外)是否合法,不合法的话及时扔掉这帧数据,开始下一帧的数据检查。

所以为了保证及时接收数据,最好采用 DMA 传输。

数据域

这个没啥好说的,就是整个帧你真正需要发送的数据。而为了让你的发送函数能接收各种类型的数据,那么把参数类型设置为 void * 会是不错的选择。

校验

一个数据在接收过程中可能会被干扰,导致接收到错误的数据,那么如何保证这帧数据的完整与准确性呢,就在校验这一关了。

校验有很多方式,和校验、CRC 校验等(奇偶校验是针对一个字节的,不是数据帧)。

和校验算法简单,CPU 运算量小,累加最后只取最低字节 即可(注意不是高字节,想想为什么),或者保存累加和的变量就是一个字节空间,这样就不需要额外操作了。

CRC 校验,这个算法复杂,理解起来比较困难,但一般来说可以直接拿来用,因为它是对每一位(bit)进行校验,所以纠错率很高,几乎不存在发现不了的数据错误,但正因为对每一位进行检查,所以 CPU 运算量较大,但是有的单片机是可以硬件计算 CRC 校验值的(比如 stm32)。不过现在 CPU 运算速度都挺快的,软件运算也是可以接受的。

那么该怎么校验呢?是从帧头开始到数据域部分,还是说直接校验数据部分?其实都可以,区别就是运算量问题,不过问题不大(最好是从头开始校验,以保证整帧数据的准确性)。

帧尾

前面说了,帧尾在空闲中断中可以不用,RXNE 中断接收时其实也可以不用,当然也可以加上,好处就是当你用串口助手查看数据流时,可以观察出一帧数据是否发送完整了。

最后再说说为什么在数据域 前面设计四个字节大小 ,除了协议本身需要外,还有一个原因就是强制类型转化 需要,我们知道,一般来说,赋值时都有字节对齐的限制(实际上有的 CPU 可以不对齐进行赋值),stm32 是 32 位的,那么四字节对齐是最合适的,这样就可以直接将我们收到的数据转化为需要的数据类型了。

传输过程

聊完了帧格式,再从大的方向看串口的传输过程:

当发送端发送第一帧数据包时,接收端通过某种方式接收(串口接收非空 RXNE 中断、串口空闲 IDLE 中断),为了让串口能够触发空闲中断,必须在发送端两个发送帧之间插入一段空闲时间(就是在此时间内不发数据,红色部分),保证空闲中断的准确触发。

同理,为了让发送端也能正常接收接收端的数据,也需要控制接收端的发送,不能在返回一帧数据时立马发送下一帧数据,不然触发不了发送端的空闲中断。

事实上,有些程序员设计的发送、接收过程比这个简单一些。即只有当接收端接收到一帧数据并返回一帧数据之后,发送端才能继续发送数据,这样一来,我们只需要控制好接收端的频率,就可以控制整个通信过程,也能控制通信频率。

但为什么还要设计成第一种传输情况呢?这是为了充分利用串口,增大数据吞吐率(这个后面再说)。

另外,不知道你是否观察到图中的每个数据帧占用的时间是不一样的,这是因为每个数据帧不可能都是一样长的,它们是不定长的数据包,所以你的定时不能从发送开始定时,而是从发送完成后开始定时控制空闲时间。

下集精彩,串口的软件设计,喜欢的话记得关注鱼鹰哦!

相关问答

个人电脑如何控制 单片机 ?

个人电脑控制单片机,主要通过以下步骤:1)建立个人电脑和单片机的通信连接普通的单片机通常有UART、SPI、IIC、USB等通信模块。和电脑通信最常用的是UART,可...

51 单片机 和stm 单片机 的区别?

两者的主要区别如下:1、内核:51单片机采用的是51Core,8Bit@2MHzMax(分频后),0.06DMIPS;STM32采用的是ARMCortex-M3,32Bit@72MHz,1....

51 单片机 c语言看门狗程序肿么写-ZOL问答

看门狗在51单片机电路里的作用是防止程序“跑飞”、“死机”后,系统不动作,而采取复位的办法“唤醒”系统。89S51、89S52系列单片机自带有看门狗...

串口通信用的是什么协议- 汇财吧专业问答

[回答]串行接口是一种可以将接受来自CPU的并行数据字符转换为连续的串行数据流发送出去,同时可将接受的串行数据流转换为并行的数据字符供给CPU的器件。一...

现代交换原理与通信网技术第8次印刷习题答案_作业帮

[回答]1.1为什么说交换设备是通信网的重要组成部分?转接交换设备是通信网络的核心,它的基本功能是对连接到交换节点的传输链路上的信号进行汇集、转接和分...

usart和uart优缺点?

USART(通用同步/异步收发器)和UART(通用异步收发器)都是用于实现串行通信的协议。下面是USART和UART的一些优缺点:USART优点:1.支持同步和异步的串行通信...

大神们,有没有 广州锐捷收发一体光模块接收灵敏度,收发一...

[回答]无限收发一体数传MODEM模块PTR2000芯片性能优异,在业界居领先水平,它的显著特点是所需外围元件少,因而设计非常方便。该模板块在内部需成了高频接收...

猜你喜欢