早两年前写过用DMA直接驱动GPIO的文章,当时写的只是比较原理性的,没有实例。最近在用到单线总线,上了RTOS,为了提高效率,减少内核的浪费,就想到用TIMER+DMA+GPIO去输出单线总线时序。
上图是单线总线的时序,常规的方法就直接控制IO
输出,电平的间隔用延时去控制,代码如下:
void SC50X0B_SDA(unsigned char data) { unsigned char i; SC5020_SDA_Reset(); HAL_Delay(3); for(i=0;i < 8;i++) { SC5020_SDA_Set(); if(data & 0x01) { delay_us(1200); SC5020_SDA_Reset(); delay_us(400); } else { delay_us(400); SC5020_SDA_Reset(); delay_us(1200); } data >>= 1; } SC5020_SDA_Set(); delay_us(200); }
整个时序执行需要2.5+
(
1.2+0.4
)
X8 = 15.3ms
,这样内核要花
15.3ms
时间去输出这个时序,如果中断频繁也会导致时序有误差,引用通信失败。尤其是上了
RTOS
的话,会占用进程太长的时间,导致其它的进程无法及时执行,或者进程被抢占,导致时序误差,通信失败。当然很多人会有说用定时器中断去控制
IO
输出也可以实现,这样就大大的提高的效率,我这里不过多的讨论。用
TIMER+DMA+GPIO
的话,全靠外设输出时序,完全不占用内核的时间,这样的效率达到最高。如下是完整的代码实现:
#define TIMEFORDMATOGPIO TIM1 #define TIMEFORDMATOGPIO_CLK __HAL_RCC_TIM1_CLK_ENABLE #define TIMEFORDMATOGPIODAN DMA1_Channel5 TIM_HandleTypeDef sc5080htim; DMA_HandleTypeDef hdma_sc5080tim_up; uint32_t SC5080Data[4 *8 +1]; void SC5080_TIMx_MspInit(void); void SC5080_TIMx_Init(void) { sc5080htim.Instance = TIMEFORDMATOGPIO; sc5080htim.Init.Prescaler = 72 - 1; sc5080htim.Init.CounterMode = TIM_COUNTERMODE_UP; sc5080htim.Init.Period = 400; sc5080htim.Init.ClockDivision = TIM_CLOCKDIVISION_DIV1; SC5080_TIMx_MspInit(); __HAL_TIM_ENABLE_DMA(&sc5080htim, TIM_DMA_UPDATE); HAL_TIM_Base_Init(&sc5080htim); } void SC5080_TIMx_MspInit(void) { /* Peripheral clock enable */ TIMEFORDMATOGPIO_CLK(); hdma_sc5080tim_up.Instance = TIMEFORDMATOGPIODAN; hdma_sc5080tim_up.Init.Direction = DMA_MEMORY_TO_PERIPH; hdma_sc5080tim_up.Init.PeriphInc = DMA_PINC_DISABLE; hdma_sc5080tim_up.Init.MemInc = DMA_MINC_ENABLE; hdma_sc5080tim_up.Init.PeriphDataAlignment = DMA_PDATAALIGN_WORD; hdma_sc5080tim_up.Init.MemDataAlignment = DMA_MDATAALIGN_WORD; hdma_sc5080tim_up.Init.Mode = DMA_NORMAL; hdma_sc5080tim_up.Init.Priority = DMA_PRIORITY_VERY_HIGH; HAL_DMA_Init(&hdma_sc5080tim_up); } void SC5080B_Set(uint8_t data) { uint8_t i; SC5020_SDA_Reset(); #if 0 HAL_Delay(3); #else osDelay(3); #endif for(i=0;i < 8;i++) { if(data & 0x01) { SC5080Data[i*4 +0] = SC5020_SDA_Pin; SC5080Data[i*4 +1] = SC5020_SDA_Pin; SC5080Data[i*4 +2] = SC5020_SDA_Pin; SC5080Data[i*4 +3] = SC5020_SDA_Pin<<16; } else { SC5080Data[i*4 +0] = SC5020_SDA_Pin; SC5080Data[i*4 +1] = SC5020_SDA_Pin<<16; SC5080Data[i*4 +2] = SC5020_SDA_Pin<<16; SC5080Data[i*4 +3] = SC5020_SDA_Pin<<16; } data >>= 1; } SC5080Data[i*4 +0] = SC5020_SDA_Pin; HAL_DMA_Start_IT(&hdma_sc5080tim_up,(uint32_t)(&SC5080Data),(uint32_t)(&(SC5020_SDA_GPIO_Port->BSRR)),4*8+1); HAL_TIM_Base_Start(&sc5080htim); }
大概的原理就是利用定时器的update
(或者
PWM
)去触发对应的
DMA
通道,
DMA
从内存搬到数据到
GPIO
的
BSRR
寄存器,
GPIO
对的
Pin
就会改变电平。这里用到了
TIM1
,查手册知道
TIM1
的
update
对应的
DMA
通道是
DMA1
的通道
5
,所以初始了
TIM1
跟
DMA1_Channel5
。
<ignore_js_op>
发送一个Bit
需要
1600us
,也就是
4
个
400us
,也就是说如果发
1
就是,定时器触发
4
次
DMA
,
DMA
搬运内存到
GPIO
的
BSRR
,只要搬到的数据到
BSRR
对应
IO
的输出
1110
,
GPIO
就是输出
1200us
的高电平,
400us
的低电,那
IO
就正好输出时序中的
1
。
8
个
Bit
,就是让
DMA
搬到
4X8
次,就可以整个时序。实际使用中只要调用
void SC5080B_Set(uint8_t data)
,计算当时发送时序需要搬到的数据,再启动
DMA
跟定时器,时序就会自动输出,不在需要内核的干预。当然这很容易拓展到同时最多输出
16
路的时序,效率上也一样的,大大的节省的内核的开销,让程序更加的高效。