看完了FreeRTOS的教程,想找个项目练手,淘宝或者网上少有基于FreeRTOS的项目,于是我就想用FreeRTOS做个智能小车,可能FreeRTOS不是做小车的最优解,但主要目的是用来练手。
文中的代码有自己写的也有引用别人的代码,基于寄存器的一些操作是自己对照手册写的,其他代码引用的有江科大、正点原子、野火和一些网上搜罗来的
完整项目代码:链接:链接:https://pan.baidu.com/s/1zkWxjnJZLNv9Jg_zTl0yhw?pwd=0f68
提取码:0f68
移植的细节不多做介绍,我是直接复制我之前学习FreeRTOS的动态创建任务的模板,只有一个任务和空闲任务,实现了PC13口的测试LED闪烁。 下面是LED闪烁的任务入口,本案例大多数都是基于寄存器。
/*测试程序:LED闪烁的任务入口*/
void Task_LED_Entry(void *p_arg)
{
while(1)
{
GPIOC->ODR &= ~(1 << 13);//关灯
vTaskDelay(1000);
GPIOC->ODR |= 1 << 13;//开灯
vTaskDelay(1000);
}
}
PB8 -------> SCL
PB9 -------> SDA
/*写SCL*/
void OLED_W_SCL(uint8_t Bit)
{
if(Bit)
{
GPIOB->ODR |= 1 << 8;//pin8写1
}else
{
GPIOB->ODR &= (uint16_t)0xfeff;//pin8写0
}
}
/*写SDA*/
void OLED_W_SDA(uint8_t Bit)
{
if(Bit)
{
GPIOB->ODR |= 1 << 9;
}else
{
GPIOB->ODR &= (uint16_t)0xfdff;//pin9写0
}
}
/*引脚初始化*/
void OLED_I2C_Init(void)
{
RCC->APB2ENR |= (1 << 3); // 使能APB2上的GPIOB
GPIOB->CRH &= 0xffffff00;
GPIOB->CRH |= 0x00000077;//PB8和PB9设置为50MHz开漏输出模式
OLED_W_SCL(1);//起始PB8和PB9置高电平
OLED_W_SDA(1);
}
void OLED_I2C_Start(void)
{
OLED_W_SDA(1);
OLED_W_SCL(1);
OLED_W_SDA(0);
OLED_W_SCL(0);
}
void OLED_I2C_Stop(void)
{
OLED_W_SDA(0);
OLED_W_SCL(1);
OLED_W_SDA(1);
}
void OLED_I2C_SendByte(uint8_t Byte)
{
unsigned char i;
//高位先行
for(i=0;i<8;i++)
{
OLED_W_SDA(Byte&(0x80>>i));
OLED_W_SCL(1);
OLED_W_SCL(0);
}
OLED_W_SCL(1);//额外的一个时钟,不处理应答信号
OLED_W_SCL(0);
}
void OLED_WriteCommand(uint8_t Command)//Command 要写入的命令
{
OLED_I2C_Start();
OLED_I2C_SendByte(0x78); //从机地址
OLED_I2C_SendByte(0x00); //写命令
OLED_I2C_SendByte(Command);
OLED_I2C_Stop();
}
void OLED_WriteData(uint8_t Data)//Data 要写入的数据
{
OLED_I2C_Start();
OLED_I2C_SendByte(0x78); //从机地址
OLED_I2C_SendByte(0x40); //写数据
OLED_I2C_SendByte(Data);
OLED_I2C_Stop();
}
void OLED_SetCursor(uint8_t Y, uint8_t X)
{
OLED_WriteCommand(0xB0 | Y); //设置Y位置
OLED_WriteCommand(0x10 | ((X & 0xF0) >> 4)); //设置X位置高4位
OLED_WriteCommand(0x00 | (X & 0x0F)); //设置X位置低4位
}
void OLED_Clear(void)
{
uint8_t i, j;
for (j = 0; j < 8; j++)
{
OLED_SetCursor(j, 0);
for (i = 0; i < 128; i++)
{
OLED_WriteData(0x00);
}
}
}
/*OLED显示一个字符*/
void OLED_ShowChar(uint8_t Line, uint8_t Column, char Char)
{
uint8_t i;
//Line 行位置,范围:1~4,Column 列位置,范围:1~16
OLED_SetCursor((Line - 1) * 2, (Column - 1) * 8); //设置光标位置在上半部分
for (i = 0; i < 8; i++)
{
OLED_WriteData(OLED_F8x16[Char - ' '][i]); //显示上半部分内容
}
OLED_SetCursor((Line - 1) * 2 + 1, (Column - 1) * 8); //设置光标位置在下半部分
for (i = 0; i < 8; i++)
{
OLED_WriteData(OLED_F8x16[Char - ' '][i + 8]); //显示下半部分内容
}
}
/*OLED显示字符串*/
void OLED_ShowString(uint8_t Line, uint8_t Column, char *String)
{
uint8_t i;
for (i = 0; String[i] != '\0'; i++)
{
OLED_ShowChar(Line, Column + i, String[i]);
}
}
/*OLED次方函数*/
uint32_t OLED_Pow(uint32_t X, uint32_t Y)
{
uint32_t Result = 1;
while (Y--)
{
Result *= X;
}
return Result;
}
/*OLED显示数字(十进制,正数)
//Number 要显示的数字,范围:0~4294967295*/
// Length 要显示数字的长度,范围:1~10
void OLED_ShowNum(uint8_t Line, uint8_t Column, uint32_t Number, uint8_t Length)
{
uint8_t i;
for (i = 0; i < Length; i++)
{
OLED_ShowChar(Line, Column + i, Number / OLED_Pow(10, Length - i - 1) % 10 + '0');
}
}
/*OLED显示数字(十进制,带符号数)*/
//要显示的数字,范围:-2147483648~2147483647
//Length 要显示数字的长度,范围:1~10
void OLED_ShowSignedNum(uint8_t Line, uint8_t Column, int32_t Number, uint8_t Length)
{
uint8_t i;
uint32_t Number1;
if (Number >= 0)
{
OLED_ShowChar(Line, Column, '+');
Number1 = Number;
}
else
{
OLED_ShowChar(Line, Column, '-');
Number1 = -Number;
}
for (i = 0; i < Length; i++)
{
OLED_ShowChar(Line, Column + i + 1, Number1 / OLED_Pow(10, Length - i - 1) % 10 + '0');
}
}
/*OLED显示数字(十六进制,正数)*/
// Number 要显示的数字,范围:0~0xFFFFFFFF
//Length 要显示数字的长度,范围:1~8
void OLED_ShowHexNum(uint8_t Line, uint8_t Column, uint32_t Number, uint8_t Length)
{
uint8_t i, SingleNumber;
for (i = 0; i < Length; i++)
{
SingleNumber = Number / OLED_Pow(16, Length - i - 1) % 16;
if (SingleNumber < 10)
{
OLED_ShowChar(Line, Column + i, SingleNumber + '0');
}
else
{
OLED_ShowChar(Line, Column + i, SingleNumber - 10 + 'A');
}
}
}
/*OLED显示数字(二进制,正数)*/
//Number 要显示的数字,范围:0~1111 1111 1111 1111
//Number 要显示的数字,范围:0~1111 1111 1111 1111
void OLED_ShowBinNum(uint8_t Line, uint8_t Column, uint32_t Number, uint8_t Length)
{
uint8_t i;
for (i = 0; i < Length; i++)
{
OLED_ShowChar(Line, Column + i, Number / OLED_Pow(2, Length - i - 1) % 2 + '0');
}
}
void OLED_Init(void)
{
uint32_t i, j;
for (i = 0; i < 1000; i++) //上电延时
{
for (j = 0; j < 1000; j++)
;
}
OLED_I2C_Init(); //端口初始化
OLED_WriteCommand(0xAE); //关闭显示
OLED_WriteCommand(0xD5); //设置显示时钟分频比/振荡器频率
OLED_WriteCommand(0x80);
OLED_WriteCommand(0xA8); //设置多路复用率
OLED_WriteCommand(0x3F);
OLED_WriteCommand(0xD3); //设置显示偏移
OLED_WriteCommand(0x00);
OLED_WriteCommand(0x40); //设置显示开始行
OLED_WriteCommand(0xA1); //设置左右方向,0xA1正常 0xA0左右反置
OLED_WriteCommand(0xC8); //设置上下方向,0xC8正常 0xC0上下反置
OLED_WriteCommand(0xDA); //设置COM引脚硬件配置
OLED_WriteCommand(0x12);
OLED_WriteCommand(0x81); //设置对比度控制
OLED_WriteCommand(0xCF);
OLED_WriteCommand(0xD9); //设置预充电周期
OLED_WriteCommand(0xF1);
OLED_WriteCommand(0xDB); //设置VCOMH取消选择级别
OLED_WriteCommand(0x30);
OLED_WriteCommand(0xA4); //设置整个显示打开/关闭
OLED_WriteCommand(0xA6); //设置正常/倒转显示
OLED_WriteCommand(0x8D); //设置充电泵
OLED_WriteCommand(0x14);
OLED_WriteCommand(0xAF); //开启显示
OLED_Clear(); // OLED清屏
}
ECHO ------> PB10
TRIG --------> PB11
void HCSR04_Init()
{
RCC->APB2ENR |= RCC_APB2ENR_IOPBEN;//使能PB口时钟
GPIOB->CRH &= 0xffff00ff;
GPIOB->CRH |= 0x00003800; //配置PB10为上下拉输入,PB11配置为推挽输出
GPIOB->ODR &= ~GPIO_ODR_ODR10; //通过往ODR寄存器写0配置PB10为下拉输入
GPIOB -> ODR &= ~GPIO_ODR_ODR11;
}
uint16_t Get_Distance()
{
uint16_t echo_time=0,distance=0;
GPIOB -> ODR |= (uint16_t)0x0800;
delay_us(20);//触发超声波模块测距
GPIOB -> ODR &= (uint16_t)~0x0800;
TIM3->CNT = 0;//清零TIM3计数寄存器,一个数1us
while((GPIOB -> IDR & (uint16_t)0x0400)==0);//等待超声波模块返回高电平
TIM3->CR1 |= ((uint16_t)0x0001); // 使能定时器,开始计数
while((GPIOB -> IDR & (uint16_t)0x0400));//超声波发送信息完毕
TIM3->CR1 &= (~TIM_CR1_CEN); // 失能定时器
echo_time = TIM3->CNT;//获取超声波模块返回的时间
distance=echo_time*340/2/10/1000;//计算距离,单位厘米
return distance;
}
IN1 -------> PA3
IN2 -------> PA2
IN3 -------> PA1
IN4 -------> PA0
void TIM2_PWM_Init()
{
RCC->APB1ENR|=1<<0; //TIM2 时钟使能
RCC->APB2ENR|=1<<2; //使能 PORTA 时钟
GPIOA->CRL&=0xFFFF0000; //PA1 到 PA3 输出
GPIOA->CRL|=0x0000BBBB; //复用功能输出
RCC->APB2ENR|=1<<0; //开启辅助时钟
AFIO->MAPR&=0xFFFFFCFF; //清除 MAPR 的[9:8]
TIM2->ARR=100; //设定计数器自动重装值
TIM2->PSC=7201-1; //预分频器7200分频,降低频率,减小电路噪音
//前进组
TIM2->CCMR1|=6<<12; //CH2 PWM1 模式
TIM2->CCMR2|=6<<12; //CH4 PWM1 模式
//后退组
TIM2->CCMR1|=6<<4; //CH1 PWM1 模式
TIM2->CCMR2|=6<<4; //CH3 PWM1 模式
TIM2->CR1=0x0080; //ARPE 使能
TIM2->CR1|=0x01; //使能定时器 2
}
#define STOP (uint8_t)0
#define ADVANCE (uint8_t)1
#define BACK (uint8_t)2
#define LEFT (uint8_t)3
#define RIGHT (uint8_t)4
#define SPEED_UP (uint8_t)5
#define SPEED_DOWN (uint8_t)6
#define SPEED_ADD (uint8_t)1
#define SPEED_SUB (uint8_t)0
void Car_Move(uint8_t Direction)
{
switch(Direction)
{
case ADVANCE:
OLED_ShowString(1,12,"ADV ");
TIM2->CCER&=0xfefe; //OC1 OC3输出失能
TIM2->CCER|=1<<4; //OC2 输出使能
TIM2->CCER|=1<<12; //OC4 输出使能
break;
case BACK:
OLED_ShowString(1,12,"BACK ");
TIM2->CCER&=0xefef; //OC2 OC4输出失能
TIM2->CCER|=1<<0; //OC1 输出使能
TIM2->CCER|=1<<8; //OC3 输出使能
break;
case LEFT:
OLED_ShowString(1,12,"LEFT ");
TIM2->CCER&=0xeffe; //OC1 OC4输出失能
TIM2->CCER|=1<<4; //OC2 输出使能
TIM2->CCER|=1<<8; //OC3 输出使能
break;
case RIGHT:
OLED_ShowString(1,12,"RIGHT");
TIM2->CCER&=0xfeef; //OC1 OC4输出失能
TIM2->CCER|=1<<0; //OC1 输出使能
TIM2->CCER|=1<<12; //OC4 输出使能
break;
case STOP:
OLED_ShowString(1,12,"STOP ");
TIM2->CCER &=0xeeee;
break;
}
}
void Car_Move_Buffer(uint8_t Speed,uint8_t dir)//加速减速缓冲程序
{
if(dir)
{
uint8_t i=0;
for(i=20;i<=Speed;i++)
{
delay_ms(10);
TIM2 -> CCR2 = i;
TIM2 -> CCR4 = i;
TIM2 -> CCR1 = i;
TIM2 -> CCR3 = i;
}
}
else{
while(Speed--){
delay_ms(5);
TIM2 -> CCR2 = Speed;
TIM2 -> CCR4 = Speed;
TIM2 -> CCR1 = Speed;
TIM2 -> CCR3 = Speed;
}
}
}
#include "stm32f10x.h" // Device header
#include "OLED.h"
uint8_t Serial_RxData;
extern unsigned char direction;
extern uint8_t flag_carmove;
void Serial_Init(void)
{
RCC->APB2ENR|=1<<14; //使能串口时钟
RCC->APB2ENR|=1<<2; //使能 PORTA 时钟
GPIOA->CRH&=0xFFFFF0FF; //PA10 输入
GPIOA->CRH|=0x00000800; //上下拉输入
GPIOA->ODR|=1<<10;//上拉输入
USART1->BRR=0x00001d4c;//设置波特率为9600
/*清除STOP[13:12]位,00:一个停止位,Clock, CPOL, CPHA and LastBit用默认的0 */
USART1->CR2&=0xcfff;
/* 不启用硬件控制流:0x0000 */
USART1->CR3|=0x0000;
/* 字长8:0x0000 奇偶校验无:0x0000 接收模式:0x0004 */
/*0x0000|0x0000|0x0004=0x0004*/
USART1->CR1|=0x0004;
USART1->CR1|=1<<5; //接收缓冲区非空中断使能
//NVIC->ICER[1]|=1<<5;//关闭中断通道
NVIC->ISER[1]|=1<<5;//打开中断通道
/*开启USART1*/
USART1->CR1|=0x2000;
}
//串口中断服务程序
void USART1_IRQHandler(void)
{
if(USART1->SR & 1 << 5)//如果接收了数据
{
Serial_RxData = (uint16_t)(USART1->DR & (uint16_t)0x01FF);
USART1->SR = (uint16_t)~(1<<5);
if(Serial_RxData != direction && Serial_RxData != 5 && Serial_RxData != 6 && Serial_RxData != 0)
{
flag_carmove = 1;
direction=Serial_RxData;
}else
{
switch(Serial_RxData){
case 5:
flag_carmove=2;
break;
case 6:
flag_carmove=3;
break;
case 0 :
flag_carmove=4;
break;
}
}
}
}
void TIM2_PWM_Init()
{
RCC->APB1ENR|=1<<0; //TIM2 时钟使能
RCC->APB2ENR|=1<<2; //使能 PORTA 时钟
GPIOA->CRL&=0xFFFF0000; //PA1 - PA3 输出
GPIOA->CRL|=0x0000BBBB; //复用功能输出
RCC->APB2ENR|=1<<0; //开启辅助时钟
AFIO->MAPR&=0xFFFFFCFF; //清除 MAPR 的[9:8]
//AFIO->MAPR|=1<<11; //部分重映像,TIM3_CH2->PB5
TIM2->ARR=100; //设定计数器自动重装值
TIM2->PSC=7201-1; //预分频器72分频,一次计数1us
//前进组
TIM2->CCMR1|=6<<12; //CH2 PWM1 模式
TIM2->CCMR2|=6<<12; //CH4 PWM1 模式
//后退组
TIM2->CCMR1|=6<<4; //CH1 PWM1 模式
TIM2->CCMR2|=6<<4; //CH3 PWM1 模式
TIM2->CR1=0x0080; //ARPE 使能
TIM2->CR1|=0x01; //使能定时器 2
}
void TIM3_Count_Init()
{
RCC->APB1ENR |= RCC_APB1ENR_TIM3EN; // TIM3时钟使能
TIM3->SMCR &= (uint16_t)(~((uint16_t)TIM_SMCR_SMS));//关闭从模式
TIM3->CR1 &= (uint16_t)(~((uint16_t)TIM_CR1_CKD));
TIM3->CR1 |= (uint32_t)((uint16_t)0x0000);
TIM3->ARR = 65535 - 1; // 重装载值
TIM3->PSC = 72 - 1; // 预分频数
TIM3->EGR = 0x0001; //无事件发生
TIM3->CR1 &= ((uint16_t)~0x0001) ;//关闭时钟
}
/*时钟源为外部晶振,9倍频,SYSCLK为72MHz,APB2不分频,APB1二分频*/
void SYSCLK_Init()
{
/*打开HSE*/
RCC->CR |= RCC_CR_HSEON;
/* 等待HSE等待完毕 */
while ((RCC->CR & RCC_CR_HSERDY) == 0)
{
}
/*时钟源选择为 HSE,进行9倍频 SYSCLK = HSE*9 = 8MHz*9 = 72MHz */
RCC->CFGR |= RCC_CFGR_PLLMULL9;
RCC->CFGR |= RCC_CFGR_PLLSRC;
FLASH->ACR |= FLASH_ACR_LATENCY_2; // FLASH缓冲
/* 使能PLL倍频器 */
RCC->CR |= RCC_CR_PLLON;
/* Wait till PLL is ready */
while ((RCC->CR & RCC_CR_PLLRDY) == 0) // 等待PLL倍频器就绪
{
}
/* SYSCLK预分频系数为1,即不分频 */
RCC->CFGR |= RCC_CFGR_HPRE_DIV1;
/* APB2预分频系数为1,即不分频 =SYSCLK */
RCC->CFGR |= RCC_CFGR_PPRE2_DIV1;
//
// /* APB1预分频系数为2,即 APB2 = SYSCLK/2 */
RCC->CFGR |= RCC_CFGR_PPRE1_DIV2;
/* 复位时钟源选择,并将时钟源选择为PLL */
RCC->CFGR &= (~(RCC_CFGR_SW));
RCC->CFGR |= RCC_CFGR_SW_PLL;
//
/* 等待时钟源选择完成置位 */
while ((RCC->CFGR & RCC_CFGR_SWS) != 0x08)
{
}
/*SysTick使用外部晶振,并关闭systick定时器*/
//SysTick->CTRL |= 0x00000004;
}
static TaskHandle_t Task_CarMove_handle = NULL;
static TaskHandle_t Task_Get_Distance_handle = NULL;
static TaskHandle_t Task_HG04_handle = NULL;
void Task_CarMove_Entry(void *p_arg)
{
uint8_t temp=0,temp_dir=0;
while(1)
{
switch(flag_carmove){
case 1://转向
flag_carmove=0;
taskENTER_CRITICAL();//进入临界段保护完成转向
Car_Move_Buffer(speed,SPEED_SUB);
Car_Move(direction);
Car_Move_Buffer(speed,SPEED_ADD);
taskEXIT_CRITICAL();//退出临界段
break;
case 2://加速
flag_carmove=0;
if(speed>=100)
{
speed=20;
}
speed += 10;
TIM2->CCR2 =speed;
TIM2->CCR4 =speed;
TIM2->CCR1 =speed;
TIM2->CCR3 =speed;
break;
case 3://减速
flag_carmove=0;
if(speed<=30)
{
speed=110;
}
speed -= 10;
TIM2->CCR2 =speed;
TIM2->CCR4 =speed;
TIM2->CCR1 =speed;
TIM2->CCR3 =speed;
break;
case 4://STOP
flag_carmove=0;
direction= STOP;
Car_Move_Buffer(speed,SPEED_SUB);
Car_Move(direction);
break;
}
}
}
void Task_Get_Distance_Entry(void *p_arg)
{
uint16_t distance=0;
while(1){
vTaskDelay(100);//延迟100ms
OLED_ShowNum(2,12,speed,3);//显示速度
taskENTER_CRITICAL();//进入临界段保护,完成准确测距
distance=Get_Distance();//获取距离
taskEXIT_CRITICAL();//退出临界段保护
if(distance <= 30)//如果距离小于30,避障
{
Car_Move_Buffer(speed,SPEED_SUB);//减速缓冲,保护马达齿轮
Car_Move(BACK);//倒退一段时间
Car_Move_Buffer(speed,SPEED_ADD);//加速缓冲,保护马达齿轮
vTaskDelay(500);
Car_Move(STOP);//停止
vTaskResume(Task_HG04_handle);//恢复舵机转向任务,舵机转向任务优先级最高,恢复立即执行
}
}
}
void Task_HG04_Entry(void *p_arg)//舵机转向任务
{
uint16_t left_distance=0,right_distance=0;
uint8_t flag_hg04=0;
while(1)
{
/*舵机转向*/
delay_ms(800);//给足够多的时间使舵机完成转向
GPIOC->ODR |= (1<<13);
delay_us(650);//根据高电平的时间,舵机完成特定角度转向
GPIOC->ODR &=~(1<<13);
delay_ms(800);
if(flag_hg04)//初始化使不需要测距
{
taskENTER_CRITICAL();//进入临界段保护,使其准确测距
left_distance=Get_Distance();//测距:右方
taskEXIT_CRITICAL();//完成测距,退出临界段保护
}
GPIOC->ODR |= (1<<13);
delay_us(1310);//舵机特定角度转向
GPIOC->ODR &=~(1<<13);
delay_ms(800);
if(flag_hg04)
{
taskENTER_CRITICAL();
right_distance=Get_Distance();//测距:左方
taskEXIT_CRITICAL();
}
GPIOC->ODR |= (1<<13);
delay_us(980);
GPIOC->ODR &=~(1<<13);
OLED_ShowNum(3,1,left_distance,8);//左方距离显示,这两句主要用于测试
OLED_ShowNum(4,1,right_distance,8);//右方距离显示
if(flag_hg04)
{
if(left_distance>=right_distance)//如果左方距离大于右方,左转
{
Car_Move(LEFT);
Car_Move_Buffer(speed,SPEED_ADD);
}else//如果左方距离小于右方,右转
{
Car_Move(RIGHT);
Car_Move_Buffer(speed,SPEED_ADD);
}
vTaskDelay(500);
Car_Move_Buffer(speed,SPEED_SUB);
Car_Move(ADVANCE);
Car_Move_Buffer(speed,SPEED_ADD);
}
flag_hg04=1;
vTaskSuspend(Task_HG04_handle);//挂起舵机转头任务,只有当距离小于30cm时唤醒舵机转头任务,此任务优先级最高,所以一唤醒就执行
}
}
int main(void)
{
/*系统时钟初始化*/
SYSCLK_Init();
/*OLED初始化*/
OLED_Init();
/*串口初始化*/
Serial_Init();
/*舵机控制初始化*/
HG04_Init();
/*TIM2输出PWM 初始化*/
TIM2_PWM_Init();
/*超声波测距时间模块初始化*/
TIM3_Count_Init();
/*超声波测距模块初始化*/
HCSR04_Init();
/*创建任务*/
xTaskCreate(Task_CarMove_Entry, "CarMove", 128, NULL, 30, &Task_CarMove_handle);
xTaskCreate(Task_Get_Distance_Entry, "HCSR04", 128, NULL, 30, &Task_Get_Distance_handle);
xTaskCreate(Task_HG04_Entry, "HG04", 128, NULL, 31, &Task_HG04_handle);
//关闭中断号小于15的中断,在FreeRTOSConfig.h中配置
portDISABLE_INTERRUPTS();
// 开启调度
vTaskStartScheduler();
while(1)
{
OLED_ShowNum(3,1,88,8);//正常情况下不会执行这一句
}
}
经过大概两个星期的时间,终于完成了基于FreeRTOS实时操作系统的智能小车实验。整个项目代码等后续上传到百度云。