一、前言
本文用于在实时操作系统中使用软件IIC对系统的占用分析,通过“定时器中断调度软件IIC”和“任务调度软件IIC”,两个调度方案来分析对系统占用的优劣势。
二、系统参数介绍
2.1、硬件介绍
- 使用的开发板是正点原子开发板(精英版)STM32ZET6,使用的IIC设备是正点原子配套的触摸屏(4.3寸)上的触摸IC(GT9147)。
- 程序下载调试使用的是J-Link设备(系统分析必须的),不能使用ST-Link进行替代。
##### 2.2、软件介绍
- 操作系统:FreeRTOS V10.4.6
- 系统分析工具:SystemView V354,文件中包含了上位机分析软件和RTT通信的相关代码(需要移植到FreeRTOS)中,具体如何下载、移植、使用上位机就不在此详细讲述了(默认已会)。
- J-Link:对应的软件驱动版本可参考我的来使用,因为有些驱动版本有可能使用不了SystemView的上位机软件。
三、两种方案代码对比
3.1、任务函数
代码中包含了两种方案的执行代码;
- 第一个是定时器中断调度软件IIC方案的,不执行软件IIC时序变化的动作,只是用于触发定时器中断;
- 第二个是任务调度软件IIC方案的,执行软件IIC时序变化的动作,直接在任务中读取触摸数据。
void task_sensor(void *para)
{
while(1)
{
#if IICTIME_ENABLE /* 定时器中断调度软件IIC 使能位 */
tp_dev.scan(0);
vTaskDelay(20);
#else
tp_dev.scan(0);
vTaskDelay(10);
#endif
}
}
3.2、软件IIC读写接口(任务中调度软件IIC)
这是软件IIC的读写字节的接口,也就是常用的阻塞式的方式实现的,使用的正点原子例程中的软件IIC源码,未进行修改过。
//IIC发送一个字节
void CT_IIC_Send_Byte(u8 txd)
{
u8 t;
CT_SDA_OUT();
CT_IIC_SCL=0;//拉低时钟开始数据传输
CT_Delay();
for(t=0;t<8;t++)
{
CT_IIC_SDA=(txd&0x80)>>7;
txd<<=1;
CT_IIC_SCL=1;
CT_Delay();
CT_IIC_SCL=0;
CT_Delay();
}
}
//读1个字节
u8 CT_IIC_Read_Byte(unsigned char ack)
{
u8 i,receive=0;
CT_SDA_IN();//SDA设置为输入
delay_us(30);
for(i=0;i<8;i++ )
{
CT_IIC_SCL=0;
CT_Delay();
CT_IIC_SCL=1;
receive<<=1;
if(CT_READ_SDA)receive++;
}
if (!ack)CT_IIC_NAck();//发送nACK
else CT_IIC_Ack(); //发送ACK
return receive;
}
3.3、触摸数据读取接口(任务中调度软件IIC)
这个是读取触摸数据的接口,从正点原子的触摸例程中修改而来,将多点触摸扫描检测,修改成单点触摸检测,方便后续分析。
u8 GT9147_Scan(u8 mode)
{
u8 buf[4];
u8 res = 0;
u8 temp;
GT9147_RD_Reg(GT_GSTID_REG, &mode, 1); //读取触摸点的状态
if (mode & 0X80 && ((mode & 0XF) < 6))
{
temp = 0;
GT9147_WR_Reg(GT_GSTID_REG, &temp, 1); //清标志
}
if ((mode & 0XF) && ((mode & 0XF) < 6))
{
temp = 0XFF << (mode & 0XF); //将点的个数转换为1的位数,匹配tp_dev.sta定义
tp_dev.sta = (~temp) | TP_PRES_DOWN | TP_CATH_PRES;
if (tp_dev.sta & (1 << 0)) //触摸有效?
{
GT9147_RD_Reg(GT9147_TPX_TBL[0], buf, 4); //读取XY坐标值
if (tp_dev.touchtype & 0X01) //横屏
{
tp_dev.y[0] = ((u16)buf[1] << 8) + buf[0];
tp_dev.x[0] = 800 - (((u16)buf[3] << 8) + buf[2]);
}
else
{
tp_dev.x[0] = ((u16)buf[1] << 8) + buf[0];
tp_dev.y[0] = ((u16)buf[3] << 8) + buf[2];
}
// printf("x:%d,y:%d\r\n",tp_dev.x[0],tp_dev.y[0]);
}
res = 1;
}
else if ((mode & 0X8F) == 0X80) //无触摸点按下
{
if (tp_dev.sta & TP_PRES_DOWN) //之前是被按下的
{
tp_dev.sta &= ~(1 << 7); //标记按键松开
}
else //之前就没有被按下
{
tp_dev.x[0] = 0xffff;
tp_dev.y[0] = 0xffff;
tp_dev.sta &= 0XE0; //清除点有效标记
}
}
return res;
}
3.4、软件IIC读写接口(定时器中断调度软件IIC)
这个是IIC读写数据的接口,将IIC变化的时序通过状态机的方法去实现,然后使用定时中断进行调度执行。
// 读取数据
I2c_StateValue_t dI2c_ReadData(I2c_Struct_t *I2c_Device, uint16_t reg, uint8_t *buf, uint8_t len)
{
I2c_StateValue_t state = I2c_OK;
switch(I2c_Device->StateMachine_One)
{
// 启动信号
case I2c_StateOne:
{
state = dI2c_Start(I2c_Device);
if(state == I2c_OK) /* 信号已完成 */
{
I2c_Device->StateMachine_One = I2c_StateTwo; /* 状态转移 */
}
break;
}
// 发送写命令
case I2c_StateTwo:
{
state = dI2c_SendByte(I2c_Device, I2c_Device->Write_Cmd); //发送写命令
if(state == I2c_OK) /* 信号已完成 */
{
I2c_Device->StateMachine_One = I2c_StateThr; /* 状态转移 */
}
break;
}
// 等待ack
case I2c_StateThr:
{
state = dI2c_WaitAck(I2c_Device);
if(state == I2c_Error) /* 失败,直接转移到停止信号 */
{
I2c_Device->StateMachine_One = I2c_StateThirt; /* 状态转移 */
}
else if(state == I2c_OK) /* 成功,进入下一步 */
{
I2c_Device->StateMachine_One = I2c_StateFou;
}
break;
}
.......展示部分代码
// 写入数据
I2c_StateValue_t dI2c_WriteData(I2c_Struct_t *I2c_Device, uint16_t reg, uint8_t *buf, uint8_t len)
{
I2c_StateValue_t state = I2c_OK;
switch(I2c_Device->StateMachine_One)
{
// 启动信号
case I2c_StateOne:
{
state = dI2c_Start(I2c_Device);
if(state == I2c_OK) /* 信号已完成 */
{
I2c_Device->StateMachine_One = I2c_StateTwo; /* 状态转移 */
}
break;
}
// 发送写命令
case I2c_StateTwo:
{
state = dI2c_SendByte(I2c_Device, I2c_Device->Write_Cmd); //发送写命令
if(state == I2c_OK) /* 信号已完成 */
{
I2c_Device->StateMachine_One = I2c_StateThr; /* 状态转移 */
}
break;
}
// 等待ack
case I2c_StateThr:
{
state = dI2c_WaitAck(I2c_Device);
if(state == I2c_Error) /* 失败,直接转移到停止信号 */
{
I2c_Device->StateMachine_One = I2c_StateTen; /* 状态转移 */
}
else if(state == I2c_OK) /* 成功,进入下一步 */
{
I2c_Device->StateMachine_One = I2c_StateFou;
}
break;
}
......展示部分代码
3.5、触摸数据读取接口(定时器中断调度软件IIC)
这个是读取触摸数据的接口,此函数是在定时器中断中去执行的,当数据读取一次完成后,会自动关闭定时器中断。
/*I2C设备接口*/
I2c_Struct_t I2c_Touch_Device = {
.Sda_Port = TOUCH_SDA_PORT,
.Sda_Pin = TOUCH_SDA_PIN,
.Scl_Port = TOUCH_SCL_PORT,
.Scl_Pin = TOUCH_SCL_PIN,
.Read_Cmd = TOUCH_CMD_RD,
.Write_Cmd = TOUCH_CMD_WR,
.I2c_RW_CallBack = &dTouch_Get_Data,
};
/* 获取触摸数据 */
I2c_StateValue_t dTouch_Get_Data(void)
{
I2c_StateValue_t state;
dTouch_Data.Touch_State = false;
switch (dTouch_Data.StateMachine)
{
case I2c_StateOne:
{
state = dI2c_ReadData(&I2c_Touch_Device, GT_GSTID_REG, &dTouch_Data.mode, 1); //读取数据有效
if (state == I2c_Finish) /* 信号已完成 */
{
dTouch_Data.StateMachine = I2c_StateTwo;
}
break;
}
case I2c_StateTwo:
{
if (dTouch_Data.mode & 0X80 && ((dTouch_Data.mode & 0XF) < 6))
{
dTouch_Data.buf[0] = 0;
dTouch_Data.buf[1] = 0;
dTouch_Data.buf[2] = 0;
dTouch_Data.buf[3] = 0;
state = dI2c_WriteData(&I2c_Touch_Device, GT_GSTID_REG, dTouch_Data.buf, 1); //清除状态
if (state == I2c_Finish) /* 信号已完成 */
{
dTouch_Data.StateMachine = I2c_StateThr; /* 状态转移 */
}
}
else
{
dTouch_Data.StateMachine = I2c_StateThr; /* 状态转移 */
}
break;
}
......展示部分代码
四、方案详解
- SystemView上位机中的名称信息解析:
- SysTick:是RTOS的心跳,设置的是1ms的中断周期;
- Time_I2c:定时器中断任务信息(用于调度软件IIC使用),默认设置的是50us的中断周期(这里是未开启的);
- Scheduler:是RTOS调度器的信息;
- Sensor_Task:检测任务的信息,用于检测触摸数据、触发定时器中断,也是此处实验中唯一手动创建的任务;
- Tmr_Svc:RTOS默认创建的定时器任务信息(没有使用不用管);
- Idle:空闲任务的信息。
4.1、任务调度软件IIC方案分析
- 在任务中20ms调度读取触摸数据的信息,SystemView显示的任务调度情况:
- SystemView上位机中显示的 Sensor_Task 任务信息,执行一次读取触摸数据的接口,需要的时间大约是5ms,也就是说单执行这一个接口就需要占用系统的20%的时间,而且这个过程中是无法执行其它任务的。
- 在任务中10ms调度读取触摸数据的接口,SystemView显示的任务调度情况:
- 将任务调度速度调快,其它不变,可以看到在 Sensor_Task 任务信息中显示,对系统的占用率越来越高,达到了33.57%。
- 在任务中2ms调度读取触摸数据的接口,SystemView显示的任务调度情况:
- 再次提高任务的调度速度,其它保持不变,可以看到对系统的占有率再次提高了,达到了71.94%。
- 任务占用率计算公式:
- 单次运行周期时间 = 任务调度时间 + 单次触摸程序运行时间;
- 1s内调度的触摸接口次数 = 1000ms / 单次运行周期时间;
- 1s内触摸程序占用时间 = 1s内调度的触摸接口次数 * 单次程序运行时间;
- 换算成百分比:1s内触摸程序占用时间 / 1000 * 100;
任务调度时间(ms) | 单次触摸程序运行时间(ms) | 1s内调度的触摸接口次数(次) | 触摸任务占用率(%) | 空闲任务占有率(%) | |
---|---|---|---|---|---|
1 | 20 | 5 | 40 | 20 | 80 |
2 | 10 | 5 | 66.6 | 33.3 | 66.7 |
3 | 2 | 5 | 142.8 | 71.4 | 28.6 |
根据上方表格分析,可以看出在任务中调度触摸接口的次数越快、执行的接口函数需要的时间越久,就对系统的占有率就越高;
若随着不同的IIC设备加入到系统中来,只会让同等优先级或更低优先级的任务更加卡顿,让系统做大量无用功;
如果系统中只有个别IIC设备(使用软件IIC),且调度次数少、执行速度快、优先级低(温湿度之类的设备),加入到系统中还是无伤大雅的。
4.2、定时器中断调度软件IIC方案分析
定时器中断调度软件IIC的方法,替代了软件IIC中的延时部分;任务调度一次,便开启定时器中断功能,定时器中断调度周期为10us(后面再讲解为什么要用10us作为定时器中断调度周期),触摸数据读取成功一次,就自动关闭定时中断。
- 定时器被任务每触发一次,定时器中断需要执行大约230次才会关闭定时器中断运行,通过SystemView进行统计的;
- 定时器单次中断运行时间:也是通过SystemView进行统计的,取一段时间内的最大最小值的平均值为中断运行时间 (0.006069ms + 0.011625ms) / 2 = 0.008847ms;
- 读取一次触摸数据需要的时间:中断执行次数(230次) * (中断周期(10us) + 中断运行时间(8.8us)) = 4.324ms(非阻塞的),所以在任务中的调度时间需要大于4.324ms才能读取到有效数据。
- 在任务中20ms调度触发定时器中断,开启10us定时器中断,SystemView显示的任务调度情况:
- 已知在任务中 20ms 触发一次定时器,1s内能触发的次数为 1000ms / 20ms = 50次 (任务自身运行时间较少,已忽略不计);
- 任务触发中断一次,定时器中断运行的总时间:中断执行次数230次 * 定时器单次中断运行时间8.8us = 2.024ms;
- 计算1s内中断占用的系统资源:
- 1s内中断占用的时间:定时器中断触发次数50次 * 定时器中断运行的总时间2.024ms = 101.2ms;
- 换算成百分比:101.2ms / 1000 * 100 = 10.12% (实际9.72%,因为有些值是估算出来的,所以和实际的会有些误差)。
- 在任务中10ms调度,开启50us定时器中断,SystemView显示的任务调度情况:
- 加快任务的调度触发时间,其它参数保持不变,所以改变的值只有:
- 1s内能触发的定时器中断次数为:1000ms / 10ms = 100次 (任务自身运行时间较少,已忽略不计);
- 1s内中断占用的时间:定时器中断触发次数100次 * 定时器中断运行的总时间2.024ms = 202.4ms;
- 换算成百分比:202.4ms / 1000 * 100 = 20.24% (实际19.42%,因为有些值是估算出来的,所以和实际的会有些误差)。
- 加快任务的调度触发时间,其它参数保持不变,所以改变的值只有:
- 忽略步骤,直接看结果:中断占有率 = 1s内任务触发中断的次数 * 定时器中断运行的总时间
任务调度时间(ms) | 中断周期(us) | 定时器中断运行的总时间(ms) | 1s内任务触发中断的次数(次) | 中断被触发执行的次数(次) | 中断占有率(%) | |
---|---|---|---|---|---|---|
1 | 20 | 10 | 2.024 | 50 | 230 | 10.12 |
2 | 10 | 10 | 2.024 | 100 | 230 | 20.24 |
4.3、如何选择定时器中断周期
- 先贴一张时序图(网上找的),信号时长都需要大于4us,所以在无特殊需求的情况下,保证定时器中断周期 >= 5us。
-
选择定时器中断周期:
-
测试条件:检测任务调度周期为10ms,定时器周期设置成6us;新建一个UI任务中,在UI任务中添加延时函数,用以模拟其它工作正在运行的状态,UI任务调度周期为5ms;
/* 检测任务(低优先级) */ void task_sensor(void *para) { while(1) { tp_dev.scan(0); vTaskDelay(10); } } /* UI任务(高优先级) */ void task_ui(void *para) { while(1) { delay_us(50); //增加50us阻塞时间,模拟其它工作在运行的状态 vTaskDelay(5); } }
-
先看看SystemView视图显示的结果如何:
可以看出 SystemView视图显示,定时器中断的周期时间无论是10us或是2us,在相同任务调度周期下,占用的系统资源都是一样的,并不会因为任务的多少或者任务优先级的高低而影响占用的系统资源,所以只需要保证定时器中断周期的时间能够满足时序时间要求即可。
五、总结
- 任务触发定时器中断调度IIC的方案,只需要保证 任务调度时间大于一次数据获取的时长即可、定时器中断周期能满足时序要求即可;
- 软件IIC的调度时序还可以再优化,留待后续版本更新再去优化吧;
- 软件IIC的注册接口是仿照Linux驱动的接口风格去设计的(虽然仿的很差),后续再增加一些IIC时序变化的回调接口吧,用于兼容其它设备,这个也留待后续再去更新吧;
- 再贴一个gitee的地址
git clone https://gitee.com/pkq746/soft-i2c_-system-view.git
,方便拉取(代码是网上下载的例程修改来的),第一次写博客,望大家多多指导。