1、delay文件夹。
delay.c和delay.h两个文件,其中有七个函数:
void delay_osschedlock(void);
void delay_osschedunlock(void);
void delay_ostimedly(u32 ticks);
void SysTick_Handler(void);
void delay_init(void);
void delay_ms(u16 nms);
void delay_ns(u32 nus);
前四个函数仅在操作系统的时候需要用到,后三个无论是否支持操作系统都需要用到。
CM3内核有SysTick(滴答)定时器,是一个24位倒计数定时器。
①当需要delay_ms和delay_ns支持操作系统时,需要用到3个宏定义和4个函数。
#define delay_osrunning OSRUNNING //OS是否运行,1运行,0不运行
#define delay_ostickspersec OS_TICKS_PER_SEC //OS时钟节拍,每秒调度次数
#define delay_osintnesting OSIntNesting //中断嵌套级别,即中断嵌套次数
void delay_osschedlock(void) //关闭OS任务调度,防止打断延迟
void delay_osschedunlock(void) //恢复OS任务调度,可以打断延迟
void delay_ostimedly(u32 ticks) //OS自带延迟函数,ticks延迟节拍数
void SysTick_Handler(void) //systick中断服务函数
②delay_init函数。
两个重要的参数:fac_us和fac_ms,SysTick选择时钟源,如果需要支持操作系统,只需要在sys.h中设置SYSTEM_SUPPORT_OS宏的值为1即可,默认SYSTEM_SUPPORT_OS宏的值为0,函数如下:
void delay_init()
{
#if SYSTEM_SUPPORT_OS //如果需要支持OS.
u32 reload;
#endif
SysTick_CLKSourceConfig(SysTick_CLKSource_HCLK_Div8); //选择外部时钟 HCLK/8
fac_us=SystemCoreClock/8000000; //为系统时钟的1/8
#if SYSTEM_SUPPORT_OS //如果需要支持OS.
reload=SystemCoreClock/8000000; //每秒钟的计数次数,单位为M,SystemCoreClock值为72000000
reload*=1000000/delay_ostickspersec; //根据delay_ostickspersec 设定溢出时间
//reload 为24 位寄存器,最大值:16777216,在72M 下,约合1.86s 左右
fac_ms=1000/delay_ostickspersec; //代表OS 可以延时的最少单位
SysTick->CTRL|=SysTick_CTRL_TICKINT_Msk; //开启SYSTICK 中断
SysTick->LOAD=reload; //每1/delay_ostickspersec 秒中断一次
SysTick->CTRL|=SysTick_CTRL_ENABLE_Msk; //开启SYSTICK
#else
fac_ms=(u16)fac_us*1000; //非OS 下,代表每个ms 需要的systick 时钟数
#endif
}
delay_int函数使用了条件编译,不支持操作系统时设置fac_us和fac_ms,支持操作系统时设置了fac_us(和不支持操作系统的相同)、fac_ms、SysTick->CTRL、SysTick->LOAD,编译是根据sys.h中的宏SYSTEM_SUPPORT_OS确定。
SysTick是MDK定义的一个结构体(core_m3.h里面),里面包含CTRL、LOAD、VAL、CALIB四个成员变量。
有操作系统时,fac_ms代表操作系统自带延迟函数所能实现的最小延迟时间,如delay_ostickspessec=200,那么fac_ms就是5ms。
③无操作系统的延迟函数。
delay_us函数。
void delay_us(u32 nus)
{
u32 temp;
SysTick->LOAD=nus*fac_us; //时间加载
SysTick->VAL=0x00; //清空计数器
SysTick->CTRL|=SysTick_CTRL_ENABLE_Msk ; //开始倒数
do
{
temp=SysTick->CTRL;
}while((temp&0x01)&&!(temp&(1<<16))); //等待时间到达
SysTick->CTRL&=~SysTick_CTRL_ENABLE_Msk; //关闭计数器
SysTick->VAL =0X00; //清空计数器
}
不带操作系统的delay_us,把延迟换算为Systick时钟数,然后写入LOAD,滴答定时器关闭或者计时完成停止循环,即完成定时。有滴答定时器的开启判断是放置滴答定时器关闭后导致死循环。LOAD是24位有效位,72MHz主频时,SysTics频率9MHz,每个滴答周期1/9us,最大延迟时间 t = 2^24/9 = 1864135us = 1.86s。
delay_ms函数同delay_us,函数如下:
void delay_ms(u16 nms)
{
u32 temp;
SysTick->LOAD=(u32)nms*fac_ms; //时间加载(SysTick->LOAD为24bit)
SysTick->VAL =0x00; //清空计数器
SysTick->CTRL|=SysTick_CTRL_ENABLE_Msk ; //开始倒数
do
{
temp=SysTick->CTRL;
}while((temp&0x01)&&!(temp&(1<<16))); //等待时间到达
SysTick->CTRL&=~SysTick_CTRL_ENABLE_Msk; //关闭计数器
SysTick->VAL =0X00; //清空计数器
}
④有操作系统的延迟函数。
delay_us函数。
void delay_us(u32 nus)
{
u32 ticks;
u32 told,tnow,tcnt=0;
u32 reload=SysTick->LOAD; //LOAD的值
ticks=nus*fac_us; //需要的节拍数
tcnt=0;
delay_osschedlock(); //阻止OS调度,防止打断us延时
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; //时间超过/等于要延迟的时间,则退出.
}
};
delay_osschedunlock(); //恢复OS调度
}
delay_us可以实现2^32us的延迟,大概4294s。
delay_ms函数。
void delay_ms(u16 nms)
{
if(delay_osrunning&&delay_osintnesting==0) //如果OS已经在跑了,并且不是在中断里面(中断里面不能任务调度)
{
if(nms>=fac_ms) //延时的时间大于OS的最少时间周期
{
delay_ostimedly(nms/fac_ms); //OS延时
}
nms%=fac_ms; //OS已经无法提供这么小的延时了,采用普通方式延时
}
delay_us((u32)(nms*1000)); //普通方式延时
}
操作系统在运行时,延迟时间大于一个时钟节拍(fac_ms)时,delay_ostickspersec函数利用操作系统自带的延迟函数,实现任务级延迟,然后使用有操作系统的delay_us函数进行精确延迟。
操作系统不在运行时,delay_ms是直接由delay_us实现的,操作系统下的delay_us可以实现很长的延迟而不溢出,但是delay_us延迟时,任务调度被上锁,时间长会影响整个系统的性能。
2、SYS文件夹。
① *(volatile unsigned long *) 语法。
(volatile unsigned long *) 变量,代表变量是一个unsigned long的指针,如:
(volatile unsigned long *) 0x1234,0x1234强制转换为unsigned long类型的指针,指向0x1234的指针。
volatile 是一个修饰符,告诉编译器此代码不要优化,(volatile unsigned long *) 变量,指的是未优化指针类型的变量。
*(volatile unsigned long *) 0x1234指的是0x1234地址里面保存的内容。*是指针运算符。
②sys.h中定义了STM32的IO口输入宏定义和输出宏定义。
位带操作简单的说,就是把每个比特膨胀为一个32位的字。
位带操作只在IO口输入输出时用的比较方便,其他日常开发很少使用。
#define BITBAND(addr, bitnum) ((addr & 0xF0000000)+0x2000000+((addr &0xFFFFF)<<5)+(bitnum<<2))
#define MEM_ADDR(addr) *((volatile unsigned long *)(addr))
#define BIT_ADDR(addr, bitnum) MEM_ADDR(BITBAND(addr, bitnum))
#define PAout(n) BIT_ADDR(GPIOA_ODR_Addr,n) //输出
#define PAin(n) BIT_ADDR(GPIOA_IDR_Addr,n) //输入
PAOUT(n) ----> BIT_ADDR(GPIOA_ODR_Addr, n) ---->
MEM_ADDR(BITBAND(GPIOA_ODR_Addr, n)) ---->
MEM_ADDR((GPIOA_ODR_Addr & 0xF0000000) + 0x20000000 + ((GPIOA_ODR_Addr)<<5) + (n<<2)) ---->
*((volatile unsigned long *)(((GPIOA_ODR_Addr & 0xF0000000) + 0x20000000 + ((GPIOA_ODR_Addr)<<5) + (n<<2))))
GPIOA_ODR_Addr左移5位,相当于乘以32,1个字膨胀为32个字,n左移2位,1个位 膨胀为4个字节(即一个字),最终相当于1个比特膨胀为1个字。
3、USART文件夹。
①printf函数的引入,在usart.c最前面加入下面代码,可以通过printf函数向串口发送我们需要的内容,方便查看代码执行情况以及一些变量值。
//加入以下代码,支持printf函数,而不需要选择use MicroLIB
#if 1
#pragma import(__use_no_semihosting)
//标准库需要的支持函数
struct __FILE
{
int handle;
};
FILE __stdout;
//定义_sys_exit()以避免使用半主机模式
_sys_exit(int x)
{
x = x;
}
//重定义fputc函数
int fputc(int ch, FILE *f)
{
while((USART1->SR&0X40)==0);//循环发送,直到发送完毕
USART1->DR = (u8) ch;
return ch;
}
#endif
正点原子使用的就是这种方法,通过串口1打印出需要的信息。
②keil c的标准库函数stdio.h,如下图:
默认的输出设备是显示器。
keil c标准库中的 printf 和 scanf 等输入输出数据流函数是通过 fputc 和 fgetc 实现底层操作的,需要在工程中重定义 fputc 和 fgetc 可以实现 printf 和 scanf 等数据流函数的重映射。
https://jingyan.baidu.com/article/cdddd41cabfd6453ca00e16a.html
半主机模式:半主机是用于ARM目标的一种机制,可将来自应用程序代码的输入/输出请求传送至运行调试器的主机。例如,使用此机制可以启用c库中的函数,如 printf() 或 scanf() ,来使用主机的屏幕和键盘,而不是在目标系统上配备屏幕和键盘。
重定义:重新再一次定义函数,使其拥有新的定义,然后完成新的功能的过程。
重定向:MDK原本目标是PC机的显示器,然后由于重定向,修改了 printf 的底层函数(重定义),使 printf 打印到单片机的外设中。
原因:因 printf 之类的函数,使用了半主机模式,使用标准库会导致程序无法运行,所以要不使用半主机模式。
解决方法一:使用MicroLib,虽然避免了半主机模式,但是开发板没有直接对目标(电脑的)显示器的使用权限,它必须使用外设(串口)发送数据到电脑的串口助手上面才能显示。并且需要重新定向到外设中,充定义 printf 底层的发送程序。程序如下:
//重定义fputc函数
int fputc(int ch, FILE *f)
{
while((USART1->SR&0X40)==0); //循环发送,直到发送完毕
USART1->DR = (u8) ch;
return ch;
}
注:必须勾选Use MicroLib,然后再调用重定义fputc函数,然后就可以printf函数发送数据了。
解决方法二:使用标准库,需要加入如下代码:
#if 1
#pragma import(__use_no_semihosting) //不使用半主机模式
//标准库需要的支持函数
struct __FILE
{
int handle;
};
FILE __stdout;
//定义_sys_exit()以避免使用半主机模式
_sys_exit(int x)
{
x = x;
}
//重定义fputc函数
int fputc(int ch, FILE *f)
{
while((USART1->SR&0X40)==0);//循环发送,直到发送完毕
USART1->DR = (u8) ch;
return ch;
}
#endif
③uart_init函数。初始化串口。代码如下:
void uart_init(u32 bound)
{
//定义变量
GPIO_InitTypeDef GPIO_InitStructure;
USART_InitTypeDef USART_InitStructure;
NVIC_InitTypeDef NVIC_InitStructure;
//使能GPIO时钟和USART时钟
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA | RCC_APB2Periph_GPIOA, ENABLE);
//GPIO端口设置
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_9;
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP;
GPIO_Init(GPIOA, &GPIO_InitStructure);
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_10;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IN_FLOATING;
GPIO_Init(GPIOA ,&GPIO_InitStructure);
//NVIC 内嵌向量中断控制器设置
NVIC_InitStructure.NVIC_IRQChannel = USART1_IRQn;
NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 3;
NVIC_InitStructure.NVIC_IRQChannelSubPriority = 3;
NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE;
NVIC_Init(&NVIC_InitStructure);
//USART设置
USART_InitStructure.USART_BaudRate = bound;
USART_InitStructure.USART_WordLength = USART_WordLength_8b;
USART_InitStructure.USART_StopBits = USART_StopBits_1;
USART_InitStructure.USART_Parity = USART_Parity_No;
USART_InitStructure.USART_HardWareFlowControl = USART_HardWareFlowControl_None;
USART_InitStructure.USART_Mode = USART_Mode_Rx | USART_Mode_Tx;
USART_Init(USART1, &USART_InitStructure); //串口初始化
USART_ITConfig(USART1, USART_IT_RXNE, ENABLE); //设置串口中断
USART_Cmd(USART1, ENABLE); //使能串口
}
④USART1_IRQHandler函数。中断响应函数,代码如下:
void USART1_IRQHandler(void)
{
u8 Res;
#if SYSTEM_SUPPORT_OS //判断是否支持操作系统
OSIntEnter();
#endif
if(USART_GetITStatus(USART1, USART_IT_RXNE) != RESET) //接收中断
{
Res = USART_ReceiveData(USART1); //读取接收到的数据
if((USART_RX_STA & 0x8000)==0) //接收未完成
{
if(USART_RX_STA & 0X4000) //接收到0d
{
if(Res != 0x0a) //接收错误,接收0d后不是0a
USART_RX_STA = 0;
else //接收正确,接收od后接收0a,数据接收完成
USART_RX_STA |= 0x8000;
}
else //没有接收到0d
{
if(Res == 0x0d) //接收到0d
USART_RX_STA |= 0x4000;
else //没有接收到0d
{
USART_RX_BUF[USART_RX_STA & 0x3FFF] = Res;
USART_RX_STA++;
if(USART_RX_STA > (USART_REC_LEN-1))
USART_RX_STA= 0;
}
}
}
}
#if SYSTEM_SUPPORT_OS
OSIntExit();
#endif;
#endif
}
协议:EN_USART1_RX和USART_REC_LEN都是在usart.h头文件中定义。
需要使用串口接收数据时,只要设置EN_USART1_RX为1,不用串口接收数据只要设置EN_USART1_RX为0,这样可以省sram和flash,默认接收中断开启。
SYSTEM_SUPPORT_OS判断是否使用OS,使用OS则调用OSIntEnter和OSIntExit。。
#define USART_REC_LEN 200 //定义最大接收字节数 200
#define EN_USART1_RX 1
extern u8 USART_RX_BUF[USART_REC_LEN];
extern u16 USART_RX_STA;
usart.h头文件中有设置。
小小的协议:通过这个函数,配合一个数组USART_RX_BUF[]和一个状态寄存器USART_RX_STA实现串口数据的接收。
串口输入的数据,需要通过串口打印出来时,执行代码如下:
if(USART_RX_STA & 0x8000)
{
len = USART_RX_STA & 0X3fff; //得到数据长度
printf("\r\n您发送的消息为:\r\n\r\n");
for(t=0;t<len;t++)
{
USART_SendData(USART1, USART_RX_BUF[t]); //通过串口1发送数据
while(USART_GetFlagStatus(USART1, USART_FLAG_TC)!=SET); //等待数据发送完成
}
printf("\r\n\r\n\r\n"); //插入换行
USART_RX_STA = 0; //自定义的状态寄存器清零
}
⑤正点原子程序默认USART1打印数据,需要改为USART2时,需要修改如下:
A.需要重定义fputc函数,如下:
int fputc(int ch, FILE *f)
{
while((USART2->SR&0X40)==0);//循环发送,直到发送完毕
USART2->DR = (u8) ch;
return ch;
}
B.初始化代码如下:
void uart_init(u32 bound){
//GPIO端口设置
GPIO_InitTypeDef GPIO_InitStructure;
USART_InitTypeDef USART_InitStructure;
NVIC_InitTypeDef NVIC_InitStructure;
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE);
RCC_APB1PeriphClockCmd(RCC_APB1Periph_USART2, ENABLE);
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_2; //PA.2
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP; //复用推挽输出
GPIO_Init(GPIOA, &GPIO_InitStructure);//初始化GPIOA.2
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_3;//PA3
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IN_FLOATING;//浮空输入
GPIO_Init(GPIOA, &GPIO_InitStructure);//初始化GPIOA.3
//Usart2 NVIC 配置
NVIC_InitStructure.NVIC_IRQChannel = USART2_IRQn;
NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority=3 ;//抢占优先级3
NVIC_InitStructure.NVIC_IRQChannelSubPriority = 3; //子优先级3
NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE; //IRQ通道使能
NVIC_Init(&NVIC_InitStructure); //根据指定的参数初始化VIC寄存器
//USART 初始化设置
USART_InitStructure.USART_BaudRate = bound;//串口波特率
USART_InitStructure.USART_WordLength = USART_WordLength_8b;//字长为8位数据格式
USART_InitStructure.USART_StopBits = USART_StopBits_1;//一个停止位
USART_InitStructure.USART_Parity = USART_Parity_No;//无奇偶校验位
USART_InitStructure.USART_HardwareFlowControl = USART_HardwareFlowControl_None;//无硬件数据流控制
USART_InitStructure.USART_Mode = USART_Mode_Rx | USART_Mode_Tx; //收发模式
USART_Init(USART2, &USART_InitStructure); //初始化串口2
USART_ITConfig(USART2, USART_IT_RXNE, ENABLE);//开启串口接受中断
USART_Cmd(USART2, ENABLE);
}
C.编写串口2的中断服务函数。
void USART2_IRQHandler(void) //串口1中断服务程序
{
u8 Res;
#if SYSTEM_SUPPORT_OS //如果SYSTEM_SUPPORT_OS为真,则需要支持OS.
OSIntEnter();
#endif
if(USART_GetITStatus(USART2, USART_IT_RXNE) != RESET) //接收中断(接收到的数据必须是0x0d 0x0a结尾)
{
Res =USART_ReceiveData(USART2); //读取接收到的数据
if((USART_RX_STA&0x8000)==0)//接收未完成
{
if(USART_RX_STA&0x4000)//接收到了0x0d
{
if(Res!=0x0a)USART_RX_STA=0;//接收错误,重新开始
else USART_RX_STA|=0x8000; //接收完成了
}
else //还没收到0X0D
{
if(Res==0x0d)USART_RX_STA|=0x4000;
else
{
USART_RX_BUF[USART_RX_STA&0X3FFF]=Res ;
USART_RX_STA++;
if(USART_RX_STA>(USART_REC_LEN-1))USART_RX_STA=0;//接收数据错误,重新开始接收
}
}
}
}
#if SYSTEM_SUPPORT_OS //如果SYSTEM_SUPPORT_OS为真,则需要支持OS.
OSIntExit();
#endif
}
⑥两个串口共用printf函数。
问题描述:
多串口共用printf函数,百度到的资料大部分是建议重新写一个xx_printf(format, …)。但是使用起来还是不方便,就此问题而言加上一个判断语句便可解决。
解决方法:
printf函数最后调用的是int fputc(int ch, FILE *f),那么重新改写此函数便可。
代码:
//标志量定义
int USART_PRINTF_FLAG = 2;//默认串口2
//改写fputc
int fputc(int ch, FILE *f)
{
if (USART_PRINTF_FLAG == 2)
{
while(USART_GetFlagStatus(USART2,USART_FLAG_TC)==RESET);
USART_SendData(USART2,(uint8_t)ch);
}
else
{
while(USART_GetFlagStatus(USART1,USART_FLAG_TC)==RESET);
USART_SendData(USART1,(uint8_t)ch);
}
return ch;
}
//中断处理
void USART1_IRQHandler(void)
{
USART_PRINTF_FLAG = 1;
//your coding here...
}
void USART2_IRQHandler(void)
{
USART_PRINTF_FLAG = 2;
//your coding here...
}
本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)