A detailed analysis of the considerations of C language in embedded system programming

Although the C language provides many low-level processing functions, it still maintains good cross-platform features. The C language program written in a standard specification can be compiled on many computer platforms, even including some embedded processors ( MCU) and supercomputer and other operating platforms.

In the 1980s, in order to avoid differences in the C language grammar used by various developers, the National Bureau of Standards established a complete set of international standard grammar for the C language, called ANSI C, as the initial standard of the C language.

C language embedded system programming considerations

Different from the general form of software programming, embedded system programming is built on a specific hardware platform, which is bound to require its programming language to have strong hardware direct operation capability. Undoubtedly, assembly language has such qualities. However, due to the complexity of the assembly language development process, it is not a general choice for embedded system development. In contrast, the C language, a "high-level, low-level" language, is the best choice for embedded system development. In the development process of embedded system project, the author feels the exquisiteness of C language again and again, and indulges the convenience brought by C language to embedded development.

The hardware platform of most embedded systems. It consists of two parts:

(1) A general-purpose processor-centric protocol processing module for processing network control protocols;

(2) A digital signal processor (DSP)-centric signal processing module for modulation, demodulation, and digital/analog signal conversion.

The discussion in this article focuses on general-purpose processor-centric protocol processing modules because it involves more specific C programming skills. DSP programming focuses on specific digital signal processing algorithms, mainly related to the field of communication, and is not the focus of this article.

Focusing on the discussion of common embedded system C programming skills, the system's protocol processing module does not choose a special CPU, but chooses the well-known CPU chip --80186. Every reader who has studied "Microcomputer Principle" should The chip has a basic understanding and is familiar with its instruction set. The word length of the 80186 is 16 bits, and the memory space that can be addressed is 1MB, only the real address mode. The pointer generated by C language compilation is 32 bits (double word), the upper 16 bits are segment addresses, and the lower 16 bits are compiled in segments, and a segment is up to 64 KB.

The FLASH and RAM in the protocol processing module are almost all necessary equipment for each embedded system. The former is used to store programs, while the latter is used to store instructions and data storage locations. The FLASH and RAM selected by the system have a bit width of 16 bits, which is consistent with the CPU.

The real clock chip can be timed for the system, giving the current year, month, day and specific time (hours, minutes, seconds and milliseconds). It can be set to give an interrupt to the CPU or set the alarm time when the time comes. The CPU proposes an interrupt (similar to the alarm function).

NVRAM (Non-Volatile Detachable RAM) has the feature of power-down without losing data, and can be used to save system setup information, such as network protocol parameters. The previous setup information can still be read after the system is powered down or restarted. Its bit width is 8 bits, which is smaller than the CPU word length. The article deliberately chooses a memory chip that is inconsistent with the CPU word length, creating conditions for the discussion in the next section.

The UART completes the conversion of CPU parallel data transmission and RS-232 serial data transmission. It can send an interrupt to the CPU after receiving [1~MAX_BUFFER] bytes. MAX_BUFFER stores the maximum buffer of the received byte for the UART chip.

The keyboard controller and display controller complete the control of the system man-machine interface.

The above provides a more complete embedded system hardware architecture, the actual system may contain fewer peripherals. The reason why we choose a complete system is to discuss all aspects of embedded system C language programming skills in a more comprehensive way. All the equipment will become the analysis target of the following.

Embedded systems require good software development environment support. Because the target system resources of embedded systems are limited, it is impossible to build a large and complex development environment on them, so the development environment and target operating environment are separated from each other. Therefore, the development method of the embedded application software is generally to establish a development environment on the host (Host), perform application coding and cross-compilation, and then the host establishes a connection with the target (Target), and downloads the application to the target machine. Cross-commissioning, debugging and optimization, and finally the application is solidified to the actual operation of the target machine.

CAD-UL is an embedded application software development environment for x86 processors. It runs on top of the Windows operating system and can generate object code for x86 processors and pass the COM port (RS-232 serial port) or Ethernet of the PC. The port is downloaded to the target machine to run. The monitor program resident in the FLASH memory of the target machine can monitor the user debugging instructions on the host Windows debugging platform, and obtain the value of the CPU register, the storage space of the target machine, and the content of the I/O space.

The following chapters will explain the programming skills of the C language embedded system from the aspects of software architecture, memory operation, screen operation, keyboard operation, performance optimization and so on. Software architecture is a macro concept, and has little connection with specific hardware; memory operation mainly involves FLASH, RAM and NVRAM chips in the system; screen operation involves display controller and real clock; keyboard operation mainly involves keyboard controller; performance optimization Then give some specific techniques to reduce program time and space consumption.

There will be 25 passes in our cultivation journey. These gates are divided into two categories, one is skill type and has strong applicability; the other is common sense type, which has some meaning in theory.

So, let's go.

C language embedded system programming considerations software architecture articles

The "plan" of the module division is the meaning of planning, which means how to reasonably divide a large software into a series of functionally independent parts to complete the system.

Module division

The "plan" of the module division is the meaning of planning, which means how to reasonably divide a large software into a series of functionally independent parts to complete the system. As a structured programming language, C language mainly depends on functions in the division of modules (division according to function becomes an error in object-oriented design, Newton's law encounters relativity), C language modular programming needs to be understood as follows concept:

(1) The module is a combination of a .c file and a .h file. The header file (.h) is a declaration for the interface of the module;

(2) The external functions and data provided by a module to other modules must be declared with the extern keyword in the file in .h;

(3) The functions and global variables in the module must be declared with the staTIc keyword at the beginning of the .c file;

(4) Never define variables in the .h file! The difference between defining a variable and declaring a variable is that the definition creates an operation for memory allocation, which is the concept of the assembly phase; the declaration simply tells the module containing the declaration to look for external functions and variables from other modules during the connection phase. Such as:

/*module1.h*/

Int a = 5; /* defines int a */ in the .h file of module 1.

/*module1 .c*/

#include "module1.h" /* Contains module #'s .h file in module 1*/

/*module2 .c*/

#i nclude “module1.h” /* contains the .h file of module 1 in module 2*/

/*module3 .c*/

#i nclude "module1.h" /* Contains module #'s .h file in module 3*/

The result of the above procedure is that the integer variables a are defined in modules 1, 2, and 3, and a corresponds to different address units in different modules. This kind of program is never needed in the world. The correct way is:

/*module1.h*/

Extern int a; /* declare int a */ in the .h file of module 1.

/*module1 .c*/

#i nclude “module1.h” /* contains module 1.h file* in module 1.

Int a = 5; /* defines int a * in the .c file of module 1.

/*module2 .c*/

#i nclude “module1.h” /* contains the .h file of module 1 in module 2*/

/*module3 .c*/

#i nclude "module1.h" /* Contains module #'s .h file in module 3*/

Thus, if modules 1, 2, and 3 operate a, they correspond to the same memory unit.

An embedded system usually consists of two types of modules:

(1) a hardware driver module, one specific hardware corresponding to one module;

(2) The software function module, the division of the module should meet the requirements of low coupling and high cohesion.

Multitasking or single task

The so-called "single task system" means that the system cannot support multi-task concurrent operations and performs a task in a macroscopic manner. Multitasking systems can perform multiple tasks "simultaneously" in a macroscopic parallel (possibly serially).

Multitasking concurrent execution typically relies on a multitasking operating system (OS). The core of a multitasking OS is the system scheduler, which uses task control blocks (TCBs) to manage task scheduling functions. The TCB includes information such as the current state of the task, priority, events or resources to wait, the start address of the task code, and the initial stack pointer. The scheduler uses this information when the task is activated. In addition, the TCB is also used to store the "context" of the task. The context of a task is all the information to be saved when an ongoing task is stopped. Usually, the context is the current state of the computer, that is, the contents of each register. When a task switch occurs, the context of the currently running task is stored in the TCB, and the context of the task to be executed is taken from its TCB and placed in each register.

Typical examples of embedded multitasking OS are Vxworks, ucLinux, and so on. Embedded OS is not an unreachable altar. We can use a less than 1000 lines of code to implement a simpler OS kernel for the 80186 processor. The author is preparing for this work, hoping to contribute to everyone. .

Whether to choose multi-tasking or single-tasking, depends on whether the software system is huge. For example, most mobile phone programs are multi-tasking, but some PHS protocol stacks are single-tasking. Without an operating system, their main programs take turns to call the processing programs of various software modules to simulate a multi-tasking environment.

Single task program typical architecture

(1) Execute from the specified address at the time of CPU reset;

(2) Jump to the assembly code startup to execute;

(3) Jump to the main program of the user main program, complete in main:

a. Initially test each hardware device;

b. Initialize each software module;

c. Enter the infinite loop (infinite loop), call the processing function of each module

The user main program and the processing functions of each module are completed in C language. The user's main program finally enters an infinite loop, and its preferred solution is:

While(1)

{

}

Some programmers write this:

For(;;)

{

}

This grammar does not exactly express the meaning of the code. We can't see anything from for(;;), only to understand that for(;;) means unconditional loop in C language to understand its meaning.

Here are a few "famous" infinite loops:

(1) The operating system is an infinite loop;

(2) WIN32 program is an infinite loop;

(3) Embedded system software is an infinite loop;

(4) The thread processing function of a multithreaded program is an infinite loop.

You may argue and say out loud: "Everything is not absolute. 2, 3, and 4 are not infinite loops." Yes, you are right, but you can't get flowers and applause. In fact, this is a point that doesn't make much sense, because the world never needs a WIN32 program that calls the OS to kill it after processing a few messages. It doesn't need an embedded system that just breaks itself when it starts RUN. You don't need to start somehow to get rid of your own thread. Sometimes it is not convenience but trouble to make too strict. Never seen, the five-layer TCP/IP protocol stack goes beyond the rigorous ISO/OSI seven-layer protocol stack to become the de facto standard?

There are often netizens discussing:

Printf("%d,%d",++i,i++); /* What is the output? */

c = a+++b; /* c=? */

And so on. In the face of these problems, we can only express our heartfelt feelings: there are still many meaningful things in the world waiting for us to digest the food we eat.

In fact, embedded systems have to run to the end of the world.

Interrupt service routine

Interrupts are an important part of an embedded system, but do not include interrupts in Standard C. Many compiler developers have added support for interrupts on standard C, providing new keywords for signing interrupt service routines (ISRs), similar to __interrupt, #program interrupt, and so on. When a function is defined as an ISR, the compiler automatically adds the interrupt on-site stacking and popping code required by the interrupt service routine for the function.

The interrupt service routine needs to meet the following requirements:

(1) cannot return a value;

(2) The parameters cannot be passed to the ISR;

(3) The ISR should be as short as possible;

(4) The printf(char * lpFormatString,...) function introduces reentrancy and performance issues and cannot be used in ISR.

In the development of a project, we designed a queue. In the interrupt service program, we just add the interrupt type to the queue. In the infinite loop of the main program, we continuously scan the interrupt queue for interrupts. The first interrupt type is processed accordingly.

/* Store interrupted queues*/

Typedef struct tagIntQueue

{

Int intType; /* interrupt type */

Struct tagIntQueue *next;

}IntQueue;

IntQueue lpIntQueueHead;

__interrupt ISRexample ()

{

Int intType;

intType = GetSystemType();

QueueAddTail(lpIntQueueHead, intType);/* Add a new interrupt at the end of the queue*/

}

Determine if there is an interruption in the main program loop:

While(1)

{

If( !IsIntQueueEmpty() )

{

intType = GetFirsTInt();

Is switch(intType) /* very similar to the message parsing function of a WIN32 program? */

{

/* Yes, our interrupt type resolution is very similar to message driver*/

Case xxx: /* We call it "interrupt drive"? */

...

Break;

Case xxx:

...

Break;

...

}

}

}

The interrupt service program designed as described above is small, and the actual work is performed by the main program.

The "plan" of the module division is the meaning of planning, which means how to reasonably divide a large software into a series of functionally independent parts to complete the system.

Hardware driver module

A hardware driver module should usually include the following functions:

(1) Interrupt service program ISR

(2) Hardware initialization

a. Modify the register, set the hardware parameters (such as the UART should set its baud rate, AD / DA equipment should set its sampling rate, etc.);

b. Write the interrupt service routine entry address to the interrupt vector table:

/* Set the interrupt vector table */

m_myPtr = make_far_pointer(0l); /* returns a void far pointer void far * */

m_myPtr += ITYPE_UART; /* ITYPE_UART: uart interrupt service routine */

/* Offset from the first address of the interrupt vector table */

*m_myPtr = &UART _Isr; /* UART _Isr: Interrupt Service Routine for UART*/

(3) Set the CPU control line for the hardware

a. If the control line can be used for PIO (programmable I/O) and control signals, set the corresponding register in the CPU as a control signal;

b. Set the interrupt mask bit for the device inside the CPU and set the interrupt mode (level trigger or edge trigger).

(4) Provide a series of operational interface functions for the device. For example, for an LCD, the driver module should provide functions such as drawing pixels, drawing lines, drawing a matrix, and displaying a character dot matrix; for a real clock, the driver module needs to provide functions such as acquisition time and set time.

Object-oriented C

In the object-oriented language, the concept of a class appears. A class is a collection of specific operations on a particular piece of data. A class contains two categories: data and operations. The struct in C is just a collection of data. We can use function pointers to simulate a struct as a "class" containing data and operations. The following C program simulates one of the simplest "classes":

#ifndef C_Class

#define C_Class struct

#endif

C_Class A

{

C_Class A *A_this; /* this pointer*/

Void (*Foo)(C_Class A *A_this); /* Behavior: function pointer */

Int a; /* data*/

Int b;

};

We can use C language to simulate three object-oriented features: encapsulation, inheritance and polymorphism, but more often, we just need to encapsulate data and behavior to solve the problem of software structure confusion. The purpose of C-simulating object-oriented thinking is not to simulate the behavior itself, but to solve the problem that the overall framework structure of the program is scattered, data and functions are disconnected when programming in C language in some cases. We will see examples of this in the following chapters.

to sum up

This article introduces the knowledge of embedded system programming software architecture, including module partitioning, multitasking or single task selection, single task program typical architecture, interrupt service program, hardware driver module design, etc., which gives an embedded macroscopically. The main elements of the system software.

Remember: the software structure is the soul of the software! The confusing procedures are extremely difficult, and debugging, testing, maintenance, and upgrading are extremely difficult.

C language embedded system programming considerations memory operation

In the programming of embedded systems, it is often required to read and write content in a specific memory unit, and assemble corresponding MOV instructions, and the programming languages ​​other than C/C++ have no direct access to absolute addresses.

Data pointer

In the programming of embedded systems, it is often required to read and write content in a specific memory unit, and assemble corresponding MOV instructions, and programming languages ​​other than C/C++ have basically no direct access to absolute addresses. In the actual debugging of the embedded system, the C-language pointer has the ability to read and write the contents of the absolute address unit. Direct manipulation of memory with pointers occurs in the following situations:

(1) An I/O chip is located in the storage space of the CPU instead of the I/O space, and the register corresponds to a specific address;

(2) The two CPUs communicate with each other in a dual port RAM, and the CPU needs to write content in a specific unit (called a mail box) of the dual port RAM to generate an interrupt in the other CPU;

(3) Read Chinese characters and English fonts burned in specific units of ROM or FLASH.

for example:

Unsigned char *p = (unsigned char *)0xF000FF00;

*p=11;

The meaning of the above program is to write 11 at the absolute address 0xF0000 + 0xFF00 (80186 uses a 16-bit segment address and a 16-bit offset address).

When using an absolute address pointer, be aware that the result of the pointer incrementing and decrementing operation depends on the data type pointed to by the pointer. The result of p++ in the above example is p = 0xF000FF01, if p points to int, ie:

Int *p = (int *)0xF000FF00;

The result of p++ (or ++p) is equivalent to: p = p+sizeof(int), and the result of p-(or -p) is p = p-sizeof(int).

Similarly, if executed:

Long int *p = (long int *)0xF000FF00;

Then the result of p++ (or ++p) is equivalent to: p = p+sizeof(long int) , and the result of p-(or -p) is p = p-sizeof(long int).

Remember: the CPU is addressed in bytes, and the C language pointer is incremented and decremented by the length of the data type pointed to. Understanding this is important for manipulating memory directly with pointers.

Function pointer

First understand the following three questions:

(1) The function name in C language directly corresponds to the address of the instruction code generated by the function in memory, so the function name can be directly assigned to the pointer to the function;

(2) The calling function is actually equivalent to "transfer instruction + parameter transfer processing + return position onto the stack". Essentially, the most core operation is to assign the first address of the target code generated by the function to the PC register of the CPU;

(3) Because the essence of the function call is to jump to the code of an address unit to execute, so you can "call" a function entity that does not exist at all, halo? Please look down:

Please take out any of the university's "Microcomputer Principles" textbooks that you can get. The book says that after the 186 CPU starts, it jumps to the absolute address 0xFFFF0 (corresponding to the C language pointer is 0xF000FFF0, 0xF000 is the segment address, 0xFFF0 is the segment Offset) Execution, please see the following code:

Typedef void (*lp) ( ); /* Defines a parameterless, no return type */

/* function pointer type */

Lp lpReset = (lp)0xF000FFF0; /* Define a function pointer to */

/* The position of the first instruction executed after the CPU is started*/

lpReset(); /* Call function */

In the above program, we didn't see any function entity at all, but we executed a function call like this: lpReset(), which actually acts as a "soft restart" and jumps to the first time after the CPU starts. The location of the instruction to be executed.

Remember: the function has no it, only the instruction set ear; you can call a function without a function body, essentially just start an instruction with another address!

Array vs dynamic application

Dynamic memory applications in embedded systems have stricter requirements than general system programming. This is because the memory space of embedded systems is often very limited. Inadvertent memory leaks can quickly lead to system crashes.

So be sure to ensure that your malloc and free pairs appear, if you write a program like this:

Char * (void)

{

Char *p;

p = (char *)malloc(...);

If(p==NULL)

...;

... /* A series of operations for p*/

Return p;

}

Call () somewhere, use the memory after the dynamic application, and then free it, as follows:

Char *q = ();

...

Free(q);

The above code is obviously unreasonable because it violates the principle that malloc and free appear in pairs, that is, the principle of "who applies, who releases it". Failure to satisfy this principle will result in increased code coupling because the user needs to know the internal details when calling the function!

The correct way is to apply for memory at the call and pass in the function as follows:

Char *p=malloc(...);

If(p==NULL)

...;

(p);

...

Free(p);

p=NULL;

The function receives the parameter p as follows:

Void (char *p)

{

... /* A series of operations for p*/

}

Basically, dynamic application memory can be replaced with a larger array. For programming novices, I recommend you try to use arrays! Embedded systems can receive flaws with a broad mind, and cannot be "Haina" errors. After all, Guo Jing, who has worked hard in the most stupid way, has surpassed Yang Kang, who is clever and intelligent, but who is politically wrong and takes the counter-revolutionary path.

Give the principle:

(1) Use arrays as much as possible, and arrays cannot be accessed across borders (the truth is one step beyond the delay, and the array is gloriously completes a chaotic embedded system).

(2) If you use the dynamic application, you must judge whether the application is successful after the application, and malloc and free should appear in pairs!

In the programming of embedded systems, it is often required to read and write content in a specific memory unit, and assemble corresponding MOV instructions, and the programming languages ​​other than C/C++ have no direct access to absolute addresses.

Keyword const

Const means "read only". The function of distinguishing the following code is very important, and it is also an old growth sigh. If you don't know the difference between them, and you have been crawling in the program world for many years, you can only say that this is a sad thing:

Const int a;

Int const a;

Const int *a;

Int * const a;

Int const * a const;

(1) The role of the keyword const is to convey very useful information to those who read your code. For example, adding a const keyword in front of a function's formal parameters means that this parameter will not be modified in the body of the function and is an "input parameter." When there are multiple formal parameters, the caller of the function can clearly distinguish which input parameters and which are the possible output parameters by means of whether there is a const keyword before the parameter.

(2) Proper use of the keyword const allows the compiler to naturally protect parameters that are not desired to be changed, preventing them from being modified by unintentional code, thus reducing the occurrence of bugs.

Const contains a richer meaning in the C++ language, but in the C language only means: "can only read ordinary variables", can be called "variables that cannot be changed" (this statement seems to be very vocal, but The most accurate expression of the essence of const in C language), the constants required in the compilation phase can still only be defined by #define macro! Therefore, the following procedures are illegal in C:

Const int SIZE = 10;

Char a[SIZE]; /* Illegal: Variables cannot be used in the compile phase */

Keyword volaTIle

The C compiler optimizes the code written by the user, such as the following code:

Int a,b,c;

a = inWord(0x100); /*Read the contents of the I/O space 0x100 port and store it in the a variable*/

b = a;

a = inWord (0x100); / * Read the contents of the I/O space 0x100 port again and store it in the a variable */

c = a;

Most likely it is optimized by the compiler to:

Int a,b,c;

a = inWord(0x100); /*Read the contents of the I/O space 0x100 port and store it in the a variable*/

b = a;

c = a;

However, such an optimization result may cause an error. If the contents of the 0/100 port of the I/O space are written by the other program after the first read operation, the content read by the second read operation is different from the first time. The values ​​of b and c should be different. Adding the volaTIle keyword before the definition of the variable a prevents similar optimizations of the compiler. The correct approach is:

Volatile int a;

The volatile variable may be used in the following situations:

(1) The hardware registers of the parallel device (such as the status register, the code in the example belongs to this class);

(2) Non-automatic variables (ie global variables) that are accessed in an interrupt service routine;

(3) Variables shared by several tasks in a multi-threaded application.

CPU word length is inconsistent with memory bit width

As mentioned in the background article, this paper deliberately chooses a memory chip that is inconsistent with the CPU word length, in order to solve the discussion in this section and solve the problem that the CPU word length and the memory bit width are inconsistent. The word length of the 80186 is 16, and the bit width of the NVRAM is 8. In this case, we need to provide NVRAM with an interface for reading and writing bytes and words, as follows:

Typedef unsigned char BYTE;

Typedef unsigned int WORD;

/* Function: Read bytes in NVRAM

* Parameter: wOffset, the offset of the read position from the NVRAM base address

* Return: the byte value read

*/

Extern BYTE ReadByteNVRAM(WORD wOffset)

{

LPBYTE lpAddr = (BYTE*)(NVRAM + wOffset * 2); /* Why is the offset ×2? */

Return *lpAddr;

}

/* Function: Read the word in NVRAM

* Parameter: wOffset, the offset of the read position from the NVRAM base address

* Return: the word read

*/

Extern WORD ReadWordNVRAM(WORD wOffset)

{

WORD wTmp = 0;

LPBYTE lpAddr;

/* Read high byte */

lpAddr = (BYTE*)(NVRAM + wOffset * 2); /* Why is the offset ×2? */

wTmp += (*lpAddr)*256;

/* Read low byte */

lpAddr = (BYTE*)(NVRAM + (wOffset +1) * 2); /* Why is the offset ×2? */

wTmp += *lpAddr;

Return wTmp;

}

/* Function: Write a byte to NVRAM

*Parameter: wOffset, offset of the write position from the NVRAM base address

* byData, the byte to be written

*/

Extern void WriteByteNVRAM(WORD wOffset, BYTE byData)

{

...

}

/* Function: Write a word to NVRAM*/

*Parameter: wOffset, offset of the write position from the NVRAM base address

* wData, the word to be written

*/

Extern void WriteWordNVRAM(WORD wOffset, WORD wData)

{

...

}

Zigong asks: Why is the Why offset multiplied by 2?

Sub-æ›°: The interconnection between 16-bit 80186 and 8-bit NVRAM can only be connected to A0 by address line A1, and A0 of the CPU itself is not connected to NVRAM. Therefore, the address of NVRAM can only be an even address, so advance each time in units of 0x10!

Zigong asks again: So why is the address line A0 of the 80186 not connected to the A0 of the NVRAM?

Zi Yan: Please see the "Microcomputer Principles" in "The Analects of Science", which tells the story of the saints about computer composition.

to sum up

This article mainly describes the related skills of memory operation in embedded system C programming. Mastering and understanding the relevant knowledge about data pointers, function pointers, dynamic application memory, const and volatile keywords is a basic requirement for an excellent C language programmer. When we have mastered the above techniques, we have learned 99% of the C language, because the essence of the C language is reflected in the memory operation.

The reason why we use C language for programming in embedded systems is 99% because of its powerful memory operation!

If you love programming, please love C language;

If you love C language, please love the pointer;

If you love the pointer, please love the pointer of the pointer!

C language embedded system programming notes screen operation

The problem to be solved now is that the embedded system often does not use a complete Chinese character library, and often only needs to provide a limited number of Chinese characters for the necessary display functions.

Chinese character processing

The problem to be solved now is that the embedded system often does not use a complete Chinese character library, and often only needs to provide a limited number of Chinese characters for the necessary display functions. For example, it is not necessary to provide a function of displaying "email" on the LCD of a microwave oven; a "short message" does not need to be displayed on an LCD of an air conditioner providing a Chinese character display function, and the like. However, a mobile phone and PHS usually need to include a more complete Chinese character library.

If the included Chinese character library is relatively complete, then it is quite simple to calculate the offset of the Chinese character font in the library from the inner code: the Chinese character library is arranged in the order of the location, the previous byte is the area code of the Chinese character, the latter The byte is the bit number of the word. Each zone records 94 Chinese characters, and the bit number is the position of the word in the zone. Therefore, the formula for calculating the specific position of Chinese characters in the Chinese character library is: 94* (area number-1) + bit number-1. The decrease of 1 is because the array starts with 0 and the area code starts with 1. Simply multiply the number of bytes occupied by a Chinese character font, that is: (94* (area number-1) + bit number -1) * A Chinese character font occupies the number of bytes, taking the 16*16 dot matrix font as an example. The calculation formula is: (94 * (area number - 1) + (bit number - 1)) * 32. The 32-byte information from the position in the Chinese character library records the font information of the word.

For systems that contain a more complete Chinese character library, we can calculate the position of the font using the above rules. But what if you only provide a small amount of Chinese characters? For example, tens to hundreds? The best practice is:

Define the macro:

# define EX_FONT_CHAR()

# define EX_FONT_UNICODE_VAL() (),

# define EX_FONT_ANSI_VAL() (),

Define the structure:

Typedef struct _wide_unicode_font16x16

{

WORD ; /* inner code */

BYTE data[32]; /* font dot matrix*/

}Unicode;

#define CHINESE_CHAR_NUM ... /* Number of Chinese characters*/

Array of font storage:

Unicode chinese[CHINESE_CHAR_NUM] =

{

{

EX_FONT_CHAR("业")

EX_FONT_UNICODE_VAL(0x4e1a)

{0x04, 0x40, 0x04, 0x40, 0x04, 0x40, 0x04, 0x44, 0x44, 0x46, 0x24, 0x4c, 0x24, 0x48, 0x14, 0x50, 0x1c, 0x50, 0x14, 0x60, 0x04, 0x40, 0x04, 0x40, 0x04 , 0x44, 0xff, 0xfe, 0x00, 0x00, 0x00, 0x00}

},

{

EX_FONT_CHAR ("Medium")

EX_FONT_UNICODE_VAL(0x4e2d)

{0x01, 0x00, 0x01, 0x00, 0x21, 0x08, 0x3f, 0xfc, 0x21, 0x08, 0x21, 0x08, 0x21, 0x08, 0x21, 0x08, 0x21, 0x08,

0x3f, 0xf8, 0x21, 0x08, 0x01, 0x00, 0x01, 0x00, 0x01, 0x00, 0x01, 0x00, 0x01, 0x00}

},

{

EX_FONT_CHAR ("cloud")

EX_FONT_UNICODE_VAL (0x4e91)

{0x00, 0x00, 0x00, 0x30, 0x3f, 0xf8, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0c, 0xff, 0xfe, 0x03, 0x00, 0x07, 0x00,

0x06, 0x40, 0x0c, 0x20, 0x18, 0x10, 0x31, 0xf8, 0x7f, 0x0c, 0x20, 0x08, 0x00, 0x00}

},

{

EX_FONT_CHAR ("piece")

EX_FONT_UNICODE_VAL(0x4ef6)

{0x10, 0x40, 0x1a, 0x40, 0x13, 0x40, 0x32, 0x40, 0x23, 0xfc, 0x64, 0x40, 0xa4, 0x40, 0x28, 0x40, 0x2f, 0xfe,

0x20, 0x40, 0x20, 0x40, 0x20, 0x40, 0x20, 0x40, 0x20, 0x40, 0x20, 0x40, 0x20, 0x40}

}

}

To display a specific Chinese character, you only need to find the internal code from the array and the same required Chinese character code to get the font. If the preceding Chinese characters are arranged in the order of the inner code size in the array, the binary font search method can be used to find the font of the Chinese character more efficiently.

This is a very effective way to organize a small Chinese character library, which can ensure that the program has a good structure.

System time display

The time of the system can be read from the NVRAM. The system typically reads the current time every second and displays it on the LCD with the second interrupt generated by the NVRAM. There is an efficiency issue with regard to the display of time. Because time has its particularity, it is only one minute change in 60 seconds, and one hour change in 60 minutes. If we re-refresh the time on the screen every time, it will waste a lot of system. time.

A better approach is to store the hours, minutes, and seconds as static variables in the time display function, and update the display only when its content changes.

Extern void DisplayTime(...)

{

Static BYTE byHour,byMinute,bySecond;

BYTE byNewHour, byNewMinute, byNewSecond;

byNewHour = GetSysHour();

byNewMinute = GetSysMinute();

byNewSecond = GetSysSecond();

If(byNewHour!= byHour)

{

... /* shows hours*/

byHour = byNewHour;

}

If(byNewMinute!= byMinute)

{

... /* shows minutes*/

byMinute = byNewMinute;

}

If(byNewSecond!= bySecond)

{

... /* shows seconds*/

bySecond = byNewSecond;

}

}

This example can also be used as a proof of the power of the static keyword in C language. Of course, in the C++ language, static has a more powerful power, which makes some data and functions separate from the "object" and becomes part of the "class". It is this feature that has made countless excellent designs of software.

Animated display

The animation is indifferent, there is no such thing as it, and the still picture goes a lot, and it becomes an animation. As time changes, displaying different still images on the screen is the essence of animation. Therefore, in order to display animation on the LCD of an embedded system, it is necessary to use a timer. A world without hardware or software timers is unimaginable:

(1) 没有定时器,一个操作系统将无法进行时间片的轮转,于是无法进行多任务的调度,于是便不再成其为一个多任务操作系统;

(2) 没有定时器,一个多媒体播放软件将无法运作,因为它不知道何时应该切换到下一帧画面;

(3) 没有定时器,一个网络协议将无法运转,因为其无法获知何时包传输超时并重传之,无法在特定的时间完成特定的任务。

因此,没有定时器将意味着没有操作系统、没有网络、没有多媒体,这将是怎样的黑暗?所以,合理并灵活地使用各种定时器,是对一个软件人的最基本需求!

在80186为主芯片的嵌入式系统中,我们需要借助硬件定时器的中断来作为软件定时器,在中断发生后变更画面的显示内容。在时间显示“xx:xx”中让冒号交替有无,每次秒中断发生后,需调用ShowDot:

void ShowDot()

{

static BOOL bShowDot = TRUE; /* 再一次领略static关键字的威力*/

if(bShowDot)

{

showChar(':',xPos,yPos);

}

Else

{

showChar(' ',xPos,yPos);

}

bShowDot = ! bShowDot;

}

菜单操作

无数人为之绞尽脑汁的问题终于出现了,在这一节里,我们将看到,在C语言中哪怕用到一丁点的面向对象思想,软件结构将会有何等的改观!

要求以键盘上的“← →”键切换菜单焦点,当用户在焦点处于某菜单时,若敲击键盘上的OK、CANCEL键则调用该焦点菜单对应之处理函数。我曾经傻傻地这样做着:

/* 按下OK键*/

void onOkKey()

{

/* 判断在什么焦点菜单上按下Ok键,调用相应处理函数*/

Switch(currentFocus)

{

case MENU1:

menu1OnOk();

Break;

case MENU2:

menu2OnOk();

Break;

...

}

}

/* 按下Cancel键*/

void onCancelKey()

{

/* 判断在什么焦点菜单上按下Cancel键,调用相应处理函数*/

Switch(currentFocus)

{

case MENU1:

menu1OnCancel();

Break;

case MENU2:

menu2OnCancel();

Break;

...

}

}

终于有一天,我这样做了:

/* 将菜单的属性和操作“封装”在一起*/

typedef struct tagSysMenu

{

char *text; /* 菜单的文本*/

BYTE xPos; /* 菜单在LCD上的x坐标*/

BYTE yPos; /* 菜单在LCD上的y坐标*/

void (*onOkFun)(); /* 在该菜单上按下ok键的处理函数指针*/

void (*onCancelFun)(); /* 在该菜单上按下cancel键的处理函数指针*/

}SysMenu, *LPSysMenu;

当我定义菜单时,只需要这样:

static SysMenu menuï¼»MENU_NUMï¼½ =

{

{

“menu1”, 0, 48, menu1OnOk, menu1OnCancel

}

,

{

“ menu2”, 7, 48, menu2OnOk, menu2OnCancel

}

,

{

“ menu3”, 7, 48, menu3OnOk, menu3OnCancel

}

,

{

“ menu4”, 7, 48, menu4OnOk, menu4OnCancel

}

...

};

OK键和CANCEL键的处理变成:

/* 按下OK键*/

void onOkKey()

{

menu[currentFocusMenu].onOkFun();

}

/* 按下Cancel键*/

void onCancelKey()

{

menu[currentFocusMenu].onCancelFun();

}

程序被大大简化了,也开始具有很好的可扩展性!我们仅仅利用了面向对象中的封装思想,就让程序结构清晰,其结果是几乎可以在无需修改程序的情况下在系统中添加更多的菜单,而系统的按键处理函数保持不变。

面向对象,真神了!

模拟MessageBox函数

MessageBox函数,这个Windows编程中的超级猛料,不知道是多少入门者第一次用到的函数。还记得我们第一次在Windows中利用MessageBox输出“Hello,World!”对话框时新奇的感觉吗?无法统计,这个世界上究竟有多少程序员学习Windows编程是从MessageBox(“Hello,World!”,…)开始的。在我本科的学校,广泛流传着一个词汇,叫做“'Hello,World'级程序员”,意指入门级程序员,但似乎“'Hello,World'级”这个说法更搞笑而形象。

嵌入式系统中没有给我们提供MessageBox,但是鉴于其功能强大,我们需要模拟之,一个模拟的MessageBox函数为:

/******************************************

/* 函数名称: MessageBox

/* 功能说明: 弹出式对话框,显示提醒用户的信息

/* 参数说明: lpStr --- 提醒用户的字符串输出信息

/* TYPE --- 输出格式(ID_OK = 0, ID_OKCANCEL = 1)

/* 返回值: 返回对话框接收的键值,只有两种KEY_OK, KEY_CANCEL

/******************************************

typedef enum TYPE { ID_OK,ID_OKCANCEL }MSG_TYPE;

extern BYTE MessageBox(LPBYTE lpStr, BYTE TYPE)

{

BYTE key = -1;

ClearScreen(); /* 清除屏幕*/

DisplayString(xPos,yPos,lpStr,TRUE); /* 显示字符串*/

/* 根据对话框类型决定是否显示确定、取消*/

switch (TYPE)

{

case ID_OK:

DisplayString(13,yPos+High+1, “ 确定”, 0);

Break;

case ID_OKCANCEL:

DisplayString(8, yPos+High+1, “ 确定”, 0);

DisplayString(17,yPos+High+1, “ 取消”, 0);

Break;

Default:

Break;

}

DrawRect(0, 0, 239, yPos+High+16+4); /* 绘制外框*/

/* MessageBox是模式对话框,阻塞运行,等待按键*/

while( (key != KEY_OK) || (key != KEY_CANCEL) )

{

key = getSysKey();

}

/* 返回按键类型*/

if(key== KEY_OK)

{

return ID_OK;

}

Else

{

return ID_CANCEL;

}

}

