智能小车——下位机开发1:外设相关初始化

终于,我差不多把下位机的一系列内容这两周全部搞完了,在轮趣那边的基础上,我把看不顺眼的一系列标准库全部换掉了,同时按照自己需要的功能加了些改进,以及一直以来想要摈弃的keil难看的界面,这次一次性处理完了。下面进入正题,一步步完成芯片的一系列初始化。

工程环境配置

现在的工作环境配置成如下:STM32CubeMX高效完成外设的引脚以及参数的初始化配置,生成Makefile工程后,在VSCode中完成代码的编写,同时修改Makefile完成交叉编译,最后通过OpenOCD完成代码的烧写以及调试工作

传统的方法是通过CubeMX完成初始化,选择生成MDK工程,在Keil中完成编译以及烧写调试等一系列任务。但是Keil毕竟是盗版软件,而且功能强大但是UI界面和补全功能实在是不太行,所以切换到VSCode里面完成代码,然后通过开源的OpenOCD来烧写。网上这一方面的教程还是不少的,可以自行去检索一下;如果VSCode觉得配置起来有点麻烦,可以直接去搞一个CLion,直接里面可以连接到stm32的工程,完了之后就是同样对接OpenOCD(JetBrain虽然还是用盗版,但架不住是真的好用)。

我这次是想寻求全开源的免费线路,所以就是STM32CubeMX+VSCode+OpenOCD的路线了。

刚才看了一下,甚至可以生成cmake工程,可以说是更加好用,但是需要更新到最新版本,比较烦人,我就还是Makefile来完成编译。

ST 芯片初始化

确认芯片型号

这里因为是买的轮趣的板子,他们开发板采用的芯片是STM32F407VET6,所以在CubeMX新生成工程的时候需要进行选择,选择到正确的芯片之后去建立工程,如下:

首先根据MCU的型号,来开启工程创建:
根据MCU新建工程
可以在左上角选择搜索对应的芯片,我这边就是STM32F407VET6:
选择对应芯片
这里选择第一个,你可以点进去看到是不是你想要的芯片,然后确认创建就可以了。

开始配置

先看一下成品,之后一步步配置:

整体的引脚使用情况
可以看到资源利用的满满的,基本都用上了。

配置时钟

这里我不是那么确定,但配置成这样板子能跑而且没有报错。

在RCC中,把HSE和LSE全部选成外部晶振:

选择时钟源
之后去到 Clock Configuration,进一步完成时钟线的配置,这里有两点是确认的,APB1 不能超过 42MHz,APB2 不能超过 82MHz,其余的配置我全部是对照的我正点原子的电机开发板配置的,那个也是F407的芯片,我就直接搬过来了:

时钟配置
这里到目前为止,整个系统时钟基本就搞定了。

配置系统

这里就是配置烧写口,以及对应的整个系统的时钟源,还是选的 SW 和滴答定时器:

配置烧写以及时钟

配置模拟量

这里就是配置 ADC 转换相关的内容,这里引脚是电路设计到了 PB0 和 PB1,分别是通道 8 和通道 9,我这边最后是初始化在了 ADC1 上,同时还开启了 DMA 的转换。

先是在 ADC 中选择 ADC1,然后勾选 IN8 和 IN9 :
开启 ADC1 CH8、CH9
同时还需要适配一下的参数:独立模式,分频为 6,12位分辨率,右对齐,打开连续转换,所有转换完成才有 EOC 标志,其余的全部关闭;然后设置连续转换通道数为 2,软件触发,两个通道的转换顺序要记住自己的设置,之后会用到;最后一个关键点就是,采样一定要选择最大的,这里如果选择默认的,会因为采样时间间隔太短而造成两个通道的结果相互影响!

之后配置 DMA ,这里只要打开 DMA 其余保持默认即可,我这里最后改了一个优先级为 High:
配置 ADC1 的 DMA 传输
最后把对应的 IO 口配置一下,已经默认配置成了模拟输入,无上下拉,只要把标签名字改成自己顺手的或者认得出的就行:
GPIO 修改
至此,ADC 配置部分就完成啦。

定时器配置(直流有刷电机相关)

这里是整个工程的核心内容,因为所有的电机控制都是通过 PWM 波完成,同时为了闭环控制,还得接入编码器的位置信号,这一点也是靠定时器来实现的,接下来就一个个完成配置。

首先配置 PWM 输出相关的内容,这里主要展示配置过程,一样的内容就只放一遍,最后会提具体需要配置的通道。

打开 TIM1,这里四个通道的 PWM 输出都用到了,所以都要打开并且配置参数:
配置定时器1
配置定时器1,四个通道全部配置成 PWM 输出,之后进一步配置;配置分频系数为 1,在 HAL 库中会自动把填入的数字加 1,所以填入 1 - 1,然后配置为向上计数模式,自动重装载值配置为 16800 - 1,同时使能自动装载;之后将所有的通道的 PWM 的模式,全部配置为 PWM 模式 1,输出极性就是默认的高,使能输出预装载。

之后需要修改一下 GPIO 的配置:
定时器的 GPIO 配置
这里,四个引脚都配置成上拉,复用推挽输出,同时输出频率选择最高。

最后来看一下具体都需要配置哪些:TIM1 的 4 个引脚;TIM9 的 2 个引脚,以及 TIM10 和 TIM11 的各 1 个引脚,加起来一共 8 个 PWM 输出引脚,共可控制 4 个直流有刷电机。所有的配置都是一样的,这里就不再贴图赘述了。

定时器配置(编码器相关)

之前的直流有刷电机 PWM 已经配置好了,那么这个时候其实是已经可以完成开环的控制了,但是为了实现速度闭环,我们还需要把编码器信号接进来,这一点也需要定时器的相关配置。

打开 TIM2,配置成编码器模式:
配置定时器的编码器模式
只要在定时器的模式中,最后一个混合模式中选择编码器模式,然后预分频系数同样设置为 1 - 1自动重装载值直接设置为最大的 65536 - 1向上计数;之后将编码器模式,选择为 TI1 和 TI2。这样就可以配置好编码器模式了。

之后再配置一下对应的 GPIO 引脚:
定时器编码器模式的 GPIO 配置
这里与之前是一样的,配置为复用推挽,上拉,输出频率要设置为最高。

这样的话编码器也都设置好了,这里就是使用的 TIM2、TIM3、TIM4、TIM5 一共 4 个定时器,对应的就是 4 个电机的编码器。

定时器配置(舵机相关)

舵机的控制,需要把定时器配置为 PWM 输出,频率需要设置为 50 Hz,所以需要把定时器的 PWM 频率调好,其他的配置跟之前的就是一样的。

这里要注意,一共开启了 TIM8 的 CH1 和 CH2,TIM12 一共 3 个 PWM 输出通道;TIM8 是挂在 APB2 总线上,所以频率是 168MHz,可以设置预分频为 168 - 1,自动重装载为 20000 - 1;TIM12 挂载在 APB1 总线上,频率是 84 MHz,所以配置预分频为 84 - 1,自动重装载为 20000 - 1。其余的配置与之前都是一样的。

通讯配置(CAN)

因为项目中还要控制其他电机,会选用 CAN 的电机控制驱动器,所以还需要配置 CAN 通讯相关的参数。

打开 CAN1 并开始配置:
CAN1 配置

激活 CAN1 之后,设置参数配置波特率,按照图中设计的就可以了,最终配置出来的波特率 = 42 / ((1 + 6 + 7) * 3) = 1 M;之后再使能自动重传就可以了,最后将模式设置为正常模式

这里在 NVIC 中在完成一下设置,需要开启 RX0 的接收中断

之后最后配置一下 GPIO,配置与之前都是一样的,上拉,复用推挽以及输出频率最高

通讯配置(串口)

这里配置一下串口的相关设置,首先串口 1 和串口 3 是一样的配置。

打开串口 1 之后,配置成异步通讯:
串口1配置
配置完之后都取默认的就好了,波特率为 115200,8 位数据位,没有奇偶校验位,最后设置一个 1 位的停止位。最后要在 NVIC 之中,打开串口的接收中断

这里在打开 DMA 完成通讯:
串口的 DMA 配置
需要打开接收和发送的双向 DMA 通道。

