1 UART协议
-
USART是一种全双工、异步通讯协议,常用来串口调试。
-
起始位:一个机器周期的低电平。
-
数据位:8个机器周期的高低电平序列。
-
校验位:奇偶校验,校验位加有效数据位为奇数个或偶数个。
-
停止位:1个或者两个连续机器周期的低电平。
-
波特率:发送一位数据的速率。
115200 8nl # 无校验,加上停止位和起始位一共10位数据
发送1bit的时间:1/115200
发送1byte的时间:1/11520
那么一秒发送字节数:11520
2 USART框图
3 USART接线
4 寄存器介绍
4.1 uart线控制寄存器(ULCONn)
- 设置无红外、无校验、一个停止位、8位字长。
- ULCON0 = 0x03
4.2 uart控制寄存器(UCON0)
- 选择时钟源为PCLK,配置RX与TX为中断或轮询。
4.3 FIFO控制寄存器(UFCONn)
- 先别管,FIFO用在大量数据传输上。
4.4 uart调制调解控制器(UMCONn)
- 与流控相关,关闭就行。
4.5 TXD和TRD状态寄存器(UTRSTATn)
- 移位寄存器->发送缓冲区->接收缓存缓冲区。
- 只要发射机为NULL,表名移位寄存器空了,意味着可以写入数据,否则上次的还没发出去。
- 发送缓冲区为NULL,结束了本次发送。
- 接收缓冲区为NULL,结束了整个接收。
- 用来检测发送和接收是否结束。
4.6 错误状态寄存器(UERSTATn)
- 用来检测错误,暂时不管。
4.7 FIFO状态寄存器(UFSTATn)
- 暂时不管。
4.8 调制调节器状态寄存器(UMATATn)
- 暂时不管。
4.9 发送缓冲区(UTXHn)
- 8位对应了发送的数据。
- 需要将头文件里的地址改为char *,使用小端模式。
"s3c2440_soc.h"
#define __REG_BYTE(x) (*(volatile unsigned char *)(x))
#define UTXH0 __REG_BYTE(0x50000020) //UART 0 transmission hold
#define URXH0 __REG_BYTE(0x50000024) //UART 0 receive buffer
4.10 接收缓冲区寄存器(URXHn)
- 8位对应着接收缓冲区的值。
4.11 波特率除位寄存器(UBRDIVn)
-
UBRDIVn:波特率分频寄存器的值,需要计算后人为分配,范围16位。
-
UART clock:可以使PCLK、FCLK、UEXTCLK(小于PCLK),需要选择。
-
UCONn:用来选择哪一个作为时钟源,[11,10] = 00时选择PCLK,此处为UART0,故配置UCON0。
5 设置流程
(1)配置引脚为上拉模式:
需要先配置GPH3和GPH2的GPHCON寄存器,设置模式。
还需要配置GPHUP的2、3位为上拉模式。
(2)配置数据格式:ULCON0=0x03,配置数据位8n1。
(3)配置时钟源:UCON0=0x05,配置时钟源为PCLK,中断或轮询模式。
(4)配置波特率:UBRDIV0 =26,经过公式计算可知。
(5)判断发送接受状态: 读取UTRSTAT0寄存器判断发送或接收结束。
(6)发送或接收:写发送缓冲寄存器UTXH0或读取接收缓冲寄存器URXH0。
6 实现UART打字显示
6.1 uart.h
#ifndef UART_H__
#define UART_H__ /*防止重复包含*/
void uart_init(void);
int putchar(int c);
int getchar(void);
int puts(const char *s);
#endif
6.2 uart.c
#include "s3c2440_soc.h"
void uart_init(void){
/*******配置引脚的工作模式以及上拉******/
GPHCON &= ~((3 << 4) | (3 << 6)); /*清零*/
GPHCON |= (2 << 4) | (2 << 6); /*配置模式为2*/
GPHUP &= ~(1 << 2 | 1 << 3);/*将引脚电平上拉*/
/***************************************/
/**************设置数据格式***************/
ULCON0 = 3; /*无校验、无红外、8位字长、一个停止位*/
/*****************************************/
/**************设置时钟源***************/
UCON0 = 5; /*PCLK模式、中断轮询模式*/
/***************************************/
/**************设置波特率***************/
UBRDIV0 = 26; /*波特率分频器*/
/***************************************/
}
int putchar(int c){
/*检测发送标志位 UTRSTAT的第二位置为1,则发送缓冲区空了可以放数据*/
while(!(UTRSTAT0 & (1 << 2)));
UTXH0 = (unsigned char)c; /*为NULL才赋值给缓冲区*/
}
int getchar(void){
/*检测发送标志位 UTRSTAT的第0位置为1,则接收缓冲区已经收到数据*/
while(!(UTRSTAT0 & (1 << 0)));
return (int)URXH0;
}
int puts(const char *s)
{
/*使用指针来控制 *(s + 1) 则为下一个地址的值,结束条件不为NULL*/
while(*s != '\0'){
putchar(*s);
s++;
}
}
6.3 uart_test.c
#include "s3c2440_soc.h"
#include "uart.h"
int main(void){
unsigned char c;
uart_init();
puts("write test\r\n");
while(1){
c = getchar();
if(c == '\r'){
putchar('\n');
}
if(c == '\n'){
putchar('\r');
}
putchar(c);
}
return 0;
}
6.4 start.S
.text
.global _start
_start:
/*========= 设置看门狗==========*/
mov r0,#0
ldr r1,=0x53000000
str r0,[r1]
/*========= 设置时钟 ==========*/
/*配置locktime*/
ldr r0,=0xFFFFFFFF
ldr r1,=0x4C000000
str r0,[r1]
/*配置CLKDIVN*/
mov r0,#5
ldr r1,=0x4C000014
str r0,[r1]
/* 设置CPU工作于异步模式 */
mrc p15,0,r0,c1,c0,0
orr r0,r0,#0xc0000000 //R1_nF:OR:R1_iA
mcr p15,0,r0,c1,c0,0
/*配置MPLLCON*/
ldr r0,=(92 << 12)|(1 << 4) |(1 << 0)
ldr r1,=0x4C000004
str r0,[r1]
/*=========检查nand还是nor启动==========*/
/*假设sp从nand启动*/
ldr sp,=0x40000000+4096
/*读取地址0的数据到r1*/
mov r0,#0
ldr r1,[r0]
/*写0到0地址*/
str r0,[r0]
/*将修改后的值放到r2*/
ldr r2,[r0]
/*判断修改成功与否*/
cmp r0,r2
/*如果相等则为nor启动,将栈修改位4096*/
moveq sp,#4096
/*如果为nor启动需要恢复原来的值*/
str r1,[r0]
/*=========跳转到main,并保留pc指针============*/
bl main
/*=======死循环=========*/
_halt:
b _halt
6.5 Makefile
all:
# 生成目标文件
arm-linux-gcc -c -o uart.o uart.c -nostdlib # c代码都需要不调用标准库
arm-linux-gcc -c -o uart_test.o uart_test.c -nostdlib
arm-linux-gcc -c -o start.o start.S
arm-linux-ld -Ttext 0 start.o uart.o uart_test.o -o uart.elf
arm-linux-objcopy -O binary -S uart.elf uart.bin
arm-linux-objdump -D uart.elf > uart.dis
clean:
rm *.bin *.o *.elf *.dis
7 printf底层
7.1 printf的参数存放
#include <stdio.h>
int printf(const char *format, ...);
/*
format 为可变参数,存放第一个参数的地址,其地址加sizeof(char *)为下一个内容的地址。
再对地址的内容解引用,按照相应的格式输出即可。
*/
7.2 结构体地址对齐
#include <stdio.h>
/*
可变参数是用栈存储的
format是第一个元素的地址
format + sizeof(char *)为下一个可变参数地址
浮点数默认为double,传参后默认为double类型
*/
struct Stu{
char *name;
int age;
char score;
int height;
}__attribute((packed));
void push_test(const char *format,...){
/*
① 取值
② 移动到下一个可变参数
*/
char *p = (char *)(&format);
struct Stu s;
/***取值***/
printf("format = %s\n",format); /*打印format*/
/***移动到下一个地址***/
p += sizeof(char *);
printf("... = %d\n",*((int*)p)); /*打印整数*/
p += sizeof(char *);
s = *(struct Stu*)p;
printf("stu.name = %s\nstu.age = %d\nstu.score=%c\nstu.height=%d\n",s.name,s.age,s.score,s.height);
}
int main(int argc,char **argv){
struct Stu s = {"simon",18,'A',180};
/*
结构体默认4字节对齐,相当于结构体定义后面加了 __attribute((aligned(4)))
加__attribute((packed))取消对齐
*/
printf("sizeof(Stu) = %d\n",sizeof(s));
printf("1_%p\n",&(s.name));
printf("2_%p\n",&(s.age));
printf("3_%p\n",&(s.score));
printf("4_%p\n",&(s.height));
push_test("abc",123,s);
return 0;
}
7.3 操作可变参数的宏定义
![7_22](H:\嵌入式笔记\100Ask\images\7_22.bmp)/*可变参数宏的含义*/
#include <stdarg.h>
va_list p; // 相当于指针p
va_start(p,format); // 相当于移动p + sizeof(char *) ,这一步后面的就是可变参数了
va_arg( p, 变量类型); // 相当于先取值给val再移动地址
va_end( p ); // 相当于让p指向NULL,避免野指针
/*可变参数宏的底层 vc6.0*/
/*定义一个char *指针用来保存参数地址*/
typedef char * va_list;
/*宏实现地址对齐的移动,如果4字节对齐,那么便宜量为4*/
#define _INTSIZEOF(n) ( (sizeof(n) + sizeof(int) - 1) & ~(sizeof(int) - 1) )
/*相当于取format的地址并加上一个偏移量*/
#define va_start(ap,v) ( ap = (va_list)&v + _INTSIZEOF(v) )
/*
完成取值和移动指针
先将地址移动4字节,再将移动后的地址减4字节的值取出来
*/
#define va_arg(ap,t) ( *(t *)((ap += _INTSIZEOF(t)) - _INTSIZEOF(t)) )
/*将指针设置为NULL,避免野指针*/
#define va_end(ap) ( ap = (va_list)0 )
8 自定义printf实现arm移植
8.1 bsp_uart.h
#ifndef BSP_UART_H__
#define BSP_UART_H__
void uart_init(void);
int putchar(int c);
int getchar(void);
#endif
8.2 bsp_uart.c
#include "s3c2440_soc.h"
#include "bsp_uart.h"
void uart_init(void){
/*******配置引脚的工作模式以及上拉******/
GPHCON &= ~((3 << 4) | (3 << 6)); /*清零*/
GPHCON |= (2 << 4) | (2 << 6); /*配置模式为2*/
GPHUP &= ~(1 << 2 | 1 << 3);/*将引脚电平上拉*/
/***************************************/
/**************设置数据格式***************/
ULCON0 = 3; /*无校验、无红外、8位字长、一个停止位*/
/*****************************************/
/**************设置时钟源***************/
UCON0 = 5; /*PCLK模式、中断轮询模式*/
/***************************************/
/**************设置波特率***************/
UBRDIV0 = 26; /*波特率分频器*/
/***************************************/
}
int putchar(int c){
/*检测发送标志位 UTRSTAT的第二位置为1,则发送缓冲区空了可以放数据*/
while(!(UTRSTAT0 & (1 << 2)));
UTXH0 = (unsigned char)c; /*为NULL才赋值给缓冲区*/
}
int getchar(void){
/*检测发送标志位 UTRSTAT的第0位置为1,则接收缓冲区已经收到数据*/
while(!(UTRSTAT0 & (1 << 0)));
return (int)URXH0;
}
8.3 bsp_io.h
#ifndef BSP_IO_H__
#define BSP_IO_H__
/*******************头文件*****************/
#define GCC_M__ 0 /*调gcc的stdio库*/
#define UART_M__ 1 /*调arm开发板的uart库*/
#if GCC_M__
#include <stdio.h>
#elif UART_M__
#include "bsp_uart.h"
#endif
/********************定义宏命令************/
/*使用m_变量去调用原始的putchar可能是stdio也可能是uart的*/
#define m_putchar putchar
/*相当于一个char指针*/
typedef char * va_list;
/*获得4字节对齐的指针偏移量*/
#define _INTSIZEOF(n) ( (sizeof(n) + sizeof(int) - 1) & ~(sizeof(int) - 1) )
/*获取地址加4*/
#define va_start(ap,v) ( ap = (va_list)&v + _INTSIZEOF(v) )
/*将取值和地址加4整合*/
#define va_arg(ap,t) ( *(t *)( ap=ap + _INTSIZEOF(t), ap- _INTSIZEOF(t)) )
/*将指针置为NULL,避免野指针*/
#define va_end(ap) ( ap = (va_list)0 )
/*****************函数的声明***************/
/*printf函数的改写*/
int printf(const char *fmt, ...);
#endif
8.4 bsp_io.c
#include "bsp_io.h" /*引入头文件*/
/*打印字符串*/
static int m_puts(const char *s){
while(*s != '\0'){
m_putchar(*s++);
}
return 0;
}
/**********************打印数字*********************/
/*定义一个数组,里面存16进制的0~f*/
unsigned char hex_table[] = {'0','1','2','3','4','5','6','7',\
'8','9','a','b','c','d','e','f'};
/*
参数:n——数字 base——进制 lead——前面的数是0还是空格 maxwidth 总长度
如 (10,2,0,4); 0000 1010 前部补4个lead
思路:
1. 先找最后字符串的末尾指针地址
2. 判断n为正数还是负数,取绝对值
3. 将n的base的余数作为数组的下表查找对应字符放到字符串中
4. 将lead部分放到前面的地址
5. 先打印正负号,再puts整个字符串
*/
static int out_num(long n, int base,char lead,int maxwidth){
unsigned long m = 0;/*无符号数记录n的绝对值*/
char buf[64]; /*声明一个字符数组*/
/*s指向即最后一个元素的尾地址*/
char *s = buf + sizeof(buf);
int count = 0,i = 0;
/*先将buf的末尾改为NULL字符*/
/*此时s需要减一个地址才会等到最后一个元素的起始地址*/
*--s = '\0';
/*判断正还是负,打印符号*/
if(n < 0){
m = -n;
}else{
m = n;
}
/*将后面的元素全部保存为字符串且存在字符数组后半段*/
do{
*--s = hex_table[m%base];
count ++; /*记录数字出现的次数,便于后面求0的个数*/
m /= base;
}while(m);
/*将0 或 空格保存到字符数组*/
i = maxwidth - count;
while(i > 0){
*--s = lead;
i --;
}
/*把符号保存到字符数组中*/
if(n > 0){
*--s = '+';
}else{
*--s = '-';
}
/*打印s*/
m_puts(s);
return 0;
}
/*****************静态函数去处理printf中的打印内容*************/
static int help_printf(const char *format, va_list p){
/*
* 1. 没遇到%号的连续打印。
* 2. 遇到%号的需要判断。
* %x 十六进制
* %d 十进制打印
* %u 无符号十进制打印
* %c 字符打印
* %s 输出字符串
* 0 以0填充
* 默认 以空格填充
*/
char leap = ' '; /*默认空格填充*/
int maxwidth = 0;
/******遍历format字符串*****/
while(*format != '\0'){
/*当字符串内容为'\0'打印结束*/
if(*format == '%'){
/*当遇到了%则进入循环打印出该格式符号对应的字符串*/
format++;
if(*format == '0'){
/*代表有数字0填充*/
leap = '0';
format++;
}
/*接下来就应该判断规定了多长的打印长度*/
while(*format >= '0' && *format <= '9'){
/*当字符为0~9的数字时,需要将其拼接*/
maxwidth *= 10; /*先将自身乘以10,加上format的值*/
maxwidth += (*format-'0');
format++;
}
/*处理最后的修饰符号*/
switch(*format){
case 'd': /*打有符号十进制数*/
out_num(va_arg(p,int), 10, leap, maxwidth);
break;
case 'u': /*打印无符号十进制数*/
out_num(va_arg(p,unsigned int),10,leap,maxwidth);
break;
case 'x': /*打印十六进制数*/
out_num(va_arg(p,int),16,leap,maxwidth);
break;
case 'o': /*打印八进制数*/
out_num(va_arg(p,int),8,leap,maxwidth);
break;
case 'c': /*打印字符*/
m_putchar(va_arg(p,char));
break;
case 's': /*打印字符串*/
m_puts(va_arg(p, char*));
break;
}
}else{
/*不为%正常输出*/
m_putchar(*format);
}
/*每次都会地址加1*/
format ++;
}
return 0;
}
/***************printf函数的改写*********************/
int printf(const char *format, ...){
/*声明一个指针*/
va_list p;
/*将指针指向第一个变参位置*/
va_start(p,format);
/*调用函数处理中间的打印内容*/
help_printf(format,p);
/*将指针置null*/
va_end(p);
return 0;
}
8.5 Makefile
all:
arm-linux-gcc -c -o bsp_uart.o bsp_uart.c
arm-linux-gcc -c -o bsp_io.o bsp_io.c
arm-linux-gcc -c -o main.o main.c
arm-linux-gcc -c -o lib1funcs.o lib1funcs.S
arm-linux-gcc -c -o start.o start.S
arm-linux-ld -Ttext 0 -Tdata 0xb10 start.o lib1funcs.o bsp_uart.o bsp_io.o main.o -o demo.elf
arm-linux-objcopy -O binary -S demo.elf demo.bin
arm-linux-objdump -D demo.elf > demo.dis
clean:
rm *.bin *.o *.elf *.dis
8.6 main.c
#include "s3c2440_soc.h"
#include "bsp_io.h"
#include "bsp_uart.h"
int main(void){
unsigned char ch;
uart_init();
printf("%c%c%s\n\r",'a','b',"hello");
printf("%s: %d\n\r","total",123);
printf("%0x\n\r",0xfff);
printf("=====test======\r\n");
while(1){
ch = getchar();
if(ch == 027){ /*esc的ASCII码*/
break;
}
if(ch == '\r'){
putchar('\n');
}else if(ch == '\n'){
putchar('\r');
}
putchar(ch);
}
printf("test is end\r\n");
return 0;
}
8.7 缺少除法库
8.6.1 指定静态库
# 在arm-linuxgcc的安装目录下有一个libgcc.a的静态库。
# 在链接时需要链接该静态库。
arm-linux-ld -Ttext 0 -Tdata 0xe80 start.o bsp_uart.o bsp_io.o main.o -o demo.elf -lgcc -L/usr/local/arm/arm-2009q3/lib/gcc/arm-none-linux-gnueabi/4.4.1
# 在main函数里加个raise函数
int raise(int a)
{
return 0;
}
#include "bsp_io.h"
int main(void){
printf("%c%c%s\n",'a','b',"hello");
printf("%s: %d\n","total",123);
printf("%0x\n",0xfff);
return 0;
}
# 需要指定代码的长度,因为2440开发板只有4KRAM不够
8.6.2 直接替换arm-linux-gcc
# xshell远程连接ubuntu
sudo apt-get install lrzsz
# 使用zmordm传输文件
rz -E # 选择对应的文件
# 创建目录
sudo mkdir -p /usr/local/arm
# 解压安装包
sudo tar -jxvf arm-linux-gcc-3.4.5-glibc-2.3.6.tar.bz2 -C /usr/local/arm
# 安装路径设置到 ~/.bashrc
sudo vim ~/.bashrc #作用于该用户,每次开机都调用
# 输入环境变量
export PATH=$PATH:/usr/local/arm/4.3.5/bin
# 激活环境变量
source ~/.bashrc
# 查看安装成功与否
arm-linux-gcc -v
# 将lib1funcs.S添加到工程
9 注意事项
9.1 末尾空格
- linux的c和S文件都需要在行尾加空格,类似于KEIL MDK。
9.2 内存不够
# 2440的SRAM仅仅4K,存不下,需要查看dis反汇编文件
# 找到_data_start,数据段起始位置,一般将其拼接到上面的代码段后面,如 末尾为b08替换为b10,大一点点就行
/section # 按下n向下找找
# 在链接时设置该数据段起始位置
arm-linux-ld -Ttext 0 -Tdata 0xb10 start.o lib1funcs.o bsp_uart.o bsp_io.o main.o -o demo.elf