上述函数与我们平素在VC++等中使用的MessageBox是何等的神似啊?实现这个函数,你会看到它在嵌入式系统中的妙用是无穷的。

to sum up

本篇是本系列文章中技巧性最深的一篇,它提供了嵌入式系统屏幕显示方面一些很巧妙的处理方法,灵活使用它们,我们将不再被LCD上凌乱不堪的显示内容所困扰。

屏幕乃嵌入式系统生存之重要辅助,面目可憎之显示将另用户逃之夭夭。屏幕编程若处理不好,将是软件中最不系统、最混乱的部分,笔者曾深受其害。

C语言嵌入式系统编程注意事项之键盘操作

处理功能键

让我们来看看WIN32编程中用到的“窗口”概念,当消息(message)被发送给不同窗口的时候,该窗口的消息处理函数(是一个callback函数)最终被调用,而在该窗口的消息处理函数中,又根据消息的类型调用了该窗口中的对应处理函数。通过这种方式,WIN32有效的组织了不同的窗口,并处理不同窗口情况下的消息。

我们从中学习到的就是:

(1)将不同的画面类比为WIN32中不同的窗口,将窗口中的各种元素(菜单、按钮等)包含在窗口之中;

(2)给各个画面提供一个功能键“消息”处理函数,该函数接收按键信息为参数;

(3)在各画面的功能键“消息”处理函数中,判断按键类型和当前焦点元素,并调用对应元素的按键处理函数。

/* 将窗口元素、消息处理函数封装在窗口中*/

struct windows

{

BYTE currentFocus;

ELEMENT elementï¼»ELEMENT_NUMï¼½;

void (*messageFun) (BYTE key);

...

};

/* 消息处理函数*/

void message(BYTE key)

{

BYTE i = 0;

/* 获得焦点元素*/

while ( (element .ID!= currentFocus)&& (i 《 ELEMENT_NUM) )

{

i++;

}

/* “消息映射” */

if(i 《 ELEMENT_NUM)

{

Switch(key)

{

case OK:

element.OnOk();

Break;

...

}

}

}

在窗口的消息处理函数中调用相应元素按键函数的过程类似于“消息映射”,这是我们从WIN32编程中学习到的。编程到了一个境界,很多东西都是相通的了。其它地方的思想可以拿过来为我所用,是为编程中的“拿来主义”。

在这个例子中,如果我们还想玩得更大一点,我们可以借鉴MFC中处理MESSAGE_MAP的方法,我们也可以学习MFC定义几个精妙的宏来实现“消息映射”。

处理数字键

用户输入数字时是一位一位输入的,每一位的输入都对应着屏幕上的一个显示位置(x坐标,y坐标)。此外,程序还需要记录该位置输入的值,所以有效组织用户数字输入的最佳方式是定义一个结构体,将坐标和数值捆绑在一起:

/* 用户数字输入结构体*/

typedef struct tagInputNum

{

BYTE byNum; /* 接收用户输入赋值*/

BYTE xPos; /* 数字输入在屏幕上的显示位置x坐标*/

BYTE yPos; /* 数字输入在屏幕上的显示位置y坐标*/

}InputNum, *LPInputNum;

那么接收用户输入就可以定义一个结构体数组,用数组中的各位组成一个完整的数字:

InputNum inputElement[NUM_LENGTH]; /* 接收用户数字输入的数组*/

/* 数字按键处理函数*/

extern void onNumKey(BYTE num)

{

if(num==0|| num==1) /* 只接收二进制输入*/

{

/* 在屏幕上显示用户输入*/

DrawText(inputElement[currentElementInputPlace].xPos, inputElement[currentElementInputPlace].yPos, “%1d”, num);

/* 将输入赋值给数组元素*/

inputElementï¼»currentElementInputPlaceï¼½.byNum = num;

/* 焦点及光标右移*/

moveToRight();

}

}

将数字每一位输入的坐标和输入值捆绑后,在数字键处理函数中就可以较有结构的组织程序,使程序显得很紧凑。

整理用户输入

继续第2节的例子,在第2节的onNumKey函数中,只是获取了数字的每一位,因而我们需要将其转化为有效数据,譬如要转化为有效的XXX数据,其方法是:

/* 从2进制数据位转化为有效数据:XXX */

void convertToXXX()

{

BYTE i;

XXX = 0;

for (i = 0; i 《 NUM_LENGTH; i++)

{

XXX += inputElement.byNum*power(2, NUM_LENGTH - i - 1);

}

}

反之,我们也可能需要在屏幕上显示那些有效的数据位,因为我们也需要能够反向转化:

/* 从有效数据转化为2进制数据位:XXX */

void convertFromXXX()

{

BYTE i;

XXX = 0;

for (i = 0; i 《 NUM_LENGTH; i++)

{

inputElement.byNum = XXX / power(2, NUM_LENGTH - i - 1) % 2;

}

}

当然在上面的例子中,因为数据是2进制的,用power函数不是很好的选择,直接用“《《 》》”移位操作效率更高,我们仅是为了说明问题的方便。试想,如果用户输入是十进制的,power函数或许是唯一的选择了。

to sum up

本篇给出了键盘操作所涉及的各个方面:功能键处理、数字键处理及用户输入整理,基本上提供了一个全套的按键处理方案。对于功能键处理方法,将LCD屏幕与Windows窗口进行类比,提出了较新颖地解决屏幕、键盘繁杂交互问题的方案。

计算机学的许多知识都具有相通性,因而,不断追赶时髦技术而忽略基本功的做法是徒劳无意的。我们最多需要“精通”三种语言(精通,一个在如今的求职简历里泛滥成灾的词语),最佳拍档是汇编、C、C++(或JAVA),很显然,如果你“精通”了这三种语言,其它语言你应该是可以很快“熟悉”的,否则你就没有“精通”它们。

