终于,我差不多把下位机的一系列内容这两周全部搞完了,在轮趣那边的基础上,我把看不顺眼的一系列标准库全部换掉了,同时按照自己需要的功能加了些改进,以及一直以来想要摈弃的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的型号,来开启工程创建:
可以在左上角选择搜索对应的芯片,我这边就是STM32F407VET6:
这里选择第一个,你可以点进去看到是不是你想要的芯片,然后确认创建就可以了。
开始配置
先看一下成品,之后一步步配置:
可以看到资源利用的满满的,基本都用上了。
配置时钟
这里我不是那么确定,但配置成这样板子能跑而且没有报错。
在RCC中,把HSE和LSE全部选成外部晶振:
之后去到 Clock Configuration,进一步完成时钟线的配置,这里有两点是确认的,APB1 不能超过 42MHz,APB2 不能超过 82MHz,其余的配置我全部是对照的我正点原子的电机开发板配置的,那个也是F407的芯片,我就直接搬过来了:
这里到目前为止,整个系统时钟基本就搞定了。
配置系统
这里就是配置烧写口,以及对应的整个系统的时钟源,还是选的 SW 和滴答定时器:
配置模拟量
这里就是配置 ADC 转换相关的内容,这里引脚是电路设计到了 PB0 和 PB1,分别是通道 8 和通道 9,我这边最后是初始化在了 ADC1 上,同时还开启了 DMA 的转换。
先是在 ADC 中选择 ADC1,然后勾选 IN8 和 IN9 :
同时还需要适配一下的参数:独立模式,分频为 6,12位分辨率,右对齐,打开连续转换,所有转换完成才有 EOC 标志,其余的全部关闭;然后设置连续转换通道数为 2,软件触发,两个通道的转换顺序要记住自己的设置,之后会用到;最后一个关键点就是,采样一定要选择最大的,这里如果选择默认的,会因为采样时间间隔太短而造成两个通道的结果相互影响!
之后配置 DMA ,这里只要打开 DMA 其余保持默认即可,我这里最后改了一个优先级为 High:
最后把对应的 IO 口配置一下,已经默认配置成了模拟输入,无上下拉,只要把标签名字改成自己顺手的或者认得出的就行:
至此,ADC 配置部分就完成啦。
定时器配置(直流有刷电机相关)
这里是整个工程的核心内容,因为所有的电机控制都是通过 PWM 波完成,同时为了闭环控制,还得接入编码器的位置信号,这一点也是靠定时器来实现的,接下来就一个个完成配置。
首先配置 PWM 输出相关的内容,这里主要展示配置过程,一样的内容就只放一遍,最后会提具体需要配置的通道。
打开 TIM1,这里四个通道的 PWM 输出都用到了,所以都要打开并且配置参数:
配置定时器1,四个通道全部配置成 PWM 输出,之后进一步配置;配置分频系数为 1,在 HAL 库中会自动把填入的数字加 1,所以填入 1 - 1,然后配置为向上计数模式,自动重装载值配置为 16800 - 1,同时使能自动装载;之后将所有的通道的 PWM 的模式,全部配置为 PWM 模式 1,输出极性就是默认的高,使能输出预装载。
之后需要修改一下 GPIO 的配置:
这里,四个引脚都配置成上拉,复用推挽输出,同时输出频率选择最高。
最后来看一下具体都需要配置哪些:TIM1 的 4 个引脚;TIM9 的 2 个引脚,以及 TIM10 和 TIM11 的各 1 个引脚,加起来一共 8 个 PWM 输出引脚,共可控制 4 个直流有刷电机。所有的配置都是一样的,这里就不再贴图赘述了。
定时器配置(编码器相关)
之前的直流有刷电机 PWM 已经配置好了,那么这个时候其实是已经可以完成开环的控制了,但是为了实现速度闭环,我们还需要把编码器信号接进来,这一点也需要定时器的相关配置。
打开 TIM2,配置成编码器模式:
只要在定时器的模式中,最后一个混合模式中选择编码器模式,然后预分频系数同样设置为 1 - 1,自动重装载值直接设置为最大的 65536 - 1,向上计数;之后将编码器模式,选择为 TI1 和 TI2。这样就可以配置好编码器模式了。
之后再配置一下对应的 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 之后,设置参数配置波特率,按照图中设计的就可以了,最终配置出来的波特率 = 42 / ((1 + 6 + 7) * 3) = 1 M;之后再使能自动重传就可以了,最后将模式设置为正常模式。
这里在 NVIC 中在完成一下设置,需要开启 RX0 的接收中断。
之后最后配置一下 GPIO,配置与之前都是一样的,上拉,复用推挽以及输出频率最高。
通讯配置(串口)
这里配置一下串口的相关设置,首先串口 1 和串口 3 是一样的配置。
打开串口 1 之后,配置成异步通讯:
配置完之后都取默认的就好了,波特率为 115200,8 位数据位,没有奇偶校验位,最后设置一个 1 位的停止位。最后要在 NVIC 之中,打开串口的接收中断。
这里在打开 DMA 完成通讯:
需要打开接收和发送的双向 DMA 通道。
GPIO 的配置就是一样的,上拉,推挽输出,输出频率最高。
串口3 也是同样的配置,这里就不写了。
通讯配置(APP控制)
APP 控制是通过串口 2 的配置来完成的。
这里就配置波特率为 9600,其余都一样,同样开启接收中断,但是不需要开启 DMA 的传输,因为处理数据都是一个一个字符处理,没有太大的必要通过 DMA 来加速。
操作系统
这里在最后的大项中,开启 FreeRTOS。
配置 GPIO
最后,需要完成 LED、KEY、软件模拟的 MPU6050 的 IIC 通信引脚,以及 OLED 的 IIC 通信引脚的初始化,全部对着下图完成就可以了:
最后看一下,有些引脚和默认的不一样,这边再对着确认一下就可以:
最后生成工程
这里在 Project Manager 中,直接选择生成的 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 库自己来完成初始化配置,其实是一样的。
之后,继续更新对应的一些通信协议以及对应的状态机。