分类 Code 下的文章

今天使用 systick 的时候,只使用了头文件 core_cm4.h,结果就报错了,原因是 __FPU_PRESENT 没有定义,这个定义其实在 stm32f4xx.h 里面。所以如果要解决这个错误,有两种方法:

  1. 只使用 core_cm4.h,然后碰到所有未定义的,都手动定义,或者添加相应的头文件。
  2. 不使用 core_cm4.h,使用 stm32f4xx.h。

比较起来,1 需要包含的头文件比较少,但是操作比较繁琐,2 需要的操作比较少,相应包含的头文件就比较多了。

使用 patch 命令打补丁, -p1 标识忽略第一级目录。 在串口上使用 q 命令,推出菜单界面,然后 print 可以打印出环境变量。想要返回菜单,只要输入 menu 即可。输入 ? 打印出可选命令。输入多条命令,可以使用分号隔开。输入 help 是各个命令的 usage 短帮助信息,输入 help print 这样指定命令,就会出现 help 长帮助信息。使用 set 命令修改环境变量,使用 save 保存环境变量,使用 reset 复位。boot 命令可以启动内核。 make 最后打印出来的信息是可以看到 ld 的具体命令,里面包含了链接地址,这个就是 sdram 中的位置。 start.s 汇编文件里面包含了启动汇编。 增加 uboot 命令,只需要在源文件中增加 类似于 do_bootm 这样的函数 和 U_BOOT_CMD 这样的宏,然后修改相应文件夹下的 makefile,增加对应的 obj 即可。然后 make,实在不行就 make disclean, make xxx_config, make。 linux 分区: boot, env, kernel, root ,具体分区在源码里面写死。MTDPARTS_DEFAULT 这个地方定义的,包含在单板头文件里面。使用命令 mtd 可以看到当前分区情况。do_bootm 是用来启动内核的, jffs2 这种格式不要求对齐,其他格式可能需要各种对齐。

flash 上存在的内核是 uImage,这个是由头部和真正的内核两部分组成。in_load 加载地址, in_ep 入口地址。加载地址是 u-boot 需要把真正的内核移动到那个地址。普通 bootm 的时候,可能把 uImage 放到 0x30007FC0,然后前面64字节是头部,真正的内核在 0x30008000 这个位置,如果 in_load 也是这个地址,那么 u-boot 就可以省略移动内核这个步骤,否则还需要把内核移动到 in_load 的地址。真正的启动命令 do_bootm_linux, 里面的 theKernel 就等于 in_ep 入口地址,然后从这个地址开始执行内核。