GPIO 的配置就是一样的,上拉,推挽输出,输出频率最高。

串口3 也是同样的配置,这里就不写了。

通讯配置(APP控制)

APP 控制是通过串口 2 的配置来完成的。

这里就配置波特率为 9600,其余都一样,同样开启接收中断,但是不需要开启 DMA 的传输,因为处理数据都是一个一个字符处理,没有太大的必要通过 DMA 来加速。

操作系统

这里在最后的大项中,开启 FreeRTOS。

配置 FreeRTOS

配置 GPIO

最后,需要完成 LED、KEY、软件模拟的 MPU6050 的 IIC 通信引脚,以及 OLED 的 IIC 通信引脚的初始化,全部对着下图完成就可以了:
GPIO 配置
最后看一下,有些引脚和默认的不一样,这边再对着确认一下就可以:
CAN1 引脚
TIM 引脚
串口引脚

最后生成工程

这里在 Project Manager 中,直接选择生成的 Makefile 工程:
配置 Makefile 工程
之后在选择一下代码的内容,只需要带上必要的工程代码,然后需要生成 .c 和头文件:
配置代码生成
之后直接右上角,来完成生成就可以啦!

初始化相关修改

ADC

在 ADC 中,需要完成接受数据的编写。

首先需要在初始化的函数最后,也就是 ADC 的初始化函数(最好不要在 MspInit 函数,那个就是初始化 GPIO 相关的内容)可以添加用户代码的地方,添加 HAL_ADC_Start_DMA 开启第一次的 DMA 转换

这里需要添加 ADC 拿出检测数据的函数。这里因为已经开启了 ADC+DMA 的连续转换,需要给定一个缓冲数组之后,开启 DMA 传输,一旦完成了这个数组大小的传输,就会触发中断信号,在中断回调函数中处理好数据之后,再次开启传输即可。

编写的用户层函数如下:

/**
 * @brief       计算ADC的平均值(滤波)
 * @param       * p :存放ADC值的指针地址
 * @note        此函数对电位器、电压对应的ADC值进行滤波,
 *              p[0]-p[1]对应的分别是电位器、电压
 * @retval      无
 */
void calc_adc_val(uint16_t * p)
{
    uint32_t temp[2] = {0,0};
    int i;
    for (i = 0; i < ADC_COLL; i++)                  /* 循环ADC_COLL次取值,累加 */
    {
        temp[0] += adc_buffer[0 + i * ADC_CH_NUM];
        temp[1] += adc_buffer[1 + i * ADC_CH_NUM];
    }
    temp[0] /= ADC_COLL;                            /* 取平均值 */
    temp[1] /= ADC_COLL;
    p[0] = temp[0];                                 /* 存入电位器ADC通道平均值 */
    p[1] = temp[1];                                 /* 存入电压ADC通道平均值 */
    car_select = p[0];                              /* 存入小车选择的参数 */
    Voltage = p[1] * 3.3 * 11.0 * Revise / 4095;    /* 换算当前电压,存入对应参数 */
}

/**
 * @brief       ADC 采集中断服务回调函数
 * @param       无 
 * @retval      无
 */
void HAL_ADC_ConvCpltCallback(ADC_HandleTypeDef* hadc)
{
    if (hadc->Instance == ADC1)
    {
        HAL_ADC_Stop_DMA(&hadc1);                                                               /* 关闭DMA转换 */
        calc_adc_val(adc_value);                                                                /* 计算ADC的平均值 */
        HAL_ADC_Start_DMA(&hadc1, (uint32_t *)&adc_buffer, (uint32_t)(ADC_SUM));                /* 启动DMA转换 */
    }
}

将电压值转换为自己用户层需要的内容。

串口

同样的需要添加开启接收中断的函数,串口 1 和串口 3 需要添加 HAL_UARTEx_ReceiveToIdle_DMA 函数,串口 2 需要添加 HAL_UART_Receive_IT 函数。

串口首先需要重定向 printf,这里 printf 主要用在给 APP 调试,所以重定向到 UART2 上完成,如下:

#if 1

#if (__ARMCC_VERSION >= 6010050)                    /* 使用AC6编译器时 */
__asm(".global __use_no_semihosting\n\t");          /* 声明不使用半主机模式 */
__asm(".global __ARM_use_no_argv \n\t");            /* AC6下需要声明main函数为无参数格式,否则部分例程可能出现半主机模式 */

#else

/* 使用AC5编译器时, 要在这里定义__FILE 和 不使用半主机模式 */
#pragma import(__use_no_semihosting)

struct __FILE
{
    int handle;
    /* Whatever you require here. If the only file you are using is */
    /* standard output using printf() for debugging, no file handling */
    /* is required. */
};

#endif

/* 不使用半主机模式,至少需要重定义_ttywrch\_sys_exit\_sys_command_string函数,以同时兼容AC6和AC5模式 */
int _ttywrch(int ch)
{
    ch = ch;
    return ch;
}

/* 定义_sys_exit()以避免使用半主机模式 */
void _sys_exit(int x)
{
    x = x;
}

char *_sys_command_string(char *cmd, int len)
{
    return NULL;
}

/* FILE 在 stdio.h里面定义. */
FILE __stdout;

/* 重定义fputc函数, printf函数最终会通过调用fputc输出字符串到串口 */
int fputc(int ch, FILE *f)
{
    while ((USART2->SR & 0X40) == 0);               /* 等待上一个字符发送完成 */

    USART2->DR = (uint8_t)ch;                       /* 将要发送的字符 ch 写入到DR寄存器 */
    return ch;
}

之后就需要编写相关的发送函数和接收函数。

发送函数比较简单,调用 UART 的 DMA 传输函数,给定了发送的缓存就可以了;接收的话,就需要自己编写中断回调函数,我这边 UART2 没有开启 DMA,而 UART1 和 UART3 则开启了 DMA,所以这是两个中断回调函数,需要自己查一下对应的回调函数然后具体实现。

接收的中断函数:

/**
 * @brief       串口数据接收回调函数
                数据处理在这里进行
 * @param       huart:串口句柄
 * @retval      无
 */
void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart)
{
    if (huart->Instance == USART2)
    {
        
    }
}

/**
 * @brief       串口数据接收回调函数
                数据处理在这里进入
 * @param       huart:串口句柄
 * @param       Size:收到的数组大小
 * @retval      无
 */
void HAL_UARTEx_RxEventCallback(UART_HandleTypeDef *huart, uint16_t Size)
{
    if(huart->Instance == USART1)
    {
    
    }
    if(huart->Instance == USART3)
    {
        
    }
}

发送函数就比较简单,这里只需要实现 UART1 和 UART3,UART2 到时候直接 printf 就可以了:

/**
 * @brief       串口1发送数据
 * @param       无
 * @retval      无
 */
void USART1_SEND(void)
{
    HAL_UART_Transmit_DMA(&huart1, Send_Data.buffer, SEND_DATA_SIZE);    /* 直接DMA发送 */
}

/**
 * @brief       串口3发送数据
 * @param       无
 * @retval      无
 */
void USART3_SEND(void)
{
    HAL_UART_Transmit_DMA(&huart3, Send_Data.buffer, SEND_DATA_SIZE);    /* 直接DMA发送 */
}

定时器

定时器的配置就是 PWM 和 Encoder 相关的内容。

这里要在所有的 PWM 的配置中,最后加上对 PWM 输出的启动,也就是 HAL_TIM_PWM_Start 函数。而对于编码器的相关初始化,需要在最后添加 HAL_TIM_Encoder_Start 函数。

对于编码器,只要在每一次的调用函数的时候,把自动重装载的寄存器值清零就可以了:

/**
 * @brief       读取编码器参数
 * @param       TIMX: 定时器号
 * @retval      编码器数值
 */
