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后,问题就消失了。

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

相关推荐
小殷学长24 分钟前
【单片机毕业设计17-基于stm32c8t6的智能倒车监测系统】
stm32·单片机·课程设计
TESmart碲视2 小时前
HKS201-M24 大师版 8K60Hz USB 3.0 适用于 2 台 PC 1台显示器 无缝切换 KVM 切换器
单片机·嵌入式硬件·物联网·游戏·计算机外设·电脑·智能硬件
small_wh1te_coder3 小时前
硬件嵌入式学习路线大总结(一):C语言与linux。内功心法——从入门到精通,彻底打通你的任督二脉!
linux·c语言·汇编·嵌入式硬件·算法·c
花落已飘3 小时前
STM32中实现shell控制台(shell窗口输入实现)
stm32·单片机·嵌入式硬件
花落已飘3 小时前
STM32中实现shell控制台(命令解析实现)
stm32·shell
没有钱的钱仔4 小时前
STM32低功耗模式全面指南
css·stm32·css3
牵牛老人5 小时前
Qt处理USB摄像头开发说明与QtMultimedia与V4L2融合应用
stm32·单片机·qt
宇钶宇夕7 小时前
针对工业触摸屏维修的系统指南和资源获取途径
单片机·嵌入式硬件·自动化
黑听人7 小时前
【力扣 简单 C】70. 爬楼梯
c语言·leetcode
杜子不疼.7 小时前
二分查找,乘法口诀表,判断闰年,判断素数,使用函数实现数组操作
c语言