1. 简介:
自己写此部分的程序即耗时,也不明智,因此,此篇文章是关于移植和分析的文章
2. 移植 uart 程序:
参照的程序是 红牛开发板/例程-Example/二、基础例程/ 下的有关 usart 部分
在做uart部分之前,需要参照之前章程搭建固件库和FreeRTOS工程,搭建完成之后,将上边目录下的 driver 中的 driver 中的delay.c、delay.h、USART.c、USART.h拷贝到 PROJ\uart 目录下,部分截图如下:
在iar软件中,将新添加的两个 .c 文件放到usr目录下:
在包含进 .c 后还需要添加他们相应的 .h 文件的搜索路径:...PROJ\uart\driver
在程序中我们用到了 printf ,这个函数需要 Full 版本library 的支持:
主函数 main.c
#include <stdio.h>
#include "misc.h"
/* Scheduler includes. */
#include "FreeRTOS.h"
#include "task.h"
#include "queue.h"
#include "USART.h"
#include "delay.h"
#define LED_D1_ON() GPIO_ResetBits(GPIOF, GPIO_Pin_6)
#define LED_D1_OFF() GPIO_SetBits(GPIOF, GPIO_Pin_6)
static void LED_Init(void);
static void LED_D1_Task(void *pvParameters);
static void UART1_Task(void *pvParameters);
int main(void)
{
LED_Init(); // 初始化 LED 引脚
USART1_Init();
Delay_Init();
Delay_ms(10);
SendMessage();
printf("write by lip!\n\r");
xTaskCreate(LED_D1_Task, "LED_D1", 1000, NULL, 3, NULL);
xTaskCreate(UART1_Task, "UART1", 1000, NULL, tskIDLE_PRIORITY + 3, NULL);
/* 启动调度器 */
vTaskStartScheduler();
while(1);
return 0;
}
void LED_Init(void)
{
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOF, ENABLE); // 使能 GPIOF 的时钟
GPIO_InitTypeDef GPIO_InitStructure;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP;
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_6;
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_10MHz;
GPIO_Init(GPIOF, &GPIO_InitStructure);
}
void LED_D1_Task(void *pvParameters)
{
while ( 1 )
{
LED_D1_ON();
vTaskDelay(500 / portTICK_RATE_MS);
LED_D1_OFF();
vTaskDelay(500 / portTICK_RATE_MS);
}
}
void UART1_Task(void *pvParameters)
{
while(1)
{
vTaskDelay(100 / portTICK_RATE_MS);
if(data_length)
{
USART1_SendString(rx_buffer, data_length);
data_length = 0;
}
}
}
我们还需要在 usr 目录下 stm32f10x_it.c 中实现一下 usart 的中断入口,以便支持用户的输入:
在 void NMI_Handler(void) 函数上边添加:
void USART1_IRQHandler(void)
{
u8 res;
if(USART_GetITStatus(USART1, USART_IT_RXNE) != RESET) // 接收到数据
{
res = USART_ReceiveData(USART1); // 读取接收到的数据USART1->DR
if(data_length < DATA_BUF_SIZE)
{
rx_buffer[data_length] = res; // 记录接收到的值
data_length++;
}
}
}
当然 .h 中的声明不要忘记:
至此,可以 make、下载了,用串口助手连接上串口一,在开机的瞬间会看到打印一串的数据,并等待用户输入,当用户输入时,会原封不动的打印出来。
【1】为了尊重源作者,打印信息并没有大范围改动
3. 分析固件库中的 delay 延时:
delay 函数的实现应用的是 m3 核的 systick,是一个硬件部分,一个 24 位的倒计数定时器,当倒计时到0值时会从LOAD寄存器装载新值,还有一点的是当enable不清零的情况,他的运作就不会停息
涉及到固件库中的结构体如下:
#define __IO volatile
typedef struct
{
__IO uint32_t CTRL; /* Offset: 0x00 SysTick Control and Status Register */
__IO uint32_t LOAD; /* Offset: 0x04 SysTick Reload Value Register */
__IO uint32_t VAL; /* Offset: 0x08 SysTick Current Value Register */
__I uint32_t CALIB; /* Offset: 0x0C SysTick Calibration Register */
} SysTick_Type;
用于将首地址强制转化成此结构体,方便用户使用
SysTick寄存器说明如下:
【1】控制和状态寄存器 CTRL :
位段 | 名称 | 类型 | 描述 |
16 | COUNTFLAG | 只读 | 计数到0时置1,读取该位将清0 |
2 | CLKSOURCE | 可读可写 | 时钟来源 0=HCLK/8,1=HCLK |
1 | TICKINT | 可读可写 | 1=计数到0时产生SysTick异常请求 |
0 | ENABLE | 可读可写 | 定时器的开关,1使能 |
【2】重装在寄存器 LOAD :
位段 | 名称 | 类型 | 描述 |
23:0 | RELOAD | 可读可写 | 当计数到0时将被重装载的值 |
【3】当前值寄存器:
位段 | 名称 | 类型 | 描述 |
23:0 | CURRENT | 可读可写 | 读取时返回当前倒计数的值;向该寄存器 写入任意值都可以将其清除变为0。 清0 该寄存器还会导致CTRL寄存器的 COUNTFLAG 位清零 |
【4】校准寄存器:没见用到
关键函数:(初始化部分)
static uint8_t sysclk = 72; //默认系统时钟为72MHz
static uint8_t fac_us = 0; //us延时倍乘数
static uint16_t fac_ms = 0; //ms延时倍乘数
/**
* @brief 基准延时初始化,使用SysTick;
* SysTick时钟源由HCLK/8提供,当系统频率为72MHz时
* 最小计数周期为1/9MHz,计满9次为1us,fac_us = 9作为单位
* @param None
* @retval None
*/
void Delay_Init(void)
{
SysTick_CLKSourceConfig(SysTick_CLKSource_HCLK_Div8); //选择系统定时器的时钟源
fac_us = sysclk / 8;
fac_ms = (u16)fac_us * 1000;
}
(毫秒部分)
/**
* @brief 毫秒级延时,SysTick->LOAD为24位寄存器,
* 所以最大延时小于或等于1864ms
* @param nms(ms number) 毫秒数 不能超过1864
* @retval None
*/
void Delay_ms(u16 nms)
{
u32 temp;
SysTick->LOAD = (u32)nms*fac_ms; //时间加载(SysTick->LOAD为24bit)
SysTick->VAL = 0x00; //清空计数器
SysTick->CTRL = 0x01 ; //开始倒数
do
{
temp = SysTick->CTRL;
}
while(temp&0x01 && !(temp&(1<<16))); //等待时间到达
SysTick->CTRL = 0x00; //关闭计数器
SysTick->VAL = 0x00; //清空计数器
}
(微妙部分)
/**
* @brief 实现微秒级延时,最大延时1864ms
* 这两个函数是通过寄存器配置的,没有对应的固件库函数,参考misc.h
* @param us(number us) 延时微秒数 最大1864135
* @retval None
*/
void Delay_us(u32 nus)
{
u32 temp;
SysTick->LOAD = nus * fac_us; //时间加载 fac_us=9,走9次就是1us
SysTick->VAL = 0x00; //清空计数器
SysTick->CTRL = 0x01; //开始倒数
do
{
temp = SysTick->CTRL;
}
while(temp&0x01 && !(temp&(1<<16))); //等待时间到达
SysTick->CTRL = 0x00; //关闭计数器
SysTick->VAL = 0x00; //清空计数器
}
此部分比较简单,不再分析。
4. 分析 printf 的实现:
根据 Contex-M3 权威指南中的 《使用UART输出“Hello World”》 章节,更改了开发版提供的源代码,源代码存在以下问题:
a. 不同的串口工具,输出结果不同:经测试 secureCRT、sscom
b. 每次使用printf输出,字符串中必须在字符串的前后都加上 \n\r
#ifdef __GNUC__
/* With GCC/RAISONANCE, small printf (option LD Linker->Libraries->Small printf
set to 'Yes') calls __io_putchar() */
#define PUTCHAR_PROTOTYPE int __io_putchar(int ch)
#else
#define PUTCHAR_PROTOTYPE int fputc(int ch, FILE *f)
#endif /* __GNUC__ */
/*******************************************************************************
* Function Name : 文本输出的tetargeting代码
* Description : 把USART输出定向到printf,modified by lip
* Input : None
* Output : None
* Return : None
*******************************************************************************/
PUTCHAR_PROTOTYPE
{
if ( ch == '\n')
{
while (USART_GetFlagStatus(USART1, USART_FLAG_TC) == RESET);
USART_SendData(USART1, '\r'); // 输入附加的CR以使字符串被正常显示
USART_SendData(USART1, '\n');
}
else
{
while (USART_GetFlagStatus(USART1, USART_FLAG_TC) == RESET); // 等待发送完成
USART_SendData(USART1, (u8)ch);
}
return ch;
}
【1】要想实现 printf输出到串口上,我们重新实现了 fputc 函数
【2】在 main.c 函数中需要包含 stdio.h 头文件
【3】在工程中还需要配置如下:
【4】我们这里之重新定向了输出,输入并没有实现,因此 scanf 不能使用
【5】\r 的意思是回车,就是将时候的行的位置放在行首,\n 的意思是换行,就是重新开始新的一行
5. usart 的库函数:
【1】我们依旧从初始化开始:
/**
* @brief USART1初始化配置 包括GPIO初始化 TX必须配置为复用输出
* @param None
* @retval None
*/
void USART1_Init(void)
{
GPIO_InitTypeDef GPIO_InitStructure;
USART_InitTypeDef USART_InitStructure;
NVIC_InitTypeDef NVIC_InitStructure;
RCC_APB2PeriphClockCmd( RCC_APB2Periph_USART1 | RCC_APB2Periph_GPIOA , ENABLE );
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_9;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP; //复用推挽输出
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
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);
USART_InitStructure.USART_BaudRate = 115200 ;
USART_InitStructure.USART_WordLength = USART_WordLength_8b; //8个数据位
USART_InitStructure.USART_StopBits = USART_StopBits_1; //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);
NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2); //中断分组2
NVIC_InitStructure.NVIC_IRQChannel = USART1_IRQn; //USART1接收中断
NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 0; //先占优先级0
NVIC_InitStructure.NVIC_IRQChannelSubPriority = 0; //次占优先级
NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE;
NVIC_Init(&NVIC_InitStructure);
}
【2】开启时钟:
RCC_APB2PeriphClockCmd(RCC_APB2Periph_USART1 | RCC_APB2Periph_GPIOA, ENABLE);
打开 UART1 的时钟,和相应的引脚的时钟
【3】USART 结构体 USART_InitTypeDef :
/**
* @brief USART Init Structure definition
*/
typedef struct
{
uint32_t USART_BaudRate; /* 波特率 */
uint16_t USART_WordLength; /* 指定接收和发送的数据的位宽 */
uint16_t USART_StopBits; /* 停止位 */
uint16_t USART_Parity; /* 奇偶校验 */
uint16_t USART_Mode; /* uart的模式,可以是接收或者发送 */
uint16_t USART_HardwareFlowControl; /* 硬件流控,一般不需要 */
} USART_InitTypeDef;
其中:
USART_BaudRate 可以取值:直接是数字,如果是 115200 ,直接写这个值就行
USART_WordLength 可以取值:USART_WordLength_8b、USART_WordLength_9b
USART_StopBits 可以取值:USART_StopBits_1、USART_StopBits_0_5、USART_StopBits_2、USART_StopBits_1_5
USART_Parity 可以取值:USART_Parity_No(没有奇偶校验位)、USART_Parity_Even(偶校验)、USART_Parity_Odd(奇校验)
USART_Mode 可以取值:USART_Mode_Rx、USART_Mode_Tx
USART_HardwareFlowControl 可以取值:USART_HardwareFlowControl_None(没有硬件流控,一般是这个,其他的不再列出)
USART_Init 函数:
功能是将 USART_InitTypeDef 结构体,相应的值赋值给寄存器中相应的位
USART_ITConfig() 函数:
功能是打开/关闭串口的中断
函数原型:
/**
* @brief 打开或者关闭指定的 USART 中断.
* @param USARTx: Select the USART or the UART peripheral.
* 这个可选值如下:
* USART1, USART2, USART3, UART4 or UART5.
* @param USART_IT: 指定将被打开或关闭的 USART 的中断源.
* This parameter can be one of the following values:
* @arg USART_IT_CTS: CTS change interrupt (not available for UART4 and UART5)
* @arg USART_IT_LBD: LIN Break detection interrupt
* @arg USART_IT_TXE: Transmit Data Register empty interrupt
* @arg USART_IT_TC: Transmission complete interrupt
* @arg USART_IT_RXNE: Receive Data register not empty interrupt
* @arg USART_IT_IDLE: Idle line detection interrupt
* @arg USART_IT_PE: Parity Error interrupt
* @arg USART_IT_ERR: Error interrupt(Frame error, noise error, overrun error)
* @param NewState: new state of the specified USARTx interrupts.
* This parameter can be: ENABLE or DISABLE.
* @retval None
*/
void USART_ITConfig(USART_TypeDef* USARTx, uint16_t USART_IT, FunctionalState NewState)
USART_Cmd() 函数:
功能是打开/关闭串口
/**
* @brief 打开或关闭 USART 外设.
* @param USARTx: Select the USART or the UART peripheral.
* This parameter can be one of the following values:
* USART1, USART2, USART3, UART4 or UART5.
* @param NewState: new state of the USARTx peripheral.
* This parameter can be: ENABLE or DISABLE.
* @retval None
*/
void USART_Cmd(USART_TypeDef* USARTx, FunctionalState NewState)
6. 代码的大小
经过将所有的调试信息去掉后,输出的目标文件的大小如下:(设置方法看前边的章节:点击这里)