int Read_Encoder(uint8_t TIMX)
{
  int Encoder_TIM;
  switch(TIMX)
  {
    case 2:
      Encoder_TIM = __HAL_TIM_GET_COUNTER(&htim2);
      __HAL_TIM_SET_COUNTER(&htim2, 0);
      break;
    case 3:
      Encoder_TIM = __HAL_TIM_GET_COUNTER(&htim3);
      __HAL_TIM_SET_COUNTER(&htim3, 0);
      break;
    case 4:
      Encoder_TIM = __HAL_TIM_GET_COUNTER(&htim4);
      __HAL_TIM_SET_COUNTER(&htim4, 0);
      break;
    case 5:
      Encoder_TIM = __HAL_TIM_GET_COUNTER(&htim5);
      __HAL_TIM_SET_COUNTER(&htim5, 0);
      break;
    default: Encoder_TIM = 0;
  }
  return Encoder_TIM;
}

之后还需要编写调整 PWM 占空比的函数,这个比较简单,如下:

/**
 * @brief       舵机控制
 * @param       para: pwm比较值
 * @note        根据传入的参数控制舵机角度
 * @retval      无
 */
void servo_pwm_set(float para)
{
  int val = (int)para;

  __HAL_TIM_SetCompare(&htim12, TIM_CHANNEL_2, val);
}

其他的就是类似的完成编写就好了。

CAN

CAN 的配置相对复杂,还需要添加相关的过滤器配置,然后最后开启 CAN 传输,如下:

/* 配置CAN过滤器 */
  sFilterConfig.FilterBank = 0;                         /* 过滤器0 */
  sFilterConfig.FilterMode = CAN_FILTERMODE_IDMASK;     /* 标识符屏蔽位模式 */
  sFilterConfig.FilterScale = CAN_FILTERSCALE_32BIT;    /* 长度32位位宽*/
  sFilterConfig.FilterIdHigh = 0x0000;                  /* 32位ID */
  sFilterConfig.FilterIdLow = 0x0000;
  sFilterConfig.FilterMaskIdHigh = 0x0000;              /* 32位MASK */
  sFilterConfig.FilterMaskIdLow = 0x0000;
  sFilterConfig.FilterFIFOAssignment = CAN_FILTER_FIFO0;    /* 过滤器0关联到FIFO0 */
  sFilterConfig.FilterActivation = CAN_FILTER_ENABLE;       /* 激活滤波器0 */
  sFilterConfig.SlaveStartFilterBank = 14;

  /* 过滤器配置 */
  if (HAL_CAN_ConfigFilter(&hcan1, &sFilterConfig) != HAL_OK)
  {
    Error_Handler();
  }

  /* 启动CAN外围设备 */
  if (HAL_CAN_Start(&hcan1) != HAL_OK)
  {
    Error_Handler();
  }
  
  /* 开启CAN接收中断 */
  __HAL_CAN_ENABLE_IT(&hcan1, CAN_IT_RX_FIFO0_MSG_PENDING); /* FIFO0消息挂号中断允许 */

这一部分完成之后,需要分别编写 CAN 的发送和接收函数。

这里规定,所有的数据传输,全部是标准帧和数据帧,之后会比较方便来定义传输。首先是发送函数,只要配置好相关的格式,然后把数据填入数据帧就可以了:

/**
 * @brief       CAN 发送一组数据
 *   @note      发送格式固定为: 标准ID, 数据帧
 * @param       id  : 标准ID(11位)
 * @param       msg : CAN数据缓存数组指针
 * @param       len : 数据长度
 * @retval      发送状态 0, 未发送到邮箱; 其他, 发送成功邮箱号;
 */
uint8_t can_send_msg(uint32_t id, uint8_t *msg, uint8_t len)
{
    uint32_t TxMailbox = CAN_TX_MAILBOX0;
    /* 选择发送邮箱 */
    if (__HAL_CAN_GET_FLAG(&hcan1, CAN_FLAG_TME0) != RESET) TxMailbox = CAN_TX_MAILBOX0;
    else if (__HAL_CAN_GET_FLAG(&hcan1, CAN_FLAG_TME1) != RESET) TxMailbox = CAN_TX_MAILBOX1;
    else if (__HAL_CAN_GET_FLAG(&hcan1, CAN_FLAG_TME2) != RESET) TxMailbox = CAN_TX_MAILBOX2;
    else return 0;

    /* 配置发送 */
    hcan1_txheader.StdId = id;         /* 标准标识符 */
    hcan1_txheader.ExtId = id;         /* 扩展标识符(29位) 标准标识符情况下,该成员无效*/
    hcan1_txheader.IDE = CAN_ID_STD;   /* 使用标准标识符 */
    hcan1_txheader.RTR = CAN_RTR_DATA; /* 数据帧 */
    hcan1_txheader.DLC = len;

    if (HAL_CAN_AddTxMessage(&hcan1, &hcan1_txheader, msg, &TxMailbox) != HAL_OK) /* 发送消息 */
    {
        return 0;
    }

    return TxMailbox;
}

这里添加了对于发送邮箱的选择,因为一共有 3 个发送邮箱,所以可以判断哪个邮箱空闲就用那个,除非都被占用了那就直接 return 掉,之后重新调用。

对于接收会更加简单,只要写好对应的中断回调函数就好,对应的逻辑是一样的:

/**
 * @brief       CAN 接收数据查询
 *   @note      接收数据格式固定为: 标准ID, 数据帧
 * @param       id      : 要查询的 标准ID(11位)
 * @param       buf     : 数据缓存区
 * @retval      接收结果
 *   @arg       0   , 无数据被接收到;
 *   @arg       其他, 接收的数据长度
 */
uint8_t can_receive_msg(uint32_t id, uint8_t *buf)
{
  if (HAL_CAN_GetRxFifoFillLevel(&hcan1, CAN_RX_FIFO0) == 0)     /* 没有接收到数据 */
  {
    return 0;
  }

  if (HAL_CAN_GetRxMessage(&hcan1, CAN_RX_FIFO0, &hcan1_rxheader, buf) != HAL_OK)  /* 读取数据 */
  {
    return 0;
  }
  
  if (hcan1_rxheader.StdId!= id || hcan1_rxheader.IDE != CAN_ID_STD || hcan1_rxheader.RTR != CAN_RTR_DATA)       /* 接收到的ID不对 / 不是标准帧 / 不是数据帧 */
  {
    return 0;    
  }

  return hcan1_rxheader.DLC;
}

/**
 * @brief       CAN数据接收回调函数
                数据处理在这里进入
 * @param       hcan:CAN句柄
 * @retval      无
 */
void HAL_CAN_RxFifo0MsgPendingCallback(CAN_HandleTypeDef *hcan)
{
    if (hcan->Instance == CAN1)
    {
        uint32_t id = 0;
        uint8_t can_buffer[8] = {0};
        
        can_receive_msg(id, can_buffer);
        if (id == 0x181)
        {
            
        }
        /* 开启CAN接收中断 */
        __HAL_CAN_ENABLE_IT(&hcan1, CAN_IT_RX_FIFO0_MSG_PENDING); /* FIFO0消息挂号中断允许 */
    }
}

以上分别是对于 CAN 的接收,调用读取函数来完成判断,是否满足 id 号以及标准帧和数据帧,之后在中断回调之中加入逻辑代码处理数据即可。

GPIO

这里就是添加 LED 以及 KEY 的配置,这个就直接把正点原子的按键函数搬过来,LED 可以直接开一个 FreeRTOS 的任务:

/**
 * @brief       按键扫描函数
 * @param       mode:0 / 1, 具体含义如下:
 *   @arg       0,  不支持连续按(当按键按下不放时, 只有第一次调用会返回键值,
 *                  必须松开以后, 再次按下才会返回其他键值)
 *   @arg       1,  支持连续按(当按键按下不放时, 每次调用该函数都会返回键值)
 * @retval      键值
 */
uint8_t click_key(uint8_t mode)
{
    static uint8_t key_up = 1;  /* 按键按松开标志 */
    uint8_t keyval = 0;

    if (mode) key_up = 1;       /* 支持连按 */

    if (key_up && KEY == 0)  /* 按键松开标志为1, 且有任意一个按键按下了 */
    {
        vTaskDelay(10);           /* 去抖动 */
        key_up = 0;
        keyval = 1;
    }
    else if (KEY == 1) /* 没有任何按键按下, 标记按键松开 */
    {
        key_up = 1;
    }

    return keyval;              /* 返回键值 */
}

/**
 * @brief       led_task
 * @param       pvParameters : 传入参数(未用到)
 * @retval      无
 */
