原理图分析

image

USB供电

image

USB_IN是typeC口的5V,F1是自恢复保险丝,防止后级短路损坏USB充电器,两个电容滤除纹波,VCC_SYS为给锂电池充电的电压,R1限流电阻,8050三极管判断是否插入USB充电。8050三极管,V12大于三极管Vbe,则导通,USB_DETECT被拉低,说明USB插入。PC13默认配置为上拉输入。 三极管

锂电池充电

image

该锂电池,充电电压4.2V,放电截止电压2.75V,但实际处理时按照3V来算

charge可阅读TP4055手册,是充电状态指示。R4接在PROG,是控制充电电流的。BT1是锂电池,R5R6搭配PC5引脚是测量电池电压的。后面两个三极管电路,Q1是决定锂电池VBAT供电,还是USB的VCC_SYS直接供电。Q7是搭配拨动开关SWS,决定是否给整个设备供电(就是设备的开关机拨码开关) 二极管

Q1电路:(Q1应该是ds画反了)一个P沟道MOS管,引脚从左到右312依次是dgs。当没插USB供电时,VCC_SYS为0V,3d为VBAT电压4.2V,s电压(假设)为d电压减去Q1寄生二极管分压0.7V,为3.5V,则Q1MOS管导通,设备由电池供电。当插入USB供电时,VCC_SYS为5V,经过二极管D1分压0.3V,电压高于s,则Q1三极管截止,电池不给设备供电,而是由USB供电。D1的作用是防止倒灌,若USB没供电,若无D1,则Q1导通后,s电压VBAT又回到g处VCC_SYS,导致TP4055芯片两个引脚之间短路

Q7电路:若开关断开,则gs电压一致,MOS管截止,SYS无电压,整个设备断电。若开关闭合,g为0V,s为供电电压,MOS管导通,整个设备上电。R2的作用是防止短路,若无R2,则开关闭合,供电电压直接接地,造成短路

电压转换模块:

image

整个系统,5V稳压的作用:给锂电池充电、给PM2.5模块供电、转3.3V。转3.3V不用锂电池的4.2V是因为电池最后放电的时候,电压小于3.3V了。

软件架构方案设计

单片机软件架构设计:分层设计、模块设计、详细设计

一般地先根据需求,确定好硬件,画出整个系统的各个模块,再进行分层设计。分层设计时可以先确定好驱动层,哪些器件驱动需要实现

image

分层设计

应用层:实现业务功能逻辑、策略。比如传感器在屏幕熄灭状态下,工作1min,休息10min

中间件:放一些开源库,比如文件系统FATFS,moduleBus的库

驱动层:各个器件和模块的驱动,比如传感器数据接收、数据发送等

底层:单片机厂商提供的,就是标准库、HAL库这样的

每一层,又都有独立的一些模块

image

模块设计⭐

模块和模块之间的时序、依赖关系

详细设计

模块内部的业务流程、模块对外的API接口函数的原型

代码实现

分为裸机和FreeRTOS两个版本

裸机

手搓一个极为简单的框架

裸机调度框架

函数指针

// main.c
 
int main(void)
{
	Init();
	while(1)
	{
		TaskHandler();
	}
}
 
 
void TaskHandler(void)
{
	// 遍历每个任务
	for(uint8_t i = 1; i < TASK_NUM_MAX; i++)
	{
		if(TaskComps[i].run)				// 判断任务时间片标记
		{
			TaskComps[i].run = 0;			// 标记清零
			TaskComps[i].TaskFuncCb();		// 执行调度任务
		}
	}
}
 
/*
 
// 系统滴答定时器 中断服务函数,每1ms进入一次
// 这里SysTick_Handler(void)只做框架示范用,实际应用时应遵守代码分层规范,在systick.c中调用
void SysTick_Handler(void)
{
	TaskScheduleCb();
}
 
*/
 
 
// Cb:CallBack 因为这个函数在比他低的软件层被调用,所以叫回调函数
// 在定时器中断服务函数中被间接调用,设置时间片标记,需要定时器1ms产生1次中断
void TaskScheduleCb(void)
{
	// 遍历每个任务
	for(uint8_t i = 1; i < TASK_NUM_MAX; i++) 
	{
		if(TaskComps[i].timCount)				// 判断时间片计数
		{
			TaskComps[i].timCount--;		// 时间片计数递减
			if(TaskComps[i].timCount == 0)		// 若时间片到了
			{
				/* 时间片标记为1,并重载计数值 */
				TaskComps[i].TimCount = TaskComps[i].timRload;
				TaskComps[i].run = 1;
			}
		}
	}
}
 
typedef struct
{
	uint8_t run;				// 任务调度标志,1调度,0挂起
	uint16_t timCount;			// 时间片周期,用于递减计数
	uint16_t timRload;			// 时间片周期,用于重载
	void(*pTaskFuncCb)(void); 	// 函数指针,保存任务函数地址 
	// 函数指针,就是用 (*标识符) 代替函数名,剩下照抄,形参名可以不需要
}TaskComps_t;
 
static TaskComps_t TaskComps[] = 
{
	{0, 5, 5, HmiTask},
	/* 把任务的结构体都在这里初始化 */
}
 
#define TASK_NUM_MAX (sizeof(TaskComps) / sizeof(TaskComps[0]))
 
// main.c
// 在应用层,给驱动层注册回调函数,这样驱动层就可以调用应用层的函数了
static void Init(void)
{
	TaskScheduleCbReg(TaskScheduleCb); //
}
 
// systick.c
/*
void TaskSchedule(void) 应该每1ms被调用一次,来进行时间片计数的递减与时间片标记的判断,应该放在systick的1ms中断中执行
但是,systick的1ms中断属于驱动层,而TaskSchedule()属于应用层
按照代码分层的思想,驱动层不应该直接调用驱动层的代码,否则会使分层模糊,增加了不同层之间的耦合度
可以使用回调函数的思想,让驱动层间接调用应用层函数
*/
 
 
 
 
static void (*g_pTaskScheduleFunc)(void);          // 函数指针变量,保存任务调度函数void TaskScheduleCb(void)的地址
 
/**
***********************************************************
* @brief 注册任务调度回调函数
* @param pFunc, 传入回调函数地址
* @return 
***********************************************************
*/
void TaskScheduleCbReg(void (*pFunc)(void))
{
	g_pTaskScheduleFunc = pFunc;
}
 
 
/**
***********************************************************
* @brief 定时中断服务函数,1ms产生一次中断
* @param
* @return 
***********************************************************
*/
void SysTick_Handler(void)
{
	g_sysRunTime++;//系统运行时间递增。和调度框架没关系,可以先忽略
	if (g_pTaskScheduleFunc == NULL) // 防止出现野指针问题
	{
		return;
	}
	g_pTaskScheduleFunc(); // 每1ms调用一次 TaskScheduleCb
}

pm2.5传感器模块

用static修饰函数时,只能在本文件内调用。即使在别的c文件中include头文件,编译也会报错undefined