STM32裸机开发转FreeRTOS教程

目录

1. 简介

之前都是用CubeMX+Keil裸机开发STM32,最近第一次启用了FreeRTOS,用它可以实现多线程,但是如果写代码不严谨,单片机容易卡死,非常头疼。

2. RTOS设置

(1)分配内存

config parameters选项卡里,有个totoal heap size,意思大概是freertos占用的总内存,这个数值的默认值是比较小的,后面线程和队列加多了可能会不够,可以手动增加。我设置成的8kB,STM32f103rct6有48kB的RAM,是很充足的:

可以在heap usage里面看到使用情况,"still available"和"used"加起来正好是上面设置的总大小:

还有个minimal stack size参数,这个相当于一个底线,分配给每个任务的空间大小不能小于这个值。注意这个是用Word(字)作单位,32位单片机的一个字占4字节。

下图是设置任务的界面,每个任务默认给了128个字(半个kB)这个大小是比较适中的, 足够大部分常规任务的应用,也不会太占用单片机内存。

如果想节省内存,可以把前面minimal stack size设为64 Words(不允许更小了),然后把那些变量比较少的线程空间大小设置为64 Word。调试期间可以用随后介绍的方法查看线程空间够不够。

(2)查看任务剩余空间

为了用uxTaskGetStackHighWaterMark()查看任务剩余空间,需要在cubemx中开启它对应的使能,如下图。

在FreeRTOSConfig.h里面改会和cubemx冲突。

(3)使用osDelay

所有线程(除了IDLE)的死循环里面都需要至少加个osDelay(1),否则容易卡死。

在cmsis_os.c里查看osDelay的函数体,可见它本质上就是vTaskDelay:

3. 队列的使用

(1)创建队列

在Cube的Tasks和Queues选项卡,添加队列:

Queue Size是队列长度,设置的别让队列溢出就行,可以用osMessageAvailableSpace()查询队列剩余长度。

Item Size是每个元素的长度,这个后面会讲。

生成代码之后,cube会在freertos.c里创建一个队列句柄:

c 复制代码
osMessageQId ledQueHandle;

cube里面设置的item size,代表每个队列数据占用多少字节。但由于c语言属于初级语言,不能给函数传递不定长度的参数,添加队列元素的函数是:

c 复制代码
osStatus osMessagePut (osMessageQId queue_id, uint32_t info, uint32_t millisec)

它的第二个参数info,始终是uint32_t类型的,占4个字节。那如何传递不同长度的数据呢?答案就是"指针传值"。

如果要传递的数据可以用4个字节表示,就用"直接传值"方法,item size设为4;如果单次数据量超过了4字节,可以把数据放在数组或结构体里面,用指针传值方法,item size为被传递的数组或结构体的大小。

(1)直接传值和指针传值

直接传值示例:

c 复制代码
// 发送线程
void Task_Send(void const *arg)
{
	...
	int cmd;
	for(;;){
		...
		osMessagePut(ledQueHandle, (uint32_t)cmd, osWaitForever);// 参数是int或float等数值
		...
	}
}
// 接收线程:
void Task_Receive(void const * argument)
{
  /* USER CODE BEGIN Task_LED */
	osEvent evt;
	int cmd;
	for(;;)	{
		evt = osMessageGet(ledQueHandle,0); 
		if(evt.status==osEventMessage){
			cmd=(int)evt.value.v; 
			...
		}
		osDelay(1);		
	}
}

指针传值示例:

c 复制代码
// 发送线程
void Task_Send(void const *arg)
{
	...
	int cmd[4];//传递数组,队列的item size = 16
	// MyStructType cmd;//传递结构体,需要预先定义MyStructType类型,队列的item size = sizeof(MyStructType)
	for(;;){
		...
		osMessagePut(ledQueHandle, (uint32_t)cmd, osWaitForever);//传递数组指针
//		osMessagePut(ledQueHandle, (uint32_t)&cmd, osWaitForever);//传递结构体指针
		...
	}
}
// 接收线程:
void Task_Receive(void const * argument)
{
  /* USER CODE BEGIN Task_LED */
	osEvent evt;
	int* pcmd;//接收指针,需要和发送的指针类型一致
//	MyStructType * pcmd;
	for(;;)	{
		evt = osMessageGet(ledQueHandle,0); 
		if(evt.status==osEventMessage){
			pcmd = (int*)evt.value.p;//需要强制转型
//			pcmd = (MyStructType*)evt.value.p;			
			...
		}
		osDelay(1);
	}
}

由以上可见,直接传值就是把要传送数据直接放到队列里,接收的时候用evt.value.v;指针传值是把被传递数据的指针放在队列里,接收的时候用evt.value.p。

(2)发送/接收等待时间

osMessagePut()和osMessageGet()的最后一个参数都是等待时间,发送函数的可以设置成osWaitForever,表示阻塞线程直到把数据放入队列;

接收函数的等待时间最好设置为0,同时在循环里加个osDelay()释放主控资源。设置成osWaitForever会卡死。

