项目主要框架
主要机制
这次接手的一个项目是一个用Lora模块进行无线通信的转发电台,复位后完成MCU各个外设的初始化,再对无线模块,仪表通信进行初始化,尔后进入休眠-检测唤醒-唤醒-接收数据-处理数据-转发数据-休眠的循环当中。
主程序流程图
![](https://img-blog.csdnimg.cn/direct/0241bf56a13f4f728ef205467e5e93b3.png)
这张流程图是我刚接手到项目时花了半小时左右设计的,设计时感觉幸行云流水,觉得不出意外十个工作日左右应该可以完成,真正做项目时就遇到了非常多的难题。硬是拖了20个工作日才完成。
卡住我的几个难题
难题1:第一次接触低功耗设计,无从下手
产生的原因:实际项目经验不足,在学校,在网课的,在小实验的过程中都是针对都几方面的知识点针对性的编写代码,都是使用USB或其他能一直稳定供电的设备进行供电,根本不用考虑功耗问题。而这个系统是在户外使用电池供电的,对续航能力有着较为严苛的要求。
解决方案:从stm32l0系列单片机的了解开始,再到stm32常用低功耗机制的了解,再到Lora模块功耗模式的了解。逐步有了头绪
难题2:低功耗模式下,调试极其困难,无法下载程序,会和调试器断开连接,并且MDK会崩溃,只能用任务管理器把他请出去
产生的原因:由于产品的低功耗设计,需要使用到stm32单片机的停止模式,停止模式下,CPU停止运行,无法正常的调试。看门狗定时器无法再调试模式下正常关闭。
解决方案:使用下面语句,可以在调试状态下,进入低功耗时正常的进行调试
#if (DEBUG == 1)
// 若需要在低功耗模式下调试程序,可以打开此函数
HAL_DBGMCU_DBG_DisableLowPowerConfig(DBGMCU_SLEEP | DBGMCU_STOP | DBGMCU_STANDBY);
#endif
经过查阅资料,网上检索了解到,理论上调试状态下IWDG看门狗不会运行,不知道是BUG还是我没设置对,又或者stm32l0系列为了降低成本和功耗阉割了这个特性,调试状态下依然会起作用,导致MCU在我调试时复位,然后我的MDK就会崩,又要去请任务管理器大神出马。于是只能添加了这段代码
#if (DEBUG != 1)
MX_RTC_Init();
MX_IWDG_Init();
#endif
调试状态下,直接不初始化这个外设
然后就是无法正常下载,低功耗模式下,下载的IO口是无法正常工作的,所以也是无法正常下载,最终我选择了复位时延时5s来解决,只要在这5s内下载程序就可以以正常下载(PS:使用上面的代码虽然可以在低功耗下正常调试,但前提时进入低功耗模式前进入调试模式,那我这个举例,如果想要下载调试,必须在这5s内进行)
难题3:因为各种原因错误,导致Lora丢包
产生的原因:第一次接触Lora模块,对这个模块的各项参数都不熟悉,导致在低功耗模式下在需要休眠时仍未正确休眠功耗翻车,在需要运行时未正确唤醒,或者唤醒超时时间过短,还没有处理完数据就又休眠了。总之各种对参数的不熟悉导致丢包,功耗翻车
解决方案:深入理解lora的唤醒间隔,空闲时间,超时时间,准确估算MCU处理时间,在需要的地方及时唤醒Lora。
难题4:各种问题导致系统整体电流降不下来
产生的原因:没有关闭应该关闭的外设,时钟,没有妥善处理悬空的引脚,系统频率太高,实际根本不需要这么高的频率
解决方案:将未使用的引脚全部置为模拟态,低功耗模式下将除了与唤醒有关引脚以外的引脚全部置为模拟态,频率不是越高越好,而是越合适越好,不用的外设就关掉,用到时再重新初始化(这是我在学校学习留下来的毛病,拿到芯片直接置为最高频率,不用的引脚直接不管,外设被mx初始化完就不管了)
难题5:较高的实时性要求,需要精准的在需要的地方进行延时
产生的原因:对MCU和Lora模块处理任务时间的不理解,一开始的Lora执行命令函数中,我只会给模块50ms的处理命令时间,于是所有AT命令执行的都非常不稳定,有时候能成功,有时候不能,和玄学一样。
解决方案:无论是任何模块都要了解他的处理时序,嵌入式开发是对实时性要求很高的一门技术,需要精确的指导每一个步骤所需时间,经过我的细致观察我发现Lora执行一个AT指令至少要300ms,部分指令要400ms,有些指令重启生效,波特率等用于通信的指令不可以随便更改
难题6:Flash只有16K,内存资源极其紧张
产生的原因:和前面几个问题产生的根本原因几乎一样----缺乏经验。由于Flash太小,我使用的单片机是装不下多少代码的。我一开始竟然妄图使用RTOS,设计了七八个任务,好几个消息队列,最后足足编写了两天的代码结果Flash完全装不下
解决方案:依据适合的场景选择适合的开发方式,内存紧张时就要选用裸机开发,不用的外设驱动就裁剪掉,增加编译器优化等级,减少全局变量的使用,内存实在紧张可以考虑使用微库
做的不足的地方
1.没有清晰的开发计划
在这次开发的过程中,没有预先制定每一个模块的开发时间,开发时一直都是走一步看一步,遇到卡壳的地方,就一直死磕,先搜索,搜索不到就AI,AI不行就单步调试,再不行就看手册,再不行就继续这个循环,直到行为止。造成的后果就是原先计划10个工作日左右完成的内容硬是拖了20个工作日才完成。
2.开发习惯不好
在开发的过程中,会随着程序的流程转移开发任务,比如当某个模块要用到串口时,我会转移到串口开发,等开发完串口,再回到原先开发的程序进行编写(有一点像汇编中函数跳转一样),这样的缺点很明显,当开发完子流程后回到原先的开发,那种行云流水,一气呵成的感觉就没了,需要花费额外的时间重新找回这个感觉,经过这次实践,我发现,先写一个子流程函数声明,把这个程序块写完,然后在去编写子流程,这样不仅能增加开发效率,还可以降低开发难度(因为我大脑的栈空间有限,无法支持来回的跳转),这就有一点像C++中的虚函数。
3.封装过度
很合理的封装可以增加可维护性和可读性,但本次的项目中有很多地方我进行了过度封装,这是多余的部分,比如在进入/退出低功耗的函数设计中,我在这些函数中又添加了进入/退出低功耗模式硬件初始化函数,这是多余的,完全可以在进入/退出低功耗的函数中直接实现
做的比较好的地方
1.代码习惯良好。
我学过HAL库,Freertos,Linux驱动开发(虽然只到点灯的地步),这些优秀的代码对我产生了深远影响,他们良好的设计方式,恰逢其时的注释,让我学会了很多增加代码可读性,可移植性的技巧。例如使用结构体封装某一功能,尽量多的使用宏代替常数,每隔函数都是用标准的注释方法注释清楚,以StatusType代替void作为程序的返回值,以if(xxx(xxx,xxx)==FALSE){ErrorHandle();}代替xxx(xxx,xxx);使用#define xxx_IO GPIOx,#define xxx_pin GPIO_PIN_xxx。以下也是一个比较合适的案例
//封装前
uint8_t Uart1_Buf[150];
uint8_t Uart1_Num;
uint8_t Uart1_Flag;
uint32_t Uart1_Time;
uint8_t Uart1_Temp;
uint8_t Uart2_Buf[150];
uint8_t Uart2_Num;
uint8_t Uart2_Flag;
uint32_t Uart2_Time;
uint8_t Uart2_Temp;
//封装后
//usart.h
#define UART_BUFF_SIZE 150
typedef struct
{
uint8_t Buff[UART_BUFF_SIZE];
uint8_t Num;
uint8_t temp;
volatile uint8_t Flag;
volatile uint32_t Time;
} _UART_Rx;
extern _UART_Rx UART1_Rx;
extern _UART_Rx UART2_Rx;
//usart.c
_UART_Rx UART1_Rx={0};
_UART_Rx UART2_Rx={0};
2.保持自信,没有被困难吓退
这个项目其实是有一套老代码的,但是使用的Lora模块功耗较大,所以要换模块,原先的代码极其混乱,不易阅读,更不易移植,而这个重构的任务竟然全部交到了我一个人手上,要知道我大四还没毕业,没有任何拿得出手的项目经验,尽然让我来做优化功耗这种极其吃经验的项目。但是我没有退缩,迎难而上。从单片机低功耗系列,到单片机低功耗模式,到单片机低功耗技巧,到啊模块的低功耗,一步一个脚印的去了解,去实践,最终超额完成了任务,任务指标是休眠时,3.6V下,电流小于50uA,经过我的优化达到了12uA。
3.敢于革新
我没有寄希望于老项目微调(其实有,只是老项目实在。。。),用我学习linux开发的模块化思想重构了项目,在解决丢包的过程中我没有守旧使用老工程的办法,而是采用新模块的新特性数据重传,前向纠错,信道检测等等优化数据传输,最终在测试中实现了100%准确传输
我的收获
1.收获了低功耗设计方法,思路,调试技巧
方法:选择低功耗的芯片,选择合适的低功耗模式,仅在需要的时候打开需要的外设,不用的引脚要设为模拟态,如果希望能正常调试,不能复位后立马进入低功耗模式,要预留一些时间用来连接调试器,以便下载程序,调试程序
2.更深刻的明白良好的代码习惯的好处
将一个模块封装为结构体,而不是一大堆散乱的全局变量(diss一下老项目。。。),单独的模块要放在单独的文件中,并且用对应的头文件去声明它的函数,变量,而不是全放在main.c里面(再diss一下老项目)在主程序中只留下清晰的逻辑,将单独的功能放在单独的函数中,而不是所有细节全放在main函数里面(再再diss一下老项目)
3.学会了沟通技巧
之前都是在一些非常成熟的开发板上面进行操作,而且针对初学者,资料特别多,有什么问题一查就知道,实际开发的工程中,板子的功能是比较单一的,最了解它的还是设计这个PCB的人,有什么问题一定要多问,但是也不是高频次的问,而是思考后囤积一下问题,在合适的时间一次性问。同时也学会了相处时的态度把控,有时需要虚心求教,但有时保持自己的观点也是很重要的,如果确实不是自己的问题,找出有说服力的论据,保持理性沟通的心态,据理力争也是有必要的。
4.学习了Lora模块的使用
通过本次项目,我全面的了解了Lora模块的基本使用,包括信道,空中速率,地址等等最只因本的参数,也包括唤醒间隔,超时时间,空闲时间等等与低功耗有关的参数,通过调节这些参数,我学会了控制一个远程通信保持稳定,不丢包的方法。
5.学习了下位机与上位机通信协议技巧
在学校的学习中,上位机下位机通信,最常用的办法就是发送时使用printf重映射,接收时按照固定的长度接收,并且多使用ascii字符,然后解析字符。通过这次实际的项目开发,我发现,使用sprintf+HAL_UART_Transmit+strlen的方式效率会比printf更高,适用范围也更广,使用通信协议,先发送链接码表示已经和上位机链接,要发送数据时,可以先发固定长度字符串,其中包含下一步要发送的数据大小,然后下位机发送确认码,然后上位机再发送数据,这样做在数据比较长时,有比较明显的优势,使用HEX+索引的方式要比字符串更高效,如下代码所示
//字符串解析方式
//上位机发送:A=1,B=2,C=3
//下位机
sscanf(Rx,"A=%d,B=%d,C=%d",&A,&B,&C);
//HEX+索引方式
//上位机发送L:00 01 02
//下位机接收
#define A_Index 0
#define B_Index 1
#define C_Index 2
A=Rx[A_Index];
B=Rx[B_Index];
C=Rx[C_Index];
6.学会了内存省着用
直到这次实际的项目开发,我才体会到MCU资源紧张是什么概念,之前在学校里用的是128KB的Flash,20KB的SRAM,完全想象不到竟然会有MCU装不下RTOS(或许是我不会裁剪),这次使用的是stm32L0系列的一个小容量产品,内存资源极其紧张,使用稍微大一点的数组时,我都要用代码块{}括起来,以便于节省空间,并且减少了库函数的使用,用自己编写的更简短的代码代替库函数。学会了看map文件,学会了看链接脚本sct文件以达到了解内存的分布(其实没有学会,只达到了初步看懂)。