赞
踩
第一个程序,当然是“Hello World! ”,在上一篇教程中,讲解了如何编译裸机程序。本文将介绍怎么实现。
首先需要一种方式输出我们的程序Hello world,这里我们首先选取了难度相对不高的串口。实现方法就是通过前两节讲的方法,讲本节写的程序编译好并通过uboot上的tftp传到树莓派执行。
树莓派共有两种类型5个(应该是5个)串口控制器。一种类型是miniuart,另外一种是普通的PL011串口控制器,除uart1是miniuart以外都是PL011普通的串口。网上找到的资料大都通过树莓派的miniuart来实现程序的控制台功能,参考 https://github.com/isometimes/rpi4-osdev 中的part3.
(后续是我个人经验,仅作参考)
但是我自己在实现过程中遇到了输出乱码的问题(用的MobaXterm程序作为宿主机端的串口接收和发送工具),原因应该是波特率设置的问题(本来配置的波特率是115200,宿主机修改波特率到57600后输出不再乱码但输出字符与实际不能相互对应),尝试修改寄存器配置和修改config.txt但是没能成功。
通过查阅网上的相关介绍,简单理解miniuart没有自己的时钟,用的是CPU的时钟,也有一些其它功能上的阉割,时钟频率和波特率又是强相关的关系,因此我认为出错的原因应该就是因为这个。所以我弃用了github上miniuart作为控制台,改为配置为普通的pl011串口控制器,关于PL011串口控制器,可以参阅:ARM PrimeCell UART(PL011)
我本人用的是CH340 USB转TTL模块连接。
minicom -s
命令设置串口的波特率,然后运行 sudo minicom -D /dev/ttyUSBx
即可打开对应的串口设备CH340 可以有4个引脚引出,小板背面有标记
GND: 接地,一般接黑色杜邦线
Rx:从树莓派接收数据,一般是白色杜邦线,可比作嘴巴
Tx:向树莓派写入数据,一般是绿色杜邦线,可比作耳朵
Vcc:供电,一般是红色杜邦线
另外可以选择Vcc电压的值,一般3v3和5v二选一,通过跳线帽选择,暂时不清楚有什么影响,欢迎读者评论补充,我选的3v3。杜邦线颜色没有任何影响,只是符合一般使用习惯而已。
需要特别注意的是在应用中CH340的Rx需要连接板子的Tx,CH340的Tx连接板子的Rx,类似于嘴巴和耳朵的关系,嘴巴需要对着耳朵说话,如果接反就不能正常通信,就会发现你的嘴巴对着对面的嘴巴说话,反过来你耳朵在听对面耳朵发出的声音,这是一件很容易理解的逻辑。另外需要连接GND实现CH340串口小板子和树莓派的共地,避免因为电平的差异造成接收或发送的数据乱码。Vcc一般可以不连接。
树莓派板子上我们选用了14和15作为我们配置的串口引脚如下图所示。
依照上面介绍,连线后示意图如下
其实连接方法与github上相同,树莓派14和15引脚可以复用为UART0和UART1,UART0是普通的PL011串口控制器,UART1是miniUART。
关于代码中链接文件,初始化程序等本文就不做详细说明,可以参考代码仓库02_helloworld,重点讲一下如何对串口的配置。关于串口的配置可以参考官方文档 bcm2711 peripherals 。
首先,完整的串口裸机程序大概包含以下步骤:
uart_init();
uart_send("Hello world!\n");
while(1)
{
uart_send_char(uart_recv());
}
其中uart_init为uart初始化函数,uart_send(char *s)函数为发送字符串的函数,uart_send_char(char c)函数为发送字符函数。uart_recv() 为接收字符函数,接下来分别讲解。
void uart_init(void) { unsigned int selector; selector = read_reg32(GPFSEL1); // get gpio control register value selector &= ~(7<<12); // clean gpio14 selector |= 4<<12; // set alt0 for gpio14 selector &= ~(7<<15); // clean gpio15 selector |= 4<<15; // set alt0 for gpio15 write_reg32(GPFSEL1, selector); write_reg32(GPPUD,0); delay(150); write_reg32(GPPUDCLK0,(1<<14)|(1<<15)); delay(150); write_reg32(GPPUDCLK0,0); write_reg32(UART0_CR, 0); //disable uart0 write_reg32(UART0_LCRH, 0x60); // set to character mode, 8 bits word len and disable Parity write_reg32(UART0_IBRD, 0x1a); write_reg32(UART0_FBRD, 0x3); // set burdrate to 115200 write_reg32(UART0_DMACR, 0x00); //disable dma write_reg32(UART0_ITCR, 0x0); write_reg32(UART0_ITOP, 0x0); write_reg32(UART0_TDR, 0x0); write_reg32(UART0_CR, 0x301); //enable uart0 }
说明:write_reg32为写寄存器函数,相应的read_reg32为读寄存器函数。delay()函数并不是以ms为单位,而是执行n次的空指令。其中用到的寄存器地址的宏定义在源码头文件中有定义。与官方文档中介绍的地址不同,文档中PERIPHERAL_BASE的地址位0x7e000000, 而我们用的是0xfe000000. 主要原因我们使用了low_memery模式.
首先是配置两个pin脚,树莓派上40个pin脚有默认的6个功能,分别为Alt0,Alt1…Alt5。如下图,需要完成特定功能只需要将特定引脚配置成特定功能即可。
如上图所示,我们需要将14和15引脚选择ALT0(miniuart选择ALT5)。配置方法可以根据官方文档的说明操作,配置GPFSEL1寄存器,文档中关于该寄存器的说明如下。
如上图,我们仅需配置第17-12位即可,对应的代码如下。
selector = read_reg32(GPFSEL1); // get gpio control register value
selector &= ~(7<<12); // clean gpio14
selector |= 4<<12; // set alt0 for gpio14
selector &= ~(7<<15); // clean gpio15
selector |= 4<<15; // set alt0 for gpio15
write_reg32(GPFSEL1, selector);
配置完function功能后,我们需要配置gpio无初始状态,因为树莓派默认可以拉高或者拉低,但是我们的引脚一开始就可以用起来,因此不需要配置初始状态,需要配置GPPUDCLK0和GPPUD寄存器。需要指出的是,这两个寄存器每配置一次就要运行150次的空指令(文档要求)。代码如下:
write_reg32(GPPUD,0);
delay(150);
write_reg32(GPPUDCLK0,(1<<14)|(1<<15));
delay(150);
write_reg32(GPPUDCLK0,0);
最后配置UART0的控制器,首先关闭串口控制器,然后配置成字符模式,不适用fifo模式。然后设置波特率如代码所示。关闭DMA,最后打开uart0。代码如下,关于波特率的计算公式及寄存器的配置方法,文档中有详细介绍,不再赘述。
write_reg32(UART0_CR, 0); //disable uart0
write_reg32(UART0_LCRH, 0x60); // set to character mode, 8 bits word len and disable Parity
write_reg32(UART0_IBRD, 0x1a);
write_reg32(UART0_FBRD, 0x3); // set burdrate to 115200
write_reg32(UART0_DMACR, 0x00); //disable dma
write_reg32(UART0_ITCR, 0x0);
write_reg32(UART0_ITOP, 0x0);
write_reg32(UART0_TDR, 0x0);
write_reg32(UART0_CR, 0x301); //enable uart0
void uart_send_char(char c)
{
if(c == '\n')
uart_send_char('\r');
while(1){
if(read_reg32(UART0_FR)&0x80 || !(read_reg32(UART0_FR)&0x20))
break;
}
write_reg32(UART0_DR, c);
}
发送换行字符’\n‘之前需要发送’\r’ 字符,与串口的转义字符有关,没有深入研究。
需要等待串口的帧状态寄存器,循环读取读取是否可写,如可写,就向数据寄存器写入字符,反之,则循环等待。(尚未用到中断控制)
void uart_send(char* str)
{
while(*str!='\0'){
uart_send_char(*str);
str++;
}
}
循环调用uart_send_char(char c)函数写入字符。
char uart_recv(void)
{
while (1){
if(read_reg32(UART0_FR)&0x40)
break;
}
return(read_reg32(UART0_DR)&0xff);
}
与写入相反,读取则是线读取帧状态寄存器数据是否就绪,如就绪则读取
连接好串口Uboot启动之后按任意键进命令行(关于Uboot配置并从串口控制可以参考【树莓派学习笔记】树莓派4B上运行uboot并从网络启动linux内核(上)和【树莓派学习笔记】树莓派4B上运行uboot并从网络启动linux内核(下) 文档),分别敲入tftp 0x80000 kernel8.img
和go 0x80000
(其实也可以设置bootcmd= tftp 0x80000 kernel8.img; go 0x80000
这样uboot每次启动会自动加载裸机程序并执行)执行裸机程序,就可以在串口中看到输出。
一定注意源码需要先修改交叉编译器路径
欢迎各位批评指正
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。