(3)不要在硬件中断发送队列

cmsis_os.h开头注释有:

意思是osMessagePut可以放中断,但是经过实测,在硬件中断中调用osMessagePut()函数会卡死。

所以,只能在操作系统函数(线程,定时器)操作队列,中断函数传值可以用全局变量。

4. 数据传递和共享

(1)尽量用全局常量代替函数指针传参

用指针传递维度高、数据量大的变量,容易导致各种错误。可以定义成全局变量,在函数里直接用。

如果全局变量需要被多个文件调用,可以先在.c文件定义,再在.h文件用 extern 声明一下,这样其他的C文件只要#include这个.h文件就能用全局变量了。

(2)同一资源需要被多个线程访问的两种方法

①互斥锁:在读写函数里面,先获取Mutex,操作之后再释放Mutex。

②队列:其他线程请求压入队列,再由资源访问线程接收处理。如果是读取操作,可以在队列元素里放个接收变量的指针(没验证过!)

经过测试,即便是4字节的变量,也要避免不同线程直接访问,不然会出错。

5. 开发调试

(1)修改任务名称前备份代码,否则都会被删除

在cube里面修改任务名称和入口函数前千万记得备份代码,否则重新生成代码之后,之前写的代码都会被擦除。

(2)keil的字体和编码,vscode的使用

在菜单栏Edit最下面打开configuration窗口,设置编码和字体:

Editor选项卡里面,编码设置有两个选择:

①Courier字体方案(字体易读):编码改成UTF-8,这是为了适配Courier字体。同时为了让cube适配UTF-8,需要添加一个系统环境变量,变量名称:JAVA_TOOL_OPTIONS,变量值:-Dfile.encoding=UTF-8。如果不加环境变量,cube会把中文注释搞成乱码。

②Keil默认字体方案(较难阅读):保持GB2312编码,也不用设置全局变量了。

同时勾选右边的"Automatic reload of externally modified files",避免每次都提示要不要重新加载:

如果选Courier字体方案,还需要在Colors & Fonts选项卡设置:

开发过程中,可以用vscode打开项目文件夹,在里面写代码,再在keil里面编译下载。VSC的代码辅助比Keil好多了,而且深色主题更护眼。

(3)DMA串口日志

启用日志打印串口的发送DMA可以最小的干预主程序的运行。方法是在cube里面添加一个tx的dma通道,DMA参数默认

在NVIC页面里面,可以把DMA的中断关上,因为日志打印要求不高,不需要在DMA终端里面判断数据有没有发送完:

代码里面,可以先定义个全局数组作为发送缓冲区,在函数里用sprintf格式化字符串,先调用DMAStop,再发送,不然只能发送一次:

c 复制代码
char uart_buf[50]; // 日志发送缓冲区
void Timer_Callback() // 要发送日志的函数,例如软件定时器
{
	sprintf(uart_buf,"%.2f %.2f %.2f %.2f\r\n",Mot.spd_sv, Mot.spd_pv, Mot.pos_sv, Mot.pos_pv);
	HAL_UART_DMAStop(&huart3);
	HAL_UART_Transmit_DMA(&huart3,uart_buf,strlen(uart_buf));
}

这个方法适用于周期循环发送日志的情况,发送周期基本上大于一次发送用时就行了,偶尔一次数据覆盖也没关系。如果日志量比较大,可以提高串口波特率。

(4)文档放在项目文件夹外面,以免被cube删除

如果要在项目里新建一个文件夹用来放文档,需要用全英文,避免特殊符号,以防被cube搞坏。或者把文档放项目文件夹外面。

6. LCD乱码问题

调试期间发现写入数据到芯片内部Flash之后,显示屏会出现字符错误。

解决方法是把把Flash写入地址往后移,从0x0800A000移到0x0800B000后,问题就消失了。

应该是代码地址和参数写入地址冲突了。

相关推荐
晚雾也有归处4 分钟前
结构体(C语言)
c语言·开发语言·数据结构·算法
shadoubuhuijiji15 分钟前
C语言——指针和数组名含义的辨析
c语言·算法
亿道电子Emdoor6 小时前
【ARM】MDK-快捷键添加及修改
arm开发·stm32·单片机
heater4048 小时前
【STM32】stm32启动流程
stm32·单片机·嵌入式硬件
WeeJot嵌入式9 小时前
C语言----函数
c语言
快乐飒男9 小时前
C语言基础18(GDB调试)
c语言·笔记·学习
honey ball10 小时前
滤波器的主要参数
人工智能·单片机·嵌入式硬件·学习
浅陌pa11 小时前
RTC:实时时钟
c语言·stm32·单片机·嵌入式硬件
湘の子12 小时前
汇编语言与接口技术--跑马灯
汇编·stm32·单片机·嵌入式硬件·51单片机
佳心饼干-12 小时前
单片机-蜂鸣器实验
单片机·嵌入式硬件