The C Programmers Guide to Verilog
To determine the output signal, the hardware can simply compare the contents of the period and pulse_width registers to the output of a running counter it keeps.Next, choose the ports for the PWM, most of which are already determined based on the bus architecture. Table 2 has a brief description of the signals for a generic memory-mapped PWM. Note that a popular naming convention for active low signals is to add an "_n" to the signal name, which are fairly common for control signals. The signals write_n, and clr_n in Table 2 are active low (falling-edge triggered) signals.
Table 2: Ports for PWM
| Signal Name | Direction | Description |
| clk | Input | System clock |
| write_data[31:0] | Input | Write data (for registers in register map) |
| cs | Input | Chip select |
| write_n | Input | Write enable, active low |
| addr | Input | Address (to access registers in register map) |
| clr_n | Input | Clear, active low |
| read_data[31:0] | Output | Read data output |
| pwm_out | Output | PWM output |
Now that we have defined the interface of the hardware module, we can start writing the Verilog code. An example implementation is shown in Listing 3.
Listing 3: PWM hardware implementation in Verilog
| module pwm (clk, write_data, cs, write_n, addr, clr_n, read_data, pwm_out); | ||||||
| // port declarations | ||||||
|
input input [31:0] input input input input output [31:0] output |
clk; write_data; cs; write_n; addr; clr_n; read_data; pwm_out; |
|||||
| // signal declarations | ||||||
|
reg [31:0] reg [31:0] reg [31:0] reg reg [31:0] wire |
period; pulse_width; counter; off; read_data; period_en, pulse_width_en; // write enables |
|||||
|
// Define contents of period and pulse_width registers // including write access for these registers always @(posedge clk or negedge clr_n) begin |
||||||
| if (clr_n == 0) | ||||||
| begin |
period <= 32'h 00000000; pulse_width <= 32'h 00000000; |
|||||
| end | ||||||
| else | ||||||
| begin | ||||||
| if (period_en) | ||||||
| period <= write_data[31:0]; | ||||||
| else | ||||||
| period <= period; | ||||||
| if (pulse_width_en) | ||||||
| pulse_width <= write_data[31:0]; | ||||||
| else | ||||||
| pulse_width <= pulse_width; | ||||||
| end | ||||||
| end | ||||||
| // read access for period and pulse_width registers always @(addr or period or pulse_width) | ||||||
| if (addr == 0) | ||||||
| read_data = period; | ||||||
| else | ||||||
|
read_data = pulse_width; |
||||||
|
// counter which continually counts up to period always @(posedge clk or negedge clr_n) begin |
||||||
| if (clr_n == 0) | ||||||
| counter <= 0; | ||||||
|
else if (counter >= period - 1) // count from 0 to (period-1) |
||||||
| counter <= 0; | ||||||
| else | ||||||
| counter <= counter + 1; | ||||||
|
end // Turns output on while counter is less than pulse_width; otherwise // turns output off. // !off is connected to PWM output always @(posedge clk or negedge clr_n) begin |
||||||
| if (clr_n == 0) | ||||||
| off <= 0; | ||||||
| else | ||||||
| if (counter >= pulse_width) | ||||||
| off <= 1; | ||||||
| else | ||||||
| if (counter == 0) | ||||||
| off <= 0; | ||||||
| else | ||||||
| off <= off; | ||||||
| end | ||||||
|
// write enable signals for writing to period and pulse_width registers assign period_en = cs & !write_n & !addr; assign pulse_width_en = cs & !write_n & addr;
// PWM output |
||||||
| endmodule | ||||||
The first signals are the port declarations, which were described in Table 2. After the port declarations come the internal signal declarations. The memory-mapped registers that make up the software interface to control the PWM are declared reg. The code allows for only 32-bit accesses to these memory-mapped registers. If you need 8-bit or 16-bit access, then you would split the registers into four 8-bit registers and add logic for byte enable signals. The Verilog code to implement this is straightforward. All the signals with assigned values in the always blocks are also declared reg. The signals declared wire are the write enables for the registers period and pulse_width. These signals are assigned values using continuous assignment statements.
The rest of the listing contains the actual code. There are four always blocks and several assignment statements at the end. Each always block describes the behavior for one signal or a group of signals that have the same basic behavior (in other words, use the same control logic). This is a clean way of writing Verilog code that keeps the code readable and less prone to errors. All of the always blocks have reset logic that sets the signal(s) to 0 when the clr_n signal is asserted (set to 0). While not strictly necessary, this is a good design practice so that every signal has a known value upon reset.
The first always block describes the behavior of the registers in the register map. The value of the write_data register is written into the period or pulse_width register if the appropriate enable signal is asserted. That is the only way to change the values of either register. The write enable signals are defined in the continuous assignment statements at the bottom of the file. The write enables for the period and pulse_width registers are asserted when the main write enable signal and the chip select signal are asserted; the addr bit should be set to 0 for period and 1 for pulse_width.
The second always block defines reading the registers in the register map. The period register will be at the base address of the peripheral, and the pulse_width register will be at the next 32-bit word.
The third and fourth always blocks work together to determine the output of the PWM. The third always block implements a counter that continually counts up to the value in the period register, resets to 0, and begins counting again. The fourth always block compares this counter value to the pulse_width register. While the counter value is less than the pulse_width, the PWM output is kept high; otherwise it's set low.
One thing to keep in mind is that every signal must have a defined value under all conditions. This goes back to one of the fundamental behaviors of hardwareit's always running. For example, in the last always block (the one that describes the off signal) the last line of code assigns off to itself. This may seem strange at first, but without this line, the value of off would be undefined for that case. An easy way to keep track of this is to make sure that every time a signal is assigned a value in an if statement, it is assigned a value in the corresponding else statement as well.
Software access
Now that the hardware is complete, the PWM can be controlled via software using the registers in the register map. You can use a simple data structure along with a pointer to connect to the registers in the PWM.
typedef volatile struct
{
uint32_t period;
uint32_t pulse_width;
} PWM;
For example, the PWM could be hooked to an LED. A variable called pLED of type PWM * could be initialized to point to the PWM base address. This abstracts the hardware into a data structure. Writing to pLED->period will set or change the period. Writing to pLED->pulse_width will change the duty cycle and cause the brightness of the LED to increase or decrease. If a blinking LED is desired, the period need only be lengthened, so that the human eye perceives the on and off periods as distinct.
The Verilog PWM implementation shown in Listing 3 was tested as a peripheral for Altera's Nios processor system and accessed via software using a C struct like the one I previously described. Altera's SOPC Builder creates macros that facilitate performing co-simulation in ModelSim, a hardware simulator from Mentor Graphics. Using the ModelSim simulator, the behavior of the PWM signals, along with the rest of the system's signals, can be observed while the system is executing C code.
Listing 4 shows the C code that was used to generate the waveform in Figure 2. The waveform shows the behavior of the pertinent PWM signals. The C code writes to the PWM registers to create a PWM output with a period of five cycles and a pulse width of four. Notice that at the beginning of the waveform, the cs and wr_n signals are asserted twice since we're writing to both the period and pulse_width registers. (The address signal is low when writing to the period register and high when writing to the pulse_width register.)
Listing 4 Test software used to produce waveforms in Figure 2
void
main(void)
{
PWM * const pLED = ...
pLED->period = 5;
pLED->pulse_width = 4;
asm("nop");
asm("nop");
asm("nop");
pLED->pulse_width = 2;
}

Figure 2: Waveform for software-controlled PWM hardware
After the new values have been written to the registers, the pwm_output signal begins to reflect the change. Then, just to add some delay so we can see the output, some NOP instructions are executed by the C code. Finally, the pulse width is changed to two cycles, and the PWM waveform changes accordingly while still having a period of five cycles.
Best of both worlds
Part of architecting an embedded system is partitioning the system into hardware and software modules to take advantage of the benefits of each. As development tools evolve, interchanging software and hardware modules is becoming more transparent to the designer.
Once you understand the concepts discussed in this article, you'll have the knowledge to develop hardware on an FPGA that can be hooked up as a memory-mapped peripheral in a microprocessor system and interfaced by simply writing software. Because certain algorithms run much faster in hardware, converting an algorithm from software to hardware may greatly increase system performance. Known as hardware acceleration, the ability to do this is key to using configurable processors implemented effectively in programmable logic. At long last, even a software engineer has the power to improve system performance and efficiency through hardware acceleration.
Lara Simsic is an applications engineer at Altera. She has developed embedded hardware and software for five years and has an EE degree from the University of Dayton. Contact her at lsimsic@altera.com.


Loading comments... Write a comment