u-boot 需要把一些参数给 内核,这些参数放在一些约定好的地址,然后从这些地址按照指定的格式读取,可能是 0x30000100 地址。通过 setup_start_tag(bd) 这样的方式设定,最后一个是 setup_end_tag(bd)。bd->bi_arch_number 机器地址。

  1. ram 初始化: 在 start.S 中, bl cpu_init_crit 这句,在 tq2440 中是直接调用,在韦东山里面是通过和 TEXT_BASE 进行比较,如果从 RAM 中运行就不进行 初始化。

  2. uboot 第一阶段初始化主要在 start.S 里面,主要是芯片本身初始化。第二阶段初始化是从 lib_arm/board.c 里面的 start_armboot 开始的,里面有一个 for 循环,通过函数指针调用函数,存储这些指针的结构体 gd 在 SDRAM 中的 128 字节的 CPU_GBL_DATA_SIZE 里面,相应的函数实现在 board/EmbedSky/EmbedSky.c 里面。

  3. 在 start_armboot 函数中,韦东山代码增加了一个判断,从调试器加载时,相应的处理。if (PreLoadedONRAM)

  4. command.c command.h 中有 u_boot_cmd 这个段相关的变量,最后通过 链接文件,链接到一起。然后函数执行的时候,遍历段中所有的结构体的命令,相等的就执行相应的函数。

  5. 增加 uboot 命令,可以仿照 dobootm 命令来制作自己的命令,需要提供 函数 do 和 通过宏定义来实现的结构体变量 U_BOOT_CMD。还有别忘了相应的头文件。

  6. uboot 的 清除编译过程文件是 make distclean,配置命令 make EmbedSky_config,编译是 make。

  7. uboot 读出内核,通过 nand read.jffs2 0x30007FC0 kernel 来从 nand 中的 kernel 分区里面读出内核。nand 中的 kernel 分区是在 include/configs/EmbedSky.h 中定义的,通过宏定义 MTDPARTS_DEFAULT 来定义具体的各个分区。也可以通过 uboot 的命令行模式中输入 mtd 命令来查看当前 uboot 的 分区信息。 分区的名字只是个标识不重要,主要的是分区的开始偏移地址和分区大小。

  8. 命令 nand read.jffs2 不需要页对齐,可以长度是任意的,其他的 read 命令需要对齐。

  9. uboot 的内核镜像是 uImage,由头部和真正的内核组成,头部中的 ih_load 是加载地址,ih_ep 是入口地址,uboot 通过读取头部数据,把内核放到加载地址,然后跳转到入口地址执行。

  10. 编译时链接地址 0x30007FC0,这个地址加上 64 字节的 uImage 头部内容,结果就是 0x30008000,这个就是真正的内核加载地址。这样做的好处是,因为地址能够匹配上,那么 cmd_bootm.c 中 ntohl(hdr->ih_load) == addr 成立,就不会移动内核,否则通过 memmove 函数在调用把内核移动到加载地址处,这样能够加快启动速度。

  11. do_bootm_linux 来启动内核。

  12. uboot 通过设置启动参数的方法告诉内核一些必要的参数,然后跳转到入口地址,真正的开始启动内核。

  13. ih_ep 中包含函数指针,把这个函数指针交给 theKernel,然后调用 theKernel 来启动内核。

  14. uboot 在 0x30001000 按照一定的格式存放内核参数,然后由内核来读取这些参数。 在 cmd_bootm.c 中使用 setup_start_tag(bd),setup_memory_tags(bd), setup_end_tag(bd) 这种格式来存放 tag 参数。

  15. uboot 中 armlinux.c 里面 memory_tags 里面关于 sdram 开始地址和大小,是在 board 文件中的 dram_init()中 通过 gd->bd->bi_dram[0].start, .size 来赋值的。

  16. commandline_tas() 中的 参数 commandline 来源于 getenv 中的 “bootargs",这个可以在 uboot 运行起来会直接 print 查看,也可以查看源码。root= 是根文件系统位置,init= 这个是第一个应用程序,console= 这个是信息打印的窗口。

  17. theKernel(0, bd->bi_arch_number, bd->bi_boot_parms), 第三个参数是从 0x30001000 开始的 start_tag 开始的一连串的 parms。bi_arch_number 这个是机器ID,在 board 文件里面通过 gd->bd->bi_arch_number 来定义,arch_number 不同的电路板是不一样的数值。uboot 把 arch_number 交给内核,内核检查是否支持这个 arch_number 的PCBA。如果支持,系统控制权就交给内核,后面就没有 uboot 的事情了。

  18. 在 uboot 命令行下,输入 boot,就可以通过 bootcmd 和 bootarg 两个参数来启动内核。

使用 stm32f4 调试uart 接收, 使用 空闲中断,dma 双缓冲模式,有以下几点需要注意的。

  1. 调试的时候断点不要打在 if (USART_GetITStatus(USART6, USART_IT_IDLE) != RESET) 这种语句上面,要打在 if 的代码块里面。mdk 调试的时候,会出现一些 bug,当在 if (USART_GetITStatus(USART6, USART_IT_IDLE) != RESET) 断点的时候,能够看到 idle 信号出现,但是只要向下执行,不管是单步还是怎样,下一步的时候 idle 标志位就会消失。导致进不了空闲中断处理语句。但是如果断点打在代码块里面,就能够正常进入了。

  2. 使用双缓冲的时候, DMA_DoubleBufferModeConfig, DMA_DoubleBufferModeCmd 需要放在 DMA_Cmd 前面,还有别忘了 开启 circular 模式。

  3. 接收的时候,已经接收的字节数,等于 buffersize - ndtr 的结果。

  4. 开启双缓冲的时候,可以不用 disable dma,只需要在 空闲中断里面,识别出 ndtr 和 ct,然后经过计算得出本次传送的数据个数,并把本次开始的地址和数据个数记录下来,就可以由非中断部分程序来读取这一帧接收的数据。

