目录
因为最近买了一个三轴陀螺仪想用stm32进行开发,但是因为我工作能用的只有华芯的板子,想着自己还有一块stm32M3的最小系统板子(这个板子目前卖的很多,基本都是一致的那种,型号是stm32f10c8t6),就打算用这个板子进行开发,突然想到这个板子没有串口转TTL电路,意思就是这个板子有串口这个外设,但是它跟你电脑的串口上位机软件连接不了,我之前开发的时候就借助另外一个板子,用串口给它发数据,然后它在发给PC,这样就可以看到数据了
但是我目前能用一个板子华芯的SWM341还剩为数几个不多的IO,但是不是串口的映射,是SPI的,所以我就在想刚好SWM341已经驱动好LCD屏了,而且GUI已经上好了,只要它能拿到我stm32板子的数据就可以显示在LCD上,所以想法成立,开始建立通信,因为之前在学SPI和IIC时没少模拟过通信,对于时序这些比较敏感,也前也有过用IO模拟串口,只不过没成功,所以我当时的想法是准备以下几种
两端IO模拟SPI或者IIC;
两端硬件SPI或者IIC;
两端IO模拟串口
两端自定义IO通信
其实开始我想的就是不能重定向printf函数,数据无法可视化对于我开发太不爽了,就是想着能够有个输出数据端,所以主要任务就是建立通信,并且重定向printf,而且考虑到特殊情况,比如没有串口,SPI和IIC的硬件映射的时候,怎么样用最少的资源快速建立稳定的通信呢,先说说想法。
1.两端IO模拟SPI或者IIC
想必大家在学习SPI和IIC的过程中应该也用IO模拟通信开发各种模块吧,比如经典的w25q64和AT24C04之类的,但是其实对于主机模式的模拟还是比较简单的,不用进行寻址还有数据协议识别的处理,因为我之前没有用IO模拟过从机模式,所以我去网上找了一下资料,都是说不太建议IO模拟从机模式太复杂且容易出错,所以我也没有太大的信心,因为感觉IO模拟一端数据是不好处理而且一定会出错,就是总觉得没有硬件靠谱。
2.两端硬件SPI或者IIC
我心想用硬件控制器总应该没问题吧,还是比较稳的,于是马上就配好了两边的硬件SPI,STM32F103为主机模式,SWM341为从机模式,但是对于片选线(CSS)的管理我一开始选择了软件管理,因为我这里想到了华芯的片选线是会连续跳动了,其实也没关系,因为在这里华芯是做了从机,控制不了CSS,规定数据帧是8bit,然后下面是重定向操作。
这里重定向操作的话,我们发送一字节数据,CS线是会跳8次的,我也试过在片选中间多夹几条发送数据函数,进行一次CS的跳动,结果大差不差。
但是问题来了,接收端确实有数据,但是我发现数据没有失真,比如我发送“ABCD”,只能随机拿到其中两个,发送更多的数据也会丢失非常多,所以我怀疑我数据处理代码的问题,我在SWM341那边开启了CS线上升沿和下降沿中断不断调试,我发送SPI的数据寄存器跟我发送的数据个数有差异,意思就是时序有点对不上,所以这里我很疑惑,明明时钟线已经同步了。还有就是我想实现的效果就是我在stm32板子上运行printf函数的时候,SWM341就马上可以收到数据,而且对于我代码中变量的格式转换什么的,所以我不能放弃printf,也想过自己写一个格式转换的数据发生函数,比如利用sprintf进行字符串的转换和填充,然后发送,想了一下太麻烦。最终因为是硬件控制器,我也只能不停的修改控制寄存器和中断进行调试,所以我总结了以下问题。
硬件外设差异 SWM341的SPI控制器的数据寄存器为32位可配置,8级深度FIFO,跟STM32的SPI控制器还是有差异,所以导致有误差(当然我觉得这个可能性不大)
数据处理不好,因为在STM32不停的一字节发送,我试过有一字节就收一字节出来,但是数据量还是对不上,我也想过做一个空闲处理,当一波数据发送完,在一口气读出来,但是FIFO就8字节,如果满了来不及读出来那数据又废了,总之试过很多方法,感觉达不到我想要的效果。
IIC的控制器我就没配了,不过我觉得肯定是可以了,主要是这里SPI的主从模式配不上,调试到这里其实用了我很多时间了,所以有点想放弃
3.两端IO模拟串口
这个更是重量级,以前我就模拟过,没把我折磨的够呛,IO模拟串口的数据发送和接收函数都好写,但是最重要的问题就是时序一致,我们就定一帧数据 1bit起始位 8bit数据位 1bit停止位
时序的差异在哪里,比如RX空闲时拉高,为高电平,但是TX检测到RX为低的时候,也就是起始位,但是你TX拉低起始位的同时已经开始发送数据了,那边检测到需要时间,这种往往就是有数据,但是数据失真严重,需要不断去调,网上也有很多IO模拟串口的教程,感兴趣可以去看看,时序一致要考虑很多情况,硬件特性和软件处理等,主要还是不够准和快,基本上有用定时器做时基和中断响应的,甚至中断响应要非常频繁,需要大量的资源而且在保证连续快速的数据收发时比较难处理吧,所有我就没有考虑了。
4.两端自定义IO通信
(包含源码以及框架)
当你开发的模块够多的时候会发现往往很多集成度很高的模块都是需要通信的,而且通信协议不只是传统的UART,SPI,IIC这些,很多协议都是那种厂商自定义的,比如用DHT11单总线用时序来分辨,还有一些模块是IO的高低加延时判断组成数据0或者数据1这种,总之MCU通信的本质也就是判断IO口电平的高低来给出数据,然后进行的数据处理(时基和时序),所以我在想自己能不能定义一个通信来实现呢,所以就有了以下通信方式。
暂定为单工通信,stm32f103作为主机发送,SWM341作为从机接收数据。通信总线为两根线,一根为SCK作为时钟线,一根为SDA作为数据线,有点像IIC,也算是从IIC上获得的灵感,这里只需要两端各出两个普通的IO口就可以了,不用上下拉任何操作,还需要一个延时函数,最好是ms和us延时都有,为了追求误差小和精准,我这里用的是定时器的延时函数,用自己写的软件延时函数也可以。
4.1 发送端
对于stm32f103来说 配置SCK 和 SDA 为通用推挽输出就可以了
这里我们规定时钟空闲电平为低电平(其实高低都可以,后面会说到)
void IO_IIC_Init(void)
{
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA,ENABLE);
GPIO_InitTypeDef GPIO_InitStruct = {0};
GPIO_InitStruct.GPIO_Mode = GPIO_Mode_Out_PP;
GPIO_InitStruct.GPIO_Pin = GPIO_Pin_4 | GPIO_Pin_5;
GPIO_InitStruct.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOA,&GPIO_InitStruct);
IO_SCK_L;//拉低时钟
}
经典的8位发送函数,高位先发,反正是自己定义的,高低都可以
//发送数据函数 时钟线空闲为低,在时钟线上沿发送数据,为了时钟线能够稳定下沿,下沿也给一个保持时间 MSB
void IO_send_8bdata(u8 data)
{
for(u8 i = 0;i < 8;i++)
{
IO_SCK_H;//拉高发送数据
if(data & (0x80 >> i))
{
IO_SDA_H;
}
else
{
IO_SDA_L;
}
TIM2_Delay_us(10);//电平保持时间 2-20
IO_SCK_L;
TIM2_Delay_us(50);//这里的延时一定要够,因为是自拟协议比较糙 比1ms小
}
TIM2_Delay_us(110);//写数据忙碌时间
}
这里的话我们的时钟线需要跟着数据一起跳,比如SCK拉高后,我们发送数据,然后操作数据线,这个时候给个延时保持电平时间,这里主要是为了让接受端捕获到这个上升沿知道数据来了,至于延时的时间,测试的话是最低2us,不然边沿捕获不到,给个10us折中,这里不需要延时太久,因为数据的处理它可以在SCK的下降沿也处理,只要在下一个SCK上升沿来之前处理完就可以了,所以这里我们操作完SDA后,拉低SCK,给一个比较大的延时,拿来处理数据。
然后8位发送完后,就是已经发送完一字节数据了,给一个写忙碌的时间,因为SWM341接收那端收到了这个数据后它还要把这个字符输出到LCD做显示(就算是接收端通过串口打在PC上也是需要时间的),所以给一个时间。
4.2 接收端
对于SWM341来说,SCK配置为输入模式,然后开启外部中断,上升沿捕获,SDA为输入。
void IO_IIC_Init(void)
{
GPIO_Init(GPIOM,PIN2,0,0,0,0);
GPIO_Init(GPIOM,PIN3,0,0,0,0);
PORT_Init(PORTM,PIN2,PORTM_PIN2_GPIO,1);//输入使能 这里的输入初始化里面没有开时钟
PORT_Init(PORTM,PIN3,PORTM_PIN3_GPIO,1);//输入使能 这里的输入初始化里面没有开时钟
EXTI_Init(GPIOM,PIN2,EXTI_RISE_EDGE);//上沿触发
EXTI_Open(GPIOM,PIN2);
NVIC_EnableIRQ(GPIOM_IRQn);
}
然后中断服务函数进行数据处理
//进一次收一位
uint8_t IO_DATA = 0;//数据
uint8_t IO_count = 0;//计数位
uint8_t IO_flag = 0;//完成标志位
void GPIOM_Handler(void)
{
EXTI_Clear(GPIOM,PIN2);
if(IO_SDA)
{
IO_DATA |= (0x80 >> IO_count);
}
else
{
IO_DATA &= ~(0x80 >> IO_count);
}
IO_count++;
if(IO_count == 8)
{
IO_flag = 1;
IO_count = 0;
}
}
uint8_t IO_DATA = 0;//数据
uint8_t IO_count = 0;//计数位
uint8_t IO_flag = 0;//完成标志位
注意这3个需要外部声明,我们在main函数里进行操作
L:
if(IO_flag)
{
IO_flag = 0;
if(IO_DATA == '\n' || show_x > (800 - 12))//换行判断
{
show_x = 312;
show_y += 16;
if(show_y == 480)
{
UG_FillFrame(300,0,800,480,C_WHITE);//写满清屏
show_x = 312;
show_y = 0;
}
}
if(IO_DATA == '\n') goto L;
UG_PutChar(IO_DATA,show_x,show_y,C_BLACK,C_WHITE);
show_x += 12;
}
else
{
if(!IO_SCK)//数据滤波 总线拉低时为空闲和断电
{
__NOP();
count++;
}
else //高电平刷新
{
count = 0;
}
if(count >= 1400)//数据滤波
{
count = 0;
IO_flag = 0;
IO_DATA = 0;
IO_count = 0;
}
}
if(GPIO_GetBit(GPIOA,PIN8) == 0)
{
for(uint32_t i = 0;i < 140000;i++) __NOP();
UG_FillFrame(312,0,800,480,C_WHITE);//写满清屏
show_x = 312;
show_y = 0;
}
这里是我功能代码的实现,当我们接收完一字节数据后,标志位置1,然后进行数据的操作,但是这是理想的状态,在这个过程只要时序对不上,一定会失真,比如对于计数位的操作,如果在你烧录程序的时候,外设处于断电状态,如果你配置了下降沿捕获,会出现很多乱码,以及打乱一个数据的完整性,为什么这么说,因为IIO_count如果在你的有效数据来之前不为0,那么后续的数据将没有意义,
我上面代码最重要的就是那端数据滤波代码,当标志位不为1,就是一字节数据没有完成时候,一直判断总线,如果总线空闲开始计时,当超出一定时间,自动清0所有,当然如果在数据传输过程也可以检测,但是数据传输过程中检测总线一但是处于非空闲的就计数清0
到了这里我就要说一下我的调试过程了,因为我们想实现stm32f103板子的printf自由,但是在烧录程度的时候,数据会破坏总线的平缓,所以我这段滤波也可跳过这段干扰同时也可以做一个空闲保护,为下一次接收新数据做准备,至于为什么是1400,因为我这里的话SWM341系统主频为140Mhz,但是实际上这里不可能是10us,肯定要大的,这里的判断时间至少要大于你的下降沿保持时间,不然你在传输过程中就被判断空闲了也不行
然后说一下为什么总线空闲是低,拉高发送,想想我们平常调试的时候会用printf函数进行输出,改好程序是不是需要烧录,烧录过程中总线会是低电平然后触发数据,出现乱码,所以为了避免这个过程我选择了拉高总线作为信号。
以下是我stm32板子的主函数代码
// 主频72M
int main()
{
NVIC_SetPriorityGrouping(5);
LED_Init();
TIM1_NVIC_Init(1);
IO_IIC_Init();
TIM2_Delay_ms(10);//上电要稳定一会,不然数据容易失真 最低1ms,这里10ms
printf("hello world\n");
u32 a = 0;
while(1)
{
a++;
TIM2_Delay_ms(1000);
printf("a:%d\n",a);
}
}
上电给个延时会比较稳定,如果不给,有极小概念出现乱码
然后数据通信还是比较快的,因为我们的延时都是us级别的,然后不管是固定的打印,还是循环打印都非常稳定,数据没有失真而且响应速度非常快,
//调试过程图,处理乱码是真的头痛
//建议先测单次单个数据 然后连续单个数据 单次多个数据 连续多个数据
目前就是写到这种功能,能够改良做成半双工通信,类似IIC那样。
上述框架中所有涉及到通信时序延时的地方,可以根据实际情况和硬件特性进行修改,源码只是一个参考值,并不代表绝对稳定值。
附上工作图片: