原理图分析
USB供电
USB_IN是typeC口的5V,F1是自恢复保险丝,防止后级短路损坏USB充电器,两个电容滤除纹波,VCC_SYS为给锂电池充电的电压,R1限流电阻,8050三极管判断是否插入USB充电。8050三极管,V12大于三极管Vbe,则导通,USB_DETECT被拉低,说明USB插入。PC13默认配置为上拉输入。 三极管
锂电池充电
该锂电池,充电电压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,则开关闭合,供电电压直接接地,造成短路
电压转换模块:
整个系统,5V稳压的作用:给锂电池充电、给PM2.5模块供电、转3.3V。转3.3V不用锂电池的4.2V是因为电池最后放电的时候,电压小于3.3V了。
软件架构方案设计
单片机软件架构设计:分层设计、模块设计、详细设计
一般地先根据需求,确定好硬件,画出整个系统的各个模块,再进行分层设计。分层设计时可以先确定好驱动层,哪些器件驱动需要实现
分层设计
应用层:实现业务功能逻辑、策略。比如传感器在屏幕熄灭状态下,工作1min,休息10min
中间件:放一些开源库,比如文件系统FATFS,moduleBus的库
驱动层:各个器件和模块的驱动,比如传感器数据接收、数据发送等
底层:单片机厂商提供的,就是标准库、HAL库这样的
每一层,又都有独立的一些模块
模块设计⭐
模块和模块之间的时序、依赖关系
详细设计
模块内部的业务流程、模块对外的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