因工作需要,最近写了几段32的单片机程序。就来总结一下呗。
今天的主题:
1.在没有硬件的情况下,如何布局单片机程序。
2.在拿到硬件的情况下,如何快速调试自己的单片机程序
3.在单片机出现故障时,如何快速定位问题,并排除问题?
三个小问题呢,其实就是一个问题,就是如何为调试布局。(我的思路也不一定是最优解。请多多指教)
参考代码:
https://github.com/zhaozhi0810/gd32f103ret6-2022-04/blob/main/src/main.c
本节主要是main.c
/*
宏定义功能控制,定义在includes.h文件中
// 1. lcd 亮度pwm控制,调整液晶屏亮度
// 2. lcd 加热pwm控制,没有定义时就是直接加热或关闭,不能降低功率
// 3. lcd 加热控制,用于控制lcd加热功能和18b20温度读取功能,关闭后,不再读取18b20温度,液晶屏加热停止
本软件用于GD32f103ret6,主要功能包含如下: 8M晶振输入
1. 按键检测(PA0,低电平有效),控制另一个引脚(PA1)输出高低电平,用于控制另一个cpu的开机关机操作
短按开机(输出高电平),长按(大于5s)关机(串口发出关机命令),上电默认自启动(高),
2. 串口2用于与cpu通信,应用通信协议待定。115200,8N1 (PA2,PA3)
3. 串口1用于调试输出,115200,8N1 (PA9,PA10)
4. 调试led 低电平点亮 PA8
5. 风扇控制。高开启,低关闭 PA12
6. 预留风扇PWM控制,默认不使用PWM功能 PA11
7. 18b20温度传感器。(PB0,PB1) -60~+100(高低温实验)
8. 液晶屏加热使能(PB3 高电平)
9. 液晶屏背光pwm(PB4,使用定时器)
10.液晶屏背光使能(PB5)
11.iic1(PB6,PB7)[1.sm2900-11 4路电压采集,2.sm2900-01 两路温度采集]
12.看门狗芯片使能(PB8)
13.看门狗喂狗信号(PB9)
14.iic2(PB10,PB11) [1.28v电源的电流上报]
15.io输入(PB12,PB13,PB14,PB15)
16.cpu复位信号(PC0,输入)
17.复位控制(PC1,输出)
18.7A桥复位信号1(PC2,输出)
19.7A桥复位信号2(PC3,输出)
20.与cpu的io通信引脚(PC4-PC6,PC10,PC11)
21.LVDS转换使能引脚(PC12,输出)
22.硬盘销毁引脚(PC13,输出),!!!!已更改
新原理图的修改
23. 4个状态的读取,PA4,PA5,PA6,PA7
24. 硬盘销毁引脚 PA15,可能需要对调试端口的引脚进行设置。
25. 液晶屏供电使能 PC15,输出高电平有效
26. 转换使能高有效 PC14,输出高电平有效
27. 与FPGA通信预留引脚,PC7,PC8,PC9
*/
#include "includes.h"
const char* g_build_time_str = "Buildtime :"__DATE__" "__TIME__; //获得编译时间
uint16_t g_task_id; //每一个位对应一个任务,为1表示需要启动任务,在任务中清零该位
800ms 看门狗
static void iwdog_init(void)
{
fwdgt_write_enable();
fwdgt_config(0xfff,FWDGT_PSC_DIV8); //设置分配系数,最长819ms
fwdgt_enable(); //使能看门狗
}
static void iwdog_feed(void)
{
// if(mcu_reboot) //设置mcu重启,不喂狗。2021-12-17增加
// return ;
fwdgt_counter_reload();
}
static void BoardInit(void)
{
//0. 中断分组初始化
//NVIC_SetPriorityGrouping(4); //均为4个等级
nvic_priority_group_set(NVIC_PRIGROUP_PRE2_SUB2);
//0.1 复用功能模块通电
rcu_periph_clock_enable(RCU_AF);
//1.串口初始化
//#define DEBUG_COM_NUM 0 //调试串口号
//#define TOCPU_COM_NUM 1 //与cpu通信的串口
gd_eval_com_init(DEBUG_COM_NUM); //用于调试
gd_eval_com_init(TOCPU_COM_NUM); //用于与cpu数据通信,改到cpu上电后再初始化
//2.systick 初始化
SystickConfig();
//3.lcd加热引脚控制初始化,默认输出低
LcdHeat_Init(); //没有加热功能时,也要保证初始化后引脚为低
#ifdef LCD_HEAT_ENABLE
//4.18B20初始化
DS18B20_Init(ds18b20_ind1);
DS18B20_Init(ds18b20_ind2);
#endif
//5. 电源按键和cpu电源控制
PowerBtn_Control_Init();
//6. cpu运行状态监控引脚初始化
Cpu_Run_Pins_Init();
//7.散热风扇初始化
Fan_Control_Init();
//8. 4路di输入的引脚配置
Di_4Ttl_Pins_Init();
//9. lcd控制引脚初始化
lcd_pwm_init(70); //亮度默认为70% ,此时显示屏不开启!!!!
//10. 初始化外部硬件看门狗,默认不开启
Hard_WatchDog_Pins_Init();
// 11.led初始化
Led_Show_Work_init();
//12. 电压电流,温度,iic的初始化
Vol_Temp_Api_Init();
//13. 销毁
Msata_Destroy_Pin_Init(); //默认输出高
//14. 启动系统,默认上电时是启动系统的。
Enable_Cpu_Poweron();
//15. 启动单片机内部看门狗
iwdog_init();
}
int main(void)
{
uint8_t i;
const task_t task[TASK_MAX]={Btn_Power_Change_Scan //任务1,上电按钮扫描
// ,[1] = task1_btn_scan //任务2,无
,[2] = Task_Check_CPU_Run_Status //任务3,运行状态检测,关机重启控制,这个优先级可以低一点
,[3] = Task_Get_Temp_Vol //任务4,温湿度,电压监控读取任务,2000ms调用一次
// ,[4] = Task_ReportTo_Cpu_status //任务5,定时向cpu汇报,500ms一次 //2022-04-21不再主动发送
// ,[5] = com3_frame_handle //无需要
,[14] = iwdog_feed //最后一个任务喂狗
,[15]=Task_Led_Show_Work //任务16,最后一个任务,让工作led灯闪烁,1s调用一次
//因为工作灯不能正常使用,所以删除该任务。2021-12-01
};
//1. 初始化
BoardInit();
printf("%s\r\n", g_build_time_str);
printf("BoardInit done! 2022-04-18\r\n");
while(1)
{
for(i=0;i<TASK_MAX && g_task_id;i++){
if(g_task_id & (1<<i)) //定时时间到,要执行
{
g_task_id &= ~(1<<i); //对应的位置被清零,等待定时器设置
if(task[i]) //指针不能为空
{
task[i](); //执行该任务
break; //一次只执行一个任务,任务靠前的优先级高,任务靠后的优先级低
}
}
}//end for
}
}
这个文件的内容不会很多,主要分成两个主要部分,一个是初始化,一个是主循环。
1. 初始化部分
考虑到程序写成时,并不能上硬件调试,所以需要考虑的是各个模块的独立初始化,我可以注释任意模块的初始化部分,而其他并不受到影响。
那这个部分主要考虑的就是每个模块独立初始化,并且不相互包含。如果一定要相互包含,请注意包含关系,并且尽量在依赖没有初始化的时候,考虑自身初始化的过程(是阻止自身初始化(可能就是直接返回了)?还是自己先部分初始化?)
这里我想到了一个小插曲,就是调试18B20的时候,参考了网上的代码,在进行通信阶段是关闭了中断的,而18B20出现通信错误的时候,是直接返回,而没有开中断。
平时正常使用的时候没啥问题,而有一次同事为了调试某个功能,把18b20给断开了,然后单片机就傻逼了(因为我的程序需要依赖中断才能运行)。然后就去调试,发现程序跑一会(大概几秒)中断就没了,但是启动的时候是有中断的,就觉得很莫名其妙。
当时没有想到18B20拆掉后造成的影响,花了很多功夫,才看到中断关闭而没有重新开启的问题。真是太。。。。汗颜了。
第二的话,我觉得要考虑部分模块的初始化的先后顺序,包括中断优先级,串口初始化等,这些就可以优先初始化,为后面的初始化做准备。
这两点就是我在初始化的阶段考虑的问题,我相信大家也都会这么去考虑。
毕竟即使单片机程序编译通过,真正上机测试的时候还是会出现很多意想不到的问题,这个时候就需要注释一些初始化的部分,来排除问题,初始化的部分不受其他影响,那我任意注释,问题都不会大。这样能加速我在初始化阶段问题的定位。
2. 主循环部分
初始化问题,其实是比较好解决的,毕竟程序只运行一次,要么成功,要么失败,失败就去定位是什么原因引发的失败。
而主循环的问题估计就不好解决,程序要运行多次,每一次运行的时候条件可能又会有一些差异,这样就会导致问题可能会出现,或者不出现。
一个模块出现问题还是比较好排除,反正程序跑到那个点,就会有问题。
然而,上机之前,所有的模块都没有真的跑过,这时候问题可能就比较复杂了,可能是各个模块之间的干扰,或者依赖关系,或者逻辑问题,那真是比较繁杂。
所以大家看到的贴出的代码,我用的是函数指针的方式,每一个函数指针基本就是某个模块循环采集或者控制。
我在主循环中,循环去调用这些函数指针,这样就能任意控制哪些模块的循环函数可能被调用,那如果出现问题,我可以把部分或者全部的函数指针注释,然后单独跑某一个模块的循环函数,这样我就可以逐步排除是哪个模块出现的问题,或者追踪到是哪个模块加入后引发的逻辑问题。
2.1 我考虑了每个模块的循环函数的周期的差异,所以使用的是用定时器(systick,1ms周期)控制,定时器不同的时间来设置某个标志位,只有这个位置1的时候,任务才有可能被执行。考虑到单片机程序不要跑飞了,判断一下函数的指针不为0,则执行该函数。
2.2 我设计的所有的周期函数,要考虑的是执行时间短,不延时的方式,如果需要延时,如何处理?请期待我后期的按键扫描识别程序的讲解(或者直接参考代码吧)。
2.3 循环函数也要考虑对其他模块的依赖情况,万一其他模块没有运行时的处理机制。
3.总结
这是我对如何进行问题排除的一个简单思路,并且这些方法已经用于多个项目,程序经过多轮实验测试,证明稳定可靠,暂时没有出现什么问题。
这是讲解的第一个部分,后续还会把代码中其他的关键部分再列举出来,如果大家有跟高明的方法,也希望能与大家多多交流学习。
如有需要,可以私聊我的。