Statement:延续上篇四路PWM波驱动电机的文章。
一、MSP432E401Y介绍
关于这款MCU的基础介绍,我在前面的《MSP432E401Y——4路PWM波输出驱动小车》文章中已经介绍了,需要的小伙伴可以查看。
二、线性CCD循迹
普通的循迹模块采用的是一对红外对管检测黑线,检测时的灵敏度精度不够,有时候需要多个循迹模块配合才能更好的去循迹。而线性CCD摄像头,相对与普通的循迹模块具有检测准确率高。
采集数据时要使用到8位的ADC,我用的ADC0的通道0,采用软件触发方式,及每一次采集数据时,触发ADC进行数据采集。ADC初始化及开始转换代码
void ADC_Init(void)
{
/* Enable the clock to GPIO Port E and wait for it to be ready */
SysCtlPeripheralEnable(SYSCTL_PERIPH_GPIOE);
while(!(SysCtlPeripheralReady(SYSCTL_PERIPH_GPIOE)))
{
}
/* Configure PE3 as ADC input channel */
GPIOPinTypeADC(GPIO_PORTE_BASE, GPIO_PIN_3);
/* Enable the clock to ADC-0 and wait for it to be ready */
SysCtlPeripheralEnable(SYSCTL_PERIPH_ADC0);
while(!(SysCtlPeripheralReady(SYSCTL_PERIPH_ADC0)))
{
}
/* Configure Sequencer 2 to sample the analog channel : AIN0-AIN3. The
* end of conversion and interrupt generation is set for AIN3 */
ADCSequenceStepConfigure(ADC0_BASE,2, 0, ADC_CTL_CH0 | ADC_CTL_IE |
ADC_CTL_END);
/* Enable sample sequence 2 with a processor signal trigger. Sequencer 2
* will do a single sample when the processor sends a signal to start the
* conversion */
//ADCSequenceConfigure(ADC0_BASE,2, ADC_TRIGGER_TIMER, 0);
/* Since sample sequence 2 is now configured, it must be enabled. */
ADCSequenceEnable(ADC0_BASE,2);
/* Clear the interrupt status flag. This is done to make sure the
* interrupt flag is cleared before we sample. */
ADCIntClear(ADC0_BASE,2);
ADCIntEnable(ADC0_BASE, 2);
/* Enable the Interrupt generation from the ADC-0 Sequencer */
IntEnable(INT_ADC0SS2);
}
uint32_t ADC_Start(void) //ADC开始转换
{
//uint8_t trmp[4]={0x55,0x55,0x55,0x55};
/* Trigger the ADC conversion. */
ADCProcessorTrigger(ADC0_BASE,2);
/* Wait for conversion to be completed. */
while(!ADCIntStatus(ADC0_BASE,2, false))
//delay(500000);
/* Clear the ADC interrupt flag. */
ADCIntClear(ADC0_BASE,2);
/* Read ADC Value. */
ADCSequenceDataGet(ADC0_BASE,2, getADCValue);
// trmp[0]=getADCValue[0]/1000+0x30;
// trmp[1]=((getADCValue[0]%1000)/100)+0x30;
// trmp[2]=((getADCValue[0]%100)/10)+0x30;
// trmp[3]=(getADCValue[0]%10)+0x30;
// UARTSend(trmp,4);
//UARTCharPut(UART4_BASE,getADCValue[0]); //测试
return (getADCValue[0]);
}
引脚说明:
GDN | 供电引脚,接地 |
VCC | 供电引脚,接DC 3.3v~5v |
AO | 模拟信号输出 |
CLK | 工作时钟线 |
SI | 模块工作片选线,数字电平1,表示工作,0表示不工作 |
工作原理:
线性CCD内部是一行光电二级管(光电二级管具有抗干扰能力强的优点)构成的,共128个,每个光电二级管后面都有一个独立的积分电路,用于存储检测到像素点的电荷信号。存储的电荷越多表示那个点光强越强,通过模块的工作引脚控制CCD是否采集一次数据(一次128个像素点)。工作时,需要严格的时序。
以上是线性CCD的时序图,通过软件模拟工作的时钟信号,通过拉高SI端一定的时间(10us左右),来控制模块工作,注意该端口的高点平信号必须要在下一个时钟信号的上升沿置为低电平,通过SI的上升沿的位置,各自的积分器断开,在18个周期信号内,是不积分的,18周期信号过后开始进行积分,后面通过SI移位寄存器计时,存储在电容上的电荷连接到放大器,通过模拟引脚输出模拟电压。每次读出128个像素点的数据。
void RD_TSL(void)
{
uint8_t i=0,tslp=0;
TSL_CLK_H;
TSL_SI_L;
Dly_us();
TSL_SI_H;
TSL_CLK_L;
Dly_us();
TSL_CLK_H;
TSL_SI_L;
Dly_us();
for(i=0;i<128;i++) //读取128个像素点电压值
{
TSL_CLK_L;
Dly_us(); //调节曝光时间
Dly_us();
ADV[tslp]=(ADC_Start())>>4;
++tslp;
TSL_CLK_H;
Dly_us();
}
}
获取中值,通过比较黑线的位置在中线偏离的那边,从而调整车子的转向,这个地方的算法有点不太好理解,(举一个简单的例子,我们不是采集的128个像素点嘛,那么此时64的位置就是中线的位置,假设现在的小车偏离右边了,那么此时中值就会小于64,此时我们就需要控制车子左转一些调节车子前进的方向)。其他的情况同理可得。附上计算中值的代码。
void Find_CCD_Zhongzhi(void)
{
static uint16_t i,j,Left,Right;
static uint16_t value1_max,value1_min;
value1_max=ADV[0]; //动态阈值算法,读取最大和最小值
for(i=5;i<123;i++) //两边各去掉5个点
{
if(value1_max<=ADV[i])
value1_max=ADV[i];
}
value1_min=ADV[0]; //最小值
for(i=5;i<123;i++)
{
if(value1_min>=ADV[i])
value1_min=ADV[i];
}
CCD_Yuzhi=(value1_max+value1_min)/2; //计算出本次中线提取的阈值
for(i = 5;i<118; i++) //寻找左边跳变沿
{
if(ADV[i]>CCD_Yuzhi&&ADV[i+1]>CCD_Yuzhi&&ADV[i+2]>CCD_Yuzhi&&ADV[i+3]<CCD_Yuzhi&&ADV[i+4]<CCD_Yuzhi&&ADV[i+5]<CCD_Yuzhi)
{
Left=i;
break;
}
}
for(j = 118;j>5; j--)//寻找右边跳变沿
{
if(ADV[j]<CCD_Yuzhi&&ADV[j+1]<CCD_Yuzhi&&ADV[j+2]<CCD_Yuzhi&&ADV[j+3]>CCD_Yuzhi&&ADV[j+4]>CCD_Yuzhi&&ADV[j+5]>CCD_Yuzhi)
{
Right=j;
break;
}
}
CCD_Zhongzhi=(Right+Left)/2;//计算中线位置
//根据实际情况
// if(math_abs(CCD_Zhongzhi-Last_CCD_Zhongzhi)>90) //计算中线的偏差,如果太大
// CCD_Zhongzhi=Last_CCD_Zhongzhi; //则取上一次的值
// Last_CCD_Zhongzhi=CCD_Zhongzhi; //保存上一次的偏差
}
根据计算的中值调节车子的运动方向。
/*摄像头循迹-普通轮子*/
/*这里75,55包括下面的这些值,都是我根据赛道测试出来的,这个值需要根据实际赛道情况作出调整。*/
void tuxiangxunji(void)
{
if(CCD_Zhongzhi<=75&&CCD_Zhongzhi>=55)
{
contrl(5,0,5,0); //前进
}
if(CCD_Zhongzhi>85)
{
contrl(0,0,18,0); //右转
}
if(CCD_Zhongzhi<45)
{
contrl(18,0,0,0); //左转
}
if(CCD_Zhongzhi>75&&CCD_Zhongzhi<=85)
{
contrl(0,0,6,0); //小右转
}
if(CCD_Zhongzhi<55&&CCD_Zhongzhi>=45)
{
contrl(6,0,0,0); //小左转
}
// if(distance_cm < 5) CarStop(); //距离小于20CM停车
}
void CCD_ceshi(void)
{
int i;
RD_TSL();
Find_CCD_Zhongzhi();
for(i=0;i<128;i++)
{
UARTprintf("%d",ADV[i]);
UARTprintf(" ");
}
UARTprintf("\r\n ");
UARTprintf("%d",CCD_Zhongzhi);
UARTprintf("\r\n ");
UARTprintf("%d",ADV[CCD_Zhongzhi]);
UARTprintf("\r\n ");
UARTprintf("*------------* ");
UARTprintf("\r\n ");
tuxiangxunji();
}
测试的视频:
MSP432E401Y_循迹小车
注意,由于摄像头是感光元件,测试要注意光线的影响,需要根据光线的强弱,调节计算中值时的偏差值。
二、WiFi通信
为了模拟2022年Ti电赛的双车跟随的题目,我和队友分别在MSP的板子上和STM32的板子上使用了ESP8266-01这个模块,MSP上面操作这个模块时,由于第一次接触这个MCU,在时序上吃了很多亏,最终才解决问题。
模块介绍:
ESP8266-01是最常用的WiFi模块,功耗较低。使用起来也非常方便,简单的AT指令就可以完成对它的控制。在正式开发时,可以用CH340串口烧录器,直接输入AT指令,检测模块是否完好。
AT指令表:
命令类型 | 语法 | 返回/说明 |
测试命令 | AT | OK/表明模块完好已经响应 |
设置命令 | AT+CWMODE = <mode> | OK/此指令需重启后生效(AT+RST) |
查询命令 | AT+CWMODE? | +CWMODE:<mode> OK /当前处于哪种模式? |
测试命令 | AT+CWMODE? | +CWMODE:(<mode>取值列表) OK 当前可支持哪些模式? |
设置命令 | AT+CWJAP=<ssid>,<pwd>(ssid指的热点的名字,pwd为热点的钥匙🔑) | OK 或 ERROR /加入该AP成功则返回OK,失败则返回ERROR |
查询命令 | AT+CWJAP? | +CWJAP:<ssid> OK /返回当前选择的AP |
执行命令 | AT+CWQAP | OK /表示成功退出该AP |
测试命令 | AT+CWQAP=? | OK /查询该命令是否支持 |
当然关于ESP8266-01的AT操控指令还有很多,由于我本次只用到了这些,所以剩下的我就不列举了,需要的小伙伴自己去开发的用户手册查询。
引脚说明:
与MSP结合:
理解了ESP8266-01的工作原理之后,就可以用MSP来对他进行控制了,由于WiFi模块是通过串口的通信方式进行AT指令控制的。所以需要要开一个串口来进行指令的传输。
配置MSP的串口:
void UART6_IRQHandler(void) //串口6的中断服务函数
{
uint32_t ucCh;
uint32_t ui32Status;
// Get the interrrupt status.
ui32Status = UARTIntStatus(UART6_BASE, true);
if(UARTCharsAvail(UART6_BASE) != false) //如果FIFO不为空
{
ucCh=UARTCharGet(UART6_BASE); //测试
if(ESP8266_FrameStructure.InfBit.FramLength < (RX_BUF_MAXLEN - 1))
{
//预留1个字节写结束符
ESP8266_FrameStructure.RX_BUF[ESP8266_FrameStructure.InfBit.FramLength ++] = ucCh;
}
}
if(UARTCharsAvail(UART6_BASE) == true) //数据FIFO不为空
{
ESP8266_FrameStructure.InfBit.FramFinishFlag = 1;
//ucCh=UARTCharGet(UART6_BASE);
}
// Clear the asserted interrupts.
//
UARTIntClear(UART6_BASE, ui32Status);
}
void ConfigureUART6(uint32_t systemClock)
{
/* Enable the clock to GPIO port A and UART 0 */
SysCtlPeripheralEnable(SYSCTL_PERIPH_GPIOP);
SysCtlPeripheralEnable(SYSCTL_PERIPH_UART6);
/* Configure the GPIO Port A for UART 0 */
GPIOPinConfigure(GPIO_PP0_U6RX);
GPIOPinConfigure(GPIO_PP1_U6TX);
GPIOPinTypeUART(GPIO_PORTP_BASE, GPIO_PIN_0 | GPIO_PIN_1);
GPIOIntRegisterPin(GPIO_PORTP_BASE, GPIO_PIN_0 ,UART6_IRQHandler); //注册具体中断的接收口
/* Configure the UART for 115200 bps 8-N-1 format */
UARTStdioConfig(3, 115200, systemClock); //选择串口二,设置波特率115200
UARTIntRegister(UART6_BASE, UART6_IRQHandler); //说明中断的入口处
UARTIntEnable(UART6_BASE,UART_INT_RX); //使能串口接收中断
UARTEnable(UART6_BASE); //使能串口6中断
}
配置ESP8266-01的TCP模式:
void ESP8266_Init(void)
{
/* 将GPIO PH1、配置为输入并启用内部上拉功能。 配置PE0进行双向中断检测*/
SysCtlPeripheralEnable(ESP8266_RST_Pin_Periph_Clock); //初始化GPIOE
GPIOPinTypeGPIOOutput(ESP8266_RST_Pin_Port, (ESP8266_RST_Pin | ESP8266_CH_PD_Pin)); //设置GPIOE1为输出,PE2为测试定时器的周期
ConfigureUART6(getSystemClock); //串口6初始化
ESP8266_RST_Pin_SetH;
ESP8266_CH_PD_Pin_SetL;
}
//对ESP8266模块发送AT指令
// cmd:待发送的指令
// ack1,ack2:期待的响应,为NULL表不需响应,两者为或逻辑关系
// time:等待响应的时间
//返回1:发送成功 0:失败
bool ESP8266_SendCmd(char *cmd,char *ack1,char *ack2,uint32_t time)
{
ESP8266_FrameStructure.InfBit.FramLength = 0; //重新开始接收新的数据包
UARTprintf("%s\r\n",cmd); //通过USART2发送AT命令
if(ack1==0 && ack2==0) //不需要接收响应信息
{
return true;
}
delay_ms1(time); //为接收到的响应数据帧加上结束符
ESP8266_FrameStructure.RX_BUF[ESP8266_FrameStructure.InfBit.FramLength ] = '\0';
//UARTprintf("%s", ESP8266_FrameStructure.RX_BUF);//USART1输出接收到的响应数据帧
if(ack1!=0 && ack2!=0)
{
return ((bool)strstr(ESP8266_FrameStructure.RX_BUF, ack1) ||
(bool)strstr(ESP8266_FrameStructure.RX_BUF, ack2));
}
else if( ack1 != 0 )
return ((bool)strstr(ESP8266_FrameStructure.RX_BUF, ack1));
else
return ((bool)strstr(ESP8266_FrameStructure.RX_BUF, ack2));
}
//重启WF-ESP8266模块
void ESP8266_Rst(void)
{
ESP8266_RST_Pin_SetL;
delay_500ms();
ESP8266_RST_Pin_SetH;
}
//ESP8266模块发送字符串
//pStr:要发送的字符串
//ulStrLength:要发送的字符串长度(字节数)
//返回1:发送成功 0:发送失败
bool ESP8266_SendString(char * pStr, uint32_t ulStrLength)
{
bool bRet = false;
UARTprintf("%s", pStr); //直接发送这个字符串,串长度不用考虑长度-stm32,要考虑长度-msp432
bRet = true; //返回发送成功标志
return bRet;
}
//ESP8266退出透传模式
void ESP8266_ExitUnvarnishSend( void )
{
delay_500ms();
delay_500ms();
UARTprintf("+++");
delay_500ms();
}
//获取 ESP8266 的连接状态,较适合单端口时使用
//返回0:获取状态失败
//返回2:获得IP
//返回3:建立连接
//返回4:失去连接
uint8_t ESP8266_Get_LinkStatus(void)
{
if(ESP8266_SendCmd("AT+CIPSTATUS", "OK", 0, 500))
{
if (strstr(ESP8266_FrameStructure.RX_BUF, "STATUS:2\r\n"))
return 2;
else if(strstr(ESP8266_FrameStructure.RX_BUF, "STATUS:3\r\n"))
return 3;
else if(strstr(ESP8266_FrameStructure.RX_BUF, "STATUS:4\r\n"))
return 4;
}
return 0;
}
void ESP8266_TCPClient(void)
{
//UARTprintf("正在配置ESP8266,请耐心等待...\r\n" );
ESP8266_CH_PD_Pin_SetH;
while(!ESP8266_SendCmd("AT", "OK", NULL, 500));
while(!ESP8266_SendCmd("AT+CWMODE=1", "OK", "no change", 2500));
while(!ESP8266_SendCmd("AT+CWJAP=\"kevin\",\"12345678\"", "OK", NULL, 500)); //热点名称密码,是你自己的热点,或者WiFi名字和🔑
while(!ESP8266_SendCmd ("AT+CIPMUX=0", "OK", 0, 500));
while(!ESP8266_SendCmd("AT+CIPSTART=\"TCP\",\"192.168.31.241\",8000", "OK", NULL, 2500)); //这个IP地址是需要改的
while(!ESP8266_SendCmd("AT+CIPMODE=1", "OK", 0, 500));
while(!ESP8266_SendCmd("AT+CIPSEND", "OK", ">", 500));
//UARTprintf("正在配置ESP8266,请耐心等待...\r\n" );
}
以上就是增加的两个主要功能,另外我添加了一个语音播报模块JQ8900,这个操作跟WiFi模块操作一样的,都是通过串口发送指令控制的,很简单,大家可以对照手册就能完成。以上就是我准备2023年电赛的情况,最后也没有去打小车的题目(由于小车和无人机出在一起了),有点小可惜!🥲
没有MSP的MDK包的可以去我下面的链接去下载
最后我附上我到这一步的工程源码
WiFi通信和线性CCD摄像头循迹资源-CSDN文库https://download.csdn.net/download/qq_62291061/88664222