void led_task(void *pvParameters)
{
    while (1)
    {
        LED_TOGGLE();
        vTaskDelay(Led_Count);
    }
}

通过对 vTaskDelay 函数填入不同的延时数字大小,来控制具体的闪烁频率。

IMU

对于 IMU,采用的时 MPU6050 的传感器,是通过 IIC 来完成通讯的,这里可以直接对着 IIC 的时序来完成软件模拟,我就把我自己修改过的版本贴上来:

#include "I2C.h"

/* MPU6050 IIC IO操作 */
#define IIC_SDA(x)     do{ x ? \
                          HAL_GPIO_WritePin(IIC_SDA_GPIO_Port, IIC_SDA_Pin, GPIO_PIN_SET) : \
                          HAL_GPIO_WritePin(IIC_SDA_GPIO_Port, IIC_SDA_Pin, GPIO_PIN_RESET); \
                       }while(0)
#define IIC_SCL(x)     do{ x ? \
                          HAL_GPIO_WritePin(IIC_SCL_GPIO_Port, IIC_SCL_Pin, GPIO_PIN_SET) : \
                          HAL_GPIO_WritePin(IIC_SCL_GPIO_Port, IIC_SCL_Pin, GPIO_PIN_RESET); \
                       }while(0)
#define READ_SDA       HAL_GPIO_ReadPin(IIC_SDA_GPIO_Port, IIC_SDA_Pin)
#define IIC_SDA_IN()   {IIC_SDA_GPIO_Port->MODER&=~(3<<(11*2));IIC_SDA_GPIO_Port->MODER|=0<<11*2;}
#define IIC_SDA_OUT()  {IIC_SDA_GPIO_Port->MODER&=~(3<<(11*2));IIC_SDA_GPIO_Port->MODER|=1<<11*2;}

/**
 * @brief       产生IIC起始信号
 * @param       无
 * @retval      无
 */
void I2C_Start(void)
{
    IIC_SDA_OUT();
    IIC_SDA(1);
    if(!READ_SDA) return;
    IIC_SCL(1);
    delay_us(1);
    IIC_SDA(0);
    if(READ_SDA) return;
    delay_us(1);
    IIC_SCL(0);
    return ;
}

/**
 * @brief       产生IIC停止信号
 * @param       无
 * @retval      无
 */
void I2C_Stop(void)
{
    IIC_SDA_OUT();
    IIC_SCL(0);
    IIC_SDA(0);
    delay_us(1);
    IIC_SCL(1); 
    IIC_SDA(1);
    delay_us(1);
}

/**
 * @brief       等待IIC应答信号
 * @param       无
 * @retval      1: 应答信号接收成功
 *              0: 应答信号接收失败
 */
uint8_t I2C_WaiteForAck(void)
{
    uint8_t ucErrTime = 0;
    IIC_SDA_IN();
    IIC_SDA(1);
    delay_us(1);
    IIC_SCL(1);
    delay_us(1);
    while(READ_SDA)
    {
        ucErrTime++;
        if(ucErrTime > 50)
        {
            I2C_Stop();
            return 0;
        }
      delay_us(1);
    }
    IIC_SCL(0);
    return 1;
}

/**
 * @brief       产生ACK应答信号
 * @param       无
 * @retval      无
 */
void I2C_Ack(void)
{
    IIC_SCL(0);
    IIC_SDA_OUT();
    IIC_SDA(0);
    delay_us(1);
    IIC_SCL(1);
    delay_us(1);
    IIC_SCL(0);
}

/**
 * @brief       不产生ACK应答信号
 * @param       无
 * @retval      无
 */  
void I2C_NAck(void)
{
    IIC_SCL(0);
    IIC_SDA_OUT();
    IIC_SDA(1);
    delay_us(1);
    IIC_SCL(1);
    delay_us(1);
    IIC_SCL(0);
}

/**
 * @brief       IIC发送数据
 */
uint8_t I2C_WriteOneBit(uint8_t DevAddr, uint8_t RegAddr, uint8_t BitNum, uint8_t Data)
{
    uint8_t Dat;
    
    Dat =I2C_ReadOneByte(DevAddr, RegAddr);
    Dat = (Data != 0) ? (Dat | (1 << BitNum)):
    (Dat & ~(1 << BitNum));
    I2C_WriteOneByte(DevAddr, RegAddr, Dat);
    
    return 1;
}

uint8_t I2C_WriteBits(uint8_t DevAddr, uint8_t RegAddr, uint8_t BitStart, uint8_t Length, uint8_t Data)
{

    uint8_t Dat, Mask;
    
    Dat = I2C_ReadOneByte(DevAddr, RegAddr);
    Mask = (0xFF << (BitStart + 1)) | 0xFF >> ((8 - BitStart) + Length - 1);
    Data <<= (8 - Length);
    Data >>= (7 - BitStart);
    Dat &= Mask;
    Dat |= Data;
    I2C_WriteOneByte(DevAddr, RegAddr, Dat);
    
    return 1;
}

/**
 * @brief       IIC发送一个字节
 * @param       Data: 要发送的数据
 * @retval      无
 */
void I2C_WriteByte(uint8_t Data)
{
    uint8_t t;
    IIC_SDA_OUT();
    IIC_SCL(0); /* 拉低时钟开始数据传输 */
    for(t=0;t<8;t++)
    {              
        IIC_SDA((Data&0x80)>>7);
        Data<<=1;
        delay_us(1);
        IIC_SCL(1);
        delay_us(1); 
        IIC_SCL(0);	
        delay_us(1);
    }
}


uint8_t I2C_WriteOneByte(uint8_t DevAddr, uint8_t RegAddr, uint8_t Data)
{
    I2C_Start();
    I2C_WriteByte(DevAddr | I2C_Direction_Transmitter);
    I2C_WaiteForAck();
    I2C_WriteByte(RegAddr);
    I2C_WaiteForAck();
    I2C_WriteByte(Data);
    I2C_WaiteForAck();
    I2C_Stop();
    return 1;
}

/**
 * @brief       IIC接收一个字节
 * @param       ack: ack=1时,发送ack; ack=0时,发送nack
 * @retval      接收到的数据
 */
uint8_t I2C_ReadByte(uint8_t Ack)
{
    uint8_t i, RecDat = 0;

    IIC_SDA_IN();
    for(i = 0; i < 8; i ++)
    {
        IIC_SCL(0);
        delay_us(1);
        IIC_SCL(1);
        RecDat <<= 1;
        if(READ_SDA)
            RecDat |= 0x01;
        delay_us(1);
    }
    if(I2C_ACK == Ack)
        I2C_Ack();
    else
        I2C_NAck();

    return RecDat;
}

uint8_t I2C_ReadOneByte(uint8_t DevAddr, uint8_t RegAddr)
{
    uint8_t TempVal = 0;

    I2C_Start();
    I2C_WriteByte(DevAddr | I2C_Direction_Transmitter);
    I2C_WaiteForAck();
    I2C_WriteByte(RegAddr);
    I2C_WaiteForAck();
    I2C_Start();
    I2C_WriteByte(DevAddr | I2C_Direction_Receiver);
    I2C_WaiteForAck();
    TempVal = I2C_ReadByte(I2C_NACK);
    I2C_Stop();

    return TempVal;
}

这里要注意,我一开始是对着正点原子那个版本,在之前做平衡小车的时候就有用过,但是发现他的更新会稍微慢一点。这一次我仔细研究了一下,现在这个版本之所以能更新快一点,就是因为每一次在完成 IIC 的时候,来把 SDA 引脚的输入输出模式改变,就可以加速数据获取。以上修改之后就完成了对于 IMU 读取的 IIC 时序的软件模拟。

之后需要写好 MPU6050 的驱动,这个有很多现成的,我修改精简了一下:

#include "MPU6050.h"
#include "I2C.h"
    
short gyro[3], accel[3];
/* 零点漂移计数 */
int Deviation_Count = 0;
/* 陀螺仪静差,原始数据 */
short Deviation_gyro[3],Original_gyro[3];  
short Deviation_accel[3],Original_accel[3];

