TSL1401驱动开发与状态机思想小议
TSL1401
Why TSL1401?
校赛的时候由于连熬几个大夜,赛前忘了调红外阈值,开局跑飞。遂下定决心,扔掉红外,换一种循迹方案。正好老师买了TSL1401CL,于是便有了本文。
驱动时序
这是从TSL1401的DataSheet上截取的时序图:


可以看到,TSL1401的时序还是相对简单的,连续给129个CLK,在起止周期分别给SI脉冲就可以了。但实际上这里有一点坑, 曝光时间是固定的110个周期,如果要做自动曝光的话,就需要调节时钟频率来控制曝光时间。
第一版代码
由于TSL1401的时序并不复杂,我们可以直接用GPIO模拟时序。代码大概是这样:
void TSL1401Initialize(void) {
AverageBrightness = 0;
memset(FrameBuffer, 0, 128);
HAL_SYSTICK_Config(HAL_RCC_GetHCLKFreq() / 1000000);
}
uint16_t TSL1401ReadOut(uint16_t* buffer, uint16_t HalfClockPeriod) {
uint32_t AverageBrightness = 0;
HAL_GPIO_WritePin(TSL_CLK_GPIO_Port, TSL_CLK_Pin, GPIO_PIN_SET);
HAL_GPIO_WritePin(TSL_SI_GPIO_Port, TSL_SI_Pin, GPIO_PIN_RESET);
HAL_Delay(1);
HAL_GPIO_WritePin(TSL_CLK_GPIO_Port, TSL_CLK_Pin, GPIO_PIN_RESET);
HAL_GPIO_WritePin(TSL_SI_GPIO_Port, TSL_SI_Pin, GPIO_PIN_SET);
HAL_Delay(1);
HAL_GPIO_WritePin(TSL_CLK_GPIO_Port, TSL_CLK_Pin, GPIO_PIN_SET);
HAL_GPIO_WritePin(TSL_SI_GPIO_Port, TSL_SI_Pin, GPIO_PIN_RESET);
for(uint8_t i = 0; i < 128; i++) {
HAL_GPIO_WritePin(TSL_CLK_GPIO_Port, TSL_CLK_Pin, GPIO_PIN_RESET);
HAL_ADC_PollForConversion(&TSL_ADC_HANDLE, HAL_MAX_DELAY);
buffer[i] = HAL_ADC_GetValue(&TSL_ADC_HANDLE);
AverageBrightness = AverageBrightness + buffer[i];
HAL_Delay(HalfClockPeriod);
HAL_GPIO_WritePin(TSL_CLK_GPIO_Port, TSL_CLK_Pin, GPIO_PIN_SET);
}
AverageBrightness = AverageBrightness / 128;
return AverageBrightness;
}
void TSL1401AutoExposure(void) {
uint16_t avg = 0;
uint16_t frameBuffer[128];
uint16_t halfClockPeriod = 90;
while ((avg < 1000 && halfClockPeriod < 500) || (avg > 3500 && halfClockPeriod > 5)) {
avg = TSL1401ReadOut(frameBuffer);
if (avg < 1000)
halfClockPeriod = halfClockPeriod + 5;
else if (avg > 3500)
halfClockPeriod = halfClockPeriod - 5;
}
}
写起来非常的轻松愉快,可谓是一气呵成。但就在我写完的一瞬间,我意识到了一个问题:这个程序是阻塞的。这也就意味着这个程序在执行的时候将把整个程序卡住。这玩意可是要整合上PID + 循迹来调小车的,这要是卡个100mS(最大曝光时间)我的PID不得飞到天上去,还是得重写一版非阻塞的驱动。
非阻塞版驱动
API设计
考虑到是非阻塞式的API,最简单的实现方式就是采用观察者模式,即当读取完成时通知。而在MCU裸机编程中,最简单的方式就是Callback,于是我们有了第一个API:
TSL1401ReadCpltCallback()
那么参数和返回值该如何定义呢?Callback将由驱动调用,返回值在这个驱动中似乎是不必要的。而我们希望驱动能将读取完的FrameBuffer传递给外界,于是就定义成这样:
void TSL1401ReadCpltCallback(uint16_t* frameBuffer);
同时,为了在默认情况下过编译,定义一个weak的实现:
__attribute__((weak)) void TSL1401ReadCpltCallback(uint16_t* frameBuffer) {
}
其实这里还塞了其他的东西,暂且按下不表。
由于这里还涉及了ADC读取的问题,既然都非阻塞了,那就全部非阻塞好了,ADC配置成中断模式,当ADC读取完成的时候通知一下驱动就好了,于是我们再设计一个回调服务函数,在ADC读取完成回调中调用:
void __ADCCallbackService_TSL1401(ADC_HandleTypeDef* hadc);
```
剩下的部分其实就比较常规了,无非是功能的启用禁用、曝光时间修改、触发单次曝光和初始化。依然由于是非阻塞的关系,我们需要做一个循环被调用的函数,不妨按OS编程的叫法,叫他Service,于是整体的API就出来了,大概是这样:
```c
void TSL1401ReadCpltCallback(uint16_t* frameBuffer);
void __ADCCallbackService_TSL1401(ADC_HandleTypeDef* hadc);
void TSL1401Initialize(void);
void TSL1401EnableAutoExposure(void);
void TSL1401DisableAutoExposure(void);
void TSL1401EnableContinouosExposure(void);
void TSL1401DisableContinouosExposure(void);
void TSL1401SetExposureTime(uint32_t exposureTime_uS);
void TSL1401BurstReadAsync(void);
void TSL1401ServiceFunction(void);
注意我给回调服务函数加了两个下划线作为前缀,这是笔者的代码习惯,通常只希望在内部或某个特定地点调用的函数或是变量,笔者都会加上这个前缀。
驱动实现
笔者习惯于将一个c文件内部的变量pack成结构体(面向对象后遗症属于是),先把变量定义下:
struct {
uint8_t NextBurstReadFlag:1;
uint8_t AutoExposureEnable:1;
uint8_t AdcDataAvailableFlag:1;
uint8_t ContinuousExposureEnable:1;
uint8_t ExposureAndReadOutState:4;
uint16_t HalfClockPeroid;
uint8_t ClockCycleCounter;
uint32_t AverageBrightness;
uint32_t OperationStartTick;
} __TSL1401DriverVariablesPack = { 0 };
uint16_t __TSL1401FrameBuffer[128] = { 0 };
然后把只需要动配置的函数都实现了:
__attribute__((weak)) void TSL1401ReadCpltCallback(uint16_t* frameBuffer) {
}
void __ADCCallbackService_TSL1401(ADC_HandleTypeDef *hadc) {
if (hadc->Instance = TSL_ADC.Instance)
__TSL1401DriverVariablesPack.AdcDataAvailableFlag = 1;
}
void TSL1401Initialize(void) {
__TSL1401DriverVariablesPack.NextBurstReadFlag = 0;
__TSL1401DriverVariablesPack.AutoExposureEnable = 1;
__TSL1401DriverVariablesPack.AdcDataAvailableFlag = 0;
__TSL1401DriverVariablesPack.ContinouosExposureEnable = 1;
__TSL1401DriverVariablesPack.ExposureAndReadOutState = 0;
__TSL1401DriverVariablesPack.HalfClockPeriod = 90;
__TSL1401DriverVariablesPack.ClockCycleCounter = 0;
__TSL1401DriverVariablesPack.AverageBrightness = 0;
__TSL1401DriverVariablesPack.OperationStartTick = 0;
HAL_SYSTICK_Config(HAL_RCC_GetHCLK() / 1000000);
}
void TSL1401EnableAutoExposure(void) { __TSL1401DriverVariablesPack.AutoExposureEnable = 1; }
void TSL1401DisableAutoExposure(void) { __TSL1401DriverVariablesPack.AutoExposureEnable = 0; }
void TSL1401EnableContinouosExposure(void) { __TSL1401DriverVariablesPack.ContinouosExposureEnable = 1; }
void TSL1401DisableContinouosExposure(void) { __TSL1401DriverVariablesPack.ContinouosExposureEnable = 0; }
void TSL1401SetExposureTime(uint32_t exposureTime_uS) { __TSL1401DriverVariablesPack.HalfClockPeriod = exposureTime_uS / 220; }
void TSL1401BurstReadAsync(void) {
if (__TSL1401DriverVariablesPack.ContinouosExposureEnable)
return;
__TSL1401DriverVariablesPack.NextBurstReadFlag = 1;
}
值得注意的是笔者将SysTick配置到了1uS来提供时钟源(因为小车上的定时器不够用了),这将导致HAL_Delay()的延时单位从mS变成uS,如果需要HAL_Delay()的读者需要自己配置一个1uS的tick来提供时钟源。
接下来就是重头戏——Service的编写。其实从__TSL1401DriverVariablesPack的定义中就能看出我的意图——状态机。对着时序图切分状态,我们不妨定义一个枚举类型:
typedef enum {
SIPulseGenerate_ClkHighSILow_E = 0x0,
SIPulseGenerate_ClkLowSIHigh_E = 0x1,
ReadOut_ClkLowAdcStart_E = 0x2,
ReadOut_ClkLowAdcConvCplt_E = 0x3,
ReadOut_ClkHigh_E = 0x4,
ReadOut_AllDone_E = 0x5,
Idle_E = 0xF
} ExposureAndReadOutState_E;
然后把__TSL1401DriverVariablesPack改写成这样:
struct {
uint8_t NextBurstReadFlag:1;
uint8_t AutoExposureEnable:1;
uint8_t AdcDataAvailableFlag:1;
uint8_t ContinuousExposureEnable:1;
uint8_t reservedBits:4;
uint16_t HalfClockPeroid;
uint8_t ClockCycleCounter;
uint32_t AverageBrightness;
uint32_t OperationStartTick;
ExposureAndReadOutState_E ExposureAndReadOutState;
} __TSL1401DriverVariablesPack = { 0 };
同时改写下初始化:
void TSL1401Initialize(void) {
__TSL1401DriverVariablesPack.NextBurstReadFlag = 0;
__TSL1401DriverVariablesPack.AutoExposureEnable = 1;
__TSL1401DriverVariablesPack.AdcDataAvailableFlag = 0;
__TSL1401DriverVariablesPack.ContinouosExposureEnable = 1;
__TSL1401DriverVariablesPack.ExposureAndReadOutState = SIPulseGenerate_ClkHighSILow_E;
__TSL1401DriverVariablesPack.HalfClockPeriod = 90;
__TSL1401DriverVariablesPack.ClockCycleCounter = 0;
__TSL1401DriverVariablesPack.AverageBrightness = 0;
__TSL1401DriverVariablesPack.OperationStartTick = 0;
HAL_SYSTICK_Config(HAL_RCC_GetHCLK() / 1000000);
}
然后开始对着时序图画一个状态机:

接下来对着状态机写出函数即可:
void TSL1401ServiceFunction(void) {
switch(__TSL1401DriverVariablesPack.ExposureAndReadOutState) {
case SIPulseGenerate_ClkHighSILow_E:
if(__TSL1401DriverVariablesPack.ClockCycleCounter == 0) {
if(HAL_GetTick() > __TSL1401DriverVariablesPack.OperationStartick + __TSL1401DriverVariablesPack.HalfClockPeriod){
HAL_GPIO_WritePin(TSL_CLK_GPIP_Port, TSL_CLK_Pin, GPIO_PIN_SET);
HAL_GPIO_WritePin(TSL_SI_GPIO_Port, TSL_SI_Pin, GPIO_PIN_RESET);
__TSL1401DriverVariablesPack.OperationStartick = HAL_GetTick();
__TSL1401DriverVariablesPack.ExposureAndReadOutState = SIPulseGenerate_ClkLowSIHigh_E;
}
} else {
__TSL1401DriverVariablesPack.ExposureAndReadOutState = ReadOut_ClkLowAdcStart_E;
}
break;
case SIPulseGenerate_ClkLowSIHigh_E:
if(HAL_GetTick() > __TSL1401DriverVariablesPack.OperationStartick + __TSL1401DriverVariablesPack.HalfClockPeriod) {
HAL_GPIO_WritePin(TSL_CLK_GPIP_Port, TSL_CLK_Pin, GPIO_PIN_RESET);
HAL_GPIO_WritePin(TSL_SI_GPIO_Port, TSL_SI_Pin, GPIO_PIN_SET);
__TSL1401DriverVariablesPack.ClockCycleCounter += 1;
__TSL1401DriverVariablesPack.OperationStartick = HAL_GetTick();
__TSL1401DriverVariablesPack = SIPulseGenerate_ClkHighSILow_E;
}
break;
case ReadOut_ClkLowAdcStart_E:
if(__TSL1401DriverVariablesPack.ClockCycleCounter < 129) {
if(HAL_GetTick() > __TSL1401DriverVariablesPack.OperationStartick + __TSL1401DriverVariablesPack.HalfClockPeriod) {
HAL_GPIO_WritePin(TSL_CLK_GPIP_Port, TSL_CLK_Pin, GPIO_PIN_RESET);
__TSL1401DriverVariablesPack.AdcDataAvailableFlag = 0;
__TSL1401DriverVariablesPack.OperationStartTick = HAL_GetTick();
HAL_ADC_Start_IT(&TSL_ADC);
__TSL1401DriverVariablesPack.ExposureAndReadOutState = ReadOut_ClkLowAdcConvCplt_E;
}
} else {
__TSL1401DriverVariablesPack.ExposureAndReadOutState = ReadOut_AllDone_E;
}
break;
case ReadOut_ClkLowAdcConvCplt_E:
__TSL1401DriverVariablesPack.AverageBrightness += HAL_ADC_GetValue(&TSL_ADC);
__TSL1401FrameBuffer[__TSL1401DriverVariablesPack.ClockCycleCounter - 1] = HAL_ADC_GetValue(&TSL_ADC);
__TSL1401DriverVariablesPack.ExposureAndReadOutState = ReadOut_ClkHigh_E;
break;
case ReadOut_ClkHigh_E:
if(HAL_GetTick() > __TSL1401DriverVariablesPack.OperationStartick + __TSL1401DriverVariablesPack.HalfClockPeriod) {
HAL_GPIO_WritePin(TSL_CLK_GPIP_Port, TSL_CLK_Pin, GPIO_PIN_SET);
__TSL1401DriverVariablesPack.OperationStartTick = HAL_GetTick();
__TSL1401DriverVariablesPack.ExposureAndReadOutState = ReadOut_ClkLowAdcStart_E;
}
break;
case ReadOut_AllDone_E:
if(__TSL1401DriverVariablesPack.AutoExposureEnable) {
if(__TSL1401DriverVariablesPack.AverageBrightness < 1000 && __TSL1401DriverVariablesPack.HalfClockPeriod < 1000) {
__TSL1401DriverVariablesPack.HalfClockPeriod += 50;
__TSL1401DriverVariablesPack.ExposureAndReadOutState = SIPulseGenerate_ClkHighSILow_E;
} else if (__TSL1401DriverVariablesPack.AverageBrightness > 3500 && __TSL1401DriverVariablesPack.HalfClockPeriod > 100) {
__TSL1401DriverVariablesPack.HalfClockPeriod -= 50;
__TSL1401DriverVariablesPack.ExposureAndReadOutState = SIPulseGenerate_ClkHighSILow_E;
} else {
TSL1401ReadCpltCallback(__TSL1401FrameBuffer);
if(__TSL1401DriverVariablesPack.ContinouosExposureEnable) {
__TSL1401DriverVariablesPack.ExposureAndReadOutState = SIPulseGenerate_ClkHighSILow_E;
} else {
__TSL1401DriverVariablesPack.ExposureAndReadOutState = Idle_E;
}
}
} else {
if(__TSL1401DriverVariablesPack.ContinouosExposureEnable) {
__TSL1401DriverVariablesPack.ExposureAndReadOutState = SIPulseGenerate_ClkHighSILow_E;
} else {
__TSL1401DriverVariablesPack.ExposureAndReadOutState = Idle_E;
}
}
break;
case Idle_E:
if(__TSL1401DriverVariablesPack.NextBurstReadFlag) {
__TSL1401DriverVariablesPack.NextBurstReadFlag = 0;
__TSL1401DriverVariablesPack.ExposureAndReadOutState = SIPulseGenerate_ClkHighSILow_E;
}
break;
}
}
至此,我们的TSL1401驱动算是编写完毕了,进入接下来的话题——状态机。
状态机编程
什么是状态机
状态机是对于事务运行规则的一种抽象,对于状态机而言,有四大基础概念
- 状态:即系统的状态
- 事件:执行某个操作所需要的触发条件
- 动作:事件发生后执行的操作
- 转移:从一个状态切换到另一个状态
以上面的TSL1401驱动为例,转移图中的判断就是“事件”,而状态转移和Flag、配置变量就是“动作”。
事实上,在FPGA中使用状态机编程更多,而这里之所以用了状态机,是因为我们希望整个系统是异步的,而使用了Visitor设计模式。在Visitor设计模式中,操作完成会触发一个“事件”,为了管理这种“事件”触发的“动作”,我们便需要引入状态机。
Why State Machine?
前文提到,要异步就得状态机,所以为什么选择状态机可以换一个问法,为什么用异步编程?其实说来说去也无非一句话——最大化执行效率。如果使用传统的同步(Synchronous)编程,IO方法事实上是阻塞的,这也就意味着,在等待IO操作完成的过程之中,CPU是空转的,效率非常低下。事实上,在新版本的Cpp标准(C++ 20)里引入了co_await和co_async关键字,可以简单的实现异步操作,然而嵌入式编译器一般跟进标准非常慢....并且在嵌入式里使用C++并不是什么明智的决定(会带来更大的RAM和ROM资源开销且g++等c++编译器的行为并不稳定),所以在大部分时候我们还是倾向于使用状态机。