位置无关码

bl 是位置无关码,指令中带的数值是,编译的时候,编译器计算好的,需要跳转的位置减去 bl 指令所在位置的结果。这样当程序最开始在 4k sram 中运行的时候,跳转的位置是在 0 + offset 的位置,当后期sdram 初始化好了之后,程序移动到 sdram 中运行的时候,跳转的位置是 0x30000000 + offset 的位置。

ldr 是位置相关码,指令中的位置是编译好的绝对位置,如果 sdram 中还没有初始化好,跳转的位置是 0x30000000地址以上的值,那就出错了。

如果是 C 语言,想要某些位置无关,那么就不能用全局变量和静态变量。

长距离跳转

b 和 bl 只能前后跳转 32M的范围,想要长距离跳转需要用 ldr pc, =main 这种方式来做。一般从sram 跳转到 sdram 中执行的时候,可以用 ldr 的指令来跳转。 bl 默认lr 是下一条指令,可以直接作为子函数的调用。ldr 只是单纯的跳转,用于调用子函数的时候,必须前面先用一条 ldr lr, =int_return 之类的语句作为返回用的 lr 位置。

链接脚本地址

编译器根据链接脚本中的地址来确定程序中各个部分的地址。链接脚本允许设定一部分放到 0地址开始的地方,一部分放到 0x30000000开始的地方,也可以都放到 0地址开始的地方,或者都放到 0x30000000 开始的地方。这里有好几个需要注意的地方:

  1. 从0地址开始的地方不能超过 4k sram 的大小。
  2. 正常使用 0x30000000 部分内容之前,必须初始化 sdram,才能正常使用。不然一些函数的跳转,全局变量等等都会引发出错。
  3. 如果是使用 0x30000000 部分的代码,并且还没来得及初始化 sdram,就需要跳转或者子函数,那么一定要使用位置无关码进行跳转,否则也会出错。

位置无关配置寄存器

本来可以把配置放到数组里面,然后用 for 循环给寄存器复制。但是如果想用位置无关代码的话,那么最好一个一个直接对寄存器赋值数值。这两种汇编出来后反编译是不一样的。直接赋值,汇编里面是一句句不太一样的数值进行加加减减,最后出来想要的数据并赋值。而使用数组,想要的数值是写死在函数中一小块位置上,数值比较清晰。

按照韦东山的视频中 sdram的裸机代码,写了一份,通过 minitools 下载到 0x30000000,然后烧录到 nand中,接过不能正常运行。 尝试过多种方法后,只有一种解决方法,就是不要用 0x30000000这个地址。 可以把 makefile 中的地址改为0x30008000,minitools 中下载的地址也改为 0x30008000,这样就可以正常的运行了。 可能 0x30000000 前面的一些内存被使用了?

后来换了一个 TQ2440的开发板,不使用 minitools,而是用 DNW配合bootload下载程序到nand,程序链接地址选择 0x30000000 是可以运行的。使用 DNW 驱动使用 韦东山的 zadig程序,安装 libusb-win32 驱动,然后使用 dnw_100ask.exe 来下载程序。

所以初步怀疑,这个现象可能和 minitools 软件相关。

汇编指令中 bne label 这条指令有以下两种特别的写法:bne 1b, bne 1f.

bne 1b 指的是 backward,倒退寻找标号为 1 的地方并跳转。

同样也有 bne 1f,值得是 forward,向前寻找标号为1的地方并跳转。