void MPU6050_task(void *pvParameters)
{
    uint32_t lastWakeTime = getSysTickCnt();
    while(1)
    {
        /* 此任务以100Hz的频率运行 */
        vTaskDelayUntil(&lastWakeTime, F2T(RATE_100_HZ));
        
        /* 开机前,读取陀螺仪零点 */
        if (Deviation_Count < CONTROL_DELAY)
        {
            Deviation_Count++;
            memcpy(Deviation_gyro,gyro,sizeof(gyro));
            memcpy(Deviation_accel,accel,sizeof(accel));
        }

        MPU_Get_Gyroscope();  /* 得到陀螺仪数据 */
        MPU_Get_Accelscope(); /* 获得加速度计值(原始值) */
    }
}

/**
 * @brief       MPU6050设置陀螺仪传感器量程范围
 * @param       range: MPU6050_GYRO_FS_250 ±250dps
 *                     MPU6050_RA_GYRO_CONFIG ±500dps
 *                     MPU6050_RA_GYRO_CONFIG ±500dps
 *                     MPU6050_GCONFIG_FS_SEL_LENGTH ±2000dps
 */
void MPU6050_setFullScaleGyroRange(uint8_t range)
{
    I2C_WriteBits(devAddr, MPU6050_RA_GYRO_CONFIG, MPU6050_GCONFIG_FS_SEL_BIT, MPU6050_GCONFIG_FS_SEL_LENGTH, range);
}

/**
 * @brief       MPU6050设置加速度传感器量程范围
 * @param       range:MPU6050_ACCEL_FS_2 ±2G
 *                     MPU6050_ACCEL_FS_4 ±4G
 *                     MPU6050_ACCEL_FS_8 ±8G
 *                     MPU6050_ACCEL_FS_16 ±16G
 * @retval      无
 */
void MPU6050_setFullScaleAccelRange(uint8_t range)
{
    I2C_WriteBits(devAddr, MPU6050_RA_ACCEL_CONFIG, MPU6050_ACONFIG_AFS_SEL_BIT, MPU6050_ACONFIG_AFS_SEL_LENGTH, range);
}

/**
 * @brief       MPU6050初始化
 * @param       无
 * @retval      0,正确初始化;1,初始化失败
 */
uint8_t MPU6050_initialize(void) 
{
    uint8_t res;
    /* MPU6050 软件复位 */
    I2C_WriteOneByte(devAddr, MPU6050_RA_PWR_MGMT_1,0X80);
    vTaskDelay(200);
    I2C_WriteOneByte(devAddr, MPU6050_RA_PWR_MGMT_1,0X00);

    MPU6050_setFullScaleGyroRange(MPU6050_GYRO_FS_500);           /* 陀螺仪传感器,±500dps=±500°/s ±32768 (gyro/32768*500)*PI/180(rad/s)=gyro/3754.9(rad/s) */
    MPU6050_setFullScaleAccelRange(MPU6050_ACCEL_FS_2);           /* 加速度传感器,±2g=±2*9.8m/s^2 ±32768 accel/32768*19.6=accel/1671.84 */
    MPU6050_Set_Rate(50);                                         /* 采样率,50Hz */

    I2C_WriteOneByte(devAddr, MPU6050_RA_INT_ENABLE, 0X00);       /* 关闭所有中断 */
    I2C_WriteOneByte(devAddr, MPU6050_RA_USER_CTRL, 0X00);        /* 关闭IIC主模式 */
    I2C_WriteOneByte(devAddr, MPU6050_RA_FIFO_EN, 0X00);          /* 关闭FIFO */
    I2C_WriteOneByte(devAddr, MPU6050_RA_INT_PIN_CFG, 0X80);      /* INT引脚低电平有效 */
    res = I2C_ReadOneByte(devAddr, MPU6050_RA_WHO_AM_I);          /* 读取设备ID */
    if (res == MPU6050_DEFAULT_ADDRESS)                           /* 器件ID正确, 器件ID的正确取决于AD引脚 */
    {
        I2C_WriteOneByte(devAddr, MPU6050_RA_PWR_MGMT_1, 0X01);   /* 设置CLKSEL,PLL X轴为参考 */
        I2C_WriteOneByte(devAddr, MPU6050_RA_PWR_MGMT_2, 0X00);   /* 加速度与陀螺仪都工作 */
        MPU6050_Set_Rate(50);                                     /* 采样率,50Hz */
    } else return 1;
    return 0;
}

/**
 * @brief       MPU6050获取温度值
 * @param       无
 * @retval      摄氏温度(扩大了10倍)
 */
int Read_Temperature(void)
{
    float Temp;
    Temp=(I2C_ReadOneByte(devAddr,MPU6050_RA_TEMP_OUT_H)<<8)+I2C_ReadOneByte(devAddr,MPU6050_RA_TEMP_OUT_L);
    if (Temp > 32768) Temp -= 65536;        /* 数据类型转换 */
    Temp = (36.53f + Temp / 340) * 10;      /* 温度放大十倍存放 */
    return (int)Temp;
}

/**************************************************************************
Function: Initialize TIM2 as the encoder interface mode
Input   : LPF: Digital low-pass filtering frequency (Hz)
Output  : 0: Settings successful, others: Settings failed
函数功能:设置MPUrobot_select_init.h的数字低通滤波器
入口参数:lpf:数字低通滤波频率(Hz)
返回  值:0:设置成功, 其他:设置失败
**************************************************************************/
/**
 * @brief       MPU6050设置数字低通滤波器频率
 * @param       lpf: 数字低通滤波器的频率(Hz)
 * @retval      0:设置成功, 其他:设置失败
 */
uint8_t MPU6050_Set_LPF(uint16_t lpf)
{
    uint8_t data = 0;
    if (lpf >= 188) data = 1;
    else if (lpf >= 98) data = 2;
    else if (lpf >=  42) data = 3;
    else if (lpf >= 20) data = 4;
    else if (lpf >= 10) data = 5;
    else data = 6; 
    return I2C_WriteOneByte(devAddr, MPU6050_RA_CONFIG, data); /* 设置数字低通滤波器 */
}

/**
 * @brief       MPU6050设置采样率
 * @param       rate: 采样率(4~1000Hz)
 * @retval      0:设置成功, 其他:设置失败
 */
uint8_t MPU6050_Set_Rate(uint16_t rate)
{
    uint8_t data;
    if (rate > 1000) rate = 1000;
    if (rate < 4) rate = 4;
    data = 1000 / rate - 1;
    data = I2C_WriteOneByte(devAddr, MPU6050_RA_SMPLRT_DIV, data); /* 设置数字低通滤波器 */ 
    return MPU6050_Set_LPF(rate / 2); /* 自动设置LPF为采样率的一半 */
}

/**
 * @brief       MPU6050获取陀螺仪值(原始值)
 * @param       无
 * @retval      无
 */
void MPU_Get_Gyroscope(void)
{
    gyro[0] = (I2C_ReadOneByte(devAddr, MPU6050_RA_GYRO_XOUT_H) << 8) + I2C_ReadOneByte(devAddr, MPU6050_RA_GYRO_XOUT_L);    /* 读取X轴陀螺仪 */
    gyro[1] = (I2C_ReadOneByte(devAddr, MPU6050_RA_GYRO_YOUT_H) << 8) + I2C_ReadOneByte(devAddr, MPU6050_RA_GYRO_YOUT_L);    /* 读取Y轴陀螺仪 */
    gyro[2] = (I2C_ReadOneByte(devAddr, MPU6050_RA_GYRO_ZOUT_H) << 8) + I2C_ReadOneByte(devAddr, MPU6050_RA_GYRO_ZOUT_L);    /* 读取Z轴陀螺仪 */

    if (Deviation_Count < CONTROL_DELAY) /* 开机前10秒 */
    {
        Led_Count=1; /* LED高频闪烁 */
        Flag_Stop=1; /* 软件失能标志位置1 */
    }
    else
    {  
        if (Deviation_Count == CONTROL_DELAY)
        Flag_Stop=0;    /* 软件失能标志位置0 */
        Led_Count=300;  /* LED恢复正常闪烁频率 */
        
        /* 保存原始数据用于单击用户按键更新零点 */
        Original_gyro[0] = gyro[0];  
        Original_gyro[1] = gyro[1];  
        Original_gyro[2] = gyro[2];
        
        /* 去除零点漂移的数据 */
        gyro[0] = Original_gyro[0] - Deviation_gyro[0];  
        gyro[1] = Original_gyro[1] - Deviation_gyro[1];  
        gyro[2] = Original_gyro[2] - Deviation_gyro[2];
    }
}

