协议转换---从串口到IIC
一、前言:
-
前段时间用QT写了个上位机,需要通信调试,因此写了下位机,这个过程中遇到了一些坑及知识点,为防止后续遗忘及顺便对知识进行梳理,写这篇文章做个总结。
-
单片机(stm32系列)通过UART接收上位机发送的数据,再通过IIC的通讯方式将数据转发给从机设备,实现串口到IIC之间的协议转换,保证不同设备之间的通讯正常。
二、正文:
2.1.串口:串口做为上位机与单片机之间的通讯方式,主要是2个方面:
2.1.1.接收:由于有大量数据及不同长度类型的指令,因此我采用DMA+空闲中断的方式。
-
下面只列出了我认为需要注意的点:
USART_ITConfig(USART1,USART_IT_IDLE, ENABLE);//开启串口空闲中断 -
需要使用DMA,串口需要使能DMA请求
USART_DMACmd(USART1,USART_DMAReq_Rx|USART_DMAReq_Tx,ENABLE);
正常模式:DMA接收完一轮后,需要手动置数。
连续模式:DMA接收完一轮后,自动置数。
DMA_InitStructure.DMA_Mode = DMA_Mode_Normal; //工作在正常模式;
//开启一次DMA传输
void MYDMA_Enable(DMA_Channel_TypeDef*DMA_CHx,u16 Len)
{
DMA_Cmd(DMA_CHx, DISABLE ); //关闭USART1 TX DMA1 所指示的通道
DMA_SetCurrDataCounter(DMA_CHx,Len);//DMA通道的DMA缓存的大小
DMA_Cmd(DMA_CHx, ENABLE); //使能USART1 TX DMA1 所指示的通道
}
2.1.2 发送:同样需要大量数据发送给上位机,采用DMA+中断(发送完成)
DMA_ITConfig(DMA1_Channel4, DMA_IT_TC, ENABLE);
- 在中断函数中将标志位DMA_TC_FLAG 置位,主函数main中通过标志位为下一次传输做准备
为什么仅在中断函数中置标志位,不直接做准备操作呢?
答案在于:记住一个原则,尽可能少在中断函数中做过多操作。
//DMA中断函数的重定义
void DMA1_Channel4_IRQHandler(void)
{
if(DMA_GetITStatus(DMA1_IT_TC4) != RESET){
DMA_ClearITPendingBit(DMA1_IT_TC4);
DMA_TC_FLAG = 1;
}
}
思考点:
思考1:
DMA是在发送完指定数量的字节后(计数为0时),才会触发完成中断,但发送过程中有不同长度的类型数据,怎么才能让不同长度数据发送完都及时的产生发送中断呢?
突破点:
- DMA工作在普通模式下,每轮中断都需要手动开启,设置传输字节。
- 每次发送完数据,触发中断后,根据每次传输不同类型数据的长度,来设置DMA传输字节量。
- 这就需要通过一个全局变量(例如下图中的DMA_Tx_Len)来存储设置的传输字节量。
if(DMA_TC_FLAG){ //判断上一次的DMA传输是否完成 1:完成,可以开启新的传输 2:未完成,无法进行新一轮的数据传输
if(Timer_flag){
Timer_flag=0;
USART_TxRead_FLAG = 0;
DMA_TC_FLAG=0;
MYDMA_Enable(DMA1_Channel4,DMA_Tx_Len);//开始下次DMA传输! ---这时DMA将需要发送的数据搬到串口进行发送
LED0=!LED0;//数据的发送频率
}
}
思考2:DMA传输速度很快,基本只要是使能开启,就会很快发送出去,不能控制发送频率,这样就不容易匹配有频率要求的接收方。
答:往往常用的方法是结合定时器来控制:
TIM_ITConfig(TIM3,TIM_IT_Update,ENABLE ); //使能指定的TIM3中断,允许更新中断
//定时器3中断服务程序
void TIM3_IRQHandler(void) //TIM3中断
{
if (TIM_GetITStatus(TIM3, TIM_IT_Update) != RESET) //检查TIM3更新中断发生与否
{
TIM_ClearITPendingBit(TIM3, TIM_IT_Update ); //清除TIMx更新中断标志
Timer_flag = 1;
}
}
主函数中通过Timer_flag来控制DMA准备语句的实现。
if(Timer_flag){
Timer_flag=0;
USART_TxRead_FLAG = 0;
DMA_TC_FLAG=0;
MYDMA_Enable(DMA1_Channel4,DMA_Tx_Len);//开始下次DMA传输! ---这时DMA将需要发送的数据搬到串口进行发送
LED0=!LED0;//数据的发送频率
}
2.2 IIC:
IIC主要用于主控板和从设备之间的通信方式,将上位机下发的的数据传输给从设备。
IIC通信实现的方式分为2类,软件模拟和硬件IIC。考虑到硬件IIC出现问题后容易卡死,所以本章采用的为软件模拟
来实现IIC的通信。
关于软件IIC的实现主要注意的为2点:IIC协议的熟悉、SCL时序的控制。
IIC协议简介:
IIC通信只需要2根线就可以实现,控制时钟信号的:SCL,读写高低电平的:SDA。
协议规则:
起始信号:SCL:1 SDA:1 -> 0;
结束信号:SCL:1 SDA:0 -> 1;
准备数据:SCL:0 SDA:可变,高低电平变化
读取数据:SCL:1 SDA:不变,此时读取上面电平
规律总结:
SCL高电平时,SDA变化的话,就是起停信号。
SCL低电平时,SDA变化的话,就是数据准备。
SCL高电平时,SDA不变化的话,就是数据读取。
IIC的实现:
IIC通信的本质,其实就是控制2个IO口,按照合适的顺序,进行高低电平的变化。
- IO口:开漏输出,可以输出高低电平,同时还可以读取IO电平(IO结构决定的,可查看数据手册)。
GPIO_InitStruct.GPIO_Mode = GPIO_Mode_Out_OD; - IO函数的分装
//读SDA电平
void IIC_W_SDA(uint8_t value)
{
GPIO_WriteBit(GPIOB,GPIO_Pin_7,(BitAction)value);
}
//写SDA电平
uint8_t IIC_R_SDA()
{
return GPIO_ReadInputDataBit(GPIOB,GPIO_Pin_7);
}
//写SCL电平
void IIC_W_SCL(uint8_t value)
{
GPIO_WriteBit(GPIOB,GPIO_Pin_6,(BitAction)value);
}
- IO初始化时,将SCL、SDA置1为空闲状态
/* IIC初始化*/
void IIC_Init(void)
{
GPIO_InitTypeDef GPIO_InitStruct;
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB,ENABLE);
GPIO_InitStruct.GPIO_Mode = GPIO_Mode_Out_OD;
GPIO_InitStruct.GPIO_Pin = GPIO_Pin_6 | GPIO_Pin_7;
GPIO_InitStruct.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOB,&GPIO_InitStruct);
IIC_W_SCL(1);
IIC_W_SDA(1);
}
- 起始信号函数的封装
/* IIC开始 */
void IIC_Start(void)
{
IIC_W_SDA(1); //释放SDA
delay_us(5);
IIC_W_SCL(1); //释放SCL
delay_us(5);
IIC_W_SDA(0); //拉低电平,启动开始
delay_us(5);
IIC_W_SCL(0); //拉低电平,占据主线
delay_us(5);
}
- 停止信号函数的封装
/* IIC停止 */
void IIC_Stop(void)
{
IIC_W_SDA(0);
delay_us(5);
IIC_W_SCL(1);
delay_us(5);
IIC_W_SDA(1);
delay_us(5);
}
- 发送一个字节数据函数的封装
/* 发送一个字节 ,接收应答*/
void IIC_Senddata(uint8_t data)
{
uint8_t i = 0;
for(i = 0;i < 8;i++){
IIC_W_SDA(data & (0x80 >> i));
delay_us(5);
IIC_W_SCL(1);//从机读取SDA总线数据
delay_us(5);
IIC_W_SCL(0);//拉低电平,为写入下一个数据做准备
delay_us(5);
}
IIC_RxceAck(); //额外的一个时钟,读取从机响应
}
- 接收响应(往往发送一个字节数据后,要获取一个返回的响应,才继续下一个字节的传输)
/* 接收应答 */
uint8_t IIC_RxceAck(void)
{
uint8_t Ack = 0;
IIC_W_SDA(1);//拉高SDA,确保主机释放SDA,以免干扰从机
delay_us(5);
IIC_W_SCL(1);//拉高SCL,主机在高电平进行读取从机的响应
delay_us(5);
Ack = IIC_R_SDA();
delay_us(5);
IIC_W_SCL(0);
delay_us(5);
return Ack;
}
- 接收字节数据
/* 接收一个字节,并发送应答 */
uint8_t IIC_Recedata(void)
{
uint8_t i ,rxdata =0x00;
IIC_W_SDA(1);//释放SDA,确保从机能够发送数据
delay_us(5);
for(i = 0;i < 8;i++)
{
IIC_W_SCL(1); //主机高电平读取数据
delay_us(5);
if(IIC_R_SDA( ) == 1)
rxdata |= (0x80 >> i);
IIC_W_SCL(0); //从机传送数据
delay_us(5);
}
IIC_SendAck(0); //主机发送响应
return rxdata;
}
- 发送应答
/* 发动应答 */
void IIC_SendAck(uint8_t Ackbit)
{
IIC_W_SCL(0);//拉低SCL电平,为主机在SDA总线上发送响应做准备
delay_us(5);
IIC_W_SDA(Ackbit);//主机拉低SDA电平,进行响应
delay_us(5);
IIC_W_SCL(1); //从机读取响应
delay_us(5);
IIC_W_SCL(0);//拉低SCL电平,从机开始发送下一个数据做准备
delay_us(5);
}
- 上面的这些函数都只是IIC通信的最基本的函数,但平常我们发送的数据可不止一个字节数据。这就需要我们来将基本函数再次进行封装成可以发送多个字节数据的函数。
void send_data(uint8_t *databuffer,uint16_t len)
{
uint8_t i;
IIC_Start(); //起始信号
for(i =0;i<len;i++)
{
IIC_Senddata(databuffer[i]);
}
IIC_Stop(); //停止信号
}
//接收函数
void Rx_data(uint8_t *arry,uint8_t len){
if(rece_flag){
uint8_t rcev_byte=0,i;
for(i=0;i<len;i++){
rcev_byte =IIC_Recedata();
arry[i] = rcev_byte;
}
rece_flag = 0;
}
}
- 注意点:
- IO口操作之间一定得加延时函数!!!,否则就会出现多个IO操作最终只执行最后一个IO口电平。
三、总结:
-
串口到IIC之间协议的转化,无非分为3个步骤:串口接发数据、中间量存储数据、IIC发收数据,将整个过程单独分开来实现,最后再整体组合就可以实现。
-
这整个过程中,需要注意的点在于:数据接收和发送的实现方式(需要考虑到数据量、不定长)、IO电平切换的有效时长(IO操作之间增加延时)、时序的控制(SCL控制时序,控制通信之间的节奏)
四、声明:
在这个过程中,我也参考了其他博主文章和视频来学习,在此基础上结合自己的一些灵感并实践,最终成功。再此感谢哪些愿意分享自己经验的博主们,也希望这个互相学习,互相分享的氛围越来越好。
浙公网安备 33010602011771号