最近使用 mdk526,编辑设置使用 utf-8,编辑窗口中文正常,但是编译的时候提示 warning: #870-D: invalid multibyte character sequence,解决的方法很简单,在编辑选项 misc 里面填写 --no_multibyte_chars 或者 locale=english,都可以顺利编译。

但是编译完成之后还有一个烦心的事情,市面上很多串口工具的中文都是 gbk 编码的,不能识别 utf-8 的串口中文,需要专门找支持的串口工具。

找来找去,找到一个 teraterm-4.97.exe,这个是支持中文的,只需要如下修改即可。

Setup->Terminal
locale: chinese
CodePage: 936

今天在 写了个简单的 led 的汇编程序,下载到 mini2440 的 nand flash 里面可以正常运行,但是下载到 sdram 里面不能运行。

后来发现有几个注意点,

  1. 要在 sdram 中运行,链接脚本的地址不能像 nand 里面一样是0,必须是 sdram 里面的地址,比如说 0x30000000.
  2. 下载到 ram 中的地址必须和 链接脚本的地址一致。
  3. 0x30000000 这个地址不行,0x30008000 这个地址肯定没有问题,0x30000020 超过这个地址,也都可以运行,0x3000001c 这个地址不能运行,但是 minitools 会断掉链接后自动链接上,其他小于 0x30000020 的地址,都是没有运行反应的。

具体是什么原因,还要后续进一步的调试了。

但是也不是每个程序都能在 sdram 中运行,具体情况暂时还不清楚,尽量还是烧录到 nand 中在运行,不容易出问题。

stm32h7xx_hal_conf.h 中需要注意的几个地方:

  1. HSE_VALUE 这个外接晶振的频率

  2. TICK_INT_PRIORITY 这个 tick 的中断优先级,因为 HAL_DELAY 这个函数是基于这个 tick 的中断的,所以如果有其他高优先级的中断中调用了 HAL_DELAY 函数,会造成 tick 的中断一直进不来。这样的话 HAL_DELAY 也会一直卡住,这个高优先级的中断也会一直卡在这里。

  3. 断言的参考实现函数。

    /* 
    ********************************************************************************************************* 
    *  函 数 名: assert_failed 
    *  形    参:file : 源代码文件名称。关键字__FILE__表示源代码文件名。 
    *        line :代码行号。关键字 __LINE__ 表示源代码行号 
    *  返 回 值: 无 
    ********************************************************************************************************* 
    */ 
    void assert_failed(uint8_t* file, uint32_t line) 
    {  
    /*  
    用户可以添加自己的代码报告源代码文件名和代码行号,比如将错误文件和行号打印到串口 
    printf("Wrong parameters value: file %s on line %d\r\n", file, line) */ 
    */ 
    
    /* 这是一个死循环,断言失败时程序会在此处死机,以便于用户查错 */ 
    while (1) 
    { 
    } 
    } 
  4. HAL 库不像之前的标准库,在系统启动函数 SystemInit 里面做了 RCC 初始化,HAL 库是没有做的,所以进入到 main 函数后,系统还在用内部高速时钟 HSI,对于 H7 来说,HSI 主频是 64MHz。

  5. HAL_MspInit 和 HAL_MspDeInit 在 stm32h7xx_hal_msp.c 里面做具体实现。

  6. PA0_C 这类的引脚只有在 TFBGA240+25 ballout 这种封装上面才有。

  7. LSE Bypass 和 LSE On 之间的切换,必须要先关闭 LSE 才行。同样 HSE Bypass 和 HSE On 之间的切换,也必须先关闭 HSE 才行。

  8. HAL_RCC_OscConfig 会更新全局变量 SystemCoreClock 的主频值,并且会再次调用函数 HAL_InitTick 更新系统滴答时钟。

使用 segger 的 hard fault 的源文件后,当调试时,发生硬件错误的时候,可以查看 HardFaultRegs 中的内容,并对比 segger_HardFaultHandler.c 中的定义,就能得出具体是哪种错误了。