/**
 * @brief       MPU6050获取加速度值
 * @param       无
 * @retval      加速度计值(带符号原始值)
 */
void MPU_Get_Accelscope(void)
{
    accel[0] = (I2C_ReadOneByte(devAddr, MPU6050_RA_ACCEL_XOUT_H) << 8) + I2C_ReadOneByte(devAddr, MPU6050_RA_ACCEL_XOUT_L); /* 读取X轴加速度计 */
    accel[1] = (I2C_ReadOneByte(devAddr, MPU6050_RA_ACCEL_YOUT_H) << 8) + I2C_ReadOneByte(devAddr, MPU6050_RA_ACCEL_YOUT_L); /* 读取X轴加速度计 */
    accel[2] = (I2C_ReadOneByte(devAddr, MPU6050_RA_ACCEL_ZOUT_H) << 8) + I2C_ReadOneByte(devAddr, MPU6050_RA_ACCEL_ZOUT_L); /* 读取Z轴加速度计 */

    if (Deviation_Count < CONTROL_DELAY) /* 开机前10秒 */
    {
        /* MPU_Get_Gyroscope中处理过,直接返回 */
        return;
    }
    else
    {
        /* 保存原始数据用于单击用户按键更新零点 */
        Original_accel[0] = accel[0];  
        Original_accel[1] = accel[1];  
        Original_accel[2] = accel[2];
        
        /* 去除零点漂移的数据 */
        accel[0] = Original_accel[0] - Deviation_accel[0];  
        accel[1] = Original_accel[1] - Deviation_accel[1];  
        accel[2] = Original_accel[2] - Deviation_accel[2] + 16384;
    }
}

这里直接是开了一个 FreeRTOS 的任务来完成 IMU 的轮询读取。这里在读取的时候,还加上了零点漂移的函数,需要在一开始开机的时候,保持开发板的平地上静止,以此来完成标零,避免零点漂移

OLED

这个也是通用的函数,写的 IIC 然后来完成内容的显示:

#include "oled.h"
#include "oledfont.h"

uint8_t OLED_GRAM[128][8]; 

/**
 * @brief       刷新OLED屏幕
 * @param       无
 * @retval      无
 */
void OLED_Refresh_Gram(void)
{
    uint8_t i, n;
    for(i = 0; i < 8; i++)  
    {  
        OLED_WR_Byte(0xb0+i, OLED_CMD);    /* 设置页地址(0~7)*/
        OLED_WR_Byte(0x00, OLED_CMD);      /* 设置显示位置—列低地址 */
        OLED_WR_Byte(0x10, OLED_CMD);      /* 设置显示位置—列高地址 */
        for(n = 0; n < 128; n++) OLED_WR_Byte(OLED_GRAM[n][i], OLED_DATA); 
    }
}

/**
 * @brief       向OLED写入一个字节
 * @param       dat : 写入的数据/命令
 * @param       cmd : 数据/命令标志 0,表示命令;1,表示数据
 * @retval      无
 */
void OLED_WR_Byte(uint8_t dat,uint8_t cmd)
{
    uint8_t i;
    if (cmd)
        OLED_RS(1);
    else 
        OLED_RS(0);
    for (i = 0; i < 8; i++)
    {
        OLED_SCLK(0);
        if (dat & 0x80)
            OLED_SDIN(1);
        else 
            OLED_SDIN(0);
        OLED_SCLK(1);
        dat <<= 1;   
    }
    OLED_RS(1);
} 

/**
 * @brief       开启OLED显示
 * @param       无
 * @retval      无
 */
void OLED_Display_On(void)
{
    OLED_WR_Byte(0X8D, OLED_CMD);
    OLED_WR_Byte(0X14, OLED_CMD);
    OLED_WR_Byte(0XAF, OLED_CMD);
}

/**
 * @brief       关闭OLED显示
 * @param       无
 * @retval      无
 */
void OLED_Display_Off(void)
{
    OLED_WR_Byte(0X8D, OLED_CMD);
    OLED_WR_Byte(0X10, OLED_CMD);
    OLED_WR_Byte(0XAE, OLED_CMD);
}

/**
 * @brief       清屏OLED,全黑
 * @param       无
 * @retval      无
 */
void OLED_Clear(void)  
{  
    uint8_t i, n;  
    for(i = 0; i < 8; i++)
        for(n = 0; n < 128; n++) OLED_GRAM[n][i] = 0X00;  
    OLED_Refresh_Gram(); /* 更新显示 */
}

/**
 * @brief       画点
 * @param       x, y: 起点坐标
 * @param       t: 1,填充;0,清空
 * @retval      无
 */
void OLED_DrawPoint(uint8_t x,uint8_t y,uint8_t t)
{
    uint8_t pos, bx, temp = 0;
    if (x > 127 || y > 63) return;
    pos = 7 - y / 8;
    bx = y % 8;
    temp = 1 << (7-bx);
    if (t) OLED_GRAM[x][pos] |= temp;
    else OLED_GRAM[x][pos] &= ~temp;
}

/**
 * @brief       在指定位置显示一个字符,包括部分字符
 * @param       x, y: 起点坐标
 * @param       len : 数字的位数
 * @param       size: 字体大小
 * @param       mode: 0, 反白显示;1,正常显示
 * @retval      无
 */
void OLED_ShowChar(uint8_t x, uint8_t y, uint8_t chr, uint8_t size, uint8_t mode)
{
    uint8_t temp, t, t1;
    uint8_t y0 = y;
    chr = chr - ' '; /* 得到偏移后的值 */
    for (t = 0; t < size; t++)
    {
        if (size == 12) temp = oled_asc2_1206[chr][t];  /* 调用1206字体 */
        else temp = oled_asc2_1608[chr][t];             /* 调用1608字体 */
        for (t1 = 0; t1 < 8; t1++)
        {
            if (temp & 0x80) OLED_DrawPoint(x, y, mode);
            else OLED_DrawPoint(x, y, !mode);
            temp <<= 1;
            y++;
            if ((y-y0) == size)
            {
                y = y0;
                x++;
                break;
            }
        }
    }
}

/**
 * @brief       求m的n次方
 * @param       m:底数
 * @param       n:次方数
 * @retval      乘方数
 */
uint32_t oled_pow(uint8_t m,uint8_t n)
{
    uint32_t result = 1;
    while (n--) result *= m;    
    return result;
}

/**
 * @brief       显示2个数字
 * @param       x, y: 起点坐标
 * @param       len : 数字的位数
 * @param       size: 字体大小
 * @param       mode: 0, 填充显示;1,叠加模式
 * @param       num : 数值(0~4294967295)
 * @retval      无
 */
void OLED_ShowNumber(uint8_t x,uint8_t y,uint32_t num,uint8_t len,uint8_t size)
{
    uint8_t t, temp;
    uint8_t enshow = 0;
    for (t = 0; t < len; t++)
    {
        temp = (num / oled_pow(10, len-t-1)) % 10;
        if (enshow == 0 && t < (len - 1))
        {
            if (!temp)
            {
                OLED_ShowChar(x + (size / 2) * t, y, ' ', size, 1);
                continue;
            } else enshow = 1; 
        }
        OLED_ShowChar(x + (size / 2) * t, y, temp + '0', size, 1); 
    }
} 

/**
 * @brief       显示2个数字
 * @param       x, y: 起点坐标
 * @param       *p  : 字符串起始地址 
 * @retval      无
 */
void OLED_ShowString(uint8_t x,uint8_t y,const char *p)
{
#define MAX_CHAR_POSX 122
#define MAX_CHAR_POSY 58          
    while (*p != '\0')
    {       
        if (x > MAX_CHAR_POSX) {x = 0; y += 16;}
        if (y > MAX_CHAR_POSY) {y = x = 0; OLED_Clear();}
        OLED_ShowChar(x, y, *p, 12, 1);
        x += 8;
        p++;
    }  
}

