树莓派4B裸机程序串口uart实现

背景

近期在学习树莓派简单的裸机程序的实现,首先用到的是移植自己的uboot,并在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)

步骤

1. 连接串口工具到电脑

我本人用的是CH340 USB转TTL模块连接。

  • 当连接windows主机时
    需要安装对应的串口驱动,USB端插入电脑后,在设备管理器中可以看到对应的串口设备如下图所示,
    Windows宿主机识别串口设备
    (图片来自github,侵删)
    真实情况可能会有出入,(当前没有实验环境需要后续更新实际图片)。
    打开MobaXterm的串口选项,选择对应的串口设备,即COM号,设置波特率即可在宿主机端代开串口设备。
  • 当连接Linux主机时
    不需要安装任何驱动,当串口设备插入时,在/dev/目录下会产生一个ttyUSBx节点(x的值以实际情况为准)。
    在Linux主机内安装minicom串口工具,使用minicom -s 命令设置串口的波特率,然后运行 sudo minicom -D /dev/ttyUSBx 即可打开对应的串口设备
  • 当连接Mac时
    无实验环境。。。(多么伤心的理由)

2. 连接树莓派

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。

3. 串口配置

关于代码中链接文件,初始化程序等本文就不做详细说明,可以参考代码仓库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() 为接收字符函数,接下来分别讲解。

  • uart_init()
    初始化串口,主要完成串口引脚配置及串口控制器寄存器的配置,内容如下。
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寄存器,文档中关于该寄存器的说明如下。
pin脚功能选择说明
如上图,我们仅需配置第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
  • uart_send_char(char c)
    源码如下:
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’ 字符,与串口的转义字符有关,没有深入研究。
需要等待串口的帧状态寄存器,循环读取读取是否可写,如可写,就向数据寄存器写入字符,反之,则循环等待。(尚未用到中断控制)

  • uart_send(char *s)
void uart_send(char* str)
{
    
    
    while(*str!='\0'){
    
    
        uart_send_char(*str);
        str++;
    }
}

循环调用uart_send_char(char c)函数写入字符。

  • uart_recv()
char uart_recv(void)
{
    
    
    while (1){
    
    
        if(read_reg32(UART0_FR)&0x40)
            break;
    }
    return(read_reg32(UART0_DR)&0xff);
}

与写入相反,读取则是线读取帧状态寄存器数据是否就绪,如就绪则读取

扫描二维码关注公众号,回复: 15262219 查看本文章

验证

连接好串口Uboot启动之后按任意键进命令行(关于Uboot配置并从串口控制可以参考【树莓派学习笔记】树莓派4B上运行uboot并从网络启动linux内核(上)【树莓派学习笔记】树莓派4B上运行uboot并从网络启动linux内核(下) 文档),分别敲入tftp 0x80000 kernel8.imggo 0x80000(其实也可以设置bootcmd= tftp 0x80000 kernel8.img; go 0x80000这样uboot每次启动会自动加载裸机程序并执行)执行裸机程序,就可以在串口中看到输出。

本篇教程中的代码可以在以下地址获取:

一定注意源码需要先修改交叉编译器路径

树莓派4B裸机串口程序源码(结合uboot使用)

欢迎各位批评指正

猜你喜欢

转载自blog.csdn.net/weixin_43328157/article/details/130218995