C语言嵌入式系统编程注意事项之性能优化

在C语言中,宏是产生内嵌代码的唯一方法。对于嵌入式系统而言,为了能达到性能要求,宏是一种很好的代替函数的方法

使用宏定义

在C语言中,宏是产生内嵌代码的唯一方法。对于嵌入式系统而言,为了能达到性能要求,宏是一种很好的代替函数的方法。

写一个“标准”宏MIN ,这个宏输入两个参数并返回较小的一个:

错误做法:

#define MIN(A,B) ( A 《= B ? A : B )

正确做法:

#define MIN(A,B) ((A)《= (B) ? (A) : (B) )

对于宏,我们需要知道三点:

(1)宏定义“像”函数;

(2)宏定义不是函数,因而需要括上所有“参数”;

(3)宏定义可能产生副作用。

下面的代码:

least = MIN(*p++, b);

将被替换为:

( (*p++) 《= (b) ?(*p++):(b) )

发生的事情无法预料。

因而不要给宏定义传入有副作用的“参数”。

使用寄存器变量

当对一个变量频繁被读写时,需要反复访问内存,从而花费大量的存取时间。为此,C语言提供了一种变量,即寄存器变量。这种变量存放在CPU的寄存器中,使用时,不需要访问内存,而直接从寄存器中读写,从而提高效率。寄存器变量的说明符是register。对于循环次数较多的循环控制变量及循环体内反复使用的变量均可定义为寄存器变量,而循环计数是应用寄存器变量的最好候选者。