/**
 * @brief       初始化OLED
 * @param       无
 * @retval      无
 */
void OLED_Init(void)
{
    OLED_RST(0);
    vTaskDelay(100);
    OLED_RST(1); 
 
    OLED_WR_Byte(0xAE,OLED_CMD); /* 关闭显示 */
    OLED_WR_Byte(0xD5,OLED_CMD); /* 设置时钟分频因子,震荡频率 */
    OLED_WR_Byte(80,OLED_CMD);   /* [3:0],分频因子;[7:4],震荡频率 */
    OLED_WR_Byte(0xA8,OLED_CMD); /* 设置驱动路数 */
    OLED_WR_Byte(0X3F,OLED_CMD); /* 默认0X3F(1/64) */
    OLED_WR_Byte(0xD3,OLED_CMD); /* 设置显示偏移 */
    OLED_WR_Byte(0X00,OLED_CMD); /* 默认为0 */

    OLED_WR_Byte(0x40,OLED_CMD); /* 设置显示开始行 [5:0],行数 */
    
    OLED_WR_Byte(0x8D,OLED_CMD); /* 电荷泵设置 */
    OLED_WR_Byte(0x14,OLED_CMD); /* bit2,开启/关闭 */
    OLED_WR_Byte(0x20,OLED_CMD); /* 设置内存地址模式 */
    OLED_WR_Byte(0x02,OLED_CMD); /* [1:0],00,列地址模式;01,行地址模式;10,页地址模式;默认10; */
    OLED_WR_Byte(0xA1,OLED_CMD); /* 段重定义设置,bit0:0,0->0;1,0->127; */
    OLED_WR_Byte(0xC0,OLED_CMD); /* 设置COM扫描方向;bit3:0,普通模式;1,重定义模式 COM[N-1]->COM0;N:驱动路数 */
    OLED_WR_Byte(0xDA,OLED_CMD); /* 设置COM硬件引脚配置 */
    OLED_WR_Byte(0x12,OLED_CMD); /* [5:4]配置 */
     
    OLED_WR_Byte(0x81,OLED_CMD); /* 对比度设置 */
    OLED_WR_Byte(0xEF,OLED_CMD); /* 1~255;默认0X7F (亮度设置,越大越亮) */
    OLED_WR_Byte(0xD9,OLED_CMD); /* 设置预充电周期 */
    OLED_WR_Byte(0xf1,OLED_CMD); /* [3:0],PHASE 1;[7:4],PHASE 2; */
    OLED_WR_Byte(0xDB,OLED_CMD); /* 设置VCOMH 电压倍率 */
    OLED_WR_Byte(0x30,OLED_CMD); /* [6:4] 000,0.65*vcc;001,0.77*vcc;011,0.83*vcc; */

    OLED_WR_Byte(0xA4,OLED_CMD); /* 全局显示开启;bit0:1,开启;0,关闭;(白屏/黑屏) */
    OLED_WR_Byte(0xA6,OLED_CMD); /* 设置显示方式;bit0:1,反相显示;0,正常显示 */
    OLED_WR_Byte(0xAF,OLED_CMD); /* 开启显示	 */
    OLED_Clear();
}

同样也是软件模拟的 IIC 时序,然后完成一系列配置之后,并且显示对于屏幕的点阵。

PS2

最后就是手柄的遥控,这个就是根据手柄的时序,来完成 GPIO 的模拟:

#include "pstwo.h"
#define DELAY_TIME  delay_us(5); 

/* 按键值读取,零时存储 */
uint16_t Handkey;
/* 开始命令。请求数据 */
uint8_t Comd[2] = {0x01,0x42};
/* 数据存储数组 */
uint8_t Data[9] = {0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00}; 

uint16_t MASK[]={
    PSB_SELECT,
    PSB_L3,
    PSB_R3 ,
    PSB_START,
    PSB_PAD_UP,
    PSB_PAD_RIGHT,
    PSB_PAD_DOWN,
    PSB_PAD_LEFT,
    PSB_L2,
    PSB_R2,
    PSB_L1,
    PSB_R1 ,
    PSB_GREEN,
    PSB_RED,
    PSB_BLUE,
    PSB_PINK
};  /* 按键值与按键名 */

/**
 * @brief       pstow_task
 * @param       pvParameters : 传入参数(未用到)
 * @retval      无
 */
void pstwo_task(void *pvParameters)
{
    uint32_t lastWakeTime = getSysTickCnt();
    while(1)
    {
        /* 此任务以100Hz的频率运行 */
        vTaskDelayUntil(&lastWakeTime, F2T(RATE_100_HZ));
        /* 读取PS2的数据 */
        PS2_Read();
    }
}  

/**
 * @brief       读取PS2手柄的控制量
 * @param       无
 * @retval      无
 */
void PS2_Read(void)
{
    static int Strat;

    /* 读取按键键值 */
    PS2_KEY = PS2_DataKey(); 
    /* 读取左边遥感X轴方向的模拟量 */
    PS2_LX = PS2_AnologData(PSS_LX); 
    /* 读取左边遥感Y轴方向的模拟量 */
    PS2_LY = PS2_AnologData(PSS_LY);
    /* 读取右边遥感X轴方向的模拟量 */
    PS2_RX = PS2_AnologData(PSS_RX);
    /* 读取右边遥感Y轴方向的模拟量 */
    PS2_RY = PS2_AnologData(PSS_RY);  

    if (PS2_KEY == 4 && !PS2_ON_Flag) 
        /* 手柄上的Start按键被按下 */
        Strat = 1; 

    if (Strat && (PS2_LY < 118) && !PS2_ON_Flag && Deviation_Count >= CONTROL_DELAY)
        /* Start按键被按下后,需要推下右边前进杆,才可以正式PS2控制小车 */
        PS2_ON_Flag = 1, APP_ON_Flag = 0, Usart1_ON_Flag = 0;  
}

/**
 * @brief       向PS2手柄发送命令
 * @param       无
 * @retval      无
 */
void PS2_Cmd(uint8_t CMD)
{
    volatile uint16_t ref = 0x01;
    Data[1] = 0;
    for (ref = 0x01; ref < 0x0100; ref <<= 1)
    {
        if(ref & CMD)
        {
            DO(1);   /* 输出一位控制位 */
        }
        else DO(0);

        CLK(1);      /* 时钟拉高 */
        DELAY_TIME;
        CLK(0);
        DELAY_TIME;
        CLK(1);
        if (DI)
            Data[1] = ref | Data[1];
    }
    delay_us(16);
}

/**
 * @brief       判断是否为红灯模式,0x41=模拟绿灯,0x73=模拟红灯
 * @param       无
 * @retval      0:红灯模式,其他:其他模式
 */
uint8_t PS2_RedLight(void)
{
    CS(0);
    PS2_Cmd(Comd[0]);  /* 开始命令 */
    PS2_Cmd(Comd[1]);  /* 请求数据 */
    if (Data[1] == 0X73) return 0 ;
    else return 1;

}

/**
 * @brief       读取PS2手柄数据
 * @param       无
 * @retval      无
 */
void PS2_ReadData(void)
{
    volatile uint8_t byte = 0;
    volatile uint16_t ref = 0x01;
    CS(0);
    PS2_Cmd(Comd[0]);  /* 开始命令 */
    PS2_Cmd(Comd[1]);  /* 请求数据 */
    for (byte = 2; byte < 9; byte++) /* 开始接受数据 */
    {
        for (ref = 0x01; ref < 0x100; ref <<= 1)
        {
            CLK(1);
            DELAY_TIME;
            CLK(0);
            DELAY_TIME;
            CLK(1);
            if(DI)
            Data[byte] = ref | Data[byte];
        }
        delay_us(16);
    }
    CS(1);
}

/**
 * @brief       对读出来的PS2的数据进行处理,只处理按键部分 
 * @param       无
 * @retval      0: 只有一个按键按下时按下; 1: 未按下
 */
uint8_t PS2_DataKey()
{
    uint8_t index;

    PS2_ClearData();
    PS2_ReadData();

    Handkey = (Data[4] << 8)| Data[3]; /* 这是16个按键,按下为0,未按下为1 */
    for (index = 0; index < 16; index++)
    {
        if ((Handkey & (1 << (MASK[index] - 1))) == 0)
        return index + 1;
    }
    return 0;  /* 没有任何按键按下 */
}

