C language LED experiment
1. Assembly activation CPU
First of all, we should understand that there is no way to directly identify C without a system development board (that is, bare metal). Therefore, an assembly language is needed to configure CPU resources, select CPU operation mode and initialize pointer position.
The code is as follows:
.global _start /* Global label */ _start: /*Enter SVC mode */ mrs r0, cpsr bic r0, r0, #0x1f / * clear the lower 5 bits of r0, that is, M0~M4 of cpsr*/ orr r0, r0, #0x13/* r0 or 0x13 above indicates that SVC mode is used*/ msr cpsr, r0/* Write the data of r0 to cpsr_c medium */ ldr sp, =0X80200000/*Set stack pointer */ /*Because I.MX6U-ALPHA is on The DDR3 address range on the board is 0x80000000 ~ 0x100000000 (512MB) or 0X80000000~0X90000000(256MB),Whether it is a 512MB version or a 256MB version, the DDR3 start point The addresses are 0X80000000. Since the stack of Cortex-A7 grows downward, set the SP pointer to 0X80200000, Therefore, the stack size of SVC mode is 0x80200000-0x8000000 = 0x200000 = 2MB, and the stack space of 2Mb is already large, If you do bare metal development, it's more than enough. */ b main/*Jump to main function */
Here are two new assembly instructions:
BIC -- bit clear instruction
Instruction format:
BIC{cond}{S} Rd,Rn,operand2
The BIC instruction stores the value of Rn and the inverse code of operand operand2 in the destination register Rd by bitwise logic "and". Instruction example: BIC R0,R0,#0x0F; Clear the lowest 4 bits of R0, and the other bits remain unchanged.
ORR - position 1
The format of ORR instruction is:
ORR {condition} {S} destination register, operand 1, operand 2
The ORR instruction is used to perform a logical or operation on two operands and place the result in the destination register. Operand 1
It should be a register, and operand 2 can be a register, a shifted register, or an immediate number. This instruction is often used to set some bits of operand 1.
Examples of instructions:
ORR R0,R0,#3 ; The instruction sets bits 0 and 1 of R0, and the other bits remain unchanged.
2. Header file configuration related registers
That is to save the address of the relevant register with a variable name
The code is as follows:
#ifndef _MAIN_H #define _MAIN_H /*CCM Related register to configuration - that is, clock to address */ #define CCM_CCGR0 *((volatile unsigned int *)0X020C4068) #define CCM_CCGR1 *((volatile unsigned int *)0X020C406C) #define CCM_CCGR2 *((volatile unsigned int *)0X020C4070) #define CCM_CCGR3 *((volatile unsigned int *)0X020C4074) #define CCM_CCGR4 *((volatile unsigned int *)0X020C4078) #define CCM_CCGR5 *((volatile unsigned int *)0X020C407C) #define CCM_CCGR6 *((volatile unsigned int *)0X020C4080) /*IOMUX Related to register address*/ #define SW_MUX_GPIO1_IO03 *((volatile unsigned int *)0X020E0068) #define SW_PAD_GPIO1_IO03 *((volatile unsigned int *)0X020E02F4) /*GPIO1 Related register to address*/ #define GPIO1_DR *((volatile unsigned int *)0X0209C000) #define GPIO1_GDIR *((volatile unsigned int *)0X0209C004) #define GPIO1_PSR *((volatile unsigned int *)0X0209C008) #define GPIO1_ICR1 *((volatile unsigned int *)0X0209C00C) #define GPIO1_ICR2 *((volatile unsigned int *)0X0209C010) #define GPIO1_IMR *((volatile unsigned int *)0X0209C014) #define GPIO1_ISR *((volatile unsigned int *)0X0209C018) #define GPIO1_EDGE_SEL *((volatile unsigned int *)0X0209C01C) #endif
Here you can review the role of the keyword volatile:
volatile:
It means "changeable", which can avoid being optimized by the compiler during compilation, resulting in unexpected results.
Generally, it also has the following functions:
- Indicates that the variable can be modified by the daemon
- Statements that prevent the compiler from optimizing operational variables
- Prevent access objects to compiler optimized variables
- When accessing a variable modified by volatile, force access to the value in memory rather than in the cache.
3. C document instruction
At this time, the environment has been set up and you can start writing your own applications
The code is as follows:
#include "main.h" /*Clock enable function*/ void clk_enable(void) { CCM_CCGR0 = 0xffffffff; CCM_CCGR1 = 0xffffffff; CCM_CCGR2 = 0xffffffff; CCM_CCGR3 = 0xffffffff; CCM_CCGR4 = 0xffffffff; CCM_CCGR5 = 0xffffffff; CCM_CCGR6 = 0xffffffff; } /*Initialization led corresponds to GPIO*/ void led_init(void) { /*IO Multiplexed to GPIO1_IO03*/ SW_MUX_GPIO1_IO03 = 0x5; /*Configure gpio1_ Relevant properties of io03*/ /* *bit 16:0 HYS close *bit [15:14]: 00 Default drop-down *bit [13]: 0 kepper function *bit [12]: 1 pull/keeper Enable *bit [11]: 0 Turn off the open circuit output *bit [7:6]: 10 Speed 100Mhz *bit [5:3]: 110 R0/6 Driving capability *bit [0]: 0 Low conversion rate */ SW_PAD_GPIO1_IO03 = 0X10B0;//After inputting the corresponding number according to the configuration requirements, the above register becomes 0X10B0 GPIO1_GDIR = 0X0000008;//Initialize GPIO, GPIO1_IO03 set to output GPIO1_DR = 0X0;//Set GPIO1_IO03 outputs low level and turns on LED0 } /*Light on function*/ void led_on(void) { GPIO1_DR &= ~(1<<3);//Gpio1_ bit3 clearing of Dr } /*Light off function*/ void led_off(void) { GPIO1_DR |= (1<<3);// Gpio1_ bit3 of DR is set to 1 } /*Short time delay function*/ void short_delay(volatile unsigned int n) { while(n--){} } /*Delay function 1ms*/ void delay(volatile unsigned int n) { while(n--) { short_delay(0x7ff); } } /*Main function*/ int main() { clk_enable(); led_init(); while (1) { led_off(); delay(500); led_on(); delay(500); } return 0; }
There is only one point here: the operator used in the light on and light off function.
GPIO1_DR &= ~(1<<3);//Gpio1_ The bit3 of DR is cleared to turn on the light
- First shift 1 left by 3 bits: 0001 - > 1000
- Reverse by bit: 1000 - > 0111
- Last bitwise sum operation
It's equivalent to clearing the third digit.
GPIO1_DR |= (1<<3);// Gpio1_ Set bit3 of Dr to 1 and turn off the light
This is even simpler. First shift and operate by bit or.
4. makefile file
Look at the code first:
objs := start.o main.o ledc.bin:$(objs) arm-linux-gnueabihf-ld -Timx6ull.lds -o ledc.elf $^ arm-linux-gnueabihf-objcopy -O binary -S ledc.elf $@ arm-linux-gnueabihf-objdump -D -m arm ledc.elf > ledc.dis %.o:%.s arm-linux-gnueabihf-gcc -Wall -nostdlib -c -o $@ $< %.o:%.S arm-linux-gnueabihf-gcc -Wall -nostdlib -c -o $@ $< %.o:%.c arm-linux-gnueabihf-gcc -Wall -nostdlib -c -o $@ $< clean: rm -rf *.o ledc.bin ledc.elf ledc.dis
There are several variable symbols that need to be explained:
"$^" means a collection of all dependent files.
"$<" means to rely on the first file of the target collection.
"$@" means the target set.
Another point is that when connecting, you must put start.o in front.
objs := start.o main.o
Have you noticed this connection command
arm-linux-gnueabihf-ld -Timx6ull.lds -o ledc.elf $^
What is this imx6ull.lds?
This is actually a connected script.
5. Connection script
Do you remember such an order?
arm-linux-gnueabihf-ld -Ttext 0X87800000 -o ledc.elf $^
In the above statement, we use "- Ttext" to specify that the link address is 0X87800000, so that all files
Will be linked to the area with 0X87800000 as the starting address.
However, the reality is that we will have many files to be placed in different addresses (or segments), and the addresses of these segments are freely specified by us.
There are also text (program segment), data (data segment), bss (uninitialized variable) and so on. (if you want to be more intuitive, you can look at the. map files of many projects. I believe you will have a new feeling.)
It may be a little abstract. Here is a simple example:
SECTIONS{ . = 0X10000000; .text : {*(.text)} . = 0X30000000; .data ALIGN(4) : { *(.data) } .bss ALIGN(4) : { *(.bss) } }
There are the following key points:
1. The keyword "SECTIONS" is essential.
2. The special symbol "." is assigned a value. The "." is called the location counter in the link script. The default location counter is 0. The code is required to link to the place with 0X10000000 as the starting address, and the subsequent files or segments will start the link with 0X10000000 as the starting address.
3. ". text" is the segment name, and the colon behind it is the syntax requirement. The braces behind the colon can be filled with all the files to be linked to ". Text". The "" in "(. text)" is a wildcard, indicating that the. Text segments of all input files are placed in ". Text".
4. Reset the positioning counter "." to 0x3000000.
5. To do byte alignment for the starting address of ". data", ALIGN(4) means 4-byte alignment. In other words, the starting address of the segment ". data" should be divisible by 4. Generally, ALIGN(4) or ALIGN(8) is common, that is, 4-byte or 8-byte alignment.
After mastering these, let's look at the connection script file we need:
SECTIONS{ . = 0X87800000; .text : { start.o main.o *(.text) } .rodata ALIGN(4) : {*(.rodata*)} .data ALIGN(4) : { *(.data) } __bss_start = .; .bss ALIGN(4) : { *(.bss) *(COMMON) } __bss_end = .; }
There are also several points to note in this script:
That's it__ bss_start and__ bss_end, which is equivalent to recording the beginning and end addresses of BSS. Since we need to clear the bssZ segment later, we need to record its beginning and end addresses.
END!!!
Finally, don't forget to use imxdownload to burn into the SD card and the file format of makefile. You must pay attention!! Well, that's all.