(1) 只有局部自动变量和形参才可以定义为寄存器变量。因为寄存器变量属于动态存储方式,凡需要采用静态存储方式的量都不能定义为寄存器变量,包括:模块间全局变量、模块内全局变量、局部static变量;

(2) register是一个“建议”型关键字,意指程序建议该变量放在寄存器中,但最终该变量可能因为条件不满足并未成为寄存器变量,而是被放在了存储器中,但编译器中并不报错(在C++语言中有另一个“建议”型关键字:inline)。

下面是一个采用寄存器变量的例子:

/* 求1+2+3+….+n的值*/

WORD Addition(BYTE n)

{

register i,s=0;

for(i=1;i《=n;i++)

{

s=s+i;

}

Return s;

}

本程序循环n次,i和s都被频繁使用,因此可定义为寄存器变量。

内嵌汇编

程序中对时间要求苛刻的部分可以用内嵌汇编来重写,以带来速度上的显著提高。但是,开发和测试汇编代码是一件辛苦的工作,它将花费更长的时间,因而要慎重选择要用汇编的部分。

在程序中,存在一个80-20原则,即20%的程序消耗了80%的运行时间,因而我们要改进效率,最主要是考虑改进那20%的代码。

嵌入式C程序中主要使用在线汇编,即在C程序中直接插入_asm{ }内嵌汇编语句:

/* 把两个输入参数的值相加,结果存放到另外一个全局变量中*/

int result;

void Add(long a, long *b)

{

_asm

{

MOV AX, a

MOV BX, b

ADD AX, [BX]

MOV result, AX

}

}

利用硬件特性

首先要明白CPU对各种存储器的访问速度,基本上是:

CPU内部RAM》外部同步RAM》外部异步RAM》FLASH/ROM

对于程序代码,已经被烧录在FLASH或ROM中,我们可以让CPU直接从其中读取代码执行,但通常这不是一个好办法,我们最好在系统启动后将FLASH或ROM中的目标代码拷贝入RAM中后再执行以提高取指令速度;

对于UART等设备,其内部有一定容量的接收BUFFER,我们应尽量在BUFFER被占满后再向CPU提出中断。例如计算机终端在向目标机通过RS-232传递数据时,不宜设置UART只接收到一个BYTE就向CPU提中断,从而无谓浪费中断处理时间;

如果对某设备能采取DMA方式读取,就采用DMA读取,DMA读取方式在读取目标中包含的存储信息较大时效率较高,其数据传输的基本单位是块,而所传输的数据是从设备直接送入内存的(或者相反)。DMA方式较之中断驱动方式,减少了CPU 对外设的干预,进一步提高了CPU与外设的并行操作程度。

活用位操作

使用C语言的位操作可以减少除法和取模的运算。在计算机程序中数据的位是可以操作的最小数据单位,理论上可以用“位运算”来完成所有的运算和操作,因而,灵活的位操作可以有效地提高程序运行的效率。 Examples are as follows:

/* 方法1 */

Int i,j;

i = 879 / 16;

j = 562 % 32;

/* 方法2 */

Int i,j;

i = 879 》》 4;

j = 562 - (562 》》 5 《《 5);

对于以2的指数次方为“*”、“/”或“%”因子的数学运算,转化为移位运算“《《 》》”通常可以提高算法效率。因为乘除运算指令周期通常比移位运算大。

C语言位运算除了可以提高运算效率外,在嵌入式系统的编程中,它的另一个最典型的应用,而且十分广泛地正在被使用着的是位间的与(&)、或(|)、非(~)操作,这跟嵌入式系统的编程特点有很大关系。我们通常要对硬件寄存器进行位设置,譬如,我们通过将AM186ER型80186处理器的中断屏蔽控制寄存器的第低6位设置为0(开中断2),最通用的做法是:

#define INT_I2_MASK 0x0040

wTemp = inword(INT_MASK);

outword(INT_MASK, wTemp &~INT_I2_MASK);

而将该位设置为1的做法是:

#define INT_I2_MASK 0x0040

wTemp = inword(INT_MASK);

outword(INT_MASK, wTemp | INT_I2_MASK);

判断该位是否为1的做法是:

#define INT_I2_MASK 0x0040

wTemp = inword(INT_MASK);

if(wTemp & INT_I2_MASK)

{

… /* 该位为1 */

}

上述方法在嵌入式系统的编程中是非常常见的,我们需要牢固掌握。

to sum up

在性能优化方面永远注意80-20准备,不要优化程序中开销不大的那80%,这是劳而无功的。

宏定义是C语言中实现类似函数功能而又不具函数调用和返回开销的较好方法,但宏在本质上不是函数,因而要防止宏展开后出现不可预料的结果,对宏的定义和使用要慎而处之。很遗憾,标准C至今没有包括C++中inline函数的功能,inline函数兼具无调用开销和安全的优点。

使用寄存器变量、内嵌汇编和活用位操作也是提高程序效率的有效方法。

除了编程上的技巧外,为提高系统的运行效率,我们通常也需要最大可能地利用各种硬件设备自身的特点来减小其运转开销,例如减小中断次数、利用DMA传输方式等。


POWER BREADBOARD

Power Breadboard,Breadboard Power Supply,Breadboard Power Supply Module,Breadboard Power Module

Cixi Zhongyi Electronics Factory , https://www.cx-zhongyi.com

Posted on