/**
 * @brief       得到一个摇杆的模拟量
 * @param       摇杆
 * @retval      摇杆的模拟量, 范围0~256
 */
uint8_t PS2_AnologData(uint8_t button)
{
    return Data[button];
}

/**
 * @brief       清除数据缓冲区
 * @param       无
 * @retval      无
 */
void PS2_ClearData()
{
    uint8_t a;
    for (a = 0; a < 9; a++)
        Data[a] = 0x00;
}

/**
 * @brief       手柄震动函数
 * @param       motor1:右侧小震动电机 0x00关,其他开
 * @param       motor2:左侧大震动电机 0x40~0xFF 电机开,值越大 震动越大
 * @retval      无
 */
void PS2_Vibration(uint8_t motor1, uint8_t motor2)
{
    CS(0);
    delay_us(16);
    PS2_Cmd(0x01); /* 开始命令 */
    PS2_Cmd(0x42); /* 请求数据 */
    PS2_Cmd(0X00);
    PS2_Cmd(motor1);
    PS2_Cmd(motor2);
    PS2_Cmd(0X00);
    PS2_Cmd(0X00);
    PS2_Cmd(0X00);
    PS2_Cmd(0X00);
    CS(1);
    delay_us(16);  
}

/**
 * @brief       短按
 * @param       无
 * @retval      无
 */
void PS2_ShortPoll(void)
{
    CS(0);
    delay_us(16);
    PS2_Cmd(0x01);
    PS2_Cmd(0x42);
    PS2_Cmd(0X00);
    PS2_Cmd(0x00);
    PS2_Cmd(0x00);
    CS(1);
    delay_us(16);
}

/**
 * @brief       进入配置
 * @param       无
 * @retval      无
 */
void PS2_EnterConfing(void)
{
    CS(0);
    delay_us(16);
    PS2_Cmd(0x01);  
    PS2_Cmd(0x43);  
    PS2_Cmd(0X00);
    PS2_Cmd(0x01);
    PS2_Cmd(0x00);
    PS2_Cmd(0X00);
    PS2_Cmd(0X00);
    PS2_Cmd(0X00);
    PS2_Cmd(0X00);
    CS(1);
    delay_us(16);
}

/**
 * @brief       发送模式设置
 * @param       无
 * @retval      无
 */
void PS2_TurnOnAnalogMode(void)
{
    CS(0);
    PS2_Cmd(0x01);  
    PS2_Cmd(0x44);  
    PS2_Cmd(0X00);
    PS2_Cmd(0x01); /* 软件设置发送模式 */
    PS2_Cmd(0x03); /* 0x03锁存设置,即不可通过按键“MODE”设置模式 */
                   /* 0xEE不锁存软件设置,可通过按键“MODE”设置模式 */
    PS2_Cmd(0X00);
    PS2_Cmd(0X00);
    PS2_Cmd(0X00);
    PS2_Cmd(0X00);
    CS(1);
    delay_us(16);
}

/**
 * @brief       振动设置
 * @param       无
 * @retval      无
 */
void PS2_VibrationMode(void)
{
    CS(0);
    delay_us(16);
    PS2_Cmd(0x01);  
    PS2_Cmd(0x4D);  
    PS2_Cmd(0X00);
    PS2_Cmd(0x00);
    PS2_Cmd(0X01);
    CS(1);
    delay_us(16);
}

/**
 * @brief       完成并保存配置
 * @param       无
 * @retval      无
 */
void PS2_ExitConfing(void)
{
    CS(0);
    delay_us(16);
    PS2_Cmd(0x01);  
    PS2_Cmd(0x43);  
    PS2_Cmd(0X00);
    PS2_Cmd(0x00);
    PS2_Cmd(0x5A);
    PS2_Cmd(0x5A);
    PS2_Cmd(0x5A);
    PS2_Cmd(0x5A);
    PS2_Cmd(0x5A);
    CS(1);
    delay_us(16);
}

/**
 * @brief       手柄配置初始化
 * @param       无
 * @retval      无
 */
void PS2_SetInit(void)
{
    PS2_ShortPoll();
    PS2_ShortPoll();
    PS2_ShortPoll();
    PS2_EnterConfing();         /* 进入配置模式 */
    PS2_TurnOnAnalogMode();     /* “红绿灯”配置模式,并选择是否保存 */
//    PS2_VibrationMode();        /* 开启震动模式 */
    PS2_ExitConfing();          /* 完成并保存配置 */
}

同样的开了一个 FreeRTOS 的任务,来轮询读取手柄的遥控命令。

时钟修改

这里因为好几个软件时序之中,都有要用到 delay_us 函数,但是 FreeRTOS 是不提供 us 级别的延时的,只有 ms 级别,所以可以在 main
.c中添加以下内容:

/**
 * @brief     初始化延迟函数
 * @param     sysclk: 系统时钟频率, 即CPU频率(rcc_c_ck), 168Mhz
 * @retval    无
 */  
void delay_init(uint16_t sysclk)
{
    uint32_t reload;
    SysTick->CTRL = 0;                                          /* 清Systick状态,以便下一步重设,如果这里开了中断会关闭其中断 */
    HAL_SYSTICK_CLKSourceConfig(SYSTICK_CLKSOURCE_HCLK);        /* SYSTICK使用内核时钟源,同CPU同频率 */
    g_fac_us = sysclk;                                          /* 不论是否使用OS,g_fac_us都需要使用 */
    reload = sysclk;                                            /* 每秒钟的计数次数 单位为M */
    reload *= 1000000 / configTICK_RATE_HZ;                     /* 根据delay_ostickspersec设定溢出时间,reload为24位
                                                                 * 寄存器,最大值:16777216,在168M下,约合0.099s左右
                                                                 */
    SysTick->CTRL |= SysTick_CTRL_TICKINT_Msk;                  /* 开启SYSTICK中断 */
    SysTick->LOAD = reload;                                     /* 每1/delay_ostickspersec秒中断一次 */
    SysTick->CTRL |= SysTick_CTRL_ENABLE_Msk;                   /* 开启SYSTICK */
}

/**
 * @brief     延时nus
 * @param     nus: 要延时的us数
 * @note      nus取值范围: 0~8947848(最大值即2^32 / g_fac_us @g_fac_us = 168)
 * @retval    无
 */ 
void delay_us(uint32_t nus)
{
    uint32_t ticks;
    uint32_t told, tnow, tcnt = 0;
    uint32_t reload = SysTick->LOAD;        /* LOAD的值 */
    
    ticks = nus * g_fac_us;                 /* 需要的节拍数 */
//    vTaskSuspendAll();                      /* 锁定 OS 的任务调度器 */
    told = SysTick->VAL;                    /* 刚进入时的计数器值 */

    while (1)
    {
        tnow = SysTick->VAL;
        if (tnow != told)
        {
            if (tnow < told)
            {
                tcnt += told - tnow;        /* 这里注意一下SYSTICK是一个递减的计数器就可以了 */
            }
            else
            {
                tcnt += reload - tnow + told;
            }
            told = tnow;
            if (tcnt >= ticks) break;       /* 时间超过/等于要延迟的时间,则退出 */
        }
    }
//    xTaskResumeAll();                       /* 恢复 OS 的任务调度器 */
}

/**
 * @brief       systick中断服务函数
 * @param       无
 * @retval      当前时刻
 */
uint32_t getSysTickCnt(void)
{
    if (xTaskGetSchedulerState() != taskSCHEDULER_NOT_STARTED) /* OS开始跑,才执行正常的调度处理 */
    {
        return xTaskGetTickCount();
    }
    else return sysTickCnt;
}

这个就是滴答定时器的一些内容,可以在正点原子的示例之中直接拉过来。

总结

以上,初步的外设配置,以及对应的软件配置就全部完成啦,可以说是把这块芯片的基本所有资源全部用上了,资源利用率也比较高,也可以看到,用 CubeMX 来配置 STM32 的外设真的是省了很多事。当然,不习惯也是可以直接 HAL 库自己来完成初始化配置,其实是一样的。

之后,继续更新对应的一些通信协议以及对应的状态机。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值